@ainyc/canonry 1.48.4 → 2.2.1

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.
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  IntelligenceService,
3
+ agentMemory,
4
+ agentSessions,
3
5
  apiKeys,
4
6
  auditLog,
5
7
  bingCoverageSnapshots,
@@ -25,7 +27,7 @@ import {
25
27
  runs,
26
28
  schedules,
27
29
  usageCounters
28
- } from "./chunk-ZZ57GRV6.js";
30
+ } from "./chunk-TAII35VC.js";
29
31
 
30
32
  // src/config.ts
31
33
  import fs from "fs";
@@ -336,11 +338,11 @@ function printCliError(err, format) {
336
338
 
337
339
  // src/server.ts
338
340
  import { createRequire as createRequire2 } from "module";
339
- import crypto23 from "crypto";
340
- import fs7 from "fs";
341
- import path8 from "path";
341
+ import crypto24 from "crypto";
342
+ import fs8 from "fs";
343
+ import path9 from "path";
342
344
  import { fileURLToPath as fileURLToPath2 } from "url";
343
- import { eq as eq24 } from "drizzle-orm";
345
+ import { eq as eq26 } from "drizzle-orm";
344
346
  import Fastify from "fastify";
345
347
 
346
348
  // ../contracts/src/config-schema.ts
@@ -602,6 +604,13 @@ function notImplemented(message) {
602
604
  function deliveryFailed(message) {
603
605
  return new AppError("DELIVERY_FAILED", message, 502);
604
606
  }
607
+ function agentBusy(projectName) {
608
+ return new AppError(
609
+ "AGENT_BUSY",
610
+ `Aero is already running a turn for '${projectName}'. Retry after the current turn settles.`,
611
+ 409
612
+ );
613
+ }
605
614
 
606
615
  // ../contracts/src/google.ts
607
616
  import { z as z5 } from "zod";
@@ -889,6 +898,37 @@ var wordpressDiffDtoSchema = z7.object({
889
898
  })
890
899
  });
891
900
 
901
+ // ../contracts/src/providers.ts
902
+ var ProviderIds = {
903
+ claude: "claude",
904
+ openai: "openai",
905
+ gemini: "gemini",
906
+ perplexity: "perplexity",
907
+ local: "local",
908
+ cdpChatgpt: "cdp:chatgpt",
909
+ zai: "zai"
910
+ };
911
+ var PROVIDER_IDS = Object.values(ProviderIds);
912
+ var SweepProviderIds = {
913
+ claude: ProviderIds.claude,
914
+ openai: ProviderIds.openai,
915
+ gemini: ProviderIds.gemini,
916
+ perplexity: ProviderIds.perplexity,
917
+ local: ProviderIds.local,
918
+ cdpChatgpt: ProviderIds.cdpChatgpt
919
+ };
920
+ var SWEEP_PROVIDER_IDS = Object.values(SweepProviderIds);
921
+ var AgentProviderIds = {
922
+ claude: ProviderIds.claude,
923
+ openai: ProviderIds.openai,
924
+ gemini: ProviderIds.gemini,
925
+ zai: ProviderIds.zai
926
+ };
927
+ var AGENT_PROVIDER_IDS = Object.values(AgentProviderIds);
928
+ function isAgentProviderId(value) {
929
+ return AGENT_PROVIDER_IDS.includes(value);
930
+ }
931
+
892
932
  // ../contracts/src/run.ts
893
933
  import { z as z8 } from "zod";
894
934
  var runStatusSchema = z8.enum(["queued", "running", "completed", "partial", "failed", "cancelled"]);
@@ -1365,6 +1405,20 @@ function escapeRegExp(value) {
1365
1405
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1366
1406
  }
1367
1407
 
1408
+ // ../contracts/src/agent.ts
1409
+ import { z as z13 } from "zod";
1410
+ var memorySourceSchema = z13.enum(["aero", "user", "compaction"]);
1411
+ var MemorySources = memorySourceSchema.enum;
1412
+ var AGENT_MEMORY_VALUE_MAX_BYTES = 2 * 1024;
1413
+ var AGENT_MEMORY_KEY_MAX_LENGTH = 128;
1414
+ var agentMemoryUpsertRequestSchema = z13.object({
1415
+ key: z13.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH),
1416
+ value: z13.string().min(1)
1417
+ });
1418
+ var agentMemoryDeleteRequestSchema = z13.object({
1419
+ key: z13.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH)
1420
+ });
1421
+
1368
1422
  // ../api-routes/src/auth.ts
1369
1423
  import crypto2 from "crypto";
1370
1424
  import { eq } from "drizzle-orm";
@@ -2381,7 +2435,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
2381
2435
  const body = JSON.stringify(payload);
2382
2436
  const isHttps = target.url.protocol === "https:";
2383
2437
  const port = target.url.port ? Number(target.url.port) : isHttps ? 443 : 80;
2384
- const path9 = `${target.url.pathname}${target.url.search}`;
2438
+ const path10 = `${target.url.pathname}${target.url.search}`;
2385
2439
  const headers = {
2386
2440
  "Content-Length": String(Buffer.byteLength(body)),
2387
2441
  "Content-Type": "application/json",
@@ -2397,7 +2451,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
2397
2451
  headers,
2398
2452
  hostname: target.address,
2399
2453
  method: "POST",
2400
- path: path9,
2454
+ path: path10,
2401
2455
  port,
2402
2456
  timeout: REQUEST_TIMEOUT_MS
2403
2457
  };
@@ -5567,10 +5621,91 @@ var routeCatalog = [
5567
5621
  }
5568
5622
  }
5569
5623
  ];
5624
+ var canonryLocalRouteCatalog = [
5625
+ {
5626
+ method: "get",
5627
+ path: "/api/v1/projects/{name}/agent/transcript",
5628
+ summary: "Get the rolling Aero transcript for this project",
5629
+ description: "Returns the full message history of the project-scoped Aero session plus the persisted model provider/id and last-updated timestamp. Empty messages array when the project has no session yet.",
5630
+ tags: ["agent"],
5631
+ parameters: [nameParameter],
5632
+ responses: {
5633
+ 200: { description: "Transcript returned." },
5634
+ 404: { description: "Project not found." }
5635
+ }
5636
+ },
5637
+ {
5638
+ method: "delete",
5639
+ path: "/api/v1/projects/{name}/agent/transcript",
5640
+ summary: "Reset the Aero transcript + queued follow-ups",
5641
+ description: "Evicts any live Agent instance, clears the persisted messages and follow_up_queue. A subsequent prompt starts a fresh session.",
5642
+ tags: ["agent"],
5643
+ parameters: [nameParameter],
5644
+ responses: {
5645
+ 200: { description: "Session reset." },
5646
+ 404: { description: "Project not found." }
5647
+ }
5648
+ },
5649
+ {
5650
+ method: "get",
5651
+ path: "/api/v1/projects/{name}/agent/providers",
5652
+ summary: "List the LLM providers Aero can route to",
5653
+ description: "Returns every provider Aero knows about with its default model, whether a usable API key is configured, and where the key resolved from (`config` | `env`). `defaultProvider` is the one Aero auto-picks when a caller omits `provider` on the prompt endpoint. Path is project-scoped for auth symmetry; the response does not vary per project today.",
5654
+ tags: ["agent"],
5655
+ parameters: [nameParameter],
5656
+ responses: {
5657
+ 200: { description: "Providers returned." },
5658
+ 404: { description: "Project not found." }
5659
+ }
5660
+ },
5661
+ {
5662
+ method: "post",
5663
+ path: "/api/v1/projects/{name}/agent/prompt",
5664
+ summary: "Send a prompt to Aero and stream events back as SSE",
5665
+ description: 'Posts a prompt into the project\'s Aero session and streams `AgentEvent` frames as `text/event-stream`. Each frame is `data: <JSON>\\n\\n`. The server brackets the stream with `{"type":"stream_open"}` and `{"type":"stream_close"}` control frames; `{"type":"error","message":"..."}` surfaces in-stream failures without collapsing the stream. Returns 409 `AGENT_BUSY` if another turn is already in flight for this project. Body field `scope` accepts "all" | "read-only"; omitted defaults to "read-only" (safe dashboard surface). The CLI passes "all" to keep write tools available.',
5666
+ tags: ["agent"],
5667
+ parameters: [nameParameter],
5668
+ requestBody: {
5669
+ required: true,
5670
+ content: {
5671
+ "application/json": {
5672
+ schema: {
5673
+ type: "object",
5674
+ required: ["prompt"],
5675
+ properties: {
5676
+ prompt: { type: "string", description: "The user's message for Aero." },
5677
+ provider: {
5678
+ type: "string",
5679
+ enum: [...AGENT_PROVIDER_IDS],
5680
+ description: "Override the persisted LLM provider for this and subsequent turns."
5681
+ },
5682
+ modelId: {
5683
+ type: "string",
5684
+ description: "Override the persisted model id for this and subsequent turns."
5685
+ },
5686
+ scope: {
5687
+ type: "string",
5688
+ enum: ["all", "read-only"],
5689
+ description: 'Tool surface scope. Default "read-only". Set "all" to enable write tools.'
5690
+ }
5691
+ }
5692
+ }
5693
+ }
5694
+ }
5695
+ },
5696
+ responses: {
5697
+ 200: { description: "SSE stream of AgentEvent frames." },
5698
+ 400: { description: "Missing or empty prompt." },
5699
+ 404: { description: "Project not found." },
5700
+ 409: { description: "Another Aero turn is already in flight." }
5701
+ }
5702
+ }
5703
+ ];
5570
5704
  function buildOpenApiDocument(info = {}) {
5571
5705
  const BASE_PREFIX = "/api/v1";
5572
5706
  const prefix = info.routePrefix ?? BASE_PREFIX;
5573
- const paths = routeCatalog.reduce((acc, route) => {
5707
+ const fullCatalog = info.includeCanonryLocal ? [...routeCatalog, ...canonryLocalRouteCatalog] : routeCatalog;
5708
+ const paths = fullCatalog.reduce((acc, route) => {
5574
5709
  const subpath = route.path.startsWith(BASE_PREFIX) ? route.path.slice(BASE_PREFIX.length) : route.path;
5575
5710
  const fullPath = prefix + subpath;
5576
5711
  const operation = {
@@ -5618,8 +5753,8 @@ async function openApiRoutes(app, opts = {}) {
5618
5753
  return reply.type("application/json").send(buildOpenApiDocument(opts));
5619
5754
  });
5620
5755
  }
5621
- function buildOperationId(method, path9) {
5622
- const parts = path9.split("/").filter(Boolean).map((part) => {
5756
+ function buildOperationId(method, path10) {
5757
+ const parts = path10.split("/").filter(Boolean).map((part) => {
5623
5758
  if (part.startsWith("{") && part.endsWith("}")) {
5624
5759
  return `by-${part.slice(1, -1)}`;
5625
5760
  }
@@ -9265,10 +9400,10 @@ function buildAuthErrorMessage(res, responseText) {
9265
9400
  }
9266
9401
  return "WordPress credentials are invalid or lack permission for this action";
9267
9402
  }
9268
- async function fetchJson(connection, siteUrl, path9, init) {
9403
+ async function fetchJson(connection, siteUrl, path10, init) {
9269
9404
  if (siteUrl.startsWith("http:")) {
9270
9405
  }
9271
- const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path9}`, {
9406
+ const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path10}`, {
9272
9407
  ...init,
9273
9408
  headers: {
9274
9409
  "Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
@@ -10858,41 +10993,14 @@ async function apiRoutes(app, opts) {
10858
10993
  googleConnectionStore: opts.googleConnectionStore,
10859
10994
  getGoogleAuthConfig: opts.getGoogleAuthConfig
10860
10995
  });
10996
+ if (opts.registerAuthenticatedRoutes) {
10997
+ await opts.registerAuthenticatedRoutes(api);
10998
+ }
10861
10999
  }, { prefix: opts.routePrefix ?? "/api/v1" });
10862
11000
  }
10863
11001
 
10864
- // src/agent-webhook.ts
10865
- import crypto18 from "crypto";
10866
- import { eq as eq18 } from "drizzle-orm";
10867
- var AGENT_WEBHOOK_EVENTS = ["run.completed", "insight.critical", "insight.high", "citation.gained"];
10868
- function buildAgentWebhookUrl(gatewayPort) {
10869
- return `http://localhost:${gatewayPort}/hooks/canonry`;
10870
- }
10871
- function attachAgentWebhookDirect(db, projectId, gatewayPort) {
10872
- const agentUrl = buildAgentWebhookUrl(gatewayPort);
10873
- const existing = db.select().from(notifications).where(eq18(notifications.projectId, projectId)).all();
10874
- const hasAgent = existing.some((n) => {
10875
- const cfg = parseJsonColumn(n.config, {});
10876
- return cfg.source === "agent";
10877
- });
10878
- if (hasAgent) return "already-attached";
10879
- const now = (/* @__PURE__ */ new Date()).toISOString();
10880
- db.insert(notifications).values({
10881
- id: crypto18.randomUUID(),
10882
- projectId,
10883
- channel: "webhook",
10884
- config: JSON.stringify({
10885
- url: agentUrl,
10886
- events: [...AGENT_WEBHOOK_EVENTS],
10887
- source: "agent"
10888
- }),
10889
- enabled: 1,
10890
- webhookSecret: crypto18.randomUUID(),
10891
- createdAt: now,
10892
- updatedAt: now
10893
- }).run();
10894
- return "attached";
10895
- }
11002
+ // src/server.ts
11003
+ import os5 from "os";
10896
11004
 
10897
11005
  // ../provider-gemini/src/normalize.ts
10898
11006
  import { GoogleGenAI } from "@google/genai";
@@ -13345,11 +13453,11 @@ function removeWordpressConnection(config, projectName) {
13345
13453
  }
13346
13454
 
13347
13455
  // src/job-runner.ts
13348
- import crypto19 from "crypto";
13456
+ import crypto18 from "crypto";
13349
13457
  import fs4 from "fs";
13350
13458
  import path5 from "path";
13351
13459
  import os4 from "os";
13352
- import { and as and7, eq as eq19, inArray as inArray3, sql as sql4 } from "drizzle-orm";
13460
+ import { and as and7, eq as eq18, inArray as inArray3, sql as sql4 } from "drizzle-orm";
13353
13461
 
13354
13462
  // src/citation-utils.ts
13355
13463
  function domainMatches(domain, canonicalDomain) {
@@ -13585,7 +13693,7 @@ var JobRunner = class {
13585
13693
  if (stale.length === 0) return;
13586
13694
  const now = (/* @__PURE__ */ new Date()).toISOString();
13587
13695
  for (const run of stale) {
13588
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq19(runs.id, run.id)).run();
13696
+ this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq18(runs.id, run.id)).run();
13589
13697
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
13590
13698
  }
13591
13699
  }
@@ -13613,10 +13721,10 @@ var JobRunner = class {
13613
13721
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
13614
13722
  }
13615
13723
  if (existingRun.status === "queued") {
13616
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(eq19(runs.id, runId), eq19(runs.status, "queued"))).run();
13724
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(eq18(runs.id, runId), eq18(runs.status, "queued"))).run();
13617
13725
  }
13618
13726
  this.throwIfRunCancelled(runId);
13619
- const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
13727
+ const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
13620
13728
  if (!project) {
13621
13729
  throw new Error(`Project ${projectId} not found`);
13622
13730
  }
@@ -13636,8 +13744,8 @@ var JobRunner = class {
13636
13744
  throw new Error("No providers configured. Add at least one provider API key.");
13637
13745
  }
13638
13746
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
13639
- projectKeywords = this.db.select().from(keywords).where(eq19(keywords.projectId, projectId)).all();
13640
- const projectCompetitors = this.db.select().from(competitors).where(eq19(competitors.projectId, projectId)).all();
13747
+ projectKeywords = this.db.select().from(keywords).where(eq18(keywords.projectId, projectId)).all();
13748
+ const projectCompetitors = this.db.select().from(competitors).where(eq18(competitors.projectId, projectId)).all();
13641
13749
  const competitorDomains = projectCompetitors.map((c) => c.domain);
13642
13750
  const allDomains = effectiveDomains({
13643
13751
  canonicalDomain: project.canonicalDomain,
@@ -13653,7 +13761,7 @@ var JobRunner = class {
13653
13761
  const todayPeriod = getCurrentUsageDay();
13654
13762
  for (const p of activeProviders) {
13655
13763
  const providerScope = `${projectId}:${p.adapter.name}`;
13656
- 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);
13764
+ 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);
13657
13765
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
13658
13766
  if (providerUsage + queriesPerProvider > limit) {
13659
13767
  throw new Error(
@@ -13713,7 +13821,7 @@ var JobRunner = class {
13713
13821
  );
13714
13822
  let screenshotRelPath = null;
13715
13823
  if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
13716
- const snapshotId = crypto19.randomUUID();
13824
+ const snapshotId = crypto18.randomUUID();
13717
13825
  const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
13718
13826
  if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
13719
13827
  const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
@@ -13743,7 +13851,7 @@ var JobRunner = class {
13743
13851
  }).run();
13744
13852
  } else {
13745
13853
  this.db.insert(querySnapshots).values({
13746
- id: crypto19.randomUUID(),
13854
+ id: crypto18.randomUUID(),
13747
13855
  runId,
13748
13856
  keywordId: kw.id,
13749
13857
  provider: providerName,
@@ -13794,12 +13902,12 @@ var JobRunner = class {
13794
13902
  const someFailed = providerErrors.size > 0;
13795
13903
  if (allFailed) {
13796
13904
  const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
13797
- this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq19(runs.id, runId)).run();
13905
+ this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
13798
13906
  } else if (someFailed) {
13799
13907
  const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
13800
- this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq19(runs.id, runId)).run();
13908
+ this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
13801
13909
  } else {
13802
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13910
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
13803
13911
  }
13804
13912
  this.flushProviderUsage(projectId, providerDispatchCounts);
13805
13913
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -13834,7 +13942,7 @@ var JobRunner = class {
13834
13942
  status: "failed",
13835
13943
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13836
13944
  error: errorMessage
13837
- }).where(eq19(runs.id, runId)).run();
13945
+ }).where(eq18(runs.id, runId)).run();
13838
13946
  this.flushProviderUsage(projectId, providerDispatchCounts);
13839
13947
  trackEvent("run.completed", {
13840
13948
  status: "failed",
@@ -13855,7 +13963,7 @@ var JobRunner = class {
13855
13963
  const now = (/* @__PURE__ */ new Date()).toISOString();
13856
13964
  const period = now.slice(0, 10);
13857
13965
  this.db.insert(usageCounters).values({
13858
- id: crypto19.randomUUID(),
13966
+ id: crypto18.randomUUID(),
13859
13967
  scope,
13860
13968
  period,
13861
13969
  metric,
@@ -13877,7 +13985,7 @@ var JobRunner = class {
13877
13985
  status: runs.status,
13878
13986
  finishedAt: runs.finishedAt,
13879
13987
  error: runs.error
13880
- }).from(runs).where(eq19(runs.id, runId)).get();
13988
+ }).from(runs).where(eq18(runs.id, runId)).get();
13881
13989
  }
13882
13990
  isRunCancelled(runId) {
13883
13991
  return this.getRunState(runId)?.status === "cancelled";
@@ -13893,7 +14001,7 @@ var JobRunner = class {
13893
14001
  this.db.update(runs).set({
13894
14002
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13895
14003
  error: currentRun.error ?? "Cancelled by user"
13896
- }).where(eq19(runs.id, runId)).run();
14004
+ }).where(eq18(runs.id, runId)).run();
13897
14005
  }
13898
14006
  trackEvent("run.completed", {
13899
14007
  status: "cancelled",
@@ -13915,8 +14023,8 @@ function getCurrentUsageDay() {
13915
14023
  }
13916
14024
 
13917
14025
  // src/gsc-sync.ts
13918
- import crypto20 from "crypto";
13919
- import { eq as eq20, and as and8, sql as sql5 } from "drizzle-orm";
14026
+ import crypto19 from "crypto";
14027
+ import { eq as eq19, and as and8, sql as sql5 } from "drizzle-orm";
13920
14028
  var log2 = createLogger("GscSync");
13921
14029
  function formatDate2(d) {
13922
14030
  return d.toISOString().split("T")[0];
@@ -13928,13 +14036,13 @@ function daysAgo(n) {
13928
14036
  }
13929
14037
  async function executeGscSync(db, runId, projectId, opts) {
13930
14038
  const now = (/* @__PURE__ */ new Date()).toISOString();
13931
- db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
14039
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq19(runs.id, runId)).run();
13932
14040
  try {
13933
14041
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
13934
14042
  if (!googleClientId || !googleClientSecret) {
13935
14043
  throw new Error("Google OAuth is not configured in the local Canonry config");
13936
14044
  }
13937
- const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
14045
+ const project = db.select().from(projects).where(eq19(projects.id, projectId)).get();
13938
14046
  if (!project) {
13939
14047
  throw new Error(`Project not found: ${projectId}`);
13940
14048
  }
@@ -13969,7 +14077,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13969
14077
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
13970
14078
  db.delete(gscSearchData).where(
13971
14079
  and8(
13972
- eq20(gscSearchData.projectId, projectId),
14080
+ eq19(gscSearchData.projectId, projectId),
13973
14081
  sql5`${gscSearchData.date} >= ${startDate}`,
13974
14082
  sql5`${gscSearchData.date} <= ${endDate}`
13975
14083
  )
@@ -13981,7 +14089,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13981
14089
  for (const row of batch) {
13982
14090
  const [query, page, country, device, date] = row.keys;
13983
14091
  db.insert(gscSearchData).values({
13984
- id: crypto20.randomUUID(),
14092
+ id: crypto19.randomUUID(),
13985
14093
  projectId,
13986
14094
  syncRunId: runId,
13987
14095
  date: date ?? "",
@@ -14015,7 +14123,7 @@ async function executeGscSync(db, runId, projectId, opts) {
14015
14123
  const rich = ir.richResultsResult;
14016
14124
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
14017
14125
  db.insert(gscUrlInspections).values({
14018
- id: crypto20.randomUUID(),
14126
+ id: crypto19.randomUUID(),
14019
14127
  projectId,
14020
14128
  syncRunId: runId,
14021
14129
  url: pageUrl,
@@ -14036,7 +14144,7 @@ async function executeGscSync(db, runId, projectId, opts) {
14036
14144
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
14037
14145
  }
14038
14146
  }
14039
- const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
14147
+ const allInspections = db.select().from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).all();
14040
14148
  const latestByUrl = /* @__PURE__ */ new Map();
14041
14149
  for (const row of allInspections) {
14042
14150
  const existing = latestByUrl.get(row.url);
@@ -14057,9 +14165,9 @@ async function executeGscSync(db, runId, projectId, opts) {
14057
14165
  }
14058
14166
  }
14059
14167
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
14060
- db.delete(gscCoverageSnapshots).where(and8(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
14168
+ db.delete(gscCoverageSnapshots).where(and8(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
14061
14169
  db.insert(gscCoverageSnapshots).values({
14062
- id: crypto20.randomUUID(),
14170
+ id: crypto19.randomUUID(),
14063
14171
  projectId,
14064
14172
  syncRunId: runId,
14065
14173
  date: snapshotDate,
@@ -14068,19 +14176,19 @@ async function executeGscSync(db, runId, projectId, opts) {
14068
14176
  reasonBreakdown: JSON.stringify(reasonCounts),
14069
14177
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14070
14178
  }).run();
14071
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14179
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
14072
14180
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
14073
14181
  } catch (err) {
14074
14182
  const errorMsg = err instanceof Error ? err.message : String(err);
14075
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14183
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
14076
14184
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
14077
14185
  throw err;
14078
14186
  }
14079
14187
  }
14080
14188
 
14081
14189
  // src/gsc-inspect-sitemap.ts
14082
- import crypto21 from "crypto";
14083
- import { eq as eq21, and as and9 } from "drizzle-orm";
14190
+ import crypto20 from "crypto";
14191
+ import { eq as eq20, and as and9 } from "drizzle-orm";
14084
14192
 
14085
14193
  // src/sitemap-parser.ts
14086
14194
  var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
@@ -14149,13 +14257,13 @@ async function parseSitemapRecursive(url, urls, depth) {
14149
14257
  var log3 = createLogger("InspectSitemap");
14150
14258
  async function executeInspectSitemap(db, runId, projectId, opts) {
14151
14259
  const now = (/* @__PURE__ */ new Date()).toISOString();
14152
- db.update(runs).set({ status: "running", startedAt: now }).where(eq21(runs.id, runId)).run();
14260
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
14153
14261
  try {
14154
14262
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
14155
14263
  if (!googleClientId || !googleClientSecret) {
14156
14264
  throw new Error("Google OAuth is not configured in the local Canonry config");
14157
14265
  }
14158
- const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
14266
+ const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
14159
14267
  if (!project) {
14160
14268
  throw new Error(`Project not found: ${projectId}`);
14161
14269
  }
@@ -14196,7 +14304,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14196
14304
  const rich = ir.richResultsResult;
14197
14305
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
14198
14306
  db.insert(gscUrlInspections).values({
14199
- id: crypto21.randomUUID(),
14307
+ id: crypto20.randomUUID(),
14200
14308
  projectId,
14201
14309
  syncRunId: runId,
14202
14310
  url: pageUrl,
@@ -14223,7 +14331,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14223
14331
  await new Promise((r) => setTimeout(r, 1e3));
14224
14332
  }
14225
14333
  }
14226
- const allInspections = db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, projectId)).all();
14334
+ const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
14227
14335
  const latestByUrl = /* @__PURE__ */ new Map();
14228
14336
  for (const row of allInspections) {
14229
14337
  const existing = latestByUrl.get(row.url);
@@ -14244,9 +14352,9 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14244
14352
  }
14245
14353
  }
14246
14354
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
14247
- db.delete(gscCoverageSnapshots).where(and9(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
14355
+ db.delete(gscCoverageSnapshots).where(and9(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
14248
14356
  db.insert(gscCoverageSnapshots).values({
14249
- id: crypto21.randomUUID(),
14357
+ id: crypto20.randomUUID(),
14250
14358
  projectId,
14251
14359
  syncRunId: runId,
14252
14360
  date: snapshotDate,
@@ -14256,11 +14364,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14256
14364
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14257
14365
  }).run();
14258
14366
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
14259
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
14367
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14260
14368
  log3.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
14261
14369
  } catch (err) {
14262
14370
  const errorMsg = err instanceof Error ? err.message : String(err);
14263
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
14371
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14264
14372
  log3.error("inspect.failed", { runId, projectId, error: errorMsg });
14265
14373
  throw err;
14266
14374
  }
@@ -14319,7 +14427,7 @@ var ProviderRegistry = class {
14319
14427
 
14320
14428
  // src/scheduler.ts
14321
14429
  import cron from "node-cron";
14322
- import { eq as eq22 } from "drizzle-orm";
14430
+ import { eq as eq21 } from "drizzle-orm";
14323
14431
  var log4 = createLogger("Scheduler");
14324
14432
  var Scheduler = class {
14325
14433
  db;
@@ -14331,7 +14439,7 @@ var Scheduler = class {
14331
14439
  }
14332
14440
  /** Load all enabled schedules from DB and register cron jobs. */
14333
14441
  start() {
14334
- const allSchedules = this.db.select().from(schedules).where(eq22(schedules.enabled, 1)).all();
14442
+ const allSchedules = this.db.select().from(schedules).where(eq21(schedules.enabled, 1)).all();
14335
14443
  for (const schedule of allSchedules) {
14336
14444
  const missedRunAt = schedule.nextRunAt;
14337
14445
  this.registerCronTask(schedule);
@@ -14356,7 +14464,7 @@ var Scheduler = class {
14356
14464
  this.stopTask(projectId, existing, "Stopped");
14357
14465
  this.tasks.delete(projectId);
14358
14466
  }
14359
- const schedule = this.db.select().from(schedules).where(eq22(schedules.projectId, projectId)).get();
14467
+ const schedule = this.db.select().from(schedules).where(eq21(schedules.projectId, projectId)).get();
14360
14468
  if (schedule && schedule.enabled === 1) {
14361
14469
  this.registerCronTask(schedule);
14362
14470
  }
@@ -14389,14 +14497,14 @@ var Scheduler = class {
14389
14497
  this.db.update(schedules).set({
14390
14498
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
14391
14499
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14392
- }).where(eq22(schedules.id, scheduleId)).run();
14500
+ }).where(eq21(schedules.id, scheduleId)).run();
14393
14501
  const label = schedule.preset ?? cronExpr;
14394
14502
  log4.info("cron.registered", { projectId, schedule: label, timezone });
14395
14503
  }
14396
14504
  triggerRun(scheduleId, projectId) {
14397
14505
  try {
14398
14506
  const now = (/* @__PURE__ */ new Date()).toISOString();
14399
- const currentSchedule = this.db.select().from(schedules).where(eq22(schedules.id, scheduleId)).get();
14507
+ const currentSchedule = this.db.select().from(schedules).where(eq21(schedules.id, scheduleId)).get();
14400
14508
  if (!currentSchedule || currentSchedule.enabled !== 1) {
14401
14509
  log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
14402
14510
  this.remove(projectId);
@@ -14404,7 +14512,7 @@ var Scheduler = class {
14404
14512
  }
14405
14513
  const task = this.tasks.get(projectId);
14406
14514
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
14407
- const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
14515
+ const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
14408
14516
  if (!project) {
14409
14517
  log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
14410
14518
  this.remove(projectId);
@@ -14433,7 +14541,7 @@ var Scheduler = class {
14433
14541
  this.db.update(schedules).set({
14434
14542
  nextRunAt,
14435
14543
  updatedAt: now
14436
- }).where(eq22(schedules.id, currentSchedule.id)).run();
14544
+ }).where(eq21(schedules.id, currentSchedule.id)).run();
14437
14545
  return;
14438
14546
  }
14439
14547
  const runId = queueResult.runId;
@@ -14441,7 +14549,7 @@ var Scheduler = class {
14441
14549
  lastRunAt: now,
14442
14550
  nextRunAt,
14443
14551
  updatedAt: now
14444
- }).where(eq22(schedules.id, currentSchedule.id)).run();
14552
+ }).where(eq21(schedules.id, currentSchedule.id)).run();
14445
14553
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
14446
14554
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
14447
14555
  log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -14453,8 +14561,8 @@ var Scheduler = class {
14453
14561
  };
14454
14562
 
14455
14563
  // src/notifier.ts
14456
- import { eq as eq23, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
14457
- import crypto22 from "crypto";
14564
+ import { eq as eq22, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
14565
+ import crypto21 from "crypto";
14458
14566
  var log5 = createLogger("Notifier");
14459
14567
  var Notifier = class {
14460
14568
  db;
@@ -14466,18 +14574,18 @@ var Notifier = class {
14466
14574
  /** Called after a run completes (success, partial, or failed). */
14467
14575
  async onRunCompleted(runId, projectId) {
14468
14576
  log5.info("run.completed", { runId, projectId });
14469
- const notifs = this.db.select().from(notifications).where(eq23(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14577
+ const notifs = this.db.select().from(notifications).where(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14470
14578
  if (notifs.length === 0) {
14471
14579
  log5.info("notifications.none-enabled", { projectId });
14472
14580
  return;
14473
14581
  }
14474
14582
  log5.info("notifications.found", { projectId, count: notifs.length });
14475
- const run = this.db.select().from(runs).where(eq23(runs.id, runId)).get();
14583
+ const run = this.db.select().from(runs).where(eq22(runs.id, runId)).get();
14476
14584
  if (!run) {
14477
14585
  log5.error("run.not-found", { runId, msg: "skipping notification dispatch" });
14478
14586
  return;
14479
14587
  }
14480
- const project = this.db.select().from(projects).where(eq23(projects.id, projectId)).get();
14588
+ const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
14481
14589
  if (!project) {
14482
14590
  log5.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
14483
14591
  return;
@@ -14524,11 +14632,11 @@ var Notifier = class {
14524
14632
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
14525
14633
  if (highInsights.length > 0) insightEvents.push("insight.high");
14526
14634
  if (insightEvents.length === 0) return;
14527
- const notifs = this.db.select().from(notifications).where(eq23(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14635
+ const notifs = this.db.select().from(notifications).where(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14528
14636
  if (notifs.length === 0) return;
14529
- const run = this.db.select().from(runs).where(eq23(runs.id, runId)).get();
14637
+ const run = this.db.select().from(runs).where(eq22(runs.id, runId)).get();
14530
14638
  if (!run) return;
14531
- const project = this.db.select().from(projects).where(eq23(projects.id, projectId)).get();
14639
+ const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
14532
14640
  if (!project) return;
14533
14641
  for (const notif of notifs) {
14534
14642
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -14560,8 +14668,8 @@ var Notifier = class {
14560
14668
  computeTransitions(runId, projectId) {
14561
14669
  const recentRuns = this.db.select().from(runs).where(
14562
14670
  and10(
14563
- eq23(runs.projectId, projectId),
14564
- or2(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
14671
+ eq22(runs.projectId, projectId),
14672
+ or2(eq22(runs.status, "completed"), eq22(runs.status, "partial"))
14565
14673
  )
14566
14674
  ).orderBy(desc8(runs.createdAt)).limit(2).all();
14567
14675
  if (recentRuns.length < 2) return [];
@@ -14573,12 +14681,12 @@ var Notifier = class {
14573
14681
  keyword: keywords.keyword,
14574
14682
  provider: querySnapshots.provider,
14575
14683
  citationState: querySnapshots.citationState
14576
- }).from(querySnapshots).leftJoin(keywords, eq23(querySnapshots.keywordId, keywords.id)).where(eq23(querySnapshots.runId, currentRunId)).all();
14684
+ }).from(querySnapshots).leftJoin(keywords, eq22(querySnapshots.keywordId, keywords.id)).where(eq22(querySnapshots.runId, currentRunId)).all();
14577
14685
  const previousSnapshots = this.db.select({
14578
14686
  keywordId: querySnapshots.keywordId,
14579
14687
  provider: querySnapshots.provider,
14580
14688
  citationState: querySnapshots.citationState
14581
- }).from(querySnapshots).where(eq23(querySnapshots.runId, previousRunId)).all();
14689
+ }).from(querySnapshots).where(eq22(querySnapshots.runId, previousRunId)).all();
14582
14690
  const prevMap = /* @__PURE__ */ new Map();
14583
14691
  for (const s of previousSnapshots) {
14584
14692
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -14636,7 +14744,7 @@ var Notifier = class {
14636
14744
  }
14637
14745
  logDelivery(projectId, notificationId, event, status, error) {
14638
14746
  this.db.insert(auditLog).values({
14639
- id: crypto22.randomUUID(),
14747
+ id: crypto21.randomUUID(),
14640
14748
  projectId,
14641
14749
  actor: "scheduler",
14642
14750
  action: `notification.${status}`,
@@ -14651,19 +14759,23 @@ var Notifier = class {
14651
14759
  // src/run-coordinator.ts
14652
14760
  var log6 = createLogger("RunCoordinator");
14653
14761
  var RunCoordinator = class {
14654
- constructor(notifier, intelligenceService, onInsightsGenerated) {
14762
+ constructor(notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
14655
14763
  this.notifier = notifier;
14656
14764
  this.intelligenceService = intelligenceService;
14657
14765
  this.onInsightsGenerated = onInsightsGenerated;
14766
+ this.onAeroEvent = onAeroEvent;
14658
14767
  }
14659
14768
  async onRunCompleted(runId, projectId) {
14769
+ let insightCount = 0;
14770
+ let criticalOrHigh = 0;
14660
14771
  try {
14661
14772
  const result = this.intelligenceService.analyzeAndPersist(runId, projectId);
14662
- if (result && this.onInsightsGenerated) {
14663
- const hasHighSeverity = result.insights.some(
14773
+ if (result) {
14774
+ insightCount = result.insights.length;
14775
+ criticalOrHigh = result.insights.filter(
14664
14776
  (i) => i.severity === "critical" || i.severity === "high"
14665
- );
14666
- if (hasHighSeverity) {
14777
+ ).length;
14778
+ if (this.onInsightsGenerated && criticalOrHigh > 0) {
14667
14779
  try {
14668
14780
  await this.onInsightsGenerated(runId, projectId, result);
14669
14781
  } catch (err) {
@@ -14679,141 +14791,2202 @@ var RunCoordinator = class {
14679
14791
  } catch (err) {
14680
14792
  log6.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
14681
14793
  }
14794
+ if (this.onAeroEvent) {
14795
+ try {
14796
+ await this.onAeroEvent({ runId, projectId, insightCount, criticalOrHigh });
14797
+ } catch (err) {
14798
+ log6.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
14799
+ }
14800
+ }
14682
14801
  }
14683
14802
  };
14684
14803
 
14685
- // src/snapshot-service.ts
14686
- import { runAeoAudit } from "@ainyc/aeo-audit";
14804
+ // src/agent/session-registry.ts
14805
+ import crypto23 from "crypto";
14806
+ import { eq as eq24 } from "drizzle-orm";
14687
14807
 
14688
- // src/site-fetch.ts
14689
- import https2 from "https";
14690
- var FETCH_TIMEOUT_MS = 1e4;
14691
- var MAX_TEXT_LENGTH = 4e3;
14692
- var MAX_BODY_BYTES = 512e3;
14693
- var USER_AGENT = "Canonry/1.0 (site-analysis)";
14694
- function extractHostname(domain) {
14695
- let hostname = domain;
14696
- try {
14697
- if (hostname.includes("://")) {
14698
- hostname = new URL(hostname).hostname;
14699
- }
14700
- } catch {
14808
+ // src/agent/session.ts
14809
+ import fs7 from "fs";
14810
+ import path8 from "path";
14811
+ import { Agent } from "@mariozechner/pi-agent-core";
14812
+ import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
14813
+
14814
+ // src/agent/providers.ts
14815
+ import { getEnvApiKey, getModel } from "@mariozechner/pi-ai";
14816
+ var AGENT_PROVIDERS = {
14817
+ [AgentProviderIds.claude]: {
14818
+ piAiProvider: "anthropic",
14819
+ label: "Anthropic (Claude)",
14820
+ defaultModel: "claude-opus-4-7",
14821
+ autoDetectPriority: 0
14822
+ },
14823
+ [AgentProviderIds.openai]: {
14824
+ piAiProvider: "openai",
14825
+ label: "OpenAI",
14826
+ defaultModel: "gpt-5.1",
14827
+ autoDetectPriority: 1
14828
+ },
14829
+ [AgentProviderIds.gemini]: {
14830
+ piAiProvider: "google",
14831
+ label: "Google (Gemini)",
14832
+ defaultModel: "gemini-2.5-flash",
14833
+ autoDetectPriority: 2
14834
+ },
14835
+ [AgentProviderIds.zai]: {
14836
+ piAiProvider: "zai",
14837
+ label: "Z.ai (GLM)",
14838
+ defaultModel: "glm-5.1",
14839
+ autoDetectPriority: 3
14701
14840
  }
14702
- return hostname.replace(/^www\./, "");
14841
+ };
14842
+ function agentProvidersByPriority() {
14843
+ return Object.keys(AGENT_PROVIDERS).slice().sort((a, b) => AGENT_PROVIDERS[a].autoDetectPriority - AGENT_PROVIDERS[b].autoDetectPriority);
14703
14844
  }
14704
- function fetchWithPinnedAddress(target) {
14705
- return new Promise((resolve) => {
14706
- const port = target.url.port ? Number(target.url.port) : 443;
14707
- const path9 = target.url.pathname + target.url.search;
14708
- const req = https2.request(
14709
- {
14710
- hostname: target.address,
14711
- family: target.family,
14712
- port,
14713
- path: path9,
14714
- method: "GET",
14715
- timeout: FETCH_TIMEOUT_MS,
14716
- servername: target.url.hostname,
14717
- // SNI for TLS
14718
- headers: {
14719
- Host: target.url.host,
14720
- "User-Agent": USER_AGENT,
14721
- Accept: "text/html"
14722
- }
14723
- },
14724
- (res) => {
14725
- if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
14726
- if (res.statusCode >= 300 && res.statusCode < 400) {
14727
- const location = res.headers.location ?? "";
14728
- res.resume();
14729
- resolve(`REDIRECT:${location}`);
14730
- return;
14731
- }
14732
- res.resume();
14733
- resolve("");
14734
- return;
14735
- }
14736
- const contentType = res.headers["content-type"] ?? "";
14737
- if (!contentType.includes("text/html")) {
14738
- res.resume();
14739
- resolve("");
14740
- return;
14741
- }
14742
- const chunks = [];
14743
- let totalBytes = 0;
14744
- res.on("data", (chunk) => {
14745
- totalBytes += chunk.length;
14746
- if (totalBytes <= MAX_BODY_BYTES) {
14747
- chunks.push(chunk);
14748
- }
14749
- });
14750
- res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
14751
- res.on("error", () => resolve(""));
14752
- }
14845
+ function listAgentProviders() {
14846
+ return AGENT_PROVIDER_IDS;
14847
+ }
14848
+ function getAgentProvider(name) {
14849
+ return AGENT_PROVIDERS[name];
14850
+ }
14851
+ function coerceAgentProvider(value) {
14852
+ if (!value) return void 0;
14853
+ return isAgentProviderId(value) ? value : void 0;
14854
+ }
14855
+ function resolveModelForProvider(provider, modelId) {
14856
+ const entry = AGENT_PROVIDERS[provider];
14857
+ const id = modelId ?? entry.defaultModel;
14858
+ const model = getModel(entry.piAiProvider, id);
14859
+ if (!model) {
14860
+ throw new Error(
14861
+ `Model '${id}' not found for pi-ai provider '${entry.piAiProvider}'. Verify AGENT_PROVIDERS[${provider}].defaultModel against the installed @mariozechner/pi-ai catalog.`
14753
14862
  );
14754
- req.on("timeout", () => req.destroy(new Error("timeout")));
14755
- req.on("error", () => resolve(""));
14756
- req.end();
14757
- });
14863
+ }
14864
+ return model;
14758
14865
  }
14759
- async function fetchSiteText(domain) {
14760
- const hostname = extractHostname(domain);
14761
- const url = `https://${hostname}`;
14762
- const targetCheck = await resolveWebhookTarget(url);
14763
- if (!targetCheck.ok) return "";
14764
- try {
14765
- const result = await fetchWithPinnedAddress(targetCheck.target);
14766
- if (result.startsWith("REDIRECT:")) {
14767
- const location = result.slice("REDIRECT:".length);
14768
- if (!location) return "";
14769
- const redirectUrl = new URL(location, url).href;
14770
- const redirectCheck = await resolveWebhookTarget(redirectUrl);
14771
- if (!redirectCheck.ok) return "";
14772
- const redirectResult = await fetchWithPinnedAddress(redirectCheck.target);
14773
- if (redirectResult.startsWith("REDIRECT:")) return "";
14774
- return stripHtml2(redirectResult);
14775
- }
14776
- return stripHtml2(result);
14777
- } catch {
14778
- return "";
14866
+ function validateAgentProviderRegistry() {
14867
+ for (const provider of listAgentProviders()) {
14868
+ resolveModelForProvider(provider);
14779
14869
  }
14780
14870
  }
14781
- function stripHtml2(html) {
14782
- if (!html) return "";
14783
- let text = html.replace(/<script[\s\S]*?<\/script>/gi, " ");
14784
- text = text.replace(/<style[\s\S]*?<\/style>/gi, " ");
14785
- text = text.replace(/<[^>]+>/g, " ");
14786
- text = text.replace(/&amp;/g, "&");
14787
- text = text.replace(/&lt;/g, "<");
14788
- text = text.replace(/&gt;/g, ">");
14789
- text = text.replace(/&quot;/g, '"');
14790
- text = text.replace(/&#39;/g, "'");
14791
- text = text.replace(/&nbsp;/g, " ");
14792
- text = text.replace(/\s+/g, " ").trim();
14793
- if (text.length > MAX_TEXT_LENGTH) {
14794
- text = text.slice(0, MAX_TEXT_LENGTH);
14871
+ function resolveApiKeyFor(providerOrPiAi, config) {
14872
+ return resolveApiKeySource(providerOrPiAi, config)?.key;
14873
+ }
14874
+ function resolveApiKeySource(providerOrPiAi, config) {
14875
+ const id = resolveAgentId(providerOrPiAi);
14876
+ if (!id) return void 0;
14877
+ const entry = AGENT_PROVIDERS[id];
14878
+ const fromConfig = config.providers?.[id]?.apiKey;
14879
+ if (fromConfig) return { key: fromConfig, source: "config" };
14880
+ const fromEnv = getEnvApiKey(entry.piAiProvider);
14881
+ if (fromEnv) return { key: fromEnv, source: "env" };
14882
+ return void 0;
14883
+ }
14884
+ function resolveAgentId(providerOrPiAi) {
14885
+ if (isAgentProviderId(providerOrPiAi)) return providerOrPiAi;
14886
+ for (const id of AGENT_PROVIDER_IDS) {
14887
+ if (AGENT_PROVIDERS[id].piAiProvider === providerOrPiAi) return id;
14795
14888
  }
14796
- return text;
14889
+ return void 0;
14890
+ }
14891
+ function buildAgentProvidersResponse(config) {
14892
+ const providers = listAgentProviders().map((id) => {
14893
+ const entry = AGENT_PROVIDERS[id];
14894
+ const source = resolveApiKeySource(id, config);
14895
+ return {
14896
+ id,
14897
+ label: entry.label,
14898
+ defaultModel: entry.defaultModel,
14899
+ configured: source !== void 0,
14900
+ keySource: source?.source ?? null
14901
+ };
14902
+ });
14903
+ const firstConfigured = agentProvidersByPriority().find((p) => resolveApiKeySource(p, config));
14904
+ return {
14905
+ providers,
14906
+ defaultProvider: firstConfigured ?? null
14907
+ };
14797
14908
  }
14798
14909
 
14799
- // src/snapshot-format.ts
14800
- function formatAuditFactorScore(factor) {
14801
- return `${factor.score}/100 (${factor.weight}% weight)`;
14910
+ // src/agent/skill-paths.ts
14911
+ import fs5 from "fs";
14912
+ import path6 from "path";
14913
+ import { fileURLToPath } from "url";
14914
+ function resolveAeroSkillDir(pkgDir) {
14915
+ const here = pkgDir ?? path6.dirname(fileURLToPath(import.meta.url));
14916
+ const candidates = [
14917
+ path6.join(here, "../assets/agent-workspace/skills/aero"),
14918
+ path6.join(here, "../../assets/agent-workspace/skills/aero"),
14919
+ path6.join(here, "../../../../skills/aero")
14920
+ ];
14921
+ for (const candidate of candidates) {
14922
+ if (fs5.existsSync(path6.join(candidate, "SKILL.md"))) return candidate;
14923
+ }
14924
+ throw new Error(`Aero skill not found. Searched:
14925
+ ${candidates.join("\n ")}`);
14802
14926
  }
14803
14927
 
14804
- // src/snapshot-service.ts
14805
- var log7 = createLogger("Snapshot");
14806
- var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
14807
- var SNAPSHOT_QUERY_COUNT = 6;
14808
- var ProviderExecutionGate2 = class {
14809
- constructor(maxConcurrency, maxPerMinute) {
14810
- this.maxConcurrency = maxConcurrency;
14811
- this.maxPerMinute = maxPerMinute;
14928
+ // src/agent/skill-tools.ts
14929
+ import fs6 from "fs";
14930
+ import path7 from "path";
14931
+ import { Type } from "@sinclair/typebox";
14932
+ var MAX_DOC_CHARS = 2e4;
14933
+ function textResult(details) {
14934
+ return {
14935
+ content: [{ type: "text", text: JSON.stringify(details, null, 2) }],
14936
+ details
14937
+ };
14938
+ }
14939
+ function parseDescription(body) {
14940
+ if (!body.startsWith("---")) return "(no description)";
14941
+ const end = body.indexOf("\n---", 3);
14942
+ if (end === -1) return "(no description)";
14943
+ const block = body.slice(3, end);
14944
+ for (const line of block.split("\n")) {
14945
+ const match = line.match(/^description:\s*(.+)$/);
14946
+ if (match) return match[1].trim().replace(/^["']|["']$/g, "");
14947
+ }
14948
+ return "(no description)";
14949
+ }
14950
+ function scanSkillDocs(skillDir) {
14951
+ const refsDir = path7.join(skillDir ?? resolveAeroSkillDir(), "references");
14952
+ if (!fs6.existsSync(refsDir)) return [];
14953
+ const entries = [];
14954
+ for (const file of fs6.readdirSync(refsDir)) {
14955
+ if (!file.endsWith(".md")) continue;
14956
+ const filePath = path7.join(refsDir, file);
14957
+ const body = fs6.readFileSync(filePath, "utf-8");
14958
+ entries.push({
14959
+ slug: file.replace(/\.md$/, ""),
14960
+ description: parseDescription(body),
14961
+ bytes: Buffer.byteLength(body, "utf-8")
14962
+ });
14812
14963
  }
14813
- window = [];
14814
- waiters = [];
14815
- rateLimitChain = Promise.resolve();
14816
- inFlight = 0;
14964
+ entries.sort((a, b) => a.slug.localeCompare(b.slug));
14965
+ return entries;
14966
+ }
14967
+ var ListSchema = Type.Object({});
14968
+ function buildListSkillDocsTool() {
14969
+ return {
14970
+ name: "list_skill_docs",
14971
+ label: "List skill docs",
14972
+ description: "List reference playbooks bundled with the Aero skill. Each entry has a slug, a short description of when to use it, and byte size. Call this before read_skill_doc to pick the right doc.",
14973
+ parameters: ListSchema,
14974
+ execute: async () => {
14975
+ return textResult({ docs: scanSkillDocs() });
14976
+ }
14977
+ };
14978
+ }
14979
+ var ReadSchema = Type.Object({
14980
+ slug: Type.String({
14981
+ description: "Doc slug (no extension, no path). Must match a slug from list_skill_docs \u2014 unknown slugs return an error listing valid options."
14982
+ })
14983
+ });
14984
+ function buildReadSkillDocTool() {
14985
+ return {
14986
+ name: "read_skill_doc",
14987
+ label: "Read skill doc",
14988
+ description: 'Load the full content of a reference playbook by slug. Use when a task matches one of the docs returned by list_skill_docs \u2014 e.g. "regression-playbook" when investigating lost citations.',
14989
+ parameters: ReadSchema,
14990
+ execute: async (_toolCallId, params) => {
14991
+ const skillDir = resolveAeroSkillDir();
14992
+ const docs = scanSkillDocs(skillDir);
14993
+ const match = docs.find((d) => d.slug === params.slug);
14994
+ if (!match) {
14995
+ return textResult({
14996
+ error: `Unknown slug "${params.slug}".`,
14997
+ availableSlugs: docs.map((d) => d.slug)
14998
+ });
14999
+ }
15000
+ const filePath = path7.join(skillDir, "references", `${match.slug}.md`);
15001
+ const content = fs6.readFileSync(filePath, "utf-8");
15002
+ if (content.length > MAX_DOC_CHARS) {
15003
+ return textResult({
15004
+ slug: match.slug,
15005
+ content: content.slice(0, MAX_DOC_CHARS),
15006
+ truncated: true,
15007
+ totalBytes: match.bytes
15008
+ });
15009
+ }
15010
+ return textResult({ slug: match.slug, content, truncated: false });
15011
+ }
15012
+ };
15013
+ }
15014
+ function buildSkillDocTools() {
15015
+ return [
15016
+ buildListSkillDocsTool(),
15017
+ buildReadSkillDocTool()
15018
+ ];
15019
+ }
15020
+
15021
+ // src/agent/tools.ts
15022
+ import { Type as Type2 } from "@sinclair/typebox";
15023
+
15024
+ // src/agent/memory-store.ts
15025
+ import crypto22 from "crypto";
15026
+ import { and as and11, desc as desc9, eq as eq23, like, sql as sql6 } from "drizzle-orm";
15027
+ var COMPACTION_KEY_PREFIX = "compaction:";
15028
+ var COMPACTION_NOTES_PER_SESSION = 3;
15029
+ function rowToDto(row) {
15030
+ return {
15031
+ id: row.id,
15032
+ key: row.key,
15033
+ value: row.value,
15034
+ source: row.source,
15035
+ createdAt: row.createdAt,
15036
+ updatedAt: row.updatedAt
15037
+ };
15038
+ }
15039
+ function listMemoryEntries(db, projectId, opts = {}) {
15040
+ const query = db.select().from(agentMemory).where(eq23(agentMemory.projectId, projectId)).orderBy(desc9(agentMemory.updatedAt));
15041
+ const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
15042
+ return rows.map(rowToDto);
15043
+ }
15044
+ function upsertMemoryEntry(db, args) {
15045
+ if (Buffer.byteLength(args.value, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
15046
+ throw new Error(
15047
+ `memory value exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes (got ${Buffer.byteLength(args.value, "utf8")})`
15048
+ );
15049
+ }
15050
+ if (args.source !== MemorySources.compaction && args.key.startsWith(COMPACTION_KEY_PREFIX)) {
15051
+ throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
15052
+ }
15053
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15054
+ const id = crypto22.randomUUID();
15055
+ db.insert(agentMemory).values({
15056
+ id,
15057
+ projectId: args.projectId,
15058
+ key: args.key,
15059
+ value: args.value,
15060
+ source: args.source,
15061
+ createdAt: now,
15062
+ updatedAt: now
15063
+ }).onConflictDoUpdate({
15064
+ target: [agentMemory.projectId, agentMemory.key],
15065
+ set: {
15066
+ value: args.value,
15067
+ source: args.source,
15068
+ updatedAt: now
15069
+ }
15070
+ }).run();
15071
+ const row = db.select().from(agentMemory).where(and11(eq23(agentMemory.projectId, args.projectId), eq23(agentMemory.key, args.key))).get();
15072
+ if (!row) throw new Error("memory upsert produced no row");
15073
+ return rowToDto(row);
15074
+ }
15075
+ function deleteMemoryEntry(db, projectId, key) {
15076
+ const result = db.delete(agentMemory).where(and11(eq23(agentMemory.projectId, projectId), eq23(agentMemory.key, key))).run();
15077
+ const changes = result.changes ?? 0;
15078
+ return changes > 0;
15079
+ }
15080
+ function loadRecentForHydrate(db, projectId, limit) {
15081
+ return listMemoryEntries(db, projectId, { limit });
15082
+ }
15083
+ function writeCompactionNote(db, args) {
15084
+ if (Buffer.byteLength(args.summary, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
15085
+ throw new Error(
15086
+ `compaction summary exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes; summarizer produced too much text`
15087
+ );
15088
+ }
15089
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15090
+ const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
15091
+ const id = crypto22.randomUUID();
15092
+ let inserted;
15093
+ db.transaction((tx) => {
15094
+ tx.insert(agentMemory).values({
15095
+ id,
15096
+ projectId: args.projectId,
15097
+ key,
15098
+ value: args.summary,
15099
+ source: MemorySources.compaction,
15100
+ createdAt: now,
15101
+ updatedAt: now
15102
+ }).run();
15103
+ const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
15104
+ const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
15105
+ and11(
15106
+ eq23(agentMemory.projectId, args.projectId),
15107
+ like(agentMemory.key, `${sessionPrefix}%`)
15108
+ )
15109
+ ).orderBy(desc9(agentMemory.updatedAt)).all();
15110
+ const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
15111
+ if (stale.length > 0) {
15112
+ tx.delete(agentMemory).where(sql6`${agentMemory.id} IN (${sql6.join(stale.map((s) => sql6`${s}`), sql6`, `)})`).run();
15113
+ }
15114
+ const row = tx.select().from(agentMemory).where(and11(eq23(agentMemory.projectId, args.projectId), eq23(agentMemory.key, key))).get();
15115
+ if (row) inserted = rowToDto(row);
15116
+ });
15117
+ if (!inserted) throw new Error("compaction note write produced no row");
15118
+ return inserted;
15119
+ }
15120
+
15121
+ // src/agent/tools.ts
15122
+ var MAX_TOOL_RESULT_CHARS = 2e4;
15123
+ function truncate(json) {
15124
+ if (json.length <= MAX_TOOL_RESULT_CHARS) return json;
15125
+ return json.slice(0, MAX_TOOL_RESULT_CHARS) + "\n... (truncated \u2014 result too large)";
15126
+ }
15127
+ function textResult2(details) {
15128
+ return {
15129
+ content: [{ type: "text", text: truncate(JSON.stringify(details, null, 2)) }],
15130
+ details
15131
+ };
15132
+ }
15133
+ var StatusSchema = Type2.Object({
15134
+ runLimit: Type2.Optional(
15135
+ Type2.Number({
15136
+ description: "Max recent runs to include. Default 5.",
15137
+ minimum: 1,
15138
+ maximum: 50
15139
+ })
15140
+ )
15141
+ });
15142
+ function buildGetStatusTool(ctx) {
15143
+ return {
15144
+ name: "get_status",
15145
+ label: "Get status",
15146
+ description: "Current project overview with its most recent runs.",
15147
+ parameters: StatusSchema,
15148
+ execute: async (_toolCallId, params) => {
15149
+ const runLimit = params.runLimit ?? 5;
15150
+ const [project, runs2] = await Promise.all([
15151
+ ctx.client.getProject(ctx.projectName),
15152
+ ctx.client.listRuns(ctx.projectName, runLimit)
15153
+ ]);
15154
+ return textResult2({ project, runs: runs2 });
15155
+ }
15156
+ };
15157
+ }
15158
+ var HealthSchema = Type2.Object({});
15159
+ function buildGetHealthTool(ctx) {
15160
+ return {
15161
+ name: "get_health",
15162
+ label: "Get health",
15163
+ description: "Latest visibility health snapshot including overall cited rate, pair counts, and per-provider breakdown.",
15164
+ parameters: HealthSchema,
15165
+ execute: async () => {
15166
+ const health = await ctx.client.getHealth(ctx.projectName);
15167
+ return textResult2(health);
15168
+ }
15169
+ };
15170
+ }
15171
+ var TimelineSchema = Type2.Object({
15172
+ keyword: Type2.Optional(
15173
+ Type2.String({
15174
+ description: "Restrict the timeline to a single keyword. Omit to return all keywords."
15175
+ })
15176
+ )
15177
+ });
15178
+ function buildGetTimelineTool(ctx) {
15179
+ return {
15180
+ name: "get_timeline",
15181
+ label: "Get timeline",
15182
+ description: "Per-keyword citation timeline showing how visibility evolved across runs. Use to identify regressions, emerging citations, or competitor movement.",
15183
+ parameters: TimelineSchema,
15184
+ execute: async (_toolCallId, params) => {
15185
+ const timeline = await ctx.client.getTimeline(ctx.projectName);
15186
+ const filtered = params.keyword ? timeline.filter((row) => row.keyword === params.keyword) : timeline;
15187
+ return textResult2(filtered);
15188
+ }
15189
+ };
15190
+ }
15191
+ var InsightsSchema = Type2.Object({
15192
+ includeDismissed: Type2.Optional(
15193
+ Type2.Boolean({
15194
+ description: "Include dismissed insights. Default false (only active insights)."
15195
+ })
15196
+ ),
15197
+ runId: Type2.Optional(
15198
+ Type2.String({
15199
+ description: "Restrict insights to a specific run id. Omit for all runs."
15200
+ })
15201
+ )
15202
+ });
15203
+ function buildGetInsightsTool(ctx) {
15204
+ return {
15205
+ name: "get_insights",
15206
+ label: "Get insights",
15207
+ description: "Insights produced by the canonry intelligence engine \u2014 regressions, gains, and opportunities with cause/recommendation metadata. Query this before re-deriving conclusions from raw timeline data.",
15208
+ parameters: InsightsSchema,
15209
+ execute: async (_toolCallId, params) => {
15210
+ const insights2 = await ctx.client.getInsights(ctx.projectName, {
15211
+ dismissed: params.includeDismissed,
15212
+ runId: params.runId
15213
+ });
15214
+ return textResult2(insights2);
15215
+ }
15216
+ };
15217
+ }
15218
+ var KeywordsSchema = Type2.Object({});
15219
+ function buildListKeywordsTool(ctx) {
15220
+ return {
15221
+ name: "list_keywords",
15222
+ label: "List keywords",
15223
+ description: "All keywords currently tracked for this project.",
15224
+ parameters: KeywordsSchema,
15225
+ execute: async () => {
15226
+ const keywords2 = await ctx.client.listKeywords(ctx.projectName);
15227
+ return textResult2(keywords2);
15228
+ }
15229
+ };
15230
+ }
15231
+ var CompetitorsSchema = Type2.Object({});
15232
+ function buildListCompetitorsTool(ctx) {
15233
+ return {
15234
+ name: "list_competitors",
15235
+ label: "List competitors",
15236
+ description: "Competitor domains tracked alongside this project for side-by-side comparison.",
15237
+ parameters: CompetitorsSchema,
15238
+ execute: async () => {
15239
+ const competitors2 = await ctx.client.listCompetitors(ctx.projectName);
15240
+ return textResult2(competitors2);
15241
+ }
15242
+ };
15243
+ }
15244
+ var RunDetailSchema = Type2.Object({
15245
+ runId: Type2.String({
15246
+ description: "Run id (UUID) to fetch. Typically obtained from get_status runs[].id."
15247
+ })
15248
+ });
15249
+ function buildGetRunTool(ctx) {
15250
+ return {
15251
+ name: "get_run",
15252
+ label: "Get run detail",
15253
+ description: "Full detail for a specific run including per-keyword snapshots, error messages, and provider breakdown. Use to investigate failed runs or drill into a particular sweep.",
15254
+ parameters: RunDetailSchema,
15255
+ execute: async (_toolCallId, params) => {
15256
+ const run = await ctx.client.getRun(params.runId);
15257
+ return textResult2(run);
15258
+ }
15259
+ };
15260
+ }
15261
+ var RecallSchema = Type2.Object({
15262
+ limit: Type2.Optional(
15263
+ Type2.Number({
15264
+ description: "Max notes to return, ordered newest-first. Default 50. Max 100.",
15265
+ minimum: 1,
15266
+ maximum: 100
15267
+ })
15268
+ )
15269
+ });
15270
+ function buildRecallTool(ctx) {
15271
+ return {
15272
+ name: "recall",
15273
+ label: "Recall memory",
15274
+ description: "Read project-scoped durable notes Aero has stored via `remember` (plus compaction summaries). Returns entries newest-first. The N most-recent entries are also injected into the system prompt at session start, so you usually do not need to call this \u2014 reach for it when you need older context or the full note value.",
15275
+ parameters: RecallSchema,
15276
+ execute: async (_toolCallId, params) => {
15277
+ const entries = listMemoryEntries(ctx.db, ctx.projectId, { limit: params.limit ?? 50 });
15278
+ return textResult2({ entries });
15279
+ }
15280
+ };
15281
+ }
15282
+ function buildReadTools(ctx) {
15283
+ return [
15284
+ buildGetStatusTool(ctx),
15285
+ buildGetHealthTool(ctx),
15286
+ buildGetTimelineTool(ctx),
15287
+ buildGetInsightsTool(ctx),
15288
+ buildListKeywordsTool(ctx),
15289
+ buildListCompetitorsTool(ctx),
15290
+ buildGetRunTool(ctx),
15291
+ buildRecallTool(ctx)
15292
+ ];
15293
+ }
15294
+ var RunSweepSchema = Type2.Object({
15295
+ providers: Type2.Optional(
15296
+ Type2.Array(Type2.String(), {
15297
+ description: "Subset of providers to run. Omit to use every configured provider on the project."
15298
+ })
15299
+ ),
15300
+ noLocation: Type2.Optional(
15301
+ Type2.Boolean({
15302
+ description: "Run without a location context. Default: use the project default location."
15303
+ })
15304
+ )
15305
+ });
15306
+ function buildRunSweepTool(ctx) {
15307
+ return {
15308
+ name: "run_sweep",
15309
+ label: "Trigger sweep",
15310
+ description: "Trigger a new answer-visibility sweep for this project across configured AI providers. Returns the run id(s). Use when fresh citation data is needed.",
15311
+ parameters: RunSweepSchema,
15312
+ execute: async (_toolCallId, params) => {
15313
+ const body = {};
15314
+ if (params.providers?.length) body.providers = params.providers;
15315
+ if (params.noLocation) body.noLocation = true;
15316
+ const result = await ctx.client.triggerRun(ctx.projectName, body);
15317
+ return textResult2(result);
15318
+ }
15319
+ };
15320
+ }
15321
+ var DismissInsightSchema = Type2.Object({
15322
+ insightId: Type2.String({
15323
+ description: "Insight id to dismiss. Obtain from get_insights details[].id."
15324
+ })
15325
+ });
15326
+ function buildDismissInsightTool(ctx) {
15327
+ return {
15328
+ name: "dismiss_insight",
15329
+ label: "Dismiss insight",
15330
+ description: "Mark an insight as dismissed so it no longer surfaces in active insight lists. Reversible via the dashboard.",
15331
+ parameters: DismissInsightSchema,
15332
+ execute: async (_toolCallId, params) => {
15333
+ const result = await ctx.client.dismissInsight(ctx.projectName, params.insightId);
15334
+ return textResult2(result);
15335
+ }
15336
+ };
15337
+ }
15338
+ var AddKeywordsSchema = Type2.Object({
15339
+ keywords: Type2.Array(Type2.String(), {
15340
+ minItems: 1,
15341
+ description: "Keywords to add to the tracking list. Duplicates against existing keywords are ignored server-side."
15342
+ })
15343
+ });
15344
+ function buildAddKeywordsTool(ctx) {
15345
+ return {
15346
+ name: "add_keywords",
15347
+ label: "Add keywords",
15348
+ description: "Append keywords to the project tracking list. Additive only \u2014 existing keywords are preserved. Use exact phrasing you want tracked.",
15349
+ parameters: AddKeywordsSchema,
15350
+ execute: async (_toolCallId, params) => {
15351
+ await ctx.client.appendKeywords(ctx.projectName, params.keywords);
15352
+ return textResult2({ added: params.keywords });
15353
+ }
15354
+ };
15355
+ }
15356
+ var AddCompetitorsSchema = Type2.Object({
15357
+ domains: Type2.Array(Type2.String(), {
15358
+ minItems: 1,
15359
+ description: 'Competitor domains to track. Provide bare domains (e.g. "example.com"), not URLs.'
15360
+ })
15361
+ });
15362
+ function buildAddCompetitorsTool(ctx) {
15363
+ return {
15364
+ name: "add_competitors",
15365
+ label: "Add competitors",
15366
+ description: "Append competitor domains to the project. Fetches the current set, merges with the requested domains (dedup on exact domain match), and persists the combined list.",
15367
+ parameters: AddCompetitorsSchema,
15368
+ execute: async (_toolCallId, params) => {
15369
+ const existing = await ctx.client.listCompetitors(ctx.projectName);
15370
+ const existingDomains = new Set(existing.map((c) => c.domain));
15371
+ const newDomains = params.domains.filter((d) => !existingDomains.has(d));
15372
+ if (newDomains.length === 0) {
15373
+ return textResult2({ added: [], alreadyTracked: params.domains });
15374
+ }
15375
+ const merged = [...existing.map((c) => c.domain), ...newDomains];
15376
+ await ctx.client.putCompetitors(ctx.projectName, merged);
15377
+ return textResult2({ added: newDomains, alreadyTracked: params.domains.filter((d) => existingDomains.has(d)) });
15378
+ }
15379
+ };
15380
+ }
15381
+ var UpdateScheduleSchema = Type2.Object({
15382
+ cron: Type2.Optional(
15383
+ Type2.String({ description: 'Cron expression (e.g. "0 */6 * * *"). Provide cron OR preset, not both.' })
15384
+ ),
15385
+ preset: Type2.Optional(
15386
+ Type2.String({ description: 'Preset keyword (e.g. "daily", "hourly"). Provide cron OR preset, not both.' })
15387
+ ),
15388
+ timezone: Type2.Optional(Type2.String({ description: 'IANA timezone. Default: "UTC".' })),
15389
+ enabled: Type2.Optional(
15390
+ Type2.Boolean({ description: "Whether the schedule is active. Default: true." })
15391
+ ),
15392
+ providers: Type2.Optional(
15393
+ Type2.Array(Type2.String(), {
15394
+ description: "Providers to run on each scheduled sweep. Omit to use all configured providers."
15395
+ })
15396
+ )
15397
+ });
15398
+ function buildUpdateScheduleTool(ctx) {
15399
+ return {
15400
+ name: "update_schedule",
15401
+ label: "Update schedule",
15402
+ description: "Create or update the recurring sweep schedule for this project. Provide exactly one of `cron` (expression) or `preset` (keyword). Fully replaces any existing schedule.",
15403
+ parameters: UpdateScheduleSchema,
15404
+ execute: async (_toolCallId, params) => {
15405
+ if (params.cron && params.preset || !params.cron && !params.preset) {
15406
+ throw new Error("update_schedule: provide exactly one of `cron` or `preset`");
15407
+ }
15408
+ const body = {};
15409
+ if (params.cron) body.cron = params.cron;
15410
+ if (params.preset) body.preset = params.preset;
15411
+ if (params.timezone) body.timezone = params.timezone;
15412
+ if (params.enabled !== void 0) body.enabled = params.enabled;
15413
+ if (params.providers?.length) body.providers = params.providers;
15414
+ const result = await ctx.client.putSchedule(ctx.projectName, body);
15415
+ return textResult2(result);
15416
+ }
15417
+ };
15418
+ }
15419
+ var AttachAgentWebhookSchema = Type2.Object({
15420
+ url: Type2.String({
15421
+ description: "External agent webhook URL. Canonry will POST run.completed, insight.critical, insight.high, and citation.gained events to it."
15422
+ })
15423
+ });
15424
+ function buildAttachAgentWebhookTool(ctx) {
15425
+ return {
15426
+ name: "attach_agent_webhook",
15427
+ label: "Attach agent webhook",
15428
+ description: "Register an external agent webhook for this project. Use when wiring a Claude Code / Codex / custom agent to receive canonry run and insight events. Idempotent \u2014 skips if one already exists.",
15429
+ parameters: AttachAgentWebhookSchema,
15430
+ execute: async (_toolCallId, params) => {
15431
+ const existing = await ctx.client.listNotifications(ctx.projectName);
15432
+ const hasAgent = existing.some((n) => n.source === "agent");
15433
+ if (hasAgent) {
15434
+ return textResult2({ status: "already-attached" });
15435
+ }
15436
+ const result = await ctx.client.createNotification(ctx.projectName, {
15437
+ channel: "webhook",
15438
+ url: params.url,
15439
+ events: ["run.completed", "insight.critical", "insight.high", "citation.gained"],
15440
+ source: "agent"
15441
+ });
15442
+ return textResult2({ status: "attached", notificationId: result.id, url: params.url });
15443
+ }
15444
+ };
15445
+ }
15446
+ var RememberSchema = Type2.Object({
15447
+ key: Type2.String({
15448
+ description: `Stable identifier for this note (max ${AGENT_MEMORY_KEY_MAX_LENGTH} chars). Writing the same key overwrites the prior value. Do NOT use the "${COMPACTION_KEY_PREFIX}" prefix \u2014 that namespace is reserved for transcript compaction summaries.`,
15449
+ minLength: 1,
15450
+ maxLength: AGENT_MEMORY_KEY_MAX_LENGTH
15451
+ }),
15452
+ value: Type2.String({
15453
+ description: `Plain-text note to persist (max ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes). Use for durable operator preferences, migration context, or non-obvious reasoning you'll want on a future turn. Do NOT duplicate data canonry already tracks (runs, insights, timelines) \u2014 query those instead.`,
15454
+ minLength: 1
15455
+ })
15456
+ });
15457
+ function buildRememberTool(ctx) {
15458
+ return {
15459
+ name: "remember",
15460
+ label: "Remember",
15461
+ description: "Persist a project-scoped durable note visible to every future Aero session for this project. Upsert \u2014 writing the same key replaces the prior value. Capped at 2 KB per note.",
15462
+ parameters: RememberSchema,
15463
+ execute: async (_toolCallId, params) => {
15464
+ const entry = upsertMemoryEntry(ctx.db, {
15465
+ projectId: ctx.projectId,
15466
+ key: params.key,
15467
+ value: params.value,
15468
+ source: MemorySources.aero
15469
+ });
15470
+ return textResult2({ status: "remembered", entry });
15471
+ }
15472
+ };
15473
+ }
15474
+ var ForgetSchema = Type2.Object({
15475
+ key: Type2.String({
15476
+ description: "Exact key of the note to remove. No-op (status=missing) when no note exists for that key.",
15477
+ minLength: 1,
15478
+ maxLength: AGENT_MEMORY_KEY_MAX_LENGTH
15479
+ })
15480
+ });
15481
+ function buildForgetTool(ctx) {
15482
+ return {
15483
+ name: "forget",
15484
+ label: "Forget",
15485
+ description: "Delete a durable note by key. Use when a previously-remembered fact is wrong or no longer relevant.",
15486
+ parameters: ForgetSchema,
15487
+ execute: async (_toolCallId, params) => {
15488
+ if (params.key.startsWith(COMPACTION_KEY_PREFIX)) {
15489
+ throw new Error(
15490
+ `cannot forget compaction notes directly \u2014 they are pruned automatically (key prefix "${COMPACTION_KEY_PREFIX}" is reserved)`
15491
+ );
15492
+ }
15493
+ const removed = deleteMemoryEntry(ctx.db, ctx.projectId, params.key);
15494
+ return textResult2({ status: removed ? "forgotten" : "missing", key: params.key });
15495
+ }
15496
+ };
15497
+ }
15498
+ function buildWriteTools(ctx) {
15499
+ return [
15500
+ buildRunSweepTool(ctx),
15501
+ buildDismissInsightTool(ctx),
15502
+ buildAddKeywordsTool(ctx),
15503
+ buildAddCompetitorsTool(ctx),
15504
+ buildUpdateScheduleTool(ctx),
15505
+ buildAttachAgentWebhookTool(ctx),
15506
+ buildRememberTool(ctx),
15507
+ buildForgetTool(ctx)
15508
+ ];
15509
+ }
15510
+ function buildAllTools(ctx) {
15511
+ return [...buildReadTools(ctx), ...buildWriteTools(ctx)];
15512
+ }
15513
+
15514
+ // src/agent/session.ts
15515
+ var builtinsRegistered = false;
15516
+ function ensureBuiltinsRegistered() {
15517
+ if (!builtinsRegistered) {
15518
+ registerBuiltInApiProviders();
15519
+ validateAgentProviderRegistry();
15520
+ builtinsRegistered = true;
15521
+ }
15522
+ }
15523
+ function loadAeroSystemPrompt(pkgDir) {
15524
+ const skillDir = resolveAeroSkillDir(pkgDir);
15525
+ const skillBody = fs7.readFileSync(path8.join(skillDir, "SKILL.md"), "utf-8");
15526
+ const soulPath = path8.join(skillDir, "soul.md");
15527
+ if (!fs7.existsSync(soulPath)) return skillBody;
15528
+ const soulBody = fs7.readFileSync(soulPath, "utf-8");
15529
+ return `${soulBody.trimEnd()}
15530
+
15531
+ ---
15532
+
15533
+ ${skillBody}`;
15534
+ }
15535
+ function missingProviderMessage() {
15536
+ const configHints = agentProvidersByPriority().join(", ");
15537
+ const envHints = agentProvidersByPriority().map((p) => `${AGENT_PROVIDERS[p].piAiProvider.toUpperCase()}_API_KEY`).join(" / ");
15538
+ return `No agent LLM provider configured. Add an API key for one of: ${configHints} in ~/.canonry/config.yaml, or export ${envHints}.`;
15539
+ }
15540
+ function detectAgentProvider(config) {
15541
+ for (const provider of agentProvidersByPriority()) {
15542
+ if (resolveApiKeyFor(provider, config)) return provider;
15543
+ }
15544
+ return void 0;
15545
+ }
15546
+ function resolveAeroModel(provider, modelId) {
15547
+ ensureBuiltinsRegistered();
15548
+ return resolveModelForProvider(provider, modelId);
15549
+ }
15550
+ function buildApiKeyResolver(config) {
15551
+ return (piAiProvider) => resolveApiKeyFor(piAiProvider, config);
15552
+ }
15553
+ function createAeroSession(opts) {
15554
+ const systemPrompt = opts.systemPromptOverride ?? loadAeroSystemPrompt();
15555
+ const provider = opts.provider ?? detectAgentProvider(opts.config);
15556
+ if (!provider) throw new Error(missingProviderMessage());
15557
+ const model = resolveAeroModel(provider, opts.modelId);
15558
+ const toolScope = opts.toolScope ?? "all";
15559
+ const toolCtx = {
15560
+ client: opts.client,
15561
+ projectName: opts.projectName,
15562
+ db: opts.db,
15563
+ projectId: opts.projectId
15564
+ };
15565
+ const stateTools = toolScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
15566
+ const defaultTools = [...stateTools, ...buildSkillDocTools()];
15567
+ const tools = opts.tools ?? defaultTools;
15568
+ return new Agent({
15569
+ initialState: {
15570
+ systemPrompt,
15571
+ model,
15572
+ tools,
15573
+ ...opts.initialMessages ? { messages: opts.initialMessages } : {}
15574
+ },
15575
+ streamFn: opts.streamFn,
15576
+ getApiKey: buildApiKeyResolver(opts.config)
15577
+ });
15578
+ }
15579
+ function resolveSessionProviderAndModel(config, opts) {
15580
+ const provider = opts?.provider ?? detectAgentProvider(config);
15581
+ if (!provider) throw new Error(missingProviderMessage());
15582
+ const modelId = opts?.modelId ?? getAgentProvider(provider).defaultModel;
15583
+ return { provider, modelId };
15584
+ }
15585
+
15586
+ // src/agent/compaction.ts
15587
+ import { complete } from "@mariozechner/pi-ai";
15588
+
15589
+ // src/agent/compaction-config.ts
15590
+ var COMPACTION_TOKEN_THRESHOLD = 6e4;
15591
+ var COMPACTION_TARGET_RATIO = 0.5;
15592
+ var COMPACTION_PRESERVE_TAIL_MESSAGES = 10;
15593
+ var COMPACTION_MAX_MESSAGES = 400;
15594
+
15595
+ // src/agent/token-counter.ts
15596
+ var CHARS_PER_TOKEN = 4;
15597
+ function estimateMessageTokens(message) {
15598
+ const content = message.content;
15599
+ if (content === void 0) return 0;
15600
+ if (typeof content === "string") {
15601
+ return Math.ceil(content.length / CHARS_PER_TOKEN);
15602
+ }
15603
+ if (!Array.isArray(content)) return 0;
15604
+ let chars = 0;
15605
+ for (const part of content) {
15606
+ if (part && typeof part === "object" && "type" in part) {
15607
+ const p = part;
15608
+ switch (p.type) {
15609
+ case "text":
15610
+ chars += (p.text ?? "").length;
15611
+ break;
15612
+ case "thinking":
15613
+ chars += (p.thinking ?? "").length;
15614
+ break;
15615
+ case "toolCall":
15616
+ try {
15617
+ chars += JSON.stringify(p.arguments ?? {}).length;
15618
+ } catch {
15619
+ chars += 64;
15620
+ }
15621
+ break;
15622
+ case "image":
15623
+ chars += 1024;
15624
+ break;
15625
+ default:
15626
+ break;
15627
+ }
15628
+ }
15629
+ }
15630
+ return Math.ceil(chars / CHARS_PER_TOKEN);
15631
+ }
15632
+ function estimateTranscriptTokens(messages) {
15633
+ let total = 0;
15634
+ for (const m of messages) total += estimateMessageTokens(m);
15635
+ return total;
15636
+ }
15637
+
15638
+ // src/agent/compaction.ts
15639
+ function shouldCompact(messages) {
15640
+ if (messages.length >= COMPACTION_MAX_MESSAGES) return true;
15641
+ return estimateTranscriptTokens(messages) >= COMPACTION_TOKEN_THRESHOLD;
15642
+ }
15643
+ function findSafeSplit(messages, targetIndex) {
15644
+ const maxSplit = messages.length - COMPACTION_PRESERVE_TAIL_MESSAGES;
15645
+ if (maxSplit <= 0) return 0;
15646
+ const boundedTarget = Math.max(0, Math.min(targetIndex, maxSplit));
15647
+ for (let i = boundedTarget; i <= maxSplit; i++) {
15648
+ const m = messages[i];
15649
+ if (m && m.role === "user") return i;
15650
+ }
15651
+ return 0;
15652
+ }
15653
+ function toLlmMessages(messages) {
15654
+ const out = [];
15655
+ for (const m of messages) {
15656
+ if (m.role === "user" || m.role === "assistant" || m.role === "toolResult") {
15657
+ out.push(m);
15658
+ }
15659
+ }
15660
+ return out;
15661
+ }
15662
+ var SUMMARY_SYSTEM_PROMPT = `You compress an AI agent conversation transcript into a short durable note.
15663
+
15664
+ Extract only:
15665
+ - User intents and requests
15666
+ - Actions the agent took and their outcomes
15667
+ - Key findings, insights, decisions
15668
+ - Outstanding TODOs or deferred follow-ups
15669
+
15670
+ Style: dense bullet points. No preamble, no closing remarks, no agent self-commentary. Keep the note under 1500 characters.`;
15671
+ function truncateToByteLimit(text, maxBytes) {
15672
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
15673
+ const suffix = "\u2026[truncated]";
15674
+ const budget = maxBytes - Buffer.byteLength(suffix, "utf8");
15675
+ let buf = Buffer.from(text, "utf8").subarray(0, budget);
15676
+ while (buf.length > 0 && (buf[buf.length - 1] & 192) === 128) {
15677
+ buf = buf.subarray(0, buf.length - 1);
15678
+ }
15679
+ return buf.toString("utf8") + suffix;
15680
+ }
15681
+ async function runSummaryLlm(args) {
15682
+ const context = {
15683
+ systemPrompt: SUMMARY_SYSTEM_PROMPT,
15684
+ messages: toLlmMessages(args.chunk)
15685
+ };
15686
+ const apiKey = args.getApiKey?.(args.model.provider);
15687
+ const resp = await complete(args.model, context, apiKey ? { apiKey } : {});
15688
+ const parts = resp.content.filter((p) => p.type === "text");
15689
+ const text = parts.map((p) => p.text).join("\n").trim();
15690
+ if (!text) throw new Error("summary LLM returned no text content");
15691
+ return text;
15692
+ }
15693
+ async function compactMessages(args) {
15694
+ const target = Math.floor(args.messages.length * COMPACTION_TARGET_RATIO);
15695
+ const split = findSafeSplit(args.messages, target);
15696
+ if (split === 0) return null;
15697
+ const chunk = args.messages.slice(0, split);
15698
+ const suffix = args.messages.slice(split);
15699
+ const summarize = args.summarize ?? runSummaryLlm;
15700
+ const rawSummary = await summarize({ model: args.model, chunk, getApiKey: args.getApiKey });
15701
+ const summary = truncateToByteLimit(rawSummary, AGENT_MEMORY_VALUE_MAX_BYTES);
15702
+ writeCompactionNote(args.db, {
15703
+ projectId: args.projectId,
15704
+ sessionId: args.sessionId,
15705
+ summary,
15706
+ removedCount: chunk.length
15707
+ });
15708
+ return { messages: suffix, removedCount: chunk.length, summary };
15709
+ }
15710
+
15711
+ // src/agent/session-registry.ts
15712
+ var log7 = createLogger("SessionRegistry");
15713
+ var MAX_HYDRATE_NOTES = 20;
15714
+ var MAX_HYDRATE_BYTES = 32 * 1024;
15715
+ function escapeMemoryFragment(value) {
15716
+ return value.replace(/<(\/?)memory>/gi, "<$1\u200Cmemory>");
15717
+ }
15718
+ var SessionRegistry = class {
15719
+ live = /* @__PURE__ */ new Map();
15720
+ pending = /* @__PURE__ */ new Map();
15721
+ /** Last tool scope used on the live Agent for a project. Read in getOrCreate to know when to swap. */
15722
+ scopes = /* @__PURE__ */ new Map();
15723
+ /** Cached resolved project id per project name, used so alignScope can rebuild tool context without a DB roundtrip. */
15724
+ projectIds = /* @__PURE__ */ new Map();
15725
+ /**
15726
+ * In-flight compaction promises keyed by project name. A second
15727
+ * `acquireForTurn` that arrives while the first is still summarizing
15728
+ * awaits the same promise instead of kicking off a duplicate LLM call.
15729
+ */
15730
+ compactions = /* @__PURE__ */ new Map();
15731
+ opts;
15732
+ constructor(opts) {
15733
+ this.opts = opts;
15734
+ }
15735
+ /** Read-only access to the config snapshot the registry was built with. */
15736
+ getConfig() {
15737
+ return this.opts.config;
15738
+ }
15739
+ /**
15740
+ * Returns the live Agent for a project, hydrating from DB or creating
15741
+ * fresh. Applies caller preferences on fresh/hydrated construction. Does
15742
+ * NOT mutate an already-cached Agent — that path goes through
15743
+ * `acquireForTurn`, which gates scope/model changes behind a busy check
15744
+ * so an in-flight turn can't have its tools swapped out from under it.
15745
+ */
15746
+ getOrCreate(projectName, preferences) {
15747
+ const cached = this.live.get(projectName);
15748
+ if (cached) return cached;
15749
+ const projectId = this.resolveProjectId(projectName);
15750
+ const row = this.loadRow(projectId);
15751
+ if (row) {
15752
+ const persistedMessages = parseJsonColumn(row.messages, []);
15753
+ const queued = parseJsonColumn(row.followUpQueue, []);
15754
+ const effectiveProvider = preferences?.provider ?? row.modelProvider;
15755
+ const effectiveModelId = preferences?.modelId ?? row.modelId;
15756
+ if (preferences?.provider || preferences?.modelId) {
15757
+ this.opts.db.update(agentSessions).set({
15758
+ modelProvider: effectiveProvider,
15759
+ modelId: effectiveModelId,
15760
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15761
+ }).where(eq24(agentSessions.projectId, projectId)).run();
15762
+ }
15763
+ const agent2 = createAeroSession({
15764
+ projectName,
15765
+ client: this.opts.client,
15766
+ config: this.opts.config,
15767
+ db: this.opts.db,
15768
+ projectId,
15769
+ provider: effectiveProvider,
15770
+ modelId: effectiveModelId,
15771
+ systemPromptOverride: this.buildHydratedSystemPrompt(projectId, row.systemPrompt),
15772
+ initialMessages: persistedMessages,
15773
+ toolScope: preferences?.toolScope
15774
+ });
15775
+ this.scopes.set(projectName, preferences?.toolScope ?? "all");
15776
+ this.projectIds.set(projectName, projectId);
15777
+ if (queued.length > 0) {
15778
+ this.appendPending(projectName, queued);
15779
+ this.updateRow(projectId, { followUpQueue: "[]" });
15780
+ }
15781
+ this.live.set(projectName, agent2);
15782
+ this.registerDrainHook(agent2, projectName);
15783
+ return agent2;
15784
+ }
15785
+ const { provider, modelId } = resolveSessionProviderAndModel(this.opts.config, preferences);
15786
+ const systemPrompt = loadAeroSystemPrompt();
15787
+ const agent = createAeroSession({
15788
+ projectName,
15789
+ client: this.opts.client,
15790
+ config: this.opts.config,
15791
+ db: this.opts.db,
15792
+ projectId,
15793
+ provider,
15794
+ modelId,
15795
+ // Hydrate on the fresh path too — a brand-new session may still see
15796
+ // notes if they were seeded via CLI/API before the first prompt.
15797
+ systemPromptOverride: this.buildHydratedSystemPrompt(projectId, systemPrompt),
15798
+ toolScope: preferences?.toolScope
15799
+ });
15800
+ this.scopes.set(projectName, preferences?.toolScope ?? "all");
15801
+ this.projectIds.set(projectName, projectId);
15802
+ this.insertRow({
15803
+ projectId,
15804
+ // Persist the raw (unhydrated) prompt so the DB remains canonical —
15805
+ // the `<memory>` block is rebuilt from the notes table on every load.
15806
+ systemPrompt,
15807
+ modelProvider: provider,
15808
+ modelId,
15809
+ messages: [],
15810
+ followUpQueue: []
15811
+ });
15812
+ this.live.set(projectName, agent);
15813
+ this.registerDrainHook(agent, projectName);
15814
+ return agent;
15815
+ }
15816
+ /**
15817
+ * Append the `<memory>` block to a base system prompt, sourced from the
15818
+ * `agent_memory` table. Returns the base prompt unchanged when no notes
15819
+ * exist — an empty block would just be prompt noise. Truncates to
15820
+ * `MAX_HYDRATE_BYTES`, dropping oldest-first, so the block is bounded
15821
+ * even when notes sit near their 2 KB cap.
15822
+ *
15823
+ * Note values come from LLM-authored compaction summaries and operator
15824
+ * input, so they are treated as untrusted data: closing tags that could
15825
+ * escape the `<memory>` wrapper are neutralized before interpolation.
15826
+ */
15827
+ buildHydratedSystemPrompt(projectId, basePrompt) {
15828
+ const entries = loadRecentForHydrate(this.opts.db, projectId, MAX_HYDRATE_NOTES);
15829
+ if (entries.length === 0) return basePrompt;
15830
+ let totalBytes = 0;
15831
+ const kept = [];
15832
+ for (const entry of entries) {
15833
+ const escaped = {
15834
+ source: escapeMemoryFragment(entry.source),
15835
+ key: escapeMemoryFragment(entry.key),
15836
+ value: escapeMemoryFragment(entry.value)
15837
+ };
15838
+ const line = `- [${escaped.source}] ${escaped.key}: ${escaped.value}
15839
+ `;
15840
+ const bytes = Buffer.byteLength(line, "utf8");
15841
+ if (totalBytes + bytes > MAX_HYDRATE_BYTES) break;
15842
+ kept.push(escaped);
15843
+ totalBytes += bytes;
15844
+ }
15845
+ if (kept.length === 0) return basePrompt;
15846
+ const lines = kept.map((e) => `- [${e.source}] ${e.key}: ${e.value}`);
15847
+ return `${basePrompt.trimEnd()}
15848
+
15849
+ ---
15850
+
15851
+ <memory>
15852
+ Project-scoped durable notes (newest first). Use remember/forget/recall to manage. Entries tagged [compaction] are LLM-summarized transcript slices.
15853
+
15854
+ ${lines.join("\n")}
15855
+ </memory>`;
15856
+ }
15857
+ /**
15858
+ * Rebuild the live agent's system prompt from the latest `agent_memory`
15859
+ * rows. Called after out-of-band memory writes (CLI/API PUT/DELETE) so
15860
+ * the next turn on a hot session sees the updated notes without waiting
15861
+ * for compaction or a cold restart. No-op when no live agent exists —
15862
+ * the next `getOrCreate` will hydrate from DB anyway.
15863
+ */
15864
+ rehydrateLiveMemory(projectName) {
15865
+ const agent = this.live.get(projectName);
15866
+ if (!agent) return;
15867
+ const projectId = this.tryResolveProjectId(projectName);
15868
+ if (!projectId) return;
15869
+ const row = this.loadRow(projectId);
15870
+ if (!row) return;
15871
+ agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
15872
+ }
15873
+ /**
15874
+ * Acquire the Agent for an upcoming prompt/turn.
15875
+ *
15876
+ * Busy-check runs FIRST, before any state mutation — if two requests race
15877
+ * on the same project, one gets the 409 and the other's in-flight turn is
15878
+ * untouched. Only after confirming idle do we:
15879
+ * - align `state.tools` to the requested scope (CLI full vs dashboard
15880
+ * read-only share the same cached Agent; each request re-scopes it).
15881
+ * - align `state.model` when the caller passes `provider` or `modelId`,
15882
+ * honoring `--provider` / `--model` on hot sessions (not just on
15883
+ * fresh/hydrated construction).
15884
+ *
15885
+ * Persists the new model choice to the DB row so subsequent invocations
15886
+ * stay on it unless overridden again.
15887
+ */
15888
+ async acquireForTurn(projectName, preferences) {
15889
+ const agent = this.getOrCreate(projectName);
15890
+ if (agent.state.isStreaming) {
15891
+ throw agentBusy(projectName);
15892
+ }
15893
+ this.alignScope(projectName, agent, preferences?.toolScope ?? "all");
15894
+ if (preferences?.provider || preferences?.modelId) {
15895
+ this.alignModel(projectName, agent, preferences);
15896
+ }
15897
+ await this.maybeCompact(projectName, agent);
15898
+ return agent;
15899
+ }
15900
+ /**
15901
+ * Summarize the oldest half of the transcript into a `compaction:`
15902
+ * memory row when the transcript crosses the token/message threshold.
15903
+ * Runs before the caller's next `agent.prompt()` so the model sees the
15904
+ * trimmed transcript + a refreshed `<memory>` block that now includes
15905
+ * the new summary.
15906
+ *
15907
+ * Races are deduped through `this.compactions`: a concurrent call for
15908
+ * the same project awaits the in-flight promise instead of launching a
15909
+ * duplicate summarizer run. Failures are logged and swallowed — a flaky
15910
+ * summarizer must never block a user turn.
15911
+ */
15912
+ async maybeCompact(projectName, agent) {
15913
+ const inflight = this.compactions.get(projectName);
15914
+ if (inflight) {
15915
+ await inflight;
15916
+ return;
15917
+ }
15918
+ if (!shouldCompact(agent.state.messages)) return;
15919
+ const promise = this.runCompaction(projectName, agent).finally(() => {
15920
+ this.compactions.delete(projectName);
15921
+ });
15922
+ this.compactions.set(projectName, promise);
15923
+ await promise;
15924
+ }
15925
+ async runCompaction(projectName, agent) {
15926
+ const projectId = this.projectIds.get(projectName) ?? this.resolveProjectId(projectName);
15927
+ this.projectIds.set(projectName, projectId);
15928
+ const row = this.loadRow(projectId);
15929
+ if (!row) return;
15930
+ try {
15931
+ const result = await compactMessages({
15932
+ db: this.opts.db,
15933
+ projectId,
15934
+ sessionId: row.id,
15935
+ messages: agent.state.messages,
15936
+ model: agent.state.model,
15937
+ getApiKey: buildApiKeyResolver(this.opts.config)
15938
+ });
15939
+ if (!result) return;
15940
+ agent.state.messages = result.messages;
15941
+ agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
15942
+ this.save(projectName);
15943
+ log7.info("compaction.completed", {
15944
+ projectName,
15945
+ removedCount: result.removedCount,
15946
+ summaryBytes: Buffer.byteLength(result.summary, "utf8")
15947
+ });
15948
+ } catch (err) {
15949
+ log7.error("compaction.failed", {
15950
+ projectName,
15951
+ error: err instanceof Error ? err.message : String(err)
15952
+ });
15953
+ }
15954
+ }
15955
+ alignScope(projectName, agent, wantScope) {
15956
+ if (this.scopes.get(projectName) === wantScope) return;
15957
+ const projectId = this.projectIds.get(projectName) ?? this.resolveProjectId(projectName);
15958
+ this.projectIds.set(projectName, projectId);
15959
+ const toolCtx = { client: this.opts.client, projectName, db: this.opts.db, projectId };
15960
+ const stateTools = wantScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
15961
+ agent.state.tools = [...stateTools, ...buildSkillDocTools()];
15962
+ this.scopes.set(projectName, wantScope);
15963
+ }
15964
+ alignModel(projectName, agent, preferences) {
15965
+ const projectId = this.tryResolveProjectId(projectName);
15966
+ if (!projectId) return;
15967
+ const row = this.loadRow(projectId);
15968
+ const currentProvider = row?.modelProvider ?? AgentProviderIds.claude;
15969
+ const currentModelId = row?.modelId;
15970
+ const nextProvider = preferences.provider ?? currentProvider;
15971
+ const nextModelId = preferences.modelId ?? (preferences.provider ? getAgentProvider(nextProvider).defaultModel : currentModelId);
15972
+ if (!nextModelId) return;
15973
+ if (nextProvider === currentProvider && nextModelId === currentModelId) return;
15974
+ agent.state.model = resolveAeroModel(nextProvider, nextModelId);
15975
+ this.opts.db.update(agentSessions).set({
15976
+ modelProvider: nextProvider,
15977
+ modelId: nextModelId,
15978
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15979
+ }).where(eq24(agentSessions.projectId, projectId)).run();
15980
+ }
15981
+ /** Persist a session's transcript back to the DB. Call after any run settles. */
15982
+ save(projectName) {
15983
+ const agent = this.live.get(projectName);
15984
+ if (!agent) return;
15985
+ const projectId = this.resolveProjectId(projectName);
15986
+ this.updateRow(projectId, {
15987
+ messages: JSON.stringify(agent.state.messages)
15988
+ });
15989
+ }
15990
+ /**
15991
+ * Enqueue a message for the next turn.
15992
+ *
15993
+ * - Live session exists: append to the in-memory pending queue. The next
15994
+ * `consumePending`-backed prompt, a `drainNow` call, or the post-`agent_end`
15995
+ * drain hook will process it.
15996
+ * - No live session: persist to the DB follow-up queue. The next
15997
+ * `getOrCreate` hydrates the agent and migrates the queue into pending.
15998
+ *
15999
+ * Crucially writes to exactly ONE of the two sinks to avoid the duplicate
16000
+ * message we saw during the first end-to-end dogfood (run.completed fired,
16001
+ * both the in-memory pending and the DB-queue migration produced copies).
16002
+ */
16003
+ queueFollowUp(projectName, message) {
16004
+ if (this.live.has(projectName)) {
16005
+ this.appendPending(projectName, [message]);
16006
+ } else {
16007
+ this.persistQueueAppend(projectName, message);
16008
+ }
16009
+ }
16010
+ /** Consume (and clear) the pending queue for a project. Caller prompts with the result. */
16011
+ consumePending(projectName) {
16012
+ const msgs = this.pending.get(projectName) ?? [];
16013
+ if (msgs.length === 0) return [];
16014
+ this.pending.delete(projectName);
16015
+ const projectId = this.tryResolveProjectId(projectName);
16016
+ if (projectId) this.updateRow(projectId, { followUpQueue: "[]" });
16017
+ return msgs;
16018
+ }
16019
+ /**
16020
+ * Proactive drain — hydrate if needed, consume pending, prompt the agent.
16021
+ *
16022
+ * No-op when:
16023
+ * - there are no pending messages in memory AND no persisted queue in
16024
+ * the DB (post-restart / never-hydrated sessions still need to wake)
16025
+ * - the agent is currently streaming (it will pick them up on the next turn)
16026
+ *
16027
+ * Fire-and-forget safe: failures are logged, never thrown. This is what
16028
+ * RunCoordinator calls after a run completes to wake Aero unprompted.
16029
+ */
16030
+ async drainNow(projectName) {
16031
+ if (!this.hasPendingWork(projectName)) return;
16032
+ try {
16033
+ let agent;
16034
+ try {
16035
+ const scope = this.scopes.get(projectName) ?? "read-only";
16036
+ agent = await this.acquireForTurn(projectName, { toolScope: scope });
16037
+ } catch (err) {
16038
+ if (err.code === "AGENT_BUSY") return;
16039
+ throw err;
16040
+ }
16041
+ const msgs = this.consumePending(projectName);
16042
+ if (msgs.length === 0) return;
16043
+ await agent.prompt(msgs);
16044
+ this.save(projectName);
16045
+ } catch (err) {
16046
+ log7.error("drain.failed", {
16047
+ projectName,
16048
+ error: err instanceof Error ? err.message : String(err)
16049
+ });
16050
+ }
16051
+ }
16052
+ /** Drop the live Agent for a project. Next lookup rehydrates from DB. */
16053
+ evict(projectName) {
16054
+ this.live.delete(projectName);
16055
+ }
16056
+ /**
16057
+ * Authoritative reset for a project's session state. Drops the live Agent,
16058
+ * clears the in-memory pending follow-up buffer, and forgets the cached
16059
+ * tool scope. Caller is responsible for wiping the durable row; this only
16060
+ * touches the in-process state the registry holds.
16061
+ *
16062
+ * Use this (not `evict`) when the caller guarantees the conversation is
16063
+ * being wiped — e.g. `DELETE /agent/transcript`. `evict` alone leaves any
16064
+ * in-memory follow-ups queued on a hot session, which would leak into the
16065
+ * next turn after the reset.
16066
+ */
16067
+ reset(projectName) {
16068
+ this.live.delete(projectName);
16069
+ this.pending.delete(projectName);
16070
+ this.scopes.delete(projectName);
16071
+ this.projectIds.delete(projectName);
16072
+ }
16073
+ /** Evict every live Agent. Durable state in DB is untouched. */
16074
+ clear() {
16075
+ this.live.clear();
16076
+ }
16077
+ /** Visible so tests can assert whether a session is hot. */
16078
+ isLive(projectName) {
16079
+ return this.live.has(projectName);
16080
+ }
16081
+ /** Visible so tests can peek at the pending queue without consuming. */
16082
+ peekPending(projectName) {
16083
+ return this.pending.get(projectName) ?? [];
16084
+ }
16085
+ // ──────────────────────────────────────────────────────────────────
16086
+ /**
16087
+ * True when there's in-memory pending work OR a persisted follow-up queue
16088
+ * for this project. Checked by `drainNow` before doing any hydration work
16089
+ * so proactive wake-up fires even on cold / post-restart sessions where
16090
+ * the follow-up lives only in the DB row.
16091
+ */
16092
+ hasPendingWork(projectName) {
16093
+ if ((this.pending.get(projectName) ?? []).length > 0) return true;
16094
+ const projectId = this.tryResolveProjectId(projectName);
16095
+ if (!projectId) return false;
16096
+ const row = this.loadRow(projectId);
16097
+ if (!row) return false;
16098
+ return parseJsonColumn(row.followUpQueue, []).length > 0;
16099
+ }
16100
+ appendPending(projectName, messages) {
16101
+ if (messages.length === 0) return;
16102
+ const existing = this.pending.get(projectName) ?? [];
16103
+ this.pending.set(projectName, [...existing, ...messages]);
16104
+ }
16105
+ persistQueueAppend(projectName, message) {
16106
+ const projectId = this.tryResolveProjectId(projectName);
16107
+ if (!projectId) return;
16108
+ const row = this.loadRow(projectId);
16109
+ if (!row) {
16110
+ this.insertRow({
16111
+ projectId,
16112
+ systemPrompt: loadAeroSystemPrompt(),
16113
+ ...resolveSessionProviderAndModel(this.opts.config),
16114
+ messages: [],
16115
+ followUpQueue: [message]
16116
+ });
16117
+ return;
16118
+ }
16119
+ const existing = parseJsonColumn(row.followUpQueue, []);
16120
+ this.updateRow(projectId, { followUpQueue: JSON.stringify([...existing, message]) });
16121
+ }
16122
+ /**
16123
+ * Subscribe to agent_end so any pending messages that landed during a run
16124
+ * (from RunCoordinator callbacks or steered follow-ups) drain automatically
16125
+ * after the current turn settles. Without this, a RunCoordinator event that
16126
+ * arrives mid-CLI-turn would sit in pending until someone called drainNow.
16127
+ */
16128
+ registerDrainHook(agent, projectName) {
16129
+ agent.subscribe((event) => {
16130
+ if (event.type !== "agent_end") return;
16131
+ if ((this.pending.get(projectName) ?? []).length === 0) return;
16132
+ void this.drainNow(projectName);
16133
+ });
16134
+ }
16135
+ resolveProjectId(projectName) {
16136
+ const id = this.tryResolveProjectId(projectName);
16137
+ if (!id) throw new Error(`Project "${projectName}" not found`);
16138
+ return id;
16139
+ }
16140
+ tryResolveProjectId(projectName) {
16141
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq24(projects.name, projectName)).get();
16142
+ return row?.id;
16143
+ }
16144
+ loadRow(projectId) {
16145
+ const row = this.opts.db.select().from(agentSessions).where(eq24(agentSessions.projectId, projectId)).get();
16146
+ return row ?? null;
16147
+ }
16148
+ insertRow(params) {
16149
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16150
+ this.opts.db.insert(agentSessions).values({
16151
+ id: crypto23.randomUUID(),
16152
+ projectId: params.projectId,
16153
+ systemPrompt: params.systemPrompt,
16154
+ modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
16155
+ modelId: params.modelId ?? "claude-opus-4-7",
16156
+ messages: JSON.stringify(params.messages),
16157
+ followUpQueue: JSON.stringify(params.followUpQueue),
16158
+ createdAt: now,
16159
+ updatedAt: now
16160
+ }).run();
16161
+ }
16162
+ updateRow(projectId, patch) {
16163
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16164
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq24(agentSessions.projectId, projectId)).run();
16165
+ }
16166
+ };
16167
+
16168
+ // src/agent/agent-routes.ts
16169
+ import { eq as eq25 } from "drizzle-orm";
16170
+ function resolveProject2(db, name) {
16171
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq25(projects.name, name)).get();
16172
+ if (!row) throw notFound("project", name);
16173
+ return row;
16174
+ }
16175
+ function registerAgentRoutes(app, opts) {
16176
+ app.get(
16177
+ "/projects/:name/agent/transcript",
16178
+ async (request) => {
16179
+ const project = resolveProject2(opts.db, request.params.name);
16180
+ const row = opts.db.select().from(agentSessions).where(eq25(agentSessions.projectId, project.id)).get();
16181
+ if (!row) {
16182
+ return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
16183
+ }
16184
+ return {
16185
+ messages: parseJsonColumn(row.messages, []),
16186
+ modelProvider: row.modelProvider,
16187
+ modelId: row.modelId,
16188
+ updatedAt: row.updatedAt
16189
+ };
16190
+ }
16191
+ );
16192
+ app.get(
16193
+ "/projects/:name/agent/providers",
16194
+ async (request) => {
16195
+ resolveProject2(opts.db, request.params.name);
16196
+ return buildAgentProvidersResponse(opts.sessionRegistry.getConfig());
16197
+ }
16198
+ );
16199
+ app.delete(
16200
+ "/projects/:name/agent/transcript",
16201
+ async (request) => {
16202
+ const project = resolveProject2(opts.db, request.params.name);
16203
+ opts.sessionRegistry.reset(project.name);
16204
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(agentSessions.projectId, project.id)).run();
16205
+ return { status: "reset" };
16206
+ }
16207
+ );
16208
+ app.post("/projects/:name/agent/prompt", async (request, reply) => {
16209
+ const project = resolveProject2(opts.db, request.params.name);
16210
+ const promptText = (request.body?.prompt ?? "").trim();
16211
+ if (!promptText) throw validationError('"prompt" is required');
16212
+ const requestedScope = request.body?.scope === "all" ? "all" : "read-only";
16213
+ const agent = await opts.sessionRegistry.acquireForTurn(project.name, {
16214
+ provider: request.body?.provider,
16215
+ modelId: request.body?.modelId,
16216
+ toolScope: requestedScope
16217
+ });
16218
+ reply.raw.writeHead(200, {
16219
+ "Content-Type": "text/event-stream",
16220
+ "Cache-Control": "no-cache",
16221
+ "Connection": "keep-alive",
16222
+ "X-Accel-Buffering": "no"
16223
+ });
16224
+ const write = (payload) => {
16225
+ if (reply.raw.writableEnded) return;
16226
+ try {
16227
+ reply.raw.write(`data: ${JSON.stringify(payload)}
16228
+
16229
+ `);
16230
+ } catch {
16231
+ }
16232
+ };
16233
+ write({ type: "stream_open" });
16234
+ const unsubscribe = agent.subscribe((event) => {
16235
+ write(event);
16236
+ });
16237
+ reply.raw.once("close", () => {
16238
+ if (!reply.raw.writableEnded) {
16239
+ agent.abort();
16240
+ }
16241
+ });
16242
+ try {
16243
+ const pending = opts.sessionRegistry.consumePending(project.name);
16244
+ const userMessage = {
16245
+ role: "user",
16246
+ content: promptText,
16247
+ timestamp: Date.now()
16248
+ };
16249
+ const batch = pending.length > 0 ? [...pending, userMessage] : userMessage;
16250
+ await agent.prompt(batch);
16251
+ await agent.waitForIdle();
16252
+ opts.sessionRegistry.save(project.name);
16253
+ } catch (err) {
16254
+ write({ type: "error", message: err instanceof Error ? err.message : String(err) });
16255
+ } finally {
16256
+ unsubscribe();
16257
+ write({ type: "stream_close" });
16258
+ if (!reply.raw.writableEnded) {
16259
+ reply.raw.end();
16260
+ }
16261
+ }
16262
+ return reply;
16263
+ });
16264
+ app.get(
16265
+ "/projects/:name/agent/memory",
16266
+ async (request) => {
16267
+ const project = resolveProject2(opts.db, request.params.name);
16268
+ return { entries: listMemoryEntries(opts.db, project.id) };
16269
+ }
16270
+ );
16271
+ app.put(
16272
+ "/projects/:name/agent/memory",
16273
+ async (request) => {
16274
+ const project = resolveProject2(opts.db, request.params.name);
16275
+ const parsed = agentMemoryUpsertRequestSchema.safeParse(request.body);
16276
+ if (!parsed.success) {
16277
+ throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
16278
+ }
16279
+ if (parsed.data.key.startsWith(COMPACTION_KEY_PREFIX)) {
16280
+ throw validationError(
16281
+ `key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`
16282
+ );
16283
+ }
16284
+ if (Buffer.byteLength(parsed.data.value, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
16285
+ throw validationError(`"value" exceeds ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes`);
16286
+ }
16287
+ const entry = upsertMemoryEntry(opts.db, {
16288
+ projectId: project.id,
16289
+ key: parsed.data.key,
16290
+ value: parsed.data.value,
16291
+ source: MemorySources.user
16292
+ });
16293
+ opts.sessionRegistry.rehydrateLiveMemory(project.name);
16294
+ return { status: "ok", entry };
16295
+ }
16296
+ );
16297
+ app.delete(
16298
+ "/projects/:name/agent/memory",
16299
+ async (request) => {
16300
+ const project = resolveProject2(opts.db, request.params.name);
16301
+ const parsed = agentMemoryDeleteRequestSchema.safeParse(request.body);
16302
+ if (!parsed.success) {
16303
+ throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
16304
+ }
16305
+ if (parsed.data.key.startsWith(COMPACTION_KEY_PREFIX)) {
16306
+ throw validationError(
16307
+ `key prefix "${COMPACTION_KEY_PREFIX}" is reserved; compaction notes are pruned automatically`
16308
+ );
16309
+ }
16310
+ const removed = deleteMemoryEntry(opts.db, project.id, parsed.data.key);
16311
+ if (removed) opts.sessionRegistry.rehydrateLiveMemory(project.name);
16312
+ return { status: removed ? "forgotten" : "missing", key: parsed.data.key };
16313
+ }
16314
+ );
16315
+ }
16316
+
16317
+ // src/client.ts
16318
+ function createApiClient() {
16319
+ const config = loadConfig();
16320
+ const basePathResolved = !!config.basePath || "CANONRY_BASE_PATH" in process.env;
16321
+ return new ApiClient(config.apiUrl, config.apiKey, { skipProbe: basePathResolved });
16322
+ }
16323
+ var ApiClient = class {
16324
+ baseUrl;
16325
+ originUrl;
16326
+ apiKey;
16327
+ probePromise = null;
16328
+ probeSkipped;
16329
+ constructor(baseUrl, apiKey, opts) {
16330
+ this.originUrl = baseUrl.replace(/\/$/, "");
16331
+ this.baseUrl = this.originUrl + "/api/v1";
16332
+ this.apiKey = apiKey;
16333
+ this.probeSkipped = opts?.skipProbe ?? false;
16334
+ }
16335
+ /**
16336
+ * On first API call, probe /health to auto-discover basePath when the user
16337
+ * hasn't configured one locally. This lets `canonry run` in a separate shell
16338
+ * discover that the server is running at e.g. /canonry/ without requiring
16339
+ * config.yaml edits or CANONRY_BASE_PATH in every shell.
16340
+ */
16341
+ probeBasePath() {
16342
+ if (this.probeSkipped) return Promise.resolve();
16343
+ if (!this.probePromise) {
16344
+ this.probePromise = (async () => {
16345
+ try {
16346
+ const origin = new URL(this.originUrl).origin;
16347
+ const res = await fetch(`${origin}/health`, {
16348
+ signal: AbortSignal.timeout(2e3)
16349
+ });
16350
+ if (res.ok) {
16351
+ const body = await res.json();
16352
+ if (body.basePath && typeof body.basePath === "string") {
16353
+ const normalized = "/" + body.basePath.replace(/^\/|\/$/g, "");
16354
+ if (normalized !== "/") {
16355
+ this.originUrl = origin + normalized;
16356
+ this.baseUrl = this.originUrl + "/api/v1";
16357
+ }
16358
+ }
16359
+ }
16360
+ } catch {
16361
+ }
16362
+ })();
16363
+ }
16364
+ return this.probePromise;
16365
+ }
16366
+ async request(method, path10, body) {
16367
+ await this.probeBasePath();
16368
+ const url = `${this.baseUrl}${path10}`;
16369
+ const serializedBody = body != null ? JSON.stringify(body) : void 0;
16370
+ const headers = {
16371
+ "Authorization": `Bearer ${this.apiKey}`,
16372
+ "Accept": "application/json",
16373
+ ...serializedBody != null ? { "Content-Type": "application/json" } : {}
16374
+ };
16375
+ let res;
16376
+ try {
16377
+ res = await fetch(url, {
16378
+ method,
16379
+ headers,
16380
+ body: serializedBody
16381
+ });
16382
+ } catch (err) {
16383
+ if (err instanceof CliError) throw err;
16384
+ const msg = err instanceof Error ? err.message : String(err);
16385
+ if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("connect ECONNREFUSED")) {
16386
+ throw new CliError({
16387
+ code: "CONNECTION_ERROR",
16388
+ message: `Could not connect to canonry server at ${this.baseUrl.replace("/api/v1", "")}. Start it with "canonry serve" (or "canonry serve &" to run in background).`,
16389
+ exitCode: EXIT_SYSTEM_ERROR
16390
+ });
16391
+ }
16392
+ throw new CliError({ code: "CONNECTION_ERROR", message: msg, exitCode: EXIT_SYSTEM_ERROR });
16393
+ }
16394
+ if (!res.ok) {
16395
+ let errorBody;
16396
+ try {
16397
+ errorBody = await res.json();
16398
+ } catch {
16399
+ errorBody = { error: { code: "UNKNOWN", message: res.statusText } };
16400
+ }
16401
+ const errorObj = errorBody && typeof errorBody === "object" && "error" in errorBody && errorBody.error && typeof errorBody.error === "object" ? errorBody.error : null;
16402
+ const msg = errorObj?.message ? String(errorObj.message) : `HTTP ${res.status}: ${res.statusText}`;
16403
+ const code = errorObj?.code ? String(errorObj.code) : "API_ERROR";
16404
+ const exitCode = res.status >= 500 ? EXIT_SYSTEM_ERROR : EXIT_USER_ERROR;
16405
+ throw new CliError({ code, message: msg, exitCode });
16406
+ }
16407
+ if (res.status === 204) {
16408
+ return void 0;
16409
+ }
16410
+ return await res.json();
16411
+ }
16412
+ async getAgentTranscript(project) {
16413
+ return this.request(
16414
+ "GET",
16415
+ `/projects/${encodeURIComponent(project)}/agent/transcript`
16416
+ );
16417
+ }
16418
+ async resetAgentTranscript(project) {
16419
+ await this.request(
16420
+ "DELETE",
16421
+ `/projects/${encodeURIComponent(project)}/agent/transcript`
16422
+ );
16423
+ }
16424
+ async listAgentProviders(project) {
16425
+ return this.request(
16426
+ "GET",
16427
+ `/projects/${encodeURIComponent(project)}/agent/providers`
16428
+ );
16429
+ }
16430
+ async listAgentMemory(project) {
16431
+ return this.request(
16432
+ "GET",
16433
+ `/projects/${encodeURIComponent(project)}/agent/memory`
16434
+ );
16435
+ }
16436
+ async setAgentMemory(project, body) {
16437
+ return this.request(
16438
+ "PUT",
16439
+ `/projects/${encodeURIComponent(project)}/agent/memory`,
16440
+ body
16441
+ );
16442
+ }
16443
+ async forgetAgentMemory(project, key) {
16444
+ return this.request(
16445
+ "DELETE",
16446
+ `/projects/${encodeURIComponent(project)}/agent/memory`,
16447
+ { key }
16448
+ );
16449
+ }
16450
+ /**
16451
+ * POST a request whose response body the caller intends to consume as a
16452
+ * stream (e.g. the Aero agent SSE endpoint). Shares the probe + auth +
16453
+ * structured-error behavior of `request()`; the caller reads `res.body`
16454
+ * and releases the response when done.
16455
+ */
16456
+ async streamPost(path10, body, signal) {
16457
+ await this.probeBasePath();
16458
+ const url = `${this.baseUrl}${path10}`;
16459
+ const headers = {
16460
+ Authorization: `Bearer ${this.apiKey}`,
16461
+ "Content-Type": "application/json",
16462
+ Accept: "text/event-stream"
16463
+ };
16464
+ let res;
16465
+ try {
16466
+ res = await fetch(url, {
16467
+ method: "POST",
16468
+ headers,
16469
+ body: JSON.stringify(body ?? {}),
16470
+ signal
16471
+ });
16472
+ } catch (err) {
16473
+ if (err instanceof CliError) throw err;
16474
+ const msg = err instanceof Error ? err.message : String(err);
16475
+ if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("connect ECONNREFUSED")) {
16476
+ throw new CliError({
16477
+ code: "CONNECTION_ERROR",
16478
+ message: `Could not connect to canonry server at ${this.baseUrl.replace("/api/v1", "")}. Start it with "canonry serve" (or "canonry serve &" to run in background).`,
16479
+ exitCode: EXIT_SYSTEM_ERROR
16480
+ });
16481
+ }
16482
+ throw new CliError({ code: "CONNECTION_ERROR", message: msg, exitCode: EXIT_SYSTEM_ERROR });
16483
+ }
16484
+ if (!res.ok || !res.body) {
16485
+ let errorBody;
16486
+ try {
16487
+ errorBody = await res.json();
16488
+ } catch {
16489
+ errorBody = { error: { code: "UNKNOWN", message: res.statusText } };
16490
+ }
16491
+ const errorObj = errorBody && typeof errorBody === "object" && "error" in errorBody && errorBody.error ? errorBody.error : null;
16492
+ const msg = errorObj?.message ? String(errorObj.message) : `HTTP ${res.status}: ${res.statusText}`;
16493
+ const code = errorObj?.code ? String(errorObj.code) : "API_ERROR";
16494
+ const exitCode = res.status >= 500 ? EXIT_SYSTEM_ERROR : EXIT_USER_ERROR;
16495
+ throw new CliError({ code, message: msg, exitCode });
16496
+ }
16497
+ return res;
16498
+ }
16499
+ async putProject(name, body) {
16500
+ return this.request("PUT", `/projects/${encodeURIComponent(name)}`, body);
16501
+ }
16502
+ async listProjects() {
16503
+ return this.request("GET", "/projects");
16504
+ }
16505
+ async getProject(name) {
16506
+ return this.request("GET", `/projects/${encodeURIComponent(name)}`);
16507
+ }
16508
+ async deleteProject(name) {
16509
+ await this.request("DELETE", `/projects/${encodeURIComponent(name)}`);
16510
+ }
16511
+ async putKeywords(project, keywords2) {
16512
+ await this.request("PUT", `/projects/${encodeURIComponent(project)}/keywords`, { keywords: keywords2 });
16513
+ }
16514
+ async listKeywords(project) {
16515
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/keywords`);
16516
+ }
16517
+ async deleteKeywords(project, keywords2) {
16518
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/keywords`, { keywords: keywords2 });
16519
+ }
16520
+ async appendKeywords(project, keywords2) {
16521
+ await this.request("POST", `/projects/${encodeURIComponent(project)}/keywords`, { keywords: keywords2 });
16522
+ }
16523
+ async putCompetitors(project, competitors2) {
16524
+ await this.request("PUT", `/projects/${encodeURIComponent(project)}/competitors`, { competitors: competitors2 });
16525
+ }
16526
+ async listCompetitors(project) {
16527
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/competitors`);
16528
+ }
16529
+ async triggerRun(project, body) {
16530
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/runs`, body ?? {});
16531
+ }
16532
+ async listRuns(project, limit) {
16533
+ const query = limit != null ? `?limit=${encodeURIComponent(String(limit))}` : "";
16534
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/runs${query}`);
16535
+ }
16536
+ async getRun(id) {
16537
+ return this.request("GET", `/runs/${encodeURIComponent(id)}`);
16538
+ }
16539
+ async cancelRun(id) {
16540
+ return this.request("POST", `/runs/${encodeURIComponent(id)}/cancel`);
16541
+ }
16542
+ async getTimeline(project) {
16543
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/timeline`);
16544
+ }
16545
+ async getHistory(project) {
16546
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/history`);
16547
+ }
16548
+ async getExport(project) {
16549
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/export`);
16550
+ }
16551
+ async apply(config) {
16552
+ return this.request("POST", "/apply", config);
16553
+ }
16554
+ async getStatus(project) {
16555
+ return this.request("GET", `/projects/${encodeURIComponent(project)}`);
16556
+ }
16557
+ async getSettings() {
16558
+ return this.request("GET", "/settings");
16559
+ }
16560
+ async createSnapshot(body) {
16561
+ return this.request("POST", "/snapshot", body);
16562
+ }
16563
+ async updateProvider(name, body) {
16564
+ return this.request("PUT", `/settings/providers/${encodeURIComponent(name)}`, body);
16565
+ }
16566
+ async putSchedule(project, body) {
16567
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/schedule`, body);
16568
+ }
16569
+ async getSchedule(project) {
16570
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/schedule`);
16571
+ }
16572
+ async deleteSchedule(project) {
16573
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/schedule`);
16574
+ }
16575
+ async createNotification(project, body) {
16576
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/notifications`, body);
16577
+ }
16578
+ async listNotifications(project) {
16579
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/notifications`);
16580
+ }
16581
+ async deleteNotification(project, id) {
16582
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/notifications/${encodeURIComponent(id)}`);
16583
+ }
16584
+ async testNotification(project, id) {
16585
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/notifications/${encodeURIComponent(id)}/test`);
16586
+ }
16587
+ async addLocation(project, body) {
16588
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/locations`, body);
16589
+ }
16590
+ async listLocations(project) {
16591
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/locations`);
16592
+ }
16593
+ async removeLocation(project, label) {
16594
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/locations/${encodeURIComponent(label)}`);
16595
+ }
16596
+ async setDefaultLocation(project, label) {
16597
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/locations/default`, { label });
16598
+ }
16599
+ async getTelemetry() {
16600
+ return this.request("GET", "/telemetry");
16601
+ }
16602
+ async updateTelemetry(enabled) {
16603
+ return this.request("PUT", "/telemetry", { enabled });
16604
+ }
16605
+ async generateKeywords(project, provider, count) {
16606
+ return this.request(
16607
+ "POST",
16608
+ `/projects/${encodeURIComponent(project)}/keywords/generate`,
16609
+ { provider, count }
16610
+ );
16611
+ }
16612
+ // Google connection management
16613
+ async googleConnect(project, body) {
16614
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/connect`, body);
16615
+ }
16616
+ async googleConnections(project) {
16617
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/connections`);
16618
+ }
16619
+ async googleDisconnect(project, type) {
16620
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/google/connections/${encodeURIComponent(type)}`);
16621
+ }
16622
+ async googleProperties(project) {
16623
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/properties`);
16624
+ }
16625
+ async googleSetProperty(project, type, propertyId) {
16626
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/google/connections/${encodeURIComponent(type)}/property`, { propertyId });
16627
+ }
16628
+ async googleSetSitemap(project, type, sitemapUrl) {
16629
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/google/connections/${encodeURIComponent(type)}/sitemap`, { sitemapUrl });
16630
+ }
16631
+ // GSC data
16632
+ async gscSync(project, body) {
16633
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/sync`, body ?? {});
16634
+ }
16635
+ async gscPerformance(project, params) {
16636
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16637
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/performance${qs}`);
16638
+ }
16639
+ async gscInspect(project, url) {
16640
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/inspect`, { url });
16641
+ }
16642
+ async gscInspections(project, params) {
16643
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16644
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/inspections${qs}`);
16645
+ }
16646
+ async gscDeindexed(project) {
16647
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/deindexed`);
16648
+ }
16649
+ async gscCoverage(project) {
16650
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/coverage`);
16651
+ }
16652
+ async gscCoverageHistory(project, params) {
16653
+ const qs = params?.limit != null ? `?limit=${params.limit}` : "";
16654
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/coverage/history${qs}`);
16655
+ }
16656
+ async gscInspectSitemap(project, body) {
16657
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/inspect-sitemap`, body ?? {});
16658
+ }
16659
+ async gscSitemaps(project) {
16660
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/sitemaps`);
16661
+ }
16662
+ async gscDiscoverSitemaps(project) {
16663
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/discover-sitemaps`, {});
16664
+ }
16665
+ // Analytics
16666
+ async getAnalyticsMetrics(project, window) {
16667
+ const qs = window ? `?window=${encodeURIComponent(window)}` : "";
16668
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/analytics/metrics${qs}`);
16669
+ }
16670
+ async getAnalyticsGaps(project, window) {
16671
+ const qs = window ? `?window=${encodeURIComponent(window)}` : "";
16672
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/analytics/gaps${qs}`);
16673
+ }
16674
+ async getAnalyticsSources(project, window) {
16675
+ const qs = window ? `?window=${encodeURIComponent(window)}` : "";
16676
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/analytics/sources${qs}`);
16677
+ }
16678
+ // Google Indexing API
16679
+ async googleRequestIndexing(project, body) {
16680
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/indexing/request`, body);
16681
+ }
16682
+ // Bing Webmaster Tools
16683
+ async bingConnect(project, body) {
16684
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/bing/connect`, body);
16685
+ }
16686
+ async bingDisconnect(project) {
16687
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/bing/disconnect`);
16688
+ }
16689
+ async bingStatus(project) {
16690
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/status`);
16691
+ }
16692
+ async bingSites(project) {
16693
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/sites`);
16694
+ }
16695
+ async bingSetSite(project, siteUrl) {
16696
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/bing/set-site`, { siteUrl });
16697
+ }
16698
+ async bingCoverage(project) {
16699
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/coverage`);
16700
+ }
16701
+ async bingCoverageHistory(project, params) {
16702
+ const qs = params?.limit != null ? `?limit=${params.limit}` : "";
16703
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/coverage/history${qs}`);
16704
+ }
16705
+ async bingInspections(project, params) {
16706
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16707
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/inspections${qs}`);
16708
+ }
16709
+ async bingInspectUrl(project, url) {
16710
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/bing/inspect-url`, { url });
16711
+ }
16712
+ async bingRequestIndexing(project, body) {
16713
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/bing/request-indexing`, body);
16714
+ }
16715
+ async bingPerformance(project, params) {
16716
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16717
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/performance${qs}`);
16718
+ }
16719
+ // CDP browser provider
16720
+ async getCdpStatus() {
16721
+ return this.request("GET", "/cdp/status");
16722
+ }
16723
+ async cdpScreenshot(query, targets) {
16724
+ return this.request("POST", "/cdp/screenshot", { query, targets });
16725
+ }
16726
+ async getBrowserDiff(project, runId) {
16727
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/runs/${encodeURIComponent(runId)}/browser-diff`);
16728
+ }
16729
+ // Google Analytics 4
16730
+ async gaConnect(project, body) {
16731
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/ga/connect`, body);
16732
+ }
16733
+ async gaDisconnect(project) {
16734
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/ga/disconnect`);
16735
+ }
16736
+ async gaStatus(project) {
16737
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/status`);
16738
+ }
16739
+ async gaSync(project, body) {
16740
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/ga/sync`, body ?? {});
16741
+ }
16742
+ async gaTraffic(project, params) {
16743
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16744
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/traffic${qs}`);
16745
+ }
16746
+ async gaCoverage(project) {
16747
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/coverage`);
16748
+ }
16749
+ async gaAiReferralHistory(project, params) {
16750
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16751
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/ai-referral-history${qs}`);
16752
+ }
16753
+ async gaSocialReferralHistory(project, params) {
16754
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16755
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/social-referral-history${qs}`);
16756
+ }
16757
+ async gaSocialReferralTrend(project) {
16758
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/social-referral-trend`);
16759
+ }
16760
+ async gaAttributionTrend(project) {
16761
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/attribution-trend`);
16762
+ }
16763
+ async gaSessionHistory(project, params) {
16764
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16765
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/session-history${qs}`);
16766
+ }
16767
+ async wordpressConnect(project, body) {
16768
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/connect`, body);
16769
+ }
16770
+ async wordpressDisconnect(project) {
16771
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/wordpress/disconnect`);
16772
+ }
16773
+ async wordpressStatus(project) {
16774
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/status`);
16775
+ }
16776
+ async wordpressPages(project, env) {
16777
+ const qs = env ? `?env=${encodeURIComponent(env)}` : "";
16778
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/pages${qs}`);
16779
+ }
16780
+ async wordpressPage(project, slug, env) {
16781
+ const params = new URLSearchParams({ slug });
16782
+ if (env) params.set("env", env);
16783
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/page?${params.toString()}`);
16784
+ }
16785
+ async wordpressCreatePage(project, body) {
16786
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/pages`, body);
16787
+ }
16788
+ async wordpressUpdatePage(project, body) {
16789
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/wordpress/page`, body);
16790
+ }
16791
+ async wordpressSetMeta(project, body) {
16792
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/page/meta`, body);
16793
+ }
16794
+ async wordpressBulkSetMeta(project, body) {
16795
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/pages/meta/bulk`, body);
16796
+ }
16797
+ async wordpressSchema(project, slug, env) {
16798
+ const params = new URLSearchParams({ slug });
16799
+ if (env) params.set("env", env);
16800
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/schema?${params.toString()}`);
16801
+ }
16802
+ async wordpressSetSchema(project, body) {
16803
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/schema/manual`, body);
16804
+ }
16805
+ async wordpressSchemaDeploy(project, body) {
16806
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/schema/deploy`, body);
16807
+ }
16808
+ async wordpressSchemaStatus(project, env) {
16809
+ const params = new URLSearchParams();
16810
+ if (env) params.set("env", env);
16811
+ const qs = params.toString();
16812
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/schema/status${qs ? `?${qs}` : ""}`);
16813
+ }
16814
+ async wordpressOnboard(project, body) {
16815
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/onboard`, body);
16816
+ }
16817
+ async wordpressLlmsTxt(project, env) {
16818
+ const qs = env ? `?env=${encodeURIComponent(env)}` : "";
16819
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/llms-txt${qs}`);
16820
+ }
16821
+ async wordpressSetLlmsTxt(project, body) {
16822
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/llms-txt/manual`, body);
16823
+ }
16824
+ async wordpressAudit(project, env) {
16825
+ const qs = env ? `?env=${encodeURIComponent(env)}` : "";
16826
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/audit${qs}`);
16827
+ }
16828
+ async wordpressDiff(project, slug) {
16829
+ const params = new URLSearchParams({ slug });
16830
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/diff?${params.toString()}`);
16831
+ }
16832
+ async wordpressStagingStatus(project) {
16833
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/staging/status`);
16834
+ }
16835
+ async wordpressStagingPush(project) {
16836
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/staging/push`);
16837
+ }
16838
+ // ── Intelligence ──────────────────────────────────────────────────────
16839
+ async getInsights(project, opts) {
16840
+ const params = new URLSearchParams();
16841
+ if (opts?.dismissed) params.set("dismissed", "true");
16842
+ if (opts?.runId) params.set("runId", opts.runId);
16843
+ const qs = params.toString();
16844
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/insights${qs ? `?${qs}` : ""}`);
16845
+ }
16846
+ async dismissInsight(project, id) {
16847
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/insights/${encodeURIComponent(id)}/dismiss`);
16848
+ }
16849
+ async getHealth(project) {
16850
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/health/latest`);
16851
+ }
16852
+ async getHealthHistory(project, limit) {
16853
+ const qs = limit ? `?limit=${limit}` : "";
16854
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/health/history${qs}`);
16855
+ }
16856
+ };
16857
+
16858
+ // src/snapshot-service.ts
16859
+ import { runAeoAudit } from "@ainyc/aeo-audit";
16860
+
16861
+ // src/site-fetch.ts
16862
+ import https2 from "https";
16863
+ var FETCH_TIMEOUT_MS = 1e4;
16864
+ var MAX_TEXT_LENGTH = 4e3;
16865
+ var MAX_BODY_BYTES = 512e3;
16866
+ var USER_AGENT = "Canonry/1.0 (site-analysis)";
16867
+ function extractHostname(domain) {
16868
+ let hostname = domain;
16869
+ try {
16870
+ if (hostname.includes("://")) {
16871
+ hostname = new URL(hostname).hostname;
16872
+ }
16873
+ } catch {
16874
+ }
16875
+ return hostname.replace(/^www\./, "");
16876
+ }
16877
+ function fetchWithPinnedAddress(target) {
16878
+ return new Promise((resolve) => {
16879
+ const port = target.url.port ? Number(target.url.port) : 443;
16880
+ const path10 = target.url.pathname + target.url.search;
16881
+ const req = https2.request(
16882
+ {
16883
+ hostname: target.address,
16884
+ family: target.family,
16885
+ port,
16886
+ path: path10,
16887
+ method: "GET",
16888
+ timeout: FETCH_TIMEOUT_MS,
16889
+ servername: target.url.hostname,
16890
+ // SNI for TLS
16891
+ headers: {
16892
+ Host: target.url.host,
16893
+ "User-Agent": USER_AGENT,
16894
+ Accept: "text/html"
16895
+ }
16896
+ },
16897
+ (res) => {
16898
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
16899
+ if (res.statusCode >= 300 && res.statusCode < 400) {
16900
+ const location = res.headers.location ?? "";
16901
+ res.resume();
16902
+ resolve(`REDIRECT:${location}`);
16903
+ return;
16904
+ }
16905
+ res.resume();
16906
+ resolve("");
16907
+ return;
16908
+ }
16909
+ const contentType = res.headers["content-type"] ?? "";
16910
+ if (!contentType.includes("text/html")) {
16911
+ res.resume();
16912
+ resolve("");
16913
+ return;
16914
+ }
16915
+ const chunks = [];
16916
+ let totalBytes = 0;
16917
+ res.on("data", (chunk) => {
16918
+ totalBytes += chunk.length;
16919
+ if (totalBytes <= MAX_BODY_BYTES) {
16920
+ chunks.push(chunk);
16921
+ }
16922
+ });
16923
+ res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
16924
+ res.on("error", () => resolve(""));
16925
+ }
16926
+ );
16927
+ req.on("timeout", () => req.destroy(new Error("timeout")));
16928
+ req.on("error", () => resolve(""));
16929
+ req.end();
16930
+ });
16931
+ }
16932
+ async function fetchSiteText(domain) {
16933
+ const hostname = extractHostname(domain);
16934
+ const url = `https://${hostname}`;
16935
+ const targetCheck = await resolveWebhookTarget(url);
16936
+ if (!targetCheck.ok) return "";
16937
+ try {
16938
+ const result = await fetchWithPinnedAddress(targetCheck.target);
16939
+ if (result.startsWith("REDIRECT:")) {
16940
+ const location = result.slice("REDIRECT:".length);
16941
+ if (!location) return "";
16942
+ const redirectUrl = new URL(location, url).href;
16943
+ const redirectCheck = await resolveWebhookTarget(redirectUrl);
16944
+ if (!redirectCheck.ok) return "";
16945
+ const redirectResult = await fetchWithPinnedAddress(redirectCheck.target);
16946
+ if (redirectResult.startsWith("REDIRECT:")) return "";
16947
+ return stripHtml2(redirectResult);
16948
+ }
16949
+ return stripHtml2(result);
16950
+ } catch {
16951
+ return "";
16952
+ }
16953
+ }
16954
+ function stripHtml2(html) {
16955
+ if (!html) return "";
16956
+ let text = html.replace(/<script[\s\S]*?<\/script>/gi, " ");
16957
+ text = text.replace(/<style[\s\S]*?<\/style>/gi, " ");
16958
+ text = text.replace(/<[^>]+>/g, " ");
16959
+ text = text.replace(/&amp;/g, "&");
16960
+ text = text.replace(/&lt;/g, "<");
16961
+ text = text.replace(/&gt;/g, ">");
16962
+ text = text.replace(/&quot;/g, '"');
16963
+ text = text.replace(/&#39;/g, "'");
16964
+ text = text.replace(/&nbsp;/g, " ");
16965
+ text = text.replace(/\s+/g, " ").trim();
16966
+ if (text.length > MAX_TEXT_LENGTH) {
16967
+ text = text.slice(0, MAX_TEXT_LENGTH);
16968
+ }
16969
+ return text;
16970
+ }
16971
+
16972
+ // src/snapshot-format.ts
16973
+ function formatAuditFactorScore(factor) {
16974
+ return `${factor.score}/100 (${factor.weight}% weight)`;
16975
+ }
16976
+
16977
+ // src/snapshot-service.ts
16978
+ var log8 = createLogger("Snapshot");
16979
+ var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
16980
+ var SNAPSHOT_QUERY_COUNT = 6;
16981
+ var ProviderExecutionGate2 = class {
16982
+ constructor(maxConcurrency, maxPerMinute) {
16983
+ this.maxConcurrency = maxConcurrency;
16984
+ this.maxPerMinute = maxPerMinute;
16985
+ }
16986
+ window = [];
16987
+ waiters = [];
16988
+ rateLimitChain = Promise.resolve();
16989
+ inFlight = 0;
14817
16990
  async run(task) {
14818
16991
  await this.acquire();
14819
16992
  try {
@@ -14945,7 +17118,7 @@ var SnapshotService = class {
14945
17118
  return mapAuditReport(report);
14946
17119
  } catch (err) {
14947
17120
  const message = err instanceof Error ? err.message : String(err);
14948
- log7.warn("audit.failed", { homepageUrl, error: message });
17121
+ log8.warn("audit.failed", { homepageUrl, error: message });
14949
17122
  return {
14950
17123
  url: homepageUrl,
14951
17124
  finalUrl: homepageUrl,
@@ -14975,7 +17148,7 @@ var SnapshotService = class {
14975
17148
  phrases: parsedPhrases
14976
17149
  };
14977
17150
  } catch (err) {
14978
- log7.warn("profile.generation-failed", {
17151
+ log8.warn("profile.generation-failed", {
14979
17152
  domain: ctx.domain,
14980
17153
  provider: ctx.analysisProvider.adapter.name,
14981
17154
  error: err instanceof Error ? err.message : String(err)
@@ -15117,7 +17290,7 @@ var SnapshotService = class {
15117
17290
  recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
15118
17291
  };
15119
17292
  } catch (err) {
15120
- log7.warn("response.analysis-failed", {
17293
+ log8.warn("response.analysis-failed", {
15121
17294
  provider: ctx.analysisProvider.adapter.name,
15122
17295
  error: err instanceof Error ? err.message : String(err)
15123
17296
  });
@@ -15399,499 +17572,6 @@ function clipText(value, length) {
15399
17572
  return `${value.slice(0, length - 3)}...`;
15400
17573
  }
15401
17574
 
15402
- // src/agent-manager.ts
15403
- import { execFileSync, spawn } from "child_process";
15404
- import fs5 from "fs";
15405
- import path6 from "path";
15406
- var log8 = createLogger("AgentManager");
15407
- var PROCESS_MARKER = "canonry-openclaw-gateway";
15408
- var AgentManager = class {
15409
- constructor(config, stateDir) {
15410
- this.config = config;
15411
- this.stateDir = stateDir;
15412
- this.processJsonPath = path6.join(stateDir, "process.json");
15413
- }
15414
- processJsonPath;
15415
- /**
15416
- * Check if the gateway process is running.
15417
- * Cleans up stale process.json if the process is dead or belongs to a
15418
- * different process (PID reuse).
15419
- */
15420
- status() {
15421
- const info = this.readProcessInfo();
15422
- if (!info) {
15423
- return { state: "stopped" };
15424
- }
15425
- if (info.marker !== PROCESS_MARKER) {
15426
- this.removeProcessJson();
15427
- return { state: "stopped" };
15428
- }
15429
- if (isProcessAlive(info.pid) && this.verifyProcessIdentity(info.pid)) {
15430
- return {
15431
- state: "running",
15432
- pid: info.pid,
15433
- port: info.gatewayPort,
15434
- startedAt: info.startedAt
15435
- };
15436
- }
15437
- this.removeProcessJson();
15438
- return { state: "stopped" };
15439
- }
15440
- /**
15441
- * Start the OpenClaw gateway as a detached background process.
15442
- * Idempotent — no-op if already running.
15443
- * Waits briefly for the process to confirm it hasn't crashed on startup.
15444
- */
15445
- async start() {
15446
- const currentStatus = this.status();
15447
- if (currentStatus.state === "running") {
15448
- log8.info("already.running", { pid: currentStatus.pid });
15449
- return;
15450
- }
15451
- const binary = this.config.binary ?? "openclaw";
15452
- const profile = this.config.profile ?? "aero";
15453
- const port = this.config.gatewayPort ?? 3579;
15454
- if (!fs5.existsSync(this.stateDir)) {
15455
- fs5.mkdirSync(this.stateDir, { recursive: true });
15456
- }
15457
- const logFile = path6.join(this.stateDir, "gateway.log");
15458
- const logFd = fs5.openSync(logFile, "a");
15459
- const dotEnv = this.loadDotEnv();
15460
- const child = spawn(binary, ["--profile", profile, "gateway"], {
15461
- detached: true,
15462
- stdio: ["ignore", logFd, logFd],
15463
- env: {
15464
- ...process.env,
15465
- ...dotEnv,
15466
- OPENCLAW_PROFILE: profile,
15467
- OPENCLAW_GATEWAY_PORT: String(port),
15468
- OPENCLAW_STATE_DIR: this.stateDir
15469
- }
15470
- });
15471
- const startupResult = await new Promise((resolve) => {
15472
- let settled = false;
15473
- const settle = (r) => {
15474
- if (settled) return;
15475
- settled = true;
15476
- resolve(r);
15477
- };
15478
- child.on("error", (err) => settle({ error: err }));
15479
- child.on("exit", (code) => settle({ exitCode: code }));
15480
- setTimeout(() => settle({}), 500);
15481
- });
15482
- child.unref();
15483
- fs5.closeSync(logFd);
15484
- if (startupResult.error) {
15485
- throw new Error(`Failed to start OpenClaw gateway: ${startupResult.error.message}`);
15486
- }
15487
- if (startupResult.exitCode != null) {
15488
- throw new Error(`OpenClaw gateway exited immediately (code ${startupResult.exitCode}). Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
15489
- }
15490
- if (child.pid == null) {
15491
- throw new Error("Failed to start OpenClaw gateway: no PID returned by spawn");
15492
- }
15493
- if (!isProcessAlive(child.pid)) {
15494
- throw new Error(`OpenClaw gateway exited immediately after spawn. Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
15495
- }
15496
- const processInfo = {
15497
- pid: child.pid,
15498
- gatewayPort: port,
15499
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
15500
- marker: PROCESS_MARKER
15501
- };
15502
- fs5.writeFileSync(this.processJsonPath, JSON.stringify(processInfo, null, 2), "utf-8");
15503
- log8.info("started", { pid: child.pid, port });
15504
- }
15505
- /**
15506
- * Stop the gateway process.
15507
- * Uses DenchClaw escalation: SIGTERM → 800ms poll → SIGKILL.
15508
- * Idempotent — no-op if already stopped.
15509
- */
15510
- async stop() {
15511
- const info = this.readProcessInfo();
15512
- if (!info) return;
15513
- if (isProcessAlive(info.pid) && info.marker === PROCESS_MARKER && this.verifyProcessIdentity(info.pid)) {
15514
- await terminateWithEscalation(info.pid);
15515
- }
15516
- this.removeProcessJson();
15517
- log8.info("stopped", { pid: info.pid });
15518
- }
15519
- /**
15520
- * Stop the gateway, wipe the workspace directory, and prepare for re-seeding.
15521
- */
15522
- async reset() {
15523
- await this.stop();
15524
- const workspaceDir = path6.join(this.stateDir, "workspace");
15525
- if (fs5.existsSync(workspaceDir)) {
15526
- fs5.rmSync(workspaceDir, { recursive: true, force: true });
15527
- log8.info("workspace.wiped", { dir: workspaceDir });
15528
- }
15529
- }
15530
- /**
15531
- * Verify that the PID actually belongs to an openclaw process by checking
15532
- * the full command line. Requires "openclaw" in the args to avoid matching
15533
- * unrelated Node processes after PID reuse.
15534
- */
15535
- verifyProcessIdentity(pid) {
15536
- try {
15537
- if (process.platform === "darwin") {
15538
- const out = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
15539
- encoding: "utf-8",
15540
- timeout: 2e3
15541
- }).trim();
15542
- return out.includes("openclaw");
15543
- }
15544
- if (process.platform === "linux") {
15545
- const cmdline = fs5.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
15546
- return cmdline.includes("openclaw");
15547
- }
15548
- return true;
15549
- } catch {
15550
- return false;
15551
- }
15552
- }
15553
- readProcessInfo() {
15554
- if (!fs5.existsSync(this.processJsonPath)) return null;
15555
- try {
15556
- return JSON.parse(fs5.readFileSync(this.processJsonPath, "utf-8"));
15557
- } catch {
15558
- return null;
15559
- }
15560
- }
15561
- removeProcessJson() {
15562
- try {
15563
- fs5.unlinkSync(this.processJsonPath);
15564
- } catch {
15565
- }
15566
- }
15567
- /** Parse a simple KEY=value dotenv file from the state dir. */
15568
- loadDotEnv() {
15569
- const envFile = path6.join(this.stateDir, ".env");
15570
- if (!fs5.existsSync(envFile)) return {};
15571
- const result = {};
15572
- for (const line of fs5.readFileSync(envFile, "utf-8").split("\n")) {
15573
- const trimmed = line.trim();
15574
- if (!trimmed || trimmed.startsWith("#")) continue;
15575
- const eq25 = trimmed.indexOf("=");
15576
- if (eq25 < 1) continue;
15577
- result[trimmed.slice(0, eq25)] = trimmed.slice(eq25 + 1);
15578
- }
15579
- return result;
15580
- }
15581
- };
15582
- function isProcessAlive(pid) {
15583
- try {
15584
- process.kill(pid, 0);
15585
- return true;
15586
- } catch (err) {
15587
- if (err.code === "EPERM") return true;
15588
- return false;
15589
- }
15590
- }
15591
- async function terminateWithEscalation(pid) {
15592
- try {
15593
- process.kill(pid, "SIGTERM");
15594
- } catch {
15595
- return;
15596
- }
15597
- const deadline = Date.now() + 800;
15598
- while (Date.now() < deadline) {
15599
- if (!isProcessAlive(pid)) return;
15600
- await new Promise((resolve) => setTimeout(resolve, 100));
15601
- }
15602
- try {
15603
- process.kill(pid, "SIGKILL");
15604
- } catch {
15605
- }
15606
- }
15607
-
15608
- // src/agent-bootstrap.ts
15609
- import { execFileSync as execFileSync2, execSync } from "child_process";
15610
- import fs6 from "fs";
15611
- import os5 from "os";
15612
- import path7 from "path";
15613
- import { fileURLToPath } from "url";
15614
- var CACHE_TTL_MS = 5 * 60 * 1e3;
15615
- var OPENCLAW_VERSION = "2026.4.14";
15616
- var OPENCLAW_PACKAGE_SPEC = `openclaw@${OPENCLAW_VERSION}`;
15617
- var MIN_NODE_VERSION = "22.14.0";
15618
- var cachedResult = null;
15619
- var cachedAt = 0;
15620
- function getAeroStateDir(profile = "aero") {
15621
- return path7.join(os5.homedir(), `.openclaw-${profile}`);
15622
- }
15623
- async function detectOpenClaw(config) {
15624
- if (cachedResult && Date.now() - cachedAt < CACHE_TTL_MS) {
15625
- return cachedResult;
15626
- }
15627
- let result;
15628
- if (config?.binary) {
15629
- const version = probeVersion(config.binary);
15630
- if (version) {
15631
- result = { found: true, path: config.binary, version };
15632
- cachedResult = result;
15633
- cachedAt = Date.now();
15634
- return result;
15635
- }
15636
- }
15637
- const binaryPath = findInPath();
15638
- if (binaryPath) {
15639
- const version = probeVersion(binaryPath);
15640
- if (version) {
15641
- result = { found: true, path: binaryPath, version };
15642
- cachedResult = result;
15643
- cachedAt = Date.now();
15644
- return result;
15645
- }
15646
- }
15647
- result = { found: false };
15648
- cachedResult = result;
15649
- cachedAt = Date.now();
15650
- return result;
15651
- }
15652
- detectOpenClaw.resetCache = () => {
15653
- cachedResult = null;
15654
- cachedAt = 0;
15655
- };
15656
- function probeVersion(binaryPath) {
15657
- try {
15658
- const output = execFileSync2(binaryPath, ["--version"], {
15659
- timeout: 5e3,
15660
- encoding: "utf-8"
15661
- });
15662
- const match = output.toString().trim().match(/(\d+\.\d+\.\d+)/);
15663
- return match ? match[1] : output.toString().trim();
15664
- } catch {
15665
- return null;
15666
- }
15667
- }
15668
- function findInPath() {
15669
- const cmd = process.platform === "win32" ? "where" : "which";
15670
- try {
15671
- const output = execFileSync2(cmd, ["openclaw"], {
15672
- timeout: 5e3,
15673
- encoding: "utf-8"
15674
- });
15675
- return output.toString().trim().split("\n")[0] || null;
15676
- } catch {
15677
- return null;
15678
- }
15679
- }
15680
- async function installOpenClaw(opts) {
15681
- const unsupportedNodeError = getUnsupportedNodeError(opts?.nodeVersion);
15682
- if (unsupportedNodeError) {
15683
- return {
15684
- success: false,
15685
- error: unsupportedNodeError
15686
- };
15687
- }
15688
- try {
15689
- execSync(`npm install -g ${OPENCLAW_PACKAGE_SPEC}`, {
15690
- timeout: 12e4,
15691
- stdio: opts?.silent ? "pipe" : "inherit"
15692
- });
15693
- } catch (err) {
15694
- return {
15695
- success: false,
15696
- error: err instanceof Error ? err.message : String(err)
15697
- };
15698
- }
15699
- detectOpenClaw.resetCache();
15700
- const detection = await detectOpenClaw();
15701
- if (!detection.found) {
15702
- return {
15703
- success: false,
15704
- error: `npm install succeeded but the ${OPENCLAW_PACKAGE_SPEC} binary was not found in PATH`
15705
- };
15706
- }
15707
- if (detection.version) {
15708
- const expectedVersion = parseVersionTuple(OPENCLAW_VERSION);
15709
- const detectedVersion = parseVersionTuple(detection.version);
15710
- if (expectedVersion && detectedVersion && compareVersionTuples(detectedVersion, expectedVersion) !== 0) {
15711
- return {
15712
- success: false,
15713
- 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.`
15714
- };
15715
- }
15716
- }
15717
- return { success: true, detection };
15718
- }
15719
- function getUnsupportedNodeError(currentNodeVersionOverride) {
15720
- const currentNodeVersion = normalizeVersion(currentNodeVersionOverride ?? process.versions.node);
15721
- const minimumTuple = parseVersionTuple(MIN_NODE_VERSION);
15722
- const currentTuple = parseVersionTuple(currentNodeVersion);
15723
- if (!minimumTuple || !currentTuple || compareVersionTuples(currentTuple, minimumTuple) >= 0) {
15724
- return null;
15725
- }
15726
- 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".`;
15727
- }
15728
- function normalizeVersion(version) {
15729
- const tuple = parseVersionTuple(version);
15730
- if (!tuple) {
15731
- return version.trim().replace(/^v/i, "");
15732
- }
15733
- return tuple.join(".");
15734
- }
15735
- function parseVersionTuple(version) {
15736
- const match = version.trim().replace(/^v/i, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
15737
- if (!match) {
15738
- return null;
15739
- }
15740
- return [
15741
- Number(match[1]),
15742
- Number(match[2] ?? 0),
15743
- Number(match[3] ?? 0)
15744
- ];
15745
- }
15746
- function compareVersionTuples(left, right) {
15747
- for (let index = 0; index < left.length; index++) {
15748
- const delta = left[index] - right[index];
15749
- if (delta !== 0) {
15750
- return delta;
15751
- }
15752
- }
15753
- return 0;
15754
- }
15755
- function seedWorkspace(stateDir) {
15756
- const workspaceDir = path7.join(stateDir, "workspace");
15757
- fs6.mkdirSync(workspaceDir, { recursive: true });
15758
- const __dirname = path7.dirname(fileURLToPath(import.meta.url));
15759
- const assetsDir = path7.join(__dirname, "..", "assets", "agent-workspace");
15760
- if (!fs6.existsSync(assetsDir)) {
15761
- return;
15762
- }
15763
- copyDirRecursive(assetsDir, workspaceDir);
15764
- }
15765
- function initializeOpenClawProfile(binary, profile, workspaceDir) {
15766
- try {
15767
- execFileSync2(binary, [
15768
- "--profile",
15769
- profile,
15770
- "onboard",
15771
- "--non-interactive",
15772
- "--accept-risk",
15773
- "--mode",
15774
- "local",
15775
- "--workspace",
15776
- workspaceDir,
15777
- "--skip-channels",
15778
- "--skip-skills",
15779
- "--skip-health",
15780
- "--no-install-daemon"
15781
- ], { timeout: 3e4, stdio: "pipe" });
15782
- } catch (err) {
15783
- const stderr = err instanceof Error && "stderr" in err ? String(err.stderr) : "";
15784
- if (stderr.toLowerCase().includes("already")) return;
15785
- throw new CliError({
15786
- code: "AGENT_PROFILE_INIT_FAILED",
15787
- message: `Failed to initialize OpenClaw profile: ${stderr || (err instanceof Error ? err.message : String(err))}`,
15788
- displayMessage: `Failed to initialize OpenClaw profile "${profile}".`
15789
- });
15790
- }
15791
- }
15792
- function configureOpenClawGateway(binary, profile, gatewayPort) {
15793
- const entries = [
15794
- ["gateway.mode", "local", false],
15795
- ["gateway.port", String(gatewayPort), true]
15796
- ];
15797
- for (const [key, value, strict] of entries) {
15798
- try {
15799
- const args = ["--profile", profile, "config", "set", key, value];
15800
- if (strict) args.push("--strict-json");
15801
- execFileSync2(binary, args, { timeout: 1e4, stdio: "pipe" });
15802
- } catch (err) {
15803
- throw new CliError({
15804
- code: "AGENT_GATEWAY_CONFIG_FAILED",
15805
- message: `Failed to set ${key}=${value}: ${err instanceof Error ? err.message : String(err)}`,
15806
- displayMessage: `Failed to configure OpenClaw gateway (${key}).`
15807
- });
15808
- }
15809
- }
15810
- }
15811
- function setOpenClawModel(binary, profile, model) {
15812
- try {
15813
- execFileSync2(binary, [
15814
- "--profile",
15815
- profile,
15816
- "models",
15817
- "set",
15818
- model
15819
- ], { timeout: 1e4, stdio: "pipe" });
15820
- } catch (err) {
15821
- throw new CliError({
15822
- code: "AGENT_MODEL_SET_FAILED",
15823
- message: `Failed to set agent model to ${model}: ${err instanceof Error ? err.message : String(err)}`,
15824
- displayMessage: `Failed to set agent model to "${model}".`
15825
- });
15826
- }
15827
- }
15828
- function providerEnvVar(provider) {
15829
- const map = {
15830
- anthropic: "ANTHROPIC_API_KEY",
15831
- openai: "OPENAI_API_KEY",
15832
- google: "GOOGLE_API_KEY",
15833
- "google-vertex": "GOOGLE_API_KEY",
15834
- groq: "GROQ_API_KEY",
15835
- mistral: "MISTRAL_API_KEY",
15836
- xai: "XAI_API_KEY",
15837
- openrouter: "OPENROUTER_API_KEY",
15838
- cerebras: "CEREBRAS_API_KEY"
15839
- };
15840
- return map[provider] ?? `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
15841
- }
15842
- function writeAgentEnv(stateDir, key, value) {
15843
- const envFile = path7.join(stateDir, ".env");
15844
- let lines = [];
15845
- if (fs6.existsSync(envFile)) {
15846
- lines = fs6.readFileSync(envFile, "utf-8").split("\n");
15847
- }
15848
- const prefix = `${key}=`;
15849
- const idx = lines.findIndex((l) => l.startsWith(prefix));
15850
- const entry = `${key}=${value}`;
15851
- if (idx >= 0) {
15852
- lines[idx] = entry;
15853
- } else {
15854
- lines.push(entry);
15855
- }
15856
- while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
15857
- fs6.writeFileSync(envFile, lines.join("\n") + "\n", "utf-8");
15858
- }
15859
- function resolveAgentCredentials(opts) {
15860
- const provider = opts.agentProvider ?? "anthropic";
15861
- if (opts.agentKey) {
15862
- return { provider, key: opts.agentKey, model: opts.agentModel };
15863
- }
15864
- const envVar = providerEnvVar(provider);
15865
- const envKey = process.env[envVar];
15866
- if (envKey) {
15867
- return { provider, key: envKey, model: opts.agentModel };
15868
- }
15869
- const genericKey = process.env.CANONRY_AGENT_KEY;
15870
- if (genericKey) {
15871
- return { provider, key: genericKey, model: opts.agentModel };
15872
- }
15873
- const envFile = path7.join(opts.stateDir, ".env");
15874
- if (fs6.existsSync(envFile)) {
15875
- const hasKey = fs6.readFileSync(envFile, "utf-8").split("\n").some((l) => l.includes("_API_KEY="));
15876
- if (hasKey) {
15877
- return { provider, key: void 0, model: opts.agentModel };
15878
- }
15879
- }
15880
- return { provider, key: void 0, model: opts.agentModel };
15881
- }
15882
- function copyDirRecursive(src, dest) {
15883
- fs6.mkdirSync(dest, { recursive: true });
15884
- for (const entry of fs6.readdirSync(src, { withFileTypes: true })) {
15885
- const srcPath = path7.join(src, entry.name);
15886
- const destPath = path7.join(dest, entry.name);
15887
- if (entry.isDirectory()) {
15888
- copyDirRecursive(srcPath, destPath);
15889
- } else {
15890
- fs6.copyFileSync(srcPath, destPath);
15891
- }
15892
- }
15893
- }
15894
-
15895
17575
  // src/server.ts
15896
17576
  var _require2 = createRequire2(import.meta.url);
15897
17577
  var { version: PKG_VERSION } = _require2("../package.json");
@@ -15926,7 +17606,7 @@ function summarizeProviderConfig(provider, config) {
15926
17606
  };
15927
17607
  }
15928
17608
  function hashApiKey(key) {
15929
- return crypto23.createHash("sha256").update(key).digest("hex");
17609
+ return crypto24.createHash("sha256").update(key).digest("hex");
15930
17610
  }
15931
17611
  function parseCookies2(header) {
15932
17612
  if (!header) return {};
@@ -16073,27 +17753,35 @@ async function createServer(opts) {
16073
17753
  jobRunner.recoverStaleRuns();
16074
17754
  const notifier = new Notifier(opts.db, serverUrl);
16075
17755
  const intelligenceService = new IntelligenceService(opts.db);
17756
+ const aeroClient = new ApiClient(opts.config.apiUrl, opts.config.apiKey, { skipProbe: true });
17757
+ const sessionRegistry = new SessionRegistry({
17758
+ db: opts.db,
17759
+ client: aeroClient,
17760
+ config: opts.config
17761
+ });
16076
17762
  const runCoordinator = new RunCoordinator(
16077
17763
  notifier,
16078
17764
  intelligenceService,
16079
- (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result)
17765
+ (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
17766
+ async ({ runId, projectId, insightCount, criticalOrHigh }) => {
17767
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq26(projects.id, projectId)).get();
17768
+ if (!project) return;
17769
+ sessionRegistry.queueFollowUp(project.name, {
17770
+ role: "user",
17771
+ content: `[system] Run ${runId} completed for project ${project.name}. ${insightCount} insights generated (${criticalOrHigh} critical/high). Use get_run to inspect the run and get_insights to review new findings. Surface anything notable briefly \u2014 skip chit-chat.`,
17772
+ timestamp: Date.now()
17773
+ });
17774
+ void sessionRegistry.drainNow(project.name);
17775
+ }
16080
17776
  );
16081
17777
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
16082
17778
  const snapshotService = new SnapshotService(registry);
16083
- let agentManager;
16084
- let agentAutoStarted = false;
16085
- if (opts.config.agent) {
16086
- const stateDir = getAeroStateDir(opts.config.agent.profile ?? "aero");
16087
- agentManager = new AgentManager(opts.config.agent, stateDir);
16088
- if (opts.config.agent.autoStart) {
16089
- try {
16090
- await agentManager.start();
16091
- agentAutoStarted = true;
16092
- app.log.info({ pid: agentManager.status().pid }, "Agent gateway started");
16093
- } catch (err) {
16094
- app.log.error({ err }, "Failed to auto-start agent gateway");
16095
- }
16096
- }
17779
+ const orphanedOpenClawDir = path9.join(os5.homedir(), ".openclaw-aero");
17780
+ if (fs8.existsSync(orphanedOpenClawDir)) {
17781
+ app.log.warn(
17782
+ { path: orphanedOpenClawDir },
17783
+ "OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
17784
+ );
16097
17785
  }
16098
17786
  const scheduler = new Scheduler(opts.db, {
16099
17787
  onRunCreated: (runId, projectId, providers2, location) => {
@@ -16170,7 +17858,7 @@ async function createServer(opts) {
16170
17858
  return removed;
16171
17859
  }
16172
17860
  };
16173
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
17861
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto24.randomBytes(32).toString("hex");
16174
17862
  const googleConnectionStore = {
16175
17863
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
16176
17864
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -16216,11 +17904,11 @@ async function createServer(opts) {
16216
17904
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
16217
17905
  if (opts.config.apiKey) {
16218
17906
  const keyHash = hashApiKey(opts.config.apiKey);
16219
- const existing = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, keyHash)).get();
17907
+ const existing = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, keyHash)).get();
16220
17908
  if (!existing) {
16221
17909
  const prefix = opts.config.apiKey.slice(0, 12);
16222
17910
  opts.db.insert(apiKeys).values({
16223
- id: `key_${crypto23.randomBytes(8).toString("hex")}`,
17911
+ id: `key_${crypto24.randomBytes(8).toString("hex")}`,
16224
17912
  name: "default",
16225
17913
  keyHash,
16226
17914
  keyPrefix: prefix,
@@ -16244,7 +17932,7 @@ async function createServer(opts) {
16244
17932
  };
16245
17933
  const createSession = (apiKeyId) => {
16246
17934
  pruneExpiredSessions();
16247
- const sessionId = crypto23.randomBytes(32).toString("hex");
17935
+ const sessionId = crypto24.randomBytes(32).toString("hex");
16248
17936
  sessions.set(sessionId, {
16249
17937
  apiKeyId,
16250
17938
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -16268,7 +17956,7 @@ async function createServer(opts) {
16268
17956
  };
16269
17957
  const getDefaultApiKey = () => {
16270
17958
  if (!opts.config.apiKey) return void 0;
16271
- return opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
17959
+ return opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
16272
17960
  };
16273
17961
  const createPasswordSession = (reply) => {
16274
17962
  const key = getDefaultApiKey();
@@ -16325,12 +18013,12 @@ async function createServer(opts) {
16325
18013
  return reply.send({ authenticated: true });
16326
18014
  }
16327
18015
  if (apiKey) {
16328
- const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(apiKey))).get();
18016
+ const key = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey(apiKey))).get();
16329
18017
  if (!key || key.revokedAt) {
16330
18018
  const err2 = authInvalid();
16331
18019
  return reply.status(err2.statusCode).send(err2.toJSON());
16332
18020
  }
16333
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(apiKeys.id, key.id)).run();
18021
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(apiKeys.id, key.id)).run();
16334
18022
  const sessionId = createSession(key.id);
16335
18023
  reply.header("set-cookie", serializeSessionCookie({
16336
18024
  name: SESSION_COOKIE_NAME,
@@ -16362,6 +18050,11 @@ async function createServer(opts) {
16362
18050
  skipAuth: false,
16363
18051
  sessionCookieName: SESSION_COOKIE_NAME,
16364
18052
  resolveSessionApiKeyId,
18053
+ // Local-only Aero agent routes. Registered here so they inherit api-routes'
18054
+ // auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
18055
+ registerAuthenticatedRoutes: async (scope) => {
18056
+ registerAgentRoutes(scope, { db: opts.db, sessionRegistry });
18057
+ },
16365
18058
  getGoogleAuthConfig: () => getGoogleAuthConfig(opts.config),
16366
18059
  googleConnectionStore,
16367
18060
  googleStateSecret,
@@ -16394,7 +18087,8 @@ async function createServer(opts) {
16394
18087
  },
16395
18088
  openApiInfo: {
16396
18089
  title: "Canonry API",
16397
- version: PKG_VERSION
18090
+ version: PKG_VERSION,
18091
+ includeCanonryLocal: true
16398
18092
  },
16399
18093
  providerSummary,
16400
18094
  providerAdapters: [...API_ADAPTERS, ...BROWSER_ADAPTERS].map((a) => ({
@@ -16471,7 +18165,7 @@ async function createServer(opts) {
16471
18165
  const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
16472
18166
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
16473
18167
  opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
16474
- id: crypto23.randomUUID(),
18168
+ id: crypto24.randomUUID(),
16475
18169
  projectId,
16476
18170
  actor: "api",
16477
18171
  action: existing ? "provider.updated" : "provider.created",
@@ -16518,17 +18212,6 @@ async function createServer(opts) {
16518
18212
  onProjectDeleted: (projectId) => {
16519
18213
  scheduler.remove(projectId);
16520
18214
  },
16521
- onProjectUpserted: agentManager && opts.config.agent?.autoStart ? (_projectId, projectName) => {
16522
- try {
16523
- const gatewayPort = opts.config.agent?.gatewayPort ?? 3579;
16524
- const result = attachAgentWebhookDirect(opts.db, _projectId, gatewayPort);
16525
- if (result === "attached") {
16526
- app.log.info({ projectName }, "Auto-attached agent webhook");
16527
- }
16528
- } catch (err) {
16529
- app.log.error({ err, projectName }, "Failed to auto-attach agent webhook");
16530
- }
16531
- } : void 0,
16532
18215
  getTelemetryStatus: () => {
16533
18216
  const enabled = isTelemetryEnabled();
16534
18217
  return {
@@ -16613,15 +18296,15 @@ async function createServer(opts) {
16613
18296
  return snapshotService.createReport(input);
16614
18297
  }
16615
18298
  });
16616
- const dirname = path8.dirname(fileURLToPath2(import.meta.url));
16617
- const assetsDir = path8.join(dirname, "..", "assets");
16618
- if (fs7.existsSync(assetsDir)) {
16619
- const indexPath = path8.join(assetsDir, "index.html");
18299
+ const dirname = path9.dirname(fileURLToPath2(import.meta.url));
18300
+ const assetsDir = path9.join(dirname, "..", "assets");
18301
+ if (fs8.existsSync(assetsDir)) {
18302
+ const indexPath = path9.join(assetsDir, "index.html");
16620
18303
  const injectConfig = (html) => {
16621
18304
  const clientConfig = {};
16622
18305
  if (basePath) clientConfig.basePath = basePath;
16623
18306
  const configScript = `<script>window.__CANONRY_CONFIG__=${JSON.stringify(clientConfig)}</script>`;
16624
- const baseTag = basePath ? `<base href="${basePath}">` : "";
18307
+ const baseTag = `<base href="${basePath ?? "/"}">`;
16625
18308
  return html.replace("<head>", `<head>${baseTag}`).replace("</head>", `${configScript}</head>`);
16626
18309
  };
16627
18310
  const fastifyStatic = await import("@fastify/static");
@@ -16634,8 +18317,8 @@ async function createServer(opts) {
16634
18317
  index: false
16635
18318
  });
16636
18319
  const serveIndex = (_request, reply) => {
16637
- if (fs7.existsSync(indexPath)) {
16638
- const html = fs7.readFileSync(indexPath, "utf-8");
18320
+ if (fs8.existsSync(indexPath)) {
18321
+ const html = fs8.readFileSync(indexPath, "utf-8");
16639
18322
  return reply.type("text/html").send(injectConfig(html));
16640
18323
  }
16641
18324
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -16655,8 +18338,8 @@ async function createServer(opts) {
16655
18338
  if (basePath && !url.startsWith(basePath)) {
16656
18339
  return reply.status(404).send({ error: "Not found", path: request.url });
16657
18340
  }
16658
- if (fs7.existsSync(indexPath)) {
16659
- const html = fs7.readFileSync(indexPath, "utf-8");
18341
+ if (fs8.existsSync(indexPath)) {
18342
+ const html = fs8.readFileSync(indexPath, "utf-8");
16660
18343
  return reply.type("text/html").send(injectConfig(html));
16661
18344
  }
16662
18345
  return reply.status(404).send({ error: "Not found" });
@@ -16675,13 +18358,6 @@ async function createServer(opts) {
16675
18358
  scheduler.start();
16676
18359
  app.addHook("onClose", async () => {
16677
18360
  scheduler.stop();
16678
- if (agentManager && agentAutoStarted) {
16679
- try {
16680
- await agentManager.stop();
16681
- } catch (err) {
16682
- app.log.error({ err }, "Failed to stop agent gateway");
16683
- }
16684
- }
16685
18361
  });
16686
18362
  return app;
16687
18363
  }
@@ -16743,7 +18419,6 @@ export {
16743
18419
  isFirstRun,
16744
18420
  showFirstRunNotice,
16745
18421
  trackEvent,
16746
- EXIT_USER_ERROR,
16747
18422
  EXIT_SYSTEM_ERROR,
16748
18423
  CliError,
16749
18424
  usageError,
@@ -16762,21 +18437,10 @@ export {
16762
18437
  determineCitationState,
16763
18438
  computeCompetitorOverlap,
16764
18439
  extractRecommendedCompetitors,
18440
+ createApiClient,
16765
18441
  setGoogleAuthConfig,
16766
18442
  formatAuditFactorScore,
16767
- AGENT_WEBHOOK_EVENTS,
16768
- buildAgentWebhookUrl,
16769
- attachAgentWebhookDirect,
16770
- AgentManager,
16771
- getAeroStateDir,
16772
- detectOpenClaw,
16773
- installOpenClaw,
16774
- seedWorkspace,
16775
- initializeOpenClawProfile,
16776
- configureOpenClawGateway,
16777
- setOpenClawModel,
16778
- providerEnvVar,
16779
- writeAgentEnv,
16780
- resolveAgentCredentials,
18443
+ listAgentProviders,
18444
+ coerceAgentProvider,
16781
18445
  createServer
16782
18446
  };