seedproject-web/agents/scripts/lib/util.mjs
Carlos Arias 2c969c0753 feat: content-pipeline/ → agents/ — formalize the agent system in the seed
Adopt the agents/ architecture proven on medellin.co (reference impl):

- Move the content engine to a top-level agents/ dir: orchestrators, prompts,
  config, run.sh, admin console, shared libs. All content-pipeline literals
  repointed (config paths, scripts, admin, LLM-facing prompts/image.md string,
  configure.mjs, new-site.sh, astroagent tokenFile, .gitignore runtime block).
- Every script carries a parseable @agent-manifest header: name, title, class
  (content|operational|runtime|plumbing), trigger, model, prompts, skills (MCP),
  tools, reads/writes tables. 5 content agents + 3 plumbing scripts.
- New agents/catalog.mjs generates the catalog from the headers:
  agents/AGENTS.md (human, grouped by class) + agents/agents.json (machine
  manifest — a clone diffs it against a source to find missing tools/tables/MCP
  before running). configure.mjs regenerates the catalog on every identity
  stamp. No DB table, no watcher.
- config.json gains paths.stateDir/newsDir; publish-tick, write-daily, and
  news-radar read them instead of hardcoding.
- Full cut: content-pipeline/ deleted (the seed has no live crons, so no
  hybrid period needed). Docs updated (AGENTS.md structure + pipeline section,
  README paths).

Clones migrating from content-pipeline/: see medellin.co's
.memory/handoffs/agents-directory-migration.md for the cutover playbook
(one cron set active at a time; migrate drafts/state after repointing cron).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMQeUnUrAeexcZ7P2Hxa6G
2026-07-11 17:09:15 -05:00

84 lines
2.4 KiB
JavaScript

import { readFileSync } from "node:fs";
export function slugify(s) {
return String(s)
.toLowerCase()
.normalize("NFKD")
.replace(/[̀-ͯ]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80);
}
export function titleCase(slug) {
return String(slug)
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
}
export function todayInTz(tz) {
const fmt = new Intl.DateTimeFormat("en-CA", {
timeZone: tz,
year: "numeric",
month: "2-digit",
day: "2-digit",
});
return fmt.format(new Date()); // en-CA => YYYY-MM-DD
}
/** ISO timestamp with Medellín's fixed -05:00 offset, e.g. 2026-06-28T14:37:09-05:00. */
export function isoInBogota(date = new Date()) {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: "America/Bogota",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
}).formatToParts(date);
const g = (t) => parts.find((p) => p.type === t).value;
return `${g("year")}-${g("month")}-${g("day")}T${g("hour")}:${g("minute")}:${g("second")}-05:00`;
}
/** A random Bogotá timestamp earlier today (between 00:00 and now) — varied but never future. */
export function randomTimestampTodaySoFar(tz = "America/Bogota") {
const today = todayInTz(tz);
const startMs = Date.parse(`${today}T00:00:00-05:00`);
const nowMs = Date.now();
const r = startMs + Math.floor(Math.random() * Math.max(1, nowMs - startMs));
return isoInBogota(new Date(r));
}
export function addDays(isoDate, n) {
const d = new Date(isoDate + "T00:00:00Z");
d.setUTCDate(d.getUTCDate() + n);
return d.toISOString().slice(0, 10);
}
export function loadJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
/** Extract the YAML frontmatter block (between the first two --- lines) as raw text. */
export function readFrontmatter(mdx) {
const m = mdx.match(/^---\n([\s\S]*?)\n---/);
return m ? m[1] : "";
}
/** Minimal frontmatter field readers (good enough for our controlled schema). */
export function fmString(fm, key) {
const m = fm.match(new RegExp(`^${key}:\\s*"?([^"\\n]+?)"?\\s*$`, "m"));
return m ? m[1].trim() : null;
}
export function fmArray(fm, key) {
const m = fm.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m"));
if (!m) return [];
return m[1]
.split(",")
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
.filter(Boolean);
}