@ainyc/canonry 4.96.0 → 4.97.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +1 -0
  2. package/assets/assets/{BacklinksPage-BiuaXwM3.js → BacklinksPage-BI9BtHjp.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-C7jCdApm.js → ChartPrimitives-BcjGunGr.js} +1 -1
  4. package/assets/assets/ProjectPage-BdoberYt.js +6 -0
  5. package/assets/assets/{RunRow-DLn-qaBh.js → RunRow-BT2kpdQR.js} +1 -1
  6. package/assets/assets/{RunsPage-BArLwd1T.js → RunsPage-BAgZuX7j.js} +1 -1
  7. package/assets/assets/{SettingsPage-BJmzcMsh.js → SettingsPage-BvKcpivM.js} +1 -1
  8. package/assets/assets/{TrafficPage-DqG9hcNu.js → TrafficPage-BdGOZv1e.js} +1 -1
  9. package/assets/assets/{TrafficSourceDetailPage-yW5pdgrM.js → TrafficSourceDetailPage-C_N2D9xy.js} +1 -1
  10. package/assets/assets/{arrow-left-BUhA2bZu.js → arrow-left-C8Fbup8X.js} +1 -1
  11. package/assets/assets/{extract-error-message-D2vzFSUQ.js → extract-error-message-C29K35Gm.js} +1 -1
  12. package/assets/assets/index-BGfyt3LX.js +210 -0
  13. package/assets/assets/{index-DiykVUPr.css → index-BsZNCwg8.css} +1 -1
  14. package/assets/assets/{trash-2-C94CCTsF.js → trash-2-DWLIELE_.js} +1 -1
  15. package/assets/assets/{vendor-markdown-DK7fbRNb.js → vendor-markdown-6dwjPOKK.js} +1 -1
  16. package/assets/assets/{vendor-radix-B57xfQbP.js → vendor-radix-CQ77EO2y.js} +1 -1
  17. package/assets/assets/{vendor-recharts-ClRVR6aX.js → vendor-recharts-C9EZkgbP.js} +1 -1
  18. package/assets/assets/vendor-tanstack-Be8JdV5M.js +1 -0
  19. package/assets/index.html +6 -6
  20. package/dist/{chunk-CEYDMSQO.js → chunk-E2FDXNMW.js} +53 -7
  21. package/dist/{chunk-ROLTZPV7.js → chunk-NUGGUQH3.js} +13 -7
  22. package/dist/{chunk-IFB43PRW.js → chunk-SRJ5IGNK.js} +14 -6
  23. package/dist/{chunk-CNTBEUVE.js → chunk-TXNKKTFD.js} +90 -60
  24. package/dist/cli.js +19 -6
  25. package/dist/index.js +4 -4
  26. package/dist/{intelligence-service-SJKUVFZ6.js → intelligence-service-DC2TD7RR.js} +2 -2
  27. package/dist/mcp.js +2 -2
  28. package/package.json +8 -8
  29. package/assets/assets/ProjectPage-BHo6vIlj.js +0 -6
  30. package/assets/assets/index-afT0D_K8.js +0 -210
  31. package/assets/assets/vendor-tanstack-Dq7p98wZ.js +0 -1
@@ -180,6 +180,7 @@ import {
180
180
  notImplemented,
181
181
  notificationDtoSchema,
182
182
  parseInclusiveEndMs,
183
+ parseReportPeriodDays,
183
184
  parseRunError,
184
185
  parseWindow,
185
186
  pickClusterRepresentative,
@@ -198,6 +199,7 @@ import {
198
199
  registrableDomain,
199
200
  reportActionCategoryLabel,
200
201
  reportActionTone,
202
+ reportComparisonWindowDays,
201
203
  reportConfidenceLabel,
202
204
  reportHorizonLabel,
203
205
  reportSeverityLabel,
@@ -261,7 +263,7 @@ import {
261
263
  wordpressSchemaDeployResultDtoSchema,
262
264
  wordpressSchemaStatusResultDtoSchema,
263
265
  wordpressStatusDtoSchema
264
- } from "./chunk-CEYDMSQO.js";
266
+ } from "./chunk-E2FDXNMW.js";
265
267
 
266
268
  // src/intelligence-service.ts
267
269
  import { eq as eq37, desc as desc18, asc as asc5, and as and27, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
@@ -1301,6 +1303,7 @@ var adsConnections = sqliteTable("ads_connections", {
1301
1303
  timezone: text("timezone"),
1302
1304
  status: text("status"),
1303
1305
  lastSyncedAt: text("last_synced_at"),
1306
+ conversionTrackingConfigured: integer("conversion_tracking_configured", { mode: "boolean" }).notNull().default(false),
1304
1307
  createdAt: text("created_at").notNull(),
1305
1308
  updatedAt: text("updated_at").notNull()
1306
1309
  }, (table) => [
@@ -1364,6 +1367,7 @@ var adsInsightsDaily = sqliteTable("ads_insights_daily", {
1364
1367
  impressions: integer("impressions").notNull().default(0),
1365
1368
  clicks: integer("clicks").notNull().default(0),
1366
1369
  spendMicros: integer("spend_micros").notNull().default(0),
1370
+ conversions: integer("conversions").notNull().default(0),
1367
1371
  syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" })
1368
1372
  }, (table) => [
1369
1373
  uniqueIndex("uniq_ads_insights_daily").on(table.projectId, table.level, table.entityId, table.date),
@@ -3199,6 +3203,14 @@ var MIGRATION_VERSIONS = [
3199
3203
  )`,
3200
3204
  `CREATE INDEX IF NOT EXISTS idx_gbp_attributes_loc ON gbp_attributes_snapshots(project_id, location_name, synced_at)`
3201
3205
  ]
3206
+ },
3207
+ {
3208
+ version: 83,
3209
+ name: "ads-insights-conversions",
3210
+ statements: [
3211
+ `ALTER TABLE ads_insights_daily ADD COLUMN conversions INTEGER NOT NULL DEFAULT 0`,
3212
+ `ALTER TABLE ads_connections ADD COLUMN conversion_tracking_configured INTEGER NOT NULL DEFAULT 0`
3213
+ ]
3202
3214
  }
3203
3215
  ];
3204
3216
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -10135,18 +10147,17 @@ function renderRateDeltaTile(label, delta, unit) {
10135
10147
  <div class="delta">${deltaText}</div>
10136
10148
  </div>`;
10137
10149
  }
10138
- function renderTrafficDeltaTile(label, delta, countLabel) {
10150
+ function renderTrafficDeltaTile(label, delta, countLabel, comparisonWindowDays) {
10139
10151
  if (!delta) {
10140
10152
  return `<div class="metric"><div class="label">${escapeHtml(label)}</div><div class="value">\u2014</div><div class="delta">Not enough trend data</div></div>`;
10141
10153
  }
10142
- const deltaText = formatWindowCountDelta(delta, countLabel, `vs prior ${WHATS_CHANGED_PERIOD_DAYS} days`);
10154
+ const deltaText = formatWindowCountDelta(delta, countLabel, `vs prior ${comparisonWindowDays} days`);
10143
10155
  return `<div class="metric">
10144
10156
  <div class="label">${escapeHtml(label)}</div>
10145
10157
  <div class="value ${deltaToneClass(delta.direction)}">${formatNumber(delta.current)} <span style="font-size:14px;font-weight:500;">${deltaArrow(delta.direction)}</span></div>
10146
10158
  <div class="delta">${deltaText}</div>
10147
10159
  </div>`;
10148
10160
  }
10149
- var WHATS_CHANGED_PERIOD_DAYS = 14;
10150
10161
  function renderProviderMovements(movements, audience) {
10151
10162
  const meaningful = movements.filter((m) => m.direction !== "flat");
10152
10163
  if (meaningful.length === 0) return "";
@@ -10213,8 +10224,8 @@ function renderWhatsChanged(report, audience) {
10213
10224
  ${renderRateDeltaTile(isClient ? "AI links to your website" : "Citation rate", w.citationRate, "%")}
10214
10225
  ${renderRateDeltaTile(isClient ? "AI mentions your name" : "Mention rate", w.mentionRate, "%")}
10215
10226
  ${renderRateDeltaTile(isClient ? "Questions AI answered with you" : "Cited queries", w.citedQueryCount, "count")}
10216
- ${renderTrafficDeltaTile(isClient ? "Visitors from Google" : "GSC clicks", w.gscClicksDelta, isClient ? "visits" : "clicks")}
10217
- ${renderTrafficDeltaTile(isClient ? "Visitors from AI tools" : "AI referral sessions", w.aiReferralsDelta, isClient ? "visits" : "sessions")}
10227
+ ${renderTrafficDeltaTile(isClient ? "Visitors from Google" : "GSC clicks", w.gscClicksDelta, isClient ? "visits" : "clicks", w.comparisonWindowDays)}
10228
+ ${renderTrafficDeltaTile(isClient ? "Visitors from AI tools" : "AI referral sessions", w.aiReferralsDelta, isClient ? "visits" : "sessions", w.comparisonWindowDays)}
10218
10229
  </div>`;
10219
10230
  const movements = renderProviderMovements(w.providerMovements, audience);
10220
10231
  const winsHeading = isClient ? "What got better" : "Wins";
@@ -10679,33 +10690,36 @@ function renderAiReferrals(report) {
10679
10690
  </div>`
10680
10691
  );
10681
10692
  }
10682
- function serverActivityHeading(audience, hasData) {
10693
+ function serverActivityHeading(audience, hasData, windowDays) {
10683
10694
  const isClient = audience === "client";
10684
10695
  return {
10685
10696
  id: "server-activity",
10686
10697
  eyebrow: isClient ? "AI engine attention" : "Section 10",
10687
10698
  title: "AI Visibility \u2014 Server-Side",
10688
- intro: isClient ? hasData ? "What AI engines actually do in your server logs over the last 7 days \u2014 the other half of citations." : "Live telemetry from your server logs." : "What AI engines actually do in your server logs \u2014 direct evidence, complementary to citations (which measure what they say)."
10699
+ intro: isClient ? hasData ? `What AI engines actually do in your server logs over the last ${windowDays} days \u2014 the other half of citations.` : "Live telemetry from your server logs." : "What AI engines actually do in your server logs \u2014 direct evidence, complementary to citations (which measure what they say)."
10689
10700
  };
10690
10701
  }
10691
10702
  function renderServerActivity(report, audience) {
10692
10703
  const sa = report.serverActivity;
10693
10704
  const isClient = audience === "client";
10705
+ const windowDays = report.meta.periodDays;
10706
+ const windowLabel = `${windowDays}d`;
10707
+ const priorWindowLabel = `vs prior ${windowDays} days`;
10694
10708
  if (!sa) {
10695
10709
  if (isClient) return "";
10696
10710
  return section(
10697
- serverActivityHeading("agency", false),
10711
+ serverActivityHeading("agency", false, windowDays),
10698
10712
  renderEmpty("Connect a server-side traffic source to surface what AI engines do directly in your server logs \u2014 distinct from GA4 click-throughs.")
10699
10713
  );
10700
10714
  }
10701
10715
  if (!sa.hasData) {
10702
10716
  return section(
10703
- serverActivityHeading(audience, false),
10717
+ serverActivityHeading(audience, false, windowDays),
10704
10718
  renderEmpty(isClient ? "Your server-side traffic source is connected. Numbers will appear after the next sync." : "Source connected \u2014 collecting your first data. Numbers will appear after the next sync.")
10705
10719
  );
10706
10720
  }
10707
10721
  const formatDelta = (d, suffix) => {
10708
- const copy = formatDeltaCopy(d, suffix);
10722
+ const copy = formatDeltaCopy(d, suffix, priorWindowLabel);
10709
10723
  if (!copy) return "";
10710
10724
  return `<span class="tone-${deltaTone(d.deltaPct)}">${escapeHtml(copy)}</span>`;
10711
10725
  };
@@ -10732,7 +10746,7 @@ function renderServerActivity(report, audience) {
10732
10746
  <td class="numeric">${formatNumber(o.referralArrivals)}</td>
10733
10747
  </tr>`).join("");
10734
10748
  return section(
10735
- serverActivityHeading("client", true),
10749
+ serverActivityHeading("client", true, windowDays),
10736
10750
  `<div class="metric-grid">
10737
10751
  <div class="metric">
10738
10752
  <div class="label">AI bot requests observed</div>
@@ -10752,7 +10766,7 @@ function renderServerActivity(report, audience) {
10752
10766
  </div>
10753
10767
  ${clientOperatorRows ? `<div class="chart-card"><h3>By AI tool</h3>
10754
10768
  <table class="report-table">
10755
- <thead><tr><th>AI tool</th><th class="numeric">Bot requests (7d)</th><th class="numeric">User fetches (7d)</th><th class="numeric">Referral sessions</th></tr></thead>
10769
+ <thead><tr><th>AI tool</th><th class="numeric">Bot requests (${windowLabel})</th><th class="numeric">User fetches (${windowLabel})</th><th class="numeric">Referral sessions</th></tr></thead>
10756
10770
  <tbody>${clientOperatorRows}</tbody>
10757
10771
  </table>
10758
10772
  <p class="meta">Bot requests are bulk crawl (GPTBot, PerplexityBot, \u2026). User fetches are on-demand reads triggered by real users inside an AI surface (ChatGPT-User, Perplexity-User, \u2026). Verified means the request came from an IP the operator publishes as its own; unverified means the user-agent matched but the IP is not in a published range. User-fetch totals count both, since many genuine user fetches come from outside any published range.</p>
@@ -10793,28 +10807,28 @@ function renderServerActivity(report, audience) {
10793
10807
  const trendChart = sa.dailyTrend.length > 0 ? renderLineChart(
10794
10808
  sa.dailyTrend.map((d) => ({ x: d.date, y: d.verifiedCrawlerHits, label: d.date.slice(5) })),
10795
10809
  COLORS.series[1],
10796
- "Verified crawler hits over time (last 14 days)"
10810
+ `Verified crawler hits over time (last ${windowDays} days)`
10797
10811
  ) : "";
10798
10812
  return section(
10799
- serverActivityHeading("agency", true),
10813
+ serverActivityHeading("agency", true, windowDays),
10800
10814
  `<div class="metric-grid">
10801
10815
  <div class="metric">
10802
- <div class="label">Verified crawler hits (7d)</div>
10816
+ <div class="label">Verified crawler hits (${windowLabel})</div>
10803
10817
  <div class="value">${formatNumber(sa.verifiedCrawlerHits.current)}</div>
10804
10818
  <div class="subtitle">${formatDelta(sa.verifiedCrawlerHits, "hits")}</div>
10805
10819
  </div>
10806
10820
  <div class="metric">
10807
- <div class="label">Unverified crawler hits (7d)</div>
10821
+ <div class="label">Unverified crawler hits (${windowLabel})</div>
10808
10822
  <div class="value">${formatNumber(sa.unverifiedCrawlerHits.current)}</div>
10809
10823
  <div class="subtitle">${formatDelta(sa.unverifiedCrawlerHits, "hits")}</div>
10810
10824
  </div>
10811
10825
  <div class="metric">
10812
- <div class="label">AI user-fetch hits (7d)</div>
10826
+ <div class="label">AI user-fetch hits (${windowLabel})</div>
10813
10827
  <div class="value">${formatNumber(sa.aiUserFetchHits.current)}</div>
10814
10828
  <div class="subtitle">${formatDelta(sa.aiUserFetchHits, "hits")}</div>
10815
10829
  </div>
10816
10830
  <div class="metric">
10817
- <div class="label">AI-referral sessions (7d)</div>
10831
+ <div class="label">AI-referral sessions (${windowLabel})</div>
10818
10832
  <div class="value">${formatNumber(sa.referralArrivals.current)}</div>
10819
10833
  <div class="subtitle">${formatDelta(sa.referralArrivals, "sessions")}</div>
10820
10834
  </div>
@@ -10823,12 +10837,12 @@ function renderServerActivity(report, audience) {
10823
10837
  ${operatorRows ? `<div class="chart-card"><h3>Per AI operator</h3>
10824
10838
  <p class="meta">Verified means the request's source IP falls inside the operator's published range. Unverified bots claim the user-agent but the IP is not in a published range, so it could be the real bot or an imitator. User fetches are on-demand reads from an AI surface on behalf of a real user (ChatGPT-User, Perplexity-User, \u2026), disjoint from bulk crawl and counted whether or not the IP can be verified.</p>
10825
10839
  <table class="report-table">
10826
- <thead><tr><th>Operator</th><th class="numeric">Verified hits</th><th class="numeric">Unverified</th><th class="numeric">User fetches</th><th class="numeric">Referral sessions</th><th class="numeric">7d delta</th></tr></thead>
10840
+ <thead><tr><th>Operator</th><th class="numeric">Verified hits</th><th class="numeric">Unverified</th><th class="numeric">User fetches</th><th class="numeric">Referral sessions</th><th class="numeric">${windowLabel} delta</th></tr></thead>
10827
10841
  <tbody>${operatorRows}</tbody>
10828
10842
  </table>
10829
10843
  </div>` : ""}
10830
10844
  ${pathRows ? `<div class="chart-card"><h3>Top crawled paths</h3>
10831
- <p class="meta">Pages AI bots fetched most often (verified only, last 7d).</p>
10845
+ <p class="meta">Pages AI bots fetched most often (verified only, last ${windowLabel}).</p>
10832
10846
  <table class="report-table">
10833
10847
  <thead><tr><th>Path</th><th class="numeric">Verified hits</th><th class="numeric">Distinct operators</th></tr></thead>
10834
10848
  <tbody>${pathRows}</tbody>
@@ -11308,7 +11322,7 @@ function renderReportHtml(report, opts = {}) {
11308
11322
  <header class="header">
11309
11323
  <div class="eyebrow">AI Visibility Report</div>
11310
11324
  <h1>${escapeHtml(report.meta.project.displayName)}</h1>
11311
- <div class="subtitle">${escapeHtml(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml(report.meta.project.country)} / ${escapeHtml(report.meta.project.language.toUpperCase())}${renderHeaderLocationFragment(report.meta.location)} \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
11325
+ <div class="subtitle">${escapeHtml(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml(report.meta.project.country)} / ${escapeHtml(report.meta.project.language.toUpperCase())}${renderHeaderLocationFragment(report.meta.location)} \xB7 Last ${report.meta.periodDays} days \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
11312
11326
  </header>
11313
11327
  ${sections}
11314
11328
  <footer class="footer">Generated by <a href="https://canonry.ai">canonry</a> \xB7 ${escapeHtml(formatIsoDate(report.meta.generatedAt))}</footer>
@@ -11324,10 +11338,8 @@ var TOP_LANDING_PAGES_LIMIT = 20;
11324
11338
  var TOP_AI_REFERRAL_PAGES_LIMIT = 10;
11325
11339
  var TOP_CAMPAIGN_LIMIT = 10;
11326
11340
  var INSIGHT_LOOKBACK_RUNS = 5;
11327
- var REPORT_WINDOW_DAYS = 30;
11328
- var SERVER_ACTIVITY_HEADLINE_DAYS = 7;
11329
- var SERVER_ACTIVITY_TREND_DAYS = 14;
11330
11341
  var SERVER_ACTIVITY_TOP_PATHS_LIMIT = 10;
11342
+ var GA_WINDOW_SUMMARY_KEYS = { 7: "7d", 30: "30d", 90: "90d" };
11331
11343
  function windowStartDate(endDate, windowDays) {
11332
11344
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(endDate);
11333
11345
  if (!m) return endDate;
@@ -11373,12 +11385,12 @@ function loadQueryLookup(db, projectId) {
11373
11385
  for (const row of rows) byId.set(row.id, row.query);
11374
11386
  return { byId };
11375
11387
  }
11376
- function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries) {
11388
+ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries, windowDays) {
11377
11389
  const allRows = db.select().from(gscSearchData).where(eq14(gscSearchData.projectId, projectId)).all();
11378
11390
  if (allRows.length === 0) return null;
11379
11391
  let maxDate = "";
11380
11392
  for (const r of allRows) if (r.date > maxDate) maxDate = r.date;
11381
- const startDate = windowStartDate(maxDate, REPORT_WINDOW_DAYS);
11393
+ const startDate = windowStartDate(maxDate, windowDays);
11382
11394
  const rows = allRows.filter((r) => r.date >= startDate && r.date <= maxDate);
11383
11395
  if (rows.length === 0) return null;
11384
11396
  let totalClicks = 0;
@@ -11445,19 +11457,20 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
11445
11457
  gscButNotTracked
11446
11458
  };
11447
11459
  }
11448
- function buildGaSection(db, projectId) {
11449
- const windowSummary = db.select().from(gaTrafficWindowSummaries).where(
11460
+ function buildGaSection(db, projectId, windowDays) {
11461
+ const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[windowDays];
11462
+ const windowSummary = gaWindowKey ? db.select().from(gaTrafficWindowSummaries).where(
11450
11463
  and9(
11451
11464
  eq14(gaTrafficWindowSummaries.projectId, projectId),
11452
- eq14(gaTrafficWindowSummaries.windowKey, "30d")
11465
+ eq14(gaTrafficWindowSummaries.windowKey, gaWindowKey)
11453
11466
  )
11454
- ).limit(1).get();
11467
+ ).limit(1).get() : void 0;
11455
11468
  const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(eq14(gaTrafficSummaries.projectId, projectId)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
11456
11469
  const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(eq14(gaTrafficSnapshots.projectId, projectId)).all();
11457
11470
  if (!windowSummary && !fallbackSummary && allSnapshotRows.length === 0) return null;
11458
11471
  let snapshotMaxDate = "";
11459
11472
  for (const r of allSnapshotRows) if (r.date > snapshotMaxDate) snapshotMaxDate = r.date;
11460
- const snapshotStartDate = snapshotMaxDate ? windowStartDate(snapshotMaxDate, REPORT_WINDOW_DAYS) : "";
11473
+ const snapshotStartDate = snapshotMaxDate ? windowStartDate(snapshotMaxDate, windowDays) : "";
11461
11474
  const snapshotRows = snapshotStartDate ? allSnapshotRows.filter((r) => r.date >= snapshotStartDate && r.date <= snapshotMaxDate) : allSnapshotRows;
11462
11475
  const totalSessions = windowSummary?.totalSessions ?? fallbackSummary?.totalSessions ?? snapshotRows.reduce((s, r) => s + r.sessions, 0);
11463
11476
  const totalUsers = windowSummary?.totalUsers ?? fallbackSummary?.totalUsers ?? snapshotRows.reduce((s, r) => s + r.users, 0);
@@ -11623,7 +11636,7 @@ function nonSubresourceReferralPathCondition() {
11623
11636
  AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '%.woff2'
11624
11637
  `;
11625
11638
  }
11626
- function buildServerActivity(db, projectId) {
11639
+ function buildServerActivity(db, projectId, windowDays) {
11627
11640
  const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
11628
11641
  and9(
11629
11642
  eq14(trafficSources.projectId, projectId),
@@ -11633,9 +11646,9 @@ function buildServerActivity(db, projectId) {
11633
11646
  if (sourceRows.length === 0) return null;
11634
11647
  const now = /* @__PURE__ */ new Date();
11635
11648
  const headlineEnd = now.toISOString();
11636
- const headlineStartMs = now.getTime() - SERVER_ACTIVITY_HEADLINE_DAYS * 24 * 60 * 6e4;
11637
- const priorStartMs = headlineStartMs - SERVER_ACTIVITY_HEADLINE_DAYS * 24 * 60 * 6e4;
11638
- const trendStartMs = now.getTime() - SERVER_ACTIVITY_TREND_DAYS * 24 * 60 * 6e4;
11649
+ const headlineStartMs = now.getTime() - windowDays * 24 * 60 * 6e4;
11650
+ const priorStartMs = headlineStartMs - windowDays * 24 * 60 * 6e4;
11651
+ const trendStartMs = headlineStartMs;
11639
11652
  const headlineStart = new Date(headlineStartMs).toISOString();
11640
11653
  const priorStart = new Date(priorStartMs).toISOString();
11641
11654
  const trendStart = new Date(trendStartMs).toISOString();
@@ -12419,8 +12432,6 @@ function buildAgencyDiagnostics(input) {
12419
12432
  diagnostics
12420
12433
  };
12421
12434
  }
12422
- var WHATS_CHANGED_PERIOD_DAYS2 = 14;
12423
- var WHATS_CHANGED_MIN_TREND_POINTS = WHATS_CHANGED_PERIOD_DAYS2 * 2;
12424
12435
  var RATE_REAL_MOVEMENT_THRESHOLD_PP = 3;
12425
12436
  var COUNT_REAL_MOVEMENT_THRESHOLD = 0.5;
12426
12437
  var WIN_REGRESSION_LIMIT = 5;
@@ -12429,10 +12440,10 @@ function rateDirection(delta, threshold = 0.5) {
12429
12440
  if (delta < -threshold) return "down";
12430
12441
  return "flat";
12431
12442
  }
12432
- function periodOverPeriodDelta(trend) {
12433
- if (trend.length < WHATS_CHANGED_MIN_TREND_POINTS) return null;
12434
- const tail = trend.slice(-WHATS_CHANGED_PERIOD_DAYS2);
12435
- const prior = trend.slice(-WHATS_CHANGED_PERIOD_DAYS2 * 2, -WHATS_CHANGED_PERIOD_DAYS2);
12443
+ function periodOverPeriodDelta(trend, halfWindow) {
12444
+ if (trend.length < halfWindow * 2) return null;
12445
+ const tail = trend.slice(-halfWindow);
12446
+ const prior = trend.slice(-halfWindow * 2, -halfWindow);
12436
12447
  const current = tail.reduce((s, p) => s + p.value, 0);
12437
12448
  const priorTotal = prior.reduce((s, p) => s + p.value, 0);
12438
12449
  const deltaAbs = current - priorTotal;
@@ -12444,7 +12455,7 @@ function periodOverPeriodDelta(trend) {
12444
12455
  direction: rateDirection(deltaAbs, 0)
12445
12456
  };
12446
12457
  }
12447
- function buildWhatsChangedHeadline(citation, gscClicks, aiReferrals, enoughHistory, trendLength) {
12458
+ function buildWhatsChangedHeadline(citation, gscClicks, aiReferrals, enoughHistory, trendLength, comparisonWindowDays) {
12448
12459
  if (!enoughHistory) {
12449
12460
  return `Building baseline (${trendLength} of ${MIN_TREND_POINTS} checks completed). Trends appear after a few more checks.`;
12450
12461
  }
@@ -12457,15 +12468,15 @@ function buildWhatsChangedHeadline(citation, gscClicks, aiReferrals, enoughHisto
12457
12468
  }
12458
12469
  if (aiReferrals && aiReferrals.direction !== "flat") {
12459
12470
  const arrow = aiReferrals.direction === "up" ? "\u2191" : "\u2193";
12460
- parts.push(`AI referrals ${arrow}${Math.abs(aiReferrals.deltaAbs)} sessions vs prior 14 days`);
12471
+ parts.push(`AI referrals ${arrow}${Math.abs(aiReferrals.deltaAbs)} sessions vs prior ${comparisonWindowDays} days`);
12461
12472
  } else if (gscClicks && gscClicks.direction !== "flat") {
12462
12473
  const arrow = gscClicks.direction === "up" ? "\u2191" : "\u2193";
12463
- parts.push(`GSC clicks ${arrow}${Math.abs(gscClicks.deltaAbs)} vs prior 14 days`);
12474
+ parts.push(`GSC clicks ${arrow}${Math.abs(gscClicks.deltaAbs)} vs prior ${comparisonWindowDays} days`);
12464
12475
  }
12465
12476
  return parts.length > 0 ? `${parts.join(" \xB7 ")}.` : "No meaningful movement vs the prior period.";
12466
12477
  }
12467
12478
  function buildWhatsChanged(input) {
12468
- const { citationsTrend, gsc, aiReferrals, insights: insightList } = input;
12479
+ const { citationsTrend, gsc, aiReferrals, insights: insightList, comparisonWindowDays } = input;
12469
12480
  const baseline = isTrendBaseline(citationsTrend);
12470
12481
  const latest = citationsTrend.at(-1);
12471
12482
  const prior = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
@@ -12502,8 +12513,8 @@ function buildWhatsChanged(input) {
12502
12513
  }
12503
12514
  providerMovements.sort((a, b) => Math.abs(b.deltaAbs) - Math.abs(a.deltaAbs));
12504
12515
  }
12505
- const gscClicksDelta = gsc ? periodOverPeriodDelta(gsc.trend.map((t) => ({ date: t.date, value: t.clicks }))) : null;
12506
- const aiReferralsDelta = aiReferrals ? periodOverPeriodDelta(aiReferrals.trend.map((t) => ({ date: t.date, value: t.sessions }))) : null;
12516
+ const gscClicksDelta = gsc ? periodOverPeriodDelta(gsc.trend.map((t) => ({ date: t.date, value: t.clicks })), comparisonWindowDays) : null;
12517
+ const aiReferralsDelta = aiReferrals ? periodOverPeriodDelta(aiReferrals.trend.map((t) => ({ date: t.date, value: t.sessions })), comparisonWindowDays) : null;
12507
12518
  const wins = insightList.filter((i) => i.type === "gain").slice(0, WIN_REGRESSION_LIMIT);
12508
12519
  const regressions = insightList.filter((i) => i.type === "regression").slice(0, WIN_REGRESSION_LIMIT);
12509
12520
  const headline = buildWhatsChangedHeadline(
@@ -12511,7 +12522,8 @@ function buildWhatsChanged(input) {
12511
12522
  gscClicksDelta,
12512
12523
  aiReferralsDelta,
12513
12524
  enoughHistory,
12514
- citationsTrend.length
12525
+ citationsTrend.length,
12526
+ comparisonWindowDays
12515
12527
  );
12516
12528
  return {
12517
12529
  enoughHistory,
@@ -12521,14 +12533,16 @@ function buildWhatsChanged(input) {
12521
12533
  citedQueryCount,
12522
12534
  gscClicksDelta,
12523
12535
  aiReferralsDelta,
12536
+ comparisonWindowDays,
12524
12537
  providerMovements,
12525
12538
  wins,
12526
12539
  regressions
12527
12540
  };
12528
12541
  }
12529
- function buildProjectReport(db, projectName) {
12542
+ function buildProjectReport(db, projectName, periodDays) {
12530
12543
  const project = resolveProject(db, projectName);
12531
12544
  const queryLookup = loadQueryLookup(db, project.id);
12545
+ const comparisonWindowDays = reportComparisonWindowDays(periodDays);
12532
12546
  const allRuns = db.select().from(runs).where(and9(eq14(runs.projectId, project.id), notProbeRun())).orderBy(desc7(runs.createdAt), desc7(runs.id)).all();
12533
12547
  const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
12534
12548
  const completedVisRunGroups = groupRunsByCreatedAt(
@@ -12568,12 +12582,13 @@ function buildProjectReport(db, projectName) {
12568
12582
  project.id,
12569
12583
  projectBrandNames,
12570
12584
  project.canonicalDomain,
12571
- trackedQueries
12585
+ trackedQueries,
12586
+ periodDays
12572
12587
  );
12573
- const gaSection = buildGaSection(db, project.id);
12588
+ const gaSection = buildGaSection(db, project.id, periodDays);
12574
12589
  const socialSection = buildSocialReferrals(db, project.id);
12575
12590
  const aiReferralsSection = buildAiReferrals(db, project.id);
12576
- const serverActivitySection = buildServerActivity(db, project.id);
12591
+ const serverActivitySection = buildServerActivity(db, project.id, periodDays);
12577
12592
  const indexingHealthSection = buildIndexingHealth(db, project.id);
12578
12593
  const citationsTrend = buildCitationsTrend(db, project.id, queryLookup, latestRunLocation);
12579
12594
  const insightList = buildInsightList(db, project.id, latestRunLocation);
@@ -12592,7 +12607,8 @@ function buildProjectReport(db, projectName) {
12592
12607
  citationsTrend,
12593
12608
  gsc: gscSection,
12594
12609
  aiReferrals: aiReferralsSection,
12595
- insights: insightList
12610
+ insights: insightList,
12611
+ comparisonWindowDays
12596
12612
  });
12597
12613
  const totalQueryCount = queryLookup.byId.size;
12598
12614
  const citedQueryIds = /* @__PURE__ */ new Set();
@@ -12708,7 +12724,8 @@ function buildProjectReport(db, projectName) {
12708
12724
  location: reportLocation,
12709
12725
  providerLocationHandling,
12710
12726
  periodStart,
12711
- periodEnd
12727
+ periodEnd,
12728
+ periodDays
12712
12729
  },
12713
12730
  executiveSummary,
12714
12731
  citationScorecard,
@@ -12744,12 +12761,14 @@ function reportFilenameFor(project, generatedAt, audience) {
12744
12761
  }
12745
12762
  async function reportRoutes(app) {
12746
12763
  app.get("/projects/:name/report", async (request, reply) => {
12747
- const dto = buildProjectReport(app.db, request.params.name);
12764
+ const periodDays = parseReportPeriodDays(request.query.period);
12765
+ const dto = buildProjectReport(app.db, request.params.name, periodDays);
12748
12766
  return reply.send(dto);
12749
12767
  });
12750
12768
  app.get("/projects/:name/report.html", async (request, reply) => {
12751
12769
  const audience = parseReportAudience(request.query.audience);
12752
- const dto = buildProjectReport(app.db, request.params.name);
12770
+ const periodDays = parseReportPeriodDays(request.query.period);
12771
+ const dto = buildProjectReport(app.db, request.params.name, periodDays);
12753
12772
  const html = renderReportHtml(dto, { audience });
12754
12773
  const filename = reportFilenameFor(dto.meta.project, dto.meta.generatedAt, audience);
12755
12774
  reply.header("Content-Type", "text/html; charset=utf-8");
@@ -14109,6 +14128,12 @@ var analyticsWindowParameter = {
14109
14128
  description: "Time window for analytics queries.",
14110
14129
  schema: { type: "string", enum: ["7d", "30d", "90d", "all"] }
14111
14130
  };
14131
+ var reportPeriodQueryParameter = {
14132
+ name: "period",
14133
+ in: "query",
14134
+ description: "Report window in days \u2014 one of 7, 14, 30, 90 (default 30). Scopes the GSC, GA4, and server-side AI activity sections and the period-over-period comparisons to this window.",
14135
+ schema: { type: "integer", enum: [7, 14, 30, 90] }
14136
+ };
14112
14137
  var sinceQueryParameter = {
14113
14138
  name: "since",
14114
14139
  in: "query",
@@ -16843,7 +16868,7 @@ var routeCatalog = [
16843
16868
  summary: "Aggregated canonical AEO report",
16844
16869
  tags: ["report"],
16845
16870
  description: "Bundles every section the canonry-report HTML output needs (executive summary, client summary, agency diagnostics, action plan, citation scorecard, competitor landscape \u2014 citation + mention landscapes, AI citation sources, GSC, GA4, social/AI referrals, indexing health, citations trend, insights, and recommended next steps) into a single canonical JSON payload. Backs `canonry report <project>` and MCP report reads.",
16846
- parameters: [nameParameter],
16871
+ parameters: [nameParameter, reportPeriodQueryParameter],
16847
16872
  responses: {
16848
16873
  200: jsonResponse("Report returned.", "ProjectReportDto"),
16849
16874
  404: errorResponse("Project not found.")
@@ -16855,7 +16880,7 @@ var routeCatalog = [
16855
16880
  summary: "Standalone HTML AEO report",
16856
16881
  tags: ["report"],
16857
16882
  description: "Server-rendered self-contained HTML version of the project report. Same data as `/projects/{name}/report` (JSON), rendered through the canonry HTML report renderer in agency or client mode. Returns `text/html` with `Content-Disposition: attachment` so browsers download it as `canonry-report-<project>-<audience>-YYYY-MM-DD.html`. Open in a browser and Print \u2192 Save as PDF for a PDF copy.",
16858
- parameters: [nameParameter, reportAudienceQueryParameter],
16883
+ parameters: [nameParameter, reportAudienceQueryParameter, reportPeriodQueryParameter],
16859
16884
  responses: {
16860
16885
  200: { description: "HTML report returned.", content: { "text/html": { schema: { type: "string" } } } },
16861
16886
  404: errorResponse("Project not found.")
@@ -21512,7 +21537,8 @@ function statusDto(row) {
21512
21537
  currencyCode: row.currencyCode,
21513
21538
  timezone: row.timezone,
21514
21539
  status: row.status,
21515
- lastSyncedAt: row.lastSyncedAt
21540
+ lastSyncedAt: row.lastSyncedAt,
21541
+ conversionTrackingConfigured: row.conversionTrackingConfigured
21516
21542
  };
21517
21543
  }
21518
21544
  function creativeDto(raw) {
@@ -21710,6 +21736,7 @@ async function adsRoutes(app, opts) {
21710
21736
  impressions: row.impressions,
21711
21737
  clicks: row.clicks,
21712
21738
  spendMicros: row.spendMicros,
21739
+ conversions: row.conversions,
21713
21740
  ctr: adsCtr(row.clicks, row.impressions),
21714
21741
  cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
21715
21742
  }));
@@ -21730,12 +21757,14 @@ async function adsRoutes(app, opts) {
21730
21757
  let impressions = 0;
21731
21758
  let clicks = 0;
21732
21759
  let spendMicros = 0;
21760
+ let conversions = 0;
21733
21761
  let fromDate = null;
21734
21762
  let toDate = null;
21735
21763
  for (const insight of campaignInsights) {
21736
21764
  impressions += insight.impressions;
21737
21765
  clicks += insight.clicks;
21738
21766
  spendMicros += insight.spendMicros;
21767
+ conversions += insight.conversions;
21739
21768
  if (fromDate === null || insight.date < fromDate) fromDate = insight.date;
21740
21769
  if (toDate === null || insight.date > toDate) toDate = insight.date;
21741
21770
  }
@@ -21752,6 +21781,7 @@ async function adsRoutes(app, opts) {
21752
21781
  impressions,
21753
21782
  clicks,
21754
21783
  spendMicros,
21784
+ conversions,
21755
21785
  ctr: adsCtr(clicks, impressions),
21756
21786
  cpcMicros: adsCpcMicros(spendMicros, clicks)
21757
21787
  }
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  setTelemetrySource,
28
28
  showFirstRunNotice,
29
29
  trackEvent
30
- } from "./chunk-ROLTZPV7.js";
30
+ } from "./chunk-NUGGUQH3.js";
31
31
  import {
32
32
  CliError,
33
33
  EXIT_SYSTEM_ERROR,
@@ -44,7 +44,7 @@ import {
44
44
  saveConfig,
45
45
  saveConfigPatch,
46
46
  usageError
47
- } from "./chunk-IFB43PRW.js";
47
+ } from "./chunk-SRJ5IGNK.js";
48
48
  import {
49
49
  apiKeys,
50
50
  createClient,
@@ -52,7 +52,7 @@ import {
52
52
  projects,
53
53
  queries,
54
54
  renderReportHtml
55
- } from "./chunk-CNTBEUVE.js";
55
+ } from "./chunk-TXNKKTFD.js";
56
56
  import {
57
57
  BacklinkSources,
58
58
  CcReleaseSyncStatuses,
@@ -60,6 +60,7 @@ import {
60
60
  CheckStatuses,
61
61
  CitationStates,
62
62
  READ_ONLY_SCOPE,
63
+ REPORT_PERIOD_OPTIONS,
63
64
  RunStatuses,
64
65
  TrafficEventKinds,
65
66
  backlinkSourceSchema,
@@ -77,7 +78,7 @@ import {
77
78
  providerQuotaPolicySchema,
78
79
  resolveProviderInput,
79
80
  winnabilityClassSchema
80
- } from "./chunk-CEYDMSQO.js";
81
+ } from "./chunk-E2FDXNMW.js";
81
82
 
82
83
  // src/cli.ts
83
84
  import { pathToFileURL } from "url";
@@ -7868,7 +7869,7 @@ function defaultOutputPath(project, audience) {
7868
7869
  }
7869
7870
  async function runReportCommand(project, opts = {}) {
7870
7871
  const client = createApiClient();
7871
- const report = await client.getReport(project);
7872
+ const report = await client.getReport(project, opts.period !== void 0 ? { period: opts.period } : void 0);
7872
7873
  const audience = opts.audience ?? "agency";
7873
7874
  if (isMachineFormat(opts.format)) {
7874
7875
  console.log(JSON.stringify(report, null, 2));
@@ -7885,12 +7886,22 @@ async function runReportCommand(project, opts = {}) {
7885
7886
  }
7886
7887
 
7887
7888
  // src/cli-commands/report.ts
7888
- var USAGE3 = "canonry report <project> [--audience agency|client] [--output <path>] [--format json]";
7889
+ var USAGE3 = "canonry report <project> [--audience agency|client] [--period 7|14|30|90] [--output <path>] [--format json]";
7889
7890
  function parseAudience(value) {
7890
7891
  if (value === void 0) return void 0;
7891
7892
  if (value === "agency" || value === "client") return value;
7892
7893
  throw usageError(`Error: --audience must be "agency" or "client"
7893
7894
 
7895
+ Usage: ${USAGE3}`);
7896
+ }
7897
+ function parsePeriod(value) {
7898
+ if (value === void 0) return void 0;
7899
+ const n = Number(value);
7900
+ if (Number.isInteger(n) && REPORT_PERIOD_OPTIONS.includes(n)) {
7901
+ return n;
7902
+ }
7903
+ throw usageError(`Error: --period must be one of ${REPORT_PERIOD_OPTIONS.join(", ")}
7904
+
7894
7905
  Usage: ${USAGE3}`);
7895
7906
  }
7896
7907
  var REPORT_CLI_COMMANDS = [
@@ -7899,6 +7910,7 @@ var REPORT_CLI_COMMANDS = [
7899
7910
  usage: USAGE3,
7900
7911
  options: {
7901
7912
  audience: { type: "string" },
7913
+ period: { type: "string" },
7902
7914
  output: { type: "string", short: "o" }
7903
7915
  },
7904
7916
  run: async (input) => {
@@ -7906,6 +7918,7 @@ var REPORT_CLI_COMMANDS = [
7906
7918
  await runReportCommand(project, {
7907
7919
  format: input.format,
7908
7920
  audience: parseAudience(getString(input.values, "audience")),
7921
+ period: parsePeriod(getString(input.values, "period")),
7909
7922
  output: getString(input.values, "output")
7910
7923
  });
7911
7924
  }
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-ROLTZPV7.js";
3
+ } from "./chunk-NUGGUQH3.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-IFB43PRW.js";
7
- import "./chunk-CNTBEUVE.js";
8
- import "./chunk-CEYDMSQO.js";
6
+ } from "./chunk-SRJ5IGNK.js";
7
+ import "./chunk-TXNKKTFD.js";
8
+ import "./chunk-E2FDXNMW.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-CNTBEUVE.js";
4
- import "./chunk-CEYDMSQO.js";
3
+ } from "./chunk-TXNKKTFD.js";
4
+ import "./chunk-E2FDXNMW.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  PACKAGE_VERSION,
4
4
  canonryMcpTools,
5
5
  createApiClient
6
- } from "./chunk-IFB43PRW.js";
6
+ } from "./chunk-SRJ5IGNK.js";
7
7
  import {
8
8
  isReadOnlyKey
9
- } from "./chunk-CEYDMSQO.js";
9
+ } from "./chunk-E2FDXNMW.js";
10
10
 
11
11
  // src/mcp/cli.ts
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";