@ainyc/canonry 5.1.2 → 5.1.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.
@@ -1258,6 +1258,8 @@ Every command takes `--format`:
1258
1258
  - **`json`** — one pretty-printed JSON document (the full envelope). Stable contract.
1259
1259
  - **`jsonl`** — newline-delimited JSON: the command's **primary collection**, one self-contained record per line. The agent-friendly machine format — no envelope key to guess (`.checks` vs `.results` vs `.rows`), no `jq` flattening, greppable line by line.
1260
1260
 
1261
+ **Update notice.** When a newer canonry is published, the CLI writes one line to **stderr** before the command runs (stdout is never touched): `[canonry] UPDATE_AVAILABLE: ...` in text mode, or `{"notice":{"code":"UPDATE_AVAILABLE","current":"…","latest":"…","installMethod":"npm|homebrew|docker","upgradeCommand":"…","url":"…"}}` with `--format json|jsonl`. Tell the operator, or run the `upgradeCommand` (it already matches how canonry was installed) and restart `canonry serve`. `cnry doctor --check canonry.version.current` reports the same for the running server (`version.outdated` warns). Silence with `CANONRY_DISABLE_UPDATE_CHECK=1`. Over MCP the same notice arrives in the initialize instructions and as `updateAvailable` in `canonry_help`.
1262
+
1261
1263
  `jsonl` is supported by every **collection** command — one whose primary output is a list: `insights`, `runs`, `evidence`, `history`, `query/keyword/competitor list`, `notify list/events`, `google` reads (`performance`, `performance-daily`, `inspections`, `coverage-history`, `deindexed`, `status`, `properties`, `list-sitemaps`), `bing` reads (`coverage-history`, `inspections`, `performance`, `sites`), `ga` reads (`ai-referral-daily`, `ai-referral-history`, `social-referral-history`, `session-history`, `coverage`), `google-ads` customer/snapshot reads, `gtm` account/container/workspace/snapshot reads, conversion-tracking contracts and integrity findings, `ads geo search` and `ads conversions` reads, `traffic events/sources/status`, `discover list/show`, `content targets/sources/gaps/map`, `backlinks list/releases`, `project list/locations`, `key list`, `agent memory list`, `agent providers`, `sources` (streams the ranked cited-domain list), and `doctor`. (`content brief` is an object command — `jsonl` degrades to its JSON document.)
1262
1264
 
1263
1265
  Each `jsonl` line re-injects the envelope context it would otherwise lose, so a line lifted out still self-describes:
@@ -17,7 +17,7 @@ import {
17
17
  resolveOperatorApiKeyIds,
18
18
  runWithUsageTags,
19
19
  saveConfigPatch
20
- } from "./chunk-5K2NUQOV.js";
20
+ } from "./chunk-AEE5DGTE.js";
21
21
  import {
22
22
  CC_CACHE_DIR,
23
23
  DUCKDB_SPEC,
@@ -168,7 +168,7 @@ import {
168
168
  siteCrawlSnapshots,
169
169
  toAlertView,
170
170
  usageCounters
171
- } from "./chunk-WSYJ4IT7.js";
171
+ } from "./chunk-EBUX3C5H.js";
172
172
  import {
173
173
  AGENT_MEMORY_VALUE_MAX_BYTES,
174
174
  AGENT_NONE,
@@ -179,6 +179,7 @@ import {
179
179
  AdsCampaignBiddingTypes,
180
180
  AgentProviderIds,
181
181
  BacklinkSources,
182
+ CANONRY_NPM_PACKAGE_URL,
182
183
  CITED_URL_CAPTURE_VERSION,
183
184
  CcReleaseSyncStatuses,
184
185
  DEFAULT_VIEWER_RESEARCH_DAILY_RUN_LIMIT,
@@ -239,6 +240,7 @@ import {
239
240
  classifyProviderErrorMessages,
240
241
  classifyTemplateLinkEdge,
241
242
  cloudflareQueueNameSchema,
243
+ compareSemver,
242
244
  computeBacklinkSummaryMetrics,
243
245
  contentActionLabel,
244
246
  contentBriefDtoSchema,
@@ -279,9 +281,11 @@ import {
279
281
  isAgentProviderId,
280
282
  isBrowserProvider,
281
283
  isGhostTelemetryEvent,
284
+ isInstallMethod,
282
285
  isReadOnlyKey,
283
286
  isRetryableHttpError,
284
287
  isSelfLink,
288
+ isStrictSemver,
285
289
  isVertexGroundingRedirect,
286
290
  mapWithConcurrency,
287
291
  maskTelemetryAnonymousId,
@@ -313,11 +317,14 @@ import {
313
317
  templateLinkUbiquityAvailable,
314
318
  textContainsBrandAlias,
315
319
  textContainsDomain,
320
+ updateCheckEnvOptOut,
321
+ upgradeCaveatFor,
322
+ upgradeCommandFor,
316
323
  usageSurfaceSchema,
317
324
  validationError,
318
325
  winnabilityClassLabel,
319
326
  withRetry
320
- } from "./chunk-5ZIUROAQ.js";
327
+ } from "./chunk-MMSPU72Z.js";
321
328
 
322
329
  // src/runtime-context.ts
323
330
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -569,47 +576,46 @@ function deliverEvent(event, properties, options, delivery = {}) {
569
576
  }
570
577
 
571
578
  // src/update-check.ts
579
+ import fs from "fs";
572
580
  import { createRequire as createRequire2 } from "module";
581
+ import { fileURLToPath } from "url";
573
582
  var _require2 = createRequire2(import.meta.url);
574
583
  var { version: PKG_VERSION } = _require2("../package.json");
575
584
  var PKG_NAME = "@canonry/canonry";
576
585
  var NPM_DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PKG_NAME}/dist-tags`;
577
- var NPM_PACKAGE_URL = `https://www.npmjs.com/package/${PKG_NAME}`;
586
+ var NPM_PACKAGE_URL = CANONRY_NPM_PACKAGE_URL;
578
587
  var FETCH_TIMEOUT_MS = 1500;
579
- function isUpdateCheckEnabled() {
580
- if (process.env.CANONRY_DISABLE_UPDATE_CHECK === "1") return false;
581
- if (process.env.DO_NOT_TRACK === "1") return false;
582
- if (process.env.CI) return false;
583
- if (!configExists()) return true;
588
+ var CONTAINER_MARKERS = ["/.dockerenv", "/run/.containerenv"];
589
+ function detectInstallMethod(opts) {
590
+ const declared = (opts?.env ?? process.env).CANONRY_INSTALL_METHOD;
591
+ if (isInstallMethod(declared)) return declared;
592
+ let modulePath = opts?.modulePath ?? fileURLToPath(import.meta.url);
584
593
  try {
585
- const raw = loadConfigRaw();
586
- return raw?.updateCheck !== false;
594
+ modulePath = fs.realpathSync(modulePath);
587
595
  } catch {
588
- return true;
589
596
  }
597
+ if (modulePath.replace(/\\/g, "/").includes("/Cellar/canonry/")) return "homebrew";
598
+ const exists = opts?.exists ?? fs.existsSync;
599
+ if (CONTAINER_MARKERS.some((marker) => exists(marker))) return "docker";
600
+ return "npm";
590
601
  }
591
- function compareSemver(a, b) {
592
- const parse = (v) => {
593
- const core = v.split(/[-+]/)[0];
594
- if (!core) return null;
595
- const parts = core.split(".");
596
- if (parts.length < 3) return null;
597
- const nums = [];
598
- for (let i = 0; i < 3; i++) {
599
- const n = Number(parts[i]);
600
- if (!Number.isInteger(n) || n < 0) return null;
601
- nums.push(n);
602
- }
603
- return [nums[0], nums[1], nums[2]];
604
- };
605
- const pa = parse(a);
606
- const pb = parse(b);
607
- if (!pa || !pb) return 0;
608
- for (let i = 0; i < 3; i++) {
609
- if (pa[i] > pb[i]) return 1;
610
- if (pa[i] < pb[i]) return -1;
602
+ var detectedInstallMethod;
603
+ function currentInstallMethod() {
604
+ detectedInstallMethod ??= detectInstallMethod();
605
+ return detectedInstallMethod;
606
+ }
607
+ function isUpdateCheckEnabled() {
608
+ return updateCheckDisabledReason() === null;
609
+ }
610
+ function updateCheckDisabledReason() {
611
+ const envOptOut = updateCheckEnvOptOut(process.env);
612
+ if (envOptOut) return envOptOut;
613
+ if (!configExists()) return null;
614
+ try {
615
+ return loadConfigRaw()?.updateCheck === false ? "config" : null;
616
+ } catch {
617
+ return null;
611
618
  }
612
- return 0;
613
619
  }
614
620
  async function fetchLatestVersion(opts) {
615
621
  const controller = new AbortController();
@@ -622,7 +628,7 @@ async function fetchLatestVersion(opts) {
622
628
  });
623
629
  if (!res.ok) return null;
624
630
  const data = await res.json();
625
- if (typeof data.latest !== "string") return null;
631
+ if (typeof data.latest !== "string" || !isStrictSemver(data.latest)) return null;
626
632
  return data.latest;
627
633
  } catch {
628
634
  return null;
@@ -630,13 +636,15 @@ async function fetchLatestVersion(opts) {
630
636
  clearTimeout(timeout);
631
637
  }
632
638
  }
633
- function buildUpdateAvailable(current, latest) {
639
+ function buildUpdateAvailable(current, latest, installMethod = currentInstallMethod()) {
640
+ if (!isStrictSemver(latest)) return null;
634
641
  if (compareSemver(latest, current) <= 0) return null;
635
642
  return {
636
643
  current,
637
644
  latest,
638
645
  url: NPM_PACKAGE_URL,
639
- upgradeCommand: `npm install -g ${PKG_NAME}`
646
+ upgradeCommand: upgradeCommandFor(installMethod),
647
+ installMethod
640
648
  };
641
649
  }
642
650
  async function checkLatestVersionForCli(opts) {
@@ -674,6 +682,45 @@ async function checkLatestVersionForCli(opts) {
674
682
  }
675
683
  return buildUpdateAvailable(PKG_VERSION, latest);
676
684
  }
685
+ function readCachedUpdateAvailable() {
686
+ if (!isUpdateCheckEnabled()) return null;
687
+ if (!configExists()) return null;
688
+ try {
689
+ const cachedLatest = loadConfigRaw()?.lastKnownLatestVersion;
690
+ if (typeof cachedLatest !== "string") return null;
691
+ return buildUpdateAvailable(PKG_VERSION, cachedLatest);
692
+ } catch {
693
+ return null;
694
+ }
695
+ }
696
+ var UPDATE_AVAILABLE_NOTICE_CODE = "UPDATE_AVAILABLE";
697
+ function formatUpdateNotice(update, opts) {
698
+ const caveat = upgradeCaveatFor(update.installMethod);
699
+ if (opts.format === "json" || opts.format === "jsonl") {
700
+ return `${JSON.stringify({
701
+ notice: {
702
+ code: UPDATE_AVAILABLE_NOTICE_CODE,
703
+ current: update.current,
704
+ latest: update.latest,
705
+ installMethod: update.installMethod,
706
+ upgradeCommand: update.upgradeCommand,
707
+ url: update.url,
708
+ ...caveat ? { note: caveat } : {}
709
+ }
710
+ })}
711
+ `;
712
+ }
713
+ if (opts.interactive) {
714
+ return `
715
+ \u2192 canonry ${update.latest} is available (you have ${update.current}).
716
+ Upgrade: ${update.upgradeCommand}
717
+ ` + (caveat ? ` ${caveat}
718
+ ` : "") + "\n";
719
+ }
720
+ const upgrade = update.installMethod === "docker" ? `Upgrade: ${update.upgradeCommand}.` : `Upgrade with \`${update.upgradeCommand}\`, then restart any running \`canonry serve\`.`;
721
+ return `[canonry] ${UPDATE_AVAILABLE_NOTICE_CODE}: canonry ${update.latest} is available (installed ${update.current}). ${upgrade} ${caveat ? `${caveat} ` : ""}Silence with CANONRY_DISABLE_UPDATE_CHECK=1.
722
+ `;
723
+ }
677
724
  var memoryCache = null;
678
725
  var inFlight = null;
679
726
  function startBackgroundRefresh(getNow) {
@@ -700,6 +747,22 @@ function checkLatestVersionForServer(opts) {
700
747
  if (!memoryCache || !memoryCache.latest) return null;
701
748
  return buildUpdateAvailable(PKG_VERSION, memoryCache.latest);
702
749
  }
750
+ function getServerUpdateStatus(opts) {
751
+ const installMethod = currentInstallMethod();
752
+ const base = { current: PKG_VERSION, installMethod, upgradeCommand: upgradeCommandFor(installMethod), url: NPM_PACKAGE_URL };
753
+ const disabledBy = updateCheckDisabledReason();
754
+ if (disabledBy) return { ...base, enabled: false, disabledBy, latest: null };
755
+ checkLatestVersionForServer(opts);
756
+ let latest = memoryCache?.latest ?? null;
757
+ if (!latest && configExists()) {
758
+ try {
759
+ const cached = loadConfigRaw()?.lastKnownLatestVersion;
760
+ if (typeof cached === "string") latest = cached;
761
+ } catch {
762
+ }
763
+ }
764
+ return { ...base, enabled: true, latest: latest && isStrictSemver(latest) ? latest : null };
765
+ }
703
766
 
704
767
  // src/google-marketing-runtime.ts
705
768
  import crypto3 from "crypto";
@@ -3470,9 +3533,9 @@ function createGoogleMarketingRuntime(options) {
3470
3533
  // src/server.ts
3471
3534
  import { createRequire as createRequire4 } from "module";
3472
3535
  import crypto27 from "crypto";
3473
- import fs7 from "fs";
3536
+ import fs8 from "fs";
3474
3537
  import path8 from "path";
3475
- import { fileURLToPath as fileURLToPath2 } from "url";
3538
+ import { fileURLToPath as fileURLToPath3 } from "url";
3476
3539
  import { and as and20, eq as eq26 } from "drizzle-orm";
3477
3540
  import Fastify from "fastify";
3478
3541
  import os4 from "os";
@@ -5393,12 +5456,12 @@ function sleep2(ms) {
5393
5456
  }
5394
5457
 
5395
5458
  // ../provider-cdp/src/screenshot.ts
5396
- import fs from "fs";
5459
+ import fs2 from "fs";
5397
5460
  import path from "path";
5398
5461
  async function captureElementScreenshot(client, selector, outputPath) {
5399
5462
  const dir = path.dirname(outputPath);
5400
- if (!fs.existsSync(dir)) {
5401
- fs.mkdirSync(dir, { recursive: true });
5463
+ if (!fs2.existsSync(dir)) {
5464
+ fs2.mkdirSync(dir, { recursive: true });
5402
5465
  }
5403
5466
  let clip;
5404
5467
  try {
@@ -5432,7 +5495,7 @@ async function captureElementScreenshot(client, selector, outputPath) {
5432
5495
  }
5433
5496
  const { data } = await client.Page.captureScreenshot(screenshotParams);
5434
5497
  const buffer = Buffer.from(data, "base64");
5435
- fs.writeFileSync(outputPath, buffer);
5498
+ fs2.writeFileSync(outputPath, buffer);
5436
5499
  return outputPath;
5437
5500
  }
5438
5501
 
@@ -6393,7 +6456,7 @@ function nonBlank(value) {
6393
6456
  return trimmed ? trimmed : void 0;
6394
6457
  }
6395
6458
  function resolveBuildCommit(env = process.env) {
6396
- const embedded = true ? "267b6410fa27a2bf032d4957047859e1f9eee6d1" : void 0;
6459
+ const embedded = true ? "1f10658e1c5f2b615dfbee110d3f8cf68dbfbda9" : void 0;
6397
6460
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
6398
6461
  }
6399
6462
  function resolveInstanceIdentity(env = process.env) {
@@ -6405,7 +6468,7 @@ function resolveInstanceIdentity(env = process.env) {
6405
6468
 
6406
6469
  // src/job-runner.ts
6407
6470
  import crypto6 from "crypto";
6408
- import fs2 from "fs";
6471
+ import fs3 from "fs";
6409
6472
  import path3 from "path";
6410
6473
  import os3 from "os";
6411
6474
  import { and as and2, eq as eq2, inArray, ne, sql as sql2 } from "drizzle-orm";
@@ -6973,12 +7036,12 @@ var JobRunner = class {
6973
7036
  allBrandNames
6974
7037
  );
6975
7038
  let screenshotRelPath = null;
6976
- if (raw.screenshotPath && fs2.existsSync(raw.screenshotPath)) {
7039
+ if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
6977
7040
  const snapshotId = crypto6.randomUUID();
6978
7041
  const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
6979
- if (!fs2.existsSync(screenshotDir)) fs2.mkdirSync(screenshotDir, { recursive: true });
7042
+ if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
6980
7043
  const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
6981
- fs2.renameSync(raw.screenshotPath, destPath);
7044
+ fs3.renameSync(raw.screenshotPath, destPath);
6982
7045
  screenshotRelPath = `${runId}/${snapshotId}.png`;
6983
7046
  this.db.insert(querySnapshots).values({
6984
7047
  id: snapshotId,
@@ -7131,11 +7194,11 @@ var JobRunner = class {
7131
7194
  );
7132
7195
  const snapshotId = crypto6.randomUUID();
7133
7196
  let screenshotRelPath = null;
7134
- if (raw.screenshotPath && fs2.existsSync(raw.screenshotPath)) {
7197
+ if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
7135
7198
  const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
7136
- if (!fs2.existsSync(screenshotDir)) fs2.mkdirSync(screenshotDir, { recursive: true });
7199
+ if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
7137
7200
  const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
7138
- fs2.renameSync(raw.screenshotPath, destPath);
7201
+ fs3.renameSync(raw.screenshotPath, destPath);
7139
7202
  screenshotRelPath = `${runId}/${snapshotId}.png`;
7140
7203
  }
7141
7204
  this.db.insert(querySnapshots).values({
@@ -9695,7 +9758,7 @@ function computeSummary(rows) {
9695
9758
 
9696
9759
  // src/backlink-extract.ts
9697
9760
  import crypto16 from "crypto";
9698
- import fs3 from "fs";
9761
+ import fs4 from "fs";
9699
9762
  import { and as and9, desc as desc5, eq as eq11 } from "drizzle-orm";
9700
9763
  var log11 = createLogger("BacklinkExtract");
9701
9764
  function defaultDeps3() {
@@ -9724,7 +9787,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
9724
9787
  if (!sync.vertexPath || !sync.edgesPath) {
9725
9788
  throw new Error(`Release ${sync.release} is missing cached file paths`);
9726
9789
  }
9727
- if (!fs3.existsSync(sync.vertexPath) || !fs3.existsSync(sync.edgesPath)) {
9790
+ if (!fs4.existsSync(sync.vertexPath) || !fs4.existsSync(sync.edgesPath)) {
9728
9791
  throw new Error(
9729
9792
  `Cache for release ${sync.release} is missing from disk (expected at ${sync.vertexPath}). The sync record exists in the database, but the ~16 GB dump was deleted or never present on this machine. Re-sync this release from the Backlinks admin page to restore the cache.`
9730
9793
  );
@@ -12319,7 +12382,7 @@ function readStoredGroundingSources(rawResponse) {
12319
12382
  return result;
12320
12383
  }
12321
12384
  async function backfillInsightsCommand(project, opts) {
12322
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-D2HY2S5F.js");
12385
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-25NQCETC.js");
12323
12386
  const config = loadConfig();
12324
12387
  const db = createClient(config.database);
12325
12388
  migrate(db);
@@ -14054,7 +14117,7 @@ import crypto25 from "crypto";
14054
14117
  import { eq as eq21 } from "drizzle-orm";
14055
14118
 
14056
14119
  // src/agent/session.ts
14057
- import fs6 from "fs";
14120
+ import fs7 from "fs";
14058
14121
  import path7 from "path";
14059
14122
  import { Agent } from "@mariozechner/pi-agent-core";
14060
14123
  import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
@@ -14298,25 +14361,25 @@ function buildAgentProvidersResponse(config) {
14298
14361
  }
14299
14362
 
14300
14363
  // src/agent/skill-paths.ts
14301
- import fs4 from "fs";
14364
+ import fs5 from "fs";
14302
14365
  import path5 from "path";
14303
- import { fileURLToPath } from "url";
14366
+ import { fileURLToPath as fileURLToPath2 } from "url";
14304
14367
  function resolveAeroSkillDir(pkgDir) {
14305
- const here = pkgDir ?? path5.dirname(fileURLToPath(import.meta.url));
14368
+ const here = pkgDir ?? path5.dirname(fileURLToPath2(import.meta.url));
14306
14369
  const candidates = [
14307
14370
  path5.join(here, "../assets/agent-workspace/skills/aero"),
14308
14371
  path5.join(here, "../../assets/agent-workspace/skills/aero"),
14309
14372
  path5.join(here, "../../../../skills/aero")
14310
14373
  ];
14311
14374
  for (const candidate of candidates) {
14312
- if (fs4.existsSync(path5.join(candidate, "SKILL.md"))) return candidate;
14375
+ if (fs5.existsSync(path5.join(candidate, "SKILL.md"))) return candidate;
14313
14376
  }
14314
14377
  throw new Error(`Aero skill not found. Searched:
14315
14378
  ${candidates.join("\n ")}`);
14316
14379
  }
14317
14380
 
14318
14381
  // src/agent/skill-tools.ts
14319
- import fs5 from "fs";
14382
+ import fs6 from "fs";
14320
14383
  import path6 from "path";
14321
14384
  import { Type } from "@sinclair/typebox";
14322
14385
  var MAX_DOC_CHARS = 2e4;
@@ -14339,12 +14402,12 @@ function parseDescription(body) {
14339
14402
  }
14340
14403
  function scanSkillDocs(skillDir) {
14341
14404
  const refsDir = path6.join(skillDir ?? resolveAeroSkillDir(), "references");
14342
- if (!fs5.existsSync(refsDir)) return [];
14405
+ if (!fs6.existsSync(refsDir)) return [];
14343
14406
  const entries = [];
14344
- for (const file of fs5.readdirSync(refsDir)) {
14407
+ for (const file of fs6.readdirSync(refsDir)) {
14345
14408
  if (!file.endsWith(".md")) continue;
14346
14409
  const filePath = path6.join(refsDir, file);
14347
- const body = fs5.readFileSync(filePath, "utf-8");
14410
+ const body = fs6.readFileSync(filePath, "utf-8");
14348
14411
  entries.push({
14349
14412
  slug: file.replace(/\.md$/, ""),
14350
14413
  description: parseDescription(body),
@@ -14388,7 +14451,7 @@ function buildReadSkillDocTool() {
14388
14451
  });
14389
14452
  }
14390
14453
  const filePath = path6.join(skillDir, "references", `${match.slug}.md`);
14391
- const content = fs5.readFileSync(filePath, "utf-8");
14454
+ const content = fs6.readFileSync(filePath, "utf-8");
14392
14455
  if (content.length > MAX_DOC_CHARS) {
14393
14456
  return textResult({
14394
14457
  slug: match.slug,
@@ -15049,9 +15112,9 @@ function ensureBuiltinsRegistered() {
15049
15112
  }
15050
15113
  function loadAeroSystemPrompt(pkgDir) {
15051
15114
  const skillDir = resolveAeroSkillDir(pkgDir);
15052
- const skillBody = fs6.readFileSync(path7.join(skillDir, "SKILL.md"), "utf-8");
15115
+ const skillBody = fs7.readFileSync(path7.join(skillDir, "SKILL.md"), "utf-8");
15053
15116
  const soulPath = path7.join(skillDir, "soul.md");
15054
- const base = fs6.existsSync(soulPath) ? `${fs6.readFileSync(soulPath, "utf-8").trimEnd()}
15117
+ const base = fs7.existsSync(soulPath) ? `${fs7.readFileSync(soulPath, "utf-8").trimEnd()}
15055
15118
 
15056
15119
  ---
15057
15120
 
@@ -15064,7 +15127,7 @@ function appendSystemPromptExtras(base, env = process.env) {
15064
15127
  const filePath = env.AERO_SYSTEM_PROMPT_FILE?.trim();
15065
15128
  if (filePath) {
15066
15129
  try {
15067
- fileBody = fs6.readFileSync(filePath, "utf-8").trim();
15130
+ fileBody = fs7.readFileSync(filePath, "utf-8").trim();
15068
15131
  } catch {
15069
15132
  fileBody = "";
15070
15133
  }
@@ -16088,7 +16151,8 @@ function registerMcpHttpRoutes(scope, opts) {
16088
16151
  credentialScopes: scopes,
16089
16152
  operator: request.operatorAccess === true,
16090
16153
  tiers: segment.tiers,
16091
- clientFactory: () => client
16154
+ clientFactory: () => client,
16155
+ updateAvailable: opts.getUpdateAvailable
16092
16156
  });
16093
16157
  } catch (error) {
16094
16158
  if (sessionKey) revokeSessionKey(opts.db, sessionKey.id);
@@ -18141,7 +18205,7 @@ async function createServer(opts) {
18141
18205
  });
18142
18206
  };
18143
18207
  const orphanedOpenClawDir = path8.join(os4.homedir(), ".openclaw-aero");
18144
- if (fs7.existsSync(orphanedOpenClawDir)) {
18208
+ if (fs8.existsSync(orphanedOpenClawDir)) {
18145
18209
  app.log.warn(
18146
18210
  { path: orphanedOpenClawDir },
18147
18211
  "OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
@@ -19051,7 +19115,7 @@ async function createServer(opts) {
19051
19115
  const configPath = getConfigPath();
19052
19116
  return {
19053
19117
  databasePath: opts.config.database,
19054
- configPath: fs7.existsSync(configPath) ? configPath : null
19118
+ configPath: fs8.existsSync(configPath) ? configPath : null
19055
19119
  };
19056
19120
  })(),
19057
19121
  // Snapshot the bundled skill trees (version + file hashes) so the
@@ -19066,6 +19130,8 @@ async function createServer(opts) {
19066
19130
  }
19067
19131
  })(),
19068
19132
  getAgentPluginState: opts.getAgentPluginState,
19133
+ // Powers the `canonry.version.current` doctor check. Non-blocking.
19134
+ getUpdateStatus: () => getServerUpdateStatus(),
19069
19135
  // Local canonry serve runs on the operator's machine, where pointing a
19070
19136
  // webhook at localhost (Discord test container, Pipedream-mock dev server,
19071
19137
  // etc.) is a legitimate workflow. Default to allowing it for the local
@@ -19084,7 +19150,13 @@ async function createServer(opts) {
19084
19150
  credentials: credentialChecker,
19085
19151
  oauthResourceUrl: publicOrigin ? `${publicOrigin}${`${basePath ?? "/"}api/v1/mcp`.replace("//", "/")}` : void 0,
19086
19152
  registerAuthenticatedRoutes: async (scope) => {
19087
- registerMcpHttpRoutes(scope, { selfApiUrl: opts.config.apiUrl, issuer: publicOrigin, db: opts.db });
19153
+ registerMcpHttpRoutes(scope, {
19154
+ selfApiUrl: opts.config.apiUrl,
19155
+ issuer: publicOrigin,
19156
+ db: opts.db,
19157
+ // Same non-blocking cache as /health, so hosted agents see the notice too.
19158
+ getUpdateAvailable: () => checkLatestVersionForServer()
19159
+ });
19088
19160
  registerOAuthAdminRoutes(scope, { db: opts.db });
19089
19161
  if (!sessionRegistry) return;
19090
19162
  registerAgentRoutes(scope, { db: opts.db, sessionRegistry });
@@ -19627,9 +19699,9 @@ async function createServer(opts) {
19627
19699
  return snapshotService.createReport(input);
19628
19700
  }
19629
19701
  });
19630
- const dirname = path8.dirname(fileURLToPath2(import.meta.url));
19702
+ const dirname = path8.dirname(fileURLToPath3(import.meta.url));
19631
19703
  const assetsDir = opts.assetsDir ?? path8.join(dirname, "..", "assets");
19632
- if (fs7.existsSync(assetsDir)) {
19704
+ if (fs8.existsSync(assetsDir)) {
19633
19705
  const indexPath = path8.join(assetsDir, "index.html");
19634
19706
  const injectConfig = (html, projectTabsOverride, themeOverride, renderTokenOverride) => {
19635
19707
  const clientConfig = {};
@@ -19703,8 +19775,8 @@ async function createServer(opts) {
19703
19775
  }
19704
19776
  });
19705
19777
  const serveIndex = (_request, reply) => {
19706
- if (fs7.existsSync(indexPath)) {
19707
- const html = fs7.readFileSync(indexPath, "utf-8");
19778
+ if (fs8.existsSync(indexPath)) {
19779
+ const html = fs8.readFileSync(indexPath, "utf-8");
19708
19780
  return sendSpaDocument(reply, html);
19709
19781
  }
19710
19782
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -19735,8 +19807,8 @@ async function createServer(opts) {
19735
19807
  if (basePath && !url.startsWith(basePath)) {
19736
19808
  return reply.status(404).send({ error: "Not found", path: request.url });
19737
19809
  }
19738
- if (fs7.existsSync(indexPath)) {
19739
- const html = fs7.readFileSync(indexPath, "utf-8");
19810
+ if (fs8.existsSync(indexPath)) {
19811
+ const html = fs8.readFileSync(indexPath, "utf-8");
19740
19812
  return sendSpaDocument(reply, html);
19741
19813
  }
19742
19814
  return reply.status(404).send({ error: "Not found" });
@@ -19900,6 +19972,8 @@ export {
19900
19972
  setGoogleAuthConfig,
19901
19973
  formatAuditFactorScore,
19902
19974
  checkLatestVersionForCli,
19975
+ readCachedUpdateAvailable,
19976
+ formatUpdateNotice,
19903
19977
  listAgentProviders,
19904
19978
  coerceAgentProvider,
19905
19979
  AERO_TOOL_PROFILES,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AGENT_MEMORY_KEY_MAX_LENGTH,
3
3
  AGENT_MEMORY_VALUE_MAX_BYTES,
4
+ CANONRY_NPM_PACKAGE_URL,
4
5
  CodingAgents,
5
6
  DEFAULT_VIEWER_RESEARCH_DAILY_RUN_LIMIT,
6
7
  DISCOVERY_MAX_PROBES_CAP,
@@ -28,6 +29,7 @@ import {
28
29
  canonicalizeGtmResourceSelection,
29
30
  classifySkillFile,
30
31
  coerceSkillManifest,
32
+ compareSemver,
31
33
  competitorBatchRequestSchema,
32
34
  competitorLandscapeQuerySchema,
33
35
  describeError,
@@ -40,8 +42,10 @@ import {
40
42
  gaMeasurementHostScopeSchema,
41
43
  googleAdsMetricsWindowSchema,
42
44
  gscPerformanceOrderBySchema,
45
+ isInstallMethod,
43
46
  isReadOnlyKey,
44
47
  isSensitiveDiagnosticQueryKey,
48
+ isStrictSemver,
45
49
  keywordBatchRequestSchema,
46
50
  keywordGenerateRequestSchema,
47
51
  logQuerySchema,
@@ -117,8 +121,10 @@ import {
117
121
  trafficConnectWordpressRequestSchema,
118
122
  trafficEventKindSchema,
119
123
  trafficSeriesGranularitySchema,
124
+ upgradeCaveatFor,
125
+ upgradeCommandFor,
120
126
  visibilityReportRequestSchema
121
- } from "./chunk-5ZIUROAQ.js";
127
+ } from "./chunk-MMSPU72Z.js";
122
128
 
123
129
  // src/cli-error.ts
124
130
  function isMachineFormat(format) {
@@ -6027,6 +6033,16 @@ function createClient2(opts) {
6027
6033
  }
6028
6034
 
6029
6035
  // src/client.ts
6036
+ function parseServerUpdateAvailable(value) {
6037
+ if (!value || typeof value !== "object") return null;
6038
+ const { current, latest, installMethod } = value;
6039
+ if (typeof current !== "string" || current.length > 32 || !isStrictSemver(current)) return null;
6040
+ if (typeof latest !== "string" || latest.length > 32 || !isStrictSemver(latest)) return null;
6041
+ if (compareSemver(latest, current) <= 0) return null;
6042
+ const method = installMethod === void 0 ? "npm" : installMethod;
6043
+ if (!isInstallMethod(method)) return null;
6044
+ return { current, latest, installMethod: method, upgradeCommand: upgradeCommandFor(method), url: CANONRY_NPM_PACKAGE_URL };
6045
+ }
6030
6046
  var usageTagStore = new AsyncLocalStorage();
6031
6047
  function runWithUsageTags(client2, tags, fn) {
6032
6048
  if (!(client2 instanceof ApiClient)) return fn();
@@ -7095,6 +7111,22 @@ var ApiClient = class {
7095
7111
  async getApiKeySelf() {
7096
7112
  return this.invoke(() => getApiV1KeysSelf({ client: this.heyClient }));
7097
7113
  }
7114
+ /**
7115
+ * The connected server's update notice from `/health`, or null. Best-effort
7116
+ * and bounded (2s), never throws: `canonry-mcp` uses it to tell agents that
7117
+ * never run the CLI about a newer release.
7118
+ */
7119
+ async getServerUpdateAvailable() {
7120
+ try {
7121
+ await this.probeBasePath();
7122
+ const res = await fetch(`${this.originUrl}/health`, { signal: AbortSignal.timeout(2e3) });
7123
+ if (!res.ok) return null;
7124
+ const body = await res.json();
7125
+ return parseServerUpdateAvailable(body?.updateAvailable);
7126
+ } catch {
7127
+ return null;
7128
+ }
7129
+ }
7098
7130
  async createApiKey(body) {
7099
7131
  return this.invoke(() => postApiV1Keys({ client: this.heyClient, body }));
7100
7132
  }
@@ -13139,15 +13171,34 @@ function createCanonryMcpServer(options = {}) {
13139
13171
  return createCanonryMcpServerWithCatalog(options).server;
13140
13172
  }
13141
13173
  var SERVER_INSTRUCTIONS = OPERATIONS_GUIDE.initialize;
13174
+ function updateNoticeInstructions(update) {
13175
+ const upgrade = update.installMethod === "docker" ? update.upgradeCommand : `${update.upgradeCommand}, then restart the Canonry server`;
13176
+ const caveat = upgradeCaveatFor(update.installMethod);
13177
+ return `Update available (UPDATE_AVAILABLE): canonry ${update.latest} is published; the connected Canonry server runs ${update.current}. Tell the operator. Upgrade: ${upgrade}. ${caveat ? `${caveat} ` : ""}Only upgrade with the operator's approval.`;
13178
+ }
13179
+ function updateNoticePayload(update) {
13180
+ const caveat = upgradeCaveatFor(update.installMethod);
13181
+ return { code: "UPDATE_AVAILABLE", ...update, ...caveat ? { note: caveat } : {} };
13182
+ }
13183
+ function readUpdateAvailable(getter) {
13184
+ try {
13185
+ return getter?.() ?? null;
13186
+ } catch {
13187
+ return null;
13188
+ }
13189
+ }
13142
13190
  function createCanonryMcpServerWithCatalog(options = {}) {
13143
13191
  const clientFactory = options.clientFactory ?? (() => createApiClient({ clientName: "canonry-mcp", surface: "mcp-stdio", actorSession: randomUUID() }));
13144
13192
  const client2 = clientFactory();
13145
13193
  const scope = options.scope ?? "all";
13194
+ const update = readUpdateAvailable(options.updateAvailable);
13146
13195
  const server = new McpServer({
13147
13196
  name: "canonry",
13148
13197
  version: PACKAGE_VERSION
13149
13198
  }, {
13150
- instructions: SERVER_INSTRUCTIONS
13199
+ instructions: update ? `${SERVER_INSTRUCTIONS.trimEnd()}
13200
+
13201
+ ${updateNoticeInstructions(update)}` : SERVER_INSTRUCTIONS
13151
13202
  });
13152
13203
  server.validateToolInput = async (_tool, args) => args;
13153
13204
  const entries = [];
@@ -13178,7 +13229,7 @@ function createCanonryMcpServerWithCatalog(options = {}) {
13178
13229
  const catalog = new DynamicToolCatalog(server, entries, scope, { eager });
13179
13230
  catalog.applyInitialEnablement();
13180
13231
  const mode = options.tiers !== void 0 ? "hosted-fixed-catalog" : eager ? "stdio-fixed-catalog" : "stdio-progressive";
13181
- registerMetaTools(server, catalog, { includeToolkitLoader: options.tiers === void 0, mode });
13232
+ registerMetaTools(server, catalog, { includeToolkitLoader: options.tiers === void 0, mode, updateAvailable: options.updateAvailable });
13182
13233
  server.registerResource("canonry-agent-operations-v1", OPERATIONS_GUIDE.resourceUri, {
13183
13234
  title: "Canonry Operations Guide v1",
13184
13235
  description: "Optional public operations guidance. Use canonry_help when resources are unavailable.",
@@ -13205,7 +13256,11 @@ function registerMetaTools(server, catalog, opts) {
13205
13256
  async (input) => {
13206
13257
  try {
13207
13258
  const parsed = helpInputSchema.parse(input ?? {});
13208
- const result = operationsHelp(catalog.helpResult(), opts.mode, parsed.intent, parsed.includeCatalog);
13259
+ const update = readUpdateAvailable(opts.updateAvailable);
13260
+ const result = {
13261
+ ...operationsHelp(catalog.helpResult(), opts.mode, parsed.intent, parsed.includeCatalog),
13262
+ ...update ? { updateAvailable: updateNoticePayload(update) } : {}
13263
+ };
13209
13264
  return { ...jsonToolResult(result), structuredContent: result };
13210
13265
  } catch (error) {
13211
13266
  return errorToolResult(error);
@@ -241,6 +241,7 @@ import {
241
241
  clusterByCosine,
242
242
  coerceSkillManifest,
243
243
  compactDateToIso,
244
+ compareSemver,
244
245
  competitorBatchRequestSchema,
245
246
  competitorDtoSchema,
246
247
  competitorLandscapeQuerySchema,
@@ -391,6 +392,7 @@ import {
391
392
  isLocationRedirectStatus,
392
393
  isReadOnlyKey,
393
394
  isRetryableHttpError,
395
+ isStrictSemver,
394
396
  isTemplateLinkRatio,
395
397
  isTrendBaseline,
396
398
  isVertexGroundingRedirect,
@@ -678,6 +680,7 @@ import {
678
680
  trafficStatusResponseSchema,
679
681
  trafficSyncResponseSchema,
680
682
  unsupportedKind,
683
+ upgradeCaveatFor,
681
684
  userDtoSchema,
682
685
  userListDtoSchema,
683
686
  validationError,
@@ -703,7 +706,7 @@ import {
703
706
  wordpressSchemaDeployResultDtoSchema,
704
707
  wordpressSchemaStatusResultDtoSchema,
705
708
  wordpressStatusDtoSchema
706
- } from "./chunk-5ZIUROAQ.js";
709
+ } from "./chunk-MMSPU72Z.js";
707
710
 
708
711
  // src/intelligence-service.ts
709
712
  import { eq as eq67, desc as desc29, asc as asc12, and as and57, ne as ne11, or as or16, inArray as inArray25, gte as gte18, lte as lte15, isNull as isNull12, sql as sql27, exists } from "drizzle-orm";
@@ -81302,11 +81305,72 @@ var GOOGLE_MARKETING_DOCTOR_CHECKS = createGoogleMarketingDoctorChecks(
81302
81305
  (ctx) => ctx.getGoogleMarketingDoctorInput?.(ctx)
81303
81306
  );
81304
81307
 
81308
+ // ../api-routes/src/doctor/checks/version.ts
81309
+ var versionCurrentCheck = {
81310
+ id: "canonry.version.current",
81311
+ category: CheckCategories.config,
81312
+ scope: CheckScopes.global,
81313
+ title: "Canonry version",
81314
+ run: (ctx) => {
81315
+ if (!ctx.getUpdateStatus) {
81316
+ return {
81317
+ status: CheckStatuses.skipped,
81318
+ code: "version.status-unavailable",
81319
+ summary: "This deployment does not report update status."
81320
+ };
81321
+ }
81322
+ const status = ctx.getUpdateStatus();
81323
+ if (!status.enabled) {
81324
+ return {
81325
+ status: CheckStatuses.skipped,
81326
+ code: "version.check-disabled",
81327
+ summary: `Update check is off (${status.disabledBy ?? "opted out"}); running ${status.current}.`,
81328
+ details: { current: status.current, ...status.disabledBy ? { disabledBy: status.disabledBy } : {} }
81329
+ };
81330
+ }
81331
+ if (!status.latest || !isStrictSemver(status.latest)) {
81332
+ return {
81333
+ status: CheckStatuses.skipped,
81334
+ code: "version.latest-unknown",
81335
+ summary: `Latest published version is not known yet (running ${status.current}).`,
81336
+ remediation: "The npm registry has not been reached yet. Re-run doctor in a moment; offline hosts stay skipped.",
81337
+ details: { current: status.current }
81338
+ };
81339
+ }
81340
+ if (compareSemver(status.latest, status.current) > 0) {
81341
+ const caveat = upgradeCaveatFor(status.installMethod);
81342
+ return {
81343
+ status: CheckStatuses.warn,
81344
+ code: "version.outdated",
81345
+ summary: `canonry ${status.latest} is available; this server runs ${status.current}.`,
81346
+ remediation: status.installMethod === "docker" ? `Upgrade the container: ${status.upgradeCommand}.` : `Run \`${status.upgradeCommand}\`, then restart the server (\`canonry stop && canonry start\`, or restart \`canonry serve\`).${caveat ? ` ${caveat}` : ""}`,
81347
+ details: {
81348
+ current: status.current,
81349
+ latest: status.latest,
81350
+ installMethod: status.installMethod,
81351
+ upgradeCommand: status.upgradeCommand,
81352
+ url: status.url
81353
+ }
81354
+ };
81355
+ }
81356
+ return {
81357
+ status: CheckStatuses.ok,
81358
+ code: "version.current",
81359
+ // "latest" would overstate it: when the registry is unreachable this can
81360
+ // come from an older on-disk cache, which proves only that nothing newer is known.
81361
+ summary: `No newer canonry known (running ${status.current}; latest seen ${status.latest}).`,
81362
+ details: { current: status.current, latest: status.latest }
81363
+ };
81364
+ }
81365
+ };
81366
+ var VERSION_CHECKS = [versionCurrentCheck];
81367
+
81305
81368
  // ../api-routes/src/doctor/registry.ts
81306
81369
  var ALL_CHECKS = [
81307
81370
  // Runtime-state checks run first so file-system gone errors surface
81308
81371
  // before any auth/integration checks try to touch the (orphaned) DB.
81309
81372
  ...RUNTIME_STATE_CHECKS,
81373
+ ...VERSION_CHECKS,
81310
81374
  ...GOOGLE_AUTH_CHECKS,
81311
81375
  ...GBP_AUTH_CHECKS,
81312
81376
  ...PLACES_CHECKS,
@@ -81412,6 +81476,7 @@ async function doctorRoutes(app, opts) {
81412
81476
  runtimeStatePaths: opts.runtimeStatePaths,
81413
81477
  bundledSkills: opts.bundledSkills,
81414
81478
  getAgentPluginState: opts.getAgentPluginState,
81479
+ getUpdateStatus: opts.getUpdateStatus,
81415
81480
  getGoogleMarketingDoctorInput: opts.getGoogleMarketingDoctorInput
81416
81481
  };
81417
81482
  return runChecks(ctx, ALL_CHECKS, { checkIds });
@@ -81441,6 +81506,7 @@ async function doctorRoutes(app, opts) {
81441
81506
  runtimeStatePaths: opts.runtimeStatePaths,
81442
81507
  bundledSkills: opts.bundledSkills,
81443
81508
  getAgentPluginState: opts.getAgentPluginState,
81509
+ getUpdateStatus: opts.getUpdateStatus,
81444
81510
  getGoogleMarketingDoctorInput: opts.getGoogleMarketingDoctorInput
81445
81511
  };
81446
81512
  return runChecks(ctx, ALL_CHECKS, { checkIds });
@@ -84750,6 +84816,7 @@ async function apiRoutes(app, opts) {
84750
84816
  runtimeStatePaths: opts.runtimeStatePaths,
84751
84817
  bundledSkills: opts.bundledSkills,
84752
84818
  getAgentPluginState: opts.getAgentPluginState,
84819
+ getUpdateStatus: opts.getUpdateStatus,
84753
84820
  getGoogleMarketingDoctorInput: opts.getGoogleMarketingDoctorInput
84754
84821
  });
84755
84822
  if (opts.registerAuthenticatedRoutes) {
@@ -5,11 +5,11 @@ import {
5
5
  installSkills,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-5K2NUQOV.js";
8
+ } from "./chunk-AEE5DGTE.js";
9
9
  import {
10
10
  SKILL_MANIFEST_FILENAME,
11
11
  SkillsClients
12
- } from "./chunk-5ZIUROAQ.js";
12
+ } from "./chunk-MMSPU72Z.js";
13
13
 
14
14
  // src/skills-autosync.ts
15
15
  import fs from "fs";
@@ -18054,6 +18054,63 @@ function redactEmbeddedUrls(value) {
18054
18054
  });
18055
18055
  }
18056
18056
 
18057
+ // ../contracts/src/semver.ts
18058
+ var STRICT_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?$/i;
18059
+ var MAX_SEMVER_LENGTH = 128;
18060
+ function isStrictSemver(value) {
18061
+ return value.length <= MAX_SEMVER_LENGTH && STRICT_SEMVER.test(value);
18062
+ }
18063
+ function compareSemver(a, b) {
18064
+ const parse = (v) => {
18065
+ const parts = (v.split(/[-+]/)[0] ?? "").split(".");
18066
+ if (parts.length < 3) return null;
18067
+ const nums = parts.slice(0, 3).map(Number);
18068
+ if (!nums.every((n) => Number.isInteger(n) && n >= 0)) return null;
18069
+ return [nums[0], nums[1], nums[2]];
18070
+ };
18071
+ const pa = parse(a);
18072
+ const pb = parse(b);
18073
+ if (!pa || !pb) return 0;
18074
+ for (let i = 0; i < 3; i++) {
18075
+ if (pa[i] > pb[i]) return 1;
18076
+ if (pa[i] < pb[i]) return -1;
18077
+ }
18078
+ return 0;
18079
+ }
18080
+
18081
+ // ../contracts/src/update-notice.ts
18082
+ var CANONRY_NPM_PACKAGE = "@canonry/canonry";
18083
+ var CANONRY_NPM_PACKAGE_URL = `https://www.npmjs.com/package/${CANONRY_NPM_PACKAGE}`;
18084
+ var INSTALL_METHODS = ["npm", "homebrew", "docker"];
18085
+ function isInstallMethod(value) {
18086
+ return typeof value === "string" && INSTALL_METHODS.includes(value);
18087
+ }
18088
+ function upgradeCommandFor(method) {
18089
+ switch (method) {
18090
+ case "npm":
18091
+ return `npm install -g ${CANONRY_NPM_PACKAGE}`;
18092
+ case "homebrew":
18093
+ return "brew upgrade canonry";
18094
+ case "docker":
18095
+ return "pull or rebuild your canonry image, then recreate the container";
18096
+ }
18097
+ }
18098
+ function upgradeCaveatFor(method) {
18099
+ switch (method) {
18100
+ case "homebrew":
18101
+ return "Homebrew can trail npm briefly; if brew says canonry is up to date, retry later.";
18102
+ case "npm":
18103
+ case "docker":
18104
+ return null;
18105
+ }
18106
+ }
18107
+ function updateCheckEnvOptOut(env) {
18108
+ if (env.CANONRY_DISABLE_UPDATE_CHECK === "1") return "CANONRY_DISABLE_UPDATE_CHECK";
18109
+ if (env.DO_NOT_TRACK === "1") return "DO_NOT_TRACK";
18110
+ if (env.CI) return "CI";
18111
+ return null;
18112
+ }
18113
+
18057
18114
  export {
18058
18115
  __export,
18059
18116
  apiKeyDtoSchema,
@@ -18849,5 +18906,12 @@ export {
18849
18906
  isSensitiveDiagnosticQueryKey,
18850
18907
  redactLogValue,
18851
18908
  redactLogString,
18852
- diagnosticIdentity
18909
+ diagnosticIdentity,
18910
+ isStrictSemver,
18911
+ compareSemver,
18912
+ CANONRY_NPM_PACKAGE_URL,
18913
+ isInstallMethod,
18914
+ upgradeCommandFor,
18915
+ upgradeCaveatFor,
18916
+ updateCheckEnvOptOut
18853
18917
  };
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  createServer,
17
17
  detectAndTrackUpgrade,
18
18
  formatAuditFactorScore,
19
+ formatUpdateNotice,
19
20
  getCloudflareTrafficConnectionBySourceId,
20
21
  getOrCreateAnonymousId,
21
22
  getTelemetryStatus,
@@ -24,6 +25,7 @@ import {
24
25
  isLoopbackBindHost,
25
26
  isTelemetryEnabled,
26
27
  listAgentProviders,
28
+ readCachedUpdateAvailable,
27
29
  setGoogleAuthConfig,
28
30
  setTelemetryPreference,
29
31
  setTelemetrySource,
@@ -31,11 +33,11 @@ import {
31
33
  trackCliCommandFinished,
32
34
  trackEvent,
33
35
  waitForServerRuntimeStartup
34
- } from "./chunk-BS4ZC7FG.js";
36
+ } from "./chunk-5ZOLPHTO.js";
35
37
  import {
36
38
  autoSyncSkills,
37
39
  formatAutoSyncNotice
38
- } from "./chunk-NKEKDWGX.js";
40
+ } from "./chunk-KJM4DRZN.js";
39
41
  import {
40
42
  CliError,
41
43
  EXIT_SYSTEM_ERROR,
@@ -61,7 +63,7 @@ import {
61
63
  saveConfigPatch,
62
64
  systemError,
63
65
  usageError
64
- } from "./chunk-5K2NUQOV.js";
66
+ } from "./chunk-AEE5DGTE.js";
65
67
  import {
66
68
  CLOUDFLARE_WORKER_BINDINGS,
67
69
  CLOUDFLARE_WORKER_GENERATED_MARKER,
@@ -75,7 +77,7 @@ import {
75
77
  projects,
76
78
  queries,
77
79
  renderReportHtml
78
- } from "./chunk-WSYJ4IT7.js";
80
+ } from "./chunk-EBUX3C5H.js";
79
81
  import {
80
82
  AdsDeliverySnapshotStatuses,
81
83
  AdsHistoricalCampaignRollupStatuses,
@@ -154,7 +156,7 @@ import {
154
156
  snapshotProviderModeSchema,
155
157
  visibilityReportRequestSchema,
156
158
  winnabilityClassSchema
157
- } from "./chunk-5ZIUROAQ.js";
159
+ } from "./chunk-MMSPU72Z.js";
158
160
 
159
161
  // src/cli.ts
160
162
  import { pathToFileURL } from "url";
@@ -19304,17 +19306,12 @@ async function runCli(args = process.argv.slice(2)) {
19304
19306
  ...cliRuntimeContext()
19305
19307
  });
19306
19308
  }
19307
- if (!isHelpRequest && command !== "telemetry" && process.stderr.isTTY) {
19308
- void checkLatestVersionForCli().then((update) => {
19309
- if (!update) return;
19310
- process.stderr.write(
19311
- `
19312
- \u2192 canonry ${update.latest} is available (you have ${update.current}).
19313
- Upgrade: ${update.upgradeCommand}
19314
-
19315
- `
19316
- );
19317
- });
19309
+ if (!isHelpRequest && command !== "telemetry") {
19310
+ const update = readCachedUpdateAvailable();
19311
+ if (update) {
19312
+ process.stderr.write(formatUpdateNotice(update, { format, interactive: Boolean(process.stderr.isTTY) }));
19313
+ }
19314
+ void checkLatestVersionForCli();
19318
19315
  }
19319
19316
  const commandStartedAt = Date.now();
19320
19317
  try {
package/dist/index.js CHANGED
@@ -3,12 +3,12 @@ import {
3
3
  createGoogleMarketingCredentialStore,
4
4
  createGoogleMarketingRuntime,
5
5
  createServer
6
- } from "./chunk-BS4ZC7FG.js";
6
+ } from "./chunk-5ZOLPHTO.js";
7
7
  import {
8
8
  loadConfig
9
- } from "./chunk-5K2NUQOV.js";
10
- import "./chunk-WSYJ4IT7.js";
11
- import "./chunk-5ZIUROAQ.js";
9
+ } from "./chunk-AEE5DGTE.js";
10
+ import "./chunk-EBUX3C5H.js";
11
+ import "./chunk-MMSPU72Z.js";
12
12
  export {
13
13
  GoogleMarketingRuntimeError,
14
14
  createGoogleMarketingCredentialStore,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-WSYJ4IT7.js";
4
- import "./chunk-5ZIUROAQ.js";
3
+ } from "./chunk-EBUX3C5H.js";
4
+ import "./chunk-MMSPU72Z.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -1,17 +1,49 @@
1
1
  import {
2
2
  autoSyncSkills
3
- } from "./chunk-NKEKDWGX.js";
3
+ } from "./chunk-KJM4DRZN.js";
4
4
  import {
5
5
  createApiClient,
6
6
  createCanonryMcpServer
7
- } from "./chunk-5K2NUQOV.js";
7
+ } from "./chunk-AEE5DGTE.js";
8
8
  import {
9
- isReadOnlyKey
10
- } from "./chunk-5ZIUROAQ.js";
9
+ isReadOnlyKey,
10
+ updateCheckEnvOptOut
11
+ } from "./chunk-MMSPU72Z.js";
11
12
 
12
13
  // src/mcp/cli.ts
13
14
  import { randomUUID } from "crypto";
14
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+
17
+ // src/mcp/update-notice.ts
18
+ function createUpdateNoticeSource(client, opts = {}) {
19
+ const disabled = updateCheckEnvOptOut(opts.env ?? process.env) !== null;
20
+ const ttlMs = opts.ttlMs ?? 60 * 60 * 1e3;
21
+ const now = opts.now ?? Date.now;
22
+ let value = null;
23
+ let fetchedAt = Number.NEGATIVE_INFINITY;
24
+ let inFlight = null;
25
+ const refresh = () => {
26
+ if (disabled) return Promise.resolve();
27
+ inFlight ??= client.getServerUpdateAvailable().then((next) => {
28
+ value = next;
29
+ }, () => {
30
+ value = null;
31
+ }).finally(() => {
32
+ fetchedAt = now();
33
+ inFlight = null;
34
+ });
35
+ return inFlight;
36
+ };
37
+ return {
38
+ refresh,
39
+ get: () => {
40
+ if (!disabled && !inFlight && now() - fetchedAt >= ttlMs) void refresh();
41
+ return value;
42
+ }
43
+ };
44
+ }
45
+
46
+ // src/mcp/cli.ts
15
47
  var HELP_TEXT = `Usage: canonry-mcp [--read-only | --scope=<all|read-only>] [--eager]
16
48
 
17
49
  Stdio MCP adapter over the Canonry public API. Inherits config from
@@ -47,8 +79,17 @@ async function main(argv = process.argv.slice(2)) {
47
79
  }
48
80
  void autoSyncSkills();
49
81
  const client = createApiClient({ clientName: "canonry-mcp", surface: "mcp-stdio", actorSession: randomUUID() });
50
- const authorization = await resolveEffectiveAuthorization(client, options.scope);
51
- const server = createCanonryMcpServer({ ...authorization, eager: options.eager, clientFactory: () => client });
82
+ const updateNotice = createUpdateNoticeSource(client);
83
+ const [authorization] = await Promise.all([
84
+ resolveEffectiveAuthorization(client, options.scope),
85
+ updateNotice.refresh()
86
+ ]);
87
+ const server = createCanonryMcpServer({
88
+ ...authorization,
89
+ eager: options.eager,
90
+ clientFactory: () => client,
91
+ updateAvailable: updateNotice.get
92
+ });
52
93
  await server.connect(new StdioServerTransport());
53
94
  }
54
95
  async function resolveEffectiveScope(client, flagScope) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "5.1.2",
3
+ "version": "5.1.3",
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
  "keywords": [
@@ -87,28 +87,28 @@
87
87
  "tsup": "^8.5.1",
88
88
  "tsx": "^4.19.0",
89
89
  "@ainyc/canonry-web": "0.0.0",
90
- "@ainyc/canonry-api-client": "0.0.0",
91
90
  "@ainyc/canonry-api-routes": "0.0.0",
91
+ "@ainyc/canonry-api-client": "0.0.0",
92
92
  "@ainyc/canonry-config": "0.0.0",
93
93
  "@ainyc/canonry-contracts": "0.0.0",
94
- "@ainyc/canonry-integration-bing": "0.0.0",
95
94
  "@ainyc/canonry-db": "0.0.0",
96
- "@ainyc/canonry-integration-cloud-run": "0.0.0",
95
+ "@ainyc/canonry-integration-bing": "0.0.0",
97
96
  "@ainyc/canonry-integration-cloudflare-queue": "0.0.0",
98
97
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
99
- "@ainyc/canonry-integration-google-ads": "0.0.0",
98
+ "@ainyc/canonry-integration-cloud-run": "0.0.0",
100
99
  "@ainyc/canonry-integration-google": "0.0.0",
101
100
  "@ainyc/canonry-integration-cloudflare-worker": "0.0.0",
102
- "@ainyc/canonry-integration-google-places": "0.0.0",
103
101
  "@ainyc/canonry-integration-google-business-profile": "0.0.0",
102
+ "@ainyc/canonry-integration-google-tag-manager": "0.0.0",
103
+ "@ainyc/canonry-integration-google-places": "0.0.0",
104
104
  "@ainyc/canonry-integration-openai-ads": "0.0.0",
105
- "@ainyc/canonry-integration-wordpress": "0.0.0",
106
- "@ainyc/canonry-intelligence": "0.0.0",
105
+ "@ainyc/canonry-integration-google-ads": "0.0.0",
107
106
  "@ainyc/canonry-integration-traffic": "0.0.0",
107
+ "@ainyc/canonry-integration-wordpress": "0.0.0",
108
108
  "@ainyc/canonry-provider-cdp": "0.0.0",
109
- "@ainyc/canonry-integration-google-tag-manager": "0.0.0",
110
- "@ainyc/canonry-provider-claude": "0.0.0",
111
109
  "@ainyc/canonry-provider-gemini": "0.0.0",
110
+ "@ainyc/canonry-intelligence": "0.0.0",
111
+ "@ainyc/canonry-provider-claude": "0.0.0",
112
112
  "@ainyc/canonry-provider-local": "0.0.0",
113
113
  "@ainyc/canonry-provider-openai": "0.0.0",
114
114
  "@ainyc/canonry-provider-perplexity": "0.0.0"