@ainyc/canonry 1.48.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -27
- package/assets/agent-workspace/AGENTS.md +3 -4
- package/assets/agent-workspace/skills/aero/SKILL.md +7 -10
- package/assets/agent-workspace/skills/aero/references/memory-patterns.md +35 -25
- package/assets/agent-workspace/skills/aero/references/orchestration.md +5 -0
- package/assets/agent-workspace/skills/aero/references/regression-playbook.md +5 -0
- package/assets/agent-workspace/skills/aero/references/reporting.md +5 -0
- package/assets/agent-workspace/skills/aero/references/wordpress-elementor-mcp.md +5 -0
- package/assets/agent-workspace/skills/aero/soul.md +30 -0
- package/assets/agent-workspace/skills/canonry-setup/references/canonry-cli.md +38 -38
- package/assets/assets/index-DnlDoqE-.js +301 -0
- package/assets/assets/{index--ev1Bjls.css → index-yF1fs-OW.css} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-ZZ57GRV6.js → chunk-GH6WGN5B.js} +37 -1
- package/dist/{chunk-IPOVH342.js → chunk-YZKLIUH4.js} +1923 -791
- package/dist/cli.js +281 -773
- package/dist/index.d.ts +2 -8
- package/dist/index.js +2 -2
- package/dist/{intelligence-service-MZ7SXEGE.js → intelligence-service-LHWXONQJ.js} +1 -1
- package/package.json +8 -5
- package/assets/agent-workspace/SOUL.md +0 -54
- package/assets/assets/index-CVk23m8J.js +0 -282
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IntelligenceService,
|
|
3
|
+
agentSessions,
|
|
3
4
|
apiKeys,
|
|
4
5
|
auditLog,
|
|
5
6
|
bingCoverageSnapshots,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
runs,
|
|
26
27
|
schedules,
|
|
27
28
|
usageCounters
|
|
28
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-GH6WGN5B.js";
|
|
29
30
|
|
|
30
31
|
// src/config.ts
|
|
31
32
|
import fs from "fs";
|
|
@@ -337,10 +338,10 @@ function printCliError(err, format) {
|
|
|
337
338
|
// src/server.ts
|
|
338
339
|
import { createRequire as createRequire2 } from "module";
|
|
339
340
|
import crypto23 from "crypto";
|
|
340
|
-
import
|
|
341
|
-
import
|
|
341
|
+
import fs8 from "fs";
|
|
342
|
+
import path9 from "path";
|
|
342
343
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
343
|
-
import { eq as
|
|
344
|
+
import { eq as eq25 } from "drizzle-orm";
|
|
344
345
|
import Fastify from "fastify";
|
|
345
346
|
|
|
346
347
|
// ../contracts/src/config-schema.ts
|
|
@@ -602,6 +603,13 @@ function notImplemented(message) {
|
|
|
602
603
|
function deliveryFailed(message) {
|
|
603
604
|
return new AppError("DELIVERY_FAILED", message, 502);
|
|
604
605
|
}
|
|
606
|
+
function agentBusy(projectName) {
|
|
607
|
+
return new AppError(
|
|
608
|
+
"AGENT_BUSY",
|
|
609
|
+
`Aero is already running a turn for '${projectName}'. Retry after the current turn settles.`,
|
|
610
|
+
409
|
|
611
|
+
);
|
|
612
|
+
}
|
|
605
613
|
|
|
606
614
|
// ../contracts/src/google.ts
|
|
607
615
|
import { z as z5 } from "zod";
|
|
@@ -889,6 +897,37 @@ var wordpressDiffDtoSchema = z7.object({
|
|
|
889
897
|
})
|
|
890
898
|
});
|
|
891
899
|
|
|
900
|
+
// ../contracts/src/providers.ts
|
|
901
|
+
var ProviderIds = {
|
|
902
|
+
claude: "claude",
|
|
903
|
+
openai: "openai",
|
|
904
|
+
gemini: "gemini",
|
|
905
|
+
perplexity: "perplexity",
|
|
906
|
+
local: "local",
|
|
907
|
+
cdpChatgpt: "cdp:chatgpt",
|
|
908
|
+
zai: "zai"
|
|
909
|
+
};
|
|
910
|
+
var PROVIDER_IDS = Object.values(ProviderIds);
|
|
911
|
+
var SweepProviderIds = {
|
|
912
|
+
claude: ProviderIds.claude,
|
|
913
|
+
openai: ProviderIds.openai,
|
|
914
|
+
gemini: ProviderIds.gemini,
|
|
915
|
+
perplexity: ProviderIds.perplexity,
|
|
916
|
+
local: ProviderIds.local,
|
|
917
|
+
cdpChatgpt: ProviderIds.cdpChatgpt
|
|
918
|
+
};
|
|
919
|
+
var SWEEP_PROVIDER_IDS = Object.values(SweepProviderIds);
|
|
920
|
+
var AgentProviderIds = {
|
|
921
|
+
claude: ProviderIds.claude,
|
|
922
|
+
openai: ProviderIds.openai,
|
|
923
|
+
gemini: ProviderIds.gemini,
|
|
924
|
+
zai: ProviderIds.zai
|
|
925
|
+
};
|
|
926
|
+
var AGENT_PROVIDER_IDS = Object.values(AgentProviderIds);
|
|
927
|
+
function isAgentProviderId(value) {
|
|
928
|
+
return AGENT_PROVIDER_IDS.includes(value);
|
|
929
|
+
}
|
|
930
|
+
|
|
892
931
|
// ../contracts/src/run.ts
|
|
893
932
|
import { z as z8 } from "zod";
|
|
894
933
|
var runStatusSchema = z8.enum(["queued", "running", "completed", "partial", "failed", "cancelled"]);
|
|
@@ -2381,7 +2420,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
2381
2420
|
const body = JSON.stringify(payload);
|
|
2382
2421
|
const isHttps = target.url.protocol === "https:";
|
|
2383
2422
|
const port = target.url.port ? Number(target.url.port) : isHttps ? 443 : 80;
|
|
2384
|
-
const
|
|
2423
|
+
const path10 = `${target.url.pathname}${target.url.search}`;
|
|
2385
2424
|
const headers = {
|
|
2386
2425
|
"Content-Length": String(Buffer.byteLength(body)),
|
|
2387
2426
|
"Content-Type": "application/json",
|
|
@@ -2397,7 +2436,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
2397
2436
|
headers,
|
|
2398
2437
|
hostname: target.address,
|
|
2399
2438
|
method: "POST",
|
|
2400
|
-
path:
|
|
2439
|
+
path: path10,
|
|
2401
2440
|
port,
|
|
2402
2441
|
timeout: REQUEST_TIMEOUT_MS
|
|
2403
2442
|
};
|
|
@@ -5567,10 +5606,91 @@ var routeCatalog = [
|
|
|
5567
5606
|
}
|
|
5568
5607
|
}
|
|
5569
5608
|
];
|
|
5609
|
+
var canonryLocalRouteCatalog = [
|
|
5610
|
+
{
|
|
5611
|
+
method: "get",
|
|
5612
|
+
path: "/api/v1/projects/{name}/agent/transcript",
|
|
5613
|
+
summary: "Get the rolling Aero transcript for this project",
|
|
5614
|
+
description: "Returns the full message history of the project-scoped Aero session plus the persisted model provider/id and last-updated timestamp. Empty messages array when the project has no session yet.",
|
|
5615
|
+
tags: ["agent"],
|
|
5616
|
+
parameters: [nameParameter],
|
|
5617
|
+
responses: {
|
|
5618
|
+
200: { description: "Transcript returned." },
|
|
5619
|
+
404: { description: "Project not found." }
|
|
5620
|
+
}
|
|
5621
|
+
},
|
|
5622
|
+
{
|
|
5623
|
+
method: "delete",
|
|
5624
|
+
path: "/api/v1/projects/{name}/agent/transcript",
|
|
5625
|
+
summary: "Reset the Aero transcript + queued follow-ups",
|
|
5626
|
+
description: "Evicts any live Agent instance, clears the persisted messages and follow_up_queue. A subsequent prompt starts a fresh session.",
|
|
5627
|
+
tags: ["agent"],
|
|
5628
|
+
parameters: [nameParameter],
|
|
5629
|
+
responses: {
|
|
5630
|
+
200: { description: "Session reset." },
|
|
5631
|
+
404: { description: "Project not found." }
|
|
5632
|
+
}
|
|
5633
|
+
},
|
|
5634
|
+
{
|
|
5635
|
+
method: "get",
|
|
5636
|
+
path: "/api/v1/projects/{name}/agent/providers",
|
|
5637
|
+
summary: "List the LLM providers Aero can route to",
|
|
5638
|
+
description: "Returns every provider Aero knows about with its default model, whether a usable API key is configured, and where the key resolved from (`config` | `env`). `defaultProvider` is the one Aero auto-picks when a caller omits `provider` on the prompt endpoint. Path is project-scoped for auth symmetry; the response does not vary per project today.",
|
|
5639
|
+
tags: ["agent"],
|
|
5640
|
+
parameters: [nameParameter],
|
|
5641
|
+
responses: {
|
|
5642
|
+
200: { description: "Providers returned." },
|
|
5643
|
+
404: { description: "Project not found." }
|
|
5644
|
+
}
|
|
5645
|
+
},
|
|
5646
|
+
{
|
|
5647
|
+
method: "post",
|
|
5648
|
+
path: "/api/v1/projects/{name}/agent/prompt",
|
|
5649
|
+
summary: "Send a prompt to Aero and stream events back as SSE",
|
|
5650
|
+
description: 'Posts a prompt into the project\'s Aero session and streams `AgentEvent` frames as `text/event-stream`. Each frame is `data: <JSON>\\n\\n`. The server brackets the stream with `{"type":"stream_open"}` and `{"type":"stream_close"}` control frames; `{"type":"error","message":"..."}` surfaces in-stream failures without collapsing the stream. Returns 409 `AGENT_BUSY` if another turn is already in flight for this project. Body field `scope` accepts "all" | "read-only"; omitted defaults to "read-only" (safe dashboard surface). The CLI passes "all" to keep write tools available.',
|
|
5651
|
+
tags: ["agent"],
|
|
5652
|
+
parameters: [nameParameter],
|
|
5653
|
+
requestBody: {
|
|
5654
|
+
required: true,
|
|
5655
|
+
content: {
|
|
5656
|
+
"application/json": {
|
|
5657
|
+
schema: {
|
|
5658
|
+
type: "object",
|
|
5659
|
+
required: ["prompt"],
|
|
5660
|
+
properties: {
|
|
5661
|
+
prompt: { type: "string", description: "The user's message for Aero." },
|
|
5662
|
+
provider: {
|
|
5663
|
+
type: "string",
|
|
5664
|
+
enum: [...AGENT_PROVIDER_IDS],
|
|
5665
|
+
description: "Override the persisted LLM provider for this and subsequent turns."
|
|
5666
|
+
},
|
|
5667
|
+
modelId: {
|
|
5668
|
+
type: "string",
|
|
5669
|
+
description: "Override the persisted model id for this and subsequent turns."
|
|
5670
|
+
},
|
|
5671
|
+
scope: {
|
|
5672
|
+
type: "string",
|
|
5673
|
+
enum: ["all", "read-only"],
|
|
5674
|
+
description: 'Tool surface scope. Default "read-only". Set "all" to enable write tools.'
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5680
|
+
},
|
|
5681
|
+
responses: {
|
|
5682
|
+
200: { description: "SSE stream of AgentEvent frames." },
|
|
5683
|
+
400: { description: "Missing or empty prompt." },
|
|
5684
|
+
404: { description: "Project not found." },
|
|
5685
|
+
409: { description: "Another Aero turn is already in flight." }
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
];
|
|
5570
5689
|
function buildOpenApiDocument(info = {}) {
|
|
5571
5690
|
const BASE_PREFIX = "/api/v1";
|
|
5572
5691
|
const prefix = info.routePrefix ?? BASE_PREFIX;
|
|
5573
|
-
const
|
|
5692
|
+
const fullCatalog = info.includeCanonryLocal ? [...routeCatalog, ...canonryLocalRouteCatalog] : routeCatalog;
|
|
5693
|
+
const paths = fullCatalog.reduce((acc, route) => {
|
|
5574
5694
|
const subpath = route.path.startsWith(BASE_PREFIX) ? route.path.slice(BASE_PREFIX.length) : route.path;
|
|
5575
5695
|
const fullPath = prefix + subpath;
|
|
5576
5696
|
const operation = {
|
|
@@ -5618,8 +5738,8 @@ async function openApiRoutes(app, opts = {}) {
|
|
|
5618
5738
|
return reply.type("application/json").send(buildOpenApiDocument(opts));
|
|
5619
5739
|
});
|
|
5620
5740
|
}
|
|
5621
|
-
function buildOperationId(method,
|
|
5622
|
-
const parts =
|
|
5741
|
+
function buildOperationId(method, path10) {
|
|
5742
|
+
const parts = path10.split("/").filter(Boolean).map((part) => {
|
|
5623
5743
|
if (part.startsWith("{") && part.endsWith("}")) {
|
|
5624
5744
|
return `by-${part.slice(1, -1)}`;
|
|
5625
5745
|
}
|
|
@@ -9265,10 +9385,10 @@ function buildAuthErrorMessage(res, responseText) {
|
|
|
9265
9385
|
}
|
|
9266
9386
|
return "WordPress credentials are invalid or lack permission for this action";
|
|
9267
9387
|
}
|
|
9268
|
-
async function fetchJson(connection, siteUrl,
|
|
9388
|
+
async function fetchJson(connection, siteUrl, path10, init) {
|
|
9269
9389
|
if (siteUrl.startsWith("http:")) {
|
|
9270
9390
|
}
|
|
9271
|
-
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${
|
|
9391
|
+
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path10}`, {
|
|
9272
9392
|
...init,
|
|
9273
9393
|
headers: {
|
|
9274
9394
|
"Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
|
|
@@ -10858,41 +10978,14 @@ async function apiRoutes(app, opts) {
|
|
|
10858
10978
|
googleConnectionStore: opts.googleConnectionStore,
|
|
10859
10979
|
getGoogleAuthConfig: opts.getGoogleAuthConfig
|
|
10860
10980
|
});
|
|
10981
|
+
if (opts.registerAuthenticatedRoutes) {
|
|
10982
|
+
await opts.registerAuthenticatedRoutes(api);
|
|
10983
|
+
}
|
|
10861
10984
|
}, { prefix: opts.routePrefix ?? "/api/v1" });
|
|
10862
10985
|
}
|
|
10863
10986
|
|
|
10864
|
-
// src/
|
|
10865
|
-
import
|
|
10866
|
-
import { eq as eq18 } from "drizzle-orm";
|
|
10867
|
-
var AGENT_WEBHOOK_EVENTS = ["run.completed", "insight.critical", "insight.high", "citation.gained"];
|
|
10868
|
-
function buildAgentWebhookUrl(gatewayPort) {
|
|
10869
|
-
return `http://localhost:${gatewayPort}/hooks/canonry`;
|
|
10870
|
-
}
|
|
10871
|
-
function attachAgentWebhookDirect(db, projectId, gatewayPort) {
|
|
10872
|
-
const agentUrl = buildAgentWebhookUrl(gatewayPort);
|
|
10873
|
-
const existing = db.select().from(notifications).where(eq18(notifications.projectId, projectId)).all();
|
|
10874
|
-
const hasAgent = existing.some((n) => {
|
|
10875
|
-
const cfg = parseJsonColumn(n.config, {});
|
|
10876
|
-
return cfg.source === "agent";
|
|
10877
|
-
});
|
|
10878
|
-
if (hasAgent) return "already-attached";
|
|
10879
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10880
|
-
db.insert(notifications).values({
|
|
10881
|
-
id: crypto18.randomUUID(),
|
|
10882
|
-
projectId,
|
|
10883
|
-
channel: "webhook",
|
|
10884
|
-
config: JSON.stringify({
|
|
10885
|
-
url: agentUrl,
|
|
10886
|
-
events: [...AGENT_WEBHOOK_EVENTS],
|
|
10887
|
-
source: "agent"
|
|
10888
|
-
}),
|
|
10889
|
-
enabled: 1,
|
|
10890
|
-
webhookSecret: crypto18.randomUUID(),
|
|
10891
|
-
createdAt: now,
|
|
10892
|
-
updatedAt: now
|
|
10893
|
-
}).run();
|
|
10894
|
-
return "attached";
|
|
10895
|
-
}
|
|
10987
|
+
// src/server.ts
|
|
10988
|
+
import os5 from "os";
|
|
10896
10989
|
|
|
10897
10990
|
// ../provider-gemini/src/normalize.ts
|
|
10898
10991
|
import { GoogleGenAI } from "@google/genai";
|
|
@@ -13345,11 +13438,11 @@ function removeWordpressConnection(config, projectName) {
|
|
|
13345
13438
|
}
|
|
13346
13439
|
|
|
13347
13440
|
// src/job-runner.ts
|
|
13348
|
-
import
|
|
13441
|
+
import crypto18 from "crypto";
|
|
13349
13442
|
import fs4 from "fs";
|
|
13350
13443
|
import path5 from "path";
|
|
13351
13444
|
import os4 from "os";
|
|
13352
|
-
import { and as and7, eq as
|
|
13445
|
+
import { and as and7, eq as eq18, inArray as inArray3, sql as sql4 } from "drizzle-orm";
|
|
13353
13446
|
|
|
13354
13447
|
// src/citation-utils.ts
|
|
13355
13448
|
function domainMatches(domain, canonicalDomain) {
|
|
@@ -13585,7 +13678,7 @@ var JobRunner = class {
|
|
|
13585
13678
|
if (stale.length === 0) return;
|
|
13586
13679
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13587
13680
|
for (const run of stale) {
|
|
13588
|
-
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(
|
|
13681
|
+
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq18(runs.id, run.id)).run();
|
|
13589
13682
|
log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
|
|
13590
13683
|
}
|
|
13591
13684
|
}
|
|
@@ -13613,10 +13706,10 @@ var JobRunner = class {
|
|
|
13613
13706
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
13614
13707
|
}
|
|
13615
13708
|
if (existingRun.status === "queued") {
|
|
13616
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(
|
|
13709
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(eq18(runs.id, runId), eq18(runs.status, "queued"))).run();
|
|
13617
13710
|
}
|
|
13618
13711
|
this.throwIfRunCancelled(runId);
|
|
13619
|
-
const project = this.db.select().from(projects).where(
|
|
13712
|
+
const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
|
|
13620
13713
|
if (!project) {
|
|
13621
13714
|
throw new Error(`Project ${projectId} not found`);
|
|
13622
13715
|
}
|
|
@@ -13636,8 +13729,8 @@ var JobRunner = class {
|
|
|
13636
13729
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
13637
13730
|
}
|
|
13638
13731
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
13639
|
-
projectKeywords = this.db.select().from(keywords).where(
|
|
13640
|
-
const projectCompetitors = this.db.select().from(competitors).where(
|
|
13732
|
+
projectKeywords = this.db.select().from(keywords).where(eq18(keywords.projectId, projectId)).all();
|
|
13733
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq18(competitors.projectId, projectId)).all();
|
|
13641
13734
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
13642
13735
|
const allDomains = effectiveDomains({
|
|
13643
13736
|
canonicalDomain: project.canonicalDomain,
|
|
@@ -13653,7 +13746,7 @@ var JobRunner = class {
|
|
|
13653
13746
|
const todayPeriod = getCurrentUsageDay();
|
|
13654
13747
|
for (const p of activeProviders) {
|
|
13655
13748
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
13656
|
-
const providerUsage = this.db.select().from(usageCounters).where(
|
|
13749
|
+
const providerUsage = this.db.select().from(usageCounters).where(eq18(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
|
|
13657
13750
|
const limit = p.config.quotaPolicy.maxRequestsPerDay;
|
|
13658
13751
|
if (providerUsage + queriesPerProvider > limit) {
|
|
13659
13752
|
throw new Error(
|
|
@@ -13713,7 +13806,7 @@ var JobRunner = class {
|
|
|
13713
13806
|
);
|
|
13714
13807
|
let screenshotRelPath = null;
|
|
13715
13808
|
if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
|
|
13716
|
-
const snapshotId =
|
|
13809
|
+
const snapshotId = crypto18.randomUUID();
|
|
13717
13810
|
const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
|
|
13718
13811
|
if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
|
|
13719
13812
|
const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
|
|
@@ -13743,7 +13836,7 @@ var JobRunner = class {
|
|
|
13743
13836
|
}).run();
|
|
13744
13837
|
} else {
|
|
13745
13838
|
this.db.insert(querySnapshots).values({
|
|
13746
|
-
id:
|
|
13839
|
+
id: crypto18.randomUUID(),
|
|
13747
13840
|
runId,
|
|
13748
13841
|
keywordId: kw.id,
|
|
13749
13842
|
provider: providerName,
|
|
@@ -13794,12 +13887,12 @@ var JobRunner = class {
|
|
|
13794
13887
|
const someFailed = providerErrors.size > 0;
|
|
13795
13888
|
if (allFailed) {
|
|
13796
13889
|
const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
|
|
13797
|
-
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13890
|
+
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
|
|
13798
13891
|
} else if (someFailed) {
|
|
13799
13892
|
const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
|
|
13800
|
-
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13893
|
+
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
|
|
13801
13894
|
} else {
|
|
13802
|
-
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13895
|
+
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
|
|
13803
13896
|
}
|
|
13804
13897
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13805
13898
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
@@ -13834,7 +13927,7 @@ var JobRunner = class {
|
|
|
13834
13927
|
status: "failed",
|
|
13835
13928
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13836
13929
|
error: errorMessage
|
|
13837
|
-
}).where(
|
|
13930
|
+
}).where(eq18(runs.id, runId)).run();
|
|
13838
13931
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13839
13932
|
trackEvent("run.completed", {
|
|
13840
13933
|
status: "failed",
|
|
@@ -13855,7 +13948,7 @@ var JobRunner = class {
|
|
|
13855
13948
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13856
13949
|
const period = now.slice(0, 10);
|
|
13857
13950
|
this.db.insert(usageCounters).values({
|
|
13858
|
-
id:
|
|
13951
|
+
id: crypto18.randomUUID(),
|
|
13859
13952
|
scope,
|
|
13860
13953
|
period,
|
|
13861
13954
|
metric,
|
|
@@ -13877,7 +13970,7 @@ var JobRunner = class {
|
|
|
13877
13970
|
status: runs.status,
|
|
13878
13971
|
finishedAt: runs.finishedAt,
|
|
13879
13972
|
error: runs.error
|
|
13880
|
-
}).from(runs).where(
|
|
13973
|
+
}).from(runs).where(eq18(runs.id, runId)).get();
|
|
13881
13974
|
}
|
|
13882
13975
|
isRunCancelled(runId) {
|
|
13883
13976
|
return this.getRunState(runId)?.status === "cancelled";
|
|
@@ -13893,7 +13986,7 @@ var JobRunner = class {
|
|
|
13893
13986
|
this.db.update(runs).set({
|
|
13894
13987
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13895
13988
|
error: currentRun.error ?? "Cancelled by user"
|
|
13896
|
-
}).where(
|
|
13989
|
+
}).where(eq18(runs.id, runId)).run();
|
|
13897
13990
|
}
|
|
13898
13991
|
trackEvent("run.completed", {
|
|
13899
13992
|
status: "cancelled",
|
|
@@ -13915,8 +14008,8 @@ function getCurrentUsageDay() {
|
|
|
13915
14008
|
}
|
|
13916
14009
|
|
|
13917
14010
|
// src/gsc-sync.ts
|
|
13918
|
-
import
|
|
13919
|
-
import { eq as
|
|
14011
|
+
import crypto19 from "crypto";
|
|
14012
|
+
import { eq as eq19, and as and8, sql as sql5 } from "drizzle-orm";
|
|
13920
14013
|
var log2 = createLogger("GscSync");
|
|
13921
14014
|
function formatDate2(d) {
|
|
13922
14015
|
return d.toISOString().split("T")[0];
|
|
@@ -13928,13 +14021,13 @@ function daysAgo(n) {
|
|
|
13928
14021
|
}
|
|
13929
14022
|
async function executeGscSync(db, runId, projectId, opts) {
|
|
13930
14023
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13931
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14024
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq19(runs.id, runId)).run();
|
|
13932
14025
|
try {
|
|
13933
14026
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13934
14027
|
if (!googleClientId || !googleClientSecret) {
|
|
13935
14028
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13936
14029
|
}
|
|
13937
|
-
const project = db.select().from(projects).where(
|
|
14030
|
+
const project = db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
13938
14031
|
if (!project) {
|
|
13939
14032
|
throw new Error(`Project not found: ${projectId}`);
|
|
13940
14033
|
}
|
|
@@ -13969,7 +14062,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13969
14062
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
13970
14063
|
db.delete(gscSearchData).where(
|
|
13971
14064
|
and8(
|
|
13972
|
-
|
|
14065
|
+
eq19(gscSearchData.projectId, projectId),
|
|
13973
14066
|
sql5`${gscSearchData.date} >= ${startDate}`,
|
|
13974
14067
|
sql5`${gscSearchData.date} <= ${endDate}`
|
|
13975
14068
|
)
|
|
@@ -13981,7 +14074,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13981
14074
|
for (const row of batch) {
|
|
13982
14075
|
const [query, page, country, device, date] = row.keys;
|
|
13983
14076
|
db.insert(gscSearchData).values({
|
|
13984
|
-
id:
|
|
14077
|
+
id: crypto19.randomUUID(),
|
|
13985
14078
|
projectId,
|
|
13986
14079
|
syncRunId: runId,
|
|
13987
14080
|
date: date ?? "",
|
|
@@ -14015,7 +14108,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
14015
14108
|
const rich = ir.richResultsResult;
|
|
14016
14109
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14017
14110
|
db.insert(gscUrlInspections).values({
|
|
14018
|
-
id:
|
|
14111
|
+
id: crypto19.randomUUID(),
|
|
14019
14112
|
projectId,
|
|
14020
14113
|
syncRunId: runId,
|
|
14021
14114
|
url: pageUrl,
|
|
@@ -14036,7 +14129,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
14036
14129
|
log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
|
|
14037
14130
|
}
|
|
14038
14131
|
}
|
|
14039
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14132
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).all();
|
|
14040
14133
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
14041
14134
|
for (const row of allInspections) {
|
|
14042
14135
|
const existing = latestByUrl.get(row.url);
|
|
@@ -14057,9 +14150,9 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
14057
14150
|
}
|
|
14058
14151
|
}
|
|
14059
14152
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
14060
|
-
db.delete(gscCoverageSnapshots).where(and8(
|
|
14153
|
+
db.delete(gscCoverageSnapshots).where(and8(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
14061
14154
|
db.insert(gscCoverageSnapshots).values({
|
|
14062
|
-
id:
|
|
14155
|
+
id: crypto19.randomUUID(),
|
|
14063
14156
|
projectId,
|
|
14064
14157
|
syncRunId: runId,
|
|
14065
14158
|
date: snapshotDate,
|
|
@@ -14068,19 +14161,19 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
14068
14161
|
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
14069
14162
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14070
14163
|
}).run();
|
|
14071
|
-
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14164
|
+
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
|
|
14072
14165
|
log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
14073
14166
|
} catch (err) {
|
|
14074
14167
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
14075
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14168
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
|
|
14076
14169
|
log2.error("sync.failed", { runId, projectId, error: errorMsg });
|
|
14077
14170
|
throw err;
|
|
14078
14171
|
}
|
|
14079
14172
|
}
|
|
14080
14173
|
|
|
14081
14174
|
// src/gsc-inspect-sitemap.ts
|
|
14082
|
-
import
|
|
14083
|
-
import { eq as
|
|
14175
|
+
import crypto20 from "crypto";
|
|
14176
|
+
import { eq as eq20, and as and9 } from "drizzle-orm";
|
|
14084
14177
|
|
|
14085
14178
|
// src/sitemap-parser.ts
|
|
14086
14179
|
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
@@ -14149,13 +14242,13 @@ async function parseSitemapRecursive(url, urls, depth) {
|
|
|
14149
14242
|
var log3 = createLogger("InspectSitemap");
|
|
14150
14243
|
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
14151
14244
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14152
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14245
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
|
|
14153
14246
|
try {
|
|
14154
14247
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
14155
14248
|
if (!googleClientId || !googleClientSecret) {
|
|
14156
14249
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
14157
14250
|
}
|
|
14158
|
-
const project = db.select().from(projects).where(
|
|
14251
|
+
const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
|
|
14159
14252
|
if (!project) {
|
|
14160
14253
|
throw new Error(`Project not found: ${projectId}`);
|
|
14161
14254
|
}
|
|
@@ -14196,7 +14289,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14196
14289
|
const rich = ir.richResultsResult;
|
|
14197
14290
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14198
14291
|
db.insert(gscUrlInspections).values({
|
|
14199
|
-
id:
|
|
14292
|
+
id: crypto20.randomUUID(),
|
|
14200
14293
|
projectId,
|
|
14201
14294
|
syncRunId: runId,
|
|
14202
14295
|
url: pageUrl,
|
|
@@ -14223,7 +14316,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14223
14316
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
14224
14317
|
}
|
|
14225
14318
|
}
|
|
14226
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14319
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
|
|
14227
14320
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
14228
14321
|
for (const row of allInspections) {
|
|
14229
14322
|
const existing = latestByUrl.get(row.url);
|
|
@@ -14244,9 +14337,9 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14244
14337
|
}
|
|
14245
14338
|
}
|
|
14246
14339
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
14247
|
-
db.delete(gscCoverageSnapshots).where(and9(
|
|
14340
|
+
db.delete(gscCoverageSnapshots).where(and9(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
14248
14341
|
db.insert(gscCoverageSnapshots).values({
|
|
14249
|
-
id:
|
|
14342
|
+
id: crypto20.randomUUID(),
|
|
14250
14343
|
projectId,
|
|
14251
14344
|
syncRunId: runId,
|
|
14252
14345
|
date: snapshotDate,
|
|
@@ -14256,11 +14349,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14256
14349
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14257
14350
|
}).run();
|
|
14258
14351
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
14259
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14352
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
14260
14353
|
log3.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
14261
14354
|
} catch (err) {
|
|
14262
14355
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
14263
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14356
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
14264
14357
|
log3.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
14265
14358
|
throw err;
|
|
14266
14359
|
}
|
|
@@ -14319,7 +14412,7 @@ var ProviderRegistry = class {
|
|
|
14319
14412
|
|
|
14320
14413
|
// src/scheduler.ts
|
|
14321
14414
|
import cron from "node-cron";
|
|
14322
|
-
import { eq as
|
|
14415
|
+
import { eq as eq21 } from "drizzle-orm";
|
|
14323
14416
|
var log4 = createLogger("Scheduler");
|
|
14324
14417
|
var Scheduler = class {
|
|
14325
14418
|
db;
|
|
@@ -14331,7 +14424,7 @@ var Scheduler = class {
|
|
|
14331
14424
|
}
|
|
14332
14425
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
14333
14426
|
start() {
|
|
14334
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
14427
|
+
const allSchedules = this.db.select().from(schedules).where(eq21(schedules.enabled, 1)).all();
|
|
14335
14428
|
for (const schedule of allSchedules) {
|
|
14336
14429
|
const missedRunAt = schedule.nextRunAt;
|
|
14337
14430
|
this.registerCronTask(schedule);
|
|
@@ -14356,7 +14449,7 @@ var Scheduler = class {
|
|
|
14356
14449
|
this.stopTask(projectId, existing, "Stopped");
|
|
14357
14450
|
this.tasks.delete(projectId);
|
|
14358
14451
|
}
|
|
14359
|
-
const schedule = this.db.select().from(schedules).where(
|
|
14452
|
+
const schedule = this.db.select().from(schedules).where(eq21(schedules.projectId, projectId)).get();
|
|
14360
14453
|
if (schedule && schedule.enabled === 1) {
|
|
14361
14454
|
this.registerCronTask(schedule);
|
|
14362
14455
|
}
|
|
@@ -14389,14 +14482,14 @@ var Scheduler = class {
|
|
|
14389
14482
|
this.db.update(schedules).set({
|
|
14390
14483
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
14391
14484
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14392
|
-
}).where(
|
|
14485
|
+
}).where(eq21(schedules.id, scheduleId)).run();
|
|
14393
14486
|
const label = schedule.preset ?? cronExpr;
|
|
14394
14487
|
log4.info("cron.registered", { projectId, schedule: label, timezone });
|
|
14395
14488
|
}
|
|
14396
14489
|
triggerRun(scheduleId, projectId) {
|
|
14397
14490
|
try {
|
|
14398
14491
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14399
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
14492
|
+
const currentSchedule = this.db.select().from(schedules).where(eq21(schedules.id, scheduleId)).get();
|
|
14400
14493
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
14401
14494
|
log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
|
|
14402
14495
|
this.remove(projectId);
|
|
@@ -14404,7 +14497,7 @@ var Scheduler = class {
|
|
|
14404
14497
|
}
|
|
14405
14498
|
const task = this.tasks.get(projectId);
|
|
14406
14499
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
14407
|
-
const project = this.db.select().from(projects).where(
|
|
14500
|
+
const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
|
|
14408
14501
|
if (!project) {
|
|
14409
14502
|
log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
|
|
14410
14503
|
this.remove(projectId);
|
|
@@ -14433,7 +14526,7 @@ var Scheduler = class {
|
|
|
14433
14526
|
this.db.update(schedules).set({
|
|
14434
14527
|
nextRunAt,
|
|
14435
14528
|
updatedAt: now
|
|
14436
|
-
}).where(
|
|
14529
|
+
}).where(eq21(schedules.id, currentSchedule.id)).run();
|
|
14437
14530
|
return;
|
|
14438
14531
|
}
|
|
14439
14532
|
const runId = queueResult.runId;
|
|
@@ -14441,7 +14534,7 @@ var Scheduler = class {
|
|
|
14441
14534
|
lastRunAt: now,
|
|
14442
14535
|
nextRunAt,
|
|
14443
14536
|
updatedAt: now
|
|
14444
|
-
}).where(
|
|
14537
|
+
}).where(eq21(schedules.id, currentSchedule.id)).run();
|
|
14445
14538
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
14446
14539
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
14447
14540
|
log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -14453,8 +14546,8 @@ var Scheduler = class {
|
|
|
14453
14546
|
};
|
|
14454
14547
|
|
|
14455
14548
|
// src/notifier.ts
|
|
14456
|
-
import { eq as
|
|
14457
|
-
import
|
|
14549
|
+
import { eq as eq22, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
|
|
14550
|
+
import crypto21 from "crypto";
|
|
14458
14551
|
var log5 = createLogger("Notifier");
|
|
14459
14552
|
var Notifier = class {
|
|
14460
14553
|
db;
|
|
@@ -14466,18 +14559,18 @@ var Notifier = class {
|
|
|
14466
14559
|
/** Called after a run completes (success, partial, or failed). */
|
|
14467
14560
|
async onRunCompleted(runId, projectId) {
|
|
14468
14561
|
log5.info("run.completed", { runId, projectId });
|
|
14469
|
-
const notifs = this.db.select().from(notifications).where(
|
|
14562
|
+
const notifs = this.db.select().from(notifications).where(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14470
14563
|
if (notifs.length === 0) {
|
|
14471
14564
|
log5.info("notifications.none-enabled", { projectId });
|
|
14472
14565
|
return;
|
|
14473
14566
|
}
|
|
14474
14567
|
log5.info("notifications.found", { projectId, count: notifs.length });
|
|
14475
|
-
const run = this.db.select().from(runs).where(
|
|
14568
|
+
const run = this.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
14476
14569
|
if (!run) {
|
|
14477
14570
|
log5.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
14478
14571
|
return;
|
|
14479
14572
|
}
|
|
14480
|
-
const project = this.db.select().from(projects).where(
|
|
14573
|
+
const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
14481
14574
|
if (!project) {
|
|
14482
14575
|
log5.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
14483
14576
|
return;
|
|
@@ -14524,11 +14617,11 @@ var Notifier = class {
|
|
|
14524
14617
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
14525
14618
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
14526
14619
|
if (insightEvents.length === 0) return;
|
|
14527
|
-
const notifs = this.db.select().from(notifications).where(
|
|
14620
|
+
const notifs = this.db.select().from(notifications).where(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14528
14621
|
if (notifs.length === 0) return;
|
|
14529
|
-
const run = this.db.select().from(runs).where(
|
|
14622
|
+
const run = this.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
14530
14623
|
if (!run) return;
|
|
14531
|
-
const project = this.db.select().from(projects).where(
|
|
14624
|
+
const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
14532
14625
|
if (!project) return;
|
|
14533
14626
|
for (const notif of notifs) {
|
|
14534
14627
|
const config = parseJsonColumn(notif.config, { url: "", events: [] });
|
|
@@ -14560,8 +14653,8 @@ var Notifier = class {
|
|
|
14560
14653
|
computeTransitions(runId, projectId) {
|
|
14561
14654
|
const recentRuns = this.db.select().from(runs).where(
|
|
14562
14655
|
and10(
|
|
14563
|
-
|
|
14564
|
-
or2(
|
|
14656
|
+
eq22(runs.projectId, projectId),
|
|
14657
|
+
or2(eq22(runs.status, "completed"), eq22(runs.status, "partial"))
|
|
14565
14658
|
)
|
|
14566
14659
|
).orderBy(desc8(runs.createdAt)).limit(2).all();
|
|
14567
14660
|
if (recentRuns.length < 2) return [];
|
|
@@ -14573,12 +14666,12 @@ var Notifier = class {
|
|
|
14573
14666
|
keyword: keywords.keyword,
|
|
14574
14667
|
provider: querySnapshots.provider,
|
|
14575
14668
|
citationState: querySnapshots.citationState
|
|
14576
|
-
}).from(querySnapshots).leftJoin(keywords,
|
|
14669
|
+
}).from(querySnapshots).leftJoin(keywords, eq22(querySnapshots.keywordId, keywords.id)).where(eq22(querySnapshots.runId, currentRunId)).all();
|
|
14577
14670
|
const previousSnapshots = this.db.select({
|
|
14578
14671
|
keywordId: querySnapshots.keywordId,
|
|
14579
14672
|
provider: querySnapshots.provider,
|
|
14580
14673
|
citationState: querySnapshots.citationState
|
|
14581
|
-
}).from(querySnapshots).where(
|
|
14674
|
+
}).from(querySnapshots).where(eq22(querySnapshots.runId, previousRunId)).all();
|
|
14582
14675
|
const prevMap = /* @__PURE__ */ new Map();
|
|
14583
14676
|
for (const s of previousSnapshots) {
|
|
14584
14677
|
prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
|
|
@@ -14636,7 +14729,7 @@ var Notifier = class {
|
|
|
14636
14729
|
}
|
|
14637
14730
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
14638
14731
|
this.db.insert(auditLog).values({
|
|
14639
|
-
id:
|
|
14732
|
+
id: crypto21.randomUUID(),
|
|
14640
14733
|
projectId,
|
|
14641
14734
|
actor: "scheduler",
|
|
14642
14735
|
action: `notification.${status}`,
|
|
@@ -14651,19 +14744,23 @@ var Notifier = class {
|
|
|
14651
14744
|
// src/run-coordinator.ts
|
|
14652
14745
|
var log6 = createLogger("RunCoordinator");
|
|
14653
14746
|
var RunCoordinator = class {
|
|
14654
|
-
constructor(notifier, intelligenceService, onInsightsGenerated) {
|
|
14747
|
+
constructor(notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
|
|
14655
14748
|
this.notifier = notifier;
|
|
14656
14749
|
this.intelligenceService = intelligenceService;
|
|
14657
14750
|
this.onInsightsGenerated = onInsightsGenerated;
|
|
14751
|
+
this.onAeroEvent = onAeroEvent;
|
|
14658
14752
|
}
|
|
14659
14753
|
async onRunCompleted(runId, projectId) {
|
|
14754
|
+
let insightCount = 0;
|
|
14755
|
+
let criticalOrHigh = 0;
|
|
14660
14756
|
try {
|
|
14661
14757
|
const result = this.intelligenceService.analyzeAndPersist(runId, projectId);
|
|
14662
|
-
if (result
|
|
14663
|
-
|
|
14758
|
+
if (result) {
|
|
14759
|
+
insightCount = result.insights.length;
|
|
14760
|
+
criticalOrHigh = result.insights.filter(
|
|
14664
14761
|
(i) => i.severity === "critical" || i.severity === "high"
|
|
14665
|
-
);
|
|
14666
|
-
if (
|
|
14762
|
+
).length;
|
|
14763
|
+
if (this.onInsightsGenerated && criticalOrHigh > 0) {
|
|
14667
14764
|
try {
|
|
14668
14765
|
await this.onInsightsGenerated(runId, projectId, result);
|
|
14669
14766
|
} catch (err) {
|
|
@@ -14679,147 +14776,1691 @@ var RunCoordinator = class {
|
|
|
14679
14776
|
} catch (err) {
|
|
14680
14777
|
log6.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
14681
14778
|
}
|
|
14779
|
+
if (this.onAeroEvent) {
|
|
14780
|
+
try {
|
|
14781
|
+
await this.onAeroEvent({ runId, projectId, insightCount, criticalOrHigh });
|
|
14782
|
+
} catch (err) {
|
|
14783
|
+
log6.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
|
|
14784
|
+
}
|
|
14785
|
+
}
|
|
14682
14786
|
}
|
|
14683
14787
|
};
|
|
14684
14788
|
|
|
14685
|
-
// src/
|
|
14686
|
-
import
|
|
14789
|
+
// src/agent/session-registry.ts
|
|
14790
|
+
import crypto22 from "crypto";
|
|
14791
|
+
import { eq as eq23 } from "drizzle-orm";
|
|
14687
14792
|
|
|
14688
|
-
// src/
|
|
14689
|
-
import
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14793
|
+
// src/agent/session.ts
|
|
14794
|
+
import fs7 from "fs";
|
|
14795
|
+
import path8 from "path";
|
|
14796
|
+
import { Agent } from "@mariozechner/pi-agent-core";
|
|
14797
|
+
import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
|
|
14798
|
+
|
|
14799
|
+
// src/agent/providers.ts
|
|
14800
|
+
import { getEnvApiKey, getModel } from "@mariozechner/pi-ai";
|
|
14801
|
+
var AGENT_PROVIDERS = {
|
|
14802
|
+
[AgentProviderIds.claude]: {
|
|
14803
|
+
piAiProvider: "anthropic",
|
|
14804
|
+
label: "Anthropic (Claude)",
|
|
14805
|
+
defaultModel: "claude-opus-4-7",
|
|
14806
|
+
autoDetectPriority: 0
|
|
14807
|
+
},
|
|
14808
|
+
[AgentProviderIds.openai]: {
|
|
14809
|
+
piAiProvider: "openai",
|
|
14810
|
+
label: "OpenAI",
|
|
14811
|
+
defaultModel: "gpt-5.1",
|
|
14812
|
+
autoDetectPriority: 1
|
|
14813
|
+
},
|
|
14814
|
+
[AgentProviderIds.gemini]: {
|
|
14815
|
+
piAiProvider: "google",
|
|
14816
|
+
label: "Google (Gemini)",
|
|
14817
|
+
defaultModel: "gemini-2.5-pro",
|
|
14818
|
+
autoDetectPriority: 2
|
|
14819
|
+
},
|
|
14820
|
+
[AgentProviderIds.zai]: {
|
|
14821
|
+
piAiProvider: "zai",
|
|
14822
|
+
label: "Z.ai (GLM)",
|
|
14823
|
+
defaultModel: "glm-5.1",
|
|
14824
|
+
autoDetectPriority: 3
|
|
14701
14825
|
}
|
|
14702
|
-
|
|
14826
|
+
};
|
|
14827
|
+
function agentProvidersByPriority() {
|
|
14828
|
+
return Object.keys(AGENT_PROVIDERS).slice().sort((a, b) => AGENT_PROVIDERS[a].autoDetectPriority - AGENT_PROVIDERS[b].autoDetectPriority);
|
|
14703
14829
|
}
|
|
14704
|
-
function
|
|
14705
|
-
return
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
Accept: "text/html"
|
|
14722
|
-
}
|
|
14723
|
-
},
|
|
14724
|
-
(res) => {
|
|
14725
|
-
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
14726
|
-
if (res.statusCode >= 300 && res.statusCode < 400) {
|
|
14727
|
-
const location = res.headers.location ?? "";
|
|
14728
|
-
res.resume();
|
|
14729
|
-
resolve(`REDIRECT:${location}`);
|
|
14730
|
-
return;
|
|
14731
|
-
}
|
|
14732
|
-
res.resume();
|
|
14733
|
-
resolve("");
|
|
14734
|
-
return;
|
|
14735
|
-
}
|
|
14736
|
-
const contentType = res.headers["content-type"] ?? "";
|
|
14737
|
-
if (!contentType.includes("text/html")) {
|
|
14738
|
-
res.resume();
|
|
14739
|
-
resolve("");
|
|
14740
|
-
return;
|
|
14741
|
-
}
|
|
14742
|
-
const chunks = [];
|
|
14743
|
-
let totalBytes = 0;
|
|
14744
|
-
res.on("data", (chunk) => {
|
|
14745
|
-
totalBytes += chunk.length;
|
|
14746
|
-
if (totalBytes <= MAX_BODY_BYTES) {
|
|
14747
|
-
chunks.push(chunk);
|
|
14748
|
-
}
|
|
14749
|
-
});
|
|
14750
|
-
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
14751
|
-
res.on("error", () => resolve(""));
|
|
14752
|
-
}
|
|
14830
|
+
function listAgentProviders() {
|
|
14831
|
+
return AGENT_PROVIDER_IDS;
|
|
14832
|
+
}
|
|
14833
|
+
function getAgentProvider(name) {
|
|
14834
|
+
return AGENT_PROVIDERS[name];
|
|
14835
|
+
}
|
|
14836
|
+
function coerceAgentProvider(value) {
|
|
14837
|
+
if (!value) return void 0;
|
|
14838
|
+
return isAgentProviderId(value) ? value : void 0;
|
|
14839
|
+
}
|
|
14840
|
+
function resolveModelForProvider(provider, modelId) {
|
|
14841
|
+
const entry = AGENT_PROVIDERS[provider];
|
|
14842
|
+
const id = modelId ?? entry.defaultModel;
|
|
14843
|
+
const model = getModel(entry.piAiProvider, id);
|
|
14844
|
+
if (!model) {
|
|
14845
|
+
throw new Error(
|
|
14846
|
+
`Model '${id}' not found for pi-ai provider '${entry.piAiProvider}'. Verify AGENT_PROVIDERS[${provider}].defaultModel against the installed @mariozechner/pi-ai catalog.`
|
|
14753
14847
|
);
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
req.end();
|
|
14757
|
-
});
|
|
14848
|
+
}
|
|
14849
|
+
return model;
|
|
14758
14850
|
}
|
|
14759
|
-
|
|
14760
|
-
const
|
|
14761
|
-
|
|
14762
|
-
const targetCheck = await resolveWebhookTarget(url);
|
|
14763
|
-
if (!targetCheck.ok) return "";
|
|
14764
|
-
try {
|
|
14765
|
-
const result = await fetchWithPinnedAddress(targetCheck.target);
|
|
14766
|
-
if (result.startsWith("REDIRECT:")) {
|
|
14767
|
-
const location = result.slice("REDIRECT:".length);
|
|
14768
|
-
if (!location) return "";
|
|
14769
|
-
const redirectUrl = new URL(location, url).href;
|
|
14770
|
-
const redirectCheck = await resolveWebhookTarget(redirectUrl);
|
|
14771
|
-
if (!redirectCheck.ok) return "";
|
|
14772
|
-
const redirectResult = await fetchWithPinnedAddress(redirectCheck.target);
|
|
14773
|
-
if (redirectResult.startsWith("REDIRECT:")) return "";
|
|
14774
|
-
return stripHtml2(redirectResult);
|
|
14775
|
-
}
|
|
14776
|
-
return stripHtml2(result);
|
|
14777
|
-
} catch {
|
|
14778
|
-
return "";
|
|
14851
|
+
function validateAgentProviderRegistry() {
|
|
14852
|
+
for (const provider of listAgentProviders()) {
|
|
14853
|
+
resolveModelForProvider(provider);
|
|
14779
14854
|
}
|
|
14780
14855
|
}
|
|
14781
|
-
function
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14856
|
+
function resolveApiKeyFor(providerOrPiAi, config) {
|
|
14857
|
+
return resolveApiKeySource(providerOrPiAi, config)?.key;
|
|
14858
|
+
}
|
|
14859
|
+
function resolveApiKeySource(providerOrPiAi, config) {
|
|
14860
|
+
const id = resolveAgentId(providerOrPiAi);
|
|
14861
|
+
if (!id) return void 0;
|
|
14862
|
+
const entry = AGENT_PROVIDERS[id];
|
|
14863
|
+
const fromConfig = config.providers?.[id]?.apiKey;
|
|
14864
|
+
if (fromConfig) return { key: fromConfig, source: "config" };
|
|
14865
|
+
const fromEnv = getEnvApiKey(entry.piAiProvider);
|
|
14866
|
+
if (fromEnv) return { key: fromEnv, source: "env" };
|
|
14867
|
+
return void 0;
|
|
14868
|
+
}
|
|
14869
|
+
function resolveAgentId(providerOrPiAi) {
|
|
14870
|
+
if (isAgentProviderId(providerOrPiAi)) return providerOrPiAi;
|
|
14871
|
+
for (const id of AGENT_PROVIDER_IDS) {
|
|
14872
|
+
if (AGENT_PROVIDERS[id].piAiProvider === providerOrPiAi) return id;
|
|
14795
14873
|
}
|
|
14796
|
-
return
|
|
14874
|
+
return void 0;
|
|
14875
|
+
}
|
|
14876
|
+
function buildAgentProvidersResponse(config) {
|
|
14877
|
+
const providers = listAgentProviders().map((id) => {
|
|
14878
|
+
const entry = AGENT_PROVIDERS[id];
|
|
14879
|
+
const source = resolveApiKeySource(id, config);
|
|
14880
|
+
return {
|
|
14881
|
+
id,
|
|
14882
|
+
label: entry.label,
|
|
14883
|
+
defaultModel: entry.defaultModel,
|
|
14884
|
+
configured: source !== void 0,
|
|
14885
|
+
keySource: source?.source ?? null
|
|
14886
|
+
};
|
|
14887
|
+
});
|
|
14888
|
+
const firstConfigured = agentProvidersByPriority().find((p) => resolveApiKeySource(p, config));
|
|
14889
|
+
return {
|
|
14890
|
+
providers,
|
|
14891
|
+
defaultProvider: firstConfigured ?? null
|
|
14892
|
+
};
|
|
14797
14893
|
}
|
|
14798
14894
|
|
|
14799
|
-
// src/
|
|
14800
|
-
|
|
14801
|
-
|
|
14895
|
+
// src/agent/skill-paths.ts
|
|
14896
|
+
import fs5 from "fs";
|
|
14897
|
+
import path6 from "path";
|
|
14898
|
+
import { fileURLToPath } from "url";
|
|
14899
|
+
function resolveAeroSkillDir(pkgDir) {
|
|
14900
|
+
const here = pkgDir ?? path6.dirname(fileURLToPath(import.meta.url));
|
|
14901
|
+
const candidates = [
|
|
14902
|
+
path6.join(here, "../assets/agent-workspace/skills/aero"),
|
|
14903
|
+
path6.join(here, "../../assets/agent-workspace/skills/aero"),
|
|
14904
|
+
path6.join(here, "../../../../skills/aero")
|
|
14905
|
+
];
|
|
14906
|
+
for (const candidate of candidates) {
|
|
14907
|
+
if (fs5.existsSync(path6.join(candidate, "SKILL.md"))) return candidate;
|
|
14908
|
+
}
|
|
14909
|
+
throw new Error(`Aero skill not found. Searched:
|
|
14910
|
+
${candidates.join("\n ")}`);
|
|
14802
14911
|
}
|
|
14803
14912
|
|
|
14804
|
-
// src/
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
var
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14913
|
+
// src/agent/skill-tools.ts
|
|
14914
|
+
import fs6 from "fs";
|
|
14915
|
+
import path7 from "path";
|
|
14916
|
+
import { Type } from "@sinclair/typebox";
|
|
14917
|
+
var MAX_DOC_CHARS = 2e4;
|
|
14918
|
+
function textResult(details) {
|
|
14919
|
+
return {
|
|
14920
|
+
content: [{ type: "text", text: JSON.stringify(details, null, 2) }],
|
|
14921
|
+
details
|
|
14922
|
+
};
|
|
14923
|
+
}
|
|
14924
|
+
function parseDescription(body) {
|
|
14925
|
+
if (!body.startsWith("---")) return "(no description)";
|
|
14926
|
+
const end = body.indexOf("\n---", 3);
|
|
14927
|
+
if (end === -1) return "(no description)";
|
|
14928
|
+
const block = body.slice(3, end);
|
|
14929
|
+
for (const line of block.split("\n")) {
|
|
14930
|
+
const match = line.match(/^description:\s*(.+)$/);
|
|
14931
|
+
if (match) return match[1].trim().replace(/^["']|["']$/g, "");
|
|
14932
|
+
}
|
|
14933
|
+
return "(no description)";
|
|
14934
|
+
}
|
|
14935
|
+
function scanSkillDocs(skillDir) {
|
|
14936
|
+
const refsDir = path7.join(skillDir ?? resolveAeroSkillDir(), "references");
|
|
14937
|
+
if (!fs6.existsSync(refsDir)) return [];
|
|
14938
|
+
const entries = [];
|
|
14939
|
+
for (const file of fs6.readdirSync(refsDir)) {
|
|
14940
|
+
if (!file.endsWith(".md")) continue;
|
|
14941
|
+
const filePath = path7.join(refsDir, file);
|
|
14942
|
+
const body = fs6.readFileSync(filePath, "utf-8");
|
|
14943
|
+
entries.push({
|
|
14944
|
+
slug: file.replace(/\.md$/, ""),
|
|
14945
|
+
description: parseDescription(body),
|
|
14946
|
+
bytes: Buffer.byteLength(body, "utf-8")
|
|
14947
|
+
});
|
|
14812
14948
|
}
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
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(/&/g, "&");
|
|
16428
|
+
text = text.replace(/</g, "<");
|
|
16429
|
+
text = text.replace(/>/g, ">");
|
|
16430
|
+
text = text.replace(/"/g, '"');
|
|
16431
|
+
text = text.replace(/'/g, "'");
|
|
16432
|
+
text = text.replace(/ /g, " ");
|
|
16433
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
16434
|
+
if (text.length > MAX_TEXT_LENGTH) {
|
|
16435
|
+
text = text.slice(0, MAX_TEXT_LENGTH);
|
|
16436
|
+
}
|
|
16437
|
+
return text;
|
|
16438
|
+
}
|
|
16439
|
+
|
|
16440
|
+
// src/snapshot-format.ts
|
|
16441
|
+
function formatAuditFactorScore(factor) {
|
|
16442
|
+
return `${factor.score}/100 (${factor.weight}% weight)`;
|
|
16443
|
+
}
|
|
16444
|
+
|
|
16445
|
+
// src/snapshot-service.ts
|
|
16446
|
+
var log8 = createLogger("Snapshot");
|
|
16447
|
+
var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
|
|
16448
|
+
var SNAPSHOT_QUERY_COUNT = 6;
|
|
16449
|
+
var ProviderExecutionGate2 = class {
|
|
16450
|
+
constructor(maxConcurrency, maxPerMinute) {
|
|
16451
|
+
this.maxConcurrency = maxConcurrency;
|
|
16452
|
+
this.maxPerMinute = maxPerMinute;
|
|
16453
|
+
}
|
|
16454
|
+
window = [];
|
|
16455
|
+
waiters = [];
|
|
16456
|
+
rateLimitChain = Promise.resolve();
|
|
16457
|
+
inFlight = 0;
|
|
16458
|
+
async run(task) {
|
|
16459
|
+
await this.acquire();
|
|
16460
|
+
try {
|
|
16461
|
+
await this.waitForRateLimit();
|
|
16462
|
+
return await task();
|
|
16463
|
+
} finally {
|
|
14823
16464
|
this.release();
|
|
14824
16465
|
}
|
|
14825
16466
|
}
|
|
@@ -14945,7 +16586,7 @@ var SnapshotService = class {
|
|
|
14945
16586
|
return mapAuditReport(report);
|
|
14946
16587
|
} catch (err) {
|
|
14947
16588
|
const message = err instanceof Error ? err.message : String(err);
|
|
14948
|
-
|
|
16589
|
+
log8.warn("audit.failed", { homepageUrl, error: message });
|
|
14949
16590
|
return {
|
|
14950
16591
|
url: homepageUrl,
|
|
14951
16592
|
finalUrl: homepageUrl,
|
|
@@ -14975,7 +16616,7 @@ var SnapshotService = class {
|
|
|
14975
16616
|
phrases: parsedPhrases
|
|
14976
16617
|
};
|
|
14977
16618
|
} catch (err) {
|
|
14978
|
-
|
|
16619
|
+
log8.warn("profile.generation-failed", {
|
|
14979
16620
|
domain: ctx.domain,
|
|
14980
16621
|
provider: ctx.analysisProvider.adapter.name,
|
|
14981
16622
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -15117,7 +16758,7 @@ var SnapshotService = class {
|
|
|
15117
16758
|
recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
|
|
15118
16759
|
};
|
|
15119
16760
|
} catch (err) {
|
|
15120
|
-
|
|
16761
|
+
log8.warn("response.analysis-failed", {
|
|
15121
16762
|
provider: ctx.analysisProvider.adapter.name,
|
|
15122
16763
|
error: err instanceof Error ? err.message : String(err)
|
|
15123
16764
|
});
|
|
@@ -15399,499 +17040,6 @@ function clipText(value, length) {
|
|
|
15399
17040
|
return `${value.slice(0, length - 3)}...`;
|
|
15400
17041
|
}
|
|
15401
17042
|
|
|
15402
|
-
// src/agent-manager.ts
|
|
15403
|
-
import { execFileSync, spawn } from "child_process";
|
|
15404
|
-
import fs5 from "fs";
|
|
15405
|
-
import path6 from "path";
|
|
15406
|
-
var log8 = createLogger("AgentManager");
|
|
15407
|
-
var PROCESS_MARKER = "canonry-openclaw-gateway";
|
|
15408
|
-
var AgentManager = class {
|
|
15409
|
-
constructor(config, stateDir) {
|
|
15410
|
-
this.config = config;
|
|
15411
|
-
this.stateDir = stateDir;
|
|
15412
|
-
this.processJsonPath = path6.join(stateDir, "process.json");
|
|
15413
|
-
}
|
|
15414
|
-
processJsonPath;
|
|
15415
|
-
/**
|
|
15416
|
-
* Check if the gateway process is running.
|
|
15417
|
-
* Cleans up stale process.json if the process is dead or belongs to a
|
|
15418
|
-
* different process (PID reuse).
|
|
15419
|
-
*/
|
|
15420
|
-
status() {
|
|
15421
|
-
const info = this.readProcessInfo();
|
|
15422
|
-
if (!info) {
|
|
15423
|
-
return { state: "stopped" };
|
|
15424
|
-
}
|
|
15425
|
-
if (info.marker !== PROCESS_MARKER) {
|
|
15426
|
-
this.removeProcessJson();
|
|
15427
|
-
return { state: "stopped" };
|
|
15428
|
-
}
|
|
15429
|
-
if (isProcessAlive(info.pid) && this.verifyProcessIdentity(info.pid)) {
|
|
15430
|
-
return {
|
|
15431
|
-
state: "running",
|
|
15432
|
-
pid: info.pid,
|
|
15433
|
-
port: info.gatewayPort,
|
|
15434
|
-
startedAt: info.startedAt
|
|
15435
|
-
};
|
|
15436
|
-
}
|
|
15437
|
-
this.removeProcessJson();
|
|
15438
|
-
return { state: "stopped" };
|
|
15439
|
-
}
|
|
15440
|
-
/**
|
|
15441
|
-
* Start the OpenClaw gateway as a detached background process.
|
|
15442
|
-
* Idempotent — no-op if already running.
|
|
15443
|
-
* Waits briefly for the process to confirm it hasn't crashed on startup.
|
|
15444
|
-
*/
|
|
15445
|
-
async start() {
|
|
15446
|
-
const currentStatus = this.status();
|
|
15447
|
-
if (currentStatus.state === "running") {
|
|
15448
|
-
log8.info("already.running", { pid: currentStatus.pid });
|
|
15449
|
-
return;
|
|
15450
|
-
}
|
|
15451
|
-
const binary = this.config.binary ?? "openclaw";
|
|
15452
|
-
const profile = this.config.profile ?? "aero";
|
|
15453
|
-
const port = this.config.gatewayPort ?? 3579;
|
|
15454
|
-
if (!fs5.existsSync(this.stateDir)) {
|
|
15455
|
-
fs5.mkdirSync(this.stateDir, { recursive: true });
|
|
15456
|
-
}
|
|
15457
|
-
const logFile = path6.join(this.stateDir, "gateway.log");
|
|
15458
|
-
const logFd = fs5.openSync(logFile, "a");
|
|
15459
|
-
const dotEnv = this.loadDotEnv();
|
|
15460
|
-
const child = spawn(binary, ["--profile", profile, "gateway"], {
|
|
15461
|
-
detached: true,
|
|
15462
|
-
stdio: ["ignore", logFd, logFd],
|
|
15463
|
-
env: {
|
|
15464
|
-
...process.env,
|
|
15465
|
-
...dotEnv,
|
|
15466
|
-
OPENCLAW_PROFILE: profile,
|
|
15467
|
-
OPENCLAW_GATEWAY_PORT: String(port),
|
|
15468
|
-
OPENCLAW_STATE_DIR: this.stateDir
|
|
15469
|
-
}
|
|
15470
|
-
});
|
|
15471
|
-
const startupResult = await new Promise((resolve) => {
|
|
15472
|
-
let settled = false;
|
|
15473
|
-
const settle = (r) => {
|
|
15474
|
-
if (settled) return;
|
|
15475
|
-
settled = true;
|
|
15476
|
-
resolve(r);
|
|
15477
|
-
};
|
|
15478
|
-
child.on("error", (err) => settle({ error: err }));
|
|
15479
|
-
child.on("exit", (code) => settle({ exitCode: code }));
|
|
15480
|
-
setTimeout(() => settle({}), 500);
|
|
15481
|
-
});
|
|
15482
|
-
child.unref();
|
|
15483
|
-
fs5.closeSync(logFd);
|
|
15484
|
-
if (startupResult.error) {
|
|
15485
|
-
throw new Error(`Failed to start OpenClaw gateway: ${startupResult.error.message}`);
|
|
15486
|
-
}
|
|
15487
|
-
if (startupResult.exitCode != null) {
|
|
15488
|
-
throw new Error(`OpenClaw gateway exited immediately (code ${startupResult.exitCode}). Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
|
|
15489
|
-
}
|
|
15490
|
-
if (child.pid == null) {
|
|
15491
|
-
throw new Error("Failed to start OpenClaw gateway: no PID returned by spawn");
|
|
15492
|
-
}
|
|
15493
|
-
if (!isProcessAlive(child.pid)) {
|
|
15494
|
-
throw new Error(`OpenClaw gateway exited immediately after spawn. Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
|
|
15495
|
-
}
|
|
15496
|
-
const processInfo = {
|
|
15497
|
-
pid: child.pid,
|
|
15498
|
-
gatewayPort: port,
|
|
15499
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15500
|
-
marker: PROCESS_MARKER
|
|
15501
|
-
};
|
|
15502
|
-
fs5.writeFileSync(this.processJsonPath, JSON.stringify(processInfo, null, 2), "utf-8");
|
|
15503
|
-
log8.info("started", { pid: child.pid, port });
|
|
15504
|
-
}
|
|
15505
|
-
/**
|
|
15506
|
-
* Stop the gateway process.
|
|
15507
|
-
* Uses DenchClaw escalation: SIGTERM → 800ms poll → SIGKILL.
|
|
15508
|
-
* Idempotent — no-op if already stopped.
|
|
15509
|
-
*/
|
|
15510
|
-
async stop() {
|
|
15511
|
-
const info = this.readProcessInfo();
|
|
15512
|
-
if (!info) return;
|
|
15513
|
-
if (isProcessAlive(info.pid) && info.marker === PROCESS_MARKER && this.verifyProcessIdentity(info.pid)) {
|
|
15514
|
-
await terminateWithEscalation(info.pid);
|
|
15515
|
-
}
|
|
15516
|
-
this.removeProcessJson();
|
|
15517
|
-
log8.info("stopped", { pid: info.pid });
|
|
15518
|
-
}
|
|
15519
|
-
/**
|
|
15520
|
-
* Stop the gateway, wipe the workspace directory, and prepare for re-seeding.
|
|
15521
|
-
*/
|
|
15522
|
-
async reset() {
|
|
15523
|
-
await this.stop();
|
|
15524
|
-
const workspaceDir = path6.join(this.stateDir, "workspace");
|
|
15525
|
-
if (fs5.existsSync(workspaceDir)) {
|
|
15526
|
-
fs5.rmSync(workspaceDir, { recursive: true, force: true });
|
|
15527
|
-
log8.info("workspace.wiped", { dir: workspaceDir });
|
|
15528
|
-
}
|
|
15529
|
-
}
|
|
15530
|
-
/**
|
|
15531
|
-
* Verify that the PID actually belongs to an openclaw process by checking
|
|
15532
|
-
* the full command line. Requires "openclaw" in the args to avoid matching
|
|
15533
|
-
* unrelated Node processes after PID reuse.
|
|
15534
|
-
*/
|
|
15535
|
-
verifyProcessIdentity(pid) {
|
|
15536
|
-
try {
|
|
15537
|
-
if (process.platform === "darwin") {
|
|
15538
|
-
const out = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
15539
|
-
encoding: "utf-8",
|
|
15540
|
-
timeout: 2e3
|
|
15541
|
-
}).trim();
|
|
15542
|
-
return out.includes("openclaw");
|
|
15543
|
-
}
|
|
15544
|
-
if (process.platform === "linux") {
|
|
15545
|
-
const cmdline = fs5.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
15546
|
-
return cmdline.includes("openclaw");
|
|
15547
|
-
}
|
|
15548
|
-
return true;
|
|
15549
|
-
} catch {
|
|
15550
|
-
return false;
|
|
15551
|
-
}
|
|
15552
|
-
}
|
|
15553
|
-
readProcessInfo() {
|
|
15554
|
-
if (!fs5.existsSync(this.processJsonPath)) return null;
|
|
15555
|
-
try {
|
|
15556
|
-
return JSON.parse(fs5.readFileSync(this.processJsonPath, "utf-8"));
|
|
15557
|
-
} catch {
|
|
15558
|
-
return null;
|
|
15559
|
-
}
|
|
15560
|
-
}
|
|
15561
|
-
removeProcessJson() {
|
|
15562
|
-
try {
|
|
15563
|
-
fs5.unlinkSync(this.processJsonPath);
|
|
15564
|
-
} catch {
|
|
15565
|
-
}
|
|
15566
|
-
}
|
|
15567
|
-
/** Parse a simple KEY=value dotenv file from the state dir. */
|
|
15568
|
-
loadDotEnv() {
|
|
15569
|
-
const envFile = path6.join(this.stateDir, ".env");
|
|
15570
|
-
if (!fs5.existsSync(envFile)) return {};
|
|
15571
|
-
const result = {};
|
|
15572
|
-
for (const line of fs5.readFileSync(envFile, "utf-8").split("\n")) {
|
|
15573
|
-
const trimmed = line.trim();
|
|
15574
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
15575
|
-
const eq25 = trimmed.indexOf("=");
|
|
15576
|
-
if (eq25 < 1) continue;
|
|
15577
|
-
result[trimmed.slice(0, eq25)] = trimmed.slice(eq25 + 1);
|
|
15578
|
-
}
|
|
15579
|
-
return result;
|
|
15580
|
-
}
|
|
15581
|
-
};
|
|
15582
|
-
function isProcessAlive(pid) {
|
|
15583
|
-
try {
|
|
15584
|
-
process.kill(pid, 0);
|
|
15585
|
-
return true;
|
|
15586
|
-
} catch (err) {
|
|
15587
|
-
if (err.code === "EPERM") return true;
|
|
15588
|
-
return false;
|
|
15589
|
-
}
|
|
15590
|
-
}
|
|
15591
|
-
async function terminateWithEscalation(pid) {
|
|
15592
|
-
try {
|
|
15593
|
-
process.kill(pid, "SIGTERM");
|
|
15594
|
-
} catch {
|
|
15595
|
-
return;
|
|
15596
|
-
}
|
|
15597
|
-
const deadline = Date.now() + 800;
|
|
15598
|
-
while (Date.now() < deadline) {
|
|
15599
|
-
if (!isProcessAlive(pid)) return;
|
|
15600
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
15601
|
-
}
|
|
15602
|
-
try {
|
|
15603
|
-
process.kill(pid, "SIGKILL");
|
|
15604
|
-
} catch {
|
|
15605
|
-
}
|
|
15606
|
-
}
|
|
15607
|
-
|
|
15608
|
-
// src/agent-bootstrap.ts
|
|
15609
|
-
import { execFileSync as execFileSync2, execSync } from "child_process";
|
|
15610
|
-
import fs6 from "fs";
|
|
15611
|
-
import os5 from "os";
|
|
15612
|
-
import path7 from "path";
|
|
15613
|
-
import { fileURLToPath } from "url";
|
|
15614
|
-
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
15615
|
-
var OPENCLAW_VERSION = "2026.4.14";
|
|
15616
|
-
var OPENCLAW_PACKAGE_SPEC = `openclaw@${OPENCLAW_VERSION}`;
|
|
15617
|
-
var MIN_NODE_VERSION = "22.14.0";
|
|
15618
|
-
var cachedResult = null;
|
|
15619
|
-
var cachedAt = 0;
|
|
15620
|
-
function getAeroStateDir(profile = "aero") {
|
|
15621
|
-
return path7.join(os5.homedir(), `.openclaw-${profile}`);
|
|
15622
|
-
}
|
|
15623
|
-
async function detectOpenClaw(config) {
|
|
15624
|
-
if (cachedResult && Date.now() - cachedAt < CACHE_TTL_MS) {
|
|
15625
|
-
return cachedResult;
|
|
15626
|
-
}
|
|
15627
|
-
let result;
|
|
15628
|
-
if (config?.binary) {
|
|
15629
|
-
const version = probeVersion(config.binary);
|
|
15630
|
-
if (version) {
|
|
15631
|
-
result = { found: true, path: config.binary, version };
|
|
15632
|
-
cachedResult = result;
|
|
15633
|
-
cachedAt = Date.now();
|
|
15634
|
-
return result;
|
|
15635
|
-
}
|
|
15636
|
-
}
|
|
15637
|
-
const binaryPath = findInPath();
|
|
15638
|
-
if (binaryPath) {
|
|
15639
|
-
const version = probeVersion(binaryPath);
|
|
15640
|
-
if (version) {
|
|
15641
|
-
result = { found: true, path: binaryPath, version };
|
|
15642
|
-
cachedResult = result;
|
|
15643
|
-
cachedAt = Date.now();
|
|
15644
|
-
return result;
|
|
15645
|
-
}
|
|
15646
|
-
}
|
|
15647
|
-
result = { found: false };
|
|
15648
|
-
cachedResult = result;
|
|
15649
|
-
cachedAt = Date.now();
|
|
15650
|
-
return result;
|
|
15651
|
-
}
|
|
15652
|
-
detectOpenClaw.resetCache = () => {
|
|
15653
|
-
cachedResult = null;
|
|
15654
|
-
cachedAt = 0;
|
|
15655
|
-
};
|
|
15656
|
-
function probeVersion(binaryPath) {
|
|
15657
|
-
try {
|
|
15658
|
-
const output = execFileSync2(binaryPath, ["--version"], {
|
|
15659
|
-
timeout: 5e3,
|
|
15660
|
-
encoding: "utf-8"
|
|
15661
|
-
});
|
|
15662
|
-
const match = output.toString().trim().match(/(\d+\.\d+\.\d+)/);
|
|
15663
|
-
return match ? match[1] : output.toString().trim();
|
|
15664
|
-
} catch {
|
|
15665
|
-
return null;
|
|
15666
|
-
}
|
|
15667
|
-
}
|
|
15668
|
-
function findInPath() {
|
|
15669
|
-
const cmd = process.platform === "win32" ? "where" : "which";
|
|
15670
|
-
try {
|
|
15671
|
-
const output = execFileSync2(cmd, ["openclaw"], {
|
|
15672
|
-
timeout: 5e3,
|
|
15673
|
-
encoding: "utf-8"
|
|
15674
|
-
});
|
|
15675
|
-
return output.toString().trim().split("\n")[0] || null;
|
|
15676
|
-
} catch {
|
|
15677
|
-
return null;
|
|
15678
|
-
}
|
|
15679
|
-
}
|
|
15680
|
-
async function installOpenClaw(opts) {
|
|
15681
|
-
const unsupportedNodeError = getUnsupportedNodeError(opts?.nodeVersion);
|
|
15682
|
-
if (unsupportedNodeError) {
|
|
15683
|
-
return {
|
|
15684
|
-
success: false,
|
|
15685
|
-
error: unsupportedNodeError
|
|
15686
|
-
};
|
|
15687
|
-
}
|
|
15688
|
-
try {
|
|
15689
|
-
execSync(`npm install -g ${OPENCLAW_PACKAGE_SPEC}`, {
|
|
15690
|
-
timeout: 12e4,
|
|
15691
|
-
stdio: opts?.silent ? "pipe" : "inherit"
|
|
15692
|
-
});
|
|
15693
|
-
} catch (err) {
|
|
15694
|
-
return {
|
|
15695
|
-
success: false,
|
|
15696
|
-
error: err instanceof Error ? err.message : String(err)
|
|
15697
|
-
};
|
|
15698
|
-
}
|
|
15699
|
-
detectOpenClaw.resetCache();
|
|
15700
|
-
const detection = await detectOpenClaw();
|
|
15701
|
-
if (!detection.found) {
|
|
15702
|
-
return {
|
|
15703
|
-
success: false,
|
|
15704
|
-
error: `npm install succeeded but the ${OPENCLAW_PACKAGE_SPEC} binary was not found in PATH`
|
|
15705
|
-
};
|
|
15706
|
-
}
|
|
15707
|
-
if (detection.version) {
|
|
15708
|
-
const expectedVersion = parseVersionTuple(OPENCLAW_VERSION);
|
|
15709
|
-
const detectedVersion = parseVersionTuple(detection.version);
|
|
15710
|
-
if (expectedVersion && detectedVersion && compareVersionTuples(detectedVersion, expectedVersion) !== 0) {
|
|
15711
|
-
return {
|
|
15712
|
-
success: false,
|
|
15713
|
-
error: `Installed OpenClaw binary reports version ${detection.version}, but Canonry pinned ${OPENCLAW_VERSION}. A different openclaw binary may be shadowing the npm-installed package in PATH.`
|
|
15714
|
-
};
|
|
15715
|
-
}
|
|
15716
|
-
}
|
|
15717
|
-
return { success: true, detection };
|
|
15718
|
-
}
|
|
15719
|
-
function getUnsupportedNodeError(currentNodeVersionOverride) {
|
|
15720
|
-
const currentNodeVersion = normalizeVersion(currentNodeVersionOverride ?? process.versions.node);
|
|
15721
|
-
const minimumTuple = parseVersionTuple(MIN_NODE_VERSION);
|
|
15722
|
-
const currentTuple = parseVersionTuple(currentNodeVersion);
|
|
15723
|
-
if (!minimumTuple || !currentTuple || compareVersionTuples(currentTuple, minimumTuple) >= 0) {
|
|
15724
|
-
return null;
|
|
15725
|
-
}
|
|
15726
|
-
return `Canonry requires Node.js >=${MIN_NODE_VERSION} and installs pinned OpenClaw ${OPENCLAW_VERSION}, but the current runtime is ${currentNodeVersion}. Upgrade Node.js before running "canonry agent setup".`;
|
|
15727
|
-
}
|
|
15728
|
-
function normalizeVersion(version) {
|
|
15729
|
-
const tuple = parseVersionTuple(version);
|
|
15730
|
-
if (!tuple) {
|
|
15731
|
-
return version.trim().replace(/^v/i, "");
|
|
15732
|
-
}
|
|
15733
|
-
return tuple.join(".");
|
|
15734
|
-
}
|
|
15735
|
-
function parseVersionTuple(version) {
|
|
15736
|
-
const match = version.trim().replace(/^v/i, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
|
|
15737
|
-
if (!match) {
|
|
15738
|
-
return null;
|
|
15739
|
-
}
|
|
15740
|
-
return [
|
|
15741
|
-
Number(match[1]),
|
|
15742
|
-
Number(match[2] ?? 0),
|
|
15743
|
-
Number(match[3] ?? 0)
|
|
15744
|
-
];
|
|
15745
|
-
}
|
|
15746
|
-
function compareVersionTuples(left, right) {
|
|
15747
|
-
for (let index = 0; index < left.length; index++) {
|
|
15748
|
-
const delta = left[index] - right[index];
|
|
15749
|
-
if (delta !== 0) {
|
|
15750
|
-
return delta;
|
|
15751
|
-
}
|
|
15752
|
-
}
|
|
15753
|
-
return 0;
|
|
15754
|
-
}
|
|
15755
|
-
function seedWorkspace(stateDir) {
|
|
15756
|
-
const workspaceDir = path7.join(stateDir, "workspace");
|
|
15757
|
-
fs6.mkdirSync(workspaceDir, { recursive: true });
|
|
15758
|
-
const __dirname = path7.dirname(fileURLToPath(import.meta.url));
|
|
15759
|
-
const assetsDir = path7.join(__dirname, "..", "assets", "agent-workspace");
|
|
15760
|
-
if (!fs6.existsSync(assetsDir)) {
|
|
15761
|
-
return;
|
|
15762
|
-
}
|
|
15763
|
-
copyDirRecursive(assetsDir, workspaceDir);
|
|
15764
|
-
}
|
|
15765
|
-
function initializeOpenClawProfile(binary, profile, workspaceDir) {
|
|
15766
|
-
try {
|
|
15767
|
-
execFileSync2(binary, [
|
|
15768
|
-
"--profile",
|
|
15769
|
-
profile,
|
|
15770
|
-
"onboard",
|
|
15771
|
-
"--non-interactive",
|
|
15772
|
-
"--accept-risk",
|
|
15773
|
-
"--mode",
|
|
15774
|
-
"local",
|
|
15775
|
-
"--workspace",
|
|
15776
|
-
workspaceDir,
|
|
15777
|
-
"--skip-channels",
|
|
15778
|
-
"--skip-skills",
|
|
15779
|
-
"--skip-health",
|
|
15780
|
-
"--no-install-daemon"
|
|
15781
|
-
], { timeout: 3e4, stdio: "pipe" });
|
|
15782
|
-
} catch (err) {
|
|
15783
|
-
const stderr = err instanceof Error && "stderr" in err ? String(err.stderr) : "";
|
|
15784
|
-
if (stderr.toLowerCase().includes("already")) return;
|
|
15785
|
-
throw new CliError({
|
|
15786
|
-
code: "AGENT_PROFILE_INIT_FAILED",
|
|
15787
|
-
message: `Failed to initialize OpenClaw profile: ${stderr || (err instanceof Error ? err.message : String(err))}`,
|
|
15788
|
-
displayMessage: `Failed to initialize OpenClaw profile "${profile}".`
|
|
15789
|
-
});
|
|
15790
|
-
}
|
|
15791
|
-
}
|
|
15792
|
-
function configureOpenClawGateway(binary, profile, gatewayPort) {
|
|
15793
|
-
const entries = [
|
|
15794
|
-
["gateway.mode", "local", false],
|
|
15795
|
-
["gateway.port", String(gatewayPort), true]
|
|
15796
|
-
];
|
|
15797
|
-
for (const [key, value, strict] of entries) {
|
|
15798
|
-
try {
|
|
15799
|
-
const args = ["--profile", profile, "config", "set", key, value];
|
|
15800
|
-
if (strict) args.push("--strict-json");
|
|
15801
|
-
execFileSync2(binary, args, { timeout: 1e4, stdio: "pipe" });
|
|
15802
|
-
} catch (err) {
|
|
15803
|
-
throw new CliError({
|
|
15804
|
-
code: "AGENT_GATEWAY_CONFIG_FAILED",
|
|
15805
|
-
message: `Failed to set ${key}=${value}: ${err instanceof Error ? err.message : String(err)}`,
|
|
15806
|
-
displayMessage: `Failed to configure OpenClaw gateway (${key}).`
|
|
15807
|
-
});
|
|
15808
|
-
}
|
|
15809
|
-
}
|
|
15810
|
-
}
|
|
15811
|
-
function setOpenClawModel(binary, profile, model) {
|
|
15812
|
-
try {
|
|
15813
|
-
execFileSync2(binary, [
|
|
15814
|
-
"--profile",
|
|
15815
|
-
profile,
|
|
15816
|
-
"models",
|
|
15817
|
-
"set",
|
|
15818
|
-
model
|
|
15819
|
-
], { timeout: 1e4, stdio: "pipe" });
|
|
15820
|
-
} catch (err) {
|
|
15821
|
-
throw new CliError({
|
|
15822
|
-
code: "AGENT_MODEL_SET_FAILED",
|
|
15823
|
-
message: `Failed to set agent model to ${model}: ${err instanceof Error ? err.message : String(err)}`,
|
|
15824
|
-
displayMessage: `Failed to set agent model to "${model}".`
|
|
15825
|
-
});
|
|
15826
|
-
}
|
|
15827
|
-
}
|
|
15828
|
-
function providerEnvVar(provider) {
|
|
15829
|
-
const map = {
|
|
15830
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
15831
|
-
openai: "OPENAI_API_KEY",
|
|
15832
|
-
google: "GOOGLE_API_KEY",
|
|
15833
|
-
"google-vertex": "GOOGLE_API_KEY",
|
|
15834
|
-
groq: "GROQ_API_KEY",
|
|
15835
|
-
mistral: "MISTRAL_API_KEY",
|
|
15836
|
-
xai: "XAI_API_KEY",
|
|
15837
|
-
openrouter: "OPENROUTER_API_KEY",
|
|
15838
|
-
cerebras: "CEREBRAS_API_KEY"
|
|
15839
|
-
};
|
|
15840
|
-
return map[provider] ?? `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
15841
|
-
}
|
|
15842
|
-
function writeAgentEnv(stateDir, key, value) {
|
|
15843
|
-
const envFile = path7.join(stateDir, ".env");
|
|
15844
|
-
let lines = [];
|
|
15845
|
-
if (fs6.existsSync(envFile)) {
|
|
15846
|
-
lines = fs6.readFileSync(envFile, "utf-8").split("\n");
|
|
15847
|
-
}
|
|
15848
|
-
const prefix = `${key}=`;
|
|
15849
|
-
const idx = lines.findIndex((l) => l.startsWith(prefix));
|
|
15850
|
-
const entry = `${key}=${value}`;
|
|
15851
|
-
if (idx >= 0) {
|
|
15852
|
-
lines[idx] = entry;
|
|
15853
|
-
} else {
|
|
15854
|
-
lines.push(entry);
|
|
15855
|
-
}
|
|
15856
|
-
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
15857
|
-
fs6.writeFileSync(envFile, lines.join("\n") + "\n", "utf-8");
|
|
15858
|
-
}
|
|
15859
|
-
function resolveAgentCredentials(opts) {
|
|
15860
|
-
const provider = opts.agentProvider ?? "anthropic";
|
|
15861
|
-
if (opts.agentKey) {
|
|
15862
|
-
return { provider, key: opts.agentKey, model: opts.agentModel };
|
|
15863
|
-
}
|
|
15864
|
-
const envVar = providerEnvVar(provider);
|
|
15865
|
-
const envKey = process.env[envVar];
|
|
15866
|
-
if (envKey) {
|
|
15867
|
-
return { provider, key: envKey, model: opts.agentModel };
|
|
15868
|
-
}
|
|
15869
|
-
const genericKey = process.env.CANONRY_AGENT_KEY;
|
|
15870
|
-
if (genericKey) {
|
|
15871
|
-
return { provider, key: genericKey, model: opts.agentModel };
|
|
15872
|
-
}
|
|
15873
|
-
const envFile = path7.join(opts.stateDir, ".env");
|
|
15874
|
-
if (fs6.existsSync(envFile)) {
|
|
15875
|
-
const hasKey = fs6.readFileSync(envFile, "utf-8").split("\n").some((l) => l.includes("_API_KEY="));
|
|
15876
|
-
if (hasKey) {
|
|
15877
|
-
return { provider, key: void 0, model: opts.agentModel };
|
|
15878
|
-
}
|
|
15879
|
-
}
|
|
15880
|
-
return { provider, key: void 0, model: opts.agentModel };
|
|
15881
|
-
}
|
|
15882
|
-
function copyDirRecursive(src, dest) {
|
|
15883
|
-
fs6.mkdirSync(dest, { recursive: true });
|
|
15884
|
-
for (const entry of fs6.readdirSync(src, { withFileTypes: true })) {
|
|
15885
|
-
const srcPath = path7.join(src, entry.name);
|
|
15886
|
-
const destPath = path7.join(dest, entry.name);
|
|
15887
|
-
if (entry.isDirectory()) {
|
|
15888
|
-
copyDirRecursive(srcPath, destPath);
|
|
15889
|
-
} else {
|
|
15890
|
-
fs6.copyFileSync(srcPath, destPath);
|
|
15891
|
-
}
|
|
15892
|
-
}
|
|
15893
|
-
}
|
|
15894
|
-
|
|
15895
17043
|
// src/server.ts
|
|
15896
17044
|
var _require2 = createRequire2(import.meta.url);
|
|
15897
17045
|
var { version: PKG_VERSION } = _require2("../package.json");
|
|
@@ -16073,27 +17221,35 @@ async function createServer(opts) {
|
|
|
16073
17221
|
jobRunner.recoverStaleRuns();
|
|
16074
17222
|
const notifier = new Notifier(opts.db, serverUrl);
|
|
16075
17223
|
const intelligenceService = new IntelligenceService(opts.db);
|
|
17224
|
+
const aeroClient = new ApiClient(opts.config.apiUrl, opts.config.apiKey, { skipProbe: true });
|
|
17225
|
+
const sessionRegistry = new SessionRegistry({
|
|
17226
|
+
db: opts.db,
|
|
17227
|
+
client: aeroClient,
|
|
17228
|
+
config: opts.config
|
|
17229
|
+
});
|
|
16076
17230
|
const runCoordinator = new RunCoordinator(
|
|
16077
17231
|
notifier,
|
|
16078
17232
|
intelligenceService,
|
|
16079
|
-
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result)
|
|
17233
|
+
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
17234
|
+
async ({ runId, projectId, insightCount, criticalOrHigh }) => {
|
|
17235
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq25(projects.id, projectId)).get();
|
|
17236
|
+
if (!project) return;
|
|
17237
|
+
sessionRegistry.queueFollowUp(project.name, {
|
|
17238
|
+
role: "user",
|
|
17239
|
+
content: `[system] Run ${runId} completed for project ${project.name}. ${insightCount} insights generated (${criticalOrHigh} critical/high). Use get_run to inspect the run and get_insights to review new findings. Surface anything notable briefly \u2014 skip chit-chat.`,
|
|
17240
|
+
timestamp: Date.now()
|
|
17241
|
+
});
|
|
17242
|
+
void sessionRegistry.drainNow(project.name);
|
|
17243
|
+
}
|
|
16080
17244
|
);
|
|
16081
17245
|
jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
|
|
16082
17246
|
const snapshotService = new SnapshotService(registry);
|
|
16083
|
-
|
|
16084
|
-
|
|
16085
|
-
|
|
16086
|
-
|
|
16087
|
-
|
|
16088
|
-
|
|
16089
|
-
try {
|
|
16090
|
-
await agentManager.start();
|
|
16091
|
-
agentAutoStarted = true;
|
|
16092
|
-
app.log.info({ pid: agentManager.status().pid }, "Agent gateway started");
|
|
16093
|
-
} catch (err) {
|
|
16094
|
-
app.log.error({ err }, "Failed to auto-start agent gateway");
|
|
16095
|
-
}
|
|
16096
|
-
}
|
|
17247
|
+
const orphanedOpenClawDir = path9.join(os5.homedir(), ".openclaw-aero");
|
|
17248
|
+
if (fs8.existsSync(orphanedOpenClawDir)) {
|
|
17249
|
+
app.log.warn(
|
|
17250
|
+
{ path: orphanedOpenClawDir },
|
|
17251
|
+
"OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
|
|
17252
|
+
);
|
|
16097
17253
|
}
|
|
16098
17254
|
const scheduler = new Scheduler(opts.db, {
|
|
16099
17255
|
onRunCreated: (runId, projectId, providers2, location) => {
|
|
@@ -16216,7 +17372,7 @@ async function createServer(opts) {
|
|
|
16216
17372
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
16217
17373
|
if (opts.config.apiKey) {
|
|
16218
17374
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
16219
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
17375
|
+
const existing = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, keyHash)).get();
|
|
16220
17376
|
if (!existing) {
|
|
16221
17377
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
16222
17378
|
opts.db.insert(apiKeys).values({
|
|
@@ -16268,7 +17424,7 @@ async function createServer(opts) {
|
|
|
16268
17424
|
};
|
|
16269
17425
|
const getDefaultApiKey = () => {
|
|
16270
17426
|
if (!opts.config.apiKey) return void 0;
|
|
16271
|
-
return opts.db.select().from(apiKeys).where(
|
|
17427
|
+
return opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
16272
17428
|
};
|
|
16273
17429
|
const createPasswordSession = (reply) => {
|
|
16274
17430
|
const key = getDefaultApiKey();
|
|
@@ -16325,12 +17481,12 @@ async function createServer(opts) {
|
|
|
16325
17481
|
return reply.send({ authenticated: true });
|
|
16326
17482
|
}
|
|
16327
17483
|
if (apiKey) {
|
|
16328
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
17484
|
+
const key = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
16329
17485
|
if (!key || key.revokedAt) {
|
|
16330
17486
|
const err2 = authInvalid();
|
|
16331
17487
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
16332
17488
|
}
|
|
16333
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
17489
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(apiKeys.id, key.id)).run();
|
|
16334
17490
|
const sessionId = createSession(key.id);
|
|
16335
17491
|
reply.header("set-cookie", serializeSessionCookie({
|
|
16336
17492
|
name: SESSION_COOKIE_NAME,
|
|
@@ -16362,6 +17518,11 @@ async function createServer(opts) {
|
|
|
16362
17518
|
skipAuth: false,
|
|
16363
17519
|
sessionCookieName: SESSION_COOKIE_NAME,
|
|
16364
17520
|
resolveSessionApiKeyId,
|
|
17521
|
+
// Local-only Aero agent routes. Registered here so they inherit api-routes'
|
|
17522
|
+
// auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
|
|
17523
|
+
registerAuthenticatedRoutes: async (scope) => {
|
|
17524
|
+
registerAgentRoutes(scope, { db: opts.db, sessionRegistry });
|
|
17525
|
+
},
|
|
16365
17526
|
getGoogleAuthConfig: () => getGoogleAuthConfig(opts.config),
|
|
16366
17527
|
googleConnectionStore,
|
|
16367
17528
|
googleStateSecret,
|
|
@@ -16394,7 +17555,8 @@ async function createServer(opts) {
|
|
|
16394
17555
|
},
|
|
16395
17556
|
openApiInfo: {
|
|
16396
17557
|
title: "Canonry API",
|
|
16397
|
-
version: PKG_VERSION
|
|
17558
|
+
version: PKG_VERSION,
|
|
17559
|
+
includeCanonryLocal: true
|
|
16398
17560
|
},
|
|
16399
17561
|
providerSummary,
|
|
16400
17562
|
providerAdapters: [...API_ADAPTERS, ...BROWSER_ADAPTERS].map((a) => ({
|
|
@@ -16518,17 +17680,6 @@ async function createServer(opts) {
|
|
|
16518
17680
|
onProjectDeleted: (projectId) => {
|
|
16519
17681
|
scheduler.remove(projectId);
|
|
16520
17682
|
},
|
|
16521
|
-
onProjectUpserted: agentManager && opts.config.agent?.autoStart ? (_projectId, projectName) => {
|
|
16522
|
-
try {
|
|
16523
|
-
const gatewayPort = opts.config.agent?.gatewayPort ?? 3579;
|
|
16524
|
-
const result = attachAgentWebhookDirect(opts.db, _projectId, gatewayPort);
|
|
16525
|
-
if (result === "attached") {
|
|
16526
|
-
app.log.info({ projectName }, "Auto-attached agent webhook");
|
|
16527
|
-
}
|
|
16528
|
-
} catch (err) {
|
|
16529
|
-
app.log.error({ err, projectName }, "Failed to auto-attach agent webhook");
|
|
16530
|
-
}
|
|
16531
|
-
} : void 0,
|
|
16532
17683
|
getTelemetryStatus: () => {
|
|
16533
17684
|
const enabled = isTelemetryEnabled();
|
|
16534
17685
|
return {
|
|
@@ -16613,15 +17764,15 @@ async function createServer(opts) {
|
|
|
16613
17764
|
return snapshotService.createReport(input);
|
|
16614
17765
|
}
|
|
16615
17766
|
});
|
|
16616
|
-
const dirname =
|
|
16617
|
-
const assetsDir =
|
|
16618
|
-
if (
|
|
16619
|
-
const indexPath =
|
|
17767
|
+
const dirname = path9.dirname(fileURLToPath2(import.meta.url));
|
|
17768
|
+
const assetsDir = path9.join(dirname, "..", "assets");
|
|
17769
|
+
if (fs8.existsSync(assetsDir)) {
|
|
17770
|
+
const indexPath = path9.join(assetsDir, "index.html");
|
|
16620
17771
|
const injectConfig = (html) => {
|
|
16621
17772
|
const clientConfig = {};
|
|
16622
17773
|
if (basePath) clientConfig.basePath = basePath;
|
|
16623
17774
|
const configScript = `<script>window.__CANONRY_CONFIG__=${JSON.stringify(clientConfig)}</script>`;
|
|
16624
|
-
const baseTag =
|
|
17775
|
+
const baseTag = `<base href="${basePath ?? "/"}">`;
|
|
16625
17776
|
return html.replace("<head>", `<head>${baseTag}`).replace("</head>", `${configScript}</head>`);
|
|
16626
17777
|
};
|
|
16627
17778
|
const fastifyStatic = await import("@fastify/static");
|
|
@@ -16634,8 +17785,8 @@ async function createServer(opts) {
|
|
|
16634
17785
|
index: false
|
|
16635
17786
|
});
|
|
16636
17787
|
const serveIndex = (_request, reply) => {
|
|
16637
|
-
if (
|
|
16638
|
-
const html =
|
|
17788
|
+
if (fs8.existsSync(indexPath)) {
|
|
17789
|
+
const html = fs8.readFileSync(indexPath, "utf-8");
|
|
16639
17790
|
return reply.type("text/html").send(injectConfig(html));
|
|
16640
17791
|
}
|
|
16641
17792
|
return reply.status(404).send({ error: "Dashboard not built" });
|
|
@@ -16655,8 +17806,8 @@ async function createServer(opts) {
|
|
|
16655
17806
|
if (basePath && !url.startsWith(basePath)) {
|
|
16656
17807
|
return reply.status(404).send({ error: "Not found", path: request.url });
|
|
16657
17808
|
}
|
|
16658
|
-
if (
|
|
16659
|
-
const html =
|
|
17809
|
+
if (fs8.existsSync(indexPath)) {
|
|
17810
|
+
const html = fs8.readFileSync(indexPath, "utf-8");
|
|
16660
17811
|
return reply.type("text/html").send(injectConfig(html));
|
|
16661
17812
|
}
|
|
16662
17813
|
return reply.status(404).send({ error: "Not found" });
|
|
@@ -16675,13 +17826,6 @@ async function createServer(opts) {
|
|
|
16675
17826
|
scheduler.start();
|
|
16676
17827
|
app.addHook("onClose", async () => {
|
|
16677
17828
|
scheduler.stop();
|
|
16678
|
-
if (agentManager && agentAutoStarted) {
|
|
16679
|
-
try {
|
|
16680
|
-
await agentManager.stop();
|
|
16681
|
-
} catch (err) {
|
|
16682
|
-
app.log.error({ err }, "Failed to stop agent gateway");
|
|
16683
|
-
}
|
|
16684
|
-
}
|
|
16685
17829
|
});
|
|
16686
17830
|
return app;
|
|
16687
17831
|
}
|
|
@@ -16743,7 +17887,6 @@ export {
|
|
|
16743
17887
|
isFirstRun,
|
|
16744
17888
|
showFirstRunNotice,
|
|
16745
17889
|
trackEvent,
|
|
16746
|
-
EXIT_USER_ERROR,
|
|
16747
17890
|
EXIT_SYSTEM_ERROR,
|
|
16748
17891
|
CliError,
|
|
16749
17892
|
usageError,
|
|
@@ -16762,21 +17905,10 @@ export {
|
|
|
16762
17905
|
determineCitationState,
|
|
16763
17906
|
computeCompetitorOverlap,
|
|
16764
17907
|
extractRecommendedCompetitors,
|
|
17908
|
+
createApiClient,
|
|
16765
17909
|
setGoogleAuthConfig,
|
|
16766
17910
|
formatAuditFactorScore,
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
attachAgentWebhookDirect,
|
|
16770
|
-
AgentManager,
|
|
16771
|
-
getAeroStateDir,
|
|
16772
|
-
detectOpenClaw,
|
|
16773
|
-
installOpenClaw,
|
|
16774
|
-
seedWorkspace,
|
|
16775
|
-
initializeOpenClawProfile,
|
|
16776
|
-
configureOpenClawGateway,
|
|
16777
|
-
setOpenClawModel,
|
|
16778
|
-
providerEnvVar,
|
|
16779
|
-
writeAgentEnv,
|
|
16780
|
-
resolveAgentCredentials,
|
|
17911
|
+
listAgentProviders,
|
|
17912
|
+
coerceAgentProvider,
|
|
16781
17913
|
createServer
|
|
16782
17914
|
};
|