@ainyc/canonry 4.180.7 → 4.182.3

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 (41) hide show
  1. package/README.md +80 -45
  2. package/assets/agent-workspace/skills/canonry/SKILL.md +13 -10
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +7 -6
  4. package/assets/assets/{AuditHistoryPanel-B1lmI7wm.js → AuditHistoryPanel-1_FHpzj-.js} +1 -1
  5. package/assets/assets/{BacklinksPage-Bw61uQu8.js → BacklinksPage-045za10_.js} +1 -1
  6. package/assets/assets/{ChartPrimitives-DG35TyhH.js → ChartPrimitives-DLEICrrL.js} +1 -1
  7. package/assets/assets/{HistoryPage-D8z9-NkT.js → HistoryPage-36K_6asu.js} +1 -1
  8. package/assets/assets/{MeasurementPropertyPage-CXX9GFj8.js → MeasurementPropertyPage-B4DJGoY6.js} +1 -1
  9. package/assets/assets/ProjectPage-CshC68Ne.js +9 -0
  10. package/assets/assets/{RunRow-DXHy16T3.js → RunRow-U2CN755e.js} +1 -1
  11. package/assets/assets/{RunsPage-D2aE620i.js → RunsPage-D0CtU4d-.js} +1 -1
  12. package/assets/assets/SettingsPage-zMWieTPK.js +1 -0
  13. package/assets/assets/SiteHealthSection-TpQPu_Uj.js +4 -0
  14. package/assets/assets/{TrafficPage-DFHWgvkU.js → TrafficPage-DfLUt-64.js} +1 -1
  15. package/assets/assets/{TrafficSourceDetailPage-DxjyRL8Y.js → TrafficSourceDetailPage-DbjNNWSt.js} +1 -1
  16. package/assets/assets/extract-error-message-CWqz1QPC.js +1 -0
  17. package/assets/assets/index-Brb7M_6G.js +86 -0
  18. package/assets/assets/index-DwdK2e4e.css +1 -0
  19. package/assets/assets/{react-sigma_core.esm.min-DAcOZae7.js → react-sigma_core.esm.min-Drqprr9L.js} +1 -1
  20. package/assets/assets/{vendor-lucide-nYKv-BB7.js → vendor-lucide-XjCNFK0x.js} +1 -1
  21. package/assets/assets/{vendor-markdown-Bn7fRwrx.js → vendor-markdown-9tTeP8v7.js} +1 -1
  22. package/assets/assets/{vendor-radix-4UOaVWza.js → vendor-radix-BpmvLvpp.js} +1 -1
  23. package/assets/assets/{vendor-recharts-D8rt5lKn.js → vendor-recharts-FA0uySU7.js} +1 -1
  24. package/assets/assets/{vendor-tanstack-Dock0eH1.js → vendor-tanstack-CYBjbqVr.js} +1 -1
  25. package/assets/index.html +7 -7
  26. package/dist/{chunk-VZO6CLHY.js → chunk-BDJT7XZL.js} +169 -64
  27. package/dist/{chunk-RXXZ5BU5.js → chunk-DY5ZDKQW.js} +183 -1
  28. package/dist/{chunk-CWXPBQR5.js → chunk-FVC7PGYQ.js} +47 -5
  29. package/dist/{chunk-XYTSKRO3.js → chunk-I3KSOMJA.js} +2 -2
  30. package/dist/{chunk-WJFBNCVD.js → chunk-K45YKXE4.js} +4310 -3323
  31. package/dist/cli.js +323 -99
  32. package/dist/index.js +4 -4
  33. package/dist/{intelligence-service-MEPYXRBS.js → intelligence-service-HGA553GT.js} +2 -2
  34. package/dist/mcp.js +3 -3
  35. package/package.json +7 -7
  36. package/assets/assets/ProjectPage-BaN_rvoV.js +0 -9
  37. package/assets/assets/SettingsPage-BiiFqarS.js +0 -1
  38. package/assets/assets/SiteHealthSection-DCH2Ejtc.js +0 -4
  39. package/assets/assets/extract-error-message-BdXUZQW9.js +0 -1
  40. package/assets/assets/index-BuEq5Rhg.css +0 -1
  41. package/assets/assets/index-CSictO9U.js +0 -83
package/dist/cli.js CHANGED
@@ -19,18 +19,20 @@ import {
19
19
  getOrCreateAnonymousId,
20
20
  isAeroToolProfile,
21
21
  isFirstRun,
22
+ isLoopbackBindHost,
22
23
  isTelemetryEnabled,
23
24
  listAgentProviders,
24
25
  setGoogleAuthConfig,
25
26
  setTelemetrySource,
26
27
  showFirstRunNotice,
27
28
  trackCliCommandFinished,
28
- trackEvent
29
- } from "./chunk-VZO6CLHY.js";
29
+ trackEvent,
30
+ waitForServerRuntimeStartup
31
+ } from "./chunk-BDJT7XZL.js";
30
32
  import {
31
33
  autoSyncSkills,
32
34
  formatAutoSyncNotice
33
- } from "./chunk-XYTSKRO3.js";
35
+ } from "./chunk-I3KSOMJA.js";
34
36
  import {
35
37
  CliError,
36
38
  EXIT_SYSTEM_ERROR,
@@ -55,7 +57,7 @@ import {
55
57
  saveConfigPatch,
56
58
  systemError,
57
59
  usageError
58
- } from "./chunk-CWXPBQR5.js";
60
+ } from "./chunk-FVC7PGYQ.js";
59
61
  import {
60
62
  CLOUDFLARE_WORKER_BINDINGS,
61
63
  CLOUDFLARE_WORKER_GENERATED_MARKER,
@@ -69,7 +71,7 @@ import {
69
71
  projects,
70
72
  queries,
71
73
  renderReportHtml
72
- } from "./chunk-WJFBNCVD.js";
74
+ } from "./chunk-K45YKXE4.js";
73
75
  import {
74
76
  AdsDeliverySnapshotStatuses,
75
77
  AdsHistoricalCampaignRollupStatuses,
@@ -138,7 +140,7 @@ import {
138
140
  providerQuotaPolicySchema,
139
141
  resolveProviderInput,
140
142
  winnabilityClassSchema
141
- } from "./chunk-RXXZ5BU5.js";
143
+ } from "./chunk-DY5ZDKQW.js";
142
144
 
143
145
  // src/cli.ts
144
146
  import { pathToFileURL } from "url";
@@ -184,6 +186,13 @@ import { parseArgs } from "util";
184
186
  function commandId(spec) {
185
187
  return spec.path.join(".");
186
188
  }
189
+ function printCommandHelp(spec) {
190
+ console.log(`
191
+ Usage: ${spec.usage}
192
+ `);
193
+ if (spec.help) console.log(`${spec.help}
194
+ `);
195
+ }
187
196
  function matchesPath(args, path13) {
188
197
  if (args.length < path13.length) return false;
189
198
  return path13.every((segment, index) => args[index] === segment);
@@ -222,9 +231,7 @@ async function dispatchRegisteredCommand(args, fallbackFormat, specs) {
222
231
  const argsWithoutHelp = args.filter((a) => a !== "--help" && a !== "-h");
223
232
  const exactMatch = [...specs].sort((a, b) => b.path.length - a.path.length).find((candidate) => matchesPath(argsWithoutHelp, candidate.path));
224
233
  if (exactMatch && exactMatch.path.length > 1) {
225
- console.log(`
226
- Usage: ${exactMatch.usage}
227
- `);
234
+ printCommandHelp(exactMatch);
228
235
  return true;
229
236
  }
230
237
  const group = argsWithoutHelp[0];
@@ -236,9 +243,7 @@ Usage: ${exactMatch.usage}
236
243
  }
237
244
  const single = specs.find((s) => s.path.length === 1 && s.path[0] === group);
238
245
  if (single) {
239
- console.log(`
240
- Usage: ${single.usage}
241
- `);
246
+ printCommandHelp(single);
242
247
  return true;
243
248
  }
244
249
  }
@@ -7564,8 +7569,95 @@ async function listCompetitors(project, format) {
7564
7569
  console.log(` ${c.domain}`);
7565
7570
  }
7566
7571
  }
7572
+ async function showCompetitorLandscape(project, options) {
7573
+ const client = getClient10();
7574
+ const data = await client.getCompetitorLandscape(project, {
7575
+ window: options.window,
7576
+ scope: options.scope,
7577
+ groupKey: options.groupKey,
7578
+ provider: options.provider,
7579
+ ...options.groupBy ? { groupBy: options.groupBy } : {},
7580
+ ...options.model !== void 0 ? { model: options.model } : {},
7581
+ queryClass: options.queryClass,
7582
+ location: options.location,
7583
+ runId: options.runId
7584
+ });
7585
+ if (options.format === "json") {
7586
+ console.log(JSON.stringify(data, null, 2));
7587
+ return;
7588
+ }
7589
+ if (options.format === "jsonl") {
7590
+ console.log(JSON.stringify(data));
7591
+ return;
7592
+ }
7593
+ printCompetitorLandscape(data);
7594
+ }
7595
+ function printCompetitorLandscape(data) {
7596
+ const scope = data.scope.kind === "group" ? `market ${data.scope.groupKey}` : data.scope.kind === "all-markets" ? "all markets" : "project";
7597
+ console.log(`Competitor landscape \xB7 ${scope} \xB7 ${data.window}`);
7598
+ if (data.filters.model !== void 0) {
7599
+ console.log(`Requested model filter: ${data.filters.provider} \xB7 ${data.filters.model}`);
7600
+ }
7601
+ console.log("Mention share is answer-text evidence; citations are independent source-list evidence.");
7602
+ console.log(
7603
+ data.filters.queryClass === "all" ? "Share of voice is blank: branded and non-brand queries are pooled here. Add --query-class non-brand for a ratio." : `Counting ${data.filters.queryClass} queries only.`
7604
+ );
7605
+ if (data.truncated) console.log("Top 100 observed competitors and top 100 other cited sources shown; pinned competitors are complete.");
7606
+ console.log("");
7607
+ printLandscapeRows("Your brand", [data.project]);
7608
+ printLandscapeRows("Pinned competitors", data.pinned);
7609
+ printLandscapeRows("Observed competitors", data.observed);
7610
+ printLandscapeRows("Other cited sources", data.otherSources);
7611
+ console.log("");
7612
+ console.log(
7613
+ `Evidence: ${data.evidence.answeredResults} answer-text result(s), ${data.evidence.sourceResults} source result(s); excluded: ${data.evidence.excludedProbeResults} probe, ${data.evidence.excludedNonCompletedResults} non-completed.`
7614
+ );
7615
+ if (data.modelComparison) printModelComparison(data.modelComparison);
7616
+ }
7617
+ function describeServedModels(evidence) {
7618
+ if (evidence.status === "unknown") return "Unknown (not disclosed)";
7619
+ if (evidence.status === "known") return evidence.model;
7620
+ return [...evidence.models, ...evidence.includesUnknown ? ["Unknown (not disclosed)"] : []].join(", ");
7621
+ }
7622
+ function printModelComparison(comparison) {
7623
+ console.log("");
7624
+ console.log(`Model comparison \xB7 requested-model basis \xB7 ${comparison.groups.length} of ${comparison.totalGroups} groups`);
7625
+ console.log("Groups use stored observations. They do not form a matched-query or equal-weight comparison.");
7626
+ if (comparison.truncated) console.log("The first 50 provider/model groups are shown. Additional groups are omitted.");
7627
+ for (const group of comparison.groups) {
7628
+ console.log("");
7629
+ console.log(`${group.provider} \xB7 requested model: ${group.model ?? "Unknown (not recorded)"}`);
7630
+ console.log(`Served model evidence: ${describeServedModels(group.servedModels)}`);
7631
+ console.log(`Samples: ${group.snapshotCount} snapshot(s), ${group.evidence.answeredResults} answer-text result(s), ${group.evidence.sourceResults} source result(s).`);
7632
+ printLandscapeRows("Your brand", [group.project], true);
7633
+ printLandscapeRows("Pinned competitors", group.pinned, true);
7634
+ printLandscapeRows("Observed competitors", group.observed, true);
7635
+ printLandscapeRows("Other cited sources", group.otherSources, true);
7636
+ if (group.truncated) console.log("Top 100 observed competitors and top 100 other sources shown. Pinned competitors are complete.");
7637
+ }
7638
+ }
7639
+ function printLandscapeRows(heading, rows, showSampleCount = false) {
7640
+ console.log(`${heading}:`);
7641
+ if (rows.length === 0) {
7642
+ console.log(" \u2014");
7643
+ return;
7644
+ }
7645
+ for (const row of rows) {
7646
+ const sov = row.shareOfVoice === null ? "\u2014" : `${row.shareOfVoice.toFixed(1)}%`;
7647
+ console.log(` ${row.domain} mention ${row.mentionCount} \xB7 citation ${row.citationCount} \xB7 SOV ${sov}${showSampleCount ? ` \xB7 answers ${row.answeredResults}` : ""}`);
7648
+ }
7649
+ }
7567
7650
 
7568
7651
  // src/cli-commands/competitor.ts
7652
+ var LANDSCAPE_USAGE = "canonry competitor landscape <project> [--window 7d|30d|90d|all] [--group-key <key>|--scope all-markets] [--by-model] [--provider <provider> [--model <id>]] [--query-class all|branded|non-brand] [--location <label>] [--run-id <id>] [--format json|jsonl]";
7653
+ function parseLandscapeScope(value) {
7654
+ if (value === void 0 || value === "") return void 0;
7655
+ if (value !== "all-markets") throw usageError(`--scope must be "all-markets" (got "${value}")`, {
7656
+ message: '--scope must be "all-markets"',
7657
+ details: { command: "competitor.landscape", usage: LANDSCAPE_USAGE }
7658
+ });
7659
+ return value;
7660
+ }
7569
7661
  var COMPETITOR_CLI_COMMANDS = [
7570
7662
  {
7571
7663
  path: ["competitor", "add"],
@@ -7629,14 +7721,60 @@ var COMPETITOR_CLI_COMMANDS = [
7629
7721
  await listCompetitors(project, input.format);
7630
7722
  }
7631
7723
  },
7724
+ {
7725
+ path: ["competitor", "landscape"],
7726
+ usage: LANDSCAPE_USAGE,
7727
+ options: {
7728
+ window: stringOption(),
7729
+ "group-key": stringOption(),
7730
+ scope: stringOption(),
7731
+ provider: stringOption(),
7732
+ "by-model": { type: "boolean" },
7733
+ model: stringOption(),
7734
+ "query-class": stringOption(),
7735
+ location: stringOption(),
7736
+ "run-id": stringOption()
7737
+ },
7738
+ run: async (input) => {
7739
+ const project = requireProject(input, "competitor.landscape", LANDSCAPE_USAGE);
7740
+ const scope = parseLandscapeScope(getString(input.values, "scope"));
7741
+ const groupKey = getString(input.values, "group-key");
7742
+ const provider = getString(input.values, "provider");
7743
+ const model = getString(input.values, "model");
7744
+ if (model !== void 0 && !provider?.trim()) {
7745
+ throw usageError("--model requires --provider", {
7746
+ message: "--model requires --provider",
7747
+ details: { command: "competitor.landscape", usage: LANDSCAPE_USAGE }
7748
+ });
7749
+ }
7750
+ if (scope === "all-markets" && groupKey) {
7751
+ throw usageError("--group-key cannot be combined with --scope all-markets", {
7752
+ message: "--group-key cannot be combined with --scope all-markets",
7753
+ details: { command: "competitor.landscape", usage: LANDSCAPE_USAGE }
7754
+ });
7755
+ }
7756
+ await showCompetitorLandscape(project, {
7757
+ window: getString(input.values, "window"),
7758
+ groupKey,
7759
+ scope,
7760
+ provider,
7761
+ ...getBoolean(input.values, "by-model") ? { groupBy: "model" } : {},
7762
+ ...model !== void 0 ? { model } : {},
7763
+ queryClass: getString(input.values, "query-class"),
7764
+ location: getString(input.values, "location"),
7765
+ runId: getString(input.values, "run-id"),
7766
+ format: input.format
7767
+ });
7768
+ }
7769
+ },
7632
7770
  {
7633
7771
  path: ["competitor"],
7634
- usage: "canonry competitor <add|remove|delete|list> <project> [args]",
7772
+ usage: "canonry competitor <add|remove|delete|list|landscape> <project> [args]",
7635
7773
  run: async (input) => {
7636
7774
  unknownSubcommand(input.positionals[0], {
7637
7775
  command: "competitor",
7638
- usage: "canonry competitor <add|remove|delete|list> <project> [args]",
7639
- available: ["add", "remove", "delete", "list"]
7776
+ usage: "canonry competitor <add|remove|delete|list|landscape> <project> [args]",
7777
+ available: ["add", "remove", "delete", "list", "landscape"]
7640
7778
  });
7641
7779
  }
7642
7780
  }
@@ -13866,6 +14004,7 @@ var CONTENT_CLI_COMMANDS = [
13866
14004
  // src/commands/bootstrap.ts
13867
14005
  import crypto from "crypto";
13868
14006
  import path9 from "path";
14007
+ import { isDeepStrictEqual } from "util";
13869
14008
  import { eq } from "drizzle-orm";
13870
14009
 
13871
14010
  // ../config/src/index.ts
@@ -14011,36 +14150,40 @@ function getBootstrapEnv(source, overrides) {
14011
14150
  }
14012
14151
 
14013
14152
  // src/commands/bootstrap.ts
14014
- async function bootstrapCommand(_opts) {
14015
- const format = _opts?.format ?? "text";
14016
- const env = getBootstrapEnv(process.env);
14017
- const providers = env.providers;
14018
- const hasProvider = providers?.gemini || providers?.openai || providers?.claude || providers?.perplexity || providers?.local;
14019
- if (!hasProvider) {
14020
- console.warn(
14021
- "Warning: No provider env vars set (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, PERPLEXITY_API_KEY, or LOCAL_BASE_URL). You can configure providers later via the dashboard."
14153
+ function persistedValue(value) {
14154
+ if (Array.isArray(value)) return value.map(persistedValue);
14155
+ if (value && typeof value === "object") {
14156
+ return Object.fromEntries(
14157
+ Object.entries(value).filter(([, entry]) => entry !== void 0).map(([key, entry]) => [key, persistedValue(entry)])
14022
14158
  );
14023
14159
  }
14160
+ return value;
14161
+ }
14162
+ async function bootstrapCommand(opts) {
14163
+ const format = opts?.format ?? "text";
14164
+ const env = getBootstrapEnv(process.env);
14165
+ const providers = env.providers;
14024
14166
  const configDir = getConfigDir();
14025
- const databasePath = env.databasePath || path9.join(configDir, "data.db");
14026
14167
  const existing = configExists();
14027
14168
  const existingConfig = existing ? loadConfig() : void 0;
14169
+ const existingRaw = existing ? loadConfigRaw() : null;
14170
+ const databasePath = env.databasePath || existingRaw?.database || path9.join(configDir, "data.db");
14028
14171
  let rawApiKey;
14029
14172
  let generatedApiKey;
14030
14173
  if (env.apiKey) {
14031
14174
  rawApiKey = env.apiKey;
14032
- } else if (existingConfig) {
14033
- rawApiKey = existingConfig.apiKey;
14175
+ } else if (existingRaw) {
14176
+ rawApiKey = existingRaw.apiKey;
14034
14177
  } else {
14035
14178
  generatedApiKey = `cnry_${crypto.randomBytes(16).toString("hex")}`;
14036
14179
  rawApiKey = generatedApiKey;
14037
14180
  }
14038
14181
  const mergedProviders = { ...existingConfig?.providers };
14039
- if (providers?.gemini) mergedProviders.gemini = providers.gemini;
14040
- if (providers?.openai) mergedProviders.openai = providers.openai;
14041
- if (providers?.claude) mergedProviders.claude = providers.claude;
14042
- if (providers?.perplexity) mergedProviders.perplexity = providers.perplexity;
14043
- if (providers?.local) mergedProviders.local = providers.local;
14182
+ if (providers.gemini) mergedProviders.gemini = providers.gemini;
14183
+ if (providers.openai) mergedProviders.openai = providers.openai;
14184
+ if (providers.claude) mergedProviders.claude = providers.claude;
14185
+ if (providers.perplexity) mergedProviders.perplexity = providers.perplexity;
14186
+ if (providers.local) mergedProviders.local = providers.local;
14044
14187
  if (env.googleClientId && !env.googleClientSecret || !env.googleClientId && env.googleClientSecret) {
14045
14188
  console.warn("Warning: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must both be set to configure Google OAuth. Skipping Google auth config.");
14046
14189
  }
@@ -14051,25 +14194,42 @@ async function bootstrapCommand(_opts) {
14051
14194
  } : existingConfig?.google;
14052
14195
  const keyHash = crypto.createHash("sha256").update(rawApiKey).digest("hex");
14053
14196
  const keyPrefix = rawApiKey.slice(0, 9);
14054
- const existingConfigKeyHash = existingConfig ? crypto.createHash("sha256").update(existingConfig.apiKey).digest("hex") : void 0;
14197
+ const existingConfigKeyHash = existingRaw ? crypto.createHash("sha256").update(existingRaw.apiKey).digest("hex") : void 0;
14055
14198
  const db = createClient(databasePath);
14056
14199
  migrate(db);
14057
- db.transaction((tx) => {
14200
+ const keyChanged = db.transaction((tx) => {
14201
+ let changed = false;
14058
14202
  const rotatedAt = (/* @__PURE__ */ new Date()).toISOString();
14059
- const existingDefaults = tx.select({ id: apiKeys.id, keyHash: apiKeys.keyHash }).from(apiKeys).where(eq(apiKeys.name, "default")).all();
14203
+ const existingDefaults = tx.select({
14204
+ id: apiKeys.id,
14205
+ keyHash: apiKeys.keyHash,
14206
+ keyPrefix: apiKeys.keyPrefix,
14207
+ scopes: apiKeys.scopes,
14208
+ projectId: apiKeys.projectId,
14209
+ lastUsedAt: apiKeys.lastUsedAt,
14210
+ revokedAt: apiKeys.revokedAt
14211
+ }).from(apiKeys).where(eq(apiKeys.name, "default")).all();
14060
14212
  const existingDefault = existingDefaults.find((key) => key.keyHash === existingConfigKeyHash) ?? existingDefaults.at(0);
14061
14213
  if (existingDefault) {
14062
- tx.update(apiKeys).set({
14063
- keyHash,
14064
- keyPrefix,
14065
- scopes: ["*"],
14066
- projectId: null,
14067
- lastUsedAt: null,
14068
- revokedAt: null
14069
- }).where(eq(apiKeys.id, existingDefault.id)).run();
14214
+ const rotating = existingDefault.keyHash !== keyHash;
14215
+ const needsUpdate = rotating || existingDefault.keyPrefix !== keyPrefix || !isDeepStrictEqual(existingDefault.scopes, ["*"]) || existingDefault.projectId !== null || existingDefault.revokedAt !== null;
14216
+ if (needsUpdate) {
14217
+ tx.update(apiKeys).set({
14218
+ keyHash,
14219
+ keyPrefix,
14220
+ scopes: ["*"],
14221
+ projectId: null,
14222
+ ...rotating ? { lastUsedAt: null } : {},
14223
+ revokedAt: null
14224
+ }).where(eq(apiKeys.id, existingDefault.id)).run();
14225
+ changed = true;
14226
+ }
14070
14227
  for (const duplicate of existingDefaults) {
14071
14228
  if (duplicate.id === existingDefault.id) continue;
14072
- tx.update(apiKeys).set({ revokedAt: rotatedAt }).where(eq(apiKeys.id, duplicate.id)).run();
14229
+ if (duplicate.revokedAt === null) {
14230
+ tx.update(apiKeys).set({ revokedAt: rotatedAt }).where(eq(apiKeys.id, duplicate.id)).run();
14231
+ changed = true;
14232
+ }
14073
14233
  }
14074
14234
  } else {
14075
14235
  tx.insert(apiKeys).values({
@@ -14080,29 +14240,41 @@ async function bootstrapCommand(_opts) {
14080
14240
  scopes: ["*"],
14081
14241
  createdAt: rotatedAt
14082
14242
  }).run();
14243
+ changed = true;
14083
14244
  }
14245
+ return changed;
14084
14246
  });
14085
- saveConfig({
14086
- apiUrl: env.apiUrl || existingConfig?.apiUrl || `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
14247
+ const apiUrl = env.apiUrl || existingRaw?.apiUrl || `http://127.0.0.1:${process.env.CANONRY_PORT || "4100"}`;
14248
+ const nextConfig = {
14249
+ ...existingRaw,
14250
+ apiUrl,
14087
14251
  database: databasePath,
14088
14252
  apiKey: rawApiKey,
14089
14253
  providers: mergedProviders,
14090
14254
  google: mergedGoogle
14091
- });
14255
+ };
14256
+ const configChanged = !existingRaw || existingRaw.apiUrl !== apiUrl || existingRaw.database !== databasePath || existingRaw.apiKey !== rawApiKey || !isDeepStrictEqual(persistedValue(existingRaw.providers ?? {}), persistedValue(mergedProviders)) || !isDeepStrictEqual(persistedValue(existingRaw.google), persistedValue(mergedGoogle));
14257
+ if (configChanged) saveConfig(nextConfig);
14258
+ const status = !existing ? "created" : configChanged || keyChanged ? "updated" : "unchanged";
14092
14259
  if (format === "json") {
14093
14260
  console.log(JSON.stringify({
14094
14261
  bootstrapped: true,
14262
+ status,
14263
+ changed: status !== "unchanged",
14095
14264
  configPath: getConfigPath(),
14096
14265
  databasePath,
14097
- apiUrl: env.apiUrl || existingConfig?.apiUrl || `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
14098
- providers: Object.keys(mergedProviders ?? {}),
14266
+ apiUrl,
14267
+ providers: Object.keys(mergedProviders),
14099
14268
  googleConfigured: !!mergedGoogle,
14100
14269
  generatedApiKey
14101
14270
  }, null, 2));
14102
14271
  return;
14103
14272
  }
14104
- console.log(`Bootstrap complete. Config saved to ${getConfigPath()}`);
14273
+ console.log(`Bootstrap ${status}. Config: ${getConfigPath()}`);
14105
14274
  console.log(`SQLite database path: ${databasePath}`);
14275
+ if (Object.keys(mergedProviders).length === 0) {
14276
+ console.log("Providers: none (Page Health works now; add one later to enable AI Visibility).");
14277
+ }
14106
14278
  if (generatedApiKey) {
14107
14279
  console.log(`API key: ${generatedApiKey}`);
14108
14280
  }
@@ -14112,6 +14284,27 @@ async function bootstrapCommand(_opts) {
14112
14284
  import { spawn as spawn2 } from "child_process";
14113
14285
  import fs15 from "fs";
14114
14286
  import path10 from "path";
14287
+
14288
+ // src/operator-url.ts
14289
+ function operatorHost(bindHost) {
14290
+ if (bindHost === "0.0.0.0") return "127.0.0.1";
14291
+ if (bindHost === "::" || bindHost === "[::]") return "::1";
14292
+ return bindHost;
14293
+ }
14294
+ function operatorHttpUrl(bindHost, port) {
14295
+ const host = operatorHost(bindHost);
14296
+ const bare = host.replace(/^\[|\]$/g, "");
14297
+ return `http://${bare.includes(":") ? `[${bare}]` : bare}:${port}`;
14298
+ }
14299
+
14300
+ // src/serve-endpoint.ts
14301
+ function resolveServePort(envPort, configPort) {
14302
+ const trimmed = envPort?.trim();
14303
+ if (trimmed) return parseInt(trimmed, 10);
14304
+ return configPort ?? 4100;
14305
+ }
14306
+
14307
+ // src/commands/daemon.ts
14115
14308
  function getPidPath() {
14116
14309
  return path10.join(getConfigDir(), "canonry.pid");
14117
14310
  }
@@ -14125,7 +14318,7 @@ function isProcessAlive(pid) {
14125
14318
  }
14126
14319
  }
14127
14320
  async function waitForReady(host, port, maxMs = 1e4) {
14128
- const url = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}/health`;
14321
+ const url = `${operatorHttpUrl(host, port)}/health`;
14129
14322
  const deadline = Date.now() + maxMs;
14130
14323
  while (Date.now() < deadline) {
14131
14324
  try {
@@ -14137,6 +14330,13 @@ async function waitForReady(host, port, maxMs = 1e4) {
14137
14330
  }
14138
14331
  return false;
14139
14332
  }
14333
+ function resolveDaemonEndpoint(opts, env, configPort) {
14334
+ const portInput = opts.port || env.CANONRY_PORT;
14335
+ return {
14336
+ host: opts.host || env.CANONRY_HOST || "127.0.0.1",
14337
+ port: resolveServePort(portInput, configPort)
14338
+ };
14339
+ }
14140
14340
  function buildServeForwardArgs(opts) {
14141
14341
  const args = [];
14142
14342
  if (opts.port) args.push("--port", opts.port);
@@ -14167,10 +14367,14 @@ async function startDaemon(opts) {
14167
14367
  }
14168
14368
  fs15.unlinkSync(pidPath);
14169
14369
  }
14370
+ const config = loadConfig();
14371
+ const endpoint = resolveDaemonEndpoint(opts, process.env, config.port);
14372
+ const host = endpoint.host;
14373
+ const port = String(endpoint.port);
14170
14374
  const cliPath = path10.resolve(new URL(import.meta.url).pathname);
14171
14375
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
14172
14376
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
14173
- args.push(...buildServeForwardArgs(opts));
14377
+ args.push(...buildServeForwardArgs({ ...opts, host, port }));
14174
14378
  const child = spawn2(process.execPath, args, {
14175
14379
  detached: true,
14176
14380
  stdio: "ignore"
@@ -14188,13 +14392,15 @@ async function startDaemon(opts) {
14188
14392
  fs15.mkdirSync(configDir, { recursive: true });
14189
14393
  }
14190
14394
  fs15.writeFileSync(pidPath, String(child.pid), "utf-8");
14191
- const port = opts.port ?? "4100";
14192
- const host = opts.host ?? "127.0.0.1";
14193
14395
  if (!isMachineFormat(format)) {
14194
14396
  process.stderr.write("Waiting for server to start...");
14195
14397
  }
14196
14398
  const ready = await waitForReady(host, port);
14197
14399
  if (!ready) {
14400
+ try {
14401
+ child.kill("SIGTERM");
14402
+ } catch {
14403
+ }
14198
14404
  try {
14199
14405
  fs15.unlinkSync(pidPath);
14200
14406
  } catch {
@@ -14213,7 +14419,7 @@ async function startDaemon(opts) {
14213
14419
  if (!isMachineFormat(format)) {
14214
14420
  process.stderr.write("\n");
14215
14421
  }
14216
- const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
14422
+ const url = operatorHttpUrl(host, port);
14217
14423
  if (isMachineFormat(format)) {
14218
14424
  console.log(JSON.stringify({
14219
14425
  started: true,
@@ -14490,7 +14696,7 @@ async function initCommand(opts) {
14490
14696
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14491
14697
  }).run();
14492
14698
  saveConfig({
14493
- apiUrl: `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
14699
+ apiUrl: `http://127.0.0.1:${process.env.CANONRY_PORT || "4100"}`,
14494
14700
  database: databasePath,
14495
14701
  apiKey: rawApiKey,
14496
14702
  providers,
@@ -14537,7 +14743,7 @@ async function initCommand(opts) {
14537
14743
  initialized: true,
14538
14744
  configPath: getConfigPath(),
14539
14745
  databasePath,
14540
- apiUrl: `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
14746
+ apiUrl: `http://127.0.0.1:${process.env.CANONRY_PORT || "4100"}`,
14541
14747
  apiKey: rawApiKey,
14542
14748
  providers: providerNames,
14543
14749
  googleConfigured: !!google,
@@ -14594,7 +14800,7 @@ ${mcpTip}`);
14594
14800
  }
14595
14801
  if (!isMachineFormat(format)) {
14596
14802
  showFirstRunNotice();
14597
- console.log("\nNext: canonry serve to open the dashboard, or canonry settings provider gemini --api-key <key> to configure Gemini.");
14803
+ console.log("\nNext: canonry serve to map your site and capture a Page Health baseline.");
14598
14804
  console.log("\nNext steps:");
14599
14805
  for (const line of nextSteps) {
14600
14806
  console.log(` ${line}`);
@@ -14630,27 +14836,27 @@ ${mcpTip}`);
14630
14836
  const answer = await prompt("\nStart the dashboard now? [Y/n]: ");
14631
14837
  if (!/^n/i.test(answer.trim())) {
14632
14838
  pendingServeHandoff = true;
14633
- console.log("Handing off to the dashboard. Finish setup at /setup once it is up.");
14839
+ console.log("Handing off to the dashboard. Map your site at /setup once it is up.");
14634
14840
  }
14635
14841
  }
14636
14842
  return agentLLM;
14637
14843
  }
14638
14844
  function buildNextSteps() {
14639
14845
  return [
14640
- "1. Start the dashboard and open the setup wizard:",
14846
+ "1. Start the dashboard and open Page Health setup:",
14641
14847
  " canonry serve",
14642
- " \u2192 http://localhost:4100/setup",
14848
+ " \u2192 http://127.0.0.1:4100/setup",
14643
14849
  "",
14644
- " The wizard walks through project \u2192 queries \u2192 competitors \u2192 first sweep",
14645
- " in 5 guided steps and shows your first results inline.",
14850
+ " Map your public site and capture a persisted Page Health baseline.",
14851
+ " AI Visibility is optional and can be configured after Page Health.",
14646
14852
  " For remote/exposed hosts, complete dashboard password setup from loopback first.",
14647
14853
  "",
14648
14854
  "Prefer the terminal? The same flow as CLI commands:",
14649
14855
  "",
14650
14856
  " a. canonry project create my-site --domain example.com --country US --language en",
14651
- ' b. canonry query add my-site "best <category> for <use case>"',
14652
- " c. canonry run my-site --wait",
14653
- " d. canonry overview my-site",
14857
+ " b. canonry technical-aeo run my-site --max-pages 100 --wait --format json",
14858
+ " c. canonry technical-aeo score my-site --format json",
14859
+ " d. canonry technical-aeo pages my-site --sort score-asc --limit 10 --format jsonl",
14654
14860
  "",
14655
14861
  'Tip: "canonry doctor" verifies your setup before you start.'
14656
14862
  ];
@@ -14869,10 +15075,8 @@ function detectCanonryAgentPlugin(opts = {}) {
14869
15075
  }
14870
15076
 
14871
15077
  // src/commands/serve.ts
14872
- function resolveServePort(envPort, configPort) {
14873
- const trimmed = envPort?.trim();
14874
- if (trimmed) return parseInt(trimmed, 10);
14875
- return configPort ?? 4100;
15078
+ function shouldWarnAboutRemoteSetup(host) {
15079
+ return !isLoopbackBindHost(host);
14876
15080
  }
14877
15081
  async function serveCommand(format = "text") {
14878
15082
  const config = loadConfig();
@@ -14911,26 +15115,29 @@ async function serveCommand(format = "text") {
14911
15115
  codexHome: process.env.CODEX_HOME
14912
15116
  });
14913
15117
  const app = await createServer({ config, db, host, getAgentPluginState });
14914
- let shuttingDown = false;
14915
- const shutdown = (signal) => {
14916
- if (shuttingDown) return;
14917
- shuttingDown = true;
14918
- if (format === "text") {
14919
- console.log(`
14920
- Received ${signal}, stopping server...`);
14921
- }
14922
- closeWithIdleSweep(app).then(() => {
14923
- process.exit(0);
14924
- }).catch((err) => {
14925
- console.error("Error during shutdown:", err);
14926
- process.exit(1);
14927
- });
14928
- };
14929
- process.on("SIGTERM", () => shutdown("SIGTERM"));
14930
- process.on("SIGINT", () => shutdown("SIGINT"));
15118
+ let listening = false;
14931
15119
  try {
14932
15120
  await app.listen({ host, port });
14933
- const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
15121
+ await waitForServerRuntimeStartup(app);
15122
+ listening = true;
15123
+ let shuttingDown = false;
15124
+ const shutdown = (signal) => {
15125
+ if (shuttingDown) return;
15126
+ shuttingDown = true;
15127
+ if (format === "text") {
15128
+ console.log(`
15129
+ Received ${signal}, stopping server...`);
15130
+ }
15131
+ closeWithIdleSweep(app).then(() => {
15132
+ process.exit(0);
15133
+ }).catch((err) => {
15134
+ console.error("Error during shutdown:", err);
15135
+ process.exit(1);
15136
+ });
15137
+ };
15138
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
15139
+ process.on("SIGINT", () => shutdown("SIGINT"));
15140
+ const url = operatorHttpUrl(host, port);
14934
15141
  if (isMachineFormat(format)) {
14935
15142
  console.log(JSON.stringify({
14936
15143
  started: true,
@@ -14941,8 +15148,8 @@ Received ${signal}, stopping server...`);
14941
15148
  } else {
14942
15149
  console.log(`
14943
15150
  Canonry server running at ${url}`);
14944
- console.log(`Open ${url}/setup to finish onboarding and run your first visibility check.`);
14945
- if (host === "0.0.0.0") {
15151
+ console.log(`Open ${url}/setup to map your site and run your first Page Health scan.`);
15152
+ if (shouldWarnAboutRemoteSetup(host)) {
14946
15153
  console.log("First-run dashboard password setup is unauthenticated only on loopback; complete setup from this machine first or use a bearer cnry_... key.");
14947
15154
  }
14948
15155
  console.log("Press Ctrl+C to stop.\n");
@@ -14960,6 +15167,17 @@ Canonry server running at ${url}`);
14960
15167
  });
14961
15168
  } catch (err) {
14962
15169
  const message = describeError(err);
15170
+ if (listening) {
15171
+ process.stderr.write(`warning: server started but post-startup reporting failed: ${message}
15172
+ `);
15173
+ return;
15174
+ }
15175
+ try {
15176
+ await closeWithIdleSweep(app);
15177
+ } catch (closeErr) {
15178
+ process.stderr.write(`warning: failed to close server after startup error: ${describeError(closeErr)}
15179
+ `);
15180
+ }
14963
15181
  throw new CliError({
14964
15182
  code: "SERVE_START_FAILED",
14965
15183
  message: `Failed to start server: ${message}`,
@@ -15016,7 +15234,7 @@ function telemetryCommand(subcommand, format = "text") {
15016
15234
  console.log(JSON.stringify(payload, null, 2));
15017
15235
  return;
15018
15236
  }
15019
- console.log('Telemetry: enabled (no config yet \u2014 run "canonry init" first)');
15237
+ console.log('Telemetry: enabled (no config yet \u2014 run "canonry bootstrap" first)');
15020
15238
  return;
15021
15239
  }
15022
15240
  const config = loadConfig();
@@ -15041,8 +15259,8 @@ function telemetryCommand(subcommand, format = "text") {
15041
15259
  if (!configExists()) {
15042
15260
  throw new CliError({
15043
15261
  code: "CONFIG_REQUIRED",
15044
- message: 'No config found. Run "canonry init" first.',
15045
- displayMessage: 'No config found. Run "canonry init" first.',
15262
+ message: 'No config found. Run "canonry bootstrap" first.',
15263
+ displayMessage: 'No config found. Run "canonry bootstrap" first.',
15046
15264
  details: {
15047
15265
  command: "telemetry.enable"
15048
15266
  }
@@ -15065,8 +15283,8 @@ function telemetryCommand(subcommand, format = "text") {
15065
15283
  if (!configExists()) {
15066
15284
  throw new CliError({
15067
15285
  code: "CONFIG_REQUIRED",
15068
- message: 'No config found. Run "canonry init" first.',
15069
- displayMessage: 'No config found. Run "canonry init" first.',
15286
+ message: 'No config found. Run "canonry bootstrap" first.',
15287
+ displayMessage: 'No config found. Run "canonry bootstrap" first.',
15070
15288
  details: {
15071
15289
  command: "telemetry.disable"
15072
15290
  }
@@ -15156,14 +15374,16 @@ var SYSTEM_CLI_COMMANDS = [
15156
15374
  },
15157
15375
  {
15158
15376
  path: ["bootstrap"],
15159
- usage: "canonry bootstrap [--force] [--format json]",
15377
+ usage: "canonry bootstrap [--format json]",
15378
+ 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.",
15160
15379
  options: {
15380
+ // Compatibility for scripts written before bootstrap became idempotent.
15381
+ // Keep this out of usage: the flag is unnecessary and has no behavior.
15161
15382
  force: { type: "boolean", short: "f", default: false }
15162
15383
  },
15163
15384
  allowPositionals: false,
15164
15385
  run: async (input) => {
15165
15386
  await bootstrapCommand({
15166
- force: getBoolean(input.values, "force"),
15167
15387
  format: input.format
15168
15388
  });
15169
15389
  }
@@ -18784,17 +19004,21 @@ function buildSetupNudgeLine(input) {
18784
19004
  // src/cli.ts
18785
19005
  import { createRequire as createRequire2 } from "module";
18786
19006
  var USAGE6 = `
18787
- cnry \u2014 AEO monitoring CLI ('canonry' also works)
19007
+ cnry \u2014 Page Health and AI visibility CLI ('canonry' also works)
18788
19008
 
18789
19009
  Usage: cnry <command> [options]
18790
19010
 
18791
19011
  Setup:
18792
- init Initialize config and database
18793
- bootstrap Bootstrap config/database from env vars
19012
+ bootstrap Create local config/database (no provider required)
18794
19013
  serve Start the local server (foreground)
18795
19014
  start / stop Start/stop as a background daemon
19015
+ init Optional interactive provider/OAuth provisioning
18796
19016
  skills List or install bundled agent skills (claude/codex)
18797
19017
 
19018
+ Page Health:
19019
+ technical-aeo Crawl, score, and inspect site health evidence
19020
+ doctor Diagnose runtime and integration health
19021
+
18798
19022
  Projects:
18799
19023
  project Create, update, list, show, delete projects
18800
19024
  query Add, replace, remove, list, import, generate queries