swarm-code 0.1.0 → 0.1.2

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/dist/cli.js CHANGED
@@ -21,10 +21,10 @@ const { runRlmLoop } = await import("./core/rlm.js");
21
21
  // ── Arg parsing ─────────────────────────────────────────────────────────────
22
22
  function usage() {
23
23
  console.error(`
24
- swarm run — Recursive Language Model CLI (arXiv:2512.24601)
24
+ swarm run — Text processing mode (RLM)
25
25
 
26
26
  USAGE
27
- rlm run [OPTIONS] "<query>"
27
+ swarm run [OPTIONS] "<query>"
28
28
 
29
29
  OPTIONS
30
30
  --model <id> Model ID (default: RLM_MODEL from .env)
@@ -34,9 +34,9 @@ OPTIONS
34
34
  --verbose Show iteration progress
35
35
 
36
36
  EXAMPLES
37
- rlm run --file big.txt "List all classes"
38
- curl -s https://example.com/large.py | rlm run --stdin "Summarize"
39
- rlm run --url https://raw.githubusercontent.com/.../typing.py "Count public classes"
37
+ swarm run --file big.txt "List all classes"
38
+ curl -s https://example.com/large.py | swarm run --stdin "Summarize"
39
+ swarm run --url https://raw.githubusercontent.com/.../typing.py "Count public classes"
40
40
  `.trim());
41
41
  process.exit(1);
42
42
  }
@@ -8,6 +8,7 @@
8
8
  * - Browse previous trajectories
9
9
  */
10
10
  import "./env.js";
11
+ import { execFileSync, execSync, spawn as spawnChild } from "node:child_process";
11
12
  import * as fs from "node:fs";
12
13
  import * as os from "node:os";
13
14
  import * as path from "node:path";
@@ -306,17 +307,127 @@ function handleMultiLineAsContext(input) {
306
307
  return null;
307
308
  }
308
309
  // ── Banner ──────────────────────────────────────────────────────────────────
310
+ // ── Ollama helpers ──────────────────────────────────────────────────────────
311
+ function isOllamaInstalled() {
312
+ try {
313
+ execFileSync("ollama", ["--version"], { stdio: ["ignore", "pipe", "pipe"], timeout: 5000 });
314
+ return true;
315
+ }
316
+ catch {
317
+ return false;
318
+ }
319
+ }
320
+ function isOllamaModelAvailable(model) {
321
+ try {
322
+ const output = execFileSync("ollama", ["list"], {
323
+ encoding: "utf-8",
324
+ stdio: ["ignore", "pipe", "pipe"],
325
+ timeout: 10000,
326
+ });
327
+ return output.includes(model);
328
+ }
329
+ catch {
330
+ return false;
331
+ }
332
+ }
333
+ async function installOllama() {
334
+ console.log(`\n ${c.bold}Installing Ollama...${c.reset}\n`);
335
+ if (process.platform === "darwin") {
336
+ // macOS: check for brew first, otherwise use the install script
337
+ try {
338
+ execFileSync("brew", ["--version"], { stdio: "ignore", timeout: 5000 });
339
+ console.log(` ${c.dim}Using Homebrew...${c.reset}`);
340
+ try {
341
+ execSync("brew install ollama", { stdio: "inherit", timeout: 120000 });
342
+ return true;
343
+ }
344
+ catch {
345
+ console.log(` ${c.dim}Homebrew install failed, trying curl installer...${c.reset}`);
346
+ }
347
+ }
348
+ catch {
349
+ // No brew — fall through to curl
350
+ }
351
+ }
352
+ // Linux / macOS fallback: official install script
353
+ if (process.platform === "linux" || process.platform === "darwin") {
354
+ try {
355
+ execSync("curl -fsSL https://ollama.com/install.sh | sh", { stdio: "inherit", timeout: 180000 });
356
+ return true;
357
+ }
358
+ catch {
359
+ return false;
360
+ }
361
+ }
362
+ // Windows: direct user to download page
363
+ console.log(` ${c.dim}Download Ollama from: https://ollama.com/download${c.reset}`);
364
+ return false;
365
+ }
366
+ async function pullOllamaModel(model) {
367
+ console.log(`\n ${c.bold}Pulling ${model}...${c.reset} ${c.dim}(this may take a few minutes)${c.reset}\n`);
368
+ return new Promise((resolve) => {
369
+ const child = spawnChild("ollama", ["pull", model], { stdio: "inherit" });
370
+ child.on("close", (code) => resolve(code === 0));
371
+ child.on("error", () => resolve(false));
372
+ });
373
+ }
374
+ async function ensureOllamaSetup(rl, model) {
375
+ // 1. Check if Ollama is installed
376
+ if (!isOllamaInstalled()) {
377
+ console.log(`\n ${c.dim}Ollama is not installed. It's needed to run open-source models locally.${c.reset}`);
378
+ const install = await questionWithEsc(rl, ` ${c.cyan}Install Ollama now? [Y/n]:${c.reset} `);
379
+ if (install !== null && install.toLowerCase() !== "n" && install.toLowerCase() !== "no") {
380
+ const ok = await installOllama();
381
+ if (!ok) {
382
+ console.log(`\n ${c.red}Failed to install Ollama.${c.reset}`);
383
+ console.log(` ${c.dim}Install manually from https://ollama.com/download${c.reset}\n`);
384
+ return false;
385
+ }
386
+ console.log(`\n ${c.green}✓${c.reset} Ollama installed\n`);
387
+ }
388
+ else {
389
+ console.log(`\n ${c.dim}Ollama is required for open-source models.${c.reset}`);
390
+ console.log(` ${c.dim}Install later from https://ollama.com/download${c.reset}\n`);
391
+ return false;
392
+ }
393
+ }
394
+ else {
395
+ console.log(`\n ${c.green}✓${c.reset} Ollama installed`);
396
+ }
397
+ // 2. Check if model is already pulled
398
+ const shortModel = model.replace("ollama/", "");
399
+ if (isOllamaModelAvailable(shortModel)) {
400
+ console.log(` ${c.green}✓${c.reset} Model ${c.bold}${shortModel}${c.reset} ready\n`);
401
+ return true;
402
+ }
403
+ // 3. Pull the model
404
+ console.log(` ${c.dim}Model ${shortModel} not found locally.${c.reset}`);
405
+ const pull = await questionWithEsc(rl, ` ${c.cyan}Pull ${shortModel} now? [Y/n]:${c.reset} `);
406
+ if (pull !== null && pull.toLowerCase() !== "n" && pull.toLowerCase() !== "no") {
407
+ const ok = await pullOllamaModel(shortModel);
408
+ if (!ok) {
409
+ console.log(`\n ${c.red}Failed to pull ${shortModel}.${c.reset}`);
410
+ console.log(` ${c.dim}Try manually: ollama pull ${shortModel}${c.reset}\n`);
411
+ return false;
412
+ }
413
+ console.log(`\n ${c.green}✓${c.reset} Model ${c.bold}${shortModel}${c.reset} ready\n`);
414
+ return true;
415
+ }
416
+ console.log(`\n ${c.dim}Run later: ollama pull ${shortModel}${c.reset}\n`);
417
+ return false;
418
+ }
419
+ // ── Banner ──────────────────────────────────────────────────────────────────
309
420
  function printBanner() {
310
421
  console.log(`
311
422
  ${c.cyan}${c.bold}
312
- ██████╗ ██╗ ███╗ ███╗
313
- ██╔══██╗██║ ████╗ ████║
314
- ██████╔╝██║ ██╔████╔██║
315
- ██╔══██╗██║ ██║╚██╔╝██║
316
- ██║ ██║███████╗██║ ╚═╝ ██║
317
- ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
423
+ ███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗
424
+ ██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║
425
+ ███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║
426
+ ╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║
427
+ ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║
428
+ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
318
429
  ${c.reset}
319
- ${c.dim} Recursive Language Models — arXiv:2512.24601${c.reset}
430
+ ${c.dim} Swarm-native coding agent orchestrator${c.reset}
320
431
  `);
321
432
  }
322
433
  // ── Status line ─────────────────────────────────────────────────────────────
@@ -1285,45 +1396,145 @@ async function interactive() {
1285
1396
  if (!hasAnyApiKey()) {
1286
1397
  printBanner();
1287
1398
  console.log(` ${c.bold}Welcome! Let's get you set up.${c.reset}\n`);
1399
+ // Agent definitions — each lists which API keys it needs (if any)
1400
+ const AGENT_CHOICES = [
1401
+ {
1402
+ name: "OpenCode",
1403
+ id: "opencode",
1404
+ desc: "Multi-provider + open-source (Ollama, DeepSeek, Kimi, GLM, ...)",
1405
+ keys: SETUP_PROVIDERS, // optionally accepts any provider
1406
+ requiresKey: false, // can run with local/open-source models, no API key needed
1407
+ },
1408
+ {
1409
+ name: "Claude Code",
1410
+ id: "claude-code",
1411
+ desc: "Anthropic",
1412
+ keys: SETUP_PROVIDERS.filter((p) => p.piProvider === "anthropic"),
1413
+ requiresKey: true,
1414
+ },
1415
+ {
1416
+ name: "Codex",
1417
+ id: "codex",
1418
+ desc: "OpenAI",
1419
+ keys: SETUP_PROVIDERS.filter((p) => p.piProvider === "openai"),
1420
+ requiresKey: true,
1421
+ },
1422
+ {
1423
+ name: "Aider",
1424
+ id: "aider",
1425
+ desc: "Git-aware AI pair programmer (Anthropic, OpenAI)",
1426
+ keys: SETUP_PROVIDERS.filter((p) => p.piProvider === "anthropic" || p.piProvider === "openai"),
1427
+ requiresKey: true,
1428
+ },
1429
+ {
1430
+ name: "Direct LLM",
1431
+ id: "direct-llm",
1432
+ desc: "Bare API calls, no coding agent",
1433
+ keys: SETUP_PROVIDERS,
1434
+ requiresKey: true,
1435
+ },
1436
+ ];
1288
1437
  const setupRl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
1289
1438
  let setupDone = false;
1290
1439
  while (!setupDone) {
1291
- console.log(` ${c.bold}Select your provider:${c.reset}\n`);
1292
- for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
1293
- console.log(` ${c.dim}${i + 1}${c.reset} ${SETUP_PROVIDERS[i].name} ${c.dim}(${SETUP_PROVIDERS[i].label})${c.reset}`);
1440
+ console.log(` ${c.bold}Select your coding agent(s):${c.reset}\n`);
1441
+ for (let i = 0; i < AGENT_CHOICES.length; i++) {
1442
+ const a = AGENT_CHOICES[i];
1443
+ console.log(` ${c.dim}${i + 1}${c.reset} ${a.name} ${c.dim}${a.desc}${c.reset}`);
1294
1444
  }
1295
1445
  console.log();
1296
- const choice = await questionWithEsc(setupRl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} `);
1446
+ console.log(` ${c.dim}Enter numbers separated by commas (e.g. 1,2,3)${c.reset}\n`);
1447
+ const choice = await questionWithEsc(setupRl, ` ${c.cyan}Agent(s) [1-${AGENT_CHOICES.length}]:${c.reset} `);
1297
1448
  if (choice === null) {
1298
- // ESC at provider selection → exit
1299
1449
  console.log(`\n ${c.dim}Exiting.${c.reset}\n`);
1300
1450
  setupRl.close();
1301
1451
  process.exit(0);
1302
1452
  }
1303
- const idx = parseInt(choice, 10) - 1;
1304
- if (Number.isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
1453
+ // Parse comma-separated selections
1454
+ const indices = choice
1455
+ .split(",")
1456
+ .map((s) => parseInt(s.trim(), 10) - 1)
1457
+ .filter((i) => !Number.isNaN(i) && i >= 0 && i < AGENT_CHOICES.length);
1458
+ // Deduplicate
1459
+ const uniqueIndices = [...new Set(indices)];
1460
+ if (uniqueIndices.length === 0) {
1305
1461
  console.log(`\n ${c.dim}Invalid choice.${c.reset}\n`);
1306
1462
  continue;
1307
1463
  }
1308
- const provider = SETUP_PROVIDERS[idx];
1309
- const gotKey = await promptForProviderKey(setupRl, provider);
1310
- if (gotKey === null) {
1311
- // ESC at key entry → back to provider selection
1312
- console.log();
1313
- continue;
1464
+ const selectedAgents = uniqueIndices.map((i) => AGENT_CHOICES[i]);
1465
+ console.log();
1466
+ for (const agent of selectedAgents) {
1467
+ console.log(` ${c.green}✓${c.reset} ${c.bold}${agent.name}${c.reset}`);
1314
1468
  }
1315
- if (!gotKey) {
1316
- console.log(`\n ${c.dim}No key provided. Exiting.${c.reset}\n`);
1317
- setupRl.close();
1318
- process.exit(0);
1469
+ console.log();
1470
+ // Walk through each selected agent and configure keys
1471
+ const configuredProviders = new Set();
1472
+ for (const agent of selectedAgents) {
1473
+ if (agent.requiresKey) {
1474
+ console.log(` ${c.bold}${agent.name}${c.reset} ${c.dim}requires an API key. Configure one now:${c.reset}\n`);
1475
+ for (const provider of agent.keys) {
1476
+ // Skip providers already configured in this session
1477
+ if (configuredProviders.has(provider.env)) {
1478
+ console.log(` ${c.green}✓${c.reset} ${provider.name} ${c.dim}(already configured)${c.reset}`);
1479
+ continue;
1480
+ }
1481
+ const gotKey = await promptForProviderKey(setupRl, provider);
1482
+ if (gotKey === null)
1483
+ break; // ESC — skip remaining
1484
+ if (gotKey) {
1485
+ configuredProviders.add(provider.env);
1486
+ break; // Got one — enough for this agent
1487
+ }
1488
+ }
1489
+ // Check if this agent still has no key
1490
+ const agentHasKey = agent.keys.some((p) => configuredProviders.has(p.env) || process.env[p.env]);
1491
+ if (!agentHasKey) {
1492
+ console.log(`\n ${c.dim}No key for ${agent.name} — it won't be available until a key is set in .env${c.reset}\n`);
1493
+ }
1494
+ }
1495
+ else {
1496
+ // Agent works without keys (e.g. OpenCode with open-source models)
1497
+ console.log(` ${c.bold}${agent.name}${c.reset} ${c.dim}works with open-source models (no API key needed).${c.reset}`);
1498
+ console.log(` ${c.dim}You can run locally with Ollama, or add a cloud API key.${c.reset}\n`);
1499
+ const setupChoice = await questionWithEsc(setupRl, ` ${c.cyan}Set up with: [1] Ollama (open-source) [2] API key [1]:${c.reset} `);
1500
+ const wantsApiKey = setupChoice !== null && setupChoice.trim() === "2";
1501
+ if (wantsApiKey) {
1502
+ for (const provider of agent.keys) {
1503
+ if (configuredProviders.has(provider.env)) {
1504
+ console.log(` ${c.green}✓${c.reset} ${provider.name} ${c.dim}(already configured)${c.reset}`);
1505
+ continue;
1506
+ }
1507
+ const gotKey = await promptForProviderKey(setupRl, provider);
1508
+ if (gotKey === null)
1509
+ break;
1510
+ if (gotKey) {
1511
+ configuredProviders.add(provider.env);
1512
+ break;
1513
+ }
1514
+ }
1515
+ }
1516
+ else {
1517
+ // Set up Ollama + pull model
1518
+ await ensureOllamaSetup(setupRl, "ollama/deepseek-coder-v2");
1519
+ }
1520
+ console.log();
1521
+ }
1522
+ }
1523
+ // Set default model
1524
+ const activeProvider = Object.keys(PROVIDER_KEYS).find((p) => process.env[providerEnvKey(p)]);
1525
+ if (activeProvider) {
1526
+ currentProviderName = activeProvider;
1527
+ const defaultModel = getDefaultModelForProvider(activeProvider);
1528
+ if (defaultModel) {
1529
+ currentModelId = defaultModel;
1530
+ saveModelPreference(currentModelId);
1531
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
1532
+ }
1319
1533
  }
1320
- // Auto-select default model for chosen provider
1321
- currentProviderName = provider.piProvider;
1322
- const defaultModel = getDefaultModelForProvider(provider.piProvider);
1323
- if (defaultModel) {
1324
- currentModelId = defaultModel;
1534
+ else {
1535
+ currentModelId = "ollama/deepseek-coder-v2";
1325
1536
  saveModelPreference(currentModelId);
1326
- console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
1537
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset} ${c.dim}(open-source)${c.reset}`);
1327
1538
  }
1328
1539
  console.log();
1329
1540
  setupDone = true;
@@ -1358,8 +1569,19 @@ async function interactive() {
1358
1569
  }
1359
1570
  }
1360
1571
  if (!currentModel) {
1572
+ if (currentModelId.startsWith("ollama/")) {
1573
+ // Ollama model selected — this interactive REPL mode needs a cloud API.
1574
+ // Redirect to swarm mode which works with OpenCode + Ollama.
1575
+ console.log(`\n ${c.green}✓${c.reset} Ollama model selected: ${c.bold}${currentModelId}${c.reset}`);
1576
+ console.log(`\n ${c.dim}This interactive REPL uses direct LLM API calls.${c.reset}`);
1577
+ console.log(` ${c.dim}To use Ollama models with OpenCode, run:${c.reset}\n`);
1578
+ console.log(` ${c.bold}swarm --dir ./your-project "your task"${c.reset}\n`);
1579
+ process.exit(0);
1580
+ }
1361
1581
  console.log(`\n ${c.red}Model "${currentModelId}" not found.${c.reset}`);
1362
- console.log(` Check ${c.bold}RLM_MODEL${c.reset} in your .env file.\n`);
1582
+ console.log(` Set ${c.bold}ANTHROPIC_API_KEY${c.reset}, ${c.bold}OPENAI_API_KEY${c.reset}, or ${c.bold}GEMINI_API_KEY${c.reset} in your .env file.`);
1583
+ console.log(`\n ${c.dim}For agent-based mode (works with open-source models):${c.reset}`);
1584
+ console.log(` ${c.bold}swarm --dir ./your-project "your task"${c.reset}\n`);
1363
1585
  process.exit(1);
1364
1586
  }
1365
1587
  // Auto-load cwd context so the LLM knows the project structure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarm-code",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Open-source swarm-native coding agent orchestrator — spawns parallel coding agents in isolated git worktrees, built on RLM (arXiv:2512.24601)",
5
5
  "type": "module",
6
6
  "bin": {