@ainyc/canonry 4.56.1 → 4.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/agent-workspace/skills/aero/references/orchestration.md +1 -1
- package/assets/agent-workspace/skills/canonry/SKILL.md +8 -1
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +44 -0
- package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +219 -0
- package/assets/assets/{BacklinksPage-CmeFZ8UJ.js → BacklinksPage-D_mc7c-b.js} +1 -1
- package/assets/assets/{ChartPrimitives-D7C1Cp8w.js → ChartPrimitives-BViWneKX.js} +1 -1
- package/assets/assets/{ProjectPage-Y6uCyjGb.js → ProjectPage-_hpYJAN1.js} +1 -1
- package/assets/assets/{RunRow-BntNdrgM.js → RunRow-DK69_0iD.js} +1 -1
- package/assets/assets/{RunsPage-Btp6qn10.js → RunsPage-DRu1peAA.js} +1 -1
- package/assets/assets/{SettingsPage-DkyNiU2i.js → SettingsPage-BrednApH.js} +1 -1
- package/assets/assets/{TrafficPage-CBl4Mwdc.js → TrafficPage-oFA65ZZc.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-BZzuWCn-.js → TrafficSourceDetailPage-CUzzaYFC.js} +1 -1
- package/assets/assets/{extract-error-message-De8_qAzs.js → extract-error-message-Cv4MXGtB.js} +1 -1
- package/assets/assets/{index-XUKhruAg.js → index-BrCh3uvb.js} +90 -90
- package/assets/assets/{server-traffic-bn9LSZN9.js → server-traffic-rYE-NlE-.js} +1 -1
- package/assets/assets/{trash-2-B5clF2rU.js → trash-2-BgGGPjQf.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-4KWPOVIT.js → chunk-JW6TQFU7.js} +171 -1
- package/dist/{chunk-WFVUZVJD.js → chunk-LPPW7O26.js} +1421 -1190
- package/dist/{chunk-I2LAM5IM.js → chunk-NOQ4ZE3E.js} +2210 -615
- package/dist/{chunk-6X5TF73A.js → chunk-TFBPLY77.js} +414 -1
- package/dist/cli.js +610 -458
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-NY3MAVPB.js → intelligence-service-V4SWVKEQ.js} +2 -2
- package/dist/mcp.js +10 -8
- package/package.json +11 -10
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ApiClient,
|
|
3
|
+
CliError,
|
|
4
|
+
PACKAGE_VERSION,
|
|
3
5
|
canonryMcpTools,
|
|
4
6
|
configExists,
|
|
5
7
|
getConfigPath,
|
|
6
8
|
loadConfig,
|
|
7
9
|
loadConfigRaw,
|
|
8
10
|
saveConfigPatch
|
|
9
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-TFBPLY77.js";
|
|
10
12
|
import {
|
|
11
13
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
12
14
|
IntelligenceService,
|
|
@@ -62,6 +64,11 @@ import {
|
|
|
62
64
|
gaTrafficSnapshots,
|
|
63
65
|
gaTrafficSummaries,
|
|
64
66
|
gaTrafficWindowSummaries,
|
|
67
|
+
gbpDailyMetrics,
|
|
68
|
+
gbpKeywordImpressions,
|
|
69
|
+
gbpLocations,
|
|
70
|
+
gbpLodgingSnapshots,
|
|
71
|
+
gbpPlaceActions,
|
|
65
72
|
groupInsights,
|
|
66
73
|
groupRunsByCreatedAt,
|
|
67
74
|
gscCoverageSnapshots,
|
|
@@ -87,7 +94,7 @@ import {
|
|
|
87
94
|
smoothedRunDelta,
|
|
88
95
|
trafficSources,
|
|
89
96
|
usageCounters
|
|
90
|
-
} from "./chunk-
|
|
97
|
+
} from "./chunk-JW6TQFU7.js";
|
|
91
98
|
import {
|
|
92
99
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
93
100
|
AGENT_PROVIDER_IDS,
|
|
@@ -101,6 +108,7 @@ import {
|
|
|
101
108
|
CheckScopes,
|
|
102
109
|
CheckStatuses,
|
|
103
110
|
CitationStates,
|
|
111
|
+
CodingAgents,
|
|
104
112
|
DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
|
|
105
113
|
DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES,
|
|
106
114
|
DISCOVERY_PROMOTE_COMPETITOR_CAP,
|
|
@@ -115,7 +123,9 @@ import {
|
|
|
115
123
|
RunKinds,
|
|
116
124
|
RunStatuses,
|
|
117
125
|
RunTriggers,
|
|
126
|
+
SKILL_MANIFEST_FILENAME,
|
|
118
127
|
SchedulableRunKinds,
|
|
128
|
+
SkillsClients,
|
|
119
129
|
TrafficEventConfidences,
|
|
120
130
|
TrafficEventKinds,
|
|
121
131
|
TrafficEvidenceKinds,
|
|
@@ -158,7 +168,9 @@ import {
|
|
|
158
168
|
citationStateSchema,
|
|
159
169
|
citationStateToCited,
|
|
160
170
|
citationVisibilityResponseSchema,
|
|
171
|
+
classifySkillFile,
|
|
161
172
|
clusterByCosine,
|
|
173
|
+
coerceSkillManifest,
|
|
162
174
|
competitorBatchRequestSchema,
|
|
163
175
|
competitorDtoSchema,
|
|
164
176
|
contentActionLabel,
|
|
@@ -199,6 +211,18 @@ import {
|
|
|
199
211
|
ga4SocialReferralHistoryEntrySchema,
|
|
200
212
|
ga4StatusDtoSchema,
|
|
201
213
|
ga4SyncResponseDtoSchema,
|
|
214
|
+
gbpAccountListResponseSchema,
|
|
215
|
+
gbpDailyMetricListResponseSchema,
|
|
216
|
+
gbpDiscoverRequestSchema,
|
|
217
|
+
gbpKeywordImpressionListResponseSchema,
|
|
218
|
+
gbpLocationDtoSchema,
|
|
219
|
+
gbpLocationListResponseSchema,
|
|
220
|
+
gbpLocationSelectionRequestSchema,
|
|
221
|
+
gbpLodgingListResponseSchema,
|
|
222
|
+
gbpPlaceActionListResponseSchema,
|
|
223
|
+
gbpSummaryDtoSchema,
|
|
224
|
+
gbpSyncRequestSchema,
|
|
225
|
+
gbpSyncResponseSchema,
|
|
202
226
|
getProviderLocationHandling,
|
|
203
227
|
googleConnectionDtoSchema,
|
|
204
228
|
gscCoverageSnapshotDtoSchema,
|
|
@@ -214,6 +238,7 @@ import {
|
|
|
214
238
|
internalError,
|
|
215
239
|
isAgentProviderId,
|
|
216
240
|
isBrowserProvider,
|
|
241
|
+
isRetryableHttpError,
|
|
217
242
|
keywordDtoSchema,
|
|
218
243
|
keywordGenerateRequestSchema,
|
|
219
244
|
latestProjectRunDtoSchema,
|
|
@@ -236,6 +261,7 @@ import {
|
|
|
236
261
|
providerError,
|
|
237
262
|
queryDtoSchema,
|
|
238
263
|
queryGenerateRequestSchema,
|
|
264
|
+
quotaExceeded,
|
|
239
265
|
recommendationExplainRequestSchema,
|
|
240
266
|
recommendationExplanationDtoSchema,
|
|
241
267
|
registrableDomain,
|
|
@@ -258,6 +284,7 @@ import {
|
|
|
258
284
|
scheduleUpsertRequestSchema,
|
|
259
285
|
serializeRunError,
|
|
260
286
|
settingsDtoSchema,
|
|
287
|
+
skillsClientSchema,
|
|
261
288
|
snapshotDiffResponseSchema,
|
|
262
289
|
snapshotListResponseSchema,
|
|
263
290
|
snapshotReportSchema,
|
|
@@ -277,6 +304,7 @@ import {
|
|
|
277
304
|
validationError,
|
|
278
305
|
visibilityStateFromAnswerMentioned,
|
|
279
306
|
windowCutoff,
|
|
307
|
+
withRetry,
|
|
280
308
|
wordpressAuditPageDtoSchema,
|
|
281
309
|
wordpressBulkMetaResultDtoSchema,
|
|
282
310
|
wordpressDiffDtoSchema,
|
|
@@ -289,7 +317,7 @@ import {
|
|
|
289
317
|
wordpressSchemaDeployResultDtoSchema,
|
|
290
318
|
wordpressSchemaStatusResultDtoSchema,
|
|
291
319
|
wordpressStatusDtoSchema
|
|
292
|
-
} from "./chunk-
|
|
320
|
+
} from "./chunk-LPPW7O26.js";
|
|
293
321
|
|
|
294
322
|
// src/telemetry.ts
|
|
295
323
|
import crypto from "crypto";
|
|
@@ -577,11 +605,11 @@ function checkLatestVersionForServer(opts) {
|
|
|
577
605
|
|
|
578
606
|
// src/server.ts
|
|
579
607
|
import { createRequire as createRequire4 } from "module";
|
|
580
|
-
import
|
|
581
|
-
import
|
|
582
|
-
import
|
|
583
|
-
import { fileURLToPath as
|
|
584
|
-
import { eq as
|
|
608
|
+
import crypto40 from "crypto";
|
|
609
|
+
import fs16 from "fs";
|
|
610
|
+
import path16 from "path";
|
|
611
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
612
|
+
import { eq as eq44 } from "drizzle-orm";
|
|
585
613
|
import Fastify from "fastify";
|
|
586
614
|
|
|
587
615
|
// ../api-routes/src/index.ts
|
|
@@ -2029,7 +2057,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
2029
2057
|
const body = JSON.stringify(payload);
|
|
2030
2058
|
const isHttps = target.url.protocol === "https:";
|
|
2031
2059
|
const port = target.url.port ? Number(target.url.port) : isHttps ? 443 : 80;
|
|
2032
|
-
const
|
|
2060
|
+
const path17 = `${target.url.pathname}${target.url.search}`;
|
|
2033
2061
|
const headers = {
|
|
2034
2062
|
"Content-Length": String(Buffer.byteLength(body)),
|
|
2035
2063
|
"Content-Type": "application/json",
|
|
@@ -2045,7 +2073,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
2045
2073
|
headers,
|
|
2046
2074
|
hostname: target.address,
|
|
2047
2075
|
method: "POST",
|
|
2048
|
-
path:
|
|
2076
|
+
path: path17,
|
|
2049
2077
|
port,
|
|
2050
2078
|
timeout: REQUEST_TIMEOUT_MS
|
|
2051
2079
|
};
|
|
@@ -3392,9 +3420,9 @@ function buildGaTrafficByPage(db, projectId) {
|
|
|
3392
3420
|
}).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
|
|
3393
3421
|
const map = /* @__PURE__ */ new Map();
|
|
3394
3422
|
for (const row of rows) {
|
|
3395
|
-
const
|
|
3396
|
-
if (!
|
|
3397
|
-
map.set(
|
|
3423
|
+
const path17 = extractPath(row.landingPage);
|
|
3424
|
+
if (!path17) continue;
|
|
3425
|
+
map.set(path17, (map.get(path17) ?? 0) + (row.sessions ?? 0));
|
|
3398
3426
|
}
|
|
3399
3427
|
return map;
|
|
3400
3428
|
}
|
|
@@ -3589,13 +3617,13 @@ function normalizeDomain(domain) {
|
|
|
3589
3617
|
function extractPath(url) {
|
|
3590
3618
|
if (!url) return "";
|
|
3591
3619
|
const trimmed = url.trim();
|
|
3592
|
-
let
|
|
3620
|
+
let path17;
|
|
3593
3621
|
try {
|
|
3594
|
-
|
|
3622
|
+
path17 = new URL(trimmed).pathname;
|
|
3595
3623
|
} catch {
|
|
3596
|
-
|
|
3624
|
+
path17 = trimmed;
|
|
3597
3625
|
}
|
|
3598
|
-
const stripped =
|
|
3626
|
+
const stripped = path17.replace(/\/+$/, "");
|
|
3599
3627
|
return stripped || "/";
|
|
3600
3628
|
}
|
|
3601
3629
|
|
|
@@ -3867,9 +3895,9 @@ function inferAdSource(params) {
|
|
|
3867
3895
|
function formatLandingPageHtml(raw) {
|
|
3868
3896
|
const value = raw ?? "";
|
|
3869
3897
|
const queryIdx = value.indexOf("?");
|
|
3870
|
-
const
|
|
3898
|
+
const path17 = queryIdx === -1 ? value : value.slice(0, queryIdx);
|
|
3871
3899
|
const query = queryIdx === -1 ? "" : value.slice(queryIdx + 1);
|
|
3872
|
-
const pathHtml = `<span class="page-path">${escapeHtml(
|
|
3900
|
+
const pathHtml = `<span class="page-path">${escapeHtml(path17 || "/")}</span>`;
|
|
3873
3901
|
if (!query) return pathHtml;
|
|
3874
3902
|
let summary = "";
|
|
3875
3903
|
try {
|
|
@@ -5227,7 +5255,7 @@ function renderLineChart(points, color, title, height = 200) {
|
|
|
5227
5255
|
y: padY + usableH - p.y / max * usableH,
|
|
5228
5256
|
raw: p
|
|
5229
5257
|
}));
|
|
5230
|
-
const
|
|
5258
|
+
const path17 = xy.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
|
|
5231
5259
|
const dots = xy.map((p) => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="${color}" />`).join("");
|
|
5232
5260
|
const xLabels = xy.map((p, i) => {
|
|
5233
5261
|
if (points.length > 8 && i % Math.ceil(points.length / 6) !== 0 && i !== points.length - 1) return "";
|
|
@@ -5239,7 +5267,7 @@ function renderLineChart(points, color, title, height = 200) {
|
|
|
5239
5267
|
<line x1="${padX}" y1="${padY + usableH}" x2="${padX + usableW}" y2="${padY + usableH}" stroke="${COLORS.border}" stroke-width="1" />
|
|
5240
5268
|
<text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatNumber(max)}</text>
|
|
5241
5269
|
<text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">0</text>
|
|
5242
|
-
<path d="${
|
|
5270
|
+
<path d="${path17}" stroke="${color}" stroke-width="2" fill="none" />
|
|
5243
5271
|
${dots}
|
|
5244
5272
|
${xLabels}
|
|
5245
5273
|
</svg>
|
|
@@ -8332,6 +8360,15 @@ var SCHEMA_TABLE = {
|
|
|
8332
8360
|
GA4SocialReferralHistoryEntry: ga4SocialReferralHistoryEntrySchema,
|
|
8333
8361
|
GA4StatusDto: ga4StatusDtoSchema,
|
|
8334
8362
|
GA4SyncResponseDto: ga4SyncResponseDtoSchema,
|
|
8363
|
+
GbpAccountListResponse: gbpAccountListResponseSchema,
|
|
8364
|
+
GbpDailyMetricListResponse: gbpDailyMetricListResponseSchema,
|
|
8365
|
+
GbpKeywordImpressionListResponse: gbpKeywordImpressionListResponseSchema,
|
|
8366
|
+
GbpLocationDto: gbpLocationDtoSchema,
|
|
8367
|
+
GbpLocationListResponse: gbpLocationListResponseSchema,
|
|
8368
|
+
GbpLodgingListResponse: gbpLodgingListResponseSchema,
|
|
8369
|
+
GbpPlaceActionListResponse: gbpPlaceActionListResponseSchema,
|
|
8370
|
+
GbpSummaryDto: gbpSummaryDtoSchema,
|
|
8371
|
+
GbpSyncResponse: gbpSyncResponseSchema,
|
|
8335
8372
|
GoogleConnectionDto: googleConnectionDtoSchema,
|
|
8336
8373
|
GscCoverageSnapshotDto: gscCoverageSnapshotDtoSchema,
|
|
8337
8374
|
GscCoverageSummaryDto: gscCoverageSummaryDtoSchema,
|
|
@@ -8435,7 +8472,7 @@ var booleanSchema = { type: "boolean" };
|
|
|
8435
8472
|
var integerSchema = { type: "integer" };
|
|
8436
8473
|
var objectSchema = { type: "object", additionalProperties: true };
|
|
8437
8474
|
var stringArraySchema = { type: "array", items: stringSchema };
|
|
8438
|
-
var googleConnectionTypeSchema = { type: "string", enum: ["gsc", "ga4"] };
|
|
8475
|
+
var googleConnectionTypeSchema = { type: "string", enum: ["gsc", "ga4", "gbp"] };
|
|
8439
8476
|
var locationSchema = {
|
|
8440
8477
|
type: "object",
|
|
8441
8478
|
required: ["label", "city", "region", "country"],
|
|
@@ -10051,6 +10088,196 @@ var routeCatalog = [
|
|
|
10051
10088
|
404: errorResponse("Project or connection not found.")
|
|
10052
10089
|
}
|
|
10053
10090
|
},
|
|
10091
|
+
{
|
|
10092
|
+
method: "post",
|
|
10093
|
+
path: "/api/v1/projects/{name}/gbp/locations/discover",
|
|
10094
|
+
summary: "Discover Google Business Profile locations and persist selection state",
|
|
10095
|
+
tags: ["gbp"],
|
|
10096
|
+
parameters: [nameParameter],
|
|
10097
|
+
requestBody: {
|
|
10098
|
+
content: {
|
|
10099
|
+
"application/json": {
|
|
10100
|
+
schema: {
|
|
10101
|
+
type: "object",
|
|
10102
|
+
properties: {
|
|
10103
|
+
selectAllNew: booleanSchema,
|
|
10104
|
+
accountName: stringSchema,
|
|
10105
|
+
switchAccount: booleanSchema
|
|
10106
|
+
}
|
|
10107
|
+
}
|
|
10108
|
+
}
|
|
10109
|
+
}
|
|
10110
|
+
},
|
|
10111
|
+
responses: {
|
|
10112
|
+
200: jsonResponse("List of discovered locations and selection summary returned.", "GbpLocationListResponse"),
|
|
10113
|
+
400: errorResponse("Invalid discover request, unknown account, account-switch not opted into, or scope/API problem."),
|
|
10114
|
+
404: errorResponse("Project not found."),
|
|
10115
|
+
429: errorResponse("GBP API quota exceeded (access form may not be approved).")
|
|
10116
|
+
}
|
|
10117
|
+
},
|
|
10118
|
+
{
|
|
10119
|
+
method: "get",
|
|
10120
|
+
path: "/api/v1/projects/{name}/gbp/accounts",
|
|
10121
|
+
summary: "List Google Business Profile accounts the connected user can access",
|
|
10122
|
+
tags: ["gbp"],
|
|
10123
|
+
parameters: [nameParameter],
|
|
10124
|
+
responses: {
|
|
10125
|
+
200: jsonResponse("Accounts the OAuth user manages or owns.", "GbpAccountListResponse"),
|
|
10126
|
+
400: errorResponse("No GBP connection or scope/API problem."),
|
|
10127
|
+
404: errorResponse("Project not found."),
|
|
10128
|
+
429: errorResponse("GBP API quota exceeded (access form may not be approved).")
|
|
10129
|
+
}
|
|
10130
|
+
},
|
|
10131
|
+
{
|
|
10132
|
+
method: "get",
|
|
10133
|
+
path: "/api/v1/projects/{name}/gbp/locations",
|
|
10134
|
+
summary: "List Google Business Profile locations + selection state",
|
|
10135
|
+
tags: ["gbp"],
|
|
10136
|
+
parameters: [
|
|
10137
|
+
nameParameter,
|
|
10138
|
+
{ in: "query", name: "selected", required: false, description: "Filter to selected=true or selected=false", schema: { type: "string", enum: ["true", "false"] } }
|
|
10139
|
+
],
|
|
10140
|
+
responses: {
|
|
10141
|
+
200: jsonResponse("List of locations returned.", "GbpLocationListResponse"),
|
|
10142
|
+
404: errorResponse("Project not found.")
|
|
10143
|
+
}
|
|
10144
|
+
},
|
|
10145
|
+
{
|
|
10146
|
+
method: "put",
|
|
10147
|
+
path: "/api/v1/projects/{name}/gbp/locations/{locationName}/selection",
|
|
10148
|
+
summary: "Toggle a Google Business Profile location's sync selection",
|
|
10149
|
+
tags: ["gbp"],
|
|
10150
|
+
parameters: [
|
|
10151
|
+
nameParameter,
|
|
10152
|
+
{ in: "path", name: "locationName", required: true, schema: stringSchema, description: 'URL-encoded "locations/{n}" resource name' }
|
|
10153
|
+
],
|
|
10154
|
+
requestBody: {
|
|
10155
|
+
required: true,
|
|
10156
|
+
content: {
|
|
10157
|
+
"application/json": {
|
|
10158
|
+
schema: {
|
|
10159
|
+
type: "object",
|
|
10160
|
+
required: ["selected"],
|
|
10161
|
+
properties: { selected: booleanSchema }
|
|
10162
|
+
}
|
|
10163
|
+
}
|
|
10164
|
+
}
|
|
10165
|
+
},
|
|
10166
|
+
responses: {
|
|
10167
|
+
200: jsonResponse("Updated location returned.", "GbpLocationDto"),
|
|
10168
|
+
400: errorResponse("Invalid selection request."),
|
|
10169
|
+
404: errorResponse("Project or location not found.")
|
|
10170
|
+
}
|
|
10171
|
+
},
|
|
10172
|
+
{
|
|
10173
|
+
method: "delete",
|
|
10174
|
+
path: "/api/v1/projects/{name}/gbp/connection",
|
|
10175
|
+
summary: "Disconnect Google Business Profile and remove discovered locations",
|
|
10176
|
+
tags: ["gbp"],
|
|
10177
|
+
parameters: [nameParameter],
|
|
10178
|
+
responses: {
|
|
10179
|
+
204: { description: "Disconnected." },
|
|
10180
|
+
404: errorResponse("Project not found.")
|
|
10181
|
+
}
|
|
10182
|
+
},
|
|
10183
|
+
{
|
|
10184
|
+
method: "post",
|
|
10185
|
+
path: "/api/v1/projects/{name}/gbp/sync",
|
|
10186
|
+
summary: "Trigger a Google Business Profile performance sync (daily metrics + monthly keywords)",
|
|
10187
|
+
tags: ["gbp"],
|
|
10188
|
+
parameters: [nameParameter],
|
|
10189
|
+
requestBody: {
|
|
10190
|
+
content: {
|
|
10191
|
+
"application/json": {
|
|
10192
|
+
schema: {
|
|
10193
|
+
type: "object",
|
|
10194
|
+
properties: {
|
|
10195
|
+
locationNames: stringArraySchema,
|
|
10196
|
+
daysOfMetrics: integerSchema,
|
|
10197
|
+
monthsOfKeywords: integerSchema
|
|
10198
|
+
}
|
|
10199
|
+
}
|
|
10200
|
+
}
|
|
10201
|
+
}
|
|
10202
|
+
},
|
|
10203
|
+
responses: {
|
|
10204
|
+
200: jsonResponse("Sync run queued.", "GbpSyncResponse"),
|
|
10205
|
+
400: errorResponse("Invalid sync request or no GBP connection."),
|
|
10206
|
+
404: errorResponse("Project not found.")
|
|
10207
|
+
}
|
|
10208
|
+
},
|
|
10209
|
+
{
|
|
10210
|
+
method: "get",
|
|
10211
|
+
path: "/api/v1/projects/{name}/gbp/metrics",
|
|
10212
|
+
summary: "List stored Google Business Profile daily performance metrics",
|
|
10213
|
+
tags: ["gbp"],
|
|
10214
|
+
parameters: [
|
|
10215
|
+
nameParameter,
|
|
10216
|
+
{ in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema },
|
|
10217
|
+
{ in: "query", name: "metric", required: false, description: "Filter to one DailyMetric", schema: stringSchema }
|
|
10218
|
+
],
|
|
10219
|
+
responses: {
|
|
10220
|
+
200: jsonResponse("Daily metrics returned.", "GbpDailyMetricListResponse"),
|
|
10221
|
+
404: errorResponse("Project not found.")
|
|
10222
|
+
}
|
|
10223
|
+
},
|
|
10224
|
+
{
|
|
10225
|
+
method: "get",
|
|
10226
|
+
path: "/api/v1/projects/{name}/gbp/keywords",
|
|
10227
|
+
summary: "List stored Google Business Profile monthly keyword impressions",
|
|
10228
|
+
tags: ["gbp"],
|
|
10229
|
+
parameters: [
|
|
10230
|
+
nameParameter,
|
|
10231
|
+
{ in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema },
|
|
10232
|
+
{ in: "query", name: "month", required: false, description: "Filter to one YYYY-MM month", schema: stringSchema }
|
|
10233
|
+
],
|
|
10234
|
+
responses: {
|
|
10235
|
+
200: jsonResponse("Keyword impressions returned.", "GbpKeywordImpressionListResponse"),
|
|
10236
|
+
404: errorResponse("Project not found.")
|
|
10237
|
+
}
|
|
10238
|
+
},
|
|
10239
|
+
{
|
|
10240
|
+
method: "get",
|
|
10241
|
+
path: "/api/v1/projects/{name}/gbp/place-actions",
|
|
10242
|
+
summary: "List stored Google Business Profile place action links (booking CTAs)",
|
|
10243
|
+
tags: ["gbp"],
|
|
10244
|
+
parameters: [
|
|
10245
|
+
nameParameter,
|
|
10246
|
+
{ in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema }
|
|
10247
|
+
],
|
|
10248
|
+
responses: {
|
|
10249
|
+
200: jsonResponse("Place actions returned.", "GbpPlaceActionListResponse"),
|
|
10250
|
+
404: errorResponse("Project not found.")
|
|
10251
|
+
}
|
|
10252
|
+
},
|
|
10253
|
+
{
|
|
10254
|
+
method: "get",
|
|
10255
|
+
path: "/api/v1/projects/{name}/gbp/lodging",
|
|
10256
|
+
summary: "List latest Google Business Profile lodging snapshots per location",
|
|
10257
|
+
tags: ["gbp"],
|
|
10258
|
+
parameters: [
|
|
10259
|
+
nameParameter,
|
|
10260
|
+
{ in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema }
|
|
10261
|
+
],
|
|
10262
|
+
responses: {
|
|
10263
|
+
200: jsonResponse("Lodging snapshots returned.", "GbpLodgingListResponse"),
|
|
10264
|
+
404: errorResponse("Project not found.")
|
|
10265
|
+
}
|
|
10266
|
+
},
|
|
10267
|
+
{
|
|
10268
|
+
method: "get",
|
|
10269
|
+
path: "/api/v1/projects/{name}/gbp/summary",
|
|
10270
|
+
summary: "Composite Google Business Profile local-AEO summary (all derived metrics)",
|
|
10271
|
+
tags: ["gbp"],
|
|
10272
|
+
parameters: [
|
|
10273
|
+
nameParameter,
|
|
10274
|
+
{ in: "query", name: "locationName", required: false, description: "Scope to one location (omit = aggregate across selected)", schema: stringSchema }
|
|
10275
|
+
],
|
|
10276
|
+
responses: {
|
|
10277
|
+
200: jsonResponse("Summary returned.", "GbpSummaryDto"),
|
|
10278
|
+
404: errorResponse("Project not found.")
|
|
10279
|
+
}
|
|
10280
|
+
},
|
|
10054
10281
|
{
|
|
10055
10282
|
method: "post",
|
|
10056
10283
|
path: "/api/v1/projects/{name}/bing/connect",
|
|
@@ -11928,8 +12155,8 @@ async function openApiRoutes(app, opts = {}) {
|
|
|
11928
12155
|
return reply.type("application/json").send(buildOpenApiDocument(opts));
|
|
11929
12156
|
});
|
|
11930
12157
|
}
|
|
11931
|
-
function buildOperationId(method,
|
|
11932
|
-
const parts =
|
|
12158
|
+
function buildOperationId(method, path17) {
|
|
12159
|
+
const parts = path17.split("/").filter(Boolean).map((part) => {
|
|
11933
12160
|
if (part.startsWith("{") && part.endsWith("}")) {
|
|
11934
12161
|
return `by-${part.slice(1, -1)}`;
|
|
11935
12162
|
}
|
|
@@ -12368,8 +12595,89 @@ function formatNotification(row) {
|
|
|
12368
12595
|
}
|
|
12369
12596
|
|
|
12370
12597
|
// ../api-routes/src/google.ts
|
|
12371
|
-
import
|
|
12372
|
-
import { eq as eq19, and as and13, desc as desc9, sql as sql7 } from "drizzle-orm";
|
|
12598
|
+
import crypto16 from "crypto";
|
|
12599
|
+
import { eq as eq19, and as and13, desc as desc9, sql as sql7, inArray as inArray9 } from "drizzle-orm";
|
|
12600
|
+
|
|
12601
|
+
// ../api-routes/src/gbp-summary.ts
|
|
12602
|
+
function computeMetricTotals(rows) {
|
|
12603
|
+
const totals = {};
|
|
12604
|
+
for (const row of rows) {
|
|
12605
|
+
totals[row.metric] = (totals[row.metric] ?? 0) + row.value;
|
|
12606
|
+
}
|
|
12607
|
+
return totals;
|
|
12608
|
+
}
|
|
12609
|
+
function shiftDate(date, days) {
|
|
12610
|
+
const [y, m, d] = date.split("-").map(Number);
|
|
12611
|
+
const dt = new Date(Date.UTC(y, m - 1, d));
|
|
12612
|
+
dt.setUTCDate(dt.getUTCDate() + days);
|
|
12613
|
+
const yy = dt.getUTCFullYear();
|
|
12614
|
+
const mm = String(dt.getUTCMonth() + 1).padStart(2, "0");
|
|
12615
|
+
const dd = String(dt.getUTCDate()).padStart(2, "0");
|
|
12616
|
+
return `${yy}-${mm}-${dd}`;
|
|
12617
|
+
}
|
|
12618
|
+
function roundPct(numerator, denominator) {
|
|
12619
|
+
if (denominator === 0) return 0;
|
|
12620
|
+
return Math.round(numerator / denominator * 100);
|
|
12621
|
+
}
|
|
12622
|
+
function computeWindowDelta(rows, referenceDate) {
|
|
12623
|
+
const recentLow = shiftDate(referenceDate, -7);
|
|
12624
|
+
const priorLow = shiftDate(referenceDate, -14);
|
|
12625
|
+
const recent7d = {};
|
|
12626
|
+
const prior7d = {};
|
|
12627
|
+
for (const row of rows) {
|
|
12628
|
+
if (row.date > recentLow && row.date <= referenceDate) {
|
|
12629
|
+
recent7d[row.metric] = (recent7d[row.metric] ?? 0) + row.value;
|
|
12630
|
+
} else if (row.date > priorLow && row.date <= recentLow) {
|
|
12631
|
+
prior7d[row.metric] = (prior7d[row.metric] ?? 0) + row.value;
|
|
12632
|
+
}
|
|
12633
|
+
}
|
|
12634
|
+
const deltaPct = {};
|
|
12635
|
+
const metrics = /* @__PURE__ */ new Set([...Object.keys(recent7d), ...Object.keys(prior7d)]);
|
|
12636
|
+
for (const metric of metrics) {
|
|
12637
|
+
const recent = recent7d[metric] ?? 0;
|
|
12638
|
+
const prior = prior7d[metric] ?? 0;
|
|
12639
|
+
recent7d[metric] = recent;
|
|
12640
|
+
prior7d[metric] = prior;
|
|
12641
|
+
deltaPct[metric] = prior === 0 ? null : Math.round((recent - prior) / prior * 100);
|
|
12642
|
+
}
|
|
12643
|
+
return { recent7d, prior7d, deltaPct };
|
|
12644
|
+
}
|
|
12645
|
+
function computeKeywordCoverage(rows) {
|
|
12646
|
+
const total = rows.length;
|
|
12647
|
+
const thresholdedCount = rows.filter((r) => r.valueCount === null).length;
|
|
12648
|
+
return { total, thresholdedCount, thresholdedPct: roundPct(thresholdedCount, total) };
|
|
12649
|
+
}
|
|
12650
|
+
function summarizePlaceActions(rows) {
|
|
12651
|
+
return {
|
|
12652
|
+
total: rows.length,
|
|
12653
|
+
hasReservationCta: rows.some((r) => r.placeActionType === "RESERVATION"),
|
|
12654
|
+
hasBookingCta: rows.some((r) => r.placeActionType === "BOOK"),
|
|
12655
|
+
hasDirectMerchantCta: rows.some((r) => r.providerType === "MERCHANT")
|
|
12656
|
+
};
|
|
12657
|
+
}
|
|
12658
|
+
function summarizeLodging(rows) {
|
|
12659
|
+
const populatedLodgingCount = rows.filter((r) => r.populatedGroupCount > 0).length;
|
|
12660
|
+
return {
|
|
12661
|
+
lodgingLocationCount: rows.length,
|
|
12662
|
+
populatedLodgingCount,
|
|
12663
|
+
emptyLodgingCount: rows.length - populatedLodgingCount
|
|
12664
|
+
};
|
|
12665
|
+
}
|
|
12666
|
+
function buildGbpSummary(input) {
|
|
12667
|
+
const window = computeWindowDelta(input.dailyMetrics, input.referenceDate);
|
|
12668
|
+
return {
|
|
12669
|
+
scope: { locationName: input.locationName, locationCount: input.locationCount },
|
|
12670
|
+
performance: {
|
|
12671
|
+
totals: computeMetricTotals(input.dailyMetrics),
|
|
12672
|
+
recent7d: window.recent7d,
|
|
12673
|
+
prior7d: window.prior7d,
|
|
12674
|
+
deltaPct: window.deltaPct
|
|
12675
|
+
},
|
|
12676
|
+
keywords: computeKeywordCoverage(input.keywords),
|
|
12677
|
+
placeActions: summarizePlaceActions(input.placeActions),
|
|
12678
|
+
lodging: summarizeLodging(input.lodging)
|
|
12679
|
+
};
|
|
12680
|
+
}
|
|
12373
12681
|
|
|
12374
12682
|
// ../integration-google/src/constants.ts
|
|
12375
12683
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -12923,27 +13231,34 @@ function isRetryableGa4Error(err) {
|
|
|
12923
13231
|
return false;
|
|
12924
13232
|
}
|
|
12925
13233
|
async function withGa4Retry(fn, errLabel) {
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
13234
|
+
return withRetry(fn, {
|
|
13235
|
+
maxRetries: GA4_MAX_RETRIES,
|
|
13236
|
+
baseDelayMs: GA4_INITIAL_RETRY_DELAY_MS,
|
|
13237
|
+
// Retry loop predates jittered backoff; preserve the deterministic
|
|
13238
|
+
// schedule operators have been seeing in production until we have a
|
|
13239
|
+
// reason to introduce variance.
|
|
13240
|
+
jitter: false,
|
|
13241
|
+
isRetryable: isRetryableGa4Error,
|
|
13242
|
+
// Honor `Retry-After` when the server supplies it; fall back to the
|
|
13243
|
+
// computed exponential delay otherwise.
|
|
13244
|
+
computeDelayMs: (_attempt, err, defaultMs) => {
|
|
13245
|
+
const ga4Err = err;
|
|
13246
|
+
if (ga4Err?.retryAfterSeconds !== void 0) {
|
|
13247
|
+
return Math.max(0, ga4Err.retryAfterSeconds * 1e3);
|
|
13248
|
+
}
|
|
13249
|
+
return defaultMs;
|
|
13250
|
+
},
|
|
13251
|
+
onRetry: ({ attempt, err, delayMs }) => {
|
|
12933
13252
|
const ga4Err = err;
|
|
12934
|
-
const computedDelayMs = GA4_INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
12935
|
-
const delayMs = ga4Err.retryAfterSeconds !== void 0 ? Math.max(0, ga4Err.retryAfterSeconds * 1e3) : computedDelayMs;
|
|
12936
13253
|
ga4Log("info", `${errLabel}.retry`, {
|
|
12937
13254
|
attempt: attempt + 1,
|
|
12938
13255
|
maxAttempts: GA4_MAX_RETRIES + 1,
|
|
12939
|
-
status: ga4Err
|
|
13256
|
+
status: ga4Err?.status,
|
|
12940
13257
|
delayMs,
|
|
12941
|
-
usedRetryAfter: ga4Err
|
|
13258
|
+
usedRetryAfter: ga4Err?.retryAfterSeconds !== void 0
|
|
12942
13259
|
});
|
|
12943
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
12944
13260
|
}
|
|
12945
|
-
}
|
|
12946
|
-
throw lastError;
|
|
13261
|
+
});
|
|
12947
13262
|
}
|
|
12948
13263
|
async function runReport(accessToken, propertyId, request) {
|
|
12949
13264
|
validatePropertyId(propertyId);
|
|
@@ -13430,9 +13745,307 @@ async function fetchSocialReferrals(accessToken, propertyId, days) {
|
|
|
13430
13745
|
return rows;
|
|
13431
13746
|
}
|
|
13432
13747
|
|
|
13748
|
+
// ../integration-google-business-profile/src/constants.ts
|
|
13749
|
+
var GBP_SCOPE = "https://www.googleapis.com/auth/business.manage";
|
|
13750
|
+
var GBP_ACCOUNT_MANAGEMENT_BASE = "https://mybusinessaccountmanagement.googleapis.com/v1";
|
|
13751
|
+
var GBP_BUSINESS_INFO_BASE = "https://mybusinessbusinessinformation.googleapis.com/v1";
|
|
13752
|
+
var GBP_PERFORMANCE_BASE = "https://businessprofileperformance.googleapis.com/v1";
|
|
13753
|
+
var GBP_LODGING_BASE = "https://mybusinesslodging.googleapis.com/v1";
|
|
13754
|
+
var GBP_DAILY_METRICS = [
|
|
13755
|
+
"BUSINESS_IMPRESSIONS_DESKTOP_MAPS",
|
|
13756
|
+
"BUSINESS_IMPRESSIONS_DESKTOP_SEARCH",
|
|
13757
|
+
"BUSINESS_IMPRESSIONS_MOBILE_MAPS",
|
|
13758
|
+
"BUSINESS_IMPRESSIONS_MOBILE_SEARCH",
|
|
13759
|
+
"BUSINESS_CONVERSATIONS",
|
|
13760
|
+
"BUSINESS_DIRECTION_REQUESTS",
|
|
13761
|
+
"CALL_CLICKS",
|
|
13762
|
+
"WEBSITE_CLICKS",
|
|
13763
|
+
"BUSINESS_BOOKINGS",
|
|
13764
|
+
"BUSINESS_FOOD_ORDERS",
|
|
13765
|
+
"BUSINESS_FOOD_MENU_CLICKS"
|
|
13766
|
+
];
|
|
13767
|
+
var GBP_REQUEST_TIMEOUT_MS = 3e4;
|
|
13768
|
+
var GBP_DEFAULT_PAGE_SIZE = 100;
|
|
13769
|
+
var GBP_MAX_PAGES = 200;
|
|
13770
|
+
var GBP_LOCATIONS_DEFAULT_READ_MASK = [
|
|
13771
|
+
"name",
|
|
13772
|
+
"title",
|
|
13773
|
+
"storefrontAddress",
|
|
13774
|
+
"websiteUri",
|
|
13775
|
+
"categories.primaryCategory.displayName"
|
|
13776
|
+
].join(",");
|
|
13777
|
+
|
|
13778
|
+
// ../integration-google-business-profile/src/types.ts
|
|
13779
|
+
var GbpApiError = class extends Error {
|
|
13780
|
+
status;
|
|
13781
|
+
reason;
|
|
13782
|
+
body;
|
|
13783
|
+
quotaLimitValue;
|
|
13784
|
+
constructor(message, status, reason, body, quotaLimitValue = null) {
|
|
13785
|
+
super(message);
|
|
13786
|
+
this.name = "GbpApiError";
|
|
13787
|
+
this.status = status;
|
|
13788
|
+
this.reason = reason;
|
|
13789
|
+
this.body = body;
|
|
13790
|
+
this.quotaLimitValue = quotaLimitValue;
|
|
13791
|
+
}
|
|
13792
|
+
};
|
|
13793
|
+
|
|
13794
|
+
// ../integration-google-business-profile/src/http.ts
|
|
13795
|
+
function extractReason(body) {
|
|
13796
|
+
if (!body || typeof body !== "object") return null;
|
|
13797
|
+
const payload = body;
|
|
13798
|
+
const details = payload.error?.details;
|
|
13799
|
+
if (!Array.isArray(details)) return null;
|
|
13800
|
+
for (const detail of details) {
|
|
13801
|
+
if (detail && typeof detail.reason === "string") return detail.reason;
|
|
13802
|
+
}
|
|
13803
|
+
return null;
|
|
13804
|
+
}
|
|
13805
|
+
function extractQuotaLimitValue(body) {
|
|
13806
|
+
if (!body || typeof body !== "object") return null;
|
|
13807
|
+
const payload = body;
|
|
13808
|
+
const details = payload.error?.details;
|
|
13809
|
+
if (!Array.isArray(details)) return null;
|
|
13810
|
+
for (const detail of details) {
|
|
13811
|
+
const raw = detail?.metadata?.quota_limit_value;
|
|
13812
|
+
if (typeof raw === "string") {
|
|
13813
|
+
const parsed = Number(raw);
|
|
13814
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
13815
|
+
}
|
|
13816
|
+
}
|
|
13817
|
+
return null;
|
|
13818
|
+
}
|
|
13819
|
+
function isRetryable(err) {
|
|
13820
|
+
if (!(err instanceof GbpApiError)) return false;
|
|
13821
|
+
if (err.status === 429) {
|
|
13822
|
+
return err.quotaLimitValue !== 0;
|
|
13823
|
+
}
|
|
13824
|
+
if (err.status === 503) return true;
|
|
13825
|
+
return false;
|
|
13826
|
+
}
|
|
13827
|
+
async function gbpFetchOnce(url, accessToken, opts) {
|
|
13828
|
+
const controller = new AbortController();
|
|
13829
|
+
const timeout = setTimeout(() => controller.abort(), GBP_REQUEST_TIMEOUT_MS);
|
|
13830
|
+
try {
|
|
13831
|
+
const headers = {
|
|
13832
|
+
authorization: `Bearer ${accessToken}`,
|
|
13833
|
+
accept: "application/json"
|
|
13834
|
+
};
|
|
13835
|
+
if (opts.quotaProject) headers["x-goog-user-project"] = opts.quotaProject;
|
|
13836
|
+
const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
|
|
13837
|
+
const text = await res.text();
|
|
13838
|
+
let body;
|
|
13839
|
+
try {
|
|
13840
|
+
body = text ? JSON.parse(text) : void 0;
|
|
13841
|
+
} catch {
|
|
13842
|
+
body = text;
|
|
13843
|
+
}
|
|
13844
|
+
if (!res.ok) {
|
|
13845
|
+
const payload = body;
|
|
13846
|
+
const message = typeof payload === "object" && payload?.error?.message ? payload.error.message : typeof payload === "string" ? payload : `HTTP ${res.status}`;
|
|
13847
|
+
throw new GbpApiError(message, res.status, extractReason(body), body, extractQuotaLimitValue(body));
|
|
13848
|
+
}
|
|
13849
|
+
return body;
|
|
13850
|
+
} finally {
|
|
13851
|
+
clearTimeout(timeout);
|
|
13852
|
+
}
|
|
13853
|
+
}
|
|
13854
|
+
async function gbpFetchGet(url, accessToken, opts = {}) {
|
|
13855
|
+
return withRetry(() => gbpFetchOnce(url, accessToken, opts), {
|
|
13856
|
+
maxRetries: opts.retry?.maxRetries ?? 5,
|
|
13857
|
+
baseDelayMs: opts.retry?.baseDelayMs ?? 1e3,
|
|
13858
|
+
jitter: true,
|
|
13859
|
+
isRetryable,
|
|
13860
|
+
sleep: opts.retry?.sleep
|
|
13861
|
+
});
|
|
13862
|
+
}
|
|
13863
|
+
|
|
13864
|
+
// ../integration-google-business-profile/src/accounts-client.ts
|
|
13865
|
+
async function listAccounts(accessToken, opts = {}) {
|
|
13866
|
+
const collected = [];
|
|
13867
|
+
let pageToken;
|
|
13868
|
+
let page = 0;
|
|
13869
|
+
do {
|
|
13870
|
+
const url = new URL(`${GBP_ACCOUNT_MANAGEMENT_BASE}/accounts`);
|
|
13871
|
+
url.searchParams.set("pageSize", String(GBP_DEFAULT_PAGE_SIZE));
|
|
13872
|
+
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
|
13873
|
+
const res = await gbpFetchGet(url.toString(), accessToken, opts);
|
|
13874
|
+
if (res.accounts) collected.push(...res.accounts);
|
|
13875
|
+
pageToken = res.nextPageToken;
|
|
13876
|
+
page++;
|
|
13877
|
+
} while (pageToken && page < GBP_MAX_PAGES);
|
|
13878
|
+
return collected;
|
|
13879
|
+
}
|
|
13880
|
+
|
|
13881
|
+
// ../integration-google-business-profile/src/locations-client.ts
|
|
13882
|
+
async function listLocations(accessToken, accountName, opts = {}) {
|
|
13883
|
+
const readMask = opts.readMask ?? GBP_LOCATIONS_DEFAULT_READ_MASK;
|
|
13884
|
+
const collected = [];
|
|
13885
|
+
let pageToken;
|
|
13886
|
+
let page = 0;
|
|
13887
|
+
do {
|
|
13888
|
+
const url = new URL(`${GBP_BUSINESS_INFO_BASE}/${accountName}/locations`);
|
|
13889
|
+
url.searchParams.set("readMask", readMask);
|
|
13890
|
+
url.searchParams.set("pageSize", String(GBP_DEFAULT_PAGE_SIZE));
|
|
13891
|
+
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
|
13892
|
+
const res = await gbpFetchGet(url.toString(), accessToken, opts);
|
|
13893
|
+
if (res.locations) collected.push(...res.locations);
|
|
13894
|
+
pageToken = res.nextPageToken;
|
|
13895
|
+
page++;
|
|
13896
|
+
} while (pageToken && page < GBP_MAX_PAGES);
|
|
13897
|
+
return collected;
|
|
13898
|
+
}
|
|
13899
|
+
function formatStorefrontAddress(loc) {
|
|
13900
|
+
const addr = loc.storefrontAddress;
|
|
13901
|
+
if (!addr) return null;
|
|
13902
|
+
const parts = [
|
|
13903
|
+
...addr.addressLines ?? [],
|
|
13904
|
+
addr.locality,
|
|
13905
|
+
addr.administrativeArea,
|
|
13906
|
+
addr.postalCode,
|
|
13907
|
+
addr.regionCode
|
|
13908
|
+
].filter((p) => Boolean(p));
|
|
13909
|
+
return parts.length ? parts.join(", ") : null;
|
|
13910
|
+
}
|
|
13911
|
+
|
|
13912
|
+
// ../integration-google-business-profile/src/performance-client.ts
|
|
13913
|
+
function pad2(n) {
|
|
13914
|
+
return String(n).padStart(2, "0");
|
|
13915
|
+
}
|
|
13916
|
+
function formatGoogleDate(d) {
|
|
13917
|
+
return `${d.year}-${pad2(d.month)}-${pad2(d.day)}`;
|
|
13918
|
+
}
|
|
13919
|
+
async function fetchDailyMetrics(accessToken, locationName, opts) {
|
|
13920
|
+
const params = new URLSearchParams();
|
|
13921
|
+
for (const m of opts.metrics) params.append("dailyMetrics", m);
|
|
13922
|
+
params.set("dailyRange.startDate.year", String(opts.startDate.getUTCFullYear()));
|
|
13923
|
+
params.set("dailyRange.startDate.month", String(opts.startDate.getUTCMonth() + 1));
|
|
13924
|
+
params.set("dailyRange.startDate.day", String(opts.startDate.getUTCDate()));
|
|
13925
|
+
params.set("dailyRange.endDate.year", String(opts.endDate.getUTCFullYear()));
|
|
13926
|
+
params.set("dailyRange.endDate.month", String(opts.endDate.getUTCMonth() + 1));
|
|
13927
|
+
params.set("dailyRange.endDate.day", String(opts.endDate.getUTCDate()));
|
|
13928
|
+
const url = `${GBP_PERFORMANCE_BASE}/${locationName}:fetchMultiDailyMetricsTimeSeries?${params.toString()}`;
|
|
13929
|
+
const res = await gbpFetchGet(url, accessToken, opts);
|
|
13930
|
+
const rows = [];
|
|
13931
|
+
const series = res.multiDailyMetricTimeSeries?.[0]?.dailyMetricTimeSeries ?? [];
|
|
13932
|
+
for (const s of series) {
|
|
13933
|
+
for (const dv of s.timeSeries?.datedValues ?? []) {
|
|
13934
|
+
rows.push({
|
|
13935
|
+
metric: s.dailyMetric,
|
|
13936
|
+
date: formatGoogleDate(dv.date),
|
|
13937
|
+
// Omitted value = zero traffic that day.
|
|
13938
|
+
value: dv.value !== void 0 ? Number(dv.value) : 0
|
|
13939
|
+
});
|
|
13940
|
+
}
|
|
13941
|
+
}
|
|
13942
|
+
return rows;
|
|
13943
|
+
}
|
|
13944
|
+
async function listMonthlyKeywords(accessToken, locationName, opts) {
|
|
13945
|
+
const collected = [];
|
|
13946
|
+
let pageToken;
|
|
13947
|
+
let page = 0;
|
|
13948
|
+
do {
|
|
13949
|
+
const params = new URLSearchParams();
|
|
13950
|
+
params.set("monthlyRange.startMonth.year", String(opts.startMonth.year));
|
|
13951
|
+
params.set("monthlyRange.startMonth.month", String(opts.startMonth.month));
|
|
13952
|
+
params.set("monthlyRange.endMonth.year", String(opts.endMonth.year));
|
|
13953
|
+
params.set("monthlyRange.endMonth.month", String(opts.endMonth.month));
|
|
13954
|
+
params.set("pageSize", String(GBP_DEFAULT_PAGE_SIZE));
|
|
13955
|
+
if (pageToken) params.set("pageToken", pageToken);
|
|
13956
|
+
const url = `${GBP_PERFORMANCE_BASE}/${locationName}/searchkeywords/impressions/monthly?${params.toString()}`;
|
|
13957
|
+
const res = await gbpFetchGet(url, accessToken, opts);
|
|
13958
|
+
for (const c of res.searchKeywordsCounts ?? []) {
|
|
13959
|
+
const value = c.insightsValue?.value;
|
|
13960
|
+
const threshold = c.insightsValue?.threshold;
|
|
13961
|
+
collected.push({
|
|
13962
|
+
keyword: c.searchKeyword,
|
|
13963
|
+
valueCount: value !== void 0 ? Number(value) : null,
|
|
13964
|
+
valueThreshold: value === void 0 && threshold !== void 0 ? Number(threshold) : null
|
|
13965
|
+
});
|
|
13966
|
+
}
|
|
13967
|
+
pageToken = res.nextPageToken;
|
|
13968
|
+
page++;
|
|
13969
|
+
} while (pageToken && page < GBP_MAX_PAGES);
|
|
13970
|
+
return collected;
|
|
13971
|
+
}
|
|
13972
|
+
|
|
13973
|
+
// ../integration-google-business-profile/src/place-actions-client.ts
|
|
13974
|
+
async function listPlaceActionLinks(accessToken, locationName, opts = {}) {
|
|
13975
|
+
const collected = [];
|
|
13976
|
+
let pageToken;
|
|
13977
|
+
let page = 0;
|
|
13978
|
+
do {
|
|
13979
|
+
const url = new URL(`${GBP_BUSINESS_INFO_BASE}/${locationName}/placeActionLinks`);
|
|
13980
|
+
url.searchParams.set("pageSize", String(GBP_DEFAULT_PAGE_SIZE));
|
|
13981
|
+
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
|
13982
|
+
const res = await gbpFetchGet(url.toString(), accessToken, opts);
|
|
13983
|
+
for (const link of res.placeActionLinks ?? []) {
|
|
13984
|
+
collected.push({
|
|
13985
|
+
placeActionLinkName: link.name,
|
|
13986
|
+
placeActionType: link.placeActionType,
|
|
13987
|
+
uri: link.uri ?? null,
|
|
13988
|
+
isPreferred: link.isPreferred ?? false,
|
|
13989
|
+
providerType: link.providerType ?? null
|
|
13990
|
+
});
|
|
13991
|
+
}
|
|
13992
|
+
pageToken = res.nextPageToken;
|
|
13993
|
+
page++;
|
|
13994
|
+
} while (pageToken && page < GBP_MAX_PAGES);
|
|
13995
|
+
return collected;
|
|
13996
|
+
}
|
|
13997
|
+
|
|
13998
|
+
// ../integration-google-business-profile/src/lodging-client.ts
|
|
13999
|
+
import crypto15 from "crypto";
|
|
14000
|
+
async function getLodging(accessToken, locationName, opts = {}) {
|
|
14001
|
+
const url = `${GBP_LODGING_BASE}/${locationName}/lodging?readMask=*`;
|
|
14002
|
+
try {
|
|
14003
|
+
return await gbpFetchGet(url, accessToken, opts);
|
|
14004
|
+
} catch (err) {
|
|
14005
|
+
if (err instanceof GbpApiError && err.status === 400) {
|
|
14006
|
+
return null;
|
|
14007
|
+
}
|
|
14008
|
+
throw err;
|
|
14009
|
+
}
|
|
14010
|
+
}
|
|
14011
|
+
function countPopulatedGroups(lodging) {
|
|
14012
|
+
let count = 0;
|
|
14013
|
+
for (const [key, value] of Object.entries(lodging)) {
|
|
14014
|
+
if (key === "name" || key === "metadata") continue;
|
|
14015
|
+
if (isPopulated(value)) count++;
|
|
14016
|
+
}
|
|
14017
|
+
return count;
|
|
14018
|
+
}
|
|
14019
|
+
function isPopulated(value) {
|
|
14020
|
+
if (value === null || value === void 0) return false;
|
|
14021
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
14022
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
14023
|
+
return true;
|
|
14024
|
+
}
|
|
14025
|
+
function hashLodging(lodging) {
|
|
14026
|
+
return crypto15.createHash("sha256").update(stableStringify(lodging)).digest("hex");
|
|
14027
|
+
}
|
|
14028
|
+
function stableStringify(value) {
|
|
14029
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
14030
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
14031
|
+
const obj = value;
|
|
14032
|
+
const keys = Object.keys(obj).sort();
|
|
14033
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
14034
|
+
}
|
|
14035
|
+
|
|
13433
14036
|
// ../api-routes/src/google.ts
|
|
14037
|
+
function scopesForConnectionType(type) {
|
|
14038
|
+
switch (type) {
|
|
14039
|
+
case "gsc":
|
|
14040
|
+
return [GSC_SCOPE, INDEXING_SCOPE];
|
|
14041
|
+
case "ga4":
|
|
14042
|
+
return [GA4_SCOPE];
|
|
14043
|
+
case "gbp":
|
|
14044
|
+
return [GBP_SCOPE];
|
|
14045
|
+
}
|
|
14046
|
+
}
|
|
13434
14047
|
function signState(payload, secret) {
|
|
13435
|
-
return
|
|
14048
|
+
return crypto16.createHmac("sha256", secret).update(payload).digest("hex");
|
|
13436
14049
|
}
|
|
13437
14050
|
function buildSignedState(data, secret) {
|
|
13438
14051
|
const payload = JSON.stringify(data);
|
|
@@ -13443,7 +14056,7 @@ function verifySignedState(encoded, secret) {
|
|
|
13443
14056
|
try {
|
|
13444
14057
|
const { payload, sig } = JSON.parse(Buffer.from(encoded, "base64url").toString());
|
|
13445
14058
|
const expected = signState(payload, secret);
|
|
13446
|
-
if (!
|
|
14059
|
+
if (!crypto16.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
|
|
13447
14060
|
return JSON.parse(payload);
|
|
13448
14061
|
} catch {
|
|
13449
14062
|
return null;
|
|
@@ -13524,8 +14137,8 @@ async function googleRoutes(app, opts) {
|
|
|
13524
14137
|
throw validationError("Google OAuth is not configured. Set Google OAuth credentials in the local Canonry config.");
|
|
13525
14138
|
}
|
|
13526
14139
|
const { type, propertyId, publicUrl } = request.body ?? {};
|
|
13527
|
-
if (!type || type !== "gsc" && type !== "ga4") {
|
|
13528
|
-
throw validationError('type must be "gsc" or "
|
|
14140
|
+
if (!type || type !== "gsc" && type !== "ga4" && type !== "gbp") {
|
|
14141
|
+
throw validationError('type must be "gsc", "ga4", or "gbp"');
|
|
13529
14142
|
}
|
|
13530
14143
|
const project = resolveProject(app.db, request.params.name);
|
|
13531
14144
|
let redirectUri;
|
|
@@ -13538,7 +14151,7 @@ async function googleRoutes(app, opts) {
|
|
|
13538
14151
|
const host = request.headers.host ?? "localhost:4100";
|
|
13539
14152
|
redirectUri = `${proto}://${host}${opts.routePrefix ?? "/api/v1"}/projects/${encodeURIComponent(request.params.name)}/google/callback`;
|
|
13540
14153
|
}
|
|
13541
|
-
const scopes = type
|
|
14154
|
+
const scopes = scopesForConnectionType(type);
|
|
13542
14155
|
const stateEncoded = buildSignedState(
|
|
13543
14156
|
{
|
|
13544
14157
|
projectId: project.id,
|
|
@@ -13713,7 +14326,7 @@ async function googleRoutes(app, opts) {
|
|
|
13713
14326
|
throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
|
|
13714
14327
|
}
|
|
13715
14328
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13716
|
-
const runId =
|
|
14329
|
+
const runId = crypto16.randomUUID();
|
|
13717
14330
|
app.db.insert(runs).values({
|
|
13718
14331
|
id: runId,
|
|
13719
14332
|
projectId: project.id,
|
|
@@ -13806,7 +14419,7 @@ async function googleRoutes(app, opts) {
|
|
|
13806
14419
|
const mob = ir.mobileUsabilityResult;
|
|
13807
14420
|
const rich = ir.richResultsResult;
|
|
13808
14421
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13809
|
-
const id =
|
|
14422
|
+
const id = crypto16.randomUUID();
|
|
13810
14423
|
app.db.insert(gscUrlInspections).values({
|
|
13811
14424
|
id,
|
|
13812
14425
|
projectId: project.id,
|
|
@@ -14041,7 +14654,7 @@ async function googleRoutes(app, opts) {
|
|
|
14041
14654
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14042
14655
|
});
|
|
14043
14656
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14044
|
-
const runId =
|
|
14657
|
+
const runId = crypto16.randomUUID();
|
|
14045
14658
|
app.db.insert(runs).values({
|
|
14046
14659
|
id: runId,
|
|
14047
14660
|
projectId: project.id,
|
|
@@ -14067,7 +14680,7 @@ async function googleRoutes(app, opts) {
|
|
|
14067
14680
|
throw validationError("No GSC property configured for this connection");
|
|
14068
14681
|
}
|
|
14069
14682
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14070
|
-
const runId =
|
|
14683
|
+
const runId = crypto16.randomUUID();
|
|
14071
14684
|
app.db.insert(runs).values({
|
|
14072
14685
|
id: runId,
|
|
14073
14686
|
projectId: project.id,
|
|
@@ -14194,10 +14807,385 @@ async function googleRoutes(app, opts) {
|
|
|
14194
14807
|
results
|
|
14195
14808
|
};
|
|
14196
14809
|
});
|
|
14810
|
+
function gbpErrorToAppError(err, context) {
|
|
14811
|
+
if (err.reason === "ACCESS_TOKEN_SCOPE_INSUFFICIENT") {
|
|
14812
|
+
return validationError(
|
|
14813
|
+
`${context}: OAuth token is missing the business.manage scope. Reconnect with "canonry gbp connect".`
|
|
14814
|
+
);
|
|
14815
|
+
}
|
|
14816
|
+
if (err.reason === "RATE_LIMIT_EXCEEDED" || /quota/i.test(err.message)) {
|
|
14817
|
+
if (err.quotaLimitValue === 0) {
|
|
14818
|
+
return quotaExceeded(
|
|
14819
|
+
"Google Business Profile API (0 QPM \u2014 access form pending approval). See https://support.google.com/business/contact/api_default"
|
|
14820
|
+
);
|
|
14821
|
+
}
|
|
14822
|
+
return quotaExceeded(
|
|
14823
|
+
`Google Business Profile API rate limit exceeded${err.quotaLimitValue ? ` (${err.quotaLimitValue} QPM cap)` : ""}. Retries exhausted; try again shortly.`
|
|
14824
|
+
);
|
|
14825
|
+
}
|
|
14826
|
+
if (err.reason === "API_DISABLED" || err.reason === "CONSUMER_INVALID") {
|
|
14827
|
+
return providerError(
|
|
14828
|
+
`${context}: required Business Profile API is not enabled on the configured GCP project.`,
|
|
14829
|
+
{ reason: err.reason, body: err.body }
|
|
14830
|
+
);
|
|
14831
|
+
}
|
|
14832
|
+
if (err.status === 401) return authRequired();
|
|
14833
|
+
return providerError(`${context}: ${err.message}`, { reason: err.reason ?? void 0, status: err.status });
|
|
14834
|
+
}
|
|
14835
|
+
function rowToDto3(row) {
|
|
14836
|
+
return {
|
|
14837
|
+
id: row.id,
|
|
14838
|
+
projectId: row.projectId,
|
|
14839
|
+
accountName: row.accountName,
|
|
14840
|
+
locationName: row.locationName,
|
|
14841
|
+
displayName: row.displayName,
|
|
14842
|
+
primaryCategoryDisplayName: row.primaryCategoryDisplayName ?? null,
|
|
14843
|
+
storefrontAddress: row.storefrontAddress ?? null,
|
|
14844
|
+
websiteUri: row.websiteUri ?? null,
|
|
14845
|
+
selected: Boolean(row.selected),
|
|
14846
|
+
syncedAt: row.syncedAt ?? null,
|
|
14847
|
+
createdAt: row.createdAt,
|
|
14848
|
+
updatedAt: row.updatedAt
|
|
14849
|
+
};
|
|
14850
|
+
}
|
|
14851
|
+
function listSelectionResponse(projectId) {
|
|
14852
|
+
const rows = app.db.select().from(gbpLocations).where(eq19(gbpLocations.projectId, projectId)).all();
|
|
14853
|
+
const dtos = rows.map(rowToDto3);
|
|
14854
|
+
return {
|
|
14855
|
+
locations: dtos,
|
|
14856
|
+
totalDiscovered: dtos.length,
|
|
14857
|
+
totalSelected: dtos.filter((d) => d.selected).length
|
|
14858
|
+
};
|
|
14859
|
+
}
|
|
14860
|
+
function clearGbpProjectData(tx, projectId) {
|
|
14861
|
+
tx.delete(gbpDailyMetrics).where(eq19(gbpDailyMetrics.projectId, projectId)).run();
|
|
14862
|
+
tx.delete(gbpKeywordImpressions).where(eq19(gbpKeywordImpressions.projectId, projectId)).run();
|
|
14863
|
+
tx.delete(gbpPlaceActions).where(eq19(gbpPlaceActions.projectId, projectId)).run();
|
|
14864
|
+
tx.delete(gbpLodgingSnapshots).where(eq19(gbpLodgingSnapshots.projectId, projectId)).run();
|
|
14865
|
+
tx.delete(gbpLocations).where(eq19(gbpLocations.projectId, projectId)).run();
|
|
14866
|
+
}
|
|
14867
|
+
function currentProjectAccount(projectId) {
|
|
14868
|
+
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq19(gbpLocations.projectId, projectId)).limit(1).get();
|
|
14869
|
+
return row?.accountName ?? null;
|
|
14870
|
+
}
|
|
14871
|
+
app.post("/projects/:name/gbp/locations/discover", async (request) => {
|
|
14872
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
14873
|
+
if (!googleClientId || !googleClientSecret) {
|
|
14874
|
+
throw validationError("Google OAuth is not configured");
|
|
14875
|
+
}
|
|
14876
|
+
const project = resolveProject(app.db, request.params.name);
|
|
14877
|
+
const store = requireConnectionStore();
|
|
14878
|
+
const parsed = gbpDiscoverRequestSchema.safeParse(request.body ?? {});
|
|
14879
|
+
if (!parsed.success) {
|
|
14880
|
+
throw validationError(parsed.error.issues[0]?.message ?? "Invalid discover request");
|
|
14881
|
+
}
|
|
14882
|
+
const { selectAllNew, accountName: requestedAccount, switchAccount } = parsed.data;
|
|
14883
|
+
const { accessToken } = await getValidToken(
|
|
14884
|
+
store,
|
|
14885
|
+
project.canonicalDomain,
|
|
14886
|
+
"gbp",
|
|
14887
|
+
googleClientId,
|
|
14888
|
+
googleClientSecret
|
|
14889
|
+
);
|
|
14890
|
+
const fetchAccounts = async () => {
|
|
14891
|
+
try {
|
|
14892
|
+
return await listAccounts(accessToken);
|
|
14893
|
+
} catch (err) {
|
|
14894
|
+
if (err instanceof GbpApiError) throw gbpErrorToAppError(err, "list accounts");
|
|
14895
|
+
throw err;
|
|
14896
|
+
}
|
|
14897
|
+
};
|
|
14898
|
+
const conn = store.getConnection(project.canonicalDomain, "gbp");
|
|
14899
|
+
const current = currentProjectAccount(project.id);
|
|
14900
|
+
let accountName;
|
|
14901
|
+
if (requestedAccount) {
|
|
14902
|
+
const accounts = await fetchAccounts();
|
|
14903
|
+
if (!accounts.some((a) => a.name === requestedAccount)) {
|
|
14904
|
+
throw validationError(`GBP account "${requestedAccount}" is not accessible to this connection. Run "canonry gbp accounts <project>" to list available accounts.`);
|
|
14905
|
+
}
|
|
14906
|
+
accountName = requestedAccount;
|
|
14907
|
+
} else {
|
|
14908
|
+
const remembered = current ?? conn?.gbpAccountName ?? null;
|
|
14909
|
+
if (remembered) {
|
|
14910
|
+
accountName = remembered;
|
|
14911
|
+
} else {
|
|
14912
|
+
const accounts = await fetchAccounts();
|
|
14913
|
+
if (accounts.length === 0) {
|
|
14914
|
+
throw validationError("No GBP accounts are visible to this OAuth user. Confirm the user has manager/owner access on the target Business Profile.");
|
|
14915
|
+
}
|
|
14916
|
+
accountName = accounts[0].name;
|
|
14917
|
+
}
|
|
14918
|
+
}
|
|
14919
|
+
const switching = current !== null && current !== accountName;
|
|
14920
|
+
if (switching && !switchAccount) {
|
|
14921
|
+
throw validationError(`This project currently tracks GBP account "${current}". Re-pointing it at "${accountName}" would replace its locations and all synced data. Pass switchAccount=true (CLI: --switch-account) to confirm, or run "canonry gbp disconnect <project>" first.`);
|
|
14922
|
+
}
|
|
14923
|
+
let remoteLocations;
|
|
14924
|
+
try {
|
|
14925
|
+
remoteLocations = await listLocations(accessToken, accountName);
|
|
14926
|
+
} catch (err) {
|
|
14927
|
+
if (err instanceof GbpApiError) throw gbpErrorToAppError(err, "list locations");
|
|
14928
|
+
throw err;
|
|
14929
|
+
}
|
|
14930
|
+
store.updateConnection(project.canonicalDomain, "gbp", {
|
|
14931
|
+
gbpAccountName: accountName,
|
|
14932
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14933
|
+
});
|
|
14934
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14935
|
+
app.db.transaction((tx) => {
|
|
14936
|
+
if (switching) clearGbpProjectData(tx, project.id);
|
|
14937
|
+
for (const remote of remoteLocations) {
|
|
14938
|
+
const existing = tx.select().from(gbpLocations).where(and13(eq19(gbpLocations.projectId, project.id), eq19(gbpLocations.locationName, remote.name))).get();
|
|
14939
|
+
if (existing) {
|
|
14940
|
+
tx.update(gbpLocations).set({
|
|
14941
|
+
accountName,
|
|
14942
|
+
displayName: remote.title ?? existing.displayName,
|
|
14943
|
+
primaryCategoryDisplayName: remote.categories?.primaryCategory?.displayName ?? null,
|
|
14944
|
+
storefrontAddress: formatStorefrontAddress(remote),
|
|
14945
|
+
websiteUri: remote.websiteUri ?? null,
|
|
14946
|
+
updatedAt: now
|
|
14947
|
+
}).where(eq19(gbpLocations.id, existing.id)).run();
|
|
14948
|
+
} else {
|
|
14949
|
+
tx.insert(gbpLocations).values({
|
|
14950
|
+
id: crypto16.randomUUID(),
|
|
14951
|
+
projectId: project.id,
|
|
14952
|
+
accountName,
|
|
14953
|
+
locationName: remote.name,
|
|
14954
|
+
displayName: remote.title ?? remote.name,
|
|
14955
|
+
primaryCategoryDisplayName: remote.categories?.primaryCategory?.displayName ?? null,
|
|
14956
|
+
storefrontAddress: formatStorefrontAddress(remote),
|
|
14957
|
+
websiteUri: remote.websiteUri ?? null,
|
|
14958
|
+
selected: selectAllNew,
|
|
14959
|
+
createdAt: now,
|
|
14960
|
+
updatedAt: now
|
|
14961
|
+
}).run();
|
|
14962
|
+
}
|
|
14963
|
+
}
|
|
14964
|
+
writeAuditLog(tx, {
|
|
14965
|
+
projectId: project.id,
|
|
14966
|
+
actor: "api",
|
|
14967
|
+
action: switching ? "gbp.account.switched" : "gbp.locations.discovered",
|
|
14968
|
+
entityType: "gbp_locations",
|
|
14969
|
+
diff: { account: accountName, switchedFrom: switching ? current : null, count: remoteLocations.length, selectAllNew }
|
|
14970
|
+
});
|
|
14971
|
+
});
|
|
14972
|
+
return listSelectionResponse(project.id);
|
|
14973
|
+
});
|
|
14974
|
+
app.get("/projects/:name/gbp/accounts", async (request) => {
|
|
14975
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
14976
|
+
if (!googleClientId || !googleClientSecret) {
|
|
14977
|
+
throw validationError("Google OAuth is not configured");
|
|
14978
|
+
}
|
|
14979
|
+
const project = resolveProject(app.db, request.params.name);
|
|
14980
|
+
const store = requireConnectionStore();
|
|
14981
|
+
const conn = store.getConnection(project.canonicalDomain, "gbp");
|
|
14982
|
+
if (!conn) {
|
|
14983
|
+
throw validationError('No GBP connection found for this project. Run "canonry gbp connect" first.');
|
|
14984
|
+
}
|
|
14985
|
+
const { accessToken } = await getValidToken(
|
|
14986
|
+
store,
|
|
14987
|
+
project.canonicalDomain,
|
|
14988
|
+
"gbp",
|
|
14989
|
+
googleClientId,
|
|
14990
|
+
googleClientSecret
|
|
14991
|
+
);
|
|
14992
|
+
let accounts;
|
|
14993
|
+
try {
|
|
14994
|
+
accounts = await listAccounts(accessToken);
|
|
14995
|
+
} catch (err) {
|
|
14996
|
+
if (err instanceof GbpApiError) throw gbpErrorToAppError(err, "list accounts");
|
|
14997
|
+
throw err;
|
|
14998
|
+
}
|
|
14999
|
+
const response = {
|
|
15000
|
+
accounts: accounts.map((a) => ({
|
|
15001
|
+
name: a.name,
|
|
15002
|
+
accountName: a.accountName ?? null,
|
|
15003
|
+
type: a.type ?? null,
|
|
15004
|
+
role: a.role ?? null
|
|
15005
|
+
})),
|
|
15006
|
+
total: accounts.length
|
|
15007
|
+
};
|
|
15008
|
+
return response;
|
|
15009
|
+
});
|
|
15010
|
+
app.get("/projects/:name/gbp/locations", async (request) => {
|
|
15011
|
+
const project = resolveProject(app.db, request.params.name);
|
|
15012
|
+
const response = listSelectionResponse(project.id);
|
|
15013
|
+
const filter = request.query.selected;
|
|
15014
|
+
if (filter === "true" || filter === "false") {
|
|
15015
|
+
const want = filter === "true";
|
|
15016
|
+
response.locations = response.locations.filter((l) => l.selected === want);
|
|
15017
|
+
}
|
|
15018
|
+
return response;
|
|
15019
|
+
});
|
|
15020
|
+
app.put("/projects/:name/gbp/locations/:locationName/selection", async (request) => {
|
|
15021
|
+
const project = resolveProject(app.db, request.params.name);
|
|
15022
|
+
const locationName = decodeURIComponent(request.params.locationName);
|
|
15023
|
+
const parsed = gbpLocationSelectionRequestSchema.safeParse(request.body ?? {});
|
|
15024
|
+
if (!parsed.success) {
|
|
15025
|
+
throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
|
|
15026
|
+
}
|
|
15027
|
+
const { selected } = parsed.data;
|
|
15028
|
+
const existing = app.db.select().from(gbpLocations).where(and13(eq19(gbpLocations.projectId, project.id), eq19(gbpLocations.locationName, locationName))).get();
|
|
15029
|
+
if (!existing) throw notFound("GBP location", locationName);
|
|
15030
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15031
|
+
app.db.transaction((tx) => {
|
|
15032
|
+
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq19(gbpLocations.id, existing.id)).run();
|
|
15033
|
+
writeAuditLog(tx, {
|
|
15034
|
+
projectId: project.id,
|
|
15035
|
+
actor: "api",
|
|
15036
|
+
action: selected ? "gbp.location.selected" : "gbp.location.deselected",
|
|
15037
|
+
entityType: "gbp_location",
|
|
15038
|
+
entityId: locationName
|
|
15039
|
+
});
|
|
15040
|
+
});
|
|
15041
|
+
const refreshed = app.db.select().from(gbpLocations).where(eq19(gbpLocations.id, existing.id)).get();
|
|
15042
|
+
return rowToDto3(refreshed);
|
|
15043
|
+
});
|
|
15044
|
+
app.delete("/projects/:name/gbp/connection", async (request, reply) => {
|
|
15045
|
+
const project = resolveProject(app.db, request.params.name);
|
|
15046
|
+
const store = requireConnectionStore();
|
|
15047
|
+
app.db.transaction((tx) => {
|
|
15048
|
+
clearGbpProjectData(tx, project.id);
|
|
15049
|
+
writeAuditLog(tx, {
|
|
15050
|
+
projectId: project.id,
|
|
15051
|
+
actor: "api",
|
|
15052
|
+
action: "gbp.disconnected",
|
|
15053
|
+
entityType: "gbp_connection"
|
|
15054
|
+
});
|
|
15055
|
+
});
|
|
15056
|
+
store.deleteConnection(project.canonicalDomain, "gbp");
|
|
15057
|
+
return reply.status(204).send();
|
|
15058
|
+
});
|
|
15059
|
+
app.post("/projects/:name/gbp/sync", async (request) => {
|
|
15060
|
+
const project = resolveProject(app.db, request.params.name);
|
|
15061
|
+
const store = requireConnectionStore();
|
|
15062
|
+
const conn = store.getConnection(project.canonicalDomain, "gbp");
|
|
15063
|
+
if (!conn) {
|
|
15064
|
+
throw validationError('No GBP connection found for this project. Run "canonry gbp connect" first.');
|
|
15065
|
+
}
|
|
15066
|
+
const parsed = gbpSyncRequestSchema.safeParse(request.body ?? {});
|
|
15067
|
+
if (!parsed.success) {
|
|
15068
|
+
throw validationError(parsed.error.issues[0]?.message ?? "Invalid sync request");
|
|
15069
|
+
}
|
|
15070
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15071
|
+
const runId = crypto16.randomUUID();
|
|
15072
|
+
app.db.insert(runs).values({
|
|
15073
|
+
id: runId,
|
|
15074
|
+
projectId: project.id,
|
|
15075
|
+
kind: "gbp-sync",
|
|
15076
|
+
status: "queued",
|
|
15077
|
+
trigger: "manual",
|
|
15078
|
+
createdAt: now
|
|
15079
|
+
}).run();
|
|
15080
|
+
opts.onGbpSyncRequested?.(runId, project.id, parsed.data);
|
|
15081
|
+
return { runId, status: "running" };
|
|
15082
|
+
});
|
|
15083
|
+
app.get("/projects/:name/gbp/metrics", async (request) => {
|
|
15084
|
+
const project = resolveProject(app.db, request.params.name);
|
|
15085
|
+
const conditions = [eq19(gbpDailyMetrics.projectId, project.id)];
|
|
15086
|
+
if (request.query.locationName) conditions.push(eq19(gbpDailyMetrics.locationName, request.query.locationName));
|
|
15087
|
+
if (request.query.metric) conditions.push(eq19(gbpDailyMetrics.metric, request.query.metric));
|
|
15088
|
+
const rows = app.db.select().from(gbpDailyMetrics).where(and13(...conditions)).orderBy(desc9(gbpDailyMetrics.date)).all();
|
|
15089
|
+
return {
|
|
15090
|
+
metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
|
|
15091
|
+
total: rows.length
|
|
15092
|
+
};
|
|
15093
|
+
});
|
|
15094
|
+
app.get("/projects/:name/gbp/keywords", async (request) => {
|
|
15095
|
+
const project = resolveProject(app.db, request.params.name);
|
|
15096
|
+
const conditions = [eq19(gbpKeywordImpressions.projectId, project.id)];
|
|
15097
|
+
if (request.query.locationName) conditions.push(eq19(gbpKeywordImpressions.locationName, request.query.locationName));
|
|
15098
|
+
const rows = app.db.select().from(gbpKeywordImpressions).where(and13(...conditions)).all();
|
|
15099
|
+
rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
|
|
15100
|
+
const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
|
|
15101
|
+
return {
|
|
15102
|
+
keywords: rows.map((r) => ({
|
|
15103
|
+
locationName: r.locationName,
|
|
15104
|
+
periodStart: r.periodStart,
|
|
15105
|
+
periodEnd: r.periodEnd,
|
|
15106
|
+
keyword: r.keyword,
|
|
15107
|
+
valueCount: r.valueCount ?? null,
|
|
15108
|
+
valueThreshold: r.valueThreshold ?? null
|
|
15109
|
+
})),
|
|
15110
|
+
total: rows.length,
|
|
15111
|
+
thresholdedPct: rows.length ? Math.round(thresholded / rows.length * 100) : 0
|
|
15112
|
+
};
|
|
15113
|
+
});
|
|
15114
|
+
app.get("/projects/:name/gbp/place-actions", async (request) => {
|
|
15115
|
+
const project = resolveProject(app.db, request.params.name);
|
|
15116
|
+
const conditions = [eq19(gbpPlaceActions.projectId, project.id)];
|
|
15117
|
+
if (request.query.locationName) conditions.push(eq19(gbpPlaceActions.locationName, request.query.locationName));
|
|
15118
|
+
const rows = app.db.select().from(gbpPlaceActions).where(and13(...conditions)).all();
|
|
15119
|
+
return {
|
|
15120
|
+
placeActions: rows.map((r) => ({
|
|
15121
|
+
locationName: r.locationName,
|
|
15122
|
+
placeActionLinkName: r.placeActionLinkName,
|
|
15123
|
+
placeActionType: r.placeActionType,
|
|
15124
|
+
uri: r.uri ?? null,
|
|
15125
|
+
isPreferred: Boolean(r.isPreferred),
|
|
15126
|
+
providerType: r.providerType ?? null
|
|
15127
|
+
})),
|
|
15128
|
+
total: rows.length
|
|
15129
|
+
};
|
|
15130
|
+
});
|
|
15131
|
+
app.get("/projects/:name/gbp/lodging", async (request) => {
|
|
15132
|
+
const project = resolveProject(app.db, request.params.name);
|
|
15133
|
+
const conditions = [eq19(gbpLodgingSnapshots.projectId, project.id)];
|
|
15134
|
+
if (request.query.locationName) conditions.push(eq19(gbpLodgingSnapshots.locationName, request.query.locationName));
|
|
15135
|
+
const rows = app.db.select().from(gbpLodgingSnapshots).where(and13(...conditions)).orderBy(desc9(gbpLodgingSnapshots.syncedAt)).all();
|
|
15136
|
+
const latestByLocation = /* @__PURE__ */ new Map();
|
|
15137
|
+
for (const row of rows) {
|
|
15138
|
+
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
15139
|
+
}
|
|
15140
|
+
const lodging = [...latestByLocation.values()].map((r) => ({
|
|
15141
|
+
locationName: r.locationName,
|
|
15142
|
+
populatedGroupCount: r.populatedGroupCount,
|
|
15143
|
+
syncedAt: r.syncedAt,
|
|
15144
|
+
attributes: r.attributes
|
|
15145
|
+
}));
|
|
15146
|
+
return { lodging, total: lodging.length };
|
|
15147
|
+
});
|
|
15148
|
+
app.get("/projects/:name/gbp/summary", async (request) => {
|
|
15149
|
+
const project = resolveProject(app.db, request.params.name);
|
|
15150
|
+
const locationName = request.query.locationName ?? null;
|
|
15151
|
+
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and13(eq19(gbpLocations.projectId, project.id), eq19(gbpLocations.selected, true))).all().map((r) => r.n);
|
|
15152
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
15153
|
+
if (locationNames.length === 0) {
|
|
15154
|
+
return buildGbpSummary({
|
|
15155
|
+
locationName,
|
|
15156
|
+
locationCount: 0,
|
|
15157
|
+
referenceDate: today,
|
|
15158
|
+
dailyMetrics: [],
|
|
15159
|
+
keywords: [],
|
|
15160
|
+
placeActions: [],
|
|
15161
|
+
lodging: []
|
|
15162
|
+
});
|
|
15163
|
+
}
|
|
15164
|
+
const metricRows = app.db.select().from(gbpDailyMetrics).where(and13(eq19(gbpDailyMetrics.projectId, project.id), inArray9(gbpDailyMetrics.locationName, locationNames))).all();
|
|
15165
|
+
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and13(eq19(gbpKeywordImpressions.projectId, project.id), inArray9(gbpKeywordImpressions.locationName, locationNames))).all();
|
|
15166
|
+
const placeActionRows = app.db.select().from(gbpPlaceActions).where(and13(eq19(gbpPlaceActions.projectId, project.id), inArray9(gbpPlaceActions.locationName, locationNames))).all();
|
|
15167
|
+
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and13(eq19(gbpLodgingSnapshots.projectId, project.id), inArray9(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc9(gbpLodgingSnapshots.syncedAt)).all();
|
|
15168
|
+
const latestLodgingByLocation = /* @__PURE__ */ new Map();
|
|
15169
|
+
for (const row of lodgingRows) {
|
|
15170
|
+
if (!latestLodgingByLocation.has(row.locationName)) {
|
|
15171
|
+
latestLodgingByLocation.set(row.locationName, { locationName: row.locationName, populatedGroupCount: row.populatedGroupCount });
|
|
15172
|
+
}
|
|
15173
|
+
}
|
|
15174
|
+
const referenceDate = metricRows.reduce((max, r) => r.date > max ? r.date : max, "") || today;
|
|
15175
|
+
return buildGbpSummary({
|
|
15176
|
+
locationName,
|
|
15177
|
+
locationCount: locationNames.length,
|
|
15178
|
+
referenceDate,
|
|
15179
|
+
dailyMetrics: metricRows.map((r) => ({ metric: r.metric, date: r.date, value: r.value })),
|
|
15180
|
+
keywords: keywordRows.map((r) => ({ valueCount: r.valueCount ?? null, valueThreshold: r.valueThreshold ?? null })),
|
|
15181
|
+
placeActions: placeActionRows.map((r) => ({ placeActionType: r.placeActionType, providerType: r.providerType ?? null })),
|
|
15182
|
+
lodging: [...latestLodgingByLocation.values()]
|
|
15183
|
+
});
|
|
15184
|
+
});
|
|
14197
15185
|
}
|
|
14198
15186
|
|
|
14199
15187
|
// ../api-routes/src/bing.ts
|
|
14200
|
-
import
|
|
15188
|
+
import crypto17 from "crypto";
|
|
14201
15189
|
import { eq as eq20, and as and14, desc as desc10 } from "drizzle-orm";
|
|
14202
15190
|
|
|
14203
15191
|
// ../integration-bing/src/constants.ts
|
|
@@ -14583,7 +15571,7 @@ async function bingRoutes(app, opts) {
|
|
|
14583
15571
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
14584
15572
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14585
15573
|
app.db.insert(bingCoverageSnapshots).values({
|
|
14586
|
-
id:
|
|
15574
|
+
id: crypto17.randomUUID(),
|
|
14587
15575
|
projectId: project.id,
|
|
14588
15576
|
syncRunId: snapshotRunId,
|
|
14589
15577
|
date: snapshotDate,
|
|
@@ -14654,7 +15642,7 @@ async function bingRoutes(app, opts) {
|
|
|
14654
15642
|
throw validationError("url is required");
|
|
14655
15643
|
}
|
|
14656
15644
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14657
|
-
const runId =
|
|
15645
|
+
const runId = crypto17.randomUUID();
|
|
14658
15646
|
app.db.insert(runs).values({
|
|
14659
15647
|
id: runId,
|
|
14660
15648
|
projectId: project.id,
|
|
@@ -14675,7 +15663,7 @@ async function bingRoutes(app, opts) {
|
|
|
14675
15663
|
discoveryDate: result.DiscoveryDate ?? null
|
|
14676
15664
|
});
|
|
14677
15665
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14678
|
-
const id =
|
|
15666
|
+
const id = crypto17.randomUUID();
|
|
14679
15667
|
const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
|
|
14680
15668
|
const lastCrawledDate = parseBingDate(result.LastCrawledDate);
|
|
14681
15669
|
const inIndexDate = parseBingDate(result.InIndexDate);
|
|
@@ -14745,7 +15733,7 @@ async function bingRoutes(app, opts) {
|
|
|
14745
15733
|
throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
|
|
14746
15734
|
}
|
|
14747
15735
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14748
|
-
const runId =
|
|
15736
|
+
const runId = crypto17.randomUUID();
|
|
14749
15737
|
app.db.insert(runs).values({
|
|
14750
15738
|
id: runId,
|
|
14751
15739
|
projectId: project.id,
|
|
@@ -15029,7 +16017,7 @@ async function cdpRoutes(app, opts) {
|
|
|
15029
16017
|
}
|
|
15030
16018
|
|
|
15031
16019
|
// ../api-routes/src/ga.ts
|
|
15032
|
-
import
|
|
16020
|
+
import crypto18 from "crypto";
|
|
15033
16021
|
import { eq as eq22, desc as desc11, and as and16, sql as sql8 } from "drizzle-orm";
|
|
15034
16022
|
function gaLog(level, action, ctx) {
|
|
15035
16023
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
@@ -15285,7 +16273,7 @@ async function ga4Routes(app, opts) {
|
|
|
15285
16273
|
const syncAi = !only || only === "ai";
|
|
15286
16274
|
const syncSocial = !only || only === "social";
|
|
15287
16275
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15288
|
-
const runId =
|
|
16276
|
+
const runId = crypto18.randomUUID();
|
|
15289
16277
|
app.db.insert(runs).values({
|
|
15290
16278
|
id: runId,
|
|
15291
16279
|
projectId: project.id,
|
|
@@ -15333,7 +16321,7 @@ async function ga4Routes(app, opts) {
|
|
|
15333
16321
|
).run();
|
|
15334
16322
|
for (const row of rows) {
|
|
15335
16323
|
tx.insert(gaTrafficSnapshots).values({
|
|
15336
|
-
id:
|
|
16324
|
+
id: crypto18.randomUUID(),
|
|
15337
16325
|
projectId: project.id,
|
|
15338
16326
|
date: row.date,
|
|
15339
16327
|
landingPage: row.landingPage,
|
|
@@ -15357,7 +16345,7 @@ async function ga4Routes(app, opts) {
|
|
|
15357
16345
|
).run();
|
|
15358
16346
|
for (const row of aiReferrals) {
|
|
15359
16347
|
tx.insert(gaAiReferrals).values({
|
|
15360
|
-
id:
|
|
16348
|
+
id: crypto18.randomUUID(),
|
|
15361
16349
|
projectId: project.id,
|
|
15362
16350
|
date: row.date,
|
|
15363
16351
|
source: row.source,
|
|
@@ -15383,7 +16371,7 @@ async function ga4Routes(app, opts) {
|
|
|
15383
16371
|
).run();
|
|
15384
16372
|
for (const row of socialReferrals) {
|
|
15385
16373
|
tx.insert(gaSocialReferrals).values({
|
|
15386
|
-
id:
|
|
16374
|
+
id: crypto18.randomUUID(),
|
|
15387
16375
|
projectId: project.id,
|
|
15388
16376
|
date: row.date,
|
|
15389
16377
|
source: row.source,
|
|
@@ -15399,7 +16387,7 @@ async function ga4Routes(app, opts) {
|
|
|
15399
16387
|
if (syncSummary) {
|
|
15400
16388
|
tx.delete(gaTrafficSummaries).where(eq22(gaTrafficSummaries.projectId, project.id)).run();
|
|
15401
16389
|
tx.insert(gaTrafficSummaries).values({
|
|
15402
|
-
id:
|
|
16390
|
+
id: crypto18.randomUUID(),
|
|
15403
16391
|
projectId: project.id,
|
|
15404
16392
|
periodStart: summary.periodStart,
|
|
15405
16393
|
periodEnd: summary.periodEnd,
|
|
@@ -15412,7 +16400,7 @@ async function ga4Routes(app, opts) {
|
|
|
15412
16400
|
tx.delete(gaTrafficWindowSummaries).where(eq22(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
15413
16401
|
for (const ws of windowSummaries) {
|
|
15414
16402
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
15415
|
-
id:
|
|
16403
|
+
id: crypto18.randomUUID(),
|
|
15416
16404
|
projectId: project.id,
|
|
15417
16405
|
windowKey: ws.windowKey,
|
|
15418
16406
|
periodStart: ws.periodStart,
|
|
@@ -15698,7 +16686,7 @@ async function ga4Routes(app, opts) {
|
|
|
15698
16686
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
15699
16687
|
const today = /* @__PURE__ */ new Date();
|
|
15700
16688
|
const fmt = (d) => d.toISOString().split("T")[0];
|
|
15701
|
-
const
|
|
16689
|
+
const daysAgo3 = (n) => {
|
|
15702
16690
|
const d = new Date(today);
|
|
15703
16691
|
d.setDate(d.getDate() - n);
|
|
15704
16692
|
return fmt(d);
|
|
@@ -15708,17 +16696,17 @@ async function ga4Routes(app, opts) {
|
|
|
15708
16696
|
sql8`${gaSocialReferrals.date} >= ${from}`,
|
|
15709
16697
|
sql8`${gaSocialReferrals.date} < ${to}`
|
|
15710
16698
|
)).get();
|
|
15711
|
-
const current7d = sumSocial(
|
|
15712
|
-
const prev7d = sumSocial(
|
|
15713
|
-
const current30d = sumSocial(
|
|
15714
|
-
const prev30d = sumSocial(
|
|
16699
|
+
const current7d = sumSocial(daysAgo3(7), fmt(today));
|
|
16700
|
+
const prev7d = sumSocial(daysAgo3(14), daysAgo3(7));
|
|
16701
|
+
const current30d = sumSocial(daysAgo3(30), fmt(today));
|
|
16702
|
+
const prev30d = sumSocial(daysAgo3(60), daysAgo3(30));
|
|
15715
16703
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
15716
16704
|
const sourceCurrent = app.db.select({
|
|
15717
16705
|
source: gaSocialReferrals.source,
|
|
15718
16706
|
sessions: sql8`SUM(${gaSocialReferrals.sessions})`
|
|
15719
16707
|
}).from(gaSocialReferrals).where(and16(
|
|
15720
16708
|
eq22(gaSocialReferrals.projectId, project.id),
|
|
15721
|
-
sql8`${gaSocialReferrals.date} >= ${
|
|
16709
|
+
sql8`${gaSocialReferrals.date} >= ${daysAgo3(7)}`,
|
|
15722
16710
|
sql8`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
15723
16711
|
)).groupBy(gaSocialReferrals.source).all();
|
|
15724
16712
|
const sourcePrev = app.db.select({
|
|
@@ -15726,8 +16714,8 @@ async function ga4Routes(app, opts) {
|
|
|
15726
16714
|
sessions: sql8`SUM(${gaSocialReferrals.sessions})`
|
|
15727
16715
|
}).from(gaSocialReferrals).where(and16(
|
|
15728
16716
|
eq22(gaSocialReferrals.projectId, project.id),
|
|
15729
|
-
sql8`${gaSocialReferrals.date} >= ${
|
|
15730
|
-
sql8`${gaSocialReferrals.date} < ${
|
|
16717
|
+
sql8`${gaSocialReferrals.date} >= ${daysAgo3(14)}`,
|
|
16718
|
+
sql8`${gaSocialReferrals.date} < ${daysAgo3(7)}`
|
|
15731
16719
|
)).groupBy(gaSocialReferrals.source).all();
|
|
15732
16720
|
const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
|
|
15733
16721
|
let biggestMover = null;
|
|
@@ -15760,7 +16748,7 @@ async function ga4Routes(app, opts) {
|
|
|
15760
16748
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
15761
16749
|
const today = /* @__PURE__ */ new Date();
|
|
15762
16750
|
const fmt = (d) => d.toISOString().split("T")[0];
|
|
15763
|
-
const
|
|
16751
|
+
const daysAgo3 = (n) => {
|
|
15764
16752
|
const d = new Date(today);
|
|
15765
16753
|
d.setDate(d.getDate() - n);
|
|
15766
16754
|
return fmt(d);
|
|
@@ -15778,22 +16766,22 @@ async function ga4Routes(app, opts) {
|
|
|
15778
16766
|
const sumSocial = (from, to) => app.db.select({ sessions: sql8`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and16(eq22(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${from}`, sql8`${gaSocialReferrals.date} < ${to}`)).get();
|
|
15779
16767
|
const todayStr = fmt(today);
|
|
15780
16768
|
const buildTrend = (sum) => {
|
|
15781
|
-
const c7 = sum(
|
|
15782
|
-
const p7 = sum(
|
|
15783
|
-
const c30 = sum(
|
|
15784
|
-
const p30 = sum(
|
|
16769
|
+
const c7 = sum(daysAgo3(7), todayStr)?.sessions ?? 0;
|
|
16770
|
+
const p7 = sum(daysAgo3(14), daysAgo3(7))?.sessions ?? 0;
|
|
16771
|
+
const c30 = sum(daysAgo3(30), todayStr)?.sessions ?? 0;
|
|
16772
|
+
const p30 = sum(daysAgo3(60), daysAgo3(30))?.sessions ?? 0;
|
|
15785
16773
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
15786
16774
|
};
|
|
15787
16775
|
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql8`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and16(
|
|
15788
16776
|
eq22(gaAiReferrals.projectId, project.id),
|
|
15789
|
-
sql8`${gaAiReferrals.date} >= ${
|
|
16777
|
+
sql8`${gaAiReferrals.date} >= ${daysAgo3(7)}`,
|
|
15790
16778
|
sql8`${gaAiReferrals.date} < ${todayStr}`,
|
|
15791
16779
|
eq22(gaAiReferrals.sourceDimension, "session")
|
|
15792
16780
|
)).groupBy(gaAiReferrals.source).all();
|
|
15793
16781
|
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql8`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and16(
|
|
15794
16782
|
eq22(gaAiReferrals.projectId, project.id),
|
|
15795
|
-
sql8`${gaAiReferrals.date} >= ${
|
|
15796
|
-
sql8`${gaAiReferrals.date} < ${
|
|
16783
|
+
sql8`${gaAiReferrals.date} >= ${daysAgo3(14)}`,
|
|
16784
|
+
sql8`${gaAiReferrals.date} < ${daysAgo3(7)}`,
|
|
15797
16785
|
eq22(gaAiReferrals.sourceDimension, "session")
|
|
15798
16786
|
)).groupBy(gaAiReferrals.source).all();
|
|
15799
16787
|
const findBiggestMover = (current, prev) => {
|
|
@@ -15810,8 +16798,8 @@ async function ga4Routes(app, opts) {
|
|
|
15810
16798
|
}
|
|
15811
16799
|
return mover;
|
|
15812
16800
|
};
|
|
15813
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql8`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and16(eq22(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${
|
|
15814
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql8`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and16(eq22(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${
|
|
16801
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql8`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and16(eq22(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${daysAgo3(7)}`, sql8`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
16802
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql8`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and16(eq22(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${daysAgo3(14)}`, sql8`${gaSocialReferrals.date} < ${daysAgo3(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
15815
16803
|
return {
|
|
15816
16804
|
total: buildTrend(sumTotal),
|
|
15817
16805
|
organic: buildTrend(sumOrganic),
|
|
@@ -15983,7 +16971,7 @@ function parseSchemaPageEntry(entry) {
|
|
|
15983
16971
|
}
|
|
15984
16972
|
|
|
15985
16973
|
// ../integration-wordpress/src/wordpress-client.ts
|
|
15986
|
-
import
|
|
16974
|
+
import crypto19 from "crypto";
|
|
15987
16975
|
function validateUsername(username) {
|
|
15988
16976
|
if (!username || typeof username !== "string" || username.trim().length === 0) {
|
|
15989
16977
|
throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
|
|
@@ -16065,10 +17053,10 @@ function buildAuthErrorMessage(res, responseText) {
|
|
|
16065
17053
|
}
|
|
16066
17054
|
return "WordPress credentials are invalid or lack permission for this action";
|
|
16067
17055
|
}
|
|
16068
|
-
async function fetchJson(connection, siteUrl,
|
|
17056
|
+
async function fetchJson(connection, siteUrl, path17, init) {
|
|
16069
17057
|
if (siteUrl.startsWith("http:")) {
|
|
16070
17058
|
}
|
|
16071
|
-
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${
|
|
17059
|
+
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path17}`, {
|
|
16072
17060
|
...init,
|
|
16073
17061
|
headers: {
|
|
16074
17062
|
"Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
|
|
@@ -16196,7 +17184,7 @@ function buildSnippet(content) {
|
|
|
16196
17184
|
return `${text.slice(0, 157)}...`;
|
|
16197
17185
|
}
|
|
16198
17186
|
function contentHash(content) {
|
|
16199
|
-
return
|
|
17187
|
+
return crypto19.createHash("sha256").update(content).digest("hex");
|
|
16200
17188
|
}
|
|
16201
17189
|
function buildAmbiguousSlugMessage(slug, pages) {
|
|
16202
17190
|
const candidates = pages.map((page) => {
|
|
@@ -17485,7 +18473,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
17485
18473
|
}
|
|
17486
18474
|
|
|
17487
18475
|
// ../api-routes/src/backlinks.ts
|
|
17488
|
-
import
|
|
18476
|
+
import crypto20 from "crypto";
|
|
17489
18477
|
import { and as and18, asc as asc2, desc as desc12, eq as eq23, sql as sql9 } from "drizzle-orm";
|
|
17490
18478
|
|
|
17491
18479
|
// ../integration-commoncrawl/src/constants.ts
|
|
@@ -18065,7 +19053,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
18065
19053
|
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq23(ccReleaseSyncs.id, existing.id)).get();
|
|
18066
19054
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
18067
19055
|
}
|
|
18068
|
-
const id =
|
|
19056
|
+
const id = crypto20.randomUUID();
|
|
18069
19057
|
app.db.insert(ccReleaseSyncs).values({
|
|
18070
19058
|
id,
|
|
18071
19059
|
release,
|
|
@@ -18122,7 +19110,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
18122
19110
|
throw validationError("Invalid release id");
|
|
18123
19111
|
}
|
|
18124
19112
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18125
|
-
const runId =
|
|
19113
|
+
const runId = crypto20.randomUUID();
|
|
18126
19114
|
app.db.insert(runs).values({
|
|
18127
19115
|
id: runId,
|
|
18128
19116
|
projectId: project.id,
|
|
@@ -18195,12 +19183,12 @@ async function backlinksRoutes(app, opts) {
|
|
|
18195
19183
|
}
|
|
18196
19184
|
|
|
18197
19185
|
// ../api-routes/src/traffic.ts
|
|
18198
|
-
import
|
|
19186
|
+
import crypto22 from "crypto";
|
|
18199
19187
|
import { Agent as UndiciAgent } from "undici";
|
|
18200
19188
|
import { and as and19, desc as desc13, eq as eq24, gte as gte3, lte as lte2, sql as sql10 } from "drizzle-orm";
|
|
18201
19189
|
|
|
18202
19190
|
// ../integration-cloud-run/src/auth.ts
|
|
18203
|
-
import
|
|
19191
|
+
import crypto21 from "crypto";
|
|
18204
19192
|
var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
|
|
18205
19193
|
var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
|
|
18206
19194
|
var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -18229,7 +19217,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
|
|
|
18229
19217
|
const headerB64 = encode(header);
|
|
18230
19218
|
const payloadB64 = encode(payload);
|
|
18231
19219
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
18232
|
-
const sign =
|
|
19220
|
+
const sign = crypto21.createSign("RSA-SHA256");
|
|
18233
19221
|
sign.update(signingInput);
|
|
18234
19222
|
const signature = sign.sign(privateKey, "base64url");
|
|
18235
19223
|
return `${signingInput}.${signature}`;
|
|
@@ -21103,8 +22091,8 @@ var ASSET_PATH_PREFIXES = [
|
|
|
21103
22091
|
"/img/",
|
|
21104
22092
|
"/static/"
|
|
21105
22093
|
];
|
|
21106
|
-
function normalizeTrafficPathPattern(
|
|
21107
|
-
const cleanPath =
|
|
22094
|
+
function normalizeTrafficPathPattern(path17) {
|
|
22095
|
+
const cleanPath = path17.trim() || "/";
|
|
21108
22096
|
const pathOnly = cleanPath.split("?")[0] || "/";
|
|
21109
22097
|
const segments = pathOnly.split("/").map((segment) => {
|
|
21110
22098
|
if (!segment) return segment;
|
|
@@ -21153,8 +22141,8 @@ function resolveAiReferralLandingPath(event, evidenceType) {
|
|
|
21153
22141
|
}
|
|
21154
22142
|
return normalizeTrafficPathPattern(event.path);
|
|
21155
22143
|
}
|
|
21156
|
-
function isLikelySubresourcePath(
|
|
21157
|
-
const cleanPath =
|
|
22144
|
+
function isLikelySubresourcePath(path17) {
|
|
22145
|
+
const cleanPath = path17.split("?")[0] || "/";
|
|
21158
22146
|
return ASSET_PATH_PREFIXES.some((prefix) => cleanPath.startsWith(prefix)) || ASSET_EXTENSION_PATTERN.test(cleanPath);
|
|
21159
22147
|
}
|
|
21160
22148
|
function actorKey(event) {
|
|
@@ -21387,11 +22375,11 @@ function buildEventId2(event) {
|
|
|
21387
22375
|
function normalizeWordpressTrafficEvent(event) {
|
|
21388
22376
|
if (!event.observed_at) return null;
|
|
21389
22377
|
if (typeof event.id !== "number" || !Number.isFinite(event.id)) return null;
|
|
21390
|
-
const
|
|
21391
|
-
if (!
|
|
22378
|
+
const path17 = event.path?.trim();
|
|
22379
|
+
if (!path17) return null;
|
|
21392
22380
|
const queryString = trimOrNull(event.query_string);
|
|
21393
22381
|
const host = trimOrNull(event.host);
|
|
21394
|
-
const requestUrl = host ? `https://${host}${
|
|
22382
|
+
const requestUrl = host ? `https://${host}${path17}${queryString ? `?${queryString}` : ""}` : `${path17}${queryString ? `?${queryString}` : ""}`;
|
|
21395
22383
|
return {
|
|
21396
22384
|
sourceType: TrafficSourceTypes.wordpress,
|
|
21397
22385
|
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
@@ -21401,7 +22389,7 @@ function normalizeWordpressTrafficEvent(event) {
|
|
|
21401
22389
|
method: trimOrNull(event.method),
|
|
21402
22390
|
requestUrl,
|
|
21403
22391
|
host,
|
|
21404
|
-
path:
|
|
22392
|
+
path: path17,
|
|
21405
22393
|
queryString,
|
|
21406
22394
|
status: typeof event.status === "number" && Number.isFinite(event.status) ? event.status : null,
|
|
21407
22395
|
userAgent: trimOrNull(event.user_agent),
|
|
@@ -21572,15 +22560,15 @@ function stringLabels(input) {
|
|
|
21572
22560
|
);
|
|
21573
22561
|
}
|
|
21574
22562
|
function normalizeVercelLogRow(row) {
|
|
21575
|
-
const
|
|
21576
|
-
if (!
|
|
22563
|
+
const path17 = row.requestPath;
|
|
22564
|
+
if (!path17) return null;
|
|
21577
22565
|
const observedAt = row.timestamp;
|
|
21578
22566
|
if (!observedAt) return null;
|
|
21579
22567
|
const requestId = row.requestId;
|
|
21580
22568
|
if (!requestId) return null;
|
|
21581
22569
|
const host = emptyToNull(row.domain);
|
|
21582
22570
|
const queryString = serializeSearchParams(row.requestSearchParams);
|
|
21583
|
-
const requestUrl = host ? `https://${host}${
|
|
22571
|
+
const requestUrl = host ? `https://${host}${path17}${queryString ? `?${queryString}` : ""}` : null;
|
|
21584
22572
|
return {
|
|
21585
22573
|
sourceType: TrafficSourceTypes.vercel,
|
|
21586
22574
|
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
@@ -21590,7 +22578,7 @@ function normalizeVercelLogRow(row) {
|
|
|
21590
22578
|
method: row.requestMethod ?? null,
|
|
21591
22579
|
requestUrl,
|
|
21592
22580
|
host,
|
|
21593
|
-
path:
|
|
22581
|
+
path: path17,
|
|
21594
22582
|
queryString,
|
|
21595
22583
|
status: resolveStatus(row),
|
|
21596
22584
|
userAgent: emptyToNull(row.clientUserAgent),
|
|
@@ -22095,7 +23083,7 @@ async function runBackfillTask(options) {
|
|
|
22095
23083
|
}
|
|
22096
23084
|
})();
|
|
22097
23085
|
tx.insert(rawEventSamples).values({
|
|
22098
|
-
id:
|
|
23086
|
+
id: crypto22.randomUUID(),
|
|
22099
23087
|
projectId: project.id,
|
|
22100
23088
|
sourceId: sourceRow.id,
|
|
22101
23089
|
ts: sample.observedAt,
|
|
@@ -22220,7 +23208,7 @@ async function trafficRoutes(app, opts) {
|
|
|
22220
23208
|
}).where(eq24(trafficSources.id, activeSource.id)).run();
|
|
22221
23209
|
sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
|
|
22222
23210
|
} else {
|
|
22223
|
-
const newId =
|
|
23211
|
+
const newId = crypto22.randomUUID();
|
|
22224
23212
|
app.db.insert(trafficSources).values({
|
|
22225
23213
|
id: newId,
|
|
22226
23214
|
projectId: project.id,
|
|
@@ -22301,7 +23289,7 @@ async function trafficRoutes(app, opts) {
|
|
|
22301
23289
|
}).where(eq24(trafficSources.id, activeSource.id)).run();
|
|
22302
23290
|
sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
|
|
22303
23291
|
} else {
|
|
22304
|
-
const newId =
|
|
23292
|
+
const newId = crypto22.randomUUID();
|
|
22305
23293
|
app.db.insert(trafficSources).values({
|
|
22306
23294
|
id: newId,
|
|
22307
23295
|
projectId: project.id,
|
|
@@ -22384,7 +23372,7 @@ async function trafficRoutes(app, opts) {
|
|
|
22384
23372
|
}).where(eq24(trafficSources.id, activeSource.id)).run();
|
|
22385
23373
|
sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
|
|
22386
23374
|
} else {
|
|
22387
|
-
const newId =
|
|
23375
|
+
const newId = crypto22.randomUUID();
|
|
22388
23376
|
app.db.insert(trafficSources).values({
|
|
22389
23377
|
id: newId,
|
|
22390
23378
|
projectId: project.id,
|
|
@@ -22432,7 +23420,7 @@ async function trafficRoutes(app, opts) {
|
|
|
22432
23420
|
const windowEnd = /* @__PURE__ */ new Date();
|
|
22433
23421
|
const startedAt = windowEnd.toISOString();
|
|
22434
23422
|
const syncStartedAtMs = windowEnd.getTime();
|
|
22435
|
-
const runId =
|
|
23423
|
+
const runId = crypto22.randomUUID();
|
|
22436
23424
|
app.db.insert(runs).values({
|
|
22437
23425
|
id: runId,
|
|
22438
23426
|
projectId: project.id,
|
|
@@ -22761,7 +23749,7 @@ async function trafficRoutes(app, opts) {
|
|
|
22761
23749
|
}
|
|
22762
23750
|
})();
|
|
22763
23751
|
tx.insert(rawEventSamples).values({
|
|
22764
|
-
id:
|
|
23752
|
+
id: crypto22.randomUUID(),
|
|
22765
23753
|
projectId: project.id,
|
|
22766
23754
|
sourceId: sourceRow.id,
|
|
22767
23755
|
ts: sample.observedAt,
|
|
@@ -22986,7 +23974,7 @@ async function trafficRoutes(app, opts) {
|
|
|
22986
23974
|
};
|
|
22987
23975
|
}
|
|
22988
23976
|
const startedAt = windowEnd.toISOString();
|
|
22989
|
-
const runId =
|
|
23977
|
+
const runId = crypto22.randomUUID();
|
|
22990
23978
|
app.db.insert(runs).values({
|
|
22991
23979
|
id: runId,
|
|
22992
23980
|
projectId: project.id,
|
|
@@ -23266,6 +24254,7 @@ async function trafficRoutes(app, opts) {
|
|
|
23266
24254
|
}
|
|
23267
24255
|
|
|
23268
24256
|
// ../api-routes/src/doctor/checks/agent.ts
|
|
24257
|
+
import crypto23 from "crypto";
|
|
23269
24258
|
import fs6 from "fs";
|
|
23270
24259
|
import path7 from "path";
|
|
23271
24260
|
var REQUIRED_SKILLS = ["canonry", "aero"];
|
|
@@ -23324,6 +24313,90 @@ var skillsInstalledCheck = {
|
|
|
23324
24313
|
};
|
|
23325
24314
|
}
|
|
23326
24315
|
};
|
|
24316
|
+
var skillsCurrentCheck = {
|
|
24317
|
+
id: "agent.skills.current",
|
|
24318
|
+
category: CheckCategories.agent,
|
|
24319
|
+
scope: CheckScopes.global,
|
|
24320
|
+
title: "Agent skills up to date (~/.claude/skills/)",
|
|
24321
|
+
run: (ctx) => {
|
|
24322
|
+
const bundled = ctx.bundledSkills;
|
|
24323
|
+
if (!bundled || bundled.length === 0) {
|
|
24324
|
+
return {
|
|
24325
|
+
status: CheckStatuses.skipped,
|
|
24326
|
+
code: "agent.skills.bundle-unavailable",
|
|
24327
|
+
summary: "Bundled skill snapshot is not available in this deployment \u2014 cannot assess skill drift.",
|
|
24328
|
+
remediation: null
|
|
24329
|
+
};
|
|
24330
|
+
}
|
|
24331
|
+
const home = process.env.HOME;
|
|
24332
|
+
if (!home) {
|
|
24333
|
+
return {
|
|
24334
|
+
status: CheckStatuses.skipped,
|
|
24335
|
+
code: "agent.skills.no-home",
|
|
24336
|
+
summary: "Cannot determine $HOME \u2014 skip skills staleness check.",
|
|
24337
|
+
remediation: null
|
|
24338
|
+
};
|
|
24339
|
+
}
|
|
24340
|
+
const skillsBase = path7.join(home, ".claude", "skills");
|
|
24341
|
+
const bundledVersion = bundled[0].version;
|
|
24342
|
+
const skills = bundled.map((snap) => {
|
|
24343
|
+
const skillDir = path7.join(skillsBase, snap.name);
|
|
24344
|
+
if (!isInstalled(skillDir)) {
|
|
24345
|
+
return { name: snap.name, installed: false, installedVersion: null, missing: [], stale: [], edited: [], behind: false };
|
|
24346
|
+
}
|
|
24347
|
+
const manifest = readInstalledManifest(skillDir);
|
|
24348
|
+
const missing = [];
|
|
24349
|
+
const stale = [];
|
|
24350
|
+
const edited = [];
|
|
24351
|
+
for (const [rel, bundledHash] of Object.entries(snap.files)) {
|
|
24352
|
+
const installedHash = hashInstalledFile(path7.join(skillDir, rel));
|
|
24353
|
+
const state = classifySkillFile({ bundledHash, installedHash, manifestHash: manifest?.files[rel] });
|
|
24354
|
+
if (state === "missing") missing.push(rel);
|
|
24355
|
+
else if (state === "stale") stale.push(rel);
|
|
24356
|
+
else if (state === "edited") edited.push(rel);
|
|
24357
|
+
}
|
|
24358
|
+
return {
|
|
24359
|
+
name: snap.name,
|
|
24360
|
+
installed: true,
|
|
24361
|
+
installedVersion: manifest?.version ?? null,
|
|
24362
|
+
missing,
|
|
24363
|
+
stale,
|
|
24364
|
+
edited,
|
|
24365
|
+
behind: missing.length > 0 || stale.length > 0
|
|
24366
|
+
};
|
|
24367
|
+
});
|
|
24368
|
+
const details = { checkedPath: skillsBase, bundledVersion, skills };
|
|
24369
|
+
const installedSkills = skills.filter((s) => s.installed);
|
|
24370
|
+
if (installedSkills.length === 0) {
|
|
24371
|
+
return {
|
|
24372
|
+
status: CheckStatuses.skipped,
|
|
24373
|
+
code: "agent.skills.none-installed",
|
|
24374
|
+
summary: "No agent skills are installed under ~/.claude/skills/ \u2014 see agent.skills.installed.",
|
|
24375
|
+
remediation: null,
|
|
24376
|
+
details
|
|
24377
|
+
};
|
|
24378
|
+
}
|
|
24379
|
+
const behind = installedSkills.filter((s) => s.behind);
|
|
24380
|
+
if (behind.length === 0) {
|
|
24381
|
+
return {
|
|
24382
|
+
status: CheckStatuses.ok,
|
|
24383
|
+
code: "agent.skills.current",
|
|
24384
|
+
summary: `Installed agent skills are up to date with the bundled version (v${bundledVersion}).`,
|
|
24385
|
+
remediation: null,
|
|
24386
|
+
details
|
|
24387
|
+
};
|
|
24388
|
+
}
|
|
24389
|
+
const newFiles = behind.reduce((n, s) => n + s.missing.length, 0);
|
|
24390
|
+
const updatedFiles = behind.reduce((n, s) => n + s.stale.length, 0);
|
|
24391
|
+
return {
|
|
24392
|
+
status: CheckStatuses.warn,
|
|
24393
|
+
code: "agent.skills.behind",
|
|
24394
|
+
summary: `${behind.map((s) => s.name).join(", ")} ${behind.length === 1 ? "is" : "are"} behind the bundled skill version (v${bundledVersion}): ${newFiles} new file(s), ${updatedFiles} updated file(s) not yet installed.`,
|
|
24395
|
+
remediation: "Run `canonry skills install --user` to additively refresh \u2014 new and upstream-updated files are copied; your local edits are preserved.",
|
|
24396
|
+
details
|
|
24397
|
+
};
|
|
24398
|
+
}
|
|
24399
|
+
};
|
|
23327
24400
|
function isInstalled(dir) {
|
|
23328
24401
|
try {
|
|
23329
24402
|
if (!fs6.existsSync(dir)) return false;
|
|
@@ -23332,7 +24405,21 @@ function isInstalled(dir) {
|
|
|
23332
24405
|
return false;
|
|
23333
24406
|
}
|
|
23334
24407
|
}
|
|
23335
|
-
|
|
24408
|
+
function hashInstalledFile(filePath) {
|
|
24409
|
+
try {
|
|
24410
|
+
return crypto23.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
|
|
24411
|
+
} catch {
|
|
24412
|
+
return void 0;
|
|
24413
|
+
}
|
|
24414
|
+
}
|
|
24415
|
+
function readInstalledManifest(skillDir) {
|
|
24416
|
+
try {
|
|
24417
|
+
return coerceSkillManifest(JSON.parse(fs6.readFileSync(path7.join(skillDir, SKILL_MANIFEST_FILENAME), "utf-8")));
|
|
24418
|
+
} catch {
|
|
24419
|
+
return null;
|
|
24420
|
+
}
|
|
24421
|
+
}
|
|
24422
|
+
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
23336
24423
|
|
|
23337
24424
|
// ../api-routes/src/doctor/checks/bing-auth.ts
|
|
23338
24425
|
var BING_AUTH_CHECKS = [
|
|
@@ -23962,8 +25049,8 @@ var dbFilePresentCheck = {
|
|
|
23962
25049
|
scope: CheckScopes.global,
|
|
23963
25050
|
title: "Database file present",
|
|
23964
25051
|
run: (ctx) => {
|
|
23965
|
-
const
|
|
23966
|
-
if (!
|
|
25052
|
+
const path17 = ctx.runtimeStatePaths?.databasePath;
|
|
25053
|
+
if (!path17) {
|
|
23967
25054
|
return {
|
|
23968
25055
|
status: CheckStatuses.skipped,
|
|
23969
25056
|
code: "db.file.path-not-wired",
|
|
@@ -23971,21 +25058,21 @@ var dbFilePresentCheck = {
|
|
|
23971
25058
|
remediation: null
|
|
23972
25059
|
};
|
|
23973
25060
|
}
|
|
23974
|
-
if (!fs7.existsSync(
|
|
25061
|
+
if (!fs7.existsSync(path17)) {
|
|
23975
25062
|
return {
|
|
23976
25063
|
status: CheckStatuses.fail,
|
|
23977
25064
|
code: "db.file.missing",
|
|
23978
|
-
summary: `Database file at \`${
|
|
25065
|
+
summary: `Database file at \`${path17}\` has been deleted while the daemon is running.`,
|
|
23979
25066
|
remediation: "Restart `canonry serve` so a fresh database is created and migrations re-run. Until you do, the daemon will keep serving stale data from a deleted-but-open file handle and writes will be lost.",
|
|
23980
|
-
details: { path:
|
|
25067
|
+
details: { path: path17 }
|
|
23981
25068
|
};
|
|
23982
25069
|
}
|
|
23983
25070
|
return {
|
|
23984
25071
|
status: CheckStatuses.ok,
|
|
23985
25072
|
code: "db.file.present",
|
|
23986
|
-
summary: `Database file present at \`${
|
|
25073
|
+
summary: `Database file present at \`${path17}\`.`,
|
|
23987
25074
|
remediation: null,
|
|
23988
|
-
details: { path:
|
|
25075
|
+
details: { path: path17 }
|
|
23989
25076
|
};
|
|
23990
25077
|
}
|
|
23991
25078
|
};
|
|
@@ -23995,8 +25082,8 @@ var configFilePresentCheck = {
|
|
|
23995
25082
|
scope: CheckScopes.global,
|
|
23996
25083
|
title: "Config file present",
|
|
23997
25084
|
run: (ctx) => {
|
|
23998
|
-
const
|
|
23999
|
-
if (!
|
|
25085
|
+
const path17 = ctx.runtimeStatePaths?.configPath;
|
|
25086
|
+
if (!path17) {
|
|
24000
25087
|
return {
|
|
24001
25088
|
status: CheckStatuses.skipped,
|
|
24002
25089
|
code: "config.file.path-not-wired",
|
|
@@ -24004,21 +25091,21 @@ var configFilePresentCheck = {
|
|
|
24004
25091
|
remediation: null
|
|
24005
25092
|
};
|
|
24006
25093
|
}
|
|
24007
|
-
if (!fs7.existsSync(
|
|
25094
|
+
if (!fs7.existsSync(path17)) {
|
|
24008
25095
|
return {
|
|
24009
25096
|
status: CheckStatuses.fail,
|
|
24010
25097
|
code: "config.file.missing",
|
|
24011
|
-
summary: `Config file at \`${
|
|
25098
|
+
summary: `Config file at \`${path17}\` has been deleted while the daemon is running.`,
|
|
24012
25099
|
remediation: "Restart `canonry serve` after the file is restored (provider keys, OAuth tokens, and integration credentials live in this file; the in-memory copy is read-only until restart).",
|
|
24013
|
-
details: { path:
|
|
25100
|
+
details: { path: path17 }
|
|
24014
25101
|
};
|
|
24015
25102
|
}
|
|
24016
25103
|
return {
|
|
24017
25104
|
status: CheckStatuses.ok,
|
|
24018
25105
|
code: "config.file.present",
|
|
24019
|
-
summary: `Config file present at \`${
|
|
25106
|
+
summary: `Config file present at \`${path17}\`.`,
|
|
24020
25107
|
remediation: null,
|
|
24021
|
-
details: { path:
|
|
25108
|
+
details: { path: path17 }
|
|
24022
25109
|
};
|
|
24023
25110
|
}
|
|
24024
25111
|
};
|
|
@@ -24498,7 +25585,8 @@ async function doctorRoutes(app, opts) {
|
|
|
24498
25585
|
redirectUri,
|
|
24499
25586
|
providerSummary: opts.providerSummary,
|
|
24500
25587
|
trafficSourceValidators: opts.trafficSourceValidators,
|
|
24501
|
-
runtimeStatePaths: opts.runtimeStatePaths
|
|
25588
|
+
runtimeStatePaths: opts.runtimeStatePaths,
|
|
25589
|
+
bundledSkills: opts.bundledSkills
|
|
24502
25590
|
};
|
|
24503
25591
|
return runChecks(ctx, ALL_CHECKS, { checkIds });
|
|
24504
25592
|
});
|
|
@@ -24521,15 +25609,16 @@ async function doctorRoutes(app, opts) {
|
|
|
24521
25609
|
redirectUri,
|
|
24522
25610
|
providerSummary: opts.providerSummary,
|
|
24523
25611
|
trafficSourceValidators: opts.trafficSourceValidators,
|
|
24524
|
-
runtimeStatePaths: opts.runtimeStatePaths
|
|
25612
|
+
runtimeStatePaths: opts.runtimeStatePaths,
|
|
25613
|
+
bundledSkills: opts.bundledSkills
|
|
24525
25614
|
};
|
|
24526
25615
|
return runChecks(ctx, ALL_CHECKS, { checkIds });
|
|
24527
25616
|
});
|
|
24528
25617
|
}
|
|
24529
25618
|
|
|
24530
25619
|
// ../api-routes/src/discovery/routes.ts
|
|
24531
|
-
import
|
|
24532
|
-
import { and as and21, desc as desc14, eq as eq26, gte as gte5, inArray as
|
|
25620
|
+
import crypto24 from "crypto";
|
|
25621
|
+
import { and as and21, desc as desc14, eq as eq26, gte as gte5, inArray as inArray10 } from "drizzle-orm";
|
|
24533
25622
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
24534
25623
|
async function discoveryRoutes(app, opts) {
|
|
24535
25624
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -24564,7 +25653,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
24564
25653
|
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and21(
|
|
24565
25654
|
eq26(discoverySessions.projectId, project.id),
|
|
24566
25655
|
eq26(discoverySessions.icpDescription, icpDescription),
|
|
24567
|
-
|
|
25656
|
+
inArray10(discoverySessions.status, [
|
|
24568
25657
|
DiscoverySessionStatuses.queued,
|
|
24569
25658
|
DiscoverySessionStatuses.seeding,
|
|
24570
25659
|
DiscoverySessionStatuses.probing
|
|
@@ -24574,8 +25663,8 @@ async function discoveryRoutes(app, opts) {
|
|
|
24574
25663
|
if (existing && existing.runId) {
|
|
24575
25664
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
24576
25665
|
}
|
|
24577
|
-
const sessionId =
|
|
24578
|
-
const runId =
|
|
25666
|
+
const sessionId = crypto24.randomUUID();
|
|
25667
|
+
const runId = crypto24.randomUUID();
|
|
24579
25668
|
tx.insert(discoverySessions).values({
|
|
24580
25669
|
id: sessionId,
|
|
24581
25670
|
projectId: project.id,
|
|
@@ -24757,7 +25846,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
24757
25846
|
app.db.transaction((tx) => {
|
|
24758
25847
|
for (const query of promotedQueries) {
|
|
24759
25848
|
tx.insert(queries).values({
|
|
24760
|
-
id:
|
|
25849
|
+
id: crypto24.randomUUID(),
|
|
24761
25850
|
projectId: project.id,
|
|
24762
25851
|
query,
|
|
24763
25852
|
provenance,
|
|
@@ -24766,7 +25855,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
24766
25855
|
}
|
|
24767
25856
|
for (const domain of promotedCompetitors) {
|
|
24768
25857
|
tx.insert(competitors).values({
|
|
24769
|
-
id:
|
|
25858
|
+
id: crypto24.randomUUID(),
|
|
24770
25859
|
projectId: project.id,
|
|
24771
25860
|
domain,
|
|
24772
25861
|
provenance,
|
|
@@ -24840,7 +25929,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
24840
25929
|
}
|
|
24841
25930
|
|
|
24842
25931
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
24843
|
-
import
|
|
25932
|
+
import crypto25 from "crypto";
|
|
24844
25933
|
import { eq as eq27 } from "drizzle-orm";
|
|
24845
25934
|
var DEFAULT_DEDUP_THRESHOLD = 0.85;
|
|
24846
25935
|
var DEFAULT_MAX_PROBES = 100;
|
|
@@ -24929,7 +26018,7 @@ async function executeDiscovery(opts) {
|
|
|
24929
26018
|
probeRows.push({ citedDomains: probe.citedDomains, bucket });
|
|
24930
26019
|
buckets[bucket]++;
|
|
24931
26020
|
opts.db.insert(discoveryProbes).values({
|
|
24932
|
-
id:
|
|
26021
|
+
id: crypto25.randomUUID(),
|
|
24933
26022
|
sessionId: opts.sessionId,
|
|
24934
26023
|
projectId: opts.project.id,
|
|
24935
26024
|
query,
|
|
@@ -25102,7 +26191,8 @@ async function apiRoutes(app, opts) {
|
|
|
25102
26191
|
publicUrl: opts.publicUrl,
|
|
25103
26192
|
routePrefix: opts.routePrefix,
|
|
25104
26193
|
onGscSyncRequested: opts.onGscSyncRequested,
|
|
25105
|
-
onInspectSitemapRequested: opts.onInspectSitemapRequested
|
|
26194
|
+
onInspectSitemapRequested: opts.onInspectSitemapRequested,
|
|
26195
|
+
onGbpSyncRequested: opts.onGbpSyncRequested
|
|
25106
26196
|
});
|
|
25107
26197
|
await api.register(wordpressRoutes, {
|
|
25108
26198
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
@@ -25151,7 +26241,8 @@ async function apiRoutes(app, opts) {
|
|
|
25151
26241
|
publicUrl: opts.publicUrl,
|
|
25152
26242
|
providerSummary: opts.providerSummary,
|
|
25153
26243
|
trafficSourceValidators: buildTrafficSourceValidators(opts),
|
|
25154
|
-
runtimeStatePaths: opts.runtimeStatePaths
|
|
26244
|
+
runtimeStatePaths: opts.runtimeStatePaths,
|
|
26245
|
+
bundledSkills: opts.bundledSkills
|
|
25155
26246
|
});
|
|
25156
26247
|
if (opts.registerAuthenticatedRoutes) {
|
|
25157
26248
|
await opts.registerAuthenticatedRoutes(api);
|
|
@@ -25295,45 +26386,28 @@ function buildTrafficSourceValidators(opts) {
|
|
|
25295
26386
|
}
|
|
25296
26387
|
|
|
25297
26388
|
// src/server.ts
|
|
25298
|
-
import
|
|
26389
|
+
import os7 from "os";
|
|
25299
26390
|
|
|
25300
26391
|
// ../provider-gemini/src/normalize.ts
|
|
25301
26392
|
import { GoogleGenAI } from "@google/genai";
|
|
25302
26393
|
|
|
25303
26394
|
// ../provider-gemini/src/utils.ts
|
|
25304
|
-
function
|
|
25305
|
-
|
|
25306
|
-
|
|
25307
|
-
|
|
25308
|
-
|
|
25309
|
-
|
|
25310
|
-
|
|
25311
|
-
|
|
25312
|
-
|
|
25313
|
-
|
|
25314
|
-
|
|
25315
|
-
|
|
25316
|
-
|
|
25317
|
-
|
|
25318
|
-
}
|
|
25319
|
-
async function withRetry(fn, options = {}) {
|
|
25320
|
-
const { maxRetries = 3, initialDelay = 1e3 } = options;
|
|
25321
|
-
let lastError;
|
|
25322
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
25323
|
-
try {
|
|
25324
|
-
return await fn();
|
|
25325
|
-
} catch (err) {
|
|
25326
|
-
lastError = err;
|
|
25327
|
-
if (attempt < maxRetries && isRetryableError(err)) {
|
|
25328
|
-
const delay = initialDelay * Math.pow(2, attempt);
|
|
25329
|
-
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
25330
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
25331
|
-
} else {
|
|
25332
|
-
throw err;
|
|
25333
|
-
}
|
|
26395
|
+
async function withRetry2(fn, options = {}) {
|
|
26396
|
+
return withRetry(fn, {
|
|
26397
|
+
maxRetries: options.maxRetries ?? 3,
|
|
26398
|
+
baseDelayMs: options.initialDelay ?? 1e3,
|
|
26399
|
+
// Jitter off preserves the historical deterministic backoff that every
|
|
26400
|
+
// provider has shipped to date (1000, 2000, 4000ms). Enable per-caller
|
|
26401
|
+
// if you need it.
|
|
26402
|
+
jitter: false,
|
|
26403
|
+
isRetryable: isRetryableHttpError,
|
|
26404
|
+
onRetry: ({ attempt, err, delayMs }) => {
|
|
26405
|
+
console.warn(
|
|
26406
|
+
`[provider] Attempt ${attempt + 1} failed, retrying in ${delayMs}ms...`,
|
|
26407
|
+
err instanceof Error ? err.message : String(err)
|
|
26408
|
+
);
|
|
25334
26409
|
}
|
|
25335
|
-
}
|
|
25336
|
-
throw lastError;
|
|
26410
|
+
});
|
|
25337
26411
|
}
|
|
25338
26412
|
|
|
25339
26413
|
// ../provider-gemini/src/normalize.ts
|
|
@@ -25385,7 +26459,7 @@ async function healthcheck(config) {
|
|
|
25385
26459
|
try {
|
|
25386
26460
|
const model = resolveModel(config);
|
|
25387
26461
|
const client = createClient2(config);
|
|
25388
|
-
const result = await
|
|
26462
|
+
const result = await withRetry2(
|
|
25389
26463
|
() => client.models.generateContent({
|
|
25390
26464
|
model,
|
|
25391
26465
|
contents: 'Say "ok"'
|
|
@@ -25413,7 +26487,7 @@ async function executeTrackedQuery(input) {
|
|
|
25413
26487
|
const prompt = buildPrompt(input.query, input.location);
|
|
25414
26488
|
const client = createClient2(input.config);
|
|
25415
26489
|
try {
|
|
25416
|
-
const result = await
|
|
26490
|
+
const result = await withRetry2(
|
|
25417
26491
|
() => client.models.generateContent({
|
|
25418
26492
|
model,
|
|
25419
26493
|
contents: prompt,
|
|
@@ -25577,7 +26651,7 @@ function extractDomainFromUri(uri) {
|
|
|
25577
26651
|
async function generateText(prompt, config) {
|
|
25578
26652
|
const model = resolveModel(config);
|
|
25579
26653
|
const client = createClient2(config);
|
|
25580
|
-
const result = await
|
|
26654
|
+
const result = await withRetry2(
|
|
25581
26655
|
() => client.models.generateContent({
|
|
25582
26656
|
model,
|
|
25583
26657
|
contents: prompt
|
|
@@ -25744,39 +26818,22 @@ var geminiAdapter = {
|
|
|
25744
26818
|
import OpenAI from "openai";
|
|
25745
26819
|
|
|
25746
26820
|
// ../provider-openai/src/utils.ts
|
|
25747
|
-
function
|
|
25748
|
-
|
|
25749
|
-
|
|
25750
|
-
|
|
25751
|
-
|
|
25752
|
-
|
|
25753
|
-
|
|
25754
|
-
|
|
25755
|
-
|
|
25756
|
-
|
|
25757
|
-
|
|
25758
|
-
|
|
25759
|
-
|
|
25760
|
-
|
|
25761
|
-
}
|
|
25762
|
-
async function withRetry2(fn, options = {}) {
|
|
25763
|
-
const { maxRetries = 3, initialDelay = 1e3 } = options;
|
|
25764
|
-
let lastError;
|
|
25765
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
25766
|
-
try {
|
|
25767
|
-
return await fn();
|
|
25768
|
-
} catch (err) {
|
|
25769
|
-
lastError = err;
|
|
25770
|
-
if (attempt < maxRetries && isRetryableError2(err)) {
|
|
25771
|
-
const delay = initialDelay * Math.pow(2, attempt);
|
|
25772
|
-
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
25773
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
25774
|
-
} else {
|
|
25775
|
-
throw err;
|
|
25776
|
-
}
|
|
26821
|
+
async function withRetry3(fn, options = {}) {
|
|
26822
|
+
return withRetry(fn, {
|
|
26823
|
+
maxRetries: options.maxRetries ?? 3,
|
|
26824
|
+
baseDelayMs: options.initialDelay ?? 1e3,
|
|
26825
|
+
// Jitter off preserves the historical deterministic backoff that every
|
|
26826
|
+
// provider has shipped to date (1000, 2000, 4000ms). Enable per-caller
|
|
26827
|
+
// if you need it.
|
|
26828
|
+
jitter: false,
|
|
26829
|
+
isRetryable: isRetryableHttpError,
|
|
26830
|
+
onRetry: ({ attempt, err, delayMs }) => {
|
|
26831
|
+
console.warn(
|
|
26832
|
+
`[provider] Attempt ${attempt + 1} failed, retrying in ${delayMs}ms...`,
|
|
26833
|
+
err instanceof Error ? err.message : String(err)
|
|
26834
|
+
);
|
|
25777
26835
|
}
|
|
25778
|
-
}
|
|
25779
|
-
throw lastError;
|
|
26836
|
+
});
|
|
25780
26837
|
}
|
|
25781
26838
|
|
|
25782
26839
|
// ../provider-openai/src/normalize.ts
|
|
@@ -25797,7 +26854,7 @@ async function healthcheck2(config) {
|
|
|
25797
26854
|
if (!validation.ok) return validation;
|
|
25798
26855
|
try {
|
|
25799
26856
|
const client = new OpenAI({ apiKey: config.apiKey });
|
|
25800
|
-
const response = await
|
|
26857
|
+
const response = await withRetry3(
|
|
25801
26858
|
() => client.responses.create({
|
|
25802
26859
|
model: config.model ?? DEFAULT_MODEL2,
|
|
25803
26860
|
input: 'Say "ok"'
|
|
@@ -25833,7 +26890,7 @@ async function executeTrackedQuery2(input) {
|
|
|
25833
26890
|
};
|
|
25834
26891
|
}
|
|
25835
26892
|
try {
|
|
25836
|
-
const response = await
|
|
26893
|
+
const response = await withRetry3(
|
|
25837
26894
|
() => client.responses.create({
|
|
25838
26895
|
model,
|
|
25839
26896
|
tools: [webSearchTool],
|
|
@@ -25995,7 +27052,7 @@ function extractDomainFromUri2(uri) {
|
|
|
25995
27052
|
async function generateText2(prompt, config) {
|
|
25996
27053
|
const model = config.model ?? DEFAULT_MODEL2;
|
|
25997
27054
|
const client = new OpenAI({ apiKey: config.apiKey });
|
|
25998
|
-
const response = await
|
|
27055
|
+
const response = await withRetry3(
|
|
25999
27056
|
() => client.responses.create({
|
|
26000
27057
|
model,
|
|
26001
27058
|
input: prompt
|
|
@@ -26098,39 +27155,22 @@ var openaiAdapter = {
|
|
|
26098
27155
|
import Anthropic from "@anthropic-ai/sdk";
|
|
26099
27156
|
|
|
26100
27157
|
// ../provider-claude/src/utils.ts
|
|
26101
|
-
function
|
|
26102
|
-
|
|
26103
|
-
|
|
26104
|
-
|
|
26105
|
-
|
|
26106
|
-
|
|
26107
|
-
|
|
26108
|
-
|
|
26109
|
-
|
|
26110
|
-
|
|
26111
|
-
|
|
26112
|
-
|
|
26113
|
-
|
|
26114
|
-
|
|
26115
|
-
}
|
|
26116
|
-
async function withRetry3(fn, options = {}) {
|
|
26117
|
-
const { maxRetries = 3, initialDelay = 1e3 } = options;
|
|
26118
|
-
let lastError;
|
|
26119
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
26120
|
-
try {
|
|
26121
|
-
return await fn();
|
|
26122
|
-
} catch (err) {
|
|
26123
|
-
lastError = err;
|
|
26124
|
-
if (attempt < maxRetries && isRetryableError3(err)) {
|
|
26125
|
-
const delay = initialDelay * Math.pow(2, attempt);
|
|
26126
|
-
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
26127
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
26128
|
-
} else {
|
|
26129
|
-
throw err;
|
|
26130
|
-
}
|
|
27158
|
+
async function withRetry4(fn, options = {}) {
|
|
27159
|
+
return withRetry(fn, {
|
|
27160
|
+
maxRetries: options.maxRetries ?? 3,
|
|
27161
|
+
baseDelayMs: options.initialDelay ?? 1e3,
|
|
27162
|
+
// Jitter off preserves the historical deterministic backoff that every
|
|
27163
|
+
// provider has shipped to date (1000, 2000, 4000ms). Enable per-caller
|
|
27164
|
+
// if you need it.
|
|
27165
|
+
jitter: false,
|
|
27166
|
+
isRetryable: isRetryableHttpError,
|
|
27167
|
+
onRetry: ({ attempt, err, delayMs }) => {
|
|
27168
|
+
console.warn(
|
|
27169
|
+
`[provider] Attempt ${attempt + 1} failed, retrying in ${delayMs}ms...`,
|
|
27170
|
+
err instanceof Error ? err.message : String(err)
|
|
27171
|
+
);
|
|
26131
27172
|
}
|
|
26132
|
-
}
|
|
26133
|
-
throw lastError;
|
|
27173
|
+
});
|
|
26134
27174
|
}
|
|
26135
27175
|
|
|
26136
27176
|
// ../provider-claude/src/normalize.ts
|
|
@@ -26164,7 +27204,7 @@ async function healthcheck3(config) {
|
|
|
26164
27204
|
try {
|
|
26165
27205
|
const model = resolveModel2(config);
|
|
26166
27206
|
const client = new Anthropic({ apiKey: config.apiKey });
|
|
26167
|
-
const response = await
|
|
27207
|
+
const response = await withRetry4(
|
|
26168
27208
|
() => client.messages.create({
|
|
26169
27209
|
model,
|
|
26170
27210
|
max_tokens: 32,
|
|
@@ -26205,7 +27245,7 @@ async function executeTrackedQuery3(input) {
|
|
|
26205
27245
|
};
|
|
26206
27246
|
}
|
|
26207
27247
|
try {
|
|
26208
|
-
const response = await
|
|
27248
|
+
const response = await withRetry4(
|
|
26209
27249
|
() => client.messages.create({
|
|
26210
27250
|
model,
|
|
26211
27251
|
max_tokens: 4096,
|
|
@@ -26374,7 +27414,7 @@ function extractDomainFromUri3(uri) {
|
|
|
26374
27414
|
async function generateText3(prompt, config) {
|
|
26375
27415
|
const model = resolveModel2(config);
|
|
26376
27416
|
const client = new Anthropic({ apiKey: config.apiKey });
|
|
26377
|
-
const response = await
|
|
27417
|
+
const response = await withRetry4(
|
|
26378
27418
|
() => client.messages.create({
|
|
26379
27419
|
model,
|
|
26380
27420
|
max_tokens: 2048,
|
|
@@ -26475,39 +27515,22 @@ var claudeAdapter = {
|
|
|
26475
27515
|
import OpenAI2 from "openai";
|
|
26476
27516
|
|
|
26477
27517
|
// ../provider-local/src/utils.ts
|
|
26478
|
-
function
|
|
26479
|
-
|
|
26480
|
-
|
|
26481
|
-
|
|
26482
|
-
|
|
26483
|
-
|
|
26484
|
-
|
|
26485
|
-
|
|
26486
|
-
|
|
26487
|
-
|
|
26488
|
-
|
|
26489
|
-
|
|
26490
|
-
|
|
26491
|
-
|
|
26492
|
-
}
|
|
26493
|
-
async function withRetry4(fn, options = {}) {
|
|
26494
|
-
const { maxRetries = 3, initialDelay = 1e3 } = options;
|
|
26495
|
-
let lastError;
|
|
26496
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
26497
|
-
try {
|
|
26498
|
-
return await fn();
|
|
26499
|
-
} catch (err) {
|
|
26500
|
-
lastError = err;
|
|
26501
|
-
if (attempt < maxRetries && isRetryableError4(err)) {
|
|
26502
|
-
const delay = initialDelay * Math.pow(2, attempt);
|
|
26503
|
-
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
26504
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
26505
|
-
} else {
|
|
26506
|
-
throw err;
|
|
26507
|
-
}
|
|
27518
|
+
async function withRetry5(fn, options = {}) {
|
|
27519
|
+
return withRetry(fn, {
|
|
27520
|
+
maxRetries: options.maxRetries ?? 3,
|
|
27521
|
+
baseDelayMs: options.initialDelay ?? 1e3,
|
|
27522
|
+
// Jitter off preserves the historical deterministic backoff that every
|
|
27523
|
+
// provider has shipped to date (1000, 2000, 4000ms). Enable per-caller
|
|
27524
|
+
// if you need it.
|
|
27525
|
+
jitter: false,
|
|
27526
|
+
isRetryable: isRetryableHttpError,
|
|
27527
|
+
onRetry: ({ attempt, err, delayMs }) => {
|
|
27528
|
+
console.warn(
|
|
27529
|
+
`[provider] Attempt ${attempt + 1} failed, retrying in ${delayMs}ms...`,
|
|
27530
|
+
err instanceof Error ? err.message : String(err)
|
|
27531
|
+
);
|
|
26508
27532
|
}
|
|
26509
|
-
}
|
|
26510
|
-
throw lastError;
|
|
27533
|
+
});
|
|
26511
27534
|
}
|
|
26512
27535
|
|
|
26513
27536
|
// ../provider-local/src/normalize.ts
|
|
@@ -26531,7 +27554,7 @@ async function healthcheck4(config) {
|
|
|
26531
27554
|
baseURL: config.baseUrl,
|
|
26532
27555
|
apiKey: config.apiKey || "not-needed"
|
|
26533
27556
|
});
|
|
26534
|
-
const models = await
|
|
27557
|
+
const models = await withRetry5(async () => {
|
|
26535
27558
|
const list = await client.models.list();
|
|
26536
27559
|
const items = [];
|
|
26537
27560
|
for await (const m of list) {
|
|
@@ -26562,7 +27585,7 @@ async function executeTrackedQuery4(input) {
|
|
|
26562
27585
|
apiKey: input.config.apiKey || "not-needed"
|
|
26563
27586
|
});
|
|
26564
27587
|
try {
|
|
26565
|
-
const response = await
|
|
27588
|
+
const response = await withRetry5(
|
|
26566
27589
|
() => client.chat.completions.create({
|
|
26567
27590
|
model,
|
|
26568
27591
|
messages: [
|
|
@@ -26623,7 +27646,7 @@ async function generateText4(prompt, config) {
|
|
|
26623
27646
|
baseURL: config.baseUrl,
|
|
26624
27647
|
apiKey: config.apiKey || "not-needed"
|
|
26625
27648
|
});
|
|
26626
|
-
const response = await
|
|
27649
|
+
const response = await withRetry5(
|
|
26627
27650
|
() => client.chat.completions.create({
|
|
26628
27651
|
model,
|
|
26629
27652
|
messages: [{ role: "user", content: prompt }]
|
|
@@ -27301,39 +28324,22 @@ var cdpChatgptAdapter = {
|
|
|
27301
28324
|
import OpenAI3 from "openai";
|
|
27302
28325
|
|
|
27303
28326
|
// ../provider-perplexity/src/utils.ts
|
|
27304
|
-
function
|
|
27305
|
-
|
|
27306
|
-
|
|
27307
|
-
|
|
27308
|
-
|
|
27309
|
-
|
|
27310
|
-
|
|
27311
|
-
|
|
27312
|
-
|
|
27313
|
-
|
|
27314
|
-
|
|
27315
|
-
|
|
27316
|
-
|
|
27317
|
-
|
|
27318
|
-
}
|
|
27319
|
-
async function withRetry5(fn, options = {}) {
|
|
27320
|
-
const { maxRetries = 3, initialDelay = 1e3 } = options;
|
|
27321
|
-
let lastError;
|
|
27322
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
27323
|
-
try {
|
|
27324
|
-
return await fn();
|
|
27325
|
-
} catch (err) {
|
|
27326
|
-
lastError = err;
|
|
27327
|
-
if (attempt < maxRetries && isRetryableError5(err)) {
|
|
27328
|
-
const delay = initialDelay * Math.pow(2, attempt);
|
|
27329
|
-
console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
|
|
27330
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
27331
|
-
} else {
|
|
27332
|
-
throw err;
|
|
27333
|
-
}
|
|
28327
|
+
async function withRetry6(fn, options = {}) {
|
|
28328
|
+
return withRetry(fn, {
|
|
28329
|
+
maxRetries: options.maxRetries ?? 3,
|
|
28330
|
+
baseDelayMs: options.initialDelay ?? 1e3,
|
|
28331
|
+
// Jitter off preserves the historical deterministic backoff that every
|
|
28332
|
+
// provider has shipped to date (1000, 2000, 4000ms). Enable per-caller
|
|
28333
|
+
// if you need it.
|
|
28334
|
+
jitter: false,
|
|
28335
|
+
isRetryable: isRetryableHttpError,
|
|
28336
|
+
onRetry: ({ attempt, err, delayMs }) => {
|
|
28337
|
+
console.warn(
|
|
28338
|
+
`[provider] Attempt ${attempt + 1} failed, retrying in ${delayMs}ms...`,
|
|
28339
|
+
err instanceof Error ? err.message : String(err)
|
|
28340
|
+
);
|
|
27334
28341
|
}
|
|
27335
|
-
}
|
|
27336
|
-
throw lastError;
|
|
28342
|
+
});
|
|
27337
28343
|
}
|
|
27338
28344
|
|
|
27339
28345
|
// ../provider-perplexity/src/normalize.ts
|
|
@@ -27355,7 +28361,7 @@ async function healthcheck5(config) {
|
|
|
27355
28361
|
if (!validation.ok) return validation;
|
|
27356
28362
|
try {
|
|
27357
28363
|
const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
|
|
27358
|
-
const response = await
|
|
28364
|
+
const response = await withRetry6(
|
|
27359
28365
|
() => client.chat.completions.create({
|
|
27360
28366
|
model: config.model ?? DEFAULT_MODEL5,
|
|
27361
28367
|
messages: [{ role: "user", content: 'Say "ok"' }]
|
|
@@ -27382,7 +28388,7 @@ async function executeTrackedQuery5(input) {
|
|
|
27382
28388
|
const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
|
|
27383
28389
|
const prompt = buildPrompt4(input.query, input.location);
|
|
27384
28390
|
try {
|
|
27385
|
-
const response = await
|
|
28391
|
+
const response = await withRetry6(
|
|
27386
28392
|
() => client.chat.completions.create({
|
|
27387
28393
|
model,
|
|
27388
28394
|
messages: [{ role: "user", content: prompt }]
|
|
@@ -27543,7 +28549,7 @@ function extractDomainFromUri4(uri) {
|
|
|
27543
28549
|
async function generateText5(prompt, config) {
|
|
27544
28550
|
const model = config.model ?? DEFAULT_MODEL5;
|
|
27545
28551
|
const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
|
|
27546
|
-
const response = await
|
|
28552
|
+
const response = await withRetry6(
|
|
27547
28553
|
() => client.chat.completions.create({
|
|
27548
28554
|
model,
|
|
27549
28555
|
messages: [{ role: "user", content: prompt }]
|
|
@@ -27894,14 +28900,14 @@ function removeWordpressConnection(config, projectName) {
|
|
|
27894
28900
|
}
|
|
27895
28901
|
|
|
27896
28902
|
// src/job-runner.ts
|
|
27897
|
-
import
|
|
28903
|
+
import crypto27 from "crypto";
|
|
27898
28904
|
import fs10 from "fs";
|
|
27899
28905
|
import path10 from "path";
|
|
27900
28906
|
import os5 from "os";
|
|
27901
|
-
import { and as and22, eq as eq28, inArray as
|
|
28907
|
+
import { and as and22, eq as eq28, inArray as inArray11, sql as sql12 } from "drizzle-orm";
|
|
27902
28908
|
|
|
27903
28909
|
// src/run-telemetry.ts
|
|
27904
|
-
import
|
|
28910
|
+
import crypto26 from "crypto";
|
|
27905
28911
|
function extractRegistrableHost(input) {
|
|
27906
28912
|
if (!input) return null;
|
|
27907
28913
|
const trimmed = input.trim();
|
|
@@ -27921,7 +28927,7 @@ function extractRegistrableHost(input) {
|
|
|
27921
28927
|
function hashDomain(input) {
|
|
27922
28928
|
const host = extractRegistrableHost(input);
|
|
27923
28929
|
if (!host) return null;
|
|
27924
|
-
return
|
|
28930
|
+
return crypto26.createHash("sha256").update(host).digest("hex");
|
|
27925
28931
|
}
|
|
27926
28932
|
function buildRunCompletedProps(input) {
|
|
27927
28933
|
const totalMs = input.phases?.total_ms ?? Date.now() - input.startTime;
|
|
@@ -28083,7 +29089,7 @@ var JobRunner = class {
|
|
|
28083
29089
|
this.registry = registry;
|
|
28084
29090
|
}
|
|
28085
29091
|
recoverStaleRuns() {
|
|
28086
|
-
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
29092
|
+
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray11(runs.status, ["running", "queued"])).all();
|
|
28087
29093
|
if (stale.length === 0) return;
|
|
28088
29094
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
28089
29095
|
for (const run of stale) {
|
|
@@ -28146,7 +29152,7 @@ var JobRunner = class {
|
|
|
28146
29152
|
}
|
|
28147
29153
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
28148
29154
|
const scopedQueryNames = existingRun.queries;
|
|
28149
|
-
projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and22(eq28(queries.projectId, projectId),
|
|
29155
|
+
projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and22(eq28(queries.projectId, projectId), inArray11(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq28(queries.projectId, projectId)).all();
|
|
28150
29156
|
const projectCompetitors = this.db.select().from(competitors).where(eq28(competitors.projectId, projectId)).all();
|
|
28151
29157
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
28152
29158
|
const allDomains = effectiveDomains({
|
|
@@ -28230,7 +29236,7 @@ var JobRunner = class {
|
|
|
28230
29236
|
);
|
|
28231
29237
|
let screenshotRelPath = null;
|
|
28232
29238
|
if (raw.screenshotPath && fs10.existsSync(raw.screenshotPath)) {
|
|
28233
|
-
const snapshotId =
|
|
29239
|
+
const snapshotId = crypto27.randomUUID();
|
|
28234
29240
|
const screenshotDir = path10.join(os5.homedir(), ".canonry", "screenshots", runId);
|
|
28235
29241
|
if (!fs10.existsSync(screenshotDir)) fs10.mkdirSync(screenshotDir, { recursive: true });
|
|
28236
29242
|
const destPath = path10.join(screenshotDir, `${snapshotId}.png`);
|
|
@@ -28261,7 +29267,7 @@ var JobRunner = class {
|
|
|
28261
29267
|
}).run();
|
|
28262
29268
|
} else {
|
|
28263
29269
|
this.db.insert(querySnapshots).values({
|
|
28264
|
-
id:
|
|
29270
|
+
id: crypto27.randomUUID(),
|
|
28265
29271
|
runId,
|
|
28266
29272
|
queryId: q.id,
|
|
28267
29273
|
queryText: q.query,
|
|
@@ -28411,7 +29417,7 @@ var JobRunner = class {
|
|
|
28411
29417
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
28412
29418
|
const period = now.slice(0, 10);
|
|
28413
29419
|
this.db.insert(usageCounters).values({
|
|
28414
|
-
id:
|
|
29420
|
+
id: crypto27.randomUUID(),
|
|
28415
29421
|
scope,
|
|
28416
29422
|
period,
|
|
28417
29423
|
metric,
|
|
@@ -28488,7 +29494,7 @@ function buildPhases(input) {
|
|
|
28488
29494
|
}
|
|
28489
29495
|
|
|
28490
29496
|
// src/gsc-sync.ts
|
|
28491
|
-
import
|
|
29497
|
+
import crypto28 from "crypto";
|
|
28492
29498
|
import { eq as eq29, and as and23, sql as sql13 } from "drizzle-orm";
|
|
28493
29499
|
var log2 = createLogger("GscSync");
|
|
28494
29500
|
function formatDate3(d) {
|
|
@@ -28554,7 +29560,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
28554
29560
|
for (const row of batch) {
|
|
28555
29561
|
const [query, page, country, device, date] = row.keys;
|
|
28556
29562
|
db.insert(gscSearchData).values({
|
|
28557
|
-
id:
|
|
29563
|
+
id: crypto28.randomUUID(),
|
|
28558
29564
|
projectId,
|
|
28559
29565
|
syncRunId: runId,
|
|
28560
29566
|
date: date ?? "",
|
|
@@ -28588,7 +29594,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
28588
29594
|
const rich = ir.richResultsResult;
|
|
28589
29595
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28590
29596
|
db.insert(gscUrlInspections).values({
|
|
28591
|
-
id:
|
|
29597
|
+
id: crypto28.randomUUID(),
|
|
28592
29598
|
projectId,
|
|
28593
29599
|
syncRunId: runId,
|
|
28594
29600
|
url: pageUrl,
|
|
@@ -28632,7 +29638,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
28632
29638
|
const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
|
|
28633
29639
|
db.delete(gscCoverageSnapshots).where(and23(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
28634
29640
|
db.insert(gscCoverageSnapshots).values({
|
|
28635
|
-
id:
|
|
29641
|
+
id: crypto28.randomUUID(),
|
|
28636
29642
|
projectId,
|
|
28637
29643
|
syncRunId: runId,
|
|
28638
29644
|
date: snapshotDate,
|
|
@@ -28651,12 +29657,184 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
28651
29657
|
}
|
|
28652
29658
|
}
|
|
28653
29659
|
|
|
29660
|
+
// src/gbp-sync.ts
|
|
29661
|
+
import crypto29 from "crypto";
|
|
29662
|
+
import { eq as eq30, and as and24, desc as desc15 } from "drizzle-orm";
|
|
29663
|
+
var log3 = createLogger("GbpSync");
|
|
29664
|
+
var LOCATION_CONCURRENCY = 4;
|
|
29665
|
+
var DEFAULT_DAYS_OF_METRICS = 30;
|
|
29666
|
+
var DEFAULT_MONTHS_OF_KEYWORDS = 12;
|
|
29667
|
+
function daysAgo2(n) {
|
|
29668
|
+
const d = /* @__PURE__ */ new Date();
|
|
29669
|
+
d.setUTCDate(d.getUTCDate() - n);
|
|
29670
|
+
return d;
|
|
29671
|
+
}
|
|
29672
|
+
function monthMinus(n) {
|
|
29673
|
+
const d = /* @__PURE__ */ new Date();
|
|
29674
|
+
d.setUTCMonth(d.getUTCMonth() - n);
|
|
29675
|
+
return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1 };
|
|
29676
|
+
}
|
|
29677
|
+
async function executeGbpSync(db, runId, projectId, opts) {
|
|
29678
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
29679
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq30(runs.id, runId)).run();
|
|
29680
|
+
try {
|
|
29681
|
+
const { clientId, clientSecret } = getGoogleAuthConfig(opts.config);
|
|
29682
|
+
if (!clientId || !clientSecret) {
|
|
29683
|
+
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
29684
|
+
}
|
|
29685
|
+
const project = db.select().from(projects).where(eq30(projects.id, projectId)).get();
|
|
29686
|
+
if (!project) throw new Error(`Project not found: ${projectId}`);
|
|
29687
|
+
const conn = getGoogleConnection(opts.config, project.canonicalDomain, "gbp");
|
|
29688
|
+
if (!conn || !conn.refreshToken) {
|
|
29689
|
+
throw new Error('No GBP connection found or connection is incomplete. Run "canonry gbp connect" first.');
|
|
29690
|
+
}
|
|
29691
|
+
let accessToken = conn.accessToken;
|
|
29692
|
+
const expiresAt = conn.tokenExpiresAt ? new Date(conn.tokenExpiresAt).getTime() : 0;
|
|
29693
|
+
if (Date.now() > expiresAt - 5 * 60 * 1e3) {
|
|
29694
|
+
const tokens = await refreshAccessToken(clientId, clientSecret, conn.refreshToken);
|
|
29695
|
+
accessToken = tokens.access_token;
|
|
29696
|
+
patchGoogleConnection(opts.config, project.canonicalDomain, "gbp", {
|
|
29697
|
+
accessToken: tokens.access_token,
|
|
29698
|
+
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
|
|
29699
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
29700
|
+
});
|
|
29701
|
+
saveConfigPatch(opts.config);
|
|
29702
|
+
}
|
|
29703
|
+
let locationRows = db.select().from(gbpLocations).where(and24(eq30(gbpLocations.projectId, projectId), eq30(gbpLocations.selected, true))).all();
|
|
29704
|
+
if (opts.locationNames?.length) {
|
|
29705
|
+
const wanted = new Set(opts.locationNames);
|
|
29706
|
+
locationRows = locationRows.filter((l) => wanted.has(l.locationName));
|
|
29707
|
+
}
|
|
29708
|
+
if (locationRows.length === 0) {
|
|
29709
|
+
throw new Error("No selected GBP locations to sync. Discover and select locations first.");
|
|
29710
|
+
}
|
|
29711
|
+
const daysOfMetrics = opts.daysOfMetrics ?? DEFAULT_DAYS_OF_METRICS;
|
|
29712
|
+
const monthsOfKeywords = opts.monthsOfKeywords ?? DEFAULT_MONTHS_OF_KEYWORDS;
|
|
29713
|
+
const metricsStart = daysAgo2(daysOfMetrics);
|
|
29714
|
+
const metricsEnd = daysAgo2(1);
|
|
29715
|
+
const keywordsStart = monthMinus(monthsOfKeywords);
|
|
29716
|
+
const keywordsEnd = monthMinus(0);
|
|
29717
|
+
log3.info("sync.start", { runId, projectId, locations: locationRows.length, daysOfMetrics, monthsOfKeywords });
|
|
29718
|
+
const errors = /* @__PURE__ */ new Map();
|
|
29719
|
+
let okCount = 0;
|
|
29720
|
+
for (let i = 0; i < locationRows.length; i += LOCATION_CONCURRENCY) {
|
|
29721
|
+
const batch = locationRows.slice(i, i + LOCATION_CONCURRENCY);
|
|
29722
|
+
await Promise.all(batch.map(async (loc) => {
|
|
29723
|
+
try {
|
|
29724
|
+
const [metricRows, keywordRows, placeActionRows, lodging] = await Promise.all([
|
|
29725
|
+
fetchDailyMetrics(accessToken, loc.locationName, {
|
|
29726
|
+
metrics: GBP_DAILY_METRICS,
|
|
29727
|
+
startDate: metricsStart,
|
|
29728
|
+
endDate: metricsEnd
|
|
29729
|
+
}),
|
|
29730
|
+
listMonthlyKeywords(accessToken, loc.locationName, {
|
|
29731
|
+
startMonth: keywordsStart,
|
|
29732
|
+
endMonth: keywordsEnd
|
|
29733
|
+
}),
|
|
29734
|
+
listPlaceActionLinks(accessToken, loc.locationName),
|
|
29735
|
+
// null when the location is not a lodging-category property.
|
|
29736
|
+
getLodging(accessToken, loc.locationName)
|
|
29737
|
+
]);
|
|
29738
|
+
const lodgingHash = lodging ? hashLodging(lodging) : null;
|
|
29739
|
+
const latestLodging = lodging ? db.select().from(gbpLodgingSnapshots).where(and24(eq30(gbpLodgingSnapshots.projectId, projectId), eq30(gbpLodgingSnapshots.locationName, loc.locationName))).orderBy(desc15(gbpLodgingSnapshots.syncedAt)).limit(1).get() : void 0;
|
|
29740
|
+
const lodgingChanged = lodging !== null && latestLodging?.contentHash !== lodgingHash;
|
|
29741
|
+
const insertNow = (/* @__PURE__ */ new Date()).toISOString();
|
|
29742
|
+
db.transaction((tx) => {
|
|
29743
|
+
tx.delete(gbpDailyMetrics).where(and24(eq30(gbpDailyMetrics.projectId, projectId), eq30(gbpDailyMetrics.locationName, loc.locationName))).run();
|
|
29744
|
+
for (const row of metricRows) {
|
|
29745
|
+
tx.insert(gbpDailyMetrics).values({
|
|
29746
|
+
id: crypto29.randomUUID(),
|
|
29747
|
+
projectId,
|
|
29748
|
+
locationName: loc.locationName,
|
|
29749
|
+
date: row.date,
|
|
29750
|
+
metric: row.metric,
|
|
29751
|
+
value: row.value,
|
|
29752
|
+
syncRunId: runId
|
|
29753
|
+
}).run();
|
|
29754
|
+
}
|
|
29755
|
+
tx.delete(gbpKeywordImpressions).where(and24(eq30(gbpKeywordImpressions.projectId, projectId), eq30(gbpKeywordImpressions.locationName, loc.locationName))).run();
|
|
29756
|
+
for (const row of keywordRows) {
|
|
29757
|
+
tx.insert(gbpKeywordImpressions).values({
|
|
29758
|
+
id: crypto29.randomUUID(),
|
|
29759
|
+
projectId,
|
|
29760
|
+
locationName: loc.locationName,
|
|
29761
|
+
periodStart: monthKey(keywordsStart),
|
|
29762
|
+
periodEnd: monthKey(keywordsEnd),
|
|
29763
|
+
keyword: row.keyword,
|
|
29764
|
+
valueCount: row.valueCount,
|
|
29765
|
+
valueThreshold: row.valueThreshold,
|
|
29766
|
+
syncRunId: runId
|
|
29767
|
+
}).run();
|
|
29768
|
+
}
|
|
29769
|
+
tx.delete(gbpPlaceActions).where(and24(eq30(gbpPlaceActions.projectId, projectId), eq30(gbpPlaceActions.locationName, loc.locationName))).run();
|
|
29770
|
+
for (const row of placeActionRows) {
|
|
29771
|
+
tx.insert(gbpPlaceActions).values({
|
|
29772
|
+
id: crypto29.randomUUID(),
|
|
29773
|
+
projectId,
|
|
29774
|
+
locationName: loc.locationName,
|
|
29775
|
+
placeActionLinkName: row.placeActionLinkName,
|
|
29776
|
+
placeActionType: row.placeActionType,
|
|
29777
|
+
uri: row.uri,
|
|
29778
|
+
isPreferred: row.isPreferred,
|
|
29779
|
+
providerType: row.providerType,
|
|
29780
|
+
syncRunId: runId
|
|
29781
|
+
}).run();
|
|
29782
|
+
}
|
|
29783
|
+
if (lodging !== null && lodgingChanged) {
|
|
29784
|
+
tx.insert(gbpLodgingSnapshots).values({
|
|
29785
|
+
id: crypto29.randomUUID(),
|
|
29786
|
+
projectId,
|
|
29787
|
+
locationName: loc.locationName,
|
|
29788
|
+
contentHash: lodgingHash,
|
|
29789
|
+
attributes: lodging,
|
|
29790
|
+
populatedGroupCount: countPopulatedGroups(lodging),
|
|
29791
|
+
syncedAt: insertNow,
|
|
29792
|
+
syncRunId: runId
|
|
29793
|
+
}).run();
|
|
29794
|
+
}
|
|
29795
|
+
tx.update(gbpLocations).set({ syncedAt: insertNow, updatedAt: insertNow }).where(eq30(gbpLocations.id, loc.id)).run();
|
|
29796
|
+
});
|
|
29797
|
+
okCount++;
|
|
29798
|
+
} catch (err) {
|
|
29799
|
+
errors.set(loc.locationName, err instanceof Error ? err.message : String(err));
|
|
29800
|
+
log3.error("location.failed", { runId, location: loc.locationName, error: err instanceof Error ? err.message : String(err) });
|
|
29801
|
+
}
|
|
29802
|
+
}));
|
|
29803
|
+
}
|
|
29804
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29805
|
+
if (errors.size === 0) {
|
|
29806
|
+
db.update(runs).set({ status: "completed", finishedAt }).where(eq30(runs.id, runId)).run();
|
|
29807
|
+
} else if (okCount > 0) {
|
|
29808
|
+
db.update(runs).set({
|
|
29809
|
+
status: "partial",
|
|
29810
|
+
error: serializeRunError(buildRunErrorFromMessages(errors)),
|
|
29811
|
+
finishedAt
|
|
29812
|
+
}).where(eq30(runs.id, runId)).run();
|
|
29813
|
+
} else {
|
|
29814
|
+
db.update(runs).set({
|
|
29815
|
+
status: "failed",
|
|
29816
|
+
error: serializeRunError(buildRunErrorFromMessages(errors)),
|
|
29817
|
+
finishedAt
|
|
29818
|
+
}).where(eq30(runs.id, runId)).run();
|
|
29819
|
+
}
|
|
29820
|
+
log3.info("sync.done", { runId, projectId, ok: okCount, failed: errors.size });
|
|
29821
|
+
} catch (err) {
|
|
29822
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
29823
|
+
db.update(runs).set({ status: "failed", error: serializeRunError({ message: errorMsg }), finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(runs.id, runId)).run();
|
|
29824
|
+
log3.error("sync.failed", { runId, projectId, error: errorMsg });
|
|
29825
|
+
throw err;
|
|
29826
|
+
}
|
|
29827
|
+
}
|
|
29828
|
+
function monthKey(m) {
|
|
29829
|
+
return `${m.year}-${String(m.month).padStart(2, "0")}`;
|
|
29830
|
+
}
|
|
29831
|
+
|
|
28654
29832
|
// src/gsc-inspect-sitemap.ts
|
|
28655
|
-
import
|
|
28656
|
-
import { eq as
|
|
29833
|
+
import crypto30 from "crypto";
|
|
29834
|
+
import { eq as eq31, and as and25 } from "drizzle-orm";
|
|
28657
29835
|
|
|
28658
29836
|
// src/sitemap-parser.ts
|
|
28659
|
-
var
|
|
29837
|
+
var log4 = createLogger("SitemapParser");
|
|
28660
29838
|
var LOC_REGEX = /<loc>([^<]+)<\/loc>/gi;
|
|
28661
29839
|
var SITEMAP_TAG_REGEX = /<sitemap>[\s\S]*?<\/sitemap>/gi;
|
|
28662
29840
|
var PRIVATE_IP_PATTERNS = [
|
|
@@ -28719,7 +29897,7 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
|
|
|
28719
29897
|
res = await fetch(url);
|
|
28720
29898
|
} catch (err) {
|
|
28721
29899
|
if (!isChild) throw err;
|
|
28722
|
-
|
|
29900
|
+
log4.warn("child-sitemap.fetch-failed", {
|
|
28723
29901
|
url,
|
|
28724
29902
|
error: err instanceof Error ? err.message : String(err)
|
|
28725
29903
|
});
|
|
@@ -28729,7 +29907,7 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
|
|
|
28729
29907
|
if (!isChild) {
|
|
28730
29908
|
throw new Error(`Failed to fetch sitemap at ${url}: ${res.status} ${res.statusText}`);
|
|
28731
29909
|
}
|
|
28732
|
-
|
|
29910
|
+
log4.warn("child-sitemap.http-error", { url, status: res.status, statusText: res.statusText });
|
|
28733
29911
|
return;
|
|
28734
29912
|
}
|
|
28735
29913
|
let xml;
|
|
@@ -28737,7 +29915,7 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
|
|
|
28737
29915
|
xml = await readSitemapBody(res);
|
|
28738
29916
|
} catch (err) {
|
|
28739
29917
|
if (!isChild) throw err;
|
|
28740
|
-
|
|
29918
|
+
log4.warn("child-sitemap.parse-failed", {
|
|
28741
29919
|
url,
|
|
28742
29920
|
error: err instanceof Error ? err.message : String(err)
|
|
28743
29921
|
});
|
|
@@ -28773,16 +29951,16 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
|
|
|
28773
29951
|
}
|
|
28774
29952
|
|
|
28775
29953
|
// src/gsc-inspect-sitemap.ts
|
|
28776
|
-
var
|
|
29954
|
+
var log5 = createLogger("InspectSitemap");
|
|
28777
29955
|
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
28778
29956
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
28779
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
29957
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq31(runs.id, runId)).run();
|
|
28780
29958
|
try {
|
|
28781
29959
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
28782
29960
|
if (!googleClientId || !googleClientSecret) {
|
|
28783
29961
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
28784
29962
|
}
|
|
28785
|
-
const project = db.select().from(projects).where(
|
|
29963
|
+
const project = db.select().from(projects).where(eq31(projects.id, projectId)).get();
|
|
28786
29964
|
if (!project) {
|
|
28787
29965
|
throw new Error(`Project not found: ${projectId}`);
|
|
28788
29966
|
}
|
|
@@ -28806,9 +29984,9 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
28806
29984
|
saveConfigPatch(opts.config);
|
|
28807
29985
|
}
|
|
28808
29986
|
const sitemapUrl = opts.sitemapUrl || conn.sitemapUrl || `https://${project.canonicalDomain}/sitemap.xml`;
|
|
28809
|
-
|
|
29987
|
+
log5.info("sitemap.fetch", { runId, projectId, sitemapUrl });
|
|
28810
29988
|
const urls = await fetchAndParseSitemap(sitemapUrl);
|
|
28811
|
-
|
|
29989
|
+
log5.info("sitemap.parsed", { runId, projectId, urlCount: urls.length, sitemapUrl });
|
|
28812
29990
|
if (urls.length === 0) {
|
|
28813
29991
|
throw new Error("No URLs found in sitemap");
|
|
28814
29992
|
}
|
|
@@ -28823,7 +30001,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
28823
30001
|
const rich = ir.richResultsResult;
|
|
28824
30002
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28825
30003
|
db.insert(gscUrlInspections).values({
|
|
28826
|
-
id:
|
|
30004
|
+
id: crypto30.randomUUID(),
|
|
28827
30005
|
projectId,
|
|
28828
30006
|
syncRunId: runId,
|
|
28829
30007
|
url: pageUrl,
|
|
@@ -28841,16 +30019,16 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
28841
30019
|
createdAt: inspectedAt
|
|
28842
30020
|
}).run();
|
|
28843
30021
|
inspected++;
|
|
28844
|
-
|
|
30022
|
+
log5.info("inspect.url-done", { runId, projectId, url: pageUrl, progress: `${inspected}/${urls.length}` });
|
|
28845
30023
|
} catch (err) {
|
|
28846
30024
|
errors++;
|
|
28847
|
-
|
|
30025
|
+
log5.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
|
|
28848
30026
|
}
|
|
28849
30027
|
if (inspected + errors < urls.length) {
|
|
28850
30028
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
28851
30029
|
}
|
|
28852
30030
|
}
|
|
28853
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
30031
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq31(gscUrlInspections.projectId, projectId)).all();
|
|
28854
30032
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
28855
30033
|
for (const row of allInspections) {
|
|
28856
30034
|
const existing = latestByUrl.get(row.url);
|
|
@@ -28871,9 +30049,9 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
28871
30049
|
}
|
|
28872
30050
|
}
|
|
28873
30051
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
28874
|
-
db.delete(gscCoverageSnapshots).where(
|
|
30052
|
+
db.delete(gscCoverageSnapshots).where(and25(eq31(gscCoverageSnapshots.projectId, projectId), eq31(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
28875
30053
|
db.insert(gscCoverageSnapshots).values({
|
|
28876
|
-
id:
|
|
30054
|
+
id: crypto30.randomUUID(),
|
|
28877
30055
|
projectId,
|
|
28878
30056
|
syncRunId: runId,
|
|
28879
30057
|
date: snapshotDate,
|
|
@@ -28883,20 +30061,20 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
28883
30061
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
28884
30062
|
}).run();
|
|
28885
30063
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
28886
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
28887
|
-
|
|
30064
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(runs.id, runId)).run();
|
|
30065
|
+
log5.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
28888
30066
|
} catch (err) {
|
|
28889
30067
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
28890
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
28891
|
-
|
|
30068
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(runs.id, runId)).run();
|
|
30069
|
+
log5.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
28892
30070
|
throw err;
|
|
28893
30071
|
}
|
|
28894
30072
|
}
|
|
28895
30073
|
|
|
28896
30074
|
// src/bing-inspect-sitemap.ts
|
|
28897
|
-
import
|
|
28898
|
-
import { eq as
|
|
28899
|
-
var
|
|
30075
|
+
import crypto31 from "crypto";
|
|
30076
|
+
import { eq as eq32, desc as desc16 } from "drizzle-orm";
|
|
30077
|
+
var log6 = createLogger("BingInspectSitemap");
|
|
28900
30078
|
function parseBingDate2(value) {
|
|
28901
30079
|
if (!value) return null;
|
|
28902
30080
|
const match = /\/Date\((-?\d+)(?:[-+]\d+)?\)\//.exec(value);
|
|
@@ -28913,9 +30091,9 @@ function isBlockingIssueType2(issueType) {
|
|
|
28913
30091
|
}
|
|
28914
30092
|
async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
28915
30093
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
28916
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
30094
|
+
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq32(runs.id, runId)).run();
|
|
28917
30095
|
try {
|
|
28918
|
-
const project = db.select().from(projects).where(
|
|
30096
|
+
const project = db.select().from(projects).where(eq32(projects.id, projectId)).get();
|
|
28919
30097
|
if (!project) {
|
|
28920
30098
|
throw new Error(`Project not found: ${projectId}`);
|
|
28921
30099
|
}
|
|
@@ -28927,16 +30105,16 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
28927
30105
|
throw new Error('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
|
|
28928
30106
|
}
|
|
28929
30107
|
const sitemapUrl = opts.sitemapUrl ?? `https://${project.canonicalDomain}/sitemap.xml`;
|
|
28930
|
-
|
|
30108
|
+
log6.info("sitemap.fetch", { runId, projectId, sitemapUrl });
|
|
28931
30109
|
const sitemapUrls = await fetchAndParseSitemap(sitemapUrl);
|
|
28932
|
-
|
|
30110
|
+
log6.info("sitemap.parsed", { runId, projectId, urlCount: sitemapUrls.length, sitemapUrl });
|
|
28933
30111
|
if (sitemapUrls.length === 0) {
|
|
28934
30112
|
throw new Error("No URLs found in sitemap");
|
|
28935
30113
|
}
|
|
28936
|
-
const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(
|
|
30114
|
+
const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq32(bingUrlInspections.projectId, projectId)).all();
|
|
28937
30115
|
const trackedUrls = new Set(trackedRows.map((r) => r.url));
|
|
28938
30116
|
const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
|
|
28939
|
-
|
|
30117
|
+
log6.info("sitemap.diff", {
|
|
28940
30118
|
runId,
|
|
28941
30119
|
projectId,
|
|
28942
30120
|
sitemapTotal: sitemapUrls.length,
|
|
@@ -28951,9 +30129,9 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
28951
30129
|
blockedUrls.add(issue.Url);
|
|
28952
30130
|
}
|
|
28953
30131
|
}
|
|
28954
|
-
|
|
30132
|
+
log6.info("crawl-issues.loaded", { runId, projectId, blockedCount: blockedUrls.size });
|
|
28955
30133
|
} catch (err) {
|
|
28956
|
-
|
|
30134
|
+
log6.warn("crawl-issues.lookup-failed", {
|
|
28957
30135
|
runId,
|
|
28958
30136
|
projectId,
|
|
28959
30137
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -28982,7 +30160,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
28982
30160
|
derivedInIndex = false;
|
|
28983
30161
|
}
|
|
28984
30162
|
db.insert(bingUrlInspections).values({
|
|
28985
|
-
id:
|
|
30163
|
+
id: crypto31.randomUUID(),
|
|
28986
30164
|
projectId,
|
|
28987
30165
|
url: pageUrl,
|
|
28988
30166
|
httpCode,
|
|
@@ -28997,7 +30175,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
28997
30175
|
discoveryDate
|
|
28998
30176
|
}).run();
|
|
28999
30177
|
inspected++;
|
|
29000
|
-
|
|
30178
|
+
log6.info("inspect.url-done", {
|
|
29001
30179
|
runId,
|
|
29002
30180
|
projectId,
|
|
29003
30181
|
url: pageUrl,
|
|
@@ -29005,7 +30183,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
29005
30183
|
});
|
|
29006
30184
|
} catch (err) {
|
|
29007
30185
|
errors++;
|
|
29008
|
-
|
|
30186
|
+
log6.error("inspect.url-failed", {
|
|
29009
30187
|
runId,
|
|
29010
30188
|
projectId,
|
|
29011
30189
|
url: pageUrl,
|
|
@@ -29016,7 +30194,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
29016
30194
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
29017
30195
|
}
|
|
29018
30196
|
}
|
|
29019
|
-
const allInspections = db.select().from(bingUrlInspections).where(
|
|
30197
|
+
const allInspections = db.select().from(bingUrlInspections).where(eq32(bingUrlInspections.projectId, projectId)).orderBy(desc16(bingUrlInspections.inspectedAt)).all();
|
|
29020
30198
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
29021
30199
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
29022
30200
|
for (const row of allInspections) {
|
|
@@ -29040,7 +30218,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
29040
30218
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
29041
30219
|
const snapNow = (/* @__PURE__ */ new Date()).toISOString();
|
|
29042
30220
|
db.insert(bingCoverageSnapshots).values({
|
|
29043
|
-
id:
|
|
30221
|
+
id: crypto31.randomUUID(),
|
|
29044
30222
|
projectId,
|
|
29045
30223
|
syncRunId: runId,
|
|
29046
30224
|
date: snapshotDate,
|
|
@@ -29059,8 +30237,8 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
29059
30237
|
}
|
|
29060
30238
|
}).run();
|
|
29061
30239
|
const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
|
|
29062
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
29063
|
-
|
|
30240
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(runs.id, runId)).run();
|
|
30241
|
+
log6.info("inspect.completed", {
|
|
29064
30242
|
runId,
|
|
29065
30243
|
projectId,
|
|
29066
30244
|
inspected,
|
|
@@ -29073,19 +30251,74 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
29073
30251
|
});
|
|
29074
30252
|
} catch (err) {
|
|
29075
30253
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
29076
|
-
db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
29077
|
-
|
|
30254
|
+
db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(runs.id, runId)).run();
|
|
30255
|
+
log6.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
29078
30256
|
throw err;
|
|
29079
30257
|
}
|
|
29080
30258
|
}
|
|
29081
30259
|
|
|
30260
|
+
// src/coverage-refresh.ts
|
|
30261
|
+
import crypto32 from "crypto";
|
|
30262
|
+
import { and as and26, desc as desc17, eq as eq33, inArray as inArray12 } from "drizzle-orm";
|
|
30263
|
+
var log7 = createLogger("CoverageRefresh");
|
|
30264
|
+
var COVERAGE_REFRESH_MIN_INTERVAL_MS = 60 * 60 * 1e3;
|
|
30265
|
+
var ACTIVE_OR_DONE_STATUSES = [
|
|
30266
|
+
RunStatuses.queued,
|
|
30267
|
+
RunStatuses.running,
|
|
30268
|
+
RunStatuses.completed,
|
|
30269
|
+
RunStatuses.partial
|
|
30270
|
+
];
|
|
30271
|
+
var defaultDeps = { executeInspectSitemap };
|
|
30272
|
+
async function maybeRefreshGscCoverage(db, config, projectId, deps = defaultDeps, nowMs = Date.now()) {
|
|
30273
|
+
const project = db.select({ canonicalDomain: projects.canonicalDomain }).from(projects).where(eq33(projects.id, projectId)).get();
|
|
30274
|
+
if (!project) return null;
|
|
30275
|
+
const { clientId, clientSecret } = getGoogleAuthConfig(config);
|
|
30276
|
+
if (!clientId || !clientSecret) return null;
|
|
30277
|
+
const conn = getGoogleConnection(config, project.canonicalDomain, "gsc");
|
|
30278
|
+
if (!conn?.refreshToken || !conn.propertyId) return null;
|
|
30279
|
+
const recent = db.select({ createdAt: runs.createdAt }).from(runs).where(
|
|
30280
|
+
and26(
|
|
30281
|
+
eq33(runs.projectId, projectId),
|
|
30282
|
+
eq33(runs.kind, RunKinds["inspect-sitemap"]),
|
|
30283
|
+
inArray12(runs.status, ACTIVE_OR_DONE_STATUSES)
|
|
30284
|
+
)
|
|
30285
|
+
).orderBy(desc17(runs.createdAt)).limit(1).get();
|
|
30286
|
+
if (recent) {
|
|
30287
|
+
const ageMs = nowMs - Date.parse(recent.createdAt);
|
|
30288
|
+
if (Number.isFinite(ageMs) && ageMs < COVERAGE_REFRESH_MIN_INTERVAL_MS) {
|
|
30289
|
+
log7.info("skip.recent", { projectId, ageMs });
|
|
30290
|
+
return null;
|
|
30291
|
+
}
|
|
30292
|
+
}
|
|
30293
|
+
const runId = crypto32.randomUUID();
|
|
30294
|
+
db.insert(runs).values({
|
|
30295
|
+
id: runId,
|
|
30296
|
+
projectId,
|
|
30297
|
+
kind: RunKinds["inspect-sitemap"],
|
|
30298
|
+
status: RunStatuses.queued,
|
|
30299
|
+
trigger: RunTriggers.scheduled,
|
|
30300
|
+
createdAt: new Date(nowMs).toISOString()
|
|
30301
|
+
}).run();
|
|
30302
|
+
log7.info("refresh.start", { projectId, runId });
|
|
30303
|
+
try {
|
|
30304
|
+
await deps.executeInspectSitemap(db, runId, projectId, { config });
|
|
30305
|
+
} catch (err) {
|
|
30306
|
+
log7.error("refresh.failed", {
|
|
30307
|
+
projectId,
|
|
30308
|
+
runId,
|
|
30309
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30310
|
+
});
|
|
30311
|
+
}
|
|
30312
|
+
return runId;
|
|
30313
|
+
}
|
|
30314
|
+
|
|
29082
30315
|
// src/commoncrawl-sync.ts
|
|
29083
|
-
import
|
|
30316
|
+
import crypto33 from "crypto";
|
|
29084
30317
|
import path11 from "path";
|
|
29085
|
-
import { and as
|
|
29086
|
-
var
|
|
30318
|
+
import { and as and27, eq as eq34, sql as sql14 } from "drizzle-orm";
|
|
30319
|
+
var log8 = createLogger("CommonCrawlSync");
|
|
29087
30320
|
var INSERT_CHUNK_SIZE = 1e4;
|
|
29088
|
-
function
|
|
30321
|
+
function defaultDeps2() {
|
|
29089
30322
|
return {
|
|
29090
30323
|
downloadFile,
|
|
29091
30324
|
queryBacklinks,
|
|
@@ -29095,7 +30328,7 @@ function defaultDeps() {
|
|
|
29095
30328
|
};
|
|
29096
30329
|
}
|
|
29097
30330
|
async function executeReleaseSync(db, syncId, opts) {
|
|
29098
|
-
const deps = { ...
|
|
30331
|
+
const deps = { ...defaultDeps2(), ...opts.deps };
|
|
29099
30332
|
const release = opts.release;
|
|
29100
30333
|
try {
|
|
29101
30334
|
if (!isValidReleaseId(release)) {
|
|
@@ -29108,7 +30341,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
29108
30341
|
phaseDetail: "downloading vertices + edges",
|
|
29109
30342
|
updatedAt: downloadStartedAt,
|
|
29110
30343
|
error: null
|
|
29111
|
-
}).where(
|
|
30344
|
+
}).where(eq34(ccReleaseSyncs.id, syncId)).run();
|
|
29112
30345
|
const paths = ccReleasePaths(release);
|
|
29113
30346
|
const releaseCacheDir = path11.join(deps.cacheDir, release);
|
|
29114
30347
|
const vertexPath = path11.join(releaseCacheDir, paths.vertexFilename);
|
|
@@ -29131,7 +30364,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
29131
30364
|
vertexSha256: vertex.sha256,
|
|
29132
30365
|
edgesSha256: edges.sha256,
|
|
29133
30366
|
updatedAt: downloadFinishedAt
|
|
29134
|
-
}).where(
|
|
30367
|
+
}).where(eq34(ccReleaseSyncs.id, syncId)).run();
|
|
29135
30368
|
const allProjects = db.select().from(projects).all();
|
|
29136
30369
|
const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
|
|
29137
30370
|
let rows = [];
|
|
@@ -29147,15 +30380,15 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
29147
30380
|
}
|
|
29148
30381
|
const queriedAt = deps.now().toISOString();
|
|
29149
30382
|
db.transaction((tx) => {
|
|
29150
|
-
tx.delete(backlinkDomains).where(
|
|
29151
|
-
tx.delete(backlinkSummaries).where(
|
|
30383
|
+
tx.delete(backlinkDomains).where(eq34(backlinkDomains.releaseSyncId, syncId)).run();
|
|
30384
|
+
tx.delete(backlinkSummaries).where(eq34(backlinkSummaries.releaseSyncId, syncId)).run();
|
|
29152
30385
|
const expanded = [];
|
|
29153
30386
|
for (const r of rows) {
|
|
29154
30387
|
const projectIds = projectsByDomain.get(r.targetDomain);
|
|
29155
30388
|
if (!projectIds) continue;
|
|
29156
30389
|
for (const projectId of projectIds) {
|
|
29157
30390
|
expanded.push({
|
|
29158
|
-
id:
|
|
30391
|
+
id: crypto33.randomUUID(),
|
|
29159
30392
|
projectId,
|
|
29160
30393
|
releaseSyncId: syncId,
|
|
29161
30394
|
release,
|
|
@@ -29175,7 +30408,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
29175
30408
|
const projectRows = rowsByProject.get(p.id) ?? [];
|
|
29176
30409
|
const summary = computeSummary(projectRows);
|
|
29177
30410
|
tx.insert(backlinkSummaries).values({
|
|
29178
|
-
id:
|
|
30411
|
+
id: crypto33.randomUUID(),
|
|
29179
30412
|
projectId: p.id,
|
|
29180
30413
|
releaseSyncId: syncId,
|
|
29181
30414
|
release,
|
|
@@ -29207,8 +30440,8 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
29207
30440
|
domainsDiscovered: rows.length,
|
|
29208
30441
|
updatedAt: finishedAt,
|
|
29209
30442
|
error: null
|
|
29210
|
-
}).where(
|
|
29211
|
-
|
|
30443
|
+
}).where(eq34(ccReleaseSyncs.id, syncId)).run();
|
|
30444
|
+
log8.info("sync.completed", {
|
|
29212
30445
|
syncId,
|
|
29213
30446
|
release,
|
|
29214
30447
|
projectsProcessed: allProjects.length,
|
|
@@ -29220,7 +30453,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
29220
30453
|
try {
|
|
29221
30454
|
deps.enqueueAutoExtract({ projectId: p.id, release });
|
|
29222
30455
|
} catch (err) {
|
|
29223
|
-
|
|
30456
|
+
log8.error("auto-extract.enqueue-failed", {
|
|
29224
30457
|
syncId,
|
|
29225
30458
|
release,
|
|
29226
30459
|
projectId: p.id,
|
|
@@ -29237,8 +30470,8 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
29237
30470
|
error: errorMsg,
|
|
29238
30471
|
phaseDetail: null,
|
|
29239
30472
|
updatedAt: finishedAt
|
|
29240
|
-
}).where(
|
|
29241
|
-
|
|
30473
|
+
}).where(eq34(ccReleaseSyncs.id, syncId)).run();
|
|
30474
|
+
log8.error("sync.failed", { syncId, release, error: errorMsg });
|
|
29242
30475
|
throw err;
|
|
29243
30476
|
}
|
|
29244
30477
|
}
|
|
@@ -29271,11 +30504,11 @@ function computeSummary(rows) {
|
|
|
29271
30504
|
}
|
|
29272
30505
|
|
|
29273
30506
|
// src/backlink-extract.ts
|
|
29274
|
-
import
|
|
30507
|
+
import crypto34 from "crypto";
|
|
29275
30508
|
import fs11 from "fs";
|
|
29276
|
-
import { and as
|
|
29277
|
-
var
|
|
29278
|
-
function
|
|
30509
|
+
import { and as and28, desc as desc18, eq as eq35 } from "drizzle-orm";
|
|
30510
|
+
var log9 = createLogger("BacklinkExtract");
|
|
30511
|
+
function defaultDeps3() {
|
|
29279
30512
|
return {
|
|
29280
30513
|
queryBacklinks,
|
|
29281
30514
|
loadDuckdb,
|
|
@@ -29283,15 +30516,15 @@ function defaultDeps2() {
|
|
|
29283
30516
|
};
|
|
29284
30517
|
}
|
|
29285
30518
|
async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
29286
|
-
const deps = { ...
|
|
30519
|
+
const deps = { ...defaultDeps3(), ...opts.deps };
|
|
29287
30520
|
const startedAt = deps.now().toISOString();
|
|
29288
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
30521
|
+
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq35(runs.id, runId)).run();
|
|
29289
30522
|
try {
|
|
29290
|
-
const project = db.select().from(projects).where(
|
|
30523
|
+
const project = db.select().from(projects).where(eq35(projects.id, projectId)).get();
|
|
29291
30524
|
if (!project) {
|
|
29292
30525
|
throw new Error(`Project not found: ${projectId}`);
|
|
29293
30526
|
}
|
|
29294
|
-
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(
|
|
30527
|
+
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq35(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq35(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc18(ccReleaseSyncs.createdAt)).limit(1).get();
|
|
29295
30528
|
if (!sync) {
|
|
29296
30529
|
throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
|
|
29297
30530
|
}
|
|
@@ -29319,11 +30552,11 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
29319
30552
|
const targetDomain = project.canonicalDomain;
|
|
29320
30553
|
db.transaction((tx) => {
|
|
29321
30554
|
tx.delete(backlinkDomains).where(
|
|
29322
|
-
|
|
30555
|
+
and28(eq35(backlinkDomains.projectId, projectId), eq35(backlinkDomains.release, release))
|
|
29323
30556
|
).run();
|
|
29324
30557
|
if (rows.length > 0) {
|
|
29325
30558
|
const values = rows.map((r) => ({
|
|
29326
|
-
id:
|
|
30559
|
+
id: crypto34.randomUUID(),
|
|
29327
30560
|
projectId,
|
|
29328
30561
|
releaseSyncId: syncId,
|
|
29329
30562
|
release,
|
|
@@ -29336,7 +30569,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
29336
30569
|
}
|
|
29337
30570
|
const summary = computeSummary2(rows);
|
|
29338
30571
|
tx.insert(backlinkSummaries).values({
|
|
29339
|
-
id:
|
|
30572
|
+
id: crypto34.randomUUID(),
|
|
29340
30573
|
projectId,
|
|
29341
30574
|
releaseSyncId: syncId,
|
|
29342
30575
|
release,
|
|
@@ -29359,8 +30592,8 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
29359
30592
|
}).run();
|
|
29360
30593
|
});
|
|
29361
30594
|
const finishedAt = deps.now().toISOString();
|
|
29362
|
-
db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
29363
|
-
|
|
30595
|
+
db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq35(runs.id, runId)).run();
|
|
30596
|
+
log9.info("extract.completed", { runId, projectId, release, rows: rows.length });
|
|
29364
30597
|
} catch (err) {
|
|
29365
30598
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
29366
30599
|
const finishedAt = deps.now().toISOString();
|
|
@@ -29368,8 +30601,8 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
29368
30601
|
status: RunStatuses.failed,
|
|
29369
30602
|
error: errorMsg,
|
|
29370
30603
|
finishedAt
|
|
29371
|
-
}).where(
|
|
29372
|
-
|
|
30604
|
+
}).where(eq35(runs.id, runId)).run();
|
|
30605
|
+
log9.error("extract.failed", { runId, projectId, error: errorMsg });
|
|
29373
30606
|
throw err;
|
|
29374
30607
|
}
|
|
29375
30608
|
}
|
|
@@ -29389,18 +30622,18 @@ function computeSummary2(rows) {
|
|
|
29389
30622
|
}
|
|
29390
30623
|
|
|
29391
30624
|
// src/discovery-run.ts
|
|
29392
|
-
import
|
|
29393
|
-
import { and as
|
|
29394
|
-
var
|
|
30625
|
+
import crypto35 from "crypto";
|
|
30626
|
+
import { and as and29, eq as eq36 } from "drizzle-orm";
|
|
30627
|
+
var log10 = createLogger("DiscoveryRun");
|
|
29395
30628
|
var DEFAULT_SEED_COUNT = 30;
|
|
29396
30629
|
var QUERIES_PER_INTENT_BUCKET = 6;
|
|
29397
30630
|
async function executeDiscoveryRun(opts) {
|
|
29398
30631
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29399
|
-
opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
30632
|
+
opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq36(runs.id, opts.runId)).run();
|
|
29400
30633
|
try {
|
|
29401
|
-
const projectRow = opts.db.select().from(projects).where(
|
|
30634
|
+
const projectRow = opts.db.select().from(projects).where(eq36(projects.id, opts.projectId)).get();
|
|
29402
30635
|
if (!projectRow) throw new Error(`Project ${opts.projectId} not found`);
|
|
29403
|
-
const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
30636
|
+
const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, opts.projectId)).all().map((r) => r.domain.toLowerCase());
|
|
29404
30637
|
const canonicalDomains = effectiveDomains({
|
|
29405
30638
|
canonicalDomain: projectRow.canonicalDomain,
|
|
29406
30639
|
ownedDomains: projectRow.ownedDomains
|
|
@@ -29430,8 +30663,8 @@ async function executeDiscoveryRun(opts) {
|
|
|
29430
30663
|
seedProvider: result.seedProvider,
|
|
29431
30664
|
result
|
|
29432
30665
|
});
|
|
29433
|
-
opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
29434
|
-
|
|
30666
|
+
opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq36(runs.id, opts.runId)).run();
|
|
30667
|
+
log10.info("discovery.completed", {
|
|
29435
30668
|
runId: opts.runId,
|
|
29436
30669
|
sessionId: opts.sessionId,
|
|
29437
30670
|
buckets: result.buckets,
|
|
@@ -29439,13 +30672,13 @@ async function executeDiscoveryRun(opts) {
|
|
|
29439
30672
|
});
|
|
29440
30673
|
} catch (err) {
|
|
29441
30674
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
29442
|
-
|
|
30675
|
+
log10.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
|
|
29443
30676
|
markSessionFailed(opts.db, opts.sessionId, errorMsg);
|
|
29444
30677
|
opts.db.update(runs).set({
|
|
29445
30678
|
status: RunStatuses.failed,
|
|
29446
30679
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29447
30680
|
error: errorMsg
|
|
29448
|
-
}).where(
|
|
30681
|
+
}).where(eq36(runs.id, opts.runId)).run();
|
|
29449
30682
|
}
|
|
29450
30683
|
}
|
|
29451
30684
|
function buildDefaultDeps(registry) {
|
|
@@ -29650,13 +30883,13 @@ function writeDiscoveryInsight(db, input) {
|
|
|
29650
30883
|
totalProbes
|
|
29651
30884
|
});
|
|
29652
30885
|
db.transaction((tx) => {
|
|
29653
|
-
tx.update(insights).set({ dismissed: true }).where(
|
|
29654
|
-
|
|
29655
|
-
|
|
29656
|
-
|
|
30886
|
+
tx.update(insights).set({ dismissed: true }).where(and29(
|
|
30887
|
+
eq36(insights.projectId, input.projectId),
|
|
30888
|
+
eq36(insights.type, "discovery.basket-divergence"),
|
|
30889
|
+
eq36(insights.dismissed, false)
|
|
29657
30890
|
)).run();
|
|
29658
30891
|
tx.insert(insights).values({
|
|
29659
|
-
id:
|
|
30892
|
+
id: crypto35.randomUUID(),
|
|
29660
30893
|
projectId: input.projectId,
|
|
29661
30894
|
runId: input.runId,
|
|
29662
30895
|
type: "discovery.basket-divergence",
|
|
@@ -29692,7 +30925,7 @@ function buildDiscoveryInsightTitle(input) {
|
|
|
29692
30925
|
}
|
|
29693
30926
|
|
|
29694
30927
|
// src/commands/backfill.ts
|
|
29695
|
-
import { and as
|
|
30928
|
+
import { and as and30, eq as eq37, inArray as inArray13, isNull, sql as sql15 } from "drizzle-orm";
|
|
29696
30929
|
var SNAPSHOT_BATCH_SIZE = 500;
|
|
29697
30930
|
async function backfillAnswerVisibilityCommand(opts) {
|
|
29698
30931
|
const config = loadConfig();
|
|
@@ -29700,7 +30933,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
29700
30933
|
migrate(db);
|
|
29701
30934
|
const projectFilter = opts?.project?.trim();
|
|
29702
30935
|
const isDryRun = opts?.dryRun === true;
|
|
29703
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(
|
|
30936
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq37(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
29704
30937
|
let examined = 0;
|
|
29705
30938
|
let updated = 0;
|
|
29706
30939
|
let wouldUpdate = 0;
|
|
@@ -29708,10 +30941,10 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
29708
30941
|
let reparsed = 0;
|
|
29709
30942
|
let providerErrors = 0;
|
|
29710
30943
|
if (scopedProjects.length > 0) {
|
|
29711
|
-
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(
|
|
29712
|
-
|
|
29713
|
-
|
|
29714
|
-
)).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(
|
|
30944
|
+
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and30(
|
|
30945
|
+
eq37(runs.kind, RunKinds["answer-visibility"]),
|
|
30946
|
+
inArray13(runs.projectId, scopedProjects.map((project) => project.id))
|
|
30947
|
+
)).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq37(runs.kind, RunKinds["answer-visibility"])).all();
|
|
29715
30948
|
const runIdsByProject = /* @__PURE__ */ new Map();
|
|
29716
30949
|
for (const run of runRows) {
|
|
29717
30950
|
const existing = runIdsByProject.get(run.projectId);
|
|
@@ -29719,7 +30952,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
29719
30952
|
else runIdsByProject.set(run.projectId, [run.id]);
|
|
29720
30953
|
}
|
|
29721
30954
|
for (const project of scopedProjects) {
|
|
29722
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
30955
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq37(competitors.projectId, project.id)).all().map((row) => row.domain);
|
|
29723
30956
|
const runIds = runIdsByProject.get(project.id) ?? [];
|
|
29724
30957
|
if (runIds.length === 0) continue;
|
|
29725
30958
|
const projectDomains = effectiveDomains({
|
|
@@ -29742,7 +30975,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
29742
30975
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
29743
30976
|
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
29744
30977
|
rawResponse: querySnapshots.rawResponse
|
|
29745
|
-
}).from(querySnapshots).where(
|
|
30978
|
+
}).from(querySnapshots).where(inArray13(querySnapshots.runId, batchRunIds)).all();
|
|
29746
30979
|
const pendingUpdates = [];
|
|
29747
30980
|
for (const snapshot of snapshotRows) {
|
|
29748
30981
|
examined++;
|
|
@@ -29807,7 +31040,7 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
29807
31040
|
} else {
|
|
29808
31041
|
db.transaction((tx) => {
|
|
29809
31042
|
for (const update of pendingUpdates) {
|
|
29810
|
-
tx.update(querySnapshots).set(update.patch).where(
|
|
31043
|
+
tx.update(querySnapshots).set(update.patch).where(eq37(querySnapshots.id, update.id)).run();
|
|
29811
31044
|
}
|
|
29812
31045
|
});
|
|
29813
31046
|
updated += pendingUpdates.length;
|
|
@@ -29856,13 +31089,13 @@ No DB writes performed. Re-run without --dry-run to apply.`);
|
|
|
29856
31089
|
function backfillNormalizedPaths(db, opts) {
|
|
29857
31090
|
const baseConditions = [];
|
|
29858
31091
|
if (opts?.projectId) {
|
|
29859
|
-
baseConditions.push(
|
|
31092
|
+
baseConditions.push(eq37(gaTrafficSnapshots.projectId, opts.projectId));
|
|
29860
31093
|
}
|
|
29861
31094
|
const rows = db.select({
|
|
29862
31095
|
id: gaTrafficSnapshots.id,
|
|
29863
31096
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
29864
31097
|
landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
|
|
29865
|
-
}).from(gaTrafficSnapshots).where(baseConditions.length > 0 ?
|
|
31098
|
+
}).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and30(...baseConditions) : void 0).all();
|
|
29866
31099
|
let updated = 0;
|
|
29867
31100
|
let unchanged = 0;
|
|
29868
31101
|
if (rows.length > 0) {
|
|
@@ -29877,7 +31110,7 @@ function backfillNormalizedPaths(db, opts) {
|
|
|
29877
31110
|
unchanged++;
|
|
29878
31111
|
continue;
|
|
29879
31112
|
}
|
|
29880
|
-
tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(
|
|
31113
|
+
tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq37(gaTrafficSnapshots.id, row.id)).run();
|
|
29881
31114
|
updated++;
|
|
29882
31115
|
}
|
|
29883
31116
|
});
|
|
@@ -29891,7 +31124,7 @@ async function backfillNormalizedPathsCommand(opts) {
|
|
|
29891
31124
|
const projectFilter = opts?.project?.trim();
|
|
29892
31125
|
let projectId;
|
|
29893
31126
|
if (projectFilter) {
|
|
29894
|
-
const project = db.select({ id: projects.id }).from(projects).where(
|
|
31127
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq37(projects.name, projectFilter)).get();
|
|
29895
31128
|
if (!project) {
|
|
29896
31129
|
const result2 = {
|
|
29897
31130
|
project: projectFilter,
|
|
@@ -29928,13 +31161,13 @@ async function backfillNormalizedPathsCommand(opts) {
|
|
|
29928
31161
|
function backfillAiReferralPaths(db, opts) {
|
|
29929
31162
|
const baseConditions = [];
|
|
29930
31163
|
if (opts?.projectId) {
|
|
29931
|
-
baseConditions.push(
|
|
31164
|
+
baseConditions.push(eq37(gaAiReferrals.projectId, opts.projectId));
|
|
29932
31165
|
}
|
|
29933
31166
|
const rows = db.select({
|
|
29934
31167
|
id: gaAiReferrals.id,
|
|
29935
31168
|
landingPage: gaAiReferrals.landingPage,
|
|
29936
31169
|
landingPageNormalized: gaAiReferrals.landingPageNormalized
|
|
29937
|
-
}).from(gaAiReferrals).where(baseConditions.length > 0 ?
|
|
31170
|
+
}).from(gaAiReferrals).where(baseConditions.length > 0 ? and30(...baseConditions) : void 0).all();
|
|
29938
31171
|
let updated = 0;
|
|
29939
31172
|
let unchanged = 0;
|
|
29940
31173
|
if (rows.length > 0) {
|
|
@@ -29949,7 +31182,7 @@ function backfillAiReferralPaths(db, opts) {
|
|
|
29949
31182
|
unchanged++;
|
|
29950
31183
|
continue;
|
|
29951
31184
|
}
|
|
29952
|
-
tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(
|
|
31185
|
+
tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq37(gaAiReferrals.id, row.id)).run();
|
|
29953
31186
|
updated++;
|
|
29954
31187
|
}
|
|
29955
31188
|
});
|
|
@@ -29963,7 +31196,7 @@ async function backfillAiReferralPathsCommand(opts) {
|
|
|
29963
31196
|
const projectFilter = opts?.project?.trim();
|
|
29964
31197
|
let projectId;
|
|
29965
31198
|
if (projectFilter) {
|
|
29966
|
-
const project = db.select({ id: projects.id }).from(projects).where(
|
|
31199
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq37(projects.name, projectFilter)).get();
|
|
29967
31200
|
if (!project) {
|
|
29968
31201
|
const result2 = {
|
|
29969
31202
|
project: projectFilter,
|
|
@@ -29999,10 +31232,10 @@ async function backfillAiReferralPathsCommand(opts) {
|
|
|
29999
31232
|
}
|
|
30000
31233
|
function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
30001
31234
|
const isDryRun = opts?.dryRun === true;
|
|
30002
|
-
const project = db.select().from(projects).where(
|
|
31235
|
+
const project = db.select().from(projects).where(eq37(projects.id, projectId)).get();
|
|
30003
31236
|
if (!project) return { examined: 0, updated: 0, mentioned: 0 };
|
|
30004
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
30005
|
-
const runRows = db.select({ id: runs.id }).from(runs).where(
|
|
31237
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq37(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
31238
|
+
const runRows = db.select({ id: runs.id }).from(runs).where(and30(eq37(runs.kind, RunKinds["answer-visibility"]), eq37(runs.projectId, projectId))).all();
|
|
30006
31239
|
const runIds = runRows.map((r) => r.id);
|
|
30007
31240
|
let examined = 0;
|
|
30008
31241
|
let updated = 0;
|
|
@@ -30030,7 +31263,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
|
30030
31263
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
30031
31264
|
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
30032
31265
|
rawResponse: querySnapshots.rawResponse
|
|
30033
|
-
}).from(querySnapshots).where(
|
|
31266
|
+
}).from(querySnapshots).where(inArray13(querySnapshots.runId, batchRunIds)).all();
|
|
30034
31267
|
const pendingUpdates = [];
|
|
30035
31268
|
for (const snapshot of snapshotRows) {
|
|
30036
31269
|
examined++;
|
|
@@ -30074,7 +31307,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
|
30074
31307
|
} else {
|
|
30075
31308
|
db.transaction((tx) => {
|
|
30076
31309
|
for (const update of pendingUpdates) {
|
|
30077
|
-
tx.update(querySnapshots).set(update.patch).where(
|
|
31310
|
+
tx.update(querySnapshots).set(update.patch).where(eq37(querySnapshots.id, update.id)).run();
|
|
30078
31311
|
}
|
|
30079
31312
|
});
|
|
30080
31313
|
updated += pendingUpdates.length;
|
|
@@ -30089,7 +31322,7 @@ async function backfillAnswerMentionsCommand(opts) {
|
|
|
30089
31322
|
migrate(db);
|
|
30090
31323
|
const projectFilter = opts?.project?.trim();
|
|
30091
31324
|
const isDryRun = opts?.dryRun === true;
|
|
30092
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(
|
|
31325
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq37(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
30093
31326
|
let examined = 0;
|
|
30094
31327
|
let updated = 0;
|
|
30095
31328
|
let wouldUpdate = 0;
|
|
@@ -30149,7 +31382,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
30149
31382
|
return result;
|
|
30150
31383
|
}
|
|
30151
31384
|
async function backfillInsightsCommand(project, opts) {
|
|
30152
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
31385
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-V4SWVKEQ.js");
|
|
30153
31386
|
const config = loadConfig();
|
|
30154
31387
|
const db = createClient(config.database);
|
|
30155
31388
|
migrate(db);
|
|
@@ -30308,7 +31541,7 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
30308
31541
|
const config = loadConfig();
|
|
30309
31542
|
const db = createClient(config.database);
|
|
30310
31543
|
migrate(db);
|
|
30311
|
-
const project = db.select().from(projects).where(
|
|
31544
|
+
const project = db.select().from(projects).where(eq37(projects.name, opts.project)).get();
|
|
30312
31545
|
if (!project) {
|
|
30313
31546
|
throw new Error(`Project "${opts.project}" not found`);
|
|
30314
31547
|
}
|
|
@@ -30319,17 +31552,17 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
30319
31552
|
process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
|
|
30320
31553
|
`);
|
|
30321
31554
|
}
|
|
30322
|
-
const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(
|
|
30323
|
-
|
|
30324
|
-
|
|
31555
|
+
const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and30(
|
|
31556
|
+
eq37(auditLog.projectId, project.id),
|
|
31557
|
+
inArray13(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
|
|
30325
31558
|
)).orderBy(auditLog.createdAt).all();
|
|
30326
31559
|
const history = replayQueryAuditLog(events);
|
|
30327
31560
|
const orphanRuns = db.select({
|
|
30328
31561
|
runId: runs.id,
|
|
30329
31562
|
createdAt: runs.createdAt,
|
|
30330
31563
|
location: runs.location
|
|
30331
|
-
}).from(runs).innerJoin(querySnapshots,
|
|
30332
|
-
|
|
31564
|
+
}).from(runs).innerJoin(querySnapshots, eq37(querySnapshots.runId, runs.id)).where(and30(
|
|
31565
|
+
eq37(runs.projectId, project.id),
|
|
30333
31566
|
isNull(querySnapshots.queryId),
|
|
30334
31567
|
isNull(querySnapshots.queryText)
|
|
30335
31568
|
)).groupBy(runs.id).orderBy(runs.createdAt).all();
|
|
@@ -30351,8 +31584,8 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
30351
31584
|
provider: querySnapshots.provider,
|
|
30352
31585
|
createdAt: querySnapshots.createdAt,
|
|
30353
31586
|
answerText: querySnapshots.answerText
|
|
30354
|
-
}).from(querySnapshots).where(
|
|
30355
|
-
|
|
31587
|
+
}).from(querySnapshots).where(and30(
|
|
31588
|
+
eq37(querySnapshots.runId, run.runId),
|
|
30356
31589
|
isNull(querySnapshots.queryId),
|
|
30357
31590
|
isNull(querySnapshots.queryText)
|
|
30358
31591
|
)).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
|
|
@@ -30418,7 +31651,7 @@ async function backfillSnapshotAttributionCommand(opts) {
|
|
|
30418
31651
|
if (!isDryRun && updates.length > 0) {
|
|
30419
31652
|
db.transaction((tx) => {
|
|
30420
31653
|
for (const u of updates) {
|
|
30421
|
-
tx.update(querySnapshots).set({ queryText: u.queryText }).where(
|
|
31654
|
+
tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq37(querySnapshots.id, u.id)).run();
|
|
30422
31655
|
}
|
|
30423
31656
|
});
|
|
30424
31657
|
}
|
|
@@ -30492,7 +31725,7 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
30492
31725
|
const projectFilter = opts?.project?.trim();
|
|
30493
31726
|
const isDryRun = opts?.dryRun === true;
|
|
30494
31727
|
const isJson = opts?.format === "json";
|
|
30495
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(
|
|
31728
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq37(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
30496
31729
|
if (scopedProjects.length === 0) {
|
|
30497
31730
|
if (projectFilter && !isJson) {
|
|
30498
31731
|
process.stderr.write(`No project named "${projectFilter}".
|
|
@@ -30517,9 +31750,9 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
30517
31750
|
dryRun: isDryRun,
|
|
30518
31751
|
byBot: {}
|
|
30519
31752
|
};
|
|
30520
|
-
const unknownCountRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(
|
|
30521
|
-
|
|
30522
|
-
|
|
31753
|
+
const unknownCountRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(and30(
|
|
31754
|
+
eq37(rawEventSamples.eventType, "unknown"),
|
|
31755
|
+
inArray13(rawEventSamples.projectId, projectIds)
|
|
30523
31756
|
)).get();
|
|
30524
31757
|
result.unknownBefore = Number(unknownCountRow?.n ?? 0);
|
|
30525
31758
|
const unknownSamples = db.select({
|
|
@@ -30530,9 +31763,9 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
30530
31763
|
userAgent: rawEventSamples.userAgent,
|
|
30531
31764
|
pathNormalized: rawEventSamples.pathNormalized,
|
|
30532
31765
|
status: rawEventSamples.status
|
|
30533
|
-
}).from(rawEventSamples).where(
|
|
30534
|
-
|
|
30535
|
-
|
|
31766
|
+
}).from(rawEventSamples).where(and30(
|
|
31767
|
+
eq37(rawEventSamples.eventType, "unknown"),
|
|
31768
|
+
inArray13(rawEventSamples.projectId, projectIds)
|
|
30536
31769
|
)).all();
|
|
30537
31770
|
result.examined = unknownSamples.length;
|
|
30538
31771
|
if (unknownSamples.length === 0) {
|
|
@@ -30570,7 +31803,7 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
30570
31803
|
result.reclassified++;
|
|
30571
31804
|
result.byBot[classified.botId] = (result.byBot[classified.botId] ?? 0) + 1;
|
|
30572
31805
|
if (isDryRun) continue;
|
|
30573
|
-
db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(
|
|
31806
|
+
db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq37(rawEventSamples.id, snap.id)).run();
|
|
30574
31807
|
const tsHour = new Date(snap.ts);
|
|
30575
31808
|
tsHour.setUTCMinutes(0, 0, 0);
|
|
30576
31809
|
if (userFetch) {
|
|
@@ -30634,9 +31867,9 @@ async function backfillTrafficClassificationCommand(opts) {
|
|
|
30634
31867
|
}
|
|
30635
31868
|
}
|
|
30636
31869
|
if (!isDryRun) {
|
|
30637
|
-
const afterRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(
|
|
30638
|
-
|
|
30639
|
-
|
|
31870
|
+
const afterRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(and30(
|
|
31871
|
+
eq37(rawEventSamples.eventType, "unknown"),
|
|
31872
|
+
inArray13(rawEventSamples.projectId, projectIds)
|
|
30640
31873
|
)).get();
|
|
30641
31874
|
result.unknownAfter = Number(afterRow?.n ?? 0);
|
|
30642
31875
|
} else {
|
|
@@ -30669,6 +31902,333 @@ No DB writes performed. Re-run without --dry-run to apply.`);
|
|
|
30669
31902
|
}
|
|
30670
31903
|
}
|
|
30671
31904
|
|
|
31905
|
+
// src/commands/skills.ts
|
|
31906
|
+
import crypto36 from "crypto";
|
|
31907
|
+
import fs12 from "fs";
|
|
31908
|
+
import os6 from "os";
|
|
31909
|
+
import path12 from "path";
|
|
31910
|
+
import { fileURLToPath } from "url";
|
|
31911
|
+
var BUNDLED_SKILL_NAMES = ["canonry", "aero"];
|
|
31912
|
+
function resolveBundledSkillsRoot(pkgDir) {
|
|
31913
|
+
const here = pkgDir ?? path12.dirname(fileURLToPath(import.meta.url));
|
|
31914
|
+
const candidates = [
|
|
31915
|
+
path12.join(here, "../assets/agent-workspace/skills"),
|
|
31916
|
+
path12.join(here, "../../assets/agent-workspace/skills"),
|
|
31917
|
+
path12.join(here, "../../../../skills")
|
|
31918
|
+
];
|
|
31919
|
+
for (const candidate of candidates) {
|
|
31920
|
+
if (BUNDLED_SKILL_NAMES.every((name) => fs12.existsSync(path12.join(candidate, name, "SKILL.md")))) {
|
|
31921
|
+
return candidate;
|
|
31922
|
+
}
|
|
31923
|
+
}
|
|
31924
|
+
throw new CliError({
|
|
31925
|
+
code: "INTERNAL_ERROR",
|
|
31926
|
+
message: `Bundled skills not found. Searched:
|
|
31927
|
+
${candidates.join("\n ")}`,
|
|
31928
|
+
exitCode: 2
|
|
31929
|
+
});
|
|
31930
|
+
}
|
|
31931
|
+
function parseDescription(content) {
|
|
31932
|
+
const fmMatch = /^---\n([\s\S]*?)\n---/.exec(content);
|
|
31933
|
+
if (!fmMatch) return "";
|
|
31934
|
+
const descMatch = /^description:\s*(\S.*)$/m.exec(fmMatch[1]);
|
|
31935
|
+
if (!descMatch) return "";
|
|
31936
|
+
return descMatch[1].replace(/^["']|["']$/g, "").trim();
|
|
31937
|
+
}
|
|
31938
|
+
function getBundledSkills(pkgDir) {
|
|
31939
|
+
const root = resolveBundledSkillsRoot(pkgDir);
|
|
31940
|
+
return BUNDLED_SKILL_NAMES.map((name) => {
|
|
31941
|
+
const skillDir = path12.join(root, name);
|
|
31942
|
+
const skillFile = path12.join(skillDir, "SKILL.md");
|
|
31943
|
+
const content = fs12.readFileSync(skillFile, "utf-8");
|
|
31944
|
+
return { name, description: parseDescription(content), bundledPath: skillDir };
|
|
31945
|
+
});
|
|
31946
|
+
}
|
|
31947
|
+
function walkRelative(dir, prefix = "") {
|
|
31948
|
+
const out = [];
|
|
31949
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
31950
|
+
const rel = prefix ? path12.join(prefix, entry.name) : entry.name;
|
|
31951
|
+
const full = path12.join(dir, entry.name);
|
|
31952
|
+
if (entry.isDirectory()) {
|
|
31953
|
+
out.push(...walkRelative(full, rel));
|
|
31954
|
+
} else if (entry.isFile()) {
|
|
31955
|
+
out.push(rel);
|
|
31956
|
+
}
|
|
31957
|
+
}
|
|
31958
|
+
return out.sort();
|
|
31959
|
+
}
|
|
31960
|
+
function sha256File(filePath) {
|
|
31961
|
+
return crypto36.createHash("sha256").update(fs12.readFileSync(filePath)).digest("hex");
|
|
31962
|
+
}
|
|
31963
|
+
function readSkillManifest(skillDir) {
|
|
31964
|
+
try {
|
|
31965
|
+
return coerceSkillManifest(JSON.parse(fs12.readFileSync(path12.join(skillDir, SKILL_MANIFEST_FILENAME), "utf-8")));
|
|
31966
|
+
} catch {
|
|
31967
|
+
return null;
|
|
31968
|
+
}
|
|
31969
|
+
}
|
|
31970
|
+
function writeSkillManifest(skillDir, manifest) {
|
|
31971
|
+
fs12.writeFileSync(path12.join(skillDir, SKILL_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}
|
|
31972
|
+
`, "utf-8");
|
|
31973
|
+
}
|
|
31974
|
+
function reconcileSkillTree(srcDir, destDir, manifest, force) {
|
|
31975
|
+
const result = { added: [], updated: [], unchanged: [], conflicts: [], bundledHashes: /* @__PURE__ */ new Map() };
|
|
31976
|
+
for (const rel of walkRelative(srcDir)) {
|
|
31977
|
+
const srcPath = path12.join(srcDir, rel);
|
|
31978
|
+
const destPath = path12.join(destDir, rel);
|
|
31979
|
+
const bundledHash = sha256File(srcPath);
|
|
31980
|
+
result.bundledHashes.set(rel, bundledHash);
|
|
31981
|
+
const installedHash = fs12.existsSync(destPath) ? sha256File(destPath) : void 0;
|
|
31982
|
+
const state = classifySkillFile({ bundledHash, installedHash, manifestHash: manifest?.files[rel] });
|
|
31983
|
+
switch (state) {
|
|
31984
|
+
case "missing":
|
|
31985
|
+
fs12.mkdirSync(path12.dirname(destPath), { recursive: true });
|
|
31986
|
+
fs12.copyFileSync(srcPath, destPath);
|
|
31987
|
+
result.added.push(rel);
|
|
31988
|
+
break;
|
|
31989
|
+
case "unchanged":
|
|
31990
|
+
result.unchanged.push(rel);
|
|
31991
|
+
break;
|
|
31992
|
+
case "stale":
|
|
31993
|
+
fs12.copyFileSync(srcPath, destPath);
|
|
31994
|
+
result.updated.push(rel);
|
|
31995
|
+
break;
|
|
31996
|
+
case "edited":
|
|
31997
|
+
if (force) {
|
|
31998
|
+
fs12.copyFileSync(srcPath, destPath);
|
|
31999
|
+
result.updated.push(rel);
|
|
32000
|
+
} else {
|
|
32001
|
+
result.conflicts.push(rel);
|
|
32002
|
+
}
|
|
32003
|
+
break;
|
|
32004
|
+
}
|
|
32005
|
+
}
|
|
32006
|
+
return result;
|
|
32007
|
+
}
|
|
32008
|
+
function buildManifest(skillName, recon, prior) {
|
|
32009
|
+
const conflicts = new Set(recon.conflicts);
|
|
32010
|
+
const files = {};
|
|
32011
|
+
for (const [rel, bundledHash] of recon.bundledHashes) {
|
|
32012
|
+
files[rel] = conflicts.has(rel) ? prior?.files[rel] ?? bundledHash : bundledHash;
|
|
32013
|
+
}
|
|
32014
|
+
return { skill: skillName, version: PACKAGE_VERSION, files };
|
|
32015
|
+
}
|
|
32016
|
+
function describeChanges(recon) {
|
|
32017
|
+
const parts = [];
|
|
32018
|
+
if (recon.added.length > 0) parts.push(`${recon.added.length} added`);
|
|
32019
|
+
if (recon.updated.length > 0) parts.push(`${recon.updated.length} refreshed`);
|
|
32020
|
+
return parts.length > 0 ? parts.join(", ") : "no changes";
|
|
32021
|
+
}
|
|
32022
|
+
function buildClaudeMessage(name, status, recon) {
|
|
32023
|
+
const rel = `.claude/skills/${name}`;
|
|
32024
|
+
let message = status === "installed" ? `Installed ${rel}` : status === "updated" ? `Updated ${rel} (${describeChanges(recon)})` : `Already installed: ${rel}`;
|
|
32025
|
+
if (recon.conflicts.length > 0) {
|
|
32026
|
+
message += ` \u2014 ${recon.conflicts.length} file(s) differ from the bundle (local edits kept; pass --force to overwrite)`;
|
|
32027
|
+
}
|
|
32028
|
+
return message;
|
|
32029
|
+
}
|
|
32030
|
+
function installClaudeSkill(skill, targetDir, force) {
|
|
32031
|
+
const targetPath = path12.join(targetDir, ".claude", "skills", skill.name);
|
|
32032
|
+
const existedBefore = fs12.existsSync(path12.join(targetPath, "SKILL.md"));
|
|
32033
|
+
const priorManifest = readSkillManifest(targetPath);
|
|
32034
|
+
fs12.mkdirSync(targetPath, { recursive: true });
|
|
32035
|
+
const recon = reconcileSkillTree(skill.bundledPath, targetPath, priorManifest, force);
|
|
32036
|
+
writeSkillManifest(targetPath, buildManifest(skill.name, recon, priorManifest));
|
|
32037
|
+
const changed = recon.added.length + recon.updated.length;
|
|
32038
|
+
const status = !existedBefore ? "installed" : changed > 0 ? "updated" : "already-installed";
|
|
32039
|
+
return {
|
|
32040
|
+
skill: skill.name,
|
|
32041
|
+
client: CodingAgents.claude,
|
|
32042
|
+
targetPath,
|
|
32043
|
+
status,
|
|
32044
|
+
message: buildClaudeMessage(skill.name, status, recon),
|
|
32045
|
+
added: recon.added,
|
|
32046
|
+
updated: recon.updated,
|
|
32047
|
+
unchanged: recon.unchanged,
|
|
32048
|
+
conflicts: recon.conflicts
|
|
32049
|
+
};
|
|
32050
|
+
}
|
|
32051
|
+
function installCodexSymlink(skill, targetDir, force) {
|
|
32052
|
+
const codexPath = path12.join(targetDir, ".codex", "skills", skill.name);
|
|
32053
|
+
const claudePath = path12.join(targetDir, ".claude", "skills", skill.name);
|
|
32054
|
+
const linkTarget = path12.relative(path12.dirname(codexPath), claudePath);
|
|
32055
|
+
fs12.mkdirSync(path12.dirname(codexPath), { recursive: true });
|
|
32056
|
+
let stat;
|
|
32057
|
+
try {
|
|
32058
|
+
stat = fs12.lstatSync(codexPath);
|
|
32059
|
+
} catch {
|
|
32060
|
+
stat = void 0;
|
|
32061
|
+
}
|
|
32062
|
+
if (stat?.isSymbolicLink()) {
|
|
32063
|
+
const existing = fs12.readlinkSync(codexPath);
|
|
32064
|
+
if (existing === linkTarget) {
|
|
32065
|
+
return {
|
|
32066
|
+
skill: skill.name,
|
|
32067
|
+
client: CodingAgents.codex,
|
|
32068
|
+
targetPath: codexPath,
|
|
32069
|
+
status: "already-linked",
|
|
32070
|
+
message: `Already linked: .codex/skills/${skill.name}`
|
|
32071
|
+
};
|
|
32072
|
+
}
|
|
32073
|
+
if (!force) {
|
|
32074
|
+
throw new CliError({
|
|
32075
|
+
code: "VALIDATION_ERROR",
|
|
32076
|
+
message: `.codex/skills/${skill.name} is a symlink pointing elsewhere (${existing}). Pass --force to relink.`,
|
|
32077
|
+
details: { skill: skill.name, targetPath: codexPath, existingTarget: existing },
|
|
32078
|
+
exitCode: 1
|
|
32079
|
+
});
|
|
32080
|
+
}
|
|
32081
|
+
fs12.unlinkSync(codexPath);
|
|
32082
|
+
fs12.symlinkSync(linkTarget, codexPath);
|
|
32083
|
+
return {
|
|
32084
|
+
skill: skill.name,
|
|
32085
|
+
client: CodingAgents.codex,
|
|
32086
|
+
targetPath: codexPath,
|
|
32087
|
+
status: "relinked",
|
|
32088
|
+
message: `Relinked .codex/skills/${skill.name} \u2192 ${linkTarget}`
|
|
32089
|
+
};
|
|
32090
|
+
}
|
|
32091
|
+
if (stat) {
|
|
32092
|
+
if (!force) {
|
|
32093
|
+
throw new CliError({
|
|
32094
|
+
code: "VALIDATION_ERROR",
|
|
32095
|
+
message: `.codex/skills/${skill.name} exists but is not a symlink. Pass --force to replace.`,
|
|
32096
|
+
details: { skill: skill.name, targetPath: codexPath },
|
|
32097
|
+
exitCode: 1
|
|
32098
|
+
});
|
|
32099
|
+
}
|
|
32100
|
+
fs12.rmSync(codexPath, { recursive: true, force: true });
|
|
32101
|
+
}
|
|
32102
|
+
fs12.symlinkSync(linkTarget, codexPath);
|
|
32103
|
+
return {
|
|
32104
|
+
skill: skill.name,
|
|
32105
|
+
client: CodingAgents.codex,
|
|
32106
|
+
targetPath: codexPath,
|
|
32107
|
+
status: stat ? "relinked" : "linked",
|
|
32108
|
+
message: stat ? `Replaced and linked .codex/skills/${skill.name} \u2192 ${linkTarget}` : `Linked .codex/skills/${skill.name} \u2192 ${linkTarget}`
|
|
32109
|
+
};
|
|
32110
|
+
}
|
|
32111
|
+
function buildSummaryMessage(results) {
|
|
32112
|
+
const counts = {};
|
|
32113
|
+
for (const r of results) counts[r.status] = (counts[r.status] ?? 0) + 1;
|
|
32114
|
+
const parts = Object.entries(counts).map(([status, n]) => `${n} ${status}`);
|
|
32115
|
+
let message = `Skills install summary: ${parts.join(", ")}.`;
|
|
32116
|
+
const totalConflicts = results.reduce((sum, r) => sum + (r.conflicts?.length ?? 0), 0);
|
|
32117
|
+
if (totalConflicts > 0) {
|
|
32118
|
+
message += ` ${totalConflicts} file(s) differ from the bundle and were kept \u2014 pass --force to overwrite local edits.`;
|
|
32119
|
+
}
|
|
32120
|
+
return message;
|
|
32121
|
+
}
|
|
32122
|
+
function getBundledSkillSnapshots(pkgDir) {
|
|
32123
|
+
return getBundledSkills(pkgDir).map((skill) => {
|
|
32124
|
+
const files = {};
|
|
32125
|
+
for (const rel of walkRelative(skill.bundledPath)) {
|
|
32126
|
+
files[rel] = sha256File(path12.join(skill.bundledPath, rel));
|
|
32127
|
+
}
|
|
32128
|
+
return { name: skill.name, version: PACKAGE_VERSION, files };
|
|
32129
|
+
});
|
|
32130
|
+
}
|
|
32131
|
+
async function installSkills(opts = {}) {
|
|
32132
|
+
const targetDir = opts.user ? os6.homedir() : path12.resolve(opts.dir ?? process.cwd());
|
|
32133
|
+
const client = opts.client ?? SkillsClients.all;
|
|
32134
|
+
const force = opts.force ?? false;
|
|
32135
|
+
const allSkills = getBundledSkills();
|
|
32136
|
+
const requestedNames = opts.skills && opts.skills.length > 0 ? opts.skills : allSkills.map((s) => s.name);
|
|
32137
|
+
const knownNames = new Set(allSkills.map((s) => s.name));
|
|
32138
|
+
const unknown = requestedNames.filter((n) => !knownNames.has(n));
|
|
32139
|
+
if (unknown.length > 0) {
|
|
32140
|
+
throw new CliError({
|
|
32141
|
+
code: "VALIDATION_ERROR",
|
|
32142
|
+
message: `Unknown skill(s): ${unknown.join(", ")}. Available: ${[...knownNames].join(", ")}`,
|
|
32143
|
+
details: { unknownSkills: unknown, availableSkills: [...knownNames] },
|
|
32144
|
+
exitCode: 1
|
|
32145
|
+
});
|
|
32146
|
+
}
|
|
32147
|
+
const skillsToInstall = allSkills.filter((s) => requestedNames.includes(s.name));
|
|
32148
|
+
fs12.mkdirSync(targetDir, { recursive: true });
|
|
32149
|
+
const results = [];
|
|
32150
|
+
for (const skill of skillsToInstall) {
|
|
32151
|
+
results.push(installClaudeSkill(skill, targetDir, force));
|
|
32152
|
+
if (client !== SkillsClients.claude) {
|
|
32153
|
+
results.push(installCodexSymlink(skill, targetDir, force));
|
|
32154
|
+
}
|
|
32155
|
+
}
|
|
32156
|
+
return {
|
|
32157
|
+
targetDir,
|
|
32158
|
+
results,
|
|
32159
|
+
message: buildSummaryMessage(results)
|
|
32160
|
+
};
|
|
32161
|
+
}
|
|
32162
|
+
async function listSkills(opts = {}) {
|
|
32163
|
+
const skills = getBundledSkills();
|
|
32164
|
+
if (opts.format === "json") {
|
|
32165
|
+
console.log(JSON.stringify({
|
|
32166
|
+
skills: skills.map((s) => ({
|
|
32167
|
+
name: s.name,
|
|
32168
|
+
description: s.description,
|
|
32169
|
+
claudePath: `.claude/skills/${s.name}`,
|
|
32170
|
+
codexPath: `.codex/skills/${s.name}`
|
|
32171
|
+
}))
|
|
32172
|
+
}, null, 2));
|
|
32173
|
+
return;
|
|
32174
|
+
}
|
|
32175
|
+
console.log("Bundled canonry skills:\n");
|
|
32176
|
+
for (const skill of skills) {
|
|
32177
|
+
console.log(` ${skill.name}`);
|
|
32178
|
+
if (skill.description) console.log(` ${skill.description}`);
|
|
32179
|
+
console.log(` Claude: .claude/skills/${skill.name}/`);
|
|
32180
|
+
console.log(` Codex: .codex/skills/${skill.name} (symlink \u2192 ../../.claude/skills/${skill.name})`);
|
|
32181
|
+
console.log();
|
|
32182
|
+
}
|
|
32183
|
+
}
|
|
32184
|
+
function emitInstallSummary(summary, format) {
|
|
32185
|
+
if (format === "json") {
|
|
32186
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
32187
|
+
return;
|
|
32188
|
+
}
|
|
32189
|
+
for (const r of summary.results) console.log(r.message);
|
|
32190
|
+
console.log(`
|
|
32191
|
+
Target: ${summary.targetDir}`);
|
|
32192
|
+
console.log(summary.message);
|
|
32193
|
+
}
|
|
32194
|
+
function getMissingUserSkillsNudge(home) {
|
|
32195
|
+
if (!home) return null;
|
|
32196
|
+
const skillsBase = path12.join(home, ".claude", "skills");
|
|
32197
|
+
const installed = [];
|
|
32198
|
+
const missing = [];
|
|
32199
|
+
for (const name of BUNDLED_SKILL_NAMES) {
|
|
32200
|
+
const skillFile = path12.join(skillsBase, name, "SKILL.md");
|
|
32201
|
+
if (existsSafe(skillFile)) installed.push(name);
|
|
32202
|
+
else missing.push(name);
|
|
32203
|
+
}
|
|
32204
|
+
if (missing.length === 0) return null;
|
|
32205
|
+
const fix = missing.length === BUNDLED_SKILL_NAMES.length ? "canonry skills install --user" : `canonry skills install ${missing.join(" ")} --user`;
|
|
32206
|
+
return {
|
|
32207
|
+
message: `Tip: ${missing.join(" + ")} skill${missing.length === 1 ? "" : "s"} not installed in ~/.claude/skills/. Run \`${fix}\` so Claude/Codex sessions on this host auto-load the canonry reference docs.`,
|
|
32208
|
+
missing,
|
|
32209
|
+
installed
|
|
32210
|
+
};
|
|
32211
|
+
}
|
|
32212
|
+
function existsSafe(p) {
|
|
32213
|
+
try {
|
|
32214
|
+
return fs12.existsSync(p);
|
|
32215
|
+
} catch {
|
|
32216
|
+
return false;
|
|
32217
|
+
}
|
|
32218
|
+
}
|
|
32219
|
+
function parseSkillsClient(value) {
|
|
32220
|
+
if (!value) return SkillsClients.all;
|
|
32221
|
+
const parsed = skillsClientSchema.safeParse(value);
|
|
32222
|
+
if (parsed.success) return parsed.data;
|
|
32223
|
+
const allowed = skillsClientSchema.options;
|
|
32224
|
+
throw new CliError({
|
|
32225
|
+
code: "VALIDATION_ERROR",
|
|
32226
|
+
message: `Invalid --client value "${value}". Must be one of: ${allowed.join(", ")}`,
|
|
32227
|
+
details: { flag: "client", value, allowed },
|
|
32228
|
+
exitCode: 1
|
|
32229
|
+
});
|
|
32230
|
+
}
|
|
32231
|
+
|
|
30672
32232
|
// src/provider-registry.ts
|
|
30673
32233
|
var ProviderRegistry = class {
|
|
30674
32234
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -30722,8 +32282,8 @@ var ProviderRegistry = class {
|
|
|
30722
32282
|
|
|
30723
32283
|
// src/scheduler.ts
|
|
30724
32284
|
import cron from "node-cron";
|
|
30725
|
-
import { and as
|
|
30726
|
-
var
|
|
32285
|
+
import { and as and31, eq as eq38 } from "drizzle-orm";
|
|
32286
|
+
var log11 = createLogger("Scheduler");
|
|
30727
32287
|
function taskKey(projectId, kind) {
|
|
30728
32288
|
return `${projectId}::${kind}`;
|
|
30729
32289
|
}
|
|
@@ -30737,16 +32297,16 @@ var Scheduler = class {
|
|
|
30737
32297
|
}
|
|
30738
32298
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
30739
32299
|
start() {
|
|
30740
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
32300
|
+
const allSchedules = this.db.select().from(schedules).where(eq38(schedules.enabled, true)).all();
|
|
30741
32301
|
for (const schedule of allSchedules) {
|
|
30742
32302
|
const missedRunAt = schedule.nextRunAt;
|
|
30743
32303
|
this.registerCronTask(schedule);
|
|
30744
32304
|
if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
|
|
30745
|
-
|
|
32305
|
+
log11.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
|
|
30746
32306
|
this.triggerRun(schedule.id, schedule.projectId, schedule.kind);
|
|
30747
32307
|
}
|
|
30748
32308
|
}
|
|
30749
|
-
|
|
32309
|
+
log11.info("started", { scheduleCount: allSchedules.length });
|
|
30750
32310
|
}
|
|
30751
32311
|
/** Stop all cron tasks for graceful shutdown. */
|
|
30752
32312
|
stop() {
|
|
@@ -30767,7 +32327,7 @@ var Scheduler = class {
|
|
|
30767
32327
|
this.stopTask(key, existing, "Stopped");
|
|
30768
32328
|
this.tasks.delete(key);
|
|
30769
32329
|
}
|
|
30770
|
-
const schedule = this.db.select().from(schedules).where(
|
|
32330
|
+
const schedule = this.db.select().from(schedules).where(and31(eq38(schedules.projectId, projectId), eq38(schedules.kind, kind))).get();
|
|
30771
32331
|
if (schedule && schedule.enabled) {
|
|
30772
32332
|
this.registerCronTask(schedule);
|
|
30773
32333
|
}
|
|
@@ -30790,13 +32350,13 @@ var Scheduler = class {
|
|
|
30790
32350
|
stopTask(key, task, verb) {
|
|
30791
32351
|
void task.stop();
|
|
30792
32352
|
void task.destroy();
|
|
30793
|
-
|
|
32353
|
+
log11.info(`task.${verb.toLowerCase()}`, { key });
|
|
30794
32354
|
}
|
|
30795
32355
|
registerCronTask(schedule) {
|
|
30796
32356
|
const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
|
|
30797
32357
|
const kind = schedule.kind;
|
|
30798
32358
|
if (!cron.validate(cronExpr)) {
|
|
30799
|
-
|
|
32359
|
+
log11.error("cron.invalid", { projectId, kind, cronExpr });
|
|
30800
32360
|
return;
|
|
30801
32361
|
}
|
|
30802
32362
|
const task = cron.schedule(cronExpr, () => {
|
|
@@ -30808,43 +32368,43 @@ var Scheduler = class {
|
|
|
30808
32368
|
this.db.update(schedules).set({
|
|
30809
32369
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
30810
32370
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
30811
|
-
}).where(
|
|
32371
|
+
}).where(eq38(schedules.id, scheduleId)).run();
|
|
30812
32372
|
const label = schedule.preset ?? cronExpr;
|
|
30813
|
-
|
|
32373
|
+
log11.info("cron.registered", { projectId, kind, schedule: label, timezone });
|
|
30814
32374
|
}
|
|
30815
32375
|
triggerRun(scheduleId, projectId, kind) {
|
|
30816
32376
|
try {
|
|
30817
32377
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
30818
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
32378
|
+
const currentSchedule = this.db.select().from(schedules).where(eq38(schedules.id, scheduleId)).get();
|
|
30819
32379
|
if (!currentSchedule || !currentSchedule.enabled) {
|
|
30820
|
-
|
|
32380
|
+
log11.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
|
|
30821
32381
|
this.remove(projectId, kind);
|
|
30822
32382
|
return;
|
|
30823
32383
|
}
|
|
30824
32384
|
const task = this.tasks.get(taskKey(projectId, kind));
|
|
30825
32385
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
30826
|
-
const project = this.db.select().from(projects).where(
|
|
32386
|
+
const project = this.db.select().from(projects).where(eq38(projects.id, projectId)).get();
|
|
30827
32387
|
if (!project) {
|
|
30828
|
-
|
|
32388
|
+
log11.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
|
|
30829
32389
|
this.remove(projectId, kind);
|
|
30830
32390
|
return;
|
|
30831
32391
|
}
|
|
30832
32392
|
if (kind === SchedulableRunKinds["traffic-sync"]) {
|
|
30833
32393
|
const sourceId = currentSchedule.sourceId;
|
|
30834
32394
|
if (!sourceId) {
|
|
30835
|
-
|
|
32395
|
+
log11.warn("traffic-sync.missing-source", { scheduleId, projectId });
|
|
30836
32396
|
return;
|
|
30837
32397
|
}
|
|
30838
32398
|
if (!this.callbacks.onTrafficSyncRequested) {
|
|
30839
|
-
|
|
32399
|
+
log11.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
|
|
30840
32400
|
return;
|
|
30841
32401
|
}
|
|
30842
32402
|
this.db.update(schedules).set({
|
|
30843
32403
|
lastRunAt: now,
|
|
30844
32404
|
nextRunAt,
|
|
30845
32405
|
updatedAt: now
|
|
30846
|
-
}).where(
|
|
30847
|
-
|
|
32406
|
+
}).where(eq38(schedules.id, currentSchedule.id)).run();
|
|
32407
|
+
log11.info("traffic-sync.triggered", { projectName: project.name, sourceId });
|
|
30848
32408
|
this.callbacks.onTrafficSyncRequested(project.name, sourceId);
|
|
30849
32409
|
return;
|
|
30850
32410
|
}
|
|
@@ -30853,7 +32413,7 @@ var Scheduler = class {
|
|
|
30853
32413
|
if (project.defaultLocation) {
|
|
30854
32414
|
const loc = projectLocations.find((l) => l.label === project.defaultLocation);
|
|
30855
32415
|
if (!loc) {
|
|
30856
|
-
|
|
32416
|
+
log11.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
|
|
30857
32417
|
return;
|
|
30858
32418
|
}
|
|
30859
32419
|
resolvedLocation = loc;
|
|
@@ -30867,11 +32427,11 @@ var Scheduler = class {
|
|
|
30867
32427
|
location: locationLabel
|
|
30868
32428
|
});
|
|
30869
32429
|
if (queueResult.conflict) {
|
|
30870
|
-
|
|
32430
|
+
log11.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
|
|
30871
32431
|
this.db.update(schedules).set({
|
|
30872
32432
|
nextRunAt,
|
|
30873
32433
|
updatedAt: now
|
|
30874
|
-
}).where(
|
|
32434
|
+
}).where(eq38(schedules.id, currentSchedule.id)).run();
|
|
30875
32435
|
return;
|
|
30876
32436
|
}
|
|
30877
32437
|
const runId = queueResult.runId;
|
|
@@ -30879,21 +32439,21 @@ var Scheduler = class {
|
|
|
30879
32439
|
lastRunAt: now,
|
|
30880
32440
|
nextRunAt,
|
|
30881
32441
|
updatedAt: now
|
|
30882
|
-
}).where(
|
|
32442
|
+
}).where(eq38(schedules.id, currentSchedule.id)).run();
|
|
30883
32443
|
const scheduleProviders = currentSchedule.providers;
|
|
30884
32444
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
30885
|
-
|
|
32445
|
+
log11.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
30886
32446
|
this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
|
|
30887
32447
|
} catch (err) {
|
|
30888
|
-
|
|
32448
|
+
log11.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
|
|
30889
32449
|
}
|
|
30890
32450
|
}
|
|
30891
32451
|
};
|
|
30892
32452
|
|
|
30893
32453
|
// src/notifier.ts
|
|
30894
|
-
import { eq as
|
|
30895
|
-
import
|
|
30896
|
-
var
|
|
32454
|
+
import { eq as eq39, desc as desc19, and as and32, inArray as inArray14, or as or5 } from "drizzle-orm";
|
|
32455
|
+
import crypto37 from "crypto";
|
|
32456
|
+
var log12 = createLogger("Notifier");
|
|
30897
32457
|
var Notifier = class {
|
|
30898
32458
|
db;
|
|
30899
32459
|
serverUrl;
|
|
@@ -30903,26 +32463,26 @@ var Notifier = class {
|
|
|
30903
32463
|
}
|
|
30904
32464
|
/** Called after a run completes (success, partial, or failed). */
|
|
30905
32465
|
async onRunCompleted(runId, projectId) {
|
|
30906
|
-
|
|
30907
|
-
const notifs = this.db.select().from(notifications).where(
|
|
32466
|
+
log12.info("run.completed", { runId, projectId });
|
|
32467
|
+
const notifs = this.db.select().from(notifications).where(eq39(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
30908
32468
|
if (notifs.length === 0) {
|
|
30909
|
-
|
|
32469
|
+
log12.info("notifications.none-enabled", { projectId });
|
|
30910
32470
|
return;
|
|
30911
32471
|
}
|
|
30912
|
-
|
|
30913
|
-
const run = this.db.select().from(runs).where(
|
|
32472
|
+
log12.info("notifications.found", { projectId, count: notifs.length });
|
|
32473
|
+
const run = this.db.select().from(runs).where(eq39(runs.id, runId)).get();
|
|
30914
32474
|
if (!run) {
|
|
30915
|
-
|
|
32475
|
+
log12.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
30916
32476
|
return;
|
|
30917
32477
|
}
|
|
30918
|
-
const project = this.db.select().from(projects).where(
|
|
32478
|
+
const project = this.db.select().from(projects).where(eq39(projects.id, projectId)).get();
|
|
30919
32479
|
if (!project) {
|
|
30920
|
-
|
|
32480
|
+
log12.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
30921
32481
|
return;
|
|
30922
32482
|
}
|
|
30923
32483
|
const transitions = this.computeTransitions(runId, projectId);
|
|
30924
32484
|
const events = [];
|
|
30925
|
-
|
|
32485
|
+
log12.info("run.status", { runId: run.id, status: run.status, projectId });
|
|
30926
32486
|
if (run.status === "completed" || run.status === "partial") {
|
|
30927
32487
|
events.push("run.completed");
|
|
30928
32488
|
}
|
|
@@ -30938,7 +32498,7 @@ var Notifier = class {
|
|
|
30938
32498
|
if (!config.url) continue;
|
|
30939
32499
|
const subscribedEvents = config.events;
|
|
30940
32500
|
const matchingEvents = events.filter((e) => subscribedEvents.includes(e));
|
|
30941
|
-
|
|
32501
|
+
log12.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
|
|
30942
32502
|
if (matchingEvents.length === 0) continue;
|
|
30943
32503
|
for (const event of matchingEvents) {
|
|
30944
32504
|
const relevantTransitions = event === "citation.lost" ? lostTransitions : event === "citation.gained" ? gainedTransitions : transitions;
|
|
@@ -30962,11 +32522,11 @@ var Notifier = class {
|
|
|
30962
32522
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
30963
32523
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
30964
32524
|
if (insightEvents.length === 0) return;
|
|
30965
|
-
const notifs = this.db.select().from(notifications).where(
|
|
32525
|
+
const notifs = this.db.select().from(notifications).where(eq39(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
30966
32526
|
if (notifs.length === 0) return;
|
|
30967
|
-
const run = this.db.select().from(runs).where(
|
|
32527
|
+
const run = this.db.select().from(runs).where(eq39(runs.id, runId)).get();
|
|
30968
32528
|
if (!run) return;
|
|
30969
|
-
const project = this.db.select().from(projects).where(
|
|
32529
|
+
const project = this.db.select().from(projects).where(eq39(projects.id, projectId)).get();
|
|
30970
32530
|
if (!project) return;
|
|
30971
32531
|
for (const notif of notifs) {
|
|
30972
32532
|
const config = notif.config;
|
|
@@ -30996,12 +32556,12 @@ var Notifier = class {
|
|
|
30996
32556
|
}
|
|
30997
32557
|
}
|
|
30998
32558
|
computeTransitions(runId, projectId) {
|
|
30999
|
-
const thisRun = this.db.select().from(runs).where(
|
|
32559
|
+
const thisRun = this.db.select().from(runs).where(eq39(runs.id, runId)).get();
|
|
31000
32560
|
if (!thisRun) return [];
|
|
31001
|
-
const groupSiblings = this.db.select().from(runs).where(
|
|
31002
|
-
|
|
31003
|
-
|
|
31004
|
-
|
|
32561
|
+
const groupSiblings = this.db.select().from(runs).where(and32(
|
|
32562
|
+
eq39(runs.projectId, projectId),
|
|
32563
|
+
eq39(runs.kind, thisRun.kind),
|
|
32564
|
+
eq39(runs.createdAt, thisRun.createdAt)
|
|
31005
32565
|
)).all();
|
|
31006
32566
|
const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
|
|
31007
32567
|
if (stillPending) return [];
|
|
@@ -31017,19 +32577,19 @@ var Notifier = class {
|
|
|
31017
32577
|
return candidate.id > best.id ? candidate : best;
|
|
31018
32578
|
});
|
|
31019
32579
|
if (winner.id !== runId) return [];
|
|
31020
|
-
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
32580
|
+
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq39(projects.id, projectId)).get();
|
|
31021
32581
|
const locationCount = Math.max(
|
|
31022
32582
|
1,
|
|
31023
32583
|
(projectLocations?.locations ?? []).length
|
|
31024
32584
|
);
|
|
31025
32585
|
const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
|
|
31026
32586
|
const recentRuns = this.db.select().from(runs).where(
|
|
31027
|
-
|
|
31028
|
-
|
|
31029
|
-
|
|
31030
|
-
or5(
|
|
32587
|
+
and32(
|
|
32588
|
+
eq39(runs.projectId, projectId),
|
|
32589
|
+
eq39(runs.kind, thisRun.kind),
|
|
32590
|
+
or5(eq39(runs.status, "completed"), eq39(runs.status, "partial"))
|
|
31031
32591
|
)
|
|
31032
|
-
).orderBy(
|
|
32592
|
+
).orderBy(desc19(runs.createdAt), desc19(runs.id)).limit(RECENT_FETCH_LIMIT).all();
|
|
31033
32593
|
const groups = groupRunsByCreatedAt(recentRuns);
|
|
31034
32594
|
const currentGroupIdx = groups.findIndex((g) => g[0]?.createdAt === thisRun.createdAt);
|
|
31035
32595
|
if (currentGroupIdx < 0) return [];
|
|
@@ -31044,13 +32604,13 @@ var Notifier = class {
|
|
|
31044
32604
|
provider: querySnapshots.provider,
|
|
31045
32605
|
location: querySnapshots.location,
|
|
31046
32606
|
citationState: querySnapshots.citationState
|
|
31047
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
32607
|
+
}).from(querySnapshots).leftJoin(queries, eq39(querySnapshots.queryId, queries.id)).where(inArray14(querySnapshots.runId, currentRunIds)).all();
|
|
31048
32608
|
const previousSnapshots = this.db.select({
|
|
31049
32609
|
queryId: querySnapshots.queryId,
|
|
31050
32610
|
provider: querySnapshots.provider,
|
|
31051
32611
|
location: querySnapshots.location,
|
|
31052
32612
|
citationState: querySnapshots.citationState
|
|
31053
|
-
}).from(querySnapshots).where(
|
|
32613
|
+
}).from(querySnapshots).where(inArray14(querySnapshots.runId, previousRunIds)).all();
|
|
31054
32614
|
const prevMap = /* @__PURE__ */ new Map();
|
|
31055
32615
|
for (const s of previousSnapshots) {
|
|
31056
32616
|
if (s.queryId == null) continue;
|
|
@@ -31077,23 +32637,23 @@ var Notifier = class {
|
|
|
31077
32637
|
const targetLabel = redactNotificationUrl(url).urlDisplay;
|
|
31078
32638
|
const targetCheck = await resolveWebhookTarget(url);
|
|
31079
32639
|
if (!targetCheck.ok) {
|
|
31080
|
-
|
|
32640
|
+
log12.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
|
|
31081
32641
|
this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
|
|
31082
32642
|
return;
|
|
31083
32643
|
}
|
|
31084
|
-
|
|
32644
|
+
log12.info("webhook.send", { event: payload.event, url: targetLabel });
|
|
31085
32645
|
const maxRetries = 3;
|
|
31086
32646
|
const delays = [1e3, 4e3, 16e3];
|
|
31087
32647
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
31088
32648
|
try {
|
|
31089
32649
|
const response = await deliverWebhook(targetCheck.target, payload, webhookSecret);
|
|
31090
32650
|
if (response.status >= 200 && response.status < 300) {
|
|
31091
|
-
|
|
32651
|
+
log12.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
|
|
31092
32652
|
this.logDelivery(projectId, notificationId, payload.event, "sent", null);
|
|
31093
32653
|
return;
|
|
31094
32654
|
}
|
|
31095
32655
|
const errorDetail = response.error ?? `HTTP ${response.status}`;
|
|
31096
|
-
|
|
32656
|
+
log12.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
|
|
31097
32657
|
if (attempt === maxRetries - 1) {
|
|
31098
32658
|
this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
|
|
31099
32659
|
}
|
|
@@ -31101,7 +32661,7 @@ var Notifier = class {
|
|
|
31101
32661
|
const errorDetail = err instanceof Error ? err.message : String(err);
|
|
31102
32662
|
if (attempt === maxRetries - 1) {
|
|
31103
32663
|
this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
|
|
31104
|
-
|
|
32664
|
+
log12.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
|
|
31105
32665
|
}
|
|
31106
32666
|
}
|
|
31107
32667
|
if (attempt < maxRetries - 1) {
|
|
@@ -31111,7 +32671,7 @@ var Notifier = class {
|
|
|
31111
32671
|
}
|
|
31112
32672
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
31113
32673
|
this.db.insert(auditLog).values({
|
|
31114
|
-
id:
|
|
32674
|
+
id: crypto37.randomUUID(),
|
|
31115
32675
|
projectId,
|
|
31116
32676
|
actor: "scheduler",
|
|
31117
32677
|
action: `notification.${status}`,
|
|
@@ -31124,8 +32684,8 @@ var Notifier = class {
|
|
|
31124
32684
|
};
|
|
31125
32685
|
|
|
31126
32686
|
// src/run-coordinator.ts
|
|
31127
|
-
import { eq as
|
|
31128
|
-
var
|
|
32687
|
+
import { eq as eq40 } from "drizzle-orm";
|
|
32688
|
+
var log13 = createLogger("RunCoordinator");
|
|
31129
32689
|
var RunCoordinator = class {
|
|
31130
32690
|
constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
|
|
31131
32691
|
this.db = db;
|
|
@@ -31135,10 +32695,10 @@ var RunCoordinator = class {
|
|
|
31135
32695
|
this.onAeroEvent = onAeroEvent;
|
|
31136
32696
|
}
|
|
31137
32697
|
async onRunCompleted(runId, projectId) {
|
|
31138
|
-
const runRow = this.db.select().from(runs).where(
|
|
32698
|
+
const runRow = this.db.select().from(runs).where(eq40(runs.id, runId)).get();
|
|
31139
32699
|
const kind = runRow?.kind ?? RunKinds["answer-visibility"];
|
|
31140
32700
|
if (runRow?.trigger === RunTriggers.probe) {
|
|
31141
|
-
|
|
32701
|
+
log13.info("probe.skip-side-effects", { runId, projectId, kind });
|
|
31142
32702
|
return;
|
|
31143
32703
|
}
|
|
31144
32704
|
let insightCount = 0;
|
|
@@ -31155,18 +32715,18 @@ var RunCoordinator = class {
|
|
|
31155
32715
|
try {
|
|
31156
32716
|
await this.onInsightsGenerated(runId, projectId, result);
|
|
31157
32717
|
} catch (err) {
|
|
31158
|
-
|
|
32718
|
+
log13.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
31159
32719
|
}
|
|
31160
32720
|
}
|
|
31161
32721
|
}
|
|
31162
32722
|
} catch (err) {
|
|
31163
|
-
|
|
32723
|
+
log13.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
31164
32724
|
}
|
|
31165
32725
|
}
|
|
31166
32726
|
try {
|
|
31167
32727
|
await this.notifier.onRunCompleted(runId, projectId);
|
|
31168
32728
|
} catch (err) {
|
|
31169
|
-
|
|
32729
|
+
log13.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
31170
32730
|
}
|
|
31171
32731
|
if (this.onAeroEvent) {
|
|
31172
32732
|
try {
|
|
@@ -31179,7 +32739,7 @@ var RunCoordinator = class {
|
|
|
31179
32739
|
};
|
|
31180
32740
|
await this.onAeroEvent(ctx);
|
|
31181
32741
|
} catch (err) {
|
|
31182
|
-
|
|
32742
|
+
log13.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
31183
32743
|
}
|
|
31184
32744
|
}
|
|
31185
32745
|
}
|
|
@@ -31194,7 +32754,7 @@ var RunCoordinator = class {
|
|
|
31194
32754
|
* so the Aero queue is never starved of a follow-up.
|
|
31195
32755
|
*/
|
|
31196
32756
|
buildDiscoveryAeroContext(runId, projectId, status, error) {
|
|
31197
|
-
const session = this.db.select().from(discoverySessions).where(
|
|
32757
|
+
const session = this.db.select().from(discoverySessions).where(eq40(discoverySessions.runId, runId)).get();
|
|
31198
32758
|
const competitorMap = session ? session.competitorMap : [];
|
|
31199
32759
|
return {
|
|
31200
32760
|
kind: RunKinds["aeo-discover-probe"],
|
|
@@ -31216,12 +32776,12 @@ var RunCoordinator = class {
|
|
|
31216
32776
|
};
|
|
31217
32777
|
|
|
31218
32778
|
// src/agent/session-registry.ts
|
|
31219
|
-
import
|
|
31220
|
-
import { eq as
|
|
32779
|
+
import crypto39 from "crypto";
|
|
32780
|
+
import { eq as eq42 } from "drizzle-orm";
|
|
31221
32781
|
|
|
31222
32782
|
// src/agent/session.ts
|
|
31223
|
-
import
|
|
31224
|
-
import
|
|
32783
|
+
import fs15 from "fs";
|
|
32784
|
+
import path15 from "path";
|
|
31225
32785
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
31226
32786
|
import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
|
|
31227
32787
|
|
|
@@ -31361,26 +32921,26 @@ function buildAgentProvidersResponse(config) {
|
|
|
31361
32921
|
}
|
|
31362
32922
|
|
|
31363
32923
|
// src/agent/skill-paths.ts
|
|
31364
|
-
import
|
|
31365
|
-
import
|
|
31366
|
-
import { fileURLToPath } from "url";
|
|
32924
|
+
import fs13 from "fs";
|
|
32925
|
+
import path13 from "path";
|
|
32926
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
31367
32927
|
function resolveAeroSkillDir(pkgDir) {
|
|
31368
|
-
const here = pkgDir ??
|
|
32928
|
+
const here = pkgDir ?? path13.dirname(fileURLToPath2(import.meta.url));
|
|
31369
32929
|
const candidates = [
|
|
31370
|
-
|
|
31371
|
-
|
|
31372
|
-
|
|
32930
|
+
path13.join(here, "../assets/agent-workspace/skills/aero"),
|
|
32931
|
+
path13.join(here, "../../assets/agent-workspace/skills/aero"),
|
|
32932
|
+
path13.join(here, "../../../../skills/aero")
|
|
31373
32933
|
];
|
|
31374
32934
|
for (const candidate of candidates) {
|
|
31375
|
-
if (
|
|
32935
|
+
if (fs13.existsSync(path13.join(candidate, "SKILL.md"))) return candidate;
|
|
31376
32936
|
}
|
|
31377
32937
|
throw new Error(`Aero skill not found. Searched:
|
|
31378
32938
|
${candidates.join("\n ")}`);
|
|
31379
32939
|
}
|
|
31380
32940
|
|
|
31381
32941
|
// src/agent/skill-tools.ts
|
|
31382
|
-
import
|
|
31383
|
-
import
|
|
32942
|
+
import fs14 from "fs";
|
|
32943
|
+
import path14 from "path";
|
|
31384
32944
|
import { Type } from "@sinclair/typebox";
|
|
31385
32945
|
var MAX_DOC_CHARS = 2e4;
|
|
31386
32946
|
function textResult(details) {
|
|
@@ -31389,7 +32949,7 @@ function textResult(details) {
|
|
|
31389
32949
|
details
|
|
31390
32950
|
};
|
|
31391
32951
|
}
|
|
31392
|
-
function
|
|
32952
|
+
function parseDescription2(body) {
|
|
31393
32953
|
if (!body.startsWith("---")) return "(no description)";
|
|
31394
32954
|
const end = body.indexOf("\n---", 3);
|
|
31395
32955
|
if (end === -1) return "(no description)";
|
|
@@ -31401,16 +32961,16 @@ function parseDescription(body) {
|
|
|
31401
32961
|
return "(no description)";
|
|
31402
32962
|
}
|
|
31403
32963
|
function scanSkillDocs(skillDir) {
|
|
31404
|
-
const refsDir =
|
|
31405
|
-
if (!
|
|
32964
|
+
const refsDir = path14.join(skillDir ?? resolveAeroSkillDir(), "references");
|
|
32965
|
+
if (!fs14.existsSync(refsDir)) return [];
|
|
31406
32966
|
const entries = [];
|
|
31407
|
-
for (const file of
|
|
32967
|
+
for (const file of fs14.readdirSync(refsDir)) {
|
|
31408
32968
|
if (!file.endsWith(".md")) continue;
|
|
31409
|
-
const filePath =
|
|
31410
|
-
const body =
|
|
32969
|
+
const filePath = path14.join(refsDir, file);
|
|
32970
|
+
const body = fs14.readFileSync(filePath, "utf-8");
|
|
31411
32971
|
entries.push({
|
|
31412
32972
|
slug: file.replace(/\.md$/, ""),
|
|
31413
|
-
description:
|
|
32973
|
+
description: parseDescription2(body),
|
|
31414
32974
|
bytes: Buffer.byteLength(body, "utf-8")
|
|
31415
32975
|
});
|
|
31416
32976
|
}
|
|
@@ -31450,8 +33010,8 @@ function buildReadSkillDocTool() {
|
|
|
31450
33010
|
availableSlugs: docs.map((d) => d.slug)
|
|
31451
33011
|
});
|
|
31452
33012
|
}
|
|
31453
|
-
const filePath =
|
|
31454
|
-
const content =
|
|
33013
|
+
const filePath = path14.join(skillDir, "references", `${match.slug}.md`);
|
|
33014
|
+
const content = fs14.readFileSync(filePath, "utf-8");
|
|
31455
33015
|
if (content.length > MAX_DOC_CHARS) {
|
|
31456
33016
|
return textResult({
|
|
31457
33017
|
slug: match.slug,
|
|
@@ -31545,10 +33105,10 @@ function ensureBuiltinsRegistered() {
|
|
|
31545
33105
|
}
|
|
31546
33106
|
function loadAeroSystemPrompt(pkgDir) {
|
|
31547
33107
|
const skillDir = resolveAeroSkillDir(pkgDir);
|
|
31548
|
-
const skillBody =
|
|
31549
|
-
const soulPath =
|
|
31550
|
-
if (!
|
|
31551
|
-
const soulBody =
|
|
33108
|
+
const skillBody = fs15.readFileSync(path15.join(skillDir, "SKILL.md"), "utf-8");
|
|
33109
|
+
const soulPath = path15.join(skillDir, "soul.md");
|
|
33110
|
+
if (!fs15.existsSync(soulPath)) return skillBody;
|
|
33111
|
+
const soulBody = fs15.readFileSync(soulPath, "utf-8");
|
|
31552
33112
|
return `${soulBody.trimEnd()}
|
|
31553
33113
|
|
|
31554
33114
|
---
|
|
@@ -31605,8 +33165,8 @@ function resolveSessionProviderAndModel(config, opts) {
|
|
|
31605
33165
|
}
|
|
31606
33166
|
|
|
31607
33167
|
// src/agent/memory-store.ts
|
|
31608
|
-
import
|
|
31609
|
-
import { and as
|
|
33168
|
+
import crypto38 from "crypto";
|
|
33169
|
+
import { and as and33, desc as desc20, eq as eq41, like as like2, sql as sql16 } from "drizzle-orm";
|
|
31610
33170
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
31611
33171
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
31612
33172
|
function rowToDto2(row) {
|
|
@@ -31620,7 +33180,7 @@ function rowToDto2(row) {
|
|
|
31620
33180
|
};
|
|
31621
33181
|
}
|
|
31622
33182
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
31623
|
-
const query = db.select().from(agentMemory).where(
|
|
33183
|
+
const query = db.select().from(agentMemory).where(eq41(agentMemory.projectId, projectId)).orderBy(desc20(agentMemory.updatedAt));
|
|
31624
33184
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
31625
33185
|
return rows.map(rowToDto2);
|
|
31626
33186
|
}
|
|
@@ -31634,7 +33194,7 @@ function upsertMemoryEntry(db, args) {
|
|
|
31634
33194
|
throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
|
|
31635
33195
|
}
|
|
31636
33196
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
31637
|
-
const id =
|
|
33197
|
+
const id = crypto38.randomUUID();
|
|
31638
33198
|
db.insert(agentMemory).values({
|
|
31639
33199
|
id,
|
|
31640
33200
|
projectId: args.projectId,
|
|
@@ -31651,12 +33211,12 @@ function upsertMemoryEntry(db, args) {
|
|
|
31651
33211
|
updatedAt: now
|
|
31652
33212
|
}
|
|
31653
33213
|
}).run();
|
|
31654
|
-
const row = db.select().from(agentMemory).where(
|
|
33214
|
+
const row = db.select().from(agentMemory).where(and33(eq41(agentMemory.projectId, args.projectId), eq41(agentMemory.key, args.key))).get();
|
|
31655
33215
|
if (!row) throw new Error("memory upsert produced no row");
|
|
31656
33216
|
return rowToDto2(row);
|
|
31657
33217
|
}
|
|
31658
33218
|
function deleteMemoryEntry(db, projectId, key) {
|
|
31659
|
-
const result = db.delete(agentMemory).where(
|
|
33219
|
+
const result = db.delete(agentMemory).where(and33(eq41(agentMemory.projectId, projectId), eq41(agentMemory.key, key))).run();
|
|
31660
33220
|
const changes = result.changes ?? 0;
|
|
31661
33221
|
return changes > 0;
|
|
31662
33222
|
}
|
|
@@ -31671,7 +33231,7 @@ function writeCompactionNote(db, args) {
|
|
|
31671
33231
|
}
|
|
31672
33232
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
31673
33233
|
const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
|
|
31674
|
-
const id =
|
|
33234
|
+
const id = crypto38.randomUUID();
|
|
31675
33235
|
let inserted;
|
|
31676
33236
|
db.transaction((tx) => {
|
|
31677
33237
|
tx.insert(agentMemory).values({
|
|
@@ -31685,16 +33245,16 @@ function writeCompactionNote(db, args) {
|
|
|
31685
33245
|
}).run();
|
|
31686
33246
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
31687
33247
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
31688
|
-
|
|
31689
|
-
|
|
33248
|
+
and33(
|
|
33249
|
+
eq41(agentMemory.projectId, args.projectId),
|
|
31690
33250
|
like2(agentMemory.key, `${sessionPrefix}%`)
|
|
31691
33251
|
)
|
|
31692
|
-
).orderBy(
|
|
33252
|
+
).orderBy(desc20(agentMemory.updatedAt)).all();
|
|
31693
33253
|
const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
|
|
31694
33254
|
if (stale.length > 0) {
|
|
31695
33255
|
tx.delete(agentMemory).where(sql16`${agentMemory.id} IN (${sql16.join(stale.map((s) => sql16`${s}`), sql16`, `)})`).run();
|
|
31696
33256
|
}
|
|
31697
|
-
const row = tx.select().from(agentMemory).where(
|
|
33257
|
+
const row = tx.select().from(agentMemory).where(and33(eq41(agentMemory.projectId, args.projectId), eq41(agentMemory.key, key))).get();
|
|
31698
33258
|
if (row) inserted = rowToDto2(row);
|
|
31699
33259
|
});
|
|
31700
33260
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -31827,7 +33387,7 @@ async function compactMessages(args) {
|
|
|
31827
33387
|
}
|
|
31828
33388
|
|
|
31829
33389
|
// src/agent/session-registry.ts
|
|
31830
|
-
var
|
|
33390
|
+
var log14 = createLogger("SessionRegistry");
|
|
31831
33391
|
var MAX_HYDRATE_NOTES = 20;
|
|
31832
33392
|
var MAX_HYDRATE_BYTES = 32 * 1024;
|
|
31833
33393
|
function escapeMemoryFragment(value) {
|
|
@@ -31876,7 +33436,7 @@ var SessionRegistry = class {
|
|
|
31876
33436
|
modelProvider: effectiveProvider,
|
|
31877
33437
|
modelId: effectiveModelId,
|
|
31878
33438
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
31879
|
-
}).where(
|
|
33439
|
+
}).where(eq42(agentSessions.projectId, projectId)).run();
|
|
31880
33440
|
}
|
|
31881
33441
|
const agent2 = createAeroSession({
|
|
31882
33442
|
projectName,
|
|
@@ -32054,13 +33614,13 @@ ${lines.join("\n")}
|
|
|
32054
33614
|
agent.state.messages = result.messages;
|
|
32055
33615
|
agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
|
|
32056
33616
|
this.save(projectName);
|
|
32057
|
-
|
|
33617
|
+
log14.info("compaction.completed", {
|
|
32058
33618
|
projectName,
|
|
32059
33619
|
removedCount: result.removedCount,
|
|
32060
33620
|
summaryBytes: Buffer.byteLength(result.summary, "utf8")
|
|
32061
33621
|
});
|
|
32062
33622
|
} catch (err) {
|
|
32063
|
-
|
|
33623
|
+
log14.error("compaction.failed", {
|
|
32064
33624
|
projectName,
|
|
32065
33625
|
error: err instanceof Error ? err.message : String(err)
|
|
32066
33626
|
});
|
|
@@ -32090,7 +33650,7 @@ ${lines.join("\n")}
|
|
|
32090
33650
|
modelProvider: nextProvider,
|
|
32091
33651
|
modelId: nextModelId,
|
|
32092
33652
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
32093
|
-
}).where(
|
|
33653
|
+
}).where(eq42(agentSessions.projectId, projectId)).run();
|
|
32094
33654
|
}
|
|
32095
33655
|
/** Persist a session's transcript back to the DB. Call after any run settles. */
|
|
32096
33656
|
save(projectName) {
|
|
@@ -32157,7 +33717,7 @@ ${lines.join("\n")}
|
|
|
32157
33717
|
await agent.prompt(msgs);
|
|
32158
33718
|
this.save(projectName);
|
|
32159
33719
|
} catch (err) {
|
|
32160
|
-
|
|
33720
|
+
log14.error("drain.failed", {
|
|
32161
33721
|
projectName,
|
|
32162
33722
|
error: err instanceof Error ? err.message : String(err)
|
|
32163
33723
|
});
|
|
@@ -32252,17 +33812,17 @@ ${lines.join("\n")}
|
|
|
32252
33812
|
return id;
|
|
32253
33813
|
}
|
|
32254
33814
|
tryResolveProjectId(projectName) {
|
|
32255
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
33815
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq42(projects.name, projectName)).get();
|
|
32256
33816
|
return row?.id;
|
|
32257
33817
|
}
|
|
32258
33818
|
loadRow(projectId) {
|
|
32259
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
33819
|
+
const row = this.opts.db.select().from(agentSessions).where(eq42(agentSessions.projectId, projectId)).get();
|
|
32260
33820
|
return row ?? null;
|
|
32261
33821
|
}
|
|
32262
33822
|
insertRow(params) {
|
|
32263
33823
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32264
33824
|
this.opts.db.insert(agentSessions).values({
|
|
32265
|
-
id:
|
|
33825
|
+
id: crypto39.randomUUID(),
|
|
32266
33826
|
projectId: params.projectId,
|
|
32267
33827
|
systemPrompt: params.systemPrompt,
|
|
32268
33828
|
modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
|
|
@@ -32275,14 +33835,14 @@ ${lines.join("\n")}
|
|
|
32275
33835
|
}
|
|
32276
33836
|
updateRow(projectId, patch) {
|
|
32277
33837
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32278
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
33838
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq42(agentSessions.projectId, projectId)).run();
|
|
32279
33839
|
}
|
|
32280
33840
|
};
|
|
32281
33841
|
|
|
32282
33842
|
// src/agent/agent-routes.ts
|
|
32283
|
-
import { eq as
|
|
33843
|
+
import { eq as eq43 } from "drizzle-orm";
|
|
32284
33844
|
function resolveProject2(db, name) {
|
|
32285
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
33845
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq43(projects.name, name)).get();
|
|
32286
33846
|
if (!row) throw notFound("project", name);
|
|
32287
33847
|
return row;
|
|
32288
33848
|
}
|
|
@@ -32291,7 +33851,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
32291
33851
|
"/projects/:name/agent/transcript",
|
|
32292
33852
|
async (request) => {
|
|
32293
33853
|
const project = resolveProject2(opts.db, request.params.name);
|
|
32294
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
33854
|
+
const row = opts.db.select().from(agentSessions).where(eq43(agentSessions.projectId, project.id)).get();
|
|
32295
33855
|
if (!row) {
|
|
32296
33856
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
32297
33857
|
}
|
|
@@ -32315,7 +33875,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
32315
33875
|
async (request) => {
|
|
32316
33876
|
const project = resolveProject2(opts.db, request.params.name);
|
|
32317
33877
|
opts.sessionRegistry.reset(project.name);
|
|
32318
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
33878
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq43(agentSessions.projectId, project.id)).run();
|
|
32319
33879
|
return { status: "reset" };
|
|
32320
33880
|
}
|
|
32321
33881
|
);
|
|
@@ -32574,13 +34134,13 @@ function extractHostname(domain) {
|
|
|
32574
34134
|
function fetchWithPinnedAddress(target) {
|
|
32575
34135
|
return new Promise((resolve) => {
|
|
32576
34136
|
const port = target.url.port ? Number(target.url.port) : 443;
|
|
32577
|
-
const
|
|
34137
|
+
const path17 = target.url.pathname + target.url.search;
|
|
32578
34138
|
const req = https2.request(
|
|
32579
34139
|
{
|
|
32580
34140
|
hostname: target.address,
|
|
32581
34141
|
family: target.family,
|
|
32582
34142
|
port,
|
|
32583
|
-
path:
|
|
34143
|
+
path: path17,
|
|
32584
34144
|
method: "GET",
|
|
32585
34145
|
timeout: FETCH_TIMEOUT_MS2,
|
|
32586
34146
|
servername: target.url.hostname,
|
|
@@ -32672,7 +34232,7 @@ function formatAuditFactorScore(factor) {
|
|
|
32672
34232
|
}
|
|
32673
34233
|
|
|
32674
34234
|
// src/snapshot-service.ts
|
|
32675
|
-
var
|
|
34235
|
+
var log15 = createLogger("Snapshot");
|
|
32676
34236
|
var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
|
|
32677
34237
|
var SNAPSHOT_QUERY_COUNT = 6;
|
|
32678
34238
|
var ProviderExecutionGate2 = class {
|
|
@@ -32815,7 +34375,7 @@ var SnapshotService = class {
|
|
|
32815
34375
|
return mapAuditReport(report);
|
|
32816
34376
|
} catch (err) {
|
|
32817
34377
|
const message = err instanceof Error ? err.message : String(err);
|
|
32818
|
-
|
|
34378
|
+
log15.warn("audit.failed", { homepageUrl, error: message });
|
|
32819
34379
|
return {
|
|
32820
34380
|
url: homepageUrl,
|
|
32821
34381
|
finalUrl: homepageUrl,
|
|
@@ -32845,7 +34405,7 @@ var SnapshotService = class {
|
|
|
32845
34405
|
queries: parsedQueries
|
|
32846
34406
|
};
|
|
32847
34407
|
} catch (err) {
|
|
32848
|
-
|
|
34408
|
+
log15.warn("profile.generation-failed", {
|
|
32849
34409
|
domain: ctx.domain,
|
|
32850
34410
|
provider: ctx.analysisProvider.adapter.name,
|
|
32851
34411
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -32987,7 +34547,7 @@ var SnapshotService = class {
|
|
|
32987
34547
|
recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
|
|
32988
34548
|
};
|
|
32989
34549
|
} catch (err) {
|
|
32990
|
-
|
|
34550
|
+
log15.warn("response.analysis-failed", {
|
|
32991
34551
|
provider: ctx.analysisProvider.adapter.name,
|
|
32992
34552
|
error: err instanceof Error ? err.message : String(err)
|
|
32993
34553
|
});
|
|
@@ -33272,7 +34832,7 @@ function clipText(value, length) {
|
|
|
33272
34832
|
// src/server.ts
|
|
33273
34833
|
var _require3 = createRequire4(import.meta.url);
|
|
33274
34834
|
var { version: PKG_VERSION2 } = _require3("../package.json");
|
|
33275
|
-
var
|
|
34835
|
+
var log16 = createLogger("Server");
|
|
33276
34836
|
var DEFAULT_QUOTA = {
|
|
33277
34837
|
maxConcurrency: 2,
|
|
33278
34838
|
maxRequestsPerMinute: 10,
|
|
@@ -33302,14 +34862,14 @@ function summarizeProviderConfig(provider, config) {
|
|
|
33302
34862
|
};
|
|
33303
34863
|
}
|
|
33304
34864
|
function hashApiKey(key) {
|
|
33305
|
-
return
|
|
34865
|
+
return crypto40.createHash("sha256").update(key).digest("hex");
|
|
33306
34866
|
}
|
|
33307
34867
|
var DASHBOARD_SCRYPT_KEYLEN = 64;
|
|
33308
34868
|
var DASHBOARD_SCRYPT_COST = 1 << 15;
|
|
33309
34869
|
var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
|
|
33310
34870
|
function hashDashboardPassword(password) {
|
|
33311
|
-
const salt =
|
|
33312
|
-
const derived =
|
|
34871
|
+
const salt = crypto40.randomBytes(16);
|
|
34872
|
+
const derived = crypto40.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
|
|
33313
34873
|
N: DASHBOARD_SCRYPT_COST,
|
|
33314
34874
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
33315
34875
|
});
|
|
@@ -33330,18 +34890,18 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
33330
34890
|
} catch {
|
|
33331
34891
|
return { ok: false, needsRehash: false };
|
|
33332
34892
|
}
|
|
33333
|
-
const derived =
|
|
34893
|
+
const derived = crypto40.scryptSync(password, salt, expected.length, {
|
|
33334
34894
|
N: DASHBOARD_SCRYPT_COST,
|
|
33335
34895
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
33336
34896
|
});
|
|
33337
34897
|
if (derived.length !== expected.length) return { ok: false, needsRehash: false };
|
|
33338
|
-
return { ok:
|
|
34898
|
+
return { ok: crypto40.timingSafeEqual(derived, expected), needsRehash: false };
|
|
33339
34899
|
}
|
|
33340
34900
|
if (/^[a-f0-9]{64}$/i.test(storedHash)) {
|
|
33341
34901
|
const candidate = Buffer.from(hashApiKey(password), "hex");
|
|
33342
34902
|
const expected = Buffer.from(storedHash, "hex");
|
|
33343
34903
|
if (candidate.length !== expected.length) return { ok: false, needsRehash: false };
|
|
33344
|
-
const ok =
|
|
34904
|
+
const ok = crypto40.timingSafeEqual(candidate, expected);
|
|
33345
34905
|
return { ok, needsRehash: ok };
|
|
33346
34906
|
}
|
|
33347
34907
|
return { ok: false, needsRehash: false };
|
|
@@ -33400,7 +34960,7 @@ function applyLegacyCredentials(rows, config) {
|
|
|
33400
34960
|
}
|
|
33401
34961
|
if (migratedGoogle > 0) {
|
|
33402
34962
|
saveConfigPatch({ google: config.google });
|
|
33403
|
-
|
|
34963
|
+
log16.info("credentials.migrated", { type: "google", count: migratedGoogle });
|
|
33404
34964
|
}
|
|
33405
34965
|
let migratedGa4 = 0;
|
|
33406
34966
|
for (const row of rows.ga4) {
|
|
@@ -33418,7 +34978,7 @@ function applyLegacyCredentials(rows, config) {
|
|
|
33418
34978
|
}
|
|
33419
34979
|
if (migratedGa4 > 0) {
|
|
33420
34980
|
saveConfigPatch({ ga4: config.ga4 });
|
|
33421
|
-
|
|
34981
|
+
log16.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
|
|
33422
34982
|
}
|
|
33423
34983
|
}
|
|
33424
34984
|
async function createServer(opts) {
|
|
@@ -33450,11 +35010,11 @@ async function createServer(opts) {
|
|
|
33450
35010
|
applyLegacyCredentials(legacyRows, opts.config);
|
|
33451
35011
|
dropLegacyCredentialColumns(opts.db);
|
|
33452
35012
|
} catch (err) {
|
|
33453
|
-
|
|
35013
|
+
log16.warn("credentials.migration.failed", {
|
|
33454
35014
|
error: err instanceof Error ? err.message : String(err)
|
|
33455
35015
|
});
|
|
33456
35016
|
}
|
|
33457
|
-
|
|
35017
|
+
log16.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
|
|
33458
35018
|
const p = providers[k];
|
|
33459
35019
|
return p?.apiKey || p?.baseUrl || p?.vertexProject;
|
|
33460
35020
|
}) });
|
|
@@ -33503,7 +35063,7 @@ async function createServer(opts) {
|
|
|
33503
35063
|
intelligenceService,
|
|
33504
35064
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
33505
35065
|
async (ctx) => {
|
|
33506
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
35066
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq44(projects.id, ctx.projectId)).get();
|
|
33507
35067
|
if (!project) return;
|
|
33508
35068
|
let content;
|
|
33509
35069
|
if (ctx.kind === RunKinds["aeo-discover-probe"]) {
|
|
@@ -33526,8 +35086,8 @@ async function createServer(opts) {
|
|
|
33526
35086
|
);
|
|
33527
35087
|
jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
|
|
33528
35088
|
const snapshotService = new SnapshotService(registry);
|
|
33529
|
-
const orphanedOpenClawDir =
|
|
33530
|
-
if (
|
|
35089
|
+
const orphanedOpenClawDir = path16.join(os7.homedir(), ".openclaw-aero");
|
|
35090
|
+
if (fs16.existsSync(orphanedOpenClawDir)) {
|
|
33531
35091
|
app.log.warn(
|
|
33532
35092
|
{ path: orphanedOpenClawDir },
|
|
33533
35093
|
"OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
|
|
@@ -33660,7 +35220,7 @@ async function createServer(opts) {
|
|
|
33660
35220
|
return removed;
|
|
33661
35221
|
}
|
|
33662
35222
|
};
|
|
33663
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
35223
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto40.randomBytes(32).toString("hex");
|
|
33664
35224
|
const googleConnectionStore = {
|
|
33665
35225
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
33666
35226
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -33706,11 +35266,11 @@ async function createServer(opts) {
|
|
|
33706
35266
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
33707
35267
|
if (opts.config.apiKey) {
|
|
33708
35268
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
33709
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
35269
|
+
const existing = opts.db.select().from(apiKeys).where(eq44(apiKeys.keyHash, keyHash)).get();
|
|
33710
35270
|
if (!existing) {
|
|
33711
35271
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
33712
35272
|
opts.db.insert(apiKeys).values({
|
|
33713
|
-
id: `key_${
|
|
35273
|
+
id: `key_${crypto40.randomBytes(8).toString("hex")}`,
|
|
33714
35274
|
name: "default",
|
|
33715
35275
|
keyHash,
|
|
33716
35276
|
keyPrefix: prefix,
|
|
@@ -33734,7 +35294,7 @@ async function createServer(opts) {
|
|
|
33734
35294
|
};
|
|
33735
35295
|
const createSession = (apiKeyId) => {
|
|
33736
35296
|
pruneExpiredSessions();
|
|
33737
|
-
const sessionId =
|
|
35297
|
+
const sessionId = crypto40.randomBytes(32).toString("hex");
|
|
33738
35298
|
sessions.set(sessionId, {
|
|
33739
35299
|
apiKeyId,
|
|
33740
35300
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -33758,7 +35318,7 @@ async function createServer(opts) {
|
|
|
33758
35318
|
};
|
|
33759
35319
|
const getDefaultApiKey = () => {
|
|
33760
35320
|
if (!opts.config.apiKey) return void 0;
|
|
33761
|
-
return opts.db.select().from(apiKeys).where(
|
|
35321
|
+
return opts.db.select().from(apiKeys).where(eq44(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
33762
35322
|
};
|
|
33763
35323
|
const createPasswordSession = (reply) => {
|
|
33764
35324
|
const key = getDefaultApiKey();
|
|
@@ -33820,12 +35380,12 @@ async function createServer(opts) {
|
|
|
33820
35380
|
return reply.send({ authenticated: true });
|
|
33821
35381
|
}
|
|
33822
35382
|
if (apiKey) {
|
|
33823
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
35383
|
+
const key = opts.db.select().from(apiKeys).where(eq44(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
33824
35384
|
if (!key || key.revokedAt) {
|
|
33825
35385
|
const err2 = authInvalid();
|
|
33826
35386
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
33827
35387
|
}
|
|
33828
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
35388
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq44(apiKeys.id, key.id)).run();
|
|
33829
35389
|
const sessionId = createSession(key.id);
|
|
33830
35390
|
reply.header("set-cookie", serializeSessionCookie({
|
|
33831
35391
|
name: SESSION_COOKIE_NAME,
|
|
@@ -33896,9 +35456,20 @@ async function createServer(opts) {
|
|
|
33896
35456
|
const configPath = getConfigPath();
|
|
33897
35457
|
return {
|
|
33898
35458
|
databasePath: opts.config.database,
|
|
33899
|
-
configPath:
|
|
35459
|
+
configPath: fs16.existsSync(configPath) ? configPath : null
|
|
33900
35460
|
};
|
|
33901
35461
|
})(),
|
|
35462
|
+
// Snapshot the bundled skill trees (version + file hashes) so the
|
|
35463
|
+
// `agent.skills.current` doctor check can flag a `~/.claude/skills/` install
|
|
35464
|
+
// that has drifted behind this build. Best-effort: if the bundled assets
|
|
35465
|
+
// can't be resolved the check simply skips rather than failing boot.
|
|
35466
|
+
bundledSkills: (() => {
|
|
35467
|
+
try {
|
|
35468
|
+
return getBundledSkillSnapshots();
|
|
35469
|
+
} catch {
|
|
35470
|
+
return void 0;
|
|
35471
|
+
}
|
|
35472
|
+
})(),
|
|
33902
35473
|
// Local canonry serve runs on the operator's machine, where pointing a
|
|
33903
35474
|
// webhook at localhost (Discord test container, Pipedream-mock dev server,
|
|
33904
35475
|
// etc.) is a legitimate workflow. Default to allowing it for the local
|
|
@@ -33923,7 +35494,7 @@ async function createServer(opts) {
|
|
|
33923
35494
|
executeGscSync(opts.db, runId, projectId, {
|
|
33924
35495
|
...syncOpts,
|
|
33925
35496
|
config: opts.config
|
|
33926
|
-
}).catch((err) => {
|
|
35497
|
+
}).then(() => maybeRefreshGscCoverage(opts.db, opts.config, projectId)).catch((err) => {
|
|
33927
35498
|
app.log.error({ runId, err }, "GSC sync failed");
|
|
33928
35499
|
});
|
|
33929
35500
|
},
|
|
@@ -33940,6 +35511,19 @@ async function createServer(opts) {
|
|
|
33940
35511
|
app.log.error({ runId, err }, "Inspect sitemap failed");
|
|
33941
35512
|
});
|
|
33942
35513
|
},
|
|
35514
|
+
onGbpSyncRequested: (runId, projectId, syncOpts) => {
|
|
35515
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
35516
|
+
if (!googleClientId || !googleClientSecret) {
|
|
35517
|
+
app.log.error("GBP sync requested but Google OAuth credentials are not configured in the local config");
|
|
35518
|
+
return;
|
|
35519
|
+
}
|
|
35520
|
+
executeGbpSync(opts.db, runId, projectId, {
|
|
35521
|
+
...syncOpts,
|
|
35522
|
+
config: opts.config
|
|
35523
|
+
}).catch((err) => {
|
|
35524
|
+
app.log.error({ runId, err }, "GBP sync failed");
|
|
35525
|
+
});
|
|
35526
|
+
},
|
|
33943
35527
|
getBacklinksStatus: () => ({
|
|
33944
35528
|
duckdbInstalled: isDuckdbInstalled(),
|
|
33945
35529
|
duckdbVersion: readInstalledVersion() ?? void 0,
|
|
@@ -33961,7 +35545,7 @@ async function createServer(opts) {
|
|
|
33961
35545
|
deps: {
|
|
33962
35546
|
enqueueAutoExtract: ({ projectId, release: r }) => {
|
|
33963
35547
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33964
|
-
const runId =
|
|
35548
|
+
const runId = crypto40.randomUUID();
|
|
33965
35549
|
opts.db.insert(runs).values({
|
|
33966
35550
|
id: runId,
|
|
33967
35551
|
projectId,
|
|
@@ -34043,6 +35627,12 @@ async function createServer(opts) {
|
|
|
34043
35627
|
executeBingInspectSitemap(opts.db, runId, projectId, {
|
|
34044
35628
|
...inspectOpts,
|
|
34045
35629
|
config: opts.config
|
|
35630
|
+
}).then(() => {
|
|
35631
|
+
const finished = opts.db.select({ status: runs.status }).from(runs).where(eq44(runs.id, runId)).get();
|
|
35632
|
+
if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
|
|
35633
|
+
return maybeRefreshGscCoverage(opts.db, opts.config, projectId);
|
|
35634
|
+
}
|
|
35635
|
+
return null;
|
|
34046
35636
|
}).catch((err) => {
|
|
34047
35637
|
app.log.error({ runId, err }, "Bing inspect sitemap failed");
|
|
34048
35638
|
});
|
|
@@ -34125,7 +35715,7 @@ async function createServer(opts) {
|
|
|
34125
35715
|
const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
|
|
34126
35716
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
34127
35717
|
opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
|
|
34128
|
-
id:
|
|
35718
|
+
id: crypto40.randomUUID(),
|
|
34129
35719
|
projectId,
|
|
34130
35720
|
actor: "api",
|
|
34131
35721
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -34269,10 +35859,10 @@ async function createServer(opts) {
|
|
|
34269
35859
|
return snapshotService.createReport(input);
|
|
34270
35860
|
}
|
|
34271
35861
|
});
|
|
34272
|
-
const dirname =
|
|
34273
|
-
const assetsDir =
|
|
34274
|
-
if (
|
|
34275
|
-
const indexPath =
|
|
35862
|
+
const dirname = path16.dirname(fileURLToPath3(import.meta.url));
|
|
35863
|
+
const assetsDir = path16.join(dirname, "..", "assets");
|
|
35864
|
+
if (fs16.existsSync(assetsDir)) {
|
|
35865
|
+
const indexPath = path16.join(assetsDir, "index.html");
|
|
34276
35866
|
const injectConfig = (html) => {
|
|
34277
35867
|
const clientConfig = {};
|
|
34278
35868
|
if (basePath) clientConfig.basePath = basePath;
|
|
@@ -34304,8 +35894,8 @@ async function createServer(opts) {
|
|
|
34304
35894
|
}
|
|
34305
35895
|
});
|
|
34306
35896
|
const serveIndex = (_request, reply) => {
|
|
34307
|
-
if (
|
|
34308
|
-
const html =
|
|
35897
|
+
if (fs16.existsSync(indexPath)) {
|
|
35898
|
+
const html = fs16.readFileSync(indexPath, "utf-8");
|
|
34309
35899
|
return reply.header("Cache-Control", "no-cache, must-revalidate").type("text/html").send(injectConfig(html));
|
|
34310
35900
|
}
|
|
34311
35901
|
return reply.status(404).send({ error: "Dashboard not built" });
|
|
@@ -34325,8 +35915,8 @@ async function createServer(opts) {
|
|
|
34325
35915
|
if (basePath && !url.startsWith(basePath)) {
|
|
34326
35916
|
return reply.status(404).send({ error: "Not found", path: request.url });
|
|
34327
35917
|
}
|
|
34328
|
-
if (
|
|
34329
|
-
const html =
|
|
35918
|
+
if (fs16.existsSync(indexPath)) {
|
|
35919
|
+
const html = fs16.readFileSync(indexPath, "utf-8");
|
|
34330
35920
|
return reply.header("Cache-Control", "no-cache, must-revalidate").type("text/html").send(injectConfig(html));
|
|
34331
35921
|
}
|
|
34332
35922
|
return reply.status(404).send({ error: "Not found" });
|
|
@@ -34418,6 +36008,11 @@ export {
|
|
|
34418
36008
|
backfillTrafficClassificationCommand,
|
|
34419
36009
|
renderReportHtml,
|
|
34420
36010
|
setGoogleAuthConfig,
|
|
36011
|
+
installSkills,
|
|
36012
|
+
listSkills,
|
|
36013
|
+
emitInstallSummary,
|
|
36014
|
+
getMissingUserSkillsNudge,
|
|
36015
|
+
parseSkillsClient,
|
|
34421
36016
|
formatAuditFactorScore,
|
|
34422
36017
|
checkLatestVersionForCli,
|
|
34423
36018
|
listAgentProviders,
|