fimo 2.8.0-staging.9 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/assets/skills/fimo/SKILL.md +0 -1
  2. package/assets/skills/fimo-cli/SKILL.md +6 -7
  3. package/assets/skills/fimo-cli/references/assets.md +2 -0
  4. package/assets/skills/fimo-cli/references/deploy.md +1 -1
  5. package/assets/skills/fimo-cli/references/init.md +1 -2
  6. package/assets/skills/fimo-cli/references/integrations/firecrawl.md +1 -41
  7. package/assets/skills/fimo-cli/references/integrations.md +2 -38
  8. package/assets/skills/fimo-cli/references/knowledge.md +1 -1
  9. package/assets/skills/fimo-cli/references/mcp.md +5 -6
  10. package/assets/skills/fimo-cli/references/preview.md +1 -1
  11. package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +0 -16
  12. package/assets/skills/fimo-cli/references/webhooks.md +0 -2
  13. package/assets/skills/fimo-code/SKILL.md +1 -0
  14. package/assets/skills/fimo-code/references/assets.md +5 -5
  15. package/assets/skills/fimo-code/references/design.md +1 -1
  16. package/assets/skills/fimo-code/references/ui.md +1 -1
  17. package/assets/skills/fimo-studio/SKILL.md +6 -9
  18. package/assets/skills/fimo-studio/references/ai-chat.md +9 -4
  19. package/assets/skills/fimo-studio/references/billing-and-credits.md +2 -4
  20. package/assets/skills/fimo-studio/references/command-palette.md +0 -2
  21. package/assets/skills/fimo-studio/references/connections.md +2 -2
  22. package/assets/skills/fimo-studio/references/project-creation.md +5 -3
  23. package/assets/skills/fimo-studio/references/webhooks.md +3 -3
  24. package/dist/cli/bundle.json +2 -2
  25. package/dist/cli/index.js +153 -162
  26. package/dist/runtime/paths/get-fimo-paths.js +2 -24
  27. package/dist/runtime/project-layout.d.ts +0 -1
  28. package/dist/runtime/project-layout.js +1 -5
  29. package/dist/runtime/shared/local-runtime-env.js +2 -14
  30. package/package.json +10 -10
  31. package/release.json +2 -2
  32. package/templates/astro/.fimo/config.json +0 -14
  33. package/templates/astro/AGENTS.md +2 -1
  34. package/templates/astro/_gitignore +3 -0
  35. package/templates/astro/package.json +3 -3
  36. package/templates/astro/src/layouts/Base.astro +0 -48
  37. package/templates/nextjs/AGENTS.md +1 -1
  38. package/templates/nextjs/_gitignore +3 -0
  39. package/templates/nextjs/package.json +5 -5
  40. package/templates/react-router/AGENTS.md +1 -1
  41. package/templates/react-router/_gitignore +3 -0
  42. package/templates/react-router/package.json +5 -5
  43. package/templates/sveltekit/AGENTS.md +1 -0
  44. package/templates/sveltekit/_gitignore +3 -0
  45. package/templates/sveltekit/package.json +3 -3
  46. package/templates/vite/AGENTS.md +2 -0
  47. package/templates/vite/_gitignore +3 -0
  48. package/templates/vite/package.json +5 -5
  49. package/assets/agent-templates/_default/GOAL.md +0 -21
  50. package/assets/agent-templates/_default/capabilities.yaml +0 -11
  51. package/assets/agent-templates/_default/config.yaml +0 -13
  52. package/assets/agent-templates/broken-link-sweeper/GOAL.md +0 -51
  53. package/assets/agent-templates/broken-link-sweeper/capabilities.yaml +0 -1
  54. package/assets/agent-templates/broken-link-sweeper/config.yaml +0 -13
  55. package/assets/agent-templates/content-translator/GOAL.md +0 -33
  56. package/assets/agent-templates/content-translator/capabilities.yaml +0 -5
  57. package/assets/agent-templates/content-translator/config.yaml +0 -12
  58. package/assets/agent-templates/content-translator/scripts/translate-entries.ts +0 -69
  59. package/assets/agent-templates/post-deploy-check/GOAL.md +0 -23
  60. package/assets/agent-templates/post-deploy-check/capabilities.yaml +0 -1
  61. package/assets/agent-templates/post-deploy-check/config.yaml +0 -10
  62. package/assets/agent-templates/post-deploy-check/scripts/check-logs.ts +0 -39
  63. package/assets/agent-templates/post-deploy-check/scripts/probe-routes.ts +0 -53
  64. package/assets/agent-templates/seo-audit/GOAL.md +0 -43
  65. package/assets/agent-templates/seo-audit/capabilities.yaml +0 -3
  66. package/assets/agent-templates/seo-audit/config.yaml +0 -14
  67. package/assets/agent-templates/seo-audit/references/report-template.md +0 -28
  68. package/assets/agent-templates/seo-audit/references/scoring-methodology.md +0 -30
  69. package/assets/agent-templates/seo-audit/scripts/analyze-html.ts +0 -82
  70. package/assets/agent-templates/seo-audit/scripts/fetch-page.ts +0 -39
  71. package/assets/agent-templates/seo-audit/scripts/list-routes.ts +0 -46
  72. package/assets/agent-templates/seo-audit/scripts/report.ts +0 -82
  73. package/assets/agent-templates/seo-audit/scripts/score.ts +0 -131
  74. package/assets/skills/fimo-cli/references/agents.md +0 -298
  75. package/assets/skills/fimo-studio/references/agents.md +0 -33
  76. package/assets/skills/fimo-studio/references/drawing-and-wireframing.md +0 -61
  77. package/templates/astro/src/components/ProjectOverview.astro +0 -45
  78. package/templates/astro/src/pages/[locale]/projects/[slug].astro +0 -38
  79. package/templates/astro/src/pages/[locale]/projects/index.astro +0 -19
  80. package/templates/astro/src/pages/projects/[slug].astro +0 -31
  81. package/templates/astro/src/pages/projects/index.astro +0 -13
  82. package/templates/astro/src/schemas/Project.json +0 -9
  83. package/templates/astro/src/schemas/Project.ts +0 -29
  84. package/templates/astro/src/schemas/Project.types.ts +0 -70
  85. package/templates/astro/src/schemas/SiteSettings.json +0 -10
  86. package/templates/astro/src/schemas/SiteSettings.ts +0 -31
  87. package/templates/astro/src/schemas/SiteSettings.types.ts +0 -69
  88. package/templates/react-router/src/pages/Privacy.tsx +0 -79
  89. package/templates/react-router/src/pages/Terms.tsx +0 -48
  90. package/templates/react-router/src/pages/legal/LegalPage.tsx +0 -42
@@ -1,23 +0,0 @@
1
- # Post-deploy check
2
-
3
- Run a quick health probe every time the project deploys, and post a
4
- summary back to the deploy log. Triggered automatically by the
5
- `deploy.succeeded` event.
6
-
7
- ## What to do
8
-
9
- 1. Read the routes the deploy declares as critical (`probe-routes.ts`).
10
- 2. For each one, fetch and assert that the response is 2xx and renders
11
- the expected key element (a known DOM selector).
12
- 3. Read the last 5 minutes of production logs (`check-logs.ts`) and
13
- surface any errors emitted during the deploy window.
14
- 4. Write a single-line summary to stdout that the deploy notification
15
- includes verbatim. Examples:
16
- - `✅ 12 routes pass · 0 errors`
17
- - `❌ /pricing failed (500) · 3 errors in last 5min`
18
-
19
- ## What NOT to do
20
-
21
- - Don't roll back the deploy — that's a separate human-in-the-loop
22
- decision; this agent only reports.
23
- - Don't write to CMS or files. Read-only probes plus log reads.
@@ -1 +0,0 @@
1
- - fimo:logs:read
@@ -1,10 +0,0 @@
1
- version: 1
2
- description: 'Probe routes and inspect logs after a deploy; report any issues to email recipients.'
3
- model: openai/gpt-5.6-luna
4
- data_sources:
5
- - prod
6
-
7
- triggers:
8
- - type: manual
9
- - type: webhook
10
- path: deploy-done
@@ -1,39 +0,0 @@
1
- /**
2
- * Tool: check-logs
3
- *
4
- * Query the project's production log stream for the last N minutes and
5
- * count errors. Used by the post-deploy-check summary line.
6
- *
7
- * Inputs (argv):
8
- * 1. Minutes to look back (default 5)
9
- *
10
- * Outputs (stdout JSON): { errors: number; first?: { ts: string; message: string } }
11
- */
12
-
13
- async function main(): Promise<void> {
14
- const minutes = Number(process.argv[2] ?? '5');
15
- const projectId = process.env.FIMO_PROJECT_ID ?? '';
16
- const env = process.env.FIMO_ENV ?? 'main';
17
- const base = process.env.FIMO_API_URL ?? 'http://localhost:3000';
18
-
19
- const since = new Date(Date.now() - minutes * 60_000).toISOString();
20
- const url = `${base}/api/management/projects/${projectId}/branches/${encodeURIComponent(env)}/logs?since=${encodeURIComponent(since)}&level=error`;
21
-
22
- try {
23
- const res = await fetch(url, { credentials: 'include' });
24
- if (!res.ok) {
25
- process.stdout.write(JSON.stringify({ errors: 0, note: `log endpoint returned ${res.status}` }));
26
- return;
27
- }
28
- const payload = (await res.json()) as { data?: { entries?: Array<{ ts: string; message: string }> } };
29
- const entries = payload.data?.entries ?? [];
30
- process.stdout.write(JSON.stringify({ errors: entries.length, first: entries[0] }));
31
- } catch (err) {
32
- process.stdout.write(JSON.stringify({ errors: 0, note: `log fetch failed: ${(err as Error).message}` }));
33
- }
34
- }
35
-
36
- main().catch((err) => {
37
- console.error(err);
38
- process.exit(1);
39
- });
@@ -1,53 +0,0 @@
1
- /**
2
- * Tool: probe-routes
3
- *
4
- * Fetch a list of critical routes and assert that each one responds
5
- * with a 2xx. Optionally checks that a known selector is present.
6
- *
7
- * Inputs (argv): JSON array of `{ url, selector? }`
8
- * Outputs (stdout JSON): { ok: number; failed: { url: string; status: number; reason: string }[] }
9
- */
10
-
11
- interface Probe {
12
- url: string;
13
- selector?: string;
14
- }
15
-
16
- interface Result {
17
- ok: number;
18
- failed: { url: string; status: number; reason: string }[];
19
- }
20
-
21
- async function probe(p: Probe): Promise<{ url: string; status: number; reason: string } | null> {
22
- const res = await fetch(p.url, { redirect: 'manual' });
23
- if (res.status < 200 || res.status >= 300) {
24
- return { url: p.url, status: res.status, reason: `non-2xx status ${res.status}` };
25
- }
26
- if (p.selector) {
27
- const body = await res.text();
28
- if (!body.includes(p.selector)) {
29
- return { url: p.url, status: res.status, reason: `expected selector "${p.selector}" not found` };
30
- }
31
- }
32
- return null;
33
- }
34
-
35
- async function main(): Promise<void> {
36
- const raw = process.argv[2] ?? '[]';
37
- const probes = JSON.parse(raw) as Probe[];
38
- const result: Result = { ok: 0, failed: [] };
39
- for (const p of probes) {
40
- const failure = await probe(p);
41
- if (failure) {
42
- result.failed.push(failure);
43
- } else {
44
- result.ok += 1;
45
- }
46
- }
47
- process.stdout.write(JSON.stringify(result));
48
- }
49
-
50
- main().catch((err) => {
51
- console.error(err);
52
- process.exit(1);
53
- });
@@ -1,43 +0,0 @@
1
- # SEO audit
2
-
3
- Audit the project's pages for SEO health and deliver the findings as your session report.
4
-
5
- ## Inputs
6
-
7
- - The project's published routes (via `scripts/list-routes.ts`)
8
- - Each page's HTML + response headers (via `scripts/fetch-page.ts`)
9
- - The current analytics data (read-only, via `fimo:analytics:read`)
10
-
11
- ## What to check (per page)
12
-
13
- 1. **Title tag** — present, 30–60 chars, unique across routes
14
- 2. **Meta description** — present, 50–160 chars, unique
15
- 3. **Open Graph** — `og:title`, `og:description`, `og:image`, `og:url`
16
- 4. **Schema.org** — at minimum `WebPage`; `Article` on blog posts;
17
- `BreadcrumbList` on nested routes
18
- 5. **Headings** — exactly one `h1`; `h2`/`h3` nest correctly
19
- 6. **Canonical URL** — `<link rel="canonical">` present and self-referential
20
- 7. **Robots / X-Robots-Tag** — not `noindex` on routes that should be indexed
21
- 8. **Sitemap** — every route is present in `/sitemap.xml`
22
-
23
- ## What to produce
24
-
25
- Deliver the audit as your final session report — Fimo renders your last
26
- message as the report, so you do not have (and do not need) write access.
27
- Structure it as:
28
-
29
- - An executive summary (score / 100, top 3 issues)
30
- - A per-page table with status chips (✅ / ⚠️ / ❌)
31
- - A "fix recipes" appendix linking each failure to the smallest change
32
- that would resolve it
33
- - A diff-friendly machine-readable footer (`<!-- agent-summary: ... -->`)
34
- so subsequent runs can show the trend
35
-
36
- ## What NOT to do
37
-
38
- - This audit is strictly read-only. Don't attempt to write files, create a
39
- report file, or commit — and don't mention capabilities or write access in
40
- the report itself. The report is your final message, nothing more.
41
- - Don't fetch external URLs other than the project's own domain.
42
- - Don't include traffic / conversion data in the report — it lives
43
- elsewhere in the Fimo dashboard.
@@ -1,3 +0,0 @@
1
- # Read-only audit — no writes to project files or CMS.
2
- - cms:read
3
- - fimo:analytics:read
@@ -1,14 +0,0 @@
1
- version: 1
2
- description: "Audit the project's pages for SEO health and write a markdown report."
3
- model: openai/gpt-5.6-luna
4
- # Read from main env. Switch to prod when running the audit against the
5
- # live site (M13 keeps data_sources here so M16's sandbox adapter can
6
- # bind the right Fimo API base URL).
7
- data_sources:
8
- - main
9
-
10
- # Manual-only in v1. Add a schedule trigger when you're ready to run
11
- # this nightly — make sure .fimo/config.json has
12
- # `agents.notifications.email_recipients` set before activating.
13
- triggers:
14
- - type: manual
@@ -1,28 +0,0 @@
1
- # Report template
2
-
3
- The agent renders `reports/seo-audit-<YYYY-MM-DD>.md` using the shape:
4
-
5
- ```markdown
6
- # SEO audit · 2026-05-26
7
-
8
- **Overall score:** 78 / 100
9
-
10
- ## Per-page summary
11
-
12
- | Path | Score | Status |
13
- | -------- | ----- | ------ |
14
- | `/` | 95 | OK |
15
- | `/about` | 70 | WARN |
16
-
17
- ## Findings
18
-
19
- ### `/about`
20
-
21
- - ⚠️ **title-length** — Title length 22 chars — recommend 30–60
22
- - ❌ **h1-present** — No <h1> on the page
23
-
24
- <!-- agent-summary: { "score": 78, "pages": 12 } -->
25
- ```
26
-
27
- The `<!-- agent-summary -->` comment is parsed by subsequent runs to
28
- plot a trend; keep its shape stable.
@@ -1,30 +0,0 @@
1
- # Scoring methodology
2
-
3
- A page starts at 100 points and loses:
4
-
5
- - **-15** per `fail` finding (missing title, missing `<h1>`, `noindex` on a public page, etc.)
6
- - **-5** per `warn` finding (length out of range, missing OG tag, no canonical, etc.)
7
-
8
- The overall project score is the arithmetic mean of per-page scores.
9
-
10
- ## Severity guide
11
-
12
- | Rule | Severity | Why |
13
- | ---------------- | --------- | -------------------------------------------------------------------- |
14
- | title-present | fail | Without a title, search snippets fall back to the URL — costly |
15
- | title-length | warn | Outside 30–60 chars, Google truncates or pads — still ranks |
16
- | description-\* | warn/fail | Less impact on rank but huge impact on CTR |
17
- | og-{title,image} | warn | Affects social shares only; no SEO rank impact |
18
- | h1-present | fail | Page has no semantic root — both screen-readers and search hurt |
19
- | h1-single | warn | Multiple h1s — accessibility lint, not a ranking penalty |
20
- | canonical | warn | Self-referential canonical is best practice; rarely required |
21
- | robots-noindex | fail | Page explicitly excluded from index — review and remove if a mistake |
22
- | schema-org | warn | Missing rich-result eligibility — won't hurt rank but loses CTR |
23
-
24
- ## How the agent should act on findings
25
-
26
- Don't write to project files in v1. Instead, the report's "fix recipes"
27
- appendix should link to the file the agent suspects is responsible (e.g.
28
- "fix this in `src/routes/[slug].tsx`'s `meta` export"). A follow-up
29
- agent — the one that resolves recipes into actual PRs — is the
30
- appropriate writer.
@@ -1,82 +0,0 @@
1
- /**
2
- * Tool: analyze-html
3
- *
4
- * Extract SEO signals from HTML — title, meta description, OG tags,
5
- * schema.org JSON-LD, heading structure, canonical, robots directive.
6
- * Returns a structured shape `score.ts` consumes.
7
- *
8
- * Inputs (stdin): HTML body
9
- * Outputs (stdout JSON): SEOSignals
10
- */
11
-
12
- interface SEOSignals {
13
- title: string | null;
14
- description: string | null;
15
- canonical: string | null;
16
- robots: string | null;
17
- openGraph: Record<string, string>;
18
- schemaOrg: unknown[];
19
- headings: { h1: string[]; h2: string[]; h3: string[] };
20
- }
21
-
22
- function getMatch(html: string, regex: RegExp): string | null {
23
- const m = html.match(regex);
24
- return m && m[1] ? m[1].trim() : null;
25
- }
26
-
27
- function getAllMatches(html: string, regex: RegExp): string[] {
28
- const out: string[] = [];
29
- for (const m of html.matchAll(regex)) {
30
- if (m[1]) {
31
- out.push(m[1].trim());
32
- }
33
- }
34
- return out;
35
- }
36
-
37
- async function readStdin(): Promise<string> {
38
- const chunks: Buffer[] = [];
39
- for await (const chunk of process.stdin) {
40
- chunks.push(Buffer.from(chunk));
41
- }
42
- return Buffer.concat(chunks).toString('utf8');
43
- }
44
-
45
- async function main(): Promise<void> {
46
- const html = await readStdin();
47
-
48
- const openGraph: Record<string, string> = {};
49
- for (const m of html.matchAll(/<meta\s+property=["']og:([^"']+)["']\s+content=["']([^"']*)["']/gi)) {
50
- openGraph[m[1]!] = m[2] ?? '';
51
- }
52
-
53
- const schemaOrg: unknown[] = [];
54
- for (const m of html.matchAll(/<script\s+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)) {
55
- try {
56
- schemaOrg.push(JSON.parse(m[1]!.trim()));
57
- } catch {
58
- // skip invalid JSON-LD blocks
59
- }
60
- }
61
-
62
- const signals: SEOSignals = {
63
- title: getMatch(html, /<title>([\s\S]*?)<\/title>/i),
64
- description: getMatch(html, /<meta\s+name=["']description["']\s+content=["']([^"']*)["']/i),
65
- canonical: getMatch(html, /<link\s+rel=["']canonical["']\s+href=["']([^"']*)["']/i),
66
- robots: getMatch(html, /<meta\s+name=["']robots["']\s+content=["']([^"']*)["']/i),
67
- openGraph,
68
- schemaOrg,
69
- headings: {
70
- h1: getAllMatches(html, /<h1[^>]*>([\s\S]*?)<\/h1>/gi).map((s) => s.replace(/<[^>]+>/g, '').trim()),
71
- h2: getAllMatches(html, /<h2[^>]*>([\s\S]*?)<\/h2>/gi).map((s) => s.replace(/<[^>]+>/g, '').trim()),
72
- h3: getAllMatches(html, /<h3[^>]*>([\s\S]*?)<\/h3>/gi).map((s) => s.replace(/<[^>]+>/g, '').trim()),
73
- },
74
- };
75
-
76
- process.stdout.write(JSON.stringify(signals, null, 2));
77
- }
78
-
79
- main().catch((err) => {
80
- console.error(err);
81
- process.exit(1);
82
- });
@@ -1,39 +0,0 @@
1
- /**
2
- * Tool: fetch-page
3
- *
4
- * Fetch HTML + headers for a given URL. Used by the audit to walk every
5
- * route returned by `list-routes` and feed the bytes to `analyze-html`.
6
- *
7
- * Inputs (argv):
8
- * 1. The URL to fetch (must be the project's own domain)
9
- *
10
- * Outputs (stdout JSON):
11
- * {
12
- * "url": "...",
13
- * "status": 200,
14
- * "headers": { "content-type": "text/html; charset=utf-8", ... },
15
- * "body": "<!doctype html>..."
16
- * }
17
- */
18
-
19
- async function main(): Promise<void> {
20
- const url = process.argv[2];
21
- if (!url) {
22
- console.error('Usage: fetch-page <url>');
23
- process.exit(1);
24
- }
25
-
26
- const res = await fetch(url, { redirect: 'manual' });
27
- const headers: Record<string, string> = {};
28
- res.headers.forEach((value, key) => {
29
- headers[key] = value;
30
- });
31
-
32
- const body = await res.text();
33
- process.stdout.write(JSON.stringify({ url, status: res.status, headers, body }, null, 2));
34
- }
35
-
36
- main().catch((err) => {
37
- console.error(err);
38
- process.exit(1);
39
- });
@@ -1,46 +0,0 @@
1
- /**
2
- * Tool: list-routes
3
- *
4
- * Enumerate every public route in the project via the Fimo management
5
- * API. Returns paths the audit walks one-by-one through `fetch-page`.
6
- *
7
- * Inputs (branch):
8
- * - FIMO_PROJECT_ID — provided by the runtime (M16 sandbox adapter)
9
- * - FIMO_ENV — provided by the runtime (legacy runtime selector)
10
- * - FIMO_API_URL — provided by the runtime
11
- *
12
- * Outputs (stdout JSON):
13
- * { "routes": [{ "path": "/", "label": "Home" }, ...] }
14
- */
15
-
16
- interface Route {
17
- path: string;
18
- label: string;
19
- }
20
-
21
- async function main(): Promise<void> {
22
- const projectId = process.env.FIMO_PROJECT_ID;
23
- const env = process.env.FIMO_ENV ?? 'main';
24
- const base = process.env.FIMO_API_URL ?? 'http://localhost:3000';
25
-
26
- if (!projectId) {
27
- console.error('FIMO_PROJECT_ID is required');
28
- process.exit(1);
29
- }
30
-
31
- const url = `${base}/api/management/projects/${projectId}/branches/${encodeURIComponent(env)}/routes`;
32
- const res = await fetch(url, { credentials: 'include' });
33
- if (!res.ok) {
34
- console.error(`Failed to list routes: ${res.status} ${res.statusText}`);
35
- process.exit(1);
36
- }
37
-
38
- const payload = (await res.json()) as { data?: { routes?: Record<string, Route> } };
39
- const routes = Object.values(payload.data?.routes ?? {});
40
- process.stdout.write(JSON.stringify({ routes }, null, 2));
41
- }
42
-
43
- main().catch((err) => {
44
- console.error(err);
45
- process.exit(1);
46
- });
@@ -1,82 +0,0 @@
1
- /**
2
- * Tool: report
3
- *
4
- * Render a markdown report from the per-page audit results.
5
- *
6
- * Inputs (stdin JSON): { score: number, pages: PageReport[] }
7
- * Outputs (stdout): markdown
8
- */
9
-
10
- interface Finding {
11
- rule: string;
12
- severity: 'ok' | 'warn' | 'fail';
13
- message: string;
14
- }
15
-
16
- interface PageReport {
17
- path: string;
18
- score: number;
19
- findings: Finding[];
20
- }
21
-
22
- function statusChip(score: number): string {
23
- if (score >= 80) {
24
- return 'OK';
25
- }
26
- if (score >= 60) {
27
- return 'WARN';
28
- }
29
- return 'FAIL';
30
- }
31
-
32
- async function readStdin(): Promise<string> {
33
- const chunks: Buffer[] = [];
34
- for await (const chunk of process.stdin) {
35
- chunks.push(Buffer.from(chunk));
36
- }
37
- return Buffer.concat(chunks).toString('utf8');
38
- }
39
-
40
- async function main(): Promise<void> {
41
- const text = await readStdin();
42
- const data = JSON.parse(text) as { score: number; pages: PageReport[] };
43
-
44
- const date = new Date().toISOString().slice(0, 10);
45
- const lines: string[] = [];
46
- lines.push(`# SEO audit · ${date}`);
47
- lines.push('');
48
- lines.push(`**Overall score:** ${data.score} / 100`);
49
- lines.push('');
50
- lines.push('## Per-page summary');
51
- lines.push('');
52
- lines.push('| Path | Score | Status |');
53
- lines.push('|------|-------|--------|');
54
- for (const page of data.pages) {
55
- lines.push(`| \`${page.path}\` | ${page.score} | ${statusChip(page.score)} |`);
56
- }
57
- lines.push('');
58
- lines.push('## Findings');
59
- lines.push('');
60
- for (const page of data.pages) {
61
- const failures = page.findings.filter((f) => f.severity !== 'ok');
62
- if (failures.length === 0) {
63
- continue;
64
- }
65
- lines.push(`### \`${page.path}\``);
66
- lines.push('');
67
- for (const finding of failures) {
68
- const sev = finding.severity === 'fail' ? '❌' : '⚠️';
69
- lines.push(`- ${sev} **${finding.rule}** — ${finding.message}`);
70
- }
71
- lines.push('');
72
- }
73
- lines.push('');
74
- lines.push(`<!-- agent-summary: { "score": ${data.score}, "pages": ${data.pages.length} } -->`);
75
-
76
- process.stdout.write(lines.join('\n'));
77
- }
78
-
79
- main().catch((err) => {
80
- console.error(err);
81
- process.exit(1);
82
- });
@@ -1,131 +0,0 @@
1
- /**
2
- * Tool: score
3
- *
4
- * Apply the SEO scoring methodology (see references/scoring-methodology.md)
5
- * to a list of `(path, SEOSignals)` tuples and produce a per-page report.
6
- *
7
- * Inputs (stdin JSON): { signals: Record<path, SEOSignals> }
8
- * Outputs (stdout JSON): { score: 0..100, pages: PageReport[] }
9
- */
10
-
11
- interface SEOSignals {
12
- title: string | null;
13
- description: string | null;
14
- canonical: string | null;
15
- robots: string | null;
16
- openGraph: Record<string, string>;
17
- schemaOrg: unknown[];
18
- headings: { h1: string[]; h2: string[]; h3: string[] };
19
- }
20
-
21
- interface Finding {
22
- rule: string;
23
- severity: 'ok' | 'warn' | 'fail';
24
- message: string;
25
- }
26
-
27
- interface PageReport {
28
- path: string;
29
- score: number;
30
- findings: Finding[];
31
- }
32
-
33
- function audit(signals: SEOSignals): Finding[] {
34
- const out: Finding[] = [];
35
-
36
- // Title
37
- if (!signals.title) {
38
- out.push({ rule: 'title-present', severity: 'fail', message: 'Missing <title>' });
39
- } else if (signals.title.length < 30 || signals.title.length > 60) {
40
- out.push({
41
- rule: 'title-length',
42
- severity: 'warn',
43
- message: `Title length ${signals.title.length} chars — recommend 30–60`,
44
- });
45
- } else {
46
- out.push({ rule: 'title', severity: 'ok', message: 'Title looks good' });
47
- }
48
-
49
- // Description
50
- if (!signals.description) {
51
- out.push({ rule: 'description-present', severity: 'fail', message: 'Missing meta description' });
52
- } else if (signals.description.length < 50 || signals.description.length > 160) {
53
- out.push({
54
- rule: 'description-length',
55
- severity: 'warn',
56
- message: `Description length ${signals.description.length} chars — recommend 50–160`,
57
- });
58
- }
59
-
60
- // OG tags
61
- for (const key of ['title', 'description', 'image', 'url']) {
62
- if (!signals.openGraph[key]) {
63
- out.push({ rule: `og-${key}`, severity: 'warn', message: `Missing og:${key}` });
64
- }
65
- }
66
-
67
- // Headings
68
- if (signals.headings.h1.length === 0) {
69
- out.push({ rule: 'h1-present', severity: 'fail', message: 'No <h1> on the page' });
70
- } else if (signals.headings.h1.length > 1) {
71
- out.push({ rule: 'h1-single', severity: 'warn', message: `Multiple <h1> tags (${signals.headings.h1.length})` });
72
- }
73
-
74
- // Canonical
75
- if (!signals.canonical) {
76
- out.push({ rule: 'canonical', severity: 'warn', message: 'Missing canonical link' });
77
- }
78
-
79
- // Robots
80
- if (signals.robots && /noindex/i.test(signals.robots)) {
81
- out.push({ rule: 'robots-noindex', severity: 'fail', message: 'Page is marked noindex' });
82
- }
83
-
84
- // Schema.org
85
- if (signals.schemaOrg.length === 0) {
86
- out.push({ rule: 'schema-org', severity: 'warn', message: 'No JSON-LD schema.org block' });
87
- }
88
-
89
- return out;
90
- }
91
-
92
- function scoreOf(findings: Finding[]): number {
93
- // Each fail = -15, each warn = -5. Floor at 0.
94
- let pts = 100;
95
- for (const f of findings) {
96
- if (f.severity === 'fail') {
97
- pts -= 15;
98
- } else if (f.severity === 'warn') {
99
- pts -= 5;
100
- }
101
- }
102
- return Math.max(0, pts);
103
- }
104
-
105
- async function readStdin(): Promise<string> {
106
- const chunks: Buffer[] = [];
107
- for await (const chunk of process.stdin) {
108
- chunks.push(Buffer.from(chunk));
109
- }
110
- return Buffer.concat(chunks).toString('utf8');
111
- }
112
-
113
- async function main(): Promise<void> {
114
- const text = await readStdin();
115
- const payload = JSON.parse(text) as { signals: Record<string, SEOSignals> };
116
-
117
- const pages: PageReport[] = [];
118
- for (const [path, signals] of Object.entries(payload.signals)) {
119
- const findings = audit(signals);
120
- pages.push({ path, score: scoreOf(findings), findings });
121
- }
122
-
123
- const overall = pages.length === 0 ? 0 : Math.round(pages.reduce((a, p) => a + p.score, 0) / pages.length);
124
-
125
- process.stdout.write(JSON.stringify({ score: overall, pages }, null, 2));
126
- }
127
-
128
- main().catch((err) => {
129
- console.error(err);
130
- process.exit(1);
131
- });