@ainyc/canonry 2.15.1 → 2.16.2

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.
@@ -1,5 +1,4 @@
1
1
  import {
2
- AGENT_MEMORY_KEY_MAX_LENGTH,
3
2
  AGENT_MEMORY_VALUE_MAX_BYTES,
4
3
  AGENT_PROVIDER_IDS,
5
4
  AgentProviderIds,
@@ -21,13 +20,16 @@ import {
21
20
  authRequired,
22
21
  brandKeyFromText,
23
22
  buildRunErrorFromMessages,
23
+ canonryMcpTools,
24
24
  categorizeSource,
25
25
  categoryLabel,
26
+ citationStateToCited,
26
27
  competitorBatchRequestSchema,
27
28
  configExists,
28
29
  deliveryFailed,
29
30
  determineAnswerMentioned,
30
31
  effectiveDomains,
32
+ emptyCitationVisibility,
31
33
  extractAnswerMentions,
32
34
  findDuplicateLocationLabels,
33
35
  hasLocationLabel,
@@ -60,7 +62,7 @@ import {
60
62
  visibilityStateFromAnswerMentioned,
61
63
  windowCutoff,
62
64
  wordpressEnvSchema
63
- } from "./chunk-OX24LLIH.js";
65
+ } from "./chunk-7DVIJC6L.js";
64
66
  import {
65
67
  IntelligenceService,
66
68
  agentMemory,
@@ -178,7 +180,7 @@ import crypto28 from "crypto";
178
180
  import fs12 from "fs";
179
181
  import path14 from "path";
180
182
  import { fileURLToPath as fileURLToPath2 } from "url";
181
- import { eq as eq32 } from "drizzle-orm";
183
+ import { eq as eq33 } from "drizzle-orm";
182
184
  import Fastify from "fastify";
183
185
 
184
186
  // ../api-routes/src/auth.ts
@@ -2358,22 +2360,168 @@ async function intelligenceRoutes(app) {
2358
2360
  });
2359
2361
  }
2360
2362
 
2363
+ // ../api-routes/src/citations.ts
2364
+ import { eq as eq12, inArray as inArray3 } from "drizzle-orm";
2365
+ async function citationRoutes(app) {
2366
+ app.get("/projects/:name/citations/visibility", async (request, reply) => {
2367
+ const project = resolveProject(app.db, request.params.name);
2368
+ const configuredProviders = parseJsonColumn(project.providers, []);
2369
+ const projectKeywords = app.db.select().from(keywords).where(eq12(keywords.projectId, project.id)).all();
2370
+ if (projectKeywords.length === 0) {
2371
+ return reply.send(emptyCitationVisibility("no-keywords"));
2372
+ }
2373
+ const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(eq12(runs.projectId, project.id)).all();
2374
+ if (projectRuns.length === 0) {
2375
+ return reply.send(emptyCitationVisibility("no-runs-yet"));
2376
+ }
2377
+ const runCreatedAt = new Map(projectRuns.map((r) => [r.id, r.createdAt]));
2378
+ const rawSnapshots = app.db.select({
2379
+ id: querySnapshots.id,
2380
+ runId: querySnapshots.runId,
2381
+ keywordId: querySnapshots.keywordId,
2382
+ provider: querySnapshots.provider,
2383
+ citationState: querySnapshots.citationState,
2384
+ citedDomains: querySnapshots.citedDomains,
2385
+ competitorOverlap: querySnapshots.competitorOverlap,
2386
+ createdAt: querySnapshots.createdAt
2387
+ }).from(querySnapshots).where(inArray3(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
2388
+ if (rawSnapshots.length === 0) {
2389
+ return reply.send(emptyCitationVisibility("no-runs-yet"));
2390
+ }
2391
+ const snapshots = rawSnapshots.map((s) => ({
2392
+ ...s,
2393
+ runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
2394
+ }));
2395
+ const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq12(competitors.projectId, project.id)).all().map((c) => normalizeDomain(c.domain)).filter((d) => d.length > 0);
2396
+ const response = computeCitationVisibility({
2397
+ keywords: projectKeywords.map((k) => ({ id: k.id, keyword: k.keyword })),
2398
+ snapshots,
2399
+ configuredProviders,
2400
+ competitorDomains: projectCompetitors
2401
+ });
2402
+ return reply.send(response);
2403
+ });
2404
+ }
2405
+ function computeCitationVisibility(input) {
2406
+ const { keywords: kws, snapshots, configuredProviders, competitorDomains } = input;
2407
+ const latestByPair = /* @__PURE__ */ new Map();
2408
+ for (const snap of snapshots) {
2409
+ const key = `${snap.keywordId}::${snap.provider}`;
2410
+ const existing = latestByPair.get(key);
2411
+ if (!existing || snap.createdAt > existing.createdAt) {
2412
+ latestByPair.set(key, snap);
2413
+ }
2414
+ }
2415
+ const observedProviders = /* @__PURE__ */ new Set();
2416
+ for (const pair of latestByPair.values()) observedProviders.add(pair.provider);
2417
+ const providerUniverse = configuredProviders.length > 0 ? Array.from(new Set(configuredProviders)) : Array.from(observedProviders).sort();
2418
+ const byKeyword = [];
2419
+ const providersCitingTracker = /* @__PURE__ */ new Set();
2420
+ let keywordsCited = 0;
2421
+ let keywordsFullyCovered = 0;
2422
+ let keywordsUncovered = 0;
2423
+ for (const kw of kws) {
2424
+ const providers = [];
2425
+ let citedCount = 0;
2426
+ for (const provider of providerUniverse) {
2427
+ const snap = latestByPair.get(`${kw.id}::${provider}`);
2428
+ if (!snap) continue;
2429
+ const state = snap.citationState;
2430
+ const cited = citationStateToCited(state);
2431
+ if (cited) {
2432
+ citedCount++;
2433
+ providersCitingTracker.add(provider);
2434
+ }
2435
+ providers.push({
2436
+ provider,
2437
+ citationState: state,
2438
+ cited,
2439
+ runId: snap.runId,
2440
+ runCreatedAt: snap.runCreatedAt
2441
+ });
2442
+ }
2443
+ if (citedCount > 0) keywordsCited++;
2444
+ if (providerUniverse.length > 0 && citedCount === providerUniverse.length) keywordsFullyCovered++;
2445
+ if (providers.length > 0 && citedCount === 0) keywordsUncovered++;
2446
+ byKeyword.push({
2447
+ keywordId: kw.id,
2448
+ keyword: kw.keyword,
2449
+ providers,
2450
+ citedCount,
2451
+ totalProviders: providers.length
2452
+ });
2453
+ }
2454
+ const competitorSet = new Set(competitorDomains);
2455
+ const competitorGaps = [];
2456
+ const keywordById = new Map(kws.map((k) => [k.id, k.keyword]));
2457
+ for (const snap of latestByPair.values()) {
2458
+ if (citationStateToCited(snap.citationState)) continue;
2459
+ if (competitorSet.size === 0) continue;
2460
+ const cited = parseJsonColumn(snap.citedDomains, []);
2461
+ const overlap = parseJsonColumn(snap.competitorOverlap, []);
2462
+ const candidates = new Set(
2463
+ [...cited, ...overlap].map((d) => normalizeDomain(d)).filter((d) => d.length > 0)
2464
+ );
2465
+ const citingCompetitors = Array.from(candidates).filter((d) => competitorSet.has(d));
2466
+ if (citingCompetitors.length === 0) continue;
2467
+ competitorGaps.push({
2468
+ keywordId: snap.keywordId,
2469
+ keyword: keywordById.get(snap.keywordId) ?? "",
2470
+ provider: snap.provider,
2471
+ citingCompetitors: citingCompetitors.sort(),
2472
+ runId: snap.runId,
2473
+ runCreatedAt: snap.runCreatedAt
2474
+ });
2475
+ }
2476
+ competitorGaps.sort((a, b) => {
2477
+ if (a.keyword !== b.keyword) return a.keyword.localeCompare(b.keyword);
2478
+ return a.provider.localeCompare(b.provider);
2479
+ });
2480
+ let latestRunId = null;
2481
+ let latestRunAt = null;
2482
+ for (const snap of latestByPair.values()) {
2483
+ if (latestRunAt === null || snap.runCreatedAt > latestRunAt) {
2484
+ latestRunAt = snap.runCreatedAt;
2485
+ latestRunId = snap.runId;
2486
+ }
2487
+ }
2488
+ const summary = {
2489
+ providersConfigured: providerUniverse.length,
2490
+ providersCiting: providersCitingTracker.size,
2491
+ totalKeywords: kws.length,
2492
+ keywordsCited,
2493
+ keywordsFullyCovered,
2494
+ keywordsUncovered,
2495
+ latestRunId,
2496
+ latestRunAt
2497
+ };
2498
+ return {
2499
+ summary,
2500
+ byKeyword,
2501
+ competitorGaps,
2502
+ status: "ready"
2503
+ };
2504
+ }
2505
+ function normalizeDomain(domain) {
2506
+ return domain.toLowerCase().trim().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
2507
+ }
2508
+
2361
2509
  // ../api-routes/src/composites.ts
2362
- import { eq as eq12, and as and3, desc as desc5, sql as sql3, like, or as or2 } from "drizzle-orm";
2510
+ import { eq as eq13, and as and3, desc as desc5, sql as sql3, like, or as or2 } from "drizzle-orm";
2363
2511
  var TOP_INSIGHT_LIMIT = 5;
2364
2512
  var SEARCH_HIT_HARD_LIMIT = 50;
2365
2513
  var SEARCH_SNIPPET_RADIUS = 80;
2366
2514
  async function compositeRoutes(app) {
2367
2515
  app.get("/projects/:name/overview", async (request, reply) => {
2368
2516
  const project = resolveProject(app.db, request.params.name);
2369
- const totalRunsRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq12(runs.projectId, project.id)).get();
2517
+ const totalRunsRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq13(runs.projectId, project.id)).get();
2370
2518
  const totalRuns = totalRunsRow?.count ?? 0;
2371
- const recentRuns = app.db.select().from(runs).where(eq12(runs.projectId, project.id)).orderBy(desc5(runs.createdAt)).limit(2).all();
2519
+ const recentRuns = app.db.select().from(runs).where(eq13(runs.projectId, project.id)).orderBy(desc5(runs.createdAt)).limit(2).all();
2372
2520
  const [latestRunRow, previousRunRow] = recentRuns;
2373
2521
  const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
2374
- const healthRow = app.db.select().from(healthSnapshots).where(eq12(healthSnapshots.projectId, project.id)).orderBy(desc5(healthSnapshots.createdAt)).limit(1).get();
2522
+ const healthRow = app.db.select().from(healthSnapshots).where(eq13(healthSnapshots.projectId, project.id)).orderBy(desc5(healthSnapshots.createdAt)).limit(1).get();
2375
2523
  const health = healthRow ? mapHealthRow2(healthRow) : null;
2376
- const insightRows = app.db.select().from(insights).where(eq12(insights.projectId, project.id)).orderBy(desc5(insights.createdAt)).all();
2524
+ const insightRows = app.db.select().from(insights).where(eq13(insights.projectId, project.id)).orderBy(desc5(insights.createdAt)).all();
2377
2525
  const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
2378
2526
  const { keywordCounts, providers } = summarizeLatestRun(app, latestRunRow ?? null);
2379
2527
  const transitions = summarizeTransitions(app, latestRunRow ?? null, previousRunRow ?? null);
@@ -2409,9 +2557,9 @@ async function compositeRoutes(app) {
2409
2557
  citedDomains: querySnapshots.citedDomains,
2410
2558
  rawResponse: querySnapshots.rawResponse,
2411
2559
  createdAt: querySnapshots.createdAt
2412
- }).from(querySnapshots).innerJoin(keywords, eq12(querySnapshots.keywordId, keywords.id)).where(
2560
+ }).from(querySnapshots).innerJoin(keywords, eq13(querySnapshots.keywordId, keywords.id)).where(
2413
2561
  and3(
2414
- eq12(keywords.projectId, project.id),
2562
+ eq13(keywords.projectId, project.id),
2415
2563
  or2(
2416
2564
  sql3`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
2417
2565
  sql3`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
@@ -2422,7 +2570,7 @@ async function compositeRoutes(app) {
2422
2570
  ).orderBy(desc5(querySnapshots.createdAt)).limit(limit + 1).all();
2423
2571
  const insightMatches = app.db.select().from(insights).where(
2424
2572
  and3(
2425
- eq12(insights.projectId, project.id),
2573
+ eq13(insights.projectId, project.id),
2426
2574
  or2(
2427
2575
  like(insights.title, pattern),
2428
2576
  like(insights.keyword, pattern),
@@ -2485,7 +2633,7 @@ function summarizeLatestRun(app, run) {
2485
2633
  keywordId: querySnapshots.keywordId,
2486
2634
  provider: querySnapshots.provider,
2487
2635
  citationState: querySnapshots.citationState
2488
- }).from(querySnapshots).where(eq12(querySnapshots.runId, run.id)).all();
2636
+ }).from(querySnapshots).where(eq13(querySnapshots.runId, run.id)).all();
2489
2637
  if (rows.length === 0) return empty;
2490
2638
  const perKeyword = /* @__PURE__ */ new Map();
2491
2639
  const perProvider = /* @__PURE__ */ new Map();
@@ -2524,7 +2672,7 @@ function summarizeTransitions(app, latest, previous) {
2524
2672
  const rows = app.db.select({
2525
2673
  keywordId: querySnapshots.keywordId,
2526
2674
  citationState: querySnapshots.citationState
2527
- }).from(querySnapshots).where(eq12(querySnapshots.runId, runId)).all();
2675
+ }).from(querySnapshots).where(eq13(querySnapshots.runId, runId)).all();
2528
2676
  const map = /* @__PURE__ */ new Map();
2529
2677
  for (const row of rows) {
2530
2678
  const cited = row.citationState === "cited";
@@ -2681,16 +2829,16 @@ function makeSnippet(text, query) {
2681
2829
  }
2682
2830
 
2683
2831
  // ../api-routes/src/content-data.ts
2684
- import { and as and4, eq as eq13, desc as desc6, inArray as inArray3 } from "drizzle-orm";
2832
+ import { and as and4, eq as eq14, desc as desc6, inArray as inArray4 } from "drizzle-orm";
2685
2833
  var RECENT_RUNS_WINDOW = 5;
2686
2834
  function loadOrchestratorInput(db, project) {
2687
2835
  const projectId = project.id;
2688
- const ownDomain = normalizeDomain(project.canonicalDomain);
2836
+ const ownDomain = normalizeDomain2(project.canonicalDomain);
2689
2837
  const ownedDomains = parseJsonColumn(project.ownedDomains, []);
2690
- const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain)]);
2838
+ const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain2)]);
2691
2839
  const trackedKeywords = listKeywords(db, projectId);
2692
2840
  const candidateQueryStrings = trackedKeywords.filter(isBlogShapedQuery);
2693
- const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain);
2841
+ const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain2);
2694
2842
  const competitorSet = new Set(trackedCompetitors);
2695
2843
  const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW);
2696
2844
  const latestRunId = recentRunIds[0] ?? "";
@@ -2727,43 +2875,43 @@ function loadOrchestratorInput(db, project) {
2727
2875
  };
2728
2876
  }
2729
2877
  function listKeywords(db, projectId) {
2730
- const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq13(keywords.projectId, projectId)).all();
2878
+ const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq14(keywords.projectId, projectId)).all();
2731
2879
  return rows.map((r) => r.text);
2732
2880
  }
2733
2881
  function listCompetitorDomains(db, projectId) {
2734
- const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all();
2882
+ const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, projectId)).all();
2735
2883
  return rows.map((r) => r.domain);
2736
2884
  }
2737
2885
  function listRecentAnswerVisibilityRunIds(db, projectId, limit) {
2738
2886
  const rows = db.select({ id: runs.id }).from(runs).where(
2739
2887
  and4(
2740
- eq13(runs.projectId, projectId),
2741
- eq13(runs.kind, RunKinds["answer-visibility"]),
2888
+ eq14(runs.projectId, projectId),
2889
+ eq14(runs.kind, RunKinds["answer-visibility"]),
2742
2890
  // Queued/running/failed/cancelled runs may have partial or no
2743
2891
  // snapshots; including them risks pointing latestRunId at a run with
2744
2892
  // no usable evidence.
2745
- inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
2893
+ inArray4(runs.status, [RunStatuses.completed, RunStatuses.partial])
2746
2894
  )
2747
2895
  ).orderBy(desc6(runs.createdAt)).limit(limit).all();
2748
2896
  return rows.map((r) => r.id);
2749
2897
  }
2750
2898
  function lookupRunTimestamp(db, runId) {
2751
- const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq13(runs.id, runId)).get();
2899
+ const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq14(runs.id, runId)).get();
2752
2900
  return row?.createdAt ?? "";
2753
2901
  }
2754
2902
  function listGscPagesForProject(db, projectId) {
2755
- const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
2903
+ const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq14(gscSearchData.projectId, projectId)).all();
2756
2904
  return rows.map((r) => r.page);
2757
2905
  }
2758
2906
  function listGa4LandingPagesForProject(db, projectId) {
2759
- const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
2907
+ const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq14(gaTrafficSnapshots.projectId, projectId)).all();
2760
2908
  return rows.map((r) => r.landingPage);
2761
2909
  }
2762
2910
  function buildGaTrafficByPage(db, projectId) {
2763
2911
  const rows = db.select({
2764
2912
  landingPage: gaTrafficSnapshots.landingPage,
2765
2913
  sessions: gaTrafficSnapshots.sessions
2766
- }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
2914
+ }).from(gaTrafficSnapshots).where(eq14(gaTrafficSnapshots.projectId, projectId)).all();
2767
2915
  const map = /* @__PURE__ */ new Map();
2768
2916
  for (const row of rows) {
2769
2917
  const path15 = extractPath(row.landingPage);
@@ -2773,24 +2921,24 @@ function buildGaTrafficByPage(db, projectId) {
2773
2921
  return map;
2774
2922
  }
2775
2923
  function sumAiReferralSessions(db, projectId) {
2776
- const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq13(gaAiReferrals.projectId, projectId)).all();
2924
+ const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq14(gaAiReferrals.projectId, projectId)).all();
2777
2925
  return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
2778
2926
  }
2779
2927
  function buildCandidateQueries(opts) {
2780
2928
  if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
2781
2929
  return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
2782
2930
  }
2783
- const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq13(keywords.projectId, opts.projectId)).all();
2931
+ const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq14(keywords.projectId, opts.projectId)).all();
2784
2932
  const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
2785
2933
  const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
2786
- const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
2934
+ const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
2787
2935
  const snapshotsByKeyword = /* @__PURE__ */ new Map();
2788
2936
  for (const row of snapshotRows) {
2789
2937
  const list = snapshotsByKeyword.get(row.keywordId) ?? [];
2790
2938
  list.push(row);
2791
2939
  snapshotsByKeyword.set(row.keywordId, list);
2792
2940
  }
2793
- const gscRows = opts.db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, opts.projectId)).all();
2941
+ const gscRows = opts.db.select().from(gscSearchData).where(eq14(gscSearchData.projectId, opts.projectId)).all();
2794
2942
  const gscByQuery = aggregateGscByQuery(gscRows);
2795
2943
  return opts.candidateQueryStrings.map((query) => {
2796
2944
  const keywordId = keywordIdByText.get(query);
@@ -2854,13 +3002,13 @@ function aggregateCandidate(opts) {
2854
3002
  const isLatestRun = snap.runId === opts.latestRunId;
2855
3003
  const competitorOverlap = parseJsonColumn(snap.competitorOverlap, []);
2856
3004
  for (const domain of competitorOverlap) {
2857
- const normalized = normalizeDomain(domain);
3005
+ const normalized = normalizeDomain2(domain);
2858
3006
  if (!opts.competitorSet.has(normalized)) continue;
2859
3007
  competitorTally.set(normalized, (competitorTally.get(normalized) ?? 0) + 1);
2860
3008
  }
2861
3009
  const grounding = extractGroundingSources(snap.rawResponse);
2862
3010
  for (const g of grounding) {
2863
- const domain = normalizeDomain(extractHostFromUri(g.uri));
3011
+ const domain = normalizeDomain2(extractHostFromUri(g.uri));
2864
3012
  if (!domain) continue;
2865
3013
  if (opts.ourDomains.has(domain)) {
2866
3014
  if (isLatestRun) ourCitedInLatestRun = true;
@@ -2946,7 +3094,7 @@ function extractHostFromUri(uri) {
2946
3094
  return "";
2947
3095
  }
2948
3096
  }
2949
- function normalizeDomain(domain) {
3097
+ function normalizeDomain2(domain) {
2950
3098
  return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
2951
3099
  }
2952
3100
  function extractPath(url) {
@@ -5237,6 +5385,18 @@ var routeCatalog = [
5237
5385
  404: { description: "Project not found." }
5238
5386
  }
5239
5387
  },
5388
+ {
5389
+ method: "get",
5390
+ path: "/api/v1/projects/{name}/citations/visibility",
5391
+ summary: "Citation visibility headline (cited by N of M engines)",
5392
+ description: 'Single-call read for the AI citation surface. Returns project headline (`providersConfigured`/`providersCiting`/keyword coverage counts), per-keyword engine coverage rows from the latest snapshot per (keyword \xD7 provider), and a competitor-gap list (keywords where the project is not cited but a configured competitor is). Status `no-data` with `reason: "no-runs-yet"` or `"no-keywords"` when the project lacks the inputs.',
5393
+ tags: ["intelligence"],
5394
+ parameters: [nameParameter],
5395
+ responses: {
5396
+ 200: { description: "Citation visibility report or no-data sentinel returned." },
5397
+ 404: { description: "Project not found." }
5398
+ }
5399
+ },
5240
5400
  // Content opportunity engine
5241
5401
  {
5242
5402
  method: "get",
@@ -5547,6 +5707,73 @@ var canonryLocalRouteCatalog = [
5547
5707
  404: { description: "Project not found." }
5548
5708
  }
5549
5709
  },
5710
+ {
5711
+ method: "get",
5712
+ path: "/api/v1/projects/{name}/agent/memory",
5713
+ summary: "List durable Aero memory entries for a project",
5714
+ description: "Returns the project-scoped agent_memory rows newest-first. Includes both operator-authored notes (source `user`/`aero`) and LLM-authored compaction summaries (source `compaction`, key prefix `compaction:`). The N most-recent rows are also injected into the system prompt at every new session start.",
5715
+ tags: ["agent"],
5716
+ parameters: [nameParameter],
5717
+ responses: {
5718
+ 200: { description: "Memory entries returned." },
5719
+ 404: { description: "Project not found." }
5720
+ }
5721
+ },
5722
+ {
5723
+ method: "put",
5724
+ path: "/api/v1/projects/{name}/agent/memory",
5725
+ summary: "Upsert a durable Aero memory entry",
5726
+ description: "Creates or replaces a project-scoped note (max 2 KB, max 128-char key). Same key replaces the prior value. Keys with the reserved `compaction:` prefix are rejected \u2014 that namespace is owned by transcript compaction.",
5727
+ tags: ["agent"],
5728
+ parameters: [nameParameter],
5729
+ requestBody: {
5730
+ required: true,
5731
+ content: {
5732
+ "application/json": {
5733
+ schema: {
5734
+ type: "object",
5735
+ required: ["key", "value"],
5736
+ properties: {
5737
+ key: { type: "string", description: "Stable identifier for this note (max 128 chars)." },
5738
+ value: { type: "string", description: "Plain-text note body (max 2 KB)." }
5739
+ }
5740
+ }
5741
+ }
5742
+ }
5743
+ },
5744
+ responses: {
5745
+ 200: { description: "Entry upserted." },
5746
+ 400: { description: "Validation failed (key length, value size, reserved prefix)." },
5747
+ 404: { description: "Project not found." }
5748
+ }
5749
+ },
5750
+ {
5751
+ method: "delete",
5752
+ path: "/api/v1/projects/{name}/agent/memory",
5753
+ summary: "Delete a durable Aero memory entry",
5754
+ description: "Removes a single project-scoped note by key. Returns `status: missing` (non-error) when the key never existed. Keys with the reserved `compaction:` prefix are rejected \u2014 those notes are pruned automatically.",
5755
+ tags: ["agent"],
5756
+ parameters: [nameParameter],
5757
+ requestBody: {
5758
+ required: true,
5759
+ content: {
5760
+ "application/json": {
5761
+ schema: {
5762
+ type: "object",
5763
+ required: ["key"],
5764
+ properties: {
5765
+ key: { type: "string", description: "Exact key of the note to remove." }
5766
+ }
5767
+ }
5768
+ }
5769
+ }
5770
+ },
5771
+ responses: {
5772
+ 200: { description: "Entry removed or already absent." },
5773
+ 400: { description: "Validation failed (reserved prefix)." },
5774
+ 404: { description: "Project not found." }
5775
+ }
5776
+ },
5550
5777
  {
5551
5778
  method: "get",
5552
5779
  path: "/api/v1/projects/{name}/agent/providers",
@@ -5814,7 +6041,7 @@ async function telemetryRoutes(app, opts) {
5814
6041
 
5815
6042
  // ../api-routes/src/schedules.ts
5816
6043
  import crypto11 from "crypto";
5817
- import { eq as eq14 } from "drizzle-orm";
6044
+ import { eq as eq15 } from "drizzle-orm";
5818
6045
  async function scheduleRoutes(app, opts) {
5819
6046
  app.put("/projects/:name/schedule", async (request, reply) => {
5820
6047
  const project = resolveProject(app.db, request.params.name);
@@ -5857,7 +6084,7 @@ async function scheduleRoutes(app, opts) {
5857
6084
  }
5858
6085
  const now = (/* @__PURE__ */ new Date()).toISOString();
5859
6086
  const enabledInt = enabled === false ? 0 : 1;
5860
- const existing = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
6087
+ const existing = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
5861
6088
  if (existing) {
5862
6089
  app.db.update(schedules).set({
5863
6090
  cronExpr,
@@ -5866,7 +6093,7 @@ async function scheduleRoutes(app, opts) {
5866
6093
  providers: JSON.stringify(providers),
5867
6094
  enabled: enabledInt,
5868
6095
  updatedAt: now
5869
- }).where(eq14(schedules.id, existing.id)).run();
6096
+ }).where(eq15(schedules.id, existing.id)).run();
5870
6097
  } else {
5871
6098
  app.db.insert(schedules).values({
5872
6099
  id: crypto11.randomUUID(),
@@ -5888,12 +6115,12 @@ async function scheduleRoutes(app, opts) {
5888
6115
  diff: { cronExpr, preset, timezone, providers }
5889
6116
  });
5890
6117
  opts.onScheduleUpdated?.("upsert", project.id);
5891
- const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
6118
+ const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
5892
6119
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
5893
6120
  });
5894
6121
  app.get("/projects/:name/schedule", async (request, reply) => {
5895
6122
  const project = resolveProject(app.db, request.params.name);
5896
- const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
6123
+ const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
5897
6124
  if (!schedule) {
5898
6125
  throw notFound("Schedule", request.params.name);
5899
6126
  }
@@ -5901,11 +6128,11 @@ async function scheduleRoutes(app, opts) {
5901
6128
  });
5902
6129
  app.delete("/projects/:name/schedule", async (request, reply) => {
5903
6130
  const project = resolveProject(app.db, request.params.name);
5904
- const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
6131
+ const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
5905
6132
  if (!schedule) {
5906
6133
  throw notFound("Schedule", request.params.name);
5907
6134
  }
5908
- app.db.delete(schedules).where(eq14(schedules.id, schedule.id)).run();
6135
+ app.db.delete(schedules).where(eq15(schedules.id, schedule.id)).run();
5909
6136
  writeAuditLog(app.db, {
5910
6137
  projectId: project.id,
5911
6138
  actor: "api",
@@ -5935,7 +6162,7 @@ function formatSchedule(row) {
5935
6162
 
5936
6163
  // ../api-routes/src/notifications.ts
5937
6164
  import crypto12 from "crypto";
5938
- import { eq as eq15 } from "drizzle-orm";
6165
+ import { eq as eq16 } from "drizzle-orm";
5939
6166
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
5940
6167
  async function notificationRoutes(app) {
5941
6168
  app.get("/notifications/events", async (_request, reply) => {
@@ -5974,22 +6201,22 @@ async function notificationRoutes(app) {
5974
6201
  diff: { channel, ...redactNotificationUrl(url), events }
5975
6202
  });
5976
6203
  return reply.status(201).send({
5977
- ...formatNotification(app.db.select().from(notifications).where(eq15(notifications.id, id)).get()),
6204
+ ...formatNotification(app.db.select().from(notifications).where(eq16(notifications.id, id)).get()),
5978
6205
  webhookSecret
5979
6206
  });
5980
6207
  });
5981
6208
  app.get("/projects/:name/notifications", async (request, reply) => {
5982
6209
  const project = resolveProject(app.db, request.params.name);
5983
- const rows = app.db.select().from(notifications).where(eq15(notifications.projectId, project.id)).all();
6210
+ const rows = app.db.select().from(notifications).where(eq16(notifications.projectId, project.id)).all();
5984
6211
  return reply.send(rows.map(formatNotification));
5985
6212
  });
5986
6213
  app.delete("/projects/:name/notifications/:id", async (request, reply) => {
5987
6214
  const project = resolveProject(app.db, request.params.name);
5988
- const notification = app.db.select().from(notifications).where(eq15(notifications.id, request.params.id)).get();
6215
+ const notification = app.db.select().from(notifications).where(eq16(notifications.id, request.params.id)).get();
5989
6216
  if (!notification || notification.projectId !== project.id) {
5990
6217
  throw notFound("Notification", request.params.id);
5991
6218
  }
5992
- app.db.delete(notifications).where(eq15(notifications.id, notification.id)).run();
6219
+ app.db.delete(notifications).where(eq16(notifications.id, notification.id)).run();
5993
6220
  writeAuditLog(app.db, {
5994
6221
  projectId: project.id,
5995
6222
  actor: "api",
@@ -6001,7 +6228,7 @@ async function notificationRoutes(app) {
6001
6228
  });
6002
6229
  app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
6003
6230
  const project = resolveProject(app.db, request.params.name);
6004
- const notification = app.db.select().from(notifications).where(eq15(notifications.id, request.params.id)).get();
6231
+ const notification = app.db.select().from(notifications).where(eq16(notifications.id, request.params.id)).get();
6005
6232
  if (!notification || notification.projectId !== project.id) {
6006
6233
  throw notFound("Notification", request.params.id);
6007
6234
  }
@@ -6054,7 +6281,7 @@ function formatNotification(row) {
6054
6281
 
6055
6282
  // ../api-routes/src/google.ts
6056
6283
  import crypto14 from "crypto";
6057
- import { eq as eq16, and as and5, desc as desc7, sql as sql4 } from "drizzle-orm";
6284
+ import { eq as eq17, and as and5, desc as desc7, sql as sql4 } from "drizzle-orm";
6058
6285
 
6059
6286
  // ../integration-google/src/constants.ts
6060
6287
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -7189,14 +7416,14 @@ async function googleRoutes(app, opts) {
7189
7416
  if (opts.onGscSyncRequested) {
7190
7417
  opts.onGscSyncRequested(runId, project.id, { days, full });
7191
7418
  }
7192
- const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
7419
+ const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
7193
7420
  return run;
7194
7421
  });
7195
7422
  app.get("/projects/:name/google/gsc/performance", async (request) => {
7196
7423
  const project = resolveProject(app.db, request.params.name);
7197
7424
  const { startDate, endDate, query, page, limit } = request.query;
7198
7425
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
7199
- const conditions = [eq16(gscSearchData.projectId, project.id)];
7426
+ const conditions = [eq17(gscSearchData.projectId, project.id)];
7200
7427
  if (startDate) conditions.push(sql4`${gscSearchData.date} >= ${startDate}`);
7201
7428
  else if (cutoffDate) conditions.push(sql4`${gscSearchData.date} >= ${cutoffDate}`);
7202
7429
  if (endDate) conditions.push(sql4`${gscSearchData.date} <= ${endDate}`);
@@ -7274,8 +7501,8 @@ async function googleRoutes(app, opts) {
7274
7501
  app.get("/projects/:name/google/gsc/inspections", async (request) => {
7275
7502
  const project = resolveProject(app.db, request.params.name);
7276
7503
  const { url, limit } = request.query;
7277
- const conditions = [eq16(gscUrlInspections.projectId, project.id)];
7278
- if (url) conditions.push(eq16(gscUrlInspections.url, url));
7504
+ const conditions = [eq17(gscUrlInspections.projectId, project.id)];
7505
+ if (url) conditions.push(eq17(gscUrlInspections.url, url));
7279
7506
  const rows = app.db.select().from(gscUrlInspections).where(and5(...conditions)).orderBy(desc7(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
7280
7507
  return rows.map((r) => ({
7281
7508
  id: r.id,
@@ -7295,7 +7522,7 @@ async function googleRoutes(app, opts) {
7295
7522
  });
7296
7523
  app.get("/projects/:name/google/gsc/deindexed", async (request) => {
7297
7524
  const project = resolveProject(app.db, request.params.name);
7298
- const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7525
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7299
7526
  const byUrl = /* @__PURE__ */ new Map();
7300
7527
  for (const row of allInspections) {
7301
7528
  const existing = byUrl.get(row.url);
@@ -7323,7 +7550,7 @@ async function googleRoutes(app, opts) {
7323
7550
  });
7324
7551
  app.get("/projects/:name/google/gsc/coverage", async (request) => {
7325
7552
  const project = resolveProject(app.db, request.params.name);
7326
- const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7553
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7327
7554
  const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
7328
7555
  const latestByUrl = /* @__PURE__ */ new Map();
7329
7556
  const historyByUrl = /* @__PURE__ */ new Map();
@@ -7420,7 +7647,7 @@ async function googleRoutes(app, opts) {
7420
7647
  const project = resolveProject(app.db, request.params.name);
7421
7648
  const parsed = parseInt(request.query.limit ?? "90", 10);
7422
7649
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
7423
- const rows = app.db.select().from(gscCoverageSnapshots).where(eq16(gscCoverageSnapshots.projectId, project.id)).orderBy(desc7(gscCoverageSnapshots.date)).limit(limit).all();
7650
+ const rows = app.db.select().from(gscCoverageSnapshots).where(eq17(gscCoverageSnapshots.projectId, project.id)).orderBy(desc7(gscCoverageSnapshots.date)).limit(limit).all();
7424
7651
  return rows.map((r) => ({
7425
7652
  date: r.date,
7426
7653
  indexed: r.indexed,
@@ -7480,7 +7707,7 @@ async function googleRoutes(app, opts) {
7480
7707
  if (opts.onInspectSitemapRequested) {
7481
7708
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
7482
7709
  }
7483
- const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
7710
+ const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
7484
7711
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
7485
7712
  });
7486
7713
  app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
@@ -7507,7 +7734,7 @@ async function googleRoutes(app, opts) {
7507
7734
  if (opts.onInspectSitemapRequested) {
7508
7735
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
7509
7736
  }
7510
- const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
7737
+ const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
7511
7738
  return run;
7512
7739
  });
7513
7740
  app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
@@ -7554,7 +7781,7 @@ async function googleRoutes(app, opts) {
7554
7781
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7555
7782
  let urlsToNotify = request.body?.urls ?? [];
7556
7783
  if (request.body?.allUnindexed) {
7557
- const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7784
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7558
7785
  const latestByUrl = /* @__PURE__ */ new Map();
7559
7786
  for (const row of allInspections) {
7560
7787
  if (!latestByUrl.has(row.url)) {
@@ -7625,7 +7852,7 @@ async function googleRoutes(app, opts) {
7625
7852
 
7626
7853
  // ../api-routes/src/bing.ts
7627
7854
  import crypto15 from "crypto";
7628
- import { eq as eq17, and as and6, desc as desc8 } from "drizzle-orm";
7855
+ import { eq as eq18, and as and6, desc as desc8 } from "drizzle-orm";
7629
7856
 
7630
7857
  // ../integration-bing/src/constants.ts
7631
7858
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -7938,7 +8165,7 @@ async function bingRoutes(app, opts) {
7938
8165
  const store = requireConnectionStore();
7939
8166
  const project = resolveProject(app.db, request.params.name);
7940
8167
  requireConnection(store, project.canonicalDomain);
7941
- const allInspections = app.db.select().from(bingUrlInspections).where(eq17(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
8168
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq18(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
7942
8169
  const latestByUrl = /* @__PURE__ */ new Map();
7943
8170
  const definitiveByUrl = /* @__PURE__ */ new Map();
7944
8171
  for (const row of allInspections) {
@@ -8027,7 +8254,7 @@ async function bingRoutes(app, opts) {
8027
8254
  const project = resolveProject(app.db, request.params.name);
8028
8255
  const parsed = parseInt(request.query.limit ?? "90", 10);
8029
8256
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
8030
- const rows = app.db.select().from(bingCoverageSnapshots).where(eq17(bingCoverageSnapshots.projectId, project.id)).orderBy(desc8(bingCoverageSnapshots.date)).limit(limit).all();
8257
+ const rows = app.db.select().from(bingCoverageSnapshots).where(eq18(bingCoverageSnapshots.projectId, project.id)).orderBy(desc8(bingCoverageSnapshots.date)).limit(limit).all();
8031
8258
  return rows.map((r) => ({
8032
8259
  date: r.date,
8033
8260
  indexed: r.indexed,
@@ -8039,7 +8266,7 @@ async function bingRoutes(app, opts) {
8039
8266
  requireConnectionStore();
8040
8267
  const project = resolveProject(app.db, request.params.name);
8041
8268
  const { url, limit } = request.query;
8042
- const whereClause = url ? and6(eq17(bingUrlInspections.projectId, project.id), eq17(bingUrlInspections.url, url)) : eq17(bingUrlInspections.projectId, project.id);
8269
+ const whereClause = url ? and6(eq18(bingUrlInspections.projectId, project.id), eq18(bingUrlInspections.url, url)) : eq18(bingUrlInspections.projectId, project.id);
8043
8270
  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();
8044
8271
  return filtered.map((r) => ({
8045
8272
  id: r.id,
@@ -8129,7 +8356,7 @@ async function bingRoutes(app, opts) {
8129
8356
  anchorCount: result.AnchorCount ?? null,
8130
8357
  discoveryDate
8131
8358
  }).run();
8132
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq17(runs.id, runId)).run();
8359
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq18(runs.id, runId)).run();
8133
8360
  return {
8134
8361
  id,
8135
8362
  url,
@@ -8145,7 +8372,7 @@ async function bingRoutes(app, opts) {
8145
8372
  } catch (e) {
8146
8373
  const msg = e instanceof Error ? e.message : String(e);
8147
8374
  bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
8148
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq17(runs.id, runId)).run();
8375
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
8149
8376
  throw e;
8150
8377
  }
8151
8378
  });
@@ -8172,7 +8399,7 @@ async function bingRoutes(app, opts) {
8172
8399
  } else {
8173
8400
  bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
8174
8401
  }
8175
- const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
8402
+ const run = app.db.select().from(runs).where(eq18(runs.id, runId)).get();
8176
8403
  return run;
8177
8404
  });
8178
8405
  app.post("/projects/:name/bing/request-indexing", async (request) => {
@@ -8184,7 +8411,7 @@ async function bingRoutes(app, opts) {
8184
8411
  }
8185
8412
  let urlsToSubmit = request.body?.urls ?? [];
8186
8413
  if (request.body?.allUnindexed) {
8187
- const allInspections = app.db.select().from(bingUrlInspections).where(eq17(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
8414
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq18(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
8188
8415
  const latestByUrl = /* @__PURE__ */ new Map();
8189
8416
  for (const row of allInspections) {
8190
8417
  if (!latestByUrl.has(row.url)) {
@@ -8271,14 +8498,14 @@ async function bingRoutes(app, opts) {
8271
8498
  import fs from "fs";
8272
8499
  import path from "path";
8273
8500
  import os from "os";
8274
- import { eq as eq18, and as and7 } from "drizzle-orm";
8501
+ import { eq as eq19, and as and7 } from "drizzle-orm";
8275
8502
  function getScreenshotDir() {
8276
8503
  return path.join(os.homedir(), ".canonry", "screenshots");
8277
8504
  }
8278
8505
  async function cdpRoutes(app, opts) {
8279
8506
  app.get("/screenshots/:snapshotId", async (request, reply) => {
8280
8507
  const { snapshotId } = request.params;
8281
- const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq18(querySnapshots.id, snapshotId)).get();
8508
+ const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq19(querySnapshots.id, snapshotId)).get();
8282
8509
  if (!snapshot?.screenshotPath) {
8283
8510
  const err = notFound("Screenshot", snapshotId);
8284
8511
  return reply.code(err.statusCode).send(err.toJSON());
@@ -8344,7 +8571,7 @@ async function cdpRoutes(app, opts) {
8344
8571
  async (request, reply) => {
8345
8572
  const project = resolveProject(app.db, request.params.name);
8346
8573
  const { runId } = request.params;
8347
- const run = app.db.select().from(runs).where(and7(eq18(runs.id, runId), eq18(runs.projectId, project.id))).get();
8574
+ const run = app.db.select().from(runs).where(and7(eq19(runs.id, runId), eq19(runs.projectId, project.id))).get();
8348
8575
  if (!run) {
8349
8576
  const err = notFound("Run", runId);
8350
8577
  return reply.code(err.statusCode).send(err.toJSON());
@@ -8357,8 +8584,8 @@ async function cdpRoutes(app, opts) {
8357
8584
  citedDomains: querySnapshots.citedDomains,
8358
8585
  screenshotPath: querySnapshots.screenshotPath,
8359
8586
  rawResponse: querySnapshots.rawResponse
8360
- }).from(querySnapshots).where(eq18(querySnapshots.runId, runId)).all();
8361
- const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq18(keywords.projectId, project.id)).all();
8587
+ }).from(querySnapshots).where(eq19(querySnapshots.runId, runId)).all();
8588
+ const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq19(keywords.projectId, project.id)).all();
8362
8589
  const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
8363
8590
  const byKeyword = /* @__PURE__ */ new Map();
8364
8591
  for (const snap of snapshots) {
@@ -8441,7 +8668,7 @@ async function cdpRoutes(app, opts) {
8441
8668
 
8442
8669
  // ../api-routes/src/ga.ts
8443
8670
  import crypto16 from "crypto";
8444
- import { eq as eq19, desc as desc9, and as and8, sql as sql5 } from "drizzle-orm";
8671
+ import { eq as eq20, desc as desc9, and as and8, sql as sql5 } from "drizzle-orm";
8445
8672
  function gaLog(level, action, ctx) {
8446
8673
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
8447
8674
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -8598,10 +8825,10 @@ async function ga4Routes(app, opts) {
8598
8825
  if (!saConn && !oauthConn) {
8599
8826
  throw notFound("GA4 connection", project.name);
8600
8827
  }
8601
- app.db.delete(gaTrafficSnapshots).where(eq19(gaTrafficSnapshots.projectId, project.id)).run();
8602
- app.db.delete(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).run();
8603
- app.db.delete(gaAiReferrals).where(eq19(gaAiReferrals.projectId, project.id)).run();
8604
- app.db.delete(gaSocialReferrals).where(eq19(gaSocialReferrals.projectId, project.id)).run();
8828
+ app.db.delete(gaTrafficSnapshots).where(eq20(gaTrafficSnapshots.projectId, project.id)).run();
8829
+ app.db.delete(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).run();
8830
+ app.db.delete(gaAiReferrals).where(eq20(gaAiReferrals.projectId, project.id)).run();
8831
+ app.db.delete(gaSocialReferrals).where(eq20(gaSocialReferrals.projectId, project.id)).run();
8605
8832
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
8606
8833
  opts.ga4CredentialStore?.deleteConnection(project.name);
8607
8834
  opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
@@ -8622,7 +8849,7 @@ async function ga4Routes(app, opts) {
8622
8849
  if (!connected) {
8623
8850
  return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
8624
8851
  }
8625
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
8852
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
8626
8853
  return {
8627
8854
  connected: true,
8628
8855
  propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
@@ -8682,7 +8909,7 @@ async function ga4Routes(app, opts) {
8682
8909
  if (syncTraffic) {
8683
8910
  tx.delete(gaTrafficSnapshots).where(
8684
8911
  and8(
8685
- eq19(gaTrafficSnapshots.projectId, project.id),
8912
+ eq20(gaTrafficSnapshots.projectId, project.id),
8686
8913
  sql5`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
8687
8914
  sql5`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
8688
8915
  )
@@ -8706,7 +8933,7 @@ async function ga4Routes(app, opts) {
8706
8933
  if (syncAi) {
8707
8934
  tx.delete(gaAiReferrals).where(
8708
8935
  and8(
8709
- eq19(gaAiReferrals.projectId, project.id),
8936
+ eq20(gaAiReferrals.projectId, project.id),
8710
8937
  sql5`${gaAiReferrals.date} >= ${summary.periodStart}`,
8711
8938
  sql5`${gaAiReferrals.date} <= ${summary.periodEnd}`
8712
8939
  )
@@ -8729,7 +8956,7 @@ async function ga4Routes(app, opts) {
8729
8956
  if (syncSocial) {
8730
8957
  tx.delete(gaSocialReferrals).where(
8731
8958
  and8(
8732
- eq19(gaSocialReferrals.projectId, project.id),
8959
+ eq20(gaSocialReferrals.projectId, project.id),
8733
8960
  sql5`${gaSocialReferrals.date} >= ${summary.periodStart}`,
8734
8961
  sql5`${gaSocialReferrals.date} <= ${summary.periodEnd}`
8735
8962
  )
@@ -8750,7 +8977,7 @@ async function ga4Routes(app, opts) {
8750
8977
  }
8751
8978
  }
8752
8979
  if (syncSummary) {
8753
- tx.delete(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).run();
8980
+ tx.delete(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).run();
8754
8981
  tx.insert(gaTrafficSummaries).values({
8755
8982
  id: crypto16.randomUUID(),
8756
8983
  projectId: project.id,
@@ -8764,7 +8991,7 @@ async function ga4Routes(app, opts) {
8764
8991
  }).run();
8765
8992
  }
8766
8993
  });
8767
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq19(runs.id, runId)).run();
8994
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq20(runs.id, runId)).run();
8768
8995
  const syncedComponents = only ? [only, ...only !== "social" && only !== "ai" && only !== "traffic" ? [] : []] : void 0;
8769
8996
  gaLog("info", "sync.complete", {
8770
8997
  projectId: project.id,
@@ -8788,7 +9015,7 @@ async function ga4Routes(app, opts) {
8788
9015
  } catch (e) {
8789
9016
  const msg = e instanceof Error ? e.message : String(e);
8790
9017
  gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
8791
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
9018
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
8792
9019
  throw e;
8793
9020
  }
8794
9021
  });
@@ -8799,11 +9026,11 @@ async function ga4Routes(app, opts) {
8799
9026
  const window = parseWindow(request.query.window);
8800
9027
  const cutoff = windowCutoff(window);
8801
9028
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
8802
- const snapshotConditions = [eq19(gaTrafficSnapshots.projectId, project.id)];
9029
+ const snapshotConditions = [eq20(gaTrafficSnapshots.projectId, project.id)];
8803
9030
  if (cutoffDate) snapshotConditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
8804
- const aiConditions = [eq19(gaAiReferrals.projectId, project.id)];
9031
+ const aiConditions = [eq20(gaAiReferrals.projectId, project.id)];
8805
9032
  if (cutoffDate) aiConditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
8806
- const socialConditions = [eq19(gaSocialReferrals.projectId, project.id)];
9033
+ const socialConditions = [eq20(gaSocialReferrals.projectId, project.id)];
8807
9034
  if (cutoffDate) socialConditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
8808
9035
  const summaryRow = cutoffDate ? app.db.select({
8809
9036
  totalSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
@@ -8813,14 +9040,14 @@ async function ga4Routes(app, opts) {
8813
9040
  totalSessions: gaTrafficSummaries.totalSessions,
8814
9041
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
8815
9042
  totalUsers: gaTrafficSummaries.totalUsers
8816
- }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
9043
+ }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).get();
8817
9044
  const directTotalRow = app.db.select({
8818
9045
  totalDirectSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
8819
9046
  }).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).get();
8820
9047
  const summaryMeta = app.db.select({
8821
9048
  periodStart: gaTrafficSummaries.periodStart,
8822
9049
  periodEnd: gaTrafficSummaries.periodEnd
8823
- }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
9050
+ }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).get();
8824
9051
  const rows = app.db.select({
8825
9052
  landingPage: sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
8826
9053
  sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
@@ -8851,7 +9078,7 @@ async function ga4Routes(app, opts) {
8851
9078
  const aiBySession = app.db.select({
8852
9079
  sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
8853
9080
  users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
8854
- }).from(gaAiReferrals).where(and8(...aiConditions, eq19(gaAiReferrals.sourceDimension, "session"))).get();
9081
+ }).from(gaAiReferrals).where(and8(...aiConditions, eq20(gaAiReferrals.sourceDimension, "session"))).get();
8855
9082
  const socialReferrals = app.db.select({
8856
9083
  source: gaSocialReferrals.source,
8857
9084
  medium: gaSocialReferrals.medium,
@@ -8863,7 +9090,7 @@ async function ga4Routes(app, opts) {
8863
9090
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
8864
9091
  users: sql5`SUM(${gaSocialReferrals.users})`
8865
9092
  }).from(gaSocialReferrals).where(and8(...socialConditions)).get();
8866
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
9093
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
8867
9094
  const total = summaryRow?.totalSessions ?? 0;
8868
9095
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
8869
9096
  return {
@@ -8917,7 +9144,7 @@ async function ga4Routes(app, opts) {
8917
9144
  const project = resolveProject(app.db, request.params.name);
8918
9145
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8919
9146
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8920
- const conditions = [eq19(gaAiReferrals.projectId, project.id)];
9147
+ const conditions = [eq20(gaAiReferrals.projectId, project.id)];
8921
9148
  if (cutoffDate) conditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
8922
9149
  const rows = app.db.select({
8923
9150
  date: gaAiReferrals.date,
@@ -8933,7 +9160,7 @@ async function ga4Routes(app, opts) {
8933
9160
  const project = resolveProject(app.db, request.params.name);
8934
9161
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8935
9162
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8936
- const conditions = [eq19(gaSocialReferrals.projectId, project.id)];
9163
+ const conditions = [eq20(gaSocialReferrals.projectId, project.id)];
8937
9164
  if (cutoffDate) conditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
8938
9165
  const rows = app.db.select({
8939
9166
  date: gaSocialReferrals.date,
@@ -8956,7 +9183,7 @@ async function ga4Routes(app, opts) {
8956
9183
  return fmt(d);
8957
9184
  };
8958
9185
  const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(
8959
- eq19(gaSocialReferrals.projectId, project.id),
9186
+ eq20(gaSocialReferrals.projectId, project.id),
8960
9187
  sql5`${gaSocialReferrals.date} >= ${from}`,
8961
9188
  sql5`${gaSocialReferrals.date} < ${to}`
8962
9189
  )).get();
@@ -8969,7 +9196,7 @@ async function ga4Routes(app, opts) {
8969
9196
  source: gaSocialReferrals.source,
8970
9197
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`
8971
9198
  }).from(gaSocialReferrals).where(and8(
8972
- eq19(gaSocialReferrals.projectId, project.id),
9199
+ eq20(gaSocialReferrals.projectId, project.id),
8973
9200
  sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
8974
9201
  sql5`${gaSocialReferrals.date} < ${fmt(today)}`
8975
9202
  )).groupBy(gaSocialReferrals.source).all();
@@ -8977,7 +9204,7 @@ async function ga4Routes(app, opts) {
8977
9204
  source: gaSocialReferrals.source,
8978
9205
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`
8979
9206
  }).from(gaSocialReferrals).where(and8(
8980
- eq19(gaSocialReferrals.projectId, project.id),
9207
+ eq20(gaSocialReferrals.projectId, project.id),
8981
9208
  sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
8982
9209
  sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`
8983
9210
  )).groupBy(gaSocialReferrals.source).all();
@@ -9018,16 +9245,16 @@ async function ga4Routes(app, opts) {
9018
9245
  return fmt(d);
9019
9246
  };
9020
9247
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
9021
- 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();
9022
- 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();
9023
- const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9248
+ const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9249
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9250
+ const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9024
9251
  const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9025
- eq19(gaAiReferrals.projectId, project.id),
9252
+ eq20(gaAiReferrals.projectId, project.id),
9026
9253
  sql5`${gaAiReferrals.date} >= ${from}`,
9027
9254
  sql5`${gaAiReferrals.date} < ${to}`,
9028
- eq19(gaAiReferrals.sourceDimension, "session")
9255
+ eq20(gaAiReferrals.sourceDimension, "session")
9029
9256
  )).get();
9030
- 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();
9257
+ const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
9031
9258
  const todayStr = fmt(today);
9032
9259
  const buildTrend = (sum) => {
9033
9260
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -9037,16 +9264,16 @@ async function ga4Routes(app, opts) {
9037
9264
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
9038
9265
  };
9039
9266
  const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9040
- eq19(gaAiReferrals.projectId, project.id),
9267
+ eq20(gaAiReferrals.projectId, project.id),
9041
9268
  sql5`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
9042
9269
  sql5`${gaAiReferrals.date} < ${todayStr}`,
9043
- eq19(gaAiReferrals.sourceDimension, "session")
9270
+ eq20(gaAiReferrals.sourceDimension, "session")
9044
9271
  )).groupBy(gaAiReferrals.source).all();
9045
9272
  const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9046
- eq19(gaAiReferrals.projectId, project.id),
9273
+ eq20(gaAiReferrals.projectId, project.id),
9047
9274
  sql5`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
9048
9275
  sql5`${gaAiReferrals.date} < ${daysAgo2(7)}`,
9049
- eq19(gaAiReferrals.sourceDimension, "session")
9276
+ eq20(gaAiReferrals.sourceDimension, "session")
9050
9277
  )).groupBy(gaAiReferrals.source).all();
9051
9278
  const findBiggestMover = (current, prev) => {
9052
9279
  const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
@@ -9062,8 +9289,8 @@ async function ga4Routes(app, opts) {
9062
9289
  }
9063
9290
  return mover;
9064
9291
  };
9065
- 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();
9066
- 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();
9292
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
9293
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
9067
9294
  return {
9068
9295
  total: buildTrend(sumTotal),
9069
9296
  organic: buildTrend(sumOrganic),
@@ -9078,7 +9305,7 @@ async function ga4Routes(app, opts) {
9078
9305
  const project = resolveProject(app.db, request.params.name);
9079
9306
  requireGa4Connection(opts, project.name, project.canonicalDomain);
9080
9307
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
9081
- const conditions = [eq19(gaTrafficSnapshots.projectId, project.id)];
9308
+ const conditions = [eq20(gaTrafficSnapshots.projectId, project.id)];
9082
9309
  if (cutoffDate) conditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
9083
9310
  const rows = app.db.select({
9084
9311
  date: gaTrafficSnapshots.date,
@@ -9101,7 +9328,7 @@ async function ga4Routes(app, opts) {
9101
9328
  sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
9102
9329
  organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
9103
9330
  users: sql5`SUM(${gaTrafficSnapshots.users})`
9104
- }).from(gaTrafficSnapshots).where(eq19(gaTrafficSnapshots.projectId, project.id)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
9331
+ }).from(gaTrafficSnapshots).where(eq20(gaTrafficSnapshots.projectId, project.id)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
9105
9332
  return {
9106
9333
  pages: trafficPages.map((r) => ({
9107
9334
  landingPage: r.landingPage,
@@ -10738,7 +10965,7 @@ async function wordpressRoutes(app, opts) {
10738
10965
 
10739
10966
  // ../api-routes/src/backlinks.ts
10740
10967
  import crypto18 from "crypto";
10741
- import { and as and9, asc as asc2, desc as desc10, eq as eq20, sql as sql6 } from "drizzle-orm";
10968
+ import { and as and9, asc as asc2, desc as desc10, eq as eq21, sql as sql6 } from "drizzle-orm";
10742
10969
 
10743
10970
  // ../integration-commoncrawl/src/constants.ts
10744
10971
  import os2 from "os";
@@ -11190,7 +11417,7 @@ function mapRunRow(row) {
11190
11417
  };
11191
11418
  }
11192
11419
  function latestSummaryForProject(db, projectId, release) {
11193
- const condition = release ? and9(eq20(backlinkSummaries.projectId, projectId), eq20(backlinkSummaries.release, release)) : eq20(backlinkSummaries.projectId, projectId);
11420
+ const condition = release ? and9(eq21(backlinkSummaries.projectId, projectId), eq21(backlinkSummaries.release, release)) : eq21(backlinkSummaries.projectId, projectId);
11194
11421
  return db.select().from(backlinkSummaries).where(condition).orderBy(desc10(backlinkSummaries.queriedAt)).limit(1).get();
11195
11422
  }
11196
11423
  async function backlinksRoutes(app, opts) {
@@ -11234,7 +11461,7 @@ async function backlinksRoutes(app, opts) {
11234
11461
  "@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
11235
11462
  );
11236
11463
  }
11237
- const existing = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.release, release)).get();
11464
+ const existing = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.release, release)).get();
11238
11465
  const now = (/* @__PURE__ */ new Date()).toISOString();
11239
11466
  if (existing) {
11240
11467
  if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
@@ -11245,9 +11472,9 @@ async function backlinksRoutes(app, opts) {
11245
11472
  phaseDetail: null,
11246
11473
  error: null,
11247
11474
  updatedAt: now
11248
- }).where(eq20(ccReleaseSyncs.id, existing.id)).run();
11475
+ }).where(eq21(ccReleaseSyncs.id, existing.id)).run();
11249
11476
  opts.onReleaseSyncRequested(existing.id, release);
11250
- const refreshed = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.id, existing.id)).get();
11477
+ const refreshed = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.id, existing.id)).get();
11251
11478
  return reply.status(200).send(mapSyncRow(refreshed));
11252
11479
  }
11253
11480
  const id = crypto18.randomUUID();
@@ -11259,7 +11486,7 @@ async function backlinksRoutes(app, opts) {
11259
11486
  updatedAt: now
11260
11487
  }).run();
11261
11488
  opts.onReleaseSyncRequested(id, release);
11262
- const inserted = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.id, id)).get();
11489
+ const inserted = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.id, id)).get();
11263
11490
  return reply.status(201).send(mapSyncRow(inserted));
11264
11491
  });
11265
11492
  app.get("/backlinks/syncs/latest", async (_request, reply) => {
@@ -11317,7 +11544,7 @@ async function backlinksRoutes(app, opts) {
11317
11544
  createdAt: now
11318
11545
  }).run();
11319
11546
  opts.onBacklinkExtractRequested(runId, project.id, release);
11320
- const run = app.db.select().from(runs).where(eq20(runs.id, runId)).get();
11547
+ const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
11321
11548
  return reply.status(201).send(mapRunRow(run));
11322
11549
  });
11323
11550
  app.get(
@@ -11339,8 +11566,8 @@ async function backlinksRoutes(app, opts) {
11339
11566
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
11340
11567
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
11341
11568
  const domainCondition = and9(
11342
- eq20(backlinkDomains.projectId, project.id),
11343
- eq20(backlinkDomains.release, targetRelease)
11569
+ eq21(backlinkDomains.projectId, project.id),
11570
+ eq21(backlinkDomains.release, targetRelease)
11344
11571
  );
11345
11572
  const totalRow = app.db.select({ count: sql6`count(*)` }).from(backlinkDomains).where(domainCondition).get();
11346
11573
  const rows = app.db.select({
@@ -11358,7 +11585,7 @@ async function backlinksRoutes(app, opts) {
11358
11585
  "/projects/:name/backlinks/history",
11359
11586
  async (request, reply) => {
11360
11587
  const project = resolveProject(app.db, request.params.name);
11361
- const rows = app.db.select().from(backlinkSummaries).where(eq20(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
11588
+ const rows = app.db.select().from(backlinkSummaries).where(eq21(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
11362
11589
  const response = rows.map((r) => ({
11363
11590
  release: r.release,
11364
11591
  totalLinkingDomains: r.totalLinkingDomains,
@@ -11371,6 +11598,152 @@ async function backlinksRoutes(app, opts) {
11371
11598
  );
11372
11599
  }
11373
11600
 
11601
+ // ../api-routes/src/doctor/checks/bing-auth.ts
11602
+ var BING_AUTH_CHECKS = [
11603
+ {
11604
+ id: "bing.auth.connection",
11605
+ category: CheckCategories.auth,
11606
+ scope: CheckScopes.project,
11607
+ title: "Bing WMT connection",
11608
+ run: async (ctx) => {
11609
+ if (!ctx.project) {
11610
+ return {
11611
+ status: CheckStatuses.skipped,
11612
+ code: "bing.auth.no-project",
11613
+ summary: "Project context required.",
11614
+ remediation: null
11615
+ };
11616
+ }
11617
+ const store = ctx.bingConnectionStore;
11618
+ if (!store) {
11619
+ return {
11620
+ status: CheckStatuses.skipped,
11621
+ code: "bing.auth.store-unavailable",
11622
+ summary: "Bing connection store is not configured for this deployment.",
11623
+ remediation: null
11624
+ };
11625
+ }
11626
+ const conn = store.getConnection(ctx.project.canonicalDomain);
11627
+ if (!conn) {
11628
+ return {
11629
+ status: CheckStatuses.fail,
11630
+ code: "bing.auth.no-connection",
11631
+ summary: `No Bing connection for ${ctx.project.canonicalDomain}.`,
11632
+ remediation: `Run \`canonry bing connect ${ctx.project.name} --api-key <key>\` to authorize.`
11633
+ };
11634
+ }
11635
+ if (!conn.apiKey) {
11636
+ return {
11637
+ status: CheckStatuses.fail,
11638
+ code: "bing.auth.no-api-key",
11639
+ summary: "Bing connection exists but has no API key stored.",
11640
+ remediation: `Run \`canonry bing connect ${ctx.project.name} --api-key <key>\` to re-authorize.`
11641
+ };
11642
+ }
11643
+ try {
11644
+ await getSites(conn.apiKey);
11645
+ return {
11646
+ status: CheckStatuses.ok,
11647
+ code: "bing.auth.connected",
11648
+ summary: "Bing API key is valid and can list sites.",
11649
+ remediation: null
11650
+ };
11651
+ } catch (err) {
11652
+ const message = err instanceof Error ? err.message : String(err);
11653
+ return {
11654
+ status: CheckStatuses.fail,
11655
+ code: "bing.auth.verification-failed",
11656
+ summary: "Bing API key verification failed.",
11657
+ remediation: "Verify your Bing API key is correct and active in Bing Webmaster Tools.",
11658
+ details: { error: message }
11659
+ };
11660
+ }
11661
+ }
11662
+ },
11663
+ {
11664
+ id: "bing.auth.site-access",
11665
+ category: CheckCategories.auth,
11666
+ scope: CheckScopes.project,
11667
+ title: "Bing site access",
11668
+ run: async (ctx) => {
11669
+ if (!ctx.project) {
11670
+ return {
11671
+ status: CheckStatuses.skipped,
11672
+ code: "bing.auth.no-project",
11673
+ summary: "Project context required.",
11674
+ remediation: null
11675
+ };
11676
+ }
11677
+ const store = ctx.bingConnectionStore;
11678
+ if (!store) {
11679
+ return {
11680
+ status: CheckStatuses.skipped,
11681
+ code: "bing.auth.store-unavailable",
11682
+ summary: "Bing connection store is not configured.",
11683
+ remediation: null
11684
+ };
11685
+ }
11686
+ const conn = store.getConnection(ctx.project.canonicalDomain);
11687
+ if (!conn || !conn.apiKey) {
11688
+ return {
11689
+ status: CheckStatuses.skipped,
11690
+ code: "bing.auth.no-connection",
11691
+ summary: "Skipped \u2014 no Bing connection (see bing.auth.connection).",
11692
+ remediation: null
11693
+ };
11694
+ }
11695
+ if (!conn.siteUrl) {
11696
+ return {
11697
+ status: CheckStatuses.fail,
11698
+ code: "bing.auth.no-site-selected",
11699
+ summary: "Bing connection has no site URL selected.",
11700
+ remediation: `Run \`canonry bing sites ${ctx.project.name}\` to see available sites, then \`canonry bing set-site ${ctx.project.name} <url>\`.`
11701
+ };
11702
+ }
11703
+ try {
11704
+ const sites = await getSites(conn.apiKey);
11705
+ const match = sites.find((s) => s.Url === conn.siteUrl);
11706
+ if (!match) {
11707
+ return {
11708
+ status: CheckStatuses.fail,
11709
+ code: "bing.auth.site-not-found",
11710
+ summary: `Configured site "${conn.siteUrl}" is not in the authorized account's site list.`,
11711
+ remediation: `Add and verify "${conn.siteUrl}" in Bing Webmaster Tools, or pick an existing site using \`canonry bing set-site ${ctx.project.name}\`.`,
11712
+ details: {
11713
+ configuredSite: conn.siteUrl,
11714
+ availableSites: sites.map((s) => s.Url)
11715
+ }
11716
+ };
11717
+ }
11718
+ if (!match.Verified) {
11719
+ return {
11720
+ status: CheckStatuses.fail,
11721
+ code: "bing.auth.site-not-verified",
11722
+ summary: `Site "${conn.siteUrl}" is registered but not verified in Bing.`,
11723
+ remediation: "Complete site verification in Bing Webmaster Tools (DNS, HTML file, or Meta tag).",
11724
+ details: { siteUrl: conn.siteUrl }
11725
+ };
11726
+ }
11727
+ return {
11728
+ status: CheckStatuses.ok,
11729
+ code: "bing.auth.site-verified",
11730
+ summary: `Site "${conn.siteUrl}" is verified and accessible.`,
11731
+ remediation: null
11732
+ };
11733
+ } catch (err) {
11734
+ const message = err instanceof Error ? err.message : String(err);
11735
+ return {
11736
+ status: CheckStatuses.fail,
11737
+ code: "bing.auth.site-check-failed",
11738
+ summary: "Failed to verify Bing site access.",
11739
+ remediation: "Check Bing Webmaster Tools availability.",
11740
+ details: { error: message }
11741
+ };
11742
+ }
11743
+ }
11744
+ }
11745
+ ];
11746
+
11374
11747
  // ../api-routes/src/doctor/checks/ga-auth.ts
11375
11748
  async function checkServiceAccount(conn) {
11376
11749
  if (!conn.propertyId) {
@@ -11848,6 +12221,7 @@ var PROVIDERS_CHECKS = [providersConfiguredCheck];
11848
12221
  // ../api-routes/src/doctor/registry.ts
11849
12222
  var ALL_CHECKS = [
11850
12223
  ...GOOGLE_AUTH_CHECKS,
12224
+ ...BING_AUTH_CHECKS,
11851
12225
  ...GA_AUTH_CHECKS,
11852
12226
  ...PROVIDERS_CHECKS
11853
12227
  ];
@@ -11932,6 +12306,7 @@ async function doctorRoutes(app, opts) {
11932
12306
  db: app.db,
11933
12307
  project: null,
11934
12308
  googleConnectionStore: opts.googleConnectionStore,
12309
+ bingConnectionStore: opts.bingConnectionStore,
11935
12310
  ga4CredentialStore: opts.ga4CredentialStore,
11936
12311
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
11937
12312
  redirectUri,
@@ -11951,6 +12326,7 @@ async function doctorRoutes(app, opts) {
11951
12326
  displayName: project.displayName
11952
12327
  },
11953
12328
  googleConnectionStore: opts.googleConnectionStore,
12329
+ bingConnectionStore: opts.bingConnectionStore,
11954
12330
  ga4CredentialStore: opts.ga4CredentialStore,
11955
12331
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
11956
12332
  redirectUri,
@@ -12020,6 +12396,7 @@ async function apiRoutes(app, opts) {
12020
12396
  await api.register(historyRoutes);
12021
12397
  await api.register(analyticsRoutes);
12022
12398
  await api.register(intelligenceRoutes);
12399
+ await api.register(citationRoutes);
12023
12400
  await api.register(compositeRoutes);
12024
12401
  await api.register(contentRoutes);
12025
12402
  await api.register(settingsRoutes, {
@@ -12082,6 +12459,7 @@ async function apiRoutes(app, opts) {
12082
12459
  });
12083
12460
  await api.register(doctorRoutes, {
12084
12461
  googleConnectionStore: opts.googleConnectionStore,
12462
+ bingConnectionStore: opts.bingConnectionStore,
12085
12463
  ga4CredentialStore: opts.ga4CredentialStore,
12086
12464
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
12087
12465
  publicUrl: opts.publicUrl,
@@ -14551,7 +14929,7 @@ import crypto19 from "crypto";
14551
14929
  import fs7 from "fs";
14552
14930
  import path9 from "path";
14553
14931
  import os4 from "os";
14554
- import { and as and10, eq as eq21, inArray as inArray4, sql as sql7 } from "drizzle-orm";
14932
+ import { and as and10, eq as eq22, inArray as inArray5, sql as sql7 } from "drizzle-orm";
14555
14933
 
14556
14934
  // src/citation-utils.ts
14557
14935
  function domainMatches(domain, canonicalDomain) {
@@ -14803,11 +15181,11 @@ var JobRunner = class {
14803
15181
  this.registry = registry;
14804
15182
  }
14805
15183
  recoverStaleRuns() {
14806
- const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray4(runs.status, ["running", "queued"])).all();
15184
+ const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray5(runs.status, ["running", "queued"])).all();
14807
15185
  if (stale.length === 0) return;
14808
15186
  const now = (/* @__PURE__ */ new Date()).toISOString();
14809
15187
  for (const run of stale) {
14810
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq21(runs.id, run.id)).run();
15188
+ this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq22(runs.id, run.id)).run();
14811
15189
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
14812
15190
  }
14813
15191
  }
@@ -14835,10 +15213,10 @@ var JobRunner = class {
14835
15213
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
14836
15214
  }
14837
15215
  if (existingRun.status === "queued") {
14838
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and10(eq21(runs.id, runId), eq21(runs.status, "queued"))).run();
15216
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and10(eq22(runs.id, runId), eq22(runs.status, "queued"))).run();
14839
15217
  }
14840
15218
  this.throwIfRunCancelled(runId);
14841
- const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
15219
+ const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
14842
15220
  if (!project) {
14843
15221
  throw new Error(`Project ${projectId} not found`);
14844
15222
  }
@@ -14858,8 +15236,8 @@ var JobRunner = class {
14858
15236
  throw new Error("No providers configured. Add at least one provider API key.");
14859
15237
  }
14860
15238
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
14861
- projectKeywords = this.db.select().from(keywords).where(eq21(keywords.projectId, projectId)).all();
14862
- const projectCompetitors = this.db.select().from(competitors).where(eq21(competitors.projectId, projectId)).all();
15239
+ projectKeywords = this.db.select().from(keywords).where(eq22(keywords.projectId, projectId)).all();
15240
+ const projectCompetitors = this.db.select().from(competitors).where(eq22(competitors.projectId, projectId)).all();
14863
15241
  const competitorDomains = projectCompetitors.map((c) => c.domain);
14864
15242
  const allDomains = effectiveDomains({
14865
15243
  canonicalDomain: project.canonicalDomain,
@@ -14875,7 +15253,7 @@ var JobRunner = class {
14875
15253
  const todayPeriod = getCurrentUsageDay();
14876
15254
  for (const p of activeProviders) {
14877
15255
  const providerScope = `${projectId}:${p.adapter.name}`;
14878
- 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);
15256
+ const providerUsage = this.db.select().from(usageCounters).where(eq22(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
14879
15257
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
14880
15258
  if (providerUsage + queriesPerProvider > limit) {
14881
15259
  throw new Error(
@@ -15016,12 +15394,12 @@ var JobRunner = class {
15016
15394
  const someFailed = providerErrors.size > 0;
15017
15395
  if (allFailed) {
15018
15396
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
15019
- this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq21(runs.id, runId)).run();
15397
+ this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq22(runs.id, runId)).run();
15020
15398
  } else if (someFailed) {
15021
15399
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
15022
- this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq21(runs.id, runId)).run();
15400
+ this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq22(runs.id, runId)).run();
15023
15401
  } else {
15024
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
15402
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
15025
15403
  }
15026
15404
  this.flushProviderUsage(projectId, providerDispatchCounts);
15027
15405
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -15056,7 +15434,7 @@ var JobRunner = class {
15056
15434
  status: "failed",
15057
15435
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
15058
15436
  error: errorMessage
15059
- }).where(eq21(runs.id, runId)).run();
15437
+ }).where(eq22(runs.id, runId)).run();
15060
15438
  this.flushProviderUsage(projectId, providerDispatchCounts);
15061
15439
  trackEvent("run.completed", {
15062
15440
  status: "failed",
@@ -15099,7 +15477,7 @@ var JobRunner = class {
15099
15477
  status: runs.status,
15100
15478
  finishedAt: runs.finishedAt,
15101
15479
  error: runs.error
15102
- }).from(runs).where(eq21(runs.id, runId)).get();
15480
+ }).from(runs).where(eq22(runs.id, runId)).get();
15103
15481
  }
15104
15482
  isRunCancelled(runId) {
15105
15483
  return this.getRunState(runId)?.status === "cancelled";
@@ -15115,7 +15493,7 @@ var JobRunner = class {
15115
15493
  this.db.update(runs).set({
15116
15494
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
15117
15495
  error: currentRun.error ?? "Cancelled by user"
15118
- }).where(eq21(runs.id, runId)).run();
15496
+ }).where(eq22(runs.id, runId)).run();
15119
15497
  }
15120
15498
  trackEvent("run.completed", {
15121
15499
  status: "cancelled",
@@ -15138,7 +15516,7 @@ function getCurrentUsageDay() {
15138
15516
 
15139
15517
  // src/gsc-sync.ts
15140
15518
  import crypto20 from "crypto";
15141
- import { eq as eq22, and as and11, sql as sql8 } from "drizzle-orm";
15519
+ import { eq as eq23, and as and11, sql as sql8 } from "drizzle-orm";
15142
15520
  var log2 = createLogger("GscSync");
15143
15521
  function formatDate2(d) {
15144
15522
  return d.toISOString().split("T")[0];
@@ -15150,13 +15528,13 @@ function daysAgo(n) {
15150
15528
  }
15151
15529
  async function executeGscSync(db, runId, projectId, opts) {
15152
15530
  const now = (/* @__PURE__ */ new Date()).toISOString();
15153
- db.update(runs).set({ status: "running", startedAt: now }).where(eq22(runs.id, runId)).run();
15531
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq23(runs.id, runId)).run();
15154
15532
  try {
15155
15533
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
15156
15534
  if (!googleClientId || !googleClientSecret) {
15157
15535
  throw new Error("Google OAuth is not configured in the local Canonry config");
15158
15536
  }
15159
- const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
15537
+ const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
15160
15538
  if (!project) {
15161
15539
  throw new Error(`Project not found: ${projectId}`);
15162
15540
  }
@@ -15191,7 +15569,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15191
15569
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
15192
15570
  db.delete(gscSearchData).where(
15193
15571
  and11(
15194
- eq22(gscSearchData.projectId, projectId),
15572
+ eq23(gscSearchData.projectId, projectId),
15195
15573
  sql8`${gscSearchData.date} >= ${startDate}`,
15196
15574
  sql8`${gscSearchData.date} <= ${endDate}`
15197
15575
  )
@@ -15258,7 +15636,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15258
15636
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
15259
15637
  }
15260
15638
  }
15261
- const allInspections = db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, projectId)).all();
15639
+ const allInspections = db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, projectId)).all();
15262
15640
  const latestByUrl = /* @__PURE__ */ new Map();
15263
15641
  for (const row of allInspections) {
15264
15642
  const existing = latestByUrl.get(row.url);
@@ -15279,7 +15657,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15279
15657
  }
15280
15658
  }
15281
15659
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
15282
- db.delete(gscCoverageSnapshots).where(and11(eq22(gscCoverageSnapshots.projectId, projectId), eq22(gscCoverageSnapshots.date, snapshotDate))).run();
15660
+ db.delete(gscCoverageSnapshots).where(and11(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
15283
15661
  db.insert(gscCoverageSnapshots).values({
15284
15662
  id: crypto20.randomUUID(),
15285
15663
  projectId,
@@ -15290,11 +15668,11 @@ async function executeGscSync(db, runId, projectId, opts) {
15290
15668
  reasonBreakdown: JSON.stringify(reasonCounts),
15291
15669
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
15292
15670
  }).run();
15293
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
15671
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
15294
15672
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
15295
15673
  } catch (err) {
15296
15674
  const errorMsg = err instanceof Error ? err.message : String(err);
15297
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
15675
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
15298
15676
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
15299
15677
  throw err;
15300
15678
  }
@@ -15302,7 +15680,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15302
15680
 
15303
15681
  // src/gsc-inspect-sitemap.ts
15304
15682
  import crypto21 from "crypto";
15305
- import { eq as eq23, and as and12 } from "drizzle-orm";
15683
+ import { eq as eq24, and as and12 } from "drizzle-orm";
15306
15684
 
15307
15685
  // src/sitemap-parser.ts
15308
15686
  var log3 = createLogger("SitemapParser");
@@ -15423,13 +15801,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
15423
15801
  var log4 = createLogger("InspectSitemap");
15424
15802
  async function executeInspectSitemap(db, runId, projectId, opts) {
15425
15803
  const now = (/* @__PURE__ */ new Date()).toISOString();
15426
- db.update(runs).set({ status: "running", startedAt: now }).where(eq23(runs.id, runId)).run();
15804
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq24(runs.id, runId)).run();
15427
15805
  try {
15428
15806
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
15429
15807
  if (!googleClientId || !googleClientSecret) {
15430
15808
  throw new Error("Google OAuth is not configured in the local Canonry config");
15431
15809
  }
15432
- const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
15810
+ const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
15433
15811
  if (!project) {
15434
15812
  throw new Error(`Project not found: ${projectId}`);
15435
15813
  }
@@ -15497,7 +15875,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
15497
15875
  await new Promise((r) => setTimeout(r, 1e3));
15498
15876
  }
15499
15877
  }
15500
- const allInspections = db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, projectId)).all();
15878
+ const allInspections = db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, projectId)).all();
15501
15879
  const latestByUrl = /* @__PURE__ */ new Map();
15502
15880
  for (const row of allInspections) {
15503
15881
  const existing = latestByUrl.get(row.url);
@@ -15518,7 +15896,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
15518
15896
  }
15519
15897
  }
15520
15898
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
15521
- db.delete(gscCoverageSnapshots).where(and12(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
15899
+ db.delete(gscCoverageSnapshots).where(and12(eq24(gscCoverageSnapshots.projectId, projectId), eq24(gscCoverageSnapshots.date, snapshotDate))).run();
15522
15900
  db.insert(gscCoverageSnapshots).values({
15523
15901
  id: crypto21.randomUUID(),
15524
15902
  projectId,
@@ -15530,11 +15908,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
15530
15908
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
15531
15909
  }).run();
15532
15910
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
15533
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
15911
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
15534
15912
  log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
15535
15913
  } catch (err) {
15536
15914
  const errorMsg = err instanceof Error ? err.message : String(err);
15537
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
15915
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
15538
15916
  log4.error("inspect.failed", { runId, projectId, error: errorMsg });
15539
15917
  throw err;
15540
15918
  }
@@ -15542,7 +15920,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
15542
15920
 
15543
15921
  // src/bing-inspect-sitemap.ts
15544
15922
  import crypto22 from "crypto";
15545
- import { eq as eq24, desc as desc11 } from "drizzle-orm";
15923
+ import { eq as eq25, desc as desc11 } from "drizzle-orm";
15546
15924
  var log5 = createLogger("BingInspectSitemap");
15547
15925
  function parseBingDate2(value) {
15548
15926
  if (!value) return null;
@@ -15560,9 +15938,9 @@ function isBlockingIssueType2(issueType) {
15560
15938
  }
15561
15939
  async function executeBingInspectSitemap(db, runId, projectId, opts) {
15562
15940
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
15563
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq24(runs.id, runId)).run();
15941
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq25(runs.id, runId)).run();
15564
15942
  try {
15565
- const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
15943
+ const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
15566
15944
  if (!project) {
15567
15945
  throw new Error(`Project not found: ${projectId}`);
15568
15946
  }
@@ -15580,7 +15958,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
15580
15958
  if (sitemapUrls.length === 0) {
15581
15959
  throw new Error("No URLs found in sitemap");
15582
15960
  }
15583
- const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, projectId)).all();
15961
+ const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, projectId)).all();
15584
15962
  const trackedUrls = new Set(trackedRows.map((r) => r.url));
15585
15963
  const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
15586
15964
  log5.info("sitemap.diff", {
@@ -15663,7 +16041,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
15663
16041
  await new Promise((r) => setTimeout(r, 1e3));
15664
16042
  }
15665
16043
  }
15666
- const allInspections = db.select().from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, projectId)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
16044
+ const allInspections = db.select().from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, projectId)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
15667
16045
  const latestByUrl = /* @__PURE__ */ new Map();
15668
16046
  const definitiveByUrl = /* @__PURE__ */ new Map();
15669
16047
  for (const row of allInspections) {
@@ -15706,7 +16084,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
15706
16084
  }
15707
16085
  }).run();
15708
16086
  const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
15709
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
16087
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
15710
16088
  log5.info("inspect.completed", {
15711
16089
  runId,
15712
16090
  projectId,
@@ -15720,7 +16098,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
15720
16098
  });
15721
16099
  } catch (err) {
15722
16100
  const errorMsg = err instanceof Error ? err.message : String(err);
15723
- db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
16101
+ db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
15724
16102
  log5.error("inspect.failed", { runId, projectId, error: errorMsg });
15725
16103
  throw err;
15726
16104
  }
@@ -15729,7 +16107,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
15729
16107
  // src/commoncrawl-sync.ts
15730
16108
  import crypto23 from "crypto";
15731
16109
  import path10 from "path";
15732
- import { and as and13, eq as eq25, sql as sql9 } from "drizzle-orm";
16110
+ import { and as and13, eq as eq26, sql as sql9 } from "drizzle-orm";
15733
16111
  var log6 = createLogger("CommonCrawlSync");
15734
16112
  var INSERT_CHUNK_SIZE = 1e4;
15735
16113
  function defaultDeps() {
@@ -15755,7 +16133,7 @@ async function executeReleaseSync(db, syncId, opts) {
15755
16133
  phaseDetail: "downloading vertices + edges",
15756
16134
  updatedAt: downloadStartedAt,
15757
16135
  error: null
15758
- }).where(eq25(ccReleaseSyncs.id, syncId)).run();
16136
+ }).where(eq26(ccReleaseSyncs.id, syncId)).run();
15759
16137
  const paths = ccReleasePaths(release);
15760
16138
  const releaseCacheDir = path10.join(deps.cacheDir, release);
15761
16139
  const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
@@ -15778,7 +16156,7 @@ async function executeReleaseSync(db, syncId, opts) {
15778
16156
  vertexSha256: vertex.sha256,
15779
16157
  edgesSha256: edges.sha256,
15780
16158
  updatedAt: downloadFinishedAt
15781
- }).where(eq25(ccReleaseSyncs.id, syncId)).run();
16159
+ }).where(eq26(ccReleaseSyncs.id, syncId)).run();
15782
16160
  const allProjects = db.select().from(projects).all();
15783
16161
  const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
15784
16162
  let rows = [];
@@ -15794,8 +16172,8 @@ async function executeReleaseSync(db, syncId, opts) {
15794
16172
  }
15795
16173
  const queriedAt = deps.now().toISOString();
15796
16174
  db.transaction((tx) => {
15797
- tx.delete(backlinkDomains).where(eq25(backlinkDomains.releaseSyncId, syncId)).run();
15798
- tx.delete(backlinkSummaries).where(eq25(backlinkSummaries.releaseSyncId, syncId)).run();
16175
+ tx.delete(backlinkDomains).where(eq26(backlinkDomains.releaseSyncId, syncId)).run();
16176
+ tx.delete(backlinkSummaries).where(eq26(backlinkSummaries.releaseSyncId, syncId)).run();
15799
16177
  const expanded = [];
15800
16178
  for (const r of rows) {
15801
16179
  const projectIds = projectsByDomain.get(r.targetDomain);
@@ -15854,7 +16232,7 @@ async function executeReleaseSync(db, syncId, opts) {
15854
16232
  domainsDiscovered: rows.length,
15855
16233
  updatedAt: finishedAt,
15856
16234
  error: null
15857
- }).where(eq25(ccReleaseSyncs.id, syncId)).run();
16235
+ }).where(eq26(ccReleaseSyncs.id, syncId)).run();
15858
16236
  log6.info("sync.completed", {
15859
16237
  syncId,
15860
16238
  release,
@@ -15884,7 +16262,7 @@ async function executeReleaseSync(db, syncId, opts) {
15884
16262
  error: errorMsg,
15885
16263
  phaseDetail: null,
15886
16264
  updatedAt: finishedAt
15887
- }).where(eq25(ccReleaseSyncs.id, syncId)).run();
16265
+ }).where(eq26(ccReleaseSyncs.id, syncId)).run();
15888
16266
  log6.error("sync.failed", { syncId, release, error: errorMsg });
15889
16267
  throw err;
15890
16268
  }
@@ -15920,7 +16298,7 @@ function computeSummary(rows) {
15920
16298
  // src/backlink-extract.ts
15921
16299
  import crypto24 from "crypto";
15922
16300
  import fs8 from "fs";
15923
- import { and as and14, desc as desc12, eq as eq26 } from "drizzle-orm";
16301
+ import { and as and14, desc as desc12, eq as eq27 } from "drizzle-orm";
15924
16302
  var log7 = createLogger("BacklinkExtract");
15925
16303
  function defaultDeps2() {
15926
16304
  return {
@@ -15932,13 +16310,13 @@ function defaultDeps2() {
15932
16310
  async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
15933
16311
  const deps = { ...defaultDeps2(), ...opts.deps };
15934
16312
  const startedAt = deps.now().toISOString();
15935
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq26(runs.id, runId)).run();
16313
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq27(runs.id, runId)).run();
15936
16314
  try {
15937
- const project = db.select().from(projects).where(eq26(projects.id, projectId)).get();
16315
+ const project = db.select().from(projects).where(eq27(projects.id, projectId)).get();
15938
16316
  if (!project) {
15939
16317
  throw new Error(`Project not found: ${projectId}`);
15940
16318
  }
15941
- 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();
16319
+ const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc12(ccReleaseSyncs.createdAt)).limit(1).get();
15942
16320
  if (!sync) {
15943
16321
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
15944
16322
  }
@@ -15966,7 +16344,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
15966
16344
  const targetDomain = project.canonicalDomain;
15967
16345
  db.transaction((tx) => {
15968
16346
  tx.delete(backlinkDomains).where(
15969
- and14(eq26(backlinkDomains.projectId, projectId), eq26(backlinkDomains.release, release))
16347
+ and14(eq27(backlinkDomains.projectId, projectId), eq27(backlinkDomains.release, release))
15970
16348
  ).run();
15971
16349
  if (rows.length > 0) {
15972
16350
  const values = rows.map((r) => ({
@@ -16006,7 +16384,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16006
16384
  }).run();
16007
16385
  });
16008
16386
  const finishedAt = deps.now().toISOString();
16009
- db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
16387
+ db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq27(runs.id, runId)).run();
16010
16388
  log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
16011
16389
  } catch (err) {
16012
16390
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -16015,7 +16393,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16015
16393
  status: RunStatuses.failed,
16016
16394
  error: errorMsg,
16017
16395
  finishedAt
16018
- }).where(eq26(runs.id, runId)).run();
16396
+ }).where(eq27(runs.id, runId)).run();
16019
16397
  log7.error("extract.failed", { runId, projectId, error: errorMsg });
16020
16398
  throw err;
16021
16399
  }
@@ -16088,7 +16466,7 @@ var ProviderRegistry = class {
16088
16466
 
16089
16467
  // src/scheduler.ts
16090
16468
  import cron from "node-cron";
16091
- import { eq as eq27 } from "drizzle-orm";
16469
+ import { eq as eq28 } from "drizzle-orm";
16092
16470
  var log8 = createLogger("Scheduler");
16093
16471
  var Scheduler = class {
16094
16472
  db;
@@ -16100,7 +16478,7 @@ var Scheduler = class {
16100
16478
  }
16101
16479
  /** Load all enabled schedules from DB and register cron jobs. */
16102
16480
  start() {
16103
- const allSchedules = this.db.select().from(schedules).where(eq27(schedules.enabled, 1)).all();
16481
+ const allSchedules = this.db.select().from(schedules).where(eq28(schedules.enabled, 1)).all();
16104
16482
  for (const schedule of allSchedules) {
16105
16483
  const missedRunAt = schedule.nextRunAt;
16106
16484
  this.registerCronTask(schedule);
@@ -16125,7 +16503,7 @@ var Scheduler = class {
16125
16503
  this.stopTask(projectId, existing, "Stopped");
16126
16504
  this.tasks.delete(projectId);
16127
16505
  }
16128
- const schedule = this.db.select().from(schedules).where(eq27(schedules.projectId, projectId)).get();
16506
+ const schedule = this.db.select().from(schedules).where(eq28(schedules.projectId, projectId)).get();
16129
16507
  if (schedule && schedule.enabled === 1) {
16130
16508
  this.registerCronTask(schedule);
16131
16509
  }
@@ -16158,14 +16536,14 @@ var Scheduler = class {
16158
16536
  this.db.update(schedules).set({
16159
16537
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
16160
16538
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16161
- }).where(eq27(schedules.id, scheduleId)).run();
16539
+ }).where(eq28(schedules.id, scheduleId)).run();
16162
16540
  const label = schedule.preset ?? cronExpr;
16163
16541
  log8.info("cron.registered", { projectId, schedule: label, timezone });
16164
16542
  }
16165
16543
  triggerRun(scheduleId, projectId) {
16166
16544
  try {
16167
16545
  const now = (/* @__PURE__ */ new Date()).toISOString();
16168
- const currentSchedule = this.db.select().from(schedules).where(eq27(schedules.id, scheduleId)).get();
16546
+ const currentSchedule = this.db.select().from(schedules).where(eq28(schedules.id, scheduleId)).get();
16169
16547
  if (!currentSchedule || currentSchedule.enabled !== 1) {
16170
16548
  log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
16171
16549
  this.remove(projectId);
@@ -16173,7 +16551,7 @@ var Scheduler = class {
16173
16551
  }
16174
16552
  const task = this.tasks.get(projectId);
16175
16553
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
16176
- const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
16554
+ const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
16177
16555
  if (!project) {
16178
16556
  log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
16179
16557
  this.remove(projectId);
@@ -16202,7 +16580,7 @@ var Scheduler = class {
16202
16580
  this.db.update(schedules).set({
16203
16581
  nextRunAt,
16204
16582
  updatedAt: now
16205
- }).where(eq27(schedules.id, currentSchedule.id)).run();
16583
+ }).where(eq28(schedules.id, currentSchedule.id)).run();
16206
16584
  return;
16207
16585
  }
16208
16586
  const runId = queueResult.runId;
@@ -16210,7 +16588,7 @@ var Scheduler = class {
16210
16588
  lastRunAt: now,
16211
16589
  nextRunAt,
16212
16590
  updatedAt: now
16213
- }).where(eq27(schedules.id, currentSchedule.id)).run();
16591
+ }).where(eq28(schedules.id, currentSchedule.id)).run();
16214
16592
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
16215
16593
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
16216
16594
  log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -16222,7 +16600,7 @@ var Scheduler = class {
16222
16600
  };
16223
16601
 
16224
16602
  // src/notifier.ts
16225
- import { eq as eq28, desc as desc13, and as and15, or as or3 } from "drizzle-orm";
16603
+ import { eq as eq29, desc as desc13, and as and15, or as or3 } from "drizzle-orm";
16226
16604
  import crypto25 from "crypto";
16227
16605
  var log9 = createLogger("Notifier");
16228
16606
  var Notifier = class {
@@ -16235,18 +16613,18 @@ var Notifier = class {
16235
16613
  /** Called after a run completes (success, partial, or failed). */
16236
16614
  async onRunCompleted(runId, projectId) {
16237
16615
  log9.info("run.completed", { runId, projectId });
16238
- const notifs = this.db.select().from(notifications).where(eq28(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
16616
+ const notifs = this.db.select().from(notifications).where(eq29(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
16239
16617
  if (notifs.length === 0) {
16240
16618
  log9.info("notifications.none-enabled", { projectId });
16241
16619
  return;
16242
16620
  }
16243
16621
  log9.info("notifications.found", { projectId, count: notifs.length });
16244
- const run = this.db.select().from(runs).where(eq28(runs.id, runId)).get();
16622
+ const run = this.db.select().from(runs).where(eq29(runs.id, runId)).get();
16245
16623
  if (!run) {
16246
16624
  log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
16247
16625
  return;
16248
16626
  }
16249
- const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
16627
+ const project = this.db.select().from(projects).where(eq29(projects.id, projectId)).get();
16250
16628
  if (!project) {
16251
16629
  log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
16252
16630
  return;
@@ -16293,11 +16671,11 @@ var Notifier = class {
16293
16671
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
16294
16672
  if (highInsights.length > 0) insightEvents.push("insight.high");
16295
16673
  if (insightEvents.length === 0) return;
16296
- const notifs = this.db.select().from(notifications).where(eq28(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
16674
+ const notifs = this.db.select().from(notifications).where(eq29(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
16297
16675
  if (notifs.length === 0) return;
16298
- const run = this.db.select().from(runs).where(eq28(runs.id, runId)).get();
16676
+ const run = this.db.select().from(runs).where(eq29(runs.id, runId)).get();
16299
16677
  if (!run) return;
16300
- const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
16678
+ const project = this.db.select().from(projects).where(eq29(projects.id, projectId)).get();
16301
16679
  if (!project) return;
16302
16680
  for (const notif of notifs) {
16303
16681
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -16329,8 +16707,8 @@ var Notifier = class {
16329
16707
  computeTransitions(runId, projectId) {
16330
16708
  const recentRuns = this.db.select().from(runs).where(
16331
16709
  and15(
16332
- eq28(runs.projectId, projectId),
16333
- or3(eq28(runs.status, "completed"), eq28(runs.status, "partial"))
16710
+ eq29(runs.projectId, projectId),
16711
+ or3(eq29(runs.status, "completed"), eq29(runs.status, "partial"))
16334
16712
  )
16335
16713
  ).orderBy(desc13(runs.createdAt)).limit(2).all();
16336
16714
  if (recentRuns.length < 2) return [];
@@ -16342,12 +16720,12 @@ var Notifier = class {
16342
16720
  keyword: keywords.keyword,
16343
16721
  provider: querySnapshots.provider,
16344
16722
  citationState: querySnapshots.citationState
16345
- }).from(querySnapshots).leftJoin(keywords, eq28(querySnapshots.keywordId, keywords.id)).where(eq28(querySnapshots.runId, currentRunId)).all();
16723
+ }).from(querySnapshots).leftJoin(keywords, eq29(querySnapshots.keywordId, keywords.id)).where(eq29(querySnapshots.runId, currentRunId)).all();
16346
16724
  const previousSnapshots = this.db.select({
16347
16725
  keywordId: querySnapshots.keywordId,
16348
16726
  provider: querySnapshots.provider,
16349
16727
  citationState: querySnapshots.citationState
16350
- }).from(querySnapshots).where(eq28(querySnapshots.runId, previousRunId)).all();
16728
+ }).from(querySnapshots).where(eq29(querySnapshots.runId, previousRunId)).all();
16351
16729
  const prevMap = /* @__PURE__ */ new Map();
16352
16730
  for (const s of previousSnapshots) {
16353
16731
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -16464,7 +16842,7 @@ var RunCoordinator = class {
16464
16842
 
16465
16843
  // src/agent/session-registry.ts
16466
16844
  import crypto27 from "crypto";
16467
- import { eq as eq30 } from "drizzle-orm";
16845
+ import { eq as eq31 } from "drizzle-orm";
16468
16846
 
16469
16847
  // src/agent/session.ts
16470
16848
  import fs11 from "fs";
@@ -16679,582 +17057,67 @@ function buildSkillDocTools() {
16679
17057
  ];
16680
17058
  }
16681
17059
 
16682
- // src/agent/tools.ts
17060
+ // src/agent/mcp-to-agent-tool.ts
16683
17061
  import { Type as Type2 } from "@sinclair/typebox";
16684
-
16685
- // src/agent/memory-store.ts
16686
- import crypto26 from "crypto";
16687
- import { and as and16, desc as desc14, eq as eq29, like as like2, sql as sql10 } from "drizzle-orm";
16688
- var COMPACTION_KEY_PREFIX = "compaction:";
16689
- var COMPACTION_NOTES_PER_SESSION = 3;
16690
- function rowToDto(row) {
17062
+ var MAX_TOOL_RESULT_CHARS = 2e4;
17063
+ function truncate(json) {
17064
+ if (json.length <= MAX_TOOL_RESULT_CHARS) return json;
17065
+ return json.slice(0, MAX_TOOL_RESULT_CHARS) + "\n... (truncated \u2014 result too large)";
17066
+ }
17067
+ function textResult2(details) {
16691
17068
  return {
16692
- id: row.id,
16693
- key: row.key,
16694
- value: row.value,
16695
- source: row.source,
16696
- createdAt: row.createdAt,
16697
- updatedAt: row.updatedAt
17069
+ content: [{ type: "text", text: truncate(JSON.stringify(details, null, 2)) }],
17070
+ details
16698
17071
  };
16699
17072
  }
16700
- function listMemoryEntries(db, projectId, opts = {}) {
16701
- const query = db.select().from(agentMemory).where(eq29(agentMemory.projectId, projectId)).orderBy(desc14(agentMemory.updatedAt));
16702
- const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
16703
- return rows.map(rowToDto);
16704
- }
16705
- function upsertMemoryEntry(db, args) {
16706
- if (Buffer.byteLength(args.value, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
16707
- throw new Error(
16708
- `memory value exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes (got ${Buffer.byteLength(args.value, "utf8")})`
16709
- );
17073
+ function stripProjectFromJsonSchema(jsonSchema) {
17074
+ if (!jsonSchema || typeof jsonSchema !== "object") {
17075
+ return { schema: jsonSchema, hadProject: false };
16710
17076
  }
16711
- if (args.source !== MemorySources.compaction && args.key.startsWith(COMPACTION_KEY_PREFIX)) {
16712
- throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
17077
+ const obj = jsonSchema;
17078
+ const properties = obj.properties;
17079
+ if (!properties || typeof properties !== "object" || !("project" in properties)) {
17080
+ return { schema: jsonSchema, hadProject: false };
16713
17081
  }
16714
- const now = (/* @__PURE__ */ new Date()).toISOString();
16715
- const id = crypto26.randomUUID();
16716
- db.insert(agentMemory).values({
16717
- id,
16718
- projectId: args.projectId,
16719
- key: args.key,
16720
- value: args.value,
16721
- source: args.source,
16722
- createdAt: now,
16723
- updatedAt: now
16724
- }).onConflictDoUpdate({
16725
- target: [agentMemory.projectId, agentMemory.key],
16726
- set: {
16727
- value: args.value,
16728
- source: args.source,
16729
- updatedAt: now
16730
- }
16731
- }).run();
16732
- const row = db.select().from(agentMemory).where(and16(eq29(agentMemory.projectId, args.projectId), eq29(agentMemory.key, args.key))).get();
16733
- if (!row) throw new Error("memory upsert produced no row");
16734
- return rowToDto(row);
17082
+ const { project: _project, ...remainingProps } = properties;
17083
+ const required = Array.isArray(obj.required) ? obj.required.filter((name) => name !== "project") : obj.required;
17084
+ const stripped = { ...obj, properties: remainingProps };
17085
+ if (required === void 0) {
17086
+ delete stripped.required;
17087
+ } else {
17088
+ stripped.required = required;
17089
+ }
17090
+ return { schema: stripped, hadProject: true };
16735
17091
  }
16736
- function deleteMemoryEntry(db, projectId, key) {
16737
- const result = db.delete(agentMemory).where(and16(eq29(agentMemory.projectId, projectId), eq29(agentMemory.key, key))).run();
16738
- const changes = result.changes ?? 0;
16739
- return changes > 0;
17092
+ function mcpToAgentTool(tool, ctx) {
17093
+ const { schema: visibleSchema, hadProject } = stripProjectFromJsonSchema(tool.inputJsonSchema);
17094
+ const parameters = Type2.Unsafe(visibleSchema);
17095
+ const execute = async (_toolCallId, params) => {
17096
+ const handlerInput = hadProject ? { ...params, project: ctx.projectName } : params;
17097
+ const result = await tool.handler(ctx.client, handlerInput);
17098
+ return textResult2(result);
17099
+ };
17100
+ return {
17101
+ name: tool.name,
17102
+ label: tool.title,
17103
+ description: tool.description,
17104
+ parameters,
17105
+ execute
17106
+ };
16740
17107
  }
16741
- function loadRecentForHydrate(db, projectId, limit) {
16742
- return listMemoryEntries(db, projectId, { limit });
16743
- }
16744
- function writeCompactionNote(db, args) {
16745
- if (Buffer.byteLength(args.summary, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
16746
- throw new Error(
16747
- `compaction summary exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes; summarizer produced too much text`
16748
- );
16749
- }
16750
- const now = (/* @__PURE__ */ new Date()).toISOString();
16751
- const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
16752
- const id = crypto26.randomUUID();
16753
- let inserted;
16754
- db.transaction((tx) => {
16755
- tx.insert(agentMemory).values({
16756
- id,
16757
- projectId: args.projectId,
16758
- key,
16759
- value: args.summary,
16760
- source: MemorySources.compaction,
16761
- createdAt: now,
16762
- updatedAt: now
16763
- }).run();
16764
- const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
16765
- const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
16766
- and16(
16767
- eq29(agentMemory.projectId, args.projectId),
16768
- like2(agentMemory.key, `${sessionPrefix}%`)
16769
- )
16770
- ).orderBy(desc14(agentMemory.updatedAt)).all();
16771
- const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
16772
- if (stale.length > 0) {
16773
- tx.delete(agentMemory).where(sql10`${agentMemory.id} IN (${sql10.join(stale.map((s) => sql10`${s}`), sql10`, `)})`).run();
16774
- }
16775
- const row = tx.select().from(agentMemory).where(and16(eq29(agentMemory.projectId, args.projectId), eq29(agentMemory.key, key))).get();
16776
- if (row) inserted = rowToDto(row);
16777
- });
16778
- if (!inserted) throw new Error("compaction note write produced no row");
16779
- return inserted;
17108
+ var AERO_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
17109
+ "canonry_agent_clear"
17110
+ ]);
17111
+ function buildMcpAgentTools(registry, ctx, opts = {}) {
17112
+ return registry.filter((tool) => !AERO_EXCLUDED_MCP_TOOLS.has(tool.name)).filter((tool) => opts.readOnly ? tool.access === "read" : true).map((tool) => mcpToAgentTool(tool, ctx));
16780
17113
  }
16781
17114
 
16782
17115
  // src/agent/tools.ts
16783
- var MAX_TOOL_RESULT_CHARS = 2e4;
16784
- function truncate(json) {
16785
- if (json.length <= MAX_TOOL_RESULT_CHARS) return json;
16786
- return json.slice(0, MAX_TOOL_RESULT_CHARS) + "\n... (truncated \u2014 result too large)";
16787
- }
16788
- function textResult2(details) {
16789
- return {
16790
- content: [{ type: "text", text: truncate(JSON.stringify(details, null, 2)) }],
16791
- details
16792
- };
16793
- }
16794
- var StatusSchema = Type2.Object({
16795
- runLimit: Type2.Optional(
16796
- Type2.Number({
16797
- description: "Max recent runs to include. Default 5.",
16798
- minimum: 1,
16799
- maximum: 50
16800
- })
16801
- )
16802
- });
16803
- function buildGetStatusTool(ctx) {
16804
- return {
16805
- name: "get_status",
16806
- label: "Get status",
16807
- description: "Current project overview with its most recent runs.",
16808
- parameters: StatusSchema,
16809
- execute: async (_toolCallId, params) => {
16810
- const runLimit = params.runLimit ?? 5;
16811
- const [project, runs2] = await Promise.all([
16812
- ctx.client.getProject(ctx.projectName),
16813
- ctx.client.listRuns(ctx.projectName, runLimit)
16814
- ]);
16815
- return textResult2({ project, runs: runs2 });
16816
- }
16817
- };
16818
- }
16819
- var HealthSchema = Type2.Object({});
16820
- function buildGetHealthTool(ctx) {
16821
- return {
16822
- name: "get_health",
16823
- label: "Get health",
16824
- description: 'Latest visibility health snapshot including overall cited rate, pair counts, and per-provider breakdown. Returns `status: "no-data"` with `reason: "no-runs-yet"` and zeroed metrics for projects with no successful runs yet.',
16825
- parameters: HealthSchema,
16826
- execute: async () => {
16827
- const health = await ctx.client.getHealth(ctx.projectName);
16828
- return textResult2(health);
16829
- }
16830
- };
16831
- }
16832
- var TimelineSchema = Type2.Object({
16833
- keyword: Type2.Optional(
16834
- Type2.String({
16835
- description: "Restrict the timeline to a single keyword. Omit to return all keywords."
16836
- })
16837
- )
16838
- });
16839
- function buildGetTimelineTool(ctx) {
16840
- return {
16841
- name: "get_timeline",
16842
- label: "Get timeline",
16843
- description: "Per-keyword citation timeline showing how visibility evolved across runs. Use to identify regressions, emerging citations, or competitor movement.",
16844
- parameters: TimelineSchema,
16845
- execute: async (_toolCallId, params) => {
16846
- const timeline = await ctx.client.getTimeline(ctx.projectName);
16847
- const filtered = params.keyword ? timeline.filter((row) => row.keyword === params.keyword) : timeline;
16848
- return textResult2(filtered);
16849
- }
16850
- };
16851
- }
16852
- var InsightsSchema = Type2.Object({
16853
- includeDismissed: Type2.Optional(
16854
- Type2.Boolean({
16855
- description: "Include dismissed insights. Default false (only active insights)."
16856
- })
16857
- ),
16858
- runId: Type2.Optional(
16859
- Type2.String({
16860
- description: "Restrict insights to a specific run id. Omit for all runs."
16861
- })
16862
- )
16863
- });
16864
- function buildGetInsightsTool(ctx) {
16865
- return {
16866
- name: "get_insights",
16867
- label: "Get insights",
16868
- description: "Insights produced by the canonry intelligence engine \u2014 regressions, gains, and opportunities with cause/recommendation metadata. Query this before re-deriving conclusions from raw timeline data.",
16869
- parameters: InsightsSchema,
16870
- execute: async (_toolCallId, params) => {
16871
- const insights2 = await ctx.client.getInsights(ctx.projectName, {
16872
- dismissed: params.includeDismissed,
16873
- runId: params.runId
16874
- });
16875
- return textResult2(insights2);
16876
- }
16877
- };
16878
- }
16879
- var KeywordsSchema = Type2.Object({});
16880
- function buildListKeywordsTool(ctx) {
16881
- return {
16882
- name: "list_keywords",
16883
- label: "List keywords",
16884
- description: "All keywords currently tracked for this project.",
16885
- parameters: KeywordsSchema,
16886
- execute: async () => {
16887
- const keywords2 = await ctx.client.listKeywords(ctx.projectName);
16888
- return textResult2(keywords2);
16889
- }
16890
- };
16891
- }
16892
- var CompetitorsSchema = Type2.Object({});
16893
- function buildListCompetitorsTool(ctx) {
16894
- return {
16895
- name: "list_competitors",
16896
- label: "List competitors",
16897
- description: "Competitor domains tracked alongside this project for side-by-side comparison.",
16898
- parameters: CompetitorsSchema,
16899
- execute: async () => {
16900
- const competitors2 = await ctx.client.listCompetitors(ctx.projectName);
16901
- return textResult2(competitors2);
16902
- }
16903
- };
16904
- }
16905
- var ContentTargetsSchema = Type2.Object({
16906
- limit: Type2.Optional(
16907
- Type2.Number({
16908
- description: "Max rows. Defaults to all. Use a small number (3\u201310) when summarizing for the user."
16909
- })
16910
- ),
16911
- includeInProgress: Type2.Optional(
16912
- Type2.Boolean({
16913
- description: "Include rows that already have an in-flight tracked action. Default false."
16914
- })
16915
- )
16916
- });
16917
- function buildGetContentTargetsTool(ctx) {
16918
- return {
16919
- name: "get_content_targets",
16920
- label: "Get content targets",
16921
- 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.",
16922
- parameters: ContentTargetsSchema,
16923
- execute: async (_toolCallId, params) => {
16924
- const response = await ctx.client.getContentTargets(ctx.projectName, {
16925
- limit: params.limit,
16926
- includeInProgress: params.includeInProgress === true
16927
- });
16928
- return textResult2(response);
16929
- }
16930
- };
16931
- }
16932
- var ContentSourcesSchema = Type2.Object({});
16933
- function buildGetContentSourcesTool(ctx) {
16934
- return {
16935
- name: "get_grounding_sources",
16936
- label: "Get grounding sources",
16937
- 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.",
16938
- parameters: ContentSourcesSchema,
16939
- execute: async () => {
16940
- const response = await ctx.client.getContentSources(ctx.projectName);
16941
- return textResult2(response);
16942
- }
16943
- };
16944
- }
16945
- var ContentGapsSchema = Type2.Object({});
16946
- function buildGetContentGapsTool(ctx) {
16947
- return {
16948
- name: "get_content_gaps",
16949
- label: "Get content gaps",
16950
- 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.',
16951
- parameters: ContentGapsSchema,
16952
- execute: async () => {
16953
- const response = await ctx.client.getContentGaps(ctx.projectName);
16954
- return textResult2(response);
16955
- }
16956
- };
16957
- }
16958
- var RunDetailSchema = Type2.Object({
16959
- runId: Type2.String({
16960
- description: "Run id (UUID) to fetch. Typically obtained from get_status runs[].id."
16961
- })
16962
- });
16963
- function buildGetRunTool(ctx) {
16964
- return {
16965
- name: "get_run",
16966
- label: "Get run detail",
16967
- description: "Full detail for a specific run including per-keyword snapshots, error messages, and provider breakdown. Use to investigate failed runs or drill into a particular sweep.",
16968
- parameters: RunDetailSchema,
16969
- execute: async (_toolCallId, params) => {
16970
- const run = await ctx.client.getRun(params.runId);
16971
- return textResult2(run);
16972
- }
16973
- };
16974
- }
16975
- var BacklinksSchema = Type2.Object({
16976
- limit: Type2.Optional(
16977
- Type2.Number({
16978
- description: "Max linking-domain rows to include. Default 50, max 200.",
16979
- minimum: 1,
16980
- maximum: 200
16981
- })
16982
- ),
16983
- release: Type2.Optional(
16984
- Type2.String({
16985
- description: "Common Crawl release id (e.g., cc-main-2026-jan-feb-mar). Omit for the most recent release with data."
16986
- })
16987
- )
16988
- });
16989
- function buildListBacklinksTool(ctx) {
16990
- return {
16991
- name: "list_backlinks",
16992
- label: "List backlinks",
16993
- description: "Backlink summary and top linking domains from the most recent ready Common Crawl release. Off-site authority signal that correlates with citation likelihood. Returns null summary when no release sync has completed for this workspace.",
16994
- parameters: BacklinksSchema,
16995
- execute: async (_toolCallId, params) => {
16996
- const response = await ctx.client.backlinksDomains(ctx.projectName, {
16997
- limit: params.limit ?? 50,
16998
- release: params.release
16999
- });
17000
- return textResult2(response);
17001
- }
17002
- };
17003
- }
17004
- var RecallSchema = Type2.Object({
17005
- limit: Type2.Optional(
17006
- Type2.Number({
17007
- description: "Max notes to return, ordered newest-first. Default 50. Max 100.",
17008
- minimum: 1,
17009
- maximum: 100
17010
- })
17011
- )
17012
- });
17013
- function buildRecallTool(ctx) {
17014
- return {
17015
- name: "recall",
17016
- label: "Recall memory",
17017
- description: "Read project-scoped durable notes Aero has stored via `remember` (plus compaction summaries). Returns entries newest-first. The N most-recent entries are also injected into the system prompt at session start, so you usually do not need to call this \u2014 reach for it when you need older context or the full note value.",
17018
- parameters: RecallSchema,
17019
- execute: async (_toolCallId, params) => {
17020
- const entries = listMemoryEntries(ctx.db, ctx.projectId, { limit: params.limit ?? 50 });
17021
- return textResult2({ entries });
17022
- }
17023
- };
17024
- }
17025
17116
  function buildReadTools(ctx) {
17026
- return [
17027
- buildGetStatusTool(ctx),
17028
- buildGetHealthTool(ctx),
17029
- buildGetTimelineTool(ctx),
17030
- buildGetInsightsTool(ctx),
17031
- buildListKeywordsTool(ctx),
17032
- buildListCompetitorsTool(ctx),
17033
- buildGetRunTool(ctx),
17034
- buildRecallTool(ctx),
17035
- buildListBacklinksTool(ctx),
17036
- buildGetContentTargetsTool(ctx),
17037
- buildGetContentSourcesTool(ctx),
17038
- buildGetContentGapsTool(ctx)
17039
- ];
17040
- }
17041
- var RunSweepSchema = Type2.Object({
17042
- providers: Type2.Optional(
17043
- Type2.Array(Type2.String(), {
17044
- description: "Subset of providers to run. Omit to use every configured provider on the project."
17045
- })
17046
- ),
17047
- noLocation: Type2.Optional(
17048
- Type2.Boolean({
17049
- description: "Run without a location context. Default: use the project default location."
17050
- })
17051
- )
17052
- });
17053
- function buildRunSweepTool(ctx) {
17054
- return {
17055
- name: "run_sweep",
17056
- label: "Trigger sweep",
17057
- description: "Trigger a new answer-visibility sweep for this project across configured AI providers. Returns the run id(s). Use when fresh citation data is needed.",
17058
- parameters: RunSweepSchema,
17059
- execute: async (_toolCallId, params) => {
17060
- const body = {};
17061
- if (params.providers?.length) body.providers = params.providers;
17062
- if (params.noLocation) body.noLocation = true;
17063
- const result = await ctx.client.triggerRun(ctx.projectName, body);
17064
- return textResult2(result);
17065
- }
17066
- };
17067
- }
17068
- var DismissInsightSchema = Type2.Object({
17069
- insightId: Type2.String({
17070
- description: "Insight id to dismiss. Obtain from get_insights details[].id."
17071
- })
17072
- });
17073
- function buildDismissInsightTool(ctx) {
17074
- return {
17075
- name: "dismiss_insight",
17076
- label: "Dismiss insight",
17077
- description: "Mark an insight as dismissed so it no longer surfaces in active insight lists. Reversible via the dashboard.",
17078
- parameters: DismissInsightSchema,
17079
- execute: async (_toolCallId, params) => {
17080
- const result = await ctx.client.dismissInsight(ctx.projectName, params.insightId);
17081
- return textResult2(result);
17082
- }
17083
- };
17084
- }
17085
- var AddKeywordsSchema = Type2.Object({
17086
- keywords: Type2.Array(Type2.String(), {
17087
- minItems: 1,
17088
- description: "Keywords to add to the tracking list. Duplicates against existing keywords are ignored server-side."
17089
- })
17090
- });
17091
- function buildAddKeywordsTool(ctx) {
17092
- return {
17093
- name: "add_keywords",
17094
- label: "Add keywords",
17095
- description: "Append keywords to the project tracking list. Additive only \u2014 existing keywords are preserved. Use exact phrasing you want tracked.",
17096
- parameters: AddKeywordsSchema,
17097
- execute: async (_toolCallId, params) => {
17098
- await ctx.client.appendKeywords(ctx.projectName, params.keywords);
17099
- return textResult2({ added: params.keywords });
17100
- }
17101
- };
17102
- }
17103
- var AddCompetitorsSchema = Type2.Object({
17104
- domains: Type2.Array(Type2.String(), {
17105
- minItems: 1,
17106
- description: 'Competitor domains to track. Provide bare domains (e.g. "example.com"), not URLs.'
17107
- })
17108
- });
17109
- function buildAddCompetitorsTool(ctx) {
17110
- return {
17111
- name: "add_competitors",
17112
- label: "Add competitors",
17113
- description: "Append competitor domains to the project. Existing competitors are skipped by the API.",
17114
- parameters: AddCompetitorsSchema,
17115
- execute: async (_toolCallId, params) => {
17116
- const existing = await ctx.client.listCompetitors(ctx.projectName);
17117
- const existingDomains = new Set(existing.map((c) => c.domain));
17118
- const newDomains = params.domains.filter((d) => !existingDomains.has(d));
17119
- if (newDomains.length === 0) {
17120
- return textResult2({ added: [], alreadyTracked: params.domains });
17121
- }
17122
- await ctx.client.appendCompetitors(ctx.projectName, newDomains);
17123
- return textResult2({ added: newDomains, alreadyTracked: params.domains.filter((d) => existingDomains.has(d)) });
17124
- }
17125
- };
17126
- }
17127
- var UpdateScheduleSchema = Type2.Object({
17128
- cron: Type2.Optional(
17129
- Type2.String({ description: 'Cron expression (e.g. "0 */6 * * *"). Provide cron OR preset, not both.' })
17130
- ),
17131
- preset: Type2.Optional(
17132
- Type2.String({ description: 'Preset keyword (e.g. "daily", "hourly"). Provide cron OR preset, not both.' })
17133
- ),
17134
- timezone: Type2.Optional(Type2.String({ description: 'IANA timezone. Default: "UTC".' })),
17135
- enabled: Type2.Optional(
17136
- Type2.Boolean({ description: "Whether the schedule is active. Default: true." })
17137
- ),
17138
- providers: Type2.Optional(
17139
- Type2.Array(Type2.String(), {
17140
- description: "Providers to run on each scheduled sweep. Omit to use all configured providers."
17141
- })
17142
- )
17143
- });
17144
- function buildUpdateScheduleTool(ctx) {
17145
- return {
17146
- name: "update_schedule",
17147
- label: "Update schedule",
17148
- description: "Create or update the recurring sweep schedule for this project. Provide exactly one of `cron` (expression) or `preset` (keyword). Fully replaces any existing schedule.",
17149
- parameters: UpdateScheduleSchema,
17150
- execute: async (_toolCallId, params) => {
17151
- if (params.cron && params.preset || !params.cron && !params.preset) {
17152
- throw new Error("update_schedule: provide exactly one of `cron` or `preset`");
17153
- }
17154
- const body = {};
17155
- if (params.cron) body.cron = params.cron;
17156
- if (params.preset) body.preset = params.preset;
17157
- if (params.timezone) body.timezone = params.timezone;
17158
- if (params.enabled !== void 0) body.enabled = params.enabled;
17159
- if (params.providers?.length) body.providers = params.providers;
17160
- const result = await ctx.client.putSchedule(ctx.projectName, body);
17161
- return textResult2(result);
17162
- }
17163
- };
17164
- }
17165
- var AttachAgentWebhookSchema = Type2.Object({
17166
- url: Type2.String({
17167
- description: "External agent webhook URL. Canonry will POST run.completed, insight.critical, insight.high, and citation.gained events to it."
17168
- })
17169
- });
17170
- function buildAttachAgentWebhookTool(ctx) {
17171
- return {
17172
- name: "attach_agent_webhook",
17173
- label: "Attach agent webhook",
17174
- description: "Register an external agent webhook for this project. Use when wiring a Claude Code / Codex / custom agent to receive canonry run and insight events. Idempotent \u2014 skips if one already exists.",
17175
- parameters: AttachAgentWebhookSchema,
17176
- execute: async (_toolCallId, params) => {
17177
- const existing = await ctx.client.listNotifications(ctx.projectName);
17178
- const hasAgent = existing.some((n) => n.source === "agent");
17179
- if (hasAgent) {
17180
- return textResult2({ status: "already-attached" });
17181
- }
17182
- const result = await ctx.client.createNotification(ctx.projectName, {
17183
- channel: "webhook",
17184
- url: params.url,
17185
- events: ["run.completed", "insight.critical", "insight.high", "citation.gained"],
17186
- source: "agent"
17187
- });
17188
- return textResult2({ status: "attached", notificationId: result.id, url: params.url });
17189
- }
17190
- };
17191
- }
17192
- var RememberSchema = Type2.Object({
17193
- key: Type2.String({
17194
- description: `Stable identifier for this note (max ${AGENT_MEMORY_KEY_MAX_LENGTH} chars). Writing the same key overwrites the prior value. Do NOT use the "${COMPACTION_KEY_PREFIX}" prefix \u2014 that namespace is reserved for transcript compaction summaries.`,
17195
- minLength: 1,
17196
- maxLength: AGENT_MEMORY_KEY_MAX_LENGTH
17197
- }),
17198
- value: Type2.String({
17199
- description: `Plain-text note to persist (max ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes). Use for durable operator preferences, migration context, or non-obvious reasoning you'll want on a future turn. Do NOT duplicate data canonry already tracks (runs, insights, timelines) \u2014 query those instead.`,
17200
- minLength: 1
17201
- })
17202
- });
17203
- function buildRememberTool(ctx) {
17204
- return {
17205
- name: "remember",
17206
- label: "Remember",
17207
- description: "Persist a project-scoped durable note visible to every future Aero session for this project. Upsert \u2014 writing the same key replaces the prior value. Capped at 2 KB per note.",
17208
- parameters: RememberSchema,
17209
- execute: async (_toolCallId, params) => {
17210
- const entry = upsertMemoryEntry(ctx.db, {
17211
- projectId: ctx.projectId,
17212
- key: params.key,
17213
- value: params.value,
17214
- source: MemorySources.aero
17215
- });
17216
- return textResult2({ status: "remembered", entry });
17217
- }
17218
- };
17219
- }
17220
- var ForgetSchema = Type2.Object({
17221
- key: Type2.String({
17222
- description: "Exact key of the note to remove. No-op (status=missing) when no note exists for that key.",
17223
- minLength: 1,
17224
- maxLength: AGENT_MEMORY_KEY_MAX_LENGTH
17225
- })
17226
- });
17227
- function buildForgetTool(ctx) {
17228
- return {
17229
- name: "forget",
17230
- label: "Forget",
17231
- description: "Delete a durable note by key. Use when a previously-remembered fact is wrong or no longer relevant.",
17232
- parameters: ForgetSchema,
17233
- execute: async (_toolCallId, params) => {
17234
- if (params.key.startsWith(COMPACTION_KEY_PREFIX)) {
17235
- throw new Error(
17236
- `cannot forget compaction notes directly \u2014 they are pruned automatically (key prefix "${COMPACTION_KEY_PREFIX}" is reserved)`
17237
- );
17238
- }
17239
- const removed = deleteMemoryEntry(ctx.db, ctx.projectId, params.key);
17240
- return textResult2({ status: removed ? "forgotten" : "missing", key: params.key });
17241
- }
17242
- };
17243
- }
17244
- function buildWriteTools(ctx) {
17245
- return [
17246
- buildRunSweepTool(ctx),
17247
- buildDismissInsightTool(ctx),
17248
- buildAddKeywordsTool(ctx),
17249
- buildAddCompetitorsTool(ctx),
17250
- buildUpdateScheduleTool(ctx),
17251
- buildAttachAgentWebhookTool(ctx),
17252
- buildRememberTool(ctx),
17253
- buildForgetTool(ctx)
17254
- ];
17117
+ return buildMcpAgentTools(canonryMcpTools, ctx, { readOnly: true });
17255
17118
  }
17256
17119
  function buildAllTools(ctx) {
17257
- return [...buildReadTools(ctx), ...buildWriteTools(ctx)];
17120
+ return buildMcpAgentTools(canonryMcpTools, ctx);
17258
17121
  }
17259
17122
 
17260
17123
  // src/agent/session.ts
@@ -17304,9 +17167,7 @@ function createAeroSession(opts) {
17304
17167
  const toolScope = opts.toolScope ?? "all";
17305
17168
  const toolCtx = {
17306
17169
  client: opts.client,
17307
- projectName: opts.projectName,
17308
- db: opts.db,
17309
- projectId: opts.projectId
17170
+ projectName: opts.projectName
17310
17171
  };
17311
17172
  const stateTools = toolScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
17312
17173
  const defaultTools = [...stateTools, ...buildSkillDocTools()];
@@ -17329,6 +17190,103 @@ function resolveSessionProviderAndModel(config, opts) {
17329
17190
  return { provider, modelId };
17330
17191
  }
17331
17192
 
17193
+ // src/agent/memory-store.ts
17194
+ import crypto26 from "crypto";
17195
+ import { and as and16, desc as desc14, eq as eq30, like as like2, sql as sql10 } from "drizzle-orm";
17196
+ var COMPACTION_KEY_PREFIX = "compaction:";
17197
+ var COMPACTION_NOTES_PER_SESSION = 3;
17198
+ function rowToDto(row) {
17199
+ return {
17200
+ id: row.id,
17201
+ key: row.key,
17202
+ value: row.value,
17203
+ source: row.source,
17204
+ createdAt: row.createdAt,
17205
+ updatedAt: row.updatedAt
17206
+ };
17207
+ }
17208
+ function listMemoryEntries(db, projectId, opts = {}) {
17209
+ const query = db.select().from(agentMemory).where(eq30(agentMemory.projectId, projectId)).orderBy(desc14(agentMemory.updatedAt));
17210
+ const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
17211
+ return rows.map(rowToDto);
17212
+ }
17213
+ function upsertMemoryEntry(db, args) {
17214
+ if (Buffer.byteLength(args.value, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
17215
+ throw new Error(
17216
+ `memory value exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes (got ${Buffer.byteLength(args.value, "utf8")})`
17217
+ );
17218
+ }
17219
+ if (args.source !== MemorySources.compaction && args.key.startsWith(COMPACTION_KEY_PREFIX)) {
17220
+ throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
17221
+ }
17222
+ const now = (/* @__PURE__ */ new Date()).toISOString();
17223
+ const id = crypto26.randomUUID();
17224
+ db.insert(agentMemory).values({
17225
+ id,
17226
+ projectId: args.projectId,
17227
+ key: args.key,
17228
+ value: args.value,
17229
+ source: args.source,
17230
+ createdAt: now,
17231
+ updatedAt: now
17232
+ }).onConflictDoUpdate({
17233
+ target: [agentMemory.projectId, agentMemory.key],
17234
+ set: {
17235
+ value: args.value,
17236
+ source: args.source,
17237
+ updatedAt: now
17238
+ }
17239
+ }).run();
17240
+ const row = db.select().from(agentMemory).where(and16(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, args.key))).get();
17241
+ if (!row) throw new Error("memory upsert produced no row");
17242
+ return rowToDto(row);
17243
+ }
17244
+ function deleteMemoryEntry(db, projectId, key) {
17245
+ const result = db.delete(agentMemory).where(and16(eq30(agentMemory.projectId, projectId), eq30(agentMemory.key, key))).run();
17246
+ const changes = result.changes ?? 0;
17247
+ return changes > 0;
17248
+ }
17249
+ function loadRecentForHydrate(db, projectId, limit) {
17250
+ return listMemoryEntries(db, projectId, { limit });
17251
+ }
17252
+ function writeCompactionNote(db, args) {
17253
+ if (Buffer.byteLength(args.summary, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
17254
+ throw new Error(
17255
+ `compaction summary exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes; summarizer produced too much text`
17256
+ );
17257
+ }
17258
+ const now = (/* @__PURE__ */ new Date()).toISOString();
17259
+ const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
17260
+ const id = crypto26.randomUUID();
17261
+ let inserted;
17262
+ db.transaction((tx) => {
17263
+ tx.insert(agentMemory).values({
17264
+ id,
17265
+ projectId: args.projectId,
17266
+ key,
17267
+ value: args.summary,
17268
+ source: MemorySources.compaction,
17269
+ createdAt: now,
17270
+ updatedAt: now
17271
+ }).run();
17272
+ const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
17273
+ const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
17274
+ and16(
17275
+ eq30(agentMemory.projectId, args.projectId),
17276
+ like2(agentMemory.key, `${sessionPrefix}%`)
17277
+ )
17278
+ ).orderBy(desc14(agentMemory.updatedAt)).all();
17279
+ const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
17280
+ if (stale.length > 0) {
17281
+ tx.delete(agentMemory).where(sql10`${agentMemory.id} IN (${sql10.join(stale.map((s) => sql10`${s}`), sql10`, `)})`).run();
17282
+ }
17283
+ const row = tx.select().from(agentMemory).where(and16(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, key))).get();
17284
+ if (row) inserted = rowToDto(row);
17285
+ });
17286
+ if (!inserted) throw new Error("compaction note write produced no row");
17287
+ return inserted;
17288
+ }
17289
+
17332
17290
  // src/agent/compaction.ts
17333
17291
  import { complete } from "@mariozechner/pi-ai";
17334
17292
 
@@ -17504,14 +17462,12 @@ var SessionRegistry = class {
17504
17462
  modelProvider: effectiveProvider,
17505
17463
  modelId: effectiveModelId,
17506
17464
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
17507
- }).where(eq30(agentSessions.projectId, projectId)).run();
17465
+ }).where(eq31(agentSessions.projectId, projectId)).run();
17508
17466
  }
17509
17467
  const agent2 = createAeroSession({
17510
17468
  projectName,
17511
17469
  client: this.opts.client,
17512
17470
  config: this.opts.config,
17513
- db: this.opts.db,
17514
- projectId,
17515
17471
  provider: effectiveProvider,
17516
17472
  modelId: effectiveModelId,
17517
17473
  systemPromptOverride: this.buildHydratedSystemPrompt(projectId, row.systemPrompt),
@@ -17534,8 +17490,6 @@ var SessionRegistry = class {
17534
17490
  projectName,
17535
17491
  client: this.opts.client,
17536
17492
  config: this.opts.config,
17537
- db: this.opts.db,
17538
- projectId,
17539
17493
  provider,
17540
17494
  modelId,
17541
17495
  // Hydrate on the fresh path too — a brand-new session may still see
@@ -17595,7 +17549,7 @@ var SessionRegistry = class {
17595
17549
  ---
17596
17550
 
17597
17551
  <memory>
17598
- Project-scoped durable notes (newest first). Use remember/forget/recall to manage. Entries tagged [compaction] are LLM-summarized transcript slices.
17552
+ Project-scoped durable notes (newest first). Use canonry_memory_set/canonry_memory_forget/canonry_memory_list to manage. Entries tagged [compaction] are LLM-summarized transcript slices.
17599
17553
 
17600
17554
  ${lines.join("\n")}
17601
17555
  </memory>`;
@@ -17702,7 +17656,7 @@ ${lines.join("\n")}
17702
17656
  if (this.scopes.get(projectName) === wantScope) return;
17703
17657
  const projectId = this.projectIds.get(projectName) ?? this.resolveProjectId(projectName);
17704
17658
  this.projectIds.set(projectName, projectId);
17705
- const toolCtx = { client: this.opts.client, projectName, db: this.opts.db, projectId };
17659
+ const toolCtx = { client: this.opts.client, projectName };
17706
17660
  const stateTools = wantScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
17707
17661
  agent.state.tools = [...stateTools, ...buildSkillDocTools()];
17708
17662
  this.scopes.set(projectName, wantScope);
@@ -17722,7 +17676,7 @@ ${lines.join("\n")}
17722
17676
  modelProvider: nextProvider,
17723
17677
  modelId: nextModelId,
17724
17678
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
17725
- }).where(eq30(agentSessions.projectId, projectId)).run();
17679
+ }).where(eq31(agentSessions.projectId, projectId)).run();
17726
17680
  }
17727
17681
  /** Persist a session's transcript back to the DB. Call after any run settles. */
17728
17682
  save(projectName) {
@@ -17884,11 +17838,11 @@ ${lines.join("\n")}
17884
17838
  return id;
17885
17839
  }
17886
17840
  tryResolveProjectId(projectName) {
17887
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq30(projects.name, projectName)).get();
17841
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq31(projects.name, projectName)).get();
17888
17842
  return row?.id;
17889
17843
  }
17890
17844
  loadRow(projectId) {
17891
- const row = this.opts.db.select().from(agentSessions).where(eq30(agentSessions.projectId, projectId)).get();
17845
+ const row = this.opts.db.select().from(agentSessions).where(eq31(agentSessions.projectId, projectId)).get();
17892
17846
  return row ?? null;
17893
17847
  }
17894
17848
  insertRow(params) {
@@ -17907,14 +17861,14 @@ ${lines.join("\n")}
17907
17861
  }
17908
17862
  updateRow(projectId, patch) {
17909
17863
  const now = (/* @__PURE__ */ new Date()).toISOString();
17910
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq30(agentSessions.projectId, projectId)).run();
17864
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq31(agentSessions.projectId, projectId)).run();
17911
17865
  }
17912
17866
  };
17913
17867
 
17914
17868
  // src/agent/agent-routes.ts
17915
- import { eq as eq31 } from "drizzle-orm";
17869
+ import { eq as eq32 } from "drizzle-orm";
17916
17870
  function resolveProject2(db, name) {
17917
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq31(projects.name, name)).get();
17871
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq32(projects.name, name)).get();
17918
17872
  if (!row) throw notFound("project", name);
17919
17873
  return row;
17920
17874
  }
@@ -17923,7 +17877,7 @@ function registerAgentRoutes(app, opts) {
17923
17877
  "/projects/:name/agent/transcript",
17924
17878
  async (request) => {
17925
17879
  const project = resolveProject2(opts.db, request.params.name);
17926
- const row = opts.db.select().from(agentSessions).where(eq31(agentSessions.projectId, project.id)).get();
17880
+ const row = opts.db.select().from(agentSessions).where(eq32(agentSessions.projectId, project.id)).get();
17927
17881
  if (!row) {
17928
17882
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
17929
17883
  }
@@ -17947,7 +17901,7 @@ function registerAgentRoutes(app, opts) {
17947
17901
  async (request) => {
17948
17902
  const project = resolveProject2(opts.db, request.params.name);
17949
17903
  opts.sessionRegistry.reset(project.name);
17950
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(agentSessions.projectId, project.id)).run();
17904
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(agentSessions.projectId, project.id)).run();
17951
17905
  return { status: "reset" };
17952
17906
  }
17953
17907
  );
@@ -18251,7 +18205,7 @@ var SnapshotService = class {
18251
18205
  }
18252
18206
  async createReport(input) {
18253
18207
  const companyName = input.companyName.trim();
18254
- const domain = normalizeDomain2(input.domain);
18208
+ const domain = normalizeDomain3(input.domain);
18255
18209
  const manualPhrases = normalizeStringList(input.phrases ?? []);
18256
18210
  const manualCompetitors = normalizeStringList(input.competitors ?? []);
18257
18211
  const providers = this.registry.getAll();
@@ -18691,7 +18645,7 @@ function extractCompetitorsFromResponse(ctx) {
18691
18645
  const targetDomain = extractHostname2(ctx.targetDomain);
18692
18646
  for (const hint of ctx.manualCompetitors) {
18693
18647
  if (isDomainLike(hint)) {
18694
- const normalizedHint = normalizeDomain2(hint);
18648
+ const normalizedHint = normalizeDomain3(hint);
18695
18649
  if (domainMatches2(normalizedHint, targetDomain)) continue;
18696
18650
  if (ctx.citedDomains.some((domain) => domainMatches2(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
18697
18651
  competitors2.add(normalizedHint);
@@ -18750,7 +18704,7 @@ function uniqueStrings2(values) {
18750
18704
  values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
18751
18705
  )];
18752
18706
  }
18753
- function normalizeDomain2(value) {
18707
+ function normalizeDomain3(value) {
18754
18708
  const trimmed = value.trim();
18755
18709
  if (!trimmed) return trimmed;
18756
18710
  try {
@@ -18761,15 +18715,15 @@ function normalizeDomain2(value) {
18761
18715
  }
18762
18716
  }
18763
18717
  function extractHostname2(value) {
18764
- return normalizeDomain2(value);
18718
+ return normalizeDomain3(value);
18765
18719
  }
18766
18720
  function domainMatches2(candidate, target) {
18767
- const normalizedCandidate = normalizeDomain2(candidate);
18768
- const normalizedTarget = normalizeDomain2(target);
18721
+ const normalizedCandidate = normalizeDomain3(candidate);
18722
+ const normalizedTarget = normalizeDomain3(target);
18769
18723
  return normalizedCandidate === normalizedTarget || normalizedCandidate.endsWith(`.${normalizedTarget}`);
18770
18724
  }
18771
18725
  function isDomainLike(value) {
18772
- const normalized = normalizeDomain2(value);
18726
+ const normalized = normalizeDomain3(value);
18773
18727
  return normalized.includes(".") && !normalized.includes(" ");
18774
18728
  }
18775
18729
  function clipText(value, length) {
@@ -18969,11 +18923,11 @@ async function createServer(opts) {
18969
18923
  intelligenceService,
18970
18924
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
18971
18925
  async ({ runId, projectId, insightCount, criticalOrHigh }) => {
18972
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq32(projects.id, projectId)).get();
18926
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq33(projects.id, projectId)).get();
18973
18927
  if (!project) return;
18974
18928
  sessionRegistry.queueFollowUp(project.name, {
18975
18929
  role: "user",
18976
- content: `[system] Run ${runId} completed for project ${project.name}. ${insightCount} insights generated (${criticalOrHigh} critical/high). Use get_run to inspect the run and get_insights to review new findings. Surface anything notable briefly \u2014 skip chit-chat.`,
18930
+ content: `[system] Run ${runId} completed for project ${project.name}. ${insightCount} insights generated (${criticalOrHigh} critical/high). Use canonry_run_get to inspect the run and canonry_insights_list to review new findings. Surface anything notable briefly \u2014 skip chit-chat.`,
18977
18931
  timestamp: Date.now()
18978
18932
  });
18979
18933
  void sessionRegistry.drainNow(project.name);
@@ -19109,7 +19063,7 @@ async function createServer(opts) {
19109
19063
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
19110
19064
  if (opts.config.apiKey) {
19111
19065
  const keyHash = hashApiKey(opts.config.apiKey);
19112
- const existing = opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, keyHash)).get();
19066
+ const existing = opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, keyHash)).get();
19113
19067
  if (!existing) {
19114
19068
  const prefix = opts.config.apiKey.slice(0, 12);
19115
19069
  opts.db.insert(apiKeys).values({
@@ -19161,7 +19115,7 @@ async function createServer(opts) {
19161
19115
  };
19162
19116
  const getDefaultApiKey = () => {
19163
19117
  if (!opts.config.apiKey) return void 0;
19164
- return opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
19118
+ return opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
19165
19119
  };
19166
19120
  const createPasswordSession = (reply) => {
19167
19121
  const key = getDefaultApiKey();
@@ -19218,12 +19172,12 @@ async function createServer(opts) {
19218
19172
  return reply.send({ authenticated: true });
19219
19173
  }
19220
19174
  if (apiKey) {
19221
- const key = opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, hashApiKey(apiKey))).get();
19175
+ const key = opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, hashApiKey(apiKey))).get();
19222
19176
  if (!key || key.revokedAt) {
19223
19177
  const err2 = authInvalid();
19224
19178
  return reply.status(err2.statusCode).send(err2.toJSON());
19225
19179
  }
19226
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(apiKeys.id, key.id)).run();
19180
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq33(apiKeys.id, key.id)).run();
19227
19181
  const sessionId = createSession(key.id);
19228
19182
  reply.header("set-cookie", serializeSessionCookie({
19229
19183
  name: SESSION_COOKIE_NAME,