open-agents-ai 0.24.0 → 0.26.0

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/index.js CHANGED
@@ -1425,9 +1425,14 @@ var init_file_read = __esm({
1425
1425
  required: ["path"]
1426
1426
  };
1427
1427
  workingDir;
1428
+ _contextWindowSize = 0;
1428
1429
  constructor(workingDir) {
1429
1430
  this.workingDir = workingDir;
1430
1431
  }
1432
+ /** Set actual context window size to enable auto-windowing for small contexts */
1433
+ setContextWindowSize(size) {
1434
+ this._contextWindowSize = size;
1435
+ }
1431
1436
  async execute(args) {
1432
1437
  const filePath = args["path"];
1433
1438
  const offset = args["offset"];
@@ -1437,9 +1442,23 @@ var init_file_read = __esm({
1437
1442
  const fullPath = resolve(this.workingDir, filePath);
1438
1443
  const content = await readFile(fullPath, "utf-8");
1439
1444
  let lines = content.split("\n");
1445
+ const totalLines = lines.length;
1440
1446
  if (offset !== void 0) {
1441
1447
  const startIdx = Math.max(0, offset - 1);
1442
1448
  lines = lines.slice(startIdx, limit ? startIdx + limit : void 0);
1449
+ } else if (this._contextWindowSize > 0 && this._contextWindowSize <= 32768 && !limit) {
1450
+ const maxLines = this._contextWindowSize <= 16384 ? 80 : 120;
1451
+ if (totalLines > maxLines) {
1452
+ lines = lines.slice(0, maxLines);
1453
+ const numbered2 = lines.map((line, i) => `${String(i + 1).padStart(6)} | ${line}`).join("\n");
1454
+ return {
1455
+ success: true,
1456
+ output: `${numbered2}
1457
+
1458
+ [File has ${totalLines} lines \u2014 showing first ${maxLines}. Use offset/limit to see more.]`,
1459
+ durationMs: performance.now() - start
1460
+ };
1461
+ }
1443
1462
  }
1444
1463
  const numbered = lines.map((line, i) => `${String(i + (offset ?? 1)).padStart(6)} | ${line}`).join("\n");
1445
1464
  return {
@@ -10299,9 +10318,14 @@ Rules:
10299
10318
  streamEnabled: options?.streamEnabled ?? false,
10300
10319
  bruteForce: options?.bruteForce ?? true,
10301
10320
  bruteForceMaxCycles: options?.bruteForceMaxCycles ?? 100,
10302
- modelTier: options?.modelTier ?? "large"
10321
+ modelTier: options?.modelTier ?? "large",
10322
+ contextWindowSize: options?.contextWindowSize ?? 0
10303
10323
  };
10304
10324
  }
10325
+ /** Update context window size (e.g. after querying Ollama /api/show) */
10326
+ setContextWindowSize(size) {
10327
+ this.options.contextWindowSize = size;
10328
+ }
10305
10329
  /** Register a tool for the agent to use */
10306
10330
  registerTool(tool) {
10307
10331
  this.tools.set(tool.name, tool);
@@ -10455,11 +10479,13 @@ Integrate this guidance into your current approach. Continue working on the task
10455
10479
  });
10456
10480
  }
10457
10481
  const compacted = this.compactMessages(messages);
10482
+ const ctxWindow = this.options.contextWindowSize;
10483
+ const effectiveMaxTokens = ctxWindow > 0 ? Math.min(this.options.maxTokens, Math.max(2048, Math.floor(ctxWindow * 0.25))) : this.options.maxTokens;
10458
10484
  const chatRequest = {
10459
10485
  messages: compacted,
10460
10486
  tools: toolDefs,
10461
10487
  temperature: this.options.temperature,
10462
- maxTokens: this.options.maxTokens,
10488
+ maxTokens: effectiveMaxTokens,
10463
10489
  timeoutMs: this.options.requestTimeoutMs
10464
10490
  };
10465
10491
  let response;
@@ -10553,10 +10579,10 @@ Integrate this guidance into your current approach. Continue working on the task
10553
10579
  }
10554
10580
  }
10555
10581
  }
10556
- const maxLen = 8e3;
10557
- const output = result.success ? result.output.length > maxLen ? result.output.slice(0, maxLen) + `
10558
- ...(truncated)` : result.output : `Error: ${result.error || "unknown error"}
10559
- ${result.output}`;
10582
+ const ctxW = this.options.contextWindowSize;
10583
+ const maxLen = ctxW > 0 ? Math.max(2e3, Math.min(8e3, Math.floor(ctxW * 0.5))) : 8e3;
10584
+ const output = result.success ? result.output.length > maxLen ? this.foldOutput(result.output, maxLen) : result.output : `Error: ${result.error || "unknown error"}
10585
+ ${result.output.length > maxLen ? this.foldOutput(result.output, maxLen) : result.output}`;
10560
10586
  this.emit({
10561
10587
  type: "tool_result",
10562
10588
  toolName: tc.name,
@@ -10862,6 +10888,29 @@ ${marker}` : marker);
10862
10888
  return { role: "tool", content: output, tool_call_id: toolCallId };
10863
10889
  }
10864
10890
  // -------------------------------------------------------------------------
10891
+ // Output folding — keep head + tail, omit middle (preserves errors at end)
10892
+ // -------------------------------------------------------------------------
10893
+ foldOutput(output, maxChars) {
10894
+ const lines = output.split("\n");
10895
+ if (lines.length <= 40) {
10896
+ return output.slice(0, maxChars) + "\n...(truncated)";
10897
+ }
10898
+ const headLines = 20;
10899
+ const tailLines = 10;
10900
+ const head = lines.slice(0, headLines).join("\n");
10901
+ const tail = lines.slice(-tailLines).join("\n");
10902
+ const omitted = lines.length - headLines - tailLines;
10903
+ const folded = `${head}
10904
+
10905
+ [... ${omitted} lines omitted ...]
10906
+
10907
+ ${tail}`;
10908
+ if (folded.length > maxChars) {
10909
+ return folded.slice(0, maxChars) + "\n...(truncated)";
10910
+ }
10911
+ return folded;
10912
+ }
10913
+ // -------------------------------------------------------------------------
10865
10914
  // Context compaction
10866
10915
  // -------------------------------------------------------------------------
10867
10916
  compactMessages(messages) {
@@ -10879,7 +10928,8 @@ ${marker}` : marker);
10879
10928
  if (estimatedTokens < this.options.compactionThreshold) {
10880
10929
  return messages;
10881
10930
  }
10882
- const keepRecent = 12;
10931
+ const ctxWin = this.options.contextWindowSize;
10932
+ const keepRecent = ctxWin > 0 ? Math.max(4, Math.min(12, Math.floor(ctxWin / 4e3))) : 12;
10883
10933
  const head = messages.slice(0, 2);
10884
10934
  if (messages.length <= 2 + keepRecent)
10885
10935
  return messages;
@@ -12684,6 +12734,55 @@ function findModel(models, query) {
12684
12734
  const fuzzy = models.find((m) => m.name.includes(query));
12685
12735
  return fuzzy;
12686
12736
  }
12737
+ async function queryModelContextSize(baseUrl, modelName) {
12738
+ try {
12739
+ const normalized = normalizeBaseUrl(baseUrl);
12740
+ const res = await fetch(`${normalized}/api/show`, {
12741
+ method: "POST",
12742
+ headers: { "Content-Type": "application/json" },
12743
+ body: JSON.stringify({ name: modelName }),
12744
+ signal: AbortSignal.timeout(1e4)
12745
+ });
12746
+ if (!res.ok)
12747
+ return null;
12748
+ const data = await res.json();
12749
+ if (data.parameters) {
12750
+ const match = data.parameters.match(/num_ctx\s+(\d+)/);
12751
+ if (match)
12752
+ return parseInt(match[1], 10);
12753
+ }
12754
+ if (data.model_info) {
12755
+ for (const [key, value] of Object.entries(data.model_info)) {
12756
+ if (key.endsWith(".context_length") && typeof value === "number") {
12757
+ return value;
12758
+ }
12759
+ }
12760
+ }
12761
+ return null;
12762
+ } catch {
12763
+ return null;
12764
+ }
12765
+ }
12766
+ async function queryOpenAIContextSize(baseUrl, modelName, apiKey) {
12767
+ try {
12768
+ const models = await fetchOpenAIModels(baseUrl, apiKey);
12769
+ const model = models.find((m) => m.name === modelName);
12770
+ if (model?.size) {
12771
+ const match = model.size.match(/(\d+)K ctx/);
12772
+ if (match)
12773
+ return parseInt(match[1], 10) * 1024;
12774
+ }
12775
+ return null;
12776
+ } catch {
12777
+ return null;
12778
+ }
12779
+ }
12780
+ async function queryContextSize(baseUrl, modelName, apiKey) {
12781
+ const ollamaSize = await queryModelContextSize(baseUrl, modelName);
12782
+ if (ollamaSize)
12783
+ return ollamaSize;
12784
+ return queryOpenAIContextSize(baseUrl, modelName, apiKey);
12785
+ }
12687
12786
  function formatBytes(bytes) {
12688
12787
  if (bytes < 1024)
12689
12788
  return `${bytes} B`;
@@ -13173,6 +13272,8 @@ function renderSlashHelp() {
13173
13272
  ["/evaluate", "Evaluate last completed task (LLM quality scoring)"],
13174
13273
  ["/task-type", "Set task type (code, document, analysis, plan, general, auto)"],
13175
13274
  ["/stats", "Show session dashboard (metrics, tool usage, task history)"],
13275
+ ["/stop", "Stop current task and save progress (alias: /pause)"],
13276
+ ["/resume", "Resume a previously stopped task"],
13176
13277
  ["/bruteforce", "Toggle brute-force mode (auto re-engage on turn limit)"],
13177
13278
  ["/tools", "List agent-created custom tools"],
13178
13279
  ["/skills", "List available AIWG skills"],
@@ -15097,6 +15198,34 @@ async function handleSlashCommand(input, ctx) {
15097
15198
  renderInfo(`Colors ${next ? "enabled" : "disabled"}.`);
15098
15199
  return "handled";
15099
15200
  }
15201
+ case "stop":
15202
+ case "pause": {
15203
+ if (!ctx.hasActiveTask?.()) {
15204
+ renderWarning("No active task to stop.");
15205
+ return "handled";
15206
+ }
15207
+ const saved = ctx.savePendingTaskState?.() ?? false;
15208
+ const aborted = ctx.abortTask?.() ?? false;
15209
+ if (saved && aborted) {
15210
+ renderInfo("Task stopped and saved. Use /resume to continue later.");
15211
+ } else if (aborted) {
15212
+ renderWarning("Task stopped but state could not be saved.");
15213
+ } else {
15214
+ renderWarning("Could not stop the task.");
15215
+ }
15216
+ return "handled";
15217
+ }
15218
+ case "resume": {
15219
+ if (ctx.hasActiveTask?.()) {
15220
+ renderWarning("A task is already running. Stop it first with /stop.");
15221
+ return "handled";
15222
+ }
15223
+ const resumed = ctx.resumeTask?.() ?? false;
15224
+ if (!resumed) {
15225
+ renderWarning("No saved task to resume.");
15226
+ }
15227
+ return "handled";
15228
+ }
15100
15229
  default: {
15101
15230
  const skills = discoverSkills(ctx.repoRoot);
15102
15231
  const skill = skills.find((s) => s.name === cmd || s.name === cmd.replace(/_/g, "-"));
@@ -15349,6 +15478,12 @@ async function switchModel(query, ctx, local = false) {
15349
15478
  if (local) {
15350
15479
  renderInfo("Saved as project-local override.");
15351
15480
  }
15481
+ if (ctx.setContextWindowSize) {
15482
+ const ctxSize = await queryContextSize(ctx.config.backendUrl, finalModel, ctx.config.apiKey);
15483
+ if (ctxSize) {
15484
+ ctx.setContextWindowSize(ctxSize);
15485
+ }
15486
+ }
15352
15487
  } catch (err) {
15353
15488
  renderError(`Failed to switch model: ${err instanceof Error ? err.message : String(err)}`);
15354
15489
  }
@@ -19022,7 +19157,7 @@ Use task_status("${taskId}") or task_output("${taskId}") to check progress.`
19022
19157
  }
19023
19158
  };
19024
19159
  }
19025
- function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType) {
19160
+ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType, contextWindowSize) {
19026
19161
  const modelTier = getModelTier(config.model);
19027
19162
  const projectCtx = buildProjectContext(repoRoot, taskStores?.contextStores);
19028
19163
  let dynamicContext = formatContextForPrompt(projectCtx, modelTier);
@@ -19043,10 +19178,19 @@ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce
19043
19178
  modelTier,
19044
19179
  streamEnabled: stream?.enabled ?? false,
19045
19180
  bruteForce: bruteForce ?? true,
19046
- bruteForceMaxCycles: 100
19181
+ bruteForceMaxCycles: 100,
19047
19182
  // effectively unlimited — hard timeout is the real bound
19183
+ contextWindowSize: contextWindowSize ?? 0
19048
19184
  });
19049
- runner.registerTools(buildTools(repoRoot, config));
19185
+ const tools = buildTools(repoRoot, config);
19186
+ if (contextWindowSize && contextWindowSize > 0) {
19187
+ for (const tool of tools) {
19188
+ if ("setContextWindowSize" in tool && typeof tool.setContextWindowSize === "function") {
19189
+ tool.setContextWindowSize(contextWindowSize);
19190
+ }
19191
+ }
19192
+ }
19193
+ runner.registerTools(tools);
19050
19194
  const filesTouched = /* @__PURE__ */ new Set();
19051
19195
  const toolSequence = [];
19052
19196
  const editSessionId = `task-${Date.now()}`;
@@ -19337,6 +19481,14 @@ async function startInteractive(config, repoPath) {
19337
19481
  end: () => statusBar.endContentWrite()
19338
19482
  });
19339
19483
  }
19484
+ let resolvedContextWindowSize = 0;
19485
+ queryContextSize(config.backendUrl, config.model, config.apiKey).then((ctxSize) => {
19486
+ if (ctxSize) {
19487
+ resolvedContextWindowSize = ctxSize;
19488
+ statusBar.setContextWindowSize(ctxSize);
19489
+ }
19490
+ }).catch(() => {
19491
+ });
19340
19492
  const provider = detectProvider(config.backendUrl);
19341
19493
  const costTracker = new CostTracker(provider.id);
19342
19494
  const sessionMetrics = new SessionMetrics();
@@ -19530,7 +19682,7 @@ async function startInteractive(config, repoPath) {
19530
19682
  if (lastSubmittedPrompt && activeTask) {
19531
19683
  savePendingTask(repoRoot, {
19532
19684
  prompt: lastSubmittedPrompt,
19533
- progressSummary: `Manual /update triggered. ${sessionToolCallCount} tool calls completed in last task.`,
19685
+ progressSummary: `Task paused by user. ${sessionToolCallCount} tool calls completed.`,
19534
19686
  filesModified: sessionFilesTouched,
19535
19687
  bruteForce: bruteForceEnabled,
19536
19688
  savedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -19552,6 +19704,12 @@ async function startInteractive(config, repoPath) {
19552
19704
  writeContent(() => renderToolCallStart(event.toolName ?? "unknown", event.toolArgs ?? {}));
19553
19705
  } else if (event.type === "tool_result") {
19554
19706
  writeContent(() => renderToolResult(event.toolName ?? "unknown", event.success ?? false, event.content ?? ""));
19707
+ } else if (event.type === "token_usage" && event.tokenUsage) {
19708
+ statusBar.updateMetrics({
19709
+ ...event.tokenUsage,
19710
+ estimatedCost: costTracker?.currentCost,
19711
+ hasPricing: costTracker?.hasPricing
19712
+ });
19555
19713
  }
19556
19714
  }).then((state) => {
19557
19715
  writeContent(() => renderDreamEnd(state));
@@ -19628,7 +19786,36 @@ async function startInteractive(config, repoPath) {
19628
19786
  getEmojis: () => getEmojisEnabled(),
19629
19787
  setEmojis: (enabled) => setEmojisEnabled(enabled),
19630
19788
  getColors: () => getColorsEnabled(),
19631
- setColors: (enabled) => setColorsEnabled(enabled)
19789
+ setColors: (enabled) => setColorsEnabled(enabled),
19790
+ setContextWindowSize: (size) => {
19791
+ resolvedContextWindowSize = size;
19792
+ statusBar.setContextWindowSize(size);
19793
+ },
19794
+ hasActiveTask: () => activeTask !== null,
19795
+ abortTask() {
19796
+ if (!activeTask)
19797
+ return false;
19798
+ activeTask.runner.abort();
19799
+ writeContent(() => renderInfo("Task aborted."));
19800
+ return true;
19801
+ },
19802
+ resumeTask() {
19803
+ const pendingTask = loadPendingTask(repoRoot);
19804
+ if (!pendingTask)
19805
+ return false;
19806
+ setTimeout(() => {
19807
+ const resumeContext = [
19808
+ `[RESUMED] Original task: ${pendingTask.prompt}`,
19809
+ pendingTask.progressSummary ? `Progress so far: ${pendingTask.progressSummary}` : "",
19810
+ pendingTask.filesModified.length > 0 ? `Files modified before stop: ${pendingTask.filesModified.join(", ")}` : "",
19811
+ `Tool calls completed before stop: ${pendingTask.toolCallCount}`,
19812
+ "Continue where you left off. Do not repeat work already done."
19813
+ ].filter(Boolean).join("\n\n");
19814
+ writeContent(() => renderInfo(`Resuming task: ${pendingTask.prompt.slice(0, 100)}${pendingTask.prompt.length > 100 ? "..." : ""}`));
19815
+ rl.emit("line", resumeContext);
19816
+ }, 100);
19817
+ return true;
19818
+ }
19632
19819
  };
19633
19820
  showPrompt();
19634
19821
  if (hasTaskToResume) {
@@ -19759,7 +19946,7 @@ Execute this skill now. Follow the behavioral guidance above.`;
19759
19946
  toolPatternStore: toolPatternStore ?? void 0
19760
19947
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary) => {
19761
19948
  lastCompletedSummary = summary;
19762
- }, currentTaskType);
19949
+ }, currentTaskType, resolvedContextWindowSize);
19763
19950
  activeTask = task;
19764
19951
  showPrompt();
19765
19952
  await task.promise;
@@ -19861,7 +20048,7 @@ Summarize or analyze this transcription as appropriate.`;
19861
20048
  toolPatternStore: toolPatternStore ?? void 0
19862
20049
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary) => {
19863
20050
  lastCompletedSummary = summary;
19864
- }, currentTaskType);
20051
+ }, currentTaskType, resolvedContextWindowSize);
19865
20052
  activeTask = task;
19866
20053
  showPrompt();
19867
20054
  await task.promise;
@@ -20003,6 +20190,7 @@ var init_interactive = __esm({
20003
20190
  init_updater();
20004
20191
  init_commands();
20005
20192
  init_setup();
20193
+ init_model_picker();
20006
20194
  init_project_context();
20007
20195
  init_dist7();
20008
20196
  init_oa_directory();
package/dist/launcher.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Node version gate — uses only ES5 syntax so it parses on any Node version.
3
- // On old Node: walks through nvm install, then Node 20, then reinstall.
3
+ // On old Node: walks through curl/wget install, nvm install, then Node 20, then reinstall.
4
4
  var nodeVersion = parseInt(process.versions.node, 10);
5
5
 
6
6
  if (nodeVersion < 18) {
@@ -36,6 +36,15 @@ if (nodeVersion < 18) {
36
36
  });
37
37
  }
38
38
 
39
+ function hasCmd(cmd) {
40
+ try {
41
+ childProcess.execSync("which " + cmd, { stdio: "pipe", timeout: 3000 });
42
+ return true;
43
+ } catch (e) {
44
+ return false;
45
+ }
46
+ }
47
+
39
48
  function run(cmd) {
40
49
  console.log(" $ " + cmd);
41
50
  try {
@@ -53,7 +62,58 @@ if (nodeVersion < 18) {
53
62
  process.exit(1);
54
63
  }
55
64
 
56
- function doInstallNvm(next) {
65
+ // Returns "curl -o-" or "wget -qO-" or null
66
+ function getDownloader() {
67
+ if (hasCmd("curl")) return "curl -o-";
68
+ if (hasCmd("wget")) return "wget -qO-";
69
+ return null;
70
+ }
71
+
72
+ // Try to install curl if neither curl nor wget is available
73
+ function ensureDownloader(next) {
74
+ var dl = getDownloader();
75
+ if (dl) { next(dl); return; }
76
+
77
+ console.log(" Neither curl nor wget found.");
78
+ console.log("");
79
+
80
+ ask(" Install curl now? [Y/n] ", function(a) {
81
+ if (a === "n" || a === "no") {
82
+ bail(
83
+ "\n Install manually:\n" +
84
+ " sudo apt install curl (Debian/Ubuntu)\n" +
85
+ " sudo dnf install curl (Fedora)\n" +
86
+ " sudo pacman -S curl (Arch)\n"
87
+ );
88
+ return;
89
+ }
90
+ console.log("");
91
+ var ok = false;
92
+ if (hasCmd("apt-get")) {
93
+ ok = run("sudo -n apt-get install -y curl 2>/dev/null || sudo apt-get install -y curl");
94
+ } else if (hasCmd("dnf")) {
95
+ ok = run("sudo -n dnf install -y curl 2>/dev/null || sudo dnf install -y curl");
96
+ } else if (hasCmd("pacman")) {
97
+ ok = run("sudo -n pacman -S --noconfirm curl 2>/dev/null || sudo pacman -S --noconfirm curl");
98
+ } else if (hasCmd("apk")) {
99
+ ok = run("apk add curl");
100
+ } else {
101
+ console.log(" No supported package manager found (tried apt, dnf, pacman, apk).");
102
+ }
103
+
104
+ if (ok && hasCmd("curl")) {
105
+ console.log("");
106
+ next("curl -o-");
107
+ } else {
108
+ bail(
109
+ "\n Could not install curl. Install it manually, then re-run:\n" +
110
+ " npm i -g open-agents-ai\n"
111
+ );
112
+ }
113
+ });
114
+ }
115
+
116
+ function doInstallNvm(dl, next) {
57
117
  if (hasNvm) {
58
118
  console.log(" nvm found at " + nvmDir);
59
119
  console.log("");
@@ -65,6 +125,7 @@ if (nodeVersion < 18) {
65
125
  bail(
66
126
  "\n Install nvm manually:\n" +
67
127
  " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\n" +
128
+ " # or: wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\n" +
68
129
  " source ~/.bashrc\n" +
69
130
  " nvm install 20\n" +
70
131
  " npm i -g open-agents-ai\n"
@@ -72,7 +133,7 @@ if (nodeVersion < 18) {
72
133
  return;
73
134
  }
74
135
  console.log("");
75
- var ok = run("curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash");
136
+ var ok = run(dl + " https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash");
76
137
  if (!ok) {
77
138
  bail("\n nvm install failed. See https://github.com/nvm-sh/nvm#installing-and-updating\n");
78
139
  return;
@@ -151,6 +212,7 @@ if (nodeVersion < 18) {
151
212
  bail(
152
213
  "\n To install manually:\n" +
153
214
  " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\n" +
215
+ " # or: wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\n" +
154
216
  " source ~/.bashrc\n" +
155
217
  " nvm install 20\n" +
156
218
  " nvm alias default 20\n" +
@@ -159,9 +221,12 @@ if (nodeVersion < 18) {
159
221
  return;
160
222
  }
161
223
  console.log("");
162
- doInstallNvm(function() {
163
- doInstallNode(function() {
164
- doReinstall();
224
+ // Ensure curl or wget is available first
225
+ ensureDownloader(function(dl) {
226
+ doInstallNvm(dl, function() {
227
+ doInstallNode(function() {
228
+ doReinstall();
229
+ });
165
230
  });
166
231
  });
167
232
  });
@@ -42,6 +42,15 @@ function ask(question, cb) {
42
42
  });
43
43
  }
44
44
 
45
+ function hasCmd(cmd) {
46
+ try {
47
+ childProcess.execSync("which " + cmd, { stdio: "pipe", timeout: 3000 });
48
+ return true;
49
+ } catch (e) {
50
+ return false;
51
+ }
52
+ }
53
+
45
54
  function run(cmd, opts) {
46
55
  console.log(" $ " + cmd);
47
56
  try {
@@ -58,7 +67,58 @@ function bail(msg) {
58
67
  process.exit(1);
59
68
  }
60
69
 
61
- function doInstallNvm(next) {
70
+ // Returns "curl -o-" or "wget -qO-" or null
71
+ function getDownloader() {
72
+ if (hasCmd("curl")) return "curl -o-";
73
+ if (hasCmd("wget")) return "wget -qO-";
74
+ return null;
75
+ }
76
+
77
+ // Try to install curl if neither curl nor wget is available
78
+ function ensureDownloader(next) {
79
+ var dl = getDownloader();
80
+ if (dl) { next(dl); return; }
81
+
82
+ console.log(" Neither curl nor wget found.");
83
+ console.log("");
84
+
85
+ ask(" Install curl now? [Y/n] ", function(a) {
86
+ if (a === "n" || a === "no") {
87
+ bail(
88
+ "\n Install manually:\n" +
89
+ " sudo apt install curl (Debian/Ubuntu)\n" +
90
+ " sudo dnf install curl (Fedora)\n" +
91
+ " sudo pacman -S curl (Arch)\n"
92
+ );
93
+ return;
94
+ }
95
+ console.log("");
96
+ var ok = false;
97
+ if (hasCmd("apt-get")) {
98
+ ok = run("sudo -n apt-get install -y curl 2>/dev/null || sudo apt-get install -y curl");
99
+ } else if (hasCmd("dnf")) {
100
+ ok = run("sudo -n dnf install -y curl 2>/dev/null || sudo dnf install -y curl");
101
+ } else if (hasCmd("pacman")) {
102
+ ok = run("sudo -n pacman -S --noconfirm curl 2>/dev/null || sudo pacman -S --noconfirm curl");
103
+ } else if (hasCmd("apk")) {
104
+ ok = run("apk add curl");
105
+ } else {
106
+ console.log(" No supported package manager found (tried apt, dnf, pacman, apk).");
107
+ }
108
+
109
+ if (ok && hasCmd("curl")) {
110
+ console.log("");
111
+ next("curl -o-");
112
+ } else {
113
+ bail(
114
+ "\n Could not install curl. Install it manually, then re-run:\n" +
115
+ " npm i -g open-agents-ai\n"
116
+ );
117
+ }
118
+ });
119
+ }
120
+
121
+ function doInstallNvm(dl, next) {
62
122
  if (hasNvm) {
63
123
  console.log(" nvm found at " + nvmDir);
64
124
  console.log("");
@@ -70,6 +130,7 @@ function doInstallNvm(next) {
70
130
  bail(
71
131
  "\n Install nvm manually:\n" +
72
132
  " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\n" +
133
+ " # or: wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\n" +
73
134
  " source ~/.bashrc\n" +
74
135
  " nvm install 20\n" +
75
136
  " npm i -g open-agents-ai\n"
@@ -77,7 +138,7 @@ function doInstallNvm(next) {
77
138
  return;
78
139
  }
79
140
  console.log("");
80
- var ok = run("curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash");
141
+ var ok = run(dl + " https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash");
81
142
  if (!ok) {
82
143
  bail("\n nvm install failed. See https://github.com/nvm-sh/nvm#installing-and-updating\n");
83
144
  return;
@@ -157,6 +218,7 @@ ask(" Install Node.js 20 now? [Y/n] ", function(a) {
157
218
  bail(
158
219
  "\n To install manually:\n" +
159
220
  " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\n" +
221
+ " # or: wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\n" +
160
222
  " source ~/.bashrc\n" +
161
223
  " nvm install 20\n" +
162
224
  " nvm alias default 20\n" +
@@ -165,9 +227,12 @@ ask(" Install Node.js 20 now? [Y/n] ", function(a) {
165
227
  return;
166
228
  }
167
229
  console.log("");
168
- doInstallNvm(function() {
169
- doInstallNode(function() {
170
- doReinstall();
230
+ // Ensure curl or wget is available first
231
+ ensureDownloader(function(dl) {
232
+ doInstallNvm(dl, function() {
233
+ doInstallNode(function() {
234
+ doReinstall();
235
+ });
171
236
  });
172
237
  });
173
238
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",