archondev 3.0.0 → 3.1.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 (40) hide show
  1. package/README.md +27 -32
  2. package/dist/auth-ZMBA5HYH.js +13 -0
  3. package/dist/{bug-TFICZ4OP.js → bug-MOMNYU5J.js} +2 -2
  4. package/dist/{chunk-43IIEFB2.js → chunk-7ELR6RW6.js} +1 -1
  5. package/dist/{chunk-IG3H7C7R.js → chunk-ARHCVLQW.js} +4 -4
  6. package/dist/chunk-AWHINKO2.js +244 -0
  7. package/dist/{chunk-7RXZTPXY.js → chunk-BSG5XY3C.js} +6 -6
  8. package/dist/{chunk-AJNKSFHL.js → chunk-CI5E4EX3.js} +80 -5
  9. package/dist/{chunk-4TZOCXAI.js → chunk-ECEWULAA.js} +1 -1
  10. package/dist/chunk-EF66S6ZQ.js +1189 -0
  11. package/dist/chunk-EV5QU5KG.js +18 -0
  12. package/dist/{chunk-45T2VB5R.js → chunk-IZFUFXDN.js} +98 -237
  13. package/dist/{chunk-YK5Z6U5A.js → chunk-LE5EJ6I4.js} +23 -20
  14. package/dist/{chunk-PQS3TQB6.js → chunk-LSLQIPLQ.js} +6 -6
  15. package/dist/chunk-NQBS7L2F.js +55 -0
  16. package/dist/chunk-RAM67KA6.js +57 -0
  17. package/dist/{chunk-Q3GIFHIQ.js → client-PPPOHAVY.js} +4 -3
  18. package/dist/constants-BES4STNW.js +11 -0
  19. package/dist/{execute-HWUL2M3B.js → execute-WSCLLLY6.js} +4 -4
  20. package/dist/geo-IRUGSLZS.js +50 -0
  21. package/dist/index.js +760 -1239
  22. package/dist/{interviewer-ZGKR7YQQ.js → interviewer-ZUYQ5ZFJ.js} +2 -2
  23. package/dist/{keys-3PRAVIRC.js → keys-SQUTA4L2.js} +2 -2
  24. package/dist/{list-7IBMJCCF.js → list-HZM7DNVS.js} +4 -4
  25. package/dist/{parallel-4PXJA2QD.js → parallel-MWPBKEEN.js} +5 -6
  26. package/dist/{plan-HBAUG3KD.js → plan-3Z6M4LE6.js} +3 -3
  27. package/dist/{preferences-VVFGRNPD.js → preferences-OXVXWARL.js} +2 -2
  28. package/dist/{ship-KHL6NVC2.js → ship-CTZU6RYR.js} +1 -1
  29. package/dist/{chunk-ONH6Y3CS.js → tier-selection-5KPN2RF2.js} +7 -28
  30. package/dist/truth-layer-7N32HKCE.js +19 -0
  31. package/package.json +2 -2
  32. package/dist/auth-T4C7OQWO.js +0 -14
  33. package/dist/chunk-57NSGWWD.js +0 -270
  34. package/dist/chunk-CFJECC3B.js +0 -495
  35. package/dist/chunk-GGRW4NTA.js +0 -118
  36. package/dist/chunk-M4LGRTLC.js +0 -10
  37. package/dist/client-PHW2C2HB.js +0 -11
  38. package/dist/constants-XDIWFFPN.js +0 -11
  39. package/dist/geo-BWH5PUBK.js +0 -20
  40. package/dist/tier-selection-O5AFLKD6.js +0 -18
@@ -0,0 +1,1189 @@
1
+ import {
2
+ recordWebCheckResult
3
+ } from "./chunk-3ASILTFB.js";
4
+ import {
5
+ ArchitectAgent
6
+ } from "./chunk-D3TVDCJA.js";
7
+ import {
8
+ loadConfig
9
+ } from "./chunk-NQBS7L2F.js";
10
+
11
+ // src/cli/geo.ts
12
+ import { Command } from "commander";
13
+ import chalk from "chalk";
14
+ import { readFile, writeFile, mkdir } from "fs/promises";
15
+ import { existsSync } from "fs";
16
+ import { join } from "path";
17
+ import { createInterface } from "readline";
18
+ import { glob } from "glob";
19
+ import * as yaml from "yaml";
20
+ var BANNED_WORDS_GENERIC = [
21
+ "comprehensive",
22
+ "leveraging",
23
+ "leverage",
24
+ "synergy",
25
+ "cutting-edge",
26
+ "cutting edge",
27
+ "innovative",
28
+ "leading provider",
29
+ "world-class",
30
+ "world class",
31
+ "best-in-class",
32
+ "best in class",
33
+ "next-generation",
34
+ "next generation",
35
+ "revolutionary",
36
+ "transformative",
37
+ "disruptive",
38
+ "game-changing",
39
+ "game changing",
40
+ "seamless",
41
+ "robust",
42
+ "scalable",
43
+ "enterprise-grade",
44
+ "enterprise grade",
45
+ "end-to-end",
46
+ "end to end",
47
+ "holistic",
48
+ "one-stop-shop",
49
+ "one stop shop",
50
+ "empowering",
51
+ "unlocking"
52
+ ];
53
+ var BANNED_WORDS_AI_WASHING = [
54
+ "AI-native",
55
+ "AI native",
56
+ "AI-powered",
57
+ "AI powered",
58
+ "AI-first",
59
+ "AI first",
60
+ "AI-driven",
61
+ "AI driven",
62
+ "agentic",
63
+ "autonomous AI",
64
+ "intelligent automation",
65
+ "smart AI"
66
+ ];
67
+ var BANNED_WORDS_ALL = [...BANNED_WORDS_GENERIC, ...BANNED_WORDS_AI_WASHING];
68
+ function approxTokenCount(text) {
69
+ const trimmed = text.trim();
70
+ if (!trimmed) return 0;
71
+ const byChars = Math.ceil(trimmed.length / 4);
72
+ const byWords = trimmed.split(/\s+/).length;
73
+ return Math.max(byChars, byWords);
74
+ }
75
+ function findBannedWords(text, list = BANNED_WORDS_ALL) {
76
+ const lower = text.toLowerCase();
77
+ return list.filter((word) => lower.includes(word.toLowerCase()));
78
+ }
79
+ function wordCount(text) {
80
+ return text.trim().split(/\s+/).filter(Boolean).length;
81
+ }
82
+ function splitSentences(text) {
83
+ return text.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter(Boolean);
84
+ }
85
+ function validateSevenWordPhrase(phrase) {
86
+ const issues = [];
87
+ const wc = wordCount(phrase);
88
+ if (wc !== 7) {
89
+ issues.push({ level: "warn", message: `Phrase has ${wc} words, expected 7: "${phrase}"` });
90
+ }
91
+ const tokens = approxTokenCount(phrase);
92
+ if (tokens > 18) {
93
+ issues.push({ level: "error", message: `Phrase exceeds 18 tokens (got ${tokens}): "${phrase}"` });
94
+ }
95
+ const banned = findBannedWords(phrase);
96
+ if (banned.length > 0) {
97
+ issues.push({ level: "error", message: `Banned words in phrase: ${banned.join(", ")}` });
98
+ }
99
+ return issues;
100
+ }
101
+ function validateFiftyWordDescription(description) {
102
+ const issues = [];
103
+ const wc = wordCount(description);
104
+ if (wc < 40 || wc > 60) {
105
+ issues.push({ level: "warn", message: `Description has ${wc} words, expected ~50` });
106
+ }
107
+ const banned = findBannedWords(description);
108
+ if (banned.length > 0) {
109
+ issues.push({ level: "error", message: `Banned words in description: ${banned.join(", ")}` });
110
+ }
111
+ for (const sentence of splitSentences(description)) {
112
+ const tokens = approxTokenCount(sentence);
113
+ if (tokens > 22) {
114
+ issues.push({
115
+ level: "warn",
116
+ message: `Sentence exceeds 18-token citation budget (got ~${tokens}): "${sentence.slice(0, 60)}..."`
117
+ });
118
+ }
119
+ }
120
+ return issues;
121
+ }
122
+ function validateFaqAnswer(answer) {
123
+ const issues = [];
124
+ const banned = findBannedWords(answer);
125
+ if (banned.length > 0) {
126
+ issues.push({ level: "warn", message: `Banned words in answer: ${banned.join(", ")}` });
127
+ }
128
+ for (const sentence of splitSentences(answer)) {
129
+ const tokens = approxTokenCount(sentence);
130
+ if (tokens > 22) {
131
+ issues.push({
132
+ level: "warn",
133
+ message: `FAQ sentence exceeds 18-token citation budget (~${tokens} tokens)`
134
+ });
135
+ }
136
+ }
137
+ return issues;
138
+ }
139
+ var CONFIG_PATH = ".archon/config.yaml";
140
+ function createPrompt() {
141
+ const rl = createInterface({
142
+ input: process.stdin,
143
+ output: process.stdout
144
+ });
145
+ return {
146
+ ask: (question) => new Promise((resolve) => {
147
+ rl.question(question, resolve);
148
+ }),
149
+ close: () => rl.close()
150
+ };
151
+ }
152
+ async function loadGeoConfig(cwd) {
153
+ const configPath = join(cwd, CONFIG_PATH);
154
+ if (!existsSync(configPath)) {
155
+ return {};
156
+ }
157
+ try {
158
+ const content = await readFile(configPath, "utf-8");
159
+ return yaml.parse(content);
160
+ } catch {
161
+ return {};
162
+ }
163
+ }
164
+ async function saveGeoConfig(cwd, config) {
165
+ const configPath = join(cwd, CONFIG_PATH);
166
+ const archonDir = join(cwd, ".archon");
167
+ if (!existsSync(archonDir)) {
168
+ await mkdir(archonDir, { recursive: true });
169
+ }
170
+ let existing = {};
171
+ if (existsSync(configPath)) {
172
+ try {
173
+ const content = await readFile(configPath, "utf-8");
174
+ existing = yaml.parse(content);
175
+ } catch {
176
+ }
177
+ }
178
+ const merged = { ...existing, ...config };
179
+ await writeFile(configPath, yaml.stringify(merged), "utf-8");
180
+ }
181
+ async function readPageContent(patterns) {
182
+ const cwd = process.cwd();
183
+ let content = "";
184
+ for (const pattern of patterns) {
185
+ const files = await glob(pattern, { cwd, ignore: ["**/node_modules/**", "**/dist/**"] });
186
+ for (const file of files.slice(0, 3)) {
187
+ try {
188
+ const fileContent = await readFile(join(cwd, file), "utf-8");
189
+ content += `
190
+ --- File: ${file} ---
191
+ ${fileContent.slice(0, 5e3)}
192
+ `;
193
+ } catch {
194
+ }
195
+ }
196
+ }
197
+ return content || "No content found.";
198
+ }
199
+ async function checkStrongModelAccess() {
200
+ const config = await loadConfig();
201
+ const tier = config.tier ?? "FREE";
202
+ if (tier === "BYOK") {
203
+ return { allowed: true, tier };
204
+ }
205
+ return { allowed: false, tier };
206
+ }
207
+ var IDENTITY_SYSTEM_PROMPT = `You are an Expert Direct-Response Copywriter and GEO/AEO Specialist. You produce brand identity artifacts for two audiences simultaneously:
208
+
209
+ 1. HUMANS \u2014 who must remember the company (memory, preference, trust)
210
+ 2. AI AGENTS \u2014 who must understand the company (clarity, structure, evidence)
211
+
212
+ Both audiences are non-negotiable. If a candidate works for only one, reject it.
213
+
214
+ 7-WORD IDENTITY PHRASE \u2014 strict rules:
215
+ - EXACTLY 7 words. Not 6, not 8.
216
+ - <=18 tokens total (each phrase must survive AI citation compression).
217
+ - Formula: [Action Verb] + [Target Audience or Problem] + [Unique Benefit or Result].
218
+ - No passive voice.
219
+ - No vague corporate filler.
220
+ - Must be a claim ONLY this company can honestly make. Competitor test: if any
221
+ competitor could write the same phrase after one brainstorm, REJECT IT.
222
+
223
+ 50-WORD DESCRIPTION \u2014 strict rules:
224
+ - Approximately 50 words (between 40 and 60). One cohesive paragraph.
225
+ - Structure:
226
+ a) Audience pain or problem (~15 words)
227
+ b) Solution and differentiator (~20 words)
228
+ c) Proof or mechanism (~10 words)
229
+ d) Final promise (~5 words)
230
+ - Each sentence should aim for <=18 tokens so AI agents can quote whole sentences.
231
+ - Contain at least one specific, citable fact (number, named outcome, named integration).
232
+ - No hedging language ("we believe," "we strive to," "designed to").
233
+
234
+ BANNED WORDS (do not use unless you can cite specific evidence in the rationale):
235
+ ${BANNED_WORDS_GENERIC.join(", ")},
236
+ ${BANNED_WORDS_AI_WASHING.join(", ")}
237
+
238
+ QUALITY GATE \u2014 only emit candidates that are:
239
+ - CEO-worthy (no candidate the founder would be embarrassed to read aloud)
240
+ - Differentiated (fails the competitor test? reject)
241
+ - Outcome-focused (not feature-focused)
242
+ - Specific (no abstract benefit language)
243
+
244
+ Output your response as valid JSON exactly matching the requested schema.`;
245
+ var BUSINESS_CONTEXT_PROMPT = `Analyze the source material below as an expert business analyst. Produce a comprehensive business context covering:
246
+ 1. What the business does \u2014 specific products, services, offerings.
247
+ 2. Mission and value proposition \u2014 what makes them distinctive.
248
+ 3. Target audience \u2014 who they serve.
249
+ 4. Market positioning \u2014 how they differentiate.
250
+
251
+ Be concrete. Avoid generic claims. Cite specific evidence found in the source.
252
+
253
+ Output as JSON: { "businessContext": "multi-paragraph prose..." }`;
254
+ var AUDIENCE_CONTEXT_PROMPT = `Analyze the source material plus the prior businessContext. Produce an audience-centric context covering:
255
+ 1. What kind of public surface this is (marketing site, product platform, service, e-commerce, docs).
256
+ 2. Specific industry or vertical.
257
+ 3. Audience needs, pain points, and what problems get solved.
258
+
259
+ Output as JSON: { "audienceContext": "multi-paragraph prose..." }`;
260
+ async function generateContextArtifacts(pageContent) {
261
+ const agent = new ArchitectAgent({ temperature: 0.5 });
262
+ const bizResponse = await agent.client.chat(
263
+ BUSINESS_CONTEXT_PROMPT,
264
+ `Source material:
265
+
266
+ ${pageContent}`,
267
+ { temperature: 0.5, maxTokens: 1500 }
268
+ );
269
+ const bizMatch = bizResponse.content.match(/\{[\s\S]*\}/);
270
+ const businessContext = bizMatch ? JSON.parse(bizMatch[0]).businessContext : bizResponse.content;
271
+ const audResponse = await agent.client.chat(
272
+ AUDIENCE_CONTEXT_PROMPT,
273
+ `Prior businessContext:
274
+ ${businessContext}
275
+
276
+ Source material:
277
+ ${pageContent}`,
278
+ { temperature: 0.5, maxTokens: 1500 }
279
+ );
280
+ const audMatch = audResponse.content.match(/\{[\s\S]*\}/);
281
+ const audienceContext = audMatch ? JSON.parse(audMatch[0]).audienceContext : audResponse.content;
282
+ return { businessContext, audienceContext };
283
+ }
284
+ async function generateIdentityCandidates(businessContext, audienceContext) {
285
+ const agent = new ArchitectAgent({ temperature: 0.8 });
286
+ const prompt = `INPUTS:
287
+
288
+ businessContext:
289
+ ${businessContext}
290
+
291
+ audienceContext:
292
+ ${audienceContext}
293
+
294
+ TASK: Generate EXACTLY 3 candidate 7-word brand phrases and 3 candidate 50-word descriptions. Each candidate must pass the competitor test and the specificity test as defined in the system prompt.
295
+
296
+ Output as JSON:
297
+ {
298
+ "phrases": [
299
+ { "phrase": "exactly seven words go right here", "rationale": "what evidence supports this and why a competitor could not honestly claim it" },
300
+ { "phrase": "another seven word phrase goes here", "rationale": "..." },
301
+ { "phrase": "third seven word phrase goes here", "rationale": "..." }
302
+ ],
303
+ "descriptions": [
304
+ { "description": "approximately 50-word description following the pain/solution/proof/promise structure...", "rationale": "..." },
305
+ { "description": "second variant...", "rationale": "..." },
306
+ { "description": "third variant...", "rationale": "..." }
307
+ ]
308
+ }`;
309
+ const response = await agent.client.chat(
310
+ IDENTITY_SYSTEM_PROMPT,
311
+ prompt,
312
+ { temperature: 0.8, maxTokens: 2500 }
313
+ );
314
+ const jsonMatch = response.content.match(/\{[\s\S]*\}/);
315
+ if (!jsonMatch) {
316
+ throw new Error("Failed to parse AI response");
317
+ }
318
+ const parsed = JSON.parse(jsonMatch[0]);
319
+ parsed.phrases = parsed.phrases.filter((p) => {
320
+ const issues = validateSevenWordPhrase(p.phrase);
321
+ const blocking = issues.filter((i) => i.level === "error");
322
+ if (blocking.length > 0) {
323
+ console.log(chalk.yellow(`[!] Dropping phrase: ${blocking.map((i) => i.message).join("; ")}`));
324
+ return false;
325
+ }
326
+ for (const issue of issues) {
327
+ console.log(chalk.dim(` warn: ${issue.message}`));
328
+ }
329
+ return true;
330
+ });
331
+ parsed.descriptions = parsed.descriptions.filter((d) => {
332
+ const issues = validateFiftyWordDescription(d.description);
333
+ const blocking = issues.filter((i) => i.level === "error");
334
+ if (blocking.length > 0) {
335
+ console.log(chalk.yellow(`[!] Dropping description: ${blocking.map((i) => i.message).join("; ")}`));
336
+ return false;
337
+ }
338
+ for (const issue of issues) {
339
+ console.log(chalk.dim(` warn: ${issue.message}`));
340
+ }
341
+ return true;
342
+ });
343
+ if (parsed.phrases.length === 0 || parsed.descriptions.length === 0) {
344
+ throw new Error("All generated candidates failed validation. Try again, or refine business/audience context.");
345
+ }
346
+ return parsed;
347
+ }
348
+ async function geoIdentity() {
349
+ const cwd = process.cwd();
350
+ const prompt = createPrompt();
351
+ try {
352
+ console.log(chalk.blue("\n\u{1F3AF} GEO Identity Generator\n"));
353
+ const { allowed, tier } = await checkStrongModelAccess();
354
+ if (!allowed) {
355
+ console.log(chalk.yellow(`[!] Your tier (${tier}) uses basic models.`));
356
+ console.log(chalk.dim("For better results, use BYOK mode with your own provider key.\n"));
357
+ }
358
+ console.log(chalk.dim("Reading homepage and about page content...\n"));
359
+ const pageContent = await readPageContent([
360
+ "**/index.html",
361
+ "**/index.{jsx,tsx,astro,svelte,vue}",
362
+ "**/about.{html,jsx,tsx,astro,svelte,vue,md}",
363
+ "**/about/index.{jsx,tsx,astro,svelte,vue}",
364
+ "**/pages/index.{jsx,tsx,astro,svelte,vue}",
365
+ "**/app/page.{jsx,tsx}",
366
+ "README.md"
367
+ ]);
368
+ if (pageContent === "No content found.") {
369
+ console.log(chalk.yellow("No homepage or about page content found."));
370
+ console.log(chalk.dim("Create an index file or README.md first.\n"));
371
+ return;
372
+ }
373
+ console.log(chalk.dim("Step 1/3 \u2014 Generating businessContext and audienceContext artifacts...\n"));
374
+ const { businessContext, audienceContext } = await generateContextArtifacts(pageContent);
375
+ const geoDir = join(cwd, ".archon", "geo");
376
+ if (!existsSync(geoDir)) {
377
+ await mkdir(geoDir, { recursive: true });
378
+ }
379
+ const contextPath = join(geoDir, "context.md");
380
+ const contextDoc = `# GEO Context Artifacts
381
+
382
+ Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
383
+
384
+ ## businessContext
385
+ ${businessContext}
386
+
387
+ ## audienceContext
388
+ ${audienceContext}
389
+ `;
390
+ await writeFile(contextPath, contextDoc, "utf-8");
391
+ console.log(chalk.green(` Saved to .archon/geo/context.md
392
+ `));
393
+ console.log(chalk.dim("Step 2/3 \u2014 Generating identity candidates with AI (with 18-token + competitor-test gates)...\n"));
394
+ const candidates = await generateIdentityCandidates(businessContext, audienceContext);
395
+ console.log(chalk.bold("\u{1F4CC} 7-Word Brand Phrases:\n"));
396
+ candidates.phrases.forEach((p, i) => {
397
+ console.log(` ${chalk.cyan(`${i + 1})`)} ${chalk.bold(p.phrase)}`);
398
+ console.log(` ${chalk.dim(p.rationale)}
399
+ `);
400
+ });
401
+ const phraseChoice = await prompt.ask('Select a phrase (1-3) or "r" to regenerate: ');
402
+ if (phraseChoice.toLowerCase() === "r") {
403
+ console.log(chalk.dim("\nRegenerating... Run the command again.\n"));
404
+ return;
405
+ }
406
+ const phraseIndex = parseInt(phraseChoice, 10) - 1;
407
+ const selectedPhrase = candidates.phrases[phraseIndex];
408
+ if (isNaN(phraseIndex) || phraseIndex < 0 || phraseIndex >= candidates.phrases.length || !selectedPhrase) {
409
+ console.log(chalk.red("Invalid selection."));
410
+ return;
411
+ }
412
+ console.log(chalk.bold("\n\u{1F4DD} 50-Word Descriptions:\n"));
413
+ candidates.descriptions.forEach((d, i) => {
414
+ console.log(` ${chalk.cyan(`${i + 1})`)} ${d.description}`);
415
+ console.log(` ${chalk.dim(d.rationale)}
416
+ `);
417
+ });
418
+ const descChoice = await prompt.ask('Select a description (1-3) or "r" to regenerate: ');
419
+ if (descChoice.toLowerCase() === "r") {
420
+ console.log(chalk.dim("\nRegenerating... Run the command again.\n"));
421
+ return;
422
+ }
423
+ const descIndex = parseInt(descChoice, 10) - 1;
424
+ const selectedDescription = candidates.descriptions[descIndex];
425
+ if (isNaN(descIndex) || descIndex < 0 || descIndex >= candidates.descriptions.length || !selectedDescription) {
426
+ console.log(chalk.red("Invalid selection."));
427
+ return;
428
+ }
429
+ console.log(chalk.dim("\nStep 3/3 \u2014 Saving identity + context artifacts.\n"));
430
+ const geoConfig = {
431
+ geo: {
432
+ identityPhrase: selectedPhrase.phrase,
433
+ shortDescription: selectedDescription.description,
434
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
435
+ businessContext,
436
+ audienceContext
437
+ }
438
+ };
439
+ await saveGeoConfig(cwd, geoConfig);
440
+ console.log(chalk.green("\u2705 Identity saved!\n"));
441
+ console.log(chalk.dim(` Phrase: ${selectedPhrase.phrase}`));
442
+ console.log(chalk.dim(` Description: ${selectedDescription.description.slice(0, 60)}...`));
443
+ console.log(chalk.dim(` Context artifacts: .archon/geo/context.md`));
444
+ console.log();
445
+ console.log(chalk.cyan(`Next steps:`));
446
+ console.log(chalk.dim(` archon geo schema Generate Organization + Service JSON-LD`));
447
+ console.log(chalk.dim(` archon geo faq Generate FAQPage schema`));
448
+ console.log(chalk.dim(` archon geo claims Generate atomic claims (<=18 tokens each)`));
449
+ console.log(chalk.dim(` archon truth-layer init Build a living truth-layer artifact`));
450
+ console.log(chalk.dim(` archon brand memory-map Pressure-test what humans actually remember`));
451
+ console.log(chalk.dim(` archon geo washing-audit Scan existing copy for AI-washing claims`));
452
+ } finally {
453
+ prompt.close();
454
+ }
455
+ }
456
+ async function geoSchema(options) {
457
+ const cwd = process.cwd();
458
+ console.log(chalk.blue("\n\u{1F4E6} JSON-LD Schema Generator\n"));
459
+ const config = await loadGeoConfig(cwd);
460
+ if (!config.geo?.identityPhrase || !config.geo?.shortDescription) {
461
+ console.log(chalk.yellow("No identity defined."));
462
+ console.log(chalk.dim(`Run 'archon geo identity' first.
463
+ `));
464
+ return;
465
+ }
466
+ const { identityPhrase, shortDescription } = config.geo;
467
+ let orgName = identityPhrase.split(" ").slice(0, 2).join(" ");
468
+ try {
469
+ const pkgPath = join(cwd, "package.json");
470
+ if (existsSync(pkgPath)) {
471
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
472
+ if (pkg.name) {
473
+ orgName = pkg.name.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
474
+ }
475
+ }
476
+ } catch {
477
+ }
478
+ const schema = {
479
+ "@context": "https://schema.org",
480
+ "@graph": [
481
+ {
482
+ "@type": "Organization",
483
+ "@id": "#organization",
484
+ name: orgName,
485
+ description: shortDescription,
486
+ slogan: identityPhrase
487
+ },
488
+ {
489
+ "@type": "WebSite",
490
+ "@id": "#website",
491
+ name: orgName,
492
+ description: shortDescription,
493
+ publisher: { "@id": "#organization" }
494
+ },
495
+ {
496
+ "@type": "Service",
497
+ name: orgName,
498
+ description: shortDescription,
499
+ provider: { "@id": "#organization" }
500
+ }
501
+ ]
502
+ };
503
+ const jsonLd = JSON.stringify(schema, null, 2);
504
+ if (options.output) {
505
+ await writeFile(options.output, jsonLd, "utf-8");
506
+ console.log(chalk.green(`\u2705 Schema written to ${options.output}`));
507
+ } else {
508
+ console.log(chalk.bold("Generated JSON-LD:\n"));
509
+ console.log(chalk.cyan(jsonLd));
510
+ }
511
+ if (options.apply) {
512
+ console.log(chalk.dim("\nAttempting to insert into homepage <head>...\n"));
513
+ const indexFiles = await glob("**/index.html", { cwd, ignore: ["**/node_modules/**", "**/dist/**"] });
514
+ if (indexFiles.length === 0) {
515
+ console.log(chalk.yellow("No index.html found. Manually add to your HTML <head>:"));
516
+ console.log(chalk.dim(`<script type="application/ld+json">
517
+ ${jsonLd}
518
+ </script>`));
519
+ return;
520
+ }
521
+ const firstIndexFile = indexFiles[0];
522
+ if (!firstIndexFile) {
523
+ console.log(chalk.yellow("No index.html found."));
524
+ return;
525
+ }
526
+ const indexPath = join(cwd, firstIndexFile);
527
+ let indexContent = await readFile(indexPath, "utf-8");
528
+ const scriptTag = `<script type="application/ld+json">
529
+ ${jsonLd}
530
+ </script>`;
531
+ if (indexContent.includes("application/ld+json")) {
532
+ console.log(chalk.yellow("JSON-LD already exists in index.html. Remove it first or update manually."));
533
+ return;
534
+ }
535
+ indexContent = indexContent.replace("</head>", `${scriptTag}
536
+ </head>`);
537
+ await writeFile(indexPath, indexContent, "utf-8");
538
+ console.log(chalk.green(`\u2705 JSON-LD inserted into ${firstIndexFile}`));
539
+ }
540
+ console.log();
541
+ }
542
+ var FAQ_SYSTEM_PROMPT = `You are a GEO/AEO Specialist generating FAQ content for FAQPage schema markup.
543
+
544
+ Your job is NOT to make the company sound impressive. Your job is to write Q&As that:
545
+ 1. Match what real prospects actually ask (not marketing-team wishful questions)
546
+ 2. Are citable by AI agents in a single quoted sentence
547
+ 3. Contain specific, verifiable facts (numbers, named outcomes, named integrations)
548
+
549
+ QUESTION RULES:
550
+ - 6-8 questions.
551
+ - Always include "What is [brand]?" and "How does [brand] work?"
552
+ - Each question maps to a real buyer search intent (not "Why is your product the best?")
553
+
554
+ ANSWER RULES:
555
+ - 2-3 sentences per answer.
556
+ - Each sentence aims for <=18 tokens (AI citation budget).
557
+ - First sentence must contain the most citable fact in the answer.
558
+ - No unsupported claims. No banned language.
559
+ - No "we are committed to" / "we strive to" / "we believe" hedging.
560
+
561
+ BANNED WORDS (do not use):
562
+ ${BANNED_WORDS_GENERIC.slice(0, 12).join(", ")},
563
+ ${BANNED_WORDS_AI_WASHING.join(", ")}
564
+
565
+ Output your response as valid JSON exactly matching the requested schema.`;
566
+ async function geoFaq(options) {
567
+ const cwd = process.cwd();
568
+ console.log(chalk.blue("\n\u2753 FAQ Schema Generator\n"));
569
+ const config = await loadGeoConfig(cwd);
570
+ if (!config.geo?.identityPhrase || !config.geo?.shortDescription) {
571
+ console.log(chalk.yellow("No identity defined."));
572
+ console.log(chalk.dim(`Run 'archon geo identity' first.
573
+ `));
574
+ return;
575
+ }
576
+ const { identityPhrase, shortDescription } = config.geo;
577
+ const { allowed, tier } = await checkStrongModelAccess();
578
+ if (!allowed) {
579
+ console.log(chalk.yellow(`[!] Your tier (${tier}) uses basic models.`));
580
+ console.log(chalk.dim("For better results, use BYOK mode with your own provider key.\n"));
581
+ }
582
+ console.log(chalk.dim("Generating FAQ content with AI...\n"));
583
+ const agent = new ArchitectAgent({ temperature: 0.7 });
584
+ const prompt = `Generate FAQ content for a product/service with:
585
+ - Brand phrase: "${identityPhrase}"
586
+ - Description: "${shortDescription}"
587
+
588
+ Generate 6-8 FAQs as JSON:
589
+ {
590
+ "faqs": [
591
+ { "question": "What is...?", "answer": "..." },
592
+ ...
593
+ ]
594
+ }`;
595
+ const response = await agent.client.chat(
596
+ FAQ_SYSTEM_PROMPT,
597
+ prompt,
598
+ { temperature: 0.7, maxTokens: 2e3 }
599
+ );
600
+ const jsonMatch = response.content.match(/\{[\s\S]*\}/);
601
+ if (!jsonMatch) {
602
+ console.log(chalk.red("Failed to generate FAQ content."));
603
+ return;
604
+ }
605
+ const parsed = JSON.parse(jsonMatch[0]);
606
+ for (const faq of parsed.faqs) {
607
+ const issues = validateFaqAnswer(faq.answer);
608
+ for (const issue of issues) {
609
+ console.log(chalk.yellow(`[!] FAQ "${faq.question.slice(0, 40)}...": ${issue.message}`));
610
+ }
611
+ }
612
+ const faqSchema = {
613
+ "@context": "https://schema.org",
614
+ "@type": "FAQPage",
615
+ mainEntity: parsed.faqs.map((faq) => ({
616
+ "@type": "Question",
617
+ name: faq.question,
618
+ acceptedAnswer: {
619
+ "@type": "Answer",
620
+ text: faq.answer
621
+ }
622
+ }))
623
+ };
624
+ const jsonLd = JSON.stringify(faqSchema, null, 2);
625
+ if (options.output) {
626
+ await writeFile(options.output, jsonLd, "utf-8");
627
+ console.log(chalk.green(`\u2705 FAQ schema written to ${options.output}`));
628
+ } else {
629
+ console.log(chalk.bold("Generated FAQPage JSON-LD:\n"));
630
+ console.log(chalk.cyan(jsonLd));
631
+ }
632
+ console.log();
633
+ }
634
+ var ATOMIC_CLAIMS_SYSTEM_PROMPT = `You are a GEO/AEO Specialist producing "atomic claims" \u2014 short, citable sentences AI agents can quote whole.
635
+
636
+ ATOMIC CLAIM RULES (each one):
637
+ - <=18 tokens.
638
+ - Self-contained: makes sense without surrounding context.
639
+ - Specific: one verifiable idea per sentence.
640
+ - Outcome-focused: prefer "We cut scheduling time 60% for healthcare teams" over "We help teams be more productive".
641
+ - No banned language. No hedging. No corporate filler.
642
+
643
+ BANNED WORDS (do not use):
644
+ ${BANNED_WORDS_GENERIC.join(", ")},
645
+ ${BANNED_WORDS_AI_WASHING.join(", ")}
646
+
647
+ Output strict JSON.`;
648
+ async function geoClaims(options) {
649
+ const cwd = process.cwd();
650
+ const count = options.count ?? 12;
651
+ console.log(chalk.blue("\n\u{1F9F1} Atomic Claims Generator\n"));
652
+ const config = await loadGeoConfig(cwd);
653
+ if (!config.geo?.identityPhrase || !config.geo?.shortDescription) {
654
+ console.log(chalk.yellow("No identity defined."));
655
+ console.log(chalk.dim(`Run 'archon geo identity' first.
656
+ `));
657
+ return;
658
+ }
659
+ const { identityPhrase, shortDescription, businessContext, audienceContext } = config.geo;
660
+ console.log(chalk.dim(`Generating ${count} atomic claims with AI...
661
+ `));
662
+ const agent = new ArchitectAgent({ temperature: 0.7 });
663
+ const prompt = `INPUTS:
664
+ identityPhrase: "${identityPhrase}"
665
+ shortDescription: "${shortDescription}"
666
+ businessContext: ${businessContext ?? "(not provided)"}
667
+ audienceContext: ${audienceContext ?? "(not provided)"}
668
+
669
+ TASK: Generate ${count} atomic claim sentences. Each <=18 tokens, self-contained, specific, outcome-focused.
670
+
671
+ Output JSON: { "claims": ["sentence one.", "sentence two.", ...] }`;
672
+ const response = await agent.client.chat(
673
+ ATOMIC_CLAIMS_SYSTEM_PROMPT,
674
+ prompt,
675
+ { temperature: 0.7, maxTokens: 1500 }
676
+ );
677
+ const jsonMatch = response.content.match(/\{[\s\S]*\}/);
678
+ if (!jsonMatch) {
679
+ console.log(chalk.red("Failed to parse claims response."));
680
+ return;
681
+ }
682
+ const parsed = JSON.parse(jsonMatch[0]);
683
+ const validated = [];
684
+ for (const claim of parsed.claims) {
685
+ const tokens = approxTokenCount(claim);
686
+ const banned = findBannedWords(claim);
687
+ const warnings = [];
688
+ if (tokens > 22) warnings.push(`~${tokens} tokens (over budget)`);
689
+ if (banned.length > 0) warnings.push(`banned: ${banned.join(", ")}`);
690
+ validated.push({ text: claim, tokens, warnings });
691
+ }
692
+ console.log(chalk.bold("Generated atomic claims:\n"));
693
+ validated.forEach((c, i) => {
694
+ const flag = c.warnings.length > 0 ? chalk.yellow(" [!]") : chalk.green(" \u2713");
695
+ console.log(` ${i + 1}.${flag} ${c.text} ${chalk.dim(`(~${c.tokens} tokens)`)}`);
696
+ for (const w of c.warnings) console.log(chalk.dim(` ${w}`));
697
+ });
698
+ const geoDir = join(cwd, ".archon", "geo");
699
+ if (!existsSync(geoDir)) await mkdir(geoDir, { recursive: true });
700
+ const outputPath = options.output ?? join(geoDir, "ai-claims.json");
701
+ await writeFile(outputPath, JSON.stringify({
702
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
703
+ identityPhrase,
704
+ claims: validated.map((c) => c.text)
705
+ }, null, 2), "utf-8");
706
+ console.log();
707
+ console.log(chalk.green(`\u2705 Saved to ${outputPath}`));
708
+ console.log(chalk.dim(`Place these in the first 15% of relevant pages. Aim for 3+ atomic claims above the fold.`));
709
+ }
710
+ var WASHING_AUDIT_SYSTEM_PROMPT = `You are an AI-washing auditor. Your job is to find claims in public-facing copy that:
711
+ 1. Use AI-washing language (AI-native, AI-powered, AI-driven, agentic, etc.) without specific evidence.
712
+ 2. Use generic category furniture (innovative, world-class, end-to-end, etc.).
713
+ 3. Make capability claims that cannot be cited to specific product behavior.
714
+
715
+ You are NOT a stylistic editor. You are looking for trust-debt \u2014 claims that will catch up with the company.
716
+
717
+ For each problematic claim, produce:
718
+ - The exact phrase
719
+ - Severity: critical | high | medium | low
720
+ - Why this is a problem (one sentence)
721
+ - What evidence the company would need to defend it
722
+ - A rewrite candidate that is defensible (specific, narrow, true)
723
+
724
+ Be direct. Do not soften findings. If a claim is fine, do not list it.
725
+
726
+ Output strict JSON.`;
727
+ async function geoWashingAudit(options) {
728
+ const cwd = process.cwd();
729
+ console.log(chalk.blue("\n\u{1F6A8} AI-Washing Risk Register\n"));
730
+ const surfaces = await detectSurfaces(cwd);
731
+ if (surfaces.length === 0) {
732
+ console.log(chalk.yellow("No public surfaces detected."));
733
+ return [];
734
+ }
735
+ const corpus = surfaces.map((s) => `--- ${s.label} (${s.path}) ---
736
+ ${s.content.slice(0, 4e3)}`).join("\n\n");
737
+ console.log(chalk.dim(`Scanning ${surfaces.length} surface(s)...
738
+ `));
739
+ const agent = new ArchitectAgent({ temperature: 0.3 });
740
+ const prompt = `Audit this public copy for AI-washing and unsupported claims.
741
+
742
+ ${corpus}
743
+
744
+ Output JSON:
745
+ {
746
+ "findings": [
747
+ {
748
+ "phrase": "exact phrase from copy",
749
+ "source": "surface label",
750
+ "severity": "critical|high|medium|low",
751
+ "problem": "one sentence",
752
+ "evidenceRequired": "what would justify this claim",
753
+ "rewriteCandidate": "defensible alternative"
754
+ }
755
+ ]
756
+ }`;
757
+ const response = await agent.client.chat(
758
+ WASHING_AUDIT_SYSTEM_PROMPT,
759
+ prompt,
760
+ { temperature: 0.3, maxTokens: 3e3 }
761
+ );
762
+ const jsonMatch = response.content.match(/\{[\s\S]*\}/);
763
+ if (!jsonMatch) {
764
+ console.log(chalk.red("Failed to parse audit response."));
765
+ return [];
766
+ }
767
+ const parsed = JSON.parse(jsonMatch[0]);
768
+ const findings = parsed.findings ?? [];
769
+ for (const surface of surfaces) {
770
+ for (const ban of BANNED_WORDS_AI_WASHING) {
771
+ if (surface.content.toLowerCase().includes(ban.toLowerCase())) {
772
+ if (!findings.some((f) => f.phrase.toLowerCase().includes(ban.toLowerCase()) && f.source === surface.label)) {
773
+ findings.push({
774
+ phrase: ban,
775
+ source: surface.label,
776
+ severity: "high",
777
+ problem: `AI-washing tell appears in copy without inline evidence.`,
778
+ evidenceRequired: `Cite the specific product behavior or capability that backs "${ban}".`,
779
+ rewriteCandidate: `Replace with the narrow, true description of what the product actually does.`
780
+ });
781
+ }
782
+ }
783
+ }
784
+ }
785
+ if (findings.length === 0) {
786
+ console.log(chalk.green("\u2705 No AI-washing findings."));
787
+ } else {
788
+ const bySeverity = { critical: [], high: [], medium: [], low: [] };
789
+ for (const f of findings) bySeverity[f.severity]?.push(f);
790
+ for (const sev of ["critical", "high", "medium", "low"]) {
791
+ const list = bySeverity[sev] ?? [];
792
+ if (list.length === 0) continue;
793
+ const color = sev === "critical" ? chalk.red : sev === "high" ? chalk.yellow : chalk.dim;
794
+ console.log(color.bold(`
795
+ ${sev.toUpperCase()} (${list.length})`));
796
+ list.forEach((f, i) => {
797
+ console.log(color(` ${i + 1}. "${f.phrase}" \u2014 ${f.source}`));
798
+ console.log(chalk.dim(` Problem: ${f.problem}`));
799
+ console.log(chalk.dim(` Evidence required: ${f.evidenceRequired}`));
800
+ console.log(chalk.dim(` Rewrite: ${f.rewriteCandidate}`));
801
+ });
802
+ }
803
+ }
804
+ const geoDir = join(cwd, ".archon", "geo");
805
+ if (!existsSync(geoDir)) await mkdir(geoDir, { recursive: true });
806
+ const outputPath = options.output ?? join(geoDir, "ai-washing-risk-register.md");
807
+ const md = renderWashingRegister(findings);
808
+ await writeFile(outputPath, md, "utf-8");
809
+ console.log();
810
+ console.log(chalk.green(`\u2705 Risk register saved to ${outputPath}`));
811
+ return findings;
812
+ }
813
+ function renderWashingRegister(findings) {
814
+ const lines = [];
815
+ lines.push(`# AI-Washing Risk Register`);
816
+ lines.push(``);
817
+ lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
818
+ lines.push(``);
819
+ lines.push(`Each finding = a public claim that creates trust-debt unless backed by evidence.`);
820
+ lines.push(``);
821
+ for (const sev of ["critical", "high", "medium", "low"]) {
822
+ const list = findings.filter((f) => f.severity === sev);
823
+ if (list.length === 0) continue;
824
+ lines.push(`## ${sev.toUpperCase()} (${list.length})`);
825
+ lines.push(``);
826
+ for (const f of list) {
827
+ lines.push(`### "${f.phrase}"`);
828
+ lines.push(`- **Source:** ${f.source}`);
829
+ lines.push(`- **Problem:** ${f.problem}`);
830
+ lines.push(`- **Evidence required:** ${f.evidenceRequired}`);
831
+ lines.push(`- **Rewrite candidate:** ${f.rewriteCandidate}`);
832
+ lines.push(``);
833
+ }
834
+ }
835
+ return lines.join("\n");
836
+ }
837
+ async function readFileSafe(path) {
838
+ try {
839
+ return await readFile(path, "utf-8");
840
+ } catch {
841
+ return "";
842
+ }
843
+ }
844
+ async function findFirstFile(cwd, patterns) {
845
+ for (const pattern of patterns) {
846
+ const files = await glob(pattern, { cwd, ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**"] });
847
+ if (files.length > 0) {
848
+ const first = files[0];
849
+ if (!first) continue;
850
+ const fullPath = join(cwd, first);
851
+ const content = await readFileSafe(fullPath);
852
+ if (content.trim()) {
853
+ return { path: first, content };
854
+ }
855
+ }
856
+ }
857
+ return null;
858
+ }
859
+ async function detectSurfaces(cwd) {
860
+ const surfaces = [];
861
+ const candidates = [
862
+ { label: "Homepage", category: "core", patterns: ["**/index.html", "**/src/pages/index.astro", "**/pages/index.{tsx,jsx}", "**/app/page.{tsx,jsx}", "**/src/pages/index.{tsx,jsx,svelte,vue}"] },
863
+ { label: "Pricing", category: "core", patterns: ["**/pricing.{html,astro,tsx,jsx,svelte,vue,md}", "**/pricing/index.*", "**/src/pages/pricing.*"] },
864
+ { label: "Product/Features", category: "core", patterns: ["**/features.*", "**/product.*", "**/src/pages/features.*"] },
865
+ { label: "Customer Stories", category: "evidence", patterns: ["**/customers.*", "**/case-studies/**/*.md", "**/testimonials.*", "**/src/pages/customers.*"] },
866
+ { label: "Comparison", category: "evidence", patterns: ["**/compare.*", "**/vs/**/*.md", "**/alternatives.*", "**/src/pages/compare.*"] },
867
+ { label: "Integrations", category: "technical", patterns: ["**/integrations.*", "**/integrations/**/*.md", "**/src/pages/integrations.*"] },
868
+ { label: "Docs", category: "technical", patterns: ["**/docs.*", "**/docs/index.*", "**/documentation.*", "**/docs/**/README.md"] },
869
+ { label: "Changelog", category: "technical", patterns: ["**/CHANGELOG.md", "**/changelog.*"] },
870
+ { label: "README", category: "narrative", patterns: ["README.md"] },
871
+ { label: "Help Center", category: "technical", patterns: ["**/help.*", "**/support.*", "**/faq.*"] },
872
+ { label: "Blog", category: "narrative", patterns: ["**/blog/**/*.{md,mdx}", "**/posts/**/*.{md,mdx}"] }
873
+ ];
874
+ for (const c of candidates) {
875
+ const found = await findFirstFile(cwd, c.patterns);
876
+ if (found) {
877
+ surfaces.push({ label: c.label, category: c.category, path: found.path, content: found.content });
878
+ }
879
+ }
880
+ return surfaces;
881
+ }
882
+ async function geoQuickAudit() {
883
+ const cwd = process.cwd();
884
+ console.log(chalk.blue("\n\u{1F50D} GEO Audit\n"));
885
+ const result = {
886
+ identityDefined: false,
887
+ phraseInH1: false,
888
+ phraseInMetaDescription: false,
889
+ hasOrganizationSchema: false,
890
+ hasServiceSchema: false,
891
+ hasFAQSchema: false,
892
+ issues: []
893
+ };
894
+ const config = await loadGeoConfig(cwd);
895
+ if (config.geo?.identityPhrase && config.geo?.shortDescription) {
896
+ result.identityDefined = true;
897
+ console.log(chalk.green("\u2705 Identity defined"));
898
+ console.log(chalk.dim(` Phrase: ${config.geo.identityPhrase}`));
899
+ } else {
900
+ result.issues.push('Identity not defined. Run "archon geo identity"');
901
+ console.log(chalk.red("\u274C Identity not defined"));
902
+ }
903
+ const htmlFiles = await glob("**/index.html", { cwd, ignore: ["**/node_modules/**", "**/dist/**"] });
904
+ const firstHtmlFile = htmlFiles[0];
905
+ if (firstHtmlFile && config.geo?.identityPhrase) {
906
+ const indexPath = join(cwd, firstHtmlFile);
907
+ const content = await readFile(indexPath, "utf-8");
908
+ const identityPhrase = config.geo.identityPhrase;
909
+ const firstKeyword = identityPhrase.toLowerCase().split(" ")[0] ?? "";
910
+ const h1Match = content.match(/<h1[^>]*>(.*?)<\/h1>/is);
911
+ if (h1Match?.[1] && h1Match[1].toLowerCase().includes(firstKeyword)) {
912
+ result.phraseInH1 = true;
913
+ console.log(chalk.green("\u2705 Brand keyword in H1"));
914
+ } else {
915
+ result.issues.push("Brand phrase keyword not found in H1");
916
+ console.log(chalk.yellow("[!] Brand keyword not in H1"));
917
+ }
918
+ const metaMatch = content.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["']/i);
919
+ if (metaMatch?.[1] && metaMatch[1].toLowerCase().includes(firstKeyword)) {
920
+ result.phraseInMetaDescription = true;
921
+ console.log(chalk.green("\u2705 Brand keyword in meta description"));
922
+ } else {
923
+ result.issues.push("Brand phrase keyword not found in meta description");
924
+ console.log(chalk.yellow("[!] Brand keyword not in meta description"));
925
+ }
926
+ if (content.includes("application/ld+json")) {
927
+ if (content.includes('"@type":"Organization"') || content.includes('"@type": "Organization"')) {
928
+ result.hasOrganizationSchema = true;
929
+ console.log(chalk.green("\u2705 Organization schema present"));
930
+ } else {
931
+ result.issues.push("Organization schema not found");
932
+ console.log(chalk.yellow("[!] Organization schema missing"));
933
+ }
934
+ if (content.includes('"@type":"Service"') || content.includes('"@type": "Service"')) {
935
+ result.hasServiceSchema = true;
936
+ console.log(chalk.green("\u2705 Service schema present"));
937
+ } else {
938
+ result.issues.push("Service schema not found");
939
+ console.log(chalk.yellow("[!] Service schema missing"));
940
+ }
941
+ if (content.includes('"@type":"FAQPage"') || content.includes('"@type": "FAQPage"')) {
942
+ result.hasFAQSchema = true;
943
+ console.log(chalk.green("\u2705 FAQPage schema present"));
944
+ } else {
945
+ result.issues.push("FAQPage schema not found");
946
+ console.log(chalk.yellow("[!] FAQPage schema missing"));
947
+ }
948
+ } else {
949
+ result.issues.push("No JSON-LD schemas found");
950
+ console.log(chalk.red("\u274C No JSON-LD schemas found"));
951
+ }
952
+ } else if (htmlFiles.length === 0) {
953
+ result.issues.push("No index.html found");
954
+ console.log(chalk.yellow("[!] No index.html found to audit"));
955
+ }
956
+ console.log();
957
+ const passed = result.issues.length === 0;
958
+ if (passed) {
959
+ console.log(chalk.green.bold("\u2705 GEO Audit Passed"));
960
+ } else {
961
+ console.log(chalk.yellow.bold(`[!] ${result.issues.length} issue(s) found`));
962
+ console.log();
963
+ console.log(chalk.bold("Recommendations:"));
964
+ result.issues.forEach((issue, i) => {
965
+ console.log(chalk.dim(` ${i + 1}. ${issue}`));
966
+ });
967
+ }
968
+ console.log();
969
+ await recordWebCheckResult(cwd, "geo", passed);
970
+ return result;
971
+ }
972
+ var SURFACE_AUDIT_SYSTEM_PROMPT = `You are an Agent Clarity Auditor. You read public surfaces from the perspective of an AI agent that must shortlist, summarize, compare, or categorize this company on behalf of a buyer.
973
+
974
+ You are literal. Evidence-driven. Intolerant of vagueness. You build a model of the company from whatever you can find.
975
+
976
+ For EACH surface, produce:
977
+ - inference: 1-3 sentences. What model would an AI build from this surface alone?
978
+ - missing: specific gaps. Not "could be clearer" \u2014 name what is absent.
979
+ - contradictionRisk: where this conflicts with other surfaces. If none, say so.
980
+ - fixPriority: high | medium | low with one-sentence rationale.
981
+
982
+ After per-surface analysis:
983
+ - aggregateCoherence: 2-3 paragraphs. When an agent reads ALL surfaces, what company emerges? Is it one story or several? Be specific about which surfaces pull in different directions.
984
+ - invisibleLossScenarios: 3-5 concrete scenarios. Format: "A buyer asks an AI to [task]. The agent [what happens]. Result: [what the company loses and why]."
985
+ - rankedFixes: top 5 fixes ordered by impact. Name the surface, the current problem, what the improved version looks like.
986
+
987
+ Be direct. Do not soften findings. If a surface is incoherent, say so.
988
+
989
+ Output strict JSON.`;
990
+ async function geoSurfaceAudit(options = {}) {
991
+ const cwd = process.cwd();
992
+ console.log(chalk.blue("\n\u{1F6F0} Agent Clarity Audit (surface walk)\n"));
993
+ const surfaces = await detectSurfaces(cwd);
994
+ const allLabels = ["Homepage", "Pricing", "Product/Features", "Customer Stories", "Comparison", "Integrations", "Docs", "Changelog", "README", "Help Center", "Blog"];
995
+ const detectedLabels = surfaces.map((s) => s.label);
996
+ const blindSpots = allLabels.filter((l) => !detectedLabels.includes(l));
997
+ if (surfaces.length === 0) {
998
+ console.log(chalk.yellow("No public surfaces detected \u2014 nothing to audit."));
999
+ return null;
1000
+ }
1001
+ console.log(chalk.dim(`Detected ${surfaces.length} surface(s): ${detectedLabels.join(", ")}`));
1002
+ if (blindSpots.length > 0) {
1003
+ console.log(chalk.yellow(`Blind spots (not found): ${blindSpots.join(", ")}`));
1004
+ }
1005
+ console.log();
1006
+ console.log(chalk.dim("Running agent-perspective audit...\n"));
1007
+ const corpus = surfaces.map((s) => `--- ${s.label} (${s.path}) ---
1008
+ ${s.content.slice(0, 5e3)}`).join("\n\n");
1009
+ const agent = new ArchitectAgent({ temperature: 0.4 });
1010
+ const prompt = `Audit these public surfaces from an AI agent's perspective.
1011
+
1012
+ ${corpus}
1013
+
1014
+ Detected surfaces: ${detectedLabels.join(", ")}
1015
+ Blind spots (surfaces that do not exist): ${blindSpots.join(", ") || "(none)"}
1016
+
1017
+ For blind spots, add entries with surface label "[Surface] \u2014 BLIND SPOT" explaining what an agent would look for and what the absence implies.
1018
+
1019
+ Output JSON:
1020
+ {
1021
+ "findings": [
1022
+ { "surface": "...", "inference": "...", "missing": "...", "contradictionRisk": "...", "fixPriority": "high|medium|low", "rationale": "..." }
1023
+ ],
1024
+ "aggregateCoherence": "2-3 paragraphs",
1025
+ "invisibleLossScenarios": ["scenario 1", "scenario 2", "scenario 3"],
1026
+ "rankedFixes": [
1027
+ { "surface": "...", "problem": "...", "improvedVersion": "..." }
1028
+ ]
1029
+ }`;
1030
+ const response = await agent.client.chat(
1031
+ SURFACE_AUDIT_SYSTEM_PROMPT,
1032
+ prompt,
1033
+ { temperature: 0.4, maxTokens: 4e3 }
1034
+ );
1035
+ const jsonMatch = response.content.match(/\{[\s\S]*\}/);
1036
+ if (!jsonMatch) {
1037
+ console.log(chalk.red("Failed to parse audit response."));
1038
+ return null;
1039
+ }
1040
+ const parsed = JSON.parse(jsonMatch[0]);
1041
+ const report = {
1042
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1043
+ surfacesAudited: detectedLabels,
1044
+ blindSpots,
1045
+ ...parsed
1046
+ };
1047
+ console.log(chalk.bold("Per-surface findings:\n"));
1048
+ for (const f of report.findings) {
1049
+ const color = f.fixPriority === "high" ? chalk.red : f.fixPriority === "medium" ? chalk.yellow : chalk.dim;
1050
+ console.log(color.bold(` ${f.surface} [${f.fixPriority.toUpperCase()}]`));
1051
+ console.log(chalk.dim(` inference: ${f.inference}`));
1052
+ console.log(chalk.dim(` missing: ${f.missing}`));
1053
+ console.log(chalk.dim(` contradiction risk: ${f.contradictionRisk}`));
1054
+ console.log(chalk.dim(` rationale: ${f.rationale}`));
1055
+ console.log();
1056
+ }
1057
+ console.log(chalk.bold("Aggregate coherence:\n"));
1058
+ console.log(chalk.dim(` ${report.aggregateCoherence}`));
1059
+ console.log();
1060
+ console.log(chalk.bold("Invisible loss scenarios:\n"));
1061
+ report.invisibleLossScenarios.forEach((s, i) => console.log(chalk.dim(` ${i + 1}. ${s}`)));
1062
+ console.log();
1063
+ console.log(chalk.bold("Ranked fixes:\n"));
1064
+ report.rankedFixes.forEach((f, i) => {
1065
+ console.log(` ${i + 1}. ${chalk.cyan(f.surface)}`);
1066
+ console.log(chalk.dim(` Problem: ${f.problem}`));
1067
+ console.log(chalk.dim(` Improved: ${f.improvedVersion}`));
1068
+ });
1069
+ console.log();
1070
+ const geoDir = join(cwd, ".archon", "geo");
1071
+ if (!existsSync(geoDir)) await mkdir(geoDir, { recursive: true });
1072
+ const outputPath = options.output ?? join(geoDir, "surface-audit.md");
1073
+ await writeFile(outputPath, renderSurfaceAudit(report), "utf-8");
1074
+ console.log(chalk.green(`\u2705 Report saved to ${outputPath}`));
1075
+ await recordWebCheckResult(cwd, "geo", report.findings.every((f) => f.fixPriority !== "high"));
1076
+ return report;
1077
+ }
1078
+ function renderSurfaceAudit(report) {
1079
+ const lines = [];
1080
+ lines.push(`# Agent Clarity Audit`);
1081
+ lines.push(``);
1082
+ lines.push(`Generated: ${report.generatedAt}`);
1083
+ lines.push(``);
1084
+ lines.push(`Surfaces audited: ${report.surfacesAudited.join(", ")}`);
1085
+ if (report.blindSpots.length > 0) {
1086
+ lines.push(`Blind spots: ${report.blindSpots.join(", ")}`);
1087
+ }
1088
+ lines.push(``);
1089
+ lines.push(`## Per-Surface Findings`);
1090
+ lines.push(``);
1091
+ for (const f of report.findings) {
1092
+ lines.push(`### ${f.surface} \u2014 ${f.fixPriority.toUpperCase()}`);
1093
+ lines.push(``);
1094
+ lines.push(`- **Inference:** ${f.inference}`);
1095
+ lines.push(`- **Missing:** ${f.missing}`);
1096
+ lines.push(`- **Contradiction risk:** ${f.contradictionRisk}`);
1097
+ lines.push(`- **Rationale:** ${f.rationale}`);
1098
+ lines.push(``);
1099
+ }
1100
+ lines.push(`## Aggregate Coherence`);
1101
+ lines.push(``);
1102
+ lines.push(report.aggregateCoherence);
1103
+ lines.push(``);
1104
+ lines.push(`## Invisible Loss Scenarios`);
1105
+ lines.push(``);
1106
+ report.invisibleLossScenarios.forEach((s, i) => lines.push(`${i + 1}. ${s}`));
1107
+ lines.push(``);
1108
+ lines.push(`## Ranked Fixes`);
1109
+ lines.push(``);
1110
+ report.rankedFixes.forEach((f, i) => {
1111
+ lines.push(`### ${i + 1}. ${f.surface}`);
1112
+ lines.push(`- **Problem:** ${f.problem}`);
1113
+ lines.push(`- **Improved:** ${f.improvedVersion}`);
1114
+ lines.push(``);
1115
+ });
1116
+ return lines.join("\n");
1117
+ }
1118
+ async function geoAudit(options = {}) {
1119
+ if (options.quick) {
1120
+ return geoQuickAudit();
1121
+ }
1122
+ const report = await geoSurfaceAudit();
1123
+ if (!report) {
1124
+ console.log(chalk.dim("Falling back to quick audit (index.html-only)...\n"));
1125
+ return geoQuickAudit();
1126
+ }
1127
+ return report;
1128
+ }
1129
+ function createGeoCommand() {
1130
+ const geo = new Command("geo").description("GEO (Generative Engine Optimization) \u2014 memory for humans, clarity for agents").addHelpText(
1131
+ "after",
1132
+ `
1133
+ Examples:
1134
+ archon geo identity Generate brand identity (7-word + 50-word + context artifacts)
1135
+ archon geo schema Generate Organization + Service JSON-LD
1136
+ archon geo schema --apply Insert JSON-LD into homepage
1137
+ archon geo faq Generate FAQPage schema (with 18-token sentence validation)
1138
+ archon geo claims Generate atomic claims (<=18 tokens each)
1139
+ archon geo audit Full surface walk (homepage, pricing, docs, comparison, etc.)
1140
+ archon geo audit --quick Legacy per-file check (index.html only)
1141
+ archon geo washing-audit Scan public copy for AI-washing claims
1142
+ `
1143
+ );
1144
+ geo.command("identity").description("Generate brand identity + businessContext + audienceContext artifacts").action(async () => {
1145
+ await geoIdentity();
1146
+ });
1147
+ geo.command("schema").description("Generate JSON-LD Organization and Service schemas").option("-o, --output <file>", "Write schema to file").option("-a, --apply", "Insert schema into homepage <head>").action(async (options) => {
1148
+ await geoSchema(options);
1149
+ });
1150
+ geo.command("faq").description("Generate FAQPage JSON-LD schema (sentence-budget validated)").option("-o, --output <file>", "Write FAQ schema to file").action(async (options) => {
1151
+ await geoFaq(options);
1152
+ });
1153
+ geo.command("claims").description("Generate atomic claims (<=18 tokens each) for agent citation").option("-c, --count <n>", "Number of claims to generate", (v) => parseInt(v, 10), 12).option("-o, --output <file>", "Write claims to file (default .archon/geo/ai-claims.json)").action(async (options) => {
1154
+ await geoClaims(options);
1155
+ });
1156
+ geo.command("audit").description("Agent Clarity Audit \u2014 multi-surface walk (default) or per-file quick check").option("--quick", "Run legacy per-file check (index.html only)").action(async (options) => {
1157
+ await geoAudit(options);
1158
+ });
1159
+ geo.command("washing-audit").description("AI-washing risk register \u2014 find unsupported claims in public copy").option("-o, --output <file>", "Write register to file (default .archon/geo/ai-washing-risk-register.md)").action(async (options) => {
1160
+ await geoWashingAudit(options);
1161
+ });
1162
+ geo.action(async () => {
1163
+ await geoAudit();
1164
+ });
1165
+ return geo;
1166
+ }
1167
+
1168
+ export {
1169
+ BANNED_WORDS_GENERIC,
1170
+ BANNED_WORDS_AI_WASHING,
1171
+ BANNED_WORDS_ALL,
1172
+ approxTokenCount,
1173
+ findBannedWords,
1174
+ wordCount,
1175
+ splitSentences,
1176
+ validateSevenWordPhrase,
1177
+ validateFiftyWordDescription,
1178
+ validateFaqAnswer,
1179
+ geoIdentity,
1180
+ geoSchema,
1181
+ geoFaq,
1182
+ geoClaims,
1183
+ geoWashingAudit,
1184
+ detectSurfaces,
1185
+ geoQuickAudit,
1186
+ geoSurfaceAudit,
1187
+ geoAudit,
1188
+ createGeoCommand
1189
+ };