@desplega.ai/agent-swarm 1.80.0 → 1.80.2

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 (100) hide show
  1. package/openapi.json +399 -14
  2. package/package.json +3 -1
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/db.ts +1 -1
  5. package/src/be/migrations/064_scripts.sql +39 -0
  6. package/src/be/migrations/065_script_embeddings.sql +7 -0
  7. package/src/be/migrations/066_scripts_args_json_schema.sql +1 -0
  8. package/src/be/scripts/db.ts +417 -0
  9. package/src/be/scripts/embeddings.ts +233 -0
  10. package/src/be/scripts/extract-schema.ts +55 -0
  11. package/src/be/scripts/maintenance.ts +9 -0
  12. package/src/be/scripts/typecheck.ts +199 -0
  13. package/src/cli.tsx +22 -5
  14. package/src/commands/artifact.ts +3 -2
  15. package/src/commands/claude-managed-setup.ts +2 -1
  16. package/src/commands/codex-login.ts +5 -3
  17. package/src/commands/onboard.tsx +2 -1
  18. package/src/commands/runner.ts +153 -20
  19. package/src/commands/setup.tsx +5 -3
  20. package/src/hooks/hook.ts +4 -3
  21. package/src/http/index.ts +40 -29
  22. package/src/http/memory.ts +28 -0
  23. package/src/http/openapi.ts +1 -0
  24. package/src/http/page-proxy.ts +2 -1
  25. package/src/http/route-def.ts +1 -0
  26. package/src/http/schedules.ts +37 -0
  27. package/src/http/scripts.ts +388 -0
  28. package/src/linear/outbound.ts +9 -2
  29. package/src/otel.ts +5 -0
  30. package/src/providers/claude-adapter.ts +23 -1
  31. package/src/providers/types.ts +8 -0
  32. package/src/scripts-runtime/ctx.ts +23 -0
  33. package/src/scripts-runtime/eval-harness.ts +63 -0
  34. package/src/scripts-runtime/executors/native.ts +232 -0
  35. package/src/scripts-runtime/executors/registry.ts +16 -0
  36. package/src/scripts-runtime/executors/types.ts +63 -0
  37. package/src/scripts-runtime/extract-args-schema.ts +69 -0
  38. package/src/scripts-runtime/extract-signature.ts +81 -0
  39. package/src/scripts-runtime/import-allowlist.ts +109 -0
  40. package/src/scripts-runtime/loader.ts +96 -0
  41. package/src/scripts-runtime/redacted.ts +48 -0
  42. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  43. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  44. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  45. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  46. package/src/scripts-runtime/stdlib/index.ts +16 -0
  47. package/src/scripts-runtime/stdlib/table.ts +17 -0
  48. package/src/scripts-runtime/swarm-config.ts +35 -0
  49. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  50. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  51. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  52. package/src/server.ts +12 -0
  53. package/src/tests/api-key.test.ts +33 -0
  54. package/src/tests/codex-login.test.ts +1 -1
  55. package/src/tests/error-tracker.test.ts +44 -0
  56. package/src/tests/linear-outbound-sync.test.ts +109 -0
  57. package/src/tests/mcp-tools.test.ts +69 -0
  58. package/src/tests/rate-limit-event.test.ts +292 -0
  59. package/src/tests/redacted.test.ts +29 -0
  60. package/src/tests/runner-tool-spans.test.ts +268 -0
  61. package/src/tests/script-executor-conformance.test.ts +142 -0
  62. package/src/tests/script-executor-registry.test.ts +17 -0
  63. package/src/tests/scripts-db.test.ts +329 -0
  64. package/src/tests/scripts-embeddings.test.ts +291 -0
  65. package/src/tests/scripts-extract-signature.test.ts +47 -0
  66. package/src/tests/scripts-http.test.ts +403 -0
  67. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  68. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  69. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  70. package/src/tests/scripts-runtime.test.ts +344 -0
  71. package/src/tests/sdk-allowlist.test.ts +59 -0
  72. package/src/tests/secret-scrubber.test.ts +35 -1
  73. package/src/tests/swarm-config.test.ts +38 -0
  74. package/src/tests/tool-annotations.test.ts +2 -2
  75. package/src/tests/tool-call-progress.test.ts +30 -0
  76. package/src/tests/workflow-e2e.test.ts +218 -0
  77. package/src/tests/workflow-executors.test.ts +32 -2
  78. package/src/tests/workflow-input-redaction.test.ts +232 -0
  79. package/src/tests/workflow-swarm-script.test.ts +273 -0
  80. package/src/tools/memory-rate.ts +2 -1
  81. package/src/tools/script-common.ts +88 -0
  82. package/src/tools/script-delete.ts +35 -0
  83. package/src/tools/script-query-types.ts +37 -0
  84. package/src/tools/script-run.ts +43 -0
  85. package/src/tools/script-search.ts +32 -0
  86. package/src/tools/script-upsert.ts +43 -0
  87. package/src/tools/tool-config.ts +7 -0
  88. package/src/types.ts +61 -1
  89. package/src/utils/api-key.ts +28 -0
  90. package/src/utils/error-tracker.ts +58 -0
  91. package/src/utils/page-session.ts +8 -6
  92. package/src/utils/secret-scrubber.ts +22 -1
  93. package/src/workflows/engine.ts +12 -4
  94. package/src/workflows/executors/index.ts +1 -0
  95. package/src/workflows/executors/registry.ts +2 -0
  96. package/src/workflows/executors/script.ts +12 -1
  97. package/src/workflows/executors/swarm-script.ts +170 -0
  98. package/src/workflows/input.ts +65 -0
  99. package/src/workflows/recovery.ts +31 -3
  100. package/src/workflows/resume.ts +43 -5
@@ -0,0 +1,199 @@
1
+ import ts from "typescript";
2
+
3
+ export type ScriptTypecheckResult = { ok: true } | { ok: false; diagnostics: string[] };
4
+
5
+ export const SCRIPT_SDK_TYPES = `
6
+ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
7
+ export type ScriptScope = "agent" | "global";
8
+ export type ScriptFsMode = "none" | "workspace-rw";
9
+
10
+ export interface Redacted<T> {
11
+ readonly __redactedBrand?: T;
12
+ toString(): "<redacted>";
13
+ toJSON(): "<redacted>";
14
+ }
15
+
16
+ export interface RedactedStatic {
17
+ value<T>(self: Redacted<T>): T;
18
+ meta<T>(self: Redacted<T>): { type: "system" | "user"; isSecret: boolean };
19
+ isSecret<T>(self: Redacted<T>): boolean;
20
+ }
21
+
22
+ export interface SwarmConfig {
23
+ apiKey: Redacted<string>;
24
+ agentId: Redacted<string>;
25
+ mcpBaseUrl: Redacted<string>;
26
+ get<T = string>(key: string): Redacted<T> | undefined;
27
+ }
28
+
29
+ export interface SwarmSdk {
30
+ memory_search(args: { query: string; scope?: "all" | "agent" | "swarm"; limit?: number; source?: string }): Promise<unknown>;
31
+ memory_get(args: { memoryId: string }): Promise<unknown>;
32
+ memory_rate(args: { id: string; useful: boolean; note?: string }): Promise<unknown>;
33
+ task_list(args?: Record<string, unknown>): Promise<unknown>;
34
+ task_get(args: { taskId: string }): Promise<unknown>;
35
+ task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
36
+ kv_get(args: { key: string; namespace?: string }): Promise<unknown>;
37
+ kv_set(args: { key: string; value: unknown; namespace?: string; ttlSeconds?: number; valueType?: "string" | "json" | "integer" }): Promise<unknown>;
38
+ kv_del(args: { key: string; namespace?: string }): Promise<unknown>;
39
+ kv_incr(args: { key: string; by?: number; namespace?: string }): Promise<unknown>;
40
+ kv_list(args?: { prefix?: string; namespace?: string; limit?: number }): Promise<unknown>;
41
+ repo_list(args?: Record<string, unknown>): Promise<unknown>;
42
+ schedule_list(args?: Record<string, unknown>): Promise<unknown>;
43
+ script_search(args: { query?: string; scope?: ScriptScope; limit?: number }): Promise<unknown>;
44
+ script_run(args: { name?: string; source?: string; args?: unknown; intent?: string; scope?: ScriptScope; fsMode?: ScriptFsMode }): Promise<unknown>;
45
+ }
46
+
47
+ export interface ScriptStdlib {
48
+ fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
49
+ fetchJson(input: string | URL | Request, init?: RequestInit): Promise<unknown>;
50
+ grep(pattern: string, files?: string | string[]): Promise<string>;
51
+ glob(pattern: string): Promise<string[]>;
52
+ table(rows: Array<Record<string, unknown>>): string;
53
+ Redacted: RedactedStatic;
54
+ }
55
+
56
+ export interface ScriptLogger extends Console {}
57
+
58
+ export interface ScriptContext {
59
+ swarm: SwarmSdk & { config: SwarmConfig };
60
+ stdlib: ScriptStdlib;
61
+ logger: ScriptLogger;
62
+ }
63
+
64
+ // biome-ignore lint/suspicious/noExplicitAny: scripts may narrow their args type at the entrypoint.
65
+ export type ScriptMain = (args: any, ctx: ScriptContext) => unknown | Promise<unknown>;
66
+ `;
67
+
68
+ export const SCRIPT_STDLIB_TYPES = `
69
+ declare module "stdlib" {
70
+ export interface Redacted<T> {
71
+ readonly __redactedBrand?: T;
72
+ toString(): "<redacted>";
73
+ toJSON(): "<redacted>";
74
+ }
75
+ export const Redacted: {
76
+ value<T>(self: Redacted<T>): T;
77
+ meta<T>(self: Redacted<T>): { type: "system" | "user"; isSecret: boolean };
78
+ isSecret<T>(self: Redacted<T>): boolean;
79
+ };
80
+ export function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
81
+ export function fetchJson(input: string | URL | Request, init?: RequestInit): Promise<unknown>;
82
+ export function grep(pattern: string, files?: string | string[]): Promise<string>;
83
+ export function glob(pattern: string): Promise<string[]>;
84
+ export function table(rows: Array<Record<string, unknown>>): string;
85
+ }
86
+
87
+ declare module "swarm-sdk" {
88
+ ${SCRIPT_SDK_TYPES.replace(/^/gm, " ")}
89
+ }
90
+ `;
91
+
92
+ const USER_FILE = "/virtual/user-script.ts";
93
+ const CHECK_FILE = "/virtual/check.ts";
94
+ const SDK_FILE = "/virtual/swarm-sdk.d.ts";
95
+ const STDLIB_FILE = "/virtual/stdlib.d.ts";
96
+
97
+ function createCompilerHost(
98
+ files: Map<string, string>,
99
+ options: ts.CompilerOptions,
100
+ ): ts.CompilerHost {
101
+ const host = ts.createCompilerHost(options, true);
102
+ const originalGetSourceFile = host.getSourceFile.bind(host);
103
+
104
+ host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
105
+ const normalized = fileName.replace(/\\/g, "/");
106
+ const source = files.get(normalized);
107
+ if (source !== undefined) {
108
+ return ts.createSourceFile(normalized, source, languageVersion, true, ts.ScriptKind.TS);
109
+ }
110
+ return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
111
+ };
112
+
113
+ host.fileExists = (fileName) => {
114
+ const normalized = fileName.replace(/\\/g, "/");
115
+ return files.has(normalized) || ts.sys.fileExists(fileName);
116
+ };
117
+
118
+ host.readFile = (fileName) => {
119
+ const normalized = fileName.replace(/\\/g, "/");
120
+ return files.get(normalized) ?? ts.sys.readFile(fileName);
121
+ };
122
+
123
+ // Resolve external packages (e.g. "zod") from the project root rather than
124
+ // the virtual path "/virtual/..." so TypeScript can find real node_modules.
125
+ const projectBase = new URL("../../index.ts", import.meta.url).pathname;
126
+
127
+ host.resolveModuleNames = (moduleNames, containingFile) =>
128
+ moduleNames.map((moduleName) => {
129
+ if (moduleName === "./user-script") {
130
+ return { resolvedFileName: USER_FILE, extension: ts.Extension.Ts };
131
+ }
132
+ if (moduleName === "swarm-sdk") {
133
+ return { resolvedFileName: SDK_FILE, extension: ts.Extension.Dts };
134
+ }
135
+ if (moduleName === "stdlib") {
136
+ return { resolvedFileName: STDLIB_FILE, extension: ts.Extension.Dts };
137
+ }
138
+ // For external packages, resolve from project root so node_modules is found
139
+ const base = containingFile.startsWith("/virtual/") ? projectBase : containingFile;
140
+ return ts.resolveModuleName(moduleName, base, options, host).resolvedModule;
141
+ });
142
+
143
+ // In compiled binary mode, TypeScript's lib .d.ts files live alongside
144
+ // typescript.js in /$bunfs/ — but .d.ts files are not embedded in the binary.
145
+ // Redirect lib lookups to TS_LIB_DIR where the Dockerfile copies real copies.
146
+ const tsLibDir = process.env.TS_LIB_DIR;
147
+ if (tsLibDir) {
148
+ host.getDefaultLibLocation = () => tsLibDir;
149
+ }
150
+
151
+ return host;
152
+ }
153
+
154
+ export function typecheckScript(source: string): ScriptTypecheckResult {
155
+ const options: ts.CompilerOptions = {
156
+ allowImportingTsExtensions: true,
157
+ module: ts.ModuleKind.ESNext,
158
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
159
+ noEmit: true,
160
+ skipLibCheck: true,
161
+ strict: true,
162
+ target: ts.ScriptTarget.ES2022,
163
+ types: [],
164
+ };
165
+
166
+ const files = new Map<string, string>([
167
+ [USER_FILE, source],
168
+ [SDK_FILE, SCRIPT_SDK_TYPES],
169
+ [STDLIB_FILE, SCRIPT_STDLIB_TYPES],
170
+ [
171
+ CHECK_FILE,
172
+ `import run from "./user-script";
173
+ import type { ScriptMain } from "swarm-sdk";
174
+ const _scriptMain: ScriptMain = run;
175
+ void _scriptMain;
176
+ `,
177
+ ],
178
+ ]);
179
+
180
+ const host = createCompilerHost(files, options);
181
+ const program = ts.createProgram([USER_FILE, CHECK_FILE, SDK_FILE, STDLIB_FILE], options, host);
182
+ const diagnostics = [
183
+ ...program.getSyntacticDiagnostics(),
184
+ ...program.getSemanticDiagnostics(),
185
+ ].filter((diagnostic) => {
186
+ const fileName = diagnostic.file?.fileName.replace(/\\/g, "/");
187
+ return fileName === USER_FILE || fileName === CHECK_FILE;
188
+ });
189
+
190
+ if (diagnostics.length === 0) return { ok: true };
191
+
192
+ const formatted = ts.formatDiagnosticsWithColorAndContext(diagnostics, {
193
+ getCanonicalFileName: (fileName) => fileName,
194
+ getCurrentDirectory: () => "/virtual",
195
+ getNewLine: () => "\n",
196
+ });
197
+
198
+ return { ok: false, diagnostics: formatted.split("\n\n").filter(Boolean) };
199
+ }
package/src/cli.tsx CHANGED
@@ -9,6 +9,7 @@ import { runLead } from "./commands/lead.ts";
9
9
  import { Onboard } from "./commands/onboard.tsx";
10
10
  import { Setup as Connect } from "./commands/setup.tsx";
11
11
  import { runWorker } from "./commands/worker.ts";
12
+ import { getApiKey, setApiKey } from "./utils/api-key.ts";
12
13
 
13
14
  // Get CLI name from bin field (assumes single key)
14
15
  const binName = Object.keys(pkg.bin)[0];
@@ -43,7 +44,7 @@ interface ParsedArgs {
43
44
  function parseArgs(args: string[]): ParsedArgs {
44
45
  const command = args[0] && !args[0].startsWith("-") ? args[0] : undefined;
45
46
  let port = process.env.PORT || "3013";
46
- let key = process.env.API_KEY || "";
47
+ let key = getApiKey();
47
48
  let msg = "";
48
49
  let headless = false;
49
50
  let dryRun = false;
@@ -151,7 +152,7 @@ const COMMAND_HELP: Record<
151
152
  connect: {
152
153
  usage: `${binName} connect [options]`,
153
154
  description:
154
- "Connect this project to an existing swarm.\nCreates .mcp.json and .claude/settings.local.json with server URL and API key.\nAuto-reads API_KEY from .env if present.",
155
+ "Connect this project to an existing swarm.\nCreates .mcp.json and .claude/settings.local.json with server URL and API key.\nAuto-reads AGENT_SWARM_API_KEY (or legacy API_KEY) from .env if present.",
155
156
  options: [
156
157
  " --dry-run Show what would be changed without writing",
157
158
  " --restore Restore files from .bak backups",
@@ -248,13 +249,19 @@ const COMMAND_HELP: Record<
248
249
  options: " -h, --help Show this help",
249
250
  examples: [` ${binName} artifact serve`, ` ${binName} artifact help`].join("\n"),
250
251
  },
252
+ scripts: {
253
+ usage: `${binName} scripts reembed`,
254
+ description: "Maintenance commands for reusable swarm scripts.",
255
+ options: " -h, --help Show this help",
256
+ examples: ` ${binName} scripts reembed`,
257
+ },
251
258
  "codex-login": {
252
259
  usage: `${binName} codex-login [options]`,
253
260
  description:
254
261
  "Authenticate Codex via ChatGPT OAuth (browser or manual paste).\nPrompts interactively for the target API URL and a best-effort masked API key, then stores credentials in the swarm API config store for deployed workers.",
255
262
  options: [
256
263
  " --api-url <url> Swarm API URL (default: MCP_BASE_URL or http://localhost:3013)",
257
- " --api-key <key> Swarm API key (default: API_KEY or 123123)",
264
+ " --api-key <key> Swarm API key (default: AGENT_SWARM_API_KEY or API_KEY, falling back to 123123)",
258
265
  " -h, --help Show this help",
259
266
  ].join("\n"),
260
267
  examples: [
@@ -269,7 +276,7 @@ const COMMAND_HELP: Record<
269
276
  "Bootstrap Anthropic Managed Agents for the swarm: create the cloud environment, upload plugin/commands/*.md skills, create the managed agent, and persist the resulting IDs to swarm_config so deployed workers restore them at boot. Prompts interactively for ANTHROPIC_API_KEY when not set in env. Idempotent — re-run with --force to recreate.",
270
277
  options: [
271
278
  " --api-url <url> Swarm API URL (default: MCP_BASE_URL or http://localhost:3013)",
272
- " --api-key <key> Swarm API key (default: API_KEY or 123123)",
279
+ " --api-key <key> Swarm API key (default: AGENT_SWARM_API_KEY or API_KEY, falling back to 123123)",
273
280
  " --force Recreate Anthropic-side resources even if already configured",
274
281
  " -h, --help Show this help",
275
282
  ].join("\n"),
@@ -306,6 +313,7 @@ function printHelp(command?: string) {
306
313
  ["claude", "Run Claude CLI"],
307
314
  ["hook", "Handle Claude Code hook events (stdin)"],
308
315
  ["artifact", "Manage agent artifacts"],
316
+ ["scripts", "Reusable scripts maintenance"],
309
317
  ["docs", "Open documentation (--open to launch in browser)"],
310
318
  ["codex-login", "Authenticate Codex via ChatGPT OAuth"],
311
319
  ["claude-managed-setup", "Bootstrap Anthropic Managed Agents (agent + env + skills)"],
@@ -324,7 +332,7 @@ function McpServer({ port, apiKey, dbPath }: { port: string; apiKey: string; dbP
324
332
 
325
333
  useEffect(() => {
326
334
  process.env.PORT = port;
327
- process.env.API_KEY = apiKey;
335
+ setApiKey(apiKey);
328
336
  if (dbPath) {
329
337
  process.env.DATABASE_PATH = dbPath;
330
338
  }
@@ -547,6 +555,15 @@ if (args.showHelp || args.command === "help" || args.command === undefined) {
547
555
  port: args.port,
548
556
  key: args.key,
549
557
  });
558
+ } else if (args.command === "scripts") {
559
+ const scriptsArgs = process.argv.slice(process.argv.indexOf("scripts") + 1);
560
+ if (args.showHelp || scriptsArgs[0] !== "reembed") {
561
+ printHelp("scripts");
562
+ process.exit(scriptsArgs[0] === "reembed" || args.showHelp ? 0 : 1);
563
+ }
564
+ const { runScriptsMaintenanceCommand } = await import("./be/scripts/maintenance");
565
+ await runScriptsMaintenanceCommand(scriptsArgs);
566
+ console.log("Scripts re-embedded.");
550
567
  } else if (args.command === "codex-login") {
551
568
  const { runCodexLogin } = await import("./commands/codex-login");
552
569
  const codexLoginArgs = process.argv.slice(process.argv.indexOf("codex-login") + 1);
@@ -1,5 +1,6 @@
1
1
  import { Hono } from "hono";
2
2
  import { createArtifactServer } from "../artifact-sdk";
3
+ import { getApiKey } from "../utils/api-key";
3
4
 
4
5
  interface ArtifactArgs {
5
6
  additionalArgs: string[];
@@ -137,7 +138,7 @@ async function artifactServe(args: ArtifactArgs) {
137
138
  }
138
139
 
139
140
  async function artifactList() {
140
- const apiKey = process.env.API_KEY || "";
141
+ const apiKey = getApiKey();
141
142
  const mcpBaseUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
142
143
  const agentId = process.env.AGENT_ID || "";
143
144
 
@@ -195,7 +196,7 @@ async function artifactStop(args: ArtifactArgs) {
195
196
  process.exit(1);
196
197
  }
197
198
 
198
- const apiKey = process.env.API_KEY || "";
199
+ const apiKey = getApiKey();
199
200
  const mcpBaseUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
200
201
  const agentId = process.env.AGENT_ID || "";
201
202
 
@@ -33,6 +33,7 @@ import type { BetaEnvironment } from "@anthropic-ai/sdk/resources/beta/environme
33
33
  import type { SkillCreateResponse } from "@anthropic-ai/sdk/resources/beta/skills";
34
34
  import { toFile } from "@anthropic-ai/sdk/uploads";
35
35
 
36
+ import { getApiKey } from "../utils/api-key";
36
37
  import { promptHiddenInput } from "./codex-login.js";
37
38
 
38
39
  // ─── Types ───────────────────────────────────────────────────────────────────
@@ -397,7 +398,7 @@ export async function resolveClaudeManagedSetupConfig(
397
398
  const isInteractive = deps.isInteractive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
398
399
 
399
400
  const apiUrl = parsed.apiUrl ?? env.MCP_BASE_URL ?? "http://localhost:3013";
400
- const apiKey = parsed.apiKey ?? env.API_KEY ?? "123123";
401
+ const apiKey = parsed.apiKey ?? (getApiKey(env) || "123123");
401
402
 
402
403
  let anthropicApiKey = env.ANTHROPIC_API_KEY ?? "";
403
404
  if (!anthropicApiKey && isInteractive) {
@@ -14,6 +14,7 @@ import { emitKeypressEvents } from "node:readline";
14
14
 
15
15
  import { loginCodexOAuth } from "../providers/codex-oauth/flow.js";
16
16
  import { storeCodexOAuth } from "../providers/codex-oauth/storage.js";
17
+ import { getApiKey } from "../utils/api-key";
17
18
 
18
19
  type PromptTextFn = (label: string, defaultValue: string) => Promise<string>;
19
20
  type PromptSecretFn = (label: string, defaultValue: string, helpText?: string) => Promise<string>;
@@ -146,7 +147,8 @@ export async function resolveCodexLoginConfig(
146
147
  const promptSecret = deps.promptSecret ?? promptHiddenInput;
147
148
  const isInteractive = deps.isInteractive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
148
149
  const defaultApiUrl = env.MCP_BASE_URL || "http://localhost:3013";
149
- const defaultApiKey = env.API_KEY || "123123";
150
+ const envApiKey = getApiKey(env);
151
+ const defaultApiKey = envApiKey || "123123";
150
152
 
151
153
  let apiUrl = parsed.apiUrl ?? defaultApiUrl;
152
154
  let apiKey = parsed.apiKey ?? defaultApiKey;
@@ -156,8 +158,8 @@ export async function resolveCodexLoginConfig(
156
158
  }
157
159
 
158
160
  if (!parsed.apiKey && isInteractive) {
159
- const apiKeyHelp = env.API_KEY
160
- ? "Press Enter to use API_KEY from the environment"
161
+ const apiKeyHelp = envApiKey
162
+ ? "Press Enter to use AGENT_SWARM_API_KEY/API_KEY from the environment"
161
163
  : "Press Enter to use the default local API key";
162
164
  apiKey =
163
165
  (await promptSecret("Swarm API key", defaultApiKey, apiKeyHelp)).trim() || defaultApiKey;
@@ -3,6 +3,7 @@ import { Select } from "@inkjs/ui";
3
3
  import { Box, Text, useApp, useInput } from "ink";
4
4
  import { useCallback, useEffect, useRef, useState } from "react";
5
5
  import pkg from "../../package.json";
6
+ import { getApiKey } from "../utils/api-key.ts";
6
7
  import { getAgentSummary, getPresetById, PRESETS } from "./onboard/presets.ts";
7
8
  import { CoreCredentialsStep } from "./onboard/steps/core-credentials.tsx";
8
9
  import { CustomTemplatesStep } from "./onboard/steps/custom-templates.tsx";
@@ -140,7 +141,7 @@ export function Onboard({ dryRun = false, yes = false, preset }: OnboardProps) {
140
141
  }
141
142
 
142
143
  const credentialType = anthropicKey ? "api_key" : "oauth";
143
- const apiKey = process.env.API_KEY || crypto.randomBytes(16).toString("hex");
144
+ const apiKey = getApiKey() || crypto.randomBytes(16).toString("hex");
144
145
 
145
146
  const agentIds: Record<string, string> = {};
146
147
  for (const svc of selectedPreset.services) {
@@ -6,6 +6,7 @@ import {
6
6
  type Attributes,
7
7
  initOtel,
8
8
  injectTraceContext,
9
+ isPollTracingEnabled,
9
10
  type SwarmSpan,
10
11
  startSpan,
11
12
  withSpan,
@@ -31,6 +32,7 @@ import {
31
32
  } from "../providers/index.ts";
32
33
  import { initTelemetry, telemetry } from "../telemetry.ts";
33
34
  import type { ProviderName, RepoGuidelines } from "../types.ts";
35
+ import { getApiKey } from "../utils/api-key.ts";
34
36
  import { computeBudgetBackoffMs } from "../utils/budget-backoff.ts";
35
37
  import { getContextWindowSize } from "../utils/context-window.ts";
36
38
  import { type CredentialSelection, resolveCredentialPools } from "../utils/credentials.ts";
@@ -366,7 +368,17 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
366
368
  "inject-learning": "🧠 Storing learning",
367
369
  "memory-search": "🧠 Searching memory",
368
370
  "memory-get": "🧠 Retrieving memory",
371
+ "memory-delete": "🧠 Deleting memory",
369
372
  "update-profile": "🪪 Updating profile",
373
+ // Users
374
+ "manage-user": "👤 Managing user",
375
+ "resolve-user": "👤 Resolving user",
376
+ // Key-value store
377
+ "kv-get": "🔑 Reading KV value",
378
+ "kv-set": "🔑 Setting KV value",
379
+ "kv-list": "🔑 Listing KV keys",
380
+ "kv-delete": "🔑 Deleting KV value",
381
+ "kv-incr": "🔑 Incrementing KV value",
370
382
  // Slack
371
383
  "slack-post": "💬 Posting to Slack",
372
384
  "slack-start-thread": "💬 Starting Slack thread",
@@ -386,20 +398,37 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
386
398
  "get-workflow": "⚙️ Checking workflow",
387
399
  "list-workflows": "⚙️ Listing workflows",
388
400
  "create-workflow": "⚙️ Creating workflow",
401
+ "update-workflow": "⚙️ Updating workflow",
402
+ "delete-workflow": "⚙️ Deleting workflow",
403
+ "patch-workflow": "⚙️ Patching workflow",
404
+ "patch-workflow-node": "⚙️ Patching workflow node",
405
+ "get-workflow-run": "⚙️ Checking workflow run",
406
+ "list-workflow-runs": "⚙️ Listing workflow runs",
407
+ "cancel-workflow-run": "⚙️ Cancelling workflow run",
408
+ "retry-workflow-run": "⚙️ Retrying workflow run",
389
409
  // Skills
390
410
  "skill-search": "🔎 Searching skills",
391
411
  "skill-install": "📦 Installing skill",
392
412
  "skill-install-remote": "📦 Installing remote skill",
393
413
  "skill-get": "📦 Getting skill details",
394
414
  "skill-list": "📦 Listing skills",
415
+ "skill-create": "📦 Creating skill",
416
+ "skill-update": "📦 Updating skill",
417
+ "skill-delete": "📦 Deleting skill",
418
+ "skill-publish": "📦 Publishing skill",
419
+ "skill-uninstall": "📦 Uninstalling skill",
420
+ "skill-sync-remote": "📦 Syncing remote skills",
395
421
  // Config
396
422
  "get-config": "⚙️ Reading config",
397
423
  "set-config": "⚙️ Setting config",
398
424
  "list-config": "⚙️ Listing config",
425
+ "delete-config": "⚙️ Deleting config",
399
426
  // Schedules
400
427
  "create-schedule": "📅 Creating schedule",
401
428
  "list-schedules": "📅 Listing schedules",
402
429
  "run-schedule-now": "📅 Running schedule",
430
+ "update-schedule": "📅 Updating schedule",
431
+ "delete-schedule": "📅 Deleting schedule",
403
432
  // Context
404
433
  "context-diff": "📜 Viewing context diff",
405
434
  "context-history": "📜 Viewing context history",
@@ -412,12 +441,42 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
412
441
  "list-services": "🔌 Listing services",
413
442
  "unregister-service": "🔌 Unregistering service",
414
443
  "update-service-status": "🔌 Updating service status",
444
+ // Reusable scripts
445
+ "script-search": "📜 Searching scripts",
446
+ "script-run": "📜 Running script",
447
+ "script-upsert": "📜 Saving script",
448
+ "script-delete": "📜 Deleting script",
449
+ "script-query-types": "📜 Reading script types",
415
450
  };
416
451
 
417
- /** Convert kebab-case to sentence case: "get-task-details" "Get task details" */
452
+ /** Words that keep specific casing when humanizing tool names. */
453
+ const TOOL_NAME_ACRONYMS: Record<string, string> = {
454
+ mcp: "MCP",
455
+ kv: "KV",
456
+ api: "API",
457
+ url: "URL",
458
+ id: "ID",
459
+ };
460
+
461
+ /**
462
+ * Convert kebab/snake-case to sentence case, preserving known acronyms.
463
+ * "get-task-details" → "Get task details"; "mcp-server-create" → "MCP server create".
464
+ */
418
465
  export function humanizeToolName(name: string): string {
419
466
  if (!name) return name;
420
- return name.charAt(0).toUpperCase() + name.slice(1).replaceAll("-", " ");
467
+ const words = name
468
+ .replaceAll("_", " ")
469
+ .replaceAll("-", " ")
470
+ .trim()
471
+ .split(/\s+/)
472
+ .filter(Boolean)
473
+ .map((w) => TOOL_NAME_ACRONYMS[w.toLowerCase()] ?? w);
474
+ const first = words[0];
475
+ if (!first) return name;
476
+ const head = TOOL_NAME_ACRONYMS[first.toLowerCase()]
477
+ ? first
478
+ : first.charAt(0).toUpperCase() + first.slice(1);
479
+ return [head, ...words.slice(1)].join(" ");
421
480
  }
422
481
 
423
482
  /**
@@ -487,15 +546,13 @@ export function toolCallToProgress(toolName: string, args: unknown): string | nu
487
546
  }
488
547
 
489
548
  // Pi-mono exposes tools from the built-in swarm MCP endpoint as bare
490
- // names ("store-progress", "send-task", ...), not as mcp__ names.
549
+ // names ("store-progress", "send-task", "script-run", ...), not as mcp__ names.
491
550
  // Treat those names as agent-swarm tools so activity stays readable.
492
- if (toolName.includes("-")) {
493
- const label = SWARM_TOOL_LABELS[toolName];
494
- if (label === null) return null;
495
- if (label) return label;
496
- }
551
+ const label = SWARM_TOOL_LABELS[toolName];
552
+ if (label === null) return null;
553
+ if (label) return label;
497
554
 
498
- return `🔧 ${toolName}`;
555
+ return `🔧 ${humanizeToolName(toolName)}`;
499
556
  }
500
557
  }
501
558
  }
@@ -1513,6 +1570,9 @@ async function registerAgent(opts: {
1513
1570
 
1514
1571
  /** Poll for triggers via HTTP API */
1515
1572
  async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
1573
+ if (!isPollTracingEnabled()) {
1574
+ return pollForTriggerOnce(opts);
1575
+ }
1516
1576
  return withSpan(
1517
1577
  "worker.poll",
1518
1578
  async (span) => {
@@ -1917,6 +1977,47 @@ function providerEventAttributes(event: ProviderEvent): Attributes {
1917
1977
  }
1918
1978
  }
1919
1979
 
1980
+ /**
1981
+ * Entry shape for the `activeToolSpans` map maintained by `runWithSession`.
1982
+ * Exported for unit tests that exercise `implicitCloseActiveToolSpans`.
1983
+ */
1984
+ export type ActiveToolSpanEntry = {
1985
+ span: SwarmSpan;
1986
+ startedAt: number;
1987
+ };
1988
+
1989
+ /**
1990
+ * Closes any still-open tool spans (both `worker.tool` and `worker.mcp.tool`)
1991
+ * in `activeToolSpans` at the assistant-message boundary, tagging them with
1992
+ * `agentswarm.tool.implicit_close=true` and removing them from the map.
1993
+ *
1994
+ * The Claude SDK adapter doesn't emit per-tool completion events for any
1995
+ * tool kind, MCP or harness-side — so the assistant-message boundary serves
1996
+ * as an implicit `tool_end` for everything. Spans closed here are still
1997
+ * successful (code 1, OK); the boundary is just a signal that the prior
1998
+ * tool turn is done.
1999
+ *
2000
+ * Returns the number of spans closed (handy for tests / metrics).
2001
+ */
2002
+ export function implicitCloseActiveToolSpans(
2003
+ activeToolSpans: Map<string, ActiveToolSpanEntry>,
2004
+ now: number = Date.now(),
2005
+ ): number {
2006
+ let closed = 0;
2007
+ for (const [toolCallId, active] of activeToolSpans) {
2008
+ active.span.setAttributes({
2009
+ "agentswarm.tool.duration_ms": now - active.startedAt,
2010
+ "agentswarm.tool.implicit_close": true,
2011
+ "agentswarm.tool.call_id": toolCallId,
2012
+ });
2013
+ active.span.setStatus({ code: 1 });
2014
+ active.span.end();
2015
+ activeToolSpans.delete(toolCallId);
2016
+ closed++;
2017
+ }
2018
+ return closed;
2019
+ }
2020
+
1920
2021
  async function spawnProviderProcess(
1921
2022
  adapter: ReturnType<typeof createProviderAdapter>,
1922
2023
  opts: {
@@ -2155,6 +2256,18 @@ async function spawnProviderProcess(
2155
2256
  sessionId: event.sessionId,
2156
2257
  });
2157
2258
  break;
2259
+ case "message": {
2260
+ // Assistant-message boundary acts as an implicit tool_end for ALL
2261
+ // still-open tool spans (both `worker.tool` and `worker.mcp.tool`).
2262
+ // The Claude SDK adapter doesn't emit per-tool completion events for
2263
+ // any tool kind, so without this their spans would only close at
2264
+ // session shutdown via `closeActiveToolSpans` and report wall-clock
2265
+ // duration from tool_start to session end.
2266
+ if (event.role === "assistant") {
2267
+ implicitCloseActiveToolSpans(activeToolSpans);
2268
+ }
2269
+ break;
2270
+ }
2158
2271
  case "tool_start": {
2159
2272
  const tool = classifyTool(event.toolName, event.args);
2160
2273
  const toolSpan = startSpan(tool.kind === "mcp" ? "worker.mcp.tool" : "worker.tool", {
@@ -2619,15 +2732,35 @@ async function checkCompletedProcesses(
2619
2732
  credentialInfo &&
2620
2733
  /rate.?limit|hit your limit|usage[ _-]?limit|too many requests/i.test(failureReason)
2621
2734
  ) {
2622
- // Try to extract reset time from the error message (e.g. "resets 3pm (UTC)")
2623
- const parsedResetTime = parseRateLimitResetTime(failureReason);
2624
- const defaultCooldownMs = 5 * 60 * 1000;
2625
- const rateLimitedUntil =
2626
- parsedResetTime ?? new Date(Date.now() + defaultCooldownMs).toISOString();
2627
- if (parsedResetTime) {
2735
+ // Three-tier reset-time resolver (most to least precise):
2736
+ // Tier 1: structured rate_limit_event from Claude CLI (resetsAt epoch sec)
2737
+ // Tier 2: regex on the error message (e.g. "resets 3pm (UTC)")
2738
+ // Tier 3: 5-min hard fallback — only when both structured and regex fail
2739
+ // Tiers 1 & 2 are clamped to [now+60s, now+6h] at their source.
2740
+ const clampResetTime = (isoString: string): string => {
2741
+ const nowMs = Date.now();
2742
+ const minMs = nowMs + 60_000;
2743
+ const maxMs = nowMs + 6 * 60 * 60 * 1000;
2744
+ const candidateMs = new Date(isoString).getTime();
2745
+ return new Date(Math.min(Math.max(candidateMs, minMs), maxMs)).toISOString();
2746
+ };
2747
+
2748
+ let rateLimitedUntil: string;
2749
+ if (result.rateLimitResetAt) {
2750
+ rateLimitedUntil = clampResetTime(result.rateLimitResetAt);
2628
2751
  console.log(
2629
- `[credentials] Parsed rate limit reset time from error: ${parsedResetTime}`,
2752
+ `[credentials] Rate limit reset from rate_limit_event: ${rateLimitedUntil}`,
2630
2753
  );
2754
+ } else {
2755
+ const parsedResetTime = parseRateLimitResetTime(failureReason);
2756
+ if (parsedResetTime) {
2757
+ rateLimitedUntil = clampResetTime(parsedResetTime);
2758
+ console.log(
2759
+ `[credentials] Parsed rate limit reset time from error: ${rateLimitedUntil}`,
2760
+ );
2761
+ } else {
2762
+ rateLimitedUntil = new Date(Date.now() + 5 * 60 * 1000).toISOString();
2763
+ }
2631
2764
  }
2632
2765
  reportKeyRateLimit(
2633
2766
  apiConfig.apiUrl,
@@ -2772,7 +2905,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2772
2905
 
2773
2906
  const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
2774
2907
  const swarmUrl = process.env.SWARM_URL || "localhost";
2775
- const apiKey = process.env.API_KEY || "";
2908
+ const apiKey = getApiKey();
2776
2909
 
2777
2910
  // Resolve the boot harness provider from swarm_config (repo > agent > global,
2778
2911
  // overlaid on top of `process.env`). This is what selects the adapter for
@@ -2798,8 +2931,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2798
2931
  let adapter = createProviderAdapter(bootProvider);
2799
2932
 
2800
2933
  // Configure HTTP-based template resolution (workers resolve via API, not local DB)
2801
- if (process.env.API_KEY) {
2802
- configureHttpResolver(apiUrl, process.env.API_KEY);
2934
+ if (apiKey) {
2935
+ configureHttpResolver(apiUrl, apiKey);
2803
2936
  }
2804
2937
 
2805
2938
  // Initialize anonymized telemetry (opt-out via ANONYMIZED_TELEMETRY=false).
@@ -2810,7 +2943,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2810
2943
  // skips telemetry instead of minting a fresh `install_<hex>` ID per
2811
2944
  // restart, which floods prod metrics with phantom installs.
2812
2945
  {
2813
- const telemetryApiKey = process.env.API_KEY;
2946
+ const telemetryApiKey = apiKey || undefined;
2814
2947
  await initTelemetry(
2815
2948
  "worker",
2816
2949
  async (key) => {