@ainyc/canonry 4.102.1 → 4.103.0

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 (23) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +5 -3
  2. package/assets/assets/{BacklinksPage-CoPo93LS.js → BacklinksPage-CAzeiWX-.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-DqBMtYpk.js → ChartPrimitives-Cl91UJcT.js} +1 -1
  4. package/assets/assets/{ProjectPage-DSJElQg_.js → ProjectPage-6n6qXa-G.js} +1 -1
  5. package/assets/assets/{RunRow-2HNKAfK9.js → RunRow-DqumjxZP.js} +1 -1
  6. package/assets/assets/{RunsPage-Dw7ZIp-K.js → RunsPage-DOdjY-ia.js} +1 -1
  7. package/assets/assets/{SettingsPage-CYmxSK3g.js → SettingsPage-DYW3Lfre.js} +1 -1
  8. package/assets/assets/{TrafficPage-BI8BNANb.js → TrafficPage-DX8A16LI.js} +1 -1
  9. package/assets/assets/{TrafficSourceDetailPage-rdvZpta7.js → TrafficSourceDetailPage-COLKpEny.js} +1 -1
  10. package/assets/assets/{arrow-left-C37crrqh.js → arrow-left-DsQ3GWJh.js} +1 -1
  11. package/assets/assets/{extract-error-message-DZnbGnSd.js → extract-error-message-DQEs2_vw.js} +1 -1
  12. package/assets/assets/{index-sh-T-1TA.js → index-CY4Xe0F8.js} +43 -43
  13. package/assets/assets/{trash-2-UvZu9cfJ.js → trash-2-BARaYs9Y.js} +1 -1
  14. package/assets/index.html +1 -1
  15. package/dist/{chunk-2FUODJQR.js → chunk-23DCY7DL.js} +14 -8
  16. package/dist/{chunk-3PVCILSN.js → chunk-BTJY73GW.js} +4 -4
  17. package/dist/{chunk-B2H2RRIT.js → chunk-GNMZTR6Y.js} +102 -16
  18. package/dist/{chunk-EOQXB63S.js → chunk-Q7FFDQU4.js} +33 -2
  19. package/dist/cli.js +24 -7
  20. package/dist/index.js +4 -4
  21. package/dist/{intelligence-service-RC4BY5HF.js → intelligence-service-GM3RXE2F.js} +2 -2
  22. package/dist/mcp.js +2 -2
  23. package/package.json +9 -9
@@ -1 +1 @@
1
- import{c}from"./index-sh-T-1TA.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-CY4Xe0F8.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-sh-T-1TA.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-CY4Xe0F8.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-C9EZkgbP.js">
@@ -24,7 +24,7 @@ import {
24
24
  trafficConnectVercelRequestSchema,
25
25
  trafficConnectWordpressRequestSchema,
26
26
  trafficEventKindSchema
27
- } from "./chunk-EOQXB63S.js";
27
+ } from "./chunk-Q7FFDQU4.js";
28
28
 
29
29
  // src/config.ts
30
30
  import fs from "fs";
@@ -5163,7 +5163,9 @@ var ApiClient = class {
5163
5163
  since: opts.since,
5164
5164
  until: opts.until,
5165
5165
  lastRuns: opts.lastRuns,
5166
- groupBy: opts.groupBy
5166
+ groupBy: opts.groupBy,
5167
+ month: opts.month,
5168
+ shareOfVoice: opts.shareOfVoice ? "1" : void 0
5167
5169
  }
5168
5170
  })
5169
5171
  );
@@ -5886,15 +5888,17 @@ var canonryMcpTools = [
5886
5888
  defineTool({
5887
5889
  name: "canonry_visibility_stats",
5888
5890
  title: "Get aggregated mention/citation stats",
5889
- description: 'Per-query mention (answer-text) and citation (source-list) counts WITH a sample size, pooled across many answer-visibility runs (probe-excluded) \u2014 the data to compute a confidence-aware (Wilson) proportion or detect drift without fetching every run. Tri-state aware: `checked` (the n for the mention proportion) counts only snapshots where answerMentioned was recorded; `null` ("not checked") is excluded, never counted as not-mentioned. Returns per-query `total`/`checked`/`mentioned`/`cited` + derived `mentionRate` (mentioned/checked) and `citedRate` (cited/total), `firstObserved`/`lastObserved`, and pooled `totals`. Window with `since`/`until` (ISO) OR `lastRuns` (mutually exclusive); with none set, EVERY completed/partial run is pooled (`window.runCount` says how many) \u2014 pass `lastRuns` for a recent sample. Set `groupBy=provider` for a per-provider breakdown whose counts sum to the pooled counts (`groupBy` is omitted from the response otherwise).',
5891
+ description: 'Per-query mention (answer-text) and citation (source-list) counts WITH a sample size, pooled across many answer-visibility runs (probe-excluded) \u2014 the data to compute a confidence-aware (Wilson) proportion or detect drift without fetching every run. Tri-state aware: `checked` (the n for the mention proportion) counts only snapshots where answerMentioned was recorded; `null` ("not checked") is excluded, never counted as not-mentioned. Returns per-query `total`/`checked`/`mentioned`/`cited` + derived `mentionRate` (mentioned/checked) and `citedRate` (cited/total), `firstObserved`/`lastObserved`, and pooled `totals`. Window with `since`/`until` (ISO), `lastRuns`, or `month=YYYY-MM` (mutually exclusive); with none set, EVERY completed/partial run is pooled (`window.runCount` says how many) \u2014 pass `lastRuns` for a recent sample. Set `groupBy=provider` for a per-provider breakdown whose counts sum to the pooled counts (`groupBy` is omitted from the response otherwise). Set `shareOfVoice=true` for pooled project-vs-tracked-competitor brand-mention share across the same attributed snapshot set.',
5890
5892
  access: "read",
5891
5893
  tier: "monitoring",
5892
5894
  inputSchema: z2.object({
5893
5895
  project: projectNameSchema,
5894
- since: z2.string().optional().describe("Inclusive lower bound on run createdAt (ISO 8601). A date-only value (YYYY-MM-DD) is the start of that UTC day. Mutually exclusive with lastRuns."),
5895
- until: z2.string().optional().describe("Inclusive upper bound on run createdAt (ISO 8601). A date-only value (YYYY-MM-DD) covers the whole UTC day (through 23:59:59.999). Mutually exclusive with lastRuns."),
5896
- lastRuns: z2.number().int().positive().optional().describe("Aggregate only the most recent N answer-visibility runs. Mutually exclusive with since/until."),
5897
- groupBy: z2.enum(["provider"]).optional().describe('Set to "provider" for a per-provider breakdown.')
5896
+ since: z2.string().optional().describe("Inclusive lower bound on run createdAt (ISO 8601). A date-only value (YYYY-MM-DD) is the start of that UTC day. Mutually exclusive with lastRuns/month."),
5897
+ until: z2.string().optional().describe("Inclusive upper bound on run createdAt (ISO 8601). A date-only value (YYYY-MM-DD) covers the whole UTC day (through 23:59:59.999). Mutually exclusive with lastRuns/month."),
5898
+ lastRuns: z2.number().int().positive().optional().describe("Aggregate only the most recent N answer-visibility runs. Mutually exclusive with since/until/month."),
5899
+ month: z2.string().optional().describe("Aggregate one calendar month (YYYY-MM), expanded to that month's inclusive UTC bounds. Mutually exclusive with since/until/lastRuns."),
5900
+ groupBy: z2.enum(["provider"]).optional().describe('Set to "provider" for a per-provider breakdown.'),
5901
+ shareOfVoice: z2.boolean().optional().describe("Include pooled project-vs-tracked-competitor brand-mention share across the same window.")
5898
5902
  }),
5899
5903
  annotations: readAnnotations(),
5900
5904
  openApiOperations: ["GET /api/v1/projects/{name}/visibility-stats"],
@@ -5902,7 +5906,9 @@ var canonryMcpTools = [
5902
5906
  since: input.since,
5903
5907
  until: input.until,
5904
5908
  lastRuns: input.lastRuns,
5905
- groupBy: input.groupBy
5909
+ groupBy: input.groupBy,
5910
+ month: input.month,
5911
+ shareOfVoice: input.shareOfVoice
5906
5912
  })
5907
5913
  }),
5908
5914
  defineTool({
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-2FUODJQR.js";
13
+ } from "./chunk-23DCY7DL.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -115,7 +115,7 @@ import {
115
115
  siteAuditPages,
116
116
  siteAuditSnapshots,
117
117
  usageCounters
118
- } from "./chunk-B2H2RRIT.js";
118
+ } from "./chunk-GNMZTR6Y.js";
119
119
  import {
120
120
  AGENT_MEMORY_VALUE_MAX_BYTES,
121
121
  AGENT_PROVIDER_IDS,
@@ -176,7 +176,7 @@ import {
176
176
  validationError,
177
177
  winnabilityClassLabel,
178
178
  withRetry
179
- } from "./chunk-EOQXB63S.js";
179
+ } from "./chunk-Q7FFDQU4.js";
180
180
 
181
181
  // src/telemetry.ts
182
182
  import crypto from "crypto";
@@ -6438,7 +6438,7 @@ function readStoredGroundingSources(rawResponse) {
6438
6438
  return result;
6439
6439
  }
6440
6440
  async function backfillInsightsCommand(project, opts) {
6441
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-RC4BY5HF.js");
6441
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-GM3RXE2F.js");
6442
6442
  const config = loadConfig();
6443
6443
  const db = createClient(config.database);
6444
6444
  migrate(db);
@@ -73,6 +73,7 @@ import {
73
73
  brandLabelFromDomain,
74
74
  brandMetricsDtoSchema,
75
75
  buildHarvestAnchorTerms,
76
+ calendarMonthBounds,
76
77
  categorizeSource,
77
78
  categorizeSourceWithCompetitors,
78
79
  categoryLabel,
@@ -263,7 +264,7 @@ import {
263
264
  wordpressSchemaDeployResultDtoSchema,
264
265
  wordpressSchemaStatusResultDtoSchema,
265
266
  wordpressStatusDtoSchema
266
- } from "./chunk-EOQXB63S.js";
267
+ } from "./chunk-Q7FFDQU4.js";
267
268
 
268
269
  // src/intelligence-service.ts
269
270
  import { eq as eq38, desc as desc18, asc as asc6, and as and28, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
@@ -13258,22 +13259,29 @@ function providerEntries(byProvider) {
13258
13259
  lastObserved: agg.last ?? ""
13259
13260
  })).sort((a, b) => a.provider.localeCompare(b.provider));
13260
13261
  }
13262
+ function buildQueryAttribution(projectQueries) {
13263
+ const byId = /* @__PURE__ */ new Map();
13264
+ const byText = /* @__PURE__ */ new Map();
13265
+ for (const q of projectQueries) {
13266
+ byId.set(q.id, q);
13267
+ byText.set(q.query, q);
13268
+ }
13269
+ return { byId, byText };
13270
+ }
13271
+ function resolveCurrentQuery(attribution, snap) {
13272
+ if (snap.queryId && attribution.byId.has(snap.queryId)) return attribution.byId.get(snap.queryId);
13273
+ if (snap.queryText && attribution.byText.has(snap.queryText)) return attribution.byText.get(snap.queryText);
13274
+ return void 0;
13275
+ }
13261
13276
  function computeVisibilityStats(input) {
13262
13277
  const { groupBy } = input;
13263
13278
  const wantProviders = groupBy === "provider";
13264
- const queryById = /* @__PURE__ */ new Map();
13265
- const queryByText = /* @__PURE__ */ new Map();
13266
- for (const q of input.queries) {
13267
- queryById.set(q.id, q);
13268
- queryByText.set(q.query, q);
13269
- }
13279
+ const attribution = buildQueryAttribution(input.queries);
13270
13280
  const byQuery = /* @__PURE__ */ new Map();
13271
13281
  const totals = emptyAgg();
13272
13282
  const totalsByProvider = /* @__PURE__ */ new Map();
13273
13283
  for (const snap of input.snapshots) {
13274
- let resolved;
13275
- if (snap.queryId && queryById.has(snap.queryId)) resolved = queryById.get(snap.queryId);
13276
- else if (snap.queryText && queryByText.has(snap.queryText)) resolved = queryByText.get(snap.queryText);
13284
+ const resolved = resolveCurrentQuery(attribution, snap);
13277
13285
  if (!resolved) continue;
13278
13286
  let bucket = byQuery.get(resolved.id);
13279
13287
  if (!bucket) {
@@ -13308,7 +13316,14 @@ function computeVisibilityStats(input) {
13308
13316
  async function visibilityStatsRoutes(app) {
13309
13317
  app.get("/projects/:name/visibility-stats", async (request, reply) => {
13310
13318
  const project = resolveProject(app.db, request.params.name);
13311
- const { since: sinceRaw, until: untilRaw, lastRuns: lastRunsRaw, groupBy: groupByRaw } = request.query;
13319
+ const {
13320
+ since: sinceRaw,
13321
+ until: untilRaw,
13322
+ lastRuns: lastRunsRaw,
13323
+ groupBy: groupByRaw,
13324
+ month: monthRaw,
13325
+ shareOfVoice: shareOfVoiceRaw
13326
+ } = request.query;
13312
13327
  let groupBy = null;
13313
13328
  if (groupByRaw !== void 0 && groupByRaw !== "") {
13314
13329
  if (groupByRaw !== "provider") throw validationError('"groupBy" must be "provider"');
@@ -13317,11 +13332,17 @@ async function visibilityStatsRoutes(app) {
13317
13332
  const hasSince = sinceRaw !== void 0 && sinceRaw !== "";
13318
13333
  const hasUntil = untilRaw !== void 0 && untilRaw !== "";
13319
13334
  const hasLastRuns = lastRunsRaw !== void 0 && lastRunsRaw !== "";
13335
+ const hasMonth = monthRaw !== void 0 && monthRaw !== "";
13336
+ const wantShareOfVoice = shareOfVoiceRaw === "1" || shareOfVoiceRaw === "true";
13320
13337
  if (hasLastRuns && (hasSince || hasUntil)) {
13321
13338
  throw validationError('"lastRuns" cannot be combined with "since"/"until" \u2014 use one or the other');
13322
13339
  }
13340
+ if (hasMonth && (hasSince || hasUntil || hasLastRuns)) {
13341
+ throw validationError('"month" cannot be combined with "since"/"until"/"lastRuns" \u2014 use one or the other');
13342
+ }
13323
13343
  let sinceMs = null;
13324
13344
  let untilMs = null;
13345
+ let resolvedMonthWindow = null;
13325
13346
  if (hasSince) {
13326
13347
  const ms = Date.parse(sinceRaw);
13327
13348
  if (Number.isNaN(ms)) throw validationError('"since" must be an ISO 8601 date/time');
@@ -13332,6 +13353,17 @@ async function visibilityStatsRoutes(app) {
13332
13353
  if (ms === null) throw validationError('"until" must be an ISO 8601 date/time');
13333
13354
  untilMs = ms;
13334
13355
  }
13356
+ if (hasMonth) {
13357
+ let bounds;
13358
+ try {
13359
+ bounds = calendarMonthBounds(monthRaw);
13360
+ } catch (err) {
13361
+ throw validationError(err instanceof RangeError ? err.message : '"month" must be in YYYY-MM format');
13362
+ }
13363
+ sinceMs = Date.parse(bounds.since);
13364
+ untilMs = Date.parse(bounds.until);
13365
+ resolvedMonthWindow = bounds;
13366
+ }
13335
13367
  if (sinceMs !== null && untilMs !== null && untilMs < sinceMs) {
13336
13368
  throw validationError('"until" must be on or after "since"');
13337
13369
  }
@@ -13357,11 +13389,44 @@ async function visibilityStatsRoutes(app) {
13357
13389
  createdAt: querySnapshots.createdAt
13358
13390
  }).from(querySnapshots).where(inArray8(querySnapshots.runId, runIds)).all() : [];
13359
13391
  const stats = computeVisibilityStats({ queries: projectQueries, snapshots, groupBy });
13392
+ const queryAttribution = buildQueryAttribution(projectQueries);
13393
+ let shareOfVoice;
13394
+ if (wantShareOfVoice) {
13395
+ const competitorRows = app.db.select({ domain: competitors.domain }).from(competitors).where(eq17(competitors.projectId, project.id)).all();
13396
+ const mentionShareCompetitors = competitorRows.map((c) => ({
13397
+ domain: c.domain,
13398
+ brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
13399
+ }));
13400
+ const sovSnapshots = runIds.length > 0 ? app.db.select({
13401
+ queryId: querySnapshots.queryId,
13402
+ queryText: querySnapshots.queryText,
13403
+ answerMentioned: querySnapshots.answerMentioned,
13404
+ answerText: querySnapshots.answerText
13405
+ }).from(querySnapshots).where(inArray8(querySnapshots.runId, runIds)).all() : [];
13406
+ const attributedSovSnapshots = sovSnapshots.filter((s) => resolveCurrentQuery(queryAttribution, s) !== void 0);
13407
+ const result = buildMentionShare(
13408
+ attributedSovSnapshots.map((s) => ({ projectMentioned: s.answerMentioned === true, answerText: s.answerText })),
13409
+ { competitors: mentionShareCompetitors }
13410
+ );
13411
+ const b = result.breakdown;
13412
+ const denom = b.projectMentionSnapshots + b.competitorMentionSnapshots;
13413
+ shareOfVoice = {
13414
+ // `null` (not 0) when there is no competitive frame configured — a 0 here
13415
+ // would read as "losing" when the head-to-head metric is simply undefined.
13416
+ percent: competitorRows.length === 0 ? null : denom > 0 ? Math.round(b.projectMentionSnapshots / denom * 100) : 0,
13417
+ projectMentions: b.projectMentionSnapshots,
13418
+ competitorMentions: b.competitorMentionSnapshots,
13419
+ snapshotsWithAnswerText: b.snapshotsWithAnswerText,
13420
+ perCompetitor: b.perCompetitor.map((c) => ({ domain: c.domain, mentions: c.mentionSnapshots }))
13421
+ };
13422
+ }
13360
13423
  const response = {
13361
13424
  project: project.name,
13362
13425
  window: {
13363
- since: hasSince ? sinceRaw : null,
13364
- until: hasUntil ? untilRaw : null,
13426
+ // A `month` request echoes the resolved inclusive bounds it expanded to,
13427
+ // so the window is self-documenting regardless of which input was used.
13428
+ since: resolvedMonthWindow ? resolvedMonthWindow.since : hasSince ? sinceRaw : null,
13429
+ until: resolvedMonthWindow ? resolvedMonthWindow.until : hasUntil ? untilRaw : null,
13365
13430
  lastRuns,
13366
13431
  runCount
13367
13432
  },
@@ -13371,7 +13436,8 @@ async function visibilityStatsRoutes(app) {
13371
13436
  // requested; both are OMITTED otherwise (absent = no breakdown) so the
13372
13437
  // SDK types `groupBy` as `groupBy?: 'provider'` rather than a misleading
13373
13438
  // always-present literal.
13374
- ...groupBy === "provider" ? { groupBy, byProvider: stats.byProvider ?? [] } : {}
13439
+ ...groupBy === "provider" ? { groupBy, byProvider: stats.byProvider ?? [] } : {},
13440
+ ...shareOfVoice ? { shareOfVoice } : {}
13375
13441
  };
13376
13442
  return reply.send(response);
13377
13443
  });
@@ -14436,6 +14502,18 @@ var groupByProviderQueryParameter = {
14436
14502
  description: 'Set to "provider" to include a per-provider breakdown whose counts sum to the pooled counts.',
14437
14503
  schema: { type: "string", enum: ["provider"] }
14438
14504
  };
14505
+ var monthQueryParameter = {
14506
+ name: "month",
14507
+ in: "query",
14508
+ description: `Aggregate a single calendar month (YYYY-MM), expanded to that month's inclusive UTC bounds. Mutually exclusive with "since"/"until"/"lastRuns".`,
14509
+ schema: stringSchema
14510
+ };
14511
+ var shareOfVoiceQueryParameter = {
14512
+ name: "shareOfVoice",
14513
+ in: "query",
14514
+ description: 'Set to "1" to include pooled share of voice (project vs tracked-competitor brand mentions in answer text) across the window.',
14515
+ schema: { type: "string", enum: ["1"] }
14516
+ };
14439
14517
  var wordpressEnvQueryParameter = {
14440
14518
  name: "env",
14441
14519
  in: "query",
@@ -15166,9 +15244,17 @@ var routeCatalog = [
15166
15244
  method: "get",
15167
15245
  path: "/api/v1/projects/{name}/visibility-stats",
15168
15246
  summary: "Get aggregated mention/citation stats per query",
15169
- description: "Per-query mention (answer-text) and citation (source-list) counts with a sample size, pooled across many answer-visibility runs (probe-excluded). Tri-state aware: `checked` counts only snapshots where answerMentioned was recorded (null = not checked is excluded). Lets a consumer compute confidence-aware (e.g. Wilson) proportions without N+1 run fetches. With no since/until/lastRuns, EVERY completed/partial answer-visibility run is pooled \u2014 `window.runCount` reports how many; bound the window with lastRuns or since/until for a recent sample. Set groupBy=provider for a per-provider breakdown whose counts sum to the pooled counts.",
15247
+ description: "Per-query mention (answer-text) and citation (source-list) counts with a sample size, pooled across many answer-visibility runs (probe-excluded). Tri-state aware: `checked` counts only snapshots where answerMentioned was recorded (null = not checked is excluded). Lets a consumer compute confidence-aware (e.g. Wilson) proportions without N+1 run fetches. With no since/until/lastRuns, EVERY completed/partial answer-visibility run is pooled \u2014 `window.runCount` reports how many; bound the window with lastRuns, since/until, or month=YYYY-MM for a recent sample. Set groupBy=provider for a per-provider breakdown whose counts sum to the pooled counts, or shareOfVoice=1 to add pooled share of voice vs tracked competitors.",
15170
15248
  tags: ["analytics"],
15171
- parameters: [nameParameter, sinceQueryParameter, untilQueryParameter, lastRunsQueryParameter, groupByProviderQueryParameter],
15249
+ parameters: [
15250
+ nameParameter,
15251
+ sinceQueryParameter,
15252
+ untilQueryParameter,
15253
+ lastRunsQueryParameter,
15254
+ groupByProviderQueryParameter,
15255
+ monthQueryParameter,
15256
+ shareOfVoiceQueryParameter
15257
+ ],
15172
15258
  responses: {
15173
15259
  200: jsonResponse("Aggregated visibility stats returned.", "VisibilityStatsDto"),
15174
15260
  400: errorResponse("Invalid query parameters."),
@@ -2686,6 +2686,23 @@ var visibilityStatsWindowSchema = z20.object({
2686
2686
  runCount: z20.number().int()
2687
2687
  });
2688
2688
  var visibilityStatsGroupBySchema = z20.enum(["provider"]);
2689
+ var visibilityStatsShareCompetitorSchema = z20.object({
2690
+ domain: z20.string(),
2691
+ /** Snapshots (with answer text) in the window where this competitor's brand appeared. */
2692
+ mentions: z20.number().int()
2693
+ });
2694
+ var visibilityStatsShareOfVoiceSchema = z20.object({
2695
+ /** `projectMentions / (projectMentions + competitorMentions)` as 0-100; `null` when no competitors configured. */
2696
+ percent: z20.number().nullable(),
2697
+ /** Snapshots (with answer text) where the project's brand appeared in the answer. */
2698
+ projectMentions: z20.number().int(),
2699
+ /** Snapshots (with answer text) where any tracked competitor's brand appeared. */
2700
+ competitorMentions: z20.number().int(),
2701
+ /** Snapshots that carried answer text — the universe the mentions were counted over. */
2702
+ snapshotsWithAnswerText: z20.number().int(),
2703
+ /** Per-competitor mention counts, descending; only competitors with ≥1 mention appear. */
2704
+ perCompetitor: z20.array(visibilityStatsShareCompetitorSchema)
2705
+ });
2689
2706
  var visibilityStatsDtoSchema = z20.object({
2690
2707
  project: z20.string(),
2691
2708
  /**
@@ -2704,8 +2721,21 @@ var visibilityStatsDtoSchema = z20.object({
2704
2721
  */
2705
2722
  byProvider: z20.array(visibilityStatsProviderEntrySchema).optional(),
2706
2723
  /** Per-query stats, sorted by query text. Only queries with ≥1 snapshot in the window appear. */
2707
- queries: z20.array(visibilityStatsQueryEntrySchema)
2708
- });
2724
+ queries: z20.array(visibilityStatsQueryEntrySchema),
2725
+ /** Pooled share of voice vs tracked competitors — present only when `shareOfVoice=1` was requested. */
2726
+ shareOfVoice: visibilityStatsShareOfVoiceSchema.optional()
2727
+ });
2728
+ function calendarMonthBounds(month) {
2729
+ const match = /^(\d{4})-(\d{2})$/.exec(month);
2730
+ if (!match) throw new RangeError("month must be in YYYY-MM format");
2731
+ const year = Number(match[1]);
2732
+ const mon = Number(match[2]);
2733
+ if (mon < 1 || mon > 12) throw new RangeError("month must be between 01 and 12");
2734
+ return {
2735
+ since: new Date(Date.UTC(year, mon - 1, 1)).toISOString(),
2736
+ until: new Date(Date.UTC(year, mon, 1) - 1).toISOString()
2737
+ };
2738
+ }
2709
2739
 
2710
2740
  // ../contracts/src/ga.ts
2711
2741
  import { z as z21 } from "zod";
@@ -5577,6 +5607,7 @@ export {
5577
5607
  parseWindow,
5578
5608
  windowCutoff,
5579
5609
  visibilityStatsDtoSchema,
5610
+ calendarMonthBounds,
5580
5611
  ga4StatusDtoSchema,
5581
5612
  ga4SyncResponseDtoSchema,
5582
5613
  ga4AiReferralHistoryEntrySchema,
package/dist/cli.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  setTelemetrySource,
30
30
  showFirstRunNotice,
31
31
  trackEvent
32
- } from "./chunk-3PVCILSN.js";
32
+ } from "./chunk-BTJY73GW.js";
33
33
  import {
34
34
  CliError,
35
35
  EXIT_SYSTEM_ERROR,
@@ -46,7 +46,7 @@ import {
46
46
  saveConfig,
47
47
  saveConfigPatch,
48
48
  usageError
49
- } from "./chunk-2FUODJQR.js";
49
+ } from "./chunk-23DCY7DL.js";
50
50
  import {
51
51
  apiKeys,
52
52
  createClient,
@@ -54,7 +54,7 @@ import {
54
54
  projects,
55
55
  queries,
56
56
  renderReportHtml
57
- } from "./chunk-B2H2RRIT.js";
57
+ } from "./chunk-GNMZTR6Y.js";
58
58
  import {
59
59
  BacklinkSources,
60
60
  CcReleaseSyncStatuses,
@@ -80,7 +80,7 @@ import {
80
80
  providerQuotaPolicySchema,
81
81
  resolveProviderInput,
82
82
  winnabilityClassSchema
83
- } from "./chunk-EOQXB63S.js";
83
+ } from "./chunk-Q7FFDQU4.js";
84
84
 
85
85
  // src/cli.ts
86
86
  import { pathToFileURL } from "url";
@@ -11447,8 +11447,10 @@ async function showVisibilityStats(project, opts) {
11447
11447
  const data = await client.getVisibilityStats(project, {
11448
11448
  since: opts.since,
11449
11449
  until: opts.until,
11450
+ month: opts.month,
11450
11451
  lastRuns: opts.lastRuns,
11451
- groupBy: opts.byProvider ? "provider" : void 0
11452
+ groupBy: opts.byProvider ? "provider" : void 0,
11453
+ shareOfVoice: opts.shareOfVoice
11452
11454
  });
11453
11455
  if (opts.format === "jsonl") {
11454
11456
  emitJsonl(data.queries.map((q) => ({ project: data.project, runCount: data.window.runCount, ...q })));
@@ -11539,10 +11541,21 @@ function printVisibilityStats(data) {
11539
11541
  );
11540
11542
  }
11541
11543
  }
11544
+ const sov = data.shareOfVoice;
11545
+ if (sov) {
11546
+ console.log("");
11547
+ const pctStr = sov.percent === null ? "\u2014 (no competitors configured)" : `${sov.percent}%`;
11548
+ console.log(
11549
+ `Share of voice: ${pctStr} (you ${sov.projectMentions} vs competitors ${sov.competitorMentions} brand mentions across ${sov.snapshotsWithAnswerText} answers)`
11550
+ );
11551
+ for (const c of sov.perCompetitor.slice(0, 8)) {
11552
+ console.log(` ${c.domain}: ${c.mentions}`);
11553
+ }
11554
+ }
11542
11555
  }
11543
11556
 
11544
11557
  // src/cli-commands/visibility-stats.ts
11545
- var USAGE4 = "canonry visibility-stats <project> [--since <iso>] [--until <iso>] [--last-runs <n>] [--by-provider] [--format json|jsonl]";
11558
+ var USAGE4 = "canonry visibility-stats <project> [--since <iso>] [--until <iso>] [--month <YYYY-MM>] [--last-runs <n>] [--by-provider] [--share-of-voice] [--format json|jsonl]";
11546
11559
  var VISIBILITY_STATS_CLI_COMMANDS = [
11547
11560
  {
11548
11561
  path: ["visibility-stats"],
@@ -11550,20 +11563,24 @@ var VISIBILITY_STATS_CLI_COMMANDS = [
11550
11563
  options: {
11551
11564
  since: stringOption(),
11552
11565
  until: stringOption(),
11566
+ month: stringOption(),
11553
11567
  "last-runs": stringOption(),
11554
- "by-provider": { type: "boolean", default: false }
11568
+ "by-provider": { type: "boolean", default: false },
11569
+ "share-of-voice": { type: "boolean", default: false }
11555
11570
  },
11556
11571
  run: async (input) => {
11557
11572
  const project = requireProject(input, "visibility-stats", USAGE4);
11558
11573
  await showVisibilityStats(project, {
11559
11574
  since: getString(input.values, "since"),
11560
11575
  until: getString(input.values, "until"),
11576
+ month: getString(input.values, "month"),
11561
11577
  lastRuns: parseIntegerOption(input, "last-runs", {
11562
11578
  command: "visibility-stats",
11563
11579
  usage: USAGE4,
11564
11580
  message: "--last-runs must be an integer"
11565
11581
  }),
11566
11582
  byProvider: getBoolean(input.values, "by-provider"),
11583
+ shareOfVoice: getBoolean(input.values, "share-of-voice"),
11567
11584
  format: input.format
11568
11585
  });
11569
11586
  }
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-3PVCILSN.js";
3
+ } from "./chunk-BTJY73GW.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-2FUODJQR.js";
7
- import "./chunk-B2H2RRIT.js";
8
- import "./chunk-EOQXB63S.js";
6
+ } from "./chunk-23DCY7DL.js";
7
+ import "./chunk-GNMZTR6Y.js";
8
+ import "./chunk-Q7FFDQU4.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-B2H2RRIT.js";
4
- import "./chunk-EOQXB63S.js";
3
+ } from "./chunk-GNMZTR6Y.js";
4
+ import "./chunk-Q7FFDQU4.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  PACKAGE_VERSION,
4
4
  canonryMcpTools,
5
5
  createApiClient
6
- } from "./chunk-2FUODJQR.js";
6
+ } from "./chunk-23DCY7DL.js";
7
7
  import {
8
8
  isReadOnlyKey
9
- } from "./chunk-EOQXB63S.js";
9
+ } from "./chunk-Q7FFDQU4.js";
10
10
 
11
11
  // src/mcp/cli.ts
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "4.102.1",
3
+ "version": "4.103.0",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -68,21 +68,21 @@
68
68
  "@ainyc/canonry-db": "0.0.0",
69
69
  "@ainyc/canonry-integration-bing": "0.0.0",
70
70
  "@ainyc/canonry-config": "0.0.0",
71
- "@ainyc/canonry-integration-cloud-run": "0.0.0",
71
+ "@ainyc/canonry-integration-openai-ads": "0.0.0",
72
72
  "@ainyc/canonry-integration-google": "0.0.0",
73
- "@ainyc/canonry-integration-google-business-profile": "0.0.0",
73
+ "@ainyc/canonry-integration-cloud-run": "0.0.0",
74
74
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
75
+ "@ainyc/canonry-integration-google-business-profile": "0.0.0",
75
76
  "@ainyc/canonry-integration-google-places": "0.0.0",
76
- "@ainyc/canonry-integration-openai-ads": "0.0.0",
77
77
  "@ainyc/canonry-integration-wordpress": "0.0.0",
78
- "@ainyc/canonry-integration-traffic": "0.0.0",
79
- "@ainyc/canonry-provider-cdp": "0.0.0",
80
78
  "@ainyc/canonry-intelligence": "0.0.0",
79
+ "@ainyc/canonry-provider-cdp": "0.0.0",
81
80
  "@ainyc/canonry-provider-claude": "0.0.0",
82
- "@ainyc/canonry-provider-local": "0.0.0",
83
- "@ainyc/canonry-provider-openai": "0.0.0",
84
81
  "@ainyc/canonry-provider-gemini": "0.0.0",
85
- "@ainyc/canonry-provider-perplexity": "0.0.0"
82
+ "@ainyc/canonry-integration-traffic": "0.0.0",
83
+ "@ainyc/canonry-provider-local": "0.0.0",
84
+ "@ainyc/canonry-provider-perplexity": "0.0.0",
85
+ "@ainyc/canonry-provider-openai": "0.0.0"
86
86
  },
87
87
  "scripts": {
88
88
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",