open-agents-ai 0.184.66 → 0.184.68

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 +2187 -2195
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -11893,8 +11893,8 @@ async function loadTranscribeCli() {
11893
11893
  }).trim();
11894
11894
  const tcPath = join19(globalRoot, "transcribe-cli");
11895
11895
  if (existsSync16(join19(tcPath, "dist", "index.js"))) {
11896
- const { createRequire: createRequire5 } = await import("node:module");
11897
- const req = createRequire5(import.meta.url);
11896
+ const { createRequire: createRequire6 } = await import("node:module");
11897
+ const req = createRequire6(import.meta.url);
11898
11898
  _tcModule = req(join19(tcPath, "dist", "index.js"));
11899
11899
  return _tcModule;
11900
11900
  }
@@ -11907,8 +11907,8 @@ async function loadTranscribeCli() {
11907
11907
  for (const ver of readdirSync23(nvmBase)) {
11908
11908
  const tcPath = join19(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
11909
11909
  if (existsSync16(join19(tcPath, "dist", "index.js"))) {
11910
- const { createRequire: createRequire5 } = await import("node:module");
11911
- const req = createRequire5(import.meta.url);
11910
+ const { createRequire: createRequire6 } = await import("node:module");
11911
+ const req = createRequire6(import.meta.url);
11912
11912
  _tcModule = req(join19(tcPath, "dist", "index.js"));
11913
11913
  return _tcModule;
11914
11914
  }
@@ -30101,8 +30101,8 @@ var init_listen = __esm({
30101
30101
  /** Load transcribe-cli — bundled as a dependency of open-agents-ai. */
30102
30102
  async loadTranscribeCli() {
30103
30103
  try {
30104
- const { createRequire: createRequire5 } = await import("node:module");
30105
- const req = createRequire5(import.meta.url);
30104
+ const { createRequire: createRequire6 } = await import("node:module");
30105
+ const req = createRequire6(import.meta.url);
30106
30106
  return req("transcribe-cli");
30107
30107
  } catch {
30108
30108
  }
@@ -30114,8 +30114,8 @@ var init_listen = __esm({
30114
30114
  }).trim();
30115
30115
  const tcPath = join48(globalRoot, "transcribe-cli");
30116
30116
  if (existsSync32(join48(tcPath, "dist", "index.js"))) {
30117
- const { createRequire: createRequire5 } = await import("node:module");
30118
- const req = createRequire5(import.meta.url);
30117
+ const { createRequire: createRequire6 } = await import("node:module");
30118
+ const req = createRequire6(import.meta.url);
30119
30119
  return req(join48(tcPath, "dist", "index.js"));
30120
30120
  }
30121
30121
  } catch {
@@ -30127,8 +30127,8 @@ var init_listen = __esm({
30127
30127
  for (const ver of readdirSync23(nvmBase)) {
30128
30128
  const tcPath = join48(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
30129
30129
  if (existsSync32(join48(tcPath, "dist", "index.js"))) {
30130
- const { createRequire: createRequire5 } = await import("node:module");
30131
- const req = createRequire5(import.meta.url);
30130
+ const { createRequire: createRequire6 } = await import("node:module");
30131
+ const req = createRequire6(import.meta.url);
30132
30132
  return req(join48(tcPath, "dist", "index.js"));
30133
30133
  }
30134
30134
  }
@@ -39665,2422 +39665,2397 @@ var init_oa_directory = __esm({
39665
39665
  }
39666
39666
  });
39667
39667
 
39668
- // packages/cli/dist/tui/setup.js
39669
- import * as readline from "node:readline";
39670
- import { execSync as execSync27, spawn as spawn19, exec as exec2 } from "node:child_process";
39671
- import { promisify as promisify6 } from "node:util";
39672
- import { existsSync as existsSync37, writeFileSync as writeFileSync16, readFileSync as readFileSync28, appendFileSync as appendFileSync2, mkdirSync as mkdirSync15 } from "node:fs";
39673
- import { join as join54 } from "node:path";
39674
- import { homedir as homedir13, platform as platform2 } from "node:os";
39675
- async function checkToolSupport(modelName, backendUrl = "http://localhost:11434") {
39676
- if (_toolSupportCache.has(modelName))
39677
- return _toolSupportCache.get(modelName);
39678
- try {
39679
- const resp = await fetch(`${backendUrl.replace(/\/+$/, "")}/api/chat`, {
39680
- method: "POST",
39681
- headers: { "Content-Type": "application/json" },
39682
- body: JSON.stringify({
39683
- model: modelName,
39684
- messages: [{ role: "user", content: "test" }],
39685
- tools: [{ type: "function", function: { name: "test", description: "test", parameters: { type: "object", properties: {} } } }],
39686
- stream: false,
39687
- options: { num_predict: 1 }
39688
- }),
39689
- signal: AbortSignal.timeout(1e4)
39690
- });
39691
- const hasTools = resp.ok;
39692
- _toolSupportCache.set(modelName, hasTools);
39693
- return hasTools;
39694
- } catch {
39695
- _toolSupportCache.set(modelName, true);
39696
- return true;
39668
+ // packages/cli/dist/tui/overlay-lock.js
39669
+ var overlay_lock_exports = {};
39670
+ __export(overlay_lock_exports, {
39671
+ bufferIfOverlay: () => bufferIfOverlay,
39672
+ enterOverlay: () => enterOverlay,
39673
+ isOverlayActive: () => isOverlayActive,
39674
+ leaveOverlay: () => leaveOverlay,
39675
+ onOverlayLeave: () => onOverlayLeave,
39676
+ overlayWrite: () => overlayWrite
39677
+ });
39678
+ function onOverlayLeave(cb) {
39679
+ _onLeaveCallback = cb;
39680
+ }
39681
+ function isOverlayActive() {
39682
+ return _overlayActive;
39683
+ }
39684
+ function overlayWrite(data) {
39685
+ if (_origStdoutWrite) {
39686
+ return _origStdoutWrite.call(process.stdout, data);
39697
39687
  }
39688
+ return process.stdout.write(data);
39698
39689
  }
39699
- function detectSystemSpecs() {
39700
- let totalRamGB = 0;
39701
- let availableRamGB = 0;
39702
- let gpuVramGB = 0;
39703
- let gpuName = "";
39704
- try {
39705
- const memInfo = execSync27("free -b 2>/dev/null || sysctl -n hw.memsize 2>/dev/null", {
39706
- encoding: "utf8",
39707
- timeout: 5e3
39708
- });
39709
- if (memInfo.includes("Mem:")) {
39710
- const match = memInfo.match(/^Mem:\s+(\d+)\s+\d+\s+\d+\s+\d+\s+\d+\s+(\d+)/m);
39711
- if (match) {
39712
- totalRamGB = parseInt(match[1], 10) / 1024 ** 3;
39713
- availableRamGB = parseInt(match[2], 10) / 1024 ** 3;
39714
- }
39715
- } else {
39716
- const bytes = parseInt(memInfo.trim(), 10);
39717
- if (!isNaN(bytes)) {
39718
- totalRamGB = bytes / 1024 ** 3;
39719
- availableRamGB = totalRamGB * 0.7;
39690
+ function bufferIfOverlay(stream, data) {
39691
+ if (!_overlayActive)
39692
+ return false;
39693
+ _buffer.push({ stream, data });
39694
+ return true;
39695
+ }
39696
+ function enterOverlay() {
39697
+ _depth++;
39698
+ if (_depth === 1) {
39699
+ _overlayActive = true;
39700
+ _origStdoutWrite = process.stdout.write.bind(process.stdout);
39701
+ _origStderrWrite = process.stderr.write.bind(process.stderr);
39702
+ process.stdout.write = function(chunk, ...args) {
39703
+ _buffer.push({ stream: "stdout", data: chunk });
39704
+ const cb = typeof args[args.length - 1] === "function" ? args[args.length - 1] : void 0;
39705
+ if (cb)
39706
+ cb();
39707
+ return true;
39708
+ };
39709
+ process.stderr.write = function(chunk, ...args) {
39710
+ _buffer.push({ stream: "stderr", data: chunk });
39711
+ const cb = typeof args[args.length - 1] === "function" ? args[args.length - 1] : void 0;
39712
+ if (cb)
39713
+ cb();
39714
+ return true;
39715
+ };
39716
+ }
39717
+ }
39718
+ function leaveOverlay() {
39719
+ _depth = Math.max(0, _depth - 1);
39720
+ if (_depth === 0) {
39721
+ _overlayActive = false;
39722
+ if (_origStdoutWrite) {
39723
+ process.stdout.write = _origStdoutWrite;
39724
+ _origStdoutWrite = null;
39725
+ }
39726
+ if (_origStderrWrite) {
39727
+ process.stderr.write = _origStderrWrite;
39728
+ _origStderrWrite = null;
39729
+ }
39730
+ if (_onLeaveCallback) {
39731
+ try {
39732
+ _onLeaveCallback();
39733
+ } catch {
39720
39734
  }
39721
39735
  }
39722
- } catch {
39723
- }
39724
- try {
39725
- const nvidiaSmi = execSync27("nvidia-smi --query-gpu=memory.total,name --format=csv,noheader,nounits 2>/dev/null", { encoding: "utf8", timeout: 5e3 });
39726
- const lines = nvidiaSmi.trim().split("\n");
39727
- if (lines.length > 0) {
39728
- for (const line of lines) {
39729
- const parts = line.split(",").map((s) => s.trim());
39730
- const vramMB = parseInt(parts[0] ?? "0", 10);
39731
- if (!isNaN(vramMB))
39732
- gpuVramGB += vramMB / 1024;
39733
- if (!gpuName && parts[1])
39734
- gpuName = parts[1];
39736
+ const pending = _buffer;
39737
+ _buffer = [];
39738
+ for (const { stream, data } of pending) {
39739
+ if (stream === "stdout") {
39740
+ process.stdout.write(data);
39741
+ } else {
39742
+ process.stderr.write(data);
39735
39743
  }
39736
39744
  }
39737
- } catch {
39738
39745
  }
39739
- return {
39740
- totalRamGB: Math.round(totalRamGB * 10) / 10,
39741
- availableRamGB: Math.round(availableRamGB * 10) / 10,
39742
- gpuVramGB: Math.round(gpuVramGB * 10) / 10,
39743
- gpuName
39744
- };
39745
39746
  }
39746
- async function detectSystemSpecsAsync() {
39747
- let totalRamGB = 0;
39748
- let availableRamGB = 0;
39749
- let gpuVramGB = 0;
39750
- let gpuName = "";
39751
- try {
39752
- const { stdout: memInfo } = await execAsync("free -b 2>/dev/null || sysctl -n hw.memsize 2>/dev/null", { timeout: 5e3 });
39753
- if (memInfo.includes("Mem:")) {
39754
- const match = memInfo.match(/^Mem:\s+(\d+)\s+\d+\s+\d+\s+\d+\s+\d+\s+(\d+)/m);
39755
- if (match) {
39756
- totalRamGB = parseInt(match[1], 10) / 1024 ** 3;
39757
- availableRamGB = parseInt(match[2], 10) / 1024 ** 3;
39758
- }
39747
+ var _overlayActive, _depth, _buffer, _origStdoutWrite, _origStderrWrite, _onLeaveCallback;
39748
+ var init_overlay_lock = __esm({
39749
+ "packages/cli/dist/tui/overlay-lock.js"() {
39750
+ "use strict";
39751
+ _overlayActive = false;
39752
+ _depth = 0;
39753
+ _buffer = [];
39754
+ _origStdoutWrite = null;
39755
+ _origStderrWrite = null;
39756
+ _onLeaveCallback = null;
39757
+ }
39758
+ });
39759
+
39760
+ // packages/cli/dist/tui/tui-select.js
39761
+ function ansi3(code, text) {
39762
+ return isTTY3 ? `\x1B[${code}m${text}\x1B[0m` : text;
39763
+ }
39764
+ function fg2562(code, text) {
39765
+ return isTTY3 ? `\x1B[38;5;${code}m${text}\x1B[0m` : text;
39766
+ }
39767
+ function stripAnsi(s) {
39768
+ return s.replace(/\x1B\[[0-9;]*m/g, "");
39769
+ }
39770
+ function defaultRenderRow(item, focused, isActive) {
39771
+ const marker = isActive ? selectColors.green("\u25CF") : focused ? selectColors.orange("\u25CF") : selectColors.dim("\u25CB");
39772
+ const label = focused ? selectColors.orange(selectColors.bold(item.label)) : isActive ? selectColors.green(item.label) : item.label;
39773
+ const detail = item.detail ? ` ${selectColors.dim(item.detail)}` : "";
39774
+ return ` ${marker} ${label}${detail}`;
39775
+ }
39776
+ function matchRow(item, focused, isActive) {
39777
+ if (focused || isActive) {
39778
+ return defaultRenderRow(item, focused, isActive);
39779
+ }
39780
+ const marker = selectColors.matchLight("\u25CB");
39781
+ const label = selectColors.matchLight(stripAnsi(item.label));
39782
+ const detail = item.detail ? ` ${selectColors.dim(stripAnsi(item.detail))}` : "";
39783
+ return ` ${marker} ${label}${detail}`;
39784
+ }
39785
+ function tuiSelect(opts) {
39786
+ const { items, title, rl } = opts;
39787
+ const renderRow = opts.renderRow ?? defaultRenderRow;
39788
+ const activeKey = opts.activeKey ?? null;
39789
+ const skipSet = new Set(opts.skipKeys ?? []);
39790
+ if (items.length === 0) {
39791
+ return Promise.resolve({ confirmed: false, key: null, index: -1 });
39792
+ }
39793
+ const isSkippable = (idx) => skipSet.has(items[idx].key);
39794
+ let filter = "";
39795
+ let matchSet = /* @__PURE__ */ new Set();
39796
+ let deleteConfirmIdx = -1;
39797
+ let deleteConfirmSel = false;
39798
+ function updateFilter() {
39799
+ if (!filter) {
39800
+ matchSet = new Set(items.map((_, i) => i));
39759
39801
  } else {
39760
- const bytes = parseInt(memInfo.trim(), 10);
39761
- if (!isNaN(bytes)) {
39762
- totalRamGB = bytes / 1024 ** 3;
39763
- availableRamGB = totalRamGB * 0.7;
39802
+ const lower = filter.toLowerCase();
39803
+ matchSet = /* @__PURE__ */ new Set();
39804
+ for (let i = 0; i < items.length; i++) {
39805
+ if (isSkippable(i))
39806
+ continue;
39807
+ const plain = stripAnsi(items[i].label).toLowerCase();
39808
+ const detailPlain = items[i].detail ? stripAnsi(items[i].detail).toLowerCase() : "";
39809
+ if (plain.includes(lower) || detailPlain.includes(lower)) {
39810
+ matchSet.add(i);
39811
+ }
39764
39812
  }
39765
39813
  }
39766
- } catch {
39767
39814
  }
39768
- try {
39769
- const { stdout: nvidiaSmi } = await execAsync("nvidia-smi --query-gpu=memory.total,name --format=csv,noheader,nounits 2>/dev/null", { timeout: 5e3 });
39770
- const lines = nvidiaSmi.trim().split("\n");
39771
- if (lines.length > 0) {
39772
- for (const line of lines) {
39773
- const parts = line.split(",").map((s) => s.trim());
39774
- const vramMB = parseInt(parts[0] ?? "0", 10);
39775
- if (!isNaN(vramMB))
39776
- gpuVramGB += vramMB / 1024;
39777
- if (!gpuName && parts[1])
39778
- gpuName = parts[1];
39779
- }
39815
+ updateFilter();
39816
+ const findSelectable = (from, dir) => {
39817
+ let idx = from;
39818
+ while (idx >= 0 && idx < items.length) {
39819
+ if (!isSkippable(idx) && matchSet.has(idx))
39820
+ return idx;
39821
+ idx += dir;
39780
39822
  }
39781
- } catch {
39782
- }
39783
- return {
39784
- totalRamGB: Math.round(totalRamGB * 10) / 10,
39785
- availableRamGB: Math.round(availableRamGB * 10) / 10,
39786
- gpuVramGB: Math.round(gpuVramGB * 10) / 10,
39787
- gpuName
39823
+ return -1;
39788
39824
  };
39789
- }
39790
- function recommendModel(specs) {
39791
- const effectiveGB = Math.max(specs.gpuVramGB, specs.availableRamGB);
39792
- const budget = effectiveGB * 0.8;
39793
- const localVariants = QWEN_VARIANTS.filter((v) => !v.cloud);
39794
- for (let i = localVariants.length - 1; i >= 0; i--) {
39795
- if (localVariants[i].sizeGB <= budget) {
39796
- return localVariants[i];
39797
- }
39798
- }
39799
- return QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:cloud");
39800
- }
39801
- function calculateContextWindow(specs, modelSizeGB2, kvBytesPerToken, archMax) {
39802
- const totalAvail = Math.max(specs.gpuVramGB, specs.totalRamGB);
39803
- const remaining = Math.max(0, totalAvail - modelSizeGB2);
39804
- const usableGB = remaining * 0.85;
39805
- let numCtx;
39806
- if (kvBytesPerToken && kvBytesPerToken > 0) {
39807
- const maxTokens = Math.floor(usableGB * 1024 ** 3 / kvBytesPerToken);
39808
- numCtx = Math.max(2048, Math.floor(maxTokens / 1024) * 1024);
39809
- } else {
39810
- const kvEstimate = modelSizeGB2 <= 5 ? 524288 : modelSizeGB2 <= 20 ? 1048576 : 1572864;
39811
- const maxTokens = Math.floor(usableGB * 1024 ** 3 / kvEstimate);
39812
- numCtx = Math.max(2048, Math.floor(maxTokens / 1024) * 1024);
39813
- }
39814
- numCtx = Math.min(numCtx, 131072);
39815
- if (archMax && archMax > 0)
39816
- numCtx = Math.min(numCtx, archMax);
39817
- if (kvBytesPerToken && kvBytesPerToken > 0 && modelSizeGB2 > 0) {
39818
- const maxKVBytes = modelSizeGB2 * 4 * 1024 ** 3;
39819
- const maxTokensFromBudget = Math.floor(maxKVBytes / kvBytesPerToken);
39820
- const budgetCap = Math.max(2048, Math.floor(maxTokensFromBudget / 1024) * 1024);
39821
- numCtx = Math.min(numCtx, budgetCap);
39825
+ let cursor = activeKey ? items.findIndex((i) => i.key === activeKey) : -1;
39826
+ if (cursor < 0 || isSkippable(cursor)) {
39827
+ const first = findSelectable(0, 1);
39828
+ cursor = first >= 0 ? first : 0;
39822
39829
  }
39823
- const label = numCtx >= 1024 ? `${Math.floor(numCtx / 1024)}K` : String(numCtx);
39824
- return { numCtx, label };
39825
- }
39826
- function ask(rl, question) {
39827
- return new Promise((resolve36) => {
39828
- rl.question(question, (answer) => resolve36(answer.trim()));
39829
- });
39830
- }
39831
- function askSecret(rl, question) {
39830
+ const reservedTopBottom = 6;
39831
+ const hasCrumbs = opts.breadcrumbs && opts.breadcrumbs.length > 0;
39832
+ const selectChrome = (hasCrumbs ? 11 : 10) + 3;
39833
+ const contentArea = opts.availableRows ? opts.availableRows + reservedTopBottom : process.stdout.rows ?? 24;
39834
+ const maxVisible = opts.maxVisible ?? Math.max(3, contentArea - selectChrome);
39835
+ let scrollOffset = 0;
39836
+ let lastRenderedLines = 0;
39832
39837
  return new Promise((resolve36) => {
39833
- process.stdout.write(question);
39834
- let secret = "";
39835
39838
  const stdin = process.stdin;
39836
39839
  const hadRawMode = stdin.isRaw;
39840
+ const savedRlListeners = [];
39841
+ if (rl) {
39842
+ rl.pause();
39843
+ for (const event of ["keypress", "data"]) {
39844
+ const listeners = stdin.listeners(event);
39845
+ for (const fn of listeners) {
39846
+ savedRlListeners.push({ event, fn });
39847
+ stdin.removeListener(event, fn);
39848
+ }
39849
+ }
39850
+ }
39837
39851
  if (typeof stdin.setRawMode === "function") {
39838
39852
  stdin.setRawMode(true);
39839
39853
  }
39840
39854
  stdin.resume();
39841
- const onData = (chunk) => {
39842
- const ch = chunk.toString("utf8");
39843
- for (const c3 of ch) {
39844
- if (c3 === "\r" || c3 === "\n") {
39845
- stdin.removeListener("data", onData);
39846
- if (typeof stdin.setRawMode === "function") {
39847
- stdin.setRawMode(hadRawMode ?? false);
39848
- }
39849
- process.stdout.write("\n");
39850
- resolve36(secret.trim());
39851
- return;
39852
- } else if (c3 === "") {
39853
- stdin.removeListener("data", onData);
39854
- if (typeof stdin.setRawMode === "function") {
39855
- stdin.setRawMode(hadRawMode ?? false);
39856
- }
39857
- process.stdout.write("\n");
39858
- resolve36("");
39859
- return;
39860
- } else if (c3 === "\x7F" || c3 === "\b") {
39861
- if (secret.length > 0) {
39862
- secret = secret.slice(0, -1);
39863
- process.stdout.write("\b \b");
39855
+ enterOverlay();
39856
+ overlayWrite("\x1B[?1049h\x1B[48;5;234m\x1B[2J\x1B[H\x1B[?25l\x1B[?1003h\x1B[?1006h");
39857
+ let listRowOffset = 0;
39858
+ function clampScroll(displayList) {
39859
+ const cursorPos = displayList.indexOf(cursor);
39860
+ if (cursorPos < 0)
39861
+ return;
39862
+ if (cursorPos < scrollOffset) {
39863
+ scrollOffset = cursorPos;
39864
+ } else if (cursorPos >= scrollOffset + maxVisible) {
39865
+ scrollOffset = cursorPos - maxVisible + 1;
39866
+ }
39867
+ const maxOffset = Math.max(0, displayList.length - maxVisible);
39868
+ scrollOffset = Math.max(0, Math.min(maxOffset, scrollOffset));
39869
+ }
39870
+ const hasBreadcrumbs = opts.breadcrumbs && opts.breadcrumbs.length > 0;
39871
+ function render() {
39872
+ overlayWrite("\x1B[48;5;234m\x1B[H\x1B[2J");
39873
+ const lines = [];
39874
+ lines.push("", "", "");
39875
+ if (hasBreadcrumbs) {
39876
+ const trail = opts.breadcrumbs.map((b) => selectColors.dim(b)).join(selectColors.dim(" \u203A "));
39877
+ lines.push(`
39878
+ ${selectColors.cyan("\u2190")} ${trail}`);
39879
+ }
39880
+ if (title) {
39881
+ if (!hasBreadcrumbs)
39882
+ lines.push("");
39883
+ lines.push(` ${selectColors.bold(title)}`);
39884
+ }
39885
+ if (filter) {
39886
+ const count = matchSet.size;
39887
+ lines.push(` ${selectColors.cyan("/")} ${selectColors.bold(filter)} ${selectColors.dim(`(${count} match${count !== 1 ? "es" : ""})`)}`);
39888
+ } else {
39889
+ lines.push(` ${selectColors.dim("Type to filter...")}`);
39890
+ }
39891
+ lines.push("");
39892
+ let displayList;
39893
+ if (filter) {
39894
+ displayList = [];
39895
+ for (let i = 0; i < items.length; i++) {
39896
+ if (matchSet.has(i) || isSkippable(i)) {
39897
+ displayList.push(i);
39864
39898
  }
39865
- } else if (c3.charCodeAt(0) >= 32) {
39866
- secret += c3;
39867
- process.stdout.write("*");
39868
39899
  }
39900
+ displayList = displayList.filter((idx, pos) => {
39901
+ if (!isSkippable(idx))
39902
+ return true;
39903
+ for (let j = pos + 1; j < displayList.length; j++) {
39904
+ if (!isSkippable(displayList[j]))
39905
+ return true;
39906
+ break;
39907
+ }
39908
+ return false;
39909
+ });
39910
+ } else {
39911
+ displayList = items.map((_, i) => i);
39869
39912
  }
39870
- };
39871
- stdin.on("data", onData);
39872
- });
39873
- }
39874
- function ensureCurl() {
39875
- if (hasCmd("curl"))
39876
- return true;
39877
- const plat = platform2();
39878
- if (plat === "win32") {
39879
- process.stdout.write(` ${c2.yellow("\u26A0")} curl not found on Windows \u2014 install it manually.
39880
- `);
39881
- return false;
39882
- }
39883
- process.stdout.write(` ${c2.cyan("\u25CF")} curl not found. Installing...
39884
- `);
39885
- const strategies = [
39886
- { check: "apt-get", install: "sudo apt-get update -qq && sudo apt-get install -y -qq curl", label: "apt" },
39887
- { check: "dnf", install: "sudo dnf install -y -q curl", label: "dnf" },
39888
- { check: "yum", install: "sudo yum install -y -q curl", label: "yum" },
39889
- { check: "pacman", install: "sudo pacman -S --noconfirm curl", label: "pacman" },
39890
- { check: "apk", install: "sudo apk add --quiet curl", label: "apk" },
39891
- { check: "zypper", install: "sudo zypper install -y curl", label: "zypper" }
39892
- ];
39893
- for (const s of strategies) {
39894
- if (hasCmd(s.check)) {
39895
- try {
39896
- execSync27(s.install, { stdio: "inherit", timeout: 12e4 });
39897
- if (hasCmd("curl")) {
39898
- process.stdout.write(` ${c2.green("\u2714")} curl installed via ${s.label}.
39899
- `);
39900
- return true;
39913
+ clampScroll(displayList);
39914
+ const visibleStart = scrollOffset;
39915
+ const visibleEnd = Math.min(displayList.length, scrollOffset + maxVisible);
39916
+ if (visibleStart > 0) {
39917
+ lines.push(` ${selectColors.dim(` \u25B2 ${visibleStart} more`)}`);
39918
+ }
39919
+ listRowOffset = lines.length;
39920
+ for (let vi = visibleStart; vi < visibleEnd; vi++) {
39921
+ const idx = displayList[vi];
39922
+ const item = items[idx];
39923
+ if (isSkippable(idx)) {
39924
+ lines.push(` ${item.label}`);
39925
+ continue;
39901
39926
  }
39902
- } catch {
39903
- process.stdout.write(` ${c2.yellow("\u26A0")} ${s.label} install failed, trying next...
39904
- `);
39927
+ const focused = idx === cursor;
39928
+ const isActive = item.key === activeKey;
39929
+ if (deleteConfirmIdx === idx) {
39930
+ const yesLabel = deleteConfirmSel ? selectColors.bold(selectColors.green("[Yes]")) : selectColors.dim("[Yes]");
39931
+ const noLabel = !deleteConfirmSel ? selectColors.bold(selectColors.orange("[No]")) : selectColors.dim("[No]");
39932
+ lines.push(` ${ansi3("31", "\u2715")} ${ansi3("31", stripAnsi(item.label))} Delete? ${yesLabel} ${noLabel}`);
39933
+ } else if (filter) {
39934
+ lines.push(matchRow(item, focused, isActive));
39935
+ } else {
39936
+ lines.push(renderRow(item, focused, isActive));
39937
+ }
39938
+ }
39939
+ const remaining = displayList.length - visibleEnd;
39940
+ if (remaining > 0) {
39941
+ lines.push(` ${selectColors.dim(` \u25BC ${remaining} more`)}`);
39942
+ }
39943
+ lines.push("");
39944
+ if (deleteConfirmIdx >= 0) {
39945
+ lines.push(` ${selectColors.dim("\u2190/\u2192 select Enter confirm Esc cancel")}`);
39946
+ } else {
39947
+ const actionHint = opts.onAction ? " \u2190/\u2192/Space toggle" : "";
39948
+ const deleteHint = opts.onDelete ? " Del remove" : "";
39949
+ const customHint = opts.customKeyHint ?? "";
39950
+ const escLabel = filter ? "clear filter" : hasBreadcrumbs ? "\u2190 back" : "cancel";
39951
+ lines.push(` ${selectColors.dim("\u2191/\u2193 navigate Enter/Click select" + actionHint + deleteHint + customHint + " Esc " + escLabel + " Type to filter")}`);
39905
39952
  }
39953
+ lines.push("");
39954
+ let output = lines.join("\n").replace(/\x1B\[0m/g, "\x1B[0m\x1B[48;5;234m").replace(/\n/g, "\x1B[K\n\x1B[48;5;234m");
39955
+ overlayWrite("\x1B[48;5;234m" + output + "\x1B[K");
39956
+ lastRenderedLines = lines.length;
39906
39957
  }
39907
- }
39908
- if (plat === "darwin") {
39909
- try {
39910
- execSync27("xcode-select --install", { stdio: "inherit", timeout: 3e5 });
39911
- if (hasCmd("curl"))
39912
- return true;
39913
- } catch {
39958
+ function cleanup() {
39959
+ stdin.removeListener("data", onData);
39960
+ process.stdout.removeListener("resize", onResize);
39961
+ overlayWrite("\x1B[?1003l\x1B[?1002l\x1B[?1006l\x1B[?1049l\x1B[?25h");
39962
+ leaveOverlay();
39963
+ if (typeof stdin.setRawMode === "function") {
39964
+ stdin.setRawMode(hadRawMode ?? false);
39965
+ }
39966
+ for (const { event, fn } of savedRlListeners) {
39967
+ stdin.on(event, fn);
39968
+ }
39969
+ if (rl) {
39970
+ rl.resume();
39971
+ rl.prompt(false);
39972
+ }
39914
39973
  }
39915
- }
39916
- process.stdout.write(` ${c2.red("\u2716")} Could not install curl. Install it manually and retry.
39917
- `);
39918
- return false;
39919
- }
39920
- async function autoInstallOllama(rl) {
39921
- const plat = platform2();
39922
- if (plat !== "win32" && !ensureCurl()) {
39923
- return false;
39924
- }
39925
- if (plat === "linux") {
39926
- return installOllamaLinux();
39927
- } else if (plat === "darwin") {
39928
- return await installOllamaMac(rl);
39929
- } else if (plat === "win32") {
39930
- return installOllamaWindows();
39931
- }
39932
- process.stdout.write(` ${c2.yellow("\u26A0")} Unsupported platform: ${plat}
39933
- `);
39934
- process.stdout.write(` ${c2.dim("Visit https://ollama.com to install manually.")}
39935
-
39936
- `);
39937
- return false;
39938
- }
39939
- function installOllamaLinux() {
39940
- process.stdout.write(`
39941
- ${c2.cyan("\u25CF")} Installing Ollama via official install script...
39942
-
39943
- `);
39944
- try {
39945
- execSync27("curl -fsSL https://ollama.com/install.sh | sh", {
39946
- stdio: "inherit",
39947
- timeout: 3e5
39948
- });
39949
- if (hasCmd("ollama")) {
39950
- process.stdout.write(`
39951
- ${c2.green("\u2714")} Ollama installed successfully.
39952
- `);
39953
- return true;
39954
- }
39955
- process.stdout.write(`
39956
- ${c2.yellow("\u26A0")} Install script ran but ollama not found on PATH.
39957
- `);
39958
- return false;
39959
- } catch (err) {
39960
- process.stdout.write(`
39961
- ${c2.red("\u2716")} Ollama install failed: ${err instanceof Error ? err.message : String(err)}
39962
- `);
39963
- return false;
39964
- }
39965
- }
39966
- async function installOllamaMac(_rl) {
39967
- if (!hasCmd("brew")) {
39968
- process.stdout.write(`
39969
- ${c2.cyan("\u25CF")} Homebrew not found. Installing automatically (needed for Ollama on macOS)...
39970
-
39971
- `);
39972
- try {
39973
- execSync27('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"', { stdio: "inherit", timeout: 6e5 });
39974
- if (!hasCmd("brew")) {
39975
- try {
39976
- const brewPrefix = existsSync37("/opt/homebrew/bin/brew") ? "/opt/homebrew" : "/usr/local";
39977
- process.env["PATH"] = `${brewPrefix}/bin:${process.env["PATH"]}`;
39978
- } catch {
39974
+ function onData(chunk) {
39975
+ let seq = chunk.toString("utf8");
39976
+ const mouseRe = /\x1B\[<(\d+);(\d+);(\d+)([Mm])/g;
39977
+ let mouseProcessed = false;
39978
+ let mouseM;
39979
+ while ((mouseM = mouseRe.exec(seq)) !== null) {
39980
+ mouseProcessed = true;
39981
+ const btn = parseInt(mouseM[1]);
39982
+ const mCol = parseInt(mouseM[2]);
39983
+ const mRow = parseInt(mouseM[3]);
39984
+ const suffix = mouseM[4];
39985
+ if (btn === 0 && suffix === "M") {
39986
+ const listIdx = mRow - listRowOffset - 1;
39987
+ if (listIdx >= 0 && listIdx < maxVisible) {
39988
+ let displayList;
39989
+ if (filter) {
39990
+ displayList = [];
39991
+ for (let i = 0; i < items.length; i++) {
39992
+ if (matchSet.has(i) || isSkippable(i))
39993
+ displayList.push(i);
39994
+ }
39995
+ displayList = displayList.filter((idx, pos) => {
39996
+ if (!isSkippable(idx))
39997
+ return true;
39998
+ for (let j = pos + 1; j < displayList.length; j++) {
39999
+ if (!isSkippable(displayList[j]))
40000
+ return true;
40001
+ break;
40002
+ }
40003
+ return false;
40004
+ });
40005
+ } else {
40006
+ displayList = items.map((_, i) => i);
40007
+ }
40008
+ const vi = scrollOffset + listIdx;
40009
+ if (vi < displayList.length) {
40010
+ const itemIdx = displayList[vi];
40011
+ if (!isSkippable(itemIdx) && matchSet.has(itemIdx)) {
40012
+ cursor = itemIdx;
40013
+ cleanup();
40014
+ resolve36({ confirmed: true, key: items[cursor].key, index: cursor });
40015
+ return;
40016
+ } else if (!isSkippable(itemIdx)) {
40017
+ cursor = itemIdx;
40018
+ render();
40019
+ }
40020
+ }
40021
+ }
40022
+ }
40023
+ if ((btn === 35 || btn === 32 || btn === 67) && suffix === "M") {
40024
+ const listIdx = mRow - listRowOffset - 1;
40025
+ if (listIdx >= 0 && listIdx < maxVisible) {
40026
+ let displayList;
40027
+ if (filter) {
40028
+ displayList = [];
40029
+ for (let i = 0; i < items.length; i++) {
40030
+ if (matchSet.has(i) || isSkippable(i))
40031
+ displayList.push(i);
40032
+ }
40033
+ displayList = displayList.filter((idx, pos) => {
40034
+ if (!isSkippable(idx))
40035
+ return true;
40036
+ for (let j = pos + 1; j < displayList.length; j++) {
40037
+ if (!isSkippable(displayList[j]))
40038
+ return true;
40039
+ break;
40040
+ }
40041
+ return false;
40042
+ });
40043
+ } else {
40044
+ displayList = items.map((_, i) => i);
40045
+ }
40046
+ const vi = scrollOffset + listIdx;
40047
+ if (vi < displayList.length) {
40048
+ const itemIdx = displayList[vi];
40049
+ if (!isSkippable(itemIdx) && itemIdx !== cursor) {
40050
+ cursor = itemIdx;
40051
+ render();
40052
+ }
40053
+ }
40054
+ }
40055
+ }
40056
+ if (btn === 64) {
40057
+ const next = findSelectable(cursor - 1, -1);
40058
+ if (next >= 0 && next !== cursor) {
40059
+ cursor = next;
40060
+ render();
40061
+ }
40062
+ } else if (btn === 65) {
40063
+ const next = findSelectable(cursor + 1, 1);
40064
+ if (next >= 0 && next !== cursor) {
40065
+ cursor = next;
40066
+ render();
40067
+ }
39979
40068
  }
39980
40069
  }
39981
- if (hasCmd("brew")) {
39982
- process.stdout.write(`
39983
- ${c2.green("\u2714")} Homebrew installed.
39984
- `);
39985
- } else {
39986
- process.stdout.write(`
39987
- ${c2.yellow("\u26A0")} Homebrew install completed but brew not found on PATH.
39988
- `);
39989
- process.stdout.write(` ${c2.dim('Try: eval "$(/opt/homebrew/bin/brew shellenv)"')}
39990
-
39991
- `);
39992
- return false;
40070
+ seq = seq.replace(mouseRe, "");
40071
+ if (!seq && mouseProcessed)
40072
+ return;
40073
+ if (deleteConfirmIdx >= 0) {
40074
+ if (seq === "\x1B[D") {
40075
+ deleteConfirmSel = true;
40076
+ render();
40077
+ } else if (seq === "\x1B[C") {
40078
+ deleteConfirmSel = false;
40079
+ render();
40080
+ } else if (seq === "\r" || seq === "\n") {
40081
+ if (deleteConfirmSel && opts.onDelete) {
40082
+ const deletedIdx = deleteConfirmIdx;
40083
+ deleteConfirmIdx = -1;
40084
+ deleteConfirmSel = false;
40085
+ opts.onDelete(items[deletedIdx], (removed) => {
40086
+ if (removed) {
40087
+ items.splice(deletedIdx, 1);
40088
+ if (items.length === 0) {
40089
+ cleanup();
40090
+ resolve36({ confirmed: false, key: null, index: -1 });
40091
+ return;
40092
+ }
40093
+ updateFilter();
40094
+ if (cursor >= items.length)
40095
+ cursor = items.length - 1;
40096
+ const valid = findSelectable(cursor, 1) ?? findSelectable(cursor, -1);
40097
+ if (valid >= 0)
40098
+ cursor = valid;
40099
+ scrollOffset = 0;
40100
+ }
40101
+ render();
40102
+ });
40103
+ } else {
40104
+ deleteConfirmIdx = -1;
40105
+ deleteConfirmSel = false;
40106
+ render();
40107
+ }
40108
+ } else if (seq === "\x1B" || seq === "\x1B\x1B" || seq === "") {
40109
+ deleteConfirmIdx = -1;
40110
+ deleteConfirmSel = false;
40111
+ render();
40112
+ }
40113
+ return;
39993
40114
  }
39994
- } catch (err) {
39995
- process.stdout.write(`
39996
- ${c2.red("\u2716")} Homebrew install failed: ${err instanceof Error ? err.message : String(err)}
39997
- `);
39998
- return false;
40115
+ if (seq === "\x1B[A") {
40116
+ const next = findSelectable(cursor - 1, -1);
40117
+ if (next >= 0 && next !== cursor) {
40118
+ cursor = next;
40119
+ render();
40120
+ }
40121
+ } else if (seq === "\x1B[B") {
40122
+ const next = findSelectable(cursor + 1, 1);
40123
+ if (next >= 0 && next !== cursor) {
40124
+ cursor = next;
40125
+ render();
40126
+ }
40127
+ } else if (seq === "\x1B[5~") {
40128
+ for (let i = 0; i < maxVisible; i++) {
40129
+ const next = findSelectable(cursor - 1, -1);
40130
+ if (next < 0 || next === cursor)
40131
+ break;
40132
+ cursor = next;
40133
+ }
40134
+ render();
40135
+ } else if (seq === "\x1B[6~") {
40136
+ for (let i = 0; i < maxVisible; i++) {
40137
+ const next = findSelectable(cursor + 1, 1);
40138
+ if (next < 0 || next === cursor)
40139
+ break;
40140
+ cursor = next;
40141
+ }
40142
+ render();
40143
+ } else if (seq === "\x1B[H") {
40144
+ const first = findSelectable(0, 1);
40145
+ if (first >= 0) {
40146
+ cursor = first;
40147
+ render();
40148
+ }
40149
+ } else if (seq === "\x1B[F") {
40150
+ const last = findSelectable(items.length - 1, -1);
40151
+ if (last >= 0) {
40152
+ cursor = last;
40153
+ render();
40154
+ }
40155
+ } else if (seq === "\x1B[3~" && opts.onDelete) {
40156
+ if (!isSkippable(cursor) && matchSet.has(cursor) && items[cursor].key !== activeKey) {
40157
+ deleteConfirmIdx = cursor;
40158
+ deleteConfirmSel = false;
40159
+ render();
40160
+ }
40161
+ } else if (seq === "\x1B[D" && opts.onAction) {
40162
+ if (!isSkippable(cursor) && matchSet.has(cursor)) {
40163
+ if (opts.onAction(items[cursor], "left"))
40164
+ render();
40165
+ }
40166
+ } else if (seq === "\x1B[C" && opts.onAction) {
40167
+ if (!isSkippable(cursor) && matchSet.has(cursor)) {
40168
+ if (opts.onAction(items[cursor], "right"))
40169
+ render();
40170
+ }
40171
+ } else if (seq === " " && opts.onAction) {
40172
+ if (!isSkippable(cursor) && matchSet.has(cursor)) {
40173
+ if (opts.onAction(items[cursor], "space"))
40174
+ render();
40175
+ }
40176
+ } else if (seq === "\r" || seq === "\n") {
40177
+ if (!isSkippable(cursor) && matchSet.has(cursor)) {
40178
+ if (opts.onEnter) {
40179
+ const consumed = opts.onEnter(items[cursor], {
40180
+ done: () => render(),
40181
+ resolve: (result) => {
40182
+ cleanup();
40183
+ resolve36(result);
40184
+ },
40185
+ getInput: (prompt, prefill) => getInputFromUser(prompt, prefill),
40186
+ render: () => render(),
40187
+ updateItem: (index, updates) => {
40188
+ Object.assign(items[index], updates);
40189
+ render();
40190
+ }
40191
+ });
40192
+ if (consumed)
40193
+ return;
40194
+ }
40195
+ cleanup();
40196
+ resolve36({ confirmed: true, key: items[cursor].key, index: cursor });
40197
+ }
40198
+ } else if (seq === "\x1B" || seq === "\x1B\x1B") {
40199
+ if (filter) {
40200
+ filter = "";
40201
+ updateFilter();
40202
+ const valid = findSelectable(cursor, 1);
40203
+ if (valid >= 0)
40204
+ cursor = valid;
40205
+ scrollOffset = 0;
40206
+ render();
40207
+ } else if (hasBreadcrumbs) {
40208
+ cleanup();
40209
+ resolve36({ confirmed: false, key: "__back__", index: cursor });
40210
+ } else {
40211
+ cleanup();
40212
+ resolve36({ confirmed: false, key: null, index: cursor });
40213
+ }
40214
+ } else if (seq === "") {
40215
+ cleanup();
40216
+ resolve36({ confirmed: false, key: null, index: cursor });
40217
+ } else if (seq === "\x7F" || seq === "\b") {
40218
+ if (filter.length > 0) {
40219
+ filter = filter.slice(0, -1);
40220
+ updateFilter();
40221
+ if (!matchSet.has(cursor) || isSkippable(cursor)) {
40222
+ const next = findSelectable(0, 1);
40223
+ if (next >= 0)
40224
+ cursor = next;
40225
+ }
40226
+ scrollOffset = 0;
40227
+ render();
40228
+ } else if (hasBreadcrumbs) {
40229
+ cleanup();
40230
+ resolve36({ confirmed: false, key: "__back__", index: cursor });
40231
+ }
40232
+ } else if (seq.length === 1 && seq.charCodeAt(0) >= 32 && seq.charCodeAt(0) < 127) {
40233
+ if (opts.onCustomKey && !isSkippable(cursor) && matchSet.has(cursor)) {
40234
+ const consumed = opts.onCustomKey(items[cursor], seq, {
40235
+ done: () => render(),
40236
+ resolve: (result) => {
40237
+ cleanup();
40238
+ resolve36(result);
40239
+ },
40240
+ getInput: (prompt, prefill) => getInputFromUser(prompt, prefill),
40241
+ render: () => render(),
40242
+ updateItem: (index, updates) => {
40243
+ Object.assign(items[index], updates);
40244
+ render();
40245
+ }
40246
+ });
40247
+ if (consumed)
40248
+ return;
40249
+ }
40250
+ filter += seq;
40251
+ updateFilter();
40252
+ if (matchSet.size > 0) {
40253
+ const first = findSelectable(0, 1);
40254
+ if (first >= 0)
40255
+ cursor = first;
40256
+ }
40257
+ scrollOffset = 0;
40258
+ render();
40259
+ }
40260
+ }
40261
+ function getInputFromUser(prompt, prefill = "") {
40262
+ return new Promise((inputResolve) => {
40263
+ let inputBuf = prefill;
40264
+ let inputCursor = prefill.length;
40265
+ overlayWrite("\x1B[?25h");
40266
+ function renderInput() {
40267
+ overlayWrite(`\x1B[${lastRenderedLines}A`);
40268
+ const saveFooter = lastRenderedLines;
40269
+ for (let i = 0; i < saveFooter; i++)
40270
+ overlayWrite("\x1B[B");
40271
+ overlayWrite("\x1B[2A");
40272
+ overlayWrite("\x1B[2K");
40273
+ const before = inputBuf.slice(0, inputCursor);
40274
+ const after = inputBuf.slice(inputCursor);
40275
+ overlayWrite(` ${selectColors.cyan("\u203A")} ${selectColors.bold(prompt)} ${before}\x1B[7m${after.charAt(0) || " "}\x1B[27m${after.slice(1)}`);
40276
+ overlayWrite("\n\x1B[2K");
40277
+ overlayWrite(` ${selectColors.dim("Enter confirm Esc cancel")}`);
40278
+ overlayWrite("\n");
40279
+ }
40280
+ function onInputData(chunk) {
40281
+ const s = chunk.toString("utf8");
40282
+ if (s === "\r" || s === "\n") {
40283
+ stdin.removeListener("data", onInputData);
40284
+ stdin.on("data", onData);
40285
+ overlayWrite("\x1B[?25l");
40286
+ render();
40287
+ inputResolve(inputBuf);
40288
+ } else if (s === "\x1B" || s === "\x1B\x1B" || s === "") {
40289
+ stdin.removeListener("data", onInputData);
40290
+ stdin.on("data", onData);
40291
+ overlayWrite("\x1B[?25l");
40292
+ render();
40293
+ inputResolve(null);
40294
+ } else if (s === "\x7F" || s === "\b") {
40295
+ if (inputCursor > 0) {
40296
+ inputBuf = inputBuf.slice(0, inputCursor - 1) + inputBuf.slice(inputCursor);
40297
+ inputCursor--;
40298
+ renderInput();
40299
+ }
40300
+ } else if (s === "\x1B[D") {
40301
+ if (inputCursor > 0) {
40302
+ inputCursor--;
40303
+ renderInput();
40304
+ }
40305
+ } else if (s === "\x1B[C") {
40306
+ if (inputCursor < inputBuf.length) {
40307
+ inputCursor++;
40308
+ renderInput();
40309
+ }
40310
+ } else if (s === "\x1B[H") {
40311
+ inputCursor = 0;
40312
+ renderInput();
40313
+ } else if (s === "\x1B[F") {
40314
+ inputCursor = inputBuf.length;
40315
+ renderInput();
40316
+ } else if (s.length === 1 && s.charCodeAt(0) >= 32 && s.charCodeAt(0) < 127) {
40317
+ inputBuf = inputBuf.slice(0, inputCursor) + s + inputBuf.slice(inputCursor);
40318
+ inputCursor++;
40319
+ renderInput();
40320
+ }
40321
+ }
40322
+ stdin.removeListener("data", onData);
40323
+ stdin.on("data", onInputData);
40324
+ renderInput();
40325
+ });
39999
40326
  }
40327
+ const onResize = () => render();
40328
+ process.stdout.on("resize", onResize);
40329
+ stdin.on("data", onData);
40330
+ render();
40331
+ });
40332
+ }
40333
+ var isTTY3, selectColors;
40334
+ var init_tui_select = __esm({
40335
+ "packages/cli/dist/tui/tui-select.js"() {
40336
+ "use strict";
40337
+ init_overlay_lock();
40338
+ isTTY3 = process.stdout.isTTY ?? false;
40339
+ selectColors = {
40340
+ orange: (t) => fg2562(208, t),
40341
+ green: (t) => ansi3("32", t),
40342
+ dim: (t) => ansi3("2", t),
40343
+ bold: (t) => ansi3("1", t),
40344
+ cyan: (t) => ansi3("36", t),
40345
+ /** Lighter grey for filter matches (252 = near-white) */
40346
+ matchLight: (t) => fg2562(252, t),
40347
+ /** Dark grey for non-matching items (240 = dark grey) */
40348
+ matchDark: (t) => fg2562(240, t)
40349
+ };
40000
40350
  }
40001
- process.stdout.write(`
40002
- ${c2.cyan("\u25CF")} Installing Ollama via Homebrew...
40351
+ });
40003
40352
 
40004
- `);
40353
+ // packages/cli/dist/tui/setup.js
40354
+ import * as readline from "node:readline";
40355
+ import { execSync as execSync27, spawn as spawn19, exec as exec2 } from "node:child_process";
40356
+ import { promisify as promisify6 } from "node:util";
40357
+ import { existsSync as existsSync37, writeFileSync as writeFileSync16, readFileSync as readFileSync28, appendFileSync as appendFileSync2, mkdirSync as mkdirSync15 } from "node:fs";
40358
+ import { join as join54 } from "node:path";
40359
+ import { homedir as homedir13, platform as platform2 } from "node:os";
40360
+ async function checkToolSupport(modelName, backendUrl = "http://localhost:11434") {
40361
+ if (_toolSupportCache.has(modelName))
40362
+ return _toolSupportCache.get(modelName);
40005
40363
  try {
40006
- execSync27("brew install ollama", {
40007
- stdio: "inherit",
40008
- timeout: 3e5
40364
+ const resp = await fetch(`${backendUrl.replace(/\/+$/, "")}/api/chat`, {
40365
+ method: "POST",
40366
+ headers: { "Content-Type": "application/json" },
40367
+ body: JSON.stringify({
40368
+ model: modelName,
40369
+ messages: [{ role: "user", content: "test" }],
40370
+ tools: [{ type: "function", function: { name: "test", description: "test", parameters: { type: "object", properties: {} } } }],
40371
+ stream: false,
40372
+ options: { num_predict: 1 }
40373
+ }),
40374
+ signal: AbortSignal.timeout(1e4)
40009
40375
  });
40010
- if (hasCmd("ollama")) {
40011
- process.stdout.write(`
40012
- ${c2.green("\u2714")} Ollama installed successfully.
40013
- `);
40014
- return true;
40015
- }
40016
- process.stdout.write(`
40017
- ${c2.yellow("\u26A0")} brew install completed but ollama not found on PATH.
40018
- `);
40019
- return false;
40020
- } catch (err) {
40021
- process.stdout.write(`
40022
- ${c2.red("\u2716")} Ollama install failed: ${err instanceof Error ? err.message : String(err)}
40023
- `);
40024
- return false;
40376
+ const hasTools = resp.ok;
40377
+ _toolSupportCache.set(modelName, hasTools);
40378
+ return hasTools;
40379
+ } catch {
40380
+ _toolSupportCache.set(modelName, true);
40381
+ return true;
40025
40382
  }
40026
40383
  }
40027
- function installOllamaWindows() {
40028
- process.stdout.write(`
40029
- ${c2.cyan("\u25CF")} Installing Ollama via PowerShell...
40030
-
40031
- `);
40384
+ function detectSystemSpecs() {
40385
+ let totalRamGB = 0;
40386
+ let availableRamGB = 0;
40387
+ let gpuVramGB = 0;
40388
+ let gpuName = "";
40032
40389
  try {
40033
- execSync27('powershell -Command "irm https://ollama.com/install.ps1 | iex"', {
40034
- stdio: "inherit",
40035
- timeout: 3e5
40390
+ const memInfo = execSync27("free -b 2>/dev/null || sysctl -n hw.memsize 2>/dev/null", {
40391
+ encoding: "utf8",
40392
+ timeout: 5e3
40036
40393
  });
40037
- if (hasCmd("ollama")) {
40038
- process.stdout.write(`
40039
- ${c2.green("\u2714")} Ollama installed successfully.
40040
- `);
40041
- return true;
40042
- }
40043
- process.stdout.write(`
40044
- ${c2.yellow("\u26A0")} Install script ran but ollama not found on PATH.
40045
- `);
40046
- return false;
40047
- } catch (err) {
40048
- process.stdout.write(`
40049
- ${c2.red("\u2716")} Ollama install failed: ${err instanceof Error ? err.message : String(err)}
40050
- `);
40051
- return false;
40052
- }
40053
- }
40054
- async function ensureOllamaRunning(backendUrl, rl) {
40055
- const ollamaInstalled = hasCmd("ollama");
40056
- if (!ollamaInstalled) {
40057
- if (rl) {
40058
- process.stdout.write(`
40059
- ${c2.yellow("\u26A0")} Ollama is not installed on this system.
40060
- `);
40061
- const answer = await ask(rl, ` ${c2.bold("Install Ollama now?")} (Y/n) `);
40062
- if (answer.toLowerCase() === "n") {
40063
- return false;
40394
+ if (memInfo.includes("Mem:")) {
40395
+ const match = memInfo.match(/^Mem:\s+(\d+)\s+\d+\s+\d+\s+\d+\s+\d+\s+(\d+)/m);
40396
+ if (match) {
40397
+ totalRamGB = parseInt(match[1], 10) / 1024 ** 3;
40398
+ availableRamGB = parseInt(match[2], 10) / 1024 ** 3;
40064
40399
  }
40065
40400
  } else {
40066
- process.stdout.write(`
40067
- ${c2.cyan("\u25CF")} Ollama not found. Installing automatically...
40068
- `);
40069
- }
40070
- const installRl = rl ?? readline.createInterface({ input: process.stdin, output: process.stdout });
40071
- const installed = await autoInstallOllama(installRl);
40072
- if (!rl)
40073
- installRl.close();
40074
- if (!installed) {
40075
- process.stdout.write(` ${c2.red("\u2716")} Ollama installation failed.
40076
-
40077
- `);
40078
- return false;
40401
+ const bytes = parseInt(memInfo.trim(), 10);
40402
+ if (!isNaN(bytes)) {
40403
+ totalRamGB = bytes / 1024 ** 3;
40404
+ availableRamGB = totalRamGB * 0.7;
40405
+ }
40079
40406
  }
40080
- }
40081
- process.stdout.write(` ${c2.cyan("\u25CF")} Starting ollama serve...
40082
- `);
40083
- try {
40084
- const child = spawn19("ollama", ["serve"], { stdio: "ignore", detached: true });
40085
- child.unref();
40086
40407
  } catch {
40087
- process.stdout.write(` ${c2.yellow("\u26A0")} Could not start ollama serve.
40088
-
40089
- `);
40090
- return false;
40091
40408
  }
40092
- for (let i = 0; i < 5; i++) {
40093
- await new Promise((resolve36) => setTimeout(resolve36, 2e3));
40094
- try {
40095
- const resp = await fetch(`${backendUrl}/api/tags`, {
40096
- signal: AbortSignal.timeout(3e3)
40097
- });
40098
- if (resp.ok) {
40099
- process.stdout.write(` ${c2.green("\u2714")} Ollama is running.
40100
-
40101
- `);
40102
- return true;
40409
+ try {
40410
+ const nvidiaSmi = execSync27("nvidia-smi --query-gpu=memory.total,name --format=csv,noheader,nounits 2>/dev/null", { encoding: "utf8", timeout: 5e3 });
40411
+ const lines = nvidiaSmi.trim().split("\n");
40412
+ if (lines.length > 0) {
40413
+ for (const line of lines) {
40414
+ const parts = line.split(",").map((s) => s.trim());
40415
+ const vramMB = parseInt(parts[0] ?? "0", 10);
40416
+ if (!isNaN(vramMB))
40417
+ gpuVramGB += vramMB / 1024;
40418
+ if (!gpuName && parts[1])
40419
+ gpuName = parts[1];
40103
40420
  }
40104
- } catch {
40105
40421
  }
40422
+ } catch {
40106
40423
  }
40107
- process.stdout.write(` ${c2.yellow("\u26A0")} Ollama started but not responding. Try ${c2.bold("ollama serve")} manually.
40108
-
40109
- `);
40110
- return false;
40424
+ return {
40425
+ totalRamGB: Math.round(totalRamGB * 10) / 10,
40426
+ availableRamGB: Math.round(availableRamGB * 10) / 10,
40427
+ gpuVramGB: Math.round(gpuVramGB * 10) / 10,
40428
+ gpuName
40429
+ };
40111
40430
  }
40112
- function pullModelWithAutoUpdate(tag) {
40431
+ async function detectSystemSpecsAsync() {
40432
+ let totalRamGB = 0;
40433
+ let availableRamGB = 0;
40434
+ let gpuVramGB = 0;
40435
+ let gpuName = "";
40113
40436
  try {
40114
- execSync27(`ollama pull ${tag}`, {
40115
- stdio: "inherit",
40116
- timeout: 36e5
40117
- // 1 hour max
40118
- });
40119
- } catch (err) {
40120
- const errMsg = err instanceof Error ? err.message : String(err);
40121
- const stderr = err?.stderr?.toString?.() ?? errMsg;
40122
- const combined = errMsg + "\n" + stderr;
40123
- if (combined.includes("412") || combined.includes("newer version") || combined.includes("requires a newer version")) {
40124
- process.stdout.write(`
40125
- ${c2.yellow("\u26A0")} Ollama needs to be updated for this model.
40126
- `);
40127
- if (!ensureCurl()) {
40128
- throw new Error("curl is required to update Ollama but could not be installed.");
40129
- }
40130
- process.stdout.write(` ${c2.cyan("\u25CF")} Updating Ollama via official install script...
40131
-
40132
- `);
40133
- try {
40134
- execSync27("curl -fsSL https://ollama.com/install.sh | sh", {
40135
- stdio: "inherit",
40136
- timeout: 3e5
40137
- // 5 min max for install
40138
- });
40139
- process.stdout.write(`
40140
- ${c2.green("\u2714")} Ollama updated successfully.
40141
- `);
40142
- process.stdout.write(` ${c2.cyan("\u25CF")} Retrying pull of ${c2.bold(tag)}...
40143
-
40144
- `);
40145
- execSync27(`ollama pull ${tag}`, {
40146
- stdio: "inherit",
40147
- timeout: 36e5
40148
- });
40149
- } catch (updateErr) {
40150
- const updateMsg = updateErr instanceof Error ? updateErr.message : String(updateErr);
40151
- throw new Error(`Failed to update Ollama and retry pull: ${updateMsg}
40152
- Try manually:
40153
- curl -fsSL https://ollama.com/install.sh | sh
40154
- ollama pull ${tag}`);
40437
+ const { stdout: memInfo } = await execAsync("free -b 2>/dev/null || sysctl -n hw.memsize 2>/dev/null", { timeout: 5e3 });
40438
+ if (memInfo.includes("Mem:")) {
40439
+ const match = memInfo.match(/^Mem:\s+(\d+)\s+\d+\s+\d+\s+\d+\s+\d+\s+(\d+)/m);
40440
+ if (match) {
40441
+ totalRamGB = parseInt(match[1], 10) / 1024 ** 3;
40442
+ availableRamGB = parseInt(match[2], 10) / 1024 ** 3;
40155
40443
  }
40156
40444
  } else {
40157
- throw err;
40445
+ const bytes = parseInt(memInfo.trim(), 10);
40446
+ if (!isNaN(bytes)) {
40447
+ totalRamGB = bytes / 1024 ** 3;
40448
+ availableRamGB = totalRamGB * 0.7;
40449
+ }
40158
40450
  }
40451
+ } catch {
40159
40452
  }
40160
- }
40161
- function computeInferenceScore(specs) {
40162
- const effectiveGB = Math.max(specs.gpuVramGB, specs.availableRamGB);
40163
- const memoryScore = Math.min(100, Math.round(effectiveGB <= 4 ? effectiveGB * 2.5 : effectiveGB <= 16 ? 10 + (effectiveGB - 4) * 2.5 : effectiveGB <= 48 ? 40 + (effectiveGB - 16) * 0.94 : effectiveGB <= 128 ? 70 + (effectiveGB - 48) * 0.375 : 100));
40164
- let computeScore;
40165
- if (specs.gpuVramGB >= 24)
40166
- computeScore = 90;
40167
- else if (specs.gpuVramGB >= 12)
40168
- computeScore = 70;
40169
- else if (specs.gpuVramGB >= 8)
40170
- computeScore = 55;
40171
- else if (specs.gpuVramGB >= 4)
40172
- computeScore = 35;
40173
- else if (specs.totalRamGB >= 32)
40174
- computeScore = 25;
40175
- else
40176
- computeScore = Math.max(5, Math.round(specs.totalRamGB * 1.5));
40177
- let speedScore;
40178
- if (specs.gpuVramGB >= 48)
40179
- speedScore = 95;
40180
- else if (specs.gpuVramGB >= 24)
40181
- speedScore = 80;
40182
- else if (specs.gpuVramGB >= 12)
40183
- speedScore = 65;
40184
- else if (specs.gpuVramGB >= 8)
40185
- speedScore = 50;
40186
- else if (specs.gpuVramGB > 0)
40187
- speedScore = 35;
40188
- else
40189
- speedScore = Math.min(30, Math.round(specs.totalRamGB * 0.6));
40190
- const overall = Math.round(memoryScore * 0.45 + computeScore * 0.35 + speedScore * 0.2);
40191
- const modelCompat = QWEN_VARIANTS.filter((v) => !v.cloud).map((v) => {
40192
- const budget = effectiveGB * 0.8;
40193
- const fits = v.sizeGB <= budget;
40194
- let note;
40195
- if (fits) {
40196
- const headroom = budget - v.sizeGB;
40197
- note = headroom > v.sizeGB * 0.5 ? "comfortable" : headroom > v.sizeGB * 0.15 ? "good fit" : "tight fit";
40198
- if (specs.gpuVramGB > 0 && v.sizeGB <= specs.gpuVramGB * 0.8) {
40199
- note += " (GPU accelerated)";
40453
+ try {
40454
+ const { stdout: nvidiaSmi } = await execAsync("nvidia-smi --query-gpu=memory.total,name --format=csv,noheader,nounits 2>/dev/null", { timeout: 5e3 });
40455
+ const lines = nvidiaSmi.trim().split("\n");
40456
+ if (lines.length > 0) {
40457
+ for (const line of lines) {
40458
+ const parts = line.split(",").map((s) => s.trim());
40459
+ const vramMB = parseInt(parts[0] ?? "0", 10);
40460
+ if (!isNaN(vramMB))
40461
+ gpuVramGB += vramMB / 1024;
40462
+ if (!gpuName && parts[1])
40463
+ gpuName = parts[1];
40200
40464
  }
40201
- } else {
40202
- const overshoot = v.sizeGB - budget;
40203
- note = `need ${overshoot.toFixed(0)} GB more`;
40204
40465
  }
40205
- return { tag: v.tag, fits, note };
40206
- });
40207
- let summary;
40208
- if (overall >= 80)
40209
- summary = "Excellent \u2014 can run large models with full context windows";
40210
- else if (overall >= 60)
40211
- summary = "Strong \u2014 medium to large models with good context";
40212
- else if (overall >= 40)
40213
- summary = "Moderate \u2014 small to medium models, reasonable performance";
40214
- else if (overall >= 20)
40215
- summary = "Basic \u2014 small models only, limited context";
40216
- else
40217
- summary = "Minimal \u2014 very small models or cloud inference recommended";
40218
- return { overall, memory: memoryScore, compute: computeScore, speed: speedScore, summary, modelCompat };
40466
+ } catch {
40467
+ }
40468
+ return {
40469
+ totalRamGB: Math.round(totalRamGB * 10) / 10,
40470
+ availableRamGB: Math.round(availableRamGB * 10) / 10,
40471
+ gpuVramGB: Math.round(gpuVramGB * 10) / 10,
40472
+ gpuName
40473
+ };
40219
40474
  }
40220
- function renderScoreBar(score, width = 20) {
40221
- const filled = Math.round(score / 100 * width);
40222
- const empty = width - filled;
40223
- const color = score >= 70 ? c2.green : score >= 40 ? c2.yellow : c2.red;
40224
- return color("\u2588".repeat(filled)) + c2.dim("\u2591".repeat(empty));
40475
+ function recommendModel(specs) {
40476
+ const effectiveGB = Math.max(specs.gpuVramGB, specs.availableRamGB);
40477
+ const budget = effectiveGB * 0.8;
40478
+ const localVariants = QWEN_VARIANTS.filter((v) => !v.cloud);
40479
+ for (let i = localVariants.length - 1; i >= 0; i--) {
40480
+ if (localVariants[i].sizeGB <= budget) {
40481
+ return localVariants[i];
40482
+ }
40483
+ }
40484
+ return QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:cloud");
40225
40485
  }
40226
- function ensurePython3() {
40227
- process.stdout.write(` ${c2.cyan("\u25CF")} Installing Python3...
40228
- `);
40229
- const plat = platform2();
40230
- if (plat === "win32") {
40231
- process.stdout.write(` ${c2.dim("Download Python from https://python.org/downloads/")}
40232
-
40233
- `);
40234
- return;
40486
+ function calculateContextWindow(specs, modelSizeGB2, kvBytesPerToken, archMax) {
40487
+ const totalAvail = Math.max(specs.gpuVramGB, specs.totalRamGB);
40488
+ const remaining = Math.max(0, totalAvail - modelSizeGB2);
40489
+ const usableGB = remaining * 0.85;
40490
+ let numCtx;
40491
+ if (kvBytesPerToken && kvBytesPerToken > 0) {
40492
+ const maxTokens = Math.floor(usableGB * 1024 ** 3 / kvBytesPerToken);
40493
+ numCtx = Math.max(2048, Math.floor(maxTokens / 1024) * 1024);
40494
+ } else {
40495
+ const kvEstimate = modelSizeGB2 <= 5 ? 524288 : modelSizeGB2 <= 20 ? 1048576 : 1572864;
40496
+ const maxTokens = Math.floor(usableGB * 1024 ** 3 / kvEstimate);
40497
+ numCtx = Math.max(2048, Math.floor(maxTokens / 1024) * 1024);
40235
40498
  }
40236
- const strategies = [
40237
- { check: "apt-get", install: "sudo apt-get update -qq && sudo apt-get install -y -qq python3 python3-venv python3-pip", label: "apt" },
40238
- { check: "dnf", install: "sudo dnf install -y -q python3 python3-pip", label: "dnf" },
40239
- { check: "yum", install: "sudo yum install -y -q python3 python3-pip", label: "yum" },
40240
- { check: "pacman", install: "sudo pacman -S --noconfirm python python-pip", label: "pacman" },
40241
- { check: "apk", install: "sudo apk add --quiet python3 py3-pip", label: "apk" },
40242
- { check: "zypper", install: "sudo zypper install -y python3 python3-pip", label: "zypper" }
40243
- ];
40244
- if (plat === "darwin") {
40245
- if (hasCmd("brew")) {
40246
- try {
40247
- execSync27("brew install python3", { stdio: "inherit", timeout: 3e5 });
40248
- if (hasCmd("python3")) {
40249
- process.stdout.write(` ${c2.green("\u2714")} Python3 installed via Homebrew.
40250
- `);
40251
- return;
40252
- }
40253
- } catch {
40254
- }
40255
- }
40499
+ numCtx = Math.min(numCtx, 131072);
40500
+ if (archMax && archMax > 0)
40501
+ numCtx = Math.min(numCtx, archMax);
40502
+ if (kvBytesPerToken && kvBytesPerToken > 0 && modelSizeGB2 > 0) {
40503
+ const maxKVBytes = modelSizeGB2 * 4 * 1024 ** 3;
40504
+ const maxTokensFromBudget = Math.floor(maxKVBytes / kvBytesPerToken);
40505
+ const budgetCap = Math.max(2048, Math.floor(maxTokensFromBudget / 1024) * 1024);
40506
+ numCtx = Math.min(numCtx, budgetCap);
40256
40507
  }
40257
- for (const s of strategies) {
40258
- if (hasCmd(s.check)) {
40259
- try {
40260
- execSync27(s.install, { stdio: "inherit", timeout: 12e4 });
40261
- if (hasCmd("python3") || hasCmd("python")) {
40262
- process.stdout.write(` ${c2.green("\u2714")} Python3 installed via ${s.label}.
40263
- `);
40508
+ const label = numCtx >= 1024 ? `${Math.floor(numCtx / 1024)}K` : String(numCtx);
40509
+ return { numCtx, label };
40510
+ }
40511
+ function ask(rl, question) {
40512
+ return new Promise((resolve36) => {
40513
+ rl.question(question, (answer) => resolve36(answer.trim()));
40514
+ });
40515
+ }
40516
+ function askSecret(rl, question) {
40517
+ return new Promise((resolve36) => {
40518
+ process.stdout.write(question);
40519
+ let secret = "";
40520
+ const stdin = process.stdin;
40521
+ const hadRawMode = stdin.isRaw;
40522
+ if (typeof stdin.setRawMode === "function") {
40523
+ stdin.setRawMode(true);
40524
+ }
40525
+ stdin.resume();
40526
+ const onData = (chunk) => {
40527
+ const ch = chunk.toString("utf8");
40528
+ for (const c3 of ch) {
40529
+ if (c3 === "\r" || c3 === "\n") {
40530
+ stdin.removeListener("data", onData);
40531
+ if (typeof stdin.setRawMode === "function") {
40532
+ stdin.setRawMode(hadRawMode ?? false);
40533
+ }
40534
+ process.stdout.write("\n");
40535
+ resolve36(secret.trim());
40264
40536
  return;
40537
+ } else if (c3 === "") {
40538
+ stdin.removeListener("data", onData);
40539
+ if (typeof stdin.setRawMode === "function") {
40540
+ stdin.setRawMode(hadRawMode ?? false);
40541
+ }
40542
+ process.stdout.write("\n");
40543
+ resolve36("");
40544
+ return;
40545
+ } else if (c3 === "\x7F" || c3 === "\b") {
40546
+ if (secret.length > 0) {
40547
+ secret = secret.slice(0, -1);
40548
+ process.stdout.write("\b \b");
40549
+ }
40550
+ } else if (c3.charCodeAt(0) >= 32) {
40551
+ secret += c3;
40552
+ process.stdout.write("*");
40265
40553
  }
40266
- } catch {
40267
40554
  }
40268
- }
40269
- }
40270
- process.stdout.write(` ${c2.yellow("\u26A0")} Could not install Python3 automatically. Install manually.
40271
-
40272
- `);
40555
+ };
40556
+ stdin.on("data", onData);
40557
+ });
40273
40558
  }
40274
- function checkPythonVenv() {
40275
- try {
40276
- execSync27("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
40559
+ function ensureCurl() {
40560
+ if (hasCmd("curl"))
40277
40561
  return true;
40278
- } catch {
40279
- try {
40280
- execSync27("python -m venv --help", { stdio: "pipe", timeout: 5e3 });
40281
- return true;
40282
- } catch {
40283
- return false;
40284
- }
40562
+ const plat = platform2();
40563
+ if (plat === "win32") {
40564
+ process.stdout.write(` ${c2.yellow("\u26A0")} curl not found on Windows \u2014 install it manually.
40565
+ `);
40566
+ return false;
40285
40567
  }
40286
- }
40287
- function ensurePythonVenv() {
40288
- process.stdout.write(` ${c2.cyan("\u25CF")} Installing python3-venv...
40568
+ process.stdout.write(` ${c2.cyan("\u25CF")} curl not found. Installing...
40289
40569
  `);
40290
40570
  const strategies = [
40291
- { check: "apt-get", install: "sudo apt-get update -qq && sudo apt-get install -y -qq python3-venv", label: "apt" },
40292
- { check: "dnf", install: "sudo dnf install -y -q python3-libs", label: "dnf" },
40293
- { check: "pacman", install: "sudo pacman -S --noconfirm python", label: "pacman" },
40294
- { check: "apk", install: "sudo apk add --quiet python3", label: "apk" }
40571
+ { check: "apt-get", install: "sudo apt-get update -qq && sudo apt-get install -y -qq curl", label: "apt" },
40572
+ { check: "dnf", install: "sudo dnf install -y -q curl", label: "dnf" },
40573
+ { check: "yum", install: "sudo yum install -y -q curl", label: "yum" },
40574
+ { check: "pacman", install: "sudo pacman -S --noconfirm curl", label: "pacman" },
40575
+ { check: "apk", install: "sudo apk add --quiet curl", label: "apk" },
40576
+ { check: "zypper", install: "sudo zypper install -y curl", label: "zypper" }
40295
40577
  ];
40296
40578
  for (const s of strategies) {
40297
40579
  if (hasCmd(s.check)) {
40298
40580
  try {
40299
40581
  execSync27(s.install, { stdio: "inherit", timeout: 12e4 });
40300
- if (checkPythonVenv()) {
40301
- process.stdout.write(` ${c2.green("\u2714")} python3-venv installed via ${s.label}.
40582
+ if (hasCmd("curl")) {
40583
+ process.stdout.write(` ${c2.green("\u2714")} curl installed via ${s.label}.
40302
40584
  `);
40303
- return;
40585
+ return true;
40304
40586
  }
40305
40587
  } catch {
40306
- }
40307
- }
40308
- }
40309
- process.stdout.write(` ${c2.yellow("\u26A0")} Could not install python3-venv. Install manually: sudo apt install python3-venv
40310
-
40311
- `);
40312
- }
40313
- async function runSetupWizard(config) {
40314
- const rl = readline.createInterface({
40315
- input: process.stdin,
40316
- output: process.stdout,
40317
- terminal: true
40318
- });
40319
- try {
40320
- return await doSetup(config, rl);
40321
- } finally {
40322
- rl.close();
40323
- }
40324
- }
40325
- async function promptForCustomEndpoint(config, rl) {
40326
- process.stdout.write(`
40327
- ${c2.cyan("\u25CF")} Enter an OpenAI-compatible inference endpoint.
40328
- `);
40329
- process.stdout.write(` ${c2.dim("Examples:")}
40330
- `);
40331
- process.stdout.write(` ${c2.dim(" https://chutes.ai/v1")}
40332
- `);
40333
- process.stdout.write(` ${c2.dim(" http://10.0.0.5:11434")}
40334
- `);
40335
- process.stdout.write(` ${c2.dim(" https://api.together.xyz/v1")}
40336
-
40337
- `);
40338
- const endpoint = await ask(rl, ` ${c2.bold("Endpoint URL:")} `);
40339
- if (!endpoint) {
40340
- process.stdout.write(` ${c2.dim("No endpoint entered.")}
40341
- `);
40342
- const startAnyway = await ask(rl, `
40343
- ${c2.bold("Start anyway without a backend?")} (y/n) `);
40344
- if (startAnyway.toLowerCase() === "y" || startAnyway.toLowerCase() === "yes") {
40345
- return config.model;
40346
- }
40347
- return config.model;
40348
- }
40349
- const cleanUrl = normalizeBaseUrl(endpoint);
40350
- const needsKey = await ask(rl, `
40351
- ${c2.bold("Does this endpoint require an API key?")} (y/n) `);
40352
- let apiKey = "";
40353
- if (needsKey.toLowerCase() === "y" || needsKey.toLowerCase() === "yes") {
40354
- apiKey = await askSecret(rl, ` ${c2.bold("API key:")} `);
40355
- }
40356
- process.stdout.write(`
40357
- ${c2.cyan("\u25CF")} Enter the model name for this endpoint.
40358
- `);
40359
- process.stdout.write(` ${c2.dim("Examples: qwen3.5:122b, meta-llama/Llama-3.3-70B, etc.")}
40360
-
40361
- `);
40362
- const modelName = await ask(rl, ` ${c2.bold("Model name")} (Enter for ${c2.dim(config.model)}): `);
40363
- const chosenModel = modelName || config.model;
40364
- const provider = detectProvider(endpoint);
40365
- process.stdout.write(`
40366
- ${c2.cyan("\u25CF")} Detected provider: ${c2.bold(provider.label)}
40367
- `);
40368
- process.stdout.write(` ${c2.cyan("\u25CF")} Testing endpoint ${c2.bold(cleanUrl)}...
40369
- `);
40370
- let testOk = false;
40371
- try {
40372
- const testUrl = `${cleanUrl}${provider.modelsPath}`;
40373
- const headers = { "Content-Type": "application/json" };
40374
- if (apiKey) {
40375
- headers["Authorization"] = `Bearer ${apiKey}`;
40376
- }
40377
- const resp = await fetch(testUrl, { headers, signal: AbortSignal.timeout(1e4) });
40378
- if (resp.ok) {
40379
- process.stdout.write(` ${c2.green("\u2714")} Endpoint reachable.
40380
- `);
40381
- testOk = true;
40382
- } else {
40383
- if (provider.id !== "ollama") {
40384
- try {
40385
- const ollamaResp = await fetch(`${cleanUrl}/api/tags`, { signal: AbortSignal.timeout(1e4) });
40386
- if (ollamaResp.ok) {
40387
- process.stdout.write(` ${c2.green("\u2714")} Ollama endpoint detected.
40388
- `);
40389
- testOk = true;
40390
- }
40391
- } catch {
40392
- }
40393
- }
40394
- if (!testOk) {
40395
- process.stdout.write(` ${c2.yellow("\u26A0")} Endpoint returned HTTP ${resp.status}
40588
+ process.stdout.write(` ${c2.yellow("\u26A0")} ${s.label} install failed, trying next...
40396
40589
  `);
40397
40590
  }
40398
40591
  }
40399
- } catch (err) {
40400
- process.stdout.write(` ${c2.yellow("\u26A0")} Could not reach endpoint: ${err instanceof Error ? err.message : String(err)}
40401
- `);
40402
40592
  }
40403
- if (!testOk) {
40404
- if (provider.authRequired && !apiKey) {
40405
- process.stdout.write(` ${c2.dim(`${provider.label} typically requires an API key.`)}
40406
- `);
40593
+ if (plat === "darwin") {
40594
+ try {
40595
+ execSync27("xcode-select --install", { stdio: "inherit", timeout: 3e5 });
40596
+ if (hasCmd("curl"))
40597
+ return true;
40598
+ } catch {
40407
40599
  }
40408
- const startAnyway = await ask(rl, `
40409
- ${c2.bold("Endpoint unreachable. Start anyway?")} (y/n) `);
40410
- if (startAnyway.toLowerCase() !== "y" && startAnyway.toLowerCase() !== "yes") {
40411
- process.stdout.write(` ${c2.dim("You can configure the endpoint later with /endpoint")}
40412
-
40600
+ }
40601
+ process.stdout.write(` ${c2.red("\u2716")} Could not install curl. Install it manually and retry.
40413
40602
  `);
40414
- return config.model;
40415
- }
40603
+ return false;
40604
+ }
40605
+ async function autoInstallOllama(rl) {
40606
+ const plat = platform2();
40607
+ if (plat !== "win32" && !ensureCurl()) {
40608
+ return false;
40416
40609
  }
40417
- setConfigValue("backendUrl", cleanUrl);
40418
- setConfigValue("model", chosenModel);
40419
- if (apiKey) {
40420
- setConfigValue("apiKey", apiKey);
40610
+ if (plat === "linux") {
40611
+ return installOllamaLinux();
40612
+ } else if (plat === "darwin") {
40613
+ return await installOllamaMac(rl);
40614
+ } else if (plat === "win32") {
40615
+ return installOllamaWindows();
40421
40616
  }
40422
- const backendType = provider.id === "ollama" ? "ollama" : "vllm";
40423
- setConfigValue("backendType", backendType);
40424
- process.stdout.write(`
40425
- ${c2.green("\u2714")} Configured: ${c2.bold(chosenModel)} at ${c2.bold(cleanUrl)}
40426
- `);
40427
- process.stdout.write(` ${c2.green("\u2714")} Provider: ${c2.bold(provider.label)}
40428
- `);
40429
- if (apiKey)
40430
- process.stdout.write(` ${c2.green("\u2714")} API key saved.
40617
+ process.stdout.write(` ${c2.yellow("\u26A0")} Unsupported platform: ${plat}
40431
40618
  `);
40432
- process.stdout.write(` ${c2.green("\u2714")} Backend type: ${c2.bold(backendType)}
40619
+ process.stdout.write(` ${c2.dim("Visit https://ollama.com to install manually.")}
40433
40620
 
40434
40621
  `);
40435
- return chosenModel;
40622
+ return false;
40436
40623
  }
40437
- async function doSetup(config, rl) {
40438
- process.stdout.write(`
40439
- ${c2.bold(c2.cyan("open-agents"))}
40440
- `);
40441
- process.stdout.write(` ${c2.dim("\u2500".repeat(60))}
40442
- `);
40443
- process.stdout.write(` ${c2.bold("First-run setup")}
40444
-
40445
- `);
40446
- process.stdout.write(` ${c2.cyan("\u25CF")} Detecting system specs...
40447
- `);
40448
- const specs = detectSystemSpecs();
40449
- process.stdout.write(` ${c2.dim(" RAM:")} ${specs.totalRamGB.toFixed(1)} GB total, ${specs.availableRamGB.toFixed(1)} GB available
40450
- `);
40451
- if (specs.gpuVramGB > 0) {
40452
- process.stdout.write(` ${c2.dim(" GPU:")} ${specs.gpuName || "NVIDIA"} \u2014 ${specs.gpuVramGB.toFixed(1)} GB VRAM
40453
- `);
40454
- } else {
40455
- process.stdout.write(` ${c2.dim(" GPU:")} No NVIDIA GPU detected (CPU inference)
40456
- `);
40457
- }
40458
- const score = computeInferenceScore(specs);
40459
- const w = 24;
40460
- const bw = 30;
40624
+ function installOllamaLinux() {
40461
40625
  process.stdout.write(`
40462
- ${c2.dim("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510")}
40463
- `);
40464
- process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold(" Inference Capability")} ${c2.dim("\u2502")}
40465
- `);
40466
- process.stdout.write(` ${c2.dim("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524")}
40467
- `);
40468
- process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold("Overall")} ${c2.dim("\u2502")}
40469
- `);
40470
- process.stdout.write(` ${c2.dim("\u2502")} ${renderScoreBar(score.overall, bw)} ${c2.bold(String(score.overall).padStart(3))}${c2.dim("/100")} ${c2.dim("\u2502")}
40471
- `);
40472
- process.stdout.write(` ${c2.dim("\u2502")} ${c2.dim(score.summary.padEnd(46))} ${c2.dim("\u2502")}
40473
- `);
40474
- process.stdout.write(` ${c2.dim("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524")}
40475
- `);
40476
- process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold("Memory")} ${c2.dim(`${specs.totalRamGB.toFixed(0)} GB RAM` + (specs.gpuVramGB > 0 ? ` + ${specs.gpuVramGB.toFixed(0)} GB VRAM` : ""))}${" ".repeat(Math.max(0, 27 - (`${specs.totalRamGB.toFixed(0)} GB RAM` + (specs.gpuVramGB > 0 ? ` + ${specs.gpuVramGB.toFixed(0)} GB VRAM` : "")).length))} ${c2.dim("\u2502")}
40477
- `);
40478
- process.stdout.write(` ${c2.dim("\u2502")} ${renderScoreBar(score.memory, w)} ${String(score.memory).padStart(3)} ${c2.dim("\u2502")}
40479
- `);
40480
- process.stdout.write(` ${c2.dim("\u2502")} ${c2.dim("\u2502")}
40481
- `);
40482
- process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold("Compute")} ${c2.dim(specs.gpuVramGB > 0 ? specs.gpuName || "NVIDIA GPU" : "CPU only")}${" ".repeat(Math.max(0, 28 - (specs.gpuVramGB > 0 ? specs.gpuName || "NVIDIA GPU" : "CPU only").length))} ${c2.dim("\u2502")}
40483
- `);
40484
- process.stdout.write(` ${c2.dim("\u2502")} ${renderScoreBar(score.compute, w)} ${String(score.compute).padStart(3)} ${c2.dim("\u2502")}
40485
- `);
40486
- process.stdout.write(` ${c2.dim("\u2502")} ${c2.dim("\u2502")}
40487
- `);
40488
- process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold("Speed")} ${c2.dim(specs.gpuVramGB > 0 ? "GPU accelerated" : "CPU inference")}${" ".repeat(Math.max(0, 28 - (specs.gpuVramGB > 0 ? "GPU accelerated" : "CPU inference").length))} ${c2.dim("\u2502")}
40489
- `);
40490
- process.stdout.write(` ${c2.dim("\u2502")} ${renderScoreBar(score.speed, w)} ${String(score.speed).padStart(3)} ${c2.dim("\u2502")}
40491
- `);
40492
- process.stdout.write(` ${c2.dim("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524")}
40493
- `);
40494
- process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold(" Model Compatibility")} ${c2.dim("\u2502")}
40495
- `);
40496
- for (const compat of score.modelCompat) {
40497
- const icon = compat.fits ? c2.green("\u2714") : c2.red("\u2716");
40498
- const tag = compat.fits ? compat.tag : c2.dim(compat.tag);
40499
- const note = c2.dim(compat.note);
40500
- const visTag = compat.tag;
40501
- const visNote = compat.note;
40502
- const pad = Math.max(0, 44 - 2 - visTag.length - 1 - visNote.length);
40503
- process.stdout.write(` ${c2.dim("\u2502")} ${icon} ${tag} ${" ".repeat(pad)}${note} ${c2.dim("\u2502")}
40504
- `);
40505
- }
40506
- process.stdout.write(` ${c2.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518")}
40626
+ ${c2.cyan("\u25CF")} Installing Ollama via official install script...
40507
40627
 
40508
40628
  `);
40509
- let hasPython = hasCmd("python3") || hasCmd("python");
40510
- if (!hasPython) {
40511
- process.stdout.write(` ${c2.yellow("\u26A0")} Python3 not found (needed for vision, OCR, browser automation).
40629
+ try {
40630
+ execSync27("curl -fsSL https://ollama.com/install.sh | sh", {
40631
+ stdio: "inherit",
40632
+ timeout: 3e5
40633
+ });
40634
+ if (hasCmd("ollama")) {
40635
+ process.stdout.write(`
40636
+ ${c2.green("\u2714")} Ollama installed successfully.
40512
40637
  `);
40513
- const installPy = await ask(rl, ` ${c2.bold("Install Python3?")} (Y/n) `);
40514
- if (installPy.toLowerCase() !== "n") {
40515
- ensurePython3();
40516
- hasPython = hasCmd("python3") || hasCmd("python");
40638
+ return true;
40517
40639
  }
40518
- }
40519
- if (hasPython) {
40520
- const hasVenv = checkPythonVenv();
40521
- if (!hasVenv) {
40522
- process.stdout.write(` ${c2.yellow("\u26A0")} Python3 venv module not found (needed for Moondream, OCR, browser automation).
40640
+ process.stdout.write(`
40641
+ ${c2.yellow("\u26A0")} Install script ran but ollama not found on PATH.
40523
40642
  `);
40524
- const installVenv = await ask(rl, ` ${c2.bold("Install python3-venv?")} (Y/n) `);
40525
- if (installVenv.toLowerCase() !== "n") {
40526
- ensurePythonVenv();
40527
- }
40528
- }
40643
+ return false;
40644
+ } catch (err) {
40645
+ process.stdout.write(`
40646
+ ${c2.red("\u2716")} Ollama install failed: ${err instanceof Error ? err.message : String(err)}
40647
+ `);
40648
+ return false;
40529
40649
  }
40530
- let models = [];
40531
- let usingCustomEndpoint = false;
40532
- try {
40533
- models = await fetchOllamaModels(config.backendUrl);
40534
- } catch {
40535
- process.stdout.write(` ${c2.yellow("\u26A0")} Cannot reach Ollama at ${c2.bold(config.backendUrl)}
40650
+ }
40651
+ async function installOllamaMac(_rl) {
40652
+ if (!hasCmd("brew")) {
40653
+ process.stdout.write(`
40654
+ ${c2.cyan("\u25CF")} Homebrew not found. Installing automatically (needed for Ollama on macOS)...
40536
40655
 
40537
40656
  `);
40538
- const useCustom = await ask(rl, ` ${c2.bold("Use a custom inference endpoint instead?")} (Y/n) `);
40539
- if (useCustom.toLowerCase() === "y" || useCustom.toLowerCase() === "yes") {
40540
- const endpointResult = await promptForCustomEndpoint(config, rl);
40541
- if (endpointResult) {
40542
- return endpointResult;
40543
- }
40544
- usingCustomEndpoint = true;
40545
- } else {
40546
- const ollamaInstalled = hasCmd("ollama");
40547
- if (ollamaInstalled) {
40548
- process.stdout.write(`
40549
- ${c2.cyan("\u25CF")} Ollama is installed but not running. Starting automatically...
40550
- `);
40657
+ try {
40658
+ execSync27('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"', { stdio: "inherit", timeout: 6e5 });
40659
+ if (!hasCmd("brew")) {
40551
40660
  try {
40552
- const child = spawn19("ollama", ["serve"], { stdio: "ignore", detached: true });
40553
- child.unref();
40554
- await new Promise((resolve36) => setTimeout(resolve36, 3e3));
40555
- try {
40556
- models = await fetchOllamaModels(config.backendUrl);
40557
- process.stdout.write(` ${c2.green("\u2714")} Ollama is running.
40558
-
40559
- `);
40560
- } catch {
40561
- process.stdout.write(` ${c2.yellow("\u26A0")} Ollama started but not responding yet. It may need a moment.
40562
-
40563
- `);
40564
- }
40661
+ const brewPrefix = existsSync37("/opt/homebrew/bin/brew") ? "/opt/homebrew" : "/usr/local";
40662
+ process.env["PATH"] = `${brewPrefix}/bin:${process.env["PATH"]}`;
40565
40663
  } catch {
40566
- process.stdout.write(` ${c2.yellow("\u26A0")} Could not start Ollama. Try running ${c2.bold("ollama serve")} manually.
40567
-
40568
- `);
40569
40664
  }
40570
- } else {
40571
- const installOllama = await ask(rl, `
40572
- ${c2.bold("Install Ollama for local inference?")} (Y/n) `);
40573
- if (installOllama.toLowerCase() !== "n") {
40574
- const installed = await autoInstallOllama(rl);
40575
- if (installed) {
40576
- process.stdout.write(`
40577
- ${c2.cyan("\u25CF")} Starting ollama serve...
40578
- `);
40579
- try {
40580
- const child = spawn19("ollama", ["serve"], { stdio: "ignore", detached: true });
40581
- child.unref();
40582
- await new Promise((resolve36) => setTimeout(resolve36, 3e3));
40583
- try {
40584
- models = await fetchOllamaModels(config.backendUrl);
40585
- process.stdout.write(` ${c2.green("\u2714")} Ollama is running.
40586
-
40587
- `);
40588
- } catch {
40589
- process.stdout.write(` ${c2.yellow("\u26A0")} Ollama installed but not responding yet. Try ${c2.bold("ollama serve")} manually.
40590
-
40591
- `);
40592
- }
40593
- } catch {
40594
- process.stdout.write(` ${c2.yellow("\u26A0")} Ollama installed. Start it with: ${c2.bold("ollama serve")}
40595
-
40665
+ }
40666
+ if (hasCmd("brew")) {
40667
+ process.stdout.write(`
40668
+ ${c2.green("\u2714")} Homebrew installed.
40596
40669
  `);
40597
- }
40598
- }
40599
- } else {
40600
- process.stdout.write(`
40601
- ${c2.dim("You'll need to configure an inference endpoint.")}
40670
+ } else {
40671
+ process.stdout.write(`
40672
+ ${c2.yellow("\u26A0")} Homebrew install completed but brew not found on PATH.
40602
40673
  `);
40603
- process.stdout.write(` ${c2.dim("Use:")} ${c2.bold("/endpoint <url> --auth <key>")} ${c2.dim("once inside the TUI.")}
40674
+ process.stdout.write(` ${c2.dim('Try: eval "$(/opt/homebrew/bin/brew shellenv)"')}
40604
40675
 
40605
40676
  `);
40606
- return config.model;
40607
- }
40677
+ return false;
40608
40678
  }
40679
+ } catch (err) {
40680
+ process.stdout.write(`
40681
+ ${c2.red("\u2716")} Homebrew install failed: ${err instanceof Error ? err.message : String(err)}
40682
+ `);
40683
+ return false;
40609
40684
  }
40610
40685
  }
40611
- if (usingCustomEndpoint) {
40612
- return config.model;
40613
- }
40614
- const currentModel = findModel(models, config.model);
40615
- if (currentModel) {
40616
- process.stdout.write(` ${c2.green("\u2714")} Model ${c2.bold(currentModel.name)} is available.
40686
+ process.stdout.write(`
40687
+ ${c2.cyan("\u25CF")} Installing Ollama via Homebrew...
40617
40688
 
40618
40689
  `);
40619
- return currentModel.name;
40620
- }
40621
- process.stdout.write(` ${c2.yellow("\u26A0")} Default model ${c2.bold(config.model)} is not available.
40622
-
40690
+ try {
40691
+ execSync27("brew install ollama", {
40692
+ stdio: "inherit",
40693
+ timeout: 3e5
40694
+ });
40695
+ if (hasCmd("ollama")) {
40696
+ process.stdout.write(`
40697
+ ${c2.green("\u2714")} Ollama installed successfully.
40623
40698
  `);
40624
- if (models.length > 0) {
40625
- const backendUrl = config.backendUrl || "http://localhost:11434";
40626
- const modelChecks = await Promise.all(models.slice(0, 15).map(async (m) => ({
40627
- ...m,
40628
- hasTools: await checkToolSupport(m.name, backendUrl)
40629
- })));
40630
- process.stdout.write(` ${c2.cyan("\u25CF")} Available models:
40699
+ return true;
40700
+ }
40701
+ process.stdout.write(`
40702
+ ${c2.yellow("\u26A0")} brew install completed but ollama not found on PATH.
40703
+ `);
40704
+ return false;
40705
+ } catch (err) {
40706
+ process.stdout.write(`
40707
+ ${c2.red("\u2716")} Ollama install failed: ${err instanceof Error ? err.message : String(err)}
40708
+ `);
40709
+ return false;
40710
+ }
40711
+ }
40712
+ function installOllamaWindows() {
40713
+ process.stdout.write(`
40714
+ ${c2.cyan("\u25CF")} Installing Ollama via PowerShell...
40631
40715
 
40632
40716
  `);
40633
- for (let i = 0; i < modelChecks.length; i++) {
40634
- const m = modelChecks[i];
40635
- const toolTag = m.hasTools ? c2.green("tools \u2713") : c2.yellow("text mode");
40636
- process.stdout.write(` ${c2.bold(String(i + 1))}. ${m.name} ${c2.dim(`(${m.size})`)} ${toolTag}
40717
+ try {
40718
+ execSync27('powershell -Command "irm https://ollama.com/install.ps1 | iex"', {
40719
+ stdio: "inherit",
40720
+ timeout: 3e5
40721
+ });
40722
+ if (hasCmd("ollama")) {
40723
+ process.stdout.write(`
40724
+ ${c2.green("\u2714")} Ollama installed successfully.
40637
40725
  `);
40726
+ return true;
40638
40727
  }
40639
40728
  process.stdout.write(`
40640
- ${c2.dim("0")}. Pull a new qwen3.5 model instead
40729
+ ${c2.yellow("\u26A0")} Install script ran but ollama not found on PATH.
40641
40730
  `);
40731
+ return false;
40732
+ } catch (err) {
40642
40733
  process.stdout.write(`
40643
- ${c2.dim("Models marked 'text mode' work via instruct-and-parse (no native tool API needed)")}
40734
+ ${c2.red("\u2716")} Ollama install failed: ${err instanceof Error ? err.message : String(err)}
40644
40735
  `);
40645
- process.stdout.write("\n");
40646
- const choice = await ask(rl, ` ${c2.bold("Select a model")} (1-${modelChecks.length}, or 0 to pull new): `);
40647
- const idx = parseInt(choice, 10);
40648
- if (idx > 0 && idx <= modelChecks.length) {
40649
- const selected = modelChecks[idx - 1];
40650
- setConfigValue("model", selected.name);
40651
- const mode = selected.hasTools ? "native tool calling" : "text mode (instruct-and-parse)";
40736
+ return false;
40737
+ }
40738
+ }
40739
+ async function ensureOllamaRunning(backendUrl, rl) {
40740
+ const ollamaInstalled = hasCmd("ollama");
40741
+ if (!ollamaInstalled) {
40742
+ if (rl) {
40652
40743
  process.stdout.write(`
40653
- ${c2.green("\u2714")} Selected ${c2.bold(selected.name)} \u2014 ${mode}. Saved to config.
40654
-
40744
+ ${c2.yellow("\u26A0")} Ollama is not installed on this system.
40655
40745
  `);
40656
- return selected.name;
40657
- }
40658
- } else {
40659
- process.stdout.write(` ${c2.yellow("\u26A0")} No models found on this system.
40660
-
40746
+ const answer = await ask(rl, ` ${c2.bold("Install Ollama now?")} (Y/n) `);
40747
+ if (answer.toLowerCase() === "n") {
40748
+ return false;
40749
+ }
40750
+ } else {
40751
+ process.stdout.write(`
40752
+ ${c2.cyan("\u25CF")} Ollama not found. Installing automatically...
40661
40753
  `);
40662
- }
40663
- const recommended = recommendModel(specs);
40664
- process.stdout.write(` ${c2.cyan("\u25CF")} Recommended model based on your system:
40754
+ }
40755
+ const installRl = rl ?? readline.createInterface({ input: process.stdin, output: process.stdout });
40756
+ const installed = await autoInstallOllama(installRl);
40757
+ if (!rl)
40758
+ installRl.close();
40759
+ if (!installed) {
40760
+ process.stdout.write(` ${c2.red("\u2716")} Ollama installation failed.
40665
40761
 
40666
40762
  `);
40667
- const localVariants = QWEN_VARIANTS.filter((v) => !v.cloud);
40668
- for (let i = 0; i < localVariants.length; i++) {
40669
- const v = localVariants[i];
40670
- const fits = v.sizeGB <= Math.max(specs.gpuVramGB, specs.availableRamGB) * 0.8;
40671
- const isRec = v.tag === recommended.tag;
40672
- const marker = isRec ? c2.green("\u2192") : fits ? c2.dim(" ") : c2.red("\u2716");
40673
- const name = isRec ? c2.bold(c2.green(v.tag)) : fits ? v.tag : c2.dim(v.tag);
40674
- const label = isRec ? c2.bold(v.label) : c2.dim(v.label);
40675
- const tooLarge = !fits && !v.cloud ? c2.red(" (exceeds available memory)") : "";
40676
- process.stdout.write(` ${marker} ${String(i + 1).padStart(2)}. ${name.padEnd(isRec ? 45 : 25)} ${label}${tooLarge}
40677
- `);
40763
+ return false;
40764
+ }
40678
40765
  }
40679
- process.stdout.write(`
40680
- ${c2.dim(" ")} ${String(localVariants.length + 1).padStart(2)}. ${c2.dim("qwen3.5:cloud")} ${c2.dim("Ollama Cloud")}
40766
+ process.stdout.write(` ${c2.cyan("\u25CF")} Starting ollama serve...
40681
40767
  `);
40682
- process.stdout.write(` ${c2.dim(" ")} ${String(localVariants.length + 2).padStart(2)}. ${c2.dim("qwen3.5:397b-cloud")} ${c2.dim("397B Ollama Cloud")}
40768
+ try {
40769
+ const child = spawn19("ollama", ["serve"], { stdio: "ignore", detached: true });
40770
+ child.unref();
40771
+ } catch {
40772
+ process.stdout.write(` ${c2.yellow("\u26A0")} Could not start ollama serve.
40773
+
40683
40774
  `);
40684
- process.stdout.write("\n");
40685
- const pullChoice = await ask(rl, ` ${c2.bold("Select a model to pull")} (1-${localVariants.length + 2}, or Enter for recommended): `);
40686
- const pullIdx = pullChoice ? parseInt(pullChoice, 10) : 0;
40687
- let selectedVariant;
40688
- if (pullIdx === 0 || isNaN(pullIdx)) {
40689
- selectedVariant = recommended;
40690
- } else if (pullIdx <= localVariants.length) {
40691
- selectedVariant = localVariants[pullIdx - 1];
40692
- } else if (pullIdx === localVariants.length + 1) {
40693
- selectedVariant = QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:cloud");
40694
- } else {
40695
- selectedVariant = QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:397b-cloud");
40775
+ return false;
40696
40776
  }
40697
- const confirmPull = await ask(rl, `
40698
- Pull ${c2.bold(selectedVariant.tag)} (${selectedVariant.label})? (Y/n) `);
40699
- if (confirmPull.toLowerCase() === "n") {
40700
- process.stdout.write(`
40701
- ${c2.dim("Skipping model pull. You can pull manually with: ollama pull <model>")}
40777
+ for (let i = 0; i < 5; i++) {
40778
+ await new Promise((resolve36) => setTimeout(resolve36, 2e3));
40779
+ try {
40780
+ const resp = await fetch(`${backendUrl}/api/tags`, {
40781
+ signal: AbortSignal.timeout(3e3)
40782
+ });
40783
+ if (resp.ok) {
40784
+ process.stdout.write(` ${c2.green("\u2714")} Ollama is running.
40702
40785
 
40703
40786
  `);
40704
- return config.model;
40787
+ return true;
40788
+ }
40789
+ } catch {
40790
+ }
40705
40791
  }
40706
- process.stdout.write(`
40707
- ${c2.cyan("\u25CF")} Pulling ${c2.bold(selectedVariant.tag)}... (this may take a while)
40708
- `);
40709
- try {
40710
- pullModelWithAutoUpdate(selectedVariant.tag);
40711
- process.stdout.write(`
40712
- ${c2.green("\u2714")} Model ${c2.bold(selectedVariant.tag)} pulled successfully.
40792
+ process.stdout.write(` ${c2.yellow("\u26A0")} Ollama started but not responding. Try ${c2.bold("ollama serve")} manually.
40713
40793
 
40714
40794
  `);
40795
+ return false;
40796
+ }
40797
+ function pullModelWithAutoUpdate(tag) {
40798
+ try {
40799
+ execSync27(`ollama pull ${tag}`, {
40800
+ stdio: "inherit",
40801
+ timeout: 36e5
40802
+ // 1 hour max
40803
+ });
40715
40804
  } catch (err) {
40716
- renderError(`Failed to pull model: ${err instanceof Error ? err.message : String(err)}`);
40717
- renderInfo("Try manually: ollama pull " + selectedVariant.tag);
40718
- return config.model;
40719
- }
40720
- if (!selectedVariant.cloud) {
40721
- const ctx = calculateContextWindow(specs, selectedVariant.sizeGB);
40722
- const customName = `open-agents-${selectedVariant.tag.replace(":", "-").replace(".", "")}`;
40723
- process.stdout.write(` ${c2.cyan("\u25CF")} Context window recommendation: ${c2.bold(ctx.label)} (${ctx.numCtx} tokens)
40805
+ const errMsg = err instanceof Error ? err.message : String(err);
40806
+ const stderr = err?.stderr?.toString?.() ?? errMsg;
40807
+ const combined = errMsg + "\n" + stderr;
40808
+ if (combined.includes("412") || combined.includes("newer version") || combined.includes("requires a newer version")) {
40809
+ process.stdout.write(`
40810
+ ${c2.yellow("\u26A0")} Ollama needs to be updated for this model.
40724
40811
  `);
40725
- process.stdout.write(` ${c2.dim(`Based on ${specs.totalRamGB.toFixed(0)} GB RAM, ${selectedVariant.sizeGB} GB model`)}
40812
+ if (!ensureCurl()) {
40813
+ throw new Error("curl is required to update Ollama but could not be installed.");
40814
+ }
40815
+ process.stdout.write(` ${c2.cyan("\u25CF")} Updating Ollama via official install script...
40726
40816
 
40727
40817
  `);
40728
- const createModelfile = await ask(rl, ` Create optimized model "${c2.bold(customName)}" with ${ctx.label} context? (Y/n) `);
40729
- if (createModelfile.toLowerCase() !== "n") {
40730
40818
  try {
40731
- const numPredict = Math.min(16384, Math.max(2048, Math.floor(ctx.numCtx * 0.25)));
40732
- const modelfileContent = [
40733
- `FROM ${selectedVariant.tag}`,
40734
- `PARAMETER num_ctx ${ctx.numCtx}`,
40735
- `PARAMETER temperature 0`,
40736
- `PARAMETER num_predict ${numPredict}`,
40737
- `PARAMETER stop "<|endoftext|>"`
40738
- ].join("\n");
40739
- const modelDir2 = join54(homedir13(), ".open-agents", "models");
40740
- mkdirSync15(modelDir2, { recursive: true });
40741
- const modelfilePath = join54(modelDir2, `Modelfile.${customName}`);
40742
- writeFileSync16(modelfilePath, modelfileContent + "\n", "utf8");
40743
- process.stdout.write(` ${c2.dim("Creating model...")} `);
40744
- execSync27(`ollama create ${customName} -f ${modelfilePath}`, {
40745
- stdio: "pipe",
40746
- timeout: 12e4
40819
+ execSync27("curl -fsSL https://ollama.com/install.sh | sh", {
40820
+ stdio: "inherit",
40821
+ timeout: 3e5
40822
+ // 5 min max for install
40747
40823
  });
40748
- process.stdout.write(`${c2.green("\u2714")}
40749
- `);
40750
- setConfigValue("model", customName);
40751
40824
  process.stdout.write(`
40752
- ${c2.green("\u2714")} Model ${c2.bold(customName)} created with ${ctx.label} context.
40825
+ ${c2.green("\u2714")} Ollama updated successfully.
40753
40826
  `);
40754
- process.stdout.write(` ${c2.green("\u2714")} Saved as default model in config.
40827
+ process.stdout.write(` ${c2.cyan("\u25CF")} Retrying pull of ${c2.bold(tag)}...
40755
40828
 
40756
40829
  `);
40757
- return customName;
40758
- } catch (err) {
40759
- renderWarning(`Could not create custom model: ${err instanceof Error ? err.message : String(err)}`);
40760
- renderInfo(`Using base model ${selectedVariant.tag} instead.`);
40830
+ execSync27(`ollama pull ${tag}`, {
40831
+ stdio: "inherit",
40832
+ timeout: 36e5
40833
+ });
40834
+ } catch (updateErr) {
40835
+ const updateMsg = updateErr instanceof Error ? updateErr.message : String(updateErr);
40836
+ throw new Error(`Failed to update Ollama and retry pull: ${updateMsg}
40837
+ Try manually:
40838
+ curl -fsSL https://ollama.com/install.sh | sh
40839
+ ollama pull ${tag}`);
40761
40840
  }
40762
- }
40763
- setConfigValue("model", selectedVariant.tag);
40764
- process.stdout.write(`
40765
- ${c2.green("\u2714")} Saved ${c2.bold(selectedVariant.tag)} as default model.
40766
-
40767
- `);
40768
- return selectedVariant.tag;
40769
- }
40770
- setConfigValue("model", selectedVariant.tag);
40771
- process.stdout.write(`
40772
- ${c2.green("\u2714")} Saved ${c2.bold(selectedVariant.tag)} as default model.
40773
-
40774
- `);
40775
- return selectedVariant.tag;
40776
- }
40777
- async function isModelAvailable(config) {
40778
- try {
40779
- const models = await fetchModels(config.backendUrl, config.apiKey);
40780
- return !!findModel(models, config.model);
40781
- } catch {
40782
- return false;
40783
- }
40784
- }
40785
- function isFirstRun() {
40786
- try {
40787
- return !existsSync37(join54(homedir13(), ".open-agents", "config.json"));
40788
- } catch {
40789
- return true;
40790
- }
40791
- }
40792
- function hasCmd(cmd) {
40793
- try {
40794
- const whichCmd = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
40795
- execSync27(whichCmd, { stdio: "pipe", timeout: 3e3 });
40796
- return true;
40797
- } catch {
40798
- return false;
40799
- }
40800
- }
40801
- function detectPkgManager() {
40802
- const plat = process.platform;
40803
- if (plat === "win32") {
40804
- if (hasCmd("choco"))
40805
- return "choco";
40806
- return null;
40807
- }
40808
- if (plat === "darwin") {
40809
- if (hasCmd("brew"))
40810
- return "brew";
40811
- return null;
40812
- }
40813
- if (hasCmd("apt-get"))
40814
- return "apt";
40815
- if (hasCmd("dnf"))
40816
- return "dnf";
40817
- if (hasCmd("pacman"))
40818
- return "pacman";
40819
- if (hasCmd("brew"))
40820
- return "brew";
40821
- return null;
40822
- }
40823
- function getVenvDir() {
40824
- return join54(homedir13(), ".open-agents", "venv");
40825
- }
40826
- function hasVenvModule() {
40827
- try {
40828
- execSync27("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
40829
- return true;
40830
- } catch {
40831
- return false;
40832
- }
40833
- }
40834
- function ensureVenv(log) {
40835
- const venvDir = getVenvDir();
40836
- const isWin2 = process.platform === "win32";
40837
- const pipPath = isWin2 ? join54(venvDir, "Scripts", "pip.exe") : join54(venvDir, "bin", "pip");
40838
- const pythonCmd = isWin2 ? "python" : "python3";
40839
- if (existsSync37(pipPath))
40840
- return venvDir;
40841
- log("Creating Python venv for vision deps...");
40842
- if (!hasCmd(pythonCmd) && !hasCmd("python3")) {
40843
- log(`${pythonCmd} not found \u2014 cannot create venv.`);
40844
- return null;
40845
- }
40846
- if (!isWin2 && !hasVenvModule()) {
40847
- log("python3 venv module not available \u2014 venv creation skipped.");
40848
- return null;
40849
- }
40850
- try {
40851
- mkdirSync15(join54(homedir13(), ".open-agents"), { recursive: true });
40852
- const pyCmd = hasCmd(pythonCmd) ? pythonCmd : "python3";
40853
- execSync27(`${pyCmd} -m venv "${venvDir}"`, { stdio: "pipe", timeout: 3e4 });
40854
- execSync27(`"${pipPath}" install --upgrade pip`, {
40855
- stdio: "pipe",
40856
- timeout: 6e4
40857
- });
40858
- log("Python venv created at ~/.open-agents/venv");
40859
- return venvDir;
40860
- } catch (err) {
40861
- log(`Failed to create venv: ${err instanceof Error ? err.message : String(err)}`);
40862
- return null;
40863
- }
40864
- }
40865
- function trySudoPasswordless(cmd, timeoutMs = 12e4) {
40866
- try {
40867
- execSync27(`sudo -n ${cmd}`, {
40868
- stdio: "pipe",
40869
- timeout: timeoutMs,
40870
- env: { ...process.env, DEBIAN_FRONTEND: "noninteractive" }
40871
- });
40872
- return true;
40873
- } catch {
40874
- return false;
40875
- }
40876
- }
40877
- function runWithSudo(cmd, password, timeoutMs = 12e4) {
40878
- try {
40879
- const escaped = cmd.replace(/'/g, "'\\''");
40880
- execSync27(`sudo -S bash -c '${escaped}'`, {
40881
- input: password + "\n",
40882
- stdio: ["pipe", "pipe", "pipe"],
40883
- timeout: timeoutMs,
40884
- env: { ...process.env, DEBIAN_FRONTEND: "noninteractive" }
40885
- });
40886
- return "ok";
40887
- } catch (err) {
40888
- const stderr = err?.stderr?.toString() ?? "";
40889
- if (/incorrect password|authentication failure|Sorry, try again|not in the sudoers/i.test(stderr)) {
40890
- return "auth_fail";
40891
- }
40892
- return "cmd_fail";
40893
- }
40894
- }
40895
- function validateSudoPassword(password) {
40896
- return runWithSudo("true", password, 1e4) === "ok";
40897
- }
40898
- async function acquireSudoPassword(getSudoPassword, log, cachedPasswordRef) {
40899
- if (cachedPasswordRef.value && validateSudoPassword(cachedPasswordRef.value)) {
40900
- return cachedPasswordRef.value;
40901
- }
40902
- if (trySudoPasswordless("true", 1e4)) {
40903
- return "";
40904
- }
40905
- for (let attempt = 0; attempt < 2; attempt++) {
40906
- if (attempt > 0)
40907
- log("Authentication failed \u2014 please re-enter password.");
40908
- const pw = await getSudoPassword();
40909
- if (!pw)
40910
- return null;
40911
- if (validateSudoPassword(pw)) {
40912
- cachedPasswordRef.value = pw;
40913
- return pw;
40914
- }
40915
- }
40916
- return null;
40917
- }
40918
- async function sudoInstall(cmd, getSudoPassword, log, cachedPasswordRef, timeoutMs = 12e4) {
40919
- if (trySudoPasswordless(cmd, timeoutMs))
40920
- return true;
40921
- const pw = await acquireSudoPassword(getSudoPassword, log, cachedPasswordRef);
40922
- if (pw === null)
40923
- return false;
40924
- if (pw === "")
40925
- return false;
40926
- const result = runWithSudo(cmd, pw, timeoutMs);
40927
- if (result === "ok")
40928
- return true;
40929
- if (result === "cmd_fail") {
40930
- log(`Install command failed (not an auth issue) \u2014 package may not be available for this OS.`);
40931
- }
40932
- return false;
40933
- }
40934
- async function ensureVisionDeps(onInfo, getSudoPassword) {
40935
- const log = onInfo ?? (() => {
40936
- });
40937
- const cachedPasswordRef = { value: null };
40938
- const getPassword = getSudoPassword ?? (() => Promise.resolve(null));
40939
- const allDeps = [
40940
- { binary: "tesseract", label: "tesseract-ocr", pkgs: { apt: "tesseract-ocr", dnf: "tesseract", pacman: "tesseract", brew: "tesseract", choco: "tesseract" } },
40941
- { binary: "pdftotext", label: "poppler-utils", pkgs: { apt: "poppler-utils", dnf: "poppler-utils", pacman: "poppler", brew: "poppler", choco: "poppler" } },
40942
- { binary: "gs", label: "ghostscript", pkgs: { apt: "ghostscript", dnf: "ghostscript", pacman: "ghostscript", brew: "ghostscript", choco: "ghostscript" } },
40943
- { binary: "ocrmypdf", label: "ocrmypdf", pkgs: { apt: "ocrmypdf", dnf: "ocrmypdf", pacman: "ocrmypdf", brew: "ocrmypdf", choco: "ocrmypdf" } }
40944
- ];
40945
- {
40946
- const pm2 = detectPkgManager();
40947
- const missing = allDeps.filter((d) => !hasCmd(d.binary));
40948
- if (missing.length === 0) {
40949
- } else if (!pm2) {
40950
- for (const d of missing)
40951
- log(`No supported package manager \u2014 ${d.label} unavailable.`);
40952
40841
  } else {
40953
- const pkgNames = missing.map((d) => d.pkgs[pm2]).filter(Boolean);
40954
- if (pkgNames.length === 0) {
40955
- for (const d of missing)
40956
- log(`${d.label} not available for ${pm2}.`);
40957
- } else {
40958
- const labels = missing.map((d) => d.label).join(", ");
40959
- log(`Installing ${labels}...`);
40960
- const needsSudo = pm2 !== "brew" && pm2 !== "choco";
40961
- let batchCmd;
40962
- switch (pm2) {
40963
- case "apt":
40964
- batchCmd = `apt-get update -qq && apt-get install -y -qq ${pkgNames.join(" ")}`;
40965
- break;
40966
- case "dnf":
40967
- batchCmd = `dnf install -y -q ${pkgNames.join(" ")}`;
40968
- break;
40969
- case "pacman":
40970
- batchCmd = `pacman -S --noconfirm ${pkgNames.join(" ")}`;
40971
- break;
40972
- case "brew":
40973
- batchCmd = `brew install ${pkgNames.join(" ")}`;
40974
- break;
40975
- case "choco":
40976
- batchCmd = `choco install -y ${pkgNames.join(" ")}`;
40977
- break;
40978
- default:
40979
- batchCmd = `echo "unsupported pm: ${pm2}" && exit 1`;
40980
- break;
40981
- }
40982
- let ok = false;
40983
- if (needsSudo) {
40984
- ok = await sudoInstall(batchCmd, getPassword, log, cachedPasswordRef, 18e4);
40985
- } else {
40986
- try {
40987
- execSync27(batchCmd, { stdio: "pipe", timeout: 18e4 });
40988
- ok = true;
40989
- } catch {
40990
- ok = false;
40991
- }
40992
- }
40993
- for (const d of missing) {
40994
- if (hasCmd(d.binary)) {
40995
- log(`${d.label} installed.`);
40996
- } else if (!ok) {
40997
- log(`${d.label} could not be installed \u2014 features will be limited.`);
40998
- } else {
40999
- log(`${d.label} install completed but binary not found.`);
41000
- }
41001
- }
41002
- }
40842
+ throw err;
41003
40843
  }
41004
40844
  }
41005
- const pm = detectPkgManager();
41006
- if (!hasCmd("pip3") && !hasCmd("pip") && pm) {
41007
- const pipCmds = {
41008
- apt: "apt-get install -y python3-pip",
41009
- dnf: "dnf install -y python3-pip"
41010
- };
41011
- const pipCmd = pipCmds[pm];
41012
- if (pipCmd) {
41013
- log("Installing python3-pip...");
41014
- const ok = await sudoInstall(pipCmd, getPassword, log, cachedPasswordRef);
41015
- if (!ok) {
41016
- log("python3-pip could not be installed \u2014 moondream-station may be unavailable.");
40845
+ }
40846
+ function computeInferenceScore(specs) {
40847
+ const effectiveGB = Math.max(specs.gpuVramGB, specs.availableRamGB);
40848
+ const memoryScore = Math.min(100, Math.round(effectiveGB <= 4 ? effectiveGB * 2.5 : effectiveGB <= 16 ? 10 + (effectiveGB - 4) * 2.5 : effectiveGB <= 48 ? 40 + (effectiveGB - 16) * 0.94 : effectiveGB <= 128 ? 70 + (effectiveGB - 48) * 0.375 : 100));
40849
+ let computeScore;
40850
+ if (specs.gpuVramGB >= 24)
40851
+ computeScore = 90;
40852
+ else if (specs.gpuVramGB >= 12)
40853
+ computeScore = 70;
40854
+ else if (specs.gpuVramGB >= 8)
40855
+ computeScore = 55;
40856
+ else if (specs.gpuVramGB >= 4)
40857
+ computeScore = 35;
40858
+ else if (specs.totalRamGB >= 32)
40859
+ computeScore = 25;
40860
+ else
40861
+ computeScore = Math.max(5, Math.round(specs.totalRamGB * 1.5));
40862
+ let speedScore;
40863
+ if (specs.gpuVramGB >= 48)
40864
+ speedScore = 95;
40865
+ else if (specs.gpuVramGB >= 24)
40866
+ speedScore = 80;
40867
+ else if (specs.gpuVramGB >= 12)
40868
+ speedScore = 65;
40869
+ else if (specs.gpuVramGB >= 8)
40870
+ speedScore = 50;
40871
+ else if (specs.gpuVramGB > 0)
40872
+ speedScore = 35;
40873
+ else
40874
+ speedScore = Math.min(30, Math.round(specs.totalRamGB * 0.6));
40875
+ const overall = Math.round(memoryScore * 0.45 + computeScore * 0.35 + speedScore * 0.2);
40876
+ const modelCompat = QWEN_VARIANTS.filter((v) => !v.cloud).map((v) => {
40877
+ const budget = effectiveGB * 0.8;
40878
+ const fits = v.sizeGB <= budget;
40879
+ let note;
40880
+ if (fits) {
40881
+ const headroom = budget - v.sizeGB;
40882
+ note = headroom > v.sizeGB * 0.5 ? "comfortable" : headroom > v.sizeGB * 0.15 ? "good fit" : "tight fit";
40883
+ if (specs.gpuVramGB > 0 && v.sizeGB <= specs.gpuVramGB * 0.8) {
40884
+ note += " (GPU accelerated)";
41017
40885
  }
40886
+ } else {
40887
+ const overshoot = v.sizeGB - budget;
40888
+ note = `need ${overshoot.toFixed(0)} GB more`;
41018
40889
  }
40890
+ return { tag: v.tag, fits, note };
40891
+ });
40892
+ let summary;
40893
+ if (overall >= 80)
40894
+ summary = "Excellent \u2014 can run large models with full context windows";
40895
+ else if (overall >= 60)
40896
+ summary = "Strong \u2014 medium to large models with good context";
40897
+ else if (overall >= 40)
40898
+ summary = "Moderate \u2014 small to medium models, reasonable performance";
40899
+ else if (overall >= 20)
40900
+ summary = "Basic \u2014 small models only, limited context";
40901
+ else
40902
+ summary = "Minimal \u2014 very small models or cloud inference recommended";
40903
+ return { overall, memory: memoryScore, compute: computeScore, speed: speedScore, summary, modelCompat };
40904
+ }
40905
+ function renderScoreBar(score, width = 20) {
40906
+ const filled = Math.round(score / 100 * width);
40907
+ const empty = width - filled;
40908
+ const color = score >= 70 ? c2.green : score >= 40 ? c2.yellow : c2.red;
40909
+ return color("\u2588".repeat(filled)) + c2.dim("\u2591".repeat(empty));
40910
+ }
40911
+ function ensurePython3() {
40912
+ process.stdout.write(` ${c2.cyan("\u25CF")} Installing Python3...
40913
+ `);
40914
+ const plat = platform2();
40915
+ if (plat === "win32") {
40916
+ process.stdout.write(` ${c2.dim("Download Python from https://python.org/downloads/")}
40917
+
40918
+ `);
40919
+ return;
41019
40920
  }
41020
- if (hasCmd("python3") && !hasVenvModule() && pm) {
41021
- const venvCmds = {
41022
- apt: () => {
41023
- try {
41024
- const pyVer = execSync27(`python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"`, { encoding: "utf8", stdio: "pipe", timeout: 5e3 }).trim();
41025
- return `apt-get install -y python3-venv python${pyVer}-venv`;
41026
- } catch {
41027
- return "apt-get install -y python3-venv";
41028
- }
41029
- },
41030
- dnf: () => "dnf install -y python3-venv"
41031
- };
41032
- const cmdFn = venvCmds[pm];
41033
- if (cmdFn) {
41034
- const cmd = cmdFn();
41035
- if (cmd) {
41036
- log("Installing python3-venv...");
41037
- const ok = await sudoInstall(cmd, getPassword, log, cachedPasswordRef);
41038
- if (!ok) {
41039
- log("python3-venv could not be installed \u2014 moondream-station may be unavailable.");
40921
+ const strategies = [
40922
+ { check: "apt-get", install: "sudo apt-get update -qq && sudo apt-get install -y -qq python3 python3-venv python3-pip", label: "apt" },
40923
+ { check: "dnf", install: "sudo dnf install -y -q python3 python3-pip", label: "dnf" },
40924
+ { check: "yum", install: "sudo yum install -y -q python3 python3-pip", label: "yum" },
40925
+ { check: "pacman", install: "sudo pacman -S --noconfirm python python-pip", label: "pacman" },
40926
+ { check: "apk", install: "sudo apk add --quiet python3 py3-pip", label: "apk" },
40927
+ { check: "zypper", install: "sudo zypper install -y python3 python3-pip", label: "zypper" }
40928
+ ];
40929
+ if (plat === "darwin") {
40930
+ if (hasCmd("brew")) {
40931
+ try {
40932
+ execSync27("brew install python3", { stdio: "inherit", timeout: 3e5 });
40933
+ if (hasCmd("python3")) {
40934
+ process.stdout.write(` ${c2.green("\u2714")} Python3 installed via Homebrew.
40935
+ `);
40936
+ return;
41040
40937
  }
40938
+ } catch {
41041
40939
  }
41042
40940
  }
41043
40941
  }
41044
- const venvDir = getVenvDir();
41045
- const venvBin = join54(venvDir, "bin");
41046
- const venvMoondream = join54(venvBin, "moondream-station");
41047
- const venv = ensureVenv(log);
41048
- if (venv && !hasCmd("moondream-station") && !existsSync37(venvMoondream)) {
41049
- const venvPip2 = join54(venvBin, "pip");
41050
- log("Installing moondream-station in ~/.open-agents/venv...");
41051
- try {
41052
- execSync27(`"${venvPip2}" install moondream-station`, { stdio: "pipe", timeout: 3e5 });
41053
- if (existsSync37(venvMoondream)) {
41054
- log("moondream-station installed successfully.");
41055
- } else {
41056
- try {
41057
- const check = execSync27(`"${venvPip2}" show moondream-station`, { encoding: "utf8", stdio: "pipe", timeout: 5e3 });
41058
- if (check.includes("moondream")) {
41059
- log("moondream-station package installed.");
41060
- }
41061
- } catch {
41062
- log("moondream-station install completed.");
40942
+ for (const s of strategies) {
40943
+ if (hasCmd(s.check)) {
40944
+ try {
40945
+ execSync27(s.install, { stdio: "inherit", timeout: 12e4 });
40946
+ if (hasCmd("python3") || hasCmd("python")) {
40947
+ process.stdout.write(` ${c2.green("\u2714")} Python3 installed via ${s.label}.
40948
+ `);
40949
+ return;
41063
40950
  }
40951
+ } catch {
41064
40952
  }
41065
- } catch (err) {
41066
- log(`moondream-station install failed: ${err instanceof Error ? err.message : String(err)}`);
41067
40953
  }
41068
40954
  }
41069
- if (venv) {
41070
- const venvPython2 = join54(venvBin, "python");
41071
- const venvPip2 = join54(venvBin, "pip");
41072
- let ocrStackInstalled = false;
40955
+ process.stdout.write(` ${c2.yellow("\u26A0")} Could not install Python3 automatically. Install manually.
40956
+
40957
+ `);
40958
+ }
40959
+ function checkPythonVenv() {
40960
+ try {
40961
+ execSync27("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
40962
+ return true;
40963
+ } catch {
41073
40964
  try {
41074
- execSync27(`"${venvPython2}" -c "import cv2, pytesseract, numpy, PIL"`, { stdio: "pipe", timeout: 1e4 });
41075
- ocrStackInstalled = true;
40965
+ execSync27("python -m venv --help", { stdio: "pipe", timeout: 5e3 });
40966
+ return true;
41076
40967
  } catch {
40968
+ return false;
41077
40969
  }
41078
- if (!ocrStackInstalled) {
41079
- const ocrPackages = "pytesseract Pillow opencv-python-headless numpy";
41080
- log("Installing OCR Python stack (pytesseract, OpenCV, Pillow, numpy)...");
40970
+ }
40971
+ }
40972
+ function ensurePythonVenv() {
40973
+ process.stdout.write(` ${c2.cyan("\u25CF")} Installing python3-venv...
40974
+ `);
40975
+ const strategies = [
40976
+ { check: "apt-get", install: "sudo apt-get update -qq && sudo apt-get install -y -qq python3-venv", label: "apt" },
40977
+ { check: "dnf", install: "sudo dnf install -y -q python3-libs", label: "dnf" },
40978
+ { check: "pacman", install: "sudo pacman -S --noconfirm python", label: "pacman" },
40979
+ { check: "apk", install: "sudo apk add --quiet python3", label: "apk" }
40980
+ ];
40981
+ for (const s of strategies) {
40982
+ if (hasCmd(s.check)) {
41081
40983
  try {
41082
- execSync27(`"${venvPip2}" install ${ocrPackages}`, { stdio: "pipe", timeout: 3e5 });
41083
- try {
41084
- execSync27(`"${venvPython2}" -c "import cv2, pytesseract, numpy, PIL"`, { stdio: "pipe", timeout: 1e4 });
41085
- log("OCR Python stack installed successfully.");
41086
- } catch {
41087
- log("OCR Python stack install completed but import verification failed.");
40984
+ execSync27(s.install, { stdio: "inherit", timeout: 12e4 });
40985
+ if (checkPythonVenv()) {
40986
+ process.stdout.write(` ${c2.green("\u2714")} python3-venv installed via ${s.label}.
40987
+ `);
40988
+ return;
41088
40989
  }
41089
- } catch (err) {
41090
- log(`OCR Python stack install failed: ${err instanceof Error ? err.message : String(err)}`);
40990
+ } catch {
41091
40991
  }
41092
40992
  }
41093
- } else {
41094
- log("Python venv unavailable \u2014 advanced OCR pipeline will fall back to basic tesseract.");
41095
40993
  }
40994
+ process.stdout.write(` ${c2.yellow("\u26A0")} Could not install python3-venv. Install manually: sudo apt install python3-venv
40995
+
40996
+ `);
41096
40997
  }
41097
- function ensureCloudflaredBackground(onInfo) {
41098
- if (_cloudflaredInstallPromise)
41099
- return;
41100
- if (hasCmd("cloudflared")) {
41101
- _cloudflaredInstallPromise = Promise.resolve(true);
41102
- return;
41103
- }
41104
- const log = onInfo ?? (() => {
40998
+ async function runSetupWizard(config) {
40999
+ const rl = readline.createInterface({
41000
+ input: process.stdin,
41001
+ output: process.stdout,
41002
+ terminal: true
41105
41003
  });
41106
- log("Installing cloudflared for live voice sessions...");
41107
- _cloudflaredInstallPromise = (async () => {
41108
- const arch2 = process.arch;
41109
- const os = platform2();
41110
- if (os !== "win32" && !ensureCurl()) {
41111
- log("curl not available \u2014 cannot install cloudflared.");
41112
- return false;
41004
+ try {
41005
+ return await doSetup(config, rl);
41006
+ } finally {
41007
+ rl.close();
41008
+ }
41009
+ }
41010
+ async function promptForCustomEndpoint(config, rl) {
41011
+ process.stdout.write(`
41012
+ ${c2.cyan("\u25CF")} Enter an OpenAI-compatible inference endpoint.
41013
+ `);
41014
+ process.stdout.write(` ${c2.dim("Examples:")}
41015
+ `);
41016
+ process.stdout.write(` ${c2.dim(" https://chutes.ai/v1")}
41017
+ `);
41018
+ process.stdout.write(` ${c2.dim(" http://10.0.0.5:11434")}
41019
+ `);
41020
+ process.stdout.write(` ${c2.dim(" https://api.together.xyz/v1")}
41021
+
41022
+ `);
41023
+ const endpoint = await ask(rl, ` ${c2.bold("Endpoint URL:")} `);
41024
+ if (!endpoint) {
41025
+ process.stdout.write(` ${c2.dim("No endpoint entered.")}
41026
+ `);
41027
+ const startAnyway = await ask(rl, `
41028
+ ${c2.bold("Start anyway without a backend?")} (y/n) `);
41029
+ if (startAnyway.toLowerCase() === "y" || startAnyway.toLowerCase() === "yes") {
41030
+ return config.model;
41113
41031
  }
41114
- if (os === "linux") {
41115
- const archMap = { x64: "amd64", arm64: "arm64", arm: "arm" };
41116
- const cfArch = archMap[arch2] ?? "amd64";
41117
- try {
41118
- execSync27(`curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}" -o /tmp/cloudflared && chmod +x /tmp/cloudflared && mkdir -p "${homedir13()}/.local/bin" && mv /tmp/cloudflared "${homedir13()}/.local/bin/cloudflared"`, { stdio: "pipe", timeout: 6e4 });
41119
- if (!process.env.PATH?.includes(`${homedir13()}/.local/bin`)) {
41120
- process.env.PATH = `${homedir13()}/.local/bin:${process.env.PATH}`;
41121
- }
41122
- if (hasCmd("cloudflared")) {
41123
- log("cloudflared installed.");
41124
- return true;
41125
- }
41126
- } catch {
41127
- }
41128
- try {
41129
- execSync27(`curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}" -o /tmp/cloudflared && chmod +x /tmp/cloudflared && sudo mv /tmp/cloudflared /usr/local/bin/cloudflared 2>/dev/null`, { stdio: "pipe", timeout: 6e4 });
41130
- if (hasCmd("cloudflared")) {
41131
- log("cloudflared installed.");
41132
- return true;
41032
+ return config.model;
41033
+ }
41034
+ const cleanUrl = normalizeBaseUrl(endpoint);
41035
+ const needsKey = await ask(rl, `
41036
+ ${c2.bold("Does this endpoint require an API key?")} (y/n) `);
41037
+ let apiKey = "";
41038
+ if (needsKey.toLowerCase() === "y" || needsKey.toLowerCase() === "yes") {
41039
+ apiKey = await askSecret(rl, ` ${c2.bold("API key:")} `);
41040
+ }
41041
+ process.stdout.write(`
41042
+ ${c2.cyan("\u25CF")} Enter the model name for this endpoint.
41043
+ `);
41044
+ process.stdout.write(` ${c2.dim("Examples: qwen3.5:122b, meta-llama/Llama-3.3-70B, etc.")}
41045
+
41046
+ `);
41047
+ const modelName = await ask(rl, ` ${c2.bold("Model name")} (Enter for ${c2.dim(config.model)}): `);
41048
+ const chosenModel = modelName || config.model;
41049
+ const provider = detectProvider(endpoint);
41050
+ process.stdout.write(`
41051
+ ${c2.cyan("\u25CF")} Detected provider: ${c2.bold(provider.label)}
41052
+ `);
41053
+ process.stdout.write(` ${c2.cyan("\u25CF")} Testing endpoint ${c2.bold(cleanUrl)}...
41054
+ `);
41055
+ let testOk = false;
41056
+ try {
41057
+ const testUrl = `${cleanUrl}${provider.modelsPath}`;
41058
+ const headers = { "Content-Type": "application/json" };
41059
+ if (apiKey) {
41060
+ headers["Authorization"] = `Bearer ${apiKey}`;
41061
+ }
41062
+ const resp = await fetch(testUrl, { headers, signal: AbortSignal.timeout(1e4) });
41063
+ if (resp.ok) {
41064
+ process.stdout.write(` ${c2.green("\u2714")} Endpoint reachable.
41065
+ `);
41066
+ testOk = true;
41067
+ } else {
41068
+ if (provider.id !== "ollama") {
41069
+ try {
41070
+ const ollamaResp = await fetch(`${cleanUrl}/api/tags`, { signal: AbortSignal.timeout(1e4) });
41071
+ if (ollamaResp.ok) {
41072
+ process.stdout.write(` ${c2.green("\u2714")} Ollama endpoint detected.
41073
+ `);
41074
+ testOk = true;
41075
+ }
41076
+ } catch {
41133
41077
  }
41134
- } catch {
41135
41078
  }
41136
- } else if (os === "darwin") {
41137
- try {
41138
- execSync27("brew install cloudflared", { stdio: "pipe", timeout: 12e4 });
41139
- if (hasCmd("cloudflared")) {
41140
- log("cloudflared installed via Homebrew.");
41141
- return true;
41142
- }
41143
- } catch {
41079
+ if (!testOk) {
41080
+ process.stdout.write(` ${c2.yellow("\u26A0")} Endpoint returned HTTP ${resp.status}
41081
+ `);
41144
41082
  }
41145
41083
  }
41146
- log("cloudflared not available \u2014 live voice sessions disabled.");
41147
- return false;
41148
- })();
41149
- }
41150
- function expandedModelName(baseModel) {
41151
- return `open-agents-${baseModel.replace(":", "-").replace(/\./g, "")}`;
41152
- }
41153
- async function checkExpandedVariant(modelName, backendUrl) {
41154
- if (modelName.startsWith("open-agents-"))
41155
- return null;
41156
- const target = expandedModelName(modelName);
41157
- try {
41158
- const models = await fetchOllamaModels(backendUrl);
41159
- const found = models.find((m) => m.name === target || m.name.startsWith(target + ":"));
41160
- return found ? found.name : false;
41161
- } catch {
41162
- return false;
41084
+ } catch (err) {
41085
+ process.stdout.write(` ${c2.yellow("\u26A0")} Could not reach endpoint: ${err instanceof Error ? err.message : String(err)}
41086
+ `);
41163
41087
  }
41164
- }
41165
- function modelSizeGB(models, modelName) {
41166
- const m = findModel(models, modelName);
41167
- if (m)
41168
- return m.sizeBytes / 1024 ** 3;
41169
- const known = QWEN_VARIANTS.find((v) => modelName.includes(v.tag.split(":")[1] ?? ""));
41170
- return known?.sizeGB ?? 4;
41171
- }
41172
- async function queryModelKVInfo(backendUrl, modelName) {
41173
- try {
41174
- const normalized = backendUrl.replace(/\/+$/, "");
41175
- const res = await fetch(`${normalized}/api/show`, {
41176
- method: "POST",
41177
- headers: { "Content-Type": "application/json" },
41178
- body: JSON.stringify({ name: modelName }),
41179
- signal: AbortSignal.timeout(5e3)
41180
- });
41181
- if (!res.ok)
41182
- return null;
41183
- const data = await res.json();
41184
- if (!data.model_info)
41185
- return null;
41186
- const info = data.model_info;
41187
- const arch2 = info["general.architecture"];
41188
- if (!arch2)
41189
- return null;
41190
- const nLayers = info[`${arch2}.block_count`];
41191
- const nKVHeads = info[`${arch2}.attention.head_count_kv`] ?? info[`${arch2}.attention.head_count`];
41192
- const keyDim = info[`${arch2}.attention.key_length`];
41193
- const valDim = info[`${arch2}.attention.value_length`] ?? keyDim;
41194
- const archMax = info[`${arch2}.context_length`];
41195
- if (!nLayers || !nKVHeads || !keyDim || !valDim || !archMax)
41196
- return null;
41197
- const kvBytesPerToken = nLayers * nKVHeads * (keyDim + valDim) * 2;
41198
- return { kvBytesPerToken, archMax };
41199
- } catch {
41200
- return null;
41088
+ if (!testOk) {
41089
+ if (provider.authRequired && !apiKey) {
41090
+ process.stdout.write(` ${c2.dim(`${provider.label} typically requires an API key.`)}
41091
+ `);
41092
+ }
41093
+ const startAnyway = await ask(rl, `
41094
+ ${c2.bold("Endpoint unreachable. Start anyway?")} (y/n) `);
41095
+ if (startAnyway.toLowerCase() !== "y" && startAnyway.toLowerCase() !== "yes") {
41096
+ process.stdout.write(` ${c2.dim("You can configure the endpoint later with /endpoint")}
41097
+
41098
+ `);
41099
+ return config.model;
41100
+ }
41201
41101
  }
41202
- }
41203
- async function createExpandedVariantAsync(baseModel, specs, sizeGB, kvBytesPerToken, archMax) {
41204
- const customName = expandedModelName(baseModel);
41205
- const ctx = calculateContextWindow(specs, sizeGB, kvBytesPerToken, archMax);
41206
- try {
41207
- const numPredict = Math.min(16384, Math.max(2048, Math.floor(ctx.numCtx * 0.25)));
41208
- const modelfileContent = [
41209
- `FROM ${baseModel}`,
41210
- `PARAMETER num_ctx ${ctx.numCtx}`,
41211
- `PARAMETER temperature 0`,
41212
- `PARAMETER num_predict ${numPredict}`,
41213
- `PARAMETER stop "<|endoftext|>"`
41214
- ].join("\n");
41215
- const modelDir2 = join54(homedir13(), ".open-agents", "models");
41216
- mkdirSync15(modelDir2, { recursive: true });
41217
- const modelfilePath = join54(modelDir2, `Modelfile.${customName}`);
41218
- writeFileSync16(modelfilePath, modelfileContent + "\n", "utf8");
41219
- await execAsync(`ollama create ${customName} -f ${modelfilePath}`, {
41220
- timeout: 12e4
41221
- });
41222
- return customName;
41223
- } catch {
41224
- return null;
41102
+ setConfigValue("backendUrl", cleanUrl);
41103
+ setConfigValue("model", chosenModel);
41104
+ if (apiKey) {
41105
+ setConfigValue("apiKey", apiKey);
41225
41106
  }
41107
+ const backendType = provider.id === "ollama" ? "ollama" : "vllm";
41108
+ setConfigValue("backendType", backendType);
41109
+ process.stdout.write(`
41110
+ ${c2.green("\u2714")} Configured: ${c2.bold(chosenModel)} at ${c2.bold(cleanUrl)}
41111
+ `);
41112
+ process.stdout.write(` ${c2.green("\u2714")} Provider: ${c2.bold(provider.label)}
41113
+ `);
41114
+ if (apiKey)
41115
+ process.stdout.write(` ${c2.green("\u2714")} API key saved.
41116
+ `);
41117
+ process.stdout.write(` ${c2.green("\u2714")} Backend type: ${c2.bold(backendType)}
41118
+
41119
+ `);
41120
+ return chosenModel;
41226
41121
  }
41227
- async function ensureExpandedContext(modelName, backendUrl) {
41228
- if (modelName.includes("cloud") || modelName.includes(":cloud")) {
41229
- return { model: modelName, created: false, contextLabel: "remote", numCtx: 0 };
41122
+ async function doSetup(config, rl) {
41123
+ process.stdout.write(`
41124
+ ${c2.bold(c2.cyan("open-agents"))}
41125
+ `);
41126
+ process.stdout.write(` ${c2.dim("\u2500".repeat(60))}
41127
+ `);
41128
+ process.stdout.write(` ${c2.bold("First-run setup")}
41129
+
41130
+ `);
41131
+ process.stdout.write(` ${c2.cyan("\u25CF")} Detecting system specs...
41132
+ `);
41133
+ const specs = detectSystemSpecs();
41134
+ process.stdout.write(` ${c2.dim(" RAM:")} ${specs.totalRamGB.toFixed(1)} GB total, ${specs.availableRamGB.toFixed(1)} GB available
41135
+ `);
41136
+ if (specs.gpuVramGB > 0) {
41137
+ process.stdout.write(` ${c2.dim(" GPU:")} ${specs.gpuName || "NVIDIA"} \u2014 ${specs.gpuVramGB.toFixed(1)} GB VRAM
41138
+ `);
41139
+ } else {
41140
+ process.stdout.write(` ${c2.dim(" GPU:")} No NVIDIA GPU detected (CPU inference)
41141
+ `);
41230
41142
  }
41231
- const specs = await detectSystemSpecsAsync();
41232
- const kvInfo = await queryModelKVInfo(backendUrl, modelName);
41233
- let sizeGB = 4;
41143
+ const score = computeInferenceScore(specs);
41144
+ const w = 24;
41145
+ const bw = 30;
41146
+ process.stdout.write(`
41147
+ ${c2.dim("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510")}
41148
+ `);
41149
+ process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold(" Inference Capability")} ${c2.dim("\u2502")}
41150
+ `);
41151
+ process.stdout.write(` ${c2.dim("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524")}
41152
+ `);
41153
+ process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold("Overall")} ${c2.dim("\u2502")}
41154
+ `);
41155
+ process.stdout.write(` ${c2.dim("\u2502")} ${renderScoreBar(score.overall, bw)} ${c2.bold(String(score.overall).padStart(3))}${c2.dim("/100")} ${c2.dim("\u2502")}
41156
+ `);
41157
+ process.stdout.write(` ${c2.dim("\u2502")} ${c2.dim(score.summary.padEnd(46))} ${c2.dim("\u2502")}
41158
+ `);
41159
+ process.stdout.write(` ${c2.dim("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524")}
41160
+ `);
41161
+ process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold("Memory")} ${c2.dim(`${specs.totalRamGB.toFixed(0)} GB RAM` + (specs.gpuVramGB > 0 ? ` + ${specs.gpuVramGB.toFixed(0)} GB VRAM` : ""))}${" ".repeat(Math.max(0, 27 - (`${specs.totalRamGB.toFixed(0)} GB RAM` + (specs.gpuVramGB > 0 ? ` + ${specs.gpuVramGB.toFixed(0)} GB VRAM` : "")).length))} ${c2.dim("\u2502")}
41162
+ `);
41163
+ process.stdout.write(` ${c2.dim("\u2502")} ${renderScoreBar(score.memory, w)} ${String(score.memory).padStart(3)} ${c2.dim("\u2502")}
41164
+ `);
41165
+ process.stdout.write(` ${c2.dim("\u2502")} ${c2.dim("\u2502")}
41166
+ `);
41167
+ process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold("Compute")} ${c2.dim(specs.gpuVramGB > 0 ? specs.gpuName || "NVIDIA GPU" : "CPU only")}${" ".repeat(Math.max(0, 28 - (specs.gpuVramGB > 0 ? specs.gpuName || "NVIDIA GPU" : "CPU only").length))} ${c2.dim("\u2502")}
41168
+ `);
41169
+ process.stdout.write(` ${c2.dim("\u2502")} ${renderScoreBar(score.compute, w)} ${String(score.compute).padStart(3)} ${c2.dim("\u2502")}
41170
+ `);
41171
+ process.stdout.write(` ${c2.dim("\u2502")} ${c2.dim("\u2502")}
41172
+ `);
41173
+ process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold("Speed")} ${c2.dim(specs.gpuVramGB > 0 ? "GPU accelerated" : "CPU inference")}${" ".repeat(Math.max(0, 28 - (specs.gpuVramGB > 0 ? "GPU accelerated" : "CPU inference").length))} ${c2.dim("\u2502")}
41174
+ `);
41175
+ process.stdout.write(` ${c2.dim("\u2502")} ${renderScoreBar(score.speed, w)} ${String(score.speed).padStart(3)} ${c2.dim("\u2502")}
41176
+ `);
41177
+ process.stdout.write(` ${c2.dim("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524")}
41178
+ `);
41179
+ process.stdout.write(` ${c2.dim("\u2502")} ${c2.bold(" Model Compatibility")} ${c2.dim("\u2502")}
41180
+ `);
41181
+ for (const compat of score.modelCompat) {
41182
+ const icon = compat.fits ? c2.green("\u2714") : c2.red("\u2716");
41183
+ const tag = compat.fits ? compat.tag : c2.dim(compat.tag);
41184
+ const note = c2.dim(compat.note);
41185
+ const visTag = compat.tag;
41186
+ const visNote = compat.note;
41187
+ const pad = Math.max(0, 44 - 2 - visTag.length - 1 - visNote.length);
41188
+ process.stdout.write(` ${c2.dim("\u2502")} ${icon} ${tag} ${" ".repeat(pad)}${note} ${c2.dim("\u2502")}
41189
+ `);
41190
+ }
41191
+ process.stdout.write(` ${c2.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518")}
41192
+
41193
+ `);
41194
+ let hasPython = hasCmd("python3") || hasCmd("python");
41195
+ if (!hasPython) {
41196
+ process.stdout.write(` ${c2.yellow("\u26A0")} Python3 not found (needed for vision, OCR, browser automation).
41197
+ `);
41198
+ const installPy = await ask(rl, ` ${c2.bold("Install Python3?")} (Y/n) `);
41199
+ if (installPy.toLowerCase() !== "n") {
41200
+ ensurePython3();
41201
+ hasPython = hasCmd("python3") || hasCmd("python");
41202
+ }
41203
+ }
41204
+ if (hasPython) {
41205
+ const hasVenv = checkPythonVenv();
41206
+ if (!hasVenv) {
41207
+ process.stdout.write(` ${c2.yellow("\u26A0")} Python3 venv module not found (needed for Moondream, OCR, browser automation).
41208
+ `);
41209
+ const installVenv = await ask(rl, ` ${c2.bold("Install python3-venv?")} (Y/n) `);
41210
+ if (installVenv.toLowerCase() !== "n") {
41211
+ ensurePythonVenv();
41212
+ }
41213
+ }
41214
+ }
41215
+ let models = [];
41216
+ let usingCustomEndpoint = false;
41234
41217
  try {
41235
- const models = await fetchOllamaModels(backendUrl);
41236
- sizeGB = modelSizeGB(models, modelName);
41218
+ models = await fetchOllamaModels(config.backendUrl);
41237
41219
  } catch {
41238
- }
41239
- const ctx = calculateContextWindow(specs, sizeGB, kvInfo?.kvBytesPerToken, kvInfo?.archMax);
41240
- if (modelName.startsWith("open-agents-")) {
41241
- try {
41242
- const normalized = backendUrl.replace(/\/+$/, "");
41243
- const showRes = await fetch(`${normalized}/api/show`, {
41244
- method: "POST",
41245
- headers: { "Content-Type": "application/json" },
41246
- body: JSON.stringify({ name: modelName }),
41247
- signal: AbortSignal.timeout(5e3)
41248
- });
41249
- if (showRes.ok) {
41250
- const showData = await showRes.json();
41251
- const numCtxMatch = showData.parameters?.match(/num_ctx\s+(\d+)/);
41252
- const currentNumCtx = numCtxMatch ? parseInt(numCtxMatch[1], 10) : 0;
41253
- if (currentNumCtx !== ctx.numCtx) {
41254
- const fromMatch = showData.modelfile?.match(/^FROM\s+(.+)$/m);
41255
- const baseModel = fromMatch?.[1]?.trim();
41256
- if (baseModel && !baseModel.startsWith("open-agents-")) {
41257
- await createExpandedVariantAsync(baseModel, specs, sizeGB, kvInfo?.kvBytesPerToken, kvInfo?.archMax);
41220
+ process.stdout.write(` ${c2.yellow("\u26A0")} Cannot reach Ollama at ${c2.bold(config.backendUrl)}
41221
+
41222
+ `);
41223
+ const useCustom = await ask(rl, ` ${c2.bold("Use a custom inference endpoint instead?")} (Y/n) `);
41224
+ if (useCustom.toLowerCase() === "y" || useCustom.toLowerCase() === "yes") {
41225
+ const endpointResult = await promptForCustomEndpoint(config, rl);
41226
+ if (endpointResult) {
41227
+ return endpointResult;
41228
+ }
41229
+ usingCustomEndpoint = true;
41230
+ } else {
41231
+ const ollamaInstalled = hasCmd("ollama");
41232
+ if (ollamaInstalled) {
41233
+ process.stdout.write(`
41234
+ ${c2.cyan("\u25CF")} Ollama is installed but not running. Starting automatically...
41235
+ `);
41236
+ try {
41237
+ const child = spawn19("ollama", ["serve"], { stdio: "ignore", detached: true });
41238
+ child.unref();
41239
+ await new Promise((resolve36) => setTimeout(resolve36, 3e3));
41240
+ try {
41241
+ models = await fetchOllamaModels(config.backendUrl);
41242
+ process.stdout.write(` ${c2.green("\u2714")} Ollama is running.
41243
+
41244
+ `);
41245
+ } catch {
41246
+ process.stdout.write(` ${c2.yellow("\u26A0")} Ollama started but not responding yet. It may need a moment.
41247
+
41248
+ `);
41249
+ }
41250
+ } catch {
41251
+ process.stdout.write(` ${c2.yellow("\u26A0")} Could not start Ollama. Try running ${c2.bold("ollama serve")} manually.
41252
+
41253
+ `);
41254
+ }
41255
+ } else {
41256
+ const installOllama = await ask(rl, `
41257
+ ${c2.bold("Install Ollama for local inference?")} (Y/n) `);
41258
+ if (installOllama.toLowerCase() !== "n") {
41259
+ const installed = await autoInstallOllama(rl);
41260
+ if (installed) {
41261
+ process.stdout.write(`
41262
+ ${c2.cyan("\u25CF")} Starting ollama serve...
41263
+ `);
41264
+ try {
41265
+ const child = spawn19("ollama", ["serve"], { stdio: "ignore", detached: true });
41266
+ child.unref();
41267
+ await new Promise((resolve36) => setTimeout(resolve36, 3e3));
41268
+ try {
41269
+ models = await fetchOllamaModels(config.backendUrl);
41270
+ process.stdout.write(` ${c2.green("\u2714")} Ollama is running.
41271
+
41272
+ `);
41273
+ } catch {
41274
+ process.stdout.write(` ${c2.yellow("\u26A0")} Ollama installed but not responding yet. Try ${c2.bold("ollama serve")} manually.
41275
+
41276
+ `);
41277
+ }
41278
+ } catch {
41279
+ process.stdout.write(` ${c2.yellow("\u26A0")} Ollama installed. Start it with: ${c2.bold("ollama serve")}
41280
+
41281
+ `);
41282
+ }
41258
41283
  }
41284
+ } else {
41285
+ process.stdout.write(`
41286
+ ${c2.dim("You'll need to configure an inference endpoint.")}
41287
+ `);
41288
+ process.stdout.write(` ${c2.dim("Use:")} ${c2.bold("/endpoint <url> --auth <key>")} ${c2.dim("once inside the TUI.")}
41289
+
41290
+ `);
41291
+ return config.model;
41259
41292
  }
41260
41293
  }
41261
- } catch {
41262
41294
  }
41263
- return { model: modelName, created: false, contextLabel: ctx.label, numCtx: ctx.numCtx };
41264
- }
41265
- const existing = await checkExpandedVariant(modelName, backendUrl);
41266
- if (existing === null) {
41267
- return { model: modelName, created: false, contextLabel: "", numCtx: 0 };
41268
- }
41269
- if (typeof existing === "string") {
41270
- return { model: existing, created: false, contextLabel: ctx.label, numCtx: ctx.numCtx };
41271
41295
  }
41272
- const created = await createExpandedVariantAsync(modelName, specs, sizeGB, kvInfo?.kvBytesPerToken, kvInfo?.archMax);
41273
- if (created) {
41274
- return { model: created, created: true, contextLabel: ctx.label, numCtx: ctx.numCtx };
41296
+ if (usingCustomEndpoint) {
41297
+ return config.model;
41275
41298
  }
41276
- return { model: modelName, created: false, contextLabel: ctx.label, numCtx: ctx.numCtx };
41277
- }
41278
- async function ensureNeovim() {
41279
- try {
41280
- const nvimPath = execSync27("which nvim 2>/dev/null || where nvim 2>nul", {
41281
- encoding: "utf8",
41282
- stdio: "pipe",
41283
- timeout: 5e3
41284
- }).trim();
41285
- if (nvimPath)
41286
- return nvimPath;
41287
- } catch {
41299
+ const currentModel = findModel(models, config.model);
41300
+ if (currentModel) {
41301
+ process.stdout.write(` ${c2.green("\u2714")} Model ${c2.bold(currentModel.name)} is available.
41302
+
41303
+ `);
41304
+ return currentModel.name;
41288
41305
  }
41289
- const platform6 = process.platform;
41290
- const arch2 = process.arch;
41291
- if (platform6 === "linux") {
41292
- const binDir = join54(homedir13(), ".local", "bin");
41293
- const nvimDest = join54(binDir, "nvim");
41294
- try {
41295
- mkdirSync15(binDir, { recursive: true });
41296
- } catch {
41297
- }
41298
- const appImageName = arch2 === "arm64" ? "nvim-linux-arm64.appimage" : "nvim-linux-x86_64.appimage";
41299
- const url = `https://github.com/neovim/neovim/releases/latest/download/${appImageName}`;
41300
- console.log(` Downloading Neovim (${appImageName})...`);
41301
- try {
41302
- execSync27(`curl -fsSL "${url}" -o "${nvimDest}"`, { stdio: "pipe", timeout: 6e4 });
41303
- execSync27(`chmod +x "${nvimDest}"`, { stdio: "pipe", timeout: 3e3 });
41304
- } catch (err) {
41305
- console.log(` Failed to download Neovim: ${err instanceof Error ? err.message : String(err)}`);
41306
- return null;
41307
- }
41308
- try {
41309
- const ver = execSync27(`"${nvimDest}" --version`, { encoding: "utf8", stdio: "pipe", timeout: 5e3 }).split("\n")[0];
41310
- console.log(` Installed: ${ver}`);
41311
- } catch {
41312
- console.log(" Warning: nvim binary downloaded but may not work (missing FUSE? Try: nvim --appimage-extract)");
41313
- }
41314
- if (!process.env.PATH?.includes(binDir)) {
41315
- process.env.PATH = `${binDir}:${process.env.PATH}`;
41306
+ process.stdout.write(` ${c2.yellow("\u26A0")} Default model ${c2.bold(config.model)} is not available.
41307
+
41308
+ `);
41309
+ if (models.length > 0) {
41310
+ const backendUrl = config.backendUrl || "http://localhost:11434";
41311
+ const modelChecks = await Promise.all(models.slice(0, 15).map(async (m) => ({
41312
+ ...m,
41313
+ hasTools: await checkToolSupport(m.name, backendUrl)
41314
+ })));
41315
+ const modelItems = modelChecks.map((m) => {
41316
+ const toolTag = m.hasTools ? "tools \u2713" : "text mode";
41317
+ return { key: m.name, label: m.name, detail: `${m.size} ${toolTag}` };
41318
+ });
41319
+ modelItems.push({ key: "__pull_new__", label: "Pull a new qwen3.5 model instead", detail: "" });
41320
+ const modelResult = await tuiSelect({
41321
+ items: modelItems,
41322
+ title: "Select a Model",
41323
+ rl
41324
+ });
41325
+ if (modelResult.confirmed && modelResult.key && modelResult.key !== "__pull_new__") {
41326
+ const selected = modelChecks.find((m) => m.name === modelResult.key);
41327
+ setConfigValue("model", selected.name);
41328
+ const mode = selected.hasTools ? "native tool calling" : "text mode (instruct-and-parse)";
41329
+ process.stdout.write(`
41330
+ ${c2.green("\u2714")} Selected ${c2.bold(selected.name)} \u2014 ${mode}. Saved to config.
41331
+
41332
+ `);
41333
+ return selected.name;
41316
41334
  }
41317
- ensurePathInShellRc(binDir);
41318
- return nvimDest;
41335
+ } else {
41336
+ process.stdout.write(` ${c2.yellow("\u26A0")} No models found on this system.
41337
+
41338
+ `);
41319
41339
  }
41320
- if (platform6 === "darwin") {
41321
- if (hasCmd("brew")) {
41322
- console.log(" Installing Neovim via Homebrew...");
41323
- try {
41324
- execSync27("brew install neovim", { stdio: "inherit", timeout: 12e4 });
41325
- const nvimPath = execSync27("which nvim", { encoding: "utf8", stdio: "pipe", timeout: 3e3 }).trim();
41326
- return nvimPath || null;
41327
- } catch {
41328
- console.log(" brew install neovim failed.");
41329
- return null;
41330
- }
41331
- }
41332
- console.log(" Homebrew not found. Install Neovim: brew install neovim");
41333
- return null;
41340
+ const recommended = recommendModel(specs);
41341
+ const localVariants = QWEN_VARIANTS.filter((v) => !v.cloud);
41342
+ const pullItems = localVariants.map((v) => {
41343
+ const fits = v.sizeGB <= Math.max(specs.gpuVramGB, specs.availableRamGB) * 0.8;
41344
+ const isRec = v.tag === recommended.tag;
41345
+ const suffix = isRec ? " \u2190 recommended" : !fits ? " (exceeds memory)" : "";
41346
+ return { key: v.tag, label: v.tag, detail: `${v.label}${suffix}` };
41347
+ });
41348
+ pullItems.push({ key: "qwen3.5:cloud", label: "qwen3.5:cloud", detail: "Ollama Cloud" });
41349
+ pullItems.push({ key: "qwen3.5:397b-cloud", label: "qwen3.5:397b-cloud", detail: "397B Ollama Cloud" });
41350
+ const pullResult = await tuiSelect({
41351
+ items: pullItems,
41352
+ title: "Select a Model to Pull",
41353
+ activeKey: recommended.tag,
41354
+ rl
41355
+ });
41356
+ let selectedVariant;
41357
+ if (!pullResult.confirmed || !pullResult.key) {
41358
+ process.stdout.write(`
41359
+ ${c2.dim("Skipping model pull. You can pull manually with: ollama pull <model>")}
41360
+
41361
+ `);
41362
+ return config.model;
41334
41363
  }
41335
- if (platform6 === "win32") {
41336
- if (hasCmd("choco")) {
41337
- console.log(" Installing Neovim via Chocolatey...");
41338
- try {
41339
- execSync27("choco install neovim -y", { stdio: "inherit", timeout: 12e4 });
41340
- return "nvim";
41341
- } catch {
41342
- console.log(" choco install neovim failed.");
41343
- }
41344
- }
41345
- if (hasCmd("winget")) {
41346
- console.log(" Installing Neovim via winget...");
41364
+ selectedVariant = QWEN_VARIANTS.find((v) => v.tag === pullResult.key) ?? recommended;
41365
+ process.stdout.write(`
41366
+ ${c2.cyan("\u25CF")} Pulling ${c2.bold(selectedVariant.tag)}... (this may take a while)
41367
+ `);
41368
+ try {
41369
+ pullModelWithAutoUpdate(selectedVariant.tag);
41370
+ process.stdout.write(`
41371
+ ${c2.green("\u2714")} Model ${c2.bold(selectedVariant.tag)} pulled successfully.
41372
+
41373
+ `);
41374
+ } catch (err) {
41375
+ renderError(`Failed to pull model: ${err instanceof Error ? err.message : String(err)}`);
41376
+ renderInfo("Try manually: ollama pull " + selectedVariant.tag);
41377
+ return config.model;
41378
+ }
41379
+ if (!selectedVariant.cloud) {
41380
+ const ctx = calculateContextWindow(specs, selectedVariant.sizeGB);
41381
+ const customName = `open-agents-${selectedVariant.tag.replace(":", "-").replace(".", "")}`;
41382
+ process.stdout.write(` ${c2.cyan("\u25CF")} Context window recommendation: ${c2.bold(ctx.label)} (${ctx.numCtx} tokens)
41383
+ `);
41384
+ process.stdout.write(` ${c2.dim(`Based on ${specs.totalRamGB.toFixed(0)} GB RAM, ${selectedVariant.sizeGB} GB model`)}
41385
+
41386
+ `);
41387
+ const createModelfile = await ask(rl, ` Create optimized model "${c2.bold(customName)}" with ${ctx.label} context? (Y/n) `);
41388
+ if (createModelfile.toLowerCase() !== "n") {
41347
41389
  try {
41348
- execSync27("winget install Neovim.Neovim --accept-source-agreements --accept-package-agreements", {
41349
- stdio: "inherit",
41390
+ const numPredict = Math.min(16384, Math.max(2048, Math.floor(ctx.numCtx * 0.25)));
41391
+ const modelfileContent = [
41392
+ `FROM ${selectedVariant.tag}`,
41393
+ `PARAMETER num_ctx ${ctx.numCtx}`,
41394
+ `PARAMETER temperature 0`,
41395
+ `PARAMETER num_predict ${numPredict}`,
41396
+ `PARAMETER stop "<|endoftext|>"`
41397
+ ].join("\n");
41398
+ const modelDir2 = join54(homedir13(), ".open-agents", "models");
41399
+ mkdirSync15(modelDir2, { recursive: true });
41400
+ const modelfilePath = join54(modelDir2, `Modelfile.${customName}`);
41401
+ writeFileSync16(modelfilePath, modelfileContent + "\n", "utf8");
41402
+ process.stdout.write(` ${c2.dim("Creating model...")} `);
41403
+ execSync27(`ollama create ${customName} -f ${modelfilePath}`, {
41404
+ stdio: "pipe",
41350
41405
  timeout: 12e4
41351
41406
  });
41352
- return "nvim";
41353
- } catch {
41354
- console.log(" winget install neovim failed.");
41407
+ process.stdout.write(`${c2.green("\u2714")}
41408
+ `);
41409
+ setConfigValue("model", customName);
41410
+ process.stdout.write(`
41411
+ ${c2.green("\u2714")} Model ${c2.bold(customName)} created with ${ctx.label} context.
41412
+ `);
41413
+ process.stdout.write(` ${c2.green("\u2714")} Saved as default model in config.
41414
+
41415
+ `);
41416
+ return customName;
41417
+ } catch (err) {
41418
+ renderWarning(`Could not create custom model: ${err instanceof Error ? err.message : String(err)}`);
41419
+ renderInfo(`Using base model ${selectedVariant.tag} instead.`);
41355
41420
  }
41356
41421
  }
41357
- console.log(" Install Neovim manually: https://neovim.io");
41358
- return null;
41422
+ setConfigValue("model", selectedVariant.tag);
41423
+ process.stdout.write(`
41424
+ ${c2.green("\u2714")} Saved ${c2.bold(selectedVariant.tag)} as default model.
41425
+
41426
+ `);
41427
+ return selectedVariant.tag;
41359
41428
  }
41360
- return null;
41429
+ setConfigValue("model", selectedVariant.tag);
41430
+ process.stdout.write(`
41431
+ ${c2.green("\u2714")} Saved ${c2.bold(selectedVariant.tag)} as default model.
41432
+
41433
+ `);
41434
+ return selectedVariant.tag;
41361
41435
  }
41362
- function ensurePathInShellRc(binDir) {
41363
- const shell = process.env.SHELL ?? "";
41364
- const rcFile = shell.includes("zsh") ? join54(homedir13(), ".zshrc") : join54(homedir13(), ".bashrc");
41436
+ async function isModelAvailable(config) {
41365
41437
  try {
41366
- const rcContent = existsSync37(rcFile) ? readFileSync28(rcFile, "utf8") : "";
41367
- if (rcContent.includes(binDir))
41368
- return;
41369
- const exportLine = `
41370
- export PATH="${binDir}:$PATH" # Added by open-agents for nvim
41371
- `;
41372
- appendFileSync2(rcFile, exportLine, "utf8");
41373
- console.log(` Added ${binDir} to ${rcFile}`);
41438
+ const models = await fetchModels(config.backendUrl, config.apiKey);
41439
+ return !!findModel(models, config.model);
41374
41440
  } catch {
41441
+ return false;
41375
41442
  }
41376
41443
  }
41377
- var execAsync, QWEN_VARIANTS, _toolSupportCache, _cloudflaredInstallPromise;
41378
- var init_setup = __esm({
41379
- "packages/cli/dist/tui/setup.js"() {
41380
- "use strict";
41381
- init_model_picker();
41382
- init_render();
41383
- init_config();
41384
- init_dist();
41385
- execAsync = promisify6(exec2);
41386
- QWEN_VARIANTS = [
41387
- { tag: "qwen3.5:0.8b", sizeGB: 1, label: "0.8B params (1.0 GB)", cloud: false },
41388
- { tag: "qwen3.5:2b", sizeGB: 2.7, label: "2B params (2.7 GB)", cloud: false },
41389
- { tag: "qwen3.5:4b", sizeGB: 3.4, label: "4B params (3.4 GB)", cloud: false },
41390
- { tag: "qwen3.5:9b", sizeGB: 6.6, label: "9B params (6.6 GB) \u2014 recommended minimum", cloud: false },
41391
- { tag: "qwen3.5:27b", sizeGB: 17, label: "27B params (17 GB)", cloud: false },
41392
- { tag: "qwen3.5:35b", sizeGB: 24, label: "35B params (24 GB)", cloud: false },
41393
- { tag: "qwen3.5:122b", sizeGB: 81, label: "122B params (81 GB) \u2014 best local", cloud: false },
41394
- { tag: "qwen3.5:cloud", sizeGB: 0, label: "Cloud (Ollama Cloud)", cloud: true },
41395
- { tag: "qwen3.5:397b-cloud", sizeGB: 0, label: "397B Cloud (Ollama Cloud)", cloud: true }
41396
- ];
41397
- _toolSupportCache = /* @__PURE__ */ new Map();
41398
- _cloudflaredInstallPromise = null;
41399
- }
41400
- });
41401
-
41402
- // packages/cli/dist/tui/overlay-lock.js
41403
- var overlay_lock_exports = {};
41404
- __export(overlay_lock_exports, {
41405
- bufferIfOverlay: () => bufferIfOverlay,
41406
- enterOverlay: () => enterOverlay,
41407
- isOverlayActive: () => isOverlayActive,
41408
- leaveOverlay: () => leaveOverlay,
41409
- onOverlayLeave: () => onOverlayLeave,
41410
- overlayWrite: () => overlayWrite
41411
- });
41412
- function onOverlayLeave(cb) {
41413
- _onLeaveCallback = cb;
41414
- }
41415
- function isOverlayActive() {
41416
- return _overlayActive;
41417
- }
41418
- function overlayWrite(data) {
41419
- if (_origStdoutWrite) {
41420
- return _origStdoutWrite.call(process.stdout, data);
41444
+ function isFirstRun() {
41445
+ try {
41446
+ return !existsSync37(join54(homedir13(), ".open-agents", "config.json"));
41447
+ } catch {
41448
+ return true;
41421
41449
  }
41422
- return process.stdout.write(data);
41423
41450
  }
41424
- function bufferIfOverlay(stream, data) {
41425
- if (!_overlayActive)
41451
+ function hasCmd(cmd) {
41452
+ try {
41453
+ const whichCmd = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
41454
+ execSync27(whichCmd, { stdio: "pipe", timeout: 3e3 });
41455
+ return true;
41456
+ } catch {
41426
41457
  return false;
41427
- _buffer.push({ stream, data });
41428
- return true;
41429
- }
41430
- function enterOverlay() {
41431
- _depth++;
41432
- if (_depth === 1) {
41433
- _overlayActive = true;
41434
- _origStdoutWrite = process.stdout.write.bind(process.stdout);
41435
- _origStderrWrite = process.stderr.write.bind(process.stderr);
41436
- process.stdout.write = function(chunk, ...args) {
41437
- _buffer.push({ stream: "stdout", data: chunk });
41438
- const cb = typeof args[args.length - 1] === "function" ? args[args.length - 1] : void 0;
41439
- if (cb)
41440
- cb();
41441
- return true;
41442
- };
41443
- process.stderr.write = function(chunk, ...args) {
41444
- _buffer.push({ stream: "stderr", data: chunk });
41445
- const cb = typeof args[args.length - 1] === "function" ? args[args.length - 1] : void 0;
41446
- if (cb)
41447
- cb();
41448
- return true;
41449
- };
41450
41458
  }
41451
41459
  }
41452
- function leaveOverlay() {
41453
- _depth = Math.max(0, _depth - 1);
41454
- if (_depth === 0) {
41455
- _overlayActive = false;
41456
- if (_origStdoutWrite) {
41457
- process.stdout.write = _origStdoutWrite;
41458
- _origStdoutWrite = null;
41459
- }
41460
- if (_origStderrWrite) {
41461
- process.stderr.write = _origStderrWrite;
41462
- _origStderrWrite = null;
41463
- }
41464
- if (_onLeaveCallback) {
41465
- try {
41466
- _onLeaveCallback();
41467
- } catch {
41468
- }
41469
- }
41470
- const pending = _buffer;
41471
- _buffer = [];
41472
- for (const { stream, data } of pending) {
41473
- if (stream === "stdout") {
41474
- process.stdout.write(data);
41475
- } else {
41476
- process.stderr.write(data);
41477
- }
41478
- }
41460
+ function detectPkgManager() {
41461
+ const plat = process.platform;
41462
+ if (plat === "win32") {
41463
+ if (hasCmd("choco"))
41464
+ return "choco";
41465
+ return null;
41479
41466
  }
41480
- }
41481
- var _overlayActive, _depth, _buffer, _origStdoutWrite, _origStderrWrite, _onLeaveCallback;
41482
- var init_overlay_lock = __esm({
41483
- "packages/cli/dist/tui/overlay-lock.js"() {
41484
- "use strict";
41485
- _overlayActive = false;
41486
- _depth = 0;
41487
- _buffer = [];
41488
- _origStdoutWrite = null;
41489
- _origStderrWrite = null;
41490
- _onLeaveCallback = null;
41467
+ if (plat === "darwin") {
41468
+ if (hasCmd("brew"))
41469
+ return "brew";
41470
+ return null;
41491
41471
  }
41492
- });
41493
-
41494
- // packages/cli/dist/tui/tui-select.js
41495
- function ansi3(code, text) {
41496
- return isTTY3 ? `\x1B[${code}m${text}\x1B[0m` : text;
41497
- }
41498
- function fg2562(code, text) {
41499
- return isTTY3 ? `\x1B[38;5;${code}m${text}\x1B[0m` : text;
41500
- }
41501
- function stripAnsi(s) {
41502
- return s.replace(/\x1B\[[0-9;]*m/g, "");
41472
+ if (hasCmd("apt-get"))
41473
+ return "apt";
41474
+ if (hasCmd("dnf"))
41475
+ return "dnf";
41476
+ if (hasCmd("pacman"))
41477
+ return "pacman";
41478
+ if (hasCmd("brew"))
41479
+ return "brew";
41480
+ return null;
41503
41481
  }
41504
- function defaultRenderRow(item, focused, isActive) {
41505
- const marker = isActive ? selectColors.green("\u25CF") : focused ? selectColors.orange("\u25CF") : selectColors.dim("\u25CB");
41506
- const label = focused ? selectColors.orange(selectColors.bold(item.label)) : isActive ? selectColors.green(item.label) : item.label;
41507
- const detail = item.detail ? ` ${selectColors.dim(item.detail)}` : "";
41508
- return ` ${marker} ${label}${detail}`;
41482
+ function getVenvDir() {
41483
+ return join54(homedir13(), ".open-agents", "venv");
41509
41484
  }
41510
- function matchRow(item, focused, isActive) {
41511
- if (focused || isActive) {
41512
- return defaultRenderRow(item, focused, isActive);
41485
+ function hasVenvModule() {
41486
+ try {
41487
+ execSync27("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
41488
+ return true;
41489
+ } catch {
41490
+ return false;
41513
41491
  }
41514
- const marker = selectColors.matchLight("\u25CB");
41515
- const label = selectColors.matchLight(stripAnsi(item.label));
41516
- const detail = item.detail ? ` ${selectColors.dim(stripAnsi(item.detail))}` : "";
41517
- return ` ${marker} ${label}${detail}`;
41518
41492
  }
41519
- function tuiSelect(opts) {
41520
- const { items, title, rl } = opts;
41521
- const renderRow = opts.renderRow ?? defaultRenderRow;
41522
- const activeKey = opts.activeKey ?? null;
41523
- const skipSet = new Set(opts.skipKeys ?? []);
41524
- if (items.length === 0) {
41525
- return Promise.resolve({ confirmed: false, key: null, index: -1 });
41493
+ function ensureVenv(log) {
41494
+ const venvDir = getVenvDir();
41495
+ const isWin2 = process.platform === "win32";
41496
+ const pipPath = isWin2 ? join54(venvDir, "Scripts", "pip.exe") : join54(venvDir, "bin", "pip");
41497
+ const pythonCmd = isWin2 ? "python" : "python3";
41498
+ if (existsSync37(pipPath))
41499
+ return venvDir;
41500
+ log("Creating Python venv for vision deps...");
41501
+ if (!hasCmd(pythonCmd) && !hasCmd("python3")) {
41502
+ log(`${pythonCmd} not found \u2014 cannot create venv.`);
41503
+ return null;
41526
41504
  }
41527
- const isSkippable = (idx) => skipSet.has(items[idx].key);
41528
- let filter = "";
41529
- let matchSet = /* @__PURE__ */ new Set();
41530
- let deleteConfirmIdx = -1;
41531
- let deleteConfirmSel = false;
41532
- function updateFilter() {
41533
- if (!filter) {
41534
- matchSet = new Set(items.map((_, i) => i));
41535
- } else {
41536
- const lower = filter.toLowerCase();
41537
- matchSet = /* @__PURE__ */ new Set();
41538
- for (let i = 0; i < items.length; i++) {
41539
- if (isSkippable(i))
41540
- continue;
41541
- const plain = stripAnsi(items[i].label).toLowerCase();
41542
- const detailPlain = items[i].detail ? stripAnsi(items[i].detail).toLowerCase() : "";
41543
- if (plain.includes(lower) || detailPlain.includes(lower)) {
41544
- matchSet.add(i);
41545
- }
41546
- }
41547
- }
41505
+ if (!isWin2 && !hasVenvModule()) {
41506
+ log("python3 venv module not available \u2014 venv creation skipped.");
41507
+ return null;
41508
+ }
41509
+ try {
41510
+ mkdirSync15(join54(homedir13(), ".open-agents"), { recursive: true });
41511
+ const pyCmd = hasCmd(pythonCmd) ? pythonCmd : "python3";
41512
+ execSync27(`${pyCmd} -m venv "${venvDir}"`, { stdio: "pipe", timeout: 3e4 });
41513
+ execSync27(`"${pipPath}" install --upgrade pip`, {
41514
+ stdio: "pipe",
41515
+ timeout: 6e4
41516
+ });
41517
+ log("Python venv created at ~/.open-agents/venv");
41518
+ return venvDir;
41519
+ } catch (err) {
41520
+ log(`Failed to create venv: ${err instanceof Error ? err.message : String(err)}`);
41521
+ return null;
41522
+ }
41523
+ }
41524
+ function trySudoPasswordless(cmd, timeoutMs = 12e4) {
41525
+ try {
41526
+ execSync27(`sudo -n ${cmd}`, {
41527
+ stdio: "pipe",
41528
+ timeout: timeoutMs,
41529
+ env: { ...process.env, DEBIAN_FRONTEND: "noninteractive" }
41530
+ });
41531
+ return true;
41532
+ } catch {
41533
+ return false;
41548
41534
  }
41549
- updateFilter();
41550
- const findSelectable = (from, dir) => {
41551
- let idx = from;
41552
- while (idx >= 0 && idx < items.length) {
41553
- if (!isSkippable(idx) && matchSet.has(idx))
41554
- return idx;
41555
- idx += dir;
41535
+ }
41536
+ function runWithSudo(cmd, password, timeoutMs = 12e4) {
41537
+ try {
41538
+ const escaped = cmd.replace(/'/g, "'\\''");
41539
+ execSync27(`sudo -S bash -c '${escaped}'`, {
41540
+ input: password + "\n",
41541
+ stdio: ["pipe", "pipe", "pipe"],
41542
+ timeout: timeoutMs,
41543
+ env: { ...process.env, DEBIAN_FRONTEND: "noninteractive" }
41544
+ });
41545
+ return "ok";
41546
+ } catch (err) {
41547
+ const stderr = err?.stderr?.toString() ?? "";
41548
+ if (/incorrect password|authentication failure|Sorry, try again|not in the sudoers/i.test(stderr)) {
41549
+ return "auth_fail";
41556
41550
  }
41557
- return -1;
41558
- };
41559
- let cursor = activeKey ? items.findIndex((i) => i.key === activeKey) : -1;
41560
- if (cursor < 0 || isSkippable(cursor)) {
41561
- const first = findSelectable(0, 1);
41562
- cursor = first >= 0 ? first : 0;
41551
+ return "cmd_fail";
41563
41552
  }
41564
- const reservedTopBottom = 6;
41565
- const hasCrumbs = opts.breadcrumbs && opts.breadcrumbs.length > 0;
41566
- const selectChrome = (hasCrumbs ? 11 : 10) + 3;
41567
- const contentArea = opts.availableRows ? opts.availableRows + reservedTopBottom : process.stdout.rows ?? 24;
41568
- const maxVisible = opts.maxVisible ?? Math.max(3, contentArea - selectChrome);
41569
- let scrollOffset = 0;
41570
- let lastRenderedLines = 0;
41571
- return new Promise((resolve36) => {
41572
- const stdin = process.stdin;
41573
- const hadRawMode = stdin.isRaw;
41574
- const savedRlListeners = [];
41575
- if (rl) {
41576
- rl.pause();
41577
- for (const event of ["keypress", "data"]) {
41578
- const listeners = stdin.listeners(event);
41579
- for (const fn of listeners) {
41580
- savedRlListeners.push({ event, fn });
41581
- stdin.removeListener(event, fn);
41582
- }
41583
- }
41584
- }
41585
- if (typeof stdin.setRawMode === "function") {
41586
- stdin.setRawMode(true);
41587
- }
41588
- stdin.resume();
41589
- enterOverlay();
41590
- overlayWrite("\x1B[?1049h\x1B[48;5;234m\x1B[2J\x1B[H\x1B[?25l\x1B[?1003h\x1B[?1006h");
41591
- let listRowOffset = 0;
41592
- function clampScroll(displayList) {
41593
- const cursorPos = displayList.indexOf(cursor);
41594
- if (cursorPos < 0)
41595
- return;
41596
- if (cursorPos < scrollOffset) {
41597
- scrollOffset = cursorPos;
41598
- } else if (cursorPos >= scrollOffset + maxVisible) {
41599
- scrollOffset = cursorPos - maxVisible + 1;
41600
- }
41601
- const maxOffset = Math.max(0, displayList.length - maxVisible);
41602
- scrollOffset = Math.max(0, Math.min(maxOffset, scrollOffset));
41553
+ }
41554
+ function validateSudoPassword(password) {
41555
+ return runWithSudo("true", password, 1e4) === "ok";
41556
+ }
41557
+ async function acquireSudoPassword(getSudoPassword, log, cachedPasswordRef) {
41558
+ if (cachedPasswordRef.value && validateSudoPassword(cachedPasswordRef.value)) {
41559
+ return cachedPasswordRef.value;
41560
+ }
41561
+ if (trySudoPasswordless("true", 1e4)) {
41562
+ return "";
41563
+ }
41564
+ for (let attempt = 0; attempt < 2; attempt++) {
41565
+ if (attempt > 0)
41566
+ log("Authentication failed \u2014 please re-enter password.");
41567
+ const pw = await getSudoPassword();
41568
+ if (!pw)
41569
+ return null;
41570
+ if (validateSudoPassword(pw)) {
41571
+ cachedPasswordRef.value = pw;
41572
+ return pw;
41603
41573
  }
41604
- const hasBreadcrumbs = opts.breadcrumbs && opts.breadcrumbs.length > 0;
41605
- function render() {
41606
- overlayWrite("\x1B[48;5;234m\x1B[H\x1B[2J");
41607
- const lines = [];
41608
- lines.push("", "", "");
41609
- if (hasBreadcrumbs) {
41610
- const trail = opts.breadcrumbs.map((b) => selectColors.dim(b)).join(selectColors.dim(" \u203A "));
41611
- lines.push(`
41612
- ${selectColors.cyan("\u2190")} ${trail}`);
41613
- }
41614
- if (title) {
41615
- if (!hasBreadcrumbs)
41616
- lines.push("");
41617
- lines.push(` ${selectColors.bold(title)}`);
41618
- }
41619
- if (filter) {
41620
- const count = matchSet.size;
41621
- lines.push(` ${selectColors.cyan("/")} ${selectColors.bold(filter)} ${selectColors.dim(`(${count} match${count !== 1 ? "es" : ""})`)}`);
41574
+ }
41575
+ return null;
41576
+ }
41577
+ async function sudoInstall(cmd, getSudoPassword, log, cachedPasswordRef, timeoutMs = 12e4) {
41578
+ if (trySudoPasswordless(cmd, timeoutMs))
41579
+ return true;
41580
+ const pw = await acquireSudoPassword(getSudoPassword, log, cachedPasswordRef);
41581
+ if (pw === null)
41582
+ return false;
41583
+ if (pw === "")
41584
+ return false;
41585
+ const result = runWithSudo(cmd, pw, timeoutMs);
41586
+ if (result === "ok")
41587
+ return true;
41588
+ if (result === "cmd_fail") {
41589
+ log(`Install command failed (not an auth issue) \u2014 package may not be available for this OS.`);
41590
+ }
41591
+ return false;
41592
+ }
41593
+ async function ensureVisionDeps(onInfo, getSudoPassword) {
41594
+ const log = onInfo ?? (() => {
41595
+ });
41596
+ const cachedPasswordRef = { value: null };
41597
+ const getPassword = getSudoPassword ?? (() => Promise.resolve(null));
41598
+ const allDeps = [
41599
+ { binary: "tesseract", label: "tesseract-ocr", pkgs: { apt: "tesseract-ocr", dnf: "tesseract", pacman: "tesseract", brew: "tesseract", choco: "tesseract" } },
41600
+ { binary: "pdftotext", label: "poppler-utils", pkgs: { apt: "poppler-utils", dnf: "poppler-utils", pacman: "poppler", brew: "poppler", choco: "poppler" } },
41601
+ { binary: "gs", label: "ghostscript", pkgs: { apt: "ghostscript", dnf: "ghostscript", pacman: "ghostscript", brew: "ghostscript", choco: "ghostscript" } },
41602
+ { binary: "ocrmypdf", label: "ocrmypdf", pkgs: { apt: "ocrmypdf", dnf: "ocrmypdf", pacman: "ocrmypdf", brew: "ocrmypdf", choco: "ocrmypdf" } }
41603
+ ];
41604
+ {
41605
+ const pm2 = detectPkgManager();
41606
+ const missing = allDeps.filter((d) => !hasCmd(d.binary));
41607
+ if (missing.length === 0) {
41608
+ } else if (!pm2) {
41609
+ for (const d of missing)
41610
+ log(`No supported package manager \u2014 ${d.label} unavailable.`);
41611
+ } else {
41612
+ const pkgNames = missing.map((d) => d.pkgs[pm2]).filter(Boolean);
41613
+ if (pkgNames.length === 0) {
41614
+ for (const d of missing)
41615
+ log(`${d.label} not available for ${pm2}.`);
41622
41616
  } else {
41623
- lines.push(` ${selectColors.dim("Type to filter...")}`);
41624
- }
41625
- lines.push("");
41626
- let displayList;
41627
- if (filter) {
41628
- displayList = [];
41629
- for (let i = 0; i < items.length; i++) {
41630
- if (matchSet.has(i) || isSkippable(i)) {
41631
- displayList.push(i);
41632
- }
41633
- }
41634
- displayList = displayList.filter((idx, pos) => {
41635
- if (!isSkippable(idx))
41636
- return true;
41637
- for (let j = pos + 1; j < displayList.length; j++) {
41638
- if (!isSkippable(displayList[j]))
41639
- return true;
41617
+ const labels = missing.map((d) => d.label).join(", ");
41618
+ log(`Installing ${labels}...`);
41619
+ const needsSudo = pm2 !== "brew" && pm2 !== "choco";
41620
+ let batchCmd;
41621
+ switch (pm2) {
41622
+ case "apt":
41623
+ batchCmd = `apt-get update -qq && apt-get install -y -qq ${pkgNames.join(" ")}`;
41624
+ break;
41625
+ case "dnf":
41626
+ batchCmd = `dnf install -y -q ${pkgNames.join(" ")}`;
41627
+ break;
41628
+ case "pacman":
41629
+ batchCmd = `pacman -S --noconfirm ${pkgNames.join(" ")}`;
41630
+ break;
41631
+ case "brew":
41632
+ batchCmd = `brew install ${pkgNames.join(" ")}`;
41633
+ break;
41634
+ case "choco":
41635
+ batchCmd = `choco install -y ${pkgNames.join(" ")}`;
41636
+ break;
41637
+ default:
41638
+ batchCmd = `echo "unsupported pm: ${pm2}" && exit 1`;
41640
41639
  break;
41641
- }
41642
- return false;
41643
- });
41644
- } else {
41645
- displayList = items.map((_, i) => i);
41646
- }
41647
- clampScroll(displayList);
41648
- const visibleStart = scrollOffset;
41649
- const visibleEnd = Math.min(displayList.length, scrollOffset + maxVisible);
41650
- if (visibleStart > 0) {
41651
- lines.push(` ${selectColors.dim(` \u25B2 ${visibleStart} more`)}`);
41652
- }
41653
- listRowOffset = lines.length;
41654
- for (let vi = visibleStart; vi < visibleEnd; vi++) {
41655
- const idx = displayList[vi];
41656
- const item = items[idx];
41657
- if (isSkippable(idx)) {
41658
- lines.push(` ${item.label}`);
41659
- continue;
41660
41640
  }
41661
- const focused = idx === cursor;
41662
- const isActive = item.key === activeKey;
41663
- if (deleteConfirmIdx === idx) {
41664
- const yesLabel = deleteConfirmSel ? selectColors.bold(selectColors.green("[Yes]")) : selectColors.dim("[Yes]");
41665
- const noLabel = !deleteConfirmSel ? selectColors.bold(selectColors.orange("[No]")) : selectColors.dim("[No]");
41666
- lines.push(` ${ansi3("31", "\u2715")} ${ansi3("31", stripAnsi(item.label))} Delete? ${yesLabel} ${noLabel}`);
41667
- } else if (filter) {
41668
- lines.push(matchRow(item, focused, isActive));
41641
+ let ok = false;
41642
+ if (needsSudo) {
41643
+ ok = await sudoInstall(batchCmd, getPassword, log, cachedPasswordRef, 18e4);
41669
41644
  } else {
41670
- lines.push(renderRow(item, focused, isActive));
41671
- }
41672
- }
41673
- const remaining = displayList.length - visibleEnd;
41674
- if (remaining > 0) {
41675
- lines.push(` ${selectColors.dim(` \u25BC ${remaining} more`)}`);
41676
- }
41677
- lines.push("");
41678
- if (deleteConfirmIdx >= 0) {
41679
- lines.push(` ${selectColors.dim("\u2190/\u2192 select Enter confirm Esc cancel")}`);
41680
- } else {
41681
- const actionHint = opts.onAction ? " \u2190/\u2192/Space toggle" : "";
41682
- const deleteHint = opts.onDelete ? " Del remove" : "";
41683
- const customHint = opts.customKeyHint ?? "";
41684
- const escLabel = filter ? "clear filter" : hasBreadcrumbs ? "\u2190 back" : "cancel";
41685
- lines.push(` ${selectColors.dim("\u2191/\u2193 navigate Enter/Click select" + actionHint + deleteHint + customHint + " Esc " + escLabel + " Type to filter")}`);
41686
- }
41687
- lines.push("");
41688
- let output = lines.join("\n").replace(/\x1B\[0m/g, "\x1B[0m\x1B[48;5;234m").replace(/\n/g, "\x1B[K\n\x1B[48;5;234m");
41689
- overlayWrite("\x1B[48;5;234m" + output + "\x1B[K");
41690
- lastRenderedLines = lines.length;
41691
- }
41692
- function cleanup() {
41693
- stdin.removeListener("data", onData);
41694
- process.stdout.removeListener("resize", onResize);
41695
- overlayWrite("\x1B[?1003l\x1B[?1002l\x1B[?1006l\x1B[?1049l\x1B[?25h");
41696
- leaveOverlay();
41697
- if (typeof stdin.setRawMode === "function") {
41698
- stdin.setRawMode(hadRawMode ?? false);
41699
- }
41700
- for (const { event, fn } of savedRlListeners) {
41701
- stdin.on(event, fn);
41702
- }
41703
- if (rl) {
41704
- rl.resume();
41705
- rl.prompt(false);
41706
- }
41707
- }
41708
- function onData(chunk) {
41709
- let seq = chunk.toString("utf8");
41710
- const mouseRe = /\x1B\[<(\d+);(\d+);(\d+)([Mm])/g;
41711
- let mouseProcessed = false;
41712
- let mouseM;
41713
- while ((mouseM = mouseRe.exec(seq)) !== null) {
41714
- mouseProcessed = true;
41715
- const btn = parseInt(mouseM[1]);
41716
- const mCol = parseInt(mouseM[2]);
41717
- const mRow = parseInt(mouseM[3]);
41718
- const suffix = mouseM[4];
41719
- if (btn === 0 && suffix === "M") {
41720
- const listIdx = mRow - listRowOffset - 1;
41721
- if (listIdx >= 0 && listIdx < maxVisible) {
41722
- let displayList;
41723
- if (filter) {
41724
- displayList = [];
41725
- for (let i = 0; i < items.length; i++) {
41726
- if (matchSet.has(i) || isSkippable(i))
41727
- displayList.push(i);
41728
- }
41729
- displayList = displayList.filter((idx, pos) => {
41730
- if (!isSkippable(idx))
41731
- return true;
41732
- for (let j = pos + 1; j < displayList.length; j++) {
41733
- if (!isSkippable(displayList[j]))
41734
- return true;
41735
- break;
41736
- }
41737
- return false;
41738
- });
41739
- } else {
41740
- displayList = items.map((_, i) => i);
41741
- }
41742
- const vi = scrollOffset + listIdx;
41743
- if (vi < displayList.length) {
41744
- const itemIdx = displayList[vi];
41745
- if (!isSkippable(itemIdx) && matchSet.has(itemIdx)) {
41746
- cursor = itemIdx;
41747
- cleanup();
41748
- resolve36({ confirmed: true, key: items[cursor].key, index: cursor });
41749
- return;
41750
- } else if (!isSkippable(itemIdx)) {
41751
- cursor = itemIdx;
41752
- render();
41753
- }
41754
- }
41755
- }
41756
- }
41757
- if ((btn === 35 || btn === 32 || btn === 67) && suffix === "M") {
41758
- const listIdx = mRow - listRowOffset - 1;
41759
- if (listIdx >= 0 && listIdx < maxVisible) {
41760
- let displayList;
41761
- if (filter) {
41762
- displayList = [];
41763
- for (let i = 0; i < items.length; i++) {
41764
- if (matchSet.has(i) || isSkippable(i))
41765
- displayList.push(i);
41766
- }
41767
- displayList = displayList.filter((idx, pos) => {
41768
- if (!isSkippable(idx))
41769
- return true;
41770
- for (let j = pos + 1; j < displayList.length; j++) {
41771
- if (!isSkippable(displayList[j]))
41772
- return true;
41773
- break;
41774
- }
41775
- return false;
41776
- });
41777
- } else {
41778
- displayList = items.map((_, i) => i);
41779
- }
41780
- const vi = scrollOffset + listIdx;
41781
- if (vi < displayList.length) {
41782
- const itemIdx = displayList[vi];
41783
- if (!isSkippable(itemIdx) && itemIdx !== cursor) {
41784
- cursor = itemIdx;
41785
- render();
41786
- }
41787
- }
41788
- }
41789
- }
41790
- if (btn === 64) {
41791
- const next = findSelectable(cursor - 1, -1);
41792
- if (next >= 0 && next !== cursor) {
41793
- cursor = next;
41794
- render();
41795
- }
41796
- } else if (btn === 65) {
41797
- const next = findSelectable(cursor + 1, 1);
41798
- if (next >= 0 && next !== cursor) {
41799
- cursor = next;
41800
- render();
41645
+ try {
41646
+ execSync27(batchCmd, { stdio: "pipe", timeout: 18e4 });
41647
+ ok = true;
41648
+ } catch {
41649
+ ok = false;
41801
41650
  }
41802
41651
  }
41803
- }
41804
- seq = seq.replace(mouseRe, "");
41805
- if (!seq && mouseProcessed)
41806
- return;
41807
- if (deleteConfirmIdx >= 0) {
41808
- if (seq === "\x1B[D") {
41809
- deleteConfirmSel = true;
41810
- render();
41811
- } else if (seq === "\x1B[C") {
41812
- deleteConfirmSel = false;
41813
- render();
41814
- } else if (seq === "\r" || seq === "\n") {
41815
- if (deleteConfirmSel && opts.onDelete) {
41816
- const deletedIdx = deleteConfirmIdx;
41817
- deleteConfirmIdx = -1;
41818
- deleteConfirmSel = false;
41819
- opts.onDelete(items[deletedIdx], (removed) => {
41820
- if (removed) {
41821
- items.splice(deletedIdx, 1);
41822
- if (items.length === 0) {
41823
- cleanup();
41824
- resolve36({ confirmed: false, key: null, index: -1 });
41825
- return;
41826
- }
41827
- updateFilter();
41828
- if (cursor >= items.length)
41829
- cursor = items.length - 1;
41830
- const valid = findSelectable(cursor, 1) ?? findSelectable(cursor, -1);
41831
- if (valid >= 0)
41832
- cursor = valid;
41833
- scrollOffset = 0;
41834
- }
41835
- render();
41836
- });
41652
+ for (const d of missing) {
41653
+ if (hasCmd(d.binary)) {
41654
+ log(`${d.label} installed.`);
41655
+ } else if (!ok) {
41656
+ log(`${d.label} could not be installed \u2014 features will be limited.`);
41837
41657
  } else {
41838
- deleteConfirmIdx = -1;
41839
- deleteConfirmSel = false;
41840
- render();
41658
+ log(`${d.label} install completed but binary not found.`);
41841
41659
  }
41842
- } else if (seq === "\x1B" || seq === "\x1B\x1B" || seq === "") {
41843
- deleteConfirmIdx = -1;
41844
- deleteConfirmSel = false;
41845
- render();
41846
41660
  }
41847
- return;
41848
41661
  }
41849
- if (seq === "\x1B[A") {
41850
- const next = findSelectable(cursor - 1, -1);
41851
- if (next >= 0 && next !== cursor) {
41852
- cursor = next;
41853
- render();
41854
- }
41855
- } else if (seq === "\x1B[B") {
41856
- const next = findSelectable(cursor + 1, 1);
41857
- if (next >= 0 && next !== cursor) {
41858
- cursor = next;
41859
- render();
41860
- }
41861
- } else if (seq === "\x1B[5~") {
41862
- for (let i = 0; i < maxVisible; i++) {
41863
- const next = findSelectable(cursor - 1, -1);
41864
- if (next < 0 || next === cursor)
41865
- break;
41866
- cursor = next;
41867
- }
41868
- render();
41869
- } else if (seq === "\x1B[6~") {
41870
- for (let i = 0; i < maxVisible; i++) {
41871
- const next = findSelectable(cursor + 1, 1);
41872
- if (next < 0 || next === cursor)
41873
- break;
41874
- cursor = next;
41875
- }
41876
- render();
41877
- } else if (seq === "\x1B[H") {
41878
- const first = findSelectable(0, 1);
41879
- if (first >= 0) {
41880
- cursor = first;
41881
- render();
41882
- }
41883
- } else if (seq === "\x1B[F") {
41884
- const last = findSelectable(items.length - 1, -1);
41885
- if (last >= 0) {
41886
- cursor = last;
41887
- render();
41888
- }
41889
- } else if (seq === "\x1B[3~" && opts.onDelete) {
41890
- if (!isSkippable(cursor) && matchSet.has(cursor) && items[cursor].key !== activeKey) {
41891
- deleteConfirmIdx = cursor;
41892
- deleteConfirmSel = false;
41893
- render();
41894
- }
41895
- } else if (seq === "\x1B[D" && opts.onAction) {
41896
- if (!isSkippable(cursor) && matchSet.has(cursor)) {
41897
- if (opts.onAction(items[cursor], "left"))
41898
- render();
41899
- }
41900
- } else if (seq === "\x1B[C" && opts.onAction) {
41901
- if (!isSkippable(cursor) && matchSet.has(cursor)) {
41902
- if (opts.onAction(items[cursor], "right"))
41903
- render();
41904
- }
41905
- } else if (seq === " " && opts.onAction) {
41906
- if (!isSkippable(cursor) && matchSet.has(cursor)) {
41907
- if (opts.onAction(items[cursor], "space"))
41908
- render();
41662
+ }
41663
+ }
41664
+ const pm = detectPkgManager();
41665
+ if (!hasCmd("pip3") && !hasCmd("pip") && pm) {
41666
+ const pipCmds = {
41667
+ apt: "apt-get install -y python3-pip",
41668
+ dnf: "dnf install -y python3-pip"
41669
+ };
41670
+ const pipCmd = pipCmds[pm];
41671
+ if (pipCmd) {
41672
+ log("Installing python3-pip...");
41673
+ const ok = await sudoInstall(pipCmd, getPassword, log, cachedPasswordRef);
41674
+ if (!ok) {
41675
+ log("python3-pip could not be installed \u2014 moondream-station may be unavailable.");
41676
+ }
41677
+ }
41678
+ }
41679
+ if (hasCmd("python3") && !hasVenvModule() && pm) {
41680
+ const venvCmds = {
41681
+ apt: () => {
41682
+ try {
41683
+ const pyVer = execSync27(`python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"`, { encoding: "utf8", stdio: "pipe", timeout: 5e3 }).trim();
41684
+ return `apt-get install -y python3-venv python${pyVer}-venv`;
41685
+ } catch {
41686
+ return "apt-get install -y python3-venv";
41909
41687
  }
41910
- } else if (seq === "\r" || seq === "\n") {
41911
- if (!isSkippable(cursor) && matchSet.has(cursor)) {
41912
- if (opts.onEnter) {
41913
- const consumed = opts.onEnter(items[cursor], {
41914
- done: () => render(),
41915
- resolve: (result) => {
41916
- cleanup();
41917
- resolve36(result);
41918
- },
41919
- getInput: (prompt, prefill) => getInputFromUser(prompt, prefill),
41920
- render: () => render(),
41921
- updateItem: (index, updates) => {
41922
- Object.assign(items[index], updates);
41923
- render();
41924
- }
41925
- });
41926
- if (consumed)
41927
- return;
41688
+ },
41689
+ dnf: () => "dnf install -y python3-venv"
41690
+ };
41691
+ const cmdFn = venvCmds[pm];
41692
+ if (cmdFn) {
41693
+ const cmd = cmdFn();
41694
+ if (cmd) {
41695
+ log("Installing python3-venv...");
41696
+ const ok = await sudoInstall(cmd, getPassword, log, cachedPasswordRef);
41697
+ if (!ok) {
41698
+ log("python3-venv could not be installed \u2014 moondream-station may be unavailable.");
41699
+ }
41700
+ }
41701
+ }
41702
+ }
41703
+ const venvDir = getVenvDir();
41704
+ const venvBin = join54(venvDir, "bin");
41705
+ const venvMoondream = join54(venvBin, "moondream-station");
41706
+ const venv = ensureVenv(log);
41707
+ if (venv && !hasCmd("moondream-station") && !existsSync37(venvMoondream)) {
41708
+ const venvPip2 = join54(venvBin, "pip");
41709
+ log("Installing moondream-station in ~/.open-agents/venv...");
41710
+ try {
41711
+ execSync27(`"${venvPip2}" install moondream-station`, { stdio: "pipe", timeout: 3e5 });
41712
+ if (existsSync37(venvMoondream)) {
41713
+ log("moondream-station installed successfully.");
41714
+ } else {
41715
+ try {
41716
+ const check = execSync27(`"${venvPip2}" show moondream-station`, { encoding: "utf8", stdio: "pipe", timeout: 5e3 });
41717
+ if (check.includes("moondream")) {
41718
+ log("moondream-station package installed.");
41928
41719
  }
41929
- cleanup();
41930
- resolve36({ confirmed: true, key: items[cursor].key, index: cursor });
41720
+ } catch {
41721
+ log("moondream-station install completed.");
41931
41722
  }
41932
- } else if (seq === "\x1B" || seq === "\x1B\x1B") {
41933
- if (filter) {
41934
- filter = "";
41935
- updateFilter();
41936
- const valid = findSelectable(cursor, 1);
41937
- if (valid >= 0)
41938
- cursor = valid;
41939
- scrollOffset = 0;
41940
- render();
41941
- } else if (hasBreadcrumbs) {
41942
- cleanup();
41943
- resolve36({ confirmed: false, key: "__back__", index: cursor });
41944
- } else {
41945
- cleanup();
41946
- resolve36({ confirmed: false, key: null, index: cursor });
41723
+ }
41724
+ } catch (err) {
41725
+ log(`moondream-station install failed: ${err instanceof Error ? err.message : String(err)}`);
41726
+ }
41727
+ }
41728
+ if (venv) {
41729
+ const venvPython2 = join54(venvBin, "python");
41730
+ const venvPip2 = join54(venvBin, "pip");
41731
+ let ocrStackInstalled = false;
41732
+ try {
41733
+ execSync27(`"${venvPython2}" -c "import cv2, pytesseract, numpy, PIL"`, { stdio: "pipe", timeout: 1e4 });
41734
+ ocrStackInstalled = true;
41735
+ } catch {
41736
+ }
41737
+ if (!ocrStackInstalled) {
41738
+ const ocrPackages = "pytesseract Pillow opencv-python-headless numpy";
41739
+ log("Installing OCR Python stack (pytesseract, OpenCV, Pillow, numpy)...");
41740
+ try {
41741
+ execSync27(`"${venvPip2}" install ${ocrPackages}`, { stdio: "pipe", timeout: 3e5 });
41742
+ try {
41743
+ execSync27(`"${venvPython2}" -c "import cv2, pytesseract, numpy, PIL"`, { stdio: "pipe", timeout: 1e4 });
41744
+ log("OCR Python stack installed successfully.");
41745
+ } catch {
41746
+ log("OCR Python stack install completed but import verification failed.");
41947
41747
  }
41948
- } else if (seq === "") {
41949
- cleanup();
41950
- resolve36({ confirmed: false, key: null, index: cursor });
41951
- } else if (seq === "\x7F" || seq === "\b") {
41952
- if (filter.length > 0) {
41953
- filter = filter.slice(0, -1);
41954
- updateFilter();
41955
- if (!matchSet.has(cursor) || isSkippable(cursor)) {
41956
- const next = findSelectable(0, 1);
41957
- if (next >= 0)
41958
- cursor = next;
41959
- }
41960
- scrollOffset = 0;
41961
- render();
41962
- } else if (hasBreadcrumbs) {
41963
- cleanup();
41964
- resolve36({ confirmed: false, key: "__back__", index: cursor });
41748
+ } catch (err) {
41749
+ log(`OCR Python stack install failed: ${err instanceof Error ? err.message : String(err)}`);
41750
+ }
41751
+ }
41752
+ } else {
41753
+ log("Python venv unavailable \u2014 advanced OCR pipeline will fall back to basic tesseract.");
41754
+ }
41755
+ }
41756
+ function ensureCloudflaredBackground(onInfo) {
41757
+ if (_cloudflaredInstallPromise)
41758
+ return;
41759
+ if (hasCmd("cloudflared")) {
41760
+ _cloudflaredInstallPromise = Promise.resolve(true);
41761
+ return;
41762
+ }
41763
+ const log = onInfo ?? (() => {
41764
+ });
41765
+ log("Installing cloudflared for live voice sessions...");
41766
+ _cloudflaredInstallPromise = (async () => {
41767
+ const arch2 = process.arch;
41768
+ const os = platform2();
41769
+ if (os !== "win32" && !ensureCurl()) {
41770
+ log("curl not available \u2014 cannot install cloudflared.");
41771
+ return false;
41772
+ }
41773
+ if (os === "linux") {
41774
+ const archMap = { x64: "amd64", arm64: "arm64", arm: "arm" };
41775
+ const cfArch = archMap[arch2] ?? "amd64";
41776
+ try {
41777
+ execSync27(`curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}" -o /tmp/cloudflared && chmod +x /tmp/cloudflared && mkdir -p "${homedir13()}/.local/bin" && mv /tmp/cloudflared "${homedir13()}/.local/bin/cloudflared"`, { stdio: "pipe", timeout: 6e4 });
41778
+ if (!process.env.PATH?.includes(`${homedir13()}/.local/bin`)) {
41779
+ process.env.PATH = `${homedir13()}/.local/bin:${process.env.PATH}`;
41965
41780
  }
41966
- } else if (seq.length === 1 && seq.charCodeAt(0) >= 32 && seq.charCodeAt(0) < 127) {
41967
- if (opts.onCustomKey && !isSkippable(cursor) && matchSet.has(cursor)) {
41968
- const consumed = opts.onCustomKey(items[cursor], seq, {
41969
- done: () => render(),
41970
- resolve: (result) => {
41971
- cleanup();
41972
- resolve36(result);
41973
- },
41974
- getInput: (prompt, prefill) => getInputFromUser(prompt, prefill),
41975
- render: () => render(),
41976
- updateItem: (index, updates) => {
41977
- Object.assign(items[index], updates);
41978
- render();
41979
- }
41980
- });
41981
- if (consumed)
41982
- return;
41781
+ if (hasCmd("cloudflared")) {
41782
+ log("cloudflared installed.");
41783
+ return true;
41983
41784
  }
41984
- filter += seq;
41985
- updateFilter();
41986
- if (matchSet.size > 0) {
41987
- const first = findSelectable(0, 1);
41988
- if (first >= 0)
41989
- cursor = first;
41785
+ } catch {
41786
+ }
41787
+ try {
41788
+ execSync27(`curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}" -o /tmp/cloudflared && chmod +x /tmp/cloudflared && sudo mv /tmp/cloudflared /usr/local/bin/cloudflared 2>/dev/null`, { stdio: "pipe", timeout: 6e4 });
41789
+ if (hasCmd("cloudflared")) {
41790
+ log("cloudflared installed.");
41791
+ return true;
41990
41792
  }
41991
- scrollOffset = 0;
41992
- render();
41793
+ } catch {
41993
41794
  }
41994
- }
41995
- function getInputFromUser(prompt, prefill = "") {
41996
- return new Promise((inputResolve) => {
41997
- let inputBuf = prefill;
41998
- let inputCursor = prefill.length;
41999
- overlayWrite("\x1B[?25h");
42000
- function renderInput() {
42001
- overlayWrite(`\x1B[${lastRenderedLines}A`);
42002
- const saveFooter = lastRenderedLines;
42003
- for (let i = 0; i < saveFooter; i++)
42004
- overlayWrite("\x1B[B");
42005
- overlayWrite("\x1B[2A");
42006
- overlayWrite("\x1B[2K");
42007
- const before = inputBuf.slice(0, inputCursor);
42008
- const after = inputBuf.slice(inputCursor);
42009
- overlayWrite(` ${selectColors.cyan("\u203A")} ${selectColors.bold(prompt)} ${before}\x1B[7m${after.charAt(0) || " "}\x1B[27m${after.slice(1)}`);
42010
- overlayWrite("\n\x1B[2K");
42011
- overlayWrite(` ${selectColors.dim("Enter confirm Esc cancel")}`);
42012
- overlayWrite("\n");
41795
+ } else if (os === "darwin") {
41796
+ try {
41797
+ execSync27("brew install cloudflared", { stdio: "pipe", timeout: 12e4 });
41798
+ if (hasCmd("cloudflared")) {
41799
+ log("cloudflared installed via Homebrew.");
41800
+ return true;
42013
41801
  }
42014
- function onInputData(chunk) {
42015
- const s = chunk.toString("utf8");
42016
- if (s === "\r" || s === "\n") {
42017
- stdin.removeListener("data", onInputData);
42018
- stdin.on("data", onData);
42019
- overlayWrite("\x1B[?25l");
42020
- render();
42021
- inputResolve(inputBuf);
42022
- } else if (s === "\x1B" || s === "\x1B\x1B" || s === "") {
42023
- stdin.removeListener("data", onInputData);
42024
- stdin.on("data", onData);
42025
- overlayWrite("\x1B[?25l");
42026
- render();
42027
- inputResolve(null);
42028
- } else if (s === "\x7F" || s === "\b") {
42029
- if (inputCursor > 0) {
42030
- inputBuf = inputBuf.slice(0, inputCursor - 1) + inputBuf.slice(inputCursor);
42031
- inputCursor--;
42032
- renderInput();
42033
- }
42034
- } else if (s === "\x1B[D") {
42035
- if (inputCursor > 0) {
42036
- inputCursor--;
42037
- renderInput();
42038
- }
42039
- } else if (s === "\x1B[C") {
42040
- if (inputCursor < inputBuf.length) {
42041
- inputCursor++;
42042
- renderInput();
42043
- }
42044
- } else if (s === "\x1B[H") {
42045
- inputCursor = 0;
42046
- renderInput();
42047
- } else if (s === "\x1B[F") {
42048
- inputCursor = inputBuf.length;
42049
- renderInput();
42050
- } else if (s.length === 1 && s.charCodeAt(0) >= 32 && s.charCodeAt(0) < 127) {
42051
- inputBuf = inputBuf.slice(0, inputCursor) + s + inputBuf.slice(inputCursor);
42052
- inputCursor++;
42053
- renderInput();
41802
+ } catch {
41803
+ }
41804
+ }
41805
+ log("cloudflared not available \u2014 live voice sessions disabled.");
41806
+ return false;
41807
+ })();
41808
+ }
41809
+ function expandedModelName(baseModel) {
41810
+ return `open-agents-${baseModel.replace(":", "-").replace(/\./g, "")}`;
41811
+ }
41812
+ async function checkExpandedVariant(modelName, backendUrl) {
41813
+ if (modelName.startsWith("open-agents-"))
41814
+ return null;
41815
+ const target = expandedModelName(modelName);
41816
+ try {
41817
+ const models = await fetchOllamaModels(backendUrl);
41818
+ const found = models.find((m) => m.name === target || m.name.startsWith(target + ":"));
41819
+ return found ? found.name : false;
41820
+ } catch {
41821
+ return false;
41822
+ }
41823
+ }
41824
+ function modelSizeGB(models, modelName) {
41825
+ const m = findModel(models, modelName);
41826
+ if (m)
41827
+ return m.sizeBytes / 1024 ** 3;
41828
+ const known = QWEN_VARIANTS.find((v) => modelName.includes(v.tag.split(":")[1] ?? ""));
41829
+ return known?.sizeGB ?? 4;
41830
+ }
41831
+ async function queryModelKVInfo(backendUrl, modelName) {
41832
+ try {
41833
+ const normalized = backendUrl.replace(/\/+$/, "");
41834
+ const res = await fetch(`${normalized}/api/show`, {
41835
+ method: "POST",
41836
+ headers: { "Content-Type": "application/json" },
41837
+ body: JSON.stringify({ name: modelName }),
41838
+ signal: AbortSignal.timeout(5e3)
41839
+ });
41840
+ if (!res.ok)
41841
+ return null;
41842
+ const data = await res.json();
41843
+ if (!data.model_info)
41844
+ return null;
41845
+ const info = data.model_info;
41846
+ const arch2 = info["general.architecture"];
41847
+ if (!arch2)
41848
+ return null;
41849
+ const nLayers = info[`${arch2}.block_count`];
41850
+ const nKVHeads = info[`${arch2}.attention.head_count_kv`] ?? info[`${arch2}.attention.head_count`];
41851
+ const keyDim = info[`${arch2}.attention.key_length`];
41852
+ const valDim = info[`${arch2}.attention.value_length`] ?? keyDim;
41853
+ const archMax = info[`${arch2}.context_length`];
41854
+ if (!nLayers || !nKVHeads || !keyDim || !valDim || !archMax)
41855
+ return null;
41856
+ const kvBytesPerToken = nLayers * nKVHeads * (keyDim + valDim) * 2;
41857
+ return { kvBytesPerToken, archMax };
41858
+ } catch {
41859
+ return null;
41860
+ }
41861
+ }
41862
+ async function createExpandedVariantAsync(baseModel, specs, sizeGB, kvBytesPerToken, archMax) {
41863
+ const customName = expandedModelName(baseModel);
41864
+ const ctx = calculateContextWindow(specs, sizeGB, kvBytesPerToken, archMax);
41865
+ try {
41866
+ const numPredict = Math.min(16384, Math.max(2048, Math.floor(ctx.numCtx * 0.25)));
41867
+ const modelfileContent = [
41868
+ `FROM ${baseModel}`,
41869
+ `PARAMETER num_ctx ${ctx.numCtx}`,
41870
+ `PARAMETER temperature 0`,
41871
+ `PARAMETER num_predict ${numPredict}`,
41872
+ `PARAMETER stop "<|endoftext|>"`
41873
+ ].join("\n");
41874
+ const modelDir2 = join54(homedir13(), ".open-agents", "models");
41875
+ mkdirSync15(modelDir2, { recursive: true });
41876
+ const modelfilePath = join54(modelDir2, `Modelfile.${customName}`);
41877
+ writeFileSync16(modelfilePath, modelfileContent + "\n", "utf8");
41878
+ await execAsync(`ollama create ${customName} -f ${modelfilePath}`, {
41879
+ timeout: 12e4
41880
+ });
41881
+ return customName;
41882
+ } catch {
41883
+ return null;
41884
+ }
41885
+ }
41886
+ async function ensureExpandedContext(modelName, backendUrl) {
41887
+ if (modelName.includes("cloud") || modelName.includes(":cloud")) {
41888
+ return { model: modelName, created: false, contextLabel: "remote", numCtx: 0 };
41889
+ }
41890
+ const specs = await detectSystemSpecsAsync();
41891
+ const kvInfo = await queryModelKVInfo(backendUrl, modelName);
41892
+ let sizeGB = 4;
41893
+ try {
41894
+ const models = await fetchOllamaModels(backendUrl);
41895
+ sizeGB = modelSizeGB(models, modelName);
41896
+ } catch {
41897
+ }
41898
+ const ctx = calculateContextWindow(specs, sizeGB, kvInfo?.kvBytesPerToken, kvInfo?.archMax);
41899
+ if (modelName.startsWith("open-agents-")) {
41900
+ try {
41901
+ const normalized = backendUrl.replace(/\/+$/, "");
41902
+ const showRes = await fetch(`${normalized}/api/show`, {
41903
+ method: "POST",
41904
+ headers: { "Content-Type": "application/json" },
41905
+ body: JSON.stringify({ name: modelName }),
41906
+ signal: AbortSignal.timeout(5e3)
41907
+ });
41908
+ if (showRes.ok) {
41909
+ const showData = await showRes.json();
41910
+ const numCtxMatch = showData.parameters?.match(/num_ctx\s+(\d+)/);
41911
+ const currentNumCtx = numCtxMatch ? parseInt(numCtxMatch[1], 10) : 0;
41912
+ if (currentNumCtx !== ctx.numCtx) {
41913
+ const fromMatch = showData.modelfile?.match(/^FROM\s+(.+)$/m);
41914
+ const baseModel = fromMatch?.[1]?.trim();
41915
+ if (baseModel && !baseModel.startsWith("open-agents-")) {
41916
+ await createExpandedVariantAsync(baseModel, specs, sizeGB, kvInfo?.kvBytesPerToken, kvInfo?.archMax);
42054
41917
  }
42055
41918
  }
42056
- stdin.removeListener("data", onData);
42057
- stdin.on("data", onInputData);
42058
- renderInput();
42059
- });
41919
+ }
41920
+ } catch {
42060
41921
  }
42061
- const onResize = () => render();
42062
- process.stdout.on("resize", onResize);
42063
- stdin.on("data", onData);
42064
- render();
42065
- });
41922
+ return { model: modelName, created: false, contextLabel: ctx.label, numCtx: ctx.numCtx };
41923
+ }
41924
+ const existing = await checkExpandedVariant(modelName, backendUrl);
41925
+ if (existing === null) {
41926
+ return { model: modelName, created: false, contextLabel: "", numCtx: 0 };
41927
+ }
41928
+ if (typeof existing === "string") {
41929
+ return { model: existing, created: false, contextLabel: ctx.label, numCtx: ctx.numCtx };
41930
+ }
41931
+ const created = await createExpandedVariantAsync(modelName, specs, sizeGB, kvInfo?.kvBytesPerToken, kvInfo?.archMax);
41932
+ if (created) {
41933
+ return { model: created, created: true, contextLabel: ctx.label, numCtx: ctx.numCtx };
41934
+ }
41935
+ return { model: modelName, created: false, contextLabel: ctx.label, numCtx: ctx.numCtx };
42066
41936
  }
42067
- var isTTY3, selectColors;
42068
- var init_tui_select = __esm({
42069
- "packages/cli/dist/tui/tui-select.js"() {
41937
+ async function ensureNeovim() {
41938
+ try {
41939
+ const nvimPath = execSync27("which nvim 2>/dev/null || where nvim 2>nul", {
41940
+ encoding: "utf8",
41941
+ stdio: "pipe",
41942
+ timeout: 5e3
41943
+ }).trim();
41944
+ if (nvimPath)
41945
+ return nvimPath;
41946
+ } catch {
41947
+ }
41948
+ const platform6 = process.platform;
41949
+ const arch2 = process.arch;
41950
+ if (platform6 === "linux") {
41951
+ const binDir = join54(homedir13(), ".local", "bin");
41952
+ const nvimDest = join54(binDir, "nvim");
41953
+ try {
41954
+ mkdirSync15(binDir, { recursive: true });
41955
+ } catch {
41956
+ }
41957
+ const appImageName = arch2 === "arm64" ? "nvim-linux-arm64.appimage" : "nvim-linux-x86_64.appimage";
41958
+ const url = `https://github.com/neovim/neovim/releases/latest/download/${appImageName}`;
41959
+ console.log(` Downloading Neovim (${appImageName})...`);
41960
+ try {
41961
+ execSync27(`curl -fsSL "${url}" -o "${nvimDest}"`, { stdio: "pipe", timeout: 6e4 });
41962
+ execSync27(`chmod +x "${nvimDest}"`, { stdio: "pipe", timeout: 3e3 });
41963
+ } catch (err) {
41964
+ console.log(` Failed to download Neovim: ${err instanceof Error ? err.message : String(err)}`);
41965
+ return null;
41966
+ }
41967
+ try {
41968
+ const ver = execSync27(`"${nvimDest}" --version`, { encoding: "utf8", stdio: "pipe", timeout: 5e3 }).split("\n")[0];
41969
+ console.log(` Installed: ${ver}`);
41970
+ } catch {
41971
+ console.log(" Warning: nvim binary downloaded but may not work (missing FUSE? Try: nvim --appimage-extract)");
41972
+ }
41973
+ if (!process.env.PATH?.includes(binDir)) {
41974
+ process.env.PATH = `${binDir}:${process.env.PATH}`;
41975
+ }
41976
+ ensurePathInShellRc(binDir);
41977
+ return nvimDest;
41978
+ }
41979
+ if (platform6 === "darwin") {
41980
+ if (hasCmd("brew")) {
41981
+ console.log(" Installing Neovim via Homebrew...");
41982
+ try {
41983
+ execSync27("brew install neovim", { stdio: "inherit", timeout: 12e4 });
41984
+ const nvimPath = execSync27("which nvim", { encoding: "utf8", stdio: "pipe", timeout: 3e3 }).trim();
41985
+ return nvimPath || null;
41986
+ } catch {
41987
+ console.log(" brew install neovim failed.");
41988
+ return null;
41989
+ }
41990
+ }
41991
+ console.log(" Homebrew not found. Install Neovim: brew install neovim");
41992
+ return null;
41993
+ }
41994
+ if (platform6 === "win32") {
41995
+ if (hasCmd("choco")) {
41996
+ console.log(" Installing Neovim via Chocolatey...");
41997
+ try {
41998
+ execSync27("choco install neovim -y", { stdio: "inherit", timeout: 12e4 });
41999
+ return "nvim";
42000
+ } catch {
42001
+ console.log(" choco install neovim failed.");
42002
+ }
42003
+ }
42004
+ if (hasCmd("winget")) {
42005
+ console.log(" Installing Neovim via winget...");
42006
+ try {
42007
+ execSync27("winget install Neovim.Neovim --accept-source-agreements --accept-package-agreements", {
42008
+ stdio: "inherit",
42009
+ timeout: 12e4
42010
+ });
42011
+ return "nvim";
42012
+ } catch {
42013
+ console.log(" winget install neovim failed.");
42014
+ }
42015
+ }
42016
+ console.log(" Install Neovim manually: https://neovim.io");
42017
+ return null;
42018
+ }
42019
+ return null;
42020
+ }
42021
+ function ensurePathInShellRc(binDir) {
42022
+ const shell = process.env.SHELL ?? "";
42023
+ const rcFile = shell.includes("zsh") ? join54(homedir13(), ".zshrc") : join54(homedir13(), ".bashrc");
42024
+ try {
42025
+ const rcContent = existsSync37(rcFile) ? readFileSync28(rcFile, "utf8") : "";
42026
+ if (rcContent.includes(binDir))
42027
+ return;
42028
+ const exportLine = `
42029
+ export PATH="${binDir}:$PATH" # Added by open-agents for nvim
42030
+ `;
42031
+ appendFileSync2(rcFile, exportLine, "utf8");
42032
+ console.log(` Added ${binDir} to ${rcFile}`);
42033
+ } catch {
42034
+ }
42035
+ }
42036
+ var execAsync, QWEN_VARIANTS, _toolSupportCache, _cloudflaredInstallPromise;
42037
+ var init_setup = __esm({
42038
+ "packages/cli/dist/tui/setup.js"() {
42070
42039
  "use strict";
42071
- init_overlay_lock();
42072
- isTTY3 = process.stdout.isTTY ?? false;
42073
- selectColors = {
42074
- orange: (t) => fg2562(208, t),
42075
- green: (t) => ansi3("32", t),
42076
- dim: (t) => ansi3("2", t),
42077
- bold: (t) => ansi3("1", t),
42078
- cyan: (t) => ansi3("36", t),
42079
- /** Lighter grey for filter matches (252 = near-white) */
42080
- matchLight: (t) => fg2562(252, t),
42081
- /** Dark grey for non-matching items (240 = dark grey) */
42082
- matchDark: (t) => fg2562(240, t)
42083
- };
42040
+ init_model_picker();
42041
+ init_render();
42042
+ init_config();
42043
+ init_dist();
42044
+ init_tui_select();
42045
+ execAsync = promisify6(exec2);
42046
+ QWEN_VARIANTS = [
42047
+ { tag: "qwen3.5:0.8b", sizeGB: 1, label: "0.8B params (1.0 GB)", cloud: false },
42048
+ { tag: "qwen3.5:2b", sizeGB: 2.7, label: "2B params (2.7 GB)", cloud: false },
42049
+ { tag: "qwen3.5:4b", sizeGB: 3.4, label: "4B params (3.4 GB)", cloud: false },
42050
+ { tag: "qwen3.5:9b", sizeGB: 6.6, label: "9B params (6.6 GB) \u2014 recommended minimum", cloud: false },
42051
+ { tag: "qwen3.5:27b", sizeGB: 17, label: "27B params (17 GB)", cloud: false },
42052
+ { tag: "qwen3.5:35b", sizeGB: 24, label: "35B params (24 GB)", cloud: false },
42053
+ { tag: "qwen3.5:122b", sizeGB: 81, label: "122B params (81 GB) \u2014 best local", cloud: false },
42054
+ { tag: "qwen3.5:cloud", sizeGB: 0, label: "Cloud (Ollama Cloud)", cloud: true },
42055
+ { tag: "qwen3.5:397b-cloud", sizeGB: 0, label: "397B Cloud (Ollama Cloud)", cloud: true }
42056
+ ];
42057
+ _toolSupportCache = /* @__PURE__ */ new Map();
42058
+ _cloudflaredInstallPromise = null;
42084
42059
  }
42085
42060
  });
42086
42061
 
@@ -50633,11 +50608,11 @@ async function handleUpdate(subcommand, ctx) {
50633
50608
  }
50634
50609
  let currentVersion = "0.0.0";
50635
50610
  try {
50636
- const { createRequire: createRequire5 } = await import("node:module");
50611
+ const { createRequire: createRequire6 } = await import("node:module");
50637
50612
  const { fileURLToPath: fileURLToPath15 } = await import("node:url");
50638
50613
  const { dirname: dirname23, join: join76 } = await import("node:path");
50639
50614
  const { existsSync: existsSync56 } = await import("node:fs");
50640
- const req = createRequire5(import.meta.url);
50615
+ const req = createRequire6(import.meta.url);
50641
50616
  const thisDir = dirname23(fileURLToPath15(import.meta.url));
50642
50617
  const candidates = [
50643
50618
  join76(thisDir, "..", "package.json"),
@@ -51817,8 +51792,20 @@ var init_project_context = __esm({
51817
51792
  });
51818
51793
 
51819
51794
  // packages/memory/dist/db.js
51820
- import Database from "better-sqlite3";
51795
+ import { createRequire as createRequire2 } from "node:module";
51796
+ function getDatabase() {
51797
+ if (_Database !== null)
51798
+ return _Database;
51799
+ try {
51800
+ const req = createRequire2(import.meta.url);
51801
+ _Database = req("better-sqlite3");
51802
+ } catch {
51803
+ throw new Error("better-sqlite3 is not available (native module build may have failed).\nTry: npm rebuild better-sqlite3\nMemory features (SQLite) are disabled until this is resolved.");
51804
+ }
51805
+ return _Database;
51806
+ }
51821
51807
  function initDb(dbPath) {
51808
+ const Database = getDatabase();
51822
51809
  const db = new Database(dbPath);
51823
51810
  db.pragma("journal_mode = WAL");
51824
51811
  db.pragma("foreign_keys = ON");
@@ -52040,9 +52027,11 @@ function runMigrations(db) {
52040
52027
  ON memory_link (target_id);
52041
52028
  `);
52042
52029
  }
52030
+ var _Database;
52043
52031
  var init_db = __esm({
52044
52032
  "packages/memory/dist/db.js"() {
52045
52033
  "use strict";
52034
+ _Database = null;
52046
52035
  }
52047
52036
  });
52048
52037
 
@@ -62102,7 +62091,7 @@ __export(serve_exports, {
62102
62091
  });
62103
62092
  import * as http from "node:http";
62104
62093
  import * as https from "node:https";
62105
- import { createRequire as createRequire2 } from "node:module";
62094
+ import { createRequire as createRequire3 } from "node:module";
62106
62095
  import { fileURLToPath as fileURLToPath12 } from "node:url";
62107
62096
  import { dirname as dirname20, join as join69, resolve as resolve31 } from "node:path";
62108
62097
  import { spawn as spawn20 } from "node:child_process";
@@ -62110,7 +62099,7 @@ import { mkdirSync as mkdirSync26, writeFileSync as writeFileSync25, readFileSyn
62110
62099
  import { randomBytes as randomBytes16 } from "node:crypto";
62111
62100
  function getVersion3() {
62112
62101
  try {
62113
- const require2 = createRequire2(import.meta.url);
62102
+ const require2 = createRequire3(import.meta.url);
62114
62103
  const thisDir = dirname20(fileURLToPath12(import.meta.url));
62115
62104
  const candidates = [
62116
62105
  join69(thisDir, "..", "package.json"),
@@ -63530,7 +63519,7 @@ import * as readline2 from "node:readline";
63530
63519
  import { Writable } from "node:stream";
63531
63520
  import { cwd } from "node:process";
63532
63521
  import { resolve as resolve32, join as join70, dirname as dirname21, extname as extname11 } from "node:path";
63533
- import { createRequire as createRequire3 } from "node:module";
63522
+ import { createRequire as createRequire4 } from "node:module";
63534
63523
  import { fileURLToPath as fileURLToPath13 } from "node:url";
63535
63524
  import { readFileSync as readFileSync42, writeFileSync as writeFileSync26, appendFileSync as appendFileSync4, rmSync as rmSync3, readdirSync as readdirSync21, mkdirSync as mkdirSync27 } from "node:fs";
63536
63525
  import { existsSync as existsSync53 } from "node:fs";
@@ -63551,7 +63540,7 @@ function formatTimeAgo(date) {
63551
63540
  }
63552
63541
  function getVersion4() {
63553
63542
  try {
63554
- const require2 = createRequire3(import.meta.url);
63543
+ const require2 = createRequire4(import.meta.url);
63555
63544
  const thisDir = dirname21(fileURLToPath13(import.meta.url));
63556
63545
  const candidates = [
63557
63546
  join70(thisDir, "..", "package.json"),
@@ -64881,7 +64870,7 @@ async function startInteractive(config, repoPath) {
64881
64870
  initOaDirectory(repoRoot);
64882
64871
  const savedSettings = resolveSettings(repoRoot);
64883
64872
  if (process.stdout.isTTY) {
64884
- process.stdout.write("\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1015l\x1B[?1049h\x1B[48;5;233m\x1B[2J\x1B[3J\x1B[H\x1B[?25l\x1B[?1002h\x1B[?1006h");
64873
+ process.stdout.write("\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1015l\x1B[?1049h\x1B[48;5;233m\x1B[2J\x1B[3J\x1B[H\x1B[?25l");
64885
64874
  const restoreScreen = () => {
64886
64875
  process.stdout.write("\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1015l\x1B[?25h\x1B[?1049l");
64887
64876
  };
@@ -65403,6 +65392,9 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
65403
65392
  statusBar.handlePointerEvent(type, col, row);
65404
65393
  });
65405
65394
  process.stdin.pipe(mouseFilter);
65395
+ if (process.stdout.isTTY) {
65396
+ process.stdout.write("\x1B[?1002h\x1B[?1006h");
65397
+ }
65406
65398
  process.stdin.on("error", (err) => {
65407
65399
  if (err.code === "EIO" || err.code === "ENOTTY")
65408
65400
  return;
@@ -69314,7 +69306,7 @@ init_config();
69314
69306
  init_output();
69315
69307
  init_updater();
69316
69308
  import { parseArgs as nodeParseArgs2 } from "node:util";
69317
- import { createRequire as createRequire4 } from "node:module";
69309
+ import { createRequire as createRequire5 } from "node:module";
69318
69310
  import { fileURLToPath as fileURLToPath14 } from "node:url";
69319
69311
  import { dirname as dirname22, join as join75 } from "node:path";
69320
69312
 
@@ -69422,7 +69414,7 @@ init_spinner();
69422
69414
  init_output();
69423
69415
  function getVersion5() {
69424
69416
  try {
69425
- const require2 = createRequire4(import.meta.url);
69417
+ const require2 = createRequire5(import.meta.url);
69426
69418
  const pkgPath = join75(dirname22(fileURLToPath14(import.meta.url)), "..", "package.json");
69427
69419
  const pkg = require2(pkgPath);
69428
69420
  return pkg.version;