@canonry/canonry 5.21.0 → 5.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) 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 +101 -6
  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-CjxWA3lx.js → AuditHistoryPanel-CWd9UN6L.js} +1 -1
  10. package/assets/assets/{BacklinksPage-Bdd164Up.js → BacklinksPage-B2gPTzyE.js} +1 -1
  11. package/assets/assets/ChartPrimitives-B5okB5g-.js +1 -0
  12. package/assets/assets/{HistoryPage-BvJmE-kP.js → HistoryPage-xtkG8SZN.js} +1 -1
  13. package/assets/assets/{MeasurementPropertyPage-HuP6RMZ-.js → MeasurementPropertyPage-DxIMU3_T.js} +1 -1
  14. package/assets/assets/{ProjectPage-DOvXVbyy.js → ProjectPage-CMAFZq_s.js} +5 -5
  15. package/assets/assets/{RunRow-Cnb3JOBw.js → RunRow-DnSo7Nle.js} +1 -1
  16. package/assets/assets/{RunsPage-Dvz1JYIm.js → RunsPage-FAEuo4Ra.js} +1 -1
  17. package/assets/assets/{SettingsPage-DYGvphkg.js → SettingsPage-C4OZ1HB9.js} +1 -1
  18. package/assets/assets/{SiteHealthSection-CUNiKaod.js → SiteHealthSection-BzMxlz8-.js} +3 -3
  19. package/assets/assets/{TrafficPage-Bf4Tq95q.js → TrafficPage-D03S6xF6.js} +1 -1
  20. package/assets/assets/{TrafficSourceDetailPage-CIuLWZgT.js → TrafficSourceDetailPage-DBu6oFFK.js} +1 -1
  21. package/assets/assets/{extract-error-message-DZex1e4H.js → extract-error-message-BuH2CCaJ.js} +1 -1
  22. package/assets/assets/index-Ba-Zs45G.js +86 -0
  23. package/assets/assets/index-DfhmHULl.css +1 -0
  24. package/assets/assets/{react-sigma_core.esm.min-CxvtwEkv.js → react-sigma_core.esm.min-CxwZsgxR.js} +1 -1
  25. package/assets/assets/{v2-overview-adapter-Bh4qIzLp.js → v2-overview-adapter-DTCKxzZT.js} +1 -1
  26. package/assets/index.html +2 -2
  27. package/dist/{chunk-FAVYLZQV.js → chunk-7RKCDOWO.js} +7005 -5086
  28. package/dist/{chunk-MEAKE5W7.js → chunk-DX2JUC5H.js} +3 -3
  29. package/dist/{chunk-NFN5GSEJ.js → chunk-EP4STEPI.js} +3558 -1170
  30. package/dist/{chunk-FRWZWJB3.js → chunk-FNGITGDF.js} +176 -8
  31. package/dist/{chunk-2LO7FLVD.js → chunk-KVQHG3WB.js} +6721 -5889
  32. package/dist/{chunk-WUT3G2GM.js → chunk-MPICT6V7.js} +131 -36
  33. package/dist/{chunk-HDJHQPW3.js → chunk-ONTEFAA7.js} +2 -2
  34. package/dist/cli.js +443 -48
  35. package/dist/{demo-server-7BVVVXPM.js → demo-server-HWWIMGD6.js} +4 -4
  36. package/dist/index.d.ts +9 -1
  37. package/dist/index.js +6 -6
  38. package/dist/{intelligence-service-3UUKRAZ5.js → intelligence-service-6LDMVI66.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/index-CeXW-lzj.js +0 -86
  43. 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-NFN5GSEJ.js";
38
+ } from "./chunk-EP4STEPI.js";
39
39
  import {
40
40
  autoSyncSkills,
41
41
  formatAutoSyncNotice
42
- } from "./chunk-MEAKE5W7.js";
42
+ } from "./chunk-DX2JUC5H.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-FRWZWJB3.js";
59
+ } from "./chunk-FNGITGDF.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-HDJHQPW3.js";
65
+ import "./chunk-ONTEFAA7.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-WUT3G2GM.js";
77
+ } from "./chunk-MPICT6V7.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-FAVYLZQV.js";
92
+ } from "./chunk-7RKCDOWO.js";
93
93
  import {
94
94
  AdsDeliverySnapshotStatuses,
95
95
  AdsHistoricalCampaignRollupStatuses,
@@ -103,6 +103,8 @@ import {
103
103
  CheckStatuses,
104
104
  CitationStates,
105
105
  MeasurementEvidenceShapes,
106
+ OUTSTANDING_PROVIDER_BATCH_STATUSES,
107
+ ProviderBatchStatuses,
106
108
  READ_ONLY_SCOPE,
107
109
  REPORT_PERIOD_OPTIONS,
108
110
  ResearchRunStatuses,
@@ -145,6 +147,7 @@ import {
145
147
  formatRatio,
146
148
  formatRunErrorOneLine,
147
149
  formatWholePercent,
150
+ gbpNegativeReviewMaxStarsSchema,
148
151
  googleAdsMetricsWindowSchema,
149
152
  isVisibilityCompareClassMetric,
150
153
  logQuerySchema,
@@ -166,11 +169,14 @@ import {
166
169
  normalizeProjectAliases,
167
170
  normalizeTelemetryStatus,
168
171
  notificationEventSchema,
172
+ providerDispatchModeSchema,
169
173
  queryClassFilterSchema,
170
174
  queryTrackingCommitRequestSchema,
171
175
  queryTrackingPreviewRequestSchema,
176
+ referralAssessmentQuerySchema,
172
177
  reportUnattributedAnswers,
173
178
  researchBatchCreateSchema,
179
+ resolveNegativeReviewMaxStars,
174
180
  resolveProviderInput,
175
181
  resultsClearRequestSchema,
176
182
  shareOfVoiceLabel,
@@ -179,7 +185,7 @@ import {
179
185
  visibilityCompareSelectionSchema,
180
186
  visibilityReportRequestSchema,
181
187
  winnabilityClassSchema
182
- } from "./chunk-2LO7FLVD.js";
188
+ } from "./chunk-KVQHG3WB.js";
183
189
 
184
190
  // src/cli.ts
185
191
  import { pathToFileURL } from "url";
@@ -444,7 +450,7 @@ function parseDemoTrustedProxies(values) {
444
450
  async function demoCommand(options) {
445
451
  const { host, port } = parseDemoListenOptions(options);
446
452
  const trustProxy = parseDemoTrustedProxies(options.trustProxy);
447
- const { createDemoServer } = await import("./demo-server-7BVVVXPM.js");
453
+ const { createDemoServer } = await import("./demo-server-HWWIMGD6.js");
448
454
  const app = await createDemoServer({ trustProxy });
449
455
  let stopping = false;
450
456
  const stop = () => {
@@ -3051,7 +3057,8 @@ async function doctorCommand(opts) {
3051
3057
  const client = createApiClient();
3052
3058
  const report = await client.runDoctor({
3053
3059
  project: opts.project,
3054
- checkIds: opts.checks
3060
+ checkIds: opts.checks,
3061
+ ...opts.reportMonth ? { reportMonth: opts.reportMonth } : {}
3055
3062
  });
3056
3063
  if (opts.format === "json") {
3057
3064
  console.log(JSON.stringify(report, null, 2));
@@ -3077,8 +3084,8 @@ async function runDoctorAll(opts) {
3077
3084
  const client = createApiClient();
3078
3085
  const projects2 = await client.listProjects();
3079
3086
  const [globalReport, ...projectReports] = await Promise.all([
3080
- client.runDoctor({ checkIds: opts.checks }),
3081
- ...projects2.map((p) => client.runDoctor({ project: p.name, checkIds: opts.checks }))
3087
+ client.runDoctor({ checkIds: opts.checks, ...opts.reportMonth ? { reportMonth: opts.reportMonth } : {} }),
3088
+ ...projects2.map((p) => client.runDoctor({ project: p.name, checkIds: opts.checks, ...opts.reportMonth ? { reportMonth: opts.reportMonth } : {} }))
3082
3089
  ]);
3083
3090
  const byKey = { __global__: globalReport };
3084
3091
  projects2.forEach((p, i) => {
@@ -3174,13 +3181,14 @@ ${header}`);
3174
3181
  }
3175
3182
 
3176
3183
  // src/cli-commands/doctor.ts
3177
- var USAGE = "canonry doctor [--project <name>|--all] [--check <id>...] [--format json|jsonl]";
3184
+ var USAGE = "canonry doctor [--project <name>|--all] [--check <id>...] [--report-month YYYY-MM] [--format json|jsonl]";
3178
3185
  var DOCTOR_CLI_COMMANDS = [
3179
3186
  {
3180
3187
  path: ["doctor"],
3181
3188
  usage: USAGE,
3182
3189
  options: {
3183
3190
  project: stringOption(),
3191
+ "report-month": stringOption(),
3184
3192
  check: multiStringOption(),
3185
3193
  all: { type: "boolean" }
3186
3194
  },
@@ -3188,6 +3196,7 @@ var DOCTOR_CLI_COMMANDS = [
3188
3196
  run: async (input) => {
3189
3197
  await doctorCommand({
3190
3198
  project: getString(input.values, "project"),
3199
+ reportMonth: getString(input.values, "report-month"),
3191
3200
  all: getBoolean(input.values, "all"),
3192
3201
  checks: getStringArray(input.values, "check"),
3193
3202
  format: input.format
@@ -5223,6 +5232,45 @@ async function gbpPlaces(project, opts) {
5223
5232
  console.log(` ${p.locationName} [${p.tier}] ${amenities}`);
5224
5233
  }
5225
5234
  }
5235
+ async function gbpReviews(project, opts) {
5236
+ const client = getClient8();
5237
+ const response = await client.listGbpReviews(project, {
5238
+ locationName: opts.location,
5239
+ ...opts.negative ? { negative: true } : {},
5240
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {}
5241
+ });
5242
+ if (isMachineFormat(opts.format)) {
5243
+ console.log(JSON.stringify(response, null, 2));
5244
+ return;
5245
+ }
5246
+ if (response.locations.length === 0 && response.reviews.length === 0) {
5247
+ console.log('No reviews yet. Run "canonry gbp sync" first.');
5248
+ return;
5249
+ }
5250
+ const names = new Map(response.locations.map((loc) => [loc.locationName, loc.displayName]));
5251
+ for (const loc of response.locations) {
5252
+ const rating = loc.rating !== null ? `${Number(loc.rating.toFixed(2))} from ${loc.reviewCount ?? "?"} reviews (${loc.ratingOrigin === "gbp" ? "Business Profile" : "public listing"})` : "no rating yet";
5253
+ const access = loc.reviewsAccess === "ok" ? "every review" : loc.reviewsAccess === null ? "review access not checked yet" : `Business Profile reviews ${loc.reviewsAccess}${loc.reviewsAccessReason ? ` (${loc.reviewsAccessReason})` : ""}`;
5254
+ console.log(`${loc.displayName} ${loc.locationName}`);
5255
+ console.log(` ${rating}; ${access}`);
5256
+ }
5257
+ const label = opts.negative ? "negative review(s)" : "review(s)";
5258
+ if (response.reviews.length === 0) {
5259
+ console.log(`
5260
+ No ${label} stored.`);
5261
+ return;
5262
+ }
5263
+ const shown = response.reviews.length < response.total ? `, newest ${response.reviews.length} shown` : "";
5264
+ console.log(`
5265
+ ${response.total} ${label}${shown}:`);
5266
+ const manyLocations = response.locations.length > 1;
5267
+ for (const r of response.reviews) {
5268
+ const stars = r.starRating !== null ? `${r.starRating}\u2605` : "--";
5269
+ const where = manyLocations ? ` ${names.get(r.locationName) ?? r.locationName}` : "";
5270
+ const text = r.comment ? `"${r.comment.length > 80 ? `${r.comment.slice(0, 79)}\u2026` : r.comment}"` : "(rating only)";
5271
+ console.log(` ${r.updateTime.slice(0, 10)} ${stars}${where} ${r.reviewerName ?? "Anonymous"} ${text} [${r.origin}, ${r.alertState}]`);
5272
+ }
5273
+ }
5226
5274
  function fmtDelta(pct4) {
5227
5275
  if (pct4 === null) return "n/a";
5228
5276
  return `${pct4 >= 0 ? "+" : ""}${pct4}%`;
@@ -5443,6 +5491,25 @@ var GBP_CLI_COMMANDS = [
5443
5491
  await gbpPlaces(project, { location: getString(input.values, "location"), format: input.format });
5444
5492
  }
5445
5493
  },
5494
+ {
5495
+ path: ["gbp", "reviews"],
5496
+ usage: "canonry gbp reviews <project> [--location <name>] [--negative] [--limit N] [--format json]",
5497
+ options: {
5498
+ location: stringOption(),
5499
+ negative: { type: "boolean" },
5500
+ limit: stringOption()
5501
+ },
5502
+ run: async (input) => {
5503
+ const usage = "canonry gbp reviews <project> [--location <name>] [--negative] [--limit N] [--format json]";
5504
+ const project = requireProject(input, "gbp.reviews", usage);
5505
+ await gbpReviews(project, {
5506
+ location: getString(input.values, "location"),
5507
+ negative: getBoolean(input.values, "negative") ?? false,
5508
+ limit: parseIntegerOption(input, "limit", { message: "--limit must be an integer", usage, command: "gbp.reviews" }),
5509
+ format: input.format
5510
+ });
5511
+ }
5512
+ },
5446
5513
  {
5447
5514
  path: ["gbp", "summary"],
5448
5515
  usage: "canonry gbp summary <project> [--location <name>] [--format json]",
@@ -7362,9 +7429,47 @@ async function trafficEvents(project, opts) {
7362
7429
  console.log(` ${formatEventLine(event)}`);
7363
7430
  }
7364
7431
  }
7432
+ async function trafficReferralAssessment(project, options) {
7433
+ const { format, ...query } = options;
7434
+ const parsed = referralAssessmentQuerySchema.safeParse(query);
7435
+ if (!parsed.success) throw new CliError({ code: "TRAFFIC_INVALID_ASSESSMENT", message: parsed.error.message });
7436
+ const result = await createApiClient().trafficReferralAssessment(project, parsed.data);
7437
+ if (isMachineFormat(format)) {
7438
+ console.log(JSON.stringify(result, null, 2));
7439
+ return;
7440
+ }
7441
+ console.log(`AI referral assessment for "${project}" \xB7 ${result.window.startDate} to ${result.window.endDate} UTC`);
7442
+ console.log(` Raw stored hits: ${result.totals.raw.total}`);
7443
+ console.log(` Countable stored hits: ${result.totals.countable.total}`);
7444
+ console.log(` Suspected burst hits: ${result.totals.suspected.total} (not confirmed automation)`);
7445
+ console.log(` Adjusted estimate: ${result.totals.adjustedEstimate.total} (countable minus candidate bursts)`);
7446
+ console.log(` Rule: ${result.rule.version}; threshold ${result.rule.burstThreshold}; ${result.rule.calibration}`);
7447
+ console.log(` Observed server/GA quotient: ${result.comparison.observedRatio ?? "unavailable"}; comparable coverage: unavailable`);
7448
+ console.log(` Evidence: ${result.evidence.returned} of ${result.evidence.total} candidate groups (source, product, normalized path, UTC hour)`);
7449
+ for (const burst of result.bursts) console.log(` ${burst.tsHour} ${burst.sourceId} ${burst.product} ${burst.landingPathNormalized} ${burst.counts.total}`);
7450
+ for (const caveat of result.caveats) console.log(` ${caveat}`);
7451
+ }
7365
7452
 
7366
7453
  // src/cli-commands/traffic.ts
7367
7454
  var TRAFFIC_CLI_COMMANDS = [
7455
+ {
7456
+ path: ["traffic", "referral-assessment"],
7457
+ 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]",
7458
+ 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.",
7459
+ options: { "start-date": stringOption(), "end-date": stringOption(), source: stringOption(), "burst-threshold": stringOption(), "ratio-threshold": stringOption(), limit: stringOption() },
7460
+ run: async (input) => {
7461
+ const project = requireProject(input, "traffic.referral-assessment", "canonry traffic referral-assessment <project> --start-date YYYY-MM-DD --end-date YYYY-MM-DD");
7462
+ await trafficReferralAssessment(project, {
7463
+ startDate: getString(input.values, "start-date"),
7464
+ endDate: getString(input.values, "end-date"),
7465
+ sourceId: getString(input.values, "source"),
7466
+ burstThreshold: getString(input.values, "burst-threshold") === void 0 ? void 0 : Number(getString(input.values, "burst-threshold")),
7467
+ ratioThreshold: getString(input.values, "ratio-threshold") === void 0 ? void 0 : Number(getString(input.values, "ratio-threshold")),
7468
+ limit: getString(input.values, "limit") === void 0 ? void 0 : Number(getString(input.values, "limit")),
7469
+ format: input.format
7470
+ });
7471
+ }
7472
+ },
7368
7473
  {
7369
7474
  path: ["traffic", "connect", "cloudflare"],
7370
7475
  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]",
@@ -8307,6 +8412,65 @@ async function googleTopPages(project, opts) {
8307
8412
  console.log(' Adding up the page rows would not give it. Run "canonry google sync" to fetch it.');
8308
8413
  }
8309
8414
  }
8415
+ async function googleQueryTotals(project, opts) {
8416
+ const client = getClient11();
8417
+ const params = {};
8418
+ if (opts.startDate) params.startDate = opts.startDate;
8419
+ if (opts.endDate) params.endDate = opts.endDate;
8420
+ if (opts.window) params.window = opts.window;
8421
+ if (opts.limit !== void 0) params.limit = String(opts.limit);
8422
+ if (opts.offset !== void 0) params.offset = String(opts.offset);
8423
+ const data = await client.gscQueryTotals(project, Object.keys(params).length > 0 ? params : void 0);
8424
+ const { rows, totalMatching, truncated, window } = data;
8425
+ if (opts.format === "json") {
8426
+ console.log(JSON.stringify(data, null, 2));
8427
+ return;
8428
+ } else if (opts.format === "jsonl") {
8429
+ emitJsonl(rows.map((row) => ({ project, window, ...row })));
8430
+ return;
8431
+ }
8432
+ if (rows.length === 0) {
8433
+ if (totalMatching > 0) {
8434
+ console.log(
8435
+ `Offset ${opts.offset ?? 0} is past the end of the result set (${totalMatching.toLocaleString()} queries). Lower --offset to page through them.`
8436
+ );
8437
+ return;
8438
+ }
8439
+ console.log('No GSC query data found in this window. Run "canonry google sync" first.');
8440
+ return;
8441
+ }
8442
+ const range = `${window.startDate ?? "earliest"} to ${window.endDate ?? "latest"}`;
8443
+ console.log(`GSC query totals (${rows.length.toLocaleString()} of ${totalMatching.toLocaleString()} queries, ${range}):
8444
+ `);
8445
+ const showSource = rows.some((row) => row.source !== "google");
8446
+ console.log(
8447
+ ` ${"QUERY".padEnd(40)}${"CLICKS".padStart(8)}${"IMPR".padStart(10)}${"CTR".padStart(8)}${"POS".padStart(7)}${"DAYS".padStart(6)}${showSource ? " SOURCE" : ""}`
8448
+ );
8449
+ console.log(` ${"\u2500".repeat(40)}${"\u2500".repeat(8)}${"\u2500".repeat(10)}${"\u2500".repeat(8)}${"\u2500".repeat(7)}${"\u2500".repeat(6)}${showSource ? ` ${"\u2500".repeat(11)}` : ""}`);
8450
+ for (const row of rows.slice(0, 50)) {
8451
+ const query = row.query.length > 38 ? row.query.slice(0, 35) + "..." : row.query;
8452
+ console.log(
8453
+ ` ${query.padEnd(40)}${row.clicks.toLocaleString().padStart(8)}${row.impressions.toLocaleString().padStart(10)}${`${(row.ctr * 100).toFixed(1)}%`.padStart(8)}${row.position.toFixed(1).padStart(7)}${String(row.days).padStart(6)}${showSource ? ` ${row.source}` : ""}`
8454
+ );
8455
+ }
8456
+ if (rows.length > 50) {
8457
+ console.log(`
8458
+ ... and ${rows.length - 50} more rows on this page (use --format json for full output)`);
8459
+ }
8460
+ if (truncated) {
8461
+ const nextOffset = (opts.offset ?? 0) + rows.length;
8462
+ console.log(`
8463
+ This is one page. Use --offset ${nextOffset} (with the same --limit) to read the next.`);
8464
+ }
8465
+ console.log();
8466
+ if (showSource) {
8467
+ console.log(" Rows marked page-summed or mixed use the legacy page table for some days. Their impressions");
8468
+ console.log(" over-count, because one search showing several of your pages was stored once per page.");
8469
+ console.log(` Run "canonry google sync" over the window to replace them with Google's per-query totals.`);
8470
+ }
8471
+ console.log(" These are the queries Google names. Google leaves rare queries out, so the rows do not add up");
8472
+ console.log(' to the property total. Use "canonry google performance-daily" for that.');
8473
+ }
8310
8474
  async function googlePerformance(project, opts) {
8311
8475
  const client = getClient11();
8312
8476
  const params = {};
@@ -8889,6 +9053,7 @@ async function googleDeindexed(project, format) {
8889
9053
 
8890
9054
  // src/cli-commands/google.ts
8891
9055
  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]";
9056
+ 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]";
8892
9057
  var GOOGLE_CLI_COMMANDS = [
8893
9058
  {
8894
9059
  path: ["google", "connect"],
@@ -9122,6 +9287,36 @@ Usage: ${GOOGLE_PERFORMANCE_USAGE}`,
9122
9287
  });
9123
9288
  }
9124
9289
  },
9290
+ {
9291
+ path: ["google", "query-totals"],
9292
+ usage: GOOGLE_QUERY_TOTALS_USAGE,
9293
+ options: {
9294
+ window: stringOption(),
9295
+ start: stringOption(),
9296
+ end: stringOption(),
9297
+ limit: stringOption(),
9298
+ offset: stringOption()
9299
+ },
9300
+ run: async (input) => {
9301
+ const project = requireProject(input, "google.query-totals", GOOGLE_QUERY_TOTALS_USAGE);
9302
+ await googleQueryTotals(project, {
9303
+ window: getString(input.values, "window"),
9304
+ startDate: getString(input.values, "start"),
9305
+ endDate: getString(input.values, "end"),
9306
+ limit: parseIntegerOption(input, "limit", {
9307
+ command: "google.query-totals",
9308
+ usage: GOOGLE_QUERY_TOTALS_USAGE,
9309
+ message: "--limit must be an integer"
9310
+ }),
9311
+ offset: parseIntegerOption(input, "offset", {
9312
+ command: "google.query-totals",
9313
+ usage: GOOGLE_QUERY_TOTALS_USAGE,
9314
+ message: "--offset must be an integer"
9315
+ }),
9316
+ format: input.format
9317
+ });
9318
+ }
9319
+ },
9125
9320
  {
9126
9321
  path: ["google", "inspect"],
9127
9322
  usage: "canonry google inspect <project> <url> [--format json]",
@@ -9251,12 +9446,12 @@ Usage: ${GOOGLE_PERFORMANCE_USAGE}`,
9251
9446
  },
9252
9447
  {
9253
9448
  path: ["google"],
9254
- 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]",
9449
+ 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]",
9255
9450
  run: async (input) => {
9256
9451
  unknownSubcommand(input.positionals[0], {
9257
9452
  command: "google",
9258
- 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]",
9259
- 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"]
9453
+ 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]",
9454
+ 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"]
9260
9455
  });
9261
9456
  }
9262
9457
  }
@@ -9886,7 +10081,7 @@ var KEYWORD_CLI_COMMANDS = [
9886
10081
  const provider = requireStringOption(input, "provider", {
9887
10082
  command: "keyword.generate",
9888
10083
  usage: "canonry keyword generate <project> --provider <name> [--count <n>] [--save] [--format json]",
9889
- message: "--provider is required (e.g. gemini, openai, claude, perplexity, local)"
10084
+ message: "--provider is required (e.g. gemini, openai, claude, perplexity, muse, local)"
9890
10085
  });
9891
10086
  await generateKeywords(project, provider, {
9892
10087
  count: parseIntegerOption(input, "count", {
@@ -10630,7 +10825,7 @@ var QUERY_CLI_COMMANDS = [
10630
10825
  const provider = requireStringOption(input, "provider", {
10631
10826
  command: "query.generate",
10632
10827
  usage: "canonry query generate <project> --provider <name> [--count <n>] [--save] [--format json]",
10633
- message: "--provider is required (e.g. gemini, openai, claude, perplexity, local)"
10828
+ message: "--provider is required (e.g. gemini, openai, claude, perplexity, muse, local)"
10634
10829
  });
10635
10830
  await generateQueries(project, provider, {
10636
10831
  count: parseIntegerOption(input, "count", {
@@ -11047,7 +11242,9 @@ var EVENT_DESCRIPTIONS = {
11047
11242
  "insight.critical": "A critical-severity insight was generated",
11048
11243
  "insight.high": "A high-severity insight was generated",
11049
11244
  "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)",
11050
- "health.recovered": "A degraded health check is passing again, or a website that was paged as down answers again"
11245
+ "health.recovered": "A degraded health check is passing again, or a website that was paged as down answers again",
11246
+ "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)",
11247
+ "review.rating-dropped": "A location's public Google rating fell (sent for locations without Business Profile review access)"
11051
11248
  };
11052
11249
  function listEvents(format, target = "local") {
11053
11250
  if (target === "server") return getClient16().listNotificationEvents().then((events) => printEvents(events, format));
@@ -11953,14 +12150,23 @@ async function createProject(name, opts) {
11953
12150
  country: opts.country,
11954
12151
  language: opts.language,
11955
12152
  providers: opts.providers ?? [],
11956
- providerModels: opts.providerModels ?? {}
12153
+ providerModels: opts.providerModels ?? {},
12154
+ ...opts.providerDispatchModes && Object.keys(opts.providerDispatchModes).length > 0 ? { providerDispatchModes: opts.providerDispatchModes } : {}
11957
12155
  });
12156
+ warnDroppedDispatchModes(opts.providerDispatchModes ?? {}, result);
11958
12157
  if (isMachineFormat(opts.format)) {
11959
12158
  console.log(JSON.stringify(result, null, 2));
11960
12159
  return;
11961
12160
  }
11962
12161
  console.log(`Project created: ${result.name} (${result.id})`);
11963
12162
  }
12163
+ function warnDroppedDispatchModes(sent, result) {
12164
+ const kept = result.providerDispatchModes ?? {};
12165
+ const dropped = Object.keys(sent).filter((provider) => !(provider in kept));
12166
+ if (dropped.length > 0) {
12167
+ console.error(`Warning: Dropped dispatch mode for engine(s) the project does not measure: ${dropped.join(", ")}`);
12168
+ }
12169
+ }
11964
12170
  async function listProjects(format) {
11965
12171
  const client = getClient22();
11966
12172
  const projects2 = await client.listProjects();
@@ -12019,6 +12225,9 @@ async function showProject(name, format) {
12019
12225
  console.log(` Providers: ${(project.providers ?? []).length > 0 ? project.providers.join(", ") : "all configured"}`);
12020
12226
  const providerModels = Object.entries(project.providerModels ?? {});
12021
12227
  console.log(` Model overrides: ${providerModels.length > 0 ? providerModels.map(([provider, model]) => `${provider}=${model}`).join(", ") : "(none; instance settings inherited)"}`);
12228
+ const dispatchModes = Object.entries(project.providerDispatchModes ?? {}).sort(([left], [right]) => left.localeCompare(right));
12229
+ console.log(` Dispatch modes: ${dispatchModes.length > 0 ? `${dispatchModes.map(([provider, mode]) => `${provider}=${mode}`).join(", ")} (scheduled sweeps; others sync)` : "(none; every provider runs sync)"}`);
12230
+ console.log(` Negative reviews: ${resolveNegativeReviewMaxStars(project.negativeReviewMaxStars)} stars or fewer${project.negativeReviewMaxStars == null ? " (default)" : ""}`);
12022
12231
  console.log(` Tags: ${project.tags.length > 0 ? project.tags.join(", ") : "(none)"}`);
12023
12232
  const labelEntries = Object.entries(project.labels);
12024
12233
  console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "(none)"}`);
@@ -12063,6 +12272,9 @@ async function updateProjectSettings(name, opts) {
12063
12272
  );
12064
12273
  }
12065
12274
  }
12275
+ const touchesDispatch = Object.keys(opts.dispatchModes ?? {}).length > 0 || (opts.clearDispatchModes ?? []).length > 0;
12276
+ const providerDispatchModes = { ...project.providerDispatchModes ?? {}, ...opts.dispatchModes ?? {} };
12277
+ for (const provider of opts.clearDispatchModes ?? []) delete providerDispatchModes[provider];
12066
12278
  const result = await client.putProject(name, {
12067
12279
  displayName: nextDisplayName,
12068
12280
  canonicalDomain: opts.domain ?? project.canonicalDomain,
@@ -12074,10 +12286,15 @@ async function updateProjectSettings(name, opts) {
12074
12286
  labels: project.labels,
12075
12287
  providers: opts.providers ?? project.providers,
12076
12288
  providerModels,
12289
+ ...touchesDispatch ? { providerDispatchModes } : {},
12077
12290
  locations: project.locations,
12078
12291
  defaultLocation: project.defaultLocation,
12079
- autoExtractBacklinks: project.autoExtractBacklinks
12292
+ autoExtractBacklinks: project.autoExtractBacklinks,
12293
+ // Sent only when asked for: the server keeps the stored threshold when the
12294
+ // field is absent, so a stale read cannot overwrite a newer setting.
12295
+ ...opts.negativeReviewMaxStars !== void 0 ? { negativeReviewMaxStars: opts.negativeReviewMaxStars } : {}
12080
12296
  });
12297
+ warnDroppedDispatchModes(touchesDispatch ? providerDispatchModes : project.providerDispatchModes ?? {}, result);
12081
12298
  if (isMachineFormat(opts.format)) {
12082
12299
  console.log(JSON.stringify(result, null, 2));
12083
12300
  return;
@@ -12190,7 +12407,7 @@ async function setDefaultLocation(project, label, format) {
12190
12407
  var PROJECT_CLI_COMMANDS = [
12191
12408
  {
12192
12409
  path: ["project", "create"],
12193
- 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]",
12410
+ 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]",
12194
12411
  help: "Create a project. Pass --domain for the public site to scan. Provider credentials are not required for Page Health.",
12195
12412
  options: {
12196
12413
  domain: { type: "string", short: "d" },
@@ -12200,13 +12417,14 @@ var PROJECT_CLI_COMMANDS = [
12200
12417
  language: stringOption(),
12201
12418
  "display-name": stringOption(),
12202
12419
  provider: multiStringOption(),
12203
- "provider-model": multiStringOption()
12420
+ "provider-model": multiStringOption(),
12421
+ "dispatch-mode": multiStringOption()
12204
12422
  },
12205
12423
  run: async (input) => {
12206
12424
  const name = requireProject(
12207
12425
  input,
12208
12426
  "project.create",
12209
- "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]"
12427
+ "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]"
12210
12428
  );
12211
12429
  await createProject(name, {
12212
12430
  domain: getString(input.values, "domain") ?? name,
@@ -12217,13 +12435,14 @@ var PROJECT_CLI_COMMANDS = [
12217
12435
  displayName: getString(input.values, "display-name") ?? name,
12218
12436
  providers: getStringArray(input.values, "provider") ?? [],
12219
12437
  providerModels: parseProviderModelAssignments(getStringArray(input.values, "provider-model")),
12438
+ providerDispatchModes: parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode")),
12220
12439
  format: input.format
12221
12440
  });
12222
12441
  }
12223
12442
  },
12224
12443
  {
12225
12444
  path: ["project", "update"],
12226
- 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]",
12445
+ 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]",
12227
12446
  options: {
12228
12447
  domain: { type: "string", short: "d" },
12229
12448
  "owned-domain": multiStringOption(),
@@ -12238,13 +12457,16 @@ var PROJECT_CLI_COMMANDS = [
12238
12457
  provider: multiStringOption(),
12239
12458
  "all-providers": { type: "boolean" },
12240
12459
  "provider-model": multiStringOption(),
12241
- "clear-provider-model": multiStringOption()
12460
+ "clear-provider-model": multiStringOption(),
12461
+ "dispatch-mode": multiStringOption(),
12462
+ "clear-dispatch-mode": multiStringOption(),
12463
+ "negative-review-max-stars": stringOption()
12242
12464
  },
12243
12465
  run: async (input) => {
12244
12466
  const name = requireProject(
12245
12467
  input,
12246
12468
  "project.update",
12247
- "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]"
12469
+ "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]"
12248
12470
  );
12249
12471
  const providers = getStringArray(input.values, "provider");
12250
12472
  const allProviders = getBoolean(input.values, "all-providers");
@@ -12254,6 +12476,11 @@ var PROJECT_CLI_COMMANDS = [
12254
12476
  for (const provider of clearProviderModels) {
12255
12477
  if (provider in providerModels) throw usageError(`Error: --provider-model and --clear-provider-model conflict for ${provider}`);
12256
12478
  }
12479
+ const dispatchModes = parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode"));
12480
+ const clearDispatchModes = getStringArray(input.values, "clear-dispatch-mode") ?? [];
12481
+ for (const provider of clearDispatchModes) {
12482
+ if (provider in dispatchModes) throw usageError(`Error: --dispatch-mode and --clear-dispatch-mode conflict for ${provider}`);
12483
+ }
12257
12484
  await updateProjectSettings(name, {
12258
12485
  displayName: getString(input.values, "display-name"),
12259
12486
  domain: getString(input.values, "domain"),
@@ -12268,6 +12495,9 @@ var PROJECT_CLI_COMMANDS = [
12268
12495
  providers: allProviders ? [] : providers,
12269
12496
  providerModels,
12270
12497
  clearProviderModels,
12498
+ dispatchModes,
12499
+ clearDispatchModes,
12500
+ negativeReviewMaxStars: parseNegativeReviewMaxStars(getString(input.values, "negative-review-max-stars")),
12271
12501
  format: input.format
12272
12502
  });
12273
12503
  }
@@ -12383,6 +12613,18 @@ var PROJECT_CLI_COMMANDS = [
12383
12613
  }
12384
12614
  }
12385
12615
  ];
12616
+ function parseDispatchModeAssignments(assignments) {
12617
+ const result = {};
12618
+ for (const assignment of assignments ?? []) {
12619
+ const separator = assignment.indexOf("=");
12620
+ const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
12621
+ const mode = providerDispatchModeSchema.safeParse(separator === -1 ? "" : assignment.slice(separator + 1).trim().toLowerCase());
12622
+ if (!provider || !mode.success) throw usageError(`Error: --dispatch-mode must use provider=sync or provider=batch (received ${assignment})`);
12623
+ if (provider in result) throw usageError(`Error: duplicate --dispatch-mode assignment for ${provider}`);
12624
+ result[provider] = mode.data;
12625
+ }
12626
+ return result;
12627
+ }
12386
12628
  function parseProviderModelAssignments(assignments) {
12387
12629
  const result = {};
12388
12630
  for (const assignment of assignments ?? []) {
@@ -12395,6 +12637,18 @@ function parseProviderModelAssignments(assignments) {
12395
12637
  }
12396
12638
  return result;
12397
12639
  }
12640
+ function parseNegativeReviewMaxStars(raw) {
12641
+ if (raw === void 0) return void 0;
12642
+ if (raw === "default") return null;
12643
+ const parsed = gbpNegativeReviewMaxStarsSchema.safeParse(Number(raw));
12644
+ if (!parsed.success) {
12645
+ throw usageError('Error: --negative-review-max-stars must be 1, 2, 3, 4, or "default" (3)', {
12646
+ message: '--negative-review-max-stars must be 1-4 or "default"',
12647
+ details: { command: "project.update", value: raw }
12648
+ });
12649
+ }
12650
+ return parsed.data;
12651
+ }
12398
12652
 
12399
12653
  // src/commands/report.ts
12400
12654
  import fs13 from "fs";
@@ -12530,6 +12784,27 @@ function getClient23() {
12530
12784
  return createApiClient();
12531
12785
  }
12532
12786
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
12787
+ function throwIfWaitedRunFailed(waited) {
12788
+ const failed = waited.filter((r) => r.status === RunStatuses.failed);
12789
+ if (failed.length === 0) return;
12790
+ const reason = (r) => r.error ? formatRunErrorOneLine(r.error) : null;
12791
+ const label = (r) => [r.project, r.location ? `(${r.location})` : null, `run ${r.runId}`].filter(Boolean).join(" ");
12792
+ 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("; ")}`;
12793
+ throw new CliError({
12794
+ code: "RUN_FAILED",
12795
+ message,
12796
+ exitCode: EXIT_SYSTEM_ERROR,
12797
+ details: {
12798
+ waitedRunCount: waited.length,
12799
+ failedRuns: failed.map((r) => ({
12800
+ runId: r.runId,
12801
+ ...r.project ? { project: r.project } : {},
12802
+ ...r.location ? { location: r.location } : {},
12803
+ error: reason(r)
12804
+ }))
12805
+ }
12806
+ });
12807
+ }
12533
12808
  async function triggerRun(project, opts) {
12534
12809
  const client = getClient23();
12535
12810
  const body = {};
@@ -12559,19 +12834,22 @@ async function triggerRun(project, opts) {
12559
12834
  if (opts?.probe) {
12560
12835
  body.trigger = "probe";
12561
12836
  }
12837
+ if (opts?.dispatchMode) {
12838
+ body.dispatchMode = opts.dispatchMode;
12839
+ }
12562
12840
  const response = await client.triggerRun(project, body);
12563
12841
  if (Array.isArray(response)) {
12564
12842
  const locationRuns = response;
12565
12843
  if (isMachineFormat(opts?.format)) {
12566
12844
  if (opts?.wait) {
12567
- const settled = await Promise.all(
12568
- locationRuns.map(async (r) => {
12569
- if (!r.id || r.status === "conflict") return r;
12570
- const final = await pollRun2(client, r.id);
12571
- return { ...r, ...final };
12572
- })
12845
+ const finals = await Promise.all(
12846
+ locationRuns.map(async (r) => !r.id || r.status === "conflict" ? null : pollRun2(client, r.id, false))
12573
12847
  );
12574
- console.log(JSON.stringify(settled, null, 2));
12848
+ console.log(JSON.stringify(locationRuns.map((r, i) => finals[i] ? { ...r, ...finals[i] } : r), null, 2));
12849
+ throwIfWaitedRunFailed(locationRuns.flatMap((r, i) => {
12850
+ const final = finals[i];
12851
+ return final ? [{ runId: final.id, status: final.status, project, location: r.location ?? null, error: final.error }] : [];
12852
+ }));
12575
12853
  } else {
12576
12854
  console.log(JSON.stringify(locationRuns, null, 2));
12577
12855
  }
@@ -12588,12 +12866,17 @@ async function triggerRun(project, opts) {
12588
12866
  }
12589
12867
  if (opts?.wait) {
12590
12868
  const pending = locationRuns.filter((r) => r.id && r.status !== "conflict" && !TERMINAL_STATUSES.has(r.status));
12869
+ const errors = /* @__PURE__ */ new Map();
12591
12870
  if (pending.length > 0) {
12592
12871
  process.stderr.write(`Waiting for ${pending.length} run(s)`);
12872
+ const batchWaits = /* @__PURE__ */ new Map();
12593
12873
  await Promise.all(
12594
12874
  pending.map(async (r) => {
12595
12875
  const final = await pollRun2(client, r.id);
12596
12876
  r.status = final.status;
12877
+ errors.set(r.id, final.error);
12878
+ const waiting = batchWaitLine(final);
12879
+ if (waiting) batchWaits.set(r.id, waiting);
12597
12880
  })
12598
12881
  );
12599
12882
  process.stderr.write("\n");
@@ -12602,20 +12885,31 @@ async function triggerRun(project, opts) {
12602
12885
  const loc = (r.location ?? "(unknown)").padEnd(15);
12603
12886
  console.log(` ${loc} ${r.status}`);
12604
12887
  }
12888
+ if (batchWaits.size > 0) console.log("");
12889
+ for (const r of locationRuns) {
12890
+ const waiting = batchWaits.get(r.id);
12891
+ if (waiting) console.log(` ${r.location ?? "(unknown)"}: ${waiting}`);
12892
+ }
12605
12893
  }
12894
+ 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) })));
12606
12895
  }
12607
12896
  return;
12608
12897
  }
12609
12898
  const run = response;
12610
12899
  if (opts?.wait && run.id && !TERMINAL_STATUSES.has(run.status)) {
12611
- process.stderr.write(`Run ${run.id} started`);
12612
- const result = await pollRun2(client, run.id);
12900
+ const showProgress = !isMachineFormat(opts?.format);
12901
+ if (showProgress) process.stderr.write(`Run ${run.id} started`);
12902
+ const result = await pollRun2(client, run.id, showProgress);
12613
12903
  if (isMachineFormat(opts?.format)) {
12614
12904
  console.log(JSON.stringify(result, null, 2));
12615
12905
  } else {
12616
12906
  process.stderr.write("\n");
12617
12907
  printRunDetail(result);
12908
+ const waiting = batchWaitLine(result);
12909
+ if (waiting) console.log(`
12910
+ ${waiting}`);
12618
12911
  }
12912
+ throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project, error: result.error }]);
12619
12913
  return;
12620
12914
  }
12621
12915
  if (opts?.wait && (TERMINAL_STATUSES.has(run.status) || !run.id)) {
@@ -12625,6 +12919,7 @@ async function triggerRun(project, opts) {
12625
12919
  } else {
12626
12920
  printRunDetail(result);
12627
12921
  }
12922
+ throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project, error: result.error }]);
12628
12923
  return;
12629
12924
  }
12630
12925
  if (isMachineFormat(opts?.format)) {
@@ -12661,6 +12956,9 @@ async function triggerRunAll(opts) {
12661
12956
  if (opts?.noLocation) {
12662
12957
  baseBody.noLocation = true;
12663
12958
  }
12959
+ if (opts?.dispatchMode) {
12960
+ baseBody.dispatchMode = opts.dispatchMode;
12961
+ }
12664
12962
  const results = [];
12665
12963
  for (const p of projects2) {
12666
12964
  const body = { ...baseBody };
@@ -12683,19 +12981,27 @@ async function triggerRunAll(opts) {
12683
12981
  results.push({ project: p.name, runId: "", status: "error", location: null, error: msg });
12684
12982
  }
12685
12983
  }
12984
+ const errors = /* @__PURE__ */ new Map();
12985
+ const batchWaits = /* @__PURE__ */ new Map();
12686
12986
  if (opts?.wait) {
12687
12987
  const pending = results.filter((r) => r.runId && !TERMINAL_STATUSES.has(r.status));
12688
12988
  if (pending.length > 0) {
12689
- process.stderr.write(`Waiting for ${pending.length} run(s)`);
12989
+ const showProgress = !isMachineFormat(opts?.format);
12990
+ if (showProgress) process.stderr.write(`Waiting for ${pending.length} run(s)`);
12690
12991
  await Promise.all(pending.map(async (r) => {
12691
- const final = await pollRun2(client, r.runId);
12992
+ const final = await pollRun2(client, r.runId, showProgress);
12692
12993
  r.status = final.status;
12994
+ errors.set(r.runId, final.error);
12995
+ const waiting = batchWaitLine(final);
12996
+ if (waiting) batchWaits.set(r.runId, waiting);
12693
12997
  }));
12694
- process.stderr.write("\n");
12998
+ if (showProgress) process.stderr.write("\n");
12695
12999
  }
12696
13000
  }
13001
+ 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) }));
12697
13002
  if (isMachineFormat(opts?.format)) {
12698
13003
  console.log(JSON.stringify(results, null, 2));
13004
+ if (opts?.wait) throwIfWaitedRunFailed(waited);
12699
13005
  return;
12700
13006
  }
12701
13007
  const showLocationColumn = results.some((r) => r.location !== null);
@@ -12720,6 +13026,12 @@ async function triggerRunAll(opts) {
12720
13026
  console.log(` ${proj} ${id} ${r.status}`);
12721
13027
  }
12722
13028
  }
13029
+ if (batchWaits.size > 0) console.log("");
13030
+ for (const r of results) {
13031
+ const waiting = batchWaits.get(r.runId);
13032
+ if (waiting) console.log(` ${r.location ? `${r.project} (${r.location})` : r.project}: ${waiting}`);
13033
+ }
13034
+ if (opts?.wait) throwIfWaitedRunFailed(waited);
12723
13035
  }
12724
13036
  async function cancelRun(project, runId, format) {
12725
13037
  const client = getClient23();
@@ -12803,7 +13115,7 @@ async function listRuns(project, opts) {
12803
13115
  }
12804
13116
  }
12805
13117
  var POLL_TIMEOUT_MS3 = 10 * 60 * 1e3;
12806
- async function pollRun2(client, runId) {
13118
+ async function pollRun2(client, runId, showProgress = true) {
12807
13119
  const deadline = Date.now() + POLL_TIMEOUT_MS3;
12808
13120
  for (; ; ) {
12809
13121
  await new Promise((r) => setTimeout(r, 2e3));
@@ -12811,12 +13123,22 @@ async function pollRun2(client, runId) {
12811
13123
  throw new Error(`Timed out waiting for run ${runId} after ${POLL_TIMEOUT_MS3 / 1e3}s`);
12812
13124
  }
12813
13125
  const run = await client.getRun(runId);
12814
- process.stderr.write(".");
12815
- if (TERMINAL_STATUSES.has(run.status)) {
13126
+ if (showProgress) process.stderr.write(".");
13127
+ if (TERMINAL_STATUSES.has(run.status) || outstandingBatches(run).length > 0) {
12816
13128
  return run;
12817
13129
  }
12818
13130
  }
12819
13131
  }
13132
+ function outstandingBatches(run) {
13133
+ if (TERMINAL_STATUSES.has(run.status)) return [];
13134
+ return (run.providerBatches ?? []).filter((batch) => OUTSTANDING_PROVIDER_BATCH_STATUSES.includes(batch.status));
13135
+ }
13136
+ function batchWaitLine(run) {
13137
+ const outstanding = outstandingBatches(run);
13138
+ if (outstanding.length === 0) return null;
13139
+ const batches = outstanding.map((batch) => `${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`);
13140
+ return `Waiting on provider batch(es): ${batches.join("; ")}; check with canonry run show ${run.id}`;
13141
+ }
12820
13142
  function printRunDetail(run) {
12821
13143
  console.log(`Run: ${run.id}`);
12822
13144
  console.log(` Status: ${run.status}`);
@@ -12833,6 +13155,13 @@ function printRunDetail(run) {
12833
13155
  }
12834
13156
  }
12835
13157
  }
13158
+ const batched = Object.keys(run.dispatchModes ?? {}).sort();
13159
+ if (batched.length > 0) console.log(` Dispatch: ${batched.map((provider) => `${provider}=batch`).join(", ")} (other providers sync)`);
13160
+ if (run.providerBatches && run.providerBatches.length > 0) {
13161
+ console.log("\n Provider batches:");
13162
+ for (const batch of run.providerBatches) console.log(` ${providerBatchLine(batch)}`);
13163
+ }
13164
+ if (run.usage && run.usage.length > 0) printUsageTable(run.usage);
12836
13165
  if (run.snapshots && run.snapshots.length > 0) {
12837
13166
  console.log(`
12838
13167
  Snapshots: ${run.snapshots.length} (cell = [citation][mention]; C=cited c=not, M=mentioned m=not, \u2013=no data)`);
@@ -12844,6 +13173,46 @@ function printRunDetail(run) {
12844
13173
  }
12845
13174
  }
12846
13175
  }
13176
+ function providerBatchLine(batch) {
13177
+ const recorded = `${batch.recordedCount} of ${batch.requestCount} answers recorded`;
13178
+ const withError = (text) => batch.error ? `${text} (${batch.error})` : text;
13179
+ switch (batch.status) {
13180
+ case ProviderBatchStatuses.submitting:
13181
+ return `submitting provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests`;
13182
+ // The two outstanding statuses: the run stays `running` until they clear.
13183
+ case ProviderBatchStatuses.submitted:
13184
+ case ProviderBatchStatuses.ended:
13185
+ return `waiting on provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`;
13186
+ case ProviderBatchStatuses.ingested:
13187
+ return withError(`provider batch ${batch.provider}: ingested \u2014 ${recorded}`);
13188
+ case ProviderBatchStatuses.cancelled:
13189
+ return withError(`provider batch ${batch.provider}: cancelled \u2014 ${recorded}; the rest are missing and can be filled`);
13190
+ case ProviderBatchStatuses.failed:
13191
+ return withError(`provider batch ${batch.provider}: rejected by the provider \u2014 its ${batch.requestCount} answers ran sync instead`);
13192
+ case ProviderBatchStatuses.unknown:
13193
+ return withError(`provider batch ${batch.provider}: submit outcome unknown \u2014 never resubmitted; its ${batch.requestCount} answers stay missing`);
13194
+ }
13195
+ }
13196
+ function printUsageTable(rows) {
13197
+ const cost = (row) => row.estimatedCostMicros === null ? "unpriced" : `${formatMicros(row.estimatedCostMicros, "USD", { fractionDigits: 4, showTinyAsLessThan: true })}${row.unpricedAnswers > 0 ? ` (+${row.unpricedAnswers} unpriced)` : ""}`;
13198
+ const table = rows.map((row) => [
13199
+ row.provider,
13200
+ row.pricingTier,
13201
+ row.answers.toLocaleString("en-US"),
13202
+ row.inputTokens.toLocaleString("en-US"),
13203
+ row.cachedInputTokens.toLocaleString("en-US"),
13204
+ row.cacheWriteTokens.toLocaleString("en-US"),
13205
+ row.outputTokens.toLocaleString("en-US"),
13206
+ row.searchCount.toLocaleString("en-US"),
13207
+ cost(row)
13208
+ ]);
13209
+ const header = ["PROVIDER", "TIER", "ANSWERS", "INPUT", "CACHED", "CACHE WRITE", "OUTPUT", "SEARCHES", "EST. COST"];
13210
+ const widths = header.map((title, index) => Math.max(title.length, ...table.map((cells) => cells[index].length)));
13211
+ const line = (cells) => ` ${cells.map((cell2, index) => cell2.padEnd(widths[index])).join(" ").trimEnd()}`;
13212
+ console.log("\n Usage (answers with recorded usage; cost is an estimate):");
13213
+ console.log(line(header));
13214
+ for (const cells of table) console.log(line(cells));
13215
+ }
12847
13216
  var FILL_POLL_INTERVAL_MS = 3e3;
12848
13217
  var FILL_POLL_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
12849
13218
  function missingSummary(completeness) {
@@ -12931,8 +13300,8 @@ async function fillRun(runId, opts = {}) {
12931
13300
  }
12932
13301
 
12933
13302
  // src/cli-commands/run.ts
12934
- 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]";
12935
- 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]";
13303
+ 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]";
13304
+ 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]";
12936
13305
  var RUN_FILL_USAGE = "canonry run fill <run-id> [--provider <name>[,<name>]] [--dry-run] [--wait] [--format json]";
12937
13306
  var RUNS_USAGE = "canonry runs <project> [--limit <n>] [--kind <kind>] [--status <status>] [--format json]";
12938
13307
  var RUN_TRIGGER_OPTIONS = {
@@ -12951,13 +13320,26 @@ var RUN_TRIGGER_OPTIONS = {
12951
13320
  // for inspection but are excluded from dashboard / analytics /
12952
13321
  // intelligence / notifications. Use when you want to verify a
12953
13322
  // provider works without polluting the project's metrics.
12954
- probe: { type: "boolean", default: false }
13323
+ probe: { type: "boolean", default: false },
13324
+ // `batch` sends every provider that can to its asynchronous batch API (a
13325
+ // full plan sweep, enabled in config.yaml); refused when none can.
13326
+ "dispatch-mode": stringOption()
12955
13327
  };
13328
+ function parseDispatchMode(value, command, usage) {
13329
+ if (value === void 0) return void 0;
13330
+ const parsed = providerDispatchModeSchema.safeParse(value.trim().toLowerCase());
13331
+ if (parsed.success) return parsed.data;
13332
+ throw usageError(`Error: --dispatch-mode must be one of: ${providerDispatchModeSchema.options.join(", ")} (received ${value})`, {
13333
+ message: "--dispatch-mode must be sync or batch",
13334
+ details: { command, usage }
13335
+ });
13336
+ }
12956
13337
  async function triggerRunCommand(input, command) {
12957
13338
  const groups = getStringArray(input.values, "group") ?? [];
12958
13339
  const targets = getStringArray(input.values, "target") ?? [];
12959
13340
  const queries2 = getStringArray(input.values, "query") ?? [];
12960
13341
  const usage = command === "run" ? RUN_USAGE2 : RUN_TRIGGER_USAGE;
13342
+ const dispatchMode = parseDispatchMode(getString(input.values, "dispatch-mode"), command, usage);
12961
13343
  if (getBoolean(input.values, "all")) {
12962
13344
  if (input.positionals.length > 0) {
12963
13345
  throw usageError("Error: --all cannot be combined with a project name", {
@@ -12988,6 +13370,7 @@ async function triggerRunCommand(input, command) {
12988
13370
  wait: getBoolean(input.values, "wait"),
12989
13371
  allLocations: getBoolean(input.values, "all-locations"),
12990
13372
  noLocation: getBoolean(input.values, "no-location"),
13373
+ dispatchMode,
12991
13374
  format: input.format
12992
13375
  });
12993
13376
  return;
@@ -13014,6 +13397,7 @@ async function triggerRunCommand(input, command) {
13014
13397
  allLocations: getBoolean(input.values, "all-locations"),
13015
13398
  noLocation: getBoolean(input.values, "no-location"),
13016
13399
  probe: getBoolean(input.values, "probe"),
13400
+ dispatchMode,
13017
13401
  format: input.format
13018
13402
  });
13019
13403
  }
@@ -13471,7 +13855,7 @@ var SETTINGS_CLI_COMMANDS = [
13471
13855
  const name = requirePositional(input, 0, {
13472
13856
  command: "settings.provider",
13473
13857
  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]",
13474
- message: "provider name is required (e.g. gemini, openai, claude, perplexity, local)"
13858
+ message: "provider name is required (e.g. gemini, openai, claude, perplexity, muse, local)"
13475
13859
  });
13476
13860
  const apiKey = getString(input.values, "api-key");
13477
13861
  const baseUrl = getString(input.values, "base-url");
@@ -15057,6 +15441,7 @@ async function bootstrapCommand(opts) {
15057
15441
  if (providers.openai) mergedProviders.openai = providers.openai;
15058
15442
  if (providers.claude) mergedProviders.claude = providers.claude;
15059
15443
  if (providers.perplexity) mergedProviders.perplexity = providers.perplexity;
15444
+ if (providers.muse) mergedProviders.muse = providers.muse;
15060
15445
  if (providers.local) mergedProviders.local = providers.local;
15061
15446
  if (env.googleClientId && !env.googleClientSecret || !env.googleClientId && env.googleClientSecret) {
15062
15447
  console.warn("Warning: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must both be set to configure Google OAuth. Skipping Google auth config.");
@@ -15450,6 +15835,7 @@ async function initCommand(opts) {
15450
15835
  OPENAI_API_KEY: opts?.openaiKey,
15451
15836
  ANTHROPIC_API_KEY: opts?.claudeKey,
15452
15837
  PERPLEXITY_API_KEY: opts?.perplexityKey,
15838
+ MUSE_API_KEY: opts?.museKey,
15453
15839
  LOCAL_BASE_URL: opts?.localUrl,
15454
15840
  LOCAL_MODEL: opts?.localModel,
15455
15841
  LOCAL_API_KEY: opts?.localKey,
@@ -15468,7 +15854,7 @@ async function initCommand(opts) {
15468
15854
  }
15469
15855
  const envProviders = bootstrapEnv.providers;
15470
15856
  const envGoogleConfigured = !!(bootstrapEnv.googleClientId && bootstrapEnv.googleClientSecret);
15471
- const nonInteractive = !!(envProviders.gemini || envProviders.openai || envProviders.claude || envProviders.perplexity || envProviders.local || envGoogleConfigured);
15857
+ const nonInteractive = !!(envProviders.gemini || envProviders.openai || envProviders.claude || envProviders.perplexity || envProviders.muse || envProviders.local || envGoogleConfigured);
15472
15858
  const providers = {};
15473
15859
  let google;
15474
15860
  if (isMachineFormat(format) && !nonInteractive) {
@@ -15494,6 +15880,7 @@ async function initCommand(opts) {
15494
15880
  console.log("Configure AI providers (at least one required):\n");
15495
15881
  console.log("Tip: For non-interactive setup, pass provider flags or set");
15496
15882
  console.log("GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, PERPLEXITY_API_KEY,");
15883
+ console.log("MUSE_API_KEY,");
15497
15884
  console.log("GOOGLE_CLIENT_ID, and GOOGLE_CLIENT_SECRET env vars.");
15498
15885
  console.log('Or use "canonry bootstrap".\n');
15499
15886
  const geminiApiKey = await promptProviderApiKey("Gemini", "GEMINI_API_KEY");
@@ -15516,6 +15903,11 @@ async function initCommand(opts) {
15516
15903
  const perplexityModel = await prompt(" Perplexity preset or model [fast]: ") || "fast";
15517
15904
  providers.perplexity = { apiKey: perplexityApiKey, model: perplexityModel, quota: DEFAULT_QUOTA };
15518
15905
  }
15906
+ const museApiKey = await promptProviderApiKey("Muse", "MUSE_API_KEY");
15907
+ if (museApiKey) {
15908
+ const museModel = await prompt(" Muse model [muse-spark-1.3]: ") || "muse-spark-1.3";
15909
+ providers.muse = { apiKey: museApiKey, model: museModel, quota: DEFAULT_QUOTA };
15910
+ }
15519
15911
  console.log("\nLocal LLM (Ollama, LM Studio, llama.cpp, vLLM \u2014 any OpenAI-compatible API):");
15520
15912
  const localBaseUrl = await prompt("Local LLM base URL (press Enter to skip, e.g. http://localhost:11434/v1): ");
15521
15913
  if (localBaseUrl) {
@@ -15544,7 +15936,7 @@ async function initCommand(opts) {
15544
15936
  };
15545
15937
  }
15546
15938
  }
15547
- const hasProvider = providers.gemini || providers.openai || providers.claude || providers.perplexity || providers.local;
15939
+ const hasProvider = providers.gemini || providers.openai || providers.claude || providers.perplexity || providers.muse || providers.local;
15548
15940
  if (!hasProvider) {
15549
15941
  throw new CliError({
15550
15942
  code: "INIT_PROVIDER_REQUIRED",
@@ -16261,7 +16653,7 @@ var SYSTEM_CLI_COMMANDS = [
16261
16653
  },
16262
16654
  {
16263
16655
  path: ["init"],
16264
- 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]",
16656
+ 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]",
16265
16657
  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.',
16266
16658
  options: {
16267
16659
  force: { type: "boolean", short: "f", default: false },
@@ -16269,6 +16661,7 @@ var SYSTEM_CLI_COMMANDS = [
16269
16661
  "openai-key": stringOption(),
16270
16662
  "claude-key": stringOption(),
16271
16663
  "perplexity-key": stringOption(),
16664
+ "muse-key": stringOption(),
16272
16665
  "local-url": stringOption(),
16273
16666
  "local-model": stringOption(),
16274
16667
  "local-key": stringOption(),
@@ -16286,6 +16679,7 @@ var SYSTEM_CLI_COMMANDS = [
16286
16679
  openaiKey: getString(input.values, "openai-key"),
16287
16680
  claudeKey: getString(input.values, "claude-key"),
16288
16681
  perplexityKey: getString(input.values, "perplexity-key"),
16682
+ museKey: getString(input.values, "muse-key"),
16289
16683
  localUrl: getString(input.values, "local-url"),
16290
16684
  localModel: getString(input.values, "local-model"),
16291
16685
  localKey: getString(input.values, "local-key"),
@@ -19786,6 +20180,7 @@ var TELEMETRY_ERROR_CODES = /* @__PURE__ */ new Set([
19786
20180
  "RUN_FILL_REFUSED",
19787
20181
  "RUN_FILL_IN_PROGRESS",
19788
20182
  "RUN_FILL_INCOMPLETE",
20183
+ "RUN_FAILED",
19789
20184
  "API_ERROR",
19790
20185
  "CLI_ERROR",
19791
20186
  "CLI_USAGE_ERROR",