@ainyc/canonry 1.48.2 → 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,11 +1,14 @@
1
1
  import {
2
2
  IntelligenceService,
3
+ agentSessions,
3
4
  apiKeys,
4
5
  auditLog,
5
6
  bingCoverageSnapshots,
6
7
  bingUrlInspections,
7
8
  competitors,
8
9
  createLogger,
10
+ dropLegacyCredentialColumns,
11
+ extractLegacyCredentials,
9
12
  gaAiReferrals,
10
13
  gaSocialReferrals,
11
14
  gaTrafficSnapshots,
@@ -23,7 +26,7 @@ import {
23
26
  runs,
24
27
  schedules,
25
28
  usageCounters
26
- } from "./chunk-JTKHPNGL.js";
29
+ } from "./chunk-GH6WGN5B.js";
27
30
 
28
31
  // src/config.ts
29
32
  import fs from "fs";
@@ -335,10 +338,10 @@ function printCliError(err, format) {
335
338
  // src/server.ts
336
339
  import { createRequire as createRequire2 } from "module";
337
340
  import crypto23 from "crypto";
338
- import fs7 from "fs";
339
- import path8 from "path";
341
+ import fs8 from "fs";
342
+ import path9 from "path";
340
343
  import { fileURLToPath as fileURLToPath2 } from "url";
341
- import { eq as eq24, sql as sql6 } from "drizzle-orm";
344
+ import { eq as eq25 } from "drizzle-orm";
342
345
  import Fastify from "fastify";
343
346
 
344
347
  // ../contracts/src/config-schema.ts
@@ -600,6 +603,13 @@ function notImplemented(message) {
600
603
  function deliveryFailed(message) {
601
604
  return new AppError("DELIVERY_FAILED", message, 502);
602
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
+ }
603
613
 
604
614
  // ../contracts/src/google.ts
605
615
  import { z as z5 } from "zod";
@@ -887,6 +897,37 @@ var wordpressDiffDtoSchema = z7.object({
887
897
  })
888
898
  });
889
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
+
890
931
  // ../contracts/src/run.ts
891
932
  import { z as z8 } from "zod";
892
933
  var runStatusSchema = z8.enum(["queued", "running", "completed", "partial", "failed", "cancelled"]);
@@ -2379,7 +2420,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
2379
2420
  const body = JSON.stringify(payload);
2380
2421
  const isHttps = target.url.protocol === "https:";
2381
2422
  const port = target.url.port ? Number(target.url.port) : isHttps ? 443 : 80;
2382
- const path9 = `${target.url.pathname}${target.url.search}`;
2423
+ const path10 = `${target.url.pathname}${target.url.search}`;
2383
2424
  const headers = {
2384
2425
  "Content-Length": String(Buffer.byteLength(body)),
2385
2426
  "Content-Type": "application/json",
@@ -2395,7 +2436,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
2395
2436
  headers,
2396
2437
  hostname: target.address,
2397
2438
  method: "POST",
2398
- path: path9,
2439
+ path: path10,
2399
2440
  port,
2400
2441
  timeout: REQUEST_TIMEOUT_MS
2401
2442
  };
@@ -5565,10 +5606,91 @@ var routeCatalog = [
5565
5606
  }
5566
5607
  }
5567
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
+ ];
5568
5689
  function buildOpenApiDocument(info = {}) {
5569
5690
  const BASE_PREFIX = "/api/v1";
5570
5691
  const prefix = info.routePrefix ?? BASE_PREFIX;
5571
- const paths = routeCatalog.reduce((acc, route) => {
5692
+ const fullCatalog = info.includeCanonryLocal ? [...routeCatalog, ...canonryLocalRouteCatalog] : routeCatalog;
5693
+ const paths = fullCatalog.reduce((acc, route) => {
5572
5694
  const subpath = route.path.startsWith(BASE_PREFIX) ? route.path.slice(BASE_PREFIX.length) : route.path;
5573
5695
  const fullPath = prefix + subpath;
5574
5696
  const operation = {
@@ -5616,8 +5738,8 @@ async function openApiRoutes(app, opts = {}) {
5616
5738
  return reply.type("application/json").send(buildOpenApiDocument(opts));
5617
5739
  });
5618
5740
  }
5619
- function buildOperationId(method, path9) {
5620
- const parts = path9.split("/").filter(Boolean).map((part) => {
5741
+ function buildOperationId(method, path10) {
5742
+ const parts = path10.split("/").filter(Boolean).map((part) => {
5621
5743
  if (part.startsWith("{") && part.endsWith("}")) {
5622
5744
  return `by-${part.slice(1, -1)}`;
5623
5745
  }
@@ -9263,10 +9385,10 @@ function buildAuthErrorMessage(res, responseText) {
9263
9385
  }
9264
9386
  return "WordPress credentials are invalid or lack permission for this action";
9265
9387
  }
9266
- async function fetchJson(connection, siteUrl, path9, init) {
9388
+ async function fetchJson(connection, siteUrl, path10, init) {
9267
9389
  if (siteUrl.startsWith("http:")) {
9268
9390
  }
9269
- const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path9}`, {
9391
+ const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path10}`, {
9270
9392
  ...init,
9271
9393
  headers: {
9272
9394
  "Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
@@ -10856,41 +10978,14 @@ async function apiRoutes(app, opts) {
10856
10978
  googleConnectionStore: opts.googleConnectionStore,
10857
10979
  getGoogleAuthConfig: opts.getGoogleAuthConfig
10858
10980
  });
10981
+ if (opts.registerAuthenticatedRoutes) {
10982
+ await opts.registerAuthenticatedRoutes(api);
10983
+ }
10859
10984
  }, { prefix: opts.routePrefix ?? "/api/v1" });
10860
10985
  }
10861
10986
 
10862
- // src/agent-webhook.ts
10863
- import crypto18 from "crypto";
10864
- import { eq as eq18 } from "drizzle-orm";
10865
- var AGENT_WEBHOOK_EVENTS = ["run.completed", "insight.critical", "insight.high", "citation.gained"];
10866
- function buildAgentWebhookUrl(gatewayPort) {
10867
- return `http://localhost:${gatewayPort}/hooks/canonry`;
10868
- }
10869
- function attachAgentWebhookDirect(db, projectId, gatewayPort) {
10870
- const agentUrl = buildAgentWebhookUrl(gatewayPort);
10871
- const existing = db.select().from(notifications).where(eq18(notifications.projectId, projectId)).all();
10872
- const hasAgent = existing.some((n) => {
10873
- const cfg = parseJsonColumn(n.config, {});
10874
- return cfg.source === "agent";
10875
- });
10876
- if (hasAgent) return "already-attached";
10877
- const now = (/* @__PURE__ */ new Date()).toISOString();
10878
- db.insert(notifications).values({
10879
- id: crypto18.randomUUID(),
10880
- projectId,
10881
- channel: "webhook",
10882
- config: JSON.stringify({
10883
- url: agentUrl,
10884
- events: [...AGENT_WEBHOOK_EVENTS],
10885
- source: "agent"
10886
- }),
10887
- enabled: 1,
10888
- webhookSecret: crypto18.randomUUID(),
10889
- createdAt: now,
10890
- updatedAt: now
10891
- }).run();
10892
- return "attached";
10893
- }
10987
+ // src/server.ts
10988
+ import os5 from "os";
10894
10989
 
10895
10990
  // ../provider-gemini/src/normalize.ts
10896
10991
  import { GoogleGenAI } from "@google/genai";
@@ -13343,11 +13438,11 @@ function removeWordpressConnection(config, projectName) {
13343
13438
  }
13344
13439
 
13345
13440
  // src/job-runner.ts
13346
- import crypto19 from "crypto";
13441
+ import crypto18 from "crypto";
13347
13442
  import fs4 from "fs";
13348
13443
  import path5 from "path";
13349
13444
  import os4 from "os";
13350
- 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";
13351
13446
 
13352
13447
  // src/citation-utils.ts
13353
13448
  function domainMatches(domain, canonicalDomain) {
@@ -13583,7 +13678,7 @@ var JobRunner = class {
13583
13678
  if (stale.length === 0) return;
13584
13679
  const now = (/* @__PURE__ */ new Date()).toISOString();
13585
13680
  for (const run of stale) {
13586
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq19(runs.id, run.id)).run();
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();
13587
13682
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
13588
13683
  }
13589
13684
  }
@@ -13611,10 +13706,10 @@ var JobRunner = class {
13611
13706
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
13612
13707
  }
13613
13708
  if (existingRun.status === "queued") {
13614
- 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();
13615
13710
  }
13616
13711
  this.throwIfRunCancelled(runId);
13617
- 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();
13618
13713
  if (!project) {
13619
13714
  throw new Error(`Project ${projectId} not found`);
13620
13715
  }
@@ -13634,8 +13729,8 @@ var JobRunner = class {
13634
13729
  throw new Error("No providers configured. Add at least one provider API key.");
13635
13730
  }
13636
13731
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
13637
- projectKeywords = this.db.select().from(keywords).where(eq19(keywords.projectId, projectId)).all();
13638
- const projectCompetitors = this.db.select().from(competitors).where(eq19(competitors.projectId, projectId)).all();
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();
13639
13734
  const competitorDomains = projectCompetitors.map((c) => c.domain);
13640
13735
  const allDomains = effectiveDomains({
13641
13736
  canonicalDomain: project.canonicalDomain,
@@ -13651,7 +13746,7 @@ var JobRunner = class {
13651
13746
  const todayPeriod = getCurrentUsageDay();
13652
13747
  for (const p of activeProviders) {
13653
13748
  const providerScope = `${projectId}:${p.adapter.name}`;
13654
- const providerUsage = this.db.select().from(usageCounters).where(eq19(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
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);
13655
13750
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
13656
13751
  if (providerUsage + queriesPerProvider > limit) {
13657
13752
  throw new Error(
@@ -13711,7 +13806,7 @@ var JobRunner = class {
13711
13806
  );
13712
13807
  let screenshotRelPath = null;
13713
13808
  if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
13714
- const snapshotId = crypto19.randomUUID();
13809
+ const snapshotId = crypto18.randomUUID();
13715
13810
  const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
13716
13811
  if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
13717
13812
  const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
@@ -13741,7 +13836,7 @@ var JobRunner = class {
13741
13836
  }).run();
13742
13837
  } else {
13743
13838
  this.db.insert(querySnapshots).values({
13744
- id: crypto19.randomUUID(),
13839
+ id: crypto18.randomUUID(),
13745
13840
  runId,
13746
13841
  keywordId: kw.id,
13747
13842
  provider: providerName,
@@ -13792,12 +13887,12 @@ var JobRunner = class {
13792
13887
  const someFailed = providerErrors.size > 0;
13793
13888
  if (allFailed) {
13794
13889
  const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
13795
- 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();
13796
13891
  } else if (someFailed) {
13797
13892
  const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
13798
- 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();
13799
13894
  } else {
13800
- 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();
13801
13896
  }
13802
13897
  this.flushProviderUsage(projectId, providerDispatchCounts);
13803
13898
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -13832,7 +13927,7 @@ var JobRunner = class {
13832
13927
  status: "failed",
13833
13928
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13834
13929
  error: errorMessage
13835
- }).where(eq19(runs.id, runId)).run();
13930
+ }).where(eq18(runs.id, runId)).run();
13836
13931
  this.flushProviderUsage(projectId, providerDispatchCounts);
13837
13932
  trackEvent("run.completed", {
13838
13933
  status: "failed",
@@ -13853,7 +13948,7 @@ var JobRunner = class {
13853
13948
  const now = (/* @__PURE__ */ new Date()).toISOString();
13854
13949
  const period = now.slice(0, 10);
13855
13950
  this.db.insert(usageCounters).values({
13856
- id: crypto19.randomUUID(),
13951
+ id: crypto18.randomUUID(),
13857
13952
  scope,
13858
13953
  period,
13859
13954
  metric,
@@ -13875,7 +13970,7 @@ var JobRunner = class {
13875
13970
  status: runs.status,
13876
13971
  finishedAt: runs.finishedAt,
13877
13972
  error: runs.error
13878
- }).from(runs).where(eq19(runs.id, runId)).get();
13973
+ }).from(runs).where(eq18(runs.id, runId)).get();
13879
13974
  }
13880
13975
  isRunCancelled(runId) {
13881
13976
  return this.getRunState(runId)?.status === "cancelled";
@@ -13891,7 +13986,7 @@ var JobRunner = class {
13891
13986
  this.db.update(runs).set({
13892
13987
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13893
13988
  error: currentRun.error ?? "Cancelled by user"
13894
- }).where(eq19(runs.id, runId)).run();
13989
+ }).where(eq18(runs.id, runId)).run();
13895
13990
  }
13896
13991
  trackEvent("run.completed", {
13897
13992
  status: "cancelled",
@@ -13913,8 +14008,8 @@ function getCurrentUsageDay() {
13913
14008
  }
13914
14009
 
13915
14010
  // src/gsc-sync.ts
13916
- import crypto20 from "crypto";
13917
- 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";
13918
14013
  var log2 = createLogger("GscSync");
13919
14014
  function formatDate2(d) {
13920
14015
  return d.toISOString().split("T")[0];
@@ -13926,13 +14021,13 @@ function daysAgo(n) {
13926
14021
  }
13927
14022
  async function executeGscSync(db, runId, projectId, opts) {
13928
14023
  const now = (/* @__PURE__ */ new Date()).toISOString();
13929
- 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();
13930
14025
  try {
13931
14026
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
13932
14027
  if (!googleClientId || !googleClientSecret) {
13933
14028
  throw new Error("Google OAuth is not configured in the local Canonry config");
13934
14029
  }
13935
- 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();
13936
14031
  if (!project) {
13937
14032
  throw new Error(`Project not found: ${projectId}`);
13938
14033
  }
@@ -13967,7 +14062,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13967
14062
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
13968
14063
  db.delete(gscSearchData).where(
13969
14064
  and8(
13970
- eq20(gscSearchData.projectId, projectId),
14065
+ eq19(gscSearchData.projectId, projectId),
13971
14066
  sql5`${gscSearchData.date} >= ${startDate}`,
13972
14067
  sql5`${gscSearchData.date} <= ${endDate}`
13973
14068
  )
@@ -13979,7 +14074,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13979
14074
  for (const row of batch) {
13980
14075
  const [query, page, country, device, date] = row.keys;
13981
14076
  db.insert(gscSearchData).values({
13982
- id: crypto20.randomUUID(),
14077
+ id: crypto19.randomUUID(),
13983
14078
  projectId,
13984
14079
  syncRunId: runId,
13985
14080
  date: date ?? "",
@@ -14013,7 +14108,7 @@ async function executeGscSync(db, runId, projectId, opts) {
14013
14108
  const rich = ir.richResultsResult;
14014
14109
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
14015
14110
  db.insert(gscUrlInspections).values({
14016
- id: crypto20.randomUUID(),
14111
+ id: crypto19.randomUUID(),
14017
14112
  projectId,
14018
14113
  syncRunId: runId,
14019
14114
  url: pageUrl,
@@ -14034,7 +14129,7 @@ async function executeGscSync(db, runId, projectId, opts) {
14034
14129
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
14035
14130
  }
14036
14131
  }
14037
- 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();
14038
14133
  const latestByUrl = /* @__PURE__ */ new Map();
14039
14134
  for (const row of allInspections) {
14040
14135
  const existing = latestByUrl.get(row.url);
@@ -14055,9 +14150,9 @@ async function executeGscSync(db, runId, projectId, opts) {
14055
14150
  }
14056
14151
  }
14057
14152
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
14058
- 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();
14059
14154
  db.insert(gscCoverageSnapshots).values({
14060
- id: crypto20.randomUUID(),
14155
+ id: crypto19.randomUUID(),
14061
14156
  projectId,
14062
14157
  syncRunId: runId,
14063
14158
  date: snapshotDate,
@@ -14066,19 +14161,19 @@ async function executeGscSync(db, runId, projectId, opts) {
14066
14161
  reasonBreakdown: JSON.stringify(reasonCounts),
14067
14162
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14068
14163
  }).run();
14069
- 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();
14070
14165
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
14071
14166
  } catch (err) {
14072
14167
  const errorMsg = err instanceof Error ? err.message : String(err);
14073
- 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();
14074
14169
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
14075
14170
  throw err;
14076
14171
  }
14077
14172
  }
14078
14173
 
14079
14174
  // src/gsc-inspect-sitemap.ts
14080
- import crypto21 from "crypto";
14081
- 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";
14082
14177
 
14083
14178
  // src/sitemap-parser.ts
14084
14179
  var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
@@ -14147,13 +14242,13 @@ async function parseSitemapRecursive(url, urls, depth) {
14147
14242
  var log3 = createLogger("InspectSitemap");
14148
14243
  async function executeInspectSitemap(db, runId, projectId, opts) {
14149
14244
  const now = (/* @__PURE__ */ new Date()).toISOString();
14150
- 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();
14151
14246
  try {
14152
14247
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
14153
14248
  if (!googleClientId || !googleClientSecret) {
14154
14249
  throw new Error("Google OAuth is not configured in the local Canonry config");
14155
14250
  }
14156
- 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();
14157
14252
  if (!project) {
14158
14253
  throw new Error(`Project not found: ${projectId}`);
14159
14254
  }
@@ -14194,7 +14289,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14194
14289
  const rich = ir.richResultsResult;
14195
14290
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
14196
14291
  db.insert(gscUrlInspections).values({
14197
- id: crypto21.randomUUID(),
14292
+ id: crypto20.randomUUID(),
14198
14293
  projectId,
14199
14294
  syncRunId: runId,
14200
14295
  url: pageUrl,
@@ -14221,7 +14316,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14221
14316
  await new Promise((r) => setTimeout(r, 1e3));
14222
14317
  }
14223
14318
  }
14224
- 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();
14225
14320
  const latestByUrl = /* @__PURE__ */ new Map();
14226
14321
  for (const row of allInspections) {
14227
14322
  const existing = latestByUrl.get(row.url);
@@ -14242,9 +14337,9 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14242
14337
  }
14243
14338
  }
14244
14339
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
14245
- 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();
14246
14341
  db.insert(gscCoverageSnapshots).values({
14247
- id: crypto21.randomUUID(),
14342
+ id: crypto20.randomUUID(),
14248
14343
  projectId,
14249
14344
  syncRunId: runId,
14250
14345
  date: snapshotDate,
@@ -14254,11 +14349,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14254
14349
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14255
14350
  }).run();
14256
14351
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
14257
- 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();
14258
14353
  log3.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
14259
14354
  } catch (err) {
14260
14355
  const errorMsg = err instanceof Error ? err.message : String(err);
14261
- 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();
14262
14357
  log3.error("inspect.failed", { runId, projectId, error: errorMsg });
14263
14358
  throw err;
14264
14359
  }
@@ -14317,7 +14412,7 @@ var ProviderRegistry = class {
14317
14412
 
14318
14413
  // src/scheduler.ts
14319
14414
  import cron from "node-cron";
14320
- import { eq as eq22 } from "drizzle-orm";
14415
+ import { eq as eq21 } from "drizzle-orm";
14321
14416
  var log4 = createLogger("Scheduler");
14322
14417
  var Scheduler = class {
14323
14418
  db;
@@ -14329,7 +14424,7 @@ var Scheduler = class {
14329
14424
  }
14330
14425
  /** Load all enabled schedules from DB and register cron jobs. */
14331
14426
  start() {
14332
- 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();
14333
14428
  for (const schedule of allSchedules) {
14334
14429
  const missedRunAt = schedule.nextRunAt;
14335
14430
  this.registerCronTask(schedule);
@@ -14354,7 +14449,7 @@ var Scheduler = class {
14354
14449
  this.stopTask(projectId, existing, "Stopped");
14355
14450
  this.tasks.delete(projectId);
14356
14451
  }
14357
- 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();
14358
14453
  if (schedule && schedule.enabled === 1) {
14359
14454
  this.registerCronTask(schedule);
14360
14455
  }
@@ -14387,14 +14482,14 @@ var Scheduler = class {
14387
14482
  this.db.update(schedules).set({
14388
14483
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
14389
14484
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14390
- }).where(eq22(schedules.id, scheduleId)).run();
14485
+ }).where(eq21(schedules.id, scheduleId)).run();
14391
14486
  const label = schedule.preset ?? cronExpr;
14392
14487
  log4.info("cron.registered", { projectId, schedule: label, timezone });
14393
14488
  }
14394
14489
  triggerRun(scheduleId, projectId) {
14395
14490
  try {
14396
14491
  const now = (/* @__PURE__ */ new Date()).toISOString();
14397
- 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();
14398
14493
  if (!currentSchedule || currentSchedule.enabled !== 1) {
14399
14494
  log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
14400
14495
  this.remove(projectId);
@@ -14402,7 +14497,7 @@ var Scheduler = class {
14402
14497
  }
14403
14498
  const task = this.tasks.get(projectId);
14404
14499
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
14405
- 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();
14406
14501
  if (!project) {
14407
14502
  log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
14408
14503
  this.remove(projectId);
@@ -14431,7 +14526,7 @@ var Scheduler = class {
14431
14526
  this.db.update(schedules).set({
14432
14527
  nextRunAt,
14433
14528
  updatedAt: now
14434
- }).where(eq22(schedules.id, currentSchedule.id)).run();
14529
+ }).where(eq21(schedules.id, currentSchedule.id)).run();
14435
14530
  return;
14436
14531
  }
14437
14532
  const runId = queueResult.runId;
@@ -14439,7 +14534,7 @@ var Scheduler = class {
14439
14534
  lastRunAt: now,
14440
14535
  nextRunAt,
14441
14536
  updatedAt: now
14442
- }).where(eq22(schedules.id, currentSchedule.id)).run();
14537
+ }).where(eq21(schedules.id, currentSchedule.id)).run();
14443
14538
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
14444
14539
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
14445
14540
  log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -14451,8 +14546,8 @@ var Scheduler = class {
14451
14546
  };
14452
14547
 
14453
14548
  // src/notifier.ts
14454
- import { eq as eq23, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
14455
- 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";
14456
14551
  var log5 = createLogger("Notifier");
14457
14552
  var Notifier = class {
14458
14553
  db;
@@ -14464,18 +14559,18 @@ var Notifier = class {
14464
14559
  /** Called after a run completes (success, partial, or failed). */
14465
14560
  async onRunCompleted(runId, projectId) {
14466
14561
  log5.info("run.completed", { runId, projectId });
14467
- 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);
14468
14563
  if (notifs.length === 0) {
14469
14564
  log5.info("notifications.none-enabled", { projectId });
14470
14565
  return;
14471
14566
  }
14472
14567
  log5.info("notifications.found", { projectId, count: notifs.length });
14473
- 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();
14474
14569
  if (!run) {
14475
14570
  log5.error("run.not-found", { runId, msg: "skipping notification dispatch" });
14476
14571
  return;
14477
14572
  }
14478
- 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();
14479
14574
  if (!project) {
14480
14575
  log5.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
14481
14576
  return;
@@ -14522,11 +14617,11 @@ var Notifier = class {
14522
14617
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
14523
14618
  if (highInsights.length > 0) insightEvents.push("insight.high");
14524
14619
  if (insightEvents.length === 0) return;
14525
- 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);
14526
14621
  if (notifs.length === 0) return;
14527
- 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();
14528
14623
  if (!run) return;
14529
- 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();
14530
14625
  if (!project) return;
14531
14626
  for (const notif of notifs) {
14532
14627
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -14558,8 +14653,8 @@ var Notifier = class {
14558
14653
  computeTransitions(runId, projectId) {
14559
14654
  const recentRuns = this.db.select().from(runs).where(
14560
14655
  and10(
14561
- eq23(runs.projectId, projectId),
14562
- or2(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
14656
+ eq22(runs.projectId, projectId),
14657
+ or2(eq22(runs.status, "completed"), eq22(runs.status, "partial"))
14563
14658
  )
14564
14659
  ).orderBy(desc8(runs.createdAt)).limit(2).all();
14565
14660
  if (recentRuns.length < 2) return [];
@@ -14571,12 +14666,12 @@ var Notifier = class {
14571
14666
  keyword: keywords.keyword,
14572
14667
  provider: querySnapshots.provider,
14573
14668
  citationState: querySnapshots.citationState
14574
- }).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();
14575
14670
  const previousSnapshots = this.db.select({
14576
14671
  keywordId: querySnapshots.keywordId,
14577
14672
  provider: querySnapshots.provider,
14578
14673
  citationState: querySnapshots.citationState
14579
- }).from(querySnapshots).where(eq23(querySnapshots.runId, previousRunId)).all();
14674
+ }).from(querySnapshots).where(eq22(querySnapshots.runId, previousRunId)).all();
14580
14675
  const prevMap = /* @__PURE__ */ new Map();
14581
14676
  for (const s of previousSnapshots) {
14582
14677
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -14634,7 +14729,7 @@ var Notifier = class {
14634
14729
  }
14635
14730
  logDelivery(projectId, notificationId, event, status, error) {
14636
14731
  this.db.insert(auditLog).values({
14637
- id: crypto22.randomUUID(),
14732
+ id: crypto21.randomUUID(),
14638
14733
  projectId,
14639
14734
  actor: "scheduler",
14640
14735
  action: `notification.${status}`,
@@ -14649,19 +14744,23 @@ var Notifier = class {
14649
14744
  // src/run-coordinator.ts
14650
14745
  var log6 = createLogger("RunCoordinator");
14651
14746
  var RunCoordinator = class {
14652
- constructor(notifier, intelligenceService, onInsightsGenerated) {
14747
+ constructor(notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
14653
14748
  this.notifier = notifier;
14654
14749
  this.intelligenceService = intelligenceService;
14655
14750
  this.onInsightsGenerated = onInsightsGenerated;
14751
+ this.onAeroEvent = onAeroEvent;
14656
14752
  }
14657
14753
  async onRunCompleted(runId, projectId) {
14754
+ let insightCount = 0;
14755
+ let criticalOrHigh = 0;
14658
14756
  try {
14659
14757
  const result = this.intelligenceService.analyzeAndPersist(runId, projectId);
14660
- if (result && this.onInsightsGenerated) {
14661
- const hasHighSeverity = result.insights.some(
14758
+ if (result) {
14759
+ insightCount = result.insights.length;
14760
+ criticalOrHigh = result.insights.filter(
14662
14761
  (i) => i.severity === "critical" || i.severity === "high"
14663
- );
14664
- if (hasHighSeverity) {
14762
+ ).length;
14763
+ if (this.onInsightsGenerated && criticalOrHigh > 0) {
14665
14764
  try {
14666
14765
  await this.onInsightsGenerated(runId, projectId, result);
14667
14766
  } catch (err) {
@@ -14677,148 +14776,1692 @@ var RunCoordinator = class {
14677
14776
  } catch (err) {
14678
14777
  log6.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
14679
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
+ }
14680
14786
  }
14681
14787
  };
14682
14788
 
14683
- // src/snapshot-service.ts
14684
- 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";
14685
14792
 
14686
- // src/site-fetch.ts
14687
- import https2 from "https";
14688
- var FETCH_TIMEOUT_MS = 1e4;
14689
- var MAX_TEXT_LENGTH = 4e3;
14690
- var MAX_BODY_BYTES = 512e3;
14691
- var USER_AGENT = "Canonry/1.0 (site-analysis)";
14692
- function extractHostname(domain) {
14693
- let hostname = domain;
14694
- try {
14695
- if (hostname.includes("://")) {
14696
- hostname = new URL(hostname).hostname;
14697
- }
14698
- } 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
14699
14825
  }
14700
- 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);
14701
14829
  }
14702
- function fetchWithPinnedAddress(target) {
14703
- return new Promise((resolve) => {
14704
- const port = target.url.port ? Number(target.url.port) : 443;
14705
- const path9 = target.url.pathname + target.url.search;
14706
- const req = https2.request(
14707
- {
14708
- hostname: target.address,
14709
- family: target.family,
14710
- port,
14711
- path: path9,
14712
- method: "GET",
14713
- timeout: FETCH_TIMEOUT_MS,
14714
- servername: target.url.hostname,
14715
- // SNI for TLS
14716
- headers: {
14717
- Host: target.url.host,
14718
- "User-Agent": USER_AGENT,
14719
- Accept: "text/html"
14720
- }
14721
- },
14722
- (res) => {
14723
- if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
14724
- if (res.statusCode >= 300 && res.statusCode < 400) {
14725
- const location = res.headers.location ?? "";
14726
- res.resume();
14727
- resolve(`REDIRECT:${location}`);
14728
- return;
14729
- }
14730
- res.resume();
14731
- resolve("");
14732
- return;
14733
- }
14734
- const contentType = res.headers["content-type"] ?? "";
14735
- if (!contentType.includes("text/html")) {
14736
- res.resume();
14737
- resolve("");
14738
- return;
14739
- }
14740
- const chunks = [];
14741
- let totalBytes = 0;
14742
- res.on("data", (chunk) => {
14743
- totalBytes += chunk.length;
14744
- if (totalBytes <= MAX_BODY_BYTES) {
14745
- chunks.push(chunk);
14746
- }
14747
- });
14748
- res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
14749
- res.on("error", () => resolve(""));
14750
- }
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.`
14751
14847
  );
14752
- req.on("timeout", () => req.destroy(new Error("timeout")));
14753
- req.on("error", () => resolve(""));
14754
- req.end();
14755
- });
14848
+ }
14849
+ return model;
14756
14850
  }
14757
- async function fetchSiteText(domain) {
14758
- const hostname = extractHostname(domain);
14759
- const url = `https://${hostname}`;
14760
- const targetCheck = await resolveWebhookTarget(url);
14761
- if (!targetCheck.ok) return "";
14762
- try {
14763
- const result = await fetchWithPinnedAddress(targetCheck.target);
14764
- if (result.startsWith("REDIRECT:")) {
14765
- const location = result.slice("REDIRECT:".length);
14766
- if (!location) return "";
14767
- const redirectUrl = new URL(location, url).href;
14768
- const redirectCheck = await resolveWebhookTarget(redirectUrl);
14769
- if (!redirectCheck.ok) return "";
14770
- const redirectResult = await fetchWithPinnedAddress(redirectCheck.target);
14771
- if (redirectResult.startsWith("REDIRECT:")) return "";
14772
- return stripHtml2(redirectResult);
14773
- }
14774
- return stripHtml2(result);
14775
- } catch {
14776
- return "";
14851
+ function validateAgentProviderRegistry() {
14852
+ for (const provider of listAgentProviders()) {
14853
+ resolveModelForProvider(provider);
14777
14854
  }
14778
14855
  }
14779
- function stripHtml2(html) {
14780
- if (!html) return "";
14781
- let text = html.replace(/<script[\s\S]*?<\/script>/gi, " ");
14782
- text = text.replace(/<style[\s\S]*?<\/style>/gi, " ");
14783
- text = text.replace(/<[^>]+>/g, " ");
14784
- text = text.replace(/&amp;/g, "&");
14785
- text = text.replace(/&lt;/g, "<");
14786
- text = text.replace(/&gt;/g, ">");
14787
- text = text.replace(/&quot;/g, '"');
14788
- text = text.replace(/&#39;/g, "'");
14789
- text = text.replace(/&nbsp;/g, " ");
14790
- text = text.replace(/\s+/g, " ").trim();
14791
- if (text.length > MAX_TEXT_LENGTH) {
14792
- 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;
14793
14873
  }
14794
- 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
+ };
14795
14893
  }
14796
14894
 
14797
- // src/snapshot-format.ts
14798
- function formatAuditFactorScore(factor) {
14799
- 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 ")}`);
14800
14911
  }
14801
14912
 
14802
- // src/snapshot-service.ts
14803
- var log7 = createLogger("Snapshot");
14804
- var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
14805
- var SNAPSHOT_QUERY_COUNT = 6;
14806
- var ProviderExecutionGate2 = class {
14807
- constructor(maxConcurrency, maxPerMinute) {
14808
- this.maxConcurrency = maxConcurrency;
14809
- 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
+ });
14810
14948
  }
14811
- window = [];
14812
- waiters = [];
14813
- rateLimitChain = Promise.resolve();
14814
- inFlight = 0;
14815
- async run(task) {
14816
- await this.acquire();
14817
- try {
14818
- await this.waitForRateLimit();
14819
- return await task();
14820
- } finally {
14821
- this.release();
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 {
16464
+ this.release();
14822
16465
  }
14823
16466
  }
14824
16467
  async acquire() {
@@ -14943,7 +16586,7 @@ var SnapshotService = class {
14943
16586
  return mapAuditReport(report);
14944
16587
  } catch (err) {
14945
16588
  const message = err instanceof Error ? err.message : String(err);
14946
- log7.warn("audit.failed", { homepageUrl, error: message });
16589
+ log8.warn("audit.failed", { homepageUrl, error: message });
14947
16590
  return {
14948
16591
  url: homepageUrl,
14949
16592
  finalUrl: homepageUrl,
@@ -14973,7 +16616,7 @@ var SnapshotService = class {
14973
16616
  phrases: parsedPhrases
14974
16617
  };
14975
16618
  } catch (err) {
14976
- log7.warn("profile.generation-failed", {
16619
+ log8.warn("profile.generation-failed", {
14977
16620
  domain: ctx.domain,
14978
16621
  provider: ctx.analysisProvider.adapter.name,
14979
16622
  error: err instanceof Error ? err.message : String(err)
@@ -15115,7 +16758,7 @@ var SnapshotService = class {
15115
16758
  recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
15116
16759
  };
15117
16760
  } catch (err) {
15118
- log7.warn("response.analysis-failed", {
16761
+ log8.warn("response.analysis-failed", {
15119
16762
  provider: ctx.analysisProvider.adapter.name,
15120
16763
  error: err instanceof Error ? err.message : String(err)
15121
16764
  });
@@ -15397,499 +17040,6 @@ function clipText(value, length) {
15397
17040
  return `${value.slice(0, length - 3)}...`;
15398
17041
  }
15399
17042
 
15400
- // src/agent-manager.ts
15401
- import { execFileSync, spawn } from "child_process";
15402
- import fs5 from "fs";
15403
- import path6 from "path";
15404
- var log8 = createLogger("AgentManager");
15405
- var PROCESS_MARKER = "canonry-openclaw-gateway";
15406
- var AgentManager = class {
15407
- constructor(config, stateDir) {
15408
- this.config = config;
15409
- this.stateDir = stateDir;
15410
- this.processJsonPath = path6.join(stateDir, "process.json");
15411
- }
15412
- processJsonPath;
15413
- /**
15414
- * Check if the gateway process is running.
15415
- * Cleans up stale process.json if the process is dead or belongs to a
15416
- * different process (PID reuse).
15417
- */
15418
- status() {
15419
- const info = this.readProcessInfo();
15420
- if (!info) {
15421
- return { state: "stopped" };
15422
- }
15423
- if (info.marker !== PROCESS_MARKER) {
15424
- this.removeProcessJson();
15425
- return { state: "stopped" };
15426
- }
15427
- if (isProcessAlive(info.pid) && this.verifyProcessIdentity(info.pid)) {
15428
- return {
15429
- state: "running",
15430
- pid: info.pid,
15431
- port: info.gatewayPort,
15432
- startedAt: info.startedAt
15433
- };
15434
- }
15435
- this.removeProcessJson();
15436
- return { state: "stopped" };
15437
- }
15438
- /**
15439
- * Start the OpenClaw gateway as a detached background process.
15440
- * Idempotent — no-op if already running.
15441
- * Waits briefly for the process to confirm it hasn't crashed on startup.
15442
- */
15443
- async start() {
15444
- const currentStatus = this.status();
15445
- if (currentStatus.state === "running") {
15446
- log8.info("already.running", { pid: currentStatus.pid });
15447
- return;
15448
- }
15449
- const binary = this.config.binary ?? "openclaw";
15450
- const profile = this.config.profile ?? "aero";
15451
- const port = this.config.gatewayPort ?? 3579;
15452
- if (!fs5.existsSync(this.stateDir)) {
15453
- fs5.mkdirSync(this.stateDir, { recursive: true });
15454
- }
15455
- const logFile = path6.join(this.stateDir, "gateway.log");
15456
- const logFd = fs5.openSync(logFile, "a");
15457
- const dotEnv = this.loadDotEnv();
15458
- const child = spawn(binary, ["--profile", profile, "gateway"], {
15459
- detached: true,
15460
- stdio: ["ignore", logFd, logFd],
15461
- env: {
15462
- ...process.env,
15463
- ...dotEnv,
15464
- OPENCLAW_PROFILE: profile,
15465
- OPENCLAW_GATEWAY_PORT: String(port),
15466
- OPENCLAW_STATE_DIR: this.stateDir
15467
- }
15468
- });
15469
- const startupResult = await new Promise((resolve) => {
15470
- let settled = false;
15471
- const settle = (r) => {
15472
- if (settled) return;
15473
- settled = true;
15474
- resolve(r);
15475
- };
15476
- child.on("error", (err) => settle({ error: err }));
15477
- child.on("exit", (code) => settle({ exitCode: code }));
15478
- setTimeout(() => settle({}), 500);
15479
- });
15480
- child.unref();
15481
- fs5.closeSync(logFd);
15482
- if (startupResult.error) {
15483
- throw new Error(`Failed to start OpenClaw gateway: ${startupResult.error.message}`);
15484
- }
15485
- if (startupResult.exitCode != null) {
15486
- throw new Error(`OpenClaw gateway exited immediately (code ${startupResult.exitCode}). Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
15487
- }
15488
- if (child.pid == null) {
15489
- throw new Error("Failed to start OpenClaw gateway: no PID returned by spawn");
15490
- }
15491
- if (!isProcessAlive(child.pid)) {
15492
- throw new Error(`OpenClaw gateway exited immediately after spawn. Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
15493
- }
15494
- const processInfo = {
15495
- pid: child.pid,
15496
- gatewayPort: port,
15497
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
15498
- marker: PROCESS_MARKER
15499
- };
15500
- fs5.writeFileSync(this.processJsonPath, JSON.stringify(processInfo, null, 2), "utf-8");
15501
- log8.info("started", { pid: child.pid, port });
15502
- }
15503
- /**
15504
- * Stop the gateway process.
15505
- * Uses DenchClaw escalation: SIGTERM → 800ms poll → SIGKILL.
15506
- * Idempotent — no-op if already stopped.
15507
- */
15508
- async stop() {
15509
- const info = this.readProcessInfo();
15510
- if (!info) return;
15511
- if (isProcessAlive(info.pid) && info.marker === PROCESS_MARKER && this.verifyProcessIdentity(info.pid)) {
15512
- await terminateWithEscalation(info.pid);
15513
- }
15514
- this.removeProcessJson();
15515
- log8.info("stopped", { pid: info.pid });
15516
- }
15517
- /**
15518
- * Stop the gateway, wipe the workspace directory, and prepare for re-seeding.
15519
- */
15520
- async reset() {
15521
- await this.stop();
15522
- const workspaceDir = path6.join(this.stateDir, "workspace");
15523
- if (fs5.existsSync(workspaceDir)) {
15524
- fs5.rmSync(workspaceDir, { recursive: true, force: true });
15525
- log8.info("workspace.wiped", { dir: workspaceDir });
15526
- }
15527
- }
15528
- /**
15529
- * Verify that the PID actually belongs to an openclaw process by checking
15530
- * the full command line. Requires "openclaw" in the args to avoid matching
15531
- * unrelated Node processes after PID reuse.
15532
- */
15533
- verifyProcessIdentity(pid) {
15534
- try {
15535
- if (process.platform === "darwin") {
15536
- const out = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
15537
- encoding: "utf-8",
15538
- timeout: 2e3
15539
- }).trim();
15540
- return out.includes("openclaw");
15541
- }
15542
- if (process.platform === "linux") {
15543
- const cmdline = fs5.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
15544
- return cmdline.includes("openclaw");
15545
- }
15546
- return true;
15547
- } catch {
15548
- return false;
15549
- }
15550
- }
15551
- readProcessInfo() {
15552
- if (!fs5.existsSync(this.processJsonPath)) return null;
15553
- try {
15554
- return JSON.parse(fs5.readFileSync(this.processJsonPath, "utf-8"));
15555
- } catch {
15556
- return null;
15557
- }
15558
- }
15559
- removeProcessJson() {
15560
- try {
15561
- fs5.unlinkSync(this.processJsonPath);
15562
- } catch {
15563
- }
15564
- }
15565
- /** Parse a simple KEY=value dotenv file from the state dir. */
15566
- loadDotEnv() {
15567
- const envFile = path6.join(this.stateDir, ".env");
15568
- if (!fs5.existsSync(envFile)) return {};
15569
- const result = {};
15570
- for (const line of fs5.readFileSync(envFile, "utf-8").split("\n")) {
15571
- const trimmed = line.trim();
15572
- if (!trimmed || trimmed.startsWith("#")) continue;
15573
- const eq25 = trimmed.indexOf("=");
15574
- if (eq25 < 1) continue;
15575
- result[trimmed.slice(0, eq25)] = trimmed.slice(eq25 + 1);
15576
- }
15577
- return result;
15578
- }
15579
- };
15580
- function isProcessAlive(pid) {
15581
- try {
15582
- process.kill(pid, 0);
15583
- return true;
15584
- } catch (err) {
15585
- if (err.code === "EPERM") return true;
15586
- return false;
15587
- }
15588
- }
15589
- async function terminateWithEscalation(pid) {
15590
- try {
15591
- process.kill(pid, "SIGTERM");
15592
- } catch {
15593
- return;
15594
- }
15595
- const deadline = Date.now() + 800;
15596
- while (Date.now() < deadline) {
15597
- if (!isProcessAlive(pid)) return;
15598
- await new Promise((resolve) => setTimeout(resolve, 100));
15599
- }
15600
- try {
15601
- process.kill(pid, "SIGKILL");
15602
- } catch {
15603
- }
15604
- }
15605
-
15606
- // src/agent-bootstrap.ts
15607
- import { execFileSync as execFileSync2, execSync } from "child_process";
15608
- import fs6 from "fs";
15609
- import os5 from "os";
15610
- import path7 from "path";
15611
- import { fileURLToPath } from "url";
15612
- var CACHE_TTL_MS = 5 * 60 * 1e3;
15613
- var OPENCLAW_VERSION = "2026.4.14";
15614
- var OPENCLAW_PACKAGE_SPEC = `openclaw@${OPENCLAW_VERSION}`;
15615
- var MIN_NODE_VERSION = "22.14.0";
15616
- var cachedResult = null;
15617
- var cachedAt = 0;
15618
- function getAeroStateDir(profile = "aero") {
15619
- return path7.join(os5.homedir(), `.openclaw-${profile}`);
15620
- }
15621
- async function detectOpenClaw(config) {
15622
- if (cachedResult && Date.now() - cachedAt < CACHE_TTL_MS) {
15623
- return cachedResult;
15624
- }
15625
- let result;
15626
- if (config?.binary) {
15627
- const version = probeVersion(config.binary);
15628
- if (version) {
15629
- result = { found: true, path: config.binary, version };
15630
- cachedResult = result;
15631
- cachedAt = Date.now();
15632
- return result;
15633
- }
15634
- }
15635
- const binaryPath = findInPath();
15636
- if (binaryPath) {
15637
- const version = probeVersion(binaryPath);
15638
- if (version) {
15639
- result = { found: true, path: binaryPath, version };
15640
- cachedResult = result;
15641
- cachedAt = Date.now();
15642
- return result;
15643
- }
15644
- }
15645
- result = { found: false };
15646
- cachedResult = result;
15647
- cachedAt = Date.now();
15648
- return result;
15649
- }
15650
- detectOpenClaw.resetCache = () => {
15651
- cachedResult = null;
15652
- cachedAt = 0;
15653
- };
15654
- function probeVersion(binaryPath) {
15655
- try {
15656
- const output = execFileSync2(binaryPath, ["--version"], {
15657
- timeout: 5e3,
15658
- encoding: "utf-8"
15659
- });
15660
- const match = output.toString().trim().match(/(\d+\.\d+\.\d+)/);
15661
- return match ? match[1] : output.toString().trim();
15662
- } catch {
15663
- return null;
15664
- }
15665
- }
15666
- function findInPath() {
15667
- const cmd = process.platform === "win32" ? "where" : "which";
15668
- try {
15669
- const output = execFileSync2(cmd, ["openclaw"], {
15670
- timeout: 5e3,
15671
- encoding: "utf-8"
15672
- });
15673
- return output.toString().trim().split("\n")[0] || null;
15674
- } catch {
15675
- return null;
15676
- }
15677
- }
15678
- async function installOpenClaw(opts) {
15679
- const unsupportedNodeError = getUnsupportedNodeError(opts?.nodeVersion);
15680
- if (unsupportedNodeError) {
15681
- return {
15682
- success: false,
15683
- error: unsupportedNodeError
15684
- };
15685
- }
15686
- try {
15687
- execSync(`npm install -g ${OPENCLAW_PACKAGE_SPEC}`, {
15688
- timeout: 12e4,
15689
- stdio: opts?.silent ? "pipe" : "inherit"
15690
- });
15691
- } catch (err) {
15692
- return {
15693
- success: false,
15694
- error: err instanceof Error ? err.message : String(err)
15695
- };
15696
- }
15697
- detectOpenClaw.resetCache();
15698
- const detection = await detectOpenClaw();
15699
- if (!detection.found) {
15700
- return {
15701
- success: false,
15702
- error: `npm install succeeded but the ${OPENCLAW_PACKAGE_SPEC} binary was not found in PATH`
15703
- };
15704
- }
15705
- if (detection.version) {
15706
- const expectedVersion = parseVersionTuple(OPENCLAW_VERSION);
15707
- const detectedVersion = parseVersionTuple(detection.version);
15708
- if (expectedVersion && detectedVersion && compareVersionTuples(detectedVersion, expectedVersion) !== 0) {
15709
- return {
15710
- success: false,
15711
- error: `Installed OpenClaw binary reports version ${detection.version}, but Canonry pinned ${OPENCLAW_VERSION}. A different openclaw binary may be shadowing the npm-installed package in PATH.`
15712
- };
15713
- }
15714
- }
15715
- return { success: true, detection };
15716
- }
15717
- function getUnsupportedNodeError(currentNodeVersionOverride) {
15718
- const currentNodeVersion = normalizeVersion(currentNodeVersionOverride ?? process.versions.node);
15719
- const minimumTuple = parseVersionTuple(MIN_NODE_VERSION);
15720
- const currentTuple = parseVersionTuple(currentNodeVersion);
15721
- if (!minimumTuple || !currentTuple || compareVersionTuples(currentTuple, minimumTuple) >= 0) {
15722
- return null;
15723
- }
15724
- return `Canonry requires Node.js >=${MIN_NODE_VERSION} and installs pinned OpenClaw ${OPENCLAW_VERSION}, but the current runtime is ${currentNodeVersion}. Upgrade Node.js before running "canonry agent setup".`;
15725
- }
15726
- function normalizeVersion(version) {
15727
- const tuple = parseVersionTuple(version);
15728
- if (!tuple) {
15729
- return version.trim().replace(/^v/i, "");
15730
- }
15731
- return tuple.join(".");
15732
- }
15733
- function parseVersionTuple(version) {
15734
- const match = version.trim().replace(/^v/i, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
15735
- if (!match) {
15736
- return null;
15737
- }
15738
- return [
15739
- Number(match[1]),
15740
- Number(match[2] ?? 0),
15741
- Number(match[3] ?? 0)
15742
- ];
15743
- }
15744
- function compareVersionTuples(left, right) {
15745
- for (let index = 0; index < left.length; index++) {
15746
- const delta = left[index] - right[index];
15747
- if (delta !== 0) {
15748
- return delta;
15749
- }
15750
- }
15751
- return 0;
15752
- }
15753
- function seedWorkspace(stateDir) {
15754
- const workspaceDir = path7.join(stateDir, "workspace");
15755
- fs6.mkdirSync(workspaceDir, { recursive: true });
15756
- const __dirname = path7.dirname(fileURLToPath(import.meta.url));
15757
- const assetsDir = path7.join(__dirname, "..", "assets", "agent-workspace");
15758
- if (!fs6.existsSync(assetsDir)) {
15759
- return;
15760
- }
15761
- copyDirRecursive(assetsDir, workspaceDir);
15762
- }
15763
- function initializeOpenClawProfile(binary, profile, workspaceDir) {
15764
- try {
15765
- execFileSync2(binary, [
15766
- "--profile",
15767
- profile,
15768
- "onboard",
15769
- "--non-interactive",
15770
- "--accept-risk",
15771
- "--mode",
15772
- "local",
15773
- "--workspace",
15774
- workspaceDir,
15775
- "--skip-channels",
15776
- "--skip-skills",
15777
- "--skip-health",
15778
- "--no-install-daemon"
15779
- ], { timeout: 3e4, stdio: "pipe" });
15780
- } catch (err) {
15781
- const stderr = err instanceof Error && "stderr" in err ? String(err.stderr) : "";
15782
- if (stderr.toLowerCase().includes("already")) return;
15783
- throw new CliError({
15784
- code: "AGENT_PROFILE_INIT_FAILED",
15785
- message: `Failed to initialize OpenClaw profile: ${stderr || (err instanceof Error ? err.message : String(err))}`,
15786
- displayMessage: `Failed to initialize OpenClaw profile "${profile}".`
15787
- });
15788
- }
15789
- }
15790
- function configureOpenClawGateway(binary, profile, gatewayPort) {
15791
- const entries = [
15792
- ["gateway.mode", "local", false],
15793
- ["gateway.port", String(gatewayPort), true]
15794
- ];
15795
- for (const [key, value, strict] of entries) {
15796
- try {
15797
- const args = ["--profile", profile, "config", "set", key, value];
15798
- if (strict) args.push("--strict-json");
15799
- execFileSync2(binary, args, { timeout: 1e4, stdio: "pipe" });
15800
- } catch (err) {
15801
- throw new CliError({
15802
- code: "AGENT_GATEWAY_CONFIG_FAILED",
15803
- message: `Failed to set ${key}=${value}: ${err instanceof Error ? err.message : String(err)}`,
15804
- displayMessage: `Failed to configure OpenClaw gateway (${key}).`
15805
- });
15806
- }
15807
- }
15808
- }
15809
- function setOpenClawModel(binary, profile, model) {
15810
- try {
15811
- execFileSync2(binary, [
15812
- "--profile",
15813
- profile,
15814
- "models",
15815
- "set",
15816
- model
15817
- ], { timeout: 1e4, stdio: "pipe" });
15818
- } catch (err) {
15819
- throw new CliError({
15820
- code: "AGENT_MODEL_SET_FAILED",
15821
- message: `Failed to set agent model to ${model}: ${err instanceof Error ? err.message : String(err)}`,
15822
- displayMessage: `Failed to set agent model to "${model}".`
15823
- });
15824
- }
15825
- }
15826
- function providerEnvVar(provider) {
15827
- const map = {
15828
- anthropic: "ANTHROPIC_API_KEY",
15829
- openai: "OPENAI_API_KEY",
15830
- google: "GOOGLE_API_KEY",
15831
- "google-vertex": "GOOGLE_API_KEY",
15832
- groq: "GROQ_API_KEY",
15833
- mistral: "MISTRAL_API_KEY",
15834
- xai: "XAI_API_KEY",
15835
- openrouter: "OPENROUTER_API_KEY",
15836
- cerebras: "CEREBRAS_API_KEY"
15837
- };
15838
- return map[provider] ?? `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
15839
- }
15840
- function writeAgentEnv(stateDir, key, value) {
15841
- const envFile = path7.join(stateDir, ".env");
15842
- let lines = [];
15843
- if (fs6.existsSync(envFile)) {
15844
- lines = fs6.readFileSync(envFile, "utf-8").split("\n");
15845
- }
15846
- const prefix = `${key}=`;
15847
- const idx = lines.findIndex((l) => l.startsWith(prefix));
15848
- const entry = `${key}=${value}`;
15849
- if (idx >= 0) {
15850
- lines[idx] = entry;
15851
- } else {
15852
- lines.push(entry);
15853
- }
15854
- while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
15855
- fs6.writeFileSync(envFile, lines.join("\n") + "\n", "utf-8");
15856
- }
15857
- function resolveAgentCredentials(opts) {
15858
- const provider = opts.agentProvider ?? "anthropic";
15859
- if (opts.agentKey) {
15860
- return { provider, key: opts.agentKey, model: opts.agentModel };
15861
- }
15862
- const envVar = providerEnvVar(provider);
15863
- const envKey = process.env[envVar];
15864
- if (envKey) {
15865
- return { provider, key: envKey, model: opts.agentModel };
15866
- }
15867
- const genericKey = process.env.CANONRY_AGENT_KEY;
15868
- if (genericKey) {
15869
- return { provider, key: genericKey, model: opts.agentModel };
15870
- }
15871
- const envFile = path7.join(opts.stateDir, ".env");
15872
- if (fs6.existsSync(envFile)) {
15873
- const hasKey = fs6.readFileSync(envFile, "utf-8").split("\n").some((l) => l.includes("_API_KEY="));
15874
- if (hasKey) {
15875
- return { provider, key: void 0, model: opts.agentModel };
15876
- }
15877
- }
15878
- return { provider, key: void 0, model: opts.agentModel };
15879
- }
15880
- function copyDirRecursive(src, dest) {
15881
- fs6.mkdirSync(dest, { recursive: true });
15882
- for (const entry of fs6.readdirSync(src, { withFileTypes: true })) {
15883
- const srcPath = path7.join(src, entry.name);
15884
- const destPath = path7.join(dest, entry.name);
15885
- if (entry.isDirectory()) {
15886
- copyDirRecursive(srcPath, destPath);
15887
- } else {
15888
- fs6.copyFileSync(srcPath, destPath);
15889
- }
15890
- }
15891
- }
15892
-
15893
17043
  // src/server.ts
15894
17044
  var _require2 = createRequire2(import.meta.url);
15895
17045
  var { version: PKG_VERSION } = _require2("../package.json");
@@ -15959,68 +17109,46 @@ function serializeSessionCookie(opts) {
15959
17109
  }
15960
17110
  return parts.join("; ");
15961
17111
  }
15962
- function migrateDbCredentialsToConfig(db, config) {
15963
- try {
15964
- const googleColCheck = db.all(sql6.raw(
15965
- `SELECT COUNT(*) as c FROM pragma_table_info('google_connections') WHERE name = 'access_token'`
15966
- ));
15967
- if (googleColCheck[0]?.c) {
15968
- const rows = db.all(sql6.raw(
15969
- `SELECT domain, connection_type, property_id, sitemap_url, access_token, refresh_token, token_expires_at, scopes, created_at, updated_at FROM google_connections WHERE refresh_token IS NOT NULL AND refresh_token != ''`
15970
- ));
15971
- let migrated = 0;
15972
- for (const row of rows) {
15973
- const connType = row.connection_type;
15974
- const existing = getGoogleConnection(config, row.domain, connType);
15975
- if (existing?.refreshToken) continue;
15976
- upsertGoogleConnection(config, {
15977
- domain: row.domain,
15978
- connectionType: connType,
15979
- propertyId: row.property_id ?? null,
15980
- sitemapUrl: row.sitemap_url ?? null,
15981
- accessToken: row.access_token ?? void 0,
15982
- refreshToken: row.refresh_token ?? null,
15983
- tokenExpiresAt: row.token_expires_at ?? null,
15984
- scopes: parseJsonColumn(row.scopes, []),
15985
- createdAt: row.created_at,
15986
- updatedAt: row.updated_at
15987
- });
15988
- migrated++;
15989
- }
15990
- if (migrated > 0) {
15991
- saveConfigPatch({ google: config.google });
15992
- log9.info("credentials.migrated", { type: "google", count: migrated });
15993
- }
15994
- }
15995
- const gaColCheck = db.all(sql6.raw(
15996
- `SELECT COUNT(*) as c FROM pragma_table_info('ga_connections') WHERE name = 'private_key'`
15997
- ));
15998
- if (gaColCheck[0]?.c) {
15999
- const rows = db.all(sql6.raw(
16000
- `SELECT id, project_id, property_id, client_email, private_key, created_at, updated_at FROM ga_connections WHERE private_key IS NOT NULL AND private_key != ''`
16001
- ));
16002
- let migrated = 0;
16003
- for (const row of rows) {
16004
- const project = db.select({ name: projects.name }).from(projects).where(eq24(projects.id, row.project_id)).get();
16005
- if (!project) continue;
16006
- const existing = getGa4Connection(config, project.name);
16007
- if (existing?.privateKey) continue;
16008
- upsertGa4Connection(config, {
16009
- projectName: project.name,
16010
- propertyId: row.property_id,
16011
- clientEmail: row.client_email,
16012
- privateKey: row.private_key,
16013
- createdAt: row.created_at,
16014
- updatedAt: row.updated_at
16015
- });
16016
- migrated++;
16017
- }
16018
- if (migrated > 0) {
16019
- saveConfigPatch({ ga4: config.ga4 });
16020
- log9.info("credentials.migrated", { type: "ga4", count: migrated });
16021
- }
16022
- }
16023
- } catch {
17112
+ function applyLegacyCredentials(rows, config) {
17113
+ let migratedGoogle = 0;
17114
+ for (const row of rows.google) {
17115
+ const existing = getGoogleConnection(config, row.domain, row.connectionType);
17116
+ if (existing?.refreshToken) continue;
17117
+ upsertGoogleConnection(config, {
17118
+ domain: row.domain,
17119
+ connectionType: row.connectionType,
17120
+ propertyId: row.propertyId,
17121
+ sitemapUrl: row.sitemapUrl,
17122
+ accessToken: row.accessToken ?? void 0,
17123
+ refreshToken: row.refreshToken,
17124
+ tokenExpiresAt: row.tokenExpiresAt,
17125
+ scopes: row.scopes,
17126
+ createdAt: row.createdAt,
17127
+ updatedAt: row.updatedAt
17128
+ });
17129
+ migratedGoogle++;
17130
+ }
17131
+ if (migratedGoogle > 0) {
17132
+ saveConfigPatch({ google: config.google });
17133
+ log9.info("credentials.migrated", { type: "google", count: migratedGoogle });
17134
+ }
17135
+ let migratedGa4 = 0;
17136
+ for (const row of rows.ga4) {
17137
+ const existing = getGa4Connection(config, row.projectName);
17138
+ if (existing?.privateKey) continue;
17139
+ upsertGa4Connection(config, {
17140
+ projectName: row.projectName,
17141
+ propertyId: row.propertyId,
17142
+ clientEmail: row.clientEmail,
17143
+ privateKey: row.privateKey,
17144
+ createdAt: row.createdAt,
17145
+ updatedAt: row.updatedAt
17146
+ });
17147
+ migratedGa4++;
17148
+ }
17149
+ if (migratedGa4 > 0) {
17150
+ saveConfigPatch({ ga4: config.ga4 });
17151
+ log9.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
16024
17152
  }
16025
17153
  }
16026
17154
  async function createServer(opts) {
@@ -16047,7 +17175,15 @@ async function createServer(opts) {
16047
17175
  quota: opts.config.geminiQuota
16048
17176
  };
16049
17177
  }
16050
- migrateDbCredentialsToConfig(opts.db, opts.config);
17178
+ try {
17179
+ const legacyRows = extractLegacyCredentials(opts.db);
17180
+ applyLegacyCredentials(legacyRows, opts.config);
17181
+ dropLegacyCredentialColumns(opts.db);
17182
+ } catch (err) {
17183
+ log9.warn("credentials.migration.failed", {
17184
+ error: err instanceof Error ? err.message : String(err)
17185
+ });
17186
+ }
16051
17187
  log9.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
16052
17188
  const p = providers[k];
16053
17189
  return p?.apiKey || p?.baseUrl || p?.vertexProject;
@@ -16085,27 +17221,35 @@ async function createServer(opts) {
16085
17221
  jobRunner.recoverStaleRuns();
16086
17222
  const notifier = new Notifier(opts.db, serverUrl);
16087
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
+ });
16088
17230
  const runCoordinator = new RunCoordinator(
16089
17231
  notifier,
16090
17232
  intelligenceService,
16091
- (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
+ }
16092
17244
  );
16093
17245
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
16094
17246
  const snapshotService = new SnapshotService(registry);
16095
- let agentManager;
16096
- let agentAutoStarted = false;
16097
- if (opts.config.agent) {
16098
- const stateDir = getAeroStateDir(opts.config.agent.profile ?? "aero");
16099
- agentManager = new AgentManager(opts.config.agent, stateDir);
16100
- if (opts.config.agent.autoStart) {
16101
- try {
16102
- await agentManager.start();
16103
- agentAutoStarted = true;
16104
- app.log.info({ pid: agentManager.status().pid }, "Agent gateway started");
16105
- } catch (err) {
16106
- app.log.error({ err }, "Failed to auto-start agent gateway");
16107
- }
16108
- }
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
+ );
16109
17253
  }
16110
17254
  const scheduler = new Scheduler(opts.db, {
16111
17255
  onRunCreated: (runId, projectId, providers2, location) => {
@@ -16228,7 +17372,7 @@ async function createServer(opts) {
16228
17372
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
16229
17373
  if (opts.config.apiKey) {
16230
17374
  const keyHash = hashApiKey(opts.config.apiKey);
16231
- 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();
16232
17376
  if (!existing) {
16233
17377
  const prefix = opts.config.apiKey.slice(0, 12);
16234
17378
  opts.db.insert(apiKeys).values({
@@ -16280,7 +17424,7 @@ async function createServer(opts) {
16280
17424
  };
16281
17425
  const getDefaultApiKey = () => {
16282
17426
  if (!opts.config.apiKey) return void 0;
16283
- 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();
16284
17428
  };
16285
17429
  const createPasswordSession = (reply) => {
16286
17430
  const key = getDefaultApiKey();
@@ -16337,12 +17481,12 @@ async function createServer(opts) {
16337
17481
  return reply.send({ authenticated: true });
16338
17482
  }
16339
17483
  if (apiKey) {
16340
- 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();
16341
17485
  if (!key || key.revokedAt) {
16342
17486
  const err2 = authInvalid();
16343
17487
  return reply.status(err2.statusCode).send(err2.toJSON());
16344
17488
  }
16345
- 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();
16346
17490
  const sessionId = createSession(key.id);
16347
17491
  reply.header("set-cookie", serializeSessionCookie({
16348
17492
  name: SESSION_COOKIE_NAME,
@@ -16374,6 +17518,11 @@ async function createServer(opts) {
16374
17518
  skipAuth: false,
16375
17519
  sessionCookieName: SESSION_COOKIE_NAME,
16376
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
+ },
16377
17526
  getGoogleAuthConfig: () => getGoogleAuthConfig(opts.config),
16378
17527
  googleConnectionStore,
16379
17528
  googleStateSecret,
@@ -16406,7 +17555,8 @@ async function createServer(opts) {
16406
17555
  },
16407
17556
  openApiInfo: {
16408
17557
  title: "Canonry API",
16409
- version: PKG_VERSION
17558
+ version: PKG_VERSION,
17559
+ includeCanonryLocal: true
16410
17560
  },
16411
17561
  providerSummary,
16412
17562
  providerAdapters: [...API_ADAPTERS, ...BROWSER_ADAPTERS].map((a) => ({
@@ -16530,17 +17680,6 @@ async function createServer(opts) {
16530
17680
  onProjectDeleted: (projectId) => {
16531
17681
  scheduler.remove(projectId);
16532
17682
  },
16533
- onProjectUpserted: agentManager && opts.config.agent?.autoStart ? (_projectId, projectName) => {
16534
- try {
16535
- const gatewayPort = opts.config.agent?.gatewayPort ?? 3579;
16536
- const result = attachAgentWebhookDirect(opts.db, _projectId, gatewayPort);
16537
- if (result === "attached") {
16538
- app.log.info({ projectName }, "Auto-attached agent webhook");
16539
- }
16540
- } catch (err) {
16541
- app.log.error({ err, projectName }, "Failed to auto-attach agent webhook");
16542
- }
16543
- } : void 0,
16544
17683
  getTelemetryStatus: () => {
16545
17684
  const enabled = isTelemetryEnabled();
16546
17685
  return {
@@ -16625,15 +17764,15 @@ async function createServer(opts) {
16625
17764
  return snapshotService.createReport(input);
16626
17765
  }
16627
17766
  });
16628
- const dirname = path8.dirname(fileURLToPath2(import.meta.url));
16629
- const assetsDir = path8.join(dirname, "..", "assets");
16630
- if (fs7.existsSync(assetsDir)) {
16631
- const indexPath = path8.join(assetsDir, "index.html");
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");
16632
17771
  const injectConfig = (html) => {
16633
17772
  const clientConfig = {};
16634
17773
  if (basePath) clientConfig.basePath = basePath;
16635
17774
  const configScript = `<script>window.__CANONRY_CONFIG__=${JSON.stringify(clientConfig)}</script>`;
16636
- const baseTag = basePath ? `<base href="${basePath}">` : "";
17775
+ const baseTag = `<base href="${basePath ?? "/"}">`;
16637
17776
  return html.replace("<head>", `<head>${baseTag}`).replace("</head>", `${configScript}</head>`);
16638
17777
  };
16639
17778
  const fastifyStatic = await import("@fastify/static");
@@ -16646,8 +17785,8 @@ async function createServer(opts) {
16646
17785
  index: false
16647
17786
  });
16648
17787
  const serveIndex = (_request, reply) => {
16649
- if (fs7.existsSync(indexPath)) {
16650
- const html = fs7.readFileSync(indexPath, "utf-8");
17788
+ if (fs8.existsSync(indexPath)) {
17789
+ const html = fs8.readFileSync(indexPath, "utf-8");
16651
17790
  return reply.type("text/html").send(injectConfig(html));
16652
17791
  }
16653
17792
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -16667,8 +17806,8 @@ async function createServer(opts) {
16667
17806
  if (basePath && !url.startsWith(basePath)) {
16668
17807
  return reply.status(404).send({ error: "Not found", path: request.url });
16669
17808
  }
16670
- if (fs7.existsSync(indexPath)) {
16671
- const html = fs7.readFileSync(indexPath, "utf-8");
17809
+ if (fs8.existsSync(indexPath)) {
17810
+ const html = fs8.readFileSync(indexPath, "utf-8");
16672
17811
  return reply.type("text/html").send(injectConfig(html));
16673
17812
  }
16674
17813
  return reply.status(404).send({ error: "Not found" });
@@ -16687,13 +17826,6 @@ async function createServer(opts) {
16687
17826
  scheduler.start();
16688
17827
  app.addHook("onClose", async () => {
16689
17828
  scheduler.stop();
16690
- if (agentManager && agentAutoStarted) {
16691
- try {
16692
- await agentManager.stop();
16693
- } catch (err) {
16694
- app.log.error({ err }, "Failed to stop agent gateway");
16695
- }
16696
- }
16697
17829
  });
16698
17830
  return app;
16699
17831
  }
@@ -16755,7 +17887,6 @@ export {
16755
17887
  isFirstRun,
16756
17888
  showFirstRunNotice,
16757
17889
  trackEvent,
16758
- EXIT_USER_ERROR,
16759
17890
  EXIT_SYSTEM_ERROR,
16760
17891
  CliError,
16761
17892
  usageError,
@@ -16774,21 +17905,10 @@ export {
16774
17905
  determineCitationState,
16775
17906
  computeCompetitorOverlap,
16776
17907
  extractRecommendedCompetitors,
17908
+ createApiClient,
16777
17909
  setGoogleAuthConfig,
16778
17910
  formatAuditFactorScore,
16779
- AGENT_WEBHOOK_EVENTS,
16780
- buildAgentWebhookUrl,
16781
- attachAgentWebhookDirect,
16782
- AgentManager,
16783
- getAeroStateDir,
16784
- detectOpenClaw,
16785
- installOpenClaw,
16786
- seedWorkspace,
16787
- initializeOpenClawProfile,
16788
- configureOpenClawGateway,
16789
- setOpenClawModel,
16790
- providerEnvVar,
16791
- writeAgentEnv,
16792
- resolveAgentCredentials,
17911
+ listAgentProviders,
17912
+ coerceAgentProvider,
16793
17913
  createServer
16794
17914
  };