@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,193 @@
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
+ host.resolveModuleNames = (moduleNames, containingFile) =>
124
+ moduleNames.map((moduleName) => {
125
+ if (moduleName === "./user-script") {
126
+ return { resolvedFileName: USER_FILE, extension: ts.Extension.Ts };
127
+ }
128
+ if (moduleName === "swarm-sdk") {
129
+ return { resolvedFileName: SDK_FILE, extension: ts.Extension.Dts };
130
+ }
131
+ if (moduleName === "stdlib") {
132
+ return { resolvedFileName: STDLIB_FILE, extension: ts.Extension.Dts };
133
+ }
134
+ return ts.resolveModuleName(moduleName, containingFile, options, host).resolvedModule;
135
+ });
136
+
137
+ // In compiled binary mode, TypeScript's lib .d.ts files live alongside
138
+ // typescript.js in /$bunfs/ — but .d.ts files are not embedded in the binary.
139
+ // Redirect lib lookups to TS_LIB_DIR where the Dockerfile copies real copies.
140
+ const tsLibDir = process.env.TS_LIB_DIR;
141
+ if (tsLibDir) {
142
+ host.getDefaultLibLocation = () => tsLibDir;
143
+ }
144
+
145
+ return host;
146
+ }
147
+
148
+ export function typecheckScript(source: string): ScriptTypecheckResult {
149
+ const options: ts.CompilerOptions = {
150
+ allowImportingTsExtensions: true,
151
+ module: ts.ModuleKind.ESNext,
152
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
153
+ noEmit: true,
154
+ skipLibCheck: true,
155
+ strict: true,
156
+ target: ts.ScriptTarget.ES2022,
157
+ types: [],
158
+ };
159
+
160
+ const files = new Map<string, string>([
161
+ [USER_FILE, source],
162
+ [SDK_FILE, SCRIPT_SDK_TYPES],
163
+ [STDLIB_FILE, SCRIPT_STDLIB_TYPES],
164
+ [
165
+ CHECK_FILE,
166
+ `import run from "./user-script";
167
+ import type { ScriptMain } from "swarm-sdk";
168
+ const _scriptMain: ScriptMain = run;
169
+ void _scriptMain;
170
+ `,
171
+ ],
172
+ ]);
173
+
174
+ const host = createCompilerHost(files, options);
175
+ const program = ts.createProgram([USER_FILE, CHECK_FILE, SDK_FILE, STDLIB_FILE], options, host);
176
+ const diagnostics = [
177
+ ...program.getSyntacticDiagnostics(),
178
+ ...program.getSemanticDiagnostics(),
179
+ ].filter((diagnostic) => {
180
+ const fileName = diagnostic.file?.fileName.replace(/\\/g, "/");
181
+ return fileName === USER_FILE || fileName === CHECK_FILE;
182
+ });
183
+
184
+ if (diagnostics.length === 0) return { ok: true };
185
+
186
+ const formatted = ts.formatDiagnosticsWithColorAndContext(diagnostics, {
187
+ getCanonicalFileName: (fileName) => fileName,
188
+ getCurrentDirectory: () => "/virtual",
189
+ getNewLine: () => "\n",
190
+ });
191
+
192
+ return { ok: false, diagnostics: formatted.split("\n\n").filter(Boolean) };
193
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Phase 2 of the cost-tracking plan — seed the `pricing` table at server boot.
3
+ *
4
+ * The vendored models.dev snapshot at `ui/src/lib/modelsdev-cache.json` is the
5
+ * single source of truth for per-token rates. We project it into rows keyed by
6
+ * `(provider, model, token_class)` so the recompute path in
7
+ * `src/http/session-data.ts` can rebuild USD from tokens regardless of which
8
+ * adapter wrote the row.
9
+ *
10
+ * Manual overrides (Anthropic runtime fee, Cognition ACU) live in
11
+ * {@link MANUAL_PRICING_OVERRIDES} — models.dev doesn't surface those.
12
+ *
13
+ * The seeder uses `INSERT OR IGNORE` keyed on the pricing PK
14
+ * `(provider, model, token_class, effective_from)` with `effective_from = 0`,
15
+ * so re-runs on every boot are no-ops once seeded. Operators who need to bump
16
+ * a rate insert a new row with a later `effective_from` via the existing
17
+ * admin route (`POST /api/pricing`) — we don't overwrite seed rows.
18
+ */
19
+
20
+ import { readFileSync } from "node:fs";
21
+ import path from "node:path";
22
+ import type { PricingProvider, PricingTokenClass } from "../types";
23
+ import { getDb } from "./db";
24
+ import { normalizeModelKey } from "./pricing-normalize";
25
+
26
+ interface ModelsDevCostBlock {
27
+ input?: number;
28
+ output?: number;
29
+ cache_read?: number;
30
+ cache_write?: number;
31
+ }
32
+
33
+ interface ModelsDevModel {
34
+ id?: string;
35
+ cost?: ModelsDevCostBlock;
36
+ }
37
+
38
+ interface ModelsDevProvider {
39
+ models?: Record<string, ModelsDevModel>;
40
+ }
41
+
42
+ type ModelsDevCache = Record<string, ModelsDevProvider>;
43
+
44
+ /**
45
+ * Per-harness manual rates that models.dev doesn't carry. Keep the source URL
46
+ * and a verification date next to each entry so {@link MANUAL_PRICING_OVERRIDES}
47
+ * doubles as living documentation.
48
+ */
49
+ const MANUAL_PRICING_OVERRIDES: Array<{
50
+ provider: PricingProvider;
51
+ model: string;
52
+ tokenClass: PricingTokenClass;
53
+ pricePerMillionUsd: number;
54
+ source: string;
55
+ verified: string; // YYYY-MM-DD
56
+ }> = [
57
+ {
58
+ provider: "claude-managed",
59
+ // '*' = applies regardless of which Claude model the managed run picks.
60
+ // The runtime fee is per session-hour, not per model.
61
+ model: "*",
62
+ tokenClass: "runtime_hour",
63
+ // $0.08 / hour expressed as USD per "million units" so it fits the same
64
+ // rate table. The adapter will multiply by hours, not by tokens — the
65
+ // unit is a convention specific to `runtime_hour`.
66
+ pricePerMillionUsd: 0.08 * 1_000_000,
67
+ source: "https://docs.claude.com/en/api/agent-sdk/managed-runtime#pricing",
68
+ verified: "2026-04-28",
69
+ },
70
+ {
71
+ provider: "devin",
72
+ model: "*",
73
+ tokenClass: "acu",
74
+ pricePerMillionUsd: 2.25 * 1_000_000,
75
+ source: "https://devin.ai/pricing",
76
+ verified: "2026-04-28",
77
+ },
78
+ ];
79
+
80
+ /**
81
+ * Adapter-specific shortname → models.dev key. Some adapters report `model`
82
+ * fields the models.dev snapshot doesn't index directly; we map them here.
83
+ */
84
+ const ANTHROPIC_SHORTNAME_TO_MODELSDEV: Record<string, string> = {
85
+ opus: "claude-opus-4-7",
86
+ sonnet: "claude-sonnet-4-6",
87
+ haiku: "claude-haiku-4-5",
88
+ };
89
+
90
+ /**
91
+ * Resolve the path to the vendored models.dev cache. The UI copy is canonical.
92
+ * We treat this as best-effort: if the file is missing (developer ran the
93
+ * server without `ui/` checked out), we log and continue with manual rates
94
+ * only — better than crashing the boot.
95
+ */
96
+ function loadModelsDevCache(): ModelsDevCache | null {
97
+ const candidates = [
98
+ path.join(process.cwd(), "ui", "src", "lib", "modelsdev-cache.json"),
99
+ path.join(process.cwd(), "..", "ui", "src", "lib", "modelsdev-cache.json"),
100
+ ];
101
+ for (const cand of candidates) {
102
+ try {
103
+ const raw = readFileSync(cand, "utf-8");
104
+ return JSON.parse(raw) as ModelsDevCache;
105
+ } catch {
106
+ // try next candidate
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+
112
+ interface PricingSeedRow {
113
+ provider: PricingProvider;
114
+ model: string;
115
+ tokenClass: PricingTokenClass;
116
+ pricePerMillionUsd: number;
117
+ }
118
+
119
+ /**
120
+ * Project a models.dev `cost` block into our pricing-table token classes.
121
+ * Returns one row per non-null cost field.
122
+ */
123
+ function projectCostBlock(
124
+ provider: PricingProvider,
125
+ model: string,
126
+ cost: ModelsDevCostBlock,
127
+ ): PricingSeedRow[] {
128
+ // Phase 2 fix — canonicalize the seed key with the same normalizer the
129
+ // lookup path uses. Idempotent for keys models.dev already serves in
130
+ // canonical form (the common case); also collapses any future drift.
131
+ const key = normalizeModelKey(provider, model);
132
+ const rows: PricingSeedRow[] = [];
133
+ if (typeof cost.input === "number") {
134
+ rows.push({ provider, model: key, tokenClass: "input", pricePerMillionUsd: cost.input });
135
+ }
136
+ if (typeof cost.output === "number") {
137
+ rows.push({ provider, model: key, tokenClass: "output", pricePerMillionUsd: cost.output });
138
+ }
139
+ if (typeof cost.cache_read === "number") {
140
+ rows.push({
141
+ provider,
142
+ model: key,
143
+ tokenClass: "cached_input",
144
+ pricePerMillionUsd: cost.cache_read,
145
+ });
146
+ }
147
+ if (typeof cost.cache_write === "number") {
148
+ rows.push({
149
+ provider,
150
+ model: key,
151
+ tokenClass: "cache_write",
152
+ pricePerMillionUsd: cost.cache_write,
153
+ });
154
+ }
155
+ return rows;
156
+ }
157
+
158
+ /**
159
+ * Build the full set of seed rows from a loaded models.dev cache.
160
+ *
161
+ * The mapping logic is intentionally per-provider so the matrix between
162
+ * "what the adapter writes for `model`" and "what models.dev keys by" is
163
+ * explicit and auditable.
164
+ */
165
+ function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[] {
166
+ const rows: PricingSeedRow[] = [];
167
+
168
+ // ---- Anthropic / claude family ----------------------------------------
169
+ // The 'claude' provider (local-CLI adapter) reports the model id as the
170
+ // Anthropic CLI returns it. The 'claude-managed' provider may report
171
+ // either a dated full id or a non-dated id. We project both keyed forms
172
+ // for each model so the recompute path resolves either way.
173
+ const anthropic = cache.anthropic?.models ?? {};
174
+ for (const [id, model] of Object.entries(anthropic)) {
175
+ if (!model?.cost) continue;
176
+ for (const provider of ["claude", "claude-managed"] as const) {
177
+ for (const row of projectCostBlock(provider, id, model.cost)) {
178
+ rows.push(row);
179
+ }
180
+ }
181
+ }
182
+ // Anthropic shortnames (opus/sonnet/haiku) → resolve to the current default.
183
+ for (const [shortname, fullId] of Object.entries(ANTHROPIC_SHORTNAME_TO_MODELSDEV)) {
184
+ const target = anthropic[fullId];
185
+ if (!target?.cost) continue;
186
+ for (const provider of ["claude", "claude-managed"] as const) {
187
+ for (const row of projectCostBlock(provider, shortname, target.cost)) {
188
+ rows.push(row);
189
+ }
190
+ }
191
+ }
192
+ // Pi-mono uses anthropic models via OpenRouter mirrors; project those too.
193
+ for (const [shortname, fullId] of Object.entries(ANTHROPIC_SHORTNAME_TO_MODELSDEV)) {
194
+ const target = anthropic[fullId];
195
+ if (!target?.cost) continue;
196
+ for (const row of projectCostBlock("pi", shortname, target.cost)) {
197
+ rows.push(row);
198
+ }
199
+ }
200
+
201
+ // ---- OpenAI / codex family --------------------------------------------
202
+ const openai = cache.openai?.models ?? {};
203
+ for (const [id, model] of Object.entries(openai)) {
204
+ if (!model?.cost) continue;
205
+ for (const row of projectCostBlock("codex", id, model.cost)) {
206
+ rows.push(row);
207
+ }
208
+ // Phase 2 fix — pi-mono can route to openai models through the
209
+ // github-copilot proxy (`github-copilot/gpt-5.4`). The lookup helper
210
+ // strips the prefix, so we seed the bare id under `pi` too. Without this
211
+ // every gh-copilot-backed pi run fell through to `costSource='unpriced'`.
212
+ for (const row of projectCostBlock("pi", id, model.cost)) {
213
+ rows.push(row);
214
+ }
215
+ }
216
+
217
+ // ---- OpenRouter passthrough (covers gemini + every opencode-routed model)
218
+ const openrouter = cache.openrouter?.models ?? {};
219
+ for (const [id, model] of Object.entries(openrouter)) {
220
+ if (!model?.cost) continue;
221
+ // opencode routes whatever model the user picks; we project them all.
222
+ for (const row of projectCostBlock("opencode", id, model.cost)) {
223
+ rows.push(row);
224
+ }
225
+ // pi-mono also routes via OpenRouter when only OPENROUTER_API_KEY is set
226
+ // (see src/providers/pi-mono-adapter.ts). Without this projection, pi runs
227
+ // against non-anthropic models (e.g. deepseek/deepseek-v4-flash) fall
228
+ // through to costSource='unpriced' even though the model is in the
229
+ // models.dev snapshot.
230
+ for (const row of projectCostBlock("pi", id, model.cost)) {
231
+ rows.push(row);
232
+ }
233
+ // Gemini specifically: also project under the 'gemini' provider so
234
+ // internal-ai callers that tag with provider='gemini' find a hit.
235
+ if (id.startsWith("google/")) {
236
+ const geminiKey = id.replace(/^google\//, "");
237
+ for (const row of projectCostBlock("gemini", geminiKey, model.cost)) {
238
+ rows.push(row);
239
+ }
240
+ // Also store under the full openrouter id so the same row resolves
241
+ // whether the caller passes "google/..." or the stripped name.
242
+ for (const row of projectCostBlock("gemini", id, model.cost)) {
243
+ rows.push(row);
244
+ }
245
+ }
246
+ }
247
+
248
+ return rows;
249
+ }
250
+
251
+ /**
252
+ * Phase 2 entrypoint. Idempotent — safe to call on every boot. Logs a one-line
253
+ * summary so operators can tell whether the boot picked up new rates.
254
+ */
255
+ export function seedPricingFromModelsDev(opts?: { quiet?: boolean }): {
256
+ inserted: number;
257
+ modelsdevFound: boolean;
258
+ } {
259
+ const db = getDb();
260
+ const cache = loadModelsDevCache();
261
+ const modelsdevRows = cache ? buildModelsDevSeedRows(cache) : [];
262
+ const manualRows = MANUAL_PRICING_OVERRIDES.map((o) => ({
263
+ provider: o.provider,
264
+ model: o.model,
265
+ tokenClass: o.tokenClass,
266
+ pricePerMillionUsd: o.pricePerMillionUsd,
267
+ }));
268
+ const allRows = [...modelsdevRows, ...manualRows];
269
+
270
+ const insert = db.prepare<null, [string, string, string, number]>(
271
+ `INSERT OR IGNORE INTO pricing
272
+ (provider, model, token_class, effective_from, price_per_million_usd, createdAt, lastUpdatedAt)
273
+ VALUES (?, ?, ?, 0, ?, 0, 0)`,
274
+ );
275
+
276
+ let inserted = 0;
277
+ const tx = db.transaction((rows: PricingSeedRow[]) => {
278
+ for (const row of rows) {
279
+ const result = insert.run(row.provider, row.model, row.tokenClass, row.pricePerMillionUsd);
280
+ if (result.changes > 0) inserted += 1;
281
+ }
282
+ });
283
+ tx(allRows);
284
+
285
+ if (!opts?.quiet) {
286
+ console.log(
287
+ `[pricing] seed: ${inserted} new row(s); ${allRows.length} candidate(s); modelsdev=${
288
+ cache ? "loaded" : "missing"
289
+ }`,
290
+ );
291
+ }
292
+ return { inserted, modelsdevFound: !!cache };
293
+ }
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) {
@@ -553,12 +554,28 @@ export async function runClaudeManagedSetupFlow(
553
554
  system: mcpServer
554
555
  ? "You are an agent-swarm worker. Per-task instructions arrive in the next user message. Use the agent-swarm MCP server for swarm operations."
555
556
  : "You are an agent-swarm worker. Per-task instructions arrive in the next user message. (No MCP tools available in this configuration.)",
557
+ // Headless workers can't satisfy interactive approval prompts — the
558
+ // Anthropic console parks tool calls in `awaiting approval` and the
559
+ // session stalls. Apply `always_allow` to both toolsets so the sandbox
560
+ // executes tool calls (incl. swarm MCP `store-progress`) without HITL.
556
561
  tools: mcpServer
557
562
  ? [
558
- { type: "agent_toolset_20260401" },
559
- { type: "mcp_toolset", mcp_server_name: mcpServer.name },
563
+ {
564
+ type: "agent_toolset_20260401",
565
+ default_config: { permission_policy: { type: "always_allow" } },
566
+ },
567
+ {
568
+ type: "mcp_toolset",
569
+ mcp_server_name: mcpServer.name,
570
+ default_config: { permission_policy: { type: "always_allow" } },
571
+ },
560
572
  ]
561
- : [{ type: "agent_toolset_20260401" }],
573
+ : [
574
+ {
575
+ type: "agent_toolset_20260401",
576
+ default_config: { permission_policy: { type: "always_allow" } },
577
+ },
578
+ ],
562
579
  skills: skillsParam,
563
580
  ...(mcpServer ? { mcp_servers: [mcpServer] } : {}),
564
581
  };
@@ -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;