@ainyc/canonry 5.1.1 → 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.
Files changed (27) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +2 -0
  2. package/assets/assets/{AuditHistoryPanel-9F_74jKS.js → AuditHistoryPanel-Dc_pymFg.js} +1 -1
  3. package/assets/assets/{BacklinksPage-DmF2rbwy.js → BacklinksPage-BkSo48YX.js} +1 -1
  4. package/assets/assets/{HistoryPage-DKopwAjo.js → HistoryPage-BbswsdU7.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-Dpd_DYQv.js → MeasurementPropertyPage-BbgtalkM.js} +1 -1
  6. package/assets/assets/{ProjectPage-b1-57SzP.js → ProjectPage-CAcRQp-T.js} +1 -1
  7. package/assets/assets/{RunRow-BjrSRp6X.js → RunRow-2cNW8FHz.js} +1 -1
  8. package/assets/assets/{RunsPage-BFrymZ7D.js → RunsPage-Cz3lJ6VJ.js} +1 -1
  9. package/assets/assets/{SettingsPage-DdVa6pLK.js → SettingsPage-BQwB0r9k.js} +1 -1
  10. package/assets/assets/{SiteHealthSection-CizmiBjA.js → SiteHealthSection-C0aJ_UhN.js} +3 -3
  11. package/assets/assets/{TrafficPage-N7sZgb9x.js → TrafficPage-DTic3zhO.js} +1 -1
  12. package/assets/assets/{TrafficSourceDetailPage-BNQmMJ5D.js → TrafficSourceDetailPage-BKfabnCw.js} +1 -1
  13. package/assets/assets/{extract-error-message-Bh9mRIz9.js → extract-error-message-BzB8j3fC.js} +1 -1
  14. package/assets/assets/{index-BnR1Tu9j.js → index-DQGEzxrN.js} +3 -3
  15. package/assets/assets/{react-sigma_core.esm.min-DGaYwjLZ.js → react-sigma_core.esm.min-rYv0aKKx.js} +1 -1
  16. package/assets/assets/{v2-overview-adapter-eqN-8OK5.js → v2-overview-adapter-CWWPfsm-.js} +1 -1
  17. package/assets/index.html +1 -1
  18. package/dist/{chunk-ZWVXUPLP.js → chunk-5ZOLPHTO.js} +286 -96
  19. package/dist/{chunk-MEQGGODM.js → chunk-AEE5DGTE.js} +92 -8
  20. package/dist/{chunk-LHHJVGH7.js → chunk-EBUX3C5H.js} +110 -15
  21. package/dist/{chunk-66KGNC4U.js → chunk-KJM4DRZN.js} +2 -2
  22. package/dist/{chunk-3DL3KV6C.js → chunk-MMSPU72Z.js} +113 -1
  23. package/dist/cli.js +25 -26
  24. package/dist/index.js +4 -4
  25. package/dist/{intelligence-service-7XX5ZRYE.js → intelligence-service-25NQCETC.js} +2 -2
  26. package/dist/mcp.js +49 -7
  27. package/package.json +7 -7
@@ -15,8 +15,9 @@ import {
15
15
  loadConfig,
16
16
  loadConfigRaw,
17
17
  resolveOperatorApiKeyIds,
18
+ runWithUsageTags,
18
19
  saveConfigPatch
19
- } from "./chunk-MEQGGODM.js";
20
+ } from "./chunk-AEE5DGTE.js";
20
21
  import {
21
22
  CC_CACHE_DIR,
22
23
  DUCKDB_SPEC,
@@ -167,9 +168,10 @@ import {
167
168
  siteCrawlSnapshots,
168
169
  toAlertView,
169
170
  usageCounters
170
- } from "./chunk-LHHJVGH7.js";
171
+ } from "./chunk-EBUX3C5H.js";
171
172
  import {
172
173
  AGENT_MEMORY_VALUE_MAX_BYTES,
174
+ AGENT_NONE,
173
175
  AGENT_PROVIDER_IDS,
174
176
  AI_ENGINE_DOMAINS,
175
177
  AI_ENGINE_SELF_DOMAINS,
@@ -177,6 +179,7 @@ import {
177
179
  AdsCampaignBiddingTypes,
178
180
  AgentProviderIds,
179
181
  BacklinkSources,
182
+ CANONRY_NPM_PACKAGE_URL,
180
183
  CITED_URL_CAPTURE_VERSION,
181
184
  CcReleaseSyncStatuses,
182
185
  DEFAULT_VIEWER_RESEARCH_DAILY_RUN_LIMIT,
@@ -237,6 +240,7 @@ import {
237
240
  classifyProviderErrorMessages,
238
241
  classifyTemplateLinkEdge,
239
242
  cloudflareQueueNameSchema,
243
+ compareSemver,
240
244
  computeBacklinkSummaryMetrics,
241
245
  contentActionLabel,
242
246
  contentBriefDtoSchema,
@@ -247,6 +251,7 @@ import {
247
251
  deriveIndexCoverage,
248
252
  deriveSiteHealthState,
249
253
  describeError,
254
+ detectAgentRuntime,
250
255
  determineAnswerMentioned,
251
256
  dollarsToMicros,
252
257
  effectiveBrandNames,
@@ -276,13 +281,16 @@ import {
276
281
  isAgentProviderId,
277
282
  isBrowserProvider,
278
283
  isGhostTelemetryEvent,
284
+ isInstallMethod,
279
285
  isReadOnlyKey,
280
286
  isRetryableHttpError,
281
287
  isSelfLink,
288
+ isStrictSemver,
282
289
  isVertexGroundingRedirect,
283
290
  mapWithConcurrency,
284
291
  maskTelemetryAnonymousId,
285
292
  nextScheduleUpdatedAt,
293
+ normalizeAgentSlug,
286
294
  normalizeIdTokens,
287
295
  normalizeMeasurementExecutionQueryText,
288
296
  normalizeServedModel,
@@ -309,10 +317,22 @@ import {
309
317
  templateLinkUbiquityAvailable,
310
318
  textContainsBrandAlias,
311
319
  textContainsDomain,
320
+ updateCheckEnvOptOut,
321
+ upgradeCaveatFor,
322
+ upgradeCommandFor,
323
+ usageSurfaceSchema,
312
324
  validationError,
313
325
  winnabilityClassLabel,
314
326
  withRetry
315
- } from "./chunk-3DL3KV6C.js";
327
+ } from "./chunk-MMSPU72Z.js";
328
+
329
+ // src/runtime-context.ts
330
+ function cliRuntimeContext(env = process.env, stdio = process) {
331
+ return {
332
+ agent: detectAgentRuntime(env),
333
+ interactive: Boolean(stdio.stdin.isTTY && stdio.stdout.isTTY)
334
+ };
335
+ }
316
336
 
317
337
  // src/telemetry.ts
318
338
  import crypto from "crypto";
@@ -343,7 +363,8 @@ function trackCliCommandFinished(input) {
343
363
  command: input.command,
344
364
  success: input.success,
345
365
  duration_bucket: bucketCliCommandDuration(input.durationMs),
346
- ...input.setupState ? { setup_state: input.setupState } : {}
366
+ ...input.setupState ? { setup_state: input.setupState } : {},
367
+ ...cliRuntimeContext()
347
368
  },
348
369
  input.errorCode ? { errorCode: input.errorCode } : void 0
349
370
  );
@@ -418,7 +439,8 @@ function getOrCreateAnonymousId() {
418
439
  try {
419
440
  const config = loadConfig();
420
441
  if (config.anonymousId) return config.anonymousId;
421
- const id = crypto.randomUUID();
442
+ const carried = preConfigAnonymousId?.configPath === getConfigPath() ? preConfigAnonymousId.id : void 0;
443
+ const id = carried ?? crypto.randomUUID();
422
444
  config.anonymousId = id;
423
445
  try {
424
446
  saveConfigPatch(config);
@@ -430,8 +452,11 @@ function getOrCreateAnonymousId() {
430
452
  return getDeterministicAnonymousId();
431
453
  }
432
454
  }
433
- return getDeterministicAnonymousId();
455
+ const fallback = getDeterministicAnonymousId();
456
+ if (fallback) preConfigAnonymousId = { configPath: getConfigPath(), id: fallback };
457
+ return fallback;
434
458
  }
459
+ var preConfigAnonymousId;
435
460
  function readEnvAnonymousId() {
436
461
  const raw = process.env[ANON_ID_ENV_VAR]?.trim();
437
462
  if (!raw) return void 0;
@@ -506,10 +531,19 @@ function detectAndTrackUpgrade() {
506
531
  trackEvent("cli.upgraded", { fromVersion: lastSeen, toVersion: VERSION });
507
532
  }
508
533
  function trackEvent(event, properties, options) {
509
- if (!isTelemetryEnabled()) return;
510
- if (shouldDropTelemetryEvent(event, properties)) return;
534
+ void deliverEvent(event, properties, options);
535
+ }
536
+ function setTelemetryPreference(enabled, method) {
537
+ loadConfig();
538
+ const announce = !enabled && isTelemetryEnabled();
539
+ saveConfigPatch({ telemetry: enabled });
540
+ return announce ? deliverEvent("telemetry.disabled", { method }, void 0, { preferenceChecked: true }) : Promise.resolve();
541
+ }
542
+ function deliverEvent(event, properties, options, delivery = {}) {
543
+ if (!delivery.preferenceChecked && !isTelemetryEnabled()) return Promise.resolve();
544
+ if (shouldDropTelemetryEvent(event, properties)) return Promise.resolve();
511
545
  const anonymousId = getOrCreateAnonymousId();
512
- if (!anonymousId) return;
546
+ if (!anonymousId) return Promise.resolve();
513
547
  const payload = {
514
548
  eventId: options?.eventId ?? crypto.randomUUID(),
515
549
  anonymousId,
@@ -529,60 +563,59 @@ function trackEvent(event, properties, options) {
529
563
  const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
530
564
  timeout.unref();
531
565
  try {
532
- void fetch(TELEMETRY_ENDPOINT, {
566
+ return fetch(TELEMETRY_ENDPOINT, {
533
567
  method: "POST",
534
568
  headers: { "Content-Type": "application/json" },
535
569
  body: JSON.stringify(payload),
536
570
  signal: controller.signal
537
- }).catch(() => {
538
- }).finally(() => clearTimeout(timeout));
571
+ }).then(() => void 0, () => void 0).finally(() => clearTimeout(timeout));
539
572
  } catch {
540
573
  clearTimeout(timeout);
574
+ return Promise.resolve();
541
575
  }
542
576
  }
543
577
 
544
578
  // src/update-check.ts
579
+ import fs from "fs";
545
580
  import { createRequire as createRequire2 } from "module";
581
+ import { fileURLToPath } from "url";
546
582
  var _require2 = createRequire2(import.meta.url);
547
583
  var { version: PKG_VERSION } = _require2("../package.json");
548
584
  var PKG_NAME = "@canonry/canonry";
549
585
  var NPM_DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PKG_NAME}/dist-tags`;
550
- var NPM_PACKAGE_URL = `https://www.npmjs.com/package/${PKG_NAME}`;
586
+ var NPM_PACKAGE_URL = CANONRY_NPM_PACKAGE_URL;
551
587
  var FETCH_TIMEOUT_MS = 1500;
552
- function isUpdateCheckEnabled() {
553
- if (process.env.CANONRY_DISABLE_UPDATE_CHECK === "1") return false;
554
- if (process.env.DO_NOT_TRACK === "1") return false;
555
- if (process.env.CI) return false;
556
- 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);
557
593
  try {
558
- const raw = loadConfigRaw();
559
- return raw?.updateCheck !== false;
594
+ modulePath = fs.realpathSync(modulePath);
560
595
  } catch {
561
- return true;
562
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";
563
601
  }
564
- function compareSemver(a, b) {
565
- const parse = (v) => {
566
- const core = v.split(/[-+]/)[0];
567
- if (!core) return null;
568
- const parts = core.split(".");
569
- if (parts.length < 3) return null;
570
- const nums = [];
571
- for (let i = 0; i < 3; i++) {
572
- const n = Number(parts[i]);
573
- if (!Number.isInteger(n) || n < 0) return null;
574
- nums.push(n);
575
- }
576
- return [nums[0], nums[1], nums[2]];
577
- };
578
- const pa = parse(a);
579
- const pb = parse(b);
580
- if (!pa || !pb) return 0;
581
- for (let i = 0; i < 3; i++) {
582
- if (pa[i] > pb[i]) return 1;
583
- 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;
584
618
  }
585
- return 0;
586
619
  }
587
620
  async function fetchLatestVersion(opts) {
588
621
  const controller = new AbortController();
@@ -595,7 +628,7 @@ async function fetchLatestVersion(opts) {
595
628
  });
596
629
  if (!res.ok) return null;
597
630
  const data = await res.json();
598
- if (typeof data.latest !== "string") return null;
631
+ if (typeof data.latest !== "string" || !isStrictSemver(data.latest)) return null;
599
632
  return data.latest;
600
633
  } catch {
601
634
  return null;
@@ -603,13 +636,15 @@ async function fetchLatestVersion(opts) {
603
636
  clearTimeout(timeout);
604
637
  }
605
638
  }
606
- function buildUpdateAvailable(current, latest) {
639
+ function buildUpdateAvailable(current, latest, installMethod = currentInstallMethod()) {
640
+ if (!isStrictSemver(latest)) return null;
607
641
  if (compareSemver(latest, current) <= 0) return null;
608
642
  return {
609
643
  current,
610
644
  latest,
611
645
  url: NPM_PACKAGE_URL,
612
- upgradeCommand: `npm install -g ${PKG_NAME}`
646
+ upgradeCommand: upgradeCommandFor(installMethod),
647
+ installMethod
613
648
  };
614
649
  }
615
650
  async function checkLatestVersionForCli(opts) {
@@ -647,6 +682,45 @@ async function checkLatestVersionForCli(opts) {
647
682
  }
648
683
  return buildUpdateAvailable(PKG_VERSION, latest);
649
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
+ }
650
724
  var memoryCache = null;
651
725
  var inFlight = null;
652
726
  function startBackgroundRefresh(getNow) {
@@ -673,6 +747,22 @@ function checkLatestVersionForServer(opts) {
673
747
  if (!memoryCache || !memoryCache.latest) return null;
674
748
  return buildUpdateAvailable(PKG_VERSION, memoryCache.latest);
675
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
+ }
676
766
 
677
767
  // src/google-marketing-runtime.ts
678
768
  import crypto3 from "crypto";
@@ -3443,9 +3533,9 @@ function createGoogleMarketingRuntime(options) {
3443
3533
  // src/server.ts
3444
3534
  import { createRequire as createRequire4 } from "module";
3445
3535
  import crypto27 from "crypto";
3446
- import fs7 from "fs";
3536
+ import fs8 from "fs";
3447
3537
  import path8 from "path";
3448
- import { fileURLToPath as fileURLToPath2 } from "url";
3538
+ import { fileURLToPath as fileURLToPath3 } from "url";
3449
3539
  import { and as and20, eq as eq26 } from "drizzle-orm";
3450
3540
  import Fastify from "fastify";
3451
3541
  import os4 from "os";
@@ -5366,12 +5456,12 @@ function sleep2(ms) {
5366
5456
  }
5367
5457
 
5368
5458
  // ../provider-cdp/src/screenshot.ts
5369
- import fs from "fs";
5459
+ import fs2 from "fs";
5370
5460
  import path from "path";
5371
5461
  async function captureElementScreenshot(client, selector, outputPath) {
5372
5462
  const dir = path.dirname(outputPath);
5373
- if (!fs.existsSync(dir)) {
5374
- fs.mkdirSync(dir, { recursive: true });
5463
+ if (!fs2.existsSync(dir)) {
5464
+ fs2.mkdirSync(dir, { recursive: true });
5375
5465
  }
5376
5466
  let clip;
5377
5467
  try {
@@ -5405,7 +5495,7 @@ async function captureElementScreenshot(client, selector, outputPath) {
5405
5495
  }
5406
5496
  const { data } = await client.Page.captureScreenshot(screenshotParams);
5407
5497
  const buffer = Buffer.from(data, "base64");
5408
- fs.writeFileSync(outputPath, buffer);
5498
+ fs2.writeFileSync(outputPath, buffer);
5409
5499
  return outputPath;
5410
5500
  }
5411
5501
 
@@ -6280,13 +6370,93 @@ function removeWordpressConnection(config, projectName) {
6280
6370
  return true;
6281
6371
  }
6282
6372
 
6373
+ // src/usage-telemetry.ts
6374
+ var API_REQUEST_BUCKET_CAPACITY = 20;
6375
+ var API_REQUEST_REFILL_PER_MS = 1 / 1e4;
6376
+ var MAX_TRACKED_MCP_SESSIONS = 2e3;
6377
+ var MAX_ROUTE_LENGTH = 200;
6378
+ var SKIPPED_ROUTE_PATTERN = /(?:^|\/)(?:health|openapi\.json|telemetry)(?:\/|$)/;
6379
+ var MCP_CALL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6380
+ var MCP_TOOL_PATTERN = /^[a-z][a-z0-9_]{0,79}$/;
6381
+ function classifyUsageSurface(info) {
6382
+ const labelled = usageSurfaceSchema.safeParse(info.usageLabels.surface);
6383
+ if (labelled.success) return labelled.data;
6384
+ const ua = info.userAgent ?? "";
6385
+ if (ua.startsWith("canonry-cli")) return "cli";
6386
+ if (ua.startsWith("canonry-mcp/")) return "mcp-http";
6387
+ if (ua === "canonry-mcp") return "mcp-stdio";
6388
+ if (ua.startsWith("Mozilla/")) return "dashboard";
6389
+ return "api";
6390
+ }
6391
+ function statusClass(statusCode) {
6392
+ if (statusCode >= 500) return "5xx";
6393
+ if (statusCode >= 400) return "4xx";
6394
+ if (statusCode >= 300) return "3xx";
6395
+ return "2xx";
6396
+ }
6397
+ function createApiUsageTelemetry(options = {}) {
6398
+ const emit = options.emit ?? trackEvent;
6399
+ const now = options.now ?? Date.now;
6400
+ let tokens = API_REQUEST_BUCKET_CAPACITY;
6401
+ let refilledAt = now();
6402
+ let droppedBefore = 0;
6403
+ const seenSessions = /* @__PURE__ */ new Set();
6404
+ const takeToken = () => {
6405
+ const at = now();
6406
+ tokens = Math.min(API_REQUEST_BUCKET_CAPACITY, tokens + (at - refilledAt) * API_REQUEST_REFILL_PER_MS);
6407
+ refilledAt = at;
6408
+ if (tokens < 1) return false;
6409
+ tokens -= 1;
6410
+ return true;
6411
+ };
6412
+ return (info) => {
6413
+ if (info.method === "OPTIONS" || info.method === "HEAD") return;
6414
+ if (SKIPPED_ROUTE_PATTERN.test(info.route)) return;
6415
+ const surface = classifyUsageSurface(info);
6416
+ if (surface === "cli" || surface === "dashboard") return;
6417
+ const labels = info.usageLabels;
6418
+ const mcpClient = normalizeAgentSlug(labels.mcpClient);
6419
+ const detected = normalizeAgentSlug(labels.agent);
6420
+ const agent = detected && detected !== AGENT_NONE ? detected : mcpClient ?? detected ?? AGENT_NONE;
6421
+ const mcpTool = labels.mcpTool && MCP_TOOL_PATTERN.test(labels.mcpTool) ? labels.mcpTool : void 0;
6422
+ const mcpCallId = labels.mcpCall && MCP_CALL_ID_PATTERN.test(labels.mcpCall) ? labels.mcpCall.toLowerCase() : void 0;
6423
+ const isMcp = surface === "mcp-stdio" || surface === "mcp-http";
6424
+ if (isMcp && mcpTool && info.actorSession && !seenSessions.has(info.actorSession)) {
6425
+ if (seenSessions.size >= MAX_TRACKED_MCP_SESSIONS) {
6426
+ const oldest = seenSessions.values().next().value;
6427
+ if (oldest !== void 0) seenSessions.delete(oldest);
6428
+ }
6429
+ seenSessions.add(info.actorSession);
6430
+ emit("mcp.session.started", { surface, agent, ...mcpClient ? { mcpClient } : {} }, { source: "cli-server" });
6431
+ }
6432
+ if (!takeToken()) {
6433
+ droppedBefore += 1;
6434
+ return;
6435
+ }
6436
+ const properties = {
6437
+ surface,
6438
+ agent,
6439
+ method: info.method,
6440
+ route: info.route.slice(0, MAX_ROUTE_LENGTH),
6441
+ statusClass: statusClass(info.statusCode),
6442
+ durationBucket: bucketCliCommandDuration(info.durationMs),
6443
+ ...mcpClient ? { mcpClient } : {},
6444
+ ...mcpTool ? { mcpTool } : {},
6445
+ ...mcpCallId ? { mcpCallId } : {},
6446
+ ...droppedBefore > 0 ? { droppedBefore } : {}
6447
+ };
6448
+ droppedBefore = 0;
6449
+ emit("api.request", properties, { source: "cli-server" });
6450
+ };
6451
+ }
6452
+
6283
6453
  // src/instance-identity.ts
6284
6454
  function nonBlank(value) {
6285
6455
  const trimmed = value?.trim();
6286
6456
  return trimmed ? trimmed : void 0;
6287
6457
  }
6288
6458
  function resolveBuildCommit(env = process.env) {
6289
- const embedded = true ? "2c66fea459e20ff285096090dbc6a6ee788c4c00" : void 0;
6459
+ const embedded = true ? "1f10658e1c5f2b615dfbee110d3f8cf68dbfbda9" : void 0;
6290
6460
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
6291
6461
  }
6292
6462
  function resolveInstanceIdentity(env = process.env) {
@@ -6298,7 +6468,7 @@ function resolveInstanceIdentity(env = process.env) {
6298
6468
 
6299
6469
  // src/job-runner.ts
6300
6470
  import crypto6 from "crypto";
6301
- import fs2 from "fs";
6471
+ import fs3 from "fs";
6302
6472
  import path3 from "path";
6303
6473
  import os3 from "os";
6304
6474
  import { and as and2, eq as eq2, inArray, ne, sql as sql2 } from "drizzle-orm";
@@ -6866,12 +7036,12 @@ var JobRunner = class {
6866
7036
  allBrandNames
6867
7037
  );
6868
7038
  let screenshotRelPath = null;
6869
- if (raw.screenshotPath && fs2.existsSync(raw.screenshotPath)) {
7039
+ if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
6870
7040
  const snapshotId = crypto6.randomUUID();
6871
7041
  const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
6872
- if (!fs2.existsSync(screenshotDir)) fs2.mkdirSync(screenshotDir, { recursive: true });
7042
+ if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
6873
7043
  const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
6874
- fs2.renameSync(raw.screenshotPath, destPath);
7044
+ fs3.renameSync(raw.screenshotPath, destPath);
6875
7045
  screenshotRelPath = `${runId}/${snapshotId}.png`;
6876
7046
  this.db.insert(querySnapshots).values({
6877
7047
  id: snapshotId,
@@ -7024,11 +7194,11 @@ var JobRunner = class {
7024
7194
  );
7025
7195
  const snapshotId = crypto6.randomUUID();
7026
7196
  let screenshotRelPath = null;
7027
- if (raw.screenshotPath && fs2.existsSync(raw.screenshotPath)) {
7197
+ if (raw.screenshotPath && fs3.existsSync(raw.screenshotPath)) {
7028
7198
  const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
7029
- if (!fs2.existsSync(screenshotDir)) fs2.mkdirSync(screenshotDir, { recursive: true });
7199
+ if (!fs3.existsSync(screenshotDir)) fs3.mkdirSync(screenshotDir, { recursive: true });
7030
7200
  const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
7031
- fs2.renameSync(raw.screenshotPath, destPath);
7201
+ fs3.renameSync(raw.screenshotPath, destPath);
7032
7202
  screenshotRelPath = `${runId}/${snapshotId}.png`;
7033
7203
  }
7034
7204
  this.db.insert(querySnapshots).values({
@@ -9588,7 +9758,7 @@ function computeSummary(rows) {
9588
9758
 
9589
9759
  // src/backlink-extract.ts
9590
9760
  import crypto16 from "crypto";
9591
- import fs3 from "fs";
9761
+ import fs4 from "fs";
9592
9762
  import { and as and9, desc as desc5, eq as eq11 } from "drizzle-orm";
9593
9763
  var log11 = createLogger("BacklinkExtract");
9594
9764
  function defaultDeps3() {
@@ -9617,7 +9787,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
9617
9787
  if (!sync.vertexPath || !sync.edgesPath) {
9618
9788
  throw new Error(`Release ${sync.release} is missing cached file paths`);
9619
9789
  }
9620
- if (!fs3.existsSync(sync.vertexPath) || !fs3.existsSync(sync.edgesPath)) {
9790
+ if (!fs4.existsSync(sync.vertexPath) || !fs4.existsSync(sync.edgesPath)) {
9621
9791
  throw new Error(
9622
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.`
9623
9793
  );
@@ -12212,7 +12382,7 @@ function readStoredGroundingSources(rawResponse) {
12212
12382
  return result;
12213
12383
  }
12214
12384
  async function backfillInsightsCommand(project, opts) {
12215
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-7XX5ZRYE.js");
12385
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-25NQCETC.js");
12216
12386
  const config = loadConfig();
12217
12387
  const db = createClient(config.database);
12218
12388
  migrate(db);
@@ -13947,7 +14117,7 @@ import crypto25 from "crypto";
13947
14117
  import { eq as eq21 } from "drizzle-orm";
13948
14118
 
13949
14119
  // src/agent/session.ts
13950
- import fs6 from "fs";
14120
+ import fs7 from "fs";
13951
14121
  import path7 from "path";
13952
14122
  import { Agent } from "@mariozechner/pi-agent-core";
13953
14123
  import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
@@ -14191,25 +14361,25 @@ function buildAgentProvidersResponse(config) {
14191
14361
  }
14192
14362
 
14193
14363
  // src/agent/skill-paths.ts
14194
- import fs4 from "fs";
14364
+ import fs5 from "fs";
14195
14365
  import path5 from "path";
14196
- import { fileURLToPath } from "url";
14366
+ import { fileURLToPath as fileURLToPath2 } from "url";
14197
14367
  function resolveAeroSkillDir(pkgDir) {
14198
- const here = pkgDir ?? path5.dirname(fileURLToPath(import.meta.url));
14368
+ const here = pkgDir ?? path5.dirname(fileURLToPath2(import.meta.url));
14199
14369
  const candidates = [
14200
14370
  path5.join(here, "../assets/agent-workspace/skills/aero"),
14201
14371
  path5.join(here, "../../assets/agent-workspace/skills/aero"),
14202
14372
  path5.join(here, "../../../../skills/aero")
14203
14373
  ];
14204
14374
  for (const candidate of candidates) {
14205
- if (fs4.existsSync(path5.join(candidate, "SKILL.md"))) return candidate;
14375
+ if (fs5.existsSync(path5.join(candidate, "SKILL.md"))) return candidate;
14206
14376
  }
14207
14377
  throw new Error(`Aero skill not found. Searched:
14208
14378
  ${candidates.join("\n ")}`);
14209
14379
  }
14210
14380
 
14211
14381
  // src/agent/skill-tools.ts
14212
- import fs5 from "fs";
14382
+ import fs6 from "fs";
14213
14383
  import path6 from "path";
14214
14384
  import { Type } from "@sinclair/typebox";
14215
14385
  var MAX_DOC_CHARS = 2e4;
@@ -14232,12 +14402,12 @@ function parseDescription(body) {
14232
14402
  }
14233
14403
  function scanSkillDocs(skillDir) {
14234
14404
  const refsDir = path6.join(skillDir ?? resolveAeroSkillDir(), "references");
14235
- if (!fs5.existsSync(refsDir)) return [];
14405
+ if (!fs6.existsSync(refsDir)) return [];
14236
14406
  const entries = [];
14237
- for (const file of fs5.readdirSync(refsDir)) {
14407
+ for (const file of fs6.readdirSync(refsDir)) {
14238
14408
  if (!file.endsWith(".md")) continue;
14239
14409
  const filePath = path6.join(refsDir, file);
14240
- const body = fs5.readFileSync(filePath, "utf-8");
14410
+ const body = fs6.readFileSync(filePath, "utf-8");
14241
14411
  entries.push({
14242
14412
  slug: file.replace(/\.md$/, ""),
14243
14413
  description: parseDescription(body),
@@ -14281,7 +14451,7 @@ function buildReadSkillDocTool() {
14281
14451
  });
14282
14452
  }
14283
14453
  const filePath = path6.join(skillDir, "references", `${match.slug}.md`);
14284
- const content = fs5.readFileSync(filePath, "utf-8");
14454
+ const content = fs6.readFileSync(filePath, "utf-8");
14285
14455
  if (content.length > MAX_DOC_CHARS) {
14286
14456
  return textResult({
14287
14457
  slug: match.slug,
@@ -14306,6 +14476,7 @@ import { Type as Type3 } from "@sinclair/typebox";
14306
14476
 
14307
14477
  // src/agent/mcp-to-agent-tool.ts
14308
14478
  import { Type as Type2 } from "@sinclair/typebox";
14479
+ import { randomUUID } from "crypto";
14309
14480
  var MAX_TOOL_RESULT_CHARS = 2e4;
14310
14481
  var TRUNCATION_NOTE = "... (truncated \u2014 result too large)";
14311
14482
  function serializeResult(value) {
@@ -14455,7 +14626,7 @@ function mcpToAgentTool(tool, ctx) {
14455
14626
  const parameters = Type2.Unsafe(visibleSchema);
14456
14627
  const execute = async (_toolCallId, params) => {
14457
14628
  const handlerInput = hadProject ? { ...params, project: ctx.projectName } : params;
14458
- const result = await tool.handler(ctx.client, handlerInput);
14629
+ const result = await runWithUsageTags(ctx.client, { mcpTool: tool.name, mcpCall: randomUUID() }, () => tool.handler(ctx.client, handlerInput));
14459
14630
  return textResult2(result);
14460
14631
  };
14461
14632
  return {
@@ -14941,9 +15112,9 @@ function ensureBuiltinsRegistered() {
14941
15112
  }
14942
15113
  function loadAeroSystemPrompt(pkgDir) {
14943
15114
  const skillDir = resolveAeroSkillDir(pkgDir);
14944
- const skillBody = fs6.readFileSync(path7.join(skillDir, "SKILL.md"), "utf-8");
15115
+ const skillBody = fs7.readFileSync(path7.join(skillDir, "SKILL.md"), "utf-8");
14945
15116
  const soulPath = path7.join(skillDir, "soul.md");
14946
- const base = fs6.existsSync(soulPath) ? `${fs6.readFileSync(soulPath, "utf-8").trimEnd()}
15117
+ const base = fs7.existsSync(soulPath) ? `${fs7.readFileSync(soulPath, "utf-8").trimEnd()}
14947
15118
 
14948
15119
  ---
14949
15120
 
@@ -14956,7 +15127,7 @@ function appendSystemPromptExtras(base, env = process.env) {
14956
15127
  const filePath = env.AERO_SYSTEM_PROMPT_FILE?.trim();
14957
15128
  if (filePath) {
14958
15129
  try {
14959
- fileBody = fs6.readFileSync(filePath, "utf-8").trim();
15130
+ fileBody = fs7.readFileSync(filePath, "utf-8").trim();
14960
15131
  } catch {
14961
15132
  fileBody = "";
14962
15133
  }
@@ -15969,6 +16140,7 @@ function registerMcpHttpRoutes(scope, opts) {
15969
16140
  const client = new ApiClient(opts.selfApiUrl, sessionKey?.raw ?? bearer, {
15970
16141
  skipProbe: true,
15971
16142
  clientName: `canonry-mcp/${PACKAGE_VERSION}`,
16143
+ surface: "mcp-http",
15972
16144
  // Correlation only: this is never accepted as caller identity or authority.
15973
16145
  actorSession: crypto26.randomUUID()
15974
16146
  });
@@ -15979,7 +16151,8 @@ function registerMcpHttpRoutes(scope, opts) {
15979
16151
  credentialScopes: scopes,
15980
16152
  operator: request.operatorAccess === true,
15981
16153
  tiers: segment.tiers,
15982
- clientFactory: () => client
16154
+ clientFactory: () => client,
16155
+ updateAvailable: opts.getUpdateAvailable
15983
16156
  });
15984
16157
  } catch (error) {
15985
16158
  if (sessionKey) revokeSessionKey(opts.db, sessionKey.id);
@@ -17723,7 +17896,7 @@ function cloneGtmConfig(config) {
17723
17896
  function createGoogleMarketingConfigCredentialStore(input) {
17724
17897
  const persistConfigPatch = input.saveConfigPatch ?? saveConfigPatch;
17725
17898
  const env = input.env ?? process.env;
17726
- const randomUUID = input.randomUUID ?? crypto27.randomUUID;
17899
+ const randomUUID2 = input.randomUUID ?? crypto27.randomUUID;
17727
17900
  const snapshot = () => ({
17728
17901
  googleAds: cloneGoogleAdsConfig(input.config.googleAds),
17729
17902
  gtm: cloneGtmConfig(input.config.gtm)
@@ -17771,7 +17944,7 @@ function createGoogleMarketingConfigCredentialStore(input) {
17771
17944
  upsertGoogleAdsConnection(input.config, {
17772
17945
  projectId: project.id,
17773
17946
  projectName: project.name,
17774
- credentialGeneration: randomUUID(),
17947
+ credentialGeneration: randomUUID2(),
17775
17948
  ...credential.accessToken ? { accessToken: credential.accessToken } : {},
17776
17949
  refreshToken: credential.refreshToken ?? null,
17777
17950
  tokenExpiresAt: credential.expiresAt ?? null,
@@ -17783,7 +17956,7 @@ function createGoogleMarketingConfigCredentialStore(input) {
17783
17956
  upsertGtmConnection(input.config, {
17784
17957
  projectId: project.id,
17785
17958
  projectName: project.name,
17786
- credentialGeneration: randomUUID(),
17959
+ credentialGeneration: randomUUID2(),
17787
17960
  ...credential.accessToken ? { accessToken: credential.accessToken } : {},
17788
17961
  refreshToken: credential.refreshToken ?? null,
17789
17962
  tokenExpiresAt: credential.expiresAt ?? null,
@@ -17896,6 +18069,10 @@ async function createServer(opts) {
17896
18069
  const notifier = new Notifier(opts.db, serverUrl);
17897
18070
  const intelligenceService = new IntelligenceService(opts.db);
17898
18071
  const aeroClient = new ApiClient(opts.config.apiUrl, opts.config.apiKey, {
18072
+ skipProbe: true,
18073
+ surface: "aero"
18074
+ });
18075
+ const schedulerClient = new ApiClient(opts.config.apiUrl, opts.config.apiKey, {
17899
18076
  skipProbe: true
17900
18077
  });
17901
18078
  const agentEnabled = resolveAgentEnabled(process.env, opts.config);
@@ -18028,7 +18205,7 @@ async function createServer(opts) {
18028
18205
  });
18029
18206
  };
18030
18207
  const orphanedOpenClawDir = path8.join(os4.homedir(), ".openclaw-aero");
18031
- if (fs7.existsSync(orphanedOpenClawDir)) {
18208
+ if (fs8.existsSync(orphanedOpenClawDir)) {
18032
18209
  app.log.warn(
18033
18210
  { path: orphanedOpenClawDir },
18034
18211
  "OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
@@ -18336,7 +18513,7 @@ async function createServer(opts) {
18336
18513
  getRunnableProviderNames: () => registry.getAll().map((provider) => provider.adapter.name),
18337
18514
  getEffectiveProviderModels: () => effectiveProviderModels(registry),
18338
18515
  onTrafficSyncRequested: (projectName, sourceId) => {
18339
- aeroClient.trafficSync(projectName, sourceId).catch((err) => {
18516
+ schedulerClient.trafficSync(projectName, sourceId).catch((err) => {
18340
18517
  app.log.error(
18341
18518
  {
18342
18519
  projectName,
@@ -18356,7 +18533,7 @@ async function createServer(opts) {
18356
18533
  onDoctorRequested: (projectName) => {
18357
18534
  void (async () => {
18358
18535
  try {
18359
- const report = await aeroClient.runDoctor({ project: projectName });
18536
+ const report = await schedulerClient.runDoctor({ project: projectName });
18360
18537
  const project = opts.db.select().from(projects).where(eq26(projects.name, projectName)).get();
18361
18538
  if (!project) {
18362
18539
  app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
@@ -18372,7 +18549,7 @@ async function createServer(opts) {
18372
18549
  })();
18373
18550
  },
18374
18551
  onDataRefreshRequested: (projectName) => {
18375
- void refreshAllIntegrations(aeroClient, projectName);
18552
+ void refreshAllIntegrations(schedulerClient, projectName);
18376
18553
  },
18377
18554
  onBacklinksSyncRequested: (projectName) => {
18378
18555
  void (async () => {
@@ -18397,7 +18574,7 @@ async function createServer(opts) {
18397
18574
  );
18398
18575
  return;
18399
18576
  }
18400
- aeroClient.backlinksTriggerSync(probed.release).catch((err) => {
18577
+ schedulerClient.backlinksTriggerSync(probed.release).catch((err) => {
18401
18578
  app.log.error(
18402
18579
  {
18403
18580
  projectName,
@@ -18898,6 +19075,9 @@ async function createServer(opts) {
18898
19075
  await app.register(apiRoutes, {
18899
19076
  db: opts.db,
18900
19077
  routePrefix: apiPrefix,
19078
+ // Agent-surface usage (MCP, Aero, raw API). CLI and dashboard requests are
19079
+ // measured elsewhere and skipped inside.
19080
+ onRequestCompleted: createApiUsageTelemetry(),
18901
19081
  skipAuth: false,
18902
19082
  sessionCookieName: SESSION_COOKIE_NAME,
18903
19083
  resolveSessionApiKeyId,
@@ -18935,7 +19115,7 @@ async function createServer(opts) {
18935
19115
  const configPath = getConfigPath();
18936
19116
  return {
18937
19117
  databasePath: opts.config.database,
18938
- configPath: fs7.existsSync(configPath) ? configPath : null
19118
+ configPath: fs8.existsSync(configPath) ? configPath : null
18939
19119
  };
18940
19120
  })(),
18941
19121
  // Snapshot the bundled skill trees (version + file hashes) so the
@@ -18950,6 +19130,8 @@ async function createServer(opts) {
18950
19130
  }
18951
19131
  })(),
18952
19132
  getAgentPluginState: opts.getAgentPluginState,
19133
+ // Powers the `canonry.version.current` doctor check. Non-blocking.
19134
+ getUpdateStatus: () => getServerUpdateStatus(),
18953
19135
  // Local canonry serve runs on the operator's machine, where pointing a
18954
19136
  // webhook at localhost (Discord test container, Pipedream-mock dev server,
18955
19137
  // etc.) is a legitimate workflow. Default to allowing it for the local
@@ -18968,7 +19150,13 @@ async function createServer(opts) {
18968
19150
  credentials: credentialChecker,
18969
19151
  oauthResourceUrl: publicOrigin ? `${publicOrigin}${`${basePath ?? "/"}api/v1/mcp`.replace("//", "/")}` : void 0,
18970
19152
  registerAuthenticatedRoutes: async (scope) => {
18971
- 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
+ });
18972
19160
  registerOAuthAdminRoutes(scope, { db: opts.db });
18973
19161
  if (!sessionRegistry) return;
18974
19162
  registerAgentRoutes(scope, { db: opts.db, sessionRegistry });
@@ -19428,9 +19616,7 @@ async function createServer(opts) {
19428
19616
  listOperationalLogs: (query) => operationalLogs.list(query),
19429
19617
  getTelemetryStatus,
19430
19618
  setTelemetryEnabled: (enabled) => {
19431
- const config = loadConfig();
19432
- config.telemetry = enabled;
19433
- saveConfigPatch(config);
19619
+ void setTelemetryPreference(enabled, "api");
19434
19620
  opts.config.telemetry = enabled;
19435
19621
  },
19436
19622
  recordOnboardingEvent: (event) => {
@@ -19513,9 +19699,9 @@ async function createServer(opts) {
19513
19699
  return snapshotService.createReport(input);
19514
19700
  }
19515
19701
  });
19516
- const dirname = path8.dirname(fileURLToPath2(import.meta.url));
19702
+ const dirname = path8.dirname(fileURLToPath3(import.meta.url));
19517
19703
  const assetsDir = opts.assetsDir ?? path8.join(dirname, "..", "assets");
19518
- if (fs7.existsSync(assetsDir)) {
19704
+ if (fs8.existsSync(assetsDir)) {
19519
19705
  const indexPath = path8.join(assetsDir, "index.html");
19520
19706
  const injectConfig = (html, projectTabsOverride, themeOverride, renderTokenOverride) => {
19521
19707
  const clientConfig = {};
@@ -19589,8 +19775,8 @@ async function createServer(opts) {
19589
19775
  }
19590
19776
  });
19591
19777
  const serveIndex = (_request, reply) => {
19592
- if (fs7.existsSync(indexPath)) {
19593
- const html = fs7.readFileSync(indexPath, "utf-8");
19778
+ if (fs8.existsSync(indexPath)) {
19779
+ const html = fs8.readFileSync(indexPath, "utf-8");
19594
19780
  return sendSpaDocument(reply, html);
19595
19781
  }
19596
19782
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -19621,8 +19807,8 @@ async function createServer(opts) {
19621
19807
  if (basePath && !url.startsWith(basePath)) {
19622
19808
  return reply.status(404).send({ error: "Not found", path: request.url });
19623
19809
  }
19624
- if (fs7.existsSync(indexPath)) {
19625
- const html = fs7.readFileSync(indexPath, "utf-8");
19810
+ if (fs8.existsSync(indexPath)) {
19811
+ const html = fs8.readFileSync(indexPath, "utf-8");
19626
19812
  return sendSpaDocument(reply, html);
19627
19813
  }
19628
19814
  return reply.status(404).send({ error: "Not found" });
@@ -19762,6 +19948,7 @@ function parseQueryResponse(raw, count2) {
19762
19948
  }
19763
19949
 
19764
19950
  export {
19951
+ cliRuntimeContext,
19765
19952
  trackCliCommandFinished,
19766
19953
  setTelemetrySource,
19767
19954
  getTelemetryStatus,
@@ -19771,6 +19958,7 @@ export {
19771
19958
  showFirstRunNotice,
19772
19959
  detectAndTrackUpgrade,
19773
19960
  trackEvent,
19961
+ setTelemetryPreference,
19774
19962
  backfillAnswerVisibilityCommand,
19775
19963
  backfillNormalizedPaths,
19776
19964
  backfillNormalizedPathsCommand,
@@ -19784,6 +19972,8 @@ export {
19784
19972
  setGoogleAuthConfig,
19785
19973
  formatAuditFactorScore,
19786
19974
  checkLatestVersionForCli,
19975
+ readCachedUpdateAvailable,
19976
+ formatUpdateNotice,
19787
19977
  listAgentProviders,
19788
19978
  coerceAgentProvider,
19789
19979
  AERO_TOOL_PROFILES,