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.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/bin/swarm.mjs +45 -0
- package/dist/agents/aider.d.ts +12 -0
- package/dist/agents/aider.js +182 -0
- package/dist/agents/claude-code.d.ts +9 -0
- package/dist/agents/claude-code.js +216 -0
- package/dist/agents/codex.d.ts +14 -0
- package/dist/agents/codex.js +193 -0
- package/dist/agents/direct-llm.d.ts +9 -0
- package/dist/agents/direct-llm.js +78 -0
- package/dist/agents/mock.d.ts +9 -0
- package/dist/agents/mock.js +77 -0
- package/dist/agents/opencode.d.ts +23 -0
- package/dist/agents/opencode.js +571 -0
- package/dist/agents/provider.d.ts +11 -0
- package/dist/agents/provider.js +31 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +285 -0
- package/dist/compression/compressor.d.ts +28 -0
- package/dist/compression/compressor.js +265 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +170 -0
- package/dist/core/repl.d.ts +69 -0
- package/dist/core/repl.js +336 -0
- package/dist/core/rlm.d.ts +63 -0
- package/dist/core/rlm.js +409 -0
- package/dist/core/runtime.py +335 -0
- package/dist/core/types.d.ts +131 -0
- package/dist/core/types.js +19 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +75 -0
- package/dist/interactive-swarm.d.ts +20 -0
- package/dist/interactive-swarm.js +1041 -0
- package/dist/interactive.d.ts +10 -0
- package/dist/interactive.js +1765 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +242 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.js +72 -0
- package/dist/mcp/session.d.ts +73 -0
- package/dist/mcp/session.js +184 -0
- package/dist/mcp/tools.d.ts +15 -0
- package/dist/mcp/tools.js +377 -0
- package/dist/memory/episodic.d.ts +132 -0
- package/dist/memory/episodic.js +390 -0
- package/dist/prompts/orchestrator.d.ts +5 -0
- package/dist/prompts/orchestrator.js +191 -0
- package/dist/routing/model-router.d.ts +130 -0
- package/dist/routing/model-router.js +515 -0
- package/dist/swarm.d.ts +14 -0
- package/dist/swarm.js +557 -0
- package/dist/threads/cache.d.ts +58 -0
- package/dist/threads/cache.js +198 -0
- package/dist/threads/manager.d.ts +85 -0
- package/dist/threads/manager.js +659 -0
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +42 -0
- package/dist/ui/dashboard.d.ts +33 -0
- package/dist/ui/dashboard.js +151 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.js +11 -0
- package/dist/ui/log.d.ts +39 -0
- package/dist/ui/log.js +126 -0
- package/dist/ui/onboarding.d.ts +14 -0
- package/dist/ui/onboarding.js +518 -0
- package/dist/ui/spinner.d.ts +25 -0
- package/dist/ui/spinner.js +113 -0
- package/dist/ui/summary.d.ts +18 -0
- package/dist/ui/summary.js +113 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.js +97 -0
- package/dist/viewer.d.ts +12 -0
- package/dist/viewer.js +1284 -0
- package/dist/worktree/manager.d.ts +45 -0
- package/dist/worktree/manager.js +266 -0
- package/dist/worktree/merge.d.ts +28 -0
- package/dist/worktree/merge.js +138 -0
- 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>;
|