@ainyc/canonry 4.34.0 → 4.36.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.
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-5EBN7736.js";
8
+ } from "./chunk-O7S623DL.js";
9
9
  import {
10
10
  DEFAULT_RUN_HISTORY_LIMIT,
11
11
  IntelligenceService,
@@ -29,6 +29,7 @@ import {
29
29
  buildContentTargetRows,
30
30
  buildGapQueryScore,
31
31
  buildInventory,
32
+ buildMentionCoverage,
32
33
  buildMentionLandscape,
33
34
  buildMovementSummary,
34
35
  buildOverviewCompetitors,
@@ -39,6 +40,7 @@ import {
39
40
  ccReleaseSyncs,
40
41
  competitors,
41
42
  crawlerEventsHourly,
43
+ createClient,
42
44
  createLogger,
43
45
  discoveryProbes,
44
46
  discoverySessions,
@@ -60,6 +62,7 @@ import {
60
62
  isBlogShapedQuery,
61
63
  isTrendBaseline,
62
64
  mapOpportunitiesToNextSteps,
65
+ migrate,
63
66
  notifications,
64
67
  parseJsonColumn,
65
68
  pickGroupRepresentative,
@@ -71,7 +74,7 @@ import {
71
74
  schedules,
72
75
  trafficSources,
73
76
  usageCounters
74
- } from "./chunk-7256SFYT.js";
77
+ } from "./chunk-JQQXMCQ7.js";
75
78
  import {
76
79
  AGENT_MEMORY_VALUE_MAX_BYTES,
77
80
  AGENT_PROVIDER_IDS,
@@ -90,6 +93,7 @@ import {
90
93
  DiscoveryCompetitorTypes,
91
94
  DiscoverySessionStatuses,
92
95
  MemorySources,
96
+ ProviderNames,
93
97
  RunKinds,
94
98
  RunStatuses,
95
99
  RunTriggers,
@@ -127,6 +131,7 @@ import {
127
131
  discoveryBucketSchema,
128
132
  discoveryPromoteRequestSchema,
129
133
  discoveryRunRequestSchema,
134
+ effectiveBrandNames,
130
135
  effectiveDomains,
131
136
  emptyCitationVisibility,
132
137
  extractAnswerMentions,
@@ -144,7 +149,9 @@ import {
144
149
  isBrowserProvider,
145
150
  keywordGenerateRequestSchema,
146
151
  locationContextSchema,
152
+ mentionStateFromAnswerMentioned,
147
153
  missingDependency,
154
+ normalizeProjectAliases,
148
155
  normalizeProjectDomain,
149
156
  normalizeUrlPath,
150
157
  notFound,
@@ -180,7 +187,7 @@ import {
180
187
  visibilityStateFromAnswerMentioned,
181
188
  windowCutoff,
182
189
  wordpressEnvSchema
183
- } from "./chunk-XW3F5EEW.js";
190
+ } from "./chunk-XJVYVURK.js";
184
191
 
185
192
  // src/telemetry.ts
186
193
  import crypto from "crypto";
@@ -333,13 +340,146 @@ function trackEvent(event, properties, options) {
333
340
  }).finally(() => clearTimeout(timeout));
334
341
  }
335
342
 
343
+ // src/update-check.ts
344
+ import { createRequire as createRequire2 } from "module";
345
+ var _require2 = createRequire2(import.meta.url);
346
+ var { version: PKG_VERSION } = _require2("../package.json");
347
+ var PKG_NAME = "@ainyc/canonry";
348
+ var NPM_DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PKG_NAME}/dist-tags`;
349
+ var NPM_PACKAGE_URL = `https://www.npmjs.com/package/${PKG_NAME}`;
350
+ var FETCH_TIMEOUT_MS = 1500;
351
+ function isUpdateCheckEnabled() {
352
+ if (process.env.CANONRY_DISABLE_UPDATE_CHECK === "1") return false;
353
+ if (process.env.DO_NOT_TRACK === "1") return false;
354
+ if (process.env.CI) return false;
355
+ if (!configExists()) return true;
356
+ try {
357
+ const raw = loadConfigRaw();
358
+ return raw?.updateCheck !== false;
359
+ } catch {
360
+ return true;
361
+ }
362
+ }
363
+ function compareSemver(a, b) {
364
+ const parse = (v) => {
365
+ const core = v.split(/[-+]/)[0];
366
+ if (!core) return null;
367
+ const parts = core.split(".");
368
+ if (parts.length < 3) return null;
369
+ const nums = [];
370
+ for (let i = 0; i < 3; i++) {
371
+ const n = Number(parts[i]);
372
+ if (!Number.isInteger(n) || n < 0) return null;
373
+ nums.push(n);
374
+ }
375
+ return [nums[0], nums[1], nums[2]];
376
+ };
377
+ const pa = parse(a);
378
+ const pb = parse(b);
379
+ if (!pa || !pb) return 0;
380
+ for (let i = 0; i < 3; i++) {
381
+ if (pa[i] > pb[i]) return 1;
382
+ if (pa[i] < pb[i]) return -1;
383
+ }
384
+ return 0;
385
+ }
386
+ async function fetchLatestVersion(opts) {
387
+ const controller = new AbortController();
388
+ const timeout = setTimeout(() => controller.abort(), opts?.timeoutMs ?? FETCH_TIMEOUT_MS);
389
+ timeout.unref();
390
+ try {
391
+ const res = await fetch(NPM_DIST_TAGS_URL, {
392
+ signal: controller.signal,
393
+ headers: { accept: "application/json" }
394
+ });
395
+ if (!res.ok) return null;
396
+ const data = await res.json();
397
+ if (typeof data.latest !== "string") return null;
398
+ return data.latest;
399
+ } catch {
400
+ return null;
401
+ } finally {
402
+ clearTimeout(timeout);
403
+ }
404
+ }
405
+ function buildUpdateAvailable(current, latest) {
406
+ if (compareSemver(latest, current) <= 0) return null;
407
+ return {
408
+ current,
409
+ latest,
410
+ url: NPM_PACKAGE_URL,
411
+ upgradeCommand: `npm install -g ${PKG_NAME}`
412
+ };
413
+ }
414
+ async function checkLatestVersionForCli(opts) {
415
+ if (!isUpdateCheckEnabled()) return null;
416
+ if (!configExists()) return null;
417
+ const now = opts?.now ? opts.now() : /* @__PURE__ */ new Date();
418
+ const ttlMs = (opts?.ttlHours ?? 24) * 60 * 60 * 1e3;
419
+ let raw;
420
+ try {
421
+ raw = loadConfigRaw();
422
+ } catch {
423
+ return null;
424
+ }
425
+ if (!raw) return null;
426
+ const lastCheckedAt = raw.lastUpdateCheckAt ? Date.parse(raw.lastUpdateCheckAt) : NaN;
427
+ const cachedLatest = typeof raw.lastKnownLatestVersion === "string" ? raw.lastKnownLatestVersion : void 0;
428
+ if (Number.isFinite(lastCheckedAt) && now.getTime() - lastCheckedAt < ttlMs) {
429
+ if (!cachedLatest) return null;
430
+ return buildUpdateAvailable(PKG_VERSION, cachedLatest);
431
+ }
432
+ const latest = await fetchLatestVersion();
433
+ if (!latest) {
434
+ try {
435
+ saveConfigPatch({ lastUpdateCheckAt: now.toISOString() });
436
+ } catch {
437
+ }
438
+ return null;
439
+ }
440
+ try {
441
+ saveConfigPatch({
442
+ lastUpdateCheckAt: now.toISOString(),
443
+ lastKnownLatestVersion: latest
444
+ });
445
+ } catch {
446
+ }
447
+ return buildUpdateAvailable(PKG_VERSION, latest);
448
+ }
449
+ var memoryCache = null;
450
+ var inFlight = null;
451
+ function startBackgroundRefresh(getNow) {
452
+ if (inFlight) return;
453
+ inFlight = (async () => {
454
+ try {
455
+ const latest = await fetchLatestVersion();
456
+ memoryCache = { fetchedAt: getNow(), latest };
457
+ } catch {
458
+ memoryCache = { fetchedAt: getNow(), latest: null };
459
+ } finally {
460
+ inFlight = null;
461
+ }
462
+ })();
463
+ }
464
+ function checkLatestVersionForServer(opts) {
465
+ if (!isUpdateCheckEnabled()) return null;
466
+ const getNow = opts?.now ?? Date.now;
467
+ const ttl = opts?.ttlMs ?? 60 * 60 * 1e3;
468
+ const now = getNow();
469
+ if (!memoryCache || now - memoryCache.fetchedAt >= ttl) {
470
+ startBackgroundRefresh(getNow);
471
+ }
472
+ if (!memoryCache || !memoryCache.latest) return null;
473
+ return buildUpdateAvailable(PKG_VERSION, memoryCache.latest);
474
+ }
475
+
336
476
  // src/server.ts
337
- import { createRequire as createRequire3 } from "module";
477
+ import { createRequire as createRequire4 } from "module";
338
478
  import crypto34 from "crypto";
339
- import fs12 from "fs";
340
- import path14 from "path";
479
+ import fs13 from "fs";
480
+ import path15 from "path";
341
481
  import { fileURLToPath as fileURLToPath2 } from "url";
342
- import { eq as eq40 } from "drizzle-orm";
482
+ import { eq as eq41 } from "drizzle-orm";
343
483
  import Fastify from "fastify";
344
484
 
345
485
  // ../api-routes/src/auth.ts
@@ -409,7 +549,7 @@ async function authPlugin(app, opts = {}) {
409
549
 
410
550
  // ../api-routes/src/projects.ts
411
551
  import crypto4 from "crypto";
412
- import { eq as eq3 } from "drizzle-orm";
552
+ import { eq as eq3, sql as sql2 } from "drizzle-orm";
413
553
 
414
554
  // ../api-routes/src/helpers.ts
415
555
  import crypto3 from "crypto";
@@ -440,7 +580,11 @@ function resolveSnapshotMentionResult(snapshot, project) {
440
580
  canonicalDomain: project.canonicalDomain,
441
581
  ownedDomains: normalizeOwnedDomains(project.ownedDomains)
442
582
  });
443
- return extractAnswerMentions(snapshot.answerText, project.displayName, domains);
583
+ const brandNames = effectiveBrandNames({
584
+ displayName: project.displayName,
585
+ aliases: normalizeOwnedDomains(project.aliases)
586
+ });
587
+ return extractAnswerMentions(snapshot.answerText, brandNames, domains);
444
588
  }
445
589
  if (typeof snapshot.answerMentioned === "boolean") {
446
590
  return { mentioned: snapshot.answerMentioned, matchedTerms: [] };
@@ -453,6 +597,9 @@ function resolveSnapshotAnswerMentioned(snapshot, project) {
453
597
  function resolveSnapshotVisibilityState(snapshot, project) {
454
598
  return visibilityStateFromAnswerMentioned(resolveSnapshotMentionResult(snapshot, project).mentioned);
455
599
  }
600
+ function resolveSnapshotMentionState(snapshot, project) {
601
+ return mentionStateFromAnswerMentioned(resolveSnapshotMentionResult(snapshot, project).mentioned);
602
+ }
456
603
  function resolveSnapshotMatchedTerms(snapshot, project) {
457
604
  return resolveSnapshotMentionResult(snapshot, project).matchedTerms;
458
605
  }
@@ -503,12 +650,16 @@ async function projectRoutes(app, opts) {
503
650
  });
504
651
  }
505
652
  const nextAutoExtractBacklinks = body.autoExtractBacklinks !== void 0 ? body.autoExtractBacklinks ? 1 : 0 : existing?.autoExtractBacklinks ?? 0;
653
+ const nextAliases = normalizeProjectAliases(body.displayName, body.aliases ?? []);
506
654
  if (existing) {
655
+ const prevAliases = parseJsonColumn(existing.aliases, []);
656
+ const aliasesChanged = !aliasArraysEqual(prevAliases, nextAliases);
507
657
  app.db.transaction((tx) => {
508
658
  tx.update(projects).set({
509
659
  displayName: body.displayName,
510
660
  canonicalDomain: body.canonicalDomain,
511
661
  ownedDomains: JSON.stringify(body.ownedDomains ?? []),
662
+ aliases: JSON.stringify(nextAliases),
512
663
  country: body.country,
513
664
  language: body.language,
514
665
  tags: JSON.stringify(body.tags ?? []),
@@ -530,6 +681,7 @@ async function projectRoutes(app, opts) {
530
681
  });
531
682
  });
532
683
  opts.onProjectUpserted?.(existing.id, name);
684
+ if (aliasesChanged) opts.onAliasesChanged?.(existing.id, name);
533
685
  const updated = app.db.select().from(projects).where(eq3(projects.id, existing.id)).get();
534
686
  return reply.status(200).send(formatProject(updated));
535
687
  }
@@ -541,6 +693,7 @@ async function projectRoutes(app, opts) {
541
693
  displayName: body.displayName,
542
694
  canonicalDomain: body.canonicalDomain,
543
695
  ownedDomains: JSON.stringify(body.ownedDomains ?? []),
696
+ aliases: JSON.stringify(nextAliases),
544
697
  country: body.country,
545
698
  language: body.language,
546
699
  tags: JSON.stringify(body.tags ?? []),
@@ -574,6 +727,30 @@ async function projectRoutes(app, opts) {
574
727
  const project = resolveProject(app.db, request.params.name);
575
728
  return reply.send(formatProject(project));
576
729
  });
730
+ app.get("/projects/:name/delete-preview", async (request, reply) => {
731
+ const project = resolveProject(app.db, request.params.name);
732
+ const pid = project.id;
733
+ const count = (n) => n ?? 0;
734
+ const queryCount = app.db.select({ n: sql2`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
735
+ const competitorCount = app.db.select({ n: sql2`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
736
+ const runCount = app.db.select({ n: sql2`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
737
+ const snapshotCount = app.db.select({ n: sql2`count(*)` }).from(querySnapshots).innerJoin(runs, eq3(querySnapshots.runId, runs.id)).where(eq3(runs.projectId, pid)).get();
738
+ const insightCount = app.db.select({ n: sql2`count(*)` }).from(insights).where(eq3(insights.projectId, pid)).get();
739
+ const auditLogCount = app.db.select({ n: sql2`count(*)` }).from(auditLog).where(eq3(auditLog.projectId, pid)).get();
740
+ return reply.send({
741
+ project: { id: project.id, name: project.name },
742
+ cascadeRows: {
743
+ queries: count(queryCount?.n),
744
+ competitors: count(competitorCount?.n),
745
+ runs: count(runCount?.n),
746
+ snapshots: count(snapshotCount?.n),
747
+ insights: count(insightCount?.n)
748
+ },
749
+ detachedRows: {
750
+ auditLog: count(auditLogCount?.n)
751
+ }
752
+ });
753
+ });
577
754
  app.delete("/projects/:name", async (request, reply) => {
578
755
  const project = resolveProject(app.db, request.params.name);
579
756
  writeAuditLog(app.db, {
@@ -688,6 +865,7 @@ async function projectRoutes(app, opts) {
688
865
  displayName: project.displayName,
689
866
  canonicalDomain: project.canonicalDomain,
690
867
  ownedDomains: parseJsonColumn(project.ownedDomains, []),
868
+ aliases: parseJsonColumn(project.aliases, []),
691
869
  country: project.country,
692
870
  language: project.language,
693
871
  queries: qs.map((q) => q.query),
@@ -723,6 +901,7 @@ function formatProject(row) {
723
901
  displayName: row.displayName,
724
902
  canonicalDomain: row.canonicalDomain,
725
903
  ownedDomains: parseJsonColumn(row.ownedDomains, []),
904
+ aliases: parseJsonColumn(row.aliases, []),
726
905
  country: row.country,
727
906
  language: row.language,
728
907
  tags: parseJsonColumn(row.tags, []),
@@ -737,6 +916,13 @@ function formatProject(row) {
737
916
  updatedAt: row.updatedAt
738
917
  };
739
918
  }
919
+ function aliasArraysEqual(a, b) {
920
+ if (a.length !== b.length) return false;
921
+ for (let i = 0; i < a.length; i++) {
922
+ if (a[i].toLowerCase() !== b[i].toLowerCase()) return false;
923
+ }
924
+ return true;
925
+ }
740
926
 
741
927
  // ../api-routes/src/queries.ts
742
928
  import crypto5 from "crypto";
@@ -1139,7 +1325,7 @@ function parseCompetitorBatch(value) {
1139
1325
 
1140
1326
  // ../api-routes/src/runs.ts
1141
1327
  import crypto8 from "crypto";
1142
- import { eq as eq7, asc, desc, sql as sql2 } from "drizzle-orm";
1328
+ import { and as and2, eq as eq7, asc, desc, or as or2, sql as sql3 } from "drizzle-orm";
1143
1329
 
1144
1330
  // ../api-routes/src/run-queue.ts
1145
1331
  import crypto7 from "crypto";
@@ -1233,23 +1419,36 @@ async function runRoutes(app, opts) {
1233
1419
  if (projectLocations.length === 0) {
1234
1420
  throw validationError("No locations configured for this project");
1235
1421
  }
1236
- const newRuns = [];
1237
- for (const loc of projectLocations) {
1238
- const runId2 = crypto8.randomUUID();
1239
- app.db.insert(runs).values({
1240
- id: runId2,
1241
- projectId: project.id,
1242
- kind,
1243
- status: "queued",
1244
- trigger,
1245
- location: loc.label,
1246
- queries: queriesColumn,
1247
- createdAt: now
1248
- }).run();
1249
- newRuns.push({ runId: runId2, loc });
1422
+ const result = app.db.transaction((tx) => {
1423
+ const activeRun = tx.select({ id: runs.id }).from(runs).where(and2(
1424
+ eq7(runs.projectId, project.id),
1425
+ or2(eq7(runs.status, "queued"), eq7(runs.status, "running"))
1426
+ )).get();
1427
+ if (activeRun) {
1428
+ return { conflict: true };
1429
+ }
1430
+ const inserted = [];
1431
+ for (const loc of projectLocations) {
1432
+ const runId2 = crypto8.randomUUID();
1433
+ tx.insert(runs).values({
1434
+ id: runId2,
1435
+ projectId: project.id,
1436
+ kind,
1437
+ status: "queued",
1438
+ trigger,
1439
+ location: loc.label,
1440
+ queries: queriesColumn,
1441
+ createdAt: now
1442
+ }).run();
1443
+ inserted.push({ runId: runId2, loc });
1444
+ }
1445
+ return { conflict: false, inserted };
1446
+ });
1447
+ if (result.conflict) {
1448
+ throw runInProgress(project.name);
1250
1449
  }
1251
1450
  const results = [];
1252
- for (const { runId: runId2, loc } of newRuns) {
1451
+ for (const { runId: runId2, loc } of result.inserted) {
1253
1452
  writeAuditLog(app.db, {
1254
1453
  projectId: project.id,
1255
1454
  actor: "api",
@@ -1298,7 +1497,7 @@ async function runRoutes(app, opts) {
1298
1497
  });
1299
1498
  app.get("/projects/:name/runs/latest", async (request, reply) => {
1300
1499
  const project = resolveProject(app.db, request.params.name);
1301
- const countRow = app.db.select({ count: sql2`count(*)` }).from(runs).where(eq7(runs.projectId, project.id)).get();
1500
+ const countRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq7(runs.projectId, project.id)).get();
1302
1501
  const totalRuns = countRow?.count ?? 0;
1303
1502
  const latestRun = app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
1304
1503
  if (!latestRun) {
@@ -1437,7 +1636,8 @@ function loadRunDetail(app, run) {
1437
1636
  const project = app.db.select({
1438
1637
  displayName: projects.displayName,
1439
1638
  canonicalDomain: projects.canonicalDomain,
1440
- ownedDomains: projects.ownedDomains
1639
+ ownedDomains: projects.ownedDomains,
1640
+ aliases: projects.aliases
1441
1641
  }).from(projects).where(eq7(projects.id, run.projectId)).get();
1442
1642
  const snapshots = app.db.select({
1443
1643
  id: querySnapshots.id,
@@ -1469,7 +1669,10 @@ function loadRunDetail(app, run) {
1469
1669
  provider: s.provider,
1470
1670
  citationState: s.citationState,
1471
1671
  answerMentioned,
1672
+ // Legacy alias of `mentionState`, retained for backwards compatibility.
1472
1673
  visibilityState: project ? resolveSnapshotVisibilityState(s, project) : answerMentioned ? "visible" : "not-visible",
1674
+ // Canonical vocabulary for answer-text presence; new consumers prefer this.
1675
+ mentionState: project ? resolveSnapshotMentionState(s, project) : answerMentioned ? "mentioned" : "not-mentioned",
1473
1676
  answerText: s.answerText,
1474
1677
  citedDomains: parseJsonColumn(s.citedDomains, []),
1475
1678
  competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
@@ -1487,7 +1690,7 @@ function loadRunDetail(app, run) {
1487
1690
 
1488
1691
  // ../api-routes/src/apply.ts
1489
1692
  import crypto10 from "crypto";
1490
- import { and as and2, eq as eq8 } from "drizzle-orm";
1693
+ import { and as and3, eq as eq8 } from "drizzle-orm";
1491
1694
 
1492
1695
  // ../api-routes/src/schedule-utils.ts
1493
1696
  var DAY_MAP = {
@@ -1584,7 +1787,7 @@ import http from "http";
1584
1787
  import https from "https";
1585
1788
  import net from "net";
1586
1789
  var REQUEST_TIMEOUT_MS = 1e4;
1587
- async function resolveWebhookTarget(raw) {
1790
+ async function resolveWebhookTarget(raw, options = {}) {
1588
1791
  let parsed;
1589
1792
  try {
1590
1793
  parsed = new URL(raw);
@@ -1605,7 +1808,7 @@ async function resolveWebhookTarget(raw) {
1605
1808
  if (addresses.length === 0) {
1606
1809
  return { ok: false, message: '"url" hostname could not be resolved' };
1607
1810
  }
1608
- const blocked = addresses.find((entry) => isBlockedAddress(entry.address));
1811
+ const blocked = addresses.find((entry) => isBlockedAddress(entry.address, options));
1609
1812
  if (blocked) {
1610
1813
  return { ok: false, message: '"url" must not resolve to a private or loopback address' };
1611
1814
  }
@@ -1622,7 +1825,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
1622
1825
  const body = JSON.stringify(payload);
1623
1826
  const isHttps = target.url.protocol === "https:";
1624
1827
  const port = target.url.port ? Number(target.url.port) : isHttps ? 443 : 80;
1625
- const path15 = `${target.url.pathname}${target.url.search}`;
1828
+ const path16 = `${target.url.pathname}${target.url.search}`;
1626
1829
  const headers = {
1627
1830
  "Content-Length": String(Buffer.byteLength(body)),
1628
1831
  "Content-Type": "application/json",
@@ -1638,7 +1841,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
1638
1841
  headers,
1639
1842
  hostname: target.address,
1640
1843
  method: "POST",
1641
- path: path15,
1844
+ path: path16,
1642
1845
  port,
1643
1846
  timeout: REQUEST_TIMEOUT_MS
1644
1847
  };
@@ -1683,34 +1886,40 @@ async function resolveHostAddresses(hostname) {
1683
1886
  return [];
1684
1887
  }
1685
1888
  }
1686
- function isBlockedAddress(address) {
1889
+ function isBlockedAddress(address, options) {
1687
1890
  const normalized = stripIpv6Brackets(address).toLowerCase();
1688
1891
  const family = net.isIP(normalized);
1689
1892
  if (family === 4) {
1690
- return isBlockedIpv4(normalized);
1893
+ return isBlockedIpv4(normalized, options);
1691
1894
  }
1692
1895
  if (family === 6) {
1693
1896
  const mappedIpv4 = extractMappedIpv4(normalized);
1694
1897
  if (mappedIpv4) {
1695
- return isBlockedIpv4(mappedIpv4);
1898
+ return isBlockedIpv4(mappedIpv4, options);
1696
1899
  }
1697
- return isBlockedIpv6(normalized);
1900
+ return isBlockedIpv6(normalized, options);
1698
1901
  }
1699
1902
  return true;
1700
1903
  }
1701
- function isBlockedIpv4(address) {
1904
+ function isBlockedIpv4(address, options) {
1702
1905
  const octets = address.split(".").map((part) => Number.parseInt(part, 10));
1703
1906
  if (octets.length !== 4 || octets.some(Number.isNaN)) {
1704
1907
  return true;
1705
1908
  }
1706
1909
  const [first, second] = octets;
1910
+ if (first === 127 && !options.allowLoopback) {
1911
+ return true;
1912
+ }
1707
1913
  return first === 0 || first === 10 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 198 && (second === 18 || second === 19);
1708
1914
  }
1709
- function isBlockedIpv6(address) {
1915
+ function isBlockedIpv6(address, options) {
1710
1916
  const normalized = address.split("%")[0].toLowerCase();
1711
1917
  if (normalized === "::") {
1712
1918
  return true;
1713
1919
  }
1920
+ if (normalized === "::1") {
1921
+ return !options.allowLoopback;
1922
+ }
1714
1923
  const firstHextetText = normalized.split(":")[0] ?? "";
1715
1924
  const firstHextet = firstHextetText === "" ? 0 : Number.parseInt(firstHextetText, 16);
1716
1925
  if (Number.isNaN(firstHextet)) {
@@ -1741,6 +1950,7 @@ function stripIpv6Brackets(value) {
1741
1950
 
1742
1951
  // ../api-routes/src/apply.ts
1743
1952
  async function applyRoutes(app, opts) {
1953
+ const allowLoopback = opts?.allowLoopbackWebhooks === true;
1744
1954
  app.post("/apply", async (request, reply) => {
1745
1955
  const parsed = projectConfigSchema.safeParse(request.body);
1746
1956
  if (!parsed.success) {
@@ -1795,7 +2005,7 @@ async function applyRoutes(app, opts) {
1795
2005
  const hasNotifications = "notifications" in rawSpec;
1796
2006
  if (hasNotifications) {
1797
2007
  for (const notif of config.spec.notifications) {
1798
- const urlCheck = await resolveWebhookTarget(notif.url ?? "");
2008
+ const urlCheck = await resolveWebhookTarget(notif.url ?? "", { allowLoopback });
1799
2009
  if (!urlCheck.ok) throw validationError(`Notification URL invalid: ${urlCheck.message}`);
1800
2010
  }
1801
2011
  }
@@ -1804,14 +2014,21 @@ async function applyRoutes(app, opts) {
1804
2014
  const configQueries = resolveConfigSpecQueries(config.spec);
1805
2015
  let projectId;
1806
2016
  let scheduleAction = null;
2017
+ let aliasesChanged = false;
1807
2018
  app.db.transaction((tx) => {
1808
2019
  const existing = tx.select().from(projects).where(eq8(projects.name, name)).get();
2020
+ const nextAliases = normalizeProjectAliases(config.spec.displayName, config.spec.aliases ?? []);
2021
+ if (existing) {
2022
+ const prevAliases = parseJsonColumn(existing.aliases, []);
2023
+ aliasesChanged = !aliasArraysEqual2(prevAliases, nextAliases);
2024
+ }
1809
2025
  if (existing) {
1810
2026
  projectId = existing.id;
1811
2027
  tx.update(projects).set({
1812
2028
  displayName: config.spec.displayName,
1813
2029
  canonicalDomain: config.spec.canonicalDomain,
1814
2030
  ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
2031
+ aliases: JSON.stringify(nextAliases),
1815
2032
  country: config.spec.country,
1816
2033
  language: config.spec.language,
1817
2034
  labels: JSON.stringify(config.metadata.labels),
@@ -1838,6 +2055,7 @@ async function applyRoutes(app, opts) {
1838
2055
  displayName: config.spec.displayName,
1839
2056
  canonicalDomain: config.spec.canonicalDomain,
1840
2057
  ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
2058
+ aliases: JSON.stringify(nextAliases),
1841
2059
  country: config.spec.country,
1842
2060
  language: config.spec.language,
1843
2061
  tags: "[]",
@@ -1896,7 +2114,7 @@ async function applyRoutes(app, opts) {
1896
2114
  });
1897
2115
  const AV_KIND = SchedulableRunKinds["answer-visibility"];
1898
2116
  if (resolvedSchedule) {
1899
- const existingSched = tx.select().from(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
2117
+ const existingSched = tx.select().from(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
1900
2118
  if (existingSched) {
1901
2119
  tx.update(schedules).set({
1902
2120
  cronExpr: resolvedSchedule.cronExpr,
@@ -1922,9 +2140,9 @@ async function applyRoutes(app, opts) {
1922
2140
  }
1923
2141
  scheduleAction = "upsert";
1924
2142
  } else if (deleteSchedule) {
1925
- const existingSched = tx.select().from(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
2143
+ const existingSched = tx.select().from(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
1926
2144
  if (existingSched) {
1927
- tx.delete(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).run();
2145
+ tx.delete(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).run();
1928
2146
  scheduleAction = "delete";
1929
2147
  }
1930
2148
  }
@@ -1957,6 +2175,9 @@ async function applyRoutes(app, opts) {
1957
2175
  if (!hasNotifications) {
1958
2176
  opts?.onProjectUpserted?.(projectId, config.metadata.name);
1959
2177
  }
2178
+ if (aliasesChanged) {
2179
+ opts?.onAliasesChanged?.(projectId, config.metadata.name);
2180
+ }
1960
2181
  if ("google" in rawSpec && config.spec.google?.gsc?.propertyUrl) {
1961
2182
  opts?.onGoogleConnectionPropertyUpdated?.(config.spec.canonicalDomain, "gsc", config.spec.google.gsc.propertyUrl);
1962
2183
  }
@@ -1967,6 +2188,7 @@ async function applyRoutes(app, opts) {
1967
2188
  displayName: project.displayName,
1968
2189
  canonicalDomain: project.canonicalDomain,
1969
2190
  ownedDomains: parseJsonColumn(project.ownedDomains, []),
2191
+ aliases: parseJsonColumn(project.aliases, []),
1970
2192
  country: project.country,
1971
2193
  language: project.language,
1972
2194
  tags: parseJsonColumn(project.tags, []),
@@ -1982,6 +2204,13 @@ async function applyRoutes(app, opts) {
1982
2204
  });
1983
2205
  });
1984
2206
  }
2207
+ function aliasArraysEqual2(a, b) {
2208
+ if (a.length !== b.length) return false;
2209
+ for (let i = 0; i < a.length; i++) {
2210
+ if (a[i].toLowerCase() !== b[i].toLowerCase()) return false;
2211
+ }
2212
+ return true;
2213
+ }
1985
2214
  function normalizeCompetitorList2(domains) {
1986
2215
  const seen = /* @__PURE__ */ new Set();
1987
2216
  const result = [];
@@ -2089,6 +2318,7 @@ async function historyRoutes(app) {
2089
2318
  citationState: s.citationState,
2090
2319
  answerMentioned: resolveSnapshotAnswerMentioned(s, project),
2091
2320
  visibilityState: resolveSnapshotVisibilityState(s, project),
2321
+ mentionState: resolveSnapshotMentionState(s, project),
2092
2322
  answerText: s.answerText,
2093
2323
  citedDomains: parseJsonColumn(s.citedDomains, []),
2094
2324
  competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
@@ -2142,9 +2372,11 @@ async function historyRoutes(app) {
2142
2372
  const run = projectRuns.find((r) => r.id === snap.runId);
2143
2373
  let transition = snap.citationState === CitationStates.cited ? "cited" : "not-cited";
2144
2374
  let visibilityTransition = snap.answerMentioned ? "visible" : "not-visible";
2375
+ let mentionTransition = snap.answerMentioned ? "mentioned" : "not-mentioned";
2145
2376
  if (idx === 0) {
2146
2377
  transition = "new";
2147
2378
  visibilityTransition = "new";
2379
+ mentionTransition = "new";
2148
2380
  } else {
2149
2381
  const prev = snaps[idx - 1];
2150
2382
  if (prev.citationState === CitationStates["not-cited"] && snap.citationState === CitationStates.cited) {
@@ -2154,8 +2386,10 @@ async function historyRoutes(app) {
2154
2386
  }
2155
2387
  if (!prev.answerMentioned && snap.answerMentioned) {
2156
2388
  visibilityTransition = "emerging";
2389
+ mentionTransition = "emerging";
2157
2390
  } else if (prev.answerMentioned && !snap.answerMentioned) {
2158
2391
  visibilityTransition = "lost";
2392
+ mentionTransition = "lost";
2159
2393
  }
2160
2394
  }
2161
2395
  return {
@@ -2164,8 +2398,12 @@ async function historyRoutes(app) {
2164
2398
  citationState: snap.citationState,
2165
2399
  transition,
2166
2400
  answerMentioned: snap.answerMentioned,
2401
+ // Legacy aliases of `mentionState` / `mentionTransition`.
2167
2402
  visibilityState: snap.answerMentioned ? "visible" : "not-visible",
2168
2403
  visibilityTransition,
2404
+ // Canonical-vocabulary fields — new consumers prefer these.
2405
+ mentionState: snap.answerMentioned ? "mentioned" : "not-mentioned",
2406
+ mentionTransition,
2169
2407
  location: snap.location
2170
2408
  };
2171
2409
  });
@@ -2221,7 +2459,8 @@ async function historyRoutes(app) {
2221
2459
  const resolved = {
2222
2460
  ...s,
2223
2461
  resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
2224
- resolvedVisibilityState: resolveSnapshotVisibilityState(s, project)
2462
+ resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
2463
+ resolvedMentionState: resolveSnapshotMentionState(s, project)
2225
2464
  };
2226
2465
  const existing = map1.get(s.queryId);
2227
2466
  if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
@@ -2233,7 +2472,8 @@ async function historyRoutes(app) {
2233
2472
  const resolved = {
2234
2473
  ...s,
2235
2474
  resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
2236
- resolvedVisibilityState: resolveSnapshotVisibilityState(s, project)
2475
+ resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
2476
+ resolvedMentionState: resolveSnapshotMentionState(s, project)
2237
2477
  };
2238
2478
  const existing = map2.get(s.queryId);
2239
2479
  if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
@@ -2251,8 +2491,11 @@ async function historyRoutes(app) {
2251
2491
  run2State: s2?.citationState ?? null,
2252
2492
  run1AnswerMentioned: s1?.resolvedAnswerMentioned ?? null,
2253
2493
  run2AnswerMentioned: s2?.resolvedAnswerMentioned ?? null,
2494
+ // Legacy aliases — same data as run{1,2}MentionState below.
2254
2495
  run1VisibilityState: s1?.resolvedVisibilityState ?? null,
2255
2496
  run2VisibilityState: s2?.resolvedVisibilityState ?? null,
2497
+ run1MentionState: s1?.resolvedMentionState ?? null,
2498
+ run2MentionState: s2?.resolvedMentionState ?? null,
2256
2499
  changed: (s1?.citationState ?? null) !== (s2?.citationState ?? null),
2257
2500
  visibilityChanged: (s1?.resolvedAnswerMentioned ?? null) !== (s2?.resolvedAnswerMentioned ?? null)
2258
2501
  };
@@ -2621,7 +2864,7 @@ function buildCategoryCounts(counts) {
2621
2864
  }
2622
2865
 
2623
2866
  // ../api-routes/src/intelligence.ts
2624
- import { eq as eq11, desc as desc4, and as and3, inArray as inArray3 } from "drizzle-orm";
2867
+ import { eq as eq11, desc as desc4, and as and4, inArray as inArray3 } from "drizzle-orm";
2625
2868
  function emptyHealthSnapshot(projectId) {
2626
2869
  return {
2627
2870
  id: `no-data:${projectId}`,
@@ -2710,7 +2953,7 @@ async function intelligenceRoutes(app) {
2710
2953
  if (request.query.runId) {
2711
2954
  conditions.push(eq11(insights.runId, request.query.runId));
2712
2955
  }
2713
- const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and3(...conditions)).orderBy(desc4(insights.createdAt)).all();
2956
+ const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and4(...conditions)).orderBy(desc4(insights.createdAt)).all();
2714
2957
  const showDismissed = request.query.dismissed === "true";
2715
2958
  const result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
2716
2959
  return reply.send(result);
@@ -2734,7 +2977,7 @@ async function intelligenceRoutes(app) {
2734
2977
  });
2735
2978
  app.get("/projects/:name/health/latest", async (request, reply) => {
2736
2979
  const project = resolveProject(app.db, request.params.name);
2737
- const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and3(
2980
+ const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and4(
2738
2981
  eq11(runs.projectId, project.id),
2739
2982
  eq11(runs.kind, RunKinds["answer-visibility"]),
2740
2983
  inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
@@ -2742,7 +2985,7 @@ async function intelligenceRoutes(app) {
2742
2985
  const latestGroup = groupRunsByCreatedAt(projectVisRuns)[0] ?? [];
2743
2986
  const latestGroupRunIds = latestGroup.map((r) => r.id);
2744
2987
  if (latestGroupRunIds.length > 0) {
2745
- const groupRows = app.db.select().from(healthSnapshots).where(and3(
2988
+ const groupRows = app.db.select().from(healthSnapshots).where(and4(
2746
2989
  eq11(healthSnapshots.projectId, project.id),
2747
2990
  inArray3(healthSnapshots.runId, latestGroupRunIds)
2748
2991
  )).all();
@@ -2766,7 +3009,7 @@ async function intelligenceRoutes(app) {
2766
3009
  }
2767
3010
 
2768
3011
  // ../api-routes/src/report.ts
2769
- import { and as and5, desc as desc6, eq as eq13, gte, inArray as inArray5, lt, lte, ne, or as or2, sql as sql3 } from "drizzle-orm";
3012
+ import { and as and6, desc as desc6, eq as eq13, gte, inArray as inArray5, lt, lte, ne, or as or3, sql as sql4 } from "drizzle-orm";
2770
3013
 
2771
3014
  // ../api-routes/src/report-renderer.ts
2772
3015
  var COLORS = {
@@ -2815,9 +3058,9 @@ function inferAdSource(params) {
2815
3058
  function formatLandingPageHtml(raw) {
2816
3059
  const value = raw ?? "";
2817
3060
  const queryIdx = value.indexOf("?");
2818
- const path15 = queryIdx === -1 ? value : value.slice(0, queryIdx);
3061
+ const path16 = queryIdx === -1 ? value : value.slice(0, queryIdx);
2819
3062
  const query = queryIdx === -1 ? "" : value.slice(queryIdx + 1);
2820
- const pathHtml = `<span class="page-path">${escapeHtml(path15 || "/")}</span>`;
3063
+ const pathHtml = `<span class="page-path">${escapeHtml(path16 || "/")}</span>`;
2821
3064
  if (!query) return pathHtml;
2822
3065
  let summary = "";
2823
3066
  try {
@@ -4171,7 +4414,7 @@ function renderLineChart(points, color, title, height = 200) {
4171
4414
  y: padY + usableH - p.y / max * usableH,
4172
4415
  raw: p
4173
4416
  }));
4174
- const path15 = xy.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
4417
+ const path16 = xy.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
4175
4418
  const dots = xy.map((p) => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="${color}" />`).join("");
4176
4419
  const xLabels = xy.map((p, i) => {
4177
4420
  if (points.length > 8 && i % Math.ceil(points.length / 6) !== 0 && i !== points.length - 1) return "";
@@ -4183,7 +4426,7 @@ function renderLineChart(points, color, title, height = 200) {
4183
4426
  <line x1="${padX}" y1="${padY + usableH}" x2="${padX + usableW}" y2="${padY + usableH}" stroke="${COLORS.border}" stroke-width="1" />
4184
4427
  <text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatNumber(max)}</text>
4185
4428
  <text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">0</text>
4186
- <path d="${path15}" stroke="${color}" stroke-width="2" fill="none" />
4429
+ <path d="${path16}" stroke="${color}" stroke-width="2" fill="none" />
4187
4430
  ${dots}
4188
4431
  ${xLabels}
4189
4432
  </svg>
@@ -5002,7 +5245,7 @@ function renderReportHtml(report, opts = {}) {
5002
5245
  }
5003
5246
 
5004
5247
  // ../api-routes/src/content-data.ts
5005
- import { and as and4, eq as eq12, desc as desc5, inArray as inArray4 } from "drizzle-orm";
5248
+ import { and as and5, eq as eq12, desc as desc5, inArray as inArray4 } from "drizzle-orm";
5006
5249
  var RECENT_RUNS_WINDOW = 5;
5007
5250
  function loadOrchestratorInput(db, project, locationFilter = void 0) {
5008
5251
  const projectId = project.id;
@@ -5126,7 +5369,7 @@ function listCompetitorDomains(db, projectId) {
5126
5369
  }
5127
5370
  function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter) {
5128
5371
  const rows = db.select({ id: runs.id, location: runs.location }).from(runs).where(
5129
- and4(
5372
+ and5(
5130
5373
  eq12(runs.projectId, projectId),
5131
5374
  eq12(runs.kind, RunKinds["answer-visibility"]),
5132
5375
  // Queued/running/failed/cancelled runs may have partial or no
@@ -5157,9 +5400,9 @@ function buildGaTrafficByPage(db, projectId) {
5157
5400
  }).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
5158
5401
  const map = /* @__PURE__ */ new Map();
5159
5402
  for (const row of rows) {
5160
- const path15 = extractPath(row.landingPage);
5161
- if (!path15) continue;
5162
- map.set(path15, (map.get(path15) ?? 0) + (row.sessions ?? 0));
5403
+ const path16 = extractPath(row.landingPage);
5404
+ if (!path16) continue;
5405
+ map.set(path16, (map.get(path16) ?? 0) + (row.sessions ?? 0));
5163
5406
  }
5164
5407
  return map;
5165
5408
  }
@@ -5354,8 +5597,8 @@ function normalizeDomain(domain) {
5354
5597
  function extractPath(url) {
5355
5598
  if (!url) return "";
5356
5599
  const match = /^https?:\/\/[^/]+(.*)$/.exec(url.trim());
5357
- const path15 = match ? match[1] : url.trim();
5358
- const stripped = path15.replace(/\/+$/, "");
5600
+ const path16 = match ? match[1] : url.trim();
5601
+ const stripped = path16.replace(/\/+$/, "");
5359
5602
  return stripped || "/";
5360
5603
  }
5361
5604
 
@@ -5384,8 +5627,8 @@ function safeNum(value) {
5384
5627
  }
5385
5628
  return 0;
5386
5629
  }
5387
- function categorizeQuery(query, projectDisplayName, canonicalDomain) {
5388
- return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectDisplayName));
5630
+ function categorizeQuery(query, projectBrandNames, canonicalDomain) {
5631
+ return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectBrandNames));
5389
5632
  }
5390
5633
  function loadSnapshotsForRun(db, runId) {
5391
5634
  return loadSnapshotsForRunIds(db, [runId]);
@@ -5414,7 +5657,7 @@ function loadQueryLookup(db, projectId) {
5414
5657
  for (const row of rows) byId.set(row.id, row.query);
5415
5658
  return { byId };
5416
5659
  }
5417
- function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, trackedQueries) {
5660
+ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries) {
5418
5661
  const allRows = db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
5419
5662
  if (allRows.length === 0) return null;
5420
5663
  let maxDate = "";
@@ -5449,11 +5692,11 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
5449
5692
  impressions: agg.impressions,
5450
5693
  ctr: agg.impressions > 0 ? agg.clicks / agg.impressions : 0,
5451
5694
  avgPosition: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0,
5452
- category: categorizeQuery(query, projectDisplayName, canonicalDomain)
5695
+ category: categorizeQuery(query, projectBrandNames, canonicalDomain)
5453
5696
  })).sort((a, b) => b.clicks - a.clicks).slice(0, TOP_QUERIES_LIMIT);
5454
5697
  const categoryAgg = /* @__PURE__ */ new Map();
5455
5698
  for (const [query, agg] of queryAgg) {
5456
- const cat = categorizeQuery(query, projectDisplayName, canonicalDomain);
5699
+ const cat = categorizeQuery(query, projectBrandNames, canonicalDomain);
5457
5700
  const bucket = categoryAgg.get(cat) ?? { clicks: 0, impressions: 0 };
5458
5701
  bucket.clicks += agg.clicks;
5459
5702
  bucket.impressions += agg.impressions;
@@ -5471,7 +5714,7 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
5471
5714
  const trackedSet = new Set(trackedQueries.map((q) => q.toLowerCase()));
5472
5715
  const gscQuerySet = new Set([...queryAgg.keys()].map((q) => q.toLowerCase()));
5473
5716
  const trackedButNoGsc = trackedQueries.filter((q) => !gscQuerySet.has(q.toLowerCase())).sort();
5474
- const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q, projectDisplayName, canonicalDomain) !== "brand").sort((a, b) => b[1].impressions - a[1].impressions).map(([q]) => q).slice(0, TOP_QUERIES_LIMIT);
5717
+ const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q, projectBrandNames, canonicalDomain) !== "brand").sort((a, b) => b[1].impressions - a[1].impressions).map(([q]) => q).slice(0, TOP_QUERIES_LIMIT);
5475
5718
  return {
5476
5719
  periodStart,
5477
5720
  periodEnd,
@@ -5488,7 +5731,7 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
5488
5731
  }
5489
5732
  function buildGaSection(db, projectId) {
5490
5733
  const windowSummary = db.select().from(gaTrafficWindowSummaries).where(
5491
- and5(
5734
+ and6(
5492
5735
  eq13(gaTrafficWindowSummaries.projectId, projectId),
5493
5736
  eq13(gaTrafficWindowSummaries.windowKey, "30d")
5494
5737
  )
@@ -5642,7 +5885,7 @@ function buildAiReferrals(db, projectId) {
5642
5885
  return { totalSessions: total, totalUsers, bySource, trend, topLandingPages };
5643
5886
  }
5644
5887
  function nonSubresourceReferralPathCondition() {
5645
- return sql3`
5888
+ return sql4`
5646
5889
  LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/_next/static/%'
5647
5890
  AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/assets/%'
5648
5891
  AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/static/%'
@@ -5666,7 +5909,7 @@ function nonSubresourceReferralPathCondition() {
5666
5909
  }
5667
5910
  function buildServerActivity(db, projectId) {
5668
5911
  const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
5669
- and5(
5912
+ and6(
5670
5913
  eq13(trafficSources.projectId, projectId),
5671
5914
  ne(trafficSources.status, TrafficSourceStatuses.archived)
5672
5915
  )
@@ -5681,8 +5924,8 @@ function buildServerActivity(db, projectId) {
5681
5924
  const priorStart = new Date(priorStartMs).toISOString();
5682
5925
  const trendStart = new Date(trendStartMs).toISOString();
5683
5926
  const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5684
- db.select({ total: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5685
- and5(
5927
+ db.select({ total: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5928
+ and6(
5686
5929
  eq13(crawlerEventsHourly.projectId, projectId),
5687
5930
  eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5688
5931
  gte(crawlerEventsHourly.tsHour, windowStartIso),
@@ -5691,8 +5934,8 @@ function buildServerActivity(db, projectId) {
5691
5934
  ).get()?.total ?? 0
5692
5935
  );
5693
5936
  const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5694
- db.select({ total: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5695
- and5(
5937
+ db.select({ total: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5938
+ and6(
5696
5939
  eq13(crawlerEventsHourly.projectId, projectId),
5697
5940
  ne(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5698
5941
  gte(crawlerEventsHourly.tsHour, windowStartIso),
@@ -5701,8 +5944,8 @@ function buildServerActivity(db, projectId) {
5701
5944
  ).get()?.total ?? 0
5702
5945
  );
5703
5946
  const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5704
- db.select({ total: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
5705
- and5(
5947
+ db.select({ total: sql4`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
5948
+ and6(
5706
5949
  eq13(aiReferralEventsHourly.projectId, projectId),
5707
5950
  nonSubresourceReferralPathCondition(),
5708
5951
  gte(aiReferralEventsHourly.tsHour, windowStartIso),
@@ -5719,9 +5962,9 @@ function buildServerActivity(db, projectId) {
5719
5962
  const crawlerByOperatorRows = db.select({
5720
5963
  operator: crawlerEventsHourly.operator,
5721
5964
  verificationStatus: crawlerEventsHourly.verificationStatus,
5722
- hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5965
+ hits: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5723
5966
  }).from(crawlerEventsHourly).where(
5724
- and5(
5967
+ and6(
5725
5968
  eq13(crawlerEventsHourly.projectId, projectId),
5726
5969
  gte(crawlerEventsHourly.tsHour, headlineStart),
5727
5970
  lte(crawlerEventsHourly.tsHour, headlineEnd)
@@ -5729,9 +5972,9 @@ function buildServerActivity(db, projectId) {
5729
5972
  ).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
5730
5973
  const crawlerByOperatorPriorRows = db.select({
5731
5974
  operator: crawlerEventsHourly.operator,
5732
- hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5975
+ hits: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5733
5976
  }).from(crawlerEventsHourly).where(
5734
- and5(
5977
+ and6(
5735
5978
  eq13(crawlerEventsHourly.projectId, projectId),
5736
5979
  eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5737
5980
  gte(crawlerEventsHourly.tsHour, priorStart),
@@ -5740,9 +5983,9 @@ function buildServerActivity(db, projectId) {
5740
5983
  ).groupBy(crawlerEventsHourly.operator).all();
5741
5984
  const referralByOperatorRows = db.select({
5742
5985
  operator: aiReferralEventsHourly.operator,
5743
- hits: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
5986
+ hits: sql4`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
5744
5987
  }).from(aiReferralEventsHourly).where(
5745
- and5(
5988
+ and6(
5746
5989
  eq13(aiReferralEventsHourly.projectId, projectId),
5747
5990
  nonSubresourceReferralPathCondition(),
5748
5991
  gte(aiReferralEventsHourly.tsHour, headlineStart),
@@ -5780,16 +6023,16 @@ function buildServerActivity(db, projectId) {
5780
6023
  );
5781
6024
  const topPathsRows = db.select({
5782
6025
  path: crawlerEventsHourly.pathNormalized,
5783
- hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
5784
- operators: sql3`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
6026
+ hits: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
6027
+ operators: sql4`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
5785
6028
  }).from(crawlerEventsHourly).where(
5786
- and5(
6029
+ and6(
5787
6030
  eq13(crawlerEventsHourly.projectId, projectId),
5788
6031
  eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5789
6032
  gte(crawlerEventsHourly.tsHour, headlineStart),
5790
6033
  lte(crawlerEventsHourly.tsHour, headlineEnd)
5791
6034
  )
5792
- ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc6(sql3`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
6035
+ ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc6(sql4`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
5793
6036
  const topCrawledPaths = topPathsRows.map((r) => ({
5794
6037
  path: r.path,
5795
6038
  verifiedHits: Number(r.hits),
@@ -5797,16 +6040,16 @@ function buildServerActivity(db, projectId) {
5797
6040
  }));
5798
6041
  const referralProductsRows = db.select({
5799
6042
  product: aiReferralEventsHourly.product,
5800
- arrivals: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
5801
- landingPaths: sql3`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
6043
+ arrivals: sql4`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
6044
+ landingPaths: sql4`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
5802
6045
  }).from(aiReferralEventsHourly).where(
5803
- and5(
6046
+ and6(
5804
6047
  eq13(aiReferralEventsHourly.projectId, projectId),
5805
6048
  nonSubresourceReferralPathCondition(),
5806
6049
  gte(aiReferralEventsHourly.tsHour, headlineStart),
5807
6050
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
5808
6051
  )
5809
- ).groupBy(aiReferralEventsHourly.product).orderBy(desc6(sql3`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
6052
+ ).groupBy(aiReferralEventsHourly.product).orderBy(desc6(sql4`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
5810
6053
  const referralProducts = referralProductsRows.map((r) => ({
5811
6054
  product: r.product,
5812
6055
  arrivals: Number(r.arrivals),
@@ -5814,43 +6057,43 @@ function buildServerActivity(db, projectId) {
5814
6057
  }));
5815
6058
  const topReferralRows = db.select({
5816
6059
  path: aiReferralEventsHourly.landingPathNormalized,
5817
- arrivals: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
5818
- products: sql3`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
6060
+ arrivals: sql4`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
6061
+ products: sql4`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
5819
6062
  }).from(aiReferralEventsHourly).where(
5820
- and5(
6063
+ and6(
5821
6064
  eq13(aiReferralEventsHourly.projectId, projectId),
5822
6065
  nonSubresourceReferralPathCondition(),
5823
6066
  gte(aiReferralEventsHourly.tsHour, headlineStart),
5824
6067
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
5825
6068
  )
5826
- ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc6(sql3`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
6069
+ ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc6(sql4`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
5827
6070
  const topReferralLandingPaths = topReferralRows.map((r) => ({
5828
6071
  path: r.path,
5829
6072
  arrivals: Number(r.arrivals),
5830
6073
  distinctProducts: Number(r.products)
5831
6074
  }));
5832
6075
  const crawlerTrendRows = db.select({
5833
- date: sql3`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
5834
- hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
6076
+ date: sql4`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
6077
+ hits: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5835
6078
  }).from(crawlerEventsHourly).where(
5836
- and5(
6079
+ and6(
5837
6080
  eq13(crawlerEventsHourly.projectId, projectId),
5838
6081
  eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5839
6082
  gte(crawlerEventsHourly.tsHour, trendStart),
5840
6083
  lte(crawlerEventsHourly.tsHour, headlineEnd)
5841
6084
  )
5842
- ).groupBy(sql3`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
6085
+ ).groupBy(sql4`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
5843
6086
  const referralTrendRows = db.select({
5844
- date: sql3`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
5845
- hits: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
6087
+ date: sql4`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
6088
+ hits: sql4`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
5846
6089
  }).from(aiReferralEventsHourly).where(
5847
- and5(
6090
+ and6(
5848
6091
  eq13(aiReferralEventsHourly.projectId, projectId),
5849
6092
  nonSubresourceReferralPathCondition(),
5850
6093
  gte(aiReferralEventsHourly.tsHour, trendStart),
5851
6094
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
5852
6095
  )
5853
- ).groupBy(sql3`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
6096
+ ).groupBy(sql4`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
5854
6097
  const dailyTrendMap = /* @__PURE__ */ new Map();
5855
6098
  for (const r of crawlerTrendRows) {
5856
6099
  const e = dailyTrendMap.get(r.date) ?? { verifiedCrawlerHits: 0, referralArrivals: 0 };
@@ -5919,7 +6162,7 @@ function buildIndexingHealth(db, projectId) {
5919
6162
  return null;
5920
6163
  }
5921
6164
  function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
5922
- const visibilityRuns = db.select().from(runs).where(and5(eq13(runs.projectId, projectId), eq13(runs.kind, RunKinds["answer-visibility"]))).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
6165
+ const visibilityRuns = db.select().from(runs).where(and6(eq13(runs.projectId, projectId), eq13(runs.kind, RunKinds["answer-visibility"]))).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
5923
6166
  const totalQueries = queryLookup.byId.size;
5924
6167
  const points = [];
5925
6168
  for (const run of visibilityRuns) {
@@ -5965,14 +6208,14 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
5965
6208
  }
5966
6209
  function buildInsightList(db, projectId, locationFilter) {
5967
6210
  const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
5968
- and5(
6211
+ and6(
5969
6212
  eq13(runs.projectId, projectId),
5970
6213
  eq13(runs.kind, RunKinds["answer-visibility"]),
5971
- or2(eq13(runs.status, RunStatuses.completed), eq13(runs.status, RunStatuses.partial))
6214
+ or3(eq13(runs.status, RunStatuses.completed), eq13(runs.status, RunStatuses.partial))
5972
6215
  )
5973
6216
  ).orderBy(desc6(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
5974
6217
  if (recentRunIds.length === 0) return [];
5975
- const rows = db.select().from(insights).where(and5(eq13(insights.projectId, projectId), inArray5(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
6218
+ const rows = db.select().from(insights).where(and6(eq13(insights.projectId, projectId), inArray5(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
5976
6219
  const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
5977
6220
  const flat = rows.filter((r) => !r.dismissed).map((r) => {
5978
6221
  const recommendation = parseJsonColumn(r.recommendation, null);
@@ -6160,7 +6403,8 @@ function buildReportActionPlan(input) {
6160
6403
  }
6161
6404
  for (const [index, opportunity] of input.contentOpportunities.slice(0, 2).entries()) {
6162
6405
  const verb = contentActionVerb(opportunity.action);
6163
- const target = opportunity.ourBestPage?.url ?? `a new page for "${opportunity.query}"`;
6406
+ const bestPageUrl = opportunity.ourBestPage?.url;
6407
+ const hasUsablePage = !!bestPageUrl && bestPageUrl !== "/" && bestPageUrl.trim() !== "";
6164
6408
  const evidence = [
6165
6409
  `Opportunity score ${Math.round(opportunity.score)} with ${opportunity.actionConfidence} confidence`,
6166
6410
  `Demand source: ${opportunity.demandSource}`
@@ -6169,17 +6413,22 @@ function buildReportActionPlan(input) {
6169
6413
  evidence.push(`${opportunity.winningCompetitor.domain} is the current winning cited source`);
6170
6414
  }
6171
6415
  if (opportunity.ourBestPage) {
6172
- evidence.push(`Best matching owned page: ${opportunity.ourBestPage.url}`);
6416
+ if (bestPageUrl === "/") {
6417
+ evidence.push("No topical page yet \u2014 homepage is the closest slug match");
6418
+ } else {
6419
+ evidence.push(`Best matching owned page: ${bestPageUrl}`);
6420
+ }
6173
6421
  } else {
6174
6422
  evidence.push("No matching owned page was found");
6175
6423
  }
6424
+ const targetIsExistingPage = opportunity.action !== "create" && hasUsablePage;
6176
6425
  actions.push({
6177
6426
  audience: "both",
6178
6427
  priority: 20 + index,
6179
6428
  horizon: opportunity.actionConfidence === "high" ? "short-term" : "medium-term",
6180
6429
  category: "content",
6181
6430
  title: `${verb} content for "${opportunity.query}"`,
6182
- action: opportunity.ourBestPage ? `${verb} ${target} so it directly answers the tracked query and cites the strongest supporting evidence.` : `${verb} ${target} that directly answers the query and earns citations from AI answer engines.`,
6431
+ action: targetIsExistingPage ? `${verb} the existing page so it directly answers the tracked query and cites the strongest supporting evidence.` : `${verb} a new page for "${opportunity.query}" that directly answers the query and earns citations from AI answer engines.`,
6183
6432
  why: opportunity.drivers.length > 0 ? opportunity.drivers : ["Canonry ranked this as a content opportunity from search-demand and citation evidence."],
6184
6433
  evidence,
6185
6434
  successMetric: `A future check cites ${input.canonicalDomain} for "${opportunity.query}" and the matching GSC query/page improves.`,
@@ -6524,6 +6773,11 @@ function buildProjectReport(db, projectName) {
6524
6773
  const competitorDomains = competitorRows.map((c) => c.domain);
6525
6774
  const ownedDomains = parseJsonColumn(project.ownedDomains, []);
6526
6775
  const projectDomains = [project.canonicalDomain, ...ownedDomains];
6776
+ const projectAliases = parseJsonColumn(project.aliases, []);
6777
+ const projectBrandNames = effectiveBrandNames({
6778
+ displayName: project.displayName,
6779
+ aliases: projectAliases
6780
+ });
6527
6781
  const citationScorecard = buildCitationScorecard(latestSnapshots, queryLookup);
6528
6782
  const competitorLandscape = buildCompetitorLandscape(
6529
6783
  latestSnapshots,
@@ -6534,7 +6788,7 @@ function buildProjectReport(db, projectName) {
6534
6788
  const mentionLandscape = buildMentionLandscape(
6535
6789
  latestSnapshots,
6536
6790
  competitorDomains,
6537
- project.displayName,
6791
+ projectBrandNames,
6538
6792
  projectDomains,
6539
6793
  queryLookup
6540
6794
  );
@@ -6543,7 +6797,7 @@ function buildProjectReport(db, projectName) {
6543
6797
  const gscSection = buildGscSection(
6544
6798
  db,
6545
6799
  project.id,
6546
- project.displayName,
6800
+ projectBrandNames,
6547
6801
  project.canonicalDomain,
6548
6802
  trackedQueries
6549
6803
  );
@@ -6899,7 +7153,7 @@ function normalizeDomain2(domain) {
6899
7153
  }
6900
7154
 
6901
7155
  // ../api-routes/src/composites.ts
6902
- import { eq as eq15, and as and6, desc as desc7, sql as sql4, like, or as or3, inArray as inArray7 } from "drizzle-orm";
7156
+ import { eq as eq15, and as and7, desc as desc7, sql as sql5, like, or as or4, inArray as inArray7 } from "drizzle-orm";
6903
7157
  var TOP_INSIGHT_LIMIT = 5;
6904
7158
  var SEARCH_HIT_HARD_LIMIT = 50;
6905
7159
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -6952,6 +7206,7 @@ async function compositeRoutes(app) {
6952
7206
  const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
6953
7207
  const configuredApiProviders = parseJsonColumn(project.providers, []).filter((p) => !p.startsWith("cdp:"));
6954
7208
  const scores = {
7209
+ mention: buildMentionCoverage(latestSnapshots, { configuredApiProviders }),
6955
7210
  visibility: buildVisibilityScore(latestSnapshots, { configuredApiProviders }),
6956
7211
  gapQueries: buildGapQueryScore(latestSnapshots),
6957
7212
  indexCoverage: buildIndexCoverageScore(app, project.id),
@@ -7013,24 +7268,24 @@ async function compositeRoutes(app) {
7013
7268
  rawResponse: querySnapshots.rawResponse,
7014
7269
  createdAt: querySnapshots.createdAt
7015
7270
  }).from(querySnapshots).innerJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(
7016
- and6(
7271
+ and7(
7017
7272
  eq15(queries.projectId, project.id),
7018
- or3(
7019
- sql4`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
7020
- sql4`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
7021
- sql4`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
7273
+ or4(
7274
+ sql5`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
7275
+ sql5`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
7276
+ sql5`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
7022
7277
  like(queries.query, pattern)
7023
7278
  )
7024
7279
  )
7025
7280
  ).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all());
7026
7281
  const insightMatches = app.db.select().from(insights).where(
7027
- and6(
7282
+ and7(
7028
7283
  eq15(insights.projectId, project.id),
7029
- or3(
7284
+ or4(
7030
7285
  like(insights.title, pattern),
7031
7286
  like(insights.query, pattern),
7032
- sql4`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
7033
- sql4`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
7287
+ sql5`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
7288
+ sql5`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
7034
7289
  )
7035
7290
  )
7036
7291
  ).orderBy(desc7(insights.createdAt)).limit(limit + 1).all();
@@ -7102,6 +7357,7 @@ function loadSnapshotsByRunIds(app, runIds) {
7102
7357
  provider: querySnapshots.provider,
7103
7358
  model: querySnapshots.model,
7104
7359
  citationState: querySnapshots.citationState,
7360
+ answerMentioned: querySnapshots.answerMentioned,
7105
7361
  competitorOverlap: querySnapshots.competitorOverlap,
7106
7362
  citedDomains: querySnapshots.citedDomains
7107
7363
  }).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all());
@@ -7112,6 +7368,7 @@ function loadSnapshotsByRunIds(app, runIds) {
7112
7368
  provider: row.provider,
7113
7369
  model: row.model,
7114
7370
  citationState: row.citationState,
7371
+ answerMentioned: row.answerMentioned,
7115
7372
  competitorOverlap: parseJsonColumn(row.competitorOverlap, []),
7116
7373
  citedDomains: parseJsonColumn(row.citedDomains, [])
7117
7374
  });
@@ -7354,6 +7611,7 @@ function formatProject2(row) {
7354
7611
  displayName: row.displayName,
7355
7612
  canonicalDomain: row.canonicalDomain,
7356
7613
  ownedDomains: parseJsonColumn(row.ownedDomains, []),
7614
+ aliases: parseJsonColumn(row.aliases, []),
7357
7615
  country: row.country,
7358
7616
  language: row.language,
7359
7617
  tags: parseJsonColumn(row.tags, []),
@@ -7656,6 +7914,7 @@ var routeCatalog = [
7656
7914
  displayName: stringSchema,
7657
7915
  canonicalDomain: stringSchema,
7658
7916
  ownedDomains: stringArraySchema,
7917
+ aliases: stringArraySchema,
7659
7918
  country: stringSchema,
7660
7919
  language: stringSchema,
7661
7920
  tags: stringArraySchema,
@@ -7705,6 +7964,18 @@ var routeCatalog = [
7705
7964
  404: { description: "Project not found." }
7706
7965
  }
7707
7966
  },
7967
+ {
7968
+ method: "get",
7969
+ path: "/api/v1/projects/{name}/delete-preview",
7970
+ summary: "Preview the cascade impact of deleting a project",
7971
+ description: "Read-only impact summary backing `canonry project delete --dry-run`. Returns counts of rows that would cascade-delete (queries, competitors, runs, snapshots, insights) and rows that would be detached (audit_log \u2014 `project_id` set to NULL).",
7972
+ tags: ["projects"],
7973
+ parameters: [nameParameter],
7974
+ responses: {
7975
+ 200: { description: "Preview of cascade impact." },
7976
+ 404: { description: "Project not found." }
7977
+ }
7978
+ },
7708
7979
  {
7709
7980
  method: "post",
7710
7981
  path: "/api/v1/projects/{name}/locations",
@@ -10710,8 +10981,8 @@ async function openApiRoutes(app, opts = {}) {
10710
10981
  return reply.type("application/json").send(buildOpenApiDocument(opts));
10711
10982
  });
10712
10983
  }
10713
- function buildOperationId(method, path15) {
10714
- const parts = path15.split("/").filter(Boolean).map((part) => {
10984
+ function buildOperationId(method, path16) {
10985
+ const parts = path16.split("/").filter(Boolean).map((part) => {
10715
10986
  if (part.startsWith("{") && part.endsWith("}")) {
10716
10987
  return `by-${part.slice(1, -1)}`;
10717
10988
  }
@@ -10875,7 +11146,7 @@ async function telemetryRoutes(app, opts) {
10875
11146
 
10876
11147
  // ../api-routes/src/schedules.ts
10877
11148
  import crypto11 from "crypto";
10878
- import { and as and7, eq as eq16 } from "drizzle-orm";
11149
+ import { and as and8, eq as eq16 } from "drizzle-orm";
10879
11150
  function parseKindParam(raw) {
10880
11151
  if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
10881
11152
  const parsed = schedulableRunKindSchema.safeParse(raw);
@@ -10941,7 +11212,7 @@ async function scheduleRoutes(app, opts) {
10941
11212
  }
10942
11213
  const now = (/* @__PURE__ */ new Date()).toISOString();
10943
11214
  const enabledInt = enabled === false ? 0 : 1;
10944
- const existing = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
11215
+ const existing = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10945
11216
  if (existing) {
10946
11217
  app.db.update(schedules).set({
10947
11218
  cronExpr,
@@ -10975,13 +11246,13 @@ async function scheduleRoutes(app, opts) {
10975
11246
  diff: { kind, cronExpr, preset, timezone, providers, sourceId }
10976
11247
  });
10977
11248
  opts.onScheduleUpdated?.("upsert", project.id, kind);
10978
- const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
11249
+ const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10979
11250
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
10980
11251
  });
10981
11252
  app.get("/projects/:name/schedule", async (request, reply) => {
10982
11253
  const project = resolveProject(app.db, request.params.name);
10983
11254
  const kind = parseKindParam(request.query?.kind);
10984
- const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
11255
+ const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10985
11256
  if (!schedule) {
10986
11257
  throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
10987
11258
  }
@@ -10990,7 +11261,7 @@ async function scheduleRoutes(app, opts) {
10990
11261
  app.delete("/projects/:name/schedule", async (request, reply) => {
10991
11262
  const project = resolveProject(app.db, request.params.name);
10992
11263
  const kind = parseKindParam(request.query?.kind);
10993
- const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
11264
+ const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10994
11265
  if (!schedule) {
10995
11266
  throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
10996
11267
  }
@@ -11029,7 +11300,8 @@ function formatSchedule(row) {
11029
11300
  import crypto12 from "crypto";
11030
11301
  import { eq as eq17 } from "drizzle-orm";
11031
11302
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
11032
- async function notificationRoutes(app) {
11303
+ async function notificationRoutes(app, opts = {}) {
11304
+ const allowLoopback = opts.allowLoopbackWebhooks === true;
11033
11305
  app.get("/notifications/events", async (_request, reply) => {
11034
11306
  return reply.send(VALID_EVENTS);
11035
11307
  });
@@ -11037,7 +11309,7 @@ async function notificationRoutes(app) {
11037
11309
  const project = resolveProject(app.db, request.params.name);
11038
11310
  const { channel, url, events, source } = request.body ?? {};
11039
11311
  if (channel !== "webhook") throw validationError('Only "webhook" channel is supported');
11040
- const urlCheck = await resolveWebhookTarget(url ?? "");
11312
+ const urlCheck = await resolveWebhookTarget(url ?? "", { allowLoopback });
11041
11313
  if (!urlCheck.ok) throw validationError(urlCheck.message);
11042
11314
  if (!events?.length) throw validationError('"events" must be a non-empty array');
11043
11315
  const invalid = events.filter((e) => !VALID_EVENTS.includes(e));
@@ -11098,7 +11370,7 @@ async function notificationRoutes(app) {
11098
11370
  throw notFound("Notification", request.params.id);
11099
11371
  }
11100
11372
  const config = parseJsonColumn(notification.config, { url: "", events: [] });
11101
- const urlCheck = await resolveWebhookTarget(config.url);
11373
+ const urlCheck = await resolveWebhookTarget(config.url, { allowLoopback });
11102
11374
  if (!urlCheck.ok) throw validationError(`Stored webhook URL is invalid: ${urlCheck.message}`);
11103
11375
  const payload = {
11104
11376
  source: "canonry",
@@ -11146,7 +11418,7 @@ function formatNotification(row) {
11146
11418
 
11147
11419
  // ../api-routes/src/google.ts
11148
11420
  import crypto14 from "crypto";
11149
- import { eq as eq18, and as and8, desc as desc8, sql as sql5 } from "drizzle-orm";
11421
+ import { eq as eq18, and as and9, desc as desc8, sql as sql6 } from "drizzle-orm";
11150
11422
 
11151
11423
  // ../integration-google/src/constants.ts
11152
11424
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -12156,7 +12428,23 @@ async function getValidToken(store, domain, connectionType, clientId, clientSecr
12156
12428
  };
12157
12429
  }
12158
12430
  async function googleRoutes(app, opts) {
12159
- const stateSecret = opts.googleStateSecret ?? "insecure-default-secret";
12431
+ if (opts.googleStateSecret === void 0) {
12432
+ app.log.warn(
12433
+ "googleStateSecret is not configured \u2014 Google OAuth routes will not be registered. Set GOOGLE_STATE_SECRET to enable Google integrations."
12434
+ );
12435
+ return;
12436
+ }
12437
+ if (opts.googleStateSecret === "") {
12438
+ throw new Error(
12439
+ "googleStateSecret is empty. Set a non-empty secret (e.g. `openssl rand -hex 32`) via the GOOGLE_STATE_SECRET environment variable."
12440
+ );
12441
+ }
12442
+ if (opts.googleStateSecret === "insecure-default-secret") {
12443
+ throw new Error(
12444
+ "googleStateSecret is set to the legacy insecure default. Generate a real secret (e.g. `openssl rand -hex 32`) and set GOOGLE_STATE_SECRET."
12445
+ );
12446
+ }
12447
+ const stateSecret = opts.googleStateSecret;
12160
12448
  function getAuthConfig() {
12161
12449
  return opts.getGoogleAuthConfig?.() ?? {};
12162
12450
  }
@@ -12356,14 +12644,14 @@ async function googleRoutes(app, opts) {
12356
12644
  const { startDate, endDate, query, page, limit, offset } = request.query;
12357
12645
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
12358
12646
  const conditions = [eq18(gscSearchData.projectId, project.id)];
12359
- if (startDate) conditions.push(sql5`${gscSearchData.date} >= ${startDate}`);
12360
- else if (cutoffDate) conditions.push(sql5`${gscSearchData.date} >= ${cutoffDate}`);
12361
- if (endDate) conditions.push(sql5`${gscSearchData.date} <= ${endDate}`);
12362
- if (query) conditions.push(sql5`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
12363
- if (page) conditions.push(sql5`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
12647
+ if (startDate) conditions.push(sql6`${gscSearchData.date} >= ${startDate}`);
12648
+ else if (cutoffDate) conditions.push(sql6`${gscSearchData.date} >= ${cutoffDate}`);
12649
+ if (endDate) conditions.push(sql6`${gscSearchData.date} <= ${endDate}`);
12650
+ if (query) conditions.push(sql6`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
12651
+ if (page) conditions.push(sql6`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
12364
12652
  const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
12365
12653
  const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
12366
- const rows = app.db.select().from(gscSearchData).where(and8(...conditions)).orderBy(desc8(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
12654
+ const rows = app.db.select().from(gscSearchData).where(and9(...conditions)).orderBy(desc8(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
12367
12655
  return rows.map((r) => ({
12368
12656
  date: r.date,
12369
12657
  query: r.query,
@@ -12437,7 +12725,7 @@ async function googleRoutes(app, opts) {
12437
12725
  const { url, limit } = request.query;
12438
12726
  const conditions = [eq18(gscUrlInspections.projectId, project.id)];
12439
12727
  if (url) conditions.push(eq18(gscUrlInspections.url, url));
12440
- const rows = app.db.select().from(gscUrlInspections).where(and8(...conditions)).orderBy(desc8(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
12728
+ const rows = app.db.select().from(gscUrlInspections).where(and9(...conditions)).orderBy(desc8(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
12441
12729
  return rows.map((r) => ({
12442
12730
  id: r.id,
12443
12731
  url: r.url,
@@ -12789,7 +13077,7 @@ async function googleRoutes(app, opts) {
12789
13077
 
12790
13078
  // ../api-routes/src/bing.ts
12791
13079
  import crypto15 from "crypto";
12792
- import { eq as eq19, and as and9, desc as desc9 } from "drizzle-orm";
13080
+ import { eq as eq19, and as and10, desc as desc9 } from "drizzle-orm";
12793
13081
 
12794
13082
  // ../integration-bing/src/constants.ts
12795
13083
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -13203,7 +13491,7 @@ async function bingRoutes(app, opts) {
13203
13491
  requireConnectionStore();
13204
13492
  const project = resolveProject(app.db, request.params.name);
13205
13493
  const { url, limit } = request.query;
13206
- const whereClause = url ? and9(eq19(bingUrlInspections.projectId, project.id), eq19(bingUrlInspections.url, url)) : eq19(bingUrlInspections.projectId, project.id);
13494
+ const whereClause = url ? and10(eq19(bingUrlInspections.projectId, project.id), eq19(bingUrlInspections.url, url)) : eq19(bingUrlInspections.projectId, project.id);
13207
13495
  const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc9(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
13208
13496
  return filtered.map((r) => ({
13209
13497
  id: r.id,
@@ -13435,7 +13723,7 @@ async function bingRoutes(app, opts) {
13435
13723
  import fs from "fs";
13436
13724
  import path from "path";
13437
13725
  import os2 from "os";
13438
- import { eq as eq20, and as and10 } from "drizzle-orm";
13726
+ import { eq as eq20, and as and11 } from "drizzle-orm";
13439
13727
  function getScreenshotDir() {
13440
13728
  return path.join(os2.homedir(), ".canonry", "screenshots");
13441
13729
  }
@@ -13508,7 +13796,7 @@ async function cdpRoutes(app, opts) {
13508
13796
  async (request, reply) => {
13509
13797
  const project = resolveProject(app.db, request.params.name);
13510
13798
  const { runId } = request.params;
13511
- const run = app.db.select().from(runs).where(and10(eq20(runs.id, runId), eq20(runs.projectId, project.id))).get();
13799
+ const run = app.db.select().from(runs).where(and11(eq20(runs.id, runId), eq20(runs.projectId, project.id))).get();
13512
13800
  if (!run) {
13513
13801
  const err = notFound("Run", runId);
13514
13802
  return reply.code(err.statusCode).send(err.toJSON());
@@ -13605,7 +13893,7 @@ async function cdpRoutes(app, opts) {
13605
13893
 
13606
13894
  // ../api-routes/src/ga.ts
13607
13895
  import crypto16 from "crypto";
13608
- import { eq as eq21, desc as desc10, and as and11, sql as sql6 } from "drizzle-orm";
13896
+ import { eq as eq21, desc as desc10, and as and12, sql as sql7 } from "drizzle-orm";
13609
13897
  function gaLog(level, action, ctx) {
13610
13898
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
13611
13899
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -13900,10 +14188,10 @@ async function ga4Routes(app, opts) {
13900
14188
  app.db.transaction((tx) => {
13901
14189
  if (syncTraffic) {
13902
14190
  tx.delete(gaTrafficSnapshots).where(
13903
- and11(
14191
+ and12(
13904
14192
  eq21(gaTrafficSnapshots.projectId, project.id),
13905
- sql6`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
13906
- sql6`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
14193
+ sql7`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
14194
+ sql7`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
13907
14195
  )
13908
14196
  ).run();
13909
14197
  for (const row of rows) {
@@ -13924,10 +14212,10 @@ async function ga4Routes(app, opts) {
13924
14212
  }
13925
14213
  if (syncAi) {
13926
14214
  tx.delete(gaAiReferrals).where(
13927
- and11(
14215
+ and12(
13928
14216
  eq21(gaAiReferrals.projectId, project.id),
13929
- sql6`${gaAiReferrals.date} >= ${summary.periodStart}`,
13930
- sql6`${gaAiReferrals.date} <= ${summary.periodEnd}`
14217
+ sql7`${gaAiReferrals.date} >= ${summary.periodStart}`,
14218
+ sql7`${gaAiReferrals.date} <= ${summary.periodEnd}`
13931
14219
  )
13932
14220
  ).run();
13933
14221
  for (const row of aiReferrals) {
@@ -13950,10 +14238,10 @@ async function ga4Routes(app, opts) {
13950
14238
  }
13951
14239
  if (syncSocial) {
13952
14240
  tx.delete(gaSocialReferrals).where(
13953
- and11(
14241
+ and12(
13954
14242
  eq21(gaSocialReferrals.projectId, project.id),
13955
- sql6`${gaSocialReferrals.date} >= ${summary.periodStart}`,
13956
- sql6`${gaSocialReferrals.date} <= ${summary.periodEnd}`
14243
+ sql7`${gaSocialReferrals.date} >= ${summary.periodStart}`,
14244
+ sql7`${gaSocialReferrals.date} <= ${summary.periodEnd}`
13957
14245
  )
13958
14246
  ).run();
13959
14247
  for (const row of socialReferrals) {
@@ -14043,65 +14331,65 @@ async function ga4Routes(app, opts) {
14043
14331
  const cutoff = windowCutoff(window);
14044
14332
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
14045
14333
  const snapshotConditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
14046
- if (cutoffDate) snapshotConditions.push(sql6`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
14334
+ if (cutoffDate) snapshotConditions.push(sql7`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
14047
14335
  const aiConditions = [eq21(gaAiReferrals.projectId, project.id)];
14048
- if (cutoffDate) aiConditions.push(sql6`${gaAiReferrals.date} >= ${cutoffDate}`);
14336
+ if (cutoffDate) aiConditions.push(sql7`${gaAiReferrals.date} >= ${cutoffDate}`);
14049
14337
  const socialConditions = [eq21(gaSocialReferrals.projectId, project.id)];
14050
- if (cutoffDate) socialConditions.push(sql6`${gaSocialReferrals.date} >= ${cutoffDate}`);
14338
+ if (cutoffDate) socialConditions.push(sql7`${gaSocialReferrals.date} >= ${cutoffDate}`);
14051
14339
  const windowSummaryRow = cutoffDate ? app.db.select({
14052
14340
  totalSessions: gaTrafficWindowSummaries.totalSessions,
14053
14341
  totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
14054
14342
  totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
14055
14343
  totalUsers: gaTrafficWindowSummaries.totalUsers
14056
14344
  }).from(gaTrafficWindowSummaries).where(
14057
- and11(
14345
+ and12(
14058
14346
  eq21(gaTrafficWindowSummaries.projectId, project.id),
14059
14347
  eq21(gaTrafficWindowSummaries.windowKey, window)
14060
14348
  )
14061
14349
  ).get() : null;
14062
14350
  const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
14063
- totalSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
14064
- totalOrganicSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
14065
- totalUsers: sql6`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
14066
- }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).get() : null;
14351
+ totalSessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
14352
+ totalOrganicSessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
14353
+ totalUsers: sql7`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
14354
+ }).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).get() : null;
14067
14355
  const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
14068
14356
  totalSessions: gaTrafficSummaries.totalSessions,
14069
14357
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
14070
14358
  totalUsers: gaTrafficSummaries.totalUsers
14071
14359
  }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
14072
14360
  const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
14073
- totalDirectSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
14074
- }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).get();
14361
+ totalDirectSessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
14362
+ }).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).get();
14075
14363
  const summaryMeta = app.db.select({
14076
14364
  periodStart: gaTrafficSummaries.periodStart,
14077
14365
  periodEnd: gaTrafficSummaries.periodEnd
14078
14366
  }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
14079
14367
  const rows = app.db.select({
14080
- landingPage: sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
14081
- sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
14082
- organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
14083
- directSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
14084
- users: sql6`SUM(${gaTrafficSnapshots.users})`
14085
- }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).groupBy(sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql6`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
14368
+ landingPage: sql7`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
14369
+ sessions: sql7`SUM(${gaTrafficSnapshots.sessions})`,
14370
+ organicSessions: sql7`SUM(${gaTrafficSnapshots.organicSessions})`,
14371
+ directSessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
14372
+ users: sql7`SUM(${gaTrafficSnapshots.users})`
14373
+ }).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).groupBy(sql7`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql7`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
14086
14374
  const aiReferralRows = app.db.select({
14087
14375
  source: gaAiReferrals.source,
14088
14376
  medium: gaAiReferrals.medium,
14089
14377
  sourceDimension: gaAiReferrals.sourceDimension,
14090
- sessions: sql6`SUM(${gaAiReferrals.sessions})`,
14091
- users: sql6`SUM(${gaAiReferrals.users})`
14092
- }).from(gaAiReferrals).where(and11(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
14378
+ sessions: sql7`SUM(${gaAiReferrals.sessions})`,
14379
+ users: sql7`SUM(${gaAiReferrals.users})`
14380
+ }).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
14093
14381
  const aiReferralLandingPageRows = app.db.select({
14094
14382
  source: gaAiReferrals.source,
14095
14383
  medium: gaAiReferrals.medium,
14096
14384
  sourceDimension: gaAiReferrals.sourceDimension,
14097
- landingPage: sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
14098
- sessions: sql6`SUM(${gaAiReferrals.sessions})`,
14099
- users: sql6`SUM(${gaAiReferrals.users})`
14100
- }).from(gaAiReferrals).where(and11(...aiConditions)).groupBy(
14385
+ landingPage: sql7`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
14386
+ sessions: sql7`SUM(${gaAiReferrals.sessions})`,
14387
+ users: sql7`SUM(${gaAiReferrals.users})`
14388
+ }).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(
14101
14389
  gaAiReferrals.source,
14102
14390
  gaAiReferrals.medium,
14103
14391
  gaAiReferrals.sourceDimension,
14104
- sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
14392
+ sql7`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
14105
14393
  ).all();
14106
14394
  const aiReferrals = pickWinningDimension(
14107
14395
  aiReferralRows,
@@ -14112,10 +14400,10 @@ async function ga4Routes(app, opts) {
14112
14400
  (r) => `${r.source}\0${r.medium}\0${r.landingPage}`
14113
14401
  );
14114
14402
  const aiDeduped = app.db.select({
14115
- sessions: sql6`COALESCE(SUM(max_sessions), 0)`,
14116
- users: sql6`COALESCE(SUM(max_users), 0)`
14403
+ sessions: sql7`COALESCE(SUM(max_sessions), 0)`,
14404
+ users: sql7`COALESCE(SUM(max_users), 0)`
14117
14405
  }).from(
14118
- sql6`(
14406
+ sql7`(
14119
14407
  SELECT date, source, medium,
14120
14408
  MAX(dimension_sessions) AS max_sessions,
14121
14409
  MAX(dimension_users) AS max_users
@@ -14124,7 +14412,7 @@ async function ga4Routes(app, opts) {
14124
14412
  SUM(sessions) AS dimension_sessions,
14125
14413
  SUM(users) AS dimension_users
14126
14414
  FROM ga_ai_referrals
14127
- WHERE project_id = ${project.id}${cutoffDate ? sql6` AND date >= ${cutoffDate}` : sql6``}
14415
+ WHERE project_id = ${project.id}${cutoffDate ? sql7` AND date >= ${cutoffDate}` : sql7``}
14128
14416
  GROUP BY date, source, medium, source_dimension
14129
14417
  )
14130
14418
  GROUP BY date, source, medium
@@ -14132,9 +14420,9 @@ async function ga4Routes(app, opts) {
14132
14420
  ).get();
14133
14421
  const aiBySessionRows = app.db.select({
14134
14422
  channelGroup: gaAiReferrals.channelGroup,
14135
- sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
14136
- users: sql6`COALESCE(SUM(${gaAiReferrals.users}), 0)`
14137
- }).from(gaAiReferrals).where(and11(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
14423
+ sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
14424
+ users: sql7`COALESCE(SUM(${gaAiReferrals.users}), 0)`
14425
+ }).from(gaAiReferrals).where(and12(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
14138
14426
  const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
14139
14427
  let aiBySessionUsers = 0;
14140
14428
  for (const row of aiBySessionRows) {
@@ -14146,13 +14434,13 @@ async function ga4Routes(app, opts) {
14146
14434
  source: gaSocialReferrals.source,
14147
14435
  medium: gaSocialReferrals.medium,
14148
14436
  channelGroup: gaSocialReferrals.channelGroup,
14149
- sessions: sql6`SUM(${gaSocialReferrals.sessions})`,
14150
- users: sql6`SUM(${gaSocialReferrals.users})`
14151
- }).from(gaSocialReferrals).where(and11(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql6`SUM(${gaSocialReferrals.sessions}) DESC`).all();
14437
+ sessions: sql7`SUM(${gaSocialReferrals.sessions})`,
14438
+ users: sql7`SUM(${gaSocialReferrals.users})`
14439
+ }).from(gaSocialReferrals).where(and12(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql7`SUM(${gaSocialReferrals.sessions}) DESC`).all();
14152
14440
  const socialTotals = app.db.select({
14153
- sessions: sql6`SUM(${gaSocialReferrals.sessions})`,
14154
- users: sql6`SUM(${gaSocialReferrals.users})`
14155
- }).from(gaSocialReferrals).where(and11(...socialConditions)).get();
14441
+ sessions: sql7`SUM(${gaSocialReferrals.sessions})`,
14442
+ users: sql7`SUM(${gaSocialReferrals.users})`
14443
+ }).from(gaSocialReferrals).where(and12(...socialConditions)).get();
14156
14444
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
14157
14445
  const total = summaryRow?.totalSessions ?? 0;
14158
14446
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
@@ -14234,21 +14522,21 @@ async function ga4Routes(app, opts) {
14234
14522
  requireGa4Connection(opts, project.name, project.canonicalDomain);
14235
14523
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
14236
14524
  const conditions = [eq21(gaAiReferrals.projectId, project.id)];
14237
- if (cutoffDate) conditions.push(sql6`${gaAiReferrals.date} >= ${cutoffDate}`);
14525
+ if (cutoffDate) conditions.push(sql7`${gaAiReferrals.date} >= ${cutoffDate}`);
14238
14526
  const rows = app.db.select({
14239
14527
  date: gaAiReferrals.date,
14240
14528
  source: gaAiReferrals.source,
14241
14529
  medium: gaAiReferrals.medium,
14242
- landingPage: sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
14530
+ landingPage: sql7`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
14243
14531
  sourceDimension: gaAiReferrals.sourceDimension,
14244
- sessions: sql6`SUM(${gaAiReferrals.sessions})`,
14245
- users: sql6`SUM(${gaAiReferrals.users})`
14246
- }).from(gaAiReferrals).where(and11(...conditions)).groupBy(
14532
+ sessions: sql7`SUM(${gaAiReferrals.sessions})`,
14533
+ users: sql7`SUM(${gaAiReferrals.users})`
14534
+ }).from(gaAiReferrals).where(and12(...conditions)).groupBy(
14247
14535
  gaAiReferrals.date,
14248
14536
  gaAiReferrals.source,
14249
14537
  gaAiReferrals.medium,
14250
14538
  gaAiReferrals.sourceDimension,
14251
- sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
14539
+ sql7`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
14252
14540
  ).orderBy(gaAiReferrals.date).all();
14253
14541
  return rows;
14254
14542
  });
@@ -14257,7 +14545,7 @@ async function ga4Routes(app, opts) {
14257
14545
  requireGa4Connection(opts, project.name, project.canonicalDomain);
14258
14546
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
14259
14547
  const conditions = [eq21(gaSocialReferrals.projectId, project.id)];
14260
- if (cutoffDate) conditions.push(sql6`${gaSocialReferrals.date} >= ${cutoffDate}`);
14548
+ if (cutoffDate) conditions.push(sql7`${gaSocialReferrals.date} >= ${cutoffDate}`);
14261
14549
  const rows = app.db.select({
14262
14550
  date: gaSocialReferrals.date,
14263
14551
  source: gaSocialReferrals.source,
@@ -14265,7 +14553,7 @@ async function ga4Routes(app, opts) {
14265
14553
  channelGroup: gaSocialReferrals.channelGroup,
14266
14554
  sessions: gaSocialReferrals.sessions,
14267
14555
  users: gaSocialReferrals.users
14268
- }).from(gaSocialReferrals).where(and11(...conditions)).orderBy(gaSocialReferrals.date).all();
14556
+ }).from(gaSocialReferrals).where(and12(...conditions)).orderBy(gaSocialReferrals.date).all();
14269
14557
  return rows;
14270
14558
  });
14271
14559
  app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
@@ -14278,10 +14566,10 @@ async function ga4Routes(app, opts) {
14278
14566
  d.setDate(d.getDate() - n);
14279
14567
  return fmt(d);
14280
14568
  };
14281
- const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(
14569
+ const sumSocial = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(
14282
14570
  eq21(gaSocialReferrals.projectId, project.id),
14283
- sql6`${gaSocialReferrals.date} >= ${from}`,
14284
- sql6`${gaSocialReferrals.date} < ${to}`
14571
+ sql7`${gaSocialReferrals.date} >= ${from}`,
14572
+ sql7`${gaSocialReferrals.date} < ${to}`
14285
14573
  )).get();
14286
14574
  const current7d = sumSocial(daysAgo2(7), fmt(today));
14287
14575
  const prev7d = sumSocial(daysAgo2(14), daysAgo2(7));
@@ -14290,19 +14578,19 @@ async function ga4Routes(app, opts) {
14290
14578
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
14291
14579
  const sourceCurrent = app.db.select({
14292
14580
  source: gaSocialReferrals.source,
14293
- sessions: sql6`SUM(${gaSocialReferrals.sessions})`
14294
- }).from(gaSocialReferrals).where(and11(
14581
+ sessions: sql7`SUM(${gaSocialReferrals.sessions})`
14582
+ }).from(gaSocialReferrals).where(and12(
14295
14583
  eq21(gaSocialReferrals.projectId, project.id),
14296
- sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
14297
- sql6`${gaSocialReferrals.date} < ${fmt(today)}`
14584
+ sql7`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
14585
+ sql7`${gaSocialReferrals.date} < ${fmt(today)}`
14298
14586
  )).groupBy(gaSocialReferrals.source).all();
14299
14587
  const sourcePrev = app.db.select({
14300
14588
  source: gaSocialReferrals.source,
14301
- sessions: sql6`SUM(${gaSocialReferrals.sessions})`
14302
- }).from(gaSocialReferrals).where(and11(
14589
+ sessions: sql7`SUM(${gaSocialReferrals.sessions})`
14590
+ }).from(gaSocialReferrals).where(and12(
14303
14591
  eq21(gaSocialReferrals.projectId, project.id),
14304
- sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
14305
- sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`
14592
+ sql7`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
14593
+ sql7`${gaSocialReferrals.date} < ${daysAgo2(7)}`
14306
14594
  )).groupBy(gaSocialReferrals.source).all();
14307
14595
  const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
14308
14596
  let biggestMover = null;
@@ -14341,16 +14629,16 @@ async function ga4Routes(app, opts) {
14341
14629
  return fmt(d);
14342
14630
  };
14343
14631
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
14344
- const sumTotal = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14345
- const sumOrganic = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14346
- const sumDirect = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14347
- const sumAi = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
14632
+ const sumTotal = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql7`${gaTrafficSnapshots.date} >= ${from}`, sql7`${gaTrafficSnapshots.date} < ${to}`)).get();
14633
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql7`${gaTrafficSnapshots.date} >= ${from}`, sql7`${gaTrafficSnapshots.date} < ${to}`)).get();
14634
+ const sumDirect = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql7`${gaTrafficSnapshots.date} >= ${from}`, sql7`${gaTrafficSnapshots.date} < ${to}`)).get();
14635
+ const sumAi = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14348
14636
  eq21(gaAiReferrals.projectId, project.id),
14349
- sql6`${gaAiReferrals.date} >= ${from}`,
14350
- sql6`${gaAiReferrals.date} < ${to}`,
14637
+ sql7`${gaAiReferrals.date} >= ${from}`,
14638
+ sql7`${gaAiReferrals.date} < ${to}`,
14351
14639
  eq21(gaAiReferrals.sourceDimension, "session")
14352
14640
  )).get();
14353
- const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${from}`, sql6`${gaSocialReferrals.date} < ${to}`)).get();
14641
+ const sumSocial = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql7`${gaSocialReferrals.date} >= ${from}`, sql7`${gaSocialReferrals.date} < ${to}`)).get();
14354
14642
  const todayStr = fmt(today);
14355
14643
  const buildTrend = (sum) => {
14356
14644
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -14359,16 +14647,16 @@ async function ga4Routes(app, opts) {
14359
14647
  const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
14360
14648
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
14361
14649
  };
14362
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
14650
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14363
14651
  eq21(gaAiReferrals.projectId, project.id),
14364
- sql6`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
14365
- sql6`${gaAiReferrals.date} < ${todayStr}`,
14652
+ sql7`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
14653
+ sql7`${gaAiReferrals.date} < ${todayStr}`,
14366
14654
  eq21(gaAiReferrals.sourceDimension, "session")
14367
14655
  )).groupBy(gaAiReferrals.source).all();
14368
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
14656
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14369
14657
  eq21(gaAiReferrals.projectId, project.id),
14370
- sql6`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
14371
- sql6`${gaAiReferrals.date} < ${daysAgo2(7)}`,
14658
+ sql7`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
14659
+ sql7`${gaAiReferrals.date} < ${daysAgo2(7)}`,
14372
14660
  eq21(gaAiReferrals.sourceDimension, "session")
14373
14661
  )).groupBy(gaAiReferrals.source).all();
14374
14662
  const findBiggestMover = (current, prev) => {
@@ -14385,8 +14673,8 @@ async function ga4Routes(app, opts) {
14385
14673
  }
14386
14674
  return mover;
14387
14675
  };
14388
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql6`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
14389
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
14676
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql7`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql7`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql7`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
14677
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql7`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql7`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql7`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
14390
14678
  return {
14391
14679
  total: buildTrend(sumTotal),
14392
14680
  organic: buildTrend(sumOrganic),
@@ -14402,13 +14690,13 @@ async function ga4Routes(app, opts) {
14402
14690
  requireGa4Connection(opts, project.name, project.canonicalDomain);
14403
14691
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
14404
14692
  const conditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
14405
- if (cutoffDate) conditions.push(sql6`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
14693
+ if (cutoffDate) conditions.push(sql7`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
14406
14694
  const rows = app.db.select({
14407
14695
  date: gaTrafficSnapshots.date,
14408
- sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
14409
- organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
14410
- users: sql6`SUM(${gaTrafficSnapshots.users})`
14411
- }).from(gaTrafficSnapshots).where(and11(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
14696
+ sessions: sql7`SUM(${gaTrafficSnapshots.sessions})`,
14697
+ organicSessions: sql7`SUM(${gaTrafficSnapshots.organicSessions})`,
14698
+ users: sql7`SUM(${gaTrafficSnapshots.users})`
14699
+ }).from(gaTrafficSnapshots).where(and12(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
14412
14700
  return rows.map((r) => ({
14413
14701
  date: r.date,
14414
14702
  sessions: r.sessions ?? 0,
@@ -14420,11 +14708,11 @@ async function ga4Routes(app, opts) {
14420
14708
  const project = resolveProject(app.db, request.params.name);
14421
14709
  requireGa4Connection(opts, project.name, project.canonicalDomain);
14422
14710
  const trafficPages = app.db.select({
14423
- landingPage: sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
14424
- sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
14425
- organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
14426
- users: sql6`SUM(${gaTrafficSnapshots.users})`
14427
- }).from(gaTrafficSnapshots).where(eq21(gaTrafficSnapshots.projectId, project.id)).groupBy(sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql6`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
14711
+ landingPage: sql7`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
14712
+ sessions: sql7`SUM(${gaTrafficSnapshots.sessions})`,
14713
+ organicSessions: sql7`SUM(${gaTrafficSnapshots.organicSessions})`,
14714
+ users: sql7`SUM(${gaTrafficSnapshots.users})`
14715
+ }).from(gaTrafficSnapshots).where(eq21(gaTrafficSnapshots.projectId, project.id)).groupBy(sql7`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql7`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
14428
14716
  return {
14429
14717
  pages: trafficPages.map((r) => ({
14430
14718
  landingPage: r.landingPage,
@@ -14640,10 +14928,10 @@ function buildAuthErrorMessage(res, responseText) {
14640
14928
  }
14641
14929
  return "WordPress credentials are invalid or lack permission for this action";
14642
14930
  }
14643
- async function fetchJson(connection, siteUrl, path15, init) {
14931
+ async function fetchJson(connection, siteUrl, path16, init) {
14644
14932
  if (siteUrl.startsWith("http:")) {
14645
14933
  }
14646
- const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path15}`, {
14934
+ const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path16}`, {
14647
14935
  ...init,
14648
14936
  headers: {
14649
14937
  "Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
@@ -16061,7 +16349,7 @@ async function wordpressRoutes(app, opts) {
16061
16349
 
16062
16350
  // ../api-routes/src/backlinks.ts
16063
16351
  import crypto18 from "crypto";
16064
- import { and as and13, asc as asc2, desc as desc11, eq as eq22, sql as sql7 } from "drizzle-orm";
16352
+ import { and as and14, asc as asc2, desc as desc11, eq as eq22, sql as sql8 } from "drizzle-orm";
16065
16353
 
16066
16354
  // ../integration-commoncrawl/src/constants.ts
16067
16355
  import os3 from "os";
@@ -16236,7 +16524,7 @@ function parseContentLength2(value) {
16236
16524
 
16237
16525
  // ../integration-commoncrawl/src/plugin-resolver.ts
16238
16526
  import fs3 from "fs";
16239
- import { createRequire as createRequire2 } from "module";
16527
+ import { createRequire as createRequire3 } from "module";
16240
16528
  import path4 from "path";
16241
16529
  function pluginDirFor(pkgJson) {
16242
16530
  return path4.dirname(pkgJson);
@@ -16255,7 +16543,7 @@ function loadDuckdb(opts = {}) {
16255
16543
  );
16256
16544
  }
16257
16545
  try {
16258
- const pluginRequire = createRequire2(duckdbPkg);
16546
+ const pluginRequire = createRequire3(duckdbPkg);
16259
16547
  return pluginRequire("@duckdb/node-api");
16260
16548
  } catch {
16261
16549
  throw missingDependency(
@@ -16345,7 +16633,7 @@ async function queryBacklinks(opts) {
16345
16633
  const reversed = opts.targets.map(reverseDomain);
16346
16634
  const targetList = reversed.map(quote).join(", ");
16347
16635
  const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
16348
- const sql14 = `
16636
+ const sql15 = `
16349
16637
  WITH vertices AS (
16350
16638
  SELECT * FROM read_csv(
16351
16639
  ${quote(opts.vertexPath)},
@@ -16381,7 +16669,7 @@ async function queryBacklinks(opts) {
16381
16669
  const conn = await instance.connect();
16382
16670
  let rows;
16383
16671
  try {
16384
- const reader = await conn.runAndReadAll(sql14);
16672
+ const reader = await conn.runAndReadAll(sql15);
16385
16673
  rows = reader.getRowObjects();
16386
16674
  } finally {
16387
16675
  conn.disconnectSync?.();
@@ -16458,7 +16746,7 @@ function pruneCachedRelease(release, opts = {}) {
16458
16746
  }
16459
16747
 
16460
16748
  // ../api-routes/src/backlinks-filter.ts
16461
- import { and as and12, ne as ne2, notLike } from "drizzle-orm";
16749
+ import { and as and13, ne as ne2, notLike } from "drizzle-orm";
16462
16750
  var BACKLINK_FILTER_PATTERNS = [
16463
16751
  "*.google.com",
16464
16752
  "*.googleusercontent.com",
@@ -16481,7 +16769,7 @@ function backlinkCrawlerExclusionClause() {
16481
16769
  conditions.push(ne2(backlinkDomains.linkingDomain, pattern));
16482
16770
  }
16483
16771
  }
16484
- const combined = and12(...conditions);
16772
+ const combined = and13(...conditions);
16485
16773
  if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
16486
16774
  return combined;
16487
16775
  }
@@ -16542,7 +16830,7 @@ function mapRunRow(row) {
16542
16830
  };
16543
16831
  }
16544
16832
  function latestSummaryForProject(db, projectId, release) {
16545
- const condition = release ? and13(eq22(backlinkSummaries.projectId, projectId), eq22(backlinkSummaries.release, release)) : eq22(backlinkSummaries.projectId, projectId);
16833
+ const condition = release ? and14(eq22(backlinkSummaries.projectId, projectId), eq22(backlinkSummaries.release, release)) : eq22(backlinkSummaries.projectId, projectId);
16546
16834
  return db.select().from(backlinkSummaries).where(condition).orderBy(desc11(backlinkSummaries.queriedAt)).limit(1).get();
16547
16835
  }
16548
16836
  function parseExcludeCrawlers(value) {
@@ -16551,18 +16839,18 @@ function parseExcludeCrawlers(value) {
16551
16839
  return lower === "1" || lower === "true" || lower === "yes";
16552
16840
  }
16553
16841
  function computeFilteredSummary(db, base) {
16554
- const baseDomainCondition = and13(
16842
+ const baseDomainCondition = and14(
16555
16843
  eq22(backlinkDomains.projectId, base.projectId),
16556
16844
  eq22(backlinkDomains.release, base.release)
16557
16845
  );
16558
- const filteredCondition = and13(baseDomainCondition, backlinkCrawlerExclusionClause());
16846
+ const filteredCondition = and14(baseDomainCondition, backlinkCrawlerExclusionClause());
16559
16847
  const unfilteredAgg = db.select({
16560
- count: sql7`count(*)`,
16561
- total: sql7`coalesce(sum(${backlinkDomains.numHosts}), 0)`
16848
+ count: sql8`count(*)`,
16849
+ total: sql8`coalesce(sum(${backlinkDomains.numHosts}), 0)`
16562
16850
  }).from(backlinkDomains).where(baseDomainCondition).get();
16563
16851
  const filteredAgg = db.select({
16564
- count: sql7`count(*)`,
16565
- total: sql7`coalesce(sum(${backlinkDomains.numHosts}), 0)`
16852
+ count: sql8`count(*)`,
16853
+ total: sql8`coalesce(sum(${backlinkDomains.numHosts}), 0)`
16566
16854
  }).from(backlinkDomains).where(filteredCondition).get();
16567
16855
  const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc11(backlinkDomains.numHosts)).limit(10).all();
16568
16856
  const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
@@ -16731,12 +17019,12 @@ async function backlinksRoutes(app, opts) {
16731
17019
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
16732
17020
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
16733
17021
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
16734
- const baseDomainCondition = and13(
17022
+ const baseDomainCondition = and14(
16735
17023
  eq22(backlinkDomains.projectId, project.id),
16736
17024
  eq22(backlinkDomains.release, targetRelease)
16737
17025
  );
16738
- const domainCondition = excludeCrawlers ? and13(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
16739
- const totalRow = app.db.select({ count: sql7`count(*)` }).from(backlinkDomains).where(domainCondition).get();
17026
+ const domainCondition = excludeCrawlers ? and14(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
17027
+ const totalRow = app.db.select({ count: sql8`count(*)` }).from(backlinkDomains).where(domainCondition).get();
16740
17028
  const rows = app.db.select({
16741
17029
  linkingDomain: backlinkDomains.linkingDomain,
16742
17030
  numHosts: backlinkDomains.numHosts
@@ -16771,7 +17059,7 @@ async function backlinksRoutes(app, opts) {
16771
17059
 
16772
17060
  // ../api-routes/src/traffic.ts
16773
17061
  import crypto20 from "crypto";
16774
- import { and as and14, desc as desc12, eq as eq23, gte as gte2, lte as lte2, sql as sql8 } from "drizzle-orm";
17062
+ import { and as and15, desc as desc12, eq as eq23, gte as gte2, lte as lte2, sql as sql9 } from "drizzle-orm";
16775
17063
 
16776
17064
  // ../integration-cloud-run/src/auth.ts
16777
17065
  import crypto19 from "crypto";
@@ -17270,8 +17558,8 @@ var ASSET_PATH_PREFIXES = [
17270
17558
  "/img/",
17271
17559
  "/static/"
17272
17560
  ];
17273
- function normalizeTrafficPathPattern(path15) {
17274
- const cleanPath = path15.trim() || "/";
17561
+ function normalizeTrafficPathPattern(path16) {
17562
+ const cleanPath = path16.trim() || "/";
17275
17563
  const pathOnly = cleanPath.split("?")[0] || "/";
17276
17564
  const segments = pathOnly.split("/").map((segment) => {
17277
17565
  if (!segment) return segment;
@@ -17320,8 +17608,8 @@ function resolveAiReferralLandingPath(event, evidenceType) {
17320
17608
  }
17321
17609
  return normalizeTrafficPathPattern(event.path);
17322
17610
  }
17323
- function isLikelySubresourcePath(path15) {
17324
- const cleanPath = path15.split("?")[0] || "/";
17611
+ function isLikelySubresourcePath(path16) {
17612
+ const cleanPath = path16.split("?")[0] || "/";
17325
17613
  return ASSET_PATH_PREFIXES.some((prefix) => cleanPath.startsWith(prefix)) || ASSET_EXTENSION_PATTERN.test(cleanPath);
17326
17614
  }
17327
17615
  function actorKey(event) {
@@ -17507,11 +17795,11 @@ function buildEventId2(event) {
17507
17795
  function normalizeWordpressTrafficEvent(event) {
17508
17796
  if (!event.observed_at) return null;
17509
17797
  if (typeof event.id !== "number" || !Number.isFinite(event.id)) return null;
17510
- const path15 = event.path?.trim();
17511
- if (!path15) return null;
17798
+ const path16 = event.path?.trim();
17799
+ if (!path16) return null;
17512
17800
  const queryString = trimOrNull(event.query_string);
17513
17801
  const host = trimOrNull(event.host);
17514
- const requestUrl = host ? `https://${host}${path15}${queryString ? `?${queryString}` : ""}` : `${path15}${queryString ? `?${queryString}` : ""}`;
17802
+ const requestUrl = host ? `https://${host}${path16}${queryString ? `?${queryString}` : ""}` : `${path16}${queryString ? `?${queryString}` : ""}`;
17515
17803
  return {
17516
17804
  sourceType: TrafficSourceTypes.wordpress,
17517
17805
  evidenceKind: TrafficEvidenceKinds["raw-request"],
@@ -17521,7 +17809,7 @@ function normalizeWordpressTrafficEvent(event) {
17521
17809
  method: trimOrNull(event.method),
17522
17810
  requestUrl,
17523
17811
  host,
17524
- path: path15,
17812
+ path: path16,
17525
17813
  queryString,
17526
17814
  status: typeof event.status === "number" && Number.isFinite(event.status) ? event.status : null,
17527
17815
  userAgent: trimOrNull(event.user_agent),
@@ -17685,15 +17973,15 @@ function stringLabels(input) {
17685
17973
  );
17686
17974
  }
17687
17975
  function normalizeVercelLogRow(row) {
17688
- const path15 = row.requestPath;
17689
- if (!path15) return null;
17976
+ const path16 = row.requestPath;
17977
+ if (!path16) return null;
17690
17978
  const observedAt = row.timestamp;
17691
17979
  if (!observedAt) return null;
17692
17980
  const requestId = row.requestId;
17693
17981
  if (!requestId) return null;
17694
17982
  const host = emptyToNull(row.domain);
17695
17983
  const queryString = serializeSearchParams(row.requestSearchParams);
17696
- const requestUrl = host ? `https://${host}${path15}${queryString ? `?${queryString}` : ""}` : null;
17984
+ const requestUrl = host ? `https://${host}${path16}${queryString ? `?${queryString}` : ""}` : null;
17697
17985
  return {
17698
17986
  sourceType: TrafficSourceTypes.vercel,
17699
17987
  evidenceKind: TrafficEvidenceKinds["raw-request"],
@@ -17703,7 +17991,7 @@ function normalizeVercelLogRow(row) {
17703
17991
  method: row.requestMethod ?? null,
17704
17992
  requestUrl,
17705
17993
  host,
17706
- path: path15,
17994
+ path: path16,
17707
17995
  queryString,
17708
17996
  status: resolveStatus(row),
17709
17997
  userAgent: emptyToNull(row.clientUserAgent),
@@ -17839,7 +18127,7 @@ var DEFAULT_WP_MAX_PAGES = 20;
17839
18127
  var DEFAULT_VERCEL_MAX_PAGES = 50;
17840
18128
  var MAX_TRACKED_EVENT_IDS = 1e3;
17841
18129
  var DEFAULT_BACKFILL_DAYS = 30;
17842
- var MAX_BACKFILL_DAYS = 30;
18130
+ var MAX_BACKFILL_DAYS = 90;
17843
18131
  var BACKFILL_MAX_PAGES = 1e3;
17844
18132
  var BACKFILL_SAMPLE_LIMIT = 500;
17845
18133
  function parseSourceConfig(row) {
@@ -17919,21 +18207,21 @@ async function runBackfillTask(options) {
17919
18207
  try {
17920
18208
  app.db.transaction((tx) => {
17921
18209
  tx.delete(crawlerEventsHourly).where(
17922
- and14(
18210
+ and15(
17923
18211
  eq23(crawlerEventsHourly.sourceId, sourceRow.id),
17924
18212
  gte2(crawlerEventsHourly.tsHour, windowStartIso),
17925
18213
  lte2(crawlerEventsHourly.tsHour, windowEndIso)
17926
18214
  )
17927
18215
  ).run();
17928
18216
  tx.delete(aiReferralEventsHourly).where(
17929
- and14(
18217
+ and15(
17930
18218
  eq23(aiReferralEventsHourly.sourceId, sourceRow.id),
17931
18219
  gte2(aiReferralEventsHourly.tsHour, windowStartIso),
17932
18220
  lte2(aiReferralEventsHourly.tsHour, windowEndIso)
17933
18221
  )
17934
18222
  ).run();
17935
18223
  tx.delete(rawEventSamples).where(
17936
- and14(
18224
+ and15(
17937
18225
  eq23(rawEventSamples.sourceId, sourceRow.id),
17938
18226
  gte2(rawEventSamples.ts, windowStartIso),
17939
18227
  lte2(rawEventSamples.ts, windowEndIso)
@@ -18356,7 +18644,8 @@ async function trafficRoutes(app, opts) {
18356
18644
  endTime: windowEnd.toISOString(),
18357
18645
  pageSize,
18358
18646
  maxPages,
18359
- firstSync: isFirstSync
18647
+ firstSync: isFirstSync,
18648
+ requestUrlSubstrings: [project.canonicalDomain]
18360
18649
  });
18361
18650
  allEvents = page.events;
18362
18651
  } catch (e) {
@@ -18508,7 +18797,7 @@ async function trafficRoutes(app, opts) {
18508
18797
  crawlerEventsHourly.status
18509
18798
  ],
18510
18799
  set: {
18511
- hits: sql8`${crawlerEventsHourly.hits} + ${bucket.hits}`,
18800
+ hits: sql9`${crawlerEventsHourly.hits} + ${bucket.hits}`,
18512
18801
  sampledUserAgent: bucket.sampledUserAgent,
18513
18802
  updatedAt: finishedAt
18514
18803
  }
@@ -18543,7 +18832,7 @@ async function trafficRoutes(app, opts) {
18543
18832
  aiReferralEventsHourly.status
18544
18833
  ],
18545
18834
  set: {
18546
- sessionsOrHits: sql8`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
18835
+ sessionsOrHits: sql9`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
18547
18836
  updatedAt: finishedAt
18548
18837
  }
18549
18838
  }).run();
@@ -18682,7 +18971,8 @@ async function trafficRoutes(app, opts) {
18682
18971
  // ring-buffer reseed at the end takes the most-recent IDs from the
18683
18972
  // dedupedEvents anyway.
18684
18973
  firstSync: false,
18685
- orderBy: "timestamp asc"
18974
+ orderBy: "timestamp asc",
18975
+ requestUrlSubstrings: [project.canonicalDomain]
18686
18976
  });
18687
18977
  return page.events;
18688
18978
  };
@@ -18794,26 +19084,26 @@ async function trafficRoutes(app, opts) {
18794
19084
  return response;
18795
19085
  });
18796
19086
  function buildSourceDetail(projectId, row, since) {
18797
- const crawlerTotals = app.db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
18798
- and14(
19087
+ const crawlerTotals = app.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
19088
+ and15(
18799
19089
  eq23(crawlerEventsHourly.sourceId, row.id),
18800
19090
  gte2(crawlerEventsHourly.tsHour, since)
18801
19091
  )
18802
19092
  ).get();
18803
- const aiTotals = app.db.select({ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
18804
- and14(
19093
+ const aiTotals = app.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
19094
+ and15(
18805
19095
  eq23(aiReferralEventsHourly.sourceId, row.id),
18806
19096
  gte2(aiReferralEventsHourly.tsHour, since)
18807
19097
  )
18808
19098
  ).get();
18809
- const sampleTotals = app.db.select({ total: sql8`COUNT(*)` }).from(rawEventSamples).where(
18810
- and14(
19099
+ const sampleTotals = app.db.select({ total: sql9`COUNT(*)` }).from(rawEventSamples).where(
19100
+ and15(
18811
19101
  eq23(rawEventSamples.sourceId, row.id),
18812
19102
  gte2(rawEventSamples.ts, since)
18813
19103
  )
18814
19104
  ).get();
18815
19105
  const latestRun = app.db.select().from(runs).where(
18816
- and14(
19106
+ and15(
18817
19107
  eq23(runs.projectId, projectId),
18818
19108
  eq23(runs.kind, RunKinds["traffic-sync"]),
18819
19109
  eq23(runs.sourceId, row.id)
@@ -18907,8 +19197,8 @@ async function trafficRoutes(app, opts) {
18907
19197
  lte2(crawlerEventsHourly.tsHour, untilIso)
18908
19198
  ];
18909
19199
  if (sourceIdParam) crawlerFilters.push(eq23(crawlerEventsHourly.sourceId, sourceIdParam));
18910
- const crawlerWhere = and14(...crawlerFilters);
18911
- const total = app.db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
19200
+ const crawlerWhere = and15(...crawlerFilters);
19201
+ const total = app.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
18912
19202
  crawlerTotal = Number(total?.total ?? 0);
18913
19203
  const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc12(crawlerEventsHourly.tsHour)).limit(limit).all();
18914
19204
  for (const r of rows) {
@@ -18932,8 +19222,8 @@ async function trafficRoutes(app, opts) {
18932
19222
  lte2(aiReferralEventsHourly.tsHour, untilIso)
18933
19223
  ];
18934
19224
  if (sourceIdParam) aiFilters.push(eq23(aiReferralEventsHourly.sourceId, sourceIdParam));
18935
- const aiWhere = and14(...aiFilters);
18936
- const total = app.db.select({ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
19225
+ const aiWhere = and15(...aiFilters);
19226
+ const total = app.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
18937
19227
  aiReferralTotal = Number(total?.total ?? 0);
18938
19228
  const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc12(aiReferralEventsHourly.tsHour)).limit(limit).all();
18939
19229
  for (const r of rows) {
@@ -18966,6 +19256,75 @@ async function trafficRoutes(app, opts) {
18966
19256
  });
18967
19257
  }
18968
19258
 
19259
+ // ../api-routes/src/doctor/checks/agent.ts
19260
+ import fs6 from "fs";
19261
+ import path7 from "path";
19262
+ var REQUIRED_SKILLS = ["canonry", "aero"];
19263
+ var skillsInstalledCheck = {
19264
+ id: "agent.skills.installed",
19265
+ category: CheckCategories.agent,
19266
+ scope: CheckScopes.global,
19267
+ title: "Agent skills installed (~/.claude/skills/)",
19268
+ run: () => {
19269
+ const home = process.env.HOME;
19270
+ if (!home) {
19271
+ return {
19272
+ status: CheckStatuses.skipped,
19273
+ code: "agent.skills.no-home",
19274
+ summary: "Cannot determine $HOME \u2014 skip skills filesystem check.",
19275
+ remediation: null
19276
+ };
19277
+ }
19278
+ const skillsBase = path7.join(home, ".claude", "skills");
19279
+ const installed = [];
19280
+ const missing = [];
19281
+ for (const name of REQUIRED_SKILLS) {
19282
+ const dir = path7.join(skillsBase, name);
19283
+ if (isInstalled(dir)) installed.push(name);
19284
+ else missing.push(name);
19285
+ }
19286
+ const details = {
19287
+ checkedPath: skillsBase,
19288
+ installed,
19289
+ missing
19290
+ };
19291
+ if (missing.length === 0) {
19292
+ return {
19293
+ status: CheckStatuses.ok,
19294
+ code: "agent.skills.installed",
19295
+ summary: `Both canonry and aero skills are installed in ${skillsBase}.`,
19296
+ remediation: null,
19297
+ details
19298
+ };
19299
+ }
19300
+ if (installed.length === 0) {
19301
+ return {
19302
+ status: CheckStatuses.warn,
19303
+ code: "agent.skills.not-installed",
19304
+ summary: "Agent skills are not installed for Claude Code on this machine. Claude sessions on this host will not auto-load canonry/aero reference docs.",
19305
+ remediation: "Run `canonry skills install --dir ~` (or `canonry skills install --user`) to install both skills to ~/.claude/skills/ and ~/.codex/skills/.",
19306
+ details
19307
+ };
19308
+ }
19309
+ return {
19310
+ status: CheckStatuses.warn,
19311
+ code: "agent.skills.partial",
19312
+ summary: `Only ${installed.length} of ${REQUIRED_SKILLS.length} agent skills are installed (${installed.join(", ")}); ${missing.join(", ")} missing.`,
19313
+ remediation: `Run \`canonry skills install ${missing.join(" ")} --dir ~\` to fill the gap.`,
19314
+ details
19315
+ };
19316
+ }
19317
+ };
19318
+ function isInstalled(dir) {
19319
+ try {
19320
+ if (!fs6.existsSync(dir)) return false;
19321
+ return fs6.existsSync(path7.join(dir, "SKILL.md"));
19322
+ } catch {
19323
+ return false;
19324
+ }
19325
+ }
19326
+ var AGENT_CHECKS = [skillsInstalledCheck];
19327
+
18969
19328
  // ../api-routes/src/doctor/checks/bing-auth.ts
18970
19329
  var BING_AUTH_CHECKS = [
18971
19330
  {
@@ -19587,7 +19946,7 @@ var providersConfiguredCheck = {
19587
19946
  var PROVIDERS_CHECKS = [providersConfiguredCheck];
19588
19947
 
19589
19948
  // ../api-routes/src/doctor/checks/traffic-source.ts
19590
- import { and as and15, eq as eq24, gte as gte3, ne as ne3, sql as sql9 } from "drizzle-orm";
19949
+ import { and as and16, eq as eq24, gte as gte3, ne as ne3, sql as sql10 } from "drizzle-orm";
19591
19950
  var RECENT_DATA_WARN_DAYS = 7;
19592
19951
  var RECENT_DATA_FAIL_DAYS = 30;
19593
19952
  function skippedNoProject2() {
@@ -19601,7 +19960,7 @@ function skippedNoProject2() {
19601
19960
  function loadProbes(ctx) {
19602
19961
  if (!ctx.project) return [];
19603
19962
  const rows = ctx.db.select().from(trafficSources).where(
19604
- and15(
19963
+ and16(
19605
19964
  eq24(trafficSources.projectId, ctx.project.id),
19606
19965
  ne3(trafficSources.status, TrafficSourceStatuses.archived)
19607
19966
  )
@@ -19681,16 +20040,16 @@ var recentDataCheck = {
19681
20040
  const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
19682
20041
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
19683
20042
  const recentCrawlers = Number(
19684
- ctx.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
19685
- and15(
20043
+ ctx.db.select({ total: sql10`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
20044
+ and16(
19686
20045
  eq24(crawlerEventsHourly.projectId, ctx.project.id),
19687
20046
  gte3(crawlerEventsHourly.tsHour, warnCutoff)
19688
20047
  )
19689
20048
  ).get()?.total ?? 0
19690
20049
  );
19691
20050
  const recentReferrals = Number(
19692
- ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
19693
- and15(
20051
+ ctx.db.select({ total: sql10`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
20052
+ and16(
19694
20053
  eq24(aiReferralEventsHourly.projectId, ctx.project.id),
19695
20054
  gte3(aiReferralEventsHourly.tsHour, warnCutoff)
19696
20055
  )
@@ -19705,16 +20064,16 @@ var recentDataCheck = {
19705
20064
  };
19706
20065
  }
19707
20066
  const olderCrawlers = Number(
19708
- ctx.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
19709
- and15(
20067
+ ctx.db.select({ total: sql10`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
20068
+ and16(
19710
20069
  eq24(crawlerEventsHourly.projectId, ctx.project.id),
19711
20070
  gte3(crawlerEventsHourly.tsHour, failCutoff)
19712
20071
  )
19713
20072
  ).get()?.total ?? 0
19714
20073
  );
19715
20074
  const olderReferrals = Number(
19716
- ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
19717
- and15(
20075
+ ctx.db.select({ total: sql10`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
20076
+ and16(
19718
20077
  eq24(aiReferralEventsHourly.projectId, ctx.project.id),
19719
20078
  gte3(aiReferralEventsHourly.tsHour, failCutoff)
19720
20079
  )
@@ -19895,7 +20254,8 @@ var ALL_CHECKS = [
19895
20254
  ...BING_AUTH_CHECKS,
19896
20255
  ...GA_AUTH_CHECKS,
19897
20256
  ...PROVIDERS_CHECKS,
19898
- ...TRAFFIC_SOURCE_CHECKS
20257
+ ...TRAFFIC_SOURCE_CHECKS,
20258
+ ...AGENT_CHECKS
19899
20259
  ];
19900
20260
  var CHECK_BY_ID = Object.fromEntries(
19901
20261
  ALL_CHECKS.map((check) => [check.id, check])
@@ -20012,7 +20372,7 @@ async function doctorRoutes(app, opts) {
20012
20372
 
20013
20373
  // ../api-routes/src/discovery/routes.ts
20014
20374
  import crypto21 from "crypto";
20015
- import { and as and16, desc as desc13, eq as eq25, gte as gte4, inArray as inArray8 } from "drizzle-orm";
20375
+ import { and as and17, desc as desc13, eq as eq25, gte as gte4, inArray as inArray8 } from "drizzle-orm";
20016
20376
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
20017
20377
  async function discoveryRoutes(app, opts) {
20018
20378
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -20044,7 +20404,7 @@ async function discoveryRoutes(app, opts) {
20044
20404
  const now = (/* @__PURE__ */ new Date()).toISOString();
20045
20405
  const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
20046
20406
  const decision = app.db.transaction((tx) => {
20047
- const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and16(
20407
+ const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and17(
20048
20408
  eq25(discoverySessions.projectId, project.id),
20049
20409
  eq25(discoverySessions.icpDescription, icpDescription),
20050
20410
  inArray8(discoverySessions.status, [
@@ -20508,6 +20868,7 @@ async function apiRoutes(app, opts) {
20508
20868
  await api.register(projectRoutes, {
20509
20869
  onProjectDeleted: opts.onProjectDeleted,
20510
20870
  onProjectUpserted: opts.onProjectUpserted,
20871
+ onAliasesChanged: opts.onAliasesChanged,
20511
20872
  validProviderNames: opts.providerAdapters?.map((a) => a.name)
20512
20873
  });
20513
20874
  await api.register(queryRoutes, {
@@ -20522,7 +20883,9 @@ async function apiRoutes(app, opts) {
20522
20883
  await api.register(applyRoutes, {
20523
20884
  onScheduleUpdated: opts.onScheduleUpdated,
20524
20885
  onProjectUpserted: opts.onProjectUpserted,
20886
+ onAliasesChanged: opts.onAliasesChanged,
20525
20887
  validProviderNames: opts.providerAdapters?.map((a) => a.name),
20888
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks,
20526
20889
  onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
20527
20890
  opts.googleConnectionStore?.updateConnection(domain, connectionType, {
20528
20891
  propertyId,
@@ -20553,7 +20916,9 @@ async function apiRoutes(app, opts) {
20553
20916
  onScheduleUpdated: opts.onScheduleUpdated,
20554
20917
  validProviderNames: opts.providerAdapters?.map((a) => a.name)
20555
20918
  });
20556
- await api.register(notificationRoutes);
20919
+ await api.register(notificationRoutes, {
20920
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks
20921
+ });
20557
20922
  await api.register(telemetryRoutes, {
20558
20923
  getTelemetryStatus: opts.getTelemetryStatus,
20559
20924
  setTelemetryEnabled: opts.setTelemetryEnabled
@@ -20808,7 +21173,7 @@ function isVertexConfig(config) {
20808
21173
  function resolveModel(config) {
20809
21174
  return config.model || DEFAULT_MODEL;
20810
21175
  }
20811
- function createClient(config) {
21176
+ function createClient2(config) {
20812
21177
  if (isVertexConfig(config)) {
20813
21178
  return new GoogleGenAI({
20814
21179
  vertexai: true,
@@ -20848,7 +21213,7 @@ async function healthcheck(config) {
20848
21213
  if (!validation.ok) return validation;
20849
21214
  try {
20850
21215
  const model = resolveModel(config);
20851
- const client = createClient(config);
21216
+ const client = createClient2(config);
20852
21217
  const result = await withRetry(
20853
21218
  () => client.models.generateContent({
20854
21219
  model,
@@ -20875,7 +21240,7 @@ async function healthcheck(config) {
20875
21240
  async function executeTrackedQuery(input) {
20876
21241
  const model = resolveModel(input.config);
20877
21242
  const prompt = buildPrompt(input.query, input.location);
20878
- const client = createClient(input.config);
21243
+ const client = createClient2(input.config);
20879
21244
  try {
20880
21245
  const result = await withRetry(
20881
21246
  () => client.models.generateContent({
@@ -21040,7 +21405,7 @@ function extractDomainFromUri(uri) {
21040
21405
  }
21041
21406
  async function generateText(prompt, config) {
21042
21407
  const model = resolveModel(config);
21043
- const client = createClient(config);
21408
+ const client = createClient2(config);
21044
21409
  const result = await withRetry(
21045
21410
  () => client.models.generateContent({
21046
21411
  model,
@@ -22195,7 +22560,7 @@ var localAdapter = {
22195
22560
  };
22196
22561
 
22197
22562
  // ../provider-cdp/src/adapter.ts
22198
- import path8 from "path";
22563
+ import path9 from "path";
22199
22564
  import os4 from "os";
22200
22565
 
22201
22566
  // ../provider-cdp/src/connection.ts
@@ -22560,12 +22925,12 @@ function sleep2(ms) {
22560
22925
  }
22561
22926
 
22562
22927
  // ../provider-cdp/src/screenshot.ts
22563
- import fs6 from "fs";
22564
- import path7 from "path";
22928
+ import fs7 from "fs";
22929
+ import path8 from "path";
22565
22930
  async function captureElementScreenshot(client, selector, outputPath) {
22566
- const dir = path7.dirname(outputPath);
22567
- if (!fs6.existsSync(dir)) {
22568
- fs6.mkdirSync(dir, { recursive: true });
22931
+ const dir = path8.dirname(outputPath);
22932
+ if (!fs7.existsSync(dir)) {
22933
+ fs7.mkdirSync(dir, { recursive: true });
22569
22934
  }
22570
22935
  let clip;
22571
22936
  try {
@@ -22599,7 +22964,7 @@ async function captureElementScreenshot(client, selector, outputPath) {
22599
22964
  }
22600
22965
  const { data } = await client.Page.captureScreenshot(screenshotParams);
22601
22966
  const buffer = Buffer.from(data, "base64");
22602
- fs6.writeFileSync(outputPath, buffer);
22967
+ fs7.writeFileSync(outputPath, buffer);
22603
22968
  return outputPath;
22604
22969
  }
22605
22970
 
@@ -22660,7 +23025,7 @@ function getConnection(config) {
22660
23025
  return conn;
22661
23026
  }
22662
23027
  function getScreenshotDir2() {
22663
- return path8.join(os4.homedir(), ".canonry", "screenshots");
23028
+ return path9.join(os4.homedir(), ".canonry", "screenshots");
22664
23029
  }
22665
23030
  var cdpChatgptAdapter = {
22666
23031
  name: "cdp:chatgpt",
@@ -22724,7 +23089,7 @@ var cdpChatgptAdapter = {
22724
23089
  const answerText = await target.extractAnswer(client);
22725
23090
  const groundingSources = await target.extractCitations(client);
22726
23091
  const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
22727
- const screenshotPath = path8.join(getScreenshotDir2(), `${screenshotId}.png`);
23092
+ const screenshotPath = path9.join(getScreenshotDir2(), `${screenshotId}.png`);
22728
23093
  let capturedScreenshotPath;
22729
23094
  try {
22730
23095
  capturedScreenshotPath = await captureElementScreenshot(
@@ -23357,10 +23722,10 @@ function removeWordpressConnection(config, projectName) {
23357
23722
 
23358
23723
  // src/job-runner.ts
23359
23724
  import crypto24 from "crypto";
23360
- import fs7 from "fs";
23361
- import path9 from "path";
23725
+ import fs8 from "fs";
23726
+ import path10 from "path";
23362
23727
  import os5 from "os";
23363
- import { and as and17, eq as eq27, inArray as inArray9, sql as sql10 } from "drizzle-orm";
23728
+ import { and as and18, eq as eq27, inArray as inArray9, sql as sql11 } from "drizzle-orm";
23364
23729
 
23365
23730
  // src/run-telemetry.ts
23366
23731
  import crypto23 from "crypto";
@@ -23467,11 +23832,15 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
23467
23832
  function escapeRegExp5(value) {
23468
23833
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23469
23834
  }
23470
- function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains) {
23835
+ function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains, ownBrandNames = []) {
23471
23836
  if (!answerText || answerText.length < 20) return [];
23472
23837
  const ownBrandKeys = new Set(
23473
23838
  ownDomains.flatMap((domain) => collectBrandKeysFromDomain(domain))
23474
23839
  );
23840
+ for (const name of ownBrandNames) {
23841
+ const key = brandKeyFromText(name);
23842
+ if (key.length >= 4) ownBrandKeys.add(key);
23843
+ }
23475
23844
  const knownCompetitorKeys = new Set(
23476
23845
  [...citedDomains, ...competitorDomains].flatMap((domain) => collectBrandKeysFromDomain(domain)).filter((key) => !ownBrandKeys.has(key))
23477
23846
  );
@@ -23739,7 +24108,7 @@ var JobRunner = class {
23739
24108
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
23740
24109
  }
23741
24110
  if (existingRun.status === "queued") {
23742
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and17(eq27(runs.id, runId), eq27(runs.status, "queued"))).run();
24111
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and18(eq27(runs.id, runId), eq27(runs.status, "queued"))).run();
23743
24112
  }
23744
24113
  this.throwIfRunCancelled(runId);
23745
24114
  const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
@@ -23764,13 +24133,17 @@ var JobRunner = class {
23764
24133
  }
23765
24134
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
23766
24135
  const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
23767
- projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and17(eq27(queries.projectId, projectId), inArray9(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
24136
+ projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and18(eq27(queries.projectId, projectId), inArray9(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
23768
24137
  const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
23769
24138
  const competitorDomains = projectCompetitors.map((c) => c.domain);
23770
24139
  const allDomains = effectiveDomains({
23771
24140
  canonicalDomain: project.canonicalDomain,
23772
24141
  ownedDomains: parseJsonColumn(project.ownedDomains, [])
23773
24142
  });
24143
+ const allBrandNames = effectiveBrandNames({
24144
+ displayName: project.displayName,
24145
+ aliases: parseJsonColumn(project.aliases, [])
24146
+ });
23774
24147
  const executionContext = {
23775
24148
  providerCount: activeProviders.length,
23776
24149
  providers: activeProviders.map((provider) => provider.adapter.name),
@@ -23831,7 +24204,7 @@ var JobRunner = class {
23831
24204
  const citationState = determineCitationState(normalized, allDomains);
23832
24205
  const answerMentioned = determineAnswerMentioned(
23833
24206
  normalized.answerText,
23834
- project.displayName,
24207
+ allBrandNames,
23835
24208
  allDomains
23836
24209
  );
23837
24210
  const overlap = computeCompetitorOverlap(normalized, competitorDomains);
@@ -23839,15 +24212,16 @@ var JobRunner = class {
23839
24212
  normalized.answerText,
23840
24213
  allDomains,
23841
24214
  normalized.citedDomains,
23842
- competitorDomains
24215
+ competitorDomains,
24216
+ allBrandNames
23843
24217
  );
23844
24218
  let screenshotRelPath = null;
23845
- if (raw.screenshotPath && fs7.existsSync(raw.screenshotPath)) {
24219
+ if (raw.screenshotPath && fs8.existsSync(raw.screenshotPath)) {
23846
24220
  const snapshotId = crypto24.randomUUID();
23847
- const screenshotDir = path9.join(os5.homedir(), ".canonry", "screenshots", runId);
23848
- if (!fs7.existsSync(screenshotDir)) fs7.mkdirSync(screenshotDir, { recursive: true });
23849
- const destPath = path9.join(screenshotDir, `${snapshotId}.png`);
23850
- fs7.renameSync(raw.screenshotPath, destPath);
24221
+ const screenshotDir = path10.join(os5.homedir(), ".canonry", "screenshots", runId);
24222
+ if (!fs8.existsSync(screenshotDir)) fs8.mkdirSync(screenshotDir, { recursive: true });
24223
+ const destPath = path10.join(screenshotDir, `${snapshotId}.png`);
24224
+ fs8.renameSync(raw.screenshotPath, destPath);
23851
24225
  screenshotRelPath = `${runId}/${snapshotId}.png`;
23852
24226
  this.db.insert(querySnapshots).values({
23853
24227
  id: snapshotId,
@@ -24032,7 +24406,7 @@ var JobRunner = class {
24032
24406
  updatedAt: now
24033
24407
  }).onConflictDoUpdate({
24034
24408
  target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
24035
- set: { count: sql10`${usageCounters.count} + ${count}`, updatedAt: now }
24409
+ set: { count: sql11`${usageCounters.count} + ${count}`, updatedAt: now }
24036
24410
  }).run();
24037
24411
  }
24038
24412
  flushProviderUsage(projectId, providerDispatchCounts) {
@@ -24102,7 +24476,7 @@ function buildPhases(input) {
24102
24476
 
24103
24477
  // src/gsc-sync.ts
24104
24478
  import crypto25 from "crypto";
24105
- import { eq as eq28, and as and18, sql as sql11 } from "drizzle-orm";
24479
+ import { eq as eq28, and as and19, sql as sql12 } from "drizzle-orm";
24106
24480
  var log2 = createLogger("GscSync");
24107
24481
  function formatDate3(d) {
24108
24482
  return d.toISOString().split("T")[0];
@@ -24154,10 +24528,10 @@ async function executeGscSync(db, runId, projectId, opts) {
24154
24528
  });
24155
24529
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
24156
24530
  db.delete(gscSearchData).where(
24157
- and18(
24531
+ and19(
24158
24532
  eq28(gscSearchData.projectId, projectId),
24159
- sql11`${gscSearchData.date} >= ${startDate}`,
24160
- sql11`${gscSearchData.date} <= ${endDate}`
24533
+ sql12`${gscSearchData.date} >= ${startDate}`,
24534
+ sql12`${gscSearchData.date} <= ${endDate}`
24161
24535
  )
24162
24536
  ).run();
24163
24537
  const batchSize = 500;
@@ -24243,7 +24617,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24243
24617
  }
24244
24618
  }
24245
24619
  const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
24246
- db.delete(gscCoverageSnapshots).where(and18(eq28(gscCoverageSnapshots.projectId, projectId), eq28(gscCoverageSnapshots.date, snapshotDate))).run();
24620
+ db.delete(gscCoverageSnapshots).where(and19(eq28(gscCoverageSnapshots.projectId, projectId), eq28(gscCoverageSnapshots.date, snapshotDate))).run();
24247
24621
  db.insert(gscCoverageSnapshots).values({
24248
24622
  id: crypto25.randomUUID(),
24249
24623
  projectId,
@@ -24266,7 +24640,7 @@ async function executeGscSync(db, runId, projectId, opts) {
24266
24640
 
24267
24641
  // src/gsc-inspect-sitemap.ts
24268
24642
  import crypto26 from "crypto";
24269
- import { eq as eq29, and as and19 } from "drizzle-orm";
24643
+ import { eq as eq29, and as and20 } from "drizzle-orm";
24270
24644
 
24271
24645
  // src/sitemap-parser.ts
24272
24646
  var log3 = createLogger("SitemapParser");
@@ -24482,7 +24856,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
24482
24856
  }
24483
24857
  }
24484
24858
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
24485
- db.delete(gscCoverageSnapshots).where(and19(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
24859
+ db.delete(gscCoverageSnapshots).where(and20(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
24486
24860
  db.insert(gscCoverageSnapshots).values({
24487
24861
  id: crypto26.randomUUID(),
24488
24862
  projectId,
@@ -24692,8 +25066,8 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
24692
25066
 
24693
25067
  // src/commoncrawl-sync.ts
24694
25068
  import crypto28 from "crypto";
24695
- import path10 from "path";
24696
- import { and as and20, eq as eq31, sql as sql12 } from "drizzle-orm";
25069
+ import path11 from "path";
25070
+ import { and as and21, eq as eq31, sql as sql13 } from "drizzle-orm";
24697
25071
  var log6 = createLogger("CommonCrawlSync");
24698
25072
  var INSERT_CHUNK_SIZE = 1e4;
24699
25073
  function defaultDeps() {
@@ -24721,9 +25095,9 @@ async function executeReleaseSync(db, syncId, opts) {
24721
25095
  error: null
24722
25096
  }).where(eq31(ccReleaseSyncs.id, syncId)).run();
24723
25097
  const paths = ccReleasePaths(release);
24724
- const releaseCacheDir = path10.join(deps.cacheDir, release);
24725
- const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
24726
- const edgesPath = path10.join(releaseCacheDir, paths.edgesFilename);
25098
+ const releaseCacheDir = path11.join(deps.cacheDir, release);
25099
+ const vertexPath = path11.join(releaseCacheDir, paths.vertexFilename);
25100
+ const edgesPath = path11.join(releaseCacheDir, paths.edgesFilename);
24727
25101
  const [vertex, edges] = await Promise.all([
24728
25102
  deps.downloadFile({ url: paths.vertexUrl, destPath: vertexPath }),
24729
25103
  deps.downloadFile({ url: paths.edgesUrl, destPath: edgesPath })
@@ -24883,8 +25257,8 @@ function computeSummary(rows) {
24883
25257
 
24884
25258
  // src/backlink-extract.ts
24885
25259
  import crypto29 from "crypto";
24886
- import fs8 from "fs";
24887
- import { and as and21, desc as desc15, eq as eq32 } from "drizzle-orm";
25260
+ import fs9 from "fs";
25261
+ import { and as and22, desc as desc15, eq as eq32 } from "drizzle-orm";
24888
25262
  var log7 = createLogger("BacklinkExtract");
24889
25263
  function defaultDeps2() {
24890
25264
  return {
@@ -24912,7 +25286,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
24912
25286
  if (!sync.vertexPath || !sync.edgesPath) {
24913
25287
  throw new Error(`Release ${sync.release} is missing cached file paths`);
24914
25288
  }
24915
- if (!fs8.existsSync(sync.vertexPath) || !fs8.existsSync(sync.edgesPath)) {
25289
+ if (!fs9.existsSync(sync.vertexPath) || !fs9.existsSync(sync.edgesPath)) {
24916
25290
  throw new Error(
24917
25291
  `Cache for release ${sync.release} is missing from disk (expected at ${sync.vertexPath}). The sync record exists in the database, but the ~16 GB dump was deleted or never present on this machine. Re-sync this release from the Backlinks admin page to restore the cache.`
24918
25292
  );
@@ -24930,7 +25304,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
24930
25304
  const targetDomain = project.canonicalDomain;
24931
25305
  db.transaction((tx) => {
24932
25306
  tx.delete(backlinkDomains).where(
24933
- and21(eq32(backlinkDomains.projectId, projectId), eq32(backlinkDomains.release, release))
25307
+ and22(eq32(backlinkDomains.projectId, projectId), eq32(backlinkDomains.release, release))
24934
25308
  ).run();
24935
25309
  if (rows.length > 0) {
24936
25310
  const values = rows.map((r) => ({
@@ -25001,9 +25375,10 @@ function computeSummary2(rows) {
25001
25375
 
25002
25376
  // src/discovery-run.ts
25003
25377
  import crypto30 from "crypto";
25004
- import { eq as eq33 } from "drizzle-orm";
25378
+ import { and as and23, eq as eq33 } from "drizzle-orm";
25005
25379
  var log8 = createLogger("DiscoveryRun");
25006
25380
  var DEFAULT_SEED_COUNT = 30;
25381
+ var QUERIES_PER_INTENT_BUCKET = 6;
25007
25382
  async function executeDiscoveryRun(opts) {
25008
25383
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
25009
25384
  opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq33(runs.id, opts.runId)).run();
@@ -25208,6 +25583,7 @@ function buildLocationConstraint(locations) {
25208
25583
  }
25209
25584
  function buildSeedPrompt(input) {
25210
25585
  const locationConstraint = buildLocationConstraint(input.locations ?? []);
25586
+ const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
25211
25587
  return [
25212
25588
  "You are an AEO (Answer Engine Optimization) analyst expanding a tracked-query basket for a customer.",
25213
25589
  "",
@@ -25215,14 +25591,15 @@ function buildSeedPrompt(input) {
25215
25591
  `ICP: ${input.icpDescription}`,
25216
25592
  ...locationConstraint.length > 0 ? ["", ...locationConstraint] : [],
25217
25593
  "",
25218
- "Brainstorm a wide set of queries a member of this ICP would type into an AI answer engine (Gemini, ChatGPT, Perplexity) when they are about to make a decision in this space. Aim for 30+ candidates covering:",
25219
- ' - Comparison queries ("best X for Y")',
25220
- " - Specific feature / capability queries",
25221
- " - Pricing / vendor-shortlist queries",
25222
- " - Workflow / how-to queries",
25223
- " - Adjacent jobs-to-be-done queries",
25594
+ "Brainstorm queries a member of this ICP would type into an AI answer engine (Gemini, ChatGPT, Perplexity). Generate candidates across the five intent buckets below \u2014 these are SEMANTICALLY DISTINCT search intents, not stylistic variants. A query should fit one bucket cleanly. Diversity across buckets is the point: it keeps the list from collapsing into near-synonyms of a single intent.",
25224
25595
  "",
25225
- "Return ONE query per line. Plain text only \u2014 no numbering, bullets, quotes, or commentary."
25596
+ ' 1. Informational \u2014 the searcher wants to understand a concept, market, or problem. Templates: "what is X", "how does X work", "X explained", "why X matters".',
25597
+ ` 2. Commercial \u2014 the searcher is researching category leaders before a purchase. Templates: "best X for Y", "top X ${currentYear}", "leading X providers", "X for [use case]".`,
25598
+ ' 3. Navigational \u2014 the searcher is looking for a specific brand, place, or directory. Templates: "X near me", "X reviews", "X website", "X directory".',
25599
+ ' 4. Comparative \u2014 the searcher is weighing named alternatives head-to-head. Templates: "X vs Y", "X or Y for Z", "alternatives to X".',
25600
+ ' 5. Transactional \u2014 the searcher is ready to act on a purchase or booking. Templates: "book X", "X pricing", "X discount code", "buy X online".',
25601
+ "",
25602
+ `Generate EXACTLY ${QUERIES_PER_INTENT_BUCKET} queries per bucket \u2014 ${QUERIES_PER_INTENT_BUCKET * 5} total. Return ONE query per line. Plain text only \u2014 no numbering, bullets, quotes, bucket labels, or commentary.`
25226
25603
  ].join("\n");
25227
25604
  }
25228
25605
  function parseQueryLines(text, max) {
@@ -25257,33 +25634,40 @@ function writeDiscoveryInsight(db, input) {
25257
25634
  aspirational: buckets.aspirational,
25258
25635
  totalProbes
25259
25636
  });
25260
- db.insert(insights).values({
25261
- id: crypto30.randomUUID(),
25262
- projectId: input.projectId,
25263
- runId: input.runId,
25264
- type: "discovery.basket-divergence",
25265
- severity,
25266
- title,
25267
- // query/provider fields don't fit the visibility-snapshot model for a
25268
- // session-level insight. Use the session marker so the
25269
- // (query, provider) index stays distinct across sessions; PR 5 will
25270
- // formalize a session-scoped insight subtype.
25271
- query: `discovery:${input.sessionId}`,
25272
- provider: input.seedProvider,
25273
- recommendation: JSON.stringify({
25274
- action: "review-discovered-basket",
25275
- summary: `Run \`canonry discover show ${input.sessionId} --format json\` to inspect the per-query breakdown, then \`canonry discover promote <project> ${input.sessionId}\` to merge cited + aspirational findings into the project.`,
25276
- bucketCounts: buckets,
25277
- topCompetitors
25278
- }),
25279
- cause: JSON.stringify({
25280
- sessionId: input.sessionId,
25281
- totalProbes,
25282
- seedProvider: input.seedProvider
25283
- }),
25284
- dismissed: false,
25285
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
25286
- }).run();
25637
+ db.transaction((tx) => {
25638
+ tx.update(insights).set({ dismissed: true }).where(and23(
25639
+ eq33(insights.projectId, input.projectId),
25640
+ eq33(insights.type, "discovery.basket-divergence"),
25641
+ eq33(insights.dismissed, false)
25642
+ )).run();
25643
+ tx.insert(insights).values({
25644
+ id: crypto30.randomUUID(),
25645
+ projectId: input.projectId,
25646
+ runId: input.runId,
25647
+ type: "discovery.basket-divergence",
25648
+ severity,
25649
+ title,
25650
+ // query/provider fields don't fit the visibility-snapshot model for
25651
+ // a session-level insight. Use the session marker so the
25652
+ // (query, provider) index stays distinct across sessions; PR 5 will
25653
+ // formalize a session-scoped insight subtype.
25654
+ query: `discovery:${input.sessionId}`,
25655
+ provider: input.seedProvider,
25656
+ recommendation: JSON.stringify({
25657
+ action: "review-discovered-basket",
25658
+ summary: `Run \`canonry discover show ${input.sessionId} --format json\` to inspect the per-query breakdown, then \`canonry discover promote <project> ${input.sessionId}\` to merge cited + aspirational findings into the project.`,
25659
+ bucketCounts: buckets,
25660
+ topCompetitors
25661
+ }),
25662
+ cause: JSON.stringify({
25663
+ sessionId: input.sessionId,
25664
+ totalProbes,
25665
+ seedProvider: input.seedProvider
25666
+ }),
25667
+ dismissed: false,
25668
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
25669
+ }).run();
25670
+ });
25287
25671
  }
25288
25672
  function buildDiscoveryInsightTitle(input) {
25289
25673
  const parts = [];
@@ -25294,6 +25678,525 @@ function buildDiscoveryInsightTitle(input) {
25294
25678
  return parts.join(" \u2022 ");
25295
25679
  }
25296
25680
 
25681
+ // src/commands/backfill.ts
25682
+ import { and as and24, eq as eq34, inArray as inArray10 } from "drizzle-orm";
25683
+ var SNAPSHOT_BATCH_SIZE = 500;
25684
+ async function backfillAnswerVisibilityCommand(opts) {
25685
+ const config = loadConfig();
25686
+ const db = createClient(config.database);
25687
+ migrate(db);
25688
+ const projectFilter = opts?.project?.trim();
25689
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq34(projects.name, projectFilter)).all() : db.select().from(projects).all();
25690
+ let examined = 0;
25691
+ let updated = 0;
25692
+ let mentioned = 0;
25693
+ let reparsed = 0;
25694
+ let providerErrors = 0;
25695
+ if (scopedProjects.length > 0) {
25696
+ const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and24(
25697
+ eq34(runs.kind, RunKinds["answer-visibility"]),
25698
+ inArray10(runs.projectId, scopedProjects.map((project) => project.id))
25699
+ )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq34(runs.kind, RunKinds["answer-visibility"])).all();
25700
+ const runIdsByProject = /* @__PURE__ */ new Map();
25701
+ for (const run of runRows) {
25702
+ const existing = runIdsByProject.get(run.projectId);
25703
+ if (existing) existing.push(run.id);
25704
+ else runIdsByProject.set(run.projectId, [run.id]);
25705
+ }
25706
+ for (const project of scopedProjects) {
25707
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, project.id)).all().map((row) => row.domain);
25708
+ const runIds = runIdsByProject.get(project.id) ?? [];
25709
+ if (runIds.length === 0) continue;
25710
+ const projectDomains = effectiveDomains({
25711
+ canonicalDomain: project.canonicalDomain,
25712
+ ownedDomains: parseJsonColumn(project.ownedDomains, [])
25713
+ });
25714
+ const projectBrandNames = effectiveBrandNames({
25715
+ displayName: project.displayName,
25716
+ aliases: parseJsonColumn(project.aliases, [])
25717
+ });
25718
+ for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
25719
+ const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
25720
+ const snapshotRows = db.select({
25721
+ id: querySnapshots.id,
25722
+ provider: querySnapshots.provider,
25723
+ citationState: querySnapshots.citationState,
25724
+ answerMentioned: querySnapshots.answerMentioned,
25725
+ answerText: querySnapshots.answerText,
25726
+ citedDomains: querySnapshots.citedDomains,
25727
+ competitorOverlap: querySnapshots.competitorOverlap,
25728
+ recommendedCompetitors: querySnapshots.recommendedCompetitors,
25729
+ rawResponse: querySnapshots.rawResponse
25730
+ }).from(querySnapshots).where(inArray10(querySnapshots.runId, batchRunIds)).all();
25731
+ const pendingUpdates = [];
25732
+ for (const snapshot of snapshotRows) {
25733
+ examined++;
25734
+ const reparsedResult = reparseProviderSnapshot(snapshot.provider, snapshot.rawResponse);
25735
+ if (reparsedResult) reparsed++;
25736
+ if (reparsedResult?.providerError) providerErrors++;
25737
+ const answerText = reparsedResult?.answerText ?? snapshot.answerText ?? "";
25738
+ const nextValue = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
25739
+ if (nextValue) mentioned++;
25740
+ const nextPatch = {};
25741
+ if (snapshot.answerMentioned !== nextValue) {
25742
+ nextPatch.answerMentioned = nextValue;
25743
+ }
25744
+ if ((snapshot.answerText ?? "") !== answerText) {
25745
+ nextPatch.answerText = answerText;
25746
+ }
25747
+ if (reparsedResult) {
25748
+ const normalized = {
25749
+ provider: snapshot.provider,
25750
+ answerText,
25751
+ citedDomains: reparsedResult.citedDomains,
25752
+ groundingSources: reparsedResult.groundingSources,
25753
+ searchQueries: reparsedResult.searchQueries
25754
+ };
25755
+ const nextCitationState = determineCitationState(normalized, projectDomains);
25756
+ const nextCitedDomains = JSON.stringify(reparsedResult.citedDomains);
25757
+ const nextCompetitorOverlap = JSON.stringify(
25758
+ computeCompetitorOverlap(normalized, competitorDomains)
25759
+ );
25760
+ const nextRecommendedCompetitors = JSON.stringify(
25761
+ extractRecommendedCompetitors(
25762
+ normalized.answerText,
25763
+ projectDomains,
25764
+ normalized.citedDomains,
25765
+ competitorDomains,
25766
+ projectBrandNames
25767
+ )
25768
+ );
25769
+ const nextRawResponse = stringifyStoredSnapshotEnvelope(
25770
+ snapshot.rawResponse,
25771
+ reparsedResult
25772
+ );
25773
+ if (snapshot.citationState !== nextCitationState) {
25774
+ nextPatch.citationState = nextCitationState;
25775
+ }
25776
+ if (snapshot.citedDomains !== nextCitedDomains) {
25777
+ nextPatch.citedDomains = nextCitedDomains;
25778
+ }
25779
+ if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
25780
+ nextPatch.competitorOverlap = nextCompetitorOverlap;
25781
+ }
25782
+ if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
25783
+ nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
25784
+ }
25785
+ if (snapshot.rawResponse !== nextRawResponse) {
25786
+ nextPatch.rawResponse = nextRawResponse;
25787
+ }
25788
+ }
25789
+ if (Object.keys(nextPatch).length > 0) {
25790
+ pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
25791
+ }
25792
+ }
25793
+ if (pendingUpdates.length > 0) {
25794
+ db.transaction((tx) => {
25795
+ for (const update of pendingUpdates) {
25796
+ tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
25797
+ }
25798
+ });
25799
+ updated += pendingUpdates.length;
25800
+ }
25801
+ }
25802
+ }
25803
+ }
25804
+ const result = {
25805
+ project: projectFilter ?? null,
25806
+ projects: scopedProjects.length,
25807
+ examined,
25808
+ updated,
25809
+ mentioned,
25810
+ reparsed,
25811
+ providerErrors
25812
+ };
25813
+ if (opts?.format === "json") {
25814
+ console.log(JSON.stringify(result, null, 2));
25815
+ return;
25816
+ }
25817
+ console.log("Answer visibility backfill complete.\n");
25818
+ if (projectFilter) {
25819
+ console.log(` Project: ${projectFilter}`);
25820
+ }
25821
+ console.log(` Projects: ${scopedProjects.length}`);
25822
+ console.log(` Examined: ${examined}`);
25823
+ console.log(` Updated: ${updated}`);
25824
+ console.log(` Mentioned: ${mentioned}`);
25825
+ console.log(` Reparsed: ${reparsed}`);
25826
+ console.log(` Errors: ${providerErrors}`);
25827
+ }
25828
+ function backfillNormalizedPaths(db, opts) {
25829
+ const baseConditions = [];
25830
+ if (opts?.projectId) {
25831
+ baseConditions.push(eq34(gaTrafficSnapshots.projectId, opts.projectId));
25832
+ }
25833
+ const rows = db.select({
25834
+ id: gaTrafficSnapshots.id,
25835
+ landingPage: gaTrafficSnapshots.landingPage,
25836
+ landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
25837
+ }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and24(...baseConditions) : void 0).all();
25838
+ let updated = 0;
25839
+ let unchanged = 0;
25840
+ if (rows.length > 0) {
25841
+ db.transaction((tx) => {
25842
+ for (const row of rows) {
25843
+ const next = normalizeUrlPath(row.landingPage);
25844
+ if (next === null) {
25845
+ unchanged++;
25846
+ continue;
25847
+ }
25848
+ if (row.landingPageNormalized === next) {
25849
+ unchanged++;
25850
+ continue;
25851
+ }
25852
+ tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq34(gaTrafficSnapshots.id, row.id)).run();
25853
+ updated++;
25854
+ }
25855
+ });
25856
+ }
25857
+ return { examined: rows.length, updated, unchanged };
25858
+ }
25859
+ async function backfillNormalizedPathsCommand(opts) {
25860
+ const config = loadConfig();
25861
+ const db = createClient(config.database);
25862
+ migrate(db);
25863
+ const projectFilter = opts?.project?.trim();
25864
+ let projectId;
25865
+ if (projectFilter) {
25866
+ const project = db.select({ id: projects.id }).from(projects).where(eq34(projects.name, projectFilter)).get();
25867
+ if (!project) {
25868
+ const result2 = {
25869
+ project: projectFilter,
25870
+ examined: 0,
25871
+ updated: 0,
25872
+ unchanged: 0
25873
+ };
25874
+ if (opts?.format === "json") {
25875
+ console.log(JSON.stringify(result2, null, 2));
25876
+ return;
25877
+ }
25878
+ console.log(`Backfill normalized-paths: project "${projectFilter}" not found.`);
25879
+ return;
25880
+ }
25881
+ projectId = project.id;
25882
+ }
25883
+ const { examined, updated, unchanged } = backfillNormalizedPaths(db, { projectId });
25884
+ const result = {
25885
+ project: projectFilter ?? null,
25886
+ examined,
25887
+ updated,
25888
+ unchanged
25889
+ };
25890
+ if (opts?.format === "json") {
25891
+ console.log(JSON.stringify(result, null, 2));
25892
+ return;
25893
+ }
25894
+ console.log("Normalized-path backfill complete.\n");
25895
+ if (projectFilter) console.log(` Project: ${projectFilter}`);
25896
+ console.log(` Examined: ${examined}`);
25897
+ console.log(` Updated: ${updated}`);
25898
+ console.log(` Unchanged: ${unchanged}`);
25899
+ }
25900
+ function backfillAiReferralPaths(db, opts) {
25901
+ const baseConditions = [];
25902
+ if (opts?.projectId) {
25903
+ baseConditions.push(eq34(gaAiReferrals.projectId, opts.projectId));
25904
+ }
25905
+ const rows = db.select({
25906
+ id: gaAiReferrals.id,
25907
+ landingPage: gaAiReferrals.landingPage,
25908
+ landingPageNormalized: gaAiReferrals.landingPageNormalized
25909
+ }).from(gaAiReferrals).where(baseConditions.length > 0 ? and24(...baseConditions) : void 0).all();
25910
+ let updated = 0;
25911
+ let unchanged = 0;
25912
+ if (rows.length > 0) {
25913
+ db.transaction((tx) => {
25914
+ for (const row of rows) {
25915
+ const next = normalizeUrlPath(row.landingPage);
25916
+ if (next === null) {
25917
+ unchanged++;
25918
+ continue;
25919
+ }
25920
+ if (row.landingPageNormalized === next) {
25921
+ unchanged++;
25922
+ continue;
25923
+ }
25924
+ tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq34(gaAiReferrals.id, row.id)).run();
25925
+ updated++;
25926
+ }
25927
+ });
25928
+ }
25929
+ return { examined: rows.length, updated, unchanged };
25930
+ }
25931
+ async function backfillAiReferralPathsCommand(opts) {
25932
+ const config = loadConfig();
25933
+ const db = createClient(config.database);
25934
+ migrate(db);
25935
+ const projectFilter = opts?.project?.trim();
25936
+ let projectId;
25937
+ if (projectFilter) {
25938
+ const project = db.select({ id: projects.id }).from(projects).where(eq34(projects.name, projectFilter)).get();
25939
+ if (!project) {
25940
+ const result2 = {
25941
+ project: projectFilter,
25942
+ examined: 0,
25943
+ updated: 0,
25944
+ unchanged: 0
25945
+ };
25946
+ if (opts?.format === "json") {
25947
+ console.log(JSON.stringify(result2, null, 2));
25948
+ return;
25949
+ }
25950
+ console.log(`Backfill ai-referral-paths: project "${projectFilter}" not found.`);
25951
+ return;
25952
+ }
25953
+ projectId = project.id;
25954
+ }
25955
+ const { examined, updated, unchanged } = backfillAiReferralPaths(db, { projectId });
25956
+ const result = {
25957
+ project: projectFilter ?? null,
25958
+ examined,
25959
+ updated,
25960
+ unchanged
25961
+ };
25962
+ if (opts?.format === "json") {
25963
+ console.log(JSON.stringify(result, null, 2));
25964
+ return;
25965
+ }
25966
+ console.log("AI referral landing-page backfill complete.\n");
25967
+ if (projectFilter) console.log(` Project: ${projectFilter}`);
25968
+ console.log(` Examined: ${examined}`);
25969
+ console.log(` Updated: ${updated}`);
25970
+ console.log(` Unchanged: ${unchanged}`);
25971
+ }
25972
+ function backfillProjectAnswerMentions(db, projectId) {
25973
+ const project = db.select().from(projects).where(eq34(projects.id, projectId)).get();
25974
+ if (!project) return { examined: 0, updated: 0, mentioned: 0 };
25975
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, projectId)).all().map((row) => row.domain);
25976
+ const runRows = db.select({ id: runs.id }).from(runs).where(and24(eq34(runs.kind, RunKinds["answer-visibility"]), eq34(runs.projectId, projectId))).all();
25977
+ const runIds = runRows.map((r) => r.id);
25978
+ let examined = 0;
25979
+ let updated = 0;
25980
+ let mentioned = 0;
25981
+ if (runIds.length === 0) return { examined, updated, mentioned };
25982
+ const projectDomains = effectiveDomains({
25983
+ canonicalDomain: project.canonicalDomain,
25984
+ ownedDomains: parseJsonColumn(project.ownedDomains, [])
25985
+ });
25986
+ const projectBrandNames = effectiveBrandNames({
25987
+ displayName: project.displayName,
25988
+ aliases: parseJsonColumn(project.aliases, [])
25989
+ });
25990
+ for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
25991
+ const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
25992
+ const snapshotRows = db.select({
25993
+ id: querySnapshots.id,
25994
+ provider: querySnapshots.provider,
25995
+ answerMentioned: querySnapshots.answerMentioned,
25996
+ answerText: querySnapshots.answerText,
25997
+ citedDomains: querySnapshots.citedDomains,
25998
+ competitorOverlap: querySnapshots.competitorOverlap,
25999
+ recommendedCompetitors: querySnapshots.recommendedCompetitors,
26000
+ rawResponse: querySnapshots.rawResponse
26001
+ }).from(querySnapshots).where(inArray10(querySnapshots.runId, batchRunIds)).all();
26002
+ const pendingUpdates = [];
26003
+ for (const snapshot of snapshotRows) {
26004
+ examined++;
26005
+ const answerText = snapshot.answerText ?? "";
26006
+ const nextAnswerMentioned = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
26007
+ if (nextAnswerMentioned) mentioned++;
26008
+ const citedDomains = parseJsonColumn(snapshot.citedDomains, []);
26009
+ const groundingSources = readStoredGroundingSources(snapshot.rawResponse);
26010
+ const normalized = {
26011
+ provider: snapshot.provider,
26012
+ answerText,
26013
+ citedDomains,
26014
+ groundingSources,
26015
+ searchQueries: []
26016
+ };
26017
+ const nextCompetitorOverlap = JSON.stringify(
26018
+ computeCompetitorOverlap(normalized, competitorDomains)
26019
+ );
26020
+ const nextRecommendedCompetitors = JSON.stringify(
26021
+ extractRecommendedCompetitors(
26022
+ answerText,
26023
+ projectDomains,
26024
+ citedDomains,
26025
+ competitorDomains,
26026
+ projectBrandNames
26027
+ )
26028
+ );
26029
+ const nextPatch = {};
26030
+ if (snapshot.answerMentioned !== nextAnswerMentioned) {
26031
+ nextPatch.answerMentioned = nextAnswerMentioned;
26032
+ }
26033
+ if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
26034
+ nextPatch.competitorOverlap = nextCompetitorOverlap;
26035
+ }
26036
+ if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
26037
+ nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
26038
+ }
26039
+ if (Object.keys(nextPatch).length > 0) {
26040
+ pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
26041
+ }
26042
+ }
26043
+ if (pendingUpdates.length > 0) {
26044
+ db.transaction((tx) => {
26045
+ for (const update of pendingUpdates) {
26046
+ tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
26047
+ }
26048
+ });
26049
+ updated += pendingUpdates.length;
26050
+ }
26051
+ }
26052
+ return { examined, updated, mentioned };
26053
+ }
26054
+ async function backfillAnswerMentionsCommand(opts) {
26055
+ const config = loadConfig();
26056
+ const db = createClient(config.database);
26057
+ migrate(db);
26058
+ const projectFilter = opts?.project?.trim();
26059
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq34(projects.name, projectFilter)).all() : db.select().from(projects).all();
26060
+ let examined = 0;
26061
+ let updated = 0;
26062
+ let mentioned = 0;
26063
+ for (const project of scopedProjects) {
26064
+ const result2 = backfillProjectAnswerMentions(db, project.id);
26065
+ examined += result2.examined;
26066
+ updated += result2.updated;
26067
+ mentioned += result2.mentioned;
26068
+ }
26069
+ const result = {
26070
+ project: projectFilter ?? null,
26071
+ projects: scopedProjects.length,
26072
+ examined,
26073
+ updated,
26074
+ mentioned
26075
+ };
26076
+ if (opts?.format === "json") {
26077
+ console.log(JSON.stringify(result, null, 2));
26078
+ return;
26079
+ }
26080
+ console.log("Answer mentions backfill complete.\n");
26081
+ if (projectFilter) console.log(` Project: ${projectFilter}`);
26082
+ console.log(` Projects: ${scopedProjects.length}`);
26083
+ console.log(` Examined: ${examined}`);
26084
+ console.log(` Updated: ${updated}`);
26085
+ console.log(` Mentioned: ${mentioned}`);
26086
+ }
26087
+ function readStoredGroundingSources(rawResponse) {
26088
+ const envelope = parseJsonColumn(rawResponse, {});
26089
+ const sources = envelope.groundingSources;
26090
+ if (!Array.isArray(sources)) return [];
26091
+ const result = [];
26092
+ for (const source of sources) {
26093
+ if (source && typeof source === "object") {
26094
+ const uri = source.uri;
26095
+ const title = source.title;
26096
+ if (typeof uri === "string") {
26097
+ result.push({ uri, title: typeof title === "string" ? title : "" });
26098
+ }
26099
+ }
26100
+ }
26101
+ return result;
26102
+ }
26103
+ async function backfillInsightsCommand(project, opts) {
26104
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-7AWRUNI2.js");
26105
+ const config = loadConfig();
26106
+ const db = createClient(config.database);
26107
+ migrate(db);
26108
+ const service = new IntelligenceService2(db);
26109
+ const isJson = opts?.format === "json";
26110
+ const isDryRun = opts?.dryRun === true;
26111
+ if (!isJson) {
26112
+ const scope = opts?.since ? ` (since ${opts.since})` : "";
26113
+ const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
26114
+ process.stderr.write(`Backfilling insights for "${project}"${scope}${mode}...
26115
+ `);
26116
+ }
26117
+ const result = service.backfill(project, {
26118
+ fromRunId: opts?.fromRun,
26119
+ toRunId: opts?.toRun,
26120
+ since: opts?.since,
26121
+ dryRun: isDryRun
26122
+ }, (info) => {
26123
+ if (!isJson) {
26124
+ process.stderr.write(` [${info.index}/${info.total}] ${info.runId} \u2014 ${info.insights} insights
26125
+ `);
26126
+ }
26127
+ });
26128
+ const output = {
26129
+ project,
26130
+ processed: result.processed,
26131
+ skipped: result.skipped,
26132
+ totalInsights: result.totalInsights
26133
+ };
26134
+ if (result.dryRun) {
26135
+ output.dryRun = true;
26136
+ output.delta = result.delta;
26137
+ }
26138
+ if (isJson) {
26139
+ console.log(JSON.stringify(output, null, 2));
26140
+ return;
26141
+ }
26142
+ console.log(`
26143
+ Backfill ${isDryRun ? "preview" : "complete"}.`);
26144
+ console.log(` Processed: ${result.processed}`);
26145
+ console.log(` Skipped: ${result.skipped}`);
26146
+ console.log(` Insights: ${result.totalInsights}`);
26147
+ if (result.delta) {
26148
+ console.log(` Delta: -${result.delta.wouldDelete} existing +${result.delta.wouldCreate} new (net ${result.delta.netChange >= 0 ? "+" : ""}${result.delta.netChange})`);
26149
+ console.log(` No DB writes performed. Re-run without --dry-run to apply.`);
26150
+ }
26151
+ }
26152
+ function reparseProviderSnapshot(provider, rawResponse) {
26153
+ const envelope = parseJsonColumn(rawResponse, {});
26154
+ const apiResponse = resolveStoredApiResponse(envelope);
26155
+ if (!apiResponse) return null;
26156
+ switch (provider) {
26157
+ case ProviderNames.openai:
26158
+ return reparseStoredResult2(apiResponse);
26159
+ case ProviderNames.claude:
26160
+ return reparseStoredResult3(apiResponse);
26161
+ case ProviderNames.gemini:
26162
+ return reparseStoredResult(apiResponse);
26163
+ case ProviderNames.perplexity:
26164
+ return reparseStoredResult4(apiResponse);
26165
+ default:
26166
+ return null;
26167
+ }
26168
+ }
26169
+ function resolveStoredApiResponse(parsed) {
26170
+ const nested = parsed.apiResponse;
26171
+ if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
26172
+ return nested;
26173
+ }
26174
+ if (looksLikeProviderApiResponse(parsed)) {
26175
+ return parsed;
26176
+ }
26177
+ return null;
26178
+ }
26179
+ function looksLikeProviderApiResponse(value) {
26180
+ return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
26181
+ }
26182
+ function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
26183
+ const parsed = parseJsonColumn(rawResponse, {});
26184
+ const apiResponse = resolveStoredApiResponse(parsed);
26185
+ const envelope = apiResponse === parsed ? {} : { ...parsed };
26186
+ delete envelope.answerText;
26187
+ delete envelope.citedDomains;
26188
+ delete envelope.competitorOverlap;
26189
+ delete envelope.recommendedCompetitors;
26190
+ delete envelope.providerError;
26191
+ return JSON.stringify({
26192
+ ...envelope,
26193
+ groundingSources: reparsed.groundingSources,
26194
+ searchQueries: reparsed.searchQueries,
26195
+ ...reparsed.providerError ? { providerError: reparsed.providerError } : {},
26196
+ ...apiResponse ? { apiResponse } : {}
26197
+ });
26198
+ }
26199
+
25297
26200
  // src/provider-registry.ts
25298
26201
  var ProviderRegistry = class {
25299
26202
  providers = /* @__PURE__ */ new Map();
@@ -25347,7 +26250,7 @@ var ProviderRegistry = class {
25347
26250
 
25348
26251
  // src/scheduler.ts
25349
26252
  import cron from "node-cron";
25350
- import { and as and22, eq as eq34 } from "drizzle-orm";
26253
+ import { and as and25, eq as eq35 } from "drizzle-orm";
25351
26254
  var log9 = createLogger("Scheduler");
25352
26255
  function taskKey(projectId, kind) {
25353
26256
  return `${projectId}::${kind}`;
@@ -25362,7 +26265,7 @@ var Scheduler = class {
25362
26265
  }
25363
26266
  /** Load all enabled schedules from DB and register cron jobs. */
25364
26267
  start() {
25365
- const allSchedules = this.db.select().from(schedules).where(eq34(schedules.enabled, 1)).all();
26268
+ const allSchedules = this.db.select().from(schedules).where(eq35(schedules.enabled, 1)).all();
25366
26269
  for (const schedule of allSchedules) {
25367
26270
  const missedRunAt = schedule.nextRunAt;
25368
26271
  this.registerCronTask(schedule);
@@ -25392,7 +26295,7 @@ var Scheduler = class {
25392
26295
  this.stopTask(key, existing, "Stopped");
25393
26296
  this.tasks.delete(key);
25394
26297
  }
25395
- const schedule = this.db.select().from(schedules).where(and22(eq34(schedules.projectId, projectId), eq34(schedules.kind, kind))).get();
26298
+ const schedule = this.db.select().from(schedules).where(and25(eq35(schedules.projectId, projectId), eq35(schedules.kind, kind))).get();
25396
26299
  if (schedule && schedule.enabled === 1) {
25397
26300
  this.registerCronTask(schedule);
25398
26301
  }
@@ -25433,14 +26336,14 @@ var Scheduler = class {
25433
26336
  this.db.update(schedules).set({
25434
26337
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
25435
26338
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
25436
- }).where(eq34(schedules.id, scheduleId)).run();
26339
+ }).where(eq35(schedules.id, scheduleId)).run();
25437
26340
  const label = schedule.preset ?? cronExpr;
25438
26341
  log9.info("cron.registered", { projectId, kind, schedule: label, timezone });
25439
26342
  }
25440
26343
  triggerRun(scheduleId, projectId, kind) {
25441
26344
  try {
25442
26345
  const now = (/* @__PURE__ */ new Date()).toISOString();
25443
- const currentSchedule = this.db.select().from(schedules).where(eq34(schedules.id, scheduleId)).get();
26346
+ const currentSchedule = this.db.select().from(schedules).where(eq35(schedules.id, scheduleId)).get();
25444
26347
  if (!currentSchedule || currentSchedule.enabled !== 1) {
25445
26348
  log9.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
25446
26349
  this.remove(projectId, kind);
@@ -25448,7 +26351,7 @@ var Scheduler = class {
25448
26351
  }
25449
26352
  const task = this.tasks.get(taskKey(projectId, kind));
25450
26353
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
25451
- const project = this.db.select().from(projects).where(eq34(projects.id, projectId)).get();
26354
+ const project = this.db.select().from(projects).where(eq35(projects.id, projectId)).get();
25452
26355
  if (!project) {
25453
26356
  log9.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
25454
26357
  this.remove(projectId, kind);
@@ -25468,7 +26371,7 @@ var Scheduler = class {
25468
26371
  lastRunAt: now,
25469
26372
  nextRunAt,
25470
26373
  updatedAt: now
25471
- }).where(eq34(schedules.id, currentSchedule.id)).run();
26374
+ }).where(eq35(schedules.id, currentSchedule.id)).run();
25472
26375
  log9.info("traffic-sync.triggered", { projectName: project.name, sourceId });
25473
26376
  this.callbacks.onTrafficSyncRequested(project.name, sourceId);
25474
26377
  return;
@@ -25496,7 +26399,7 @@ var Scheduler = class {
25496
26399
  this.db.update(schedules).set({
25497
26400
  nextRunAt,
25498
26401
  updatedAt: now
25499
- }).where(eq34(schedules.id, currentSchedule.id)).run();
26402
+ }).where(eq35(schedules.id, currentSchedule.id)).run();
25500
26403
  return;
25501
26404
  }
25502
26405
  const runId = queueResult.runId;
@@ -25504,7 +26407,7 @@ var Scheduler = class {
25504
26407
  lastRunAt: now,
25505
26408
  nextRunAt,
25506
26409
  updatedAt: now
25507
- }).where(eq34(schedules.id, currentSchedule.id)).run();
26410
+ }).where(eq35(schedules.id, currentSchedule.id)).run();
25508
26411
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
25509
26412
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
25510
26413
  log9.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -25516,7 +26419,7 @@ var Scheduler = class {
25516
26419
  };
25517
26420
 
25518
26421
  // src/notifier.ts
25519
- import { eq as eq35, desc as desc16, and as and23, inArray as inArray10, or as or4 } from "drizzle-orm";
26422
+ import { eq as eq36, desc as desc16, and as and26, inArray as inArray11, or as or5 } from "drizzle-orm";
25520
26423
  import crypto31 from "crypto";
25521
26424
  var log10 = createLogger("Notifier");
25522
26425
  var Notifier = class {
@@ -25529,18 +26432,18 @@ var Notifier = class {
25529
26432
  /** Called after a run completes (success, partial, or failed). */
25530
26433
  async onRunCompleted(runId, projectId) {
25531
26434
  log10.info("run.completed", { runId, projectId });
25532
- const notifs = this.db.select().from(notifications).where(eq35(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
26435
+ const notifs = this.db.select().from(notifications).where(eq36(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
25533
26436
  if (notifs.length === 0) {
25534
26437
  log10.info("notifications.none-enabled", { projectId });
25535
26438
  return;
25536
26439
  }
25537
26440
  log10.info("notifications.found", { projectId, count: notifs.length });
25538
- const run = this.db.select().from(runs).where(eq35(runs.id, runId)).get();
26441
+ const run = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
25539
26442
  if (!run) {
25540
26443
  log10.error("run.not-found", { runId, msg: "skipping notification dispatch" });
25541
26444
  return;
25542
26445
  }
25543
- const project = this.db.select().from(projects).where(eq35(projects.id, projectId)).get();
26446
+ const project = this.db.select().from(projects).where(eq36(projects.id, projectId)).get();
25544
26447
  if (!project) {
25545
26448
  log10.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
25546
26449
  return;
@@ -25587,11 +26490,11 @@ var Notifier = class {
25587
26490
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
25588
26491
  if (highInsights.length > 0) insightEvents.push("insight.high");
25589
26492
  if (insightEvents.length === 0) return;
25590
- const notifs = this.db.select().from(notifications).where(eq35(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
26493
+ const notifs = this.db.select().from(notifications).where(eq36(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
25591
26494
  if (notifs.length === 0) return;
25592
- const run = this.db.select().from(runs).where(eq35(runs.id, runId)).get();
26495
+ const run = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
25593
26496
  if (!run) return;
25594
- const project = this.db.select().from(projects).where(eq35(projects.id, projectId)).get();
26497
+ const project = this.db.select().from(projects).where(eq36(projects.id, projectId)).get();
25595
26498
  if (!project) return;
25596
26499
  for (const notif of notifs) {
25597
26500
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -25621,12 +26524,12 @@ var Notifier = class {
25621
26524
  }
25622
26525
  }
25623
26526
  computeTransitions(runId, projectId) {
25624
- const thisRun = this.db.select().from(runs).where(eq35(runs.id, runId)).get();
26527
+ const thisRun = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
25625
26528
  if (!thisRun) return [];
25626
- const groupSiblings = this.db.select().from(runs).where(and23(
25627
- eq35(runs.projectId, projectId),
25628
- eq35(runs.kind, thisRun.kind),
25629
- eq35(runs.createdAt, thisRun.createdAt)
26529
+ const groupSiblings = this.db.select().from(runs).where(and26(
26530
+ eq36(runs.projectId, projectId),
26531
+ eq36(runs.kind, thisRun.kind),
26532
+ eq36(runs.createdAt, thisRun.createdAt)
25630
26533
  )).all();
25631
26534
  const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
25632
26535
  if (stillPending) return [];
@@ -25642,17 +26545,17 @@ var Notifier = class {
25642
26545
  return candidate.id > best.id ? candidate : best;
25643
26546
  });
25644
26547
  if (winner.id !== runId) return [];
25645
- const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq35(projects.id, projectId)).get();
26548
+ const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq36(projects.id, projectId)).get();
25646
26549
  const locationCount = Math.max(
25647
26550
  1,
25648
26551
  parseJsonColumn(projectLocations?.locations ?? null, []).length
25649
26552
  );
25650
26553
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
25651
26554
  const recentRuns = this.db.select().from(runs).where(
25652
- and23(
25653
- eq35(runs.projectId, projectId),
25654
- eq35(runs.kind, thisRun.kind),
25655
- or4(eq35(runs.status, "completed"), eq35(runs.status, "partial"))
26555
+ and26(
26556
+ eq36(runs.projectId, projectId),
26557
+ eq36(runs.kind, thisRun.kind),
26558
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial"))
25656
26559
  )
25657
26560
  ).orderBy(desc16(runs.createdAt), desc16(runs.id)).limit(RECENT_FETCH_LIMIT).all();
25658
26561
  const groups = groupRunsByCreatedAt(recentRuns);
@@ -25669,13 +26572,13 @@ var Notifier = class {
25669
26572
  provider: querySnapshots.provider,
25670
26573
  location: querySnapshots.location,
25671
26574
  citationState: querySnapshots.citationState
25672
- }).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(inArray10(querySnapshots.runId, currentRunIds)).all();
26575
+ }).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(inArray11(querySnapshots.runId, currentRunIds)).all();
25673
26576
  const previousSnapshots = this.db.select({
25674
26577
  queryId: querySnapshots.queryId,
25675
26578
  provider: querySnapshots.provider,
25676
26579
  location: querySnapshots.location,
25677
26580
  citationState: querySnapshots.citationState
25678
- }).from(querySnapshots).where(inArray10(querySnapshots.runId, previousRunIds)).all();
26581
+ }).from(querySnapshots).where(inArray11(querySnapshots.runId, previousRunIds)).all();
25679
26582
  const prevMap = /* @__PURE__ */ new Map();
25680
26583
  for (const s of previousSnapshots) {
25681
26584
  if (s.queryId == null) continue;
@@ -25749,7 +26652,7 @@ var Notifier = class {
25749
26652
  };
25750
26653
 
25751
26654
  // src/run-coordinator.ts
25752
- import { eq as eq36 } from "drizzle-orm";
26655
+ import { eq as eq37 } from "drizzle-orm";
25753
26656
  var log11 = createLogger("RunCoordinator");
25754
26657
  var RunCoordinator = class {
25755
26658
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
@@ -25760,7 +26663,7 @@ var RunCoordinator = class {
25760
26663
  this.onAeroEvent = onAeroEvent;
25761
26664
  }
25762
26665
  async onRunCompleted(runId, projectId) {
25763
- const runRow = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
26666
+ const runRow = this.db.select().from(runs).where(eq37(runs.id, runId)).get();
25764
26667
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
25765
26668
  let insightCount = 0;
25766
26669
  let criticalOrHigh = 0;
@@ -25815,7 +26718,7 @@ var RunCoordinator = class {
25815
26718
  * so the Aero queue is never starved of a follow-up.
25816
26719
  */
25817
26720
  buildDiscoveryAeroContext(runId, projectId, status, error) {
25818
- const session = this.db.select().from(discoverySessions).where(eq36(discoverySessions.runId, runId)).get();
26721
+ const session = this.db.select().from(discoverySessions).where(eq37(discoverySessions.runId, runId)).get();
25819
26722
  const competitorMap = session ? parseJsonColumn(session.competitorMap, []) : [];
25820
26723
  return {
25821
26724
  kind: RunKinds["aeo-discover-probe"],
@@ -25838,11 +26741,11 @@ var RunCoordinator = class {
25838
26741
 
25839
26742
  // src/agent/session-registry.ts
25840
26743
  import crypto33 from "crypto";
25841
- import { eq as eq38 } from "drizzle-orm";
26744
+ import { eq as eq39 } from "drizzle-orm";
25842
26745
 
25843
26746
  // src/agent/session.ts
25844
- import fs11 from "fs";
25845
- import path13 from "path";
26747
+ import fs12 from "fs";
26748
+ import path14 from "path";
25846
26749
  import { Agent } from "@mariozechner/pi-agent-core";
25847
26750
  import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
25848
26751
 
@@ -25943,26 +26846,26 @@ function buildAgentProvidersResponse(config) {
25943
26846
  }
25944
26847
 
25945
26848
  // src/agent/skill-paths.ts
25946
- import fs9 from "fs";
25947
- import path11 from "path";
26849
+ import fs10 from "fs";
26850
+ import path12 from "path";
25948
26851
  import { fileURLToPath } from "url";
25949
26852
  function resolveAeroSkillDir(pkgDir) {
25950
- const here = pkgDir ?? path11.dirname(fileURLToPath(import.meta.url));
26853
+ const here = pkgDir ?? path12.dirname(fileURLToPath(import.meta.url));
25951
26854
  const candidates = [
25952
- path11.join(here, "../assets/agent-workspace/skills/aero"),
25953
- path11.join(here, "../../assets/agent-workspace/skills/aero"),
25954
- path11.join(here, "../../../../skills/aero")
26855
+ path12.join(here, "../assets/agent-workspace/skills/aero"),
26856
+ path12.join(here, "../../assets/agent-workspace/skills/aero"),
26857
+ path12.join(here, "../../../../skills/aero")
25955
26858
  ];
25956
26859
  for (const candidate of candidates) {
25957
- if (fs9.existsSync(path11.join(candidate, "SKILL.md"))) return candidate;
26860
+ if (fs10.existsSync(path12.join(candidate, "SKILL.md"))) return candidate;
25958
26861
  }
25959
26862
  throw new Error(`Aero skill not found. Searched:
25960
26863
  ${candidates.join("\n ")}`);
25961
26864
  }
25962
26865
 
25963
26866
  // src/agent/skill-tools.ts
25964
- import fs10 from "fs";
25965
- import path12 from "path";
26867
+ import fs11 from "fs";
26868
+ import path13 from "path";
25966
26869
  import { Type } from "@sinclair/typebox";
25967
26870
  var MAX_DOC_CHARS = 2e4;
25968
26871
  function textResult(details) {
@@ -25983,13 +26886,13 @@ function parseDescription(body) {
25983
26886
  return "(no description)";
25984
26887
  }
25985
26888
  function scanSkillDocs(skillDir) {
25986
- const refsDir = path12.join(skillDir ?? resolveAeroSkillDir(), "references");
25987
- if (!fs10.existsSync(refsDir)) return [];
26889
+ const refsDir = path13.join(skillDir ?? resolveAeroSkillDir(), "references");
26890
+ if (!fs11.existsSync(refsDir)) return [];
25988
26891
  const entries = [];
25989
- for (const file of fs10.readdirSync(refsDir)) {
26892
+ for (const file of fs11.readdirSync(refsDir)) {
25990
26893
  if (!file.endsWith(".md")) continue;
25991
- const filePath = path12.join(refsDir, file);
25992
- const body = fs10.readFileSync(filePath, "utf-8");
26894
+ const filePath = path13.join(refsDir, file);
26895
+ const body = fs11.readFileSync(filePath, "utf-8");
25993
26896
  entries.push({
25994
26897
  slug: file.replace(/\.md$/, ""),
25995
26898
  description: parseDescription(body),
@@ -26032,8 +26935,8 @@ function buildReadSkillDocTool() {
26032
26935
  availableSlugs: docs.map((d) => d.slug)
26033
26936
  });
26034
26937
  }
26035
- const filePath = path12.join(skillDir, "references", `${match.slug}.md`);
26036
- const content = fs10.readFileSync(filePath, "utf-8");
26938
+ const filePath = path13.join(skillDir, "references", `${match.slug}.md`);
26939
+ const content = fs11.readFileSync(filePath, "utf-8");
26037
26940
  if (content.length > MAX_DOC_CHARS) {
26038
26941
  return textResult({
26039
26942
  slug: match.slug,
@@ -26127,10 +27030,10 @@ function ensureBuiltinsRegistered() {
26127
27030
  }
26128
27031
  function loadAeroSystemPrompt(pkgDir) {
26129
27032
  const skillDir = resolveAeroSkillDir(pkgDir);
26130
- const skillBody = fs11.readFileSync(path13.join(skillDir, "SKILL.md"), "utf-8");
26131
- const soulPath = path13.join(skillDir, "soul.md");
26132
- if (!fs11.existsSync(soulPath)) return skillBody;
26133
- const soulBody = fs11.readFileSync(soulPath, "utf-8");
27033
+ const skillBody = fs12.readFileSync(path14.join(skillDir, "SKILL.md"), "utf-8");
27034
+ const soulPath = path14.join(skillDir, "soul.md");
27035
+ if (!fs12.existsSync(soulPath)) return skillBody;
27036
+ const soulBody = fs12.readFileSync(soulPath, "utf-8");
26134
27037
  return `${soulBody.trimEnd()}
26135
27038
 
26136
27039
  ---
@@ -26188,7 +27091,7 @@ function resolveSessionProviderAndModel(config, opts) {
26188
27091
 
26189
27092
  // src/agent/memory-store.ts
26190
27093
  import crypto32 from "crypto";
26191
- import { and as and24, desc as desc17, eq as eq37, like as like2, sql as sql13 } from "drizzle-orm";
27094
+ import { and as and27, desc as desc17, eq as eq38, like as like2, sql as sql14 } from "drizzle-orm";
26192
27095
  var COMPACTION_KEY_PREFIX = "compaction:";
26193
27096
  var COMPACTION_NOTES_PER_SESSION = 3;
26194
27097
  function rowToDto2(row) {
@@ -26202,7 +27105,7 @@ function rowToDto2(row) {
26202
27105
  };
26203
27106
  }
26204
27107
  function listMemoryEntries(db, projectId, opts = {}) {
26205
- const query = db.select().from(agentMemory).where(eq37(agentMemory.projectId, projectId)).orderBy(desc17(agentMemory.updatedAt));
27108
+ const query = db.select().from(agentMemory).where(eq38(agentMemory.projectId, projectId)).orderBy(desc17(agentMemory.updatedAt));
26206
27109
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
26207
27110
  return rows.map(rowToDto2);
26208
27111
  }
@@ -26233,12 +27136,12 @@ function upsertMemoryEntry(db, args) {
26233
27136
  updatedAt: now
26234
27137
  }
26235
27138
  }).run();
26236
- const row = db.select().from(agentMemory).where(and24(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, args.key))).get();
27139
+ const row = db.select().from(agentMemory).where(and27(eq38(agentMemory.projectId, args.projectId), eq38(agentMemory.key, args.key))).get();
26237
27140
  if (!row) throw new Error("memory upsert produced no row");
26238
27141
  return rowToDto2(row);
26239
27142
  }
26240
27143
  function deleteMemoryEntry(db, projectId, key) {
26241
- const result = db.delete(agentMemory).where(and24(eq37(agentMemory.projectId, projectId), eq37(agentMemory.key, key))).run();
27144
+ const result = db.delete(agentMemory).where(and27(eq38(agentMemory.projectId, projectId), eq38(agentMemory.key, key))).run();
26242
27145
  const changes = result.changes ?? 0;
26243
27146
  return changes > 0;
26244
27147
  }
@@ -26267,16 +27170,16 @@ function writeCompactionNote(db, args) {
26267
27170
  }).run();
26268
27171
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
26269
27172
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
26270
- and24(
26271
- eq37(agentMemory.projectId, args.projectId),
27173
+ and27(
27174
+ eq38(agentMemory.projectId, args.projectId),
26272
27175
  like2(agentMemory.key, `${sessionPrefix}%`)
26273
27176
  )
26274
27177
  ).orderBy(desc17(agentMemory.updatedAt)).all();
26275
27178
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
26276
27179
  if (stale.length > 0) {
26277
- tx.delete(agentMemory).where(sql13`${agentMemory.id} IN (${sql13.join(stale.map((s) => sql13`${s}`), sql13`, `)})`).run();
27180
+ tx.delete(agentMemory).where(sql14`${agentMemory.id} IN (${sql14.join(stale.map((s) => sql14`${s}`), sql14`, `)})`).run();
26278
27181
  }
26279
- const row = tx.select().from(agentMemory).where(and24(eq37(agentMemory.projectId, args.projectId), eq37(agentMemory.key, key))).get();
27182
+ const row = tx.select().from(agentMemory).where(and27(eq38(agentMemory.projectId, args.projectId), eq38(agentMemory.key, key))).get();
26280
27183
  if (row) inserted = rowToDto2(row);
26281
27184
  });
26282
27185
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -26458,7 +27361,7 @@ var SessionRegistry = class {
26458
27361
  modelProvider: effectiveProvider,
26459
27362
  modelId: effectiveModelId,
26460
27363
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
26461
- }).where(eq38(agentSessions.projectId, projectId)).run();
27364
+ }).where(eq39(agentSessions.projectId, projectId)).run();
26462
27365
  }
26463
27366
  const agent2 = createAeroSession({
26464
27367
  projectName,
@@ -26672,7 +27575,7 @@ ${lines.join("\n")}
26672
27575
  modelProvider: nextProvider,
26673
27576
  modelId: nextModelId,
26674
27577
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
26675
- }).where(eq38(agentSessions.projectId, projectId)).run();
27578
+ }).where(eq39(agentSessions.projectId, projectId)).run();
26676
27579
  }
26677
27580
  /** Persist a session's transcript back to the DB. Call after any run settles. */
26678
27581
  save(projectName) {
@@ -26834,11 +27737,11 @@ ${lines.join("\n")}
26834
27737
  return id;
26835
27738
  }
26836
27739
  tryResolveProjectId(projectName) {
26837
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq38(projects.name, projectName)).get();
27740
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq39(projects.name, projectName)).get();
26838
27741
  return row?.id;
26839
27742
  }
26840
27743
  loadRow(projectId) {
26841
- const row = this.opts.db.select().from(agentSessions).where(eq38(agentSessions.projectId, projectId)).get();
27744
+ const row = this.opts.db.select().from(agentSessions).where(eq39(agentSessions.projectId, projectId)).get();
26842
27745
  return row ?? null;
26843
27746
  }
26844
27747
  insertRow(params) {
@@ -26857,14 +27760,14 @@ ${lines.join("\n")}
26857
27760
  }
26858
27761
  updateRow(projectId, patch) {
26859
27762
  const now = (/* @__PURE__ */ new Date()).toISOString();
26860
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq38(agentSessions.projectId, projectId)).run();
27763
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq39(agentSessions.projectId, projectId)).run();
26861
27764
  }
26862
27765
  };
26863
27766
 
26864
27767
  // src/agent/agent-routes.ts
26865
- import { eq as eq39 } from "drizzle-orm";
27768
+ import { eq as eq40 } from "drizzle-orm";
26866
27769
  function resolveProject2(db, name) {
26867
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq39(projects.name, name)).get();
27770
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq40(projects.name, name)).get();
26868
27771
  if (!row) throw notFound("project", name);
26869
27772
  return row;
26870
27773
  }
@@ -26873,7 +27776,7 @@ function registerAgentRoutes(app, opts) {
26873
27776
  "/projects/:name/agent/transcript",
26874
27777
  async (request) => {
26875
27778
  const project = resolveProject2(opts.db, request.params.name);
26876
- const row = opts.db.select().from(agentSessions).where(eq39(agentSessions.projectId, project.id)).get();
27779
+ const row = opts.db.select().from(agentSessions).where(eq40(agentSessions.projectId, project.id)).get();
26877
27780
  if (!row) {
26878
27781
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
26879
27782
  }
@@ -26897,7 +27800,7 @@ function registerAgentRoutes(app, opts) {
26897
27800
  async (request) => {
26898
27801
  const project = resolveProject2(opts.db, request.params.name);
26899
27802
  opts.sessionRegistry.reset(project.name);
26900
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq39(agentSessions.projectId, project.id)).run();
27803
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq40(agentSessions.projectId, project.id)).run();
26901
27804
  return { status: "reset" };
26902
27805
  }
26903
27806
  );
@@ -27015,7 +27918,7 @@ import { runAeoAudit } from "@ainyc/aeo-audit";
27015
27918
 
27016
27919
  // src/site-fetch.ts
27017
27920
  import https2 from "https";
27018
- var FETCH_TIMEOUT_MS = 1e4;
27921
+ var FETCH_TIMEOUT_MS2 = 1e4;
27019
27922
  var MAX_TEXT_LENGTH = 4e3;
27020
27923
  var MAX_BODY_BYTES = 512e3;
27021
27924
  var USER_AGENT = "Canonry/1.0 (site-analysis)";
@@ -27032,15 +27935,15 @@ function extractHostname(domain) {
27032
27935
  function fetchWithPinnedAddress(target) {
27033
27936
  return new Promise((resolve) => {
27034
27937
  const port = target.url.port ? Number(target.url.port) : 443;
27035
- const path15 = target.url.pathname + target.url.search;
27938
+ const path16 = target.url.pathname + target.url.search;
27036
27939
  const req = https2.request(
27037
27940
  {
27038
27941
  hostname: target.address,
27039
27942
  family: target.family,
27040
27943
  port,
27041
- path: path15,
27944
+ path: path16,
27042
27945
  method: "GET",
27043
- timeout: FETCH_TIMEOUT_MS,
27946
+ timeout: FETCH_TIMEOUT_MS2,
27044
27947
  servername: target.url.hostname,
27045
27948
  // SNI for TLS
27046
27949
  headers: {
@@ -27361,7 +28264,7 @@ var SnapshotService = class {
27361
28264
  provider: provider.adapter.name,
27362
28265
  displayName: provider.adapter.displayName,
27363
28266
  model: raw.model,
27364
- mentioned: determineAnswerMentioned(normalized.answerText, ctx.companyName, answerVisibilityDomains),
28267
+ mentioned: determineAnswerMentioned(normalized.answerText, [ctx.companyName], answerVisibilityDomains),
27365
28268
  cited: citesTargetDomain(normalized.citedDomains, normalized.groundingSources, ctx.domain),
27366
28269
  describedAccurately: "unknown",
27367
28270
  accuracyNotes: null,
@@ -27728,8 +28631,8 @@ function clipText(value, length) {
27728
28631
  }
27729
28632
 
27730
28633
  // src/server.ts
27731
- var _require2 = createRequire3(import.meta.url);
27732
- var { version: PKG_VERSION } = _require2("../package.json");
28634
+ var _require3 = createRequire4(import.meta.url);
28635
+ var { version: PKG_VERSION2 } = _require3("../package.json");
27733
28636
  var log14 = createLogger("Server");
27734
28637
  var DEFAULT_QUOTA = {
27735
28638
  maxConcurrency: 2,
@@ -27920,7 +28823,7 @@ async function createServer(opts) {
27920
28823
  intelligenceService,
27921
28824
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
27922
28825
  async (ctx) => {
27923
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq40(projects.id, ctx.projectId)).get();
28826
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq41(projects.id, ctx.projectId)).get();
27924
28827
  if (!project) return;
27925
28828
  let content;
27926
28829
  if (ctx.kind === RunKinds["aeo-discover-probe"]) {
@@ -27943,8 +28846,8 @@ async function createServer(opts) {
27943
28846
  );
27944
28847
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
27945
28848
  const snapshotService = new SnapshotService(registry);
27946
- const orphanedOpenClawDir = path14.join(os6.homedir(), ".openclaw-aero");
27947
- if (fs12.existsSync(orphanedOpenClawDir)) {
28849
+ const orphanedOpenClawDir = path15.join(os6.homedir(), ".openclaw-aero");
28850
+ if (fs13.existsSync(orphanedOpenClawDir)) {
27948
28851
  app.log.warn(
27949
28852
  { path: orphanedOpenClawDir },
27950
28853
  "OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
@@ -28121,7 +29024,7 @@ async function createServer(opts) {
28121
29024
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
28122
29025
  if (opts.config.apiKey) {
28123
29026
  const keyHash = hashApiKey(opts.config.apiKey);
28124
- const existing = opts.db.select().from(apiKeys).where(eq40(apiKeys.keyHash, keyHash)).get();
29027
+ const existing = opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, keyHash)).get();
28125
29028
  if (!existing) {
28126
29029
  const prefix = opts.config.apiKey.slice(0, 12);
28127
29030
  opts.db.insert(apiKeys).values({
@@ -28173,7 +29076,7 @@ async function createServer(opts) {
28173
29076
  };
28174
29077
  const getDefaultApiKey = () => {
28175
29078
  if (!opts.config.apiKey) return void 0;
28176
- return opts.db.select().from(apiKeys).where(eq40(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
29079
+ return opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
28177
29080
  };
28178
29081
  const createPasswordSession = (reply) => {
28179
29082
  const key = getDefaultApiKey();
@@ -28230,12 +29133,12 @@ async function createServer(opts) {
28230
29133
  return reply.send({ authenticated: true });
28231
29134
  }
28232
29135
  if (apiKey) {
28233
- const key = opts.db.select().from(apiKeys).where(eq40(apiKeys.keyHash, hashApiKey(apiKey))).get();
29136
+ const key = opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, hashApiKey(apiKey))).get();
28234
29137
  if (!key || key.revokedAt) {
28235
29138
  const err2 = authInvalid();
28236
29139
  return reply.status(err2.statusCode).send(err2.toJSON());
28237
29140
  }
28238
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq40(apiKeys.id, key.id)).run();
29141
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq41(apiKeys.id, key.id)).run();
28239
29142
  const sessionId = createSession(key.id);
28240
29143
  reply.header("set-cookie", serializeSessionCookie({
28241
29144
  name: SESSION_COOKIE_NAME,
@@ -28289,6 +29192,12 @@ async function createServer(opts) {
28289
29192
  skipAuth: false,
28290
29193
  sessionCookieName: SESSION_COOKIE_NAME,
28291
29194
  resolveSessionApiKeyId,
29195
+ // Local canonry serve runs on the operator's machine, where pointing a
29196
+ // webhook at localhost (Discord test container, Pipedream-mock dev server,
29197
+ // etc.) is a legitimate workflow. Default to allowing it for the local
29198
+ // installer; cloud deployments inherit the secure default of `false` by
29199
+ // not passing this option. Override with CANONRY_ALLOW_LOOPBACK_WEBHOOKS=0.
29200
+ allowLoopbackWebhooks: process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0",
28292
29201
  // Local-only Aero agent routes. Registered here so they inherit api-routes'
28293
29202
  // auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
28294
29203
  registerAuthenticatedRoutes: async (scope) => {
@@ -28409,7 +29318,7 @@ async function createServer(opts) {
28409
29318
  discoverLatestRelease,
28410
29319
  openApiInfo: {
28411
29320
  title: "Canonry API",
28412
- version: PKG_VERSION,
29321
+ version: PKG_VERSION2,
28413
29322
  includeCanonryLocal: true
28414
29323
  },
28415
29324
  providerSummary,
@@ -28556,6 +29465,19 @@ async function createServer(opts) {
28556
29465
  onProjectDeleted: (projectId) => {
28557
29466
  scheduler.removeAllForProject(projectId);
28558
29467
  },
29468
+ onAliasesChanged: (projectId, projectName) => {
29469
+ setImmediate(() => {
29470
+ try {
29471
+ const result = backfillProjectAnswerMentions(opts.db, projectId);
29472
+ app.log.info(
29473
+ { projectId, projectName, ...result },
29474
+ "aliases changed \u2014 recomputed mention fields on historical snapshots"
29475
+ );
29476
+ } catch (err) {
29477
+ app.log.error({ err, projectId, projectName }, "alias-triggered backfill failed");
29478
+ }
29479
+ });
29480
+ },
28559
29481
  getTelemetryStatus: () => {
28560
29482
  const enabled = isTelemetryEnabled();
28561
29483
  return {
@@ -28640,10 +29562,10 @@ async function createServer(opts) {
28640
29562
  return snapshotService.createReport(input);
28641
29563
  }
28642
29564
  });
28643
- const dirname = path14.dirname(fileURLToPath2(import.meta.url));
28644
- const assetsDir = path14.join(dirname, "..", "assets");
28645
- if (fs12.existsSync(assetsDir)) {
28646
- const indexPath = path14.join(assetsDir, "index.html");
29565
+ const dirname = path15.dirname(fileURLToPath2(import.meta.url));
29566
+ const assetsDir = path15.join(dirname, "..", "assets");
29567
+ if (fs13.existsSync(assetsDir)) {
29568
+ const indexPath = path15.join(assetsDir, "index.html");
28647
29569
  const injectConfig = (html) => {
28648
29570
  const clientConfig = {};
28649
29571
  if (basePath) clientConfig.basePath = basePath;
@@ -28661,8 +29583,8 @@ async function createServer(opts) {
28661
29583
  index: false
28662
29584
  });
28663
29585
  const serveIndex = (_request, reply) => {
28664
- if (fs12.existsSync(indexPath)) {
28665
- const html = fs12.readFileSync(indexPath, "utf-8");
29586
+ if (fs13.existsSync(indexPath)) {
29587
+ const html = fs13.readFileSync(indexPath, "utf-8");
28666
29588
  return reply.type("text/html").send(injectConfig(html));
28667
29589
  }
28668
29590
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -28682,23 +29604,28 @@ async function createServer(opts) {
28682
29604
  if (basePath && !url.startsWith(basePath)) {
28683
29605
  return reply.status(404).send({ error: "Not found", path: request.url });
28684
29606
  }
28685
- if (fs12.existsSync(indexPath)) {
28686
- const html = fs12.readFileSync(indexPath, "utf-8");
29607
+ if (fs13.existsSync(indexPath)) {
29608
+ const html = fs13.readFileSync(indexPath, "utf-8");
28687
29609
  return reply.type("text/html").send(injectConfig(html));
28688
29610
  }
28689
29611
  return reply.status(404).send({ error: "Not found" });
28690
29612
  });
28691
29613
  }
28692
- const healthHandler = async () => ({
28693
- status: "ok",
28694
- service: "canonry",
28695
- version: PKG_VERSION,
28696
- ...basePath ? { basePath: basePath.replace(/\/$/, "") } : {}
28697
- });
29614
+ const healthHandler = () => {
29615
+ const update = checkLatestVersionForServer();
29616
+ return {
29617
+ status: "ok",
29618
+ service: "canonry",
29619
+ version: PKG_VERSION2,
29620
+ ...basePath ? { basePath: basePath.replace(/\/$/, "") } : {},
29621
+ ...update ? { updateAvailable: update } : {}
29622
+ };
29623
+ };
28698
29624
  app.get("/health", healthHandler);
28699
29625
  if (basePath) {
28700
29626
  app.get(`${basePath}health`, healthHandler);
28701
29627
  }
29628
+ checkLatestVersionForServer();
28702
29629
  scheduler.start();
28703
29630
  app.addHook("onClose", async () => {
28704
29631
  scheduler.stop();
@@ -28759,16 +29686,17 @@ export {
28759
29686
  showFirstRunNotice,
28760
29687
  detectAndTrackUpgrade,
28761
29688
  trackEvent,
28762
- reparseStoredResult2 as reparseStoredResult,
28763
- reparseStoredResult3 as reparseStoredResult2,
28764
- reparseStoredResult as reparseStoredResult3,
28765
- reparseStoredResult4,
28766
- determineCitationState,
28767
- computeCompetitorOverlap,
28768
- extractRecommendedCompetitors,
29689
+ backfillAnswerVisibilityCommand,
29690
+ backfillNormalizedPaths,
29691
+ backfillNormalizedPathsCommand,
29692
+ backfillAiReferralPaths,
29693
+ backfillAiReferralPathsCommand,
29694
+ backfillAnswerMentionsCommand,
29695
+ backfillInsightsCommand,
28769
29696
  renderReportHtml,
28770
29697
  setGoogleAuthConfig,
28771
29698
  formatAuditFactorScore,
29699
+ checkLatestVersionForCli,
28772
29700
  listAgentProviders,
28773
29701
  coerceAgentProvider,
28774
29702
  createServer