anymorph 0.4.0 → 0.6.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 (76) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +79 -6
  3. package/package.json +1 -1
  4. package/dist/skillpacks/geo/scaffold/AGENTS.md +0 -38
  5. package/dist/skillpacks/geo/scaffold/CLAUDE.md +0 -33
  6. package/dist/skillpacks/geo/shared/evidence-principles.md +0 -35
  7. package/dist/skillpacks/geo/shared/geo-principles.md +0 -281
  8. package/dist/skillpacks/geo/shared/vertical-playbooks/beauty.md +0 -65
  9. package/dist/skillpacks/geo/shared/vertical-playbooks/commerce.md +0 -65
  10. package/dist/skillpacks/geo/shared/vertical-playbooks/ota.md +0 -62
  11. package/dist/skillpacks/geo/shared/vertical-playbooks/saas.md +0 -64
  12. package/dist/skillpacks/geo/skills/brand-owned-diagnosis/SKILL.md +0 -54
  13. package/dist/skillpacks/geo/skills/brand-owned-diagnosis/agents/openai.yaml +0 -4
  14. package/dist/skillpacks/geo/skills/brand-owned-diagnosis/references/diagnosis-contract.md +0 -95
  15. package/dist/skillpacks/geo/skills/brand-owned-diagnosis/references/workflow.md +0 -194
  16. package/dist/skillpacks/geo/skills/geo-generating-actions/SKILL.md +0 -188
  17. package/dist/skillpacks/geo/skills/geo-generating-actions/agents/openai.yaml +0 -4
  18. package/dist/skillpacks/geo/skills/geo-generating-actions/references/orchestrator.workflow.md +0 -440
  19. package/dist/skillpacks/geo/skills/geo-generating-actions/scripts/geo-scaffold.mjs +0 -358
  20. package/dist/skillpacks/geo/skills/geo-generating-actions/scripts/geo.mjs +0 -66
  21. package/dist/skillpacks/geo/skills/geo-initializing-strategy/SKILL.md +0 -50
  22. package/dist/skillpacks/geo/skills/geo-initializing-strategy/agents/openai.yaml +0 -4
  23. package/dist/skillpacks/geo/skills/geo-initializing-strategy/references/external-authority-diagnosis.md +0 -66
  24. package/dist/skillpacks/geo/skills/geo-initializing-strategy/references/foundation-diagnosis.md +0 -86
  25. package/dist/skillpacks/geo/skills/geo-initializing-strategy/references/memory-contract.md +0 -15
  26. package/dist/skillpacks/geo/skills/geo-initializing-strategy/references/semantic-clusters-diagnosis.md +0 -58
  27. package/dist/skillpacks/geo/skills/geo-initializing-strategy/references/strategy-map-contract.md +0 -26
  28. package/dist/skillpacks/geo/skills/geo-initializing-strategy/references/visibility-diagnosis.md +0 -50
  29. package/dist/skillpacks/geo/skills/geo-local-setup/SKILL.md +0 -66
  30. package/dist/skillpacks/geo/skills/geo-local-setup/agents/openai.yaml +0 -4
  31. package/dist/skillpacks/geo/skills/geo-page-writer/SKILL.md +0 -81
  32. package/dist/skillpacks/geo/skills/geo-page-writer/agents/openai.yaml +0 -4
  33. package/dist/skillpacks/geo/skills/geo-page-writer/references/research.md +0 -61
  34. package/dist/skillpacks/geo/skills/geo-page-writer/references/validation.md +0 -59
  35. package/dist/skillpacks/geo/skills/geo-page-writer/references/writing.md +0 -55
  36. package/dist/skillpacks/geo/skills/geo-page-writer/scripts/check-page-mdx.mjs +0 -210
  37. package/dist/skillpacks/geo/skills/geo-page-writer/scripts/collect-page-sources.mjs +0 -303
  38. package/dist/skillpacks/geo/skills/geo-pages-diagnosis/SKILL.md +0 -51
  39. package/dist/skillpacks/geo/skills/geo-pages-diagnosis/agents/openai.yaml +0 -4
  40. package/dist/skillpacks/geo/skills/geo-pages-diagnosis/references/diagnosis-contract.md +0 -125
  41. package/dist/skillpacks/geo/skills/geo-pages-diagnosis/references/workflow.md +0 -269
  42. package/dist/skillpacks/geo/skills/geo-writer/SKILL.md +0 -234
  43. package/dist/skillpacks/geo/skills/geo-writer/agents/openai.yaml +0 -4
  44. package/dist/skillpacks/geo/skills/geo-writer/references/content-writer-contract.md +0 -316
  45. package/dist/skillpacks/geo/skills/geo-writer/references/direct-sql.md +0 -187
  46. package/dist/skillpacks/geo/skills/geo-writer/references/seo-geo-insights.md +0 -269
  47. package/dist/skillpacks/geo/skills/seo-ecommerce-opportunity/SKILL.md +0 -82
  48. package/dist/skillpacks/geo/skills/seo-ecommerce-opportunity/agents/openai.yaml +0 -5
  49. package/dist/skillpacks/geo/skills/seo-ecommerce-opportunity/references/ecommerce-rules.md +0 -31
  50. package/dist/skillpacks/geo/skills/seo-internal-link-opportunity/SKILL.md +0 -57
  51. package/dist/skillpacks/geo/skills/seo-internal-link-opportunity/agents/openai.yaml +0 -5
  52. package/dist/skillpacks/geo/skills/seo-internal-link-opportunity/references/link-rules.md +0 -28
  53. package/dist/skillpacks/geo/skills/seo-opportunity-audit/SKILL.md +0 -141
  54. package/dist/skillpacks/geo/skills/seo-opportunity-audit/agents/openai.yaml +0 -5
  55. package/dist/skillpacks/geo/skills/seo-opportunity-audit/references/action-contract.md +0 -62
  56. package/dist/skillpacks/geo/skills/seo-opportunity-audit/scripts/seo-toolkit.mjs +0 -248
  57. package/dist/skillpacks/geo/skills/seo-page-diagnosis/SKILL.md +0 -56
  58. package/dist/skillpacks/geo/skills/seo-page-diagnosis/agents/openai.yaml +0 -5
  59. package/dist/skillpacks/geo/skills/seo-page-diagnosis/references/page-checks.md +0 -38
  60. package/dist/skillpacks/geo/skills/seo-serp-opportunity-research/SKILL.md +0 -66
  61. package/dist/skillpacks/geo/skills/seo-serp-opportunity-research/agents/openai.yaml +0 -5
  62. package/dist/skillpacks/geo/skills/seo-serp-opportunity-research/references/page-type-taxonomy.md +0 -40
  63. package/dist/skillpacks/geo/skills/seo-serp-opportunity-research/references/serp-methodology.md +0 -38
  64. package/dist/skillpacks/geo/skills/seo-technical-diagnosis/SKILL.md +0 -64
  65. package/dist/skillpacks/geo/skills/seo-technical-diagnosis/agents/openai.yaml +0 -5
  66. package/dist/skillpacks/geo/skills/seo-technical-diagnosis/references/checks.md +0 -58
  67. package/dist/skillpacks/geo/skills/third-party-diagnosis/SKILL.md +0 -54
  68. package/dist/skillpacks/geo/skills/third-party-diagnosis/agents/openai.yaml +0 -4
  69. package/dist/skillpacks/geo/skills/third-party-diagnosis/references/diagnosis-contract.md +0 -111
  70. package/dist/skillpacks/geo/skills/third-party-diagnosis/references/workflow.md +0 -174
  71. package/dist/skillpacks/geo/skills/third-party-execution-planning/SKILL.md +0 -64
  72. package/dist/skillpacks/geo/skills/third-party-execution-planning/agents/openai.yaml +0 -4
  73. package/dist/skillpacks/geo/skills/third-party-execution-planning/references/execution-contract.md +0 -90
  74. package/dist/skillpacks/geo/skills/third-party-execution-planning/references/non-social-surface-playbooks.md +0 -123
  75. package/dist/skillpacks/geo/skills/third-party-execution-planning/references/reddit-rules.md +0 -69
  76. package/dist/skillpacks/geo/skills/third-party-execution-planning/references/social-platform-playbooks.md +0 -59
@@ -1,58 +0,0 @@
1
- # Strategy Map: Semantic Clusters Diagnosis
2
-
3
- Map the brand's long-term opportunity space into semantic clusters.
4
-
5
- Each cluster should describe:
6
-
7
- - intent
8
- - AI decision criteria
9
- - current coverage state
10
- - missing surface types
11
- - known overlap or cannibalization risk
12
- - confidence
13
-
14
- Do not include recommended actions, briefs, or page creation instructions. The orchestrator will turn cluster diagnosis into long-term strategy.
15
-
16
- Use DataForSEO-backed tools first for keyword demand, SERP shape, ranked-keyword gaps, and competitor content gaps:
17
-
18
- - `mcp__anymorph__search_keywords`
19
- - `mcp__anymorph__serp_snapshot`
20
- - `mcp__anymorph__dataforseo_research`
21
- - `mcp__anymorph__get_competitor_gap`
22
-
23
- Use Ahrefs only when available for in-content internal-link hub evidence. If Ahrefs returns "API units limit reached", times out, fails auth, or is unavailable, stop calling Ahrefs and omit `hubSurfaceId` unless a real non-Ahrefs surface id is known.
24
-
25
- Never emit `null`. Omit optional fields like `hubSurfaceId` and `overlaps` when unknown. Required unknown enum fields must use `unknown`.
26
-
27
- Submit exactly one section artifact with `mcp__strategy_map_artifacts__submit_strategy_map_section`.
28
-
29
- ```json
30
- {
31
- "section": "semanticClusters",
32
- "artifact": [
33
- {
34
- "id": "cl-short-stable-id",
35
- "name": "Cluster name",
36
- "intent": "User intent this cluster captures",
37
- "aiDecisionCriteria": ["Criteria AI answers compare"],
38
- "priority": "high | medium | low",
39
- "coverageState": "covered | gap | weak | overlap | monitor | unknown",
40
- "hubSurfaceId": "optional existing hub surface id",
41
- "missingSurfaceTypes": ["proof_surface", "comparison_page"],
42
- "overlaps": ["optional overlapping cluster ids"],
43
- "confidence": "high | medium | low",
44
- "evidenceRefs": ["ev-semantic-1"]
45
- }
46
- ],
47
- "evidence": [
48
- {
49
- "id": "ev-semantic-1",
50
- "type": "signal | workspace | source | research",
51
- "ref": "tool or source reference",
52
- "summary": "Specific evidence summary."
53
- }
54
- ]
55
- }
56
- ```
57
-
58
- Final response: one short sentence naming the saved section.
@@ -1,26 +0,0 @@
1
- # Strategy Map Contract
2
-
3
- The Strategy Map is a diagnosis artifact, not an execution plan.
4
-
5
- Include:
6
-
7
- - Owned-site foundation status and blockers.
8
- - Current AI visibility posture.
9
- - Semantic clusters with priority and coverage state.
10
- - External authority gaps.
11
- - Long-term investment mix and guardrails.
12
-
13
- Do not include:
14
-
15
- - Recommended page actions.
16
- - Outreach copy.
17
- - Concrete prompt panels.
18
- - Measurement targets.
19
- - Weekly success criteria.
20
-
21
- Evidence rules:
22
-
23
- - Use DataForSEO On-Page as the primary owned-site crawl and internal-link source.
24
- - Use Ahrefs only as optional enrichment for in-content internal-link and crawl evidence.
25
- - Use workspace MCP tools before freeform research.
26
- - Mark missing evidence as `unknown`; do not infer certainty from absence.
@@ -1,50 +0,0 @@
1
- # Strategy Map: Visibility Diagnosis
2
-
3
- Summarize current AI visibility.
4
-
5
- Classify durable patterns by cluster or engine:
6
-
7
- - `absent`
8
- - `cited_not_mentioned`
9
- - `mentioned_not_recommended`
10
- - `cited_not_recommended`
11
- - `recommended`
12
- - `declining`
13
- - `unknown`
14
-
15
- Do not paste raw AI answers. Do not propose weekly actions.
16
-
17
- Never emit `null`. Omit optional fields like `clusterId` and `engine` when unknown. Required unknown enum fields must use `unknown`.
18
-
19
- Submit exactly one section artifact with `mcp__strategy_map_artifacts__submit_strategy_map_section`.
20
-
21
- ```json
22
- {
23
- "section": "visibility",
24
- "artifact": {
25
- "overall": "strong | medium | weak | unknown",
26
- "patterns": [
27
- {
28
- "id": "vis-short-stable-id",
29
- "clusterId": "optional-cluster-id",
30
- "engine": "optional engine name",
31
- "state": "absent | cited_not_mentioned | mentioned_not_recommended | cited_not_recommended | recommended | declining | unknown",
32
- "summary": "Cluster- or engine-level visibility pattern.",
33
- "competitors": ["Competitor names"],
34
- "evidenceRefs": ["ev-visibility-1"]
35
- }
36
- ],
37
- "evidenceRefs": ["ev-visibility-1"]
38
- },
39
- "evidence": [
40
- {
41
- "id": "ev-visibility-1",
42
- "type": "signal | workspace | source | research",
43
- "ref": "tool or source reference",
44
- "summary": "Specific evidence summary."
45
- }
46
- ]
47
- }
48
- ```
49
-
50
- Final response: one short sentence naming the saved section.
@@ -1,66 +0,0 @@
1
- ---
2
- name: geo-local-setup
3
- description: Use when setting up, checking, repairing, or preparing a tenant repo for local Anymorph GEO work.
4
- ---
5
-
6
- # GEO Local Setup
7
-
8
- Use this skill before local GEO action generation in a tenant repo.
9
-
10
- ## CLI
11
-
12
- Use the top-level `anymorph` CLI. Run commands from the tenant repo root.
13
-
14
- ## Setup
15
-
16
- Run these from the tenant repo root:
17
-
18
- ```bash
19
- anymorph status
20
- anymorph login
21
- anymorph init
22
- anymorph doctor
23
- ```
24
-
25
- If credentials are missing, authenticated commands start device login
26
- automatically. Use `login` directly when you want to authenticate first.
27
- For non-interactive runs, set `ANYMORPH_ACCESS_TOKEN` and optionally
28
- `ANYMORPH_API_URL`.
29
-
30
- `init` installs managed skills, contracts, run folders, and missing memory files.
31
- `doctor` reports exact missing, extra, or drifted managed files. Use
32
- `doctor --fix` to repair managed files without touching memory or run artifacts.
33
-
34
- To compare against a known source checkout instead of the installed copy:
35
-
36
- ```bash
37
- anymorph doctor --skills-source /path/to/anymorph-geo-skills/skills
38
- ```
39
-
40
- ## Prepare
41
-
42
- ```bash
43
- anymorph status
44
- anymorph workspaces
45
- anymorph prepare <workspaceId>
46
- ```
47
-
48
- After the agent writes `actions.json`:
49
-
50
- ```bash
51
- anymorph validate {runId}
52
- git add agent/runs/{runId} agent/STRATEGY.md agent/LEARNINGS.md
53
- git commit -m "chore: add geo strategy run {runId}"
54
- git push origin main
55
- anymorph submit {runId}
56
- ```
57
-
58
- `submit` registers or updates the backend run and immediately materializes it
59
- in Dashboard. There is no separate approve step.
60
- A submitted run is immutable: once it is `executed`, re-submitting the same
61
- `runId` (even with an edited `actions.json`) is a no-op. To add or change
62
- actions, run a fresh `prepare` for a new `runId` and submit that new run.
63
- For commerce workspaces, `validate` requires every `geo_page` create action to
64
- include `target.targetProductIds` from the prepared product files.
65
-
66
- The `anymorph` CLI owns local GEO setup, prepare, validate, and submit.
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "GEO Local Setup"
3
- short_description: "Set up and verify a tenant repo for local GEO work."
4
- default_prompt: "Use $geo-local-setup to set up or check this tenant repo for local GEO work."
@@ -1,81 +0,0 @@
1
- ---
2
- name: geo-page-writer
3
- description: Use when writing or updating the actual MDX for an Anymorph-managed GEO page in a CMS tenant repo after a geo_page action or generated-page opportunity has been selected.
4
- ---
5
-
6
- # GEO Page Writer
7
-
8
- Use this skill to write or revise a real CMS MDX page for a selected
9
- `geo_page` action. This is an execution skill, not a strategy-planning skill.
10
-
11
- ## Scope
12
-
13
- Write or update:
14
-
15
- - A tenant repo MDX page for a generated GEO page.
16
- - The supporting page-writer evidence artifacts under `agent/page-writer/`.
17
-
18
- Do not:
19
-
20
- - Write `actions.json`.
21
- - Submit GEO strategy runs.
22
- - Replace the backend CMS generation pipeline.
23
- - Invent claims, product facts, prices, image URLs, testimonials, quotes, or
24
- source names.
25
-
26
- ## References
27
-
28
- Read these references before writing:
29
-
30
- - `references/research.md` for source collection, Exa Deep Search, KB search,
31
- image search, and source-pack rules.
32
- - `references/writing.md` for MDX structure, frontmatter, components, images,
33
- CTA, and GEO writing rules.
34
- - `references/validation.md` for agent-owned brand and fact validation plus
35
- script-backed MDX static checks.
36
-
37
- ## Workflow
38
-
39
- 1. Identify the selected `geo_page` action from `agent/runs/{runId}/actions.json`.
40
- 2. Collect page sources:
41
-
42
- ```bash
43
- node .agents/skills/geo-page-writer/scripts/collect-page-sources.mjs all \
44
- --run-id <runId> \
45
- --action-id <actionId> \
46
- --workspace <workspace-id-or-domain>
47
- ```
48
-
49
- Use `.claude/skills/...` instead of `.agents/skills/...` when running under
50
- Claude Code.
51
-
52
- 3. Read `agent/page-writer/{runId}/{actionId}/sources/source-pack.json`.
53
- 4. Write or edit the MDX manually using the source pack and `references/writing.md`.
54
- 5. Self-review brand and fact claims with `references/validation.md`.
55
- 6. Run the static MDX checker:
56
-
57
- ```bash
58
- node .agents/skills/geo-page-writer/scripts/check-page-mdx.mjs \
59
- --file <path/to/page.mdx> \
60
- --sources agent/page-writer/<runId>/<actionId>/sources/source-pack.json
61
- ```
62
-
63
- 7. Fix every error. Review warnings before commit.
64
-
65
- ## Artifact Contract
66
-
67
- Write page-writer artifacts here:
68
-
69
- ```text
70
- agent/page-writer/{runId}/{actionId}/
71
- sources/
72
- exa-research.md
73
- exa-response.json
74
- kb-results.json
75
- image-results.json
76
- source-pack.json
77
- checks/
78
- mdx-static.json
79
- ```
80
-
81
- Commit the MDX and relevant `agent/page-writer/...` artifacts together.
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "GEO Page Writer"
3
- short_description: "Write and validate real CMS MDX for selected GEO page actions."
4
- default_prompt: "Use $geo-page-writer to collect sources, write the GEO page MDX, and run static MDX validation."
@@ -1,61 +0,0 @@
1
- # Research
2
-
3
- The research phase builds the source pack used to write the page. It does not
4
- write the MDX.
5
-
6
- ## Source Order
7
-
8
- Use sources in this order:
9
-
10
- 1. Selected `geo_page` action and run context.
11
- 2. Tenant workspace KB / living document.
12
- 3. Product catalog or action `targetProductIds`.
13
- 4. Exa Deep Search for external evidence.
14
- 5. Image search for existing brand assets.
15
-
16
- Prefer first-party and workspace facts over external summaries. Use external
17
- research to fill market, comparison, definition, and citation gaps.
18
-
19
- ## Exa Deep Search
20
-
21
- `collect-page-sources.mjs exa` uses Exa `/search` with `type:
22
- "deep-reasoning"`. Exa's older `/research/v1` API is deprecated, so do not use
23
- it for new work.
24
-
25
- The Exa result is a research input, not page copy. Treat its synthesized answer
26
- as a draft finding that still needs source-aware judgment.
27
-
28
- ## KB Search
29
-
30
- Use KB search for brand-specific facts:
31
-
32
- - product positioning
33
- - process and methodology
34
- - pricing or packaging claims
35
- - company history
36
- - differentiators
37
- - support, FAQ, and policy details
38
-
39
- If KB search is unavailable, mark the missing query in the source pack and avoid
40
- brand-specific factual claims that need that evidence.
41
-
42
- ## Image Search
43
-
44
- Use image search only for existing workspace assets. Do not generate images in
45
- this skill.
46
-
47
- Every MDX image URL should come from:
48
-
49
- - the source pack image results
50
- - product catalog image inputs
51
- - an existing tenant asset already present in the MDX/repo
52
-
53
- ## Source Pack Rules
54
-
55
- The source pack must make writing decisions auditable:
56
-
57
- - Keep source URLs and titles.
58
- - Keep KB snippets separate from external web sources.
59
- - Keep image results separate from factual sources.
60
- - Record failures instead of silently omitting a source class.
61
- - Do not promote a source into a claim unless the source actually supports it.
@@ -1,59 +0,0 @@
1
- # Validation
2
-
3
- Validation has two owners:
4
-
5
- - The agent owns brand and fact validation.
6
- - `check-page-mdx.mjs` owns deterministic MDX static validation.
7
-
8
- ## Brand Validation
9
-
10
- Before running the script, review the MDX against the source pack and tenant
11
- brand context.
12
-
13
- Check:
14
-
15
- - Tone matches the brand guidance.
16
- - Blocklisted phrases are absent.
17
- - Required positioning is represented.
18
- - "Must avoid" claims or angles are not used.
19
- - The brand role is honest and not forced into unrelated claims.
20
- - Competitive comparisons are measured and sourced.
21
-
22
- If unsure, soften the claim or remove it.
23
-
24
- ## Fact Validation
25
-
26
- Every concrete factual claim must be supported by the source pack or existing
27
- tenant/product data.
28
-
29
- Remove or soften:
30
-
31
- - numbers not present in sources
32
- - dates not present in sources
33
- - "best", "#1", "first", "only", "guaranteed", or "clinically proven" without
34
- direct evidence
35
- - product efficacy claims without product/KB/source support
36
- - competitor comparisons without a source for both sides
37
- - exact quotes not present verbatim in a source
38
-
39
- Use hedged language when evidence is directional but not conclusive.
40
-
41
- ## MDX Static Validation
42
-
43
- Run:
44
-
45
- ```bash
46
- node .agents/skills/geo-page-writer/scripts/check-page-mdx.mjs \
47
- --file <path/to/page.mdx> \
48
- --sources agent/page-writer/<runId>/<actionId>/sources/source-pack.json
49
- ```
50
-
51
- The checker validates:
52
-
53
- - frontmatter presence
54
- - MDX/JSX parse when a compiler is available
55
- - import and component allowlists
56
- - image URL provenance
57
- - link and CTA URL shape
58
-
59
- Fix all errors. Review warnings before committing.
@@ -1,55 +0,0 @@
1
- # Writing
2
-
3
- Write the actual MDX directly. Do not use a writing script.
4
-
5
- ## Page Shape
6
-
7
- The page should be useful to a human reader and easy for AI engines to cite.
8
-
9
- Use:
10
-
11
- - Answer-first opening.
12
- - Clear H2 sections that each answer one question.
13
- - Self-contained paragraphs with explicit entities.
14
- - Specific product, process, or methodology details when sourced.
15
- - Tables or lists only when they improve comparison or extraction.
16
-
17
- Avoid:
18
-
19
- - Generic SEO filler.
20
- - Keyword stuffing.
21
- - Unsupported rankings or superlatives.
22
- - Fake quotes, fake studies, fake customers, or invented numbers.
23
- - Images that are merely decorative when product/source evidence is needed.
24
-
25
- ## Frontmatter
26
-
27
- Include the tenant's expected MDX frontmatter fields. At minimum:
28
-
29
- - `title`
30
- - `description`
31
- - `lang`
32
-
33
- Include `heroImage` only when the URL is from the source pack, product catalog,
34
- or an existing tenant asset.
35
-
36
- ## Components
37
-
38
- Use only components already supported by the tenant CMS renderer. Prefer plain
39
- MDX when unsure.
40
-
41
- For commerce/product pages:
42
-
43
- - Only mention products present in the source pack or product context.
44
- - Use product image, price, URL, and title verbatim.
45
- - Do not compute discounts or localize prices yourself.
46
-
47
- ## CTA
48
-
49
- Use CTA URLs from tenant inputs or existing safe routes. Do not invent checkout,
50
- demo, pricing, or contact URLs.
51
-
52
- ## Images
53
-
54
- Use image URLs verbatim. Add useful alt text. Do not crop product imagery in a
55
- way that hides packaging, texture, label, or use context.
@@ -1,210 +0,0 @@
1
- #!/usr/bin/env node
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { dirname, isAbsolute, join, resolve } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
-
6
- const args = parseArgs(process.argv.slice(2));
7
-
8
- try {
9
- const result = await check(args);
10
- console.log(JSON.stringify(result, null, 2));
11
- process.exit(result.ok ? 0 : 1);
12
- } catch (err) {
13
- const message = err instanceof Error ? err.message : String(err);
14
- console.error(message);
15
- process.exit(1);
16
- }
17
-
18
- function parseArgs(argv) {
19
- const out = {};
20
- for (let i = 0; i < argv.length; i += 1) {
21
- const arg = argv[i];
22
- if (arg === "--file") out.file = requireValue(argv, ++i, arg);
23
- else if (arg === "--sources") out.sources = requireValue(argv, ++i, arg);
24
- else if (arg === "--out") out.out = requireValue(argv, ++i, arg);
25
- else throw new Error(`Unknown argument: ${arg}`);
26
- }
27
- return out;
28
- }
29
-
30
- function requireValue(argv, index, flag) {
31
- const value = argv[index];
32
- if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`);
33
- return value;
34
- }
35
-
36
- async function check(input) {
37
- const file = required(input.file, "--file");
38
- const sourcePath = input.sources;
39
- const mdxPath = resolve(file);
40
- const mdx = await readFile(mdxPath, "utf8");
41
- const sourcePack = sourcePath ? JSON.parse(await readFile(resolve(sourcePath), "utf8")) : null;
42
- const errors = [];
43
- const warnings = [];
44
- const frontmatter = parseFrontmatter(mdx, errors);
45
- checkFrontmatter(frontmatter, errors);
46
- checkImports(mdx, errors, warnings);
47
- checkComponents(mdx, errors);
48
- checkLinks(mdx, warnings);
49
- checkImages(mdx, sourcePack, warnings);
50
- await checkMdxCompile(mdx, errors, warnings);
51
- const result = {
52
- ok: errors.length === 0,
53
- file: mdxPath,
54
- checkedAt: new Date().toISOString(),
55
- errors,
56
- warnings,
57
- };
58
- if (input.out) {
59
- const outPath = isAbsolute(input.out) ? input.out : resolve(input.out);
60
- await mkdir(dirname(outPath), { recursive: true });
61
- await writeFile(outPath, `${JSON.stringify(result, null, 2)}\n`);
62
- }
63
- return result;
64
- }
65
-
66
- function parseFrontmatter(mdx, errors) {
67
- if (!mdx.startsWith("---\n")) {
68
- errors.push("frontmatter: file must start with YAML frontmatter");
69
- return {};
70
- }
71
- const end = mdx.indexOf("\n---", 4);
72
- if (end === -1) {
73
- errors.push("frontmatter: closing --- not found");
74
- return {};
75
- }
76
- const raw = mdx.slice(4, end).trim();
77
- const data = {};
78
- for (const line of raw.split(/\r?\n/)) {
79
- const trimmed = line.trim();
80
- if (!trimmed || trimmed.startsWith("#")) continue;
81
- const match = trimmed.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
82
- if (!match) continue;
83
- data[match[1]] = unquote(match[2]);
84
- }
85
- return data;
86
- }
87
-
88
- function unquote(value) {
89
- const trimmed = value.trim();
90
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
91
- return trimmed.slice(1, -1);
92
- }
93
- return trimmed;
94
- }
95
-
96
- function checkFrontmatter(frontmatter, errors) {
97
- for (const field of ["title", "description", "lang"]) {
98
- if (!frontmatter[field]) errors.push(`frontmatter: ${field} is required`);
99
- }
100
- if (frontmatter.heroImage && !isHttpUrl(frontmatter.heroImage) && !frontmatter.heroImage.startsWith("/")) {
101
- errors.push("frontmatter: heroImage must be an absolute http(s) URL or root-relative path");
102
- }
103
- }
104
-
105
- const ALLOWED_IMPORTS = new Set([
106
- "@/components/mdx",
107
- "@/components/cms",
108
- "@/components/content",
109
- ]);
110
-
111
- function checkImports(mdx, errors, warnings) {
112
- const imports = [...mdx.matchAll(/^\s*import\s+[^'"]+['"]([^'"]+)['"];?\s*$/gm)].map((m) => m[1]);
113
- for (const specifier of imports) {
114
- if (![...ALLOWED_IMPORTS].some((allowed) => specifier.startsWith(allowed))) {
115
- errors.push(`import: unsupported import "${specifier}"`);
116
- }
117
- }
118
- }
119
-
120
- const ALLOWED_COMPONENTS = new Set([
121
- "ArticleHeader",
122
- "Callout",
123
- "CTA",
124
- "FAQ",
125
- "Image",
126
- "ImageGrid",
127
- "ProductCard",
128
- "RelatedContent",
129
- "Table",
130
- ]);
131
-
132
- function checkComponents(mdx, errors) {
133
- const tags = [...mdx.matchAll(/<([A-Z][A-Za-z0-9.]*)\b/g)].map((m) => m[1].split(".")[0]);
134
- for (const tag of new Set(tags)) {
135
- if (!ALLOWED_COMPONENTS.has(tag)) errors.push(`component: unsupported component <${tag}>`);
136
- }
137
- }
138
-
139
- function checkLinks(mdx, warnings) {
140
- const markdownLinks = [...mdx.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)].map((m) => m[1]);
141
- const hrefs = [...mdx.matchAll(/\bhref=["']([^"']+)["']/g)].map((m) => m[1]);
142
- for (const url of [...markdownLinks, ...hrefs]) {
143
- if (url.startsWith("#") || url.startsWith("/") || isHttpUrl(url) || url.startsWith("mailto:") || url.startsWith("tel:")) continue;
144
- warnings.push(`link: review non-standard URL "${url}"`);
145
- }
146
- }
147
-
148
- function checkImages(mdx, sourcePack, warnings) {
149
- const urls = [
150
- ...[...mdx.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g)].map((m) => m[1]),
151
- ...[...mdx.matchAll(/\bsrc=["']([^"']+)["']/g)].map((m) => m[1]),
152
- ...[...mdx.matchAll(/\bimage=["']([^"']+)["']/g)].map((m) => m[1]),
153
- ...[...mdx.matchAll(/\bheroImage:\s*["']?([^"'\n]+)["']?/g)].map((m) => m[1].trim()),
154
- ].filter((url) => isHttpUrl(url));
155
- const allowed = collectAllowedImageUrls(sourcePack);
156
- for (const url of new Set(urls)) {
157
- if (allowed.size > 0 && !allowed.has(url)) {
158
- warnings.push(`image: "${url}" is not present in source-pack image results`);
159
- }
160
- }
161
- }
162
-
163
- function collectAllowedImageUrls(sourcePack) {
164
- const urls = new Set();
165
- const add = (value) => {
166
- if (typeof value === "string" && isHttpUrl(value)) urls.add(value);
167
- };
168
- const walk = (value) => {
169
- if (Array.isArray(value)) return value.forEach(walk);
170
- if (!value || typeof value !== "object") return;
171
- for (const [key, child] of Object.entries(value)) {
172
- if (/^(url|imageUrl|src|heroImage)$/i.test(key)) add(child);
173
- walk(child);
174
- }
175
- };
176
- walk(sourcePack?.images);
177
- walk(sourcePack?.imageResults);
178
- walk(sourcePack?.action?.target);
179
- return urls;
180
- }
181
-
182
- async function checkMdxCompile(mdx, errors, warnings) {
183
- try {
184
- const mod = await import("@mdx-js/mdx");
185
- await mod.compile(mdx, { jsx: true, development: false });
186
- } catch (err) {
187
- const message = err instanceof Error ? err.message : String(err);
188
- if (message.includes("Cannot find package '@mdx-js/mdx'")) {
189
- warnings.push("mdx: @mdx-js/mdx is not installed in this repo; skipped compiler parse check");
190
- checkBalancedBraces(mdx, warnings);
191
- return;
192
- }
193
- errors.push(`mdx: compile failed: ${message}`);
194
- }
195
- }
196
-
197
- function checkBalancedBraces(mdx, warnings) {
198
- const opens = (mdx.match(/{/g) ?? []).length;
199
- const closes = (mdx.match(/}/g) ?? []).length;
200
- if (opens !== closes) warnings.push(`mdx: brace count differs ({=${opens}, }=${closes}); review JSX expressions`);
201
- }
202
-
203
- function isHttpUrl(value) {
204
- return /^https?:\/\//i.test(value);
205
- }
206
-
207
- function required(value, name) {
208
- if (!value) throw new Error(`${name} is required`);
209
- return value;
210
- }