@ainyc/canonry 1.46.0 → 1.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -441
- 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 +42 -0
- package/assets/agent-workspace/skills/aero/references/memory-patterns.md +37 -0
- package/assets/agent-workspace/skills/aero/references/orchestration.md +52 -0
- package/assets/agent-workspace/skills/aero/references/regression-playbook.md +34 -0
- package/assets/agent-workspace/skills/aero/references/reporting.md +67 -0
- package/assets/agent-workspace/skills/canonry-setup/SKILL.md +274 -0
- package/assets/agent-workspace/skills/canonry-setup/references/aeo-analysis.md +130 -0
- package/assets/agent-workspace/skills/canonry-setup/references/canonry-cli.md +349 -0
- package/assets/agent-workspace/skills/canonry-setup/references/indexing.md +155 -0
- package/assets/agent-workspace/skills/canonry-setup/references/wordpress-integration.md +57 -0
- package/assets/assets/{index-Cxg_4UWs.js → index-CVk23m8J.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-22RIKNII.js → chunk-25QLMK4F.js} +803 -138
- package/dist/cli.js +457 -87
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -1
- package/package.json +7 -7
|
@@ -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
|
};
|
|
@@ -6880,9 +6964,9 @@ async function googleRoutes(app, opts) {
|
|
|
6880
6964
|
const project = resolveProject(app.db, request.params.name);
|
|
6881
6965
|
let redirectUri;
|
|
6882
6966
|
if (publicUrl) {
|
|
6883
|
-
redirectUri = publicUrl.replace(/\/$/, "") +
|
|
6967
|
+
redirectUri = publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
|
|
6884
6968
|
} else if (opts.publicUrl) {
|
|
6885
|
-
redirectUri = opts.publicUrl.replace(/\/$/, "") +
|
|
6969
|
+
redirectUri = opts.publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
|
|
6886
6970
|
} else {
|
|
6887
6971
|
const proto = request.headers["x-forwarded-proto"] ?? "http";
|
|
6888
6972
|
const host = request.headers.host ?? "localhost:4100";
|
|
@@ -8025,12 +8109,12 @@ async function bingRoutes(app, opts) {
|
|
|
8025
8109
|
}
|
|
8026
8110
|
const unindexedUrls = [];
|
|
8027
8111
|
for (const [url, row] of latestByUrl) {
|
|
8028
|
-
if (row.inIndex === 0) {
|
|
8112
|
+
if (row.inIndex === 0 || row.inIndex === null) {
|
|
8029
8113
|
unindexedUrls.push(url);
|
|
8030
8114
|
}
|
|
8031
8115
|
}
|
|
8032
8116
|
if (unindexedUrls.length === 0) {
|
|
8033
|
-
const err = validationError('No
|
|
8117
|
+
const err = validationError('No unindexed or unknown URLs found. Run "canonry bing inspect <project> <url>" first.');
|
|
8034
8118
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
8035
8119
|
}
|
|
8036
8120
|
urlsToSubmit = unindexedUrls;
|
|
@@ -9136,8 +9220,8 @@ function buildAuthErrorMessage(res, responseText) {
|
|
|
9136
9220
|
}
|
|
9137
9221
|
return "WordPress credentials are invalid or lack permission for this action";
|
|
9138
9222
|
}
|
|
9139
|
-
async function fetchJson(connection, siteUrl,
|
|
9140
|
-
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${
|
|
9223
|
+
async function fetchJson(connection, siteUrl, path9, init) {
|
|
9224
|
+
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path9}`, {
|
|
9141
9225
|
...init,
|
|
9142
9226
|
headers: {
|
|
9143
9227
|
"Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
|
|
@@ -10650,6 +10734,7 @@ async function apiRoutes(app, opts) {
|
|
|
10650
10734
|
await api.register(openApiRoutes, { ...opts.openApiInfo, routePrefix: opts.routePrefix });
|
|
10651
10735
|
await api.register(projectRoutes, {
|
|
10652
10736
|
onProjectDeleted: opts.onProjectDeleted,
|
|
10737
|
+
onProjectUpserted: opts.onProjectUpserted,
|
|
10653
10738
|
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
10654
10739
|
});
|
|
10655
10740
|
await api.register(keywordRoutes, {
|
|
@@ -10663,6 +10748,7 @@ async function apiRoutes(app, opts) {
|
|
|
10663
10748
|
});
|
|
10664
10749
|
await api.register(applyRoutes, {
|
|
10665
10750
|
onScheduleUpdated: opts.onScheduleUpdated,
|
|
10751
|
+
onProjectUpserted: opts.onProjectUpserted,
|
|
10666
10752
|
validProviderNames: opts.providerAdapters?.map((a) => a.name),
|
|
10667
10753
|
onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
|
|
10668
10754
|
opts.googleConnectionStore?.updateConnection(domain, connectionType, {
|
|
@@ -10725,6 +10811,39 @@ async function apiRoutes(app, opts) {
|
|
|
10725
10811
|
}, { prefix: opts.routePrefix ?? "/api/v1" });
|
|
10726
10812
|
}
|
|
10727
10813
|
|
|
10814
|
+
// src/agent-webhook.ts
|
|
10815
|
+
import crypto18 from "crypto";
|
|
10816
|
+
import { eq as eq18 } from "drizzle-orm";
|
|
10817
|
+
var AGENT_WEBHOOK_EVENTS = ["run.completed", "insight.critical", "insight.high", "citation.gained"];
|
|
10818
|
+
function buildAgentWebhookUrl(gatewayPort) {
|
|
10819
|
+
return `http://localhost:${gatewayPort}/hooks/canonry`;
|
|
10820
|
+
}
|
|
10821
|
+
function attachAgentWebhookDirect(db, projectId, gatewayPort) {
|
|
10822
|
+
const agentUrl = buildAgentWebhookUrl(gatewayPort);
|
|
10823
|
+
const existing = db.select().from(notifications).where(eq18(notifications.projectId, projectId)).all();
|
|
10824
|
+
const hasAgent = existing.some((n) => {
|
|
10825
|
+
const cfg = parseJsonColumn(n.config, {});
|
|
10826
|
+
return cfg.source === "agent";
|
|
10827
|
+
});
|
|
10828
|
+
if (hasAgent) return "already-attached";
|
|
10829
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10830
|
+
db.insert(notifications).values({
|
|
10831
|
+
id: crypto18.randomUUID(),
|
|
10832
|
+
projectId,
|
|
10833
|
+
channel: "webhook",
|
|
10834
|
+
config: JSON.stringify({
|
|
10835
|
+
url: agentUrl,
|
|
10836
|
+
events: [...AGENT_WEBHOOK_EVENTS],
|
|
10837
|
+
source: "agent"
|
|
10838
|
+
}),
|
|
10839
|
+
enabled: 1,
|
|
10840
|
+
webhookSecret: crypto18.randomUUID(),
|
|
10841
|
+
createdAt: now,
|
|
10842
|
+
updatedAt: now
|
|
10843
|
+
}).run();
|
|
10844
|
+
return "attached";
|
|
10845
|
+
}
|
|
10846
|
+
|
|
10728
10847
|
// ../provider-gemini/src/normalize.ts
|
|
10729
10848
|
import { GoogleGenAI } from "@google/genai";
|
|
10730
10849
|
|
|
@@ -13124,11 +13243,11 @@ function removeWordpressConnection(config, projectName) {
|
|
|
13124
13243
|
}
|
|
13125
13244
|
|
|
13126
13245
|
// src/job-runner.ts
|
|
13127
|
-
import
|
|
13246
|
+
import crypto19 from "crypto";
|
|
13128
13247
|
import fs4 from "fs";
|
|
13129
13248
|
import path5 from "path";
|
|
13130
13249
|
import os4 from "os";
|
|
13131
|
-
import { and as and7, eq as
|
|
13250
|
+
import { and as and7, eq as eq19, inArray as inArray3, sql as sql4 } from "drizzle-orm";
|
|
13132
13251
|
|
|
13133
13252
|
// src/citation-utils.ts
|
|
13134
13253
|
function domainMatches(domain, canonicalDomain) {
|
|
@@ -13364,7 +13483,7 @@ var JobRunner = class {
|
|
|
13364
13483
|
if (stale.length === 0) return;
|
|
13365
13484
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13366
13485
|
for (const run of stale) {
|
|
13367
|
-
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(
|
|
13486
|
+
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
13487
|
log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
|
|
13369
13488
|
}
|
|
13370
13489
|
}
|
|
@@ -13392,10 +13511,10 @@ var JobRunner = class {
|
|
|
13392
13511
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
13393
13512
|
}
|
|
13394
13513
|
if (existingRun.status === "queued") {
|
|
13395
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(
|
|
13514
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(eq19(runs.id, runId), eq19(runs.status, "queued"))).run();
|
|
13396
13515
|
}
|
|
13397
13516
|
this.throwIfRunCancelled(runId);
|
|
13398
|
-
const project = this.db.select().from(projects).where(
|
|
13517
|
+
const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
13399
13518
|
if (!project) {
|
|
13400
13519
|
throw new Error(`Project ${projectId} not found`);
|
|
13401
13520
|
}
|
|
@@ -13415,8 +13534,8 @@ var JobRunner = class {
|
|
|
13415
13534
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
13416
13535
|
}
|
|
13417
13536
|
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(
|
|
13537
|
+
projectKeywords = this.db.select().from(keywords).where(eq19(keywords.projectId, projectId)).all();
|
|
13538
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq19(competitors.projectId, projectId)).all();
|
|
13420
13539
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
13421
13540
|
const allDomains = effectiveDomains({
|
|
13422
13541
|
canonicalDomain: project.canonicalDomain,
|
|
@@ -13432,7 +13551,7 @@ var JobRunner = class {
|
|
|
13432
13551
|
const todayPeriod = getCurrentUsageDay();
|
|
13433
13552
|
for (const p of activeProviders) {
|
|
13434
13553
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
13435
|
-
const providerUsage = this.db.select().from(usageCounters).where(
|
|
13554
|
+
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
13555
|
const limit = p.config.quotaPolicy.maxRequestsPerDay;
|
|
13437
13556
|
if (providerUsage + queriesPerProvider > limit) {
|
|
13438
13557
|
throw new Error(
|
|
@@ -13492,7 +13611,7 @@ var JobRunner = class {
|
|
|
13492
13611
|
);
|
|
13493
13612
|
let screenshotRelPath = null;
|
|
13494
13613
|
if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
|
|
13495
|
-
const snapshotId =
|
|
13614
|
+
const snapshotId = crypto19.randomUUID();
|
|
13496
13615
|
const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
|
|
13497
13616
|
if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
|
|
13498
13617
|
const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
|
|
@@ -13522,7 +13641,7 @@ var JobRunner = class {
|
|
|
13522
13641
|
}).run();
|
|
13523
13642
|
} else {
|
|
13524
13643
|
this.db.insert(querySnapshots).values({
|
|
13525
|
-
id:
|
|
13644
|
+
id: crypto19.randomUUID(),
|
|
13526
13645
|
runId,
|
|
13527
13646
|
keywordId: kw.id,
|
|
13528
13647
|
provider: providerName,
|
|
@@ -13573,12 +13692,12 @@ var JobRunner = class {
|
|
|
13573
13692
|
const someFailed = providerErrors.size > 0;
|
|
13574
13693
|
if (allFailed) {
|
|
13575
13694
|
const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
|
|
13576
|
-
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13695
|
+
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq19(runs.id, runId)).run();
|
|
13577
13696
|
} else if (someFailed) {
|
|
13578
13697
|
const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
|
|
13579
|
-
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13698
|
+
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq19(runs.id, runId)).run();
|
|
13580
13699
|
} else {
|
|
13581
|
-
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13700
|
+
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
|
|
13582
13701
|
}
|
|
13583
13702
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13584
13703
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
@@ -13613,7 +13732,7 @@ var JobRunner = class {
|
|
|
13613
13732
|
status: "failed",
|
|
13614
13733
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13615
13734
|
error: errorMessage
|
|
13616
|
-
}).where(
|
|
13735
|
+
}).where(eq19(runs.id, runId)).run();
|
|
13617
13736
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13618
13737
|
trackEvent("run.completed", {
|
|
13619
13738
|
status: "failed",
|
|
@@ -13634,7 +13753,7 @@ var JobRunner = class {
|
|
|
13634
13753
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13635
13754
|
const period = now.slice(0, 10);
|
|
13636
13755
|
this.db.insert(usageCounters).values({
|
|
13637
|
-
id:
|
|
13756
|
+
id: crypto19.randomUUID(),
|
|
13638
13757
|
scope,
|
|
13639
13758
|
period,
|
|
13640
13759
|
metric,
|
|
@@ -13656,7 +13775,7 @@ var JobRunner = class {
|
|
|
13656
13775
|
status: runs.status,
|
|
13657
13776
|
finishedAt: runs.finishedAt,
|
|
13658
13777
|
error: runs.error
|
|
13659
|
-
}).from(runs).where(
|
|
13778
|
+
}).from(runs).where(eq19(runs.id, runId)).get();
|
|
13660
13779
|
}
|
|
13661
13780
|
isRunCancelled(runId) {
|
|
13662
13781
|
return this.getRunState(runId)?.status === "cancelled";
|
|
@@ -13672,7 +13791,7 @@ var JobRunner = class {
|
|
|
13672
13791
|
this.db.update(runs).set({
|
|
13673
13792
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13674
13793
|
error: currentRun.error ?? "Cancelled by user"
|
|
13675
|
-
}).where(
|
|
13794
|
+
}).where(eq19(runs.id, runId)).run();
|
|
13676
13795
|
}
|
|
13677
13796
|
trackEvent("run.completed", {
|
|
13678
13797
|
status: "cancelled",
|
|
@@ -13694,8 +13813,8 @@ function getCurrentUsageDay() {
|
|
|
13694
13813
|
}
|
|
13695
13814
|
|
|
13696
13815
|
// src/gsc-sync.ts
|
|
13697
|
-
import
|
|
13698
|
-
import { eq as
|
|
13816
|
+
import crypto20 from "crypto";
|
|
13817
|
+
import { eq as eq20, and as and8, sql as sql5 } from "drizzle-orm";
|
|
13699
13818
|
var log2 = createLogger("GscSync");
|
|
13700
13819
|
function formatDate2(d) {
|
|
13701
13820
|
return d.toISOString().split("T")[0];
|
|
@@ -13707,13 +13826,13 @@ function daysAgo(n) {
|
|
|
13707
13826
|
}
|
|
13708
13827
|
async function executeGscSync(db, runId, projectId, opts) {
|
|
13709
13828
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13710
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
13829
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
|
|
13711
13830
|
try {
|
|
13712
13831
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13713
13832
|
if (!googleClientId || !googleClientSecret) {
|
|
13714
13833
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13715
13834
|
}
|
|
13716
|
-
const project = db.select().from(projects).where(
|
|
13835
|
+
const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
|
|
13717
13836
|
if (!project) {
|
|
13718
13837
|
throw new Error(`Project not found: ${projectId}`);
|
|
13719
13838
|
}
|
|
@@ -13748,7 +13867,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13748
13867
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
13749
13868
|
db.delete(gscSearchData).where(
|
|
13750
13869
|
and8(
|
|
13751
|
-
|
|
13870
|
+
eq20(gscSearchData.projectId, projectId),
|
|
13752
13871
|
sql5`${gscSearchData.date} >= ${startDate}`,
|
|
13753
13872
|
sql5`${gscSearchData.date} <= ${endDate}`
|
|
13754
13873
|
)
|
|
@@ -13760,7 +13879,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13760
13879
|
for (const row of batch) {
|
|
13761
13880
|
const [query, page, country, device, date] = row.keys;
|
|
13762
13881
|
db.insert(gscSearchData).values({
|
|
13763
|
-
id:
|
|
13882
|
+
id: crypto20.randomUUID(),
|
|
13764
13883
|
projectId,
|
|
13765
13884
|
syncRunId: runId,
|
|
13766
13885
|
date: date ?? "",
|
|
@@ -13794,7 +13913,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13794
13913
|
const rich = ir.richResultsResult;
|
|
13795
13914
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13796
13915
|
db.insert(gscUrlInspections).values({
|
|
13797
|
-
id:
|
|
13916
|
+
id: crypto20.randomUUID(),
|
|
13798
13917
|
projectId,
|
|
13799
13918
|
syncRunId: runId,
|
|
13800
13919
|
url: pageUrl,
|
|
@@ -13815,7 +13934,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13815
13934
|
log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
|
|
13816
13935
|
}
|
|
13817
13936
|
}
|
|
13818
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
13937
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
|
|
13819
13938
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13820
13939
|
for (const row of allInspections) {
|
|
13821
13940
|
const existing = latestByUrl.get(row.url);
|
|
@@ -13836,9 +13955,9 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13836
13955
|
}
|
|
13837
13956
|
}
|
|
13838
13957
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
13839
|
-
db.delete(gscCoverageSnapshots).where(and8(
|
|
13958
|
+
db.delete(gscCoverageSnapshots).where(and8(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13840
13959
|
db.insert(gscCoverageSnapshots).values({
|
|
13841
|
-
id:
|
|
13960
|
+
id: crypto20.randomUUID(),
|
|
13842
13961
|
projectId,
|
|
13843
13962
|
syncRunId: runId,
|
|
13844
13963
|
date: snapshotDate,
|
|
@@ -13847,19 +13966,19 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13847
13966
|
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
13848
13967
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13849
13968
|
}).run();
|
|
13850
|
-
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13969
|
+
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
13851
13970
|
log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
13852
13971
|
} catch (err) {
|
|
13853
13972
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
13854
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13973
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
13855
13974
|
log2.error("sync.failed", { runId, projectId, error: errorMsg });
|
|
13856
13975
|
throw err;
|
|
13857
13976
|
}
|
|
13858
13977
|
}
|
|
13859
13978
|
|
|
13860
13979
|
// src/gsc-inspect-sitemap.ts
|
|
13861
|
-
import
|
|
13862
|
-
import { eq as
|
|
13980
|
+
import crypto21 from "crypto";
|
|
13981
|
+
import { eq as eq21, and as and9 } from "drizzle-orm";
|
|
13863
13982
|
|
|
13864
13983
|
// src/sitemap-parser.ts
|
|
13865
13984
|
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
@@ -13928,13 +14047,13 @@ async function parseSitemapRecursive(url, urls, depth) {
|
|
|
13928
14047
|
var log3 = createLogger("InspectSitemap");
|
|
13929
14048
|
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
13930
14049
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13931
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14050
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq21(runs.id, runId)).run();
|
|
13932
14051
|
try {
|
|
13933
14052
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13934
14053
|
if (!googleClientId || !googleClientSecret) {
|
|
13935
14054
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13936
14055
|
}
|
|
13937
|
-
const project = db.select().from(projects).where(
|
|
14056
|
+
const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
|
|
13938
14057
|
if (!project) {
|
|
13939
14058
|
throw new Error(`Project not found: ${projectId}`);
|
|
13940
14059
|
}
|
|
@@ -13975,7 +14094,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13975
14094
|
const rich = ir.richResultsResult;
|
|
13976
14095
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13977
14096
|
db.insert(gscUrlInspections).values({
|
|
13978
|
-
id:
|
|
14097
|
+
id: crypto21.randomUUID(),
|
|
13979
14098
|
projectId,
|
|
13980
14099
|
syncRunId: runId,
|
|
13981
14100
|
url: pageUrl,
|
|
@@ -14002,7 +14121,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14002
14121
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
14003
14122
|
}
|
|
14004
14123
|
}
|
|
14005
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14124
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, projectId)).all();
|
|
14006
14125
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
14007
14126
|
for (const row of allInspections) {
|
|
14008
14127
|
const existing = latestByUrl.get(row.url);
|
|
@@ -14023,9 +14142,9 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14023
14142
|
}
|
|
14024
14143
|
}
|
|
14025
14144
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
14026
|
-
db.delete(gscCoverageSnapshots).where(and9(
|
|
14145
|
+
db.delete(gscCoverageSnapshots).where(and9(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
14027
14146
|
db.insert(gscCoverageSnapshots).values({
|
|
14028
|
-
id:
|
|
14147
|
+
id: crypto21.randomUUID(),
|
|
14029
14148
|
projectId,
|
|
14030
14149
|
syncRunId: runId,
|
|
14031
14150
|
date: snapshotDate,
|
|
@@ -14035,11 +14154,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14035
14154
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14036
14155
|
}).run();
|
|
14037
14156
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
14038
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14157
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
|
|
14039
14158
|
log3.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
14040
14159
|
} catch (err) {
|
|
14041
14160
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
14042
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14161
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
|
|
14043
14162
|
log3.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
14044
14163
|
throw err;
|
|
14045
14164
|
}
|
|
@@ -14098,7 +14217,7 @@ var ProviderRegistry = class {
|
|
|
14098
14217
|
|
|
14099
14218
|
// src/scheduler.ts
|
|
14100
14219
|
import cron from "node-cron";
|
|
14101
|
-
import { eq as
|
|
14220
|
+
import { eq as eq22 } from "drizzle-orm";
|
|
14102
14221
|
var log4 = createLogger("Scheduler");
|
|
14103
14222
|
var Scheduler = class {
|
|
14104
14223
|
db;
|
|
@@ -14110,7 +14229,7 @@ var Scheduler = class {
|
|
|
14110
14229
|
}
|
|
14111
14230
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
14112
14231
|
start() {
|
|
14113
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
14232
|
+
const allSchedules = this.db.select().from(schedules).where(eq22(schedules.enabled, 1)).all();
|
|
14114
14233
|
for (const schedule of allSchedules) {
|
|
14115
14234
|
const missedRunAt = schedule.nextRunAt;
|
|
14116
14235
|
this.registerCronTask(schedule);
|
|
@@ -14135,7 +14254,7 @@ var Scheduler = class {
|
|
|
14135
14254
|
this.stopTask(projectId, existing, "Stopped");
|
|
14136
14255
|
this.tasks.delete(projectId);
|
|
14137
14256
|
}
|
|
14138
|
-
const schedule = this.db.select().from(schedules).where(
|
|
14257
|
+
const schedule = this.db.select().from(schedules).where(eq22(schedules.projectId, projectId)).get();
|
|
14139
14258
|
if (schedule && schedule.enabled === 1) {
|
|
14140
14259
|
this.registerCronTask(schedule);
|
|
14141
14260
|
}
|
|
@@ -14168,14 +14287,14 @@ var Scheduler = class {
|
|
|
14168
14287
|
this.db.update(schedules).set({
|
|
14169
14288
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
14170
14289
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14171
|
-
}).where(
|
|
14290
|
+
}).where(eq22(schedules.id, scheduleId)).run();
|
|
14172
14291
|
const label = schedule.preset ?? cronExpr;
|
|
14173
14292
|
log4.info("cron.registered", { projectId, schedule: label, timezone });
|
|
14174
14293
|
}
|
|
14175
14294
|
triggerRun(scheduleId, projectId) {
|
|
14176
14295
|
try {
|
|
14177
14296
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14178
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
14297
|
+
const currentSchedule = this.db.select().from(schedules).where(eq22(schedules.id, scheduleId)).get();
|
|
14179
14298
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
14180
14299
|
log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
|
|
14181
14300
|
this.remove(projectId);
|
|
@@ -14183,7 +14302,7 @@ var Scheduler = class {
|
|
|
14183
14302
|
}
|
|
14184
14303
|
const task = this.tasks.get(projectId);
|
|
14185
14304
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
14186
|
-
const project = this.db.select().from(projects).where(
|
|
14305
|
+
const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
14187
14306
|
if (!project) {
|
|
14188
14307
|
log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
|
|
14189
14308
|
this.remove(projectId);
|
|
@@ -14212,7 +14331,7 @@ var Scheduler = class {
|
|
|
14212
14331
|
this.db.update(schedules).set({
|
|
14213
14332
|
nextRunAt,
|
|
14214
14333
|
updatedAt: now
|
|
14215
|
-
}).where(
|
|
14334
|
+
}).where(eq22(schedules.id, currentSchedule.id)).run();
|
|
14216
14335
|
return;
|
|
14217
14336
|
}
|
|
14218
14337
|
const runId = queueResult.runId;
|
|
@@ -14220,7 +14339,7 @@ var Scheduler = class {
|
|
|
14220
14339
|
lastRunAt: now,
|
|
14221
14340
|
nextRunAt,
|
|
14222
14341
|
updatedAt: now
|
|
14223
|
-
}).where(
|
|
14342
|
+
}).where(eq22(schedules.id, currentSchedule.id)).run();
|
|
14224
14343
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
14225
14344
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
14226
14345
|
log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -14232,8 +14351,8 @@ var Scheduler = class {
|
|
|
14232
14351
|
};
|
|
14233
14352
|
|
|
14234
14353
|
// src/notifier.ts
|
|
14235
|
-
import { eq as
|
|
14236
|
-
import
|
|
14354
|
+
import { eq as eq23, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
|
|
14355
|
+
import crypto22 from "crypto";
|
|
14237
14356
|
var log5 = createLogger("Notifier");
|
|
14238
14357
|
var Notifier = class {
|
|
14239
14358
|
db;
|
|
@@ -14245,18 +14364,18 @@ var Notifier = class {
|
|
|
14245
14364
|
/** Called after a run completes (success, partial, or failed). */
|
|
14246
14365
|
async onRunCompleted(runId, projectId) {
|
|
14247
14366
|
log5.info("run.completed", { runId, projectId });
|
|
14248
|
-
const notifs = this.db.select().from(notifications).where(
|
|
14367
|
+
const notifs = this.db.select().from(notifications).where(eq23(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14249
14368
|
if (notifs.length === 0) {
|
|
14250
14369
|
log5.info("notifications.none-enabled", { projectId });
|
|
14251
14370
|
return;
|
|
14252
14371
|
}
|
|
14253
14372
|
log5.info("notifications.found", { projectId, count: notifs.length });
|
|
14254
|
-
const run = this.db.select().from(runs).where(
|
|
14373
|
+
const run = this.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
14255
14374
|
if (!run) {
|
|
14256
14375
|
log5.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
14257
14376
|
return;
|
|
14258
14377
|
}
|
|
14259
|
-
const project = this.db.select().from(projects).where(
|
|
14378
|
+
const project = this.db.select().from(projects).where(eq23(projects.id, projectId)).get();
|
|
14260
14379
|
if (!project) {
|
|
14261
14380
|
log5.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
14262
14381
|
return;
|
|
@@ -14295,11 +14414,52 @@ var Notifier = class {
|
|
|
14295
14414
|
}
|
|
14296
14415
|
}
|
|
14297
14416
|
}
|
|
14417
|
+
/** Dispatch insight webhooks for critical/high severity insights after a run. */
|
|
14418
|
+
async dispatchInsightWebhooks(runId, projectId, result) {
|
|
14419
|
+
const insightEvents = [];
|
|
14420
|
+
const criticalInsights = result.insights.filter((i) => i.severity === "critical");
|
|
14421
|
+
const highInsights = result.insights.filter((i) => i.severity === "high");
|
|
14422
|
+
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
14423
|
+
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
14424
|
+
if (insightEvents.length === 0) return;
|
|
14425
|
+
const notifs = this.db.select().from(notifications).where(eq23(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14426
|
+
if (notifs.length === 0) return;
|
|
14427
|
+
const run = this.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
14428
|
+
if (!run) return;
|
|
14429
|
+
const project = this.db.select().from(projects).where(eq23(projects.id, projectId)).get();
|
|
14430
|
+
if (!project) return;
|
|
14431
|
+
for (const notif of notifs) {
|
|
14432
|
+
const config = parseJsonColumn(notif.config, { url: "", events: [] });
|
|
14433
|
+
if (!config.url) continue;
|
|
14434
|
+
const subscribedEvents = config.events;
|
|
14435
|
+
const matchingEvents = insightEvents.filter((e) => subscribedEvents.includes(e));
|
|
14436
|
+
if (matchingEvents.length === 0) continue;
|
|
14437
|
+
for (const event of matchingEvents) {
|
|
14438
|
+
const relevantInsights = event === "insight.critical" ? criticalInsights : highInsights;
|
|
14439
|
+
const payload = {
|
|
14440
|
+
source: "canonry",
|
|
14441
|
+
event,
|
|
14442
|
+
project: { name: project.name, canonicalDomain: project.canonicalDomain },
|
|
14443
|
+
run: { id: run.id, status: run.status, finishedAt: run.finishedAt },
|
|
14444
|
+
insights: relevantInsights.map((i) => ({
|
|
14445
|
+
id: i.id,
|
|
14446
|
+
type: i.type,
|
|
14447
|
+
severity: i.severity,
|
|
14448
|
+
title: i.title,
|
|
14449
|
+
keyword: i.keyword,
|
|
14450
|
+
provider: i.provider
|
|
14451
|
+
})),
|
|
14452
|
+
dashboardUrl: `${this.serverUrl}/projects/${project.name}`
|
|
14453
|
+
};
|
|
14454
|
+
await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null);
|
|
14455
|
+
}
|
|
14456
|
+
}
|
|
14457
|
+
}
|
|
14298
14458
|
computeTransitions(runId, projectId) {
|
|
14299
14459
|
const recentRuns = this.db.select().from(runs).where(
|
|
14300
14460
|
and10(
|
|
14301
|
-
|
|
14302
|
-
or2(
|
|
14461
|
+
eq23(runs.projectId, projectId),
|
|
14462
|
+
or2(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
|
|
14303
14463
|
)
|
|
14304
14464
|
).orderBy(desc8(runs.createdAt)).limit(2).all();
|
|
14305
14465
|
if (recentRuns.length < 2) return [];
|
|
@@ -14311,12 +14471,12 @@ var Notifier = class {
|
|
|
14311
14471
|
keyword: keywords.keyword,
|
|
14312
14472
|
provider: querySnapshots.provider,
|
|
14313
14473
|
citationState: querySnapshots.citationState
|
|
14314
|
-
}).from(querySnapshots).leftJoin(keywords,
|
|
14474
|
+
}).from(querySnapshots).leftJoin(keywords, eq23(querySnapshots.keywordId, keywords.id)).where(eq23(querySnapshots.runId, currentRunId)).all();
|
|
14315
14475
|
const previousSnapshots = this.db.select({
|
|
14316
14476
|
keywordId: querySnapshots.keywordId,
|
|
14317
14477
|
provider: querySnapshots.provider,
|
|
14318
14478
|
citationState: querySnapshots.citationState
|
|
14319
|
-
}).from(querySnapshots).where(
|
|
14479
|
+
}).from(querySnapshots).where(eq23(querySnapshots.runId, previousRunId)).all();
|
|
14320
14480
|
const prevMap = /* @__PURE__ */ new Map();
|
|
14321
14481
|
for (const s of previousSnapshots) {
|
|
14322
14482
|
prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
|
|
@@ -14374,7 +14534,7 @@ var Notifier = class {
|
|
|
14374
14534
|
}
|
|
14375
14535
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
14376
14536
|
this.db.insert(auditLog).values({
|
|
14377
|
-
id:
|
|
14537
|
+
id: crypto22.randomUUID(),
|
|
14378
14538
|
projectId,
|
|
14379
14539
|
actor: "scheduler",
|
|
14380
14540
|
action: `notification.${status}`,
|
|
@@ -14389,13 +14549,26 @@ var Notifier = class {
|
|
|
14389
14549
|
// src/run-coordinator.ts
|
|
14390
14550
|
var log6 = createLogger("RunCoordinator");
|
|
14391
14551
|
var RunCoordinator = class {
|
|
14392
|
-
constructor(notifier, intelligenceService) {
|
|
14552
|
+
constructor(notifier, intelligenceService, onInsightsGenerated) {
|
|
14393
14553
|
this.notifier = notifier;
|
|
14394
14554
|
this.intelligenceService = intelligenceService;
|
|
14555
|
+
this.onInsightsGenerated = onInsightsGenerated;
|
|
14395
14556
|
}
|
|
14396
14557
|
async onRunCompleted(runId, projectId) {
|
|
14397
14558
|
try {
|
|
14398
|
-
this.intelligenceService.analyzeAndPersist(runId, projectId);
|
|
14559
|
+
const result = this.intelligenceService.analyzeAndPersist(runId, projectId);
|
|
14560
|
+
if (result && this.onInsightsGenerated) {
|
|
14561
|
+
const hasHighSeverity = result.insights.some(
|
|
14562
|
+
(i) => i.severity === "critical" || i.severity === "high"
|
|
14563
|
+
);
|
|
14564
|
+
if (hasHighSeverity) {
|
|
14565
|
+
try {
|
|
14566
|
+
await this.onInsightsGenerated(runId, projectId, result);
|
|
14567
|
+
} catch (err) {
|
|
14568
|
+
log6.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
14569
|
+
}
|
|
14570
|
+
}
|
|
14571
|
+
}
|
|
14399
14572
|
} catch (err) {
|
|
14400
14573
|
log6.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
14401
14574
|
}
|
|
@@ -14429,13 +14602,13 @@ function extractHostname(domain) {
|
|
|
14429
14602
|
function fetchWithPinnedAddress(target) {
|
|
14430
14603
|
return new Promise((resolve) => {
|
|
14431
14604
|
const port = target.url.port ? Number(target.url.port) : 443;
|
|
14432
|
-
const
|
|
14605
|
+
const path9 = target.url.pathname + target.url.search;
|
|
14433
14606
|
const req = https2.request(
|
|
14434
14607
|
{
|
|
14435
14608
|
hostname: target.address,
|
|
14436
14609
|
family: target.family,
|
|
14437
14610
|
port,
|
|
14438
|
-
path:
|
|
14611
|
+
path: path9,
|
|
14439
14612
|
method: "GET",
|
|
14440
14613
|
timeout: FETCH_TIMEOUT_MS,
|
|
14441
14614
|
servername: target.url.hostname,
|
|
@@ -15124,10 +15297,446 @@ function clipText(value, length) {
|
|
|
15124
15297
|
return `${value.slice(0, length - 3)}...`;
|
|
15125
15298
|
}
|
|
15126
15299
|
|
|
15300
|
+
// src/agent-manager.ts
|
|
15301
|
+
import { execFileSync, spawn } from "child_process";
|
|
15302
|
+
import fs5 from "fs";
|
|
15303
|
+
import path6 from "path";
|
|
15304
|
+
var log8 = createLogger("AgentManager");
|
|
15305
|
+
var PROCESS_MARKER = "canonry-openclaw-gateway";
|
|
15306
|
+
var AgentManager = class {
|
|
15307
|
+
constructor(config, stateDir) {
|
|
15308
|
+
this.config = config;
|
|
15309
|
+
this.stateDir = stateDir;
|
|
15310
|
+
this.processJsonPath = path6.join(stateDir, "process.json");
|
|
15311
|
+
}
|
|
15312
|
+
processJsonPath;
|
|
15313
|
+
/**
|
|
15314
|
+
* Check if the gateway process is running.
|
|
15315
|
+
* Cleans up stale process.json if the process is dead or belongs to a
|
|
15316
|
+
* different process (PID reuse).
|
|
15317
|
+
*/
|
|
15318
|
+
status() {
|
|
15319
|
+
const info = this.readProcessInfo();
|
|
15320
|
+
if (!info) {
|
|
15321
|
+
return { state: "stopped" };
|
|
15322
|
+
}
|
|
15323
|
+
if (info.marker !== PROCESS_MARKER) {
|
|
15324
|
+
this.removeProcessJson();
|
|
15325
|
+
return { state: "stopped" };
|
|
15326
|
+
}
|
|
15327
|
+
if (isProcessAlive(info.pid) && this.verifyProcessIdentity(info.pid)) {
|
|
15328
|
+
return {
|
|
15329
|
+
state: "running",
|
|
15330
|
+
pid: info.pid,
|
|
15331
|
+
port: info.gatewayPort,
|
|
15332
|
+
startedAt: info.startedAt
|
|
15333
|
+
};
|
|
15334
|
+
}
|
|
15335
|
+
this.removeProcessJson();
|
|
15336
|
+
return { state: "stopped" };
|
|
15337
|
+
}
|
|
15338
|
+
/**
|
|
15339
|
+
* Start the OpenClaw gateway as a detached background process.
|
|
15340
|
+
* Idempotent — no-op if already running.
|
|
15341
|
+
* Waits briefly for the process to confirm it hasn't crashed on startup.
|
|
15342
|
+
*/
|
|
15343
|
+
async start() {
|
|
15344
|
+
const currentStatus = this.status();
|
|
15345
|
+
if (currentStatus.state === "running") {
|
|
15346
|
+
log8.info("already.running", { pid: currentStatus.pid });
|
|
15347
|
+
return;
|
|
15348
|
+
}
|
|
15349
|
+
const binary = this.config.binary ?? "openclaw";
|
|
15350
|
+
const profile = this.config.profile ?? "aero";
|
|
15351
|
+
const port = this.config.gatewayPort ?? 3579;
|
|
15352
|
+
if (!fs5.existsSync(this.stateDir)) {
|
|
15353
|
+
fs5.mkdirSync(this.stateDir, { recursive: true });
|
|
15354
|
+
}
|
|
15355
|
+
const logFile = path6.join(this.stateDir, "gateway.log");
|
|
15356
|
+
const logFd = fs5.openSync(logFile, "a");
|
|
15357
|
+
const dotEnv = this.loadDotEnv();
|
|
15358
|
+
const child = spawn(binary, ["--profile", profile, "gateway"], {
|
|
15359
|
+
detached: true,
|
|
15360
|
+
stdio: ["ignore", logFd, logFd],
|
|
15361
|
+
env: {
|
|
15362
|
+
...process.env,
|
|
15363
|
+
...dotEnv,
|
|
15364
|
+
OPENCLAW_PROFILE: profile,
|
|
15365
|
+
OPENCLAW_GATEWAY_PORT: String(port),
|
|
15366
|
+
OPENCLAW_STATE_DIR: this.stateDir
|
|
15367
|
+
}
|
|
15368
|
+
});
|
|
15369
|
+
const startupResult = await new Promise((resolve) => {
|
|
15370
|
+
let settled = false;
|
|
15371
|
+
const settle = (r) => {
|
|
15372
|
+
if (settled) return;
|
|
15373
|
+
settled = true;
|
|
15374
|
+
resolve(r);
|
|
15375
|
+
};
|
|
15376
|
+
child.on("error", (err) => settle({ error: err }));
|
|
15377
|
+
child.on("exit", (code) => settle({ exitCode: code }));
|
|
15378
|
+
setTimeout(() => settle({}), 500);
|
|
15379
|
+
});
|
|
15380
|
+
child.unref();
|
|
15381
|
+
fs5.closeSync(logFd);
|
|
15382
|
+
if (startupResult.error) {
|
|
15383
|
+
throw new Error(`Failed to start OpenClaw gateway: ${startupResult.error.message}`);
|
|
15384
|
+
}
|
|
15385
|
+
if (startupResult.exitCode != null) {
|
|
15386
|
+
throw new Error(`OpenClaw gateway exited immediately (code ${startupResult.exitCode}). Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
|
|
15387
|
+
}
|
|
15388
|
+
if (child.pid == null) {
|
|
15389
|
+
throw new Error("Failed to start OpenClaw gateway: no PID returned by spawn");
|
|
15390
|
+
}
|
|
15391
|
+
if (!isProcessAlive(child.pid)) {
|
|
15392
|
+
throw new Error(`OpenClaw gateway exited immediately after spawn. Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
|
|
15393
|
+
}
|
|
15394
|
+
const processInfo = {
|
|
15395
|
+
pid: child.pid,
|
|
15396
|
+
gatewayPort: port,
|
|
15397
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15398
|
+
marker: PROCESS_MARKER
|
|
15399
|
+
};
|
|
15400
|
+
fs5.writeFileSync(this.processJsonPath, JSON.stringify(processInfo, null, 2), "utf-8");
|
|
15401
|
+
log8.info("started", { pid: child.pid, port });
|
|
15402
|
+
}
|
|
15403
|
+
/**
|
|
15404
|
+
* Stop the gateway process.
|
|
15405
|
+
* Uses DenchClaw escalation: SIGTERM → 800ms poll → SIGKILL.
|
|
15406
|
+
* Idempotent — no-op if already stopped.
|
|
15407
|
+
*/
|
|
15408
|
+
async stop() {
|
|
15409
|
+
const info = this.readProcessInfo();
|
|
15410
|
+
if (!info) return;
|
|
15411
|
+
if (isProcessAlive(info.pid) && info.marker === PROCESS_MARKER && this.verifyProcessIdentity(info.pid)) {
|
|
15412
|
+
await terminateWithEscalation(info.pid);
|
|
15413
|
+
}
|
|
15414
|
+
this.removeProcessJson();
|
|
15415
|
+
log8.info("stopped", { pid: info.pid });
|
|
15416
|
+
}
|
|
15417
|
+
/**
|
|
15418
|
+
* Stop the gateway, wipe the workspace directory, and prepare for re-seeding.
|
|
15419
|
+
*/
|
|
15420
|
+
async reset() {
|
|
15421
|
+
await this.stop();
|
|
15422
|
+
const workspaceDir = path6.join(this.stateDir, "workspace");
|
|
15423
|
+
if (fs5.existsSync(workspaceDir)) {
|
|
15424
|
+
fs5.rmSync(workspaceDir, { recursive: true, force: true });
|
|
15425
|
+
log8.info("workspace.wiped", { dir: workspaceDir });
|
|
15426
|
+
}
|
|
15427
|
+
}
|
|
15428
|
+
/**
|
|
15429
|
+
* Verify that the PID actually belongs to an openclaw process by checking
|
|
15430
|
+
* the full command line. Requires "openclaw" in the args to avoid matching
|
|
15431
|
+
* unrelated Node processes after PID reuse.
|
|
15432
|
+
*/
|
|
15433
|
+
verifyProcessIdentity(pid) {
|
|
15434
|
+
try {
|
|
15435
|
+
if (process.platform === "darwin") {
|
|
15436
|
+
const out = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
15437
|
+
encoding: "utf-8",
|
|
15438
|
+
timeout: 2e3
|
|
15439
|
+
}).trim();
|
|
15440
|
+
return out.includes("openclaw");
|
|
15441
|
+
}
|
|
15442
|
+
if (process.platform === "linux") {
|
|
15443
|
+
const cmdline = fs5.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
15444
|
+
return cmdline.includes("openclaw");
|
|
15445
|
+
}
|
|
15446
|
+
return true;
|
|
15447
|
+
} catch {
|
|
15448
|
+
return false;
|
|
15449
|
+
}
|
|
15450
|
+
}
|
|
15451
|
+
readProcessInfo() {
|
|
15452
|
+
if (!fs5.existsSync(this.processJsonPath)) return null;
|
|
15453
|
+
try {
|
|
15454
|
+
return JSON.parse(fs5.readFileSync(this.processJsonPath, "utf-8"));
|
|
15455
|
+
} catch {
|
|
15456
|
+
return null;
|
|
15457
|
+
}
|
|
15458
|
+
}
|
|
15459
|
+
removeProcessJson() {
|
|
15460
|
+
try {
|
|
15461
|
+
fs5.unlinkSync(this.processJsonPath);
|
|
15462
|
+
} catch {
|
|
15463
|
+
}
|
|
15464
|
+
}
|
|
15465
|
+
/** Parse a simple KEY=value dotenv file from the state dir. */
|
|
15466
|
+
loadDotEnv() {
|
|
15467
|
+
const envFile = path6.join(this.stateDir, ".env");
|
|
15468
|
+
if (!fs5.existsSync(envFile)) return {};
|
|
15469
|
+
const result = {};
|
|
15470
|
+
for (const line of fs5.readFileSync(envFile, "utf-8").split("\n")) {
|
|
15471
|
+
const trimmed = line.trim();
|
|
15472
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
15473
|
+
const eq25 = trimmed.indexOf("=");
|
|
15474
|
+
if (eq25 < 1) continue;
|
|
15475
|
+
result[trimmed.slice(0, eq25)] = trimmed.slice(eq25 + 1);
|
|
15476
|
+
}
|
|
15477
|
+
return result;
|
|
15478
|
+
}
|
|
15479
|
+
};
|
|
15480
|
+
function isProcessAlive(pid) {
|
|
15481
|
+
try {
|
|
15482
|
+
process.kill(pid, 0);
|
|
15483
|
+
return true;
|
|
15484
|
+
} catch {
|
|
15485
|
+
return false;
|
|
15486
|
+
}
|
|
15487
|
+
}
|
|
15488
|
+
async function terminateWithEscalation(pid) {
|
|
15489
|
+
try {
|
|
15490
|
+
process.kill(pid, "SIGTERM");
|
|
15491
|
+
} catch {
|
|
15492
|
+
return;
|
|
15493
|
+
}
|
|
15494
|
+
const deadline = Date.now() + 800;
|
|
15495
|
+
while (Date.now() < deadline) {
|
|
15496
|
+
if (!isProcessAlive(pid)) return;
|
|
15497
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
15498
|
+
}
|
|
15499
|
+
try {
|
|
15500
|
+
process.kill(pid, "SIGKILL");
|
|
15501
|
+
} catch {
|
|
15502
|
+
}
|
|
15503
|
+
}
|
|
15504
|
+
|
|
15505
|
+
// src/agent-bootstrap.ts
|
|
15506
|
+
import { execFileSync as execFileSync2, execSync } from "child_process";
|
|
15507
|
+
import fs6 from "fs";
|
|
15508
|
+
import os5 from "os";
|
|
15509
|
+
import path7 from "path";
|
|
15510
|
+
import { fileURLToPath } from "url";
|
|
15511
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
15512
|
+
var cachedResult = null;
|
|
15513
|
+
var cachedAt = 0;
|
|
15514
|
+
function getAeroStateDir(profile = "aero") {
|
|
15515
|
+
return path7.join(os5.homedir(), `.openclaw-${profile}`);
|
|
15516
|
+
}
|
|
15517
|
+
async function detectOpenClaw(config) {
|
|
15518
|
+
if (cachedResult && Date.now() - cachedAt < CACHE_TTL_MS) {
|
|
15519
|
+
return cachedResult;
|
|
15520
|
+
}
|
|
15521
|
+
let result;
|
|
15522
|
+
if (config?.binary) {
|
|
15523
|
+
const version = probeVersion(config.binary);
|
|
15524
|
+
if (version) {
|
|
15525
|
+
result = { found: true, path: config.binary, version };
|
|
15526
|
+
cachedResult = result;
|
|
15527
|
+
cachedAt = Date.now();
|
|
15528
|
+
return result;
|
|
15529
|
+
}
|
|
15530
|
+
}
|
|
15531
|
+
const binaryPath = findInPath();
|
|
15532
|
+
if (binaryPath) {
|
|
15533
|
+
const version = probeVersion(binaryPath);
|
|
15534
|
+
if (version) {
|
|
15535
|
+
result = { found: true, path: binaryPath, version };
|
|
15536
|
+
cachedResult = result;
|
|
15537
|
+
cachedAt = Date.now();
|
|
15538
|
+
return result;
|
|
15539
|
+
}
|
|
15540
|
+
}
|
|
15541
|
+
result = { found: false };
|
|
15542
|
+
cachedResult = result;
|
|
15543
|
+
cachedAt = Date.now();
|
|
15544
|
+
return result;
|
|
15545
|
+
}
|
|
15546
|
+
detectOpenClaw.resetCache = () => {
|
|
15547
|
+
cachedResult = null;
|
|
15548
|
+
cachedAt = 0;
|
|
15549
|
+
};
|
|
15550
|
+
function probeVersion(binaryPath) {
|
|
15551
|
+
try {
|
|
15552
|
+
const output = execFileSync2(binaryPath, ["--version"], {
|
|
15553
|
+
timeout: 5e3,
|
|
15554
|
+
encoding: "utf-8"
|
|
15555
|
+
});
|
|
15556
|
+
const match = output.toString().trim().match(/(\d+\.\d+\.\d+)/);
|
|
15557
|
+
return match ? match[1] : output.toString().trim();
|
|
15558
|
+
} catch {
|
|
15559
|
+
return null;
|
|
15560
|
+
}
|
|
15561
|
+
}
|
|
15562
|
+
function findInPath() {
|
|
15563
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
15564
|
+
try {
|
|
15565
|
+
const output = execFileSync2(cmd, ["openclaw"], {
|
|
15566
|
+
timeout: 5e3,
|
|
15567
|
+
encoding: "utf-8"
|
|
15568
|
+
});
|
|
15569
|
+
return output.toString().trim().split("\n")[0] || null;
|
|
15570
|
+
} catch {
|
|
15571
|
+
return null;
|
|
15572
|
+
}
|
|
15573
|
+
}
|
|
15574
|
+
async function installOpenClaw(opts) {
|
|
15575
|
+
try {
|
|
15576
|
+
execSync("npm install -g openclaw", {
|
|
15577
|
+
timeout: 12e4,
|
|
15578
|
+
stdio: opts?.silent ? "pipe" : "inherit"
|
|
15579
|
+
});
|
|
15580
|
+
} catch (err) {
|
|
15581
|
+
return {
|
|
15582
|
+
success: false,
|
|
15583
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15584
|
+
};
|
|
15585
|
+
}
|
|
15586
|
+
detectOpenClaw.resetCache();
|
|
15587
|
+
const detection = await detectOpenClaw();
|
|
15588
|
+
if (!detection.found) {
|
|
15589
|
+
return {
|
|
15590
|
+
success: false,
|
|
15591
|
+
error: "npm install succeeded but openclaw binary was not found in PATH"
|
|
15592
|
+
};
|
|
15593
|
+
}
|
|
15594
|
+
return { success: true, detection };
|
|
15595
|
+
}
|
|
15596
|
+
function seedWorkspace(stateDir) {
|
|
15597
|
+
const workspaceDir = path7.join(stateDir, "workspace");
|
|
15598
|
+
fs6.mkdirSync(workspaceDir, { recursive: true });
|
|
15599
|
+
const __dirname = path7.dirname(fileURLToPath(import.meta.url));
|
|
15600
|
+
const assetsDir = path7.join(__dirname, "..", "assets", "agent-workspace");
|
|
15601
|
+
if (!fs6.existsSync(assetsDir)) {
|
|
15602
|
+
return;
|
|
15603
|
+
}
|
|
15604
|
+
copyDirRecursive(assetsDir, workspaceDir);
|
|
15605
|
+
}
|
|
15606
|
+
function initializeOpenClawProfile(binary, profile, workspaceDir) {
|
|
15607
|
+
try {
|
|
15608
|
+
execFileSync2(binary, [
|
|
15609
|
+
"--profile",
|
|
15610
|
+
profile,
|
|
15611
|
+
"onboard",
|
|
15612
|
+
"--non-interactive",
|
|
15613
|
+
"--accept-risk",
|
|
15614
|
+
"--mode",
|
|
15615
|
+
"local",
|
|
15616
|
+
"--workspace",
|
|
15617
|
+
workspaceDir,
|
|
15618
|
+
"--skip-channels",
|
|
15619
|
+
"--skip-skills",
|
|
15620
|
+
"--skip-health",
|
|
15621
|
+
"--no-install-daemon"
|
|
15622
|
+
], { timeout: 3e4, stdio: "pipe" });
|
|
15623
|
+
} catch (err) {
|
|
15624
|
+
const stderr = err instanceof Error && "stderr" in err ? String(err.stderr) : "";
|
|
15625
|
+
if (stderr.toLowerCase().includes("already")) return;
|
|
15626
|
+
throw new CliError({
|
|
15627
|
+
code: "AGENT_PROFILE_INIT_FAILED",
|
|
15628
|
+
message: `Failed to initialize OpenClaw profile: ${stderr || (err instanceof Error ? err.message : String(err))}`,
|
|
15629
|
+
displayMessage: `Failed to initialize OpenClaw profile "${profile}".`
|
|
15630
|
+
});
|
|
15631
|
+
}
|
|
15632
|
+
}
|
|
15633
|
+
function configureOpenClawGateway(binary, profile, gatewayPort) {
|
|
15634
|
+
const entries = [
|
|
15635
|
+
["gateway.mode", "local", false],
|
|
15636
|
+
["gateway.port", String(gatewayPort), true]
|
|
15637
|
+
];
|
|
15638
|
+
for (const [key, value, strict] of entries) {
|
|
15639
|
+
try {
|
|
15640
|
+
const args = ["--profile", profile, "config", "set", key, value];
|
|
15641
|
+
if (strict) args.push("--strict-json");
|
|
15642
|
+
execFileSync2(binary, args, { timeout: 1e4, stdio: "pipe" });
|
|
15643
|
+
} catch (err) {
|
|
15644
|
+
throw new CliError({
|
|
15645
|
+
code: "AGENT_GATEWAY_CONFIG_FAILED",
|
|
15646
|
+
message: `Failed to set ${key}=${value}: ${err instanceof Error ? err.message : String(err)}`,
|
|
15647
|
+
displayMessage: `Failed to configure OpenClaw gateway (${key}).`
|
|
15648
|
+
});
|
|
15649
|
+
}
|
|
15650
|
+
}
|
|
15651
|
+
}
|
|
15652
|
+
function setOpenClawModel(binary, profile, model) {
|
|
15653
|
+
try {
|
|
15654
|
+
execFileSync2(binary, [
|
|
15655
|
+
"--profile",
|
|
15656
|
+
profile,
|
|
15657
|
+
"models",
|
|
15658
|
+
"set",
|
|
15659
|
+
model
|
|
15660
|
+
], { timeout: 1e4, stdio: "pipe" });
|
|
15661
|
+
} catch (err) {
|
|
15662
|
+
throw new CliError({
|
|
15663
|
+
code: "AGENT_MODEL_SET_FAILED",
|
|
15664
|
+
message: `Failed to set agent model to ${model}: ${err instanceof Error ? err.message : String(err)}`,
|
|
15665
|
+
displayMessage: `Failed to set agent model to "${model}".`
|
|
15666
|
+
});
|
|
15667
|
+
}
|
|
15668
|
+
}
|
|
15669
|
+
function providerEnvVar(provider) {
|
|
15670
|
+
const map = {
|
|
15671
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
15672
|
+
openai: "OPENAI_API_KEY",
|
|
15673
|
+
google: "GOOGLE_API_KEY",
|
|
15674
|
+
"google-vertex": "GOOGLE_API_KEY",
|
|
15675
|
+
groq: "GROQ_API_KEY",
|
|
15676
|
+
mistral: "MISTRAL_API_KEY",
|
|
15677
|
+
xai: "XAI_API_KEY",
|
|
15678
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
15679
|
+
cerebras: "CEREBRAS_API_KEY"
|
|
15680
|
+
};
|
|
15681
|
+
return map[provider] ?? `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
15682
|
+
}
|
|
15683
|
+
function writeAgentEnv(stateDir, key, value) {
|
|
15684
|
+
const envFile = path7.join(stateDir, ".env");
|
|
15685
|
+
let lines = [];
|
|
15686
|
+
if (fs6.existsSync(envFile)) {
|
|
15687
|
+
lines = fs6.readFileSync(envFile, "utf-8").split("\n");
|
|
15688
|
+
}
|
|
15689
|
+
const prefix = `${key}=`;
|
|
15690
|
+
const idx = lines.findIndex((l) => l.startsWith(prefix));
|
|
15691
|
+
const entry = `${key}=${value}`;
|
|
15692
|
+
if (idx >= 0) {
|
|
15693
|
+
lines[idx] = entry;
|
|
15694
|
+
} else {
|
|
15695
|
+
lines.push(entry);
|
|
15696
|
+
}
|
|
15697
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
15698
|
+
fs6.writeFileSync(envFile, lines.join("\n") + "\n", "utf-8");
|
|
15699
|
+
}
|
|
15700
|
+
function resolveAgentCredentials(opts) {
|
|
15701
|
+
const provider = opts.agentProvider ?? "anthropic";
|
|
15702
|
+
if (opts.agentKey) {
|
|
15703
|
+
return { provider, key: opts.agentKey, model: opts.agentModel };
|
|
15704
|
+
}
|
|
15705
|
+
const envVar = providerEnvVar(provider);
|
|
15706
|
+
const envKey = process.env[envVar];
|
|
15707
|
+
if (envKey) {
|
|
15708
|
+
return { provider, key: envKey, model: opts.agentModel };
|
|
15709
|
+
}
|
|
15710
|
+
const genericKey = process.env.CANONRY_AGENT_KEY;
|
|
15711
|
+
if (genericKey) {
|
|
15712
|
+
return { provider, key: genericKey, model: opts.agentModel };
|
|
15713
|
+
}
|
|
15714
|
+
const envFile = path7.join(opts.stateDir, ".env");
|
|
15715
|
+
if (fs6.existsSync(envFile)) {
|
|
15716
|
+
const hasKey = fs6.readFileSync(envFile, "utf-8").split("\n").some((l) => l.includes("_API_KEY="));
|
|
15717
|
+
if (hasKey) {
|
|
15718
|
+
return { provider, key: void 0, model: opts.agentModel };
|
|
15719
|
+
}
|
|
15720
|
+
}
|
|
15721
|
+
return { provider, key: void 0, model: opts.agentModel };
|
|
15722
|
+
}
|
|
15723
|
+
function copyDirRecursive(src, dest) {
|
|
15724
|
+
fs6.mkdirSync(dest, { recursive: true });
|
|
15725
|
+
for (const entry of fs6.readdirSync(src, { withFileTypes: true })) {
|
|
15726
|
+
const srcPath = path7.join(src, entry.name);
|
|
15727
|
+
const destPath = path7.join(dest, entry.name);
|
|
15728
|
+
if (entry.isDirectory()) {
|
|
15729
|
+
copyDirRecursive(srcPath, destPath);
|
|
15730
|
+
} else {
|
|
15731
|
+
fs6.copyFileSync(srcPath, destPath);
|
|
15732
|
+
}
|
|
15733
|
+
}
|
|
15734
|
+
}
|
|
15735
|
+
|
|
15127
15736
|
// src/server.ts
|
|
15128
15737
|
var _require2 = createRequire2(import.meta.url);
|
|
15129
15738
|
var { version: PKG_VERSION } = _require2("../package.json");
|
|
15130
|
-
var
|
|
15739
|
+
var log9 = createLogger("Server");
|
|
15131
15740
|
var DEFAULT_QUOTA = {
|
|
15132
15741
|
maxConcurrency: 2,
|
|
15133
15742
|
maxRequestsPerMinute: 10,
|
|
@@ -15158,7 +15767,7 @@ function summarizeProviderConfig(provider, config) {
|
|
|
15158
15767
|
};
|
|
15159
15768
|
}
|
|
15160
15769
|
function hashApiKey(key) {
|
|
15161
|
-
return
|
|
15770
|
+
return crypto23.createHash("sha256").update(key).digest("hex");
|
|
15162
15771
|
}
|
|
15163
15772
|
function parseCookies2(header) {
|
|
15164
15773
|
if (!header) return {};
|
|
@@ -15223,7 +15832,7 @@ function migrateDbCredentialsToConfig(db, config) {
|
|
|
15223
15832
|
}
|
|
15224
15833
|
if (migrated > 0) {
|
|
15225
15834
|
saveConfigPatch({ google: config.google });
|
|
15226
|
-
|
|
15835
|
+
log9.info("credentials.migrated", { type: "google", count: migrated });
|
|
15227
15836
|
}
|
|
15228
15837
|
}
|
|
15229
15838
|
const gaColCheck = db.all(sql6.raw(
|
|
@@ -15235,7 +15844,7 @@ function migrateDbCredentialsToConfig(db, config) {
|
|
|
15235
15844
|
));
|
|
15236
15845
|
let migrated = 0;
|
|
15237
15846
|
for (const row of rows) {
|
|
15238
|
-
const project = db.select({ name: projects.name }).from(projects).where(
|
|
15847
|
+
const project = db.select({ name: projects.name }).from(projects).where(eq24(projects.id, row.project_id)).get();
|
|
15239
15848
|
if (!project) continue;
|
|
15240
15849
|
const existing = getGa4Connection(config, project.name);
|
|
15241
15850
|
if (existing?.privateKey) continue;
|
|
@@ -15251,7 +15860,7 @@ function migrateDbCredentialsToConfig(db, config) {
|
|
|
15251
15860
|
}
|
|
15252
15861
|
if (migrated > 0) {
|
|
15253
15862
|
saveConfigPatch({ ga4: config.ga4 });
|
|
15254
|
-
|
|
15863
|
+
log9.info("credentials.migrated", { type: "ga4", count: migrated });
|
|
15255
15864
|
}
|
|
15256
15865
|
}
|
|
15257
15866
|
} catch {
|
|
@@ -15282,7 +15891,7 @@ async function createServer(opts) {
|
|
|
15282
15891
|
};
|
|
15283
15892
|
}
|
|
15284
15893
|
migrateDbCredentialsToConfig(opts.db, opts.config);
|
|
15285
|
-
|
|
15894
|
+
log9.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
|
|
15286
15895
|
const p = providers[k];
|
|
15287
15896
|
return p?.apiKey || p?.baseUrl || p?.vertexProject;
|
|
15288
15897
|
}) });
|
|
@@ -15319,9 +15928,28 @@ async function createServer(opts) {
|
|
|
15319
15928
|
jobRunner.recoverStaleRuns();
|
|
15320
15929
|
const notifier = new Notifier(opts.db, serverUrl);
|
|
15321
15930
|
const intelligenceService = new IntelligenceService(opts.db);
|
|
15322
|
-
const runCoordinator = new RunCoordinator(
|
|
15931
|
+
const runCoordinator = new RunCoordinator(
|
|
15932
|
+
notifier,
|
|
15933
|
+
intelligenceService,
|
|
15934
|
+
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result)
|
|
15935
|
+
);
|
|
15323
15936
|
jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
|
|
15324
15937
|
const snapshotService = new SnapshotService(registry);
|
|
15938
|
+
let agentManager;
|
|
15939
|
+
let agentAutoStarted = false;
|
|
15940
|
+
if (opts.config.agent) {
|
|
15941
|
+
const stateDir = getAeroStateDir(opts.config.agent.profile ?? "aero");
|
|
15942
|
+
agentManager = new AgentManager(opts.config.agent, stateDir);
|
|
15943
|
+
if (opts.config.agent.autoStart) {
|
|
15944
|
+
try {
|
|
15945
|
+
await agentManager.start();
|
|
15946
|
+
agentAutoStarted = true;
|
|
15947
|
+
app.log.info({ pid: agentManager.status().pid }, "Agent gateway started");
|
|
15948
|
+
} catch (err) {
|
|
15949
|
+
app.log.error({ err }, "Failed to auto-start agent gateway");
|
|
15950
|
+
}
|
|
15951
|
+
}
|
|
15952
|
+
}
|
|
15325
15953
|
const scheduler = new Scheduler(opts.db, {
|
|
15326
15954
|
onRunCreated: (runId, projectId, providers2, location) => {
|
|
15327
15955
|
jobRunner.executeRun(runId, projectId, providers2, location).catch((err) => {
|
|
@@ -15397,7 +16025,7 @@ async function createServer(opts) {
|
|
|
15397
16025
|
return removed;
|
|
15398
16026
|
}
|
|
15399
16027
|
};
|
|
15400
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
16028
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
|
|
15401
16029
|
const googleConnectionStore = {
|
|
15402
16030
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
15403
16031
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -15443,11 +16071,11 @@ async function createServer(opts) {
|
|
|
15443
16071
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
15444
16072
|
if (opts.config.apiKey) {
|
|
15445
16073
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
15446
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
16074
|
+
const existing = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, keyHash)).get();
|
|
15447
16075
|
if (!existing) {
|
|
15448
16076
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
15449
16077
|
opts.db.insert(apiKeys).values({
|
|
15450
|
-
id: `key_${
|
|
16078
|
+
id: `key_${crypto23.randomBytes(8).toString("hex")}`,
|
|
15451
16079
|
name: "default",
|
|
15452
16080
|
keyHash,
|
|
15453
16081
|
keyPrefix: prefix,
|
|
@@ -15471,7 +16099,7 @@ async function createServer(opts) {
|
|
|
15471
16099
|
};
|
|
15472
16100
|
const createSession = (apiKeyId) => {
|
|
15473
16101
|
pruneExpiredSessions();
|
|
15474
|
-
const sessionId =
|
|
16102
|
+
const sessionId = crypto23.randomBytes(32).toString("hex");
|
|
15475
16103
|
sessions.set(sessionId, {
|
|
15476
16104
|
apiKeyId,
|
|
15477
16105
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -15495,7 +16123,7 @@ async function createServer(opts) {
|
|
|
15495
16123
|
};
|
|
15496
16124
|
const getDefaultApiKey = () => {
|
|
15497
16125
|
if (!opts.config.apiKey) return void 0;
|
|
15498
|
-
return opts.db.select().from(apiKeys).where(
|
|
16126
|
+
return opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
15499
16127
|
};
|
|
15500
16128
|
const createPasswordSession = (reply) => {
|
|
15501
16129
|
const key = getDefaultApiKey();
|
|
@@ -15552,12 +16180,12 @@ async function createServer(opts) {
|
|
|
15552
16180
|
return reply.send({ authenticated: true });
|
|
15553
16181
|
}
|
|
15554
16182
|
if (apiKey) {
|
|
15555
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
16183
|
+
const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
15556
16184
|
if (!key || key.revokedAt) {
|
|
15557
16185
|
const err2 = authInvalid();
|
|
15558
16186
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
15559
16187
|
}
|
|
15560
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
16188
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(apiKeys.id, key.id)).run();
|
|
15561
16189
|
const sessionId = createSession(key.id);
|
|
15562
16190
|
reply.header("set-cookie", serializeSessionCookie({
|
|
15563
16191
|
name: SESSION_COOKIE_NAME,
|
|
@@ -15698,7 +16326,7 @@ async function createServer(opts) {
|
|
|
15698
16326
|
const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
|
|
15699
16327
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15700
16328
|
opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
|
|
15701
|
-
id:
|
|
16329
|
+
id: crypto23.randomUUID(),
|
|
15702
16330
|
projectId,
|
|
15703
16331
|
actor: "api",
|
|
15704
16332
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -15745,6 +16373,17 @@ async function createServer(opts) {
|
|
|
15745
16373
|
onProjectDeleted: (projectId) => {
|
|
15746
16374
|
scheduler.remove(projectId);
|
|
15747
16375
|
},
|
|
16376
|
+
onProjectUpserted: agentManager && opts.config.agent?.autoStart ? (_projectId, projectName) => {
|
|
16377
|
+
try {
|
|
16378
|
+
const gatewayPort = opts.config.agent?.gatewayPort ?? 3579;
|
|
16379
|
+
const result = attachAgentWebhookDirect(opts.db, _projectId, gatewayPort);
|
|
16380
|
+
if (result === "attached") {
|
|
16381
|
+
app.log.info({ projectName }, "Auto-attached agent webhook");
|
|
16382
|
+
}
|
|
16383
|
+
} catch (err) {
|
|
16384
|
+
app.log.error({ err, projectName }, "Failed to auto-attach agent webhook");
|
|
16385
|
+
}
|
|
16386
|
+
} : void 0,
|
|
15748
16387
|
getTelemetryStatus: () => {
|
|
15749
16388
|
const enabled = isTelemetryEnabled();
|
|
15750
16389
|
return {
|
|
@@ -15829,10 +16468,10 @@ async function createServer(opts) {
|
|
|
15829
16468
|
return snapshotService.createReport(input);
|
|
15830
16469
|
}
|
|
15831
16470
|
});
|
|
15832
|
-
const dirname =
|
|
15833
|
-
const assetsDir =
|
|
15834
|
-
if (
|
|
15835
|
-
const indexPath =
|
|
16471
|
+
const dirname = path8.dirname(fileURLToPath2(import.meta.url));
|
|
16472
|
+
const assetsDir = path8.join(dirname, "..", "assets");
|
|
16473
|
+
if (fs7.existsSync(assetsDir)) {
|
|
16474
|
+
const indexPath = path8.join(assetsDir, "index.html");
|
|
15836
16475
|
const injectConfig = (html) => {
|
|
15837
16476
|
const clientConfig = {};
|
|
15838
16477
|
if (basePath) clientConfig.basePath = basePath;
|
|
@@ -15850,8 +16489,8 @@ async function createServer(opts) {
|
|
|
15850
16489
|
index: false
|
|
15851
16490
|
});
|
|
15852
16491
|
const serveIndex = (_request, reply) => {
|
|
15853
|
-
if (
|
|
15854
|
-
const html =
|
|
16492
|
+
if (fs7.existsSync(indexPath)) {
|
|
16493
|
+
const html = fs7.readFileSync(indexPath, "utf-8");
|
|
15855
16494
|
return reply.type("text/html").send(injectConfig(html));
|
|
15856
16495
|
}
|
|
15857
16496
|
return reply.status(404).send({ error: "Dashboard not built" });
|
|
@@ -15871,8 +16510,8 @@ async function createServer(opts) {
|
|
|
15871
16510
|
if (basePath && !url.startsWith(basePath)) {
|
|
15872
16511
|
return reply.status(404).send({ error: "Not found", path: request.url });
|
|
15873
16512
|
}
|
|
15874
|
-
if (
|
|
15875
|
-
const html =
|
|
16513
|
+
if (fs7.existsSync(indexPath)) {
|
|
16514
|
+
const html = fs7.readFileSync(indexPath, "utf-8");
|
|
15876
16515
|
return reply.type("text/html").send(injectConfig(html));
|
|
15877
16516
|
}
|
|
15878
16517
|
return reply.status(404).send({ error: "Not found" });
|
|
@@ -15891,6 +16530,13 @@ async function createServer(opts) {
|
|
|
15891
16530
|
scheduler.start();
|
|
15892
16531
|
app.addHook("onClose", async () => {
|
|
15893
16532
|
scheduler.stop();
|
|
16533
|
+
if (agentManager && agentAutoStarted) {
|
|
16534
|
+
try {
|
|
16535
|
+
await agentManager.stop();
|
|
16536
|
+
} catch (err) {
|
|
16537
|
+
app.log.error({ err }, "Failed to stop agent gateway");
|
|
16538
|
+
}
|
|
16539
|
+
}
|
|
15894
16540
|
});
|
|
15895
16541
|
return app;
|
|
15896
16542
|
}
|
|
@@ -15952,6 +16598,11 @@ export {
|
|
|
15952
16598
|
isFirstRun,
|
|
15953
16599
|
showFirstRunNotice,
|
|
15954
16600
|
trackEvent,
|
|
16601
|
+
EXIT_USER_ERROR,
|
|
16602
|
+
EXIT_SYSTEM_ERROR,
|
|
16603
|
+
CliError,
|
|
16604
|
+
usageError,
|
|
16605
|
+
printCliError,
|
|
15955
16606
|
providerQuotaPolicySchema,
|
|
15956
16607
|
ProviderNames,
|
|
15957
16608
|
resolveProviderInput,
|
|
@@ -15968,5 +16619,19 @@ export {
|
|
|
15968
16619
|
extractRecommendedCompetitors,
|
|
15969
16620
|
setGoogleAuthConfig,
|
|
15970
16621
|
formatAuditFactorScore,
|
|
16622
|
+
AGENT_WEBHOOK_EVENTS,
|
|
16623
|
+
buildAgentWebhookUrl,
|
|
16624
|
+
attachAgentWebhookDirect,
|
|
16625
|
+
AgentManager,
|
|
16626
|
+
getAeroStateDir,
|
|
16627
|
+
detectOpenClaw,
|
|
16628
|
+
installOpenClaw,
|
|
16629
|
+
seedWorkspace,
|
|
16630
|
+
initializeOpenClawProfile,
|
|
16631
|
+
configureOpenClawGateway,
|
|
16632
|
+
setOpenClawModel,
|
|
16633
|
+
providerEnvVar,
|
|
16634
|
+
writeAgentEnv,
|
|
16635
|
+
resolveAgentCredentials,
|
|
15971
16636
|
createServer
|
|
15972
16637
|
};
|