@ainyc/canonry 4.180.6 → 4.180.8

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 (35) hide show
  1. package/README.md +4 -0
  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-Cwgbldst.js → AuditHistoryPanel-qiuqNGvG.js} +1 -1
  5. package/assets/assets/{BacklinksPage-h08vP0Of.js → BacklinksPage-D-g5f0Xv.js} +1 -1
  6. package/assets/assets/{HistoryPage-Dwyad0V0.js → HistoryPage-q8_pCuOs.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-DOyOHeiU.js → MeasurementPropertyPage-C_wMjUZe.js} +1 -1
  8. package/assets/assets/ProjectPage-33THfDtC.js +9 -0
  9. package/assets/assets/{RunRow-CVl2cqfG.js → RunRow-D5hatdlY.js} +1 -1
  10. package/assets/assets/{RunsPage-zzN2iQbk.js → RunsPage-Bj2snMM3.js} +1 -1
  11. package/assets/assets/SettingsPage-Cf5YgyJv.js +1 -0
  12. package/assets/assets/SiteHealthSection-D8RLvMAE.js +4 -0
  13. package/assets/assets/TrafficPage-CotJ14le.js +1 -0
  14. package/assets/assets/{TrafficSourceDetailPage-BnbNPkis.js → TrafficSourceDetailPage-BcxYX-uq.js} +1 -1
  15. package/assets/assets/{extract-error-message-Ctp1FqW1.js → extract-error-message-Bxt42scd.js} +1 -1
  16. package/assets/assets/index-BlILyZJ5.js +86 -0
  17. package/assets/assets/index-DXCQf5s1.css +1 -0
  18. package/assets/assets/{react-sigma_core.esm.min-DwnhdseV.js → react-sigma_core.esm.min-DxZINEmi.js} +1 -1
  19. package/assets/index.html +2 -2
  20. package/dist/{chunk-XYTSKRO3.js → chunk-G7M2KM7Q.js} +2 -2
  21. package/dist/{chunk-VZO6CLHY.js → chunk-GFVZOVG3.js} +105 -64
  22. package/dist/{chunk-RXXZ5BU5.js → chunk-GN2G7VFK.js} +24 -1
  23. package/dist/{chunk-CWXPBQR5.js → chunk-IM4HFNGF.js} +7 -5
  24. package/dist/{chunk-WJFBNCVD.js → chunk-ZJ2VGTYU.js} +197 -95
  25. package/dist/cli.js +187 -96
  26. package/dist/index.js +4 -4
  27. package/dist/{intelligence-service-MEPYXRBS.js → intelligence-service-IURLYRB3.js} +2 -2
  28. package/dist/mcp.js +3 -3
  29. package/package.json +10 -10
  30. package/assets/assets/ProjectPage-D9e_1Gpz.js +0 -9
  31. package/assets/assets/SettingsPage-qiQLOtpd.js +0 -1
  32. package/assets/assets/SiteHealthSection-R0MWiuZX.js +0 -4
  33. package/assets/assets/TrafficPage-CEA76iwx.js +0 -1
  34. package/assets/assets/index-BD8hYPvq.css +0 -1
  35. package/assets/assets/index-D-OGmIKF.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-GFVZOVG3.js";
30
32
  import {
31
33
  autoSyncSkills,
32
34
  formatAutoSyncNotice
33
- } from "./chunk-XYTSKRO3.js";
35
+ } from "./chunk-G7M2KM7Q.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-IM4HFNGF.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-ZJ2VGTYU.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-GN2G7VFK.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
  }
@@ -13866,6 +13871,7 @@ var CONTENT_CLI_COMMANDS = [
13866
13871
  // src/commands/bootstrap.ts
13867
13872
  import crypto from "crypto";
13868
13873
  import path9 from "path";
13874
+ import { isDeepStrictEqual } from "util";
13869
13875
  import { eq } from "drizzle-orm";
13870
13876
 
13871
13877
  // ../config/src/index.ts
@@ -14011,36 +14017,40 @@ function getBootstrapEnv(source, overrides) {
14011
14017
  }
14012
14018
 
14013
14019
  // 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."
14020
+ function persistedValue(value) {
14021
+ if (Array.isArray(value)) return value.map(persistedValue);
14022
+ if (value && typeof value === "object") {
14023
+ return Object.fromEntries(
14024
+ Object.entries(value).filter(([, entry]) => entry !== void 0).map(([key, entry]) => [key, persistedValue(entry)])
14022
14025
  );
14023
14026
  }
14027
+ return value;
14028
+ }
14029
+ async function bootstrapCommand(opts) {
14030
+ const format = opts?.format ?? "text";
14031
+ const env = getBootstrapEnv(process.env);
14032
+ const providers = env.providers;
14024
14033
  const configDir = getConfigDir();
14025
- const databasePath = env.databasePath || path9.join(configDir, "data.db");
14026
14034
  const existing = configExists();
14027
14035
  const existingConfig = existing ? loadConfig() : void 0;
14036
+ const existingRaw = existing ? loadConfigRaw() : null;
14037
+ const databasePath = env.databasePath || existingRaw?.database || path9.join(configDir, "data.db");
14028
14038
  let rawApiKey;
14029
14039
  let generatedApiKey;
14030
14040
  if (env.apiKey) {
14031
14041
  rawApiKey = env.apiKey;
14032
- } else if (existingConfig) {
14033
- rawApiKey = existingConfig.apiKey;
14042
+ } else if (existingRaw) {
14043
+ rawApiKey = existingRaw.apiKey;
14034
14044
  } else {
14035
14045
  generatedApiKey = `cnry_${crypto.randomBytes(16).toString("hex")}`;
14036
14046
  rawApiKey = generatedApiKey;
14037
14047
  }
14038
14048
  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;
14049
+ if (providers.gemini) mergedProviders.gemini = providers.gemini;
14050
+ if (providers.openai) mergedProviders.openai = providers.openai;
14051
+ if (providers.claude) mergedProviders.claude = providers.claude;
14052
+ if (providers.perplexity) mergedProviders.perplexity = providers.perplexity;
14053
+ if (providers.local) mergedProviders.local = providers.local;
14044
14054
  if (env.googleClientId && !env.googleClientSecret || !env.googleClientId && env.googleClientSecret) {
14045
14055
  console.warn("Warning: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must both be set to configure Google OAuth. Skipping Google auth config.");
14046
14056
  }
@@ -14051,25 +14061,42 @@ async function bootstrapCommand(_opts) {
14051
14061
  } : existingConfig?.google;
14052
14062
  const keyHash = crypto.createHash("sha256").update(rawApiKey).digest("hex");
14053
14063
  const keyPrefix = rawApiKey.slice(0, 9);
14054
- const existingConfigKeyHash = existingConfig ? crypto.createHash("sha256").update(existingConfig.apiKey).digest("hex") : void 0;
14064
+ const existingConfigKeyHash = existingRaw ? crypto.createHash("sha256").update(existingRaw.apiKey).digest("hex") : void 0;
14055
14065
  const db = createClient(databasePath);
14056
14066
  migrate(db);
14057
- db.transaction((tx) => {
14067
+ const keyChanged = db.transaction((tx) => {
14068
+ let changed = false;
14058
14069
  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();
14070
+ const existingDefaults = tx.select({
14071
+ id: apiKeys.id,
14072
+ keyHash: apiKeys.keyHash,
14073
+ keyPrefix: apiKeys.keyPrefix,
14074
+ scopes: apiKeys.scopes,
14075
+ projectId: apiKeys.projectId,
14076
+ lastUsedAt: apiKeys.lastUsedAt,
14077
+ revokedAt: apiKeys.revokedAt
14078
+ }).from(apiKeys).where(eq(apiKeys.name, "default")).all();
14060
14079
  const existingDefault = existingDefaults.find((key) => key.keyHash === existingConfigKeyHash) ?? existingDefaults.at(0);
14061
14080
  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();
14081
+ const rotating = existingDefault.keyHash !== keyHash;
14082
+ const needsUpdate = rotating || existingDefault.keyPrefix !== keyPrefix || !isDeepStrictEqual(existingDefault.scopes, ["*"]) || existingDefault.projectId !== null || existingDefault.revokedAt !== null;
14083
+ if (needsUpdate) {
14084
+ tx.update(apiKeys).set({
14085
+ keyHash,
14086
+ keyPrefix,
14087
+ scopes: ["*"],
14088
+ projectId: null,
14089
+ ...rotating ? { lastUsedAt: null } : {},
14090
+ revokedAt: null
14091
+ }).where(eq(apiKeys.id, existingDefault.id)).run();
14092
+ changed = true;
14093
+ }
14070
14094
  for (const duplicate of existingDefaults) {
14071
14095
  if (duplicate.id === existingDefault.id) continue;
14072
- tx.update(apiKeys).set({ revokedAt: rotatedAt }).where(eq(apiKeys.id, duplicate.id)).run();
14096
+ if (duplicate.revokedAt === null) {
14097
+ tx.update(apiKeys).set({ revokedAt: rotatedAt }).where(eq(apiKeys.id, duplicate.id)).run();
14098
+ changed = true;
14099
+ }
14073
14100
  }
14074
14101
  } else {
14075
14102
  tx.insert(apiKeys).values({
@@ -14080,29 +14107,41 @@ async function bootstrapCommand(_opts) {
14080
14107
  scopes: ["*"],
14081
14108
  createdAt: rotatedAt
14082
14109
  }).run();
14110
+ changed = true;
14083
14111
  }
14112
+ return changed;
14084
14113
  });
14085
- saveConfig({
14086
- apiUrl: env.apiUrl || existingConfig?.apiUrl || `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
14114
+ const apiUrl = env.apiUrl || existingRaw?.apiUrl || `http://127.0.0.1:${process.env.CANONRY_PORT || "4100"}`;
14115
+ const nextConfig = {
14116
+ ...existingRaw,
14117
+ apiUrl,
14087
14118
  database: databasePath,
14088
14119
  apiKey: rawApiKey,
14089
14120
  providers: mergedProviders,
14090
14121
  google: mergedGoogle
14091
- });
14122
+ };
14123
+ const configChanged = !existingRaw || existingRaw.apiUrl !== apiUrl || existingRaw.database !== databasePath || existingRaw.apiKey !== rawApiKey || !isDeepStrictEqual(persistedValue(existingRaw.providers ?? {}), persistedValue(mergedProviders)) || !isDeepStrictEqual(persistedValue(existingRaw.google), persistedValue(mergedGoogle));
14124
+ if (configChanged) saveConfig(nextConfig);
14125
+ const status = !existing ? "created" : configChanged || keyChanged ? "updated" : "unchanged";
14092
14126
  if (format === "json") {
14093
14127
  console.log(JSON.stringify({
14094
14128
  bootstrapped: true,
14129
+ status,
14130
+ changed: status !== "unchanged",
14095
14131
  configPath: getConfigPath(),
14096
14132
  databasePath,
14097
- apiUrl: env.apiUrl || existingConfig?.apiUrl || `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
14098
- providers: Object.keys(mergedProviders ?? {}),
14133
+ apiUrl,
14134
+ providers: Object.keys(mergedProviders),
14099
14135
  googleConfigured: !!mergedGoogle,
14100
14136
  generatedApiKey
14101
14137
  }, null, 2));
14102
14138
  return;
14103
14139
  }
14104
- console.log(`Bootstrap complete. Config saved to ${getConfigPath()}`);
14140
+ console.log(`Bootstrap ${status}. Config: ${getConfigPath()}`);
14105
14141
  console.log(`SQLite database path: ${databasePath}`);
14142
+ if (Object.keys(mergedProviders).length === 0) {
14143
+ console.log("Providers: none (Page Health works now; add one later to enable AI Visibility).");
14144
+ }
14106
14145
  if (generatedApiKey) {
14107
14146
  console.log(`API key: ${generatedApiKey}`);
14108
14147
  }
@@ -14112,6 +14151,27 @@ async function bootstrapCommand(_opts) {
14112
14151
  import { spawn as spawn2 } from "child_process";
14113
14152
  import fs15 from "fs";
14114
14153
  import path10 from "path";
14154
+
14155
+ // src/operator-url.ts
14156
+ function operatorHost(bindHost) {
14157
+ if (bindHost === "0.0.0.0") return "127.0.0.1";
14158
+ if (bindHost === "::" || bindHost === "[::]") return "::1";
14159
+ return bindHost;
14160
+ }
14161
+ function operatorHttpUrl(bindHost, port) {
14162
+ const host = operatorHost(bindHost);
14163
+ const bare = host.replace(/^\[|\]$/g, "");
14164
+ return `http://${bare.includes(":") ? `[${bare}]` : bare}:${port}`;
14165
+ }
14166
+
14167
+ // src/serve-endpoint.ts
14168
+ function resolveServePort(envPort, configPort) {
14169
+ const trimmed = envPort?.trim();
14170
+ if (trimmed) return parseInt(trimmed, 10);
14171
+ return configPort ?? 4100;
14172
+ }
14173
+
14174
+ // src/commands/daemon.ts
14115
14175
  function getPidPath() {
14116
14176
  return path10.join(getConfigDir(), "canonry.pid");
14117
14177
  }
@@ -14125,7 +14185,7 @@ function isProcessAlive(pid) {
14125
14185
  }
14126
14186
  }
14127
14187
  async function waitForReady(host, port, maxMs = 1e4) {
14128
- const url = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}/health`;
14188
+ const url = `${operatorHttpUrl(host, port)}/health`;
14129
14189
  const deadline = Date.now() + maxMs;
14130
14190
  while (Date.now() < deadline) {
14131
14191
  try {
@@ -14137,6 +14197,13 @@ async function waitForReady(host, port, maxMs = 1e4) {
14137
14197
  }
14138
14198
  return false;
14139
14199
  }
14200
+ function resolveDaemonEndpoint(opts, env, configPort) {
14201
+ const portInput = opts.port || env.CANONRY_PORT;
14202
+ return {
14203
+ host: opts.host || env.CANONRY_HOST || "127.0.0.1",
14204
+ port: resolveServePort(portInput, configPort)
14205
+ };
14206
+ }
14140
14207
  function buildServeForwardArgs(opts) {
14141
14208
  const args = [];
14142
14209
  if (opts.port) args.push("--port", opts.port);
@@ -14167,10 +14234,14 @@ async function startDaemon(opts) {
14167
14234
  }
14168
14235
  fs15.unlinkSync(pidPath);
14169
14236
  }
14237
+ const config = loadConfig();
14238
+ const endpoint = resolveDaemonEndpoint(opts, process.env, config.port);
14239
+ const host = endpoint.host;
14240
+ const port = String(endpoint.port);
14170
14241
  const cliPath = path10.resolve(new URL(import.meta.url).pathname);
14171
14242
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
14172
14243
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
14173
- args.push(...buildServeForwardArgs(opts));
14244
+ args.push(...buildServeForwardArgs({ ...opts, host, port }));
14174
14245
  const child = spawn2(process.execPath, args, {
14175
14246
  detached: true,
14176
14247
  stdio: "ignore"
@@ -14188,13 +14259,15 @@ async function startDaemon(opts) {
14188
14259
  fs15.mkdirSync(configDir, { recursive: true });
14189
14260
  }
14190
14261
  fs15.writeFileSync(pidPath, String(child.pid), "utf-8");
14191
- const port = opts.port ?? "4100";
14192
- const host = opts.host ?? "127.0.0.1";
14193
14262
  if (!isMachineFormat(format)) {
14194
14263
  process.stderr.write("Waiting for server to start...");
14195
14264
  }
14196
14265
  const ready = await waitForReady(host, port);
14197
14266
  if (!ready) {
14267
+ try {
14268
+ child.kill("SIGTERM");
14269
+ } catch {
14270
+ }
14198
14271
  try {
14199
14272
  fs15.unlinkSync(pidPath);
14200
14273
  } catch {
@@ -14213,7 +14286,7 @@ async function startDaemon(opts) {
14213
14286
  if (!isMachineFormat(format)) {
14214
14287
  process.stderr.write("\n");
14215
14288
  }
14216
- const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
14289
+ const url = operatorHttpUrl(host, port);
14217
14290
  if (isMachineFormat(format)) {
14218
14291
  console.log(JSON.stringify({
14219
14292
  started: true,
@@ -14490,7 +14563,7 @@ async function initCommand(opts) {
14490
14563
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14491
14564
  }).run();
14492
14565
  saveConfig({
14493
- apiUrl: `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
14566
+ apiUrl: `http://127.0.0.1:${process.env.CANONRY_PORT || "4100"}`,
14494
14567
  database: databasePath,
14495
14568
  apiKey: rawApiKey,
14496
14569
  providers,
@@ -14537,7 +14610,7 @@ async function initCommand(opts) {
14537
14610
  initialized: true,
14538
14611
  configPath: getConfigPath(),
14539
14612
  databasePath,
14540
- apiUrl: `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
14613
+ apiUrl: `http://127.0.0.1:${process.env.CANONRY_PORT || "4100"}`,
14541
14614
  apiKey: rawApiKey,
14542
14615
  providers: providerNames,
14543
14616
  googleConfigured: !!google,
@@ -14594,7 +14667,7 @@ ${mcpTip}`);
14594
14667
  }
14595
14668
  if (!isMachineFormat(format)) {
14596
14669
  showFirstRunNotice();
14597
- console.log("\nNext: canonry serve to open the dashboard, or canonry settings provider gemini --api-key <key> to configure Gemini.");
14670
+ console.log("\nNext: canonry serve to map your site and capture a Page Health baseline.");
14598
14671
  console.log("\nNext steps:");
14599
14672
  for (const line of nextSteps) {
14600
14673
  console.log(` ${line}`);
@@ -14630,27 +14703,27 @@ ${mcpTip}`);
14630
14703
  const answer = await prompt("\nStart the dashboard now? [Y/n]: ");
14631
14704
  if (!/^n/i.test(answer.trim())) {
14632
14705
  pendingServeHandoff = true;
14633
- console.log("Handing off to the dashboard. Finish setup at /setup once it is up.");
14706
+ console.log("Handing off to the dashboard. Map your site at /setup once it is up.");
14634
14707
  }
14635
14708
  }
14636
14709
  return agentLLM;
14637
14710
  }
14638
14711
  function buildNextSteps() {
14639
14712
  return [
14640
- "1. Start the dashboard and open the setup wizard:",
14713
+ "1. Start the dashboard and open Page Health setup:",
14641
14714
  " canonry serve",
14642
- " \u2192 http://localhost:4100/setup",
14715
+ " \u2192 http://127.0.0.1:4100/setup",
14643
14716
  "",
14644
- " The wizard walks through project \u2192 queries \u2192 competitors \u2192 first sweep",
14645
- " in 5 guided steps and shows your first results inline.",
14717
+ " Map your public site and capture a persisted Page Health baseline.",
14718
+ " AI Visibility is optional and can be configured after Page Health.",
14646
14719
  " For remote/exposed hosts, complete dashboard password setup from loopback first.",
14647
14720
  "",
14648
14721
  "Prefer the terminal? The same flow as CLI commands:",
14649
14722
  "",
14650
14723
  " 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",
14724
+ " b. canonry technical-aeo run my-site --max-pages 100 --wait --format json",
14725
+ " c. canonry technical-aeo score my-site --format json",
14726
+ " d. canonry technical-aeo pages my-site --sort score-asc --limit 10 --format jsonl",
14654
14727
  "",
14655
14728
  'Tip: "canonry doctor" verifies your setup before you start.'
14656
14729
  ];
@@ -14869,10 +14942,8 @@ function detectCanonryAgentPlugin(opts = {}) {
14869
14942
  }
14870
14943
 
14871
14944
  // 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;
14945
+ function shouldWarnAboutRemoteSetup(host) {
14946
+ return !isLoopbackBindHost(host);
14876
14947
  }
14877
14948
  async function serveCommand(format = "text") {
14878
14949
  const config = loadConfig();
@@ -14911,26 +14982,29 @@ async function serveCommand(format = "text") {
14911
14982
  codexHome: process.env.CODEX_HOME
14912
14983
  });
14913
14984
  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"));
14985
+ let listening = false;
14931
14986
  try {
14932
14987
  await app.listen({ host, port });
14933
- const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
14988
+ await waitForServerRuntimeStartup(app);
14989
+ listening = true;
14990
+ let shuttingDown = false;
14991
+ const shutdown = (signal) => {
14992
+ if (shuttingDown) return;
14993
+ shuttingDown = true;
14994
+ if (format === "text") {
14995
+ console.log(`
14996
+ Received ${signal}, stopping server...`);
14997
+ }
14998
+ closeWithIdleSweep(app).then(() => {
14999
+ process.exit(0);
15000
+ }).catch((err) => {
15001
+ console.error("Error during shutdown:", err);
15002
+ process.exit(1);
15003
+ });
15004
+ };
15005
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
15006
+ process.on("SIGINT", () => shutdown("SIGINT"));
15007
+ const url = operatorHttpUrl(host, port);
14934
15008
  if (isMachineFormat(format)) {
14935
15009
  console.log(JSON.stringify({
14936
15010
  started: true,
@@ -14941,8 +15015,8 @@ Received ${signal}, stopping server...`);
14941
15015
  } else {
14942
15016
  console.log(`
14943
15017
  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") {
15018
+ console.log(`Open ${url}/setup to map your site and run your first Page Health scan.`);
15019
+ if (shouldWarnAboutRemoteSetup(host)) {
14946
15020
  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
15021
  }
14948
15022
  console.log("Press Ctrl+C to stop.\n");
@@ -14960,6 +15034,17 @@ Canonry server running at ${url}`);
14960
15034
  });
14961
15035
  } catch (err) {
14962
15036
  const message = describeError(err);
15037
+ if (listening) {
15038
+ process.stderr.write(`warning: server started but post-startup reporting failed: ${message}
15039
+ `);
15040
+ return;
15041
+ }
15042
+ try {
15043
+ await closeWithIdleSweep(app);
15044
+ } catch (closeErr) {
15045
+ process.stderr.write(`warning: failed to close server after startup error: ${describeError(closeErr)}
15046
+ `);
15047
+ }
14963
15048
  throw new CliError({
14964
15049
  code: "SERVE_START_FAILED",
14965
15050
  message: `Failed to start server: ${message}`,
@@ -15016,7 +15101,7 @@ function telemetryCommand(subcommand, format = "text") {
15016
15101
  console.log(JSON.stringify(payload, null, 2));
15017
15102
  return;
15018
15103
  }
15019
- console.log('Telemetry: enabled (no config yet \u2014 run "canonry init" first)');
15104
+ console.log('Telemetry: enabled (no config yet \u2014 run "canonry bootstrap" first)');
15020
15105
  return;
15021
15106
  }
15022
15107
  const config = loadConfig();
@@ -15041,8 +15126,8 @@ function telemetryCommand(subcommand, format = "text") {
15041
15126
  if (!configExists()) {
15042
15127
  throw new CliError({
15043
15128
  code: "CONFIG_REQUIRED",
15044
- message: 'No config found. Run "canonry init" first.',
15045
- displayMessage: 'No config found. Run "canonry init" first.',
15129
+ message: 'No config found. Run "canonry bootstrap" first.',
15130
+ displayMessage: 'No config found. Run "canonry bootstrap" first.',
15046
15131
  details: {
15047
15132
  command: "telemetry.enable"
15048
15133
  }
@@ -15065,8 +15150,8 @@ function telemetryCommand(subcommand, format = "text") {
15065
15150
  if (!configExists()) {
15066
15151
  throw new CliError({
15067
15152
  code: "CONFIG_REQUIRED",
15068
- message: 'No config found. Run "canonry init" first.',
15069
- displayMessage: 'No config found. Run "canonry init" first.',
15153
+ message: 'No config found. Run "canonry bootstrap" first.',
15154
+ displayMessage: 'No config found. Run "canonry bootstrap" first.',
15070
15155
  details: {
15071
15156
  command: "telemetry.disable"
15072
15157
  }
@@ -15156,14 +15241,16 @@ var SYSTEM_CLI_COMMANDS = [
15156
15241
  },
15157
15242
  {
15158
15243
  path: ["bootstrap"],
15159
- usage: "canonry bootstrap [--force] [--format json]",
15244
+ usage: "canonry bootstrap [--format json]",
15245
+ 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
15246
  options: {
15247
+ // Compatibility for scripts written before bootstrap became idempotent.
15248
+ // Keep this out of usage: the flag is unnecessary and has no behavior.
15161
15249
  force: { type: "boolean", short: "f", default: false }
15162
15250
  },
15163
15251
  allowPositionals: false,
15164
15252
  run: async (input) => {
15165
15253
  await bootstrapCommand({
15166
- force: getBoolean(input.values, "force"),
15167
15254
  format: input.format
15168
15255
  });
15169
15256
  }
@@ -18784,17 +18871,21 @@ function buildSetupNudgeLine(input) {
18784
18871
  // src/cli.ts
18785
18872
  import { createRequire as createRequire2 } from "module";
18786
18873
  var USAGE6 = `
18787
- cnry \u2014 AEO monitoring CLI ('canonry' also works)
18874
+ cnry \u2014 Page Health and AI visibility CLI ('canonry' also works)
18788
18875
 
18789
18876
  Usage: cnry <command> [options]
18790
18877
 
18791
18878
  Setup:
18792
- init Initialize config and database
18793
- bootstrap Bootstrap config/database from env vars
18879
+ bootstrap Create local config/database (no provider required)
18794
18880
  serve Start the local server (foreground)
18795
18881
  start / stop Start/stop as a background daemon
18882
+ init Optional interactive provider/OAuth provisioning
18796
18883
  skills List or install bundled agent skills (claude/codex)
18797
18884
 
18885
+ Page Health:
18886
+ technical-aeo Crawl, score, and inspect site health evidence
18887
+ doctor Diagnose runtime and integration health
18888
+
18798
18889
  Projects:
18799
18890
  project Create, update, list, show, delete projects
18800
18891
  query Add, replace, remove, list, import, generate queries
package/dist/index.js CHANGED
@@ -3,12 +3,12 @@ import {
3
3
  createGoogleMarketingCredentialStore,
4
4
  createGoogleMarketingRuntime,
5
5
  createServer
6
- } from "./chunk-VZO6CLHY.js";
6
+ } from "./chunk-GFVZOVG3.js";
7
7
  import {
8
8
  loadConfig
9
- } from "./chunk-CWXPBQR5.js";
10
- import "./chunk-WJFBNCVD.js";
11
- import "./chunk-RXXZ5BU5.js";
9
+ } from "./chunk-IM4HFNGF.js";
10
+ import "./chunk-ZJ2VGTYU.js";
11
+ import "./chunk-GN2G7VFK.js";
12
12
  export {
13
13
  GoogleMarketingRuntimeError,
14
14
  createGoogleMarketingCredentialStore,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-WJFBNCVD.js";
4
- import "./chunk-RXXZ5BU5.js";
3
+ } from "./chunk-ZJ2VGTYU.js";
4
+ import "./chunk-GN2G7VFK.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  autoSyncSkills
3
- } from "./chunk-XYTSKRO3.js";
3
+ } from "./chunk-G7M2KM7Q.js";
4
4
  import {
5
5
  createApiClient,
6
6
  createCanonryMcpServer
7
- } from "./chunk-CWXPBQR5.js";
7
+ } from "./chunk-IM4HFNGF.js";
8
8
  import {
9
9
  isReadOnlyKey
10
- } from "./chunk-RXXZ5BU5.js";
10
+ } from "./chunk-GN2G7VFK.js";
11
11
 
12
12
  // src/mcp/cli.ts
13
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "4.180.6",
3
+ "version": "4.180.8",
4
4
  "type": "module",
5
5
  "description": "Self-hosted AI visibility (AEO) platform: track how ChatGPT, Claude, Gemini, and Perplexity cite your domain, join it with Search Console, GA4, server-side traffic, and paid media, and fix what you find through agent tools (CLI, REST, MCP). Local SQLite.",
6
6
  "license": "FSL-1.1-ALv2",
@@ -71,30 +71,30 @@
71
71
  "tsup": "^8.5.1",
72
72
  "tsx": "^4.19.0",
73
73
  "@ainyc/canonry-api-client": "0.0.0",
74
- "@ainyc/canonry-config": "0.0.0",
75
74
  "@ainyc/canonry-api-routes": "0.0.0",
76
- "@ainyc/canonry-contracts": "0.0.0",
75
+ "@ainyc/canonry-config": "0.0.0",
77
76
  "@ainyc/canonry-db": "0.0.0",
77
+ "@ainyc/canonry-contracts": "0.0.0",
78
78
  "@ainyc/canonry-integration-bing": "0.0.0",
79
79
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
80
- "@ainyc/canonry-integration-cloudflare-worker": "0.0.0",
81
80
  "@ainyc/canonry-integration-cloudflare-queue": "0.0.0",
81
+ "@ainyc/canonry-integration-cloudflare-worker": "0.0.0",
82
82
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
83
83
  "@ainyc/canonry-integration-google": "0.0.0",
84
- "@ainyc/canonry-integration-google-business-profile": "0.0.0",
85
- "@ainyc/canonry-integration-google-ads": "0.0.0",
86
84
  "@ainyc/canonry-integration-google-places": "0.0.0",
85
+ "@ainyc/canonry-integration-google-ads": "0.0.0",
87
86
  "@ainyc/canonry-integration-google-tag-manager": "0.0.0",
87
+ "@ainyc/canonry-integration-wordpress": "0.0.0",
88
88
  "@ainyc/canonry-integration-openai-ads": "0.0.0",
89
+ "@ainyc/canonry-integration-google-business-profile": "0.0.0",
89
90
  "@ainyc/canonry-integration-traffic": "0.0.0",
90
- "@ainyc/canonry-integration-wordpress": "0.0.0",
91
+ "@ainyc/canonry-intelligence": "0.0.0",
91
92
  "@ainyc/canonry-provider-cdp": "0.0.0",
92
93
  "@ainyc/canonry-provider-claude": "0.0.0",
93
94
  "@ainyc/canonry-provider-gemini": "0.0.0",
94
- "@ainyc/canonry-intelligence": "0.0.0",
95
- "@ainyc/canonry-provider-openai": "0.0.0",
95
+ "@ainyc/canonry-provider-local": "0.0.0",
96
96
  "@ainyc/canonry-provider-perplexity": "0.0.0",
97
- "@ainyc/canonry-provider-local": "0.0.0"
97
+ "@ainyc/canonry-provider-openai": "0.0.0"
98
98
  },
99
99
  "scripts": {
100
100
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",