@ainyc/canonry 2.10.1 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ import {
6
6
  ApiClient,
7
7
  AppError,
8
8
  CcReleaseSyncStatuses,
9
+ CitationStates,
9
10
  MemorySources,
10
11
  RunKinds,
11
12
  RunStatuses,
@@ -54,7 +55,7 @@ import {
54
55
  visibilityStateFromAnswerMentioned,
55
56
  windowCutoff,
56
57
  wordpressEnvSchema
57
- } from "./chunk-KWQCQMPY.js";
58
+ } from "./chunk-PLI7EOPM.js";
58
59
  import {
59
60
  IntelligenceService,
60
61
  agentMemory,
@@ -65,6 +66,10 @@ import {
65
66
  backlinkSummaries,
66
67
  bingCoverageSnapshots,
67
68
  bingUrlInspections,
69
+ buildContentGapRows,
70
+ buildContentSourceRows,
71
+ buildContentTargetRows,
72
+ buildInventory,
68
73
  ccReleaseSyncs,
69
74
  competitors,
70
75
  createLogger,
@@ -79,6 +84,7 @@ import {
79
84
  gscUrlInspections,
80
85
  healthSnapshots,
81
86
  insights,
87
+ isBlogShapedQuery,
82
88
  keywords,
83
89
  notifications,
84
90
  parseJsonColumn,
@@ -87,7 +93,7 @@ import {
87
93
  runs,
88
94
  schedules,
89
95
  usageCounters
90
- } from "./chunk-PYHANJ3B.js";
96
+ } from "./chunk-UM6RDSRJ.js";
91
97
 
92
98
  // src/telemetry.ts
93
99
  import crypto from "crypto";
@@ -167,7 +173,7 @@ import crypto28 from "crypto";
167
173
  import fs12 from "fs";
168
174
  import path14 from "path";
169
175
  import { fileURLToPath as fileURLToPath2 } from "url";
170
- import { eq as eq30 } from "drizzle-orm";
176
+ import { eq as eq32 } from "drizzle-orm";
171
177
  import Fastify from "fastify";
172
178
 
173
179
  // ../api-routes/src/auth.ts
@@ -2347,6 +2353,659 @@ async function intelligenceRoutes(app) {
2347
2353
  });
2348
2354
  }
2349
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
+
2678
+ // ../api-routes/src/content-data.ts
2679
+ import { and as and4, eq as eq13, desc as desc6, inArray as inArray3 } from "drizzle-orm";
2680
+ var RECENT_RUNS_WINDOW = 5;
2681
+ function loadOrchestratorInput(db, project) {
2682
+ const projectId = project.id;
2683
+ const ownDomain = normalizeDomain(project.canonicalDomain);
2684
+ const ownedDomains = parseJsonColumn(project.ownedDomains, []);
2685
+ const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain)]);
2686
+ const trackedKeywords = listKeywords(db, projectId);
2687
+ const candidateQueryStrings = trackedKeywords.filter(isBlogShapedQuery);
2688
+ const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain);
2689
+ const competitorSet = new Set(trackedCompetitors);
2690
+ const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW);
2691
+ const latestRunId = recentRunIds[0] ?? "";
2692
+ const latestRunTimestamp = latestRunId ? lookupRunTimestamp(db, latestRunId) : "";
2693
+ const candidateQueries = buildCandidateQueries({
2694
+ db,
2695
+ projectId,
2696
+ candidateQueryStrings,
2697
+ recentRunIds,
2698
+ latestRunId,
2699
+ ourDomains,
2700
+ competitorSet
2701
+ });
2702
+ const inventory = buildInventory({
2703
+ gscPages: listGscPagesForProject(db, projectId),
2704
+ ga4LandingPages: listGa4LandingPagesForProject(db, projectId),
2705
+ sitemapUrls: [],
2706
+ wpPosts: []
2707
+ });
2708
+ const gaTrafficByPage = buildGaTrafficByPage(db, projectId);
2709
+ const totalAiReferralSessions = sumAiReferralSessions(db, projectId);
2710
+ return {
2711
+ projectId,
2712
+ ownDomain,
2713
+ competitors: trackedCompetitors,
2714
+ candidateQueries,
2715
+ inventory,
2716
+ wpSchemaAudit: /* @__PURE__ */ new Map(),
2717
+ gaTrafficByPage,
2718
+ totalAiReferralSessions,
2719
+ latestRunId,
2720
+ latestRunTimestamp,
2721
+ inProgressActions: /* @__PURE__ */ new Map()
2722
+ };
2723
+ }
2724
+ function listKeywords(db, projectId) {
2725
+ const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq13(keywords.projectId, projectId)).all();
2726
+ return rows.map((r) => r.text);
2727
+ }
2728
+ function listCompetitorDomains(db, projectId) {
2729
+ const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all();
2730
+ return rows.map((r) => r.domain);
2731
+ }
2732
+ function listRecentAnswerVisibilityRunIds(db, projectId, limit) {
2733
+ const rows = db.select({ id: runs.id }).from(runs).where(
2734
+ and4(
2735
+ eq13(runs.projectId, projectId),
2736
+ eq13(runs.kind, RunKinds["answer-visibility"]),
2737
+ // Queued/running/failed/cancelled runs may have partial or no
2738
+ // snapshots; including them risks pointing latestRunId at a run with
2739
+ // no usable evidence.
2740
+ inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
2741
+ )
2742
+ ).orderBy(desc6(runs.createdAt)).limit(limit).all();
2743
+ return rows.map((r) => r.id);
2744
+ }
2745
+ function lookupRunTimestamp(db, runId) {
2746
+ const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq13(runs.id, runId)).get();
2747
+ return row?.createdAt ?? "";
2748
+ }
2749
+ function listGscPagesForProject(db, projectId) {
2750
+ const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
2751
+ return rows.map((r) => r.page);
2752
+ }
2753
+ function listGa4LandingPagesForProject(db, projectId) {
2754
+ const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
2755
+ return rows.map((r) => r.landingPage);
2756
+ }
2757
+ function buildGaTrafficByPage(db, projectId) {
2758
+ const rows = db.select({
2759
+ landingPage: gaTrafficSnapshots.landingPage,
2760
+ sessions: gaTrafficSnapshots.sessions
2761
+ }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
2762
+ const map = /* @__PURE__ */ new Map();
2763
+ for (const row of rows) {
2764
+ const path15 = extractPath(row.landingPage);
2765
+ if (!path15) continue;
2766
+ map.set(path15, (map.get(path15) ?? 0) + (row.sessions ?? 0));
2767
+ }
2768
+ return map;
2769
+ }
2770
+ function sumAiReferralSessions(db, projectId) {
2771
+ const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq13(gaAiReferrals.projectId, projectId)).all();
2772
+ return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
2773
+ }
2774
+ function buildCandidateQueries(opts) {
2775
+ if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
2776
+ return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
2777
+ }
2778
+ const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq13(keywords.projectId, opts.projectId)).all();
2779
+ const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
2780
+ const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
2781
+ const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
2782
+ const snapshotsByKeyword = /* @__PURE__ */ new Map();
2783
+ for (const row of snapshotRows) {
2784
+ const list = snapshotsByKeyword.get(row.keywordId) ?? [];
2785
+ list.push(row);
2786
+ snapshotsByKeyword.set(row.keywordId, list);
2787
+ }
2788
+ const gscRows = opts.db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, opts.projectId)).all();
2789
+ const gscByQuery = aggregateGscByQuery(gscRows);
2790
+ return opts.candidateQueryStrings.map((query) => {
2791
+ const keywordId = keywordIdByText.get(query);
2792
+ const snaps = keywordId ? snapshotsByKeyword.get(keywordId) ?? [] : [];
2793
+ const gsc = gscByQuery.get(query) ?? null;
2794
+ return aggregateCandidate({
2795
+ query,
2796
+ snapshots: snaps,
2797
+ gsc,
2798
+ ourDomains: opts.ourDomains,
2799
+ competitorSet: opts.competitorSet,
2800
+ latestRunId: opts.latestRunId
2801
+ });
2802
+ });
2803
+ }
2804
+ function aggregateGscByQuery(rows) {
2805
+ const byQuery = /* @__PURE__ */ new Map();
2806
+ for (const r of rows) {
2807
+ const existing = byQuery.get(r.query);
2808
+ const candidate = {
2809
+ // GSC stores `page` as a full URL for url-prefix properties; normalize to
2810
+ // a path so it can be joined against `gaTrafficByPage` (which is keyed by
2811
+ // path) and so `ourBestPage.url` / `targetRef` stay consistent regardless
2812
+ // of whether the page is sourced from GSC or from inventory.
2813
+ page: extractPath(r.page),
2814
+ position: Number(r.position) || 0,
2815
+ impressions: r.impressions,
2816
+ clicks: r.clicks,
2817
+ ctr: Number(r.ctr) || 0
2818
+ };
2819
+ if (!existing) {
2820
+ byQuery.set(r.query, candidate);
2821
+ continue;
2822
+ }
2823
+ if (candidate.impressions > existing.impressions) {
2824
+ byQuery.set(r.query, candidate);
2825
+ }
2826
+ }
2827
+ return byQuery;
2828
+ }
2829
+ function aggregateCandidate(opts) {
2830
+ const totalSnaps = opts.snapshots.length;
2831
+ if (totalSnaps === 0) {
2832
+ return {
2833
+ ...emptyCandidate(opts.query),
2834
+ gscPage: opts.gsc?.page ?? null,
2835
+ gscPosition: opts.gsc ? opts.gsc.position : null,
2836
+ gscImpressions: opts.gsc?.impressions ?? 0,
2837
+ gscClicks: opts.gsc?.clicks ?? 0,
2838
+ gscCtr: opts.gsc?.ctr ?? 0
2839
+ };
2840
+ }
2841
+ const citedCount = opts.snapshots.filter((s) => s.citationState === CitationStates.cited).length;
2842
+ const ourCitedRate = citedCount / totalSnaps;
2843
+ const recentMissRate = 1 - ourCitedRate;
2844
+ const competitorTally = /* @__PURE__ */ new Map();
2845
+ const competitorGroundingTally = /* @__PURE__ */ new Map();
2846
+ const ourGroundingTally = /* @__PURE__ */ new Map();
2847
+ let ourCitedInLatestRun = false;
2848
+ for (const snap of opts.snapshots) {
2849
+ const isLatestRun = snap.runId === opts.latestRunId;
2850
+ const competitorOverlap = parseJsonColumn(snap.competitorOverlap, []);
2851
+ for (const domain of competitorOverlap) {
2852
+ const normalized = normalizeDomain(domain);
2853
+ if (!opts.competitorSet.has(normalized)) continue;
2854
+ competitorTally.set(normalized, (competitorTally.get(normalized) ?? 0) + 1);
2855
+ }
2856
+ const grounding = extractGroundingSources(snap.rawResponse);
2857
+ for (const g of grounding) {
2858
+ const domain = normalizeDomain(extractHostFromUri(g.uri));
2859
+ if (!domain) continue;
2860
+ if (opts.ourDomains.has(domain)) {
2861
+ if (isLatestRun) ourCitedInLatestRun = true;
2862
+ recordGroundingHit(ourGroundingTally, g, domain, snap.provider);
2863
+ continue;
2864
+ }
2865
+ if (!opts.competitorSet.has(domain)) continue;
2866
+ recordGroundingHit(competitorGroundingTally, g, domain, snap.provider);
2867
+ }
2868
+ }
2869
+ return {
2870
+ query: opts.query,
2871
+ gscPage: opts.gsc?.page ?? null,
2872
+ gscPosition: opts.gsc ? opts.gsc.position : null,
2873
+ gscImpressions: opts.gsc?.impressions ?? 0,
2874
+ gscClicks: opts.gsc?.clicks ?? 0,
2875
+ gscCtr: opts.gsc?.ctr ?? 0,
2876
+ ourCitedRate,
2877
+ ourCitedInLatestRun,
2878
+ competitorDomains: Array.from(competitorTally.keys()),
2879
+ competitorCitationCount: Array.from(competitorTally.values()).reduce((a, b) => a + b, 0),
2880
+ recentMissRate,
2881
+ ourGroundingUrls: Array.from(ourGroundingTally.values()),
2882
+ competitorGroundingUrls: Array.from(competitorGroundingTally.values()),
2883
+ runsOfHistory: new Set(opts.snapshots.map((s) => s.runId)).size
2884
+ };
2885
+ }
2886
+ function recordGroundingHit(tally, g, domain, provider) {
2887
+ const existing = tally.get(g.uri);
2888
+ if (existing) {
2889
+ existing.citationCount += 1;
2890
+ if (provider && !existing.providers.includes(provider)) {
2891
+ existing.providers.push(provider);
2892
+ }
2893
+ return;
2894
+ }
2895
+ tally.set(g.uri, {
2896
+ uri: g.uri,
2897
+ title: g.title,
2898
+ domain,
2899
+ citationCount: 1,
2900
+ providers: provider ? [provider] : []
2901
+ });
2902
+ }
2903
+ function emptyCandidate(query) {
2904
+ return {
2905
+ query,
2906
+ gscPage: null,
2907
+ gscPosition: null,
2908
+ gscImpressions: 0,
2909
+ gscClicks: 0,
2910
+ gscCtr: 0,
2911
+ ourCitedRate: 0,
2912
+ ourCitedInLatestRun: false,
2913
+ competitorDomains: [],
2914
+ competitorCitationCount: 0,
2915
+ recentMissRate: 0,
2916
+ ourGroundingUrls: [],
2917
+ competitorGroundingUrls: [],
2918
+ runsOfHistory: 0
2919
+ };
2920
+ }
2921
+ function extractGroundingSources(rawResponse) {
2922
+ if (!rawResponse) return [];
2923
+ try {
2924
+ const parsed = JSON.parse(rawResponse);
2925
+ if (parsed && typeof parsed === "object" && "groundingSources" in parsed) {
2926
+ const grounding = parsed.groundingSources;
2927
+ if (Array.isArray(grounding)) {
2928
+ return grounding.filter(
2929
+ (g) => typeof g === "object" && g !== null && typeof g.uri === "string"
2930
+ ).map((g) => ({ uri: g.uri, title: g.title ?? "" }));
2931
+ }
2932
+ }
2933
+ } catch {
2934
+ }
2935
+ return [];
2936
+ }
2937
+ function extractHostFromUri(uri) {
2938
+ try {
2939
+ return new URL(uri).hostname;
2940
+ } catch {
2941
+ return "";
2942
+ }
2943
+ }
2944
+ function normalizeDomain(domain) {
2945
+ return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
2946
+ }
2947
+ function extractPath(url) {
2948
+ if (!url) return "";
2949
+ const match = /^https?:\/\/[^/]+(.*)$/.exec(url.trim());
2950
+ const path15 = match ? match[1] : url.trim();
2951
+ const stripped = path15.replace(/\/+$/, "");
2952
+ return stripped || "/";
2953
+ }
2954
+
2955
+ // ../api-routes/src/content.ts
2956
+ async function contentRoutes(app) {
2957
+ app.get("/projects/:name/content/targets", async (request) => {
2958
+ const project = resolveProject(app.db, request.params.name);
2959
+ const includeInProgress = request.query["include-in-progress"] === "true";
2960
+ const limit = parseLimitParam(request.query.limit);
2961
+ const input = loadOrchestratorInput(app.db, project);
2962
+ let rows = buildContentTargetRows(input);
2963
+ if (!includeInProgress) {
2964
+ rows = rows.filter((r) => r.existingAction === null);
2965
+ }
2966
+ if (limit !== void 0) {
2967
+ rows = rows.slice(0, limit);
2968
+ }
2969
+ const response = {
2970
+ targets: rows,
2971
+ contextMetrics: {
2972
+ totalAiReferralSessions: input.totalAiReferralSessions,
2973
+ latestRunId: input.latestRunId,
2974
+ runTimestamp: input.latestRunTimestamp
2975
+ }
2976
+ };
2977
+ return response;
2978
+ });
2979
+ app.get("/projects/:name/content/sources", async (request) => {
2980
+ const project = resolveProject(app.db, request.params.name);
2981
+ const input = loadOrchestratorInput(app.db, project);
2982
+ const rows = buildContentSourceRows(input);
2983
+ const response = {
2984
+ sources: rows,
2985
+ latestRunId: input.latestRunId
2986
+ };
2987
+ return response;
2988
+ });
2989
+ app.get("/projects/:name/content/gaps", async (request) => {
2990
+ const project = resolveProject(app.db, request.params.name);
2991
+ const input = loadOrchestratorInput(app.db, project);
2992
+ const rows = buildContentGapRows(input);
2993
+ const response = {
2994
+ gaps: rows,
2995
+ latestRunId: input.latestRunId
2996
+ };
2997
+ return response;
2998
+ });
2999
+ }
3000
+ function parseLimitParam(raw) {
3001
+ if (raw === void 0) return void 0;
3002
+ const parsed = Number(raw);
3003
+ if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
3004
+ throw validationError('"limit" must be a non-negative integer');
3005
+ }
3006
+ return parsed;
3007
+ }
3008
+
2350
3009
  // ../api-routes/src/openapi.ts
2351
3010
  var stringSchema = { type: "string" };
2352
3011
  var booleanSchema = { type: "boolean" };
@@ -4573,6 +5232,77 @@ var routeCatalog = [
4573
5232
  404: { description: "Project not found." }
4574
5233
  }
4575
5234
  },
5235
+ // Content opportunity engine
5236
+ {
5237
+ method: "get",
5238
+ path: "/api/v1/projects/{name}/content/targets",
5239
+ summary: "Ranked, action-typed content opportunities",
5240
+ description: "Returns the canonical opportunity list. Each row is `{query, action, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence, existingAction?}`. Hides rows with in-progress actions by default; pass `?include-in-progress=true` to include them annotated.",
5241
+ tags: ["content"],
5242
+ parameters: [
5243
+ nameParameter,
5244
+ { name: "limit", in: "query", description: "Max rows returned.", schema: stringSchema },
5245
+ { name: "include-in-progress", in: "query", description: "Include rows with in-flight tracked actions.", schema: stringSchema }
5246
+ ],
5247
+ responses: {
5248
+ 200: { description: "Targets returned." },
5249
+ 400: { description: "Invalid limit." },
5250
+ 404: { description: "Project not found." }
5251
+ }
5252
+ },
5253
+ {
5254
+ method: "get",
5255
+ path: "/api/v1/projects/{name}/content/sources",
5256
+ summary: "URL-level competitive grounding-source map per query",
5257
+ description: "Returns one row per blog-shaped query containing the grounding URLs the LLM cited. Distinguishes our domain (isOurDomain) from competitor URLs (isCompetitor). Pure DB read \u2014 canonry surfaces URLs but never fetches them.",
5258
+ tags: ["content"],
5259
+ parameters: [nameParameter],
5260
+ responses: {
5261
+ 200: { description: "Sources returned." },
5262
+ 404: { description: "Project not found." }
5263
+ }
5264
+ },
5265
+ {
5266
+ method: "get",
5267
+ path: "/api/v1/projects/{name}/content/gaps",
5268
+ summary: "Queries where competitors are cited but you are not",
5269
+ description: "Returns gap rows ranked by miss rate then by competitor count. Excludes queries with no competitor citations and queries where our cited rate is 100%.",
5270
+ tags: ["content"],
5271
+ parameters: [nameParameter],
5272
+ responses: {
5273
+ 200: { description: "Gaps returned." },
5274
+ 404: { description: "Project not found." }
5275
+ }
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
+ },
4576
5306
  {
4577
5307
  method: "get",
4578
5308
  path: "/api/v1/backlinks/status",
@@ -5031,7 +5761,7 @@ async function telemetryRoutes(app, opts) {
5031
5761
 
5032
5762
  // ../api-routes/src/schedules.ts
5033
5763
  import crypto11 from "crypto";
5034
- import { eq as eq12 } from "drizzle-orm";
5764
+ import { eq as eq14 } from "drizzle-orm";
5035
5765
  async function scheduleRoutes(app, opts) {
5036
5766
  app.put("/projects/:name/schedule", async (request, reply) => {
5037
5767
  const project = resolveProject(app.db, request.params.name);
@@ -5074,7 +5804,7 @@ async function scheduleRoutes(app, opts) {
5074
5804
  }
5075
5805
  const now = (/* @__PURE__ */ new Date()).toISOString();
5076
5806
  const enabledInt = enabled === false ? 0 : 1;
5077
- const existing = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
5807
+ const existing = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
5078
5808
  if (existing) {
5079
5809
  app.db.update(schedules).set({
5080
5810
  cronExpr,
@@ -5083,7 +5813,7 @@ async function scheduleRoutes(app, opts) {
5083
5813
  providers: JSON.stringify(providers),
5084
5814
  enabled: enabledInt,
5085
5815
  updatedAt: now
5086
- }).where(eq12(schedules.id, existing.id)).run();
5816
+ }).where(eq14(schedules.id, existing.id)).run();
5087
5817
  } else {
5088
5818
  app.db.insert(schedules).values({
5089
5819
  id: crypto11.randomUUID(),
@@ -5105,12 +5835,12 @@ async function scheduleRoutes(app, opts) {
5105
5835
  diff: { cronExpr, preset, timezone, providers }
5106
5836
  });
5107
5837
  opts.onScheduleUpdated?.("upsert", project.id);
5108
- const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
5838
+ const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
5109
5839
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
5110
5840
  });
5111
5841
  app.get("/projects/:name/schedule", async (request, reply) => {
5112
5842
  const project = resolveProject(app.db, request.params.name);
5113
- const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
5843
+ const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
5114
5844
  if (!schedule) {
5115
5845
  throw notFound("Schedule", request.params.name);
5116
5846
  }
@@ -5118,11 +5848,11 @@ async function scheduleRoutes(app, opts) {
5118
5848
  });
5119
5849
  app.delete("/projects/:name/schedule", async (request, reply) => {
5120
5850
  const project = resolveProject(app.db, request.params.name);
5121
- const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
5851
+ const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
5122
5852
  if (!schedule) {
5123
5853
  throw notFound("Schedule", request.params.name);
5124
5854
  }
5125
- app.db.delete(schedules).where(eq12(schedules.id, schedule.id)).run();
5855
+ app.db.delete(schedules).where(eq14(schedules.id, schedule.id)).run();
5126
5856
  writeAuditLog(app.db, {
5127
5857
  projectId: project.id,
5128
5858
  actor: "api",
@@ -5152,7 +5882,7 @@ function formatSchedule(row) {
5152
5882
 
5153
5883
  // ../api-routes/src/notifications.ts
5154
5884
  import crypto12 from "crypto";
5155
- import { eq as eq13 } from "drizzle-orm";
5885
+ import { eq as eq15 } from "drizzle-orm";
5156
5886
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
5157
5887
  async function notificationRoutes(app) {
5158
5888
  app.get("/notifications/events", async (_request, reply) => {
@@ -5191,22 +5921,22 @@ async function notificationRoutes(app) {
5191
5921
  diff: { channel, ...redactNotificationUrl(url), events }
5192
5922
  });
5193
5923
  return reply.status(201).send({
5194
- ...formatNotification(app.db.select().from(notifications).where(eq13(notifications.id, id)).get()),
5924
+ ...formatNotification(app.db.select().from(notifications).where(eq15(notifications.id, id)).get()),
5195
5925
  webhookSecret
5196
5926
  });
5197
5927
  });
5198
5928
  app.get("/projects/:name/notifications", async (request, reply) => {
5199
5929
  const project = resolveProject(app.db, request.params.name);
5200
- const rows = app.db.select().from(notifications).where(eq13(notifications.projectId, project.id)).all();
5930
+ const rows = app.db.select().from(notifications).where(eq15(notifications.projectId, project.id)).all();
5201
5931
  return reply.send(rows.map(formatNotification));
5202
5932
  });
5203
5933
  app.delete("/projects/:name/notifications/:id", async (request, reply) => {
5204
5934
  const project = resolveProject(app.db, request.params.name);
5205
- const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
5935
+ const notification = app.db.select().from(notifications).where(eq15(notifications.id, request.params.id)).get();
5206
5936
  if (!notification || notification.projectId !== project.id) {
5207
5937
  throw notFound("Notification", request.params.id);
5208
5938
  }
5209
- app.db.delete(notifications).where(eq13(notifications.id, notification.id)).run();
5939
+ app.db.delete(notifications).where(eq15(notifications.id, notification.id)).run();
5210
5940
  writeAuditLog(app.db, {
5211
5941
  projectId: project.id,
5212
5942
  actor: "api",
@@ -5218,7 +5948,7 @@ async function notificationRoutes(app) {
5218
5948
  });
5219
5949
  app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
5220
5950
  const project = resolveProject(app.db, request.params.name);
5221
- const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
5951
+ const notification = app.db.select().from(notifications).where(eq15(notifications.id, request.params.id)).get();
5222
5952
  if (!notification || notification.projectId !== project.id) {
5223
5953
  throw notFound("Notification", request.params.id);
5224
5954
  }
@@ -5271,7 +6001,7 @@ function formatNotification(row) {
5271
6001
 
5272
6002
  // ../api-routes/src/google.ts
5273
6003
  import crypto14 from "crypto";
5274
- import { eq as eq14, and as and3, desc as desc5, sql as sql3 } from "drizzle-orm";
6004
+ import { eq as eq16, and as and5, desc as desc7, sql as sql4 } from "drizzle-orm";
5275
6005
 
5276
6006
  // ../integration-google/src/constants.ts
5277
6007
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -6375,20 +7105,20 @@ async function googleRoutes(app, opts) {
6375
7105
  if (opts.onGscSyncRequested) {
6376
7106
  opts.onGscSyncRequested(runId, project.id, { days, full });
6377
7107
  }
6378
- const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7108
+ const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
6379
7109
  return run;
6380
7110
  });
6381
7111
  app.get("/projects/:name/google/gsc/performance", async (request) => {
6382
7112
  const project = resolveProject(app.db, request.params.name);
6383
7113
  const { startDate, endDate, query, page, limit } = request.query;
6384
7114
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
6385
- const conditions = [eq14(gscSearchData.projectId, project.id)];
6386
- if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
6387
- else if (cutoffDate) conditions.push(sql3`${gscSearchData.date} >= ${cutoffDate}`);
6388
- if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
6389
- if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
6390
- if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
6391
- const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc5(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();
6392
7122
  return rows.map((r) => ({
6393
7123
  date: r.date,
6394
7124
  query: r.query,
@@ -6460,9 +7190,9 @@ async function googleRoutes(app, opts) {
6460
7190
  app.get("/projects/:name/google/gsc/inspections", async (request) => {
6461
7191
  const project = resolveProject(app.db, request.params.name);
6462
7192
  const { url, limit } = request.query;
6463
- const conditions = [eq14(gscUrlInspections.projectId, project.id)];
6464
- if (url) conditions.push(eq14(gscUrlInspections.url, url));
6465
- const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc5(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();
6466
7196
  return rows.map((r) => ({
6467
7197
  id: r.id,
6468
7198
  url: r.url,
@@ -6481,7 +7211,7 @@ async function googleRoutes(app, opts) {
6481
7211
  });
6482
7212
  app.get("/projects/:name/google/gsc/deindexed", async (request) => {
6483
7213
  const project = resolveProject(app.db, request.params.name);
6484
- const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
7214
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
6485
7215
  const byUrl = /* @__PURE__ */ new Map();
6486
7216
  for (const row of allInspections) {
6487
7217
  const existing = byUrl.get(row.url);
@@ -6509,7 +7239,7 @@ async function googleRoutes(app, opts) {
6509
7239
  });
6510
7240
  app.get("/projects/:name/google/gsc/coverage", async (request) => {
6511
7241
  const project = resolveProject(app.db, request.params.name);
6512
- const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
7242
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
6513
7243
  const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
6514
7244
  const latestByUrl = /* @__PURE__ */ new Map();
6515
7245
  const historyByUrl = /* @__PURE__ */ new Map();
@@ -6606,7 +7336,7 @@ async function googleRoutes(app, opts) {
6606
7336
  const project = resolveProject(app.db, request.params.name);
6607
7337
  const parsed = parseInt(request.query.limit ?? "90", 10);
6608
7338
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
6609
- const rows = app.db.select().from(gscCoverageSnapshots).where(eq14(gscCoverageSnapshots.projectId, project.id)).orderBy(desc5(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();
6610
7340
  return rows.map((r) => ({
6611
7341
  date: r.date,
6612
7342
  indexed: r.indexed,
@@ -6666,7 +7396,7 @@ async function googleRoutes(app, opts) {
6666
7396
  if (opts.onInspectSitemapRequested) {
6667
7397
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
6668
7398
  }
6669
- const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7399
+ const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
6670
7400
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
6671
7401
  });
6672
7402
  app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
@@ -6693,7 +7423,7 @@ async function googleRoutes(app, opts) {
6693
7423
  if (opts.onInspectSitemapRequested) {
6694
7424
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
6695
7425
  }
6696
- const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7426
+ const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
6697
7427
  return run;
6698
7428
  });
6699
7429
  app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
@@ -6740,7 +7470,7 @@ async function googleRoutes(app, opts) {
6740
7470
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
6741
7471
  let urlsToNotify = request.body?.urls ?? [];
6742
7472
  if (request.body?.allUnindexed) {
6743
- const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
7473
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
6744
7474
  const latestByUrl = /* @__PURE__ */ new Map();
6745
7475
  for (const row of allInspections) {
6746
7476
  if (!latestByUrl.has(row.url)) {
@@ -6811,7 +7541,7 @@ async function googleRoutes(app, opts) {
6811
7541
 
6812
7542
  // ../api-routes/src/bing.ts
6813
7543
  import crypto15 from "crypto";
6814
- import { eq as eq15, and as and4, desc as desc6 } from "drizzle-orm";
7544
+ import { eq as eq17, and as and6, desc as desc8 } from "drizzle-orm";
6815
7545
 
6816
7546
  // ../integration-bing/src/constants.ts
6817
7547
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -7124,7 +7854,7 @@ async function bingRoutes(app, opts) {
7124
7854
  const store = requireConnectionStore();
7125
7855
  const project = resolveProject(app.db, request.params.name);
7126
7856
  requireConnection(store, project.canonicalDomain);
7127
- const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
7857
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq17(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
7128
7858
  const latestByUrl = /* @__PURE__ */ new Map();
7129
7859
  const definitiveByUrl = /* @__PURE__ */ new Map();
7130
7860
  for (const row of allInspections) {
@@ -7213,7 +7943,7 @@ async function bingRoutes(app, opts) {
7213
7943
  const project = resolveProject(app.db, request.params.name);
7214
7944
  const parsed = parseInt(request.query.limit ?? "90", 10);
7215
7945
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
7216
- const rows = app.db.select().from(bingCoverageSnapshots).where(eq15(bingCoverageSnapshots.projectId, project.id)).orderBy(desc6(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();
7217
7947
  return rows.map((r) => ({
7218
7948
  date: r.date,
7219
7949
  indexed: r.indexed,
@@ -7225,8 +7955,8 @@ async function bingRoutes(app, opts) {
7225
7955
  requireConnectionStore();
7226
7956
  const project = resolveProject(app.db, request.params.name);
7227
7957
  const { url, limit } = request.query;
7228
- const whereClause = url ? and4(eq15(bingUrlInspections.projectId, project.id), eq15(bingUrlInspections.url, url)) : eq15(bingUrlInspections.projectId, project.id);
7229
- const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc6(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();
7230
7960
  return filtered.map((r) => ({
7231
7961
  id: r.id,
7232
7962
  url: r.url,
@@ -7315,7 +8045,7 @@ async function bingRoutes(app, opts) {
7315
8045
  anchorCount: result.AnchorCount ?? null,
7316
8046
  discoveryDate
7317
8047
  }).run();
7318
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq15(runs.id, runId)).run();
8048
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq17(runs.id, runId)).run();
7319
8049
  return {
7320
8050
  id,
7321
8051
  url,
@@ -7331,7 +8061,7 @@ async function bingRoutes(app, opts) {
7331
8061
  } catch (e) {
7332
8062
  const msg = e instanceof Error ? e.message : String(e);
7333
8063
  bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
7334
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq15(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();
7335
8065
  throw e;
7336
8066
  }
7337
8067
  });
@@ -7358,7 +8088,7 @@ async function bingRoutes(app, opts) {
7358
8088
  } else {
7359
8089
  bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
7360
8090
  }
7361
- const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
8091
+ const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
7362
8092
  return run;
7363
8093
  });
7364
8094
  app.post("/projects/:name/bing/request-indexing", async (request) => {
@@ -7370,7 +8100,7 @@ async function bingRoutes(app, opts) {
7370
8100
  }
7371
8101
  let urlsToSubmit = request.body?.urls ?? [];
7372
8102
  if (request.body?.allUnindexed) {
7373
- const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
8103
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq17(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
7374
8104
  const latestByUrl = /* @__PURE__ */ new Map();
7375
8105
  for (const row of allInspections) {
7376
8106
  if (!latestByUrl.has(row.url)) {
@@ -7457,14 +8187,14 @@ async function bingRoutes(app, opts) {
7457
8187
  import fs from "fs";
7458
8188
  import path from "path";
7459
8189
  import os from "os";
7460
- import { eq as eq16, and as and5 } from "drizzle-orm";
8190
+ import { eq as eq18, and as and7 } from "drizzle-orm";
7461
8191
  function getScreenshotDir() {
7462
8192
  return path.join(os.homedir(), ".canonry", "screenshots");
7463
8193
  }
7464
8194
  async function cdpRoutes(app, opts) {
7465
8195
  app.get("/screenshots/:snapshotId", async (request, reply) => {
7466
8196
  const { snapshotId } = request.params;
7467
- const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq16(querySnapshots.id, snapshotId)).get();
8197
+ const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq18(querySnapshots.id, snapshotId)).get();
7468
8198
  if (!snapshot?.screenshotPath) {
7469
8199
  const err = notFound("Screenshot", snapshotId);
7470
8200
  return reply.code(err.statusCode).send(err.toJSON());
@@ -7530,7 +8260,7 @@ async function cdpRoutes(app, opts) {
7530
8260
  async (request, reply) => {
7531
8261
  const project = resolveProject(app.db, request.params.name);
7532
8262
  const { runId } = request.params;
7533
- const run = app.db.select().from(runs).where(and5(eq16(runs.id, runId), eq16(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();
7534
8264
  if (!run) {
7535
8265
  const err = notFound("Run", runId);
7536
8266
  return reply.code(err.statusCode).send(err.toJSON());
@@ -7543,8 +8273,8 @@ async function cdpRoutes(app, opts) {
7543
8273
  citedDomains: querySnapshots.citedDomains,
7544
8274
  screenshotPath: querySnapshots.screenshotPath,
7545
8275
  rawResponse: querySnapshots.rawResponse
7546
- }).from(querySnapshots).where(eq16(querySnapshots.runId, runId)).all();
7547
- const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq16(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();
7548
8278
  const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
7549
8279
  const byKeyword = /* @__PURE__ */ new Map();
7550
8280
  for (const snap of snapshots) {
@@ -7627,7 +8357,7 @@ async function cdpRoutes(app, opts) {
7627
8357
 
7628
8358
  // ../api-routes/src/ga.ts
7629
8359
  import crypto16 from "crypto";
7630
- import { eq as eq17, desc as desc7, and as and6, sql as sql4 } from "drizzle-orm";
8360
+ import { eq as eq19, desc as desc9, and as and8, sql as sql5 } from "drizzle-orm";
7631
8361
  function gaLog(level, action, ctx) {
7632
8362
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
7633
8363
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -7784,10 +8514,10 @@ async function ga4Routes(app, opts) {
7784
8514
  if (!saConn && !oauthConn) {
7785
8515
  throw notFound("GA4 connection", project.name);
7786
8516
  }
7787
- app.db.delete(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).run();
7788
- app.db.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
7789
- app.db.delete(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).run();
7790
- app.db.delete(gaSocialReferrals).where(eq17(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();
7791
8521
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
7792
8522
  opts.ga4CredentialStore?.deleteConnection(project.name);
7793
8523
  opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
@@ -7808,7 +8538,7 @@ async function ga4Routes(app, opts) {
7808
8538
  if (!connected) {
7809
8539
  return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
7810
8540
  }
7811
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(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();
7812
8542
  return {
7813
8543
  connected: true,
7814
8544
  propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
@@ -7867,10 +8597,10 @@ async function ga4Routes(app, opts) {
7867
8597
  app.db.transaction((tx) => {
7868
8598
  if (syncTraffic) {
7869
8599
  tx.delete(gaTrafficSnapshots).where(
7870
- and6(
7871
- eq17(gaTrafficSnapshots.projectId, project.id),
7872
- sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
7873
- 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}`
7874
8604
  )
7875
8605
  ).run();
7876
8606
  for (const row of rows) {
@@ -7889,10 +8619,10 @@ async function ga4Routes(app, opts) {
7889
8619
  }
7890
8620
  if (syncAi) {
7891
8621
  tx.delete(gaAiReferrals).where(
7892
- and6(
7893
- eq17(gaAiReferrals.projectId, project.id),
7894
- sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
7895
- 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}`
7896
8626
  )
7897
8627
  ).run();
7898
8628
  for (const row of aiReferrals) {
@@ -7912,10 +8642,10 @@ async function ga4Routes(app, opts) {
7912
8642
  }
7913
8643
  if (syncSocial) {
7914
8644
  tx.delete(gaSocialReferrals).where(
7915
- and6(
7916
- eq17(gaSocialReferrals.projectId, project.id),
7917
- sql4`${gaSocialReferrals.date} >= ${summary.periodStart}`,
7918
- 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}`
7919
8649
  )
7920
8650
  ).run();
7921
8651
  for (const row of socialReferrals) {
@@ -7934,7 +8664,7 @@ async function ga4Routes(app, opts) {
7934
8664
  }
7935
8665
  }
7936
8666
  if (syncSummary) {
7937
- tx.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
8667
+ tx.delete(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).run();
7938
8668
  tx.insert(gaTrafficSummaries).values({
7939
8669
  id: crypto16.randomUUID(),
7940
8670
  projectId: project.id,
@@ -7948,7 +8678,7 @@ async function ga4Routes(app, opts) {
7948
8678
  }).run();
7949
8679
  }
7950
8680
  });
7951
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq17(runs.id, runId)).run();
8681
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq19(runs.id, runId)).run();
7952
8682
  const syncedComponents = only ? [only, ...only !== "social" && only !== "ai" && only !== "traffic" ? [] : []] : void 0;
7953
8683
  gaLog("info", "sync.complete", {
7954
8684
  projectId: project.id,
@@ -7972,7 +8702,7 @@ async function ga4Routes(app, opts) {
7972
8702
  } catch (e) {
7973
8703
  const msg = e instanceof Error ? e.message : String(e);
7974
8704
  gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
7975
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq17(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();
7976
8706
  throw e;
7977
8707
  }
7978
8708
  });
@@ -7983,48 +8713,48 @@ async function ga4Routes(app, opts) {
7983
8713
  const window = parseWindow(request.query.window);
7984
8714
  const cutoff = windowCutoff(window);
7985
8715
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
7986
- const snapshotConditions = [eq17(gaTrafficSnapshots.projectId, project.id)];
7987
- if (cutoffDate) snapshotConditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
7988
- const aiConditions = [eq17(gaAiReferrals.projectId, project.id)];
7989
- if (cutoffDate) aiConditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
7990
- const socialConditions = [eq17(gaSocialReferrals.projectId, project.id)];
7991
- 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}`);
7992
8722
  const summaryRow = cutoffDate ? app.db.select({
7993
- totalSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
7994
- totalOrganicSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
7995
- totalUsers: sql4`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
7996
- }).from(gaTrafficSnapshots).where(and6(...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({
7997
8727
  totalSessions: gaTrafficSummaries.totalSessions,
7998
8728
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
7999
8729
  totalUsers: gaTrafficSummaries.totalUsers
8000
- }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).get();
8730
+ }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
8001
8731
  const summaryMeta = app.db.select({
8002
8732
  periodStart: gaTrafficSummaries.periodStart,
8003
8733
  periodEnd: gaTrafficSummaries.periodEnd
8004
- }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).get();
8734
+ }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
8005
8735
  const rows = app.db.select({
8006
8736
  landingPage: gaTrafficSnapshots.landingPage,
8007
- sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8008
- organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8009
- users: sql4`SUM(${gaTrafficSnapshots.users})`
8010
- }).from(gaTrafficSnapshots).where(and6(...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();
8011
8741
  const aiReferrals = app.db.select({
8012
8742
  source: gaAiReferrals.source,
8013
8743
  medium: gaAiReferrals.medium,
8014
8744
  sourceDimension: gaAiReferrals.sourceDimension,
8015
- sessions: sql4`SUM(${gaAiReferrals.sessions})`,
8016
- users: sql4`SUM(${gaAiReferrals.users})`
8017
- }).from(gaAiReferrals).where(and6(...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();
8018
8748
  const aiDeduped = app.db.select({
8019
- sessions: sql4`SUM(max_sessions)`,
8020
- users: sql4`SUM(max_users)`
8749
+ sessions: sql5`SUM(max_sessions)`,
8750
+ users: sql5`SUM(max_users)`
8021
8751
  }).from(
8022
- sql4`(
8752
+ sql5`(
8023
8753
  SELECT date, source, medium,
8024
8754
  MAX(sessions) AS max_sessions,
8025
8755
  MAX(users) AS max_users
8026
8756
  FROM ga_ai_referrals
8027
- WHERE project_id = ${project.id}${cutoffDate ? sql4` AND date >= ${cutoffDate}` : sql4``}
8757
+ WHERE project_id = ${project.id}${cutoffDate ? sql5` AND date >= ${cutoffDate}` : sql5``}
8028
8758
  GROUP BY date, source, medium
8029
8759
  )`
8030
8760
  ).get();
@@ -8032,14 +8762,14 @@ async function ga4Routes(app, opts) {
8032
8762
  source: gaSocialReferrals.source,
8033
8763
  medium: gaSocialReferrals.medium,
8034
8764
  channelGroup: gaSocialReferrals.channelGroup,
8035
- sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
8036
- users: sql4`SUM(${gaSocialReferrals.users})`
8037
- }).from(gaSocialReferrals).where(and6(...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();
8038
8768
  const socialTotals = app.db.select({
8039
- sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
8040
- users: sql4`SUM(${gaSocialReferrals.users})`
8041
- }).from(gaSocialReferrals).where(and6(...socialConditions)).get();
8042
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(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();
8043
8773
  const total = summaryRow?.totalSessions ?? 0;
8044
8774
  return {
8045
8775
  totalSessions: total,
@@ -8086,8 +8816,8 @@ async function ga4Routes(app, opts) {
8086
8816
  const project = resolveProject(app.db, request.params.name);
8087
8817
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8088
8818
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8089
- const conditions = [eq17(gaAiReferrals.projectId, project.id)];
8090
- 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}`);
8091
8821
  const rows = app.db.select({
8092
8822
  date: gaAiReferrals.date,
8093
8823
  source: gaAiReferrals.source,
@@ -8095,15 +8825,15 @@ async function ga4Routes(app, opts) {
8095
8825
  sourceDimension: gaAiReferrals.sourceDimension,
8096
8826
  sessions: gaAiReferrals.sessions,
8097
8827
  users: gaAiReferrals.users
8098
- }).from(gaAiReferrals).where(and6(...conditions)).orderBy(gaAiReferrals.date).all();
8828
+ }).from(gaAiReferrals).where(and8(...conditions)).orderBy(gaAiReferrals.date).all();
8099
8829
  return rows;
8100
8830
  });
8101
8831
  app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
8102
8832
  const project = resolveProject(app.db, request.params.name);
8103
8833
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8104
8834
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8105
- const conditions = [eq17(gaSocialReferrals.projectId, project.id)];
8106
- 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}`);
8107
8837
  const rows = app.db.select({
8108
8838
  date: gaSocialReferrals.date,
8109
8839
  source: gaSocialReferrals.source,
@@ -8111,7 +8841,7 @@ async function ga4Routes(app, opts) {
8111
8841
  channelGroup: gaSocialReferrals.channelGroup,
8112
8842
  sessions: gaSocialReferrals.sessions,
8113
8843
  users: gaSocialReferrals.users
8114
- }).from(gaSocialReferrals).where(and6(...conditions)).orderBy(gaSocialReferrals.date).all();
8844
+ }).from(gaSocialReferrals).where(and8(...conditions)).orderBy(gaSocialReferrals.date).all();
8115
8845
  return rows;
8116
8846
  });
8117
8847
  app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
@@ -8124,10 +8854,10 @@ async function ga4Routes(app, opts) {
8124
8854
  d.setDate(d.getDate() - n);
8125
8855
  return fmt(d);
8126
8856
  };
8127
- const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and6(
8128
- eq17(gaSocialReferrals.projectId, project.id),
8129
- sql4`${gaSocialReferrals.date} >= ${from}`,
8130
- 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}`
8131
8861
  )).get();
8132
8862
  const current7d = sumSocial(daysAgo2(7), fmt(today));
8133
8863
  const prev7d = sumSocial(daysAgo2(14), daysAgo2(7));
@@ -8136,19 +8866,19 @@ async function ga4Routes(app, opts) {
8136
8866
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
8137
8867
  const sourceCurrent = app.db.select({
8138
8868
  source: gaSocialReferrals.source,
8139
- sessions: sql4`SUM(${gaSocialReferrals.sessions})`
8140
- }).from(gaSocialReferrals).where(and6(
8141
- eq17(gaSocialReferrals.projectId, project.id),
8142
- sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
8143
- 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)}`
8144
8874
  )).groupBy(gaSocialReferrals.source).all();
8145
8875
  const sourcePrev = app.db.select({
8146
8876
  source: gaSocialReferrals.source,
8147
- sessions: sql4`SUM(${gaSocialReferrals.sessions})`
8148
- }).from(gaSocialReferrals).where(and6(
8149
- eq17(gaSocialReferrals.projectId, project.id),
8150
- sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
8151
- 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)}`
8152
8882
  )).groupBy(gaSocialReferrals.source).all();
8153
8883
  const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
8154
8884
  let biggestMover = null;
@@ -8187,15 +8917,15 @@ async function ga4Routes(app, opts) {
8187
8917
  return fmt(d);
8188
8918
  };
8189
8919
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
8190
- const sumTotal = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and6(eq17(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
8191
- const sumOrganic = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and6(eq17(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
8192
- 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`(
8193
8923
  SELECT date, source, medium, MAX(sessions) AS max_sessions
8194
8924
  FROM ga_ai_referrals
8195
8925
  WHERE project_id = ${project.id} AND date >= ${from} AND date < ${to}
8196
8926
  GROUP BY date, source, medium
8197
8927
  )`).get();
8198
- const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and6(eq17(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();
8199
8929
  const todayStr = fmt(today);
8200
8930
  const buildTrend = (sum) => {
8201
8931
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -8204,18 +8934,18 @@ async function ga4Routes(app, opts) {
8204
8934
  const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
8205
8935
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
8206
8936
  };
8207
- 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`(
8208
8938
  SELECT date, source, medium, MAX(sessions) AS max_sessions
8209
8939
  FROM ga_ai_referrals
8210
8940
  WHERE project_id = ${project.id} AND date >= ${daysAgo2(7)} AND date < ${todayStr}
8211
8941
  GROUP BY date, source, medium
8212
- )`).groupBy(sql4`source`).all();
8213
- 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`(
8214
8944
  SELECT date, source, medium, MAX(sessions) AS max_sessions
8215
8945
  FROM ga_ai_referrals
8216
8946
  WHERE project_id = ${project.id} AND date >= ${daysAgo2(14)} AND date < ${daysAgo2(7)}
8217
8947
  GROUP BY date, source, medium
8218
- )`).groupBy(sql4`source`).all();
8948
+ )`).groupBy(sql5`source`).all();
8219
8949
  const findBiggestMover = (current, prev) => {
8220
8950
  const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
8221
8951
  let mover = null;
@@ -8230,8 +8960,8 @@ async function ga4Routes(app, opts) {
8230
8960
  }
8231
8961
  return mover;
8232
8962
  };
8233
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql4`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
8234
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and6(eq17(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();
8235
8965
  return {
8236
8966
  total: buildTrend(sumTotal),
8237
8967
  organic: buildTrend(sumOrganic),
@@ -8245,14 +8975,14 @@ async function ga4Routes(app, opts) {
8245
8975
  const project = resolveProject(app.db, request.params.name);
8246
8976
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8247
8977
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8248
- const conditions = [eq17(gaTrafficSnapshots.projectId, project.id)];
8249
- 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}`);
8250
8980
  const rows = app.db.select({
8251
8981
  date: gaTrafficSnapshots.date,
8252
- sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8253
- organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8254
- users: sql4`SUM(${gaTrafficSnapshots.users})`
8255
- }).from(gaTrafficSnapshots).where(and6(...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();
8256
8986
  return rows.map((r) => ({
8257
8987
  date: r.date,
8258
8988
  sessions: r.sessions ?? 0,
@@ -8265,10 +8995,10 @@ async function ga4Routes(app, opts) {
8265
8995
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8266
8996
  const trafficPages = app.db.select({
8267
8997
  landingPage: gaTrafficSnapshots.landingPage,
8268
- sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8269
- organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8270
- users: sql4`SUM(${gaTrafficSnapshots.users})`
8271
- }).from(gaTrafficSnapshots).where(eq17(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();
8272
9002
  return {
8273
9003
  pages: trafficPages.map((r) => ({
8274
9004
  landingPage: r.landingPage,
@@ -9905,7 +10635,7 @@ async function wordpressRoutes(app, opts) {
9905
10635
 
9906
10636
  // ../api-routes/src/backlinks.ts
9907
10637
  import crypto18 from "crypto";
9908
- import { and as and7, asc as asc2, desc as desc8, eq as eq18, 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";
9909
10639
 
9910
10640
  // ../integration-commoncrawl/src/constants.ts
9911
10641
  import os2 from "os";
@@ -10135,7 +10865,7 @@ async function queryBacklinks(opts) {
10135
10865
  const reversed = opts.targets.map(reverseDomain);
10136
10866
  const targetList = reversed.map(quote).join(", ");
10137
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)}` : "";
10138
- const sql10 = `
10868
+ const sql11 = `
10139
10869
  WITH vertices AS (
10140
10870
  SELECT * FROM read_csv(
10141
10871
  ${quote(opts.vertexPath)},
@@ -10171,7 +10901,7 @@ async function queryBacklinks(opts) {
10171
10901
  const conn = await instance.connect();
10172
10902
  let rows;
10173
10903
  try {
10174
- const reader = await conn.runAndReadAll(sql10);
10904
+ const reader = await conn.runAndReadAll(sql11);
10175
10905
  rows = reader.getRowObjects();
10176
10906
  } finally {
10177
10907
  conn.disconnectSync?.();
@@ -10303,8 +11033,8 @@ function mapRunRow(row) {
10303
11033
  };
10304
11034
  }
10305
11035
  function latestSummaryForProject(db, projectId, release) {
10306
- const condition = release ? and7(eq18(backlinkSummaries.projectId, projectId), eq18(backlinkSummaries.release, release)) : eq18(backlinkSummaries.projectId, projectId);
10307
- return db.select().from(backlinkSummaries).where(condition).orderBy(desc8(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();
10308
11038
  }
10309
11039
  async function backlinksRoutes(app, opts) {
10310
11040
  app.get("/backlinks/status", async (_request, reply) => {
@@ -10333,7 +11063,7 @@ async function backlinksRoutes(app, opts) {
10333
11063
  "@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
10334
11064
  );
10335
11065
  }
10336
- const existing = app.db.select().from(ccReleaseSyncs).where(eq18(ccReleaseSyncs.release, release)).get();
11066
+ const existing = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.release, release)).get();
10337
11067
  const now = (/* @__PURE__ */ new Date()).toISOString();
10338
11068
  if (existing) {
10339
11069
  if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
@@ -10344,9 +11074,9 @@ async function backlinksRoutes(app, opts) {
10344
11074
  phaseDetail: null,
10345
11075
  error: null,
10346
11076
  updatedAt: now
10347
- }).where(eq18(ccReleaseSyncs.id, existing.id)).run();
11077
+ }).where(eq20(ccReleaseSyncs.id, existing.id)).run();
10348
11078
  opts.onReleaseSyncRequested(existing.id, release);
10349
- const refreshed = app.db.select().from(ccReleaseSyncs).where(eq18(ccReleaseSyncs.id, existing.id)).get();
11079
+ const refreshed = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.id, existing.id)).get();
10350
11080
  return reply.status(200).send(mapSyncRow(refreshed));
10351
11081
  }
10352
11082
  const id = crypto18.randomUUID();
@@ -10358,15 +11088,15 @@ async function backlinksRoutes(app, opts) {
10358
11088
  updatedAt: now
10359
11089
  }).run();
10360
11090
  opts.onReleaseSyncRequested(id, release);
10361
- const inserted = app.db.select().from(ccReleaseSyncs).where(eq18(ccReleaseSyncs.id, id)).get();
11091
+ const inserted = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.id, id)).get();
10362
11092
  return reply.status(201).send(mapSyncRow(inserted));
10363
11093
  });
10364
11094
  app.get("/backlinks/syncs/latest", async (_request, reply) => {
10365
- const row = app.db.select().from(ccReleaseSyncs).orderBy(desc8(ccReleaseSyncs.updatedAt)).limit(1).get();
11095
+ const row = app.db.select().from(ccReleaseSyncs).orderBy(desc10(ccReleaseSyncs.updatedAt)).limit(1).get();
10366
11096
  return reply.send(row ? mapSyncRow(row) : null);
10367
11097
  });
10368
11098
  app.get("/backlinks/syncs", async (_request, reply) => {
10369
- const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc8(ccReleaseSyncs.updatedAt)).all();
11099
+ const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc10(ccReleaseSyncs.updatedAt)).all();
10370
11100
  return reply.send(rows.map(mapSyncRow));
10371
11101
  });
10372
11102
  app.get("/backlinks/releases", async (_request, reply) => {
@@ -10409,7 +11139,7 @@ async function backlinksRoutes(app, opts) {
10409
11139
  createdAt: now
10410
11140
  }).run();
10411
11141
  opts.onBacklinkExtractRequested(runId, project.id, release);
10412
- const run = app.db.select().from(runs).where(eq18(runs.id, runId)).get();
11142
+ const run = app.db.select().from(runs).where(eq20(runs.id, runId)).get();
10413
11143
  return reply.status(201).send(mapRunRow(run));
10414
11144
  });
10415
11145
  app.get(
@@ -10430,15 +11160,15 @@ async function backlinksRoutes(app, opts) {
10430
11160
  }
10431
11161
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
10432
11162
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
10433
- const domainCondition = and7(
10434
- eq18(backlinkDomains.projectId, project.id),
10435
- eq18(backlinkDomains.release, targetRelease)
11163
+ const domainCondition = and9(
11164
+ eq20(backlinkDomains.projectId, project.id),
11165
+ eq20(backlinkDomains.release, targetRelease)
10436
11166
  );
10437
- 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();
10438
11168
  const rows = app.db.select({
10439
11169
  linkingDomain: backlinkDomains.linkingDomain,
10440
11170
  numHosts: backlinkDomains.numHosts
10441
- }).from(backlinkDomains).where(domainCondition).orderBy(desc8(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
11171
+ }).from(backlinkDomains).where(domainCondition).orderBy(desc10(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
10442
11172
  const response = {
10443
11173
  summary: summaryRow ? mapSummaryRow(summaryRow) : null,
10444
11174
  total: Number(totalRow?.count ?? 0),
@@ -10450,7 +11180,7 @@ async function backlinksRoutes(app, opts) {
10450
11180
  "/projects/:name/backlinks/history",
10451
11181
  async (request, reply) => {
10452
11182
  const project = resolveProject(app.db, request.params.name);
10453
- const rows = app.db.select().from(backlinkSummaries).where(eq18(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();
10454
11184
  const response = rows.map((r) => ({
10455
11185
  release: r.release,
10456
11186
  totalLinkingDomains: r.totalLinkingDomains,
@@ -10523,6 +11253,8 @@ async function apiRoutes(app, opts) {
10523
11253
  await api.register(historyRoutes);
10524
11254
  await api.register(analyticsRoutes);
10525
11255
  await api.register(intelligenceRoutes);
11256
+ await api.register(compositeRoutes);
11257
+ await api.register(contentRoutes);
10526
11258
  await api.register(settingsRoutes, {
10527
11259
  providerSummary: opts.providerSummary,
10528
11260
  providerAdapters: opts.providerAdapters,
@@ -12668,7 +13400,7 @@ function hasParsedResponseContent4(rawResponse) {
12668
13400
  return Array.isArray(nestedResponse.choices) && nestedResponse.choices.length > 0 || Array.isArray(nestedResponse.search_results) && nestedResponse.search_results.length > 0 || Array.isArray(nestedResponse.citations) && nestedResponse.citations.length > 0;
12669
13401
  }
12670
13402
  function reparseStoredResult4(rawResponse) {
12671
- const groundingSources = extractGroundingSources(rawResponse);
13403
+ const groundingSources = extractGroundingSources2(rawResponse);
12672
13404
  return {
12673
13405
  provider: "perplexity",
12674
13406
  answerText: extractAnswerText3(rawResponse),
@@ -12699,7 +13431,7 @@ function extractCitations(rawResponse) {
12699
13431
  }
12700
13432
  return [];
12701
13433
  }
12702
- function extractGroundingSources(rawResponse) {
13434
+ function extractGroundingSources2(rawResponse) {
12703
13435
  const searchResults = extractSearchResults(rawResponse);
12704
13436
  if (searchResults.length > 0) {
12705
13437
  const seen = /* @__PURE__ */ new Set();
@@ -13044,7 +13776,7 @@ import crypto19 from "crypto";
13044
13776
  import fs7 from "fs";
13045
13777
  import path9 from "path";
13046
13778
  import os4 from "os";
13047
- import { and as and8, eq as eq19, inArray as inArray3, sql as sql6 } from "drizzle-orm";
13779
+ import { and as and10, eq as eq21, inArray as inArray4, sql as sql7 } from "drizzle-orm";
13048
13780
 
13049
13781
  // src/citation-utils.ts
13050
13782
  function domainMatches(domain, canonicalDomain) {
@@ -13296,11 +14028,11 @@ var JobRunner = class {
13296
14028
  this.registry = registry;
13297
14029
  }
13298
14030
  recoverStaleRuns() {
13299
- const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray3(runs.status, ["running", "queued"])).all();
14031
+ const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray4(runs.status, ["running", "queued"])).all();
13300
14032
  if (stale.length === 0) return;
13301
14033
  const now = (/* @__PURE__ */ new Date()).toISOString();
13302
14034
  for (const run of stale) {
13303
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq19(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();
13304
14036
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
13305
14037
  }
13306
14038
  }
@@ -13328,10 +14060,10 @@ var JobRunner = class {
13328
14060
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
13329
14061
  }
13330
14062
  if (existingRun.status === "queued") {
13331
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and8(eq19(runs.id, runId), eq19(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();
13332
14064
  }
13333
14065
  this.throwIfRunCancelled(runId);
13334
- const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
14066
+ const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
13335
14067
  if (!project) {
13336
14068
  throw new Error(`Project ${projectId} not found`);
13337
14069
  }
@@ -13351,8 +14083,8 @@ var JobRunner = class {
13351
14083
  throw new Error("No providers configured. Add at least one provider API key.");
13352
14084
  }
13353
14085
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
13354
- projectKeywords = this.db.select().from(keywords).where(eq19(keywords.projectId, projectId)).all();
13355
- const projectCompetitors = this.db.select().from(competitors).where(eq19(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();
13356
14088
  const competitorDomains = projectCompetitors.map((c) => c.domain);
13357
14089
  const allDomains = effectiveDomains({
13358
14090
  canonicalDomain: project.canonicalDomain,
@@ -13368,7 +14100,7 @@ var JobRunner = class {
13368
14100
  const todayPeriod = getCurrentUsageDay();
13369
14101
  for (const p of activeProviders) {
13370
14102
  const providerScope = `${projectId}:${p.adapter.name}`;
13371
- const providerUsage = this.db.select().from(usageCounters).where(eq19(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);
13372
14104
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
13373
14105
  if (providerUsage + queriesPerProvider > limit) {
13374
14106
  throw new Error(
@@ -13509,12 +14241,12 @@ var JobRunner = class {
13509
14241
  const someFailed = providerErrors.size > 0;
13510
14242
  if (allFailed) {
13511
14243
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
13512
- this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq19(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();
13513
14245
  } else if (someFailed) {
13514
14246
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
13515
- this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq19(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();
13516
14248
  } else {
13517
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
14249
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
13518
14250
  }
13519
14251
  this.flushProviderUsage(projectId, providerDispatchCounts);
13520
14252
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -13549,7 +14281,7 @@ var JobRunner = class {
13549
14281
  status: "failed",
13550
14282
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13551
14283
  error: errorMessage
13552
- }).where(eq19(runs.id, runId)).run();
14284
+ }).where(eq21(runs.id, runId)).run();
13553
14285
  this.flushProviderUsage(projectId, providerDispatchCounts);
13554
14286
  trackEvent("run.completed", {
13555
14287
  status: "failed",
@@ -13578,7 +14310,7 @@ var JobRunner = class {
13578
14310
  updatedAt: now
13579
14311
  }).onConflictDoUpdate({
13580
14312
  target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
13581
- set: { count: sql6`${usageCounters.count} + ${count}`, updatedAt: now }
14313
+ set: { count: sql7`${usageCounters.count} + ${count}`, updatedAt: now }
13582
14314
  }).run();
13583
14315
  }
13584
14316
  flushProviderUsage(projectId, providerDispatchCounts) {
@@ -13592,7 +14324,7 @@ var JobRunner = class {
13592
14324
  status: runs.status,
13593
14325
  finishedAt: runs.finishedAt,
13594
14326
  error: runs.error
13595
- }).from(runs).where(eq19(runs.id, runId)).get();
14327
+ }).from(runs).where(eq21(runs.id, runId)).get();
13596
14328
  }
13597
14329
  isRunCancelled(runId) {
13598
14330
  return this.getRunState(runId)?.status === "cancelled";
@@ -13608,7 +14340,7 @@ var JobRunner = class {
13608
14340
  this.db.update(runs).set({
13609
14341
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13610
14342
  error: currentRun.error ?? "Cancelled by user"
13611
- }).where(eq19(runs.id, runId)).run();
14343
+ }).where(eq21(runs.id, runId)).run();
13612
14344
  }
13613
14345
  trackEvent("run.completed", {
13614
14346
  status: "cancelled",
@@ -13631,7 +14363,7 @@ function getCurrentUsageDay() {
13631
14363
 
13632
14364
  // src/gsc-sync.ts
13633
14365
  import crypto20 from "crypto";
13634
- import { eq as eq20, and as and9, sql as sql7 } from "drizzle-orm";
14366
+ import { eq as eq22, and as and11, sql as sql8 } from "drizzle-orm";
13635
14367
  var log2 = createLogger("GscSync");
13636
14368
  function formatDate2(d) {
13637
14369
  return d.toISOString().split("T")[0];
@@ -13643,13 +14375,13 @@ function daysAgo(n) {
13643
14375
  }
13644
14376
  async function executeGscSync(db, runId, projectId, opts) {
13645
14377
  const now = (/* @__PURE__ */ new Date()).toISOString();
13646
- db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
14378
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq22(runs.id, runId)).run();
13647
14379
  try {
13648
14380
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
13649
14381
  if (!googleClientId || !googleClientSecret) {
13650
14382
  throw new Error("Google OAuth is not configured in the local Canonry config");
13651
14383
  }
13652
- const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
14384
+ const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
13653
14385
  if (!project) {
13654
14386
  throw new Error(`Project not found: ${projectId}`);
13655
14387
  }
@@ -13683,10 +14415,10 @@ async function executeGscSync(db, runId, projectId, opts) {
13683
14415
  });
13684
14416
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
13685
14417
  db.delete(gscSearchData).where(
13686
- and9(
13687
- eq20(gscSearchData.projectId, projectId),
13688
- sql7`${gscSearchData.date} >= ${startDate}`,
13689
- sql7`${gscSearchData.date} <= ${endDate}`
14418
+ and11(
14419
+ eq22(gscSearchData.projectId, projectId),
14420
+ sql8`${gscSearchData.date} >= ${startDate}`,
14421
+ sql8`${gscSearchData.date} <= ${endDate}`
13690
14422
  )
13691
14423
  ).run();
13692
14424
  const batchSize = 500;
@@ -13751,7 +14483,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13751
14483
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
13752
14484
  }
13753
14485
  }
13754
- const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
14486
+ const allInspections = db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, projectId)).all();
13755
14487
  const latestByUrl = /* @__PURE__ */ new Map();
13756
14488
  for (const row of allInspections) {
13757
14489
  const existing = latestByUrl.get(row.url);
@@ -13772,7 +14504,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13772
14504
  }
13773
14505
  }
13774
14506
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
13775
- db.delete(gscCoverageSnapshots).where(and9(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
14507
+ db.delete(gscCoverageSnapshots).where(and11(eq22(gscCoverageSnapshots.projectId, projectId), eq22(gscCoverageSnapshots.date, snapshotDate))).run();
13776
14508
  db.insert(gscCoverageSnapshots).values({
13777
14509
  id: crypto20.randomUUID(),
13778
14510
  projectId,
@@ -13783,11 +14515,11 @@ async function executeGscSync(db, runId, projectId, opts) {
13783
14515
  reasonBreakdown: JSON.stringify(reasonCounts),
13784
14516
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
13785
14517
  }).run();
13786
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14518
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
13787
14519
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
13788
14520
  } catch (err) {
13789
14521
  const errorMsg = err instanceof Error ? err.message : String(err);
13790
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14522
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
13791
14523
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
13792
14524
  throw err;
13793
14525
  }
@@ -13795,7 +14527,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13795
14527
 
13796
14528
  // src/gsc-inspect-sitemap.ts
13797
14529
  import crypto21 from "crypto";
13798
- import { eq as eq21, and as and10 } from "drizzle-orm";
14530
+ import { eq as eq23, and as and12 } from "drizzle-orm";
13799
14531
 
13800
14532
  // src/sitemap-parser.ts
13801
14533
  var log3 = createLogger("SitemapParser");
@@ -13916,13 +14648,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
13916
14648
  var log4 = createLogger("InspectSitemap");
13917
14649
  async function executeInspectSitemap(db, runId, projectId, opts) {
13918
14650
  const now = (/* @__PURE__ */ new Date()).toISOString();
13919
- db.update(runs).set({ status: "running", startedAt: now }).where(eq21(runs.id, runId)).run();
14651
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq23(runs.id, runId)).run();
13920
14652
  try {
13921
14653
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
13922
14654
  if (!googleClientId || !googleClientSecret) {
13923
14655
  throw new Error("Google OAuth is not configured in the local Canonry config");
13924
14656
  }
13925
- const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
14657
+ const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
13926
14658
  if (!project) {
13927
14659
  throw new Error(`Project not found: ${projectId}`);
13928
14660
  }
@@ -13990,7 +14722,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
13990
14722
  await new Promise((r) => setTimeout(r, 1e3));
13991
14723
  }
13992
14724
  }
13993
- const allInspections = db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, projectId)).all();
14725
+ const allInspections = db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, projectId)).all();
13994
14726
  const latestByUrl = /* @__PURE__ */ new Map();
13995
14727
  for (const row of allInspections) {
13996
14728
  const existing = latestByUrl.get(row.url);
@@ -14011,7 +14743,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14011
14743
  }
14012
14744
  }
14013
14745
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
14014
- db.delete(gscCoverageSnapshots).where(and10(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
14746
+ db.delete(gscCoverageSnapshots).where(and12(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
14015
14747
  db.insert(gscCoverageSnapshots).values({
14016
14748
  id: crypto21.randomUUID(),
14017
14749
  projectId,
@@ -14023,11 +14755,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14023
14755
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14024
14756
  }).run();
14025
14757
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
14026
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
14758
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
14027
14759
  log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
14028
14760
  } catch (err) {
14029
14761
  const errorMsg = err instanceof Error ? err.message : String(err);
14030
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
14762
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
14031
14763
  log4.error("inspect.failed", { runId, projectId, error: errorMsg });
14032
14764
  throw err;
14033
14765
  }
@@ -14035,7 +14767,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14035
14767
 
14036
14768
  // src/bing-inspect-sitemap.ts
14037
14769
  import crypto22 from "crypto";
14038
- import { eq as eq22, desc as desc9 } from "drizzle-orm";
14770
+ import { eq as eq24, desc as desc11 } from "drizzle-orm";
14039
14771
  var log5 = createLogger("BingInspectSitemap");
14040
14772
  function parseBingDate2(value) {
14041
14773
  if (!value) return null;
@@ -14053,9 +14785,9 @@ function isBlockingIssueType2(issueType) {
14053
14785
  }
14054
14786
  async function executeBingInspectSitemap(db, runId, projectId, opts) {
14055
14787
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14056
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq22(runs.id, runId)).run();
14788
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq24(runs.id, runId)).run();
14057
14789
  try {
14058
- const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
14790
+ const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
14059
14791
  if (!project) {
14060
14792
  throw new Error(`Project not found: ${projectId}`);
14061
14793
  }
@@ -14073,7 +14805,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14073
14805
  if (sitemapUrls.length === 0) {
14074
14806
  throw new Error("No URLs found in sitemap");
14075
14807
  }
14076
- const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq22(bingUrlInspections.projectId, projectId)).all();
14808
+ const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, projectId)).all();
14077
14809
  const trackedUrls = new Set(trackedRows.map((r) => r.url));
14078
14810
  const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
14079
14811
  log5.info("sitemap.diff", {
@@ -14156,7 +14888,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14156
14888
  await new Promise((r) => setTimeout(r, 1e3));
14157
14889
  }
14158
14890
  }
14159
- const allInspections = db.select().from(bingUrlInspections).where(eq22(bingUrlInspections.projectId, projectId)).orderBy(desc9(bingUrlInspections.inspectedAt)).all();
14891
+ const allInspections = db.select().from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, projectId)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
14160
14892
  const latestByUrl = /* @__PURE__ */ new Map();
14161
14893
  const definitiveByUrl = /* @__PURE__ */ new Map();
14162
14894
  for (const row of allInspections) {
@@ -14199,7 +14931,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14199
14931
  }
14200
14932
  }).run();
14201
14933
  const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
14202
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
14934
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
14203
14935
  log5.info("inspect.completed", {
14204
14936
  runId,
14205
14937
  projectId,
@@ -14213,7 +14945,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14213
14945
  });
14214
14946
  } catch (err) {
14215
14947
  const errorMsg = err instanceof Error ? err.message : String(err);
14216
- db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(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();
14217
14949
  log5.error("inspect.failed", { runId, projectId, error: errorMsg });
14218
14950
  throw err;
14219
14951
  }
@@ -14222,7 +14954,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14222
14954
  // src/commoncrawl-sync.ts
14223
14955
  import crypto23 from "crypto";
14224
14956
  import path10 from "path";
14225
- import { and as and11, eq as eq23, sql as sql8 } from "drizzle-orm";
14957
+ import { and as and13, eq as eq25, sql as sql9 } from "drizzle-orm";
14226
14958
  var log6 = createLogger("CommonCrawlSync");
14227
14959
  var INSERT_CHUNK_SIZE = 1e4;
14228
14960
  function defaultDeps() {
@@ -14248,7 +14980,7 @@ async function executeReleaseSync(db, syncId, opts) {
14248
14980
  phaseDetail: "downloading vertices + edges",
14249
14981
  updatedAt: downloadStartedAt,
14250
14982
  error: null
14251
- }).where(eq23(ccReleaseSyncs.id, syncId)).run();
14983
+ }).where(eq25(ccReleaseSyncs.id, syncId)).run();
14252
14984
  const paths = ccReleasePaths(release);
14253
14985
  const releaseCacheDir = path10.join(deps.cacheDir, release);
14254
14986
  const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
@@ -14271,7 +15003,7 @@ async function executeReleaseSync(db, syncId, opts) {
14271
15003
  vertexSha256: vertex.sha256,
14272
15004
  edgesSha256: edges.sha256,
14273
15005
  updatedAt: downloadFinishedAt
14274
- }).where(eq23(ccReleaseSyncs.id, syncId)).run();
15006
+ }).where(eq25(ccReleaseSyncs.id, syncId)).run();
14275
15007
  const allProjects = db.select().from(projects).all();
14276
15008
  const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
14277
15009
  let rows = [];
@@ -14287,8 +15019,8 @@ async function executeReleaseSync(db, syncId, opts) {
14287
15019
  }
14288
15020
  const queriedAt = deps.now().toISOString();
14289
15021
  db.transaction((tx) => {
14290
- tx.delete(backlinkDomains).where(eq23(backlinkDomains.releaseSyncId, syncId)).run();
14291
- tx.delete(backlinkSummaries).where(eq23(backlinkSummaries.releaseSyncId, syncId)).run();
15022
+ tx.delete(backlinkDomains).where(eq25(backlinkDomains.releaseSyncId, syncId)).run();
15023
+ tx.delete(backlinkSummaries).where(eq25(backlinkSummaries.releaseSyncId, syncId)).run();
14292
15024
  const expanded = [];
14293
15025
  for (const r of rows) {
14294
15026
  const projectIds = projectsByDomain.get(r.targetDomain);
@@ -14347,7 +15079,7 @@ async function executeReleaseSync(db, syncId, opts) {
14347
15079
  domainsDiscovered: rows.length,
14348
15080
  updatedAt: finishedAt,
14349
15081
  error: null
14350
- }).where(eq23(ccReleaseSyncs.id, syncId)).run();
15082
+ }).where(eq25(ccReleaseSyncs.id, syncId)).run();
14351
15083
  log6.info("sync.completed", {
14352
15084
  syncId,
14353
15085
  release,
@@ -14377,7 +15109,7 @@ async function executeReleaseSync(db, syncId, opts) {
14377
15109
  error: errorMsg,
14378
15110
  phaseDetail: null,
14379
15111
  updatedAt: finishedAt
14380
- }).where(eq23(ccReleaseSyncs.id, syncId)).run();
15112
+ }).where(eq25(ccReleaseSyncs.id, syncId)).run();
14381
15113
  log6.error("sync.failed", { syncId, release, error: errorMsg });
14382
15114
  throw err;
14383
15115
  }
@@ -14413,7 +15145,7 @@ function computeSummary(rows) {
14413
15145
  // src/backlink-extract.ts
14414
15146
  import crypto24 from "crypto";
14415
15147
  import fs8 from "fs";
14416
- import { and as and12, desc as desc10, eq as eq24 } from "drizzle-orm";
15148
+ import { and as and14, desc as desc12, eq as eq26 } from "drizzle-orm";
14417
15149
  var log7 = createLogger("BacklinkExtract");
14418
15150
  function defaultDeps2() {
14419
15151
  return {
@@ -14425,13 +15157,13 @@ function defaultDeps2() {
14425
15157
  async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14426
15158
  const deps = { ...defaultDeps2(), ...opts.deps };
14427
15159
  const startedAt = deps.now().toISOString();
14428
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq24(runs.id, runId)).run();
15160
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq26(runs.id, runId)).run();
14429
15161
  try {
14430
- const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
15162
+ const project = db.select().from(projects).where(eq26(projects.id, projectId)).get();
14431
15163
  if (!project) {
14432
15164
  throw new Error(`Project not found: ${projectId}`);
14433
15165
  }
14434
- const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq24(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq24(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc10(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();
14435
15167
  if (!sync) {
14436
15168
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
14437
15169
  }
@@ -14459,7 +15191,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14459
15191
  const targetDomain = project.canonicalDomain;
14460
15192
  db.transaction((tx) => {
14461
15193
  tx.delete(backlinkDomains).where(
14462
- and12(eq24(backlinkDomains.projectId, projectId), eq24(backlinkDomains.release, release))
15194
+ and14(eq26(backlinkDomains.projectId, projectId), eq26(backlinkDomains.release, release))
14463
15195
  ).run();
14464
15196
  if (rows.length > 0) {
14465
15197
  const values = rows.map((r) => ({
@@ -14499,7 +15231,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14499
15231
  }).run();
14500
15232
  });
14501
15233
  const finishedAt = deps.now().toISOString();
14502
- db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq24(runs.id, runId)).run();
15234
+ db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
14503
15235
  log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
14504
15236
  } catch (err) {
14505
15237
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -14508,7 +15240,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14508
15240
  status: RunStatuses.failed,
14509
15241
  error: errorMsg,
14510
15242
  finishedAt
14511
- }).where(eq24(runs.id, runId)).run();
15243
+ }).where(eq26(runs.id, runId)).run();
14512
15244
  log7.error("extract.failed", { runId, projectId, error: errorMsg });
14513
15245
  throw err;
14514
15246
  }
@@ -14581,7 +15313,7 @@ var ProviderRegistry = class {
14581
15313
 
14582
15314
  // src/scheduler.ts
14583
15315
  import cron from "node-cron";
14584
- import { eq as eq25 } from "drizzle-orm";
15316
+ import { eq as eq27 } from "drizzle-orm";
14585
15317
  var log8 = createLogger("Scheduler");
14586
15318
  var Scheduler = class {
14587
15319
  db;
@@ -14593,7 +15325,7 @@ var Scheduler = class {
14593
15325
  }
14594
15326
  /** Load all enabled schedules from DB and register cron jobs. */
14595
15327
  start() {
14596
- const allSchedules = this.db.select().from(schedules).where(eq25(schedules.enabled, 1)).all();
15328
+ const allSchedules = this.db.select().from(schedules).where(eq27(schedules.enabled, 1)).all();
14597
15329
  for (const schedule of allSchedules) {
14598
15330
  const missedRunAt = schedule.nextRunAt;
14599
15331
  this.registerCronTask(schedule);
@@ -14618,7 +15350,7 @@ var Scheduler = class {
14618
15350
  this.stopTask(projectId, existing, "Stopped");
14619
15351
  this.tasks.delete(projectId);
14620
15352
  }
14621
- const schedule = this.db.select().from(schedules).where(eq25(schedules.projectId, projectId)).get();
15353
+ const schedule = this.db.select().from(schedules).where(eq27(schedules.projectId, projectId)).get();
14622
15354
  if (schedule && schedule.enabled === 1) {
14623
15355
  this.registerCronTask(schedule);
14624
15356
  }
@@ -14651,14 +15383,14 @@ var Scheduler = class {
14651
15383
  this.db.update(schedules).set({
14652
15384
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
14653
15385
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14654
- }).where(eq25(schedules.id, scheduleId)).run();
15386
+ }).where(eq27(schedules.id, scheduleId)).run();
14655
15387
  const label = schedule.preset ?? cronExpr;
14656
15388
  log8.info("cron.registered", { projectId, schedule: label, timezone });
14657
15389
  }
14658
15390
  triggerRun(scheduleId, projectId) {
14659
15391
  try {
14660
15392
  const now = (/* @__PURE__ */ new Date()).toISOString();
14661
- const currentSchedule = this.db.select().from(schedules).where(eq25(schedules.id, scheduleId)).get();
15393
+ const currentSchedule = this.db.select().from(schedules).where(eq27(schedules.id, scheduleId)).get();
14662
15394
  if (!currentSchedule || currentSchedule.enabled !== 1) {
14663
15395
  log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
14664
15396
  this.remove(projectId);
@@ -14666,7 +15398,7 @@ var Scheduler = class {
14666
15398
  }
14667
15399
  const task = this.tasks.get(projectId);
14668
15400
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
14669
- const project = this.db.select().from(projects).where(eq25(projects.id, projectId)).get();
15401
+ const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
14670
15402
  if (!project) {
14671
15403
  log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
14672
15404
  this.remove(projectId);
@@ -14695,7 +15427,7 @@ var Scheduler = class {
14695
15427
  this.db.update(schedules).set({
14696
15428
  nextRunAt,
14697
15429
  updatedAt: now
14698
- }).where(eq25(schedules.id, currentSchedule.id)).run();
15430
+ }).where(eq27(schedules.id, currentSchedule.id)).run();
14699
15431
  return;
14700
15432
  }
14701
15433
  const runId = queueResult.runId;
@@ -14703,7 +15435,7 @@ var Scheduler = class {
14703
15435
  lastRunAt: now,
14704
15436
  nextRunAt,
14705
15437
  updatedAt: now
14706
- }).where(eq25(schedules.id, currentSchedule.id)).run();
15438
+ }).where(eq27(schedules.id, currentSchedule.id)).run();
14707
15439
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
14708
15440
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
14709
15441
  log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -14715,7 +15447,7 @@ var Scheduler = class {
14715
15447
  };
14716
15448
 
14717
15449
  // src/notifier.ts
14718
- import { eq as eq26, desc as desc11, and as and13, or as or2 } from "drizzle-orm";
15450
+ import { eq as eq28, desc as desc13, and as and15, or as or3 } from "drizzle-orm";
14719
15451
  import crypto25 from "crypto";
14720
15452
  var log9 = createLogger("Notifier");
14721
15453
  var Notifier = class {
@@ -14728,18 +15460,18 @@ var Notifier = class {
14728
15460
  /** Called after a run completes (success, partial, or failed). */
14729
15461
  async onRunCompleted(runId, projectId) {
14730
15462
  log9.info("run.completed", { runId, projectId });
14731
- const notifs = this.db.select().from(notifications).where(eq26(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);
14732
15464
  if (notifs.length === 0) {
14733
15465
  log9.info("notifications.none-enabled", { projectId });
14734
15466
  return;
14735
15467
  }
14736
15468
  log9.info("notifications.found", { projectId, count: notifs.length });
14737
- const run = this.db.select().from(runs).where(eq26(runs.id, runId)).get();
15469
+ const run = this.db.select().from(runs).where(eq28(runs.id, runId)).get();
14738
15470
  if (!run) {
14739
15471
  log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
14740
15472
  return;
14741
15473
  }
14742
- const project = this.db.select().from(projects).where(eq26(projects.id, projectId)).get();
15474
+ const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
14743
15475
  if (!project) {
14744
15476
  log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
14745
15477
  return;
@@ -14786,11 +15518,11 @@ var Notifier = class {
14786
15518
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
14787
15519
  if (highInsights.length > 0) insightEvents.push("insight.high");
14788
15520
  if (insightEvents.length === 0) return;
14789
- const notifs = this.db.select().from(notifications).where(eq26(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);
14790
15522
  if (notifs.length === 0) return;
14791
- const run = this.db.select().from(runs).where(eq26(runs.id, runId)).get();
15523
+ const run = this.db.select().from(runs).where(eq28(runs.id, runId)).get();
14792
15524
  if (!run) return;
14793
- const project = this.db.select().from(projects).where(eq26(projects.id, projectId)).get();
15525
+ const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
14794
15526
  if (!project) return;
14795
15527
  for (const notif of notifs) {
14796
15528
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -14821,11 +15553,11 @@ var Notifier = class {
14821
15553
  }
14822
15554
  computeTransitions(runId, projectId) {
14823
15555
  const recentRuns = this.db.select().from(runs).where(
14824
- and13(
14825
- eq26(runs.projectId, projectId),
14826
- or2(eq26(runs.status, "completed"), eq26(runs.status, "partial"))
15556
+ and15(
15557
+ eq28(runs.projectId, projectId),
15558
+ or3(eq28(runs.status, "completed"), eq28(runs.status, "partial"))
14827
15559
  )
14828
- ).orderBy(desc11(runs.createdAt)).limit(2).all();
15560
+ ).orderBy(desc13(runs.createdAt)).limit(2).all();
14829
15561
  if (recentRuns.length < 2) return [];
14830
15562
  const currentRunId = recentRuns[0].id;
14831
15563
  const previousRunId = recentRuns[1].id;
@@ -14835,12 +15567,12 @@ var Notifier = class {
14835
15567
  keyword: keywords.keyword,
14836
15568
  provider: querySnapshots.provider,
14837
15569
  citationState: querySnapshots.citationState
14838
- }).from(querySnapshots).leftJoin(keywords, eq26(querySnapshots.keywordId, keywords.id)).where(eq26(querySnapshots.runId, currentRunId)).all();
15570
+ }).from(querySnapshots).leftJoin(keywords, eq28(querySnapshots.keywordId, keywords.id)).where(eq28(querySnapshots.runId, currentRunId)).all();
14839
15571
  const previousSnapshots = this.db.select({
14840
15572
  keywordId: querySnapshots.keywordId,
14841
15573
  provider: querySnapshots.provider,
14842
15574
  citationState: querySnapshots.citationState
14843
- }).from(querySnapshots).where(eq26(querySnapshots.runId, previousRunId)).all();
15575
+ }).from(querySnapshots).where(eq28(querySnapshots.runId, previousRunId)).all();
14844
15576
  const prevMap = /* @__PURE__ */ new Map();
14845
15577
  for (const s of previousSnapshots) {
14846
15578
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -14957,7 +15689,7 @@ var RunCoordinator = class {
14957
15689
 
14958
15690
  // src/agent/session-registry.ts
14959
15691
  import crypto27 from "crypto";
14960
- import { eq as eq28 } from "drizzle-orm";
15692
+ import { eq as eq30 } from "drizzle-orm";
14961
15693
 
14962
15694
  // src/agent/session.ts
14963
15695
  import fs11 from "fs";
@@ -15177,7 +15909,7 @@ import { Type as Type2 } from "@sinclair/typebox";
15177
15909
 
15178
15910
  // src/agent/memory-store.ts
15179
15911
  import crypto26 from "crypto";
15180
- import { and as and14, desc as desc12, eq as eq27, 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";
15181
15913
  var COMPACTION_KEY_PREFIX = "compaction:";
15182
15914
  var COMPACTION_NOTES_PER_SESSION = 3;
15183
15915
  function rowToDto(row) {
@@ -15191,7 +15923,7 @@ function rowToDto(row) {
15191
15923
  };
15192
15924
  }
15193
15925
  function listMemoryEntries(db, projectId, opts = {}) {
15194
- const query = db.select().from(agentMemory).where(eq27(agentMemory.projectId, projectId)).orderBy(desc12(agentMemory.updatedAt));
15926
+ const query = db.select().from(agentMemory).where(eq29(agentMemory.projectId, projectId)).orderBy(desc14(agentMemory.updatedAt));
15195
15927
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
15196
15928
  return rows.map(rowToDto);
15197
15929
  }
@@ -15222,12 +15954,12 @@ function upsertMemoryEntry(db, args) {
15222
15954
  updatedAt: now
15223
15955
  }
15224
15956
  }).run();
15225
- const row = db.select().from(agentMemory).where(and14(eq27(agentMemory.projectId, args.projectId), eq27(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();
15226
15958
  if (!row) throw new Error("memory upsert produced no row");
15227
15959
  return rowToDto(row);
15228
15960
  }
15229
15961
  function deleteMemoryEntry(db, projectId, key) {
15230
- const result = db.delete(agentMemory).where(and14(eq27(agentMemory.projectId, projectId), eq27(agentMemory.key, key))).run();
15962
+ const result = db.delete(agentMemory).where(and16(eq29(agentMemory.projectId, projectId), eq29(agentMemory.key, key))).run();
15231
15963
  const changes = result.changes ?? 0;
15232
15964
  return changes > 0;
15233
15965
  }
@@ -15256,16 +15988,16 @@ function writeCompactionNote(db, args) {
15256
15988
  }).run();
15257
15989
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
15258
15990
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
15259
- and14(
15260
- eq27(agentMemory.projectId, args.projectId),
15261
- like(agentMemory.key, `${sessionPrefix}%`)
15991
+ and16(
15992
+ eq29(agentMemory.projectId, args.projectId),
15993
+ like2(agentMemory.key, `${sessionPrefix}%`)
15262
15994
  )
15263
- ).orderBy(desc12(agentMemory.updatedAt)).all();
15995
+ ).orderBy(desc14(agentMemory.updatedAt)).all();
15264
15996
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
15265
15997
  if (stale.length > 0) {
15266
- 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();
15267
15999
  }
15268
- const row = tx.select().from(agentMemory).where(and14(eq27(agentMemory.projectId, args.projectId), eq27(agentMemory.key, key))).get();
16000
+ const row = tx.select().from(agentMemory).where(and16(eq29(agentMemory.projectId, args.projectId), eq29(agentMemory.key, key))).get();
15269
16001
  if (row) inserted = rowToDto(row);
15270
16002
  });
15271
16003
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -15395,6 +16127,59 @@ function buildListCompetitorsTool(ctx) {
15395
16127
  }
15396
16128
  };
15397
16129
  }
16130
+ var ContentTargetsSchema = Type2.Object({
16131
+ limit: Type2.Optional(
16132
+ Type2.Number({
16133
+ description: "Max rows. Defaults to all. Use a small number (3\u201310) when summarizing for the user."
16134
+ })
16135
+ ),
16136
+ includeInProgress: Type2.Optional(
16137
+ Type2.Boolean({
16138
+ description: "Include rows that already have an in-flight tracked action. Default false."
16139
+ })
16140
+ )
16141
+ });
16142
+ function buildGetContentTargetsTool(ctx) {
16143
+ return {
16144
+ name: "get_content_targets",
16145
+ label: "Get content targets",
16146
+ description: "Ranked, action-typed content opportunities. Each row is `{query, action \u2208 create|expand|refresh|add-schema, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence}`. Use this to recommend which post the user should write/refresh next.",
16147
+ parameters: ContentTargetsSchema,
16148
+ execute: async (_toolCallId, params) => {
16149
+ const response = await ctx.client.getContentTargets(ctx.projectName, {
16150
+ limit: params.limit,
16151
+ includeInProgress: params.includeInProgress === true
16152
+ });
16153
+ return textResult2(response);
16154
+ }
16155
+ };
16156
+ }
16157
+ var ContentSourcesSchema = Type2.Object({});
16158
+ function buildGetContentSourcesTool(ctx) {
16159
+ return {
16160
+ name: "get_grounding_sources",
16161
+ label: "Get grounding sources",
16162
+ description: "URL-level competitive grounding-source map. Per query, lists every URL the LLM cited (our domain vs competitors), with citation count and providers. Read this to understand what specific competitor URL is winning a query.",
16163
+ parameters: ContentSourcesSchema,
16164
+ execute: async () => {
16165
+ const response = await ctx.client.getContentSources(ctx.projectName);
16166
+ return textResult2(response);
16167
+ }
16168
+ };
16169
+ }
16170
+ var ContentGapsSchema = Type2.Object({});
16171
+ function buildGetContentGapsTool(ctx) {
16172
+ return {
16173
+ name: "get_content_gaps",
16174
+ label: "Get content gaps",
16175
+ description: 'Queries where competitors are cited but our domain is not. Ranked by miss rate. The blunt-instrument view of "what competitors are winning that we are not." Use `get_content_targets` for action-typed recommendations on the same data.',
16176
+ parameters: ContentGapsSchema,
16177
+ execute: async () => {
16178
+ const response = await ctx.client.getContentGaps(ctx.projectName);
16179
+ return textResult2(response);
16180
+ }
16181
+ };
16182
+ }
15398
16183
  var RunDetailSchema = Type2.Object({
15399
16184
  runId: Type2.String({
15400
16185
  description: "Run id (UUID) to fetch. Typically obtained from get_status runs[].id."
@@ -15472,7 +16257,10 @@ function buildReadTools(ctx) {
15472
16257
  buildListCompetitorsTool(ctx),
15473
16258
  buildGetRunTool(ctx),
15474
16259
  buildRecallTool(ctx),
15475
- buildListBacklinksTool(ctx)
16260
+ buildListBacklinksTool(ctx),
16261
+ buildGetContentTargetsTool(ctx),
16262
+ buildGetContentSourcesTool(ctx),
16263
+ buildGetContentGapsTool(ctx)
15476
16264
  ];
15477
16265
  }
15478
16266
  var RunSweepSchema = Type2.Object({
@@ -15941,7 +16729,7 @@ var SessionRegistry = class {
15941
16729
  modelProvider: effectiveProvider,
15942
16730
  modelId: effectiveModelId,
15943
16731
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15944
- }).where(eq28(agentSessions.projectId, projectId)).run();
16732
+ }).where(eq30(agentSessions.projectId, projectId)).run();
15945
16733
  }
15946
16734
  const agent2 = createAeroSession({
15947
16735
  projectName,
@@ -16159,7 +16947,7 @@ ${lines.join("\n")}
16159
16947
  modelProvider: nextProvider,
16160
16948
  modelId: nextModelId,
16161
16949
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16162
- }).where(eq28(agentSessions.projectId, projectId)).run();
16950
+ }).where(eq30(agentSessions.projectId, projectId)).run();
16163
16951
  }
16164
16952
  /** Persist a session's transcript back to the DB. Call after any run settles. */
16165
16953
  save(projectName) {
@@ -16321,11 +17109,11 @@ ${lines.join("\n")}
16321
17109
  return id;
16322
17110
  }
16323
17111
  tryResolveProjectId(projectName) {
16324
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq28(projects.name, projectName)).get();
17112
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq30(projects.name, projectName)).get();
16325
17113
  return row?.id;
16326
17114
  }
16327
17115
  loadRow(projectId) {
16328
- const row = this.opts.db.select().from(agentSessions).where(eq28(agentSessions.projectId, projectId)).get();
17116
+ const row = this.opts.db.select().from(agentSessions).where(eq30(agentSessions.projectId, projectId)).get();
16329
17117
  return row ?? null;
16330
17118
  }
16331
17119
  insertRow(params) {
@@ -16344,14 +17132,14 @@ ${lines.join("\n")}
16344
17132
  }
16345
17133
  updateRow(projectId, patch) {
16346
17134
  const now = (/* @__PURE__ */ new Date()).toISOString();
16347
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq28(agentSessions.projectId, projectId)).run();
17135
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq30(agentSessions.projectId, projectId)).run();
16348
17136
  }
16349
17137
  };
16350
17138
 
16351
17139
  // src/agent/agent-routes.ts
16352
- import { eq as eq29 } from "drizzle-orm";
17140
+ import { eq as eq31 } from "drizzle-orm";
16353
17141
  function resolveProject2(db, name) {
16354
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq29(projects.name, name)).get();
17142
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq31(projects.name, name)).get();
16355
17143
  if (!row) throw notFound("project", name);
16356
17144
  return row;
16357
17145
  }
@@ -16360,7 +17148,7 @@ function registerAgentRoutes(app, opts) {
16360
17148
  "/projects/:name/agent/transcript",
16361
17149
  async (request) => {
16362
17150
  const project = resolveProject2(opts.db, request.params.name);
16363
- const row = opts.db.select().from(agentSessions).where(eq29(agentSessions.projectId, project.id)).get();
17151
+ const row = opts.db.select().from(agentSessions).where(eq31(agentSessions.projectId, project.id)).get();
16364
17152
  if (!row) {
16365
17153
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
16366
17154
  }
@@ -16384,7 +17172,7 @@ function registerAgentRoutes(app, opts) {
16384
17172
  async (request) => {
16385
17173
  const project = resolveProject2(opts.db, request.params.name);
16386
17174
  opts.sessionRegistry.reset(project.name);
16387
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq29(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();
16388
17176
  return { status: "reset" };
16389
17177
  }
16390
17178
  );
@@ -16688,7 +17476,7 @@ var SnapshotService = class {
16688
17476
  }
16689
17477
  async createReport(input) {
16690
17478
  const companyName = input.companyName.trim();
16691
- const domain = normalizeDomain(input.domain);
17479
+ const domain = normalizeDomain2(input.domain);
16692
17480
  const manualPhrases = normalizeStringList(input.phrases ?? []);
16693
17481
  const manualCompetitors = normalizeStringList(input.competitors ?? []);
16694
17482
  const providers = this.registry.getAll();
@@ -17128,7 +17916,7 @@ function extractCompetitorsFromResponse(ctx) {
17128
17916
  const targetDomain = extractHostname2(ctx.targetDomain);
17129
17917
  for (const hint of ctx.manualCompetitors) {
17130
17918
  if (isDomainLike(hint)) {
17131
- const normalizedHint = normalizeDomain(hint);
17919
+ const normalizedHint = normalizeDomain2(hint);
17132
17920
  if (domainMatches2(normalizedHint, targetDomain)) continue;
17133
17921
  if (ctx.citedDomains.some((domain) => domainMatches2(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
17134
17922
  competitors2.add(normalizedHint);
@@ -17187,7 +17975,7 @@ function uniqueStrings2(values) {
17187
17975
  values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
17188
17976
  )];
17189
17977
  }
17190
- function normalizeDomain(value) {
17978
+ function normalizeDomain2(value) {
17191
17979
  const trimmed = value.trim();
17192
17980
  if (!trimmed) return trimmed;
17193
17981
  try {
@@ -17198,15 +17986,15 @@ function normalizeDomain(value) {
17198
17986
  }
17199
17987
  }
17200
17988
  function extractHostname2(value) {
17201
- return normalizeDomain(value);
17989
+ return normalizeDomain2(value);
17202
17990
  }
17203
17991
  function domainMatches2(candidate, target) {
17204
- const normalizedCandidate = normalizeDomain(candidate);
17205
- const normalizedTarget = normalizeDomain(target);
17992
+ const normalizedCandidate = normalizeDomain2(candidate);
17993
+ const normalizedTarget = normalizeDomain2(target);
17206
17994
  return normalizedCandidate === normalizedTarget || normalizedCandidate.endsWith(`.${normalizedTarget}`);
17207
17995
  }
17208
17996
  function isDomainLike(value) {
17209
- const normalized = normalizeDomain(value);
17997
+ const normalized = normalizeDomain2(value);
17210
17998
  return normalized.includes(".") && !normalized.includes(" ");
17211
17999
  }
17212
18000
  function clipText(value, length) {
@@ -17406,7 +18194,7 @@ async function createServer(opts) {
17406
18194
  intelligenceService,
17407
18195
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
17408
18196
  async ({ runId, projectId, insightCount, criticalOrHigh }) => {
17409
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq30(projects.id, projectId)).get();
18197
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq32(projects.id, projectId)).get();
17410
18198
  if (!project) return;
17411
18199
  sessionRegistry.queueFollowUp(project.name, {
17412
18200
  role: "user",
@@ -17546,7 +18334,7 @@ async function createServer(opts) {
17546
18334
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
17547
18335
  if (opts.config.apiKey) {
17548
18336
  const keyHash = hashApiKey(opts.config.apiKey);
17549
- const existing = opts.db.select().from(apiKeys).where(eq30(apiKeys.keyHash, keyHash)).get();
18337
+ const existing = opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, keyHash)).get();
17550
18338
  if (!existing) {
17551
18339
  const prefix = opts.config.apiKey.slice(0, 12);
17552
18340
  opts.db.insert(apiKeys).values({
@@ -17598,7 +18386,7 @@ async function createServer(opts) {
17598
18386
  };
17599
18387
  const getDefaultApiKey = () => {
17600
18388
  if (!opts.config.apiKey) return void 0;
17601
- return opts.db.select().from(apiKeys).where(eq30(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
18389
+ return opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
17602
18390
  };
17603
18391
  const createPasswordSession = (reply) => {
17604
18392
  const key = getDefaultApiKey();
@@ -17655,12 +18443,12 @@ async function createServer(opts) {
17655
18443
  return reply.send({ authenticated: true });
17656
18444
  }
17657
18445
  if (apiKey) {
17658
- const key = opts.db.select().from(apiKeys).where(eq30(apiKeys.keyHash, hashApiKey(apiKey))).get();
18446
+ const key = opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, hashApiKey(apiKey))).get();
17659
18447
  if (!key || key.revokedAt) {
17660
18448
  const err2 = authInvalid();
17661
18449
  return reply.status(err2.statusCode).send(err2.toJSON());
17662
18450
  }
17663
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(apiKeys.id, key.id)).run();
18451
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(apiKeys.id, key.id)).run();
17664
18452
  const sessionId = createSession(key.id);
17665
18453
  reply.header("set-cookie", serializeSessionCookie({
17666
18454
  name: SESSION_COOKIE_NAME,