@canonry/canonry 7.22.0 → 7.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +14 -0
  2. package/assets/assets/{AuditHistoryPanel-J8AR3x5F.js → AuditHistoryPanel-DKKnX8IT.js} +1 -1
  3. package/assets/assets/{BacklinksPage-CRDPutGA.js → BacklinksPage-DNbur8am.js} +1 -1
  4. package/assets/assets/{HistoryPage-C5taVW3t.js → HistoryPage-DahtHlpM.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-txucmLuz.js → MeasurementPropertyPage-BoNDtLfp.js} +1 -1
  6. package/assets/assets/{ProjectPage-C4FaBoWI.js → ProjectPage--ZlpQduH.js} +1 -1
  7. package/assets/assets/{RunsPage-D-3XKUhh.js → RunsPage-DKB2CHTM.js} +1 -1
  8. package/assets/assets/{SettingsPage-Cq_SJRj_.js → SettingsPage-lnmMZzEj.js} +1 -1
  9. package/assets/assets/{SiteHealthSection-Cn_IZVO1.js → SiteHealthSection-BBjucRed.js} +3 -3
  10. package/assets/assets/{TrafficPage-BMdFZs3X.js → TrafficPage-DNyTmPLd.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-BmrIOc-2.js → TrafficSourceDetailPage-D1zKZHSh.js} +1 -1
  12. package/assets/assets/{extract-error-message-HlQy5L7O.js → extract-error-message-v0b8P-wc.js} +1 -1
  13. package/assets/assets/index-CfaBRiBG.js +86 -0
  14. package/assets/assets/{react-sigma_core.esm.min-CogAr1i4.js → react-sigma_core.esm.min-BZSJoRrV.js} +1 -1
  15. package/assets/assets/{v2-overview-adapter-tps7TUxq.js → v2-overview-adapter-DXAl-O_h.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-222NAFTY.js → chunk-7Q7QCBJ3.js} +2 -2
  18. package/dist/{chunk-5IXQAOVK.js → chunk-DS2ZKB3T.js} +15 -6
  19. package/dist/{chunk-MRKV43TV.js → chunk-IM2BFAAT.js} +565 -206
  20. package/dist/{chunk-NDQOCFJQ.js → chunk-JJBVYL45.js} +39 -4
  21. package/dist/{chunk-46JZR2IV.js → chunk-O5UMRQVN.js} +1303 -504
  22. package/dist/{chunk-6Z2LFFRT.js → chunk-SG3MPCDL.js} +5731 -4596
  23. package/dist/{chunk-6H2BC5EY.js → chunk-YCPTP2NC.js} +3 -3
  24. package/dist/cli.js +371 -143
  25. package/dist/{demo-server-32PU7BM6.js → demo-server-C7UGXXN7.js} +4 -4
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +6 -6
  28. package/dist/{intelligence-service-256DQBVR.js → intelligence-service-37TX4YV7.js} +2 -2
  29. package/dist/mcp.js +4 -4
  30. package/package.json +13 -13
  31. package/assets/assets/index-BJcjB5XW.js +0 -86
package/dist/cli.js CHANGED
@@ -14,13 +14,16 @@ import {
14
14
  getOrCreateAnonymousId,
15
15
  getPrintedUpdateAvailable,
16
16
  getTelemetryStatus,
17
+ installAttribution,
17
18
  isAeroToolProfile,
18
19
  isFirstRun,
19
20
  isLoopbackBindHost,
20
21
  isTelemetryEnabled,
21
22
  listAgentProviders,
22
23
  notePrintedUpdateAvailable,
24
+ outcomeFailure,
23
25
  readCachedUpdateAvailable,
26
+ recordInstallRef,
24
27
  registeredProviderNames,
25
28
  reparseStoredResult,
26
29
  reparseStoredResult2,
@@ -34,13 +37,16 @@ import {
34
37
  showFirstRunNotice,
35
38
  trackCliCommandFinished,
36
39
  trackEvent,
40
+ trackFeatureCompleted,
41
+ trackIntegrationConnection,
37
42
  waitForServerRuntimeStartup
38
- } from "./chunk-6Z2LFFRT.js";
43
+ } from "./chunk-SG3MPCDL.js";
39
44
  import {
40
45
  autoSyncSkills,
41
46
  formatAutoSyncNotice
42
- } from "./chunk-6H2BC5EY.js";
47
+ } from "./chunk-YCPTP2NC.js";
43
48
  import {
49
+ clearDashboardPassword,
44
50
  configExists,
45
51
  createApiClient,
46
52
  emitInstallSummary,
@@ -57,14 +63,16 @@ import {
57
63
  saveConfigPatch,
58
64
  shouldPrintServeSkillsNudge,
59
65
  withBootstrapProviderDefaults
60
- } from "./chunk-NDQOCFJQ.js";
66
+ } from "./chunk-JJBVYL45.js";
61
67
  import {
62
68
  DEFAULT_DEMO_TRUSTED_PROXIES,
63
69
  trustedProxiesProblem,
64
70
  trustedProxyProblem
65
71
  } from "./chunk-RB2F4D3Z.js";
66
- import "./chunk-222NAFTY.js";
72
+ import "./chunk-7Q7QCBJ3.js";
67
73
  import {
74
+ CANONRY_DISCORD_URL,
75
+ CANONRY_ISSUES_URL,
68
76
  CliError,
69
77
  EXIT_SYSTEM_ERROR,
70
78
  EXIT_USER_ERROR,
@@ -75,13 +83,14 @@ import {
75
83
  runMeasurementDraftAction,
76
84
  systemError,
77
85
  usageError
78
- } from "./chunk-5IXQAOVK.js";
86
+ } from "./chunk-DS2ZKB3T.js";
79
87
  import {
80
88
  CLOUDFLARE_WORKER_BINDINGS,
81
89
  CLOUDFLARE_WORKER_GENERATED_MARKER,
82
90
  CLOUDFLARE_WRANGLER_GENERATED_MARKER,
83
91
  DEFAULT_BOT_LIST,
84
92
  aiUserFetchEventsHourly,
93
+ anyUsersExist,
85
94
  apiKeys,
86
95
  auditLog,
87
96
  backfillProjectAnswerMentions,
@@ -107,7 +116,7 @@ import {
107
116
  rawEventSamples,
108
117
  readMarketCompetitorNames,
109
118
  runs
110
- } from "./chunk-46JZR2IV.js";
119
+ } from "./chunk-O5UMRQVN.js";
111
120
  import {
112
121
  AdsDeliverySnapshotStatuses,
113
122
  AdsHistoricalCampaignRollupStatuses,
@@ -126,6 +135,8 @@ import {
126
135
  GaMoverChangeBases,
127
136
  MeasurementEvidenceShapes,
128
137
  OUTSTANDING_PROVIDER_BATCH_STATUSES,
138
+ OutcomeReasonCodes,
139
+ OutcomeTriggers,
129
140
  ProviderBatchStatuses,
130
141
  ProviderNames,
131
142
  ProviderReloadErrorReasons,
@@ -164,6 +175,7 @@ import {
164
175
  buildModelChangeNotice,
165
176
  canonicalizeGtmAccountId,
166
177
  canonicalizeGtmResourceSelection,
178
+ classifyOutcomeError,
167
179
  competitorAutoAliasModeSchema,
168
180
  competitorLandscapeQuerySchema,
169
181
  computeCompetitorOverlap,
@@ -241,7 +253,7 @@ import {
241
253
  visibilityCompareSelectionSchema,
242
254
  visibilityReportRequestSchema,
243
255
  winnabilityClassSchema
244
- } from "./chunk-MRKV43TV.js";
256
+ } from "./chunk-IM2BFAAT.js";
245
257
 
246
258
  // src/cli.ts
247
259
  import { pathToFileURL } from "url";
@@ -290,9 +302,9 @@ Usage: ${spec.usage}
290
302
  if (spec.help) console.log(`${spec.help}
291
303
  `);
292
304
  }
293
- function matchesPath(args, path12) {
294
- if (args.length < path12.length) return false;
295
- return path12.every((segment, index) => args[index] === segment);
305
+ function matchesPath(args, path13) {
306
+ if (args.length < path13.length) return false;
307
+ return path13.every((segment, index) => args[index] === segment);
296
308
  }
297
309
  function withGlobalOptions(options) {
298
310
  const base = options ? { ...options } : {};
@@ -787,7 +799,7 @@ function parseDemoTrustedProxies(values) {
787
799
  async function demoCommand(options) {
788
800
  const { host, port } = parseDemoListenOptions(options);
789
801
  const trustProxy = parseDemoTrustedProxies(options.trustProxy);
790
- const { createDemoServer } = await import("./demo-server-32PU7BM6.js");
802
+ const { createDemoServer } = await import("./demo-server-C7UGXXN7.js");
791
803
  const app = await createDemoServer({ trustProxy });
792
804
  let stopping = false;
793
805
  const stop = () => {
@@ -1201,7 +1213,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
1201
1213
  }
1202
1214
  }
1203
1215
  async function backfillInsightsCommand(project2, opts) {
1204
- const { IntelligenceService } = await import("./intelligence-service-256DQBVR.js");
1216
+ const { IntelligenceService } = await import("./intelligence-service-37TX4YV7.js");
1205
1217
  const config = loadConfig();
1206
1218
  const db = createClient(config.database);
1207
1219
  migrate(db);
@@ -2824,6 +2836,11 @@ var BING_CLI_COMMANDS = [
2824
2836
  }
2825
2837
  ];
2826
2838
 
2839
+ // src/cli-connection-telemetry.ts
2840
+ function trackCliConnection(properties) {
2841
+ trackIntegrationConnection({ ...properties, surface: "cli", agent: cliRuntimeContext().agent });
2842
+ }
2843
+
2827
2844
  // src/commands/cdp.ts
2828
2845
  function getClient3() {
2829
2846
  return createApiClient();
@@ -2837,7 +2854,13 @@ async function cdpConnect(opts) {
2837
2854
  host,
2838
2855
  port
2839
2856
  };
2840
- saveConfigPatch(config);
2857
+ try {
2858
+ saveConfigPatch(config);
2859
+ } catch (err) {
2860
+ trackCliConnection({ integration: "cdp", action: "connect", status: "failed", ...outcomeFailure(err) });
2861
+ throw err;
2862
+ }
2863
+ trackCliConnection({ integration: "cdp", action: "connect", status: "succeeded" });
2841
2864
  if (isMachineFormat(opts.format)) {
2842
2865
  console.log(JSON.stringify({
2843
2866
  host,
@@ -3015,6 +3038,80 @@ var CDP_CLI_COMMANDS = [
3015
3038
  }
3016
3039
  ];
3017
3040
 
3041
+ // src/commands/dashboard.ts
3042
+ import fs2 from "fs";
3043
+ function namedAccountsInUse() {
3044
+ const database = loadConfigRaw()?.database;
3045
+ if (typeof database !== "string" || !fs2.existsSync(database)) return false;
3046
+ try {
3047
+ const db = createClient(database);
3048
+ try {
3049
+ return anyUsersExist(db);
3050
+ } finally {
3051
+ db.$client.close();
3052
+ }
3053
+ } catch {
3054
+ return false;
3055
+ }
3056
+ }
3057
+ function nextStepsFor(cleared, namedAccounts) {
3058
+ if (namedAccounts) {
3059
+ return [
3060
+ "This install signs in with named accounts, so the shared dashboard password is not used and clearing it does not change who can sign in.",
3061
+ 'To restore an account, run "canonry user list", then "canonry user delete <name>" and "canonry user create".'
3062
+ ];
3063
+ }
3064
+ if (!cleared) {
3065
+ return ["If the dashboard still asks for a password, restart the server: it keeps the password it started with."];
3066
+ }
3067
+ return [
3068
+ "Restart the server to apply the reset. Until then it still accepts the old password.",
3069
+ // First-run setup on a loopback bind takes the password from any process
3070
+ // that can open a loopback connection (docs/deployment.md).
3071
+ "Then open the dashboard and create the new password right away. Until one is set, a server that listens only on this machine accepts it from any local connection, so stop any port forwarder (ssh -R, ngrok tcp, tailscale serve --tcp) first."
3072
+ ];
3073
+ }
3074
+ async function resetDashboardPassword(opts) {
3075
+ const { cleared, configPath } = clearDashboardPassword();
3076
+ const namedAccounts = namedAccountsInUse();
3077
+ const nextSteps = nextStepsFor(cleared, namedAccounts);
3078
+ if (isMachineFormat(opts.format)) {
3079
+ console.log(JSON.stringify({
3080
+ reset: cleared,
3081
+ configPath,
3082
+ restartRequired: cleared && !namedAccounts,
3083
+ namedAccounts,
3084
+ nextSteps
3085
+ }, null, 2));
3086
+ return;
3087
+ }
3088
+ console.log(cleared ? `Dashboard password cleared from ${configPath}.` : `No dashboard password is set in ${configPath}; nothing to reset.`);
3089
+ for (const step of nextSteps) console.log(step);
3090
+ }
3091
+
3092
+ // src/cli-commands/dashboard.ts
3093
+ var DASHBOARD_CLI_COMMANDS = [
3094
+ {
3095
+ path: ["dashboard", "reset-password"],
3096
+ usage: "canonry dashboard reset-password [--format json]",
3097
+ help: "Clears the dashboard password from config.yaml, so the dashboard asks for a new one after a restart.\nLocal only: it edits the config file under CANONRY_CONFIG_DIR and does not call the server.",
3098
+ run: async (input) => {
3099
+ await resetDashboardPassword({ format: input.format });
3100
+ }
3101
+ },
3102
+ {
3103
+ path: ["dashboard"],
3104
+ usage: "canonry dashboard <reset-password>",
3105
+ run: async (input) => {
3106
+ unknownSubcommand(input.positionals[0], {
3107
+ command: "dashboard",
3108
+ usage: "canonry dashboard <reset-password>",
3109
+ available: ["reset-password"]
3110
+ });
3111
+ }
3112
+ }
3113
+ ];
3114
+
3018
3115
  // src/commands/discover.ts
3019
3116
  var TERMINAL_DISCOVERY_STATUSES = /* @__PURE__ */ new Set([
3020
3117
  "completed",
@@ -3360,7 +3457,7 @@ async function pollSession(client, project2, sessionId, quiet = false) {
3360
3457
  }
3361
3458
 
3362
3459
  // src/commands/discover-eval.ts
3363
- import fs2 from "fs";
3460
+ import fs3 from "fs";
3364
3461
  import path from "path";
3365
3462
 
3366
3463
  // src/discovery-eval.ts
@@ -3589,7 +3686,7 @@ async function discoverEvalWithClient(client, opts) {
3589
3686
  const baselinePath = path.resolve(opts.baseline ?? DEFAULT_BASELINE_PATH);
3590
3687
  if (opts.updateBaseline) {
3591
3688
  const baseline2 = { capturedAt: (/* @__PURE__ */ new Date()).toISOString(), scorecards };
3592
- fs2.writeFileSync(baselinePath, JSON.stringify(baseline2, null, 2) + "\n");
3689
+ fs3.writeFileSync(baselinePath, JSON.stringify(baseline2, null, 2) + "\n");
3593
3690
  if (opts.format === "json" || opts.format === "jsonl") {
3594
3691
  console.log(JSON.stringify({ scorecards, baselineWritten: baselinePath }, null, 2));
3595
3692
  } else {
@@ -3598,14 +3695,14 @@ async function discoverEvalWithClient(client, opts) {
3598
3695
  }
3599
3696
  return;
3600
3697
  }
3601
- if (!fs2.existsSync(baselinePath)) {
3698
+ if (!fs3.existsSync(baselinePath)) {
3602
3699
  throw new CliError({
3603
3700
  code: "EVAL_NO_BASELINE",
3604
3701
  message: `no baseline at ${baselinePath}; run with --update-baseline to capture one`,
3605
3702
  exitCode: EXIT_USER_ERROR
3606
3703
  });
3607
3704
  }
3608
- const baseline = JSON.parse(fs2.readFileSync(baselinePath, "utf8"));
3705
+ const baseline = JSON.parse(fs3.readFileSync(baselinePath, "utf8"));
3609
3706
  const verdict = compareToBaseline(scorecards, baseline);
3610
3707
  if (opts.format === "json" || opts.format === "jsonl") {
3611
3708
  console.log(JSON.stringify({ scorecards, verdict, baseline: { capturedAt: baseline.capturedAt } }, null, 2));
@@ -4437,7 +4534,7 @@ var DOCTOR_CLI_COMMANDS = [
4437
4534
  ];
4438
4535
 
4439
4536
  // src/commands/ads.ts
4440
- import fs3 from "fs";
4537
+ import fs4 from "fs";
4441
4538
  function getClient6() {
4442
4539
  return createApiClient();
4443
4540
  }
@@ -4450,7 +4547,7 @@ function readRequest(inputPath, schema) {
4450
4547
  });
4451
4548
  }
4452
4549
  try {
4453
- const raw = fs3.readFileSync(inputPath === "-" ? 0 : inputPath, "utf8");
4550
+ const raw = fs4.readFileSync(inputPath === "-" ? 0 : inputPath, "utf8");
4454
4551
  return schema.parse(JSON.parse(raw));
4455
4552
  } catch (err) {
4456
4553
  if (err instanceof CliError) throw err;
@@ -5326,9 +5423,9 @@ async function gaConnect(project2, opts) {
5326
5423
  propertyId: opts.propertyId
5327
5424
  };
5328
5425
  if (opts.keyFile) {
5329
- const fs19 = await import("fs");
5426
+ const fs21 = await import("fs");
5330
5427
  try {
5331
- const content = fs19.readFileSync(opts.keyFile, "utf-8");
5428
+ const content = fs21.readFileSync(opts.keyFile, "utf-8");
5332
5429
  JSON.parse(content);
5333
5430
  body.keyJson = content;
5334
5431
  } catch (e) {
@@ -6916,7 +7013,7 @@ var GBP_CLI_COMMANDS = [
6916
7013
  ];
6917
7014
 
6918
7015
  // src/commands/google-marketing.ts
6919
- import fs4 from "fs";
7016
+ import fs5 from "fs";
6920
7017
  function printMachine(value, format) {
6921
7018
  if (!isMachineFormat(format)) return false;
6922
7019
  console.log(JSON.stringify(value, null, 2));
@@ -6957,7 +7054,7 @@ function readConversionTrackingContractInput(inputPath) {
6957
7054
  });
6958
7055
  }
6959
7056
  try {
6960
- const raw = fs4.readFileSync(inputPath === "-" ? 0 : inputPath, "utf8");
7057
+ const raw = fs5.readFileSync(inputPath === "-" ? 0 : inputPath, "utf8");
6961
7058
  return conversionTrackingContractWriteRequestSchema.parse(JSON.parse(raw));
6962
7059
  } catch (err) {
6963
7060
  const message = describeError(err);
@@ -7609,9 +7706,9 @@ async function getCommand(opts) {
7609
7706
  console.log(JSON.stringify(leaf, null, 2));
7610
7707
  }
7611
7708
  }
7612
- function walkPath(value, path12) {
7613
- if (!path12 || path12 === ".") return value;
7614
- const segments = path12.split(".").flatMap((part) => {
7709
+ function walkPath(value, path13) {
7710
+ if (!path13 || path13 === ".") return value;
7711
+ const segments = path13.split(".").flatMap((part) => {
7615
7712
  const tokens = [];
7616
7713
  let i = 0;
7617
7714
  const bracketStart = part.indexOf("[");
@@ -7659,24 +7756,24 @@ var GET_CLI_COMMANDS = [
7659
7756
  },
7660
7757
  run: async (input) => {
7661
7758
  const project2 = requireProject(input, "get", USAGE2);
7662
- const path12 = requirePositional(input, 1, {
7759
+ const path13 = requirePositional(input, 1, {
7663
7760
  command: "get",
7664
7761
  usage: USAGE2,
7665
7762
  message: 'path is required (e.g. "scores.mentionShare.value")'
7666
7763
  });
7667
7764
  const from = getString(input.values, "from");
7668
- await getCommand({ project: project2, path: path12, from, format: input.format });
7765
+ await getCommand({ project: project2, path: path13, from, format: input.format });
7669
7766
  }
7670
7767
  }
7671
7768
  ];
7672
7769
 
7673
7770
  // src/commands/traffic.ts
7674
- import fs6 from "fs";
7771
+ import fs7 from "fs";
7675
7772
 
7676
7773
  // src/cloudflare-worker-deploy.ts
7677
7774
  import { spawn } from "child_process";
7678
7775
  import { randomUUID } from "crypto";
7679
- import fs5 from "fs";
7776
+ import fs6 from "fs";
7680
7777
  import os from "os";
7681
7778
  import path2 from "path";
7682
7779
  import { stripVTControlCharacters } from "util";
@@ -7694,7 +7791,7 @@ function projectSlug(project2) {
7694
7791
  }
7695
7792
  function lstatIfPresent(filePath) {
7696
7793
  try {
7697
- return fs5.lstatSync(filePath);
7794
+ return fs6.lstatSync(filePath);
7698
7795
  } catch (error) {
7699
7796
  if (error.code === "ENOENT") return null;
7700
7797
  throw error;
@@ -7712,7 +7809,7 @@ function prepareCloudflareWorkerOutputDirectory(outputDirectory) {
7712
7809
  throw new Error(`output directory is not a regular directory: ${resolved}`);
7713
7810
  }
7714
7811
  } else {
7715
- fs5.mkdirSync(resolved, { recursive: true, mode: 448 });
7812
+ fs6.mkdirSync(resolved, { recursive: true, mode: 448 });
7716
7813
  }
7717
7814
  const artifacts = {
7718
7815
  outputDirectory: resolved,
@@ -7725,7 +7822,7 @@ function prepareCloudflareWorkerOutputDirectory(outputDirectory) {
7725
7822
  if (!stat.isFile() || stat.isSymbolicLink()) {
7726
7823
  throw new Error(`artifact path is not a regular file: ${artifactPath}`);
7727
7824
  }
7728
- const contents = fs5.readFileSync(artifactPath, "utf-8");
7825
+ const contents = fs6.readFileSync(artifactPath, "utf-8");
7729
7826
  if (!isRecognizablyCanonryGenerated(artifactPath, contents)) {
7730
7827
  throw new Error(`refusing to overwrite an operator-owned artifact: ${artifactPath}`);
7731
7828
  }
@@ -7738,7 +7835,7 @@ function readRegularArtifact(filePath) {
7738
7835
  if (!stat.isFile() || stat.isSymbolicLink()) {
7739
7836
  throw new Error(`artifact path is not a regular file: ${filePath}`);
7740
7837
  }
7741
- return fs5.readFileSync(filePath, "utf-8");
7838
+ return fs6.readFileSync(filePath, "utf-8");
7742
7839
  }
7743
7840
  function isRecognizablyCanonryGenerated(filePath, contents) {
7744
7841
  if (path2.basename(filePath) === "worker.js") {
@@ -7755,22 +7852,22 @@ function writePrivateTemporaryFile(filePath, contents) {
7755
7852
  path2.dirname(filePath),
7756
7853
  `.${path2.basename(filePath)}.${randomUUID()}.tmp`
7757
7854
  );
7758
- const fd = fs5.openSync(temporaryPath, "wx", 384);
7855
+ const fd = fs6.openSync(temporaryPath, "wx", 384);
7759
7856
  let closed = false;
7760
7857
  try {
7761
- fs5.writeFileSync(fd, contents, "utf-8");
7762
- fs5.fsyncSync(fd);
7763
- fs5.closeSync(fd);
7858
+ fs6.writeFileSync(fd, contents, "utf-8");
7859
+ fs6.fsyncSync(fd);
7860
+ fs6.closeSync(fd);
7764
7861
  closed = true;
7765
7862
  } catch (error) {
7766
7863
  if (!closed) {
7767
7864
  try {
7768
- fs5.closeSync(fd);
7865
+ fs6.closeSync(fd);
7769
7866
  } catch {
7770
7867
  }
7771
7868
  }
7772
7869
  try {
7773
- fs5.unlinkSync(temporaryPath);
7870
+ fs6.unlinkSync(temporaryPath);
7774
7871
  } catch {
7775
7872
  }
7776
7873
  throw error;
@@ -7804,10 +7901,10 @@ function writeCloudflareWorkerArtifacts(artifacts, contents) {
7804
7901
  throw new Error(`artifact changed during update: ${change.filePath}`);
7805
7902
  }
7806
7903
  if (change.previous === null) {
7807
- fs5.linkSync(change.temporaryPath, change.filePath);
7808
- fs5.unlinkSync(change.temporaryPath);
7904
+ fs6.linkSync(change.temporaryPath, change.filePath);
7905
+ fs6.unlinkSync(change.temporaryPath);
7809
7906
  } else {
7810
- fs5.renameSync(change.temporaryPath, change.filePath);
7907
+ fs6.renameSync(change.temporaryPath, change.filePath);
7811
7908
  }
7812
7909
  applied.push(change);
7813
7910
  }
@@ -7816,10 +7913,10 @@ function writeCloudflareWorkerArtifacts(artifacts, contents) {
7816
7913
  try {
7817
7914
  if (readRegularArtifact(change.filePath) !== change.contents) continue;
7818
7915
  if (change.previous === null) {
7819
- fs5.unlinkSync(change.filePath);
7916
+ fs6.unlinkSync(change.filePath);
7820
7917
  } else {
7821
7918
  const restorePath = writePrivateTemporaryFile(change.filePath, change.previous);
7822
- fs5.renameSync(restorePath, change.filePath);
7919
+ fs6.renameSync(restorePath, change.filePath);
7823
7920
  }
7824
7921
  } catch {
7825
7922
  }
@@ -7828,7 +7925,7 @@ function writeCloudflareWorkerArtifacts(artifacts, contents) {
7828
7925
  } finally {
7829
7926
  for (const change of pending) {
7830
7927
  try {
7831
- if (fs5.existsSync(change.temporaryPath)) fs5.unlinkSync(change.temporaryPath);
7928
+ if (fs6.existsSync(change.temporaryPath)) fs6.unlinkSync(change.temporaryPath);
7832
7929
  } catch {
7833
7930
  }
7834
7931
  }
@@ -7914,7 +8011,7 @@ async function preflightCloudflareWrangler(opts = {}) {
7914
8011
  if (missing.length > 0) {
7915
8012
  throw new Error(`Wrangler deploy does not support required flags: ${missing.join(", ")}`);
7916
8013
  }
7917
- const tempDirectory = fs5.mkdtempSync(path2.join(
8014
+ const tempDirectory = fs6.mkdtempSync(path2.join(
7918
8015
  opts.tempRoot ?? os.tmpdir(),
7919
8016
  "canonry-cloudflare-wrangler-preflight-"
7920
8017
  ));
@@ -7922,13 +8019,13 @@ async function preflightCloudflareWrangler(opts = {}) {
7922
8019
  const wranglerTomlPath = path2.join(tempDirectory, "wrangler.toml");
7923
8020
  const secretsPath = path2.join(tempDirectory, "secrets.json");
7924
8021
  try {
7925
- fs5.chmodSync(tempDirectory, 448);
7926
- fs5.writeFileSync(workerScriptPath, generateWorkerScript({
8022
+ fs6.chmodSync(tempDirectory, 448);
8023
+ fs6.writeFileSync(workerScriptPath, generateWorkerScript({
7927
8024
  deliveryMode,
7928
8025
  workerVersion: "preflight",
7929
8026
  botList: DEFAULT_BOT_LIST
7930
8027
  }), { encoding: "utf-8", flag: "wx", mode: 384 });
7931
- fs5.writeFileSync(wranglerTomlPath, generateWranglerToml(
8028
+ fs6.writeFileSync(wranglerTomlPath, generateWranglerToml(
7932
8029
  deliveryMode === "queue-pull" ? {
7933
8030
  deliveryMode,
7934
8031
  sourceId: "preflight",
@@ -7944,7 +8041,7 @@ async function preflightCloudflareWrangler(opts = {}) {
7944
8041
  }
7945
8042
  ), { encoding: "utf-8", flag: "wx", mode: 384 });
7946
8043
  if (deliveryMode === "direct-push") {
7947
- fs5.writeFileSync(secretsPath, JSON.stringify({
8044
+ fs6.writeFileSync(secretsPath, JSON.stringify({
7948
8045
  [CLOUDFLARE_WORKER_BINDINGS.bearerToken]: "preflight-bearer",
7949
8046
  [CLOUDFLARE_WORKER_BINDINGS.hmacSecret]: "preflight-hmac"
7950
8047
  }), { encoding: "utf-8", flag: "wx", mode: 384 });
@@ -7972,7 +8069,7 @@ ${wranglerDiagnostic(validationOutput)}`
7972
8069
  );
7973
8070
  }
7974
8071
  } finally {
7975
- fs5.rmSync(tempDirectory, { recursive: true, force: true });
8072
+ fs6.rmSync(tempDirectory, { recursive: true, force: true });
7976
8073
  }
7977
8074
  }
7978
8075
  async function deployCloudflareWorker(opts) {
@@ -7988,11 +8085,11 @@ async function deployCloudflareWorker(opts) {
7988
8085
  }
7989
8086
  if (!opts.secrets) throw new Error("Direct-push Worker deploy requires bearer and HMAC secrets");
7990
8087
  const secrets = opts.secrets;
7991
- const tempDirectory = fs5.mkdtempSync(path2.join(opts.tempRoot ?? os.tmpdir(), "canonry-cloudflare-secrets-"));
8088
+ const tempDirectory = fs6.mkdtempSync(path2.join(opts.tempRoot ?? os.tmpdir(), "canonry-cloudflare-secrets-"));
7992
8089
  const secretsPath = path2.join(tempDirectory, "secrets.json");
7993
8090
  try {
7994
- fs5.chmodSync(tempDirectory, 448);
7995
- fs5.writeFileSync(
8091
+ fs6.chmodSync(tempDirectory, 448);
8092
+ fs6.writeFileSync(
7996
8093
  secretsPath,
7997
8094
  JSON.stringify({
7998
8095
  [CLOUDFLARE_WORKER_BINDINGS.bearerToken]: secrets.bearerToken,
@@ -8000,7 +8097,7 @@ async function deployCloudflareWorker(opts) {
8000
8097
  }),
8001
8098
  { encoding: "utf-8", flag: "wx", mode: 384 }
8002
8099
  );
8003
- fs5.chmodSync(secretsPath, 384);
8100
+ fs6.chmodSync(secretsPath, 384);
8004
8101
  const runner = opts.run ?? runWrangler;
8005
8102
  await runner(
8006
8103
  "wrangler",
@@ -8016,9 +8113,9 @@ async function deployCloudflareWorker(opts) {
8016
8113
  );
8017
8114
  } finally {
8018
8115
  try {
8019
- if (fs5.existsSync(secretsPath)) fs5.unlinkSync(secretsPath);
8116
+ if (fs6.existsSync(secretsPath)) fs6.unlinkSync(secretsPath);
8020
8117
  } finally {
8021
- fs5.rmSync(tempDirectory, { recursive: true, force: true });
8118
+ fs6.rmSync(tempDirectory, { recursive: true, force: true });
8022
8119
  }
8023
8120
  }
8024
8121
  }
@@ -8048,11 +8145,11 @@ function requireCloudflareQueueOption(project2, value, option) {
8048
8145
  }
8049
8146
  function readCloudflareQueueApiToken(project2, tokenFile) {
8050
8147
  try {
8051
- const stat = fs6.lstatSync(tokenFile);
8148
+ const stat = fs7.lstatSync(tokenFile);
8052
8149
  if (!stat.isFile() || stat.isSymbolicLink()) {
8053
8150
  throw new Error("not a regular file");
8054
8151
  }
8055
- const apiToken = fs6.readFileSync(tokenFile, "utf-8").trim();
8152
+ const apiToken = fs7.readFileSync(tokenFile, "utf-8").trim();
8056
8153
  if (!apiToken) throw new Error("file is empty");
8057
8154
  return apiToken;
8058
8155
  } catch (error) {
@@ -8334,9 +8431,9 @@ async function trafficConnectWordpress(project2, opts) {
8334
8431
  }
8335
8432
  let applicationPassword = opts.appPassword?.trim() ?? "";
8336
8433
  if (!applicationPassword && opts.appPasswordFile) {
8337
- const fs19 = await import("fs");
8434
+ const fs21 = await import("fs");
8338
8435
  try {
8339
- applicationPassword = fs19.readFileSync(opts.appPasswordFile, "utf-8").trim();
8436
+ applicationPassword = fs21.readFileSync(opts.appPasswordFile, "utf-8").trim();
8340
8437
  } catch (e) {
8341
8438
  const msg = describeError(e);
8342
8439
  throw new CliError({
@@ -8392,10 +8489,10 @@ async function trafficConnectCloudRun(project2, opts) {
8392
8489
  details: { project: project2 }
8393
8490
  });
8394
8491
  }
8395
- const fs19 = await import("fs");
8492
+ const fs21 = await import("fs");
8396
8493
  let keyJson;
8397
8494
  try {
8398
- keyJson = fs19.readFileSync(opts.serviceAccountKey, "utf-8");
8495
+ keyJson = fs21.readFileSync(opts.serviceAccountKey, "utf-8");
8399
8496
  JSON.parse(keyJson);
8400
8497
  } catch (e) {
8401
8498
  const msg = describeError(e);
@@ -8455,9 +8552,9 @@ async function trafficConnectVercel(project2, opts) {
8455
8552
  }
8456
8553
  let token = opts.token?.trim() ?? "";
8457
8554
  if (!token && opts.tokenFile) {
8458
- const fs19 = await import("fs");
8555
+ const fs21 = await import("fs");
8459
8556
  try {
8460
- token = fs19.readFileSync(opts.tokenFile, "utf-8").trim();
8557
+ token = fs21.readFileSync(opts.tokenFile, "utf-8").trim();
8461
8558
  } catch (e) {
8462
8559
  const msg = describeError(e);
8463
8560
  throw new CliError({
@@ -11504,7 +11601,7 @@ var USERS_CLI_COMMANDS = [
11504
11601
  ];
11505
11602
 
11506
11603
  // src/commands/keyword.ts
11507
- import fs7 from "fs";
11604
+ import fs8 from "fs";
11508
11605
  function getClient14() {
11509
11606
  return createApiClient();
11510
11607
  }
@@ -11572,7 +11669,7 @@ async function listKeywords(project2, format) {
11572
11669
  }
11573
11670
  }
11574
11671
  async function importKeywords(project2, filePath, format) {
11575
- if (!fs7.existsSync(filePath)) {
11672
+ if (!fs8.existsSync(filePath)) {
11576
11673
  throw new CliError({
11577
11674
  code: "KEYWORD_IMPORT_FILE_NOT_FOUND",
11578
11675
  message: `File not found: ${filePath}`,
@@ -11583,7 +11680,7 @@ async function importKeywords(project2, filePath, format) {
11583
11680
  }
11584
11681
  });
11585
11682
  }
11586
- const content = fs7.readFileSync(filePath, "utf-8");
11683
+ const content = fs8.readFileSync(filePath, "utf-8");
11587
11684
  const keywords = content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
11588
11685
  if (keywords.length === 0) {
11589
11686
  if (isMachineFormat(format)) {
@@ -11784,7 +11881,7 @@ var KEYWORD_CLI_COMMANDS = [
11784
11881
  ];
11785
11882
 
11786
11883
  // src/commands/query.ts
11787
- import fs8 from "fs";
11884
+ import fs9 from "fs";
11788
11885
  function getClient15() {
11789
11886
  return createApiClient();
11790
11887
  }
@@ -11875,7 +11972,7 @@ async function listQueries(project2, format) {
11875
11972
  }
11876
11973
  }
11877
11974
  async function importQueries(project2, filePath, format) {
11878
- if (!fs8.existsSync(filePath)) {
11975
+ if (!fs9.existsSync(filePath)) {
11879
11976
  throw new CliError({
11880
11977
  code: "QUERY_IMPORT_FILE_NOT_FOUND",
11881
11978
  message: `File not found: ${filePath}`,
@@ -11886,7 +11983,7 @@ async function importQueries(project2, filePath, format) {
11886
11983
  }
11887
11984
  });
11888
11985
  }
11889
- const content = fs8.readFileSync(filePath, "utf-8");
11986
+ const content = fs9.readFileSync(filePath, "utf-8");
11890
11987
  const queries2 = content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
11891
11988
  if (queries2.length === 0) {
11892
11989
  if (isMachineFormat(format)) {
@@ -11950,16 +12047,16 @@ Saved ${result.queries.length} ${result.queries.length === 1 ? "query" : "querie
11950
12047
 
11951
12048
  // src/commands/measurement-plan.ts
11952
12049
  import crypto from "crypto";
11953
- import fs9 from "fs";
12050
+ import fs10 from "fs";
11954
12051
  import { parse as parse2 } from "yaml";
11955
12052
  import { z as z2 } from "zod";
11956
12053
  function readPlan(source) {
11957
- const content = source === "-" ? fs9.readFileSync(0, "utf8") : fs9.readFileSync(source, "utf8");
12054
+ const content = source === "-" ? fs10.readFileSync(0, "utf8") : fs10.readFileSync(source, "utf8");
11958
12055
  const parsed = source.endsWith(".json") ? JSON.parse(content) : parse2(content);
11959
12056
  return measurementPlanInputSchema.parse(parsed);
11960
12057
  }
11961
12058
  function readDiscoveryRule(source) {
11962
- const content = source === "-" ? fs9.readFileSync(0, "utf8") : fs9.readFileSync(source, "utf8");
12059
+ const content = source === "-" ? fs10.readFileSync(0, "utf8") : fs10.readFileSync(source, "utf8");
11963
12060
  const parsed = source.endsWith(".json") ? JSON.parse(content) : parse2(content);
11964
12061
  return measurementDiscoveryRuleSchema.parse(parsed);
11965
12062
  }
@@ -12012,7 +12109,7 @@ var advancedDeactivateInputSchema = z2.object({
12012
12109
  }).strict();
12013
12110
  function readAdvancedMeasurementInput(source) {
12014
12111
  if (source === void 0) return {};
12015
- const content = source === "-" ? fs9.readFileSync(0, "utf8") : fs9.readFileSync(source, "utf8");
12112
+ const content = source === "-" ? fs10.readFileSync(0, "utf8") : fs10.readFileSync(source, "utf8");
12016
12113
  return JSON.parse(content);
12017
12114
  }
12018
12115
  var advancedMeasurementCollectionKeys = {
@@ -12211,7 +12308,7 @@ async function replaceMeasurementPlanAssignments(project2, opts) {
12211
12308
  console.log(JSON.stringify({ preview, result }, null, 2));
12212
12309
  }
12213
12310
  function readGroupMembershipCsv(source) {
12214
- return source === "-" ? fs9.readFileSync(0, "utf8") : fs9.readFileSync(source, "utf8");
12311
+ return source === "-" ? fs10.readFileSync(0, "utf8") : fs10.readFileSync(source, "utf8");
12215
12312
  }
12216
12313
  async function previewMeasurementPlanGroups(project2, source) {
12217
12314
  const preview = await createApiClient().previewMeasurementDraftGroupMembership(project2, {
@@ -12562,7 +12659,7 @@ var QUERY_CLI_COMMANDS = [
12562
12659
  ];
12563
12660
 
12564
12661
  // src/commands/mcp.ts
12565
- import fs10 from "fs";
12662
+ import fs11 from "fs";
12566
12663
  import path4 from "path";
12567
12664
  import { createRequire } from "module";
12568
12665
 
@@ -12678,8 +12775,8 @@ function renderClientSnippet(client, serverName, entry) {
12678
12775
  return renderJsonSnippet(serverName, entry, client.format);
12679
12776
  }
12680
12777
  function readJsonConfig(configPath) {
12681
- if (!fs10.existsSync(configPath)) return {};
12682
- const raw = fs10.readFileSync(configPath, "utf-8").trim();
12778
+ if (!fs11.existsSync(configPath)) return {};
12779
+ const raw = fs11.readFileSync(configPath, "utf-8").trim();
12683
12780
  if (!raw) return {};
12684
12781
  try {
12685
12782
  const parsed = JSON.parse(raw);
@@ -12697,14 +12794,14 @@ function readJsonConfig(configPath) {
12697
12794
  }
12698
12795
  }
12699
12796
  function writeJsonConfig(configPath, value) {
12700
- fs10.mkdirSync(path4.dirname(configPath), { recursive: true });
12701
- fs10.writeFileSync(configPath, `${JSON.stringify(value, null, 2)}
12797
+ fs11.mkdirSync(path4.dirname(configPath), { recursive: true });
12798
+ fs11.writeFileSync(configPath, `${JSON.stringify(value, null, 2)}
12702
12799
  `, "utf-8");
12703
12800
  }
12704
12801
  function backupConfigIfPresent(configPath) {
12705
- if (!fs10.existsSync(configPath)) return void 0;
12802
+ if (!fs11.existsSync(configPath)) return void 0;
12706
12803
  const backupPath = `${configPath}.canonry.bak`;
12707
- fs10.copyFileSync(configPath, backupPath);
12804
+ fs11.copyFileSync(configPath, backupPath);
12708
12805
  return backupPath;
12709
12806
  }
12710
12807
  function findClientOrThrow(id) {
@@ -13067,13 +13164,13 @@ var NOTIFY_CLI_COMMANDS = [
13067
13164
  ];
13068
13165
 
13069
13166
  // src/commands/apply.ts
13070
- import fs11 from "fs";
13167
+ import fs12 from "fs";
13071
13168
  import { parseAllDocuments } from "yaml";
13072
13169
  async function applyConfigFile(filePath) {
13073
- if (!fs11.existsSync(filePath)) {
13170
+ if (!fs12.existsSync(filePath)) {
13074
13171
  throw new Error(`File not found: ${filePath}`);
13075
13172
  }
13076
- const content = fs11.readFileSync(filePath, "utf-8");
13173
+ const content = fs12.readFileSync(filePath, "utf-8");
13077
13174
  const docs = parseAllDocuments(content);
13078
13175
  const client = createApiClient();
13079
13176
  const errors = [];
@@ -13539,7 +13636,7 @@ async function loadLatestRunForExport(client, project2) {
13539
13636
  }
13540
13637
 
13541
13638
  // src/commands/results-export.ts
13542
- import fs12 from "fs";
13639
+ import fs13 from "fs";
13543
13640
  import path5 from "path";
13544
13641
  async function exportResults(project2, opts) {
13545
13642
  const { output, ...request } = opts;
@@ -13549,8 +13646,8 @@ async function exportResults(project2, opts) {
13549
13646
  return;
13550
13647
  }
13551
13648
  const target = output ? path5.resolve(output) : path5.resolve(process.cwd(), path5.basename(artifact.filename));
13552
- fs12.mkdirSync(path5.dirname(target), { recursive: true });
13553
- fs12.writeFileSync(target, artifact.content, "utf8");
13649
+ fs13.mkdirSync(path5.dirname(target), { recursive: true });
13650
+ fs13.writeFileSync(target, artifact.content, "utf8");
13554
13651
  console.log(`Results export written to ${target}`);
13555
13652
  }
13556
13653
 
@@ -15852,11 +15949,11 @@ var SKILLS_CLI_COMMANDS = [
15852
15949
  ];
15853
15950
 
15854
15951
  // src/commands/snapshot.ts
15855
- import fs14 from "fs";
15952
+ import fs15 from "fs";
15856
15953
  import path7 from "path";
15857
15954
 
15858
15955
  // src/snapshot-pdf.ts
15859
- import fs13 from "fs";
15956
+ import fs14 from "fs";
15860
15957
  import path6 from "path";
15861
15958
  import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
15862
15959
  var PAGE_WIDTH = 612;
@@ -16070,8 +16167,8 @@ async function writeSnapshotPdf(report, outputPath) {
16070
16167
  renderQueries(pdf, report);
16071
16168
  const bytes = await doc.save();
16072
16169
  const resolvedPath = path6.resolve(outputPath);
16073
- fs13.mkdirSync(path6.dirname(resolvedPath), { recursive: true });
16074
- fs13.writeFileSync(resolvedPath, bytes);
16170
+ fs14.mkdirSync(path6.dirname(resolvedPath), { recursive: true });
16171
+ fs14.writeFileSync(resolvedPath, bytes);
16075
16172
  return resolvedPath;
16076
16173
  }
16077
16174
  function renderCover(pdf, report) {
@@ -16232,8 +16329,8 @@ PDF saved: ${savedPdfPath}`);
16232
16329
  }
16233
16330
  function writeSnapshotMarkdown(report, outputPath) {
16234
16331
  const resolvedPath = path7.resolve(outputPath);
16235
- fs14.mkdirSync(path7.dirname(resolvedPath), { recursive: true });
16236
- fs14.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
16332
+ fs15.mkdirSync(path7.dirname(resolvedPath), { recursive: true });
16333
+ fs15.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
16237
16334
  return resolvedPath;
16238
16335
  }
16239
16336
  function formatSnapshotMarkdown(report) {
@@ -17472,7 +17569,17 @@ async function bootstrapCommand(opts) {
17472
17569
  };
17473
17570
  const providersChanged = !isDeepStrictEqual(persistedValue(existingRaw?.providers ?? {}), persistedValue(mergedProviders));
17474
17571
  const configChanged = !existingRaw || existingRaw.apiUrl !== apiUrl || existingRaw.database !== databasePath || existingRaw.apiKey !== rawApiKey || providersChanged || !isDeepStrictEqual(persistedValue(existingRaw.google), persistedValue(mergedGoogle));
17475
- if (configChanged) saveConfig(nextConfig);
17572
+ const providerConnections = Object.keys(env.providers).filter((name) => env.providers[name] && !isDeepStrictEqual(persistedValue(existingRaw?.providers?.[name]), persistedValue(mergedProviders[name]))).map((name) => ({ integration: "provider", provider: name, action: existingRaw?.providers?.[name] ? "reauth" : "connect" }));
17573
+ if (configChanged) {
17574
+ try {
17575
+ saveConfig(nextConfig);
17576
+ } catch (err) {
17577
+ for (const connection of providerConnections) trackCliConnection({ ...connection, status: "failed", ...outcomeFailure(err) });
17578
+ throw err;
17579
+ }
17580
+ }
17581
+ if (isTelemetryEnabled()) recordInstallRef(opts?.ref);
17582
+ for (const connection of providerConnections) trackCliConnection({ ...connection, status: "succeeded" });
17476
17583
  const status = !existing ? "created" : configChanged || keyChanged ? "updated" : "unchanged";
17477
17584
  const providerFree = registeredProviderNames(nextConfig).length === 0;
17478
17585
  const serverUrl = loadConfig().apiUrl;
@@ -17567,9 +17674,24 @@ async function bootstrapCommand(opts) {
17567
17674
  if (reloadFailure) throw reloadFailure;
17568
17675
  }
17569
17676
 
17677
+ // src/commands/support.ts
17678
+ var FEEDBACK_COMMAND = 'canonry feedback "<what happened>"';
17679
+ function supportCommand(format) {
17680
+ if (isMachineFormat(format)) {
17681
+ console.log(JSON.stringify({ discord: CANONRY_DISCORD_URL, issues: CANONRY_ISSUES_URL, feedbackCommand: FEEDBACK_COMMAND }, null, 2));
17682
+ return;
17683
+ }
17684
+ console.log([
17685
+ "Get help with Canonry:",
17686
+ ` Discord: ${CANONRY_DISCORD_URL}`,
17687
+ ` Issues: ${CANONRY_ISSUES_URL}`,
17688
+ ` From here: ${FEEDBACK_COMMAND}`
17689
+ ].join("\n"));
17690
+ }
17691
+
17570
17692
  // src/commands/daemon.ts
17571
17693
  import { spawn as spawn2 } from "child_process";
17572
- import fs15 from "fs";
17694
+ import fs16 from "fs";
17573
17695
  import path9 from "path";
17574
17696
 
17575
17697
  // src/operator-url.ts
@@ -17640,8 +17762,8 @@ function buildServeForwardArgs(opts) {
17640
17762
  async function startDaemon(opts) {
17641
17763
  const pidPath = getPidPath();
17642
17764
  const format = opts.format ?? "text";
17643
- if (fs15.existsSync(pidPath)) {
17644
- const existingPid = parseInt(fs15.readFileSync(pidPath, "utf-8").trim(), 10);
17765
+ if (fs16.existsSync(pidPath)) {
17766
+ const existingPid = parseInt(fs16.readFileSync(pidPath, "utf-8").trim(), 10);
17645
17767
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
17646
17768
  throw new CliError({
17647
17769
  code: "DAEMON_ALREADY_RUNNING",
@@ -17652,7 +17774,7 @@ async function startDaemon(opts) {
17652
17774
  }
17653
17775
  });
17654
17776
  }
17655
- fs15.unlinkSync(pidPath);
17777
+ fs16.unlinkSync(pidPath);
17656
17778
  }
17657
17779
  const config = loadConfig();
17658
17780
  const endpoint = resolveDaemonEndpoint(opts, process.env, config.port);
@@ -17675,10 +17797,10 @@ async function startDaemon(opts) {
17675
17797
  });
17676
17798
  }
17677
17799
  const configDir = getConfigDir();
17678
- if (!fs15.existsSync(configDir)) {
17679
- fs15.mkdirSync(configDir, { recursive: true });
17800
+ if (!fs16.existsSync(configDir)) {
17801
+ fs16.mkdirSync(configDir, { recursive: true });
17680
17802
  }
17681
- fs15.writeFileSync(pidPath, String(child.pid), "utf-8");
17803
+ fs16.writeFileSync(pidPath, String(child.pid), "utf-8");
17682
17804
  if (!isMachineFormat(format)) {
17683
17805
  process.stderr.write("Waiting for server to start...");
17684
17806
  }
@@ -17689,7 +17811,7 @@ async function startDaemon(opts) {
17689
17811
  } catch {
17690
17812
  }
17691
17813
  try {
17692
- fs15.unlinkSync(pidPath);
17814
+ fs16.unlinkSync(pidPath);
17693
17815
  } catch {
17694
17816
  }
17695
17817
  throw new CliError({
@@ -17721,7 +17843,7 @@ async function startDaemon(opts) {
17721
17843
  }
17722
17844
  function stopDaemon(format = "text") {
17723
17845
  const pidPath = getPidPath();
17724
- if (!fs15.existsSync(pidPath)) {
17846
+ if (!fs16.existsSync(pidPath)) {
17725
17847
  if (isMachineFormat(format)) {
17726
17848
  console.log(JSON.stringify({
17727
17849
  stopped: false,
@@ -17732,7 +17854,7 @@ function stopDaemon(format = "text") {
17732
17854
  console.log("Canonry is not running (no PID file found)");
17733
17855
  return;
17734
17856
  }
17735
- const pid = parseInt(fs15.readFileSync(pidPath, "utf-8").trim(), 10);
17857
+ const pid = parseInt(fs16.readFileSync(pidPath, "utf-8").trim(), 10);
17736
17858
  if (isNaN(pid)) {
17737
17859
  if (isMachineFormat(format)) {
17738
17860
  console.log(JSON.stringify({
@@ -17743,7 +17865,7 @@ function stopDaemon(format = "text") {
17743
17865
  } else {
17744
17866
  console.error("Invalid PID file. Removing it.");
17745
17867
  }
17746
- fs15.unlinkSync(pidPath);
17868
+ fs16.unlinkSync(pidPath);
17747
17869
  return;
17748
17870
  }
17749
17871
  if (!isProcessAlive(pid)) {
@@ -17757,12 +17879,12 @@ function stopDaemon(format = "text") {
17757
17879
  } else {
17758
17880
  console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
17759
17881
  }
17760
- fs15.unlinkSync(pidPath);
17882
+ fs16.unlinkSync(pidPath);
17761
17883
  return;
17762
17884
  }
17763
17885
  try {
17764
17886
  process.kill(pid, "SIGTERM");
17765
- fs15.unlinkSync(pidPath);
17887
+ fs16.unlinkSync(pidPath);
17766
17888
  if (isMachineFormat(format)) {
17767
17889
  console.log(JSON.stringify({
17768
17890
  stopped: true,
@@ -17786,7 +17908,7 @@ function stopDaemon(format = "text") {
17786
17908
 
17787
17909
  // src/commands/init.ts
17788
17910
  import crypto3 from "crypto";
17789
- import fs16 from "fs";
17911
+ import fs17 from "fs";
17790
17912
  import readline2 from "readline";
17791
17913
  import path10 from "path";
17792
17914
  var pendingServeHandoff = false;
@@ -17824,7 +17946,7 @@ var PROJECT_MARKERS = [".git", "canonry.yaml", "canonry.yml", "package.json"];
17824
17946
  function cwdLooksLikeProject(dir) {
17825
17947
  const home = process.env.HOME ?? "";
17826
17948
  if (home && path10.resolve(dir) === path10.resolve(home)) return false;
17827
- return PROJECT_MARKERS.some((marker) => fs16.existsSync(path10.join(dir, marker)));
17949
+ return PROJECT_MARKERS.some((marker) => fs17.existsSync(path10.join(dir, marker)));
17828
17950
  }
17829
17951
  var DEFAULT_AGENT_MODELS = {
17830
17952
  anthropic: "anthropic/claude-sonnet-4-6",
@@ -17855,8 +17977,8 @@ async function initCommand(opts) {
17855
17977
  return void 0;
17856
17978
  }
17857
17979
  const configDir = getConfigDir();
17858
- if (!fs16.existsSync(configDir)) {
17859
- fs16.mkdirSync(configDir, { recursive: true });
17980
+ if (!fs17.existsSync(configDir)) {
17981
+ fs17.mkdirSync(configDir, { recursive: true });
17860
17982
  }
17861
17983
  const bootstrapEnv = getBootstrapEnv(process.env, {
17862
17984
  GEMINI_API_KEY: opts?.geminiKey,
@@ -18104,6 +18226,7 @@ ${mcpTip}`);
18104
18226
  }
18105
18227
  if (isTelemetryEnabled()) {
18106
18228
  const postInitSetupState = buildSetupState();
18229
+ recordInstallRef(opts?.ref);
18107
18230
  trackEvent("cli.init", {
18108
18231
  providerCount: providerNames.length,
18109
18232
  providers: providerNames,
@@ -18126,8 +18249,12 @@ ${mcpTip}`);
18126
18249
  hasAgent: !!agentLLM
18127
18250
  }),
18128
18251
  skillsInstalled: !!skillsSummary,
18252
+ ...installAttribution(),
18129
18253
  ...cliRuntimeContext()
18130
18254
  });
18255
+ for (const provider of Object.keys(providers)) {
18256
+ trackCliConnection({ integration: "provider", provider, action: "connect", status: "succeeded" });
18257
+ }
18131
18258
  }
18132
18259
  if (!isMachineFormat(format) && process.stdin.isTTY && process.stdout.isTTY) {
18133
18260
  const answer = await prompt("\nStart the dashboard now? [Y/n]: ");
@@ -18208,7 +18335,7 @@ async function repairPathsOnce(db, name, repair) {
18208
18335
  }
18209
18336
 
18210
18337
  // src/agent-plugin.ts
18211
- import fs17 from "fs";
18338
+ import fs18 from "fs";
18212
18339
  import path11 from "path";
18213
18340
  var CANONRY_PLUGIN_ID = "canonry@canonry";
18214
18341
  var REQUIRED_SKILLS = ["canonry", "aero"];
@@ -18253,7 +18380,7 @@ function isCanonryPluginId(value) {
18253
18380
  }
18254
18381
  function readJson(filePath) {
18255
18382
  try {
18256
- return JSON.parse(fs17.readFileSync(filePath, "utf8"));
18383
+ return JSON.parse(fs18.readFileSync(filePath, "utf8"));
18257
18384
  } catch {
18258
18385
  return null;
18259
18386
  }
@@ -18272,7 +18399,7 @@ function readCodexPluginConfigured(filePath) {
18272
18399
  try {
18273
18400
  let currentPlugin = null;
18274
18401
  let inPluginsTable = false;
18275
- for (const line of fs17.readFileSync(filePath, "utf8").split(/\r?\n/)) {
18402
+ for (const line of fs18.readFileSync(filePath, "utf8").split(/\r?\n/)) {
18276
18403
  const section = /^\s*\[plugins\.(?:"([^"]+)"|'([^']+)'|(\S+))\]\s*(?:#.*)?$/.exec(line);
18277
18404
  if (section) {
18278
18405
  const captures = section.slice(1, 4);
@@ -18305,7 +18432,7 @@ function readCodexPluginConfigured(filePath) {
18305
18432
  }
18306
18433
  }
18307
18434
  function verifiedPluginVersion(root, client) {
18308
- if (client === "codex" && fs17.existsSync(path11.join(root, "plugin.json"))) {
18435
+ if (client === "codex" && fs18.existsSync(path11.join(root, "plugin.json"))) {
18309
18436
  const manifest2 = readJson(path11.join(root, "plugin.json"));
18310
18437
  if (manifest2 && typeof manifest2 === "object") {
18311
18438
  const plugin2 = manifest2;
@@ -18319,7 +18446,7 @@ function verifiedPluginVersion(root, client) {
18319
18446
  if (portableMcp.$schema !== AGENT_PLUGINS_MCP_SCHEMA || portableMcp.mcpServers?.canonry?.type !== "stdio" || portableMcp.mcpServers.canonry.command !== "canonry-mcp") {
18320
18447
  return null;
18321
18448
  }
18322
- return REQUIRED_SKILLS.every((name) => fs17.existsSync(path11.join(root, "skills", name, "SKILL.md"))) ? plugin2.version : null;
18449
+ return REQUIRED_SKILLS.every((name) => fs18.existsSync(path11.join(root, "skills", name, "SKILL.md"))) ? plugin2.version : null;
18323
18450
  }
18324
18451
  if (typeof plugin2.$schema === "string" && plugin2.$schema.startsWith("https://agent-plugins.org/schemas/")) {
18325
18452
  return null;
@@ -18337,7 +18464,7 @@ function verifiedPluginVersion(root, client) {
18337
18464
  if (!mcp || typeof mcp !== "object") return null;
18338
18465
  const canonryServer = mcp.mcpServers?.canonry;
18339
18466
  if (canonryServer?.command !== "canonry-mcp") return null;
18340
- return REQUIRED_SKILLS.every((name) => fs17.existsSync(path11.join(root, "skills", name, "SKILL.md"))) ? plugin.version : null;
18467
+ return REQUIRED_SKILLS.every((name) => fs18.existsSync(path11.join(root, "skills", name, "SKILL.md"))) ? plugin.version : null;
18341
18468
  }
18342
18469
  function readClaudeUserInstallPaths(claudeConfigDir) {
18343
18470
  const parsed = readJson(path11.join(claudeConfigDir, "plugins", "installed_plugins.json"));
@@ -18354,7 +18481,7 @@ function readClaudeUserInstallPaths(claudeConfigDir) {
18354
18481
  }
18355
18482
  function listDirectories(dir) {
18356
18483
  try {
18357
- return fs17.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path11.join(dir, entry.name));
18484
+ return fs18.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path11.join(dir, entry.name));
18358
18485
  } catch {
18359
18486
  return [];
18360
18487
  }
@@ -18401,6 +18528,80 @@ function detectCanonryAgentPlugin(opts = {}) {
18401
18528
  return { configuredClients, verifiedClients, verifiedClientVersions };
18402
18529
  }
18403
18530
 
18531
+ // src/server-crash-telemetry.ts
18532
+ import fs19 from "fs";
18533
+ import path12 from "path";
18534
+ var CRASH_MARKER = "crash-telemetry.json";
18535
+ var MAX_MARKER_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
18536
+ function telemetryAllowed() {
18537
+ try {
18538
+ return isTelemetryEnabled();
18539
+ } catch {
18540
+ return false;
18541
+ }
18542
+ }
18543
+ function markerPath(configDir) {
18544
+ return path12.join(configDir, CRASH_MARKER);
18545
+ }
18546
+ function trackServerStartFailure(err, reasonCode) {
18547
+ try {
18548
+ const classified = classifyOutcomeError(err);
18549
+ const code = err?.code;
18550
+ const reason = reasonCode ?? (code === "EADDRINUSE" ? OutcomeReasonCodes.PORT_IN_USE : classified.reasonCode);
18551
+ trackFeatureCompleted({
18552
+ feature: "server",
18553
+ operation: "start",
18554
+ status: "failed",
18555
+ trigger: OutcomeTriggers.startup,
18556
+ reasonCode: reason,
18557
+ ...classified.errorName ? { errorName: classified.errorName } : {}
18558
+ });
18559
+ } catch {
18560
+ }
18561
+ }
18562
+ var watched = /* @__PURE__ */ new WeakSet();
18563
+ function watchServerCrashes(configDir = getConfigDir(), proc = process, now = Date.now) {
18564
+ if (watched.has(proc)) return;
18565
+ watched.add(proc);
18566
+ proc.on("uncaughtExceptionMonitor", (err, origin) => {
18567
+ if (!telemetryAllowed()) return;
18568
+ try {
18569
+ const { errorName } = classifyOutcomeError(err);
18570
+ fs19.writeFileSync(markerPath(configDir), JSON.stringify({
18571
+ origin: origin === "unhandledRejection" ? "unhandledRejection" : "uncaughtException",
18572
+ at: now(),
18573
+ ...errorName ? { errorName } : {}
18574
+ }));
18575
+ } catch {
18576
+ }
18577
+ });
18578
+ }
18579
+ function reportPreviousServerCrash(configDir = getConfigDir(), now = Date.now) {
18580
+ if (!telemetryAllowed()) return;
18581
+ const file = markerPath(configDir);
18582
+ let marker;
18583
+ try {
18584
+ marker = JSON.parse(fs19.readFileSync(file, "utf8"));
18585
+ } catch {
18586
+ return;
18587
+ }
18588
+ try {
18589
+ fs19.rmSync(file, { force: true });
18590
+ } catch {
18591
+ return;
18592
+ }
18593
+ if (typeof marker.at !== "number" || now() - marker.at > MAX_MARKER_AGE_MS) return;
18594
+ const errorName = typeof marker.errorName === "string" && /^[A-Z_$][\w$]{0,39}$/i.test(marker.errorName) ? marker.errorName : void 0;
18595
+ trackFeatureCompleted({
18596
+ feature: "server",
18597
+ operation: "crash",
18598
+ status: "failed",
18599
+ trigger: OutcomeTriggers.startup,
18600
+ reasonCode: marker.origin === "unhandledRejection" ? OutcomeReasonCodes.UNHANDLED_REJECTION : OutcomeReasonCodes.UNCAUGHT_EXCEPTION,
18601
+ ...errorName ? { errorName } : {}
18602
+ });
18603
+ }
18604
+
18404
18605
  // src/commands/serve.ts
18405
18606
  function readServeOpenState(db) {
18406
18607
  try {
@@ -18449,7 +18650,12 @@ async function serveCommand(format = "text") {
18449
18650
  const host = process.env.CANONRY_HOST ?? "127.0.0.1";
18450
18651
  config.port = port;
18451
18652
  const db = createClient(config.database);
18452
- migrate(db);
18653
+ try {
18654
+ migrate(db);
18655
+ } catch (err) {
18656
+ trackServerStartFailure(err, "MIGRATION_FAILED");
18657
+ throw err;
18658
+ }
18453
18659
  try {
18454
18660
  const result = await repairNormalizedPathsOnStartup(db);
18455
18661
  if (result && result.updated > 0 && format === "text") {
@@ -18526,10 +18732,13 @@ Canonry server running at ${url}`);
18526
18732
  }, null, 2));
18527
18733
  }
18528
18734
  setTelemetrySource("cli-server");
18735
+ watchServerCrashes();
18736
+ reportPreviousServerCrash();
18529
18737
  const providerNames = registeredProviderNames(config);
18530
18738
  trackEvent("serve.started", {
18531
18739
  providerCount: providerNames.length,
18532
18740
  providers: providerNames,
18741
+ ...installAttribution(),
18533
18742
  // Who launched the server: an agent-started install is its own funnel.
18534
18743
  ...cliRuntimeContext()
18535
18744
  });
@@ -18540,6 +18749,7 @@ Canonry server running at ${url}`);
18540
18749
  `);
18541
18750
  return;
18542
18751
  }
18752
+ trackServerStartFailure(err);
18543
18753
  try {
18544
18754
  await closeWithIdleSweep(app);
18545
18755
  } catch (closeErr) {
@@ -18732,10 +18942,11 @@ var SYSTEM_CLI_COMMANDS = [
18732
18942
  },
18733
18943
  {
18734
18944
  path: ["init"],
18735
- usage: "canonry init [--force] [--gemini-key <key>] [--openai-key <key>] [--claude-key <key>] [--perplexity-key <key>] [--muse-key <key>] [--local-url <url>] [--local-model <name>] [--local-key <key>] [--google-client-id <id>] [--google-client-secret <key>] [--skip-skills] [--skip-mcp] [--skills-dir <path>] [--format json]",
18945
+ usage: "canonry init [--force] [--ref <tag>] [--gemini-key <key>] [--openai-key <key>] [--claude-key <key>] [--perplexity-key <key>] [--muse-key <key>] [--local-url <url>] [--local-model <name>] [--local-key <key>] [--google-client-id <id>] [--google-client-secret <key>] [--skip-skills] [--skip-mcp] [--skills-dir <path>] [--format json]",
18736
18946
  help: 'Optional interactive provider and OAuth setup. First-run Page Health does not need this: use "canonry bootstrap", then "canonry serve". At least one provider is required here. --format json needs provider flags or environment variables.',
18737
18947
  options: {
18738
18948
  force: { type: "boolean", short: "f", default: false },
18949
+ ref: stringOption(),
18739
18950
  "gemini-key": stringOption(),
18740
18951
  "openai-key": stringOption(),
18741
18952
  "claude-key": stringOption(),
@@ -18754,6 +18965,7 @@ var SYSTEM_CLI_COMMANDS = [
18754
18965
  run: async (input) => {
18755
18966
  await initCommand({
18756
18967
  force: getBoolean(input.values, "force"),
18968
+ ref: getString(input.values, "ref"),
18757
18969
  geminiKey: getString(input.values, "gemini-key"),
18758
18970
  openaiKey: getString(input.values, "openai-key"),
18759
18971
  claudeKey: getString(input.values, "claude-key"),
@@ -18773,17 +18985,19 @@ var SYSTEM_CLI_COMMANDS = [
18773
18985
  },
18774
18986
  {
18775
18987
  path: ["bootstrap"],
18776
- usage: "canonry bootstrap [--format json]",
18988
+ usage: "canonry bootstrap [--ref <tag>] [--format json]",
18777
18989
  help: "Create or reconcile the local config, SQLite database, and default API key from supported environment variables. Provider credentials are optional. Safe to rerun: existing settings are preserved and the result reports created, updated, or unchanged. The legacy --force/-f flag is accepted but has no effect.",
18778
18990
  options: {
18779
18991
  // Compatibility for scripts written before bootstrap became idempotent.
18780
18992
  // Keep this out of usage: the flag is unnecessary and has no behavior.
18781
- force: { type: "boolean", short: "f", default: false }
18993
+ force: { type: "boolean", short: "f", default: false },
18994
+ ref: stringOption()
18782
18995
  },
18783
18996
  allowPositionals: false,
18784
18997
  run: async (input) => {
18785
18998
  await bootstrapCommand({
18786
- format: input.format
18999
+ format: input.format,
19000
+ ref: getString(input.values, "ref")
18787
19001
  });
18788
19002
  }
18789
19003
  },
@@ -18839,6 +19053,15 @@ var SYSTEM_CLI_COMMANDS = [
18839
19053
  stopDaemon(input.format);
18840
19054
  }
18841
19055
  },
19056
+ {
19057
+ path: ["support"],
19058
+ usage: "canonry support [--format json]",
19059
+ help: "Where to get help: the Canonry Discord, the issue tracker, and canonry feedback.",
19060
+ allowPositionals: false,
19061
+ run: async (input) => {
19062
+ supportCommand(input.format);
19063
+ }
19064
+ },
18842
19065
  {
18843
19066
  path: ["feedback"],
18844
19067
  usage: FEEDBACK_USAGE,
@@ -19851,14 +20074,14 @@ Usage: ${usage}`, {
19851
20074
  }
19852
20075
  }
19853
20076
  ];
19854
- function siteHealthAlias(sourcePath, path12, usage) {
20077
+ function siteHealthAlias(sourcePath, path13, usage) {
19855
20078
  const source = TECHNICAL_AEO_CLI_COMMANDS_BASE.find(
19856
20079
  (candidate) => candidate.path.join(" ") === sourcePath.join(" ")
19857
20080
  );
19858
20081
  if (!source) throw new Error(`Missing Site Health compatibility source: ${sourcePath.join(" ")}`);
19859
20082
  return {
19860
20083
  ...source,
19861
- path: path12,
20084
+ path: path13,
19862
20085
  usage,
19863
20086
  run: async (input) => {
19864
20087
  try {
@@ -19869,7 +20092,7 @@ function siteHealthAlias(sourcePath, path12, usage) {
19869
20092
  throw usageError(`${firstLine}
19870
20093
  Usage: ${usage}`, {
19871
20094
  message: error.message,
19872
- details: { ...error.details, command: path12.join("."), usage }
20095
+ details: { ...error.details, command: path13.join("."), usage }
19873
20096
  });
19874
20097
  }
19875
20098
  }
@@ -20242,7 +20465,7 @@ var VISIBILITY_STATS_CLI_COMMANDS = [
20242
20465
  ];
20243
20466
 
20244
20467
  // src/cli-commands/wordpress.ts
20245
- import fs18 from "fs";
20468
+ import fs20 from "fs";
20246
20469
 
20247
20470
  // src/commands/wordpress.ts
20248
20471
  function getClient28() {
@@ -20481,12 +20704,12 @@ async function wordpressSetMeta(project2, body) {
20481
20704
  printPageDetail(result);
20482
20705
  }
20483
20706
  async function wordpressBulkSetMeta(project2, opts) {
20484
- const fs19 = await import("fs/promises");
20485
- const path12 = await import("path");
20486
- const filePath = path12.resolve(opts.from);
20707
+ const fs21 = await import("fs/promises");
20708
+ const path13 = await import("path");
20709
+ const filePath = path13.resolve(opts.from);
20487
20710
  let raw;
20488
20711
  try {
20489
- raw = await fs19.readFile(filePath, "utf8");
20712
+ raw = await fs21.readFile(filePath, "utf8");
20490
20713
  } catch {
20491
20714
  throw new CliError({
20492
20715
  code: "FILE_READ_ERROR",
@@ -20583,13 +20806,13 @@ async function wordpressSetSchema(project2, body) {
20583
20806
  printManualAssist(`Schema update for "${body.slug}"`, result);
20584
20807
  }
20585
20808
  async function wordpressSchemaDeploy(project2, opts) {
20586
- const fs19 = await import("fs/promises");
20587
- const path12 = await import("path");
20809
+ const fs21 = await import("fs/promises");
20810
+ const path13 = await import("path");
20588
20811
  const yaml = await loadYamlModule();
20589
- const filePath = path12.resolve(opts.profile);
20812
+ const filePath = path13.resolve(opts.profile);
20590
20813
  let raw;
20591
20814
  try {
20592
- raw = await fs19.readFile(filePath, "utf8");
20815
+ raw = await fs21.readFile(filePath, "utf8");
20593
20816
  } catch {
20594
20817
  throw new CliError({
20595
20818
  code: "FILE_READ_ERROR",
@@ -20694,13 +20917,13 @@ async function wordpressOnboard(project2, opts) {
20694
20917
  }
20695
20918
  let profileData;
20696
20919
  if (opts.profile) {
20697
- const fs19 = await import("fs/promises");
20698
- const path12 = await import("path");
20920
+ const fs21 = await import("fs/promises");
20921
+ const path13 = await import("path");
20699
20922
  const yaml = await loadYamlModule();
20700
- const filePath = path12.resolve(opts.profile);
20923
+ const filePath = path13.resolve(opts.profile);
20701
20924
  let raw;
20702
20925
  try {
20703
- raw = await fs19.readFile(filePath, "utf8");
20926
+ raw = await fs21.readFile(filePath, "utf8");
20704
20927
  } catch {
20705
20928
  throw new CliError({
20706
20929
  code: "FILE_READ_ERROR",
@@ -20849,7 +21072,7 @@ function resolveContent(input, command, usage, options) {
20849
21072
  }
20850
21073
  if (contentFile) {
20851
21074
  try {
20852
- return fs18.readFileSync(contentFile, "utf-8");
21075
+ return fs20.readFileSync(contentFile, "utf-8");
20853
21076
  } catch (error) {
20854
21077
  const message = describeError(error);
20855
21078
  throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
@@ -21327,6 +21550,7 @@ async function agentAttach(opts) {
21327
21550
  const existing = await client.listNotifications(opts.project);
21328
21551
  const hasAgent = existing.some((n) => n.source === "agent");
21329
21552
  if (hasAgent) {
21553
+ trackCliConnection({ integration: "agent_webhook", action: "connect", status: "cancelled", reasonCode: "ALREADY_CONNECTED" });
21330
21554
  if (isMachineFormat(opts.format)) {
21331
21555
  console.log(JSON.stringify({ status: "already-attached", project: opts.project }));
21332
21556
  } else {
@@ -21351,6 +21575,7 @@ async function agentDetach(opts) {
21351
21575
  const existing = await client.listNotifications(opts.project);
21352
21576
  const agentNotif = existing.find((n) => n.source === "agent");
21353
21577
  if (!agentNotif) {
21578
+ trackCliConnection({ integration: "agent_webhook", action: "disconnect", status: "cancelled", reasonCode: "NOT_CONNECTED" });
21354
21579
  if (isMachineFormat(opts.format)) {
21355
21580
  console.log(JSON.stringify({ status: "not-attached", project: opts.project }));
21356
21581
  } else {
@@ -22165,6 +22390,7 @@ var REGISTERED_CLI_COMMANDS = [
22165
22390
  ...USERS_CLI_COMMANDS,
22166
22391
  ...COMPETITOR_CLI_COMMANDS,
22167
22392
  ...SETTINGS_CLI_COMMANDS,
22393
+ ...DASHBOARD_CLI_COMMANDS,
22168
22394
  ...SKILLS_CLI_COMMANDS,
22169
22395
  ...SNAPSHOT_CLI_COMMANDS,
22170
22396
  ...RUN_CLI_COMMANDS,
@@ -22304,9 +22530,11 @@ Automation:
22304
22530
 
22305
22531
  Admin:
22306
22532
  settings Show/update provider and quota settings
22533
+ dashboard Reset the dashboard password
22307
22534
  backfill Backfill answer visibility or insights
22308
22535
  telemetry Manage anonymous telemetry
22309
22536
  history <project> Show audit trail
22537
+ support Get help: Canonry Discord and issue tracker
22310
22538
 
22311
22539
  Global options:
22312
22540
  --format json Machine-readable output: one JSON document (all commands)