@canonry/canonry 7.13.0 → 7.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +19 -9
  2. package/assets/assets/AuditHistoryPanel-Cl9tUvSZ.js +1 -0
  3. package/assets/assets/{BacklinksPage-BQxB_eFt.js → BacklinksPage-CTeTJ9dh.js} +1 -1
  4. package/assets/assets/{HistoryPage-DgGUM2tr.js → HistoryPage-DpOoXr2P.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-Cl91vUu5.js → MeasurementPropertyPage-DxXsgM25.js} +1 -1
  6. package/assets/assets/ProjectPage-By9arjLJ.js +8 -0
  7. package/assets/assets/{RunsPage-BtjVuurK.js → RunsPage-CfDi7I1z.js} +1 -1
  8. package/assets/assets/SettingsPage-BHDb1apK.js +1 -0
  9. package/assets/assets/{SiteHealthSection-DMHWsfM4.js → SiteHealthSection-DQZyy4P_.js} +3 -3
  10. package/assets/assets/{TrafficPage-Bf0NkbIR.js → TrafficPage-DX_vN2RL.js} +2 -2
  11. package/assets/assets/{TrafficSourceDetailPage-BtwOdTAO.js → TrafficSourceDetailPage-Cg92zrtZ.js} +1 -1
  12. package/assets/assets/{extract-error-message-Cyc59Xi7.js → extract-error-message-BKxVCbov.js} +1 -1
  13. package/assets/assets/index-DoQJETc6.js +86 -0
  14. package/assets/assets/{index-D4QB1Igv.css → index-aBS1f2Bj.css} +1 -1
  15. package/assets/assets/{react-sigma_core.esm.min-CFB4bYg_.js → react-sigma_core.esm.min-BhqZcSgh.js} +1 -1
  16. package/assets/assets/{v2-overview-adapter-CLF10Pij.js → v2-overview-adapter-DZ5YsE_G.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-GQMSJEMZ.js → chunk-7HJYGDRW.js} +384 -57
  19. package/dist/{chunk-AU7OUDWR.js → chunk-KOBK4I5N.js} +5677 -5412
  20. package/dist/{chunk-BTHEFJCI.js → chunk-LC7GMHOL.js} +6 -6
  21. package/dist/{chunk-GBH7N3VZ.js → chunk-N2MRW2DH.js} +93 -31
  22. package/dist/{chunk-7I64KEGM.js → chunk-SG6T3DEP.js} +2 -2
  23. package/dist/{chunk-SM6ZZAPK.js → chunk-SGQBPTWQ.js} +2 -2
  24. package/dist/{chunk-EPSHIH63.js → chunk-YFCKHEKM.js} +3 -3
  25. package/dist/cli.js +1339 -1240
  26. package/dist/{demo-server-WJUA4EIG.js → demo-server-YWGUGZMI.js} +4 -4
  27. package/dist/index.js +6 -6
  28. package/dist/{intelligence-service-DJRH4Z3D.js → intelligence-service-7RKUUR7O.js} +2 -2
  29. package/dist/mcp.js +4 -4
  30. package/package.json +11 -11
  31. package/assets/assets/AuditHistoryPanel-BYxGn9mS.js +0 -1
  32. package/assets/assets/ProjectPage-B_S900QN.js +0 -8
  33. package/assets/assets/SettingsPage-p0x0ZwGY.js +0 -1
  34. package/assets/assets/index-DR_kNdE4.js +0 -86
package/dist/cli.js CHANGED
@@ -33,11 +33,11 @@ import {
33
33
  trackCliCommandFinished,
34
34
  trackEvent,
35
35
  waitForServerRuntimeStartup
36
- } from "./chunk-GBH7N3VZ.js";
36
+ } from "./chunk-N2MRW2DH.js";
37
37
  import {
38
38
  autoSyncSkills,
39
39
  formatAutoSyncNotice
40
- } from "./chunk-EPSHIH63.js";
40
+ } from "./chunk-YFCKHEKM.js";
41
41
  import {
42
42
  configExists,
43
43
  createApiClient,
@@ -54,13 +54,13 @@ import {
54
54
  saveConfig,
55
55
  saveConfigPatch,
56
56
  shouldPrintServeSkillsNudge
57
- } from "./chunk-SM6ZZAPK.js";
57
+ } from "./chunk-SGQBPTWQ.js";
58
58
  import {
59
59
  DEFAULT_DEMO_TRUSTED_PROXIES,
60
60
  trustedProxiesProblem,
61
61
  trustedProxyProblem
62
62
  } from "./chunk-RB2F4D3Z.js";
63
- import "./chunk-7I64KEGM.js";
63
+ import "./chunk-SG6T3DEP.js";
64
64
  import {
65
65
  CliError,
66
66
  EXIT_SYSTEM_ERROR,
@@ -72,7 +72,7 @@ import {
72
72
  runMeasurementDraftAction,
73
73
  systemError,
74
74
  usageError
75
- } from "./chunk-BTHEFJCI.js";
75
+ } from "./chunk-LC7GMHOL.js";
76
76
  import {
77
77
  CLOUDFLARE_WORKER_BINDINGS,
78
78
  CLOUDFLARE_WORKER_GENERATED_MARKER,
@@ -104,7 +104,7 @@ import {
104
104
  rawEventSamples,
105
105
  readMarketCompetitorNames,
106
106
  runs
107
- } from "./chunk-GQMSJEMZ.js";
107
+ } from "./chunk-7HJYGDRW.js";
108
108
  import {
109
109
  AdsDeliverySnapshotStatuses,
110
110
  AdsHistoricalCampaignRollupStatuses,
@@ -234,7 +234,7 @@ import {
234
234
  visibilityCompareSelectionSchema,
235
235
  visibilityReportRequestSchema,
236
236
  winnabilityClassSchema
237
- } from "./chunk-AU7OUDWR.js";
237
+ } from "./chunk-KOBK4I5N.js";
238
238
 
239
239
  // src/cli.ts
240
240
  import { pathToFileURL } from "url";
@@ -784,7 +784,7 @@ function parseDemoTrustedProxies(values) {
784
784
  async function demoCommand(options) {
785
785
  const { host, port } = parseDemoListenOptions(options);
786
786
  const trustProxy = parseDemoTrustedProxies(options.trustProxy);
787
- const { createDemoServer } = await import("./demo-server-WJUA4EIG.js");
787
+ const { createDemoServer } = await import("./demo-server-YWGUGZMI.js");
788
788
  const app = await createDemoServer({ trustProxy });
789
789
  let stopping = false;
790
790
  const stop = () => {
@@ -1198,7 +1198,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
1198
1198
  }
1199
1199
  }
1200
1200
  async function backfillInsightsCommand(project2, opts) {
1201
- const { IntelligenceService } = await import("./intelligence-service-DJRH4Z3D.js");
1201
+ const { IntelligenceService } = await import("./intelligence-service-7RKUUR7O.js");
1202
1202
  const config = loadConfig();
1203
1203
  const db = createClient(config.database);
1204
1204
  migrate(db);
@@ -5530,6 +5530,48 @@ async function gaTraffic(project2, opts) {
5530
5530
  Last synced: ${result.lastSyncedAt}`);
5531
5531
  }
5532
5532
  }
5533
+ var AI_ENGINE_LEAD_RATE_NOTES = {
5534
+ "no-data": null,
5535
+ "sync-not-ready": "the latest acquisition or lead sync failed, so sessions and lead events may cover different days; rerun the GA sync for lead rates.",
5536
+ "channel-leads-unfiltered": "lead events are channel-level here, so host and path filters narrow sessions but not leads; rerun with --host-scope all and no --path-prefix for lead rates.",
5537
+ "paid-split-needs-landing-page": "lead events are channel-level here, so the paid/organic split cannot read landing-page utm tags, and some AI sessions were paid only by those tags (counted as organic above); tag paid AI links with a paid utm_medium such as cpc for lead rates.",
5538
+ "sessions-behind-leads": "stored lead events run past the last stored session date, so recent leads have no sessions to divide by; rerun the GA sync for lead rates.",
5539
+ "sessions-missing-on-lead-days": "some days in this window have stored lead events but no stored sessions, so those leads have no sessions to divide by; rerun the GA sync with --days covering the window for lead rates."
5540
+ };
5541
+ function hasAiEngineLeadRows(block) {
5542
+ return block.engines.length > 0 || block.unattributed.sources.length > 0;
5543
+ }
5544
+ function printAiEngineLeadClass(heading, block) {
5545
+ const cell2 = (period) => `${period.eventCount} / ${period.sessions} ${formatPercent(period.leadRate)}`;
5546
+ const rows = [
5547
+ ...block.engines.map((engine) => ({ label: engine.label, cells: engine.periods.map(cell2) })),
5548
+ ...block.unattributed.sources.length > 0 ? [{ label: "Other AI Assistant", cells: block.unattributed.periods.map(cell2) }] : [],
5549
+ { label: "All AI", cells: block.periods.map(cell2) }
5550
+ ];
5551
+ const labelWidth = Math.max("ENGINE".length, ...rows.map((row) => row.label.length));
5552
+ const widths = block.periods.map((period, index) => Math.max(
5553
+ period.label.length,
5554
+ ...rows.map((row) => row.cells[index]?.length ?? 0)
5555
+ ));
5556
+ const line = (label, cells) => ` ${label.padEnd(labelWidth)} ${cells.map((value, index) => value.padEnd(widths[index] ?? 0)).join(" ")}`.trimEnd();
5557
+ console.log(` ${heading} (lead events / AI sessions, lead rate)`);
5558
+ console.log(line("ENGINE", block.periods.map((period) => period.label.toUpperCase())));
5559
+ for (const row of rows) console.log(line(row.label, row.cells));
5560
+ }
5561
+ function printAiEngineLeads(aiEngines) {
5562
+ if (!aiEngines) return;
5563
+ if (aiEngines.organic.periods.length === 0) return;
5564
+ if (!hasAiEngineLeadRows(aiEngines.organic) && !hasAiEngineLeadRows(aiEngines.paid)) {
5565
+ console.log(" Leads by AI engine: no AI engine sessions or lead events in this window");
5566
+ return;
5567
+ }
5568
+ printAiEngineLeadClass("Leads by AI engine, organic", aiEngines.organic);
5569
+ if (hasAiEngineLeadRows(aiEngines.paid)) {
5570
+ printAiEngineLeadClass("Leads by AI engine, paid clicks", aiEngines.paid);
5571
+ }
5572
+ const note = aiEngines.leadRateUnavailableReason === null ? null : AI_ENGINE_LEAD_RATE_NOTES[aiEngines.leadRateUnavailableReason];
5573
+ if (note) console.log(` Note: ${note}`);
5574
+ }
5533
5575
  async function gaMeasurementAnalysis(project2, opts) {
5534
5576
  const client = getClient7();
5535
5577
  const params = {};
@@ -5563,6 +5605,7 @@ async function gaMeasurementAnalysis(project2, opts) {
5563
5605
  for (const period of result.leads.periods) {
5564
5606
  console.log(` ${period.label.padEnd(8)} ${period.eventCount} leads ${period.startDate} to ${period.endDate}`);
5565
5607
  }
5608
+ printAiEngineLeads(result.leads.aiEngines);
5566
5609
  console.log(` Search demand: ${result.searchDemand.status}`);
5567
5610
  for (const period of result.searchDemand.periods) {
5568
5611
  console.log(` ${period.label.padEnd(8)} ${period.propertyClicks} clicks / ${period.propertyImpressions} impressions (${period.brandedClicks} branded clicks / ${period.brandedImpressions} branded impressions; ${period.nonBrandedClicks} reported non-brand clicks / ${period.nonBrandedImpressions} reported non-brand impressions; ${period.unreportedClicks} unreported clicks / ${period.unreportedImpressions} unreported impressions)`);
@@ -13444,1432 +13487,1485 @@ async function clearResults(project2, opts) {
13444
13487
  }
13445
13488
  }
13446
13489
 
13447
- // src/commands/status.ts
13490
+ // src/commands/run.ts
13448
13491
  function getClient21() {
13449
13492
  return createApiClient();
13450
13493
  }
13451
- async function showStatus(project2, format) {
13494
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
13495
+ function shellArg(value) {
13496
+ return /^[\w.-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
13497
+ }
13498
+ function providerAccountStreakLine(provider, streak) {
13499
+ return `${provider} (${streak.code}): failed on its account in each of its last ${streak.consecutiveRuns} runs; called again after ${streak.retryAfter}`;
13500
+ }
13501
+ function providersThatKeepFailing(count) {
13502
+ return count === 1 ? "a provider that keeps failing on its account" : `${count} providers that keep failing on their accounts`;
13503
+ }
13504
+ function providerAccountFixLine(project2) {
13505
+ return `Fix the key, access or billing in the provider's console. Saving a new key, model or endpoint (canonry settings provider <name>) retries at once; canonry run ${shellArg(project2)} --force calls every provider now.`;
13506
+ }
13507
+ function admissionDecisionKey(admission) {
13508
+ return JSON.stringify([
13509
+ admission.refused,
13510
+ admission.retryAfter,
13511
+ Object.entries(admission.providers).sort(([left], [right]) => left.localeCompare(right)).map(([provider, streak]) => [provider, streak.code, streak.consecutiveRuns, streak.since, streak.latestRunId, streak.retryAfter])
13512
+ ]);
13513
+ }
13514
+ function admissionScopeLines(admission, label, showAllowed) {
13515
+ const held = Object.entries(admission.providers);
13516
+ if (held.length === 0) return showAllowed ? [`${label}: no providers held back.`] : [];
13517
+ const retry = label === "Manual sweep" ? `it can retry after ${admission.retryAfter}` : `scheduled sweeps skip their slots until ${admission.retryAfter}`;
13518
+ return [
13519
+ admission.refused ? `${label}: refused (PROVIDERS_FAILING). Every provider it would call keeps failing on its account; ${retry}.` : `${label}: skips ${providersThatKeepFailing(held.length)}; the rest run.`,
13520
+ ...held.map(([provider, streak]) => ` ${providerAccountStreakLine(provider, streak)}`)
13521
+ ];
13522
+ }
13523
+ function runAdmissionLines(project2, admission, manualAdmission) {
13524
+ if (!admission) return [];
13525
+ const differs = manualAdmission !== void 0 && admissionDecisionKey(admission) !== admissionDecisionKey(manualAdmission);
13526
+ const lines = differs ? [...admissionScopeLines(admission, "Scheduled sweep", true), ...admissionScopeLines(manualAdmission, "Manual sweep", true)] : admissionScopeLines(admission, "Next sweep", false);
13527
+ return lines.length > 0 ? [...lines, providerAccountFixLine(project2)] : [];
13528
+ }
13529
+ function printSkippedProviders(project2, skipped) {
13530
+ const held = Object.entries(skipped ?? {});
13531
+ if (held.length === 0) return;
13532
+ console.log(`
13533
+ Not calling ${providersThatKeepFailing(held.length)}:`);
13534
+ for (const [provider, streak] of held) console.log(` ${providerAccountStreakLine(provider, streak)}`);
13535
+ console.log(providerAccountFixLine(project2));
13536
+ }
13537
+ function throwIfWaitedRunFailed(waited) {
13538
+ const failed = waited.filter((r) => r.status === RunStatuses.failed);
13539
+ if (failed.length === 0) return;
13540
+ const reason = (r) => r.error ? formatRunErrorOneLine(r.error) : null;
13541
+ const label = (r) => [r.project, r.location ? `(${r.location})` : null, `run ${r.runId}`].filter(Boolean).join(" ");
13542
+ 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("; ")}`;
13543
+ throw new CliError({
13544
+ code: "RUN_FAILED",
13545
+ message,
13546
+ exitCode: EXIT_SYSTEM_ERROR,
13547
+ details: {
13548
+ waitedRunCount: waited.length,
13549
+ failedRuns: failed.map((r) => ({
13550
+ runId: r.runId,
13551
+ ...r.project ? { project: r.project } : {},
13552
+ ...r.location ? { location: r.location } : {},
13553
+ error: reason(r)
13554
+ }))
13555
+ }
13556
+ });
13557
+ }
13558
+ async function triggerRun(project2, opts) {
13452
13559
  const client = getClient21();
13453
- const projectData = await client.getProject(project2);
13454
- const latest = await getLatestRunSummary(client, project2);
13455
- if (isMachineFormat(format)) {
13456
- let runs2 = [];
13457
- try {
13458
- runs2 = await client.listRuns(project2);
13459
- } catch {
13560
+ const body = {};
13561
+ if (opts?.provider) {
13562
+ const providerInputs = opts.provider.split(",").map((s) => s.trim()).filter(Boolean);
13563
+ const resolved = providerInputs.flatMap((p) => resolveProviderInput(p));
13564
+ body.providers = resolved.length > 0 ? resolved : providerInputs;
13565
+ }
13566
+ if (opts?.queries?.length) {
13567
+ body.queries = opts.queries;
13568
+ }
13569
+ if (opts?.groups?.length || opts?.targets?.length) {
13570
+ body.measurementScope = {
13571
+ ...opts.groups?.length ? { groups: opts.groups } : {},
13572
+ ...opts.targets?.length ? { targets: opts.targets } : {}
13573
+ };
13574
+ }
13575
+ if (opts?.location) {
13576
+ body.location = opts.location;
13577
+ }
13578
+ if (opts?.allLocations) {
13579
+ body.allLocations = true;
13580
+ }
13581
+ if (opts?.noLocation) {
13582
+ body.noLocation = true;
13583
+ }
13584
+ if (opts?.probe) {
13585
+ body.trigger = "probe";
13586
+ }
13587
+ if (opts?.dispatchMode) {
13588
+ body.dispatchMode = opts.dispatchMode;
13589
+ }
13590
+ if (opts?.force) {
13591
+ body.force = true;
13592
+ }
13593
+ const response = await client.triggerRun(project2, body);
13594
+ if (Array.isArray(response)) {
13595
+ const locationRuns = response;
13596
+ if (isMachineFormat(opts?.format)) {
13597
+ if (opts?.wait) {
13598
+ const finals = await Promise.all(
13599
+ locationRuns.map(async (r) => !r.id || r.status === "conflict" ? null : pollRun2(client, r.id, false))
13600
+ );
13601
+ console.log(JSON.stringify(locationRuns.map((r, i) => finals[i] ? { ...r, ...finals[i] } : r), null, 2));
13602
+ throwIfWaitedRunFailed(locationRuns.flatMap((r, i) => {
13603
+ const final = finals[i];
13604
+ return final ? [{ runId: final.id, status: final.status, project: project2, location: r.location ?? null, error: final.error }] : [];
13605
+ }));
13606
+ } else {
13607
+ console.log(JSON.stringify(locationRuns, null, 2));
13608
+ }
13609
+ return;
13610
+ }
13611
+ console.log(`Triggered ${locationRuns.length} location sweep(s) \u2014 ${locationRuns.length}\xD7 API calls:
13612
+ `);
13613
+ console.log(" LOCATION RUN ID STATUS");
13614
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
13615
+ for (const r of locationRuns) {
13616
+ const loc = (r.location ?? "(unknown)").padEnd(15);
13617
+ const id = (r.id ?? "(conflict)").padEnd(36);
13618
+ console.log(` ${loc} ${id} ${r.status}`);
13619
+ }
13620
+ printSkippedProviders(project2, locationRuns.find((r) => r.skippedProviders)?.skippedProviders);
13621
+ if (opts?.wait) {
13622
+ const pending = locationRuns.filter((r) => r.id && r.status !== "conflict" && !TERMINAL_STATUSES.has(r.status));
13623
+ const errors = /* @__PURE__ */ new Map();
13624
+ if (pending.length > 0) {
13625
+ process.stderr.write(`Waiting for ${pending.length} run(s)`);
13626
+ const batchWaits = /* @__PURE__ */ new Map();
13627
+ await Promise.all(
13628
+ pending.map(async (r) => {
13629
+ const final = await pollRun2(client, r.id);
13630
+ r.status = final.status;
13631
+ errors.set(r.id, final.error);
13632
+ const waiting = batchWaitLine(final);
13633
+ if (waiting) batchWaits.set(r.id, waiting);
13634
+ })
13635
+ );
13636
+ process.stderr.write("\n");
13637
+ console.log("\nFinal statuses:");
13638
+ for (const r of locationRuns) {
13639
+ const loc = (r.location ?? "(unknown)").padEnd(15);
13640
+ console.log(` ${loc} ${r.status}`);
13641
+ }
13642
+ if (batchWaits.size > 0) console.log("");
13643
+ for (const r of locationRuns) {
13644
+ const waiting = batchWaits.get(r.id);
13645
+ if (waiting) console.log(` ${r.location ?? "(unknown)"}: ${waiting}`);
13646
+ }
13647
+ }
13648
+ throwIfWaitedRunFailed(locationRuns.filter((r) => r.id && r.status !== "conflict").map((r) => ({ runId: r.id, status: r.status, project: project2, location: r.location ?? null, error: errors.get(r.id) })));
13460
13649
  }
13461
- console.log(JSON.stringify({
13462
- project: projectData,
13463
- runs: runs2,
13464
- latestRun: latest.run,
13465
- totalRuns: latest.totalRuns
13466
- }, null, 2));
13467
13650
  return;
13468
13651
  }
13469
- console.log(`Status: ${projectData.displayName ?? projectData.name} (${projectData.name})
13470
- `);
13471
- console.log(` Domain: ${projectData.canonicalDomain}`);
13472
- console.log(` Country: ${projectData.country}`);
13473
- console.log(` Language: ${projectData.language}`);
13474
- if (latest.run) {
13475
- console.log(`
13476
- Latest run:`);
13477
- console.log(` ID: ${latest.run.id}`);
13478
- console.log(` Status: ${latest.run.status}`);
13479
- console.log(` Created: ${latest.run.createdAt}`);
13480
- if (latest.run.finishedAt) {
13481
- console.log(` Finished: ${latest.run.finishedAt}`);
13652
+ const run = response;
13653
+ if (opts?.wait && run.id && !TERMINAL_STATUSES.has(run.status)) {
13654
+ const showProgress = !isMachineFormat(opts?.format);
13655
+ if (showProgress) process.stderr.write(`Run ${run.id} started`);
13656
+ const result = await pollRun2(client, run.id, showProgress);
13657
+ if (isMachineFormat(opts?.format)) {
13658
+ console.log(JSON.stringify(result, null, 2));
13659
+ } else {
13660
+ process.stderr.write("\n");
13661
+ printRunDetail(result);
13662
+ const waiting = batchWaitLine(result);
13663
+ if (waiting) console.log(`
13664
+ ${waiting}`);
13482
13665
  }
13483
- console.log(`
13484
- Total runs: ${latest.totalRuns}`);
13485
- } else {
13486
- const projectArg = /^[\w.-]+$/.test(project2) ? project2 : `'${project2.replaceAll("'", `'\\''`)}'`;
13487
- console.log(`
13488
- No runs yet. Capture Page Health with "canonry technical-aeo run ${projectArg}", or run an AI Visibility sweep after adding a provider.`);
13666
+ throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project: project2, error: result.error }]);
13667
+ return;
13489
13668
  }
13490
- }
13491
- async function getLatestRunSummary(client, project2) {
13492
- try {
13493
- return await client.getLatestRun(project2);
13494
- } catch (err) {
13495
- if (!isEndpointMissing(err)) throw err;
13496
- const runs2 = await client.listRuns(project2);
13497
- if (runs2.length === 0) {
13498
- return { totalRuns: 0, run: null };
13669
+ if (opts?.wait && (TERMINAL_STATUSES.has(run.status) || !run.id)) {
13670
+ const result = run.id ? await client.getRun(run.id) : run;
13671
+ if (isMachineFormat(opts?.format)) {
13672
+ console.log(JSON.stringify(result, null, 2));
13673
+ } else {
13674
+ printRunDetail(result);
13499
13675
  }
13500
- const latestRun = runs2.reduce(
13501
- (current, candidate) => candidate.createdAt > current.createdAt ? candidate : current
13502
- );
13503
- return {
13504
- totalRuns: runs2.length,
13505
- run: latestRun
13506
- };
13676
+ throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project: project2, error: result.error }]);
13677
+ return;
13507
13678
  }
13508
- }
13509
-
13510
- // src/cli-commands/operator.ts
13511
- var RESULTS_EXPORT_USAGE = "canonry results export <project> [--format json|csv] [--since <ISO>] [--until <ISO>] [--include-probes] [--output <path>|-]";
13512
- function parseResultsExportFormat(value) {
13513
- if (value === void 0 || value === "json") return "json";
13514
- if (value === "csv") return "csv";
13515
- throw usageError(`Error: --format must be "json" or "csv"
13516
-
13517
- Usage: ${RESULTS_EXPORT_USAGE}`);
13518
- }
13519
- var RESULTS_CLEAR_USAGE = "canonry results clear <project> [--run <id> ...] [--research-run <id> ...] [--confirm] [--format json|jsonl]";
13520
- function parseHistoryPageOption(input, key, usage) {
13521
- const value = getString(input.values, key);
13522
- if (value === void 0) return void 0;
13523
- if (!/^\d+$/.test(value)) {
13524
- throw usageError(`Error: --${key} must be a ${key === "limit" ? "whole number from 1 to 500" : "nonnegative integer"}
13525
- Usage: ${usage}`);
13679
+ if (isMachineFormat(opts?.format)) {
13680
+ console.log(JSON.stringify(run, null, 2));
13681
+ return;
13526
13682
  }
13527
- const parsed = Number(value);
13528
- const valid = Number.isSafeInteger(parsed) && (key === "limit" ? parsed >= 1 && parsed <= 500 : parsed >= 0);
13529
- if (!valid) {
13530
- throw usageError(`Error: --${key} must be a ${key === "limit" ? "whole number from 1 to 500" : "nonnegative integer"}
13531
- Usage: ${usage}`);
13683
+ console.log(`Run created: ${run.id}`);
13684
+ console.log(` Kind: ${run.kind}`);
13685
+ console.log(` Status: ${run.status}`);
13686
+ if (opts?.provider) {
13687
+ console.log(` Provider: ${opts.provider}`);
13532
13688
  }
13533
- return parsed;
13689
+ printSkippedProviders(project2, run.skippedProviders);
13534
13690
  }
13535
- var OPERATOR_CLI_COMMANDS = [
13536
- {
13537
- path: ["results", "clear"],
13538
- usage: RESULTS_CLEAR_USAGE,
13539
- options: { run: multiStringOption(), "research-run": multiStringOption(), confirm: { type: "boolean", default: false } },
13540
- run: async (input) => {
13541
- await clearResults(requireProject(input, "results.clear", RESULTS_CLEAR_USAGE), {
13542
- runIds: getStringArray(input.values, "run") ?? [],
13543
- researchRunIds: getStringArray(input.values, "research-run") ?? [],
13544
- confirm: getBoolean(input.values, "confirm"),
13545
- format: input.format
13546
- });
13691
+ async function triggerRunAll(opts) {
13692
+ const client = getClient21();
13693
+ const projects2 = await client.listProjects();
13694
+ if (projects2.length === 0) {
13695
+ if (isMachineFormat(opts?.format)) {
13696
+ console.log("[]");
13697
+ } else {
13698
+ console.log("No projects found.");
13547
13699
  }
13548
- },
13549
- {
13550
- path: ["results", "export"],
13551
- usage: RESULTS_EXPORT_USAGE,
13552
- options: {
13553
- since: stringOption(),
13554
- until: stringOption(),
13555
- "include-probes": { type: "boolean", default: false },
13556
- output: { type: "string", short: "o" }
13557
- },
13558
- run: async (input) => {
13559
- const project2 = requireProject(input, "results export", RESULTS_EXPORT_USAGE);
13560
- await exportResults(project2, {
13561
- format: parseResultsExportFormat(getString(input.values, "format")),
13562
- since: getString(input.values, "since"),
13563
- until: getString(input.values, "until"),
13564
- includeProbes: getBoolean(input.values, "include-probes"),
13565
- output: getString(input.values, "output")
13566
- });
13700
+ return;
13701
+ }
13702
+ const baseBody = {};
13703
+ if (opts?.provider) {
13704
+ const providerInputs = opts.provider.split(",").map((s) => s.trim()).filter(Boolean);
13705
+ const resolved = providerInputs.flatMap((p) => resolveProviderInput(p));
13706
+ baseBody.providers = resolved.length > 0 ? resolved : providerInputs;
13707
+ }
13708
+ if (opts?.allLocations) {
13709
+ baseBody.allLocations = true;
13710
+ }
13711
+ if (opts?.noLocation) {
13712
+ baseBody.noLocation = true;
13713
+ }
13714
+ if (opts?.dispatchMode) {
13715
+ baseBody.dispatchMode = opts.dispatchMode;
13716
+ }
13717
+ if (opts?.force) {
13718
+ baseBody.force = true;
13719
+ }
13720
+ const results = [];
13721
+ for (const p of projects2) {
13722
+ const body = { ...baseBody };
13723
+ if (body.allLocations && p.locations.length === 0) {
13724
+ delete body.allLocations;
13567
13725
  }
13568
- },
13569
- {
13570
- path: ["status"],
13571
- usage: "canonry status <project> [--format json]",
13572
- run: async (input) => {
13573
- const project2 = requireProject(input, "status", "canonry status <project> [--format json]");
13574
- await showStatus(project2, input.format);
13726
+ try {
13727
+ const response = await client.triggerRun(p.name, body);
13728
+ const dispatched = Array.isArray(response) ? response : [response];
13729
+ for (const r of dispatched) {
13730
+ results.push({
13731
+ project: p.name,
13732
+ runId: r.id,
13733
+ status: r.status,
13734
+ location: r.location ?? null
13735
+ });
13736
+ }
13737
+ } catch (err) {
13738
+ const msg = describeError(err);
13739
+ results.push({ project: p.name, runId: "", status: "error", location: null, error: msg });
13575
13740
  }
13576
- },
13577
- {
13578
- path: ["evidence"],
13579
- usage: "canonry evidence <project> [--format json]",
13580
- run: async (input) => {
13581
- const project2 = requireProject(input, "evidence", "canonry evidence <project> [--format json]");
13582
- await showEvidence(project2, input.format);
13741
+ }
13742
+ const errors = /* @__PURE__ */ new Map();
13743
+ const batchWaits = /* @__PURE__ */ new Map();
13744
+ if (opts?.wait) {
13745
+ const pending = results.filter((r) => r.runId && !TERMINAL_STATUSES.has(r.status));
13746
+ if (pending.length > 0) {
13747
+ const showProgress = !isMachineFormat(opts?.format);
13748
+ if (showProgress) process.stderr.write(`Waiting for ${pending.length} run(s)`);
13749
+ await Promise.all(pending.map(async (r) => {
13750
+ const final = await pollRun2(client, r.runId, showProgress);
13751
+ r.status = final.status;
13752
+ errors.set(r.runId, final.error);
13753
+ const waiting = batchWaitLine(final);
13754
+ if (waiting) batchWaits.set(r.runId, waiting);
13755
+ }));
13756
+ if (showProgress) process.stderr.write("\n");
13583
13757
  }
13584
- },
13585
- {
13586
- path: ["history"],
13587
- usage: "canonry history <project> [--limit <1..500>] [--offset <n>] [--since <ISO>] [--action <action>] [--actor <actor>] [--entity-type <type>] [--format json|jsonl]\n canonry history --all [same filters]",
13588
- options: {
13589
- all: { type: "boolean", default: false },
13590
- limit: stringOption(),
13591
- offset: stringOption(),
13592
- since: stringOption(),
13593
- action: stringOption(),
13594
- actor: stringOption(),
13595
- "entity-type": stringOption()
13596
- },
13597
- run: async (input) => {
13598
- const usage = "canonry history <project> [--limit <1..500>] [--offset <n>] [--since <ISO>] [--action <action>] [--actor <actor>] [--entity-type <type>] [--format json|jsonl]\n canonry history --all [same filters]";
13599
- const project2 = getBoolean(input.values, "all") ? void 0 : requireProject(input, "history", usage);
13600
- await showHistory(project2, input.format, {
13601
- limit: parseHistoryPageOption(input, "limit", usage),
13602
- offset: parseHistoryPageOption(input, "offset", usage),
13603
- since: getString(input.values, "since"),
13604
- action: getString(input.values, "action"),
13605
- actor: getString(input.values, "actor"),
13606
- entityType: getString(input.values, "entity-type")
13607
- });
13758
+ }
13759
+ 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) }));
13760
+ if (isMachineFormat(opts?.format)) {
13761
+ console.log(JSON.stringify(results, null, 2));
13762
+ if (opts?.wait) throwIfWaitedRunFailed(waited);
13763
+ return;
13764
+ }
13765
+ const showLocationColumn = results.some((r) => r.location !== null);
13766
+ const projectCount = new Set(results.map((r) => r.project)).size;
13767
+ console.log(`Triggered ${results.length} run(s) across ${projectCount} project(s):
13768
+ `);
13769
+ if (showLocationColumn) {
13770
+ console.log(" PROJECT LOCATION RUN ID STATUS");
13771
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
13772
+ for (const r of results) {
13773
+ const proj = r.project.padEnd(31);
13774
+ const loc = (r.location ?? "\u2014").padEnd(15);
13775
+ const id = (r.runId || "(failed)").padEnd(36);
13776
+ console.log(` ${proj} ${loc} ${id} ${r.status}`);
13608
13777
  }
13609
- },
13610
- {
13611
- path: ["export"],
13612
- usage: "canonry export <project> [--include-results] [--format json]",
13613
- options: {
13614
- "include-results": { type: "boolean", default: false }
13615
- },
13616
- run: async (input) => {
13617
- const project2 = requireProject(input, "export", "canonry export <project> [--include-results] [--format json]");
13618
- await exportProject(project2, {
13619
- includeResults: getBoolean(input.values, "include-results"),
13620
- format: input.format
13621
- });
13778
+ } else {
13779
+ console.log(" PROJECT RUN ID STATUS");
13780
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
13781
+ for (const r of results) {
13782
+ const proj = r.project.padEnd(31);
13783
+ const id = (r.runId || "(failed)").padEnd(36);
13784
+ console.log(` ${proj} ${id} ${r.status}`);
13622
13785
  }
13623
- },
13624
- {
13625
- path: ["analytics"],
13626
- usage: "canonry analytics <project> [--feature metrics|gaps|sources] [--window 7d|30d|90d|all] [--format json]",
13627
- options: {
13628
- feature: stringOption(),
13629
- window: stringOption()
13630
- },
13631
- run: async (input) => {
13632
- const project2 = requireProject(input, "analytics", "canonry analytics <project> [--feature metrics|gaps|sources] [--window 7d|30d|90d|all] [--format json]");
13633
- await showAnalytics(project2, {
13634
- feature: getString(input.values, "feature"),
13635
- window: getString(input.values, "window"),
13636
- format: input.format
13786
+ }
13787
+ if (batchWaits.size > 0) console.log("");
13788
+ for (const r of results) {
13789
+ const waiting = batchWaits.get(r.runId);
13790
+ if (waiting) console.log(` ${r.location ? `${r.project} (${r.location})` : r.project}: ${waiting}`);
13791
+ }
13792
+ if (opts?.wait) throwIfWaitedRunFailed(waited);
13793
+ }
13794
+ async function cancelRun(project2, runId, format) {
13795
+ const client = getClient21();
13796
+ let targetId = runId;
13797
+ if (!targetId) {
13798
+ const runs2 = await client.listRuns(project2);
13799
+ const activeRuns = runs2.filter((r) => r.status === "queued" || r.status === "running");
13800
+ if (activeRuns.length > 1) {
13801
+ const candidates = activeRuns.map(({ id, kind, status }) => ({ id, kind, status }));
13802
+ throw new CliError({
13803
+ code: "MULTIPLE_ACTIVE_RUNS",
13804
+ message: `Multiple active runs found for project "${project2}". Specify a run ID.`,
13805
+ displayMessage: `Error: Multiple active runs found for project "${project2}". Specify a run ID.
13806
+ ` + candidates.map((r) => ` ${r.id} ${r.kind} ${r.status}`).join("\n") + `
13807
+ To cancel by ID: canonry run cancel ${project2} <run-id>`,
13808
+ details: {
13809
+ project: project2,
13810
+ activeRuns: candidates,
13811
+ suggestedCommands: [`canonry run cancel ${project2} <run-id>`]
13812
+ }
13637
13813
  });
13638
13814
  }
13639
- },
13640
- {
13641
- path: ["sources"],
13642
- usage: SOURCES_USAGE,
13643
- options: {
13644
- rank: { type: "boolean", default: false },
13645
- "by-provider": { type: "boolean", default: false },
13646
- limit: stringOption(),
13647
- window: stringOption(),
13648
- "query-class": stringOption(),
13649
- "run-id": stringOption(),
13650
- "include-by-query": stringOption()
13651
- },
13652
- run: async (input) => {
13653
- const project2 = requireProject(input, "sources", SOURCES_USAGE);
13654
- await showSources(project2, {
13655
- rank: getBoolean(input.values, "rank"),
13656
- byProvider: getBoolean(input.values, "by-provider"),
13657
- limit: parseIntegerOption(input, "limit", {
13658
- message: "--limit must be an integer",
13659
- usage: SOURCES_USAGE,
13660
- command: "sources"
13661
- }),
13662
- window: getString(input.values, "window"),
13663
- runId: getString(input.values, "run-id"),
13664
- queryClass: parseSourcesQueryClass(getString(input.values, "query-class")),
13665
- includeByQuery: parseSourcesIncludeByQuery(getString(input.values, "include-by-query")),
13666
- format: input.format
13815
+ const active = activeRuns[0];
13816
+ if (!active) {
13817
+ throw new CliError({
13818
+ code: "NO_ACTIVE_RUN",
13819
+ message: `No active run found for project "${project2}"`,
13820
+ displayMessage: `Error: canonry run cancel "${project2}" \u2014 no active run found (status must be queued or running).
13821
+ Check run status : canonry status ${project2}
13822
+ To cancel by ID : canonry run cancel ${project2} <run-id>`,
13823
+ details: {
13824
+ project: project2,
13825
+ allowedStatuses: ["queued", "running"],
13826
+ suggestedCommands: [
13827
+ `canonry status ${project2}`,
13828
+ `canonry run cancel ${project2} <run-id>`
13829
+ ]
13830
+ }
13667
13831
  });
13668
13832
  }
13669
- },
13670
- {
13671
- path: ["apply"],
13672
- usage: "canonry apply <file...> [--format json]",
13673
- run: async (input) => {
13674
- if (input.positionals.length === 0) {
13675
- throw usageError("Error: at least one file path is required\nUsage: canonry apply <file...> [--format json]", {
13676
- message: "at least one file path is required",
13677
- details: {
13678
- command: "apply",
13679
- usage: "canonry apply <file...> [--format json]"
13680
- }
13681
- });
13682
- }
13683
- await applyConfigs(input.positionals, input.format);
13684
- }
13833
+ targetId = active.id;
13685
13834
  }
13686
- ];
13687
-
13688
- // src/commands/project.ts
13689
- function getClient22() {
13690
- return createApiClient();
13691
- }
13692
- async function createProject(name, opts) {
13693
- const client = getClient22();
13694
- const result = await client.putProject(name, {
13695
- displayName: opts.displayName,
13696
- canonicalDomain: opts.domain,
13697
- ownedDomains: opts.ownedDomains ?? [],
13698
- aliases: normalizeProjectAliases(opts.displayName, opts.aliases ?? []),
13699
- ...opts.qualifiedAliases?.length ? { qualifiedAliases: opts.qualifiedAliases } : {},
13700
- country: opts.country,
13701
- language: opts.language,
13702
- providers: opts.providers ?? [],
13703
- providerModels: opts.providerModels ?? {},
13704
- ...opts.providerDispatchModes && Object.keys(opts.providerDispatchModes).length > 0 ? { providerDispatchModes: opts.providerDispatchModes } : {},
13705
- // Sent only when given: this PUT also re-runs over an existing project,
13706
- // and an omitted budget keeps the stored one.
13707
- ...opts.siteAuditMaxPages !== void 0 ? { siteAuditMaxPages: opts.siteAuditMaxPages } : {}
13708
- });
13709
- warnDroppedDispatchModes(opts.providerDispatchModes ?? {}, result);
13710
- if (isMachineFormat(opts.format)) {
13835
+ const result = await client.cancelRun(targetId);
13836
+ if (isMachineFormat(format)) {
13711
13837
  console.log(JSON.stringify(result, null, 2));
13712
13838
  return;
13713
13839
  }
13714
- console.log(`Project created: ${result.name} (${result.id})`);
13840
+ console.log(`Run ${result.id} cancelled.`);
13715
13841
  }
13716
- function warnDroppedDispatchModes(sent, result) {
13717
- const kept = result.providerDispatchModes ?? {};
13718
- const dropped = Object.keys(sent).filter((provider) => !(provider in kept));
13719
- if (dropped.length > 0) {
13720
- console.error(`Warning: Dropped dispatch mode for engine(s) the project does not measure: ${dropped.join(", ")}`);
13842
+ async function showRun(id, format) {
13843
+ const client = getClient21();
13844
+ const run = await client.getRun(id);
13845
+ if (isMachineFormat(format)) {
13846
+ console.log(JSON.stringify(run, null, 2));
13847
+ return;
13721
13848
  }
13849
+ printRunDetail(run);
13722
13850
  }
13723
- async function listProjects(format) {
13724
- const client = getClient22();
13725
- const projects2 = await client.listProjects();
13726
- if (format === "json") {
13727
- console.log(JSON.stringify(projects2, null, 2));
13851
+ async function listRuns(project2, opts) {
13852
+ const client = getClient21();
13853
+ const runs2 = await client.listRuns(project2, opts?.limit, opts?.kind, opts?.status, opts?.excludeKind, opts?.excludeKinds);
13854
+ if (opts?.format === "json") {
13855
+ console.log(JSON.stringify(runs2, null, 2));
13728
13856
  return;
13729
- }
13730
- if (format === "jsonl") {
13731
- emitJsonl(projects2);
13857
+ } else if (opts?.format === "jsonl") {
13858
+ emitJsonl(runs2.map((run) => ({ project: project2, ...run })));
13732
13859
  return;
13733
13860
  }
13734
- if (projects2.length === 0) {
13735
- console.log("No projects found.");
13861
+ if (runs2.length === 0) {
13862
+ console.log(`No runs found for "${project2}".`);
13736
13863
  return;
13737
13864
  }
13738
- console.log("Projects:\n");
13739
- const nameWidth = Math.max(4, ...projects2.map((p) => p.name.length));
13740
- const domainLabel = (p) => {
13741
- const extra = Math.max(0, effectiveDomains(p).length - 1);
13742
- return extra > 0 ? `${p.canonicalDomain} (+${extra})` : p.canonicalDomain;
13743
- };
13744
- const domainWidth = Math.max(6, ...projects2.map((p) => domainLabel(p).length));
13745
- console.log(
13746
- ` ${"NAME".padEnd(nameWidth)} ${"DOMAIN".padEnd(domainWidth)} COUNTRY LANGUAGE`
13747
- );
13748
- console.log(` ${"\u2500".repeat(nameWidth)} ${"\u2500".repeat(domainWidth)} \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
13749
- for (const p of projects2) {
13865
+ console.log(`Runs for "${project2}" (${runs2.length}):
13866
+ `);
13867
+ console.log(" ID STATUS KIND TRIGGER CREATED");
13868
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
13869
+ for (const run of runs2) {
13750
13870
  console.log(
13751
- ` ${p.name.padEnd(nameWidth)} ${domainLabel(p).padEnd(domainWidth)} ${p.country.padEnd(7)} ${p.language}`
13871
+ ` ${run.id} ${run.status.padEnd(10)} ${run.kind.padEnd(18)} ${run.trigger.padEnd(9)} ${run.createdAt}`
13752
13872
  );
13753
13873
  }
13754
13874
  }
13755
- function storedQualifiedAliases(project2) {
13756
- return project2.qualifiedAliases ?? [];
13757
- }
13758
- async function showProject(name, format) {
13759
- const client = getClient22();
13760
- const project2 = await client.getProject(name);
13761
- if (isMachineFormat(format)) {
13762
- console.log(JSON.stringify(project2, null, 2));
13763
- return;
13764
- }
13765
- console.log(`Project: ${project2.displayName ?? project2.name}
13766
- `);
13767
- console.log(` Name: ${project2.name}`);
13768
- console.log(` ID: ${project2.id}`);
13769
- console.log(` Domain: ${project2.canonicalDomain}`);
13770
- const secondaryDomains = effectiveDomains(project2).slice(1);
13771
- if (secondaryDomains.length > 0) {
13772
- console.log(` Owned domains: ${secondaryDomains.join(", ")}`);
13875
+ var POLL_TIMEOUT_MS3 = 10 * 60 * 1e3;
13876
+ async function pollRun2(client, runId, showProgress = true) {
13877
+ const deadline = Date.now() + POLL_TIMEOUT_MS3;
13878
+ for (; ; ) {
13879
+ await new Promise((r) => setTimeout(r, 2e3));
13880
+ if (Date.now() > deadline) {
13881
+ throw new Error(`Timed out waiting for run ${runId} after ${POLL_TIMEOUT_MS3 / 1e3}s`);
13882
+ }
13883
+ const run = await client.getRun(runId);
13884
+ if (showProgress) process.stderr.write(".");
13885
+ if (TERMINAL_STATUSES.has(run.status) || outstandingBatches(run).length > 0) {
13886
+ return run;
13887
+ }
13773
13888
  }
13774
- if (project2.aliases && project2.aliases.length > 0) {
13775
- console.log(` Aliases: ${project2.aliases.join(", ")}`);
13889
+ }
13890
+ function outstandingBatches(run) {
13891
+ if (TERMINAL_STATUSES.has(run.status)) return [];
13892
+ return (run.providerBatches ?? []).filter((batch) => OUTSTANDING_PROVIDER_BATCH_STATUSES.includes(batch.status));
13893
+ }
13894
+ function batchWaitLine(run) {
13895
+ const outstanding = outstandingBatches(run);
13896
+ if (outstanding.length === 0) return null;
13897
+ const batches = outstanding.map((batch) => `${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`);
13898
+ return `Waiting on provider batch(es): ${batches.join("; ")}; check with canonry run show ${run.id}`;
13899
+ }
13900
+ function printRunDetail(run) {
13901
+ console.log(`Run: ${run.id}`);
13902
+ console.log(` Status: ${run.status}`);
13903
+ console.log(` Kind: ${run.kind}`);
13904
+ if (run.trigger) console.log(` Trigger: ${run.trigger}`);
13905
+ if (run.startedAt) console.log(` Started: ${run.startedAt}`);
13906
+ if (run.finishedAt) console.log(` Finished: ${run.finishedAt}`);
13907
+ if (run.createdAt) console.log(` Created: ${run.createdAt}`);
13908
+ const skipped = Object.entries(run.skippedProviders ?? {});
13909
+ for (const [provider, streak] of skipped) console.log(` Skipped: ${providerAccountStreakLine(provider, streak)}`);
13910
+ if (run.error) {
13911
+ if (run.error.message) console.log(` Error: ${run.error.message}`);
13912
+ if (run.error.providers) {
13913
+ for (const [provider, detail] of Object.entries(run.error.providers)) {
13914
+ if (detail.skipped && run.skippedProviders?.[provider]) continue;
13915
+ console.log(` Error (${provider}): ${detail.message}`);
13916
+ }
13917
+ }
13776
13918
  }
13777
- const qualifiedAliases = storedQualifiedAliases(project2);
13778
- if (qualifiedAliases.length > 0) {
13779
- console.log(` Qualified: ${qualifiedAliases.join(", ")} (sentiment)`);
13919
+ const batched = Object.keys(run.dispatchModes ?? {}).sort();
13920
+ if (batched.length > 0) console.log(` Dispatch: ${batched.map((provider) => `${provider}=batch`).join(", ")} (other providers sync)`);
13921
+ if (run.providerBatches && run.providerBatches.length > 0) {
13922
+ console.log("\n Provider batches:");
13923
+ for (const batch of run.providerBatches) console.log(` ${providerBatchLine(batch)}`);
13780
13924
  }
13781
- console.log(` Country: ${project2.country}`);
13782
- console.log(` Language: ${project2.language}`);
13783
- console.log(` Config source: ${project2.configSource}`);
13784
- console.log(` Config revision: ${project2.configRevision}`);
13785
- console.log(` Providers: ${(project2.providers ?? []).length > 0 ? project2.providers.join(", ") : "all configured"}`);
13786
- const providerModels = Object.entries(project2.providerModels ?? {});
13787
- console.log(` Model overrides: ${providerModels.length > 0 ? providerModels.map(([provider, model]) => `${provider}=${model}`).join(", ") : "(none; instance settings inherited)"}`);
13788
- const dispatchModes = Object.entries(project2.providerDispatchModes ?? {}).sort(([left], [right]) => left.localeCompare(right));
13789
- console.log(` Dispatch modes: ${dispatchModes.length > 0 ? `${dispatchModes.map(([provider, mode]) => `${provider}=${mode}`).join(", ")} (scheduled sweeps; others sync)` : "(none; every provider runs sync)"}`);
13790
- console.log(` Negative reviews: ${resolveNegativeReviewMaxStars(project2.negativeReviewMaxStars)} stars or fewer${project2.negativeReviewMaxStars == null ? " (default)" : ""}`);
13791
- if (project2.siteAuditMaxPages !== void 0) {
13792
- console.log(` Site Health page budget: ${formatSiteAuditPageBudget(project2.siteAuditMaxPages)}`);
13925
+ if (run.usage && run.usage.length > 0) printUsageTable(run.usage);
13926
+ if (run.snapshots && run.snapshots.length > 0) {
13927
+ console.log(`
13928
+ Snapshots: ${run.snapshots.length} (cell = [citation][mention]; C=cited c=not, M=mentioned m=not, \u2013=no data)`);
13929
+ for (const s of run.snapshots) {
13930
+ const citationGlyph = s.citationState === CitationStates.cited ? "C" : "c";
13931
+ const mentionGlyph = typeof s.answerMentioned === "boolean" ? s.answerMentioned ? "M" : "m" : "\u2013";
13932
+ const modelLabel = s.model ? ` (${s.model})` : "";
13933
+ console.log(` [${citationGlyph}${mentionGlyph}] ${s.provider}${modelLabel} ${s.query}`);
13934
+ }
13793
13935
  }
13794
- if (project2.competitorAutoAliases !== void 0) {
13795
- console.log(` Competitor auto aliases: ${formatCompetitorAutoAliasMode(project2.competitorAutoAliases)}`);
13936
+ }
13937
+ function providerBatchLine(batch) {
13938
+ const recorded = `${batch.recordedCount} of ${batch.requestCount} answers recorded`;
13939
+ const withError = (text) => batch.error ? `${text} (${batch.error})` : text;
13940
+ switch (batch.status) {
13941
+ case ProviderBatchStatuses.submitting:
13942
+ return `submitting provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests`;
13943
+ // The two outstanding statuses: the run stays `running` until they clear.
13944
+ case ProviderBatchStatuses.submitted:
13945
+ case ProviderBatchStatuses.ended:
13946
+ return `waiting on provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`;
13947
+ case ProviderBatchStatuses.ingested:
13948
+ return withError(`provider batch ${batch.provider}: ingested \u2014 ${recorded}`);
13949
+ case ProviderBatchStatuses.cancelled:
13950
+ return withError(`provider batch ${batch.provider}: cancelled \u2014 ${recorded}; the rest are missing and can be filled`);
13951
+ case ProviderBatchStatuses.failed:
13952
+ return withError(`provider batch ${batch.provider}: rejected by the provider \u2014 its ${batch.requestCount} answers ran sync instead`);
13953
+ case ProviderBatchStatuses.unknown:
13954
+ return withError(`provider batch ${batch.provider}: submit outcome unknown \u2014 never resubmitted; its ${batch.requestCount} answers stay missing`);
13796
13955
  }
13797
- console.log(` Tags: ${project2.tags.length > 0 ? project2.tags.join(", ") : "(none)"}`);
13798
- const labelEntries = Object.entries(project2.labels);
13799
- console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "(none)"}`);
13800
- if (project2.createdAt) console.log(` Created: ${project2.createdAt}`);
13801
- if (project2.updatedAt) console.log(` Updated: ${project2.updatedAt}`);
13802
13956
  }
13803
- function formatCompetitorAutoAliasMode(mode) {
13804
- if (mode === "apply") return "apply (names detected after each sweep are stored and counted)";
13805
- if (mode === "off") return "off (no detection after sweeps)";
13806
- return "preview (detected after each sweep and logged; nothing stored)";
13957
+ function printUsageTable(rows) {
13958
+ const cost = (row) => row.estimatedCostMicros === null ? "unpriced" : `${formatMicros(row.estimatedCostMicros, "USD", { fractionDigits: 4, showTinyAsLessThan: true })}${row.unpricedAnswers > 0 ? ` (+${row.unpricedAnswers} unpriced)` : ""}`;
13959
+ const table = rows.map((row) => [
13960
+ row.provider,
13961
+ row.pricingTier,
13962
+ row.answers.toLocaleString("en-US"),
13963
+ row.inputTokens.toLocaleString("en-US"),
13964
+ row.cachedInputTokens.toLocaleString("en-US"),
13965
+ row.cacheWriteTokens.toLocaleString("en-US"),
13966
+ row.outputTokens.toLocaleString("en-US"),
13967
+ row.searchCount.toLocaleString("en-US"),
13968
+ cost(row)
13969
+ ]);
13970
+ const header = ["PROVIDER", "TIER", "ANSWERS", "INPUT", "CACHED", "CACHE WRITE", "OUTPUT", "SEARCHES", "EST. COST"];
13971
+ const widths = header.map((title, index) => Math.max(title.length, ...table.map((cells) => cells[index].length)));
13972
+ const line = (cells) => ` ${cells.map((cell2, index) => cell2.padEnd(widths[index])).join(" ").trimEnd()}`;
13973
+ console.log("\n Usage (answers with recorded usage; cost is an estimate):");
13974
+ console.log(line(header));
13975
+ for (const cells of table) console.log(line(cells));
13807
13976
  }
13808
- async function updateProjectSettings(name, opts) {
13809
- const client = getClient22();
13810
- const project2 = await client.getProject(name);
13811
- let ownedDomains = opts.ownedDomains ?? project2.ownedDomains ?? [];
13812
- if (opts.addOwnedDomain) {
13813
- const toAdd = opts.addOwnedDomain.filter((d) => !ownedDomains.includes(d));
13814
- ownedDomains = [...ownedDomains, ...toAdd];
13815
- }
13816
- if (opts.removeOwnedDomain) {
13817
- const toRemove = new Set(opts.removeOwnedDomain);
13818
- ownedDomains = ownedDomains.filter((d) => !toRemove.has(d));
13977
+ var FILL_POLL_INTERVAL_MS = 3e3;
13978
+ var FILL_POLL_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
13979
+ function missingSummary(completeness) {
13980
+ const parts = Object.entries(completeness.missingByProvider).sort(([left], [right]) => left.localeCompare(right)).map(([provider, count]) => `${provider} ${count}`);
13981
+ return parts.length ? parts.join(", ") : "none";
13982
+ }
13983
+ function printCompleteness(completeness) {
13984
+ console.log(`Run: ${completeness.runId}`);
13985
+ console.log(` Status: ${completeness.status}`);
13986
+ if (!completeness.planned) {
13987
+ console.log(" Not a measurement-plan run: there are no expected answers to count.");
13988
+ return;
13819
13989
  }
13820
- const nextDisplayName = opts.displayName ?? project2.displayName ?? project2.name;
13821
- let aliases = opts.aliases ?? project2.aliases ?? [];
13822
- if (opts.addAlias) {
13823
- const existingKeys = new Set(aliases.map((a) => a.toLowerCase()));
13824
- const toAdd = opts.addAlias.filter((a) => !existingKeys.has(a.toLowerCase()));
13825
- aliases = [...aliases, ...toAdd];
13990
+ if (!completeness.readable) {
13991
+ console.log(" Answers: unknown (the run's manifest cannot be read)");
13992
+ } else {
13993
+ console.log(` Answers: ${completeness.executed}/${completeness.expected}`);
13994
+ console.log(` Missing: ${completeness.missing} (${missingSummary(completeness)})`);
13826
13995
  }
13827
- if (opts.removeAlias) {
13828
- const toRemove = new Set(opts.removeAlias.map((a) => a.toLowerCase()));
13829
- aliases = aliases.filter((a) => !toRemove.has(a.toLowerCase()));
13996
+ if (completeness.refusal) console.log(` Fillable: no. ${completeness.refusal.message}`);
13997
+ else console.log(` Fillable: ${completeness.fillable ? "yes" : "not right now (a sweep or another fill is running)"}`);
13998
+ const fill = completeness.latestFill;
13999
+ if (fill) {
14000
+ console.log(` Last fill: ${fill.id} ${fill.status}, ${fill.filled}/${fill.expected} recorded${fill.error ? `. ${fill.error}` : ""}`);
13830
14001
  }
13831
- const nextAliases = normalizeProjectAliases(nextDisplayName, aliases);
13832
- const touchesQualified = (opts.addQualifiedAlias?.length ?? 0) > 0 || (opts.removeQualifiedAlias?.length ?? 0) > 0;
13833
- let qualifiedAliases = resolveProjectQualifiedAliases(
13834
- { displayName: nextDisplayName, aliases: nextAliases },
13835
- storedQualifiedAliases(project2)
13836
- ).value;
13837
- if (opts.addQualifiedAlias) {
13838
- const existingKeys = new Set(qualifiedAliases.map((alias) => alias.toLowerCase()));
13839
- for (const alias of opts.addQualifiedAlias) {
13840
- if (existingKeys.has(alias.toLowerCase())) continue;
13841
- existingKeys.add(alias.toLowerCase());
13842
- qualifiedAliases = [...qualifiedAliases, alias];
13843
- }
13844
- }
13845
- if (opts.removeQualifiedAlias) {
13846
- const toRemove = new Set(opts.removeQualifiedAlias.map((alias) => alias.toLowerCase()));
13847
- qualifiedAliases = qualifiedAliases.filter((alias) => !toRemove.has(alias.toLowerCase()));
13848
- }
13849
- const providerModels = { ...project2.providerModels ?? {}, ...opts.providerModels ?? {} };
13850
- for (const provider of opts.clearProviderModels ?? []) delete providerModels[provider];
13851
- const nextProviders = opts.providers ?? project2.providers ?? [];
13852
- if (nextProviders.length > 0) {
13853
- const requested = Object.keys(opts.providerModels ?? {}).filter((p) => !nextProviders.includes(p));
13854
- if (requested.length > 0) {
13855
- throw usageError(
13856
- `Error: --provider-model set for ${requested.join(", ")}, which ${requested.length > 1 ? "are" : "is"} not a selected engine for "${name}"`,
13857
- {
13858
- message: "provider-model set for a provider the project does not run",
13859
- details: { command: "project.update", project: name, providers: nextProviders, unselected: requested }
13860
- }
13861
- );
13862
- }
13863
- }
13864
- const touchesDispatch = Object.keys(opts.dispatchModes ?? {}).length > 0 || (opts.clearDispatchModes ?? []).length > 0;
13865
- const providerDispatchModes = { ...project2.providerDispatchModes ?? {}, ...opts.dispatchModes ?? {} };
13866
- for (const provider of opts.clearDispatchModes ?? []) delete providerDispatchModes[provider];
13867
- const result = await client.putProject(name, {
13868
- displayName: nextDisplayName,
13869
- canonicalDomain: opts.domain ?? project2.canonicalDomain,
13870
- ownedDomains,
13871
- aliases: nextAliases,
13872
- ...touchesQualified ? { qualifiedAliases } : {},
13873
- country: opts.country ?? project2.country,
13874
- language: opts.language ?? project2.language,
13875
- tags: project2.tags,
13876
- labels: project2.labels,
13877
- providers: opts.providers ?? project2.providers,
13878
- providerModels,
13879
- ...touchesDispatch ? { providerDispatchModes } : {},
13880
- locations: project2.locations,
13881
- defaultLocation: project2.defaultLocation,
13882
- autoExtractBacklinks: project2.autoExtractBacklinks,
13883
- // Sent only when asked for: the server keeps the stored threshold when the
13884
- // field is absent, so a stale read cannot overwrite a newer setting.
13885
- ...opts.negativeReviewMaxStars !== void 0 ? { negativeReviewMaxStars: opts.negativeReviewMaxStars } : {},
13886
- // Same for the page budget: never echoed from the read above, so an edit
13887
- // that does not touch it cannot overwrite a newer one or send a stray null.
13888
- ...opts.siteAuditMaxPages !== void 0 ? { siteAuditMaxPages: opts.siteAuditMaxPages } : {},
13889
- // Same for the detection mode.
13890
- ...opts.competitorAutoAliases !== void 0 ? { competitorAutoAliases: opts.competitorAutoAliases } : {}
13891
- });
13892
- warnDroppedDispatchModes(touchesDispatch ? providerDispatchModes : project2.providerDispatchModes ?? {}, result);
13893
- if (isMachineFormat(opts.format)) {
13894
- console.log(JSON.stringify(result, null, 2));
14002
+ }
14003
+ async function showRunCompleteness(runId, format) {
14004
+ const completeness = await getClient21().getRunCompleteness(runId);
14005
+ if (isMachineFormat(format)) {
14006
+ console.log(JSON.stringify(completeness, null, 2));
13895
14007
  return;
13896
14008
  }
13897
- console.log(`Project updated: ${result.name}`);
13898
- const dropped = Object.keys(providerModels).filter((provider) => !(provider in result.providerModels));
13899
- if (dropped.length > 0) {
13900
- console.log(` Dropped model override for deselected engine(s): ${dropped.join(", ")}`);
13901
- }
14009
+ printCompleteness(completeness);
13902
14010
  }
13903
- async function deleteProject(name, opts) {
13904
- const client = getClient22();
13905
- const isJson = isMachineFormat(opts?.format);
13906
- if (opts?.dryRun) {
13907
- const preview = await client.previewProjectDelete(name);
13908
- if (isJson) {
13909
- console.log(JSON.stringify({ dryRun: true, ...preview }, null, 2));
14011
+ async function fillRun(runId, opts = {}) {
14012
+ const client = getClient21();
14013
+ const response = await client.fillRun(runId, {
14014
+ ...opts.providers?.length ? { providers: opts.providers } : {},
14015
+ ...opts.dryRun ? { dryRun: true } : {}
14016
+ });
14017
+ if (!opts.wait || response.outcome !== "queued" || !response.fill) {
14018
+ if (isMachineFormat(opts.format)) {
14019
+ console.log(JSON.stringify(response, null, 2));
13910
14020
  return;
13911
14021
  }
13912
- const { cascadeRows: cr, detachedRows: dr } = preview;
13913
- console.log(`Project delete preview for "${name}":`);
13914
- console.log(` Cascade-deletes:`);
13915
- console.log(` queries: ${cr.queries}`);
13916
- console.log(` competitors: ${cr.competitors}`);
13917
- console.log(` runs: ${cr.runs}`);
13918
- console.log(` snapshots: ${cr.snapshots}`);
13919
- console.log(` insights: ${cr.insights}`);
13920
- console.log(` Detached (project_id set to NULL):`);
13921
- console.log(` audit_log: ${dr.auditLog}`);
13922
- console.log(``);
13923
- console.log(`No DB writes performed. Re-run without --dry-run to delete.`);
14022
+ if (response.outcome === "already-complete") console.log(`Run ${runId} is already complete: nothing to fill.`);
14023
+ else if (response.outcome === "dry-run") console.log("Dry run: nothing was queued.");
14024
+ else if (response.fill) console.log(`Fill ${response.fill.id} queued: ${response.fill.expected} missing answer(s) (${missingSummary(response.completeness)}).`);
14025
+ printCompleteness(response.completeness);
13924
14026
  return;
13925
14027
  }
13926
- await client.deleteProject(name);
13927
- if (isJson) {
13928
- console.log(JSON.stringify({ name, deleted: true }, null, 2));
13929
- return;
14028
+ const fillId = response.fill.id;
14029
+ const deadline = Date.now() + FILL_POLL_TIMEOUT_MS;
14030
+ let completeness = response.completeness;
14031
+ if (!isMachineFormat(opts.format)) {
14032
+ process.stderr.write(`Filling ${response.fill.expected} missing answer(s) in run ${runId}`);
13930
14033
  }
13931
- console.log(`Project deleted: ${name}`);
13932
- }
13933
- async function addLocation(project2, opts) {
13934
- const client = getClient22();
13935
- const location = await client.addLocation(project2, {
13936
- label: opts.label,
13937
- city: opts.city,
13938
- region: opts.region,
13939
- country: opts.country,
13940
- timezone: opts.timezone
13941
- });
13942
- if (isMachineFormat(opts.format)) {
13943
- console.log(JSON.stringify(location, null, 2));
13944
- return;
14034
+ for (; ; ) {
14035
+ await new Promise((resolve) => setTimeout(resolve, FILL_POLL_INTERVAL_MS));
14036
+ if (Date.now() > deadline) {
14037
+ throw new CliError({
14038
+ code: "RUN_FILL_INCOMPLETE",
14039
+ message: `Timed out waiting for fill ${fillId}; it may still be running. Check with: canonry run completeness ${runId}`,
14040
+ exitCode: EXIT_SYSTEM_ERROR,
14041
+ details: { runId, fillId }
14042
+ });
14043
+ }
14044
+ completeness = await client.getRunCompleteness(runId);
14045
+ if (!isMachineFormat(opts.format)) process.stderr.write(".");
14046
+ const latest = completeness.latestFill;
14047
+ const working = latest !== null && (latest.status === "queued" || latest.status === "running");
14048
+ if (completeness.status === "completed" || !working) break;
14049
+ }
14050
+ if (!isMachineFormat(opts.format)) process.stderr.write("\n");
14051
+ if (isMachineFormat(opts.format)) console.log(JSON.stringify(completeness, null, 2));
14052
+ else printCompleteness(completeness);
14053
+ if (completeness.status !== "completed") {
14054
+ throw new CliError({
14055
+ code: "RUN_FILL_INCOMPLETE",
14056
+ message: `Run ${runId} is still ${completeness.status}: ${completeness.missing} answer(s) missing (${missingSummary(completeness)}).`,
14057
+ exitCode: EXIT_SYSTEM_ERROR,
14058
+ details: { runId, fillId, missing: completeness.missing, missingByProvider: completeness.missingByProvider }
14059
+ });
13945
14060
  }
13946
- console.log(`Location added: ${opts.label} (${opts.city}, ${opts.region}, ${opts.country})`);
13947
14061
  }
13948
- async function listLocations(project2, format) {
14062
+
14063
+ // src/commands/status.ts
14064
+ function getClient22() {
14065
+ return createApiClient();
14066
+ }
14067
+ async function showStatus(project2, format) {
13949
14068
  const client = getClient22();
13950
- const result = await client.listLocations(project2);
13951
- if (format === "json") {
13952
- console.log(JSON.stringify(result, null, 2));
13953
- return;
13954
- }
13955
- if (format === "jsonl") {
13956
- emitJsonl(result.locations.map((loc) => ({
13957
- project: project2,
13958
- isDefault: loc.label === result.defaultLocation,
13959
- ...loc
13960
- })));
13961
- return;
13962
- }
13963
- if (result.locations.length === 0) {
13964
- console.log(`No locations configured for "${project2}".`);
14069
+ const projectData = await client.getProject(project2);
14070
+ const latest = await getLatestRunSummary(client, project2);
14071
+ if (isMachineFormat(format)) {
14072
+ let runs2 = [];
14073
+ try {
14074
+ runs2 = await client.listRuns(project2);
14075
+ } catch {
14076
+ }
14077
+ console.log(JSON.stringify({
14078
+ project: projectData,
14079
+ runs: runs2,
14080
+ latestRun: latest.run,
14081
+ totalRuns: latest.totalRuns,
14082
+ // Absent from a server that predates it.
14083
+ ...latest.admission ? { admission: latest.admission } : {},
14084
+ ...latest.manualAdmission ? { manualAdmission: latest.manualAdmission } : {}
14085
+ }, null, 2));
13965
14086
  return;
13966
14087
  }
13967
- console.log(`Locations for "${project2}" (${result.locations.length}):
14088
+ console.log(`Status: ${projectData.displayName ?? projectData.name} (${projectData.name})
13968
14089
  `);
13969
- console.log(" LABEL CITY REGION COUNTRY DEFAULT");
13970
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500");
13971
- for (const loc of result.locations) {
13972
- const isDefault = loc.label === result.defaultLocation ? " *" : "";
13973
- console.log(
13974
- ` ${loc.label.padEnd(15)} ${loc.city.padEnd(19)} ${loc.region.padEnd(19)} ${loc.country.padEnd(7)}${isDefault}`
13975
- );
13976
- }
13977
- if (result.defaultLocation) {
14090
+ console.log(` Domain: ${projectData.canonicalDomain}`);
14091
+ console.log(` Country: ${projectData.country}`);
14092
+ console.log(` Language: ${projectData.language}`);
14093
+ if (latest.run) {
13978
14094
  console.log(`
13979
- Default: ${result.defaultLocation}`);
13980
- }
13981
- }
13982
- async function removeLocation(project2, label, format) {
13983
- const client = getClient22();
13984
- await client.removeLocation(project2, label);
13985
- if (isMachineFormat(format)) {
13986
- console.log(JSON.stringify({ project: project2, label, removed: true }, null, 2));
13987
- return;
14095
+ Latest run:`);
14096
+ console.log(` ID: ${latest.run.id}`);
14097
+ console.log(` Status: ${latest.run.status}`);
14098
+ console.log(` Created: ${latest.run.createdAt}`);
14099
+ if (latest.run.finishedAt) {
14100
+ console.log(` Finished: ${latest.run.finishedAt}`);
14101
+ }
14102
+ console.log(`
14103
+ Total runs: ${latest.totalRuns}`);
14104
+ } else {
14105
+ const projectArg = /^[\w.-]+$/.test(project2) ? project2 : `'${project2.replaceAll("'", `'\\''`)}'`;
14106
+ console.log(`
14107
+ No runs yet. Capture Page Health with "canonry technical-aeo run ${projectArg}", or run an AI Visibility sweep after adding a provider.`);
13988
14108
  }
13989
- console.log(`Location removed: ${label}`);
14109
+ const admission = runAdmissionLines(project2, latest.admission, latest.manualAdmission);
14110
+ if (admission.length > 0) console.log(`
14111
+ ${admission.map((line) => ` ${line}`).join("\n")}`);
13990
14112
  }
13991
- async function setDefaultLocation(project2, label, format) {
13992
- const client = getClient22();
13993
- const result = await client.setDefaultLocation(project2, label);
13994
- if (isMachineFormat(format)) {
13995
- console.log(JSON.stringify({ project: project2, ...result }, null, 2));
13996
- return;
14113
+ async function getLatestRunSummary(client, project2) {
14114
+ try {
14115
+ return await client.getLatestRun(project2);
14116
+ } catch (err) {
14117
+ if (!isEndpointMissing(err)) throw err;
14118
+ const runs2 = await client.listRuns(project2);
14119
+ if (runs2.length === 0) {
14120
+ return { totalRuns: 0, run: null };
14121
+ }
14122
+ const latestRun = runs2.reduce(
14123
+ (current, candidate) => candidate.createdAt > current.createdAt ? candidate : current
14124
+ );
14125
+ return {
14126
+ totalRuns: runs2.length,
14127
+ run: latestRun
14128
+ };
13997
14129
  }
13998
- console.log(`Default location set to: ${label}`);
13999
14130
  }
14000
14131
 
14001
- // src/cli-commands/project.ts
14002
- var PROJECT_CLI_COMMANDS = [
14003
- {
14004
- path: ["project", "create"],
14005
- usage: "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--dispatch-mode provider=sync|batch...] [--site-audit-max-pages <1-50000|full>] [--format json]",
14006
- help: "Create a project. Pass --domain for the public site to scan. Provider credentials are not required for Page Health.",
14007
- options: {
14008
- domain: { type: "string", short: "d" },
14009
- "owned-domain": multiStringOption(),
14010
- alias: multiStringOption(),
14011
- "qualified-alias": multiStringOption(),
14012
- country: stringOption(),
14013
- language: stringOption(),
14014
- "display-name": stringOption(),
14015
- provider: multiStringOption(),
14016
- "provider-model": multiStringOption(),
14017
- "dispatch-mode": multiStringOption(),
14018
- "site-audit-max-pages": stringOption()
14019
- },
14132
+ // src/cli-commands/operator.ts
14133
+ var RESULTS_EXPORT_USAGE = "canonry results export <project> [--format json|csv] [--since <ISO>] [--until <ISO>] [--include-probes] [--output <path>|-]";
14134
+ function parseResultsExportFormat(value) {
14135
+ if (value === void 0 || value === "json") return "json";
14136
+ if (value === "csv") return "csv";
14137
+ throw usageError(`Error: --format must be "json" or "csv"
14138
+
14139
+ Usage: ${RESULTS_EXPORT_USAGE}`);
14140
+ }
14141
+ var RESULTS_CLEAR_USAGE = "canonry results clear <project> [--run <id> ...] [--research-run <id> ...] [--confirm] [--format json|jsonl]";
14142
+ function parseHistoryPageOption(input, key, usage) {
14143
+ const value = getString(input.values, key);
14144
+ if (value === void 0) return void 0;
14145
+ if (!/^\d+$/.test(value)) {
14146
+ throw usageError(`Error: --${key} must be a ${key === "limit" ? "whole number from 1 to 500" : "nonnegative integer"}
14147
+ Usage: ${usage}`);
14148
+ }
14149
+ const parsed = Number(value);
14150
+ const valid = Number.isSafeInteger(parsed) && (key === "limit" ? parsed >= 1 && parsed <= 500 : parsed >= 0);
14151
+ if (!valid) {
14152
+ throw usageError(`Error: --${key} must be a ${key === "limit" ? "whole number from 1 to 500" : "nonnegative integer"}
14153
+ Usage: ${usage}`);
14154
+ }
14155
+ return parsed;
14156
+ }
14157
+ var OPERATOR_CLI_COMMANDS = [
14158
+ {
14159
+ path: ["results", "clear"],
14160
+ usage: RESULTS_CLEAR_USAGE,
14161
+ options: { run: multiStringOption(), "research-run": multiStringOption(), confirm: { type: "boolean", default: false } },
14020
14162
  run: async (input) => {
14021
- const name = requireProject(
14022
- input,
14023
- "project.create",
14024
- "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--dispatch-mode provider=sync|batch...] [--site-audit-max-pages <1-50000|full>] [--format json]"
14025
- );
14026
- await createProject(name, {
14027
- domain: getString(input.values, "domain") ?? name,
14028
- ownedDomains: getStringArray(input.values, "owned-domain") ?? [],
14029
- aliases: getStringArray(input.values, "alias") ?? [],
14030
- qualifiedAliases: getStringArray(input.values, "qualified-alias") ?? [],
14031
- country: getString(input.values, "country") ?? "US",
14032
- language: getString(input.values, "language") ?? "en",
14033
- displayName: getString(input.values, "display-name") ?? name,
14034
- providers: getStringArray(input.values, "provider") ?? [],
14035
- providerModels: parseProviderModelAssignments(getStringArray(input.values, "provider-model")),
14036
- providerDispatchModes: parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode")),
14037
- siteAuditMaxPages: parseSiteAuditMaxPages(getString(input.values, "site-audit-max-pages"), "project.create"),
14163
+ await clearResults(requireProject(input, "results.clear", RESULTS_CLEAR_USAGE), {
14164
+ runIds: getStringArray(input.values, "run") ?? [],
14165
+ researchRunIds: getStringArray(input.values, "research-run") ?? [],
14166
+ confirm: getBoolean(input.values, "confirm"),
14038
14167
  format: input.format
14039
14168
  });
14040
14169
  }
14041
14170
  },
14042
14171
  {
14043
- path: ["project", "update"],
14044
- usage: "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--add-qualified-alias <name>...] [--remove-qualified-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>] [--site-audit-max-pages <1-50000|full>] [--competitor-auto-aliases <off|preview|apply>] [--format json]",
14172
+ path: ["results", "export"],
14173
+ usage: RESULTS_EXPORT_USAGE,
14045
14174
  options: {
14046
- domain: { type: "string", short: "d" },
14047
- "owned-domain": multiStringOption(),
14048
- "add-domain": multiStringOption(),
14049
- "remove-domain": multiStringOption(),
14050
- alias: multiStringOption(),
14051
- "add-alias": multiStringOption(),
14052
- "remove-alias": multiStringOption(),
14053
- "add-qualified-alias": multiStringOption(),
14054
- "remove-qualified-alias": multiStringOption(),
14055
- country: stringOption(),
14056
- language: stringOption(),
14057
- "display-name": stringOption(),
14058
- provider: multiStringOption(),
14059
- "all-providers": { type: "boolean" },
14060
- "provider-model": multiStringOption(),
14061
- "clear-provider-model": multiStringOption(),
14062
- "dispatch-mode": multiStringOption(),
14063
- "clear-dispatch-mode": multiStringOption(),
14064
- "negative-review-max-stars": stringOption(),
14065
- "site-audit-max-pages": stringOption(),
14066
- "competitor-auto-aliases": stringOption()
14175
+ since: stringOption(),
14176
+ until: stringOption(),
14177
+ "include-probes": { type: "boolean", default: false },
14178
+ output: { type: "string", short: "o" }
14067
14179
  },
14068
14180
  run: async (input) => {
14069
- const name = requireProject(
14070
- input,
14071
- "project.update",
14072
- "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--add-qualified-alias <name>...] [--remove-qualified-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>] [--site-audit-max-pages <1-50000|full>] [--competitor-auto-aliases <off|preview|apply>] [--format json]"
14073
- );
14074
- const providers = getStringArray(input.values, "provider");
14075
- const allProviders = getBoolean(input.values, "all-providers");
14076
- if (allProviders && providers?.length) throw usageError("Error: --all-providers conflicts with --provider");
14077
- const providerModels = parseProviderModelAssignments(getStringArray(input.values, "provider-model"));
14078
- const clearProviderModels = getStringArray(input.values, "clear-provider-model") ?? [];
14079
- for (const provider of clearProviderModels) {
14080
- if (provider in providerModels) throw usageError(`Error: --provider-model and --clear-provider-model conflict for ${provider}`);
14081
- }
14082
- const dispatchModes = parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode"));
14083
- const clearDispatchModes = getStringArray(input.values, "clear-dispatch-mode") ?? [];
14084
- for (const provider of clearDispatchModes) {
14085
- if (provider in dispatchModes) throw usageError(`Error: --dispatch-mode and --clear-dispatch-mode conflict for ${provider}`);
14086
- }
14087
- await updateProjectSettings(name, {
14088
- displayName: getString(input.values, "display-name"),
14089
- domain: getString(input.values, "domain"),
14090
- ownedDomains: getStringArray(input.values, "owned-domain"),
14091
- addOwnedDomain: getStringArray(input.values, "add-domain"),
14092
- removeOwnedDomain: getStringArray(input.values, "remove-domain"),
14093
- aliases: getStringArray(input.values, "alias"),
14094
- addAlias: getStringArray(input.values, "add-alias"),
14095
- removeAlias: getStringArray(input.values, "remove-alias"),
14096
- addQualifiedAlias: getStringArray(input.values, "add-qualified-alias"),
14097
- removeQualifiedAlias: getStringArray(input.values, "remove-qualified-alias"),
14098
- country: getString(input.values, "country"),
14099
- language: getString(input.values, "language"),
14100
- providers: allProviders ? [] : providers,
14101
- providerModels,
14102
- clearProviderModels,
14103
- dispatchModes,
14104
- clearDispatchModes,
14105
- negativeReviewMaxStars: parseNegativeReviewMaxStars(getString(input.values, "negative-review-max-stars")),
14106
- siteAuditMaxPages: parseSiteAuditMaxPages(getString(input.values, "site-audit-max-pages"), "project.update"),
14107
- competitorAutoAliases: parseCompetitorAutoAliasMode(getString(input.values, "competitor-auto-aliases")),
14108
- format: input.format
14181
+ const project2 = requireProject(input, "results export", RESULTS_EXPORT_USAGE);
14182
+ await exportResults(project2, {
14183
+ format: parseResultsExportFormat(getString(input.values, "format")),
14184
+ since: getString(input.values, "since"),
14185
+ until: getString(input.values, "until"),
14186
+ includeProbes: getBoolean(input.values, "include-probes"),
14187
+ output: getString(input.values, "output")
14109
14188
  });
14110
14189
  }
14111
14190
  },
14112
14191
  {
14113
- path: ["project", "list"],
14114
- usage: "canonry project list [--format json]",
14115
- allowPositionals: false,
14192
+ path: ["status"],
14193
+ usage: "canonry status <project> [--format json]",
14116
14194
  run: async (input) => {
14117
- await listProjects(input.format);
14195
+ const project2 = requireProject(input, "status", "canonry status <project> [--format json]");
14196
+ await showStatus(project2, input.format);
14118
14197
  }
14119
14198
  },
14120
14199
  {
14121
- path: ["project", "show"],
14122
- usage: "canonry project show <name> [--format json]",
14200
+ path: ["evidence"],
14201
+ usage: "canonry evidence <project> [--format json]",
14123
14202
  run: async (input) => {
14124
- const name = requireProject(input, "project.show", "canonry project show <name>");
14125
- await showProject(name, input.format);
14203
+ const project2 = requireProject(input, "evidence", "canonry evidence <project> [--format json]");
14204
+ await showEvidence(project2, input.format);
14126
14205
  }
14127
14206
  },
14128
14207
  {
14129
- path: ["project", "delete"],
14130
- usage: "canonry project delete <name> [--dry-run] [--format json]",
14131
- supportsDryRun: true,
14208
+ path: ["history"],
14209
+ usage: "canonry history <project> [--limit <1..500>] [--offset <n>] [--since <ISO>] [--action <action>] [--actor <actor>] [--entity-type <type>] [--format json|jsonl]\n canonry history --all [same filters]",
14210
+ options: {
14211
+ all: { type: "boolean", default: false },
14212
+ limit: stringOption(),
14213
+ offset: stringOption(),
14214
+ since: stringOption(),
14215
+ action: stringOption(),
14216
+ actor: stringOption(),
14217
+ "entity-type": stringOption()
14218
+ },
14132
14219
  run: async (input) => {
14133
- const name = requireProject(input, "project.delete", "canonry project delete <name> [--dry-run] [--format json]");
14134
- await deleteProject(name, { dryRun: input.dryRun, format: input.format });
14220
+ const usage = "canonry history <project> [--limit <1..500>] [--offset <n>] [--since <ISO>] [--action <action>] [--actor <actor>] [--entity-type <type>] [--format json|jsonl]\n canonry history --all [same filters]";
14221
+ const project2 = getBoolean(input.values, "all") ? void 0 : requireProject(input, "history", usage);
14222
+ await showHistory(project2, input.format, {
14223
+ limit: parseHistoryPageOption(input, "limit", usage),
14224
+ offset: parseHistoryPageOption(input, "offset", usage),
14225
+ since: getString(input.values, "since"),
14226
+ action: getString(input.values, "action"),
14227
+ actor: getString(input.values, "actor"),
14228
+ entityType: getString(input.values, "entity-type")
14229
+ });
14135
14230
  }
14136
14231
  },
14137
14232
  {
14138
- path: ["project", "add-location"],
14139
- usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]",
14233
+ path: ["export"],
14234
+ usage: "canonry export <project> [--include-results] [--format json]",
14140
14235
  options: {
14141
- label: stringOption(),
14142
- city: stringOption(),
14143
- region: stringOption(),
14144
- country: stringOption(),
14145
- timezone: stringOption()
14236
+ "include-results": { type: "boolean", default: false }
14146
14237
  },
14147
14238
  run: async (input) => {
14148
- const name = requireProject(
14149
- input,
14150
- "project.add-location",
14151
- "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]"
14152
- );
14153
- const label = getString(input.values, "label");
14154
- const city = getString(input.values, "city");
14155
- const region = getString(input.values, "region");
14156
- const country = getString(input.values, "country");
14157
- if (!label || !city || !region || !country) {
14158
- throw usageError("Error: --label, --city, --region, and --country are all required", {
14159
- message: "location label, city, region, and country are required",
14160
- details: {
14161
- command: "project.add-location",
14162
- usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]",
14163
- required: ["label", "city", "region", "country"]
14164
- }
14165
- });
14166
- }
14167
- await addLocation(name, {
14168
- label,
14169
- city,
14170
- region,
14171
- country,
14172
- timezone: getString(input.values, "timezone"),
14239
+ const project2 = requireProject(input, "export", "canonry export <project> [--include-results] [--format json]");
14240
+ await exportProject(project2, {
14241
+ includeResults: getBoolean(input.values, "include-results"),
14173
14242
  format: input.format
14174
14243
  });
14175
14244
  }
14176
14245
  },
14177
14246
  {
14178
- path: ["project", "locations"],
14179
- usage: "canonry project locations <name> [--format json]",
14180
- run: async (input) => {
14181
- const name = requireProject(input, "project.locations", "canonry project locations <name> [--format json]");
14182
- await listLocations(name, input.format);
14183
- }
14184
- },
14185
- {
14186
- path: ["project", "remove-location"],
14187
- usage: "canonry project remove-location <name> <label> [--format json]",
14247
+ path: ["analytics"],
14248
+ usage: "canonry analytics <project> [--feature metrics|gaps|sources] [--window 7d|30d|90d|all] [--format json]",
14249
+ options: {
14250
+ feature: stringOption(),
14251
+ window: stringOption()
14252
+ },
14188
14253
  run: async (input) => {
14189
- const name = requireProject(input, "project.remove-location", "canonry project remove-location <name> <label> [--format json]");
14190
- const label = requirePositional(input, 1, {
14191
- command: "project.remove-location",
14192
- usage: "canonry project remove-location <name> <label> [--format json]",
14193
- message: "project name and location label are required"
14254
+ const project2 = requireProject(input, "analytics", "canonry analytics <project> [--feature metrics|gaps|sources] [--window 7d|30d|90d|all] [--format json]");
14255
+ await showAnalytics(project2, {
14256
+ feature: getString(input.values, "feature"),
14257
+ window: getString(input.values, "window"),
14258
+ format: input.format
14194
14259
  });
14195
- await removeLocation(name, label, input.format);
14196
14260
  }
14197
14261
  },
14198
14262
  {
14199
- path: ["project", "set-default-location"],
14200
- usage: "canonry project set-default-location <name> <label> [--format json]",
14201
- run: async (input) => {
14202
- const name = requireProject(input, "project.set-default-location", "canonry project set-default-location <name> <label> [--format json]");
14203
- const label = requirePositional(input, 1, {
14204
- command: "project.set-default-location",
14205
- usage: "canonry project set-default-location <name> <label> [--format json]",
14206
- message: "project name and location label are required"
14263
+ path: ["sources"],
14264
+ usage: SOURCES_USAGE,
14265
+ options: {
14266
+ rank: { type: "boolean", default: false },
14267
+ "by-provider": { type: "boolean", default: false },
14268
+ limit: stringOption(),
14269
+ window: stringOption(),
14270
+ "query-class": stringOption(),
14271
+ "run-id": stringOption(),
14272
+ "include-by-query": stringOption()
14273
+ },
14274
+ run: async (input) => {
14275
+ const project2 = requireProject(input, "sources", SOURCES_USAGE);
14276
+ await showSources(project2, {
14277
+ rank: getBoolean(input.values, "rank"),
14278
+ byProvider: getBoolean(input.values, "by-provider"),
14279
+ limit: parseIntegerOption(input, "limit", {
14280
+ message: "--limit must be an integer",
14281
+ usage: SOURCES_USAGE,
14282
+ command: "sources"
14283
+ }),
14284
+ window: getString(input.values, "window"),
14285
+ runId: getString(input.values, "run-id"),
14286
+ queryClass: parseSourcesQueryClass(getString(input.values, "query-class")),
14287
+ includeByQuery: parseSourcesIncludeByQuery(getString(input.values, "include-by-query")),
14288
+ format: input.format
14207
14289
  });
14208
- await setDefaultLocation(name, label, input.format);
14209
14290
  }
14210
14291
  },
14211
14292
  {
14212
- path: ["project"],
14213
- usage: "canonry project <create|update|list|show|delete|add-location|locations|remove-location|set-default-location> [args]",
14293
+ path: ["apply"],
14294
+ usage: "canonry apply <file...> [--format json]",
14214
14295
  run: async (input) => {
14215
- unknownSubcommand(input.positionals[0], {
14216
- command: "project",
14217
- usage: "canonry project <create|update|list|show|delete|add-location|locations|remove-location|set-default-location> [args]",
14218
- available: ["create", "update", "list", "show", "delete", "add-location", "locations", "remove-location", "set-default-location"]
14219
- });
14296
+ if (input.positionals.length === 0) {
14297
+ throw usageError("Error: at least one file path is required\nUsage: canonry apply <file...> [--format json]", {
14298
+ message: "at least one file path is required",
14299
+ details: {
14300
+ command: "apply",
14301
+ usage: "canonry apply <file...> [--format json]"
14302
+ }
14303
+ });
14304
+ }
14305
+ await applyConfigs(input.positionals, input.format);
14220
14306
  }
14221
14307
  }
14222
14308
  ];
14223
- function parseDispatchModeAssignments(assignments) {
14224
- const result = {};
14225
- for (const assignment of assignments ?? []) {
14226
- const separator = assignment.indexOf("=");
14227
- const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
14228
- const mode = providerDispatchModeSchema.safeParse(separator === -1 ? "" : assignment.slice(separator + 1).trim().toLowerCase());
14229
- if (!provider || !mode.success) throw usageError(`Error: --dispatch-mode must use provider=sync or provider=batch (received ${assignment})`);
14230
- if (provider in result) throw usageError(`Error: duplicate --dispatch-mode assignment for ${provider}`);
14231
- result[provider] = mode.data;
14232
- }
14233
- return result;
14309
+
14310
+ // src/commands/project.ts
14311
+ function getClient23() {
14312
+ return createApiClient();
14234
14313
  }
14235
- function parseProviderModelAssignments(assignments) {
14236
- const result = {};
14237
- for (const assignment of assignments ?? []) {
14238
- const separator = assignment.indexOf("=");
14239
- const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
14240
- const model = separator === -1 ? "" : assignment.slice(separator + 1).trim();
14241
- if (!provider || !model) throw usageError(`Error: --provider-model must use provider=model (received ${assignment})`);
14242
- if (provider in result) throw usageError(`Error: duplicate --provider-model assignment for ${provider}`);
14243
- result[provider] = model;
14314
+ async function createProject(name, opts) {
14315
+ const client = getClient23();
14316
+ const result = await client.putProject(name, {
14317
+ displayName: opts.displayName,
14318
+ canonicalDomain: opts.domain,
14319
+ ownedDomains: opts.ownedDomains ?? [],
14320
+ aliases: normalizeProjectAliases(opts.displayName, opts.aliases ?? []),
14321
+ ...opts.qualifiedAliases?.length ? { qualifiedAliases: opts.qualifiedAliases } : {},
14322
+ country: opts.country,
14323
+ language: opts.language,
14324
+ providers: opts.providers ?? [],
14325
+ providerModels: opts.providerModels ?? {},
14326
+ ...opts.providerDispatchModes && Object.keys(opts.providerDispatchModes).length > 0 ? { providerDispatchModes: opts.providerDispatchModes } : {},
14327
+ // Sent only when given: this PUT also re-runs over an existing project,
14328
+ // and an omitted budget keeps the stored one.
14329
+ ...opts.siteAuditMaxPages !== void 0 ? { siteAuditMaxPages: opts.siteAuditMaxPages } : {}
14330
+ });
14331
+ warnDroppedDispatchModes(opts.providerDispatchModes ?? {}, result);
14332
+ if (isMachineFormat(opts.format)) {
14333
+ console.log(JSON.stringify(result, null, 2));
14334
+ return;
14244
14335
  }
14245
- return result;
14336
+ console.log(`Project created: ${result.name} (${result.id})`);
14246
14337
  }
14247
- function parseNegativeReviewMaxStars(raw) {
14248
- if (raw === void 0) return void 0;
14249
- if (raw === "default") return null;
14250
- const parsed = gbpNegativeReviewMaxStarsSchema.safeParse(Number(raw));
14251
- if (!parsed.success) {
14252
- throw usageError('Error: --negative-review-max-stars must be 1, 2, 3, 4, or "default" (3)', {
14253
- message: '--negative-review-max-stars must be 1-4 or "default"',
14254
- details: { command: "project.update", value: raw }
14255
- });
14338
+ function warnDroppedDispatchModes(sent, result) {
14339
+ const kept = result.providerDispatchModes ?? {};
14340
+ const dropped = Object.keys(sent).filter((provider) => !(provider in kept));
14341
+ if (dropped.length > 0) {
14342
+ console.error(`Warning: Dropped dispatch mode for engine(s) the project does not measure: ${dropped.join(", ")}`);
14256
14343
  }
14257
- return parsed.data;
14258
14344
  }
14259
- function parseSiteAuditMaxPages(raw, command) {
14260
- if (raw === void 0) return void 0;
14261
- const value = raw.trim();
14262
- if (value.toLowerCase() === "full") return null;
14263
- const parsed = /^\d+$/.test(value) ? siteAuditPageBudgetSchema.safeParse(Number(value)) : void 0;
14264
- if (!parsed?.success) {
14265
- throw usageError(`Error: --site-audit-max-pages must be a whole number of pages from 1 to ${SITE_AUDIT_MAX_PAGE_LIMIT}, or "full" for the full site`, {
14266
- message: `--site-audit-max-pages must be 1-${SITE_AUDIT_MAX_PAGE_LIMIT} or "full"`,
14267
- details: { command, option: "site-audit-max-pages", value: raw }
14268
- });
14345
+ async function listProjects(format) {
14346
+ const client = getClient23();
14347
+ const projects2 = await client.listProjects();
14348
+ if (format === "json") {
14349
+ console.log(JSON.stringify(projects2, null, 2));
14350
+ return;
14269
14351
  }
14270
- return parsed.data;
14271
- }
14272
- function parseCompetitorAutoAliasMode(raw) {
14273
- if (raw === void 0) return void 0;
14274
- const parsed = competitorAutoAliasModeSchema.safeParse(raw.trim().toLowerCase());
14275
- if (!parsed.success) {
14276
- throw usageError("Error: --competitor-auto-aliases must be off, preview or apply", {
14277
- message: "--competitor-auto-aliases must be off, preview or apply",
14278
- details: { command: "project.update", option: "competitor-auto-aliases", value: raw }
14279
- });
14352
+ if (format === "jsonl") {
14353
+ emitJsonl(projects2);
14354
+ return;
14280
14355
  }
14281
- return parsed.data;
14282
- }
14283
-
14284
- // src/commands/organic-evidence.ts
14285
- async function showOrganicEvidence(project2, opts) {
14286
- const data = await createApiClient().getOrganicEvidence(project2, opts.period);
14287
- if (isMachineFormat(opts.format)) {
14288
- console.log(JSON.stringify(data, null, 2));
14356
+ if (projects2.length === 0) {
14357
+ console.log("No projects found.");
14289
14358
  return;
14290
14359
  }
14291
- printOrganicEvidence(project2, data);
14360
+ console.log("Projects:\n");
14361
+ const nameWidth = Math.max(4, ...projects2.map((p) => p.name.length));
14362
+ const domainLabel = (p) => {
14363
+ const extra = Math.max(0, effectiveDomains(p).length - 1);
14364
+ return extra > 0 ? `${p.canonicalDomain} (+${extra})` : p.canonicalDomain;
14365
+ };
14366
+ const domainWidth = Math.max(6, ...projects2.map((p) => domainLabel(p).length));
14367
+ console.log(
14368
+ ` ${"NAME".padEnd(nameWidth)} ${"DOMAIN".padEnd(domainWidth)} COUNTRY LANGUAGE`
14369
+ );
14370
+ console.log(` ${"\u2500".repeat(nameWidth)} ${"\u2500".repeat(domainWidth)} \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
14371
+ for (const p of projects2) {
14372
+ console.log(
14373
+ ` ${p.name.padEnd(nameWidth)} ${domainLabel(p).padEnd(domainWidth)} ${p.country.padEnd(7)} ${p.language}`
14374
+ );
14375
+ }
14292
14376
  }
14293
- function formatCounts(clicks, impressions) {
14294
- return `${clicks} clicks / ${impressions} impressions`;
14377
+ function storedQualifiedAliases(project2) {
14378
+ return project2.qualifiedAliases ?? [];
14295
14379
  }
14296
- function printOrganicEvidence(project2, data) {
14297
- const asOf = data.asOfDate ? ` through ${data.asOfDate}` : "";
14298
- console.log(`Organic evidence: ${project2} (${data.periodDays} days${asOf})`);
14299
- console.log(`Coverage: GSC ${data.coverage.gsc ? "yes" : "no"} \xB7 GA4 ${data.coverage.ga4 ? "yes" : "no"} \xB7 server ${data.coverage.server ? "yes" : "no"} \xB7 AI visibility ${data.coverage.visibility ? "yes" : "no"}`);
14300
- if (data.gsc) {
14301
- console.log(`Google Search: ${formatCounts(data.gsc.propertyTotals.clicks, data.gsc.propertyTotals.impressions)} (named non-brand: ${formatCounts(data.gsc.namedNonBrand.clicks, data.gsc.namedNonBrand.impressions)})`);
14380
+ async function showProject(name, format) {
14381
+ const client = getClient23();
14382
+ const project2 = await client.getProject(name);
14383
+ if (isMachineFormat(format)) {
14384
+ console.log(JSON.stringify(project2, null, 2));
14385
+ return;
14302
14386
  }
14303
- if (data.ga4) console.log(`GA4 organic: ${data.ga4.organicSessions} sessions`);
14304
- if (data.gaAiReferrals) console.log(`GA4 AI referrals: ${data.gaAiReferrals.organicSessions} organic, ${data.gaAiReferrals.paidSessions} paid sessions`);
14305
- if (data.server) {
14306
- const s = data.server;
14307
- const fetchTotal = s.userFetchHits.verified + s.userFetchHits.claimedUnverified + s.userFetchHits.unknownAiLike;
14308
- console.log(`Server AI: ${s.crawlerHits.verified} verified + ${s.crawlerHits.claimedUnverified} claimed + ${s.crawlerHits.unknownAiLike} heuristic crawls; ${fetchTotal} user-agent fetches (${s.userFetchHits.verified} verified, ${s.userFetchHits.claimedUnverified} claimed, ${s.userFetchHits.unknownAiLike} heuristic); ${s.referralSessions.organic} organic referrals, ${s.referralSessions.paid} paid, ${s.referralSessions.unknown} unclassified`);
14387
+ console.log(`Project: ${project2.displayName ?? project2.name}
14388
+ `);
14389
+ console.log(` Name: ${project2.name}`);
14390
+ console.log(` ID: ${project2.id}`);
14391
+ console.log(` Domain: ${project2.canonicalDomain}`);
14392
+ const secondaryDomains = effectiveDomains(project2).slice(1);
14393
+ if (secondaryDomains.length > 0) {
14394
+ console.log(` Owned domains: ${secondaryDomains.join(", ")}`);
14309
14395
  }
14310
- if (data.visibility) {
14311
- const v = data.visibility;
14312
- console.log(`Latest AI visibility: ${v.mentionedPairs}/${v.answerPairs} mentioned, ${v.citedPairs}/${v.answerPairs} cited (${Math.floor(v.ageDays)}d old)`);
14396
+ if (project2.aliases && project2.aliases.length > 0) {
14397
+ console.log(` Aliases: ${project2.aliases.join(", ")}`);
14313
14398
  }
14314
- if (data.findings.length > 0) {
14315
- console.log("\nFindings:");
14316
- for (const finding of data.findings) console.log(`- ${finding.title}: ${finding.detail}`);
14399
+ const qualifiedAliases = storedQualifiedAliases(project2);
14400
+ if (qualifiedAliases.length > 0) {
14401
+ console.log(` Qualified: ${qualifiedAliases.join(", ")} (sentiment)`);
14317
14402
  }
14318
- if (data.limitations.length > 0) {
14319
- console.log("\nLimitations:");
14320
- for (const limitation of data.limitations) console.log(`- ${limitation.detail}`);
14403
+ console.log(` Country: ${project2.country}`);
14404
+ console.log(` Language: ${project2.language}`);
14405
+ console.log(` Config source: ${project2.configSource}`);
14406
+ console.log(` Config revision: ${project2.configRevision}`);
14407
+ console.log(` Providers: ${(project2.providers ?? []).length > 0 ? project2.providers.join(", ") : "all configured"}`);
14408
+ const providerModels = Object.entries(project2.providerModels ?? {});
14409
+ console.log(` Model overrides: ${providerModels.length > 0 ? providerModels.map(([provider, model]) => `${provider}=${model}`).join(", ") : "(none; instance settings inherited)"}`);
14410
+ const dispatchModes = Object.entries(project2.providerDispatchModes ?? {}).sort(([left], [right]) => left.localeCompare(right));
14411
+ console.log(` Dispatch modes: ${dispatchModes.length > 0 ? `${dispatchModes.map(([provider, mode]) => `${provider}=${mode}`).join(", ")} (scheduled sweeps; others sync)` : "(none; every provider runs sync)"}`);
14412
+ console.log(` Negative reviews: ${resolveNegativeReviewMaxStars(project2.negativeReviewMaxStars)} stars or fewer${project2.negativeReviewMaxStars == null ? " (default)" : ""}`);
14413
+ if (project2.siteAuditMaxPages !== void 0) {
14414
+ console.log(` Site Health page budget: ${formatSiteAuditPageBudget(project2.siteAuditMaxPages)}`);
14321
14415
  }
14322
- }
14323
-
14324
- // src/cli-commands/organic-evidence.ts
14325
- var USAGE3 = "canonry organic-evidence <project> [--period 60|90] [--format json|jsonl]";
14326
- function parsePeriod(value) {
14327
- if (value === void 0) return void 0;
14328
- if (value === "60" || value === "90") return Number(value);
14329
- throw usageError(`Error: --period must be 60 or 90
14330
-
14331
- Usage: ${USAGE3}`);
14332
- }
14333
- var ORGANIC_EVIDENCE_CLI_COMMANDS = [
14334
- {
14335
- path: ["organic-evidence"],
14336
- usage: USAGE3,
14337
- options: { period: stringOption() },
14338
- run: async (input) => {
14339
- const project2 = requireProject(input, "organic-evidence", USAGE3);
14340
- await showOrganicEvidence(project2, {
14341
- period: parsePeriod(getString(input.values, "period")),
14342
- format: input.format
14343
- });
14344
- }
14416
+ if (project2.competitorAutoAliases !== void 0) {
14417
+ console.log(` Competitor auto aliases: ${formatCompetitorAutoAliasMode(project2.competitorAutoAliases)}`);
14345
14418
  }
14346
- ];
14347
-
14348
- // src/commands/run.ts
14349
- function getClient23() {
14350
- return createApiClient();
14419
+ console.log(` Tags: ${project2.tags.length > 0 ? project2.tags.join(", ") : "(none)"}`);
14420
+ const labelEntries = Object.entries(project2.labels);
14421
+ console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "(none)"}`);
14422
+ if (project2.createdAt) console.log(` Created: ${project2.createdAt}`);
14423
+ if (project2.updatedAt) console.log(` Updated: ${project2.updatedAt}`);
14351
14424
  }
14352
- var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
14353
- function throwIfWaitedRunFailed(waited) {
14354
- const failed = waited.filter((r) => r.status === RunStatuses.failed);
14355
- if (failed.length === 0) return;
14356
- const reason = (r) => r.error ? formatRunErrorOneLine(r.error) : null;
14357
- const label = (r) => [r.project, r.location ? `(${r.location})` : null, `run ${r.runId}`].filter(Boolean).join(" ");
14358
- 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("; ")}`;
14359
- throw new CliError({
14360
- code: "RUN_FAILED",
14361
- message,
14362
- exitCode: EXIT_SYSTEM_ERROR,
14363
- details: {
14364
- waitedRunCount: waited.length,
14365
- failedRuns: failed.map((r) => ({
14366
- runId: r.runId,
14367
- ...r.project ? { project: r.project } : {},
14368
- ...r.location ? { location: r.location } : {},
14369
- error: reason(r)
14370
- }))
14371
- }
14372
- });
14425
+ function formatCompetitorAutoAliasMode(mode) {
14426
+ if (mode === "apply") return "apply (names detected after each sweep are stored and counted)";
14427
+ if (mode === "off") return "off (no detection after sweeps)";
14428
+ return "preview (detected after each sweep and logged; nothing stored)";
14373
14429
  }
14374
- async function triggerRun(project2, opts) {
14430
+ async function updateProjectSettings(name, opts) {
14375
14431
  const client = getClient23();
14376
- const body = {};
14377
- if (opts?.provider) {
14378
- const providerInputs = opts.provider.split(",").map((s) => s.trim()).filter(Boolean);
14379
- const resolved = providerInputs.flatMap((p) => resolveProviderInput(p));
14380
- body.providers = resolved.length > 0 ? resolved : providerInputs;
14432
+ const project2 = await client.getProject(name);
14433
+ let ownedDomains = opts.ownedDomains ?? project2.ownedDomains ?? [];
14434
+ if (opts.addOwnedDomain) {
14435
+ const toAdd = opts.addOwnedDomain.filter((d) => !ownedDomains.includes(d));
14436
+ ownedDomains = [...ownedDomains, ...toAdd];
14381
14437
  }
14382
- if (opts?.queries?.length) {
14383
- body.queries = opts.queries;
14438
+ if (opts.removeOwnedDomain) {
14439
+ const toRemove = new Set(opts.removeOwnedDomain);
14440
+ ownedDomains = ownedDomains.filter((d) => !toRemove.has(d));
14384
14441
  }
14385
- if (opts?.groups?.length || opts?.targets?.length) {
14386
- body.measurementScope = {
14387
- ...opts.groups?.length ? { groups: opts.groups } : {},
14388
- ...opts.targets?.length ? { targets: opts.targets } : {}
14389
- };
14442
+ const nextDisplayName = opts.displayName ?? project2.displayName ?? project2.name;
14443
+ let aliases = opts.aliases ?? project2.aliases ?? [];
14444
+ if (opts.addAlias) {
14445
+ const existingKeys = new Set(aliases.map((a) => a.toLowerCase()));
14446
+ const toAdd = opts.addAlias.filter((a) => !existingKeys.has(a.toLowerCase()));
14447
+ aliases = [...aliases, ...toAdd];
14390
14448
  }
14391
- if (opts?.location) {
14392
- body.location = opts.location;
14449
+ if (opts.removeAlias) {
14450
+ const toRemove = new Set(opts.removeAlias.map((a) => a.toLowerCase()));
14451
+ aliases = aliases.filter((a) => !toRemove.has(a.toLowerCase()));
14393
14452
  }
14394
- if (opts?.allLocations) {
14395
- body.allLocations = true;
14453
+ const nextAliases = normalizeProjectAliases(nextDisplayName, aliases);
14454
+ const touchesQualified = (opts.addQualifiedAlias?.length ?? 0) > 0 || (opts.removeQualifiedAlias?.length ?? 0) > 0;
14455
+ let qualifiedAliases = resolveProjectQualifiedAliases(
14456
+ { displayName: nextDisplayName, aliases: nextAliases },
14457
+ storedQualifiedAliases(project2)
14458
+ ).value;
14459
+ if (opts.addQualifiedAlias) {
14460
+ const existingKeys = new Set(qualifiedAliases.map((alias) => alias.toLowerCase()));
14461
+ for (const alias of opts.addQualifiedAlias) {
14462
+ if (existingKeys.has(alias.toLowerCase())) continue;
14463
+ existingKeys.add(alias.toLowerCase());
14464
+ qualifiedAliases = [...qualifiedAliases, alias];
14465
+ }
14396
14466
  }
14397
- if (opts?.noLocation) {
14398
- body.noLocation = true;
14467
+ if (opts.removeQualifiedAlias) {
14468
+ const toRemove = new Set(opts.removeQualifiedAlias.map((alias) => alias.toLowerCase()));
14469
+ qualifiedAliases = qualifiedAliases.filter((alias) => !toRemove.has(alias.toLowerCase()));
14399
14470
  }
14400
- if (opts?.probe) {
14401
- body.trigger = "probe";
14471
+ const providerModels = { ...project2.providerModels ?? {}, ...opts.providerModels ?? {} };
14472
+ for (const provider of opts.clearProviderModels ?? []) delete providerModels[provider];
14473
+ const nextProviders = opts.providers ?? project2.providers ?? [];
14474
+ if (nextProviders.length > 0) {
14475
+ const requested = Object.keys(opts.providerModels ?? {}).filter((p) => !nextProviders.includes(p));
14476
+ if (requested.length > 0) {
14477
+ throw usageError(
14478
+ `Error: --provider-model set for ${requested.join(", ")}, which ${requested.length > 1 ? "are" : "is"} not a selected engine for "${name}"`,
14479
+ {
14480
+ message: "provider-model set for a provider the project does not run",
14481
+ details: { command: "project.update", project: name, providers: nextProviders, unselected: requested }
14482
+ }
14483
+ );
14484
+ }
14402
14485
  }
14403
- if (opts?.dispatchMode) {
14404
- body.dispatchMode = opts.dispatchMode;
14486
+ const touchesDispatch = Object.keys(opts.dispatchModes ?? {}).length > 0 || (opts.clearDispatchModes ?? []).length > 0;
14487
+ const providerDispatchModes = { ...project2.providerDispatchModes ?? {}, ...opts.dispatchModes ?? {} };
14488
+ for (const provider of opts.clearDispatchModes ?? []) delete providerDispatchModes[provider];
14489
+ const result = await client.putProject(name, {
14490
+ displayName: nextDisplayName,
14491
+ canonicalDomain: opts.domain ?? project2.canonicalDomain,
14492
+ ownedDomains,
14493
+ aliases: nextAliases,
14494
+ ...touchesQualified ? { qualifiedAliases } : {},
14495
+ country: opts.country ?? project2.country,
14496
+ language: opts.language ?? project2.language,
14497
+ tags: project2.tags,
14498
+ labels: project2.labels,
14499
+ providers: opts.providers ?? project2.providers,
14500
+ providerModels,
14501
+ ...touchesDispatch ? { providerDispatchModes } : {},
14502
+ locations: project2.locations,
14503
+ defaultLocation: project2.defaultLocation,
14504
+ autoExtractBacklinks: project2.autoExtractBacklinks,
14505
+ // Sent only when asked for: the server keeps the stored threshold when the
14506
+ // field is absent, so a stale read cannot overwrite a newer setting.
14507
+ ...opts.negativeReviewMaxStars !== void 0 ? { negativeReviewMaxStars: opts.negativeReviewMaxStars } : {},
14508
+ // Same for the page budget: never echoed from the read above, so an edit
14509
+ // that does not touch it cannot overwrite a newer one or send a stray null.
14510
+ ...opts.siteAuditMaxPages !== void 0 ? { siteAuditMaxPages: opts.siteAuditMaxPages } : {},
14511
+ // Same for the detection mode.
14512
+ ...opts.competitorAutoAliases !== void 0 ? { competitorAutoAliases: opts.competitorAutoAliases } : {}
14513
+ });
14514
+ warnDroppedDispatchModes(touchesDispatch ? providerDispatchModes : project2.providerDispatchModes ?? {}, result);
14515
+ if (isMachineFormat(opts.format)) {
14516
+ console.log(JSON.stringify(result, null, 2));
14517
+ return;
14405
14518
  }
14406
- if (opts?.force) {
14407
- body.force = true;
14519
+ console.log(`Project updated: ${result.name}`);
14520
+ const dropped = Object.keys(providerModels).filter((provider) => !(provider in result.providerModels));
14521
+ if (dropped.length > 0) {
14522
+ console.log(` Dropped model override for deselected engine(s): ${dropped.join(", ")}`);
14408
14523
  }
14409
- const response = await client.triggerRun(project2, body);
14410
- if (Array.isArray(response)) {
14411
- const locationRuns = response;
14412
- if (isMachineFormat(opts?.format)) {
14413
- if (opts?.wait) {
14414
- const finals = await Promise.all(
14415
- locationRuns.map(async (r) => !r.id || r.status === "conflict" ? null : pollRun2(client, r.id, false))
14416
- );
14417
- console.log(JSON.stringify(locationRuns.map((r, i) => finals[i] ? { ...r, ...finals[i] } : r), null, 2));
14418
- throwIfWaitedRunFailed(locationRuns.flatMap((r, i) => {
14419
- const final = finals[i];
14420
- return final ? [{ runId: final.id, status: final.status, project: project2, location: r.location ?? null, error: final.error }] : [];
14421
- }));
14422
- } else {
14423
- console.log(JSON.stringify(locationRuns, null, 2));
14424
- }
14524
+ }
14525
+ async function deleteProject(name, opts) {
14526
+ const client = getClient23();
14527
+ const isJson = isMachineFormat(opts?.format);
14528
+ if (opts?.dryRun) {
14529
+ const preview = await client.previewProjectDelete(name);
14530
+ if (isJson) {
14531
+ console.log(JSON.stringify({ dryRun: true, ...preview }, null, 2));
14425
14532
  return;
14426
14533
  }
14427
- console.log(`Triggered ${locationRuns.length} location sweep(s) \u2014 ${locationRuns.length}\xD7 API calls:
14428
- `);
14429
- console.log(" LOCATION RUN ID STATUS");
14430
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14431
- for (const r of locationRuns) {
14432
- const loc = (r.location ?? "(unknown)").padEnd(15);
14433
- const id = (r.id ?? "(conflict)").padEnd(36);
14434
- console.log(` ${loc} ${id} ${r.status}`);
14435
- }
14436
- if (opts?.wait) {
14437
- const pending = locationRuns.filter((r) => r.id && r.status !== "conflict" && !TERMINAL_STATUSES.has(r.status));
14438
- const errors = /* @__PURE__ */ new Map();
14439
- if (pending.length > 0) {
14440
- process.stderr.write(`Waiting for ${pending.length} run(s)`);
14441
- const batchWaits = /* @__PURE__ */ new Map();
14442
- await Promise.all(
14443
- pending.map(async (r) => {
14444
- const final = await pollRun2(client, r.id);
14445
- r.status = final.status;
14446
- errors.set(r.id, final.error);
14447
- const waiting = batchWaitLine(final);
14448
- if (waiting) batchWaits.set(r.id, waiting);
14449
- })
14450
- );
14451
- process.stderr.write("\n");
14452
- console.log("\nFinal statuses:");
14453
- for (const r of locationRuns) {
14454
- const loc = (r.location ?? "(unknown)").padEnd(15);
14455
- console.log(` ${loc} ${r.status}`);
14456
- }
14457
- if (batchWaits.size > 0) console.log("");
14458
- for (const r of locationRuns) {
14459
- const waiting = batchWaits.get(r.id);
14460
- if (waiting) console.log(` ${r.location ?? "(unknown)"}: ${waiting}`);
14461
- }
14462
- }
14463
- throwIfWaitedRunFailed(locationRuns.filter((r) => r.id && r.status !== "conflict").map((r) => ({ runId: r.id, status: r.status, project: project2, location: r.location ?? null, error: errors.get(r.id) })));
14464
- }
14534
+ const { cascadeRows: cr, detachedRows: dr } = preview;
14535
+ console.log(`Project delete preview for "${name}":`);
14536
+ console.log(` Cascade-deletes:`);
14537
+ console.log(` queries: ${cr.queries}`);
14538
+ console.log(` competitors: ${cr.competitors}`);
14539
+ console.log(` runs: ${cr.runs}`);
14540
+ console.log(` snapshots: ${cr.snapshots}`);
14541
+ console.log(` insights: ${cr.insights}`);
14542
+ console.log(` Detached (project_id set to NULL):`);
14543
+ console.log(` audit_log: ${dr.auditLog}`);
14544
+ console.log(``);
14545
+ console.log(`No DB writes performed. Re-run without --dry-run to delete.`);
14465
14546
  return;
14466
14547
  }
14467
- const run = response;
14468
- if (opts?.wait && run.id && !TERMINAL_STATUSES.has(run.status)) {
14469
- const showProgress = !isMachineFormat(opts?.format);
14470
- if (showProgress) process.stderr.write(`Run ${run.id} started`);
14471
- const result = await pollRun2(client, run.id, showProgress);
14472
- if (isMachineFormat(opts?.format)) {
14473
- console.log(JSON.stringify(result, null, 2));
14474
- } else {
14475
- process.stderr.write("\n");
14476
- printRunDetail(result);
14477
- const waiting = batchWaitLine(result);
14478
- if (waiting) console.log(`
14479
- ${waiting}`);
14480
- }
14481
- throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project: project2, error: result.error }]);
14548
+ await client.deleteProject(name);
14549
+ if (isJson) {
14550
+ console.log(JSON.stringify({ name, deleted: true }, null, 2));
14482
14551
  return;
14483
14552
  }
14484
- if (opts?.wait && (TERMINAL_STATUSES.has(run.status) || !run.id)) {
14485
- const result = run.id ? await client.getRun(run.id) : run;
14486
- if (isMachineFormat(opts?.format)) {
14487
- console.log(JSON.stringify(result, null, 2));
14488
- } else {
14489
- printRunDetail(result);
14490
- }
14491
- throwIfWaitedRunFailed([{ runId: result.id, status: result.status, project: project2, error: result.error }]);
14553
+ console.log(`Project deleted: ${name}`);
14554
+ }
14555
+ async function addLocation(project2, opts) {
14556
+ const client = getClient23();
14557
+ const location = await client.addLocation(project2, {
14558
+ label: opts.label,
14559
+ city: opts.city,
14560
+ region: opts.region,
14561
+ country: opts.country,
14562
+ timezone: opts.timezone
14563
+ });
14564
+ if (isMachineFormat(opts.format)) {
14565
+ console.log(JSON.stringify(location, null, 2));
14492
14566
  return;
14493
14567
  }
14494
- if (isMachineFormat(opts?.format)) {
14495
- console.log(JSON.stringify(run, null, 2));
14568
+ console.log(`Location added: ${opts.label} (${opts.city}, ${opts.region}, ${opts.country})`);
14569
+ }
14570
+ async function listLocations(project2, format) {
14571
+ const client = getClient23();
14572
+ const result = await client.listLocations(project2);
14573
+ if (format === "json") {
14574
+ console.log(JSON.stringify(result, null, 2));
14496
14575
  return;
14497
14576
  }
14498
- console.log(`Run created: ${run.id}`);
14499
- console.log(` Kind: ${run.kind}`);
14500
- console.log(` Status: ${run.status}`);
14501
- if (opts?.provider) {
14502
- console.log(` Provider: ${opts.provider}`);
14503
- }
14504
- }
14505
- async function triggerRunAll(opts) {
14506
- const client = getClient23();
14507
- const projects2 = await client.listProjects();
14508
- if (projects2.length === 0) {
14509
- if (isMachineFormat(opts?.format)) {
14510
- console.log("[]");
14511
- } else {
14512
- console.log("No projects found.");
14513
- }
14577
+ if (format === "jsonl") {
14578
+ emitJsonl(result.locations.map((loc) => ({
14579
+ project: project2,
14580
+ isDefault: loc.label === result.defaultLocation,
14581
+ ...loc
14582
+ })));
14514
14583
  return;
14515
14584
  }
14516
- const baseBody = {};
14517
- if (opts?.provider) {
14518
- const providerInputs = opts.provider.split(",").map((s) => s.trim()).filter(Boolean);
14519
- const resolved = providerInputs.flatMap((p) => resolveProviderInput(p));
14520
- baseBody.providers = resolved.length > 0 ? resolved : providerInputs;
14521
- }
14522
- if (opts?.allLocations) {
14523
- baseBody.allLocations = true;
14524
- }
14525
- if (opts?.noLocation) {
14526
- baseBody.noLocation = true;
14527
- }
14528
- if (opts?.dispatchMode) {
14529
- baseBody.dispatchMode = opts.dispatchMode;
14530
- }
14531
- if (opts?.force) {
14532
- baseBody.force = true;
14533
- }
14534
- const results = [];
14535
- for (const p of projects2) {
14536
- const body = { ...baseBody };
14537
- if (body.allLocations && p.locations.length === 0) {
14538
- delete body.allLocations;
14539
- }
14540
- try {
14541
- const response = await client.triggerRun(p.name, body);
14542
- const dispatched = Array.isArray(response) ? response : [response];
14543
- for (const r of dispatched) {
14544
- results.push({
14545
- project: p.name,
14546
- runId: r.id,
14547
- status: r.status,
14548
- location: r.location ?? null
14549
- });
14550
- }
14551
- } catch (err) {
14552
- const msg = describeError(err);
14553
- results.push({ project: p.name, runId: "", status: "error", location: null, error: msg });
14554
- }
14555
- }
14556
- const errors = /* @__PURE__ */ new Map();
14557
- const batchWaits = /* @__PURE__ */ new Map();
14558
- if (opts?.wait) {
14559
- const pending = results.filter((r) => r.runId && !TERMINAL_STATUSES.has(r.status));
14560
- if (pending.length > 0) {
14561
- const showProgress = !isMachineFormat(opts?.format);
14562
- if (showProgress) process.stderr.write(`Waiting for ${pending.length} run(s)`);
14563
- await Promise.all(pending.map(async (r) => {
14564
- const final = await pollRun2(client, r.runId, showProgress);
14565
- r.status = final.status;
14566
- errors.set(r.runId, final.error);
14567
- const waiting = batchWaitLine(final);
14568
- if (waiting) batchWaits.set(r.runId, waiting);
14569
- }));
14570
- if (showProgress) process.stderr.write("\n");
14571
- }
14572
- }
14573
- 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) }));
14574
- if (isMachineFormat(opts?.format)) {
14575
- console.log(JSON.stringify(results, null, 2));
14576
- if (opts?.wait) throwIfWaitedRunFailed(waited);
14585
+ if (result.locations.length === 0) {
14586
+ console.log(`No locations configured for "${project2}".`);
14577
14587
  return;
14578
14588
  }
14579
- const showLocationColumn = results.some((r) => r.location !== null);
14580
- const projectCount = new Set(results.map((r) => r.project)).size;
14581
- console.log(`Triggered ${results.length} run(s) across ${projectCount} project(s):
14589
+ console.log(`Locations for "${project2}" (${result.locations.length}):
14582
14590
  `);
14583
- if (showLocationColumn) {
14584
- console.log(" PROJECT LOCATION RUN ID STATUS");
14585
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14586
- for (const r of results) {
14587
- const proj = r.project.padEnd(31);
14588
- const loc = (r.location ?? "\u2014").padEnd(15);
14589
- const id = (r.runId || "(failed)").padEnd(36);
14590
- console.log(` ${proj} ${loc} ${id} ${r.status}`);
14591
- }
14592
- } else {
14593
- console.log(" PROJECT RUN ID STATUS");
14594
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14595
- for (const r of results) {
14596
- const proj = r.project.padEnd(31);
14597
- const id = (r.runId || "(failed)").padEnd(36);
14598
- console.log(` ${proj} ${id} ${r.status}`);
14599
- }
14591
+ console.log(" LABEL CITY REGION COUNTRY DEFAULT");
14592
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14593
+ for (const loc of result.locations) {
14594
+ const isDefault = loc.label === result.defaultLocation ? " *" : "";
14595
+ console.log(
14596
+ ` ${loc.label.padEnd(15)} ${loc.city.padEnd(19)} ${loc.region.padEnd(19)} ${loc.country.padEnd(7)}${isDefault}`
14597
+ );
14600
14598
  }
14601
- if (batchWaits.size > 0) console.log("");
14602
- for (const r of results) {
14603
- const waiting = batchWaits.get(r.runId);
14604
- if (waiting) console.log(` ${r.location ? `${r.project} (${r.location})` : r.project}: ${waiting}`);
14599
+ if (result.defaultLocation) {
14600
+ console.log(`
14601
+ Default: ${result.defaultLocation}`);
14605
14602
  }
14606
- if (opts?.wait) throwIfWaitedRunFailed(waited);
14607
14603
  }
14608
- async function cancelRun(project2, runId, format) {
14604
+ async function removeLocation(project2, label, format) {
14609
14605
  const client = getClient23();
14610
- let targetId = runId;
14611
- if (!targetId) {
14612
- const runs2 = await client.listRuns(project2);
14613
- const activeRuns = runs2.filter((r) => r.status === "queued" || r.status === "running");
14614
- if (activeRuns.length > 1) {
14615
- const candidates = activeRuns.map(({ id, kind, status }) => ({ id, kind, status }));
14616
- throw new CliError({
14617
- code: "MULTIPLE_ACTIVE_RUNS",
14618
- message: `Multiple active runs found for project "${project2}". Specify a run ID.`,
14619
- displayMessage: `Error: Multiple active runs found for project "${project2}". Specify a run ID.
14620
- ` + candidates.map((r) => ` ${r.id} ${r.kind} ${r.status}`).join("\n") + `
14621
- To cancel by ID: canonry run cancel ${project2} <run-id>`,
14622
- details: {
14623
- project: project2,
14624
- activeRuns: candidates,
14625
- suggestedCommands: [`canonry run cancel ${project2} <run-id>`]
14626
- }
14627
- });
14628
- }
14629
- const active = activeRuns[0];
14630
- if (!active) {
14631
- throw new CliError({
14632
- code: "NO_ACTIVE_RUN",
14633
- message: `No active run found for project "${project2}"`,
14634
- displayMessage: `Error: canonry run cancel "${project2}" \u2014 no active run found (status must be queued or running).
14635
- Check run status : canonry status ${project2}
14636
- To cancel by ID : canonry run cancel ${project2} <run-id>`,
14637
- details: {
14638
- project: project2,
14639
- allowedStatuses: ["queued", "running"],
14640
- suggestedCommands: [
14641
- `canonry status ${project2}`,
14642
- `canonry run cancel ${project2} <run-id>`
14643
- ]
14644
- }
14645
- });
14646
- }
14647
- targetId = active.id;
14648
- }
14649
- const result = await client.cancelRun(targetId);
14606
+ await client.removeLocation(project2, label);
14650
14607
  if (isMachineFormat(format)) {
14651
- console.log(JSON.stringify(result, null, 2));
14608
+ console.log(JSON.stringify({ project: project2, label, removed: true }, null, 2));
14652
14609
  return;
14653
14610
  }
14654
- console.log(`Run ${result.id} cancelled.`);
14611
+ console.log(`Location removed: ${label}`);
14655
14612
  }
14656
- async function showRun(id, format) {
14613
+ async function setDefaultLocation(project2, label, format) {
14657
14614
  const client = getClient23();
14658
- const run = await client.getRun(id);
14615
+ const result = await client.setDefaultLocation(project2, label);
14659
14616
  if (isMachineFormat(format)) {
14660
- console.log(JSON.stringify(run, null, 2));
14661
- return;
14662
- }
14663
- printRunDetail(run);
14664
- }
14665
- async function listRuns(project2, opts) {
14666
- const client = getClient23();
14667
- const runs2 = await client.listRuns(project2, opts?.limit, opts?.kind, opts?.status, opts?.excludeKind, opts?.excludeKinds);
14668
- if (opts?.format === "json") {
14669
- console.log(JSON.stringify(runs2, null, 2));
14670
- return;
14671
- } else if (opts?.format === "jsonl") {
14672
- emitJsonl(runs2.map((run) => ({ project: project2, ...run })));
14673
- return;
14674
- }
14675
- if (runs2.length === 0) {
14676
- console.log(`No runs found for "${project2}".`);
14617
+ console.log(JSON.stringify({ project: project2, ...result }, null, 2));
14677
14618
  return;
14678
14619
  }
14679
- console.log(`Runs for "${project2}" (${runs2.length}):
14680
- `);
14681
- console.log(" ID STATUS KIND TRIGGER CREATED");
14682
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
14683
- for (const run of runs2) {
14684
- console.log(
14685
- ` ${run.id} ${run.status.padEnd(10)} ${run.kind.padEnd(18)} ${run.trigger.padEnd(9)} ${run.createdAt}`
14686
- );
14687
- }
14620
+ console.log(`Default location set to: ${label}`);
14688
14621
  }
14689
- var POLL_TIMEOUT_MS3 = 10 * 60 * 1e3;
14690
- async function pollRun2(client, runId, showProgress = true) {
14691
- const deadline = Date.now() + POLL_TIMEOUT_MS3;
14692
- for (; ; ) {
14693
- await new Promise((r) => setTimeout(r, 2e3));
14694
- if (Date.now() > deadline) {
14695
- throw new Error(`Timed out waiting for run ${runId} after ${POLL_TIMEOUT_MS3 / 1e3}s`);
14622
+
14623
+ // src/cli-commands/project.ts
14624
+ var PROJECT_CLI_COMMANDS = [
14625
+ {
14626
+ path: ["project", "create"],
14627
+ usage: "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--dispatch-mode provider=sync|batch...] [--site-audit-max-pages <1-50000|full>] [--format json]",
14628
+ help: "Create a project. Pass --domain for the public site to scan. Provider credentials are not required for Page Health.",
14629
+ options: {
14630
+ domain: { type: "string", short: "d" },
14631
+ "owned-domain": multiStringOption(),
14632
+ alias: multiStringOption(),
14633
+ "qualified-alias": multiStringOption(),
14634
+ country: stringOption(),
14635
+ language: stringOption(),
14636
+ "display-name": stringOption(),
14637
+ provider: multiStringOption(),
14638
+ "provider-model": multiStringOption(),
14639
+ "dispatch-mode": multiStringOption(),
14640
+ "site-audit-max-pages": stringOption()
14641
+ },
14642
+ run: async (input) => {
14643
+ const name = requireProject(
14644
+ input,
14645
+ "project.create",
14646
+ "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--qualified-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--dispatch-mode provider=sync|batch...] [--site-audit-max-pages <1-50000|full>] [--format json]"
14647
+ );
14648
+ await createProject(name, {
14649
+ domain: getString(input.values, "domain") ?? name,
14650
+ ownedDomains: getStringArray(input.values, "owned-domain") ?? [],
14651
+ aliases: getStringArray(input.values, "alias") ?? [],
14652
+ qualifiedAliases: getStringArray(input.values, "qualified-alias") ?? [],
14653
+ country: getString(input.values, "country") ?? "US",
14654
+ language: getString(input.values, "language") ?? "en",
14655
+ displayName: getString(input.values, "display-name") ?? name,
14656
+ providers: getStringArray(input.values, "provider") ?? [],
14657
+ providerModels: parseProviderModelAssignments(getStringArray(input.values, "provider-model")),
14658
+ providerDispatchModes: parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode")),
14659
+ siteAuditMaxPages: parseSiteAuditMaxPages(getString(input.values, "site-audit-max-pages"), "project.create"),
14660
+ format: input.format
14661
+ });
14696
14662
  }
14697
- const run = await client.getRun(runId);
14698
- if (showProgress) process.stderr.write(".");
14699
- if (TERMINAL_STATUSES.has(run.status) || outstandingBatches(run).length > 0) {
14700
- return run;
14663
+ },
14664
+ {
14665
+ path: ["project", "update"],
14666
+ usage: "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--add-qualified-alias <name>...] [--remove-qualified-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>] [--site-audit-max-pages <1-50000|full>] [--competitor-auto-aliases <off|preview|apply>] [--format json]",
14667
+ options: {
14668
+ domain: { type: "string", short: "d" },
14669
+ "owned-domain": multiStringOption(),
14670
+ "add-domain": multiStringOption(),
14671
+ "remove-domain": multiStringOption(),
14672
+ alias: multiStringOption(),
14673
+ "add-alias": multiStringOption(),
14674
+ "remove-alias": multiStringOption(),
14675
+ "add-qualified-alias": multiStringOption(),
14676
+ "remove-qualified-alias": multiStringOption(),
14677
+ country: stringOption(),
14678
+ language: stringOption(),
14679
+ "display-name": stringOption(),
14680
+ provider: multiStringOption(),
14681
+ "all-providers": { type: "boolean" },
14682
+ "provider-model": multiStringOption(),
14683
+ "clear-provider-model": multiStringOption(),
14684
+ "dispatch-mode": multiStringOption(),
14685
+ "clear-dispatch-mode": multiStringOption(),
14686
+ "negative-review-max-stars": stringOption(),
14687
+ "site-audit-max-pages": stringOption(),
14688
+ "competitor-auto-aliases": stringOption()
14689
+ },
14690
+ run: async (input) => {
14691
+ const name = requireProject(
14692
+ input,
14693
+ "project.update",
14694
+ "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--add-qualified-alias <name>...] [--remove-qualified-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>] [--site-audit-max-pages <1-50000|full>] [--competitor-auto-aliases <off|preview|apply>] [--format json]"
14695
+ );
14696
+ const providers = getStringArray(input.values, "provider");
14697
+ const allProviders = getBoolean(input.values, "all-providers");
14698
+ if (allProviders && providers?.length) throw usageError("Error: --all-providers conflicts with --provider");
14699
+ const providerModels = parseProviderModelAssignments(getStringArray(input.values, "provider-model"));
14700
+ const clearProviderModels = getStringArray(input.values, "clear-provider-model") ?? [];
14701
+ for (const provider of clearProviderModels) {
14702
+ if (provider in providerModels) throw usageError(`Error: --provider-model and --clear-provider-model conflict for ${provider}`);
14703
+ }
14704
+ const dispatchModes = parseDispatchModeAssignments(getStringArray(input.values, "dispatch-mode"));
14705
+ const clearDispatchModes = getStringArray(input.values, "clear-dispatch-mode") ?? [];
14706
+ for (const provider of clearDispatchModes) {
14707
+ if (provider in dispatchModes) throw usageError(`Error: --dispatch-mode and --clear-dispatch-mode conflict for ${provider}`);
14708
+ }
14709
+ await updateProjectSettings(name, {
14710
+ displayName: getString(input.values, "display-name"),
14711
+ domain: getString(input.values, "domain"),
14712
+ ownedDomains: getStringArray(input.values, "owned-domain"),
14713
+ addOwnedDomain: getStringArray(input.values, "add-domain"),
14714
+ removeOwnedDomain: getStringArray(input.values, "remove-domain"),
14715
+ aliases: getStringArray(input.values, "alias"),
14716
+ addAlias: getStringArray(input.values, "add-alias"),
14717
+ removeAlias: getStringArray(input.values, "remove-alias"),
14718
+ addQualifiedAlias: getStringArray(input.values, "add-qualified-alias"),
14719
+ removeQualifiedAlias: getStringArray(input.values, "remove-qualified-alias"),
14720
+ country: getString(input.values, "country"),
14721
+ language: getString(input.values, "language"),
14722
+ providers: allProviders ? [] : providers,
14723
+ providerModels,
14724
+ clearProviderModels,
14725
+ dispatchModes,
14726
+ clearDispatchModes,
14727
+ negativeReviewMaxStars: parseNegativeReviewMaxStars(getString(input.values, "negative-review-max-stars")),
14728
+ siteAuditMaxPages: parseSiteAuditMaxPages(getString(input.values, "site-audit-max-pages"), "project.update"),
14729
+ competitorAutoAliases: parseCompetitorAutoAliasMode(getString(input.values, "competitor-auto-aliases")),
14730
+ format: input.format
14731
+ });
14701
14732
  }
14702
- }
14703
- }
14704
- function outstandingBatches(run) {
14705
- if (TERMINAL_STATUSES.has(run.status)) return [];
14706
- return (run.providerBatches ?? []).filter((batch) => OUTSTANDING_PROVIDER_BATCH_STATUSES.includes(batch.status));
14707
- }
14708
- function batchWaitLine(run) {
14709
- const outstanding = outstandingBatches(run);
14710
- if (outstanding.length === 0) return null;
14711
- const batches = outstanding.map((batch) => `${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`);
14712
- return `Waiting on provider batch(es): ${batches.join("; ")}; check with canonry run show ${run.id}`;
14713
- }
14714
- function printRunDetail(run) {
14715
- console.log(`Run: ${run.id}`);
14716
- console.log(` Status: ${run.status}`);
14717
- console.log(` Kind: ${run.kind}`);
14718
- if (run.trigger) console.log(` Trigger: ${run.trigger}`);
14719
- if (run.startedAt) console.log(` Started: ${run.startedAt}`);
14720
- if (run.finishedAt) console.log(` Finished: ${run.finishedAt}`);
14721
- if (run.createdAt) console.log(` Created: ${run.createdAt}`);
14722
- if (run.error) {
14723
- if (run.error.message) console.log(` Error: ${run.error.message}`);
14724
- if (run.error.providers) {
14725
- for (const [provider, detail] of Object.entries(run.error.providers)) {
14726
- console.log(` Error (${provider}): ${detail.message}`);
14733
+ },
14734
+ {
14735
+ path: ["project", "list"],
14736
+ usage: "canonry project list [--format json]",
14737
+ allowPositionals: false,
14738
+ run: async (input) => {
14739
+ await listProjects(input.format);
14740
+ }
14741
+ },
14742
+ {
14743
+ path: ["project", "show"],
14744
+ usage: "canonry project show <name> [--format json]",
14745
+ run: async (input) => {
14746
+ const name = requireProject(input, "project.show", "canonry project show <name>");
14747
+ await showProject(name, input.format);
14748
+ }
14749
+ },
14750
+ {
14751
+ path: ["project", "delete"],
14752
+ usage: "canonry project delete <name> [--dry-run] [--format json]",
14753
+ supportsDryRun: true,
14754
+ run: async (input) => {
14755
+ const name = requireProject(input, "project.delete", "canonry project delete <name> [--dry-run] [--format json]");
14756
+ await deleteProject(name, { dryRun: input.dryRun, format: input.format });
14757
+ }
14758
+ },
14759
+ {
14760
+ path: ["project", "add-location"],
14761
+ usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]",
14762
+ options: {
14763
+ label: stringOption(),
14764
+ city: stringOption(),
14765
+ region: stringOption(),
14766
+ country: stringOption(),
14767
+ timezone: stringOption()
14768
+ },
14769
+ run: async (input) => {
14770
+ const name = requireProject(
14771
+ input,
14772
+ "project.add-location",
14773
+ "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]"
14774
+ );
14775
+ const label = getString(input.values, "label");
14776
+ const city = getString(input.values, "city");
14777
+ const region = getString(input.values, "region");
14778
+ const country = getString(input.values, "country");
14779
+ if (!label || !city || !region || !country) {
14780
+ throw usageError("Error: --label, --city, --region, and --country are all required", {
14781
+ message: "location label, city, region, and country are required",
14782
+ details: {
14783
+ command: "project.add-location",
14784
+ usage: "canonry project add-location <name> --label <label> --city <city> --region <region> --country <country> [--format json]",
14785
+ required: ["label", "city", "region", "country"]
14786
+ }
14787
+ });
14727
14788
  }
14789
+ await addLocation(name, {
14790
+ label,
14791
+ city,
14792
+ region,
14793
+ country,
14794
+ timezone: getString(input.values, "timezone"),
14795
+ format: input.format
14796
+ });
14797
+ }
14798
+ },
14799
+ {
14800
+ path: ["project", "locations"],
14801
+ usage: "canonry project locations <name> [--format json]",
14802
+ run: async (input) => {
14803
+ const name = requireProject(input, "project.locations", "canonry project locations <name> [--format json]");
14804
+ await listLocations(name, input.format);
14805
+ }
14806
+ },
14807
+ {
14808
+ path: ["project", "remove-location"],
14809
+ usage: "canonry project remove-location <name> <label> [--format json]",
14810
+ run: async (input) => {
14811
+ const name = requireProject(input, "project.remove-location", "canonry project remove-location <name> <label> [--format json]");
14812
+ const label = requirePositional(input, 1, {
14813
+ command: "project.remove-location",
14814
+ usage: "canonry project remove-location <name> <label> [--format json]",
14815
+ message: "project name and location label are required"
14816
+ });
14817
+ await removeLocation(name, label, input.format);
14818
+ }
14819
+ },
14820
+ {
14821
+ path: ["project", "set-default-location"],
14822
+ usage: "canonry project set-default-location <name> <label> [--format json]",
14823
+ run: async (input) => {
14824
+ const name = requireProject(input, "project.set-default-location", "canonry project set-default-location <name> <label> [--format json]");
14825
+ const label = requirePositional(input, 1, {
14826
+ command: "project.set-default-location",
14827
+ usage: "canonry project set-default-location <name> <label> [--format json]",
14828
+ message: "project name and location label are required"
14829
+ });
14830
+ await setDefaultLocation(name, label, input.format);
14831
+ }
14832
+ },
14833
+ {
14834
+ path: ["project"],
14835
+ usage: "canonry project <create|update|list|show|delete|add-location|locations|remove-location|set-default-location> [args]",
14836
+ run: async (input) => {
14837
+ unknownSubcommand(input.positionals[0], {
14838
+ command: "project",
14839
+ usage: "canonry project <create|update|list|show|delete|add-location|locations|remove-location|set-default-location> [args]",
14840
+ available: ["create", "update", "list", "show", "delete", "add-location", "locations", "remove-location", "set-default-location"]
14841
+ });
14728
14842
  }
14729
14843
  }
14730
- const batched = Object.keys(run.dispatchModes ?? {}).sort();
14731
- if (batched.length > 0) console.log(` Dispatch: ${batched.map((provider) => `${provider}=batch`).join(", ")} (other providers sync)`);
14732
- if (run.providerBatches && run.providerBatches.length > 0) {
14733
- console.log("\n Provider batches:");
14734
- for (const batch of run.providerBatches) console.log(` ${providerBatchLine(batch)}`);
14844
+ ];
14845
+ function parseDispatchModeAssignments(assignments) {
14846
+ const result = {};
14847
+ for (const assignment of assignments ?? []) {
14848
+ const separator = assignment.indexOf("=");
14849
+ const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
14850
+ const mode = providerDispatchModeSchema.safeParse(separator === -1 ? "" : assignment.slice(separator + 1).trim().toLowerCase());
14851
+ if (!provider || !mode.success) throw usageError(`Error: --dispatch-mode must use provider=sync or provider=batch (received ${assignment})`);
14852
+ if (provider in result) throw usageError(`Error: duplicate --dispatch-mode assignment for ${provider}`);
14853
+ result[provider] = mode.data;
14735
14854
  }
14736
- if (run.usage && run.usage.length > 0) printUsageTable(run.usage);
14737
- if (run.snapshots && run.snapshots.length > 0) {
14738
- console.log(`
14739
- Snapshots: ${run.snapshots.length} (cell = [citation][mention]; C=cited c=not, M=mentioned m=not, \u2013=no data)`);
14740
- for (const s of run.snapshots) {
14741
- const citationGlyph = s.citationState === CitationStates.cited ? "C" : "c";
14742
- const mentionGlyph = typeof s.answerMentioned === "boolean" ? s.answerMentioned ? "M" : "m" : "\u2013";
14743
- const modelLabel = s.model ? ` (${s.model})` : "";
14744
- console.log(` [${citationGlyph}${mentionGlyph}] ${s.provider}${modelLabel} ${s.query}`);
14745
- }
14855
+ return result;
14856
+ }
14857
+ function parseProviderModelAssignments(assignments) {
14858
+ const result = {};
14859
+ for (const assignment of assignments ?? []) {
14860
+ const separator = assignment.indexOf("=");
14861
+ const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
14862
+ const model = separator === -1 ? "" : assignment.slice(separator + 1).trim();
14863
+ if (!provider || !model) throw usageError(`Error: --provider-model must use provider=model (received ${assignment})`);
14864
+ if (provider in result) throw usageError(`Error: duplicate --provider-model assignment for ${provider}`);
14865
+ result[provider] = model;
14746
14866
  }
14867
+ return result;
14747
14868
  }
14748
- function providerBatchLine(batch) {
14749
- const recorded = `${batch.recordedCount} of ${batch.requestCount} answers recorded`;
14750
- const withError = (text) => batch.error ? `${text} (${batch.error})` : text;
14751
- switch (batch.status) {
14752
- case ProviderBatchStatuses.submitting:
14753
- return `submitting provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests`;
14754
- // The two outstanding statuses: the run stays `running` until they clear.
14755
- case ProviderBatchStatuses.submitted:
14756
- case ProviderBatchStatuses.ended:
14757
- return `waiting on provider batch: ${batch.provider} \u2014 ${batch.requestCount} requests, submitted ${batch.submittedAt ?? "unknown"}, deadline ${batch.deadlineAt}`;
14758
- case ProviderBatchStatuses.ingested:
14759
- return withError(`provider batch ${batch.provider}: ingested \u2014 ${recorded}`);
14760
- case ProviderBatchStatuses.cancelled:
14761
- return withError(`provider batch ${batch.provider}: cancelled \u2014 ${recorded}; the rest are missing and can be filled`);
14762
- case ProviderBatchStatuses.failed:
14763
- return withError(`provider batch ${batch.provider}: rejected by the provider \u2014 its ${batch.requestCount} answers ran sync instead`);
14764
- case ProviderBatchStatuses.unknown:
14765
- return withError(`provider batch ${batch.provider}: submit outcome unknown \u2014 never resubmitted; its ${batch.requestCount} answers stay missing`);
14869
+ function parseNegativeReviewMaxStars(raw) {
14870
+ if (raw === void 0) return void 0;
14871
+ if (raw === "default") return null;
14872
+ const parsed = gbpNegativeReviewMaxStarsSchema.safeParse(Number(raw));
14873
+ if (!parsed.success) {
14874
+ throw usageError('Error: --negative-review-max-stars must be 1, 2, 3, 4, or "default" (3)', {
14875
+ message: '--negative-review-max-stars must be 1-4 or "default"',
14876
+ details: { command: "project.update", value: raw }
14877
+ });
14766
14878
  }
14879
+ return parsed.data;
14767
14880
  }
14768
- function printUsageTable(rows) {
14769
- const cost = (row) => row.estimatedCostMicros === null ? "unpriced" : `${formatMicros(row.estimatedCostMicros, "USD", { fractionDigits: 4, showTinyAsLessThan: true })}${row.unpricedAnswers > 0 ? ` (+${row.unpricedAnswers} unpriced)` : ""}`;
14770
- const table = rows.map((row) => [
14771
- row.provider,
14772
- row.pricingTier,
14773
- row.answers.toLocaleString("en-US"),
14774
- row.inputTokens.toLocaleString("en-US"),
14775
- row.cachedInputTokens.toLocaleString("en-US"),
14776
- row.cacheWriteTokens.toLocaleString("en-US"),
14777
- row.outputTokens.toLocaleString("en-US"),
14778
- row.searchCount.toLocaleString("en-US"),
14779
- cost(row)
14780
- ]);
14781
- const header = ["PROVIDER", "TIER", "ANSWERS", "INPUT", "CACHED", "CACHE WRITE", "OUTPUT", "SEARCHES", "EST. COST"];
14782
- const widths = header.map((title, index) => Math.max(title.length, ...table.map((cells) => cells[index].length)));
14783
- const line = (cells) => ` ${cells.map((cell2, index) => cell2.padEnd(widths[index])).join(" ").trimEnd()}`;
14784
- console.log("\n Usage (answers with recorded usage; cost is an estimate):");
14785
- console.log(line(header));
14786
- for (const cells of table) console.log(line(cells));
14881
+ function parseSiteAuditMaxPages(raw, command) {
14882
+ if (raw === void 0) return void 0;
14883
+ const value = raw.trim();
14884
+ if (value.toLowerCase() === "full") return null;
14885
+ const parsed = /^\d+$/.test(value) ? siteAuditPageBudgetSchema.safeParse(Number(value)) : void 0;
14886
+ if (!parsed?.success) {
14887
+ throw usageError(`Error: --site-audit-max-pages must be a whole number of pages from 1 to ${SITE_AUDIT_MAX_PAGE_LIMIT}, or "full" for the full site`, {
14888
+ message: `--site-audit-max-pages must be 1-${SITE_AUDIT_MAX_PAGE_LIMIT} or "full"`,
14889
+ details: { command, option: "site-audit-max-pages", value: raw }
14890
+ });
14891
+ }
14892
+ return parsed.data;
14787
14893
  }
14788
- var FILL_POLL_INTERVAL_MS = 3e3;
14789
- var FILL_POLL_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
14790
- function missingSummary(completeness) {
14791
- const parts = Object.entries(completeness.missingByProvider).sort(([left], [right]) => left.localeCompare(right)).map(([provider, count]) => `${provider} ${count}`);
14792
- return parts.length ? parts.join(", ") : "none";
14894
+ function parseCompetitorAutoAliasMode(raw) {
14895
+ if (raw === void 0) return void 0;
14896
+ const parsed = competitorAutoAliasModeSchema.safeParse(raw.trim().toLowerCase());
14897
+ if (!parsed.success) {
14898
+ throw usageError("Error: --competitor-auto-aliases must be off, preview or apply", {
14899
+ message: "--competitor-auto-aliases must be off, preview or apply",
14900
+ details: { command: "project.update", option: "competitor-auto-aliases", value: raw }
14901
+ });
14902
+ }
14903
+ return parsed.data;
14793
14904
  }
14794
- function printCompleteness(completeness) {
14795
- console.log(`Run: ${completeness.runId}`);
14796
- console.log(` Status: ${completeness.status}`);
14797
- if (!completeness.planned) {
14798
- console.log(" Not a measurement-plan run: there are no expected answers to count.");
14905
+
14906
+ // src/commands/organic-evidence.ts
14907
+ async function showOrganicEvidence(project2, opts) {
14908
+ const data = await createApiClient().getOrganicEvidence(project2, opts.period);
14909
+ if (isMachineFormat(opts.format)) {
14910
+ console.log(JSON.stringify(data, null, 2));
14799
14911
  return;
14800
14912
  }
14801
- if (!completeness.readable) {
14802
- console.log(" Answers: unknown (the run's manifest cannot be read)");
14803
- } else {
14804
- console.log(` Answers: ${completeness.executed}/${completeness.expected}`);
14805
- console.log(` Missing: ${completeness.missing} (${missingSummary(completeness)})`);
14913
+ printOrganicEvidence(project2, data);
14914
+ }
14915
+ function formatCounts(clicks, impressions) {
14916
+ return `${clicks} clicks / ${impressions} impressions`;
14917
+ }
14918
+ function printOrganicEvidence(project2, data) {
14919
+ const asOf = data.asOfDate ? ` through ${data.asOfDate}` : "";
14920
+ console.log(`Organic evidence: ${project2} (${data.periodDays} days${asOf})`);
14921
+ console.log(`Coverage: GSC ${data.coverage.gsc ? "yes" : "no"} \xB7 GA4 ${data.coverage.ga4 ? "yes" : "no"} \xB7 server ${data.coverage.server ? "yes" : "no"} \xB7 AI visibility ${data.coverage.visibility ? "yes" : "no"}`);
14922
+ if (data.gsc) {
14923
+ console.log(`Google Search: ${formatCounts(data.gsc.propertyTotals.clicks, data.gsc.propertyTotals.impressions)} (named non-brand: ${formatCounts(data.gsc.namedNonBrand.clicks, data.gsc.namedNonBrand.impressions)})`);
14806
14924
  }
14807
- if (completeness.refusal) console.log(` Fillable: no. ${completeness.refusal.message}`);
14808
- else console.log(` Fillable: ${completeness.fillable ? "yes" : "not right now (a sweep or another fill is running)"}`);
14809
- const fill = completeness.latestFill;
14810
- if (fill) {
14811
- console.log(` Last fill: ${fill.id} ${fill.status}, ${fill.filled}/${fill.expected} recorded${fill.error ? `. ${fill.error}` : ""}`);
14925
+ if (data.ga4) console.log(`GA4 organic: ${data.ga4.organicSessions} sessions`);
14926
+ if (data.gaAiReferrals) console.log(`GA4 AI referrals: ${data.gaAiReferrals.organicSessions} organic, ${data.gaAiReferrals.paidSessions} paid sessions`);
14927
+ if (data.server) {
14928
+ const s = data.server;
14929
+ const fetchTotal = s.userFetchHits.verified + s.userFetchHits.claimedUnverified + s.userFetchHits.unknownAiLike;
14930
+ console.log(`Server AI: ${s.crawlerHits.verified} verified + ${s.crawlerHits.claimedUnverified} claimed + ${s.crawlerHits.unknownAiLike} heuristic crawls; ${fetchTotal} user-agent fetches (${s.userFetchHits.verified} verified, ${s.userFetchHits.claimedUnverified} claimed, ${s.userFetchHits.unknownAiLike} heuristic); ${s.referralSessions.organic} organic referrals, ${s.referralSessions.paid} paid, ${s.referralSessions.unknown} unclassified`);
14812
14931
  }
14813
- }
14814
- async function showRunCompleteness(runId, format) {
14815
- const completeness = await getClient23().getRunCompleteness(runId);
14816
- if (isMachineFormat(format)) {
14817
- console.log(JSON.stringify(completeness, null, 2));
14818
- return;
14932
+ if (data.visibility) {
14933
+ const v = data.visibility;
14934
+ console.log(`Latest AI visibility: ${v.mentionedPairs}/${v.answerPairs} mentioned, ${v.citedPairs}/${v.answerPairs} cited (${Math.floor(v.ageDays)}d old)`);
14819
14935
  }
14820
- printCompleteness(completeness);
14821
- }
14822
- async function fillRun(runId, opts = {}) {
14823
- const client = getClient23();
14824
- const response = await client.fillRun(runId, {
14825
- ...opts.providers?.length ? { providers: opts.providers } : {},
14826
- ...opts.dryRun ? { dryRun: true } : {}
14827
- });
14828
- if (!opts.wait || response.outcome !== "queued" || !response.fill) {
14829
- if (isMachineFormat(opts.format)) {
14830
- console.log(JSON.stringify(response, null, 2));
14831
- return;
14832
- }
14833
- if (response.outcome === "already-complete") console.log(`Run ${runId} is already complete: nothing to fill.`);
14834
- else if (response.outcome === "dry-run") console.log("Dry run: nothing was queued.");
14835
- else if (response.fill) console.log(`Fill ${response.fill.id} queued: ${response.fill.expected} missing answer(s) (${missingSummary(response.completeness)}).`);
14836
- printCompleteness(response.completeness);
14837
- return;
14936
+ if (data.findings.length > 0) {
14937
+ console.log("\nFindings:");
14938
+ for (const finding of data.findings) console.log(`- ${finding.title}: ${finding.detail}`);
14838
14939
  }
14839
- const fillId = response.fill.id;
14840
- const deadline = Date.now() + FILL_POLL_TIMEOUT_MS;
14841
- let completeness = response.completeness;
14842
- if (!isMachineFormat(opts.format)) {
14843
- process.stderr.write(`Filling ${response.fill.expected} missing answer(s) in run ${runId}`);
14940
+ if (data.limitations.length > 0) {
14941
+ console.log("\nLimitations:");
14942
+ for (const limitation of data.limitations) console.log(`- ${limitation.detail}`);
14844
14943
  }
14845
- for (; ; ) {
14846
- await new Promise((resolve) => setTimeout(resolve, FILL_POLL_INTERVAL_MS));
14847
- if (Date.now() > deadline) {
14848
- throw new CliError({
14849
- code: "RUN_FILL_INCOMPLETE",
14850
- message: `Timed out waiting for fill ${fillId}; it may still be running. Check with: canonry run completeness ${runId}`,
14851
- exitCode: EXIT_SYSTEM_ERROR,
14852
- details: { runId, fillId }
14944
+ }
14945
+
14946
+ // src/cli-commands/organic-evidence.ts
14947
+ var USAGE3 = "canonry organic-evidence <project> [--period 60|90] [--format json|jsonl]";
14948
+ function parsePeriod(value) {
14949
+ if (value === void 0) return void 0;
14950
+ if (value === "60" || value === "90") return Number(value);
14951
+ throw usageError(`Error: --period must be 60 or 90
14952
+
14953
+ Usage: ${USAGE3}`);
14954
+ }
14955
+ var ORGANIC_EVIDENCE_CLI_COMMANDS = [
14956
+ {
14957
+ path: ["organic-evidence"],
14958
+ usage: USAGE3,
14959
+ options: { period: stringOption() },
14960
+ run: async (input) => {
14961
+ const project2 = requireProject(input, "organic-evidence", USAGE3);
14962
+ await showOrganicEvidence(project2, {
14963
+ period: parsePeriod(getString(input.values, "period")),
14964
+ format: input.format
14853
14965
  });
14854
14966
  }
14855
- completeness = await client.getRunCompleteness(runId);
14856
- if (!isMachineFormat(opts.format)) process.stderr.write(".");
14857
- const latest = completeness.latestFill;
14858
- const working = latest !== null && (latest.status === "queued" || latest.status === "running");
14859
- if (completeness.status === "completed" || !working) break;
14860
- }
14861
- if (!isMachineFormat(opts.format)) process.stderr.write("\n");
14862
- if (isMachineFormat(opts.format)) console.log(JSON.stringify(completeness, null, 2));
14863
- else printCompleteness(completeness);
14864
- if (completeness.status !== "completed") {
14865
- throw new CliError({
14866
- code: "RUN_FILL_INCOMPLETE",
14867
- message: `Run ${runId} is still ${completeness.status}: ${completeness.missing} answer(s) missing (${missingSummary(completeness)}).`,
14868
- exitCode: EXIT_SYSTEM_ERROR,
14869
- details: { runId, fillId, missing: completeness.missing, missingByProvider: completeness.missingByProvider }
14870
- });
14871
14967
  }
14872
- }
14968
+ ];
14873
14969
 
14874
14970
  // src/cli-commands/run.ts
14875
14971
  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] [--force] [--wait] [--format json]";
@@ -16364,6 +16460,9 @@ function renderHuman2(overview) {
16364
16460
  } else {
16365
16461
  console.log("\n No runs yet.");
16366
16462
  }
16463
+ const admission = runAdmissionLines(meta.name, latestRun.admission, latestRun.manualAdmission);
16464
+ if (admission.length > 0) console.log(`
16465
+ ${admission.map((line) => ` ${line}`).join("\n")}`);
16367
16466
  console.log("\nScores:");
16368
16467
  printScore("Mention ", scores.mention);
16369
16468
  printScore("Visibility ", scores.visibility);