diff --git a/agents/console/agents/in-page-console.mjs b/agents/console/agents/in-page-console.mjs index 3e916c9..3f6de38 100644 --- a/agents/console/agents/in-page-console.mjs +++ b/agents/console/agents/in-page-console.mjs @@ -5,15 +5,16 @@ * revert. Owns: /run, /stream (SSE), /publish, /rebuild, /discard. */ +import { site } from "../site.mjs"; + let H; // the host, set in register() function buildPrompt({ message, page, selections }) { const parts = []; parts.push( - "You are the editing agent for the Carlos Arias website (an Astro + Tailwind v4 static site).", + `You are the editing agent for the ${site.name} website (an Astro + Tailwind v4 static site).`, "Make ONLY the change the operator asks for. Keep everything on-brand:", - "the brand guide is brand/BRAND.md and the design tokens are in app/src/styles.css", - "(sumi-e — ink on washi paper, one vermillion seal, restraint, near-square radii).", + "the brand guide is brand/BRAND.md and the design tokens are in app/src/styles.css — read them and match the existing components.", "Do NOT run builds or git commands — the console builds and publishes for you.", "", ); diff --git a/agents/console/agents/project-builder.mjs b/agents/console/agents/project-builder.mjs index 96d3ad6..6cae604 100644 --- a/agents/console/agents/project-builder.mjs +++ b/agents/console/agents/project-builder.mjs @@ -4,6 +4,8 @@ * it via the PHP API); never mutates the repo. Owns: /draft, /build-project. */ +import { site } from "../site.mjs"; + let H; const MEDIA_RE = /^\/media\/[\w.-]+$/; @@ -17,7 +19,7 @@ function stripFence(t) { // Turn a rough draft + prompt into polished project copy. No tools. async function draftCopy({ name, category, draft, prompt }) { const ask = [ - "You are writing copy for Carlos Arias's portfolio (carlosarias.co) — an Agentic AI & Automation Engineer who builds for law firms and service businesses.", + `You are writing copy for ${site.name}'s portfolio (${site.host})${site.author.role ? " — " + site.author.role : ""}.`, "Voice: precise, understated, confident. No hype, no buzzwords, no exclamation marks. Write a project case study.", "", `Project name: ${name}`, @@ -48,10 +50,10 @@ async function buildProject({ name, category, draft, prompt, images, instagram } const imageLines = paths.length ? paths.map((p, i) => ` ${i + 1}. ${p}`).join("\n") : ""; const ask = [ - "You are the Web Designer for Carlos Arias's portfolio (carlosarias.co) — an Agentic AI & Automation Engineer who builds for law firms and service businesses.", + `You are the Web Designer for ${site.name}'s portfolio (${site.host})${site.author.role ? " — " + site.author.role : ""}.`, "Your job: turn the brief below into a COMPLETE project case study for a fixed, on-brand page template. You author STRUCTURED CONTENT, not HTML or layout.", "", - "Brand (from brand/BRAND.md — honor it): sumi-e, ink on washi paper, a single vermillion seal, restraint. Voice is precise, understated, confident — no hype, no buzzwords, no exclamation marks. Never describe colours or layout; the template owns all styling.", + "Brand: honor the brand guide in brand/BRAND.md. Voice is precise, understated, confident — no hype, no buzzwords, no exclamation marks. Never describe colours or layout; the template owns all styling.", "", "The page auto-renders these sections from the fields you return — populate the ones the brief supports, leave the rest empty:", "- header: title + one-line lede", @@ -81,7 +83,7 @@ async function buildProject({ name, category, draft, prompt, images, instagram } '- "body": the Markdown case study.', '- "kind": short project type, e.g. "Website" or "SaaS / Media" (or "").', '- "period": a timeline label, e.g. "2024 — present" (or "").', - '- "role": Carlos\'s role on the project (or "").', + `- "role": ${site.author.name}'s role on the project (or "").`, '- "categories": array of 1-3 short strings.', '- "stack": array of technology strings.', '- "skills": array of {"group": string, "items": [string, ...]}.', diff --git a/agents/console/agents/qa.mjs b/agents/console/agents/qa.mjs index dc00f28..0dedfbc 100644 --- a/agents/console/agents/qa.mjs +++ b/agents/console/agents/qa.mjs @@ -7,15 +7,13 @@ */ import { writeFileSync, rmSync } from "node:fs"; +import { site } from "../site.mjs"; let H; -const QA_BASE = "https://carlosarias.co"; -const QA_UA = "Mozilla/5.0 (compatible; CarlosAriasQA/1.0; +https://carlosarias.co)"; -const QA_STATIC_ROUTES = [ - "/", "/about", "/services", "/services/website-design", - "/projects", "/blog", "/contact", "/changelog", "/resume", "/faq", -]; +const QA_BASE = site.url; +const QA_UA = `Mozilla/5.0 (compatible; AstroAgentQA/1.0; +${site.url})`; +const QA_STATIC_ROUTES = site.qaRoutes; const QA_AUTOFIX = (process.env.QA_AUTOFIX ?? "1") !== "0"; const QA_HEARTBEAT_MIN = Number(process.env.QA_HEARTBEAT_MIN || 60); let qaRunning = false; @@ -91,7 +89,7 @@ async function runQa() { if (!title) add("seo", "warning", path, "Missing .", `${path} has no <title>. Add a page-specific title via its BaseLayout props.`); else { if (!titles.has(title)) titles.set(title, []); titles.get(title).push(path); } if (!desc) add("seo", "warning", path, "Missing meta description.", `${path} has no meta description. Add a page-specific description via its BaseLayout props.`); - if (canon) { try { const h = new URL(canon).host; if (h && h !== "carlosarias.co") add("seo", "error", path, `Canonical points to ${h}.`, `${path} canonical points to ${h} instead of carlosarias.co. Fix the site URL / canonical.`); } catch {} } + if (canon) { try { const h = new URL(canon).host; if (h && h !== site.host) add("seo", "error", path, `Canonical points to ${h}.`, `${path} canonical points to ${h} instead of ${site.host}. Fix the site URL / canonical.`); } catch {} } if (!/<html[^>]+lang=/i.test(html)) add("a11y", "warning", path, "<html> has no lang attribute.", `${path} <html> tag has no lang attribute. Add lang="en".`); for (const href of qaGrabAll(/<a\b[^>]*\bhref=["']([^"']+)["']/gi, html)) { @@ -139,13 +137,13 @@ async function runQa() { const cHeaders = { "content-type": "application/json", origin: QA_BASE, referer: QA_BASE + "/contact" }; const hp = await probe(QA_BASE + "/api/contact/submit", { method: "POST", readBody: true, headers: cHeaders, - body: JSON.stringify({ name: "QA Bot", email: "qa@carlosarias.co", subject: "other", message: "QA honeypot probe — please ignore.", company: "qa-honeypot" }), + body: JSON.stringify({ name: "QA Bot", email: site.qaEmail, subject: "other", message: "QA honeypot probe — please ignore.", company: "qa-honeypot" }), }); if (hp.status !== 200) add("form", "error", "/api/contact/submit", `Contact honeypot probe returned ${hp.status || hp.error} (expected 200).`, `POST /api/contact/submit returned ${hp.status || hp.error} instead of 200 for a probe. The contact form endpoint may be broken — check api/public/controllers/contact.php.`); const val = await probe(QA_BASE + "/api/contact/submit", { method: "POST", readBody: true, headers: cHeaders, - body: JSON.stringify({ name: "QA", email: "qa@carlosarias.co", subject: "other", message: "hi" }), + body: JSON.stringify({ name: "QA", email: site.qaEmail, subject: "other", message: "hi" }), }); if (val.status !== 422) add("form", "warning", "/api/contact/submit", `Validation probe returned ${val.status || val.error} (expected 422 for a too-short message).`, `POST /api/contact/submit did not reject an invalid submission (got ${val.status || val.error}, expected 422). Server-side validation may be off.`); @@ -177,7 +175,7 @@ async function qaTriage(findings, counts) { const lines = top.map((f) => `- [${f.severity}] ${f.check_type} ${f.url}: ${f.detail}`).join("\n") || "(no issues found)"; const fallback = `${counts.error} error(s) and ${counts.warning} warning(s) across ${counts.pages} pages.`; const ask = [ - "You are the QA agent for carlosarias.co. A crawler just tested the live site. Consult your `qa` skill.", + `You are the QA agent for ${site.host}. A crawler just tested the live site. Consult your qa skill.`, `Counts: ${counts.error} errors, ${counts.warning} warnings across ${counts.pages} pages.`, "Top findings:", lines, "", diff --git a/agents/console/agents/web-designer.mjs b/agents/console/agents/web-designer.mjs index bb91a96..06f8cc5 100644 --- a/agents/console/agents/web-designer.mjs +++ b/agents/console/agents/web-designer.mjs @@ -5,6 +5,8 @@ * reverts itself. Owns: /tasks/run. Exposes host.drainTasks for QA autofix. */ +import { site } from "../site.mjs"; + let H; let draining = false; @@ -39,18 +41,18 @@ function buildDesignPrompt(task) { const p = []; p.push( - "You are the Web Designer for the Carlos Arias website (carlosarias.co) — an Astro + Tailwind v4 static site.", + `You are the Web Designer for the ${site.name} website (${site.host}) — an Astro + Tailwind v4 static site.`, "You are a real designer: make considered, on-brand design decisions, not just literal edits.", "", - "Before you start, consult your design skills and APPLY them: read .claude/skills/brand/SKILL.md and .claude/skills/ui-ux/SKILL.md.", - "The brand is sumi-e — ink on washi paper, a single vermillion seal, restraint, near-square 2px radii. Reuse the existing .ca-* classes and --ca-* tokens in app/src/styles.css and match nearby components; do not invent new one-off styles or add a second accent colour.", + "Before you start, consult your design skills and APPLY them: read .claude/skills/brand/SKILL.md and .claude/skills/ui-ux/SKILL.md, plus the brand guide in brand/BRAND.md.", + "Reuse the existing utility classes and CSS tokens in app/src/styles.css and match nearby components; do not invent new one-off styles or add a second accent colour.", "Do NOT run builds or git — the console builds and publishes for you.", "", ); if (isNew) { p.push( `TASK: create a NEW page at /${slug}.`, - `- Create app/src/pages/${slug}.astro using BaseLayout and existing .ca-* section patterns (study app/src/pages/about.astro and services.astro for structure).`, + `- Create app/src/pages/${slug}.astro using BaseLayout and the existing section patterns (study an existing page under app/src/pages, e.g. about.astro, for structure).`, "- Register it in the nav: add it to the nav array in app/src/components/Header.astro AND the footer links in app/src/components/Footer.astro.", "", ); diff --git a/agents/console/host.mjs b/agents/console/host.mjs index 212f6a1..dfb6c47 100644 --- a/agents/console/host.mjs +++ b/agents/console/host.mjs @@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto"; import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync } from "node:fs"; import { dirname, resolve, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { site } from "./site.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); // agents/console const REPO = resolve(HERE, "..", ".."); // repo root @@ -25,9 +26,9 @@ const ENV_FILE = join(REPO, "agents", ".env"); const CLAUDE = "/usr/local/bin/claude"; const PHP = "/usr/bin/php"; const PORT = Number(process.env.ADMIN_PORT || 3011); -const AGENT_TOOLS = "Read Write Edit Glob Grep WebSearch"; -const DEFAULT_MODEL = "claude-sonnet-4-6"; -const GIT_SCOPE = ["app", "brand", "api/db", "api/cli"]; // paths the console may commit +const AGENT_TOOLS = site.tools; // from astroagent.config.json ai.tools +const DEFAULT_MODEL = site.model; // from astroagent.config.json ai.model +const GIT_SCOPE = ["app", "brand", "api/db", "api/cli"]; // SeedProject-conventional commit scope // ---- env (.env) -------------------------------------------------------------- function loadEnv() { @@ -53,7 +54,7 @@ function agentEnv() { const env = loadEnv(); return { ...process.env, - HOME: process.env.HOME || "/var/lib/carlos-arias-agent", + HOME: process.env.HOME || site.home, PATH: "/usr/local/bin:/usr/bin:/bin", CLAUDE_CODE_OAUTH_TOKEN: env.CLAUDE_CODE_OAUTH_TOKEN || "", }; diff --git a/agents/console/site.mjs b/agents/console/site.mjs new file mode 100644 index 0000000..827c556 --- /dev/null +++ b/agents/console/site.mjs @@ -0,0 +1,43 @@ +/** + * Site identity for the AstroAgent runtime — the ONLY place the framework reads + * who this deployment is. Everything site-specific (name, url, model, agent user, + * QA target) is derived here from site.config.json + astroagent.config.json, so + * host.mjs and the agent modules stay site-agnostic. Brand *rules* live in the + * per-site brand guide (brand/BRAND.md) and the `brand` skill, not here. + */ + +import { readFileSync } from "node:fs"; +import { dirname, resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const readJson = (p) => { try { return JSON.parse(readFileSync(join(REPO, p), "utf8")); } catch { return {}; } }; + +const sc = readJson("site.config.json"); +const aa = readJson("astroagent.config.json"); + +const url = String(sc.url || aa.url || "https://example.com").replace(/\/+$/, ""); +let host = url; try { host = new URL(url).host; } catch {} +const agentUser = aa.ai?.agentUser || "astroagent"; + +export const site = { + name: sc.name || "the site", + url, + host, + model: aa.ai?.model || "claude-sonnet-4-6", + tools: aa.ai?.tools || "Read Write Edit Glob Grep WebSearch", + agentUser, + home: `/var/lib/${agentUser}`, + author: { + name: sc.author?.name || sc.name || "the author", + role: sc.author?.jobTitle || "", + }, + audience: sc.audience || "", + qaEmail: `qa@${host}`, + // Static route seed for the QA crawler (it also discovers via links + sitemap + // + qa-routes.php). Site-derived; Phase 3 moves this to agents.settings.qa. + qaRoutes: [ + "/", "/about", "/services", "/services/website-design", + "/projects", "/blog", "/contact", "/changelog", "/resume", "/faq", + ], +};