contextos-mcp 0.4.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 +260 -0
- package/bin/mcp.mjs +61 -0
- package/bin/swarm.mjs +45 -0
- package/dist/agents/aider.d.ts +12 -0
- package/dist/agents/aider.js +192 -0
- package/dist/agents/aider.js.map +1 -0
- package/dist/agents/claude-code.d.ts +9 -0
- package/dist/agents/claude-code.js +236 -0
- package/dist/agents/claude-code.js.map +1 -0
- package/dist/agents/codex.d.ts +14 -0
- package/dist/agents/codex.js +203 -0
- package/dist/agents/codex.js.map +1 -0
- package/dist/agents/direct-llm.d.ts +22 -0
- package/dist/agents/direct-llm.js +254 -0
- package/dist/agents/direct-llm.js.map +1 -0
- package/dist/agents/mock.d.ts +10 -0
- package/dist/agents/mock.js +170 -0
- package/dist/agents/mock.js.map +1 -0
- package/dist/agents/opencode.d.ts +23 -0
- package/dist/agents/opencode.js +631 -0
- package/dist/agents/opencode.js.map +1 -0
- package/dist/agents/provider.d.ts +11 -0
- package/dist/agents/provider.js +31 -0
- package/dist/agents/provider.js.map +1 -0
- package/dist/compression/compressor.d.ts +28 -0
- package/dist/compression/compressor.js +268 -0
- package/dist/compression/compressor.js.map +1 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +172 -0
- package/dist/config.js.map +1 -0
- package/dist/contextos/loader.d.ts +46 -0
- package/dist/contextos/loader.js +120 -0
- package/dist/contextos/loader.js.map +1 -0
- package/dist/contextos/selector.d.ts +46 -0
- package/dist/contextos/selector.js +80 -0
- package/dist/contextos/selector.js.map +1 -0
- package/dist/core/action-dispatcher.d.ts +15 -0
- package/dist/core/action-dispatcher.js +28 -0
- package/dist/core/action-dispatcher.js.map +1 -0
- package/dist/core/action-schema.d.ts +72 -0
- package/dist/core/action-schema.js +86 -0
- package/dist/core/action-schema.js.map +1 -0
- package/dist/core/repl-interface.d.ts +48 -0
- package/dist/core/repl-interface.js +5 -0
- package/dist/core/repl-interface.js.map +1 -0
- package/dist/core/repl.d.ts +59 -0
- package/dist/core/repl.js +353 -0
- package/dist/core/repl.js.map +1 -0
- package/dist/core/rlm.d.ts +65 -0
- package/dist/core/rlm.js +564 -0
- package/dist/core/rlm.js.map +1 -0
- package/dist/core/runtime.py +470 -0
- package/dist/core/types.d.ts +265 -0
- package/dist/core/types.js +19 -0
- package/dist/core/types.js.map +1 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +75 -0
- package/dist/env.js.map +1 -0
- package/dist/hooks/runner.d.ts +43 -0
- package/dist/hooks/runner.js +173 -0
- package/dist/hooks/runner.js.map +1 -0
- package/dist/labs/cli.d.ts +15 -0
- package/dist/labs/cli.js +295 -0
- package/dist/labs/cli.js.map +1 -0
- package/dist/labs/episodic-memory.d.ts +132 -0
- package/dist/labs/episodic-memory.js +391 -0
- package/dist/labs/episodic-memory.js.map +1 -0
- package/dist/labs/interactive-swarm.d.ts +20 -0
- package/dist/labs/interactive-swarm.js +1173 -0
- package/dist/labs/interactive-swarm.js.map +1 -0
- package/dist/labs/interactive.d.ts +10 -0
- package/dist/labs/interactive.js +2099 -0
- package/dist/labs/interactive.js.map +1 -0
- package/dist/labs/ts-analyzer.d.ts +74 -0
- package/dist/labs/ts-analyzer.js +379 -0
- package/dist/labs/ts-analyzer.js.map +1 -0
- package/dist/labs/viewer.d.ts +12 -0
- package/dist/labs/viewer.js +1284 -0
- package/dist/labs/viewer.js.map +1 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +247 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp/server.d.ts +16 -0
- package/dist/mcp/server.js +192775 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +117 -0
- package/dist/mcp/session.js +450 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/state.d.ts +42 -0
- package/dist/mcp/state.js +537 -0
- package/dist/mcp/state.js.map +1 -0
- package/dist/mcp/tools/contextos.d.ts +17 -0
- package/dist/mcp/tools/contextos.js +583 -0
- package/dist/mcp/tools/contextos.js.map +1 -0
- package/dist/mcp/tools.d.ts +15 -0
- package/dist/mcp/tools.js +393 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/orchestration/reviewer-gate.d.ts +43 -0
- package/dist/orchestration/reviewer-gate.js +280 -0
- package/dist/orchestration/reviewer-gate.js.map +1 -0
- package/dist/orchestration/reviewer-prompt.d.ts +18 -0
- package/dist/orchestration/reviewer-prompt.js +67 -0
- package/dist/orchestration/reviewer-prompt.js.map +1 -0
- package/dist/orchestration/verification-runner.d.ts +53 -0
- package/dist/orchestration/verification-runner.js +352 -0
- package/dist/orchestration/verification-runner.js.map +1 -0
- package/dist/plugins/adapter.d.ts +11 -0
- package/dist/plugins/adapter.js +11 -0
- package/dist/plugins/adapter.js.map +1 -0
- package/dist/plugins/core/archive.d.ts +9 -0
- package/dist/plugins/core/archive.js +128 -0
- package/dist/plugins/core/archive.js.map +1 -0
- package/dist/plugins/core/digest.d.ts +11 -0
- package/dist/plugins/core/digest.js +70 -0
- package/dist/plugins/core/digest.js.map +1 -0
- package/dist/plugins/core/grants.d.ts +22 -0
- package/dist/plugins/core/grants.js +90 -0
- package/dist/plugins/core/grants.js.map +1 -0
- package/dist/plugins/core/index.d.ts +10 -0
- package/dist/plugins/core/index.js +11 -0
- package/dist/plugins/core/index.js.map +1 -0
- package/dist/plugins/core/pinning.d.ts +15 -0
- package/dist/plugins/core/pinning.js +86 -0
- package/dist/plugins/core/pinning.js.map +1 -0
- package/dist/plugins/core/types.d.ts +105 -0
- package/dist/plugins/core/types.js +22 -0
- package/dist/plugins/core/types.js.map +1 -0
- package/dist/plugins/core/updater.d.ts +16 -0
- package/dist/plugins/core/updater.js +64 -0
- package/dist/plugins/core/updater.js.map +1 -0
- package/dist/plugins/loader.d.ts +65 -0
- package/dist/plugins/loader.js +110 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/prompts/orchestrator.d.ts +6 -0
- package/dist/prompts/orchestrator.js +114 -0
- package/dist/prompts/orchestrator.js.map +1 -0
- package/dist/routing/model-resolver.d.ts +27 -0
- package/dist/routing/model-resolver.js +265 -0
- package/dist/routing/model-resolver.js.map +1 -0
- package/dist/routing/model-router.d.ts +130 -0
- package/dist/routing/model-router.js +515 -0
- package/dist/routing/model-router.js.map +1 -0
- package/dist/runtime/attestations.cjs +260 -0
- package/dist/runtime/reviewer-pipeline.cjs +319 -0
- package/dist/runtime/sandbox.cjs +468 -0
- package/dist/runtime/state-machine.cjs +885 -0
- package/dist/runtime/thread-store.cjs +175 -0
- package/dist/runtime/transactional-git.cjs +315 -0
- package/dist/runtime/verification-pipeline.cjs +435 -0
- package/dist/sandbox/execution.d.ts +165 -0
- package/dist/sandbox/execution.js +497 -0
- package/dist/sandbox/execution.js.map +1 -0
- package/dist/security/file-policy.d.ts +22 -0
- package/dist/security/file-policy.js +53 -0
- package/dist/security/file-policy.js.map +1 -0
- package/dist/security/repository-boundary.d.ts +21 -0
- package/dist/security/repository-boundary.js +119 -0
- package/dist/security/repository-boundary.js.map +1 -0
- package/dist/security/secret-filter.d.ts +36 -0
- package/dist/security/secret-filter.js +149 -0
- package/dist/security/secret-filter.js.map +1 -0
- package/dist/swarm.d.ts +14 -0
- package/dist/swarm.js +624 -0
- package/dist/swarm.js.map +1 -0
- package/dist/threads/cache.d.ts +64 -0
- package/dist/threads/cache.js +241 -0
- package/dist/threads/cache.js.map +1 -0
- package/dist/threads/manager.d.ts +94 -0
- package/dist/threads/manager.js +914 -0
- package/dist/threads/manager.js.map +1 -0
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +42 -0
- package/dist/ui/banner.js.map +1 -0
- package/dist/ui/dashboard.d.ts +33 -0
- package/dist/ui/dashboard.js +135 -0
- package/dist/ui/dashboard.js.map +1 -0
- package/dist/ui/index.d.ts +11 -0
- package/dist/ui/index.js +12 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/log.d.ts +39 -0
- package/dist/ui/log.js +126 -0
- package/dist/ui/log.js.map +1 -0
- package/dist/ui/onboarding.d.ts +14 -0
- package/dist/ui/onboarding.js +755 -0
- package/dist/ui/onboarding.js.map +1 -0
- package/dist/ui/run-log.d.ts +50 -0
- package/dist/ui/run-log.js +60 -0
- package/dist/ui/run-log.js.map +1 -0
- package/dist/ui/spinner.d.ts +41 -0
- package/dist/ui/spinner.js +172 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/ui/streaming-feed.d.ts +72 -0
- package/dist/ui/streaming-feed.js +386 -0
- package/dist/ui/streaming-feed.js.map +1 -0
- package/dist/ui/summary.d.ts +18 -0
- package/dist/ui/summary.js +113 -0
- package/dist/ui/summary.js.map +1 -0
- package/dist/ui/text-input.d.ts +19 -0
- package/dist/ui/text-input.js +266 -0
- package/dist/ui/text-input.js.map +1 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.js +97 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/utils/command-exists.d.ts +11 -0
- package/dist/utils/command-exists.js +41 -0
- package/dist/utils/command-exists.js.map +1 -0
- package/dist/utils/process-security.d.ts +19 -0
- package/dist/utils/process-security.js +90 -0
- package/dist/utils/process-security.js.map +1 -0
- package/dist/utils/rolling-buffer.d.ts +16 -0
- package/dist/utils/rolling-buffer.js +30 -0
- package/dist/utils/rolling-buffer.js.map +1 -0
- package/dist/utils/safe-spawn.d.ts +13 -0
- package/dist/utils/safe-spawn.js +49 -0
- package/dist/utils/safe-spawn.js.map +1 -0
- package/dist/worktree/manager.d.ts +69 -0
- package/dist/worktree/manager.js +593 -0
- package/dist/worktree/manager.js.map +1 -0
- package/dist/worktree/merge.d.ts +58 -0
- package/dist/worktree/merge.js +389 -0
- package/dist/worktree/merge.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,1173 @@
|
|
|
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
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as os from "node:os";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import { readTextInput } from "../ui/text-input.js";
|
|
24
|
+
// Dynamic imports — ensures env.js has set process.env BEFORE pi-ai loads
|
|
25
|
+
await import("@mariozechner/pi-ai");
|
|
26
|
+
const { PythonRepl } = await import("../core/repl.js");
|
|
27
|
+
const { runRlmLoop } = await import("../core/rlm.js");
|
|
28
|
+
const { loadConfig } = await import("../config.js");
|
|
29
|
+
// Register agent backends
|
|
30
|
+
const opencodeMod = await import("../agents/opencode.js");
|
|
31
|
+
await import("../agents/direct-llm.js");
|
|
32
|
+
await import("../agents/claude-code.js");
|
|
33
|
+
await import("../agents/codex.js");
|
|
34
|
+
await import("../agents/aider.js");
|
|
35
|
+
import { randomBytes } from "node:crypto";
|
|
36
|
+
import { EpisodicMemory } from "../labs/episodic-memory.js";
|
|
37
|
+
import { buildSwarmSystemPrompt } from "../prompts/orchestrator.js";
|
|
38
|
+
import { resolveModel } from "../routing/model-resolver.js";
|
|
39
|
+
import { classifyTaskComplexity, describeAvailableAgents, FailureTracker, routeTask } from "../routing/model-router.js";
|
|
40
|
+
import { ThreadManager } from "../threads/manager.js";
|
|
41
|
+
import { ThreadDashboard } from "../ui/dashboard.js";
|
|
42
|
+
import { logError, logRouter, logSuccess, logVerbose, logWarn, setLogLevel } from "../ui/log.js";
|
|
43
|
+
import { runOnboarding } from "../ui/onboarding.js";
|
|
44
|
+
import { RunLogger } from "../ui/run-log.js";
|
|
45
|
+
// UI system
|
|
46
|
+
import { Spinner } from "../ui/spinner.js";
|
|
47
|
+
import { StreamingFeed } from "../ui/streaming-feed.js";
|
|
48
|
+
import { bold, coral, cyan, dim, green, isTTY, red, symbols, termWidth, truncate, yellow } from "../ui/theme.js";
|
|
49
|
+
import { mergeAllThreads, mergeThreadBranch } from "../worktree/merge.js";
|
|
50
|
+
function parseInteractiveArgs(args) {
|
|
51
|
+
let dir = "";
|
|
52
|
+
let orchestratorModel = "";
|
|
53
|
+
let agent = "";
|
|
54
|
+
let maxBudget = null;
|
|
55
|
+
let verbose = false;
|
|
56
|
+
let quiet = false;
|
|
57
|
+
let autoRoute = false;
|
|
58
|
+
for (let i = 0; i < args.length; i++) {
|
|
59
|
+
const arg = args[i];
|
|
60
|
+
if (arg === "--dir" && i + 1 < args.length) {
|
|
61
|
+
dir = args[++i];
|
|
62
|
+
}
|
|
63
|
+
else if (arg === "--orchestrator" && i + 1 < args.length) {
|
|
64
|
+
orchestratorModel = args[++i];
|
|
65
|
+
}
|
|
66
|
+
else if (arg === "--agent" && i + 1 < args.length) {
|
|
67
|
+
agent = args[++i];
|
|
68
|
+
}
|
|
69
|
+
else if (arg === "--max-budget" && i + 1 < args.length) {
|
|
70
|
+
const raw = args[++i];
|
|
71
|
+
const parsed = parseFloat(raw);
|
|
72
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
73
|
+
maxBudget = parsed;
|
|
74
|
+
}
|
|
75
|
+
else if (arg === "--verbose") {
|
|
76
|
+
verbose = true;
|
|
77
|
+
}
|
|
78
|
+
else if (arg === "--quiet" || arg === "-q") {
|
|
79
|
+
quiet = true;
|
|
80
|
+
}
|
|
81
|
+
else if (arg === "--auto-route") {
|
|
82
|
+
autoRoute = true;
|
|
83
|
+
}
|
|
84
|
+
// Silently ignore unknown flags and positional args
|
|
85
|
+
}
|
|
86
|
+
if (!dir) {
|
|
87
|
+
dir = process.cwd();
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
dir: path.resolve(dir),
|
|
91
|
+
orchestratorModel: orchestratorModel || process.env.RLM_MODEL || "claude-sonnet-4-6",
|
|
92
|
+
agent: agent || "",
|
|
93
|
+
maxBudget,
|
|
94
|
+
verbose,
|
|
95
|
+
quiet,
|
|
96
|
+
autoRoute,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// ── Codebase scanning (mirrored from swarm.ts) ──────────────────────────────
|
|
100
|
+
const SKIP_DIRS = new Set([
|
|
101
|
+
"node_modules",
|
|
102
|
+
".git",
|
|
103
|
+
"dist",
|
|
104
|
+
"build",
|
|
105
|
+
".next",
|
|
106
|
+
".venv",
|
|
107
|
+
"venv",
|
|
108
|
+
"__pycache__",
|
|
109
|
+
".swarm-worktrees",
|
|
110
|
+
"coverage",
|
|
111
|
+
".turbo",
|
|
112
|
+
".cache",
|
|
113
|
+
]);
|
|
114
|
+
const SKIP_EXTENSIONS = new Set([
|
|
115
|
+
".png",
|
|
116
|
+
".jpg",
|
|
117
|
+
".jpeg",
|
|
118
|
+
".gif",
|
|
119
|
+
".ico",
|
|
120
|
+
".svg",
|
|
121
|
+
".woff",
|
|
122
|
+
".woff2",
|
|
123
|
+
".ttf",
|
|
124
|
+
".eot",
|
|
125
|
+
".mp3",
|
|
126
|
+
".mp4",
|
|
127
|
+
".webm",
|
|
128
|
+
".zip",
|
|
129
|
+
".tar",
|
|
130
|
+
".gz",
|
|
131
|
+
".lock",
|
|
132
|
+
".map",
|
|
133
|
+
]);
|
|
134
|
+
function scanDirectory(dir, maxFiles = 200, maxTotalSize = 2 * 1024 * 1024) {
|
|
135
|
+
const files = [];
|
|
136
|
+
let totalSize = 0;
|
|
137
|
+
function walk(currentDir, depth) {
|
|
138
|
+
if (depth > 15 || files.length >= maxFiles || totalSize >= maxTotalSize)
|
|
139
|
+
return;
|
|
140
|
+
let entries;
|
|
141
|
+
try {
|
|
142
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
if (files.length >= maxFiles || totalSize >= maxTotalSize)
|
|
150
|
+
return;
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
153
|
+
walk(path.join(currentDir, entry.name), depth + 1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (entry.isFile()) {
|
|
157
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
158
|
+
if (SKIP_EXTENSIONS.has(ext))
|
|
159
|
+
continue;
|
|
160
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
161
|
+
try {
|
|
162
|
+
const stat = fs.statSync(fullPath);
|
|
163
|
+
if (stat.size > 100 * 1024)
|
|
164
|
+
continue;
|
|
165
|
+
if (stat.size === 0)
|
|
166
|
+
continue;
|
|
167
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
168
|
+
if (content.includes("\0"))
|
|
169
|
+
continue;
|
|
170
|
+
const relPath = path.relative(dir, fullPath);
|
|
171
|
+
files.push({ relPath, content });
|
|
172
|
+
totalSize += content.length;
|
|
173
|
+
}
|
|
174
|
+
catch { }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
walk(dir, 0);
|
|
179
|
+
const parts = [];
|
|
180
|
+
parts.push(`Codebase: ${path.basename(dir)}`);
|
|
181
|
+
parts.push(`Files: ${files.length}`);
|
|
182
|
+
parts.push(`Total size: ${(totalSize / 1024).toFixed(1)}KB`);
|
|
183
|
+
parts.push("---");
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
parts.push(`\n=== ${file.relPath} ===`);
|
|
186
|
+
parts.push(file.content);
|
|
187
|
+
}
|
|
188
|
+
return parts.join("\n");
|
|
189
|
+
}
|
|
190
|
+
// ── Formatting helpers ──────────────────────────────────────────────────────
|
|
191
|
+
function formatDuration(ms) {
|
|
192
|
+
if (ms < 1000)
|
|
193
|
+
return `${ms}ms`;
|
|
194
|
+
const s = ms / 1000;
|
|
195
|
+
if (s < 60)
|
|
196
|
+
return `${s.toFixed(1)}s`;
|
|
197
|
+
const m = Math.floor(s / 60);
|
|
198
|
+
const rem = s % 60;
|
|
199
|
+
return `${m}m ${rem.toFixed(0)}s`;
|
|
200
|
+
}
|
|
201
|
+
function formatCost(usd) {
|
|
202
|
+
return `$${usd.toFixed(4)}`;
|
|
203
|
+
}
|
|
204
|
+
function statusIcon(status) {
|
|
205
|
+
switch (status) {
|
|
206
|
+
case "completed":
|
|
207
|
+
return green(symbols.check);
|
|
208
|
+
case "failed":
|
|
209
|
+
return red(symbols.cross);
|
|
210
|
+
case "cancelled":
|
|
211
|
+
return yellow(symbols.dash);
|
|
212
|
+
case "running":
|
|
213
|
+
return coral(symbols.arrow);
|
|
214
|
+
case "pending":
|
|
215
|
+
return dim(symbols.dot);
|
|
216
|
+
default:
|
|
217
|
+
return dim("?");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function statusColor(status) {
|
|
221
|
+
switch (status) {
|
|
222
|
+
case "completed":
|
|
223
|
+
return green;
|
|
224
|
+
case "failed":
|
|
225
|
+
return red;
|
|
226
|
+
case "cancelled":
|
|
227
|
+
return yellow;
|
|
228
|
+
case "running":
|
|
229
|
+
return coral;
|
|
230
|
+
default:
|
|
231
|
+
return dim;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// ── Command handlers ────────────────────────────────────────────────────────
|
|
235
|
+
function cmdHelp() {
|
|
236
|
+
const w = Math.min(termWidth(), 70);
|
|
237
|
+
const out = process.stderr;
|
|
238
|
+
out.write("\n");
|
|
239
|
+
out.write(` ${bold(cyan("Interactive Swarm Commands"))}\n`);
|
|
240
|
+
out.write(` ${dim(symbols.horizontal.repeat(Math.min(w - 4, 40)))}\n`);
|
|
241
|
+
out.write("\n");
|
|
242
|
+
out.write(` ${yellow("<task>")} ${dim("Run a task through the orchestrator")}\n`);
|
|
243
|
+
out.write(` ${cyan("/threads")} ${dim("(/t)")} ${dim("List all threads with status")}\n`);
|
|
244
|
+
out.write(` ${cyan("/thread")} ${yellow("<id>")} ${dim("Show detailed info for a thread")}\n`);
|
|
245
|
+
out.write(` ${cyan("/merge")} ${yellow("[id...]")} ${dim("Merge thread branches (all if no args)")}\n`);
|
|
246
|
+
out.write(` ${cyan("/reject")} ${yellow("<id>")} ${dim("Discard a thread worktree and branch")}\n`);
|
|
247
|
+
out.write(` ${cyan("/dag")} ${dim("Show thread DAG with status indicators")}\n`);
|
|
248
|
+
out.write(` ${cyan("/budget")} ${dim("Show budget state")}\n`);
|
|
249
|
+
out.write(` ${cyan("/status")} ${dim("Overall session status")}\n`);
|
|
250
|
+
out.write(` ${cyan("/configure")} ${dim("(/c)")} ${dim("Change agent, model, or backend")}\n`);
|
|
251
|
+
out.write(` ${cyan("/help")} ${dim("Show this help")}\n`);
|
|
252
|
+
out.write(` ${cyan("/quit")} ${dim("(/exit)")} ${dim("Cleanup and exit")}\n`);
|
|
253
|
+
out.write("\n");
|
|
254
|
+
}
|
|
255
|
+
function cmdThreads(threadManager) {
|
|
256
|
+
const threads = threadManager.getThreads();
|
|
257
|
+
const out = process.stderr;
|
|
258
|
+
if (threads.length === 0) {
|
|
259
|
+
out.write(`\n ${dim("No threads yet. Type a task to get started.")}\n\n`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
out.write("\n");
|
|
263
|
+
out.write(` ${bold(cyan("Threads"))} ${dim(`(${threads.length} total)`)}\n`);
|
|
264
|
+
out.write(` ${dim(symbols.horizontal.repeat(Math.min(termWidth() - 4, 60)))}\n`);
|
|
265
|
+
for (const t of threads) {
|
|
266
|
+
const icon = statusIcon(t.status);
|
|
267
|
+
const id = dim(t.id.slice(0, 8));
|
|
268
|
+
const status = statusColor(t.status)(t.status);
|
|
269
|
+
const dur = t.completedAt && t.startedAt
|
|
270
|
+
? dim(formatDuration(t.completedAt - t.startedAt))
|
|
271
|
+
: t.startedAt
|
|
272
|
+
? dim(formatDuration(Date.now() - t.startedAt))
|
|
273
|
+
: dim("--");
|
|
274
|
+
const cost = t.estimatedCostUsd > 0 ? dim(formatCost(t.estimatedCostUsd)) : "";
|
|
275
|
+
const files = t.result?.filesChanged.length ?? 0;
|
|
276
|
+
const fileStr = files > 0 ? dim(`${files} files`) : "";
|
|
277
|
+
const task = truncate(t.config.task, 45);
|
|
278
|
+
out.write(` ${icon} ${id} ${status} ${dur} ${cost} ${fileStr} ${dim(task)}\n`);
|
|
279
|
+
}
|
|
280
|
+
out.write("\n");
|
|
281
|
+
}
|
|
282
|
+
function cmdThread(threadManager, threadId) {
|
|
283
|
+
const out = process.stderr;
|
|
284
|
+
if (!threadId) {
|
|
285
|
+
logError("Usage: /thread <id>");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Find thread by prefix match
|
|
289
|
+
const threads = threadManager.getThreads();
|
|
290
|
+
const matches = threads.filter((t) => t.id.startsWith(threadId));
|
|
291
|
+
if (matches.length === 0) {
|
|
292
|
+
logError(`No thread found matching "${threadId}"`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (matches.length > 1) {
|
|
296
|
+
logWarn(`Multiple matches for "${threadId}": ${matches.map((t) => t.id.slice(0, 8)).join(", ")}`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const t = matches[0];
|
|
300
|
+
out.write("\n");
|
|
301
|
+
out.write(` ${bold(cyan("Thread"))} ${dim(t.id)}\n`);
|
|
302
|
+
out.write(` ${dim(symbols.horizontal.repeat(Math.min(termWidth() - 4, 60)))}\n`);
|
|
303
|
+
out.write(` ${dim("Status")} ${statusColor(t.status)(t.status)} ${statusIcon(t.status)}\n`);
|
|
304
|
+
out.write(` ${dim("Phase")} ${t.phase}\n`);
|
|
305
|
+
out.write(` ${dim("Task")} ${t.config.task}\n`);
|
|
306
|
+
out.write(` ${dim("Agent")} ${t.config.agent.backend || "default"}\n`);
|
|
307
|
+
out.write(` ${dim("Model")} ${t.config.agent.model || "default"}\n`);
|
|
308
|
+
out.write(` ${dim("Attempt")} ${t.attempt}/${t.maxAttempts}\n`);
|
|
309
|
+
if (t.startedAt) {
|
|
310
|
+
const started = new Date(t.startedAt).toLocaleTimeString();
|
|
311
|
+
out.write(` ${dim("Started")} ${started}\n`);
|
|
312
|
+
}
|
|
313
|
+
if (t.completedAt && t.startedAt) {
|
|
314
|
+
out.write(` ${dim("Duration")} ${formatDuration(t.completedAt - t.startedAt)}\n`);
|
|
315
|
+
}
|
|
316
|
+
if (t.estimatedCostUsd > 0) {
|
|
317
|
+
out.write(` ${dim("Cost")} ${formatCost(t.estimatedCostUsd)}\n`);
|
|
318
|
+
}
|
|
319
|
+
if (t.branchName) {
|
|
320
|
+
out.write(` ${dim("Branch")} ${cyan(t.branchName)}\n`);
|
|
321
|
+
}
|
|
322
|
+
if (t.worktreePath) {
|
|
323
|
+
out.write(` ${dim("Worktree")} ${t.worktreePath}\n`);
|
|
324
|
+
}
|
|
325
|
+
if (t.error) {
|
|
326
|
+
out.write(` ${dim("Error")} ${red(t.error)}\n`);
|
|
327
|
+
}
|
|
328
|
+
// Show result summary
|
|
329
|
+
if (t.result) {
|
|
330
|
+
out.write("\n");
|
|
331
|
+
out.write(` ${bold("Result")}\n`);
|
|
332
|
+
if (t.result.filesChanged.length > 0) {
|
|
333
|
+
out.write(` ${dim("Files changed:")}\n`);
|
|
334
|
+
for (const f of t.result.filesChanged) {
|
|
335
|
+
out.write(` ${green("+")} ${f}\n`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (t.result.diffStats && t.result.diffStats !== "(no changes)") {
|
|
339
|
+
out.write(`\n ${dim("Diff stats:")}\n`);
|
|
340
|
+
for (const line of t.result.diffStats.split("\n")) {
|
|
341
|
+
out.write(` ${dim(line)}\n`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (t.result.summary) {
|
|
345
|
+
out.write(`\n ${dim("Summary:")}\n`);
|
|
346
|
+
const lines = t.result.summary.split("\n");
|
|
347
|
+
for (const line of lines.slice(0, 20)) {
|
|
348
|
+
out.write(` ${line}\n`);
|
|
349
|
+
}
|
|
350
|
+
if (lines.length > 20) {
|
|
351
|
+
out.write(` ${dim(`... ${lines.length - 20} more lines`)}\n`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
out.write("\n");
|
|
356
|
+
}
|
|
357
|
+
async function cmdMerge(threadManager, dir, idArgs) {
|
|
358
|
+
const threads = threadManager.getThreads();
|
|
359
|
+
const out = process.stderr;
|
|
360
|
+
if (idArgs.length === 0) {
|
|
361
|
+
// Merge all completed threads
|
|
362
|
+
out.write(`\n ${dim("Merging all completed thread branches...")}\n`);
|
|
363
|
+
const opts = { continueOnConflict: true };
|
|
364
|
+
const results = await mergeAllThreads(dir, threads, opts);
|
|
365
|
+
if (results.length === 0) {
|
|
366
|
+
out.write(` ${dim("No eligible threads to merge.")}\n\n`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
for (const r of results) {
|
|
370
|
+
if (r.success) {
|
|
371
|
+
out.write(` ${green(symbols.check)} ${cyan(r.branch)} ${dim(r.message)}\n`);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
out.write(` ${red(symbols.cross)} ${cyan(r.branch)} ${red(r.message)}\n`);
|
|
375
|
+
if (r.conflicts.length > 0) {
|
|
376
|
+
out.write(` ${dim("Conflicts:")} ${r.conflicts.join(", ")}\n`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const merged = results.filter((r) => r.success).length;
|
|
381
|
+
const failed = results.filter((r) => !r.success).length;
|
|
382
|
+
if (merged > 0)
|
|
383
|
+
logSuccess(`Merged ${merged} branches`);
|
|
384
|
+
if (failed > 0)
|
|
385
|
+
logWarn(`${failed} branches had conflicts`);
|
|
386
|
+
out.write("\n");
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
// Merge specific threads by ID prefix
|
|
390
|
+
for (const idArg of idArgs) {
|
|
391
|
+
const match = threads.find((t) => t.id.startsWith(idArg));
|
|
392
|
+
if (!match) {
|
|
393
|
+
logError(`No thread found matching "${idArg}"`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (match.status !== "completed" || !match.branchName) {
|
|
397
|
+
logWarn(`Thread ${match.id.slice(0, 8)} is not eligible for merge (status: ${match.status})`);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (!match.result?.success) {
|
|
401
|
+
logWarn(`Thread ${match.id.slice(0, 8)} did not complete successfully`);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
out.write(` ${dim("Merging")} ${cyan(match.branchName)}${dim("...")}\n`);
|
|
405
|
+
const result = await mergeThreadBranch(dir, match.branchName, match.id);
|
|
406
|
+
if (result.success) {
|
|
407
|
+
out.write(` ${green(symbols.check)} ${dim(result.message)}\n`);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
out.write(` ${red(symbols.cross)} ${red(result.message)}\n`);
|
|
411
|
+
if (result.conflicts.length > 0) {
|
|
412
|
+
out.write(` ${dim("Conflicts:")} ${result.conflicts.join(", ")}\n`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
out.write("\n");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function cmdReject(threadManager, threadId) {
|
|
420
|
+
const out = process.stderr;
|
|
421
|
+
if (!threadId) {
|
|
422
|
+
logError("Usage: /reject <id>");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const threads = threadManager.getThreads();
|
|
426
|
+
const match = threads.find((t) => t.id.startsWith(threadId));
|
|
427
|
+
if (!match) {
|
|
428
|
+
logError(`No thread found matching "${threadId}"`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const wtManager = threadManager.getWorktreeManager();
|
|
432
|
+
const wtInfo = wtManager.getWorktreeInfo(match.id);
|
|
433
|
+
if (wtInfo) {
|
|
434
|
+
out.write(` ${dim("Destroying worktree and branch for")} ${dim(match.id.slice(0, 8))}${dim("...")}\n`);
|
|
435
|
+
await wtManager.destroy(match.id, true);
|
|
436
|
+
logSuccess(`Rejected thread ${match.id.slice(0, 8)} — worktree and branch removed`);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
logWarn(`Thread ${match.id.slice(0, 8)} has no active worktree (may already be cleaned up)`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function cmdDag(threadManager) {
|
|
443
|
+
const threads = threadManager.getThreads();
|
|
444
|
+
const out = process.stderr;
|
|
445
|
+
if (threads.length === 0) {
|
|
446
|
+
out.write(`\n ${dim("No threads yet.")}\n\n`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
out.write("\n");
|
|
450
|
+
out.write(` ${bold(cyan("Thread DAG"))}\n`);
|
|
451
|
+
out.write(` ${dim(symbols.horizontal.repeat(Math.min(termWidth() - 4, 60)))}\n`);
|
|
452
|
+
out.write("\n");
|
|
453
|
+
// Group by status for visual clarity
|
|
454
|
+
const running = threads.filter((t) => t.status === "running" || t.status === "pending");
|
|
455
|
+
const completed = threads.filter((t) => t.status === "completed");
|
|
456
|
+
const failed = threads.filter((t) => t.status === "failed");
|
|
457
|
+
const cancelled = threads.filter((t) => t.status === "cancelled");
|
|
458
|
+
// Main branch root
|
|
459
|
+
out.write(` ${cyan(symbols.dot)} ${bold("main")}\n`);
|
|
460
|
+
const renderThread = (t, isLast) => {
|
|
461
|
+
const connector = isLast ? symbols.bottomLeft : `${symbols.vertLine}`;
|
|
462
|
+
const branch = t.branchName || `swarm/${t.id.slice(0, 8)}`;
|
|
463
|
+
const icon = statusIcon(t.status);
|
|
464
|
+
const task = truncate(t.config.task, 35);
|
|
465
|
+
const dur = t.completedAt && t.startedAt ? dim(formatDuration(t.completedAt - t.startedAt)) : "";
|
|
466
|
+
out.write(` ${cyan(symbols.vertLine)}\n`);
|
|
467
|
+
out.write(` ${cyan(connector)}${cyan(symbols.horizontal.repeat(2))} ${icon} ${dim(branch)} ${dim(task)} ${dur}\n`);
|
|
468
|
+
};
|
|
469
|
+
const allThreads = [...running, ...completed, ...failed, ...cancelled];
|
|
470
|
+
for (let i = 0; i < allThreads.length; i++) {
|
|
471
|
+
renderThread(allThreads[i], i === allThreads.length - 1);
|
|
472
|
+
}
|
|
473
|
+
out.write("\n");
|
|
474
|
+
// Legend
|
|
475
|
+
out.write(` ${dim("Legend:")} ${green(symbols.check)} completed ${red(symbols.cross)} failed ${yellow(symbols.dash)} cancelled ${coral(symbols.arrow)} running ${dim(symbols.dot)} pending\n`);
|
|
476
|
+
out.write("\n");
|
|
477
|
+
}
|
|
478
|
+
function cmdBudget(threadManager) {
|
|
479
|
+
const budget = threadManager.getBudgetState();
|
|
480
|
+
const out = process.stderr;
|
|
481
|
+
out.write("\n");
|
|
482
|
+
out.write(` ${bold(cyan("Budget"))}\n`);
|
|
483
|
+
out.write(` ${dim(symbols.horizontal.repeat(Math.min(termWidth() - 4, 40)))}\n`);
|
|
484
|
+
const pct = budget.sessionLimitUsd > 0 ? ((budget.totalSpentUsd / budget.sessionLimitUsd) * 100).toFixed(1) : "0";
|
|
485
|
+
const budgetColor = budget.totalSpentUsd > budget.sessionLimitUsd * 0.8 ? yellow : green;
|
|
486
|
+
out.write(` ${dim("Spent")} ${budgetColor(formatCost(budget.totalSpentUsd))} / ${formatCost(budget.sessionLimitUsd)} (${pct}%)\n`);
|
|
487
|
+
out.write(` ${dim("Per-thread")} ${formatCost(budget.perThreadLimitUsd)} max\n`);
|
|
488
|
+
if (budget.actualCostThreads > 0 || budget.estimatedCostThreads > 0) {
|
|
489
|
+
out.write(` ${dim("Cost source")} ${budget.actualCostThreads} actual, ${budget.estimatedCostThreads} estimated\n`);
|
|
490
|
+
}
|
|
491
|
+
const tokens = budget.totalTokens;
|
|
492
|
+
if (tokens.input > 0 || tokens.output > 0) {
|
|
493
|
+
const totalK = ((tokens.input + tokens.output) / 1000).toFixed(1);
|
|
494
|
+
out.write(` ${dim("Tokens")} ${tokens.input.toLocaleString()} in + ${tokens.output.toLocaleString()} out (${totalK}K)\n`);
|
|
495
|
+
}
|
|
496
|
+
// Per-thread costs
|
|
497
|
+
if (budget.threadCosts.size > 0) {
|
|
498
|
+
out.write(`\n ${dim("Per-thread costs:")}\n`);
|
|
499
|
+
for (const [id, cost] of budget.threadCosts) {
|
|
500
|
+
out.write(` ${dim(id.slice(0, 8))} ${formatCost(cost)}\n`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
out.write("\n");
|
|
504
|
+
}
|
|
505
|
+
function cmdStatus(threadManager, sessionStartTime, taskCount) {
|
|
506
|
+
const threads = threadManager.getThreads();
|
|
507
|
+
const budget = threadManager.getBudgetState();
|
|
508
|
+
const cache = threadManager.getCacheStats();
|
|
509
|
+
const concurrency = threadManager.getConcurrencyStats();
|
|
510
|
+
const out = process.stderr;
|
|
511
|
+
out.write("\n");
|
|
512
|
+
out.write(` ${bold(cyan("Session Status"))}\n`);
|
|
513
|
+
out.write(` ${dim(symbols.horizontal.repeat(Math.min(termWidth() - 4, 40)))}\n`);
|
|
514
|
+
const elapsed = formatDuration(Date.now() - sessionStartTime);
|
|
515
|
+
out.write(` ${dim("Uptime")} ${elapsed}\n`);
|
|
516
|
+
out.write(` ${dim("Tasks run")} ${taskCount}\n`);
|
|
517
|
+
// Thread stats
|
|
518
|
+
const completed = threads.filter((t) => t.status === "completed").length;
|
|
519
|
+
const failed = threads.filter((t) => t.status === "failed").length;
|
|
520
|
+
const running = threads.filter((t) => t.status === "running").length;
|
|
521
|
+
const pending = threads.filter((t) => t.status === "pending").length;
|
|
522
|
+
out.write(` ${dim("Threads")} ${threads.length} total`);
|
|
523
|
+
if (completed > 0)
|
|
524
|
+
out.write(` ${green(`${completed} done`)}`);
|
|
525
|
+
if (failed > 0)
|
|
526
|
+
out.write(` ${red(`${failed} failed`)}`);
|
|
527
|
+
if (running > 0)
|
|
528
|
+
out.write(` ${coral(`${running} running`)}`);
|
|
529
|
+
if (pending > 0)
|
|
530
|
+
out.write(` ${dim(`${pending} pending`)}`);
|
|
531
|
+
out.write("\n");
|
|
532
|
+
out.write(` ${dim("Concurrency")} ${concurrency.active}/${concurrency.max} active, ${concurrency.waiting} waiting\n`);
|
|
533
|
+
// Budget
|
|
534
|
+
const pct = budget.sessionLimitUsd > 0 ? ((budget.totalSpentUsd / budget.sessionLimitUsd) * 100).toFixed(1) : "0";
|
|
535
|
+
out.write(` ${dim("Budget")} ${formatCost(budget.totalSpentUsd)} / ${formatCost(budget.sessionLimitUsd)} (${pct}%)\n`);
|
|
536
|
+
// Cache
|
|
537
|
+
if (cache.hits > 0 || cache.size > 0) {
|
|
538
|
+
const saved = cache.totalSavedMs > 0 ? `, saved ${formatDuration(cache.totalSavedMs)}` : "";
|
|
539
|
+
out.write(` ${dim("Cache")} ${cache.hits} hits, ${cache.misses} misses, ${cache.size} entries${saved}\n`);
|
|
540
|
+
}
|
|
541
|
+
out.write("\n");
|
|
542
|
+
}
|
|
543
|
+
// ── Configure command ───────────────────────────────────────────────────────
|
|
544
|
+
async function cmdConfigure(config, resolved) {
|
|
545
|
+
const out = process.stderr;
|
|
546
|
+
const ask = async (q) => {
|
|
547
|
+
const { readTextInput: readInput } = await import("../ui/text-input.js");
|
|
548
|
+
const result = await readInput(q);
|
|
549
|
+
return result.text;
|
|
550
|
+
};
|
|
551
|
+
out.write("\n");
|
|
552
|
+
out.write(` ${bold(cyan("Configuration"))}\n`);
|
|
553
|
+
out.write(` ${dim(symbols.horizontal.repeat(40))}\n\n`);
|
|
554
|
+
out.write(` ${dim("Current settings:")}\n`);
|
|
555
|
+
out.write(` ${cyan("1")} Agent ${bold(config.default_agent)}\n`);
|
|
556
|
+
const displayModel = resolved.provider === "ollama"
|
|
557
|
+
? `ollama/${resolved.model.id}`
|
|
558
|
+
: resolved.provider === "openrouter"
|
|
559
|
+
? `openrouter/${resolved.model.id}`
|
|
560
|
+
: resolved.model.id;
|
|
561
|
+
out.write(` ${cyan("2")} Model ${bold(displayModel)}${displayModel !== config.default_model ? dim(` (config: ${config.default_model})`) : ""}\n`);
|
|
562
|
+
out.write(` ${cyan("3")} Max threads ${bold(String(config.max_threads))}\n`);
|
|
563
|
+
out.write(` ${cyan("4")} Auto routing ${bold(config.auto_model_selection ? "on" : "off")}\n`);
|
|
564
|
+
out.write(` ${cyan("5")} Session budget ${bold(`$${config.max_session_budget_usd.toFixed(2)}`)}\n`);
|
|
565
|
+
out.write(` ${cyan("6")} Thread budget ${bold(`$${config.max_thread_budget_usd.toFixed(2)}`)}\n`);
|
|
566
|
+
out.write(` ${cyan("7")} Compression ${bold(config.compression_strategy)}\n`);
|
|
567
|
+
out.write("\n");
|
|
568
|
+
const choice = await ask(` ${coral(symbols.arrow)} Setting to change [1-7, or enter to cancel]: `);
|
|
569
|
+
if (!choice) {
|
|
570
|
+
out.write(` ${dim("No changes made.")}\n\n`);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
switch (choice) {
|
|
574
|
+
case "1": {
|
|
575
|
+
const agents = (await import("../agents/provider.js")).listAgents();
|
|
576
|
+
out.write(`\n ${dim("Available agents:")} ${agents.join(", ")}\n`);
|
|
577
|
+
const val = await ask(` ${coral(symbols.arrow)} New agent [${config.default_agent}]: `);
|
|
578
|
+
if (val && agents.includes(val)) {
|
|
579
|
+
config.default_agent = val;
|
|
580
|
+
logSuccess(`Agent set to ${bold(val)}`);
|
|
581
|
+
}
|
|
582
|
+
else if (val) {
|
|
583
|
+
logWarn(`Unknown agent "${val}"`);
|
|
584
|
+
}
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case "2": {
|
|
588
|
+
out.write(`\n ${dim("Enter model ID (e.g. ollama/deepseek-coder-v2, anthropic/claude-sonnet-4-6, openrouter/auto)")}\n`);
|
|
589
|
+
const val = await ask(` ${coral(symbols.arrow)} New model [${displayModel}]: `);
|
|
590
|
+
if (val) {
|
|
591
|
+
// Check for required API keys
|
|
592
|
+
const keyChecks = {
|
|
593
|
+
openrouter: { env: "OPENROUTER_API_KEY", url: "https://openrouter.ai/keys" },
|
|
594
|
+
openai: { env: "OPENAI_API_KEY", url: "https://platform.openai.com/api-keys" },
|
|
595
|
+
anthropic: { env: "ANTHROPIC_API_KEY", url: "https://console.anthropic.com/settings/keys" },
|
|
596
|
+
google: { env: "GEMINI_API_KEY", url: "https://aistudio.google.com/apikey" },
|
|
597
|
+
};
|
|
598
|
+
// Determine which provider the model needs
|
|
599
|
+
let requiredProvider = "";
|
|
600
|
+
if (val.startsWith("openrouter/"))
|
|
601
|
+
requiredProvider = "openrouter";
|
|
602
|
+
else if (val.startsWith("anthropic/") || val.startsWith("claude"))
|
|
603
|
+
requiredProvider = "anthropic";
|
|
604
|
+
else if (val.startsWith("openai/") || val.startsWith("gpt") || val.startsWith("o3") || val.startsWith("o4"))
|
|
605
|
+
requiredProvider = "openai";
|
|
606
|
+
else if (val.startsWith("google/") || val.startsWith("gemini"))
|
|
607
|
+
requiredProvider = "google";
|
|
608
|
+
const check = requiredProvider ? keyChecks[requiredProvider] : undefined;
|
|
609
|
+
const envVal = check ? process.env[check.env] : undefined;
|
|
610
|
+
const hasRealKey = envVal && envVal !== "ollama-local";
|
|
611
|
+
if (check && !hasRealKey) {
|
|
612
|
+
out.write(`\n ${dim(`${requiredProvider} requires an API key (${check.url})`)}\n`);
|
|
613
|
+
const key = await ask(` ${coral(symbols.arrow)} ${check.env}: `);
|
|
614
|
+
if (key) {
|
|
615
|
+
process.env[check.env] = key;
|
|
616
|
+
logSuccess(`${check.env} set for this session`);
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
logWarn(`No API key provided — cannot use ${requiredProvider}`);
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const lookupId = val.startsWith("ollama/") || val.startsWith("openrouter/")
|
|
624
|
+
? val
|
|
625
|
+
: val.replace(/^(anthropic|openai|google)\//, "");
|
|
626
|
+
const newResolved = resolveModel(lookupId, logWarn);
|
|
627
|
+
if (newResolved) {
|
|
628
|
+
config.default_model = val;
|
|
629
|
+
resolved.model = newResolved.model;
|
|
630
|
+
resolved.provider = newResolved.provider;
|
|
631
|
+
logSuccess(`Model set to ${bold(val)}`);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
logWarn(`Could not resolve model "${val}"`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
case "3": {
|
|
640
|
+
const val = await ask(` ${coral(symbols.arrow)} Max concurrent threads [${config.max_threads}]: `);
|
|
641
|
+
const n = parseInt(val, 10);
|
|
642
|
+
if (n >= 1 && n <= 20) {
|
|
643
|
+
config.max_threads = n;
|
|
644
|
+
logSuccess(`Max threads set to ${bold(String(n))}`);
|
|
645
|
+
}
|
|
646
|
+
else if (val) {
|
|
647
|
+
logWarn("Must be between 1 and 20");
|
|
648
|
+
}
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case "4": {
|
|
652
|
+
config.auto_model_selection = !config.auto_model_selection;
|
|
653
|
+
logSuccess(`Auto routing ${bold(config.auto_model_selection ? "enabled" : "disabled")}`);
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
case "5": {
|
|
657
|
+
const val = await ask(` ${coral(symbols.arrow)} Session budget USD [${config.max_session_budget_usd}]: `);
|
|
658
|
+
const n = parseFloat(val);
|
|
659
|
+
if (Number.isFinite(n) && n > 0) {
|
|
660
|
+
config.max_session_budget_usd = n;
|
|
661
|
+
logSuccess(`Session budget set to ${bold(`$${n.toFixed(2)}`)}`);
|
|
662
|
+
}
|
|
663
|
+
else if (val) {
|
|
664
|
+
logWarn("Must be a positive number");
|
|
665
|
+
}
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
case "6": {
|
|
669
|
+
const val = await ask(` ${coral(symbols.arrow)} Per-thread budget USD [${config.max_thread_budget_usd}]: `);
|
|
670
|
+
const n = parseFloat(val);
|
|
671
|
+
if (Number.isFinite(n) && n > 0) {
|
|
672
|
+
config.max_thread_budget_usd = n;
|
|
673
|
+
logSuccess(`Thread budget set to ${bold(`$${n.toFixed(2)}`)}`);
|
|
674
|
+
}
|
|
675
|
+
else if (val) {
|
|
676
|
+
logWarn("Must be a positive number");
|
|
677
|
+
}
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
case "7": {
|
|
681
|
+
const strategies = ["structured", "llm-summary", "diff-only", "truncate"];
|
|
682
|
+
out.write(`\n ${dim("Options:")} ${strategies.join(", ")}\n`);
|
|
683
|
+
const val = await ask(` ${coral(symbols.arrow)} Compression [${config.compression_strategy}]: `);
|
|
684
|
+
if (val && strategies.includes(val)) {
|
|
685
|
+
config.compression_strategy = val;
|
|
686
|
+
logSuccess(`Compression set to ${bold(val)}`);
|
|
687
|
+
}
|
|
688
|
+
else if (val) {
|
|
689
|
+
logWarn(`Unknown strategy "${val}"`);
|
|
690
|
+
}
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
default:
|
|
694
|
+
logWarn("Invalid option");
|
|
695
|
+
}
|
|
696
|
+
// Persist config changes to ~/.swarm/config.yaml
|
|
697
|
+
if (choice >= "1" && choice <= "7") {
|
|
698
|
+
try {
|
|
699
|
+
const configDir = path.join(os.homedir(), ".swarm");
|
|
700
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
701
|
+
const configLines = [
|
|
702
|
+
"# Swarm user preferences (updated by /configure)",
|
|
703
|
+
`# Updated: ${new Date().toISOString()}`,
|
|
704
|
+
"",
|
|
705
|
+
`default_agent: ${config.default_agent}`,
|
|
706
|
+
`default_model: ${config.default_model}`,
|
|
707
|
+
"",
|
|
708
|
+
];
|
|
709
|
+
fs.writeFileSync(path.join(configDir, "config.yaml"), configLines.join("\n"), "utf-8");
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
// Non-fatal
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
out.write("\n");
|
|
716
|
+
}
|
|
717
|
+
// ── Interactive banner ──────────────────────────────────────────────────────
|
|
718
|
+
function renderInteractiveBanner(config) {
|
|
719
|
+
const w = Math.max(Math.min(termWidth(), 60), 24);
|
|
720
|
+
const out = process.stderr;
|
|
721
|
+
if (isTTY) {
|
|
722
|
+
const title = " swarm ";
|
|
723
|
+
const mode = " interactive ";
|
|
724
|
+
const padLen = Math.max(0, w - title.length - mode.length - 4);
|
|
725
|
+
const leftPad = symbols.horizontal.repeat(Math.floor(padLen / 2));
|
|
726
|
+
const rightPad = symbols.horizontal.repeat(Math.ceil(padLen / 2));
|
|
727
|
+
out.write("\n");
|
|
728
|
+
out.write(` ${cyan(`${symbols.topLeft}${leftPad}`)}${bold(coral(title))}${dim(mode)}${cyan(`${rightPad}${symbols.topRight}`)}\n`);
|
|
729
|
+
out.write(` ${cyan(symbols.vertLine)}${" ".repeat(Math.max(0, w - 2))}${cyan(symbols.vertLine)}\n`);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
out.write("\nswarm interactive\n");
|
|
733
|
+
}
|
|
734
|
+
const kv = (key, val) => {
|
|
735
|
+
out.write(` ${dim(key.padEnd(12))} ${val}\n`);
|
|
736
|
+
};
|
|
737
|
+
kv("Directory", config.dir);
|
|
738
|
+
kv("Model", `${config.model} ${dim(`(${config.provider})`)}`);
|
|
739
|
+
kv("Agent", config.agent);
|
|
740
|
+
kv("Routing", config.routing);
|
|
741
|
+
if (isTTY) {
|
|
742
|
+
out.write(` ${cyan(symbols.vertLine)}${" ".repeat(Math.max(0, w - 2))}${cyan(symbols.vertLine)}\n`);
|
|
743
|
+
out.write(` ${cyan(symbols.bottomLeft)}${cyan(symbols.horizontal.repeat(Math.max(0, w - 2)))}${cyan(symbols.bottomRight)}\n`);
|
|
744
|
+
}
|
|
745
|
+
out.write(`\n ${dim("Type a task to run, or /help for commands.")}\n\n`);
|
|
746
|
+
}
|
|
747
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
748
|
+
export async function runInteractiveSwarm(rawArgs) {
|
|
749
|
+
const args = parseInteractiveArgs(rawArgs);
|
|
750
|
+
const config = loadConfig();
|
|
751
|
+
// Configure UI
|
|
752
|
+
if (args.quiet)
|
|
753
|
+
setLogLevel("quiet");
|
|
754
|
+
else if (args.verbose)
|
|
755
|
+
setLogLevel("verbose");
|
|
756
|
+
// Verify target directory
|
|
757
|
+
if (!fs.existsSync(args.dir)) {
|
|
758
|
+
logError(`Directory "${args.dir}" does not exist`);
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
// First-run onboarding (may create ~/.swarm/config.yaml with user's chosen model)
|
|
762
|
+
await runOnboarding();
|
|
763
|
+
// Reload config to pick up any changes from onboarding
|
|
764
|
+
// (onboarding writes ~/.swarm/config.yaml but loadConfig() already ran before it)
|
|
765
|
+
Object.assign(config, loadConfig());
|
|
766
|
+
// Override config with CLI args
|
|
767
|
+
if (args.agent)
|
|
768
|
+
config.default_agent = args.agent;
|
|
769
|
+
if (args.maxBudget !== null)
|
|
770
|
+
config.max_session_budget_usd = args.maxBudget;
|
|
771
|
+
if (args.autoRoute)
|
|
772
|
+
config.auto_model_selection = true;
|
|
773
|
+
// Resolve orchestrator model — prefer CLI arg, then config's default_model
|
|
774
|
+
const orchestratorModelId = args.orchestratorModel !== "claude-sonnet-4-6"
|
|
775
|
+
? args.orchestratorModel
|
|
776
|
+
: config.default_model || args.orchestratorModel;
|
|
777
|
+
// For standard pi-ai models, strip provider prefix (e.g. "anthropic/claude-sonnet-4-6" → "claude-sonnet-4-6")
|
|
778
|
+
// Ollama/OpenRouter prefixes are kept as-is (handled by resolveModel)
|
|
779
|
+
const modelLookupId = orchestratorModelId.startsWith("ollama/") || orchestratorModelId.startsWith("openrouter/")
|
|
780
|
+
? orchestratorModelId
|
|
781
|
+
: orchestratorModelId.replace(/^(anthropic|openai|google)\//, "");
|
|
782
|
+
const resolved = resolveModel(modelLookupId, logWarn);
|
|
783
|
+
if (!resolved) {
|
|
784
|
+
logError(`Could not find model "${orchestratorModelId}"`, "Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY in your .env file, or use Ollama/OpenRouter");
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
// Initialize episodic memory and failure tracker
|
|
788
|
+
let episodicMemory;
|
|
789
|
+
if (config.episodic_memory_enabled) {
|
|
790
|
+
episodicMemory = new EpisodicMemory(config.memory_dir);
|
|
791
|
+
await episodicMemory.init();
|
|
792
|
+
}
|
|
793
|
+
const failureTracker = new FailureTracker();
|
|
794
|
+
// Render banner
|
|
795
|
+
renderInteractiveBanner({
|
|
796
|
+
dir: args.dir,
|
|
797
|
+
model: resolved.model.id,
|
|
798
|
+
provider: resolved.provider,
|
|
799
|
+
agent: config.default_agent,
|
|
800
|
+
routing: config.auto_model_selection ? "auto" : "orchestrator-driven",
|
|
801
|
+
});
|
|
802
|
+
// Scan codebase
|
|
803
|
+
const spinner = new Spinner();
|
|
804
|
+
spinner.start("scanning codebase");
|
|
805
|
+
const context = scanDirectory(args.dir);
|
|
806
|
+
spinner.stop();
|
|
807
|
+
logSuccess(`Scanned codebase — ${(context.length / 1024).toFixed(1)}KB context`);
|
|
808
|
+
// Start REPL: PythonRepl if explicitly specified; NodeVmRepl is deprecated/removed
|
|
809
|
+
const usePython = process.argv.includes("--python-repl") || process.argv.includes("--repl=python");
|
|
810
|
+
if (!usePython) {
|
|
811
|
+
throw new Error("NodeVmRepl has been removed. In-process JavaScript execution is disabled.");
|
|
812
|
+
}
|
|
813
|
+
const repl = new PythonRepl();
|
|
814
|
+
const sessionAc = new AbortController();
|
|
815
|
+
const dashboard = new ThreadDashboard();
|
|
816
|
+
const streamingFeed = new StreamingFeed();
|
|
817
|
+
spinner.setDashboard(dashboard);
|
|
818
|
+
spinner.setStreamingFeed(streamingFeed);
|
|
819
|
+
const threadProgress = (threadId, phase, detail) => {
|
|
820
|
+
if (phase === "completed" || phase === "failed" || phase === "cancelled") {
|
|
821
|
+
dashboard.complete(threadId, phase, detail);
|
|
822
|
+
streamingFeed.completeThread(threadId, phase, detail);
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
dashboard.update(threadId, phase, detail);
|
|
826
|
+
streamingFeed.updateThread(threadId, phase, detail);
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
// Enable OpenCode server mode
|
|
830
|
+
if (config.default_agent === "opencode" && config.opencode_server_mode) {
|
|
831
|
+
opencodeMod.enableServerMode();
|
|
832
|
+
logVerbose("OpenCode server mode enabled");
|
|
833
|
+
}
|
|
834
|
+
// Initialize thread manager
|
|
835
|
+
const threadManager = new ThreadManager(args.dir, config, threadProgress, sessionAc.signal);
|
|
836
|
+
threadManager.setThreadOutputCallback((threadId, chunk) => {
|
|
837
|
+
streamingFeed.appendOutput(threadId, chunk);
|
|
838
|
+
});
|
|
839
|
+
await threadManager.init();
|
|
840
|
+
if (episodicMemory) {
|
|
841
|
+
threadManager.setEpisodicMemory(episodicMemory);
|
|
842
|
+
}
|
|
843
|
+
// Register LLM summarizer if needed
|
|
844
|
+
if (config.compression_strategy === "llm-summary") {
|
|
845
|
+
const { setSummarizer } = await import("../compression/compressor.js");
|
|
846
|
+
const { completeSimple } = await import("@mariozechner/pi-ai");
|
|
847
|
+
setSummarizer(async (text, instruction) => {
|
|
848
|
+
const response = await completeSimple(resolved.model, {
|
|
849
|
+
systemPrompt: instruction,
|
|
850
|
+
messages: [
|
|
851
|
+
{
|
|
852
|
+
role: "user",
|
|
853
|
+
content: text,
|
|
854
|
+
timestamp: Date.now(),
|
|
855
|
+
},
|
|
856
|
+
],
|
|
857
|
+
});
|
|
858
|
+
return response.content
|
|
859
|
+
.filter((b) => b.type === "text")
|
|
860
|
+
.map((b) => b.text)
|
|
861
|
+
.join("");
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
// Build system prompt
|
|
865
|
+
const agentDesc = await describeAvailableAgents();
|
|
866
|
+
let systemPrompt = buildSwarmSystemPrompt(config, agentDesc, repl.getLanguage());
|
|
867
|
+
// Add episodic memory hints for general context
|
|
868
|
+
if (episodicMemory && episodicMemory.size > 0) {
|
|
869
|
+
const hints = episodicMemory.getStrategyHints("general coding tasks");
|
|
870
|
+
if (hints) {
|
|
871
|
+
systemPrompt += `\n\n## Episodic Memory\n${hints}\nConsider these strategies when decomposing your task.`;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Session state
|
|
875
|
+
const sessionStartTime = Date.now();
|
|
876
|
+
let taskCount = 0;
|
|
877
|
+
// Start Python REPL
|
|
878
|
+
await repl.start(sessionAc.signal);
|
|
879
|
+
let currentTaskAc = null;
|
|
880
|
+
let currentRunLog = null;
|
|
881
|
+
let cleanupCalled = false;
|
|
882
|
+
async function cleanup() {
|
|
883
|
+
if (cleanupCalled)
|
|
884
|
+
return;
|
|
885
|
+
cleanupCalled = true;
|
|
886
|
+
spinner.stop();
|
|
887
|
+
dashboard.clear();
|
|
888
|
+
streamingFeed.clear();
|
|
889
|
+
sessionAc.abort();
|
|
890
|
+
repl.shutdown();
|
|
891
|
+
await threadManager.cleanup();
|
|
892
|
+
await opencodeMod.disableServerMode();
|
|
893
|
+
process.exit(0);
|
|
894
|
+
}
|
|
895
|
+
// Thread handler (reused across tasks)
|
|
896
|
+
const threadHandler = async (task, threadContext, agentBackend, model, files) => {
|
|
897
|
+
let resolvedAgent = agentBackend || config.default_agent;
|
|
898
|
+
let resolvedModel = model || config.default_model;
|
|
899
|
+
let routeSlot = "";
|
|
900
|
+
let routeComplexity = "";
|
|
901
|
+
if (config.auto_model_selection && !agentBackend && !model) {
|
|
902
|
+
const route = await routeTask(task, config, episodicMemory, failureTracker);
|
|
903
|
+
resolvedAgent = route.agent;
|
|
904
|
+
resolvedModel = route.model;
|
|
905
|
+
routeSlot = route.slot;
|
|
906
|
+
routeComplexity = classifyTaskComplexity(task);
|
|
907
|
+
logRouter(`${route.reason} [slot: ${route.slot}]`);
|
|
908
|
+
}
|
|
909
|
+
const threadId = randomBytes(6).toString("hex");
|
|
910
|
+
dashboard.update(threadId, "queued", undefined, {
|
|
911
|
+
task,
|
|
912
|
+
agent: resolvedAgent,
|
|
913
|
+
model: resolvedModel,
|
|
914
|
+
});
|
|
915
|
+
const result = await threadManager.spawnThread({
|
|
916
|
+
id: threadId,
|
|
917
|
+
task,
|
|
918
|
+
context: threadContext,
|
|
919
|
+
agent: {
|
|
920
|
+
backend: resolvedAgent,
|
|
921
|
+
model: resolvedModel,
|
|
922
|
+
},
|
|
923
|
+
files,
|
|
924
|
+
});
|
|
925
|
+
// Log thread to current run
|
|
926
|
+
currentRunLog?.addThread({
|
|
927
|
+
id: threadId,
|
|
928
|
+
task,
|
|
929
|
+
agent: resolvedAgent,
|
|
930
|
+
model: resolvedModel,
|
|
931
|
+
success: result.success,
|
|
932
|
+
durationMs: result.durationMs,
|
|
933
|
+
filesChanged: result.filesChanged,
|
|
934
|
+
error: result.success ? undefined : result.summary,
|
|
935
|
+
});
|
|
936
|
+
// Record episode or failure
|
|
937
|
+
if (result.success) {
|
|
938
|
+
if (episodicMemory && routeSlot) {
|
|
939
|
+
episodicMemory
|
|
940
|
+
.record({
|
|
941
|
+
task,
|
|
942
|
+
agent: resolvedAgent,
|
|
943
|
+
model: resolvedModel,
|
|
944
|
+
slot: routeSlot,
|
|
945
|
+
complexity: routeComplexity,
|
|
946
|
+
success: true,
|
|
947
|
+
durationMs: result.durationMs,
|
|
948
|
+
estimatedCostUsd: result.estimatedCostUsd,
|
|
949
|
+
filesChanged: result.filesChanged,
|
|
950
|
+
summary: result.summary,
|
|
951
|
+
})
|
|
952
|
+
.catch(() => { });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
failureTracker.recordFailure(resolvedAgent, resolvedModel, task, result.summary || "unknown error");
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
result: result.summary,
|
|
960
|
+
success: result.success,
|
|
961
|
+
filesChanged: result.filesChanged,
|
|
962
|
+
durationMs: result.durationMs,
|
|
963
|
+
};
|
|
964
|
+
};
|
|
965
|
+
// Merge handler
|
|
966
|
+
const mergeHandler = async () => {
|
|
967
|
+
spinner.update("merging thread branches");
|
|
968
|
+
const threads = threadManager.getThreads();
|
|
969
|
+
const mergeOpts = { continueOnConflict: true };
|
|
970
|
+
const results = await mergeAllThreads(args.dir, threads, mergeOpts);
|
|
971
|
+
const merged = results.filter((r) => r.success).length;
|
|
972
|
+
const failed = results.filter((r) => !r.success).length;
|
|
973
|
+
if (failed > 0) {
|
|
974
|
+
logWarn(`Merged ${merged} branches, ${failed} failed`);
|
|
975
|
+
}
|
|
976
|
+
else if (merged > 0) {
|
|
977
|
+
logSuccess(`Merged ${merged} branches`);
|
|
978
|
+
}
|
|
979
|
+
// Clean up merged worktrees
|
|
980
|
+
if (config.auto_cleanup_worktrees) {
|
|
981
|
+
for (const r of results) {
|
|
982
|
+
if (r.success) {
|
|
983
|
+
const thread = threads.find((t) => t.branchName === r.branch);
|
|
984
|
+
if (thread) {
|
|
985
|
+
try {
|
|
986
|
+
await threadManager.destroyWorktree(thread.id);
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
// Non-fatal
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const summary = results
|
|
996
|
+
.map((r) => (r.success ? `Merged ${r.branch}: ${r.message}` : `FAILED ${r.branch}: ${r.message}`))
|
|
997
|
+
.join("\n");
|
|
998
|
+
return {
|
|
999
|
+
result: summary || "No threads to merge",
|
|
1000
|
+
success: results.every((r) => r.success),
|
|
1001
|
+
};
|
|
1002
|
+
};
|
|
1003
|
+
// Run a task through the RLM loop
|
|
1004
|
+
const runTask = async (query) => {
|
|
1005
|
+
taskCount++;
|
|
1006
|
+
currentTaskAc = new AbortController();
|
|
1007
|
+
// Link task abort to session abort
|
|
1008
|
+
const onSessionAbort = () => currentTaskAc?.abort();
|
|
1009
|
+
sessionAc.signal.addEventListener("abort", onSessionAbort, { once: true });
|
|
1010
|
+
spinner.start();
|
|
1011
|
+
const startTime = Date.now();
|
|
1012
|
+
const runLog = new RunLogger(query, resolved.model.id, config.default_agent, args.dir, config.max_iterations || 20);
|
|
1013
|
+
currentRunLog = runLog;
|
|
1014
|
+
try {
|
|
1015
|
+
// Update episodic memory hints per-task
|
|
1016
|
+
let taskSystemPrompt = systemPrompt;
|
|
1017
|
+
if (episodicMemory && episodicMemory.size > 0) {
|
|
1018
|
+
const hints = episodicMemory.getStrategyHints(query);
|
|
1019
|
+
if (hints) {
|
|
1020
|
+
// Replace existing hints or add new ones
|
|
1021
|
+
const memoryIdx = taskSystemPrompt.indexOf("## Episodic Memory");
|
|
1022
|
+
if (memoryIdx !== -1) {
|
|
1023
|
+
const endIdx = taskSystemPrompt.indexOf("\n## ", memoryIdx + 1);
|
|
1024
|
+
const before = taskSystemPrompt.slice(0, memoryIdx);
|
|
1025
|
+
const after = endIdx !== -1 ? taskSystemPrompt.slice(endIdx) : "";
|
|
1026
|
+
taskSystemPrompt = `${before}## Episodic Memory\n${hints}\nConsider these strategies when decomposing your task.${after}`;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const result = await runRlmLoop({
|
|
1031
|
+
context,
|
|
1032
|
+
query,
|
|
1033
|
+
model: resolved.model,
|
|
1034
|
+
repl,
|
|
1035
|
+
signal: currentTaskAc.signal,
|
|
1036
|
+
systemPrompt: taskSystemPrompt,
|
|
1037
|
+
threadHandler,
|
|
1038
|
+
mergeHandler,
|
|
1039
|
+
onProgress: (info) => {
|
|
1040
|
+
spinner.update(`iteration ${info.iteration}/${info.maxIterations}` +
|
|
1041
|
+
(info.subQueries > 0 ? ` \u00B7 ${info.subQueries} queries` : ""));
|
|
1042
|
+
logVerbose(`Iteration ${info.iteration}/${info.maxIterations} | ` +
|
|
1043
|
+
`Sub-queries: ${info.subQueries} | Phase: ${info.phase}`);
|
|
1044
|
+
},
|
|
1045
|
+
});
|
|
1046
|
+
spinner.stop();
|
|
1047
|
+
dashboard.clear();
|
|
1048
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
1049
|
+
// Log the run
|
|
1050
|
+
runLog.complete({ completed: result.completed, iterations: result.iterations, answer: result.answer }, Date.now() - startTime);
|
|
1051
|
+
const logPath = runLog.save();
|
|
1052
|
+
// Show concise result
|
|
1053
|
+
process.stderr.write("\n");
|
|
1054
|
+
const status = result.completed ? green("completed") : yellow("incomplete");
|
|
1055
|
+
process.stderr.write(` ${status} in ${bold(`${elapsed.toFixed(1)}s`)} ${dim(`(${result.iterations} iterations)`)}\n`);
|
|
1056
|
+
// Show answer
|
|
1057
|
+
if (result.answer) {
|
|
1058
|
+
process.stderr.write("\n");
|
|
1059
|
+
const answerLines = result.answer.split("\n");
|
|
1060
|
+
for (const line of answerLines) {
|
|
1061
|
+
process.stderr.write(` ${line}\n`);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (logPath) {
|
|
1065
|
+
process.stderr.write(` ${dim(`log: ${logPath}`)}\n`);
|
|
1066
|
+
}
|
|
1067
|
+
process.stderr.write("\n");
|
|
1068
|
+
}
|
|
1069
|
+
catch (err) {
|
|
1070
|
+
spinner.stop();
|
|
1071
|
+
dashboard.clear();
|
|
1072
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1073
|
+
runLog.complete({ completed: false, iterations: 0, error: errMsg }, Date.now() - startTime);
|
|
1074
|
+
runLog.save();
|
|
1075
|
+
if (currentTaskAc?.signal.aborted) {
|
|
1076
|
+
logWarn("Task cancelled");
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
logError(`Task failed: ${errMsg}`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
finally {
|
|
1083
|
+
sessionAc.signal.removeEventListener("abort", onSessionAbort);
|
|
1084
|
+
currentTaskAc = null;
|
|
1085
|
+
currentRunLog = null;
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
// Process a line of input
|
|
1089
|
+
const processLine = async (line) => {
|
|
1090
|
+
const trimmed = line.trim();
|
|
1091
|
+
if (!trimmed)
|
|
1092
|
+
return false;
|
|
1093
|
+
// Parse commands
|
|
1094
|
+
if (trimmed.startsWith("/")) {
|
|
1095
|
+
const parts = trimmed.split(/\s+/);
|
|
1096
|
+
const cmd = parts[0].toLowerCase();
|
|
1097
|
+
const cmdArgs = parts.slice(1);
|
|
1098
|
+
switch (cmd) {
|
|
1099
|
+
case "/help":
|
|
1100
|
+
case "/h":
|
|
1101
|
+
cmdHelp();
|
|
1102
|
+
break;
|
|
1103
|
+
case "/threads":
|
|
1104
|
+
case "/t":
|
|
1105
|
+
cmdThreads(threadManager);
|
|
1106
|
+
break;
|
|
1107
|
+
case "/thread":
|
|
1108
|
+
cmdThread(threadManager, cmdArgs[0] || "");
|
|
1109
|
+
break;
|
|
1110
|
+
case "/merge":
|
|
1111
|
+
case "/m":
|
|
1112
|
+
await cmdMerge(threadManager, args.dir, cmdArgs);
|
|
1113
|
+
break;
|
|
1114
|
+
case "/reject":
|
|
1115
|
+
case "/r":
|
|
1116
|
+
await cmdReject(threadManager, cmdArgs[0] || "");
|
|
1117
|
+
break;
|
|
1118
|
+
case "/dag":
|
|
1119
|
+
case "/d":
|
|
1120
|
+
cmdDag(threadManager);
|
|
1121
|
+
break;
|
|
1122
|
+
case "/budget":
|
|
1123
|
+
case "/b":
|
|
1124
|
+
cmdBudget(threadManager);
|
|
1125
|
+
break;
|
|
1126
|
+
case "/status":
|
|
1127
|
+
case "/s":
|
|
1128
|
+
cmdStatus(threadManager, sessionStartTime, taskCount);
|
|
1129
|
+
break;
|
|
1130
|
+
case "/configure":
|
|
1131
|
+
case "/config":
|
|
1132
|
+
case "/c":
|
|
1133
|
+
await cmdConfigure(config, resolved);
|
|
1134
|
+
break;
|
|
1135
|
+
case "/quit":
|
|
1136
|
+
case "/exit":
|
|
1137
|
+
case "/q":
|
|
1138
|
+
return true; // Signal exit
|
|
1139
|
+
default:
|
|
1140
|
+
logWarn(`Unknown command: ${cmd}. Type /help for available commands.`);
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
// Not a command — run as a task
|
|
1146
|
+
await runTask(trimmed);
|
|
1147
|
+
return false;
|
|
1148
|
+
};
|
|
1149
|
+
// REPL loop — multi-line text input
|
|
1150
|
+
const promptStr = `${coral("swarm")}${dim(">")} `;
|
|
1151
|
+
while (!cleanupCalled) {
|
|
1152
|
+
const result = await readTextInput(promptStr);
|
|
1153
|
+
if (result.action === "escape") {
|
|
1154
|
+
await cleanup();
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const text = result.text;
|
|
1158
|
+
if (!text)
|
|
1159
|
+
continue;
|
|
1160
|
+
try {
|
|
1161
|
+
const shouldExit = await processLine(text);
|
|
1162
|
+
if (shouldExit) {
|
|
1163
|
+
await cleanup();
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
catch (err) {
|
|
1168
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1169
|
+
logError(`Unexpected error: ${msg}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
//# sourceMappingURL=interactive-swarm.js.map
|