@ainyc/canonry 1.46.1 → 1.48.2

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