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