@ainyc/canonry 1.38.0 → 1.39.1

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,5 +1,6 @@
1
1
  #!/usr/bin/env node --import tsx
2
2
  import {
3
+ IntelligenceService,
3
4
  apiKeys,
4
5
  configExists,
5
6
  createClient,
@@ -26,7 +27,7 @@ import {
26
27
  setGoogleAuthConfig,
27
28
  showFirstRunNotice,
28
29
  trackEvent
29
- } from "./chunk-SAVCFB5B.js";
30
+ } from "./chunk-2MJBBEGP.js";
30
31
 
31
32
  // src/cli.ts
32
33
  import { pathToFileURL } from "url";
@@ -125,7 +126,49 @@ function withFormatOption(options) {
125
126
  function toFormat(value, fallbackFormat) {
126
127
  return value === "json" ? "json" : fallbackFormat;
127
128
  }
129
+ function printGroupHelp(group, specs) {
130
+ const parentSpec = specs.find((s) => s.path.length === 1 && s.path[0] === group);
131
+ const groupSpecs = specs.filter((s) => s.path[0] === group && s.path.length > 1).sort((a, b) => a.path.join(" ").localeCompare(b.path.join(" ")));
132
+ if (groupSpecs.length === 0) return;
133
+ console.log(`
134
+ Usage: canonry ${group} <command> [options]
135
+ `);
136
+ if (parentSpec) {
137
+ console.log(` ${parentSpec.usage}
138
+ `);
139
+ }
140
+ console.log("Subcommands:");
141
+ for (const spec of groupSpecs) {
142
+ console.log(` ${spec.usage}`);
143
+ }
144
+ console.log();
145
+ }
128
146
  async function dispatchRegisteredCommand(args, fallbackFormat, specs) {
147
+ if (args.length >= 1 && (args.includes("--help") || args.includes("-h"))) {
148
+ const argsWithoutHelp = args.filter((a) => a !== "--help" && a !== "-h");
149
+ const exactMatch = [...specs].sort((a, b) => b.path.length - a.path.length).find((candidate) => matchesPath(argsWithoutHelp, candidate.path));
150
+ if (exactMatch && exactMatch.path.length > 1) {
151
+ console.log(`
152
+ Usage: ${exactMatch.usage}
153
+ `);
154
+ return true;
155
+ }
156
+ const group = argsWithoutHelp[0];
157
+ if (group) {
158
+ const groupSpecs = specs.filter((s) => s.path[0] === group && s.path.length > 1);
159
+ if (groupSpecs.length > 0) {
160
+ printGroupHelp(group, specs);
161
+ return true;
162
+ }
163
+ const single = specs.find((s) => s.path.length === 1 && s.path[0] === group);
164
+ if (single) {
165
+ console.log(`
166
+ Usage: ${single.usage}
167
+ `);
168
+ return true;
169
+ }
170
+ }
171
+ }
129
172
  const spec = [...specs].sort((a, b) => b.path.length - a.path.length).find((candidate) => matchesPath(args, candidate.path));
130
173
  if (!spec) return false;
131
174
  const remainingArgs = args.slice(spec.path.length);
@@ -227,6 +270,41 @@ async function backfillAnswerVisibilityCommand(opts) {
227
270
  console.log(` Updated: ${updated}`);
228
271
  console.log(` Visible: ${visible}`);
229
272
  }
273
+ async function backfillInsightsCommand(project, opts) {
274
+ const config = loadConfig();
275
+ const db = createClient(config.database);
276
+ migrate(db);
277
+ const service = new IntelligenceService(db);
278
+ const isJson = opts?.format === "json";
279
+ if (!isJson) {
280
+ process.stderr.write(`Backfilling insights for "${project}"...
281
+ `);
282
+ }
283
+ const result = service.backfill(project, {
284
+ fromRunId: opts?.fromRun,
285
+ toRunId: opts?.toRun
286
+ }, (info) => {
287
+ if (!isJson) {
288
+ process.stderr.write(` [${info.index}/${info.total}] ${info.runId} \u2014 ${info.insights} insights
289
+ `);
290
+ }
291
+ });
292
+ const output = {
293
+ project,
294
+ processed: result.processed,
295
+ skipped: result.skipped,
296
+ totalInsights: result.totalInsights
297
+ };
298
+ if (isJson) {
299
+ console.log(JSON.stringify(output, null, 2));
300
+ return;
301
+ }
302
+ console.log(`
303
+ Backfill complete.`);
304
+ console.log(` Processed: ${result.processed}`);
305
+ console.log(` Skipped: ${result.skipped}`);
306
+ console.log(` Insights: ${result.totalInsights}`);
307
+ }
230
308
 
231
309
  // src/cli-command-helpers.ts
232
310
  function getString(values, key) {
@@ -324,14 +402,31 @@ var BACKFILL_CLI_COMMANDS = [
324
402
  });
325
403
  }
326
404
  },
405
+ {
406
+ path: ["backfill", "insights"],
407
+ usage: "canonry backfill insights <project> [--from-run <id>] [--to-run <id>] [--format json]",
408
+ options: {
409
+ "from-run": stringOption(),
410
+ "to-run": stringOption()
411
+ },
412
+ run: async (input) => {
413
+ const usage = "canonry backfill insights <project> [--from-run <id>] [--to-run <id>] [--format json]";
414
+ const project = requireProject(input, "backfill insights", usage);
415
+ await backfillInsightsCommand(project, {
416
+ fromRun: getString(input.values, "from-run"),
417
+ toRun: getString(input.values, "to-run"),
418
+ format: input.format
419
+ });
420
+ }
421
+ },
327
422
  {
328
423
  path: ["backfill"],
329
- usage: "canonry backfill <answer-visibility> [--project <name>] [--format json]",
424
+ usage: "canonry backfill <answer-visibility|insights> [options]",
330
425
  run: async (input) => {
331
426
  unknownSubcommand(input.positionals[0], {
332
427
  command: "backfill",
333
- usage: "canonry backfill <answer-visibility> [--project <name>] [--format json]",
334
- available: ["answer-visibility"]
428
+ usage: "canonry backfill <answer-visibility|insights> [options]",
429
+ available: ["answer-visibility", "insights"]
335
430
  });
336
431
  }
337
432
  }
@@ -754,6 +849,24 @@ var ApiClient = class {
754
849
  async wordpressStagingPush(project) {
755
850
  return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/staging/push`);
756
851
  }
852
+ // ── Intelligence ──────────────────────────────────────────────────────
853
+ async getInsights(project, opts) {
854
+ const params = new URLSearchParams();
855
+ if (opts?.dismissed) params.set("dismissed", "true");
856
+ if (opts?.runId) params.set("runId", opts.runId);
857
+ const qs = params.toString();
858
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/insights${qs ? `?${qs}` : ""}`);
859
+ }
860
+ async dismissInsight(project, id) {
861
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/insights/${encodeURIComponent(id)}/dismiss`);
862
+ }
863
+ async getHealth(project) {
864
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/health/latest`);
865
+ }
866
+ async getHealthHistory(project, limit) {
867
+ const qs = limit ? `?limit=${limit}` : "";
868
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/health/history${qs}`);
869
+ }
757
870
  };
758
871
 
759
872
  // src/commands/bing.ts
@@ -1071,17 +1184,17 @@ var BING_CLI_COMMANDS = [
1071
1184
  },
1072
1185
  {
1073
1186
  path: ["bing", "status"],
1074
- usage: "canonry bing status <project>",
1187
+ usage: "canonry bing status <project> [--format json]",
1075
1188
  run: async (input) => {
1076
- const project = requireProject(input, "bing.status", "canonry bing status <project>");
1189
+ const project = requireProject(input, "bing.status", "canonry bing status <project> [--format json]");
1077
1190
  await bingStatus(project, input.format);
1078
1191
  }
1079
1192
  },
1080
1193
  {
1081
1194
  path: ["bing", "sites"],
1082
- usage: "canonry bing sites <project>",
1195
+ usage: "canonry bing sites <project> [--format json]",
1083
1196
  run: async (input) => {
1084
- const project = requireProject(input, "bing.sites", "canonry bing sites <project>");
1197
+ const project = requireProject(input, "bing.sites", "canonry bing sites <project> [--format json]");
1085
1198
  await bingSites(project, input.format);
1086
1199
  }
1087
1200
  },
@@ -1100,20 +1213,20 @@ var BING_CLI_COMMANDS = [
1100
1213
  },
1101
1214
  {
1102
1215
  path: ["bing", "coverage"],
1103
- usage: "canonry bing coverage <project>",
1216
+ usage: "canonry bing coverage <project> [--format json]",
1104
1217
  run: async (input) => {
1105
- const project = requireProject(input, "bing.coverage", "canonry bing coverage <project>");
1218
+ const project = requireProject(input, "bing.coverage", "canonry bing coverage <project> [--format json]");
1106
1219
  await bingCoverage(project, input.format);
1107
1220
  }
1108
1221
  },
1109
1222
  {
1110
1223
  path: ["bing", "inspect"],
1111
- usage: "canonry bing inspect <project> <url>",
1224
+ usage: "canonry bing inspect <project> <url> [--format json]",
1112
1225
  run: async (input) => {
1113
- const project = requireProject(input, "bing.inspect", "canonry bing inspect <project> <url>");
1226
+ const project = requireProject(input, "bing.inspect", "canonry bing inspect <project> <url> [--format json]");
1114
1227
  const url = requirePositional(input, 1, {
1115
1228
  command: "bing.inspect",
1116
- usage: "canonry bing inspect <project> <url>",
1229
+ usage: "canonry bing inspect <project> <url> [--format json]",
1117
1230
  message: "project name and URL are required"
1118
1231
  });
1119
1232
  await bingInspect(project, url, input.format);
@@ -1161,9 +1274,9 @@ var BING_CLI_COMMANDS = [
1161
1274
  },
1162
1275
  {
1163
1276
  path: ["bing", "performance"],
1164
- usage: "canonry bing performance <project>",
1277
+ usage: "canonry bing performance <project> [--format json]",
1165
1278
  run: async (input) => {
1166
- const project = requireProject(input, "bing.performance", "canonry bing performance <project>");
1279
+ const project = requireProject(input, "bing.performance", "canonry bing performance <project> [--format json]");
1167
1280
  await bingPerformance(project, input.format);
1168
1281
  }
1169
1282
  },
@@ -1334,7 +1447,7 @@ var CDP_CLI_COMMANDS = [
1334
1447
  },
1335
1448
  {
1336
1449
  path: ["cdp", "status"],
1337
- usage: "canonry cdp status",
1450
+ usage: "canonry cdp status [--format json]",
1338
1451
  allowPositionals: false,
1339
1452
  run: async (input) => {
1340
1453
  await cdpStatus(input.format);
@@ -1342,7 +1455,7 @@ var CDP_CLI_COMMANDS = [
1342
1455
  },
1343
1456
  {
1344
1457
  path: ["cdp", "targets"],
1345
- usage: "canonry cdp targets",
1458
+ usage: "canonry cdp targets [--format json]",
1346
1459
  allowPositionals: false,
1347
1460
  run: async (input) => {
1348
1461
  await cdpTargets(input.format);
@@ -1744,7 +1857,7 @@ var COMPETITOR_CLI_COMMANDS = [
1744
1857
  },
1745
1858
  {
1746
1859
  path: ["competitor", "list"],
1747
- usage: "canonry competitor list <project>",
1860
+ usage: "canonry competitor list <project> [--format json]",
1748
1861
  run: async (input) => {
1749
1862
  const project = requireProject(input, "competitor.list", "canonry competitor list <project>");
1750
1863
  await listCompetitors(project, input.format);
@@ -2355,17 +2468,17 @@ var GOOGLE_CLI_COMMANDS = [
2355
2468
  },
2356
2469
  {
2357
2470
  path: ["google", "status"],
2358
- usage: "canonry google status <project>",
2471
+ usage: "canonry google status <project> [--format json]",
2359
2472
  run: async (input) => {
2360
- const project = requireProject(input, "google.status", "canonry google status <project>");
2473
+ const project = requireProject(input, "google.status", "canonry google status <project> [--format json]");
2361
2474
  await googleStatus(project, input.format);
2362
2475
  }
2363
2476
  },
2364
2477
  {
2365
2478
  path: ["google", "properties"],
2366
- usage: "canonry google properties <project>",
2479
+ usage: "canonry google properties <project> [--format json]",
2367
2480
  run: async (input) => {
2368
- const project = requireProject(input, "google.properties", "canonry google properties <project>");
2481
+ const project = requireProject(input, "google.properties", "canonry google properties <project> [--format json]");
2369
2482
  await googleProperties(project, input.format);
2370
2483
  }
2371
2484
  },
@@ -2397,9 +2510,9 @@ var GOOGLE_CLI_COMMANDS = [
2397
2510
  },
2398
2511
  {
2399
2512
  path: ["google", "list-sitemaps"],
2400
- usage: "canonry google list-sitemaps <project>",
2513
+ usage: "canonry google list-sitemaps <project> [--format json]",
2401
2514
  run: async (input) => {
2402
- const project = requireProject(input, "google.list-sitemaps", "canonry google list-sitemaps <project>");
2515
+ const project = requireProject(input, "google.list-sitemaps", "canonry google list-sitemaps <project> [--format json]");
2403
2516
  await googleListSitemaps(project, { format: input.format });
2404
2517
  }
2405
2518
  },
@@ -2451,12 +2564,12 @@ var GOOGLE_CLI_COMMANDS = [
2451
2564
  },
2452
2565
  {
2453
2566
  path: ["google", "inspect"],
2454
- usage: "canonry google inspect <project> <url>",
2567
+ usage: "canonry google inspect <project> <url> [--format json]",
2455
2568
  run: async (input) => {
2456
- const project = requireProject(input, "google.inspect", "canonry google inspect <project> <url>");
2569
+ const project = requireProject(input, "google.inspect", "canonry google inspect <project> <url> [--format json]");
2457
2570
  const url = requirePositional(input, 1, {
2458
2571
  command: "google.inspect",
2459
- usage: "canonry google inspect <project> <url>",
2572
+ usage: "canonry google inspect <project> <url> [--format json]",
2460
2573
  message: "project name and URL are required"
2461
2574
  });
2462
2575
  await googleInspect(project, url, input.format);
@@ -2494,9 +2607,9 @@ var GOOGLE_CLI_COMMANDS = [
2494
2607
  },
2495
2608
  {
2496
2609
  path: ["google", "coverage"],
2497
- usage: "canonry google coverage <project>",
2610
+ usage: "canonry google coverage <project> [--format json]",
2498
2611
  run: async (input) => {
2499
- const project = requireProject(input, "google.coverage", "canonry google coverage <project>");
2612
+ const project = requireProject(input, "google.coverage", "canonry google coverage <project> [--format json]");
2500
2613
  await googleCoverage(project, input.format);
2501
2614
  }
2502
2615
  },
@@ -2520,9 +2633,9 @@ var GOOGLE_CLI_COMMANDS = [
2520
2633
  },
2521
2634
  {
2522
2635
  path: ["google", "deindexed"],
2523
- usage: "canonry google deindexed <project>",
2636
+ usage: "canonry google deindexed <project> [--format json]",
2524
2637
  run: async (input) => {
2525
- const project = requireProject(input, "google.deindexed", "canonry google deindexed <project>");
2638
+ const project = requireProject(input, "google.deindexed", "canonry google deindexed <project> [--format json]");
2526
2639
  await googleDeindexed(project, input.format);
2527
2640
  }
2528
2641
  },
@@ -3618,7 +3731,7 @@ var PROJECT_CLI_COMMANDS = [
3618
3731
  },
3619
3732
  {
3620
3733
  path: ["project", "show"],
3621
- usage: "canonry project show <name>",
3734
+ usage: "canonry project show <name> [--format json]",
3622
3735
  run: async (input) => {
3623
3736
  const name = requireProject(input, "project.show", "canonry project show <name>");
3624
3737
  await showProject(name, input.format);
@@ -3634,7 +3747,7 @@ var PROJECT_CLI_COMMANDS = [
3634
3747
  },
3635
3748
  {
3636
3749
  path: ["project", "add-location"],
3637
- usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country>",
3750
+ usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]",
3638
3751
  options: {
3639
3752
  label: stringOption(),
3640
3753
  city: stringOption(),
@@ -3646,7 +3759,7 @@ var PROJECT_CLI_COMMANDS = [
3646
3759
  const name = requireProject(
3647
3760
  input,
3648
3761
  "project.add-location",
3649
- "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country>"
3762
+ "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]"
3650
3763
  );
3651
3764
  const label = getString(input.values, "label");
3652
3765
  const city = getString(input.values, "city");
@@ -3657,7 +3770,7 @@ var PROJECT_CLI_COMMANDS = [
3657
3770
  message: "location label, city, region, and country are required",
3658
3771
  details: {
3659
3772
  command: "project.add-location",
3660
- usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country>",
3773
+ usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]",
3661
3774
  required: ["label", "city", "region", "country"]
3662
3775
  }
3663
3776
  });
@@ -3674,20 +3787,20 @@ var PROJECT_CLI_COMMANDS = [
3674
3787
  },
3675
3788
  {
3676
3789
  path: ["project", "locations"],
3677
- usage: "canonry project locations <name>",
3790
+ usage: "canonry project locations <name> [--format json]",
3678
3791
  run: async (input) => {
3679
- const name = requireProject(input, "project.locations", "canonry project locations <name>");
3792
+ const name = requireProject(input, "project.locations", "canonry project locations <name> [--format json]");
3680
3793
  await listLocations(name, input.format);
3681
3794
  }
3682
3795
  },
3683
3796
  {
3684
3797
  path: ["project", "remove-location"],
3685
- usage: "canonry project remove-location <name> <label>",
3798
+ usage: "canonry project remove-location <name> <label> [--format json]",
3686
3799
  run: async (input) => {
3687
- const name = requireProject(input, "project.remove-location", "canonry project remove-location <name> <label>");
3800
+ const name = requireProject(input, "project.remove-location", "canonry project remove-location <name> <label> [--format json]");
3688
3801
  const label = requirePositional(input, 1, {
3689
3802
  command: "project.remove-location",
3690
- usage: "canonry project remove-location <name> <label>",
3803
+ usage: "canonry project remove-location <name> <label> [--format json]",
3691
3804
  message: "project name and location label are required"
3692
3805
  });
3693
3806
  await removeLocation(name, label, input.format);
@@ -3695,12 +3808,12 @@ var PROJECT_CLI_COMMANDS = [
3695
3808
  },
3696
3809
  {
3697
3810
  path: ["project", "set-default-location"],
3698
- usage: "canonry project set-default-location <name> <label>",
3811
+ usage: "canonry project set-default-location <name> <label> [--format json]",
3699
3812
  run: async (input) => {
3700
- const name = requireProject(input, "project.set-default-location", "canonry project set-default-location <name> <label>");
3813
+ const name = requireProject(input, "project.set-default-location", "canonry project set-default-location <name> <label> [--format json]");
3701
3814
  const label = requirePositional(input, 1, {
3702
3815
  command: "project.set-default-location",
3703
- usage: "canonry project set-default-location <name> <label>",
3816
+ usage: "canonry project set-default-location <name> <label> [--format json]",
3704
3817
  message: "project name and location label are required"
3705
3818
  });
3706
3819
  await setDefaultLocation(name, label, input.format);
@@ -3984,7 +4097,7 @@ var RUN_CLI_COMMANDS = [
3984
4097
  },
3985
4098
  {
3986
4099
  path: ["run"],
3987
- usage: "canonry run <project> [--provider <name>] [--wait] [--format json]",
4100
+ usage: "canonry run <project|--all> [--provider <name>] [--location <label>] [--all-locations] [--no-location] [--wait] [--format json]",
3988
4101
  options: {
3989
4102
  provider: stringOption(),
3990
4103
  wait: { type: "boolean", default: false },
@@ -4855,6 +4968,126 @@ var SNAPSHOT_CLI_COMMANDS = [
4855
4968
  }
4856
4969
  ];
4857
4970
 
4971
+ // src/commands/insights.ts
4972
+ async function listInsights(project, opts) {
4973
+ const client = createApiClient();
4974
+ const insights = await client.getInsights(project, { dismissed: opts.dismissed });
4975
+ if (opts.format === "json") {
4976
+ console.log(JSON.stringify(insights, null, 2));
4977
+ return;
4978
+ }
4979
+ if (insights.length === 0) {
4980
+ console.log("No insights found.");
4981
+ return;
4982
+ }
4983
+ for (const insight of insights) {
4984
+ const severity = insight.severity.toUpperCase().padEnd(8);
4985
+ const dismissed = insight.dismissed ? " [dismissed]" : "";
4986
+ console.log(`${severity} ${insight.type.padEnd(12)} ${insight.title}${dismissed}`);
4987
+ if (insight.recommendation) {
4988
+ console.log(` Action: ${insight.recommendation.action}${insight.recommendation.target ? ` \u2192 ${insight.recommendation.target}` : ""}`);
4989
+ console.log(` Reason: ${insight.recommendation.reason}`);
4990
+ }
4991
+ if (insight.cause) {
4992
+ console.log(` Cause: ${insight.cause.cause}${insight.cause.competitorDomain ? ` (${insight.cause.competitorDomain})` : ""}`);
4993
+ }
4994
+ console.log("");
4995
+ }
4996
+ }
4997
+ async function dismissInsight(project, id, opts) {
4998
+ const client = createApiClient();
4999
+ const result = await client.dismissInsight(project, id);
5000
+ if (opts.format === "json") {
5001
+ console.log(JSON.stringify(result, null, 2));
5002
+ return;
5003
+ }
5004
+ console.log(`Insight ${id} dismissed.`);
5005
+ }
5006
+
5007
+ // src/commands/health-cmd.ts
5008
+ async function showHealth(project, opts) {
5009
+ const client = createApiClient();
5010
+ if (opts.history) {
5011
+ const snapshots = await client.getHealthHistory(project, opts.limit);
5012
+ if (opts.format === "json") {
5013
+ console.log(JSON.stringify(snapshots, null, 2));
5014
+ return;
5015
+ }
5016
+ if (snapshots.length === 0) {
5017
+ console.log("No health history available.");
5018
+ return;
5019
+ }
5020
+ console.log("Date Cited Rate Cited/Total");
5021
+ console.log("\u2500".repeat(55));
5022
+ for (const snap of snapshots) {
5023
+ const rate2 = (snap.overallCitedRate * 100).toFixed(1).padStart(5) + "%";
5024
+ const ratio = `${snap.citedPairs}/${snap.totalPairs}`;
5025
+ const date = snap.createdAt.slice(0, 19).padEnd(25);
5026
+ console.log(`${date} ${rate2} ${ratio}`);
5027
+ }
5028
+ return;
5029
+ }
5030
+ const health = await client.getHealth(project);
5031
+ if (opts.format === "json") {
5032
+ console.log(JSON.stringify(health, null, 2));
5033
+ return;
5034
+ }
5035
+ const rate = (health.overallCitedRate * 100).toFixed(1);
5036
+ console.log(`Health: ${rate}% cited (${health.citedPairs}/${health.totalPairs} pairs)`);
5037
+ console.log("");
5038
+ if (health.providerBreakdown && Object.keys(health.providerBreakdown).length > 0) {
5039
+ console.log("Provider Breakdown:");
5040
+ for (const [provider, stats] of Object.entries(health.providerBreakdown)) {
5041
+ const pRate = (stats.citedRate * 100).toFixed(1);
5042
+ console.log(` ${provider.padEnd(15)} ${pRate}% (${stats.cited}/${stats.total})`);
5043
+ }
5044
+ }
5045
+ }
5046
+
5047
+ // src/cli-commands/intelligence.ts
5048
+ var INTELLIGENCE_CLI_COMMANDS = [
5049
+ {
5050
+ path: ["insights"],
5051
+ usage: "canonry insights <project> [--dismissed] [--format json]",
5052
+ options: {
5053
+ dismissed: { type: "boolean" }
5054
+ },
5055
+ run: async (input) => {
5056
+ const usage = "canonry insights <project> [--dismissed] [--format json]";
5057
+ const project = requireProject(input, "insights", usage);
5058
+ const dismissed = input.values.dismissed === true;
5059
+ await listInsights(project, { dismissed, format: input.format });
5060
+ }
5061
+ },
5062
+ {
5063
+ path: ["insights", "dismiss"],
5064
+ usage: "canonry insights dismiss <project> <id> [--format json]",
5065
+ options: {},
5066
+ run: async (input) => {
5067
+ const usage = "canonry insights dismiss <project> <id> [--format json]";
5068
+ const project = requireProject(input, "insights dismiss", usage);
5069
+ const id = requirePositional(input, 1, { command: "insights dismiss", usage, message: "insight ID is required" });
5070
+ await dismissInsight(project, id, { format: input.format });
5071
+ }
5072
+ },
5073
+ {
5074
+ path: ["health"],
5075
+ usage: "canonry health <project> [--history] [--limit <n>] [--format json]",
5076
+ options: {
5077
+ history: { type: "boolean" },
5078
+ limit: { type: "string" }
5079
+ },
5080
+ run: async (input) => {
5081
+ const usage = "canonry health <project> [--history] [--limit <n>] [--format json]";
5082
+ const project = requireProject(input, "health", usage);
5083
+ const history = input.values.history === true;
5084
+ const limitStr = getString(input.values, "limit");
5085
+ const limit = limitStr ? Number.parseInt(limitStr, 10) : void 0;
5086
+ await showHealth(project, { history, limit, format: input.format });
5087
+ }
5088
+ }
5089
+ ];
5090
+
4858
5091
  // src/commands/bootstrap.ts
4859
5092
  import crypto from "crypto";
4860
5093
  import path2 from "path";
@@ -5035,15 +5268,17 @@ async function bootstrapCommand(_opts) {
5035
5268
  const keyPrefix = rawApiKey.slice(0, 9);
5036
5269
  const db = createClient(databasePath);
5037
5270
  migrate(db);
5038
- db.delete(apiKeys).where(eq2(apiKeys.name, "default")).run();
5039
- db.insert(apiKeys).values({
5040
- id: crypto.randomUUID(),
5041
- name: "default",
5042
- keyHash,
5043
- keyPrefix,
5044
- scopes: '["*"]',
5045
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
5046
- }).run();
5271
+ db.transaction((tx) => {
5272
+ tx.delete(apiKeys).where(eq2(apiKeys.name, "default")).run();
5273
+ tx.insert(apiKeys).values({
5274
+ id: crypto.randomUUID(),
5275
+ name: "default",
5276
+ keyHash,
5277
+ keyPrefix,
5278
+ scopes: '["*"]',
5279
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5280
+ }).run();
5281
+ });
5047
5282
  saveConfig({
5048
5283
  apiUrl: env.apiUrl || existingConfig?.apiUrl || `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
5049
5284
  database: databasePath,
@@ -6814,7 +7049,8 @@ var REGISTERED_CLI_COMMANDS = [
6814
7049
  ...BING_CLI_COMMANDS,
6815
7050
  ...WORDPRESS_CLI_COMMANDS,
6816
7051
  ...CDP_CLI_COMMANDS,
6817
- ...GA_CLI_COMMANDS
7052
+ ...GA_CLI_COMMANDS,
7053
+ ...INTELLIGENCE_CLI_COMMANDS
6818
7054
  ];
6819
7055
 
6820
7056
  // src/cli.ts
@@ -6822,160 +7058,53 @@ import { createRequire } from "module";
6822
7058
  var USAGE = `
6823
7059
  canonry \u2014 AEO monitoring CLI
6824
7060
 
6825
- Usage:
6826
- canonry init [--force] Initialize config and database (interactive)
6827
- canonry init --gemini-key <key> Initialize non-interactively (also reads env vars)
6828
- canonry bootstrap [--force] Bootstrap config/database from env vars
6829
- canonry backfill answer-visibility Backfill answer-level visibility from stored answers
6830
- canonry serve Start the local server (foreground)
6831
- canonry start Start the server as a background daemon
6832
- canonry stop Stop the background daemon
6833
- canonry project create <name> Create a project
6834
- canonry project update <name> Update project settings
6835
- canonry project list List all projects
6836
- canonry project show <name> Show project details
6837
- canonry project delete <name> Delete a project
6838
- canonry project add-location <name> Add a location (--label, --city, --region, --country)
6839
- canonry project locations <name> List locations for a project
6840
- canonry project remove-location <name> <label> Remove a location
6841
- canonry project set-default-location <name> <label> Set default location
6842
- canonry keyword add <project> <kw> Add key phrases to a project
6843
- canonry keyword remove <project> <kw> Remove key phrases from a project
6844
- canonry keyword list <project> List key phrases for a project
6845
- canonry keyword import <project> <file> Import key phrases from file
6846
- canonry keyword generate <project> Auto-generate key phrases (--provider, --count, --save)
6847
- canonry competitor add <project> <domain> Add competitors
6848
- canonry competitor list <project> List competitors
6849
- canonry snapshot <company> --domain <domain> One-shot AI perception report
6850
- canonry run <project> Trigger a run (all providers)
6851
- canonry run <project> --provider <name> Trigger a run for a specific provider
6852
- canonry run <project> --location <label> Run with a specific location
6853
- canonry run <project> --all-locations Run for every configured location (N\xD7 API calls)
6854
- canonry run <project> --no-location Explicitly skip location context
6855
- canonry run <project> --wait Trigger and wait for completion
6856
- canonry run --all Trigger runs for all projects
6857
- canonry run show <id> Show run details and snapshots
6858
- canonry runs <project> List runs for a project (--limit <n>)
6859
- canonry status <project> Show project summary [--format json]
6860
- canonry evidence <project> Show per-phrase results [--format json]
6861
- canonry analytics <project> Show analytics (--feature metrics|gaps|sources, --window 7d|30d|90d|all)
6862
- canonry history <project> Show audit trail [--format json]
6863
- canonry export <project> Export project as YAML
6864
- canonry apply <file...> Apply declarative config (multi-doc YAML supported)
6865
- canonry schedule set <project> Set schedule (--preset or --cron)
6866
- canonry schedule show <project> Show schedule
6867
- canonry schedule enable <project> Enable schedule
6868
- canonry schedule disable <project> Disable schedule
6869
- canonry schedule remove <project> Remove schedule
6870
- canonry notify add <project> Add webhook notification
6871
- canonry notify list <project> List notifications
6872
- canonry notify remove <project> <id> Remove notification
6873
- canonry notify test <project> <id> Send test webhook
6874
- canonry notify events List available notification event types
6875
- canonry google connect <project> Connect Google Search Console (--type gsc|ga4, --public-url <url>)
6876
- canonry google disconnect <project> Disconnect Google integration
6877
- canonry google status <project> Show Google connection status
6878
- canonry google properties <project> List available GSC properties
6879
- canonry google set-property <project> <url> Set GSC property URL
6880
- canonry google set-sitemap <project> <url> Set GSC sitemap URL
6881
- canonry google list-sitemaps <project> List submitted sitemaps from GSC (no run queued)
6882
- canonry google discover-sitemaps <project> Auto-discover sitemaps from GSC and queue inspection (--wait)
6883
- canonry google sync <project> Sync GSC data (--days 30, --full, --wait)
6884
- canonry google performance <project> Show GSC search performance data
6885
- canonry google inspect <project> <url> Inspect a URL via GSC
6886
- canonry google inspect-sitemap <project> Bulk inspect all URLs from sitemap (--sitemap-url, --wait)
6887
- canonry google request-indexing <project> <url> Request Google indexing for a URL
6888
- canonry google request-indexing <project> --all-unindexed Request indexing for all unindexed URLs
6889
- canonry google coverage <project> Show index coverage summary
6890
- canonry google refresh <project> Force-fetch fresh GSC coverage data and display updated summary
6891
- canonry google inspections <project> Show URL inspection history (--url <url>)
6892
- canonry google deindexed <project> Show pages that lost indexing
6893
- canonry bing connect <project> Connect Bing Webmaster Tools (prompted for API key)
6894
- canonry bing disconnect <project> Disconnect Bing integration
6895
- canonry bing status <project> Show Bing connection status
6896
- canonry bing sites <project> List registered Bing sites
6897
- canonry bing set-site <project> <url> Set active Bing site
6898
- canonry bing coverage <project> Show Bing index coverage summary
6899
- canonry bing refresh <project> Force-fetch fresh Bing coverage data and display updated summary
6900
- canonry bing inspect <project> <url> Inspect a URL via Bing
6901
- canonry bing inspections <project> Show Bing URL inspection history (--url <url>)
6902
- canonry bing request-indexing <project> <url> Submit URL to Bing for indexing
6903
- canonry bing request-indexing <project> --all-unindexed Submit all unindexed URLs to Bing
6904
- canonry bing performance <project> Show Bing search performance data
6905
- canonry wordpress connect <project> Connect WordPress REST access (--url, --user, --staging-url)
6906
- canonry wordpress disconnect <project> Disconnect WordPress integration
6907
- canonry wordpress status <project> Show WordPress connection status
6908
- canonry wordpress pages <project> List WordPress pages (--live|--staging)
6909
- canonry wordpress page <project> <slug> Show a WordPress page
6910
- canonry wordpress create-page <project> Create a WordPress page (--title, --slug, --content/--content-file)
6911
- canonry wordpress update-page <project> <slug> Update a WordPress page (--content/--content-file)
6912
- canonry wordpress set-meta <project> <slug> Update REST-exposed SEO meta
6913
- canonry wordpress set-meta <project> --from <file> Bulk update SEO meta from JSON file
6914
- canonry wordpress schema <project> <slug> Read rendered JSON-LD schema
6915
- canonry wordpress schema deploy <project> --profile <file> Deploy JSON-LD schema to pages
6916
- canonry wordpress schema status <project> Show schema status per page
6917
- canonry wordpress set-schema <project> <slug> Generate manual schema handoff
6918
- canonry wordpress onboard <project> --url <url> --user <user> Full onboarding workflow
6919
- canonry wordpress llms-txt <project> Read /llms.txt
6920
- canonry wordpress set-llms-txt <project> Generate manual llms.txt handoff
6921
- canonry wordpress audit <project> Audit WordPress pages for SEO/content issues
6922
- canonry wordpress diff <project> <slug> Compare live vs staging for a page
6923
- canonry wordpress staging status <project> Show staging configuration
6924
- canonry wordpress staging push <project> Generate manual staging push instructions
6925
- canonry settings Show active provider and quota settings
6926
- canonry settings provider <name> Update a provider config
6927
- canonry settings google Update Google OAuth credentials
6928
- canonry telemetry status Show telemetry status
6929
- canonry telemetry enable Enable anonymous telemetry
6930
- canonry telemetry disable Disable anonymous telemetry
6931
- canonry --help Show this help
6932
- canonry --version Show version
7061
+ Usage: canonry <command> [options]
7062
+
7063
+ Setup:
7064
+ init Initialize config and database
7065
+ bootstrap Bootstrap config/database from env vars
7066
+ serve Start the local server (foreground)
7067
+ start / stop Start/stop as a background daemon
7068
+
7069
+ Projects:
7070
+ project Create, update, list, show, delete projects
7071
+ keyword Add, remove, list, import, generate key phrases
7072
+ competitor Add, list competitors
7073
+
7074
+ Monitoring:
7075
+ run Trigger visibility sweeps
7076
+ snapshot One-shot AI perception report
7077
+ status <project> Show project summary
7078
+ evidence <project> Show per-phrase results
7079
+ analytics <project> Show analytics (metrics, gaps, sources)
7080
+ insights <project> Show intelligence insights
7081
+ health <project> Show citation health
7082
+
7083
+ Config-as-Code:
7084
+ apply <file...> Apply declarative config (YAML)
7085
+ export <project> Export project as YAML
6933
7086
 
6934
- Options:
6935
- --gemini-key <key> Gemini API key (or GEMINI_API_KEY env var)
6936
- --openai-key <key> OpenAI API key (or OPENAI_API_KEY env var)
6937
- --claude-key <key> Anthropic API key (or ANTHROPIC_API_KEY env var)
6938
- --perplexity-key <key> Perplexity API key (or PERPLEXITY_API_KEY env var)
6939
- --local-url <url> Local LLM base URL (or LOCAL_BASE_URL env var)
6940
- --local-model <name> Local LLM model name (default: llama3)
6941
- --local-key <key> Local LLM API key (or LOCAL_API_KEY env var)
6942
- --google-client-id <id> Google OAuth client ID (or GOOGLE_CLIENT_ID env var)
6943
- --google-client-secret <key> Google OAuth client secret (or GOOGLE_CLIENT_SECRET env var)
6944
- --port <port> Server port (default: 4100)
6945
- --host <host> Server bind address (default: 127.0.0.1)
6946
- --domain <domain> Canonical domain for project create/update
6947
- --owned-domain <domain> Additional owned domain for citation matching (repeatable)
6948
- --add-domain <domain> Add an owned domain (project update, repeatable)
6949
- --remove-domain <domain> Remove an owned domain (project update, repeatable)
6950
- --display-name <name> Display name for project create/update
6951
- --country <code> Country code (default: US)
6952
- --language <lang> Language code (default: en)
6953
- --provider <name> Provider to use (gemini, openai, claude, perplexity, local, cdp:chatgpt, or cdp for all CDP targets)
6954
- --format <fmt> Output format: text (default) or json
6955
- --phrases <list> Comma-separated category queries (snapshot)
6956
- --competitors <list> Comma-separated competitor hints (snapshot)
6957
- --pdf <path> Write a PDF snapshot report to a file
6958
- --location <label> Run with a specific configured location
6959
- --all-locations Run for every configured location
6960
- --no-location Explicitly skip location context
6961
- --wait Wait for run to complete before returning
6962
- --all Run all projects (with 'run' command)
6963
- --project <name> Restrict maintenance commands to a single project
6964
- --include-results Include results in export
6965
- --preset <preset> Schedule preset (daily, weekly, twice-daily, daily@HH, weekly@DAY)
6966
- --cron <expr> Cron expression for schedule
6967
- --timezone <tz> IANA timezone for schedule (default: UTC)
6968
- --webhook <url> Webhook URL for notifications
6969
- --events <list> Comma-separated notification events
6970
- --limit <n> Maximum number of results to return where supported
6971
- --api-key <key> Provider API key (settings provider)
6972
- --base-url <url> Provider base URL (settings provider)
6973
- --model <name> Provider model name (settings provider)
6974
- --client-id <id> Google OAuth client ID (settings google)
6975
- --client-secret <key> Google OAuth client secret (settings google)
6976
- --max-concurrent <n> Max concurrent requests per provider
6977
- --max-per-minute <n> Max requests per minute per provider
6978
- --max-per-day <n> Max requests per day per provider
7087
+ Integrations:
7088
+ google Google Search Console / Analytics
7089
+ bing Bing Webmaster Tools
7090
+ wordpress WordPress REST API
7091
+
7092
+ Automation:
7093
+ schedule Manage scheduled runs
7094
+ notify Manage webhook notifications
7095
+
7096
+ Admin:
7097
+ settings Show/update provider and quota settings
7098
+ backfill Backfill answer visibility or insights
7099
+ telemetry Manage anonymous telemetry
7100
+ history <project> Show audit trail
7101
+
7102
+ Global options:
7103
+ --format json Machine-readable output (all commands)
7104
+ --help, -h Show help (use with any command group)
7105
+ --version, -v Show version
7106
+
7107
+ Run 'canonry <command> --help' for details on a specific command.
6979
7108
  `.trim();
6980
7109
  var _require = createRequire(import.meta.url);
6981
7110
  var { version: VERSION } = _require("../package.json");
@@ -6985,7 +7114,7 @@ function extractFormat(cmdArgs) {
6985
7114
  return "text";
6986
7115
  }
6987
7116
  async function runCli(args = process.argv.slice(2)) {
6988
- if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
7117
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
6989
7118
  console.log(USAGE);
6990
7119
  return 0;
6991
7120
  }
@@ -6995,13 +7124,25 @@ async function runCli(args = process.argv.slice(2)) {
6995
7124
  }
6996
7125
  const command = args[0];
6997
7126
  const format = extractFormat(args);
6998
- if (command !== "telemetry" && command !== "init" && isTelemetryEnabled() && isFirstRun()) {
7127
+ const isHelpRequest = args.includes("--help") || args.includes("-h");
7128
+ if (!isHelpRequest && command !== "telemetry" && command !== "init" && isTelemetryEnabled() && isFirstRun()) {
6999
7129
  showFirstRunNotice();
7000
7130
  getOrCreateAnonymousId();
7001
7131
  }
7002
7132
  const SUBCOMMAND_COMMANDS = /* @__PURE__ */ new Set(["backfill", "project", "keyword", "competitor", "schedule", "notify", "settings", "telemetry", "google", "bing", "wordpress", "cdp"]);
7003
- const resolvedCommand = SUBCOMMAND_COMMANDS.has(command) && args[1] && !args[1].startsWith("-") ? `${command}.${args[1]}` : command;
7004
- if (command !== "telemetry") {
7133
+ const MIXED_SUBCOMMANDS = {
7134
+ insights: /* @__PURE__ */ new Set(["dismiss"]),
7135
+ run: /* @__PURE__ */ new Set(["show", "cancel"])
7136
+ };
7137
+ let resolvedCommand;
7138
+ if (SUBCOMMAND_COMMANDS.has(command) && args[1] && !args[1].startsWith("-")) {
7139
+ resolvedCommand = `${command}.${args[1]}`;
7140
+ } else if (MIXED_SUBCOMMANDS[command] && args[1] && MIXED_SUBCOMMANDS[command].has(args[1])) {
7141
+ resolvedCommand = `${command}.${args[1]}`;
7142
+ } else {
7143
+ resolvedCommand = command;
7144
+ }
7145
+ if (!isHelpRequest && command !== "telemetry") {
7005
7146
  trackEvent("cli.command", { command: resolvedCommand });
7006
7147
  }
7007
7148
  try {