@ainyc/canonry 1.39.4 → 1.40.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/dist/cli.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node --import tsx
2
2
  import {
3
- IntelligenceService,
4
- apiKeys,
3
+ ProviderNames,
4
+ RunKinds,
5
+ computeCompetitorOverlap,
5
6
  configExists,
6
- createClient,
7
7
  createServer,
8
8
  determineAnswerMentioned,
9
+ determineCitationState,
9
10
  effectiveDomains,
11
+ extractRecommendedCompetitors,
10
12
  formatAuditFactorScore,
11
13
  getConfigDir,
12
14
  getConfigPath,
@@ -14,20 +16,29 @@ import {
14
16
  isFirstRun,
15
17
  isTelemetryEnabled,
16
18
  loadConfig,
17
- migrate,
18
19
  notificationEventSchema,
19
- parseJsonColumn,
20
- projects,
21
20
  providerQuotaPolicySchema,
22
- querySnapshots,
21
+ reparseStoredResult,
22
+ reparseStoredResult2,
23
+ reparseStoredResult3,
24
+ reparseStoredResult4,
23
25
  resolveProviderInput,
24
- runs,
25
26
  saveConfig,
26
27
  saveConfigPatch,
27
28
  setGoogleAuthConfig,
28
29
  showFirstRunNotice,
29
30
  trackEvent
30
- } from "./chunk-H6FO4LHC.js";
31
+ } from "./chunk-FXHVGU5S.js";
32
+ import {
33
+ apiKeys,
34
+ competitors,
35
+ createClient,
36
+ migrate,
37
+ parseJsonColumn,
38
+ projects,
39
+ querySnapshots,
40
+ runs
41
+ } from "./chunk-FOWWBLXD.js";
31
42
 
32
43
  // src/cli.ts
33
44
  import { pathToFileURL } from "url";
@@ -107,9 +118,9 @@ import { parseArgs } from "util";
107
118
  function commandId(spec) {
108
119
  return spec.path.join(".");
109
120
  }
110
- function matchesPath(args, path5) {
111
- if (args.length < path5.length) return false;
112
- return path5.every((segment, index) => args[index] === segment);
121
+ function matchesPath(args, path6) {
122
+ if (args.length < path6.length) return false;
123
+ return path6.every((segment, index) => args[index] === segment);
113
124
  }
114
125
  function withFormatOption(options) {
115
126
  if (!options) {
@@ -202,7 +213,7 @@ Usage: ${spec.usage}`, {
202
213
  }
203
214
 
204
215
  // src/commands/backfill.ts
205
- import { eq, inArray } from "drizzle-orm";
216
+ import { and, eq, inArray } from "drizzle-orm";
206
217
  var SNAPSHOT_BATCH_SIZE = 500;
207
218
  async function backfillAnswerVisibilityCommand(opts) {
208
219
  const config = loadConfig();
@@ -213,8 +224,13 @@ async function backfillAnswerVisibilityCommand(opts) {
213
224
  let examined = 0;
214
225
  let updated = 0;
215
226
  let visible = 0;
227
+ let reparsed = 0;
228
+ let providerErrors = 0;
216
229
  if (scopedProjects.length > 0) {
217
- const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(inArray(runs.projectId, scopedProjects.map((project) => project.id))).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).all();
230
+ const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and(
231
+ eq(runs.kind, RunKinds["answer-visibility"]),
232
+ inArray(runs.projectId, scopedProjects.map((project) => project.id))
233
+ )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq(runs.kind, RunKinds["answer-visibility"])).all();
218
234
  const runIdsByProject = /* @__PURE__ */ new Map();
219
235
  for (const run of runRows) {
220
236
  const existing = runIdsByProject.get(run.projectId);
@@ -222,31 +238,95 @@ async function backfillAnswerVisibilityCommand(opts) {
222
238
  else runIdsByProject.set(run.projectId, [run.id]);
223
239
  }
224
240
  for (const project of scopedProjects) {
241
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq(competitors.projectId, project.id)).all().map((row) => row.domain);
225
242
  const runIds = runIdsByProject.get(project.id) ?? [];
226
243
  if (runIds.length === 0) continue;
244
+ const projectDomains = effectiveDomains({
245
+ canonicalDomain: project.canonicalDomain,
246
+ ownedDomains: parseJsonColumn(project.ownedDomains, [])
247
+ });
227
248
  for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
228
249
  const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
229
250
  const snapshotRows = db.select({
230
251
  id: querySnapshots.id,
252
+ provider: querySnapshots.provider,
253
+ citationState: querySnapshots.citationState,
231
254
  answerMentioned: querySnapshots.answerMentioned,
232
- answerText: querySnapshots.answerText
255
+ answerText: querySnapshots.answerText,
256
+ citedDomains: querySnapshots.citedDomains,
257
+ competitorOverlap: querySnapshots.competitorOverlap,
258
+ recommendedCompetitors: querySnapshots.recommendedCompetitors,
259
+ rawResponse: querySnapshots.rawResponse
233
260
  }).from(querySnapshots).where(inArray(querySnapshots.runId, batchRunIds)).all();
261
+ const pendingUpdates = [];
234
262
  for (const snapshot of snapshotRows) {
235
263
  examined++;
236
- const nextValue = determineAnswerMentioned(
237
- snapshot.answerText,
238
- project.displayName,
239
- effectiveDomains({
240
- canonicalDomain: project.canonicalDomain,
241
- ownedDomains: parseJsonColumn(project.ownedDomains, [])
242
- })
243
- );
264
+ const reparsedResult = reparseProviderSnapshot(snapshot.provider, snapshot.rawResponse);
265
+ if (reparsedResult) reparsed++;
266
+ if (reparsedResult?.providerError) providerErrors++;
267
+ const answerText = reparsedResult?.answerText ?? snapshot.answerText ?? "";
268
+ const nextValue = determineAnswerMentioned(answerText, project.displayName, projectDomains);
244
269
  if (nextValue) visible++;
270
+ const nextPatch = {};
245
271
  if (snapshot.answerMentioned !== nextValue) {
246
- db.update(querySnapshots).set({ answerMentioned: nextValue }).where(eq(querySnapshots.id, snapshot.id)).run();
247
- updated++;
272
+ nextPatch.answerMentioned = nextValue;
273
+ }
274
+ if ((snapshot.answerText ?? "") !== answerText) {
275
+ nextPatch.answerText = answerText;
276
+ }
277
+ if (reparsedResult) {
278
+ const normalized = {
279
+ provider: snapshot.provider,
280
+ answerText,
281
+ citedDomains: reparsedResult.citedDomains,
282
+ groundingSources: reparsedResult.groundingSources,
283
+ searchQueries: reparsedResult.searchQueries
284
+ };
285
+ const nextCitationState = determineCitationState(normalized, projectDomains);
286
+ const nextCitedDomains = JSON.stringify(reparsedResult.citedDomains);
287
+ const nextCompetitorOverlap = JSON.stringify(
288
+ computeCompetitorOverlap(normalized, competitorDomains)
289
+ );
290
+ const nextRecommendedCompetitors = JSON.stringify(
291
+ extractRecommendedCompetitors(
292
+ normalized.answerText,
293
+ projectDomains,
294
+ normalized.citedDomains,
295
+ competitorDomains
296
+ )
297
+ );
298
+ const nextRawResponse = stringifyStoredSnapshotEnvelope(
299
+ snapshot.rawResponse,
300
+ reparsedResult
301
+ );
302
+ if (snapshot.citationState !== nextCitationState) {
303
+ nextPatch.citationState = nextCitationState;
304
+ }
305
+ if (snapshot.citedDomains !== nextCitedDomains) {
306
+ nextPatch.citedDomains = nextCitedDomains;
307
+ }
308
+ if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
309
+ nextPatch.competitorOverlap = nextCompetitorOverlap;
310
+ }
311
+ if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
312
+ nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
313
+ }
314
+ if (snapshot.rawResponse !== nextRawResponse) {
315
+ nextPatch.rawResponse = nextRawResponse;
316
+ }
317
+ }
318
+ if (Object.keys(nextPatch).length > 0) {
319
+ pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
248
320
  }
249
321
  }
322
+ if (pendingUpdates.length > 0) {
323
+ db.transaction((tx) => {
324
+ for (const update of pendingUpdates) {
325
+ tx.update(querySnapshots).set(update.patch).where(eq(querySnapshots.id, update.id)).run();
326
+ }
327
+ });
328
+ updated += pendingUpdates.length;
329
+ }
250
330
  }
251
331
  }
252
332
  }
@@ -255,7 +335,9 @@ async function backfillAnswerVisibilityCommand(opts) {
255
335
  projects: scopedProjects.length,
256
336
  examined,
257
337
  updated,
258
- visible
338
+ visible,
339
+ reparsed,
340
+ providerErrors
259
341
  };
260
342
  if (opts?.format === "json") {
261
343
  console.log(JSON.stringify(result, null, 2));
@@ -269,8 +351,11 @@ async function backfillAnswerVisibilityCommand(opts) {
269
351
  console.log(` Examined: ${examined}`);
270
352
  console.log(` Updated: ${updated}`);
271
353
  console.log(` Visible: ${visible}`);
354
+ console.log(` Reparsed: ${reparsed}`);
355
+ console.log(` Errors: ${providerErrors}`);
272
356
  }
273
357
  async function backfillInsightsCommand(project, opts) {
358
+ const { IntelligenceService } = await import("./intelligence-service-PDZOIB7L.js");
274
359
  const config = loadConfig();
275
360
  const db = createClient(config.database);
276
361
  migrate(db);
@@ -305,6 +390,53 @@ Backfill complete.`);
305
390
  console.log(` Skipped: ${result.skipped}`);
306
391
  console.log(` Insights: ${result.totalInsights}`);
307
392
  }
393
+ function reparseProviderSnapshot(provider, rawResponse) {
394
+ const envelope = parseJsonColumn(rawResponse, {});
395
+ const apiResponse = resolveStoredApiResponse(envelope);
396
+ if (!apiResponse) return null;
397
+ switch (provider) {
398
+ case ProviderNames.openai:
399
+ return reparseStoredResult(apiResponse);
400
+ case ProviderNames.claude:
401
+ return reparseStoredResult2(apiResponse);
402
+ case ProviderNames.gemini:
403
+ return reparseStoredResult3(apiResponse);
404
+ case ProviderNames.perplexity:
405
+ return reparseStoredResult4(apiResponse);
406
+ default:
407
+ return null;
408
+ }
409
+ }
410
+ function resolveStoredApiResponse(parsed) {
411
+ const nested = parsed.apiResponse;
412
+ if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
413
+ return nested;
414
+ }
415
+ if (looksLikeProviderApiResponse(parsed)) {
416
+ return parsed;
417
+ }
418
+ return null;
419
+ }
420
+ function looksLikeProviderApiResponse(value) {
421
+ return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
422
+ }
423
+ function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
424
+ const parsed = parseJsonColumn(rawResponse, {});
425
+ const apiResponse = resolveStoredApiResponse(parsed);
426
+ const envelope = apiResponse === parsed ? {} : { ...parsed };
427
+ delete envelope.answerText;
428
+ delete envelope.citedDomains;
429
+ delete envelope.competitorOverlap;
430
+ delete envelope.recommendedCompetitors;
431
+ delete envelope.providerError;
432
+ return JSON.stringify({
433
+ ...envelope,
434
+ groundingSources: reparsed.groundingSources,
435
+ searchQueries: reparsed.searchQueries,
436
+ ...reparsed.providerError ? { providerError: reparsed.providerError } : {},
437
+ ...apiResponse ? { apiResponse } : {}
438
+ });
439
+ }
308
440
 
309
441
  // src/cli-command-helpers.ts
310
442
  function getString(values, key) {
@@ -481,9 +613,9 @@ var ApiClient = class {
481
613
  }
482
614
  return this.probePromise;
483
615
  }
484
- async request(method, path5, body) {
616
+ async request(method, path6, body) {
485
617
  await this.probeBasePath();
486
- const url = `${this.baseUrl}${path5}`;
618
+ const url = `${this.baseUrl}${path6}`;
487
619
  const serializedBody = body != null ? JSON.stringify(body) : void 0;
488
620
  const headers = {
489
621
  "Authorization": `Bearer ${this.apiKey}`,
@@ -550,8 +682,8 @@ var ApiClient = class {
550
682
  async appendKeywords(project, keywords) {
551
683
  await this.request("POST", `/projects/${encodeURIComponent(project)}/keywords`, { keywords });
552
684
  }
553
- async putCompetitors(project, competitors) {
554
- await this.request("PUT", `/projects/${encodeURIComponent(project)}/competitors`, { competitors });
685
+ async putCompetitors(project, competitors2) {
686
+ await this.request("PUT", `/projects/${encodeURIComponent(project)}/competitors`, { competitors: competitors2 });
555
687
  }
556
688
  async listCompetitors(project) {
557
689
  return this.request("GET", `/projects/${encodeURIComponent(project)}/competitors`);
@@ -1509,9 +1641,9 @@ async function gaConnect(project, opts) {
1509
1641
  propertyId: opts.propertyId
1510
1642
  };
1511
1643
  if (opts.keyFile) {
1512
- const fs7 = await import("fs");
1644
+ const fs8 = await import("fs");
1513
1645
  try {
1514
- const content = fs7.readFileSync(opts.keyFile, "utf-8");
1646
+ const content = fs8.readFileSync(opts.keyFile, "utf-8");
1515
1647
  JSON.parse(content);
1516
1648
  body.keyJson = content;
1517
1649
  } catch (e) {
@@ -3259,10 +3391,10 @@ Brand Gap Analysis`);
3259
3391
  console.log(`
3260
3392
  Opportunity Gaps (competitors cited, you're not):`);
3261
3393
  for (const kw of data.gap) {
3262
- const competitors = kw.competitorsCiting.join(", ");
3394
+ const competitors2 = kw.competitorsCiting.join(", ");
3263
3395
  const cons = kw.consistency.totalRuns > 0 ? ` [cited ${kw.consistency.citedRuns}/${kw.consistency.totalRuns} runs]` : "";
3264
3396
  console.log(` \u2022 ${kw.keyword}${cons}`);
3265
- console.log(` Competitors: ${competitors}`);
3397
+ console.log(` Competitors: ${competitors2}`);
3266
3398
  }
3267
3399
  }
3268
3400
  if (data.cited.length > 0) {
@@ -4525,6 +4657,10 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
4525
4657
  }
4526
4658
  ];
4527
4659
 
4660
+ // src/commands/snapshot.ts
4661
+ import fs4 from "fs";
4662
+ import path2 from "path";
4663
+
4528
4664
  // src/snapshot-pdf.ts
4529
4665
  import fs3 from "fs";
4530
4666
  import path from "path";
@@ -4806,8 +4942,8 @@ function renderQueries(pdf, report) {
4806
4942
  for (const result of query.providerResults) {
4807
4943
  const status = result.error ? "error" : result.mentioned ? result.cited ? "mentioned and cited" : "mentioned" : "not mentioned";
4808
4944
  const accuracy = result.describedAccurately === "not-mentioned" ? "" : `; accuracy: ${result.describedAccurately}`;
4809
- const competitors = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
4810
- const line = `${result.displayName}: ${status}${accuracy}${competitors}`;
4945
+ const competitors2 = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
4946
+ const line = `${result.displayName}: ${status}${accuracy}${competitors2}`;
4811
4947
  pdf.bullet(line);
4812
4948
  if (result.error) {
4813
4949
  pdf.paragraph(`Error: ${result.error}`, { size: 9, color: FAIL, lineHeight: 12 });
@@ -4856,6 +4992,13 @@ function wrapText(font, text, size, maxWidth) {
4856
4992
  function getClient16() {
4857
4993
  return createApiClient();
4858
4994
  }
4995
+ function slugify(value) {
4996
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4997
+ }
4998
+ function autoOutputPath(companyName, ext) {
4999
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5000
+ return `${slugify(companyName)}-snapshot-${date}.${ext}`;
5001
+ }
4859
5002
  async function createSnapshotReport(companyName, opts) {
4860
5003
  const client = getClient16();
4861
5004
  const report = await client.createSnapshot({
@@ -4864,23 +5007,110 @@ async function createSnapshotReport(companyName, opts) {
4864
5007
  ...opts.phrases && opts.phrases.length > 0 ? { phrases: opts.phrases } : {},
4865
5008
  ...opts.competitors && opts.competitors.length > 0 ? { competitors: opts.competitors } : {}
4866
5009
  });
5010
+ let savedMdPath;
5011
+ if (opts.md) {
5012
+ const mdPath = opts.outputPath ?? autoOutputPath(companyName, "md");
5013
+ savedMdPath = writeSnapshotMarkdown(report, mdPath);
5014
+ }
4867
5015
  let savedPdfPath;
4868
5016
  if (opts.pdf) {
4869
- savedPdfPath = await writeSnapshotPdf(report, opts.pdf);
5017
+ const pdfPath = opts.outputPath && !opts.md ? opts.outputPath : autoOutputPath(companyName, "pdf");
5018
+ savedPdfPath = await writeSnapshotPdf(report, pdfPath);
4870
5019
  }
4871
5020
  if (opts.format === "json") {
4872
5021
  console.log(JSON.stringify(report, null, 2));
4873
- if (savedPdfPath) {
4874
- process.stderr.write(`Saved PDF: ${savedPdfPath}
5022
+ if (savedMdPath) process.stderr.write(`Saved markdown: ${savedMdPath}
5023
+ `);
5024
+ if (savedPdfPath) process.stderr.write(`Saved PDF: ${savedPdfPath}
4875
5025
  `);
4876
- }
4877
5026
  return;
4878
5027
  }
4879
5028
  console.log(formatSnapshotText(report));
4880
- if (savedPdfPath) {
4881
- console.log(`
5029
+ if (savedMdPath) console.log(`
5030
+ Markdown saved: ${savedMdPath}`);
5031
+ if (savedPdfPath) console.log(`
4882
5032
  PDF saved: ${savedPdfPath}`);
5033
+ }
5034
+ function writeSnapshotMarkdown(report, outputPath) {
5035
+ const resolvedPath = path2.resolve(outputPath);
5036
+ fs4.mkdirSync(path2.dirname(resolvedPath), { recursive: true });
5037
+ fs4.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
5038
+ return resolvedPath;
5039
+ }
5040
+ function formatSnapshotMarkdown(report) {
5041
+ const lines = [];
5042
+ lines.push(`# AI Perception Snapshot: ${report.companyName}`);
5043
+ lines.push("");
5044
+ lines.push(`**Domain:** ${report.domain}`);
5045
+ lines.push(`**Generated:** ${new Date(report.generatedAt).toLocaleString("en-US", { year: "numeric", month: "long", day: "numeric", hour: "numeric", minute: "2-digit" })}`);
5046
+ lines.push(`**AEO Audit Score:** ${report.audit.overallScore}/100 (${report.audit.overallGrade})`);
5047
+ lines.push("");
5048
+ lines.push("## Visibility Gap");
5049
+ lines.push("");
5050
+ lines.push(report.summary.visibilityGap);
5051
+ lines.push("");
5052
+ if (report.summary.whatThisMeans.length > 0) {
5053
+ lines.push("## What This Means");
5054
+ lines.push("");
5055
+ for (const item of report.summary.whatThisMeans) {
5056
+ lines.push(`- ${item}`);
5057
+ }
5058
+ lines.push("");
5059
+ }
5060
+ if (report.summary.recommendedActions.length > 0) {
5061
+ lines.push("## Recommended Actions");
5062
+ lines.push("");
5063
+ for (const action of report.summary.recommendedActions) {
5064
+ lines.push(`- ${action}`);
5065
+ }
5066
+ lines.push("");
5067
+ }
5068
+ if (report.summary.topCompetitors.length > 0) {
5069
+ lines.push("## Competitors AI Recommends Instead");
5070
+ lines.push("");
5071
+ lines.push("| Competitor | Mentions |");
5072
+ lines.push("|------------|----------|");
5073
+ for (const entry of report.summary.topCompetitors) {
5074
+ lines.push(`| ${entry.name} | ${entry.count} |`);
5075
+ }
5076
+ lines.push("");
5077
+ }
5078
+ lines.push("## Provider Comparison");
5079
+ lines.push("");
5080
+ for (const query of report.queryResults) {
5081
+ lines.push(`### "${query.phrase}"`);
5082
+ lines.push("");
5083
+ lines.push("| Provider | Mentioned | Cited | Accuracy | Competitors Recommended |");
5084
+ lines.push("|----------|-----------|-------|----------|------------------------|");
5085
+ for (const result of query.providerResults) {
5086
+ if (result.error) {
5087
+ lines.push(`| ${result.displayName} | ERROR | - | - | ${result.error} |`);
5088
+ continue;
5089
+ }
5090
+ const mentioned = result.mentioned ? "Yes" : "No";
5091
+ const cited = result.cited ? "Yes" : "No";
5092
+ const accuracy = result.describedAccurately === "not-mentioned" ? "-" : result.describedAccurately;
5093
+ const competitors2 = result.recommendedCompetitors.length > 0 ? result.recommendedCompetitors.join(", ") : "-";
5094
+ lines.push(`| ${result.displayName} | ${mentioned} | ${cited} | ${accuracy} | ${competitors2} |`);
5095
+ }
5096
+ lines.push("");
5097
+ }
5098
+ if (report.audit.factors.length > 0) {
5099
+ lines.push("## Audit Factors");
5100
+ lines.push("");
5101
+ lines.push(report.audit.summary);
5102
+ lines.push("");
5103
+ lines.push("| Factor | Score | Weight | Status |");
5104
+ lines.push("|--------|-------|--------|--------|");
5105
+ const sorted = [...report.audit.factors].sort((a, b) => a.score - b.score);
5106
+ for (const factor of sorted) {
5107
+ lines.push(`| ${factor.name} | ${factor.score} | ${factor.weight} | ${factor.status} |`);
5108
+ }
5109
+ lines.push("");
4883
5110
  }
5111
+ lines.push("---");
5112
+ lines.push(`*Generated by [Canonry](https://github.com/AINYC/canonry)*`);
5113
+ return lines.join("\n");
4884
5114
  }
4885
5115
  function formatSnapshotText(report) {
4886
5116
  const lines = [];
@@ -4948,15 +5178,17 @@ function parseCsvOption(value) {
4948
5178
  var SNAPSHOT_CLI_COMMANDS = [
4949
5179
  {
4950
5180
  path: ["snapshot"],
4951
- usage: 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--pdf <path>] [--format table|json]',
5181
+ usage: 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--md] [--output <path>] [--pdf] [--format table|json]',
4952
5182
  options: {
4953
5183
  domain: stringOption(),
4954
5184
  phrases: stringOption(),
4955
5185
  competitors: stringOption(),
4956
- pdf: stringOption()
5186
+ md: { type: "boolean" },
5187
+ pdf: { type: "boolean" },
5188
+ output: stringOption()
4957
5189
  },
4958
5190
  run: async (input) => {
4959
- const usage = 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--pdf <path>] [--format table|json]';
5191
+ const usage = 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--md] [--output <path>] [--pdf] [--format table|json]';
4960
5192
  const companyName = requirePositional(input, 0, {
4961
5193
  command: "snapshot",
4962
5194
  usage,
@@ -4967,11 +5199,17 @@ var SNAPSHOT_CLI_COMMANDS = [
4967
5199
  usage,
4968
5200
  message: "--domain is required"
4969
5201
  });
5202
+ const outputPath = getString(input.values, "output");
5203
+ const explicitMd = getBoolean(input.values, "md");
5204
+ const wantsPdf = getBoolean(input.values, "pdf");
5205
+ const wantsMd = explicitMd || !!outputPath && !wantsPdf;
4970
5206
  await createSnapshotReport(companyName, {
4971
5207
  domain,
4972
5208
  phrases: parseCsvOption(getString(input.values, "phrases")),
4973
5209
  competitors: parseCsvOption(getString(input.values, "competitors")),
4974
- pdf: getString(input.values, "pdf"),
5210
+ md: wantsMd,
5211
+ pdf: wantsPdf,
5212
+ outputPath,
4975
5213
  format: input.format
4976
5214
  });
4977
5215
  }
@@ -5100,7 +5338,7 @@ var INTELLIGENCE_CLI_COMMANDS = [
5100
5338
 
5101
5339
  // src/commands/bootstrap.ts
5102
5340
  import crypto from "crypto";
5103
- import path2 from "path";
5341
+ import path3 from "path";
5104
5342
  import { eq as eq2 } from "drizzle-orm";
5105
5343
 
5106
5344
  // ../config/src/index.ts
@@ -5247,7 +5485,7 @@ async function bootstrapCommand(_opts) {
5247
5485
  );
5248
5486
  }
5249
5487
  const configDir = getConfigDir();
5250
- const databasePath = env.databasePath || path2.join(configDir, "data.db");
5488
+ const databasePath = env.databasePath || path3.join(configDir, "data.db");
5251
5489
  const existing = configExists();
5252
5490
  const existingConfig = existing ? loadConfig() : void 0;
5253
5491
  let rawApiKey;
@@ -5317,10 +5555,10 @@ async function bootstrapCommand(_opts) {
5317
5555
 
5318
5556
  // src/commands/daemon.ts
5319
5557
  import { spawn } from "child_process";
5320
- import fs4 from "fs";
5321
- import path3 from "path";
5558
+ import fs5 from "fs";
5559
+ import path4 from "path";
5322
5560
  function getPidPath() {
5323
- return path3.join(getConfigDir(), "canonry.pid");
5561
+ return path4.join(getConfigDir(), "canonry.pid");
5324
5562
  }
5325
5563
  function isProcessAlive(pid) {
5326
5564
  try {
@@ -5347,8 +5585,8 @@ async function waitForReady(host, port, maxMs = 1e4) {
5347
5585
  async function startDaemon(opts) {
5348
5586
  const pidPath = getPidPath();
5349
5587
  const format = opts.format ?? "text";
5350
- if (fs4.existsSync(pidPath)) {
5351
- const existingPid = parseInt(fs4.readFileSync(pidPath, "utf-8").trim(), 10);
5588
+ if (fs5.existsSync(pidPath)) {
5589
+ const existingPid = parseInt(fs5.readFileSync(pidPath, "utf-8").trim(), 10);
5352
5590
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
5353
5591
  throw new CliError({
5354
5592
  code: "DAEMON_ALREADY_RUNNING",
@@ -5359,9 +5597,9 @@ async function startDaemon(opts) {
5359
5597
  }
5360
5598
  });
5361
5599
  }
5362
- fs4.unlinkSync(pidPath);
5600
+ fs5.unlinkSync(pidPath);
5363
5601
  }
5364
- const cliPath = path3.resolve(new URL(import.meta.url).pathname);
5602
+ const cliPath = path4.resolve(new URL(import.meta.url).pathname);
5365
5603
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
5366
5604
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
5367
5605
  if (opts.port) args.push("--port", opts.port);
@@ -5380,10 +5618,10 @@ async function startDaemon(opts) {
5380
5618
  });
5381
5619
  }
5382
5620
  const configDir = getConfigDir();
5383
- if (!fs4.existsSync(configDir)) {
5384
- fs4.mkdirSync(configDir, { recursive: true });
5621
+ if (!fs5.existsSync(configDir)) {
5622
+ fs5.mkdirSync(configDir, { recursive: true });
5385
5623
  }
5386
- fs4.writeFileSync(pidPath, String(child.pid), "utf-8");
5624
+ fs5.writeFileSync(pidPath, String(child.pid), "utf-8");
5387
5625
  const port = opts.port ?? "4100";
5388
5626
  const host = opts.host ?? "127.0.0.1";
5389
5627
  if (format !== "json") {
@@ -5392,7 +5630,7 @@ async function startDaemon(opts) {
5392
5630
  const ready = await waitForReady(host, port);
5393
5631
  if (!ready) {
5394
5632
  try {
5395
- fs4.unlinkSync(pidPath);
5633
+ fs5.unlinkSync(pidPath);
5396
5634
  } catch {
5397
5635
  }
5398
5636
  throw new CliError({
@@ -5424,7 +5662,7 @@ async function startDaemon(opts) {
5424
5662
  }
5425
5663
  function stopDaemon(format = "text") {
5426
5664
  const pidPath = getPidPath();
5427
- if (!fs4.existsSync(pidPath)) {
5665
+ if (!fs5.existsSync(pidPath)) {
5428
5666
  if (format === "json") {
5429
5667
  console.log(JSON.stringify({
5430
5668
  stopped: false,
@@ -5435,7 +5673,7 @@ function stopDaemon(format = "text") {
5435
5673
  console.log("Canonry is not running (no PID file found)");
5436
5674
  return;
5437
5675
  }
5438
- const pid = parseInt(fs4.readFileSync(pidPath, "utf-8").trim(), 10);
5676
+ const pid = parseInt(fs5.readFileSync(pidPath, "utf-8").trim(), 10);
5439
5677
  if (isNaN(pid)) {
5440
5678
  if (format === "json") {
5441
5679
  console.log(JSON.stringify({
@@ -5446,7 +5684,7 @@ function stopDaemon(format = "text") {
5446
5684
  } else {
5447
5685
  console.error("Invalid PID file. Removing it.");
5448
5686
  }
5449
- fs4.unlinkSync(pidPath);
5687
+ fs5.unlinkSync(pidPath);
5450
5688
  return;
5451
5689
  }
5452
5690
  if (!isProcessAlive(pid)) {
@@ -5460,12 +5698,12 @@ function stopDaemon(format = "text") {
5460
5698
  } else {
5461
5699
  console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
5462
5700
  }
5463
- fs4.unlinkSync(pidPath);
5701
+ fs5.unlinkSync(pidPath);
5464
5702
  return;
5465
5703
  }
5466
5704
  try {
5467
5705
  process.kill(pid, "SIGTERM");
5468
- fs4.unlinkSync(pidPath);
5706
+ fs5.unlinkSync(pidPath);
5469
5707
  if (format === "json") {
5470
5708
  console.log(JSON.stringify({
5471
5709
  stopped: true,
@@ -5489,9 +5727,9 @@ function stopDaemon(format = "text") {
5489
5727
 
5490
5728
  // src/commands/init.ts
5491
5729
  import crypto2 from "crypto";
5492
- import fs5 from "fs";
5730
+ import fs6 from "fs";
5493
5731
  import readline from "readline";
5494
- import path4 from "path";
5732
+ import path5 from "path";
5495
5733
  function prompt(question) {
5496
5734
  const rl = readline.createInterface({
5497
5735
  input: process.stdin,
@@ -5528,8 +5766,8 @@ async function initCommand(opts) {
5528
5766
  return;
5529
5767
  }
5530
5768
  const configDir = getConfigDir();
5531
- if (!fs5.existsSync(configDir)) {
5532
- fs5.mkdirSync(configDir, { recursive: true });
5769
+ if (!fs6.existsSync(configDir)) {
5770
+ fs6.mkdirSync(configDir, { recursive: true });
5533
5771
  }
5534
5772
  const bootstrapEnv = getBootstrapEnv(process.env, {
5535
5773
  GEMINI_API_KEY: opts?.geminiKey,
@@ -5644,7 +5882,7 @@ async function initCommand(opts) {
5644
5882
  const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
5645
5883
  const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
5646
5884
  const keyPrefix = rawApiKey.slice(0, 9);
5647
- const databasePath = path4.join(configDir, "data.db");
5885
+ const databasePath = path5.join(configDir, "data.db");
5648
5886
  const db = createClient(databasePath);
5649
5887
  migrate(db);
5650
5888
  db.insert(apiKeys).values({
@@ -6006,7 +6244,7 @@ var SYSTEM_CLI_COMMANDS = [
6006
6244
  ];
6007
6245
 
6008
6246
  // src/cli-commands/wordpress.ts
6009
- import fs6 from "fs";
6247
+ import fs7 from "fs";
6010
6248
 
6011
6249
  // src/commands/wordpress.ts
6012
6250
  function getClient17() {
@@ -6242,12 +6480,12 @@ async function wordpressSetMeta(project, body) {
6242
6480
  printPageDetail(result);
6243
6481
  }
6244
6482
  async function wordpressBulkSetMeta(project, opts) {
6245
- const fs7 = await import("fs/promises");
6246
- const path5 = await import("path");
6247
- const filePath = path5.resolve(opts.from);
6483
+ const fs8 = await import("fs/promises");
6484
+ const path6 = await import("path");
6485
+ const filePath = path6.resolve(opts.from);
6248
6486
  let raw;
6249
6487
  try {
6250
- raw = await fs7.readFile(filePath, "utf8");
6488
+ raw = await fs8.readFile(filePath, "utf8");
6251
6489
  } catch {
6252
6490
  throw new CliError({
6253
6491
  code: "FILE_READ_ERROR",
@@ -6344,13 +6582,13 @@ async function wordpressSetSchema(project, body) {
6344
6582
  printManualAssist(`Schema update for "${body.slug}"`, result);
6345
6583
  }
6346
6584
  async function wordpressSchemaDeploy(project, opts) {
6347
- const fs7 = await import("fs/promises");
6348
- const path5 = await import("path");
6585
+ const fs8 = await import("fs/promises");
6586
+ const path6 = await import("path");
6349
6587
  const yaml = await import("yaml").catch(() => null);
6350
- const filePath = path5.resolve(opts.profile);
6588
+ const filePath = path6.resolve(opts.profile);
6351
6589
  let raw;
6352
6590
  try {
6353
- raw = await fs7.readFile(filePath, "utf8");
6591
+ raw = await fs8.readFile(filePath, "utf8");
6354
6592
  } catch {
6355
6593
  throw new CliError({
6356
6594
  code: "FILE_READ_ERROR",
@@ -6455,13 +6693,13 @@ async function wordpressOnboard(project, opts) {
6455
6693
  }
6456
6694
  let profileData;
6457
6695
  if (opts.profile) {
6458
- const fs7 = await import("fs/promises");
6459
- const path5 = await import("path");
6696
+ const fs8 = await import("fs/promises");
6697
+ const path6 = await import("path");
6460
6698
  const yaml = await import("yaml").catch(() => null);
6461
- const filePath = path5.resolve(opts.profile);
6699
+ const filePath = path6.resolve(opts.profile);
6462
6700
  let raw;
6463
6701
  try {
6464
- raw = await fs7.readFile(filePath, "utf8");
6702
+ raw = await fs8.readFile(filePath, "utf8");
6465
6703
  } catch {
6466
6704
  throw new CliError({
6467
6705
  code: "FILE_READ_ERROR",
@@ -6610,7 +6848,7 @@ function resolveContent(input, command, usage, options) {
6610
6848
  }
6611
6849
  if (contentFile) {
6612
6850
  try {
6613
- return fs6.readFileSync(contentFile, "utf-8");
6851
+ return fs7.readFileSync(contentFile, "utf-8");
6614
6852
  } catch (error) {
6615
6853
  const message = error instanceof Error ? error.message : String(error);
6616
6854
  throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {