@ainyc/canonry 4.34.0 → 4.36.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,26 +1,27 @@
1
1
  #!/usr/bin/env node --import tsx
2
2
  import {
3
+ backfillAiReferralPaths,
4
+ backfillAiReferralPathsCommand,
5
+ backfillAnswerMentionsCommand,
6
+ backfillAnswerVisibilityCommand,
7
+ backfillInsightsCommand,
8
+ backfillNormalizedPaths,
9
+ backfillNormalizedPathsCommand,
10
+ checkLatestVersionForCli,
3
11
  coerceAgentProvider,
4
- computeCompetitorOverlap,
5
12
  createServer,
6
13
  detectAndTrackUpgrade,
7
- determineCitationState,
8
- extractRecommendedCompetitors,
9
14
  formatAuditFactorScore,
10
15
  getOrCreateAnonymousId,
11
16
  isFirstRun,
12
17
  isTelemetryEnabled,
13
18
  listAgentProviders,
14
19
  renderReportHtml,
15
- reparseStoredResult,
16
- reparseStoredResult2,
17
- reparseStoredResult3,
18
- reparseStoredResult4,
19
20
  setGoogleAuthConfig,
20
21
  setTelemetrySource,
21
22
  showFirstRunNotice,
22
23
  trackEvent
23
- } from "./chunk-7AF6B3L6.js";
24
+ } from "./chunk-F2G67CIU.js";
24
25
  import {
25
26
  CliError,
26
27
  EXIT_SYSTEM_ERROR,
@@ -36,42 +37,33 @@ import {
36
37
  saveConfig,
37
38
  saveConfigPatch,
38
39
  usageError
39
- } from "./chunk-5EBN7736.js";
40
+ } from "./chunk-O7S623DL.js";
40
41
  import {
41
42
  apiKeys,
42
- competitors,
43
43
  createClient,
44
- gaAiReferrals,
45
- gaTrafficSnapshots,
46
44
  migrate,
47
- parseJsonColumn,
48
45
  projects,
49
- queries,
50
- querySnapshots,
51
- runs
52
- } from "./chunk-7256SFYT.js";
46
+ queries
47
+ } from "./chunk-JQQXMCQ7.js";
53
48
  import {
54
49
  CcReleaseSyncStatuses,
55
50
  CheckScopes,
56
51
  CheckStatuses,
57
52
  CitationStates,
58
53
  CodingAgents,
59
- ProviderNames,
60
- RunKinds,
61
54
  RunStatuses,
62
55
  SkillsClients,
63
56
  TrafficEventKinds,
64
- determineAnswerMentioned,
65
57
  discoveryBucketSchema,
66
58
  discoveryCompetitorTypeSchema,
67
59
  effectiveDomains,
68
60
  formatRunErrorOneLine,
69
- normalizeUrlPath,
61
+ normalizeProjectAliases,
70
62
  notificationEventSchema,
71
63
  providerQuotaPolicySchema,
72
64
  resolveProviderInput,
73
65
  skillsClientSchema
74
- } from "./chunk-XW3F5EEW.js";
66
+ } from "./chunk-XJVYVURK.js";
75
67
 
76
68
  // src/cli.ts
77
69
  import { pathToFileURL } from "url";
@@ -121,17 +113,11 @@ function matchesPath(args, path10) {
121
113
  if (args.length < path10.length) return false;
122
114
  return path10.every((segment, index) => args[index] === segment);
123
115
  }
124
- function withFormatOption(options) {
125
- if (!options) {
126
- return {
127
- format: { type: "string" }
128
- };
129
- }
130
- if ("format" in options) return options;
131
- return {
132
- ...options,
133
- format: { type: "string" }
134
- };
116
+ function withGlobalOptions(options) {
117
+ const base = options ? { ...options } : {};
118
+ if (!("format" in base)) base.format = { type: "string" };
119
+ if (!("dry-run" in base)) base["dry-run"] = { type: "boolean" };
120
+ return base;
135
121
  }
136
122
  function toFormat(value, fallbackFormat) {
137
123
  return value === "json" ? "json" : fallbackFormat;
@@ -187,7 +173,7 @@ Usage: ${single.usage}
187
173
  try {
188
174
  const parsed = parseArgs({
189
175
  args: remainingArgs,
190
- options: withFormatOption(spec.options),
176
+ options: withGlobalOptions(spec.options),
191
177
  allowPositionals: spec.allowPositionals ?? true
192
178
  });
193
179
  values = parsed.values;
@@ -203,509 +189,25 @@ Usage: ${spec.usage}`, {
203
189
  }
204
190
  });
205
191
  }
192
+ const dryRunRequested = values["dry-run"] === true;
193
+ if (dryRunRequested && !spec.supportsDryRun) {
194
+ throw usageError(
195
+ `Command "${spec.path.join(" ")}" does not support --dry-run. Either drop the flag, or run a different command that supports preview mode (e.g. \`canonry doctor\` for health checks, \`canonry status <project>\` for read-only state).`,
196
+ {
197
+ message: `Command "${spec.path.join(" ")}" does not support --dry-run`,
198
+ details: { command: commandId(spec), usage: spec.usage }
199
+ }
200
+ );
201
+ }
206
202
  await spec.run({
207
203
  positionals,
208
204
  values,
209
- format: toFormat(values.format, fallbackFormat)
205
+ format: toFormat(values.format, fallbackFormat),
206
+ dryRun: dryRunRequested
210
207
  });
211
208
  return true;
212
209
  }
213
210
 
214
- // src/commands/backfill.ts
215
- import { and, eq, inArray } from "drizzle-orm";
216
- var SNAPSHOT_BATCH_SIZE = 500;
217
- async function backfillAnswerVisibilityCommand(opts) {
218
- const config = loadConfig();
219
- const db = createClient(config.database);
220
- migrate(db);
221
- const projectFilter = opts?.project?.trim();
222
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq(projects.name, projectFilter)).all() : db.select().from(projects).all();
223
- let examined = 0;
224
- let updated = 0;
225
- let mentioned = 0;
226
- let reparsed = 0;
227
- let providerErrors = 0;
228
- if (scopedProjects.length > 0) {
229
- const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and(
230
- eq(runs.kind, RunKinds["answer-visibility"]),
231
- inArray(runs.projectId, scopedProjects.map((project) => project.id))
232
- )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq(runs.kind, RunKinds["answer-visibility"])).all();
233
- const runIdsByProject = /* @__PURE__ */ new Map();
234
- for (const run of runRows) {
235
- const existing = runIdsByProject.get(run.projectId);
236
- if (existing) existing.push(run.id);
237
- else runIdsByProject.set(run.projectId, [run.id]);
238
- }
239
- for (const project of scopedProjects) {
240
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq(competitors.projectId, project.id)).all().map((row) => row.domain);
241
- const runIds = runIdsByProject.get(project.id) ?? [];
242
- if (runIds.length === 0) continue;
243
- const projectDomains = effectiveDomains({
244
- canonicalDomain: project.canonicalDomain,
245
- ownedDomains: parseJsonColumn(project.ownedDomains, [])
246
- });
247
- for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
248
- const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
249
- const snapshotRows = db.select({
250
- id: querySnapshots.id,
251
- provider: querySnapshots.provider,
252
- citationState: querySnapshots.citationState,
253
- answerMentioned: querySnapshots.answerMentioned,
254
- answerText: querySnapshots.answerText,
255
- citedDomains: querySnapshots.citedDomains,
256
- competitorOverlap: querySnapshots.competitorOverlap,
257
- recommendedCompetitors: querySnapshots.recommendedCompetitors,
258
- rawResponse: querySnapshots.rawResponse
259
- }).from(querySnapshots).where(inArray(querySnapshots.runId, batchRunIds)).all();
260
- const pendingUpdates = [];
261
- for (const snapshot of snapshotRows) {
262
- examined++;
263
- const reparsedResult = reparseProviderSnapshot(snapshot.provider, snapshot.rawResponse);
264
- if (reparsedResult) reparsed++;
265
- if (reparsedResult?.providerError) providerErrors++;
266
- const answerText = reparsedResult?.answerText ?? snapshot.answerText ?? "";
267
- const nextValue = determineAnswerMentioned(answerText, project.displayName, projectDomains);
268
- if (nextValue) mentioned++;
269
- const nextPatch = {};
270
- if (snapshot.answerMentioned !== nextValue) {
271
- nextPatch.answerMentioned = nextValue;
272
- }
273
- if ((snapshot.answerText ?? "") !== answerText) {
274
- nextPatch.answerText = answerText;
275
- }
276
- if (reparsedResult) {
277
- const normalized = {
278
- provider: snapshot.provider,
279
- answerText,
280
- citedDomains: reparsedResult.citedDomains,
281
- groundingSources: reparsedResult.groundingSources,
282
- searchQueries: reparsedResult.searchQueries
283
- };
284
- const nextCitationState = determineCitationState(normalized, projectDomains);
285
- const nextCitedDomains = JSON.stringify(reparsedResult.citedDomains);
286
- const nextCompetitorOverlap = JSON.stringify(
287
- computeCompetitorOverlap(normalized, competitorDomains)
288
- );
289
- const nextRecommendedCompetitors = JSON.stringify(
290
- extractRecommendedCompetitors(
291
- normalized.answerText,
292
- projectDomains,
293
- normalized.citedDomains,
294
- competitorDomains
295
- )
296
- );
297
- const nextRawResponse = stringifyStoredSnapshotEnvelope(
298
- snapshot.rawResponse,
299
- reparsedResult
300
- );
301
- if (snapshot.citationState !== nextCitationState) {
302
- nextPatch.citationState = nextCitationState;
303
- }
304
- if (snapshot.citedDomains !== nextCitedDomains) {
305
- nextPatch.citedDomains = nextCitedDomains;
306
- }
307
- if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
308
- nextPatch.competitorOverlap = nextCompetitorOverlap;
309
- }
310
- if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
311
- nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
312
- }
313
- if (snapshot.rawResponse !== nextRawResponse) {
314
- nextPatch.rawResponse = nextRawResponse;
315
- }
316
- }
317
- if (Object.keys(nextPatch).length > 0) {
318
- pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
319
- }
320
- }
321
- if (pendingUpdates.length > 0) {
322
- db.transaction((tx) => {
323
- for (const update of pendingUpdates) {
324
- tx.update(querySnapshots).set(update.patch).where(eq(querySnapshots.id, update.id)).run();
325
- }
326
- });
327
- updated += pendingUpdates.length;
328
- }
329
- }
330
- }
331
- }
332
- const result = {
333
- project: projectFilter ?? null,
334
- projects: scopedProjects.length,
335
- examined,
336
- updated,
337
- mentioned,
338
- reparsed,
339
- providerErrors
340
- };
341
- if (opts?.format === "json") {
342
- console.log(JSON.stringify(result, null, 2));
343
- return;
344
- }
345
- console.log("Answer visibility backfill complete.\n");
346
- if (projectFilter) {
347
- console.log(` Project: ${projectFilter}`);
348
- }
349
- console.log(` Projects: ${scopedProjects.length}`);
350
- console.log(` Examined: ${examined}`);
351
- console.log(` Updated: ${updated}`);
352
- console.log(` Mentioned: ${mentioned}`);
353
- console.log(` Reparsed: ${reparsed}`);
354
- console.log(` Errors: ${providerErrors}`);
355
- }
356
- function backfillNormalizedPaths(db, opts) {
357
- const baseConditions = [];
358
- if (opts?.projectId) {
359
- baseConditions.push(eq(gaTrafficSnapshots.projectId, opts.projectId));
360
- }
361
- const rows = db.select({
362
- id: gaTrafficSnapshots.id,
363
- landingPage: gaTrafficSnapshots.landingPage,
364
- landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
365
- }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and(...baseConditions) : void 0).all();
366
- let updated = 0;
367
- let unchanged = 0;
368
- if (rows.length > 0) {
369
- db.transaction((tx) => {
370
- for (const row of rows) {
371
- const next = normalizeUrlPath(row.landingPage);
372
- if (next === null) {
373
- unchanged++;
374
- continue;
375
- }
376
- if (row.landingPageNormalized === next) {
377
- unchanged++;
378
- continue;
379
- }
380
- tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq(gaTrafficSnapshots.id, row.id)).run();
381
- updated++;
382
- }
383
- });
384
- }
385
- return { examined: rows.length, updated, unchanged };
386
- }
387
- async function backfillNormalizedPathsCommand(opts) {
388
- const config = loadConfig();
389
- const db = createClient(config.database);
390
- migrate(db);
391
- const projectFilter = opts?.project?.trim();
392
- let projectId;
393
- if (projectFilter) {
394
- const project = db.select({ id: projects.id }).from(projects).where(eq(projects.name, projectFilter)).get();
395
- if (!project) {
396
- const result2 = {
397
- project: projectFilter,
398
- examined: 0,
399
- updated: 0,
400
- unchanged: 0
401
- };
402
- if (opts?.format === "json") {
403
- console.log(JSON.stringify(result2, null, 2));
404
- return;
405
- }
406
- console.log(`Backfill normalized-paths: project "${projectFilter}" not found.`);
407
- return;
408
- }
409
- projectId = project.id;
410
- }
411
- const { examined, updated, unchanged } = backfillNormalizedPaths(db, { projectId });
412
- const result = {
413
- project: projectFilter ?? null,
414
- examined,
415
- updated,
416
- unchanged
417
- };
418
- if (opts?.format === "json") {
419
- console.log(JSON.stringify(result, null, 2));
420
- return;
421
- }
422
- console.log("Normalized-path backfill complete.\n");
423
- if (projectFilter) console.log(` Project: ${projectFilter}`);
424
- console.log(` Examined: ${examined}`);
425
- console.log(` Updated: ${updated}`);
426
- console.log(` Unchanged: ${unchanged}`);
427
- }
428
- function backfillAiReferralPaths(db, opts) {
429
- const baseConditions = [];
430
- if (opts?.projectId) {
431
- baseConditions.push(eq(gaAiReferrals.projectId, opts.projectId));
432
- }
433
- const rows = db.select({
434
- id: gaAiReferrals.id,
435
- landingPage: gaAiReferrals.landingPage,
436
- landingPageNormalized: gaAiReferrals.landingPageNormalized
437
- }).from(gaAiReferrals).where(baseConditions.length > 0 ? and(...baseConditions) : void 0).all();
438
- let updated = 0;
439
- let unchanged = 0;
440
- if (rows.length > 0) {
441
- db.transaction((tx) => {
442
- for (const row of rows) {
443
- const next = normalizeUrlPath(row.landingPage);
444
- if (next === null) {
445
- unchanged++;
446
- continue;
447
- }
448
- if (row.landingPageNormalized === next) {
449
- unchanged++;
450
- continue;
451
- }
452
- tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq(gaAiReferrals.id, row.id)).run();
453
- updated++;
454
- }
455
- });
456
- }
457
- return { examined: rows.length, updated, unchanged };
458
- }
459
- async function backfillAiReferralPathsCommand(opts) {
460
- const config = loadConfig();
461
- const db = createClient(config.database);
462
- migrate(db);
463
- const projectFilter = opts?.project?.trim();
464
- let projectId;
465
- if (projectFilter) {
466
- const project = db.select({ id: projects.id }).from(projects).where(eq(projects.name, projectFilter)).get();
467
- if (!project) {
468
- const result2 = {
469
- project: projectFilter,
470
- examined: 0,
471
- updated: 0,
472
- unchanged: 0
473
- };
474
- if (opts?.format === "json") {
475
- console.log(JSON.stringify(result2, null, 2));
476
- return;
477
- }
478
- console.log(`Backfill ai-referral-paths: project "${projectFilter}" not found.`);
479
- return;
480
- }
481
- projectId = project.id;
482
- }
483
- const { examined, updated, unchanged } = backfillAiReferralPaths(db, { projectId });
484
- const result = {
485
- project: projectFilter ?? null,
486
- examined,
487
- updated,
488
- unchanged
489
- };
490
- if (opts?.format === "json") {
491
- console.log(JSON.stringify(result, null, 2));
492
- return;
493
- }
494
- console.log("AI referral landing-page backfill complete.\n");
495
- if (projectFilter) console.log(` Project: ${projectFilter}`);
496
- console.log(` Examined: ${examined}`);
497
- console.log(` Updated: ${updated}`);
498
- console.log(` Unchanged: ${unchanged}`);
499
- }
500
- async function backfillAnswerMentionsCommand(opts) {
501
- const config = loadConfig();
502
- const db = createClient(config.database);
503
- migrate(db);
504
- const projectFilter = opts?.project?.trim();
505
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq(projects.name, projectFilter)).all() : db.select().from(projects).all();
506
- let examined = 0;
507
- let updated = 0;
508
- let mentioned = 0;
509
- if (scopedProjects.length > 0) {
510
- const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and(
511
- eq(runs.kind, RunKinds["answer-visibility"]),
512
- inArray(runs.projectId, scopedProjects.map((project) => project.id))
513
- )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq(runs.kind, RunKinds["answer-visibility"])).all();
514
- const runIdsByProject = /* @__PURE__ */ new Map();
515
- for (const run of runRows) {
516
- const existing = runIdsByProject.get(run.projectId);
517
- if (existing) existing.push(run.id);
518
- else runIdsByProject.set(run.projectId, [run.id]);
519
- }
520
- for (const project of scopedProjects) {
521
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq(competitors.projectId, project.id)).all().map((row) => row.domain);
522
- const runIds = runIdsByProject.get(project.id) ?? [];
523
- if (runIds.length === 0) continue;
524
- const projectDomains = effectiveDomains({
525
- canonicalDomain: project.canonicalDomain,
526
- ownedDomains: parseJsonColumn(project.ownedDomains, [])
527
- });
528
- for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
529
- const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
530
- const snapshotRows = db.select({
531
- id: querySnapshots.id,
532
- provider: querySnapshots.provider,
533
- answerMentioned: querySnapshots.answerMentioned,
534
- answerText: querySnapshots.answerText,
535
- citedDomains: querySnapshots.citedDomains,
536
- competitorOverlap: querySnapshots.competitorOverlap,
537
- recommendedCompetitors: querySnapshots.recommendedCompetitors,
538
- rawResponse: querySnapshots.rawResponse
539
- }).from(querySnapshots).where(inArray(querySnapshots.runId, batchRunIds)).all();
540
- const pendingUpdates = [];
541
- for (const snapshot of snapshotRows) {
542
- examined++;
543
- const answerText = snapshot.answerText ?? "";
544
- const nextAnswerMentioned = determineAnswerMentioned(answerText, project.displayName, projectDomains);
545
- if (nextAnswerMentioned) mentioned++;
546
- const citedDomains = parseJsonColumn(snapshot.citedDomains, []);
547
- const groundingSources = readStoredGroundingSources(snapshot.rawResponse);
548
- const normalized = {
549
- provider: snapshot.provider,
550
- answerText,
551
- citedDomains,
552
- groundingSources,
553
- searchQueries: []
554
- };
555
- const nextCompetitorOverlap = JSON.stringify(
556
- computeCompetitorOverlap(normalized, competitorDomains)
557
- );
558
- const nextRecommendedCompetitors = JSON.stringify(
559
- extractRecommendedCompetitors(
560
- answerText,
561
- projectDomains,
562
- citedDomains,
563
- competitorDomains
564
- )
565
- );
566
- const nextPatch = {};
567
- if (snapshot.answerMentioned !== nextAnswerMentioned) {
568
- nextPatch.answerMentioned = nextAnswerMentioned;
569
- }
570
- if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
571
- nextPatch.competitorOverlap = nextCompetitorOverlap;
572
- }
573
- if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
574
- nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
575
- }
576
- if (Object.keys(nextPatch).length > 0) {
577
- pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
578
- }
579
- }
580
- if (pendingUpdates.length > 0) {
581
- db.transaction((tx) => {
582
- for (const update of pendingUpdates) {
583
- tx.update(querySnapshots).set(update.patch).where(eq(querySnapshots.id, update.id)).run();
584
- }
585
- });
586
- updated += pendingUpdates.length;
587
- }
588
- }
589
- }
590
- }
591
- const result = {
592
- project: projectFilter ?? null,
593
- projects: scopedProjects.length,
594
- examined,
595
- updated,
596
- mentioned
597
- };
598
- if (opts?.format === "json") {
599
- console.log(JSON.stringify(result, null, 2));
600
- return;
601
- }
602
- console.log("Answer mentions backfill complete.\n");
603
- if (projectFilter) console.log(` Project: ${projectFilter}`);
604
- console.log(` Projects: ${scopedProjects.length}`);
605
- console.log(` Examined: ${examined}`);
606
- console.log(` Updated: ${updated}`);
607
- console.log(` Mentioned: ${mentioned}`);
608
- }
609
- function readStoredGroundingSources(rawResponse) {
610
- const envelope = parseJsonColumn(rawResponse, {});
611
- const sources = envelope.groundingSources;
612
- if (!Array.isArray(sources)) return [];
613
- const result = [];
614
- for (const source of sources) {
615
- if (source && typeof source === "object") {
616
- const uri = source.uri;
617
- const title = source.title;
618
- if (typeof uri === "string") {
619
- result.push({ uri, title: typeof title === "string" ? title : "" });
620
- }
621
- }
622
- }
623
- return result;
624
- }
625
- async function backfillInsightsCommand(project, opts) {
626
- const { IntelligenceService } = await import("./intelligence-service-3P2DMYRR.js");
627
- const config = loadConfig();
628
- const db = createClient(config.database);
629
- migrate(db);
630
- const service = new IntelligenceService(db);
631
- const isJson = opts?.format === "json";
632
- if (!isJson) {
633
- process.stderr.write(`Backfilling insights for "${project}"...
634
- `);
635
- }
636
- const result = service.backfill(project, {
637
- fromRunId: opts?.fromRun,
638
- toRunId: opts?.toRun
639
- }, (info) => {
640
- if (!isJson) {
641
- process.stderr.write(` [${info.index}/${info.total}] ${info.runId} \u2014 ${info.insights} insights
642
- `);
643
- }
644
- });
645
- const output = {
646
- project,
647
- processed: result.processed,
648
- skipped: result.skipped,
649
- totalInsights: result.totalInsights
650
- };
651
- if (isJson) {
652
- console.log(JSON.stringify(output, null, 2));
653
- return;
654
- }
655
- console.log(`
656
- Backfill complete.`);
657
- console.log(` Processed: ${result.processed}`);
658
- console.log(` Skipped: ${result.skipped}`);
659
- console.log(` Insights: ${result.totalInsights}`);
660
- }
661
- function reparseProviderSnapshot(provider, rawResponse) {
662
- const envelope = parseJsonColumn(rawResponse, {});
663
- const apiResponse = resolveStoredApiResponse(envelope);
664
- if (!apiResponse) return null;
665
- switch (provider) {
666
- case ProviderNames.openai:
667
- return reparseStoredResult(apiResponse);
668
- case ProviderNames.claude:
669
- return reparseStoredResult2(apiResponse);
670
- case ProviderNames.gemini:
671
- return reparseStoredResult3(apiResponse);
672
- case ProviderNames.perplexity:
673
- return reparseStoredResult4(apiResponse);
674
- default:
675
- return null;
676
- }
677
- }
678
- function resolveStoredApiResponse(parsed) {
679
- const nested = parsed.apiResponse;
680
- if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
681
- return nested;
682
- }
683
- if (looksLikeProviderApiResponse(parsed)) {
684
- return parsed;
685
- }
686
- return null;
687
- }
688
- function looksLikeProviderApiResponse(value) {
689
- return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
690
- }
691
- function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
692
- const parsed = parseJsonColumn(rawResponse, {});
693
- const apiResponse = resolveStoredApiResponse(parsed);
694
- const envelope = apiResponse === parsed ? {} : { ...parsed };
695
- delete envelope.answerText;
696
- delete envelope.citedDomains;
697
- delete envelope.competitorOverlap;
698
- delete envelope.recommendedCompetitors;
699
- delete envelope.providerError;
700
- return JSON.stringify({
701
- ...envelope,
702
- groundingSources: reparsed.groundingSources,
703
- searchQueries: reparsed.searchQueries,
704
- ...reparsed.providerError ? { providerError: reparsed.providerError } : {},
705
- ...apiResponse ? { apiResponse } : {}
706
- });
707
- }
708
-
709
211
  // src/cli-command-helpers.ts
710
212
  function getString(values, key) {
711
213
  const value = values[key];
@@ -818,17 +320,21 @@ var BACKFILL_CLI_COMMANDS = [
818
320
  },
819
321
  {
820
322
  path: ["backfill", "insights"],
821
- usage: "canonry backfill insights <project> [--from-run <id>] [--to-run <id>] [--format json]",
323
+ usage: "canonry backfill insights <project> [--from-run <id>] [--to-run <id>] [--since <date>] [--dry-run] [--format json]",
822
324
  options: {
823
325
  "from-run": stringOption(),
824
- "to-run": stringOption()
326
+ "to-run": stringOption(),
327
+ "since": stringOption()
825
328
  },
329
+ supportsDryRun: true,
826
330
  run: async (input) => {
827
- const usage = "canonry backfill insights <project> [--from-run <id>] [--to-run <id>] [--format json]";
331
+ const usage = "canonry backfill insights <project> [--from-run <id>] [--to-run <id>] [--since <date>] [--dry-run] [--format json]";
828
332
  const project = requireProject(input, "backfill insights", usage);
829
333
  await backfillInsightsCommand(project, {
830
334
  fromRun: getString(input.values, "from-run"),
831
335
  toRun: getString(input.values, "to-run"),
336
+ since: getString(input.values, "since"),
337
+ dryRun: input.dryRun,
832
338
  format: input.format
833
339
  });
834
340
  }
@@ -1996,18 +1502,18 @@ function errorMessage(err) {
1996
1502
  async function discoverRun(project, opts) {
1997
1503
  const client = getClient4();
1998
1504
  const { angles, multiAngle } = resolveIcpAngles(opts);
1999
- const runs2 = [];
1505
+ const runs = [];
2000
1506
  for (const angle of angles) {
2001
1507
  try {
2002
1508
  const start = await client.triggerDiscoveryRun(project, buildRunBody(opts, angle));
2003
- runs2.push({ angle, start });
1509
+ runs.push({ angle, start });
2004
1510
  } catch (err) {
2005
- if (runs2.length > 0) {
1511
+ if (runs.length > 0) {
2006
1512
  process.stderr.write(
2007
1513
  `
2008
1514
  Failed to start ${angle ? `angle "${angle}"` : "discovery run"}: ${errorMessage(err)}
2009
1515
  Sessions already started (recover with \`canonry discover show ${project} <id>\`):
2010
- ` + runs2.map((r) => ` ${r.start.sessionId}`).join("\n") + "\n"
1516
+ ` + runs.map((r) => ` ${r.start.sessionId}`).join("\n") + "\n"
2011
1517
  );
2012
1518
  }
2013
1519
  throw err;
@@ -2015,10 +1521,10 @@ Sessions already started (recover with \`canonry discover show ${project} <id>\`
2015
1521
  }
2016
1522
  if (!opts.wait) {
2017
1523
  if (opts.format === "json") {
2018
- console.log(JSON.stringify(multiAngle ? runs2.map((r) => r.start) : runs2[0].start, null, 2));
1524
+ console.log(JSON.stringify(multiAngle ? runs.map((r) => r.start) : runs[0].start, null, 2));
2019
1525
  return;
2020
1526
  }
2021
- for (const { angle, start } of runs2) {
1527
+ for (const { angle, start } of runs) {
2022
1528
  if (angle) console.log(`[${angle}]`);
2023
1529
  if (start.consolidated) {
2024
1530
  console.log(`Reusing in-flight discovery session: ${start.sessionId}`);
@@ -2031,20 +1537,20 @@ Sessions already started (recover with \`canonry discover show ${project} <id>\`
2031
1537
  console.log(` Status: ${start.status}`);
2032
1538
  console.log(` Tail: canonry discover show ${project} ${start.sessionId}`);
2033
1539
  }
2034
- if (runs2.length > 1) console.log();
1540
+ if (runs.length > 1) console.log();
2035
1541
  }
2036
1542
  return;
2037
1543
  }
2038
- const parallel = runs2.length > 1;
2039
- if (parallel) process.stderr.write(`Waiting for ${runs2.length} discovery sessions...
1544
+ const parallel = runs.length > 1;
1545
+ if (parallel) process.stderr.write(`Waiting for ${runs.length} discovery sessions...
2040
1546
  `);
2041
1547
  const settled = await Promise.allSettled(
2042
- runs2.map((r) => pollSession(client, project, r.start.sessionId, parallel))
1548
+ runs.map((r) => pollSession(client, project, r.start.sessionId, parallel))
2043
1549
  );
2044
1550
  const results = [];
2045
1551
  const failures = [];
2046
1552
  settled.forEach((outcome, i) => {
2047
- const run = runs2[i];
1553
+ const run = runs[i];
2048
1554
  if (outcome.status === "fulfilled") {
2049
1555
  results.push({ angle: run.angle, session: outcome.value });
2050
1556
  } else {
@@ -2084,7 +1590,7 @@ Sessions already started (recover with \`canonry discover show ${project} <id>\`
2084
1590
  }
2085
1591
  throw new CliError({
2086
1592
  code: "DISCOVERY_INCOMPLETE",
2087
- message: `${failures.length} of ${runs2.length} discovery session(s) did not complete`,
1593
+ message: `${failures.length} of ${runs.length} discovery session(s) did not complete`,
2088
1594
  details: { failed: failures.map((f) => f.sessionId) }
2089
1595
  });
2090
1596
  }
@@ -6069,10 +5575,10 @@ Brand Gap Analysis`);
6069
5575
  console.log(`
6070
5576
  Opportunity Gaps (competitors cited, you're not):`);
6071
5577
  for (const q of data.gap) {
6072
- const competitors2 = q.competitorsCiting.join(", ");
5578
+ const competitors = q.competitorsCiting.join(", ");
6073
5579
  const cons = q.consistency.totalRuns > 0 ? ` [cited ${q.consistency.citedRuns}/${q.consistency.totalRuns} runs]` : "";
6074
5580
  console.log(` \u2022 ${q.query}${cons}`);
6075
- console.log(` Competitors: ${competitors2}`);
5581
+ console.log(` Competitors: ${competitors}`);
6076
5582
  }
6077
5583
  }
6078
5584
  if (data.cited.length > 0) {
@@ -6155,9 +5661,9 @@ async function loadLatestRunForExport(client, project) {
6155
5661
  } catch (err) {
6156
5662
  if (!isEndpointMissing(err)) throw err;
6157
5663
  }
6158
- const runs2 = await client.listRuns(project);
6159
- if (runs2.length === 0) return null;
6160
- const latestRun = runs2.reduce(
5664
+ const runs = await client.listRuns(project);
5665
+ if (runs.length === 0) return null;
5666
+ const latestRun = runs.reduce(
6161
5667
  (current, candidate) => candidate.createdAt > current.createdAt ? candidate : current
6162
5668
  );
6163
5669
  return client.getRun(latestRun.id);
@@ -6211,14 +5717,14 @@ async function showStatus(project, format) {
6211
5717
  const projectData = await client.getProject(project);
6212
5718
  const latest = await getLatestRunSummary(client, project);
6213
5719
  if (format === "json") {
6214
- let runs2 = [];
5720
+ let runs = [];
6215
5721
  try {
6216
- runs2 = await client.listRuns(project);
5722
+ runs = await client.listRuns(project);
6217
5723
  } catch {
6218
5724
  }
6219
5725
  console.log(JSON.stringify({
6220
5726
  project: projectData,
6221
- runs: runs2,
5727
+ runs,
6222
5728
  latestRun: latest.run,
6223
5729
  totalRuns: latest.totalRuns
6224
5730
  }, null, 2));
@@ -6249,15 +5755,15 @@ async function getLatestRunSummary(client, project) {
6249
5755
  return await client.getLatestRun(project);
6250
5756
  } catch (err) {
6251
5757
  if (!isEndpointMissing(err)) throw err;
6252
- const runs2 = await client.listRuns(project);
6253
- if (runs2.length === 0) {
5758
+ const runs = await client.listRuns(project);
5759
+ if (runs.length === 0) {
6254
5760
  return { totalRuns: 0, run: null };
6255
5761
  }
6256
- const latestRun = runs2.reduce(
5762
+ const latestRun = runs.reduce(
6257
5763
  (current, candidate) => candidate.createdAt > current.createdAt ? candidate : current
6258
5764
  );
6259
5765
  return {
6260
- totalRuns: runs2.length,
5766
+ totalRuns: runs.length,
6261
5767
  run: latestRun
6262
5768
  };
6263
5769
  }
@@ -6347,6 +5853,7 @@ async function createProject(name, opts) {
6347
5853
  displayName: opts.displayName,
6348
5854
  canonicalDomain: opts.domain,
6349
5855
  ownedDomains: opts.ownedDomains ?? [],
5856
+ aliases: normalizeProjectAliases(opts.displayName, opts.aliases ?? []),
6350
5857
  country: opts.country,
6351
5858
  language: opts.language
6352
5859
  });
@@ -6400,6 +5907,9 @@ async function showProject(name, format) {
6400
5907
  if (secondaryDomains.length > 0) {
6401
5908
  console.log(` Owned domains: ${secondaryDomains.join(", ")}`);
6402
5909
  }
5910
+ if (project.aliases && project.aliases.length > 0) {
5911
+ console.log(` Aliases: ${project.aliases.join(", ")}`);
5912
+ }
6403
5913
  console.log(` Country: ${project.country}`);
6404
5914
  console.log(` Language: ${project.language}`);
6405
5915
  console.log(` Config source: ${project.configSource}`);
@@ -6422,10 +5932,22 @@ async function updateProjectSettings(name, opts) {
6422
5932
  const toRemove = new Set(opts.removeOwnedDomain);
6423
5933
  ownedDomains = ownedDomains.filter((d) => !toRemove.has(d));
6424
5934
  }
5935
+ const nextDisplayName = opts.displayName ?? project.displayName ?? project.name;
5936
+ let aliases = opts.aliases ?? project.aliases ?? [];
5937
+ if (opts.addAlias) {
5938
+ const existingKeys = new Set(aliases.map((a) => a.toLowerCase()));
5939
+ const toAdd = opts.addAlias.filter((a) => !existingKeys.has(a.toLowerCase()));
5940
+ aliases = [...aliases, ...toAdd];
5941
+ }
5942
+ if (opts.removeAlias) {
5943
+ const toRemove = new Set(opts.removeAlias.map((a) => a.toLowerCase()));
5944
+ aliases = aliases.filter((a) => !toRemove.has(a.toLowerCase()));
5945
+ }
6425
5946
  const result = await client.putProject(name, {
6426
- displayName: opts.displayName ?? project.displayName ?? project.name,
5947
+ displayName: nextDisplayName,
6427
5948
  canonicalDomain: opts.domain ?? project.canonicalDomain,
6428
5949
  ownedDomains,
5950
+ aliases: normalizeProjectAliases(nextDisplayName, aliases),
6429
5951
  country: opts.country ?? project.country,
6430
5952
  language: opts.language ?? project.language
6431
5953
  });
@@ -6435,10 +5957,31 @@ async function updateProjectSettings(name, opts) {
6435
5957
  }
6436
5958
  console.log(`Project updated: ${result.name}`);
6437
5959
  }
6438
- async function deleteProject(name, format) {
5960
+ async function deleteProject(name, opts) {
6439
5961
  const client = getClient16();
5962
+ const isJson = opts?.format === "json";
5963
+ if (opts?.dryRun) {
5964
+ const preview = await client.previewProjectDelete(name);
5965
+ if (isJson) {
5966
+ console.log(JSON.stringify({ dryRun: true, ...preview }, null, 2));
5967
+ return;
5968
+ }
5969
+ const { cascadeRows: cr, detachedRows: dr } = preview;
5970
+ console.log(`Project delete preview for "${name}":`);
5971
+ console.log(` Cascade-deletes:`);
5972
+ console.log(` queries: ${cr.queries}`);
5973
+ console.log(` competitors: ${cr.competitors}`);
5974
+ console.log(` runs: ${cr.runs}`);
5975
+ console.log(` snapshots: ${cr.snapshots}`);
5976
+ console.log(` insights: ${cr.insights}`);
5977
+ console.log(` Detached (project_id set to NULL):`);
5978
+ console.log(` audit_log: ${dr.auditLog}`);
5979
+ console.log(``);
5980
+ console.log(`No DB writes performed. Re-run without --dry-run to delete.`);
5981
+ return;
5982
+ }
6440
5983
  await client.deleteProject(name);
6441
- if (format === "json") {
5984
+ if (isJson) {
6442
5985
  console.log(JSON.stringify({ name, deleted: true }, null, 2));
6443
5986
  return;
6444
5987
  }
@@ -6508,10 +6051,11 @@ async function setDefaultLocation(project, label, format) {
6508
6051
  var PROJECT_CLI_COMMANDS = [
6509
6052
  {
6510
6053
  path: ["project", "create"],
6511
- usage: "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]",
6054
+ usage: "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]",
6512
6055
  options: {
6513
6056
  domain: { type: "string", short: "d" },
6514
6057
  "owned-domain": multiStringOption(),
6058
+ alias: multiStringOption(),
6515
6059
  country: stringOption(),
6516
6060
  language: stringOption(),
6517
6061
  "display-name": stringOption()
@@ -6520,11 +6064,12 @@ var PROJECT_CLI_COMMANDS = [
6520
6064
  const name = requireProject(
6521
6065
  input,
6522
6066
  "project.create",
6523
- "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]"
6067
+ "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]"
6524
6068
  );
6525
6069
  await createProject(name, {
6526
6070
  domain: getString(input.values, "domain") ?? name,
6527
6071
  ownedDomains: getStringArray(input.values, "owned-domain") ?? [],
6072
+ aliases: getStringArray(input.values, "alias") ?? [],
6528
6073
  country: getString(input.values, "country") ?? "US",
6529
6074
  language: getString(input.values, "language") ?? "en",
6530
6075
  displayName: getString(input.values, "display-name") ?? name,
@@ -6534,12 +6079,15 @@ var PROJECT_CLI_COMMANDS = [
6534
6079
  },
6535
6080
  {
6536
6081
  path: ["project", "update"],
6537
- usage: "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]",
6082
+ usage: "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]",
6538
6083
  options: {
6539
6084
  domain: { type: "string", short: "d" },
6540
6085
  "owned-domain": multiStringOption(),
6541
6086
  "add-domain": multiStringOption(),
6542
6087
  "remove-domain": multiStringOption(),
6088
+ alias: multiStringOption(),
6089
+ "add-alias": multiStringOption(),
6090
+ "remove-alias": multiStringOption(),
6543
6091
  country: stringOption(),
6544
6092
  language: stringOption(),
6545
6093
  "display-name": stringOption()
@@ -6548,7 +6096,7 @@ var PROJECT_CLI_COMMANDS = [
6548
6096
  const name = requireProject(
6549
6097
  input,
6550
6098
  "project.update",
6551
- "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]"
6099
+ "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]"
6552
6100
  );
6553
6101
  await updateProjectSettings(name, {
6554
6102
  displayName: getString(input.values, "display-name"),
@@ -6556,6 +6104,9 @@ var PROJECT_CLI_COMMANDS = [
6556
6104
  ownedDomains: getStringArray(input.values, "owned-domain"),
6557
6105
  addOwnedDomain: getStringArray(input.values, "add-domain"),
6558
6106
  removeOwnedDomain: getStringArray(input.values, "remove-domain"),
6107
+ aliases: getStringArray(input.values, "alias"),
6108
+ addAlias: getStringArray(input.values, "add-alias"),
6109
+ removeAlias: getStringArray(input.values, "remove-alias"),
6559
6110
  country: getString(input.values, "country"),
6560
6111
  language: getString(input.values, "language"),
6561
6112
  format: input.format
@@ -6580,10 +6131,11 @@ var PROJECT_CLI_COMMANDS = [
6580
6131
  },
6581
6132
  {
6582
6133
  path: ["project", "delete"],
6583
- usage: "canonry project delete <name> [--format json]",
6134
+ usage: "canonry project delete <name> [--dry-run] [--format json]",
6135
+ supportsDryRun: true,
6584
6136
  run: async (input) => {
6585
- const name = requireProject(input, "project.delete", "canonry project delete <name> [--format json]");
6586
- await deleteProject(name, input.format);
6137
+ const name = requireProject(input, "project.delete", "canonry project delete <name> [--dry-run] [--format json]");
6138
+ await deleteProject(name, { dryRun: input.dryRun, format: input.format });
6587
6139
  }
6588
6140
  },
6589
6141
  {
@@ -6892,8 +6444,8 @@ async function cancelRun(project, runId, format) {
6892
6444
  const client = getClient17();
6893
6445
  let targetId = runId;
6894
6446
  if (!targetId) {
6895
- const runs2 = await client.listRuns(project);
6896
- const active = runs2.find((r) => r.status === "queued" || r.status === "running");
6447
+ const runs = await client.listRuns(project);
6448
+ const active = runs.find((r) => r.status === "queued" || r.status === "running");
6897
6449
  if (!active) {
6898
6450
  throw new CliError({
6899
6451
  code: "NO_ACTIVE_RUN",
@@ -6931,20 +6483,20 @@ async function showRun(id, format) {
6931
6483
  }
6932
6484
  async function listRuns(project, opts) {
6933
6485
  const client = getClient17();
6934
- const runs2 = await client.listRuns(project, opts?.limit);
6486
+ const runs = await client.listRuns(project, opts?.limit);
6935
6487
  if (opts?.format === "json") {
6936
- console.log(JSON.stringify(runs2, null, 2));
6488
+ console.log(JSON.stringify(runs, null, 2));
6937
6489
  return;
6938
6490
  }
6939
- if (runs2.length === 0) {
6491
+ if (runs.length === 0) {
6940
6492
  console.log(`No runs found for "${project}".`);
6941
6493
  return;
6942
6494
  }
6943
- console.log(`Runs for "${project}" (${runs2.length}):
6495
+ console.log(`Runs for "${project}" (${runs.length}):
6944
6496
  `);
6945
6497
  console.log(" ID STATUS KIND TRIGGER CREATED");
6946
6498
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
6947
- for (const run of runs2) {
6499
+ for (const run of runs) {
6948
6500
  console.log(
6949
6501
  ` ${run.id} ${run.status.padEnd(10)} ${run.kind.padEnd(18)} ${run.trigger.padEnd(9)} ${run.createdAt}`
6950
6502
  );
@@ -7465,6 +7017,7 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
7465
7017
 
7466
7018
  // src/commands/skills.ts
7467
7019
  import fs7 from "fs";
7020
+ import os2 from "os";
7468
7021
  import path4 from "path";
7469
7022
  import { fileURLToPath } from "url";
7470
7023
  var BUNDLED_SKILL_NAMES = ["canonry", "aero"];
@@ -7641,7 +7194,7 @@ function buildSummaryMessage(results) {
7641
7194
  return `Skills install summary: ${parts.join(", ")}.`;
7642
7195
  }
7643
7196
  async function installSkills(opts = {}) {
7644
- const targetDir = path4.resolve(opts.dir ?? process.cwd());
7197
+ const targetDir = opts.user ? os2.homedir() : path4.resolve(opts.dir ?? process.cwd());
7645
7198
  const client = opts.client ?? SkillsClients.all;
7646
7199
  const force = opts.force ?? false;
7647
7200
  const allSkills = getBundledSkills();
@@ -7727,9 +7280,10 @@ var SKILLS_CLI_COMMANDS = [
7727
7280
  },
7728
7281
  {
7729
7282
  path: ["skills", "install"],
7730
- usage: "canonry skills install [skill...] [--dir <path>] [--client claude|codex|all] [--force] [--format json]",
7283
+ usage: "canonry skills install [skill...] [--dir <path> | --user] [--client claude|codex|all] [--force] [--format json]",
7731
7284
  options: {
7732
7285
  dir: stringOption(),
7286
+ user: { type: "boolean" },
7733
7287
  client: stringOption(),
7734
7288
  force: { type: "boolean" }
7735
7289
  },
@@ -7737,6 +7291,7 @@ var SKILLS_CLI_COMMANDS = [
7737
7291
  run: async (input) => {
7738
7292
  const summary = await installSkills({
7739
7293
  dir: getString(input.values, "dir"),
7294
+ user: getBoolean(input.values, "user"),
7740
7295
  skills: input.positionals.length > 0 ? input.positionals : void 0,
7741
7296
  client: parseSkillsClient(getString(input.values, "client")),
7742
7297
  force: getBoolean(input.values, "force")
@@ -8042,8 +7597,8 @@ function renderQueries(pdf, report) {
8042
7597
  for (const result of queryResult.providerResults) {
8043
7598
  const status = result.error ? "error" : result.mentioned ? result.cited ? "mentioned and cited" : "mentioned" : "not mentioned";
8044
7599
  const accuracy = result.describedAccurately === "not-mentioned" ? "" : `; accuracy: ${result.describedAccurately}`;
8045
- const competitors2 = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
8046
- const line = `${result.displayName}: ${status}${accuracy}${competitors2}`;
7600
+ const competitors = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
7601
+ const line = `${result.displayName}: ${status}${accuracy}${competitors}`;
8047
7602
  pdf.bullet(line);
8048
7603
  if (result.error) {
8049
7604
  pdf.paragraph(`Error: ${result.error}`, { size: 9, color: FAIL, lineHeight: 12 });
@@ -8190,8 +7745,8 @@ function formatSnapshotMarkdown(report) {
8190
7745
  const mentioned = result.mentioned ? "Yes" : "No";
8191
7746
  const cited = result.cited ? "Yes" : "No";
8192
7747
  const accuracy = result.describedAccurately === "not-mentioned" ? "-" : result.describedAccurately;
8193
- const competitors2 = result.recommendedCompetitors.length > 0 ? result.recommendedCompetitors.join(", ") : "-";
8194
- lines.push(`| ${result.displayName} | ${mentioned} | ${cited} | ${accuracy} | ${competitors2} |`);
7748
+ const competitors = result.recommendedCompetitors.length > 0 ? result.recommendedCompetitors.join(", ") : "-";
7749
+ lines.push(`| ${result.displayName} | ${mentioned} | ${cited} | ${accuracy} | ${competitors} |`);
8195
7750
  }
8196
7751
  lines.push("");
8197
7752
  }
@@ -8421,7 +7976,7 @@ function renderHuman(overview) {
8421
7976
  transitions,
8422
7977
  scores,
8423
7978
  movementSummary,
8424
- competitors: competitors2,
7979
+ competitors,
8425
7980
  providerScores,
8426
7981
  attentionItems,
8427
7982
  runHistory,
@@ -8469,9 +8024,9 @@ function renderHuman(overview) {
8469
8024
  console.log(`
8470
8025
  Transitions since ${transitions.since}: +${transitions.gained} gained, -${transitions.lost} lost, ${transitions.emerging} emerging`);
8471
8026
  }
8472
- if (competitors2.length > 0) {
8027
+ if (competitors.length > 0) {
8473
8028
  console.log("\n Competitors:");
8474
- for (const c of competitors2) {
8029
+ for (const c of competitors) {
8475
8030
  console.log(` ${c.domain.padEnd(28)} ${c.citationCount}/${c.totalQueries} ${c.pressureLabel}`);
8476
8031
  }
8477
8032
  }
@@ -8860,7 +8415,7 @@ var CONTENT_CLI_COMMANDS = [
8860
8415
  // src/commands/bootstrap.ts
8861
8416
  import crypto from "crypto";
8862
8417
  import path7 from "path";
8863
- import { eq as eq2 } from "drizzle-orm";
8418
+ import { eq } from "drizzle-orm";
8864
8419
 
8865
8420
  // ../config/src/index.ts
8866
8421
  import { z } from "zod";
@@ -8898,7 +8453,11 @@ var envSchema = z.object({
8898
8453
  PERPLEXITY_MODEL: z.string().optional(),
8899
8454
  PERPLEXITY_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
8900
8455
  PERPLEXITY_MAX_REQUESTS_PER_MINUTE: z.coerce.number().int().positive().default(10),
8901
- PERPLEXITY_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3)
8456
+ PERPLEXITY_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3),
8457
+ // Secret for HMAC-signing Google OAuth state parameters. Required for
8458
+ // cloud deployments that mount googleRoutes; the plugin refuses to register
8459
+ // without it (see packages/api-routes/src/google.ts).
8460
+ GOOGLE_STATE_SECRET: z.string().optional()
8902
8461
  });
8903
8462
  var bootstrapEnvSchema = z.object({
8904
8463
  CANONRY_API_KEY: z.string().optional(),
@@ -9038,7 +8597,7 @@ async function bootstrapCommand(_opts) {
9038
8597
  const db = createClient(databasePath);
9039
8598
  migrate(db);
9040
8599
  db.transaction((tx) => {
9041
- tx.delete(apiKeys).where(eq2(apiKeys.name, "default")).run();
8600
+ tx.delete(apiKeys).where(eq(apiKeys.name, "default")).run();
9042
8601
  tx.insert(apiKeys).values({
9043
8602
  id: crypto.randomUUID(),
9044
8603
  name: "default",
@@ -11558,6 +11117,18 @@ async function runCli(args = process.argv.slice(2)) {
11558
11117
  ...setupState ? { setup_state: setupState } : {}
11559
11118
  });
11560
11119
  }
11120
+ if (!isHelpRequest && command !== "telemetry") {
11121
+ void checkLatestVersionForCli().then((update) => {
11122
+ if (!update) return;
11123
+ process.stderr.write(
11124
+ `
11125
+ \u2192 canonry ${update.latest} is available (you have ${update.current}).
11126
+ Upgrade: ${update.upgradeCommand}
11127
+
11128
+ `
11129
+ );
11130
+ });
11131
+ }
11561
11132
  try {
11562
11133
  if (await dispatchRegisteredCommand(args, format, REGISTERED_CLI_COMMANDS)) {
11563
11134
  return 0;