swarm-code 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/interactive.js +283 -70
  2. package/package.json +1 -1
@@ -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,6 +307,219 @@ 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
+ function isOllamaServing() {
367
+ try {
368
+ execFileSync("curl", ["-sf", "http://127.0.0.1:11434/api/tags"], {
369
+ stdio: ["ignore", "pipe", "pipe"],
370
+ timeout: 3000,
371
+ });
372
+ return true;
373
+ }
374
+ catch {
375
+ return false;
376
+ }
377
+ }
378
+ function startOllamaServe() {
379
+ // Start ollama serve in the background (detached)
380
+ const child = spawnChild("ollama", ["serve"], {
381
+ stdio: "ignore",
382
+ detached: true,
383
+ });
384
+ child.unref();
385
+ }
386
+ async function pullOllamaModel(model) {
387
+ console.log(`\n ${c.bold}Pulling ${model}...${c.reset} ${c.dim}(this may take a few minutes)${c.reset}\n`);
388
+ return new Promise((resolve) => {
389
+ const child = spawnChild("ollama", ["pull", model], { stdio: "inherit" });
390
+ child.on("close", (code) => resolve(code === 0));
391
+ child.on("error", () => resolve(false));
392
+ });
393
+ }
394
+ async function ensureOllamaSetup(rl, model) {
395
+ const shortModel = model.replace("ollama/", "");
396
+ // 1. Check if Ollama is installed
397
+ if (!isOllamaInstalled()) {
398
+ console.log(`\n ${c.dim}Ollama is not installed. It's needed to run open-source models locally.${c.reset}`);
399
+ const install = await questionWithEsc(rl, ` ${c.cyan}Install Ollama now? [Y/n]:${c.reset} `);
400
+ if (install !== null && install.toLowerCase() !== "n" && install.toLowerCase() !== "no") {
401
+ const ok = await installOllama();
402
+ if (!ok) {
403
+ console.log(`\n ${c.red}Failed to install Ollama.${c.reset}`);
404
+ console.log(` ${c.dim}Install manually from https://ollama.com/download${c.reset}\n`);
405
+ return false;
406
+ }
407
+ console.log(` ${c.green}✓${c.reset} Ollama installed`);
408
+ }
409
+ else {
410
+ console.log(`\n ${c.dim}Install later from https://ollama.com/download${c.reset}\n`);
411
+ return false;
412
+ }
413
+ }
414
+ else {
415
+ console.log(` ${c.green}✓${c.reset} Ollama installed`);
416
+ }
417
+ // 2. Ensure ollama serve is running
418
+ if (!isOllamaServing()) {
419
+ console.log(` ${c.dim}Starting Ollama server...${c.reset}`);
420
+ startOllamaServe();
421
+ // Give it a moment to start
422
+ let retries = 10;
423
+ while (retries > 0 && !isOllamaServing()) {
424
+ await new Promise((r) => setTimeout(r, 1000));
425
+ retries--;
426
+ }
427
+ if (isOllamaServing()) {
428
+ console.log(` ${c.green}✓${c.reset} Ollama server running`);
429
+ }
430
+ else {
431
+ console.log(` ${c.yellow}⚠${c.reset} Could not start Ollama server. Run ${c.bold}ollama serve${c.reset} manually.`);
432
+ return false;
433
+ }
434
+ }
435
+ else {
436
+ console.log(` ${c.green}✓${c.reset} Ollama server running`);
437
+ }
438
+ // 3. Check if model is already pulled
439
+ if (isOllamaModelAvailable(shortModel)) {
440
+ console.log(` ${c.green}✓${c.reset} Model ${c.bold}${shortModel}${c.reset} ready\n`);
441
+ return true;
442
+ }
443
+ // 4. Pull the model
444
+ console.log(` ${c.dim}Model ${shortModel} not found locally.${c.reset}`);
445
+ const pull = await questionWithEsc(rl, `\n ${c.cyan}Pull ${shortModel} now? [Y/n]:${c.reset} `);
446
+ if (pull !== null && pull.toLowerCase() !== "n" && pull.toLowerCase() !== "no") {
447
+ const ok = await pullOllamaModel(shortModel);
448
+ if (!ok) {
449
+ console.log(`\n ${c.red}Failed to pull ${shortModel}.${c.reset}`);
450
+ console.log(` ${c.dim}Try manually: ollama pull ${shortModel}${c.reset}\n`);
451
+ return false;
452
+ }
453
+ console.log(` ${c.green}✓${c.reset} Model ${c.bold}${shortModel}${c.reset} ready\n`);
454
+ return true;
455
+ }
456
+ console.log(`\n ${c.dim}Run later: ollama pull ${shortModel}${c.reset}\n`);
457
+ return false;
458
+ }
459
+ /**
460
+ * Interactive checkbox selector — arrow keys to navigate, space to toggle, enter to confirm.
461
+ * Returns indices of selected items.
462
+ */
463
+ function checkboxSelect(items) {
464
+ return new Promise((resolve) => {
465
+ let cursor = 0;
466
+ const render = () => {
467
+ // Move cursor up to overwrite previous render (except first time)
468
+ if (rendered) {
469
+ process.stdout.write(`\x1b[${items.length}A`);
470
+ }
471
+ for (let i = 0; i < items.length; i++) {
472
+ const item = items[i];
473
+ const pointer = i === cursor ? `${c.cyan}❯${c.reset}` : " ";
474
+ const box = item.checked ? `${c.green}◼${c.reset}` : `${c.dim}◻${c.reset}`;
475
+ const label = i === cursor ? `${c.bold}${item.label}${c.reset}` : item.label;
476
+ const desc = `${c.dim}${item.desc}${c.reset}`;
477
+ process.stdout.write(`\x1b[2K ${pointer} ${box} ${label} ${desc}\n`);
478
+ }
479
+ };
480
+ let rendered = false;
481
+ render();
482
+ rendered = true;
483
+ const wasRaw = stdin.isRaw;
484
+ if (stdin.isTTY)
485
+ stdin.setRawMode(true);
486
+ const onData = (data) => {
487
+ const key = data.toString();
488
+ if (key === "\x1b[A" || key === "k") {
489
+ // Up
490
+ cursor = (cursor - 1 + items.length) % items.length;
491
+ render();
492
+ }
493
+ else if (key === "\x1b[B" || key === "j") {
494
+ // Down
495
+ cursor = (cursor + 1) % items.length;
496
+ render();
497
+ }
498
+ else if (key === " ") {
499
+ // Toggle
500
+ items[cursor].checked = !items[cursor].checked;
501
+ render();
502
+ }
503
+ else if (key === "\r" || key === "\n") {
504
+ // Confirm
505
+ stdin.removeListener("data", onData);
506
+ if (stdin.isTTY)
507
+ stdin.setRawMode(wasRaw ?? false);
508
+ const selected = items.map((item, i) => (item.checked ? i : -1)).filter((i) => i >= 0);
509
+ resolve(selected);
510
+ }
511
+ else if (key === "\x1b" || key === "\x03") {
512
+ // Escape or Ctrl+C
513
+ stdin.removeListener("data", onData);
514
+ if (stdin.isTTY)
515
+ stdin.setRawMode(wasRaw ?? false);
516
+ resolve([]);
517
+ }
518
+ };
519
+ stdin.on("data", onData);
520
+ });
521
+ }
522
+ // ── Banner ──────────────────────────────────────────────────────────────────
309
523
  function printBanner() {
310
524
  console.log(`
311
525
  ${c.cyan}${c.bold}
@@ -316,7 +530,7 @@ ${c.cyan}${c.bold}
316
530
  ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║
317
531
  ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
318
532
  ${c.reset}
319
- ${c.dim} Swarm-native coding agent orchestrator${c.reset}
533
+ ${c.dim} Swarm-native coding agent orchestrator${c.reset}
320
534
  `);
321
535
  }
322
536
  // ── Status line ─────────────────────────────────────────────────────────────
@@ -1323,90 +1537,78 @@ async function interactive() {
1323
1537
  requiresKey: true,
1324
1538
  },
1325
1539
  ];
1540
+ // ── Step 1: Checkbox agent selection ─────────────────────────
1541
+ console.log(` ${c.bold}Select your coding agent(s):${c.reset}`);
1542
+ console.log(` ${c.dim}↑/↓ navigate · space toggle · enter confirm${c.reset}\n`);
1543
+ const checkboxItems = AGENT_CHOICES.map((a) => ({
1544
+ label: a.name,
1545
+ desc: a.desc,
1546
+ checked: false,
1547
+ }));
1548
+ const selectedIndices = await checkboxSelect(checkboxItems);
1549
+ if (selectedIndices.length === 0) {
1550
+ console.log(`\n ${c.dim}No agents selected. Exiting.${c.reset}\n`);
1551
+ process.exit(0);
1552
+ }
1553
+ const selectedAgents = selectedIndices.map((i) => AGENT_CHOICES[i]);
1554
+ console.log();
1555
+ for (const agent of selectedAgents) {
1556
+ console.log(` ${c.green}✓${c.reset} ${c.bold}${agent.name}${c.reset}`);
1557
+ }
1558
+ console.log();
1559
+ // ── Step 2: Configure each agent ─────────────────────────────
1326
1560
  const setupRl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
1327
- let setupDone = false;
1328
- while (!setupDone) {
1329
- console.log(` ${c.bold}Select your coding agent:${c.reset}\n`);
1330
- for (let i = 0; i < AGENT_CHOICES.length; i++) {
1331
- const a = AGENT_CHOICES[i];
1332
- console.log(` ${c.dim}${i + 1}${c.reset} ${a.name} ${c.dim}${a.desc}${c.reset}`);
1333
- }
1334
- console.log();
1335
- const choice = await questionWithEsc(setupRl, ` ${c.cyan}Agent [1-${AGENT_CHOICES.length}]:${c.reset} `);
1336
- if (choice === null) {
1337
- console.log(`\n ${c.dim}Exiting.${c.reset}\n`);
1338
- setupRl.close();
1339
- process.exit(0);
1340
- }
1341
- const idx = parseInt(choice, 10) - 1;
1342
- if (Number.isNaN(idx) || idx < 0 || idx >= AGENT_CHOICES.length) {
1343
- console.log(`\n ${c.dim}Invalid choice.${c.reset}\n`);
1344
- continue;
1345
- }
1346
- const agent = AGENT_CHOICES[idx];
1347
- console.log(`\n ${c.green}✓${c.reset} Agent: ${c.bold}${agent.name}${c.reset}\n`);
1348
- // Prompt for API key(s) one by one based on the selected agent
1349
- let gotAnyKey = false;
1561
+ const configuredProviders = new Set();
1562
+ let usesOllama = false;
1563
+ for (const agent of selectedAgents) {
1350
1564
  if (agent.requiresKey) {
1351
- // Agent requires at least one key — prompt until we get one
1352
- console.log(` ${c.dim}${agent.name} requires an API key. Configure one now:${c.reset}\n`);
1565
+ // Agent needs at least one API key
1566
+ console.log(` ${c.bold}${agent.name}${c.reset} ${c.dim}— configure API key:${c.reset}\n`);
1353
1567
  for (const provider of agent.keys) {
1568
+ if (configuredProviders.has(provider.env) || process.env[provider.env]) {
1569
+ console.log(` ${c.green}✓${c.reset} ${provider.name} ${c.dim}(already configured)${c.reset}`);
1570
+ continue;
1571
+ }
1354
1572
  const gotKey = await promptForProviderKey(setupRl, provider);
1355
1573
  if (gotKey === null)
1356
- break; // ESC — skip remaining
1574
+ break;
1357
1575
  if (gotKey) {
1358
- gotAnyKey = true;
1359
- break; // Got one — that's enough
1576
+ configuredProviders.add(provider.env);
1577
+ break;
1360
1578
  }
1361
1579
  }
1362
- if (!gotAnyKey) {
1363
- console.log(`\n ${c.dim}No key provided. Try another agent or set keys in .env${c.reset}\n`);
1364
- continue;
1580
+ const agentHasKey = agent.keys.some((p) => configuredProviders.has(p.env) || process.env[p.env]);
1581
+ if (!agentHasKey) {
1582
+ console.log(` ${c.dim}No key for ${agent.name} — set one in .env later${c.reset}`);
1365
1583
  }
1584
+ console.log();
1366
1585
  }
1367
1586
  else {
1368
- // Agent works without keys (e.g. OpenCode with open-source models)
1369
- // Offer to configure a provider key optionally
1370
- console.log(` ${c.dim}${agent.name} works with open-source models (no API key needed).${c.reset}`);
1371
- console.log(` ${c.dim}Optionally configure a provider for cloud models:${c.reset}\n`);
1372
- for (const provider of agent.keys) {
1373
- console.log(` ${c.dim}-${c.reset} ${provider.name} ${c.dim}(${provider.env})${c.reset}`);
1374
- }
1587
+ // Agent works without keys (e.g. OpenCode) set up Ollama directly
1588
+ console.log(` ${c.bold}${agent.name}${c.reset} ${c.dim}— setting up Ollama for local models:${c.reset}\n`);
1589
+ const ok = await ensureOllamaSetup(setupRl, "ollama/deepseek-coder-v2");
1590
+ if (ok)
1591
+ usesOllama = true;
1375
1592
  console.log();
1376
- const addKey = await questionWithEsc(setupRl, ` ${c.cyan}Add an API key? [y/N]:${c.reset} `);
1377
- if (addKey !== null && (addKey.toLowerCase() === "y" || addKey.toLowerCase() === "yes")) {
1378
- for (const provider of agent.keys) {
1379
- const gotKey = await promptForProviderKey(setupRl, provider);
1380
- if (gotKey === null)
1381
- break;
1382
- if (gotKey) {
1383
- gotAnyKey = true;
1384
- break;
1385
- }
1386
- }
1387
- }
1388
1593
  }
1389
- // Set default model
1390
- const activeProvider = Object.keys(PROVIDER_KEYS).find((p) => process.env[providerEnvKey(p)]);
1391
- if (activeProvider) {
1392
- // Has an API key — use that provider's default model
1393
- currentProviderName = activeProvider;
1394
- const defaultModel = getDefaultModelForProvider(activeProvider);
1395
- if (defaultModel) {
1396
- currentModelId = defaultModel;
1397
- saveModelPreference(currentModelId);
1398
- console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
1399
- }
1400
- }
1401
- else {
1402
- // No API key — default to open-source model via OpenCode
1403
- currentModelId = "ollama/deepseek-coder-v2";
1594
+ }
1595
+ // ── Step 3: Set default model ────────────────────────────────
1596
+ const activeProvider = Object.keys(PROVIDER_KEYS).find((p) => process.env[providerEnvKey(p)]);
1597
+ if (activeProvider) {
1598
+ currentProviderName = activeProvider;
1599
+ const defaultModel = getDefaultModelForProvider(activeProvider);
1600
+ if (defaultModel) {
1601
+ currentModelId = defaultModel;
1404
1602
  saveModelPreference(currentModelId);
1405
- console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset} ${c.dim}(open-source, requires Ollama)${c.reset}`);
1603
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
1406
1604
  }
1407
- console.log();
1408
- setupDone = true;
1409
1605
  }
1606
+ else if (usesOllama) {
1607
+ currentModelId = "ollama/deepseek-coder-v2";
1608
+ saveModelPreference(currentModelId);
1609
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset} ${c.dim}(local)${c.reset}`);
1610
+ }
1611
+ console.log();
1410
1612
  setupRl.close();
1411
1613
  }
1412
1614
  // Resolve model — ensure the resolved provider actually has an API key
@@ -1437,8 +1639,19 @@ async function interactive() {
1437
1639
  }
1438
1640
  }
1439
1641
  if (!currentModel) {
1642
+ if (currentModelId.startsWith("ollama/")) {
1643
+ // Ollama model selected — this interactive REPL mode needs a cloud API.
1644
+ // Redirect to swarm mode which works with OpenCode + Ollama.
1645
+ console.log(`\n ${c.green}✓${c.reset} Ollama model selected: ${c.bold}${currentModelId}${c.reset}`);
1646
+ console.log(`\n ${c.dim}This interactive REPL uses direct LLM API calls.${c.reset}`);
1647
+ console.log(` ${c.dim}To use Ollama models with OpenCode, run:${c.reset}\n`);
1648
+ console.log(` ${c.bold}swarm --dir ./your-project "your task"${c.reset}\n`);
1649
+ process.exit(0);
1650
+ }
1440
1651
  console.log(`\n ${c.red}Model "${currentModelId}" not found.${c.reset}`);
1441
- console.log(` Check ${c.bold}RLM_MODEL${c.reset} in your .env file.\n`);
1652
+ 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.`);
1653
+ console.log(`\n ${c.dim}For agent-based mode (works with open-source models):${c.reset}`);
1654
+ console.log(` ${c.bold}swarm --dir ./your-project "your task"${c.reset}\n`);
1442
1655
  process.exit(1);
1443
1656
  }
1444
1657
  // 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.1",
3
+ "version": "0.1.3",
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": {