@canonry/canonry 5.22.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 +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-CWd9UN6L.js} +1 -1
  10. package/assets/assets/{BacklinksPage-Bqm7bido.js → BacklinksPage-B2gPTzyE.js} +1 -1
  11. package/assets/assets/ChartPrimitives-B5okB5g-.js +1 -0
  12. package/assets/assets/{HistoryPage-IcqwPBmt.js → HistoryPage-xtkG8SZN.js} +1 -1
  13. package/assets/assets/{MeasurementPropertyPage-BLpptT3_.js → MeasurementPropertyPage-DxIMU3_T.js} +1 -1
  14. package/assets/assets/{ProjectPage-nn2Y9H-m.js → ProjectPage-CMAFZq_s.js} +5 -5
  15. package/assets/assets/{RunRow-S_SCHr0o.js → RunRow-DnSo7Nle.js} +1 -1
  16. package/assets/assets/{RunsPage-DFiif626.js → RunsPage-FAEuo4Ra.js} +1 -1
  17. package/assets/assets/{SettingsPage-CitZ6FAS.js → SettingsPage-C4OZ1HB9.js} +1 -1
  18. package/assets/assets/{SiteHealthSection-C2dsyNGY.js → SiteHealthSection-BzMxlz8-.js} +3 -3
  19. package/assets/assets/{TrafficPage-lY8na5Pg.js → TrafficPage-D03S6xF6.js} +1 -1
  20. package/assets/assets/{TrafficSourceDetailPage-BTykoVGT.js → TrafficSourceDetailPage-DBu6oFFK.js} +1 -1
  21. package/assets/assets/{extract-error-message-r0Gpc7mv.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-DDrDxbO1.js → react-sigma_core.esm.min-CxwZsgxR.js} +1 -1
  25. package/assets/assets/{v2-overview-adapter-OsQiglXU.js → v2-overview-adapter-DTCKxzZT.js} +1 -1
  26. package/assets/index.html +2 -2
  27. package/dist/{chunk-P5VXVFQW.js → chunk-7RKCDOWO.js} +6773 -5254
  28. package/dist/{chunk-YTSXQUJ3.js → chunk-DX2JUC5H.js} +3 -3
  29. package/dist/{chunk-XFHXKLNT.js → chunk-EP4STEPI.js} +3542 -1165
  30. package/dist/{chunk-GLLY2GEV.js → chunk-FNGITGDF.js} +172 -7
  31. package/dist/{chunk-S5OSY3PI.js → chunk-KVQHG3WB.js} +6693 -5891
  32. package/dist/{chunk-DDL2XSNM.js → chunk-MPICT6V7.js} +128 -35
  33. package/dist/{chunk-3LCR6WU6.js → chunk-ONTEFAA7.js} +2 -2
  34. package/dist/cli.js +436 -44
  35. package/dist/{demo-server-HULNEORN.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-CXNBKRHV.js → intelligence-service-6LDMVI66.js} +2 -2
  39. package/dist/mcp.js +4 -4
  40. package/package.json +14 -13
  41. package/assets/assets/ChartPrimitives-BQC2VH0O.js +0 -1
  42. package/assets/assets/index-DwlO8KfE.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-XFHXKLNT.js";
38
+ } from "./chunk-EP4STEPI.js";
39
39
  import {
40
40
  autoSyncSkills,
41
41
  formatAutoSyncNotice
42
- } from "./chunk-YTSXQUJ3.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-GLLY2GEV.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-3LCR6WU6.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-DDL2XSNM.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-P5VXVFQW.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-S5OSY3PI.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-HULNEORN.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 = () => {
@@ -5226,6 +5232,45 @@ async function gbpPlaces(project, opts) {
5226
5232
  console.log(` ${p.locationName} [${p.tier}] ${amenities}`);
5227
5233
  }
5228
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
+ }
5229
5274
  function fmtDelta(pct4) {
5230
5275
  if (pct4 === null) return "n/a";
5231
5276
  return `${pct4 >= 0 ? "+" : ""}${pct4}%`;
@@ -5446,6 +5491,25 @@ var GBP_CLI_COMMANDS = [
5446
5491
  await gbpPlaces(project, { location: getString(input.values, "location"), format: input.format });
5447
5492
  }
5448
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
+ },
5449
5513
  {
5450
5514
  path: ["gbp", "summary"],
5451
5515
  usage: "canonry gbp summary <project> [--location <name>] [--format json]",
@@ -7365,9 +7429,47 @@ async function trafficEvents(project, opts) {
7365
7429
  console.log(` ${formatEventLine(event)}`);
7366
7430
  }
7367
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
+ }
7368
7452
 
7369
7453
  // src/cli-commands/traffic.ts
7370
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
+ },
7371
7473
  {
7372
7474
  path: ["traffic", "connect", "cloudflare"],
7373
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]",
@@ -8310,6 +8412,65 @@ async function googleTopPages(project, opts) {
8310
8412
  console.log(' Adding up the page rows would not give it. Run "canonry google sync" to fetch it.');
8311
8413
  }
8312
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
+ }
8313
8474
  async function googlePerformance(project, opts) {
8314
8475
  const client = getClient11();
8315
8476
  const params = {};
@@ -8892,6 +9053,7 @@ async function googleDeindexed(project, format) {
8892
9053
 
8893
9054
  // src/cli-commands/google.ts
8894
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]";
8895
9057
  var GOOGLE_CLI_COMMANDS = [
8896
9058
  {
8897
9059
  path: ["google", "connect"],
@@ -9125,6 +9287,36 @@ Usage: ${GOOGLE_PERFORMANCE_USAGE}`,
9125
9287
  });
9126
9288
  }
9127
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
+ },
9128
9320
  {
9129
9321
  path: ["google", "inspect"],
9130
9322
  usage: "canonry google inspect <project> <url> [--format json]",
@@ -9254,12 +9446,12 @@ Usage: ${GOOGLE_PERFORMANCE_USAGE}`,
9254
9446
  },
9255
9447
  {
9256
9448
  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]",
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]",
9258
9450
  run: async (input) => {
9259
9451
  unknownSubcommand(input.positionals[0], {
9260
9452
  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"]
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"]
9263
9455
  });
9264
9456
  }
9265
9457
  }
@@ -9889,7 +10081,7 @@ var KEYWORD_CLI_COMMANDS = [
9889
10081
  const provider = requireStringOption(input, "provider", {
9890
10082
  command: "keyword.generate",
9891
10083
  usage: "canonry keyword generate <project> --provider <name> [--count <n>] [--save] [--format json]",
9892
- message: "--provider is required (e.g. gemini, openai, claude, perplexity, local)"
10084
+ message: "--provider is required (e.g. gemini, openai, claude, perplexity, muse, local)"
9893
10085
  });
9894
10086
  await generateKeywords(project, provider, {
9895
10087
  count: parseIntegerOption(input, "count", {
@@ -10633,7 +10825,7 @@ var QUERY_CLI_COMMANDS = [
10633
10825
  const provider = requireStringOption(input, "provider", {
10634
10826
  command: "query.generate",
10635
10827
  usage: "canonry query generate <project> --provider <name> [--count <n>] [--save] [--format json]",
10636
- message: "--provider is required (e.g. gemini, openai, claude, perplexity, local)"
10828
+ message: "--provider is required (e.g. gemini, openai, claude, perplexity, muse, local)"
10637
10829
  });
10638
10830
  await generateQueries(project, provider, {
10639
10831
  count: parseIntegerOption(input, "count", {
@@ -11050,7 +11242,9 @@ var EVENT_DESCRIPTIONS = {
11050
11242
  "insight.critical": "A critical-severity insight was generated",
11051
11243
  "insight.high": "A high-severity insight was generated",
11052
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)",
11053
- "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)"
11054
11248
  };
11055
11249
  function listEvents(format, target = "local") {
11056
11250
  if (target === "server") return getClient16().listNotificationEvents().then((events) => printEvents(events, format));
@@ -11956,14 +12150,23 @@ async function createProject(name, opts) {
11956
12150
  country: opts.country,
11957
12151
  language: opts.language,
11958
12152
  providers: opts.providers ?? [],
11959
- providerModels: opts.providerModels ?? {}
12153
+ providerModels: opts.providerModels ?? {},
12154
+ ...opts.providerDispatchModes && Object.keys(opts.providerDispatchModes).length > 0 ? { providerDispatchModes: opts.providerDispatchModes } : {}
11960
12155
  });
12156
+ warnDroppedDispatchModes(opts.providerDispatchModes ?? {}, result);
11961
12157
  if (isMachineFormat(opts.format)) {
11962
12158
  console.log(JSON.stringify(result, null, 2));
11963
12159
  return;
11964
12160
  }
11965
12161
  console.log(`Project created: ${result.name} (${result.id})`);
11966
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
+ }
11967
12170
  async function listProjects(format) {
11968
12171
  const client = getClient22();
11969
12172
  const projects2 = await client.listProjects();
@@ -12022,6 +12225,9 @@ async function showProject(name, format) {
12022
12225
  console.log(` Providers: ${(project.providers ?? []).length > 0 ? project.providers.join(", ") : "all configured"}`);
12023
12226
  const providerModels = Object.entries(project.providerModels ?? {});
12024
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)" : ""}`);
12025
12231
  console.log(` Tags: ${project.tags.length > 0 ? project.tags.join(", ") : "(none)"}`);
12026
12232
  const labelEntries = Object.entries(project.labels);
12027
12233
  console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "(none)"}`);
@@ -12066,6 +12272,9 @@ async function updateProjectSettings(name, opts) {
12066
12272
  );
12067
12273
  }
12068
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];
12069
12278
  const result = await client.putProject(name, {
12070
12279
  displayName: nextDisplayName,
12071
12280
  canonicalDomain: opts.domain ?? project.canonicalDomain,
@@ -12077,10 +12286,15 @@ async function updateProjectSettings(name, opts) {
12077
12286
  labels: project.labels,
12078
12287
  providers: opts.providers ?? project.providers,
12079
12288
  providerModels,
12289
+ ...touchesDispatch ? { providerDispatchModes } : {},
12080
12290
  locations: project.locations,
12081
12291
  defaultLocation: project.defaultLocation,
12082
- 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 } : {}
12083
12296
  });
12297
+ warnDroppedDispatchModes(touchesDispatch ? providerDispatchModes : project.providerDispatchModes ?? {}, result);
12084
12298
  if (isMachineFormat(opts.format)) {
12085
12299
  console.log(JSON.stringify(result, null, 2));
12086
12300
  return;
@@ -12193,7 +12407,7 @@ async function setDefaultLocation(project, label, format) {
12193
12407
  var PROJECT_CLI_COMMANDS = [
12194
12408
  {
12195
12409
  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]",
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]",
12197
12411
  help: "Create a project. Pass --domain for the public site to scan. Provider credentials are not required for Page Health.",
12198
12412
  options: {
12199
12413
  domain: { type: "string", short: "d" },
@@ -12203,13 +12417,14 @@ var PROJECT_CLI_COMMANDS = [
12203
12417
  language: stringOption(),
12204
12418
  "display-name": stringOption(),
12205
12419
  provider: multiStringOption(),
12206
- "provider-model": multiStringOption()
12420
+ "provider-model": multiStringOption(),
12421
+ "dispatch-mode": multiStringOption()
12207
12422
  },
12208
12423
  run: async (input) => {
12209
12424
  const name = requireProject(
12210
12425
  input,
12211
12426
  "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]"
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]"
12213
12428
  );
12214
12429
  await createProject(name, {
12215
12430
  domain: getString(input.values, "domain") ?? name,
@@ -12220,13 +12435,14 @@ var PROJECT_CLI_COMMANDS = [
12220
12435
  displayName: getString(input.values, "display-name") ?? name,
12221
12436
  providers: getStringArray(input.values, "provider") ?? [],
12222
12437
  providerModels: parseProviderModelAssignments(getStringArray(input.values, "provider-model")),
12438
+ providerDispatchModes: parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode")),
12223
12439
  format: input.format
12224
12440
  });
12225
12441
  }
12226
12442
  },
12227
12443
  {
12228
12444
  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]",
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]",
12230
12446
  options: {
12231
12447
  domain: { type: "string", short: "d" },
12232
12448
  "owned-domain": multiStringOption(),
@@ -12241,13 +12457,16 @@ var PROJECT_CLI_COMMANDS = [
12241
12457
  provider: multiStringOption(),
12242
12458
  "all-providers": { type: "boolean" },
12243
12459
  "provider-model": multiStringOption(),
12244
- "clear-provider-model": multiStringOption()
12460
+ "clear-provider-model": multiStringOption(),
12461
+ "dispatch-mode": multiStringOption(),
12462
+ "clear-dispatch-mode": multiStringOption(),
12463
+ "negative-review-max-stars": stringOption()
12245
12464
  },
12246
12465
  run: async (input) => {
12247
12466
  const name = requireProject(
12248
12467
  input,
12249
12468
  "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]"
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]"
12251
12470
  );
12252
12471
  const providers = getStringArray(input.values, "provider");
12253
12472
  const allProviders = getBoolean(input.values, "all-providers");
@@ -12257,6 +12476,11 @@ var PROJECT_CLI_COMMANDS = [
12257
12476
  for (const provider of clearProviderModels) {
12258
12477
  if (provider in providerModels) throw usageError(`Error: --provider-model and --clear-provider-model conflict for ${provider}`);
12259
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
+ }
12260
12484
  await updateProjectSettings(name, {
12261
12485
  displayName: getString(input.values, "display-name"),
12262
12486
  domain: getString(input.values, "domain"),
@@ -12271,6 +12495,9 @@ var PROJECT_CLI_COMMANDS = [
12271
12495
  providers: allProviders ? [] : providers,
12272
12496
  providerModels,
12273
12497
  clearProviderModels,
12498
+ dispatchModes,
12499
+ clearDispatchModes,
12500
+ negativeReviewMaxStars: parseNegativeReviewMaxStars(getString(input.values, "negative-review-max-stars")),
12274
12501
  format: input.format
12275
12502
  });
12276
12503
  }
@@ -12386,6 +12613,18 @@ var PROJECT_CLI_COMMANDS = [
12386
12613
  }
12387
12614
  }
12388
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
+ }
12389
12628
  function parseProviderModelAssignments(assignments) {
12390
12629
  const result = {};
12391
12630
  for (const assignment of assignments ?? []) {
@@ -12398,6 +12637,18 @@ function parseProviderModelAssignments(assignments) {
12398
12637
  }
12399
12638
  return result;
12400
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
+ }
12401
12652
 
12402
12653
  // src/commands/report.ts
12403
12654
  import fs13 from "fs";
@@ -12533,6 +12784,27 @@ function getClient23() {
12533
12784
  return createApiClient();
12534
12785
  }
12535
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
+ }
12536
12808
  async function triggerRun(project, opts) {
12537
12809
  const client = getClient23();
12538
12810
  const body = {};
@@ -12562,19 +12834,22 @@ async function triggerRun(project, opts) {
12562
12834
  if (opts?.probe) {
12563
12835
  body.trigger = "probe";
12564
12836
  }
12837
+ if (opts?.dispatchMode) {
12838
+ body.dispatchMode = opts.dispatchMode;
12839
+ }
12565
12840
  const response = await client.triggerRun(project, body);
12566
12841
  if (Array.isArray(response)) {
12567
12842
  const locationRuns = response;
12568
12843
  if (isMachineFormat(opts?.format)) {
12569
12844
  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
- })
12845
+ const finals = await Promise.all(
12846
+ locationRuns.map(async (r) => !r.id || r.status === "conflict" ? null : pollRun2(client, r.id, false))
12576
12847
  );
12577
- 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
+ }));
12578
12853
  } else {
12579
12854
  console.log(JSON.stringify(locationRuns, null, 2));
12580
12855
  }
@@ -12591,12 +12866,17 @@ async function triggerRun(project, opts) {
12591
12866
  }
12592
12867
  if (opts?.wait) {
12593
12868
  const pending = locationRuns.filter((r) => r.id && r.status !== "conflict" && !TERMINAL_STATUSES.has(r.status));
12869
+ const errors = /* @__PURE__ */ new Map();
12594
12870
  if (pending.length > 0) {
12595
12871
  process.stderr.write(`Waiting for ${pending.length} run(s)`);
12872
+ const batchWaits = /* @__PURE__ */ new Map();
12596
12873
  await Promise.all(
12597
12874
  pending.map(async (r) => {
12598
12875
  const final = await pollRun2(client, r.id);
12599
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);
12600
12880
  })
12601
12881
  );
12602
12882
  process.stderr.write("\n");
@@ -12605,20 +12885,31 @@ async function triggerRun(project, opts) {
12605
12885
  const loc = (r.location ?? "(unknown)").padEnd(15);
12606
12886
  console.log(` ${loc} ${r.status}`);
12607
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
+ }
12608
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) })));
12609
12895
  }
12610
12896
  return;
12611
12897
  }
12612
12898
  const run = response;
12613
12899
  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);
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);
12616
12903
  if (isMachineFormat(opts?.format)) {
12617
12904
  console.log(JSON.stringify(result, null, 2));
12618
12905
  } else {
12619
12906
  process.stderr.write("\n");
12620
12907
  printRunDetail(result);
12908
+ const waiting = batchWaitLine(result);
12909
+ if (waiting) console.log(`
12910
+ ${waiting}`);
12621
12911
  }
12912
+ throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project, error: result.error }]);
12622
12913
  return;
12623
12914
  }
12624
12915
  if (opts?.wait && (TERMINAL_STATUSES.has(run.status) || !run.id)) {
@@ -12628,6 +12919,7 @@ async function triggerRun(project, opts) {
12628
12919
  } else {
12629
12920
  printRunDetail(result);
12630
12921
  }
12922
+ throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project, error: result.error }]);
12631
12923
  return;
12632
12924
  }
12633
12925
  if (isMachineFormat(opts?.format)) {
@@ -12664,6 +12956,9 @@ async function triggerRunAll(opts) {
12664
12956
  if (opts?.noLocation) {
12665
12957
  baseBody.noLocation = true;
12666
12958
  }
12959
+ if (opts?.dispatchMode) {
12960
+ baseBody.dispatchMode = opts.dispatchMode;
12961
+ }
12667
12962
  const results = [];
12668
12963
  for (const p of projects2) {
12669
12964
  const body = { ...baseBody };
@@ -12686,19 +12981,27 @@ async function triggerRunAll(opts) {
12686
12981
  results.push({ project: p.name, runId: "", status: "error", location: null, error: msg });
12687
12982
  }
12688
12983
  }
12984
+ const errors = /* @__PURE__ */ new Map();
12985
+ const batchWaits = /* @__PURE__ */ new Map();
12689
12986
  if (opts?.wait) {
12690
12987
  const pending = results.filter((r) => r.runId && !TERMINAL_STATUSES.has(r.status));
12691
12988
  if (pending.length > 0) {
12692
- 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)`);
12693
12991
  await Promise.all(pending.map(async (r) => {
12694
- const final = await pollRun2(client, r.runId);
12992
+ const final = await pollRun2(client, r.runId, showProgress);
12695
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);
12696
12997
  }));
12697
- process.stderr.write("\n");
12998
+ if (showProgress) process.stderr.write("\n");
12698
12999
  }
12699
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) }));
12700
13002
  if (isMachineFormat(opts?.format)) {
12701
13003
  console.log(JSON.stringify(results, null, 2));
13004
+ if (opts?.wait) throwIfWaitedRunFailed(waited);
12702
13005
  return;
12703
13006
  }
12704
13007
  const showLocationColumn = results.some((r) => r.location !== null);
@@ -12723,6 +13026,12 @@ async function triggerRunAll(opts) {
12723
13026
  console.log(` ${proj} ${id} ${r.status}`);
12724
13027
  }
12725
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);
12726
13035
  }
12727
13036
  async function cancelRun(project, runId, format) {
12728
13037
  const client = getClient23();
@@ -12806,7 +13115,7 @@ async function listRuns(project, opts) {
12806
13115
  }
12807
13116
  }
12808
13117
  var POLL_TIMEOUT_MS3 = 10 * 60 * 1e3;
12809
- async function pollRun2(client, runId) {
13118
+ async function pollRun2(client, runId, showProgress = true) {
12810
13119
  const deadline = Date.now() + POLL_TIMEOUT_MS3;
12811
13120
  for (; ; ) {
12812
13121
  await new Promise((r) => setTimeout(r, 2e3));
@@ -12814,12 +13123,22 @@ async function pollRun2(client, runId) {
12814
13123
  throw new Error(`Timed out waiting for run ${runId} after ${POLL_TIMEOUT_MS3 / 1e3}s`);
12815
13124
  }
12816
13125
  const run = await client.getRun(runId);
12817
- process.stderr.write(".");
12818
- if (TERMINAL_STATUSES.has(run.status)) {
13126
+ if (showProgress) process.stderr.write(".");
13127
+ if (TERMINAL_STATUSES.has(run.status) || outstandingBatches(run).length > 0) {
12819
13128
  return run;
12820
13129
  }
12821
13130
  }
12822
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
+ }
12823
13142
  function printRunDetail(run) {
12824
13143
  console.log(`Run: ${run.id}`);
12825
13144
  console.log(` Status: ${run.status}`);
@@ -12836,6 +13155,13 @@ function printRunDetail(run) {
12836
13155
  }
12837
13156
  }
12838
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);
12839
13165
  if (run.snapshots && run.snapshots.length > 0) {
12840
13166
  console.log(`
12841
13167
  Snapshots: ${run.snapshots.length} (cell = [citation][mention]; C=cited c=not, M=mentioned m=not, \u2013=no data)`);
@@ -12847,6 +13173,46 @@ function printRunDetail(run) {
12847
13173
  }
12848
13174
  }
12849
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
+ }
12850
13216
  var FILL_POLL_INTERVAL_MS = 3e3;
12851
13217
  var FILL_POLL_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
12852
13218
  function missingSummary(completeness) {
@@ -12934,8 +13300,8 @@ async function fillRun(runId, opts = {}) {
12934
13300
  }
12935
13301
 
12936
13302
  // 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]";
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]";
12939
13305
  var RUN_FILL_USAGE = "canonry run fill <run-id> [--provider <name>[,<name>]] [--dry-run] [--wait] [--format json]";
12940
13306
  var RUNS_USAGE = "canonry runs <project> [--limit <n>] [--kind <kind>] [--status <status>] [--format json]";
12941
13307
  var RUN_TRIGGER_OPTIONS = {
@@ -12954,13 +13320,26 @@ var RUN_TRIGGER_OPTIONS = {
12954
13320
  // for inspection but are excluded from dashboard / analytics /
12955
13321
  // intelligence / notifications. Use when you want to verify a
12956
13322
  // provider works without polluting the project's metrics.
12957
- 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()
12958
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
+ }
12959
13337
  async function triggerRunCommand(input, command) {
12960
13338
  const groups = getStringArray(input.values, "group") ?? [];
12961
13339
  const targets = getStringArray(input.values, "target") ?? [];
12962
13340
  const queries2 = getStringArray(input.values, "query") ?? [];
12963
13341
  const usage = command === "run" ? RUN_USAGE2 : RUN_TRIGGER_USAGE;
13342
+ const dispatchMode = parseDispatchMode(getString(input.values, "dispatch-mode"), command, usage);
12964
13343
  if (getBoolean(input.values, "all")) {
12965
13344
  if (input.positionals.length > 0) {
12966
13345
  throw usageError("Error: --all cannot be combined with a project name", {
@@ -12991,6 +13370,7 @@ async function triggerRunCommand(input, command) {
12991
13370
  wait: getBoolean(input.values, "wait"),
12992
13371
  allLocations: getBoolean(input.values, "all-locations"),
12993
13372
  noLocation: getBoolean(input.values, "no-location"),
13373
+ dispatchMode,
12994
13374
  format: input.format
12995
13375
  });
12996
13376
  return;
@@ -13017,6 +13397,7 @@ async function triggerRunCommand(input, command) {
13017
13397
  allLocations: getBoolean(input.values, "all-locations"),
13018
13398
  noLocation: getBoolean(input.values, "no-location"),
13019
13399
  probe: getBoolean(input.values, "probe"),
13400
+ dispatchMode,
13020
13401
  format: input.format
13021
13402
  });
13022
13403
  }
@@ -13474,7 +13855,7 @@ var SETTINGS_CLI_COMMANDS = [
13474
13855
  const name = requirePositional(input, 0, {
13475
13856
  command: "settings.provider",
13476
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]",
13477
- 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)"
13478
13859
  });
13479
13860
  const apiKey = getString(input.values, "api-key");
13480
13861
  const baseUrl = getString(input.values, "base-url");
@@ -15060,6 +15441,7 @@ async function bootstrapCommand(opts) {
15060
15441
  if (providers.openai) mergedProviders.openai = providers.openai;
15061
15442
  if (providers.claude) mergedProviders.claude = providers.claude;
15062
15443
  if (providers.perplexity) mergedProviders.perplexity = providers.perplexity;
15444
+ if (providers.muse) mergedProviders.muse = providers.muse;
15063
15445
  if (providers.local) mergedProviders.local = providers.local;
15064
15446
  if (env.googleClientId && !env.googleClientSecret || !env.googleClientId && env.googleClientSecret) {
15065
15447
  console.warn("Warning: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must both be set to configure Google OAuth. Skipping Google auth config.");
@@ -15453,6 +15835,7 @@ async function initCommand(opts) {
15453
15835
  OPENAI_API_KEY: opts?.openaiKey,
15454
15836
  ANTHROPIC_API_KEY: opts?.claudeKey,
15455
15837
  PERPLEXITY_API_KEY: opts?.perplexityKey,
15838
+ MUSE_API_KEY: opts?.museKey,
15456
15839
  LOCAL_BASE_URL: opts?.localUrl,
15457
15840
  LOCAL_MODEL: opts?.localModel,
15458
15841
  LOCAL_API_KEY: opts?.localKey,
@@ -15471,7 +15854,7 @@ async function initCommand(opts) {
15471
15854
  }
15472
15855
  const envProviders = bootstrapEnv.providers;
15473
15856
  const envGoogleConfigured = !!(bootstrapEnv.googleClientId && bootstrapEnv.googleClientSecret);
15474
- 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);
15475
15858
  const providers = {};
15476
15859
  let google;
15477
15860
  if (isMachineFormat(format) && !nonInteractive) {
@@ -15497,6 +15880,7 @@ async function initCommand(opts) {
15497
15880
  console.log("Configure AI providers (at least one required):\n");
15498
15881
  console.log("Tip: For non-interactive setup, pass provider flags or set");
15499
15882
  console.log("GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, PERPLEXITY_API_KEY,");
15883
+ console.log("MUSE_API_KEY,");
15500
15884
  console.log("GOOGLE_CLIENT_ID, and GOOGLE_CLIENT_SECRET env vars.");
15501
15885
  console.log('Or use "canonry bootstrap".\n');
15502
15886
  const geminiApiKey = await promptProviderApiKey("Gemini", "GEMINI_API_KEY");
@@ -15519,6 +15903,11 @@ async function initCommand(opts) {
15519
15903
  const perplexityModel = await prompt(" Perplexity preset or model [fast]: ") || "fast";
15520
15904
  providers.perplexity = { apiKey: perplexityApiKey, model: perplexityModel, quota: DEFAULT_QUOTA };
15521
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
+ }
15522
15911
  console.log("\nLocal LLM (Ollama, LM Studio, llama.cpp, vLLM \u2014 any OpenAI-compatible API):");
15523
15912
  const localBaseUrl = await prompt("Local LLM base URL (press Enter to skip, e.g. http://localhost:11434/v1): ");
15524
15913
  if (localBaseUrl) {
@@ -15547,7 +15936,7 @@ async function initCommand(opts) {
15547
15936
  };
15548
15937
  }
15549
15938
  }
15550
- 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;
15551
15940
  if (!hasProvider) {
15552
15941
  throw new CliError({
15553
15942
  code: "INIT_PROVIDER_REQUIRED",
@@ -16264,7 +16653,7 @@ var SYSTEM_CLI_COMMANDS = [
16264
16653
  },
16265
16654
  {
16266
16655
  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]",
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]",
16268
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.',
16269
16658
  options: {
16270
16659
  force: { type: "boolean", short: "f", default: false },
@@ -16272,6 +16661,7 @@ var SYSTEM_CLI_COMMANDS = [
16272
16661
  "openai-key": stringOption(),
16273
16662
  "claude-key": stringOption(),
16274
16663
  "perplexity-key": stringOption(),
16664
+ "muse-key": stringOption(),
16275
16665
  "local-url": stringOption(),
16276
16666
  "local-model": stringOption(),
16277
16667
  "local-key": stringOption(),
@@ -16289,6 +16679,7 @@ var SYSTEM_CLI_COMMANDS = [
16289
16679
  openaiKey: getString(input.values, "openai-key"),
16290
16680
  claudeKey: getString(input.values, "claude-key"),
16291
16681
  perplexityKey: getString(input.values, "perplexity-key"),
16682
+ museKey: getString(input.values, "muse-key"),
16292
16683
  localUrl: getString(input.values, "local-url"),
16293
16684
  localModel: getString(input.values, "local-model"),
16294
16685
  localKey: getString(input.values, "local-key"),
@@ -19789,6 +20180,7 @@ var TELEMETRY_ERROR_CODES = /* @__PURE__ */ new Set([
19789
20180
  "RUN_FILL_REFUSED",
19790
20181
  "RUN_FILL_IN_PROGRESS",
19791
20182
  "RUN_FILL_INCOMPLETE",
20183
+ "RUN_FAILED",
19792
20184
  "API_ERROR",
19793
20185
  "CLI_ERROR",
19794
20186
  "CLI_USAGE_ERROR",