swarm-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/bin/swarm.mjs +45 -0
  4. package/dist/agents/aider.d.ts +12 -0
  5. package/dist/agents/aider.js +182 -0
  6. package/dist/agents/claude-code.d.ts +9 -0
  7. package/dist/agents/claude-code.js +216 -0
  8. package/dist/agents/codex.d.ts +14 -0
  9. package/dist/agents/codex.js +193 -0
  10. package/dist/agents/direct-llm.d.ts +9 -0
  11. package/dist/agents/direct-llm.js +78 -0
  12. package/dist/agents/mock.d.ts +9 -0
  13. package/dist/agents/mock.js +77 -0
  14. package/dist/agents/opencode.d.ts +23 -0
  15. package/dist/agents/opencode.js +571 -0
  16. package/dist/agents/provider.d.ts +11 -0
  17. package/dist/agents/provider.js +31 -0
  18. package/dist/cli.d.ts +15 -0
  19. package/dist/cli.js +285 -0
  20. package/dist/compression/compressor.d.ts +28 -0
  21. package/dist/compression/compressor.js +265 -0
  22. package/dist/config.d.ts +42 -0
  23. package/dist/config.js +170 -0
  24. package/dist/core/repl.d.ts +69 -0
  25. package/dist/core/repl.js +336 -0
  26. package/dist/core/rlm.d.ts +63 -0
  27. package/dist/core/rlm.js +409 -0
  28. package/dist/core/runtime.py +335 -0
  29. package/dist/core/types.d.ts +131 -0
  30. package/dist/core/types.js +19 -0
  31. package/dist/env.d.ts +10 -0
  32. package/dist/env.js +75 -0
  33. package/dist/interactive-swarm.d.ts +20 -0
  34. package/dist/interactive-swarm.js +1041 -0
  35. package/dist/interactive.d.ts +10 -0
  36. package/dist/interactive.js +1765 -0
  37. package/dist/main.d.ts +15 -0
  38. package/dist/main.js +242 -0
  39. package/dist/mcp/server.d.ts +15 -0
  40. package/dist/mcp/server.js +72 -0
  41. package/dist/mcp/session.d.ts +73 -0
  42. package/dist/mcp/session.js +184 -0
  43. package/dist/mcp/tools.d.ts +15 -0
  44. package/dist/mcp/tools.js +377 -0
  45. package/dist/memory/episodic.d.ts +132 -0
  46. package/dist/memory/episodic.js +390 -0
  47. package/dist/prompts/orchestrator.d.ts +5 -0
  48. package/dist/prompts/orchestrator.js +191 -0
  49. package/dist/routing/model-router.d.ts +130 -0
  50. package/dist/routing/model-router.js +515 -0
  51. package/dist/swarm.d.ts +14 -0
  52. package/dist/swarm.js +557 -0
  53. package/dist/threads/cache.d.ts +58 -0
  54. package/dist/threads/cache.js +198 -0
  55. package/dist/threads/manager.d.ts +85 -0
  56. package/dist/threads/manager.js +659 -0
  57. package/dist/ui/banner.d.ts +14 -0
  58. package/dist/ui/banner.js +42 -0
  59. package/dist/ui/dashboard.d.ts +33 -0
  60. package/dist/ui/dashboard.js +151 -0
  61. package/dist/ui/index.d.ts +10 -0
  62. package/dist/ui/index.js +11 -0
  63. package/dist/ui/log.d.ts +39 -0
  64. package/dist/ui/log.js +126 -0
  65. package/dist/ui/onboarding.d.ts +14 -0
  66. package/dist/ui/onboarding.js +518 -0
  67. package/dist/ui/spinner.d.ts +25 -0
  68. package/dist/ui/spinner.js +113 -0
  69. package/dist/ui/summary.d.ts +18 -0
  70. package/dist/ui/summary.js +113 -0
  71. package/dist/ui/theme.d.ts +63 -0
  72. package/dist/ui/theme.js +97 -0
  73. package/dist/viewer.d.ts +12 -0
  74. package/dist/viewer.js +1284 -0
  75. package/dist/worktree/manager.d.ts +45 -0
  76. package/dist/worktree/manager.js +266 -0
  77. package/dist/worktree/merge.d.ts +28 -0
  78. package/dist/worktree/merge.js +138 -0
  79. package/package.json +69 -0
@@ -0,0 +1,335 @@
1
+ """
2
+ Swarm Runtime — Python-side helpers for the swarm-cli orchestrator.
3
+
4
+ Extends the RLM runtime with thread spawning primitives:
5
+ - `thread(task, context, agent, model, files)`: spawn a coding agent thread
6
+ - `async_thread(...)`: async version for asyncio.gather()
7
+ - `merge_threads()`: merge all completed thread branches
8
+
9
+ Communication protocol (line-delimited JSON over stdio):
10
+ -> stdout: {"type":"llm_query","sub_context":"...","instruction":"...","id":"..."}
11
+ <- stdin: {"type":"llm_result","id":"...","result":"..."}
12
+ -> stdout: {"type":"thread_request","id":"...","task":"...","context":"...","agent_backend":"...","model":"...","files":[]}
13
+ <- stdin: {"type":"thread_result","id":"...","result":"...","success":true,"files_changed":[],"duration_ms":0}
14
+ -> stdout: {"type":"merge_request","id":"..."}
15
+ <- stdin: {"type":"merge_result","id":"...","result":"...","success":true}
16
+ -> stdout: {"type":"exec_done","stdout":"...","stderr":"...","has_final":bool,"final_value":"..."|null}
17
+ """
18
+
19
+ import json
20
+ import sys
21
+ import uuid
22
+ import io
23
+ import traceback
24
+ import asyncio
25
+ import threading
26
+ import queue
27
+
28
+ # Real stdio handles — saved before exec() can redirect sys.stdout/sys.stderr.
29
+ _real_stdout = sys.stdout
30
+ _real_stdin = sys.stdin
31
+
32
+ # Lock for stdout writes only
33
+ _write_lock = threading.Lock()
34
+
35
+ # Per-request events and results for concurrent llm_query/thread calls
36
+ _pending_results: dict[str, threading.Event] = {}
37
+ _result_store: dict[str, str] = {}
38
+
39
+ # Queue for commands (exec, set_context, etc.) dispatched by the reader thread
40
+ _command_queue: queue.Queue = queue.Queue()
41
+
42
+ # Will be set by the TypeScript host before each execution
43
+ context: str = ""
44
+
45
+ # Sentinel — when set to a non-None value, the loop terminates
46
+ __final_result__ = None
47
+
48
+ # User execution namespace — isolates LLM code from REPL internals
49
+ _user_ns: dict = {}
50
+
51
+
52
+ def FINAL(x):
53
+ """Set the final answer as a string and terminate the RLM loop."""
54
+ global __final_result__
55
+ if __final_result__ is not None:
56
+ print(f"[Warning] FINAL() called again — overwriting previous answer", file=sys.stderr)
57
+ __final_result__ = str(x)
58
+
59
+
60
+ def FINAL_VAR(x):
61
+ """Set the final answer from a variable and terminate the RLM loop."""
62
+ global __final_result__
63
+ if __final_result__ is not None and x is not None:
64
+ print(f"[Warning] FINAL_VAR() called again — overwriting previous answer", file=sys.stderr)
65
+ __final_result__ = str(x) if x is not None else None
66
+
67
+
68
+ def _stdin_reader_loop() -> None:
69
+ """Dedicated thread: reads all stdin lines and dispatches them.
70
+
71
+ - llm_result/thread_result/merge_result messages go to waiting threads
72
+ - All other messages (exec, set_context, etc.) go to _command_queue
73
+ """
74
+ while True:
75
+ try:
76
+ line = _real_stdin.readline()
77
+ except Exception:
78
+ break
79
+ if not line:
80
+ # stdin closed — wake all pending threads and signal main loop
81
+ for event in list(_pending_results.values()):
82
+ event.set()
83
+ _command_queue.put(None)
84
+ break
85
+ line = line.strip()
86
+ if not line:
87
+ continue
88
+ try:
89
+ msg = json.loads(line)
90
+ except json.JSONDecodeError:
91
+ continue
92
+
93
+ msg_type = msg.get("type")
94
+ if msg_type in ("llm_result", "thread_result", "merge_result"):
95
+ rid = msg.get("id", "")
96
+ if rid in _pending_results:
97
+ _result_store[rid] = json.dumps(msg) if msg_type != "llm_result" else msg.get("result", "")
98
+ _pending_results[rid].set()
99
+ elif msg_type == "shutdown":
100
+ _command_queue.put(None)
101
+ break
102
+ else:
103
+ _command_queue.put(msg)
104
+
105
+
106
+ def llm_query(sub_context: str, instruction: str = "") -> str:
107
+ """Send a sub-context and instruction to the parent LLM and return the response."""
108
+ if not instruction:
109
+ instruction = ""
110
+
111
+ request_id = uuid.uuid4().hex[:12]
112
+ event = threading.Event()
113
+ _pending_results[request_id] = event
114
+
115
+ request = {
116
+ "type": "llm_query",
117
+ "sub_context": sub_context,
118
+ "instruction": instruction,
119
+ "id": request_id,
120
+ }
121
+ with _write_lock:
122
+ _real_stdout.write(json.dumps(request) + "\n")
123
+ _real_stdout.flush()
124
+
125
+ event.wait()
126
+ _pending_results.pop(request_id, None)
127
+ result = _result_store.pop(request_id, None)
128
+ if result is None:
129
+ raise RuntimeError("Host process disconnected during llm_query — stdin closed")
130
+ return result
131
+
132
+
133
+ async def async_llm_query(sub_context: str, instruction: str = "") -> str:
134
+ """Async wrapper around llm_query for use with asyncio.gather()."""
135
+ return await asyncio.get_event_loop().run_in_executor(None, llm_query, sub_context, instruction)
136
+
137
+
138
+ def thread(task: str, context: str = "", agent: str = "opencode", model: str = "", files=None) -> str:
139
+ """Spawn a coding agent thread in an isolated git worktree.
140
+
141
+ Args:
142
+ task: What the agent should do (be specific)
143
+ context: Additional context to pass to the agent
144
+ agent: Agent backend name ("opencode", "direct-llm", etc.)
145
+ model: Model ID in provider/model-id format (e.g., "anthropic/claude-sonnet-4-6")
146
+ files: List of relevant file paths (hints for the agent)
147
+
148
+ Returns:
149
+ Compressed result string with status, files changed, diff, and output summary.
150
+ """
151
+ if files is None:
152
+ files = []
153
+
154
+ request_id = uuid.uuid4().hex[:12]
155
+ event = threading.Event()
156
+ _pending_results[request_id] = event
157
+
158
+ request = {
159
+ "type": "thread_request",
160
+ "id": request_id,
161
+ "task": task,
162
+ "context": context,
163
+ "agent_backend": agent,
164
+ "model": model,
165
+ "files": files,
166
+ }
167
+ with _write_lock:
168
+ _real_stdout.write(json.dumps(request) + "\n")
169
+ _real_stdout.flush()
170
+
171
+ event.wait()
172
+ _pending_results.pop(request_id, None)
173
+
174
+ raw = _result_store.pop(request_id, None)
175
+ if raw is None:
176
+ raise RuntimeError("Host process disconnected during thread() — stdin closed")
177
+ try:
178
+ result_msg = json.loads(raw)
179
+ return result_msg.get("result", raw)
180
+ except (json.JSONDecodeError, AttributeError):
181
+ return raw
182
+
183
+
184
+ async def async_thread(task: str, context: str = "", agent: str = "opencode", model: str = "", files=None) -> str:
185
+ """Async version of thread() for use with asyncio.gather().
186
+
187
+ Usage:
188
+ import asyncio
189
+ results = await asyncio.gather(
190
+ async_thread("fix auth", files=["src/auth.ts"]),
191
+ async_thread("fix routing", files=["src/router.ts"]),
192
+ )
193
+ """
194
+ if files is None:
195
+ files = []
196
+ return await asyncio.get_event_loop().run_in_executor(
197
+ None, lambda: thread(task, context, agent, model, files)
198
+ )
199
+
200
+
201
+ def merge_threads() -> str:
202
+ """Merge all completed thread branches back into the main branch.
203
+
204
+ Returns:
205
+ Merge status string.
206
+ """
207
+ request_id = uuid.uuid4().hex[:12]
208
+ event = threading.Event()
209
+ _pending_results[request_id] = event
210
+
211
+ request = {
212
+ "type": "merge_request",
213
+ "id": request_id,
214
+ }
215
+ with _write_lock:
216
+ _real_stdout.write(json.dumps(request) + "\n")
217
+ _real_stdout.flush()
218
+
219
+ event.wait()
220
+ _pending_results.pop(request_id, None)
221
+
222
+ raw = _result_store.pop(request_id, None)
223
+ if raw is None:
224
+ raise RuntimeError("Host process disconnected during merge_threads() — stdin closed")
225
+ try:
226
+ result_msg = json.loads(raw)
227
+ return result_msg.get("result", raw)
228
+ except (json.JSONDecodeError, AttributeError):
229
+ return raw
230
+
231
+
232
+ def _runtime_symbols() -> dict:
233
+ """Return the dict of runtime symbols injected into the user namespace."""
234
+ return {
235
+ '__builtins__': __builtins__,
236
+ 'context': context,
237
+ 'llm_query': llm_query,
238
+ 'async_llm_query': async_llm_query,
239
+ 'thread': thread,
240
+ 'async_thread': async_thread,
241
+ 'merge_threads': merge_threads,
242
+ 'FINAL': FINAL,
243
+ 'FINAL_VAR': FINAL_VAR,
244
+ }
245
+
246
+
247
+ def _refresh_user_ns() -> None:
248
+ """Ensure the user namespace has the latest runtime symbols."""
249
+ _user_ns.update(_runtime_symbols())
250
+
251
+
252
+ def _execute_code(code: str) -> None:
253
+ """Execute a code snippet in an isolated namespace, capturing output."""
254
+ global __final_result__
255
+ _refresh_user_ns()
256
+ captured_stdout = io.StringIO()
257
+ captured_stderr = io.StringIO()
258
+ old_stdout = sys.stdout
259
+ old_stderr = sys.stderr
260
+
261
+ try:
262
+ sys.stdout = captured_stdout
263
+ sys.stderr = captured_stderr
264
+ try:
265
+ compiled = compile(code, "<repl>", "exec")
266
+ exec(compiled, _user_ns)
267
+ except SyntaxError as e:
268
+ if "await" in str(code):
269
+ _protected = set(_runtime_symbols().keys())
270
+ async_code = "async def __async_exec__():\n"
271
+ for line in code.split("\n"):
272
+ async_code += f" {line}\n"
273
+ async_code += " return {k: v for k, v in locals().items()}\n"
274
+ async_code += "\nimport asyncio as _asyncio\n"
275
+ async_code += "_async_locals = _asyncio.run(__async_exec__())\n"
276
+ async_code += f"globals().update({{k: v for k, v in _async_locals.items() if k not in {_protected!r}}})\n"
277
+ exec(compile(async_code, "<repl>", "exec"), _user_ns)
278
+ else:
279
+ raise e
280
+ except Exception:
281
+ traceback.print_exc(file=captured_stderr)
282
+ finally:
283
+ sys.stdout = old_stdout
284
+ sys.stderr = old_stderr
285
+
286
+ stdout_val = captured_stdout.getvalue()
287
+ stderr_val = captured_stderr.getvalue()
288
+
289
+ result = {
290
+ "type": "exec_done",
291
+ "stdout": stdout_val,
292
+ "stderr": stderr_val,
293
+ "has_final": __final_result__ is not None,
294
+ "final_value": str(__final_result__) if __final_result__ is not None else None,
295
+ }
296
+ with _write_lock:
297
+ _real_stdout.write(json.dumps(result) + "\n")
298
+ _real_stdout.flush()
299
+
300
+
301
+ def _main_loop() -> None:
302
+ """Process commands from the queue (fed by the stdin reader thread)."""
303
+ while True:
304
+ msg = _command_queue.get()
305
+ if msg is None:
306
+ break
307
+
308
+ if msg.get("type") == "exec":
309
+ _execute_code(msg.get("code", ""))
310
+ elif msg.get("type") == "set_context":
311
+ global context
312
+ context = msg.get("value", "")
313
+ ack = {"type": "context_set"}
314
+ with _write_lock:
315
+ _real_stdout.write(json.dumps(ack) + "\n")
316
+ _real_stdout.flush()
317
+ elif msg.get("type") == "reset_final":
318
+ global __final_result__
319
+ __final_result__ = None
320
+ ack = {"type": "final_reset"}
321
+ with _write_lock:
322
+ _real_stdout.write(json.dumps(ack) + "\n")
323
+ _real_stdout.flush()
324
+
325
+
326
+ if __name__ == "__main__":
327
+ ready = {"type": "ready"}
328
+ _real_stdout.write(json.dumps(ready) + "\n")
329
+ _real_stdout.flush()
330
+
331
+ # Start dedicated stdin reader thread
332
+ reader = threading.Thread(target=_stdin_reader_loop, daemon=True)
333
+ reader.start()
334
+
335
+ _main_loop()
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Shared type definitions for swarm-code.
3
+ */
4
+ /** Token usage reported by the agent (when available). */
5
+ export interface TokenUsage {
6
+ inputTokens: number;
7
+ outputTokens: number;
8
+ totalTokens: number;
9
+ }
10
+ export interface AgentResult {
11
+ success: boolean;
12
+ output: string;
13
+ filesChanged: string[];
14
+ diff: string;
15
+ durationMs: number;
16
+ error?: string;
17
+ /** Actual token usage from the agent (if reported). */
18
+ usage?: TokenUsage;
19
+ }
20
+ export interface AgentRunOptions {
21
+ task: string;
22
+ workDir: string;
23
+ model?: string;
24
+ files?: string[];
25
+ signal?: AbortSignal;
26
+ onOutput?: (chunk: string) => void;
27
+ }
28
+ export interface AgentProvider {
29
+ readonly name: string;
30
+ readonly isAvailable: () => Promise<boolean>;
31
+ run(options: AgentRunOptions): Promise<AgentResult>;
32
+ }
33
+ export type ThreadStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
34
+ export type ThreadProgressPhase = "queued" | "creating_worktree" | "agent_running" | "capturing_diff" | "compressing" | "completed" | "failed" | "cancelled" | "retrying";
35
+ export interface ThreadConfig {
36
+ id: string;
37
+ task: string;
38
+ context: string;
39
+ agent: {
40
+ backend: string;
41
+ model: string;
42
+ };
43
+ files?: string[];
44
+ }
45
+ export interface ThreadState {
46
+ id: string;
47
+ config: ThreadConfig;
48
+ status: ThreadStatus;
49
+ phase: ThreadProgressPhase;
50
+ worktreePath?: string;
51
+ branchName?: string;
52
+ result?: CompressedResult;
53
+ startedAt?: number;
54
+ completedAt?: number;
55
+ error?: string;
56
+ attempt: number;
57
+ maxAttempts: number;
58
+ estimatedCostUsd: number;
59
+ }
60
+ export interface CompressedResult {
61
+ success: boolean;
62
+ summary: string;
63
+ filesChanged: string[];
64
+ diffStats: string;
65
+ durationMs: number;
66
+ estimatedCostUsd: number;
67
+ /** Actual token usage (when available from agent). */
68
+ usage?: TokenUsage;
69
+ /** Whether cost is based on real usage or estimates. */
70
+ costIsEstimate?: boolean;
71
+ }
72
+ export interface WorktreeInfo {
73
+ id: string;
74
+ path: string;
75
+ branch: string;
76
+ }
77
+ export interface MergeResult {
78
+ success: boolean;
79
+ branch: string;
80
+ conflicts: string[];
81
+ conflictDiff: string;
82
+ message: string;
83
+ }
84
+ export interface BudgetState {
85
+ totalSpentUsd: number;
86
+ threadCosts: Map<string, number>;
87
+ sessionLimitUsd: number;
88
+ perThreadLimitUsd: number;
89
+ /** Total tokens consumed across all threads. */
90
+ totalTokens: {
91
+ input: number;
92
+ output: number;
93
+ };
94
+ /** Number of threads with actual (non-estimated) cost data. */
95
+ actualCostThreads: number;
96
+ /** Number of threads with estimated cost data. */
97
+ estimatedCostThreads: number;
98
+ }
99
+ /** Rough per-1M-token pricing for cost estimation. */
100
+ export declare const MODEL_PRICING: Record<string, {
101
+ input: number;
102
+ output: number;
103
+ }>;
104
+ export type { ModelSlots, SwarmConfig } from "../config.js";
105
+ export interface ThreadRequestMessage {
106
+ type: "thread_request";
107
+ id: string;
108
+ task: string;
109
+ context: string;
110
+ agent_backend: string;
111
+ model: string;
112
+ files: string[];
113
+ }
114
+ export interface ThreadResultMessage {
115
+ type: "thread_result";
116
+ id: string;
117
+ result: string;
118
+ success: boolean;
119
+ files_changed: string[];
120
+ duration_ms: number;
121
+ }
122
+ export interface MergeRequestMessage {
123
+ type: "merge_request";
124
+ id: string;
125
+ }
126
+ export interface MergeResultMessage {
127
+ type: "merge_result";
128
+ id: string;
129
+ result: string;
130
+ success: boolean;
131
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared type definitions for swarm-code.
3
+ */
4
+ /** Rough per-1M-token pricing for cost estimation. */
5
+ export const MODEL_PRICING = {
6
+ // Anthropic
7
+ "claude-sonnet-4-6": { input: 3, output: 15 },
8
+ "claude-opus-4-6": { input: 15, output: 75 },
9
+ "claude-haiku-4-5": { input: 0.8, output: 4 },
10
+ // OpenAI
11
+ "gpt-4o": { input: 2.5, output: 10 },
12
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
13
+ o3: { input: 10, output: 40 },
14
+ "o3-mini": { input: 1.1, output: 4.4 },
15
+ // Google
16
+ "gemini-2.5-pro": { input: 1.25, output: 10 },
17
+ "gemini-2.5-flash": { input: 0.15, output: 0.6 },
18
+ };
19
+ //# sourceMappingURL=types.js.map
package/dist/env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Load env vars into process.env.
3
+ * Must be imported BEFORE any module that reads env vars (e.g. pi-ai).
4
+ *
5
+ * Priority (highest wins):
6
+ * 1. Shell environment variables (already in process.env)
7
+ * 2. .env in package root
8
+ * 3. ~/.rlm/credentials — persistent keys saved by first-run setup
9
+ */
10
+ export {};
package/dist/env.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Load env vars into process.env.
3
+ * Must be imported BEFORE any module that reads env vars (e.g. pi-ai).
4
+ *
5
+ * Priority (highest wins):
6
+ * 1. Shell environment variables (already in process.env)
7
+ * 2. .env in package root
8
+ * 3. ~/.rlm/credentials — persistent keys saved by first-run setup
9
+ */
10
+ import * as fs from "node:fs";
11
+ import * as os from "node:os";
12
+ import * as path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ function parseEnvFile(filePath) {
15
+ const vars = new Map();
16
+ if (!fs.existsSync(filePath))
17
+ return vars;
18
+ const content = fs.readFileSync(filePath, "utf-8");
19
+ for (const rawLine of content.split("\n")) {
20
+ let trimmed = rawLine.trim();
21
+ if (!trimmed || trimmed.startsWith("#"))
22
+ continue;
23
+ // Strip leading "export " (common in .env files)
24
+ if (trimmed.startsWith("export "))
25
+ trimmed = trimmed.slice(7);
26
+ const eqIndex = trimmed.indexOf("=");
27
+ if (eqIndex === -1)
28
+ continue;
29
+ const key = trimmed.slice(0, eqIndex).trim();
30
+ let value = trimmed.slice(eqIndex + 1).trim();
31
+ // Strip matching surrounding quotes ("..." or '...')
32
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
33
+ value = value.slice(1, -1);
34
+ }
35
+ if (key)
36
+ vars.set(key, value);
37
+ }
38
+ return vars;
39
+ }
40
+ // Collect file-based vars: credentials first, .env overwrites
41
+ const fileVars = new Map();
42
+ // 1. Load persistent credentials (lowest priority file)
43
+ // Check both swarm and rlm credential locations
44
+ const credVars = parseEnvFile(path.join(os.homedir(), ".swarm", "credentials"));
45
+ const rlmCreds = parseEnvFile(path.join(os.homedir(), ".rlm", "credentials"));
46
+ for (const [k, v] of rlmCreds) {
47
+ if (!credVars.has(k))
48
+ credVars.set(k, v);
49
+ }
50
+ for (const [k, v] of credVars)
51
+ fileVars.set(k, v);
52
+ // 2. Load .env from package root (overwrites credentials)
53
+ const __dir = path.dirname(fileURLToPath(import.meta.url));
54
+ const dotenvVars = parseEnvFile(path.resolve(__dir, "..", ".env"));
55
+ for (const [k, v] of dotenvVars)
56
+ fileVars.set(k, v);
57
+ // 3. Apply: only set if NOT already in shell env (shell always wins)
58
+ for (const [key, value] of fileVars) {
59
+ if (!process.env[key]) {
60
+ process.env[key] = value;
61
+ }
62
+ }
63
+ // Alias: GOOGLE_API_KEY → GEMINI_API_KEY (pi-ai expects GEMINI_API_KEY)
64
+ // Remove GOOGLE_API_KEY after aliasing to avoid pi-ai "both set" warning
65
+ if (process.env.GOOGLE_API_KEY) {
66
+ if (!process.env.GEMINI_API_KEY) {
67
+ process.env.GEMINI_API_KEY = process.env.GOOGLE_API_KEY;
68
+ }
69
+ delete process.env.GOOGLE_API_KEY;
70
+ }
71
+ // Default model
72
+ if (!process.env.RLM_MODEL) {
73
+ process.env.RLM_MODEL = "claude-sonnet-4-6";
74
+ }
75
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Interactive swarm REPL — a persistent session for follow-up tasks,
3
+ * thread inspection, manual merge/reject, and live DAG visualization.
4
+ *
5
+ * Usage: swarm --dir ./project (no query argument enters interactive mode)
6
+ *
7
+ * Commands:
8
+ * <task> Run a task through the RLM orchestrator
9
+ * /threads (/t) List all threads with status, cost, duration
10
+ * /thread <id> Show detailed info for a specific thread
11
+ * /merge [id...] Merge specific thread branches (or all if no args)
12
+ * /reject <id> Discard a thread's worktree and branch
13
+ * /dag Show thread dependency DAG with status indicators
14
+ * /budget Show budget state
15
+ * /status Overall session status
16
+ * /help Show available commands
17
+ * /quit (/exit) Cleanup and exit
18
+ */
19
+ import "./env.js";
20
+ export declare function runInteractiveSwarm(rawArgs: string[]): Promise<void>;