swarm-code 0.1.1 → 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.
Files changed (2) hide show
  1. package/dist/interactive.js +186 -43
  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,116 @@ 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}
@@ -316,7 +427,7 @@ ${c.cyan}${c.bold}
316
427
  ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║
317
428
  ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
318
429
  ${c.reset}
319
- ${c.dim} Swarm-native coding agent orchestrator${c.reset}
430
+ ${c.dim} Swarm-native coding agent orchestrator${c.reset}
320
431
  `);
321
432
  }
322
433
  // ── Status line ─────────────────────────────────────────────────────────────
@@ -1326,70 +1437,92 @@ async function interactive() {
1326
1437
  const setupRl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
1327
1438
  let setupDone = false;
1328
1439
  while (!setupDone) {
1329
- console.log(` ${c.bold}Select your coding agent:${c.reset}\n`);
1440
+ console.log(` ${c.bold}Select your coding agent(s):${c.reset}\n`);
1330
1441
  for (let i = 0; i < AGENT_CHOICES.length; i++) {
1331
1442
  const a = AGENT_CHOICES[i];
1332
1443
  console.log(` ${c.dim}${i + 1}${c.reset} ${a.name} ${c.dim}${a.desc}${c.reset}`);
1333
1444
  }
1334
1445
  console.log();
1335
- const choice = await questionWithEsc(setupRl, ` ${c.cyan}Agent [1-${AGENT_CHOICES.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} `);
1336
1448
  if (choice === null) {
1337
1449
  console.log(`\n ${c.dim}Exiting.${c.reset}\n`);
1338
1450
  setupRl.close();
1339
1451
  process.exit(0);
1340
1452
  }
1341
- const idx = parseInt(choice, 10) - 1;
1342
- if (Number.isNaN(idx) || idx < 0 || idx >= AGENT_CHOICES.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) {
1343
1461
  console.log(`\n ${c.dim}Invalid choice.${c.reset}\n`);
1344
1462
  continue;
1345
1463
  }
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;
1350
- 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`);
1353
- for (const provider of agent.keys) {
1354
- const gotKey = await promptForProviderKey(setupRl, provider);
1355
- if (gotKey === null)
1356
- break; // ESC — skip remaining
1357
- if (gotKey) {
1358
- gotAnyKey = true;
1359
- break; // Got one — that's enough
1360
- }
1361
- }
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;
1365
- }
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}`);
1366
1468
  }
1367
- 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
- }
1375
- 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")) {
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`);
1378
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
+ }
1379
1481
  const gotKey = await promptForProviderKey(setupRl, provider);
1380
1482
  if (gotKey === null)
1381
- break;
1483
+ break; // ESC — skip remaining
1382
1484
  if (gotKey) {
1383
- gotAnyKey = true;
1384
- break;
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
+ }
1385
1514
  }
1386
1515
  }
1516
+ else {
1517
+ // Set up Ollama + pull model
1518
+ await ensureOllamaSetup(setupRl, "ollama/deepseek-coder-v2");
1519
+ }
1520
+ console.log();
1387
1521
  }
1388
1522
  }
1389
1523
  // Set default model
1390
1524
  const activeProvider = Object.keys(PROVIDER_KEYS).find((p) => process.env[providerEnvKey(p)]);
1391
1525
  if (activeProvider) {
1392
- // Has an API key — use that provider's default model
1393
1526
  currentProviderName = activeProvider;
1394
1527
  const defaultModel = getDefaultModelForProvider(activeProvider);
1395
1528
  if (defaultModel) {
@@ -1399,10 +1532,9 @@ async function interactive() {
1399
1532
  }
1400
1533
  }
1401
1534
  else {
1402
- // No API key — default to open-source model via OpenCode
1403
1535
  currentModelId = "ollama/deepseek-coder-v2";
1404
1536
  saveModelPreference(currentModelId);
1405
- console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset} ${c.dim}(open-source, requires Ollama)${c.reset}`);
1537
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset} ${c.dim}(open-source)${c.reset}`);
1406
1538
  }
1407
1539
  console.log();
1408
1540
  setupDone = true;
@@ -1437,8 +1569,19 @@ async function interactive() {
1437
1569
  }
1438
1570
  }
1439
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
+ }
1440
1581
  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`);
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`);
1442
1585
  process.exit(1);
1443
1586
  }
1444
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.1",
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": {