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