@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.
- package/assets/agent-workspace/skills/aero/references/memory-patterns.md +9 -9
- package/assets/assets/index-CwZdqfeb.js +302 -0
- package/assets/assets/{index-DMx3Oy9W.css → index-Ec4au-rY.css} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-KANIG6ES.js → chunk-75EV36V6.js} +735 -781
- package/dist/{chunk-OX24LLIH.js → chunk-7DVIJC6L.js} +990 -5
- package/dist/cli.js +99 -2
- package/dist/index.js +2 -2
- package/dist/mcp.js +9 -796
- package/package.json +6 -6
- package/assets/assets/index-D0BgyJJT.js +0 -302
|
@@ -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-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
2560
|
+
}).from(querySnapshots).innerJoin(keywords, eq13(querySnapshots.keywordId, keywords.id)).where(
|
|
2413
2561
|
and3(
|
|
2414
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
2836
|
+
const ownDomain = normalizeDomain2(project.canonicalDomain);
|
|
2689
2837
|
const ownedDomains = parseJsonColumn(project.ownedDomains, []);
|
|
2690
|
-
const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2741
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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 = [
|
|
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 = [
|
|
7278
|
-
if (url) conditions.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
8361
|
-
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(
|
|
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
|
|
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(
|
|
8602
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
8603
|
-
app.db.delete(gaAiReferrals).where(
|
|
8604
|
-
app.db.delete(gaSocialReferrals).where(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 = [
|
|
9029
|
+
const snapshotConditions = [eq20(gaTrafficSnapshots.projectId, project.id)];
|
|
8803
9030
|
if (cutoffDate) snapshotConditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
8804
|
-
const aiConditions = [
|
|
9031
|
+
const aiConditions = [eq20(gaAiReferrals.projectId, project.id)];
|
|
8805
9032
|
if (cutoffDate) aiConditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
8806
|
-
const socialConditions = [
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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 = [
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
9022
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(
|
|
9023
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(
|
|
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
|
-
|
|
9252
|
+
eq20(gaAiReferrals.projectId, project.id),
|
|
9026
9253
|
sql5`${gaAiReferrals.date} >= ${from}`,
|
|
9027
9254
|
sql5`${gaAiReferrals.date} < ${to}`,
|
|
9028
|
-
|
|
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(
|
|
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
|
-
|
|
9267
|
+
eq20(gaAiReferrals.projectId, project.id),
|
|
9041
9268
|
sql5`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
|
|
9042
9269
|
sql5`${gaAiReferrals.date} < ${todayStr}`,
|
|
9043
|
-
|
|
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
|
-
|
|
9273
|
+
eq20(gaAiReferrals.projectId, project.id),
|
|
9047
9274
|
sql5`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
|
|
9048
9275
|
sql5`${gaAiReferrals.date} < ${daysAgo2(7)}`,
|
|
9049
|
-
|
|
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(
|
|
9066
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(
|
|
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 = [
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
11475
|
+
}).where(eq21(ccReleaseSyncs.id, existing.id)).run();
|
|
11249
11476
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
11250
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
11343
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
14862
|
-
const projectCompetitors = this.db.select().from(competitors).where(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
15798
|
-
tx.delete(backlinkSummaries).where(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
16333
|
-
or3(
|
|
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,
|
|
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(
|
|
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
|
|
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/
|
|
17060
|
+
// src/agent/mcp-to-agent-tool.ts
|
|
16683
17061
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
16684
|
-
|
|
16685
|
-
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
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
|
-
|
|
16693
|
-
|
|
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
|
|
16701
|
-
|
|
16702
|
-
|
|
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
|
-
|
|
16712
|
-
|
|
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
|
|
16715
|
-
const
|
|
16716
|
-
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
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
|
|
16737
|
-
const
|
|
16738
|
-
const
|
|
16739
|
-
|
|
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
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
function
|
|
16745
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
18718
|
+
return normalizeDomain3(value);
|
|
18765
18719
|
}
|
|
18766
18720
|
function domainMatches2(candidate, target) {
|
|
18767
|
-
const normalizedCandidate =
|
|
18768
|
-
const normalizedTarget =
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|