@canonry/canonry 4.128.0 → 4.129.1

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 +15 -10
  2. package/assets/agent-workspace/skills/aero/SKILL.md +15 -7
  3. package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +3 -1
  4. package/assets/agent-workspace/skills/aero/references/orchestration.md +5 -3
  5. package/assets/agent-workspace/skills/aero/soul.md +1 -1
  6. package/assets/agent-workspace/skills/canonry/SKILL.md +50 -13
  7. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +9 -1
  8. package/assets/agent-workspace/skills/canonry/references/indexing.md +4 -2
  9. package/assets/assets/{AuditHistoryPanel-DwJpVWme.js → AuditHistoryPanel-wJqs99Ak.js} +1 -1
  10. package/assets/assets/{BacklinksPage-K8FQUBxI.js → BacklinksPage-BBDExYo_.js} +1 -1
  11. package/assets/assets/{ChartPrimitives-6oFKrJFk.js → ChartPrimitives-DSr5ldMf.js} +1 -1
  12. package/assets/assets/{HistoryPage-DwOAoN1Z.js → HistoryPage-Ca-GGnTI.js} +1 -1
  13. package/assets/assets/{ProjectPage-CdgFUk7h.js → ProjectPage-BuaSDSwf.js} +1 -1
  14. package/assets/assets/{RunRow-lt9IS99U.js → RunRow-O9TbOdQY.js} +1 -1
  15. package/assets/assets/{RunsPage-DitYcIdx.js → RunsPage-C1Sqd7AN.js} +1 -1
  16. package/assets/assets/{SettingsPage-CAOrh65Q.js → SettingsPage-DPFh62BV.js} +1 -1
  17. package/assets/assets/{TrafficPage-BqzNvx75.js → TrafficPage-5vKoQyoP.js} +1 -1
  18. package/assets/assets/TrafficSourceDetailPage-D17oADs8.js +1 -0
  19. package/assets/assets/{arrow-left-AmG4gtUR.js → arrow-left-De5P5AIE.js} +1 -1
  20. package/assets/assets/extract-error-message-BqMd-fQ6.js +1 -0
  21. package/assets/assets/{index-BiCDkOH4.js → index-C1ABEgkr.js} +53 -53
  22. package/assets/assets/{trash-2-gFHWd1A1.js → trash-2-6ZpABj9G.js} +1 -1
  23. package/assets/index.html +1 -1
  24. package/dist/{chunk-ZKUSY4PI.js → chunk-GYAKRVE2.js} +25 -5
  25. package/dist/{chunk-BLSHHRRU.js → chunk-K4FDDCYG.js} +29 -0
  26. package/dist/{chunk-NCFH26X5.js → chunk-LPEE5AYY.js} +198 -25
  27. package/dist/{chunk-IHRW6WWV.js → chunk-WWZE7VDQ.js} +10 -6
  28. package/dist/cli.js +226 -38
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +4 -4
  31. package/dist/{intelligence-service-SH5Z6JFT.js → intelligence-service-2QBAGT5X.js} +2 -2
  32. package/dist/mcp.js +2 -2
  33. package/package.json +7 -7
  34. package/assets/assets/TrafficSourceDetailPage-DXbygZUA.js +0 -1
  35. package/assets/assets/extract-error-message-DlVtjoPz.js +0 -1
@@ -1 +1 @@
1
- import{c}from"./index-BiCDkOH4.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
1
+ import{c}from"./index-C1ABEgkr.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
package/assets/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-BiCDkOH4.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-C1ABEgkr.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Be8JdV5M.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/vendor-radix-CQ77EO2y.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/vendor-recharts-DSxTZhaH.js">
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-IHRW6WWV.js";
13
+ } from "./chunk-WWZE7VDQ.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -116,7 +116,7 @@ import {
116
116
  siteAuditPages,
117
117
  siteAuditSnapshots,
118
118
  usageCounters
119
- } from "./chunk-NCFH26X5.js";
119
+ } from "./chunk-LPEE5AYY.js";
120
120
  import {
121
121
  AGENT_MEMORY_VALUE_MAX_BYTES,
122
122
  AGENT_PROVIDER_IDS,
@@ -186,7 +186,7 @@ import {
186
186
  validationError,
187
187
  winnabilityClassLabel,
188
188
  withRetry
189
- } from "./chunk-BLSHHRRU.js";
189
+ } from "./chunk-K4FDDCYG.js";
190
190
 
191
191
  // src/telemetry.ts
192
192
  import crypto from "crypto";
@@ -6967,7 +6967,7 @@ function readStoredGroundingSources(rawResponse) {
6967
6967
  return result;
6968
6968
  }
6969
6969
  async function backfillInsightsCommand(project, opts) {
6970
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-SH5Z6JFT.js");
6970
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-2QBAGT5X.js");
6971
6971
  const config = loadConfig();
6972
6972
  const db = createClient(config.database);
6973
6973
  migrate(db);
@@ -7776,7 +7776,7 @@ function emitInstallSummary(summary, format) {
7776
7776
  Target: ${summary.targetDir}`);
7777
7777
  console.log(summary.message);
7778
7778
  }
7779
- function getMissingUserSkillsNudge(home) {
7779
+ function getMissingUserSkillsNudge(home, agentPlugin) {
7780
7780
  if (!home) return null;
7781
7781
  const skillsBase = path5.join(home, ".claude", "skills");
7782
7782
  const installed = [];
@@ -7786,6 +7786,25 @@ function getMissingUserSkillsNudge(home) {
7786
7786
  if (existsSafe(skillFile)) installed.push(name);
7787
7787
  else missing.push(name);
7788
7788
  }
7789
+ if (agentPlugin && agentPlugin.configuredClients.length > 0) {
7790
+ const unverifiedClients = agentPlugin.configuredClients.filter((client) => !agentPlugin.verifiedClients.includes(client));
7791
+ const mismatchedClients = agentPlugin.verifiedClients.filter((client) => agentPlugin.verifiedClientVersions?.[client] !== PACKAGE_VERSION);
7792
+ if (unverifiedClients.length === 0 && mismatchedClients.length === 0) return null;
7793
+ const displayClients = (clients) => clients.map((client) => client === "claude-code" ? "Claude Code" : "Codex").join(" + ");
7794
+ const problems = [];
7795
+ if (unverifiedClients.length > 0) {
7796
+ problems.push(`The Canonry plugin is enabled for ${displayClients(unverifiedClients)}, but its cached manifest and skill assets could not be verified.`);
7797
+ }
7798
+ if (mismatchedClients.length > 0) {
7799
+ const versions = mismatchedClients.map((client) => `${client === "claude-code" ? "Claude Code" : "Codex"} v${agentPlugin.verifiedClientVersions?.[client] ?? "unknown"}`).join(", ");
7800
+ problems.push(`${versions} ${mismatchedClients.length === 1 ? "does" : "do"} not match the running Canonry v${PACKAGE_VERSION}.`);
7801
+ }
7802
+ return {
7803
+ message: `Tip: ${problems.join(" ")} Update or reinstall \`canonry@canonry\` with the affected client plugin manager.`,
7804
+ missing,
7805
+ installed
7806
+ };
7807
+ }
7789
7808
  if (missing.length === 0) return null;
7790
7809
  const fix = missing.length === BUNDLED_SKILL_NAMES.length ? "canonry skills install --user" : `canonry skills install ${missing.join(" ")} --user`;
7791
7810
  return {
@@ -12574,6 +12593,7 @@ async function createServer(opts) {
12574
12593
  return void 0;
12575
12594
  }
12576
12595
  })(),
12596
+ getAgentPluginState: opts.getAgentPluginState,
12577
12597
  // Local canonry serve runs on the operator's machine, where pointing a
12578
12598
  // webhook at localhost (Discord test container, Pipedream-mock dev server,
12579
12599
  // etc.) is a legitimate workflow. Default to allowing it for the local
@@ -5995,6 +5995,18 @@ var trafficStatusResponseSchema = z34.object({
5995
5995
  });
5996
5996
  var trafficEventKindSchema = z34.enum(["crawler", "ai-user-fetch", "ai-referral"]);
5997
5997
  var TrafficEventKinds = trafficEventKindSchema.enum;
5998
+ var trafficSeriesGranularitySchema = z34.enum(["hour", "day"]);
5999
+ var TrafficSeriesGranularities = trafficSeriesGranularitySchema.enum;
6000
+ var trafficSeriesPointSchema = z34.object({
6001
+ /**
6002
+ * UTC bucket key: an ISO hour for hourly series, or `YYYY-MM-DD` for daily
6003
+ * series. The response includes zero-value buckets across the full window.
6004
+ */
6005
+ bucket: z34.string(),
6006
+ crawlerHits: z34.number().int().nonnegative(),
6007
+ aiUserFetchHits: z34.number().int().nonnegative(),
6008
+ aiReferralHits: z34.number().int().nonnegative()
6009
+ });
5998
6010
  var trafficCrawlerEventEntrySchema = z34.object({
5999
6011
  kind: z34.literal(TrafficEventKinds.crawler),
6000
6012
  sourceId: z34.string(),
@@ -6050,6 +6062,14 @@ var trafficEventEntrySchema = z34.discriminatedUnion("kind", [
6050
6062
  var trafficEventsResponseSchema = z34.object({
6051
6063
  windowStart: z34.string(),
6052
6064
  windowEnd: z34.string(),
6065
+ /**
6066
+ * Full-window chart data, aggregated independently from the capped detail
6067
+ * rows below. This prevents dense windows from silently losing old buckets.
6068
+ */
6069
+ series: z34.object({
6070
+ granularity: trafficSeriesGranularitySchema,
6071
+ points: z34.array(trafficSeriesPointSchema)
6072
+ }),
6053
6073
  totals: z34.object({
6054
6074
  /** Total classified-crawler hits across the window. UNCHANGED contract. */
6055
6075
  crawlerHits: z34.number().int().nonnegative(),
@@ -6069,6 +6089,13 @@ var trafficEventsResponseSchema = z34.object({
6069
6089
  /** AI-referral sessions ingested before the classifier shipped; unresolvable, never organic. */
6070
6090
  aiReferralUnknownHits: z34.number().int().nonnegative()
6071
6091
  }),
6092
+ eventRows: z34.object({
6093
+ /** Total detail rows matching the window/source/kind filters before `limit`. */
6094
+ total: z34.number().int().nonnegative(),
6095
+ /** Number of newest detail rows included in `events`. */
6096
+ returned: z34.number().int().nonnegative(),
6097
+ truncated: z34.boolean()
6098
+ }),
6072
6099
  events: z34.array(trafficEventEntrySchema)
6073
6100
  });
6074
6101
 
@@ -7420,6 +7447,8 @@ export {
7420
7447
  trafficStatusResponseSchema,
7421
7448
  trafficEventKindSchema,
7422
7449
  TrafficEventKinds,
7450
+ trafficSeriesGranularitySchema,
7451
+ TrafficSeriesGranularities,
7423
7452
  trafficEventsResponseSchema,
7424
7453
  classifyTrafficPath,
7425
7454
  segmentCrawlerHits,
@@ -47,6 +47,7 @@ import {
47
47
  TrafficEventConfidences,
48
48
  TrafficEventKinds,
49
49
  TrafficEvidenceKinds,
50
+ TrafficSeriesGranularities,
50
51
  TrafficSourceAuthModes,
51
52
  TrafficSourceStatuses,
52
53
  TrafficSourceTypes,
@@ -336,7 +337,7 @@ import {
336
337
  wordpressSchemaDeployResultDtoSchema,
337
338
  wordpressSchemaStatusResultDtoSchema,
338
339
  wordpressStatusDtoSchema
339
- } from "./chunk-BLSHHRRU.js";
340
+ } from "./chunk-K4FDDCYG.js";
340
341
 
341
342
  // src/intelligence-service.ts
342
343
  import { eq as eq41, desc as desc18, asc as asc9, and as and32, ne as ne6, or as or8, inArray as inArray15, gte as gte10, lte as lte6 } from "drizzle-orm";
@@ -20791,15 +20792,16 @@ var routeCatalog = [
20791
20792
  method: "get",
20792
20793
  path: "/api/v1/projects/{name}/traffic/events",
20793
20794
  summary: "List rolled-up crawler hits, AI user-fetch hits, and AI-referral sessions within a window",
20794
- description: "Returns hourly rollup rows from `crawler_events_hourly`, `ai_user_fetch_events_hourly`, and `ai_referral_events_hourly`. Defaults to the last 24h. Totals reflect the full window; the `events` array is capped by `limit` (default 500, max 5000).",
20795
+ description: "Returns full-window hourly or daily chart series plus newest-first detail rows from `crawler_events_hourly`, `ai_user_fetch_events_hourly`, and `ai_referral_events_hourly`. Defaults to the last 24h. Totals and `series.points` reflect the full window; only the `events` array is capped by `limit` (default 500, max 5000). `eventRows` reports the true pre-limit row count.",
20795
20796
  tags: ["traffic"],
20796
20797
  parameters: [
20797
20798
  nameParameter,
20798
20799
  { name: "since", in: "query", description: "ISO-8601 window start (defaults to 24h ago).", schema: stringSchema },
20799
20800
  { name: "until", in: "query", description: "ISO-8601 window end (defaults to now).", schema: stringSchema },
20800
20801
  { name: "kind", in: "query", description: 'Filter to "crawler", "ai-user-fetch", "ai-referral", or "all" (default).', schema: stringSchema },
20801
- { name: "limit", in: "query", description: "Max rows per kind in the events array (default 500, max 5000).", schema: stringSchema },
20802
- { name: "sourceId", in: "query", description: "Restrict to a single traffic source.", schema: stringSchema }
20802
+ { name: "limit", in: "query", description: "Max newest combined detail rows in the events array (default 500, max 5000).", schema: stringSchema },
20803
+ { name: "sourceId", in: "query", description: "Restrict to a single traffic source.", schema: stringSchema },
20804
+ { name: "granularity", in: "query", description: 'Full-window series bucket size: "hour" (default) or "day".', schema: { type: "string", enum: ["hour", "day"] } }
20803
20805
  ],
20804
20806
  responses: {
20805
20807
  200: jsonResponse("Events returned with windowed totals.", "TrafficEventsResponse"),
@@ -23338,6 +23340,13 @@ function stableStringify3(value) {
23338
23340
  }
23339
23341
 
23340
23342
  // ../api-routes/src/google.ts
23343
+ function isOnProjectDomain(url, projectDomain) {
23344
+ try {
23345
+ return new URL(url).hostname.toLowerCase().replace(/^www\./, "") === projectDomain;
23346
+ } catch {
23347
+ return false;
23348
+ }
23349
+ }
23341
23350
  function scopesForConnectionType(type) {
23342
23351
  switch (type) {
23343
23352
  case "gsc":
@@ -24142,14 +24151,21 @@ async function googleRoutes(app, opts) {
24142
24151
  latestByUrl.set(row.url, row);
24143
24152
  }
24144
24153
  }
24154
+ const gatherDomain = normalizeProjectDomain(project.canonicalDomain);
24145
24155
  const unindexedUrls = [];
24156
+ let skippedOtherHost = 0;
24146
24157
  for (const [url, row] of latestByUrl) {
24147
- if (row.indexingState !== "INDEXING_ALLOWED") {
24148
- unindexedUrls.push(url);
24158
+ if (row.indexingState === "INDEXING_ALLOWED") continue;
24159
+ if (!isOnProjectDomain(url, gatherDomain)) {
24160
+ skippedOtherHost += 1;
24161
+ continue;
24149
24162
  }
24163
+ unindexedUrls.push(url);
24150
24164
  }
24151
24165
  if (unindexedUrls.length === 0) {
24152
- throw validationError('No unindexed URLs found. Run "canonry google inspect-sitemap" first.');
24166
+ throw validationError(
24167
+ skippedOtherHost > 0 ? `No unindexed URLs found on "${project.canonicalDomain}" (skipped ${skippedOtherHost} on other hosts). Run "canonry google inspect-sitemap" first.` : 'No unindexed URLs found. Run "canonry google inspect-sitemap" first.'
24168
+ );
24153
24169
  }
24154
24170
  urlsToNotify = unindexedUrls;
24155
24171
  }
@@ -24160,14 +24176,7 @@ async function googleRoutes(app, opts) {
24160
24176
  throw validationError(`Cannot request indexing for more than ${INDEXING_API_DAILY_LIMIT} URLs per request (got ${urlsToNotify.length})`);
24161
24177
  }
24162
24178
  const projectDomain = normalizeProjectDomain(project.canonicalDomain);
24163
- const invalidUrls = urlsToNotify.filter((url) => {
24164
- try {
24165
- const hostname = new URL(url).hostname.toLowerCase().replace(/^www\./, "");
24166
- return hostname !== projectDomain;
24167
- } catch {
24168
- return true;
24169
- }
24170
- });
24179
+ const invalidUrls = urlsToNotify.filter((url) => !isOnProjectDomain(url, projectDomain));
24171
24180
  if (invalidUrls.length > 0) {
24172
24181
  throw validationError(
24173
24182
  `URLs must belong to project domain "${project.canonicalDomain}". Invalid: ${invalidUrls.slice(0, 5).join(", ")}`
@@ -36915,6 +36924,35 @@ var DEFAULT_TRAFFIC_SYNC_CRON = "*/30 * * * *";
36915
36924
  var MAX_BACKFILL_DAYS = 90;
36916
36925
  var BACKFILL_MAX_PAGES = 1e3;
36917
36926
  var BACKFILL_SAMPLE_LIMIT = 500;
36927
+ function emptyTrafficSeriesPoint(bucket) {
36928
+ return { bucket, crawlerHits: 0, aiUserFetchHits: 0, aiReferralHits: 0 };
36929
+ }
36930
+ function completeTrafficSeries(windowStart, windowEnd, granularity, pointsByBucket) {
36931
+ const cursor = new Date(windowStart);
36932
+ const last = new Date(windowEnd);
36933
+ if (granularity === TrafficSeriesGranularities.day) {
36934
+ cursor.setUTCHours(0, 0, 0, 0);
36935
+ last.setUTCHours(0, 0, 0, 0);
36936
+ } else {
36937
+ cursor.setUTCMinutes(0, 0, 0);
36938
+ last.setUTCMinutes(0, 0, 0);
36939
+ }
36940
+ const points = [];
36941
+ while (cursor <= last) {
36942
+ const bucket = granularity === TrafficSeriesGranularities.day ? cursor.toISOString().slice(0, 10) : cursor.toISOString();
36943
+ points.push(pointsByBucket.get(bucket) ?? emptyTrafficSeriesPoint(bucket));
36944
+ if (granularity === TrafficSeriesGranularities.day) cursor.setUTCDate(cursor.getUTCDate() + 1);
36945
+ else cursor.setUTCHours(cursor.getUTCHours() + 1);
36946
+ }
36947
+ return points;
36948
+ }
36949
+ function trafficSeriesPoint(pointsByBucket, bucket) {
36950
+ const existing = pointsByBucket.get(bucket);
36951
+ if (existing) return existing;
36952
+ const created = emptyTrafficSeriesPoint(bucket);
36953
+ pointsByBucket.set(bucket, created);
36954
+ return created;
36955
+ }
36918
36956
  function parseSourceConfig(row) {
36919
36957
  return row.configJson;
36920
36958
  }
@@ -38290,6 +38328,17 @@ async function trafficRoutes(app, opts) {
38290
38328
  throw validationError('"limit" must be a positive integer');
38291
38329
  }
38292
38330
  const limit = Math.min(requestedLimit, 5e3);
38331
+ const granularityParam = request.query?.granularity;
38332
+ let granularity = TrafficSeriesGranularities.hour;
38333
+ if (granularityParam !== void 0) {
38334
+ if (granularityParam === TrafficSeriesGranularities.hour || granularityParam === TrafficSeriesGranularities.day) {
38335
+ granularity = granularityParam;
38336
+ } else {
38337
+ throw validationError(
38338
+ `"granularity" must be one of: ${TrafficSeriesGranularities.hour}, ${TrafficSeriesGranularities.day}`
38339
+ );
38340
+ }
38341
+ }
38293
38342
  const sourceIdParam = request.query?.sourceId;
38294
38343
  const sinceIso = since.toISOString();
38295
38344
  const untilIso = until.toISOString();
@@ -38298,6 +38347,8 @@ async function trafficRoutes(app, opts) {
38298
38347
  let crawlerSegments = { content: 0, sitemap: 0, robots: 0, asset: 0, other: 0 };
38299
38348
  let aiUserFetchTotal = 0;
38300
38349
  let aiReferralCounts = aiReferralClassCounts(0, 0, 0);
38350
+ let totalEventRows = 0;
38351
+ const seriesByBucket = /* @__PURE__ */ new Map();
38301
38352
  if (kind === "all" || kind === TrafficEventKinds.crawler) {
38302
38353
  const crawlerFilters = [
38303
38354
  eq31(crawlerEventsHourly.projectId, project.id),
@@ -38308,12 +38359,22 @@ async function trafficRoutes(app, opts) {
38308
38359
  const crawlerWhere = and26(...crawlerFilters);
38309
38360
  const pathTotals = app.db.select({
38310
38361
  pathNormalized: crawlerEventsHourly.pathNormalized,
38311
- hits: sql14`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38362
+ hits: sql14`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
38363
+ rows: sql14`COUNT(*)`
38312
38364
  }).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerEventsHourly.pathNormalized).all();
38313
38365
  crawlerSegments = segmentCrawlerHits(
38314
38366
  pathTotals.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
38315
38367
  );
38316
38368
  crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
38369
+ totalEventRows += pathTotals.reduce((sum, row) => sum + Number(row.rows), 0);
38370
+ const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql14`substr(${crawlerEventsHourly.tsHour}, 1, 10)` : crawlerEventsHourly.tsHour;
38371
+ const crawlerSeries = app.db.select({
38372
+ bucket: crawlerSeriesBucket,
38373
+ hits: sql14`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38374
+ }).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerSeriesBucket).all();
38375
+ for (const row of crawlerSeries) {
38376
+ trafficSeriesPoint(seriesByBucket, row.bucket).crawlerHits = Number(row.hits);
38377
+ }
38317
38378
  const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc15(crawlerEventsHourly.tsHour)).limit(limit).all();
38318
38379
  for (const r of rows) {
38319
38380
  events.push({
@@ -38338,8 +38399,20 @@ async function trafficRoutes(app, opts) {
38338
38399
  ];
38339
38400
  if (sourceIdParam) userFetchFilters.push(eq31(aiUserFetchEventsHourly.sourceId, sourceIdParam));
38340
38401
  const userFetchWhere = and26(...userFetchFilters);
38341
- const total = app.db.select({ total: sql14`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
38402
+ const total = app.db.select({
38403
+ total: sql14`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`,
38404
+ rows: sql14`COUNT(*)`
38405
+ }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
38342
38406
  aiUserFetchTotal = Number(total?.total ?? 0);
38407
+ totalEventRows += Number(total?.rows ?? 0);
38408
+ const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql14`substr(${aiUserFetchEventsHourly.tsHour}, 1, 10)` : aiUserFetchEventsHourly.tsHour;
38409
+ const userFetchSeries = app.db.select({
38410
+ bucket: userFetchSeriesBucket,
38411
+ hits: sql14`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
38412
+ }).from(aiUserFetchEventsHourly).where(userFetchWhere).groupBy(userFetchSeriesBucket).all();
38413
+ for (const row of userFetchSeries) {
38414
+ trafficSeriesPoint(seriesByBucket, row.bucket).aiUserFetchHits = Number(row.hits);
38415
+ }
38343
38416
  const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc15(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
38344
38417
  for (const r of rows) {
38345
38418
  events.push({
@@ -38366,13 +38439,23 @@ async function trafficRoutes(app, opts) {
38366
38439
  const total = app.db.select({
38367
38440
  total: sql14`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
38368
38441
  paid: sql14`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
38369
- organic: sql14`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`
38442
+ organic: sql14`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`,
38443
+ rows: sql14`COUNT(*)`
38370
38444
  }).from(aiReferralEventsHourly).where(aiWhere).get();
38371
38445
  aiReferralCounts = aiReferralClassCounts(
38372
38446
  Number(total?.total ?? 0),
38373
38447
  Number(total?.paid ?? 0),
38374
38448
  Number(total?.organic ?? 0)
38375
38449
  );
38450
+ totalEventRows += Number(total?.rows ?? 0);
38451
+ const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql14`substr(${aiReferralEventsHourly.tsHour}, 1, 10)` : aiReferralEventsHourly.tsHour;
38452
+ const referralSeries = app.db.select({
38453
+ bucket: referralSeriesBucket,
38454
+ hits: sql14`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
38455
+ }).from(aiReferralEventsHourly).where(aiWhere).groupBy(referralSeriesBucket).all();
38456
+ for (const row of referralSeries) {
38457
+ trafficSeriesPoint(seriesByBucket, row.bucket).aiReferralHits = Number(row.hits);
38458
+ }
38376
38459
  const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc15(aiReferralEventsHourly.tsHour)).limit(limit).all();
38377
38460
  for (const r of rows) {
38378
38461
  const counts2 = aiReferralClassCounts(r.sessionsOrHits, r.paidSessionsOrHits, r.organicSessionsOrHits);
@@ -38398,6 +38481,10 @@ async function trafficRoutes(app, opts) {
38398
38481
  const response = {
38399
38482
  windowStart: sinceIso,
38400
38483
  windowEnd: untilIso,
38484
+ series: {
38485
+ granularity,
38486
+ points: completeTrafficSeries(since, until, granularity, seriesByBucket)
38487
+ },
38401
38488
  totals: {
38402
38489
  crawlerHits: crawlerTotal,
38403
38490
  crawlerContentHits: crawlerSegments.content,
@@ -38409,6 +38496,11 @@ async function trafficRoutes(app, opts) {
38409
38496
  aiReferralOrganicHits: aiReferralCounts.organic,
38410
38497
  aiReferralUnknownHits: aiReferralCounts.unknown
38411
38498
  },
38499
+ eventRows: {
38500
+ total: totalEventRows,
38501
+ returned: trimmed.length,
38502
+ truncated: trimmed.length < totalEventRows
38503
+ },
38412
38504
  events: trimmed
38413
38505
  };
38414
38506
  return response;
@@ -38420,12 +38512,47 @@ import crypto29 from "crypto";
38420
38512
  import fs6 from "fs";
38421
38513
  import path7 from "path";
38422
38514
  var REQUIRED_SKILLS = ["canonry", "aero"];
38515
+ function pluginStateFor(ctx) {
38516
+ try {
38517
+ return ctx.getAgentPluginState?.();
38518
+ } catch {
38519
+ return void 0;
38520
+ }
38521
+ }
38522
+ function unverifiedPluginClients(state) {
38523
+ if (!state) return [];
38524
+ return state.configuredClients.filter((client) => !state.verifiedClients.includes(client));
38525
+ }
38526
+ function displayPluginClients(clients) {
38527
+ return clients.map((client) => client === "claude-code" ? "Claude Code" : "Codex").join(" + ");
38528
+ }
38423
38529
  var skillsInstalledCheck = {
38424
38530
  id: "agent.skills.installed",
38425
38531
  category: CheckCategories.agent,
38426
38532
  scope: CheckScopes.global,
38427
- title: "Agent skills installed (~/.claude/skills/)",
38428
- run: () => {
38533
+ title: "Agent skills available",
38534
+ run: (ctx) => {
38535
+ const agentPlugin = pluginStateFor(ctx);
38536
+ const unverifiedClients = unverifiedPluginClients(agentPlugin);
38537
+ if (unverifiedClients.length > 0) {
38538
+ return {
38539
+ status: CheckStatuses.warn,
38540
+ code: "agent.skills.plugin-unverified",
38541
+ summary: `The Canonry plugin is enabled for ${displayPluginClients(unverifiedClients)}, but its cached manifest and skill assets could not be verified.`,
38542
+ remediation: "Reinstall `canonry@canonry` with the affected client plugin manager, or disable the stale plugin entry.",
38543
+ details: { delivery: "plugin", ...agentPlugin, unverifiedClients }
38544
+ };
38545
+ }
38546
+ if (agentPlugin && agentPlugin.configuredClients.length > 0) {
38547
+ const clients = displayPluginClients(agentPlugin.verifiedClients);
38548
+ return {
38549
+ status: CheckStatuses.ok,
38550
+ code: "agent.skills.plugin-installed",
38551
+ summary: `Canonry skills are available through the verified native ${clients} plugin${agentPlugin.verifiedClients.length === 1 ? "" : "s"}.`,
38552
+ remediation: null,
38553
+ details: { delivery: "plugin", ...agentPlugin }
38554
+ };
38555
+ }
38429
38556
  const home = process.env.HOME;
38430
38557
  if (!home) {
38431
38558
  return {
@@ -38446,7 +38573,8 @@ var skillsInstalledCheck = {
38446
38573
  const details = {
38447
38574
  checkedPath: skillsBase,
38448
38575
  installed,
38449
- missing
38576
+ missing,
38577
+ ...agentPlugin ? { agentPlugin } : {}
38450
38578
  };
38451
38579
  if (missing.length === 0) {
38452
38580
  return {
@@ -38479,9 +38607,51 @@ var skillsCurrentCheck = {
38479
38607
  id: "agent.skills.current",
38480
38608
  category: CheckCategories.agent,
38481
38609
  scope: CheckScopes.global,
38482
- title: "Agent skills up to date (~/.claude/skills/)",
38610
+ title: "Agent skills up to date",
38483
38611
  run: (ctx) => {
38612
+ const agentPlugin = pluginStateFor(ctx);
38484
38613
  const bundled = ctx.bundledSkills;
38614
+ const unverifiedClients = unverifiedPluginClients(agentPlugin);
38615
+ if (unverifiedClients.length > 0) {
38616
+ return {
38617
+ status: CheckStatuses.warn,
38618
+ code: "agent.skills.plugin-unverified",
38619
+ summary: `The Canonry plugin cache could not be verified for ${displayPluginClients(unverifiedClients)}; freshness cannot be assessed.`,
38620
+ remediation: "Reinstall `canonry@canonry` with the affected client plugin manager, or disable the stale plugin entry.",
38621
+ details: { delivery: "plugin", ...agentPlugin, unverifiedClients }
38622
+ };
38623
+ }
38624
+ if (agentPlugin && agentPlugin.configuredClients.length > 0) {
38625
+ const clients = displayPluginClients(agentPlugin.verifiedClients);
38626
+ if (!bundled || bundled.length === 0) {
38627
+ return {
38628
+ status: CheckStatuses.skipped,
38629
+ code: "agent.skills.bundle-unavailable",
38630
+ summary: `Canonry skills are supplied by the verified native ${clients} plugin${agentPlugin.verifiedClients.length === 1 ? "" : "s"}, but the running bundle version is unavailable for comparison.`,
38631
+ remediation: null,
38632
+ details: { delivery: "plugin", ...agentPlugin }
38633
+ };
38634
+ }
38635
+ const bundledVersion2 = bundled[0].version;
38636
+ const mismatchedClients = agentPlugin.verifiedClients.filter((client) => agentPlugin.verifiedClientVersions?.[client] !== bundledVersion2);
38637
+ if (mismatchedClients.length > 0) {
38638
+ const versions = mismatchedClients.map((client) => `${client === "claude-code" ? "Claude Code" : "Codex"} v${agentPlugin.verifiedClientVersions?.[client] ?? "unknown"}`).join(", ");
38639
+ return {
38640
+ status: CheckStatuses.warn,
38641
+ code: "agent.skills.plugin-version-mismatch",
38642
+ summary: `${versions} ${mismatchedClients.length === 1 ? "does" : "do"} not match the running Canonry v${bundledVersion2}; plugin skills may be stale or incompatible.`,
38643
+ remediation: "Update the Canonry runtime and `canonry@canonry` plugin to the same version with the affected client plugin manager.",
38644
+ details: { delivery: "plugin", bundledVersion: bundledVersion2, ...agentPlugin, mismatchedClients }
38645
+ };
38646
+ }
38647
+ return {
38648
+ status: CheckStatuses.ok,
38649
+ code: "agent.skills.plugin-current",
38650
+ summary: `Canonry skills are supplied by the verified native ${clients} plugin${agentPlugin.verifiedClients.length === 1 ? "" : "s"} at the running version (v${bundledVersion2}).`,
38651
+ remediation: null,
38652
+ details: { delivery: "plugin", bundledVersion: bundledVersion2, ...agentPlugin }
38653
+ };
38654
+ }
38485
38655
  if (!bundled || bundled.length === 0) {
38486
38656
  return {
38487
38657
  status: CheckStatuses.skipped,
@@ -40485,7 +40655,8 @@ async function doctorRoutes(app, opts) {
40485
40655
  getAgentProviderSummary: opts.getAgentProviderSummary,
40486
40656
  trafficSourceValidators: opts.trafficSourceValidators,
40487
40657
  runtimeStatePaths: opts.runtimeStatePaths,
40488
- bundledSkills: opts.bundledSkills
40658
+ bundledSkills: opts.bundledSkills,
40659
+ getAgentPluginState: opts.getAgentPluginState
40489
40660
  };
40490
40661
  return runChecks(ctx, ALL_CHECKS, { checkIds });
40491
40662
  });
@@ -40512,7 +40683,8 @@ async function doctorRoutes(app, opts) {
40512
40683
  getAgentProviderSummary: opts.getAgentProviderSummary,
40513
40684
  trafficSourceValidators: opts.trafficSourceValidators,
40514
40685
  runtimeStatePaths: opts.runtimeStatePaths,
40515
- bundledSkills: opts.bundledSkills
40686
+ bundledSkills: opts.bundledSkills,
40687
+ getAgentPluginState: opts.getAgentPluginState
40516
40688
  };
40517
40689
  return runChecks(ctx, ALL_CHECKS, { checkIds });
40518
40690
  });
@@ -41554,7 +41726,8 @@ async function apiRoutes(app, opts) {
41554
41726
  getAgentProviderSummary: opts.getAgentProviderSummary,
41555
41727
  trafficSourceValidators: buildTrafficSourceValidators(opts),
41556
41728
  runtimeStatePaths: opts.runtimeStatePaths,
41557
- bundledSkills: opts.bundledSkills
41729
+ bundledSkills: opts.bundledSkills,
41730
+ getAgentPluginState: opts.getAgentPluginState
41558
41731
  });
41559
41732
  if (opts.registerAuthenticatedRoutes) {
41560
41733
  await opts.registerAuthenticatedRoutes(api);
@@ -36,8 +36,9 @@ import {
36
36
  trafficConnectCloudRunRequestSchema,
37
37
  trafficConnectVercelRequestSchema,
38
38
  trafficConnectWordpressRequestSchema,
39
- trafficEventKindSchema
40
- } from "./chunk-BLSHHRRU.js";
39
+ trafficEventKindSchema,
40
+ trafficSeriesGranularitySchema
41
+ } from "./chunk-K4FDDCYG.js";
41
42
 
42
43
  // src/config.ts
43
44
  import fs from "fs";
@@ -5335,7 +5336,8 @@ var ApiClient = class {
5335
5336
  until: params?.until,
5336
5337
  kind: params?.kind,
5337
5338
  limit: params?.limit !== void 0 ? String(params.limit) : void 0,
5338
- sourceId: params?.sourceId
5339
+ sourceId: params?.sourceId,
5340
+ granularity: params?.granularity
5339
5341
  }
5340
5342
  })
5341
5343
  );
@@ -6190,9 +6192,10 @@ var trafficEventsInputSchema = z2.object({
6190
6192
  project: projectNameSchema,
6191
6193
  since: z2.string().optional().describe("ISO 8601 lower bound. Defaults to 24h ago when omitted."),
6192
6194
  until: z2.string().optional().describe("ISO 8601 upper bound. Defaults to now when omitted."),
6193
- kind: z2.union([trafficEventKindSchema, z2.literal("all")]).optional().describe('Filter to "crawler" or "ai-referral"; "all" (default) returns both.'),
6195
+ kind: z2.union([trafficEventKindSchema, z2.literal("all")]).optional().describe('Filter to one traffic kind; "all" (default) returns every kind.'),
6194
6196
  sourceId: z2.string().min(1).optional().describe("Restrict to a single traffic source ID."),
6195
- limit: z2.number().int().positive().max(5e3).optional().describe("Max combined rows. Defaults to 500, max 5000. Totals always reflect the full window.")
6197
+ limit: z2.number().int().positive().max(5e3).optional().describe("Max combined rows. Defaults to 500, max 5000. Totals always reflect the full window."),
6198
+ granularity: trafficSeriesGranularitySchema.optional().describe("Full-window chart series bucket size: hour (default) or day.")
6196
6199
  });
6197
6200
  var trafficSourceIdInputSchema = z2.object({
6198
6201
  project: projectNameSchema,
@@ -7110,7 +7113,7 @@ var canonryMcpTools = [
7110
7113
  defineTool({
7111
7114
  name: "canonry_traffic_events",
7112
7115
  title: "List traffic events",
7113
- description: 'Read crawler and AI-referral hourly rollups from server-side traffic sources. Returns a discriminated list (kind="crawler" rows carry botId/operator/verificationStatus; kind="ai-referral" rows carry product/sourceDomain/evidenceType) plus totals over the full window even when limit truncates rows. Window defaults to last 24h.',
7116
+ description: "Read crawler, AI user-fetch, and AI-referral rollups from server-side traffic sources. Returns complete full-window chart series and totals plus a capped discriminated detail list with explicit truncation metadata. Window defaults to last 24h.",
7114
7117
  access: "read",
7115
7118
  tier: "traffic",
7116
7119
  inputSchema: trafficEventsInputSchema,
@@ -7123,6 +7126,7 @@ var canonryMcpTools = [
7123
7126
  if (input.kind) params.kind = input.kind;
7124
7127
  if (input.sourceId) params.sourceId = input.sourceId;
7125
7128
  if (input.limit !== void 0) params.limit = input.limit;
7129
+ if (input.granularity) params.granularity = input.granularity;
7126
7130
  return client2.trafficListEvents(input.project, params);
7127
7131
  }
7128
7132
  }),