@canonry/canonry 5.22.0 → 5.29.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 (46) hide show
  1. package/README.md +2 -1
  2. package/assets/agent-workspace/skills/aero/SKILL.md +37 -3
  3. package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +26 -12
  4. package/assets/agent-workspace/skills/aero/references/reporting.md +1 -0
  5. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +100 -5
  6. package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +30 -6
  7. package/assets/agent-workspace/skills/canonry/references/indexing.md +1 -0
  8. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +30 -0
  9. package/assets/assets/{AuditHistoryPanel-teTDC-4-.js → AuditHistoryPanel-ru-RI1SR.js} +1 -1
  10. package/assets/assets/{BacklinksPage-Bqm7bido.js → BacklinksPage-BbVWtfAh.js} +1 -1
  11. package/assets/assets/ChartPrimitives-B5okB5g-.js +1 -0
  12. package/assets/assets/{HistoryPage-IcqwPBmt.js → HistoryPage-CxIxsxIv.js} +1 -1
  13. package/assets/assets/MeasurementPropertyPage-Oju3oAP3.js +1 -0
  14. package/assets/assets/ProjectPage-ZsQo28yM.js +8 -0
  15. package/assets/assets/{RunRow-S_SCHr0o.js → RunRow-D73cbxey.js} +1 -1
  16. package/assets/assets/{RunsPage-DFiif626.js → RunsPage-D9hOl6k6.js} +1 -1
  17. package/assets/assets/{SettingsPage-CitZ6FAS.js → SettingsPage-By5cJJaC.js} +1 -1
  18. package/assets/assets/SiteHealthSection-Gfyispq_.js +4 -0
  19. package/assets/assets/{TrafficPage-lY8na5Pg.js → TrafficPage-8khxNXDL.js} +2 -2
  20. package/assets/assets/{TrafficSourceDetailPage-BTykoVGT.js → TrafficSourceDetailPage-FDe4jKLH.js} +1 -1
  21. package/assets/assets/{extract-error-message-r0Gpc7mv.js → extract-error-message-Co1hxrsc.js} +1 -1
  22. package/assets/assets/index-DKScDg92.js +86 -0
  23. package/assets/assets/index-DfhmHULl.css +1 -0
  24. package/assets/assets/{react-sigma_core.esm.min-DDrDxbO1.js → react-sigma_core.esm.min-DxAxqyeI.js} +1 -1
  25. package/assets/assets/{v2-overview-adapter-OsQiglXU.js → v2-overview-adapter-DNbOwIJq.js} +1 -1
  26. package/assets/index.html +2 -2
  27. package/dist/{chunk-XFHXKLNT.js → chunk-7BW7BAD5.js} +3727 -1282
  28. package/dist/{chunk-S5OSY3PI.js → chunk-DCGELKRW.js} +7126 -6001
  29. package/dist/{chunk-DDL2XSNM.js → chunk-J6BJIOIN.js} +130 -37
  30. package/dist/{chunk-P5VXVFQW.js → chunk-KGYHEYHX.js} +6992 -5409
  31. package/dist/{chunk-3LCR6WU6.js → chunk-THMIDWMS.js} +57 -3
  32. package/dist/{chunk-GLLY2GEV.js → chunk-VPIOF2PB.js} +172 -7
  33. package/dist/{chunk-YTSXQUJ3.js → chunk-WPQB4UYX.js} +3 -3
  34. package/dist/cli.js +561 -158
  35. package/dist/{demo-server-HULNEORN.js → demo-server-EU6B4KNK.js} +24 -32
  36. package/dist/index.d.ts +9 -1
  37. package/dist/index.js +6 -6
  38. package/dist/{intelligence-service-CXNBKRHV.js → intelligence-service-FVUNBN66.js} +2 -2
  39. package/dist/mcp.js +4 -4
  40. package/package.json +12 -11
  41. package/assets/assets/ChartPrimitives-BQC2VH0O.js +0 -1
  42. package/assets/assets/MeasurementPropertyPage-BLpptT3_.js +0 -1
  43. package/assets/assets/ProjectPage-nn2Y9H-m.js +0 -8
  44. package/assets/assets/SiteHealthSection-C2dsyNGY.js +0 -4
  45. package/assets/assets/index-DwlO8KfE.js +0 -86
  46. package/assets/assets/index-EJC8PSbh.css +0 -1
package/dist/cli.js CHANGED
@@ -35,11 +35,11 @@ import {
35
35
  trackCliCommandFinished,
36
36
  trackEvent,
37
37
  waitForServerRuntimeStartup
38
- } from "./chunk-XFHXKLNT.js";
38
+ } from "./chunk-7BW7BAD5.js";
39
39
  import {
40
40
  autoSyncSkills,
41
41
  formatAutoSyncNotice
42
- } from "./chunk-YTSXQUJ3.js";
42
+ } from "./chunk-WPQB4UYX.js";
43
43
  import {
44
44
  configExists,
45
45
  createApiClient,
@@ -56,13 +56,13 @@ import {
56
56
  saveConfig,
57
57
  saveConfigPatch,
58
58
  shouldPrintServeSkillsNudge
59
- } from "./chunk-GLLY2GEV.js";
59
+ } from "./chunk-VPIOF2PB.js";
60
60
  import {
61
61
  DEFAULT_DEMO_TRUSTED_PROXIES,
62
62
  trustedProxiesProblem,
63
63
  trustedProxyProblem
64
64
  } from "./chunk-RB2F4D3Z.js";
65
- import "./chunk-3LCR6WU6.js";
65
+ import "./chunk-THMIDWMS.js";
66
66
  import {
67
67
  CliError,
68
68
  EXIT_SYSTEM_ERROR,
@@ -74,7 +74,7 @@ import {
74
74
  runMeasurementDraftAction,
75
75
  systemError,
76
76
  usageError
77
- } from "./chunk-DDL2XSNM.js";
77
+ } from "./chunk-J6BJIOIN.js";
78
78
  import {
79
79
  CLOUDFLARE_WORKER_BINDINGS,
80
80
  CLOUDFLARE_WORKER_GENERATED_MARKER,
@@ -89,7 +89,7 @@ import {
89
89
  queries,
90
90
  renderReportHtml,
91
91
  runs
92
- } from "./chunk-P5VXVFQW.js";
92
+ } from "./chunk-KGYHEYHX.js";
93
93
  import {
94
94
  AdsDeliverySnapshotStatuses,
95
95
  AdsHistoricalCampaignRollupStatuses,
@@ -102,9 +102,13 @@ import {
102
102
  CheckScopes,
103
103
  CheckStatuses,
104
104
  CitationStates,
105
+ GaMoverChangeBases,
105
106
  MeasurementEvidenceShapes,
107
+ OUTSTANDING_PROVIDER_BATCH_STATUSES,
108
+ ProviderBatchStatuses,
106
109
  READ_ONLY_SCOPE,
107
110
  REPORT_PERIOD_OPTIONS,
111
+ RatioUnits,
108
112
  ResearchRunStatuses,
109
113
  RunKinds,
110
114
  RunStatuses,
@@ -142,9 +146,10 @@ import {
142
146
  formatGbpMetricLabel,
143
147
  formatIsoDate,
144
148
  formatMicros,
145
- formatRatio,
149
+ formatPercent,
146
150
  formatRunErrorOneLine,
147
- formatWholePercent,
151
+ formatSignedPointDelta,
152
+ gbpNegativeReviewMaxStarsSchema,
148
153
  googleAdsMetricsWindowSchema,
149
154
  isVisibilityCompareClassMetric,
150
155
  logQuerySchema,
@@ -166,11 +171,14 @@ import {
166
171
  normalizeProjectAliases,
167
172
  normalizeTelemetryStatus,
168
173
  notificationEventSchema,
174
+ providerDispatchModeSchema,
169
175
  queryClassFilterSchema,
170
176
  queryTrackingCommitRequestSchema,
171
177
  queryTrackingPreviewRequestSchema,
178
+ referralAssessmentQuerySchema,
172
179
  reportUnattributedAnswers,
173
180
  researchBatchCreateSchema,
181
+ resolveNegativeReviewMaxStars,
174
182
  resolveProviderInput,
175
183
  resultsClearRequestSchema,
176
184
  shareOfVoiceLabel,
@@ -179,7 +187,7 @@ import {
179
187
  visibilityCompareSelectionSchema,
180
188
  visibilityReportRequestSchema,
181
189
  winnabilityClassSchema
182
- } from "./chunk-S5OSY3PI.js";
190
+ } from "./chunk-DCGELKRW.js";
183
191
 
184
192
  // src/cli.ts
185
193
  import { pathToFileURL } from "url";
@@ -444,7 +452,7 @@ function parseDemoTrustedProxies(values) {
444
452
  async function demoCommand(options) {
445
453
  const { host, port } = parseDemoListenOptions(options);
446
454
  const trustProxy = parseDemoTrustedProxies(options.trustProxy);
447
- const { createDemoServer } = await import("./demo-server-HULNEORN.js");
455
+ const { createDemoServer } = await import("./demo-server-EU6B4KNK.js");
448
456
  const app = await createDemoServer({ trustProxy });
449
457
  let stopping = false;
450
458
  const stop = () => {
@@ -680,7 +688,7 @@ function formatSummaryAndDomains(project, response) {
680
688
  lines.push(`Target: ${s.targetDomain}`);
681
689
  lines.push(`Linking domains: ${s.totalLinkingDomains}`);
682
690
  lines.push(`Total hosts: ${s.totalHosts}`);
683
- lines.push(`Top-10 share: ${s.top10HostsShare}`);
691
+ lines.push(`Top-10 share: ${formatPercent(Number(s.top10HostsShare))}`);
684
692
  if (s.excludedLinkingDomains !== void 0 && s.excludedLinkingDomains > 0) {
685
693
  lines.push(`Excluded: ${s.excludedLinkingDomains} crawler/proxy domains (${s.excludedHosts ?? 0} hosts)`);
686
694
  }
@@ -1193,7 +1201,7 @@ async function bingCoverage(project, format) {
1193
1201
  console.log(`
1194
1202
  Bing Index Coverage for "${project}"
1195
1203
  `);
1196
- console.log(` SUMMARY: ${pctColor}${summary.indexed} / ${summary.total} pages indexed (${summary.percentage}%)${reset}${unknownNote}
1204
+ console.log(` SUMMARY: ${pctColor}${summary.indexed} / ${summary.total} pages indexed (${formatPercent(summary.percentage, "percent")})${reset}${unknownNote}
1197
1205
  `);
1198
1206
  if (result.indexed.length > 0) {
1199
1207
  console.log(` INDEXED (${result.indexed.length}):`);
@@ -1418,7 +1426,7 @@ async function bingPerformance(project, format) {
1418
1426
  for (const row of rows.slice(0, 50)) {
1419
1427
  const query = row.query.length > 38 ? row.query.slice(0, 35) + "..." : row.query;
1420
1428
  console.log(
1421
- ` ${query.padEnd(40)}${String(row.clicks).padEnd(8)}${String(row.impressions).padEnd(8)}${(row.ctr * 100).toFixed(1).padStart(5)}% ${row.averagePosition.toFixed(1).padStart(5)}`
1429
+ ` ${query.padEnd(40)}${String(row.clicks).padEnd(8)}${String(row.impressions).padEnd(8)}${formatPercent(row.ctr).padStart(6)} ${row.averagePosition.toFixed(1).padStart(5)}`
1422
1430
  );
1423
1431
  }
1424
1432
  if (rows.length > 50) {
@@ -3603,7 +3611,7 @@ async function adsSummary(project, opts) {
3603
3611
  console.log(`Structure: ${result.campaignCount} campaigns / ${result.adGroupCount} ad groups / ${result.adCount} ads`);
3604
3612
  console.log(`Window: ${result.window.from ?? "\u2014"} \u2192 ${result.window.to ?? "\u2014"}${partialDayNote(result.window.inProgressDate)}`);
3605
3613
  console.log(`Impressions: ${result.totals.impressions}`);
3606
- console.log(`Clicks: ${result.totals.clicks}${result.totals.ctr != null ? ` (CTR ${(result.totals.ctr * 100).toFixed(2)}%)` : ""}`);
3614
+ console.log(`Clicks: ${result.totals.clicks}${result.totals.ctr != null ? ` (CTR ${formatPercent(result.totals.ctr)})` : ""}`);
3607
3615
  console.log(`Spend: ${formatMicros(result.totals.spendMicros, result.currencyCode ?? "USD")}${result.totals.cpcMicros != null ? ` (CPC ${formatMicros(result.totals.cpcMicros, result.currencyCode ?? "USD")})` : ""}`);
3608
3616
  console.log(`Last synced: ${result.lastSyncedAt ?? "never"}`);
3609
3617
  }
@@ -4227,8 +4235,7 @@ async function gaTraffic(project, opts) {
4227
4235
  console.log(` Organic Sessions: ${result.totalOrganicSessions}`);
4228
4236
  console.log(` Total Users: ${result.totalUsers ?? "unavailable for this range"}`);
4229
4237
  if (result.aiSessionsDeduped > 0) {
4230
- const share = result.totalSessions > 0 ? Math.round(result.aiSessionsDeduped / result.totalSessions * 100) : 0;
4231
- console.log(` AI Sessions (deduped): ${result.aiSessionsDeduped} (${share}% of total)`);
4238
+ console.log(` AI Sessions (deduped): ${result.aiSessionsDeduped} (${result.aiSharePctDisplay} of total)`);
4232
4239
  if (result.paidAiSessionsDeduped > 0) {
4233
4240
  console.log(` Paid AI: ${result.paidAiSessionsDeduped} (${result.paidAiSharePctDisplay} of total)`);
4234
4241
  }
@@ -4238,13 +4245,13 @@ async function gaTraffic(project, opts) {
4238
4245
  const attrWidth = 12;
4239
4246
  const classWidth = 8;
4240
4247
  console.log(" AI REFERRAL SOURCES");
4241
- console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CLASS".padEnd(classWidth)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}`);
4242
- console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(classWidth)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}`);
4248
+ console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CLASS".padEnd(classWidth)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)} SHARE`);
4249
+ console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(classWidth)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)} ${"\u2500".repeat(6)}`);
4243
4250
  for (const ref of result.aiReferrals) {
4244
4251
  const dimLabel = ref.sourceDimension === "first_user" ? "first-visit" : ref.sourceDimension === "manual_utm" ? "utm" : "session";
4245
4252
  const classLabel = ref.trafficClass === "paid" ? "paid" : "organic";
4246
4253
  console.log(
4247
- ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${classLabel.padEnd(classWidth)} ${dimLabel.padEnd(attrWidth)} ${String(ref.sessions).padEnd(10)}`
4254
+ ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${classLabel.padEnd(classWidth)} ${dimLabel.padEnd(attrWidth)} ${String(ref.sessions).padEnd(10)} ${formatPercent(ref.share)}`
4248
4255
  );
4249
4256
  }
4250
4257
  console.log();
@@ -4267,16 +4274,15 @@ async function gaTraffic(project, opts) {
4267
4274
  if (result.socialReferrals.length > 0) {
4268
4275
  const chanWidth = 12;
4269
4276
  if (result.socialSessions > 0) {
4270
- const share = result.totalSessions > 0 ? Math.round(result.socialSessions / result.totalSessions * 100) : 0;
4271
- console.log(` Social Sessions: ${result.socialSessions} (${share}% of total)`);
4277
+ console.log(` Social Sessions: ${result.socialSessions} (${result.socialSharePctDisplay} of total)`);
4272
4278
  }
4273
4279
  console.log(" SOCIAL REFERRAL SOURCES");
4274
- console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CHANNEL".padEnd(chanWidth)} ${"SESSIONS".padEnd(10)}`);
4275
- console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(chanWidth)} ${"\u2500".repeat(10)}`);
4280
+ console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"CHANNEL".padEnd(chanWidth)} ${"SESSIONS".padEnd(10)} SHARE`);
4281
+ console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(chanWidth)} ${"\u2500".repeat(10)} ${"\u2500".repeat(6)}`);
4276
4282
  for (const ref of result.socialReferrals) {
4277
4283
  const chanLabel = ref.channelGroup === "Paid Social" ? "paid" : "organic";
4278
4284
  console.log(
4279
- ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${chanLabel.padEnd(chanWidth)} ${String(ref.sessions).padEnd(10)}`
4285
+ ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${chanLabel.padEnd(chanWidth)} ${String(ref.sessions).padEnd(10)} ${formatPercent(ref.share)}`
4280
4286
  );
4281
4287
  }
4282
4288
  console.log();
@@ -4284,12 +4290,12 @@ async function gaTraffic(project, opts) {
4284
4290
  if (result.topPages.length > 0) {
4285
4291
  const pageWidth = Math.min(60, Math.max(15, ...result.topPages.map((r) => r.landingPage.length)));
4286
4292
  console.log(` TOP LANDING PAGES`);
4287
- console.log(` ${"PAGE".padEnd(pageWidth)} ${"SESSIONS".padEnd(10)}${"ORGANIC".padEnd(10)}`);
4288
- console.log(` ${"\u2500".repeat(pageWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(10)}`);
4293
+ console.log(` ${"PAGE".padEnd(pageWidth)} ${"SESSIONS".padEnd(10)}${"ORGANIC".padEnd(10)}ORGANIC %`);
4294
+ console.log(` ${"\u2500".repeat(pageWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(10)}${"\u2500".repeat(9)}`);
4289
4295
  for (const row of result.topPages) {
4290
4296
  const page = row.landingPage.length > pageWidth ? row.landingPage.slice(0, pageWidth - 3) + "..." : row.landingPage;
4291
4297
  console.log(
4292
- ` ${page.padEnd(pageWidth)} ${String(row.sessions).padEnd(10)}${String(row.organicSessions).padEnd(10)}`
4298
+ ` ${page.padEnd(pageWidth)} ${String(row.sessions).padEnd(10)}${String(row.organicSessions).padEnd(10)}${formatPercent(row.organicShare)}`
4293
4299
  );
4294
4300
  }
4295
4301
  }
@@ -4482,6 +4488,19 @@ async function gaCoverage(project, format) {
4482
4488
  );
4483
4489
  }
4484
4490
  }
4491
+ function fmtTrend(pct) {
4492
+ return pct === null ? "n/a" : `${pct >= 0 ? "+" : ""}${formatPercent(pct, "percent")}`;
4493
+ }
4494
+ function fmtMoverChange(mover) {
4495
+ switch (mover.changeBasis) {
4496
+ case GaMoverChangeBases.new:
4497
+ return "new";
4498
+ case GaMoverChangeBases["small-base"]:
4499
+ return `${mover.changeSessions > 0 ? "+" : ""}${mover.changeSessions} sessions`;
4500
+ case GaMoverChangeBases.percent:
4501
+ return fmtTrend(mover.changePct);
4502
+ }
4503
+ }
4485
4504
  function windowLine(traffic) {
4486
4505
  if (!traffic.windowStart || !traffic.windowEnd) return null;
4487
4506
  const span = traffic.windowDays === null ? "" : ` (${traffic.windowDays} days)`;
@@ -4510,14 +4529,13 @@ async function gaSocialReferralSummary(project, opts) {
4510
4529
  const trendWindow = windowLine(traffic);
4511
4530
  if (trendWindow) console.log(`${trendWindow}
4512
4531
  `);
4513
- console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePct}% of ${traffic.totalSessions} total)`);
4532
+ console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePctDisplay} of ${traffic.totalSessions} total)`);
4514
4533
  console.log();
4515
- const fmtTrend = (pct4) => pct4 === null ? "n/a" : `${pct4 >= 0 ? "+" : ""}${pct4}%`;
4516
4534
  console.log(` 7d trend: ${fmtTrend(trend.trend7dPct)} (${trend.socialSessions7d} vs ${trend.socialSessionsPrev7d})`);
4517
4535
  console.log(` 30d trend: ${fmtTrend(trend.trend30dPct)} (${trend.socialSessions30d} vs ${trend.socialSessionsPrev30d})`);
4518
4536
  if (trend.biggestMover) {
4519
4537
  const m = trend.biggestMover;
4520
- console.log(` Mover: ${m.source} (${m.changePct >= 0 ? "+" : ""}${m.changePct}%, ${m.sessionsPrev7d}\u2192${m.sessions7d})`);
4538
+ console.log(` Mover: ${m.source} (${fmtMoverChange(m)}, ${m.sessionsPrev7d}\u2192${m.sessions7d})`);
4521
4539
  }
4522
4540
  console.log();
4523
4541
  if (traffic.socialReferrals.length > 0) {
@@ -4546,7 +4564,7 @@ async function gaSocialReferralSummary(project, opts) {
4546
4564
  const socialWindow = windowLine(traffic);
4547
4565
  if (socialWindow) console.log(`${socialWindow}
4548
4566
  `);
4549
- console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePct}% of ${traffic.totalSessions} total)`);
4567
+ console.log(` Sessions: ${traffic.socialSessions} (${traffic.socialSharePctDisplay} of ${traffic.totalSessions} total)`);
4550
4568
  if (traffic.socialReferrals.length > 0) {
4551
4569
  console.log();
4552
4570
  console.log(" TOP SOURCES");
@@ -4559,7 +4577,6 @@ async function gaSocialReferralSummary(project, opts) {
4559
4577
  async function gaAttribution(project, opts) {
4560
4578
  const client = getClient7();
4561
4579
  const traffic = await client.gaTraffic(project);
4562
- const fmtTrend = (pct4) => pct4 === null ? "n/a" : `${pct4 >= 0 ? "+" : ""}${pct4}%`;
4563
4580
  if (opts?.trend) {
4564
4581
  const trend = await client.gaAttributionTrend(project);
4565
4582
  if (isMachineFormat(opts.format)) {
@@ -4619,25 +4636,25 @@ async function gaAttribution(project, opts) {
4619
4636
  console.log(` Total Sessions: ${traffic.totalSessions}`);
4620
4637
  console.log(` Total Users: ${traffic.totalUsers ?? "unavailable for this range"}`);
4621
4638
  console.log();
4622
- console.log(" CHANNEL BREAKDOWN 7d trend 30d trend");
4623
- console.log(` Organic Search: ${String(traffic.channelBreakdown.organic.sessions).padEnd(6)} (${traffic.channelBreakdown.organic.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.organic.trend7dPct).padEnd(12)} ${fmtTrend(trend.organic.trend30dPct)}`);
4624
- console.log(` Social: ${String(traffic.channelBreakdown.social.sessions).padEnd(6)} (${traffic.channelBreakdown.social.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.social.trend7dPct).padEnd(12)} ${fmtTrend(trend.social.trend30dPct)}`);
4625
- console.log(` Direct: ${String(traffic.channelBreakdown.direct.sessions).padEnd(6)} (${traffic.channelBreakdown.direct.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.direct.trend7dPct).padEnd(12)} ${fmtTrend(trend.direct.trend30dPct)}`);
4626
- console.log(` AI Referrals: ${String(traffic.channelBreakdown.ai.sessions).padEnd(6)} (${traffic.channelBreakdown.ai.sharePctDisplay.padStart(4)}) ${fmtTrend(trend.ai.trend7dPct).padEnd(12)} ${fmtTrend(trend.ai.trend30dPct)} (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
4639
+ console.log(` CHANNEL BREAKDOWN${" ".repeat(20)}7d trend 30d trend`);
4640
+ console.log(` Organic Search: ${String(traffic.channelBreakdown.organic.sessions).padEnd(6)} (${traffic.channelBreakdown.organic.sharePctDisplay.padStart(6)}) ${fmtTrend(trend.organic.trend7dPct).padEnd(12)} ${fmtTrend(trend.organic.trend30dPct)}`);
4641
+ console.log(` Social: ${String(traffic.channelBreakdown.social.sessions).padEnd(6)} (${traffic.channelBreakdown.social.sharePctDisplay.padStart(6)}) ${fmtTrend(trend.social.trend7dPct).padEnd(12)} ${fmtTrend(trend.social.trend30dPct)}`);
4642
+ console.log(` Direct: ${String(traffic.channelBreakdown.direct.sessions).padEnd(6)} (${traffic.channelBreakdown.direct.sharePctDisplay.padStart(6)}) ${fmtTrend(trend.direct.trend7dPct).padEnd(12)} ${fmtTrend(trend.direct.trend30dPct)}`);
4643
+ console.log(` AI Referrals: ${String(traffic.channelBreakdown.ai.sessions).padEnd(6)} (${traffic.channelBreakdown.ai.sharePctDisplay.padStart(6)}) ${fmtTrend(trend.ai.trend7dPct).padEnd(12)} ${fmtTrend(trend.ai.trend30dPct)} (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
4627
4644
  if (traffic.paidAiSessionsBySession > 0) {
4628
- console.log(` Paid AI: ${String(traffic.paidAiSessionsBySession).padEnd(6)} (${traffic.paidAiSharePctBySessionDisplay.padStart(4)})`);
4645
+ console.log(` Paid AI: ${String(traffic.paidAiSessionsBySession).padEnd(6)} (${traffic.paidAiSharePctBySessionDisplay.padStart(6)})`);
4629
4646
  }
4630
- console.log(` Other: ${String(traffic.channelBreakdown.other.sessions).padEnd(6)} (${traffic.channelBreakdown.other.sharePctDisplay.padStart(4)})`);
4647
+ console.log(` Other: ${String(traffic.channelBreakdown.other.sessions).padEnd(6)} (${traffic.channelBreakdown.other.sharePctDisplay.padStart(6)})`);
4631
4648
  console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
4632
- console.log(` Total: ${String(traffic.totalSessions).padEnd(6)} ${fmtTrend(trend.total.trend7dPct).padEnd(12)} ${fmtTrend(trend.total.trend30dPct)}`);
4649
+ console.log(` Total: ${String(traffic.totalSessions).padEnd(6)}${" ".repeat(13)}${fmtTrend(trend.total.trend7dPct).padEnd(12)} ${fmtTrend(trend.total.trend30dPct)}`);
4633
4650
  if (trend.aiBiggestMover) {
4634
4651
  const m = trend.aiBiggestMover;
4635
4652
  console.log(`
4636
- AI Mover: ${m.source} (${m.changePct >= 0 ? "+" : ""}${m.changePct}%, ${m.sessionsPrev7d}\u2192${m.sessions7d} sessions/7d)`);
4653
+ AI Mover: ${m.source} (${fmtMoverChange(m)}, ${m.sessionsPrev7d}\u2192${m.sessions7d} sessions/7d)`);
4637
4654
  }
4638
4655
  if (trend.socialBiggestMover) {
4639
4656
  const m = trend.socialBiggestMover;
4640
- console.log(` Social Mover: ${m.source} (${m.changePct >= 0 ? "+" : ""}${m.changePct}%, ${m.sessionsPrev7d}\u2192${m.sessions7d} sessions/7d)`);
4657
+ console.log(` Social Mover: ${m.source} (${fmtMoverChange(m)}, ${m.sessionsPrev7d}\u2192${m.sessions7d} sessions/7d)`);
4641
4658
  }
4642
4659
  if (traffic.lastSyncedAt) {
4643
4660
  console.log(`
@@ -5144,7 +5161,7 @@ async function gbpKeywords(project, opts) {
5144
5161
  console.log("No GBP keyword impressions stored. Run `canonry gbp sync <project>` first.");
5145
5162
  return;
5146
5163
  }
5147
- console.log(`${response.total} keyword(s), ${response.thresholdedPct}% privacy-thresholded. Top by impressions:`);
5164
+ console.log(`${response.total} keyword(s), ${formatPercent(response.thresholdedPct, "percent")} privacy-thresholded. Top by impressions:`);
5148
5165
  for (const k of response.keywords.slice(0, 15)) {
5149
5166
  const val = k.valueCount !== null ? String(k.valueCount) : `<${k.valueThreshold ?? "?"}`;
5150
5167
  console.log(` ${val.padStart(8)} ${k.keyword}`);
@@ -5226,9 +5243,48 @@ async function gbpPlaces(project, opts) {
5226
5243
  console.log(` ${p.locationName} [${p.tier}] ${amenities}`);
5227
5244
  }
5228
5245
  }
5229
- function fmtDelta(pct4) {
5230
- if (pct4 === null) return "n/a";
5231
- return `${pct4 >= 0 ? "+" : ""}${pct4}%`;
5246
+ async function gbpReviews(project, opts) {
5247
+ const client = getClient8();
5248
+ const response = await client.listGbpReviews(project, {
5249
+ locationName: opts.location,
5250
+ ...opts.negative ? { negative: true } : {},
5251
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {}
5252
+ });
5253
+ if (isMachineFormat(opts.format)) {
5254
+ console.log(JSON.stringify(response, null, 2));
5255
+ return;
5256
+ }
5257
+ if (response.locations.length === 0 && response.reviews.length === 0) {
5258
+ console.log('No reviews yet. Run "canonry gbp sync" first.');
5259
+ return;
5260
+ }
5261
+ const names = new Map(response.locations.map((loc) => [loc.locationName, loc.displayName]));
5262
+ for (const loc of response.locations) {
5263
+ const rating = loc.rating !== null ? `${Number(loc.rating.toFixed(2))} from ${loc.reviewCount ?? "?"} reviews (${loc.ratingOrigin === "gbp" ? "Business Profile" : "public listing"})` : "no rating yet";
5264
+ const access = loc.reviewsAccess === "ok" ? "every review" : loc.reviewsAccess === null ? "review access not checked yet" : `Business Profile reviews ${loc.reviewsAccess}${loc.reviewsAccessReason ? ` (${loc.reviewsAccessReason})` : ""}`;
5265
+ console.log(`${loc.displayName} ${loc.locationName}`);
5266
+ console.log(` ${rating}; ${access}`);
5267
+ }
5268
+ const label = opts.negative ? "negative review(s)" : "review(s)";
5269
+ if (response.reviews.length === 0) {
5270
+ console.log(`
5271
+ No ${label} stored.`);
5272
+ return;
5273
+ }
5274
+ const shown = response.reviews.length < response.total ? `, newest ${response.reviews.length} shown` : "";
5275
+ console.log(`
5276
+ ${response.total} ${label}${shown}:`);
5277
+ const manyLocations = response.locations.length > 1;
5278
+ for (const r of response.reviews) {
5279
+ const stars = r.starRating !== null ? `${r.starRating}\u2605` : "--";
5280
+ const where = manyLocations ? ` ${names.get(r.locationName) ?? r.locationName}` : "";
5281
+ const text = r.comment ? `"${r.comment.length > 80 ? `${r.comment.slice(0, 79)}\u2026` : r.comment}"` : "(rating only)";
5282
+ console.log(` ${r.updateTime.slice(0, 10)} ${stars}${where} ${r.reviewerName ?? "Anonymous"} ${text} [${r.origin}, ${r.alertState}]`);
5283
+ }
5284
+ }
5285
+ function fmtDelta(pct) {
5286
+ if (pct === null) return "n/a";
5287
+ return `${pct >= 0 ? "+" : ""}${formatPercent(pct, "percent")}`;
5232
5288
  }
5233
5289
  async function gbpSummary(project, opts) {
5234
5290
  const client = getClient8();
@@ -5254,7 +5310,7 @@ async function gbpSummary(project, opts) {
5254
5310
  }
5255
5311
  }
5256
5312
  console.log(`
5257
- Keywords: ${s.keywords.total} tracked, ${s.keywords.thresholdedPct}% privacy-thresholded`);
5313
+ Keywords: ${s.keywords.total} tracked, ${formatPercent(s.keywords.thresholdedPct, "percent")} privacy-thresholded`);
5258
5314
  console.log(`Place actions: ${s.placeActions.total} CTA(s) \u2014 reservation:${s.placeActions.hasReservationCta ? "yes" : "no"} booking:${s.placeActions.hasBookingCta ? "yes" : "no"} direct-merchant:${s.placeActions.hasDirectMerchantCta ? "yes" : "no"}`);
5259
5315
  console.log(`Lodging API: ${s.lodging.lodgingLocationCount} profile(s), ${s.lodging.populatedLodgingCount} with readable groups, ${s.lodging.emptyLodgingCount} with 0 readable groups`);
5260
5316
  const pc = s.profileCompleteness;
@@ -5446,6 +5502,25 @@ var GBP_CLI_COMMANDS = [
5446
5502
  await gbpPlaces(project, { location: getString(input.values, "location"), format: input.format });
5447
5503
  }
5448
5504
  },
5505
+ {
5506
+ path: ["gbp", "reviews"],
5507
+ usage: "canonry gbp reviews <project> [--location <name>] [--negative] [--limit N] [--format json]",
5508
+ options: {
5509
+ location: stringOption(),
5510
+ negative: { type: "boolean" },
5511
+ limit: stringOption()
5512
+ },
5513
+ run: async (input) => {
5514
+ const usage = "canonry gbp reviews <project> [--location <name>] [--negative] [--limit N] [--format json]";
5515
+ const project = requireProject(input, "gbp.reviews", usage);
5516
+ await gbpReviews(project, {
5517
+ location: getString(input.values, "location"),
5518
+ negative: getBoolean(input.values, "negative") ?? false,
5519
+ limit: parseIntegerOption(input, "limit", { message: "--limit must be an integer", usage, command: "gbp.reviews" }),
5520
+ format: input.format
5521
+ });
5522
+ }
5523
+ },
5449
5524
  {
5450
5525
  path: ["gbp", "summary"],
5451
5526
  usage: "canonry gbp summary <project> [--location <name>] [--format json]",
@@ -5590,13 +5665,10 @@ function performanceMicros(micros, currencyCode) {
5590
5665
  if (currencyCode === null) return (micros / 1e6).toFixed(2);
5591
5666
  return formatMicros(micros, currencyCode);
5592
5667
  }
5593
- function performanceRatio(ratio) {
5594
- return ratio === null ? "\u2014" : `${(ratio * 100).toFixed(2)}%`;
5595
- }
5596
5668
  function performanceChange(ratio) {
5597
5669
  if (ratio === null) return "";
5598
5670
  const sign = ratio > 0 ? "+" : "";
5599
- return ` (${sign}${(ratio * 100).toFixed(1)}% vs prior period)`;
5671
+ return ` (${sign}${formatPercent(ratio)} vs prior period)`;
5600
5672
  }
5601
5673
  async function googleAdsPerformance(client, project, opts) {
5602
5674
  const result = await client.getGoogleAdsPerformance(project, opts.window ? { window: opts.window } : {});
@@ -5612,9 +5684,9 @@ async function googleAdsPerformance(client, project, opts) {
5612
5684
  console.log(`As of: ${result.source.asOfDate}${result.source.openDate ? ` (${result.source.openDate} still open, excluded)` : ""}`);
5613
5685
  console.log(`Account: ${result.source.customerId} (${currency ?? "?"}, ${result.source.timeZone ?? "unknown zone"})`);
5614
5686
  console.log(`Impressions: ${totals.impressions}${performanceChange(comparison?.change.impressions ?? null)}`);
5615
- console.log(`Clicks: ${totals.clicks} (CTR ${performanceRatio(totals.ctr)})${performanceChange(comparison?.change.clicks ?? null)}`);
5687
+ console.log(`Clicks: ${totals.clicks} (CTR ${formatPercent(totals.ctr)})${performanceChange(comparison?.change.clicks ?? null)}`);
5616
5688
  console.log(`Cost: ${performanceMicros(totals.costMicros, currency)} (CPC ${performanceMicros(totals.cpcMicros, currency)})${performanceChange(comparison?.change.costMicros ?? null)}`);
5617
- console.log(`Conversions: ${totals.conversions} (rate ${performanceRatio(totals.conversionRate)}, cost/conv ${performanceMicros(totals.costPerConversionMicros, currency)})`);
5689
+ console.log(`Conversions: ${totals.conversions} (rate ${formatPercent(totals.conversionRate)}, cost/conv ${performanceMicros(totals.costPerConversionMicros, currency)})`);
5618
5690
  if (!comparison) {
5619
5691
  console.log(`Comparison: unavailable (${result.comparisonUnavailableReason ?? "unknown"})`);
5620
5692
  }
@@ -7365,9 +7437,47 @@ async function trafficEvents(project, opts) {
7365
7437
  console.log(` ${formatEventLine(event)}`);
7366
7438
  }
7367
7439
  }
7440
+ async function trafficReferralAssessment(project, options) {
7441
+ const { format, ...query } = options;
7442
+ const parsed = referralAssessmentQuerySchema.safeParse(query);
7443
+ if (!parsed.success) throw new CliError({ code: "TRAFFIC_INVALID_ASSESSMENT", message: parsed.error.message });
7444
+ const result = await createApiClient().trafficReferralAssessment(project, parsed.data);
7445
+ if (isMachineFormat(format)) {
7446
+ console.log(JSON.stringify(result, null, 2));
7447
+ return;
7448
+ }
7449
+ console.log(`AI referral assessment for "${project}" \xB7 ${result.window.startDate} to ${result.window.endDate} UTC`);
7450
+ console.log(` Raw stored hits: ${result.totals.raw.total}`);
7451
+ console.log(` Countable stored hits: ${result.totals.countable.total}`);
7452
+ console.log(` Suspected burst hits: ${result.totals.suspected.total} (not confirmed automation)`);
7453
+ console.log(` Adjusted estimate: ${result.totals.adjustedEstimate.total} (countable minus candidate bursts)`);
7454
+ console.log(` Rule: ${result.rule.version}; threshold ${result.rule.burstThreshold}; ${result.rule.calibration}`);
7455
+ console.log(` Observed server/GA quotient: ${result.comparison.observedRatio ?? "unavailable"}; comparable coverage: unavailable`);
7456
+ console.log(` Evidence: ${result.evidence.returned} of ${result.evidence.total} candidate groups (source, product, normalized path, UTC hour)`);
7457
+ for (const burst of result.bursts) console.log(` ${burst.tsHour} ${burst.sourceId} ${burst.product} ${burst.landingPathNormalized} ${burst.counts.total}`);
7458
+ for (const caveat of result.caveats) console.log(` ${caveat}`);
7459
+ }
7368
7460
 
7369
7461
  // src/cli-commands/traffic.ts
7370
7462
  var TRAFFIC_CLI_COMMANDS = [
7463
+ {
7464
+ path: ["traffic", "referral-assessment"],
7465
+ usage: "canonry traffic referral-assessment <project> --start-date YYYY-MM-DD --end-date YYYY-MM-DD [--source <id>] [--burst-threshold 100] [--ratio-threshold 3] [--limit 100] [--format json]",
7466
+ help: "DB-only diagnostic. Raw headlines stay unchanged. Candidate bursts are not confirmed automation; adjusted counts are estimates. Property, Target and market attribution are unavailable. The observed GA quotient has unknown comparable coverage.",
7467
+ options: { "start-date": stringOption(), "end-date": stringOption(), source: stringOption(), "burst-threshold": stringOption(), "ratio-threshold": stringOption(), limit: stringOption() },
7468
+ run: async (input) => {
7469
+ const project = requireProject(input, "traffic.referral-assessment", "canonry traffic referral-assessment <project> --start-date YYYY-MM-DD --end-date YYYY-MM-DD");
7470
+ await trafficReferralAssessment(project, {
7471
+ startDate: getString(input.values, "start-date"),
7472
+ endDate: getString(input.values, "end-date"),
7473
+ sourceId: getString(input.values, "source"),
7474
+ burstThreshold: getString(input.values, "burst-threshold") === void 0 ? void 0 : Number(getString(input.values, "burst-threshold")),
7475
+ ratioThreshold: getString(input.values, "ratio-threshold") === void 0 ? void 0 : Number(getString(input.values, "ratio-threshold")),
7476
+ limit: getString(input.values, "limit") === void 0 ? void 0 : Number(getString(input.values, "limit")),
7477
+ format: input.format
7478
+ });
7479
+ }
7480
+ },
7371
7481
  {
7372
7482
  path: ["traffic", "connect", "cloudflare"],
7373
7483
  usage: "canonry traffic connect cloudflare <project> [--delivery-mode direct-push|queue-pull] [--display-name <name>] [--zone-id <id>] [--account-id <id>] [--queue-id <id> --queue-name <name> --api-token-file <path> --retention-seconds 345600] [--output-dir <dir>] [--deploy --confirm-route --confirm-fail-open] [--format json]",
@@ -8194,7 +8304,7 @@ async function googlePerformanceDaily(project, opts) {
8194
8304
  }
8195
8305
  console.log(` Clicks: ${clicks.toLocaleString()}`);
8196
8306
  console.log(` Impressions: ${impressions.toLocaleString()}`);
8197
- console.log(` CTR: ${(ctr * 100).toFixed(2)}%`);
8307
+ console.log(` CTR: ${formatPercent(ctr)}`);
8198
8308
  console.log(` Position: ${position == null ? "\u2014" : position.toFixed(1)}`);
8199
8309
  const fits = data.trends;
8200
8310
  const trendLines = [];
@@ -8223,7 +8333,7 @@ Trend (least-squares fit over ${days} day${days === 1 ? "" : "s"}):`);
8223
8333
  No period comparison: period comparison needs property-level daily data. Sync again, or pick a range covered by property totals.`
8224
8334
  );
8225
8335
  } else if (cmp) {
8226
- const pct4 = (ratio, prior, trailing, inverted) => {
8336
+ const pct = (ratio, prior, trailing, inverted) => {
8227
8337
  if (ratio === null) {
8228
8338
  if (prior !== null && trailing !== null && prior <= 0 && trailing > 0) {
8229
8339
  return `new in last ${cmp.days} day${cmp.days === 1 ? "" : "s"}`;
@@ -8234,9 +8344,7 @@ No period comparison: period comparison needs property-level daily data. Sync ag
8234
8344
  }
8235
8345
  if (ratio === 0) return "no change";
8236
8346
  const better = inverted ? ratio < 0 : ratio > 0;
8237
- const magnitude = Math.abs(ratio * 100);
8238
- const shown = magnitude < 0.1 ? "<0.1" : magnitude.toFixed(1);
8239
- return `${ratio > 0 ? "+" : "-"}${shown}% ${better ? "better" : "worse"}`;
8347
+ return `${ratio > 0 ? "+" : "-"}${formatPercent(Math.abs(ratio))} ${better ? "better" : "worse"}`;
8240
8348
  };
8241
8349
  console.log(
8242
8350
  `
@@ -8253,7 +8361,7 @@ Last ${cmp.days} day${cmp.days === 1 ? "" : "s"} (${cmp.trailing.startDate} to $
8253
8361
  ["CTR", cmp.change.ctr, cmp.prior.ctr, cmp.trailing.ctr, false],
8254
8362
  ["Position", cmp.change.position, cmp.prior.position, cmp.trailing.position, true]
8255
8363
  ]) {
8256
- console.log(` ${`${label}:`.padEnd(13)}${pct4(ratio, prior, trailing, inverted)}`);
8364
+ console.log(` ${`${label}:`.padEnd(13)}${pct(ratio, prior, trailing, inverted)}`);
8257
8365
  }
8258
8366
  }
8259
8367
  console.log();
@@ -8261,7 +8369,7 @@ Last ${cmp.days} day${cmp.days === 1 ? "" : "s"} (${cmp.trailing.startDate} to $
8261
8369
  console.log(` ${"\u2500".repeat(12)}${"\u2500".repeat(10)}${"\u2500".repeat(12)}${"\u2500".repeat(10)}${"\u2500".repeat(9)}`);
8262
8370
  for (const row of data.daily) {
8263
8371
  console.log(
8264
- ` ${row.date.padEnd(12)}${row.clicks.toLocaleString().padStart(10)}${row.impressions.toLocaleString().padStart(12)}${(row.ctr * 100).toFixed(2).padStart(9)}%${(row.position == null ? "\u2014" : row.position.toFixed(1)).padStart(9)}`
8372
+ ` ${row.date.padEnd(12)}${row.clicks.toLocaleString().padStart(10)}${row.impressions.toLocaleString().padStart(12)}${formatPercent(row.ctr).padStart(10)}${(row.position == null ? "\u2014" : row.position.toFixed(1)).padStart(9)}`
8265
8373
  );
8266
8374
  }
8267
8375
  }
@@ -8292,7 +8400,7 @@ async function googleTopPages(project, opts) {
8292
8400
  for (const row of data.rows) {
8293
8401
  const page = row.page.length > pageWidth ? row.page.slice(0, pageWidth - 3) + "..." : row.page;
8294
8402
  console.log(
8295
- ` ${page.padEnd(pageWidth)}${row.clicks.toLocaleString().padStart(10)}${row.impressions.toLocaleString().padStart(12)}${(row.ctr * 100).toFixed(2).padStart(9)}%`
8403
+ ` ${page.padEnd(pageWidth)}${row.clicks.toLocaleString().padStart(10)}${row.impressions.toLocaleString().padStart(12)}${formatPercent(row.ctr).padStart(10)}`
8296
8404
  );
8297
8405
  }
8298
8406
  console.log();
@@ -8301,7 +8409,7 @@ async function googleTopPages(project, opts) {
8301
8409
  console.log(`Property total (${days} day${days === 1 ? "" : "s"}, source: ${data.totalsSource}):`);
8302
8410
  console.log(` Clicks: ${clicks.toLocaleString()}`);
8303
8411
  console.log(` Impressions: ${impressions.toLocaleString()}`);
8304
- console.log(` CTR: ${(ctr * 100).toFixed(2)}%`);
8412
+ console.log(` CTR: ${formatPercent(ctr)}`);
8305
8413
  console.log();
8306
8414
  console.log(" The page rows above are a ranking. They do not add up to this total:");
8307
8415
  console.log(" Google withholds rare queries and repeats an impression per page.");
@@ -8310,6 +8418,65 @@ async function googleTopPages(project, opts) {
8310
8418
  console.log(' Adding up the page rows would not give it. Run "canonry google sync" to fetch it.');
8311
8419
  }
8312
8420
  }
8421
+ async function googleQueryTotals(project, opts) {
8422
+ const client = getClient11();
8423
+ const params = {};
8424
+ if (opts.startDate) params.startDate = opts.startDate;
8425
+ if (opts.endDate) params.endDate = opts.endDate;
8426
+ if (opts.window) params.window = opts.window;
8427
+ if (opts.limit !== void 0) params.limit = String(opts.limit);
8428
+ if (opts.offset !== void 0) params.offset = String(opts.offset);
8429
+ const data = await client.gscQueryTotals(project, Object.keys(params).length > 0 ? params : void 0);
8430
+ const { rows, totalMatching, truncated, window } = data;
8431
+ if (opts.format === "json") {
8432
+ console.log(JSON.stringify(data, null, 2));
8433
+ return;
8434
+ } else if (opts.format === "jsonl") {
8435
+ emitJsonl(rows.map((row) => ({ project, window, ...row })));
8436
+ return;
8437
+ }
8438
+ if (rows.length === 0) {
8439
+ if (totalMatching > 0) {
8440
+ console.log(
8441
+ `Offset ${opts.offset ?? 0} is past the end of the result set (${totalMatching.toLocaleString()} queries). Lower --offset to page through them.`
8442
+ );
8443
+ return;
8444
+ }
8445
+ console.log('No GSC query data found in this window. Run "canonry google sync" first.');
8446
+ return;
8447
+ }
8448
+ const range = `${window.startDate ?? "earliest"} to ${window.endDate ?? "latest"}`;
8449
+ console.log(`GSC query totals (${rows.length.toLocaleString()} of ${totalMatching.toLocaleString()} queries, ${range}):
8450
+ `);
8451
+ const showSource = rows.some((row) => row.source !== "google");
8452
+ console.log(
8453
+ ` ${"QUERY".padEnd(40)}${"CLICKS".padStart(8)}${"IMPR".padStart(10)}${"CTR".padStart(8)}${"POS".padStart(7)}${"DAYS".padStart(6)}${showSource ? " SOURCE" : ""}`
8454
+ );
8455
+ console.log(` ${"\u2500".repeat(40)}${"\u2500".repeat(8)}${"\u2500".repeat(10)}${"\u2500".repeat(8)}${"\u2500".repeat(7)}${"\u2500".repeat(6)}${showSource ? ` ${"\u2500".repeat(11)}` : ""}`);
8456
+ for (const row of rows.slice(0, 50)) {
8457
+ const query = row.query.length > 38 ? row.query.slice(0, 35) + "..." : row.query;
8458
+ console.log(
8459
+ ` ${query.padEnd(40)}${row.clicks.toLocaleString().padStart(8)}${row.impressions.toLocaleString().padStart(10)}${formatPercent(row.ctr).padStart(8)}${row.position.toFixed(1).padStart(7)}${String(row.days).padStart(6)}${showSource ? ` ${row.source}` : ""}`
8460
+ );
8461
+ }
8462
+ if (rows.length > 50) {
8463
+ console.log(`
8464
+ ... and ${rows.length - 50} more rows on this page (use --format json for full output)`);
8465
+ }
8466
+ if (truncated) {
8467
+ const nextOffset = (opts.offset ?? 0) + rows.length;
8468
+ console.log(`
8469
+ This is one page. Use --offset ${nextOffset} (with the same --limit) to read the next.`);
8470
+ }
8471
+ console.log();
8472
+ if (showSource) {
8473
+ console.log(" Rows marked page-summed or mixed use the legacy page table for some days. Their impressions");
8474
+ console.log(" over-count, because one search showing several of your pages was stored once per page.");
8475
+ console.log(` Run "canonry google sync" over the window to replace them with Google's per-query totals.`);
8476
+ }
8477
+ console.log(" These are the queries Google names. Google leaves rare queries out, so the rows do not add up");
8478
+ console.log(' to the property total. Use "canonry google performance-daily" for that.');
8479
+ }
8313
8480
  async function googlePerformance(project, opts) {
8314
8481
  const client = getClient11();
8315
8482
  const params = {};
@@ -8357,7 +8524,7 @@ async function googlePerformance(project, opts) {
8357
8524
  for (const row of rows.slice(0, 50)) {
8358
8525
  const query = row.query.length > 28 ? row.query.slice(0, 25) + "..." : row.query;
8359
8526
  console.log(
8360
- ` ${row.date.padEnd(12)}${query.padEnd(30)}${String(row.clicks).padEnd(8)}${String(row.impressions).padEnd(8)}${(row.ctr * 100).toFixed(1).padStart(5)}% ${row.position.toFixed(1).padStart(5)}`
8527
+ ` ${row.date.padEnd(12)}${query.padEnd(30)}${String(row.clicks).padEnd(8)}${String(row.impressions).padEnd(8)}${formatPercent(row.ctr).padStart(6)} ${row.position.toFixed(1).padStart(5)}`
8361
8528
  );
8362
8529
  }
8363
8530
  if (rows.length > 50) {
@@ -8433,10 +8600,12 @@ async function googleCoverage(project, format) {
8433
8600
  }
8434
8601
  const pctColor = summary.percentage >= 80 ? "\x1B[32m" : summary.percentage >= 50 ? "\x1B[33m" : "\x1B[31m";
8435
8602
  const reset = "\x1B[0m";
8603
+ const legacyServer = summary.indexedShare === void 0;
8604
+ const shareText = legacyServer ? `(${formatPercent(summary.percentage, "percent")})` : `(${formatPercent(summary.indexedShare)}) \xB7 ${summary.notIndexed} not indexed (${formatPercent(summary.notIndexedShare)})`;
8436
8605
  console.log(`
8437
8606
  Index Coverage for "${project}"
8438
8607
  `);
8439
- console.log(` SUMMARY: ${pctColor}${summary.indexed} / ${summary.total} pages indexed (${summary.percentage}%)${reset}
8608
+ console.log(` SUMMARY: ${pctColor}${summary.indexed} / ${summary.total} pages indexed ${shareText}${reset}
8440
8609
  `);
8441
8610
  if (result.indexed.length > 0) {
8442
8611
  console.log(` INDEXED (${result.indexed.length}):`);
@@ -8892,6 +9061,7 @@ async function googleDeindexed(project, format) {
8892
9061
 
8893
9062
  // src/cli-commands/google.ts
8894
9063
  var GOOGLE_PERFORMANCE_USAGE = "canonry google performance <project> [--days <n> | --start <YYYY-MM-DD> --end <YYYY-MM-DD>] [--keyword <kw>] [--page <url>] [--limit <n>] [--offset <n>] [--order-by clicks|impressions|date] [--format json]";
9064
+ var GOOGLE_QUERY_TOTALS_USAGE = "canonry google query-totals <project> [--start <YYYY-MM-DD>] [--end <YYYY-MM-DD>] [--window 7d|30d|90d|all] [--limit <n>] [--offset <n>] [--format json]";
8895
9065
  var GOOGLE_CLI_COMMANDS = [
8896
9066
  {
8897
9067
  path: ["google", "connect"],
@@ -9125,6 +9295,36 @@ Usage: ${GOOGLE_PERFORMANCE_USAGE}`,
9125
9295
  });
9126
9296
  }
9127
9297
  },
9298
+ {
9299
+ path: ["google", "query-totals"],
9300
+ usage: GOOGLE_QUERY_TOTALS_USAGE,
9301
+ options: {
9302
+ window: stringOption(),
9303
+ start: stringOption(),
9304
+ end: stringOption(),
9305
+ limit: stringOption(),
9306
+ offset: stringOption()
9307
+ },
9308
+ run: async (input) => {
9309
+ const project = requireProject(input, "google.query-totals", GOOGLE_QUERY_TOTALS_USAGE);
9310
+ await googleQueryTotals(project, {
9311
+ window: getString(input.values, "window"),
9312
+ startDate: getString(input.values, "start"),
9313
+ endDate: getString(input.values, "end"),
9314
+ limit: parseIntegerOption(input, "limit", {
9315
+ command: "google.query-totals",
9316
+ usage: GOOGLE_QUERY_TOTALS_USAGE,
9317
+ message: "--limit must be an integer"
9318
+ }),
9319
+ offset: parseIntegerOption(input, "offset", {
9320
+ command: "google.query-totals",
9321
+ usage: GOOGLE_QUERY_TOTALS_USAGE,
9322
+ message: "--offset must be an integer"
9323
+ }),
9324
+ format: input.format
9325
+ });
9326
+ }
9327
+ },
9128
9328
  {
9129
9329
  path: ["google", "inspect"],
9130
9330
  usage: "canonry google inspect <project> <url> [--format json]",
@@ -9254,12 +9454,12 @@ Usage: ${GOOGLE_PERFORMANCE_USAGE}`,
9254
9454
  },
9255
9455
  {
9256
9456
  path: ["google"],
9257
- usage: "canonry google <connect|disconnect|status|properties|set-property|set-sitemap|list-sitemaps|submit-sitemap|discover-sitemaps|sync|performance|performance-daily|inspect|inspect-sitemap|coverage|coverage-history|inspections|deindexed|request-indexing|refresh> <project> [args]",
9457
+ usage: "canonry google <connect|disconnect|status|properties|set-property|set-sitemap|list-sitemaps|submit-sitemap|discover-sitemaps|sync|performance|performance-daily|top-pages|query-totals|inspect|inspect-sitemap|coverage|coverage-history|inspections|deindexed|request-indexing|refresh> <project> [args]",
9258
9458
  run: async (input) => {
9259
9459
  unknownSubcommand(input.positionals[0], {
9260
9460
  command: "google",
9261
- usage: "canonry google <connect|disconnect|status|properties|set-property|set-sitemap|list-sitemaps|submit-sitemap|discover-sitemaps|sync|performance|performance-daily|inspect|inspect-sitemap|coverage|coverage-history|inspections|deindexed|request-indexing|refresh> <project> [args]",
9262
- available: ["connect", "disconnect", "status", "properties", "set-property", "set-sitemap", "list-sitemaps", "submit-sitemap", "discover-sitemaps", "sync", "performance", "performance-daily", "inspect", "inspect-sitemap", "coverage", "coverage-history", "inspections", "deindexed", "request-indexing", "refresh"]
9461
+ usage: "canonry google <connect|disconnect|status|properties|set-property|set-sitemap|list-sitemaps|submit-sitemap|discover-sitemaps|sync|performance|performance-daily|top-pages|query-totals|inspect|inspect-sitemap|coverage|coverage-history|inspections|deindexed|request-indexing|refresh> <project> [args]",
9462
+ available: ["connect", "disconnect", "status", "properties", "set-property", "set-sitemap", "list-sitemaps", "submit-sitemap", "discover-sitemaps", "sync", "performance", "performance-daily", "top-pages", "query-totals", "inspect", "inspect-sitemap", "coverage", "coverage-history", "inspections", "deindexed", "request-indexing", "refresh"]
9263
9463
  });
9264
9464
  }
9265
9465
  }
@@ -9889,7 +10089,7 @@ var KEYWORD_CLI_COMMANDS = [
9889
10089
  const provider = requireStringOption(input, "provider", {
9890
10090
  command: "keyword.generate",
9891
10091
  usage: "canonry keyword generate <project> --provider <name> [--count <n>] [--save] [--format json]",
9892
- message: "--provider is required (e.g. gemini, openai, claude, perplexity, local)"
10092
+ message: "--provider is required (e.g. gemini, openai, claude, perplexity, muse, local)"
9893
10093
  });
9894
10094
  await generateKeywords(project, provider, {
9895
10095
  count: parseIntegerOption(input, "count", {
@@ -10390,7 +10590,7 @@ function unattributedText(metric) {
10390
10590
  }
10391
10591
  function metricText(metric) {
10392
10592
  if (metric.state === "unavailable") return METRIC_REASONS[metric.reason] ?? `not measured (${metric.reason})`;
10393
- const percent = `${Math.round(metric.value * 100)}%`;
10593
+ const percent = formatPercent(metric.value);
10394
10594
  return metric.numerator === void 0 || metric.denominator === void 0 ? percent : `${metric.numerator} of ${metric.denominator} (${percent})`;
10395
10595
  }
10396
10596
  async function showMeasurementProperty(project, opts) {
@@ -10633,7 +10833,7 @@ var QUERY_CLI_COMMANDS = [
10633
10833
  const provider = requireStringOption(input, "provider", {
10634
10834
  command: "query.generate",
10635
10835
  usage: "canonry query generate <project> --provider <name> [--count <n>] [--save] [--format json]",
10636
- message: "--provider is required (e.g. gemini, openai, claude, perplexity, local)"
10836
+ message: "--provider is required (e.g. gemini, openai, claude, perplexity, muse, local)"
10637
10837
  });
10638
10838
  await generateQueries(project, provider, {
10639
10839
  count: parseIntegerOption(input, "count", {
@@ -11050,7 +11250,9 @@ var EVENT_DESCRIPTIONS = {
11050
11250
  "insight.critical": "A critical-severity insight was generated",
11051
11251
  "insight.high": "A high-severity insight was generated",
11052
11252
  "health.degraded": "A scheduled health check found the measurement degraded, daily data stopped arriving, or the website stopped answering (sent on change, not every pass)",
11053
- "health.recovered": "A degraded health check is passing again, or a website that was paged as down answers again"
11253
+ "health.recovered": "A degraded health check is passing again, or a website that was paged as down answers again",
11254
+ "review.negative": "A new or edited Google review at or below the project's negative-review threshold, 3 stars by default (Business Profile reviews, or the public listing when Google has not enabled review access)",
11255
+ "review.rating-dropped": "A location's public Google rating fell (sent for locations without Business Profile review access)"
11054
11256
  };
11055
11257
  function listEvents(format, target = "local") {
11056
11258
  if (target === "server") return getClient16().listNotificationEvents().then((events) => printEvents(events, format));
@@ -11290,7 +11492,6 @@ function printMetrics(data) {
11290
11492
  console.log(`
11291
11493
  Citation Rate Trends (${data.window})`);
11292
11494
  console.log("\u2500".repeat(50));
11293
- const pct4 = (n) => `${(n * 100).toFixed(1)}%`;
11294
11495
  const modelChangeNotice = buildModelChangeNotice(readModelPointerChanges(data));
11295
11496
  if (modelChangeNotice?.kind === "change") {
11296
11497
  console.log(`
@@ -11298,13 +11499,14 @@ Citation Rate Trends (${data.window})`);
11298
11499
  console.log(` ${modelChangeNotice.text}`);
11299
11500
  console.log("");
11300
11501
  }
11301
- console.log(` Overall: ${pct4(data.overall.citationRate)} (${data.overall.cited}/${data.overall.total})`);
11502
+ console.log(` Overall: ${formatPercent(data.overall.citationRate)} (${data.overall.cited}/${data.overall.total})`);
11302
11503
  console.log(` Trend: ${data.trend}`);
11504
+ printWindowChange(data);
11303
11505
  if (Object.keys(data.byProvider).length > 0) {
11304
11506
  console.log(`
11305
11507
  By Provider:`);
11306
11508
  for (const [provider, metric] of Object.entries(data.byProvider)) {
11307
- console.log(` ${provider.padEnd(10)} ${pct4(metric.citationRate).padStart(6)} (${metric.cited}/${metric.total})`);
11509
+ console.log(` ${provider.padEnd(10)} ${formatPercent(metric.citationRate).padStart(6)} (${metric.cited}/${metric.total})`);
11308
11510
  }
11309
11511
  }
11310
11512
  if (data.buckets.length > 0) {
@@ -11312,7 +11514,7 @@ Citation Rate Trends (${data.window})`);
11312
11514
  Timeline (dates in UTC):`);
11313
11515
  for (const bucket of data.buckets) {
11314
11516
  const bar = bucket.total > 0 ? "\u2588".repeat(Math.round(bucket.citationRate * 20)) : "";
11315
- console.log(` ${bucketDates(bucket).padEnd(BUCKET_DATE_WIDTH)} ${pct4(bucket.citationRate).padStart(6)} ${bar}`);
11517
+ console.log(` ${bucketDates(bucket).padEnd(BUCKET_DATE_WIDTH)} ${formatPercent(bucket.citationRate).padStart(6)} ${bar}`);
11316
11518
  }
11317
11519
  }
11318
11520
  const providersInBuckets = [...new Set(data.buckets.flatMap((b) => Object.keys(b.byProvider ?? {})))].sort();
@@ -11325,7 +11527,7 @@ Citation Rate Trends (${data.window})`);
11325
11527
  const metric = bucket.byProvider?.[provider];
11326
11528
  if (!metric) continue;
11327
11529
  const bar = metric.total > 0 ? "\u2588".repeat(Math.round(metric.citationRate * 20)) : "";
11328
- console.log(` ${bucketDates(bucket).padEnd(BUCKET_DATE_WIDTH)} ${pct4(metric.citationRate).padStart(6)} ${bar}`);
11530
+ console.log(` ${bucketDates(bucket).padEnd(BUCKET_DATE_WIDTH)} ${formatPercent(metric.citationRate).padStart(6)} ${bar}`);
11329
11531
  }
11330
11532
  }
11331
11533
  }
@@ -11373,6 +11575,22 @@ Citation Rate Trends (${data.window})`);
11373
11575
  }
11374
11576
  }
11375
11577
  }
11578
+ function printWindowChange(data) {
11579
+ const windowChange = data.windowChange;
11580
+ if (!windowChange) return;
11581
+ const shareScope = data.mentionShareScope === "non-brand" ? "non-brand queries" : "pooled queries \xB7 classification unavailable";
11582
+ const series = [
11583
+ ["Mentioned", windowChange.mentionRate],
11584
+ ["Cited", windowChange.citationRate],
11585
+ [`Mention share \xB7 ${shareScope}`, windowChange.mentionShare]
11586
+ ];
11587
+ console.log(`
11588
+ Change Across the Window (first \u2192 latest bucket):`);
11589
+ for (const [label, change] of series) {
11590
+ const text = change ? `${formatPercent(change.first)} \u2192 ${formatPercent(change.latest)} (${formatSignedPointDelta(change.delta)})` : "fewer than two buckets to compare";
11591
+ console.log(` ${label}: ${text}`);
11592
+ }
11593
+ }
11376
11594
  function readServedModelAttribution(data) {
11377
11595
  const legacyCompatible = data;
11378
11596
  return legacyCompatible.servedModelAttribution ?? {};
@@ -11432,9 +11650,8 @@ Source Origin Breakdown`);
11432
11650
  return;
11433
11651
  }
11434
11652
  for (const cat of data.overall) {
11435
- const pct4 = `${(cat.percentage * 100).toFixed(1)}%`;
11436
11653
  const domains = cat.topDomains.slice(0, 3).map((d) => d.domain).join(", ");
11437
- console.log(` ${cat.label.padEnd(20)} ${pct4.padStart(6)} (${cat.count}) ${domains}`);
11654
+ console.log(` ${cat.label.padEnd(20)} ${formatPercent(cat.percentage).padStart(6)} (${cat.count}) ${domains}`);
11438
11655
  }
11439
11656
  }
11440
11657
 
@@ -11465,7 +11682,6 @@ Usage: ${SOURCES_USAGE}`, {
11465
11682
  details: { command: "sources", usage: SOURCES_USAGE, option: "include-by-query", value }
11466
11683
  });
11467
11684
  }
11468
- var pct = (n) => `${(n * 100).toFixed(1)}%`;
11469
11685
  async function showSources(project, options) {
11470
11686
  if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit <= 0)) {
11471
11687
  throw usageError(`Error: --limit must be a positive integer
@@ -11537,12 +11753,12 @@ function describeScope(data) {
11537
11753
  function printSurfaceClasses(list) {
11538
11754
  for (const c of list.bySurfaceClass) {
11539
11755
  const noun = c.domainCount === 1 ? "domain" : "domains";
11540
- console.log(` ${c.label.padEnd(28)} ${pct(c.percentage).padStart(6)} (${c.count}) ${c.domainCount} ${noun}`);
11756
+ console.log(` ${c.label.padEnd(28)} ${formatPercent(c.percentage).padStart(6)} (${c.count}) ${c.domainCount} ${noun}`);
11541
11757
  }
11542
11758
  }
11543
11759
  function printRankedEntries(list, indent = "") {
11544
11760
  for (const e of list.entries) {
11545
- console.log(`${indent} ${e.domain.padEnd(32)} ${String(e.count).padStart(4)} ${pct(e.percentage).padStart(6)} ${e.surfaceClass}`);
11761
+ console.log(`${indent} ${e.domain.padEnd(32)} ${String(e.count).padStart(4)} ${formatPercent(e.percentage).padStart(6)} ${e.surfaceClass}`);
11546
11762
  }
11547
11763
  if (list.truncatedDomainCount > 0) {
11548
11764
  console.log(`${indent} \u2026 +${list.truncatedDomainCount} more domains (${list.truncatedCitedSlots} cited slots)`);
@@ -11956,14 +12172,23 @@ async function createProject(name, opts) {
11956
12172
  country: opts.country,
11957
12173
  language: opts.language,
11958
12174
  providers: opts.providers ?? [],
11959
- providerModels: opts.providerModels ?? {}
12175
+ providerModels: opts.providerModels ?? {},
12176
+ ...opts.providerDispatchModes && Object.keys(opts.providerDispatchModes).length > 0 ? { providerDispatchModes: opts.providerDispatchModes } : {}
11960
12177
  });
12178
+ warnDroppedDispatchModes(opts.providerDispatchModes ?? {}, result);
11961
12179
  if (isMachineFormat(opts.format)) {
11962
12180
  console.log(JSON.stringify(result, null, 2));
11963
12181
  return;
11964
12182
  }
11965
12183
  console.log(`Project created: ${result.name} (${result.id})`);
11966
12184
  }
12185
+ function warnDroppedDispatchModes(sent, result) {
12186
+ const kept = result.providerDispatchModes ?? {};
12187
+ const dropped = Object.keys(sent).filter((provider) => !(provider in kept));
12188
+ if (dropped.length > 0) {
12189
+ console.error(`Warning: Dropped dispatch mode for engine(s) the project does not measure: ${dropped.join(", ")}`);
12190
+ }
12191
+ }
11967
12192
  async function listProjects(format) {
11968
12193
  const client = getClient22();
11969
12194
  const projects2 = await client.listProjects();
@@ -12022,6 +12247,9 @@ async function showProject(name, format) {
12022
12247
  console.log(` Providers: ${(project.providers ?? []).length > 0 ? project.providers.join(", ") : "all configured"}`);
12023
12248
  const providerModels = Object.entries(project.providerModels ?? {});
12024
12249
  console.log(` Model overrides: ${providerModels.length > 0 ? providerModels.map(([provider, model]) => `${provider}=${model}`).join(", ") : "(none; instance settings inherited)"}`);
12250
+ const dispatchModes = Object.entries(project.providerDispatchModes ?? {}).sort(([left], [right]) => left.localeCompare(right));
12251
+ console.log(` Dispatch modes: ${dispatchModes.length > 0 ? `${dispatchModes.map(([provider, mode]) => `${provider}=${mode}`).join(", ")} (scheduled sweeps; others sync)` : "(none; every provider runs sync)"}`);
12252
+ console.log(` Negative reviews: ${resolveNegativeReviewMaxStars(project.negativeReviewMaxStars)} stars or fewer${project.negativeReviewMaxStars == null ? " (default)" : ""}`);
12025
12253
  console.log(` Tags: ${project.tags.length > 0 ? project.tags.join(", ") : "(none)"}`);
12026
12254
  const labelEntries = Object.entries(project.labels);
12027
12255
  console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "(none)"}`);
@@ -12066,6 +12294,9 @@ async function updateProjectSettings(name, opts) {
12066
12294
  );
12067
12295
  }
12068
12296
  }
12297
+ const touchesDispatch = Object.keys(opts.dispatchModes ?? {}).length > 0 || (opts.clearDispatchModes ?? []).length > 0;
12298
+ const providerDispatchModes = { ...project.providerDispatchModes ?? {}, ...opts.dispatchModes ?? {} };
12299
+ for (const provider of opts.clearDispatchModes ?? []) delete providerDispatchModes[provider];
12069
12300
  const result = await client.putProject(name, {
12070
12301
  displayName: nextDisplayName,
12071
12302
  canonicalDomain: opts.domain ?? project.canonicalDomain,
@@ -12077,10 +12308,15 @@ async function updateProjectSettings(name, opts) {
12077
12308
  labels: project.labels,
12078
12309
  providers: opts.providers ?? project.providers,
12079
12310
  providerModels,
12311
+ ...touchesDispatch ? { providerDispatchModes } : {},
12080
12312
  locations: project.locations,
12081
12313
  defaultLocation: project.defaultLocation,
12082
- autoExtractBacklinks: project.autoExtractBacklinks
12314
+ autoExtractBacklinks: project.autoExtractBacklinks,
12315
+ // Sent only when asked for: the server keeps the stored threshold when the
12316
+ // field is absent, so a stale read cannot overwrite a newer setting.
12317
+ ...opts.negativeReviewMaxStars !== void 0 ? { negativeReviewMaxStars: opts.negativeReviewMaxStars } : {}
12083
12318
  });
12319
+ warnDroppedDispatchModes(touchesDispatch ? providerDispatchModes : project.providerDispatchModes ?? {}, result);
12084
12320
  if (isMachineFormat(opts.format)) {
12085
12321
  console.log(JSON.stringify(result, null, 2));
12086
12322
  return;
@@ -12193,7 +12429,7 @@ async function setDefaultLocation(project, label, format) {
12193
12429
  var PROJECT_CLI_COMMANDS = [
12194
12430
  {
12195
12431
  path: ["project", "create"],
12196
- usage: "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--format json]",
12432
+ usage: "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--dispatch-mode provider=sync|batch...] [--format json]",
12197
12433
  help: "Create a project. Pass --domain for the public site to scan. Provider credentials are not required for Page Health.",
12198
12434
  options: {
12199
12435
  domain: { type: "string", short: "d" },
@@ -12203,13 +12439,14 @@ var PROJECT_CLI_COMMANDS = [
12203
12439
  language: stringOption(),
12204
12440
  "display-name": stringOption(),
12205
12441
  provider: multiStringOption(),
12206
- "provider-model": multiStringOption()
12442
+ "provider-model": multiStringOption(),
12443
+ "dispatch-mode": multiStringOption()
12207
12444
  },
12208
12445
  run: async (input) => {
12209
12446
  const name = requireProject(
12210
12447
  input,
12211
12448
  "project.create",
12212
- "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--format json]"
12449
+ "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--dispatch-mode provider=sync|batch...] [--format json]"
12213
12450
  );
12214
12451
  await createProject(name, {
12215
12452
  domain: getString(input.values, "domain") ?? name,
@@ -12220,13 +12457,14 @@ var PROJECT_CLI_COMMANDS = [
12220
12457
  displayName: getString(input.values, "display-name") ?? name,
12221
12458
  providers: getStringArray(input.values, "provider") ?? [],
12222
12459
  providerModels: parseProviderModelAssignments(getStringArray(input.values, "provider-model")),
12460
+ providerDispatchModes: parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode")),
12223
12461
  format: input.format
12224
12462
  });
12225
12463
  }
12226
12464
  },
12227
12465
  {
12228
12466
  path: ["project", "update"],
12229
- usage: "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--all-providers] [--provider-model provider=model...] [--clear-provider-model <provider>...] [--format json]",
12467
+ usage: "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--all-providers] [--provider-model provider=model...] [--clear-provider-model <provider>...] [--dispatch-mode provider=sync|batch...] [--clear-dispatch-mode <provider>...] [--negative-review-max-stars <1-4|default>] [--format json]",
12230
12468
  options: {
12231
12469
  domain: { type: "string", short: "d" },
12232
12470
  "owned-domain": multiStringOption(),
@@ -12241,13 +12479,16 @@ var PROJECT_CLI_COMMANDS = [
12241
12479
  provider: multiStringOption(),
12242
12480
  "all-providers": { type: "boolean" },
12243
12481
  "provider-model": multiStringOption(),
12244
- "clear-provider-model": multiStringOption()
12482
+ "clear-provider-model": multiStringOption(),
12483
+ "dispatch-mode": multiStringOption(),
12484
+ "clear-dispatch-mode": multiStringOption(),
12485
+ "negative-review-max-stars": stringOption()
12245
12486
  },
12246
12487
  run: async (input) => {
12247
12488
  const name = requireProject(
12248
12489
  input,
12249
12490
  "project.update",
12250
- "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--all-providers] [--provider-model provider=model...] [--clear-provider-model <provider>...] [--format json]"
12491
+ "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--all-providers] [--provider-model provider=model...] [--clear-provider-model <provider>...] [--dispatch-mode provider=sync|batch...] [--clear-dispatch-mode <provider>...] [--negative-review-max-stars <1-4|default>] [--format json]"
12251
12492
  );
12252
12493
  const providers = getStringArray(input.values, "provider");
12253
12494
  const allProviders = getBoolean(input.values, "all-providers");
@@ -12257,6 +12498,11 @@ var PROJECT_CLI_COMMANDS = [
12257
12498
  for (const provider of clearProviderModels) {
12258
12499
  if (provider in providerModels) throw usageError(`Error: --provider-model and --clear-provider-model conflict for ${provider}`);
12259
12500
  }
12501
+ const dispatchModes = parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode"));
12502
+ const clearDispatchModes = getStringArray(input.values, "clear-dispatch-mode") ?? [];
12503
+ for (const provider of clearDispatchModes) {
12504
+ if (provider in dispatchModes) throw usageError(`Error: --dispatch-mode and --clear-dispatch-mode conflict for ${provider}`);
12505
+ }
12260
12506
  await updateProjectSettings(name, {
12261
12507
  displayName: getString(input.values, "display-name"),
12262
12508
  domain: getString(input.values, "domain"),
@@ -12271,6 +12517,9 @@ var PROJECT_CLI_COMMANDS = [
12271
12517
  providers: allProviders ? [] : providers,
12272
12518
  providerModels,
12273
12519
  clearProviderModels,
12520
+ dispatchModes,
12521
+ clearDispatchModes,
12522
+ negativeReviewMaxStars: parseNegativeReviewMaxStars(getString(input.values, "negative-review-max-stars")),
12274
12523
  format: input.format
12275
12524
  });
12276
12525
  }
@@ -12386,6 +12635,18 @@ var PROJECT_CLI_COMMANDS = [
12386
12635
  }
12387
12636
  }
12388
12637
  ];
12638
+ function parseDispatchModeAssignments(assignments) {
12639
+ const result = {};
12640
+ for (const assignment of assignments ?? []) {
12641
+ const separator = assignment.indexOf("=");
12642
+ const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
12643
+ const mode = providerDispatchModeSchema.safeParse(separator === -1 ? "" : assignment.slice(separator + 1).trim().toLowerCase());
12644
+ if (!provider || !mode.success) throw usageError(`Error: --dispatch-mode must use provider=sync or provider=batch (received ${assignment})`);
12645
+ if (provider in result) throw usageError(`Error: duplicate --dispatch-mode assignment for ${provider}`);
12646
+ result[provider] = mode.data;
12647
+ }
12648
+ return result;
12649
+ }
12389
12650
  function parseProviderModelAssignments(assignments) {
12390
12651
  const result = {};
12391
12652
  for (const assignment of assignments ?? []) {
@@ -12398,6 +12659,18 @@ function parseProviderModelAssignments(assignments) {
12398
12659
  }
12399
12660
  return result;
12400
12661
  }
12662
+ function parseNegativeReviewMaxStars(raw) {
12663
+ if (raw === void 0) return void 0;
12664
+ if (raw === "default") return null;
12665
+ const parsed = gbpNegativeReviewMaxStarsSchema.safeParse(Number(raw));
12666
+ if (!parsed.success) {
12667
+ throw usageError('Error: --negative-review-max-stars must be 1, 2, 3, 4, or "default" (3)', {
12668
+ message: '--negative-review-max-stars must be 1-4 or "default"',
12669
+ details: { command: "project.update", value: raw }
12670
+ });
12671
+ }
12672
+ return parsed.data;
12673
+ }
12401
12674
 
12402
12675
  // src/commands/report.ts
12403
12676
  import fs13 from "fs";
@@ -12533,6 +12806,27 @@ function getClient23() {
12533
12806
  return createApiClient();
12534
12807
  }
12535
12808
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
12809
+ function throwIfWaitedRunFailed(waited) {
12810
+ const failed = waited.filter((r) => r.status === RunStatuses.failed);
12811
+ if (failed.length === 0) return;
12812
+ const reason = (r) => r.error ? formatRunErrorOneLine(r.error) : null;
12813
+ const label = (r) => [r.project, r.location ? `(${r.location})` : null, `run ${r.runId}`].filter(Boolean).join(" ");
12814
+ const message = waited.length === 1 ? `Run ${failed[0].runId} failed${reason(failed[0]) ? `: ${reason(failed[0])}` : ""}` : `${failed.length} of ${waited.length} runs failed: ${failed.map((r) => `${label(r)}${reason(r) ? `: ${reason(r)}` : ""}`).join("; ")}`;
12815
+ throw new CliError({
12816
+ code: "RUN_FAILED",
12817
+ message,
12818
+ exitCode: EXIT_SYSTEM_ERROR,
12819
+ details: {
12820
+ waitedRunCount: waited.length,
12821
+ failedRuns: failed.map((r) => ({
12822
+ runId: r.runId,
12823
+ ...r.project ? { project: r.project } : {},
12824
+ ...r.location ? { location: r.location } : {},
12825
+ error: reason(r)
12826
+ }))
12827
+ }
12828
+ });
12829
+ }
12536
12830
  async function triggerRun(project, opts) {
12537
12831
  const client = getClient23();
12538
12832
  const body = {};
@@ -12562,19 +12856,22 @@ async function triggerRun(project, opts) {
12562
12856
  if (opts?.probe) {
12563
12857
  body.trigger = "probe";
12564
12858
  }
12859
+ if (opts?.dispatchMode) {
12860
+ body.dispatchMode = opts.dispatchMode;
12861
+ }
12565
12862
  const response = await client.triggerRun(project, body);
12566
12863
  if (Array.isArray(response)) {
12567
12864
  const locationRuns = response;
12568
12865
  if (isMachineFormat(opts?.format)) {
12569
12866
  if (opts?.wait) {
12570
- const settled = await Promise.all(
12571
- locationRuns.map(async (r) => {
12572
- if (!r.id || r.status === "conflict") return r;
12573
- const final = await pollRun2(client, r.id);
12574
- return { ...r, ...final };
12575
- })
12867
+ const finals = await Promise.all(
12868
+ locationRuns.map(async (r) => !r.id || r.status === "conflict" ? null : pollRun2(client, r.id, false))
12576
12869
  );
12577
- console.log(JSON.stringify(settled, null, 2));
12870
+ console.log(JSON.stringify(locationRuns.map((r, i) => finals[i] ? { ...r, ...finals[i] } : r), null, 2));
12871
+ throwIfWaitedRunFailed(locationRuns.flatMap((r, i) => {
12872
+ const final = finals[i];
12873
+ return final ? [{ runId: final.id, status: final.status, project, location: r.location ?? null, error: final.error }] : [];
12874
+ }));
12578
12875
  } else {
12579
12876
  console.log(JSON.stringify(locationRuns, null, 2));
12580
12877
  }
@@ -12591,12 +12888,17 @@ async function triggerRun(project, opts) {
12591
12888
  }
12592
12889
  if (opts?.wait) {
12593
12890
  const pending = locationRuns.filter((r) => r.id && r.status !== "conflict" && !TERMINAL_STATUSES.has(r.status));
12891
+ const errors = /* @__PURE__ */ new Map();
12594
12892
  if (pending.length > 0) {
12595
12893
  process.stderr.write(`Waiting for ${pending.length} run(s)`);
12894
+ const batchWaits = /* @__PURE__ */ new Map();
12596
12895
  await Promise.all(
12597
12896
  pending.map(async (r) => {
12598
12897
  const final = await pollRun2(client, r.id);
12599
12898
  r.status = final.status;
12899
+ errors.set(r.id, final.error);
12900
+ const waiting = batchWaitLine(final);
12901
+ if (waiting) batchWaits.set(r.id, waiting);
12600
12902
  })
12601
12903
  );
12602
12904
  process.stderr.write("\n");
@@ -12605,20 +12907,31 @@ async function triggerRun(project, opts) {
12605
12907
  const loc = (r.location ?? "(unknown)").padEnd(15);
12606
12908
  console.log(` ${loc} ${r.status}`);
12607
12909
  }
12910
+ if (batchWaits.size > 0) console.log("");
12911
+ for (const r of locationRuns) {
12912
+ const waiting = batchWaits.get(r.id);
12913
+ if (waiting) console.log(` ${r.location ?? "(unknown)"}: ${waiting}`);
12914
+ }
12608
12915
  }
12916
+ throwIfWaitedRunFailed(locationRuns.filter((r) => r.id && r.status !== "conflict").map((r) => ({ runId: r.id, status: r.status, project, location: r.location ?? null, error: errors.get(r.id) })));
12609
12917
  }
12610
12918
  return;
12611
12919
  }
12612
12920
  const run = response;
12613
12921
  if (opts?.wait && run.id && !TERMINAL_STATUSES.has(run.status)) {
12614
- process.stderr.write(`Run ${run.id} started`);
12615
- const result = await pollRun2(client, run.id);
12922
+ const showProgress = !isMachineFormat(opts?.format);
12923
+ if (showProgress) process.stderr.write(`Run ${run.id} started`);
12924
+ const result = await pollRun2(client, run.id, showProgress);
12616
12925
  if (isMachineFormat(opts?.format)) {
12617
12926
  console.log(JSON.stringify(result, null, 2));
12618
12927
  } else {
12619
12928
  process.stderr.write("\n");
12620
12929
  printRunDetail(result);
12930
+ const waiting = batchWaitLine(result);
12931
+ if (waiting) console.log(`
12932
+ ${waiting}`);
12621
12933
  }
12934
+ throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project, error: result.error }]);
12622
12935
  return;
12623
12936
  }
12624
12937
  if (opts?.wait && (TERMINAL_STATUSES.has(run.status) || !run.id)) {
@@ -12628,6 +12941,7 @@ async function triggerRun(project, opts) {
12628
12941
  } else {
12629
12942
  printRunDetail(result);
12630
12943
  }
12944
+ throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project, error: result.error }]);
12631
12945
  return;
12632
12946
  }
12633
12947
  if (isMachineFormat(opts?.format)) {
@@ -12664,6 +12978,9 @@ async function triggerRunAll(opts) {
12664
12978
  if (opts?.noLocation) {
12665
12979
  baseBody.noLocation = true;
12666
12980
  }
12981
+ if (opts?.dispatchMode) {
12982
+ baseBody.dispatchMode = opts.dispatchMode;
12983
+ }
12667
12984
  const results = [];
12668
12985
  for (const p of projects2) {
12669
12986
  const body = { ...baseBody };
@@ -12686,19 +13003,27 @@ async function triggerRunAll(opts) {
12686
13003
  results.push({ project: p.name, runId: "", status: "error", location: null, error: msg });
12687
13004
  }
12688
13005
  }
13006
+ const errors = /* @__PURE__ */ new Map();
13007
+ const batchWaits = /* @__PURE__ */ new Map();
12689
13008
  if (opts?.wait) {
12690
13009
  const pending = results.filter((r) => r.runId && !TERMINAL_STATUSES.has(r.status));
12691
13010
  if (pending.length > 0) {
12692
- process.stderr.write(`Waiting for ${pending.length} run(s)`);
13011
+ const showProgress = !isMachineFormat(opts?.format);
13012
+ if (showProgress) process.stderr.write(`Waiting for ${pending.length} run(s)`);
12693
13013
  await Promise.all(pending.map(async (r) => {
12694
- const final = await pollRun2(client, r.runId);
13014
+ const final = await pollRun2(client, r.runId, showProgress);
12695
13015
  r.status = final.status;
13016
+ errors.set(r.runId, final.error);
13017
+ const waiting = batchWaitLine(final);
13018
+ if (waiting) batchWaits.set(r.runId, waiting);
12696
13019
  }));
12697
- process.stderr.write("\n");
13020
+ if (showProgress) process.stderr.write("\n");
12698
13021
  }
12699
13022
  }
13023
+ const waited = results.filter((r) => r.runId).map((r) => ({ runId: r.runId, status: r.status, project: r.project, location: r.location, error: errors.get(r.runId) }));
12700
13024
  if (isMachineFormat(opts?.format)) {
12701
13025
  console.log(JSON.stringify(results, null, 2));
13026
+ if (opts?.wait) throwIfWaitedRunFailed(waited);
12702
13027
  return;
12703
13028
  }
12704
13029
  const showLocationColumn = results.some((r) => r.location !== null);
@@ -12723,6 +13048,12 @@ async function triggerRunAll(opts) {
12723
13048
  console.log(` ${proj} ${id} ${r.status}`);
12724
13049
  }
12725
13050
  }
13051
+ if (batchWaits.size > 0) console.log("");
13052
+ for (const r of results) {
13053
+ const waiting = batchWaits.get(r.runId);
13054
+ if (waiting) console.log(` ${r.location ? `${r.project} (${r.location})` : r.project}: ${waiting}`);
13055
+ }
13056
+ if (opts?.wait) throwIfWaitedRunFailed(waited);
12726
13057
  }
12727
13058
  async function cancelRun(project, runId, format) {
12728
13059
  const client = getClient23();
@@ -12806,7 +13137,7 @@ async function listRuns(project, opts) {
12806
13137
  }
12807
13138
  }
12808
13139
  var POLL_TIMEOUT_MS3 = 10 * 60 * 1e3;
12809
- async function pollRun2(client, runId) {
13140
+ async function pollRun2(client, runId, showProgress = true) {
12810
13141
  const deadline = Date.now() + POLL_TIMEOUT_MS3;
12811
13142
  for (; ; ) {
12812
13143
  await new Promise((r) => setTimeout(r, 2e3));
@@ -12814,12 +13145,22 @@ async function pollRun2(client, runId) {
12814
13145
  throw new Error(`Timed out waiting for run ${runId} after ${POLL_TIMEOUT_MS3 / 1e3}s`);
12815
13146
  }
12816
13147
  const run = await client.getRun(runId);
12817
- process.stderr.write(".");
12818
- if (TERMINAL_STATUSES.has(run.status)) {
13148
+ if (showProgress) process.stderr.write(".");
13149
+ if (TERMINAL_STATUSES.has(run.status) || outstandingBatches(run).length > 0) {
12819
13150
  return run;
12820
13151
  }
12821
13152
  }
12822
13153
  }
13154
+ function outstandingBatches(run) {
13155
+ if (TERMINAL_STATUSES.has(run.status)) return [];
13156
+ return (run.providerBatches ?? []).filter((batch) => OUTSTANDING_PROVIDER_BATCH_STATUSES.includes(batch.status));
13157
+ }
13158
+ function batchWaitLine(run) {
13159
+ const outstanding = outstandingBatches(run);
13160
+ if (outstanding.length === 0) return null;
13161
+ const batches = outstanding.map((batch) => `${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`);
13162
+ return `Waiting on provider batch(es): ${batches.join("; ")}; check with canonry run show ${run.id}`;
13163
+ }
12823
13164
  function printRunDetail(run) {
12824
13165
  console.log(`Run: ${run.id}`);
12825
13166
  console.log(` Status: ${run.status}`);
@@ -12836,6 +13177,13 @@ function printRunDetail(run) {
12836
13177
  }
12837
13178
  }
12838
13179
  }
13180
+ const batched = Object.keys(run.dispatchModes ?? {}).sort();
13181
+ if (batched.length > 0) console.log(` Dispatch: ${batched.map((provider) => `${provider}=batch`).join(", ")} (other providers sync)`);
13182
+ if (run.providerBatches && run.providerBatches.length > 0) {
13183
+ console.log("\n Provider batches:");
13184
+ for (const batch of run.providerBatches) console.log(` ${providerBatchLine(batch)}`);
13185
+ }
13186
+ if (run.usage && run.usage.length > 0) printUsageTable(run.usage);
12839
13187
  if (run.snapshots && run.snapshots.length > 0) {
12840
13188
  console.log(`
12841
13189
  Snapshots: ${run.snapshots.length} (cell = [citation][mention]; C=cited c=not, M=mentioned m=not, \u2013=no data)`);
@@ -12847,6 +13195,46 @@ function printRunDetail(run) {
12847
13195
  }
12848
13196
  }
12849
13197
  }
13198
+ function providerBatchLine(batch) {
13199
+ const recorded = `${batch.recordedCount} of ${batch.requestCount} answers recorded`;
13200
+ const withError = (text) => batch.error ? `${text} (${batch.error})` : text;
13201
+ switch (batch.status) {
13202
+ case ProviderBatchStatuses.submitting:
13203
+ return `submitting provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests`;
13204
+ // The two outstanding statuses: the run stays `running` until they clear.
13205
+ case ProviderBatchStatuses.submitted:
13206
+ case ProviderBatchStatuses.ended:
13207
+ return `waiting on provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`;
13208
+ case ProviderBatchStatuses.ingested:
13209
+ return withError(`provider batch ${batch.provider}: ingested \u2014 ${recorded}`);
13210
+ case ProviderBatchStatuses.cancelled:
13211
+ return withError(`provider batch ${batch.provider}: cancelled \u2014 ${recorded}; the rest are missing and can be filled`);
13212
+ case ProviderBatchStatuses.failed:
13213
+ return withError(`provider batch ${batch.provider}: rejected by the provider \u2014 its ${batch.requestCount} answers ran sync instead`);
13214
+ case ProviderBatchStatuses.unknown:
13215
+ return withError(`provider batch ${batch.provider}: submit outcome unknown \u2014 never resubmitted; its ${batch.requestCount} answers stay missing`);
13216
+ }
13217
+ }
13218
+ function printUsageTable(rows) {
13219
+ const cost = (row) => row.estimatedCostMicros === null ? "unpriced" : `${formatMicros(row.estimatedCostMicros, "USD", { fractionDigits: 4, showTinyAsLessThan: true })}${row.unpricedAnswers > 0 ? ` (+${row.unpricedAnswers} unpriced)` : ""}`;
13220
+ const table = rows.map((row) => [
13221
+ row.provider,
13222
+ row.pricingTier,
13223
+ row.answers.toLocaleString("en-US"),
13224
+ row.inputTokens.toLocaleString("en-US"),
13225
+ row.cachedInputTokens.toLocaleString("en-US"),
13226
+ row.cacheWriteTokens.toLocaleString("en-US"),
13227
+ row.outputTokens.toLocaleString("en-US"),
13228
+ row.searchCount.toLocaleString("en-US"),
13229
+ cost(row)
13230
+ ]);
13231
+ const header = ["PROVIDER", "TIER", "ANSWERS", "INPUT", "CACHED", "CACHE WRITE", "OUTPUT", "SEARCHES", "EST. COST"];
13232
+ const widths = header.map((title, index) => Math.max(title.length, ...table.map((cells) => cells[index].length)));
13233
+ const line = (cells) => ` ${cells.map((cell2, index) => cell2.padEnd(widths[index])).join(" ").trimEnd()}`;
13234
+ console.log("\n Usage (answers with recorded usage; cost is an estimate):");
13235
+ console.log(line(header));
13236
+ for (const cells of table) console.log(line(cells));
13237
+ }
12850
13238
  var FILL_POLL_INTERVAL_MS = 3e3;
12851
13239
  var FILL_POLL_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
12852
13240
  function missingSummary(completeness) {
@@ -12934,8 +13322,8 @@ async function fillRun(runId, opts = {}) {
12934
13322
  }
12935
13323
 
12936
13324
  // src/cli-commands/run.ts
12937
- var RUN_TRIGGER_USAGE = "canonry run trigger <project> [--group <key>]... [--target <key>]... [--provider <name>] [--query <q>...] [--location <label>] [--all-locations] [--no-location] [--probe] [--wait] [--format json]";
12938
- var RUN_USAGE2 = "canonry run <project|--all> [--group <key>]... [--target <key>]... [--provider <name>] [--query <q>...] [--location <label>] [--all-locations] [--no-location] [--probe] [--wait] [--format json]";
13325
+ var RUN_TRIGGER_USAGE = "canonry run trigger <project> [--group <key>]... [--target <key>]... [--provider <name>] [--query <q>...] [--location <label>] [--all-locations] [--no-location] [--probe] [--dispatch-mode sync|batch] [--wait] [--format json]";
13326
+ var RUN_USAGE2 = "canonry run <project|--all> [--group <key>]... [--target <key>]... [--provider <name>] [--query <q>...] [--location <label>] [--all-locations] [--no-location] [--probe] [--dispatch-mode sync|batch] [--wait] [--format json]";
12939
13327
  var RUN_FILL_USAGE = "canonry run fill <run-id> [--provider <name>[,<name>]] [--dry-run] [--wait] [--format json]";
12940
13328
  var RUNS_USAGE = "canonry runs <project> [--limit <n>] [--kind <kind>] [--status <status>] [--format json]";
12941
13329
  var RUN_TRIGGER_OPTIONS = {
@@ -12954,13 +13342,26 @@ var RUN_TRIGGER_OPTIONS = {
12954
13342
  // for inspection but are excluded from dashboard / analytics /
12955
13343
  // intelligence / notifications. Use when you want to verify a
12956
13344
  // provider works without polluting the project's metrics.
12957
- probe: { type: "boolean", default: false }
13345
+ probe: { type: "boolean", default: false },
13346
+ // `batch` sends every provider that can to its asynchronous batch API (a
13347
+ // full plan sweep, enabled in config.yaml); refused when none can.
13348
+ "dispatch-mode": stringOption()
12958
13349
  };
13350
+ function parseDispatchMode(value, command, usage) {
13351
+ if (value === void 0) return void 0;
13352
+ const parsed = providerDispatchModeSchema.safeParse(value.trim().toLowerCase());
13353
+ if (parsed.success) return parsed.data;
13354
+ throw usageError(`Error: --dispatch-mode must be one of: ${providerDispatchModeSchema.options.join(", ")} (received ${value})`, {
13355
+ message: "--dispatch-mode must be sync or batch",
13356
+ details: { command, usage }
13357
+ });
13358
+ }
12959
13359
  async function triggerRunCommand(input, command) {
12960
13360
  const groups = getStringArray(input.values, "group") ?? [];
12961
13361
  const targets = getStringArray(input.values, "target") ?? [];
12962
13362
  const queries2 = getStringArray(input.values, "query") ?? [];
12963
13363
  const usage = command === "run" ? RUN_USAGE2 : RUN_TRIGGER_USAGE;
13364
+ const dispatchMode = parseDispatchMode(getString(input.values, "dispatch-mode"), command, usage);
12964
13365
  if (getBoolean(input.values, "all")) {
12965
13366
  if (input.positionals.length > 0) {
12966
13367
  throw usageError("Error: --all cannot be combined with a project name", {
@@ -12991,6 +13392,7 @@ async function triggerRunCommand(input, command) {
12991
13392
  wait: getBoolean(input.values, "wait"),
12992
13393
  allLocations: getBoolean(input.values, "all-locations"),
12993
13394
  noLocation: getBoolean(input.values, "no-location"),
13395
+ dispatchMode,
12994
13396
  format: input.format
12995
13397
  });
12996
13398
  return;
@@ -13017,6 +13419,7 @@ async function triggerRunCommand(input, command) {
13017
13419
  allLocations: getBoolean(input.values, "all-locations"),
13018
13420
  noLocation: getBoolean(input.values, "no-location"),
13019
13421
  probe: getBoolean(input.values, "probe"),
13422
+ dispatchMode,
13020
13423
  format: input.format
13021
13424
  });
13022
13425
  }
@@ -13474,7 +13877,7 @@ var SETTINGS_CLI_COMMANDS = [
13474
13877
  const name = requirePositional(input, 0, {
13475
13878
  command: "settings.provider",
13476
13879
  usage: "canonry settings provider <name> [--api-key <key>] [--base-url <url>] [--model <model>] [--max-concurrent <n>] [--max-per-minute <n>] [--max-per-day <n>] [--format json]",
13477
- message: "provider name is required (e.g. gemini, openai, claude, perplexity, local)"
13880
+ message: "provider name is required (e.g. gemini, openai, claude, perplexity, muse, local)"
13478
13881
  });
13479
13882
  const apiKey = getString(input.values, "api-key");
13480
13883
  const baseUrl = getString(input.values, "base-url");
@@ -14289,9 +14692,9 @@ async function showHealth(project, opts) {
14289
14692
  console.log("Date Mention Rate Mentioned/Total Cited Rate Cited/Total");
14290
14693
  console.log("\u2500".repeat(86));
14291
14694
  for (const snap of snapshots) {
14292
- const mRate = (snap.overallMentionRate * 100).toFixed(1).padStart(5) + "%";
14695
+ const mRate = formatPercent(snap.overallMentionRate).padStart(6);
14293
14696
  const mRatio = `${snap.mentionedPairs}/${snap.totalPairs}`.padEnd(15);
14294
- const cRate = (snap.overallCitedRate * 100).toFixed(1).padStart(5) + "%";
14697
+ const cRate = formatPercent(snap.overallCitedRate).padStart(6);
14295
14698
  const cRatio = `${snap.citedPairs}/${snap.totalPairs}`;
14296
14699
  const date = snap.createdAt.slice(0, 19).padEnd(25);
14297
14700
  console.log(`${date} ${mRate} ${mRatio} ${cRate} ${cRatio}`);
@@ -14307,17 +14710,13 @@ async function showHealth(project, opts) {
14307
14710
  console.log("No health data yet \u2014 run a sweep first (canonry run <project>).");
14308
14711
  return;
14309
14712
  }
14310
- const mentionRate = (health.overallMentionRate * 100).toFixed(1);
14311
- const citedRate = (health.overallCitedRate * 100).toFixed(1);
14312
- console.log(`Health: ${mentionRate}% mentioned (${health.mentionedPairs}/${health.totalPairs} pairs)`);
14313
- console.log(` ${citedRate}% cited (${health.citedPairs}/${health.totalPairs} pairs)`);
14713
+ console.log(`Health: ${formatPercent(health.overallMentionRate)} mentioned (${health.mentionedPairs}/${health.totalPairs} pairs)`);
14714
+ console.log(` ${formatPercent(health.overallCitedRate)} cited (${health.citedPairs}/${health.totalPairs} pairs)`);
14314
14715
  console.log("");
14315
14716
  if (health.providerBreakdown && Object.keys(health.providerBreakdown).length > 0) {
14316
14717
  console.log("Provider Breakdown:");
14317
14718
  for (const [provider, stats] of Object.entries(health.providerBreakdown)) {
14318
- const pMention = (stats.mentionRate * 100).toFixed(1);
14319
- const pCited = (stats.citedRate * 100).toFixed(1);
14320
- console.log(` ${provider.padEnd(15)} ${pMention}% mentioned (${stats.mentioned}/${stats.total}) ${pCited}% cited (${stats.cited}/${stats.total})`);
14719
+ console.log(` ${provider.padEnd(15)} ${formatPercent(stats.mentionRate)} mentioned (${stats.mentioned}/${stats.total}) ${formatPercent(stats.citedRate)} cited (${stats.cited}/${stats.total})`);
14321
14720
  }
14322
14721
  }
14323
14722
  }
@@ -14417,8 +14816,8 @@ function renderHuman2(overview) {
14417
14816
  printScore("Competitor press.", scores.competitorPressure);
14418
14817
  printScore("Run status ", scores.runStatus);
14419
14818
  console.log(`
14420
- Queries cited: ${queryCounts.citedQueries}/${queryCounts.totalQueries} (${pct2(queryCounts.citedRate)})`);
14421
- console.log(` Queries mentioned: ${queryCounts.mentionedQueries}/${queryCounts.totalQueries} (${pct2(queryCounts.mentionRate)})`);
14819
+ Queries cited: ${queryCounts.citedQueries}/${queryCounts.totalQueries} (${formatPercent(queryCounts.citedRate)})`);
14820
+ console.log(` Queries mentioned: ${queryCounts.mentionedQueries}/${queryCounts.totalQueries} (${formatPercent(queryCounts.mentionRate)})`);
14422
14821
  if (movementComparison.hasPreviousRun) {
14423
14822
  const comparisonLabel = movementComparison.querySetChanged ? `changed (+${movementComparison.addedQueryCount} added, -${movementComparison.removedQueryCount} removed); movement compares ${movementComparison.comparableQueryCount} shared` : `unchanged; ${movementComparison.comparableQueryCount} comparable`;
14424
14823
  console.log(` Query basket: ${comparisonLabel}`);
@@ -14430,14 +14829,14 @@ function renderHuman2(overview) {
14430
14829
  if (providers.length > 0) {
14431
14830
  console.log("\n Providers:");
14432
14831
  for (const p of providers) {
14433
- console.log(` ${p.provider.padEnd(12)} ${p.cited}/${p.total} (${pct2(p.citedRate)})`);
14832
+ console.log(` ${p.provider.padEnd(12)} ${p.cited}/${p.total} (${formatPercent(p.citedRate)})`);
14434
14833
  }
14435
14834
  }
14436
14835
  if (providerScores.length > 0) {
14437
14836
  console.log("\n Models:");
14438
14837
  for (const m of providerScores) {
14439
14838
  const label = `${m.provider}/${m.model ?? "unknown"}`.padEnd(28);
14440
- console.log(` ${label} ${m.cited}/${m.total} (${m.score}%)`);
14839
+ console.log(` ${label} ${m.cited}/${m.total} (${formatPercent(m.score, "percent")})`);
14441
14840
  }
14442
14841
  }
14443
14842
  if (transitions.since) {
@@ -14459,7 +14858,7 @@ function renderHuman2(overview) {
14459
14858
  }
14460
14859
  if (health) {
14461
14860
  console.log(`
14462
- Health: ${pct2(health.overallCitedRate)} cited (${health.citedPairs}/${health.totalPairs} pairs)`);
14861
+ Health: ${formatPercent(health.overallCitedRate)} cited (${health.citedPairs}/${health.totalPairs} pairs)`);
14463
14862
  }
14464
14863
  if (topInsights.length > 0) {
14465
14864
  console.log("\n Top insights:");
@@ -14472,7 +14871,7 @@ function renderHuman2(overview) {
14472
14871
  Run history (last ${runHistory.length}):`);
14473
14872
  for (const point of runHistory) {
14474
14873
  const bar = "\u2588".repeat(Math.round(point.citationRate / 10));
14475
- console.log(` ${point.createdAt.slice(0, 10)} ${String(point.citationRate).padStart(3)}% ${bar}`);
14874
+ console.log(` ${point.createdAt.slice(0, 10)} ${formatPercent(point.citationRate, "percent").padStart(6)} ${bar}`);
14476
14875
  }
14477
14876
  }
14478
14877
  if (suggestedQueries.rows.length > 0) {
@@ -14492,23 +14891,19 @@ function printScore(prefix, score) {
14492
14891
  console.log(` ${prefix} ${tone} ${value} ${score.delta}`);
14493
14892
  }
14494
14893
  function printMentionShareBreakdown(mentionShare) {
14495
- const { breakdown } = mentionShare;
14496
- if (breakdown.perCompetitor.length === 0) return;
14497
- const total = breakdown.projectMentionSnapshots + breakdown.competitorMentionSnapshots;
14498
- if (total === 0) return;
14499
- const youPct = (breakdown.projectMentionSnapshots / total * 100).toFixed(1);
14500
- console.log(` you${" ".repeat(28)} ${breakdown.projectMentionSnapshots} mentions (${youPct}% of combined)`);
14501
- for (const row of breakdown.perCompetitor.slice(0, 3)) {
14502
- const pct4 = (row.mentionSnapshots / total * 100).toFixed(1);
14503
- console.log(` ${row.domain.padEnd(30)} ${row.mentionSnapshots} mentions (${pct4}% of combined)`);
14894
+ const legacyCompatible = mentionShare.breakdown;
14895
+ const ranking = legacyCompatible.ranking ?? [];
14896
+ const project = ranking.find((row) => row.kind === "project");
14897
+ if (!project) return;
14898
+ const competitors = ranking.filter((row) => row.kind === "competitor");
14899
+ console.log(` you${" ".repeat(28)} ${project.mentionSnapshots} mentions (${formatPercent(project.share)} of combined)`);
14900
+ for (const row of competitors.slice(0, 3)) {
14901
+ console.log(` ${(row.domain ?? "").padEnd(30)} ${row.mentionSnapshots} mentions (${formatPercent(row.share)} of combined)`);
14504
14902
  }
14505
- if (breakdown.perCompetitor.length > 3) {
14506
- console.log(` + ${breakdown.perCompetitor.length - 3} more competitor${breakdown.perCompetitor.length - 3 === 1 ? "" : "s"} (--format json for full breakdown)`);
14903
+ if (competitors.length > 3) {
14904
+ console.log(` + ${competitors.length - 3} more competitor${competitors.length - 3 === 1 ? "" : "s"} (--format json for full breakdown)`);
14507
14905
  }
14508
14906
  }
14509
- function pct2(value) {
14510
- return `${(value * 100).toFixed(1)}%`;
14511
- }
14512
14907
 
14513
14908
  // src/commands/search.ts
14514
14909
  async function searchProject(project, opts) {
@@ -14839,9 +15234,9 @@ async function listContentGaps(project, opts) {
14839
15234
  console.log(`${response.gaps.length} gap${response.gaps.length === 1 ? "" : "s"} found`);
14840
15235
  console.log("");
14841
15236
  for (const gap of response.gaps) {
14842
- const missPct = formatWholePercent(gap.missRate);
14843
- console.log(`${missPct.padStart(4)} ${gap.competitorCount} competitor(s) ${gap.query}`);
14844
- console.log(` competitors: ${gap.competitorDomains.join(", ")}`);
15237
+ const missPct = formatPercent(gap.missRate);
15238
+ console.log(`${missPct.padStart(6)} ${gap.competitorCount} competitor(s) ${gap.query}`);
15239
+ console.log(` competitors: ${gap.competitorDomains.join(", ")}`);
14845
15240
  console.log("");
14846
15241
  }
14847
15242
  }
@@ -15060,6 +15455,7 @@ async function bootstrapCommand(opts) {
15060
15455
  if (providers.openai) mergedProviders.openai = providers.openai;
15061
15456
  if (providers.claude) mergedProviders.claude = providers.claude;
15062
15457
  if (providers.perplexity) mergedProviders.perplexity = providers.perplexity;
15458
+ if (providers.muse) mergedProviders.muse = providers.muse;
15063
15459
  if (providers.local) mergedProviders.local = providers.local;
15064
15460
  if (env.googleClientId && !env.googleClientSecret || !env.googleClientId && env.googleClientSecret) {
15065
15461
  console.warn("Warning: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must both be set to configure Google OAuth. Skipping Google auth config.");
@@ -15453,6 +15849,7 @@ async function initCommand(opts) {
15453
15849
  OPENAI_API_KEY: opts?.openaiKey,
15454
15850
  ANTHROPIC_API_KEY: opts?.claudeKey,
15455
15851
  PERPLEXITY_API_KEY: opts?.perplexityKey,
15852
+ MUSE_API_KEY: opts?.museKey,
15456
15853
  LOCAL_BASE_URL: opts?.localUrl,
15457
15854
  LOCAL_MODEL: opts?.localModel,
15458
15855
  LOCAL_API_KEY: opts?.localKey,
@@ -15471,7 +15868,7 @@ async function initCommand(opts) {
15471
15868
  }
15472
15869
  const envProviders = bootstrapEnv.providers;
15473
15870
  const envGoogleConfigured = !!(bootstrapEnv.googleClientId && bootstrapEnv.googleClientSecret);
15474
- const nonInteractive = !!(envProviders.gemini || envProviders.openai || envProviders.claude || envProviders.perplexity || envProviders.local || envGoogleConfigured);
15871
+ const nonInteractive = !!(envProviders.gemini || envProviders.openai || envProviders.claude || envProviders.perplexity || envProviders.muse || envProviders.local || envGoogleConfigured);
15475
15872
  const providers = {};
15476
15873
  let google;
15477
15874
  if (isMachineFormat(format) && !nonInteractive) {
@@ -15497,6 +15894,7 @@ async function initCommand(opts) {
15497
15894
  console.log("Configure AI providers (at least one required):\n");
15498
15895
  console.log("Tip: For non-interactive setup, pass provider flags or set");
15499
15896
  console.log("GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, PERPLEXITY_API_KEY,");
15897
+ console.log("MUSE_API_KEY,");
15500
15898
  console.log("GOOGLE_CLIENT_ID, and GOOGLE_CLIENT_SECRET env vars.");
15501
15899
  console.log('Or use "canonry bootstrap".\n');
15502
15900
  const geminiApiKey = await promptProviderApiKey("Gemini", "GEMINI_API_KEY");
@@ -15519,6 +15917,11 @@ async function initCommand(opts) {
15519
15917
  const perplexityModel = await prompt(" Perplexity preset or model [fast]: ") || "fast";
15520
15918
  providers.perplexity = { apiKey: perplexityApiKey, model: perplexityModel, quota: DEFAULT_QUOTA };
15521
15919
  }
15920
+ const museApiKey = await promptProviderApiKey("Muse", "MUSE_API_KEY");
15921
+ if (museApiKey) {
15922
+ const museModel = await prompt(" Muse model [muse-spark-1.3]: ") || "muse-spark-1.3";
15923
+ providers.muse = { apiKey: museApiKey, model: museModel, quota: DEFAULT_QUOTA };
15924
+ }
15522
15925
  console.log("\nLocal LLM (Ollama, LM Studio, llama.cpp, vLLM \u2014 any OpenAI-compatible API):");
15523
15926
  const localBaseUrl = await prompt("Local LLM base URL (press Enter to skip, e.g. http://localhost:11434/v1): ");
15524
15927
  if (localBaseUrl) {
@@ -15547,7 +15950,7 @@ async function initCommand(opts) {
15547
15950
  };
15548
15951
  }
15549
15952
  }
15550
- const hasProvider = providers.gemini || providers.openai || providers.claude || providers.perplexity || providers.local;
15953
+ const hasProvider = providers.gemini || providers.openai || providers.claude || providers.perplexity || providers.muse || providers.local;
15551
15954
  if (!hasProvider) {
15552
15955
  throw new CliError({
15553
15956
  code: "INIT_PROVIDER_REQUIRED",
@@ -16264,7 +16667,7 @@ var SYSTEM_CLI_COMMANDS = [
16264
16667
  },
16265
16668
  {
16266
16669
  path: ["init"],
16267
- usage: "canonry init [--force] [--gemini-key <key>] [--openai-key <key>] [--claude-key <key>] [--perplexity-key <key>] [--local-url <url>] [--local-model <name>] [--local-key <key>] [--google-client-id <id>] [--google-client-secret <key>] [--skip-skills] [--skip-mcp] [--skills-dir <path>] [--format json]",
16670
+ usage: "canonry init [--force] [--gemini-key <key>] [--openai-key <key>] [--claude-key <key>] [--perplexity-key <key>] [--muse-key <key>] [--local-url <url>] [--local-model <name>] [--local-key <key>] [--google-client-id <id>] [--google-client-secret <key>] [--skip-skills] [--skip-mcp] [--skills-dir <path>] [--format json]",
16268
16671
  help: 'Optional interactive provider and OAuth setup. First-run Page Health does not need this: use "canonry bootstrap", then "canonry serve". At least one provider is required here. --format json needs provider flags or environment variables.',
16269
16672
  options: {
16270
16673
  force: { type: "boolean", short: "f", default: false },
@@ -16272,6 +16675,7 @@ var SYSTEM_CLI_COMMANDS = [
16272
16675
  "openai-key": stringOption(),
16273
16676
  "claude-key": stringOption(),
16274
16677
  "perplexity-key": stringOption(),
16678
+ "muse-key": stringOption(),
16275
16679
  "local-url": stringOption(),
16276
16680
  "local-model": stringOption(),
16277
16681
  "local-key": stringOption(),
@@ -16289,6 +16693,7 @@ var SYSTEM_CLI_COMMANDS = [
16289
16693
  openaiKey: getString(input.values, "openai-key"),
16290
16694
  claudeKey: getString(input.values, "claude-key"),
16291
16695
  perplexityKey: getString(input.values, "perplexity-key"),
16696
+ museKey: getString(input.values, "muse-key"),
16292
16697
  localUrl: getString(input.values, "local-url"),
16293
16698
  localModel: getString(input.values, "local-model"),
16294
16699
  localKey: getString(input.values, "local-key"),
@@ -16435,10 +16840,10 @@ async function technicalAeoScore(project, opts) {
16435
16840
  lines.push(`As of ${score.auditedAt}`);
16436
16841
  if (score.factors.length > 0) {
16437
16842
  lines.push("");
16438
- lines.push(`${"Factor".padEnd(32)}${"Wt".padStart(4)}${"Avg".padStart(6)}${"Status".padStart(9)} Pass/Part/Fail`);
16843
+ lines.push(`${"Factor".padEnd(32)}${"Share".padStart(7)}${"Avg".padStart(6)}${"Status".padStart(9)} Pass/Part/Fail`);
16439
16844
  for (const f of score.factors) {
16440
16845
  lines.push(
16441
- `${f.name.slice(0, 31).padEnd(32)}${String(f.weight).padStart(4)}${String(f.avgScore).padStart(6)}${f.status.padStart(9)} ${f.pagesPassing}/${f.pagesPartial}/${f.pagesFailing}`
16846
+ `${f.name.slice(0, 31).padEnd(32)}${formatPercent(f.sharePct, RatioUnits.percent).padStart(7)}${String(f.avgScore).padStart(6)}${f.status.padStart(9)} ${f.pagesPassing}/${f.pagesPartial}/${f.pagesFailing}`
16442
16847
  );
16443
16848
  }
16444
16849
  }
@@ -16726,7 +17131,8 @@ async function technicalAeoPageAudit(project, opts) {
16726
17131
  if (res.factors.length > 0) {
16727
17132
  lines.push("", "Factors:");
16728
17133
  for (const factor of res.factors) {
16729
- lines.push(` ${factor.name}: ${factor.score}/100 (${factor.status})`);
17134
+ const share = typeof factor.sharePct === "number" ? ` \xB7 worth ${formatPercent(factor.sharePct, RatioUnits.percent)} of the page score` : "";
17135
+ lines.push(` ${factor.name}: ${factor.score}/100 (${factor.status})${share}`);
16730
17136
  for (const finding of factor.findings) lines.push(` [${finding.code}] ${finding.message}`);
16731
17137
  for (const recommendation of factor.recommendations) lines.push(` Fix: ${recommendation}`);
16732
17138
  }
@@ -17419,9 +17825,6 @@ async function showVisibilityStats(project, opts) {
17419
17825
  }
17420
17826
  printVisibilityStats(data);
17421
17827
  }
17422
- function pct3(rate) {
17423
- return rate === null ? "\u2014" : formatRatio(rate);
17424
- }
17425
17828
  async function showVisibilityCompare(project, opts) {
17426
17829
  if (!opts.from || !opts.to) throw new Error("visibility-compare requires --from <YYYY-MM> and --to <YYYY-MM>");
17427
17830
  const client = createApiClient();
@@ -17438,8 +17841,7 @@ function periodCell(p) {
17438
17841
  return `unavailable: no competitive frame (${p.numerator} observed)`;
17439
17842
  }
17440
17843
  if (p.point === null || p.ciLow === null || p.ciHigh === null) return "no data";
17441
- const p1 = (v) => formatRatio(v);
17442
- return `${p1(p.point)} [${p1(p.ciLow)}, ${p1(p.ciHigh)}]`;
17844
+ return `${formatPercent(p.point)} [${formatPercent(p.ciLow)}, ${formatPercent(p.ciHigh)}]`;
17443
17845
  }
17444
17846
  function verdictCell(m) {
17445
17847
  switch (m.verdict) {
@@ -17549,9 +17951,9 @@ function printVisibilityStats(data) {
17549
17951
  const rows = data.queries.map((q) => ({
17550
17952
  label: q.query,
17551
17953
  cited: citedCell(q),
17552
- citedPct: pct3(q.citedRate),
17954
+ citedPct: formatPercent(q.citedRate),
17553
17955
  ment: mentionedCell(q),
17554
- mentPct: pct3(q.mentionRate)
17956
+ mentPct: formatPercent(q.mentionRate)
17555
17957
  }));
17556
17958
  const queryWidth = Math.max(7, ...rows.map((r) => r.label.length));
17557
17959
  const citedWidth = Math.max(7, ...rows.map((r) => r.cited.length));
@@ -17581,9 +17983,9 @@ function printVisibilityStats(data) {
17581
17983
  [
17582
17984
  "TOTAL".padEnd(queryWidth),
17583
17985
  citedCell(data.totals).padEnd(citedWidth),
17584
- pct3(data.totals.citedRate).padStart(7),
17986
+ formatPercent(data.totals.citedRate).padStart(7),
17585
17987
  mentionedCell(data.totals).padEnd(mentWidth),
17586
- pct3(data.totals.mentionRate).padStart(7)
17988
+ formatPercent(data.totals.mentionRate).padStart(7)
17587
17989
  ].join(" ")
17588
17990
  );
17589
17991
  if (data.groupBy === "provider" && data.byProvider && data.byProvider.length > 0) {
@@ -17595,9 +17997,9 @@ function printVisibilityStats(data) {
17595
17997
  [
17596
17998
  ` ${p.provider}`.padEnd(provWidth + 2),
17597
17999
  citedCell(p).padEnd(citedWidth),
17598
- pct3(p.citedRate).padStart(7),
18000
+ formatPercent(p.citedRate).padStart(7),
17599
18001
  mentionedCell(p).padEnd(mentWidth),
17600
- pct3(p.mentionRate).padStart(7)
18002
+ formatPercent(p.mentionRate).padStart(7)
17601
18003
  ].join(" ")
17602
18004
  );
17603
18005
  }
@@ -17605,7 +18007,7 @@ function printVisibilityStats(data) {
17605
18007
  const sov = data.shareOfVoice;
17606
18008
  if (sov) {
17607
18009
  console.log("");
17608
- const pctStr = sov.availability !== void 0 ? shareOfVoiceLabel(sov.percent, sov) : sov.percent !== null ? `${sov.percent}%` : sov.competitorCount === 0 ? "\u2014 (no competitors configured)" : sov.snapshotsWithAnswerText === 0 ? "\u2014 (no answer text in scope)" : "\u2014 (no brands mentioned in scope)";
18010
+ const pctStr = sov.availability !== void 0 ? shareOfVoiceLabel(sov.percent, sov) : sov.percent !== null ? formatPercent(sov.percent, "percent") : sov.competitorCount === 0 ? "\u2014 (no competitors configured)" : sov.snapshotsWithAnswerText === 0 ? "\u2014 (no answer text in scope)" : "\u2014 (no brands mentioned in scope)";
17609
18011
  const scope = sov.queryClass === "pooled" ? "pooled queries \xB7 classification unavailable" : `${sov.queryClass} queries`;
17610
18012
  console.log(
17611
18013
  `Share of voice (${scope}${sov.measurementScope === "all-markets" ? " \xB7 all markets" : ""}): ${pctStr} (you ${sov.projectMentions} vs competitors ${sov.competitorMentions} brand mentions across ${sov.snapshotsWithAnswerText} answers)`
@@ -19789,6 +20191,7 @@ var TELEMETRY_ERROR_CODES = /* @__PURE__ */ new Set([
19789
20191
  "RUN_FILL_REFUSED",
19790
20192
  "RUN_FILL_IN_PROGRESS",
19791
20193
  "RUN_FILL_INCOMPLETE",
20194
+ "RUN_FAILED",
19792
20195
  "API_ERROR",
19793
20196
  "CLI_ERROR",
19794
20197
  "CLI_USAGE_ERROR",