@ainyc/canonry 4.35.0 → 4.39.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-B3FBOECD.js";
8
+ } from "./chunk-JS6KRBBZ.js";
9
9
  import {
10
10
  DEFAULT_RUN_HISTORY_LIMIT,
11
11
  IntelligenceService,
@@ -29,11 +29,14 @@ import {
29
29
  buildContentTargetRows,
30
30
  buildGapQueryScore,
31
31
  buildInventory,
32
+ buildMentionCoverage,
33
+ buildMentionGapScore,
32
34
  buildMentionLandscape,
33
35
  buildMovementSummary,
34
36
  buildOverviewCompetitors,
35
37
  buildProviderScores,
36
38
  buildRunHistory,
39
+ buildShareOfVoice,
37
40
  buildVisibilityScore,
38
41
  categorizeQueryByIntent,
39
42
  ccReleaseSyncs,
@@ -73,7 +76,7 @@ import {
73
76
  schedules,
74
77
  trafficSources,
75
78
  usageCounters
76
- } from "./chunk-MLS5KJWK.js";
79
+ } from "./chunk-DOUV45KI.js";
77
80
  import {
78
81
  AGENT_MEMORY_VALUE_MAX_BYTES,
79
82
  AGENT_PROVIDER_IDS,
@@ -186,7 +189,7 @@ import {
186
189
  visibilityStateFromAnswerMentioned,
187
190
  windowCutoff,
188
191
  wordpressEnvSchema
189
- } from "./chunk-EM5GVF3C.js";
192
+ } from "./chunk-XJVYVURK.js";
190
193
 
191
194
  // src/telemetry.ts
192
195
  import crypto from "crypto";
@@ -475,8 +478,8 @@ function checkLatestVersionForServer(opts) {
475
478
  // src/server.ts
476
479
  import { createRequire as createRequire4 } from "module";
477
480
  import crypto34 from "crypto";
478
- import fs12 from "fs";
479
- import path14 from "path";
481
+ import fs13 from "fs";
482
+ import path15 from "path";
480
483
  import { fileURLToPath as fileURLToPath2 } from "url";
481
484
  import { eq as eq41 } from "drizzle-orm";
482
485
  import Fastify from "fastify";
@@ -548,7 +551,7 @@ async function authPlugin(app, opts = {}) {
548
551
 
549
552
  // ../api-routes/src/projects.ts
550
553
  import crypto4 from "crypto";
551
- import { eq as eq3 } from "drizzle-orm";
554
+ import { eq as eq3, sql as sql2 } from "drizzle-orm";
552
555
 
553
556
  // ../api-routes/src/helpers.ts
554
557
  import crypto3 from "crypto";
@@ -726,6 +729,30 @@ async function projectRoutes(app, opts) {
726
729
  const project = resolveProject(app.db, request.params.name);
727
730
  return reply.send(formatProject(project));
728
731
  });
732
+ app.get("/projects/:name/delete-preview", async (request, reply) => {
733
+ const project = resolveProject(app.db, request.params.name);
734
+ const pid = project.id;
735
+ const count = (n) => n ?? 0;
736
+ const queryCount = app.db.select({ n: sql2`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
737
+ const competitorCount = app.db.select({ n: sql2`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
738
+ const runCount = app.db.select({ n: sql2`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
739
+ const snapshotCount = app.db.select({ n: sql2`count(*)` }).from(querySnapshots).innerJoin(runs, eq3(querySnapshots.runId, runs.id)).where(eq3(runs.projectId, pid)).get();
740
+ const insightCount = app.db.select({ n: sql2`count(*)` }).from(insights).where(eq3(insights.projectId, pid)).get();
741
+ const auditLogCount = app.db.select({ n: sql2`count(*)` }).from(auditLog).where(eq3(auditLog.projectId, pid)).get();
742
+ return reply.send({
743
+ project: { id: project.id, name: project.name },
744
+ cascadeRows: {
745
+ queries: count(queryCount?.n),
746
+ competitors: count(competitorCount?.n),
747
+ runs: count(runCount?.n),
748
+ snapshots: count(snapshotCount?.n),
749
+ insights: count(insightCount?.n)
750
+ },
751
+ detachedRows: {
752
+ auditLog: count(auditLogCount?.n)
753
+ }
754
+ });
755
+ });
729
756
  app.delete("/projects/:name", async (request, reply) => {
730
757
  const project = resolveProject(app.db, request.params.name);
731
758
  writeAuditLog(app.db, {
@@ -901,7 +928,7 @@ function aliasArraysEqual(a, b) {
901
928
 
902
929
  // ../api-routes/src/queries.ts
903
930
  import crypto5 from "crypto";
904
- import { eq as eq4 } from "drizzle-orm";
931
+ import { eq as eq4, inArray, sql as sql3 } from "drizzle-orm";
905
932
  async function queryRoutes(app, opts) {
906
933
  app.get("/projects/:name/queries", async (request, reply) => {
907
934
  const project = resolveProject(app.db, request.params.name);
@@ -937,6 +964,36 @@ async function queryRoutes(app, opts) {
937
964
  const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
938
965
  return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
939
966
  });
967
+ app.post("/projects/:name/queries/replace-preview", async (request, reply) => {
968
+ const project = resolveProject(app.db, request.params.name);
969
+ const body = request.body;
970
+ if (!body || !Array.isArray(body.queries)) {
971
+ throw validationError('Body must contain a "queries" array');
972
+ }
973
+ const currentRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
974
+ const currentTexts = currentRows.map((r) => r.query);
975
+ const currentSet = new Set(currentTexts);
976
+ const proposedSet = new Set(body.queries);
977
+ const removed = currentTexts.filter((q) => !proposedSet.has(q));
978
+ const added = body.queries.filter((q) => !currentSet.has(q));
979
+ const unchanged = currentTexts.filter((q) => proposedSet.has(q));
980
+ const currentIds = currentRows.map((r) => r.id);
981
+ let snapshotsDetached = 0;
982
+ let affectedQueries = 0;
983
+ if (currentIds.length > 0) {
984
+ const snapshotCount = app.db.select({ n: sql3`count(*)` }).from(querySnapshots).where(inArray(querySnapshots.queryId, currentIds)).get();
985
+ snapshotsDetached = snapshotCount?.n ?? 0;
986
+ const distinctAffected = app.db.select({ n: sql3`count(distinct ${querySnapshots.queryId})` }).from(querySnapshots).where(inArray(querySnapshots.queryId, currentIds)).get();
987
+ affectedQueries = distinctAffected?.n ?? 0;
988
+ }
989
+ return reply.send({
990
+ project: { id: project.id, name: project.name },
991
+ current: currentTexts,
992
+ proposed: body.queries,
993
+ diff: { added, removed, unchanged },
994
+ snapshotImpact: { affectedQueries, snapshotsDetached }
995
+ });
996
+ });
940
997
  app.delete("/projects/:name/queries", async (request, reply) => {
941
998
  const project = resolveProject(app.db, request.params.name);
942
999
  const body = request.body;
@@ -1300,7 +1357,7 @@ function parseCompetitorBatch(value) {
1300
1357
 
1301
1358
  // ../api-routes/src/runs.ts
1302
1359
  import crypto8 from "crypto";
1303
- import { and as and2, eq as eq7, asc, desc, or as or2, sql as sql2 } from "drizzle-orm";
1360
+ import { and as and2, eq as eq7, asc, desc, or as or2, sql as sql4 } from "drizzle-orm";
1304
1361
 
1305
1362
  // ../api-routes/src/run-queue.ts
1306
1363
  import crypto7 from "crypto";
@@ -1472,7 +1529,7 @@ async function runRoutes(app, opts) {
1472
1529
  });
1473
1530
  app.get("/projects/:name/runs/latest", async (request, reply) => {
1474
1531
  const project = resolveProject(app.db, request.params.name);
1475
- const countRow = app.db.select({ count: sql2`count(*)` }).from(runs).where(eq7(runs.projectId, project.id)).get();
1532
+ const countRow = app.db.select({ count: sql4`count(*)` }).from(runs).where(eq7(runs.projectId, project.id)).get();
1476
1533
  const totalRuns = countRow?.count ?? 0;
1477
1534
  const latestRun = app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
1478
1535
  if (!latestRun) {
@@ -1800,7 +1857,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
1800
1857
  const body = JSON.stringify(payload);
1801
1858
  const isHttps = target.url.protocol === "https:";
1802
1859
  const port = target.url.port ? Number(target.url.port) : isHttps ? 443 : 80;
1803
- const path15 = `${target.url.pathname}${target.url.search}`;
1860
+ const path16 = `${target.url.pathname}${target.url.search}`;
1804
1861
  const headers = {
1805
1862
  "Content-Length": String(Buffer.byteLength(body)),
1806
1863
  "Content-Type": "application/json",
@@ -1816,7 +1873,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
1816
1873
  headers,
1817
1874
  hostname: target.address,
1818
1875
  method: "POST",
1819
- path: path15,
1876
+ path: path16,
1820
1877
  port,
1821
1878
  timeout: REQUEST_TIMEOUT_MS
1822
1879
  };
@@ -2201,7 +2258,7 @@ function normalizeCompetitorList2(domains) {
2201
2258
  }
2202
2259
 
2203
2260
  // ../api-routes/src/history.ts
2204
- import { eq as eq9, desc as desc2, inArray } from "drizzle-orm";
2261
+ import { eq as eq9, desc as desc2, inArray as inArray2 } from "drizzle-orm";
2205
2262
 
2206
2263
  // ../api-routes/src/notification-redaction.ts
2207
2264
  var REDACTED_URL = {
@@ -2277,7 +2334,7 @@ async function historyRoutes(app) {
2277
2334
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
2278
2335
  location: querySnapshots.location,
2279
2336
  createdAt: querySnapshots.createdAt
2280
- }).from(querySnapshots).leftJoin(queries, eq9(querySnapshots.queryId, queries.id)).where(inArray(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc2(querySnapshots.createdAt)).all();
2337
+ }).from(querySnapshots).leftJoin(queries, eq9(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc2(querySnapshots.createdAt)).all();
2281
2338
  const locationFilter = request.query.location;
2282
2339
  const filtered = locationFilter !== void 0 ? allSnapshots.filter((s) => s.location === (locationFilter || null)) : allSnapshots;
2283
2340
  const total = filtered.length;
@@ -2312,7 +2369,7 @@ async function historyRoutes(app) {
2312
2369
  return reply.send([]);
2313
2370
  }
2314
2371
  const runIds = new Set(projectRuns.map((r) => r.id));
2315
- const rawSnapshots = app.db.select().from(querySnapshots).where(inArray(querySnapshots.runId, [...runIds])).all();
2372
+ const rawSnapshots = app.db.select().from(querySnapshots).where(inArray2(querySnapshots.runId, [...runIds])).all();
2316
2373
  const timelineLocationFilter = request.query.location;
2317
2374
  const filteredSnapshots = timelineLocationFilter !== void 0 ? rawSnapshots.filter((s) => s.location === (timelineLocationFilter || null)) : rawSnapshots;
2318
2375
  const allSnapshots = filteredSnapshots.map((snapshot) => ({
@@ -2492,7 +2549,7 @@ function formatAuditEntry(row) {
2492
2549
  }
2493
2550
 
2494
2551
  // ../api-routes/src/analytics.ts
2495
- import { eq as eq10, desc as desc3, inArray as inArray2 } from "drizzle-orm";
2552
+ import { eq as eq10, desc as desc3, inArray as inArray3 } from "drizzle-orm";
2496
2553
  async function analyticsRoutes(app) {
2497
2554
  app.get("/projects/:name/analytics/metrics", async (request, reply) => {
2498
2555
  const project = resolveProject(app.db, request.params.name);
@@ -2519,7 +2576,7 @@ async function analyticsRoutes(app) {
2519
2576
  answerMentioned: querySnapshots.answerMentioned,
2520
2577
  answerText: querySnapshots.answerText,
2521
2578
  createdAt: querySnapshots.createdAt
2522
- }).from(querySnapshots).where(inArray2(querySnapshots.runId, runIds)).all());
2579
+ }).from(querySnapshots).where(inArray3(querySnapshots.runId, runIds)).all());
2523
2580
  const allSnapshots = rawSnapshots.map((s) => ({
2524
2581
  ...s,
2525
2582
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
@@ -2564,7 +2621,7 @@ async function analyticsRoutes(app) {
2564
2621
  citationState: querySnapshots.citationState,
2565
2622
  answerMentioned: querySnapshots.answerMentioned,
2566
2623
  answerText: querySnapshots.answerText
2567
- }).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all());
2624
+ }).from(querySnapshots).where(inArray3(querySnapshots.runId, windowRunIds)).all());
2568
2625
  for (const s of allWindowSnaps) {
2569
2626
  const timePoint = runIdToCreatedAt.get(s.runId) ?? s.runId;
2570
2627
  let entry = consistencyMap.get(s.queryId);
@@ -2585,7 +2642,7 @@ async function analyticsRoutes(app) {
2585
2642
  answerMentioned: querySnapshots.answerMentioned,
2586
2643
  answerText: querySnapshots.answerText,
2587
2644
  competitorOverlap: querySnapshots.competitorOverlap
2588
- }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all());
2645
+ }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray3(querySnapshots.runId, latestGroupRunIds)).all());
2589
2646
  const snapshots = rawSnapshots.map((s) => ({
2590
2647
  ...s,
2591
2648
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
@@ -2680,7 +2737,7 @@ async function analyticsRoutes(app) {
2680
2737
  queryId: querySnapshots.queryId,
2681
2738
  query: queries.query,
2682
2739
  rawResponse: querySnapshots.rawResponse
2683
- }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, windowRunIds)).all();
2740
+ }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray3(querySnapshots.runId, windowRunIds)).all();
2684
2741
  const overallCounts = /* @__PURE__ */ new Map();
2685
2742
  const byQuery = {};
2686
2743
  for (const snap of snapshots) {
@@ -2839,7 +2896,7 @@ function buildCategoryCounts(counts) {
2839
2896
  }
2840
2897
 
2841
2898
  // ../api-routes/src/intelligence.ts
2842
- import { eq as eq11, desc as desc4, and as and4, inArray as inArray3 } from "drizzle-orm";
2899
+ import { eq as eq11, desc as desc4, and as and4, inArray as inArray4 } from "drizzle-orm";
2843
2900
  function emptyHealthSnapshot(projectId) {
2844
2901
  return {
2845
2902
  id: `no-data:${projectId}`,
@@ -2955,14 +3012,14 @@ async function intelligenceRoutes(app) {
2955
3012
  const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and4(
2956
3013
  eq11(runs.projectId, project.id),
2957
3014
  eq11(runs.kind, RunKinds["answer-visibility"]),
2958
- inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
3015
+ inArray4(runs.status, [RunStatuses.completed, RunStatuses.partial])
2959
3016
  )).orderBy(desc4(runs.createdAt), desc4(runs.id)).all();
2960
3017
  const latestGroup = groupRunsByCreatedAt(projectVisRuns)[0] ?? [];
2961
3018
  const latestGroupRunIds = latestGroup.map((r) => r.id);
2962
3019
  if (latestGroupRunIds.length > 0) {
2963
3020
  const groupRows = app.db.select().from(healthSnapshots).where(and4(
2964
3021
  eq11(healthSnapshots.projectId, project.id),
2965
- inArray3(healthSnapshots.runId, latestGroupRunIds)
3022
+ inArray4(healthSnapshots.runId, latestGroupRunIds)
2966
3023
  )).all();
2967
3024
  if (groupRows.length > 0) {
2968
3025
  return reply.send(aggregateHealthSnapshots(project.id, groupRows));
@@ -2984,7 +3041,7 @@ async function intelligenceRoutes(app) {
2984
3041
  }
2985
3042
 
2986
3043
  // ../api-routes/src/report.ts
2987
- import { and as and6, desc as desc6, eq as eq13, gte, inArray as inArray5, lt, lte, ne, or as or3, sql as sql3 } from "drizzle-orm";
3044
+ import { and as and6, desc as desc6, eq as eq13, gte, inArray as inArray6, lt, lte, ne, or as or3, sql as sql5 } from "drizzle-orm";
2988
3045
 
2989
3046
  // ../api-routes/src/report-renderer.ts
2990
3047
  var COLORS = {
@@ -3033,9 +3090,9 @@ function inferAdSource(params) {
3033
3090
  function formatLandingPageHtml(raw) {
3034
3091
  const value = raw ?? "";
3035
3092
  const queryIdx = value.indexOf("?");
3036
- const path15 = queryIdx === -1 ? value : value.slice(0, queryIdx);
3093
+ const path16 = queryIdx === -1 ? value : value.slice(0, queryIdx);
3037
3094
  const query = queryIdx === -1 ? "" : value.slice(queryIdx + 1);
3038
- const pathHtml = `<span class="page-path">${escapeHtml(path15 || "/")}</span>`;
3095
+ const pathHtml = `<span class="page-path">${escapeHtml(path16 || "/")}</span>`;
3039
3096
  if (!query) return pathHtml;
3040
3097
  let summary = "";
3041
3098
  try {
@@ -4389,7 +4446,7 @@ function renderLineChart(points, color, title, height = 200) {
4389
4446
  y: padY + usableH - p.y / max * usableH,
4390
4447
  raw: p
4391
4448
  }));
4392
- const path15 = xy.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
4449
+ const path16 = xy.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
4393
4450
  const dots = xy.map((p) => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="${color}" />`).join("");
4394
4451
  const xLabels = xy.map((p, i) => {
4395
4452
  if (points.length > 8 && i % Math.ceil(points.length / 6) !== 0 && i !== points.length - 1) return "";
@@ -4401,7 +4458,7 @@ function renderLineChart(points, color, title, height = 200) {
4401
4458
  <line x1="${padX}" y1="${padY + usableH}" x2="${padX + usableW}" y2="${padY + usableH}" stroke="${COLORS.border}" stroke-width="1" />
4402
4459
  <text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatNumber(max)}</text>
4403
4460
  <text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">0</text>
4404
- <path d="${path15}" stroke="${color}" stroke-width="2" fill="none" />
4461
+ <path d="${path16}" stroke="${color}" stroke-width="2" fill="none" />
4405
4462
  ${dots}
4406
4463
  ${xLabels}
4407
4464
  </svg>
@@ -5220,7 +5277,7 @@ function renderReportHtml(report, opts = {}) {
5220
5277
  }
5221
5278
 
5222
5279
  // ../api-routes/src/content-data.ts
5223
- import { and as and5, eq as eq12, desc as desc5, inArray as inArray4 } from "drizzle-orm";
5280
+ import { and as and5, eq as eq12, desc as desc5, inArray as inArray5 } from "drizzle-orm";
5224
5281
  var RECENT_RUNS_WINDOW = 5;
5225
5282
  function loadOrchestratorInput(db, project, locationFilter = void 0) {
5226
5283
  const projectId = project.id;
@@ -5350,7 +5407,7 @@ function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter)
5350
5407
  // Queued/running/failed/cancelled runs may have partial or no
5351
5408
  // snapshots; including them risks pointing latestRunId at a run with
5352
5409
  // no usable evidence.
5353
- inArray4(runs.status, [RunStatuses.completed, RunStatuses.partial])
5410
+ inArray5(runs.status, [RunStatuses.completed, RunStatuses.partial])
5354
5411
  )
5355
5412
  ).orderBy(desc5(runs.createdAt)).all();
5356
5413
  const filtered = locationFilter === void 0 ? rows : rows.filter((r) => (r.location ?? null) === locationFilter);
@@ -5375,9 +5432,9 @@ function buildGaTrafficByPage(db, projectId) {
5375
5432
  }).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
5376
5433
  const map = /* @__PURE__ */ new Map();
5377
5434
  for (const row of rows) {
5378
- const path15 = extractPath(row.landingPage);
5379
- if (!path15) continue;
5380
- map.set(path15, (map.get(path15) ?? 0) + (row.sessions ?? 0));
5435
+ const path16 = extractPath(row.landingPage);
5436
+ if (!path16) continue;
5437
+ map.set(path16, (map.get(path16) ?? 0) + (row.sessions ?? 0));
5381
5438
  }
5382
5439
  return map;
5383
5440
  }
@@ -5392,7 +5449,7 @@ function buildCandidateQueries(opts) {
5392
5449
  const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq12(queries.projectId, opts.projectId)).all();
5393
5450
  const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
5394
5451
  const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
5395
- const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all()).filter((r) => candidateQueryIds.includes(r.queryId));
5452
+ const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(inArray5(querySnapshots.runId, opts.recentRunIds)).all()).filter((r) => candidateQueryIds.includes(r.queryId));
5396
5453
  const snapshotsByQuery = /* @__PURE__ */ new Map();
5397
5454
  for (const row of snapshotRows) {
5398
5455
  const list = snapshotsByQuery.get(row.queryId) ?? [];
@@ -5572,8 +5629,8 @@ function normalizeDomain(domain) {
5572
5629
  function extractPath(url) {
5573
5630
  if (!url) return "";
5574
5631
  const match = /^https?:\/\/[^/]+(.*)$/.exec(url.trim());
5575
- const path15 = match ? match[1] : url.trim();
5576
- const stripped = path15.replace(/\/+$/, "");
5632
+ const path16 = match ? match[1] : url.trim();
5633
+ const stripped = path16.replace(/\/+$/, "");
5577
5634
  return stripped || "/";
5578
5635
  }
5579
5636
 
@@ -5610,7 +5667,7 @@ function loadSnapshotsForRun(db, runId) {
5610
5667
  }
5611
5668
  function loadSnapshotsForRunIds(db, runIds) {
5612
5669
  if (runIds.length === 0) return [];
5613
- const rows = db.select().from(querySnapshots).where(inArray5(querySnapshots.runId, [...runIds])).all();
5670
+ const rows = db.select().from(querySnapshots).where(inArray6(querySnapshots.runId, [...runIds])).all();
5614
5671
  return rows.filter((r) => r.queryId !== null).map((r) => ({
5615
5672
  id: r.id,
5616
5673
  runId: r.runId,
@@ -5860,7 +5917,7 @@ function buildAiReferrals(db, projectId) {
5860
5917
  return { totalSessions: total, totalUsers, bySource, trend, topLandingPages };
5861
5918
  }
5862
5919
  function nonSubresourceReferralPathCondition() {
5863
- return sql3`
5920
+ return sql5`
5864
5921
  LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/_next/static/%'
5865
5922
  AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/assets/%'
5866
5923
  AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/static/%'
@@ -5899,7 +5956,7 @@ function buildServerActivity(db, projectId) {
5899
5956
  const priorStart = new Date(priorStartMs).toISOString();
5900
5957
  const trendStart = new Date(trendStartMs).toISOString();
5901
5958
  const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5902
- db.select({ total: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5959
+ db.select({ total: sql5`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5903
5960
  and6(
5904
5961
  eq13(crawlerEventsHourly.projectId, projectId),
5905
5962
  eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
@@ -5909,7 +5966,7 @@ function buildServerActivity(db, projectId) {
5909
5966
  ).get()?.total ?? 0
5910
5967
  );
5911
5968
  const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5912
- db.select({ total: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5969
+ db.select({ total: sql5`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5913
5970
  and6(
5914
5971
  eq13(crawlerEventsHourly.projectId, projectId),
5915
5972
  ne(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
@@ -5919,7 +5976,7 @@ function buildServerActivity(db, projectId) {
5919
5976
  ).get()?.total ?? 0
5920
5977
  );
5921
5978
  const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5922
- db.select({ total: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
5979
+ db.select({ total: sql5`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
5923
5980
  and6(
5924
5981
  eq13(aiReferralEventsHourly.projectId, projectId),
5925
5982
  nonSubresourceReferralPathCondition(),
@@ -5937,7 +5994,7 @@ function buildServerActivity(db, projectId) {
5937
5994
  const crawlerByOperatorRows = db.select({
5938
5995
  operator: crawlerEventsHourly.operator,
5939
5996
  verificationStatus: crawlerEventsHourly.verificationStatus,
5940
- hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5997
+ hits: sql5`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5941
5998
  }).from(crawlerEventsHourly).where(
5942
5999
  and6(
5943
6000
  eq13(crawlerEventsHourly.projectId, projectId),
@@ -5947,7 +6004,7 @@ function buildServerActivity(db, projectId) {
5947
6004
  ).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
5948
6005
  const crawlerByOperatorPriorRows = db.select({
5949
6006
  operator: crawlerEventsHourly.operator,
5950
- hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
6007
+ hits: sql5`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5951
6008
  }).from(crawlerEventsHourly).where(
5952
6009
  and6(
5953
6010
  eq13(crawlerEventsHourly.projectId, projectId),
@@ -5958,7 +6015,7 @@ function buildServerActivity(db, projectId) {
5958
6015
  ).groupBy(crawlerEventsHourly.operator).all();
5959
6016
  const referralByOperatorRows = db.select({
5960
6017
  operator: aiReferralEventsHourly.operator,
5961
- hits: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
6018
+ hits: sql5`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
5962
6019
  }).from(aiReferralEventsHourly).where(
5963
6020
  and6(
5964
6021
  eq13(aiReferralEventsHourly.projectId, projectId),
@@ -5998,8 +6055,8 @@ function buildServerActivity(db, projectId) {
5998
6055
  );
5999
6056
  const topPathsRows = db.select({
6000
6057
  path: crawlerEventsHourly.pathNormalized,
6001
- hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
6002
- operators: sql3`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
6058
+ hits: sql5`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
6059
+ operators: sql5`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
6003
6060
  }).from(crawlerEventsHourly).where(
6004
6061
  and6(
6005
6062
  eq13(crawlerEventsHourly.projectId, projectId),
@@ -6007,7 +6064,7 @@ function buildServerActivity(db, projectId) {
6007
6064
  gte(crawlerEventsHourly.tsHour, headlineStart),
6008
6065
  lte(crawlerEventsHourly.tsHour, headlineEnd)
6009
6066
  )
6010
- ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc6(sql3`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
6067
+ ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc6(sql5`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
6011
6068
  const topCrawledPaths = topPathsRows.map((r) => ({
6012
6069
  path: r.path,
6013
6070
  verifiedHits: Number(r.hits),
@@ -6015,8 +6072,8 @@ function buildServerActivity(db, projectId) {
6015
6072
  }));
6016
6073
  const referralProductsRows = db.select({
6017
6074
  product: aiReferralEventsHourly.product,
6018
- arrivals: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
6019
- landingPaths: sql3`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
6075
+ arrivals: sql5`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
6076
+ landingPaths: sql5`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
6020
6077
  }).from(aiReferralEventsHourly).where(
6021
6078
  and6(
6022
6079
  eq13(aiReferralEventsHourly.projectId, projectId),
@@ -6024,7 +6081,7 @@ function buildServerActivity(db, projectId) {
6024
6081
  gte(aiReferralEventsHourly.tsHour, headlineStart),
6025
6082
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
6026
6083
  )
6027
- ).groupBy(aiReferralEventsHourly.product).orderBy(desc6(sql3`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
6084
+ ).groupBy(aiReferralEventsHourly.product).orderBy(desc6(sql5`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
6028
6085
  const referralProducts = referralProductsRows.map((r) => ({
6029
6086
  product: r.product,
6030
6087
  arrivals: Number(r.arrivals),
@@ -6032,8 +6089,8 @@ function buildServerActivity(db, projectId) {
6032
6089
  }));
6033
6090
  const topReferralRows = db.select({
6034
6091
  path: aiReferralEventsHourly.landingPathNormalized,
6035
- arrivals: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
6036
- products: sql3`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
6092
+ arrivals: sql5`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
6093
+ products: sql5`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
6037
6094
  }).from(aiReferralEventsHourly).where(
6038
6095
  and6(
6039
6096
  eq13(aiReferralEventsHourly.projectId, projectId),
@@ -6041,15 +6098,15 @@ function buildServerActivity(db, projectId) {
6041
6098
  gte(aiReferralEventsHourly.tsHour, headlineStart),
6042
6099
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
6043
6100
  )
6044
- ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc6(sql3`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
6101
+ ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc6(sql5`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
6045
6102
  const topReferralLandingPaths = topReferralRows.map((r) => ({
6046
6103
  path: r.path,
6047
6104
  arrivals: Number(r.arrivals),
6048
6105
  distinctProducts: Number(r.products)
6049
6106
  }));
6050
6107
  const crawlerTrendRows = db.select({
6051
- date: sql3`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
6052
- hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
6108
+ date: sql5`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
6109
+ hits: sql5`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
6053
6110
  }).from(crawlerEventsHourly).where(
6054
6111
  and6(
6055
6112
  eq13(crawlerEventsHourly.projectId, projectId),
@@ -6057,10 +6114,10 @@ function buildServerActivity(db, projectId) {
6057
6114
  gte(crawlerEventsHourly.tsHour, trendStart),
6058
6115
  lte(crawlerEventsHourly.tsHour, headlineEnd)
6059
6116
  )
6060
- ).groupBy(sql3`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
6117
+ ).groupBy(sql5`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
6061
6118
  const referralTrendRows = db.select({
6062
- date: sql3`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
6063
- hits: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
6119
+ date: sql5`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
6120
+ hits: sql5`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
6064
6121
  }).from(aiReferralEventsHourly).where(
6065
6122
  and6(
6066
6123
  eq13(aiReferralEventsHourly.projectId, projectId),
@@ -6068,7 +6125,7 @@ function buildServerActivity(db, projectId) {
6068
6125
  gte(aiReferralEventsHourly.tsHour, trendStart),
6069
6126
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
6070
6127
  )
6071
- ).groupBy(sql3`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
6128
+ ).groupBy(sql5`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
6072
6129
  const dailyTrendMap = /* @__PURE__ */ new Map();
6073
6130
  for (const r of crawlerTrendRows) {
6074
6131
  const e = dailyTrendMap.get(r.date) ?? { verifiedCrawlerHits: 0, referralArrivals: 0 };
@@ -6190,7 +6247,7 @@ function buildInsightList(db, projectId, locationFilter) {
6190
6247
  )
6191
6248
  ).orderBy(desc6(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
6192
6249
  if (recentRunIds.length === 0) return [];
6193
- const rows = db.select().from(insights).where(and6(eq13(insights.projectId, projectId), inArray5(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
6250
+ const rows = db.select().from(insights).where(and6(eq13(insights.projectId, projectId), inArray6(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
6194
6251
  const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
6195
6252
  const flat = rows.filter((r) => !r.dismissed).map((r) => {
6196
6253
  const recommendation = parseJsonColumn(r.recommendation, null);
@@ -6963,7 +7020,7 @@ async function reportRoutes(app) {
6963
7020
  }
6964
7021
 
6965
7022
  // ../api-routes/src/citations.ts
6966
- import { eq as eq14, inArray as inArray6 } from "drizzle-orm";
7023
+ import { eq as eq14, inArray as inArray7 } from "drizzle-orm";
6967
7024
  async function citationRoutes(app) {
6968
7025
  app.get("/projects/:name/citations/visibility", async (request, reply) => {
6969
7026
  const project = resolveProject(app.db, request.params.name);
@@ -6987,7 +7044,7 @@ async function citationRoutes(app) {
6987
7044
  competitorOverlap: querySnapshots.competitorOverlap,
6988
7045
  answerMentioned: querySnapshots.answerMentioned,
6989
7046
  createdAt: querySnapshots.createdAt
6990
- }).from(querySnapshots).where(inArray6(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
7047
+ }).from(querySnapshots).where(inArray7(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
6991
7048
  if (rawSnapshots.length === 0) {
6992
7049
  return reply.send(emptyCitationVisibility("no-runs-yet"));
6993
7050
  }
@@ -7128,7 +7185,7 @@ function normalizeDomain2(domain) {
7128
7185
  }
7129
7186
 
7130
7187
  // ../api-routes/src/composites.ts
7131
- import { eq as eq15, and as and7, desc as desc7, sql as sql4, like, or as or4, inArray as inArray7 } from "drizzle-orm";
7188
+ import { eq as eq15, and as and7, desc as desc7, sql as sql6, like, or as or4, inArray as inArray8 } from "drizzle-orm";
7132
7189
  var TOP_INSIGHT_LIMIT = 5;
7133
7190
  var SEARCH_HIT_HARD_LIMIT = 50;
7134
7191
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -7180,9 +7237,19 @@ async function compositeRoutes(app) {
7180
7237
  const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq15(queries.projectId, project.id)).all();
7181
7238
  const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
7182
7239
  const configuredApiProviders = parseJsonColumn(project.providers, []).filter((p) => !p.startsWith("cdp:"));
7240
+ const projectDomains = effectiveDomains({
7241
+ canonicalDomain: project.canonicalDomain,
7242
+ ownedDomains: parseJsonColumn(project.ownedDomains, [])
7243
+ });
7183
7244
  const scores = {
7245
+ mention: buildMentionCoverage(latestSnapshots, { configuredApiProviders }),
7184
7246
  visibility: buildVisibilityScore(latestSnapshots, { configuredApiProviders }),
7247
+ shareOfVoice: buildShareOfVoice(latestSnapshots, {
7248
+ projectDomains,
7249
+ competitorDomains: competitorRows.map((c) => c.domain)
7250
+ }),
7185
7251
  gapQueries: buildGapQueryScore(latestSnapshots),
7252
+ mentionGaps: buildMentionGapScore(latestSnapshots),
7186
7253
  indexCoverage: buildIndexCoverageScore(app, project.id),
7187
7254
  competitorPressure: buildCompetitorPressureScore(
7188
7255
  latestSnapshots,
@@ -7191,7 +7258,9 @@ async function compositeRoutes(app) {
7191
7258
  ),
7192
7259
  runStatus: buildRunStatusScore(allRuns)
7193
7260
  };
7194
- const movementSummary = buildMovementSummary(latestSnapshots, previousSnapshots);
7261
+ const movementSummary = buildMovementSummary(latestSnapshots, previousSnapshots, {
7262
+ queryLookup: queryLookup.byId
7263
+ });
7195
7264
  const providerScores = buildProviderScores(latestSnapshots);
7196
7265
  const overviewCompetitors = buildOverviewCompetitors(
7197
7266
  latestSnapshots,
@@ -7245,9 +7314,9 @@ async function compositeRoutes(app) {
7245
7314
  and7(
7246
7315
  eq15(queries.projectId, project.id),
7247
7316
  or4(
7248
- sql4`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
7249
- sql4`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
7250
- sql4`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
7317
+ sql6`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
7318
+ sql6`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
7319
+ sql6`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
7251
7320
  like(queries.query, pattern)
7252
7321
  )
7253
7322
  )
@@ -7258,8 +7327,8 @@ async function compositeRoutes(app) {
7258
7327
  or4(
7259
7328
  like(insights.title, pattern),
7260
7329
  like(insights.query, pattern),
7261
- sql4`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
7262
- sql4`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
7330
+ sql6`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
7331
+ sql6`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
7263
7332
  )
7264
7333
  )
7265
7334
  ).orderBy(desc7(insights.createdAt)).limit(limit + 1).all();
@@ -7331,9 +7400,10 @@ function loadSnapshotsByRunIds(app, runIds) {
7331
7400
  provider: querySnapshots.provider,
7332
7401
  model: querySnapshots.model,
7333
7402
  citationState: querySnapshots.citationState,
7403
+ answerMentioned: querySnapshots.answerMentioned,
7334
7404
  competitorOverlap: querySnapshots.competitorOverlap,
7335
7405
  citedDomains: querySnapshots.citedDomains
7336
- }).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all());
7406
+ }).from(querySnapshots).where(inArray8(querySnapshots.runId, [...runIds])).all());
7337
7407
  for (const row of rows) {
7338
7408
  const list = result.get(row.runId) ?? [];
7339
7409
  list.push({
@@ -7341,6 +7411,7 @@ function loadSnapshotsByRunIds(app, runIds) {
7341
7411
  provider: row.provider,
7342
7412
  model: row.model,
7343
7413
  citationState: row.citationState,
7414
+ answerMentioned: row.answerMentioned,
7344
7415
  competitorOverlap: parseJsonColumn(row.competitorOverlap, []),
7345
7416
  citedDomains: parseJsonColumn(row.citedDomains, [])
7346
7417
  });
@@ -7936,6 +8007,18 @@ var routeCatalog = [
7936
8007
  404: { description: "Project not found." }
7937
8008
  }
7938
8009
  },
8010
+ {
8011
+ method: "get",
8012
+ path: "/api/v1/projects/{name}/delete-preview",
8013
+ summary: "Preview the cascade impact of deleting a project",
8014
+ 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).",
8015
+ tags: ["projects"],
8016
+ parameters: [nameParameter],
8017
+ responses: {
8018
+ 200: { description: "Preview of cascade impact." },
8019
+ 404: { description: "Project not found." }
8020
+ }
8021
+ },
7939
8022
  {
7940
8023
  method: "post",
7941
8024
  path: "/api/v1/projects/{name}/locations",
@@ -8099,6 +8182,32 @@ var routeCatalog = [
8099
8182
  200: { description: "Queries appended." }
8100
8183
  }
8101
8184
  },
8185
+ {
8186
+ method: "post",
8187
+ path: "/api/v1/projects/{name}/queries/replace-preview",
8188
+ summary: "Preview the impact of replacing tracked queries",
8189
+ description: "Read-only impact summary backing `canonry query replace --dry-run`. Returns current vs proposed query sets, the added/removed/unchanged diff, and the count of snapshots that would detach (queryId \u2192 NULL; queryText preserved).",
8190
+ tags: ["queries"],
8191
+ parameters: [nameParameter],
8192
+ requestBody: {
8193
+ required: true,
8194
+ content: {
8195
+ "application/json": {
8196
+ schema: {
8197
+ type: "object",
8198
+ required: ["queries"],
8199
+ properties: {
8200
+ queries: stringArraySchema
8201
+ }
8202
+ }
8203
+ }
8204
+ }
8205
+ },
8206
+ responses: {
8207
+ 200: { description: "Replace preview returned." },
8208
+ 404: { description: "Project not found." }
8209
+ }
8210
+ },
8102
8211
  {
8103
8212
  method: "post",
8104
8213
  path: "/api/v1/projects/{name}/queries/generate",
@@ -10941,8 +11050,8 @@ async function openApiRoutes(app, opts = {}) {
10941
11050
  return reply.type("application/json").send(buildOpenApiDocument(opts));
10942
11051
  });
10943
11052
  }
10944
- function buildOperationId(method, path15) {
10945
- const parts = path15.split("/").filter(Boolean).map((part) => {
11053
+ function buildOperationId(method, path16) {
11054
+ const parts = path16.split("/").filter(Boolean).map((part) => {
10946
11055
  if (part.startsWith("{") && part.endsWith("}")) {
10947
11056
  return `by-${part.slice(1, -1)}`;
10948
11057
  }
@@ -11378,7 +11487,7 @@ function formatNotification(row) {
11378
11487
 
11379
11488
  // ../api-routes/src/google.ts
11380
11489
  import crypto14 from "crypto";
11381
- import { eq as eq18, and as and9, desc as desc8, sql as sql5 } from "drizzle-orm";
11490
+ import { eq as eq18, and as and9, desc as desc8, sql as sql7 } from "drizzle-orm";
11382
11491
 
11383
11492
  // ../integration-google/src/constants.ts
11384
11493
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -12604,11 +12713,11 @@ async function googleRoutes(app, opts) {
12604
12713
  const { startDate, endDate, query, page, limit, offset } = request.query;
12605
12714
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
12606
12715
  const conditions = [eq18(gscSearchData.projectId, project.id)];
12607
- if (startDate) conditions.push(sql5`${gscSearchData.date} >= ${startDate}`);
12608
- else if (cutoffDate) conditions.push(sql5`${gscSearchData.date} >= ${cutoffDate}`);
12609
- if (endDate) conditions.push(sql5`${gscSearchData.date} <= ${endDate}`);
12610
- if (query) conditions.push(sql5`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
12611
- if (page) conditions.push(sql5`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
12716
+ if (startDate) conditions.push(sql7`${gscSearchData.date} >= ${startDate}`);
12717
+ else if (cutoffDate) conditions.push(sql7`${gscSearchData.date} >= ${cutoffDate}`);
12718
+ if (endDate) conditions.push(sql7`${gscSearchData.date} <= ${endDate}`);
12719
+ if (query) conditions.push(sql7`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
12720
+ if (page) conditions.push(sql7`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
12612
12721
  const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
12613
12722
  const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
12614
12723
  const rows = app.db.select().from(gscSearchData).where(and9(...conditions)).orderBy(desc8(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
@@ -13853,7 +13962,7 @@ async function cdpRoutes(app, opts) {
13853
13962
 
13854
13963
  // ../api-routes/src/ga.ts
13855
13964
  import crypto16 from "crypto";
13856
- import { eq as eq21, desc as desc10, and as and12, sql as sql6 } from "drizzle-orm";
13965
+ import { eq as eq21, desc as desc10, and as and12, sql as sql8 } from "drizzle-orm";
13857
13966
  function gaLog(level, action, ctx) {
13858
13967
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
13859
13968
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -14150,8 +14259,8 @@ async function ga4Routes(app, opts) {
14150
14259
  tx.delete(gaTrafficSnapshots).where(
14151
14260
  and12(
14152
14261
  eq21(gaTrafficSnapshots.projectId, project.id),
14153
- sql6`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
14154
- sql6`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
14262
+ sql8`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
14263
+ sql8`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
14155
14264
  )
14156
14265
  ).run();
14157
14266
  for (const row of rows) {
@@ -14174,8 +14283,8 @@ async function ga4Routes(app, opts) {
14174
14283
  tx.delete(gaAiReferrals).where(
14175
14284
  and12(
14176
14285
  eq21(gaAiReferrals.projectId, project.id),
14177
- sql6`${gaAiReferrals.date} >= ${summary.periodStart}`,
14178
- sql6`${gaAiReferrals.date} <= ${summary.periodEnd}`
14286
+ sql8`${gaAiReferrals.date} >= ${summary.periodStart}`,
14287
+ sql8`${gaAiReferrals.date} <= ${summary.periodEnd}`
14179
14288
  )
14180
14289
  ).run();
14181
14290
  for (const row of aiReferrals) {
@@ -14200,8 +14309,8 @@ async function ga4Routes(app, opts) {
14200
14309
  tx.delete(gaSocialReferrals).where(
14201
14310
  and12(
14202
14311
  eq21(gaSocialReferrals.projectId, project.id),
14203
- sql6`${gaSocialReferrals.date} >= ${summary.periodStart}`,
14204
- sql6`${gaSocialReferrals.date} <= ${summary.periodEnd}`
14312
+ sql8`${gaSocialReferrals.date} >= ${summary.periodStart}`,
14313
+ sql8`${gaSocialReferrals.date} <= ${summary.periodEnd}`
14205
14314
  )
14206
14315
  ).run();
14207
14316
  for (const row of socialReferrals) {
@@ -14291,11 +14400,11 @@ async function ga4Routes(app, opts) {
14291
14400
  const cutoff = windowCutoff(window);
14292
14401
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
14293
14402
  const snapshotConditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
14294
- if (cutoffDate) snapshotConditions.push(sql6`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
14403
+ if (cutoffDate) snapshotConditions.push(sql8`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
14295
14404
  const aiConditions = [eq21(gaAiReferrals.projectId, project.id)];
14296
- if (cutoffDate) aiConditions.push(sql6`${gaAiReferrals.date} >= ${cutoffDate}`);
14405
+ if (cutoffDate) aiConditions.push(sql8`${gaAiReferrals.date} >= ${cutoffDate}`);
14297
14406
  const socialConditions = [eq21(gaSocialReferrals.projectId, project.id)];
14298
- if (cutoffDate) socialConditions.push(sql6`${gaSocialReferrals.date} >= ${cutoffDate}`);
14407
+ if (cutoffDate) socialConditions.push(sql8`${gaSocialReferrals.date} >= ${cutoffDate}`);
14299
14408
  const windowSummaryRow = cutoffDate ? app.db.select({
14300
14409
  totalSessions: gaTrafficWindowSummaries.totalSessions,
14301
14410
  totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
@@ -14308,9 +14417,9 @@ async function ga4Routes(app, opts) {
14308
14417
  )
14309
14418
  ).get() : null;
14310
14419
  const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
14311
- totalSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
14312
- totalOrganicSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
14313
- totalUsers: sql6`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
14420
+ totalSessions: sql8`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
14421
+ totalOrganicSessions: sql8`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
14422
+ totalUsers: sql8`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
14314
14423
  }).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).get() : null;
14315
14424
  const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
14316
14425
  totalSessions: gaTrafficSummaries.totalSessions,
@@ -14318,38 +14427,38 @@ async function ga4Routes(app, opts) {
14318
14427
  totalUsers: gaTrafficSummaries.totalUsers
14319
14428
  }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
14320
14429
  const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
14321
- totalDirectSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
14430
+ totalDirectSessions: sql8`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
14322
14431
  }).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).get();
14323
14432
  const summaryMeta = app.db.select({
14324
14433
  periodStart: gaTrafficSummaries.periodStart,
14325
14434
  periodEnd: gaTrafficSummaries.periodEnd
14326
14435
  }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
14327
14436
  const rows = app.db.select({
14328
- landingPage: sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
14329
- sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
14330
- organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
14331
- directSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
14332
- users: sql6`SUM(${gaTrafficSnapshots.users})`
14333
- }).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).groupBy(sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql6`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
14437
+ landingPage: sql8`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
14438
+ sessions: sql8`SUM(${gaTrafficSnapshots.sessions})`,
14439
+ organicSessions: sql8`SUM(${gaTrafficSnapshots.organicSessions})`,
14440
+ directSessions: sql8`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
14441
+ users: sql8`SUM(${gaTrafficSnapshots.users})`
14442
+ }).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).groupBy(sql8`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql8`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
14334
14443
  const aiReferralRows = app.db.select({
14335
14444
  source: gaAiReferrals.source,
14336
14445
  medium: gaAiReferrals.medium,
14337
14446
  sourceDimension: gaAiReferrals.sourceDimension,
14338
- sessions: sql6`SUM(${gaAiReferrals.sessions})`,
14339
- users: sql6`SUM(${gaAiReferrals.users})`
14447
+ sessions: sql8`SUM(${gaAiReferrals.sessions})`,
14448
+ users: sql8`SUM(${gaAiReferrals.users})`
14340
14449
  }).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
14341
14450
  const aiReferralLandingPageRows = app.db.select({
14342
14451
  source: gaAiReferrals.source,
14343
14452
  medium: gaAiReferrals.medium,
14344
14453
  sourceDimension: gaAiReferrals.sourceDimension,
14345
- landingPage: sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
14346
- sessions: sql6`SUM(${gaAiReferrals.sessions})`,
14347
- users: sql6`SUM(${gaAiReferrals.users})`
14454
+ landingPage: sql8`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
14455
+ sessions: sql8`SUM(${gaAiReferrals.sessions})`,
14456
+ users: sql8`SUM(${gaAiReferrals.users})`
14348
14457
  }).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(
14349
14458
  gaAiReferrals.source,
14350
14459
  gaAiReferrals.medium,
14351
14460
  gaAiReferrals.sourceDimension,
14352
- sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
14461
+ sql8`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
14353
14462
  ).all();
14354
14463
  const aiReferrals = pickWinningDimension(
14355
14464
  aiReferralRows,
@@ -14360,10 +14469,10 @@ async function ga4Routes(app, opts) {
14360
14469
  (r) => `${r.source}\0${r.medium}\0${r.landingPage}`
14361
14470
  );
14362
14471
  const aiDeduped = app.db.select({
14363
- sessions: sql6`COALESCE(SUM(max_sessions), 0)`,
14364
- users: sql6`COALESCE(SUM(max_users), 0)`
14472
+ sessions: sql8`COALESCE(SUM(max_sessions), 0)`,
14473
+ users: sql8`COALESCE(SUM(max_users), 0)`
14365
14474
  }).from(
14366
- sql6`(
14475
+ sql8`(
14367
14476
  SELECT date, source, medium,
14368
14477
  MAX(dimension_sessions) AS max_sessions,
14369
14478
  MAX(dimension_users) AS max_users
@@ -14372,7 +14481,7 @@ async function ga4Routes(app, opts) {
14372
14481
  SUM(sessions) AS dimension_sessions,
14373
14482
  SUM(users) AS dimension_users
14374
14483
  FROM ga_ai_referrals
14375
- WHERE project_id = ${project.id}${cutoffDate ? sql6` AND date >= ${cutoffDate}` : sql6``}
14484
+ WHERE project_id = ${project.id}${cutoffDate ? sql8` AND date >= ${cutoffDate}` : sql8``}
14376
14485
  GROUP BY date, source, medium, source_dimension
14377
14486
  )
14378
14487
  GROUP BY date, source, medium
@@ -14380,8 +14489,8 @@ async function ga4Routes(app, opts) {
14380
14489
  ).get();
14381
14490
  const aiBySessionRows = app.db.select({
14382
14491
  channelGroup: gaAiReferrals.channelGroup,
14383
- sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
14384
- users: sql6`COALESCE(SUM(${gaAiReferrals.users}), 0)`
14492
+ sessions: sql8`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
14493
+ users: sql8`COALESCE(SUM(${gaAiReferrals.users}), 0)`
14385
14494
  }).from(gaAiReferrals).where(and12(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
14386
14495
  const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
14387
14496
  let aiBySessionUsers = 0;
@@ -14394,12 +14503,12 @@ async function ga4Routes(app, opts) {
14394
14503
  source: gaSocialReferrals.source,
14395
14504
  medium: gaSocialReferrals.medium,
14396
14505
  channelGroup: gaSocialReferrals.channelGroup,
14397
- sessions: sql6`SUM(${gaSocialReferrals.sessions})`,
14398
- users: sql6`SUM(${gaSocialReferrals.users})`
14399
- }).from(gaSocialReferrals).where(and12(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql6`SUM(${gaSocialReferrals.sessions}) DESC`).all();
14506
+ sessions: sql8`SUM(${gaSocialReferrals.sessions})`,
14507
+ users: sql8`SUM(${gaSocialReferrals.users})`
14508
+ }).from(gaSocialReferrals).where(and12(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql8`SUM(${gaSocialReferrals.sessions}) DESC`).all();
14400
14509
  const socialTotals = app.db.select({
14401
- sessions: sql6`SUM(${gaSocialReferrals.sessions})`,
14402
- users: sql6`SUM(${gaSocialReferrals.users})`
14510
+ sessions: sql8`SUM(${gaSocialReferrals.sessions})`,
14511
+ users: sql8`SUM(${gaSocialReferrals.users})`
14403
14512
  }).from(gaSocialReferrals).where(and12(...socialConditions)).get();
14404
14513
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
14405
14514
  const total = summaryRow?.totalSessions ?? 0;
@@ -14482,21 +14591,21 @@ async function ga4Routes(app, opts) {
14482
14591
  requireGa4Connection(opts, project.name, project.canonicalDomain);
14483
14592
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
14484
14593
  const conditions = [eq21(gaAiReferrals.projectId, project.id)];
14485
- if (cutoffDate) conditions.push(sql6`${gaAiReferrals.date} >= ${cutoffDate}`);
14594
+ if (cutoffDate) conditions.push(sql8`${gaAiReferrals.date} >= ${cutoffDate}`);
14486
14595
  const rows = app.db.select({
14487
14596
  date: gaAiReferrals.date,
14488
14597
  source: gaAiReferrals.source,
14489
14598
  medium: gaAiReferrals.medium,
14490
- landingPage: sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
14599
+ landingPage: sql8`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
14491
14600
  sourceDimension: gaAiReferrals.sourceDimension,
14492
- sessions: sql6`SUM(${gaAiReferrals.sessions})`,
14493
- users: sql6`SUM(${gaAiReferrals.users})`
14601
+ sessions: sql8`SUM(${gaAiReferrals.sessions})`,
14602
+ users: sql8`SUM(${gaAiReferrals.users})`
14494
14603
  }).from(gaAiReferrals).where(and12(...conditions)).groupBy(
14495
14604
  gaAiReferrals.date,
14496
14605
  gaAiReferrals.source,
14497
14606
  gaAiReferrals.medium,
14498
14607
  gaAiReferrals.sourceDimension,
14499
- sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
14608
+ sql8`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
14500
14609
  ).orderBy(gaAiReferrals.date).all();
14501
14610
  return rows;
14502
14611
  });
@@ -14505,7 +14614,7 @@ async function ga4Routes(app, opts) {
14505
14614
  requireGa4Connection(opts, project.name, project.canonicalDomain);
14506
14615
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
14507
14616
  const conditions = [eq21(gaSocialReferrals.projectId, project.id)];
14508
- if (cutoffDate) conditions.push(sql6`${gaSocialReferrals.date} >= ${cutoffDate}`);
14617
+ if (cutoffDate) conditions.push(sql8`${gaSocialReferrals.date} >= ${cutoffDate}`);
14509
14618
  const rows = app.db.select({
14510
14619
  date: gaSocialReferrals.date,
14511
14620
  source: gaSocialReferrals.source,
@@ -14526,10 +14635,10 @@ async function ga4Routes(app, opts) {
14526
14635
  d.setDate(d.getDate() - n);
14527
14636
  return fmt(d);
14528
14637
  };
14529
- const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(
14638
+ const sumSocial = (from, to) => app.db.select({ sessions: sql8`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(
14530
14639
  eq21(gaSocialReferrals.projectId, project.id),
14531
- sql6`${gaSocialReferrals.date} >= ${from}`,
14532
- sql6`${gaSocialReferrals.date} < ${to}`
14640
+ sql8`${gaSocialReferrals.date} >= ${from}`,
14641
+ sql8`${gaSocialReferrals.date} < ${to}`
14533
14642
  )).get();
14534
14643
  const current7d = sumSocial(daysAgo2(7), fmt(today));
14535
14644
  const prev7d = sumSocial(daysAgo2(14), daysAgo2(7));
@@ -14538,19 +14647,19 @@ async function ga4Routes(app, opts) {
14538
14647
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
14539
14648
  const sourceCurrent = app.db.select({
14540
14649
  source: gaSocialReferrals.source,
14541
- sessions: sql6`SUM(${gaSocialReferrals.sessions})`
14650
+ sessions: sql8`SUM(${gaSocialReferrals.sessions})`
14542
14651
  }).from(gaSocialReferrals).where(and12(
14543
14652
  eq21(gaSocialReferrals.projectId, project.id),
14544
- sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
14545
- sql6`${gaSocialReferrals.date} < ${fmt(today)}`
14653
+ sql8`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
14654
+ sql8`${gaSocialReferrals.date} < ${fmt(today)}`
14546
14655
  )).groupBy(gaSocialReferrals.source).all();
14547
14656
  const sourcePrev = app.db.select({
14548
14657
  source: gaSocialReferrals.source,
14549
- sessions: sql6`SUM(${gaSocialReferrals.sessions})`
14658
+ sessions: sql8`SUM(${gaSocialReferrals.sessions})`
14550
14659
  }).from(gaSocialReferrals).where(and12(
14551
14660
  eq21(gaSocialReferrals.projectId, project.id),
14552
- sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
14553
- sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`
14661
+ sql8`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
14662
+ sql8`${gaSocialReferrals.date} < ${daysAgo2(7)}`
14554
14663
  )).groupBy(gaSocialReferrals.source).all();
14555
14664
  const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
14556
14665
  let biggestMover = null;
@@ -14589,16 +14698,16 @@ async function ga4Routes(app, opts) {
14589
14698
  return fmt(d);
14590
14699
  };
14591
14700
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
14592
- const sumTotal = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14593
- const sumOrganic = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14594
- const sumDirect = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
14595
- const sumAi = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14701
+ const sumTotal = (from, to) => app.db.select({ sessions: sql8`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql8`${gaTrafficSnapshots.date} >= ${from}`, sql8`${gaTrafficSnapshots.date} < ${to}`)).get();
14702
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql8`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql8`${gaTrafficSnapshots.date} >= ${from}`, sql8`${gaTrafficSnapshots.date} < ${to}`)).get();
14703
+ const sumDirect = (from, to) => app.db.select({ sessions: sql8`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql8`${gaTrafficSnapshots.date} >= ${from}`, sql8`${gaTrafficSnapshots.date} < ${to}`)).get();
14704
+ const sumAi = (from, to) => app.db.select({ sessions: sql8`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14596
14705
  eq21(gaAiReferrals.projectId, project.id),
14597
- sql6`${gaAiReferrals.date} >= ${from}`,
14598
- sql6`${gaAiReferrals.date} < ${to}`,
14706
+ sql8`${gaAiReferrals.date} >= ${from}`,
14707
+ sql8`${gaAiReferrals.date} < ${to}`,
14599
14708
  eq21(gaAiReferrals.sourceDimension, "session")
14600
14709
  )).get();
14601
- const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${from}`, sql6`${gaSocialReferrals.date} < ${to}`)).get();
14710
+ const sumSocial = (from, to) => app.db.select({ sessions: sql8`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${from}`, sql8`${gaSocialReferrals.date} < ${to}`)).get();
14602
14711
  const todayStr = fmt(today);
14603
14712
  const buildTrend = (sum) => {
14604
14713
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -14607,16 +14716,16 @@ async function ga4Routes(app, opts) {
14607
14716
  const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
14608
14717
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
14609
14718
  };
14610
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14719
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql8`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14611
14720
  eq21(gaAiReferrals.projectId, project.id),
14612
- sql6`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
14613
- sql6`${gaAiReferrals.date} < ${todayStr}`,
14721
+ sql8`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
14722
+ sql8`${gaAiReferrals.date} < ${todayStr}`,
14614
14723
  eq21(gaAiReferrals.sourceDimension, "session")
14615
14724
  )).groupBy(gaAiReferrals.source).all();
14616
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14725
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql8`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
14617
14726
  eq21(gaAiReferrals.projectId, project.id),
14618
- sql6`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
14619
- sql6`${gaAiReferrals.date} < ${daysAgo2(7)}`,
14727
+ sql8`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
14728
+ sql8`${gaAiReferrals.date} < ${daysAgo2(7)}`,
14620
14729
  eq21(gaAiReferrals.sourceDimension, "session")
14621
14730
  )).groupBy(gaAiReferrals.source).all();
14622
14731
  const findBiggestMover = (current, prev) => {
@@ -14633,8 +14742,8 @@ async function ga4Routes(app, opts) {
14633
14742
  }
14634
14743
  return mover;
14635
14744
  };
14636
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql6`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
14637
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
14745
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql8`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql8`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
14746
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql8`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql8`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
14638
14747
  return {
14639
14748
  total: buildTrend(sumTotal),
14640
14749
  organic: buildTrend(sumOrganic),
@@ -14650,12 +14759,12 @@ async function ga4Routes(app, opts) {
14650
14759
  requireGa4Connection(opts, project.name, project.canonicalDomain);
14651
14760
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
14652
14761
  const conditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
14653
- if (cutoffDate) conditions.push(sql6`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
14762
+ if (cutoffDate) conditions.push(sql8`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
14654
14763
  const rows = app.db.select({
14655
14764
  date: gaTrafficSnapshots.date,
14656
- sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
14657
- organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
14658
- users: sql6`SUM(${gaTrafficSnapshots.users})`
14765
+ sessions: sql8`SUM(${gaTrafficSnapshots.sessions})`,
14766
+ organicSessions: sql8`SUM(${gaTrafficSnapshots.organicSessions})`,
14767
+ users: sql8`SUM(${gaTrafficSnapshots.users})`
14659
14768
  }).from(gaTrafficSnapshots).where(and12(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
14660
14769
  return rows.map((r) => ({
14661
14770
  date: r.date,
@@ -14668,11 +14777,11 @@ async function ga4Routes(app, opts) {
14668
14777
  const project = resolveProject(app.db, request.params.name);
14669
14778
  requireGa4Connection(opts, project.name, project.canonicalDomain);
14670
14779
  const trafficPages = app.db.select({
14671
- landingPage: sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
14672
- sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
14673
- organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
14674
- users: sql6`SUM(${gaTrafficSnapshots.users})`
14675
- }).from(gaTrafficSnapshots).where(eq21(gaTrafficSnapshots.projectId, project.id)).groupBy(sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql6`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
14780
+ landingPage: sql8`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
14781
+ sessions: sql8`SUM(${gaTrafficSnapshots.sessions})`,
14782
+ organicSessions: sql8`SUM(${gaTrafficSnapshots.organicSessions})`,
14783
+ users: sql8`SUM(${gaTrafficSnapshots.users})`
14784
+ }).from(gaTrafficSnapshots).where(eq21(gaTrafficSnapshots.projectId, project.id)).groupBy(sql8`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql8`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
14676
14785
  return {
14677
14786
  pages: trafficPages.map((r) => ({
14678
14787
  landingPage: r.landingPage,
@@ -14888,10 +14997,10 @@ function buildAuthErrorMessage(res, responseText) {
14888
14997
  }
14889
14998
  return "WordPress credentials are invalid or lack permission for this action";
14890
14999
  }
14891
- async function fetchJson(connection, siteUrl, path15, init) {
15000
+ async function fetchJson(connection, siteUrl, path16, init) {
14892
15001
  if (siteUrl.startsWith("http:")) {
14893
15002
  }
14894
- const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path15}`, {
15003
+ const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path16}`, {
14895
15004
  ...init,
14896
15005
  headers: {
14897
15006
  "Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
@@ -16309,7 +16418,7 @@ async function wordpressRoutes(app, opts) {
16309
16418
 
16310
16419
  // ../api-routes/src/backlinks.ts
16311
16420
  import crypto18 from "crypto";
16312
- import { and as and14, asc as asc2, desc as desc11, eq as eq22, sql as sql7 } from "drizzle-orm";
16421
+ import { and as and14, asc as asc2, desc as desc11, eq as eq22, sql as sql9 } from "drizzle-orm";
16313
16422
 
16314
16423
  // ../integration-commoncrawl/src/constants.ts
16315
16424
  import os3 from "os";
@@ -16593,7 +16702,7 @@ async function queryBacklinks(opts) {
16593
16702
  const reversed = opts.targets.map(reverseDomain);
16594
16703
  const targetList = reversed.map(quote).join(", ");
16595
16704
  const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
16596
- const sql14 = `
16705
+ const sql16 = `
16597
16706
  WITH vertices AS (
16598
16707
  SELECT * FROM read_csv(
16599
16708
  ${quote(opts.vertexPath)},
@@ -16629,7 +16738,7 @@ async function queryBacklinks(opts) {
16629
16738
  const conn = await instance.connect();
16630
16739
  let rows;
16631
16740
  try {
16632
- const reader = await conn.runAndReadAll(sql14);
16741
+ const reader = await conn.runAndReadAll(sql16);
16633
16742
  rows = reader.getRowObjects();
16634
16743
  } finally {
16635
16744
  conn.disconnectSync?.();
@@ -16805,12 +16914,12 @@ function computeFilteredSummary(db, base) {
16805
16914
  );
16806
16915
  const filteredCondition = and14(baseDomainCondition, backlinkCrawlerExclusionClause());
16807
16916
  const unfilteredAgg = db.select({
16808
- count: sql7`count(*)`,
16809
- total: sql7`coalesce(sum(${backlinkDomains.numHosts}), 0)`
16917
+ count: sql9`count(*)`,
16918
+ total: sql9`coalesce(sum(${backlinkDomains.numHosts}), 0)`
16810
16919
  }).from(backlinkDomains).where(baseDomainCondition).get();
16811
16920
  const filteredAgg = db.select({
16812
- count: sql7`count(*)`,
16813
- total: sql7`coalesce(sum(${backlinkDomains.numHosts}), 0)`
16921
+ count: sql9`count(*)`,
16922
+ total: sql9`coalesce(sum(${backlinkDomains.numHosts}), 0)`
16814
16923
  }).from(backlinkDomains).where(filteredCondition).get();
16815
16924
  const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc11(backlinkDomains.numHosts)).limit(10).all();
16816
16925
  const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
@@ -16984,7 +17093,7 @@ async function backlinksRoutes(app, opts) {
16984
17093
  eq22(backlinkDomains.release, targetRelease)
16985
17094
  );
16986
17095
  const domainCondition = excludeCrawlers ? and14(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
16987
- const totalRow = app.db.select({ count: sql7`count(*)` }).from(backlinkDomains).where(domainCondition).get();
17096
+ const totalRow = app.db.select({ count: sql9`count(*)` }).from(backlinkDomains).where(domainCondition).get();
16988
17097
  const rows = app.db.select({
16989
17098
  linkingDomain: backlinkDomains.linkingDomain,
16990
17099
  numHosts: backlinkDomains.numHosts
@@ -17019,7 +17128,7 @@ async function backlinksRoutes(app, opts) {
17019
17128
 
17020
17129
  // ../api-routes/src/traffic.ts
17021
17130
  import crypto20 from "crypto";
17022
- import { and as and15, desc as desc12, eq as eq23, gte as gte2, lte as lte2, sql as sql8 } from "drizzle-orm";
17131
+ import { and as and15, desc as desc12, eq as eq23, gte as gte2, lte as lte2, sql as sql10 } from "drizzle-orm";
17023
17132
 
17024
17133
  // ../integration-cloud-run/src/auth.ts
17025
17134
  import crypto19 from "crypto";
@@ -17518,8 +17627,8 @@ var ASSET_PATH_PREFIXES = [
17518
17627
  "/img/",
17519
17628
  "/static/"
17520
17629
  ];
17521
- function normalizeTrafficPathPattern(path15) {
17522
- const cleanPath = path15.trim() || "/";
17630
+ function normalizeTrafficPathPattern(path16) {
17631
+ const cleanPath = path16.trim() || "/";
17523
17632
  const pathOnly = cleanPath.split("?")[0] || "/";
17524
17633
  const segments = pathOnly.split("/").map((segment) => {
17525
17634
  if (!segment) return segment;
@@ -17568,8 +17677,8 @@ function resolveAiReferralLandingPath(event, evidenceType) {
17568
17677
  }
17569
17678
  return normalizeTrafficPathPattern(event.path);
17570
17679
  }
17571
- function isLikelySubresourcePath(path15) {
17572
- const cleanPath = path15.split("?")[0] || "/";
17680
+ function isLikelySubresourcePath(path16) {
17681
+ const cleanPath = path16.split("?")[0] || "/";
17573
17682
  return ASSET_PATH_PREFIXES.some((prefix) => cleanPath.startsWith(prefix)) || ASSET_EXTENSION_PATTERN.test(cleanPath);
17574
17683
  }
17575
17684
  function actorKey(event) {
@@ -17755,11 +17864,11 @@ function buildEventId2(event) {
17755
17864
  function normalizeWordpressTrafficEvent(event) {
17756
17865
  if (!event.observed_at) return null;
17757
17866
  if (typeof event.id !== "number" || !Number.isFinite(event.id)) return null;
17758
- const path15 = event.path?.trim();
17759
- if (!path15) return null;
17867
+ const path16 = event.path?.trim();
17868
+ if (!path16) return null;
17760
17869
  const queryString = trimOrNull(event.query_string);
17761
17870
  const host = trimOrNull(event.host);
17762
- const requestUrl = host ? `https://${host}${path15}${queryString ? `?${queryString}` : ""}` : `${path15}${queryString ? `?${queryString}` : ""}`;
17871
+ const requestUrl = host ? `https://${host}${path16}${queryString ? `?${queryString}` : ""}` : `${path16}${queryString ? `?${queryString}` : ""}`;
17763
17872
  return {
17764
17873
  sourceType: TrafficSourceTypes.wordpress,
17765
17874
  evidenceKind: TrafficEvidenceKinds["raw-request"],
@@ -17769,7 +17878,7 @@ function normalizeWordpressTrafficEvent(event) {
17769
17878
  method: trimOrNull(event.method),
17770
17879
  requestUrl,
17771
17880
  host,
17772
- path: path15,
17881
+ path: path16,
17773
17882
  queryString,
17774
17883
  status: typeof event.status === "number" && Number.isFinite(event.status) ? event.status : null,
17775
17884
  userAgent: trimOrNull(event.user_agent),
@@ -17933,15 +18042,15 @@ function stringLabels(input) {
17933
18042
  );
17934
18043
  }
17935
18044
  function normalizeVercelLogRow(row) {
17936
- const path15 = row.requestPath;
17937
- if (!path15) return null;
18045
+ const path16 = row.requestPath;
18046
+ if (!path16) return null;
17938
18047
  const observedAt = row.timestamp;
17939
18048
  if (!observedAt) return null;
17940
18049
  const requestId = row.requestId;
17941
18050
  if (!requestId) return null;
17942
18051
  const host = emptyToNull(row.domain);
17943
18052
  const queryString = serializeSearchParams(row.requestSearchParams);
17944
- const requestUrl = host ? `https://${host}${path15}${queryString ? `?${queryString}` : ""}` : null;
18053
+ const requestUrl = host ? `https://${host}${path16}${queryString ? `?${queryString}` : ""}` : null;
17945
18054
  return {
17946
18055
  sourceType: TrafficSourceTypes.vercel,
17947
18056
  evidenceKind: TrafficEvidenceKinds["raw-request"],
@@ -17951,7 +18060,7 @@ function normalizeVercelLogRow(row) {
17951
18060
  method: row.requestMethod ?? null,
17952
18061
  requestUrl,
17953
18062
  host,
17954
- path: path15,
18063
+ path: path16,
17955
18064
  queryString,
17956
18065
  status: resolveStatus(row),
17957
18066
  userAgent: emptyToNull(row.clientUserAgent),
@@ -18757,7 +18866,7 @@ async function trafficRoutes(app, opts) {
18757
18866
  crawlerEventsHourly.status
18758
18867
  ],
18759
18868
  set: {
18760
- hits: sql8`${crawlerEventsHourly.hits} + ${bucket.hits}`,
18869
+ hits: sql10`${crawlerEventsHourly.hits} + ${bucket.hits}`,
18761
18870
  sampledUserAgent: bucket.sampledUserAgent,
18762
18871
  updatedAt: finishedAt
18763
18872
  }
@@ -18792,7 +18901,7 @@ async function trafficRoutes(app, opts) {
18792
18901
  aiReferralEventsHourly.status
18793
18902
  ],
18794
18903
  set: {
18795
- sessionsOrHits: sql8`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
18904
+ sessionsOrHits: sql10`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
18796
18905
  updatedAt: finishedAt
18797
18906
  }
18798
18907
  }).run();
@@ -19044,19 +19153,19 @@ async function trafficRoutes(app, opts) {
19044
19153
  return response;
19045
19154
  });
19046
19155
  function buildSourceDetail(projectId, row, since) {
19047
- const crawlerTotals = app.db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
19156
+ const crawlerTotals = app.db.select({ total: sql10`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
19048
19157
  and15(
19049
19158
  eq23(crawlerEventsHourly.sourceId, row.id),
19050
19159
  gte2(crawlerEventsHourly.tsHour, since)
19051
19160
  )
19052
19161
  ).get();
19053
- const aiTotals = app.db.select({ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
19162
+ const aiTotals = app.db.select({ total: sql10`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
19054
19163
  and15(
19055
19164
  eq23(aiReferralEventsHourly.sourceId, row.id),
19056
19165
  gte2(aiReferralEventsHourly.tsHour, since)
19057
19166
  )
19058
19167
  ).get();
19059
- const sampleTotals = app.db.select({ total: sql8`COUNT(*)` }).from(rawEventSamples).where(
19168
+ const sampleTotals = app.db.select({ total: sql10`COUNT(*)` }).from(rawEventSamples).where(
19060
19169
  and15(
19061
19170
  eq23(rawEventSamples.sourceId, row.id),
19062
19171
  gte2(rawEventSamples.ts, since)
@@ -19158,7 +19267,7 @@ async function trafficRoutes(app, opts) {
19158
19267
  ];
19159
19268
  if (sourceIdParam) crawlerFilters.push(eq23(crawlerEventsHourly.sourceId, sourceIdParam));
19160
19269
  const crawlerWhere = and15(...crawlerFilters);
19161
- const total = app.db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
19270
+ const total = app.db.select({ total: sql10`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
19162
19271
  crawlerTotal = Number(total?.total ?? 0);
19163
19272
  const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc12(crawlerEventsHourly.tsHour)).limit(limit).all();
19164
19273
  for (const r of rows) {
@@ -19183,7 +19292,7 @@ async function trafficRoutes(app, opts) {
19183
19292
  ];
19184
19293
  if (sourceIdParam) aiFilters.push(eq23(aiReferralEventsHourly.sourceId, sourceIdParam));
19185
19294
  const aiWhere = and15(...aiFilters);
19186
- const total = app.db.select({ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
19295
+ const total = app.db.select({ total: sql10`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
19187
19296
  aiReferralTotal = Number(total?.total ?? 0);
19188
19297
  const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc12(aiReferralEventsHourly.tsHour)).limit(limit).all();
19189
19298
  for (const r of rows) {
@@ -19216,6 +19325,75 @@ async function trafficRoutes(app, opts) {
19216
19325
  });
19217
19326
  }
19218
19327
 
19328
+ // ../api-routes/src/doctor/checks/agent.ts
19329
+ import fs6 from "fs";
19330
+ import path7 from "path";
19331
+ var REQUIRED_SKILLS = ["canonry", "aero"];
19332
+ var skillsInstalledCheck = {
19333
+ id: "agent.skills.installed",
19334
+ category: CheckCategories.agent,
19335
+ scope: CheckScopes.global,
19336
+ title: "Agent skills installed (~/.claude/skills/)",
19337
+ run: () => {
19338
+ const home = process.env.HOME;
19339
+ if (!home) {
19340
+ return {
19341
+ status: CheckStatuses.skipped,
19342
+ code: "agent.skills.no-home",
19343
+ summary: "Cannot determine $HOME \u2014 skip skills filesystem check.",
19344
+ remediation: null
19345
+ };
19346
+ }
19347
+ const skillsBase = path7.join(home, ".claude", "skills");
19348
+ const installed = [];
19349
+ const missing = [];
19350
+ for (const name of REQUIRED_SKILLS) {
19351
+ const dir = path7.join(skillsBase, name);
19352
+ if (isInstalled(dir)) installed.push(name);
19353
+ else missing.push(name);
19354
+ }
19355
+ const details = {
19356
+ checkedPath: skillsBase,
19357
+ installed,
19358
+ missing
19359
+ };
19360
+ if (missing.length === 0) {
19361
+ return {
19362
+ status: CheckStatuses.ok,
19363
+ code: "agent.skills.installed",
19364
+ summary: `Both canonry and aero skills are installed in ${skillsBase}.`,
19365
+ remediation: null,
19366
+ details
19367
+ };
19368
+ }
19369
+ if (installed.length === 0) {
19370
+ return {
19371
+ status: CheckStatuses.warn,
19372
+ code: "agent.skills.not-installed",
19373
+ 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.",
19374
+ remediation: "Run `canonry skills install --dir ~` (or `canonry skills install --user`) to install both skills to ~/.claude/skills/ and ~/.codex/skills/.",
19375
+ details
19376
+ };
19377
+ }
19378
+ return {
19379
+ status: CheckStatuses.warn,
19380
+ code: "agent.skills.partial",
19381
+ summary: `Only ${installed.length} of ${REQUIRED_SKILLS.length} agent skills are installed (${installed.join(", ")}); ${missing.join(", ")} missing.`,
19382
+ remediation: `Run \`canonry skills install ${missing.join(" ")} --dir ~\` to fill the gap.`,
19383
+ details
19384
+ };
19385
+ }
19386
+ };
19387
+ function isInstalled(dir) {
19388
+ try {
19389
+ if (!fs6.existsSync(dir)) return false;
19390
+ return fs6.existsSync(path7.join(dir, "SKILL.md"));
19391
+ } catch {
19392
+ return false;
19393
+ }
19394
+ }
19395
+ var AGENT_CHECKS = [skillsInstalledCheck];
19396
+
19219
19397
  // ../api-routes/src/doctor/checks/bing-auth.ts
19220
19398
  var BING_AUTH_CHECKS = [
19221
19399
  {
@@ -19837,7 +20015,7 @@ var providersConfiguredCheck = {
19837
20015
  var PROVIDERS_CHECKS = [providersConfiguredCheck];
19838
20016
 
19839
20017
  // ../api-routes/src/doctor/checks/traffic-source.ts
19840
- import { and as and16, eq as eq24, gte as gte3, ne as ne3, sql as sql9 } from "drizzle-orm";
20018
+ import { and as and16, eq as eq24, gte as gte3, ne as ne3, sql as sql11 } from "drizzle-orm";
19841
20019
  var RECENT_DATA_WARN_DAYS = 7;
19842
20020
  var RECENT_DATA_FAIL_DAYS = 30;
19843
20021
  function skippedNoProject2() {
@@ -19931,7 +20109,7 @@ var recentDataCheck = {
19931
20109
  const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
19932
20110
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
19933
20111
  const recentCrawlers = Number(
19934
- ctx.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
20112
+ ctx.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
19935
20113
  and16(
19936
20114
  eq24(crawlerEventsHourly.projectId, ctx.project.id),
19937
20115
  gte3(crawlerEventsHourly.tsHour, warnCutoff)
@@ -19939,7 +20117,7 @@ var recentDataCheck = {
19939
20117
  ).get()?.total ?? 0
19940
20118
  );
19941
20119
  const recentReferrals = Number(
19942
- ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
20120
+ ctx.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
19943
20121
  and16(
19944
20122
  eq24(aiReferralEventsHourly.projectId, ctx.project.id),
19945
20123
  gte3(aiReferralEventsHourly.tsHour, warnCutoff)
@@ -19955,7 +20133,7 @@ var recentDataCheck = {
19955
20133
  };
19956
20134
  }
19957
20135
  const olderCrawlers = Number(
19958
- ctx.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
20136
+ ctx.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
19959
20137
  and16(
19960
20138
  eq24(crawlerEventsHourly.projectId, ctx.project.id),
19961
20139
  gte3(crawlerEventsHourly.tsHour, failCutoff)
@@ -19963,7 +20141,7 @@ var recentDataCheck = {
19963
20141
  ).get()?.total ?? 0
19964
20142
  );
19965
20143
  const olderReferrals = Number(
19966
- ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
20144
+ ctx.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
19967
20145
  and16(
19968
20146
  eq24(aiReferralEventsHourly.projectId, ctx.project.id),
19969
20147
  gte3(aiReferralEventsHourly.tsHour, failCutoff)
@@ -20145,7 +20323,8 @@ var ALL_CHECKS = [
20145
20323
  ...BING_AUTH_CHECKS,
20146
20324
  ...GA_AUTH_CHECKS,
20147
20325
  ...PROVIDERS_CHECKS,
20148
- ...TRAFFIC_SOURCE_CHECKS
20326
+ ...TRAFFIC_SOURCE_CHECKS,
20327
+ ...AGENT_CHECKS
20149
20328
  ];
20150
20329
  var CHECK_BY_ID = Object.fromEntries(
20151
20330
  ALL_CHECKS.map((check) => [check.id, check])
@@ -20262,7 +20441,7 @@ async function doctorRoutes(app, opts) {
20262
20441
 
20263
20442
  // ../api-routes/src/discovery/routes.ts
20264
20443
  import crypto21 from "crypto";
20265
- import { and as and17, desc as desc13, eq as eq25, gte as gte4, inArray as inArray8 } from "drizzle-orm";
20444
+ import { and as and17, desc as desc13, eq as eq25, gte as gte4, inArray as inArray9 } from "drizzle-orm";
20266
20445
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
20267
20446
  async function discoveryRoutes(app, opts) {
20268
20447
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -20297,7 +20476,7 @@ async function discoveryRoutes(app, opts) {
20297
20476
  const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and17(
20298
20477
  eq25(discoverySessions.projectId, project.id),
20299
20478
  eq25(discoverySessions.icpDescription, icpDescription),
20300
- inArray8(discoverySessions.status, [
20479
+ inArray9(discoverySessions.status, [
20301
20480
  DiscoverySessionStatuses.queued,
20302
20481
  DiscoverySessionStatuses.seeding,
20303
20482
  DiscoverySessionStatuses.probing
@@ -22450,7 +22629,7 @@ var localAdapter = {
22450
22629
  };
22451
22630
 
22452
22631
  // ../provider-cdp/src/adapter.ts
22453
- import path8 from "path";
22632
+ import path9 from "path";
22454
22633
  import os4 from "os";
22455
22634
 
22456
22635
  // ../provider-cdp/src/connection.ts
@@ -22815,12 +22994,12 @@ function sleep2(ms) {
22815
22994
  }
22816
22995
 
22817
22996
  // ../provider-cdp/src/screenshot.ts
22818
- import fs6 from "fs";
22819
- import path7 from "path";
22997
+ import fs7 from "fs";
22998
+ import path8 from "path";
22820
22999
  async function captureElementScreenshot(client, selector, outputPath) {
22821
- const dir = path7.dirname(outputPath);
22822
- if (!fs6.existsSync(dir)) {
22823
- fs6.mkdirSync(dir, { recursive: true });
23000
+ const dir = path8.dirname(outputPath);
23001
+ if (!fs7.existsSync(dir)) {
23002
+ fs7.mkdirSync(dir, { recursive: true });
22824
23003
  }
22825
23004
  let clip;
22826
23005
  try {
@@ -22854,7 +23033,7 @@ async function captureElementScreenshot(client, selector, outputPath) {
22854
23033
  }
22855
23034
  const { data } = await client.Page.captureScreenshot(screenshotParams);
22856
23035
  const buffer = Buffer.from(data, "base64");
22857
- fs6.writeFileSync(outputPath, buffer);
23036
+ fs7.writeFileSync(outputPath, buffer);
22858
23037
  return outputPath;
22859
23038
  }
22860
23039
 
@@ -22915,7 +23094,7 @@ function getConnection(config) {
22915
23094
  return conn;
22916
23095
  }
22917
23096
  function getScreenshotDir2() {
22918
- return path8.join(os4.homedir(), ".canonry", "screenshots");
23097
+ return path9.join(os4.homedir(), ".canonry", "screenshots");
22919
23098
  }
22920
23099
  var cdpChatgptAdapter = {
22921
23100
  name: "cdp:chatgpt",
@@ -22979,7 +23158,7 @@ var cdpChatgptAdapter = {
22979
23158
  const answerText = await target.extractAnswer(client);
22980
23159
  const groundingSources = await target.extractCitations(client);
22981
23160
  const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
22982
- const screenshotPath = path8.join(getScreenshotDir2(), `${screenshotId}.png`);
23161
+ const screenshotPath = path9.join(getScreenshotDir2(), `${screenshotId}.png`);
22983
23162
  let capturedScreenshotPath;
22984
23163
  try {
22985
23164
  capturedScreenshotPath = await captureElementScreenshot(
@@ -23612,10 +23791,10 @@ function removeWordpressConnection(config, projectName) {
23612
23791
 
23613
23792
  // src/job-runner.ts
23614
23793
  import crypto24 from "crypto";
23615
- import fs7 from "fs";
23616
- import path9 from "path";
23794
+ import fs8 from "fs";
23795
+ import path10 from "path";
23617
23796
  import os5 from "os";
23618
- import { and as and18, eq as eq27, inArray as inArray9, sql as sql10 } from "drizzle-orm";
23797
+ import { and as and18, eq as eq27, inArray as inArray10, sql as sql12 } from "drizzle-orm";
23619
23798
 
23620
23799
  // src/run-telemetry.ts
23621
23800
  import crypto23 from "crypto";
@@ -23960,7 +24139,7 @@ var JobRunner = class {
23960
24139
  this.registry = registry;
23961
24140
  }
23962
24141
  recoverStaleRuns() {
23963
- const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray9(runs.status, ["running", "queued"])).all();
24142
+ const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray10(runs.status, ["running", "queued"])).all();
23964
24143
  if (stale.length === 0) return;
23965
24144
  const now = (/* @__PURE__ */ new Date()).toISOString();
23966
24145
  for (const run of stale) {
@@ -24023,7 +24202,7 @@ var JobRunner = class {
24023
24202
  }
24024
24203
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
24025
24204
  const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
24026
- 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();
24205
+ projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and18(eq27(queries.projectId, projectId), inArray10(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
24027
24206
  const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
24028
24207
  const competitorDomains = projectCompetitors.map((c) => c.domain);
24029
24208
  const allDomains = effectiveDomains({
@@ -24106,12 +24285,12 @@ var JobRunner = class {
24106
24285
  allBrandNames
24107
24286
  );
24108
24287
  let screenshotRelPath = null;
24109
- if (raw.screenshotPath && fs7.existsSync(raw.screenshotPath)) {
24288
+ if (raw.screenshotPath && fs8.existsSync(raw.screenshotPath)) {
24110
24289
  const snapshotId = crypto24.randomUUID();
24111
- const screenshotDir = path9.join(os5.homedir(), ".canonry", "screenshots", runId);
24112
- if (!fs7.existsSync(screenshotDir)) fs7.mkdirSync(screenshotDir, { recursive: true });
24113
- const destPath = path9.join(screenshotDir, `${snapshotId}.png`);
24114
- fs7.renameSync(raw.screenshotPath, destPath);
24290
+ const screenshotDir = path10.join(os5.homedir(), ".canonry", "screenshots", runId);
24291
+ if (!fs8.existsSync(screenshotDir)) fs8.mkdirSync(screenshotDir, { recursive: true });
24292
+ const destPath = path10.join(screenshotDir, `${snapshotId}.png`);
24293
+ fs8.renameSync(raw.screenshotPath, destPath);
24115
24294
  screenshotRelPath = `${runId}/${snapshotId}.png`;
24116
24295
  this.db.insert(querySnapshots).values({
24117
24296
  id: snapshotId,
@@ -24296,7 +24475,7 @@ var JobRunner = class {
24296
24475
  updatedAt: now
24297
24476
  }).onConflictDoUpdate({
24298
24477
  target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
24299
- set: { count: sql10`${usageCounters.count} + ${count}`, updatedAt: now }
24478
+ set: { count: sql12`${usageCounters.count} + ${count}`, updatedAt: now }
24300
24479
  }).run();
24301
24480
  }
24302
24481
  flushProviderUsage(projectId, providerDispatchCounts) {
@@ -24366,7 +24545,7 @@ function buildPhases(input) {
24366
24545
 
24367
24546
  // src/gsc-sync.ts
24368
24547
  import crypto25 from "crypto";
24369
- import { eq as eq28, and as and19, sql as sql11 } from "drizzle-orm";
24548
+ import { eq as eq28, and as and19, sql as sql13 } from "drizzle-orm";
24370
24549
  var log2 = createLogger("GscSync");
24371
24550
  function formatDate3(d) {
24372
24551
  return d.toISOString().split("T")[0];
@@ -24420,8 +24599,8 @@ async function executeGscSync(db, runId, projectId, opts) {
24420
24599
  db.delete(gscSearchData).where(
24421
24600
  and19(
24422
24601
  eq28(gscSearchData.projectId, projectId),
24423
- sql11`${gscSearchData.date} >= ${startDate}`,
24424
- sql11`${gscSearchData.date} <= ${endDate}`
24602
+ sql13`${gscSearchData.date} >= ${startDate}`,
24603
+ sql13`${gscSearchData.date} <= ${endDate}`
24425
24604
  )
24426
24605
  ).run();
24427
24606
  const batchSize = 500;
@@ -24956,8 +25135,8 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
24956
25135
 
24957
25136
  // src/commoncrawl-sync.ts
24958
25137
  import crypto28 from "crypto";
24959
- import path10 from "path";
24960
- import { and as and21, eq as eq31, sql as sql12 } from "drizzle-orm";
25138
+ import path11 from "path";
25139
+ import { and as and21, eq as eq31, sql as sql14 } from "drizzle-orm";
24961
25140
  var log6 = createLogger("CommonCrawlSync");
24962
25141
  var INSERT_CHUNK_SIZE = 1e4;
24963
25142
  function defaultDeps() {
@@ -24985,9 +25164,9 @@ async function executeReleaseSync(db, syncId, opts) {
24985
25164
  error: null
24986
25165
  }).where(eq31(ccReleaseSyncs.id, syncId)).run();
24987
25166
  const paths = ccReleasePaths(release);
24988
- const releaseCacheDir = path10.join(deps.cacheDir, release);
24989
- const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
24990
- const edgesPath = path10.join(releaseCacheDir, paths.edgesFilename);
25167
+ const releaseCacheDir = path11.join(deps.cacheDir, release);
25168
+ const vertexPath = path11.join(releaseCacheDir, paths.vertexFilename);
25169
+ const edgesPath = path11.join(releaseCacheDir, paths.edgesFilename);
24991
25170
  const [vertex, edges] = await Promise.all([
24992
25171
  deps.downloadFile({ url: paths.vertexUrl, destPath: vertexPath }),
24993
25172
  deps.downloadFile({ url: paths.edgesUrl, destPath: edgesPath })
@@ -25147,7 +25326,7 @@ function computeSummary(rows) {
25147
25326
 
25148
25327
  // src/backlink-extract.ts
25149
25328
  import crypto29 from "crypto";
25150
- import fs8 from "fs";
25329
+ import fs9 from "fs";
25151
25330
  import { and as and22, desc as desc15, eq as eq32 } from "drizzle-orm";
25152
25331
  var log7 = createLogger("BacklinkExtract");
25153
25332
  function defaultDeps2() {
@@ -25176,7 +25355,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
25176
25355
  if (!sync.vertexPath || !sync.edgesPath) {
25177
25356
  throw new Error(`Release ${sync.release} is missing cached file paths`);
25178
25357
  }
25179
- if (!fs8.existsSync(sync.vertexPath) || !fs8.existsSync(sync.edgesPath)) {
25358
+ if (!fs9.existsSync(sync.vertexPath) || !fs9.existsSync(sync.edgesPath)) {
25180
25359
  throw new Error(
25181
25360
  `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.`
25182
25361
  );
@@ -25569,23 +25748,25 @@ function buildDiscoveryInsightTitle(input) {
25569
25748
  }
25570
25749
 
25571
25750
  // src/commands/backfill.ts
25572
- import { and as and24, eq as eq34, inArray as inArray10 } from "drizzle-orm";
25751
+ import { and as and24, eq as eq34, inArray as inArray11 } from "drizzle-orm";
25573
25752
  var SNAPSHOT_BATCH_SIZE = 500;
25574
25753
  async function backfillAnswerVisibilityCommand(opts) {
25575
25754
  const config = loadConfig();
25576
25755
  const db = createClient(config.database);
25577
25756
  migrate(db);
25578
25757
  const projectFilter = opts?.project?.trim();
25758
+ const isDryRun = opts?.dryRun === true;
25579
25759
  const scopedProjects = projectFilter ? db.select().from(projects).where(eq34(projects.name, projectFilter)).all() : db.select().from(projects).all();
25580
25760
  let examined = 0;
25581
25761
  let updated = 0;
25762
+ let wouldUpdate = 0;
25582
25763
  let mentioned = 0;
25583
25764
  let reparsed = 0;
25584
25765
  let providerErrors = 0;
25585
25766
  if (scopedProjects.length > 0) {
25586
25767
  const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and24(
25587
25768
  eq34(runs.kind, RunKinds["answer-visibility"]),
25588
- inArray10(runs.projectId, scopedProjects.map((project) => project.id))
25769
+ inArray11(runs.projectId, scopedProjects.map((project) => project.id))
25589
25770
  )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq34(runs.kind, RunKinds["answer-visibility"])).all();
25590
25771
  const runIdsByProject = /* @__PURE__ */ new Map();
25591
25772
  for (const run of runRows) {
@@ -25617,7 +25798,7 @@ async function backfillAnswerVisibilityCommand(opts) {
25617
25798
  competitorOverlap: querySnapshots.competitorOverlap,
25618
25799
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
25619
25800
  rawResponse: querySnapshots.rawResponse
25620
- }).from(querySnapshots).where(inArray10(querySnapshots.runId, batchRunIds)).all();
25801
+ }).from(querySnapshots).where(inArray11(querySnapshots.runId, batchRunIds)).all();
25621
25802
  const pendingUpdates = [];
25622
25803
  for (const snapshot of snapshotRows) {
25623
25804
  examined++;
@@ -25681,12 +25862,16 @@ async function backfillAnswerVisibilityCommand(opts) {
25681
25862
  }
25682
25863
  }
25683
25864
  if (pendingUpdates.length > 0) {
25684
- db.transaction((tx) => {
25685
- for (const update of pendingUpdates) {
25686
- tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
25687
- }
25688
- });
25689
- updated += pendingUpdates.length;
25865
+ if (isDryRun) {
25866
+ wouldUpdate += pendingUpdates.length;
25867
+ } else {
25868
+ db.transaction((tx) => {
25869
+ for (const update of pendingUpdates) {
25870
+ tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
25871
+ }
25872
+ });
25873
+ updated += pendingUpdates.length;
25874
+ }
25690
25875
  }
25691
25876
  }
25692
25877
  }
@@ -25700,20 +25885,33 @@ async function backfillAnswerVisibilityCommand(opts) {
25700
25885
  reparsed,
25701
25886
  providerErrors
25702
25887
  };
25888
+ if (isDryRun) {
25889
+ result.dryRun = true;
25890
+ result.wouldUpdate = wouldUpdate;
25891
+ }
25703
25892
  if (opts?.format === "json") {
25704
25893
  console.log(JSON.stringify(result, null, 2));
25705
25894
  return;
25706
25895
  }
25707
- console.log("Answer visibility backfill complete.\n");
25896
+ console.log(`Answer visibility backfill ${isDryRun ? "preview" : "complete"}.
25897
+ `);
25708
25898
  if (projectFilter) {
25709
25899
  console.log(` Project: ${projectFilter}`);
25710
25900
  }
25711
25901
  console.log(` Projects: ${scopedProjects.length}`);
25712
- console.log(` Examined: ${examined}`);
25713
- console.log(` Updated: ${updated}`);
25714
- console.log(` Mentioned: ${mentioned}`);
25715
- console.log(` Reparsed: ${reparsed}`);
25716
- console.log(` Errors: ${providerErrors}`);
25902
+ console.log(` Examined: ${examined}`);
25903
+ if (isDryRun) {
25904
+ console.log(` Would update: ${wouldUpdate}`);
25905
+ } else {
25906
+ console.log(` Updated: ${updated}`);
25907
+ }
25908
+ console.log(` Mentioned: ${mentioned}`);
25909
+ console.log(` Reparsed: ${reparsed}`);
25910
+ console.log(` Errors: ${providerErrors}`);
25911
+ if (isDryRun) {
25912
+ console.log(`
25913
+ No DB writes performed. Re-run without --dry-run to apply.`);
25914
+ }
25717
25915
  }
25718
25916
  function backfillNormalizedPaths(db, opts) {
25719
25917
  const baseConditions = [];
@@ -25859,7 +26057,8 @@ async function backfillAiReferralPathsCommand(opts) {
25859
26057
  console.log(` Updated: ${updated}`);
25860
26058
  console.log(` Unchanged: ${unchanged}`);
25861
26059
  }
25862
- function backfillProjectAnswerMentions(db, projectId) {
26060
+ function backfillProjectAnswerMentions(db, projectId, opts) {
26061
+ const isDryRun = opts?.dryRun === true;
25863
26062
  const project = db.select().from(projects).where(eq34(projects.id, projectId)).get();
25864
26063
  if (!project) return { examined: 0, updated: 0, mentioned: 0 };
25865
26064
  const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, projectId)).all().map((row) => row.domain);
@@ -25867,8 +26066,11 @@ function backfillProjectAnswerMentions(db, projectId) {
25867
26066
  const runIds = runRows.map((r) => r.id);
25868
26067
  let examined = 0;
25869
26068
  let updated = 0;
26069
+ let wouldUpdate = 0;
25870
26070
  let mentioned = 0;
25871
- if (runIds.length === 0) return { examined, updated, mentioned };
26071
+ if (runIds.length === 0) {
26072
+ return isDryRun ? { examined, updated, wouldUpdate, mentioned } : { examined, updated, mentioned };
26073
+ }
25872
26074
  const projectDomains = effectiveDomains({
25873
26075
  canonicalDomain: project.canonicalDomain,
25874
26076
  ownedDomains: parseJsonColumn(project.ownedDomains, [])
@@ -25888,7 +26090,7 @@ function backfillProjectAnswerMentions(db, projectId) {
25888
26090
  competitorOverlap: querySnapshots.competitorOverlap,
25889
26091
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
25890
26092
  rawResponse: querySnapshots.rawResponse
25891
- }).from(querySnapshots).where(inArray10(querySnapshots.runId, batchRunIds)).all();
26093
+ }).from(querySnapshots).where(inArray11(querySnapshots.runId, batchRunIds)).all();
25892
26094
  const pendingUpdates = [];
25893
26095
  for (const snapshot of snapshotRows) {
25894
26096
  examined++;
@@ -25931,29 +26133,36 @@ function backfillProjectAnswerMentions(db, projectId) {
25931
26133
  }
25932
26134
  }
25933
26135
  if (pendingUpdates.length > 0) {
25934
- db.transaction((tx) => {
25935
- for (const update of pendingUpdates) {
25936
- tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
25937
- }
25938
- });
25939
- updated += pendingUpdates.length;
26136
+ if (isDryRun) {
26137
+ wouldUpdate += pendingUpdates.length;
26138
+ } else {
26139
+ db.transaction((tx) => {
26140
+ for (const update of pendingUpdates) {
26141
+ tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
26142
+ }
26143
+ });
26144
+ updated += pendingUpdates.length;
26145
+ }
25940
26146
  }
25941
26147
  }
25942
- return { examined, updated, mentioned };
26148
+ return isDryRun ? { examined, updated, wouldUpdate, mentioned } : { examined, updated, mentioned };
25943
26149
  }
25944
26150
  async function backfillAnswerMentionsCommand(opts) {
25945
26151
  const config = loadConfig();
25946
26152
  const db = createClient(config.database);
25947
26153
  migrate(db);
25948
26154
  const projectFilter = opts?.project?.trim();
26155
+ const isDryRun = opts?.dryRun === true;
25949
26156
  const scopedProjects = projectFilter ? db.select().from(projects).where(eq34(projects.name, projectFilter)).all() : db.select().from(projects).all();
25950
26157
  let examined = 0;
25951
26158
  let updated = 0;
26159
+ let wouldUpdate = 0;
25952
26160
  let mentioned = 0;
25953
26161
  for (const project of scopedProjects) {
25954
- const result2 = backfillProjectAnswerMentions(db, project.id);
26162
+ const result2 = backfillProjectAnswerMentions(db, project.id, { dryRun: isDryRun });
25955
26163
  examined += result2.examined;
25956
26164
  updated += result2.updated;
26165
+ wouldUpdate += result2.wouldUpdate ?? 0;
25957
26166
  mentioned += result2.mentioned;
25958
26167
  }
25959
26168
  const result = {
@@ -25963,16 +26172,29 @@ async function backfillAnswerMentionsCommand(opts) {
25963
26172
  updated,
25964
26173
  mentioned
25965
26174
  };
26175
+ if (isDryRun) {
26176
+ result.dryRun = true;
26177
+ result.wouldUpdate = wouldUpdate;
26178
+ }
25966
26179
  if (opts?.format === "json") {
25967
26180
  console.log(JSON.stringify(result, null, 2));
25968
26181
  return;
25969
26182
  }
25970
- console.log("Answer mentions backfill complete.\n");
25971
- if (projectFilter) console.log(` Project: ${projectFilter}`);
25972
- console.log(` Projects: ${scopedProjects.length}`);
25973
- console.log(` Examined: ${examined}`);
25974
- console.log(` Updated: ${updated}`);
25975
- console.log(` Mentioned: ${mentioned}`);
26183
+ console.log(`Answer mentions backfill ${isDryRun ? "preview" : "complete"}.
26184
+ `);
26185
+ if (projectFilter) console.log(` Project: ${projectFilter}`);
26186
+ console.log(` Projects: ${scopedProjects.length}`);
26187
+ console.log(` Examined: ${examined}`);
26188
+ if (isDryRun) {
26189
+ console.log(` Would update: ${wouldUpdate}`);
26190
+ } else {
26191
+ console.log(` Updated: ${updated}`);
26192
+ }
26193
+ console.log(` Mentioned: ${mentioned}`);
26194
+ if (isDryRun) {
26195
+ console.log(`
26196
+ No DB writes performed. Re-run without --dry-run to apply.`);
26197
+ }
25976
26198
  }
25977
26199
  function readStoredGroundingSources(rawResponse) {
25978
26200
  const envelope = parseJsonColumn(rawResponse, {});
@@ -25991,19 +26213,24 @@ function readStoredGroundingSources(rawResponse) {
25991
26213
  return result;
25992
26214
  }
25993
26215
  async function backfillInsightsCommand(project, opts) {
25994
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-WAJOEOJV.js");
26216
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-CIGYPPMR.js");
25995
26217
  const config = loadConfig();
25996
26218
  const db = createClient(config.database);
25997
26219
  migrate(db);
25998
26220
  const service = new IntelligenceService2(db);
25999
26221
  const isJson = opts?.format === "json";
26222
+ const isDryRun = opts?.dryRun === true;
26000
26223
  if (!isJson) {
26001
- process.stderr.write(`Backfilling insights for "${project}"...
26224
+ const scope = opts?.since ? ` (since ${opts.since})` : "";
26225
+ const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
26226
+ process.stderr.write(`Backfilling insights for "${project}"${scope}${mode}...
26002
26227
  `);
26003
26228
  }
26004
26229
  const result = service.backfill(project, {
26005
26230
  fromRunId: opts?.fromRun,
26006
- toRunId: opts?.toRun
26231
+ toRunId: opts?.toRun,
26232
+ since: opts?.since,
26233
+ dryRun: isDryRun
26007
26234
  }, (info) => {
26008
26235
  if (!isJson) {
26009
26236
  process.stderr.write(` [${info.index}/${info.total}] ${info.runId} \u2014 ${info.insights} insights
@@ -26016,15 +26243,23 @@ async function backfillInsightsCommand(project, opts) {
26016
26243
  skipped: result.skipped,
26017
26244
  totalInsights: result.totalInsights
26018
26245
  };
26246
+ if (result.dryRun) {
26247
+ output.dryRun = true;
26248
+ output.delta = result.delta;
26249
+ }
26019
26250
  if (isJson) {
26020
26251
  console.log(JSON.stringify(output, null, 2));
26021
26252
  return;
26022
26253
  }
26023
26254
  console.log(`
26024
- Backfill complete.`);
26255
+ Backfill ${isDryRun ? "preview" : "complete"}.`);
26025
26256
  console.log(` Processed: ${result.processed}`);
26026
26257
  console.log(` Skipped: ${result.skipped}`);
26027
26258
  console.log(` Insights: ${result.totalInsights}`);
26259
+ if (result.delta) {
26260
+ console.log(` Delta: -${result.delta.wouldDelete} existing +${result.delta.wouldCreate} new (net ${result.delta.netChange >= 0 ? "+" : ""}${result.delta.netChange})`);
26261
+ console.log(` No DB writes performed. Re-run without --dry-run to apply.`);
26262
+ }
26028
26263
  }
26029
26264
  function reparseProviderSnapshot(provider, rawResponse) {
26030
26265
  const envelope = parseJsonColumn(rawResponse, {});
@@ -26296,7 +26531,7 @@ var Scheduler = class {
26296
26531
  };
26297
26532
 
26298
26533
  // src/notifier.ts
26299
- import { eq as eq36, desc as desc16, and as and26, inArray as inArray11, or as or5 } from "drizzle-orm";
26534
+ import { eq as eq36, desc as desc16, and as and26, inArray as inArray12, or as or5 } from "drizzle-orm";
26300
26535
  import crypto31 from "crypto";
26301
26536
  var log10 = createLogger("Notifier");
26302
26537
  var Notifier = class {
@@ -26449,13 +26684,13 @@ var Notifier = class {
26449
26684
  provider: querySnapshots.provider,
26450
26685
  location: querySnapshots.location,
26451
26686
  citationState: querySnapshots.citationState
26452
- }).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(inArray11(querySnapshots.runId, currentRunIds)).all();
26687
+ }).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(inArray12(querySnapshots.runId, currentRunIds)).all();
26453
26688
  const previousSnapshots = this.db.select({
26454
26689
  queryId: querySnapshots.queryId,
26455
26690
  provider: querySnapshots.provider,
26456
26691
  location: querySnapshots.location,
26457
26692
  citationState: querySnapshots.citationState
26458
- }).from(querySnapshots).where(inArray11(querySnapshots.runId, previousRunIds)).all();
26693
+ }).from(querySnapshots).where(inArray12(querySnapshots.runId, previousRunIds)).all();
26459
26694
  const prevMap = /* @__PURE__ */ new Map();
26460
26695
  for (const s of previousSnapshots) {
26461
26696
  if (s.queryId == null) continue;
@@ -26621,8 +26856,8 @@ import crypto33 from "crypto";
26621
26856
  import { eq as eq39 } from "drizzle-orm";
26622
26857
 
26623
26858
  // src/agent/session.ts
26624
- import fs11 from "fs";
26625
- import path13 from "path";
26859
+ import fs12 from "fs";
26860
+ import path14 from "path";
26626
26861
  import { Agent } from "@mariozechner/pi-agent-core";
26627
26862
  import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
26628
26863
 
@@ -26723,26 +26958,26 @@ function buildAgentProvidersResponse(config) {
26723
26958
  }
26724
26959
 
26725
26960
  // src/agent/skill-paths.ts
26726
- import fs9 from "fs";
26727
- import path11 from "path";
26961
+ import fs10 from "fs";
26962
+ import path12 from "path";
26728
26963
  import { fileURLToPath } from "url";
26729
26964
  function resolveAeroSkillDir(pkgDir) {
26730
- const here = pkgDir ?? path11.dirname(fileURLToPath(import.meta.url));
26965
+ const here = pkgDir ?? path12.dirname(fileURLToPath(import.meta.url));
26731
26966
  const candidates = [
26732
- path11.join(here, "../assets/agent-workspace/skills/aero"),
26733
- path11.join(here, "../../assets/agent-workspace/skills/aero"),
26734
- path11.join(here, "../../../../skills/aero")
26967
+ path12.join(here, "../assets/agent-workspace/skills/aero"),
26968
+ path12.join(here, "../../assets/agent-workspace/skills/aero"),
26969
+ path12.join(here, "../../../../skills/aero")
26735
26970
  ];
26736
26971
  for (const candidate of candidates) {
26737
- if (fs9.existsSync(path11.join(candidate, "SKILL.md"))) return candidate;
26972
+ if (fs10.existsSync(path12.join(candidate, "SKILL.md"))) return candidate;
26738
26973
  }
26739
26974
  throw new Error(`Aero skill not found. Searched:
26740
26975
  ${candidates.join("\n ")}`);
26741
26976
  }
26742
26977
 
26743
26978
  // src/agent/skill-tools.ts
26744
- import fs10 from "fs";
26745
- import path12 from "path";
26979
+ import fs11 from "fs";
26980
+ import path13 from "path";
26746
26981
  import { Type } from "@sinclair/typebox";
26747
26982
  var MAX_DOC_CHARS = 2e4;
26748
26983
  function textResult(details) {
@@ -26763,13 +26998,13 @@ function parseDescription(body) {
26763
26998
  return "(no description)";
26764
26999
  }
26765
27000
  function scanSkillDocs(skillDir) {
26766
- const refsDir = path12.join(skillDir ?? resolveAeroSkillDir(), "references");
26767
- if (!fs10.existsSync(refsDir)) return [];
27001
+ const refsDir = path13.join(skillDir ?? resolveAeroSkillDir(), "references");
27002
+ if (!fs11.existsSync(refsDir)) return [];
26768
27003
  const entries = [];
26769
- for (const file of fs10.readdirSync(refsDir)) {
27004
+ for (const file of fs11.readdirSync(refsDir)) {
26770
27005
  if (!file.endsWith(".md")) continue;
26771
- const filePath = path12.join(refsDir, file);
26772
- const body = fs10.readFileSync(filePath, "utf-8");
27006
+ const filePath = path13.join(refsDir, file);
27007
+ const body = fs11.readFileSync(filePath, "utf-8");
26773
27008
  entries.push({
26774
27009
  slug: file.replace(/\.md$/, ""),
26775
27010
  description: parseDescription(body),
@@ -26812,8 +27047,8 @@ function buildReadSkillDocTool() {
26812
27047
  availableSlugs: docs.map((d) => d.slug)
26813
27048
  });
26814
27049
  }
26815
- const filePath = path12.join(skillDir, "references", `${match.slug}.md`);
26816
- const content = fs10.readFileSync(filePath, "utf-8");
27050
+ const filePath = path13.join(skillDir, "references", `${match.slug}.md`);
27051
+ const content = fs11.readFileSync(filePath, "utf-8");
26817
27052
  if (content.length > MAX_DOC_CHARS) {
26818
27053
  return textResult({
26819
27054
  slug: match.slug,
@@ -26907,10 +27142,10 @@ function ensureBuiltinsRegistered() {
26907
27142
  }
26908
27143
  function loadAeroSystemPrompt(pkgDir) {
26909
27144
  const skillDir = resolveAeroSkillDir(pkgDir);
26910
- const skillBody = fs11.readFileSync(path13.join(skillDir, "SKILL.md"), "utf-8");
26911
- const soulPath = path13.join(skillDir, "soul.md");
26912
- if (!fs11.existsSync(soulPath)) return skillBody;
26913
- const soulBody = fs11.readFileSync(soulPath, "utf-8");
27145
+ const skillBody = fs12.readFileSync(path14.join(skillDir, "SKILL.md"), "utf-8");
27146
+ const soulPath = path14.join(skillDir, "soul.md");
27147
+ if (!fs12.existsSync(soulPath)) return skillBody;
27148
+ const soulBody = fs12.readFileSync(soulPath, "utf-8");
26914
27149
  return `${soulBody.trimEnd()}
26915
27150
 
26916
27151
  ---
@@ -26968,7 +27203,7 @@ function resolveSessionProviderAndModel(config, opts) {
26968
27203
 
26969
27204
  // src/agent/memory-store.ts
26970
27205
  import crypto32 from "crypto";
26971
- import { and as and27, desc as desc17, eq as eq38, like as like2, sql as sql13 } from "drizzle-orm";
27206
+ import { and as and27, desc as desc17, eq as eq38, like as like2, sql as sql15 } from "drizzle-orm";
26972
27207
  var COMPACTION_KEY_PREFIX = "compaction:";
26973
27208
  var COMPACTION_NOTES_PER_SESSION = 3;
26974
27209
  function rowToDto2(row) {
@@ -27054,7 +27289,7 @@ function writeCompactionNote(db, args) {
27054
27289
  ).orderBy(desc17(agentMemory.updatedAt)).all();
27055
27290
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
27056
27291
  if (stale.length > 0) {
27057
- tx.delete(agentMemory).where(sql13`${agentMemory.id} IN (${sql13.join(stale.map((s) => sql13`${s}`), sql13`, `)})`).run();
27292
+ tx.delete(agentMemory).where(sql15`${agentMemory.id} IN (${sql15.join(stale.map((s) => sql15`${s}`), sql15`, `)})`).run();
27058
27293
  }
27059
27294
  const row = tx.select().from(agentMemory).where(and27(eq38(agentMemory.projectId, args.projectId), eq38(agentMemory.key, key))).get();
27060
27295
  if (row) inserted = rowToDto2(row);
@@ -27812,13 +28047,13 @@ function extractHostname(domain) {
27812
28047
  function fetchWithPinnedAddress(target) {
27813
28048
  return new Promise((resolve) => {
27814
28049
  const port = target.url.port ? Number(target.url.port) : 443;
27815
- const path15 = target.url.pathname + target.url.search;
28050
+ const path16 = target.url.pathname + target.url.search;
27816
28051
  const req = https2.request(
27817
28052
  {
27818
28053
  hostname: target.address,
27819
28054
  family: target.family,
27820
28055
  port,
27821
- path: path15,
28056
+ path: path16,
27822
28057
  method: "GET",
27823
28058
  timeout: FETCH_TIMEOUT_MS2,
27824
28059
  servername: target.url.hostname,
@@ -28723,8 +28958,8 @@ async function createServer(opts) {
28723
28958
  );
28724
28959
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
28725
28960
  const snapshotService = new SnapshotService(registry);
28726
- const orphanedOpenClawDir = path14.join(os6.homedir(), ".openclaw-aero");
28727
- if (fs12.existsSync(orphanedOpenClawDir)) {
28961
+ const orphanedOpenClawDir = path15.join(os6.homedir(), ".openclaw-aero");
28962
+ if (fs13.existsSync(orphanedOpenClawDir)) {
28728
28963
  app.log.warn(
28729
28964
  { path: orphanedOpenClawDir },
28730
28965
  "OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
@@ -29439,10 +29674,10 @@ async function createServer(opts) {
29439
29674
  return snapshotService.createReport(input);
29440
29675
  }
29441
29676
  });
29442
- const dirname = path14.dirname(fileURLToPath2(import.meta.url));
29443
- const assetsDir = path14.join(dirname, "..", "assets");
29444
- if (fs12.existsSync(assetsDir)) {
29445
- const indexPath = path14.join(assetsDir, "index.html");
29677
+ const dirname = path15.dirname(fileURLToPath2(import.meta.url));
29678
+ const assetsDir = path15.join(dirname, "..", "assets");
29679
+ if (fs13.existsSync(assetsDir)) {
29680
+ const indexPath = path15.join(assetsDir, "index.html");
29446
29681
  const injectConfig = (html) => {
29447
29682
  const clientConfig = {};
29448
29683
  if (basePath) clientConfig.basePath = basePath;
@@ -29460,8 +29695,8 @@ async function createServer(opts) {
29460
29695
  index: false
29461
29696
  });
29462
29697
  const serveIndex = (_request, reply) => {
29463
- if (fs12.existsSync(indexPath)) {
29464
- const html = fs12.readFileSync(indexPath, "utf-8");
29698
+ if (fs13.existsSync(indexPath)) {
29699
+ const html = fs13.readFileSync(indexPath, "utf-8");
29465
29700
  return reply.type("text/html").send(injectConfig(html));
29466
29701
  }
29467
29702
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -29481,8 +29716,8 @@ async function createServer(opts) {
29481
29716
  if (basePath && !url.startsWith(basePath)) {
29482
29717
  return reply.status(404).send({ error: "Not found", path: request.url });
29483
29718
  }
29484
- if (fs12.existsSync(indexPath)) {
29485
- const html = fs12.readFileSync(indexPath, "utf-8");
29719
+ if (fs13.existsSync(indexPath)) {
29720
+ const html = fs13.readFileSync(indexPath, "utf-8");
29486
29721
  return reply.type("text/html").send(injectConfig(html));
29487
29722
  }
29488
29723
  return reply.status(404).send({ error: "Not found" });