@ainyc/canonry 3.3.3 → 3.3.9

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,988 @@ 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 summarizeQueryParams(params) {
5210
+ const keys = Array.from(params.keys());
5211
+ const total = keys.length;
5212
+ if (total === 0) return "";
5213
+ const noun = total === 1 ? "param" : "params";
5214
+ const tag = inferAdSource(params);
5215
+ return tag ? `${tag} \xB7 ${total} ${noun}` : `${total} tracking ${noun}`;
5216
+ }
5217
+ function inferAdSource(params) {
5218
+ if (params.has("fbclid")) return "Facebook Ad";
5219
+ if (params.has("gclid") || params.has("gbraid") || params.has("wbraid")) return "Google Ad";
5220
+ if (params.has("msclkid")) return "Microsoft Ad";
5221
+ if (params.has("ttclid")) return "TikTok Ad";
5222
+ if (params.has("li_fat_id")) return "LinkedIn Ad";
5223
+ if (params.has("twclid")) return "X / Twitter Ad";
5224
+ if (params.has("epik")) return "Pinterest Ad";
5225
+ for (const k of params.keys()) {
5226
+ if (k.startsWith("hsa_")) return "Search Ad";
5227
+ }
5228
+ const src = params.get("utm_source");
5229
+ const med = params.get("utm_medium");
5230
+ if (src && med) return `${src} / ${med}`;
5231
+ if (src) return `Source: ${src}`;
5232
+ if (med) return `Medium: ${med}`;
5233
+ return null;
5234
+ }
5235
+ function formatLandingPageHtml(raw) {
5236
+ const value = raw ?? "";
5237
+ const queryIdx = value.indexOf("?");
5238
+ const path10 = queryIdx === -1 ? value : value.slice(0, queryIdx);
5239
+ const query = queryIdx === -1 ? "" : value.slice(queryIdx + 1);
5240
+ const pathHtml = `<span class="page-path">${escapeHtml(path10 || "/")}</span>`;
5241
+ if (!query) return pathHtml;
5242
+ let summary = "";
5243
+ try {
5244
+ summary = summarizeQueryParams(new URLSearchParams(query));
5245
+ } catch {
5246
+ summary = "tracking params";
5247
+ }
5248
+ if (!summary) return pathHtml;
5249
+ return `${pathHtml}<span class="page-query" title="${escapeHtml(value)}">${escapeHtml(summary)}</span>`;
5250
+ }
5251
+ function formatDate(iso) {
5252
+ if (!iso) return "\u2014";
5253
+ try {
5254
+ const d = new Date(iso);
5255
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
5256
+ } catch {
5257
+ return iso;
5258
+ }
5259
+ }
5260
+ function pressureTone(label) {
5261
+ if (label === "High") return "negative";
5262
+ if (label === "Moderate") return "caution";
5263
+ if (label === "Low") return "positive";
5264
+ return "neutral";
5265
+ }
5266
+ function severityTone(severity) {
5267
+ switch (severity) {
5268
+ case "critical":
5269
+ return "negative";
5270
+ case "high":
5271
+ return "negative";
5272
+ case "medium":
5273
+ return "caution";
5274
+ case "low":
5275
+ return "neutral";
5276
+ }
5277
+ }
5278
+ var STYLE = `
5279
+ :root {
5280
+ color-scheme: dark;
5281
+ }
5282
+ * { box-sizing: border-box; }
5283
+ html, body { margin: 0; padding: 0; }
5284
+ body {
5285
+ background: ${COLORS.bg};
5286
+ color: ${COLORS.text};
5287
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
5288
+ font-size: 14px;
5289
+ line-height: 1.5;
5290
+ -webkit-font-smoothing: antialiased;
5291
+ }
5292
+ .container {
5293
+ max-width: 1100px;
5294
+ margin: 0 auto;
5295
+ padding: 48px 24px 96px;
5296
+ }
5297
+ .header {
5298
+ border-bottom: 1px solid ${COLORS.border};
5299
+ padding-bottom: 32px;
5300
+ margin-bottom: 48px;
5301
+ }
5302
+ .header h1 {
5303
+ font-size: 32px;
5304
+ font-weight: 700;
5305
+ margin: 0 0 8px;
5306
+ letter-spacing: -0.02em;
5307
+ }
5308
+ .header .subtitle {
5309
+ color: ${COLORS.textMuted};
5310
+ font-size: 14px;
5311
+ }
5312
+ .eyebrow {
5313
+ text-transform: uppercase;
5314
+ letter-spacing: 0.08em;
5315
+ font-size: 10px;
5316
+ color: ${COLORS.textFaint};
5317
+ font-weight: 600;
5318
+ margin-bottom: 8px;
5319
+ }
5320
+ section.report-section {
5321
+ margin: 64px 0;
5322
+ }
5323
+ section.report-section h2 {
5324
+ font-size: 22px;
5325
+ font-weight: 700;
5326
+ margin: 0 0 24px;
5327
+ letter-spacing: -0.01em;
5328
+ }
5329
+ section.report-section .section-intro {
5330
+ color: ${COLORS.textMuted};
5331
+ margin-bottom: 24px;
5332
+ }
5333
+ .metric-grid {
5334
+ display: grid;
5335
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
5336
+ gap: 16px;
5337
+ }
5338
+ .metric {
5339
+ background: ${COLORS.surface};
5340
+ border: 1px solid ${COLORS.border};
5341
+ border-radius: 8px;
5342
+ padding: 16px 20px;
5343
+ }
5344
+ .metric .label {
5345
+ text-transform: uppercase;
5346
+ letter-spacing: 0.08em;
5347
+ font-size: 10px;
5348
+ color: ${COLORS.textFaint};
5349
+ font-weight: 600;
5350
+ margin-bottom: 8px;
5351
+ }
5352
+ .metric .value {
5353
+ font-size: 28px;
5354
+ font-weight: 700;
5355
+ letter-spacing: -0.02em;
5356
+ }
5357
+ .metric .delta {
5358
+ font-size: 12px;
5359
+ color: ${COLORS.textMuted};
5360
+ margin-top: 4px;
5361
+ }
5362
+ .findings {
5363
+ margin-top: 24px;
5364
+ display: grid;
5365
+ gap: 12px;
5366
+ }
5367
+ .finding {
5368
+ background: ${COLORS.surface};
5369
+ border: 1px solid ${COLORS.border};
5370
+ border-left-width: 3px;
5371
+ border-radius: 6px;
5372
+ padding: 12px 16px;
5373
+ }
5374
+ .finding.tone-positive { border-left-color: ${COLORS.positive}; }
5375
+ .finding.tone-caution { border-left-color: ${COLORS.caution}; }
5376
+ .finding.tone-negative { border-left-color: ${COLORS.negative}; }
5377
+ .finding.tone-neutral { border-left-color: ${COLORS.neutral}; }
5378
+ .finding strong { display: block; margin-bottom: 4px; }
5379
+ .finding span { color: ${COLORS.textMuted}; font-size: 13px; }
5380
+ table.report-table {
5381
+ width: 100%;
5382
+ border-collapse: collapse;
5383
+ font-size: 13px;
5384
+ }
5385
+ table.report-table th, table.report-table td {
5386
+ text-align: left;
5387
+ padding: 10px 12px;
5388
+ border-bottom: 1px solid ${COLORS.border};
5389
+ vertical-align: top;
5390
+ overflow-wrap: anywhere;
5391
+ word-break: break-word;
5392
+ }
5393
+ table.report-table th {
5394
+ font-weight: 600;
5395
+ color: ${COLORS.textMuted};
5396
+ text-transform: uppercase;
5397
+ letter-spacing: 0.06em;
5398
+ font-size: 10px;
5399
+ }
5400
+ table.report-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
5401
+ table.report-table td.page-cell { max-width: 0; }
5402
+ table.report-table td.page-cell .page-path {
5403
+ display: block;
5404
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
5405
+ font-size: 12px;
5406
+ color: ${COLORS.text};
5407
+ }
5408
+ table.report-table td.page-cell .page-query {
5409
+ display: inline-block;
5410
+ margin-top: 4px;
5411
+ padding: 1px 8px;
5412
+ font-size: 11px;
5413
+ color: ${COLORS.textMuted};
5414
+ background: ${COLORS.surface};
5415
+ border: 1px solid ${COLORS.border};
5416
+ border-radius: 999px;
5417
+ cursor: help;
5418
+ }
5419
+ table.report-table td .badge {
5420
+ display: inline-block;
5421
+ padding: 2px 8px;
5422
+ border-radius: 999px;
5423
+ font-size: 11px;
5424
+ font-weight: 600;
5425
+ border: 1px solid;
5426
+ }
5427
+ .cell-cited { color: ${COLORS.positive}; font-weight: 600; }
5428
+ .cell-not-cited { color: ${COLORS.textFaint}; }
5429
+ .cell-pending { color: ${COLORS.textFaint}; font-style: italic; }
5430
+ .tone-positive { color: ${COLORS.positive}; }
5431
+ .tone-caution { color: ${COLORS.caution}; }
5432
+ .tone-negative { color: ${COLORS.negative}; }
5433
+ .tone-neutral { color: ${COLORS.neutral}; }
5434
+ .badge.tone-positive { color: ${COLORS.positive}; border-color: ${COLORS.positive}40; background: ${COLORS.positive}14; }
5435
+ .badge.tone-caution { color: ${COLORS.caution}; border-color: ${COLORS.caution}40; background: ${COLORS.caution}14; }
5436
+ .badge.tone-negative { color: ${COLORS.negative}; border-color: ${COLORS.negative}40; background: ${COLORS.negative}14; }
5437
+ .badge.tone-neutral { color: ${COLORS.textMuted}; border-color: ${COLORS.border}; background: transparent; }
5438
+ .chart-card {
5439
+ background: ${COLORS.surface};
5440
+ border: 1px solid ${COLORS.border};
5441
+ border-radius: 8px;
5442
+ padding: 20px;
5443
+ margin-bottom: 16px;
5444
+ }
5445
+ .chart-card h3 {
5446
+ font-size: 14px;
5447
+ font-weight: 600;
5448
+ margin: 0 0 16px;
5449
+ }
5450
+ .chart-grid {
5451
+ display: grid;
5452
+ grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
5453
+ gap: 16px;
5454
+ }
5455
+ .legend {
5456
+ display: flex;
5457
+ flex-wrap: wrap;
5458
+ gap: 12px;
5459
+ font-size: 12px;
5460
+ margin-top: 12px;
5461
+ }
5462
+ .legend-swatch {
5463
+ display: inline-block;
5464
+ width: 10px;
5465
+ height: 10px;
5466
+ border-radius: 2px;
5467
+ margin-right: 6px;
5468
+ vertical-align: middle;
5469
+ }
5470
+ .empty-state {
5471
+ background: ${COLORS.surface};
5472
+ border: 1px dashed ${COLORS.border};
5473
+ border-radius: 8px;
5474
+ padding: 32px;
5475
+ color: ${COLORS.textMuted};
5476
+ text-align: center;
5477
+ font-size: 13px;
5478
+ }
5479
+ .steps {
5480
+ display: grid;
5481
+ gap: 12px;
5482
+ }
5483
+ .step {
5484
+ background: ${COLORS.surface};
5485
+ border: 1px solid ${COLORS.border};
5486
+ border-radius: 8px;
5487
+ padding: 16px 20px;
5488
+ display: grid;
5489
+ gap: 4px;
5490
+ }
5491
+ .step .horizon {
5492
+ text-transform: uppercase;
5493
+ font-size: 10px;
5494
+ letter-spacing: 0.08em;
5495
+ color: ${COLORS.textFaint};
5496
+ font-weight: 600;
5497
+ }
5498
+ .step .title { font-weight: 600; }
5499
+ .step .rationale { color: ${COLORS.textMuted}; font-size: 13px; }
5500
+ .footer {
5501
+ margin-top: 96px;
5502
+ padding-top: 24px;
5503
+ border-top: 1px solid ${COLORS.border};
5504
+ text-align: center;
5505
+ color: ${COLORS.textFaint};
5506
+ font-size: 12px;
5507
+ }
5508
+ @media print {
5509
+ body { background: white; color: black; }
5510
+ section.report-section { break-inside: avoid; }
5511
+ }
5512
+ `;
5513
+ function section(opts, body) {
5514
+ return `<section class="report-section" id="${escapeHtml(opts.id)}">
5515
+ <div class="eyebrow">${escapeHtml(opts.eyebrow)}</div>
5516
+ <h2>${escapeHtml(opts.title)}</h2>
5517
+ ${opts.intro ? `<p class="section-intro">${escapeHtml(opts.intro)}</p>` : ""}
5518
+ ${body}
5519
+ </section>`;
5520
+ }
5521
+ function renderEmpty(message) {
5522
+ return `<div class="empty-state">${escapeHtml(message)}</div>`;
5523
+ }
5524
+ function renderExecutiveSummary(report) {
5525
+ const s = report.executiveSummary;
5526
+ const trendLabel = s.trend === "up" ? "\u2191 Up" : s.trend === "down" ? "\u2193 Down" : s.trend === "flat" ? "\u2192 Flat" : "\u2014";
5527
+ const trendTone = s.trend === "up" ? "positive" : s.trend === "down" ? "negative" : "neutral";
5528
+ const metrics = [
5529
+ {
5530
+ label: "Citation rate",
5531
+ value: `${s.citationRate}%`,
5532
+ delta: `<span class="tone-${trendTone}">${trendLabel}</span> \xB7 ${s.providerCount} provider${s.providerCount === 1 ? "" : "s"}`
5533
+ },
5534
+ {
5535
+ label: "Keywords tracked",
5536
+ value: formatNumber(s.keywordCount),
5537
+ delta: `${s.competitorCount} competitor${s.competitorCount === 1 ? "" : "s"} tracked`
5538
+ }
5539
+ ];
5540
+ if (s.gsc) {
5541
+ metrics.push({
5542
+ label: "GSC clicks",
5543
+ value: formatNumber(s.gsc.clicks),
5544
+ delta: `${formatNumber(s.gsc.impressions)} imp \xB7 ${formatRatio(s.gsc.ctr)} CTR`
5545
+ });
5546
+ }
5547
+ if (s.ga) {
5548
+ metrics.push({
5549
+ label: "GA sessions",
5550
+ value: formatNumber(s.ga.sessions),
5551
+ delta: `${formatNumber(s.ga.users)} users \xB7 ${formatDate(s.ga.periodStart)} \u2192 ${formatDate(s.ga.periodEnd)}`
5552
+ });
5553
+ }
5554
+ const metricsHtml = `<div class="metric-grid">
5555
+ ${metrics.map((m) => `<div class="metric">
5556
+ <div class="label">${escapeHtml(m.label)}</div>
5557
+ <div class="value">${m.value}</div>
5558
+ <div class="delta">${m.delta}</div>
5559
+ </div>`).join("")}
5560
+ </div>`;
5561
+ const findingsHtml = s.findings.length > 0 ? `<div class="findings">${s.findings.map((f) => `
5562
+ <div class="finding tone-${f.tone}">
5563
+ <strong>${escapeHtml(f.title)}</strong>
5564
+ <span>${escapeHtml(f.detail)}</span>
5565
+ </div>`).join("")}</div>` : "";
5566
+ return section(
5567
+ { id: "executive-summary", eyebrow: "Section 1", title: "Executive Summary" },
5568
+ metricsHtml + findingsHtml
5569
+ );
5570
+ }
5571
+ function renderProviderBars(rates) {
5572
+ if (rates.length === 0) return "";
5573
+ const max = Math.max(...rates.map((r) => r.citationRate), 100);
5574
+ const width = 600;
5575
+ const height = Math.max(rates.length * 32 + 24, 80);
5576
+ const labelWidth = 80;
5577
+ const padding = 8;
5578
+ const barWidth = width - labelWidth - padding * 2;
5579
+ const bars = rates.map((r, i) => {
5580
+ const y = i * 32 + padding;
5581
+ const barHeight = 22;
5582
+ const w = max > 0 ? r.citationRate / max * barWidth : 0;
5583
+ const color = COLORS.series[i % COLORS.series.length];
5584
+ return `
5585
+ <text x="${labelWidth - 8}" y="${y + 16}" fill="${COLORS.textMuted}" font-size="11" text-anchor="end">${escapeHtml(r.provider)}</text>
5586
+ <rect x="${labelWidth}" y="${y}" width="${barWidth}" height="${barHeight}" fill="${COLORS.border}" opacity="0.4" rx="3" />
5587
+ <rect x="${labelWidth}" y="${y}" width="${w}" height="${barHeight}" fill="${color}" rx="3" />
5588
+ <text x="${labelWidth + w + 6}" y="${y + 16}" fill="${COLORS.text}" font-size="11">${r.citationRate}% (${r.citedCount}/${r.totalCount})</text>`;
5589
+ }).join("");
5590
+ return `<div class="chart-card">
5591
+ <h3>Provider citation rate</h3>
5592
+ <svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="Provider citation rate bar chart">
5593
+ ${bars}
5594
+ </svg>
5595
+ </div>`;
5596
+ }
5597
+ function renderCitationMatrix(scorecard) {
5598
+ if (scorecard.keywords.length === 0 || scorecard.providers.length === 0) {
5599
+ return renderEmpty("Run a visibility sweep to populate the citation matrix.");
5600
+ }
5601
+ const headers = scorecard.providers.map((p) => `<th>${escapeHtml(p)}</th>`).join("");
5602
+ const rows = scorecard.keywords.map((kw, ki) => {
5603
+ const cells = scorecard.providers.map((_, pi) => {
5604
+ const cell = scorecard.matrix[ki]?.[pi];
5605
+ if (!cell) {
5606
+ return '<td><span class="cell-pending">\u2014</span></td>';
5607
+ }
5608
+ if (cell.citationState === "cited") {
5609
+ return '<td><span class="cell-cited">Cited</span></td>';
5610
+ }
5611
+ return '<td><span class="cell-not-cited">Not cited</span></td>';
5612
+ }).join("");
5613
+ return `<tr><td>${escapeHtml(kw)}</td>${cells}</tr>`;
5614
+ }).join("");
5615
+ return `<table class="report-table">
5616
+ <thead><tr><th>Keyword</th>${headers}</tr></thead>
5617
+ <tbody>${rows}</tbody>
5618
+ </table>`;
5619
+ }
5620
+ function renderCitationScorecard(report) {
5621
+ const body = `
5622
+ ${renderProviderBars(report.citationScorecard.providerRates)}
5623
+ ${renderCitationMatrix(report.citationScorecard)}
5624
+ `;
5625
+ return section(
5626
+ { id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "Per-keyword \xD7 per-provider citation matrix from the latest visibility sweep." },
5627
+ body
5628
+ );
5629
+ }
5630
+ function renderCompetitorBars(landscape, canonical) {
5631
+ const data = [
5632
+ { label: canonical, count: landscape.projectCitationCount, isProject: true },
5633
+ ...landscape.competitors.map((c) => ({ label: c.domain, count: c.citationCount, isProject: false }))
5634
+ ];
5635
+ if (data.length <= 1) return "";
5636
+ const max = Math.max(...data.map((d) => d.count), 1);
5637
+ const width = 600;
5638
+ const height = data.length * 28 + 16;
5639
+ const labelWidth = 160;
5640
+ const bars = data.map((d, i) => {
5641
+ const y = i * 28 + 8;
5642
+ const barHeight = 18;
5643
+ const w = d.count / max * (width - labelWidth - 60);
5644
+ const color = d.isProject ? COLORS.accent : COLORS.series[(i + 1) % COLORS.series.length];
5645
+ return `
5646
+ <text x="${labelWidth - 8}" y="${y + 13}" fill="${COLORS.textMuted}" font-size="11" text-anchor="end">${escapeHtml(d.label)}</text>
5647
+ <rect x="${labelWidth}" y="${y}" width="${w}" height="${barHeight}" fill="${color}" rx="3" />
5648
+ <text x="${labelWidth + w + 6}" y="${y + 13}" fill="${COLORS.text}" font-size="11">${d.count}</text>`;
5649
+ }).join("");
5650
+ return `<div class="chart-card">
5651
+ <h3>Citations per domain</h3>
5652
+ <svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="Citations per domain bar chart">
5653
+ ${bars}
5654
+ </svg>
5655
+ </div>`;
5656
+ }
5657
+ function renderCompetitorLandscape(report) {
5658
+ const competitors2 = report.competitorLandscape.competitors;
5659
+ if (competitors2.length === 0 && report.competitorLandscape.projectCitationCount === 0) {
5660
+ return section(
5661
+ { id: "competitor-landscape", eyebrow: "Section 3", title: "Competitor Landscape" },
5662
+ renderEmpty("No competitor data yet. Add competitors and run a visibility sweep.")
5663
+ );
5664
+ }
5665
+ const rows = competitors2.map((c) => {
5666
+ const tone = pressureTone(c.pressureLabel);
5667
+ return `<tr>
5668
+ <td>${escapeHtml(c.domain)}</td>
5669
+ <td><span class="badge tone-${tone}">${escapeHtml(c.pressureLabel)}</span></td>
5670
+ <td class="numeric">${c.citationCount} / ${c.totalCount}</td>
5671
+ <td>${escapeHtml(c.citedKeywords.slice(0, 5).join(", "))}${c.citedKeywords.length > 5 ? "\u2026" : ""}</td>
5672
+ </tr>`;
5673
+ }).join("");
5674
+ const table = competitors2.length > 0 ? `<table class="report-table">
5675
+ <thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th>Cited keywords</th></tr></thead>
5676
+ <tbody>${rows}</tbody>
5677
+ </table>` : renderEmpty("No competitors configured.");
5678
+ return section(
5679
+ { id: "competitor-landscape", eyebrow: "Section 3", title: "Competitor Landscape", intro: "Where tracked competitors appear in AI answers compared to your domain." },
5680
+ `${renderCompetitorBars(report.competitorLandscape, report.meta.project.canonicalDomain)}${table}`
5681
+ );
5682
+ }
5683
+ function renderDonut(buckets) {
5684
+ if (buckets.length === 0) return "";
5685
+ const total = buckets.reduce((s, b) => s + b.count, 0);
5686
+ if (total === 0) return "";
5687
+ const cx = 110;
5688
+ const cy = 110;
5689
+ const r = 80;
5690
+ const innerR = 48;
5691
+ let cumulative = 0;
5692
+ const slices = [];
5693
+ const legend = [];
5694
+ buckets.forEach((b, i) => {
5695
+ const startAngle = cumulative / total * Math.PI * 2 - Math.PI / 2;
5696
+ const endAngle = (cumulative + b.count) / total * Math.PI * 2 - Math.PI / 2;
5697
+ cumulative += b.count;
5698
+ const x1 = cx + Math.cos(startAngle) * r;
5699
+ const y1 = cy + Math.sin(startAngle) * r;
5700
+ const x2 = cx + Math.cos(endAngle) * r;
5701
+ const y2 = cy + Math.sin(endAngle) * r;
5702
+ const ix1 = cx + Math.cos(endAngle) * innerR;
5703
+ const iy1 = cy + Math.sin(endAngle) * innerR;
5704
+ const ix2 = cx + Math.cos(startAngle) * innerR;
5705
+ const iy2 = cy + Math.sin(startAngle) * innerR;
5706
+ const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
5707
+ const color = COLORS.series[i % COLORS.series.length];
5708
+ if (b.count > 0) {
5709
+ 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}" />`);
5710
+ legend.push(`<span><span class="legend-swatch" style="background:${color}"></span>${escapeHtml(b.label)} (${b.count})</span>`);
5711
+ }
5712
+ });
5713
+ return `<div class="chart-card">
5714
+ <h3>AI source categories</h3>
5715
+ <div style="display:flex;align-items:center;gap:24px;flex-wrap:wrap;">
5716
+ <svg viewBox="0 0 220 220" width="220" height="220" role="img" aria-label="AI source category donut chart">
5717
+ ${slices.join("")}
5718
+ </svg>
5719
+ <div class="legend" style="flex-direction:column;align-items:flex-start;gap:6px;">${legend.join("")}</div>
5720
+ </div>
5721
+ </div>`;
5722
+ }
5723
+ function renderAiSourceOrigin(report) {
5724
+ const origin = report.aiSourceOrigin;
5725
+ if (origin.categories.length === 0 && origin.topDomains.length === 0) {
5726
+ return section(
5727
+ { id: "ai-source-origin", eyebrow: "Section 4", title: "AI Source Origin" },
5728
+ renderEmpty("No source data yet. Run a visibility sweep first.")
5729
+ );
5730
+ }
5731
+ const rows = origin.topDomains.map((d) => `
5732
+ <tr>
5733
+ <td>${escapeHtml(d.domain)}</td>
5734
+ <td class="numeric">${d.count}</td>
5735
+ <td>${d.isCompetitor ? '<span class="badge tone-negative">Competitor</span>' : '<span class="badge tone-neutral">External</span>'}</td>
5736
+ </tr>`).join("");
5737
+ const table = origin.topDomains.length > 0 ? `<table class="report-table">
5738
+ <thead><tr><th>Domain</th><th>Citations</th><th>Tag</th></tr></thead>
5739
+ <tbody>${rows}</tbody>
5740
+ </table>` : "";
5741
+ return section(
5742
+ { id: "ai-source-origin", eyebrow: "Section 4", title: "AI Source Origin", intro: "Where AI answers pull from, aggregated across the latest sweep." },
5743
+ `${renderDonut(origin.categories)}${table}`
5744
+ );
5745
+ }
5746
+ function renderLineChart(points, color, title, height = 200) {
5747
+ if (points.length === 0) return "";
5748
+ const width = 600;
5749
+ const padX = 32;
5750
+ const padY = 24;
5751
+ const usableW = width - padX * 2;
5752
+ const usableH = height - padY * 2;
5753
+ const max = Math.max(...points.map((p) => p.y), 1);
5754
+ const stepX = points.length > 1 ? usableW / (points.length - 1) : 0;
5755
+ const xy = points.map((p, i) => ({
5756
+ x: padX + i * stepX,
5757
+ y: padY + usableH - p.y / max * usableH,
5758
+ raw: p
5759
+ }));
5760
+ const path10 = xy.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
5761
+ const dots = xy.map((p) => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="${color}" />`).join("");
5762
+ const xLabels = xy.map((p, i) => {
5763
+ if (points.length > 8 && i % Math.ceil(points.length / 6) !== 0 && i !== points.length - 1) return "";
5764
+ 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>`;
5765
+ }).join("");
5766
+ return `<div class="chart-card">
5767
+ <h3>${escapeHtml(title)}</h3>
5768
+ <svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${escapeHtml(title)} line chart">
5769
+ <line x1="${padX}" y1="${padY + usableH}" x2="${padX + usableW}" y2="${padY + usableH}" stroke="${COLORS.border}" stroke-width="1" />
5770
+ <text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatNumber(max)}</text>
5771
+ <text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">0</text>
5772
+ <path d="${path10}" stroke="${color}" stroke-width="2" fill="none" />
5773
+ ${dots}
5774
+ ${xLabels}
5775
+ </svg>
5776
+ </div>`;
5777
+ }
5778
+ function renderGsc(report) {
5779
+ const gsc = report.gsc;
5780
+ if (!gsc) {
5781
+ return section(
5782
+ { id: "gsc", eyebrow: "Section 5", title: "GSC Performance" },
5783
+ renderEmpty("Connect Google Search Console to populate this section.")
5784
+ );
5785
+ }
5786
+ const rows = gsc.topQueries.map((q) => `
5787
+ <tr>
5788
+ <td>${escapeHtml(q.query)}</td>
5789
+ <td class="numeric">${formatNumber(q.clicks)}</td>
5790
+ <td class="numeric">${formatNumber(q.impressions)}</td>
5791
+ <td class="numeric">${formatRatio(q.ctr)}</td>
5792
+ <td class="numeric">${q.avgPosition.toFixed(1)}</td>
5793
+ <td><span class="badge tone-neutral">${escapeHtml(q.category)}</span></td>
5794
+ </tr>`).join("");
5795
+ const breakdownRows = gsc.categoryBreakdown.map((c) => `
5796
+ <tr>
5797
+ <td>${escapeHtml(c.category)}</td>
5798
+ <td class="numeric">${formatNumber(c.clicks)}</td>
5799
+ <td class="numeric">${formatNumber(c.impressions)}</td>
5800
+ <td class="numeric">${c.sharePct}%</td>
5801
+ </tr>`).join("");
5802
+ const trendChart = renderLineChart(
5803
+ gsc.trend.map((t) => ({ x: t.date, y: t.clicks, label: t.date.slice(5) })),
5804
+ COLORS.accent,
5805
+ "Clicks over time"
5806
+ );
5807
+ return section(
5808
+ { id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro: "Top queries, category breakdown, and traffic trend from Google Search Console." },
5809
+ `<div class="metric-grid">
5810
+ <div class="metric"><div class="label">Total clicks</div><div class="value">${formatNumber(gsc.totalClicks)}</div></div>
5811
+ <div class="metric"><div class="label">Total impressions</div><div class="value">${formatNumber(gsc.totalImpressions)}</div></div>
5812
+ <div class="metric"><div class="label">Avg CTR</div><div class="value">${formatRatio(gsc.ctr)}</div></div>
5813
+ <div class="metric"><div class="label">Avg position</div><div class="value">${gsc.avgPosition.toFixed(1)}</div></div>
5814
+ </div>
5815
+ ${trendChart}
5816
+ <div class="chart-card"><h3>Top queries</h3>
5817
+ <table class="report-table">
5818
+ <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>
5819
+ <tbody>${rows}</tbody>
5820
+ </table>
5821
+ </div>
5822
+ <div class="chart-card"><h3>Category breakdown</h3>
5823
+ <table class="report-table">
5824
+ <thead><tr><th>Category</th><th class="numeric">Clicks</th><th class="numeric">Imp.</th><th class="numeric">Share</th></tr></thead>
5825
+ <tbody>${breakdownRows}</tbody>
5826
+ </table>
5827
+ </div>`
5828
+ );
5829
+ }
5830
+ function renderGa(report) {
5831
+ const ga = report.ga;
5832
+ if (!ga) {
5833
+ return section(
5834
+ { id: "ga", eyebrow: "Section 6", title: "GA4 Traffic" },
5835
+ renderEmpty("Connect Google Analytics 4 to populate this section.")
5836
+ );
5837
+ }
5838
+ const pageRows = ga.topLandingPages.map((p) => `
5839
+ <tr>
5840
+ <td class="page-cell">${formatLandingPageHtml(p.page)}</td>
5841
+ <td class="numeric">${formatNumber(p.sessions)}</td>
5842
+ <td class="numeric">${formatNumber(p.users)}</td>
5843
+ <td class="numeric">${formatNumber(p.organicSessions)}</td>
5844
+ </tr>`).join("");
5845
+ const channelRows = ga.channelBreakdown.map((c) => `
5846
+ <tr>
5847
+ <td>${escapeHtml(c.channel)}</td>
5848
+ <td class="numeric">${formatNumber(c.sessions)}</td>
5849
+ <td class="numeric">${c.sharePct}%</td>
5850
+ </tr>`).join("");
5851
+ return section(
5852
+ { id: "ga", eyebrow: "Section 6", title: "GA4 Traffic", intro: `Sessions and users for ${formatDate(ga.periodStart)} \u2192 ${formatDate(ga.periodEnd)}.` },
5853
+ `<div class="metric-grid">
5854
+ <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ga.totalSessions)}</div></div>
5855
+ <div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ga.totalUsers)}</div></div>
5856
+ <div class="metric"><div class="label">Organic sessions</div><div class="value">${formatNumber(ga.totalOrganicSessions)}</div></div>
5857
+ </div>
5858
+ <div class="chart-card"><h3>Top landing pages</h3>
5859
+ <table class="report-table">
5860
+ <thead><tr><th>Page</th><th class="numeric">Sessions</th><th class="numeric">Users</th><th class="numeric">Organic</th></tr></thead>
5861
+ <tbody>${pageRows}</tbody>
5862
+ </table>
5863
+ </div>
5864
+ <div class="chart-card"><h3>Channel breakdown</h3>
5865
+ <table class="report-table">
5866
+ <thead><tr><th>Channel</th><th class="numeric">Sessions</th><th class="numeric">Share</th></tr></thead>
5867
+ <tbody>${channelRows}</tbody>
5868
+ </table>
5869
+ </div>`
5870
+ );
5871
+ }
5872
+ function renderSocial(report) {
5873
+ const social = report.socialReferrals;
5874
+ if (!social) {
5875
+ return section(
5876
+ { id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals" },
5877
+ renderEmpty("No social referral data yet.")
5878
+ );
5879
+ }
5880
+ const channelRows = social.channels.map((c) => `
5881
+ <tr>
5882
+ <td>${escapeHtml(c.channelGroup)}</td>
5883
+ <td class="numeric">${formatNumber(c.sessions)}</td>
5884
+ <td class="numeric">${c.sharePct}%</td>
5885
+ </tr>`).join("");
5886
+ const campaignRows = social.topCampaigns.map((c) => `
5887
+ <tr>
5888
+ <td>${escapeHtml(c.source)}</td>
5889
+ <td>${escapeHtml(c.medium)}</td>
5890
+ <td class="numeric">${formatNumber(c.sessions)}</td>
5891
+ </tr>`).join("");
5892
+ return section(
5893
+ { id: "social-referrals", eyebrow: "Section 7", title: "Social Referrals", intro: "Paid vs organic split with top campaigns." },
5894
+ `<div class="metric-grid">
5895
+ <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(social.totalSessions)}</div></div>
5896
+ <div class="metric"><div class="label">Organic social</div><div class="value">${formatNumber(social.organicSessions)}</div></div>
5897
+ <div class="metric"><div class="label">Paid social</div><div class="value">${formatNumber(social.paidSessions)}</div></div>
5898
+ </div>
5899
+ <div class="chart-card"><h3>Channel groups</h3>
5900
+ <table class="report-table">
5901
+ <thead><tr><th>Channel</th><th class="numeric">Sessions</th><th class="numeric">Share</th></tr></thead>
5902
+ <tbody>${channelRows}</tbody>
5903
+ </table>
5904
+ </div>
5905
+ <div class="chart-card"><h3>Top campaigns</h3>
5906
+ <table class="report-table">
5907
+ <thead><tr><th>Source</th><th>Medium</th><th class="numeric">Sessions</th></tr></thead>
5908
+ <tbody>${campaignRows}</tbody>
5909
+ </table>
5910
+ </div>`
5911
+ );
5912
+ }
5913
+ function renderAiReferrals(report) {
5914
+ const ai = report.aiReferrals;
5915
+ if (!ai) {
5916
+ return section(
5917
+ { id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic" },
5918
+ renderEmpty("No AI referral traffic detected yet.")
5919
+ );
5920
+ }
5921
+ const sourceRows = ai.bySource.map((s) => `
5922
+ <tr>
5923
+ <td>${escapeHtml(s.source)}</td>
5924
+ <td class="numeric">${formatNumber(s.sessions)}</td>
5925
+ <td class="numeric">${formatNumber(s.users)}</td>
5926
+ <td class="numeric">${s.sharePct}%</td>
5927
+ </tr>`).join("");
5928
+ const pageRows = ai.topLandingPages.map((p) => `
5929
+ <tr>
5930
+ <td class="page-cell">${formatLandingPageHtml(p.page)}</td>
5931
+ <td class="numeric">${formatNumber(p.sessions)}</td>
5932
+ <td class="numeric">${formatNumber(p.users)}</td>
5933
+ </tr>`).join("");
5934
+ const trendChart = renderLineChart(
5935
+ ai.trend.map((t) => ({ x: t.date, y: t.sessions, label: t.date.slice(5) })),
5936
+ COLORS.series[2],
5937
+ "AI referral sessions over time"
5938
+ );
5939
+ return section(
5940
+ { id: "ai-referrals", eyebrow: "Section 8", title: "AI Referral Traffic", intro: "Sessions sent from AI answer engines." },
5941
+ `<div class="metric-grid">
5942
+ <div class="metric"><div class="label">Total sessions</div><div class="value">${formatNumber(ai.totalSessions)}</div></div>
5943
+ <div class="metric"><div class="label">Total users</div><div class="value">${formatNumber(ai.totalUsers)}</div></div>
5944
+ </div>
5945
+ ${trendChart}
5946
+ <div class="chart-card"><h3>Sessions by source</h3>
5947
+ <table class="report-table">
5948
+ <thead><tr><th>Source</th><th class="numeric">Sessions</th><th class="numeric">Users</th><th class="numeric">Share</th></tr></thead>
5949
+ <tbody>${sourceRows}</tbody>
5950
+ </table>
5951
+ </div>
5952
+ <div class="chart-card"><h3>Top AI landing pages</h3>
5953
+ <table class="report-table">
5954
+ <thead><tr><th>Page</th><th class="numeric">Sessions</th><th class="numeric">Users</th></tr></thead>
5955
+ <tbody>${pageRows}</tbody>
5956
+ </table>
5957
+ </div>`
5958
+ );
5959
+ }
5960
+ function renderIndexingHealth(report) {
5961
+ const ih = report.indexingHealth;
5962
+ if (!ih) {
5963
+ return section(
5964
+ { id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health" },
5965
+ renderEmpty("Connect Google Search Console or Bing Webmaster Tools and run a sitemap inspection.")
5966
+ );
5967
+ }
5968
+ const segments = [
5969
+ { label: "Indexed", count: ih.indexed, color: COLORS.positive },
5970
+ { label: "Not indexed", count: ih.notIndexed, color: COLORS.caution },
5971
+ { label: "Deindexed", count: ih.deindexed, color: COLORS.negative },
5972
+ { label: "Unknown", count: ih.unknown, color: COLORS.neutral }
5973
+ ].filter((s) => s.count > 0);
5974
+ const total = segments.reduce((s, x) => s + x.count, 0) || 1;
5975
+ const width = 600;
5976
+ const height = 28;
5977
+ let acc = 0;
5978
+ const bars = segments.map((s) => {
5979
+ const w = s.count / total * width;
5980
+ const x = acc;
5981
+ acc += w;
5982
+ return `<rect x="${x}" y="0" width="${w}" height="${height}" fill="${s.color}" />`;
5983
+ }).join("");
5984
+ const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml(s.label)}: ${s.count}</span>`).join("");
5985
+ return section(
5986
+ { id: "indexing-health", eyebrow: "Section 9", title: "Indexing Health", intro: `Source: ${ih.provider === "google" ? "Google Search Console" : "Bing Webmaster Tools"}.` },
5987
+ `<div class="metric-grid">
5988
+ <div class="metric"><div class="label">Indexed</div><div class="value tone-positive">${formatNumber(ih.indexed)}</div></div>
5989
+ <div class="metric"><div class="label">Total inspected</div><div class="value">${formatNumber(ih.total)}</div></div>
5990
+ <div class="metric"><div class="label">Indexed share</div><div class="value">${ih.indexedPct}%</div></div>
5991
+ </div>
5992
+ <div class="chart-card">
5993
+ <h3>Coverage breakdown</h3>
5994
+ <svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="Coverage stacked bar">${bars}</svg>
5995
+ <div class="legend">${legend}</div>
5996
+ </div>`
5997
+ );
5998
+ }
5999
+ function renderCitationsTrend(report) {
6000
+ const trend = report.citationsTrend;
6001
+ if (trend.length === 0) {
6002
+ return section(
6003
+ { id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time" },
6004
+ renderEmpty("Run multiple visibility sweeps to see a trend.")
6005
+ );
6006
+ }
6007
+ const chart = renderLineChart(
6008
+ trend.map((t) => ({ x: t.date, y: t.citationRate, label: formatDate(t.date) })),
6009
+ COLORS.positive,
6010
+ "Overall citation rate",
6011
+ 220
6012
+ );
6013
+ const rows = trend.map((t) => `
6014
+ <tr>
6015
+ <td>${formatDate(t.date)}</td>
6016
+ <td class="numeric">${t.citationRate}%</td>
6017
+ <td>${t.providerRates.map((r) => `${escapeHtml(r.provider)}: ${r.citationRate}%`).join(" \xB7 ")}</td>
6018
+ </tr>`).join("");
6019
+ return section(
6020
+ { id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time", intro: "Per-run citation rate across the project history." },
6021
+ `${chart}
6022
+ <div class="chart-card"><h3>Run-by-run breakdown</h3>
6023
+ <table class="report-table">
6024
+ <thead><tr><th>Run</th><th class="numeric">Overall rate</th><th>Per-provider rates</th></tr></thead>
6025
+ <tbody>${rows}</tbody>
6026
+ </table>
6027
+ </div>`
6028
+ );
6029
+ }
6030
+ function renderInsights(report) {
6031
+ const list = report.insights;
6032
+ if (list.length === 0) {
6033
+ return section(
6034
+ { id: "insights", eyebrow: "Section 11", title: "Insights & Alerts" },
6035
+ renderEmpty("No insights yet \u2014 run a visibility sweep to generate alerts.")
6036
+ );
6037
+ }
6038
+ const rows = list.map((i) => {
6039
+ const tone = severityTone(i.severity);
6040
+ return `<tr>
6041
+ <td><span class="badge tone-${tone}">${escapeHtml(i.severity)}</span></td>
6042
+ <td>${escapeHtml(i.title)}</td>
6043
+ <td>${escapeHtml(i.keyword)}</td>
6044
+ <td>${escapeHtml(i.provider)}</td>
6045
+ <td>${i.recommendation ? escapeHtml(i.recommendation) : '<span class="cell-pending">\u2014</span>'}</td>
6046
+ </tr>`;
6047
+ }).join("");
6048
+ return section(
6049
+ { id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Priority-ordered findings from the most recent runs." },
6050
+ `<table class="report-table">
6051
+ <thead><tr><th>Severity</th><th>Title</th><th>Keyword</th><th>Provider</th><th>Recommendation</th></tr></thead>
6052
+ <tbody>${rows}</tbody>
6053
+ </table>`
6054
+ );
6055
+ }
6056
+ function renderRecommendedNextSteps(report) {
6057
+ const steps = report.recommendedNextSteps;
6058
+ if (steps.length === 0) {
6059
+ return section(
6060
+ { id: "recommended-next-steps", eyebrow: "Section 12", title: "Recommended Next Steps" },
6061
+ renderEmpty("No outstanding actions.")
6062
+ );
6063
+ }
6064
+ const items = steps.map((s) => `
6065
+ <div class="step">
6066
+ <span class="horizon">${escapeHtml(s.horizon)}</span>
6067
+ <span class="title">${escapeHtml(s.title)}</span>
6068
+ <span class="rationale">${escapeHtml(s.rationale)}</span>
6069
+ </div>`).join("");
6070
+ return section(
6071
+ { id: "recommended-next-steps", eyebrow: "Section 12", title: "Recommended Next Steps" },
6072
+ `<div class="steps">${items}</div>`
6073
+ );
6074
+ }
6075
+ function escapeJsonForScript(json) {
6076
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
6077
+ }
6078
+ function renderReportHtml(report, opts = {}) {
6079
+ const title = opts.title ?? `Canonry report \u2014 ${report.meta.project.displayName}`;
6080
+ const sections = [
6081
+ renderExecutiveSummary(report),
6082
+ renderCitationScorecard(report),
6083
+ renderCompetitorLandscape(report),
6084
+ renderAiSourceOrigin(report),
6085
+ renderGsc(report),
6086
+ renderGa(report),
6087
+ renderSocial(report),
6088
+ renderAiReferrals(report),
6089
+ renderIndexingHealth(report),
6090
+ renderCitationsTrend(report),
6091
+ renderInsights(report),
6092
+ renderRecommendedNextSteps(report)
6093
+ ].join("\n");
6094
+ const json = escapeJsonForScript(JSON.stringify(report));
6095
+ return `<!DOCTYPE html>
6096
+ <html lang="en">
6097
+ <head>
6098
+ <meta charset="utf-8" />
6099
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6100
+ <title>${escapeHtml(title)}</title>
6101
+ <style>${STYLE}</style>
6102
+ </head>
6103
+ <body>
6104
+ <div class="container">
6105
+ <header class="header">
6106
+ <div class="eyebrow">AEO Report</div>
6107
+ <h1>${escapeHtml(report.meta.project.displayName)}</h1>
6108
+ <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>
6109
+ </header>
6110
+ ${sections}
6111
+ <footer class="footer">Generated by canonry \xB7 ${escapeHtml(report.meta.generatedAt)}</footer>
6112
+ </div>
6113
+ <script type="application/json" id="canonry-report-data">${json}</script>
6114
+ </body>
6115
+ </html>`;
6116
+ }
6117
+
6118
+ // src/commands/report.ts
6119
+ function defaultOutputPath(project) {
6120
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6121
+ return path3.resolve(process.cwd(), `canonry-report-${project}-${date}.html`);
6122
+ }
6123
+ async function runReportCommand(project, opts = {}) {
6124
+ const client = createApiClient();
6125
+ const report = await client.getReport(project);
6126
+ if (opts.format === "json") {
6127
+ console.log(JSON.stringify(report, null, 2));
6128
+ return;
6129
+ }
6130
+ const html = renderReportHtml(report);
6131
+ const targetPath = opts.output ? path3.resolve(opts.output) : defaultOutputPath(project);
6132
+ const dir = path3.dirname(targetPath);
6133
+ if (!fs4.existsSync(dir)) {
6134
+ fs4.mkdirSync(dir, { recursive: true });
6135
+ }
6136
+ fs4.writeFileSync(targetPath, html, "utf-8");
6137
+ console.log(`Report written to ${targetPath}`);
6138
+ }
6139
+
6140
+ // src/cli-commands/report.ts
6141
+ var USAGE2 = "canonry report <project> [--output <path>] [--format json]";
6142
+ var REPORT_CLI_COMMANDS = [
6143
+ {
6144
+ path: ["report"],
6145
+ usage: USAGE2,
6146
+ options: {
6147
+ output: { type: "string", short: "o" }
6148
+ },
6149
+ run: async (input) => {
6150
+ const project = requireProject(input, "report", USAGE2);
6151
+ await runReportCommand(project, {
6152
+ format: input.format,
6153
+ output: getString(input.values, "output")
6154
+ });
6155
+ }
6156
+ }
6157
+ ];
6158
+
5177
6159
  // src/commands/run.ts
5178
6160
  function getClient14() {
5179
6161
  return createApiClient();
@@ -5882,19 +6864,19 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
5882
6864
  ];
5883
6865
 
5884
6866
  // src/commands/skills.ts
5885
- import fs4 from "fs";
5886
- import path3 from "path";
6867
+ import fs5 from "fs";
6868
+ import path4 from "path";
5887
6869
  import { fileURLToPath } from "url";
5888
6870
  var BUNDLED_SKILL_NAMES = ["canonry-setup", "aero"];
5889
6871
  function resolveBundledSkillsRoot(pkgDir) {
5890
- const here = pkgDir ?? path3.dirname(fileURLToPath(import.meta.url));
6872
+ const here = pkgDir ?? path4.dirname(fileURLToPath(import.meta.url));
5891
6873
  const candidates = [
5892
- path3.join(here, "../assets/agent-workspace/skills"),
5893
- path3.join(here, "../../assets/agent-workspace/skills"),
5894
- path3.join(here, "../../../../skills")
6874
+ path4.join(here, "../assets/agent-workspace/skills"),
6875
+ path4.join(here, "../../assets/agent-workspace/skills"),
6876
+ path4.join(here, "../../../../skills")
5895
6877
  ];
5896
6878
  for (const candidate of candidates) {
5897
- if (BUNDLED_SKILL_NAMES.every((name) => fs4.existsSync(path3.join(candidate, name, "SKILL.md")))) {
6879
+ if (BUNDLED_SKILL_NAMES.every((name) => fs5.existsSync(path4.join(candidate, name, "SKILL.md")))) {
5898
6880
  return candidate;
5899
6881
  }
5900
6882
  }
@@ -5915,17 +6897,17 @@ function parseDescription(content) {
5915
6897
  function getBundledSkills(pkgDir) {
5916
6898
  const root = resolveBundledSkillsRoot(pkgDir);
5917
6899
  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");
6900
+ const skillDir = path4.join(root, name);
6901
+ const skillFile = path4.join(skillDir, "SKILL.md");
6902
+ const content = fs5.readFileSync(skillFile, "utf-8");
5921
6903
  return { name, description: parseDescription(content), bundledPath: skillDir };
5922
6904
  });
5923
6905
  }
5924
6906
  function walkRelative(dir, prefix = "") {
5925
6907
  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);
6908
+ for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
6909
+ const rel = prefix ? path4.join(prefix, entry.name) : entry.name;
6910
+ const full = path4.join(dir, entry.name);
5929
6911
  if (entry.isDirectory()) {
5930
6912
  out.push(...walkRelative(full, rel));
5931
6913
  } else if (entry.isFile()) {
@@ -5935,33 +6917,33 @@ function walkRelative(dir, prefix = "") {
5935
6917
  return out.sort();
5936
6918
  }
5937
6919
  function compareDirContent(srcDir, destDir) {
5938
- if (!fs4.existsSync(destDir)) return "missing";
5939
- if (!fs4.statSync(destDir).isDirectory()) return "different";
6920
+ if (!fs5.existsSync(destDir)) return "missing";
6921
+ if (!fs5.statSync(destDir).isDirectory()) return "different";
5940
6922
  const srcFiles = walkRelative(srcDir);
5941
6923
  const destFiles = walkRelative(destDir);
5942
6924
  if (srcFiles.length !== destFiles.length) return "different";
5943
6925
  for (let i = 0; i < srcFiles.length; i++) {
5944
6926
  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]));
6927
+ const srcBytes = fs5.readFileSync(path4.join(srcDir, srcFiles[i]));
6928
+ const destBytes = fs5.readFileSync(path4.join(destDir, destFiles[i]));
5947
6929
  if (!srcBytes.equals(destBytes)) return "different";
5948
6930
  }
5949
6931
  return "match";
5950
6932
  }
5951
6933
  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);
6934
+ fs5.mkdirSync(dest, { recursive: true });
6935
+ for (const entry of fs5.readdirSync(src, { withFileTypes: true })) {
6936
+ const srcPath = path4.join(src, entry.name);
6937
+ const destPath = path4.join(dest, entry.name);
5956
6938
  if (entry.isDirectory()) {
5957
6939
  copyDirRecursive(srcPath, destPath);
5958
6940
  } else if (entry.isFile()) {
5959
- fs4.copyFileSync(srcPath, destPath);
6941
+ fs5.copyFileSync(srcPath, destPath);
5960
6942
  }
5961
6943
  }
5962
6944
  }
5963
6945
  function installClaudeSkill(skill, targetDir, force) {
5964
- const targetPath = path3.join(targetDir, ".claude", "skills", skill.name);
6946
+ const targetPath = path4.join(targetDir, ".claude", "skills", skill.name);
5965
6947
  const compare = compareDirContent(skill.bundledPath, targetPath);
5966
6948
  if (compare === "match") {
5967
6949
  return {
@@ -5981,7 +6963,7 @@ function installClaudeSkill(skill, targetDir, force) {
5981
6963
  });
5982
6964
  }
5983
6965
  if (compare === "different") {
5984
- fs4.rmSync(targetPath, { recursive: true, force: true });
6966
+ fs5.rmSync(targetPath, { recursive: true, force: true });
5985
6967
  }
5986
6968
  copyDirRecursive(skill.bundledPath, targetPath);
5987
6969
  return {
@@ -5993,18 +6975,18 @@ function installClaudeSkill(skill, targetDir, force) {
5993
6975
  };
5994
6976
  }
5995
6977
  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 });
6978
+ const codexPath = path4.join(targetDir, ".codex", "skills", skill.name);
6979
+ const claudePath = path4.join(targetDir, ".claude", "skills", skill.name);
6980
+ const linkTarget = path4.relative(path4.dirname(codexPath), claudePath);
6981
+ fs5.mkdirSync(path4.dirname(codexPath), { recursive: true });
6000
6982
  let stat;
6001
6983
  try {
6002
- stat = fs4.lstatSync(codexPath);
6984
+ stat = fs5.lstatSync(codexPath);
6003
6985
  } catch {
6004
6986
  stat = void 0;
6005
6987
  }
6006
6988
  if (stat?.isSymbolicLink()) {
6007
- const existing = fs4.readlinkSync(codexPath);
6989
+ const existing = fs5.readlinkSync(codexPath);
6008
6990
  if (existing === linkTarget) {
6009
6991
  return {
6010
6992
  skill: skill.name,
@@ -6022,8 +7004,8 @@ function installCodexSymlink(skill, targetDir, force) {
6022
7004
  exitCode: 1
6023
7005
  });
6024
7006
  }
6025
- fs4.unlinkSync(codexPath);
6026
- fs4.symlinkSync(linkTarget, codexPath);
7007
+ fs5.unlinkSync(codexPath);
7008
+ fs5.symlinkSync(linkTarget, codexPath);
6027
7009
  return {
6028
7010
  skill: skill.name,
6029
7011
  client: CodingAgents.codex,
@@ -6041,9 +7023,9 @@ function installCodexSymlink(skill, targetDir, force) {
6041
7023
  exitCode: 1
6042
7024
  });
6043
7025
  }
6044
- fs4.rmSync(codexPath, { recursive: true, force: true });
7026
+ fs5.rmSync(codexPath, { recursive: true, force: true });
6045
7027
  }
6046
- fs4.symlinkSync(linkTarget, codexPath);
7028
+ fs5.symlinkSync(linkTarget, codexPath);
6047
7029
  return {
6048
7030
  skill: skill.name,
6049
7031
  client: CodingAgents.codex,
@@ -6059,7 +7041,7 @@ function buildSummaryMessage(results) {
6059
7041
  return `Skills install summary: ${parts.join(", ")}.`;
6060
7042
  }
6061
7043
  async function installSkills(opts = {}) {
6062
- const targetDir = path3.resolve(opts.dir ?? process.cwd());
7044
+ const targetDir = path4.resolve(opts.dir ?? process.cwd());
6063
7045
  const client = opts.client ?? SkillsClients.all;
6064
7046
  const force = opts.force ?? false;
6065
7047
  const allSkills = getBundledSkills();
@@ -6075,7 +7057,7 @@ async function installSkills(opts = {}) {
6075
7057
  });
6076
7058
  }
6077
7059
  const skillsToInstall = allSkills.filter((s) => requestedNames.includes(s.name));
6078
- fs4.mkdirSync(targetDir, { recursive: true });
7060
+ fs5.mkdirSync(targetDir, { recursive: true });
6079
7061
  const results = [];
6080
7062
  for (const skill of skillsToInstall) {
6081
7063
  results.push(installClaudeSkill(skill, targetDir, force));
@@ -6176,12 +7158,12 @@ var SKILLS_CLI_COMMANDS = [
6176
7158
  ];
6177
7159
 
6178
7160
  // src/commands/snapshot.ts
6179
- import fs6 from "fs";
6180
- import path5 from "path";
7161
+ import fs7 from "fs";
7162
+ import path6 from "path";
6181
7163
 
6182
7164
  // src/snapshot-pdf.ts
6183
- import fs5 from "fs";
6184
- import path4 from "path";
7165
+ import fs6 from "fs";
7166
+ import path5 from "path";
6185
7167
  import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
6186
7168
  var PAGE_WIDTH = 612;
6187
7169
  var PAGE_HEIGHT = 792;
@@ -6390,9 +7372,9 @@ async function writeSnapshotPdf(report, outputPath) {
6390
7372
  renderCompetitors(pdf, report);
6391
7373
  renderQueries(pdf, report);
6392
7374
  const bytes = await doc.save();
6393
- const resolvedPath = path4.resolve(outputPath);
6394
- fs5.mkdirSync(path4.dirname(resolvedPath), { recursive: true });
6395
- fs5.writeFileSync(resolvedPath, bytes);
7375
+ const resolvedPath = path5.resolve(outputPath);
7376
+ fs6.mkdirSync(path5.dirname(resolvedPath), { recursive: true });
7377
+ fs6.writeFileSync(resolvedPath, bytes);
6396
7378
  return resolvedPath;
6397
7379
  }
6398
7380
  function renderCover(pdf, report) {
@@ -6550,9 +7532,9 @@ Markdown saved: ${savedMdPath}`);
6550
7532
  PDF saved: ${savedPdfPath}`);
6551
7533
  }
6552
7534
  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");
7535
+ const resolvedPath = path6.resolve(outputPath);
7536
+ fs7.mkdirSync(path6.dirname(resolvedPath), { recursive: true });
7537
+ fs7.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
6556
7538
  return resolvedPath;
6557
7539
  }
6558
7540
  function formatSnapshotMarkdown(report) {
@@ -7207,7 +8189,7 @@ var CONTENT_CLI_COMMANDS = [
7207
8189
 
7208
8190
  // src/commands/bootstrap.ts
7209
8191
  import crypto from "crypto";
7210
- import path6 from "path";
8192
+ import path7 from "path";
7211
8193
  import { eq as eq2 } from "drizzle-orm";
7212
8194
 
7213
8195
  // ../config/src/index.ts
@@ -7354,7 +8336,7 @@ async function bootstrapCommand(_opts) {
7354
8336
  );
7355
8337
  }
7356
8338
  const configDir = getConfigDir();
7357
- const databasePath = env.databasePath || path6.join(configDir, "data.db");
8339
+ const databasePath = env.databasePath || path7.join(configDir, "data.db");
7358
8340
  const existing = configExists();
7359
8341
  const existingConfig = existing ? loadConfig() : void 0;
7360
8342
  let rawApiKey;
@@ -7424,10 +8406,10 @@ async function bootstrapCommand(_opts) {
7424
8406
 
7425
8407
  // src/commands/daemon.ts
7426
8408
  import { spawn } from "child_process";
7427
- import fs7 from "fs";
7428
- import path7 from "path";
8409
+ import fs8 from "fs";
8410
+ import path8 from "path";
7429
8411
  function getPidPath() {
7430
- return path7.join(getConfigDir(), "canonry.pid");
8412
+ return path8.join(getConfigDir(), "canonry.pid");
7431
8413
  }
7432
8414
  function isProcessAlive(pid) {
7433
8415
  try {
@@ -7454,8 +8436,8 @@ async function waitForReady(host, port, maxMs = 1e4) {
7454
8436
  async function startDaemon(opts) {
7455
8437
  const pidPath = getPidPath();
7456
8438
  const format = opts.format ?? "text";
7457
- if (fs7.existsSync(pidPath)) {
7458
- const existingPid = parseInt(fs7.readFileSync(pidPath, "utf-8").trim(), 10);
8439
+ if (fs8.existsSync(pidPath)) {
8440
+ const existingPid = parseInt(fs8.readFileSync(pidPath, "utf-8").trim(), 10);
7459
8441
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
7460
8442
  throw new CliError({
7461
8443
  code: "DAEMON_ALREADY_RUNNING",
@@ -7466,9 +8448,9 @@ async function startDaemon(opts) {
7466
8448
  }
7467
8449
  });
7468
8450
  }
7469
- fs7.unlinkSync(pidPath);
8451
+ fs8.unlinkSync(pidPath);
7470
8452
  }
7471
- const cliPath = path7.resolve(new URL(import.meta.url).pathname);
8453
+ const cliPath = path8.resolve(new URL(import.meta.url).pathname);
7472
8454
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
7473
8455
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
7474
8456
  if (opts.port) args.push("--port", opts.port);
@@ -7487,10 +8469,10 @@ async function startDaemon(opts) {
7487
8469
  });
7488
8470
  }
7489
8471
  const configDir = getConfigDir();
7490
- if (!fs7.existsSync(configDir)) {
7491
- fs7.mkdirSync(configDir, { recursive: true });
8472
+ if (!fs8.existsSync(configDir)) {
8473
+ fs8.mkdirSync(configDir, { recursive: true });
7492
8474
  }
7493
- fs7.writeFileSync(pidPath, String(child.pid), "utf-8");
8475
+ fs8.writeFileSync(pidPath, String(child.pid), "utf-8");
7494
8476
  const port = opts.port ?? "4100";
7495
8477
  const host = opts.host ?? "127.0.0.1";
7496
8478
  if (format !== "json") {
@@ -7499,7 +8481,7 @@ async function startDaemon(opts) {
7499
8481
  const ready = await waitForReady(host, port);
7500
8482
  if (!ready) {
7501
8483
  try {
7502
- fs7.unlinkSync(pidPath);
8484
+ fs8.unlinkSync(pidPath);
7503
8485
  } catch {
7504
8486
  }
7505
8487
  throw new CliError({
@@ -7531,7 +8513,7 @@ async function startDaemon(opts) {
7531
8513
  }
7532
8514
  function stopDaemon(format = "text") {
7533
8515
  const pidPath = getPidPath();
7534
- if (!fs7.existsSync(pidPath)) {
8516
+ if (!fs8.existsSync(pidPath)) {
7535
8517
  if (format === "json") {
7536
8518
  console.log(JSON.stringify({
7537
8519
  stopped: false,
@@ -7542,7 +8524,7 @@ function stopDaemon(format = "text") {
7542
8524
  console.log("Canonry is not running (no PID file found)");
7543
8525
  return;
7544
8526
  }
7545
- const pid = parseInt(fs7.readFileSync(pidPath, "utf-8").trim(), 10);
8527
+ const pid = parseInt(fs8.readFileSync(pidPath, "utf-8").trim(), 10);
7546
8528
  if (isNaN(pid)) {
7547
8529
  if (format === "json") {
7548
8530
  console.log(JSON.stringify({
@@ -7553,7 +8535,7 @@ function stopDaemon(format = "text") {
7553
8535
  } else {
7554
8536
  console.error("Invalid PID file. Removing it.");
7555
8537
  }
7556
- fs7.unlinkSync(pidPath);
8538
+ fs8.unlinkSync(pidPath);
7557
8539
  return;
7558
8540
  }
7559
8541
  if (!isProcessAlive(pid)) {
@@ -7567,12 +8549,12 @@ function stopDaemon(format = "text") {
7567
8549
  } else {
7568
8550
  console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
7569
8551
  }
7570
- fs7.unlinkSync(pidPath);
8552
+ fs8.unlinkSync(pidPath);
7571
8553
  return;
7572
8554
  }
7573
8555
  try {
7574
8556
  process.kill(pid, "SIGTERM");
7575
- fs7.unlinkSync(pidPath);
8557
+ fs8.unlinkSync(pidPath);
7576
8558
  if (format === "json") {
7577
8559
  console.log(JSON.stringify({
7578
8560
  stopped: true,
@@ -7596,9 +8578,9 @@ function stopDaemon(format = "text") {
7596
8578
 
7597
8579
  // src/commands/init.ts
7598
8580
  import crypto2 from "crypto";
7599
- import fs8 from "fs";
8581
+ import fs9 from "fs";
7600
8582
  import readline from "readline";
7601
- import path8 from "path";
8583
+ import path9 from "path";
7602
8584
  function prompt(question) {
7603
8585
  const rl = readline.createInterface({
7604
8586
  input: process.stdin,
@@ -7619,8 +8601,8 @@ var DEFAULT_QUOTA = {
7619
8601
  var PROJECT_MARKERS = [".git", "canonry.yaml", "canonry.yml", "package.json"];
7620
8602
  function cwdLooksLikeProject(dir) {
7621
8603
  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)));
8604
+ if (home && path9.resolve(dir) === path9.resolve(home)) return false;
8605
+ return PROJECT_MARKERS.some((marker) => fs9.existsSync(path9.join(dir, marker)));
7624
8606
  }
7625
8607
  var DEFAULT_AGENT_MODELS = {
7626
8608
  anthropic: "anthropic/claude-sonnet-4-6",
@@ -7650,8 +8632,8 @@ async function initCommand(opts) {
7650
8632
  return void 0;
7651
8633
  }
7652
8634
  const configDir = getConfigDir();
7653
- if (!fs8.existsSync(configDir)) {
7654
- fs8.mkdirSync(configDir, { recursive: true });
8635
+ if (!fs9.existsSync(configDir)) {
8636
+ fs9.mkdirSync(configDir, { recursive: true });
7655
8637
  }
7656
8638
  const bootstrapEnv = getBootstrapEnv(process.env, {
7657
8639
  GEMINI_API_KEY: opts?.geminiKey,
@@ -7766,7 +8748,7 @@ async function initCommand(opts) {
7766
8748
  const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
7767
8749
  const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
7768
8750
  const keyPrefix = rawApiKey.slice(0, 9);
7769
- const databasePath = path8.join(configDir, "data.db");
8751
+ const databasePath = path9.join(configDir, "data.db");
7770
8752
  const db = createClient(databasePath);
7771
8753
  migrate(db);
7772
8754
  db.insert(apiKeys).values({
@@ -8214,7 +9196,7 @@ var SYSTEM_CLI_COMMANDS = [
8214
9196
  ];
8215
9197
 
8216
9198
  // src/cli-commands/wordpress.ts
8217
- import fs9 from "fs";
9199
+ import fs10 from "fs";
8218
9200
 
8219
9201
  // src/commands/wordpress.ts
8220
9202
  function getClient18() {
@@ -8450,12 +9432,12 @@ async function wordpressSetMeta(project, body) {
8450
9432
  printPageDetail(result);
8451
9433
  }
8452
9434
  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);
9435
+ const fs11 = await import("fs/promises");
9436
+ const path10 = await import("path");
9437
+ const filePath = path10.resolve(opts.from);
8456
9438
  let raw;
8457
9439
  try {
8458
- raw = await fs10.readFile(filePath, "utf8");
9440
+ raw = await fs11.readFile(filePath, "utf8");
8459
9441
  } catch {
8460
9442
  throw new CliError({
8461
9443
  code: "FILE_READ_ERROR",
@@ -8552,13 +9534,13 @@ async function wordpressSetSchema(project, body) {
8552
9534
  printManualAssist(`Schema update for "${body.slug}"`, result);
8553
9535
  }
8554
9536
  async function wordpressSchemaDeploy(project, opts) {
8555
- const fs10 = await import("fs/promises");
8556
- const path9 = await import("path");
9537
+ const fs11 = await import("fs/promises");
9538
+ const path10 = await import("path");
8557
9539
  const yaml = await import("yaml").catch(() => null);
8558
- const filePath = path9.resolve(opts.profile);
9540
+ const filePath = path10.resolve(opts.profile);
8559
9541
  let raw;
8560
9542
  try {
8561
- raw = await fs10.readFile(filePath, "utf8");
9543
+ raw = await fs11.readFile(filePath, "utf8");
8562
9544
  } catch {
8563
9545
  throw new CliError({
8564
9546
  code: "FILE_READ_ERROR",
@@ -8663,13 +9645,13 @@ async function wordpressOnboard(project, opts) {
8663
9645
  }
8664
9646
  let profileData;
8665
9647
  if (opts.profile) {
8666
- const fs10 = await import("fs/promises");
8667
- const path9 = await import("path");
9648
+ const fs11 = await import("fs/promises");
9649
+ const path10 = await import("path");
8668
9650
  const yaml = await import("yaml").catch(() => null);
8669
- const filePath = path9.resolve(opts.profile);
9651
+ const filePath = path10.resolve(opts.profile);
8670
9652
  let raw;
8671
9653
  try {
8672
- raw = await fs10.readFile(filePath, "utf8");
9654
+ raw = await fs11.readFile(filePath, "utf8");
8673
9655
  } catch {
8674
9656
  throw new CliError({
8675
9657
  code: "FILE_READ_ERROR",
@@ -8818,7 +9800,7 @@ function resolveContent(input, command, usage, options) {
8818
9800
  }
8819
9801
  if (contentFile) {
8820
9802
  try {
8821
- return fs9.readFileSync(contentFile, "utf-8");
9803
+ return fs10.readFileSync(contentFile, "utf-8");
8822
9804
  } catch (error) {
8823
9805
  const message = error instanceof Error ? error.message : String(error);
8824
9806
  throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
@@ -9749,6 +10731,7 @@ var REGISTERED_CLI_COMMANDS = [
9749
10731
  ...BACKLINKS_CLI_COMMANDS,
9750
10732
  ...SYSTEM_CLI_COMMANDS,
9751
10733
  ...PROJECT_CLI_COMMANDS,
10734
+ ...REPORT_CLI_COMMANDS,
9752
10735
  ...KEYWORD_CLI_COMMANDS,
9753
10736
  ...COMPETITOR_CLI_COMMANDS,
9754
10737
  ...SETTINGS_CLI_COMMANDS,
@@ -9772,7 +10755,7 @@ var REGISTERED_CLI_COMMANDS = [
9772
10755
 
9773
10756
  // src/cli.ts
9774
10757
  import { createRequire as createRequire2 } from "module";
9775
- var USAGE2 = `
10758
+ var USAGE3 = `
9776
10759
  canonry \u2014 AEO monitoring CLI
9777
10760
 
9778
10761
  Usage: canonry <command> [options]
@@ -9833,7 +10816,7 @@ function extractFormat(cmdArgs) {
9833
10816
  }
9834
10817
  async function runCli(args = process.argv.slice(2)) {
9835
10818
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
9836
- console.log(USAGE2);
10819
+ console.log(USAGE3);
9837
10820
  return 0;
9838
10821
  }
9839
10822
  if (args.includes("--version") || args.includes("-v")) {