@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.
Files changed (130) hide show
  1. package/openapi.json +496 -32
  2. package/package.json +14 -6
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/db.ts +102 -31
  5. package/src/be/migrations/063_cost_context_schema_relax.sql +133 -0
  6. package/src/be/migrations/064_scripts.sql +39 -0
  7. package/src/be/migrations/065_script_embeddings.sql +7 -0
  8. package/src/be/pricing-normalize.ts +81 -0
  9. package/src/be/scripts/db.ts +391 -0
  10. package/src/be/scripts/embeddings.ts +231 -0
  11. package/src/be/scripts/maintenance.ts +9 -0
  12. package/src/be/scripts/typecheck.ts +193 -0
  13. package/src/be/seed-pricing.ts +293 -0
  14. package/src/cli.tsx +22 -5
  15. package/src/commands/artifact.ts +3 -2
  16. package/src/commands/claude-managed-setup.ts +21 -4
  17. package/src/commands/codex-login.ts +5 -3
  18. package/src/commands/onboard.tsx +2 -1
  19. package/src/commands/runner.ts +663 -246
  20. package/src/commands/setup.tsx +5 -3
  21. package/src/hooks/hook.ts +4 -3
  22. package/src/http/context.ts +6 -2
  23. package/src/http/index.ts +126 -68
  24. package/src/http/memory.ts +28 -0
  25. package/src/http/openapi.ts +1 -0
  26. package/src/http/page-proxy.ts +2 -1
  27. package/src/http/route-def.ts +1 -0
  28. package/src/http/schedules.ts +37 -0
  29. package/src/http/scripts.ts +381 -0
  30. package/src/http/session-data.ts +74 -23
  31. package/src/linear/outbound.ts +9 -2
  32. package/src/otel-impl.ts +200 -0
  33. package/src/otel.ts +132 -0
  34. package/src/providers/claude-adapter.ts +52 -6
  35. package/src/providers/claude-managed-adapter.ts +43 -17
  36. package/src/providers/claude-managed-pricing.ts +34 -0
  37. package/src/providers/codex-adapter.ts +38 -27
  38. package/src/providers/codex-models.ts +22 -3
  39. package/src/providers/devin-adapter.ts +11 -0
  40. package/src/providers/opencode-adapter.ts +31 -7
  41. package/src/providers/pi-mono-adapter.ts +39 -7
  42. package/src/providers/pricing-sources.md +52 -0
  43. package/src/providers/swarm-events-shared.ts +8 -4
  44. package/src/providers/types.ts +33 -10
  45. package/src/scripts-runtime/ctx.ts +23 -0
  46. package/src/scripts-runtime/eval-harness.ts +39 -0
  47. package/src/scripts-runtime/executors/native.ts +229 -0
  48. package/src/scripts-runtime/executors/registry.ts +16 -0
  49. package/src/scripts-runtime/executors/types.ts +63 -0
  50. package/src/scripts-runtime/extract-signature.ts +81 -0
  51. package/src/scripts-runtime/import-allowlist.ts +109 -0
  52. package/src/scripts-runtime/loader.ts +96 -0
  53. package/src/scripts-runtime/redacted.ts +48 -0
  54. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  55. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  56. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  57. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  58. package/src/scripts-runtime/stdlib/index.ts +16 -0
  59. package/src/scripts-runtime/stdlib/table.ts +17 -0
  60. package/src/scripts-runtime/swarm-config.ts +35 -0
  61. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  62. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  63. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  64. package/src/server.ts +18 -0
  65. package/src/tests/api-key.test.ts +33 -0
  66. package/src/tests/claude-managed-adapter.test.ts +17 -3
  67. package/src/tests/claude-managed-setup.test.ts +10 -1
  68. package/src/tests/codex-adapter.test.ts +20 -19
  69. package/src/tests/codex-login.test.ts +1 -1
  70. package/src/tests/context-snapshot.test.ts +2 -2
  71. package/src/tests/context-window.test.ts +65 -1
  72. package/src/tests/devin-adapter.test.ts +2 -0
  73. package/src/tests/http/context-routes.test.ts +161 -0
  74. package/src/tests/linear-outbound-sync.test.ts +109 -0
  75. package/src/tests/mcp-tools.test.ts +69 -0
  76. package/src/tests/migration-063-schema-relax.test.ts +109 -0
  77. package/src/tests/opencode-adapter.test.ts +146 -1
  78. package/src/tests/otel-impl-secret-scrubbing.test.ts +33 -0
  79. package/src/tests/pages-view-count.test.ts +30 -5
  80. package/src/tests/providers/codex-cost.test.ts +18 -0
  81. package/src/tests/providers/opencode-cost.test.ts +74 -0
  82. package/src/tests/providers/pi-cost.test.ts +128 -0
  83. package/src/tests/redacted.test.ts +29 -0
  84. package/src/tests/runner-tool-spans.test.ts +268 -0
  85. package/src/tests/script-executor-conformance.test.ts +142 -0
  86. package/src/tests/script-executor-registry.test.ts +17 -0
  87. package/src/tests/scripts-db.test.ts +329 -0
  88. package/src/tests/scripts-embeddings.test.ts +291 -0
  89. package/src/tests/scripts-extract-signature.test.ts +47 -0
  90. package/src/tests/scripts-http.test.ts +350 -0
  91. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  92. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  93. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  94. package/src/tests/scripts-runtime.test.ts +289 -0
  95. package/src/tests/sdk-allowlist.test.ts +59 -0
  96. package/src/tests/secret-scrubber.test.ts +54 -1
  97. package/src/tests/session-costs-codex-recompute.test.ts +35 -22
  98. package/src/tests/session-costs-model-key-normalize.test.ts +271 -0
  99. package/src/tests/session-costs-recompute-all-providers.test.ts +170 -0
  100. package/src/tests/store-progress-cost.test.ts +6 -1
  101. package/src/tests/swarm-config.test.ts +38 -0
  102. package/src/tests/tool-annotations.test.ts +2 -2
  103. package/src/tests/tool-call-progress.test.ts +30 -0
  104. package/src/tests/workflow-e2e.test.ts +218 -0
  105. package/src/tests/workflow-executors.test.ts +32 -2
  106. package/src/tests/workflow-input-redaction.test.ts +232 -0
  107. package/src/tests/workflow-swarm-script.test.ts +273 -0
  108. package/src/tools/memory-rate.ts +2 -1
  109. package/src/tools/script-common.ts +88 -0
  110. package/src/tools/script-delete.ts +35 -0
  111. package/src/tools/script-query-types.ts +37 -0
  112. package/src/tools/script-run.ts +43 -0
  113. package/src/tools/script-search.ts +32 -0
  114. package/src/tools/script-upsert.ts +43 -0
  115. package/src/tools/store-progress.ts +16 -60
  116. package/src/tools/tool-config.ts +7 -0
  117. package/src/tools/utils.ts +65 -12
  118. package/src/types.ts +122 -10
  119. package/src/utils/api-key.ts +28 -0
  120. package/src/utils/context-window.ts +104 -4
  121. package/src/utils/page-session.ts +8 -6
  122. package/src/utils/secret-scrubber.ts +29 -1
  123. package/src/workflows/engine.ts +12 -4
  124. package/src/workflows/executors/index.ts +1 -0
  125. package/src/workflows/executors/registry.ts +2 -0
  126. package/src/workflows/executors/script.ts +12 -1
  127. package/src/workflows/executors/swarm-script.ts +170 -0
  128. package/src/workflows/input.ts +65 -0
  129. package/src/workflows/recovery.ts +31 -3
  130. 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,8 @@
1
+ export async function glob(pattern: string, cwd = process.cwd()): Promise<string[]> {
2
+ const globber = new Bun.Glob(pattern);
3
+ const matches: string[] = [];
4
+ for await (const match of globber.scan({ cwd })) {
5
+ matches.push(match);
6
+ }
7
+ return matches;
8
+ }
@@ -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
+ });