@ainyc/canonry 2.10.3 → 2.12.1

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.
@@ -55,7 +55,7 @@ import {
55
55
  visibilityStateFromAnswerMentioned,
56
56
  windowCutoff,
57
57
  wordpressEnvSchema
58
- } from "./chunk-Z3BWDCBJ.js";
58
+ } from "./chunk-PLI7EOPM.js";
59
59
  import {
60
60
  IntelligenceService,
61
61
  agentMemory,
@@ -173,7 +173,7 @@ import crypto28 from "crypto";
173
173
  import fs12 from "fs";
174
174
  import path14 from "path";
175
175
  import { fileURLToPath as fileURLToPath2 } from "url";
176
- import { eq as eq31 } from "drizzle-orm";
176
+ import { eq as eq32 } from "drizzle-orm";
177
177
  import Fastify from "fastify";
178
178
 
179
179
  // ../api-routes/src/auth.ts
@@ -2353,8 +2353,330 @@ async function intelligenceRoutes(app) {
2353
2353
  });
2354
2354
  }
2355
2355
 
2356
+ // ../api-routes/src/composites.ts
2357
+ import { eq as eq12, and as and3, desc as desc5, sql as sql3, like, or as or2 } from "drizzle-orm";
2358
+ var TOP_INSIGHT_LIMIT = 5;
2359
+ var SEARCH_HIT_HARD_LIMIT = 50;
2360
+ var SEARCH_SNIPPET_RADIUS = 80;
2361
+ async function compositeRoutes(app) {
2362
+ app.get("/projects/:name/overview", async (request, reply) => {
2363
+ const project = resolveProject(app.db, request.params.name);
2364
+ const totalRunsRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq12(runs.projectId, project.id)).get();
2365
+ const totalRuns = totalRunsRow?.count ?? 0;
2366
+ const recentRuns = app.db.select().from(runs).where(eq12(runs.projectId, project.id)).orderBy(desc5(runs.createdAt)).limit(2).all();
2367
+ const [latestRunRow, previousRunRow] = recentRuns;
2368
+ const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
2369
+ const healthRow = app.db.select().from(healthSnapshots).where(eq12(healthSnapshots.projectId, project.id)).orderBy(desc5(healthSnapshots.createdAt)).limit(1).get();
2370
+ const health = healthRow ? mapHealthRow2(healthRow) : null;
2371
+ const insightRows = app.db.select().from(insights).where(eq12(insights.projectId, project.id)).orderBy(desc5(insights.createdAt)).all();
2372
+ const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
2373
+ const { keywordCounts, providers } = summarizeLatestRun(app, latestRunRow ?? null);
2374
+ const transitions = summarizeTransitions(app, latestRunRow ?? null, previousRunRow ?? null);
2375
+ const result = {
2376
+ project: formatProject2(project),
2377
+ latestRun,
2378
+ health,
2379
+ topInsights,
2380
+ keywordCounts,
2381
+ providers,
2382
+ transitions
2383
+ };
2384
+ return reply.send(result);
2385
+ });
2386
+ app.get("/projects/:name/search", async (request, reply) => {
2387
+ const project = resolveProject(app.db, request.params.name);
2388
+ const rawQuery = (request.query.q ?? "").trim();
2389
+ if (rawQuery.length < 2) {
2390
+ throw validationError('"q" must be at least 2 characters');
2391
+ }
2392
+ const limit = clampSearchLimit(request.query.limit);
2393
+ const escaped = escapeLikePattern(rawQuery);
2394
+ const pattern = `%${escaped}%`;
2395
+ const snapshotMatches = app.db.select({
2396
+ id: querySnapshots.id,
2397
+ runId: querySnapshots.runId,
2398
+ keywordId: querySnapshots.keywordId,
2399
+ keywordText: keywords.keyword,
2400
+ provider: querySnapshots.provider,
2401
+ model: querySnapshots.model,
2402
+ citationState: querySnapshots.citationState,
2403
+ answerText: querySnapshots.answerText,
2404
+ citedDomains: querySnapshots.citedDomains,
2405
+ rawResponse: querySnapshots.rawResponse,
2406
+ createdAt: querySnapshots.createdAt
2407
+ }).from(querySnapshots).innerJoin(keywords, eq12(querySnapshots.keywordId, keywords.id)).where(
2408
+ and3(
2409
+ eq12(keywords.projectId, project.id),
2410
+ or2(
2411
+ sql3`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
2412
+ sql3`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
2413
+ sql3`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
2414
+ like(keywords.keyword, pattern)
2415
+ )
2416
+ )
2417
+ ).orderBy(desc5(querySnapshots.createdAt)).limit(limit + 1).all();
2418
+ const insightMatches = app.db.select().from(insights).where(
2419
+ and3(
2420
+ eq12(insights.projectId, project.id),
2421
+ or2(
2422
+ like(insights.title, pattern),
2423
+ like(insights.keyword, pattern),
2424
+ sql3`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
2425
+ sql3`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
2426
+ )
2427
+ )
2428
+ ).orderBy(desc5(insights.createdAt)).limit(limit + 1).all();
2429
+ const hits = [];
2430
+ for (const row of snapshotMatches) {
2431
+ hits.push(buildSnapshotHit(row, rawQuery));
2432
+ }
2433
+ for (const row of insightMatches) {
2434
+ hits.push(buildInsightHit(row, rawQuery));
2435
+ }
2436
+ hits.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
2437
+ const truncated = hits.length > limit;
2438
+ const trimmed = truncated ? hits.slice(0, limit) : hits;
2439
+ const response = {
2440
+ query: rawQuery,
2441
+ totalHits: trimmed.length,
2442
+ truncated,
2443
+ hits: trimmed
2444
+ };
2445
+ return reply.send(response);
2446
+ });
2447
+ }
2448
+ function clampSearchLimit(raw) {
2449
+ if (!raw) return 25;
2450
+ const parsed = Number.parseInt(raw, 10);
2451
+ if (Number.isNaN(parsed)) return 25;
2452
+ if (parsed < 1) return 1;
2453
+ if (parsed > SEARCH_HIT_HARD_LIMIT) return SEARCH_HIT_HARD_LIMIT;
2454
+ return parsed;
2455
+ }
2456
+ function escapeLikePattern(value) {
2457
+ return value.replace(/[\\%_]/g, (match) => `\\${match}`);
2458
+ }
2459
+ function summarizeRun(run) {
2460
+ return {
2461
+ id: run.id,
2462
+ projectId: run.projectId,
2463
+ kind: run.kind,
2464
+ status: run.status,
2465
+ trigger: run.trigger,
2466
+ location: run.location,
2467
+ startedAt: run.startedAt,
2468
+ finishedAt: run.finishedAt,
2469
+ error: parseRunError(run.error),
2470
+ createdAt: run.createdAt
2471
+ };
2472
+ }
2473
+ function summarizeLatestRun(app, run) {
2474
+ const empty = {
2475
+ keywordCounts: { totalKeywords: 0, citedKeywords: 0, notCitedKeywords: 0, citedRate: 0 },
2476
+ providers: []
2477
+ };
2478
+ if (!run) return empty;
2479
+ const rows = app.db.select({
2480
+ keywordId: querySnapshots.keywordId,
2481
+ provider: querySnapshots.provider,
2482
+ citationState: querySnapshots.citationState
2483
+ }).from(querySnapshots).where(eq12(querySnapshots.runId, run.id)).all();
2484
+ if (rows.length === 0) return empty;
2485
+ const perKeyword = /* @__PURE__ */ new Map();
2486
+ const perProvider = /* @__PURE__ */ new Map();
2487
+ for (const row of rows) {
2488
+ const cited = row.citationState === "cited";
2489
+ if (!perKeyword.has(row.keywordId) || cited) {
2490
+ perKeyword.set(row.keywordId, cited);
2491
+ }
2492
+ const bucket = perProvider.get(row.provider) ?? { cited: 0, total: 0 };
2493
+ bucket.total += 1;
2494
+ if (cited) bucket.cited += 1;
2495
+ perProvider.set(row.provider, bucket);
2496
+ }
2497
+ const totalKeywords = perKeyword.size;
2498
+ let citedKeywords = 0;
2499
+ for (const wasCited of perKeyword.values()) {
2500
+ if (wasCited) citedKeywords += 1;
2501
+ }
2502
+ const notCitedKeywords = totalKeywords - citedKeywords;
2503
+ const citedRate = totalKeywords === 0 ? 0 : Number((citedKeywords / totalKeywords).toFixed(4));
2504
+ const providers = [...perProvider.entries()].map(([provider, { cited, total }]) => ({
2505
+ provider,
2506
+ cited,
2507
+ total,
2508
+ citedRate: total === 0 ? 0 : Number((cited / total).toFixed(4))
2509
+ })).sort((a, b) => a.provider.localeCompare(b.provider));
2510
+ return {
2511
+ keywordCounts: { totalKeywords, citedKeywords, notCitedKeywords, citedRate },
2512
+ providers
2513
+ };
2514
+ }
2515
+ function summarizeTransitions(app, latest, previous) {
2516
+ const empty = { since: null, gained: 0, lost: 0, emerging: 0 };
2517
+ if (!latest || !previous) return empty;
2518
+ const fetchCited = (runId) => {
2519
+ const rows = app.db.select({
2520
+ keywordId: querySnapshots.keywordId,
2521
+ citationState: querySnapshots.citationState
2522
+ }).from(querySnapshots).where(eq12(querySnapshots.runId, runId)).all();
2523
+ const map = /* @__PURE__ */ new Map();
2524
+ for (const row of rows) {
2525
+ const cited = row.citationState === "cited";
2526
+ if (!map.has(row.keywordId) || cited) map.set(row.keywordId, cited);
2527
+ }
2528
+ return map;
2529
+ };
2530
+ const latestMap = fetchCited(latest.id);
2531
+ const previousMap = fetchCited(previous.id);
2532
+ let gained = 0;
2533
+ let lost = 0;
2534
+ let emerging = 0;
2535
+ for (const [keywordId, latestCited] of latestMap) {
2536
+ const previousCited = previousMap.get(keywordId);
2537
+ if (previousCited === void 0) {
2538
+ if (latestCited) emerging += 1;
2539
+ continue;
2540
+ }
2541
+ if (latestCited && !previousCited) gained += 1;
2542
+ else if (!latestCited && previousCited) lost += 1;
2543
+ }
2544
+ return { since: previous.createdAt, gained, lost, emerging };
2545
+ }
2546
+ function mapInsightRow2(r) {
2547
+ return {
2548
+ id: r.id,
2549
+ projectId: r.projectId,
2550
+ runId: r.runId ?? null,
2551
+ type: r.type,
2552
+ severity: r.severity,
2553
+ title: r.title,
2554
+ keyword: r.keyword,
2555
+ provider: r.provider,
2556
+ recommendation: parseJsonColumn(r.recommendation, void 0),
2557
+ cause: parseJsonColumn(r.cause, void 0),
2558
+ dismissed: r.dismissed,
2559
+ createdAt: r.createdAt
2560
+ };
2561
+ }
2562
+ function mapHealthRow2(r) {
2563
+ return {
2564
+ id: r.id,
2565
+ projectId: r.projectId,
2566
+ runId: r.runId ?? null,
2567
+ overallCitedRate: Number(r.overallCitedRate),
2568
+ totalPairs: r.totalPairs,
2569
+ citedPairs: r.citedPairs,
2570
+ providerBreakdown: parseJsonColumn(r.providerBreakdown, {}),
2571
+ createdAt: r.createdAt,
2572
+ status: "ready"
2573
+ };
2574
+ }
2575
+ function formatProject2(row) {
2576
+ return {
2577
+ id: row.id,
2578
+ name: row.name,
2579
+ displayName: row.displayName,
2580
+ canonicalDomain: row.canonicalDomain,
2581
+ ownedDomains: parseJsonColumn(row.ownedDomains, []),
2582
+ country: row.country,
2583
+ language: row.language,
2584
+ tags: parseJsonColumn(row.tags, []),
2585
+ labels: parseJsonColumn(row.labels, {}),
2586
+ locations: parseJsonColumn(row.locations, []),
2587
+ defaultLocation: row.defaultLocation,
2588
+ autoExtractBacklinks: row.autoExtractBacklinks === 1,
2589
+ configSource: row.configSource,
2590
+ configRevision: row.configRevision,
2591
+ createdAt: row.createdAt,
2592
+ updatedAt: row.updatedAt
2593
+ };
2594
+ }
2595
+ function buildSnapshotHit(row, query) {
2596
+ const lower = query.toLowerCase();
2597
+ const keyword = row.keywordText ?? "";
2598
+ const answer = row.answerText ?? "";
2599
+ const cited = row.citedDomains;
2600
+ const raw = row.rawResponse ?? "";
2601
+ let matchedField;
2602
+ let snippet;
2603
+ if (answer.toLowerCase().includes(lower)) {
2604
+ matchedField = "answerText";
2605
+ snippet = makeSnippet(answer, query);
2606
+ } else if (cited.toLowerCase().includes(lower)) {
2607
+ matchedField = "citedDomains";
2608
+ snippet = makeSnippet(cited, query);
2609
+ } else if (raw.toLowerCase().includes(lower)) {
2610
+ matchedField = "searchQueries";
2611
+ snippet = makeSnippet(raw, query);
2612
+ } else {
2613
+ matchedField = "keyword";
2614
+ snippet = keyword;
2615
+ }
2616
+ return {
2617
+ kind: "snapshot",
2618
+ id: row.id,
2619
+ runId: row.runId,
2620
+ keyword,
2621
+ provider: row.provider,
2622
+ model: row.model,
2623
+ citationState: row.citationState,
2624
+ matchedField,
2625
+ snippet,
2626
+ createdAt: row.createdAt
2627
+ };
2628
+ }
2629
+ function buildInsightHit(row, query) {
2630
+ const lower = query.toLowerCase();
2631
+ const recommendation = row.recommendation ?? "";
2632
+ const cause = row.cause ?? "";
2633
+ let matchedField;
2634
+ let snippet;
2635
+ if (row.title.toLowerCase().includes(lower)) {
2636
+ matchedField = "title";
2637
+ snippet = makeSnippet(row.title, query);
2638
+ } else if (row.keyword.toLowerCase().includes(lower)) {
2639
+ matchedField = "keyword";
2640
+ snippet = row.keyword;
2641
+ } else if (recommendation.toLowerCase().includes(lower)) {
2642
+ matchedField = "recommendation";
2643
+ snippet = makeSnippet(recommendation, query);
2644
+ } else {
2645
+ matchedField = "cause";
2646
+ snippet = makeSnippet(cause, query);
2647
+ }
2648
+ return {
2649
+ kind: "insight",
2650
+ id: row.id,
2651
+ runId: row.runId ?? null,
2652
+ type: row.type,
2653
+ severity: row.severity,
2654
+ title: row.title,
2655
+ keyword: row.keyword,
2656
+ provider: row.provider,
2657
+ matchedField,
2658
+ snippet,
2659
+ dismissed: row.dismissed,
2660
+ createdAt: row.createdAt
2661
+ };
2662
+ }
2663
+ function makeSnippet(text, query) {
2664
+ if (!text) return "";
2665
+ const needle = query.toLowerCase();
2666
+ const haystack = text.toLowerCase();
2667
+ const idx = haystack.indexOf(needle);
2668
+ if (idx < 0) {
2669
+ return text.length <= SEARCH_SNIPPET_RADIUS * 2 ? text : `${text.slice(0, SEARCH_SNIPPET_RADIUS * 2)}\u2026`;
2670
+ }
2671
+ const start = Math.max(0, idx - SEARCH_SNIPPET_RADIUS);
2672
+ const end = Math.min(text.length, idx + query.length + SEARCH_SNIPPET_RADIUS);
2673
+ const prefix = start === 0 ? "" : "\u2026";
2674
+ const suffix = end === text.length ? "" : "\u2026";
2675
+ return `${prefix}${text.slice(start, end)}${suffix}`;
2676
+ }
2677
+
2356
2678
  // ../api-routes/src/content-data.ts
2357
- import { and as and3, eq as eq12, desc as desc5, inArray as inArray3 } from "drizzle-orm";
2679
+ import { and as and4, eq as eq13, desc as desc6, inArray as inArray3 } from "drizzle-orm";
2358
2680
  var RECENT_RUNS_WINDOW = 5;
2359
2681
  function loadOrchestratorInput(db, project) {
2360
2682
  const projectId = project.id;
@@ -2400,43 +2722,43 @@ function loadOrchestratorInput(db, project) {
2400
2722
  };
2401
2723
  }
2402
2724
  function listKeywords(db, projectId) {
2403
- const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq12(keywords.projectId, projectId)).all();
2725
+ const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq13(keywords.projectId, projectId)).all();
2404
2726
  return rows.map((r) => r.text);
2405
2727
  }
2406
2728
  function listCompetitorDomains(db, projectId) {
2407
- const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq12(competitors.projectId, projectId)).all();
2729
+ const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all();
2408
2730
  return rows.map((r) => r.domain);
2409
2731
  }
2410
2732
  function listRecentAnswerVisibilityRunIds(db, projectId, limit) {
2411
2733
  const rows = db.select({ id: runs.id }).from(runs).where(
2412
- and3(
2413
- eq12(runs.projectId, projectId),
2414
- eq12(runs.kind, RunKinds["answer-visibility"]),
2734
+ and4(
2735
+ eq13(runs.projectId, projectId),
2736
+ eq13(runs.kind, RunKinds["answer-visibility"]),
2415
2737
  // Queued/running/failed/cancelled runs may have partial or no
2416
2738
  // snapshots; including them risks pointing latestRunId at a run with
2417
2739
  // no usable evidence.
2418
2740
  inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
2419
2741
  )
2420
- ).orderBy(desc5(runs.createdAt)).limit(limit).all();
2742
+ ).orderBy(desc6(runs.createdAt)).limit(limit).all();
2421
2743
  return rows.map((r) => r.id);
2422
2744
  }
2423
2745
  function lookupRunTimestamp(db, runId) {
2424
- const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq12(runs.id, runId)).get();
2746
+ const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq13(runs.id, runId)).get();
2425
2747
  return row?.createdAt ?? "";
2426
2748
  }
2427
2749
  function listGscPagesForProject(db, projectId) {
2428
- const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq12(gscSearchData.projectId, projectId)).all();
2750
+ const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
2429
2751
  return rows.map((r) => r.page);
2430
2752
  }
2431
2753
  function listGa4LandingPagesForProject(db, projectId) {
2432
- const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
2754
+ const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
2433
2755
  return rows.map((r) => r.landingPage);
2434
2756
  }
2435
2757
  function buildGaTrafficByPage(db, projectId) {
2436
2758
  const rows = db.select({
2437
2759
  landingPage: gaTrafficSnapshots.landingPage,
2438
2760
  sessions: gaTrafficSnapshots.sessions
2439
- }).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
2761
+ }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
2440
2762
  const map = /* @__PURE__ */ new Map();
2441
2763
  for (const row of rows) {
2442
2764
  const path15 = extractPath(row.landingPage);
@@ -2446,14 +2768,14 @@ function buildGaTrafficByPage(db, projectId) {
2446
2768
  return map;
2447
2769
  }
2448
2770
  function sumAiReferralSessions(db, projectId) {
2449
- const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq12(gaAiReferrals.projectId, projectId)).all();
2771
+ const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq13(gaAiReferrals.projectId, projectId)).all();
2450
2772
  return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
2451
2773
  }
2452
2774
  function buildCandidateQueries(opts) {
2453
2775
  if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
2454
2776
  return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
2455
2777
  }
2456
- const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq12(keywords.projectId, opts.projectId)).all();
2778
+ const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq13(keywords.projectId, opts.projectId)).all();
2457
2779
  const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
2458
2780
  const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
2459
2781
  const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
@@ -2463,7 +2785,7 @@ function buildCandidateQueries(opts) {
2463
2785
  list.push(row);
2464
2786
  snapshotsByKeyword.set(row.keywordId, list);
2465
2787
  }
2466
- const gscRows = opts.db.select().from(gscSearchData).where(eq12(gscSearchData.projectId, opts.projectId)).all();
2788
+ const gscRows = opts.db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, opts.projectId)).all();
2467
2789
  const gscByQuery = aggregateGscByQuery(gscRows);
2468
2790
  return opts.candidateQueryStrings.map((query) => {
2469
2791
  const keywordId = keywordIdByText.get(query);
@@ -4952,6 +5274,35 @@ var routeCatalog = [
4952
5274
  404: { description: "Project not found." }
4953
5275
  }
4954
5276
  },
5277
+ {
5278
+ method: "get",
5279
+ path: "/api/v1/projects/{name}/overview",
5280
+ summary: "Get a composite overview of project health",
5281
+ description: 'Bundles project info, latest run, top undismissed insights, the latest health snapshot, keyword cited rate, per-provider breakdown, and transitions vs. the previous run. Designed for the "how is project X doing?" question so agents can answer in one call.',
5282
+ tags: ["intelligence"],
5283
+ parameters: [nameParameter],
5284
+ responses: {
5285
+ 200: { description: "Overview returned." },
5286
+ 404: { description: "Project not found." }
5287
+ }
5288
+ },
5289
+ {
5290
+ method: "get",
5291
+ path: "/api/v1/projects/{name}/search",
5292
+ summary: "Search query snapshots and insights for text",
5293
+ description: "Returns the most recent snapshots and insights whose answer text, cited domains, raw response, or insight title/keyword/recommendation/cause matches the query. Use to find anything mentioning a competitor, term, or URL without paginating snapshots.",
5294
+ tags: ["intelligence"],
5295
+ parameters: [
5296
+ nameParameter,
5297
+ { name: "q", in: "query", required: true, description: "Search term (>= 2 chars).", schema: stringSchema },
5298
+ { name: "limit", in: "query", description: "Max combined hits (1-50, default 25).", schema: stringSchema }
5299
+ ],
5300
+ responses: {
5301
+ 200: { description: "Search hits returned." },
5302
+ 400: { description: "Query string missing or too short." },
5303
+ 404: { description: "Project not found." }
5304
+ }
5305
+ },
4955
5306
  {
4956
5307
  method: "get",
4957
5308
  path: "/api/v1/backlinks/status",
@@ -5410,7 +5761,7 @@ async function telemetryRoutes(app, opts) {
5410
5761
 
5411
5762
  // ../api-routes/src/schedules.ts
5412
5763
  import crypto11 from "crypto";
5413
- import { eq as eq13 } from "drizzle-orm";
5764
+ import { eq as eq14 } from "drizzle-orm";
5414
5765
  async function scheduleRoutes(app, opts) {
5415
5766
  app.put("/projects/:name/schedule", async (request, reply) => {
5416
5767
  const project = resolveProject(app.db, request.params.name);
@@ -5453,7 +5804,7 @@ async function scheduleRoutes(app, opts) {
5453
5804
  }
5454
5805
  const now = (/* @__PURE__ */ new Date()).toISOString();
5455
5806
  const enabledInt = enabled === false ? 0 : 1;
5456
- const existing = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
5807
+ const existing = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
5457
5808
  if (existing) {
5458
5809
  app.db.update(schedules).set({
5459
5810
  cronExpr,
@@ -5462,7 +5813,7 @@ async function scheduleRoutes(app, opts) {
5462
5813
  providers: JSON.stringify(providers),
5463
5814
  enabled: enabledInt,
5464
5815
  updatedAt: now
5465
- }).where(eq13(schedules.id, existing.id)).run();
5816
+ }).where(eq14(schedules.id, existing.id)).run();
5466
5817
  } else {
5467
5818
  app.db.insert(schedules).values({
5468
5819
  id: crypto11.randomUUID(),
@@ -5484,12 +5835,12 @@ async function scheduleRoutes(app, opts) {
5484
5835
  diff: { cronExpr, preset, timezone, providers }
5485
5836
  });
5486
5837
  opts.onScheduleUpdated?.("upsert", project.id);
5487
- const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
5838
+ const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
5488
5839
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
5489
5840
  });
5490
5841
  app.get("/projects/:name/schedule", async (request, reply) => {
5491
5842
  const project = resolveProject(app.db, request.params.name);
5492
- const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
5843
+ const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
5493
5844
  if (!schedule) {
5494
5845
  throw notFound("Schedule", request.params.name);
5495
5846
  }
@@ -5497,11 +5848,11 @@ async function scheduleRoutes(app, opts) {
5497
5848
  });
5498
5849
  app.delete("/projects/:name/schedule", async (request, reply) => {
5499
5850
  const project = resolveProject(app.db, request.params.name);
5500
- const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
5851
+ const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
5501
5852
  if (!schedule) {
5502
5853
  throw notFound("Schedule", request.params.name);
5503
5854
  }
5504
- app.db.delete(schedules).where(eq13(schedules.id, schedule.id)).run();
5855
+ app.db.delete(schedules).where(eq14(schedules.id, schedule.id)).run();
5505
5856
  writeAuditLog(app.db, {
5506
5857
  projectId: project.id,
5507
5858
  actor: "api",
@@ -5531,7 +5882,7 @@ function formatSchedule(row) {
5531
5882
 
5532
5883
  // ../api-routes/src/notifications.ts
5533
5884
  import crypto12 from "crypto";
5534
- import { eq as eq14 } from "drizzle-orm";
5885
+ import { eq as eq15 } from "drizzle-orm";
5535
5886
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
5536
5887
  async function notificationRoutes(app) {
5537
5888
  app.get("/notifications/events", async (_request, reply) => {
@@ -5570,22 +5921,22 @@ async function notificationRoutes(app) {
5570
5921
  diff: { channel, ...redactNotificationUrl(url), events }
5571
5922
  });
5572
5923
  return reply.status(201).send({
5573
- ...formatNotification(app.db.select().from(notifications).where(eq14(notifications.id, id)).get()),
5924
+ ...formatNotification(app.db.select().from(notifications).where(eq15(notifications.id, id)).get()),
5574
5925
  webhookSecret
5575
5926
  });
5576
5927
  });
5577
5928
  app.get("/projects/:name/notifications", async (request, reply) => {
5578
5929
  const project = resolveProject(app.db, request.params.name);
5579
- const rows = app.db.select().from(notifications).where(eq14(notifications.projectId, project.id)).all();
5930
+ const rows = app.db.select().from(notifications).where(eq15(notifications.projectId, project.id)).all();
5580
5931
  return reply.send(rows.map(formatNotification));
5581
5932
  });
5582
5933
  app.delete("/projects/:name/notifications/:id", async (request, reply) => {
5583
5934
  const project = resolveProject(app.db, request.params.name);
5584
- const notification = app.db.select().from(notifications).where(eq14(notifications.id, request.params.id)).get();
5935
+ const notification = app.db.select().from(notifications).where(eq15(notifications.id, request.params.id)).get();
5585
5936
  if (!notification || notification.projectId !== project.id) {
5586
5937
  throw notFound("Notification", request.params.id);
5587
5938
  }
5588
- app.db.delete(notifications).where(eq14(notifications.id, notification.id)).run();
5939
+ app.db.delete(notifications).where(eq15(notifications.id, notification.id)).run();
5589
5940
  writeAuditLog(app.db, {
5590
5941
  projectId: project.id,
5591
5942
  actor: "api",
@@ -5597,7 +5948,7 @@ async function notificationRoutes(app) {
5597
5948
  });
5598
5949
  app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
5599
5950
  const project = resolveProject(app.db, request.params.name);
5600
- const notification = app.db.select().from(notifications).where(eq14(notifications.id, request.params.id)).get();
5951
+ const notification = app.db.select().from(notifications).where(eq15(notifications.id, request.params.id)).get();
5601
5952
  if (!notification || notification.projectId !== project.id) {
5602
5953
  throw notFound("Notification", request.params.id);
5603
5954
  }
@@ -5650,7 +6001,7 @@ function formatNotification(row) {
5650
6001
 
5651
6002
  // ../api-routes/src/google.ts
5652
6003
  import crypto14 from "crypto";
5653
- import { eq as eq15, and as and4, desc as desc6, sql as sql3 } from "drizzle-orm";
6004
+ import { eq as eq16, and as and5, desc as desc7, sql as sql4 } from "drizzle-orm";
5654
6005
 
5655
6006
  // ../integration-google/src/constants.ts
5656
6007
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -6754,20 +7105,20 @@ async function googleRoutes(app, opts) {
6754
7105
  if (opts.onGscSyncRequested) {
6755
7106
  opts.onGscSyncRequested(runId, project.id, { days, full });
6756
7107
  }
6757
- const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
7108
+ const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
6758
7109
  return run;
6759
7110
  });
6760
7111
  app.get("/projects/:name/google/gsc/performance", async (request) => {
6761
7112
  const project = resolveProject(app.db, request.params.name);
6762
7113
  const { startDate, endDate, query, page, limit } = request.query;
6763
7114
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
6764
- const conditions = [eq15(gscSearchData.projectId, project.id)];
6765
- if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
6766
- else if (cutoffDate) conditions.push(sql3`${gscSearchData.date} >= ${cutoffDate}`);
6767
- if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
6768
- if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
6769
- if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
6770
- const rows = app.db.select().from(gscSearchData).where(and4(...conditions)).orderBy(desc6(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
7115
+ const conditions = [eq16(gscSearchData.projectId, project.id)];
7116
+ if (startDate) conditions.push(sql4`${gscSearchData.date} >= ${startDate}`);
7117
+ else if (cutoffDate) conditions.push(sql4`${gscSearchData.date} >= ${cutoffDate}`);
7118
+ if (endDate) conditions.push(sql4`${gscSearchData.date} <= ${endDate}`);
7119
+ if (query) conditions.push(sql4`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
7120
+ if (page) conditions.push(sql4`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
7121
+ const rows = app.db.select().from(gscSearchData).where(and5(...conditions)).orderBy(desc7(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
6771
7122
  return rows.map((r) => ({
6772
7123
  date: r.date,
6773
7124
  query: r.query,
@@ -6839,9 +7190,9 @@ async function googleRoutes(app, opts) {
6839
7190
  app.get("/projects/:name/google/gsc/inspections", async (request) => {
6840
7191
  const project = resolveProject(app.db, request.params.name);
6841
7192
  const { url, limit } = request.query;
6842
- const conditions = [eq15(gscUrlInspections.projectId, project.id)];
6843
- if (url) conditions.push(eq15(gscUrlInspections.url, url));
6844
- const rows = app.db.select().from(gscUrlInspections).where(and4(...conditions)).orderBy(desc6(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
7193
+ const conditions = [eq16(gscUrlInspections.projectId, project.id)];
7194
+ if (url) conditions.push(eq16(gscUrlInspections.url, url));
7195
+ const rows = app.db.select().from(gscUrlInspections).where(and5(...conditions)).orderBy(desc7(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
6845
7196
  return rows.map((r) => ({
6846
7197
  id: r.id,
6847
7198
  url: r.url,
@@ -6860,7 +7211,7 @@ async function googleRoutes(app, opts) {
6860
7211
  });
6861
7212
  app.get("/projects/:name/google/gsc/deindexed", async (request) => {
6862
7213
  const project = resolveProject(app.db, request.params.name);
6863
- const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
7214
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
6864
7215
  const byUrl = /* @__PURE__ */ new Map();
6865
7216
  for (const row of allInspections) {
6866
7217
  const existing = byUrl.get(row.url);
@@ -6888,7 +7239,7 @@ async function googleRoutes(app, opts) {
6888
7239
  });
6889
7240
  app.get("/projects/:name/google/gsc/coverage", async (request) => {
6890
7241
  const project = resolveProject(app.db, request.params.name);
6891
- const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
7242
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
6892
7243
  const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
6893
7244
  const latestByUrl = /* @__PURE__ */ new Map();
6894
7245
  const historyByUrl = /* @__PURE__ */ new Map();
@@ -6985,7 +7336,7 @@ async function googleRoutes(app, opts) {
6985
7336
  const project = resolveProject(app.db, request.params.name);
6986
7337
  const parsed = parseInt(request.query.limit ?? "90", 10);
6987
7338
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
6988
- const rows = app.db.select().from(gscCoverageSnapshots).where(eq15(gscCoverageSnapshots.projectId, project.id)).orderBy(desc6(gscCoverageSnapshots.date)).limit(limit).all();
7339
+ const rows = app.db.select().from(gscCoverageSnapshots).where(eq16(gscCoverageSnapshots.projectId, project.id)).orderBy(desc7(gscCoverageSnapshots.date)).limit(limit).all();
6989
7340
  return rows.map((r) => ({
6990
7341
  date: r.date,
6991
7342
  indexed: r.indexed,
@@ -7045,7 +7396,7 @@ async function googleRoutes(app, opts) {
7045
7396
  if (opts.onInspectSitemapRequested) {
7046
7397
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
7047
7398
  }
7048
- const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
7399
+ const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
7049
7400
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
7050
7401
  });
7051
7402
  app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
@@ -7072,7 +7423,7 @@ async function googleRoutes(app, opts) {
7072
7423
  if (opts.onInspectSitemapRequested) {
7073
7424
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
7074
7425
  }
7075
- const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
7426
+ const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
7076
7427
  return run;
7077
7428
  });
7078
7429
  app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
@@ -7119,7 +7470,7 @@ async function googleRoutes(app, opts) {
7119
7470
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7120
7471
  let urlsToNotify = request.body?.urls ?? [];
7121
7472
  if (request.body?.allUnindexed) {
7122
- const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
7473
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7123
7474
  const latestByUrl = /* @__PURE__ */ new Map();
7124
7475
  for (const row of allInspections) {
7125
7476
  if (!latestByUrl.has(row.url)) {
@@ -7190,7 +7541,7 @@ async function googleRoutes(app, opts) {
7190
7541
 
7191
7542
  // ../api-routes/src/bing.ts
7192
7543
  import crypto15 from "crypto";
7193
- import { eq as eq16, and as and5, desc as desc7 } from "drizzle-orm";
7544
+ import { eq as eq17, and as and6, desc as desc8 } from "drizzle-orm";
7194
7545
 
7195
7546
  // ../integration-bing/src/constants.ts
7196
7547
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -7503,7 +7854,7 @@ async function bingRoutes(app, opts) {
7503
7854
  const store = requireConnectionStore();
7504
7855
  const project = resolveProject(app.db, request.params.name);
7505
7856
  requireConnection(store, project.canonicalDomain);
7506
- const allInspections = app.db.select().from(bingUrlInspections).where(eq16(bingUrlInspections.projectId, project.id)).orderBy(desc7(bingUrlInspections.inspectedAt)).all();
7857
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq17(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
7507
7858
  const latestByUrl = /* @__PURE__ */ new Map();
7508
7859
  const definitiveByUrl = /* @__PURE__ */ new Map();
7509
7860
  for (const row of allInspections) {
@@ -7592,7 +7943,7 @@ async function bingRoutes(app, opts) {
7592
7943
  const project = resolveProject(app.db, request.params.name);
7593
7944
  const parsed = parseInt(request.query.limit ?? "90", 10);
7594
7945
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
7595
- const rows = app.db.select().from(bingCoverageSnapshots).where(eq16(bingCoverageSnapshots.projectId, project.id)).orderBy(desc7(bingCoverageSnapshots.date)).limit(limit).all();
7946
+ const rows = app.db.select().from(bingCoverageSnapshots).where(eq17(bingCoverageSnapshots.projectId, project.id)).orderBy(desc8(bingCoverageSnapshots.date)).limit(limit).all();
7596
7947
  return rows.map((r) => ({
7597
7948
  date: r.date,
7598
7949
  indexed: r.indexed,
@@ -7604,8 +7955,8 @@ async function bingRoutes(app, opts) {
7604
7955
  requireConnectionStore();
7605
7956
  const project = resolveProject(app.db, request.params.name);
7606
7957
  const { url, limit } = request.query;
7607
- const whereClause = url ? and5(eq16(bingUrlInspections.projectId, project.id), eq16(bingUrlInspections.url, url)) : eq16(bingUrlInspections.projectId, project.id);
7608
- const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc7(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
7958
+ const whereClause = url ? and6(eq17(bingUrlInspections.projectId, project.id), eq17(bingUrlInspections.url, url)) : eq17(bingUrlInspections.projectId, project.id);
7959
+ const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc8(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
7609
7960
  return filtered.map((r) => ({
7610
7961
  id: r.id,
7611
7962
  url: r.url,
@@ -7694,7 +8045,7 @@ async function bingRoutes(app, opts) {
7694
8045
  anchorCount: result.AnchorCount ?? null,
7695
8046
  discoveryDate
7696
8047
  }).run();
7697
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq16(runs.id, runId)).run();
8048
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq17(runs.id, runId)).run();
7698
8049
  return {
7699
8050
  id,
7700
8051
  url,
@@ -7710,7 +8061,7 @@ async function bingRoutes(app, opts) {
7710
8061
  } catch (e) {
7711
8062
  const msg = e instanceof Error ? e.message : String(e);
7712
8063
  bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
7713
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq16(runs.id, runId)).run();
8064
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq17(runs.id, runId)).run();
7714
8065
  throw e;
7715
8066
  }
7716
8067
  });
@@ -7737,7 +8088,7 @@ async function bingRoutes(app, opts) {
7737
8088
  } else {
7738
8089
  bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
7739
8090
  }
7740
- const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
8091
+ const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
7741
8092
  return run;
7742
8093
  });
7743
8094
  app.post("/projects/:name/bing/request-indexing", async (request) => {
@@ -7749,7 +8100,7 @@ async function bingRoutes(app, opts) {
7749
8100
  }
7750
8101
  let urlsToSubmit = request.body?.urls ?? [];
7751
8102
  if (request.body?.allUnindexed) {
7752
- const allInspections = app.db.select().from(bingUrlInspections).where(eq16(bingUrlInspections.projectId, project.id)).orderBy(desc7(bingUrlInspections.inspectedAt)).all();
8103
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq17(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
7753
8104
  const latestByUrl = /* @__PURE__ */ new Map();
7754
8105
  for (const row of allInspections) {
7755
8106
  if (!latestByUrl.has(row.url)) {
@@ -7836,14 +8187,14 @@ async function bingRoutes(app, opts) {
7836
8187
  import fs from "fs";
7837
8188
  import path from "path";
7838
8189
  import os from "os";
7839
- import { eq as eq17, and as and6 } from "drizzle-orm";
8190
+ import { eq as eq18, and as and7 } from "drizzle-orm";
7840
8191
  function getScreenshotDir() {
7841
8192
  return path.join(os.homedir(), ".canonry", "screenshots");
7842
8193
  }
7843
8194
  async function cdpRoutes(app, opts) {
7844
8195
  app.get("/screenshots/:snapshotId", async (request, reply) => {
7845
8196
  const { snapshotId } = request.params;
7846
- const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq17(querySnapshots.id, snapshotId)).get();
8197
+ const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq18(querySnapshots.id, snapshotId)).get();
7847
8198
  if (!snapshot?.screenshotPath) {
7848
8199
  const err = notFound("Screenshot", snapshotId);
7849
8200
  return reply.code(err.statusCode).send(err.toJSON());
@@ -7909,7 +8260,7 @@ async function cdpRoutes(app, opts) {
7909
8260
  async (request, reply) => {
7910
8261
  const project = resolveProject(app.db, request.params.name);
7911
8262
  const { runId } = request.params;
7912
- const run = app.db.select().from(runs).where(and6(eq17(runs.id, runId), eq17(runs.projectId, project.id))).get();
8263
+ const run = app.db.select().from(runs).where(and7(eq18(runs.id, runId), eq18(runs.projectId, project.id))).get();
7913
8264
  if (!run) {
7914
8265
  const err = notFound("Run", runId);
7915
8266
  return reply.code(err.statusCode).send(err.toJSON());
@@ -7922,8 +8273,8 @@ async function cdpRoutes(app, opts) {
7922
8273
  citedDomains: querySnapshots.citedDomains,
7923
8274
  screenshotPath: querySnapshots.screenshotPath,
7924
8275
  rawResponse: querySnapshots.rawResponse
7925
- }).from(querySnapshots).where(eq17(querySnapshots.runId, runId)).all();
7926
- const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq17(keywords.projectId, project.id)).all();
8276
+ }).from(querySnapshots).where(eq18(querySnapshots.runId, runId)).all();
8277
+ const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq18(keywords.projectId, project.id)).all();
7927
8278
  const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
7928
8279
  const byKeyword = /* @__PURE__ */ new Map();
7929
8280
  for (const snap of snapshots) {
@@ -8006,7 +8357,7 @@ async function cdpRoutes(app, opts) {
8006
8357
 
8007
8358
  // ../api-routes/src/ga.ts
8008
8359
  import crypto16 from "crypto";
8009
- import { eq as eq18, desc as desc8, and as and7, sql as sql4 } from "drizzle-orm";
8360
+ import { eq as eq19, desc as desc9, and as and8, sql as sql5 } from "drizzle-orm";
8010
8361
  function gaLog(level, action, ctx) {
8011
8362
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
8012
8363
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -8163,10 +8514,10 @@ async function ga4Routes(app, opts) {
8163
8514
  if (!saConn && !oauthConn) {
8164
8515
  throw notFound("GA4 connection", project.name);
8165
8516
  }
8166
- app.db.delete(gaTrafficSnapshots).where(eq18(gaTrafficSnapshots.projectId, project.id)).run();
8167
- app.db.delete(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).run();
8168
- app.db.delete(gaAiReferrals).where(eq18(gaAiReferrals.projectId, project.id)).run();
8169
- app.db.delete(gaSocialReferrals).where(eq18(gaSocialReferrals.projectId, project.id)).run();
8517
+ app.db.delete(gaTrafficSnapshots).where(eq19(gaTrafficSnapshots.projectId, project.id)).run();
8518
+ app.db.delete(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).run();
8519
+ app.db.delete(gaAiReferrals).where(eq19(gaAiReferrals.projectId, project.id)).run();
8520
+ app.db.delete(gaSocialReferrals).where(eq19(gaSocialReferrals.projectId, project.id)).run();
8170
8521
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
8171
8522
  opts.ga4CredentialStore?.deleteConnection(project.name);
8172
8523
  opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
@@ -8187,7 +8538,7 @@ async function ga4Routes(app, opts) {
8187
8538
  if (!connected) {
8188
8539
  return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
8189
8540
  }
8190
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).orderBy(desc8(gaTrafficSummaries.syncedAt)).limit(1).get();
8541
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
8191
8542
  return {
8192
8543
  connected: true,
8193
8544
  propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
@@ -8246,10 +8597,10 @@ async function ga4Routes(app, opts) {
8246
8597
  app.db.transaction((tx) => {
8247
8598
  if (syncTraffic) {
8248
8599
  tx.delete(gaTrafficSnapshots).where(
8249
- and7(
8250
- eq18(gaTrafficSnapshots.projectId, project.id),
8251
- sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
8252
- sql4`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
8600
+ and8(
8601
+ eq19(gaTrafficSnapshots.projectId, project.id),
8602
+ sql5`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
8603
+ sql5`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
8253
8604
  )
8254
8605
  ).run();
8255
8606
  for (const row of rows) {
@@ -8268,10 +8619,10 @@ async function ga4Routes(app, opts) {
8268
8619
  }
8269
8620
  if (syncAi) {
8270
8621
  tx.delete(gaAiReferrals).where(
8271
- and7(
8272
- eq18(gaAiReferrals.projectId, project.id),
8273
- sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
8274
- sql4`${gaAiReferrals.date} <= ${summary.periodEnd}`
8622
+ and8(
8623
+ eq19(gaAiReferrals.projectId, project.id),
8624
+ sql5`${gaAiReferrals.date} >= ${summary.periodStart}`,
8625
+ sql5`${gaAiReferrals.date} <= ${summary.periodEnd}`
8275
8626
  )
8276
8627
  ).run();
8277
8628
  for (const row of aiReferrals) {
@@ -8291,10 +8642,10 @@ async function ga4Routes(app, opts) {
8291
8642
  }
8292
8643
  if (syncSocial) {
8293
8644
  tx.delete(gaSocialReferrals).where(
8294
- and7(
8295
- eq18(gaSocialReferrals.projectId, project.id),
8296
- sql4`${gaSocialReferrals.date} >= ${summary.periodStart}`,
8297
- sql4`${gaSocialReferrals.date} <= ${summary.periodEnd}`
8645
+ and8(
8646
+ eq19(gaSocialReferrals.projectId, project.id),
8647
+ sql5`${gaSocialReferrals.date} >= ${summary.periodStart}`,
8648
+ sql5`${gaSocialReferrals.date} <= ${summary.periodEnd}`
8298
8649
  )
8299
8650
  ).run();
8300
8651
  for (const row of socialReferrals) {
@@ -8313,7 +8664,7 @@ async function ga4Routes(app, opts) {
8313
8664
  }
8314
8665
  }
8315
8666
  if (syncSummary) {
8316
- tx.delete(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).run();
8667
+ tx.delete(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).run();
8317
8668
  tx.insert(gaTrafficSummaries).values({
8318
8669
  id: crypto16.randomUUID(),
8319
8670
  projectId: project.id,
@@ -8327,7 +8678,7 @@ async function ga4Routes(app, opts) {
8327
8678
  }).run();
8328
8679
  }
8329
8680
  });
8330
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq18(runs.id, runId)).run();
8681
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq19(runs.id, runId)).run();
8331
8682
  const syncedComponents = only ? [only, ...only !== "social" && only !== "ai" && only !== "traffic" ? [] : []] : void 0;
8332
8683
  gaLog("info", "sync.complete", {
8333
8684
  projectId: project.id,
@@ -8351,7 +8702,7 @@ async function ga4Routes(app, opts) {
8351
8702
  } catch (e) {
8352
8703
  const msg = e instanceof Error ? e.message : String(e);
8353
8704
  gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
8354
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
8705
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
8355
8706
  throw e;
8356
8707
  }
8357
8708
  });
@@ -8362,48 +8713,48 @@ async function ga4Routes(app, opts) {
8362
8713
  const window = parseWindow(request.query.window);
8363
8714
  const cutoff = windowCutoff(window);
8364
8715
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
8365
- const snapshotConditions = [eq18(gaTrafficSnapshots.projectId, project.id)];
8366
- if (cutoffDate) snapshotConditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
8367
- const aiConditions = [eq18(gaAiReferrals.projectId, project.id)];
8368
- if (cutoffDate) aiConditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
8369
- const socialConditions = [eq18(gaSocialReferrals.projectId, project.id)];
8370
- if (cutoffDate) socialConditions.push(sql4`${gaSocialReferrals.date} >= ${cutoffDate}`);
8716
+ const snapshotConditions = [eq19(gaTrafficSnapshots.projectId, project.id)];
8717
+ if (cutoffDate) snapshotConditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
8718
+ const aiConditions = [eq19(gaAiReferrals.projectId, project.id)];
8719
+ if (cutoffDate) aiConditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
8720
+ const socialConditions = [eq19(gaSocialReferrals.projectId, project.id)];
8721
+ if (cutoffDate) socialConditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
8371
8722
  const summaryRow = cutoffDate ? app.db.select({
8372
- totalSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
8373
- totalOrganicSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
8374
- totalUsers: sql4`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
8375
- }).from(gaTrafficSnapshots).where(and7(...snapshotConditions)).get() : app.db.select({
8723
+ totalSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
8724
+ totalOrganicSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
8725
+ totalUsers: sql5`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
8726
+ }).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).get() : app.db.select({
8376
8727
  totalSessions: gaTrafficSummaries.totalSessions,
8377
8728
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
8378
8729
  totalUsers: gaTrafficSummaries.totalUsers
8379
- }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).get();
8730
+ }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
8380
8731
  const summaryMeta = app.db.select({
8381
8732
  periodStart: gaTrafficSummaries.periodStart,
8382
8733
  periodEnd: gaTrafficSummaries.periodEnd
8383
- }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).get();
8734
+ }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
8384
8735
  const rows = app.db.select({
8385
8736
  landingPage: gaTrafficSnapshots.landingPage,
8386
- sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8387
- organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8388
- users: sql4`SUM(${gaTrafficSnapshots.users})`
8389
- }).from(gaTrafficSnapshots).where(and7(...snapshotConditions)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
8737
+ sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
8738
+ organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
8739
+ users: sql5`SUM(${gaTrafficSnapshots.users})`
8740
+ }).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
8390
8741
  const aiReferrals = app.db.select({
8391
8742
  source: gaAiReferrals.source,
8392
8743
  medium: gaAiReferrals.medium,
8393
8744
  sourceDimension: gaAiReferrals.sourceDimension,
8394
- sessions: sql4`SUM(${gaAiReferrals.sessions})`,
8395
- users: sql4`SUM(${gaAiReferrals.users})`
8396
- }).from(gaAiReferrals).where(and7(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8745
+ sessions: sql5`SUM(${gaAiReferrals.sessions})`,
8746
+ users: sql5`SUM(${gaAiReferrals.users})`
8747
+ }).from(gaAiReferrals).where(and8(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql5`SUM(${gaAiReferrals.sessions}) DESC`).all();
8397
8748
  const aiDeduped = app.db.select({
8398
- sessions: sql4`SUM(max_sessions)`,
8399
- users: sql4`SUM(max_users)`
8749
+ sessions: sql5`SUM(max_sessions)`,
8750
+ users: sql5`SUM(max_users)`
8400
8751
  }).from(
8401
- sql4`(
8752
+ sql5`(
8402
8753
  SELECT date, source, medium,
8403
8754
  MAX(sessions) AS max_sessions,
8404
8755
  MAX(users) AS max_users
8405
8756
  FROM ga_ai_referrals
8406
- WHERE project_id = ${project.id}${cutoffDate ? sql4` AND date >= ${cutoffDate}` : sql4``}
8757
+ WHERE project_id = ${project.id}${cutoffDate ? sql5` AND date >= ${cutoffDate}` : sql5``}
8407
8758
  GROUP BY date, source, medium
8408
8759
  )`
8409
8760
  ).get();
@@ -8411,14 +8762,14 @@ async function ga4Routes(app, opts) {
8411
8762
  source: gaSocialReferrals.source,
8412
8763
  medium: gaSocialReferrals.medium,
8413
8764
  channelGroup: gaSocialReferrals.channelGroup,
8414
- sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
8415
- users: sql4`SUM(${gaSocialReferrals.users})`
8416
- }).from(gaSocialReferrals).where(and7(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql4`SUM(${gaSocialReferrals.sessions}) DESC`).all();
8765
+ sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
8766
+ users: sql5`SUM(${gaSocialReferrals.users})`
8767
+ }).from(gaSocialReferrals).where(and8(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql5`SUM(${gaSocialReferrals.sessions}) DESC`).all();
8417
8768
  const socialTotals = app.db.select({
8418
- sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
8419
- users: sql4`SUM(${gaSocialReferrals.users})`
8420
- }).from(gaSocialReferrals).where(and7(...socialConditions)).get();
8421
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).orderBy(desc8(gaTrafficSummaries.syncedAt)).limit(1).get();
8769
+ sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
8770
+ users: sql5`SUM(${gaSocialReferrals.users})`
8771
+ }).from(gaSocialReferrals).where(and8(...socialConditions)).get();
8772
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
8422
8773
  const total = summaryRow?.totalSessions ?? 0;
8423
8774
  return {
8424
8775
  totalSessions: total,
@@ -8465,8 +8816,8 @@ async function ga4Routes(app, opts) {
8465
8816
  const project = resolveProject(app.db, request.params.name);
8466
8817
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8467
8818
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8468
- const conditions = [eq18(gaAiReferrals.projectId, project.id)];
8469
- if (cutoffDate) conditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
8819
+ const conditions = [eq19(gaAiReferrals.projectId, project.id)];
8820
+ if (cutoffDate) conditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
8470
8821
  const rows = app.db.select({
8471
8822
  date: gaAiReferrals.date,
8472
8823
  source: gaAiReferrals.source,
@@ -8474,15 +8825,15 @@ async function ga4Routes(app, opts) {
8474
8825
  sourceDimension: gaAiReferrals.sourceDimension,
8475
8826
  sessions: gaAiReferrals.sessions,
8476
8827
  users: gaAiReferrals.users
8477
- }).from(gaAiReferrals).where(and7(...conditions)).orderBy(gaAiReferrals.date).all();
8828
+ }).from(gaAiReferrals).where(and8(...conditions)).orderBy(gaAiReferrals.date).all();
8478
8829
  return rows;
8479
8830
  });
8480
8831
  app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
8481
8832
  const project = resolveProject(app.db, request.params.name);
8482
8833
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8483
8834
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8484
- const conditions = [eq18(gaSocialReferrals.projectId, project.id)];
8485
- if (cutoffDate) conditions.push(sql4`${gaSocialReferrals.date} >= ${cutoffDate}`);
8835
+ const conditions = [eq19(gaSocialReferrals.projectId, project.id)];
8836
+ if (cutoffDate) conditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
8486
8837
  const rows = app.db.select({
8487
8838
  date: gaSocialReferrals.date,
8488
8839
  source: gaSocialReferrals.source,
@@ -8490,7 +8841,7 @@ async function ga4Routes(app, opts) {
8490
8841
  channelGroup: gaSocialReferrals.channelGroup,
8491
8842
  sessions: gaSocialReferrals.sessions,
8492
8843
  users: gaSocialReferrals.users
8493
- }).from(gaSocialReferrals).where(and7(...conditions)).orderBy(gaSocialReferrals.date).all();
8844
+ }).from(gaSocialReferrals).where(and8(...conditions)).orderBy(gaSocialReferrals.date).all();
8494
8845
  return rows;
8495
8846
  });
8496
8847
  app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
@@ -8503,10 +8854,10 @@ async function ga4Routes(app, opts) {
8503
8854
  d.setDate(d.getDate() - n);
8504
8855
  return fmt(d);
8505
8856
  };
8506
- const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and7(
8507
- eq18(gaSocialReferrals.projectId, project.id),
8508
- sql4`${gaSocialReferrals.date} >= ${from}`,
8509
- sql4`${gaSocialReferrals.date} < ${to}`
8857
+ const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(
8858
+ eq19(gaSocialReferrals.projectId, project.id),
8859
+ sql5`${gaSocialReferrals.date} >= ${from}`,
8860
+ sql5`${gaSocialReferrals.date} < ${to}`
8510
8861
  )).get();
8511
8862
  const current7d = sumSocial(daysAgo2(7), fmt(today));
8512
8863
  const prev7d = sumSocial(daysAgo2(14), daysAgo2(7));
@@ -8515,19 +8866,19 @@ async function ga4Routes(app, opts) {
8515
8866
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
8516
8867
  const sourceCurrent = app.db.select({
8517
8868
  source: gaSocialReferrals.source,
8518
- sessions: sql4`SUM(${gaSocialReferrals.sessions})`
8519
- }).from(gaSocialReferrals).where(and7(
8520
- eq18(gaSocialReferrals.projectId, project.id),
8521
- sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
8522
- sql4`${gaSocialReferrals.date} < ${fmt(today)}`
8869
+ sessions: sql5`SUM(${gaSocialReferrals.sessions})`
8870
+ }).from(gaSocialReferrals).where(and8(
8871
+ eq19(gaSocialReferrals.projectId, project.id),
8872
+ sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
8873
+ sql5`${gaSocialReferrals.date} < ${fmt(today)}`
8523
8874
  )).groupBy(gaSocialReferrals.source).all();
8524
8875
  const sourcePrev = app.db.select({
8525
8876
  source: gaSocialReferrals.source,
8526
- sessions: sql4`SUM(${gaSocialReferrals.sessions})`
8527
- }).from(gaSocialReferrals).where(and7(
8528
- eq18(gaSocialReferrals.projectId, project.id),
8529
- sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
8530
- sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`
8877
+ sessions: sql5`SUM(${gaSocialReferrals.sessions})`
8878
+ }).from(gaSocialReferrals).where(and8(
8879
+ eq19(gaSocialReferrals.projectId, project.id),
8880
+ sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
8881
+ sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`
8531
8882
  )).groupBy(gaSocialReferrals.source).all();
8532
8883
  const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
8533
8884
  let biggestMover = null;
@@ -8566,15 +8917,15 @@ async function ga4Routes(app, opts) {
8566
8917
  return fmt(d);
8567
8918
  };
8568
8919
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
8569
- const sumTotal = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and7(eq18(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
8570
- const sumOrganic = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and7(eq18(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
8571
- const sumAi = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(max_sessions), 0)` }).from(sql4`(
8920
+ const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
8921
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
8922
+ const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(max_sessions), 0)` }).from(sql5`(
8572
8923
  SELECT date, source, medium, MAX(sessions) AS max_sessions
8573
8924
  FROM ga_ai_referrals
8574
8925
  WHERE project_id = ${project.id} AND date >= ${from} AND date < ${to}
8575
8926
  GROUP BY date, source, medium
8576
8927
  )`).get();
8577
- const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${from}`, sql4`${gaSocialReferrals.date} < ${to}`)).get();
8928
+ const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(eq19(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
8578
8929
  const todayStr = fmt(today);
8579
8930
  const buildTrend = (sum) => {
8580
8931
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -8583,18 +8934,18 @@ async function ga4Routes(app, opts) {
8583
8934
  const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
8584
8935
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
8585
8936
  };
8586
- const aiSourceCurrent = app.db.select({ source: sql4`source`, sessions: sql4`COALESCE(SUM(max_sessions), 0)` }).from(sql4`(
8937
+ const aiSourceCurrent = app.db.select({ source: sql5`source`, sessions: sql5`COALESCE(SUM(max_sessions), 0)` }).from(sql5`(
8587
8938
  SELECT date, source, medium, MAX(sessions) AS max_sessions
8588
8939
  FROM ga_ai_referrals
8589
8940
  WHERE project_id = ${project.id} AND date >= ${daysAgo2(7)} AND date < ${todayStr}
8590
8941
  GROUP BY date, source, medium
8591
- )`).groupBy(sql4`source`).all();
8592
- const aiSourcePrev = app.db.select({ source: sql4`source`, sessions: sql4`COALESCE(SUM(max_sessions), 0)` }).from(sql4`(
8942
+ )`).groupBy(sql5`source`).all();
8943
+ const aiSourcePrev = app.db.select({ source: sql5`source`, sessions: sql5`COALESCE(SUM(max_sessions), 0)` }).from(sql5`(
8593
8944
  SELECT date, source, medium, MAX(sessions) AS max_sessions
8594
8945
  FROM ga_ai_referrals
8595
8946
  WHERE project_id = ${project.id} AND date >= ${daysAgo2(14)} AND date < ${daysAgo2(7)}
8596
8947
  GROUP BY date, source, medium
8597
- )`).groupBy(sql4`source`).all();
8948
+ )`).groupBy(sql5`source`).all();
8598
8949
  const findBiggestMover = (current, prev) => {
8599
8950
  const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
8600
8951
  let mover = null;
@@ -8609,8 +8960,8 @@ async function ga4Routes(app, opts) {
8609
8960
  }
8610
8961
  return mover;
8611
8962
  };
8612
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql4`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
8613
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
8963
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq19(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
8964
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq19(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
8614
8965
  return {
8615
8966
  total: buildTrend(sumTotal),
8616
8967
  organic: buildTrend(sumOrganic),
@@ -8624,14 +8975,14 @@ async function ga4Routes(app, opts) {
8624
8975
  const project = resolveProject(app.db, request.params.name);
8625
8976
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8626
8977
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8627
- const conditions = [eq18(gaTrafficSnapshots.projectId, project.id)];
8628
- if (cutoffDate) conditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
8978
+ const conditions = [eq19(gaTrafficSnapshots.projectId, project.id)];
8979
+ if (cutoffDate) conditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
8629
8980
  const rows = app.db.select({
8630
8981
  date: gaTrafficSnapshots.date,
8631
- sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8632
- organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8633
- users: sql4`SUM(${gaTrafficSnapshots.users})`
8634
- }).from(gaTrafficSnapshots).where(and7(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
8982
+ sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
8983
+ organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
8984
+ users: sql5`SUM(${gaTrafficSnapshots.users})`
8985
+ }).from(gaTrafficSnapshots).where(and8(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
8635
8986
  return rows.map((r) => ({
8636
8987
  date: r.date,
8637
8988
  sessions: r.sessions ?? 0,
@@ -8644,10 +8995,10 @@ async function ga4Routes(app, opts) {
8644
8995
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8645
8996
  const trafficPages = app.db.select({
8646
8997
  landingPage: gaTrafficSnapshots.landingPage,
8647
- sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8648
- organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8649
- users: sql4`SUM(${gaTrafficSnapshots.users})`
8650
- }).from(gaTrafficSnapshots).where(eq18(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
8998
+ sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
8999
+ organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
9000
+ users: sql5`SUM(${gaTrafficSnapshots.users})`
9001
+ }).from(gaTrafficSnapshots).where(eq19(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
8651
9002
  return {
8652
9003
  pages: trafficPages.map((r) => ({
8653
9004
  landingPage: r.landingPage,
@@ -10284,7 +10635,7 @@ async function wordpressRoutes(app, opts) {
10284
10635
 
10285
10636
  // ../api-routes/src/backlinks.ts
10286
10637
  import crypto18 from "crypto";
10287
- import { and as and8, asc as asc2, desc as desc9, eq as eq19, sql as sql5 } from "drizzle-orm";
10638
+ import { and as and9, asc as asc2, desc as desc10, eq as eq20, sql as sql6 } from "drizzle-orm";
10288
10639
 
10289
10640
  // ../integration-commoncrawl/src/constants.ts
10290
10641
  import os2 from "os";
@@ -10514,7 +10865,7 @@ async function queryBacklinks(opts) {
10514
10865
  const reversed = opts.targets.map(reverseDomain);
10515
10866
  const targetList = reversed.map(quote).join(", ");
10516
10867
  const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
10517
- const sql10 = `
10868
+ const sql11 = `
10518
10869
  WITH vertices AS (
10519
10870
  SELECT * FROM read_csv(
10520
10871
  ${quote(opts.vertexPath)},
@@ -10550,7 +10901,7 @@ async function queryBacklinks(opts) {
10550
10901
  const conn = await instance.connect();
10551
10902
  let rows;
10552
10903
  try {
10553
- const reader = await conn.runAndReadAll(sql10);
10904
+ const reader = await conn.runAndReadAll(sql11);
10554
10905
  rows = reader.getRowObjects();
10555
10906
  } finally {
10556
10907
  conn.disconnectSync?.();
@@ -10682,8 +11033,8 @@ function mapRunRow(row) {
10682
11033
  };
10683
11034
  }
10684
11035
  function latestSummaryForProject(db, projectId, release) {
10685
- const condition = release ? and8(eq19(backlinkSummaries.projectId, projectId), eq19(backlinkSummaries.release, release)) : eq19(backlinkSummaries.projectId, projectId);
10686
- return db.select().from(backlinkSummaries).where(condition).orderBy(desc9(backlinkSummaries.queriedAt)).limit(1).get();
11036
+ const condition = release ? and9(eq20(backlinkSummaries.projectId, projectId), eq20(backlinkSummaries.release, release)) : eq20(backlinkSummaries.projectId, projectId);
11037
+ return db.select().from(backlinkSummaries).where(condition).orderBy(desc10(backlinkSummaries.queriedAt)).limit(1).get();
10687
11038
  }
10688
11039
  async function backlinksRoutes(app, opts) {
10689
11040
  app.get("/backlinks/status", async (_request, reply) => {
@@ -10712,7 +11063,7 @@ async function backlinksRoutes(app, opts) {
10712
11063
  "@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
10713
11064
  );
10714
11065
  }
10715
- const existing = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.release, release)).get();
11066
+ const existing = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.release, release)).get();
10716
11067
  const now = (/* @__PURE__ */ new Date()).toISOString();
10717
11068
  if (existing) {
10718
11069
  if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
@@ -10723,9 +11074,9 @@ async function backlinksRoutes(app, opts) {
10723
11074
  phaseDetail: null,
10724
11075
  error: null,
10725
11076
  updatedAt: now
10726
- }).where(eq19(ccReleaseSyncs.id, existing.id)).run();
11077
+ }).where(eq20(ccReleaseSyncs.id, existing.id)).run();
10727
11078
  opts.onReleaseSyncRequested(existing.id, release);
10728
- const refreshed = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.id, existing.id)).get();
11079
+ const refreshed = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.id, existing.id)).get();
10729
11080
  return reply.status(200).send(mapSyncRow(refreshed));
10730
11081
  }
10731
11082
  const id = crypto18.randomUUID();
@@ -10737,15 +11088,15 @@ async function backlinksRoutes(app, opts) {
10737
11088
  updatedAt: now
10738
11089
  }).run();
10739
11090
  opts.onReleaseSyncRequested(id, release);
10740
- const inserted = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.id, id)).get();
11091
+ const inserted = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.id, id)).get();
10741
11092
  return reply.status(201).send(mapSyncRow(inserted));
10742
11093
  });
10743
11094
  app.get("/backlinks/syncs/latest", async (_request, reply) => {
10744
- const row = app.db.select().from(ccReleaseSyncs).orderBy(desc9(ccReleaseSyncs.updatedAt)).limit(1).get();
11095
+ const row = app.db.select().from(ccReleaseSyncs).orderBy(desc10(ccReleaseSyncs.updatedAt)).limit(1).get();
10745
11096
  return reply.send(row ? mapSyncRow(row) : null);
10746
11097
  });
10747
11098
  app.get("/backlinks/syncs", async (_request, reply) => {
10748
- const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc9(ccReleaseSyncs.updatedAt)).all();
11099
+ const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc10(ccReleaseSyncs.updatedAt)).all();
10749
11100
  return reply.send(rows.map(mapSyncRow));
10750
11101
  });
10751
11102
  app.get("/backlinks/releases", async (_request, reply) => {
@@ -10788,7 +11139,7 @@ async function backlinksRoutes(app, opts) {
10788
11139
  createdAt: now
10789
11140
  }).run();
10790
11141
  opts.onBacklinkExtractRequested(runId, project.id, release);
10791
- const run = app.db.select().from(runs).where(eq19(runs.id, runId)).get();
11142
+ const run = app.db.select().from(runs).where(eq20(runs.id, runId)).get();
10792
11143
  return reply.status(201).send(mapRunRow(run));
10793
11144
  });
10794
11145
  app.get(
@@ -10809,15 +11160,15 @@ async function backlinksRoutes(app, opts) {
10809
11160
  }
10810
11161
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
10811
11162
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
10812
- const domainCondition = and8(
10813
- eq19(backlinkDomains.projectId, project.id),
10814
- eq19(backlinkDomains.release, targetRelease)
11163
+ const domainCondition = and9(
11164
+ eq20(backlinkDomains.projectId, project.id),
11165
+ eq20(backlinkDomains.release, targetRelease)
10815
11166
  );
10816
- const totalRow = app.db.select({ count: sql5`count(*)` }).from(backlinkDomains).where(domainCondition).get();
11167
+ const totalRow = app.db.select({ count: sql6`count(*)` }).from(backlinkDomains).where(domainCondition).get();
10817
11168
  const rows = app.db.select({
10818
11169
  linkingDomain: backlinkDomains.linkingDomain,
10819
11170
  numHosts: backlinkDomains.numHosts
10820
- }).from(backlinkDomains).where(domainCondition).orderBy(desc9(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
11171
+ }).from(backlinkDomains).where(domainCondition).orderBy(desc10(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
10821
11172
  const response = {
10822
11173
  summary: summaryRow ? mapSummaryRow(summaryRow) : null,
10823
11174
  total: Number(totalRow?.count ?? 0),
@@ -10829,7 +11180,7 @@ async function backlinksRoutes(app, opts) {
10829
11180
  "/projects/:name/backlinks/history",
10830
11181
  async (request, reply) => {
10831
11182
  const project = resolveProject(app.db, request.params.name);
10832
- const rows = app.db.select().from(backlinkSummaries).where(eq19(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
11183
+ const rows = app.db.select().from(backlinkSummaries).where(eq20(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
10833
11184
  const response = rows.map((r) => ({
10834
11185
  release: r.release,
10835
11186
  totalLinkingDomains: r.totalLinkingDomains,
@@ -10902,6 +11253,7 @@ async function apiRoutes(app, opts) {
10902
11253
  await api.register(historyRoutes);
10903
11254
  await api.register(analyticsRoutes);
10904
11255
  await api.register(intelligenceRoutes);
11256
+ await api.register(compositeRoutes);
10905
11257
  await api.register(contentRoutes);
10906
11258
  await api.register(settingsRoutes, {
10907
11259
  providerSummary: opts.providerSummary,
@@ -13424,7 +13776,7 @@ import crypto19 from "crypto";
13424
13776
  import fs7 from "fs";
13425
13777
  import path9 from "path";
13426
13778
  import os4 from "os";
13427
- import { and as and9, eq as eq20, inArray as inArray4, sql as sql6 } from "drizzle-orm";
13779
+ import { and as and10, eq as eq21, inArray as inArray4, sql as sql7 } from "drizzle-orm";
13428
13780
 
13429
13781
  // src/citation-utils.ts
13430
13782
  function domainMatches(domain, canonicalDomain) {
@@ -13680,7 +14032,7 @@ var JobRunner = class {
13680
14032
  if (stale.length === 0) return;
13681
14033
  const now = (/* @__PURE__ */ new Date()).toISOString();
13682
14034
  for (const run of stale) {
13683
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq20(runs.id, run.id)).run();
14035
+ this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq21(runs.id, run.id)).run();
13684
14036
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
13685
14037
  }
13686
14038
  }
@@ -13708,10 +14060,10 @@ var JobRunner = class {
13708
14060
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
13709
14061
  }
13710
14062
  if (existingRun.status === "queued") {
13711
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and9(eq20(runs.id, runId), eq20(runs.status, "queued"))).run();
14063
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and10(eq21(runs.id, runId), eq21(runs.status, "queued"))).run();
13712
14064
  }
13713
14065
  this.throwIfRunCancelled(runId);
13714
- const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
14066
+ const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
13715
14067
  if (!project) {
13716
14068
  throw new Error(`Project ${projectId} not found`);
13717
14069
  }
@@ -13731,8 +14083,8 @@ var JobRunner = class {
13731
14083
  throw new Error("No providers configured. Add at least one provider API key.");
13732
14084
  }
13733
14085
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
13734
- projectKeywords = this.db.select().from(keywords).where(eq20(keywords.projectId, projectId)).all();
13735
- const projectCompetitors = this.db.select().from(competitors).where(eq20(competitors.projectId, projectId)).all();
14086
+ projectKeywords = this.db.select().from(keywords).where(eq21(keywords.projectId, projectId)).all();
14087
+ const projectCompetitors = this.db.select().from(competitors).where(eq21(competitors.projectId, projectId)).all();
13736
14088
  const competitorDomains = projectCompetitors.map((c) => c.domain);
13737
14089
  const allDomains = effectiveDomains({
13738
14090
  canonicalDomain: project.canonicalDomain,
@@ -13748,7 +14100,7 @@ var JobRunner = class {
13748
14100
  const todayPeriod = getCurrentUsageDay();
13749
14101
  for (const p of activeProviders) {
13750
14102
  const providerScope = `${projectId}:${p.adapter.name}`;
13751
- const providerUsage = this.db.select().from(usageCounters).where(eq20(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
14103
+ const providerUsage = this.db.select().from(usageCounters).where(eq21(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
13752
14104
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
13753
14105
  if (providerUsage + queriesPerProvider > limit) {
13754
14106
  throw new Error(
@@ -13889,12 +14241,12 @@ var JobRunner = class {
13889
14241
  const someFailed = providerErrors.size > 0;
13890
14242
  if (allFailed) {
13891
14243
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
13892
- this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq20(runs.id, runId)).run();
14244
+ this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq21(runs.id, runId)).run();
13893
14245
  } else if (someFailed) {
13894
14246
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
13895
- this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq20(runs.id, runId)).run();
14247
+ this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq21(runs.id, runId)).run();
13896
14248
  } else {
13897
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14249
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
13898
14250
  }
13899
14251
  this.flushProviderUsage(projectId, providerDispatchCounts);
13900
14252
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -13929,7 +14281,7 @@ var JobRunner = class {
13929
14281
  status: "failed",
13930
14282
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13931
14283
  error: errorMessage
13932
- }).where(eq20(runs.id, runId)).run();
14284
+ }).where(eq21(runs.id, runId)).run();
13933
14285
  this.flushProviderUsage(projectId, providerDispatchCounts);
13934
14286
  trackEvent("run.completed", {
13935
14287
  status: "failed",
@@ -13958,7 +14310,7 @@ var JobRunner = class {
13958
14310
  updatedAt: now
13959
14311
  }).onConflictDoUpdate({
13960
14312
  target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
13961
- set: { count: sql6`${usageCounters.count} + ${count}`, updatedAt: now }
14313
+ set: { count: sql7`${usageCounters.count} + ${count}`, updatedAt: now }
13962
14314
  }).run();
13963
14315
  }
13964
14316
  flushProviderUsage(projectId, providerDispatchCounts) {
@@ -13972,7 +14324,7 @@ var JobRunner = class {
13972
14324
  status: runs.status,
13973
14325
  finishedAt: runs.finishedAt,
13974
14326
  error: runs.error
13975
- }).from(runs).where(eq20(runs.id, runId)).get();
14327
+ }).from(runs).where(eq21(runs.id, runId)).get();
13976
14328
  }
13977
14329
  isRunCancelled(runId) {
13978
14330
  return this.getRunState(runId)?.status === "cancelled";
@@ -13988,7 +14340,7 @@ var JobRunner = class {
13988
14340
  this.db.update(runs).set({
13989
14341
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13990
14342
  error: currentRun.error ?? "Cancelled by user"
13991
- }).where(eq20(runs.id, runId)).run();
14343
+ }).where(eq21(runs.id, runId)).run();
13992
14344
  }
13993
14345
  trackEvent("run.completed", {
13994
14346
  status: "cancelled",
@@ -14011,7 +14363,7 @@ function getCurrentUsageDay() {
14011
14363
 
14012
14364
  // src/gsc-sync.ts
14013
14365
  import crypto20 from "crypto";
14014
- import { eq as eq21, and as and10, sql as sql7 } from "drizzle-orm";
14366
+ import { eq as eq22, and as and11, sql as sql8 } from "drizzle-orm";
14015
14367
  var log2 = createLogger("GscSync");
14016
14368
  function formatDate2(d) {
14017
14369
  return d.toISOString().split("T")[0];
@@ -14023,13 +14375,13 @@ function daysAgo(n) {
14023
14375
  }
14024
14376
  async function executeGscSync(db, runId, projectId, opts) {
14025
14377
  const now = (/* @__PURE__ */ new Date()).toISOString();
14026
- db.update(runs).set({ status: "running", startedAt: now }).where(eq21(runs.id, runId)).run();
14378
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq22(runs.id, runId)).run();
14027
14379
  try {
14028
14380
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
14029
14381
  if (!googleClientId || !googleClientSecret) {
14030
14382
  throw new Error("Google OAuth is not configured in the local Canonry config");
14031
14383
  }
14032
- const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
14384
+ const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
14033
14385
  if (!project) {
14034
14386
  throw new Error(`Project not found: ${projectId}`);
14035
14387
  }
@@ -14063,10 +14415,10 @@ async function executeGscSync(db, runId, projectId, opts) {
14063
14415
  });
14064
14416
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
14065
14417
  db.delete(gscSearchData).where(
14066
- and10(
14067
- eq21(gscSearchData.projectId, projectId),
14068
- sql7`${gscSearchData.date} >= ${startDate}`,
14069
- sql7`${gscSearchData.date} <= ${endDate}`
14418
+ and11(
14419
+ eq22(gscSearchData.projectId, projectId),
14420
+ sql8`${gscSearchData.date} >= ${startDate}`,
14421
+ sql8`${gscSearchData.date} <= ${endDate}`
14070
14422
  )
14071
14423
  ).run();
14072
14424
  const batchSize = 500;
@@ -14131,7 +14483,7 @@ async function executeGscSync(db, runId, projectId, opts) {
14131
14483
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
14132
14484
  }
14133
14485
  }
14134
- const allInspections = db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, projectId)).all();
14486
+ const allInspections = db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, projectId)).all();
14135
14487
  const latestByUrl = /* @__PURE__ */ new Map();
14136
14488
  for (const row of allInspections) {
14137
14489
  const existing = latestByUrl.get(row.url);
@@ -14152,7 +14504,7 @@ async function executeGscSync(db, runId, projectId, opts) {
14152
14504
  }
14153
14505
  }
14154
14506
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
14155
- db.delete(gscCoverageSnapshots).where(and10(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
14507
+ db.delete(gscCoverageSnapshots).where(and11(eq22(gscCoverageSnapshots.projectId, projectId), eq22(gscCoverageSnapshots.date, snapshotDate))).run();
14156
14508
  db.insert(gscCoverageSnapshots).values({
14157
14509
  id: crypto20.randomUUID(),
14158
14510
  projectId,
@@ -14163,11 +14515,11 @@ async function executeGscSync(db, runId, projectId, opts) {
14163
14515
  reasonBreakdown: JSON.stringify(reasonCounts),
14164
14516
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14165
14517
  }).run();
14166
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
14518
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
14167
14519
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
14168
14520
  } catch (err) {
14169
14521
  const errorMsg = err instanceof Error ? err.message : String(err);
14170
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
14522
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
14171
14523
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
14172
14524
  throw err;
14173
14525
  }
@@ -14175,7 +14527,7 @@ async function executeGscSync(db, runId, projectId, opts) {
14175
14527
 
14176
14528
  // src/gsc-inspect-sitemap.ts
14177
14529
  import crypto21 from "crypto";
14178
- import { eq as eq22, and as and11 } from "drizzle-orm";
14530
+ import { eq as eq23, and as and12 } from "drizzle-orm";
14179
14531
 
14180
14532
  // src/sitemap-parser.ts
14181
14533
  var log3 = createLogger("SitemapParser");
@@ -14296,13 +14648,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
14296
14648
  var log4 = createLogger("InspectSitemap");
14297
14649
  async function executeInspectSitemap(db, runId, projectId, opts) {
14298
14650
  const now = (/* @__PURE__ */ new Date()).toISOString();
14299
- db.update(runs).set({ status: "running", startedAt: now }).where(eq22(runs.id, runId)).run();
14651
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq23(runs.id, runId)).run();
14300
14652
  try {
14301
14653
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
14302
14654
  if (!googleClientId || !googleClientSecret) {
14303
14655
  throw new Error("Google OAuth is not configured in the local Canonry config");
14304
14656
  }
14305
- const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
14657
+ const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
14306
14658
  if (!project) {
14307
14659
  throw new Error(`Project not found: ${projectId}`);
14308
14660
  }
@@ -14370,7 +14722,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14370
14722
  await new Promise((r) => setTimeout(r, 1e3));
14371
14723
  }
14372
14724
  }
14373
- const allInspections = db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, projectId)).all();
14725
+ const allInspections = db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, projectId)).all();
14374
14726
  const latestByUrl = /* @__PURE__ */ new Map();
14375
14727
  for (const row of allInspections) {
14376
14728
  const existing = latestByUrl.get(row.url);
@@ -14391,7 +14743,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14391
14743
  }
14392
14744
  }
14393
14745
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
14394
- db.delete(gscCoverageSnapshots).where(and11(eq22(gscCoverageSnapshots.projectId, projectId), eq22(gscCoverageSnapshots.date, snapshotDate))).run();
14746
+ db.delete(gscCoverageSnapshots).where(and12(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
14395
14747
  db.insert(gscCoverageSnapshots).values({
14396
14748
  id: crypto21.randomUUID(),
14397
14749
  projectId,
@@ -14403,11 +14755,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14403
14755
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14404
14756
  }).run();
14405
14757
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
14406
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
14758
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
14407
14759
  log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
14408
14760
  } catch (err) {
14409
14761
  const errorMsg = err instanceof Error ? err.message : String(err);
14410
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
14762
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
14411
14763
  log4.error("inspect.failed", { runId, projectId, error: errorMsg });
14412
14764
  throw err;
14413
14765
  }
@@ -14415,7 +14767,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14415
14767
 
14416
14768
  // src/bing-inspect-sitemap.ts
14417
14769
  import crypto22 from "crypto";
14418
- import { eq as eq23, desc as desc10 } from "drizzle-orm";
14770
+ import { eq as eq24, desc as desc11 } from "drizzle-orm";
14419
14771
  var log5 = createLogger("BingInspectSitemap");
14420
14772
  function parseBingDate2(value) {
14421
14773
  if (!value) return null;
@@ -14433,9 +14785,9 @@ function isBlockingIssueType2(issueType) {
14433
14785
  }
14434
14786
  async function executeBingInspectSitemap(db, runId, projectId, opts) {
14435
14787
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14436
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq23(runs.id, runId)).run();
14788
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq24(runs.id, runId)).run();
14437
14789
  try {
14438
- const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
14790
+ const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
14439
14791
  if (!project) {
14440
14792
  throw new Error(`Project not found: ${projectId}`);
14441
14793
  }
@@ -14453,7 +14805,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14453
14805
  if (sitemapUrls.length === 0) {
14454
14806
  throw new Error("No URLs found in sitemap");
14455
14807
  }
14456
- const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, projectId)).all();
14808
+ const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, projectId)).all();
14457
14809
  const trackedUrls = new Set(trackedRows.map((r) => r.url));
14458
14810
  const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
14459
14811
  log5.info("sitemap.diff", {
@@ -14536,7 +14888,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14536
14888
  await new Promise((r) => setTimeout(r, 1e3));
14537
14889
  }
14538
14890
  }
14539
- const allInspections = db.select().from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, projectId)).orderBy(desc10(bingUrlInspections.inspectedAt)).all();
14891
+ const allInspections = db.select().from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, projectId)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
14540
14892
  const latestByUrl = /* @__PURE__ */ new Map();
14541
14893
  const definitiveByUrl = /* @__PURE__ */ new Map();
14542
14894
  for (const row of allInspections) {
@@ -14579,7 +14931,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14579
14931
  }
14580
14932
  }).run();
14581
14933
  const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
14582
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
14934
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
14583
14935
  log5.info("inspect.completed", {
14584
14936
  runId,
14585
14937
  projectId,
@@ -14593,7 +14945,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14593
14945
  });
14594
14946
  } catch (err) {
14595
14947
  const errorMsg = err instanceof Error ? err.message : String(err);
14596
- db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
14948
+ db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
14597
14949
  log5.error("inspect.failed", { runId, projectId, error: errorMsg });
14598
14950
  throw err;
14599
14951
  }
@@ -14602,7 +14954,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14602
14954
  // src/commoncrawl-sync.ts
14603
14955
  import crypto23 from "crypto";
14604
14956
  import path10 from "path";
14605
- import { and as and12, eq as eq24, sql as sql8 } from "drizzle-orm";
14957
+ import { and as and13, eq as eq25, sql as sql9 } from "drizzle-orm";
14606
14958
  var log6 = createLogger("CommonCrawlSync");
14607
14959
  var INSERT_CHUNK_SIZE = 1e4;
14608
14960
  function defaultDeps() {
@@ -14628,7 +14980,7 @@ async function executeReleaseSync(db, syncId, opts) {
14628
14980
  phaseDetail: "downloading vertices + edges",
14629
14981
  updatedAt: downloadStartedAt,
14630
14982
  error: null
14631
- }).where(eq24(ccReleaseSyncs.id, syncId)).run();
14983
+ }).where(eq25(ccReleaseSyncs.id, syncId)).run();
14632
14984
  const paths = ccReleasePaths(release);
14633
14985
  const releaseCacheDir = path10.join(deps.cacheDir, release);
14634
14986
  const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
@@ -14651,7 +15003,7 @@ async function executeReleaseSync(db, syncId, opts) {
14651
15003
  vertexSha256: vertex.sha256,
14652
15004
  edgesSha256: edges.sha256,
14653
15005
  updatedAt: downloadFinishedAt
14654
- }).where(eq24(ccReleaseSyncs.id, syncId)).run();
15006
+ }).where(eq25(ccReleaseSyncs.id, syncId)).run();
14655
15007
  const allProjects = db.select().from(projects).all();
14656
15008
  const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
14657
15009
  let rows = [];
@@ -14667,8 +15019,8 @@ async function executeReleaseSync(db, syncId, opts) {
14667
15019
  }
14668
15020
  const queriedAt = deps.now().toISOString();
14669
15021
  db.transaction((tx) => {
14670
- tx.delete(backlinkDomains).where(eq24(backlinkDomains.releaseSyncId, syncId)).run();
14671
- tx.delete(backlinkSummaries).where(eq24(backlinkSummaries.releaseSyncId, syncId)).run();
15022
+ tx.delete(backlinkDomains).where(eq25(backlinkDomains.releaseSyncId, syncId)).run();
15023
+ tx.delete(backlinkSummaries).where(eq25(backlinkSummaries.releaseSyncId, syncId)).run();
14672
15024
  const expanded = [];
14673
15025
  for (const r of rows) {
14674
15026
  const projectIds = projectsByDomain.get(r.targetDomain);
@@ -14727,7 +15079,7 @@ async function executeReleaseSync(db, syncId, opts) {
14727
15079
  domainsDiscovered: rows.length,
14728
15080
  updatedAt: finishedAt,
14729
15081
  error: null
14730
- }).where(eq24(ccReleaseSyncs.id, syncId)).run();
15082
+ }).where(eq25(ccReleaseSyncs.id, syncId)).run();
14731
15083
  log6.info("sync.completed", {
14732
15084
  syncId,
14733
15085
  release,
@@ -14757,7 +15109,7 @@ async function executeReleaseSync(db, syncId, opts) {
14757
15109
  error: errorMsg,
14758
15110
  phaseDetail: null,
14759
15111
  updatedAt: finishedAt
14760
- }).where(eq24(ccReleaseSyncs.id, syncId)).run();
15112
+ }).where(eq25(ccReleaseSyncs.id, syncId)).run();
14761
15113
  log6.error("sync.failed", { syncId, release, error: errorMsg });
14762
15114
  throw err;
14763
15115
  }
@@ -14793,7 +15145,7 @@ function computeSummary(rows) {
14793
15145
  // src/backlink-extract.ts
14794
15146
  import crypto24 from "crypto";
14795
15147
  import fs8 from "fs";
14796
- import { and as and13, desc as desc11, eq as eq25 } from "drizzle-orm";
15148
+ import { and as and14, desc as desc12, eq as eq26 } from "drizzle-orm";
14797
15149
  var log7 = createLogger("BacklinkExtract");
14798
15150
  function defaultDeps2() {
14799
15151
  return {
@@ -14805,13 +15157,13 @@ function defaultDeps2() {
14805
15157
  async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14806
15158
  const deps = { ...defaultDeps2(), ...opts.deps };
14807
15159
  const startedAt = deps.now().toISOString();
14808
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq25(runs.id, runId)).run();
15160
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq26(runs.id, runId)).run();
14809
15161
  try {
14810
- const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
15162
+ const project = db.select().from(projects).where(eq26(projects.id, projectId)).get();
14811
15163
  if (!project) {
14812
15164
  throw new Error(`Project not found: ${projectId}`);
14813
15165
  }
14814
- const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc11(ccReleaseSyncs.createdAt)).limit(1).get();
15166
+ const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc12(ccReleaseSyncs.createdAt)).limit(1).get();
14815
15167
  if (!sync) {
14816
15168
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
14817
15169
  }
@@ -14839,7 +15191,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14839
15191
  const targetDomain = project.canonicalDomain;
14840
15192
  db.transaction((tx) => {
14841
15193
  tx.delete(backlinkDomains).where(
14842
- and13(eq25(backlinkDomains.projectId, projectId), eq25(backlinkDomains.release, release))
15194
+ and14(eq26(backlinkDomains.projectId, projectId), eq26(backlinkDomains.release, release))
14843
15195
  ).run();
14844
15196
  if (rows.length > 0) {
14845
15197
  const values = rows.map((r) => ({
@@ -14879,7 +15231,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14879
15231
  }).run();
14880
15232
  });
14881
15233
  const finishedAt = deps.now().toISOString();
14882
- db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq25(runs.id, runId)).run();
15234
+ db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
14883
15235
  log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
14884
15236
  } catch (err) {
14885
15237
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -14888,7 +15240,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14888
15240
  status: RunStatuses.failed,
14889
15241
  error: errorMsg,
14890
15242
  finishedAt
14891
- }).where(eq25(runs.id, runId)).run();
15243
+ }).where(eq26(runs.id, runId)).run();
14892
15244
  log7.error("extract.failed", { runId, projectId, error: errorMsg });
14893
15245
  throw err;
14894
15246
  }
@@ -14961,7 +15313,7 @@ var ProviderRegistry = class {
14961
15313
 
14962
15314
  // src/scheduler.ts
14963
15315
  import cron from "node-cron";
14964
- import { eq as eq26 } from "drizzle-orm";
15316
+ import { eq as eq27 } from "drizzle-orm";
14965
15317
  var log8 = createLogger("Scheduler");
14966
15318
  var Scheduler = class {
14967
15319
  db;
@@ -14973,7 +15325,7 @@ var Scheduler = class {
14973
15325
  }
14974
15326
  /** Load all enabled schedules from DB and register cron jobs. */
14975
15327
  start() {
14976
- const allSchedules = this.db.select().from(schedules).where(eq26(schedules.enabled, 1)).all();
15328
+ const allSchedules = this.db.select().from(schedules).where(eq27(schedules.enabled, 1)).all();
14977
15329
  for (const schedule of allSchedules) {
14978
15330
  const missedRunAt = schedule.nextRunAt;
14979
15331
  this.registerCronTask(schedule);
@@ -14998,7 +15350,7 @@ var Scheduler = class {
14998
15350
  this.stopTask(projectId, existing, "Stopped");
14999
15351
  this.tasks.delete(projectId);
15000
15352
  }
15001
- const schedule = this.db.select().from(schedules).where(eq26(schedules.projectId, projectId)).get();
15353
+ const schedule = this.db.select().from(schedules).where(eq27(schedules.projectId, projectId)).get();
15002
15354
  if (schedule && schedule.enabled === 1) {
15003
15355
  this.registerCronTask(schedule);
15004
15356
  }
@@ -15031,14 +15383,14 @@ var Scheduler = class {
15031
15383
  this.db.update(schedules).set({
15032
15384
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
15033
15385
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15034
- }).where(eq26(schedules.id, scheduleId)).run();
15386
+ }).where(eq27(schedules.id, scheduleId)).run();
15035
15387
  const label = schedule.preset ?? cronExpr;
15036
15388
  log8.info("cron.registered", { projectId, schedule: label, timezone });
15037
15389
  }
15038
15390
  triggerRun(scheduleId, projectId) {
15039
15391
  try {
15040
15392
  const now = (/* @__PURE__ */ new Date()).toISOString();
15041
- const currentSchedule = this.db.select().from(schedules).where(eq26(schedules.id, scheduleId)).get();
15393
+ const currentSchedule = this.db.select().from(schedules).where(eq27(schedules.id, scheduleId)).get();
15042
15394
  if (!currentSchedule || currentSchedule.enabled !== 1) {
15043
15395
  log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
15044
15396
  this.remove(projectId);
@@ -15046,7 +15398,7 @@ var Scheduler = class {
15046
15398
  }
15047
15399
  const task = this.tasks.get(projectId);
15048
15400
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
15049
- const project = this.db.select().from(projects).where(eq26(projects.id, projectId)).get();
15401
+ const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
15050
15402
  if (!project) {
15051
15403
  log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
15052
15404
  this.remove(projectId);
@@ -15075,7 +15427,7 @@ var Scheduler = class {
15075
15427
  this.db.update(schedules).set({
15076
15428
  nextRunAt,
15077
15429
  updatedAt: now
15078
- }).where(eq26(schedules.id, currentSchedule.id)).run();
15430
+ }).where(eq27(schedules.id, currentSchedule.id)).run();
15079
15431
  return;
15080
15432
  }
15081
15433
  const runId = queueResult.runId;
@@ -15083,7 +15435,7 @@ var Scheduler = class {
15083
15435
  lastRunAt: now,
15084
15436
  nextRunAt,
15085
15437
  updatedAt: now
15086
- }).where(eq26(schedules.id, currentSchedule.id)).run();
15438
+ }).where(eq27(schedules.id, currentSchedule.id)).run();
15087
15439
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
15088
15440
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
15089
15441
  log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -15095,7 +15447,7 @@ var Scheduler = class {
15095
15447
  };
15096
15448
 
15097
15449
  // src/notifier.ts
15098
- import { eq as eq27, desc as desc12, and as and14, or as or2 } from "drizzle-orm";
15450
+ import { eq as eq28, desc as desc13, and as and15, or as or3 } from "drizzle-orm";
15099
15451
  import crypto25 from "crypto";
15100
15452
  var log9 = createLogger("Notifier");
15101
15453
  var Notifier = class {
@@ -15108,18 +15460,18 @@ var Notifier = class {
15108
15460
  /** Called after a run completes (success, partial, or failed). */
15109
15461
  async onRunCompleted(runId, projectId) {
15110
15462
  log9.info("run.completed", { runId, projectId });
15111
- const notifs = this.db.select().from(notifications).where(eq27(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
15463
+ const notifs = this.db.select().from(notifications).where(eq28(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
15112
15464
  if (notifs.length === 0) {
15113
15465
  log9.info("notifications.none-enabled", { projectId });
15114
15466
  return;
15115
15467
  }
15116
15468
  log9.info("notifications.found", { projectId, count: notifs.length });
15117
- const run = this.db.select().from(runs).where(eq27(runs.id, runId)).get();
15469
+ const run = this.db.select().from(runs).where(eq28(runs.id, runId)).get();
15118
15470
  if (!run) {
15119
15471
  log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
15120
15472
  return;
15121
15473
  }
15122
- const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
15474
+ const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
15123
15475
  if (!project) {
15124
15476
  log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
15125
15477
  return;
@@ -15166,11 +15518,11 @@ var Notifier = class {
15166
15518
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
15167
15519
  if (highInsights.length > 0) insightEvents.push("insight.high");
15168
15520
  if (insightEvents.length === 0) return;
15169
- const notifs = this.db.select().from(notifications).where(eq27(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
15521
+ const notifs = this.db.select().from(notifications).where(eq28(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
15170
15522
  if (notifs.length === 0) return;
15171
- const run = this.db.select().from(runs).where(eq27(runs.id, runId)).get();
15523
+ const run = this.db.select().from(runs).where(eq28(runs.id, runId)).get();
15172
15524
  if (!run) return;
15173
- const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
15525
+ const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
15174
15526
  if (!project) return;
15175
15527
  for (const notif of notifs) {
15176
15528
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -15201,11 +15553,11 @@ var Notifier = class {
15201
15553
  }
15202
15554
  computeTransitions(runId, projectId) {
15203
15555
  const recentRuns = this.db.select().from(runs).where(
15204
- and14(
15205
- eq27(runs.projectId, projectId),
15206
- or2(eq27(runs.status, "completed"), eq27(runs.status, "partial"))
15556
+ and15(
15557
+ eq28(runs.projectId, projectId),
15558
+ or3(eq28(runs.status, "completed"), eq28(runs.status, "partial"))
15207
15559
  )
15208
- ).orderBy(desc12(runs.createdAt)).limit(2).all();
15560
+ ).orderBy(desc13(runs.createdAt)).limit(2).all();
15209
15561
  if (recentRuns.length < 2) return [];
15210
15562
  const currentRunId = recentRuns[0].id;
15211
15563
  const previousRunId = recentRuns[1].id;
@@ -15215,12 +15567,12 @@ var Notifier = class {
15215
15567
  keyword: keywords.keyword,
15216
15568
  provider: querySnapshots.provider,
15217
15569
  citationState: querySnapshots.citationState
15218
- }).from(querySnapshots).leftJoin(keywords, eq27(querySnapshots.keywordId, keywords.id)).where(eq27(querySnapshots.runId, currentRunId)).all();
15570
+ }).from(querySnapshots).leftJoin(keywords, eq28(querySnapshots.keywordId, keywords.id)).where(eq28(querySnapshots.runId, currentRunId)).all();
15219
15571
  const previousSnapshots = this.db.select({
15220
15572
  keywordId: querySnapshots.keywordId,
15221
15573
  provider: querySnapshots.provider,
15222
15574
  citationState: querySnapshots.citationState
15223
- }).from(querySnapshots).where(eq27(querySnapshots.runId, previousRunId)).all();
15575
+ }).from(querySnapshots).where(eq28(querySnapshots.runId, previousRunId)).all();
15224
15576
  const prevMap = /* @__PURE__ */ new Map();
15225
15577
  for (const s of previousSnapshots) {
15226
15578
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -15337,7 +15689,7 @@ var RunCoordinator = class {
15337
15689
 
15338
15690
  // src/agent/session-registry.ts
15339
15691
  import crypto27 from "crypto";
15340
- import { eq as eq29 } from "drizzle-orm";
15692
+ import { eq as eq30 } from "drizzle-orm";
15341
15693
 
15342
15694
  // src/agent/session.ts
15343
15695
  import fs11 from "fs";
@@ -15557,7 +15909,7 @@ import { Type as Type2 } from "@sinclair/typebox";
15557
15909
 
15558
15910
  // src/agent/memory-store.ts
15559
15911
  import crypto26 from "crypto";
15560
- import { and as and15, desc as desc13, eq as eq28, like, sql as sql9 } from "drizzle-orm";
15912
+ import { and as and16, desc as desc14, eq as eq29, like as like2, sql as sql10 } from "drizzle-orm";
15561
15913
  var COMPACTION_KEY_PREFIX = "compaction:";
15562
15914
  var COMPACTION_NOTES_PER_SESSION = 3;
15563
15915
  function rowToDto(row) {
@@ -15571,7 +15923,7 @@ function rowToDto(row) {
15571
15923
  };
15572
15924
  }
15573
15925
  function listMemoryEntries(db, projectId, opts = {}) {
15574
- const query = db.select().from(agentMemory).where(eq28(agentMemory.projectId, projectId)).orderBy(desc13(agentMemory.updatedAt));
15926
+ const query = db.select().from(agentMemory).where(eq29(agentMemory.projectId, projectId)).orderBy(desc14(agentMemory.updatedAt));
15575
15927
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
15576
15928
  return rows.map(rowToDto);
15577
15929
  }
@@ -15602,12 +15954,12 @@ function upsertMemoryEntry(db, args) {
15602
15954
  updatedAt: now
15603
15955
  }
15604
15956
  }).run();
15605
- const row = db.select().from(agentMemory).where(and15(eq28(agentMemory.projectId, args.projectId), eq28(agentMemory.key, args.key))).get();
15957
+ const row = db.select().from(agentMemory).where(and16(eq29(agentMemory.projectId, args.projectId), eq29(agentMemory.key, args.key))).get();
15606
15958
  if (!row) throw new Error("memory upsert produced no row");
15607
15959
  return rowToDto(row);
15608
15960
  }
15609
15961
  function deleteMemoryEntry(db, projectId, key) {
15610
- const result = db.delete(agentMemory).where(and15(eq28(agentMemory.projectId, projectId), eq28(agentMemory.key, key))).run();
15962
+ const result = db.delete(agentMemory).where(and16(eq29(agentMemory.projectId, projectId), eq29(agentMemory.key, key))).run();
15611
15963
  const changes = result.changes ?? 0;
15612
15964
  return changes > 0;
15613
15965
  }
@@ -15636,16 +15988,16 @@ function writeCompactionNote(db, args) {
15636
15988
  }).run();
15637
15989
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
15638
15990
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
15639
- and15(
15640
- eq28(agentMemory.projectId, args.projectId),
15641
- like(agentMemory.key, `${sessionPrefix}%`)
15991
+ and16(
15992
+ eq29(agentMemory.projectId, args.projectId),
15993
+ like2(agentMemory.key, `${sessionPrefix}%`)
15642
15994
  )
15643
- ).orderBy(desc13(agentMemory.updatedAt)).all();
15995
+ ).orderBy(desc14(agentMemory.updatedAt)).all();
15644
15996
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
15645
15997
  if (stale.length > 0) {
15646
- tx.delete(agentMemory).where(sql9`${agentMemory.id} IN (${sql9.join(stale.map((s) => sql9`${s}`), sql9`, `)})`).run();
15998
+ tx.delete(agentMemory).where(sql10`${agentMemory.id} IN (${sql10.join(stale.map((s) => sql10`${s}`), sql10`, `)})`).run();
15647
15999
  }
15648
- const row = tx.select().from(agentMemory).where(and15(eq28(agentMemory.projectId, args.projectId), eq28(agentMemory.key, key))).get();
16000
+ const row = tx.select().from(agentMemory).where(and16(eq29(agentMemory.projectId, args.projectId), eq29(agentMemory.key, key))).get();
15649
16001
  if (row) inserted = rowToDto(row);
15650
16002
  });
15651
16003
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -16377,7 +16729,7 @@ var SessionRegistry = class {
16377
16729
  modelProvider: effectiveProvider,
16378
16730
  modelId: effectiveModelId,
16379
16731
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16380
- }).where(eq29(agentSessions.projectId, projectId)).run();
16732
+ }).where(eq30(agentSessions.projectId, projectId)).run();
16381
16733
  }
16382
16734
  const agent2 = createAeroSession({
16383
16735
  projectName,
@@ -16595,7 +16947,7 @@ ${lines.join("\n")}
16595
16947
  modelProvider: nextProvider,
16596
16948
  modelId: nextModelId,
16597
16949
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16598
- }).where(eq29(agentSessions.projectId, projectId)).run();
16950
+ }).where(eq30(agentSessions.projectId, projectId)).run();
16599
16951
  }
16600
16952
  /** Persist a session's transcript back to the DB. Call after any run settles. */
16601
16953
  save(projectName) {
@@ -16757,11 +17109,11 @@ ${lines.join("\n")}
16757
17109
  return id;
16758
17110
  }
16759
17111
  tryResolveProjectId(projectName) {
16760
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq29(projects.name, projectName)).get();
17112
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq30(projects.name, projectName)).get();
16761
17113
  return row?.id;
16762
17114
  }
16763
17115
  loadRow(projectId) {
16764
- const row = this.opts.db.select().from(agentSessions).where(eq29(agentSessions.projectId, projectId)).get();
17116
+ const row = this.opts.db.select().from(agentSessions).where(eq30(agentSessions.projectId, projectId)).get();
16765
17117
  return row ?? null;
16766
17118
  }
16767
17119
  insertRow(params) {
@@ -16780,14 +17132,14 @@ ${lines.join("\n")}
16780
17132
  }
16781
17133
  updateRow(projectId, patch) {
16782
17134
  const now = (/* @__PURE__ */ new Date()).toISOString();
16783
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq29(agentSessions.projectId, projectId)).run();
17135
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq30(agentSessions.projectId, projectId)).run();
16784
17136
  }
16785
17137
  };
16786
17138
 
16787
17139
  // src/agent/agent-routes.ts
16788
- import { eq as eq30 } from "drizzle-orm";
17140
+ import { eq as eq31 } from "drizzle-orm";
16789
17141
  function resolveProject2(db, name) {
16790
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq30(projects.name, name)).get();
17142
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq31(projects.name, name)).get();
16791
17143
  if (!row) throw notFound("project", name);
16792
17144
  return row;
16793
17145
  }
@@ -16796,7 +17148,7 @@ function registerAgentRoutes(app, opts) {
16796
17148
  "/projects/:name/agent/transcript",
16797
17149
  async (request) => {
16798
17150
  const project = resolveProject2(opts.db, request.params.name);
16799
- const row = opts.db.select().from(agentSessions).where(eq30(agentSessions.projectId, project.id)).get();
17151
+ const row = opts.db.select().from(agentSessions).where(eq31(agentSessions.projectId, project.id)).get();
16800
17152
  if (!row) {
16801
17153
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
16802
17154
  }
@@ -16820,7 +17172,7 @@ function registerAgentRoutes(app, opts) {
16820
17172
  async (request) => {
16821
17173
  const project = resolveProject2(opts.db, request.params.name);
16822
17174
  opts.sessionRegistry.reset(project.name);
16823
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(agentSessions.projectId, project.id)).run();
17175
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(agentSessions.projectId, project.id)).run();
16824
17176
  return { status: "reset" };
16825
17177
  }
16826
17178
  );
@@ -17842,7 +18194,7 @@ async function createServer(opts) {
17842
18194
  intelligenceService,
17843
18195
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
17844
18196
  async ({ runId, projectId, insightCount, criticalOrHigh }) => {
17845
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq31(projects.id, projectId)).get();
18197
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq32(projects.id, projectId)).get();
17846
18198
  if (!project) return;
17847
18199
  sessionRegistry.queueFollowUp(project.name, {
17848
18200
  role: "user",
@@ -17982,7 +18334,7 @@ async function createServer(opts) {
17982
18334
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
17983
18335
  if (opts.config.apiKey) {
17984
18336
  const keyHash = hashApiKey(opts.config.apiKey);
17985
- const existing = opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, keyHash)).get();
18337
+ const existing = opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, keyHash)).get();
17986
18338
  if (!existing) {
17987
18339
  const prefix = opts.config.apiKey.slice(0, 12);
17988
18340
  opts.db.insert(apiKeys).values({
@@ -18034,7 +18386,7 @@ async function createServer(opts) {
18034
18386
  };
18035
18387
  const getDefaultApiKey = () => {
18036
18388
  if (!opts.config.apiKey) return void 0;
18037
- return opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
18389
+ return opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
18038
18390
  };
18039
18391
  const createPasswordSession = (reply) => {
18040
18392
  const key = getDefaultApiKey();
@@ -18091,12 +18443,12 @@ async function createServer(opts) {
18091
18443
  return reply.send({ authenticated: true });
18092
18444
  }
18093
18445
  if (apiKey) {
18094
- const key = opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, hashApiKey(apiKey))).get();
18446
+ const key = opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, hashApiKey(apiKey))).get();
18095
18447
  if (!key || key.revokedAt) {
18096
18448
  const err2 = authInvalid();
18097
18449
  return reply.status(err2.statusCode).send(err2.toJSON());
18098
18450
  }
18099
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(apiKeys.id, key.id)).run();
18451
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(apiKeys.id, key.id)).run();
18100
18452
  const sessionId = createSession(key.id);
18101
18453
  reply.header("set-cookie", serializeSessionCookie({
18102
18454
  name: SESSION_COOKIE_NAME,