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.
- package/dist/interactive.js +186 -43
- package/package.json +1 -1
package/dist/interactive.js
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
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
|
-
|
|
1342
|
-
|
|
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
|
|
1347
|
-
console.log(
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
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
|
|
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(`
|
|
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