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,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-run onboarding — welcome screen + interactive agent setup wizard.
|
|
3
|
+
*
|
|
4
|
+
* Two-column layout: left has greeting + swarm mesh art + version info,
|
|
5
|
+
* right has tips + environment checks. Then an interactive wizard to:
|
|
6
|
+
* 1. Pick default coding agent from detected backends
|
|
7
|
+
* 2. Configure API keys for required providers
|
|
8
|
+
* 3. Choose default model
|
|
9
|
+
* 4. Save preferences to ~/.swarm/config.yaml
|
|
10
|
+
*
|
|
11
|
+
* Triggered once on first `swarm --dir` invocation. Saves ~/.swarm/.initialized marker.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import * as readline from "node:readline";
|
|
18
|
+
import { getLogLevel, isJsonMode } from "./log.js";
|
|
19
|
+
import { bold, coral, cyan, dim, green, isTTY, red, stripAnsi, symbols, termWidth, yellow } from "./theme.js";
|
|
20
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
21
|
+
const SWARM_DIR = path.join(os.homedir(), ".swarm");
|
|
22
|
+
const MARKER_FILE = path.join(SWARM_DIR, ".initialized");
|
|
23
|
+
const CRED_FILE = path.join(SWARM_DIR, "credentials");
|
|
24
|
+
const USER_CONFIG_FILE = path.join(SWARM_DIR, "config.yaml");
|
|
25
|
+
const VERSION = "0.1.0";
|
|
26
|
+
/** Which API key each agent backend requires (or supports). */
|
|
27
|
+
const AGENT_PROVIDERS = {
|
|
28
|
+
opencode: {
|
|
29
|
+
required: ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"],
|
|
30
|
+
description: "Supports all providers (Anthropic, OpenAI, Google)",
|
|
31
|
+
install: "npm i -g opencode",
|
|
32
|
+
},
|
|
33
|
+
"claude-code": {
|
|
34
|
+
required: ["ANTHROPIC_API_KEY"],
|
|
35
|
+
description: "Anthropic's Claude Code CLI",
|
|
36
|
+
install: "npm i -g @anthropic-ai/claude-code",
|
|
37
|
+
},
|
|
38
|
+
codex: {
|
|
39
|
+
required: ["OPENAI_API_KEY"],
|
|
40
|
+
description: "OpenAI's Codex CLI",
|
|
41
|
+
install: "npm i -g @openai/codex",
|
|
42
|
+
},
|
|
43
|
+
aider: {
|
|
44
|
+
required: ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
|
|
45
|
+
description: "Git-aware AI pair programmer",
|
|
46
|
+
install: "pip install aider-chat",
|
|
47
|
+
},
|
|
48
|
+
"direct-llm": {
|
|
49
|
+
required: ["ANTHROPIC_API_KEY"],
|
|
50
|
+
description: "Direct LLM calls (no coding agent)",
|
|
51
|
+
install: "(built-in)",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
/** Provider display info. */
|
|
55
|
+
const PROVIDERS = [
|
|
56
|
+
{ name: "Anthropic", envVar: "ANTHROPIC_API_KEY", prefix: "sk-ant-", modelDefault: "anthropic/claude-sonnet-4-6" },
|
|
57
|
+
{ name: "OpenAI", envVar: "OPENAI_API_KEY", prefix: "sk-", modelDefault: "openai/gpt-4o" },
|
|
58
|
+
{ name: "Google", envVar: "GEMINI_API_KEY", prefix: "AI", modelDefault: "google/gemini-2.5-flash" },
|
|
59
|
+
];
|
|
60
|
+
// ── Marker ────────────────────────────────────────────────────────────────────
|
|
61
|
+
export function isFirstRun() {
|
|
62
|
+
return !fs.existsSync(MARKER_FILE);
|
|
63
|
+
}
|
|
64
|
+
function markInitialized() {
|
|
65
|
+
try {
|
|
66
|
+
fs.mkdirSync(SWARM_DIR, { recursive: true });
|
|
67
|
+
fs.writeFileSync(MARKER_FILE, `${new Date().toISOString()}\n`, "utf-8");
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Non-fatal — onboarding will just run again next time
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function commandExists(cmd) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const proc = spawn("which", [cmd], { stdio: "pipe" });
|
|
76
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
77
|
+
proc.on("error", () => resolve(false));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function gitVersion() {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
const proc = spawn("git", ["--version"], { stdio: "pipe" });
|
|
83
|
+
let out = "";
|
|
84
|
+
proc.stdout?.on("data", (d) => {
|
|
85
|
+
out += d.toString();
|
|
86
|
+
});
|
|
87
|
+
proc.on("close", (code) => {
|
|
88
|
+
if (code === 0) {
|
|
89
|
+
const match = out.match(/git version (\S+)/);
|
|
90
|
+
resolve(match ? match[1] : "unknown");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
resolve(null);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
proc.on("error", () => resolve(null));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function detectApiKeys() {
|
|
100
|
+
const keys = new Map();
|
|
101
|
+
for (const p of PROVIDERS) {
|
|
102
|
+
const val = process.env[p.envVar];
|
|
103
|
+
if (val && val.length > 8) {
|
|
104
|
+
keys.set(p.envVar, val);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return keys;
|
|
108
|
+
}
|
|
109
|
+
function maskKey(val) {
|
|
110
|
+
if (val.length <= 12)
|
|
111
|
+
return "***";
|
|
112
|
+
return `${val.slice(0, 7)}...${val.slice(-4)}`;
|
|
113
|
+
}
|
|
114
|
+
async function checkAgentBackends() {
|
|
115
|
+
const agents = [
|
|
116
|
+
["opencode", "opencode"],
|
|
117
|
+
["claude-code", "claude"],
|
|
118
|
+
["codex", "codex"],
|
|
119
|
+
["aider", "aider"],
|
|
120
|
+
];
|
|
121
|
+
const results = [];
|
|
122
|
+
for (const [name, cmd] of agents) {
|
|
123
|
+
const exists = await commandExists(cmd);
|
|
124
|
+
results.push({ name, ok: exists, detail: exists ? "installed" : "not found" });
|
|
125
|
+
}
|
|
126
|
+
// direct-llm is always available
|
|
127
|
+
results.push({ name: "direct-llm", ok: true, detail: "built-in" });
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
// ── Interactive helpers ───────────────────────────────────────────────────────
|
|
131
|
+
function createPrompt() {
|
|
132
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
133
|
+
return {
|
|
134
|
+
ask: (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a.trim()))),
|
|
135
|
+
close: () => rl.close(),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function readHiddenInput(prompt) {
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
let input = "";
|
|
141
|
+
const origRawMode = process.stdin.isRaw;
|
|
142
|
+
if (process.stdin.isTTY)
|
|
143
|
+
process.stdin.setRawMode(true);
|
|
144
|
+
process.stderr.write(prompt);
|
|
145
|
+
const cleanup = () => {
|
|
146
|
+
process.stdin.removeListener("data", onData);
|
|
147
|
+
if (process.stdin.isTTY && origRawMode !== undefined) {
|
|
148
|
+
process.stdin.setRawMode(origRawMode);
|
|
149
|
+
}
|
|
150
|
+
process.stdin.pause();
|
|
151
|
+
};
|
|
152
|
+
const onData = (buf) => {
|
|
153
|
+
const ch = buf.toString();
|
|
154
|
+
if (ch === "\n" || ch === "\r") {
|
|
155
|
+
process.stderr.write("\n");
|
|
156
|
+
cleanup();
|
|
157
|
+
resolve(input);
|
|
158
|
+
}
|
|
159
|
+
else if (ch === "\x7f" || ch === "\b") {
|
|
160
|
+
if (input.length > 0) {
|
|
161
|
+
input = input.slice(0, -1);
|
|
162
|
+
process.stderr.write("\b \b");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (ch === "\x03") {
|
|
166
|
+
process.stderr.write("\n");
|
|
167
|
+
cleanup();
|
|
168
|
+
resolve("");
|
|
169
|
+
}
|
|
170
|
+
else if (ch >= " ") {
|
|
171
|
+
input += ch;
|
|
172
|
+
process.stderr.write(dim("*"));
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
process.stdin.on("data", onData);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function saveCredential(envVar, key) {
|
|
179
|
+
try {
|
|
180
|
+
fs.mkdirSync(SWARM_DIR, { recursive: true });
|
|
181
|
+
let existing = "";
|
|
182
|
+
if (fs.existsSync(CRED_FILE)) {
|
|
183
|
+
existing = fs.readFileSync(CRED_FILE, "utf-8");
|
|
184
|
+
existing = existing
|
|
185
|
+
.split("\n")
|
|
186
|
+
.filter((l) => !l.startsWith(`${envVar}=`))
|
|
187
|
+
.join("\n");
|
|
188
|
+
if (existing && !existing.endsWith("\n"))
|
|
189
|
+
existing += "\n";
|
|
190
|
+
}
|
|
191
|
+
fs.writeFileSync(CRED_FILE, `${existing}${envVar}=${key}\n`, { mode: 0o600 });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Fall through — key is still set in process.env
|
|
195
|
+
}
|
|
196
|
+
process.env[envVar] = key;
|
|
197
|
+
}
|
|
198
|
+
function saveUserConfig(agent, model) {
|
|
199
|
+
try {
|
|
200
|
+
fs.mkdirSync(SWARM_DIR, { recursive: true });
|
|
201
|
+
const lines = [
|
|
202
|
+
"# Swarm user preferences (generated by onboarding)",
|
|
203
|
+
`# Created: ${new Date().toISOString()}`,
|
|
204
|
+
"",
|
|
205
|
+
`default_agent: ${agent}`,
|
|
206
|
+
`default_model: ${model}`,
|
|
207
|
+
"",
|
|
208
|
+
];
|
|
209
|
+
fs.writeFileSync(USER_CONFIG_FILE, lines.join("\n"), "utf-8");
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Non-fatal
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// ── Swarm mesh art ────────────────────────────────────────────────────────────
|
|
216
|
+
const n = (s) => cyan(s); // node
|
|
217
|
+
const c = (s) => dim(s); // connection
|
|
218
|
+
const o = (s) => coral(s); // orchestrator (center)
|
|
219
|
+
function buildSwarmArt() {
|
|
220
|
+
const COLS = 7;
|
|
221
|
+
const CENTER = 3;
|
|
222
|
+
function nodeRow(showCenter) {
|
|
223
|
+
const parts = [];
|
|
224
|
+
for (let i = 0; i < COLS; i++) {
|
|
225
|
+
if (i > 0)
|
|
226
|
+
parts.push(c("\u2500\u2500\u2500"));
|
|
227
|
+
parts.push(showCenter && i === CENTER ? o("\u25C6") : n("\u25CF"));
|
|
228
|
+
}
|
|
229
|
+
return parts.join("");
|
|
230
|
+
}
|
|
231
|
+
function diagRow(startBackslash) {
|
|
232
|
+
const parts = [];
|
|
233
|
+
for (let i = 0; i < COLS; i++) {
|
|
234
|
+
parts.push(c("\u2502"));
|
|
235
|
+
if (i < COLS - 1) {
|
|
236
|
+
const back = (i % 2 === 0) === startBackslash;
|
|
237
|
+
parts.push(` ${c(back ? "\u2572" : "\u2571")} `);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return parts.join("");
|
|
241
|
+
}
|
|
242
|
+
return [nodeRow(false), diagRow(true), nodeRow(true), diagRow(false), nodeRow(false)];
|
|
243
|
+
}
|
|
244
|
+
const SWARM_ART = buildSwarmArt();
|
|
245
|
+
// ── Two-column renderer ───────────────────────────────────────────────────────
|
|
246
|
+
function padRight(text, width) {
|
|
247
|
+
const visible = stripAnsi(text).length;
|
|
248
|
+
if (visible >= width)
|
|
249
|
+
return text;
|
|
250
|
+
return text + " ".repeat(width - visible);
|
|
251
|
+
}
|
|
252
|
+
function renderTwoColumn(left, right, leftWidth) {
|
|
253
|
+
const sep = ` ${dim(symbols.vertLine)} `;
|
|
254
|
+
const maxLines = Math.max(left.length, right.length);
|
|
255
|
+
for (let i = 0; i < maxLines; i++) {
|
|
256
|
+
const l = padRight(left[i] || "", leftWidth);
|
|
257
|
+
const r = right[i] || "";
|
|
258
|
+
process.stderr.write(`${l}${sep}${r}\n`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ── Section header helper ─────────────────────────────────────────────────────
|
|
262
|
+
function sectionHeader(title, w) {
|
|
263
|
+
const label = ` ${title} `;
|
|
264
|
+
const dashes = Math.max(0, w - stripAnsi(label).length - 4);
|
|
265
|
+
const left = symbols.horizontal.repeat(2);
|
|
266
|
+
const right = symbols.horizontal.repeat(Math.max(0, dashes));
|
|
267
|
+
process.stderr.write(`\n ${dim(left)}${coral(bold(label))}${dim(right)}\n\n`);
|
|
268
|
+
}
|
|
269
|
+
// ── Onboarding flow ───────────────────────────────────────────────────────────
|
|
270
|
+
export async function runOnboarding() {
|
|
271
|
+
if (!isFirstRun())
|
|
272
|
+
return;
|
|
273
|
+
if (isJsonMode()) {
|
|
274
|
+
markInitialized();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (getLogLevel() === "quiet") {
|
|
278
|
+
markInitialized();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const w = Math.min(termWidth(), 80);
|
|
282
|
+
// ── Gather environment info ──────────────────────────────────────────
|
|
283
|
+
const username = os.userInfo().username || "there";
|
|
284
|
+
const gitVer = await gitVersion();
|
|
285
|
+
const apiKeys = detectApiKeys();
|
|
286
|
+
const agents = await checkAgentBackends();
|
|
287
|
+
const availableAgents = agents.filter((a) => a.ok);
|
|
288
|
+
const missingAgents = agents.filter((a) => !a.ok);
|
|
289
|
+
// ── Header line ──────────────────────────────────────────────────────
|
|
290
|
+
if (isTTY) {
|
|
291
|
+
const label = ` swarm v${VERSION} `;
|
|
292
|
+
const dashCount = Math.max(0, w - label.length - 4);
|
|
293
|
+
const leftDash = symbols.horizontal.repeat(2);
|
|
294
|
+
const rightDash = symbols.horizontal.repeat(Math.max(0, dashCount));
|
|
295
|
+
process.stderr.write(`\n ${dim(`${leftDash}${bold(coral(label))}${dim(rightDash)}`)}\n`);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
process.stderr.write(`\n swarm v${VERSION}\n`);
|
|
299
|
+
}
|
|
300
|
+
if (!isTTY) {
|
|
301
|
+
process.stderr.write(` Welcome to swarm, ${username}!\n\n`);
|
|
302
|
+
process.stderr.write(` Usage: swarm --dir ./project "your task"\n`);
|
|
303
|
+
process.stderr.write(` Docs: https://github.com/kingjulio8238/swarm-code\n\n`);
|
|
304
|
+
markInitialized();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// ── Build left column ────────────────────────────────────────────────
|
|
308
|
+
const LEFT_W = 36;
|
|
309
|
+
const left = [];
|
|
310
|
+
left.push(` ${bold("Welcome to swarm, ")}${bold(coral(username))}${bold("!")}`);
|
|
311
|
+
left.push("");
|
|
312
|
+
for (const artLine of SWARM_ART) {
|
|
313
|
+
const artVisible = stripAnsi(artLine).length;
|
|
314
|
+
const artPad = Math.max(0, Math.floor((LEFT_W - artVisible) / 2));
|
|
315
|
+
left.push(" ".repeat(artPad) + artLine);
|
|
316
|
+
}
|
|
317
|
+
left.push("");
|
|
318
|
+
const primaryAgent = availableAgents.length > 0 ? availableAgents[0].name : "opencode";
|
|
319
|
+
left.push(` ${dim(`v${VERSION}`)} ${dim(symbols.dot)} ${dim(`${primaryAgent} agent`)}`);
|
|
320
|
+
left.push(` ${dim(process.cwd())}`);
|
|
321
|
+
// ── Build right column ───────────────────────────────────────────────
|
|
322
|
+
const right = [];
|
|
323
|
+
right.push(coral(bold("Tips for getting started")));
|
|
324
|
+
right.push(`Point swarm at any git repo with a task:`);
|
|
325
|
+
right.push(`${yellow("$")} swarm --dir ./project ${dim('"your task"')}`);
|
|
326
|
+
right.push(`Use ${cyan("--dry-run")} to plan without executing`);
|
|
327
|
+
right.push(`Use ${cyan("--verbose")} for detailed progress`);
|
|
328
|
+
right.push("");
|
|
329
|
+
right.push(coral(bold("Environment")));
|
|
330
|
+
if (gitVer) {
|
|
331
|
+
right.push(`${green(symbols.check)} git ${dim(`v${gitVer}`)}`);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
right.push(`${red(symbols.cross)} git ${dim("not found (required)")}`);
|
|
335
|
+
}
|
|
336
|
+
if (apiKeys.size > 0) {
|
|
337
|
+
for (const p of PROVIDERS) {
|
|
338
|
+
const val = apiKeys.get(p.envVar);
|
|
339
|
+
if (val)
|
|
340
|
+
right.push(`${green(symbols.check)} ${p.name} ${dim(maskKey(val))}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
right.push(`${yellow(symbols.warn)} ${dim("No API keys configured")}`);
|
|
345
|
+
}
|
|
346
|
+
for (const a of availableAgents) {
|
|
347
|
+
right.push(`${green(symbols.check)} ${a.name} ${dim("agent")}`);
|
|
348
|
+
}
|
|
349
|
+
for (const a of missingAgents) {
|
|
350
|
+
right.push(`${dim(symbols.dash)} ${dim(a.name)} ${dim("not found")}`);
|
|
351
|
+
}
|
|
352
|
+
// ── Render two-column layout ─────────────────────────────────────────
|
|
353
|
+
renderTwoColumn(left, right, LEFT_W);
|
|
354
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
355
|
+
// AGENT SETUP WIZARD
|
|
356
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
357
|
+
sectionHeader("Agent Setup", w);
|
|
358
|
+
// ── Step 1: Pick default agent ───────────────────────────────────────
|
|
359
|
+
let chosenAgent = "opencode";
|
|
360
|
+
if (availableAgents.length > 1) {
|
|
361
|
+
process.stderr.write(` ${bold("Choose your default coding agent:")}\n\n`);
|
|
362
|
+
const agentChoices = availableAgents.map((a, i) => {
|
|
363
|
+
const info = AGENT_PROVIDERS[a.name];
|
|
364
|
+
const desc = info ? dim(info.description) : "";
|
|
365
|
+
const rec = a.name === "opencode" ? ` ${coral("(recommended)")}` : "";
|
|
366
|
+
return { idx: i + 1, name: a.name, line: ` ${cyan(String(i + 1))} ${bold(a.name)}${rec} ${desc}` };
|
|
367
|
+
});
|
|
368
|
+
for (const c of agentChoices)
|
|
369
|
+
process.stderr.write(`${c.line}\n`);
|
|
370
|
+
process.stderr.write("\n");
|
|
371
|
+
const prompt = createPrompt();
|
|
372
|
+
const agentChoice = await prompt.ask(` ${coral(symbols.arrow)} Choice [1]: `);
|
|
373
|
+
prompt.close();
|
|
374
|
+
const idx = parseInt(agentChoice, 10);
|
|
375
|
+
if (idx >= 1 && idx <= agentChoices.length) {
|
|
376
|
+
chosenAgent = agentChoices[idx - 1].name;
|
|
377
|
+
}
|
|
378
|
+
else if (agentChoice === "") {
|
|
379
|
+
chosenAgent = agentChoices[0].name;
|
|
380
|
+
}
|
|
381
|
+
process.stderr.write(` ${green(symbols.check)} Default agent: ${bold(chosenAgent)}\n`);
|
|
382
|
+
}
|
|
383
|
+
else if (availableAgents.length === 1) {
|
|
384
|
+
chosenAgent = availableAgents[0].name;
|
|
385
|
+
process.stderr.write(` ${green(symbols.check)} Default agent: ${bold(chosenAgent)} ${dim("(only available backend)")}\n`);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
// No agents installed — show install instructions
|
|
389
|
+
process.stderr.write(` ${yellow(symbols.warn)} No coding agents found. Install at least one:\n\n`);
|
|
390
|
+
for (const [name, info] of Object.entries(AGENT_PROVIDERS)) {
|
|
391
|
+
if (name === "direct-llm")
|
|
392
|
+
continue;
|
|
393
|
+
process.stderr.write(` ${cyan(symbols.arrow)} ${bold(name)} ${dim(info.install)}\n`);
|
|
394
|
+
process.stderr.write(` ${dim(info.description)}\n`);
|
|
395
|
+
}
|
|
396
|
+
process.stderr.write(`\n ${dim("Using direct-llm (no coding agent) as fallback.")}\n`);
|
|
397
|
+
chosenAgent = "direct-llm";
|
|
398
|
+
}
|
|
399
|
+
// ── Step 2: Configure API keys ───────────────────────────────────────
|
|
400
|
+
const agentInfo = AGENT_PROVIDERS[chosenAgent];
|
|
401
|
+
const neededKeys = agentInfo?.required ?? ["ANTHROPIC_API_KEY"];
|
|
402
|
+
// For agents that accept any provider (opencode, aider), at least one key is needed
|
|
403
|
+
const needsAnyKey = neededKeys.length > 1;
|
|
404
|
+
const missingKeys = neededKeys.filter((k) => !apiKeys.has(k));
|
|
405
|
+
const hasAnyNeeded = neededKeys.some((k) => apiKeys.has(k));
|
|
406
|
+
if (missingKeys.length > 0 && !(needsAnyKey && hasAnyNeeded)) {
|
|
407
|
+
sectionHeader("API Keys", w);
|
|
408
|
+
if (needsAnyKey) {
|
|
409
|
+
process.stderr.write(` ${bold(chosenAgent)} supports multiple providers. Configure at least one:\n\n`);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
process.stderr.write(` ${bold(chosenAgent)} needs the following API key(s):\n\n`);
|
|
413
|
+
}
|
|
414
|
+
for (const envVar of missingKeys) {
|
|
415
|
+
const provider = PROVIDERS.find((p) => p.envVar === envVar);
|
|
416
|
+
if (!provider)
|
|
417
|
+
continue;
|
|
418
|
+
const prompt = createPrompt();
|
|
419
|
+
const yn = await prompt.ask(` ${coral(symbols.arrow)} Configure ${bold(provider.name)} (${dim(envVar)})? [y/n]: `);
|
|
420
|
+
prompt.close();
|
|
421
|
+
if (yn.toLowerCase() !== "y" && yn.toLowerCase() !== "yes") {
|
|
422
|
+
process.stderr.write(` ${dim(symbols.dash)} Skipped ${provider.name}\n`);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
process.stderr.write(` ${dim(`Paste your ${provider.name} API key (input hidden):`)}\n`);
|
|
426
|
+
const key = await readHiddenInput(` ${coral(symbols.arrow)} `);
|
|
427
|
+
if (key && key.length >= 10) {
|
|
428
|
+
saveCredential(envVar, key);
|
|
429
|
+
apiKeys.set(envVar, key);
|
|
430
|
+
process.stderr.write(` ${green(symbols.check)} Saved ${provider.name} key to ${dim("~/.swarm/credentials")}\n\n`);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
process.stderr.write(` ${dim("Skipped — set later in .env or ~/.swarm/credentials")}\n\n`);
|
|
434
|
+
}
|
|
435
|
+
// For multi-provider agents, stop after first successful key
|
|
436
|
+
if (needsAnyKey && apiKeys.has(envVar))
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (apiKeys.size > 0) {
|
|
441
|
+
// Keys already configured — just confirm
|
|
442
|
+
process.stderr.write(` ${green(symbols.check)} API keys already configured\n`);
|
|
443
|
+
}
|
|
444
|
+
// ── Step 3: Choose default model ─────────────────────────────────────
|
|
445
|
+
// Suggest a model based on available keys
|
|
446
|
+
const configuredProviders = PROVIDERS.filter((p) => apiKeys.has(p.envVar));
|
|
447
|
+
let chosenModel = "anthropic/claude-sonnet-4-6"; // default
|
|
448
|
+
if (configuredProviders.length > 0) {
|
|
449
|
+
sectionHeader("Default Model", w);
|
|
450
|
+
const modelOptions = configuredProviders.flatMap((p) => {
|
|
451
|
+
const models = [];
|
|
452
|
+
if (p.envVar === "ANTHROPIC_API_KEY") {
|
|
453
|
+
models.push({
|
|
454
|
+
label: `claude-sonnet-4-6 ${dim("(fast, capable)")}`,
|
|
455
|
+
value: "anthropic/claude-sonnet-4-6",
|
|
456
|
+
recommended: true,
|
|
457
|
+
}, { label: `claude-opus-4-6 ${dim("(most capable)")}`, value: "anthropic/claude-opus-4-6" });
|
|
458
|
+
}
|
|
459
|
+
else if (p.envVar === "OPENAI_API_KEY") {
|
|
460
|
+
models.push({ label: `gpt-4o ${dim("(fast, versatile)")}`, value: "openai/gpt-4o" }, { label: `o3 ${dim("(reasoning)")}`, value: "openai/o3" });
|
|
461
|
+
}
|
|
462
|
+
else if (p.envVar === "GEMINI_API_KEY") {
|
|
463
|
+
models.push({ label: `gemini-2.5-flash ${dim("(fast, cheap)")}`, value: "google/gemini-2.5-flash" }, { label: `gemini-2.5-pro ${dim("(capable)")}`, value: "google/gemini-2.5-pro" });
|
|
464
|
+
}
|
|
465
|
+
return models;
|
|
466
|
+
});
|
|
467
|
+
if (modelOptions.length > 0) {
|
|
468
|
+
process.stderr.write(` ${bold("Pick a default model for coding threads:")}\n\n`);
|
|
469
|
+
for (let i = 0; i < modelOptions.length; i++) {
|
|
470
|
+
const opt = modelOptions[i];
|
|
471
|
+
const rec = opt.recommended ? ` ${coral("(recommended)")}` : "";
|
|
472
|
+
process.stderr.write(` ${cyan(String(i + 1))} ${opt.label}${rec}\n`);
|
|
473
|
+
}
|
|
474
|
+
process.stderr.write("\n");
|
|
475
|
+
const prompt = createPrompt();
|
|
476
|
+
const modelChoice = await prompt.ask(` ${coral(symbols.arrow)} Choice [1]: `);
|
|
477
|
+
prompt.close();
|
|
478
|
+
const idx = parseInt(modelChoice, 10);
|
|
479
|
+
if (idx >= 1 && idx <= modelOptions.length) {
|
|
480
|
+
chosenModel = modelOptions[idx - 1].value;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
chosenModel = modelOptions[0].value;
|
|
484
|
+
}
|
|
485
|
+
process.stderr.write(` ${green(symbols.check)} Default model: ${bold(chosenModel)}\n`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// ── Step 4: Save config ──────────────────────────────────────────────
|
|
489
|
+
saveUserConfig(chosenAgent, chosenModel);
|
|
490
|
+
// ── Final summary ────────────────────────────────────────────────────
|
|
491
|
+
sectionHeader("Ready", w);
|
|
492
|
+
process.stderr.write(` ${green(symbols.check)} Agent: ${bold(chosenAgent)}\n`);
|
|
493
|
+
process.stderr.write(` ${green(symbols.check)} Model: ${bold(chosenModel)}\n`);
|
|
494
|
+
const keyNames = [...apiKeys.keys()].map((k) => {
|
|
495
|
+
const p = PROVIDERS.find((pr) => pr.envVar === k);
|
|
496
|
+
return p?.name ?? k;
|
|
497
|
+
});
|
|
498
|
+
if (keyNames.length > 0) {
|
|
499
|
+
process.stderr.write(` ${green(symbols.check)} Keys: ${bold(keyNames.join(", "))}\n`);
|
|
500
|
+
}
|
|
501
|
+
process.stderr.write(` ${green(symbols.check)} Config: ${dim("~/.swarm/config.yaml")}\n`);
|
|
502
|
+
process.stderr.write(`\n ${dim("Run")} ${yellow('swarm --dir ./project "your task"')} ${dim("to get started.")}\n`);
|
|
503
|
+
process.stderr.write(` ${dim("Edit")} ${cyan("~/.swarm/config.yaml")} ${dim("to change these settings anytime.")}\n`);
|
|
504
|
+
// If still missing critical deps, show warnings
|
|
505
|
+
if (!gitVer) {
|
|
506
|
+
process.stderr.write(`\n ${red(symbols.cross)} ${bold("git is required.")} Install it before using swarm.\n`);
|
|
507
|
+
}
|
|
508
|
+
if (apiKeys.size === 0) {
|
|
509
|
+
process.stderr.write(`\n ${yellow(symbols.warn)} ${bold("No API keys configured.")}\n`);
|
|
510
|
+
process.stderr.write(` ${dim("Add keys to")} ${cyan("~/.swarm/credentials")} ${dim("or")} ${cyan(".env")}${dim(":")}\n`);
|
|
511
|
+
process.stderr.write(` ${dim("ANTHROPIC_API_KEY=sk-ant-...")}\n`);
|
|
512
|
+
process.stderr.write(` ${dim("OPENAI_API_KEY=sk-...")}\n`);
|
|
513
|
+
process.stderr.write(` ${dim("GEMINI_API_KEY=AI...")}\n`);
|
|
514
|
+
}
|
|
515
|
+
process.stderr.write("\n");
|
|
516
|
+
markInitialized();
|
|
517
|
+
}
|
|
518
|
+
//# sourceMappingURL=onboarding.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animated spinner with rotating verbs — inspired by Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* Runs on an isolated 80ms animation loop separated from other output.
|
|
5
|
+
* Displays a coral-colored spinner glyph + a rotating verb + optional detail.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Spinner {
|
|
8
|
+
private static _exitHandlerRegistered;
|
|
9
|
+
private intervalId;
|
|
10
|
+
private frameIdx;
|
|
11
|
+
private totalFrames;
|
|
12
|
+
private verbIdx;
|
|
13
|
+
private detail;
|
|
14
|
+
private startTime;
|
|
15
|
+
private running;
|
|
16
|
+
/** Start the spinner. */
|
|
17
|
+
start(detail?: string): void;
|
|
18
|
+
/** Update the detail text without restarting. */
|
|
19
|
+
update(detail: string): void;
|
|
20
|
+
/** Stop the spinner and clear the line. */
|
|
21
|
+
stop(): void;
|
|
22
|
+
/** Whether the spinner is currently active. */
|
|
23
|
+
get isActive(): boolean;
|
|
24
|
+
private render;
|
|
25
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animated spinner with rotating verbs — inspired by Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* Runs on an isolated 80ms animation loop separated from other output.
|
|
5
|
+
* Displays a coral-colored spinner glyph + a rotating verb + optional detail.
|
|
6
|
+
*/
|
|
7
|
+
import { coral, dim, isTTY, stripAnsi, termWidth } from "./theme.js";
|
|
8
|
+
/** Playful verbs shown while the spinner is active. */
|
|
9
|
+
const VERBS = [
|
|
10
|
+
"Orchestrating",
|
|
11
|
+
"Decomposing",
|
|
12
|
+
"Analyzing",
|
|
13
|
+
"Spawning",
|
|
14
|
+
"Synthesizing",
|
|
15
|
+
"Weaving",
|
|
16
|
+
"Parallelizing",
|
|
17
|
+
"Routing",
|
|
18
|
+
"Compressing",
|
|
19
|
+
"Dispatching",
|
|
20
|
+
"Coordinating",
|
|
21
|
+
"Assembling",
|
|
22
|
+
"Evaluating",
|
|
23
|
+
"Distributing",
|
|
24
|
+
"Reasoning",
|
|
25
|
+
"Merging",
|
|
26
|
+
"Refactoring",
|
|
27
|
+
"Scanning",
|
|
28
|
+
"Computing",
|
|
29
|
+
"Resolving",
|
|
30
|
+
"Strategizing",
|
|
31
|
+
"Threading",
|
|
32
|
+
"Optimizing",
|
|
33
|
+
"Composing",
|
|
34
|
+
"Investigating",
|
|
35
|
+
"Constructing",
|
|
36
|
+
"Iterating",
|
|
37
|
+
"Transforming",
|
|
38
|
+
];
|
|
39
|
+
const SPINNER_CHARS = isTTY ? ["\u00B7", "\u2726", "\u2733", "\u2736", "\u273B", "\u273D"] : ["*"];
|
|
40
|
+
export class Spinner {
|
|
41
|
+
static _exitHandlerRegistered = false;
|
|
42
|
+
intervalId = null;
|
|
43
|
+
frameIdx = 0;
|
|
44
|
+
totalFrames = 0;
|
|
45
|
+
verbIdx = Math.floor(Math.random() * VERBS.length);
|
|
46
|
+
detail = "";
|
|
47
|
+
startTime = 0;
|
|
48
|
+
running = false;
|
|
49
|
+
/** Start the spinner. */
|
|
50
|
+
start(detail) {
|
|
51
|
+
if (!isTTY || this.running)
|
|
52
|
+
return;
|
|
53
|
+
this.running = true;
|
|
54
|
+
this.detail = detail || "";
|
|
55
|
+
this.startTime = Date.now();
|
|
56
|
+
this.frameIdx = 0;
|
|
57
|
+
this.totalFrames = 0;
|
|
58
|
+
this.verbIdx = Math.floor(Math.random() * VERBS.length);
|
|
59
|
+
// Hide cursor + register exit handler to restore it
|
|
60
|
+
process.stderr.write("\x1b[?25l");
|
|
61
|
+
if (!Spinner._exitHandlerRegistered) {
|
|
62
|
+
Spinner._exitHandlerRegistered = true;
|
|
63
|
+
process.on("exit", () => {
|
|
64
|
+
process.stderr.write("\x1b[?25h");
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const id = setInterval(() => {
|
|
68
|
+
this.render();
|
|
69
|
+
this.frameIdx = (this.frameIdx + 1) % SPINNER_CHARS.length;
|
|
70
|
+
this.totalFrames++;
|
|
71
|
+
// Rotate verb every ~2 seconds (25 frames at 80ms)
|
|
72
|
+
if (this.totalFrames % 25 === 0) {
|
|
73
|
+
this.verbIdx = (this.verbIdx + 1) % VERBS.length;
|
|
74
|
+
}
|
|
75
|
+
}, 80);
|
|
76
|
+
id.unref();
|
|
77
|
+
this.intervalId = id;
|
|
78
|
+
}
|
|
79
|
+
/** Update the detail text without restarting. */
|
|
80
|
+
update(detail) {
|
|
81
|
+
this.detail = detail;
|
|
82
|
+
}
|
|
83
|
+
/** Stop the spinner and clear the line. */
|
|
84
|
+
stop() {
|
|
85
|
+
if (!this.running)
|
|
86
|
+
return;
|
|
87
|
+
this.running = false;
|
|
88
|
+
if (this.intervalId) {
|
|
89
|
+
clearInterval(this.intervalId);
|
|
90
|
+
this.intervalId = null;
|
|
91
|
+
}
|
|
92
|
+
// Clear spinner line and show cursor
|
|
93
|
+
process.stderr.write("\r\x1b[K\x1b[?25h");
|
|
94
|
+
}
|
|
95
|
+
/** Whether the spinner is currently active. */
|
|
96
|
+
get isActive() {
|
|
97
|
+
return this.running;
|
|
98
|
+
}
|
|
99
|
+
render() {
|
|
100
|
+
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
101
|
+
const char = coral(SPINNER_CHARS[this.frameIdx]);
|
|
102
|
+
const verb = VERBS[this.verbIdx];
|
|
103
|
+
const time = dim(`${elapsed}s`);
|
|
104
|
+
const detail = this.detail ? dim(` ${this.detail}`) : "";
|
|
105
|
+
const line = ` ${char} ${verb}...${detail} ${time}`;
|
|
106
|
+
const maxW = termWidth();
|
|
107
|
+
const stripped = stripAnsi(line);
|
|
108
|
+
// Truncate if wider than terminal
|
|
109
|
+
const output = stripped.length > maxW ? line.slice(0, maxW - 1) : line;
|
|
110
|
+
process.stderr.write(`\r\x1b[K${output}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=spinner.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-of-session summary — shows a clean recap of what happened.
|
|
3
|
+
*/
|
|
4
|
+
import type { BudgetState, ThreadState } from "../core/types.js";
|
|
5
|
+
import type { ThreadCacheStats } from "../threads/cache.js";
|
|
6
|
+
export interface SessionSummary {
|
|
7
|
+
elapsed: number;
|
|
8
|
+
iterations: number;
|
|
9
|
+
subQueries: number;
|
|
10
|
+
completed: boolean;
|
|
11
|
+
answer: string;
|
|
12
|
+
threads: ThreadState[];
|
|
13
|
+
budget: BudgetState;
|
|
14
|
+
cacheStats?: ThreadCacheStats;
|
|
15
|
+
episodeCount?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Render the end-of-session summary. */
|
|
18
|
+
export declare function renderSummary(summary: SessionSummary): void;
|