@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.
@@ -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 crypto22 from "crypto";
268
- import fs5 from "fs";
269
- import path6 from "path";
270
- import { fileURLToPath } from "url";
271
- import { eq as eq23, sql as sql6 } from "drizzle-orm";
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.update(projects).set({
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: existing.configRevision + 1,
1575
+ configRevision: 1,
1576
+ createdAt: now,
1470
1577
  updatedAt: now
1471
- }).where(eq3(projects.id, existing.id)).run();
1472
- writeAuditLog(app.db, {
1473
- projectId: existing.id,
1578
+ }).run();
1579
+ writeAuditLog(tx, {
1580
+ projectId: id,
1474
1581
  actor: "api",
1475
- action: "project.updated",
1582
+ action: "project.created",
1476
1583
  entityType: "project",
1477
- entityId: existing.id
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 path7 = `${target.url.pathname}${target.url.search}`;
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: path7,
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, path7) {
5537
- const parts = path7.split("/").filter(Boolean).map((part) => {
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(/\/$/, "") + (opts.routePrefix ?? "/api/v1") + "/google/callback";
6967
+ redirectUri = publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
6884
6968
  } else if (opts.publicUrl) {
6885
- redirectUri = opts.publicUrl.replace(/\/$/, "") + (opts.routePrefix ?? "/api/v1") + "/google/callback";
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 explicitly unindexed URLs found. Run "canonry bing inspect <project> <url>" first.');
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, path7, init) {
9140
- const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path7}`, {
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 crypto18 from "crypto";
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 eq18, inArray as inArray3, sql as sql4 } from "drizzle-orm";
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(eq18(runs.id, run.id)).run();
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(eq18(runs.id, runId), eq18(runs.status, "queued"))).run();
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(eq18(projects.id, projectId)).get();
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(eq18(keywords.projectId, projectId)).all();
13419
- const projectCompetitors = this.db.select().from(competitors).where(eq18(competitors.projectId, projectId)).all();
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(eq18(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
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 = crypto18.randomUUID();
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: crypto18.randomUUID(),
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(eq18(runs.id, runId)).run();
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(eq18(runs.id, runId)).run();
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(eq18(runs.id, runId)).run();
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(eq18(runs.id, runId)).run();
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: crypto18.randomUUID(),
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(eq18(runs.id, runId)).get();
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(eq18(runs.id, runId)).run();
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 crypto19 from "crypto";
13698
- import { eq as eq19, and as and8, sql as sql5 } from "drizzle-orm";
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(eq19(runs.id, runId)).run();
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(eq19(projects.id, projectId)).get();
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
- eq19(gscSearchData.projectId, projectId),
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: crypto19.randomUUID(),
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: crypto19.randomUUID(),
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(eq19(gscUrlInspections.projectId, projectId)).all();
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(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
13958
+ db.delete(gscCoverageSnapshots).where(and8(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
13840
13959
  db.insert(gscCoverageSnapshots).values({
13841
- id: crypto19.randomUUID(),
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(eq19(runs.id, runId)).run();
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(eq19(runs.id, runId)).run();
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 crypto20 from "crypto";
13862
- import { eq as eq20, and as and9 } from "drizzle-orm";
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(eq20(runs.id, runId)).run();
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(eq20(projects.id, projectId)).get();
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: crypto20.randomUUID(),
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(eq20(gscUrlInspections.projectId, projectId)).all();
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(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
14145
+ db.delete(gscCoverageSnapshots).where(and9(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
14027
14146
  db.insert(gscCoverageSnapshots).values({
14028
- id: crypto20.randomUUID(),
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(eq20(runs.id, runId)).run();
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(eq20(runs.id, runId)).run();
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 eq21 } from "drizzle-orm";
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(eq21(schedules.enabled, 1)).all();
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(eq21(schedules.projectId, projectId)).get();
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(eq21(schedules.id, scheduleId)).run();
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(eq21(schedules.id, scheduleId)).get();
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(eq21(projects.id, projectId)).get();
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(eq21(schedules.id, currentSchedule.id)).run();
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(eq21(schedules.id, currentSchedule.id)).run();
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 eq22, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
14236
- import crypto21 from "crypto";
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(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
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(eq22(runs.id, runId)).get();
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(eq22(projects.id, projectId)).get();
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
- eq22(runs.projectId, projectId),
14302
- or2(eq22(runs.status, "completed"), eq22(runs.status, "partial"))
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, eq22(querySnapshots.keywordId, keywords.id)).where(eq22(querySnapshots.runId, currentRunId)).all();
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(eq22(querySnapshots.runId, previousRunId)).all();
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: crypto21.randomUUID(),
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 path7 = target.url.pathname + target.url.search;
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: path7,
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 log8 = createLogger("Server");
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 crypto22.createHash("sha256").update(key).digest("hex");
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
- log8.info("credentials.migrated", { type: "google", count: migrated });
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(eq23(projects.id, row.project_id)).get();
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
- log8.info("credentials.migrated", { type: "ga4", count: migrated });
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
- log8.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
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(notifier, intelligenceService);
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 ?? crypto22.randomBytes(32).toString("hex");
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(eq23(apiKeys.keyHash, keyHash)).get();
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_${crypto22.randomBytes(8).toString("hex")}`,
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 = crypto22.randomBytes(32).toString("hex");
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(eq23(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
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(eq23(apiKeys.keyHash, hashApiKey(apiKey))).get();
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(eq23(apiKeys.id, key.id)).run();
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: crypto22.randomUUID(),
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 = path6.dirname(fileURLToPath(import.meta.url));
15833
- const assetsDir = path6.join(dirname, "..", "assets");
15834
- if (fs5.existsSync(assetsDir)) {
15835
- const indexPath = path6.join(assetsDir, "index.html");
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 (fs5.existsSync(indexPath)) {
15854
- const html = fs5.readFileSync(indexPath, "utf-8");
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 (fs5.existsSync(indexPath)) {
15875
- const html = fs5.readFileSync(indexPath, "utf-8");
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
  };