@ainyc/canonry 1.46.1 → 1.48.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -11
- package/assets/agent-workspace/AGENTS.md +89 -0
- package/assets/agent-workspace/SOUL.md +54 -0
- package/assets/agent-workspace/USER.md +23 -0
- package/assets/agent-workspace/skills/aero/SKILL.md +1 -0
- package/assets/agent-workspace/skills/aero/references/wordpress-elementor-mcp.md +218 -0
- package/assets/agent-workspace/skills/canonry-setup/SKILL.md +14 -74
- package/assets/agent-workspace/skills/canonry-setup/references/canonry-cli.md +11 -1
- package/assets/agent-workspace/skills/canonry-setup/references/wordpress-integration.md +4 -0
- package/assets/assets/{index-Cxg_4UWs.js → index-CVk23m8J.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-HO22LHTY.js → chunk-JTKHPNGL.js} +7 -2
- package/dist/{chunk-RMLIF47M.js → chunk-YPTVJRJY.js} +976 -154
- package/dist/cli.js +180 -538
- package/dist/index.js +2 -2
- package/dist/{intelligence-service-ZISLIU4S.js → intelligence-service-Q4WX46MJ.js} +1 -1
- package/package.json +7 -7
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
runs,
|
|
24
24
|
schedules,
|
|
25
25
|
usageCounters
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-JTKHPNGL.js";
|
|
27
27
|
|
|
28
28
|
// src/config.ts
|
|
29
29
|
import fs from "fs";
|
|
@@ -262,13 +262,83 @@ function trackEvent(event, properties) {
|
|
|
262
262
|
}).finally(() => clearTimeout(timeout));
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
// src/cli-error.ts
|
|
266
|
+
var EXIT_USER_ERROR = 1;
|
|
267
|
+
var EXIT_SYSTEM_ERROR = 2;
|
|
268
|
+
var CliError = class extends Error {
|
|
269
|
+
code;
|
|
270
|
+
displayMessage;
|
|
271
|
+
details;
|
|
272
|
+
exitCode;
|
|
273
|
+
constructor(options) {
|
|
274
|
+
super(options.message);
|
|
275
|
+
this.name = "CliError";
|
|
276
|
+
this.code = options.code;
|
|
277
|
+
this.displayMessage = options.displayMessage;
|
|
278
|
+
this.details = options.details;
|
|
279
|
+
this.exitCode = options.exitCode ?? EXIT_USER_ERROR;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
function usageError(displayMessage, options) {
|
|
283
|
+
const firstLine = displayMessage.split("\n", 1)[0] ?? "Error: invalid command usage";
|
|
284
|
+
return new CliError({
|
|
285
|
+
code: "CLI_USAGE_ERROR",
|
|
286
|
+
message: options?.message ?? firstLine.replace(/^Error:\s*/, ""),
|
|
287
|
+
displayMessage,
|
|
288
|
+
details: options?.details
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function printCliError(err, format) {
|
|
292
|
+
if (format === "json") {
|
|
293
|
+
if (err instanceof CliError) {
|
|
294
|
+
console.error(
|
|
295
|
+
JSON.stringify(
|
|
296
|
+
{
|
|
297
|
+
error: {
|
|
298
|
+
code: err.code,
|
|
299
|
+
message: err.message,
|
|
300
|
+
...err.details ? { details: err.details } : {}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
null,
|
|
304
|
+
2
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const message = err instanceof Error ? err.message : "An unexpected error occurred";
|
|
310
|
+
console.error(
|
|
311
|
+
JSON.stringify(
|
|
312
|
+
{
|
|
313
|
+
error: {
|
|
314
|
+
code: "CLI_ERROR",
|
|
315
|
+
message
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
null,
|
|
319
|
+
2
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (err instanceof CliError && err.displayMessage) {
|
|
325
|
+
console.error(err.displayMessage);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (err instanceof Error) {
|
|
329
|
+
console.error(`Error: ${err.message}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
console.error("An unexpected error occurred");
|
|
333
|
+
}
|
|
334
|
+
|
|
265
335
|
// src/server.ts
|
|
266
336
|
import { createRequire as createRequire2 } from "module";
|
|
267
|
-
import
|
|
268
|
-
import
|
|
269
|
-
import
|
|
270
|
-
import { fileURLToPath } from "url";
|
|
271
|
-
import { eq as
|
|
337
|
+
import crypto23 from "crypto";
|
|
338
|
+
import fs7 from "fs";
|
|
339
|
+
import path8 from "path";
|
|
340
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
341
|
+
import { eq as eq24, sql as sql6 } from "drizzle-orm";
|
|
272
342
|
import Fastify from "fastify";
|
|
273
343
|
|
|
274
344
|
// ../contracts/src/config-schema.ts
|
|
@@ -316,7 +386,9 @@ var notificationEventSchema = z2.enum([
|
|
|
316
386
|
"citation.lost",
|
|
317
387
|
"citation.gained",
|
|
318
388
|
"run.completed",
|
|
319
|
-
"run.failed"
|
|
389
|
+
"run.failed",
|
|
390
|
+
"insight.critical",
|
|
391
|
+
"insight.high"
|
|
320
392
|
]);
|
|
321
393
|
var notificationDtoSchema = z2.object({
|
|
322
394
|
id: z2.string(),
|
|
@@ -327,6 +399,8 @@ var notificationDtoSchema = z2.object({
|
|
|
327
399
|
urlHost: z2.string(),
|
|
328
400
|
events: z2.array(notificationEventSchema),
|
|
329
401
|
enabled: z2.boolean().default(true),
|
|
402
|
+
/** Opaque tag identifying the creator (e.g. `"agent"` for Aero webhooks). */
|
|
403
|
+
source: z2.string().optional(),
|
|
330
404
|
webhookSecret: z2.string().optional(),
|
|
331
405
|
createdAt: z2.string(),
|
|
332
406
|
updatedAt: z2.string()
|
|
@@ -1454,7 +1528,39 @@ async function projectRoutes(app, opts) {
|
|
|
1454
1528
|
});
|
|
1455
1529
|
}
|
|
1456
1530
|
if (existing) {
|
|
1457
|
-
app.db.
|
|
1531
|
+
app.db.transaction((tx) => {
|
|
1532
|
+
tx.update(projects).set({
|
|
1533
|
+
displayName: body.displayName,
|
|
1534
|
+
canonicalDomain: body.canonicalDomain,
|
|
1535
|
+
ownedDomains: JSON.stringify(body.ownedDomains ?? []),
|
|
1536
|
+
country: body.country,
|
|
1537
|
+
language: body.language,
|
|
1538
|
+
tags: JSON.stringify(body.tags ?? []),
|
|
1539
|
+
labels: JSON.stringify(body.labels ?? {}),
|
|
1540
|
+
providers: JSON.stringify(body.providers ?? []),
|
|
1541
|
+
locations: JSON.stringify(nextLocations),
|
|
1542
|
+
defaultLocation: nextDefaultLocation,
|
|
1543
|
+
configSource: body.configSource ?? "api",
|
|
1544
|
+
configRevision: existing.configRevision + 1,
|
|
1545
|
+
updatedAt: now
|
|
1546
|
+
}).where(eq3(projects.id, existing.id)).run();
|
|
1547
|
+
writeAuditLog(tx, {
|
|
1548
|
+
projectId: existing.id,
|
|
1549
|
+
actor: "api",
|
|
1550
|
+
action: "project.updated",
|
|
1551
|
+
entityType: "project",
|
|
1552
|
+
entityId: existing.id
|
|
1553
|
+
});
|
|
1554
|
+
});
|
|
1555
|
+
opts.onProjectUpserted?.(existing.id, name);
|
|
1556
|
+
const updated = app.db.select().from(projects).where(eq3(projects.id, existing.id)).get();
|
|
1557
|
+
return reply.status(200).send(formatProject(updated));
|
|
1558
|
+
}
|
|
1559
|
+
const id = crypto4.randomUUID();
|
|
1560
|
+
app.db.transaction((tx) => {
|
|
1561
|
+
tx.insert(projects).values({
|
|
1562
|
+
id,
|
|
1563
|
+
name,
|
|
1458
1564
|
displayName: body.displayName,
|
|
1459
1565
|
canonicalDomain: body.canonicalDomain,
|
|
1460
1566
|
ownedDomains: JSON.stringify(body.ownedDomains ?? []),
|
|
@@ -1466,45 +1572,19 @@ async function projectRoutes(app, opts) {
|
|
|
1466
1572
|
locations: JSON.stringify(nextLocations),
|
|
1467
1573
|
defaultLocation: nextDefaultLocation,
|
|
1468
1574
|
configSource: body.configSource ?? "api",
|
|
1469
|
-
configRevision:
|
|
1575
|
+
configRevision: 1,
|
|
1576
|
+
createdAt: now,
|
|
1470
1577
|
updatedAt: now
|
|
1471
|
-
}).
|
|
1472
|
-
writeAuditLog(
|
|
1473
|
-
projectId:
|
|
1578
|
+
}).run();
|
|
1579
|
+
writeAuditLog(tx, {
|
|
1580
|
+
projectId: id,
|
|
1474
1581
|
actor: "api",
|
|
1475
|
-
action: "project.
|
|
1582
|
+
action: "project.created",
|
|
1476
1583
|
entityType: "project",
|
|
1477
|
-
entityId:
|
|
1584
|
+
entityId: id
|
|
1478
1585
|
});
|
|
1479
|
-
const updated = app.db.select().from(projects).where(eq3(projects.id, existing.id)).get();
|
|
1480
|
-
return reply.status(200).send(formatProject(updated));
|
|
1481
|
-
}
|
|
1482
|
-
const id = crypto4.randomUUID();
|
|
1483
|
-
app.db.insert(projects).values({
|
|
1484
|
-
id,
|
|
1485
|
-
name,
|
|
1486
|
-
displayName: body.displayName,
|
|
1487
|
-
canonicalDomain: body.canonicalDomain,
|
|
1488
|
-
ownedDomains: JSON.stringify(body.ownedDomains ?? []),
|
|
1489
|
-
country: body.country,
|
|
1490
|
-
language: body.language,
|
|
1491
|
-
tags: JSON.stringify(body.tags ?? []),
|
|
1492
|
-
labels: JSON.stringify(body.labels ?? {}),
|
|
1493
|
-
providers: JSON.stringify(body.providers ?? []),
|
|
1494
|
-
locations: JSON.stringify(nextLocations),
|
|
1495
|
-
defaultLocation: nextDefaultLocation,
|
|
1496
|
-
configSource: body.configSource ?? "api",
|
|
1497
|
-
configRevision: 1,
|
|
1498
|
-
createdAt: now,
|
|
1499
|
-
updatedAt: now
|
|
1500
|
-
}).run();
|
|
1501
|
-
writeAuditLog(app.db, {
|
|
1502
|
-
projectId: id,
|
|
1503
|
-
actor: "api",
|
|
1504
|
-
action: "project.created",
|
|
1505
|
-
entityType: "project",
|
|
1506
|
-
entityId: id
|
|
1507
1586
|
});
|
|
1587
|
+
opts.onProjectUpserted?.(id, name);
|
|
1508
1588
|
const created = app.db.select().from(projects).where(eq3(projects.id, id)).get();
|
|
1509
1589
|
return reply.status(201).send(formatProject(created));
|
|
1510
1590
|
});
|
|
@@ -2299,7 +2379,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
2299
2379
|
const body = JSON.stringify(payload);
|
|
2300
2380
|
const isHttps = target.url.protocol === "https:";
|
|
2301
2381
|
const port = target.url.port ? Number(target.url.port) : isHttps ? 443 : 80;
|
|
2302
|
-
const
|
|
2382
|
+
const path9 = `${target.url.pathname}${target.url.search}`;
|
|
2303
2383
|
const headers = {
|
|
2304
2384
|
"Content-Length": String(Buffer.byteLength(body)),
|
|
2305
2385
|
"Content-Type": "application/json",
|
|
@@ -2315,7 +2395,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
2315
2395
|
headers,
|
|
2316
2396
|
hostname: target.address,
|
|
2317
2397
|
method: "POST",
|
|
2318
|
-
path:
|
|
2398
|
+
path: path9,
|
|
2319
2399
|
port,
|
|
2320
2400
|
timeout: REQUEST_TIMEOUT_MS
|
|
2321
2401
|
};
|
|
@@ -2623,6 +2703,9 @@ async function applyRoutes(app, opts) {
|
|
|
2623
2703
|
if (scheduleAction) {
|
|
2624
2704
|
opts?.onScheduleUpdated?.(scheduleAction, projectId);
|
|
2625
2705
|
}
|
|
2706
|
+
if (!hasNotifications) {
|
|
2707
|
+
opts?.onProjectUpserted?.(projectId, config.metadata.name);
|
|
2708
|
+
}
|
|
2626
2709
|
if ("google" in rawSpec && config.spec.google?.gsc?.propertyUrl) {
|
|
2627
2710
|
opts?.onGoogleConnectionPropertyUpdated?.(config.spec.canonicalDomain, "gsc", config.spec.google.gsc.propertyUrl);
|
|
2628
2711
|
}
|
|
@@ -5533,8 +5616,8 @@ async function openApiRoutes(app, opts = {}) {
|
|
|
5533
5616
|
return reply.type("application/json").send(buildOpenApiDocument(opts));
|
|
5534
5617
|
});
|
|
5535
5618
|
}
|
|
5536
|
-
function buildOperationId(method,
|
|
5537
|
-
const parts =
|
|
5619
|
+
function buildOperationId(method, path9) {
|
|
5620
|
+
const parts = path9.split("/").filter(Boolean).map((part) => {
|
|
5538
5621
|
if (part.startsWith("{") && part.endsWith("}")) {
|
|
5539
5622
|
return `by-${part.slice(1, -1)}`;
|
|
5540
5623
|
}
|
|
@@ -5852,14 +5935,14 @@ function formatSchedule(row) {
|
|
|
5852
5935
|
// ../api-routes/src/notifications.ts
|
|
5853
5936
|
import crypto12 from "crypto";
|
|
5854
5937
|
import { eq as eq13 } from "drizzle-orm";
|
|
5855
|
-
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed"];
|
|
5938
|
+
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
5856
5939
|
async function notificationRoutes(app) {
|
|
5857
5940
|
app.get("/notifications/events", async (_request, reply) => {
|
|
5858
5941
|
return reply.send(VALID_EVENTS);
|
|
5859
5942
|
});
|
|
5860
5943
|
app.post("/projects/:name/notifications", async (request, reply) => {
|
|
5861
5944
|
const project = resolveProject(app.db, request.params.name);
|
|
5862
|
-
const { channel, url, events } = request.body ?? {};
|
|
5945
|
+
const { channel, url, events, source } = request.body ?? {};
|
|
5863
5946
|
if (channel !== "webhook") throw validationError('Only "webhook" channel is supported');
|
|
5864
5947
|
const urlCheck = await resolveWebhookTarget(url ?? "");
|
|
5865
5948
|
if (!urlCheck.ok) throw validationError(urlCheck.message);
|
|
@@ -5875,7 +5958,7 @@ async function notificationRoutes(app) {
|
|
|
5875
5958
|
id,
|
|
5876
5959
|
projectId: project.id,
|
|
5877
5960
|
channel: "webhook",
|
|
5878
|
-
config: JSON.stringify({ url, events }),
|
|
5961
|
+
config: JSON.stringify({ url, events, ...source ? { source } : {} }),
|
|
5879
5962
|
webhookSecret,
|
|
5880
5963
|
enabled: 1,
|
|
5881
5964
|
createdAt: now,
|
|
@@ -5962,6 +6045,7 @@ function formatNotification(row) {
|
|
|
5962
6045
|
urlHost: redacted.urlHost,
|
|
5963
6046
|
events: config.events,
|
|
5964
6047
|
enabled: row.enabled === 1,
|
|
6048
|
+
...config.source ? { source: config.source } : {},
|
|
5965
6049
|
createdAt: row.createdAt,
|
|
5966
6050
|
updatedAt: row.updatedAt
|
|
5967
6051
|
};
|
|
@@ -5987,9 +6071,11 @@ var GSC_MAX_PAGES = 40;
|
|
|
5987
6071
|
|
|
5988
6072
|
// ../integration-google/src/types.ts
|
|
5989
6073
|
var GoogleAuthError = class extends Error {
|
|
5990
|
-
|
|
6074
|
+
statusCode;
|
|
6075
|
+
constructor(message, statusCode) {
|
|
5991
6076
|
super(message);
|
|
5992
6077
|
this.name = "GoogleAuthError";
|
|
6078
|
+
this.statusCode = statusCode;
|
|
5993
6079
|
}
|
|
5994
6080
|
};
|
|
5995
6081
|
var GoogleApiError = class extends Error {
|
|
@@ -6080,13 +6166,19 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
|
6080
6166
|
}),
|
|
6081
6167
|
signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
|
|
6082
6168
|
});
|
|
6169
|
+
if (res.status === 429) {
|
|
6170
|
+
throw new GoogleAuthError("Google OAuth rate limit exceeded", 429);
|
|
6171
|
+
}
|
|
6083
6172
|
if (!res.ok) {
|
|
6084
6173
|
const body = await res.text();
|
|
6085
6174
|
let detail = "";
|
|
6086
6175
|
try {
|
|
6087
6176
|
const parsed = JSON.parse(body);
|
|
6088
6177
|
if (parsed.error) detail = parsed.error;
|
|
6089
|
-
if (parsed.error_description)
|
|
6178
|
+
if (parsed.error_description) {
|
|
6179
|
+
const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(code), "g"), "***");
|
|
6180
|
+
detail += detail ? `: ${sanitized}` : sanitized;
|
|
6181
|
+
}
|
|
6090
6182
|
} catch {
|
|
6091
6183
|
detail = body.length <= 120 ? body : `${body.slice(0, 120)}...`;
|
|
6092
6184
|
}
|
|
@@ -6094,6 +6186,9 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
|
6094
6186
|
}
|
|
6095
6187
|
return await res.json();
|
|
6096
6188
|
}
|
|
6189
|
+
function escapeRegExp2(str) {
|
|
6190
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6191
|
+
}
|
|
6097
6192
|
async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
6098
6193
|
validateClientId(clientId);
|
|
6099
6194
|
validateClientSecret(clientSecret);
|
|
@@ -6109,13 +6204,19 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
|
6109
6204
|
}),
|
|
6110
6205
|
signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
|
|
6111
6206
|
});
|
|
6207
|
+
if (res.status === 429) {
|
|
6208
|
+
throw new GoogleAuthError("Google OAuth rate limit exceeded", 429);
|
|
6209
|
+
}
|
|
6112
6210
|
if (!res.ok) {
|
|
6113
6211
|
const body = await res.text();
|
|
6114
6212
|
let detail = "";
|
|
6115
6213
|
try {
|
|
6116
6214
|
const parsed = JSON.parse(body);
|
|
6117
6215
|
if (parsed.error) detail = parsed.error;
|
|
6118
|
-
if (parsed.error_description)
|
|
6216
|
+
if (parsed.error_description) {
|
|
6217
|
+
const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(currentRefreshToken), "g"), "***");
|
|
6218
|
+
detail += detail ? `: ${sanitized}` : sanitized;
|
|
6219
|
+
}
|
|
6119
6220
|
} catch {
|
|
6120
6221
|
detail = body.length <= 120 ? body : `${body.slice(0, 120)}...`;
|
|
6121
6222
|
}
|
|
@@ -6187,6 +6288,20 @@ function gscClientLog(level, action, ctx) {
|
|
|
6187
6288
|
...ctx
|
|
6188
6289
|
};
|
|
6189
6290
|
if (entry.accessToken) entry.accessToken = "***";
|
|
6291
|
+
if (typeof entry.siteUrl === "string") {
|
|
6292
|
+
try {
|
|
6293
|
+
const url = new URL(entry.siteUrl);
|
|
6294
|
+
if (url.search) {
|
|
6295
|
+
url.searchParams.forEach((_, key) => {
|
|
6296
|
+
if (key.toLowerCase().includes("token") || key.toLowerCase().includes("key") || key.toLowerCase().includes("auth")) {
|
|
6297
|
+
url.searchParams.set(key, "***");
|
|
6298
|
+
}
|
|
6299
|
+
});
|
|
6300
|
+
entry.siteUrl = url.toString();
|
|
6301
|
+
}
|
|
6302
|
+
} catch {
|
|
6303
|
+
}
|
|
6304
|
+
}
|
|
6190
6305
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
6191
6306
|
stream.write(JSON.stringify(entry) + "\n");
|
|
6192
6307
|
}
|
|
@@ -6413,11 +6528,15 @@ async function getAccessToken(clientEmail, privateKey) {
|
|
|
6413
6528
|
const body = await res.text().catch(() => "");
|
|
6414
6529
|
ga4Log("error", "token.failed", { httpStatus: res.status });
|
|
6415
6530
|
const detail = body.length <= 200 ? body : `${body.slice(0, 200)}... [truncated]`;
|
|
6416
|
-
|
|
6531
|
+
const sanitizedDetail = detail.replace(new RegExp(escapeRegExp3(clientEmail), "g"), "***").replace(new RegExp(escapeRegExp3(privateKey.slice(0, 32)), "g"), "***");
|
|
6532
|
+
throw new GA4ApiError(`Failed to get access token: ${sanitizedDetail}`, res.status);
|
|
6417
6533
|
}
|
|
6418
6534
|
const data = await res.json();
|
|
6419
6535
|
return data.access_token;
|
|
6420
6536
|
}
|
|
6537
|
+
function escapeRegExp3(str) {
|
|
6538
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6539
|
+
}
|
|
6421
6540
|
async function runReport(accessToken, propertyId, request) {
|
|
6422
6541
|
const url = `${GA4_DATA_API_BASE}/properties/${propertyId}:runReport`;
|
|
6423
6542
|
const res = await fetch(url, {
|
|
@@ -6535,6 +6654,7 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
|
6535
6654
|
offset
|
|
6536
6655
|
};
|
|
6537
6656
|
const response = await runReport(accessToken, propertyId, request);
|
|
6657
|
+
if (!response) break;
|
|
6538
6658
|
const pageRows = (response.rows ?? []).map((row) => ({
|
|
6539
6659
|
date: row.dimensionValues[0].value,
|
|
6540
6660
|
landingPage: row.dimensionValues[1].value,
|
|
@@ -6567,6 +6687,7 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
|
6567
6687
|
offset: organicOffset
|
|
6568
6688
|
};
|
|
6569
6689
|
const organicResponse = await runReport(accessToken, propertyId, organicRequest);
|
|
6690
|
+
if (!organicResponse) break;
|
|
6570
6691
|
for (const row of organicResponse.rows ?? []) {
|
|
6571
6692
|
const key = `${row.dimensionValues[0].value}::${row.dimensionValues[1].value}`;
|
|
6572
6693
|
organicMap.set(key, parseInt(row.metricValues[0].value, 10) || 0);
|
|
@@ -6694,6 +6815,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
|
6694
6815
|
offset
|
|
6695
6816
|
};
|
|
6696
6817
|
const response = await runReport(accessToken, propertyId, request);
|
|
6818
|
+
if (!response) break;
|
|
6697
6819
|
const pageRows = (response.rows ?? []).map((row) => ({
|
|
6698
6820
|
date: row.dimensionValues[0].value,
|
|
6699
6821
|
source: row.dimensionValues[1].value,
|
|
@@ -6770,6 +6892,7 @@ async function fetchSocialReferrals(accessToken, propertyId, days) {
|
|
|
6770
6892
|
offset
|
|
6771
6893
|
};
|
|
6772
6894
|
const response = await runReport(accessToken, propertyId, request);
|
|
6895
|
+
if (!response) break;
|
|
6773
6896
|
const pageRows = (response.rows ?? []).map((row) => ({
|
|
6774
6897
|
date: row.dimensionValues[0].value,
|
|
6775
6898
|
source: row.dimensionValues[1].value,
|
|
@@ -7579,6 +7702,9 @@ function bingClientLog(level, action, ctx) {
|
|
|
7579
7702
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
7580
7703
|
stream.write(JSON.stringify(entry) + "\n");
|
|
7581
7704
|
}
|
|
7705
|
+
function escapeRegExp4(str) {
|
|
7706
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7707
|
+
}
|
|
7582
7708
|
async function bingFetch(apiKey, endpoint, opts) {
|
|
7583
7709
|
const method = opts?.method ?? "GET";
|
|
7584
7710
|
const separator = endpoint.includes("?") ? "&" : "?";
|
|
@@ -7603,7 +7729,8 @@ async function bingFetch(apiKey, endpoint, opts) {
|
|
|
7603
7729
|
if (!res.ok) {
|
|
7604
7730
|
const body = await res.text();
|
|
7605
7731
|
bingClientLog("error", "http.error", { endpoint, method, httpStatus: res.status });
|
|
7606
|
-
|
|
7732
|
+
let detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
|
|
7733
|
+
detail = detail.replace(new RegExp(escapeRegExp4(apiKey), "g"), "***");
|
|
7607
7734
|
throw new BingApiError(`Bing API error (${res.status}): ${detail}`, res.status);
|
|
7608
7735
|
}
|
|
7609
7736
|
const text = await res.text();
|
|
@@ -8025,12 +8152,12 @@ async function bingRoutes(app, opts) {
|
|
|
8025
8152
|
}
|
|
8026
8153
|
const unindexedUrls = [];
|
|
8027
8154
|
for (const [url, row] of latestByUrl) {
|
|
8028
|
-
if (row.inIndex === 0) {
|
|
8155
|
+
if (row.inIndex === 0 || row.inIndex === null) {
|
|
8029
8156
|
unindexedUrls.push(url);
|
|
8030
8157
|
}
|
|
8031
8158
|
}
|
|
8032
8159
|
if (unindexedUrls.length === 0) {
|
|
8033
|
-
const err = validationError('No
|
|
8160
|
+
const err = validationError('No unindexed or unknown URLs found. Run "canonry bing inspect <project> <url>" first.');
|
|
8034
8161
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
8035
8162
|
}
|
|
8036
8163
|
urlsToSubmit = unindexedUrls;
|
|
@@ -8099,7 +8226,7 @@ async function bingRoutes(app, opts) {
|
|
|
8099
8226
|
query: s.Query,
|
|
8100
8227
|
impressions: s.Impressions,
|
|
8101
8228
|
clicks: s.Clicks,
|
|
8102
|
-
ctr:
|
|
8229
|
+
ctr: s.Impressions > 0 ? s.Clicks / s.Impressions : 0,
|
|
8103
8230
|
averagePosition: s.AverageClickPosition ?? s.AverageImpressionPosition ?? 0
|
|
8104
8231
|
}));
|
|
8105
8232
|
});
|
|
@@ -9136,8 +9263,10 @@ function buildAuthErrorMessage(res, responseText) {
|
|
|
9136
9263
|
}
|
|
9137
9264
|
return "WordPress credentials are invalid or lack permission for this action";
|
|
9138
9265
|
}
|
|
9139
|
-
async function fetchJson(connection, siteUrl,
|
|
9140
|
-
|
|
9266
|
+
async function fetchJson(connection, siteUrl, path9, init) {
|
|
9267
|
+
if (siteUrl.startsWith("http:")) {
|
|
9268
|
+
}
|
|
9269
|
+
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path9}`, {
|
|
9141
9270
|
...init,
|
|
9142
9271
|
headers: {
|
|
9143
9272
|
"Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
|
|
@@ -9151,6 +9280,9 @@ async function fetchJson(connection, siteUrl, path7, init) {
|
|
|
9151
9280
|
const errorMessage = buildAuthErrorMessage(res, text);
|
|
9152
9281
|
throw new WordpressApiError("AUTH_INVALID", errorMessage, res.status);
|
|
9153
9282
|
}
|
|
9283
|
+
if (res.status === 429) {
|
|
9284
|
+
throw new WordpressApiError("UPSTREAM_ERROR", "WordPress API rate limit exceeded", 429);
|
|
9285
|
+
}
|
|
9154
9286
|
if (res.status === 404) {
|
|
9155
9287
|
throw new WordpressApiError("NOT_FOUND", "WordPress endpoint not found", 404);
|
|
9156
9288
|
}
|
|
@@ -9649,12 +9781,12 @@ var CANONRY_SCHEMA_START = "<!-- canonry:schema:start -->";
|
|
|
9649
9781
|
var CANONRY_SCHEMA_END = "<!-- canonry:schema:end -->";
|
|
9650
9782
|
function stripCanonrySchema(content) {
|
|
9651
9783
|
const regex = new RegExp(
|
|
9652
|
-
`${
|
|
9784
|
+
`${escapeRegExp5(CANONRY_SCHEMA_START)}[\\s\\S]*?${escapeRegExp5(CANONRY_SCHEMA_END)}`,
|
|
9653
9785
|
"g"
|
|
9654
9786
|
);
|
|
9655
9787
|
return content.replace(regex, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
9656
9788
|
}
|
|
9657
|
-
function
|
|
9789
|
+
function escapeRegExp5(str) {
|
|
9658
9790
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9659
9791
|
}
|
|
9660
9792
|
function injectCanonrySchema(content, schemas) {
|
|
@@ -9761,7 +9893,7 @@ async function getSchemaStatus(connection, env) {
|
|
|
9761
9893
|
const thirdPartySchemas = [];
|
|
9762
9894
|
if (hasCanonryMarker) {
|
|
9763
9895
|
const markerRegex = new RegExp(
|
|
9764
|
-
`${
|
|
9896
|
+
`${escapeRegExp5(CANONRY_SCHEMA_START)}([\\s\\S]*?)${escapeRegExp5(CANONRY_SCHEMA_END)}`
|
|
9765
9897
|
);
|
|
9766
9898
|
const match = markerRegex.exec(rawContent);
|
|
9767
9899
|
if (match?.[1]) {
|
|
@@ -10650,6 +10782,7 @@ async function apiRoutes(app, opts) {
|
|
|
10650
10782
|
await api.register(openApiRoutes, { ...opts.openApiInfo, routePrefix: opts.routePrefix });
|
|
10651
10783
|
await api.register(projectRoutes, {
|
|
10652
10784
|
onProjectDeleted: opts.onProjectDeleted,
|
|
10785
|
+
onProjectUpserted: opts.onProjectUpserted,
|
|
10653
10786
|
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
10654
10787
|
});
|
|
10655
10788
|
await api.register(keywordRoutes, {
|
|
@@ -10663,6 +10796,7 @@ async function apiRoutes(app, opts) {
|
|
|
10663
10796
|
});
|
|
10664
10797
|
await api.register(applyRoutes, {
|
|
10665
10798
|
onScheduleUpdated: opts.onScheduleUpdated,
|
|
10799
|
+
onProjectUpserted: opts.onProjectUpserted,
|
|
10666
10800
|
validProviderNames: opts.providerAdapters?.map((a) => a.name),
|
|
10667
10801
|
onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
|
|
10668
10802
|
opts.googleConnectionStore?.updateConnection(domain, connectionType, {
|
|
@@ -10725,6 +10859,39 @@ async function apiRoutes(app, opts) {
|
|
|
10725
10859
|
}, { prefix: opts.routePrefix ?? "/api/v1" });
|
|
10726
10860
|
}
|
|
10727
10861
|
|
|
10862
|
+
// src/agent-webhook.ts
|
|
10863
|
+
import crypto18 from "crypto";
|
|
10864
|
+
import { eq as eq18 } from "drizzle-orm";
|
|
10865
|
+
var AGENT_WEBHOOK_EVENTS = ["run.completed", "insight.critical", "insight.high", "citation.gained"];
|
|
10866
|
+
function buildAgentWebhookUrl(gatewayPort) {
|
|
10867
|
+
return `http://localhost:${gatewayPort}/hooks/canonry`;
|
|
10868
|
+
}
|
|
10869
|
+
function attachAgentWebhookDirect(db, projectId, gatewayPort) {
|
|
10870
|
+
const agentUrl = buildAgentWebhookUrl(gatewayPort);
|
|
10871
|
+
const existing = db.select().from(notifications).where(eq18(notifications.projectId, projectId)).all();
|
|
10872
|
+
const hasAgent = existing.some((n) => {
|
|
10873
|
+
const cfg = parseJsonColumn(n.config, {});
|
|
10874
|
+
return cfg.source === "agent";
|
|
10875
|
+
});
|
|
10876
|
+
if (hasAgent) return "already-attached";
|
|
10877
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10878
|
+
db.insert(notifications).values({
|
|
10879
|
+
id: crypto18.randomUUID(),
|
|
10880
|
+
projectId,
|
|
10881
|
+
channel: "webhook",
|
|
10882
|
+
config: JSON.stringify({
|
|
10883
|
+
url: agentUrl,
|
|
10884
|
+
events: [...AGENT_WEBHOOK_EVENTS],
|
|
10885
|
+
source: "agent"
|
|
10886
|
+
}),
|
|
10887
|
+
enabled: 1,
|
|
10888
|
+
webhookSecret: crypto18.randomUUID(),
|
|
10889
|
+
createdAt: now,
|
|
10890
|
+
updatedAt: now
|
|
10891
|
+
}).run();
|
|
10892
|
+
return "attached";
|
|
10893
|
+
}
|
|
10894
|
+
|
|
10728
10895
|
// ../provider-gemini/src/normalize.ts
|
|
10729
10896
|
import { GoogleGenAI } from "@google/genai";
|
|
10730
10897
|
|
|
@@ -10736,6 +10903,12 @@ function isRetryableError(err) {
|
|
|
10736
10903
|
return status >= 500 || status === 429;
|
|
10737
10904
|
}
|
|
10738
10905
|
}
|
|
10906
|
+
if (err instanceof Error) {
|
|
10907
|
+
const msg = err.message.toLowerCase();
|
|
10908
|
+
if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
|
|
10909
|
+
return true;
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10739
10912
|
return true;
|
|
10740
10913
|
}
|
|
10741
10914
|
async function withRetry(fn, options = {}) {
|
|
@@ -10955,7 +11128,10 @@ function extractCitedDomainsFromSources(groundingSources) {
|
|
|
10955
11128
|
}
|
|
10956
11129
|
if (source.title) {
|
|
10957
11130
|
const titleDomain = extractDomainFromTitle(source.title);
|
|
10958
|
-
if (titleDomain)
|
|
11131
|
+
if (titleDomain) {
|
|
11132
|
+
domains.add(titleDomain);
|
|
11133
|
+
continue;
|
|
11134
|
+
}
|
|
10959
11135
|
}
|
|
10960
11136
|
}
|
|
10961
11137
|
return [...domains];
|
|
@@ -10970,7 +11146,10 @@ function extractDomainFromTitle(title) {
|
|
|
10970
11146
|
function extractDomainFromUri(uri) {
|
|
10971
11147
|
try {
|
|
10972
11148
|
const url = new URL(uri);
|
|
10973
|
-
const hostname = url.hostname.replace(/^www\./, "");
|
|
11149
|
+
const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
11150
|
+
if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
|
|
11151
|
+
return null;
|
|
11152
|
+
}
|
|
10974
11153
|
if (hostname === "vertexaisearch.cloud.google.com") {
|
|
10975
11154
|
const redirectPath = url.pathname.replace(/^\/grounding-api-redirect\//, "");
|
|
10976
11155
|
if (redirectPath && redirectPath !== url.pathname) {
|
|
@@ -11120,6 +11299,12 @@ function isRetryableError2(err) {
|
|
|
11120
11299
|
return status >= 500 || status === 429;
|
|
11121
11300
|
}
|
|
11122
11301
|
}
|
|
11302
|
+
if (err instanceof Error) {
|
|
11303
|
+
const msg = err.message.toLowerCase();
|
|
11304
|
+
if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
|
|
11305
|
+
return true;
|
|
11306
|
+
}
|
|
11307
|
+
}
|
|
11123
11308
|
return true;
|
|
11124
11309
|
}
|
|
11125
11310
|
async function withRetry2(fn, options = {}) {
|
|
@@ -11346,7 +11531,11 @@ function extractCitedDomainsFromSources2(groundingSources) {
|
|
|
11346
11531
|
function extractDomainFromUri2(uri) {
|
|
11347
11532
|
try {
|
|
11348
11533
|
const url = new URL(uri);
|
|
11349
|
-
|
|
11534
|
+
const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
11535
|
+
if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
|
|
11536
|
+
return null;
|
|
11537
|
+
}
|
|
11538
|
+
return hostname;
|
|
11350
11539
|
} catch {
|
|
11351
11540
|
return null;
|
|
11352
11541
|
}
|
|
@@ -11464,6 +11653,12 @@ function isRetryableError3(err) {
|
|
|
11464
11653
|
return status >= 500 || status === 429;
|
|
11465
11654
|
}
|
|
11466
11655
|
}
|
|
11656
|
+
if (err instanceof Error) {
|
|
11657
|
+
const msg = err.message.toLowerCase();
|
|
11658
|
+
if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
|
|
11659
|
+
return true;
|
|
11660
|
+
}
|
|
11661
|
+
}
|
|
11467
11662
|
return true;
|
|
11468
11663
|
}
|
|
11469
11664
|
async function withRetry3(fn, options = {}) {
|
|
@@ -11715,7 +11910,11 @@ function extractCitedDomainsFromSources3(groundingSources) {
|
|
|
11715
11910
|
function extractDomainFromUri3(uri) {
|
|
11716
11911
|
try {
|
|
11717
11912
|
const url = new URL(uri);
|
|
11718
|
-
|
|
11913
|
+
const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
11914
|
+
if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
|
|
11915
|
+
return null;
|
|
11916
|
+
}
|
|
11917
|
+
return hostname;
|
|
11719
11918
|
} catch {
|
|
11720
11919
|
return null;
|
|
11721
11920
|
}
|
|
@@ -11831,6 +12030,12 @@ function isRetryableError4(err) {
|
|
|
11831
12030
|
return status >= 500 || status === 429;
|
|
11832
12031
|
}
|
|
11833
12032
|
}
|
|
12033
|
+
if (err instanceof Error) {
|
|
12034
|
+
const msg = err.message.toLowerCase();
|
|
12035
|
+
if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
|
|
12036
|
+
return true;
|
|
12037
|
+
}
|
|
12038
|
+
}
|
|
11834
12039
|
return true;
|
|
11835
12040
|
}
|
|
11836
12041
|
async function withRetry4(fn, options = {}) {
|
|
@@ -11935,11 +12140,15 @@ async function executeTrackedQuery4(input) {
|
|
|
11935
12140
|
function normalizeResult4(raw) {
|
|
11936
12141
|
const answerText = extractAnswerText2(raw.rawResponse);
|
|
11937
12142
|
const citedDomains = extractDomainMentions(answerText);
|
|
12143
|
+
const groundingSources = citedDomains.map((domain) => ({
|
|
12144
|
+
uri: `http://${domain}`,
|
|
12145
|
+
title: domain
|
|
12146
|
+
}));
|
|
11938
12147
|
return {
|
|
11939
12148
|
provider: "local",
|
|
11940
12149
|
answerText,
|
|
11941
12150
|
citedDomains,
|
|
11942
|
-
groundingSources
|
|
12151
|
+
groundingSources,
|
|
11943
12152
|
searchQueries: raw.searchQueries
|
|
11944
12153
|
};
|
|
11945
12154
|
}
|
|
@@ -12645,6 +12854,12 @@ function isRetryableError5(err) {
|
|
|
12645
12854
|
return status >= 500 || status === 429;
|
|
12646
12855
|
}
|
|
12647
12856
|
}
|
|
12857
|
+
if (err instanceof Error) {
|
|
12858
|
+
const msg = err.message.toLowerCase();
|
|
12859
|
+
if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
|
|
12860
|
+
return true;
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12648
12863
|
return true;
|
|
12649
12864
|
}
|
|
12650
12865
|
async function withRetry5(fn, options = {}) {
|
|
@@ -12862,7 +13077,11 @@ function extractCitedDomains2(groundingSources) {
|
|
|
12862
13077
|
function extractDomainFromUri4(uri) {
|
|
12863
13078
|
try {
|
|
12864
13079
|
const url = new URL(uri);
|
|
12865
|
-
|
|
13080
|
+
const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
13081
|
+
if (hostname.includes("chatgpt.com") || hostname.includes("openai.com")) {
|
|
13082
|
+
return null;
|
|
13083
|
+
}
|
|
13084
|
+
return hostname;
|
|
12866
13085
|
} catch {
|
|
12867
13086
|
return null;
|
|
12868
13087
|
}
|
|
@@ -13124,11 +13343,11 @@ function removeWordpressConnection(config, projectName) {
|
|
|
13124
13343
|
}
|
|
13125
13344
|
|
|
13126
13345
|
// src/job-runner.ts
|
|
13127
|
-
import
|
|
13346
|
+
import crypto19 from "crypto";
|
|
13128
13347
|
import fs4 from "fs";
|
|
13129
13348
|
import path5 from "path";
|
|
13130
13349
|
import os4 from "os";
|
|
13131
|
-
import { and as and7, eq as
|
|
13350
|
+
import { and as and7, eq as eq19, inArray as inArray3, sql as sql4 } from "drizzle-orm";
|
|
13132
13351
|
|
|
13133
13352
|
// src/citation-utils.ts
|
|
13134
13353
|
function domainMatches(domain, canonicalDomain) {
|
|
@@ -13364,7 +13583,7 @@ var JobRunner = class {
|
|
|
13364
13583
|
if (stale.length === 0) return;
|
|
13365
13584
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13366
13585
|
for (const run of stale) {
|
|
13367
|
-
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(
|
|
13586
|
+
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq19(runs.id, run.id)).run();
|
|
13368
13587
|
log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
|
|
13369
13588
|
}
|
|
13370
13589
|
}
|
|
@@ -13392,10 +13611,10 @@ var JobRunner = class {
|
|
|
13392
13611
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
13393
13612
|
}
|
|
13394
13613
|
if (existingRun.status === "queued") {
|
|
13395
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(
|
|
13614
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(eq19(runs.id, runId), eq19(runs.status, "queued"))).run();
|
|
13396
13615
|
}
|
|
13397
13616
|
this.throwIfRunCancelled(runId);
|
|
13398
|
-
const project = this.db.select().from(projects).where(
|
|
13617
|
+
const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
13399
13618
|
if (!project) {
|
|
13400
13619
|
throw new Error(`Project ${projectId} not found`);
|
|
13401
13620
|
}
|
|
@@ -13415,8 +13634,8 @@ var JobRunner = class {
|
|
|
13415
13634
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
13416
13635
|
}
|
|
13417
13636
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
13418
|
-
projectKeywords = this.db.select().from(keywords).where(
|
|
13419
|
-
const projectCompetitors = this.db.select().from(competitors).where(
|
|
13637
|
+
projectKeywords = this.db.select().from(keywords).where(eq19(keywords.projectId, projectId)).all();
|
|
13638
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq19(competitors.projectId, projectId)).all();
|
|
13420
13639
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
13421
13640
|
const allDomains = effectiveDomains({
|
|
13422
13641
|
canonicalDomain: project.canonicalDomain,
|
|
@@ -13432,7 +13651,7 @@ var JobRunner = class {
|
|
|
13432
13651
|
const todayPeriod = getCurrentUsageDay();
|
|
13433
13652
|
for (const p of activeProviders) {
|
|
13434
13653
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
13435
|
-
const providerUsage = this.db.select().from(usageCounters).where(
|
|
13654
|
+
const providerUsage = this.db.select().from(usageCounters).where(eq19(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
|
|
13436
13655
|
const limit = p.config.quotaPolicy.maxRequestsPerDay;
|
|
13437
13656
|
if (providerUsage + queriesPerProvider > limit) {
|
|
13438
13657
|
throw new Error(
|
|
@@ -13488,11 +13707,11 @@ var JobRunner = class {
|
|
|
13488
13707
|
normalized.answerText,
|
|
13489
13708
|
allDomains,
|
|
13490
13709
|
normalized.citedDomains,
|
|
13491
|
-
|
|
13710
|
+
competitorDomains
|
|
13492
13711
|
);
|
|
13493
13712
|
let screenshotRelPath = null;
|
|
13494
13713
|
if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
|
|
13495
|
-
const snapshotId =
|
|
13714
|
+
const snapshotId = crypto19.randomUUID();
|
|
13496
13715
|
const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
|
|
13497
13716
|
if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
|
|
13498
13717
|
const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
|
|
@@ -13522,7 +13741,7 @@ var JobRunner = class {
|
|
|
13522
13741
|
}).run();
|
|
13523
13742
|
} else {
|
|
13524
13743
|
this.db.insert(querySnapshots).values({
|
|
13525
|
-
id:
|
|
13744
|
+
id: crypto19.randomUUID(),
|
|
13526
13745
|
runId,
|
|
13527
13746
|
keywordId: kw.id,
|
|
13528
13747
|
provider: providerName,
|
|
@@ -13573,12 +13792,12 @@ var JobRunner = class {
|
|
|
13573
13792
|
const someFailed = providerErrors.size > 0;
|
|
13574
13793
|
if (allFailed) {
|
|
13575
13794
|
const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
|
|
13576
|
-
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13795
|
+
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq19(runs.id, runId)).run();
|
|
13577
13796
|
} else if (someFailed) {
|
|
13578
13797
|
const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
|
|
13579
|
-
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13798
|
+
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq19(runs.id, runId)).run();
|
|
13580
13799
|
} else {
|
|
13581
|
-
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13800
|
+
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
|
|
13582
13801
|
}
|
|
13583
13802
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13584
13803
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
@@ -13613,7 +13832,7 @@ var JobRunner = class {
|
|
|
13613
13832
|
status: "failed",
|
|
13614
13833
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13615
13834
|
error: errorMessage
|
|
13616
|
-
}).where(
|
|
13835
|
+
}).where(eq19(runs.id, runId)).run();
|
|
13617
13836
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13618
13837
|
trackEvent("run.completed", {
|
|
13619
13838
|
status: "failed",
|
|
@@ -13634,7 +13853,7 @@ var JobRunner = class {
|
|
|
13634
13853
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13635
13854
|
const period = now.slice(0, 10);
|
|
13636
13855
|
this.db.insert(usageCounters).values({
|
|
13637
|
-
id:
|
|
13856
|
+
id: crypto19.randomUUID(),
|
|
13638
13857
|
scope,
|
|
13639
13858
|
period,
|
|
13640
13859
|
metric,
|
|
@@ -13656,7 +13875,7 @@ var JobRunner = class {
|
|
|
13656
13875
|
status: runs.status,
|
|
13657
13876
|
finishedAt: runs.finishedAt,
|
|
13658
13877
|
error: runs.error
|
|
13659
|
-
}).from(runs).where(
|
|
13878
|
+
}).from(runs).where(eq19(runs.id, runId)).get();
|
|
13660
13879
|
}
|
|
13661
13880
|
isRunCancelled(runId) {
|
|
13662
13881
|
return this.getRunState(runId)?.status === "cancelled";
|
|
@@ -13672,7 +13891,7 @@ var JobRunner = class {
|
|
|
13672
13891
|
this.db.update(runs).set({
|
|
13673
13892
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13674
13893
|
error: currentRun.error ?? "Cancelled by user"
|
|
13675
|
-
}).where(
|
|
13894
|
+
}).where(eq19(runs.id, runId)).run();
|
|
13676
13895
|
}
|
|
13677
13896
|
trackEvent("run.completed", {
|
|
13678
13897
|
status: "cancelled",
|
|
@@ -13694,8 +13913,8 @@ function getCurrentUsageDay() {
|
|
|
13694
13913
|
}
|
|
13695
13914
|
|
|
13696
13915
|
// src/gsc-sync.ts
|
|
13697
|
-
import
|
|
13698
|
-
import { eq as
|
|
13916
|
+
import crypto20 from "crypto";
|
|
13917
|
+
import { eq as eq20, and as and8, sql as sql5 } from "drizzle-orm";
|
|
13699
13918
|
var log2 = createLogger("GscSync");
|
|
13700
13919
|
function formatDate2(d) {
|
|
13701
13920
|
return d.toISOString().split("T")[0];
|
|
@@ -13707,13 +13926,13 @@ function daysAgo(n) {
|
|
|
13707
13926
|
}
|
|
13708
13927
|
async function executeGscSync(db, runId, projectId, opts) {
|
|
13709
13928
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13710
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
13929
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
|
|
13711
13930
|
try {
|
|
13712
13931
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13713
13932
|
if (!googleClientId || !googleClientSecret) {
|
|
13714
13933
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13715
13934
|
}
|
|
13716
|
-
const project = db.select().from(projects).where(
|
|
13935
|
+
const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
|
|
13717
13936
|
if (!project) {
|
|
13718
13937
|
throw new Error(`Project not found: ${projectId}`);
|
|
13719
13938
|
}
|
|
@@ -13748,7 +13967,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13748
13967
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
13749
13968
|
db.delete(gscSearchData).where(
|
|
13750
13969
|
and8(
|
|
13751
|
-
|
|
13970
|
+
eq20(gscSearchData.projectId, projectId),
|
|
13752
13971
|
sql5`${gscSearchData.date} >= ${startDate}`,
|
|
13753
13972
|
sql5`${gscSearchData.date} <= ${endDate}`
|
|
13754
13973
|
)
|
|
@@ -13760,7 +13979,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13760
13979
|
for (const row of batch) {
|
|
13761
13980
|
const [query, page, country, device, date] = row.keys;
|
|
13762
13981
|
db.insert(gscSearchData).values({
|
|
13763
|
-
id:
|
|
13982
|
+
id: crypto20.randomUUID(),
|
|
13764
13983
|
projectId,
|
|
13765
13984
|
syncRunId: runId,
|
|
13766
13985
|
date: date ?? "",
|
|
@@ -13794,7 +14013,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13794
14013
|
const rich = ir.richResultsResult;
|
|
13795
14014
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13796
14015
|
db.insert(gscUrlInspections).values({
|
|
13797
|
-
id:
|
|
14016
|
+
id: crypto20.randomUUID(),
|
|
13798
14017
|
projectId,
|
|
13799
14018
|
syncRunId: runId,
|
|
13800
14019
|
url: pageUrl,
|
|
@@ -13815,7 +14034,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13815
14034
|
log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
|
|
13816
14035
|
}
|
|
13817
14036
|
}
|
|
13818
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14037
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
|
|
13819
14038
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13820
14039
|
for (const row of allInspections) {
|
|
13821
14040
|
const existing = latestByUrl.get(row.url);
|
|
@@ -13836,9 +14055,9 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13836
14055
|
}
|
|
13837
14056
|
}
|
|
13838
14057
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
13839
|
-
db.delete(gscCoverageSnapshots).where(and8(
|
|
14058
|
+
db.delete(gscCoverageSnapshots).where(and8(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13840
14059
|
db.insert(gscCoverageSnapshots).values({
|
|
13841
|
-
id:
|
|
14060
|
+
id: crypto20.randomUUID(),
|
|
13842
14061
|
projectId,
|
|
13843
14062
|
syncRunId: runId,
|
|
13844
14063
|
date: snapshotDate,
|
|
@@ -13847,19 +14066,19 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13847
14066
|
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
13848
14067
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13849
14068
|
}).run();
|
|
13850
|
-
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14069
|
+
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
13851
14070
|
log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
13852
14071
|
} catch (err) {
|
|
13853
14072
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
13854
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14073
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
13855
14074
|
log2.error("sync.failed", { runId, projectId, error: errorMsg });
|
|
13856
14075
|
throw err;
|
|
13857
14076
|
}
|
|
13858
14077
|
}
|
|
13859
14078
|
|
|
13860
14079
|
// src/gsc-inspect-sitemap.ts
|
|
13861
|
-
import
|
|
13862
|
-
import { eq as
|
|
14080
|
+
import crypto21 from "crypto";
|
|
14081
|
+
import { eq as eq21, and as and9 } from "drizzle-orm";
|
|
13863
14082
|
|
|
13864
14083
|
// src/sitemap-parser.ts
|
|
13865
14084
|
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
@@ -13928,13 +14147,13 @@ async function parseSitemapRecursive(url, urls, depth) {
|
|
|
13928
14147
|
var log3 = createLogger("InspectSitemap");
|
|
13929
14148
|
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
13930
14149
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13931
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14150
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq21(runs.id, runId)).run();
|
|
13932
14151
|
try {
|
|
13933
14152
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13934
14153
|
if (!googleClientId || !googleClientSecret) {
|
|
13935
14154
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13936
14155
|
}
|
|
13937
|
-
const project = db.select().from(projects).where(
|
|
14156
|
+
const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
|
|
13938
14157
|
if (!project) {
|
|
13939
14158
|
throw new Error(`Project not found: ${projectId}`);
|
|
13940
14159
|
}
|
|
@@ -13975,7 +14194,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13975
14194
|
const rich = ir.richResultsResult;
|
|
13976
14195
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13977
14196
|
db.insert(gscUrlInspections).values({
|
|
13978
|
-
id:
|
|
14197
|
+
id: crypto21.randomUUID(),
|
|
13979
14198
|
projectId,
|
|
13980
14199
|
syncRunId: runId,
|
|
13981
14200
|
url: pageUrl,
|
|
@@ -14002,7 +14221,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14002
14221
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
14003
14222
|
}
|
|
14004
14223
|
}
|
|
14005
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14224
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, projectId)).all();
|
|
14006
14225
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
14007
14226
|
for (const row of allInspections) {
|
|
14008
14227
|
const existing = latestByUrl.get(row.url);
|
|
@@ -14023,9 +14242,9 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14023
14242
|
}
|
|
14024
14243
|
}
|
|
14025
14244
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
14026
|
-
db.delete(gscCoverageSnapshots).where(and9(
|
|
14245
|
+
db.delete(gscCoverageSnapshots).where(and9(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
14027
14246
|
db.insert(gscCoverageSnapshots).values({
|
|
14028
|
-
id:
|
|
14247
|
+
id: crypto21.randomUUID(),
|
|
14029
14248
|
projectId,
|
|
14030
14249
|
syncRunId: runId,
|
|
14031
14250
|
date: snapshotDate,
|
|
@@ -14035,11 +14254,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14035
14254
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14036
14255
|
}).run();
|
|
14037
14256
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
14038
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14257
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
|
|
14039
14258
|
log3.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
14040
14259
|
} catch (err) {
|
|
14041
14260
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
14042
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14261
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
|
|
14043
14262
|
log3.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
14044
14263
|
throw err;
|
|
14045
14264
|
}
|
|
@@ -14098,7 +14317,7 @@ var ProviderRegistry = class {
|
|
|
14098
14317
|
|
|
14099
14318
|
// src/scheduler.ts
|
|
14100
14319
|
import cron from "node-cron";
|
|
14101
|
-
import { eq as
|
|
14320
|
+
import { eq as eq22 } from "drizzle-orm";
|
|
14102
14321
|
var log4 = createLogger("Scheduler");
|
|
14103
14322
|
var Scheduler = class {
|
|
14104
14323
|
db;
|
|
@@ -14110,7 +14329,7 @@ var Scheduler = class {
|
|
|
14110
14329
|
}
|
|
14111
14330
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
14112
14331
|
start() {
|
|
14113
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
14332
|
+
const allSchedules = this.db.select().from(schedules).where(eq22(schedules.enabled, 1)).all();
|
|
14114
14333
|
for (const schedule of allSchedules) {
|
|
14115
14334
|
const missedRunAt = schedule.nextRunAt;
|
|
14116
14335
|
this.registerCronTask(schedule);
|
|
@@ -14135,7 +14354,7 @@ var Scheduler = class {
|
|
|
14135
14354
|
this.stopTask(projectId, existing, "Stopped");
|
|
14136
14355
|
this.tasks.delete(projectId);
|
|
14137
14356
|
}
|
|
14138
|
-
const schedule = this.db.select().from(schedules).where(
|
|
14357
|
+
const schedule = this.db.select().from(schedules).where(eq22(schedules.projectId, projectId)).get();
|
|
14139
14358
|
if (schedule && schedule.enabled === 1) {
|
|
14140
14359
|
this.registerCronTask(schedule);
|
|
14141
14360
|
}
|
|
@@ -14168,14 +14387,14 @@ var Scheduler = class {
|
|
|
14168
14387
|
this.db.update(schedules).set({
|
|
14169
14388
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
14170
14389
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14171
|
-
}).where(
|
|
14390
|
+
}).where(eq22(schedules.id, scheduleId)).run();
|
|
14172
14391
|
const label = schedule.preset ?? cronExpr;
|
|
14173
14392
|
log4.info("cron.registered", { projectId, schedule: label, timezone });
|
|
14174
14393
|
}
|
|
14175
14394
|
triggerRun(scheduleId, projectId) {
|
|
14176
14395
|
try {
|
|
14177
14396
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14178
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
14397
|
+
const currentSchedule = this.db.select().from(schedules).where(eq22(schedules.id, scheduleId)).get();
|
|
14179
14398
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
14180
14399
|
log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
|
|
14181
14400
|
this.remove(projectId);
|
|
@@ -14183,7 +14402,7 @@ var Scheduler = class {
|
|
|
14183
14402
|
}
|
|
14184
14403
|
const task = this.tasks.get(projectId);
|
|
14185
14404
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
14186
|
-
const project = this.db.select().from(projects).where(
|
|
14405
|
+
const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
14187
14406
|
if (!project) {
|
|
14188
14407
|
log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
|
|
14189
14408
|
this.remove(projectId);
|
|
@@ -14212,7 +14431,7 @@ var Scheduler = class {
|
|
|
14212
14431
|
this.db.update(schedules).set({
|
|
14213
14432
|
nextRunAt,
|
|
14214
14433
|
updatedAt: now
|
|
14215
|
-
}).where(
|
|
14434
|
+
}).where(eq22(schedules.id, currentSchedule.id)).run();
|
|
14216
14435
|
return;
|
|
14217
14436
|
}
|
|
14218
14437
|
const runId = queueResult.runId;
|
|
@@ -14220,7 +14439,7 @@ var Scheduler = class {
|
|
|
14220
14439
|
lastRunAt: now,
|
|
14221
14440
|
nextRunAt,
|
|
14222
14441
|
updatedAt: now
|
|
14223
|
-
}).where(
|
|
14442
|
+
}).where(eq22(schedules.id, currentSchedule.id)).run();
|
|
14224
14443
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
14225
14444
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
14226
14445
|
log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -14232,8 +14451,8 @@ var Scheduler = class {
|
|
|
14232
14451
|
};
|
|
14233
14452
|
|
|
14234
14453
|
// src/notifier.ts
|
|
14235
|
-
import { eq as
|
|
14236
|
-
import
|
|
14454
|
+
import { eq as eq23, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
|
|
14455
|
+
import crypto22 from "crypto";
|
|
14237
14456
|
var log5 = createLogger("Notifier");
|
|
14238
14457
|
var Notifier = class {
|
|
14239
14458
|
db;
|
|
@@ -14245,18 +14464,18 @@ var Notifier = class {
|
|
|
14245
14464
|
/** Called after a run completes (success, partial, or failed). */
|
|
14246
14465
|
async onRunCompleted(runId, projectId) {
|
|
14247
14466
|
log5.info("run.completed", { runId, projectId });
|
|
14248
|
-
const notifs = this.db.select().from(notifications).where(
|
|
14467
|
+
const notifs = this.db.select().from(notifications).where(eq23(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14249
14468
|
if (notifs.length === 0) {
|
|
14250
14469
|
log5.info("notifications.none-enabled", { projectId });
|
|
14251
14470
|
return;
|
|
14252
14471
|
}
|
|
14253
14472
|
log5.info("notifications.found", { projectId, count: notifs.length });
|
|
14254
|
-
const run = this.db.select().from(runs).where(
|
|
14473
|
+
const run = this.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
14255
14474
|
if (!run) {
|
|
14256
14475
|
log5.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
14257
14476
|
return;
|
|
14258
14477
|
}
|
|
14259
|
-
const project = this.db.select().from(projects).where(
|
|
14478
|
+
const project = this.db.select().from(projects).where(eq23(projects.id, projectId)).get();
|
|
14260
14479
|
if (!project) {
|
|
14261
14480
|
log5.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
14262
14481
|
return;
|
|
@@ -14295,11 +14514,52 @@ var Notifier = class {
|
|
|
14295
14514
|
}
|
|
14296
14515
|
}
|
|
14297
14516
|
}
|
|
14517
|
+
/** Dispatch insight webhooks for critical/high severity insights after a run. */
|
|
14518
|
+
async dispatchInsightWebhooks(runId, projectId, result) {
|
|
14519
|
+
const insightEvents = [];
|
|
14520
|
+
const criticalInsights = result.insights.filter((i) => i.severity === "critical");
|
|
14521
|
+
const highInsights = result.insights.filter((i) => i.severity === "high");
|
|
14522
|
+
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
14523
|
+
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
14524
|
+
if (insightEvents.length === 0) return;
|
|
14525
|
+
const notifs = this.db.select().from(notifications).where(eq23(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14526
|
+
if (notifs.length === 0) return;
|
|
14527
|
+
const run = this.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
14528
|
+
if (!run) return;
|
|
14529
|
+
const project = this.db.select().from(projects).where(eq23(projects.id, projectId)).get();
|
|
14530
|
+
if (!project) return;
|
|
14531
|
+
for (const notif of notifs) {
|
|
14532
|
+
const config = parseJsonColumn(notif.config, { url: "", events: [] });
|
|
14533
|
+
if (!config.url) continue;
|
|
14534
|
+
const subscribedEvents = config.events;
|
|
14535
|
+
const matchingEvents = insightEvents.filter((e) => subscribedEvents.includes(e));
|
|
14536
|
+
if (matchingEvents.length === 0) continue;
|
|
14537
|
+
for (const event of matchingEvents) {
|
|
14538
|
+
const relevantInsights = event === "insight.critical" ? criticalInsights : highInsights;
|
|
14539
|
+
const payload = {
|
|
14540
|
+
source: "canonry",
|
|
14541
|
+
event,
|
|
14542
|
+
project: { name: project.name, canonicalDomain: project.canonicalDomain },
|
|
14543
|
+
run: { id: run.id, status: run.status, finishedAt: run.finishedAt },
|
|
14544
|
+
insights: relevantInsights.map((i) => ({
|
|
14545
|
+
id: i.id,
|
|
14546
|
+
type: i.type,
|
|
14547
|
+
severity: i.severity,
|
|
14548
|
+
title: i.title,
|
|
14549
|
+
keyword: i.keyword,
|
|
14550
|
+
provider: i.provider
|
|
14551
|
+
})),
|
|
14552
|
+
dashboardUrl: `${this.serverUrl}/projects/${project.name}`
|
|
14553
|
+
};
|
|
14554
|
+
await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null);
|
|
14555
|
+
}
|
|
14556
|
+
}
|
|
14557
|
+
}
|
|
14298
14558
|
computeTransitions(runId, projectId) {
|
|
14299
14559
|
const recentRuns = this.db.select().from(runs).where(
|
|
14300
14560
|
and10(
|
|
14301
|
-
|
|
14302
|
-
or2(
|
|
14561
|
+
eq23(runs.projectId, projectId),
|
|
14562
|
+
or2(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
|
|
14303
14563
|
)
|
|
14304
14564
|
).orderBy(desc8(runs.createdAt)).limit(2).all();
|
|
14305
14565
|
if (recentRuns.length < 2) return [];
|
|
@@ -14311,12 +14571,12 @@ var Notifier = class {
|
|
|
14311
14571
|
keyword: keywords.keyword,
|
|
14312
14572
|
provider: querySnapshots.provider,
|
|
14313
14573
|
citationState: querySnapshots.citationState
|
|
14314
|
-
}).from(querySnapshots).leftJoin(keywords,
|
|
14574
|
+
}).from(querySnapshots).leftJoin(keywords, eq23(querySnapshots.keywordId, keywords.id)).where(eq23(querySnapshots.runId, currentRunId)).all();
|
|
14315
14575
|
const previousSnapshots = this.db.select({
|
|
14316
14576
|
keywordId: querySnapshots.keywordId,
|
|
14317
14577
|
provider: querySnapshots.provider,
|
|
14318
14578
|
citationState: querySnapshots.citationState
|
|
14319
|
-
}).from(querySnapshots).where(
|
|
14579
|
+
}).from(querySnapshots).where(eq23(querySnapshots.runId, previousRunId)).all();
|
|
14320
14580
|
const prevMap = /* @__PURE__ */ new Map();
|
|
14321
14581
|
for (const s of previousSnapshots) {
|
|
14322
14582
|
prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
|
|
@@ -14374,7 +14634,7 @@ var Notifier = class {
|
|
|
14374
14634
|
}
|
|
14375
14635
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
14376
14636
|
this.db.insert(auditLog).values({
|
|
14377
|
-
id:
|
|
14637
|
+
id: crypto22.randomUUID(),
|
|
14378
14638
|
projectId,
|
|
14379
14639
|
actor: "scheduler",
|
|
14380
14640
|
action: `notification.${status}`,
|
|
@@ -14389,13 +14649,26 @@ var Notifier = class {
|
|
|
14389
14649
|
// src/run-coordinator.ts
|
|
14390
14650
|
var log6 = createLogger("RunCoordinator");
|
|
14391
14651
|
var RunCoordinator = class {
|
|
14392
|
-
constructor(notifier, intelligenceService) {
|
|
14652
|
+
constructor(notifier, intelligenceService, onInsightsGenerated) {
|
|
14393
14653
|
this.notifier = notifier;
|
|
14394
14654
|
this.intelligenceService = intelligenceService;
|
|
14655
|
+
this.onInsightsGenerated = onInsightsGenerated;
|
|
14395
14656
|
}
|
|
14396
14657
|
async onRunCompleted(runId, projectId) {
|
|
14397
14658
|
try {
|
|
14398
|
-
|
|
14659
|
+
const result = this.intelligenceService.analyzeAndPersist(runId, projectId);
|
|
14660
|
+
if (result && this.onInsightsGenerated) {
|
|
14661
|
+
const hasHighSeverity = result.insights.some(
|
|
14662
|
+
(i) => i.severity === "critical" || i.severity === "high"
|
|
14663
|
+
);
|
|
14664
|
+
if (hasHighSeverity) {
|
|
14665
|
+
try {
|
|
14666
|
+
await this.onInsightsGenerated(runId, projectId, result);
|
|
14667
|
+
} catch (err) {
|
|
14668
|
+
log6.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
14669
|
+
}
|
|
14670
|
+
}
|
|
14671
|
+
}
|
|
14399
14672
|
} catch (err) {
|
|
14400
14673
|
log6.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
14401
14674
|
}
|
|
@@ -14429,13 +14702,13 @@ function extractHostname(domain) {
|
|
|
14429
14702
|
function fetchWithPinnedAddress(target) {
|
|
14430
14703
|
return new Promise((resolve) => {
|
|
14431
14704
|
const port = target.url.port ? Number(target.url.port) : 443;
|
|
14432
|
-
const
|
|
14705
|
+
const path9 = target.url.pathname + target.url.search;
|
|
14433
14706
|
const req = https2.request(
|
|
14434
14707
|
{
|
|
14435
14708
|
hostname: target.address,
|
|
14436
14709
|
family: target.family,
|
|
14437
14710
|
port,
|
|
14438
|
-
path:
|
|
14711
|
+
path: path9,
|
|
14439
14712
|
method: "GET",
|
|
14440
14713
|
timeout: FETCH_TIMEOUT_MS,
|
|
14441
14714
|
servername: target.url.hostname,
|
|
@@ -15124,10 +15397,503 @@ function clipText(value, length) {
|
|
|
15124
15397
|
return `${value.slice(0, length - 3)}...`;
|
|
15125
15398
|
}
|
|
15126
15399
|
|
|
15400
|
+
// src/agent-manager.ts
|
|
15401
|
+
import { execFileSync, spawn } from "child_process";
|
|
15402
|
+
import fs5 from "fs";
|
|
15403
|
+
import path6 from "path";
|
|
15404
|
+
var log8 = createLogger("AgentManager");
|
|
15405
|
+
var PROCESS_MARKER = "canonry-openclaw-gateway";
|
|
15406
|
+
var AgentManager = class {
|
|
15407
|
+
constructor(config, stateDir) {
|
|
15408
|
+
this.config = config;
|
|
15409
|
+
this.stateDir = stateDir;
|
|
15410
|
+
this.processJsonPath = path6.join(stateDir, "process.json");
|
|
15411
|
+
}
|
|
15412
|
+
processJsonPath;
|
|
15413
|
+
/**
|
|
15414
|
+
* Check if the gateway process is running.
|
|
15415
|
+
* Cleans up stale process.json if the process is dead or belongs to a
|
|
15416
|
+
* different process (PID reuse).
|
|
15417
|
+
*/
|
|
15418
|
+
status() {
|
|
15419
|
+
const info = this.readProcessInfo();
|
|
15420
|
+
if (!info) {
|
|
15421
|
+
return { state: "stopped" };
|
|
15422
|
+
}
|
|
15423
|
+
if (info.marker !== PROCESS_MARKER) {
|
|
15424
|
+
this.removeProcessJson();
|
|
15425
|
+
return { state: "stopped" };
|
|
15426
|
+
}
|
|
15427
|
+
if (isProcessAlive(info.pid) && this.verifyProcessIdentity(info.pid)) {
|
|
15428
|
+
return {
|
|
15429
|
+
state: "running",
|
|
15430
|
+
pid: info.pid,
|
|
15431
|
+
port: info.gatewayPort,
|
|
15432
|
+
startedAt: info.startedAt
|
|
15433
|
+
};
|
|
15434
|
+
}
|
|
15435
|
+
this.removeProcessJson();
|
|
15436
|
+
return { state: "stopped" };
|
|
15437
|
+
}
|
|
15438
|
+
/**
|
|
15439
|
+
* Start the OpenClaw gateway as a detached background process.
|
|
15440
|
+
* Idempotent — no-op if already running.
|
|
15441
|
+
* Waits briefly for the process to confirm it hasn't crashed on startup.
|
|
15442
|
+
*/
|
|
15443
|
+
async start() {
|
|
15444
|
+
const currentStatus = this.status();
|
|
15445
|
+
if (currentStatus.state === "running") {
|
|
15446
|
+
log8.info("already.running", { pid: currentStatus.pid });
|
|
15447
|
+
return;
|
|
15448
|
+
}
|
|
15449
|
+
const binary = this.config.binary ?? "openclaw";
|
|
15450
|
+
const profile = this.config.profile ?? "aero";
|
|
15451
|
+
const port = this.config.gatewayPort ?? 3579;
|
|
15452
|
+
if (!fs5.existsSync(this.stateDir)) {
|
|
15453
|
+
fs5.mkdirSync(this.stateDir, { recursive: true });
|
|
15454
|
+
}
|
|
15455
|
+
const logFile = path6.join(this.stateDir, "gateway.log");
|
|
15456
|
+
const logFd = fs5.openSync(logFile, "a");
|
|
15457
|
+
const dotEnv = this.loadDotEnv();
|
|
15458
|
+
const child = spawn(binary, ["--profile", profile, "gateway"], {
|
|
15459
|
+
detached: true,
|
|
15460
|
+
stdio: ["ignore", logFd, logFd],
|
|
15461
|
+
env: {
|
|
15462
|
+
...process.env,
|
|
15463
|
+
...dotEnv,
|
|
15464
|
+
OPENCLAW_PROFILE: profile,
|
|
15465
|
+
OPENCLAW_GATEWAY_PORT: String(port),
|
|
15466
|
+
OPENCLAW_STATE_DIR: this.stateDir
|
|
15467
|
+
}
|
|
15468
|
+
});
|
|
15469
|
+
const startupResult = await new Promise((resolve) => {
|
|
15470
|
+
let settled = false;
|
|
15471
|
+
const settle = (r) => {
|
|
15472
|
+
if (settled) return;
|
|
15473
|
+
settled = true;
|
|
15474
|
+
resolve(r);
|
|
15475
|
+
};
|
|
15476
|
+
child.on("error", (err) => settle({ error: err }));
|
|
15477
|
+
child.on("exit", (code) => settle({ exitCode: code }));
|
|
15478
|
+
setTimeout(() => settle({}), 500);
|
|
15479
|
+
});
|
|
15480
|
+
child.unref();
|
|
15481
|
+
fs5.closeSync(logFd);
|
|
15482
|
+
if (startupResult.error) {
|
|
15483
|
+
throw new Error(`Failed to start OpenClaw gateway: ${startupResult.error.message}`);
|
|
15484
|
+
}
|
|
15485
|
+
if (startupResult.exitCode != null) {
|
|
15486
|
+
throw new Error(`OpenClaw gateway exited immediately (code ${startupResult.exitCode}). Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
|
|
15487
|
+
}
|
|
15488
|
+
if (child.pid == null) {
|
|
15489
|
+
throw new Error("Failed to start OpenClaw gateway: no PID returned by spawn");
|
|
15490
|
+
}
|
|
15491
|
+
if (!isProcessAlive(child.pid)) {
|
|
15492
|
+
throw new Error(`OpenClaw gateway exited immediately after spawn. Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
|
|
15493
|
+
}
|
|
15494
|
+
const processInfo = {
|
|
15495
|
+
pid: child.pid,
|
|
15496
|
+
gatewayPort: port,
|
|
15497
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15498
|
+
marker: PROCESS_MARKER
|
|
15499
|
+
};
|
|
15500
|
+
fs5.writeFileSync(this.processJsonPath, JSON.stringify(processInfo, null, 2), "utf-8");
|
|
15501
|
+
log8.info("started", { pid: child.pid, port });
|
|
15502
|
+
}
|
|
15503
|
+
/**
|
|
15504
|
+
* Stop the gateway process.
|
|
15505
|
+
* Uses DenchClaw escalation: SIGTERM → 800ms poll → SIGKILL.
|
|
15506
|
+
* Idempotent — no-op if already stopped.
|
|
15507
|
+
*/
|
|
15508
|
+
async stop() {
|
|
15509
|
+
const info = this.readProcessInfo();
|
|
15510
|
+
if (!info) return;
|
|
15511
|
+
if (isProcessAlive(info.pid) && info.marker === PROCESS_MARKER && this.verifyProcessIdentity(info.pid)) {
|
|
15512
|
+
await terminateWithEscalation(info.pid);
|
|
15513
|
+
}
|
|
15514
|
+
this.removeProcessJson();
|
|
15515
|
+
log8.info("stopped", { pid: info.pid });
|
|
15516
|
+
}
|
|
15517
|
+
/**
|
|
15518
|
+
* Stop the gateway, wipe the workspace directory, and prepare for re-seeding.
|
|
15519
|
+
*/
|
|
15520
|
+
async reset() {
|
|
15521
|
+
await this.stop();
|
|
15522
|
+
const workspaceDir = path6.join(this.stateDir, "workspace");
|
|
15523
|
+
if (fs5.existsSync(workspaceDir)) {
|
|
15524
|
+
fs5.rmSync(workspaceDir, { recursive: true, force: true });
|
|
15525
|
+
log8.info("workspace.wiped", { dir: workspaceDir });
|
|
15526
|
+
}
|
|
15527
|
+
}
|
|
15528
|
+
/**
|
|
15529
|
+
* Verify that the PID actually belongs to an openclaw process by checking
|
|
15530
|
+
* the full command line. Requires "openclaw" in the args to avoid matching
|
|
15531
|
+
* unrelated Node processes after PID reuse.
|
|
15532
|
+
*/
|
|
15533
|
+
verifyProcessIdentity(pid) {
|
|
15534
|
+
try {
|
|
15535
|
+
if (process.platform === "darwin") {
|
|
15536
|
+
const out = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
15537
|
+
encoding: "utf-8",
|
|
15538
|
+
timeout: 2e3
|
|
15539
|
+
}).trim();
|
|
15540
|
+
return out.includes("openclaw");
|
|
15541
|
+
}
|
|
15542
|
+
if (process.platform === "linux") {
|
|
15543
|
+
const cmdline = fs5.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
15544
|
+
return cmdline.includes("openclaw");
|
|
15545
|
+
}
|
|
15546
|
+
return true;
|
|
15547
|
+
} catch {
|
|
15548
|
+
return false;
|
|
15549
|
+
}
|
|
15550
|
+
}
|
|
15551
|
+
readProcessInfo() {
|
|
15552
|
+
if (!fs5.existsSync(this.processJsonPath)) return null;
|
|
15553
|
+
try {
|
|
15554
|
+
return JSON.parse(fs5.readFileSync(this.processJsonPath, "utf-8"));
|
|
15555
|
+
} catch {
|
|
15556
|
+
return null;
|
|
15557
|
+
}
|
|
15558
|
+
}
|
|
15559
|
+
removeProcessJson() {
|
|
15560
|
+
try {
|
|
15561
|
+
fs5.unlinkSync(this.processJsonPath);
|
|
15562
|
+
} catch {
|
|
15563
|
+
}
|
|
15564
|
+
}
|
|
15565
|
+
/** Parse a simple KEY=value dotenv file from the state dir. */
|
|
15566
|
+
loadDotEnv() {
|
|
15567
|
+
const envFile = path6.join(this.stateDir, ".env");
|
|
15568
|
+
if (!fs5.existsSync(envFile)) return {};
|
|
15569
|
+
const result = {};
|
|
15570
|
+
for (const line of fs5.readFileSync(envFile, "utf-8").split("\n")) {
|
|
15571
|
+
const trimmed = line.trim();
|
|
15572
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
15573
|
+
const eq25 = trimmed.indexOf("=");
|
|
15574
|
+
if (eq25 < 1) continue;
|
|
15575
|
+
result[trimmed.slice(0, eq25)] = trimmed.slice(eq25 + 1);
|
|
15576
|
+
}
|
|
15577
|
+
return result;
|
|
15578
|
+
}
|
|
15579
|
+
};
|
|
15580
|
+
function isProcessAlive(pid) {
|
|
15581
|
+
try {
|
|
15582
|
+
process.kill(pid, 0);
|
|
15583
|
+
return true;
|
|
15584
|
+
} catch (err) {
|
|
15585
|
+
if (err.code === "EPERM") return true;
|
|
15586
|
+
return false;
|
|
15587
|
+
}
|
|
15588
|
+
}
|
|
15589
|
+
async function terminateWithEscalation(pid) {
|
|
15590
|
+
try {
|
|
15591
|
+
process.kill(pid, "SIGTERM");
|
|
15592
|
+
} catch {
|
|
15593
|
+
return;
|
|
15594
|
+
}
|
|
15595
|
+
const deadline = Date.now() + 800;
|
|
15596
|
+
while (Date.now() < deadline) {
|
|
15597
|
+
if (!isProcessAlive(pid)) return;
|
|
15598
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
15599
|
+
}
|
|
15600
|
+
try {
|
|
15601
|
+
process.kill(pid, "SIGKILL");
|
|
15602
|
+
} catch {
|
|
15603
|
+
}
|
|
15604
|
+
}
|
|
15605
|
+
|
|
15606
|
+
// src/agent-bootstrap.ts
|
|
15607
|
+
import { execFileSync as execFileSync2, execSync } from "child_process";
|
|
15608
|
+
import fs6 from "fs";
|
|
15609
|
+
import os5 from "os";
|
|
15610
|
+
import path7 from "path";
|
|
15611
|
+
import { fileURLToPath } from "url";
|
|
15612
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
15613
|
+
var OPENCLAW_VERSION = "2026.4.14";
|
|
15614
|
+
var OPENCLAW_PACKAGE_SPEC = `openclaw@${OPENCLAW_VERSION}`;
|
|
15615
|
+
var MIN_NODE_VERSION = "22.14.0";
|
|
15616
|
+
var cachedResult = null;
|
|
15617
|
+
var cachedAt = 0;
|
|
15618
|
+
function getAeroStateDir(profile = "aero") {
|
|
15619
|
+
return path7.join(os5.homedir(), `.openclaw-${profile}`);
|
|
15620
|
+
}
|
|
15621
|
+
async function detectOpenClaw(config) {
|
|
15622
|
+
if (cachedResult && Date.now() - cachedAt < CACHE_TTL_MS) {
|
|
15623
|
+
return cachedResult;
|
|
15624
|
+
}
|
|
15625
|
+
let result;
|
|
15626
|
+
if (config?.binary) {
|
|
15627
|
+
const version = probeVersion(config.binary);
|
|
15628
|
+
if (version) {
|
|
15629
|
+
result = { found: true, path: config.binary, version };
|
|
15630
|
+
cachedResult = result;
|
|
15631
|
+
cachedAt = Date.now();
|
|
15632
|
+
return result;
|
|
15633
|
+
}
|
|
15634
|
+
}
|
|
15635
|
+
const binaryPath = findInPath();
|
|
15636
|
+
if (binaryPath) {
|
|
15637
|
+
const version = probeVersion(binaryPath);
|
|
15638
|
+
if (version) {
|
|
15639
|
+
result = { found: true, path: binaryPath, version };
|
|
15640
|
+
cachedResult = result;
|
|
15641
|
+
cachedAt = Date.now();
|
|
15642
|
+
return result;
|
|
15643
|
+
}
|
|
15644
|
+
}
|
|
15645
|
+
result = { found: false };
|
|
15646
|
+
cachedResult = result;
|
|
15647
|
+
cachedAt = Date.now();
|
|
15648
|
+
return result;
|
|
15649
|
+
}
|
|
15650
|
+
detectOpenClaw.resetCache = () => {
|
|
15651
|
+
cachedResult = null;
|
|
15652
|
+
cachedAt = 0;
|
|
15653
|
+
};
|
|
15654
|
+
function probeVersion(binaryPath) {
|
|
15655
|
+
try {
|
|
15656
|
+
const output = execFileSync2(binaryPath, ["--version"], {
|
|
15657
|
+
timeout: 5e3,
|
|
15658
|
+
encoding: "utf-8"
|
|
15659
|
+
});
|
|
15660
|
+
const match = output.toString().trim().match(/(\d+\.\d+\.\d+)/);
|
|
15661
|
+
return match ? match[1] : output.toString().trim();
|
|
15662
|
+
} catch {
|
|
15663
|
+
return null;
|
|
15664
|
+
}
|
|
15665
|
+
}
|
|
15666
|
+
function findInPath() {
|
|
15667
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
15668
|
+
try {
|
|
15669
|
+
const output = execFileSync2(cmd, ["openclaw"], {
|
|
15670
|
+
timeout: 5e3,
|
|
15671
|
+
encoding: "utf-8"
|
|
15672
|
+
});
|
|
15673
|
+
return output.toString().trim().split("\n")[0] || null;
|
|
15674
|
+
} catch {
|
|
15675
|
+
return null;
|
|
15676
|
+
}
|
|
15677
|
+
}
|
|
15678
|
+
async function installOpenClaw(opts) {
|
|
15679
|
+
const unsupportedNodeError = getUnsupportedNodeError(opts?.nodeVersion);
|
|
15680
|
+
if (unsupportedNodeError) {
|
|
15681
|
+
return {
|
|
15682
|
+
success: false,
|
|
15683
|
+
error: unsupportedNodeError
|
|
15684
|
+
};
|
|
15685
|
+
}
|
|
15686
|
+
try {
|
|
15687
|
+
execSync(`npm install -g ${OPENCLAW_PACKAGE_SPEC}`, {
|
|
15688
|
+
timeout: 12e4,
|
|
15689
|
+
stdio: opts?.silent ? "pipe" : "inherit"
|
|
15690
|
+
});
|
|
15691
|
+
} catch (err) {
|
|
15692
|
+
return {
|
|
15693
|
+
success: false,
|
|
15694
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15695
|
+
};
|
|
15696
|
+
}
|
|
15697
|
+
detectOpenClaw.resetCache();
|
|
15698
|
+
const detection = await detectOpenClaw();
|
|
15699
|
+
if (!detection.found) {
|
|
15700
|
+
return {
|
|
15701
|
+
success: false,
|
|
15702
|
+
error: `npm install succeeded but the ${OPENCLAW_PACKAGE_SPEC} binary was not found in PATH`
|
|
15703
|
+
};
|
|
15704
|
+
}
|
|
15705
|
+
if (detection.version) {
|
|
15706
|
+
const expectedVersion = parseVersionTuple(OPENCLAW_VERSION);
|
|
15707
|
+
const detectedVersion = parseVersionTuple(detection.version);
|
|
15708
|
+
if (expectedVersion && detectedVersion && compareVersionTuples(detectedVersion, expectedVersion) !== 0) {
|
|
15709
|
+
return {
|
|
15710
|
+
success: false,
|
|
15711
|
+
error: `Installed OpenClaw binary reports version ${detection.version}, but Canonry pinned ${OPENCLAW_VERSION}. A different openclaw binary may be shadowing the npm-installed package in PATH.`
|
|
15712
|
+
};
|
|
15713
|
+
}
|
|
15714
|
+
}
|
|
15715
|
+
return { success: true, detection };
|
|
15716
|
+
}
|
|
15717
|
+
function getUnsupportedNodeError(currentNodeVersionOverride) {
|
|
15718
|
+
const currentNodeVersion = normalizeVersion(currentNodeVersionOverride ?? process.versions.node);
|
|
15719
|
+
const minimumTuple = parseVersionTuple(MIN_NODE_VERSION);
|
|
15720
|
+
const currentTuple = parseVersionTuple(currentNodeVersion);
|
|
15721
|
+
if (!minimumTuple || !currentTuple || compareVersionTuples(currentTuple, minimumTuple) >= 0) {
|
|
15722
|
+
return null;
|
|
15723
|
+
}
|
|
15724
|
+
return `Canonry requires Node.js >=${MIN_NODE_VERSION} and installs pinned OpenClaw ${OPENCLAW_VERSION}, but the current runtime is ${currentNodeVersion}. Upgrade Node.js before running "canonry agent setup".`;
|
|
15725
|
+
}
|
|
15726
|
+
function normalizeVersion(version) {
|
|
15727
|
+
const tuple = parseVersionTuple(version);
|
|
15728
|
+
if (!tuple) {
|
|
15729
|
+
return version.trim().replace(/^v/i, "");
|
|
15730
|
+
}
|
|
15731
|
+
return tuple.join(".");
|
|
15732
|
+
}
|
|
15733
|
+
function parseVersionTuple(version) {
|
|
15734
|
+
const match = version.trim().replace(/^v/i, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
|
|
15735
|
+
if (!match) {
|
|
15736
|
+
return null;
|
|
15737
|
+
}
|
|
15738
|
+
return [
|
|
15739
|
+
Number(match[1]),
|
|
15740
|
+
Number(match[2] ?? 0),
|
|
15741
|
+
Number(match[3] ?? 0)
|
|
15742
|
+
];
|
|
15743
|
+
}
|
|
15744
|
+
function compareVersionTuples(left, right) {
|
|
15745
|
+
for (let index = 0; index < left.length; index++) {
|
|
15746
|
+
const delta = left[index] - right[index];
|
|
15747
|
+
if (delta !== 0) {
|
|
15748
|
+
return delta;
|
|
15749
|
+
}
|
|
15750
|
+
}
|
|
15751
|
+
return 0;
|
|
15752
|
+
}
|
|
15753
|
+
function seedWorkspace(stateDir) {
|
|
15754
|
+
const workspaceDir = path7.join(stateDir, "workspace");
|
|
15755
|
+
fs6.mkdirSync(workspaceDir, { recursive: true });
|
|
15756
|
+
const __dirname = path7.dirname(fileURLToPath(import.meta.url));
|
|
15757
|
+
const assetsDir = path7.join(__dirname, "..", "assets", "agent-workspace");
|
|
15758
|
+
if (!fs6.existsSync(assetsDir)) {
|
|
15759
|
+
return;
|
|
15760
|
+
}
|
|
15761
|
+
copyDirRecursive(assetsDir, workspaceDir);
|
|
15762
|
+
}
|
|
15763
|
+
function initializeOpenClawProfile(binary, profile, workspaceDir) {
|
|
15764
|
+
try {
|
|
15765
|
+
execFileSync2(binary, [
|
|
15766
|
+
"--profile",
|
|
15767
|
+
profile,
|
|
15768
|
+
"onboard",
|
|
15769
|
+
"--non-interactive",
|
|
15770
|
+
"--accept-risk",
|
|
15771
|
+
"--mode",
|
|
15772
|
+
"local",
|
|
15773
|
+
"--workspace",
|
|
15774
|
+
workspaceDir,
|
|
15775
|
+
"--skip-channels",
|
|
15776
|
+
"--skip-skills",
|
|
15777
|
+
"--skip-health",
|
|
15778
|
+
"--no-install-daemon"
|
|
15779
|
+
], { timeout: 3e4, stdio: "pipe" });
|
|
15780
|
+
} catch (err) {
|
|
15781
|
+
const stderr = err instanceof Error && "stderr" in err ? String(err.stderr) : "";
|
|
15782
|
+
if (stderr.toLowerCase().includes("already")) return;
|
|
15783
|
+
throw new CliError({
|
|
15784
|
+
code: "AGENT_PROFILE_INIT_FAILED",
|
|
15785
|
+
message: `Failed to initialize OpenClaw profile: ${stderr || (err instanceof Error ? err.message : String(err))}`,
|
|
15786
|
+
displayMessage: `Failed to initialize OpenClaw profile "${profile}".`
|
|
15787
|
+
});
|
|
15788
|
+
}
|
|
15789
|
+
}
|
|
15790
|
+
function configureOpenClawGateway(binary, profile, gatewayPort) {
|
|
15791
|
+
const entries = [
|
|
15792
|
+
["gateway.mode", "local", false],
|
|
15793
|
+
["gateway.port", String(gatewayPort), true]
|
|
15794
|
+
];
|
|
15795
|
+
for (const [key, value, strict] of entries) {
|
|
15796
|
+
try {
|
|
15797
|
+
const args = ["--profile", profile, "config", "set", key, value];
|
|
15798
|
+
if (strict) args.push("--strict-json");
|
|
15799
|
+
execFileSync2(binary, args, { timeout: 1e4, stdio: "pipe" });
|
|
15800
|
+
} catch (err) {
|
|
15801
|
+
throw new CliError({
|
|
15802
|
+
code: "AGENT_GATEWAY_CONFIG_FAILED",
|
|
15803
|
+
message: `Failed to set ${key}=${value}: ${err instanceof Error ? err.message : String(err)}`,
|
|
15804
|
+
displayMessage: `Failed to configure OpenClaw gateway (${key}).`
|
|
15805
|
+
});
|
|
15806
|
+
}
|
|
15807
|
+
}
|
|
15808
|
+
}
|
|
15809
|
+
function setOpenClawModel(binary, profile, model) {
|
|
15810
|
+
try {
|
|
15811
|
+
execFileSync2(binary, [
|
|
15812
|
+
"--profile",
|
|
15813
|
+
profile,
|
|
15814
|
+
"models",
|
|
15815
|
+
"set",
|
|
15816
|
+
model
|
|
15817
|
+
], { timeout: 1e4, stdio: "pipe" });
|
|
15818
|
+
} catch (err) {
|
|
15819
|
+
throw new CliError({
|
|
15820
|
+
code: "AGENT_MODEL_SET_FAILED",
|
|
15821
|
+
message: `Failed to set agent model to ${model}: ${err instanceof Error ? err.message : String(err)}`,
|
|
15822
|
+
displayMessage: `Failed to set agent model to "${model}".`
|
|
15823
|
+
});
|
|
15824
|
+
}
|
|
15825
|
+
}
|
|
15826
|
+
function providerEnvVar(provider) {
|
|
15827
|
+
const map = {
|
|
15828
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
15829
|
+
openai: "OPENAI_API_KEY",
|
|
15830
|
+
google: "GOOGLE_API_KEY",
|
|
15831
|
+
"google-vertex": "GOOGLE_API_KEY",
|
|
15832
|
+
groq: "GROQ_API_KEY",
|
|
15833
|
+
mistral: "MISTRAL_API_KEY",
|
|
15834
|
+
xai: "XAI_API_KEY",
|
|
15835
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
15836
|
+
cerebras: "CEREBRAS_API_KEY"
|
|
15837
|
+
};
|
|
15838
|
+
return map[provider] ?? `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
15839
|
+
}
|
|
15840
|
+
function writeAgentEnv(stateDir, key, value) {
|
|
15841
|
+
const envFile = path7.join(stateDir, ".env");
|
|
15842
|
+
let lines = [];
|
|
15843
|
+
if (fs6.existsSync(envFile)) {
|
|
15844
|
+
lines = fs6.readFileSync(envFile, "utf-8").split("\n");
|
|
15845
|
+
}
|
|
15846
|
+
const prefix = `${key}=`;
|
|
15847
|
+
const idx = lines.findIndex((l) => l.startsWith(prefix));
|
|
15848
|
+
const entry = `${key}=${value}`;
|
|
15849
|
+
if (idx >= 0) {
|
|
15850
|
+
lines[idx] = entry;
|
|
15851
|
+
} else {
|
|
15852
|
+
lines.push(entry);
|
|
15853
|
+
}
|
|
15854
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
15855
|
+
fs6.writeFileSync(envFile, lines.join("\n") + "\n", "utf-8");
|
|
15856
|
+
}
|
|
15857
|
+
function resolveAgentCredentials(opts) {
|
|
15858
|
+
const provider = opts.agentProvider ?? "anthropic";
|
|
15859
|
+
if (opts.agentKey) {
|
|
15860
|
+
return { provider, key: opts.agentKey, model: opts.agentModel };
|
|
15861
|
+
}
|
|
15862
|
+
const envVar = providerEnvVar(provider);
|
|
15863
|
+
const envKey = process.env[envVar];
|
|
15864
|
+
if (envKey) {
|
|
15865
|
+
return { provider, key: envKey, model: opts.agentModel };
|
|
15866
|
+
}
|
|
15867
|
+
const genericKey = process.env.CANONRY_AGENT_KEY;
|
|
15868
|
+
if (genericKey) {
|
|
15869
|
+
return { provider, key: genericKey, model: opts.agentModel };
|
|
15870
|
+
}
|
|
15871
|
+
const envFile = path7.join(opts.stateDir, ".env");
|
|
15872
|
+
if (fs6.existsSync(envFile)) {
|
|
15873
|
+
const hasKey = fs6.readFileSync(envFile, "utf-8").split("\n").some((l) => l.includes("_API_KEY="));
|
|
15874
|
+
if (hasKey) {
|
|
15875
|
+
return { provider, key: void 0, model: opts.agentModel };
|
|
15876
|
+
}
|
|
15877
|
+
}
|
|
15878
|
+
return { provider, key: void 0, model: opts.agentModel };
|
|
15879
|
+
}
|
|
15880
|
+
function copyDirRecursive(src, dest) {
|
|
15881
|
+
fs6.mkdirSync(dest, { recursive: true });
|
|
15882
|
+
for (const entry of fs6.readdirSync(src, { withFileTypes: true })) {
|
|
15883
|
+
const srcPath = path7.join(src, entry.name);
|
|
15884
|
+
const destPath = path7.join(dest, entry.name);
|
|
15885
|
+
if (entry.isDirectory()) {
|
|
15886
|
+
copyDirRecursive(srcPath, destPath);
|
|
15887
|
+
} else {
|
|
15888
|
+
fs6.copyFileSync(srcPath, destPath);
|
|
15889
|
+
}
|
|
15890
|
+
}
|
|
15891
|
+
}
|
|
15892
|
+
|
|
15127
15893
|
// src/server.ts
|
|
15128
15894
|
var _require2 = createRequire2(import.meta.url);
|
|
15129
15895
|
var { version: PKG_VERSION } = _require2("../package.json");
|
|
15130
|
-
var
|
|
15896
|
+
var log9 = createLogger("Server");
|
|
15131
15897
|
var DEFAULT_QUOTA = {
|
|
15132
15898
|
maxConcurrency: 2,
|
|
15133
15899
|
maxRequestsPerMinute: 10,
|
|
@@ -15158,7 +15924,7 @@ function summarizeProviderConfig(provider, config) {
|
|
|
15158
15924
|
};
|
|
15159
15925
|
}
|
|
15160
15926
|
function hashApiKey(key) {
|
|
15161
|
-
return
|
|
15927
|
+
return crypto23.createHash("sha256").update(key).digest("hex");
|
|
15162
15928
|
}
|
|
15163
15929
|
function parseCookies2(header) {
|
|
15164
15930
|
if (!header) return {};
|
|
@@ -15223,7 +15989,7 @@ function migrateDbCredentialsToConfig(db, config) {
|
|
|
15223
15989
|
}
|
|
15224
15990
|
if (migrated > 0) {
|
|
15225
15991
|
saveConfigPatch({ google: config.google });
|
|
15226
|
-
|
|
15992
|
+
log9.info("credentials.migrated", { type: "google", count: migrated });
|
|
15227
15993
|
}
|
|
15228
15994
|
}
|
|
15229
15995
|
const gaColCheck = db.all(sql6.raw(
|
|
@@ -15235,7 +16001,7 @@ function migrateDbCredentialsToConfig(db, config) {
|
|
|
15235
16001
|
));
|
|
15236
16002
|
let migrated = 0;
|
|
15237
16003
|
for (const row of rows) {
|
|
15238
|
-
const project = db.select({ name: projects.name }).from(projects).where(
|
|
16004
|
+
const project = db.select({ name: projects.name }).from(projects).where(eq24(projects.id, row.project_id)).get();
|
|
15239
16005
|
if (!project) continue;
|
|
15240
16006
|
const existing = getGa4Connection(config, project.name);
|
|
15241
16007
|
if (existing?.privateKey) continue;
|
|
@@ -15251,7 +16017,7 @@ function migrateDbCredentialsToConfig(db, config) {
|
|
|
15251
16017
|
}
|
|
15252
16018
|
if (migrated > 0) {
|
|
15253
16019
|
saveConfigPatch({ ga4: config.ga4 });
|
|
15254
|
-
|
|
16020
|
+
log9.info("credentials.migrated", { type: "ga4", count: migrated });
|
|
15255
16021
|
}
|
|
15256
16022
|
}
|
|
15257
16023
|
} catch {
|
|
@@ -15282,7 +16048,7 @@ async function createServer(opts) {
|
|
|
15282
16048
|
};
|
|
15283
16049
|
}
|
|
15284
16050
|
migrateDbCredentialsToConfig(opts.db, opts.config);
|
|
15285
|
-
|
|
16051
|
+
log9.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
|
|
15286
16052
|
const p = providers[k];
|
|
15287
16053
|
return p?.apiKey || p?.baseUrl || p?.vertexProject;
|
|
15288
16054
|
}) });
|
|
@@ -15319,9 +16085,28 @@ async function createServer(opts) {
|
|
|
15319
16085
|
jobRunner.recoverStaleRuns();
|
|
15320
16086
|
const notifier = new Notifier(opts.db, serverUrl);
|
|
15321
16087
|
const intelligenceService = new IntelligenceService(opts.db);
|
|
15322
|
-
const runCoordinator = new RunCoordinator(
|
|
16088
|
+
const runCoordinator = new RunCoordinator(
|
|
16089
|
+
notifier,
|
|
16090
|
+
intelligenceService,
|
|
16091
|
+
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result)
|
|
16092
|
+
);
|
|
15323
16093
|
jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
|
|
15324
16094
|
const snapshotService = new SnapshotService(registry);
|
|
16095
|
+
let agentManager;
|
|
16096
|
+
let agentAutoStarted = false;
|
|
16097
|
+
if (opts.config.agent) {
|
|
16098
|
+
const stateDir = getAeroStateDir(opts.config.agent.profile ?? "aero");
|
|
16099
|
+
agentManager = new AgentManager(opts.config.agent, stateDir);
|
|
16100
|
+
if (opts.config.agent.autoStart) {
|
|
16101
|
+
try {
|
|
16102
|
+
await agentManager.start();
|
|
16103
|
+
agentAutoStarted = true;
|
|
16104
|
+
app.log.info({ pid: agentManager.status().pid }, "Agent gateway started");
|
|
16105
|
+
} catch (err) {
|
|
16106
|
+
app.log.error({ err }, "Failed to auto-start agent gateway");
|
|
16107
|
+
}
|
|
16108
|
+
}
|
|
16109
|
+
}
|
|
15325
16110
|
const scheduler = new Scheduler(opts.db, {
|
|
15326
16111
|
onRunCreated: (runId, projectId, providers2, location) => {
|
|
15327
16112
|
jobRunner.executeRun(runId, projectId, providers2, location).catch((err) => {
|
|
@@ -15397,7 +16182,7 @@ async function createServer(opts) {
|
|
|
15397
16182
|
return removed;
|
|
15398
16183
|
}
|
|
15399
16184
|
};
|
|
15400
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
16185
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
|
|
15401
16186
|
const googleConnectionStore = {
|
|
15402
16187
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
15403
16188
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -15443,11 +16228,11 @@ async function createServer(opts) {
|
|
|
15443
16228
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
15444
16229
|
if (opts.config.apiKey) {
|
|
15445
16230
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
15446
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
16231
|
+
const existing = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, keyHash)).get();
|
|
15447
16232
|
if (!existing) {
|
|
15448
16233
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
15449
16234
|
opts.db.insert(apiKeys).values({
|
|
15450
|
-
id: `key_${
|
|
16235
|
+
id: `key_${crypto23.randomBytes(8).toString("hex")}`,
|
|
15451
16236
|
name: "default",
|
|
15452
16237
|
keyHash,
|
|
15453
16238
|
keyPrefix: prefix,
|
|
@@ -15471,7 +16256,7 @@ async function createServer(opts) {
|
|
|
15471
16256
|
};
|
|
15472
16257
|
const createSession = (apiKeyId) => {
|
|
15473
16258
|
pruneExpiredSessions();
|
|
15474
|
-
const sessionId =
|
|
16259
|
+
const sessionId = crypto23.randomBytes(32).toString("hex");
|
|
15475
16260
|
sessions.set(sessionId, {
|
|
15476
16261
|
apiKeyId,
|
|
15477
16262
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -15495,7 +16280,7 @@ async function createServer(opts) {
|
|
|
15495
16280
|
};
|
|
15496
16281
|
const getDefaultApiKey = () => {
|
|
15497
16282
|
if (!opts.config.apiKey) return void 0;
|
|
15498
|
-
return opts.db.select().from(apiKeys).where(
|
|
16283
|
+
return opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
15499
16284
|
};
|
|
15500
16285
|
const createPasswordSession = (reply) => {
|
|
15501
16286
|
const key = getDefaultApiKey();
|
|
@@ -15552,12 +16337,12 @@ async function createServer(opts) {
|
|
|
15552
16337
|
return reply.send({ authenticated: true });
|
|
15553
16338
|
}
|
|
15554
16339
|
if (apiKey) {
|
|
15555
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
16340
|
+
const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
15556
16341
|
if (!key || key.revokedAt) {
|
|
15557
16342
|
const err2 = authInvalid();
|
|
15558
16343
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
15559
16344
|
}
|
|
15560
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
16345
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(apiKeys.id, key.id)).run();
|
|
15561
16346
|
const sessionId = createSession(key.id);
|
|
15562
16347
|
reply.header("set-cookie", serializeSessionCookie({
|
|
15563
16348
|
name: SESSION_COOKIE_NAME,
|
|
@@ -15698,7 +16483,7 @@ async function createServer(opts) {
|
|
|
15698
16483
|
const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
|
|
15699
16484
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15700
16485
|
opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
|
|
15701
|
-
id:
|
|
16486
|
+
id: crypto23.randomUUID(),
|
|
15702
16487
|
projectId,
|
|
15703
16488
|
actor: "api",
|
|
15704
16489
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -15745,6 +16530,17 @@ async function createServer(opts) {
|
|
|
15745
16530
|
onProjectDeleted: (projectId) => {
|
|
15746
16531
|
scheduler.remove(projectId);
|
|
15747
16532
|
},
|
|
16533
|
+
onProjectUpserted: agentManager && opts.config.agent?.autoStart ? (_projectId, projectName) => {
|
|
16534
|
+
try {
|
|
16535
|
+
const gatewayPort = opts.config.agent?.gatewayPort ?? 3579;
|
|
16536
|
+
const result = attachAgentWebhookDirect(opts.db, _projectId, gatewayPort);
|
|
16537
|
+
if (result === "attached") {
|
|
16538
|
+
app.log.info({ projectName }, "Auto-attached agent webhook");
|
|
16539
|
+
}
|
|
16540
|
+
} catch (err) {
|
|
16541
|
+
app.log.error({ err, projectName }, "Failed to auto-attach agent webhook");
|
|
16542
|
+
}
|
|
16543
|
+
} : void 0,
|
|
15748
16544
|
getTelemetryStatus: () => {
|
|
15749
16545
|
const enabled = isTelemetryEnabled();
|
|
15750
16546
|
return {
|
|
@@ -15829,10 +16625,10 @@ async function createServer(opts) {
|
|
|
15829
16625
|
return snapshotService.createReport(input);
|
|
15830
16626
|
}
|
|
15831
16627
|
});
|
|
15832
|
-
const dirname =
|
|
15833
|
-
const assetsDir =
|
|
15834
|
-
if (
|
|
15835
|
-
const indexPath =
|
|
16628
|
+
const dirname = path8.dirname(fileURLToPath2(import.meta.url));
|
|
16629
|
+
const assetsDir = path8.join(dirname, "..", "assets");
|
|
16630
|
+
if (fs7.existsSync(assetsDir)) {
|
|
16631
|
+
const indexPath = path8.join(assetsDir, "index.html");
|
|
15836
16632
|
const injectConfig = (html) => {
|
|
15837
16633
|
const clientConfig = {};
|
|
15838
16634
|
if (basePath) clientConfig.basePath = basePath;
|
|
@@ -15844,14 +16640,14 @@ async function createServer(opts) {
|
|
|
15844
16640
|
await app.register(fastifyStatic.default, {
|
|
15845
16641
|
root: assetsDir,
|
|
15846
16642
|
prefix: basePath ?? "/",
|
|
15847
|
-
wildcard:
|
|
16643
|
+
wildcard: true,
|
|
15848
16644
|
// Don't serve index.html automatically — we handle it with config injection
|
|
15849
16645
|
serve: true,
|
|
15850
16646
|
index: false
|
|
15851
16647
|
});
|
|
15852
16648
|
const serveIndex = (_request, reply) => {
|
|
15853
|
-
if (
|
|
15854
|
-
const html =
|
|
16649
|
+
if (fs7.existsSync(indexPath)) {
|
|
16650
|
+
const html = fs7.readFileSync(indexPath, "utf-8");
|
|
15855
16651
|
return reply.type("text/html").send(injectConfig(html));
|
|
15856
16652
|
}
|
|
15857
16653
|
return reply.status(404).send({ error: "Dashboard not built" });
|
|
@@ -15871,8 +16667,8 @@ async function createServer(opts) {
|
|
|
15871
16667
|
if (basePath && !url.startsWith(basePath)) {
|
|
15872
16668
|
return reply.status(404).send({ error: "Not found", path: request.url });
|
|
15873
16669
|
}
|
|
15874
|
-
if (
|
|
15875
|
-
const html =
|
|
16670
|
+
if (fs7.existsSync(indexPath)) {
|
|
16671
|
+
const html = fs7.readFileSync(indexPath, "utf-8");
|
|
15876
16672
|
return reply.type("text/html").send(injectConfig(html));
|
|
15877
16673
|
}
|
|
15878
16674
|
return reply.status(404).send({ error: "Not found" });
|
|
@@ -15891,6 +16687,13 @@ async function createServer(opts) {
|
|
|
15891
16687
|
scheduler.start();
|
|
15892
16688
|
app.addHook("onClose", async () => {
|
|
15893
16689
|
scheduler.stop();
|
|
16690
|
+
if (agentManager && agentAutoStarted) {
|
|
16691
|
+
try {
|
|
16692
|
+
await agentManager.stop();
|
|
16693
|
+
} catch (err) {
|
|
16694
|
+
app.log.error({ err }, "Failed to stop agent gateway");
|
|
16695
|
+
}
|
|
16696
|
+
}
|
|
15894
16697
|
});
|
|
15895
16698
|
return app;
|
|
15896
16699
|
}
|
|
@@ -15952,6 +16755,11 @@ export {
|
|
|
15952
16755
|
isFirstRun,
|
|
15953
16756
|
showFirstRunNotice,
|
|
15954
16757
|
trackEvent,
|
|
16758
|
+
EXIT_USER_ERROR,
|
|
16759
|
+
EXIT_SYSTEM_ERROR,
|
|
16760
|
+
CliError,
|
|
16761
|
+
usageError,
|
|
16762
|
+
printCliError,
|
|
15955
16763
|
providerQuotaPolicySchema,
|
|
15956
16764
|
ProviderNames,
|
|
15957
16765
|
resolveProviderInput,
|
|
@@ -15968,5 +16776,19 @@ export {
|
|
|
15968
16776
|
extractRecommendedCompetitors,
|
|
15969
16777
|
setGoogleAuthConfig,
|
|
15970
16778
|
formatAuditFactorScore,
|
|
16779
|
+
AGENT_WEBHOOK_EVENTS,
|
|
16780
|
+
buildAgentWebhookUrl,
|
|
16781
|
+
attachAgentWebhookDirect,
|
|
16782
|
+
AgentManager,
|
|
16783
|
+
getAeroStateDir,
|
|
16784
|
+
detectOpenClaw,
|
|
16785
|
+
installOpenClaw,
|
|
16786
|
+
seedWorkspace,
|
|
16787
|
+
initializeOpenClawProfile,
|
|
16788
|
+
configureOpenClawGateway,
|
|
16789
|
+
setOpenClawModel,
|
|
16790
|
+
providerEnvVar,
|
|
16791
|
+
writeAgentEnv,
|
|
16792
|
+
resolveAgentCredentials,
|
|
15971
16793
|
createServer
|
|
15972
16794
|
};
|