open-agents-ai 0.15.1 → 0.15.3

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/index.js +845 -735
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8870,13 +8870,6 @@ ${newerSummary}` : newerSummary;
8870
8870
  acc.id = chunk.toolCallId;
8871
8871
  if (chunk.toolCallArgs) {
8872
8872
  acc.args += chunk.toolCallArgs;
8873
- this.emit({
8874
- type: "stream_token",
8875
- content: chunk.toolCallArgs,
8876
- streamKind: "tool_args",
8877
- turn,
8878
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
8879
- });
8880
8873
  }
8881
8874
  }
8882
8875
  }
@@ -10552,844 +10545,939 @@ var init_oa_directory = __esm({
10552
10545
  }
10553
10546
  });
10554
10547
 
10555
- // packages/cli/dist/tui/commands.js
10556
- async function handleSlashCommand(input, ctx) {
10557
- const trimmed = input.trim();
10558
- if (!trimmed.startsWith("/"))
10559
- return "not_a_command";
10560
- const [cmd, ...rest] = trimmed.slice(1).split(/\s+/);
10561
- const hasLocal = rest.includes("--local");
10562
- const filteredRest = rest.filter((r) => r !== "--local");
10563
- const arg = filteredRest.join(" ").trim();
10564
- switch (cmd) {
10565
- case "help":
10566
- case "h":
10567
- case "?":
10568
- renderSlashHelp();
10569
- return "handled";
10570
- case "quit":
10571
- case "exit":
10572
- case "q":
10573
- return "exit";
10574
- case "clear":
10575
- case "cls":
10576
- ctx.clearScreen();
10577
- return "handled";
10578
- case "verbose":
10579
- case "v":
10580
- ctx.setVerbose(!ctx.config.verbose);
10581
- if (hasLocal) {
10582
- ctx.saveLocalSettings({ verbose: ctx.config.verbose });
10583
- renderInfo(`Verbose mode: ${ctx.config.verbose ? "on" : "off"} (project-local)`);
10584
- } else {
10585
- ctx.saveSettings({ verbose: ctx.config.verbose });
10586
- renderInfo(`Verbose mode: ${ctx.config.verbose ? "on" : "off"}`);
10548
+ // packages/cli/dist/tui/setup.js
10549
+ import * as readline from "node:readline";
10550
+ import { execSync as execSync10 } from "node:child_process";
10551
+ import { existsSync as existsSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "node:fs";
10552
+ import { join as join18 } from "node:path";
10553
+ import { homedir as homedir8 } from "node:os";
10554
+ function detectSystemSpecs() {
10555
+ let totalRamGB = 0;
10556
+ let availableRamGB = 0;
10557
+ let gpuVramGB = 0;
10558
+ let gpuName = "";
10559
+ try {
10560
+ const memInfo = execSync10("free -b 2>/dev/null || sysctl -n hw.memsize 2>/dev/null", {
10561
+ encoding: "utf8",
10562
+ timeout: 5e3
10563
+ });
10564
+ if (memInfo.includes("Mem:")) {
10565
+ const match = memInfo.match(/^Mem:\s+(\d+)\s+\d+\s+\d+\s+\d+\s+\d+\s+(\d+)/m);
10566
+ if (match) {
10567
+ totalRamGB = parseInt(match[1], 10) / 1024 ** 3;
10568
+ availableRamGB = parseInt(match[2], 10) / 1024 ** 3;
10587
10569
  }
10588
- return "handled";
10589
- case "config":
10590
- case "cfg":
10591
- renderConfig({
10592
- model: ctx.config.model,
10593
- backendType: ctx.config.backendType,
10594
- backendUrl: ctx.config.backendUrl,
10595
- timeoutMs: String(ctx.config.timeoutMs),
10596
- maxRetries: String(ctx.config.maxRetries),
10597
- verbose: String(ctx.config.verbose),
10598
- dryRun: String(ctx.config.dryRun)
10599
- });
10600
- return "handled";
10601
- case "model":
10602
- if (arg) {
10603
- await switchModel(arg, ctx, hasLocal);
10604
- } else {
10605
- await showModelPicker(ctx);
10570
+ } else {
10571
+ const bytes = parseInt(memInfo.trim(), 10);
10572
+ if (!isNaN(bytes)) {
10573
+ totalRamGB = bytes / 1024 ** 3;
10574
+ availableRamGB = totalRamGB * 0.7;
10606
10575
  }
10607
- return "handled";
10608
- case "models":
10609
- await listModels(ctx);
10610
- return "handled";
10611
- case "endpoint":
10612
- case "ep":
10613
- await handleEndpoint(arg, ctx, hasLocal);
10614
- return "handled";
10615
- case "update":
10616
- case "upgrade":
10617
- await handleUpdate(arg, ctx.repoRoot);
10618
- return "handled";
10619
- case "voice": {
10620
- const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
10621
- if (arg) {
10622
- const msg = await ctx.voiceSetModel(arg);
10623
- save({ voice: true, voiceModel: arg });
10624
- renderInfo(msg + (hasLocal ? " (project-local)" : ""));
10625
- } else {
10626
- const msg = await ctx.voiceToggle();
10627
- const isOn = msg.toLowerCase().includes("enabled") || msg.toLowerCase().includes("on");
10628
- save({ voice: isOn });
10629
- renderInfo(msg + (hasLocal ? " (project-local)" : ""));
10576
+ }
10577
+ } catch {
10578
+ }
10579
+ try {
10580
+ const nvidiaSmi = execSync10("nvidia-smi --query-gpu=memory.total,name --format=csv,noheader,nounits 2>/dev/null", { encoding: "utf8", timeout: 5e3 });
10581
+ const lines = nvidiaSmi.trim().split("\n");
10582
+ if (lines.length > 0) {
10583
+ for (const line of lines) {
10584
+ const parts = line.split(",").map((s) => s.trim());
10585
+ const vramMB = parseInt(parts[0] ?? "0", 10);
10586
+ if (!isNaN(vramMB))
10587
+ gpuVramGB += vramMB / 1024;
10588
+ if (!gpuName && parts[1])
10589
+ gpuName = parts[1];
10630
10590
  }
10631
- return "handled";
10632
10591
  }
10633
- case "stream": {
10634
- const isOn = ctx.streamToggle();
10635
- const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
10636
- save({ stream: isOn });
10637
- renderInfo(`Token streaming: ${isOn ? "on" : "off"}${hasLocal ? " (project-local)" : ""}` + (isOn ? " \u2014 thinking tokens in grey italics, responses with pastel syntax highlighting" : ""));
10638
- return "handled";
10592
+ } catch {
10593
+ }
10594
+ return {
10595
+ totalRamGB: Math.round(totalRamGB * 10) / 10,
10596
+ availableRamGB: Math.round(availableRamGB * 10) / 10,
10597
+ gpuVramGB: Math.round(gpuVramGB * 10) / 10,
10598
+ gpuName
10599
+ };
10600
+ }
10601
+ function recommendModel(specs) {
10602
+ const effectiveGB = Math.max(specs.gpuVramGB, specs.availableRamGB);
10603
+ const budget = effectiveGB * 0.8;
10604
+ const localVariants = QWEN_VARIANTS.filter((v) => !v.cloud);
10605
+ for (let i = localVariants.length - 1; i >= 0; i--) {
10606
+ if (localVariants[i].sizeGB <= budget) {
10607
+ return localVariants[i];
10639
10608
  }
10640
- case "tools": {
10641
- const tools = listCustomToolFiles(ctx.repoRoot);
10642
- if (tools.length === 0) {
10643
- renderInfo("No custom tools installed.");
10644
- renderInfo("The agent will automatically create tools when it detects repeated workflows (3+ times).");
10645
- renderInfo('Or ask the agent: "create a tool for [workflow]"');
10646
- } else {
10647
- process.stdout.write(`
10648
- ${c2.bold("Custom Tools:")}
10609
+ }
10610
+ return QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:cloud");
10611
+ }
10612
+ function calculateContextWindow(specs, modelSizeGB2) {
10613
+ const totalAvail = Math.max(specs.gpuVramGB, specs.totalRamGB);
10614
+ const remaining = totalAvail - modelSizeGB2;
10615
+ if (remaining >= 200)
10616
+ return { numCtx: 131072, label: "128K" };
10617
+ if (remaining >= 100)
10618
+ return { numCtx: 65536, label: "64K" };
10619
+ if (remaining >= 50)
10620
+ return { numCtx: 32768, label: "32K" };
10621
+ if (remaining >= 20)
10622
+ return { numCtx: 16384, label: "16K" };
10623
+ if (remaining >= 8)
10624
+ return { numCtx: 8192, label: "8K" };
10625
+ return { numCtx: 4096, label: "4K" };
10626
+ }
10627
+ function modelSupportsToolCalling(modelName) {
10628
+ const lower = modelName.toLowerCase();
10629
+ for (const known of TOOL_CALLING_MODELS) {
10630
+ if (lower.startsWith(known) || lower.includes(known))
10631
+ return true;
10632
+ }
10633
+ return false;
10634
+ }
10635
+ function ask(rl, question) {
10636
+ return new Promise((resolve16) => {
10637
+ rl.question(question, (answer) => resolve16(answer.trim()));
10638
+ });
10639
+ }
10640
+ function pullModelWithAutoUpdate(tag) {
10641
+ try {
10642
+ execSync10(`ollama pull ${tag}`, {
10643
+ stdio: "inherit",
10644
+ timeout: 36e5
10645
+ // 1 hour max
10646
+ });
10647
+ } catch (err) {
10648
+ const errMsg = err instanceof Error ? err.message : String(err);
10649
+ const stderr = err?.stderr?.toString?.() ?? errMsg;
10650
+ const combined = errMsg + "\n" + stderr;
10651
+ if (combined.includes("412") || combined.includes("newer version") || combined.includes("requires a newer version")) {
10652
+ process.stdout.write(`
10653
+ ${c2.yellow("\u26A0")} Ollama needs to be updated for this model.
10654
+ `);
10655
+ process.stdout.write(` ${c2.cyan("\u25CF")} Updating Ollama via official install script...
10649
10656
 
10650
10657
  `);
10651
- for (const t of tools) {
10652
- process.stdout.write(` ${c2.cyan(t.name.padEnd(28))} ${c2.dim(`(${t.scope}, v${t.version}, ${t.stepsCount} steps)`)}
10658
+ try {
10659
+ execSync10("curl -fsSL https://ollama.com/install.sh | sh", {
10660
+ stdio: "inherit",
10661
+ timeout: 3e5
10662
+ // 5 min max for install
10663
+ });
10664
+ process.stdout.write(`
10665
+ ${c2.green("\u2714")} Ollama updated successfully.
10653
10666
  `);
10654
- process.stdout.write(` ${"".padEnd(28)} ${t.description}
10667
+ process.stdout.write(` ${c2.cyan("\u25CF")} Retrying pull of ${c2.bold(tag)}...
10668
+
10655
10669
  `);
10656
- }
10657
- process.stdout.write("\n");
10670
+ execSync10(`ollama pull ${tag}`, {
10671
+ stdio: "inherit",
10672
+ timeout: 36e5
10673
+ });
10674
+ } catch (updateErr) {
10675
+ const updateMsg = updateErr instanceof Error ? updateErr.message : String(updateErr);
10676
+ throw new Error(`Failed to update Ollama and retry pull: ${updateMsg}
10677
+ Try manually:
10678
+ curl -fsSL https://ollama.com/install.sh | sh
10679
+ ollama pull ${tag}`);
10658
10680
  }
10659
- return "handled";
10681
+ } else {
10682
+ throw err;
10660
10683
  }
10661
- case "skills":
10662
- case "skill": {
10663
- const skills = discoverSkills(ctx.repoRoot);
10664
- if (skills.length === 0) {
10665
- renderInfo("No skills found.");
10666
- renderInfo("Install AIWG to get skills: npm i -g aiwg && aiwg use sdlc");
10667
- renderInfo("Or add skills manually to .oa/skills/{name}/SKILL.md");
10668
- } else {
10669
- let filtered = skills;
10670
- if (arg) {
10671
- const q = arg.toLowerCase();
10672
- filtered = skills.filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.triggers.some((t) => t.toLowerCase().includes(q)));
10673
- }
10674
- if (filtered.length === 0) {
10675
- renderWarning(`No skills matching "${arg}". Showing all ${skills.length} skills:`);
10676
- filtered = skills;
10677
- }
10678
- const bySource = /* @__PURE__ */ new Map();
10679
- for (const s of filtered) {
10680
- const group = bySource.get(s.source) ?? [];
10681
- group.push(s);
10682
- bySource.set(s.source, group);
10683
- }
10684
- process.stdout.write(`
10685
- ${c2.bold(`Available Skills (${filtered.length}):`)}
10686
- `);
10687
- for (const [source, group] of bySource) {
10688
- process.stdout.write(`
10689
- ${c2.dim(`\u2500\u2500 ${source} (${group.length}) \u2500\u2500`)}
10690
- `);
10691
- for (const s of group) {
10692
- process.stdout.write(` ${c2.cyan(s.name.padEnd(32))} ${s.description.slice(0, 60)}
10693
- `);
10694
- if (s.triggers.length > 0) {
10695
- process.stdout.write(` ${"".padEnd(32)} ${c2.dim(`triggers: ${s.triggers.slice(0, 3).join(" | ")}`)}
10696
- `);
10697
- }
10698
- }
10699
- }
10700
- process.stdout.write("\n");
10701
- renderInfo('Invoke directly: /<skill-name> [args] (e.g. /ralph "fix tests" --completion "npm test passes")');
10702
- renderInfo("Filter with: /skills <keyword>");
10703
- }
10704
- return "handled";
10705
- }
10706
- case "dream": {
10707
- if (arg === "stop" || arg === "wake") {
10708
- if (ctx.isDreaming?.()) {
10709
- ctx.dreamStop?.();
10710
- renderInfo("Waking up from dream mode...");
10711
- } else {
10712
- renderWarning("Not currently dreaming.");
10713
- }
10714
- } else if (ctx.isDreaming?.()) {
10715
- renderWarning("Already dreaming. Use /dream stop to wake up first.");
10716
- } else {
10717
- const mode = arg === "lucid" ? "lucid" : arg === "deep" ? "deep" : "default";
10718
- ctx.dreamStart?.(mode);
10719
- }
10720
- return "handled";
10721
- }
10722
- case "listen":
10723
- case "mic": {
10724
- if (!ctx.listenToggle) {
10725
- renderWarning("Listen mode not available in this context.");
10726
- return "handled";
10727
- }
10728
- if (arg === "stop" || arg === "off") {
10729
- const msg2 = await (ctx.listenStop?.() ?? Promise.resolve("Not listening."));
10730
- renderInfo(msg2);
10731
- return "handled";
10732
- }
10733
- if (arg === "confirm") {
10734
- const msg2 = ctx.listenSetMode?.("confirm") ?? "Confirm mode set.";
10735
- renderInfo(msg2);
10736
- return "handled";
10737
- }
10738
- if (arg === "auto") {
10739
- const msg2 = ctx.listenSetMode?.("auto") ?? "Auto mode set.";
10740
- renderInfo(msg2);
10741
- return "handled";
10742
- }
10743
- const modelSizes = ["tiny", "base", "small", "medium", "large", "large-v3"];
10744
- if (arg && modelSizes.includes(arg.toLowerCase())) {
10745
- const model = arg.toLowerCase() === "large" ? "large-v3" : arg.toLowerCase();
10746
- const msg2 = await (ctx.listenSetModel?.(model) ?? Promise.resolve(`Model set to ${model}.`));
10747
- renderInfo(msg2);
10748
- return "handled";
10749
- }
10750
- const msg = await ctx.listenToggle();
10751
- renderInfo(msg);
10752
- return "handled";
10753
- }
10754
- case "bruteforce":
10755
- case "brute": {
10756
- const isOn = ctx.bruteForceToggle();
10757
- const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
10758
- save({ bruteforce: isOn });
10759
- renderInfo(`Brute-force mode: ${isOn ? "on" : "off"}${hasLocal ? " (project-local)" : ""}` + (isOn ? " \u2014 agent will auto re-engage when turn limit is hit, reassess and try creative strategies" : ""));
10760
- return "handled";
10761
- }
10762
- case "emojis":
10763
- case "emoji": {
10764
- const current = ctx.getEmojis?.() ?? true;
10765
- const next = !current;
10766
- ctx.setEmojis?.(next);
10767
- const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
10768
- save({ emojis: next });
10769
- renderInfo(`Emojis ${next ? "enabled" : "disabled"}.`);
10770
- return "handled";
10771
- }
10772
- case "colors":
10773
- case "color": {
10774
- const current = ctx.getColors?.() ?? true;
10775
- const next = !current;
10776
- ctx.setColors?.(next);
10777
- const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
10778
- save({ colors: next });
10779
- renderInfo(`Colors ${next ? "enabled" : "disabled"}.`);
10780
- return "handled";
10781
- }
10782
- default: {
10783
- const skills = discoverSkills(ctx.repoRoot);
10784
- const skill = skills.find((s) => s.name === cmd || s.name === cmd.replace(/_/g, "-"));
10785
- if (skill) {
10786
- const content = loadSkillContent(skill.filePath);
10787
- if (content) {
10788
- renderInfo(`Loading skill: ${c2.bold(skill.name)} (${skill.source})`);
10789
- return { type: "skill", name: skill.name, content, args: arg };
10790
- }
10791
- }
10792
- renderWarning(`Unknown command: /${cmd}. Type /help for available commands.`);
10793
- return "handled";
10794
- }
10795
- }
10796
- }
10797
- async function listModels(ctx) {
10798
- try {
10799
- const models = await fetchOllamaModels(ctx.config.backendUrl);
10800
- renderModelList(models.map((m) => ({ name: m.name, size: m.size, modified: m.modified })), ctx.config.model);
10801
- } catch (err) {
10802
- renderError(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
10803
10684
  }
10804
10685
  }
10805
- async function showModelPicker(ctx) {
10686
+ async function runSetupWizard(config) {
10687
+ const rl = readline.createInterface({
10688
+ input: process.stdin,
10689
+ output: process.stdout,
10690
+ terminal: true
10691
+ });
10806
10692
  try {
10807
- const models = await fetchOllamaModels(ctx.config.backendUrl);
10808
- if (models.length === 0) {
10809
- renderWarning("No models found. Pull a model with: ollama pull <model>");
10810
- return;
10811
- }
10812
- renderModelList(models.map((m) => ({ name: m.name, size: m.size, modified: m.modified })), ctx.config.model);
10813
- } catch (err) {
10814
- renderError(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
10693
+ return await doSetup(config, rl);
10694
+ } finally {
10695
+ rl.close();
10815
10696
  }
10816
10697
  }
10817
- async function handleEndpoint(arg, ctx, local = false) {
10818
- if (!arg) {
10819
- process.stdout.write(`
10820
- ${c2.bold("Current endpoint:")}
10821
-
10822
- `);
10823
- process.stdout.write(` ${c2.cyan("URL".padEnd(12))} ${ctx.config.backendUrl}
10698
+ async function doSetup(config, rl) {
10699
+ process.stdout.write(`
10700
+ ${c2.bold(c2.cyan("open-agents"))}
10824
10701
  `);
10825
- process.stdout.write(` ${c2.cyan("Type".padEnd(12))} ${ctx.config.backendType}
10702
+ process.stdout.write(` ${c2.dim("\u2500".repeat(60))}
10826
10703
  `);
10827
- process.stdout.write(` ${c2.cyan("Auth".padEnd(12))} ${ctx.config.apiKey ? "Bearer token set" : "none"}
10704
+ process.stdout.write(` ${c2.bold("First-run setup")}
10705
+
10828
10706
  `);
10829
- process.stdout.write(`
10830
- ${c2.dim("Usage: /endpoint <url> [--auth <token>]")}
10707
+ process.stdout.write(` ${c2.cyan("\u25CF")} Detecting system specs...
10831
10708
  `);
10832
- process.stdout.write(` ${c2.dim(" /endpoint http://localhost:11434 (Ollama, no auth)")}
10709
+ const specs = detectSystemSpecs();
10710
+ process.stdout.write(` ${c2.dim(" RAM:")} ${specs.totalRamGB.toFixed(1)} GB total, ${specs.availableRamGB.toFixed(1)} GB available
10833
10711
  `);
10834
- process.stdout.write(` ${c2.dim(" /endpoint http://remote:8000/v1 --auth sk-... (OpenAI-compatible)")}
10712
+ if (specs.gpuVramGB > 0) {
10713
+ process.stdout.write(` ${c2.dim(" GPU:")} ${specs.gpuName || "NVIDIA"} \u2014 ${specs.gpuVramGB.toFixed(1)} GB VRAM
10835
10714
  `);
10836
- process.stdout.write(` ${c2.dim(" /endpoint http://remote:8000/v1 (OpenAI-compatible, no auth)")}
10837
-
10715
+ } else {
10716
+ process.stdout.write(` ${c2.dim(" GPU:")} No NVIDIA GPU detected (CPU inference)
10838
10717
  `);
10839
- return;
10840
- }
10841
- const parts = arg.split(/\s+/);
10842
- const url = parts[0];
10843
- let apiKey;
10844
- const authIdx = parts.indexOf("--auth");
10845
- if (authIdx !== -1 && parts[authIdx + 1]) {
10846
- apiKey = parts[authIdx + 1];
10847
10718
  }
10719
+ process.stdout.write("\n");
10720
+ let models = [];
10848
10721
  try {
10849
- new URL(url);
10722
+ models = await fetchOllamaModels(config.backendUrl);
10850
10723
  } catch {
10851
- renderError(`Invalid URL: "${url}"`);
10852
- return;
10853
- }
10854
- let backendType = "ollama";
10855
- if (url.includes("/v1") || url.includes(":8000") || apiKey) {
10856
- backendType = "vllm";
10724
+ renderError(`Cannot reach Ollama at ${config.backendUrl}`);
10725
+ renderInfo("Start Ollama with: ollama serve");
10726
+ renderInfo("Or use /endpoint to configure a remote backend after startup.");
10727
+ const answer = await ask(rl, `
10728
+ ${c2.bold("Continue without Ollama?")} (y/n) `);
10729
+ if (answer.toLowerCase() !== "y")
10730
+ return null;
10731
+ return config.model;
10857
10732
  }
10858
- process.stdout.write(`
10859
- ${c2.dim("Testing connection...")} `);
10860
- try {
10861
- const healthUrl = backendType === "ollama" ? `${url.replace(/\/$/, "")}/api/tags` : `${url.replace(/\/$/, "")}/models`;
10862
- const headers = {};
10863
- if (apiKey)
10864
- headers["Authorization"] = `Bearer ${apiKey}`;
10865
- const resp = await fetch(healthUrl, {
10866
- headers,
10867
- signal: AbortSignal.timeout(1e4)
10868
- });
10869
- if (!resp.ok)
10870
- throw new Error(`HTTP ${resp.status}`);
10871
- process.stdout.write(`${c2.green("\u2714")} Connected
10872
- `);
10873
- } catch (err) {
10874
- process.stdout.write(`${c2.yellow("\u26A0")} Could not verify
10733
+ const currentModel = findModel(models, config.model);
10734
+ if (currentModel) {
10735
+ process.stdout.write(` ${c2.green("\u2714")} Model ${c2.bold(currentModel.name)} is available.
10736
+
10875
10737
  `);
10876
- renderWarning(`Endpoint may not be reachable: ${err instanceof Error ? err.message : String(err)}`);
10877
- renderInfo("Setting endpoint anyway \u2014 it may come online later.");
10878
- }
10879
- ctx.setEndpoint(url, backendType, apiKey);
10880
- const endpointSettings = { backendUrl: url, backendType, ...apiKey ? { apiKey } : {} };
10881
- if (local) {
10882
- ctx.saveLocalSettings(endpointSettings);
10883
- } else {
10884
- setConfigValue("backendUrl", url);
10885
- setConfigValue("backendType", backendType);
10886
- if (apiKey) {
10887
- setConfigValue("apiKey", apiKey);
10888
- }
10889
- ctx.saveSettings(endpointSettings);
10738
+ return currentModel.name;
10890
10739
  }
10891
- process.stdout.write(`
10892
- ${c2.green("\u2714")} Endpoint updated and saved${local ? " (project-local)" : ""}:
10740
+ process.stdout.write(` ${c2.yellow("\u26A0")} Default model ${c2.bold(config.model)} is not available.
10741
+
10893
10742
  `);
10894
- process.stdout.write(` ${c2.cyan("URL".padEnd(8))} ${url}
10743
+ const toolCallingModels = models.filter((m) => modelSupportsToolCalling(m.name));
10744
+ if (toolCallingModels.length > 0) {
10745
+ process.stdout.write(` ${c2.cyan("\u25CF")} Found ${toolCallingModels.length} model(s) with tool-calling support:
10746
+
10895
10747
  `);
10896
- process.stdout.write(` ${c2.cyan("Type".padEnd(8))} ${backendType}
10748
+ for (let i = 0; i < Math.min(toolCallingModels.length, 10); i++) {
10749
+ const m = toolCallingModels[i];
10750
+ process.stdout.write(` ${c2.bold(String(i + 1))}. ${m.name} ${c2.dim(`(${m.size})`)}
10897
10751
  `);
10898
- if (apiKey) {
10899
- process.stdout.write(` ${c2.cyan("Auth".padEnd(8))} Bearer ${apiKey.slice(0, 8)}...
10752
+ }
10753
+ process.stdout.write(`
10754
+ ${c2.dim("0")}. Pull a new qwen3.5 model instead
10755
+ `);
10756
+ process.stdout.write("\n");
10757
+ const choice = await ask(rl, ` ${c2.bold("Select a model")} (1-${Math.min(toolCallingModels.length, 10)}, or 0 to pull new): `);
10758
+ const idx = parseInt(choice, 10);
10759
+ if (idx > 0 && idx <= toolCallingModels.length) {
10760
+ const selected = toolCallingModels[idx - 1];
10761
+ setConfigValue("model", selected.name);
10762
+ process.stdout.write(`
10763
+ ${c2.green("\u2714")} Selected ${c2.bold(selected.name)}. Saved to config.
10764
+
10900
10765
  `);
10766
+ return selected.name;
10767
+ }
10901
10768
  } else {
10902
- process.stdout.write(` ${c2.cyan("Auth".padEnd(8))} none
10769
+ process.stdout.write(` ${c2.yellow("\u26A0")} No tool-calling capable models found on this system.
10770
+
10903
10771
  `);
10904
10772
  }
10905
- process.stdout.write("\n");
10906
- }
10907
- async function handleUpdate(subcommand, repoRoot) {
10908
- if (subcommand === "auto") {
10909
- const settings = { updateMode: "auto" };
10910
- saveProjectSettings(repoRoot, settings);
10911
- saveGlobalSettings(settings);
10912
- renderInfo("Update mode: auto \u2014 updates will install automatically after task completion.");
10913
- return;
10773
+ const recommended = recommendModel(specs);
10774
+ process.stdout.write(` ${c2.cyan("\u25CF")} Recommended model based on your system:
10775
+
10776
+ `);
10777
+ const localVariants = QWEN_VARIANTS.filter((v) => !v.cloud);
10778
+ for (let i = 0; i < localVariants.length; i++) {
10779
+ const v = localVariants[i];
10780
+ const fits = v.sizeGB <= Math.max(specs.gpuVramGB, specs.availableRamGB) * 0.8;
10781
+ const isRec = v.tag === recommended.tag;
10782
+ const marker = isRec ? c2.green("\u2192") : fits ? c2.dim(" ") : c2.red("\u2716");
10783
+ const name = isRec ? c2.bold(c2.green(v.tag)) : fits ? v.tag : c2.dim(v.tag);
10784
+ const label = isRec ? c2.bold(v.label) : c2.dim(v.label);
10785
+ const tooLarge = !fits && !v.cloud ? c2.red(" (exceeds available memory)") : "";
10786
+ process.stdout.write(` ${marker} ${String(i + 1).padStart(2)}. ${name.padEnd(isRec ? 45 : 25)} ${label}${tooLarge}
10787
+ `);
10914
10788
  }
10915
- if (subcommand === "manual") {
10916
- const settings = { updateMode: "manual" };
10917
- saveProjectSettings(repoRoot, settings);
10918
- saveGlobalSettings(settings);
10919
- renderInfo("Update mode: manual \u2014 updates only install when you run /update.");
10920
- return;
10789
+ process.stdout.write(`
10790
+ ${c2.dim(" ")} ${String(localVariants.length + 1).padStart(2)}. ${c2.dim("qwen3.5:cloud")} ${c2.dim("Ollama Cloud")}
10791
+ `);
10792
+ process.stdout.write(` ${c2.dim(" ")} ${String(localVariants.length + 2).padStart(2)}. ${c2.dim("qwen3.5:397b-cloud")} ${c2.dim("397B Ollama Cloud")}
10793
+ `);
10794
+ process.stdout.write("\n");
10795
+ const pullChoice = await ask(rl, ` ${c2.bold("Select a model to pull")} (1-${localVariants.length + 2}, or Enter for recommended): `);
10796
+ const pullIdx = pullChoice ? parseInt(pullChoice, 10) : 0;
10797
+ let selectedVariant;
10798
+ if (pullIdx === 0 || isNaN(pullIdx)) {
10799
+ selectedVariant = recommended;
10800
+ } else if (pullIdx <= localVariants.length) {
10801
+ selectedVariant = localVariants[pullIdx - 1];
10802
+ } else if (pullIdx === localVariants.length + 1) {
10803
+ selectedVariant = QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:cloud");
10804
+ } else {
10805
+ selectedVariant = QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:397b-cloud");
10921
10806
  }
10922
- let currentVersion = "0.0.0";
10923
- try {
10924
- const { createRequire: createRequire4 } = await import("node:module");
10925
- const { fileURLToPath: fileURLToPath3 } = await import("node:url");
10926
- const { dirname: dirname5, join: join28 } = await import("node:path");
10927
- const { existsSync: existsSync19 } = await import("node:fs");
10928
- const req = createRequire4(import.meta.url);
10929
- const thisDir = dirname5(fileURLToPath3(import.meta.url));
10930
- const candidates = [
10931
- join28(thisDir, "..", "package.json"),
10932
- join28(thisDir, "..", "..", "package.json"),
10933
- join28(thisDir, "..", "..", "..", "package.json")
10934
- ];
10935
- for (const pkgPath of candidates) {
10936
- if (existsSync19(pkgPath)) {
10937
- const pkg = req(pkgPath);
10938
- if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli") {
10939
- currentVersion = pkg.version ?? "0.0.0";
10940
- break;
10941
- }
10942
- }
10943
- }
10944
- } catch {
10807
+ const confirmPull = await ask(rl, `
10808
+ Pull ${c2.bold(selectedVariant.tag)} (${selectedVariant.label})? (Y/n) `);
10809
+ if (confirmPull.toLowerCase() === "n") {
10810
+ process.stdout.write(`
10811
+ ${c2.dim("Skipping model pull. You can pull manually with: ollama pull <model>")}
10812
+
10813
+ `);
10814
+ return config.model;
10945
10815
  }
10946
10816
  process.stdout.write(`
10947
- ${c2.cyan("\u25CF")} Checking for updates... ${c2.dim(`(current: v${currentVersion})`)}
10817
+ ${c2.cyan("\u25CF")} Pulling ${c2.bold(selectedVariant.tag)}... (this may take a while)
10948
10818
  `);
10949
- const info = await checkForUpdate(currentVersion, true);
10950
- if (!info) {
10951
- process.stdout.write(` ${c2.green("\u2714")} You're on the latest version (v${currentVersion}).
10819
+ try {
10820
+ pullModelWithAutoUpdate(selectedVariant.tag);
10821
+ process.stdout.write(`
10822
+ ${c2.green("\u2714")} Model ${c2.bold(selectedVariant.tag)} pulled successfully.
10952
10823
 
10953
10824
  `);
10954
- return;
10825
+ } catch (err) {
10826
+ renderError(`Failed to pull model: ${err instanceof Error ? err.message : String(err)}`);
10827
+ renderInfo("Try manually: ollama pull " + selectedVariant.tag);
10828
+ return config.model;
10955
10829
  }
10956
- process.stdout.write(` ${c2.yellow("\u26A0")} Update available: v${info.currentVersion} \u2192 v${c2.bold(c2.green(info.latestVersion))}
10830
+ if (!selectedVariant.cloud) {
10831
+ const ctx = calculateContextWindow(specs, selectedVariant.sizeGB);
10832
+ const customName = `open-agents-${selectedVariant.tag.replace(":", "-").replace(".", "")}`;
10833
+ process.stdout.write(` ${c2.cyan("\u25CF")} Context window recommendation: ${c2.bold(ctx.label)} (${ctx.numCtx} tokens)
10957
10834
  `);
10958
- process.stdout.write(` ${c2.cyan("\u25CF")} Installing in background...
10835
+ process.stdout.write(` ${c2.dim(`Based on ${specs.totalRamGB.toFixed(0)} GB RAM, ${selectedVariant.sizeGB} GB model`)}
10959
10836
 
10960
10837
  `);
10961
- const { exec } = await import("node:child_process");
10962
- exec(`npm cache clean --force open-agents-ai 2>/dev/null; npm install -g open-agents-ai@latest --force`, { timeout: 18e4 }, (err) => {
10963
- if (err) {
10964
- renderWarning("Update install failed. Try manually: npm i -g open-agents-ai");
10965
- } else {
10966
- renderInfo(`${c2.green("\u2714")} Updated to v${info.latestVersion}. Takes effect next session.`);
10838
+ const createModelfile = await ask(rl, ` Create optimized model "${c2.bold(customName)}" with ${ctx.label} context? (Y/n) `);
10839
+ if (createModelfile.toLowerCase() !== "n") {
10840
+ try {
10841
+ const modelfileContent = [
10842
+ `FROM ${selectedVariant.tag}`,
10843
+ `PARAMETER num_ctx ${ctx.numCtx}`,
10844
+ `PARAMETER temperature 0`,
10845
+ `PARAMETER num_predict 16384`,
10846
+ `PARAMETER stop "<|endoftext|>"`
10847
+ ].join("\n");
10848
+ const modelDir2 = join18(homedir8(), ".open-agents", "models");
10849
+ mkdirSync7(modelDir2, { recursive: true });
10850
+ const modelfilePath = join18(modelDir2, `Modelfile.${customName}`);
10851
+ writeFileSync7(modelfilePath, modelfileContent + "\n", "utf8");
10852
+ process.stdout.write(` ${c2.dim("Creating model...")} `);
10853
+ execSync10(`ollama create ${customName} -f ${modelfilePath}`, {
10854
+ stdio: "pipe",
10855
+ timeout: 12e4
10856
+ });
10857
+ process.stdout.write(`${c2.green("\u2714")}
10858
+ `);
10859
+ setConfigValue("model", customName);
10860
+ process.stdout.write(`
10861
+ ${c2.green("\u2714")} Model ${c2.bold(customName)} created with ${ctx.label} context.
10862
+ `);
10863
+ process.stdout.write(` ${c2.green("\u2714")} Saved as default model in config.
10864
+
10865
+ `);
10866
+ return customName;
10867
+ } catch (err) {
10868
+ renderWarning(`Could not create custom model: ${err instanceof Error ? err.message : String(err)}`);
10869
+ renderInfo(`Using base model ${selectedVariant.tag} instead.`);
10870
+ }
10967
10871
  }
10968
- });
10872
+ setConfigValue("model", selectedVariant.tag);
10873
+ process.stdout.write(`
10874
+ ${c2.green("\u2714")} Saved ${c2.bold(selectedVariant.tag)} as default model.
10875
+
10876
+ `);
10877
+ return selectedVariant.tag;
10878
+ }
10879
+ setConfigValue("model", selectedVariant.tag);
10880
+ process.stdout.write(`
10881
+ ${c2.green("\u2714")} Saved ${c2.bold(selectedVariant.tag)} as default model.
10882
+
10883
+ `);
10884
+ return selectedVariant.tag;
10969
10885
  }
10970
- async function switchModel(query, ctx, local = false) {
10886
+ async function isModelAvailable(config) {
10971
10887
  try {
10972
- const models = await fetchOllamaModels(ctx.config.backendUrl);
10973
- const match = findModel(models, query);
10974
- if (!match) {
10975
- renderError(`Model not found: "${query}"`);
10976
- renderInfo("Available models:");
10977
- for (const m of models.slice(0, 10)) {
10978
- renderInfo(` ${m.name}`);
10979
- }
10980
- return;
10981
- }
10982
- const oldModel = ctx.config.model;
10983
- ctx.setModel(match.name);
10984
- if (local) {
10985
- ctx.saveLocalSettings({ model: match.name });
10986
- } else {
10987
- ctx.saveSettings({ model: match.name });
10988
- }
10989
- renderModelSwitch(oldModel, match.name);
10990
- if (local) {
10991
- renderInfo("Saved as project-local override.");
10992
- }
10993
- } catch (err) {
10994
- renderError(`Failed to switch model: ${err instanceof Error ? err.message : String(err)}`);
10888
+ const models = await fetchOllamaModels(config.backendUrl);
10889
+ return !!findModel(models, config.model);
10890
+ } catch {
10891
+ return false;
10995
10892
  }
10996
10893
  }
10997
- var init_commands = __esm({
10998
- "packages/cli/dist/tui/commands.js"() {
10999
- "use strict";
11000
- init_model_picker();
11001
- init_render();
11002
- init_dist2();
11003
- init_config();
11004
- init_updater();
11005
- init_oa_directory();
10894
+ function isFirstRun() {
10895
+ try {
10896
+ return !existsSync13(join18(homedir8(), ".open-agents", "config.json"));
10897
+ } catch {
10898
+ return true;
11006
10899
  }
11007
- });
11008
-
11009
- // packages/cli/dist/tui/setup.js
11010
- import * as readline from "node:readline";
11011
- import { execSync as execSync10 } from "node:child_process";
11012
- import { existsSync as existsSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "node:fs";
11013
- import { join as join18 } from "node:path";
11014
- import { homedir as homedir8 } from "node:os";
11015
- function detectSystemSpecs() {
11016
- let totalRamGB = 0;
11017
- let availableRamGB = 0;
11018
- let gpuVramGB = 0;
11019
- let gpuName = "";
10900
+ }
10901
+ function expandedModelName(baseModel) {
10902
+ return `open-agents-${baseModel.replace(":", "-").replace(/\./g, "")}`;
10903
+ }
10904
+ async function checkExpandedVariant(modelName, backendUrl) {
10905
+ if (modelName.startsWith("open-agents-"))
10906
+ return null;
10907
+ const target = expandedModelName(modelName);
11020
10908
  try {
11021
- const memInfo = execSync10("free -b 2>/dev/null || sysctl -n hw.memsize 2>/dev/null", {
11022
- encoding: "utf8",
11023
- timeout: 5e3
11024
- });
11025
- if (memInfo.includes("Mem:")) {
11026
- const match = memInfo.match(/^Mem:\s+(\d+)\s+\d+\s+\d+\s+\d+\s+\d+\s+(\d+)/m);
11027
- if (match) {
11028
- totalRamGB = parseInt(match[1], 10) / 1024 ** 3;
11029
- availableRamGB = parseInt(match[2], 10) / 1024 ** 3;
11030
- }
11031
- } else {
11032
- const bytes = parseInt(memInfo.trim(), 10);
11033
- if (!isNaN(bytes)) {
11034
- totalRamGB = bytes / 1024 ** 3;
11035
- availableRamGB = totalRamGB * 0.7;
11036
- }
11037
- }
10909
+ const models = await fetchOllamaModels(backendUrl);
10910
+ const found = models.find((m) => m.name === target || m.name.startsWith(target + ":"));
10911
+ return found ? found.name : false;
11038
10912
  } catch {
10913
+ return false;
11039
10914
  }
10915
+ }
10916
+ function modelSizeGB(models, modelName) {
10917
+ const m = findModel(models, modelName);
10918
+ if (m)
10919
+ return m.sizeBytes / 1024 ** 3;
10920
+ const known = QWEN_VARIANTS.find((v) => modelName.includes(v.tag.split(":")[1] ?? ""));
10921
+ return known?.sizeGB ?? 4;
10922
+ }
10923
+ function createExpandedVariant(baseModel, specs, sizeGB) {
10924
+ const customName = expandedModelName(baseModel);
10925
+ const ctx = calculateContextWindow(specs, sizeGB);
11040
10926
  try {
11041
- const nvidiaSmi = execSync10("nvidia-smi --query-gpu=memory.total,name --format=csv,noheader,nounits 2>/dev/null", { encoding: "utf8", timeout: 5e3 });
11042
- const lines = nvidiaSmi.trim().split("\n");
11043
- if (lines.length > 0) {
11044
- for (const line of lines) {
11045
- const parts = line.split(",").map((s) => s.trim());
11046
- const vramMB = parseInt(parts[0] ?? "0", 10);
11047
- if (!isNaN(vramMB))
11048
- gpuVramGB += vramMB / 1024;
11049
- if (!gpuName && parts[1])
11050
- gpuName = parts[1];
11051
- }
11052
- }
10927
+ const modelfileContent = [
10928
+ `FROM ${baseModel}`,
10929
+ `PARAMETER num_ctx ${ctx.numCtx}`,
10930
+ `PARAMETER temperature 0`,
10931
+ `PARAMETER num_predict 16384`,
10932
+ `PARAMETER stop "<|endoftext|>"`
10933
+ ].join("\n");
10934
+ const modelDir2 = join18(homedir8(), ".open-agents", "models");
10935
+ mkdirSync7(modelDir2, { recursive: true });
10936
+ const modelfilePath = join18(modelDir2, `Modelfile.${customName}`);
10937
+ writeFileSync7(modelfilePath, modelfileContent + "\n", "utf8");
10938
+ execSync10(`ollama create ${customName} -f ${modelfilePath}`, {
10939
+ stdio: "pipe",
10940
+ timeout: 12e4
10941
+ });
10942
+ return customName;
11053
10943
  } catch {
10944
+ return null;
11054
10945
  }
11055
- return {
11056
- totalRamGB: Math.round(totalRamGB * 10) / 10,
11057
- availableRamGB: Math.round(availableRamGB * 10) / 10,
11058
- gpuVramGB: Math.round(gpuVramGB * 10) / 10,
11059
- gpuName
11060
- };
11061
10946
  }
11062
- function recommendModel(specs) {
11063
- const effectiveGB = Math.max(specs.gpuVramGB, specs.availableRamGB);
11064
- const budget = effectiveGB * 0.8;
11065
- const localVariants = QWEN_VARIANTS.filter((v) => !v.cloud);
11066
- for (let i = localVariants.length - 1; i >= 0; i--) {
11067
- if (localVariants[i].sizeGB <= budget) {
11068
- return localVariants[i];
10947
+ async function ensureExpandedContext(modelName, backendUrl) {
10948
+ if (modelName.startsWith("open-agents-")) {
10949
+ const specs2 = detectSystemSpecs();
10950
+ const ctx2 = calculateContextWindow(specs2, 4);
10951
+ return { model: modelName, created: false, contextLabel: ctx2.label, numCtx: ctx2.numCtx };
10952
+ }
10953
+ if (modelName.includes("cloud") || modelName.includes(":cloud")) {
10954
+ return { model: modelName, created: false, contextLabel: "remote", numCtx: 0 };
10955
+ }
10956
+ const existing = await checkExpandedVariant(modelName, backendUrl);
10957
+ if (existing === null) {
10958
+ return { model: modelName, created: false, contextLabel: "", numCtx: 0 };
10959
+ }
10960
+ const specs = detectSystemSpecs();
10961
+ if (typeof existing === "string") {
10962
+ let sizeGB2 = 4;
10963
+ try {
10964
+ const models = await fetchOllamaModels(backendUrl);
10965
+ sizeGB2 = modelSizeGB(models, modelName);
10966
+ } catch {
11069
10967
  }
10968
+ const ctx2 = calculateContextWindow(specs, sizeGB2);
10969
+ return { model: existing, created: false, contextLabel: ctx2.label, numCtx: ctx2.numCtx };
11070
10970
  }
11071
- return QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:cloud");
11072
- }
11073
- function calculateContextWindow(specs, modelSizeGB) {
11074
- const totalAvail = Math.max(specs.gpuVramGB, specs.totalRamGB);
11075
- const remaining = totalAvail - modelSizeGB;
11076
- if (remaining >= 200)
11077
- return { numCtx: 131072, label: "128K" };
11078
- if (remaining >= 100)
11079
- return { numCtx: 65536, label: "64K" };
11080
- if (remaining >= 50)
11081
- return { numCtx: 32768, label: "32K" };
11082
- if (remaining >= 20)
11083
- return { numCtx: 16384, label: "16K" };
11084
- if (remaining >= 8)
11085
- return { numCtx: 8192, label: "8K" };
11086
- return { numCtx: 4096, label: "4K" };
11087
- }
11088
- function modelSupportsToolCalling(modelName) {
11089
- const lower = modelName.toLowerCase();
11090
- for (const known of TOOL_CALLING_MODELS) {
11091
- if (lower.startsWith(known) || lower.includes(known))
11092
- return true;
10971
+ let sizeGB = 4;
10972
+ try {
10973
+ const models = await fetchOllamaModels(backendUrl);
10974
+ sizeGB = modelSizeGB(models, modelName);
10975
+ } catch {
11093
10976
  }
11094
- return false;
11095
- }
11096
- function ask(rl, question) {
11097
- return new Promise((resolve16) => {
11098
- rl.question(question, (answer) => resolve16(answer.trim()));
11099
- });
10977
+ const ctx = calculateContextWindow(specs, sizeGB);
10978
+ const created = createExpandedVariant(modelName, specs, sizeGB);
10979
+ if (created) {
10980
+ return { model: created, created: true, contextLabel: ctx.label, numCtx: ctx.numCtx };
10981
+ }
10982
+ return { model: modelName, created: false, contextLabel: ctx.label, numCtx: ctx.numCtx };
11100
10983
  }
11101
- function pullModelWithAutoUpdate(tag) {
11102
- try {
11103
- execSync10(`ollama pull ${tag}`, {
11104
- stdio: "inherit",
11105
- timeout: 36e5
11106
- // 1 hour max
11107
- });
11108
- } catch (err) {
11109
- const errMsg = err instanceof Error ? err.message : String(err);
11110
- const stderr = err?.stderr?.toString?.() ?? errMsg;
11111
- const combined = errMsg + "\n" + stderr;
11112
- if (combined.includes("412") || combined.includes("newer version") || combined.includes("requires a newer version")) {
11113
- process.stdout.write(`
11114
- ${c2.yellow("\u26A0")} Ollama needs to be updated for this model.
11115
- `);
11116
- process.stdout.write(` ${c2.cyan("\u25CF")} Updating Ollama via official install script...
11117
-
11118
- `);
11119
- try {
11120
- execSync10("curl -fsSL https://ollama.com/install.sh | sh", {
11121
- stdio: "inherit",
11122
- timeout: 3e5
11123
- // 5 min max for install
11124
- });
11125
- process.stdout.write(`
11126
- ${c2.green("\u2714")} Ollama updated successfully.
11127
- `);
11128
- process.stdout.write(` ${c2.cyan("\u25CF")} Retrying pull of ${c2.bold(tag)}...
10984
+ var QWEN_VARIANTS, TOOL_CALLING_MODELS;
10985
+ var init_setup = __esm({
10986
+ "packages/cli/dist/tui/setup.js"() {
10987
+ "use strict";
10988
+ init_model_picker();
10989
+ init_render();
10990
+ init_config();
10991
+ QWEN_VARIANTS = [
10992
+ { tag: "qwen3.5:0.8b", sizeGB: 1, label: "0.8B params (1.0 GB)", cloud: false },
10993
+ { tag: "qwen3.5:2b", sizeGB: 2.7, label: "2B params (2.7 GB)", cloud: false },
10994
+ { tag: "qwen3.5:4b", sizeGB: 3.4, label: "4B params (3.4 GB)", cloud: false },
10995
+ { tag: "qwen3.5:9b", sizeGB: 6.6, label: "9B params (6.6 GB) \u2014 recommended minimum", cloud: false },
10996
+ { tag: "qwen3.5:27b", sizeGB: 17, label: "27B params (17 GB)", cloud: false },
10997
+ { tag: "qwen3.5:35b", sizeGB: 24, label: "35B params (24 GB)", cloud: false },
10998
+ { tag: "qwen3.5:122b", sizeGB: 81, label: "122B params (81 GB) \u2014 best local", cloud: false },
10999
+ { tag: "qwen3.5:cloud", sizeGB: 0, label: "Cloud (Ollama Cloud)", cloud: true },
11000
+ { tag: "qwen3.5:397b-cloud", sizeGB: 0, label: "397B Cloud (Ollama Cloud)", cloud: true }
11001
+ ];
11002
+ TOOL_CALLING_MODELS = /* @__PURE__ */ new Set([
11003
+ "qwen3.5",
11004
+ "qwen3",
11005
+ "qwen2.5",
11006
+ "llama3.3",
11007
+ "llama3.1",
11008
+ "mistral",
11009
+ "mixtral",
11010
+ "command-r",
11011
+ "gemma3",
11012
+ "devstral",
11013
+ "deepseek"
11014
+ ]);
11015
+ }
11016
+ });
11129
11017
 
11130
- `);
11131
- execSync10(`ollama pull ${tag}`, {
11132
- stdio: "inherit",
11133
- timeout: 36e5
11134
- });
11135
- } catch (updateErr) {
11136
- const updateMsg = updateErr instanceof Error ? updateErr.message : String(updateErr);
11137
- throw new Error(`Failed to update Ollama and retry pull: ${updateMsg}
11138
- Try manually:
11139
- curl -fsSL https://ollama.com/install.sh | sh
11140
- ollama pull ${tag}`);
11018
+ // packages/cli/dist/tui/commands.js
11019
+ async function handleSlashCommand(input, ctx) {
11020
+ const trimmed = input.trim();
11021
+ if (!trimmed.startsWith("/"))
11022
+ return "not_a_command";
11023
+ const [cmd, ...rest] = trimmed.slice(1).split(/\s+/);
11024
+ const hasLocal = rest.includes("--local");
11025
+ const filteredRest = rest.filter((r) => r !== "--local");
11026
+ const arg = filteredRest.join(" ").trim();
11027
+ switch (cmd) {
11028
+ case "help":
11029
+ case "h":
11030
+ case "?":
11031
+ renderSlashHelp();
11032
+ return "handled";
11033
+ case "quit":
11034
+ case "exit":
11035
+ case "q":
11036
+ return "exit";
11037
+ case "clear":
11038
+ case "cls":
11039
+ ctx.clearScreen();
11040
+ return "handled";
11041
+ case "verbose":
11042
+ case "v":
11043
+ ctx.setVerbose(!ctx.config.verbose);
11044
+ if (hasLocal) {
11045
+ ctx.saveLocalSettings({ verbose: ctx.config.verbose });
11046
+ renderInfo(`Verbose mode: ${ctx.config.verbose ? "on" : "off"} (project-local)`);
11047
+ } else {
11048
+ ctx.saveSettings({ verbose: ctx.config.verbose });
11049
+ renderInfo(`Verbose mode: ${ctx.config.verbose ? "on" : "off"}`);
11141
11050
  }
11142
- } else {
11143
- throw err;
11051
+ return "handled";
11052
+ case "config":
11053
+ case "cfg":
11054
+ renderConfig({
11055
+ model: ctx.config.model,
11056
+ backendType: ctx.config.backendType,
11057
+ backendUrl: ctx.config.backendUrl,
11058
+ timeoutMs: String(ctx.config.timeoutMs),
11059
+ maxRetries: String(ctx.config.maxRetries),
11060
+ verbose: String(ctx.config.verbose),
11061
+ dryRun: String(ctx.config.dryRun)
11062
+ });
11063
+ return "handled";
11064
+ case "model":
11065
+ if (arg) {
11066
+ await switchModel(arg, ctx, hasLocal);
11067
+ } else {
11068
+ await showModelPicker(ctx);
11069
+ }
11070
+ return "handled";
11071
+ case "models":
11072
+ await listModels(ctx);
11073
+ return "handled";
11074
+ case "endpoint":
11075
+ case "ep":
11076
+ await handleEndpoint(arg, ctx, hasLocal);
11077
+ return "handled";
11078
+ case "update":
11079
+ case "upgrade":
11080
+ await handleUpdate(arg, ctx.repoRoot);
11081
+ return "handled";
11082
+ case "voice": {
11083
+ const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
11084
+ if (arg) {
11085
+ const msg = await ctx.voiceSetModel(arg);
11086
+ save({ voice: true, voiceModel: arg });
11087
+ renderInfo(msg + (hasLocal ? " (project-local)" : ""));
11088
+ } else {
11089
+ const msg = await ctx.voiceToggle();
11090
+ const isOn = msg.toLowerCase().includes("enabled") || msg.toLowerCase().includes("on");
11091
+ save({ voice: isOn });
11092
+ renderInfo(msg + (hasLocal ? " (project-local)" : ""));
11093
+ }
11094
+ return "handled";
11144
11095
  }
11145
- }
11146
- }
11147
- async function runSetupWizard(config) {
11148
- const rl = readline.createInterface({
11149
- input: process.stdin,
11150
- output: process.stdout,
11151
- terminal: true
11152
- });
11153
- try {
11154
- return await doSetup(config, rl);
11155
- } finally {
11156
- rl.close();
11157
- }
11158
- }
11159
- async function doSetup(config, rl) {
11160
- process.stdout.write(`
11161
- ${c2.bold(c2.cyan("open-agents"))}
11096
+ case "stream": {
11097
+ const isOn = ctx.streamToggle();
11098
+ const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
11099
+ save({ stream: isOn });
11100
+ renderInfo(`Token streaming: ${isOn ? "on" : "off"}${hasLocal ? " (project-local)" : ""}` + (isOn ? " \u2014 thinking tokens in grey italics, responses with pastel syntax highlighting" : ""));
11101
+ return "handled";
11102
+ }
11103
+ case "tools": {
11104
+ const tools = listCustomToolFiles(ctx.repoRoot);
11105
+ if (tools.length === 0) {
11106
+ renderInfo("No custom tools installed.");
11107
+ renderInfo("The agent will automatically create tools when it detects repeated workflows (3+ times).");
11108
+ renderInfo('Or ask the agent: "create a tool for [workflow]"');
11109
+ } else {
11110
+ process.stdout.write(`
11111
+ ${c2.bold("Custom Tools:")}
11112
+
11162
11113
  `);
11163
- process.stdout.write(` ${c2.dim("\u2500".repeat(60))}
11114
+ for (const t of tools) {
11115
+ process.stdout.write(` ${c2.cyan(t.name.padEnd(28))} ${c2.dim(`(${t.scope}, v${t.version}, ${t.stepsCount} steps)`)}
11164
11116
  `);
11165
- process.stdout.write(` ${c2.bold("First-run setup")}
11166
-
11117
+ process.stdout.write(` ${"".padEnd(28)} ${t.description}
11167
11118
  `);
11168
- process.stdout.write(` ${c2.cyan("\u25CF")} Detecting system specs...
11119
+ }
11120
+ process.stdout.write("\n");
11121
+ }
11122
+ return "handled";
11123
+ }
11124
+ case "skills":
11125
+ case "skill": {
11126
+ const skills = discoverSkills(ctx.repoRoot);
11127
+ if (skills.length === 0) {
11128
+ renderInfo("No skills found.");
11129
+ renderInfo("Install AIWG to get skills: npm i -g aiwg && aiwg use sdlc");
11130
+ renderInfo("Or add skills manually to .oa/skills/{name}/SKILL.md");
11131
+ } else {
11132
+ let filtered = skills;
11133
+ if (arg) {
11134
+ const q = arg.toLowerCase();
11135
+ filtered = skills.filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.triggers.some((t) => t.toLowerCase().includes(q)));
11136
+ }
11137
+ if (filtered.length === 0) {
11138
+ renderWarning(`No skills matching "${arg}". Showing all ${skills.length} skills:`);
11139
+ filtered = skills;
11140
+ }
11141
+ const bySource = /* @__PURE__ */ new Map();
11142
+ for (const s of filtered) {
11143
+ const group = bySource.get(s.source) ?? [];
11144
+ group.push(s);
11145
+ bySource.set(s.source, group);
11146
+ }
11147
+ process.stdout.write(`
11148
+ ${c2.bold(`Available Skills (${filtered.length}):`)}
11169
11149
  `);
11170
- const specs = detectSystemSpecs();
11171
- process.stdout.write(` ${c2.dim(" RAM:")} ${specs.totalRamGB.toFixed(1)} GB total, ${specs.availableRamGB.toFixed(1)} GB available
11150
+ for (const [source, group] of bySource) {
11151
+ process.stdout.write(`
11152
+ ${c2.dim(`\u2500\u2500 ${source} (${group.length}) \u2500\u2500`)}
11172
11153
  `);
11173
- if (specs.gpuVramGB > 0) {
11174
- process.stdout.write(` ${c2.dim(" GPU:")} ${specs.gpuName || "NVIDIA"} \u2014 ${specs.gpuVramGB.toFixed(1)} GB VRAM
11154
+ for (const s of group) {
11155
+ process.stdout.write(` ${c2.cyan(s.name.padEnd(32))} ${s.description.slice(0, 60)}
11175
11156
  `);
11176
- } else {
11177
- process.stdout.write(` ${c2.dim(" GPU:")} No NVIDIA GPU detected (CPU inference)
11157
+ if (s.triggers.length > 0) {
11158
+ process.stdout.write(` ${"".padEnd(32)} ${c2.dim(`triggers: ${s.triggers.slice(0, 3).join(" | ")}`)}
11178
11159
  `);
11160
+ }
11161
+ }
11162
+ }
11163
+ process.stdout.write("\n");
11164
+ renderInfo('Invoke directly: /<skill-name> [args] (e.g. /ralph "fix tests" --completion "npm test passes")');
11165
+ renderInfo("Filter with: /skills <keyword>");
11166
+ }
11167
+ return "handled";
11168
+ }
11169
+ case "dream": {
11170
+ if (arg === "stop" || arg === "wake") {
11171
+ if (ctx.isDreaming?.()) {
11172
+ ctx.dreamStop?.();
11173
+ renderInfo("Waking up from dream mode...");
11174
+ } else {
11175
+ renderWarning("Not currently dreaming.");
11176
+ }
11177
+ } else if (ctx.isDreaming?.()) {
11178
+ renderWarning("Already dreaming. Use /dream stop to wake up first.");
11179
+ } else {
11180
+ const mode = arg === "lucid" ? "lucid" : arg === "deep" ? "deep" : "default";
11181
+ ctx.dreamStart?.(mode);
11182
+ }
11183
+ return "handled";
11184
+ }
11185
+ case "listen":
11186
+ case "mic": {
11187
+ if (!ctx.listenToggle) {
11188
+ renderWarning("Listen mode not available in this context.");
11189
+ return "handled";
11190
+ }
11191
+ if (arg === "stop" || arg === "off") {
11192
+ const msg2 = await (ctx.listenStop?.() ?? Promise.resolve("Not listening."));
11193
+ renderInfo(msg2);
11194
+ return "handled";
11195
+ }
11196
+ if (arg === "confirm") {
11197
+ const msg2 = ctx.listenSetMode?.("confirm") ?? "Confirm mode set.";
11198
+ renderInfo(msg2);
11199
+ return "handled";
11200
+ }
11201
+ if (arg === "auto") {
11202
+ const msg2 = ctx.listenSetMode?.("auto") ?? "Auto mode set.";
11203
+ renderInfo(msg2);
11204
+ return "handled";
11205
+ }
11206
+ const modelSizes = ["tiny", "base", "small", "medium", "large", "large-v3"];
11207
+ if (arg && modelSizes.includes(arg.toLowerCase())) {
11208
+ const model = arg.toLowerCase() === "large" ? "large-v3" : arg.toLowerCase();
11209
+ const msg2 = await (ctx.listenSetModel?.(model) ?? Promise.resolve(`Model set to ${model}.`));
11210
+ renderInfo(msg2);
11211
+ return "handled";
11212
+ }
11213
+ const msg = await ctx.listenToggle();
11214
+ renderInfo(msg);
11215
+ return "handled";
11216
+ }
11217
+ case "bruteforce":
11218
+ case "brute": {
11219
+ const isOn = ctx.bruteForceToggle();
11220
+ const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
11221
+ save({ bruteforce: isOn });
11222
+ renderInfo(`Brute-force mode: ${isOn ? "on" : "off"}${hasLocal ? " (project-local)" : ""}` + (isOn ? " \u2014 agent will auto re-engage when turn limit is hit, reassess and try creative strategies" : ""));
11223
+ return "handled";
11224
+ }
11225
+ case "emojis":
11226
+ case "emoji": {
11227
+ const current = ctx.getEmojis?.() ?? true;
11228
+ const next = !current;
11229
+ ctx.setEmojis?.(next);
11230
+ const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
11231
+ save({ emojis: next });
11232
+ renderInfo(`Emojis ${next ? "enabled" : "disabled"}.`);
11233
+ return "handled";
11234
+ }
11235
+ case "colors":
11236
+ case "color": {
11237
+ const current = ctx.getColors?.() ?? true;
11238
+ const next = !current;
11239
+ ctx.setColors?.(next);
11240
+ const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
11241
+ save({ colors: next });
11242
+ renderInfo(`Colors ${next ? "enabled" : "disabled"}.`);
11243
+ return "handled";
11244
+ }
11245
+ default: {
11246
+ const skills = discoverSkills(ctx.repoRoot);
11247
+ const skill = skills.find((s) => s.name === cmd || s.name === cmd.replace(/_/g, "-"));
11248
+ if (skill) {
11249
+ const content = loadSkillContent(skill.filePath);
11250
+ if (content) {
11251
+ renderInfo(`Loading skill: ${c2.bold(skill.name)} (${skill.source})`);
11252
+ return { type: "skill", name: skill.name, content, args: arg };
11253
+ }
11254
+ }
11255
+ renderWarning(`Unknown command: /${cmd}. Type /help for available commands.`);
11256
+ return "handled";
11257
+ }
11179
11258
  }
11180
- process.stdout.write("\n");
11181
- let models = [];
11259
+ }
11260
+ async function listModels(ctx) {
11182
11261
  try {
11183
- models = await fetchOllamaModels(config.backendUrl);
11184
- } catch {
11185
- renderError(`Cannot reach Ollama at ${config.backendUrl}`);
11186
- renderInfo("Start Ollama with: ollama serve");
11187
- renderInfo("Or use /endpoint to configure a remote backend after startup.");
11188
- const answer = await ask(rl, `
11189
- ${c2.bold("Continue without Ollama?")} (y/n) `);
11190
- if (answer.toLowerCase() !== "y")
11191
- return null;
11192
- return config.model;
11193
- }
11194
- const currentModel = findModel(models, config.model);
11195
- if (currentModel) {
11196
- process.stdout.write(` ${c2.green("\u2714")} Model ${c2.bold(currentModel.name)} is available.
11197
-
11198
- `);
11199
- return currentModel.name;
11262
+ const models = await fetchOllamaModels(ctx.config.backendUrl);
11263
+ renderModelList(models.map((m) => ({ name: m.name, size: m.size, modified: m.modified })), ctx.config.model);
11264
+ } catch (err) {
11265
+ renderError(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
11200
11266
  }
11201
- process.stdout.write(` ${c2.yellow("\u26A0")} Default model ${c2.bold(config.model)} is not available.
11202
-
11203
- `);
11204
- const toolCallingModels = models.filter((m) => modelSupportsToolCalling(m.name));
11205
- if (toolCallingModels.length > 0) {
11206
- process.stdout.write(` ${c2.cyan("\u25CF")} Found ${toolCallingModels.length} model(s) with tool-calling support:
11207
-
11208
- `);
11209
- for (let i = 0; i < Math.min(toolCallingModels.length, 10); i++) {
11210
- const m = toolCallingModels[i];
11211
- process.stdout.write(` ${c2.bold(String(i + 1))}. ${m.name} ${c2.dim(`(${m.size})`)}
11212
- `);
11213
- }
11214
- process.stdout.write(`
11215
- ${c2.dim("0")}. Pull a new qwen3.5 model instead
11216
- `);
11217
- process.stdout.write("\n");
11218
- const choice = await ask(rl, ` ${c2.bold("Select a model")} (1-${Math.min(toolCallingModels.length, 10)}, or 0 to pull new): `);
11219
- const idx = parseInt(choice, 10);
11220
- if (idx > 0 && idx <= toolCallingModels.length) {
11221
- const selected = toolCallingModels[idx - 1];
11222
- setConfigValue("model", selected.name);
11223
- process.stdout.write(`
11224
- ${c2.green("\u2714")} Selected ${c2.bold(selected.name)}. Saved to config.
11225
-
11226
- `);
11227
- return selected.name;
11267
+ }
11268
+ async function showModelPicker(ctx) {
11269
+ try {
11270
+ const models = await fetchOllamaModels(ctx.config.backendUrl);
11271
+ if (models.length === 0) {
11272
+ renderWarning("No models found. Pull a model with: ollama pull <model>");
11273
+ return;
11228
11274
  }
11229
- } else {
11230
- process.stdout.write(` ${c2.yellow("\u26A0")} No tool-calling capable models found on this system.
11231
-
11232
- `);
11275
+ renderModelList(models.map((m) => ({ name: m.name, size: m.size, modified: m.modified })), ctx.config.model);
11276
+ } catch (err) {
11277
+ renderError(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
11233
11278
  }
11234
- const recommended = recommendModel(specs);
11235
- process.stdout.write(` ${c2.cyan("\u25CF")} Recommended model based on your system:
11279
+ }
11280
+ async function handleEndpoint(arg, ctx, local = false) {
11281
+ if (!arg) {
11282
+ process.stdout.write(`
11283
+ ${c2.bold("Current endpoint:")}
11236
11284
 
11237
11285
  `);
11238
- const localVariants = QWEN_VARIANTS.filter((v) => !v.cloud);
11239
- for (let i = 0; i < localVariants.length; i++) {
11240
- const v = localVariants[i];
11241
- const fits = v.sizeGB <= Math.max(specs.gpuVramGB, specs.availableRamGB) * 0.8;
11242
- const isRec = v.tag === recommended.tag;
11243
- const marker = isRec ? c2.green("\u2192") : fits ? c2.dim(" ") : c2.red("\u2716");
11244
- const name = isRec ? c2.bold(c2.green(v.tag)) : fits ? v.tag : c2.dim(v.tag);
11245
- const label = isRec ? c2.bold(v.label) : c2.dim(v.label);
11246
- const tooLarge = !fits && !v.cloud ? c2.red(" (exceeds available memory)") : "";
11247
- process.stdout.write(` ${marker} ${String(i + 1).padStart(2)}. ${name.padEnd(isRec ? 45 : 25)} ${label}${tooLarge}
11248
- `);
11249
- }
11250
- process.stdout.write(`
11251
- ${c2.dim(" ")} ${String(localVariants.length + 1).padStart(2)}. ${c2.dim("qwen3.5:cloud")} ${c2.dim("Ollama Cloud")}
11252
- `);
11253
- process.stdout.write(` ${c2.dim(" ")} ${String(localVariants.length + 2).padStart(2)}. ${c2.dim("qwen3.5:397b-cloud")} ${c2.dim("397B Ollama Cloud")}
11254
- `);
11255
- process.stdout.write("\n");
11256
- const pullChoice = await ask(rl, ` ${c2.bold("Select a model to pull")} (1-${localVariants.length + 2}, or Enter for recommended): `);
11257
- const pullIdx = pullChoice ? parseInt(pullChoice, 10) : 0;
11258
- let selectedVariant;
11259
- if (pullIdx === 0 || isNaN(pullIdx)) {
11260
- selectedVariant = recommended;
11261
- } else if (pullIdx <= localVariants.length) {
11262
- selectedVariant = localVariants[pullIdx - 1];
11263
- } else if (pullIdx === localVariants.length + 1) {
11264
- selectedVariant = QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:cloud");
11265
- } else {
11266
- selectedVariant = QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:397b-cloud");
11267
- }
11268
- const confirmPull = await ask(rl, `
11269
- Pull ${c2.bold(selectedVariant.tag)} (${selectedVariant.label})? (Y/n) `);
11270
- if (confirmPull.toLowerCase() === "n") {
11286
+ process.stdout.write(` ${c2.cyan("URL".padEnd(12))} ${ctx.config.backendUrl}
11287
+ `);
11288
+ process.stdout.write(` ${c2.cyan("Type".padEnd(12))} ${ctx.config.backendType}
11289
+ `);
11290
+ process.stdout.write(` ${c2.cyan("Auth".padEnd(12))} ${ctx.config.apiKey ? "Bearer token set" : "none"}
11291
+ `);
11271
11292
  process.stdout.write(`
11272
- ${c2.dim("Skipping model pull. You can pull manually with: ollama pull <model>")}
11293
+ ${c2.dim("Usage: /endpoint <url> [--auth <token>]")}
11294
+ `);
11295
+ process.stdout.write(` ${c2.dim(" /endpoint http://localhost:11434 (Ollama, no auth)")}
11296
+ `);
11297
+ process.stdout.write(` ${c2.dim(" /endpoint http://remote:8000/v1 --auth sk-... (OpenAI-compatible)")}
11298
+ `);
11299
+ process.stdout.write(` ${c2.dim(" /endpoint http://remote:8000/v1 (OpenAI-compatible, no auth)")}
11273
11300
 
11274
11301
  `);
11275
- return config.model;
11302
+ return;
11303
+ }
11304
+ const parts = arg.split(/\s+/);
11305
+ const url = parts[0];
11306
+ let apiKey;
11307
+ const authIdx = parts.indexOf("--auth");
11308
+ if (authIdx !== -1 && parts[authIdx + 1]) {
11309
+ apiKey = parts[authIdx + 1];
11310
+ }
11311
+ try {
11312
+ new URL(url);
11313
+ } catch {
11314
+ renderError(`Invalid URL: "${url}"`);
11315
+ return;
11316
+ }
11317
+ let backendType = "ollama";
11318
+ if (url.includes("/v1") || url.includes(":8000") || apiKey) {
11319
+ backendType = "vllm";
11276
11320
  }
11277
11321
  process.stdout.write(`
11278
- ${c2.cyan("\u25CF")} Pulling ${c2.bold(selectedVariant.tag)}... (this may take a while)
11279
- `);
11322
+ ${c2.dim("Testing connection...")} `);
11280
11323
  try {
11281
- pullModelWithAutoUpdate(selectedVariant.tag);
11282
- process.stdout.write(`
11283
- ${c2.green("\u2714")} Model ${c2.bold(selectedVariant.tag)} pulled successfully.
11284
-
11324
+ const healthUrl = backendType === "ollama" ? `${url.replace(/\/$/, "")}/api/tags` : `${url.replace(/\/$/, "")}/models`;
11325
+ const headers = {};
11326
+ if (apiKey)
11327
+ headers["Authorization"] = `Bearer ${apiKey}`;
11328
+ const resp = await fetch(healthUrl, {
11329
+ headers,
11330
+ signal: AbortSignal.timeout(1e4)
11331
+ });
11332
+ if (!resp.ok)
11333
+ throw new Error(`HTTP ${resp.status}`);
11334
+ process.stdout.write(`${c2.green("\u2714")} Connected
11285
11335
  `);
11286
11336
  } catch (err) {
11287
- renderError(`Failed to pull model: ${err instanceof Error ? err.message : String(err)}`);
11288
- renderInfo("Try manually: ollama pull " + selectedVariant.tag);
11289
- return config.model;
11337
+ process.stdout.write(`${c2.yellow("\u26A0")} Could not verify
11338
+ `);
11339
+ renderWarning(`Endpoint may not be reachable: ${err instanceof Error ? err.message : String(err)}`);
11340
+ renderInfo("Setting endpoint anyway \u2014 it may come online later.");
11290
11341
  }
11291
- if (!selectedVariant.cloud) {
11292
- const ctx = calculateContextWindow(specs, selectedVariant.sizeGB);
11293
- const customName = `open-agents-${selectedVariant.tag.replace(":", "-").replace(".", "")}`;
11294
- process.stdout.write(` ${c2.cyan("\u25CF")} Context window recommendation: ${c2.bold(ctx.label)} (${ctx.numCtx} tokens)
11342
+ ctx.setEndpoint(url, backendType, apiKey);
11343
+ const endpointSettings = { backendUrl: url, backendType, ...apiKey ? { apiKey } : {} };
11344
+ if (local) {
11345
+ ctx.saveLocalSettings(endpointSettings);
11346
+ } else {
11347
+ setConfigValue("backendUrl", url);
11348
+ setConfigValue("backendType", backendType);
11349
+ if (apiKey) {
11350
+ setConfigValue("apiKey", apiKey);
11351
+ }
11352
+ ctx.saveSettings(endpointSettings);
11353
+ }
11354
+ process.stdout.write(`
11355
+ ${c2.green("\u2714")} Endpoint updated and saved${local ? " (project-local)" : ""}:
11295
11356
  `);
11296
- process.stdout.write(` ${c2.dim(`Based on ${specs.totalRamGB.toFixed(0)} GB RAM, ${selectedVariant.sizeGB} GB model`)}
11297
-
11357
+ process.stdout.write(` ${c2.cyan("URL".padEnd(8))} ${url}
11298
11358
  `);
11299
- const createModelfile = await ask(rl, ` Create optimized model "${c2.bold(customName)}" with ${ctx.label} context? (Y/n) `);
11300
- if (createModelfile.toLowerCase() !== "n") {
11301
- try {
11302
- const modelfileContent = [
11303
- `FROM ${selectedVariant.tag}`,
11304
- `PARAMETER num_ctx ${ctx.numCtx}`,
11305
- `PARAMETER temperature 0`,
11306
- `PARAMETER num_predict 16384`,
11307
- `PARAMETER stop "<|endoftext|>"`
11308
- ].join("\n");
11309
- const modelDir2 = join18(homedir8(), ".open-agents", "models");
11310
- mkdirSync7(modelDir2, { recursive: true });
11311
- const modelfilePath = join18(modelDir2, `Modelfile.${customName}`);
11312
- writeFileSync7(modelfilePath, modelfileContent + "\n", "utf8");
11313
- process.stdout.write(` ${c2.dim("Creating model...")} `);
11314
- execSync10(`ollama create ${customName} -f ${modelfilePath}`, {
11315
- stdio: "pipe",
11316
- timeout: 12e4
11317
- });
11318
- process.stdout.write(`${c2.green("\u2714")}
11359
+ process.stdout.write(` ${c2.cyan("Type".padEnd(8))} ${backendType}
11319
11360
  `);
11320
- setConfigValue("model", customName);
11321
- process.stdout.write(`
11322
- ${c2.green("\u2714")} Model ${c2.bold(customName)} created with ${ctx.label} context.
11361
+ if (apiKey) {
11362
+ process.stdout.write(` ${c2.cyan("Auth".padEnd(8))} Bearer ${apiKey.slice(0, 8)}...
11323
11363
  `);
11324
- process.stdout.write(` ${c2.green("\u2714")} Saved as default model in config.
11325
-
11364
+ } else {
11365
+ process.stdout.write(` ${c2.cyan("Auth".padEnd(8))} none
11326
11366
  `);
11327
- return customName;
11328
- } catch (err) {
11329
- renderWarning(`Could not create custom model: ${err instanceof Error ? err.message : String(err)}`);
11330
- renderInfo(`Using base model ${selectedVariant.tag} instead.`);
11367
+ }
11368
+ process.stdout.write("\n");
11369
+ }
11370
+ async function handleUpdate(subcommand, repoRoot) {
11371
+ if (subcommand === "auto") {
11372
+ const settings = { updateMode: "auto" };
11373
+ saveProjectSettings(repoRoot, settings);
11374
+ saveGlobalSettings(settings);
11375
+ renderInfo("Update mode: auto \u2014 updates will install automatically after task completion.");
11376
+ return;
11377
+ }
11378
+ if (subcommand === "manual") {
11379
+ const settings = { updateMode: "manual" };
11380
+ saveProjectSettings(repoRoot, settings);
11381
+ saveGlobalSettings(settings);
11382
+ renderInfo("Update mode: manual \u2014 updates only install when you run /update.");
11383
+ return;
11384
+ }
11385
+ let currentVersion = "0.0.0";
11386
+ try {
11387
+ const { createRequire: createRequire4 } = await import("node:module");
11388
+ const { fileURLToPath: fileURLToPath3 } = await import("node:url");
11389
+ const { dirname: dirname5, join: join28 } = await import("node:path");
11390
+ const { existsSync: existsSync19 } = await import("node:fs");
11391
+ const req = createRequire4(import.meta.url);
11392
+ const thisDir = dirname5(fileURLToPath3(import.meta.url));
11393
+ const candidates = [
11394
+ join28(thisDir, "..", "package.json"),
11395
+ join28(thisDir, "..", "..", "package.json"),
11396
+ join28(thisDir, "..", "..", "..", "package.json")
11397
+ ];
11398
+ for (const pkgPath of candidates) {
11399
+ if (existsSync19(pkgPath)) {
11400
+ const pkg = req(pkgPath);
11401
+ if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli") {
11402
+ currentVersion = pkg.version ?? "0.0.0";
11403
+ break;
11404
+ }
11331
11405
  }
11332
11406
  }
11333
- setConfigValue("model", selectedVariant.tag);
11334
- process.stdout.write(`
11335
- ${c2.green("\u2714")} Saved ${c2.bold(selectedVariant.tag)} as default model.
11336
-
11337
- `);
11338
- return selectedVariant.tag;
11407
+ } catch {
11339
11408
  }
11340
- setConfigValue("model", selectedVariant.tag);
11341
11409
  process.stdout.write(`
11342
- ${c2.green("\u2714")} Saved ${c2.bold(selectedVariant.tag)} as default model.
11410
+ ${c2.cyan("\u25CF")} Checking for updates... ${c2.dim(`(current: v${currentVersion})`)}
11411
+ `);
11412
+ const info = await checkForUpdate(currentVersion, true);
11413
+ if (!info) {
11414
+ process.stdout.write(` ${c2.green("\u2714")} You're on the latest version (v${currentVersion}).
11343
11415
 
11344
11416
  `);
11345
- return selectedVariant.tag;
11346
- }
11347
- async function isModelAvailable(config) {
11348
- try {
11349
- const models = await fetchOllamaModels(config.backendUrl);
11350
- return !!findModel(models, config.model);
11351
- } catch {
11352
- return false;
11417
+ return;
11353
11418
  }
11419
+ process.stdout.write(` ${c2.yellow("\u26A0")} Update available: v${info.currentVersion} \u2192 v${c2.bold(c2.green(info.latestVersion))}
11420
+ `);
11421
+ process.stdout.write(` ${c2.cyan("\u25CF")} Installing in background...
11422
+
11423
+ `);
11424
+ const { exec } = await import("node:child_process");
11425
+ exec(`npm cache clean --force open-agents-ai 2>/dev/null; npm install -g open-agents-ai@latest --force`, { timeout: 18e4 }, (err) => {
11426
+ if (err) {
11427
+ renderWarning("Update install failed. Try manually: npm i -g open-agents-ai");
11428
+ } else {
11429
+ renderInfo(`${c2.green("\u2714")} Updated to v${info.latestVersion}. Takes effect next session.`);
11430
+ }
11431
+ });
11354
11432
  }
11355
- function isFirstRun() {
11433
+ async function switchModel(query, ctx, local = false) {
11356
11434
  try {
11357
- return !existsSync13(join18(homedir8(), ".open-agents", "config.json"));
11358
- } catch {
11359
- return true;
11435
+ const models = await fetchOllamaModels(ctx.config.backendUrl);
11436
+ const match = findModel(models, query);
11437
+ if (!match) {
11438
+ renderError(`Model not found: "${query}"`);
11439
+ renderInfo("Available models:");
11440
+ for (const m of models.slice(0, 10)) {
11441
+ renderInfo(` ${m.name}`);
11442
+ }
11443
+ return;
11444
+ }
11445
+ let finalModel = match.name;
11446
+ if (ctx.config.backendType === "ollama") {
11447
+ const result = await ensureExpandedContext(match.name, ctx.config.backendUrl);
11448
+ if (result.created) {
11449
+ renderInfo(`Created expanded context variant: ${c2.bold(result.model)} (${result.contextLabel}, ${result.numCtx} tokens)`);
11450
+ finalModel = result.model;
11451
+ } else if (result.model !== match.name) {
11452
+ renderInfo(`Using expanded context variant: ${c2.bold(result.model)} (${result.contextLabel})`);
11453
+ finalModel = result.model;
11454
+ }
11455
+ }
11456
+ const oldModel = ctx.config.model;
11457
+ ctx.setModel(finalModel);
11458
+ if (local) {
11459
+ ctx.saveLocalSettings({ model: finalModel });
11460
+ } else {
11461
+ ctx.saveSettings({ model: finalModel });
11462
+ }
11463
+ renderModelSwitch(oldModel, finalModel);
11464
+ if (local) {
11465
+ renderInfo("Saved as project-local override.");
11466
+ }
11467
+ } catch (err) {
11468
+ renderError(`Failed to switch model: ${err instanceof Error ? err.message : String(err)}`);
11360
11469
  }
11361
11470
  }
11362
- var QWEN_VARIANTS, TOOL_CALLING_MODELS;
11363
- var init_setup = __esm({
11364
- "packages/cli/dist/tui/setup.js"() {
11471
+ var init_commands = __esm({
11472
+ "packages/cli/dist/tui/commands.js"() {
11365
11473
  "use strict";
11366
11474
  init_model_picker();
11367
11475
  init_render();
11476
+ init_dist2();
11368
11477
  init_config();
11369
- QWEN_VARIANTS = [
11370
- { tag: "qwen3.5:0.8b", sizeGB: 1, label: "0.8B params (1.0 GB)", cloud: false },
11371
- { tag: "qwen3.5:2b", sizeGB: 2.7, label: "2B params (2.7 GB)", cloud: false },
11372
- { tag: "qwen3.5:4b", sizeGB: 3.4, label: "4B params (3.4 GB)", cloud: false },
11373
- { tag: "qwen3.5:9b", sizeGB: 6.6, label: "9B params (6.6 GB) \u2014 recommended minimum", cloud: false },
11374
- { tag: "qwen3.5:27b", sizeGB: 17, label: "27B params (17 GB)", cloud: false },
11375
- { tag: "qwen3.5:35b", sizeGB: 24, label: "35B params (24 GB)", cloud: false },
11376
- { tag: "qwen3.5:122b", sizeGB: 81, label: "122B params (81 GB) \u2014 best local", cloud: false },
11377
- { tag: "qwen3.5:cloud", sizeGB: 0, label: "Cloud (Ollama Cloud)", cloud: true },
11378
- { tag: "qwen3.5:397b-cloud", sizeGB: 0, label: "397B Cloud (Ollama Cloud)", cloud: true }
11379
- ];
11380
- TOOL_CALLING_MODELS = /* @__PURE__ */ new Set([
11381
- "qwen3.5",
11382
- "qwen3",
11383
- "qwen2.5",
11384
- "llama3.3",
11385
- "llama3.1",
11386
- "mistral",
11387
- "mixtral",
11388
- "command-r",
11389
- "gemma3",
11390
- "devstral",
11391
- "deepseek"
11392
- ]);
11478
+ init_updater();
11479
+ init_oa_directory();
11480
+ init_setup();
11393
11481
  }
11394
11482
  });
11395
11483
 
@@ -14965,6 +15053,19 @@ async function startInteractive(config, repoPath) {
14965
15053
  config = { ...config, model: setupModel };
14966
15054
  }
14967
15055
  }
15056
+ if (config.backendType === "ollama" && !config.model.startsWith("open-agents-")) {
15057
+ try {
15058
+ const expandResult = await ensureExpandedContext(config.model, config.backendUrl);
15059
+ if (expandResult.created) {
15060
+ renderInfo(`Created expanded context model: ${expandResult.model} (${expandResult.contextLabel}, ${expandResult.numCtx} tokens)`);
15061
+ config = { ...config, model: expandResult.model };
15062
+ } else if (expandResult.model !== config.model) {
15063
+ renderInfo(`Using expanded context model: ${expandResult.model} (${expandResult.contextLabel})`);
15064
+ config = { ...config, model: expandResult.model };
15065
+ }
15066
+ } catch {
15067
+ }
15068
+ }
14968
15069
  if (!isResumed) {
14969
15070
  try {
14970
15071
  const healthUrl = config.backendType === "ollama" ? `${config.backendUrl}/api/tags` : `${config.backendUrl}/v1/models`;
@@ -15486,6 +15587,15 @@ async function runWithTUI(task, config, repoPath) {
15486
15587
  }
15487
15588
  config = { ...config, model: setupModel };
15488
15589
  }
15590
+ if (config.backendType === "ollama" && !config.model.startsWith("open-agents-")) {
15591
+ try {
15592
+ const expandResult = await ensureExpandedContext(config.model, config.backendUrl);
15593
+ if (expandResult.model !== config.model) {
15594
+ config = { ...config, model: expandResult.model };
15595
+ }
15596
+ } catch {
15597
+ }
15598
+ }
15489
15599
  try {
15490
15600
  const healthUrl = config.backendType === "ollama" ? `${config.backendUrl}/api/tags` : `${config.backendUrl}/v1/models`;
15491
15601
  const resp = await fetch(healthUrl, { signal: AbortSignal.timeout(1e4) });