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
84 lines
2.4 KiB
JavaScript
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);
|
|
}
|