swarm-code 0.1.2 → 0.1.4

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 +172 -104
  2. package/package.json +1 -1
@@ -363,6 +363,26 @@ async function installOllama() {
363
363
  console.log(` ${c.dim}Download Ollama from: https://ollama.com/download${c.reset}`);
364
364
  return false;
365
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
+ }
366
386
  async function pullOllamaModel(model) {
367
387
  console.log(`\n ${c.bold}Pulling ${model}...${c.reset} ${c.dim}(this may take a few minutes)${c.reset}\n`);
368
388
  return new Promise((resolve) => {
@@ -372,6 +392,7 @@ async function pullOllamaModel(model) {
372
392
  });
373
393
  }
374
394
  async function ensureOllamaSetup(rl, model) {
395
+ const shortModel = model.replace("ollama/", "");
375
396
  // 1. Check if Ollama is installed
376
397
  if (!isOllamaInstalled()) {
377
398
  console.log(`\n ${c.dim}Ollama is not installed. It's needed to run open-source models locally.${c.reset}`);
@@ -383,26 +404,45 @@ async function ensureOllamaSetup(rl, model) {
383
404
  console.log(` ${c.dim}Install manually from https://ollama.com/download${c.reset}\n`);
384
405
  return false;
385
406
  }
386
- console.log(`\n ${c.green}✓${c.reset} Ollama installed\n`);
407
+ console.log(` ${c.green}✓${c.reset} Ollama installed`);
387
408
  }
388
409
  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`);
410
+ console.log(`\n ${c.dim}Install later from https://ollama.com/download${c.reset}\n`);
391
411
  return false;
392
412
  }
393
413
  }
394
414
  else {
395
- console.log(`\n ${c.green}✓${c.reset} Ollama installed`);
415
+ console.log(` ${c.green}✓${c.reset} Ollama installed`);
396
416
  }
397
- // 2. Check if model is already pulled
398
- const shortModel = model.replace("ollama/", "");
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
399
439
  if (isOllamaModelAvailable(shortModel)) {
400
440
  console.log(` ${c.green}✓${c.reset} Model ${c.bold}${shortModel}${c.reset} ready\n`);
401
441
  return true;
402
442
  }
403
- // 3. Pull the model
443
+ // 4. Pull the model
404
444
  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} `);
445
+ const pull = await questionWithEsc(rl, `\n ${c.cyan}Pull ${shortModel} now? [Y/n]:${c.reset} `);
406
446
  if (pull !== null && pull.toLowerCase() !== "n" && pull.toLowerCase() !== "no") {
407
447
  const ok = await pullOllamaModel(shortModel);
408
448
  if (!ok) {
@@ -410,12 +450,73 @@ async function ensureOllamaSetup(rl, model) {
410
450
  console.log(` ${c.dim}Try manually: ollama pull ${shortModel}${c.reset}\n`);
411
451
  return false;
412
452
  }
413
- console.log(`\n ${c.green}✓${c.reset} Model ${c.bold}${shortModel}${c.reset} ready\n`);
453
+ console.log(` ${c.green}✓${c.reset} Model ${c.bold}${shortModel}${c.reset} ready\n`);
414
454
  return true;
415
455
  }
416
456
  console.log(`\n ${c.dim}Run later: ollama pull ${shortModel}${c.reset}\n`);
417
457
  return false;
418
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
+ process.stdout.write("\x1b[?25l"); // Hide cursor
482
+ render();
483
+ rendered = true;
484
+ const wasRaw = stdin.isRaw;
485
+ if (stdin.isTTY)
486
+ stdin.setRawMode(true);
487
+ const cleanup = () => {
488
+ stdin.removeListener("data", onData);
489
+ if (stdin.isTTY)
490
+ stdin.setRawMode(wasRaw ?? false);
491
+ process.stdout.write("\x1b[?25h"); // Show cursor
492
+ };
493
+ const onData = (data) => {
494
+ const key = data.toString();
495
+ if (key === "\x1b[A" || key === "k") {
496
+ cursor = (cursor - 1 + items.length) % items.length;
497
+ render();
498
+ }
499
+ else if (key === "\x1b[B" || key === "j") {
500
+ cursor = (cursor + 1) % items.length;
501
+ render();
502
+ }
503
+ else if (key === " ") {
504
+ items[cursor].checked = !items[cursor].checked;
505
+ render();
506
+ }
507
+ else if (key === "\r" || key === "\n") {
508
+ cleanup();
509
+ const selected = items.map((item, i) => (item.checked ? i : -1)).filter((i) => i >= 0);
510
+ resolve(selected);
511
+ }
512
+ else if (key === "\x1b" || key === "\x03") {
513
+ cleanup();
514
+ resolve([]);
515
+ }
516
+ };
517
+ stdin.on("data", onData);
518
+ });
519
+ }
419
520
  // ── Banner ──────────────────────────────────────────────────────────────────
420
521
  function printBanner() {
421
522
  console.log(`
@@ -1434,111 +1535,78 @@ async function interactive() {
1434
1535
  requiresKey: true,
1435
1536
  },
1436
1537
  ];
1538
+ // ── Step 1: Checkbox agent selection ─────────────────────────
1539
+ console.log(` ${c.bold}Select your coding agent(s):${c.reset}`);
1540
+ console.log(` ${c.dim}↑/↓ navigate · space toggle · enter confirm${c.reset}\n`);
1541
+ const checkboxItems = AGENT_CHOICES.map((a) => ({
1542
+ label: a.name,
1543
+ desc: a.desc,
1544
+ checked: false,
1545
+ }));
1546
+ const selectedIndices = await checkboxSelect(checkboxItems);
1547
+ if (selectedIndices.length === 0) {
1548
+ console.log(`\n ${c.dim}No agents selected. Exiting.${c.reset}\n`);
1549
+ process.exit(0);
1550
+ }
1551
+ const selectedAgents = selectedIndices.map((i) => AGENT_CHOICES[i]);
1552
+ console.log();
1553
+ for (const agent of selectedAgents) {
1554
+ console.log(` ${c.green}✓${c.reset} ${c.bold}${agent.name}${c.reset}`);
1555
+ }
1556
+ console.log();
1557
+ // ── Step 2: Configure each agent ─────────────────────────────
1437
1558
  const setupRl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
1438
- let setupDone = false;
1439
- while (!setupDone) {
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}`);
1444
- }
1445
- console.log();
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} `);
1448
- if (choice === null) {
1449
- console.log(`\n ${c.dim}Exiting.${c.reset}\n`);
1450
- setupRl.close();
1451
- process.exit(0);
1452
- }
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) {
1461
- console.log(`\n ${c.dim}Invalid choice.${c.reset}\n`);
1462
- continue;
1463
- }
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}`);
1468
- }
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
- }
1559
+ const configuredProviders = new Set();
1560
+ let usesOllama = false;
1561
+ for (const agent of selectedAgents) {
1562
+ if (agent.requiresKey) {
1563
+ // Agent needs at least one API key
1564
+ console.log(` ${c.bold}${agent.name}${c.reset} ${c.dim}— configure API key:${c.reset}\n`);
1565
+ for (const provider of agent.keys) {
1566
+ if (configuredProviders.has(provider.env) || process.env[provider.env]) {
1567
+ console.log(` ${c.green}✓${c.reset} ${provider.name} ${c.dim}(already configured)${c.reset}`);
1568
+ continue;
1515
1569
  }
1516
- else {
1517
- // Set up Ollama + pull model
1518
- await ensureOllamaSetup(setupRl, "ollama/deepseek-coder-v2");
1570
+ const gotKey = await promptForProviderKey(setupRl, provider);
1571
+ if (gotKey === null)
1572
+ break;
1573
+ if (gotKey) {
1574
+ configuredProviders.add(provider.env);
1575
+ break;
1519
1576
  }
1520
- console.log();
1521
1577
  }
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}`);
1578
+ const agentHasKey = agent.keys.some((p) => configuredProviders.has(p.env) || process.env[p.env]);
1579
+ if (!agentHasKey) {
1580
+ console.log(` ${c.dim}No key for ${agent.name} set one in .env later${c.reset}`);
1532
1581
  }
1582
+ console.log();
1533
1583
  }
1534
1584
  else {
1535
- currentModelId = "ollama/deepseek-coder-v2";
1585
+ // Agent works without keys (e.g. OpenCode) — set up Ollama directly
1586
+ console.log(` ${c.bold}${agent.name}${c.reset} ${c.dim}— setting up Ollama for local models:${c.reset}\n`);
1587
+ const ok = await ensureOllamaSetup(setupRl, "ollama/deepseek-coder-v2");
1588
+ if (ok)
1589
+ usesOllama = true;
1590
+ console.log();
1591
+ }
1592
+ }
1593
+ // ── Step 3: Set default model ────────────────────────────────
1594
+ const activeProvider = Object.keys(PROVIDER_KEYS).find((p) => process.env[providerEnvKey(p)]);
1595
+ if (activeProvider) {
1596
+ currentProviderName = activeProvider;
1597
+ const defaultModel = getDefaultModelForProvider(activeProvider);
1598
+ if (defaultModel) {
1599
+ currentModelId = defaultModel;
1536
1600
  saveModelPreference(currentModelId);
1537
- console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset} ${c.dim}(open-source)${c.reset}`);
1601
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
1538
1602
  }
1539
- console.log();
1540
- setupDone = true;
1541
1603
  }
1604
+ else if (usesOllama) {
1605
+ currentModelId = "ollama/deepseek-coder-v2";
1606
+ saveModelPreference(currentModelId);
1607
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset} ${c.dim}(local)${c.reset}`);
1608
+ }
1609
+ console.log();
1542
1610
  setupRl.close();
1543
1611
  }
1544
1612
  // Resolve model — ensure the resolved provider actually has an API key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarm-code",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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": {