@ainyc/canonry 1.48.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  IntelligenceService,
3
+ agentSessions,
3
4
  apiKeys,
4
5
  auditLog,
5
6
  bingCoverageSnapshots,
@@ -25,7 +26,7 @@ import {
25
26
  runs,
26
27
  schedules,
27
28
  usageCounters
28
- } from "./chunk-ZZ57GRV6.js";
29
+ } from "./chunk-GH6WGN5B.js";
29
30
 
30
31
  // src/config.ts
31
32
  import fs from "fs";
@@ -337,10 +338,10 @@ function printCliError(err, format) {
337
338
  // src/server.ts
338
339
  import { createRequire as createRequire2 } from "module";
339
340
  import crypto23 from "crypto";
340
- import fs7 from "fs";
341
- import path8 from "path";
341
+ import fs8 from "fs";
342
+ import path9 from "path";
342
343
  import { fileURLToPath as fileURLToPath2 } from "url";
343
- import { eq as eq24 } from "drizzle-orm";
344
+ import { eq as eq25 } from "drizzle-orm";
344
345
  import Fastify from "fastify";
345
346
 
346
347
  // ../contracts/src/config-schema.ts
@@ -602,6 +603,13 @@ function notImplemented(message) {
602
603
  function deliveryFailed(message) {
603
604
  return new AppError("DELIVERY_FAILED", message, 502);
604
605
  }
606
+ function agentBusy(projectName) {
607
+ return new AppError(
608
+ "AGENT_BUSY",
609
+ `Aero is already running a turn for '${projectName}'. Retry after the current turn settles.`,
610
+ 409
611
+ );
612
+ }
605
613
 
606
614
  // ../contracts/src/google.ts
607
615
  import { z as z5 } from "zod";
@@ -889,6 +897,37 @@ var wordpressDiffDtoSchema = z7.object({
889
897
  })
890
898
  });
891
899
 
900
+ // ../contracts/src/providers.ts
901
+ var ProviderIds = {
902
+ claude: "claude",
903
+ openai: "openai",
904
+ gemini: "gemini",
905
+ perplexity: "perplexity",
906
+ local: "local",
907
+ cdpChatgpt: "cdp:chatgpt",
908
+ zai: "zai"
909
+ };
910
+ var PROVIDER_IDS = Object.values(ProviderIds);
911
+ var SweepProviderIds = {
912
+ claude: ProviderIds.claude,
913
+ openai: ProviderIds.openai,
914
+ gemini: ProviderIds.gemini,
915
+ perplexity: ProviderIds.perplexity,
916
+ local: ProviderIds.local,
917
+ cdpChatgpt: ProviderIds.cdpChatgpt
918
+ };
919
+ var SWEEP_PROVIDER_IDS = Object.values(SweepProviderIds);
920
+ var AgentProviderIds = {
921
+ claude: ProviderIds.claude,
922
+ openai: ProviderIds.openai,
923
+ gemini: ProviderIds.gemini,
924
+ zai: ProviderIds.zai
925
+ };
926
+ var AGENT_PROVIDER_IDS = Object.values(AgentProviderIds);
927
+ function isAgentProviderId(value) {
928
+ return AGENT_PROVIDER_IDS.includes(value);
929
+ }
930
+
892
931
  // ../contracts/src/run.ts
893
932
  import { z as z8 } from "zod";
894
933
  var runStatusSchema = z8.enum(["queued", "running", "completed", "partial", "failed", "cancelled"]);
@@ -2381,7 +2420,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
2381
2420
  const body = JSON.stringify(payload);
2382
2421
  const isHttps = target.url.protocol === "https:";
2383
2422
  const port = target.url.port ? Number(target.url.port) : isHttps ? 443 : 80;
2384
- const path9 = `${target.url.pathname}${target.url.search}`;
2423
+ const path10 = `${target.url.pathname}${target.url.search}`;
2385
2424
  const headers = {
2386
2425
  "Content-Length": String(Buffer.byteLength(body)),
2387
2426
  "Content-Type": "application/json",
@@ -2397,7 +2436,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
2397
2436
  headers,
2398
2437
  hostname: target.address,
2399
2438
  method: "POST",
2400
- path: path9,
2439
+ path: path10,
2401
2440
  port,
2402
2441
  timeout: REQUEST_TIMEOUT_MS
2403
2442
  };
@@ -5567,10 +5606,91 @@ var routeCatalog = [
5567
5606
  }
5568
5607
  }
5569
5608
  ];
5609
+ var canonryLocalRouteCatalog = [
5610
+ {
5611
+ method: "get",
5612
+ path: "/api/v1/projects/{name}/agent/transcript",
5613
+ summary: "Get the rolling Aero transcript for this project",
5614
+ 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.",
5615
+ tags: ["agent"],
5616
+ parameters: [nameParameter],
5617
+ responses: {
5618
+ 200: { description: "Transcript returned." },
5619
+ 404: { description: "Project not found." }
5620
+ }
5621
+ },
5622
+ {
5623
+ method: "delete",
5624
+ path: "/api/v1/projects/{name}/agent/transcript",
5625
+ summary: "Reset the Aero transcript + queued follow-ups",
5626
+ description: "Evicts any live Agent instance, clears the persisted messages and follow_up_queue. A subsequent prompt starts a fresh session.",
5627
+ tags: ["agent"],
5628
+ parameters: [nameParameter],
5629
+ responses: {
5630
+ 200: { description: "Session reset." },
5631
+ 404: { description: "Project not found." }
5632
+ }
5633
+ },
5634
+ {
5635
+ method: "get",
5636
+ path: "/api/v1/projects/{name}/agent/providers",
5637
+ summary: "List the LLM providers Aero can route to",
5638
+ 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.",
5639
+ tags: ["agent"],
5640
+ parameters: [nameParameter],
5641
+ responses: {
5642
+ 200: { description: "Providers returned." },
5643
+ 404: { description: "Project not found." }
5644
+ }
5645
+ },
5646
+ {
5647
+ method: "post",
5648
+ path: "/api/v1/projects/{name}/agent/prompt",
5649
+ summary: "Send a prompt to Aero and stream events back as SSE",
5650
+ 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.',
5651
+ tags: ["agent"],
5652
+ parameters: [nameParameter],
5653
+ requestBody: {
5654
+ required: true,
5655
+ content: {
5656
+ "application/json": {
5657
+ schema: {
5658
+ type: "object",
5659
+ required: ["prompt"],
5660
+ properties: {
5661
+ prompt: { type: "string", description: "The user's message for Aero." },
5662
+ provider: {
5663
+ type: "string",
5664
+ enum: [...AGENT_PROVIDER_IDS],
5665
+ description: "Override the persisted LLM provider for this and subsequent turns."
5666
+ },
5667
+ modelId: {
5668
+ type: "string",
5669
+ description: "Override the persisted model id for this and subsequent turns."
5670
+ },
5671
+ scope: {
5672
+ type: "string",
5673
+ enum: ["all", "read-only"],
5674
+ description: 'Tool surface scope. Default "read-only". Set "all" to enable write tools.'
5675
+ }
5676
+ }
5677
+ }
5678
+ }
5679
+ }
5680
+ },
5681
+ responses: {
5682
+ 200: { description: "SSE stream of AgentEvent frames." },
5683
+ 400: { description: "Missing or empty prompt." },
5684
+ 404: { description: "Project not found." },
5685
+ 409: { description: "Another Aero turn is already in flight." }
5686
+ }
5687
+ }
5688
+ ];
5570
5689
  function buildOpenApiDocument(info = {}) {
5571
5690
  const BASE_PREFIX = "/api/v1";
5572
5691
  const prefix = info.routePrefix ?? BASE_PREFIX;
5573
- const paths = routeCatalog.reduce((acc, route) => {
5692
+ const fullCatalog = info.includeCanonryLocal ? [...routeCatalog, ...canonryLocalRouteCatalog] : routeCatalog;
5693
+ const paths = fullCatalog.reduce((acc, route) => {
5574
5694
  const subpath = route.path.startsWith(BASE_PREFIX) ? route.path.slice(BASE_PREFIX.length) : route.path;
5575
5695
  const fullPath = prefix + subpath;
5576
5696
  const operation = {
@@ -5618,8 +5738,8 @@ async function openApiRoutes(app, opts = {}) {
5618
5738
  return reply.type("application/json").send(buildOpenApiDocument(opts));
5619
5739
  });
5620
5740
  }
5621
- function buildOperationId(method, path9) {
5622
- const parts = path9.split("/").filter(Boolean).map((part) => {
5741
+ function buildOperationId(method, path10) {
5742
+ const parts = path10.split("/").filter(Boolean).map((part) => {
5623
5743
  if (part.startsWith("{") && part.endsWith("}")) {
5624
5744
  return `by-${part.slice(1, -1)}`;
5625
5745
  }
@@ -9265,10 +9385,10 @@ function buildAuthErrorMessage(res, responseText) {
9265
9385
  }
9266
9386
  return "WordPress credentials are invalid or lack permission for this action";
9267
9387
  }
9268
- async function fetchJson(connection, siteUrl, path9, init) {
9388
+ async function fetchJson(connection, siteUrl, path10, init) {
9269
9389
  if (siteUrl.startsWith("http:")) {
9270
9390
  }
9271
- const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path9}`, {
9391
+ const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path10}`, {
9272
9392
  ...init,
9273
9393
  headers: {
9274
9394
  "Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
@@ -10858,41 +10978,14 @@ async function apiRoutes(app, opts) {
10858
10978
  googleConnectionStore: opts.googleConnectionStore,
10859
10979
  getGoogleAuthConfig: opts.getGoogleAuthConfig
10860
10980
  });
10981
+ if (opts.registerAuthenticatedRoutes) {
10982
+ await opts.registerAuthenticatedRoutes(api);
10983
+ }
10861
10984
  }, { prefix: opts.routePrefix ?? "/api/v1" });
10862
10985
  }
10863
10986
 
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
- }
10987
+ // src/server.ts
10988
+ import os5 from "os";
10896
10989
 
10897
10990
  // ../provider-gemini/src/normalize.ts
10898
10991
  import { GoogleGenAI } from "@google/genai";
@@ -13345,11 +13438,11 @@ function removeWordpressConnection(config, projectName) {
13345
13438
  }
13346
13439
 
13347
13440
  // src/job-runner.ts
13348
- import crypto19 from "crypto";
13441
+ import crypto18 from "crypto";
13349
13442
  import fs4 from "fs";
13350
13443
  import path5 from "path";
13351
13444
  import os4 from "os";
13352
- import { and as and7, eq as eq19, inArray as inArray3, sql as sql4 } from "drizzle-orm";
13445
+ import { and as and7, eq as eq18, inArray as inArray3, sql as sql4 } from "drizzle-orm";
13353
13446
 
13354
13447
  // src/citation-utils.ts
13355
13448
  function domainMatches(domain, canonicalDomain) {
@@ -13585,7 +13678,7 @@ var JobRunner = class {
13585
13678
  if (stale.length === 0) return;
13586
13679
  const now = (/* @__PURE__ */ new Date()).toISOString();
13587
13680
  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();
13681
+ 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
13682
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
13590
13683
  }
13591
13684
  }
@@ -13613,10 +13706,10 @@ var JobRunner = class {
13613
13706
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
13614
13707
  }
13615
13708
  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();
13709
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(eq18(runs.id, runId), eq18(runs.status, "queued"))).run();
13617
13710
  }
13618
13711
  this.throwIfRunCancelled(runId);
13619
- const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
13712
+ const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
13620
13713
  if (!project) {
13621
13714
  throw new Error(`Project ${projectId} not found`);
13622
13715
  }
@@ -13636,8 +13729,8 @@ var JobRunner = class {
13636
13729
  throw new Error("No providers configured. Add at least one provider API key.");
13637
13730
  }
13638
13731
  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();
13732
+ projectKeywords = this.db.select().from(keywords).where(eq18(keywords.projectId, projectId)).all();
13733
+ const projectCompetitors = this.db.select().from(competitors).where(eq18(competitors.projectId, projectId)).all();
13641
13734
  const competitorDomains = projectCompetitors.map((c) => c.domain);
13642
13735
  const allDomains = effectiveDomains({
13643
13736
  canonicalDomain: project.canonicalDomain,
@@ -13653,7 +13746,7 @@ var JobRunner = class {
13653
13746
  const todayPeriod = getCurrentUsageDay();
13654
13747
  for (const p of activeProviders) {
13655
13748
  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);
13749
+ 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
13750
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
13658
13751
  if (providerUsage + queriesPerProvider > limit) {
13659
13752
  throw new Error(
@@ -13713,7 +13806,7 @@ var JobRunner = class {
13713
13806
  );
13714
13807
  let screenshotRelPath = null;
13715
13808
  if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
13716
- const snapshotId = crypto19.randomUUID();
13809
+ const snapshotId = crypto18.randomUUID();
13717
13810
  const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
13718
13811
  if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
13719
13812
  const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
@@ -13743,7 +13836,7 @@ var JobRunner = class {
13743
13836
  }).run();
13744
13837
  } else {
13745
13838
  this.db.insert(querySnapshots).values({
13746
- id: crypto19.randomUUID(),
13839
+ id: crypto18.randomUUID(),
13747
13840
  runId,
13748
13841
  keywordId: kw.id,
13749
13842
  provider: providerName,
@@ -13794,12 +13887,12 @@ var JobRunner = class {
13794
13887
  const someFailed = providerErrors.size > 0;
13795
13888
  if (allFailed) {
13796
13889
  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();
13890
+ this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
13798
13891
  } else if (someFailed) {
13799
13892
  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();
13893
+ this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
13801
13894
  } else {
13802
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13895
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
13803
13896
  }
13804
13897
  this.flushProviderUsage(projectId, providerDispatchCounts);
13805
13898
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -13834,7 +13927,7 @@ var JobRunner = class {
13834
13927
  status: "failed",
13835
13928
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13836
13929
  error: errorMessage
13837
- }).where(eq19(runs.id, runId)).run();
13930
+ }).where(eq18(runs.id, runId)).run();
13838
13931
  this.flushProviderUsage(projectId, providerDispatchCounts);
13839
13932
  trackEvent("run.completed", {
13840
13933
  status: "failed",
@@ -13855,7 +13948,7 @@ var JobRunner = class {
13855
13948
  const now = (/* @__PURE__ */ new Date()).toISOString();
13856
13949
  const period = now.slice(0, 10);
13857
13950
  this.db.insert(usageCounters).values({
13858
- id: crypto19.randomUUID(),
13951
+ id: crypto18.randomUUID(),
13859
13952
  scope,
13860
13953
  period,
13861
13954
  metric,
@@ -13877,7 +13970,7 @@ var JobRunner = class {
13877
13970
  status: runs.status,
13878
13971
  finishedAt: runs.finishedAt,
13879
13972
  error: runs.error
13880
- }).from(runs).where(eq19(runs.id, runId)).get();
13973
+ }).from(runs).where(eq18(runs.id, runId)).get();
13881
13974
  }
13882
13975
  isRunCancelled(runId) {
13883
13976
  return this.getRunState(runId)?.status === "cancelled";
@@ -13893,7 +13986,7 @@ var JobRunner = class {
13893
13986
  this.db.update(runs).set({
13894
13987
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13895
13988
  error: currentRun.error ?? "Cancelled by user"
13896
- }).where(eq19(runs.id, runId)).run();
13989
+ }).where(eq18(runs.id, runId)).run();
13897
13990
  }
13898
13991
  trackEvent("run.completed", {
13899
13992
  status: "cancelled",
@@ -13915,8 +14008,8 @@ function getCurrentUsageDay() {
13915
14008
  }
13916
14009
 
13917
14010
  // src/gsc-sync.ts
13918
- import crypto20 from "crypto";
13919
- import { eq as eq20, and as and8, sql as sql5 } from "drizzle-orm";
14011
+ import crypto19 from "crypto";
14012
+ import { eq as eq19, and as and8, sql as sql5 } from "drizzle-orm";
13920
14013
  var log2 = createLogger("GscSync");
13921
14014
  function formatDate2(d) {
13922
14015
  return d.toISOString().split("T")[0];
@@ -13928,13 +14021,13 @@ function daysAgo(n) {
13928
14021
  }
13929
14022
  async function executeGscSync(db, runId, projectId, opts) {
13930
14023
  const now = (/* @__PURE__ */ new Date()).toISOString();
13931
- db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
14024
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq19(runs.id, runId)).run();
13932
14025
  try {
13933
14026
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
13934
14027
  if (!googleClientId || !googleClientSecret) {
13935
14028
  throw new Error("Google OAuth is not configured in the local Canonry config");
13936
14029
  }
13937
- const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
14030
+ const project = db.select().from(projects).where(eq19(projects.id, projectId)).get();
13938
14031
  if (!project) {
13939
14032
  throw new Error(`Project not found: ${projectId}`);
13940
14033
  }
@@ -13969,7 +14062,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13969
14062
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
13970
14063
  db.delete(gscSearchData).where(
13971
14064
  and8(
13972
- eq20(gscSearchData.projectId, projectId),
14065
+ eq19(gscSearchData.projectId, projectId),
13973
14066
  sql5`${gscSearchData.date} >= ${startDate}`,
13974
14067
  sql5`${gscSearchData.date} <= ${endDate}`
13975
14068
  )
@@ -13981,7 +14074,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13981
14074
  for (const row of batch) {
13982
14075
  const [query, page, country, device, date] = row.keys;
13983
14076
  db.insert(gscSearchData).values({
13984
- id: crypto20.randomUUID(),
14077
+ id: crypto19.randomUUID(),
13985
14078
  projectId,
13986
14079
  syncRunId: runId,
13987
14080
  date: date ?? "",
@@ -14015,7 +14108,7 @@ async function executeGscSync(db, runId, projectId, opts) {
14015
14108
  const rich = ir.richResultsResult;
14016
14109
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
14017
14110
  db.insert(gscUrlInspections).values({
14018
- id: crypto20.randomUUID(),
14111
+ id: crypto19.randomUUID(),
14019
14112
  projectId,
14020
14113
  syncRunId: runId,
14021
14114
  url: pageUrl,
@@ -14036,7 +14129,7 @@ async function executeGscSync(db, runId, projectId, opts) {
14036
14129
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
14037
14130
  }
14038
14131
  }
14039
- const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
14132
+ const allInspections = db.select().from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).all();
14040
14133
  const latestByUrl = /* @__PURE__ */ new Map();
14041
14134
  for (const row of allInspections) {
14042
14135
  const existing = latestByUrl.get(row.url);
@@ -14057,9 +14150,9 @@ async function executeGscSync(db, runId, projectId, opts) {
14057
14150
  }
14058
14151
  }
14059
14152
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
14060
- db.delete(gscCoverageSnapshots).where(and8(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
14153
+ db.delete(gscCoverageSnapshots).where(and8(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
14061
14154
  db.insert(gscCoverageSnapshots).values({
14062
- id: crypto20.randomUUID(),
14155
+ id: crypto19.randomUUID(),
14063
14156
  projectId,
14064
14157
  syncRunId: runId,
14065
14158
  date: snapshotDate,
@@ -14068,19 +14161,19 @@ async function executeGscSync(db, runId, projectId, opts) {
14068
14161
  reasonBreakdown: JSON.stringify(reasonCounts),
14069
14162
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14070
14163
  }).run();
14071
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14164
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
14072
14165
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
14073
14166
  } catch (err) {
14074
14167
  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();
14168
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
14076
14169
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
14077
14170
  throw err;
14078
14171
  }
14079
14172
  }
14080
14173
 
14081
14174
  // src/gsc-inspect-sitemap.ts
14082
- import crypto21 from "crypto";
14083
- import { eq as eq21, and as and9 } from "drizzle-orm";
14175
+ import crypto20 from "crypto";
14176
+ import { eq as eq20, and as and9 } from "drizzle-orm";
14084
14177
 
14085
14178
  // src/sitemap-parser.ts
14086
14179
  var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
@@ -14149,13 +14242,13 @@ async function parseSitemapRecursive(url, urls, depth) {
14149
14242
  var log3 = createLogger("InspectSitemap");
14150
14243
  async function executeInspectSitemap(db, runId, projectId, opts) {
14151
14244
  const now = (/* @__PURE__ */ new Date()).toISOString();
14152
- db.update(runs).set({ status: "running", startedAt: now }).where(eq21(runs.id, runId)).run();
14245
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
14153
14246
  try {
14154
14247
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
14155
14248
  if (!googleClientId || !googleClientSecret) {
14156
14249
  throw new Error("Google OAuth is not configured in the local Canonry config");
14157
14250
  }
14158
- const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
14251
+ const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
14159
14252
  if (!project) {
14160
14253
  throw new Error(`Project not found: ${projectId}`);
14161
14254
  }
@@ -14196,7 +14289,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14196
14289
  const rich = ir.richResultsResult;
14197
14290
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
14198
14291
  db.insert(gscUrlInspections).values({
14199
- id: crypto21.randomUUID(),
14292
+ id: crypto20.randomUUID(),
14200
14293
  projectId,
14201
14294
  syncRunId: runId,
14202
14295
  url: pageUrl,
@@ -14223,7 +14316,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14223
14316
  await new Promise((r) => setTimeout(r, 1e3));
14224
14317
  }
14225
14318
  }
14226
- const allInspections = db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, projectId)).all();
14319
+ const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
14227
14320
  const latestByUrl = /* @__PURE__ */ new Map();
14228
14321
  for (const row of allInspections) {
14229
14322
  const existing = latestByUrl.get(row.url);
@@ -14244,9 +14337,9 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14244
14337
  }
14245
14338
  }
14246
14339
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
14247
- db.delete(gscCoverageSnapshots).where(and9(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
14340
+ db.delete(gscCoverageSnapshots).where(and9(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
14248
14341
  db.insert(gscCoverageSnapshots).values({
14249
- id: crypto21.randomUUID(),
14342
+ id: crypto20.randomUUID(),
14250
14343
  projectId,
14251
14344
  syncRunId: runId,
14252
14345
  date: snapshotDate,
@@ -14256,11 +14349,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14256
14349
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14257
14350
  }).run();
14258
14351
  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();
14352
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14260
14353
  log3.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
14261
14354
  } catch (err) {
14262
14355
  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();
14356
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14264
14357
  log3.error("inspect.failed", { runId, projectId, error: errorMsg });
14265
14358
  throw err;
14266
14359
  }
@@ -14319,7 +14412,7 @@ var ProviderRegistry = class {
14319
14412
 
14320
14413
  // src/scheduler.ts
14321
14414
  import cron from "node-cron";
14322
- import { eq as eq22 } from "drizzle-orm";
14415
+ import { eq as eq21 } from "drizzle-orm";
14323
14416
  var log4 = createLogger("Scheduler");
14324
14417
  var Scheduler = class {
14325
14418
  db;
@@ -14331,7 +14424,7 @@ var Scheduler = class {
14331
14424
  }
14332
14425
  /** Load all enabled schedules from DB and register cron jobs. */
14333
14426
  start() {
14334
- const allSchedules = this.db.select().from(schedules).where(eq22(schedules.enabled, 1)).all();
14427
+ const allSchedules = this.db.select().from(schedules).where(eq21(schedules.enabled, 1)).all();
14335
14428
  for (const schedule of allSchedules) {
14336
14429
  const missedRunAt = schedule.nextRunAt;
14337
14430
  this.registerCronTask(schedule);
@@ -14356,7 +14449,7 @@ var Scheduler = class {
14356
14449
  this.stopTask(projectId, existing, "Stopped");
14357
14450
  this.tasks.delete(projectId);
14358
14451
  }
14359
- const schedule = this.db.select().from(schedules).where(eq22(schedules.projectId, projectId)).get();
14452
+ const schedule = this.db.select().from(schedules).where(eq21(schedules.projectId, projectId)).get();
14360
14453
  if (schedule && schedule.enabled === 1) {
14361
14454
  this.registerCronTask(schedule);
14362
14455
  }
@@ -14389,14 +14482,14 @@ var Scheduler = class {
14389
14482
  this.db.update(schedules).set({
14390
14483
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
14391
14484
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14392
- }).where(eq22(schedules.id, scheduleId)).run();
14485
+ }).where(eq21(schedules.id, scheduleId)).run();
14393
14486
  const label = schedule.preset ?? cronExpr;
14394
14487
  log4.info("cron.registered", { projectId, schedule: label, timezone });
14395
14488
  }
14396
14489
  triggerRun(scheduleId, projectId) {
14397
14490
  try {
14398
14491
  const now = (/* @__PURE__ */ new Date()).toISOString();
14399
- const currentSchedule = this.db.select().from(schedules).where(eq22(schedules.id, scheduleId)).get();
14492
+ const currentSchedule = this.db.select().from(schedules).where(eq21(schedules.id, scheduleId)).get();
14400
14493
  if (!currentSchedule || currentSchedule.enabled !== 1) {
14401
14494
  log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
14402
14495
  this.remove(projectId);
@@ -14404,7 +14497,7 @@ var Scheduler = class {
14404
14497
  }
14405
14498
  const task = this.tasks.get(projectId);
14406
14499
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
14407
- const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
14500
+ const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
14408
14501
  if (!project) {
14409
14502
  log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
14410
14503
  this.remove(projectId);
@@ -14433,7 +14526,7 @@ var Scheduler = class {
14433
14526
  this.db.update(schedules).set({
14434
14527
  nextRunAt,
14435
14528
  updatedAt: now
14436
- }).where(eq22(schedules.id, currentSchedule.id)).run();
14529
+ }).where(eq21(schedules.id, currentSchedule.id)).run();
14437
14530
  return;
14438
14531
  }
14439
14532
  const runId = queueResult.runId;
@@ -14441,7 +14534,7 @@ var Scheduler = class {
14441
14534
  lastRunAt: now,
14442
14535
  nextRunAt,
14443
14536
  updatedAt: now
14444
- }).where(eq22(schedules.id, currentSchedule.id)).run();
14537
+ }).where(eq21(schedules.id, currentSchedule.id)).run();
14445
14538
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
14446
14539
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
14447
14540
  log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -14453,8 +14546,8 @@ var Scheduler = class {
14453
14546
  };
14454
14547
 
14455
14548
  // 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";
14549
+ import { eq as eq22, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
14550
+ import crypto21 from "crypto";
14458
14551
  var log5 = createLogger("Notifier");
14459
14552
  var Notifier = class {
14460
14553
  db;
@@ -14466,18 +14559,18 @@ var Notifier = class {
14466
14559
  /** Called after a run completes (success, partial, or failed). */
14467
14560
  async onRunCompleted(runId, projectId) {
14468
14561
  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);
14562
+ const notifs = this.db.select().from(notifications).where(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14470
14563
  if (notifs.length === 0) {
14471
14564
  log5.info("notifications.none-enabled", { projectId });
14472
14565
  return;
14473
14566
  }
14474
14567
  log5.info("notifications.found", { projectId, count: notifs.length });
14475
- const run = this.db.select().from(runs).where(eq23(runs.id, runId)).get();
14568
+ const run = this.db.select().from(runs).where(eq22(runs.id, runId)).get();
14476
14569
  if (!run) {
14477
14570
  log5.error("run.not-found", { runId, msg: "skipping notification dispatch" });
14478
14571
  return;
14479
14572
  }
14480
- const project = this.db.select().from(projects).where(eq23(projects.id, projectId)).get();
14573
+ const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
14481
14574
  if (!project) {
14482
14575
  log5.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
14483
14576
  return;
@@ -14524,11 +14617,11 @@ var Notifier = class {
14524
14617
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
14525
14618
  if (highInsights.length > 0) insightEvents.push("insight.high");
14526
14619
  if (insightEvents.length === 0) return;
14527
- const notifs = this.db.select().from(notifications).where(eq23(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14620
+ const notifs = this.db.select().from(notifications).where(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14528
14621
  if (notifs.length === 0) return;
14529
- const run = this.db.select().from(runs).where(eq23(runs.id, runId)).get();
14622
+ const run = this.db.select().from(runs).where(eq22(runs.id, runId)).get();
14530
14623
  if (!run) return;
14531
- const project = this.db.select().from(projects).where(eq23(projects.id, projectId)).get();
14624
+ const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
14532
14625
  if (!project) return;
14533
14626
  for (const notif of notifs) {
14534
14627
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -14560,8 +14653,8 @@ var Notifier = class {
14560
14653
  computeTransitions(runId, projectId) {
14561
14654
  const recentRuns = this.db.select().from(runs).where(
14562
14655
  and10(
14563
- eq23(runs.projectId, projectId),
14564
- or2(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
14656
+ eq22(runs.projectId, projectId),
14657
+ or2(eq22(runs.status, "completed"), eq22(runs.status, "partial"))
14565
14658
  )
14566
14659
  ).orderBy(desc8(runs.createdAt)).limit(2).all();
14567
14660
  if (recentRuns.length < 2) return [];
@@ -14573,12 +14666,12 @@ var Notifier = class {
14573
14666
  keyword: keywords.keyword,
14574
14667
  provider: querySnapshots.provider,
14575
14668
  citationState: querySnapshots.citationState
14576
- }).from(querySnapshots).leftJoin(keywords, eq23(querySnapshots.keywordId, keywords.id)).where(eq23(querySnapshots.runId, currentRunId)).all();
14669
+ }).from(querySnapshots).leftJoin(keywords, eq22(querySnapshots.keywordId, keywords.id)).where(eq22(querySnapshots.runId, currentRunId)).all();
14577
14670
  const previousSnapshots = this.db.select({
14578
14671
  keywordId: querySnapshots.keywordId,
14579
14672
  provider: querySnapshots.provider,
14580
14673
  citationState: querySnapshots.citationState
14581
- }).from(querySnapshots).where(eq23(querySnapshots.runId, previousRunId)).all();
14674
+ }).from(querySnapshots).where(eq22(querySnapshots.runId, previousRunId)).all();
14582
14675
  const prevMap = /* @__PURE__ */ new Map();
14583
14676
  for (const s of previousSnapshots) {
14584
14677
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -14636,7 +14729,7 @@ var Notifier = class {
14636
14729
  }
14637
14730
  logDelivery(projectId, notificationId, event, status, error) {
14638
14731
  this.db.insert(auditLog).values({
14639
- id: crypto22.randomUUID(),
14732
+ id: crypto21.randomUUID(),
14640
14733
  projectId,
14641
14734
  actor: "scheduler",
14642
14735
  action: `notification.${status}`,
@@ -14651,19 +14744,23 @@ var Notifier = class {
14651
14744
  // src/run-coordinator.ts
14652
14745
  var log6 = createLogger("RunCoordinator");
14653
14746
  var RunCoordinator = class {
14654
- constructor(notifier, intelligenceService, onInsightsGenerated) {
14747
+ constructor(notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
14655
14748
  this.notifier = notifier;
14656
14749
  this.intelligenceService = intelligenceService;
14657
14750
  this.onInsightsGenerated = onInsightsGenerated;
14751
+ this.onAeroEvent = onAeroEvent;
14658
14752
  }
14659
14753
  async onRunCompleted(runId, projectId) {
14754
+ let insightCount = 0;
14755
+ let criticalOrHigh = 0;
14660
14756
  try {
14661
14757
  const result = this.intelligenceService.analyzeAndPersist(runId, projectId);
14662
- if (result && this.onInsightsGenerated) {
14663
- const hasHighSeverity = result.insights.some(
14758
+ if (result) {
14759
+ insightCount = result.insights.length;
14760
+ criticalOrHigh = result.insights.filter(
14664
14761
  (i) => i.severity === "critical" || i.severity === "high"
14665
- );
14666
- if (hasHighSeverity) {
14762
+ ).length;
14763
+ if (this.onInsightsGenerated && criticalOrHigh > 0) {
14667
14764
  try {
14668
14765
  await this.onInsightsGenerated(runId, projectId, result);
14669
14766
  } catch (err) {
@@ -14679,147 +14776,1691 @@ var RunCoordinator = class {
14679
14776
  } catch (err) {
14680
14777
  log6.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
14681
14778
  }
14779
+ if (this.onAeroEvent) {
14780
+ try {
14781
+ await this.onAeroEvent({ runId, projectId, insightCount, criticalOrHigh });
14782
+ } catch (err) {
14783
+ log6.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
14784
+ }
14785
+ }
14682
14786
  }
14683
14787
  };
14684
14788
 
14685
- // src/snapshot-service.ts
14686
- import { runAeoAudit } from "@ainyc/aeo-audit";
14789
+ // src/agent/session-registry.ts
14790
+ import crypto22 from "crypto";
14791
+ import { eq as eq23 } from "drizzle-orm";
14687
14792
 
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 {
14793
+ // src/agent/session.ts
14794
+ import fs7 from "fs";
14795
+ import path8 from "path";
14796
+ import { Agent } from "@mariozechner/pi-agent-core";
14797
+ import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
14798
+
14799
+ // src/agent/providers.ts
14800
+ import { getEnvApiKey, getModel } from "@mariozechner/pi-ai";
14801
+ var AGENT_PROVIDERS = {
14802
+ [AgentProviderIds.claude]: {
14803
+ piAiProvider: "anthropic",
14804
+ label: "Anthropic (Claude)",
14805
+ defaultModel: "claude-opus-4-7",
14806
+ autoDetectPriority: 0
14807
+ },
14808
+ [AgentProviderIds.openai]: {
14809
+ piAiProvider: "openai",
14810
+ label: "OpenAI",
14811
+ defaultModel: "gpt-5.1",
14812
+ autoDetectPriority: 1
14813
+ },
14814
+ [AgentProviderIds.gemini]: {
14815
+ piAiProvider: "google",
14816
+ label: "Google (Gemini)",
14817
+ defaultModel: "gemini-2.5-pro",
14818
+ autoDetectPriority: 2
14819
+ },
14820
+ [AgentProviderIds.zai]: {
14821
+ piAiProvider: "zai",
14822
+ label: "Z.ai (GLM)",
14823
+ defaultModel: "glm-5.1",
14824
+ autoDetectPriority: 3
14701
14825
  }
14702
- return hostname.replace(/^www\./, "");
14826
+ };
14827
+ function agentProvidersByPriority() {
14828
+ return Object.keys(AGENT_PROVIDERS).slice().sort((a, b) => AGENT_PROVIDERS[a].autoDetectPriority - AGENT_PROVIDERS[b].autoDetectPriority);
14703
14829
  }
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
- }
14830
+ function listAgentProviders() {
14831
+ return AGENT_PROVIDER_IDS;
14832
+ }
14833
+ function getAgentProvider(name) {
14834
+ return AGENT_PROVIDERS[name];
14835
+ }
14836
+ function coerceAgentProvider(value) {
14837
+ if (!value) return void 0;
14838
+ return isAgentProviderId(value) ? value : void 0;
14839
+ }
14840
+ function resolveModelForProvider(provider, modelId) {
14841
+ const entry = AGENT_PROVIDERS[provider];
14842
+ const id = modelId ?? entry.defaultModel;
14843
+ const model = getModel(entry.piAiProvider, id);
14844
+ if (!model) {
14845
+ throw new Error(
14846
+ `Model '${id}' not found for pi-ai provider '${entry.piAiProvider}'. Verify AGENT_PROVIDERS[${provider}].defaultModel against the installed @mariozechner/pi-ai catalog.`
14753
14847
  );
14754
- req.on("timeout", () => req.destroy(new Error("timeout")));
14755
- req.on("error", () => resolve(""));
14756
- req.end();
14757
- });
14848
+ }
14849
+ return model;
14758
14850
  }
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 "";
14851
+ function validateAgentProviderRegistry() {
14852
+ for (const provider of listAgentProviders()) {
14853
+ resolveModelForProvider(provider);
14779
14854
  }
14780
14855
  }
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);
14856
+ function resolveApiKeyFor(providerOrPiAi, config) {
14857
+ return resolveApiKeySource(providerOrPiAi, config)?.key;
14858
+ }
14859
+ function resolveApiKeySource(providerOrPiAi, config) {
14860
+ const id = resolveAgentId(providerOrPiAi);
14861
+ if (!id) return void 0;
14862
+ const entry = AGENT_PROVIDERS[id];
14863
+ const fromConfig = config.providers?.[id]?.apiKey;
14864
+ if (fromConfig) return { key: fromConfig, source: "config" };
14865
+ const fromEnv = getEnvApiKey(entry.piAiProvider);
14866
+ if (fromEnv) return { key: fromEnv, source: "env" };
14867
+ return void 0;
14868
+ }
14869
+ function resolveAgentId(providerOrPiAi) {
14870
+ if (isAgentProviderId(providerOrPiAi)) return providerOrPiAi;
14871
+ for (const id of AGENT_PROVIDER_IDS) {
14872
+ if (AGENT_PROVIDERS[id].piAiProvider === providerOrPiAi) return id;
14795
14873
  }
14796
- return text;
14874
+ return void 0;
14875
+ }
14876
+ function buildAgentProvidersResponse(config) {
14877
+ const providers = listAgentProviders().map((id) => {
14878
+ const entry = AGENT_PROVIDERS[id];
14879
+ const source = resolveApiKeySource(id, config);
14880
+ return {
14881
+ id,
14882
+ label: entry.label,
14883
+ defaultModel: entry.defaultModel,
14884
+ configured: source !== void 0,
14885
+ keySource: source?.source ?? null
14886
+ };
14887
+ });
14888
+ const firstConfigured = agentProvidersByPriority().find((p) => resolveApiKeySource(p, config));
14889
+ return {
14890
+ providers,
14891
+ defaultProvider: firstConfigured ?? null
14892
+ };
14797
14893
  }
14798
14894
 
14799
- // src/snapshot-format.ts
14800
- function formatAuditFactorScore(factor) {
14801
- return `${factor.score}/100 (${factor.weight}% weight)`;
14895
+ // src/agent/skill-paths.ts
14896
+ import fs5 from "fs";
14897
+ import path6 from "path";
14898
+ import { fileURLToPath } from "url";
14899
+ function resolveAeroSkillDir(pkgDir) {
14900
+ const here = pkgDir ?? path6.dirname(fileURLToPath(import.meta.url));
14901
+ const candidates = [
14902
+ path6.join(here, "../assets/agent-workspace/skills/aero"),
14903
+ path6.join(here, "../../assets/agent-workspace/skills/aero"),
14904
+ path6.join(here, "../../../../skills/aero")
14905
+ ];
14906
+ for (const candidate of candidates) {
14907
+ if (fs5.existsSync(path6.join(candidate, "SKILL.md"))) return candidate;
14908
+ }
14909
+ throw new Error(`Aero skill not found. Searched:
14910
+ ${candidates.join("\n ")}`);
14802
14911
  }
14803
14912
 
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;
14913
+ // src/agent/skill-tools.ts
14914
+ import fs6 from "fs";
14915
+ import path7 from "path";
14916
+ import { Type } from "@sinclair/typebox";
14917
+ var MAX_DOC_CHARS = 2e4;
14918
+ function textResult(details) {
14919
+ return {
14920
+ content: [{ type: "text", text: JSON.stringify(details, null, 2) }],
14921
+ details
14922
+ };
14923
+ }
14924
+ function parseDescription(body) {
14925
+ if (!body.startsWith("---")) return "(no description)";
14926
+ const end = body.indexOf("\n---", 3);
14927
+ if (end === -1) return "(no description)";
14928
+ const block = body.slice(3, end);
14929
+ for (const line of block.split("\n")) {
14930
+ const match = line.match(/^description:\s*(.+)$/);
14931
+ if (match) return match[1].trim().replace(/^["']|["']$/g, "");
14932
+ }
14933
+ return "(no description)";
14934
+ }
14935
+ function scanSkillDocs(skillDir) {
14936
+ const refsDir = path7.join(skillDir ?? resolveAeroSkillDir(), "references");
14937
+ if (!fs6.existsSync(refsDir)) return [];
14938
+ const entries = [];
14939
+ for (const file of fs6.readdirSync(refsDir)) {
14940
+ if (!file.endsWith(".md")) continue;
14941
+ const filePath = path7.join(refsDir, file);
14942
+ const body = fs6.readFileSync(filePath, "utf-8");
14943
+ entries.push({
14944
+ slug: file.replace(/\.md$/, ""),
14945
+ description: parseDescription(body),
14946
+ bytes: Buffer.byteLength(body, "utf-8")
14947
+ });
14812
14948
  }
14813
- window = [];
14814
- waiters = [];
14815
- rateLimitChain = Promise.resolve();
14816
- inFlight = 0;
14817
- async run(task) {
14818
- await this.acquire();
14819
- try {
14820
- await this.waitForRateLimit();
14821
- return await task();
14822
- } finally {
14949
+ entries.sort((a, b) => a.slug.localeCompare(b.slug));
14950
+ return entries;
14951
+ }
14952
+ var ListSchema = Type.Object({});
14953
+ function buildListSkillDocsTool() {
14954
+ return {
14955
+ name: "list_skill_docs",
14956
+ label: "List skill docs",
14957
+ 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.",
14958
+ parameters: ListSchema,
14959
+ execute: async () => {
14960
+ return textResult({ docs: scanSkillDocs() });
14961
+ }
14962
+ };
14963
+ }
14964
+ var ReadSchema = Type.Object({
14965
+ slug: Type.String({
14966
+ description: "Doc slug (no extension, no path). Must match a slug from list_skill_docs \u2014 unknown slugs return an error listing valid options."
14967
+ })
14968
+ });
14969
+ function buildReadSkillDocTool() {
14970
+ return {
14971
+ name: "read_skill_doc",
14972
+ label: "Read skill doc",
14973
+ 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.',
14974
+ parameters: ReadSchema,
14975
+ execute: async (_toolCallId, params) => {
14976
+ const skillDir = resolveAeroSkillDir();
14977
+ const docs = scanSkillDocs(skillDir);
14978
+ const match = docs.find((d) => d.slug === params.slug);
14979
+ if (!match) {
14980
+ return textResult({
14981
+ error: `Unknown slug "${params.slug}".`,
14982
+ availableSlugs: docs.map((d) => d.slug)
14983
+ });
14984
+ }
14985
+ const filePath = path7.join(skillDir, "references", `${match.slug}.md`);
14986
+ const content = fs6.readFileSync(filePath, "utf-8");
14987
+ if (content.length > MAX_DOC_CHARS) {
14988
+ return textResult({
14989
+ slug: match.slug,
14990
+ content: content.slice(0, MAX_DOC_CHARS),
14991
+ truncated: true,
14992
+ totalBytes: match.bytes
14993
+ });
14994
+ }
14995
+ return textResult({ slug: match.slug, content, truncated: false });
14996
+ }
14997
+ };
14998
+ }
14999
+ function buildSkillDocTools() {
15000
+ return [
15001
+ buildListSkillDocsTool(),
15002
+ buildReadSkillDocTool()
15003
+ ];
15004
+ }
15005
+
15006
+ // src/agent/tools.ts
15007
+ import { Type as Type2 } from "@sinclair/typebox";
15008
+ var MAX_TOOL_RESULT_CHARS = 2e4;
15009
+ function truncate(json) {
15010
+ if (json.length <= MAX_TOOL_RESULT_CHARS) return json;
15011
+ return json.slice(0, MAX_TOOL_RESULT_CHARS) + "\n... (truncated \u2014 result too large)";
15012
+ }
15013
+ function textResult2(details) {
15014
+ return {
15015
+ content: [{ type: "text", text: truncate(JSON.stringify(details, null, 2)) }],
15016
+ details
15017
+ };
15018
+ }
15019
+ var StatusSchema = Type2.Object({
15020
+ runLimit: Type2.Optional(
15021
+ Type2.Number({
15022
+ description: "Max recent runs to include. Default 5.",
15023
+ minimum: 1,
15024
+ maximum: 50
15025
+ })
15026
+ )
15027
+ });
15028
+ function buildGetStatusTool(ctx) {
15029
+ return {
15030
+ name: "get_status",
15031
+ label: "Get status",
15032
+ description: "Current project overview with its most recent runs.",
15033
+ parameters: StatusSchema,
15034
+ execute: async (_toolCallId, params) => {
15035
+ const runLimit = params.runLimit ?? 5;
15036
+ const [project, runs2] = await Promise.all([
15037
+ ctx.client.getProject(ctx.projectName),
15038
+ ctx.client.listRuns(ctx.projectName, runLimit)
15039
+ ]);
15040
+ return textResult2({ project, runs: runs2 });
15041
+ }
15042
+ };
15043
+ }
15044
+ var HealthSchema = Type2.Object({});
15045
+ function buildGetHealthTool(ctx) {
15046
+ return {
15047
+ name: "get_health",
15048
+ label: "Get health",
15049
+ description: "Latest visibility health snapshot including overall cited rate, pair counts, and per-provider breakdown.",
15050
+ parameters: HealthSchema,
15051
+ execute: async () => {
15052
+ const health = await ctx.client.getHealth(ctx.projectName);
15053
+ return textResult2(health);
15054
+ }
15055
+ };
15056
+ }
15057
+ var TimelineSchema = Type2.Object({
15058
+ keyword: Type2.Optional(
15059
+ Type2.String({
15060
+ description: "Restrict the timeline to a single keyword. Omit to return all keywords."
15061
+ })
15062
+ )
15063
+ });
15064
+ function buildGetTimelineTool(ctx) {
15065
+ return {
15066
+ name: "get_timeline",
15067
+ label: "Get timeline",
15068
+ description: "Per-keyword citation timeline showing how visibility evolved across runs. Use to identify regressions, emerging citations, or competitor movement.",
15069
+ parameters: TimelineSchema,
15070
+ execute: async (_toolCallId, params) => {
15071
+ const timeline = await ctx.client.getTimeline(ctx.projectName);
15072
+ const filtered = params.keyword ? timeline.filter((row) => row.keyword === params.keyword) : timeline;
15073
+ return textResult2(filtered);
15074
+ }
15075
+ };
15076
+ }
15077
+ var InsightsSchema = Type2.Object({
15078
+ includeDismissed: Type2.Optional(
15079
+ Type2.Boolean({
15080
+ description: "Include dismissed insights. Default false (only active insights)."
15081
+ })
15082
+ ),
15083
+ runId: Type2.Optional(
15084
+ Type2.String({
15085
+ description: "Restrict insights to a specific run id. Omit for all runs."
15086
+ })
15087
+ )
15088
+ });
15089
+ function buildGetInsightsTool(ctx) {
15090
+ return {
15091
+ name: "get_insights",
15092
+ label: "Get insights",
15093
+ 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.",
15094
+ parameters: InsightsSchema,
15095
+ execute: async (_toolCallId, params) => {
15096
+ const insights2 = await ctx.client.getInsights(ctx.projectName, {
15097
+ dismissed: params.includeDismissed,
15098
+ runId: params.runId
15099
+ });
15100
+ return textResult2(insights2);
15101
+ }
15102
+ };
15103
+ }
15104
+ var KeywordsSchema = Type2.Object({});
15105
+ function buildListKeywordsTool(ctx) {
15106
+ return {
15107
+ name: "list_keywords",
15108
+ label: "List keywords",
15109
+ description: "All keywords currently tracked for this project.",
15110
+ parameters: KeywordsSchema,
15111
+ execute: async () => {
15112
+ const keywords2 = await ctx.client.listKeywords(ctx.projectName);
15113
+ return textResult2(keywords2);
15114
+ }
15115
+ };
15116
+ }
15117
+ var CompetitorsSchema = Type2.Object({});
15118
+ function buildListCompetitorsTool(ctx) {
15119
+ return {
15120
+ name: "list_competitors",
15121
+ label: "List competitors",
15122
+ description: "Competitor domains tracked alongside this project for side-by-side comparison.",
15123
+ parameters: CompetitorsSchema,
15124
+ execute: async () => {
15125
+ const competitors2 = await ctx.client.listCompetitors(ctx.projectName);
15126
+ return textResult2(competitors2);
15127
+ }
15128
+ };
15129
+ }
15130
+ var RunDetailSchema = Type2.Object({
15131
+ runId: Type2.String({
15132
+ description: "Run id (UUID) to fetch. Typically obtained from get_status runs[].id."
15133
+ })
15134
+ });
15135
+ function buildGetRunTool(ctx) {
15136
+ return {
15137
+ name: "get_run",
15138
+ label: "Get run detail",
15139
+ 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.",
15140
+ parameters: RunDetailSchema,
15141
+ execute: async (_toolCallId, params) => {
15142
+ const run = await ctx.client.getRun(params.runId);
15143
+ return textResult2(run);
15144
+ }
15145
+ };
15146
+ }
15147
+ function buildReadTools(ctx) {
15148
+ return [
15149
+ buildGetStatusTool(ctx),
15150
+ buildGetHealthTool(ctx),
15151
+ buildGetTimelineTool(ctx),
15152
+ buildGetInsightsTool(ctx),
15153
+ buildListKeywordsTool(ctx),
15154
+ buildListCompetitorsTool(ctx),
15155
+ buildGetRunTool(ctx)
15156
+ ];
15157
+ }
15158
+ var RunSweepSchema = Type2.Object({
15159
+ providers: Type2.Optional(
15160
+ Type2.Array(Type2.String(), {
15161
+ description: "Subset of providers to run. Omit to use every configured provider on the project."
15162
+ })
15163
+ ),
15164
+ noLocation: Type2.Optional(
15165
+ Type2.Boolean({
15166
+ description: "Run without a location context. Default: use the project default location."
15167
+ })
15168
+ )
15169
+ });
15170
+ function buildRunSweepTool(ctx) {
15171
+ return {
15172
+ name: "run_sweep",
15173
+ label: "Trigger sweep",
15174
+ 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.",
15175
+ parameters: RunSweepSchema,
15176
+ execute: async (_toolCallId, params) => {
15177
+ const body = {};
15178
+ if (params.providers?.length) body.providers = params.providers;
15179
+ if (params.noLocation) body.noLocation = true;
15180
+ const result = await ctx.client.triggerRun(ctx.projectName, body);
15181
+ return textResult2(result);
15182
+ }
15183
+ };
15184
+ }
15185
+ var DismissInsightSchema = Type2.Object({
15186
+ insightId: Type2.String({
15187
+ description: "Insight id to dismiss. Obtain from get_insights details[].id."
15188
+ })
15189
+ });
15190
+ function buildDismissInsightTool(ctx) {
15191
+ return {
15192
+ name: "dismiss_insight",
15193
+ label: "Dismiss insight",
15194
+ description: "Mark an insight as dismissed so it no longer surfaces in active insight lists. Reversible via the dashboard.",
15195
+ parameters: DismissInsightSchema,
15196
+ execute: async (_toolCallId, params) => {
15197
+ const result = await ctx.client.dismissInsight(ctx.projectName, params.insightId);
15198
+ return textResult2(result);
15199
+ }
15200
+ };
15201
+ }
15202
+ var AddKeywordsSchema = Type2.Object({
15203
+ keywords: Type2.Array(Type2.String(), {
15204
+ minItems: 1,
15205
+ description: "Keywords to add to the tracking list. Duplicates against existing keywords are ignored server-side."
15206
+ })
15207
+ });
15208
+ function buildAddKeywordsTool(ctx) {
15209
+ return {
15210
+ name: "add_keywords",
15211
+ label: "Add keywords",
15212
+ description: "Append keywords to the project tracking list. Additive only \u2014 existing keywords are preserved. Use exact phrasing you want tracked.",
15213
+ parameters: AddKeywordsSchema,
15214
+ execute: async (_toolCallId, params) => {
15215
+ await ctx.client.appendKeywords(ctx.projectName, params.keywords);
15216
+ return textResult2({ added: params.keywords });
15217
+ }
15218
+ };
15219
+ }
15220
+ var AddCompetitorsSchema = Type2.Object({
15221
+ domains: Type2.Array(Type2.String(), {
15222
+ minItems: 1,
15223
+ description: 'Competitor domains to track. Provide bare domains (e.g. "example.com"), not URLs.'
15224
+ })
15225
+ });
15226
+ function buildAddCompetitorsTool(ctx) {
15227
+ return {
15228
+ name: "add_competitors",
15229
+ label: "Add competitors",
15230
+ 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.",
15231
+ parameters: AddCompetitorsSchema,
15232
+ execute: async (_toolCallId, params) => {
15233
+ const existing = await ctx.client.listCompetitors(ctx.projectName);
15234
+ const existingDomains = new Set(existing.map((c) => c.domain));
15235
+ const newDomains = params.domains.filter((d) => !existingDomains.has(d));
15236
+ if (newDomains.length === 0) {
15237
+ return textResult2({ added: [], alreadyTracked: params.domains });
15238
+ }
15239
+ const merged = [...existing.map((c) => c.domain), ...newDomains];
15240
+ await ctx.client.putCompetitors(ctx.projectName, merged);
15241
+ return textResult2({ added: newDomains, alreadyTracked: params.domains.filter((d) => existingDomains.has(d)) });
15242
+ }
15243
+ };
15244
+ }
15245
+ var UpdateScheduleSchema = Type2.Object({
15246
+ cron: Type2.Optional(
15247
+ Type2.String({ description: 'Cron expression (e.g. "0 */6 * * *"). Provide cron OR preset, not both.' })
15248
+ ),
15249
+ preset: Type2.Optional(
15250
+ Type2.String({ description: 'Preset keyword (e.g. "daily", "hourly"). Provide cron OR preset, not both.' })
15251
+ ),
15252
+ timezone: Type2.Optional(Type2.String({ description: 'IANA timezone. Default: "UTC".' })),
15253
+ enabled: Type2.Optional(
15254
+ Type2.Boolean({ description: "Whether the schedule is active. Default: true." })
15255
+ ),
15256
+ providers: Type2.Optional(
15257
+ Type2.Array(Type2.String(), {
15258
+ description: "Providers to run on each scheduled sweep. Omit to use all configured providers."
15259
+ })
15260
+ )
15261
+ });
15262
+ function buildUpdateScheduleTool(ctx) {
15263
+ return {
15264
+ name: "update_schedule",
15265
+ label: "Update schedule",
15266
+ 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.",
15267
+ parameters: UpdateScheduleSchema,
15268
+ execute: async (_toolCallId, params) => {
15269
+ if (params.cron && params.preset || !params.cron && !params.preset) {
15270
+ throw new Error("update_schedule: provide exactly one of `cron` or `preset`");
15271
+ }
15272
+ const body = {};
15273
+ if (params.cron) body.cron = params.cron;
15274
+ if (params.preset) body.preset = params.preset;
15275
+ if (params.timezone) body.timezone = params.timezone;
15276
+ if (params.enabled !== void 0) body.enabled = params.enabled;
15277
+ if (params.providers?.length) body.providers = params.providers;
15278
+ const result = await ctx.client.putSchedule(ctx.projectName, body);
15279
+ return textResult2(result);
15280
+ }
15281
+ };
15282
+ }
15283
+ var AttachAgentWebhookSchema = Type2.Object({
15284
+ url: Type2.String({
15285
+ description: "External agent webhook URL. Canonry will POST run.completed, insight.critical, insight.high, and citation.gained events to it."
15286
+ })
15287
+ });
15288
+ function buildAttachAgentWebhookTool(ctx) {
15289
+ return {
15290
+ name: "attach_agent_webhook",
15291
+ label: "Attach agent webhook",
15292
+ 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.",
15293
+ parameters: AttachAgentWebhookSchema,
15294
+ execute: async (_toolCallId, params) => {
15295
+ const existing = await ctx.client.listNotifications(ctx.projectName);
15296
+ const hasAgent = existing.some((n) => n.source === "agent");
15297
+ if (hasAgent) {
15298
+ return textResult2({ status: "already-attached" });
15299
+ }
15300
+ const result = await ctx.client.createNotification(ctx.projectName, {
15301
+ channel: "webhook",
15302
+ url: params.url,
15303
+ events: ["run.completed", "insight.critical", "insight.high", "citation.gained"],
15304
+ source: "agent"
15305
+ });
15306
+ return textResult2({ status: "attached", notificationId: result.id, url: params.url });
15307
+ }
15308
+ };
15309
+ }
15310
+ function buildWriteTools(ctx) {
15311
+ return [
15312
+ buildRunSweepTool(ctx),
15313
+ buildDismissInsightTool(ctx),
15314
+ buildAddKeywordsTool(ctx),
15315
+ buildAddCompetitorsTool(ctx),
15316
+ buildUpdateScheduleTool(ctx),
15317
+ buildAttachAgentWebhookTool(ctx)
15318
+ ];
15319
+ }
15320
+ function buildAllTools(ctx) {
15321
+ return [...buildReadTools(ctx), ...buildWriteTools(ctx)];
15322
+ }
15323
+
15324
+ // src/agent/session.ts
15325
+ var builtinsRegistered = false;
15326
+ function ensureBuiltinsRegistered() {
15327
+ if (!builtinsRegistered) {
15328
+ registerBuiltInApiProviders();
15329
+ validateAgentProviderRegistry();
15330
+ builtinsRegistered = true;
15331
+ }
15332
+ }
15333
+ function loadAeroSystemPrompt(pkgDir) {
15334
+ const skillDir = resolveAeroSkillDir(pkgDir);
15335
+ const skillBody = fs7.readFileSync(path8.join(skillDir, "SKILL.md"), "utf-8");
15336
+ const soulPath = path8.join(skillDir, "soul.md");
15337
+ if (!fs7.existsSync(soulPath)) return skillBody;
15338
+ const soulBody = fs7.readFileSync(soulPath, "utf-8");
15339
+ return `${soulBody.trimEnd()}
15340
+
15341
+ ---
15342
+
15343
+ ${skillBody}`;
15344
+ }
15345
+ function missingProviderMessage() {
15346
+ const configHints = agentProvidersByPriority().join(", ");
15347
+ const envHints = agentProvidersByPriority().map((p) => `${AGENT_PROVIDERS[p].piAiProvider.toUpperCase()}_API_KEY`).join(" / ");
15348
+ return `No agent LLM provider configured. Add an API key for one of: ${configHints} in ~/.canonry/config.yaml, or export ${envHints}.`;
15349
+ }
15350
+ function detectAgentProvider(config) {
15351
+ for (const provider of agentProvidersByPriority()) {
15352
+ if (resolveApiKeyFor(provider, config)) return provider;
15353
+ }
15354
+ return void 0;
15355
+ }
15356
+ function resolveAeroModel(provider, modelId) {
15357
+ ensureBuiltinsRegistered();
15358
+ return resolveModelForProvider(provider, modelId);
15359
+ }
15360
+ function buildApiKeyResolver(config) {
15361
+ return (piAiProvider) => resolveApiKeyFor(piAiProvider, config);
15362
+ }
15363
+ function createAeroSession(opts) {
15364
+ const systemPrompt = opts.systemPromptOverride ?? loadAeroSystemPrompt();
15365
+ const provider = opts.provider ?? detectAgentProvider(opts.config);
15366
+ if (!provider) throw new Error(missingProviderMessage());
15367
+ const model = resolveAeroModel(provider, opts.modelId);
15368
+ const toolScope = opts.toolScope ?? "all";
15369
+ const stateTools = toolScope === "read-only" ? buildReadTools({ client: opts.client, projectName: opts.projectName }) : buildAllTools({ client: opts.client, projectName: opts.projectName });
15370
+ const defaultTools = [...stateTools, ...buildSkillDocTools()];
15371
+ const tools = opts.tools ?? defaultTools;
15372
+ return new Agent({
15373
+ initialState: {
15374
+ systemPrompt,
15375
+ model,
15376
+ tools,
15377
+ ...opts.initialMessages ? { messages: opts.initialMessages } : {}
15378
+ },
15379
+ streamFn: opts.streamFn,
15380
+ getApiKey: buildApiKeyResolver(opts.config)
15381
+ });
15382
+ }
15383
+ function resolveSessionProviderAndModel(config, opts) {
15384
+ const provider = opts?.provider ?? detectAgentProvider(config);
15385
+ if (!provider) throw new Error(missingProviderMessage());
15386
+ const modelId = opts?.modelId ?? getAgentProvider(provider).defaultModel;
15387
+ return { provider, modelId };
15388
+ }
15389
+
15390
+ // src/agent/session-registry.ts
15391
+ var log7 = createLogger("SessionRegistry");
15392
+ var SessionRegistry = class {
15393
+ live = /* @__PURE__ */ new Map();
15394
+ pending = /* @__PURE__ */ new Map();
15395
+ /** Last tool scope used on the live Agent for a project. Read in getOrCreate to know when to swap. */
15396
+ scopes = /* @__PURE__ */ new Map();
15397
+ opts;
15398
+ constructor(opts) {
15399
+ this.opts = opts;
15400
+ }
15401
+ /** Read-only access to the config snapshot the registry was built with. */
15402
+ getConfig() {
15403
+ return this.opts.config;
15404
+ }
15405
+ /**
15406
+ * Returns the live Agent for a project, hydrating from DB or creating
15407
+ * fresh. Applies caller preferences on fresh/hydrated construction. Does
15408
+ * NOT mutate an already-cached Agent — that path goes through
15409
+ * `acquireForTurn`, which gates scope/model changes behind a busy check
15410
+ * so an in-flight turn can't have its tools swapped out from under it.
15411
+ */
15412
+ getOrCreate(projectName, preferences) {
15413
+ const cached = this.live.get(projectName);
15414
+ if (cached) return cached;
15415
+ const projectId = this.resolveProjectId(projectName);
15416
+ const row = this.loadRow(projectId);
15417
+ if (row) {
15418
+ const persistedMessages = parseJsonColumn(row.messages, []);
15419
+ const queued = parseJsonColumn(row.followUpQueue, []);
15420
+ const effectiveProvider = preferences?.provider ?? row.modelProvider;
15421
+ const effectiveModelId = preferences?.modelId ?? row.modelId;
15422
+ if (preferences?.provider || preferences?.modelId) {
15423
+ this.opts.db.update(agentSessions).set({
15424
+ modelProvider: effectiveProvider,
15425
+ modelId: effectiveModelId,
15426
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15427
+ }).where(eq23(agentSessions.projectId, projectId)).run();
15428
+ }
15429
+ const agent2 = createAeroSession({
15430
+ projectName,
15431
+ client: this.opts.client,
15432
+ config: this.opts.config,
15433
+ provider: effectiveProvider,
15434
+ modelId: effectiveModelId,
15435
+ systemPromptOverride: row.systemPrompt,
15436
+ initialMessages: persistedMessages,
15437
+ toolScope: preferences?.toolScope
15438
+ });
15439
+ this.scopes.set(projectName, preferences?.toolScope ?? "all");
15440
+ if (queued.length > 0) {
15441
+ this.appendPending(projectName, queued);
15442
+ this.updateRow(projectId, { followUpQueue: "[]" });
15443
+ }
15444
+ this.live.set(projectName, agent2);
15445
+ this.registerDrainHook(agent2, projectName);
15446
+ return agent2;
15447
+ }
15448
+ const { provider, modelId } = resolveSessionProviderAndModel(this.opts.config, preferences);
15449
+ const systemPrompt = loadAeroSystemPrompt();
15450
+ const agent = createAeroSession({
15451
+ projectName,
15452
+ client: this.opts.client,
15453
+ config: this.opts.config,
15454
+ provider,
15455
+ modelId,
15456
+ systemPromptOverride: systemPrompt,
15457
+ toolScope: preferences?.toolScope
15458
+ });
15459
+ this.scopes.set(projectName, preferences?.toolScope ?? "all");
15460
+ this.insertRow({
15461
+ projectId,
15462
+ systemPrompt,
15463
+ modelProvider: provider,
15464
+ modelId,
15465
+ messages: [],
15466
+ followUpQueue: []
15467
+ });
15468
+ this.live.set(projectName, agent);
15469
+ this.registerDrainHook(agent, projectName);
15470
+ return agent;
15471
+ }
15472
+ /**
15473
+ * Acquire the Agent for an upcoming prompt/turn.
15474
+ *
15475
+ * Busy-check runs FIRST, before any state mutation — if two requests race
15476
+ * on the same project, one gets the 409 and the other's in-flight turn is
15477
+ * untouched. Only after confirming idle do we:
15478
+ * - align `state.tools` to the requested scope (CLI full vs dashboard
15479
+ * read-only share the same cached Agent; each request re-scopes it).
15480
+ * - align `state.model` when the caller passes `provider` or `modelId`,
15481
+ * honoring `--provider` / `--model` on hot sessions (not just on
15482
+ * fresh/hydrated construction).
15483
+ *
15484
+ * Persists the new model choice to the DB row so subsequent invocations
15485
+ * stay on it unless overridden again.
15486
+ */
15487
+ acquireForTurn(projectName, preferences) {
15488
+ const agent = this.getOrCreate(projectName);
15489
+ if (agent.state.isStreaming) {
15490
+ throw agentBusy(projectName);
15491
+ }
15492
+ this.alignScope(projectName, agent, preferences?.toolScope ?? "all");
15493
+ if (preferences?.provider || preferences?.modelId) {
15494
+ this.alignModel(projectName, agent, preferences);
15495
+ }
15496
+ return agent;
15497
+ }
15498
+ alignScope(projectName, agent, wantScope) {
15499
+ if (this.scopes.get(projectName) === wantScope) return;
15500
+ const stateTools = wantScope === "read-only" ? buildReadTools({ client: this.opts.client, projectName }) : buildAllTools({ client: this.opts.client, projectName });
15501
+ agent.state.tools = [...stateTools, ...buildSkillDocTools()];
15502
+ this.scopes.set(projectName, wantScope);
15503
+ }
15504
+ alignModel(projectName, agent, preferences) {
15505
+ const projectId = this.tryResolveProjectId(projectName);
15506
+ if (!projectId) return;
15507
+ const row = this.loadRow(projectId);
15508
+ const currentProvider = row?.modelProvider ?? AgentProviderIds.claude;
15509
+ const currentModelId = row?.modelId;
15510
+ const nextProvider = preferences.provider ?? currentProvider;
15511
+ const nextModelId = preferences.modelId ?? (preferences.provider ? getAgentProvider(nextProvider).defaultModel : currentModelId);
15512
+ if (!nextModelId) return;
15513
+ if (nextProvider === currentProvider && nextModelId === currentModelId) return;
15514
+ agent.state.model = resolveAeroModel(nextProvider, nextModelId);
15515
+ this.opts.db.update(agentSessions).set({
15516
+ modelProvider: nextProvider,
15517
+ modelId: nextModelId,
15518
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15519
+ }).where(eq23(agentSessions.projectId, projectId)).run();
15520
+ }
15521
+ /** Persist a session's transcript back to the DB. Call after any run settles. */
15522
+ save(projectName) {
15523
+ const agent = this.live.get(projectName);
15524
+ if (!agent) return;
15525
+ const projectId = this.resolveProjectId(projectName);
15526
+ this.updateRow(projectId, {
15527
+ messages: JSON.stringify(agent.state.messages)
15528
+ });
15529
+ }
15530
+ /**
15531
+ * Enqueue a message for the next turn.
15532
+ *
15533
+ * - Live session exists: append to the in-memory pending queue. The next
15534
+ * `consumePending`-backed prompt, a `drainNow` call, or the post-`agent_end`
15535
+ * drain hook will process it.
15536
+ * - No live session: persist to the DB follow-up queue. The next
15537
+ * `getOrCreate` hydrates the agent and migrates the queue into pending.
15538
+ *
15539
+ * Crucially writes to exactly ONE of the two sinks to avoid the duplicate
15540
+ * message we saw during the first end-to-end dogfood (run.completed fired,
15541
+ * both the in-memory pending and the DB-queue migration produced copies).
15542
+ */
15543
+ queueFollowUp(projectName, message) {
15544
+ if (this.live.has(projectName)) {
15545
+ this.appendPending(projectName, [message]);
15546
+ } else {
15547
+ this.persistQueueAppend(projectName, message);
15548
+ }
15549
+ }
15550
+ /** Consume (and clear) the pending queue for a project. Caller prompts with the result. */
15551
+ consumePending(projectName) {
15552
+ const msgs = this.pending.get(projectName) ?? [];
15553
+ if (msgs.length === 0) return [];
15554
+ this.pending.delete(projectName);
15555
+ const projectId = this.tryResolveProjectId(projectName);
15556
+ if (projectId) this.updateRow(projectId, { followUpQueue: "[]" });
15557
+ return msgs;
15558
+ }
15559
+ /**
15560
+ * Proactive drain — hydrate if needed, consume pending, prompt the agent.
15561
+ *
15562
+ * No-op when:
15563
+ * - there are no pending messages in memory AND no persisted queue in
15564
+ * the DB (post-restart / never-hydrated sessions still need to wake)
15565
+ * - the agent is currently streaming (it will pick them up on the next turn)
15566
+ *
15567
+ * Fire-and-forget safe: failures are logged, never thrown. This is what
15568
+ * RunCoordinator calls after a run completes to wake Aero unprompted.
15569
+ */
15570
+ async drainNow(projectName) {
15571
+ if (!this.hasPendingWork(projectName)) return;
15572
+ try {
15573
+ let agent;
15574
+ try {
15575
+ const scope = this.scopes.get(projectName) ?? "read-only";
15576
+ agent = this.acquireForTurn(projectName, { toolScope: scope });
15577
+ } catch (err) {
15578
+ if (err.code === "AGENT_BUSY") return;
15579
+ throw err;
15580
+ }
15581
+ const msgs = this.consumePending(projectName);
15582
+ if (msgs.length === 0) return;
15583
+ await agent.prompt(msgs);
15584
+ this.save(projectName);
15585
+ } catch (err) {
15586
+ log7.error("drain.failed", {
15587
+ projectName,
15588
+ error: err instanceof Error ? err.message : String(err)
15589
+ });
15590
+ }
15591
+ }
15592
+ /** Drop the live Agent for a project. Next lookup rehydrates from DB. */
15593
+ evict(projectName) {
15594
+ this.live.delete(projectName);
15595
+ }
15596
+ /**
15597
+ * Authoritative reset for a project's session state. Drops the live Agent,
15598
+ * clears the in-memory pending follow-up buffer, and forgets the cached
15599
+ * tool scope. Caller is responsible for wiping the durable row; this only
15600
+ * touches the in-process state the registry holds.
15601
+ *
15602
+ * Use this (not `evict`) when the caller guarantees the conversation is
15603
+ * being wiped — e.g. `DELETE /agent/transcript`. `evict` alone leaves any
15604
+ * in-memory follow-ups queued on a hot session, which would leak into the
15605
+ * next turn after the reset.
15606
+ */
15607
+ reset(projectName) {
15608
+ this.live.delete(projectName);
15609
+ this.pending.delete(projectName);
15610
+ this.scopes.delete(projectName);
15611
+ }
15612
+ /** Evict every live Agent. Durable state in DB is untouched. */
15613
+ clear() {
15614
+ this.live.clear();
15615
+ }
15616
+ /** Visible so tests can assert whether a session is hot. */
15617
+ isLive(projectName) {
15618
+ return this.live.has(projectName);
15619
+ }
15620
+ /** Visible so tests can peek at the pending queue without consuming. */
15621
+ peekPending(projectName) {
15622
+ return this.pending.get(projectName) ?? [];
15623
+ }
15624
+ // ──────────────────────────────────────────────────────────────────
15625
+ /**
15626
+ * True when there's in-memory pending work OR a persisted follow-up queue
15627
+ * for this project. Checked by `drainNow` before doing any hydration work
15628
+ * so proactive wake-up fires even on cold / post-restart sessions where
15629
+ * the follow-up lives only in the DB row.
15630
+ */
15631
+ hasPendingWork(projectName) {
15632
+ if ((this.pending.get(projectName) ?? []).length > 0) return true;
15633
+ const projectId = this.tryResolveProjectId(projectName);
15634
+ if (!projectId) return false;
15635
+ const row = this.loadRow(projectId);
15636
+ if (!row) return false;
15637
+ return parseJsonColumn(row.followUpQueue, []).length > 0;
15638
+ }
15639
+ appendPending(projectName, messages) {
15640
+ if (messages.length === 0) return;
15641
+ const existing = this.pending.get(projectName) ?? [];
15642
+ this.pending.set(projectName, [...existing, ...messages]);
15643
+ }
15644
+ persistQueueAppend(projectName, message) {
15645
+ const projectId = this.tryResolveProjectId(projectName);
15646
+ if (!projectId) return;
15647
+ const row = this.loadRow(projectId);
15648
+ if (!row) {
15649
+ this.insertRow({
15650
+ projectId,
15651
+ systemPrompt: loadAeroSystemPrompt(),
15652
+ ...resolveSessionProviderAndModel(this.opts.config),
15653
+ messages: [],
15654
+ followUpQueue: [message]
15655
+ });
15656
+ return;
15657
+ }
15658
+ const existing = parseJsonColumn(row.followUpQueue, []);
15659
+ this.updateRow(projectId, { followUpQueue: JSON.stringify([...existing, message]) });
15660
+ }
15661
+ /**
15662
+ * Subscribe to agent_end so any pending messages that landed during a run
15663
+ * (from RunCoordinator callbacks or steered follow-ups) drain automatically
15664
+ * after the current turn settles. Without this, a RunCoordinator event that
15665
+ * arrives mid-CLI-turn would sit in pending until someone called drainNow.
15666
+ */
15667
+ registerDrainHook(agent, projectName) {
15668
+ agent.subscribe((event) => {
15669
+ if (event.type !== "agent_end") return;
15670
+ if ((this.pending.get(projectName) ?? []).length === 0) return;
15671
+ void this.drainNow(projectName);
15672
+ });
15673
+ }
15674
+ resolveProjectId(projectName) {
15675
+ const id = this.tryResolveProjectId(projectName);
15676
+ if (!id) throw new Error(`Project "${projectName}" not found`);
15677
+ return id;
15678
+ }
15679
+ tryResolveProjectId(projectName) {
15680
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq23(projects.name, projectName)).get();
15681
+ return row?.id;
15682
+ }
15683
+ loadRow(projectId) {
15684
+ const row = this.opts.db.select().from(agentSessions).where(eq23(agentSessions.projectId, projectId)).get();
15685
+ return row ?? null;
15686
+ }
15687
+ insertRow(params) {
15688
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15689
+ this.opts.db.insert(agentSessions).values({
15690
+ id: crypto22.randomUUID(),
15691
+ projectId: params.projectId,
15692
+ systemPrompt: params.systemPrompt,
15693
+ modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
15694
+ modelId: params.modelId ?? "claude-opus-4-7",
15695
+ messages: JSON.stringify(params.messages),
15696
+ followUpQueue: JSON.stringify(params.followUpQueue),
15697
+ createdAt: now,
15698
+ updatedAt: now
15699
+ }).run();
15700
+ }
15701
+ updateRow(projectId, patch) {
15702
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15703
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq23(agentSessions.projectId, projectId)).run();
15704
+ }
15705
+ };
15706
+
15707
+ // src/agent/agent-routes.ts
15708
+ import { eq as eq24 } from "drizzle-orm";
15709
+ function resolveProject2(db, name) {
15710
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq24(projects.name, name)).get();
15711
+ if (!row) throw notFound("project", name);
15712
+ return row;
15713
+ }
15714
+ function registerAgentRoutes(app, opts) {
15715
+ app.get(
15716
+ "/projects/:name/agent/transcript",
15717
+ async (request) => {
15718
+ const project = resolveProject2(opts.db, request.params.name);
15719
+ const row = opts.db.select().from(agentSessions).where(eq24(agentSessions.projectId, project.id)).get();
15720
+ if (!row) {
15721
+ return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
15722
+ }
15723
+ return {
15724
+ messages: parseJsonColumn(row.messages, []),
15725
+ modelProvider: row.modelProvider,
15726
+ modelId: row.modelId,
15727
+ updatedAt: row.updatedAt
15728
+ };
15729
+ }
15730
+ );
15731
+ app.get(
15732
+ "/projects/:name/agent/providers",
15733
+ async (request) => {
15734
+ resolveProject2(opts.db, request.params.name);
15735
+ return buildAgentProvidersResponse(opts.sessionRegistry.getConfig());
15736
+ }
15737
+ );
15738
+ app.delete(
15739
+ "/projects/:name/agent/transcript",
15740
+ async (request) => {
15741
+ const project = resolveProject2(opts.db, request.params.name);
15742
+ opts.sessionRegistry.reset(project.name);
15743
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(agentSessions.projectId, project.id)).run();
15744
+ return { status: "reset" };
15745
+ }
15746
+ );
15747
+ app.post("/projects/:name/agent/prompt", async (request, reply) => {
15748
+ const project = resolveProject2(opts.db, request.params.name);
15749
+ const promptText = (request.body?.prompt ?? "").trim();
15750
+ if (!promptText) throw validationError('"prompt" is required');
15751
+ const requestedScope = request.body?.scope === "all" ? "all" : "read-only";
15752
+ const agent = opts.sessionRegistry.acquireForTurn(project.name, {
15753
+ provider: request.body?.provider,
15754
+ modelId: request.body?.modelId,
15755
+ toolScope: requestedScope
15756
+ });
15757
+ reply.raw.writeHead(200, {
15758
+ "Content-Type": "text/event-stream",
15759
+ "Cache-Control": "no-cache",
15760
+ "Connection": "keep-alive",
15761
+ "X-Accel-Buffering": "no"
15762
+ });
15763
+ const write = (payload) => {
15764
+ if (reply.raw.writableEnded) return;
15765
+ try {
15766
+ reply.raw.write(`data: ${JSON.stringify(payload)}
15767
+
15768
+ `);
15769
+ } catch {
15770
+ }
15771
+ };
15772
+ write({ type: "stream_open" });
15773
+ const unsubscribe = agent.subscribe((event) => {
15774
+ write(event);
15775
+ });
15776
+ reply.raw.once("close", () => {
15777
+ if (!reply.raw.writableEnded) {
15778
+ agent.abort();
15779
+ }
15780
+ });
15781
+ try {
15782
+ const pending = opts.sessionRegistry.consumePending(project.name);
15783
+ const userMessage = {
15784
+ role: "user",
15785
+ content: promptText,
15786
+ timestamp: Date.now()
15787
+ };
15788
+ const batch = pending.length > 0 ? [...pending, userMessage] : userMessage;
15789
+ await agent.prompt(batch);
15790
+ await agent.waitForIdle();
15791
+ opts.sessionRegistry.save(project.name);
15792
+ } catch (err) {
15793
+ write({ type: "error", message: err instanceof Error ? err.message : String(err) });
15794
+ } finally {
15795
+ unsubscribe();
15796
+ write({ type: "stream_close" });
15797
+ if (!reply.raw.writableEnded) {
15798
+ reply.raw.end();
15799
+ }
15800
+ }
15801
+ return reply;
15802
+ });
15803
+ }
15804
+
15805
+ // src/client.ts
15806
+ function createApiClient() {
15807
+ const config = loadConfig();
15808
+ const basePathResolved = !!config.basePath || "CANONRY_BASE_PATH" in process.env;
15809
+ return new ApiClient(config.apiUrl, config.apiKey, { skipProbe: basePathResolved });
15810
+ }
15811
+ var ApiClient = class {
15812
+ baseUrl;
15813
+ originUrl;
15814
+ apiKey;
15815
+ probePromise = null;
15816
+ probeSkipped;
15817
+ constructor(baseUrl, apiKey, opts) {
15818
+ this.originUrl = baseUrl.replace(/\/$/, "");
15819
+ this.baseUrl = this.originUrl + "/api/v1";
15820
+ this.apiKey = apiKey;
15821
+ this.probeSkipped = opts?.skipProbe ?? false;
15822
+ }
15823
+ /**
15824
+ * On first API call, probe /health to auto-discover basePath when the user
15825
+ * hasn't configured one locally. This lets `canonry run` in a separate shell
15826
+ * discover that the server is running at e.g. /canonry/ without requiring
15827
+ * config.yaml edits or CANONRY_BASE_PATH in every shell.
15828
+ */
15829
+ probeBasePath() {
15830
+ if (this.probeSkipped) return Promise.resolve();
15831
+ if (!this.probePromise) {
15832
+ this.probePromise = (async () => {
15833
+ try {
15834
+ const origin = new URL(this.originUrl).origin;
15835
+ const res = await fetch(`${origin}/health`, {
15836
+ signal: AbortSignal.timeout(2e3)
15837
+ });
15838
+ if (res.ok) {
15839
+ const body = await res.json();
15840
+ if (body.basePath && typeof body.basePath === "string") {
15841
+ const normalized = "/" + body.basePath.replace(/^\/|\/$/g, "");
15842
+ if (normalized !== "/") {
15843
+ this.originUrl = origin + normalized;
15844
+ this.baseUrl = this.originUrl + "/api/v1";
15845
+ }
15846
+ }
15847
+ }
15848
+ } catch {
15849
+ }
15850
+ })();
15851
+ }
15852
+ return this.probePromise;
15853
+ }
15854
+ async request(method, path10, body) {
15855
+ await this.probeBasePath();
15856
+ const url = `${this.baseUrl}${path10}`;
15857
+ const serializedBody = body != null ? JSON.stringify(body) : void 0;
15858
+ const headers = {
15859
+ "Authorization": `Bearer ${this.apiKey}`,
15860
+ "Accept": "application/json",
15861
+ ...serializedBody != null ? { "Content-Type": "application/json" } : {}
15862
+ };
15863
+ let res;
15864
+ try {
15865
+ res = await fetch(url, {
15866
+ method,
15867
+ headers,
15868
+ body: serializedBody
15869
+ });
15870
+ } catch (err) {
15871
+ if (err instanceof CliError) throw err;
15872
+ const msg = err instanceof Error ? err.message : String(err);
15873
+ if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("connect ECONNREFUSED")) {
15874
+ throw new CliError({
15875
+ code: "CONNECTION_ERROR",
15876
+ 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).`,
15877
+ exitCode: EXIT_SYSTEM_ERROR
15878
+ });
15879
+ }
15880
+ throw new CliError({ code: "CONNECTION_ERROR", message: msg, exitCode: EXIT_SYSTEM_ERROR });
15881
+ }
15882
+ if (!res.ok) {
15883
+ let errorBody;
15884
+ try {
15885
+ errorBody = await res.json();
15886
+ } catch {
15887
+ errorBody = { error: { code: "UNKNOWN", message: res.statusText } };
15888
+ }
15889
+ const errorObj = errorBody && typeof errorBody === "object" && "error" in errorBody && errorBody.error && typeof errorBody.error === "object" ? errorBody.error : null;
15890
+ const msg = errorObj?.message ? String(errorObj.message) : `HTTP ${res.status}: ${res.statusText}`;
15891
+ const code = errorObj?.code ? String(errorObj.code) : "API_ERROR";
15892
+ const exitCode = res.status >= 500 ? EXIT_SYSTEM_ERROR : EXIT_USER_ERROR;
15893
+ throw new CliError({ code, message: msg, exitCode });
15894
+ }
15895
+ if (res.status === 204) {
15896
+ return void 0;
15897
+ }
15898
+ return await res.json();
15899
+ }
15900
+ async getAgentTranscript(project) {
15901
+ return this.request(
15902
+ "GET",
15903
+ `/projects/${encodeURIComponent(project)}/agent/transcript`
15904
+ );
15905
+ }
15906
+ async resetAgentTranscript(project) {
15907
+ await this.request(
15908
+ "DELETE",
15909
+ `/projects/${encodeURIComponent(project)}/agent/transcript`
15910
+ );
15911
+ }
15912
+ async listAgentProviders(project) {
15913
+ return this.request(
15914
+ "GET",
15915
+ `/projects/${encodeURIComponent(project)}/agent/providers`
15916
+ );
15917
+ }
15918
+ /**
15919
+ * POST a request whose response body the caller intends to consume as a
15920
+ * stream (e.g. the Aero agent SSE endpoint). Shares the probe + auth +
15921
+ * structured-error behavior of `request()`; the caller reads `res.body`
15922
+ * and releases the response when done.
15923
+ */
15924
+ async streamPost(path10, body, signal) {
15925
+ await this.probeBasePath();
15926
+ const url = `${this.baseUrl}${path10}`;
15927
+ const headers = {
15928
+ Authorization: `Bearer ${this.apiKey}`,
15929
+ "Content-Type": "application/json",
15930
+ Accept: "text/event-stream"
15931
+ };
15932
+ let res;
15933
+ try {
15934
+ res = await fetch(url, {
15935
+ method: "POST",
15936
+ headers,
15937
+ body: JSON.stringify(body ?? {}),
15938
+ signal
15939
+ });
15940
+ } catch (err) {
15941
+ if (err instanceof CliError) throw err;
15942
+ const msg = err instanceof Error ? err.message : String(err);
15943
+ if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("connect ECONNREFUSED")) {
15944
+ throw new CliError({
15945
+ code: "CONNECTION_ERROR",
15946
+ 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).`,
15947
+ exitCode: EXIT_SYSTEM_ERROR
15948
+ });
15949
+ }
15950
+ throw new CliError({ code: "CONNECTION_ERROR", message: msg, exitCode: EXIT_SYSTEM_ERROR });
15951
+ }
15952
+ if (!res.ok || !res.body) {
15953
+ let errorBody;
15954
+ try {
15955
+ errorBody = await res.json();
15956
+ } catch {
15957
+ errorBody = { error: { code: "UNKNOWN", message: res.statusText } };
15958
+ }
15959
+ const errorObj = errorBody && typeof errorBody === "object" && "error" in errorBody && errorBody.error ? errorBody.error : null;
15960
+ const msg = errorObj?.message ? String(errorObj.message) : `HTTP ${res.status}: ${res.statusText}`;
15961
+ const code = errorObj?.code ? String(errorObj.code) : "API_ERROR";
15962
+ const exitCode = res.status >= 500 ? EXIT_SYSTEM_ERROR : EXIT_USER_ERROR;
15963
+ throw new CliError({ code, message: msg, exitCode });
15964
+ }
15965
+ return res;
15966
+ }
15967
+ async putProject(name, body) {
15968
+ return this.request("PUT", `/projects/${encodeURIComponent(name)}`, body);
15969
+ }
15970
+ async listProjects() {
15971
+ return this.request("GET", "/projects");
15972
+ }
15973
+ async getProject(name) {
15974
+ return this.request("GET", `/projects/${encodeURIComponent(name)}`);
15975
+ }
15976
+ async deleteProject(name) {
15977
+ await this.request("DELETE", `/projects/${encodeURIComponent(name)}`);
15978
+ }
15979
+ async putKeywords(project, keywords2) {
15980
+ await this.request("PUT", `/projects/${encodeURIComponent(project)}/keywords`, { keywords: keywords2 });
15981
+ }
15982
+ async listKeywords(project) {
15983
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/keywords`);
15984
+ }
15985
+ async deleteKeywords(project, keywords2) {
15986
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/keywords`, { keywords: keywords2 });
15987
+ }
15988
+ async appendKeywords(project, keywords2) {
15989
+ await this.request("POST", `/projects/${encodeURIComponent(project)}/keywords`, { keywords: keywords2 });
15990
+ }
15991
+ async putCompetitors(project, competitors2) {
15992
+ await this.request("PUT", `/projects/${encodeURIComponent(project)}/competitors`, { competitors: competitors2 });
15993
+ }
15994
+ async listCompetitors(project) {
15995
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/competitors`);
15996
+ }
15997
+ async triggerRun(project, body) {
15998
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/runs`, body ?? {});
15999
+ }
16000
+ async listRuns(project, limit) {
16001
+ const query = limit != null ? `?limit=${encodeURIComponent(String(limit))}` : "";
16002
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/runs${query}`);
16003
+ }
16004
+ async getRun(id) {
16005
+ return this.request("GET", `/runs/${encodeURIComponent(id)}`);
16006
+ }
16007
+ async cancelRun(id) {
16008
+ return this.request("POST", `/runs/${encodeURIComponent(id)}/cancel`);
16009
+ }
16010
+ async getTimeline(project) {
16011
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/timeline`);
16012
+ }
16013
+ async getHistory(project) {
16014
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/history`);
16015
+ }
16016
+ async getExport(project) {
16017
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/export`);
16018
+ }
16019
+ async apply(config) {
16020
+ return this.request("POST", "/apply", config);
16021
+ }
16022
+ async getStatus(project) {
16023
+ return this.request("GET", `/projects/${encodeURIComponent(project)}`);
16024
+ }
16025
+ async getSettings() {
16026
+ return this.request("GET", "/settings");
16027
+ }
16028
+ async createSnapshot(body) {
16029
+ return this.request("POST", "/snapshot", body);
16030
+ }
16031
+ async updateProvider(name, body) {
16032
+ return this.request("PUT", `/settings/providers/${encodeURIComponent(name)}`, body);
16033
+ }
16034
+ async putSchedule(project, body) {
16035
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/schedule`, body);
16036
+ }
16037
+ async getSchedule(project) {
16038
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/schedule`);
16039
+ }
16040
+ async deleteSchedule(project) {
16041
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/schedule`);
16042
+ }
16043
+ async createNotification(project, body) {
16044
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/notifications`, body);
16045
+ }
16046
+ async listNotifications(project) {
16047
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/notifications`);
16048
+ }
16049
+ async deleteNotification(project, id) {
16050
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/notifications/${encodeURIComponent(id)}`);
16051
+ }
16052
+ async testNotification(project, id) {
16053
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/notifications/${encodeURIComponent(id)}/test`);
16054
+ }
16055
+ async addLocation(project, body) {
16056
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/locations`, body);
16057
+ }
16058
+ async listLocations(project) {
16059
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/locations`);
16060
+ }
16061
+ async removeLocation(project, label) {
16062
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/locations/${encodeURIComponent(label)}`);
16063
+ }
16064
+ async setDefaultLocation(project, label) {
16065
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/locations/default`, { label });
16066
+ }
16067
+ async getTelemetry() {
16068
+ return this.request("GET", "/telemetry");
16069
+ }
16070
+ async updateTelemetry(enabled) {
16071
+ return this.request("PUT", "/telemetry", { enabled });
16072
+ }
16073
+ async generateKeywords(project, provider, count) {
16074
+ return this.request(
16075
+ "POST",
16076
+ `/projects/${encodeURIComponent(project)}/keywords/generate`,
16077
+ { provider, count }
16078
+ );
16079
+ }
16080
+ // Google connection management
16081
+ async googleConnect(project, body) {
16082
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/connect`, body);
16083
+ }
16084
+ async googleConnections(project) {
16085
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/connections`);
16086
+ }
16087
+ async googleDisconnect(project, type) {
16088
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/google/connections/${encodeURIComponent(type)}`);
16089
+ }
16090
+ async googleProperties(project) {
16091
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/properties`);
16092
+ }
16093
+ async googleSetProperty(project, type, propertyId) {
16094
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/google/connections/${encodeURIComponent(type)}/property`, { propertyId });
16095
+ }
16096
+ async googleSetSitemap(project, type, sitemapUrl) {
16097
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/google/connections/${encodeURIComponent(type)}/sitemap`, { sitemapUrl });
16098
+ }
16099
+ // GSC data
16100
+ async gscSync(project, body) {
16101
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/sync`, body ?? {});
16102
+ }
16103
+ async gscPerformance(project, params) {
16104
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16105
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/performance${qs}`);
16106
+ }
16107
+ async gscInspect(project, url) {
16108
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/inspect`, { url });
16109
+ }
16110
+ async gscInspections(project, params) {
16111
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16112
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/inspections${qs}`);
16113
+ }
16114
+ async gscDeindexed(project) {
16115
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/deindexed`);
16116
+ }
16117
+ async gscCoverage(project) {
16118
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/coverage`);
16119
+ }
16120
+ async gscCoverageHistory(project, params) {
16121
+ const qs = params?.limit != null ? `?limit=${params.limit}` : "";
16122
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/coverage/history${qs}`);
16123
+ }
16124
+ async gscInspectSitemap(project, body) {
16125
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/inspect-sitemap`, body ?? {});
16126
+ }
16127
+ async gscSitemaps(project) {
16128
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/sitemaps`);
16129
+ }
16130
+ async gscDiscoverSitemaps(project) {
16131
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/discover-sitemaps`, {});
16132
+ }
16133
+ // Analytics
16134
+ async getAnalyticsMetrics(project, window) {
16135
+ const qs = window ? `?window=${encodeURIComponent(window)}` : "";
16136
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/analytics/metrics${qs}`);
16137
+ }
16138
+ async getAnalyticsGaps(project, window) {
16139
+ const qs = window ? `?window=${encodeURIComponent(window)}` : "";
16140
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/analytics/gaps${qs}`);
16141
+ }
16142
+ async getAnalyticsSources(project, window) {
16143
+ const qs = window ? `?window=${encodeURIComponent(window)}` : "";
16144
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/analytics/sources${qs}`);
16145
+ }
16146
+ // Google Indexing API
16147
+ async googleRequestIndexing(project, body) {
16148
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/indexing/request`, body);
16149
+ }
16150
+ // Bing Webmaster Tools
16151
+ async bingConnect(project, body) {
16152
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/bing/connect`, body);
16153
+ }
16154
+ async bingDisconnect(project) {
16155
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/bing/disconnect`);
16156
+ }
16157
+ async bingStatus(project) {
16158
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/status`);
16159
+ }
16160
+ async bingSites(project) {
16161
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/sites`);
16162
+ }
16163
+ async bingSetSite(project, siteUrl) {
16164
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/bing/set-site`, { siteUrl });
16165
+ }
16166
+ async bingCoverage(project) {
16167
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/coverage`);
16168
+ }
16169
+ async bingCoverageHistory(project, params) {
16170
+ const qs = params?.limit != null ? `?limit=${params.limit}` : "";
16171
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/coverage/history${qs}`);
16172
+ }
16173
+ async bingInspections(project, params) {
16174
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16175
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/inspections${qs}`);
16176
+ }
16177
+ async bingInspectUrl(project, url) {
16178
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/bing/inspect-url`, { url });
16179
+ }
16180
+ async bingRequestIndexing(project, body) {
16181
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/bing/request-indexing`, body);
16182
+ }
16183
+ async bingPerformance(project, params) {
16184
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16185
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/bing/performance${qs}`);
16186
+ }
16187
+ // CDP browser provider
16188
+ async getCdpStatus() {
16189
+ return this.request("GET", "/cdp/status");
16190
+ }
16191
+ async cdpScreenshot(query, targets) {
16192
+ return this.request("POST", "/cdp/screenshot", { query, targets });
16193
+ }
16194
+ async getBrowserDiff(project, runId) {
16195
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/runs/${encodeURIComponent(runId)}/browser-diff`);
16196
+ }
16197
+ // Google Analytics 4
16198
+ async gaConnect(project, body) {
16199
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/ga/connect`, body);
16200
+ }
16201
+ async gaDisconnect(project) {
16202
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/ga/disconnect`);
16203
+ }
16204
+ async gaStatus(project) {
16205
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/status`);
16206
+ }
16207
+ async gaSync(project, body) {
16208
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/ga/sync`, body ?? {});
16209
+ }
16210
+ async gaTraffic(project, params) {
16211
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16212
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/traffic${qs}`);
16213
+ }
16214
+ async gaCoverage(project) {
16215
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/coverage`);
16216
+ }
16217
+ async gaAiReferralHistory(project, params) {
16218
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16219
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/ai-referral-history${qs}`);
16220
+ }
16221
+ async gaSocialReferralHistory(project, params) {
16222
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16223
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/social-referral-history${qs}`);
16224
+ }
16225
+ async gaSocialReferralTrend(project) {
16226
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/social-referral-trend`);
16227
+ }
16228
+ async gaAttributionTrend(project) {
16229
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/attribution-trend`);
16230
+ }
16231
+ async gaSessionHistory(project, params) {
16232
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
16233
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/session-history${qs}`);
16234
+ }
16235
+ async wordpressConnect(project, body) {
16236
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/connect`, body);
16237
+ }
16238
+ async wordpressDisconnect(project) {
16239
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/wordpress/disconnect`);
16240
+ }
16241
+ async wordpressStatus(project) {
16242
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/status`);
16243
+ }
16244
+ async wordpressPages(project, env) {
16245
+ const qs = env ? `?env=${encodeURIComponent(env)}` : "";
16246
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/pages${qs}`);
16247
+ }
16248
+ async wordpressPage(project, slug, env) {
16249
+ const params = new URLSearchParams({ slug });
16250
+ if (env) params.set("env", env);
16251
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/page?${params.toString()}`);
16252
+ }
16253
+ async wordpressCreatePage(project, body) {
16254
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/pages`, body);
16255
+ }
16256
+ async wordpressUpdatePage(project, body) {
16257
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/wordpress/page`, body);
16258
+ }
16259
+ async wordpressSetMeta(project, body) {
16260
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/page/meta`, body);
16261
+ }
16262
+ async wordpressBulkSetMeta(project, body) {
16263
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/pages/meta/bulk`, body);
16264
+ }
16265
+ async wordpressSchema(project, slug, env) {
16266
+ const params = new URLSearchParams({ slug });
16267
+ if (env) params.set("env", env);
16268
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/schema?${params.toString()}`);
16269
+ }
16270
+ async wordpressSetSchema(project, body) {
16271
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/schema/manual`, body);
16272
+ }
16273
+ async wordpressSchemaDeploy(project, body) {
16274
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/schema/deploy`, body);
16275
+ }
16276
+ async wordpressSchemaStatus(project, env) {
16277
+ const params = new URLSearchParams();
16278
+ if (env) params.set("env", env);
16279
+ const qs = params.toString();
16280
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/schema/status${qs ? `?${qs}` : ""}`);
16281
+ }
16282
+ async wordpressOnboard(project, body) {
16283
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/onboard`, body);
16284
+ }
16285
+ async wordpressLlmsTxt(project, env) {
16286
+ const qs = env ? `?env=${encodeURIComponent(env)}` : "";
16287
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/llms-txt${qs}`);
16288
+ }
16289
+ async wordpressSetLlmsTxt(project, body) {
16290
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/llms-txt/manual`, body);
16291
+ }
16292
+ async wordpressAudit(project, env) {
16293
+ const qs = env ? `?env=${encodeURIComponent(env)}` : "";
16294
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/audit${qs}`);
16295
+ }
16296
+ async wordpressDiff(project, slug) {
16297
+ const params = new URLSearchParams({ slug });
16298
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/diff?${params.toString()}`);
16299
+ }
16300
+ async wordpressStagingStatus(project) {
16301
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/wordpress/staging/status`);
16302
+ }
16303
+ async wordpressStagingPush(project) {
16304
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/staging/push`);
16305
+ }
16306
+ // ── Intelligence ──────────────────────────────────────────────────────
16307
+ async getInsights(project, opts) {
16308
+ const params = new URLSearchParams();
16309
+ if (opts?.dismissed) params.set("dismissed", "true");
16310
+ if (opts?.runId) params.set("runId", opts.runId);
16311
+ const qs = params.toString();
16312
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/insights${qs ? `?${qs}` : ""}`);
16313
+ }
16314
+ async dismissInsight(project, id) {
16315
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/insights/${encodeURIComponent(id)}/dismiss`);
16316
+ }
16317
+ async getHealth(project) {
16318
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/health/latest`);
16319
+ }
16320
+ async getHealthHistory(project, limit) {
16321
+ const qs = limit ? `?limit=${limit}` : "";
16322
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/health/history${qs}`);
16323
+ }
16324
+ };
16325
+
16326
+ // src/snapshot-service.ts
16327
+ import { runAeoAudit } from "@ainyc/aeo-audit";
16328
+
16329
+ // src/site-fetch.ts
16330
+ import https2 from "https";
16331
+ var FETCH_TIMEOUT_MS = 1e4;
16332
+ var MAX_TEXT_LENGTH = 4e3;
16333
+ var MAX_BODY_BYTES = 512e3;
16334
+ var USER_AGENT = "Canonry/1.0 (site-analysis)";
16335
+ function extractHostname(domain) {
16336
+ let hostname = domain;
16337
+ try {
16338
+ if (hostname.includes("://")) {
16339
+ hostname = new URL(hostname).hostname;
16340
+ }
16341
+ } catch {
16342
+ }
16343
+ return hostname.replace(/^www\./, "");
16344
+ }
16345
+ function fetchWithPinnedAddress(target) {
16346
+ return new Promise((resolve) => {
16347
+ const port = target.url.port ? Number(target.url.port) : 443;
16348
+ const path10 = target.url.pathname + target.url.search;
16349
+ const req = https2.request(
16350
+ {
16351
+ hostname: target.address,
16352
+ family: target.family,
16353
+ port,
16354
+ path: path10,
16355
+ method: "GET",
16356
+ timeout: FETCH_TIMEOUT_MS,
16357
+ servername: target.url.hostname,
16358
+ // SNI for TLS
16359
+ headers: {
16360
+ Host: target.url.host,
16361
+ "User-Agent": USER_AGENT,
16362
+ Accept: "text/html"
16363
+ }
16364
+ },
16365
+ (res) => {
16366
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
16367
+ if (res.statusCode >= 300 && res.statusCode < 400) {
16368
+ const location = res.headers.location ?? "";
16369
+ res.resume();
16370
+ resolve(`REDIRECT:${location}`);
16371
+ return;
16372
+ }
16373
+ res.resume();
16374
+ resolve("");
16375
+ return;
16376
+ }
16377
+ const contentType = res.headers["content-type"] ?? "";
16378
+ if (!contentType.includes("text/html")) {
16379
+ res.resume();
16380
+ resolve("");
16381
+ return;
16382
+ }
16383
+ const chunks = [];
16384
+ let totalBytes = 0;
16385
+ res.on("data", (chunk) => {
16386
+ totalBytes += chunk.length;
16387
+ if (totalBytes <= MAX_BODY_BYTES) {
16388
+ chunks.push(chunk);
16389
+ }
16390
+ });
16391
+ res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
16392
+ res.on("error", () => resolve(""));
16393
+ }
16394
+ );
16395
+ req.on("timeout", () => req.destroy(new Error("timeout")));
16396
+ req.on("error", () => resolve(""));
16397
+ req.end();
16398
+ });
16399
+ }
16400
+ async function fetchSiteText(domain) {
16401
+ const hostname = extractHostname(domain);
16402
+ const url = `https://${hostname}`;
16403
+ const targetCheck = await resolveWebhookTarget(url);
16404
+ if (!targetCheck.ok) return "";
16405
+ try {
16406
+ const result = await fetchWithPinnedAddress(targetCheck.target);
16407
+ if (result.startsWith("REDIRECT:")) {
16408
+ const location = result.slice("REDIRECT:".length);
16409
+ if (!location) return "";
16410
+ const redirectUrl = new URL(location, url).href;
16411
+ const redirectCheck = await resolveWebhookTarget(redirectUrl);
16412
+ if (!redirectCheck.ok) return "";
16413
+ const redirectResult = await fetchWithPinnedAddress(redirectCheck.target);
16414
+ if (redirectResult.startsWith("REDIRECT:")) return "";
16415
+ return stripHtml2(redirectResult);
16416
+ }
16417
+ return stripHtml2(result);
16418
+ } catch {
16419
+ return "";
16420
+ }
16421
+ }
16422
+ function stripHtml2(html) {
16423
+ if (!html) return "";
16424
+ let text = html.replace(/<script[\s\S]*?<\/script>/gi, " ");
16425
+ text = text.replace(/<style[\s\S]*?<\/style>/gi, " ");
16426
+ text = text.replace(/<[^>]+>/g, " ");
16427
+ text = text.replace(/&amp;/g, "&");
16428
+ text = text.replace(/&lt;/g, "<");
16429
+ text = text.replace(/&gt;/g, ">");
16430
+ text = text.replace(/&quot;/g, '"');
16431
+ text = text.replace(/&#39;/g, "'");
16432
+ text = text.replace(/&nbsp;/g, " ");
16433
+ text = text.replace(/\s+/g, " ").trim();
16434
+ if (text.length > MAX_TEXT_LENGTH) {
16435
+ text = text.slice(0, MAX_TEXT_LENGTH);
16436
+ }
16437
+ return text;
16438
+ }
16439
+
16440
+ // src/snapshot-format.ts
16441
+ function formatAuditFactorScore(factor) {
16442
+ return `${factor.score}/100 (${factor.weight}% weight)`;
16443
+ }
16444
+
16445
+ // src/snapshot-service.ts
16446
+ var log8 = createLogger("Snapshot");
16447
+ var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
16448
+ var SNAPSHOT_QUERY_COUNT = 6;
16449
+ var ProviderExecutionGate2 = class {
16450
+ constructor(maxConcurrency, maxPerMinute) {
16451
+ this.maxConcurrency = maxConcurrency;
16452
+ this.maxPerMinute = maxPerMinute;
16453
+ }
16454
+ window = [];
16455
+ waiters = [];
16456
+ rateLimitChain = Promise.resolve();
16457
+ inFlight = 0;
16458
+ async run(task) {
16459
+ await this.acquire();
16460
+ try {
16461
+ await this.waitForRateLimit();
16462
+ return await task();
16463
+ } finally {
14823
16464
  this.release();
14824
16465
  }
14825
16466
  }
@@ -14945,7 +16586,7 @@ var SnapshotService = class {
14945
16586
  return mapAuditReport(report);
14946
16587
  } catch (err) {
14947
16588
  const message = err instanceof Error ? err.message : String(err);
14948
- log7.warn("audit.failed", { homepageUrl, error: message });
16589
+ log8.warn("audit.failed", { homepageUrl, error: message });
14949
16590
  return {
14950
16591
  url: homepageUrl,
14951
16592
  finalUrl: homepageUrl,
@@ -14975,7 +16616,7 @@ var SnapshotService = class {
14975
16616
  phrases: parsedPhrases
14976
16617
  };
14977
16618
  } catch (err) {
14978
- log7.warn("profile.generation-failed", {
16619
+ log8.warn("profile.generation-failed", {
14979
16620
  domain: ctx.domain,
14980
16621
  provider: ctx.analysisProvider.adapter.name,
14981
16622
  error: err instanceof Error ? err.message : String(err)
@@ -15117,7 +16758,7 @@ var SnapshotService = class {
15117
16758
  recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
15118
16759
  };
15119
16760
  } catch (err) {
15120
- log7.warn("response.analysis-failed", {
16761
+ log8.warn("response.analysis-failed", {
15121
16762
  provider: ctx.analysisProvider.adapter.name,
15122
16763
  error: err instanceof Error ? err.message : String(err)
15123
16764
  });
@@ -15399,499 +17040,6 @@ function clipText(value, length) {
15399
17040
  return `${value.slice(0, length - 3)}...`;
15400
17041
  }
15401
17042
 
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
17043
  // src/server.ts
15896
17044
  var _require2 = createRequire2(import.meta.url);
15897
17045
  var { version: PKG_VERSION } = _require2("../package.json");
@@ -16073,27 +17221,35 @@ async function createServer(opts) {
16073
17221
  jobRunner.recoverStaleRuns();
16074
17222
  const notifier = new Notifier(opts.db, serverUrl);
16075
17223
  const intelligenceService = new IntelligenceService(opts.db);
17224
+ const aeroClient = new ApiClient(opts.config.apiUrl, opts.config.apiKey, { skipProbe: true });
17225
+ const sessionRegistry = new SessionRegistry({
17226
+ db: opts.db,
17227
+ client: aeroClient,
17228
+ config: opts.config
17229
+ });
16076
17230
  const runCoordinator = new RunCoordinator(
16077
17231
  notifier,
16078
17232
  intelligenceService,
16079
- (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result)
17233
+ (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
17234
+ async ({ runId, projectId, insightCount, criticalOrHigh }) => {
17235
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq25(projects.id, projectId)).get();
17236
+ if (!project) return;
17237
+ sessionRegistry.queueFollowUp(project.name, {
17238
+ role: "user",
17239
+ 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.`,
17240
+ timestamp: Date.now()
17241
+ });
17242
+ void sessionRegistry.drainNow(project.name);
17243
+ }
16080
17244
  );
16081
17245
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
16082
17246
  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
- }
17247
+ const orphanedOpenClawDir = path9.join(os5.homedir(), ".openclaw-aero");
17248
+ if (fs8.existsSync(orphanedOpenClawDir)) {
17249
+ app.log.warn(
17250
+ { path: orphanedOpenClawDir },
17251
+ "OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
17252
+ );
16097
17253
  }
16098
17254
  const scheduler = new Scheduler(opts.db, {
16099
17255
  onRunCreated: (runId, projectId, providers2, location) => {
@@ -16216,7 +17372,7 @@ async function createServer(opts) {
16216
17372
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
16217
17373
  if (opts.config.apiKey) {
16218
17374
  const keyHash = hashApiKey(opts.config.apiKey);
16219
- const existing = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, keyHash)).get();
17375
+ const existing = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, keyHash)).get();
16220
17376
  if (!existing) {
16221
17377
  const prefix = opts.config.apiKey.slice(0, 12);
16222
17378
  opts.db.insert(apiKeys).values({
@@ -16268,7 +17424,7 @@ async function createServer(opts) {
16268
17424
  };
16269
17425
  const getDefaultApiKey = () => {
16270
17426
  if (!opts.config.apiKey) return void 0;
16271
- return opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
17427
+ return opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
16272
17428
  };
16273
17429
  const createPasswordSession = (reply) => {
16274
17430
  const key = getDefaultApiKey();
@@ -16325,12 +17481,12 @@ async function createServer(opts) {
16325
17481
  return reply.send({ authenticated: true });
16326
17482
  }
16327
17483
  if (apiKey) {
16328
- const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(apiKey))).get();
17484
+ const key = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(apiKey))).get();
16329
17485
  if (!key || key.revokedAt) {
16330
17486
  const err2 = authInvalid();
16331
17487
  return reply.status(err2.statusCode).send(err2.toJSON());
16332
17488
  }
16333
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(apiKeys.id, key.id)).run();
17489
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(apiKeys.id, key.id)).run();
16334
17490
  const sessionId = createSession(key.id);
16335
17491
  reply.header("set-cookie", serializeSessionCookie({
16336
17492
  name: SESSION_COOKIE_NAME,
@@ -16362,6 +17518,11 @@ async function createServer(opts) {
16362
17518
  skipAuth: false,
16363
17519
  sessionCookieName: SESSION_COOKIE_NAME,
16364
17520
  resolveSessionApiKeyId,
17521
+ // Local-only Aero agent routes. Registered here so they inherit api-routes'
17522
+ // auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
17523
+ registerAuthenticatedRoutes: async (scope) => {
17524
+ registerAgentRoutes(scope, { db: opts.db, sessionRegistry });
17525
+ },
16365
17526
  getGoogleAuthConfig: () => getGoogleAuthConfig(opts.config),
16366
17527
  googleConnectionStore,
16367
17528
  googleStateSecret,
@@ -16394,7 +17555,8 @@ async function createServer(opts) {
16394
17555
  },
16395
17556
  openApiInfo: {
16396
17557
  title: "Canonry API",
16397
- version: PKG_VERSION
17558
+ version: PKG_VERSION,
17559
+ includeCanonryLocal: true
16398
17560
  },
16399
17561
  providerSummary,
16400
17562
  providerAdapters: [...API_ADAPTERS, ...BROWSER_ADAPTERS].map((a) => ({
@@ -16518,17 +17680,6 @@ async function createServer(opts) {
16518
17680
  onProjectDeleted: (projectId) => {
16519
17681
  scheduler.remove(projectId);
16520
17682
  },
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
17683
  getTelemetryStatus: () => {
16533
17684
  const enabled = isTelemetryEnabled();
16534
17685
  return {
@@ -16613,15 +17764,15 @@ async function createServer(opts) {
16613
17764
  return snapshotService.createReport(input);
16614
17765
  }
16615
17766
  });
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");
17767
+ const dirname = path9.dirname(fileURLToPath2(import.meta.url));
17768
+ const assetsDir = path9.join(dirname, "..", "assets");
17769
+ if (fs8.existsSync(assetsDir)) {
17770
+ const indexPath = path9.join(assetsDir, "index.html");
16620
17771
  const injectConfig = (html) => {
16621
17772
  const clientConfig = {};
16622
17773
  if (basePath) clientConfig.basePath = basePath;
16623
17774
  const configScript = `<script>window.__CANONRY_CONFIG__=${JSON.stringify(clientConfig)}</script>`;
16624
- const baseTag = basePath ? `<base href="${basePath}">` : "";
17775
+ const baseTag = `<base href="${basePath ?? "/"}">`;
16625
17776
  return html.replace("<head>", `<head>${baseTag}`).replace("</head>", `${configScript}</head>`);
16626
17777
  };
16627
17778
  const fastifyStatic = await import("@fastify/static");
@@ -16634,8 +17785,8 @@ async function createServer(opts) {
16634
17785
  index: false
16635
17786
  });
16636
17787
  const serveIndex = (_request, reply) => {
16637
- if (fs7.existsSync(indexPath)) {
16638
- const html = fs7.readFileSync(indexPath, "utf-8");
17788
+ if (fs8.existsSync(indexPath)) {
17789
+ const html = fs8.readFileSync(indexPath, "utf-8");
16639
17790
  return reply.type("text/html").send(injectConfig(html));
16640
17791
  }
16641
17792
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -16655,8 +17806,8 @@ async function createServer(opts) {
16655
17806
  if (basePath && !url.startsWith(basePath)) {
16656
17807
  return reply.status(404).send({ error: "Not found", path: request.url });
16657
17808
  }
16658
- if (fs7.existsSync(indexPath)) {
16659
- const html = fs7.readFileSync(indexPath, "utf-8");
17809
+ if (fs8.existsSync(indexPath)) {
17810
+ const html = fs8.readFileSync(indexPath, "utf-8");
16660
17811
  return reply.type("text/html").send(injectConfig(html));
16661
17812
  }
16662
17813
  return reply.status(404).send({ error: "Not found" });
@@ -16675,13 +17826,6 @@ async function createServer(opts) {
16675
17826
  scheduler.start();
16676
17827
  app.addHook("onClose", async () => {
16677
17828
  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
17829
  });
16686
17830
  return app;
16687
17831
  }
@@ -16743,7 +17887,6 @@ export {
16743
17887
  isFirstRun,
16744
17888
  showFirstRunNotice,
16745
17889
  trackEvent,
16746
- EXIT_USER_ERROR,
16747
17890
  EXIT_SYSTEM_ERROR,
16748
17891
  CliError,
16749
17892
  usageError,
@@ -16762,21 +17905,10 @@ export {
16762
17905
  determineCitationState,
16763
17906
  computeCompetitorOverlap,
16764
17907
  extractRecommendedCompetitors,
17908
+ createApiClient,
16765
17909
  setGoogleAuthConfig,
16766
17910
  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,
17911
+ listAgentProviders,
17912
+ coerceAgentProvider,
16781
17913
  createServer
16782
17914
  };