anymorph 0.2.5 → 0.4.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.
- package/README.md +50 -36
- package/dist/index.js +9231 -251
- package/dist/skillpacks/geo/scaffold/AGENTS.md +38 -0
- package/dist/skillpacks/geo/scaffold/CLAUDE.md +33 -0
- package/dist/skillpacks/geo/shared/evidence-principles.md +35 -0
- package/dist/skillpacks/geo/shared/geo-principles.md +281 -0
- package/dist/skillpacks/geo/shared/vertical-playbooks/beauty.md +65 -0
- package/dist/skillpacks/geo/shared/vertical-playbooks/commerce.md +65 -0
- package/dist/skillpacks/geo/shared/vertical-playbooks/ota.md +62 -0
- package/dist/skillpacks/geo/shared/vertical-playbooks/saas.md +64 -0
- package/dist/skillpacks/geo/skills/brand-owned-diagnosis/SKILL.md +1 -1
- package/dist/skillpacks/geo/skills/brand-owned-diagnosis/references/diagnosis-contract.md +8 -9
- package/dist/skillpacks/geo/skills/brand-owned-diagnosis/references/workflow.md +11 -8
- package/dist/skillpacks/geo/skills/geo-generating-actions/SKILL.md +110 -22
- package/dist/skillpacks/geo/skills/geo-generating-actions/references/orchestrator.workflow.md +171 -5
- package/dist/skillpacks/geo/skills/geo-generating-actions/scripts/geo-scaffold.mjs +358 -0
- package/dist/skillpacks/geo/skills/geo-generating-actions/scripts/geo.mjs +66 -0
- package/dist/skillpacks/geo/skills/geo-initializing-strategy/references/foundation-diagnosis.md +1 -1
- package/dist/skillpacks/geo/skills/geo-local-setup/SKILL.md +66 -0
- package/dist/skillpacks/geo/skills/geo-local-setup/agents/openai.yaml +4 -0
- package/dist/skillpacks/geo/skills/geo-page-writer/SKILL.md +81 -0
- package/dist/skillpacks/geo/skills/geo-page-writer/agents/openai.yaml +4 -0
- package/dist/skillpacks/geo/skills/geo-page-writer/references/research.md +61 -0
- package/dist/skillpacks/geo/skills/geo-page-writer/references/validation.md +59 -0
- package/dist/skillpacks/geo/skills/geo-page-writer/references/writing.md +55 -0
- package/dist/skillpacks/geo/skills/geo-page-writer/scripts/check-page-mdx.mjs +210 -0
- package/dist/skillpacks/geo/skills/geo-page-writer/scripts/collect-page-sources.mjs +303 -0
- package/dist/skillpacks/geo/skills/geo-pages-diagnosis/SKILL.md +1 -1
- package/dist/skillpacks/geo/skills/geo-pages-diagnosis/references/diagnosis-contract.md +36 -17
- package/dist/skillpacks/geo/skills/geo-pages-diagnosis/references/workflow.md +65 -15
- package/dist/skillpacks/geo/skills/seo-ecommerce-opportunity/SKILL.md +82 -0
- package/dist/skillpacks/geo/skills/seo-ecommerce-opportunity/agents/openai.yaml +5 -0
- package/dist/skillpacks/geo/skills/seo-ecommerce-opportunity/references/ecommerce-rules.md +31 -0
- package/dist/skillpacks/geo/skills/seo-internal-link-opportunity/SKILL.md +57 -0
- package/dist/skillpacks/geo/skills/seo-internal-link-opportunity/agents/openai.yaml +5 -0
- package/dist/skillpacks/geo/skills/seo-internal-link-opportunity/references/link-rules.md +28 -0
- package/dist/skillpacks/geo/skills/seo-opportunity-audit/SKILL.md +141 -0
- package/dist/skillpacks/geo/skills/seo-opportunity-audit/agents/openai.yaml +5 -0
- package/dist/skillpacks/geo/skills/seo-opportunity-audit/references/action-contract.md +62 -0
- package/dist/skillpacks/geo/skills/seo-opportunity-audit/scripts/seo-toolkit.mjs +248 -0
- package/dist/skillpacks/geo/skills/seo-page-diagnosis/SKILL.md +56 -0
- package/dist/skillpacks/geo/skills/seo-page-diagnosis/agents/openai.yaml +5 -0
- package/dist/skillpacks/geo/skills/seo-page-diagnosis/references/page-checks.md +38 -0
- package/dist/skillpacks/geo/skills/seo-serp-opportunity-research/SKILL.md +66 -0
- package/dist/skillpacks/geo/skills/seo-serp-opportunity-research/agents/openai.yaml +5 -0
- package/dist/skillpacks/geo/skills/seo-serp-opportunity-research/references/page-type-taxonomy.md +40 -0
- package/dist/skillpacks/geo/skills/seo-serp-opportunity-research/references/serp-methodology.md +38 -0
- package/dist/skillpacks/geo/skills/seo-technical-diagnosis/SKILL.md +64 -0
- package/dist/skillpacks/geo/skills/seo-technical-diagnosis/agents/openai.yaml +5 -0
- package/dist/skillpacks/geo/skills/seo-technical-diagnosis/references/checks.md +58 -0
- package/dist/skillpacks/geo/skills/third-party-diagnosis/SKILL.md +1 -1
- package/dist/skillpacks/geo/skills/third-party-diagnosis/references/diagnosis-contract.md +2 -2
- package/dist/skillpacks/geo/skills/third-party-diagnosis/references/workflow.md +1 -1
- package/dist/skillpacks/geo/skills/third-party-execution-planning/SKILL.md +64 -0
- package/dist/skillpacks/geo/skills/third-party-execution-planning/agents/openai.yaml +4 -0
- package/dist/skillpacks/geo/skills/third-party-execution-planning/references/execution-contract.md +90 -0
- package/dist/skillpacks/geo/skills/third-party-execution-planning/references/non-social-surface-playbooks.md +123 -0
- package/package.json +2 -1
- package/dist/skillpacks/geo/skills/social-execution-planning/SKILL.md +0 -53
- package/dist/skillpacks/geo/skills/social-execution-planning/agents/openai.yaml +0 -5
- package/dist/skillpacks/geo/skills/social-execution-planning/references/execution-contract.md +0 -68
- /package/dist/skillpacks/geo/skills/{social-execution-planning → third-party-execution-planning}/references/reddit-rules.md +0 -0
- /package/dist/skillpacks/geo/skills/{social-execution-planning/references/platform-playbooks.md → third-party-execution-planning/references/social-platform-playbooks.md} +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
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.
|
|
@@ -0,0 +1,55 @@
|
|
|
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.
|
|
@@ -0,0 +1,210 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = join(homedir(), ".anymorph");
|
|
8
|
+
const CRED_FILE = join(CONFIG_DIR, "credentials.json");
|
|
9
|
+
|
|
10
|
+
const command = process.argv[2];
|
|
11
|
+
const args = parseArgs(process.argv.slice(3));
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const result = await run(command, args);
|
|
15
|
+
if (result !== undefined) console.log(JSON.stringify(result, null, 2));
|
|
16
|
+
if (result && result.ok === false) process.exitCode = 1;
|
|
17
|
+
} catch (err) {
|
|
18
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19
|
+
console.error(message);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function run(mode, input) {
|
|
24
|
+
if (mode === "exa") return collectExa(input);
|
|
25
|
+
if (mode === "kb") return collectKb(input);
|
|
26
|
+
if (mode === "images") return collectImages(input);
|
|
27
|
+
if (mode === "all") return collectAll(input);
|
|
28
|
+
throw new Error("Usage: collect-page-sources.mjs <exa|kb|images|all> --run-id <id> --action-id <id> --workspace <id-or-domain> [--repo .] [--query text]");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const out = { repo: ".", limit: "10", imageLimit: "10" };
|
|
33
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
34
|
+
const arg = argv[i];
|
|
35
|
+
if (arg === "--repo") out.repo = requireValue(argv, ++i, arg);
|
|
36
|
+
else if (arg === "--run-id") out.runId = requireValue(argv, ++i, arg);
|
|
37
|
+
else if (arg === "--action-id") out.actionId = requireValue(argv, ++i, arg);
|
|
38
|
+
else if (arg === "--workspace") out.workspace = requireValue(argv, ++i, arg);
|
|
39
|
+
else if (arg === "--query") out.query = requireValue(argv, ++i, arg);
|
|
40
|
+
else if (arg === "--limit") out.limit = requireValue(argv, ++i, arg);
|
|
41
|
+
else if (arg === "--image-limit") out.imageLimit = requireValue(argv, ++i, arg);
|
|
42
|
+
else if (arg === "--type") out.type = requireValue(argv, ++i, arg);
|
|
43
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function requireValue(argv, index, flag) {
|
|
49
|
+
const value = argv[index];
|
|
50
|
+
if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function collectAll(input) {
|
|
55
|
+
const results = { exa: null, kb: null, images: null, failures: [] };
|
|
56
|
+
for (const [key, fn] of [
|
|
57
|
+
["exa", collectExa],
|
|
58
|
+
["kb", collectKb],
|
|
59
|
+
["images", collectImages],
|
|
60
|
+
]) {
|
|
61
|
+
try {
|
|
62
|
+
results[key] = await fn({ ...input, quiet: true });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
results.failures.push({ source: key, error: err instanceof Error ? err.message : String(err) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const ctx = await loadContext(input);
|
|
68
|
+
const sourcesDir = await ensureSourcesDir(ctx);
|
|
69
|
+
const sourcePack = {
|
|
70
|
+
schemaVersion: 1,
|
|
71
|
+
runId: ctx.runId,
|
|
72
|
+
actionId: ctx.actionId,
|
|
73
|
+
workspace: ctx.workspace,
|
|
74
|
+
query: ctx.query,
|
|
75
|
+
action: ctx.action,
|
|
76
|
+
generatedAt: new Date().toISOString(),
|
|
77
|
+
exa: results.exa?.summary ?? null,
|
|
78
|
+
exaSources: results.exa?.sources ?? [],
|
|
79
|
+
kb: results.kb?.summary ?? null,
|
|
80
|
+
kbResults: results.kb?.results ?? [],
|
|
81
|
+
images: results.images?.summary ?? null,
|
|
82
|
+
imageResults: results.images?.results ?? [],
|
|
83
|
+
failures: results.failures,
|
|
84
|
+
};
|
|
85
|
+
await writeJson(join(sourcesDir, "source-pack.json"), sourcePack);
|
|
86
|
+
return { ok: results.failures.length === 0, dir: sourcesDir, sourcePack: join(sourcesDir, "source-pack.json"), failures: results.failures };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function collectExa(input) {
|
|
90
|
+
const apiKey = process.env.EXA_API_KEY;
|
|
91
|
+
if (!apiKey) throw new Error("EXA_API_KEY is required for Exa Deep Search");
|
|
92
|
+
const ctx = await loadContext(input);
|
|
93
|
+
const sourcesDir = await ensureSourcesDir(ctx);
|
|
94
|
+
const body = {
|
|
95
|
+
query: buildResearchPrompt(ctx),
|
|
96
|
+
type: input.type ?? "deep-reasoning",
|
|
97
|
+
contents: { highlights: true, summary: true },
|
|
98
|
+
};
|
|
99
|
+
const res = await fetch("https://api.exa.ai/search", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
"x-api-key": apiKey,
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify(body),
|
|
106
|
+
});
|
|
107
|
+
const text = await res.text();
|
|
108
|
+
if (!res.ok) throw new Error(`Exa search failed (${res.status}): ${text.slice(0, 1000)}`);
|
|
109
|
+
const data = JSON.parse(text);
|
|
110
|
+
const markdown = renderExaMarkdown(ctx, data);
|
|
111
|
+
await writeFile(join(sourcesDir, "exa-research.md"), markdown);
|
|
112
|
+
await writeJson(join(sourcesDir, "exa-response.json"), data);
|
|
113
|
+
const summary = {
|
|
114
|
+
type: body.type,
|
|
115
|
+
resultCount: Array.isArray(data.results) ? data.results.length : 0,
|
|
116
|
+
hasOutput: Boolean(data.output),
|
|
117
|
+
costDollars: data.costDollars ?? null,
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
dir: sourcesDir,
|
|
122
|
+
summary,
|
|
123
|
+
sources: (data.results ?? []).map((item) => ({
|
|
124
|
+
title: item.title ?? null,
|
|
125
|
+
url: item.url ?? null,
|
|
126
|
+
publishedDate: item.publishedDate ?? null,
|
|
127
|
+
})),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function collectKb(input) {
|
|
132
|
+
const ctx = await loadContext(input);
|
|
133
|
+
const workspaceId = await resolveWorkspaceId(ctx.workspace);
|
|
134
|
+
const sourcesDir = await ensureSourcesDir(ctx);
|
|
135
|
+
const params = new URLSearchParams({ query: ctx.query });
|
|
136
|
+
const data = await backendJson("GET", `/api/workspaces/${encodeURIComponent(workspaceId)}/knowledge-base/search?${params}`);
|
|
137
|
+
await writeJson(join(sourcesDir, "kb-results.json"), data);
|
|
138
|
+
const summary = {
|
|
139
|
+
workspaceId,
|
|
140
|
+
resultCount: Array.isArray(data.results) ? data.results.length : 0,
|
|
141
|
+
};
|
|
142
|
+
return { ok: true, dir: sourcesDir, summary, results: data.results ?? [] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function collectImages(input) {
|
|
146
|
+
const ctx = await loadContext(input);
|
|
147
|
+
const workspaceId = await resolveWorkspaceId(ctx.workspace);
|
|
148
|
+
const sourcesDir = await ensureSourcesDir(ctx);
|
|
149
|
+
const params = new URLSearchParams({
|
|
150
|
+
q: ctx.query,
|
|
151
|
+
topK: String(Number(input.imageLimit ?? input.limit ?? 10)),
|
|
152
|
+
});
|
|
153
|
+
const data = await backendJson("GET", `/api/workspaces/${encodeURIComponent(workspaceId)}/image-assets/search?${params}`);
|
|
154
|
+
await writeJson(join(sourcesDir, "image-results.json"), data);
|
|
155
|
+
const rows = Array.isArray(data.results) ? data.results : Array.isArray(data.items) ? data.items : [];
|
|
156
|
+
const summary = { workspaceId, resultCount: rows.length };
|
|
157
|
+
return { ok: true, dir: sourcesDir, summary, results: rows };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function loadContext(input) {
|
|
161
|
+
const repo = resolve(input.repo ?? ".");
|
|
162
|
+
const runId = required(input.runId, "--run-id");
|
|
163
|
+
const actionId = required(input.actionId, "--action-id");
|
|
164
|
+
const runDir = join(repo, "agent", "runs", runId);
|
|
165
|
+
const actions = await readJson(join(runDir, "actions.json"));
|
|
166
|
+
const context = await readJson(join(runDir, "context.json")).catch(() => null);
|
|
167
|
+
const action = actions.actions?.find((item) => item.id === actionId);
|
|
168
|
+
if (!action) throw new Error(`Action ${actionId} not found in agent/runs/${runId}/actions.json`);
|
|
169
|
+
if (action.assetType !== "geo_page") throw new Error(`Action ${actionId} is ${action.assetType}; geo-page-writer only supports geo_page actions`);
|
|
170
|
+
const workspace = input.workspace ?? context?.workspaceId ?? context?.workspaceDomain;
|
|
171
|
+
if (!workspace) throw new Error("--workspace is required when run context does not include workspaceId/workspaceDomain");
|
|
172
|
+
const query = input.query ?? buildDefaultQuery(action, context);
|
|
173
|
+
return { repo, runId, actionId, runDir, action, context, workspace, query };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildDefaultQuery(action, context) {
|
|
177
|
+
const target = action.target ?? {};
|
|
178
|
+
return [
|
|
179
|
+
target.title,
|
|
180
|
+
action.objective,
|
|
181
|
+
action.changeBrief,
|
|
182
|
+
action.reason,
|
|
183
|
+
Array.isArray(target.queryCluster) ? target.queryCluster.join(", ") : "",
|
|
184
|
+
context?.workspaceDomain ? `brand domain: ${context.workspaceDomain}` : "",
|
|
185
|
+
]
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildResearchPrompt(ctx) {
|
|
191
|
+
return [
|
|
192
|
+
"Research this GEO page before MDX writing.",
|
|
193
|
+
"Return concise findings, source URLs, caveats, and claims that are safe to cite.",
|
|
194
|
+
"",
|
|
195
|
+
`Workspace: ${ctx.workspace}`,
|
|
196
|
+
`Run: ${ctx.runId}`,
|
|
197
|
+
`Action: ${ctx.actionId}`,
|
|
198
|
+
`Objective: ${ctx.action.objective}`,
|
|
199
|
+
`Reason: ${ctx.action.reason}`,
|
|
200
|
+
`Brief: ${ctx.action.changeBrief}`,
|
|
201
|
+
`Expected outcome: ${ctx.action.expectedOutcome}`,
|
|
202
|
+
"",
|
|
203
|
+
`Research query:\n${ctx.query}`,
|
|
204
|
+
].join("\n");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function renderExaMarkdown(ctx, data) {
|
|
208
|
+
const lines = [
|
|
209
|
+
`# Exa Research: ${ctx.action.target?.title ?? ctx.action.objective}`,
|
|
210
|
+
"",
|
|
211
|
+
`Run: ${ctx.runId}`,
|
|
212
|
+
`Action: ${ctx.actionId}`,
|
|
213
|
+
`Generated: ${new Date().toISOString()}`,
|
|
214
|
+
"",
|
|
215
|
+
"## Synthesized Output",
|
|
216
|
+
"",
|
|
217
|
+
renderOutputContent(data.output?.content),
|
|
218
|
+
"",
|
|
219
|
+
"## Grounding",
|
|
220
|
+
"",
|
|
221
|
+
...(Array.isArray(data.output?.grounding)
|
|
222
|
+
? data.output.grounding.map((item) => `- ${item.field ?? "output"}: ${(item.citations ?? []).map((c) => `${c.title ?? c.url} (${c.url})`).join("; ")}${item.confidence ? ` [${item.confidence}]` : ""}`)
|
|
223
|
+
: ["- none"]),
|
|
224
|
+
"",
|
|
225
|
+
"## Results",
|
|
226
|
+
"",
|
|
227
|
+
...(Array.isArray(data.results)
|
|
228
|
+
? data.results.map((item) => `- ${item.title ?? item.url}\n ${item.url}\n ${(item.highlights ?? []).slice(0, 3).join("\n ") || item.summary || ""}`)
|
|
229
|
+
: ["- none"]),
|
|
230
|
+
];
|
|
231
|
+
return `${lines.join("\n")}\n`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function renderOutputContent(value) {
|
|
235
|
+
if (value == null) return "_No synthesized output returned._";
|
|
236
|
+
if (typeof value === "string") return value;
|
|
237
|
+
return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function ensureSourcesDir(ctx) {
|
|
241
|
+
const dir = join(ctx.repo, "agent", "page-writer", ctx.runId, ctx.actionId, "sources");
|
|
242
|
+
await mkdir(dir, { recursive: true });
|
|
243
|
+
return dir;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function readJson(path) {
|
|
247
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function writeJson(path, value) {
|
|
251
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function backendJson(method, path) {
|
|
255
|
+
const res = await backendRequest(method, path);
|
|
256
|
+
const text = await res.text();
|
|
257
|
+
if (!res.ok) throw new Error(`${method} ${path} failed (${res.status}): ${text.slice(0, 1000) || res.statusText}`);
|
|
258
|
+
return text ? JSON.parse(text) : {};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function backendRequest(method, path) {
|
|
262
|
+
const baseUrl = getApiUrl();
|
|
263
|
+
const headers = { "Content-Type": "application/json" };
|
|
264
|
+
const internal = process.env.ANYMORPH_INTERNAL_API_KEY || process.env.AGENT_INTERNAL_API_KEY || process.env.CRON_SECRET;
|
|
265
|
+
if (internal) headers.Authorization = `Bearer ${internal}`;
|
|
266
|
+
else if (process.env.ANYMORPH_ACCESS_TOKEN) headers.Authorization = `Bearer ${process.env.ANYMORPH_ACCESS_TOKEN}`;
|
|
267
|
+
else {
|
|
268
|
+
const creds = readCredentials();
|
|
269
|
+
if (creds?.access_token) headers.Authorization = `Bearer ${creds.access_token}`;
|
|
270
|
+
}
|
|
271
|
+
if (!headers.Authorization) throw new Error("No backend credential found. Run `anymorph login` or set ANYMORPH_ACCESS_TOKEN/ANYMORPH_INTERNAL_API_KEY.");
|
|
272
|
+
return fetch(`${baseUrl}${path}`, { method, headers });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function resolveWorkspaceId(workspace) {
|
|
276
|
+
if (/^(ws_|workspace_|[a-z0-9]{20,})/i.test(workspace)) return workspace;
|
|
277
|
+
try {
|
|
278
|
+
const payload = await backendJson("GET", "/api/cli/status");
|
|
279
|
+
const match = (payload.workspaces ?? []).find((item) => item.id === workspace || item.domain === workspace || item.domain === String(workspace).replace(/^www\./, ""));
|
|
280
|
+
return match?.id ?? workspace;
|
|
281
|
+
} catch {
|
|
282
|
+
return workspace;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function readCredentials() {
|
|
287
|
+
if (!existsSync(CRED_FILE)) return null;
|
|
288
|
+
try {
|
|
289
|
+
return JSON.parse(readFileSync(CRED_FILE, "utf8"));
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getApiUrl() {
|
|
296
|
+
const creds = readCredentials();
|
|
297
|
+
return creds?.api_url ?? process.env.ANYMORPH_API_URL ?? "https://api.anymorph.ai";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function required(value, name) {
|
|
301
|
+
if (!value) throw new Error(`${name} is required`);
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
@@ -32,7 +32,7 @@ Read these references before producing proposals:
|
|
|
32
32
|
- `references/diagnosis-contract.md` for the exact runtime diagnosis contract and artifact submission shape.
|
|
33
33
|
|
|
34
34
|
1. Read shared run context and the GEO page candidate slice.
|
|
35
|
-
2. Use `
|
|
35
|
+
2. Use `list_geo_page_candidates` before broad SQL.
|
|
36
36
|
3. Use `get_geo_intent_diagnostic` for weak intents and generated-page gaps.
|
|
37
37
|
4. Use page visibility, page insights, content, source, and products only when needed.
|
|
38
38
|
5. For commerce create actions, use product evidence before proposing product-linked pages.
|