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 +5 -5
- package/dist/interactive.js +253 -31
- package/package.json +1 -1
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 —
|
|
24
|
+
swarm run — Text processing mode (RLM)
|
|
25
25
|
|
|
26
26
|
USAGE
|
|
27
|
-
|
|
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
|
-
|
|
38
|
-
curl -s https://example.com/large.py |
|
|
39
|
-
|
|
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
|
}
|
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,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}
|
|
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
|
|
1292
|
-
for (let i = 0; i <
|
|
1293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1304
|
-
|
|
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
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
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(`
|
|
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