@desplega.ai/agent-swarm 1.79.4 → 1.80.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.json +496 -32
- package/package.json +14 -6
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +102 -31
- package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/pricing-normalize.ts +81 -0
- package/src/be/scripts/db.ts +391 -0
- package/src/be/scripts/embeddings.ts +231 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +193 -0
- package/src/be/seed-pricing.ts +293 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +21 -4
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +663 -246
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/context.ts +6 -2
- package/src/http/index.ts +126 -68
- package/src/http/memory.ts +28 -0
- package/src/http/openapi.ts +1 -0
- package/src/http/page-proxy.ts +2 -1
- package/src/http/route-def.ts +1 -0
- package/src/http/schedules.ts +37 -0
- package/src/http/scripts.ts +381 -0
- package/src/http/session-data.ts +74 -23
- package/src/linear/outbound.ts +9 -2
- package/src/otel-impl.ts +200 -0
- package/src/otel.ts +132 -0
- package/src/providers/claude-adapter.ts +52 -6
- package/src/providers/claude-managed-adapter.ts +43 -17
- package/src/providers/claude-managed-pricing.ts +34 -0
- package/src/providers/codex-adapter.ts +38 -27
- package/src/providers/codex-models.ts +22 -3
- package/src/providers/devin-adapter.ts +11 -0
- package/src/providers/opencode-adapter.ts +31 -7
- package/src/providers/pi-mono-adapter.ts +39 -7
- package/src/providers/pricing-sources.md +52 -0
- package/src/providers/swarm-events-shared.ts +8 -4
- package/src/providers/types.ts +33 -10
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +39 -0
- package/src/scripts-runtime/executors/native.ts +229 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -0
- package/src/scripts-runtime/extract-signature.ts +81 -0
- package/src/scripts-runtime/import-allowlist.ts +109 -0
- package/src/scripts-runtime/loader.ts +96 -0
- package/src/scripts-runtime/redacted.ts +48 -0
- package/src/scripts-runtime/sdk-allowlist.ts +29 -0
- package/src/scripts-runtime/stdlib/fetch.ts +46 -0
- package/src/scripts-runtime/stdlib/glob.ts +8 -0
- package/src/scripts-runtime/stdlib/grep.ts +34 -0
- package/src/scripts-runtime/stdlib/index.ts +16 -0
- package/src/scripts-runtime/stdlib/table.ts +17 -0
- package/src/scripts-runtime/swarm-config.ts +35 -0
- package/src/scripts-runtime/swarm-sdk.ts +197 -0
- package/src/scripts-runtime/types/stdlib.d.ts +104 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
- package/src/server.ts +18 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/claude-managed-adapter.test.ts +17 -3
- package/src/tests/claude-managed-setup.test.ts +10 -1
- package/src/tests/codex-adapter.test.ts +20 -19
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/context-snapshot.test.ts +2 -2
- package/src/tests/context-window.test.ts +65 -1
- package/src/tests/devin-adapter.test.ts +2 -0
- package/src/tests/http/context-routes.test.ts +161 -0
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/migration-063-schema-relax.test.ts +109 -0
- package/src/tests/opencode-adapter.test.ts +146 -1
- package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
- package/src/tests/pages-view-count.test.ts +30 -5
- package/src/tests/providers/codex-cost.test.ts +18 -0
- package/src/tests/providers/opencode-cost.test.ts +74 -0
- package/src/tests/providers/pi-cost.test.ts +128 -0
- package/src/tests/redacted.test.ts +29 -0
- package/src/tests/runner-tool-spans.test.ts +268 -0
- package/src/tests/script-executor-conformance.test.ts +142 -0
- package/src/tests/script-executor-registry.test.ts +17 -0
- package/src/tests/scripts-db.test.ts +329 -0
- package/src/tests/scripts-embeddings.test.ts +291 -0
- package/src/tests/scripts-extract-signature.test.ts +47 -0
- package/src/tests/scripts-http.test.ts +350 -0
- package/src/tests/scripts-import-allowlist.test.ts +55 -0
- package/src/tests/scripts-mcp-e2e.test.ts +269 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
- package/src/tests/scripts-runtime.test.ts +289 -0
- package/src/tests/sdk-allowlist.test.ts +59 -0
- package/src/tests/secret-scrubber.test.ts +54 -1
- package/src/tests/session-costs-codex-recompute.test.ts +35 -22
- package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
- package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
- package/src/tests/store-progress-cost.test.ts +6 -1
- package/src/tests/swarm-config.test.ts +38 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/tool-call-progress.test.ts +30 -0
- package/src/tests/workflow-e2e.test.ts +218 -0
- package/src/tests/workflow-executors.test.ts +32 -2
- package/src/tests/workflow-input-redaction.test.ts +232 -0
- package/src/tests/workflow-swarm-script.test.ts +273 -0
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/script-common.ts +88 -0
- package/src/tools/script-delete.ts +35 -0
- package/src/tools/script-query-types.ts +37 -0
- package/src/tools/script-run.ts +43 -0
- package/src/tools/script-search.ts +32 -0
- package/src/tools/script-upsert.ts +43 -0
- package/src/tools/store-progress.ts +16 -60
- package/src/tools/tool-config.ts +7 -0
- package/src/tools/utils.ts +65 -12
- package/src/types.ts +122 -10
- package/src/utils/api-key.ts +28 -0
- package/src/utils/context-window.ts +104 -4
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +29 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- package/src/workflows/resume.ts +43 -5
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type RuntimeFetchOptions = RequestInit & {
|
|
2
|
+
retries?: number;
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export async function runtimeFetch(
|
|
7
|
+
input: string | URL | Request,
|
|
8
|
+
options: RuntimeFetchOptions = {},
|
|
9
|
+
): Promise<Response> {
|
|
10
|
+
const { retries = 3, timeoutMs = 30_000, signal, ...init } = options;
|
|
11
|
+
let lastError: unknown;
|
|
12
|
+
|
|
13
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
16
|
+
const onAbort = () => controller.abort();
|
|
17
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(input, { ...init, signal: controller.signal });
|
|
21
|
+
if (!res.ok && attempt < retries - 1 && res.status >= 500) {
|
|
22
|
+
lastError = new Error(`fetch failed with ${res.status}`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
return res;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
lastError = error;
|
|
28
|
+
if (attempt === retries - 1) break;
|
|
29
|
+
} finally {
|
|
30
|
+
clearTimeout(timeout);
|
|
31
|
+
signal?.removeEventListener("abort", onAbort);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runtimeFetchJson(
|
|
39
|
+
input: string | URL | Request,
|
|
40
|
+
options: RuntimeFetchOptions = {},
|
|
41
|
+
): Promise<unknown> {
|
|
42
|
+
const res = await runtimeFetch(input, options);
|
|
43
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
44
|
+
if (contentType.includes("application/json")) return await res.json();
|
|
45
|
+
return await res.text();
|
|
46
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type GrepMatch = {
|
|
2
|
+
path: string;
|
|
3
|
+
line: number;
|
|
4
|
+
text: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export async function grep(pattern: string, cwd = process.cwd()): Promise<GrepMatch[]> {
|
|
8
|
+
const proc = Bun.spawn(["rg", "--line-number", "--no-heading", pattern, "."], {
|
|
9
|
+
cwd,
|
|
10
|
+
stdout: "pipe",
|
|
11
|
+
stderr: "pipe",
|
|
12
|
+
});
|
|
13
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
14
|
+
new Response(proc.stdout).text(),
|
|
15
|
+
new Response(proc.stderr).text(),
|
|
16
|
+
proc.exited,
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
if (exitCode === 1) return [];
|
|
20
|
+
if (exitCode !== 0) {
|
|
21
|
+
const message = stderr.includes("No such file or directory")
|
|
22
|
+
? "rg is not available on PATH"
|
|
23
|
+
: stderr.trim() || `rg exited with ${exitCode}`;
|
|
24
|
+
throw new Error(message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return stdout
|
|
28
|
+
.split("\n")
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map((line) => {
|
|
31
|
+
const [path = "", lineNo = "0", ...rest] = line.split(":");
|
|
32
|
+
return { path, line: Number(lineNo), text: rest.join(":") };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Redacted } from "../redacted";
|
|
2
|
+
import { runtimeFetch, runtimeFetchJson } from "./fetch";
|
|
3
|
+
import { glob } from "./glob";
|
|
4
|
+
import { grep } from "./grep";
|
|
5
|
+
import { table } from "./table";
|
|
6
|
+
|
|
7
|
+
export const stdlib = {
|
|
8
|
+
fetch: runtimeFetch,
|
|
9
|
+
fetchJson: runtimeFetchJson,
|
|
10
|
+
grep,
|
|
11
|
+
glob,
|
|
12
|
+
table,
|
|
13
|
+
Redacted,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export { runtimeFetch as fetch, runtimeFetchJson as fetchJson, glob, grep, table, Redacted };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function table(rows: Array<Record<string, unknown>>): string {
|
|
2
|
+
if (rows.length === 0) return "";
|
|
3
|
+
|
|
4
|
+
const headers = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
|
|
5
|
+
const renderedRows = rows.map((row) => headers.map((header) => String(row[header] ?? "")));
|
|
6
|
+
const widths = headers.map((header, index) =>
|
|
7
|
+
Math.max(header.length, ...renderedRows.map((row) => row[index]?.length ?? 0)),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const render = (cells: string[]) =>
|
|
11
|
+
cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" ");
|
|
12
|
+
return [
|
|
13
|
+
render(headers),
|
|
14
|
+
render(widths.map((width) => "-".repeat(width))),
|
|
15
|
+
...renderedRows.map(render),
|
|
16
|
+
].join("\n");
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SwarmConfigPayload } from "./executors/types";
|
|
2
|
+
import { Redacted, type Redacted as RedactedValue } from "./redacted";
|
|
3
|
+
|
|
4
|
+
export class SwarmConfig {
|
|
5
|
+
readonly apiKey: RedactedValue<string>;
|
|
6
|
+
readonly agentId: RedactedValue<string>;
|
|
7
|
+
readonly mcpBaseUrl: RedactedValue<string>;
|
|
8
|
+
|
|
9
|
+
private readonly userValues: Map<string, RedactedValue<string>>;
|
|
10
|
+
|
|
11
|
+
constructor(payload: SwarmConfigPayload) {
|
|
12
|
+
this.apiKey = Redacted.make(payload.system.apiKey.value, {
|
|
13
|
+
type: "system",
|
|
14
|
+
isSecret: payload.system.apiKey.isSecret,
|
|
15
|
+
});
|
|
16
|
+
this.agentId = Redacted.make(payload.system.agentId.value, {
|
|
17
|
+
type: "system",
|
|
18
|
+
isSecret: payload.system.agentId.isSecret,
|
|
19
|
+
});
|
|
20
|
+
this.mcpBaseUrl = Redacted.make(payload.system.mcpBaseUrl.value, {
|
|
21
|
+
type: "system",
|
|
22
|
+
isSecret: payload.system.mcpBaseUrl.isSecret,
|
|
23
|
+
});
|
|
24
|
+
this.userValues = new Map(
|
|
25
|
+
Object.entries(payload.user ?? {}).map(([key, value]) => [
|
|
26
|
+
key,
|
|
27
|
+
Redacted.make(value.value, { type: "user", isSecret: value.isSecret }),
|
|
28
|
+
]),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get<T = string>(key: string): RedactedValue<T> | undefined {
|
|
33
|
+
return this.userValues.get(key) as RedactedValue<T> | undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { scrubObject } from "../utils/secret-scrubber";
|
|
2
|
+
import { Redacted } from "./redacted";
|
|
3
|
+
import { isSdkToolAllowed } from "./sdk-allowlist";
|
|
4
|
+
import type { SwarmConfig } from "./swarm-config";
|
|
5
|
+
|
|
6
|
+
type BridgeRequest = {
|
|
7
|
+
method: string;
|
|
8
|
+
path: string;
|
|
9
|
+
body?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function headers(config: SwarmConfig): Record<string, string> {
|
|
13
|
+
return {
|
|
14
|
+
Authorization: `Bearer ${Redacted.value(config.apiKey)}`,
|
|
15
|
+
"X-Agent-ID": Redacted.value(config.agentId),
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function argsRecord(args: unknown): Record<string, unknown> {
|
|
21
|
+
return args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function appendQuery(path: string, query: Record<string, unknown>): string {
|
|
25
|
+
const params = new URLSearchParams();
|
|
26
|
+
for (const [key, value] of Object.entries(query)) {
|
|
27
|
+
if (value === undefined || value === null) continue;
|
|
28
|
+
params.set(key, String(value));
|
|
29
|
+
}
|
|
30
|
+
const encoded = params.toString();
|
|
31
|
+
return encoded ? `${path}?${encoded}` : path;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function kvPath(args: Record<string, unknown>, keyRequired = true): string {
|
|
35
|
+
const key = typeof args.key === "string" ? args.key : undefined;
|
|
36
|
+
if (keyRequired && !key) throw new Error("kv tool requires string `key`");
|
|
37
|
+
const namespace = typeof args.namespace === "string" ? args.namespace : undefined;
|
|
38
|
+
if (namespace) {
|
|
39
|
+
return key
|
|
40
|
+
? `/api/kv/_/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`
|
|
41
|
+
: `/api/kv/_/${encodeURIComponent(namespace)}`;
|
|
42
|
+
}
|
|
43
|
+
return key ? `/api/kv/${encodeURIComponent(key)}` : "/api/kv";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function bridgeRequestFor(name: string, args: unknown): BridgeRequest {
|
|
47
|
+
const body = argsRecord(args);
|
|
48
|
+
switch (name) {
|
|
49
|
+
case "memory_search":
|
|
50
|
+
return { method: "POST", path: "/api/memory/search", body };
|
|
51
|
+
case "memory_get": {
|
|
52
|
+
const memoryId = typeof body.memoryId === "string" ? body.memoryId : undefined;
|
|
53
|
+
if (!memoryId) throw new Error("memory_get requires string `memoryId`");
|
|
54
|
+
return { method: "GET", path: `/api/memory/${encodeURIComponent(memoryId)}` };
|
|
55
|
+
}
|
|
56
|
+
case "memory_rate": {
|
|
57
|
+
const event = {
|
|
58
|
+
memoryId: body.id,
|
|
59
|
+
signal: body.useful === false ? -1 : 1,
|
|
60
|
+
weight: 1,
|
|
61
|
+
source: "explicit-self",
|
|
62
|
+
reasoning: body.note ?? "",
|
|
63
|
+
...(typeof body.taskId === "string" ? { taskId: body.taskId } : {}),
|
|
64
|
+
...(typeof body.referencesSource === "string"
|
|
65
|
+
? { referencesSource: body.referencesSource }
|
|
66
|
+
: {}),
|
|
67
|
+
};
|
|
68
|
+
return { method: "POST", path: "/api/memory/rate", body: { events: [event] } };
|
|
69
|
+
}
|
|
70
|
+
case "task_list":
|
|
71
|
+
return { method: "GET", path: appendQuery("/api/tasks", body) };
|
|
72
|
+
case "task_get": {
|
|
73
|
+
const taskId = typeof body.taskId === "string" ? body.taskId : undefined;
|
|
74
|
+
if (!taskId) throw new Error("task_get requires string `taskId`");
|
|
75
|
+
return { method: "GET", path: `/api/tasks/${encodeURIComponent(taskId)}` };
|
|
76
|
+
}
|
|
77
|
+
case "task_storeProgress": {
|
|
78
|
+
const taskId = typeof body.taskId === "string" ? body.taskId : undefined;
|
|
79
|
+
if (!taskId) throw new Error("task_storeProgress requires string `taskId`");
|
|
80
|
+
if (body.status === "completed" || body.status === "failed") {
|
|
81
|
+
return {
|
|
82
|
+
method: "POST",
|
|
83
|
+
path: `/api/tasks/${encodeURIComponent(taskId)}/finish`,
|
|
84
|
+
body: {
|
|
85
|
+
status: body.status,
|
|
86
|
+
output: body.output,
|
|
87
|
+
failureReason: body.failureReason,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
method: "POST",
|
|
93
|
+
path: `/api/tasks/${encodeURIComponent(taskId)}/progress`,
|
|
94
|
+
body: { progress: body.progress ?? "" },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
case "kv_get":
|
|
98
|
+
return { method: "GET", path: kvPath(body) };
|
|
99
|
+
case "kv_set":
|
|
100
|
+
return {
|
|
101
|
+
method: "PUT",
|
|
102
|
+
path: kvPath(body),
|
|
103
|
+
body: {
|
|
104
|
+
value: body.value,
|
|
105
|
+
valueType: body.valueType,
|
|
106
|
+
expiresInSec: body.expiresInSec ?? body.ttlSeconds,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
case "kv_del":
|
|
110
|
+
return { method: "DELETE", path: kvPath(body) };
|
|
111
|
+
case "kv_incr":
|
|
112
|
+
return { method: "POST", path: `${kvPath(body)}/incr`, body: { by: body.by } };
|
|
113
|
+
case "kv_list":
|
|
114
|
+
return {
|
|
115
|
+
method: "GET",
|
|
116
|
+
path: appendQuery(kvPath(body, false), {
|
|
117
|
+
prefix: body.prefix,
|
|
118
|
+
limit: body.limit,
|
|
119
|
+
offset: body.offset,
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
case "repo_list":
|
|
123
|
+
return {
|
|
124
|
+
method: "GET",
|
|
125
|
+
path: appendQuery("/api/repos", { autoClone: body.autoClone, name: body.name }),
|
|
126
|
+
};
|
|
127
|
+
case "schedule_list":
|
|
128
|
+
return {
|
|
129
|
+
method: "GET",
|
|
130
|
+
path: appendQuery("/api/schedules", {
|
|
131
|
+
enabled: body.enabled,
|
|
132
|
+
name: body.name,
|
|
133
|
+
scheduleType: body.scheduleType,
|
|
134
|
+
hideCompleted: body.hideCompleted,
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
case "script_search":
|
|
138
|
+
return { method: "POST", path: "/api/scripts/search", body };
|
|
139
|
+
case "script_run":
|
|
140
|
+
return { method: "POST", path: "/api/scripts/run", body };
|
|
141
|
+
default:
|
|
142
|
+
throw new Error(`Tool '${name}' is not exposed through the scripts SDK bridge`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function callBridgeApi(
|
|
147
|
+
name: string,
|
|
148
|
+
args: unknown,
|
|
149
|
+
config: SwarmConfig,
|
|
150
|
+
options: { throwOnError?: boolean } = {},
|
|
151
|
+
): Promise<unknown> {
|
|
152
|
+
const baseUrl = Redacted.value(config.mcpBaseUrl).replace(/\/$/, "");
|
|
153
|
+
const request = bridgeRequestFor(name, args);
|
|
154
|
+
|
|
155
|
+
const res = await fetch(`${baseUrl}${request.path}`, {
|
|
156
|
+
method: request.method,
|
|
157
|
+
headers: headers(config),
|
|
158
|
+
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
159
|
+
});
|
|
160
|
+
const text = await res.text();
|
|
161
|
+
const data = text ? JSON.parse(text) : {};
|
|
162
|
+
if (!res.ok && options.throwOnError) {
|
|
163
|
+
const message =
|
|
164
|
+
data && typeof data === "object" && "error" in data
|
|
165
|
+
? String((data as { error: unknown }).error)
|
|
166
|
+
: `api failed with ${res.status}`;
|
|
167
|
+
throw new Error(`swarm-sdk: ${name} failed with ${res.status}: ${message}`);
|
|
168
|
+
}
|
|
169
|
+
return scrubObject({ success: res.ok, status: res.status, data });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function callTool(name: string, args: unknown, config: SwarmConfig): Promise<unknown> {
|
|
173
|
+
if (!isSdkToolAllowed(name)) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Tool '${name}' is not exposed to scripts (lifecycle/cred tool); use the MCP surface directly if you're an agent`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (name === "script_search" || name === "script_run") {
|
|
180
|
+
return callBridgeApi(name, args, config, { throwOnError: true });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return callBridgeApi(name, args, config);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function createSwarmSdk(
|
|
187
|
+
config: SwarmConfig,
|
|
188
|
+
): Record<string, (args?: unknown) => Promise<unknown>> {
|
|
189
|
+
const target: Record<string, unknown> = {};
|
|
190
|
+
return new Proxy(target, {
|
|
191
|
+
get(target, prop) {
|
|
192
|
+
if (typeof prop !== "string") return undefined;
|
|
193
|
+
if (prop in target) return target[prop];
|
|
194
|
+
return (args?: unknown) => callTool(prop, args, config);
|
|
195
|
+
},
|
|
196
|
+
}) as Record<string, (args?: unknown) => Promise<unknown>>;
|
|
197
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
declare module "stdlib" {
|
|
2
|
+
export interface Redacted<T> {
|
|
3
|
+
readonly __redactedBrand?: T;
|
|
4
|
+
toString(): "<redacted>";
|
|
5
|
+
toJSON(): "<redacted>";
|
|
6
|
+
}
|
|
7
|
+
export const Redacted: {
|
|
8
|
+
value<T>(self: Redacted<T>): T;
|
|
9
|
+
meta<T>(self: Redacted<T>): { type: "system" | "user"; isSecret: boolean };
|
|
10
|
+
isSecret<T>(self: Redacted<T>): boolean;
|
|
11
|
+
};
|
|
12
|
+
export function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
13
|
+
export function fetchJson(input: string | URL | Request, init?: RequestInit): Promise<unknown>;
|
|
14
|
+
export function grep(pattern: string, files?: string | string[]): Promise<string>;
|
|
15
|
+
export function glob(pattern: string): Promise<string[]>;
|
|
16
|
+
export function table(rows: Array<Record<string, unknown>>): string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module "swarm-sdk" {
|
|
20
|
+
export type JsonValue =
|
|
21
|
+
| null
|
|
22
|
+
| boolean
|
|
23
|
+
| number
|
|
24
|
+
| string
|
|
25
|
+
| JsonValue[]
|
|
26
|
+
| { [key: string]: JsonValue };
|
|
27
|
+
export type ScriptScope = "agent" | "global";
|
|
28
|
+
export type ScriptFsMode = "none" | "workspace-rw";
|
|
29
|
+
|
|
30
|
+
export interface Redacted<T> {
|
|
31
|
+
readonly __redactedBrand?: T;
|
|
32
|
+
toString(): "<redacted>";
|
|
33
|
+
toJSON(): "<redacted>";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RedactedStatic {
|
|
37
|
+
value<T>(self: Redacted<T>): T;
|
|
38
|
+
meta<T>(self: Redacted<T>): { type: "system" | "user"; isSecret: boolean };
|
|
39
|
+
isSecret<T>(self: Redacted<T>): boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SwarmConfig {
|
|
43
|
+
apiKey: Redacted<string>;
|
|
44
|
+
agentId: Redacted<string>;
|
|
45
|
+
mcpBaseUrl: Redacted<string>;
|
|
46
|
+
get<T = string>(key: string): Redacted<T> | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SwarmSdk {
|
|
50
|
+
memory_search(args: {
|
|
51
|
+
query: string;
|
|
52
|
+
scope?: "all" | "agent" | "swarm";
|
|
53
|
+
limit?: number;
|
|
54
|
+
source?: string;
|
|
55
|
+
}): Promise<unknown>;
|
|
56
|
+
memory_get(args: { memoryId: string }): Promise<unknown>;
|
|
57
|
+
memory_rate(args: { id: string; useful: boolean; note?: string }): Promise<unknown>;
|
|
58
|
+
task_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
59
|
+
task_get(args: { taskId: string }): Promise<unknown>;
|
|
60
|
+
task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
|
|
61
|
+
kv_get(args: { key: string; namespace?: string }): Promise<unknown>;
|
|
62
|
+
kv_set(args: {
|
|
63
|
+
key: string;
|
|
64
|
+
value: unknown;
|
|
65
|
+
namespace?: string;
|
|
66
|
+
ttlSeconds?: number;
|
|
67
|
+
valueType?: "string" | "json" | "integer";
|
|
68
|
+
}): Promise<unknown>;
|
|
69
|
+
kv_del(args: { key: string; namespace?: string }): Promise<unknown>;
|
|
70
|
+
kv_incr(args: { key: string; by?: number; namespace?: string }): Promise<unknown>;
|
|
71
|
+
kv_list(args?: { prefix?: string; namespace?: string; limit?: number }): Promise<unknown>;
|
|
72
|
+
repo_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
73
|
+
schedule_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
74
|
+
script_search(args: { query?: string; scope?: ScriptScope; limit?: number }): Promise<unknown>;
|
|
75
|
+
script_run(args: {
|
|
76
|
+
name?: string;
|
|
77
|
+
source?: string;
|
|
78
|
+
args?: unknown;
|
|
79
|
+
intent?: string;
|
|
80
|
+
scope?: ScriptScope;
|
|
81
|
+
fsMode?: ScriptFsMode;
|
|
82
|
+
}): Promise<unknown>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ScriptStdlib {
|
|
86
|
+
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
87
|
+
fetchJson(input: string | URL | Request, init?: RequestInit): Promise<unknown>;
|
|
88
|
+
grep(pattern: string, files?: string | string[]): Promise<string>;
|
|
89
|
+
glob(pattern: string): Promise<string[]>;
|
|
90
|
+
table(rows: Array<Record<string, unknown>>): string;
|
|
91
|
+
Redacted: RedactedStatic;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ScriptLogger extends Console {}
|
|
95
|
+
|
|
96
|
+
export interface ScriptContext {
|
|
97
|
+
swarm: SwarmSdk & { config: SwarmConfig };
|
|
98
|
+
stdlib: ScriptStdlib;
|
|
99
|
+
logger: ScriptLogger;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// biome-ignore lint/suspicious/noExplicitAny: scripts may narrow their args type at the entrypoint.
|
|
103
|
+
export type ScriptMain = (args: any, ctx: ScriptContext) => unknown | Promise<unknown>;
|
|
104
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
declare module "swarm-sdk" {
|
|
2
|
+
export type JsonValue =
|
|
3
|
+
| null
|
|
4
|
+
| boolean
|
|
5
|
+
| number
|
|
6
|
+
| string
|
|
7
|
+
| JsonValue[]
|
|
8
|
+
| { [key: string]: JsonValue };
|
|
9
|
+
export type ScriptScope = "agent" | "global";
|
|
10
|
+
export type ScriptFsMode = "none" | "workspace-rw";
|
|
11
|
+
|
|
12
|
+
export interface Redacted<T> {
|
|
13
|
+
readonly __redactedBrand?: T;
|
|
14
|
+
toString(): "<redacted>";
|
|
15
|
+
toJSON(): "<redacted>";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RedactedStatic {
|
|
19
|
+
value<T>(self: Redacted<T>): T;
|
|
20
|
+
meta<T>(self: Redacted<T>): { type: "system" | "user"; isSecret: boolean };
|
|
21
|
+
isSecret<T>(self: Redacted<T>): boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SwarmConfig {
|
|
25
|
+
apiKey: Redacted<string>;
|
|
26
|
+
agentId: Redacted<string>;
|
|
27
|
+
mcpBaseUrl: Redacted<string>;
|
|
28
|
+
get<T = string>(key: string): Redacted<T> | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SwarmSdk {
|
|
32
|
+
memory_search(args: {
|
|
33
|
+
query: string;
|
|
34
|
+
scope?: "all" | "agent" | "swarm";
|
|
35
|
+
limit?: number;
|
|
36
|
+
source?: string;
|
|
37
|
+
}): Promise<unknown>;
|
|
38
|
+
memory_get(args: { memoryId: string }): Promise<unknown>;
|
|
39
|
+
memory_rate(args: { id: string; useful: boolean; note?: string }): Promise<unknown>;
|
|
40
|
+
task_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
41
|
+
task_get(args: { taskId: string }): Promise<unknown>;
|
|
42
|
+
task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
|
|
43
|
+
kv_get(args: { key: string; namespace?: string }): Promise<unknown>;
|
|
44
|
+
kv_set(args: {
|
|
45
|
+
key: string;
|
|
46
|
+
value: unknown;
|
|
47
|
+
namespace?: string;
|
|
48
|
+
ttlSeconds?: number;
|
|
49
|
+
valueType?: "string" | "json" | "integer";
|
|
50
|
+
}): Promise<unknown>;
|
|
51
|
+
kv_del(args: { key: string; namespace?: string }): Promise<unknown>;
|
|
52
|
+
kv_incr(args: { key: string; by?: number; namespace?: string }): Promise<unknown>;
|
|
53
|
+
kv_list(args?: { prefix?: string; namespace?: string; limit?: number }): Promise<unknown>;
|
|
54
|
+
repo_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
55
|
+
schedule_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
56
|
+
script_search(args: { query?: string; scope?: ScriptScope; limit?: number }): Promise<unknown>;
|
|
57
|
+
script_run(args: {
|
|
58
|
+
name?: string;
|
|
59
|
+
source?: string;
|
|
60
|
+
args?: unknown;
|
|
61
|
+
intent?: string;
|
|
62
|
+
scope?: ScriptScope;
|
|
63
|
+
fsMode?: ScriptFsMode;
|
|
64
|
+
}): Promise<unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ScriptStdlib {
|
|
68
|
+
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
69
|
+
fetchJson(input: string | URL | Request, init?: RequestInit): Promise<unknown>;
|
|
70
|
+
grep(pattern: string, files?: string | string[]): Promise<string>;
|
|
71
|
+
glob(pattern: string): Promise<string[]>;
|
|
72
|
+
table(rows: Array<Record<string, unknown>>): string;
|
|
73
|
+
Redacted: RedactedStatic;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ScriptLogger extends Console {}
|
|
77
|
+
|
|
78
|
+
export interface ScriptContext {
|
|
79
|
+
swarm: SwarmSdk & { config: SwarmConfig };
|
|
80
|
+
stdlib: ScriptStdlib;
|
|
81
|
+
logger: ScriptLogger;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// biome-ignore lint/suspicious/noExplicitAny: scripts may narrow their args type at the entrypoint.
|
|
85
|
+
export type ScriptMain = (args: any, ctx: ScriptContext) => unknown | Promise<unknown>;
|
|
86
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import pkg from "../package.json";
|
|
3
3
|
import { initDb } from "./be/db";
|
|
4
|
+
import { seedPricingFromModelsDev } from "./be/seed-pricing";
|
|
4
5
|
import { registerCancelTaskTool } from "./tools/cancel-task";
|
|
5
6
|
import { registerContextDiffTool } from "./tools/context-diff";
|
|
6
7
|
import { registerContextHistoryTool } from "./tools/context-history";
|
|
@@ -67,6 +68,11 @@ import {
|
|
|
67
68
|
registerRunScheduleNowTool,
|
|
68
69
|
registerUpdateScheduleTool,
|
|
69
70
|
} from "./tools/schedules";
|
|
71
|
+
import { registerScriptDeleteTool } from "./tools/script-delete";
|
|
72
|
+
import { registerScriptQueryTypesTool } from "./tools/script-query-types";
|
|
73
|
+
import { registerScriptRunTool } from "./tools/script-run";
|
|
74
|
+
import { registerScriptSearchTool } from "./tools/script-search";
|
|
75
|
+
import { registerScriptUpsertTool } from "./tools/script-upsert";
|
|
70
76
|
import { registerSendTaskTool } from "./tools/send-task";
|
|
71
77
|
// Skills capability
|
|
72
78
|
import {
|
|
@@ -147,6 +153,11 @@ export function createServer() {
|
|
|
147
153
|
// Initialize database with WAL mode
|
|
148
154
|
// Uses DATABASE_PATH env var for Docker volume compatibility (WAL needs .sqlite, .sqlite-wal, .sqlite-shm on same filesystem)
|
|
149
155
|
initDb(process.env.DATABASE_PATH);
|
|
156
|
+
// Phase 2: project the vendored models.dev snapshot into the pricing table.
|
|
157
|
+
// Idempotent (INSERT OR IGNORE keyed on PK with effective_from=0); safe to
|
|
158
|
+
// call on every boot. See src/be/seed-pricing.ts for the projection logic
|
|
159
|
+
// and the manual-override constants for runtime-fee / ACU pricing.
|
|
160
|
+
seedPricingFromModelsDev();
|
|
150
161
|
|
|
151
162
|
const server = new McpServer(
|
|
152
163
|
{
|
|
@@ -196,6 +207,13 @@ export function createServer() {
|
|
|
196
207
|
registerDeletePromptTemplateTool(server);
|
|
197
208
|
registerPreviewPromptTemplateTool(server);
|
|
198
209
|
|
|
210
|
+
// Reusable script catalog tools - always registered (HTTP MCP only in v1).
|
|
211
|
+
registerScriptSearchTool(server);
|
|
212
|
+
registerScriptRunTool(server);
|
|
213
|
+
registerScriptUpsertTool(server);
|
|
214
|
+
registerScriptDeleteTool(server);
|
|
215
|
+
registerScriptQueryTypesTool(server);
|
|
216
|
+
|
|
199
217
|
// Slack integration tools (always registered, will no-op if Slack not configured)
|
|
200
218
|
registerSlackReplyTool(server);
|
|
201
219
|
registerSlackReadTool(server);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getApiKey, setApiKey } from "../utils/api-key";
|
|
3
|
+
|
|
4
|
+
describe("getApiKey", () => {
|
|
5
|
+
test("returns empty string when neither var is set", () => {
|
|
6
|
+
expect(getApiKey({})).toBe("");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("returns API_KEY when only legacy var is set", () => {
|
|
10
|
+
expect(getApiKey({ API_KEY: "legacy" })).toBe("legacy");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("returns AGENT_SWARM_API_KEY when only preferred var is set", () => {
|
|
14
|
+
expect(getApiKey({ AGENT_SWARM_API_KEY: "preferred" })).toBe("preferred");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("prefers AGENT_SWARM_API_KEY over API_KEY when both set", () => {
|
|
18
|
+
expect(getApiKey({ AGENT_SWARM_API_KEY: "preferred", API_KEY: "legacy" })).toBe("preferred");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("falls back to API_KEY if AGENT_SWARM_API_KEY is undefined", () => {
|
|
22
|
+
expect(getApiKey({ AGENT_SWARM_API_KEY: undefined, API_KEY: "x" })).toBe("x");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("setApiKey", () => {
|
|
27
|
+
test("populates both env var names", () => {
|
|
28
|
+
const env: Record<string, string | undefined> = {};
|
|
29
|
+
setApiKey("k", env);
|
|
30
|
+
expect(env.AGENT_SWARM_API_KEY).toBe("k");
|
|
31
|
+
expect(env.API_KEY).toBe("k");
|
|
32
|
+
});
|
|
33
|
+
});
|