@ainyc/canonry 3.3.3 → 3.3.8

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/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  setGoogleAuthConfig,
18
18
  showFirstRunNotice,
19
19
  trackEvent
20
- } from "./chunk-GH5ILITH.js";
20
+ } from "./chunk-P6D3O5JB.js";
21
21
  import {
22
22
  CcReleaseSyncStatuses,
23
23
  CheckScopes,
@@ -48,7 +48,7 @@ import {
48
48
  saveConfigPatch,
49
49
  skillsClientSchema,
50
50
  usageError
51
- } from "./chunk-NCGX6UI4.js";
51
+ } from "./chunk-24C7RMIS.js";
52
52
  import {
53
53
  apiKeys,
54
54
  competitors,
@@ -71,9 +71,9 @@ import { parseArgs } from "util";
71
71
  function commandId(spec) {
72
72
  return spec.path.join(".");
73
73
  }
74
- function matchesPath(args, path9) {
75
- if (args.length < path9.length) return false;
76
- return path9.every((segment, index) => args[index] === segment);
74
+ function matchesPath(args, path10) {
75
+ if (args.length < path10.length) return false;
76
+ return path10.every((segment, index) => args[index] === segment);
77
77
  }
78
78
  function withFormatOption(options) {
79
79
  if (!options) {
@@ -2015,9 +2015,9 @@ async function gaConnect(project, opts) {
2015
2015
  propertyId: opts.propertyId
2016
2016
  };
2017
2017
  if (opts.keyFile) {
2018
- const fs10 = await import("fs");
2018
+ const fs11 = await import("fs");
2019
2019
  try {
2020
- const content = fs10.readFileSync(opts.keyFile, "utf-8");
2020
+ const content = fs11.readFileSync(opts.keyFile, "utf-8");
2021
2021
  JSON.parse(content);
2022
2022
  body.keyJson = content;
2023
2023
  } catch (e) {
@@ -5174,6 +5174,925 @@ var PROJECT_CLI_COMMANDS = [
5174
5174
  }
5175
5175
  ];
5176
5176
 
5177
+ // src/commands/report.ts
5178
+ import fs4 from "fs";
5179
+ import path3 from "path";
5180
+
5181
+ // src/report-renderer.ts
5182
+ var COLORS = {
5183
+ bg: "#09090b",
5184
+ surface: "#18181b4d",
5185
+ border: "#27272a99",
5186
+ text: "#fafafa",
5187
+ textMuted: "#a1a1aa",
5188
+ textFaint: "#71717a",
5189
+ positive: "#10b981",
5190
+ caution: "#f59e0b",
5191
+ negative: "#f43f5e",
5192
+ neutral: "#71717a",
5193
+ accent: "#3b82f6",
5194
+ series: ["#10b981", "#3b82f6", "#ec4899", "#eab308", "#a855f7", "#f97316", "#06b6d4", "#ef4444"]
5195
+ };
5196
+ function escapeHtml(value) {
5197
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
5198
+ }
5199
+ function formatRatio(value) {
5200
+ if (!Number.isFinite(value) || value === 0) return "0%";
5201
+ return `${(value * 100).toFixed(1)}%`;
5202
+ }
5203
+ function formatNumber(value) {
5204
+ if (!Number.isFinite(value)) return "\u2014";
5205
+ if (Math.abs(value) >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
5206
+ if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
5207
+ return value.toLocaleString("en-US");
5208
+ }
5209
+ function formatDate(iso) {
5210
+ if (!iso) return "\u2014";
5211
+ try {
5212
+ const d = new Date(iso);
5213
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
5214
+ } catch {
5215
+ return iso;
5216
+ }
5217
+ }
5218
+ function pressureTone(label) {
5219
+ if (label === "High") return "negative";
5220
+ if (label === "Moderate") return "caution";
5221
+ if (label === "Low") return "positive";
5222
+ return "neutral";
5223
+ }
5224
+ function severityTone(severity) {
5225
+ switch (severity) {
5226
+ case "critical":
5227
+ return "negative";
5228
+ case "high":
5229
+ return "negative";
5230
+ case "medium":
5231
+ return "caution";
5232
+ case "low":
5233
+ return "neutral";
5234
+ }
5235
+ }
5236
+ var STYLE = `
5237
+ :root {
5238
+ color-scheme: dark;
5239
+ }
5240
+ * { box-sizing: border-box; }
5241
+ html, body { margin: 0; padding: 0; }
5242
+ body {
5243
+ background: ${COLORS.bg};
5244
+ color: ${COLORS.text};
5245
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
5246
+ font-size: 14px;
5247
+ line-height: 1.5;
5248
+ -webkit-font-smoothing: antialiased;
5249
+ }
5250
+ .container {
5251
+ max-width: 1100px;
5252
+ margin: 0 auto;
5253
+ padding: 48px 24px 96px;
5254
+ }
5255
+ .header {
5256
+ border-bottom: 1px solid ${COLORS.border};
5257
+ padding-bottom: 32px;
5258
+ margin-bottom: 48px;
5259
+ }
5260
+ .header h1 {
5261
+ font-size: 32px;
5262
+ font-weight: 700;
5263
+ margin: 0 0 8px;
5264
+ letter-spacing: -0.02em;
5265
+ }
5266
+ .header .subtitle {
5267
+ color: ${COLORS.textMuted};
5268
+ font-size: 14px;
5269
+ }
5270
+ .eyebrow {
5271
+ text-transform: uppercase;
5272
+ letter-spacing: 0.08em;
5273
+ font-size: 10px;
5274
+ color: ${COLORS.textFaint};
5275
+ font-weight: 600;
5276
+ margin-bottom: 8px;
5277
+ }
5278
+ section.report-section {
5279
+ margin: 64px 0;
5280
+ }
5281
+ section.report-section h2 {
5282
+ font-size: 22px;
5283
+ font-weight: 700;
5284
+ margin: 0 0 24px;
5285
+ letter-spacing: -0.01em;
5286
+ }
5287
+ section.report-section .section-intro {
5288
+ color: ${COLORS.textMuted};
5289
+ margin-bottom: 24px;
5290
+ }
5291
+ .metric-grid {
5292
+ display: grid;
5293
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
5294
+ gap: 16px;
5295
+ }
5296
+ .metric {
5297
+ background: ${COLORS.surface};
5298
+ border: 1px solid ${COLORS.border};
5299
+ border-radius: 8px;
5300
+ padding: 16px 20px;
5301
+ }
5302
+ .metric .label {
5303
+ text-transform: uppercase;
5304
+ letter-spacing: 0.08em;
5305
+ font-size: 10px;
5306
+ color: ${COLORS.textFaint};
5307
+ font-weight: 600;
5308
+ margin-bottom: 8px;
5309
+ }
5310
+ .metric .value {
5311
+ font-size: 28px;
5312
+ font-weight: 700;
5313
+ letter-spacing: -0.02em;
5314
+ }
5315
+ .metric .delta {
5316
+ font-size: 12px;
5317
+ color: ${COLORS.textMuted};
5318
+ margin-top: 4px;
5319
+ }
5320
+ .findings {
5321
+ margin-top: 24px;
5322
+ display: grid;
5323
+ gap: 12px;
5324
+ }
5325
+ .finding {
5326
+ background: ${COLORS.surface};
5327
+ border: 1px solid ${COLORS.border};
5328
+ border-left-width: 3px;
5329
+ border-radius: 6px;
5330
+ padding: 12px 16px;
5331
+ }
5332
+ .finding.tone-positive { border-left-color: ${COLORS.positive}; }
5333
+ .finding.tone-caution { border-left-color: ${COLORS.caution}; }
5334
+ .finding.tone-negative { border-left-color: ${COLORS.negative}; }
5335
+ .finding.tone-neutral { border-left-color: ${COLORS.neutral}; }
5336
+ .finding strong { display: block; margin-bottom: 4px; }
5337
+ .finding span { color: ${COLORS.textMuted}; font-size: 13px; }
5338
+ table.report-table {
5339
+ width: 100%;
5340
+ border-collapse: collapse;
5341
+ font-size: 13px;
5342
+ }
5343
+ table.report-table th, table.report-table td {
5344
+ text-align: left;
5345
+ padding: 10px 12px;
5346
+ border-bottom: 1px solid ${COLORS.border};
5347
+ }
5348
+ table.report-table th {
5349
+ font-weight: 600;
5350
+ color: ${COLORS.textMuted};
5351
+ text-transform: uppercase;
5352
+ letter-spacing: 0.06em;
5353
+ font-size: 10px;
5354
+ }
5355
+ table.report-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; }
5356
+ table.report-table td .badge {
5357
+ display: inline-block;
5358
+ padding: 2px 8px;
5359
+ border-radius: 999px;
5360
+ font-size: 11px;
5361
+ font-weight: 600;
5362
+ border: 1px solid;
5363
+ }
5364
+ .cell-cited { color: ${COLORS.positive}; font-weight: 600; }
5365
+ .cell-not-cited { color: ${COLORS.textFaint}; }
5366
+ .cell-pending { color: ${COLORS.textFaint}; font-style: italic; }
5367
+ .tone-positive { color: ${COLORS.positive}; }
5368
+ .tone-caution { color: ${COLORS.caution}; }
5369
+ .tone-negative { color: ${COLORS.negative}; }
5370
+ .tone-neutral { color: ${COLORS.neutral}; }
5371
+ .badge.tone-positive { color: ${COLORS.positive}; border-color: ${COLORS.positive}40; background: ${COLORS.positive}14; }
5372
+ .badge.tone-caution { color: ${COLORS.caution}; border-color: ${COLORS.caution}40; background: ${COLORS.caution}14; }
5373
+ .badge.tone-negative { color: ${COLORS.negative}; border-color: ${COLORS.negative}40; background: ${COLORS.negative}14; }
5374
+ .badge.tone-neutral { color: ${COLORS.textMuted}; border-color: ${COLORS.border}; background: transparent; }
5375
+ .chart-card {
5376
+ background: ${COLORS.surface};
5377
+ border: 1px solid ${COLORS.border};
5378
+ border-radius: 8px;
5379
+ padding: 20px;
5380
+ margin-bottom: 16px;
5381
+ }
5382
+ .chart-card h3 {
5383
+ font-size: 14px;
5384
+ font-weight: 600;
5385
+ margin: 0 0 16px;
5386
+ }
5387
+ .chart-grid {
5388
+ display: grid;
5389
+ grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
5390
+ gap: 16px;
5391
+ }
5392
+ .legend {
5393
+ display: flex;
5394
+ flex-wrap: wrap;
5395
+ gap: 12px;
5396
+ font-size: 12px;
5397
+ margin-top: 12px;
5398
+ }
5399
+ .legend-swatch {
5400
+ display: inline-block;
5401
+ width: 10px;
5402
+ height: 10px;
5403
+ border-radius: 2px;
5404
+ margin-right: 6px;
5405
+ vertical-align: middle;
5406
+ }
5407
+ .empty-state {
5408
+ background: ${COLORS.surface};
5409
+ border: 1px dashed ${COLORS.border};
5410
+ border-radius: 8px;
5411
+ padding: 32px;
5412
+ color: ${COLORS.textMuted};
5413
+ text-align: center;
5414
+ font-size: 13px;
5415
+ }
5416
+ .steps {
5417
+ display: grid;
5418
+ gap: 12px;
5419
+ }
5420
+ .step {
5421
+ background: ${COLORS.surface};
5422
+ border: 1px solid ${COLORS.border};
5423
+ border-radius: 8px;
5424
+ padding: 16px 20px;
5425
+ display: grid;
5426
+ gap: 4px;
5427
+ }
5428
+ .step .horizon {
5429
+ text-transform: uppercase;
5430
+ font-size: 10px;
5431
+ letter-spacing: 0.08em;
5432
+ color: ${COLORS.textFaint};
5433
+ font-weight: 600;
5434
+ }
5435
+ .step .title { font-weight: 600; }
5436
+ .step .rationale { color: ${COLORS.textMuted}; font-size: 13px; }
5437
+ .footer {
5438
+ margin-top: 96px;
5439
+ padding-top: 24px;
5440
+ border-top: 1px solid ${COLORS.border};
5441
+ text-align: center;
5442
+ color: ${COLORS.textFaint};
5443
+ font-size: 12px;
5444
+ }
5445
+ @media print {
5446
+ body { background: white; color: black; }
5447
+ section.report-section { break-inside: avoid; }
5448
+ }
5449
+ `;
5450
+ function section(opts, body) {
5451
+ return `<section class="report-section" id="${escapeHtml(opts.id)}">
5452
+ <div class="eyebrow">${escapeHtml(opts.eyebrow)}</div>
5453
+ <h2>${escapeHtml(opts.title)}</h2>
5454
+ ${opts.intro ? `<p class="section-intro">${escapeHtml(opts.intro)}</p>` : ""}
5455
+ ${body}
5456
+ </section>`;
5457
+ }
5458
+ function renderEmpty(message) {
5459
+ return `<div class="empty-state">${escapeHtml(message)}</div>`;
5460
+ }
5461
+ function renderExecutiveSummary(report) {
5462
+ const s = report.executiveSummary;
5463
+ const trendLabel = s.trend === "up" ? "\u2191 Up" : s.trend === "down" ? "\u2193 Down" : s.trend === "flat" ? "\u2192 Flat" : "\u2014";
5464
+ const trendTone = s.trend === "up" ? "positive" : s.trend === "down" ? "negative" : "neutral";
5465
+ const metrics = [
5466
+ {
5467
+ label: "Citation rate",
5468
+ value: `${s.citationRate}%`,
5469
+ delta: `<span class="tone-${trendTone}">${trendLabel}</span> \xB7 ${s.providerCount} provider${s.providerCount === 1 ? "" : "s"}`
5470
+ },
5471
+ {
5472
+ label: "Keywords tracked",
5473
+ value: formatNumber(s.keywordCount),
5474
+ delta: `${s.competitorCount} competitor${s.competitorCount === 1 ? "" : "s"} tracked`
5475
+ }
5476
+ ];
5477
+ if (s.gsc) {
5478
+ metrics.push({
5479
+ label: "GSC clicks",
5480
+ value: formatNumber(s.gsc.clicks),
5481
+ delta: `${formatNumber(s.gsc.impressions)} imp \xB7 ${formatRatio(s.gsc.ctr)} CTR`
5482
+ });
5483
+ }
5484
+ if (s.ga) {
5485
+ metrics.push({
5486
+ label: "GA sessions",
5487
+ value: formatNumber(s.ga.sessions),
5488
+ delta: `${formatNumber(s.ga.users)} users \xB7 ${formatDate(s.ga.periodStart)} \u2192 ${formatDate(s.ga.periodEnd)}`
5489
+ });
5490
+ }
5491
+ const metricsHtml = `<div class="metric-grid">
5492
+ ${metrics.map((m) => `<div class="metric">
5493
+ <div class="label">${escapeHtml(m.label)}</div>
5494
+ <div class="value">${m.value}</div>
5495
+ <div class="delta">${m.delta}</div>
5496
+ </div>`).join("")}
5497
+ </div>`;
5498
+ const findingsHtml = s.findings.length > 0 ? `<div class="findings">${s.findings.map((f) => `
5499
+ <div class="finding tone-${f.tone}">
5500
+ <strong>${escapeHtml(f.title)}</strong>
5501
+ <span>${escapeHtml(f.detail)}</span>
5502
+ </div>`).join("")}</div>` : "";
5503
+ return section(
5504
+ { id: "executive-summary", eyebrow: "Section 1", title: "Executive Summary" },
5505
+ metricsHtml + findingsHtml
5506
+ );
5507
+ }
5508
+ function renderProviderBars(rates) {
5509
+ if (rates.length === 0) return "";
5510
+ const max = Math.max(...rates.map((r) => r.citationRate), 100);
5511
+ const width = 600;
5512
+ const height = Math.max(rates.length * 32 + 24, 80);
5513
+ const labelWidth = 80;
5514
+ const padding = 8;
5515
+ const barWidth = width - labelWidth - padding * 2;
5516
+ const bars = rates.map((r, i) => {
5517
+ const y = i * 32 + padding;
5518
+ const barHeight = 22;
5519
+ const w = max > 0 ? r.citationRate / max * barWidth : 0;
5520
+ const color = COLORS.series[i % COLORS.series.length];
5521
+ return `
5522
+ <text x="${labelWidth - 8}" y="${y + 16}" fill="${COLORS.textMuted}" font-size="11" text-anchor="end">${escapeHtml(r.provider)}</text>
5523
+ <rect x="${labelWidth}" y="${y}" width="${barWidth}" height="${barHeight}" fill="${COLORS.border}" opacity="0.4" rx="3" />
5524
+ <rect x="${labelWidth}" y="${y}" width="${w}" height="${barHeight}" fill="${color}" rx="3" />
5525
+ <text x="${labelWidth + w + 6}" y="${y + 16}" fill="${COLORS.text}" font-size="11">${r.citationRate}% (${r.citedCount}/${r.totalCount})</text>`;
5526
+ }).join("");
5527
+ return `<div class="chart-card">
5528
+ <h3>Provider citation rate</h3>
5529
+ <svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="Provider citation rate bar chart">
5530
+ ${bars}
5531
+ </svg>
5532
+ </div>`;
5533
+ }
5534
+ function renderCitationMatrix(scorecard) {
5535
+ if (scorecard.keywords.length === 0 || scorecard.providers.length === 0) {
5536
+ return renderEmpty("Run a visibility sweep to populate the citation matrix.");
5537
+ }
5538
+ const headers = scorecard.providers.map((p) => `<th>${escapeHtml(p)}</th>`).join("");
5539
+ const rows = scorecard.keywords.map((kw, ki) => {
5540
+ const cells = scorecard.providers.map((_, pi) => {
5541
+ const cell = scorecard.matrix[ki]?.[pi];
5542
+ if (!cell) {
5543
+ return '<td><span class="cell-pending">\u2014</span></td>';
5544
+ }
5545
+ if (cell.citationState === "cited") {
5546
+ return '<td><span class="cell-cited">Cited</span></td>';
5547
+ }
5548
+ return '<td><span class="cell-not-cited">Not cited</span></td>';
5549
+ }).join("");
5550
+ return `<tr><td>${escapeHtml(kw)}</td>${cells}</tr>`;
5551
+ }).join("");
5552
+ return `<table class="report-table">
5553
+ <thead><tr><th>Keyword</th>${headers}</tr></thead>
5554
+ <tbody>${rows}</tbody>
5555
+ </table>`;
5556
+ }
5557
+ function renderCitationScorecard(report) {
5558
+ const body = `
5559
+ ${renderProviderBars(report.citationScorecard.providerRates)}
5560
+ ${renderCitationMatrix(report.citationScorecard)}
5561
+ `;
5562
+ return section(
5563
+ { id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "Per-keyword \xD7 per-provider citation matrix from the latest visibility sweep." },
5564
+ body
5565
+ );
5566
+ }
5567
+ function renderCompetitorBars(landscape, canonical) {
5568
+ const data = [
5569
+ { label: canonical, count: landscape.projectCitationCount, isProject: true },
5570
+ ...landscape.competitors.map((c) => ({ label: c.domain, count: c.citationCount, isProject: false }))
5571
+ ];
5572
+ if (data.length <= 1) return "";
5573
+ const max = Math.max(...data.map((d) => d.count), 1);
5574
+ const width = 600;
5575
+ const height = data.length * 28 + 16;
5576
+ const labelWidth = 160;
5577
+ const bars = data.map((d, i) => {
5578
+ const y = i * 28 + 8;
5579
+ const barHeight = 18;
5580
+ const w = d.count / max * (width - labelWidth - 60);
5581
+ const color = d.isProject ? COLORS.accent : COLORS.series[(i + 1) % COLORS.series.length];
5582
+ return `
5583
+ <text x="${labelWidth - 8}" y="${y + 13}" fill="${COLORS.textMuted}" font-size="11" text-anchor="end">${escapeHtml(d.label)}</text>
5584
+ <rect x="${labelWidth}" y="${y}" width="${w}" height="${barHeight}" fill="${color}" rx="3" />
5585
+ <text x="${labelWidth + w + 6}" y="${y + 13}" fill="${COLORS.text}" font-size="11">${d.count}</text>`;
5586
+ }).join("");
5587
+ return `<div class="chart-card">
5588
+ <h3>Citations per domain</h3>
5589
+ <svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="Citations per domain bar chart">
5590
+ ${bars}
5591
+ </svg>
5592
+ </div>`;
5593
+ }
5594
+ function renderCompetitorLandscape(report) {
5595
+ const competitors2 = report.competitorLandscape.competitors;
5596
+ if (competitors2.length === 0 && report.competitorLandscape.projectCitationCount === 0) {
5597
+ return section(
5598
+ { id: "competitor-landscape", eyebrow: "Section 3", title: "Competitor Landscape" },
5599
+ renderEmpty("No competitor data yet. Add competitors and run a visibility sweep.")
5600
+ );
5601
+ }
5602
+ const rows = competitors2.map((c) => {
5603
+ const tone = pressureTone(c.pressureLabel);
5604
+ return `<tr>
5605
+ <td>${escapeHtml(c.domain)}</td>
5606
+ <td><span class="badge tone-${tone}">${escapeHtml(c.pressureLabel)}</span></td>
5607
+ <td class="numeric">${c.citationCount} / ${c.totalCount}</td>
5608
+ <td>${escapeHtml(c.citedKeywords.slice(0, 5).join(", "))}${c.citedKeywords.length > 5 ? "\u2026" : ""}</td>
5609
+ </tr>`;
5610
+ }).join("");
5611
+ const table = competitors2.length > 0 ? `<table class="report-table">
5612
+ <thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th>Cited keywords</th></tr></thead>
5613
+ <tbody>${rows}</tbody>
5614
+ </table>` : renderEmpty("No competitors configured.");
5615
+ return section(
5616
+ { id: "competitor-landscape", eyebrow: "Section 3", title: "Competitor Landscape", intro: "Where tracked competitors appear in AI answers compared to your domain." },
5617
+ `${renderCompetitorBars(report.competitorLandscape, report.meta.project.canonicalDomain)}${table}`
5618
+ );
5619
+ }
5620
+ function renderDonut(buckets) {
5621
+ if (buckets.length === 0) return "";
5622
+ const total = buckets.reduce((s, b) => s + b.count, 0);
5623
+ if (total === 0) return "";
5624
+ const cx = 110;
5625
+ const cy = 110;
5626
+ const r = 80;
5627
+ const innerR = 48;
5628
+ let cumulative = 0;
5629
+ const slices = [];
5630
+ const legend = [];
5631
+ buckets.forEach((b, i) => {
5632
+ const startAngle = cumulative / total * Math.PI * 2 - Math.PI / 2;
5633
+ const endAngle = (cumulative + b.count) / total * Math.PI * 2 - Math.PI / 2;
5634
+ cumulative += b.count;
5635
+ const x1 = cx + Math.cos(startAngle) * r;
5636
+ const y1 = cy + Math.sin(startAngle) * r;
5637
+ const x2 = cx + Math.cos(endAngle) * r;
5638
+ const y2 = cy + Math.sin(endAngle) * r;
5639
+ const ix1 = cx + Math.cos(endAngle) * innerR;
5640
+ const iy1 = cy + Math.sin(endAngle) * innerR;
5641
+ const ix2 = cx + Math.cos(startAngle) * innerR;
5642
+ const iy2 = cy + Math.sin(startAngle) * innerR;
5643
+ const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
5644
+ const color = COLORS.series[i % COLORS.series.length];
5645
+ if (b.count > 0) {
5646
+ slices.push(`<path d="M ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} L ${ix1} ${iy1} A ${innerR} ${innerR} 0 ${largeArc} 0 ${ix2} ${iy2} Z" fill="${color}" />`);
5647
+ legend.push(`<span><span class="legend-swatch" style="background:${color}"></span>${escapeHtml(b.label)} (${b.count})</span>`);
5648
+ }
5649
+ });
5650
+ return `<div class="chart-card">
5651
+ <h3>AI source categories</h3>
5652
+ <div style="display:flex;align-items:center;gap:24px;flex-wrap:wrap;">
5653
+ <svg viewBox="0 0 220 220" width="220" height="220" role="img" aria-label="AI source category donut chart">
5654
+ ${slices.join("")}
5655
+ </svg>
5656
+ <div class="legend" style="flex-direction:column;align-items:flex-start;gap:6px;">${legend.join("")}</div>
5657
+ </div>
5658
+ </div>`;
5659
+ }
5660
+ function renderAiSourceOrigin(report) {
5661
+ const origin = report.aiSourceOrigin;
5662
+ if (origin.categories.length === 0 && origin.topDomains.length === 0) {
5663
+ return section(
5664
+ { id: "ai-source-origin", eyebrow: "Section 4", title: "AI Source Origin" },
5665
+ renderEmpty("No source data yet. Run a visibility sweep first.")
5666
+ );
5667
+ }
5668
+ const rows = origin.topDomains.map((d) => `
5669
+ <tr>
5670
+ <td>${escapeHtml(d.domain)}</td>
5671
+ <td class="numeric">${d.count}</td>
5672
+ <td>${d.isCompetitor ? '<span class="badge tone-negative">Competitor</span>' : '<span class="badge tone-neutral">External</span>'}</td>
5673
+ </tr>`).join("");
5674
+ const table = origin.topDomains.length > 0 ? `<table class="report-table">
5675
+ <thead><tr><th>Domain</th><th>Citations</th><th>Tag</th></tr></thead>
5676
+ <tbody>${rows}</tbody>
5677
+ </table>` : "";
5678
+ return section(
5679
+ { id: "ai-source-origin", eyebrow: "Section 4", title: "AI Source Origin", intro: "Where AI answers pull from, aggregated across the latest sweep." },
5680
+ `${renderDonut(origin.categories)}${table}`
5681
+ );
5682
+ }
5683
+ function renderLineChart(points, color, title, height = 200) {
5684
+ if (points.length === 0) return "";
5685
+ const width = 600;
5686
+ const padX = 32;
5687
+ const padY = 24;
5688
+ const usableW = width - padX * 2;
5689
+ const usableH = height - padY * 2;
5690
+ const max = Math.max(...points.map((p) => p.y), 1);
5691
+ const stepX = points.length > 1 ? usableW / (points.length - 1) : 0;
5692
+ const xy = points.map((p, i) => ({
5693
+ x: padX + i * stepX,
5694
+ y: padY + usableH - p.y / max * usableH,
5695
+ raw: p
5696
+ }));
5697
+ const path10 = xy.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
5698
+ const dots = xy.map((p) => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="${color}" />`).join("");
5699
+ const xLabels = xy.map((p, i) => {
5700
+ if (points.length > 8 && i % Math.ceil(points.length / 6) !== 0 && i !== points.length - 1) return "";
5701
+ return `<text x="${p.x.toFixed(1)}" y="${(height - 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="middle">${escapeHtml(p.raw.label ?? p.raw.x)}</text>`;
5702
+ }).join("");
5703
+ return `<div class="chart-card">
5704
+ <h3>${escapeHtml(title)}</h3>
5705
+ <svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${escapeHtml(title)} line chart">
5706
+ <line x1="${padX}" y1="${padY + usableH}" x2="${padX + usableW}" y2="${padY + usableH}" stroke="${COLORS.border}" stroke-width="1" />
5707
+ <text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatNumber(max)}</text>
5708
+ <text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">0</text>
5709
+ <path d="${path10}" stroke="${color}" stroke-width="2" fill="none" />
5710
+ ${dots}
5711
+ ${xLabels}
5712
+ </svg>
5713
+ </div>`;
5714
+ }
5715
+ function renderGsc(report) {
5716
+ const gsc = report.gsc;
5717
+ if (!gsc) {
5718
+ return section(
5719
+ { id: "gsc", eyebrow: "Section 5", title: "GSC Performance" },
5720
+ renderEmpty("Connect Google Search Console to populate this section.")
5721
+ );
5722
+ }
5723
+ const rows = gsc.topQueries.map((q) => `
5724
+ <tr>
5725
+ <td>${escapeHtml(q.query)}</td>
5726
+ <td class="numeric">${formatNumber(q.clicks)}</td>
5727
+ <td class="numeric">${formatNumber(q.impressions)}</td>
5728
+ <td class="numeric">${formatRatio(q.ctr)}</td>
5729
+ <td class="numeric">${q.avgPosition.toFixed(1)}</td>
5730
+ <td><span class="badge tone-neutral">${escapeHtml(q.category)}</span></td>
5731
+ </tr>`).join("");
5732
+ const breakdownRows = gsc.categoryBreakdown.map((c) => `
5733
+ <tr>
5734
+ <td>${escapeHtml(c.category)}</td>
5735
+ <td class="numeric">${formatNumber(c.clicks)}</td>
5736
+ <td class="numeric">${formatNumber(c.impressions)}</td>
5737
+ <td class="numeric">${c.sharePct}%</td>
5738
+ </tr>`).join("");
5739
+ const trendChart = renderLineChart(
5740
+ gsc.trend.map((t) => ({ x: t.date, y: t.clicks, label: t.date.slice(5) })),
5741
+ COLORS.accent,
5742
+ "Clicks over time"
5743
+ );
5744
+ return section(
5745
+ { id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro: "Top queries, category breakdown, and traffic trend from Google Search Console." },
5746
+ `<div class="metric-grid">
5747
+ <div class="metric"><div class="label">Total clicks</div><div class="value">${formatNumber(gsc.totalClicks)}</div></div>
5748
+ <div class="metric"><div class="label">Total impressions</div><div class="value">${formatNumber(gsc.totalImpressions)}</div></div>
5749
+ <div class="metric"><div class="label">Avg CTR</div><div class="value">${formatRatio(gsc.ctr)}</div></div>
5750
+ <div class="metric"><div class="label">Avg position</div><div class="value">${gsc.avgPosition.toFixed(1)}</div></div>
5751
+ </div>
5752
+ ${trendChart}
5753
+ <div class="chart-card"><h3>Top queries</h3>
5754
+ <table class="report-table">
5755
+ <thead><tr><th>Query</th><th class="numeric">Clicks</th><th class="numeric">Imp.</th><th class="numeric">CTR</th><th class="numeric">Pos.</th><th>Category</th></tr></thead>
5756
+ <tbody>${rows}</tbody>
5757
+ </table>
5758
+ </div>
5759
+ <div class="chart-card"><h3>Category breakdown</h3>
5760
+ <table class="report-table">
5761
+ <thead><tr><th>Category</th><th class="numeric">Clicks</th><th class="numeric">Imp.</th><th class="numeric">Share</th></tr></thead>
5762
+ <tbody>${breakdownRows}</tbody>
5763
+ </table>
5764
+ </div>`
5765
+ );
5766
+ }
5767
+ function renderGa(report) {
5768
+ const ga = report.ga;
5769
+ if (!ga) {
5770
+ return section(
5771
+ { id: "ga", eyebrow: "Section 6", title: "GA4 Traffic" },
5772
+ renderEmpty("Connect Google Analytics 4 to populate this section.")
5773
+ );
5774
+ }
5775
+ const pageRows = ga.topLandingPages.map((p) => `
5776
+ <tr>
5777
+ <td>${escapeHtml(p.page)}</td>
5778
+ <td class="numeric">${formatNumber(p.sessions)}</td>
5779
+ <td class="numeric">${formatNumber(p.users)}</td>
5780
+ <td class="numeric">${formatNumber(p.organicSessions)}</td>
5781
+ </tr>`).join("");
5782
+ const channelRows = ga.channelBreakdown.map((c) => `
5783
+ <tr>
5784
+ <td>${escapeHtml(c.channel)}</td>
5785
+ <td class="numeric">${formatNumber(c.sessions)}</td>
5786
+ <td class="numeric">${c.sharePct}%</td>
5787
+ </tr>`).join("");
5788
+ return section(
5789
+ { id: "ga", eyebrow: "Section 6", title: "GA4 Traffic", intro: `Sessions and users for ${formatDate(ga.periodStart)} \u2192 ${formatDate(ga.periodEnd)}.` },
5790
+ `<div class="metric-grid">
5791
+ <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ga.totalSessions)}</div></div>
5792
+ <div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ga.totalUsers)}</div></div>
5793
+ <div class="metric"><div class="label">Organic sessions</div><div class="value">${formatNumber(ga.totalOrganicSessions)}</div></div>
5794
+ </div>
5795
+ <div class="chart-card"><h3>Top landing pages</h3>
5796
+ <table class="report-table">
5797
+ <thead><tr><th>Page</th><th class="numeric">Sessions</th><th class="numeric">Users</th><th class="numeric">Organic</th></tr></thead>
5798
+ <tbody>${pageRows}</tbody>
5799
+ </table>
5800
+ </div>
5801
+ <div class="chart-card"><h3>Channel breakdown</h3>
5802
+ <table class="report-table">
5803
+ <thead><tr><th>Channel</th><th class="numeric">Sessions</th><th class="numeric">Share</th></tr></thead>
5804
+ <tbody>${channelRows}</tbody>
5805
+ </table>
5806
+ </div>`
5807
+ );
5808
+ }
5809
+ function renderSocial(report) {
5810
+ const social = report.socialReferrals;
5811
+ if (!social) {
5812
+ return section(
5813
+ { id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals" },
5814
+ renderEmpty("No social referral data yet.")
5815
+ );
5816
+ }
5817
+ const channelRows = social.channels.map((c) => `
5818
+ <tr>
5819
+ <td>${escapeHtml(c.channelGroup)}</td>
5820
+ <td class="numeric">${formatNumber(c.sessions)}</td>
5821
+ <td class="numeric">${c.sharePct}%</td>
5822
+ </tr>`).join("");
5823
+ const campaignRows = social.topCampaigns.map((c) => `
5824
+ <tr>
5825
+ <td>${escapeHtml(c.source)}</td>
5826
+ <td>${escapeHtml(c.medium)}</td>
5827
+ <td class="numeric">${formatNumber(c.sessions)}</td>
5828
+ </tr>`).join("");
5829
+ return section(
5830
+ { id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals", intro: "Paid vs organic split with top campaigns." },
5831
+ `<div class="metric-grid">
5832
+ <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(social.totalSessions)}</div></div>
5833
+ <div class="metric"><div class="label">Organic social</div><div class="value">${formatNumber(social.organicSessions)}</div></div>
5834
+ <div class="metric"><div class="label">Paid social</div><div class="value">${formatNumber(social.paidSessions)}</div></div>
5835
+ </div>
5836
+ <div class="chart-card"><h3>Channel groups</h3>
5837
+ <table class="report-table">
5838
+ <thead><tr><th>Channel</th><th class="numeric">Sessions</th><th class="numeric">Share</th></tr></thead>
5839
+ <tbody>${channelRows}</tbody>
5840
+ </table>
5841
+ </div>
5842
+ <div class="chart-card"><h3>Top campaigns</h3>
5843
+ <table class="report-table">
5844
+ <thead><tr><th>Source</th><th>Medium</th><th class="numeric">Sessions</th></tr></thead>
5845
+ <tbody>${campaignRows}</tbody>
5846
+ </table>
5847
+ </div>`
5848
+ );
5849
+ }
5850
+ function renderAiReferrals(report) {
5851
+ const ai = report.aiReferrals;
5852
+ if (!ai) {
5853
+ return section(
5854
+ { id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic" },
5855
+ renderEmpty("No AI referral traffic detected yet.")
5856
+ );
5857
+ }
5858
+ const sourceRows = ai.bySource.map((s) => `
5859
+ <tr>
5860
+ <td>${escapeHtml(s.source)}</td>
5861
+ <td class="numeric">${formatNumber(s.sessions)}</td>
5862
+ <td class="numeric">${formatNumber(s.users)}</td>
5863
+ <td class="numeric">${s.sharePct}%</td>
5864
+ </tr>`).join("");
5865
+ const pageRows = ai.topLandingPages.map((p) => `
5866
+ <tr>
5867
+ <td>${escapeHtml(p.page)}</td>
5868
+ <td class="numeric">${formatNumber(p.sessions)}</td>
5869
+ <td class="numeric">${formatNumber(p.users)}</td>
5870
+ </tr>`).join("");
5871
+ const trendChart = renderLineChart(
5872
+ ai.trend.map((t) => ({ x: t.date, y: t.sessions, label: t.date.slice(5) })),
5873
+ COLORS.series[2],
5874
+ "AI referral sessions over time"
5875
+ );
5876
+ return section(
5877
+ { id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic", intro: "Sessions sent from AI answer engines." },
5878
+ `<div class="metric-grid">
5879
+ <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ai.totalSessions)}</div></div>
5880
+ <div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ai.totalUsers)}</div></div>
5881
+ </div>
5882
+ ${trendChart}
5883
+ <div class="chart-card"><h3>Sessions by source</h3>
5884
+ <table class="report-table">
5885
+ <thead><tr><th>Source</th><th class="numeric">Sessions</th><th class="numeric">Users</th><th class="numeric">Share</th></tr></thead>
5886
+ <tbody>${sourceRows}</tbody>
5887
+ </table>
5888
+ </div>
5889
+ <div class="chart-card"><h3>Top AI landing pages</h3>
5890
+ <table class="report-table">
5891
+ <thead><tr><th>Page</th><th class="numeric">Sessions</th><th class="numeric">Users</th></tr></thead>
5892
+ <tbody>${pageRows}</tbody>
5893
+ </table>
5894
+ </div>`
5895
+ );
5896
+ }
5897
+ function renderIndexingHealth(report) {
5898
+ const ih = report.indexingHealth;
5899
+ if (!ih) {
5900
+ return section(
5901
+ { id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health" },
5902
+ renderEmpty("Connect Google Search Console or Bing Webmaster Tools and run a sitemap inspection.")
5903
+ );
5904
+ }
5905
+ const segments = [
5906
+ { label: "Indexed", count: ih.indexed, color: COLORS.positive },
5907
+ { label: "Not indexed", count: ih.notIndexed, color: COLORS.caution },
5908
+ { label: "Deindexed", count: ih.deindexed, color: COLORS.negative },
5909
+ { label: "Unknown", count: ih.unknown, color: COLORS.neutral }
5910
+ ].filter((s) => s.count > 0);
5911
+ const total = segments.reduce((s, x) => s + x.count, 0) || 1;
5912
+ const width = 600;
5913
+ const height = 28;
5914
+ let acc = 0;
5915
+ const bars = segments.map((s) => {
5916
+ const w = s.count / total * width;
5917
+ const x = acc;
5918
+ acc += w;
5919
+ return `<rect x="${x}" y="0" width="${w}" height="${height}" fill="${s.color}" />`;
5920
+ }).join("");
5921
+ const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml(s.label)}: ${s.count}</span>`).join("");
5922
+ return section(
5923
+ { id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health", intro: `Source: ${ih.provider === "google" ? "Google Search Console" : "Bing Webmaster Tools"}.` },
5924
+ `<div class="metric-grid">
5925
+ <div class="metric"><div class="label">Indexed</div><div class="value tone-positive">${formatNumber(ih.indexed)}</div></div>
5926
+ <div class="metric"><div class="label">Total inspected</div><div class="value">${formatNumber(ih.total)}</div></div>
5927
+ <div class="metric"><div class="label">Indexed share</div><div class="value">${ih.indexedPct}%</div></div>
5928
+ </div>
5929
+ <div class="chart-card">
5930
+ <h3>Coverage breakdown</h3>
5931
+ <svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="Coverage stacked bar">${bars}</svg>
5932
+ <div class="legend">${legend}</div>
5933
+ </div>`
5934
+ );
5935
+ }
5936
+ function renderCitationsTrend(report) {
5937
+ const trend = report.citationsTrend;
5938
+ if (trend.length === 0) {
5939
+ return section(
5940
+ { id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time" },
5941
+ renderEmpty("Run multiple visibility sweeps to see a trend.")
5942
+ );
5943
+ }
5944
+ const chart = renderLineChart(
5945
+ trend.map((t) => ({ x: t.date, y: t.citationRate, label: formatDate(t.date) })),
5946
+ COLORS.positive,
5947
+ "Overall citation rate",
5948
+ 220
5949
+ );
5950
+ const rows = trend.map((t) => `
5951
+ <tr>
5952
+ <td>${formatDate(t.date)}</td>
5953
+ <td class="numeric">${t.citationRate}%</td>
5954
+ <td>${t.providerRates.map((r) => `${escapeHtml(r.provider)}: ${r.citationRate}%`).join(" \xB7 ")}</td>
5955
+ </tr>`).join("");
5956
+ return section(
5957
+ { id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time", intro: "Per-run citation rate across the project history." },
5958
+ `${chart}
5959
+ <div class="chart-card"><h3>Run-by-run breakdown</h3>
5960
+ <table class="report-table">
5961
+ <thead><tr><th>Run</th><th class="numeric">Overall rate</th><th>Per-provider rates</th></tr></thead>
5962
+ <tbody>${rows}</tbody>
5963
+ </table>
5964
+ </div>`
5965
+ );
5966
+ }
5967
+ function renderInsights(report) {
5968
+ const list = report.insights;
5969
+ if (list.length === 0) {
5970
+ return section(
5971
+ { id: "insights", eyebrow: "Section 11", title: "Insights & Alerts" },
5972
+ renderEmpty("No insights yet \u2014 run a visibility sweep to generate alerts.")
5973
+ );
5974
+ }
5975
+ const rows = list.map((i) => {
5976
+ const tone = severityTone(i.severity);
5977
+ return `<tr>
5978
+ <td><span class="badge tone-${tone}">${escapeHtml(i.severity)}</span></td>
5979
+ <td>${escapeHtml(i.title)}</td>
5980
+ <td>${escapeHtml(i.keyword)}</td>
5981
+ <td>${escapeHtml(i.provider)}</td>
5982
+ <td>${i.recommendation ? escapeHtml(i.recommendation) : '<span class="cell-pending">\u2014</span>'}</td>
5983
+ </tr>`;
5984
+ }).join("");
5985
+ return section(
5986
+ { id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Priority-ordered findings from the most recent runs." },
5987
+ `<table class="report-table">
5988
+ <thead><tr><th>Severity</th><th>Title</th><th>Keyword</th><th>Provider</th><th>Recommendation</th></tr></thead>
5989
+ <tbody>${rows}</tbody>
5990
+ </table>`
5991
+ );
5992
+ }
5993
+ function renderRecommendedNextSteps(report) {
5994
+ const steps = report.recommendedNextSteps;
5995
+ if (steps.length === 0) {
5996
+ return section(
5997
+ { id: "recommended-next-steps", eyebrow: "Section 12", title: "Recommended Next Steps" },
5998
+ renderEmpty("No outstanding actions.")
5999
+ );
6000
+ }
6001
+ const items = steps.map((s) => `
6002
+ <div class="step">
6003
+ <span class="horizon">${escapeHtml(s.horizon)}</span>
6004
+ <span class="title">${escapeHtml(s.title)}</span>
6005
+ <span class="rationale">${escapeHtml(s.rationale)}</span>
6006
+ </div>`).join("");
6007
+ return section(
6008
+ { id: "recommended-next-steps", eyebrow: "Section 12", title: "Recommended Next Steps" },
6009
+ `<div class="steps">${items}</div>`
6010
+ );
6011
+ }
6012
+ function escapeJsonForScript(json) {
6013
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
6014
+ }
6015
+ function renderReportHtml(report, opts = {}) {
6016
+ const title = opts.title ?? `Canonry report \u2014 ${report.meta.project.displayName}`;
6017
+ const sections = [
6018
+ renderExecutiveSummary(report),
6019
+ renderCitationScorecard(report),
6020
+ renderCompetitorLandscape(report),
6021
+ renderAiSourceOrigin(report),
6022
+ renderGsc(report),
6023
+ renderGa(report),
6024
+ renderSocial(report),
6025
+ renderAiReferrals(report),
6026
+ renderIndexingHealth(report),
6027
+ renderCitationsTrend(report),
6028
+ renderInsights(report),
6029
+ renderRecommendedNextSteps(report)
6030
+ ].join("\n");
6031
+ const json = escapeJsonForScript(JSON.stringify(report));
6032
+ return `<!DOCTYPE html>
6033
+ <html lang="en">
6034
+ <head>
6035
+ <meta charset="utf-8" />
6036
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6037
+ <title>${escapeHtml(title)}</title>
6038
+ <style>${STYLE}</style>
6039
+ </head>
6040
+ <body>
6041
+ <div class="container">
6042
+ <header class="header">
6043
+ <div class="eyebrow">AEO Report</div>
6044
+ <h1>${escapeHtml(report.meta.project.displayName)}</h1>
6045
+ <div class="subtitle">${escapeHtml(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml(report.meta.project.country)} / ${escapeHtml(report.meta.project.language.toUpperCase())} \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
6046
+ </header>
6047
+ ${sections}
6048
+ <footer class="footer">Generated by canonry \xB7 ${escapeHtml(report.meta.generatedAt)}</footer>
6049
+ </div>
6050
+ <script type="application/json" id="canonry-report-data">${json}</script>
6051
+ </body>
6052
+ </html>`;
6053
+ }
6054
+
6055
+ // src/commands/report.ts
6056
+ function defaultOutputPath(project) {
6057
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6058
+ return path3.resolve(process.cwd(), `canonry-report-${project}-${date}.html`);
6059
+ }
6060
+ async function runReportCommand(project, opts = {}) {
6061
+ const client = createApiClient();
6062
+ const report = await client.getReport(project);
6063
+ if (opts.format === "json") {
6064
+ console.log(JSON.stringify(report, null, 2));
6065
+ return;
6066
+ }
6067
+ const html = renderReportHtml(report);
6068
+ const targetPath = opts.output ? path3.resolve(opts.output) : defaultOutputPath(project);
6069
+ const dir = path3.dirname(targetPath);
6070
+ if (!fs4.existsSync(dir)) {
6071
+ fs4.mkdirSync(dir, { recursive: true });
6072
+ }
6073
+ fs4.writeFileSync(targetPath, html, "utf-8");
6074
+ console.log(`Report written to ${targetPath}`);
6075
+ }
6076
+
6077
+ // src/cli-commands/report.ts
6078
+ var USAGE2 = "canonry report <project> [--output <path>] [--format json]";
6079
+ var REPORT_CLI_COMMANDS = [
6080
+ {
6081
+ path: ["report"],
6082
+ usage: USAGE2,
6083
+ options: {
6084
+ output: { type: "string", short: "o" }
6085
+ },
6086
+ run: async (input) => {
6087
+ const project = requireProject(input, "report", USAGE2);
6088
+ await runReportCommand(project, {
6089
+ format: input.format,
6090
+ output: getString(input.values, "output")
6091
+ });
6092
+ }
6093
+ }
6094
+ ];
6095
+
5177
6096
  // src/commands/run.ts
5178
6097
  function getClient14() {
5179
6098
  return createApiClient();
@@ -5882,19 +6801,19 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
5882
6801
  ];
5883
6802
 
5884
6803
  // src/commands/skills.ts
5885
- import fs4 from "fs";
5886
- import path3 from "path";
6804
+ import fs5 from "fs";
6805
+ import path4 from "path";
5887
6806
  import { fileURLToPath } from "url";
5888
6807
  var BUNDLED_SKILL_NAMES = ["canonry-setup", "aero"];
5889
6808
  function resolveBundledSkillsRoot(pkgDir) {
5890
- const here = pkgDir ?? path3.dirname(fileURLToPath(import.meta.url));
6809
+ const here = pkgDir ?? path4.dirname(fileURLToPath(import.meta.url));
5891
6810
  const candidates = [
5892
- path3.join(here, "../assets/agent-workspace/skills"),
5893
- path3.join(here, "../../assets/agent-workspace/skills"),
5894
- path3.join(here, "../../../../skills")
6811
+ path4.join(here, "../assets/agent-workspace/skills"),
6812
+ path4.join(here, "../../assets/agent-workspace/skills"),
6813
+ path4.join(here, "../../../../skills")
5895
6814
  ];
5896
6815
  for (const candidate of candidates) {
5897
- if (BUNDLED_SKILL_NAMES.every((name) => fs4.existsSync(path3.join(candidate, name, "SKILL.md")))) {
6816
+ if (BUNDLED_SKILL_NAMES.every((name) => fs5.existsSync(path4.join(candidate, name, "SKILL.md")))) {
5898
6817
  return candidate;
5899
6818
  }
5900
6819
  }
@@ -5915,17 +6834,17 @@ function parseDescription(content) {
5915
6834
  function getBundledSkills(pkgDir) {
5916
6835
  const root = resolveBundledSkillsRoot(pkgDir);
5917
6836
  return BUNDLED_SKILL_NAMES.map((name) => {
5918
- const skillDir = path3.join(root, name);
5919
- const skillFile = path3.join(skillDir, "SKILL.md");
5920
- const content = fs4.readFileSync(skillFile, "utf-8");
6837
+ const skillDir = path4.join(root, name);
6838
+ const skillFile = path4.join(skillDir, "SKILL.md");
6839
+ const content = fs5.readFileSync(skillFile, "utf-8");
5921
6840
  return { name, description: parseDescription(content), bundledPath: skillDir };
5922
6841
  });
5923
6842
  }
5924
6843
  function walkRelative(dir, prefix = "") {
5925
6844
  const out = [];
5926
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
5927
- const rel = prefix ? path3.join(prefix, entry.name) : entry.name;
5928
- const full = path3.join(dir, entry.name);
6845
+ for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
6846
+ const rel = prefix ? path4.join(prefix, entry.name) : entry.name;
6847
+ const full = path4.join(dir, entry.name);
5929
6848
  if (entry.isDirectory()) {
5930
6849
  out.push(...walkRelative(full, rel));
5931
6850
  } else if (entry.isFile()) {
@@ -5935,33 +6854,33 @@ function walkRelative(dir, prefix = "") {
5935
6854
  return out.sort();
5936
6855
  }
5937
6856
  function compareDirContent(srcDir, destDir) {
5938
- if (!fs4.existsSync(destDir)) return "missing";
5939
- if (!fs4.statSync(destDir).isDirectory()) return "different";
6857
+ if (!fs5.existsSync(destDir)) return "missing";
6858
+ if (!fs5.statSync(destDir).isDirectory()) return "different";
5940
6859
  const srcFiles = walkRelative(srcDir);
5941
6860
  const destFiles = walkRelative(destDir);
5942
6861
  if (srcFiles.length !== destFiles.length) return "different";
5943
6862
  for (let i = 0; i < srcFiles.length; i++) {
5944
6863
  if (srcFiles[i] !== destFiles[i]) return "different";
5945
- const srcBytes = fs4.readFileSync(path3.join(srcDir, srcFiles[i]));
5946
- const destBytes = fs4.readFileSync(path3.join(destDir, destFiles[i]));
6864
+ const srcBytes = fs5.readFileSync(path4.join(srcDir, srcFiles[i]));
6865
+ const destBytes = fs5.readFileSync(path4.join(destDir, destFiles[i]));
5947
6866
  if (!srcBytes.equals(destBytes)) return "different";
5948
6867
  }
5949
6868
  return "match";
5950
6869
  }
5951
6870
  function copyDirRecursive(src, dest) {
5952
- fs4.mkdirSync(dest, { recursive: true });
5953
- for (const entry of fs4.readdirSync(src, { withFileTypes: true })) {
5954
- const srcPath = path3.join(src, entry.name);
5955
- const destPath = path3.join(dest, entry.name);
6871
+ fs5.mkdirSync(dest, { recursive: true });
6872
+ for (const entry of fs5.readdirSync(src, { withFileTypes: true })) {
6873
+ const srcPath = path4.join(src, entry.name);
6874
+ const destPath = path4.join(dest, entry.name);
5956
6875
  if (entry.isDirectory()) {
5957
6876
  copyDirRecursive(srcPath, destPath);
5958
6877
  } else if (entry.isFile()) {
5959
- fs4.copyFileSync(srcPath, destPath);
6878
+ fs5.copyFileSync(srcPath, destPath);
5960
6879
  }
5961
6880
  }
5962
6881
  }
5963
6882
  function installClaudeSkill(skill, targetDir, force) {
5964
- const targetPath = path3.join(targetDir, ".claude", "skills", skill.name);
6883
+ const targetPath = path4.join(targetDir, ".claude", "skills", skill.name);
5965
6884
  const compare = compareDirContent(skill.bundledPath, targetPath);
5966
6885
  if (compare === "match") {
5967
6886
  return {
@@ -5981,7 +6900,7 @@ function installClaudeSkill(skill, targetDir, force) {
5981
6900
  });
5982
6901
  }
5983
6902
  if (compare === "different") {
5984
- fs4.rmSync(targetPath, { recursive: true, force: true });
6903
+ fs5.rmSync(targetPath, { recursive: true, force: true });
5985
6904
  }
5986
6905
  copyDirRecursive(skill.bundledPath, targetPath);
5987
6906
  return {
@@ -5993,18 +6912,18 @@ function installClaudeSkill(skill, targetDir, force) {
5993
6912
  };
5994
6913
  }
5995
6914
  function installCodexSymlink(skill, targetDir, force) {
5996
- const codexPath = path3.join(targetDir, ".codex", "skills", skill.name);
5997
- const claudePath = path3.join(targetDir, ".claude", "skills", skill.name);
5998
- const linkTarget = path3.relative(path3.dirname(codexPath), claudePath);
5999
- fs4.mkdirSync(path3.dirname(codexPath), { recursive: true });
6915
+ const codexPath = path4.join(targetDir, ".codex", "skills", skill.name);
6916
+ const claudePath = path4.join(targetDir, ".claude", "skills", skill.name);
6917
+ const linkTarget = path4.relative(path4.dirname(codexPath), claudePath);
6918
+ fs5.mkdirSync(path4.dirname(codexPath), { recursive: true });
6000
6919
  let stat;
6001
6920
  try {
6002
- stat = fs4.lstatSync(codexPath);
6921
+ stat = fs5.lstatSync(codexPath);
6003
6922
  } catch {
6004
6923
  stat = void 0;
6005
6924
  }
6006
6925
  if (stat?.isSymbolicLink()) {
6007
- const existing = fs4.readlinkSync(codexPath);
6926
+ const existing = fs5.readlinkSync(codexPath);
6008
6927
  if (existing === linkTarget) {
6009
6928
  return {
6010
6929
  skill: skill.name,
@@ -6022,8 +6941,8 @@ function installCodexSymlink(skill, targetDir, force) {
6022
6941
  exitCode: 1
6023
6942
  });
6024
6943
  }
6025
- fs4.unlinkSync(codexPath);
6026
- fs4.symlinkSync(linkTarget, codexPath);
6944
+ fs5.unlinkSync(codexPath);
6945
+ fs5.symlinkSync(linkTarget, codexPath);
6027
6946
  return {
6028
6947
  skill: skill.name,
6029
6948
  client: CodingAgents.codex,
@@ -6041,9 +6960,9 @@ function installCodexSymlink(skill, targetDir, force) {
6041
6960
  exitCode: 1
6042
6961
  });
6043
6962
  }
6044
- fs4.rmSync(codexPath, { recursive: true, force: true });
6963
+ fs5.rmSync(codexPath, { recursive: true, force: true });
6045
6964
  }
6046
- fs4.symlinkSync(linkTarget, codexPath);
6965
+ fs5.symlinkSync(linkTarget, codexPath);
6047
6966
  return {
6048
6967
  skill: skill.name,
6049
6968
  client: CodingAgents.codex,
@@ -6059,7 +6978,7 @@ function buildSummaryMessage(results) {
6059
6978
  return `Skills install summary: ${parts.join(", ")}.`;
6060
6979
  }
6061
6980
  async function installSkills(opts = {}) {
6062
- const targetDir = path3.resolve(opts.dir ?? process.cwd());
6981
+ const targetDir = path4.resolve(opts.dir ?? process.cwd());
6063
6982
  const client = opts.client ?? SkillsClients.all;
6064
6983
  const force = opts.force ?? false;
6065
6984
  const allSkills = getBundledSkills();
@@ -6075,7 +6994,7 @@ async function installSkills(opts = {}) {
6075
6994
  });
6076
6995
  }
6077
6996
  const skillsToInstall = allSkills.filter((s) => requestedNames.includes(s.name));
6078
- fs4.mkdirSync(targetDir, { recursive: true });
6997
+ fs5.mkdirSync(targetDir, { recursive: true });
6079
6998
  const results = [];
6080
6999
  for (const skill of skillsToInstall) {
6081
7000
  results.push(installClaudeSkill(skill, targetDir, force));
@@ -6176,12 +7095,12 @@ var SKILLS_CLI_COMMANDS = [
6176
7095
  ];
6177
7096
 
6178
7097
  // src/commands/snapshot.ts
6179
- import fs6 from "fs";
6180
- import path5 from "path";
7098
+ import fs7 from "fs";
7099
+ import path6 from "path";
6181
7100
 
6182
7101
  // src/snapshot-pdf.ts
6183
- import fs5 from "fs";
6184
- import path4 from "path";
7102
+ import fs6 from "fs";
7103
+ import path5 from "path";
6185
7104
  import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
6186
7105
  var PAGE_WIDTH = 612;
6187
7106
  var PAGE_HEIGHT = 792;
@@ -6390,9 +7309,9 @@ async function writeSnapshotPdf(report, outputPath) {
6390
7309
  renderCompetitors(pdf, report);
6391
7310
  renderQueries(pdf, report);
6392
7311
  const bytes = await doc.save();
6393
- const resolvedPath = path4.resolve(outputPath);
6394
- fs5.mkdirSync(path4.dirname(resolvedPath), { recursive: true });
6395
- fs5.writeFileSync(resolvedPath, bytes);
7312
+ const resolvedPath = path5.resolve(outputPath);
7313
+ fs6.mkdirSync(path5.dirname(resolvedPath), { recursive: true });
7314
+ fs6.writeFileSync(resolvedPath, bytes);
6396
7315
  return resolvedPath;
6397
7316
  }
6398
7317
  function renderCover(pdf, report) {
@@ -6550,9 +7469,9 @@ Markdown saved: ${savedMdPath}`);
6550
7469
  PDF saved: ${savedPdfPath}`);
6551
7470
  }
6552
7471
  function writeSnapshotMarkdown(report, outputPath) {
6553
- const resolvedPath = path5.resolve(outputPath);
6554
- fs6.mkdirSync(path5.dirname(resolvedPath), { recursive: true });
6555
- fs6.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
7472
+ const resolvedPath = path6.resolve(outputPath);
7473
+ fs7.mkdirSync(path6.dirname(resolvedPath), { recursive: true });
7474
+ fs7.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
6556
7475
  return resolvedPath;
6557
7476
  }
6558
7477
  function formatSnapshotMarkdown(report) {
@@ -7207,7 +8126,7 @@ var CONTENT_CLI_COMMANDS = [
7207
8126
 
7208
8127
  // src/commands/bootstrap.ts
7209
8128
  import crypto from "crypto";
7210
- import path6 from "path";
8129
+ import path7 from "path";
7211
8130
  import { eq as eq2 } from "drizzle-orm";
7212
8131
 
7213
8132
  // ../config/src/index.ts
@@ -7354,7 +8273,7 @@ async function bootstrapCommand(_opts) {
7354
8273
  );
7355
8274
  }
7356
8275
  const configDir = getConfigDir();
7357
- const databasePath = env.databasePath || path6.join(configDir, "data.db");
8276
+ const databasePath = env.databasePath || path7.join(configDir, "data.db");
7358
8277
  const existing = configExists();
7359
8278
  const existingConfig = existing ? loadConfig() : void 0;
7360
8279
  let rawApiKey;
@@ -7424,10 +8343,10 @@ async function bootstrapCommand(_opts) {
7424
8343
 
7425
8344
  // src/commands/daemon.ts
7426
8345
  import { spawn } from "child_process";
7427
- import fs7 from "fs";
7428
- import path7 from "path";
8346
+ import fs8 from "fs";
8347
+ import path8 from "path";
7429
8348
  function getPidPath() {
7430
- return path7.join(getConfigDir(), "canonry.pid");
8349
+ return path8.join(getConfigDir(), "canonry.pid");
7431
8350
  }
7432
8351
  function isProcessAlive(pid) {
7433
8352
  try {
@@ -7454,8 +8373,8 @@ async function waitForReady(host, port, maxMs = 1e4) {
7454
8373
  async function startDaemon(opts) {
7455
8374
  const pidPath = getPidPath();
7456
8375
  const format = opts.format ?? "text";
7457
- if (fs7.existsSync(pidPath)) {
7458
- const existingPid = parseInt(fs7.readFileSync(pidPath, "utf-8").trim(), 10);
8376
+ if (fs8.existsSync(pidPath)) {
8377
+ const existingPid = parseInt(fs8.readFileSync(pidPath, "utf-8").trim(), 10);
7459
8378
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
7460
8379
  throw new CliError({
7461
8380
  code: "DAEMON_ALREADY_RUNNING",
@@ -7466,9 +8385,9 @@ async function startDaemon(opts) {
7466
8385
  }
7467
8386
  });
7468
8387
  }
7469
- fs7.unlinkSync(pidPath);
8388
+ fs8.unlinkSync(pidPath);
7470
8389
  }
7471
- const cliPath = path7.resolve(new URL(import.meta.url).pathname);
8390
+ const cliPath = path8.resolve(new URL(import.meta.url).pathname);
7472
8391
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
7473
8392
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
7474
8393
  if (opts.port) args.push("--port", opts.port);
@@ -7487,10 +8406,10 @@ async function startDaemon(opts) {
7487
8406
  });
7488
8407
  }
7489
8408
  const configDir = getConfigDir();
7490
- if (!fs7.existsSync(configDir)) {
7491
- fs7.mkdirSync(configDir, { recursive: true });
8409
+ if (!fs8.existsSync(configDir)) {
8410
+ fs8.mkdirSync(configDir, { recursive: true });
7492
8411
  }
7493
- fs7.writeFileSync(pidPath, String(child.pid), "utf-8");
8412
+ fs8.writeFileSync(pidPath, String(child.pid), "utf-8");
7494
8413
  const port = opts.port ?? "4100";
7495
8414
  const host = opts.host ?? "127.0.0.1";
7496
8415
  if (format !== "json") {
@@ -7499,7 +8418,7 @@ async function startDaemon(opts) {
7499
8418
  const ready = await waitForReady(host, port);
7500
8419
  if (!ready) {
7501
8420
  try {
7502
- fs7.unlinkSync(pidPath);
8421
+ fs8.unlinkSync(pidPath);
7503
8422
  } catch {
7504
8423
  }
7505
8424
  throw new CliError({
@@ -7531,7 +8450,7 @@ async function startDaemon(opts) {
7531
8450
  }
7532
8451
  function stopDaemon(format = "text") {
7533
8452
  const pidPath = getPidPath();
7534
- if (!fs7.existsSync(pidPath)) {
8453
+ if (!fs8.existsSync(pidPath)) {
7535
8454
  if (format === "json") {
7536
8455
  console.log(JSON.stringify({
7537
8456
  stopped: false,
@@ -7542,7 +8461,7 @@ function stopDaemon(format = "text") {
7542
8461
  console.log("Canonry is not running (no PID file found)");
7543
8462
  return;
7544
8463
  }
7545
- const pid = parseInt(fs7.readFileSync(pidPath, "utf-8").trim(), 10);
8464
+ const pid = parseInt(fs8.readFileSync(pidPath, "utf-8").trim(), 10);
7546
8465
  if (isNaN(pid)) {
7547
8466
  if (format === "json") {
7548
8467
  console.log(JSON.stringify({
@@ -7553,7 +8472,7 @@ function stopDaemon(format = "text") {
7553
8472
  } else {
7554
8473
  console.error("Invalid PID file. Removing it.");
7555
8474
  }
7556
- fs7.unlinkSync(pidPath);
8475
+ fs8.unlinkSync(pidPath);
7557
8476
  return;
7558
8477
  }
7559
8478
  if (!isProcessAlive(pid)) {
@@ -7567,12 +8486,12 @@ function stopDaemon(format = "text") {
7567
8486
  } else {
7568
8487
  console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
7569
8488
  }
7570
- fs7.unlinkSync(pidPath);
8489
+ fs8.unlinkSync(pidPath);
7571
8490
  return;
7572
8491
  }
7573
8492
  try {
7574
8493
  process.kill(pid, "SIGTERM");
7575
- fs7.unlinkSync(pidPath);
8494
+ fs8.unlinkSync(pidPath);
7576
8495
  if (format === "json") {
7577
8496
  console.log(JSON.stringify({
7578
8497
  stopped: true,
@@ -7596,9 +8515,9 @@ function stopDaemon(format = "text") {
7596
8515
 
7597
8516
  // src/commands/init.ts
7598
8517
  import crypto2 from "crypto";
7599
- import fs8 from "fs";
8518
+ import fs9 from "fs";
7600
8519
  import readline from "readline";
7601
- import path8 from "path";
8520
+ import path9 from "path";
7602
8521
  function prompt(question) {
7603
8522
  const rl = readline.createInterface({
7604
8523
  input: process.stdin,
@@ -7619,8 +8538,8 @@ var DEFAULT_QUOTA = {
7619
8538
  var PROJECT_MARKERS = [".git", "canonry.yaml", "canonry.yml", "package.json"];
7620
8539
  function cwdLooksLikeProject(dir) {
7621
8540
  const home = process.env.HOME ?? "";
7622
- if (home && path8.resolve(dir) === path8.resolve(home)) return false;
7623
- return PROJECT_MARKERS.some((marker) => fs8.existsSync(path8.join(dir, marker)));
8541
+ if (home && path9.resolve(dir) === path9.resolve(home)) return false;
8542
+ return PROJECT_MARKERS.some((marker) => fs9.existsSync(path9.join(dir, marker)));
7624
8543
  }
7625
8544
  var DEFAULT_AGENT_MODELS = {
7626
8545
  anthropic: "anthropic/claude-sonnet-4-6",
@@ -7650,8 +8569,8 @@ async function initCommand(opts) {
7650
8569
  return void 0;
7651
8570
  }
7652
8571
  const configDir = getConfigDir();
7653
- if (!fs8.existsSync(configDir)) {
7654
- fs8.mkdirSync(configDir, { recursive: true });
8572
+ if (!fs9.existsSync(configDir)) {
8573
+ fs9.mkdirSync(configDir, { recursive: true });
7655
8574
  }
7656
8575
  const bootstrapEnv = getBootstrapEnv(process.env, {
7657
8576
  GEMINI_API_KEY: opts?.geminiKey,
@@ -7766,7 +8685,7 @@ async function initCommand(opts) {
7766
8685
  const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
7767
8686
  const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
7768
8687
  const keyPrefix = rawApiKey.slice(0, 9);
7769
- const databasePath = path8.join(configDir, "data.db");
8688
+ const databasePath = path9.join(configDir, "data.db");
7770
8689
  const db = createClient(databasePath);
7771
8690
  migrate(db);
7772
8691
  db.insert(apiKeys).values({
@@ -8214,7 +9133,7 @@ var SYSTEM_CLI_COMMANDS = [
8214
9133
  ];
8215
9134
 
8216
9135
  // src/cli-commands/wordpress.ts
8217
- import fs9 from "fs";
9136
+ import fs10 from "fs";
8218
9137
 
8219
9138
  // src/commands/wordpress.ts
8220
9139
  function getClient18() {
@@ -8450,12 +9369,12 @@ async function wordpressSetMeta(project, body) {
8450
9369
  printPageDetail(result);
8451
9370
  }
8452
9371
  async function wordpressBulkSetMeta(project, opts) {
8453
- const fs10 = await import("fs/promises");
8454
- const path9 = await import("path");
8455
- const filePath = path9.resolve(opts.from);
9372
+ const fs11 = await import("fs/promises");
9373
+ const path10 = await import("path");
9374
+ const filePath = path10.resolve(opts.from);
8456
9375
  let raw;
8457
9376
  try {
8458
- raw = await fs10.readFile(filePath, "utf8");
9377
+ raw = await fs11.readFile(filePath, "utf8");
8459
9378
  } catch {
8460
9379
  throw new CliError({
8461
9380
  code: "FILE_READ_ERROR",
@@ -8552,13 +9471,13 @@ async function wordpressSetSchema(project, body) {
8552
9471
  printManualAssist(`Schema update for "${body.slug}"`, result);
8553
9472
  }
8554
9473
  async function wordpressSchemaDeploy(project, opts) {
8555
- const fs10 = await import("fs/promises");
8556
- const path9 = await import("path");
9474
+ const fs11 = await import("fs/promises");
9475
+ const path10 = await import("path");
8557
9476
  const yaml = await import("yaml").catch(() => null);
8558
- const filePath = path9.resolve(opts.profile);
9477
+ const filePath = path10.resolve(opts.profile);
8559
9478
  let raw;
8560
9479
  try {
8561
- raw = await fs10.readFile(filePath, "utf8");
9480
+ raw = await fs11.readFile(filePath, "utf8");
8562
9481
  } catch {
8563
9482
  throw new CliError({
8564
9483
  code: "FILE_READ_ERROR",
@@ -8663,13 +9582,13 @@ async function wordpressOnboard(project, opts) {
8663
9582
  }
8664
9583
  let profileData;
8665
9584
  if (opts.profile) {
8666
- const fs10 = await import("fs/promises");
8667
- const path9 = await import("path");
9585
+ const fs11 = await import("fs/promises");
9586
+ const path10 = await import("path");
8668
9587
  const yaml = await import("yaml").catch(() => null);
8669
- const filePath = path9.resolve(opts.profile);
9588
+ const filePath = path10.resolve(opts.profile);
8670
9589
  let raw;
8671
9590
  try {
8672
- raw = await fs10.readFile(filePath, "utf8");
9591
+ raw = await fs11.readFile(filePath, "utf8");
8673
9592
  } catch {
8674
9593
  throw new CliError({
8675
9594
  code: "FILE_READ_ERROR",
@@ -8818,7 +9737,7 @@ function resolveContent(input, command, usage, options) {
8818
9737
  }
8819
9738
  if (contentFile) {
8820
9739
  try {
8821
- return fs9.readFileSync(contentFile, "utf-8");
9740
+ return fs10.readFileSync(contentFile, "utf-8");
8822
9741
  } catch (error) {
8823
9742
  const message = error instanceof Error ? error.message : String(error);
8824
9743
  throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
@@ -9749,6 +10668,7 @@ var REGISTERED_CLI_COMMANDS = [
9749
10668
  ...BACKLINKS_CLI_COMMANDS,
9750
10669
  ...SYSTEM_CLI_COMMANDS,
9751
10670
  ...PROJECT_CLI_COMMANDS,
10671
+ ...REPORT_CLI_COMMANDS,
9752
10672
  ...KEYWORD_CLI_COMMANDS,
9753
10673
  ...COMPETITOR_CLI_COMMANDS,
9754
10674
  ...SETTINGS_CLI_COMMANDS,
@@ -9772,7 +10692,7 @@ var REGISTERED_CLI_COMMANDS = [
9772
10692
 
9773
10693
  // src/cli.ts
9774
10694
  import { createRequire as createRequire2 } from "module";
9775
- var USAGE2 = `
10695
+ var USAGE3 = `
9776
10696
  canonry \u2014 AEO monitoring CLI
9777
10697
 
9778
10698
  Usage: canonry <command> [options]
@@ -9833,7 +10753,7 @@ function extractFormat(cmdArgs) {
9833
10753
  }
9834
10754
  async function runCli(args = process.argv.slice(2)) {
9835
10755
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
9836
- console.log(USAGE2);
10756
+ console.log(USAGE3);
9837
10757
  return 0;
9838
10758
  }
9839
10759
  if (args.includes("--version") || args.includes("-v")) {