@ainyc/canonry 4.138.0 → 4.139.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/agent-workspace/skills/aero/references/reporting.md +22 -1
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +9 -13
- package/assets/assets/{AuditHistoryPanel-CIdoSKjP.js → AuditHistoryPanel-FiMbN4_a.js} +1 -1
- package/assets/assets/BacklinksPage-CLRIsKHN.js +1 -0
- package/assets/assets/ChartPrimitives-BuTqG5NR.js +1 -0
- package/assets/assets/HistoryPage-BWRbsFAT.js +1 -0
- package/assets/assets/ProjectPage-DQ2kIj-L.js +8 -0
- package/assets/assets/{RunRow-D6W6D1BX.js → RunRow-BZobaeDq.js} +1 -1
- package/assets/assets/RunsPage-DhrY4Ouc.js +1 -0
- package/assets/assets/SettingsPage-C6VcJEgB.js +1 -0
- package/assets/assets/TrafficPage-KqYl5HUH.js +1 -0
- package/assets/assets/TrafficSourceDetailPage-mMJi-QZr.js +1 -0
- package/assets/assets/{arrow-left-ClC65OiR.js → arrow-left-CpLow2Kp.js} +1 -1
- package/assets/assets/extract-error-message-dHq11Y4U.js +1 -0
- package/assets/assets/index-CntuM6Th.css +1 -0
- package/assets/assets/index-iRW7X23W.js +213 -0
- package/assets/assets/trash-2-CnEPNBqu.js +1 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-GO24URVP.js → chunk-DJHSOPMU.js} +1609 -1149
- package/dist/{chunk-R3GAWKHM.js → chunk-IYTHIAAE.js} +5 -25
- package/dist/{chunk-RJ7OW6W6.js → chunk-OA2EGCUS.js} +2973 -2914
- package/dist/{chunk-3KGJ7BXW.js → chunk-YLHZQ7HQ.js} +567 -435
- package/dist/cli.js +10 -38
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-5RJHJLYA.js → intelligence-service-BGH2MGHB.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +8 -8
- package/assets/assets/BacklinksPage-BYnzQLFq.js +0 -1
- package/assets/assets/ChartPrimitives-bx4p5MDx.js +0 -1
- package/assets/assets/HistoryPage-CjLb8TKY.js +0 -1
- package/assets/assets/ProjectPage-C_bSGiLs.js +0 -8
- package/assets/assets/RunsPage-CQT4lXFp.js +0 -1
- package/assets/assets/SettingsPage-DVQKzz6s.js +0 -1
- package/assets/assets/TrafficPage-BdocLgWw.js +0 -1
- package/assets/assets/TrafficSourceDetailPage-CTZF_R9C.js +0 -1
- package/assets/assets/extract-error-message-CsgTjzyj.js +0 -1
- package/assets/assets/index-BZgknVrZ.js +0 -213
- package/assets/assets/index-DY7GziwY.css +0 -1
- package/assets/assets/trash-2-nr8RRH47.js +0 -1
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
loadConfig,
|
|
11
11
|
loadConfigRaw,
|
|
12
12
|
saveConfigPatch
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-IYTHIAAE.js";
|
|
14
14
|
import {
|
|
15
15
|
CC_CACHE_DIR,
|
|
16
16
|
DUCKDB_SPEC,
|
|
@@ -50,8 +50,10 @@ import {
|
|
|
50
50
|
deliverWebhook,
|
|
51
51
|
determineCitationState,
|
|
52
52
|
discoverySessions,
|
|
53
|
+
doctorHealthState,
|
|
53
54
|
downloadFile,
|
|
54
55
|
dropLegacyCredentialColumns,
|
|
56
|
+
ensureCurrentQueryBasketRevision,
|
|
55
57
|
executeDiscovery,
|
|
56
58
|
extractLegacyCredentials,
|
|
57
59
|
extractRecommendedCompetitors,
|
|
@@ -70,11 +72,9 @@ import {
|
|
|
70
72
|
gbpPlaceDetails,
|
|
71
73
|
getAttributes,
|
|
72
74
|
getCrawlIssues,
|
|
73
|
-
getLinkCounts,
|
|
74
75
|
getLodging,
|
|
75
76
|
getPlaceDetails,
|
|
76
77
|
getUrlInfo,
|
|
77
|
-
getUrlLinks,
|
|
78
78
|
groupRunsByCreatedAt,
|
|
79
79
|
gscCoverageSnapshots,
|
|
80
80
|
gscDailyTotals,
|
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
installDuckdb,
|
|
90
90
|
isDuckdbInstalled,
|
|
91
91
|
isValidReleaseId,
|
|
92
|
+
latestQueryBasketRevision,
|
|
92
93
|
listCachedReleases,
|
|
93
94
|
listLocations,
|
|
94
95
|
listMonthlyKeywords,
|
|
@@ -113,13 +114,16 @@ import {
|
|
|
113
114
|
refreshAccessToken,
|
|
114
115
|
researchRunQueries,
|
|
115
116
|
researchRuns,
|
|
117
|
+
resolveDestination,
|
|
118
|
+
resolveVercelSyncDeadlineMs,
|
|
116
119
|
resolveWebhookTarget,
|
|
117
120
|
runs,
|
|
118
121
|
schedules,
|
|
119
122
|
siteAuditPages,
|
|
120
123
|
siteAuditSnapshots,
|
|
124
|
+
toAlertView,
|
|
121
125
|
usageCounters
|
|
122
|
-
} from "./chunk-
|
|
126
|
+
} from "./chunk-DJHSOPMU.js";
|
|
123
127
|
import {
|
|
124
128
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
125
129
|
AGENT_PROVIDER_IDS,
|
|
@@ -206,7 +210,7 @@ import {
|
|
|
206
210
|
validationError,
|
|
207
211
|
winnabilityClassLabel,
|
|
208
212
|
withRetry
|
|
209
|
-
} from "./chunk-
|
|
213
|
+
} from "./chunk-OA2EGCUS.js";
|
|
210
214
|
|
|
211
215
|
// src/telemetry.ts
|
|
212
216
|
import crypto from "crypto";
|
|
@@ -537,11 +541,11 @@ function checkLatestVersionForServer(opts) {
|
|
|
537
541
|
|
|
538
542
|
// src/server.ts
|
|
539
543
|
import { createRequire as createRequire3 } from "module";
|
|
540
|
-
import
|
|
544
|
+
import crypto22 from "crypto";
|
|
541
545
|
import fs8 from "fs";
|
|
542
546
|
import path9 from "path";
|
|
543
547
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
544
|
-
import { and as
|
|
548
|
+
import { and as and15, eq as eq21 } from "drizzle-orm";
|
|
545
549
|
import Fastify from "fastify";
|
|
546
550
|
import os5 from "os";
|
|
547
551
|
|
|
@@ -958,7 +962,14 @@ var geminiAdapter = {
|
|
|
958
962
|
model: raw.model,
|
|
959
963
|
servedModel: raw.servedModel,
|
|
960
964
|
groundingSources: raw.groundingSources,
|
|
961
|
-
searchQueries: raw.searchQueries
|
|
965
|
+
searchQueries: raw.searchQueries,
|
|
966
|
+
// Retrieval detection is not implemented for this provider. Its candidate
|
|
967
|
+
// marker is present on 100% of stored rows, so it has never been shown to
|
|
968
|
+
// discriminate a non-retrieving answer and wiring it up would hardcode
|
|
969
|
+
// `used`. `unknown` states what we actually know. The contract is a
|
|
970
|
+
// declaration about how we build the request, so it is always knowable.
|
|
971
|
+
retrievalStatus: "unknown",
|
|
972
|
+
retrievalContract: "native-auto-v1"
|
|
962
973
|
};
|
|
963
974
|
},
|
|
964
975
|
normalizeResult(raw) {
|
|
@@ -975,7 +986,8 @@ var geminiAdapter = {
|
|
|
975
986
|
answerText: normalized.answerText,
|
|
976
987
|
citedDomains: normalized.citedDomains,
|
|
977
988
|
groundingSources: normalized.groundingSources,
|
|
978
|
-
searchQueries: normalized.searchQueries
|
|
989
|
+
searchQueries: normalized.searchQueries,
|
|
990
|
+
retrievalStatus: "unknown"
|
|
979
991
|
};
|
|
980
992
|
},
|
|
981
993
|
async generateText(prompt, config) {
|
|
@@ -1300,7 +1312,14 @@ var openaiAdapter = {
|
|
|
1300
1312
|
model: raw.model,
|
|
1301
1313
|
servedModel: raw.servedModel,
|
|
1302
1314
|
groundingSources: raw.groundingSources,
|
|
1303
|
-
searchQueries: raw.searchQueries
|
|
1315
|
+
searchQueries: raw.searchQueries,
|
|
1316
|
+
// Retrieval detection is not implemented for this provider. Its candidate
|
|
1317
|
+
// marker is present on 100% of stored rows, so it has never been shown to
|
|
1318
|
+
// discriminate a non-retrieving answer and wiring it up would hardcode
|
|
1319
|
+
// `used`. `unknown` states what we actually know. The contract is a
|
|
1320
|
+
// declaration about how we build the request, so it is always knowable.
|
|
1321
|
+
retrievalStatus: "unknown",
|
|
1322
|
+
retrievalContract: "native-auto-v1"
|
|
1304
1323
|
};
|
|
1305
1324
|
},
|
|
1306
1325
|
normalizeResult(raw) {
|
|
@@ -1317,7 +1336,8 @@ var openaiAdapter = {
|
|
|
1317
1336
|
answerText: normalized.answerText,
|
|
1318
1337
|
citedDomains: normalized.citedDomains,
|
|
1319
1338
|
groundingSources: normalized.groundingSources,
|
|
1320
|
-
searchQueries: normalized.searchQueries
|
|
1339
|
+
searchQueries: normalized.searchQueries,
|
|
1340
|
+
retrievalStatus: "unknown"
|
|
1321
1341
|
};
|
|
1322
1342
|
},
|
|
1323
1343
|
async generateText(prompt, config) {
|
|
@@ -1350,6 +1370,7 @@ async function withRetry4(fn, options = {}) {
|
|
|
1350
1370
|
// ../provider-claude/src/normalize.ts
|
|
1351
1371
|
var DEFAULT_MODEL3 = "claude-sonnet-4-6";
|
|
1352
1372
|
var VALIDATION_PATTERN = /^claude-/;
|
|
1373
|
+
var CLAUDE_RETRIEVAL_CONTRACT = "search-required-v1";
|
|
1353
1374
|
function resolveModel2(config) {
|
|
1354
1375
|
const m = config.model;
|
|
1355
1376
|
if (!m) return DEFAULT_MODEL3;
|
|
@@ -1424,6 +1445,9 @@ async function executeTrackedQuery3(input) {
|
|
|
1424
1445
|
model,
|
|
1425
1446
|
max_tokens: 4096,
|
|
1426
1447
|
tools: [webSearchTool],
|
|
1448
|
+
// search-required-v1: retrieval is guaranteed by the API control, so the
|
|
1449
|
+
// query text and answer substance stay untouched. See the contract note.
|
|
1450
|
+
tool_choice: { type: "tool", name: "web_search" },
|
|
1427
1451
|
messages: [{ role: "user", content: input.query }]
|
|
1428
1452
|
})
|
|
1429
1453
|
);
|
|
@@ -1438,7 +1462,9 @@ async function executeTrackedQuery3(input) {
|
|
|
1438
1462
|
model,
|
|
1439
1463
|
servedModel: extractServedModel3(rawResponse),
|
|
1440
1464
|
groundingSources: parsed.groundingSources,
|
|
1441
|
-
searchQueries: parsed.searchQueries
|
|
1465
|
+
searchQueries: parsed.searchQueries,
|
|
1466
|
+
retrievalStatus: parsed.retrievalStatus,
|
|
1467
|
+
retrievalContract: CLAUDE_RETRIEVAL_CONTRACT
|
|
1442
1468
|
};
|
|
1443
1469
|
} catch (err) {
|
|
1444
1470
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1456,7 +1482,8 @@ function normalizeResult3(raw) {
|
|
|
1456
1482
|
answerText: parsed.answerText,
|
|
1457
1483
|
citedDomains,
|
|
1458
1484
|
groundingSources,
|
|
1459
|
-
searchQueries
|
|
1485
|
+
searchQueries,
|
|
1486
|
+
retrievalStatus: useParsed ? parsed.retrievalStatus : raw.retrievalStatus
|
|
1460
1487
|
};
|
|
1461
1488
|
}
|
|
1462
1489
|
function hasParsedResponseContent3(rawResponse) {
|
|
@@ -1475,6 +1502,7 @@ function reparseStoredResult3(rawResponse) {
|
|
|
1475
1502
|
citedDomains: extractCitedDomainsFromSources3(groundingSources),
|
|
1476
1503
|
groundingSources,
|
|
1477
1504
|
searchQueries,
|
|
1505
|
+
retrievalStatus: extractRetrievalStatusFromRaw(rawResponse),
|
|
1478
1506
|
...providerErrors.length > 0 ? { providerError: `web_search tool error: ${providerErrors.join(", ")}` } : {}
|
|
1479
1507
|
};
|
|
1480
1508
|
}
|
|
@@ -1514,6 +1542,17 @@ function extractGroundingSourcesFromRaw2(rawResponse) {
|
|
|
1514
1542
|
}
|
|
1515
1543
|
return sources;
|
|
1516
1544
|
}
|
|
1545
|
+
function extractRetrievalStatusFromRaw(rawResponse) {
|
|
1546
|
+
try {
|
|
1547
|
+
const content = rawResponse.content;
|
|
1548
|
+
if (!Array.isArray(content) || content.length === 0) return "unknown";
|
|
1549
|
+
return content.some(
|
|
1550
|
+
(block) => block.type === "server_tool_use" && block.name === "web_search"
|
|
1551
|
+
) ? "used" : "not-used";
|
|
1552
|
+
} catch {
|
|
1553
|
+
return "unknown";
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1517
1556
|
function extractSearchQueriesFromRaw3(rawResponse) {
|
|
1518
1557
|
const queries2 = /* @__PURE__ */ new Set();
|
|
1519
1558
|
try {
|
|
@@ -1661,7 +1700,9 @@ var claudeAdapter = {
|
|
|
1661
1700
|
model: raw.model,
|
|
1662
1701
|
servedModel: raw.servedModel,
|
|
1663
1702
|
groundingSources: raw.groundingSources,
|
|
1664
|
-
searchQueries: raw.searchQueries
|
|
1703
|
+
searchQueries: raw.searchQueries,
|
|
1704
|
+
retrievalStatus: raw.retrievalStatus,
|
|
1705
|
+
retrievalContract: raw.retrievalContract
|
|
1665
1706
|
};
|
|
1666
1707
|
},
|
|
1667
1708
|
normalizeResult(raw) {
|
|
@@ -1670,7 +1711,12 @@ var claudeAdapter = {
|
|
|
1670
1711
|
rawResponse: raw.rawResponse,
|
|
1671
1712
|
model: raw.model,
|
|
1672
1713
|
groundingSources: raw.groundingSources,
|
|
1673
|
-
searchQueries: raw.searchQueries
|
|
1714
|
+
searchQueries: raw.searchQueries,
|
|
1715
|
+
// The shared RawQueryResult now carries retrieval, so nothing is lost
|
|
1716
|
+
// across this boundary. A reconstruction that predates the field falls
|
|
1717
|
+
// back to `unknown` rather than asserting an absence.
|
|
1718
|
+
retrievalStatus: raw.retrievalStatus ?? "unknown",
|
|
1719
|
+
retrievalContract: raw.retrievalContract ?? CLAUDE_RETRIEVAL_CONTRACT
|
|
1674
1720
|
};
|
|
1675
1721
|
const normalized = normalizeResult3(claudeRaw);
|
|
1676
1722
|
return {
|
|
@@ -1678,7 +1724,8 @@ var claudeAdapter = {
|
|
|
1678
1724
|
answerText: normalized.answerText,
|
|
1679
1725
|
citedDomains: normalized.citedDomains,
|
|
1680
1726
|
groundingSources: normalized.groundingSources,
|
|
1681
|
-
searchQueries: normalized.searchQueries
|
|
1727
|
+
searchQueries: normalized.searchQueries,
|
|
1728
|
+
retrievalStatus: normalized.retrievalStatus
|
|
1682
1729
|
};
|
|
1683
1730
|
},
|
|
1684
1731
|
async generateText(prompt, config) {
|
|
@@ -1894,7 +1941,11 @@ var localAdapter = {
|
|
|
1894
1941
|
model: raw.model,
|
|
1895
1942
|
servedModel: raw.servedModel,
|
|
1896
1943
|
groundingSources: raw.groundingSources,
|
|
1897
|
-
searchQueries: raw.searchQueries
|
|
1944
|
+
searchQueries: raw.searchQueries,
|
|
1945
|
+
// A plain chat completion with no web access: there is no retrieval step
|
|
1946
|
+
// to report, which is what `not-applicable` means.
|
|
1947
|
+
retrievalStatus: "not-applicable",
|
|
1948
|
+
retrievalContract: "native-auto-v1"
|
|
1898
1949
|
};
|
|
1899
1950
|
},
|
|
1900
1951
|
normalizeResult(raw) {
|
|
@@ -1911,7 +1962,8 @@ var localAdapter = {
|
|
|
1911
1962
|
answerText: normalized.answerText,
|
|
1912
1963
|
citedDomains: normalized.citedDomains,
|
|
1913
1964
|
groundingSources: normalized.groundingSources,
|
|
1914
|
-
searchQueries: normalized.searchQueries
|
|
1965
|
+
searchQueries: normalized.searchQueries,
|
|
1966
|
+
retrievalStatus: "not-applicable"
|
|
1915
1967
|
};
|
|
1916
1968
|
},
|
|
1917
1969
|
async generateText(prompt, config) {
|
|
@@ -2360,7 +2412,8 @@ function normalizeResult5(raw) {
|
|
|
2360
2412
|
answerText,
|
|
2361
2413
|
citedDomains: extractCitedDomains(raw.groundingSources),
|
|
2362
2414
|
groundingSources: raw.groundingSources,
|
|
2363
|
-
searchQueries: raw.searchQueries
|
|
2415
|
+
searchQueries: raw.searchQueries,
|
|
2416
|
+
retrievalStatus: raw.retrievalStatus ?? "unknown"
|
|
2364
2417
|
};
|
|
2365
2418
|
}
|
|
2366
2419
|
|
|
@@ -2470,6 +2523,12 @@ var cdpChatgptAdapter = {
|
|
|
2470
2523
|
model: "chatgpt-web",
|
|
2471
2524
|
groundingSources,
|
|
2472
2525
|
searchQueries: [input.query],
|
|
2526
|
+
// This surface is scraped from the rendered web UI, which exposes no
|
|
2527
|
+
// reliable signal for whether a retrieval step ran behind the answer.
|
|
2528
|
+
// `searchQueries` here is the query we submitted, not evidence of a
|
|
2529
|
+
// search the product performed.
|
|
2530
|
+
retrievalStatus: "unknown",
|
|
2531
|
+
retrievalContract: "native-auto-v1",
|
|
2473
2532
|
screenshotPath: capturedScreenshotPath
|
|
2474
2533
|
};
|
|
2475
2534
|
} catch (err) {
|
|
@@ -2784,7 +2843,14 @@ var perplexityAdapter = {
|
|
|
2784
2843
|
model: raw.model,
|
|
2785
2844
|
servedModel: raw.servedModel,
|
|
2786
2845
|
groundingSources: raw.groundingSources,
|
|
2787
|
-
searchQueries: raw.searchQueries
|
|
2846
|
+
searchQueries: raw.searchQueries,
|
|
2847
|
+
// Retrieval detection is not implemented for this provider. Its candidate
|
|
2848
|
+
// marker is present on 100% of stored rows, so it has never been shown to
|
|
2849
|
+
// discriminate a non-retrieving answer and wiring it up would hardcode
|
|
2850
|
+
// `used`. `unknown` states what we actually know. The contract is a
|
|
2851
|
+
// declaration about how we build the request, so it is always knowable.
|
|
2852
|
+
retrievalStatus: "unknown",
|
|
2853
|
+
retrievalContract: "native-auto-v1"
|
|
2788
2854
|
};
|
|
2789
2855
|
},
|
|
2790
2856
|
normalizeResult(raw) {
|
|
@@ -2801,7 +2867,8 @@ var perplexityAdapter = {
|
|
|
2801
2867
|
answerText: normalized.answerText,
|
|
2802
2868
|
citedDomains: normalized.citedDomains,
|
|
2803
2869
|
groundingSources: normalized.groundingSources,
|
|
2804
|
-
searchQueries: normalized.searchQueries
|
|
2870
|
+
searchQueries: normalized.searchQueries,
|
|
2871
|
+
retrievalStatus: "unknown"
|
|
2805
2872
|
};
|
|
2806
2873
|
},
|
|
2807
2874
|
async generateText(prompt, config) {
|
|
@@ -3620,6 +3687,11 @@ var JobRunner = class {
|
|
|
3620
3687
|
sourceCount: citedUrlCapture.sourceCount,
|
|
3621
3688
|
resolvedCount: citedUrlCapture.resolvedCount,
|
|
3622
3689
|
captureVersion: citedUrlCapture.captureVersion,
|
|
3690
|
+
// Retrieval is the adapter's own observation, never inferred
|
|
3691
|
+
// from citation counts. Both branches record it so no snapshot
|
|
3692
|
+
// can be written unmarked.
|
|
3693
|
+
retrievalStatus: normalized.retrievalStatus,
|
|
3694
|
+
retrievalContract: raw.retrievalContract,
|
|
3623
3695
|
competitorOverlap: overlap,
|
|
3624
3696
|
recommendedCompetitors: extractedCompetitors,
|
|
3625
3697
|
location: runLocation?.label ?? null,
|
|
@@ -3651,6 +3723,11 @@ var JobRunner = class {
|
|
|
3651
3723
|
sourceCount: citedUrlCapture.sourceCount,
|
|
3652
3724
|
resolvedCount: citedUrlCapture.resolvedCount,
|
|
3653
3725
|
captureVersion: citedUrlCapture.captureVersion,
|
|
3726
|
+
// Retrieval is the adapter's own observation, never inferred
|
|
3727
|
+
// from citation counts. Both branches record it so no snapshot
|
|
3728
|
+
// can be written unmarked.
|
|
3729
|
+
retrievalStatus: normalized.retrievalStatus,
|
|
3730
|
+
retrievalContract: raw.retrievalContract,
|
|
3654
3731
|
competitorOverlap: overlap,
|
|
3655
3732
|
recommendedCompetitors: extractedCompetitors,
|
|
3656
3733
|
location: runLocation?.label ?? null,
|
|
@@ -6087,155 +6164,10 @@ function computeSummary2(rows) {
|
|
|
6087
6164
|
return computeBacklinkSummaryMetrics(rows);
|
|
6088
6165
|
}
|
|
6089
6166
|
|
|
6090
|
-
// src/
|
|
6167
|
+
// src/discovery-run.ts
|
|
6091
6168
|
import crypto13 from "crypto";
|
|
6092
6169
|
import { and as and9, eq as eq11 } from "drizzle-orm";
|
|
6093
|
-
var log11 = createLogger("
|
|
6094
|
-
var DEFAULT_MAX_PAGES = 200;
|
|
6095
|
-
var LINK_COUNTS_MAX_PAGES = 50;
|
|
6096
|
-
var URL_LINKS_MAX_PAGES = 20;
|
|
6097
|
-
function bingReleaseId(date) {
|
|
6098
|
-
return `bing-${date.toISOString().slice(0, 10)}`;
|
|
6099
|
-
}
|
|
6100
|
-
function aggregateInboundLinksByDomain(links, targetDomain) {
|
|
6101
|
-
const target = hostOf(targetDomain);
|
|
6102
|
-
const byHost = /* @__PURE__ */ new Map();
|
|
6103
|
-
for (const link of links) {
|
|
6104
|
-
const host = hostOf(link.Url);
|
|
6105
|
-
if (!host) continue;
|
|
6106
|
-
if (target && hostMatchesDomain(host, target)) continue;
|
|
6107
|
-
let urls = byHost.get(host);
|
|
6108
|
-
if (!urls) {
|
|
6109
|
-
urls = /* @__PURE__ */ new Set();
|
|
6110
|
-
byHost.set(host, urls);
|
|
6111
|
-
}
|
|
6112
|
-
urls.add(link.Url);
|
|
6113
|
-
}
|
|
6114
|
-
return [...byHost.entries()].map(([linkingDomain, urls]) => ({ linkingDomain, numHosts: urls.size })).sort((a, b) => b.numHosts - a.numHosts || a.linkingDomain.localeCompare(b.linkingDomain));
|
|
6115
|
-
}
|
|
6116
|
-
function computeBingSummary(rows) {
|
|
6117
|
-
return computeBacklinkSummaryMetrics(rows);
|
|
6118
|
-
}
|
|
6119
|
-
function defaultDeps4() {
|
|
6120
|
-
return { getLinkCounts, getUrlLinks, now: () => /* @__PURE__ */ new Date() };
|
|
6121
|
-
}
|
|
6122
|
-
async function executeBingBacklinkSync(db, runId, projectId, opts) {
|
|
6123
|
-
const deps = { ...defaultDeps4(), ...opts.deps };
|
|
6124
|
-
const startedAt = deps.now().toISOString();
|
|
6125
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq11(runs.id, runId)).run();
|
|
6126
|
-
try {
|
|
6127
|
-
const project = db.select().from(projects).where(eq11(projects.id, projectId)).get();
|
|
6128
|
-
if (!project) throw new Error(`Project not found: ${projectId}`);
|
|
6129
|
-
const conn = opts.resolveConnection(project.canonicalDomain);
|
|
6130
|
-
if (!conn) throw new Error(`No Bing Webmaster connection for ${project.canonicalDomain}`);
|
|
6131
|
-
const siteUrl = conn.siteUrl;
|
|
6132
|
-
if (!siteUrl) {
|
|
6133
|
-
throw new Error(`Bing connection for ${project.canonicalDomain} has no verified site selected`);
|
|
6134
|
-
}
|
|
6135
|
-
const pages = await deps.getLinkCounts(conn.apiKey, siteUrl, { maxPages: LINK_COUNTS_MAX_PAGES });
|
|
6136
|
-
const maxPages = Math.max(1, opts.maxPages ?? DEFAULT_MAX_PAGES);
|
|
6137
|
-
const targetPages = [...pages].sort((a, b) => b.Count - a.Count).slice(0, maxPages);
|
|
6138
|
-
if (pages.length > targetPages.length) {
|
|
6139
|
-
log11.info("bing-sync.pages-capped", {
|
|
6140
|
-
runId,
|
|
6141
|
-
projectId,
|
|
6142
|
-
sitePagesFound: pages.length,
|
|
6143
|
-
sitePagesPulled: targetPages.length
|
|
6144
|
-
});
|
|
6145
|
-
}
|
|
6146
|
-
const allLinks = [];
|
|
6147
|
-
let pageFailures = 0;
|
|
6148
|
-
for (const page of targetPages) {
|
|
6149
|
-
try {
|
|
6150
|
-
const links = await deps.getUrlLinks(conn.apiKey, siteUrl, page.Url, { maxPages: URL_LINKS_MAX_PAGES });
|
|
6151
|
-
allLinks.push(...links);
|
|
6152
|
-
} catch (err) {
|
|
6153
|
-
pageFailures++;
|
|
6154
|
-
log11.warn("bing-sync.page-failed", {
|
|
6155
|
-
runId,
|
|
6156
|
-
projectId,
|
|
6157
|
-
page: page.Url,
|
|
6158
|
-
error: err instanceof Error ? err.message : String(err)
|
|
6159
|
-
});
|
|
6160
|
-
}
|
|
6161
|
-
}
|
|
6162
|
-
if (targetPages.length > 0 && pageFailures === targetPages.length) {
|
|
6163
|
-
throw new Error(`All ${targetPages.length} Bing inbound-link page fetch(es) failed`);
|
|
6164
|
-
}
|
|
6165
|
-
const rows = aggregateInboundLinksByDomain(allLinks, project.canonicalDomain);
|
|
6166
|
-
const summary = computeBingSummary(rows);
|
|
6167
|
-
const queriedAt = deps.now().toISOString();
|
|
6168
|
-
const release = bingReleaseId(deps.now());
|
|
6169
|
-
const targetDomain = project.canonicalDomain;
|
|
6170
|
-
const source = BacklinkSources["bing-webmaster"];
|
|
6171
|
-
const existing = db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and9(
|
|
6172
|
-
eq11(backlinkSummaries.projectId, projectId),
|
|
6173
|
-
eq11(backlinkSummaries.source, source),
|
|
6174
|
-
eq11(backlinkSummaries.release, release)
|
|
6175
|
-
)).get();
|
|
6176
|
-
const preserveExisting = pageFailures > 0 && !!existing;
|
|
6177
|
-
if (!preserveExisting) {
|
|
6178
|
-
db.transaction((tx) => {
|
|
6179
|
-
tx.delete(backlinkDomains).where(and9(
|
|
6180
|
-
eq11(backlinkDomains.projectId, projectId),
|
|
6181
|
-
eq11(backlinkDomains.source, source),
|
|
6182
|
-
eq11(backlinkDomains.release, release)
|
|
6183
|
-
)).run();
|
|
6184
|
-
if (rows.length > 0) {
|
|
6185
|
-
tx.insert(backlinkDomains).values(rows.map((r) => ({
|
|
6186
|
-
id: crypto13.randomUUID(),
|
|
6187
|
-
projectId,
|
|
6188
|
-
releaseSyncId: null,
|
|
6189
|
-
source,
|
|
6190
|
-
release,
|
|
6191
|
-
targetDomain,
|
|
6192
|
-
linkingDomain: r.linkingDomain,
|
|
6193
|
-
numHosts: r.numHosts,
|
|
6194
|
-
createdAt: queriedAt
|
|
6195
|
-
}))).run();
|
|
6196
|
-
}
|
|
6197
|
-
tx.insert(backlinkSummaries).values({
|
|
6198
|
-
id: crypto13.randomUUID(),
|
|
6199
|
-
projectId,
|
|
6200
|
-
releaseSyncId: null,
|
|
6201
|
-
source,
|
|
6202
|
-
release,
|
|
6203
|
-
targetDomain,
|
|
6204
|
-
totalLinkingDomains: summary.totalLinkingDomains,
|
|
6205
|
-
totalHosts: summary.totalHosts,
|
|
6206
|
-
top10HostsShare: summary.top10HostsShare,
|
|
6207
|
-
queriedAt,
|
|
6208
|
-
createdAt: queriedAt
|
|
6209
|
-
}).onConflictDoUpdate({
|
|
6210
|
-
target: [backlinkSummaries.projectId, backlinkSummaries.source, backlinkSummaries.release],
|
|
6211
|
-
set: {
|
|
6212
|
-
targetDomain,
|
|
6213
|
-
totalLinkingDomains: summary.totalLinkingDomains,
|
|
6214
|
-
totalHosts: summary.totalHosts,
|
|
6215
|
-
top10HostsShare: summary.top10HostsShare,
|
|
6216
|
-
queriedAt
|
|
6217
|
-
}
|
|
6218
|
-
}).run();
|
|
6219
|
-
});
|
|
6220
|
-
}
|
|
6221
|
-
const finishedAt = deps.now().toISOString();
|
|
6222
|
-
const status = pageFailures > 0 ? RunStatuses.partial : RunStatuses.completed;
|
|
6223
|
-
const error = preserveExisting ? `Kept existing ${release} snapshot; ${pageFailures} of ${targetPages.length} inbound-link page fetches failed` : pageFailures > 0 ? `${pageFailures} of ${targetPages.length} inbound-link page fetches failed` : null;
|
|
6224
|
-
db.update(runs).set({ status, error, finishedAt }).where(eq11(runs.id, runId)).run();
|
|
6225
|
-
log11.info("bing-sync.completed", { runId, projectId, release, rows: rows.length, status, pageFailures, preserveExisting });
|
|
6226
|
-
} catch (err) {
|
|
6227
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
6228
|
-
const finishedAt = deps.now().toISOString();
|
|
6229
|
-
db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt }).where(eq11(runs.id, runId)).run();
|
|
6230
|
-
log11.error("bing-sync.failed", { runId, projectId, error: errorMsg });
|
|
6231
|
-
throw err;
|
|
6232
|
-
}
|
|
6233
|
-
}
|
|
6234
|
-
|
|
6235
|
-
// src/discovery-run.ts
|
|
6236
|
-
import crypto14 from "crypto";
|
|
6237
|
-
import { and as and10, eq as eq12 } from "drizzle-orm";
|
|
6238
|
-
var log12 = createLogger("DiscoveryRun");
|
|
6170
|
+
var log11 = createLogger("DiscoveryRun");
|
|
6239
6171
|
var DEFAULT_SEED_COUNT = 30;
|
|
6240
6172
|
var EMBED_RETRY_MAX_RETRIES = 3;
|
|
6241
6173
|
var EMBED_RETRY_MAX_DELAY_MS = 1e4;
|
|
@@ -6268,7 +6200,7 @@ async function embedWithRetry(fn, opts = {}) {
|
|
|
6268
6200
|
maxDelayMs: EMBED_RETRY_MAX_DELAY_MS,
|
|
6269
6201
|
sleep: opts.sleep,
|
|
6270
6202
|
onRetry: opts.onRetry ?? (({ attempt, delayMs }) => {
|
|
6271
|
-
|
|
6203
|
+
log11.warn("discovery.embed.retry", { attempt, delayMs });
|
|
6272
6204
|
})
|
|
6273
6205
|
}
|
|
6274
6206
|
);
|
|
@@ -6276,11 +6208,11 @@ async function embedWithRetry(fn, opts = {}) {
|
|
|
6276
6208
|
var QUERIES_PER_INTENT_BUCKET = 6;
|
|
6277
6209
|
async function executeDiscoveryRun(opts) {
|
|
6278
6210
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6279
|
-
opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
6211
|
+
opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq11(runs.id, opts.runId)).run();
|
|
6280
6212
|
try {
|
|
6281
|
-
const projectRow = opts.db.select().from(projects).where(
|
|
6213
|
+
const projectRow = opts.db.select().from(projects).where(eq11(projects.id, opts.projectId)).get();
|
|
6282
6214
|
if (!projectRow) throw new Error(`Project ${opts.projectId} not found`);
|
|
6283
|
-
const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
6215
|
+
const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq11(competitors.projectId, opts.projectId)).all().map((r) => r.domain.toLowerCase());
|
|
6284
6216
|
const canonicalDomains = effectiveDomains({
|
|
6285
6217
|
canonicalDomain: projectRow.canonicalDomain,
|
|
6286
6218
|
ownedDomains: projectRow.ownedDomains
|
|
@@ -6318,8 +6250,8 @@ async function executeDiscoveryRun(opts) {
|
|
|
6318
6250
|
seedProvider: result.seedProvider,
|
|
6319
6251
|
result
|
|
6320
6252
|
});
|
|
6321
|
-
opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
6322
|
-
|
|
6253
|
+
opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq11(runs.id, opts.runId)).run();
|
|
6254
|
+
log11.info("discovery.completed", {
|
|
6323
6255
|
runId: opts.runId,
|
|
6324
6256
|
sessionId: opts.sessionId,
|
|
6325
6257
|
buckets: result.buckets,
|
|
@@ -6327,13 +6259,13 @@ async function executeDiscoveryRun(opts) {
|
|
|
6327
6259
|
});
|
|
6328
6260
|
} catch (err) {
|
|
6329
6261
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
6330
|
-
|
|
6262
|
+
log11.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
|
|
6331
6263
|
markSessionFailed(opts.db, opts.sessionId, errorMsg);
|
|
6332
6264
|
opts.db.update(runs).set({
|
|
6333
6265
|
status: RunStatuses.failed,
|
|
6334
6266
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6335
6267
|
error: errorMsg
|
|
6336
|
-
}).where(
|
|
6268
|
+
}).where(eq11(runs.id, opts.runId)).run();
|
|
6337
6269
|
}
|
|
6338
6270
|
}
|
|
6339
6271
|
function buildDefaultDeps(registry) {
|
|
@@ -6582,13 +6514,13 @@ function writeDiscoveryInsight(db, input) {
|
|
|
6582
6514
|
totalProbes
|
|
6583
6515
|
});
|
|
6584
6516
|
db.transaction((tx) => {
|
|
6585
|
-
tx.update(insights).set({ dismissed: true }).where(
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6517
|
+
tx.update(insights).set({ dismissed: true }).where(and9(
|
|
6518
|
+
eq11(insights.projectId, input.projectId),
|
|
6519
|
+
eq11(insights.type, "discovery.basket-divergence"),
|
|
6520
|
+
eq11(insights.dismissed, false)
|
|
6589
6521
|
)).run();
|
|
6590
6522
|
tx.insert(insights).values({
|
|
6591
|
-
id:
|
|
6523
|
+
id: crypto13.randomUUID(),
|
|
6592
6524
|
projectId: input.projectId,
|
|
6593
6525
|
runId: input.runId,
|
|
6594
6526
|
type: "discovery.basket-divergence",
|
|
@@ -6624,10 +6556,10 @@ function buildDiscoveryInsightTitle(input) {
|
|
|
6624
6556
|
}
|
|
6625
6557
|
|
|
6626
6558
|
// src/execute-site-audit.ts
|
|
6627
|
-
import
|
|
6628
|
-
import { eq as
|
|
6559
|
+
import crypto14 from "crypto";
|
|
6560
|
+
import { eq as eq12 } from "drizzle-orm";
|
|
6629
6561
|
import { runSitemapAudit } from "@ainyc/aeo-audit";
|
|
6630
|
-
var
|
|
6562
|
+
var log12 = createLogger("SiteAudit");
|
|
6631
6563
|
var SITE_AUDIT_DEFAULT_PAGE_LIMIT = 500;
|
|
6632
6564
|
var SITE_AUDIT_MAX_PAGE_LIMIT = 2e3;
|
|
6633
6565
|
function toHomepageUrl(canonicalDomain) {
|
|
@@ -6688,15 +6620,15 @@ function computeFactorAverages(pages) {
|
|
|
6688
6620
|
}
|
|
6689
6621
|
async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
6690
6622
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6691
|
-
db.update(runs).set({ status: "running", startedAt }).where(
|
|
6623
|
+
db.update(runs).set({ status: "running", startedAt }).where(eq12(runs.id, runId)).run();
|
|
6692
6624
|
try {
|
|
6693
|
-
const project = db.select().from(projects).where(
|
|
6625
|
+
const project = db.select().from(projects).where(eq12(projects.id, projectId)).get();
|
|
6694
6626
|
if (!project) {
|
|
6695
6627
|
throw new Error(`Project not found: ${projectId}`);
|
|
6696
6628
|
}
|
|
6697
6629
|
const homepageUrl = toHomepageUrl(project.canonicalDomain);
|
|
6698
6630
|
const limit = clampSiteAuditLimit(opts.limit);
|
|
6699
|
-
|
|
6631
|
+
log12.info("start", { runId, projectId, homepageUrl, sitemapUrl: opts.sitemapUrl ?? null, limit });
|
|
6700
6632
|
await assertSiteAuditUrlAllowed(homepageUrl, "canonicalDomain");
|
|
6701
6633
|
if (opts.sitemapUrl) await assertSiteAuditUrlAllowed(opts.sitemapUrl, "sitemapUrl");
|
|
6702
6634
|
const report = await runSitemapAudit(homepageUrl, { sitemapUrl: opts.sitemapUrl, limit });
|
|
@@ -6704,7 +6636,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
6704
6636
|
const pagesErrored = report.pages.filter((page) => page.status === "error").length;
|
|
6705
6637
|
const auditable = report.pagesDiscovered - report.pagesSkipped;
|
|
6706
6638
|
if (auditable > report.pagesAudited) {
|
|
6707
|
-
|
|
6639
|
+
log12.info("truncated", {
|
|
6708
6640
|
runId,
|
|
6709
6641
|
projectId,
|
|
6710
6642
|
auditable,
|
|
@@ -6723,7 +6655,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
6723
6655
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6724
6656
|
db.transaction((tx) => {
|
|
6725
6657
|
tx.insert(siteAuditSnapshots).values({
|
|
6726
|
-
id:
|
|
6658
|
+
id: crypto14.randomUUID(),
|
|
6727
6659
|
projectId,
|
|
6728
6660
|
runId,
|
|
6729
6661
|
sitemapUrl: report.sitemapUrl,
|
|
@@ -6752,7 +6684,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
6752
6684
|
}).run();
|
|
6753
6685
|
for (const page of report.pages) {
|
|
6754
6686
|
tx.insert(siteAuditPages).values({
|
|
6755
|
-
id:
|
|
6687
|
+
id: crypto14.randomUUID(),
|
|
6756
6688
|
projectId,
|
|
6757
6689
|
runId,
|
|
6758
6690
|
url: page.url,
|
|
@@ -6763,9 +6695,9 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
6763
6695
|
createdAt: finishedAt
|
|
6764
6696
|
}).run();
|
|
6765
6697
|
}
|
|
6766
|
-
tx.update(runs).set({ status, finishedAt }).where(
|
|
6698
|
+
tx.update(runs).set({ status, finishedAt }).where(eq12(runs.id, runId)).run();
|
|
6767
6699
|
});
|
|
6768
|
-
|
|
6700
|
+
log12.info("completed", {
|
|
6769
6701
|
runId,
|
|
6770
6702
|
projectId,
|
|
6771
6703
|
status,
|
|
@@ -6775,14 +6707,14 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
6775
6707
|
});
|
|
6776
6708
|
} catch (err) {
|
|
6777
6709
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
6778
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
6779
|
-
|
|
6710
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq12(runs.id, runId)).run();
|
|
6711
|
+
log12.error("failed", { runId, projectId, error: errorMsg });
|
|
6780
6712
|
throw err;
|
|
6781
6713
|
}
|
|
6782
6714
|
}
|
|
6783
6715
|
|
|
6784
6716
|
// src/commands/backfill.ts
|
|
6785
|
-
import { and as
|
|
6717
|
+
import { and as and10, eq as eq13, inArray as inArray4, isNull, sql as sql5 } from "drizzle-orm";
|
|
6786
6718
|
var SNAPSHOT_BATCH_SIZE = 500;
|
|
6787
6719
|
async function backfillAnswerVisibilityCommand(opts) {
|
|
6788
6720
|
const config = loadConfig();
|
|
@@ -6790,7 +6722,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
6790
6722
|
migrate(db);
|
|
6791
6723
|
const projectFilter = opts?.project?.trim();
|
|
6792
6724
|
const isDryRun = opts?.dryRun === true;
|
|
6793
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(
|
|
6725
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
6794
6726
|
let examined = 0;
|
|
6795
6727
|
let updated = 0;
|
|
6796
6728
|
let wouldUpdate = 0;
|
|
@@ -6798,10 +6730,10 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
6798
6730
|
let reparsed = 0;
|
|
6799
6731
|
let providerErrors = 0;
|
|
6800
6732
|
if (scopedProjects.length > 0) {
|
|
6801
|
-
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(
|
|
6802
|
-
|
|
6733
|
+
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and10(
|
|
6734
|
+
eq13(runs.kind, RunKinds["answer-visibility"]),
|
|
6803
6735
|
inArray4(runs.projectId, scopedProjects.map((project) => project.id))
|
|
6804
|
-
)).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(
|
|
6736
|
+
)).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq13(runs.kind, RunKinds["answer-visibility"])).all();
|
|
6805
6737
|
const runIdsByProject = /* @__PURE__ */ new Map();
|
|
6806
6738
|
for (const run of runRows) {
|
|
6807
6739
|
const existing = runIdsByProject.get(run.projectId);
|
|
@@ -6809,7 +6741,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
6809
6741
|
else runIdsByProject.set(run.projectId, [run.id]);
|
|
6810
6742
|
}
|
|
6811
6743
|
for (const project of scopedProjects) {
|
|
6812
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
6744
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, project.id)).all().map((row) => row.domain);
|
|
6813
6745
|
const runIds = runIdsByProject.get(project.id) ?? [];
|
|
6814
6746
|
if (runIds.length === 0) continue;
|
|
6815
6747
|
const projectDomains = effectiveDomains({
|
|
@@ -6855,7 +6787,12 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
6855
6787
|
answerText,
|
|
6856
6788
|
citedDomains: reparsedResult.citedDomains,
|
|
6857
6789
|
groundingSources: reparsedResult.groundingSources,
|
|
6858
|
-
searchQueries: reparsedResult.searchQueries
|
|
6790
|
+
searchQueries: reparsedResult.searchQueries,
|
|
6791
|
+
// Backfill recomputes mention and citation state from stored
|
|
6792
|
+
// rows; it never observes retrieval and never writes it back
|
|
6793
|
+
// (nextPatch carries no retrieval field). `unknown` keeps this
|
|
6794
|
+
// reconstruction from asserting anything about a search.
|
|
6795
|
+
retrievalStatus: "unknown"
|
|
6859
6796
|
};
|
|
6860
6797
|
const nextCitationState = determineCitationState(normalized, projectDomains);
|
|
6861
6798
|
const nextCitedDomains = reparsedResult.citedDomains;
|
|
@@ -6897,7 +6834,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
6897
6834
|
} else {
|
|
6898
6835
|
db.transaction((tx) => {
|
|
6899
6836
|
for (const update of pendingUpdates) {
|
|
6900
|
-
tx.update(querySnapshots).set(update.patch).where(
|
|
6837
|
+
tx.update(querySnapshots).set(update.patch).where(eq13(querySnapshots.id, update.id)).run();
|
|
6901
6838
|
}
|
|
6902
6839
|
});
|
|
6903
6840
|
updated += pendingUpdates.length;
|
|
@@ -6946,13 +6883,13 @@ No DB writes performed. Re-run without --dry-run to apply.`);
|
|
|
6946
6883
|
function backfillNormalizedPaths(db, opts) {
|
|
6947
6884
|
const baseConditions = [];
|
|
6948
6885
|
if (opts?.projectId) {
|
|
6949
|
-
baseConditions.push(
|
|
6886
|
+
baseConditions.push(eq13(gaTrafficSnapshots.projectId, opts.projectId));
|
|
6950
6887
|
}
|
|
6951
6888
|
const rows = db.select({
|
|
6952
6889
|
id: gaTrafficSnapshots.id,
|
|
6953
6890
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
6954
6891
|
landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
|
|
6955
|
-
}).from(gaTrafficSnapshots).where(baseConditions.length > 0 ?
|
|
6892
|
+
}).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and10(...baseConditions) : void 0).all();
|
|
6956
6893
|
let updated = 0;
|
|
6957
6894
|
let unchanged = 0;
|
|
6958
6895
|
if (rows.length > 0) {
|
|
@@ -6967,7 +6904,7 @@ function backfillNormalizedPaths(db, opts) {
|
|
|
6967
6904
|
unchanged++;
|
|
6968
6905
|
continue;
|
|
6969
6906
|
}
|
|
6970
|
-
tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(
|
|
6907
|
+
tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq13(gaTrafficSnapshots.id, row.id)).run();
|
|
6971
6908
|
updated++;
|
|
6972
6909
|
}
|
|
6973
6910
|
});
|
|
@@ -6981,7 +6918,7 @@ async function backfillNormalizedPathsCommand(opts) {
|
|
|
6981
6918
|
const projectFilter = opts?.project?.trim();
|
|
6982
6919
|
let projectId;
|
|
6983
6920
|
if (projectFilter) {
|
|
6984
|
-
const project = db.select({ id: projects.id }).from(projects).where(
|
|
6921
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq13(projects.name, projectFilter)).get();
|
|
6985
6922
|
if (!project) {
|
|
6986
6923
|
const result2 = {
|
|
6987
6924
|
project: projectFilter,
|
|
@@ -7018,13 +6955,13 @@ async function backfillNormalizedPathsCommand(opts) {
|
|
|
7018
6955
|
function backfillAiReferralPaths(db, opts) {
|
|
7019
6956
|
const baseConditions = [];
|
|
7020
6957
|
if (opts?.projectId) {
|
|
7021
|
-
baseConditions.push(
|
|
6958
|
+
baseConditions.push(eq13(gaAiReferrals.projectId, opts.projectId));
|
|
7022
6959
|
}
|
|
7023
6960
|
const rows = db.select({
|
|
7024
6961
|
id: gaAiReferrals.id,
|
|
7025
6962
|
landingPage: gaAiReferrals.landingPage,
|
|
7026
6963
|
landingPageNormalized: gaAiReferrals.landingPageNormalized
|
|
7027
|
-
}).from(gaAiReferrals).where(baseConditions.length > 0 ?
|
|
6964
|
+
}).from(gaAiReferrals).where(baseConditions.length > 0 ? and10(...baseConditions) : void 0).all();
|
|
7028
6965
|
let updated = 0;
|
|
7029
6966
|
let unchanged = 0;
|
|
7030
6967
|
if (rows.length > 0) {
|
|
@@ -7039,7 +6976,7 @@ function backfillAiReferralPaths(db, opts) {
|
|
|
7039
6976
|
unchanged++;
|
|
7040
6977
|
continue;
|
|
7041
6978
|
}
|
|
7042
|
-
tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(
|
|
6979
|
+
tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq13(gaAiReferrals.id, row.id)).run();
|
|
7043
6980
|
updated++;
|
|
7044
6981
|
}
|
|
7045
6982
|
});
|
|
@@ -7053,7 +6990,7 @@ async function backfillAiReferralPathsCommand(opts) {
|
|
|
7053
6990
|
const projectFilter = opts?.project?.trim();
|
|
7054
6991
|
let projectId;
|
|
7055
6992
|
if (projectFilter) {
|
|
7056
|
-
const project = db.select({ id: projects.id }).from(projects).where(
|
|
6993
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq13(projects.name, projectFilter)).get();
|
|
7057
6994
|
if (!project) {
|
|
7058
6995
|
const result2 = {
|
|
7059
6996
|
project: projectFilter,
|
|
@@ -7089,10 +7026,10 @@ async function backfillAiReferralPathsCommand(opts) {
|
|
|
7089
7026
|
}
|
|
7090
7027
|
function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
7091
7028
|
const isDryRun = opts?.dryRun === true;
|
|
7092
|
-
const project = db.select().from(projects).where(
|
|
7029
|
+
const project = db.select().from(projects).where(eq13(projects.id, projectId)).get();
|
|
7093
7030
|
if (!project) return { examined: 0, updated: 0, mentioned: 0 };
|
|
7094
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
7095
|
-
const runRows = db.select({ id: runs.id }).from(runs).where(
|
|
7031
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
7032
|
+
const runRows = db.select({ id: runs.id }).from(runs).where(and10(eq13(runs.kind, RunKinds["answer-visibility"]), eq13(runs.projectId, projectId))).all();
|
|
7096
7033
|
const runIds = runRows.map((r) => r.id);
|
|
7097
7034
|
let examined = 0;
|
|
7098
7035
|
let updated = 0;
|
|
@@ -7134,6 +7071,8 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
|
7134
7071
|
answerText,
|
|
7135
7072
|
citedDomains,
|
|
7136
7073
|
groundingSources,
|
|
7074
|
+
// See the note above: retrieval is not observed or written here.
|
|
7075
|
+
retrievalStatus: "unknown",
|
|
7137
7076
|
searchQueries: []
|
|
7138
7077
|
};
|
|
7139
7078
|
const nextCompetitorOverlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
@@ -7164,7 +7103,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
|
7164
7103
|
} else {
|
|
7165
7104
|
db.transaction((tx) => {
|
|
7166
7105
|
for (const update of pendingUpdates) {
|
|
7167
|
-
tx.update(querySnapshots).set(update.patch).where(
|
|
7106
|
+
tx.update(querySnapshots).set(update.patch).where(eq13(querySnapshots.id, update.id)).run();
|
|
7168
7107
|
}
|
|
7169
7108
|
});
|
|
7170
7109
|
updated += pendingUpdates.length;
|
|
@@ -7179,7 +7118,7 @@ async function backfillAnswerMentionsCommand(opts) {
|
|
|
7179
7118
|
migrate(db);
|
|
7180
7119
|
const projectFilter = opts?.project?.trim();
|
|
7181
7120
|
const isDryRun = opts?.dryRun === true;
|
|
7182
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(
|
|
7121
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
7183
7122
|
let examined = 0;
|
|
7184
7123
|
let updated = 0;
|
|
7185
7124
|
let wouldUpdate = 0;
|
|
@@ -7239,7 +7178,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
7239
7178
|
return result;
|
|
7240
7179
|
}
|
|
7241
7180
|
async function backfillInsightsCommand(project, opts) {
|
|
7242
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
7181
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-BGH2MGHB.js");
|
|
7243
7182
|
const config = loadConfig();
|
|
7244
7183
|
const db = createClient(config.database);
|
|
7245
7184
|
migrate(db);
|
|
@@ -7398,7 +7337,7 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
7398
7337
|
const config = loadConfig();
|
|
7399
7338
|
const db = createClient(config.database);
|
|
7400
7339
|
migrate(db);
|
|
7401
|
-
const project = db.select().from(projects).where(
|
|
7340
|
+
const project = db.select().from(projects).where(eq13(projects.name, opts.project)).get();
|
|
7402
7341
|
if (!project) {
|
|
7403
7342
|
throw new Error(`Project "${opts.project}" not found`);
|
|
7404
7343
|
}
|
|
@@ -7409,8 +7348,8 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
7409
7348
|
process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
|
|
7410
7349
|
`);
|
|
7411
7350
|
}
|
|
7412
|
-
const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(
|
|
7413
|
-
|
|
7351
|
+
const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and10(
|
|
7352
|
+
eq13(auditLog.projectId, project.id),
|
|
7414
7353
|
inArray4(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
|
|
7415
7354
|
)).orderBy(auditLog.createdAt).all();
|
|
7416
7355
|
const history = replayQueryAuditLog(events);
|
|
@@ -7418,8 +7357,8 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
7418
7357
|
runId: runs.id,
|
|
7419
7358
|
createdAt: runs.createdAt,
|
|
7420
7359
|
location: runs.location
|
|
7421
|
-
}).from(runs).innerJoin(querySnapshots,
|
|
7422
|
-
|
|
7360
|
+
}).from(runs).innerJoin(querySnapshots, eq13(querySnapshots.runId, runs.id)).where(and10(
|
|
7361
|
+
eq13(runs.projectId, project.id),
|
|
7423
7362
|
isNull(querySnapshots.queryId),
|
|
7424
7363
|
isNull(querySnapshots.queryText)
|
|
7425
7364
|
)).groupBy(runs.id).orderBy(runs.createdAt).all();
|
|
@@ -7441,8 +7380,8 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
7441
7380
|
provider: querySnapshots.provider,
|
|
7442
7381
|
createdAt: querySnapshots.createdAt,
|
|
7443
7382
|
answerText: querySnapshots.answerText
|
|
7444
|
-
}).from(querySnapshots).where(
|
|
7445
|
-
|
|
7383
|
+
}).from(querySnapshots).where(and10(
|
|
7384
|
+
eq13(querySnapshots.runId, run.runId),
|
|
7446
7385
|
isNull(querySnapshots.queryId),
|
|
7447
7386
|
isNull(querySnapshots.queryText)
|
|
7448
7387
|
)).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
|
|
@@ -7508,7 +7447,7 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
7508
7447
|
if (!isDryRun && updates.length > 0) {
|
|
7509
7448
|
db.transaction((tx) => {
|
|
7510
7449
|
for (const u of updates) {
|
|
7511
|
-
tx.update(querySnapshots).set({ queryText: u.queryText }).where(
|
|
7450
|
+
tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq13(querySnapshots.id, u.id)).run();
|
|
7512
7451
|
}
|
|
7513
7452
|
});
|
|
7514
7453
|
}
|
|
@@ -7582,7 +7521,7 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
7582
7521
|
const projectFilter = opts?.project?.trim();
|
|
7583
7522
|
const isDryRun = opts?.dryRun === true;
|
|
7584
7523
|
const isJson = isMachineFormat(opts?.format);
|
|
7585
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(
|
|
7524
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
7586
7525
|
if (scopedProjects.length === 0) {
|
|
7587
7526
|
if (projectFilter && !isJson) {
|
|
7588
7527
|
process.stderr.write(`No project named "${projectFilter}".
|
|
@@ -7607,8 +7546,8 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
7607
7546
|
dryRun: isDryRun,
|
|
7608
7547
|
byBot: {}
|
|
7609
7548
|
};
|
|
7610
|
-
const unknownCountRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(
|
|
7611
|
-
|
|
7549
|
+
const unknownCountRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and10(
|
|
7550
|
+
eq13(rawEventSamples.eventType, "unknown"),
|
|
7612
7551
|
inArray4(rawEventSamples.projectId, projectIds)
|
|
7613
7552
|
)).get();
|
|
7614
7553
|
result.unknownBefore = Number(unknownCountRow?.n ?? 0);
|
|
@@ -7620,8 +7559,8 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
7620
7559
|
userAgent: rawEventSamples.userAgent,
|
|
7621
7560
|
pathNormalized: rawEventSamples.pathNormalized,
|
|
7622
7561
|
status: rawEventSamples.status
|
|
7623
|
-
}).from(rawEventSamples).where(
|
|
7624
|
-
|
|
7562
|
+
}).from(rawEventSamples).where(and10(
|
|
7563
|
+
eq13(rawEventSamples.eventType, "unknown"),
|
|
7625
7564
|
inArray4(rawEventSamples.projectId, projectIds)
|
|
7626
7565
|
)).all();
|
|
7627
7566
|
result.examined = unknownSamples.length;
|
|
@@ -7660,7 +7599,7 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
7660
7599
|
result.reclassified++;
|
|
7661
7600
|
result.byBot[classified.botId] = (result.byBot[classified.botId] ?? 0) + 1;
|
|
7662
7601
|
if (isDryRun) continue;
|
|
7663
|
-
db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(
|
|
7602
|
+
db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq13(rawEventSamples.id, snap.id)).run();
|
|
7664
7603
|
const tsHour = new Date(snap.ts);
|
|
7665
7604
|
tsHour.setUTCMinutes(0, 0, 0);
|
|
7666
7605
|
if (userFetch) {
|
|
@@ -7724,8 +7663,8 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
7724
7663
|
}
|
|
7725
7664
|
}
|
|
7726
7665
|
if (!isDryRun) {
|
|
7727
|
-
const afterRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(
|
|
7728
|
-
|
|
7666
|
+
const afterRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and10(
|
|
7667
|
+
eq13(rawEventSamples.eventType, "unknown"),
|
|
7729
7668
|
inArray4(rawEventSamples.projectId, projectIds)
|
|
7730
7669
|
)).get();
|
|
7731
7670
|
result.unknownAfter = Number(afterRow?.n ?? 0);
|
|
@@ -7760,7 +7699,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
|
|
|
7760
7699
|
}
|
|
7761
7700
|
|
|
7762
7701
|
// src/commands/skills.ts
|
|
7763
|
-
import
|
|
7702
|
+
import crypto15 from "crypto";
|
|
7764
7703
|
import fs4 from "fs";
|
|
7765
7704
|
import os4 from "os";
|
|
7766
7705
|
import path5 from "path";
|
|
@@ -7815,7 +7754,7 @@ function walkRelative(dir, prefix = "") {
|
|
|
7815
7754
|
return out.sort();
|
|
7816
7755
|
}
|
|
7817
7756
|
function sha256File(filePath) {
|
|
7818
|
-
return
|
|
7757
|
+
return crypto15.createHash("sha256").update(fs4.readFileSync(filePath)).digest("hex");
|
|
7819
7758
|
}
|
|
7820
7759
|
function readSkillManifest(skillDir) {
|
|
7821
7760
|
try {
|
|
@@ -8157,10 +8096,11 @@ var ProviderRegistry = class {
|
|
|
8157
8096
|
};
|
|
8158
8097
|
|
|
8159
8098
|
// src/scheduler.ts
|
|
8160
|
-
import
|
|
8099
|
+
import crypto16 from "crypto";
|
|
8161
8100
|
import cron from "node-cron";
|
|
8162
|
-
import { and as
|
|
8163
|
-
var
|
|
8101
|
+
import { and as and11, eq as eq14, inArray as inArray5, notExists, sql as sql6 } from "drizzle-orm";
|
|
8102
|
+
var log13 = createLogger("Scheduler");
|
|
8103
|
+
var DEFAULT_HEALTH_CRON = "0 */6 * * *";
|
|
8164
8104
|
function taskKey(projectId, kind) {
|
|
8165
8105
|
return `${projectId}::${kind}`;
|
|
8166
8106
|
}
|
|
@@ -8172,18 +8112,87 @@ var Scheduler = class {
|
|
|
8172
8112
|
this.db = db;
|
|
8173
8113
|
this.callbacks = callbacks;
|
|
8174
8114
|
}
|
|
8115
|
+
/**
|
|
8116
|
+
* Give every project a health schedule if it has never had one.
|
|
8117
|
+
*
|
|
8118
|
+
* Ships the schedule with the feature. The six checks that existed before
|
|
8119
|
+
* this had never run on a live instance because nothing scheduled them, and
|
|
8120
|
+
* a health check nobody enables is indistinguishable from no health check.
|
|
8121
|
+
*
|
|
8122
|
+
* Keyed on the row EXISTING, not on it being enabled, so an operator who
|
|
8123
|
+
* deliberately turns this off keeps it off across restarts. Seeded here
|
|
8124
|
+
* rather than in a migration because migrations must stay additive to remain
|
|
8125
|
+
* downgrade-safe, and inserting rows is a data mutation.
|
|
8126
|
+
*
|
|
8127
|
+
* Every 6h, not daily: the cadence has to be shorter than the time-to-damage
|
|
8128
|
+
* it guards. A Vercel source begins discarding traffic once it is 24h behind,
|
|
8129
|
+
* so a daily pass could only ever observe the loss after it started.
|
|
8130
|
+
*/
|
|
8131
|
+
ensureHealthSchedules() {
|
|
8132
|
+
const projectsWithoutHealth = this.db.select({ id: projects.id }).from(projects).where(notExists(
|
|
8133
|
+
this.db.select({ one: sql6`1` }).from(schedules).where(and11(
|
|
8134
|
+
eq14(schedules.projectId, projects.id),
|
|
8135
|
+
eq14(schedules.kind, SchedulableRunKinds.doctor)
|
|
8136
|
+
))
|
|
8137
|
+
)).all();
|
|
8138
|
+
if (projectsWithoutHealth.length === 0) return;
|
|
8139
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8140
|
+
for (const project of projectsWithoutHealth) {
|
|
8141
|
+
this.db.insert(schedules).values({
|
|
8142
|
+
id: crypto16.randomUUID(),
|
|
8143
|
+
projectId: project.id,
|
|
8144
|
+
kind: SchedulableRunKinds.doctor,
|
|
8145
|
+
cronExpr: DEFAULT_HEALTH_CRON,
|
|
8146
|
+
timezone: "UTC",
|
|
8147
|
+
enabled: true,
|
|
8148
|
+
providers: [],
|
|
8149
|
+
nextRunAt: nextRunFromCron(DEFAULT_HEALTH_CRON, "UTC"),
|
|
8150
|
+
createdAt: now,
|
|
8151
|
+
updatedAt: now
|
|
8152
|
+
}).run();
|
|
8153
|
+
}
|
|
8154
|
+
log13.info("health-schedule.seeded", { projectCount: projectsWithoutHealth.length, cron: DEFAULT_HEALTH_CRON });
|
|
8155
|
+
}
|
|
8156
|
+
/**
|
|
8157
|
+
* Record each project's current query set as basket revision 1 if it has
|
|
8158
|
+
* never been recorded. Minting normally happens when a sweep is queued, but a
|
|
8159
|
+
* project whose sweeps are manual (the common cadence: twice a month) would
|
|
8160
|
+
* otherwise keep its analytics on the pre-basket date heuristic for weeks
|
|
8161
|
+
* after the feature ships — with the chart still hiding exactly the history
|
|
8162
|
+
* the basket exists to recover. Boot is the moment the current set is known
|
|
8163
|
+
* and nothing has to be guessed, and ensureCurrentQueryBasketRevision is
|
|
8164
|
+
* idempotent, so restarts are no-ops and an unchanged set never churns
|
|
8165
|
+
* revisions. Runs are NOT stamped retroactively: the revision describes the
|
|
8166
|
+
* set as of now, and historical runs keep their null stamp.
|
|
8167
|
+
*/
|
|
8168
|
+
ensureQueryBaskets() {
|
|
8169
|
+
const allProjects = this.db.select({ id: projects.id }).from(projects).all();
|
|
8170
|
+
let minted = 0;
|
|
8171
|
+
for (const project of allProjects) {
|
|
8172
|
+
try {
|
|
8173
|
+
const previous = latestQueryBasketRevision(this.db, project.id)?.revision ?? null;
|
|
8174
|
+
const current = ensureCurrentQueryBasketRevision(this.db, project.id);
|
|
8175
|
+
if (current !== null && current.revision !== previous) minted += 1;
|
|
8176
|
+
} catch (err) {
|
|
8177
|
+
log13.warn("query-basket.mint-failed", { projectId: project.id, err: String(err) });
|
|
8178
|
+
}
|
|
8179
|
+
}
|
|
8180
|
+
if (minted > 0) log13.info("query-basket.ensured", { projectCount: minted });
|
|
8181
|
+
}
|
|
8175
8182
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
8176
8183
|
start() {
|
|
8177
|
-
|
|
8184
|
+
this.ensureHealthSchedules();
|
|
8185
|
+
this.ensureQueryBaskets();
|
|
8186
|
+
const allSchedules = this.db.select().from(schedules).where(eq14(schedules.enabled, true)).all();
|
|
8178
8187
|
for (const schedule of allSchedules) {
|
|
8179
8188
|
const missedRunAt = schedule.nextRunAt;
|
|
8180
8189
|
this.registerCronTask(schedule);
|
|
8181
8190
|
if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
|
|
8182
|
-
|
|
8191
|
+
log13.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
|
|
8183
8192
|
this.triggerRun(schedule.id, schedule.projectId, schedule.kind);
|
|
8184
8193
|
}
|
|
8185
8194
|
}
|
|
8186
|
-
|
|
8195
|
+
log13.info("started", { scheduleCount: allSchedules.length });
|
|
8187
8196
|
}
|
|
8188
8197
|
/** Stop all cron tasks for graceful shutdown. */
|
|
8189
8198
|
stop() {
|
|
@@ -8204,7 +8213,7 @@ var Scheduler = class {
|
|
|
8204
8213
|
this.stopTask(key, existing, "Stopped");
|
|
8205
8214
|
this.tasks.delete(key);
|
|
8206
8215
|
}
|
|
8207
|
-
const schedule = this.db.select().from(schedules).where(
|
|
8216
|
+
const schedule = this.db.select().from(schedules).where(and11(eq14(schedules.projectId, projectId), eq14(schedules.kind, kind))).get();
|
|
8208
8217
|
if (schedule && schedule.enabled) {
|
|
8209
8218
|
this.registerCronTask(schedule);
|
|
8210
8219
|
}
|
|
@@ -8227,13 +8236,13 @@ var Scheduler = class {
|
|
|
8227
8236
|
stopTask(key, task, verb) {
|
|
8228
8237
|
void task.stop();
|
|
8229
8238
|
void task.destroy();
|
|
8230
|
-
|
|
8239
|
+
log13.info(`task.${verb.toLowerCase()}`, { key });
|
|
8231
8240
|
}
|
|
8232
8241
|
registerCronTask(schedule) {
|
|
8233
8242
|
const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
|
|
8234
8243
|
const kind = schedule.kind;
|
|
8235
8244
|
if (!cron.validate(cronExpr)) {
|
|
8236
|
-
|
|
8245
|
+
log13.error("cron.invalid", { projectId, kind, cronExpr });
|
|
8237
8246
|
return;
|
|
8238
8247
|
}
|
|
8239
8248
|
const task = cron.schedule(cronExpr, () => {
|
|
@@ -8245,51 +8254,51 @@ var Scheduler = class {
|
|
|
8245
8254
|
this.db.update(schedules).set({
|
|
8246
8255
|
nextRunAt: nextRunFromCron(cronExpr, timezone),
|
|
8247
8256
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8248
|
-
}).where(
|
|
8257
|
+
}).where(eq14(schedules.id, scheduleId)).run();
|
|
8249
8258
|
const label = schedule.preset ?? cronExpr;
|
|
8250
|
-
|
|
8259
|
+
log13.info("cron.registered", { projectId, kind, schedule: label, timezone });
|
|
8251
8260
|
}
|
|
8252
8261
|
triggerRun(scheduleId, projectId, kind) {
|
|
8253
8262
|
try {
|
|
8254
8263
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8255
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
8264
|
+
const currentSchedule = this.db.select().from(schedules).where(eq14(schedules.id, scheduleId)).get();
|
|
8256
8265
|
if (!currentSchedule || !currentSchedule.enabled) {
|
|
8257
|
-
|
|
8266
|
+
log13.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
|
|
8258
8267
|
this.remove(projectId, kind);
|
|
8259
8268
|
return;
|
|
8260
8269
|
}
|
|
8261
8270
|
const nextRunAt = nextRunFromCron(currentSchedule.cronExpr, currentSchedule.timezone);
|
|
8262
|
-
const project = this.db.select().from(projects).where(
|
|
8271
|
+
const project = this.db.select().from(projects).where(eq14(projects.id, projectId)).get();
|
|
8263
8272
|
if (!project) {
|
|
8264
|
-
|
|
8273
|
+
log13.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
|
|
8265
8274
|
this.remove(projectId, kind);
|
|
8266
8275
|
return;
|
|
8267
8276
|
}
|
|
8268
8277
|
if (kind === SchedulableRunKinds["traffic-sync"]) {
|
|
8269
8278
|
const sourceId = currentSchedule.sourceId;
|
|
8270
8279
|
if (!sourceId) {
|
|
8271
|
-
|
|
8280
|
+
log13.warn("traffic-sync.missing-source", { scheduleId, projectId });
|
|
8272
8281
|
return;
|
|
8273
8282
|
}
|
|
8274
8283
|
if (!this.callbacks.onTrafficSyncRequested) {
|
|
8275
|
-
|
|
8284
|
+
log13.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
|
|
8276
8285
|
return;
|
|
8277
8286
|
}
|
|
8278
8287
|
this.db.update(schedules).set({
|
|
8279
8288
|
lastRunAt: now,
|
|
8280
8289
|
nextRunAt,
|
|
8281
8290
|
updatedAt: now
|
|
8282
|
-
}).where(
|
|
8283
|
-
|
|
8291
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8292
|
+
log13.info("traffic-sync.triggered", { projectName: project.name, sourceId });
|
|
8284
8293
|
this.callbacks.onTrafficSyncRequested(project.name, sourceId);
|
|
8285
8294
|
return;
|
|
8286
8295
|
}
|
|
8287
8296
|
if (kind === SchedulableRunKinds["gbp-sync"]) {
|
|
8288
8297
|
if (!this.callbacks.onGbpSyncRequested) {
|
|
8289
|
-
|
|
8298
|
+
log13.warn("gbp-sync.no-callback", { scheduleId, projectId, msg: "host did not register onGbpSyncRequested" });
|
|
8290
8299
|
return;
|
|
8291
8300
|
}
|
|
8292
|
-
const runId2 =
|
|
8301
|
+
const runId2 = crypto16.randomUUID();
|
|
8293
8302
|
this.db.insert(runs).values({
|
|
8294
8303
|
id: runId2,
|
|
8295
8304
|
projectId,
|
|
@@ -8302,27 +8311,27 @@ var Scheduler = class {
|
|
|
8302
8311
|
lastRunAt: now,
|
|
8303
8312
|
nextRunAt,
|
|
8304
8313
|
updatedAt: now
|
|
8305
|
-
}).where(
|
|
8306
|
-
|
|
8314
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8315
|
+
log13.info("gbp-sync.triggered", { runId: runId2, projectName: project.name });
|
|
8307
8316
|
this.callbacks.onGbpSyncRequested(runId2, projectId);
|
|
8308
8317
|
return;
|
|
8309
8318
|
}
|
|
8310
8319
|
if (kind === SchedulableRunKinds["ads-sync"]) {
|
|
8311
8320
|
if (!this.callbacks.onAdsSyncRequested) {
|
|
8312
|
-
|
|
8321
|
+
log13.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
|
|
8313
8322
|
return;
|
|
8314
8323
|
}
|
|
8315
|
-
const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(
|
|
8316
|
-
|
|
8317
|
-
|
|
8324
|
+
const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and11(
|
|
8325
|
+
eq14(runs.projectId, projectId),
|
|
8326
|
+
eq14(runs.kind, RunKinds["ads-sync"]),
|
|
8318
8327
|
inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
8319
8328
|
)).get();
|
|
8320
8329
|
if (activeAdsRun) {
|
|
8321
|
-
|
|
8322
|
-
this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(
|
|
8330
|
+
log13.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
|
|
8331
|
+
this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8323
8332
|
return;
|
|
8324
8333
|
}
|
|
8325
|
-
const runId2 =
|
|
8334
|
+
const runId2 = crypto16.randomUUID();
|
|
8326
8335
|
this.db.insert(runs).values({
|
|
8327
8336
|
id: runId2,
|
|
8328
8337
|
projectId,
|
|
@@ -8335,55 +8344,69 @@ var Scheduler = class {
|
|
|
8335
8344
|
lastRunAt: now,
|
|
8336
8345
|
nextRunAt,
|
|
8337
8346
|
updatedAt: now
|
|
8338
|
-
}).where(
|
|
8339
|
-
|
|
8347
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8348
|
+
log13.info("ads-sync.triggered", { runId: runId2, projectName: project.name });
|
|
8340
8349
|
this.callbacks.onAdsSyncRequested(runId2, projectId);
|
|
8341
8350
|
return;
|
|
8342
8351
|
}
|
|
8343
8352
|
if (kind === SchedulableRunKinds["data-refresh"]) {
|
|
8344
8353
|
if (!this.callbacks.onDataRefreshRequested) {
|
|
8345
|
-
|
|
8354
|
+
log13.warn("data-refresh.no-callback", { scheduleId, projectId, msg: "host did not register onDataRefreshRequested" });
|
|
8346
8355
|
return;
|
|
8347
8356
|
}
|
|
8348
8357
|
this.db.update(schedules).set({
|
|
8349
8358
|
lastRunAt: now,
|
|
8350
8359
|
nextRunAt,
|
|
8351
8360
|
updatedAt: now
|
|
8352
|
-
}).where(
|
|
8353
|
-
|
|
8361
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8362
|
+
log13.info("data-refresh.triggered", { projectName: project.name });
|
|
8354
8363
|
this.callbacks.onDataRefreshRequested(project.name);
|
|
8355
8364
|
return;
|
|
8356
8365
|
}
|
|
8366
|
+
if (kind === SchedulableRunKinds.doctor) {
|
|
8367
|
+
if (!this.callbacks.onDoctorRequested) {
|
|
8368
|
+
log13.warn("doctor.no-callback", { scheduleId, projectId, msg: "host did not register onDoctorRequested" });
|
|
8369
|
+
return;
|
|
8370
|
+
}
|
|
8371
|
+
this.db.update(schedules).set({
|
|
8372
|
+
lastRunAt: now,
|
|
8373
|
+
nextRunAt,
|
|
8374
|
+
updatedAt: now
|
|
8375
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8376
|
+
log13.info("doctor.triggered", { projectName: project.name });
|
|
8377
|
+
this.callbacks.onDoctorRequested(project.name);
|
|
8378
|
+
return;
|
|
8379
|
+
}
|
|
8357
8380
|
if (kind === SchedulableRunKinds["backlinks-sync"]) {
|
|
8358
8381
|
if (!this.callbacks.onBacklinksSyncRequested) {
|
|
8359
|
-
|
|
8382
|
+
log13.warn("backlinks-sync.no-callback", { scheduleId, projectId, msg: "host did not register onBacklinksSyncRequested" });
|
|
8360
8383
|
return;
|
|
8361
8384
|
}
|
|
8362
8385
|
this.db.update(schedules).set({
|
|
8363
8386
|
lastRunAt: now,
|
|
8364
8387
|
nextRunAt,
|
|
8365
8388
|
updatedAt: now
|
|
8366
|
-
}).where(
|
|
8367
|
-
|
|
8389
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8390
|
+
log13.info("backlinks-sync.triggered", { projectName: project.name });
|
|
8368
8391
|
this.callbacks.onBacklinksSyncRequested(project.name);
|
|
8369
8392
|
return;
|
|
8370
8393
|
}
|
|
8371
8394
|
if (kind === SchedulableRunKinds["site-audit"]) {
|
|
8372
8395
|
if (!this.callbacks.onSiteAuditRequested) {
|
|
8373
|
-
|
|
8396
|
+
log13.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
|
|
8374
8397
|
return;
|
|
8375
8398
|
}
|
|
8376
|
-
const active = this.db.select({ id: runs.id }).from(runs).where(
|
|
8377
|
-
|
|
8378
|
-
|
|
8399
|
+
const active = this.db.select({ id: runs.id }).from(runs).where(and11(
|
|
8400
|
+
eq14(runs.projectId, projectId),
|
|
8401
|
+
eq14(runs.kind, RunKinds["site-audit"]),
|
|
8379
8402
|
inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
8380
8403
|
)).get();
|
|
8381
8404
|
if (active) {
|
|
8382
|
-
|
|
8383
|
-
this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(
|
|
8405
|
+
log13.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
|
|
8406
|
+
this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8384
8407
|
return;
|
|
8385
8408
|
}
|
|
8386
|
-
const runId2 =
|
|
8409
|
+
const runId2 = crypto16.randomUUID();
|
|
8387
8410
|
this.db.insert(runs).values({
|
|
8388
8411
|
id: runId2,
|
|
8389
8412
|
projectId,
|
|
@@ -8396,8 +8419,8 @@ var Scheduler = class {
|
|
|
8396
8419
|
lastRunAt: now,
|
|
8397
8420
|
nextRunAt,
|
|
8398
8421
|
updatedAt: now
|
|
8399
|
-
}).where(
|
|
8400
|
-
|
|
8422
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8423
|
+
log13.info("site-audit.triggered", { runId: runId2, projectName: project.name });
|
|
8401
8424
|
this.callbacks.onSiteAuditRequested(runId2, projectId);
|
|
8402
8425
|
return;
|
|
8403
8426
|
}
|
|
@@ -8406,7 +8429,7 @@ var Scheduler = class {
|
|
|
8406
8429
|
if (project.defaultLocation) {
|
|
8407
8430
|
const loc = projectLocations.find((l) => l.label === project.defaultLocation);
|
|
8408
8431
|
if (!loc) {
|
|
8409
|
-
|
|
8432
|
+
log13.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
|
|
8410
8433
|
return;
|
|
8411
8434
|
}
|
|
8412
8435
|
resolvedLocation = loc;
|
|
@@ -8420,11 +8443,11 @@ var Scheduler = class {
|
|
|
8420
8443
|
location: locationLabel
|
|
8421
8444
|
});
|
|
8422
8445
|
if (queueResult.conflict) {
|
|
8423
|
-
|
|
8446
|
+
log13.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
|
|
8424
8447
|
this.db.update(schedules).set({
|
|
8425
8448
|
nextRunAt,
|
|
8426
8449
|
updatedAt: now
|
|
8427
|
-
}).where(
|
|
8450
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8428
8451
|
return;
|
|
8429
8452
|
}
|
|
8430
8453
|
const runId = queueResult.runId;
|
|
@@ -8432,19 +8455,19 @@ var Scheduler = class {
|
|
|
8432
8455
|
lastRunAt: now,
|
|
8433
8456
|
nextRunAt,
|
|
8434
8457
|
updatedAt: now
|
|
8435
|
-
}).where(
|
|
8458
|
+
}).where(eq14(schedules.id, currentSchedule.id)).run();
|
|
8436
8459
|
const scheduleProviders = currentSchedule.providers;
|
|
8437
8460
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
8438
|
-
|
|
8461
|
+
log13.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
8439
8462
|
this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
|
|
8440
8463
|
} catch (err) {
|
|
8441
|
-
|
|
8464
|
+
log13.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
|
|
8442
8465
|
}
|
|
8443
8466
|
}
|
|
8444
8467
|
};
|
|
8445
8468
|
|
|
8446
8469
|
// src/data-refresh.ts
|
|
8447
|
-
var
|
|
8470
|
+
var log14 = createLogger("DataRefresh");
|
|
8448
8471
|
async function refreshAllIntegrations(client, projectName) {
|
|
8449
8472
|
const integrations = [
|
|
8450
8473
|
{ name: "gsc", run: () => client.gscSync(projectName, {}) },
|
|
@@ -8457,19 +8480,19 @@ async function refreshAllIntegrations(client, projectName) {
|
|
|
8457
8480
|
results.forEach((result, idx) => {
|
|
8458
8481
|
const integration = integrations[idx].name;
|
|
8459
8482
|
if (result.status === "fulfilled") {
|
|
8460
|
-
|
|
8483
|
+
log14.info("integration.refreshed", { projectName, integration });
|
|
8461
8484
|
} else {
|
|
8462
8485
|
const reason = result.reason;
|
|
8463
8486
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
8464
|
-
|
|
8487
|
+
log14.warn("integration.refresh-failed", { projectName, integration, error: message });
|
|
8465
8488
|
}
|
|
8466
8489
|
});
|
|
8467
8490
|
}
|
|
8468
8491
|
|
|
8469
8492
|
// src/notifier.ts
|
|
8470
|
-
import { eq as
|
|
8471
|
-
import
|
|
8472
|
-
var
|
|
8493
|
+
import { eq as eq15, desc as desc5, and as and12, inArray as inArray6, or } from "drizzle-orm";
|
|
8494
|
+
import crypto17 from "crypto";
|
|
8495
|
+
var log15 = createLogger("Notifier");
|
|
8473
8496
|
var Notifier = class {
|
|
8474
8497
|
db;
|
|
8475
8498
|
serverUrl;
|
|
@@ -8479,26 +8502,26 @@ var Notifier = class {
|
|
|
8479
8502
|
}
|
|
8480
8503
|
/** Called after a run completes (success, partial, or failed). */
|
|
8481
8504
|
async onRunCompleted(runId, projectId) {
|
|
8482
|
-
|
|
8483
|
-
const notifs = this.db.select().from(notifications).where(
|
|
8505
|
+
log15.info("run.completed", { runId, projectId });
|
|
8506
|
+
const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
8484
8507
|
if (notifs.length === 0) {
|
|
8485
|
-
|
|
8508
|
+
log15.info("notifications.none-enabled", { projectId });
|
|
8486
8509
|
return;
|
|
8487
8510
|
}
|
|
8488
|
-
|
|
8489
|
-
const run = this.db.select().from(runs).where(
|
|
8511
|
+
log15.info("notifications.found", { projectId, count: notifs.length });
|
|
8512
|
+
const run = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
8490
8513
|
if (!run) {
|
|
8491
|
-
|
|
8514
|
+
log15.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
8492
8515
|
return;
|
|
8493
8516
|
}
|
|
8494
|
-
const project = this.db.select().from(projects).where(
|
|
8517
|
+
const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
|
|
8495
8518
|
if (!project) {
|
|
8496
|
-
|
|
8519
|
+
log15.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
8497
8520
|
return;
|
|
8498
8521
|
}
|
|
8499
8522
|
const transitions = this.computeTransitions(runId, projectId);
|
|
8500
8523
|
const events = [];
|
|
8501
|
-
|
|
8524
|
+
log15.info("run.status", { runId: run.id, status: run.status, projectId });
|
|
8502
8525
|
if (run.status === "completed" || run.status === "partial") {
|
|
8503
8526
|
events.push("run.completed");
|
|
8504
8527
|
}
|
|
@@ -8514,7 +8537,7 @@ var Notifier = class {
|
|
|
8514
8537
|
if (!config.url) continue;
|
|
8515
8538
|
const subscribedEvents = config.events;
|
|
8516
8539
|
const matchingEvents = events.filter((e) => subscribedEvents.includes(e));
|
|
8517
|
-
|
|
8540
|
+
log15.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
|
|
8518
8541
|
if (matchingEvents.length === 0) continue;
|
|
8519
8542
|
for (const event of matchingEvents) {
|
|
8520
8543
|
const relevantTransitions = event === "citation.lost" ? lostTransitions : event === "citation.gained" ? gainedTransitions : transitions;
|
|
@@ -8530,6 +8553,101 @@ var Notifier = class {
|
|
|
8530
8553
|
}
|
|
8531
8554
|
}
|
|
8532
8555
|
}
|
|
8556
|
+
/**
|
|
8557
|
+
* Record the outcome of a scheduled health pass and notify on transitions.
|
|
8558
|
+
*
|
|
8559
|
+
* Edge-triggered on `(status, code)`. A pass that finds the same problem as
|
|
8560
|
+
* last time is stored but not re-sent: an operator who receives the same
|
|
8561
|
+
* warning every day stops reading the channel, which is how a real
|
|
8562
|
+
* degradation goes unnoticed. A pass whose worst code CHANGES does notify,
|
|
8563
|
+
* even at the same severity, because a different cause is different news.
|
|
8564
|
+
*
|
|
8565
|
+
* Returns the event emitted, or null when nothing changed — the return value
|
|
8566
|
+
* is what tests assert against, so "stayed quiet" is observable rather than
|
|
8567
|
+
* inferred from the absence of a webhook.
|
|
8568
|
+
*/
|
|
8569
|
+
async onHealthChecked(projectId, report) {
|
|
8570
|
+
const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
|
|
8571
|
+
if (!project) {
|
|
8572
|
+
log15.error("project.not-found", { projectId, msg: "skipping health notification" });
|
|
8573
|
+
return null;
|
|
8574
|
+
}
|
|
8575
|
+
const rank = (status2) => status2 === "fail" ? 2 : status2 === "warn" ? 1 : 0;
|
|
8576
|
+
const categoryRank = (category) => {
|
|
8577
|
+
switch (category) {
|
|
8578
|
+
case "database":
|
|
8579
|
+
return 6;
|
|
8580
|
+
case "integrations":
|
|
8581
|
+
return 5;
|
|
8582
|
+
case "providers":
|
|
8583
|
+
return 4;
|
|
8584
|
+
case "auth":
|
|
8585
|
+
return 3;
|
|
8586
|
+
case "schedules":
|
|
8587
|
+
return 2;
|
|
8588
|
+
case "config":
|
|
8589
|
+
return 1;
|
|
8590
|
+
default:
|
|
8591
|
+
return 0;
|
|
8592
|
+
}
|
|
8593
|
+
};
|
|
8594
|
+
const graded = report.checks.filter((c) => c.status === "fail" || c.status === "warn" || c.status === "ok");
|
|
8595
|
+
const failing = graded.filter((c) => c.status !== "ok").sort((a, b) => rank(b.status) - rank(a.status) || categoryRank(b.category) - categoryRank(a.category) || a.id.localeCompare(b.id));
|
|
8596
|
+
const worst = failing.at(0) ?? null;
|
|
8597
|
+
const noSignal = graded.length === 0;
|
|
8598
|
+
const status = noSignal ? "warn" : worst ? worst.status : "ok";
|
|
8599
|
+
const code = noSignal ? "health.no-signal" : worst?.code ?? "health.ok";
|
|
8600
|
+
const summary = noSignal ? `No health check produced a result (${report.checks.length} skipped) \u2014 health is unknown, not confirmed.` : worst?.summary ?? `All ${graded.length} health check(s) passing.`;
|
|
8601
|
+
const previous = this.db.select().from(doctorHealthState).where(eq15(doctorHealthState.projectId, projectId)).get();
|
|
8602
|
+
const previousStatus = previous?.status ?? null;
|
|
8603
|
+
let event = null;
|
|
8604
|
+
if (previous === void 0) {
|
|
8605
|
+
if (status !== "ok") event = "health.degraded";
|
|
8606
|
+
} else if (status === "ok" && previousStatus !== "ok") {
|
|
8607
|
+
event = "health.recovered";
|
|
8608
|
+
} else if (status !== "ok" && (previousStatus === "ok" || previous.code !== code)) {
|
|
8609
|
+
event = "health.degraded";
|
|
8610
|
+
}
|
|
8611
|
+
const now = report.checkedAt;
|
|
8612
|
+
const observation = { projectId, status, code, summary, checkedAt: now };
|
|
8613
|
+
if (previous === void 0) {
|
|
8614
|
+
this.db.insert(doctorHealthState).values({ ...observation, notifiedAt: null }).run();
|
|
8615
|
+
} else {
|
|
8616
|
+
this.db.update(doctorHealthState).set(observation).where(eq15(doctorHealthState.projectId, projectId)).run();
|
|
8617
|
+
}
|
|
8618
|
+
if (!event) {
|
|
8619
|
+
log15.info("health.unchanged", { projectId, status, code });
|
|
8620
|
+
return null;
|
|
8621
|
+
}
|
|
8622
|
+
const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
8623
|
+
const payload = {
|
|
8624
|
+
source: "canonry",
|
|
8625
|
+
event,
|
|
8626
|
+
project: { name: project.name, canonicalDomain: project.canonicalDomain },
|
|
8627
|
+
health: {
|
|
8628
|
+
status,
|
|
8629
|
+
code,
|
|
8630
|
+
summary,
|
|
8631
|
+
remediation: worst?.remediation ?? null,
|
|
8632
|
+
checkedAt: now,
|
|
8633
|
+
previousStatus,
|
|
8634
|
+
failing: failing.map((c) => ({ id: c.id, status: c.status, code: c.code, summary: c.summary }))
|
|
8635
|
+
},
|
|
8636
|
+
dashboardUrl: `${this.serverUrl}/projects/${project.name}`
|
|
8637
|
+
};
|
|
8638
|
+
let delivered = 0;
|
|
8639
|
+
for (const notif of notifs) {
|
|
8640
|
+
const config = notif.config;
|
|
8641
|
+
if (!config.url) continue;
|
|
8642
|
+
await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null);
|
|
8643
|
+
delivered += 1;
|
|
8644
|
+
}
|
|
8645
|
+
if (delivered > 0) {
|
|
8646
|
+
this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq15(doctorHealthState.projectId, projectId)).run();
|
|
8647
|
+
}
|
|
8648
|
+
log15.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
|
|
8649
|
+
return event;
|
|
8650
|
+
}
|
|
8533
8651
|
/** Dispatch insight webhooks for critical/high severity insights after a run. */
|
|
8534
8652
|
async dispatchInsightWebhooks(runId, projectId, result) {
|
|
8535
8653
|
const insightEvents = [];
|
|
@@ -8538,11 +8656,11 @@ var Notifier = class {
|
|
|
8538
8656
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
8539
8657
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
8540
8658
|
if (insightEvents.length === 0) return;
|
|
8541
|
-
const notifs = this.db.select().from(notifications).where(
|
|
8659
|
+
const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
8542
8660
|
if (notifs.length === 0) return;
|
|
8543
|
-
const run = this.db.select().from(runs).where(
|
|
8661
|
+
const run = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
8544
8662
|
if (!run) return;
|
|
8545
|
-
const project = this.db.select().from(projects).where(
|
|
8663
|
+
const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
|
|
8546
8664
|
if (!project) return;
|
|
8547
8665
|
for (const notif of notifs) {
|
|
8548
8666
|
const config = notif.config;
|
|
@@ -8572,12 +8690,12 @@ var Notifier = class {
|
|
|
8572
8690
|
}
|
|
8573
8691
|
}
|
|
8574
8692
|
computeTransitions(runId, projectId) {
|
|
8575
|
-
const thisRun = this.db.select().from(runs).where(
|
|
8693
|
+
const thisRun = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
8576
8694
|
if (!thisRun) return [];
|
|
8577
|
-
const groupSiblings = this.db.select().from(runs).where(
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8695
|
+
const groupSiblings = this.db.select().from(runs).where(and12(
|
|
8696
|
+
eq15(runs.projectId, projectId),
|
|
8697
|
+
eq15(runs.kind, thisRun.kind),
|
|
8698
|
+
eq15(runs.createdAt, thisRun.createdAt)
|
|
8581
8699
|
)).all();
|
|
8582
8700
|
const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
|
|
8583
8701
|
if (stillPending) return [];
|
|
@@ -8593,17 +8711,17 @@ var Notifier = class {
|
|
|
8593
8711
|
return candidate.id > best.id ? candidate : best;
|
|
8594
8712
|
});
|
|
8595
8713
|
if (winner.id !== runId) return [];
|
|
8596
|
-
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
8714
|
+
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq15(projects.id, projectId)).get();
|
|
8597
8715
|
const locationCount = Math.max(
|
|
8598
8716
|
1,
|
|
8599
8717
|
(projectLocations?.locations ?? []).length
|
|
8600
8718
|
);
|
|
8601
8719
|
const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
|
|
8602
8720
|
const recentRuns = this.db.select().from(runs).where(
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
or(
|
|
8721
|
+
and12(
|
|
8722
|
+
eq15(runs.projectId, projectId),
|
|
8723
|
+
eq15(runs.kind, thisRun.kind),
|
|
8724
|
+
or(eq15(runs.status, "completed"), eq15(runs.status, "partial"))
|
|
8607
8725
|
)
|
|
8608
8726
|
).orderBy(desc5(runs.createdAt), desc5(runs.id)).limit(RECENT_FETCH_LIMIT).all();
|
|
8609
8727
|
const groups = groupRunsByCreatedAt(recentRuns);
|
|
@@ -8620,7 +8738,7 @@ var Notifier = class {
|
|
|
8620
8738
|
provider: querySnapshots.provider,
|
|
8621
8739
|
location: querySnapshots.location,
|
|
8622
8740
|
citationState: querySnapshots.citationState
|
|
8623
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8741
|
+
}).from(querySnapshots).leftJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(inArray6(querySnapshots.runId, currentRunIds)).all();
|
|
8624
8742
|
const previousSnapshots = this.db.select({
|
|
8625
8743
|
queryId: querySnapshots.queryId,
|
|
8626
8744
|
provider: querySnapshots.provider,
|
|
@@ -8650,26 +8768,29 @@ var Notifier = class {
|
|
|
8650
8768
|
return transitions;
|
|
8651
8769
|
}
|
|
8652
8770
|
async sendWebhook(url, payload, notificationId, projectId, webhookSecret) {
|
|
8771
|
+
const destination = resolveDestination(url);
|
|
8772
|
+
const body = destination.render ? destination.render(toAlertView(payload)) : payload;
|
|
8773
|
+
const signingSecret = destination.signed ? webhookSecret : null;
|
|
8653
8774
|
const targetLabel = redactNotificationUrl(url).urlDisplay;
|
|
8654
8775
|
const targetCheck = await resolveWebhookTarget(url);
|
|
8655
8776
|
if (!targetCheck.ok) {
|
|
8656
|
-
|
|
8777
|
+
log15.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
|
|
8657
8778
|
this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
|
|
8658
8779
|
return;
|
|
8659
8780
|
}
|
|
8660
|
-
|
|
8781
|
+
log15.info("webhook.send", { event: payload.event, url: targetLabel });
|
|
8661
8782
|
const maxRetries = 3;
|
|
8662
8783
|
const delays = [1e3, 4e3, 16e3];
|
|
8663
8784
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
8664
8785
|
try {
|
|
8665
|
-
const response = await deliverWebhook(targetCheck.target,
|
|
8786
|
+
const response = await deliverWebhook(targetCheck.target, body, signingSecret);
|
|
8666
8787
|
if (response.status >= 200 && response.status < 300) {
|
|
8667
|
-
|
|
8788
|
+
log15.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
|
|
8668
8789
|
this.logDelivery(projectId, notificationId, payload.event, "sent", null);
|
|
8669
8790
|
return;
|
|
8670
8791
|
}
|
|
8671
8792
|
const errorDetail = response.error ?? `HTTP ${response.status}`;
|
|
8672
|
-
|
|
8793
|
+
log15.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
|
|
8673
8794
|
if (attempt === maxRetries - 1) {
|
|
8674
8795
|
this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
|
|
8675
8796
|
}
|
|
@@ -8677,7 +8798,7 @@ var Notifier = class {
|
|
|
8677
8798
|
const errorDetail = err instanceof Error ? err.message : String(err);
|
|
8678
8799
|
if (attempt === maxRetries - 1) {
|
|
8679
8800
|
this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
|
|
8680
|
-
|
|
8801
|
+
log15.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
|
|
8681
8802
|
}
|
|
8682
8803
|
}
|
|
8683
8804
|
if (attempt < maxRetries - 1) {
|
|
@@ -8687,7 +8808,7 @@ var Notifier = class {
|
|
|
8687
8808
|
}
|
|
8688
8809
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
8689
8810
|
this.db.insert(auditLog).values({
|
|
8690
|
-
id:
|
|
8811
|
+
id: crypto17.randomUUID(),
|
|
8691
8812
|
projectId,
|
|
8692
8813
|
actor: "scheduler",
|
|
8693
8814
|
action: `notification.${status}`,
|
|
@@ -8700,8 +8821,8 @@ var Notifier = class {
|
|
|
8700
8821
|
};
|
|
8701
8822
|
|
|
8702
8823
|
// src/run-coordinator.ts
|
|
8703
|
-
import { eq as
|
|
8704
|
-
var
|
|
8824
|
+
import { eq as eq16 } from "drizzle-orm";
|
|
8825
|
+
var log16 = createLogger("RunCoordinator");
|
|
8705
8826
|
var RunCoordinator = class {
|
|
8706
8827
|
constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
|
|
8707
8828
|
this.db = db;
|
|
@@ -8716,10 +8837,10 @@ var RunCoordinator = class {
|
|
|
8716
8837
|
onInsightsGenerated;
|
|
8717
8838
|
onAeroEvent;
|
|
8718
8839
|
async onRunCompleted(runId, projectId) {
|
|
8719
|
-
const runRow = this.db.select().from(runs).where(
|
|
8840
|
+
const runRow = this.db.select().from(runs).where(eq16(runs.id, runId)).get();
|
|
8720
8841
|
const kind = runRow?.kind ?? RunKinds["answer-visibility"];
|
|
8721
8842
|
if (runRow?.trigger === RunTriggers.probe) {
|
|
8722
|
-
|
|
8843
|
+
log16.info("probe.skip-side-effects", { runId, projectId, kind });
|
|
8723
8844
|
return;
|
|
8724
8845
|
}
|
|
8725
8846
|
let insightCount = 0;
|
|
@@ -8736,12 +8857,12 @@ var RunCoordinator = class {
|
|
|
8736
8857
|
try {
|
|
8737
8858
|
await this.onInsightsGenerated(runId, projectId, result);
|
|
8738
8859
|
} catch (err) {
|
|
8739
|
-
|
|
8860
|
+
log16.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8740
8861
|
}
|
|
8741
8862
|
}
|
|
8742
8863
|
}
|
|
8743
8864
|
} catch (err) {
|
|
8744
|
-
|
|
8865
|
+
log16.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8745
8866
|
}
|
|
8746
8867
|
} else if (kind === RunKinds["gbp-sync"]) {
|
|
8747
8868
|
try {
|
|
@@ -8754,17 +8875,17 @@ var RunCoordinator = class {
|
|
|
8754
8875
|
try {
|
|
8755
8876
|
await this.onInsightsGenerated(runId, projectId, analysisResultFromInsights(gbpInsights));
|
|
8756
8877
|
} catch (err) {
|
|
8757
|
-
|
|
8878
|
+
log16.error("gbp-insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8758
8879
|
}
|
|
8759
8880
|
}
|
|
8760
8881
|
} catch (err) {
|
|
8761
|
-
|
|
8882
|
+
log16.error("gbp-intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8762
8883
|
}
|
|
8763
8884
|
}
|
|
8764
8885
|
try {
|
|
8765
8886
|
await this.notifier.onRunCompleted(runId, projectId);
|
|
8766
8887
|
} catch (err) {
|
|
8767
|
-
|
|
8888
|
+
log16.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8768
8889
|
}
|
|
8769
8890
|
if (this.onAeroEvent) {
|
|
8770
8891
|
try {
|
|
@@ -8777,7 +8898,7 @@ var RunCoordinator = class {
|
|
|
8777
8898
|
};
|
|
8778
8899
|
await this.onAeroEvent(ctx);
|
|
8779
8900
|
} catch (err) {
|
|
8780
|
-
|
|
8901
|
+
log16.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
8781
8902
|
}
|
|
8782
8903
|
}
|
|
8783
8904
|
}
|
|
@@ -8792,7 +8913,7 @@ var RunCoordinator = class {
|
|
|
8792
8913
|
* so the Aero queue is never starved of a follow-up.
|
|
8793
8914
|
*/
|
|
8794
8915
|
buildDiscoveryAeroContext(runId, projectId, status, error) {
|
|
8795
|
-
const session = this.db.select().from(discoverySessions).where(
|
|
8916
|
+
const session = this.db.select().from(discoverySessions).where(eq16(discoverySessions.runId, runId)).get();
|
|
8796
8917
|
const competitorMap = session ? session.competitorMap : [];
|
|
8797
8918
|
return {
|
|
8798
8919
|
kind: RunKinds["aeo-discover-probe"],
|
|
@@ -8834,8 +8955,8 @@ function analysisResultFromInsights(insights2) {
|
|
|
8834
8955
|
}
|
|
8835
8956
|
|
|
8836
8957
|
// src/agent/session-registry.ts
|
|
8837
|
-
import
|
|
8838
|
-
import { eq as
|
|
8958
|
+
import crypto21 from "crypto";
|
|
8959
|
+
import { eq as eq18 } from "drizzle-orm";
|
|
8839
8960
|
|
|
8840
8961
|
// src/agent/session.ts
|
|
8841
8962
|
import fs7 from "fs";
|
|
@@ -9578,7 +9699,7 @@ function buildAeroStateTools(ctx, opts = {}) {
|
|
|
9578
9699
|
}
|
|
9579
9700
|
|
|
9580
9701
|
// src/agent/llm-usage.ts
|
|
9581
|
-
import
|
|
9702
|
+
import crypto18 from "crypto";
|
|
9582
9703
|
var AeroLlmUsageFeatures = {
|
|
9583
9704
|
turn: "aero.turn"
|
|
9584
9705
|
};
|
|
@@ -9597,7 +9718,7 @@ function recordLlmUsageEvent(args) {
|
|
|
9597
9718
|
const usage = args.message.usage;
|
|
9598
9719
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9599
9720
|
args.db.insert(llmUsageEvents).values({
|
|
9600
|
-
id:
|
|
9721
|
+
id: crypto18.randomUUID(),
|
|
9601
9722
|
projectId: args.projectId,
|
|
9602
9723
|
runId: args.runId,
|
|
9603
9724
|
agentSessionId: args.agentSessionId,
|
|
@@ -9673,7 +9794,7 @@ function splitAeroAnthropicSystemCachePayload(payload, model) {
|
|
|
9673
9794
|
}
|
|
9674
9795
|
|
|
9675
9796
|
// src/agent/tool-usage.ts
|
|
9676
|
-
import
|
|
9797
|
+
import crypto19 from "crypto";
|
|
9677
9798
|
var AeroToolEventStatuses = {
|
|
9678
9799
|
success: "success",
|
|
9679
9800
|
error: "error"
|
|
@@ -9736,7 +9857,7 @@ function createAeroToolUsageHooks(args) {
|
|
|
9736
9857
|
function recordAgentToolEvent(args) {
|
|
9737
9858
|
try {
|
|
9738
9859
|
args.db.insert(agentToolEvents).values({
|
|
9739
|
-
id:
|
|
9860
|
+
id: crypto19.randomUUID(),
|
|
9740
9861
|
projectId: args.projectId,
|
|
9741
9862
|
agentSessionId: args.agentSessionId,
|
|
9742
9863
|
toolCallId: args.toolCallId,
|
|
@@ -9879,7 +10000,7 @@ function resolveSessionProviderAndModel(config, opts) {
|
|
|
9879
10000
|
import { Type as Type4 } from "@sinclair/typebox";
|
|
9880
10001
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
9881
10002
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
9882
|
-
var
|
|
10003
|
+
var log17 = createLogger("RemoteMcp");
|
|
9883
10004
|
var AERO_EXCLUDED_MCP_TOOLS2 = /* @__PURE__ */ new Set([]);
|
|
9884
10005
|
var MAX_TOOL_RESULT_CHARS2 = 2e4;
|
|
9885
10006
|
function truncate(text) {
|
|
@@ -9932,7 +10053,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
|
|
|
9932
10053
|
try {
|
|
9933
10054
|
client = await connect(server);
|
|
9934
10055
|
} catch (err) {
|
|
9935
|
-
|
|
10056
|
+
log17.error("external-mcp.connect-failed", {
|
|
9936
10057
|
label,
|
|
9937
10058
|
error: err instanceof Error ? err.message : String(err)
|
|
9938
10059
|
});
|
|
@@ -9942,7 +10063,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
|
|
|
9942
10063
|
try {
|
|
9943
10064
|
listed = await client.listTools();
|
|
9944
10065
|
} catch (err) {
|
|
9945
|
-
|
|
10066
|
+
log17.error("external-mcp.list-failed", {
|
|
9946
10067
|
label,
|
|
9947
10068
|
error: err instanceof Error ? err.message : String(err)
|
|
9948
10069
|
});
|
|
@@ -9953,7 +10074,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
|
|
|
9953
10074
|
if (!isReadOnly(tool)) continue;
|
|
9954
10075
|
tools.push(adaptRemoteTool(client, tool));
|
|
9955
10076
|
}
|
|
9956
|
-
|
|
10077
|
+
log17.info("external-mcp.loaded", {
|
|
9957
10078
|
label,
|
|
9958
10079
|
discovered: listed.tools.length,
|
|
9959
10080
|
adopted: tools.length
|
|
@@ -9963,8 +10084,8 @@ async function loadExternalMcpTools(servers, opts = {}) {
|
|
|
9963
10084
|
}
|
|
9964
10085
|
|
|
9965
10086
|
// src/agent/memory-store.ts
|
|
9966
|
-
import
|
|
9967
|
-
import { and as
|
|
10087
|
+
import crypto20 from "crypto";
|
|
10088
|
+
import { and as and13, desc as desc6, eq as eq17, like, sql as sql7 } from "drizzle-orm";
|
|
9968
10089
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
9969
10090
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
9970
10091
|
function rowToDto(row) {
|
|
@@ -9978,7 +10099,7 @@ function rowToDto(row) {
|
|
|
9978
10099
|
};
|
|
9979
10100
|
}
|
|
9980
10101
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
9981
|
-
const query = db.select().from(agentMemory).where(
|
|
10102
|
+
const query = db.select().from(agentMemory).where(eq17(agentMemory.projectId, projectId)).orderBy(desc6(agentMemory.updatedAt));
|
|
9982
10103
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
9983
10104
|
return rows.map(rowToDto);
|
|
9984
10105
|
}
|
|
@@ -9992,7 +10113,7 @@ function upsertMemoryEntry(db, args) {
|
|
|
9992
10113
|
throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
|
|
9993
10114
|
}
|
|
9994
10115
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9995
|
-
const id =
|
|
10116
|
+
const id = crypto20.randomUUID();
|
|
9996
10117
|
db.insert(agentMemory).values({
|
|
9997
10118
|
id,
|
|
9998
10119
|
projectId: args.projectId,
|
|
@@ -10009,12 +10130,12 @@ function upsertMemoryEntry(db, args) {
|
|
|
10009
10130
|
updatedAt: now
|
|
10010
10131
|
}
|
|
10011
10132
|
}).run();
|
|
10012
|
-
const row = db.select().from(agentMemory).where(
|
|
10133
|
+
const row = db.select().from(agentMemory).where(and13(eq17(agentMemory.projectId, args.projectId), eq17(agentMemory.key, args.key))).get();
|
|
10013
10134
|
if (!row) throw new Error("memory upsert produced no row");
|
|
10014
10135
|
return rowToDto(row);
|
|
10015
10136
|
}
|
|
10016
10137
|
function deleteMemoryEntry(db, projectId, key) {
|
|
10017
|
-
const result = db.delete(agentMemory).where(
|
|
10138
|
+
const result = db.delete(agentMemory).where(and13(eq17(agentMemory.projectId, projectId), eq17(agentMemory.key, key))).run();
|
|
10018
10139
|
const changes = result.changes ?? 0;
|
|
10019
10140
|
return changes > 0;
|
|
10020
10141
|
}
|
|
@@ -10029,7 +10150,7 @@ function writeCompactionNote(db, args) {
|
|
|
10029
10150
|
}
|
|
10030
10151
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10031
10152
|
const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
|
|
10032
|
-
const id =
|
|
10153
|
+
const id = crypto20.randomUUID();
|
|
10033
10154
|
let inserted;
|
|
10034
10155
|
db.transaction((tx) => {
|
|
10035
10156
|
tx.insert(agentMemory).values({
|
|
@@ -10043,16 +10164,16 @@ function writeCompactionNote(db, args) {
|
|
|
10043
10164
|
}).run();
|
|
10044
10165
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
10045
10166
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
10046
|
-
|
|
10047
|
-
|
|
10167
|
+
and13(
|
|
10168
|
+
eq17(agentMemory.projectId, args.projectId),
|
|
10048
10169
|
like(agentMemory.key, `${sessionPrefix}%`)
|
|
10049
10170
|
)
|
|
10050
10171
|
).orderBy(desc6(agentMemory.updatedAt)).all();
|
|
10051
10172
|
const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
|
|
10052
10173
|
if (stale.length > 0) {
|
|
10053
|
-
tx.delete(agentMemory).where(
|
|
10174
|
+
tx.delete(agentMemory).where(sql7`${agentMemory.id} IN (${sql7.join(stale.map((s) => sql7`${s}`), sql7`, `)})`).run();
|
|
10054
10175
|
}
|
|
10055
|
-
const row = tx.select().from(agentMemory).where(
|
|
10176
|
+
const row = tx.select().from(agentMemory).where(and13(eq17(agentMemory.projectId, args.projectId), eq17(agentMemory.key, key))).get();
|
|
10056
10177
|
if (row) inserted = rowToDto(row);
|
|
10057
10178
|
});
|
|
10058
10179
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -10185,7 +10306,7 @@ async function compactMessages(args) {
|
|
|
10185
10306
|
}
|
|
10186
10307
|
|
|
10187
10308
|
// src/agent/session-registry.ts
|
|
10188
|
-
var
|
|
10309
|
+
var log18 = createLogger("SessionRegistry");
|
|
10189
10310
|
var MAX_HYDRATE_NOTES = 20;
|
|
10190
10311
|
var MAX_HYDRATE_BYTES = 32 * 1024;
|
|
10191
10312
|
function escapeMemoryFragment(value) {
|
|
@@ -10229,7 +10350,7 @@ var SessionRegistry = class {
|
|
|
10229
10350
|
if (!this.externalToolsPromise) {
|
|
10230
10351
|
this.externalToolsPromise = loadExternalMcpTools(this.opts.config.externalMcpServers).catch(
|
|
10231
10352
|
(err) => {
|
|
10232
|
-
|
|
10353
|
+
log18.error("external-mcp.load-failed", {
|
|
10233
10354
|
error: err instanceof Error ? err.message : String(err)
|
|
10234
10355
|
});
|
|
10235
10356
|
return [];
|
|
@@ -10279,7 +10400,7 @@ var SessionRegistry = class {
|
|
|
10279
10400
|
modelProvider: effectiveProvider,
|
|
10280
10401
|
modelId: effectiveModelId,
|
|
10281
10402
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10282
|
-
}).where(
|
|
10403
|
+
}).where(eq18(agentSessions.projectId, projectId)).run();
|
|
10283
10404
|
}
|
|
10284
10405
|
const agent2 = createAeroSession({
|
|
10285
10406
|
projectName,
|
|
@@ -10308,7 +10429,7 @@ var SessionRegistry = class {
|
|
|
10308
10429
|
}
|
|
10309
10430
|
const { provider, modelId } = resolveSessionProviderAndModel(this.opts.config, preferences);
|
|
10310
10431
|
const systemPrompt = loadAeroSystemPrompt();
|
|
10311
|
-
const sessionId =
|
|
10432
|
+
const sessionId = crypto21.randomUUID();
|
|
10312
10433
|
const agent = createAeroSession({
|
|
10313
10434
|
projectName,
|
|
10314
10435
|
client: this.opts.client,
|
|
@@ -10474,13 +10595,13 @@ ${lines.join("\n")}
|
|
|
10474
10595
|
agent.state.messages = result.messages;
|
|
10475
10596
|
agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
|
|
10476
10597
|
this.save(projectName);
|
|
10477
|
-
|
|
10598
|
+
log18.info("compaction.completed", {
|
|
10478
10599
|
projectName,
|
|
10479
10600
|
removedCount: result.removedCount,
|
|
10480
10601
|
summaryBytes: Buffer.byteLength(result.summary, "utf8")
|
|
10481
10602
|
});
|
|
10482
10603
|
} catch (err) {
|
|
10483
|
-
|
|
10604
|
+
log18.error("compaction.failed", {
|
|
10484
10605
|
projectName,
|
|
10485
10606
|
error: err instanceof Error ? err.message : String(err)
|
|
10486
10607
|
});
|
|
@@ -10513,7 +10634,7 @@ ${lines.join("\n")}
|
|
|
10513
10634
|
modelProvider: nextProvider,
|
|
10514
10635
|
modelId: nextModelId,
|
|
10515
10636
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10516
|
-
}).where(
|
|
10637
|
+
}).where(eq18(agentSessions.projectId, projectId)).run();
|
|
10517
10638
|
}
|
|
10518
10639
|
/** Persist a session's transcript back to the DB. Call after any run settles. */
|
|
10519
10640
|
save(projectName) {
|
|
@@ -10581,7 +10702,7 @@ ${lines.join("\n")}
|
|
|
10581
10702
|
await agent.prompt(msgs);
|
|
10582
10703
|
this.save(projectName);
|
|
10583
10704
|
} catch (err) {
|
|
10584
|
-
|
|
10705
|
+
log18.error("drain.failed", {
|
|
10585
10706
|
projectName,
|
|
10586
10707
|
error: err instanceof Error ? err.message : String(err)
|
|
10587
10708
|
});
|
|
@@ -10677,17 +10798,17 @@ ${lines.join("\n")}
|
|
|
10677
10798
|
return id;
|
|
10678
10799
|
}
|
|
10679
10800
|
tryResolveProjectId(projectName) {
|
|
10680
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
10801
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq18(projects.name, projectName)).get();
|
|
10681
10802
|
return row?.id;
|
|
10682
10803
|
}
|
|
10683
10804
|
loadRow(projectId) {
|
|
10684
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
10805
|
+
const row = this.opts.db.select().from(agentSessions).where(eq18(agentSessions.projectId, projectId)).get();
|
|
10685
10806
|
return row ?? null;
|
|
10686
10807
|
}
|
|
10687
10808
|
insertRow(params) {
|
|
10688
10809
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10689
10810
|
this.opts.db.insert(agentSessions).values({
|
|
10690
|
-
id: params.id ??
|
|
10811
|
+
id: params.id ?? crypto21.randomUUID(),
|
|
10691
10812
|
projectId: params.projectId,
|
|
10692
10813
|
systemPrompt: params.systemPrompt,
|
|
10693
10814
|
modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
|
|
@@ -10700,14 +10821,14 @@ ${lines.join("\n")}
|
|
|
10700
10821
|
}
|
|
10701
10822
|
updateRow(projectId, patch) {
|
|
10702
10823
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10703
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
10824
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq18(agentSessions.projectId, projectId)).run();
|
|
10704
10825
|
}
|
|
10705
10826
|
};
|
|
10706
10827
|
|
|
10707
10828
|
// src/agent/agent-routes.ts
|
|
10708
|
-
import { eq as
|
|
10829
|
+
import { eq as eq19 } from "drizzle-orm";
|
|
10709
10830
|
function resolveProject(db, name) {
|
|
10710
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
10831
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq19(projects.name, name)).get();
|
|
10711
10832
|
if (!row) throw notFound("project", name);
|
|
10712
10833
|
return row;
|
|
10713
10834
|
}
|
|
@@ -10716,7 +10837,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
10716
10837
|
"/projects/:name/agent/transcript",
|
|
10717
10838
|
async (request) => {
|
|
10718
10839
|
const project = resolveProject(opts.db, request.params.name);
|
|
10719
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
10840
|
+
const row = opts.db.select().from(agentSessions).where(eq19(agentSessions.projectId, project.id)).get();
|
|
10720
10841
|
if (!row) {
|
|
10721
10842
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
10722
10843
|
}
|
|
@@ -10740,7 +10861,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
10740
10861
|
async (request) => {
|
|
10741
10862
|
const project = resolveProject(opts.db, request.params.name);
|
|
10742
10863
|
opts.sessionRegistry.reset(project.name);
|
|
10743
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
10864
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(agentSessions.projectId, project.id)).run();
|
|
10744
10865
|
return { status: "reset" };
|
|
10745
10866
|
}
|
|
10746
10867
|
);
|
|
@@ -11174,7 +11295,7 @@ function formatAuditFactorScore(factor) {
|
|
|
11174
11295
|
}
|
|
11175
11296
|
|
|
11176
11297
|
// src/snapshot-service.ts
|
|
11177
|
-
var
|
|
11298
|
+
var log19 = createLogger("Snapshot");
|
|
11178
11299
|
var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
|
|
11179
11300
|
var SNAPSHOT_QUERY_COUNT = 6;
|
|
11180
11301
|
var ProviderExecutionGate2 = class {
|
|
@@ -11320,7 +11441,7 @@ var SnapshotService = class {
|
|
|
11320
11441
|
return mapAuditReport(report);
|
|
11321
11442
|
} catch (err) {
|
|
11322
11443
|
const message = err instanceof Error ? err.message : String(err);
|
|
11323
|
-
|
|
11444
|
+
log19.warn("audit.failed", { homepageUrl, error: message });
|
|
11324
11445
|
return {
|
|
11325
11446
|
url: homepageUrl,
|
|
11326
11447
|
finalUrl: homepageUrl,
|
|
@@ -11349,7 +11470,7 @@ var SnapshotService = class {
|
|
|
11349
11470
|
queries: parsedQueries
|
|
11350
11471
|
};
|
|
11351
11472
|
} catch (err) {
|
|
11352
|
-
|
|
11473
|
+
log19.warn("profile.generation-failed", {
|
|
11353
11474
|
domain: ctx.domain,
|
|
11354
11475
|
provider: ctx.analysisProvider.adapter.name,
|
|
11355
11476
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -11491,7 +11612,7 @@ var SnapshotService = class {
|
|
|
11491
11612
|
recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
|
|
11492
11613
|
};
|
|
11493
11614
|
} catch (err) {
|
|
11494
|
-
|
|
11615
|
+
log19.warn("response.analysis-failed", {
|
|
11495
11616
|
provider: ctx.analysisProvider.adapter.name,
|
|
11496
11617
|
error: err instanceof Error ? err.message : String(err)
|
|
11497
11618
|
});
|
|
@@ -11746,20 +11867,20 @@ function clipText(value, length) {
|
|
|
11746
11867
|
}
|
|
11747
11868
|
|
|
11748
11869
|
// src/research-runner.ts
|
|
11749
|
-
import { and as
|
|
11870
|
+
import { and as and14, eq as eq20, inArray as inArray7, sql as sql8 } from "drizzle-orm";
|
|
11750
11871
|
var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
|
|
11751
11872
|
function finalResearchRunStatus(completed, failed) {
|
|
11752
11873
|
if (failed === 0) return ResearchRunStatuses.completed;
|
|
11753
11874
|
return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
|
|
11754
11875
|
}
|
|
11755
11876
|
async function executeResearchRun(db, registry, runId, projectId) {
|
|
11756
|
-
const run = db.select().from(researchRuns).where(
|
|
11877
|
+
const run = db.select().from(researchRuns).where(and14(eq20(researchRuns.id, runId), eq20(researchRuns.projectId, projectId))).get();
|
|
11757
11878
|
if (!run || run.status !== ResearchRunStatuses.queued) return;
|
|
11758
|
-
const project = db.select().from(projects).where(
|
|
11879
|
+
const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
|
|
11759
11880
|
if (!project) return;
|
|
11760
11881
|
const provider = registry.get(run.provider);
|
|
11761
11882
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11762
|
-
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(
|
|
11883
|
+
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and14(eq20(researchRuns.id, runId), eq20(researchRuns.status, ResearchRunStatuses.queued))).run();
|
|
11763
11884
|
if (claim.changes !== 1) return;
|
|
11764
11885
|
let reserved = 0;
|
|
11765
11886
|
let dispatched = 0;
|
|
@@ -11783,8 +11904,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
11783
11904
|
}
|
|
11784
11905
|
reserved = run.totalQueries;
|
|
11785
11906
|
reservation = { scope, period };
|
|
11786
|
-
const rows = db.select().from(researchRunQueries).where(
|
|
11787
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
11907
|
+
const rows = db.select().from(researchRunQueries).where(eq20(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
|
|
11908
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq20(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
11788
11909
|
const domains = effectiveDomains(project);
|
|
11789
11910
|
const brands = effectiveBrandNames(project);
|
|
11790
11911
|
const config = { ...provider.config, model: run.resolvedModel };
|
|
@@ -11792,7 +11913,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
11792
11913
|
await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
|
|
11793
11914
|
try {
|
|
11794
11915
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11795
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(
|
|
11916
|
+
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and14(eq20(researchRunQueries.id, row.id), eq20(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
|
|
11796
11917
|
const raw = await gate.run(async () => {
|
|
11797
11918
|
dispatched++;
|
|
11798
11919
|
return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
|
|
@@ -11819,7 +11940,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
11819
11940
|
citationState: determineCitationState(normalized, domains),
|
|
11820
11941
|
rawResponse: raw.rawResponse,
|
|
11821
11942
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11822
|
-
}).where(
|
|
11943
|
+
}).where(and14(eq20(researchRunQueries.id, row.id), eq20(researchRunQueries.status, ResearchQueryStatuses.running))).run();
|
|
11823
11944
|
if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
|
|
11824
11945
|
} catch (error) {
|
|
11825
11946
|
try {
|
|
@@ -11843,18 +11964,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
11843
11964
|
}
|
|
11844
11965
|
}
|
|
11845
11966
|
function incrementResearchProgress(db, runId, column) {
|
|
11846
|
-
db.update(researchRuns).set({ [column]:
|
|
11967
|
+
db.update(researchRuns).set({ [column]: sql8`${researchRuns[column]} + 1` }).where(eq20(researchRuns.id, runId)).run();
|
|
11847
11968
|
}
|
|
11848
11969
|
function markResearchQueryFailed(db, runId, queryId, error) {
|
|
11849
11970
|
const message = error instanceof Error ? error.message : String(error);
|
|
11850
|
-
const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
11971
|
+
const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and14(eq20(researchRunQueries.id, queryId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
|
|
11851
11972
|
if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
|
|
11852
11973
|
}
|
|
11853
11974
|
function markUnfinishedResearchQueriesFailed(db, runId, error) {
|
|
11854
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
11975
|
+
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and14(eq20(researchRunQueries.researchRunId, runId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
|
|
11855
11976
|
}
|
|
11856
11977
|
function finalizeResearchRun(db, runId, fatalError) {
|
|
11857
|
-
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(
|
|
11978
|
+
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq20(researchRunQueries.researchRunId, runId)).all();
|
|
11858
11979
|
const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
|
|
11859
11980
|
const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
|
|
11860
11981
|
db.update(researchRuns).set({
|
|
@@ -11863,13 +11984,13 @@ function finalizeResearchRun(db, runId, fatalError) {
|
|
|
11863
11984
|
failedQueries: failed,
|
|
11864
11985
|
error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
|
|
11865
11986
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11866
|
-
}).where(
|
|
11987
|
+
}).where(eq20(researchRuns.id, runId)).run();
|
|
11867
11988
|
}
|
|
11868
11989
|
|
|
11869
11990
|
// src/server.ts
|
|
11870
11991
|
var _require3 = createRequire3(import.meta.url);
|
|
11871
11992
|
var { version: PKG_VERSION2 } = _require3("../package.json");
|
|
11872
|
-
var
|
|
11993
|
+
var log20 = createLogger("Server");
|
|
11873
11994
|
var DEFAULT_QUOTA = {
|
|
11874
11995
|
maxConcurrency: 2,
|
|
11875
11996
|
maxRequestsPerMinute: 10,
|
|
@@ -11887,6 +12008,9 @@ function parseBooleanEnv(value) {
|
|
|
11887
12008
|
function resolveDashboardRequirePassword(env, config) {
|
|
11888
12009
|
return parseBooleanEnv(env.CANONRY_DASHBOARD_REQUIRE_PASSWORD) ?? config.dashboard?.requirePassword ?? true;
|
|
11889
12010
|
}
|
|
12011
|
+
function resolveDashboardShowResourceLinks(env, config) {
|
|
12012
|
+
return parseBooleanEnv(env.CANONRY_DASHBOARD_SHOW_RESOURCE_LINKS) ?? config.dashboard?.showResourceLinks ?? true;
|
|
12013
|
+
}
|
|
11890
12014
|
var API_ADAPTERS = [
|
|
11891
12015
|
geminiAdapter,
|
|
11892
12016
|
openaiAdapter,
|
|
@@ -11912,14 +12036,14 @@ function summarizeProviderConfig(config) {
|
|
|
11912
12036
|
};
|
|
11913
12037
|
}
|
|
11914
12038
|
function hashApiKey(key) {
|
|
11915
|
-
return
|
|
12039
|
+
return crypto22.createHash("sha256").update(key).digest("hex");
|
|
11916
12040
|
}
|
|
11917
12041
|
var DASHBOARD_SCRYPT_KEYLEN = 64;
|
|
11918
12042
|
var DASHBOARD_SCRYPT_COST = 1 << 15;
|
|
11919
12043
|
var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
|
|
11920
12044
|
function hashDashboardPassword(password) {
|
|
11921
|
-
const salt =
|
|
11922
|
-
const derived =
|
|
12045
|
+
const salt = crypto22.randomBytes(16);
|
|
12046
|
+
const derived = crypto22.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
|
|
11923
12047
|
N: DASHBOARD_SCRYPT_COST,
|
|
11924
12048
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
11925
12049
|
});
|
|
@@ -11940,14 +12064,14 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
11940
12064
|
} catch {
|
|
11941
12065
|
return { ok: false, needsRehash: false };
|
|
11942
12066
|
}
|
|
11943
|
-
const derived =
|
|
12067
|
+
const derived = crypto22.scryptSync(password, salt, expected.length, {
|
|
11944
12068
|
N: DASHBOARD_SCRYPT_COST,
|
|
11945
12069
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
11946
12070
|
});
|
|
11947
12071
|
if (derived.length !== expected.length)
|
|
11948
12072
|
return { ok: false, needsRehash: false };
|
|
11949
12073
|
return {
|
|
11950
|
-
ok:
|
|
12074
|
+
ok: crypto22.timingSafeEqual(derived, expected),
|
|
11951
12075
|
needsRehash: false
|
|
11952
12076
|
};
|
|
11953
12077
|
}
|
|
@@ -11956,7 +12080,7 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
11956
12080
|
const expected = Buffer.from(storedHash, "hex");
|
|
11957
12081
|
if (candidate.length !== expected.length)
|
|
11958
12082
|
return { ok: false, needsRehash: false };
|
|
11959
|
-
const ok =
|
|
12083
|
+
const ok = crypto22.timingSafeEqual(candidate, expected);
|
|
11960
12084
|
return { ok, needsRehash: ok };
|
|
11961
12085
|
}
|
|
11962
12086
|
return { ok: false, needsRehash: false };
|
|
@@ -12019,7 +12143,7 @@ function applyLegacyCredentials(rows, config) {
|
|
|
12019
12143
|
}
|
|
12020
12144
|
if (migratedGoogle > 0) {
|
|
12021
12145
|
saveConfigPatch({ google: config.google });
|
|
12022
|
-
|
|
12146
|
+
log20.info("credentials.migrated", { type: "google", count: migratedGoogle });
|
|
12023
12147
|
}
|
|
12024
12148
|
let migratedGa4 = 0;
|
|
12025
12149
|
for (const row of rows.ga4) {
|
|
@@ -12037,7 +12161,7 @@ function applyLegacyCredentials(rows, config) {
|
|
|
12037
12161
|
}
|
|
12038
12162
|
if (migratedGa4 > 0) {
|
|
12039
12163
|
saveConfigPatch({ ga4: config.ga4 });
|
|
12040
|
-
|
|
12164
|
+
log20.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
|
|
12041
12165
|
}
|
|
12042
12166
|
}
|
|
12043
12167
|
function isLoopbackBindHost(host) {
|
|
@@ -12094,11 +12218,11 @@ async function createServer(opts) {
|
|
|
12094
12218
|
applyLegacyCredentials(legacyRows, opts.config);
|
|
12095
12219
|
dropLegacyCredentialColumns(opts.db);
|
|
12096
12220
|
} catch (err) {
|
|
12097
|
-
|
|
12221
|
+
log20.warn("credentials.migration.failed", {
|
|
12098
12222
|
error: err instanceof Error ? err.message : String(err)
|
|
12099
12223
|
});
|
|
12100
12224
|
}
|
|
12101
|
-
|
|
12225
|
+
log20.info("providers.configured", {
|
|
12102
12226
|
providers: Object.keys(providers).filter((k) => {
|
|
12103
12227
|
const p = providers[k];
|
|
12104
12228
|
return p?.apiKey || p?.baseUrl || p?.vertexProject;
|
|
@@ -12162,7 +12286,7 @@ async function createServer(opts) {
|
|
|
12162
12286
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
12163
12287
|
async (ctx) => {
|
|
12164
12288
|
if (!sessionRegistry) return;
|
|
12165
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
12289
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq21(projects.id, ctx.projectId)).get();
|
|
12166
12290
|
if (!project) return;
|
|
12167
12291
|
let content;
|
|
12168
12292
|
if (ctx.kind === RunKinds["aeo-discover-probe"]) {
|
|
@@ -12497,6 +12621,24 @@ async function createServer(opts) {
|
|
|
12497
12621
|
onAdsSyncRequested: (runId, projectId) => {
|
|
12498
12622
|
runAdsSync(runId, projectId);
|
|
12499
12623
|
},
|
|
12624
|
+
onDoctorRequested: (projectName) => {
|
|
12625
|
+
void (async () => {
|
|
12626
|
+
try {
|
|
12627
|
+
const report = await aeroClient.runDoctor({ project: projectName });
|
|
12628
|
+
const project = opts.db.select().from(projects).where(eq21(projects.name, projectName)).get();
|
|
12629
|
+
if (!project) {
|
|
12630
|
+
app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
|
|
12631
|
+
return;
|
|
12632
|
+
}
|
|
12633
|
+
await notifier.onHealthChecked(project.id, {
|
|
12634
|
+
checks: report.checks,
|
|
12635
|
+
checkedAt: report.generatedAt
|
|
12636
|
+
});
|
|
12637
|
+
} catch (err) {
|
|
12638
|
+
app.log.warn({ projectName, err }, "scheduled doctor pass failed");
|
|
12639
|
+
}
|
|
12640
|
+
})();
|
|
12641
|
+
},
|
|
12500
12642
|
onDataRefreshRequested: (projectName) => {
|
|
12501
12643
|
void refreshAllIntegrations(aeroClient, projectName);
|
|
12502
12644
|
},
|
|
@@ -12511,9 +12653,9 @@ async function createServer(opts) {
|
|
|
12511
12653
|
});
|
|
12512
12654
|
if (!probed) return;
|
|
12513
12655
|
const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
|
|
12656
|
+
and15(
|
|
12657
|
+
eq21(ccReleaseSyncs.release, probed.release),
|
|
12658
|
+
eq21(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
|
|
12517
12659
|
)
|
|
12518
12660
|
).limit(1).get();
|
|
12519
12661
|
if (alreadySynced) {
|
|
@@ -12534,18 +12676,6 @@ async function createServer(opts) {
|
|
|
12534
12676
|
);
|
|
12535
12677
|
});
|
|
12536
12678
|
})();
|
|
12537
|
-
const project = opts.db.select().from(projects).where(eq22(projects.name, projectName)).get();
|
|
12538
|
-
if (project && bingConnectionStore.getConnection(project.canonicalDomain)) {
|
|
12539
|
-
aeroClient.backlinksBingSync(projectName).catch((err) => {
|
|
12540
|
-
app.log.error(
|
|
12541
|
-
{
|
|
12542
|
-
projectName,
|
|
12543
|
-
err: err instanceof Error ? err.message : String(err)
|
|
12544
|
-
},
|
|
12545
|
-
"Scheduled Bing backlinks sync failed"
|
|
12546
|
-
);
|
|
12547
|
-
});
|
|
12548
|
-
}
|
|
12549
12679
|
},
|
|
12550
12680
|
onSiteAuditRequested: (runId, projectId) => {
|
|
12551
12681
|
runSiteAudit(runId, projectId);
|
|
@@ -12680,7 +12810,7 @@ async function createServer(opts) {
|
|
|
12680
12810
|
return removed;
|
|
12681
12811
|
}
|
|
12682
12812
|
};
|
|
12683
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
12813
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto22.randomBytes(32).toString("hex");
|
|
12684
12814
|
const googleConnectionStore = {
|
|
12685
12815
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
12686
12816
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -12745,6 +12875,7 @@ async function createServer(opts) {
|
|
|
12745
12875
|
);
|
|
12746
12876
|
}
|
|
12747
12877
|
const dashboardRequirePassword = resolveDashboardRequirePassword(process.env, opts.config);
|
|
12878
|
+
const dashboardShowResourceLinks = resolveDashboardShowResourceLinks(process.env, opts.config);
|
|
12748
12879
|
app.log.info(
|
|
12749
12880
|
{ dashboardRequirePassword },
|
|
12750
12881
|
"Dashboard password gate resolved"
|
|
@@ -12753,11 +12884,11 @@ async function createServer(opts) {
|
|
|
12753
12884
|
const googlePublicUrl = resolveGooglePublicUrl(opts.config, basePath);
|
|
12754
12885
|
if (opts.config.apiKey) {
|
|
12755
12886
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
12756
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
12887
|
+
const existing = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, keyHash)).get();
|
|
12757
12888
|
if (!existing) {
|
|
12758
12889
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
12759
12890
|
opts.db.insert(apiKeys).values({
|
|
12760
|
-
id: `key_${
|
|
12891
|
+
id: `key_${crypto22.randomBytes(8).toString("hex")}`,
|
|
12761
12892
|
name: "default",
|
|
12762
12893
|
keyHash,
|
|
12763
12894
|
keyPrefix: prefix,
|
|
@@ -12781,7 +12912,7 @@ async function createServer(opts) {
|
|
|
12781
12912
|
};
|
|
12782
12913
|
const createSession = (apiKeyId) => {
|
|
12783
12914
|
pruneExpiredSessions();
|
|
12784
|
-
const sessionId =
|
|
12915
|
+
const sessionId = crypto22.randomBytes(32).toString("hex");
|
|
12785
12916
|
sessions.set(sessionId, {
|
|
12786
12917
|
apiKeyId,
|
|
12787
12918
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -12805,7 +12936,7 @@ async function createServer(opts) {
|
|
|
12805
12936
|
};
|
|
12806
12937
|
const getDefaultApiKey = () => {
|
|
12807
12938
|
if (!opts.config.apiKey) return void 0;
|
|
12808
|
-
return opts.db.select().from(apiKeys).where(
|
|
12939
|
+
return opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
12809
12940
|
};
|
|
12810
12941
|
const createPasswordSession = (reply) => {
|
|
12811
12942
|
const key = getDefaultApiKey();
|
|
@@ -12829,7 +12960,7 @@ async function createServer(opts) {
|
|
|
12829
12960
|
if (!header) return false;
|
|
12830
12961
|
const parts = header.split(" ");
|
|
12831
12962
|
if (parts.length !== 2 || parts[0] !== "Bearer") return false;
|
|
12832
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
12963
|
+
const key = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(parts[1]))).get();
|
|
12833
12964
|
return Boolean(key && !key.revokedAt);
|
|
12834
12965
|
};
|
|
12835
12966
|
app.get(apiPrefix + "/session", async (request, reply) => {
|
|
@@ -12906,12 +13037,12 @@ async function createServer(opts) {
|
|
|
12906
13037
|
return reply.send({ authenticated: true });
|
|
12907
13038
|
}
|
|
12908
13039
|
if (apiKey) {
|
|
12909
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
13040
|
+
const key = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
12910
13041
|
if (!key || key.revokedAt) {
|
|
12911
13042
|
const err2 = authInvalid();
|
|
12912
13043
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
12913
13044
|
}
|
|
12914
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13045
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(apiKeys.id, key.id)).run();
|
|
12915
13046
|
const sessionId = createSession(key.id);
|
|
12916
13047
|
reply.header(
|
|
12917
13048
|
"set-cookie",
|
|
@@ -13022,6 +13153,11 @@ async function createServer(opts) {
|
|
|
13022
13153
|
// installer; cloud deployments inherit the secure default of `false` by
|
|
13023
13154
|
// not passing this option. Override with CANONRY_ALLOW_LOOPBACK_WEBHOOKS=0.
|
|
13024
13155
|
allowLoopbackWebhooks: process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0",
|
|
13156
|
+
// Wall-clock budget for one incremental Vercel drain. This is the only lever
|
|
13157
|
+
// that decides whether a source catches up or falls further behind, and it
|
|
13158
|
+
// was previously reachable only from tests, so a source losing ground could
|
|
13159
|
+
// not be rescued without shipping code. Unset keeps the built-in default.
|
|
13160
|
+
vercelSyncDeadlineMs: resolveVercelSyncDeadlineMs(process.env),
|
|
13025
13161
|
// Local-only Aero agent routes. Registered here so they inherit api-routes'
|
|
13026
13162
|
// auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
|
|
13027
13163
|
registerAuthenticatedRoutes: async (scope) => {
|
|
@@ -13104,7 +13240,7 @@ async function createServer(opts) {
|
|
|
13104
13240
|
deps: {
|
|
13105
13241
|
enqueueAutoExtract: ({ projectId, release: r }) => {
|
|
13106
13242
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13107
|
-
const runId =
|
|
13243
|
+
const runId = crypto22.randomUUID();
|
|
13108
13244
|
opts.db.insert(runs).values({
|
|
13109
13245
|
id: runId,
|
|
13110
13246
|
projectId,
|
|
@@ -13134,13 +13270,6 @@ async function createServer(opts) {
|
|
|
13134
13270
|
}
|
|
13135
13271
|
);
|
|
13136
13272
|
},
|
|
13137
|
-
onBingBacklinkSyncRequested: (runId, projectId) => {
|
|
13138
|
-
executeBingBacklinkSync(opts.db, runId, projectId, {
|
|
13139
|
-
resolveConnection: (domain) => bingConnectionStore.getConnection(domain)
|
|
13140
|
-
}).catch((err) => {
|
|
13141
|
-
app.log.error({ runId, err }, "Bing backlink sync failed");
|
|
13142
|
-
});
|
|
13143
|
-
},
|
|
13144
13273
|
onDiscoveryRunRequested: (input) => {
|
|
13145
13274
|
executeDiscoveryRun({
|
|
13146
13275
|
db: opts.db,
|
|
@@ -13236,7 +13365,7 @@ async function createServer(opts) {
|
|
|
13236
13365
|
...inspectOpts,
|
|
13237
13366
|
config: opts.config
|
|
13238
13367
|
}).then(() => {
|
|
13239
|
-
const finished = opts.db.select({ status: runs.status }).from(runs).where(
|
|
13368
|
+
const finished = opts.db.select({ status: runs.status }).from(runs).where(eq21(runs.id, runId)).get();
|
|
13240
13369
|
if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
|
|
13241
13370
|
return maybeRefreshGscCoverage(opts.db, opts.config, projectId);
|
|
13242
13371
|
}
|
|
@@ -13330,7 +13459,7 @@ async function createServer(opts) {
|
|
|
13330
13459
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13331
13460
|
opts.db.insert(auditLog).values(
|
|
13332
13461
|
targetProjectIds.map((projectId) => ({
|
|
13333
|
-
id:
|
|
13462
|
+
id: crypto22.randomUUID(),
|
|
13334
13463
|
projectId,
|
|
13335
13464
|
actor: "api",
|
|
13336
13465
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -13496,6 +13625,9 @@ async function createServer(opts) {
|
|
|
13496
13625
|
const injectConfig = (html, projectTabsOverride, themeOverride, renderTokenOverride) => {
|
|
13497
13626
|
const clientConfig = {};
|
|
13498
13627
|
if (basePath) clientConfig.basePath = basePath;
|
|
13628
|
+
if (!dashboardShowResourceLinks) {
|
|
13629
|
+
clientConfig.dashboard = { showResourceLinks: false };
|
|
13630
|
+
}
|
|
13499
13631
|
if (embed.enabled) {
|
|
13500
13632
|
const embedClient = embedClientConfigForRequest(embed, projectTabsOverride, themeOverride, renderTokenOverride);
|
|
13501
13633
|
if (embedClient) clientConfig.embed = embedClient;
|
|
@@ -13585,7 +13717,7 @@ async function createServer(opts) {
|
|
|
13585
13717
|
}
|
|
13586
13718
|
checkLatestVersionForServer();
|
|
13587
13719
|
scheduler.start();
|
|
13588
|
-
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(
|
|
13720
|
+
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq21(researchRuns.status, ResearchRunStatuses.queued)).all()) {
|
|
13589
13721
|
dispatchResearchRun(run.id, run.projectId);
|
|
13590
13722
|
}
|
|
13591
13723
|
app.addHook("onClose", async () => {
|