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