@ainyc/canonry 2.9.0 → 2.10.3
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/README.md +3 -0
- package/assets/assets/{index-U1lA1GKP.js → index-VW6VB3cO.js} +115 -114
- package/assets/index.html +1 -1
- package/dist/{chunk-MGBXRWLX.js → chunk-FAP76VXF.js} +671 -216
- package/dist/{chunk-PYHANJ3B.js → chunk-UM6RDSRJ.js} +327 -0
- package/dist/{chunk-FPZUQADO.js → chunk-Z3BWDCBJ.js} +204 -1
- package/dist/cli.js +157 -10
- package/dist/index.js +3 -3
- package/dist/{intelligence-service-2ZABHNR4.js → intelligence-service-54F3NGPM.js} +1 -1
- package/dist/mcp.js +71 -8
- package/package.json +5 -5
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ApiClient,
|
|
7
7
|
AppError,
|
|
8
8
|
CcReleaseSyncStatuses,
|
|
9
|
+
CitationStates,
|
|
9
10
|
MemorySources,
|
|
10
11
|
RunKinds,
|
|
11
12
|
RunStatuses,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
authInvalid,
|
|
17
18
|
authRequired,
|
|
18
19
|
brandKeyFromText,
|
|
20
|
+
buildRunErrorFromMessages,
|
|
19
21
|
categorizeSource,
|
|
20
22
|
categoryLabel,
|
|
21
23
|
competitorBatchRequestSchema,
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
normalizeProjectDomain,
|
|
37
39
|
notFound,
|
|
38
40
|
notImplemented,
|
|
41
|
+
parseRunError,
|
|
39
42
|
parseWindow,
|
|
40
43
|
projectConfigSchema,
|
|
41
44
|
projectUpsertRequestSchema,
|
|
@@ -45,13 +48,14 @@ import {
|
|
|
45
48
|
runTriggerRequestSchema,
|
|
46
49
|
saveConfigPatch,
|
|
47
50
|
scheduleUpsertRequestSchema,
|
|
51
|
+
serializeRunError,
|
|
48
52
|
snapshotRequestSchema,
|
|
49
53
|
unsupportedKind,
|
|
50
54
|
validationError,
|
|
51
55
|
visibilityStateFromAnswerMentioned,
|
|
52
56
|
windowCutoff,
|
|
53
57
|
wordpressEnvSchema
|
|
54
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-Z3BWDCBJ.js";
|
|
55
59
|
import {
|
|
56
60
|
IntelligenceService,
|
|
57
61
|
agentMemory,
|
|
@@ -62,6 +66,10 @@ import {
|
|
|
62
66
|
backlinkSummaries,
|
|
63
67
|
bingCoverageSnapshots,
|
|
64
68
|
bingUrlInspections,
|
|
69
|
+
buildContentGapRows,
|
|
70
|
+
buildContentSourceRows,
|
|
71
|
+
buildContentTargetRows,
|
|
72
|
+
buildInventory,
|
|
65
73
|
ccReleaseSyncs,
|
|
66
74
|
competitors,
|
|
67
75
|
createLogger,
|
|
@@ -76,6 +84,7 @@ import {
|
|
|
76
84
|
gscUrlInspections,
|
|
77
85
|
healthSnapshots,
|
|
78
86
|
insights,
|
|
87
|
+
isBlogShapedQuery,
|
|
79
88
|
keywords,
|
|
80
89
|
notifications,
|
|
81
90
|
parseJsonColumn,
|
|
@@ -84,7 +93,7 @@ import {
|
|
|
84
93
|
runs,
|
|
85
94
|
schedules,
|
|
86
95
|
usageCounters
|
|
87
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-UM6RDSRJ.js";
|
|
88
97
|
|
|
89
98
|
// src/telemetry.ts
|
|
90
99
|
import crypto from "crypto";
|
|
@@ -164,7 +173,7 @@ import crypto28 from "crypto";
|
|
|
164
173
|
import fs12 from "fs";
|
|
165
174
|
import path14 from "path";
|
|
166
175
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
167
|
-
import { eq as
|
|
176
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
168
177
|
import Fastify from "fastify";
|
|
169
178
|
|
|
170
179
|
// ../api-routes/src/auth.ts
|
|
@@ -1043,7 +1052,7 @@ async function runRoutes(app, opts) {
|
|
|
1043
1052
|
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
|
|
1044
1053
|
if (terminalStatuses.has(run.status)) throw runNotCancellable(run.id, run.status);
|
|
1045
1054
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1046
|
-
app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: "Cancelled by user" }).where(eq7(runs.id, run.id)).run();
|
|
1055
|
+
app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: serializeRunError({ message: "Cancelled by user" }) }).where(eq7(runs.id, run.id)).run();
|
|
1047
1056
|
writeAuditLog(app.db, {
|
|
1048
1057
|
projectId: run.projectId,
|
|
1049
1058
|
actor: "api",
|
|
@@ -1080,7 +1089,7 @@ function formatRun(row) {
|
|
|
1080
1089
|
location: row.location,
|
|
1081
1090
|
startedAt: row.startedAt,
|
|
1082
1091
|
finishedAt: row.finishedAt,
|
|
1083
|
-
error: row.error,
|
|
1092
|
+
error: parseRunError(row.error),
|
|
1084
1093
|
createdAt: row.createdAt
|
|
1085
1094
|
};
|
|
1086
1095
|
}
|
|
@@ -2255,6 +2264,20 @@ function buildCategoryCounts(counts) {
|
|
|
2255
2264
|
|
|
2256
2265
|
// ../api-routes/src/intelligence.ts
|
|
2257
2266
|
import { eq as eq11, desc as desc4, and as and2 } from "drizzle-orm";
|
|
2267
|
+
function emptyHealthSnapshot(projectId) {
|
|
2268
|
+
return {
|
|
2269
|
+
id: `no-data:${projectId}`,
|
|
2270
|
+
projectId,
|
|
2271
|
+
runId: null,
|
|
2272
|
+
overallCitedRate: 0,
|
|
2273
|
+
totalPairs: 0,
|
|
2274
|
+
citedPairs: 0,
|
|
2275
|
+
providerBreakdown: {},
|
|
2276
|
+
createdAt: "",
|
|
2277
|
+
status: "no-data",
|
|
2278
|
+
reason: "no-runs-yet"
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2258
2281
|
function mapInsightRow(r) {
|
|
2259
2282
|
return {
|
|
2260
2283
|
id: r.id,
|
|
@@ -2280,7 +2303,8 @@ function mapHealthRow(r) {
|
|
|
2280
2303
|
totalPairs: r.totalPairs,
|
|
2281
2304
|
citedPairs: r.citedPairs,
|
|
2282
2305
|
providerBreakdown: parseJsonColumn(r.providerBreakdown, {}),
|
|
2283
|
-
createdAt: r.createdAt
|
|
2306
|
+
createdAt: r.createdAt,
|
|
2307
|
+
status: "ready"
|
|
2284
2308
|
};
|
|
2285
2309
|
}
|
|
2286
2310
|
async function intelligenceRoutes(app) {
|
|
@@ -2316,7 +2340,7 @@ async function intelligenceRoutes(app) {
|
|
|
2316
2340
|
const project = resolveProject(app.db, request.params.name);
|
|
2317
2341
|
const row = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
|
|
2318
2342
|
if (!row) {
|
|
2319
|
-
|
|
2343
|
+
return reply.send(emptyHealthSnapshot(project.id));
|
|
2320
2344
|
}
|
|
2321
2345
|
return reply.send(mapHealthRow(row));
|
|
2322
2346
|
});
|
|
@@ -2329,6 +2353,337 @@ async function intelligenceRoutes(app) {
|
|
|
2329
2353
|
});
|
|
2330
2354
|
}
|
|
2331
2355
|
|
|
2356
|
+
// ../api-routes/src/content-data.ts
|
|
2357
|
+
import { and as and3, eq as eq12, desc as desc5, inArray as inArray3 } from "drizzle-orm";
|
|
2358
|
+
var RECENT_RUNS_WINDOW = 5;
|
|
2359
|
+
function loadOrchestratorInput(db, project) {
|
|
2360
|
+
const projectId = project.id;
|
|
2361
|
+
const ownDomain = normalizeDomain(project.canonicalDomain);
|
|
2362
|
+
const ownedDomains = parseJsonColumn(project.ownedDomains, []);
|
|
2363
|
+
const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain)]);
|
|
2364
|
+
const trackedKeywords = listKeywords(db, projectId);
|
|
2365
|
+
const candidateQueryStrings = trackedKeywords.filter(isBlogShapedQuery);
|
|
2366
|
+
const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain);
|
|
2367
|
+
const competitorSet = new Set(trackedCompetitors);
|
|
2368
|
+
const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW);
|
|
2369
|
+
const latestRunId = recentRunIds[0] ?? "";
|
|
2370
|
+
const latestRunTimestamp = latestRunId ? lookupRunTimestamp(db, latestRunId) : "";
|
|
2371
|
+
const candidateQueries = buildCandidateQueries({
|
|
2372
|
+
db,
|
|
2373
|
+
projectId,
|
|
2374
|
+
candidateQueryStrings,
|
|
2375
|
+
recentRunIds,
|
|
2376
|
+
latestRunId,
|
|
2377
|
+
ourDomains,
|
|
2378
|
+
competitorSet
|
|
2379
|
+
});
|
|
2380
|
+
const inventory = buildInventory({
|
|
2381
|
+
gscPages: listGscPagesForProject(db, projectId),
|
|
2382
|
+
ga4LandingPages: listGa4LandingPagesForProject(db, projectId),
|
|
2383
|
+
sitemapUrls: [],
|
|
2384
|
+
wpPosts: []
|
|
2385
|
+
});
|
|
2386
|
+
const gaTrafficByPage = buildGaTrafficByPage(db, projectId);
|
|
2387
|
+
const totalAiReferralSessions = sumAiReferralSessions(db, projectId);
|
|
2388
|
+
return {
|
|
2389
|
+
projectId,
|
|
2390
|
+
ownDomain,
|
|
2391
|
+
competitors: trackedCompetitors,
|
|
2392
|
+
candidateQueries,
|
|
2393
|
+
inventory,
|
|
2394
|
+
wpSchemaAudit: /* @__PURE__ */ new Map(),
|
|
2395
|
+
gaTrafficByPage,
|
|
2396
|
+
totalAiReferralSessions,
|
|
2397
|
+
latestRunId,
|
|
2398
|
+
latestRunTimestamp,
|
|
2399
|
+
inProgressActions: /* @__PURE__ */ new Map()
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
function listKeywords(db, projectId) {
|
|
2403
|
+
const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq12(keywords.projectId, projectId)).all();
|
|
2404
|
+
return rows.map((r) => r.text);
|
|
2405
|
+
}
|
|
2406
|
+
function listCompetitorDomains(db, projectId) {
|
|
2407
|
+
const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq12(competitors.projectId, projectId)).all();
|
|
2408
|
+
return rows.map((r) => r.domain);
|
|
2409
|
+
}
|
|
2410
|
+
function listRecentAnswerVisibilityRunIds(db, projectId, limit) {
|
|
2411
|
+
const rows = db.select({ id: runs.id }).from(runs).where(
|
|
2412
|
+
and3(
|
|
2413
|
+
eq12(runs.projectId, projectId),
|
|
2414
|
+
eq12(runs.kind, RunKinds["answer-visibility"]),
|
|
2415
|
+
// Queued/running/failed/cancelled runs may have partial or no
|
|
2416
|
+
// snapshots; including them risks pointing latestRunId at a run with
|
|
2417
|
+
// no usable evidence.
|
|
2418
|
+
inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
|
|
2419
|
+
)
|
|
2420
|
+
).orderBy(desc5(runs.createdAt)).limit(limit).all();
|
|
2421
|
+
return rows.map((r) => r.id);
|
|
2422
|
+
}
|
|
2423
|
+
function lookupRunTimestamp(db, runId) {
|
|
2424
|
+
const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq12(runs.id, runId)).get();
|
|
2425
|
+
return row?.createdAt ?? "";
|
|
2426
|
+
}
|
|
2427
|
+
function listGscPagesForProject(db, projectId) {
|
|
2428
|
+
const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq12(gscSearchData.projectId, projectId)).all();
|
|
2429
|
+
return rows.map((r) => r.page);
|
|
2430
|
+
}
|
|
2431
|
+
function listGa4LandingPagesForProject(db, projectId) {
|
|
2432
|
+
const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
|
|
2433
|
+
return rows.map((r) => r.landingPage);
|
|
2434
|
+
}
|
|
2435
|
+
function buildGaTrafficByPage(db, projectId) {
|
|
2436
|
+
const rows = db.select({
|
|
2437
|
+
landingPage: gaTrafficSnapshots.landingPage,
|
|
2438
|
+
sessions: gaTrafficSnapshots.sessions
|
|
2439
|
+
}).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
|
|
2440
|
+
const map = /* @__PURE__ */ new Map();
|
|
2441
|
+
for (const row of rows) {
|
|
2442
|
+
const path15 = extractPath(row.landingPage);
|
|
2443
|
+
if (!path15) continue;
|
|
2444
|
+
map.set(path15, (map.get(path15) ?? 0) + (row.sessions ?? 0));
|
|
2445
|
+
}
|
|
2446
|
+
return map;
|
|
2447
|
+
}
|
|
2448
|
+
function sumAiReferralSessions(db, projectId) {
|
|
2449
|
+
const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq12(gaAiReferrals.projectId, projectId)).all();
|
|
2450
|
+
return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
|
|
2451
|
+
}
|
|
2452
|
+
function buildCandidateQueries(opts) {
|
|
2453
|
+
if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
|
|
2454
|
+
return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
|
|
2455
|
+
}
|
|
2456
|
+
const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq12(keywords.projectId, opts.projectId)).all();
|
|
2457
|
+
const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
|
|
2458
|
+
const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
|
|
2459
|
+
const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
|
|
2460
|
+
const snapshotsByKeyword = /* @__PURE__ */ new Map();
|
|
2461
|
+
for (const row of snapshotRows) {
|
|
2462
|
+
const list = snapshotsByKeyword.get(row.keywordId) ?? [];
|
|
2463
|
+
list.push(row);
|
|
2464
|
+
snapshotsByKeyword.set(row.keywordId, list);
|
|
2465
|
+
}
|
|
2466
|
+
const gscRows = opts.db.select().from(gscSearchData).where(eq12(gscSearchData.projectId, opts.projectId)).all();
|
|
2467
|
+
const gscByQuery = aggregateGscByQuery(gscRows);
|
|
2468
|
+
return opts.candidateQueryStrings.map((query) => {
|
|
2469
|
+
const keywordId = keywordIdByText.get(query);
|
|
2470
|
+
const snaps = keywordId ? snapshotsByKeyword.get(keywordId) ?? [] : [];
|
|
2471
|
+
const gsc = gscByQuery.get(query) ?? null;
|
|
2472
|
+
return aggregateCandidate({
|
|
2473
|
+
query,
|
|
2474
|
+
snapshots: snaps,
|
|
2475
|
+
gsc,
|
|
2476
|
+
ourDomains: opts.ourDomains,
|
|
2477
|
+
competitorSet: opts.competitorSet,
|
|
2478
|
+
latestRunId: opts.latestRunId
|
|
2479
|
+
});
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
function aggregateGscByQuery(rows) {
|
|
2483
|
+
const byQuery = /* @__PURE__ */ new Map();
|
|
2484
|
+
for (const r of rows) {
|
|
2485
|
+
const existing = byQuery.get(r.query);
|
|
2486
|
+
const candidate = {
|
|
2487
|
+
// GSC stores `page` as a full URL for url-prefix properties; normalize to
|
|
2488
|
+
// a path so it can be joined against `gaTrafficByPage` (which is keyed by
|
|
2489
|
+
// path) and so `ourBestPage.url` / `targetRef` stay consistent regardless
|
|
2490
|
+
// of whether the page is sourced from GSC or from inventory.
|
|
2491
|
+
page: extractPath(r.page),
|
|
2492
|
+
position: Number(r.position) || 0,
|
|
2493
|
+
impressions: r.impressions,
|
|
2494
|
+
clicks: r.clicks,
|
|
2495
|
+
ctr: Number(r.ctr) || 0
|
|
2496
|
+
};
|
|
2497
|
+
if (!existing) {
|
|
2498
|
+
byQuery.set(r.query, candidate);
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
if (candidate.impressions > existing.impressions) {
|
|
2502
|
+
byQuery.set(r.query, candidate);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return byQuery;
|
|
2506
|
+
}
|
|
2507
|
+
function aggregateCandidate(opts) {
|
|
2508
|
+
const totalSnaps = opts.snapshots.length;
|
|
2509
|
+
if (totalSnaps === 0) {
|
|
2510
|
+
return {
|
|
2511
|
+
...emptyCandidate(opts.query),
|
|
2512
|
+
gscPage: opts.gsc?.page ?? null,
|
|
2513
|
+
gscPosition: opts.gsc ? opts.gsc.position : null,
|
|
2514
|
+
gscImpressions: opts.gsc?.impressions ?? 0,
|
|
2515
|
+
gscClicks: opts.gsc?.clicks ?? 0,
|
|
2516
|
+
gscCtr: opts.gsc?.ctr ?? 0
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
const citedCount = opts.snapshots.filter((s) => s.citationState === CitationStates.cited).length;
|
|
2520
|
+
const ourCitedRate = citedCount / totalSnaps;
|
|
2521
|
+
const recentMissRate = 1 - ourCitedRate;
|
|
2522
|
+
const competitorTally = /* @__PURE__ */ new Map();
|
|
2523
|
+
const competitorGroundingTally = /* @__PURE__ */ new Map();
|
|
2524
|
+
const ourGroundingTally = /* @__PURE__ */ new Map();
|
|
2525
|
+
let ourCitedInLatestRun = false;
|
|
2526
|
+
for (const snap of opts.snapshots) {
|
|
2527
|
+
const isLatestRun = snap.runId === opts.latestRunId;
|
|
2528
|
+
const competitorOverlap = parseJsonColumn(snap.competitorOverlap, []);
|
|
2529
|
+
for (const domain of competitorOverlap) {
|
|
2530
|
+
const normalized = normalizeDomain(domain);
|
|
2531
|
+
if (!opts.competitorSet.has(normalized)) continue;
|
|
2532
|
+
competitorTally.set(normalized, (competitorTally.get(normalized) ?? 0) + 1);
|
|
2533
|
+
}
|
|
2534
|
+
const grounding = extractGroundingSources(snap.rawResponse);
|
|
2535
|
+
for (const g of grounding) {
|
|
2536
|
+
const domain = normalizeDomain(extractHostFromUri(g.uri));
|
|
2537
|
+
if (!domain) continue;
|
|
2538
|
+
if (opts.ourDomains.has(domain)) {
|
|
2539
|
+
if (isLatestRun) ourCitedInLatestRun = true;
|
|
2540
|
+
recordGroundingHit(ourGroundingTally, g, domain, snap.provider);
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
if (!opts.competitorSet.has(domain)) continue;
|
|
2544
|
+
recordGroundingHit(competitorGroundingTally, g, domain, snap.provider);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return {
|
|
2548
|
+
query: opts.query,
|
|
2549
|
+
gscPage: opts.gsc?.page ?? null,
|
|
2550
|
+
gscPosition: opts.gsc ? opts.gsc.position : null,
|
|
2551
|
+
gscImpressions: opts.gsc?.impressions ?? 0,
|
|
2552
|
+
gscClicks: opts.gsc?.clicks ?? 0,
|
|
2553
|
+
gscCtr: opts.gsc?.ctr ?? 0,
|
|
2554
|
+
ourCitedRate,
|
|
2555
|
+
ourCitedInLatestRun,
|
|
2556
|
+
competitorDomains: Array.from(competitorTally.keys()),
|
|
2557
|
+
competitorCitationCount: Array.from(competitorTally.values()).reduce((a, b) => a + b, 0),
|
|
2558
|
+
recentMissRate,
|
|
2559
|
+
ourGroundingUrls: Array.from(ourGroundingTally.values()),
|
|
2560
|
+
competitorGroundingUrls: Array.from(competitorGroundingTally.values()),
|
|
2561
|
+
runsOfHistory: new Set(opts.snapshots.map((s) => s.runId)).size
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
function recordGroundingHit(tally, g, domain, provider) {
|
|
2565
|
+
const existing = tally.get(g.uri);
|
|
2566
|
+
if (existing) {
|
|
2567
|
+
existing.citationCount += 1;
|
|
2568
|
+
if (provider && !existing.providers.includes(provider)) {
|
|
2569
|
+
existing.providers.push(provider);
|
|
2570
|
+
}
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
tally.set(g.uri, {
|
|
2574
|
+
uri: g.uri,
|
|
2575
|
+
title: g.title,
|
|
2576
|
+
domain,
|
|
2577
|
+
citationCount: 1,
|
|
2578
|
+
providers: provider ? [provider] : []
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
function emptyCandidate(query) {
|
|
2582
|
+
return {
|
|
2583
|
+
query,
|
|
2584
|
+
gscPage: null,
|
|
2585
|
+
gscPosition: null,
|
|
2586
|
+
gscImpressions: 0,
|
|
2587
|
+
gscClicks: 0,
|
|
2588
|
+
gscCtr: 0,
|
|
2589
|
+
ourCitedRate: 0,
|
|
2590
|
+
ourCitedInLatestRun: false,
|
|
2591
|
+
competitorDomains: [],
|
|
2592
|
+
competitorCitationCount: 0,
|
|
2593
|
+
recentMissRate: 0,
|
|
2594
|
+
ourGroundingUrls: [],
|
|
2595
|
+
competitorGroundingUrls: [],
|
|
2596
|
+
runsOfHistory: 0
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
function extractGroundingSources(rawResponse) {
|
|
2600
|
+
if (!rawResponse) return [];
|
|
2601
|
+
try {
|
|
2602
|
+
const parsed = JSON.parse(rawResponse);
|
|
2603
|
+
if (parsed && typeof parsed === "object" && "groundingSources" in parsed) {
|
|
2604
|
+
const grounding = parsed.groundingSources;
|
|
2605
|
+
if (Array.isArray(grounding)) {
|
|
2606
|
+
return grounding.filter(
|
|
2607
|
+
(g) => typeof g === "object" && g !== null && typeof g.uri === "string"
|
|
2608
|
+
).map((g) => ({ uri: g.uri, title: g.title ?? "" }));
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
} catch {
|
|
2612
|
+
}
|
|
2613
|
+
return [];
|
|
2614
|
+
}
|
|
2615
|
+
function extractHostFromUri(uri) {
|
|
2616
|
+
try {
|
|
2617
|
+
return new URL(uri).hostname;
|
|
2618
|
+
} catch {
|
|
2619
|
+
return "";
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function normalizeDomain(domain) {
|
|
2623
|
+
return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
2624
|
+
}
|
|
2625
|
+
function extractPath(url) {
|
|
2626
|
+
if (!url) return "";
|
|
2627
|
+
const match = /^https?:\/\/[^/]+(.*)$/.exec(url.trim());
|
|
2628
|
+
const path15 = match ? match[1] : url.trim();
|
|
2629
|
+
const stripped = path15.replace(/\/+$/, "");
|
|
2630
|
+
return stripped || "/";
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// ../api-routes/src/content.ts
|
|
2634
|
+
async function contentRoutes(app) {
|
|
2635
|
+
app.get("/projects/:name/content/targets", async (request) => {
|
|
2636
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2637
|
+
const includeInProgress = request.query["include-in-progress"] === "true";
|
|
2638
|
+
const limit = parseLimitParam(request.query.limit);
|
|
2639
|
+
const input = loadOrchestratorInput(app.db, project);
|
|
2640
|
+
let rows = buildContentTargetRows(input);
|
|
2641
|
+
if (!includeInProgress) {
|
|
2642
|
+
rows = rows.filter((r) => r.existingAction === null);
|
|
2643
|
+
}
|
|
2644
|
+
if (limit !== void 0) {
|
|
2645
|
+
rows = rows.slice(0, limit);
|
|
2646
|
+
}
|
|
2647
|
+
const response = {
|
|
2648
|
+
targets: rows,
|
|
2649
|
+
contextMetrics: {
|
|
2650
|
+
totalAiReferralSessions: input.totalAiReferralSessions,
|
|
2651
|
+
latestRunId: input.latestRunId,
|
|
2652
|
+
runTimestamp: input.latestRunTimestamp
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
return response;
|
|
2656
|
+
});
|
|
2657
|
+
app.get("/projects/:name/content/sources", async (request) => {
|
|
2658
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2659
|
+
const input = loadOrchestratorInput(app.db, project);
|
|
2660
|
+
const rows = buildContentSourceRows(input);
|
|
2661
|
+
const response = {
|
|
2662
|
+
sources: rows,
|
|
2663
|
+
latestRunId: input.latestRunId
|
|
2664
|
+
};
|
|
2665
|
+
return response;
|
|
2666
|
+
});
|
|
2667
|
+
app.get("/projects/:name/content/gaps", async (request) => {
|
|
2668
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2669
|
+
const input = loadOrchestratorInput(app.db, project);
|
|
2670
|
+
const rows = buildContentGapRows(input);
|
|
2671
|
+
const response = {
|
|
2672
|
+
gaps: rows,
|
|
2673
|
+
latestRunId: input.latestRunId
|
|
2674
|
+
};
|
|
2675
|
+
return response;
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
function parseLimitParam(raw) {
|
|
2679
|
+
if (raw === void 0) return void 0;
|
|
2680
|
+
const parsed = Number(raw);
|
|
2681
|
+
if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
|
|
2682
|
+
throw validationError('"limit" must be a non-negative integer');
|
|
2683
|
+
}
|
|
2684
|
+
return parsed;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2332
2687
|
// ../api-routes/src/openapi.ts
|
|
2333
2688
|
var stringSchema = { type: "string" };
|
|
2334
2689
|
var booleanSchema = { type: "boolean" };
|
|
@@ -4533,10 +4888,11 @@ var routeCatalog = [
|
|
|
4533
4888
|
method: "get",
|
|
4534
4889
|
path: "/api/v1/projects/{name}/health/latest",
|
|
4535
4890
|
summary: "Get latest health snapshot",
|
|
4891
|
+
description: 'Returns the latest health snapshot. Always 200 once the project exists: when no snapshot exists yet (newly-created project, or only failed runs), the response carries `status: "no-data"` with `reason: "no-runs-yet"` and zeroed metrics. Real snapshots carry `status: "ready"`.',
|
|
4536
4892
|
tags: ["intelligence"],
|
|
4537
4893
|
parameters: [nameParameter],
|
|
4538
4894
|
responses: {
|
|
4539
|
-
200: { description: "Health snapshot returned." },
|
|
4895
|
+
200: { description: "Health snapshot or no-data sentinel returned." },
|
|
4540
4896
|
404: { description: "Project not found." }
|
|
4541
4897
|
}
|
|
4542
4898
|
},
|
|
@@ -4554,6 +4910,48 @@ var routeCatalog = [
|
|
|
4554
4910
|
404: { description: "Project not found." }
|
|
4555
4911
|
}
|
|
4556
4912
|
},
|
|
4913
|
+
// Content opportunity engine
|
|
4914
|
+
{
|
|
4915
|
+
method: "get",
|
|
4916
|
+
path: "/api/v1/projects/{name}/content/targets",
|
|
4917
|
+
summary: "Ranked, action-typed content opportunities",
|
|
4918
|
+
description: "Returns the canonical opportunity list. Each row is `{query, action, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence, existingAction?}`. Hides rows with in-progress actions by default; pass `?include-in-progress=true` to include them annotated.",
|
|
4919
|
+
tags: ["content"],
|
|
4920
|
+
parameters: [
|
|
4921
|
+
nameParameter,
|
|
4922
|
+
{ name: "limit", in: "query", description: "Max rows returned.", schema: stringSchema },
|
|
4923
|
+
{ name: "include-in-progress", in: "query", description: "Include rows with in-flight tracked actions.", schema: stringSchema }
|
|
4924
|
+
],
|
|
4925
|
+
responses: {
|
|
4926
|
+
200: { description: "Targets returned." },
|
|
4927
|
+
400: { description: "Invalid limit." },
|
|
4928
|
+
404: { description: "Project not found." }
|
|
4929
|
+
}
|
|
4930
|
+
},
|
|
4931
|
+
{
|
|
4932
|
+
method: "get",
|
|
4933
|
+
path: "/api/v1/projects/{name}/content/sources",
|
|
4934
|
+
summary: "URL-level competitive grounding-source map per query",
|
|
4935
|
+
description: "Returns one row per blog-shaped query containing the grounding URLs the LLM cited. Distinguishes our domain (isOurDomain) from competitor URLs (isCompetitor). Pure DB read \u2014 canonry surfaces URLs but never fetches them.",
|
|
4936
|
+
tags: ["content"],
|
|
4937
|
+
parameters: [nameParameter],
|
|
4938
|
+
responses: {
|
|
4939
|
+
200: { description: "Sources returned." },
|
|
4940
|
+
404: { description: "Project not found." }
|
|
4941
|
+
}
|
|
4942
|
+
},
|
|
4943
|
+
{
|
|
4944
|
+
method: "get",
|
|
4945
|
+
path: "/api/v1/projects/{name}/content/gaps",
|
|
4946
|
+
summary: "Queries where competitors are cited but you are not",
|
|
4947
|
+
description: "Returns gap rows ranked by miss rate then by competitor count. Excludes queries with no competitor citations and queries where our cited rate is 100%.",
|
|
4948
|
+
tags: ["content"],
|
|
4949
|
+
parameters: [nameParameter],
|
|
4950
|
+
responses: {
|
|
4951
|
+
200: { description: "Gaps returned." },
|
|
4952
|
+
404: { description: "Project not found." }
|
|
4953
|
+
}
|
|
4954
|
+
},
|
|
4557
4955
|
{
|
|
4558
4956
|
method: "get",
|
|
4559
4957
|
path: "/api/v1/backlinks/status",
|
|
@@ -5012,7 +5410,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
5012
5410
|
|
|
5013
5411
|
// ../api-routes/src/schedules.ts
|
|
5014
5412
|
import crypto11 from "crypto";
|
|
5015
|
-
import { eq as
|
|
5413
|
+
import { eq as eq13 } from "drizzle-orm";
|
|
5016
5414
|
async function scheduleRoutes(app, opts) {
|
|
5017
5415
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
5018
5416
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -5055,7 +5453,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
5055
5453
|
}
|
|
5056
5454
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5057
5455
|
const enabledInt = enabled === false ? 0 : 1;
|
|
5058
|
-
const existing = app.db.select().from(schedules).where(
|
|
5456
|
+
const existing = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
|
|
5059
5457
|
if (existing) {
|
|
5060
5458
|
app.db.update(schedules).set({
|
|
5061
5459
|
cronExpr,
|
|
@@ -5064,7 +5462,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
5064
5462
|
providers: JSON.stringify(providers),
|
|
5065
5463
|
enabled: enabledInt,
|
|
5066
5464
|
updatedAt: now
|
|
5067
|
-
}).where(
|
|
5465
|
+
}).where(eq13(schedules.id, existing.id)).run();
|
|
5068
5466
|
} else {
|
|
5069
5467
|
app.db.insert(schedules).values({
|
|
5070
5468
|
id: crypto11.randomUUID(),
|
|
@@ -5086,12 +5484,12 @@ async function scheduleRoutes(app, opts) {
|
|
|
5086
5484
|
diff: { cronExpr, preset, timezone, providers }
|
|
5087
5485
|
});
|
|
5088
5486
|
opts.onScheduleUpdated?.("upsert", project.id);
|
|
5089
|
-
const schedule = app.db.select().from(schedules).where(
|
|
5487
|
+
const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
|
|
5090
5488
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
5091
5489
|
});
|
|
5092
5490
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
5093
5491
|
const project = resolveProject(app.db, request.params.name);
|
|
5094
|
-
const schedule = app.db.select().from(schedules).where(
|
|
5492
|
+
const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
|
|
5095
5493
|
if (!schedule) {
|
|
5096
5494
|
throw notFound("Schedule", request.params.name);
|
|
5097
5495
|
}
|
|
@@ -5099,11 +5497,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
5099
5497
|
});
|
|
5100
5498
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
5101
5499
|
const project = resolveProject(app.db, request.params.name);
|
|
5102
|
-
const schedule = app.db.select().from(schedules).where(
|
|
5500
|
+
const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
|
|
5103
5501
|
if (!schedule) {
|
|
5104
5502
|
throw notFound("Schedule", request.params.name);
|
|
5105
5503
|
}
|
|
5106
|
-
app.db.delete(schedules).where(
|
|
5504
|
+
app.db.delete(schedules).where(eq13(schedules.id, schedule.id)).run();
|
|
5107
5505
|
writeAuditLog(app.db, {
|
|
5108
5506
|
projectId: project.id,
|
|
5109
5507
|
actor: "api",
|
|
@@ -5133,7 +5531,7 @@ function formatSchedule(row) {
|
|
|
5133
5531
|
|
|
5134
5532
|
// ../api-routes/src/notifications.ts
|
|
5135
5533
|
import crypto12 from "crypto";
|
|
5136
|
-
import { eq as
|
|
5534
|
+
import { eq as eq14 } from "drizzle-orm";
|
|
5137
5535
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
5138
5536
|
async function notificationRoutes(app) {
|
|
5139
5537
|
app.get("/notifications/events", async (_request, reply) => {
|
|
@@ -5172,22 +5570,22 @@ async function notificationRoutes(app) {
|
|
|
5172
5570
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
5173
5571
|
});
|
|
5174
5572
|
return reply.status(201).send({
|
|
5175
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
5573
|
+
...formatNotification(app.db.select().from(notifications).where(eq14(notifications.id, id)).get()),
|
|
5176
5574
|
webhookSecret
|
|
5177
5575
|
});
|
|
5178
5576
|
});
|
|
5179
5577
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
5180
5578
|
const project = resolveProject(app.db, request.params.name);
|
|
5181
|
-
const rows = app.db.select().from(notifications).where(
|
|
5579
|
+
const rows = app.db.select().from(notifications).where(eq14(notifications.projectId, project.id)).all();
|
|
5182
5580
|
return reply.send(rows.map(formatNotification));
|
|
5183
5581
|
});
|
|
5184
5582
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
5185
5583
|
const project = resolveProject(app.db, request.params.name);
|
|
5186
|
-
const notification = app.db.select().from(notifications).where(
|
|
5584
|
+
const notification = app.db.select().from(notifications).where(eq14(notifications.id, request.params.id)).get();
|
|
5187
5585
|
if (!notification || notification.projectId !== project.id) {
|
|
5188
5586
|
throw notFound("Notification", request.params.id);
|
|
5189
5587
|
}
|
|
5190
|
-
app.db.delete(notifications).where(
|
|
5588
|
+
app.db.delete(notifications).where(eq14(notifications.id, notification.id)).run();
|
|
5191
5589
|
writeAuditLog(app.db, {
|
|
5192
5590
|
projectId: project.id,
|
|
5193
5591
|
actor: "api",
|
|
@@ -5199,7 +5597,7 @@ async function notificationRoutes(app) {
|
|
|
5199
5597
|
});
|
|
5200
5598
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
5201
5599
|
const project = resolveProject(app.db, request.params.name);
|
|
5202
|
-
const notification = app.db.select().from(notifications).where(
|
|
5600
|
+
const notification = app.db.select().from(notifications).where(eq14(notifications.id, request.params.id)).get();
|
|
5203
5601
|
if (!notification || notification.projectId !== project.id) {
|
|
5204
5602
|
throw notFound("Notification", request.params.id);
|
|
5205
5603
|
}
|
|
@@ -5252,7 +5650,7 @@ function formatNotification(row) {
|
|
|
5252
5650
|
|
|
5253
5651
|
// ../api-routes/src/google.ts
|
|
5254
5652
|
import crypto14 from "crypto";
|
|
5255
|
-
import { eq as
|
|
5653
|
+
import { eq as eq15, and as and4, desc as desc6, sql as sql3 } from "drizzle-orm";
|
|
5256
5654
|
|
|
5257
5655
|
// ../integration-google/src/constants.ts
|
|
5258
5656
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -6356,20 +6754,20 @@ async function googleRoutes(app, opts) {
|
|
|
6356
6754
|
if (opts.onGscSyncRequested) {
|
|
6357
6755
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
6358
6756
|
}
|
|
6359
|
-
const run = app.db.select().from(runs).where(
|
|
6757
|
+
const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
6360
6758
|
return run;
|
|
6361
6759
|
});
|
|
6362
6760
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
6363
6761
|
const project = resolveProject(app.db, request.params.name);
|
|
6364
6762
|
const { startDate, endDate, query, page, limit } = request.query;
|
|
6365
6763
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
6366
|
-
const conditions = [
|
|
6764
|
+
const conditions = [eq15(gscSearchData.projectId, project.id)];
|
|
6367
6765
|
if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
|
|
6368
6766
|
else if (cutoffDate) conditions.push(sql3`${gscSearchData.date} >= ${cutoffDate}`);
|
|
6369
6767
|
if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
|
|
6370
6768
|
if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
6371
6769
|
if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
6372
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
6770
|
+
const rows = app.db.select().from(gscSearchData).where(and4(...conditions)).orderBy(desc6(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
6373
6771
|
return rows.map((r) => ({
|
|
6374
6772
|
date: r.date,
|
|
6375
6773
|
query: r.query,
|
|
@@ -6441,9 +6839,9 @@ async function googleRoutes(app, opts) {
|
|
|
6441
6839
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
6442
6840
|
const project = resolveProject(app.db, request.params.name);
|
|
6443
6841
|
const { url, limit } = request.query;
|
|
6444
|
-
const conditions = [
|
|
6445
|
-
if (url) conditions.push(
|
|
6446
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
6842
|
+
const conditions = [eq15(gscUrlInspections.projectId, project.id)];
|
|
6843
|
+
if (url) conditions.push(eq15(gscUrlInspections.url, url));
|
|
6844
|
+
const rows = app.db.select().from(gscUrlInspections).where(and4(...conditions)).orderBy(desc6(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
6447
6845
|
return rows.map((r) => ({
|
|
6448
6846
|
id: r.id,
|
|
6449
6847
|
url: r.url,
|
|
@@ -6462,7 +6860,7 @@ async function googleRoutes(app, opts) {
|
|
|
6462
6860
|
});
|
|
6463
6861
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
6464
6862
|
const project = resolveProject(app.db, request.params.name);
|
|
6465
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
6863
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
|
|
6466
6864
|
const byUrl = /* @__PURE__ */ new Map();
|
|
6467
6865
|
for (const row of allInspections) {
|
|
6468
6866
|
const existing = byUrl.get(row.url);
|
|
@@ -6490,7 +6888,7 @@ async function googleRoutes(app, opts) {
|
|
|
6490
6888
|
});
|
|
6491
6889
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
6492
6890
|
const project = resolveProject(app.db, request.params.name);
|
|
6493
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
6891
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
|
|
6494
6892
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
6495
6893
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6496
6894
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -6587,7 +6985,7 @@ async function googleRoutes(app, opts) {
|
|
|
6587
6985
|
const project = resolveProject(app.db, request.params.name);
|
|
6588
6986
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
6589
6987
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
6590
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
6988
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq15(gscCoverageSnapshots.projectId, project.id)).orderBy(desc6(gscCoverageSnapshots.date)).limit(limit).all();
|
|
6591
6989
|
return rows.map((r) => ({
|
|
6592
6990
|
date: r.date,
|
|
6593
6991
|
indexed: r.indexed,
|
|
@@ -6647,7 +7045,7 @@ async function googleRoutes(app, opts) {
|
|
|
6647
7045
|
if (opts.onInspectSitemapRequested) {
|
|
6648
7046
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
6649
7047
|
}
|
|
6650
|
-
const run = app.db.select().from(runs).where(
|
|
7048
|
+
const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
6651
7049
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
6652
7050
|
});
|
|
6653
7051
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -6674,7 +7072,7 @@ async function googleRoutes(app, opts) {
|
|
|
6674
7072
|
if (opts.onInspectSitemapRequested) {
|
|
6675
7073
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
6676
7074
|
}
|
|
6677
|
-
const run = app.db.select().from(runs).where(
|
|
7075
|
+
const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
6678
7076
|
return run;
|
|
6679
7077
|
});
|
|
6680
7078
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -6721,7 +7119,7 @@ async function googleRoutes(app, opts) {
|
|
|
6721
7119
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
6722
7120
|
let urlsToNotify = request.body?.urls ?? [];
|
|
6723
7121
|
if (request.body?.allUnindexed) {
|
|
6724
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7122
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
|
|
6725
7123
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6726
7124
|
for (const row of allInspections) {
|
|
6727
7125
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -6792,7 +7190,7 @@ async function googleRoutes(app, opts) {
|
|
|
6792
7190
|
|
|
6793
7191
|
// ../api-routes/src/bing.ts
|
|
6794
7192
|
import crypto15 from "crypto";
|
|
6795
|
-
import { eq as
|
|
7193
|
+
import { eq as eq16, and as and5, desc as desc7 } from "drizzle-orm";
|
|
6796
7194
|
|
|
6797
7195
|
// ../integration-bing/src/constants.ts
|
|
6798
7196
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -7105,7 +7503,7 @@ async function bingRoutes(app, opts) {
|
|
|
7105
7503
|
const store = requireConnectionStore();
|
|
7106
7504
|
const project = resolveProject(app.db, request.params.name);
|
|
7107
7505
|
requireConnection(store, project.canonicalDomain);
|
|
7108
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
7506
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq16(bingUrlInspections.projectId, project.id)).orderBy(desc7(bingUrlInspections.inspectedAt)).all();
|
|
7109
7507
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7110
7508
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
7111
7509
|
for (const row of allInspections) {
|
|
@@ -7194,7 +7592,7 @@ async function bingRoutes(app, opts) {
|
|
|
7194
7592
|
const project = resolveProject(app.db, request.params.name);
|
|
7195
7593
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
7196
7594
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
7197
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
7595
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq16(bingCoverageSnapshots.projectId, project.id)).orderBy(desc7(bingCoverageSnapshots.date)).limit(limit).all();
|
|
7198
7596
|
return rows.map((r) => ({
|
|
7199
7597
|
date: r.date,
|
|
7200
7598
|
indexed: r.indexed,
|
|
@@ -7206,8 +7604,8 @@ async function bingRoutes(app, opts) {
|
|
|
7206
7604
|
requireConnectionStore();
|
|
7207
7605
|
const project = resolveProject(app.db, request.params.name);
|
|
7208
7606
|
const { url, limit } = request.query;
|
|
7209
|
-
const whereClause = url ?
|
|
7210
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
7607
|
+
const whereClause = url ? and5(eq16(bingUrlInspections.projectId, project.id), eq16(bingUrlInspections.url, url)) : eq16(bingUrlInspections.projectId, project.id);
|
|
7608
|
+
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc7(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
7211
7609
|
return filtered.map((r) => ({
|
|
7212
7610
|
id: r.id,
|
|
7213
7611
|
url: r.url,
|
|
@@ -7296,7 +7694,7 @@ async function bingRoutes(app, opts) {
|
|
|
7296
7694
|
anchorCount: result.AnchorCount ?? null,
|
|
7297
7695
|
discoveryDate
|
|
7298
7696
|
}).run();
|
|
7299
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
7697
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq16(runs.id, runId)).run();
|
|
7300
7698
|
return {
|
|
7301
7699
|
id,
|
|
7302
7700
|
url,
|
|
@@ -7312,7 +7710,7 @@ async function bingRoutes(app, opts) {
|
|
|
7312
7710
|
} catch (e) {
|
|
7313
7711
|
const msg = e instanceof Error ? e.message : String(e);
|
|
7314
7712
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
7315
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
7713
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq16(runs.id, runId)).run();
|
|
7316
7714
|
throw e;
|
|
7317
7715
|
}
|
|
7318
7716
|
});
|
|
@@ -7339,7 +7737,7 @@ async function bingRoutes(app, opts) {
|
|
|
7339
7737
|
} else {
|
|
7340
7738
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
7341
7739
|
}
|
|
7342
|
-
const run = app.db.select().from(runs).where(
|
|
7740
|
+
const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
|
|
7343
7741
|
return run;
|
|
7344
7742
|
});
|
|
7345
7743
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -7351,7 +7749,7 @@ async function bingRoutes(app, opts) {
|
|
|
7351
7749
|
}
|
|
7352
7750
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
7353
7751
|
if (request.body?.allUnindexed) {
|
|
7354
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
7752
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq16(bingUrlInspections.projectId, project.id)).orderBy(desc7(bingUrlInspections.inspectedAt)).all();
|
|
7355
7753
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7356
7754
|
for (const row of allInspections) {
|
|
7357
7755
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -7438,14 +7836,14 @@ async function bingRoutes(app, opts) {
|
|
|
7438
7836
|
import fs from "fs";
|
|
7439
7837
|
import path from "path";
|
|
7440
7838
|
import os from "os";
|
|
7441
|
-
import { eq as
|
|
7839
|
+
import { eq as eq17, and as and6 } from "drizzle-orm";
|
|
7442
7840
|
function getScreenshotDir() {
|
|
7443
7841
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
7444
7842
|
}
|
|
7445
7843
|
async function cdpRoutes(app, opts) {
|
|
7446
7844
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
7447
7845
|
const { snapshotId } = request.params;
|
|
7448
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
7846
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq17(querySnapshots.id, snapshotId)).get();
|
|
7449
7847
|
if (!snapshot?.screenshotPath) {
|
|
7450
7848
|
const err = notFound("Screenshot", snapshotId);
|
|
7451
7849
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -7511,7 +7909,7 @@ async function cdpRoutes(app, opts) {
|
|
|
7511
7909
|
async (request, reply) => {
|
|
7512
7910
|
const project = resolveProject(app.db, request.params.name);
|
|
7513
7911
|
const { runId } = request.params;
|
|
7514
|
-
const run = app.db.select().from(runs).where(
|
|
7912
|
+
const run = app.db.select().from(runs).where(and6(eq17(runs.id, runId), eq17(runs.projectId, project.id))).get();
|
|
7515
7913
|
if (!run) {
|
|
7516
7914
|
const err = notFound("Run", runId);
|
|
7517
7915
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -7524,8 +7922,8 @@ async function cdpRoutes(app, opts) {
|
|
|
7524
7922
|
citedDomains: querySnapshots.citedDomains,
|
|
7525
7923
|
screenshotPath: querySnapshots.screenshotPath,
|
|
7526
7924
|
rawResponse: querySnapshots.rawResponse
|
|
7527
|
-
}).from(querySnapshots).where(
|
|
7528
|
-
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(
|
|
7925
|
+
}).from(querySnapshots).where(eq17(querySnapshots.runId, runId)).all();
|
|
7926
|
+
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq17(keywords.projectId, project.id)).all();
|
|
7529
7927
|
const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
|
|
7530
7928
|
const byKeyword = /* @__PURE__ */ new Map();
|
|
7531
7929
|
for (const snap of snapshots) {
|
|
@@ -7608,7 +8006,7 @@ async function cdpRoutes(app, opts) {
|
|
|
7608
8006
|
|
|
7609
8007
|
// ../api-routes/src/ga.ts
|
|
7610
8008
|
import crypto16 from "crypto";
|
|
7611
|
-
import { eq as
|
|
8009
|
+
import { eq as eq18, desc as desc8, and as and7, sql as sql4 } from "drizzle-orm";
|
|
7612
8010
|
function gaLog(level, action, ctx) {
|
|
7613
8011
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
7614
8012
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -7765,10 +8163,10 @@ async function ga4Routes(app, opts) {
|
|
|
7765
8163
|
if (!saConn && !oauthConn) {
|
|
7766
8164
|
throw notFound("GA4 connection", project.name);
|
|
7767
8165
|
}
|
|
7768
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
7769
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
7770
|
-
app.db.delete(gaAiReferrals).where(
|
|
7771
|
-
app.db.delete(gaSocialReferrals).where(
|
|
8166
|
+
app.db.delete(gaTrafficSnapshots).where(eq18(gaTrafficSnapshots.projectId, project.id)).run();
|
|
8167
|
+
app.db.delete(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).run();
|
|
8168
|
+
app.db.delete(gaAiReferrals).where(eq18(gaAiReferrals.projectId, project.id)).run();
|
|
8169
|
+
app.db.delete(gaSocialReferrals).where(eq18(gaSocialReferrals.projectId, project.id)).run();
|
|
7772
8170
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
7773
8171
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
7774
8172
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -7789,7 +8187,7 @@ async function ga4Routes(app, opts) {
|
|
|
7789
8187
|
if (!connected) {
|
|
7790
8188
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
7791
8189
|
}
|
|
7792
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8190
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).orderBy(desc8(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
7793
8191
|
return {
|
|
7794
8192
|
connected: true,
|
|
7795
8193
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -7848,8 +8246,8 @@ async function ga4Routes(app, opts) {
|
|
|
7848
8246
|
app.db.transaction((tx) => {
|
|
7849
8247
|
if (syncTraffic) {
|
|
7850
8248
|
tx.delete(gaTrafficSnapshots).where(
|
|
7851
|
-
|
|
7852
|
-
|
|
8249
|
+
and7(
|
|
8250
|
+
eq18(gaTrafficSnapshots.projectId, project.id),
|
|
7853
8251
|
sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
7854
8252
|
sql4`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
7855
8253
|
)
|
|
@@ -7870,8 +8268,8 @@ async function ga4Routes(app, opts) {
|
|
|
7870
8268
|
}
|
|
7871
8269
|
if (syncAi) {
|
|
7872
8270
|
tx.delete(gaAiReferrals).where(
|
|
7873
|
-
|
|
7874
|
-
|
|
8271
|
+
and7(
|
|
8272
|
+
eq18(gaAiReferrals.projectId, project.id),
|
|
7875
8273
|
sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
7876
8274
|
sql4`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
7877
8275
|
)
|
|
@@ -7893,8 +8291,8 @@ async function ga4Routes(app, opts) {
|
|
|
7893
8291
|
}
|
|
7894
8292
|
if (syncSocial) {
|
|
7895
8293
|
tx.delete(gaSocialReferrals).where(
|
|
7896
|
-
|
|
7897
|
-
|
|
8294
|
+
and7(
|
|
8295
|
+
eq18(gaSocialReferrals.projectId, project.id),
|
|
7898
8296
|
sql4`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
7899
8297
|
sql4`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
7900
8298
|
)
|
|
@@ -7915,7 +8313,7 @@ async function ga4Routes(app, opts) {
|
|
|
7915
8313
|
}
|
|
7916
8314
|
}
|
|
7917
8315
|
if (syncSummary) {
|
|
7918
|
-
tx.delete(gaTrafficSummaries).where(
|
|
8316
|
+
tx.delete(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).run();
|
|
7919
8317
|
tx.insert(gaTrafficSummaries).values({
|
|
7920
8318
|
id: crypto16.randomUUID(),
|
|
7921
8319
|
projectId: project.id,
|
|
@@ -7929,7 +8327,7 @@ async function ga4Routes(app, opts) {
|
|
|
7929
8327
|
}).run();
|
|
7930
8328
|
}
|
|
7931
8329
|
});
|
|
7932
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
8330
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq18(runs.id, runId)).run();
|
|
7933
8331
|
const syncedComponents = only ? [only, ...only !== "social" && only !== "ai" && only !== "traffic" ? [] : []] : void 0;
|
|
7934
8332
|
gaLog("info", "sync.complete", {
|
|
7935
8333
|
projectId: project.id,
|
|
@@ -7953,7 +8351,7 @@ async function ga4Routes(app, opts) {
|
|
|
7953
8351
|
} catch (e) {
|
|
7954
8352
|
const msg = e instanceof Error ? e.message : String(e);
|
|
7955
8353
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
7956
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
8354
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
|
|
7957
8355
|
throw e;
|
|
7958
8356
|
}
|
|
7959
8357
|
});
|
|
@@ -7964,38 +8362,38 @@ async function ga4Routes(app, opts) {
|
|
|
7964
8362
|
const window = parseWindow(request.query.window);
|
|
7965
8363
|
const cutoff = windowCutoff(window);
|
|
7966
8364
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
7967
|
-
const snapshotConditions = [
|
|
8365
|
+
const snapshotConditions = [eq18(gaTrafficSnapshots.projectId, project.id)];
|
|
7968
8366
|
if (cutoffDate) snapshotConditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
7969
|
-
const aiConditions = [
|
|
8367
|
+
const aiConditions = [eq18(gaAiReferrals.projectId, project.id)];
|
|
7970
8368
|
if (cutoffDate) aiConditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
7971
|
-
const socialConditions = [
|
|
8369
|
+
const socialConditions = [eq18(gaSocialReferrals.projectId, project.id)];
|
|
7972
8370
|
if (cutoffDate) socialConditions.push(sql4`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
7973
8371
|
const summaryRow = cutoffDate ? app.db.select({
|
|
7974
8372
|
totalSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
7975
8373
|
totalOrganicSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
7976
8374
|
totalUsers: sql4`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
7977
|
-
}).from(gaTrafficSnapshots).where(
|
|
8375
|
+
}).from(gaTrafficSnapshots).where(and7(...snapshotConditions)).get() : app.db.select({
|
|
7978
8376
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
7979
8377
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
7980
8378
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
7981
|
-
}).from(gaTrafficSummaries).where(
|
|
8379
|
+
}).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).get();
|
|
7982
8380
|
const summaryMeta = app.db.select({
|
|
7983
8381
|
periodStart: gaTrafficSummaries.periodStart,
|
|
7984
8382
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
7985
|
-
}).from(gaTrafficSummaries).where(
|
|
8383
|
+
}).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).get();
|
|
7986
8384
|
const rows = app.db.select({
|
|
7987
8385
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
7988
8386
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
7989
8387
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
7990
8388
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
7991
|
-
}).from(gaTrafficSnapshots).where(
|
|
8389
|
+
}).from(gaTrafficSnapshots).where(and7(...snapshotConditions)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
7992
8390
|
const aiReferrals = app.db.select({
|
|
7993
8391
|
source: gaAiReferrals.source,
|
|
7994
8392
|
medium: gaAiReferrals.medium,
|
|
7995
8393
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
7996
8394
|
sessions: sql4`SUM(${gaAiReferrals.sessions})`,
|
|
7997
8395
|
users: sql4`SUM(${gaAiReferrals.users})`
|
|
7998
|
-
}).from(gaAiReferrals).where(
|
|
8396
|
+
}).from(gaAiReferrals).where(and7(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
|
|
7999
8397
|
const aiDeduped = app.db.select({
|
|
8000
8398
|
sessions: sql4`SUM(max_sessions)`,
|
|
8001
8399
|
users: sql4`SUM(max_users)`
|
|
@@ -8015,12 +8413,12 @@ async function ga4Routes(app, opts) {
|
|
|
8015
8413
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
8016
8414
|
sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
|
|
8017
8415
|
users: sql4`SUM(${gaSocialReferrals.users})`
|
|
8018
|
-
}).from(gaSocialReferrals).where(
|
|
8416
|
+
}).from(gaSocialReferrals).where(and7(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql4`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
8019
8417
|
const socialTotals = app.db.select({
|
|
8020
8418
|
sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
|
|
8021
8419
|
users: sql4`SUM(${gaSocialReferrals.users})`
|
|
8022
|
-
}).from(gaSocialReferrals).where(
|
|
8023
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8420
|
+
}).from(gaSocialReferrals).where(and7(...socialConditions)).get();
|
|
8421
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).orderBy(desc8(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8024
8422
|
const total = summaryRow?.totalSessions ?? 0;
|
|
8025
8423
|
return {
|
|
8026
8424
|
totalSessions: total,
|
|
@@ -8067,7 +8465,7 @@ async function ga4Routes(app, opts) {
|
|
|
8067
8465
|
const project = resolveProject(app.db, request.params.name);
|
|
8068
8466
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8069
8467
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
8070
|
-
const conditions = [
|
|
8468
|
+
const conditions = [eq18(gaAiReferrals.projectId, project.id)];
|
|
8071
8469
|
if (cutoffDate) conditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
8072
8470
|
const rows = app.db.select({
|
|
8073
8471
|
date: gaAiReferrals.date,
|
|
@@ -8076,14 +8474,14 @@ async function ga4Routes(app, opts) {
|
|
|
8076
8474
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
8077
8475
|
sessions: gaAiReferrals.sessions,
|
|
8078
8476
|
users: gaAiReferrals.users
|
|
8079
|
-
}).from(gaAiReferrals).where(
|
|
8477
|
+
}).from(gaAiReferrals).where(and7(...conditions)).orderBy(gaAiReferrals.date).all();
|
|
8080
8478
|
return rows;
|
|
8081
8479
|
});
|
|
8082
8480
|
app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
|
|
8083
8481
|
const project = resolveProject(app.db, request.params.name);
|
|
8084
8482
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8085
8483
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
8086
|
-
const conditions = [
|
|
8484
|
+
const conditions = [eq18(gaSocialReferrals.projectId, project.id)];
|
|
8087
8485
|
if (cutoffDate) conditions.push(sql4`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
8088
8486
|
const rows = app.db.select({
|
|
8089
8487
|
date: gaSocialReferrals.date,
|
|
@@ -8092,7 +8490,7 @@ async function ga4Routes(app, opts) {
|
|
|
8092
8490
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
8093
8491
|
sessions: gaSocialReferrals.sessions,
|
|
8094
8492
|
users: gaSocialReferrals.users
|
|
8095
|
-
}).from(gaSocialReferrals).where(
|
|
8493
|
+
}).from(gaSocialReferrals).where(and7(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
8096
8494
|
return rows;
|
|
8097
8495
|
});
|
|
8098
8496
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -8105,8 +8503,8 @@ async function ga4Routes(app, opts) {
|
|
|
8105
8503
|
d.setDate(d.getDate() - n);
|
|
8106
8504
|
return fmt(d);
|
|
8107
8505
|
};
|
|
8108
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
8109
|
-
|
|
8506
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and7(
|
|
8507
|
+
eq18(gaSocialReferrals.projectId, project.id),
|
|
8110
8508
|
sql4`${gaSocialReferrals.date} >= ${from}`,
|
|
8111
8509
|
sql4`${gaSocialReferrals.date} < ${to}`
|
|
8112
8510
|
)).get();
|
|
@@ -8118,16 +8516,16 @@ async function ga4Routes(app, opts) {
|
|
|
8118
8516
|
const sourceCurrent = app.db.select({
|
|
8119
8517
|
source: gaSocialReferrals.source,
|
|
8120
8518
|
sessions: sql4`SUM(${gaSocialReferrals.sessions})`
|
|
8121
|
-
}).from(gaSocialReferrals).where(
|
|
8122
|
-
|
|
8519
|
+
}).from(gaSocialReferrals).where(and7(
|
|
8520
|
+
eq18(gaSocialReferrals.projectId, project.id),
|
|
8123
8521
|
sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
|
|
8124
8522
|
sql4`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
8125
8523
|
)).groupBy(gaSocialReferrals.source).all();
|
|
8126
8524
|
const sourcePrev = app.db.select({
|
|
8127
8525
|
source: gaSocialReferrals.source,
|
|
8128
8526
|
sessions: sql4`SUM(${gaSocialReferrals.sessions})`
|
|
8129
|
-
}).from(gaSocialReferrals).where(
|
|
8130
|
-
|
|
8527
|
+
}).from(gaSocialReferrals).where(and7(
|
|
8528
|
+
eq18(gaSocialReferrals.projectId, project.id),
|
|
8131
8529
|
sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
|
|
8132
8530
|
sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`
|
|
8133
8531
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -8168,15 +8566,15 @@ async function ga4Routes(app, opts) {
|
|
|
8168
8566
|
return fmt(d);
|
|
8169
8567
|
};
|
|
8170
8568
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
8171
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
8172
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
8569
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and7(eq18(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
8570
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and7(eq18(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
8173
8571
|
const sumAi = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(max_sessions), 0)` }).from(sql4`(
|
|
8174
8572
|
SELECT date, source, medium, MAX(sessions) AS max_sessions
|
|
8175
8573
|
FROM ga_ai_referrals
|
|
8176
8574
|
WHERE project_id = ${project.id} AND date >= ${from} AND date < ${to}
|
|
8177
8575
|
GROUP BY date, source, medium
|
|
8178
8576
|
)`).get();
|
|
8179
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
8577
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${from}`, sql4`${gaSocialReferrals.date} < ${to}`)).get();
|
|
8180
8578
|
const todayStr = fmt(today);
|
|
8181
8579
|
const buildTrend = (sum) => {
|
|
8182
8580
|
const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
|
|
@@ -8211,8 +8609,8 @@ async function ga4Routes(app, opts) {
|
|
|
8211
8609
|
}
|
|
8212
8610
|
return mover;
|
|
8213
8611
|
};
|
|
8214
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
8215
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
8612
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql4`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
8613
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
8216
8614
|
return {
|
|
8217
8615
|
total: buildTrend(sumTotal),
|
|
8218
8616
|
organic: buildTrend(sumOrganic),
|
|
@@ -8226,14 +8624,14 @@ async function ga4Routes(app, opts) {
|
|
|
8226
8624
|
const project = resolveProject(app.db, request.params.name);
|
|
8227
8625
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8228
8626
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
8229
|
-
const conditions = [
|
|
8627
|
+
const conditions = [eq18(gaTrafficSnapshots.projectId, project.id)];
|
|
8230
8628
|
if (cutoffDate) conditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
8231
8629
|
const rows = app.db.select({
|
|
8232
8630
|
date: gaTrafficSnapshots.date,
|
|
8233
8631
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8234
8632
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8235
8633
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8236
|
-
}).from(gaTrafficSnapshots).where(
|
|
8634
|
+
}).from(gaTrafficSnapshots).where(and7(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
8237
8635
|
return rows.map((r) => ({
|
|
8238
8636
|
date: r.date,
|
|
8239
8637
|
sessions: r.sessions ?? 0,
|
|
@@ -8249,7 +8647,7 @@ async function ga4Routes(app, opts) {
|
|
|
8249
8647
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8250
8648
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8251
8649
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8252
|
-
}).from(gaTrafficSnapshots).where(
|
|
8650
|
+
}).from(gaTrafficSnapshots).where(eq18(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
8253
8651
|
return {
|
|
8254
8652
|
pages: trafficPages.map((r) => ({
|
|
8255
8653
|
landingPage: r.landingPage,
|
|
@@ -9886,7 +10284,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
9886
10284
|
|
|
9887
10285
|
// ../api-routes/src/backlinks.ts
|
|
9888
10286
|
import crypto18 from "crypto";
|
|
9889
|
-
import { and as
|
|
10287
|
+
import { and as and8, asc as asc2, desc as desc9, eq as eq19, sql as sql5 } from "drizzle-orm";
|
|
9890
10288
|
|
|
9891
10289
|
// ../integration-commoncrawl/src/constants.ts
|
|
9892
10290
|
import os2 from "os";
|
|
@@ -10279,13 +10677,13 @@ function mapRunRow(row) {
|
|
|
10279
10677
|
location: row.location ?? null,
|
|
10280
10678
|
startedAt: row.startedAt ?? null,
|
|
10281
10679
|
finishedAt: row.finishedAt ?? null,
|
|
10282
|
-
error: row.error
|
|
10680
|
+
error: parseRunError(row.error),
|
|
10283
10681
|
createdAt: row.createdAt
|
|
10284
10682
|
};
|
|
10285
10683
|
}
|
|
10286
10684
|
function latestSummaryForProject(db, projectId, release) {
|
|
10287
|
-
const condition = release ?
|
|
10288
|
-
return db.select().from(backlinkSummaries).where(condition).orderBy(
|
|
10685
|
+
const condition = release ? and8(eq19(backlinkSummaries.projectId, projectId), eq19(backlinkSummaries.release, release)) : eq19(backlinkSummaries.projectId, projectId);
|
|
10686
|
+
return db.select().from(backlinkSummaries).where(condition).orderBy(desc9(backlinkSummaries.queriedAt)).limit(1).get();
|
|
10289
10687
|
}
|
|
10290
10688
|
async function backlinksRoutes(app, opts) {
|
|
10291
10689
|
app.get("/backlinks/status", async (_request, reply) => {
|
|
@@ -10314,7 +10712,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
10314
10712
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
10315
10713
|
);
|
|
10316
10714
|
}
|
|
10317
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
10715
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.release, release)).get();
|
|
10318
10716
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10319
10717
|
if (existing) {
|
|
10320
10718
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -10325,9 +10723,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
10325
10723
|
phaseDetail: null,
|
|
10326
10724
|
error: null,
|
|
10327
10725
|
updatedAt: now
|
|
10328
|
-
}).where(
|
|
10726
|
+
}).where(eq19(ccReleaseSyncs.id, existing.id)).run();
|
|
10329
10727
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
10330
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
10728
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.id, existing.id)).get();
|
|
10331
10729
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
10332
10730
|
}
|
|
10333
10731
|
const id = crypto18.randomUUID();
|
|
@@ -10339,15 +10737,15 @@ async function backlinksRoutes(app, opts) {
|
|
|
10339
10737
|
updatedAt: now
|
|
10340
10738
|
}).run();
|
|
10341
10739
|
opts.onReleaseSyncRequested(id, release);
|
|
10342
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
10740
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.id, id)).get();
|
|
10343
10741
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
10344
10742
|
});
|
|
10345
10743
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
10346
|
-
const row = app.db.select().from(ccReleaseSyncs).orderBy(
|
|
10744
|
+
const row = app.db.select().from(ccReleaseSyncs).orderBy(desc9(ccReleaseSyncs.updatedAt)).limit(1).get();
|
|
10347
10745
|
return reply.send(row ? mapSyncRow(row) : null);
|
|
10348
10746
|
});
|
|
10349
10747
|
app.get("/backlinks/syncs", async (_request, reply) => {
|
|
10350
|
-
const rows = app.db.select().from(ccReleaseSyncs).orderBy(
|
|
10748
|
+
const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc9(ccReleaseSyncs.updatedAt)).all();
|
|
10351
10749
|
return reply.send(rows.map(mapSyncRow));
|
|
10352
10750
|
});
|
|
10353
10751
|
app.get("/backlinks/releases", async (_request, reply) => {
|
|
@@ -10390,7 +10788,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
10390
10788
|
createdAt: now
|
|
10391
10789
|
}).run();
|
|
10392
10790
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
10393
|
-
const run = app.db.select().from(runs).where(
|
|
10791
|
+
const run = app.db.select().from(runs).where(eq19(runs.id, runId)).get();
|
|
10394
10792
|
return reply.status(201).send(mapRunRow(run));
|
|
10395
10793
|
});
|
|
10396
10794
|
app.get(
|
|
@@ -10411,15 +10809,15 @@ async function backlinksRoutes(app, opts) {
|
|
|
10411
10809
|
}
|
|
10412
10810
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
10413
10811
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
10414
|
-
const domainCondition =
|
|
10415
|
-
|
|
10416
|
-
|
|
10812
|
+
const domainCondition = and8(
|
|
10813
|
+
eq19(backlinkDomains.projectId, project.id),
|
|
10814
|
+
eq19(backlinkDomains.release, targetRelease)
|
|
10417
10815
|
);
|
|
10418
10816
|
const totalRow = app.db.select({ count: sql5`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
10419
10817
|
const rows = app.db.select({
|
|
10420
10818
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
10421
10819
|
numHosts: backlinkDomains.numHosts
|
|
10422
|
-
}).from(backlinkDomains).where(domainCondition).orderBy(
|
|
10820
|
+
}).from(backlinkDomains).where(domainCondition).orderBy(desc9(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
|
|
10423
10821
|
const response = {
|
|
10424
10822
|
summary: summaryRow ? mapSummaryRow(summaryRow) : null,
|
|
10425
10823
|
total: Number(totalRow?.count ?? 0),
|
|
@@ -10431,7 +10829,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
10431
10829
|
"/projects/:name/backlinks/history",
|
|
10432
10830
|
async (request, reply) => {
|
|
10433
10831
|
const project = resolveProject(app.db, request.params.name);
|
|
10434
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
10832
|
+
const rows = app.db.select().from(backlinkSummaries).where(eq19(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
|
|
10435
10833
|
const response = rows.map((r) => ({
|
|
10436
10834
|
release: r.release,
|
|
10437
10835
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -10504,6 +10902,7 @@ async function apiRoutes(app, opts) {
|
|
|
10504
10902
|
await api.register(historyRoutes);
|
|
10505
10903
|
await api.register(analyticsRoutes);
|
|
10506
10904
|
await api.register(intelligenceRoutes);
|
|
10905
|
+
await api.register(contentRoutes);
|
|
10507
10906
|
await api.register(settingsRoutes, {
|
|
10508
10907
|
providerSummary: opts.providerSummary,
|
|
10509
10908
|
providerAdapters: opts.providerAdapters,
|
|
@@ -12649,7 +13048,7 @@ function hasParsedResponseContent4(rawResponse) {
|
|
|
12649
13048
|
return Array.isArray(nestedResponse.choices) && nestedResponse.choices.length > 0 || Array.isArray(nestedResponse.search_results) && nestedResponse.search_results.length > 0 || Array.isArray(nestedResponse.citations) && nestedResponse.citations.length > 0;
|
|
12650
13049
|
}
|
|
12651
13050
|
function reparseStoredResult4(rawResponse) {
|
|
12652
|
-
const groundingSources =
|
|
13051
|
+
const groundingSources = extractGroundingSources2(rawResponse);
|
|
12653
13052
|
return {
|
|
12654
13053
|
provider: "perplexity",
|
|
12655
13054
|
answerText: extractAnswerText3(rawResponse),
|
|
@@ -12680,7 +13079,7 @@ function extractCitations(rawResponse) {
|
|
|
12680
13079
|
}
|
|
12681
13080
|
return [];
|
|
12682
13081
|
}
|
|
12683
|
-
function
|
|
13082
|
+
function extractGroundingSources2(rawResponse) {
|
|
12684
13083
|
const searchResults = extractSearchResults(rawResponse);
|
|
12685
13084
|
if (searchResults.length > 0) {
|
|
12686
13085
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13025,7 +13424,7 @@ import crypto19 from "crypto";
|
|
|
13025
13424
|
import fs7 from "fs";
|
|
13026
13425
|
import path9 from "path";
|
|
13027
13426
|
import os4 from "os";
|
|
13028
|
-
import { and as
|
|
13427
|
+
import { and as and9, eq as eq20, inArray as inArray4, sql as sql6 } from "drizzle-orm";
|
|
13029
13428
|
|
|
13030
13429
|
// src/citation-utils.ts
|
|
13031
13430
|
function domainMatches(domain, canonicalDomain) {
|
|
@@ -13277,11 +13676,11 @@ var JobRunner = class {
|
|
|
13277
13676
|
this.registry = registry;
|
|
13278
13677
|
}
|
|
13279
13678
|
recoverStaleRuns() {
|
|
13280
|
-
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
13679
|
+
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray4(runs.status, ["running", "queued"])).all();
|
|
13281
13680
|
if (stale.length === 0) return;
|
|
13282
13681
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13283
13682
|
for (const run of stale) {
|
|
13284
|
-
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(
|
|
13683
|
+
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq20(runs.id, run.id)).run();
|
|
13285
13684
|
log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
|
|
13286
13685
|
}
|
|
13287
13686
|
}
|
|
@@ -13309,10 +13708,10 @@ var JobRunner = class {
|
|
|
13309
13708
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
13310
13709
|
}
|
|
13311
13710
|
if (existingRun.status === "queued") {
|
|
13312
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
13711
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and9(eq20(runs.id, runId), eq20(runs.status, "queued"))).run();
|
|
13313
13712
|
}
|
|
13314
13713
|
this.throwIfRunCancelled(runId);
|
|
13315
|
-
const project = this.db.select().from(projects).where(
|
|
13714
|
+
const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
|
|
13316
13715
|
if (!project) {
|
|
13317
13716
|
throw new Error(`Project ${projectId} not found`);
|
|
13318
13717
|
}
|
|
@@ -13332,8 +13731,8 @@ var JobRunner = class {
|
|
|
13332
13731
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
13333
13732
|
}
|
|
13334
13733
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
13335
|
-
projectKeywords = this.db.select().from(keywords).where(
|
|
13336
|
-
const projectCompetitors = this.db.select().from(competitors).where(
|
|
13734
|
+
projectKeywords = this.db.select().from(keywords).where(eq20(keywords.projectId, projectId)).all();
|
|
13735
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq20(competitors.projectId, projectId)).all();
|
|
13337
13736
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
13338
13737
|
const allDomains = effectiveDomains({
|
|
13339
13738
|
canonicalDomain: project.canonicalDomain,
|
|
@@ -13349,7 +13748,7 @@ var JobRunner = class {
|
|
|
13349
13748
|
const todayPeriod = getCurrentUsageDay();
|
|
13350
13749
|
for (const p of activeProviders) {
|
|
13351
13750
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
13352
|
-
const providerUsage = this.db.select().from(usageCounters).where(
|
|
13751
|
+
const providerUsage = this.db.select().from(usageCounters).where(eq20(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
|
|
13353
13752
|
const limit = p.config.quotaPolicy.maxRequestsPerDay;
|
|
13354
13753
|
if (providerUsage + queriesPerProvider > limit) {
|
|
13355
13754
|
throw new Error(
|
|
@@ -13489,13 +13888,13 @@ var JobRunner = class {
|
|
|
13489
13888
|
const allFailed = totalSnapshotsInserted === 0 && providerErrors.size > 0;
|
|
13490
13889
|
const someFailed = providerErrors.size > 0;
|
|
13491
13890
|
if (allFailed) {
|
|
13492
|
-
const errorDetail =
|
|
13493
|
-
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13891
|
+
const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
|
|
13892
|
+
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq20(runs.id, runId)).run();
|
|
13494
13893
|
} else if (someFailed) {
|
|
13495
|
-
const errorDetail =
|
|
13496
|
-
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13894
|
+
const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
|
|
13895
|
+
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq20(runs.id, runId)).run();
|
|
13497
13896
|
} else {
|
|
13498
|
-
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13897
|
+
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
13499
13898
|
}
|
|
13500
13899
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13501
13900
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
@@ -13530,7 +13929,7 @@ var JobRunner = class {
|
|
|
13530
13929
|
status: "failed",
|
|
13531
13930
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13532
13931
|
error: errorMessage
|
|
13533
|
-
}).where(
|
|
13932
|
+
}).where(eq20(runs.id, runId)).run();
|
|
13534
13933
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13535
13934
|
trackEvent("run.completed", {
|
|
13536
13935
|
status: "failed",
|
|
@@ -13573,7 +13972,7 @@ var JobRunner = class {
|
|
|
13573
13972
|
status: runs.status,
|
|
13574
13973
|
finishedAt: runs.finishedAt,
|
|
13575
13974
|
error: runs.error
|
|
13576
|
-
}).from(runs).where(
|
|
13975
|
+
}).from(runs).where(eq20(runs.id, runId)).get();
|
|
13577
13976
|
}
|
|
13578
13977
|
isRunCancelled(runId) {
|
|
13579
13978
|
return this.getRunState(runId)?.status === "cancelled";
|
|
@@ -13589,7 +13988,7 @@ var JobRunner = class {
|
|
|
13589
13988
|
this.db.update(runs).set({
|
|
13590
13989
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13591
13990
|
error: currentRun.error ?? "Cancelled by user"
|
|
13592
|
-
}).where(
|
|
13991
|
+
}).where(eq20(runs.id, runId)).run();
|
|
13593
13992
|
}
|
|
13594
13993
|
trackEvent("run.completed", {
|
|
13595
13994
|
status: "cancelled",
|
|
@@ -13612,7 +14011,7 @@ function getCurrentUsageDay() {
|
|
|
13612
14011
|
|
|
13613
14012
|
// src/gsc-sync.ts
|
|
13614
14013
|
import crypto20 from "crypto";
|
|
13615
|
-
import { eq as
|
|
14014
|
+
import { eq as eq21, and as and10, sql as sql7 } from "drizzle-orm";
|
|
13616
14015
|
var log2 = createLogger("GscSync");
|
|
13617
14016
|
function formatDate2(d) {
|
|
13618
14017
|
return d.toISOString().split("T")[0];
|
|
@@ -13624,13 +14023,13 @@ function daysAgo(n) {
|
|
|
13624
14023
|
}
|
|
13625
14024
|
async function executeGscSync(db, runId, projectId, opts) {
|
|
13626
14025
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13627
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14026
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq21(runs.id, runId)).run();
|
|
13628
14027
|
try {
|
|
13629
14028
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13630
14029
|
if (!googleClientId || !googleClientSecret) {
|
|
13631
14030
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13632
14031
|
}
|
|
13633
|
-
const project = db.select().from(projects).where(
|
|
14032
|
+
const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
|
|
13634
14033
|
if (!project) {
|
|
13635
14034
|
throw new Error(`Project not found: ${projectId}`);
|
|
13636
14035
|
}
|
|
@@ -13664,8 +14063,8 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13664
14063
|
});
|
|
13665
14064
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
13666
14065
|
db.delete(gscSearchData).where(
|
|
13667
|
-
|
|
13668
|
-
|
|
14066
|
+
and10(
|
|
14067
|
+
eq21(gscSearchData.projectId, projectId),
|
|
13669
14068
|
sql7`${gscSearchData.date} >= ${startDate}`,
|
|
13670
14069
|
sql7`${gscSearchData.date} <= ${endDate}`
|
|
13671
14070
|
)
|
|
@@ -13732,7 +14131,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13732
14131
|
log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
|
|
13733
14132
|
}
|
|
13734
14133
|
}
|
|
13735
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14134
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, projectId)).all();
|
|
13736
14135
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13737
14136
|
for (const row of allInspections) {
|
|
13738
14137
|
const existing = latestByUrl.get(row.url);
|
|
@@ -13753,7 +14152,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13753
14152
|
}
|
|
13754
14153
|
}
|
|
13755
14154
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
13756
|
-
db.delete(gscCoverageSnapshots).where(
|
|
14155
|
+
db.delete(gscCoverageSnapshots).where(and10(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13757
14156
|
db.insert(gscCoverageSnapshots).values({
|
|
13758
14157
|
id: crypto20.randomUUID(),
|
|
13759
14158
|
projectId,
|
|
@@ -13764,11 +14163,11 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13764
14163
|
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
13765
14164
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13766
14165
|
}).run();
|
|
13767
|
-
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14166
|
+
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
|
|
13768
14167
|
log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
13769
14168
|
} catch (err) {
|
|
13770
14169
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
13771
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14170
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
|
|
13772
14171
|
log2.error("sync.failed", { runId, projectId, error: errorMsg });
|
|
13773
14172
|
throw err;
|
|
13774
14173
|
}
|
|
@@ -13776,7 +14175,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13776
14175
|
|
|
13777
14176
|
// src/gsc-inspect-sitemap.ts
|
|
13778
14177
|
import crypto21 from "crypto";
|
|
13779
|
-
import { eq as
|
|
14178
|
+
import { eq as eq22, and as and11 } from "drizzle-orm";
|
|
13780
14179
|
|
|
13781
14180
|
// src/sitemap-parser.ts
|
|
13782
14181
|
var log3 = createLogger("SitemapParser");
|
|
@@ -13897,13 +14296,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
|
|
|
13897
14296
|
var log4 = createLogger("InspectSitemap");
|
|
13898
14297
|
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
13899
14298
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13900
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14299
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq22(runs.id, runId)).run();
|
|
13901
14300
|
try {
|
|
13902
14301
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13903
14302
|
if (!googleClientId || !googleClientSecret) {
|
|
13904
14303
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13905
14304
|
}
|
|
13906
|
-
const project = db.select().from(projects).where(
|
|
14305
|
+
const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
13907
14306
|
if (!project) {
|
|
13908
14307
|
throw new Error(`Project not found: ${projectId}`);
|
|
13909
14308
|
}
|
|
@@ -13971,7 +14370,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13971
14370
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
13972
14371
|
}
|
|
13973
14372
|
}
|
|
13974
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14373
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, projectId)).all();
|
|
13975
14374
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13976
14375
|
for (const row of allInspections) {
|
|
13977
14376
|
const existing = latestByUrl.get(row.url);
|
|
@@ -13992,7 +14391,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13992
14391
|
}
|
|
13993
14392
|
}
|
|
13994
14393
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
13995
|
-
db.delete(gscCoverageSnapshots).where(
|
|
14394
|
+
db.delete(gscCoverageSnapshots).where(and11(eq22(gscCoverageSnapshots.projectId, projectId), eq22(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13996
14395
|
db.insert(gscCoverageSnapshots).values({
|
|
13997
14396
|
id: crypto21.randomUUID(),
|
|
13998
14397
|
projectId,
|
|
@@ -14004,11 +14403,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14004
14403
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14005
14404
|
}).run();
|
|
14006
14405
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
14007
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14406
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
|
|
14008
14407
|
log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
14009
14408
|
} catch (err) {
|
|
14010
14409
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
14011
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14410
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
|
|
14012
14411
|
log4.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
14013
14412
|
throw err;
|
|
14014
14413
|
}
|
|
@@ -14016,7 +14415,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14016
14415
|
|
|
14017
14416
|
// src/bing-inspect-sitemap.ts
|
|
14018
14417
|
import crypto22 from "crypto";
|
|
14019
|
-
import { eq as
|
|
14418
|
+
import { eq as eq23, desc as desc10 } from "drizzle-orm";
|
|
14020
14419
|
var log5 = createLogger("BingInspectSitemap");
|
|
14021
14420
|
function parseBingDate2(value) {
|
|
14022
14421
|
if (!value) return null;
|
|
@@ -14034,9 +14433,9 @@ function isBlockingIssueType2(issueType) {
|
|
|
14034
14433
|
}
|
|
14035
14434
|
async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
14036
14435
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14037
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
14436
|
+
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq23(runs.id, runId)).run();
|
|
14038
14437
|
try {
|
|
14039
|
-
const project = db.select().from(projects).where(
|
|
14438
|
+
const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
|
|
14040
14439
|
if (!project) {
|
|
14041
14440
|
throw new Error(`Project not found: ${projectId}`);
|
|
14042
14441
|
}
|
|
@@ -14054,7 +14453,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14054
14453
|
if (sitemapUrls.length === 0) {
|
|
14055
14454
|
throw new Error("No URLs found in sitemap");
|
|
14056
14455
|
}
|
|
14057
|
-
const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(
|
|
14456
|
+
const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, projectId)).all();
|
|
14058
14457
|
const trackedUrls = new Set(trackedRows.map((r) => r.url));
|
|
14059
14458
|
const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
|
|
14060
14459
|
log5.info("sitemap.diff", {
|
|
@@ -14137,7 +14536,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14137
14536
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
14138
14537
|
}
|
|
14139
14538
|
}
|
|
14140
|
-
const allInspections = db.select().from(bingUrlInspections).where(
|
|
14539
|
+
const allInspections = db.select().from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, projectId)).orderBy(desc10(bingUrlInspections.inspectedAt)).all();
|
|
14141
14540
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
14142
14541
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
14143
14542
|
for (const row of allInspections) {
|
|
@@ -14180,7 +14579,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14180
14579
|
}
|
|
14181
14580
|
}).run();
|
|
14182
14581
|
const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
|
|
14183
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14582
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
|
|
14184
14583
|
log5.info("inspect.completed", {
|
|
14185
14584
|
runId,
|
|
14186
14585
|
projectId,
|
|
@@ -14194,7 +14593,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14194
14593
|
});
|
|
14195
14594
|
} catch (err) {
|
|
14196
14595
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
14197
|
-
db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14596
|
+
db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
|
|
14198
14597
|
log5.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
14199
14598
|
throw err;
|
|
14200
14599
|
}
|
|
@@ -14203,7 +14602,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14203
14602
|
// src/commoncrawl-sync.ts
|
|
14204
14603
|
import crypto23 from "crypto";
|
|
14205
14604
|
import path10 from "path";
|
|
14206
|
-
import { and as
|
|
14605
|
+
import { and as and12, eq as eq24, sql as sql8 } from "drizzle-orm";
|
|
14207
14606
|
var log6 = createLogger("CommonCrawlSync");
|
|
14208
14607
|
var INSERT_CHUNK_SIZE = 1e4;
|
|
14209
14608
|
function defaultDeps() {
|
|
@@ -14229,7 +14628,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14229
14628
|
phaseDetail: "downloading vertices + edges",
|
|
14230
14629
|
updatedAt: downloadStartedAt,
|
|
14231
14630
|
error: null
|
|
14232
|
-
}).where(
|
|
14631
|
+
}).where(eq24(ccReleaseSyncs.id, syncId)).run();
|
|
14233
14632
|
const paths = ccReleasePaths(release);
|
|
14234
14633
|
const releaseCacheDir = path10.join(deps.cacheDir, release);
|
|
14235
14634
|
const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
|
|
@@ -14252,7 +14651,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14252
14651
|
vertexSha256: vertex.sha256,
|
|
14253
14652
|
edgesSha256: edges.sha256,
|
|
14254
14653
|
updatedAt: downloadFinishedAt
|
|
14255
|
-
}).where(
|
|
14654
|
+
}).where(eq24(ccReleaseSyncs.id, syncId)).run();
|
|
14256
14655
|
const allProjects = db.select().from(projects).all();
|
|
14257
14656
|
const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
|
|
14258
14657
|
let rows = [];
|
|
@@ -14268,8 +14667,8 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14268
14667
|
}
|
|
14269
14668
|
const queriedAt = deps.now().toISOString();
|
|
14270
14669
|
db.transaction((tx) => {
|
|
14271
|
-
tx.delete(backlinkDomains).where(
|
|
14272
|
-
tx.delete(backlinkSummaries).where(
|
|
14670
|
+
tx.delete(backlinkDomains).where(eq24(backlinkDomains.releaseSyncId, syncId)).run();
|
|
14671
|
+
tx.delete(backlinkSummaries).where(eq24(backlinkSummaries.releaseSyncId, syncId)).run();
|
|
14273
14672
|
const expanded = [];
|
|
14274
14673
|
for (const r of rows) {
|
|
14275
14674
|
const projectIds = projectsByDomain.get(r.targetDomain);
|
|
@@ -14328,7 +14727,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14328
14727
|
domainsDiscovered: rows.length,
|
|
14329
14728
|
updatedAt: finishedAt,
|
|
14330
14729
|
error: null
|
|
14331
|
-
}).where(
|
|
14730
|
+
}).where(eq24(ccReleaseSyncs.id, syncId)).run();
|
|
14332
14731
|
log6.info("sync.completed", {
|
|
14333
14732
|
syncId,
|
|
14334
14733
|
release,
|
|
@@ -14358,7 +14757,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14358
14757
|
error: errorMsg,
|
|
14359
14758
|
phaseDetail: null,
|
|
14360
14759
|
updatedAt: finishedAt
|
|
14361
|
-
}).where(
|
|
14760
|
+
}).where(eq24(ccReleaseSyncs.id, syncId)).run();
|
|
14362
14761
|
log6.error("sync.failed", { syncId, release, error: errorMsg });
|
|
14363
14762
|
throw err;
|
|
14364
14763
|
}
|
|
@@ -14394,7 +14793,7 @@ function computeSummary(rows) {
|
|
|
14394
14793
|
// src/backlink-extract.ts
|
|
14395
14794
|
import crypto24 from "crypto";
|
|
14396
14795
|
import fs8 from "fs";
|
|
14397
|
-
import { and as
|
|
14796
|
+
import { and as and13, desc as desc11, eq as eq25 } from "drizzle-orm";
|
|
14398
14797
|
var log7 = createLogger("BacklinkExtract");
|
|
14399
14798
|
function defaultDeps2() {
|
|
14400
14799
|
return {
|
|
@@ -14406,13 +14805,13 @@ function defaultDeps2() {
|
|
|
14406
14805
|
async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
14407
14806
|
const deps = { ...defaultDeps2(), ...opts.deps };
|
|
14408
14807
|
const startedAt = deps.now().toISOString();
|
|
14409
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
14808
|
+
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq25(runs.id, runId)).run();
|
|
14410
14809
|
try {
|
|
14411
|
-
const project = db.select().from(projects).where(
|
|
14810
|
+
const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
|
|
14412
14811
|
if (!project) {
|
|
14413
14812
|
throw new Error(`Project not found: ${projectId}`);
|
|
14414
14813
|
}
|
|
14415
|
-
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(
|
|
14814
|
+
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc11(ccReleaseSyncs.createdAt)).limit(1).get();
|
|
14416
14815
|
if (!sync) {
|
|
14417
14816
|
throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
|
|
14418
14817
|
}
|
|
@@ -14440,7 +14839,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
14440
14839
|
const targetDomain = project.canonicalDomain;
|
|
14441
14840
|
db.transaction((tx) => {
|
|
14442
14841
|
tx.delete(backlinkDomains).where(
|
|
14443
|
-
|
|
14842
|
+
and13(eq25(backlinkDomains.projectId, projectId), eq25(backlinkDomains.release, release))
|
|
14444
14843
|
).run();
|
|
14445
14844
|
if (rows.length > 0) {
|
|
14446
14845
|
const values = rows.map((r) => ({
|
|
@@ -14480,7 +14879,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
14480
14879
|
}).run();
|
|
14481
14880
|
});
|
|
14482
14881
|
const finishedAt = deps.now().toISOString();
|
|
14483
|
-
db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
14882
|
+
db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq25(runs.id, runId)).run();
|
|
14484
14883
|
log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
|
|
14485
14884
|
} catch (err) {
|
|
14486
14885
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -14489,7 +14888,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
14489
14888
|
status: RunStatuses.failed,
|
|
14490
14889
|
error: errorMsg,
|
|
14491
14890
|
finishedAt
|
|
14492
|
-
}).where(
|
|
14891
|
+
}).where(eq25(runs.id, runId)).run();
|
|
14493
14892
|
log7.error("extract.failed", { runId, projectId, error: errorMsg });
|
|
14494
14893
|
throw err;
|
|
14495
14894
|
}
|
|
@@ -14562,7 +14961,7 @@ var ProviderRegistry = class {
|
|
|
14562
14961
|
|
|
14563
14962
|
// src/scheduler.ts
|
|
14564
14963
|
import cron from "node-cron";
|
|
14565
|
-
import { eq as
|
|
14964
|
+
import { eq as eq26 } from "drizzle-orm";
|
|
14566
14965
|
var log8 = createLogger("Scheduler");
|
|
14567
14966
|
var Scheduler = class {
|
|
14568
14967
|
db;
|
|
@@ -14574,7 +14973,7 @@ var Scheduler = class {
|
|
|
14574
14973
|
}
|
|
14575
14974
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
14576
14975
|
start() {
|
|
14577
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
14976
|
+
const allSchedules = this.db.select().from(schedules).where(eq26(schedules.enabled, 1)).all();
|
|
14578
14977
|
for (const schedule of allSchedules) {
|
|
14579
14978
|
const missedRunAt = schedule.nextRunAt;
|
|
14580
14979
|
this.registerCronTask(schedule);
|
|
@@ -14599,7 +14998,7 @@ var Scheduler = class {
|
|
|
14599
14998
|
this.stopTask(projectId, existing, "Stopped");
|
|
14600
14999
|
this.tasks.delete(projectId);
|
|
14601
15000
|
}
|
|
14602
|
-
const schedule = this.db.select().from(schedules).where(
|
|
15001
|
+
const schedule = this.db.select().from(schedules).where(eq26(schedules.projectId, projectId)).get();
|
|
14603
15002
|
if (schedule && schedule.enabled === 1) {
|
|
14604
15003
|
this.registerCronTask(schedule);
|
|
14605
15004
|
}
|
|
@@ -14632,14 +15031,14 @@ var Scheduler = class {
|
|
|
14632
15031
|
this.db.update(schedules).set({
|
|
14633
15032
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
14634
15033
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14635
|
-
}).where(
|
|
15034
|
+
}).where(eq26(schedules.id, scheduleId)).run();
|
|
14636
15035
|
const label = schedule.preset ?? cronExpr;
|
|
14637
15036
|
log8.info("cron.registered", { projectId, schedule: label, timezone });
|
|
14638
15037
|
}
|
|
14639
15038
|
triggerRun(scheduleId, projectId) {
|
|
14640
15039
|
try {
|
|
14641
15040
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14642
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
15041
|
+
const currentSchedule = this.db.select().from(schedules).where(eq26(schedules.id, scheduleId)).get();
|
|
14643
15042
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
14644
15043
|
log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
|
|
14645
15044
|
this.remove(projectId);
|
|
@@ -14647,7 +15046,7 @@ var Scheduler = class {
|
|
|
14647
15046
|
}
|
|
14648
15047
|
const task = this.tasks.get(projectId);
|
|
14649
15048
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
14650
|
-
const project = this.db.select().from(projects).where(
|
|
15049
|
+
const project = this.db.select().from(projects).where(eq26(projects.id, projectId)).get();
|
|
14651
15050
|
if (!project) {
|
|
14652
15051
|
log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
|
|
14653
15052
|
this.remove(projectId);
|
|
@@ -14676,7 +15075,7 @@ var Scheduler = class {
|
|
|
14676
15075
|
this.db.update(schedules).set({
|
|
14677
15076
|
nextRunAt,
|
|
14678
15077
|
updatedAt: now
|
|
14679
|
-
}).where(
|
|
15078
|
+
}).where(eq26(schedules.id, currentSchedule.id)).run();
|
|
14680
15079
|
return;
|
|
14681
15080
|
}
|
|
14682
15081
|
const runId = queueResult.runId;
|
|
@@ -14684,7 +15083,7 @@ var Scheduler = class {
|
|
|
14684
15083
|
lastRunAt: now,
|
|
14685
15084
|
nextRunAt,
|
|
14686
15085
|
updatedAt: now
|
|
14687
|
-
}).where(
|
|
15086
|
+
}).where(eq26(schedules.id, currentSchedule.id)).run();
|
|
14688
15087
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
14689
15088
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
14690
15089
|
log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -14696,7 +15095,7 @@ var Scheduler = class {
|
|
|
14696
15095
|
};
|
|
14697
15096
|
|
|
14698
15097
|
// src/notifier.ts
|
|
14699
|
-
import { eq as
|
|
15098
|
+
import { eq as eq27, desc as desc12, and as and14, or as or2 } from "drizzle-orm";
|
|
14700
15099
|
import crypto25 from "crypto";
|
|
14701
15100
|
var log9 = createLogger("Notifier");
|
|
14702
15101
|
var Notifier = class {
|
|
@@ -14709,18 +15108,18 @@ var Notifier = class {
|
|
|
14709
15108
|
/** Called after a run completes (success, partial, or failed). */
|
|
14710
15109
|
async onRunCompleted(runId, projectId) {
|
|
14711
15110
|
log9.info("run.completed", { runId, projectId });
|
|
14712
|
-
const notifs = this.db.select().from(notifications).where(
|
|
15111
|
+
const notifs = this.db.select().from(notifications).where(eq27(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14713
15112
|
if (notifs.length === 0) {
|
|
14714
15113
|
log9.info("notifications.none-enabled", { projectId });
|
|
14715
15114
|
return;
|
|
14716
15115
|
}
|
|
14717
15116
|
log9.info("notifications.found", { projectId, count: notifs.length });
|
|
14718
|
-
const run = this.db.select().from(runs).where(
|
|
15117
|
+
const run = this.db.select().from(runs).where(eq27(runs.id, runId)).get();
|
|
14719
15118
|
if (!run) {
|
|
14720
15119
|
log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
14721
15120
|
return;
|
|
14722
15121
|
}
|
|
14723
|
-
const project = this.db.select().from(projects).where(
|
|
15122
|
+
const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
|
|
14724
15123
|
if (!project) {
|
|
14725
15124
|
log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
14726
15125
|
return;
|
|
@@ -14767,11 +15166,11 @@ var Notifier = class {
|
|
|
14767
15166
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
14768
15167
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
14769
15168
|
if (insightEvents.length === 0) return;
|
|
14770
|
-
const notifs = this.db.select().from(notifications).where(
|
|
15169
|
+
const notifs = this.db.select().from(notifications).where(eq27(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14771
15170
|
if (notifs.length === 0) return;
|
|
14772
|
-
const run = this.db.select().from(runs).where(
|
|
15171
|
+
const run = this.db.select().from(runs).where(eq27(runs.id, runId)).get();
|
|
14773
15172
|
if (!run) return;
|
|
14774
|
-
const project = this.db.select().from(projects).where(
|
|
15173
|
+
const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
|
|
14775
15174
|
if (!project) return;
|
|
14776
15175
|
for (const notif of notifs) {
|
|
14777
15176
|
const config = parseJsonColumn(notif.config, { url: "", events: [] });
|
|
@@ -14802,11 +15201,11 @@ var Notifier = class {
|
|
|
14802
15201
|
}
|
|
14803
15202
|
computeTransitions(runId, projectId) {
|
|
14804
15203
|
const recentRuns = this.db.select().from(runs).where(
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
or2(
|
|
15204
|
+
and14(
|
|
15205
|
+
eq27(runs.projectId, projectId),
|
|
15206
|
+
or2(eq27(runs.status, "completed"), eq27(runs.status, "partial"))
|
|
14808
15207
|
)
|
|
14809
|
-
).orderBy(
|
|
15208
|
+
).orderBy(desc12(runs.createdAt)).limit(2).all();
|
|
14810
15209
|
if (recentRuns.length < 2) return [];
|
|
14811
15210
|
const currentRunId = recentRuns[0].id;
|
|
14812
15211
|
const previousRunId = recentRuns[1].id;
|
|
@@ -14816,12 +15215,12 @@ var Notifier = class {
|
|
|
14816
15215
|
keyword: keywords.keyword,
|
|
14817
15216
|
provider: querySnapshots.provider,
|
|
14818
15217
|
citationState: querySnapshots.citationState
|
|
14819
|
-
}).from(querySnapshots).leftJoin(keywords,
|
|
15218
|
+
}).from(querySnapshots).leftJoin(keywords, eq27(querySnapshots.keywordId, keywords.id)).where(eq27(querySnapshots.runId, currentRunId)).all();
|
|
14820
15219
|
const previousSnapshots = this.db.select({
|
|
14821
15220
|
keywordId: querySnapshots.keywordId,
|
|
14822
15221
|
provider: querySnapshots.provider,
|
|
14823
15222
|
citationState: querySnapshots.citationState
|
|
14824
|
-
}).from(querySnapshots).where(
|
|
15223
|
+
}).from(querySnapshots).where(eq27(querySnapshots.runId, previousRunId)).all();
|
|
14825
15224
|
const prevMap = /* @__PURE__ */ new Map();
|
|
14826
15225
|
for (const s of previousSnapshots) {
|
|
14827
15226
|
prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
|
|
@@ -14938,7 +15337,7 @@ var RunCoordinator = class {
|
|
|
14938
15337
|
|
|
14939
15338
|
// src/agent/session-registry.ts
|
|
14940
15339
|
import crypto27 from "crypto";
|
|
14941
|
-
import { eq as
|
|
15340
|
+
import { eq as eq29 } from "drizzle-orm";
|
|
14942
15341
|
|
|
14943
15342
|
// src/agent/session.ts
|
|
14944
15343
|
import fs11 from "fs";
|
|
@@ -15158,7 +15557,7 @@ import { Type as Type2 } from "@sinclair/typebox";
|
|
|
15158
15557
|
|
|
15159
15558
|
// src/agent/memory-store.ts
|
|
15160
15559
|
import crypto26 from "crypto";
|
|
15161
|
-
import { and as
|
|
15560
|
+
import { and as and15, desc as desc13, eq as eq28, like, sql as sql9 } from "drizzle-orm";
|
|
15162
15561
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
15163
15562
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
15164
15563
|
function rowToDto(row) {
|
|
@@ -15172,7 +15571,7 @@ function rowToDto(row) {
|
|
|
15172
15571
|
};
|
|
15173
15572
|
}
|
|
15174
15573
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
15175
|
-
const query = db.select().from(agentMemory).where(
|
|
15574
|
+
const query = db.select().from(agentMemory).where(eq28(agentMemory.projectId, projectId)).orderBy(desc13(agentMemory.updatedAt));
|
|
15176
15575
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
15177
15576
|
return rows.map(rowToDto);
|
|
15178
15577
|
}
|
|
@@ -15203,12 +15602,12 @@ function upsertMemoryEntry(db, args) {
|
|
|
15203
15602
|
updatedAt: now
|
|
15204
15603
|
}
|
|
15205
15604
|
}).run();
|
|
15206
|
-
const row = db.select().from(agentMemory).where(
|
|
15605
|
+
const row = db.select().from(agentMemory).where(and15(eq28(agentMemory.projectId, args.projectId), eq28(agentMemory.key, args.key))).get();
|
|
15207
15606
|
if (!row) throw new Error("memory upsert produced no row");
|
|
15208
15607
|
return rowToDto(row);
|
|
15209
15608
|
}
|
|
15210
15609
|
function deleteMemoryEntry(db, projectId, key) {
|
|
15211
|
-
const result = db.delete(agentMemory).where(
|
|
15610
|
+
const result = db.delete(agentMemory).where(and15(eq28(agentMemory.projectId, projectId), eq28(agentMemory.key, key))).run();
|
|
15212
15611
|
const changes = result.changes ?? 0;
|
|
15213
15612
|
return changes > 0;
|
|
15214
15613
|
}
|
|
@@ -15237,16 +15636,16 @@ function writeCompactionNote(db, args) {
|
|
|
15237
15636
|
}).run();
|
|
15238
15637
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
15239
15638
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
15240
|
-
|
|
15241
|
-
|
|
15639
|
+
and15(
|
|
15640
|
+
eq28(agentMemory.projectId, args.projectId),
|
|
15242
15641
|
like(agentMemory.key, `${sessionPrefix}%`)
|
|
15243
15642
|
)
|
|
15244
|
-
).orderBy(
|
|
15643
|
+
).orderBy(desc13(agentMemory.updatedAt)).all();
|
|
15245
15644
|
const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
|
|
15246
15645
|
if (stale.length > 0) {
|
|
15247
15646
|
tx.delete(agentMemory).where(sql9`${agentMemory.id} IN (${sql9.join(stale.map((s) => sql9`${s}`), sql9`, `)})`).run();
|
|
15248
15647
|
}
|
|
15249
|
-
const row = tx.select().from(agentMemory).where(
|
|
15648
|
+
const row = tx.select().from(agentMemory).where(and15(eq28(agentMemory.projectId, args.projectId), eq28(agentMemory.key, key))).get();
|
|
15250
15649
|
if (row) inserted = rowToDto(row);
|
|
15251
15650
|
});
|
|
15252
15651
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -15295,7 +15694,7 @@ function buildGetHealthTool(ctx) {
|
|
|
15295
15694
|
return {
|
|
15296
15695
|
name: "get_health",
|
|
15297
15696
|
label: "Get health",
|
|
15298
|
-
description:
|
|
15697
|
+
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.',
|
|
15299
15698
|
parameters: HealthSchema,
|
|
15300
15699
|
execute: async () => {
|
|
15301
15700
|
const health = await ctx.client.getHealth(ctx.projectName);
|
|
@@ -15376,6 +15775,59 @@ function buildListCompetitorsTool(ctx) {
|
|
|
15376
15775
|
}
|
|
15377
15776
|
};
|
|
15378
15777
|
}
|
|
15778
|
+
var ContentTargetsSchema = Type2.Object({
|
|
15779
|
+
limit: Type2.Optional(
|
|
15780
|
+
Type2.Number({
|
|
15781
|
+
description: "Max rows. Defaults to all. Use a small number (3\u201310) when summarizing for the user."
|
|
15782
|
+
})
|
|
15783
|
+
),
|
|
15784
|
+
includeInProgress: Type2.Optional(
|
|
15785
|
+
Type2.Boolean({
|
|
15786
|
+
description: "Include rows that already have an in-flight tracked action. Default false."
|
|
15787
|
+
})
|
|
15788
|
+
)
|
|
15789
|
+
});
|
|
15790
|
+
function buildGetContentTargetsTool(ctx) {
|
|
15791
|
+
return {
|
|
15792
|
+
name: "get_content_targets",
|
|
15793
|
+
label: "Get content targets",
|
|
15794
|
+
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.",
|
|
15795
|
+
parameters: ContentTargetsSchema,
|
|
15796
|
+
execute: async (_toolCallId, params) => {
|
|
15797
|
+
const response = await ctx.client.getContentTargets(ctx.projectName, {
|
|
15798
|
+
limit: params.limit,
|
|
15799
|
+
includeInProgress: params.includeInProgress === true
|
|
15800
|
+
});
|
|
15801
|
+
return textResult2(response);
|
|
15802
|
+
}
|
|
15803
|
+
};
|
|
15804
|
+
}
|
|
15805
|
+
var ContentSourcesSchema = Type2.Object({});
|
|
15806
|
+
function buildGetContentSourcesTool(ctx) {
|
|
15807
|
+
return {
|
|
15808
|
+
name: "get_grounding_sources",
|
|
15809
|
+
label: "Get grounding sources",
|
|
15810
|
+
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.",
|
|
15811
|
+
parameters: ContentSourcesSchema,
|
|
15812
|
+
execute: async () => {
|
|
15813
|
+
const response = await ctx.client.getContentSources(ctx.projectName);
|
|
15814
|
+
return textResult2(response);
|
|
15815
|
+
}
|
|
15816
|
+
};
|
|
15817
|
+
}
|
|
15818
|
+
var ContentGapsSchema = Type2.Object({});
|
|
15819
|
+
function buildGetContentGapsTool(ctx) {
|
|
15820
|
+
return {
|
|
15821
|
+
name: "get_content_gaps",
|
|
15822
|
+
label: "Get content gaps",
|
|
15823
|
+
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.',
|
|
15824
|
+
parameters: ContentGapsSchema,
|
|
15825
|
+
execute: async () => {
|
|
15826
|
+
const response = await ctx.client.getContentGaps(ctx.projectName);
|
|
15827
|
+
return textResult2(response);
|
|
15828
|
+
}
|
|
15829
|
+
};
|
|
15830
|
+
}
|
|
15379
15831
|
var RunDetailSchema = Type2.Object({
|
|
15380
15832
|
runId: Type2.String({
|
|
15381
15833
|
description: "Run id (UUID) to fetch. Typically obtained from get_status runs[].id."
|
|
@@ -15453,7 +15905,10 @@ function buildReadTools(ctx) {
|
|
|
15453
15905
|
buildListCompetitorsTool(ctx),
|
|
15454
15906
|
buildGetRunTool(ctx),
|
|
15455
15907
|
buildRecallTool(ctx),
|
|
15456
|
-
buildListBacklinksTool(ctx)
|
|
15908
|
+
buildListBacklinksTool(ctx),
|
|
15909
|
+
buildGetContentTargetsTool(ctx),
|
|
15910
|
+
buildGetContentSourcesTool(ctx),
|
|
15911
|
+
buildGetContentGapsTool(ctx)
|
|
15457
15912
|
];
|
|
15458
15913
|
}
|
|
15459
15914
|
var RunSweepSchema = Type2.Object({
|
|
@@ -15922,7 +16377,7 @@ var SessionRegistry = class {
|
|
|
15922
16377
|
modelProvider: effectiveProvider,
|
|
15923
16378
|
modelId: effectiveModelId,
|
|
15924
16379
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15925
|
-
}).where(
|
|
16380
|
+
}).where(eq29(agentSessions.projectId, projectId)).run();
|
|
15926
16381
|
}
|
|
15927
16382
|
const agent2 = createAeroSession({
|
|
15928
16383
|
projectName,
|
|
@@ -16140,7 +16595,7 @@ ${lines.join("\n")}
|
|
|
16140
16595
|
modelProvider: nextProvider,
|
|
16141
16596
|
modelId: nextModelId,
|
|
16142
16597
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16143
|
-
}).where(
|
|
16598
|
+
}).where(eq29(agentSessions.projectId, projectId)).run();
|
|
16144
16599
|
}
|
|
16145
16600
|
/** Persist a session's transcript back to the DB. Call after any run settles. */
|
|
16146
16601
|
save(projectName) {
|
|
@@ -16302,11 +16757,11 @@ ${lines.join("\n")}
|
|
|
16302
16757
|
return id;
|
|
16303
16758
|
}
|
|
16304
16759
|
tryResolveProjectId(projectName) {
|
|
16305
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
16760
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq29(projects.name, projectName)).get();
|
|
16306
16761
|
return row?.id;
|
|
16307
16762
|
}
|
|
16308
16763
|
loadRow(projectId) {
|
|
16309
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
16764
|
+
const row = this.opts.db.select().from(agentSessions).where(eq29(agentSessions.projectId, projectId)).get();
|
|
16310
16765
|
return row ?? null;
|
|
16311
16766
|
}
|
|
16312
16767
|
insertRow(params) {
|
|
@@ -16325,14 +16780,14 @@ ${lines.join("\n")}
|
|
|
16325
16780
|
}
|
|
16326
16781
|
updateRow(projectId, patch) {
|
|
16327
16782
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16328
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
16783
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq29(agentSessions.projectId, projectId)).run();
|
|
16329
16784
|
}
|
|
16330
16785
|
};
|
|
16331
16786
|
|
|
16332
16787
|
// src/agent/agent-routes.ts
|
|
16333
|
-
import { eq as
|
|
16788
|
+
import { eq as eq30 } from "drizzle-orm";
|
|
16334
16789
|
function resolveProject2(db, name) {
|
|
16335
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
16790
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq30(projects.name, name)).get();
|
|
16336
16791
|
if (!row) throw notFound("project", name);
|
|
16337
16792
|
return row;
|
|
16338
16793
|
}
|
|
@@ -16341,7 +16796,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
16341
16796
|
"/projects/:name/agent/transcript",
|
|
16342
16797
|
async (request) => {
|
|
16343
16798
|
const project = resolveProject2(opts.db, request.params.name);
|
|
16344
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
16799
|
+
const row = opts.db.select().from(agentSessions).where(eq30(agentSessions.projectId, project.id)).get();
|
|
16345
16800
|
if (!row) {
|
|
16346
16801
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
16347
16802
|
}
|
|
@@ -16365,7 +16820,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
16365
16820
|
async (request) => {
|
|
16366
16821
|
const project = resolveProject2(opts.db, request.params.name);
|
|
16367
16822
|
opts.sessionRegistry.reset(project.name);
|
|
16368
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
16823
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(agentSessions.projectId, project.id)).run();
|
|
16369
16824
|
return { status: "reset" };
|
|
16370
16825
|
}
|
|
16371
16826
|
);
|
|
@@ -16669,7 +17124,7 @@ var SnapshotService = class {
|
|
|
16669
17124
|
}
|
|
16670
17125
|
async createReport(input) {
|
|
16671
17126
|
const companyName = input.companyName.trim();
|
|
16672
|
-
const domain =
|
|
17127
|
+
const domain = normalizeDomain2(input.domain);
|
|
16673
17128
|
const manualPhrases = normalizeStringList(input.phrases ?? []);
|
|
16674
17129
|
const manualCompetitors = normalizeStringList(input.competitors ?? []);
|
|
16675
17130
|
const providers = this.registry.getAll();
|
|
@@ -17109,7 +17564,7 @@ function extractCompetitorsFromResponse(ctx) {
|
|
|
17109
17564
|
const targetDomain = extractHostname2(ctx.targetDomain);
|
|
17110
17565
|
for (const hint of ctx.manualCompetitors) {
|
|
17111
17566
|
if (isDomainLike(hint)) {
|
|
17112
|
-
const normalizedHint =
|
|
17567
|
+
const normalizedHint = normalizeDomain2(hint);
|
|
17113
17568
|
if (domainMatches2(normalizedHint, targetDomain)) continue;
|
|
17114
17569
|
if (ctx.citedDomains.some((domain) => domainMatches2(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
|
|
17115
17570
|
competitors2.add(normalizedHint);
|
|
@@ -17168,7 +17623,7 @@ function uniqueStrings2(values) {
|
|
|
17168
17623
|
values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
|
|
17169
17624
|
)];
|
|
17170
17625
|
}
|
|
17171
|
-
function
|
|
17626
|
+
function normalizeDomain2(value) {
|
|
17172
17627
|
const trimmed = value.trim();
|
|
17173
17628
|
if (!trimmed) return trimmed;
|
|
17174
17629
|
try {
|
|
@@ -17179,15 +17634,15 @@ function normalizeDomain(value) {
|
|
|
17179
17634
|
}
|
|
17180
17635
|
}
|
|
17181
17636
|
function extractHostname2(value) {
|
|
17182
|
-
return
|
|
17637
|
+
return normalizeDomain2(value);
|
|
17183
17638
|
}
|
|
17184
17639
|
function domainMatches2(candidate, target) {
|
|
17185
|
-
const normalizedCandidate =
|
|
17186
|
-
const normalizedTarget =
|
|
17640
|
+
const normalizedCandidate = normalizeDomain2(candidate);
|
|
17641
|
+
const normalizedTarget = normalizeDomain2(target);
|
|
17187
17642
|
return normalizedCandidate === normalizedTarget || normalizedCandidate.endsWith(`.${normalizedTarget}`);
|
|
17188
17643
|
}
|
|
17189
17644
|
function isDomainLike(value) {
|
|
17190
|
-
const normalized =
|
|
17645
|
+
const normalized = normalizeDomain2(value);
|
|
17191
17646
|
return normalized.includes(".") && !normalized.includes(" ");
|
|
17192
17647
|
}
|
|
17193
17648
|
function clipText(value, length) {
|
|
@@ -17387,7 +17842,7 @@ async function createServer(opts) {
|
|
|
17387
17842
|
intelligenceService,
|
|
17388
17843
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
17389
17844
|
async ({ runId, projectId, insightCount, criticalOrHigh }) => {
|
|
17390
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
17845
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq31(projects.id, projectId)).get();
|
|
17391
17846
|
if (!project) return;
|
|
17392
17847
|
sessionRegistry.queueFollowUp(project.name, {
|
|
17393
17848
|
role: "user",
|
|
@@ -17527,7 +17982,7 @@ async function createServer(opts) {
|
|
|
17527
17982
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
17528
17983
|
if (opts.config.apiKey) {
|
|
17529
17984
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
17530
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
17985
|
+
const existing = opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, keyHash)).get();
|
|
17531
17986
|
if (!existing) {
|
|
17532
17987
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
17533
17988
|
opts.db.insert(apiKeys).values({
|
|
@@ -17579,7 +18034,7 @@ async function createServer(opts) {
|
|
|
17579
18034
|
};
|
|
17580
18035
|
const getDefaultApiKey = () => {
|
|
17581
18036
|
if (!opts.config.apiKey) return void 0;
|
|
17582
|
-
return opts.db.select().from(apiKeys).where(
|
|
18037
|
+
return opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
17583
18038
|
};
|
|
17584
18039
|
const createPasswordSession = (reply) => {
|
|
17585
18040
|
const key = getDefaultApiKey();
|
|
@@ -17636,12 +18091,12 @@ async function createServer(opts) {
|
|
|
17636
18091
|
return reply.send({ authenticated: true });
|
|
17637
18092
|
}
|
|
17638
18093
|
if (apiKey) {
|
|
17639
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
18094
|
+
const key = opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
17640
18095
|
if (!key || key.revokedAt) {
|
|
17641
18096
|
const err2 = authInvalid();
|
|
17642
18097
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
17643
18098
|
}
|
|
17644
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
18099
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(apiKeys.id, key.id)).run();
|
|
17645
18100
|
const sessionId = createSession(key.id);
|
|
17646
18101
|
reply.header("set-cookie", serializeSessionCookie({
|
|
17647
18102
|
name: SESSION_COOKIE_NAME,
|