open-agents-ai 0.184.67 → 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.
- package/dist/index.js +2225 -2247
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39665,2422 +39665,2397 @@ var init_oa_directory = __esm({
|
|
|
39665
39665
|
}
|
|
39666
39666
|
});
|
|
39667
39667
|
|
|
39668
|
-
// packages/cli/dist/tui/
|
|
39669
|
-
|
|
39670
|
-
|
|
39671
|
-
|
|
39672
|
-
|
|
39673
|
-
|
|
39674
|
-
|
|
39675
|
-
|
|
39676
|
-
|
|
39677
|
-
|
|
39678
|
-
|
|
39679
|
-
|
|
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;
|
|
39697
|
-
}
|
|
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;
|
|
39698
39680
|
}
|
|
39699
|
-
function
|
|
39700
|
-
|
|
39701
|
-
|
|
39702
|
-
|
|
39703
|
-
|
|
39704
|
-
|
|
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;
|
|
39720
|
-
}
|
|
39721
|
-
}
|
|
39722
|
-
} catch {
|
|
39681
|
+
function isOverlayActive() {
|
|
39682
|
+
return _overlayActive;
|
|
39683
|
+
}
|
|
39684
|
+
function overlayWrite(data) {
|
|
39685
|
+
if (_origStdoutWrite) {
|
|
39686
|
+
return _origStdoutWrite.call(process.stdout, data);
|
|
39723
39687
|
}
|
|
39724
|
-
|
|
39725
|
-
|
|
39726
|
-
|
|
39727
|
-
|
|
39728
|
-
|
|
39729
|
-
|
|
39730
|
-
|
|
39731
|
-
|
|
39732
|
-
|
|
39733
|
-
|
|
39734
|
-
|
|
39735
|
-
|
|
39736
|
-
|
|
39737
|
-
|
|
39688
|
+
return process.stdout.write(data);
|
|
39689
|
+
}
|
|
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
|
+
};
|
|
39738
39716
|
}
|
|
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
39717
|
}
|
|
39746
|
-
|
|
39747
|
-
|
|
39748
|
-
|
|
39749
|
-
|
|
39750
|
-
|
|
39751
|
-
|
|
39752
|
-
|
|
39753
|
-
|
|
39754
|
-
|
|
39755
|
-
|
|
39756
|
-
|
|
39757
|
-
|
|
39758
|
-
|
|
39759
|
-
|
|
39760
|
-
|
|
39761
|
-
|
|
39762
|
-
totalRamGB = bytes / 1024 ** 3;
|
|
39763
|
-
availableRamGB = totalRamGB * 0.7;
|
|
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 {
|
|
39764
39734
|
}
|
|
39765
39735
|
}
|
|
39766
|
-
|
|
39767
|
-
|
|
39768
|
-
|
|
39769
|
-
|
|
39770
|
-
|
|
39771
|
-
|
|
39772
|
-
|
|
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];
|
|
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);
|
|
39779
39743
|
}
|
|
39780
39744
|
}
|
|
39781
|
-
} catch {
|
|
39782
39745
|
}
|
|
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
|
|
39788
|
-
};
|
|
39789
39746
|
}
|
|
39790
|
-
|
|
39791
|
-
|
|
39792
|
-
|
|
39793
|
-
|
|
39794
|
-
|
|
39795
|
-
|
|
39796
|
-
|
|
39797
|
-
|
|
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;
|
|
39798
39757
|
}
|
|
39799
|
-
|
|
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;
|
|
39800
39763
|
}
|
|
39801
|
-
function
|
|
39802
|
-
|
|
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);
|
|
39822
|
-
}
|
|
39823
|
-
const label = numCtx >= 1024 ? `${Math.floor(numCtx / 1024)}K` : String(numCtx);
|
|
39824
|
-
return { numCtx, label };
|
|
39764
|
+
function fg2562(code, text) {
|
|
39765
|
+
return isTTY3 ? `\x1B[38;5;${code}m${text}\x1B[0m` : text;
|
|
39825
39766
|
}
|
|
39826
|
-
function
|
|
39827
|
-
return
|
|
39828
|
-
rl.question(question, (answer) => resolve36(answer.trim()));
|
|
39829
|
-
});
|
|
39767
|
+
function stripAnsi(s) {
|
|
39768
|
+
return s.replace(/\x1B\[[0-9;]*m/g, "");
|
|
39830
39769
|
}
|
|
39831
|
-
function
|
|
39832
|
-
|
|
39833
|
-
|
|
39834
|
-
|
|
39835
|
-
|
|
39836
|
-
const hadRawMode = stdin.isRaw;
|
|
39837
|
-
if (typeof stdin.setRawMode === "function") {
|
|
39838
|
-
stdin.setRawMode(true);
|
|
39839
|
-
}
|
|
39840
|
-
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");
|
|
39864
|
-
}
|
|
39865
|
-
} else if (c3.charCodeAt(0) >= 32) {
|
|
39866
|
-
secret += c3;
|
|
39867
|
-
process.stdout.write("*");
|
|
39868
|
-
}
|
|
39869
|
-
}
|
|
39870
|
-
};
|
|
39871
|
-
stdin.on("data", onData);
|
|
39872
|
-
});
|
|
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}`;
|
|
39873
39775
|
}
|
|
39874
|
-
function
|
|
39875
|
-
if (
|
|
39876
|
-
return
|
|
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;
|
|
39776
|
+
function matchRow(item, focused, isActive) {
|
|
39777
|
+
if (focused || isActive) {
|
|
39778
|
+
return defaultRenderRow(item, focused, isActive);
|
|
39882
39779
|
}
|
|
39883
|
-
|
|
39884
|
-
|
|
39885
|
-
const
|
|
39886
|
-
|
|
39887
|
-
|
|
39888
|
-
|
|
39889
|
-
|
|
39890
|
-
|
|
39891
|
-
|
|
39892
|
-
];
|
|
39893
|
-
|
|
39894
|
-
|
|
39895
|
-
|
|
39896
|
-
|
|
39897
|
-
|
|
39898
|
-
|
|
39899
|
-
|
|
39900
|
-
|
|
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));
|
|
39801
|
+
} else {
|
|
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);
|
|
39901
39811
|
}
|
|
39902
|
-
} catch {
|
|
39903
|
-
process.stdout.write(` ${c2.yellow("\u26A0")} ${s.label} install failed, trying next...
|
|
39904
|
-
`);
|
|
39905
39812
|
}
|
|
39906
39813
|
}
|
|
39907
39814
|
}
|
|
39908
|
-
|
|
39909
|
-
|
|
39910
|
-
|
|
39911
|
-
|
|
39912
|
-
|
|
39913
|
-
|
|
39914
|
-
|
|
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;
|
|
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;
|
|
39954
39822
|
}
|
|
39955
|
-
|
|
39956
|
-
|
|
39957
|
-
|
|
39958
|
-
|
|
39959
|
-
|
|
39960
|
-
|
|
39961
|
-
${c2.red("\u2716")} Ollama install failed: ${err instanceof Error ? err.message : String(err)}
|
|
39962
|
-
`);
|
|
39963
|
-
return false;
|
|
39823
|
+
return -1;
|
|
39824
|
+
};
|
|
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;
|
|
39964
39829
|
}
|
|
39965
|
-
|
|
39966
|
-
|
|
39967
|
-
|
|
39968
|
-
|
|
39969
|
-
|
|
39970
|
-
|
|
39971
|
-
|
|
39972
|
-
|
|
39973
|
-
|
|
39974
|
-
|
|
39975
|
-
|
|
39976
|
-
|
|
39977
|
-
|
|
39978
|
-
|
|
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;
|
|
39837
|
+
return new Promise((resolve36) => {
|
|
39838
|
+
const stdin = process.stdin;
|
|
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);
|
|
39979
39848
|
}
|
|
39980
39849
|
}
|
|
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;
|
|
39993
|
-
}
|
|
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;
|
|
39999
|
-
}
|
|
40000
|
-
}
|
|
40001
|
-
process.stdout.write(`
|
|
40002
|
-
${c2.cyan("\u25CF")} Installing Ollama via Homebrew...
|
|
40003
|
-
|
|
40004
|
-
`);
|
|
40005
|
-
try {
|
|
40006
|
-
execSync27("brew install ollama", {
|
|
40007
|
-
stdio: "inherit",
|
|
40008
|
-
timeout: 3e5
|
|
40009
|
-
});
|
|
40010
|
-
if (hasCmd("ollama")) {
|
|
40011
|
-
process.stdout.write(`
|
|
40012
|
-
${c2.green("\u2714")} Ollama installed successfully.
|
|
40013
|
-
`);
|
|
40014
|
-
return true;
|
|
40015
39850
|
}
|
|
40016
|
-
|
|
40017
|
-
|
|
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;
|
|
40025
|
-
}
|
|
40026
|
-
}
|
|
40027
|
-
function installOllamaWindows() {
|
|
40028
|
-
process.stdout.write(`
|
|
40029
|
-
${c2.cyan("\u25CF")} Installing Ollama via PowerShell...
|
|
40030
|
-
|
|
40031
|
-
`);
|
|
40032
|
-
try {
|
|
40033
|
-
execSync27('powershell -Command "irm https://ollama.com/install.ps1 | iex"', {
|
|
40034
|
-
stdio: "inherit",
|
|
40035
|
-
timeout: 3e5
|
|
40036
|
-
});
|
|
40037
|
-
if (hasCmd("ollama")) {
|
|
40038
|
-
process.stdout.write(`
|
|
40039
|
-
${c2.green("\u2714")} Ollama installed successfully.
|
|
40040
|
-
`);
|
|
40041
|
-
return true;
|
|
39851
|
+
if (typeof stdin.setRawMode === "function") {
|
|
39852
|
+
stdin.setRawMode(true);
|
|
40042
39853
|
}
|
|
40043
|
-
|
|
40044
|
-
|
|
40045
|
-
|
|
40046
|
-
|
|
40047
|
-
|
|
40048
|
-
|
|
40049
|
-
|
|
40050
|
-
|
|
40051
|
-
|
|
40052
|
-
|
|
40053
|
-
}
|
|
40054
|
-
|
|
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;
|
|
39854
|
+
stdin.resume();
|
|
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;
|
|
40064
39866
|
}
|
|
40065
|
-
|
|
40066
|
-
|
|
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;
|
|
39867
|
+
const maxOffset = Math.max(0, displayList.length - maxVisible);
|
|
39868
|
+
scrollOffset = Math.max(0, Math.min(maxOffset, scrollOffset));
|
|
40079
39869
|
}
|
|
40080
|
-
|
|
40081
|
-
|
|
40082
|
-
|
|
40083
|
-
|
|
40084
|
-
|
|
40085
|
-
|
|
40086
|
-
|
|
40087
|
-
|
|
40088
|
-
|
|
40089
|
-
`);
|
|
40090
|
-
return false;
|
|
40091
|
-
}
|
|
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;
|
|
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}`);
|
|
40103
39879
|
}
|
|
40104
|
-
|
|
40105
|
-
|
|
40106
|
-
|
|
40107
|
-
|
|
40108
|
-
|
|
40109
|
-
`);
|
|
40110
|
-
return false;
|
|
40111
|
-
}
|
|
40112
|
-
function pullModelWithAutoUpdate(tag) {
|
|
40113
|
-
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.");
|
|
39880
|
+
if (title) {
|
|
39881
|
+
if (!hasBreadcrumbs)
|
|
39882
|
+
lines.push("");
|
|
39883
|
+
lines.push(` ${selectColors.bold(title)}`);
|
|
40129
39884
|
}
|
|
40130
|
-
|
|
40131
|
-
|
|
40132
|
-
`);
|
|
40133
|
-
|
|
40134
|
-
|
|
40135
|
-
|
|
40136
|
-
|
|
40137
|
-
|
|
40138
|
-
|
|
40139
|
-
|
|
40140
|
-
|
|
40141
|
-
|
|
40142
|
-
|
|
40143
|
-
|
|
40144
|
-
|
|
40145
|
-
|
|
40146
|
-
|
|
40147
|
-
|
|
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);
|
|
39898
|
+
}
|
|
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;
|
|
40148
39909
|
});
|
|
40149
|
-
}
|
|
40150
|
-
|
|
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}`);
|
|
39910
|
+
} else {
|
|
39911
|
+
displayList = items.map((_, i) => i);
|
|
40155
39912
|
}
|
|
40156
|
-
|
|
40157
|
-
|
|
40158
|
-
|
|
40159
|
-
|
|
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)";
|
|
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`)}`);
|
|
40200
39918
|
}
|
|
40201
|
-
|
|
40202
|
-
|
|
40203
|
-
|
|
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;
|
|
39926
|
+
}
|
|
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")}`);
|
|
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;
|
|
40204
39957
|
}
|
|
40205
|
-
|
|
40206
|
-
|
|
40207
|
-
|
|
40208
|
-
|
|
40209
|
-
|
|
40210
|
-
|
|
40211
|
-
|
|
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 };
|
|
40219
|
-
}
|
|
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));
|
|
40225
|
-
}
|
|
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;
|
|
40235
|
-
}
|
|
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 {
|
|
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);
|
|
40254
39965
|
}
|
|
40255
|
-
|
|
40256
|
-
|
|
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
|
-
`);
|
|
40264
|
-
return;
|
|
40265
|
-
}
|
|
40266
|
-
} catch {
|
|
39966
|
+
for (const { event, fn } of savedRlListeners) {
|
|
39967
|
+
stdin.on(event, fn);
|
|
40267
39968
|
}
|
|
40268
|
-
|
|
40269
|
-
|
|
40270
|
-
|
|
40271
|
-
|
|
40272
|
-
`);
|
|
40273
|
-
}
|
|
40274
|
-
function checkPythonVenv() {
|
|
40275
|
-
try {
|
|
40276
|
-
execSync27("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
|
|
40277
|
-
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
|
-
}
|
|
40285
|
-
}
|
|
40286
|
-
}
|
|
40287
|
-
function ensurePythonVenv() {
|
|
40288
|
-
process.stdout.write(` ${c2.cyan("\u25CF")} Installing python3-venv...
|
|
40289
|
-
`);
|
|
40290
|
-
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" }
|
|
40295
|
-
];
|
|
40296
|
-
for (const s of strategies) {
|
|
40297
|
-
if (hasCmd(s.check)) {
|
|
40298
|
-
try {
|
|
40299
|
-
execSync27(s.install, { stdio: "inherit", timeout: 12e4 });
|
|
40300
|
-
if (checkPythonVenv()) {
|
|
40301
|
-
process.stdout.write(` ${c2.green("\u2714")} python3-venv installed via ${s.label}.
|
|
40302
|
-
`);
|
|
40303
|
-
return;
|
|
40304
|
-
}
|
|
40305
|
-
} catch {
|
|
39969
|
+
if (rl) {
|
|
39970
|
+
rl.resume();
|
|
39971
|
+
rl.prompt(false);
|
|
40306
39972
|
}
|
|
40307
39973
|
}
|
|
40308
|
-
|
|
40309
|
-
|
|
40310
|
-
|
|
40311
|
-
|
|
40312
|
-
|
|
40313
|
-
|
|
40314
|
-
|
|
40315
|
-
|
|
40316
|
-
|
|
40317
|
-
|
|
40318
|
-
|
|
40319
|
-
|
|
40320
|
-
|
|
40321
|
-
|
|
40322
|
-
|
|
40323
|
-
|
|
40324
|
-
|
|
40325
|
-
|
|
40326
|
-
|
|
40327
|
-
|
|
40328
|
-
|
|
40329
|
-
|
|
40330
|
-
|
|
40331
|
-
|
|
40332
|
-
|
|
40333
|
-
|
|
40334
|
-
|
|
40335
|
-
|
|
40336
|
-
|
|
40337
|
-
|
|
40338
|
-
|
|
40339
|
-
|
|
40340
|
-
|
|
40341
|
-
|
|
40342
|
-
|
|
40343
|
-
|
|
40344
|
-
|
|
40345
|
-
|
|
40346
|
-
|
|
40347
|
-
|
|
40348
|
-
|
|
40349
|
-
|
|
40350
|
-
|
|
40351
|
-
|
|
40352
|
-
|
|
40353
|
-
|
|
40354
|
-
|
|
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;
|
|
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
|
+
}
|
|
40390
40021
|
}
|
|
40391
|
-
} catch {
|
|
40392
40022
|
}
|
|
40393
|
-
|
|
40394
|
-
|
|
40395
|
-
|
|
40396
|
-
|
|
40397
|
-
|
|
40398
|
-
|
|
40399
|
-
|
|
40400
|
-
|
|
40401
|
-
|
|
40402
|
-
|
|
40403
|
-
|
|
40404
|
-
|
|
40405
|
-
|
|
40406
|
-
|
|
40407
|
-
|
|
40408
|
-
|
|
40409
|
-
|
|
40410
|
-
|
|
40411
|
-
|
|
40412
|
-
|
|
40413
|
-
|
|
40414
|
-
|
|
40415
|
-
|
|
40416
|
-
|
|
40417
|
-
|
|
40418
|
-
|
|
40419
|
-
|
|
40420
|
-
|
|
40421
|
-
|
|
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.
|
|
40431
|
-
`);
|
|
40432
|
-
process.stdout.write(` ${c2.green("\u2714")} Backend type: ${c2.bold(backendType)}
|
|
40433
|
-
|
|
40434
|
-
`);
|
|
40435
|
-
return chosenModel;
|
|
40436
|
-
}
|
|
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;
|
|
40461
|
-
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")}
|
|
40507
|
-
|
|
40508
|
-
`);
|
|
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).
|
|
40512
|
-
`);
|
|
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");
|
|
40517
|
-
}
|
|
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).
|
|
40523
|
-
`);
|
|
40524
|
-
const installVenv = await ask(rl, ` ${c2.bold("Install python3-venv?")} (Y/n) `);
|
|
40525
|
-
if (installVenv.toLowerCase() !== "n") {
|
|
40526
|
-
ensurePythonVenv();
|
|
40527
|
-
}
|
|
40528
|
-
}
|
|
40529
|
-
}
|
|
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)}
|
|
40536
|
-
|
|
40537
|
-
`);
|
|
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
|
-
`);
|
|
40551
|
-
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
|
-
}
|
|
40565
|
-
} catch {
|
|
40566
|
-
process.stdout.write(` ${c2.yellow("\u26A0")} Could not start Ollama. Try running ${c2.bold("ollama serve")} manually.
|
|
40567
|
-
|
|
40568
|
-
`);
|
|
40569
|
-
}
|
|
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
|
-
`);
|
|
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();
|
|
40592
40052
|
}
|
|
40593
|
-
} catch {
|
|
40594
|
-
process.stdout.write(` ${c2.yellow("\u26A0")} Ollama installed. Start it with: ${c2.bold("ollama serve")}
|
|
40595
|
-
|
|
40596
|
-
`);
|
|
40597
40053
|
}
|
|
40598
40054
|
}
|
|
40599
|
-
}
|
|
40600
|
-
|
|
40601
|
-
|
|
40602
|
-
|
|
40603
|
-
|
|
40604
|
-
|
|
40605
|
-
|
|
40606
|
-
|
|
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
|
+
}
|
|
40607
40068
|
}
|
|
40608
40069
|
}
|
|
40609
|
-
|
|
40610
|
-
|
|
40611
|
-
|
|
40612
|
-
|
|
40613
|
-
|
|
40614
|
-
|
|
40615
|
-
|
|
40616
|
-
|
|
40617
|
-
|
|
40618
|
-
|
|
40619
|
-
|
|
40620
|
-
|
|
40621
|
-
|
|
40622
|
-
|
|
40623
|
-
|
|
40624
|
-
|
|
40625
|
-
|
|
40626
|
-
|
|
40627
|
-
|
|
40628
|
-
|
|
40629
|
-
|
|
40630
|
-
|
|
40631
|
-
|
|
40632
|
-
|
|
40633
|
-
|
|
40634
|
-
|
|
40635
|
-
|
|
40636
|
-
|
|
40637
|
-
|
|
40638
|
-
|
|
40639
|
-
|
|
40640
|
-
|
|
40641
|
-
|
|
40642
|
-
|
|
40643
|
-
|
|
40644
|
-
|
|
40645
|
-
|
|
40646
|
-
|
|
40647
|
-
|
|
40648
|
-
|
|
40649
|
-
|
|
40650
|
-
|
|
40651
|
-
|
|
40652
|
-
|
|
40653
|
-
|
|
40654
|
-
|
|
40655
|
-
|
|
40656
|
-
|
|
40657
|
-
|
|
40658
|
-
|
|
40659
|
-
|
|
40660
|
-
|
|
40661
|
-
|
|
40662
|
-
|
|
40663
|
-
|
|
40664
|
-
|
|
40665
|
-
|
|
40666
|
-
|
|
40667
|
-
|
|
40668
|
-
|
|
40669
|
-
|
|
40670
|
-
|
|
40671
|
-
|
|
40672
|
-
|
|
40673
|
-
|
|
40674
|
-
|
|
40675
|
-
|
|
40676
|
-
|
|
40677
|
-
|
|
40678
|
-
|
|
40679
|
-
|
|
40680
|
-
|
|
40681
|
-
|
|
40682
|
-
|
|
40683
|
-
|
|
40684
|
-
|
|
40685
|
-
|
|
40686
|
-
|
|
40687
|
-
|
|
40688
|
-
|
|
40689
|
-
|
|
40690
|
-
|
|
40691
|
-
|
|
40692
|
-
|
|
40693
|
-
|
|
40694
|
-
|
|
40695
|
-
|
|
40696
|
-
|
|
40697
|
-
|
|
40698
|
-
|
|
40699
|
-
|
|
40700
|
-
|
|
40701
|
-
|
|
40702
|
-
|
|
40703
|
-
|
|
40704
|
-
|
|
40705
|
-
|
|
40706
|
-
|
|
40707
|
-
|
|
40708
|
-
|
|
40709
|
-
|
|
40710
|
-
|
|
40711
|
-
|
|
40712
|
-
|
|
40713
|
-
|
|
40714
|
-
|
|
40715
|
-
|
|
40716
|
-
|
|
40717
|
-
|
|
40718
|
-
|
|
40719
|
-
|
|
40720
|
-
|
|
40721
|
-
|
|
40722
|
-
|
|
40723
|
-
|
|
40724
|
-
|
|
40725
|
-
|
|
40726
|
-
|
|
40727
|
-
|
|
40728
|
-
|
|
40729
|
-
|
|
40730
|
-
|
|
40731
|
-
|
|
40732
|
-
|
|
40733
|
-
|
|
40734
|
-
|
|
40735
|
-
|
|
40736
|
-
|
|
40737
|
-
|
|
40738
|
-
|
|
40739
|
-
|
|
40740
|
-
|
|
40741
|
-
|
|
40742
|
-
|
|
40743
|
-
|
|
40744
|
-
|
|
40745
|
-
|
|
40746
|
-
|
|
40747
|
-
|
|
40748
|
-
|
|
40749
|
-
|
|
40750
|
-
|
|
40751
|
-
|
|
40752
|
-
|
|
40753
|
-
|
|
40754
|
-
|
|
40755
|
-
|
|
40756
|
-
|
|
40757
|
-
|
|
40758
|
-
|
|
40759
|
-
|
|
40760
|
-
|
|
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;
|
|
40114
|
+
}
|
|
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();
|
|
40761
40259
|
}
|
|
40762
40260
|
}
|
|
40763
|
-
|
|
40764
|
-
|
|
40765
|
-
|
|
40766
|
-
|
|
40767
|
-
|
|
40768
|
-
|
|
40769
|
-
|
|
40770
|
-
|
|
40771
|
-
|
|
40772
|
-
|
|
40773
|
-
|
|
40774
|
-
|
|
40775
|
-
|
|
40776
|
-
|
|
40777
|
-
|
|
40778
|
-
|
|
40779
|
-
|
|
40780
|
-
|
|
40781
|
-
|
|
40782
|
-
|
|
40783
|
-
|
|
40784
|
-
|
|
40785
|
-
|
|
40786
|
-
|
|
40787
|
-
|
|
40788
|
-
|
|
40789
|
-
|
|
40790
|
-
|
|
40791
|
-
|
|
40792
|
-
|
|
40793
|
-
|
|
40794
|
-
|
|
40795
|
-
|
|
40796
|
-
|
|
40797
|
-
|
|
40798
|
-
|
|
40799
|
-
|
|
40800
|
-
|
|
40801
|
-
|
|
40802
|
-
|
|
40803
|
-
|
|
40804
|
-
|
|
40805
|
-
|
|
40806
|
-
|
|
40807
|
-
|
|
40808
|
-
|
|
40809
|
-
|
|
40810
|
-
|
|
40811
|
-
|
|
40812
|
-
|
|
40813
|
-
|
|
40814
|
-
|
|
40815
|
-
|
|
40816
|
-
|
|
40817
|
-
|
|
40818
|
-
|
|
40819
|
-
|
|
40820
|
-
|
|
40821
|
-
|
|
40822
|
-
}
|
|
40823
|
-
|
|
40824
|
-
|
|
40825
|
-
|
|
40826
|
-
|
|
40827
|
-
|
|
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;
|
|
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
|
+
});
|
|
40914
40326
|
}
|
|
40915
|
-
|
|
40916
|
-
|
|
40327
|
+
const onResize = () => render();
|
|
40328
|
+
process.stdout.on("resize", onResize);
|
|
40329
|
+
stdin.on("data", onData);
|
|
40330
|
+
render();
|
|
40331
|
+
});
|
|
40917
40332
|
}
|
|
40918
|
-
|
|
40919
|
-
|
|
40920
|
-
|
|
40921
|
-
|
|
40922
|
-
|
|
40923
|
-
|
|
40924
|
-
|
|
40925
|
-
|
|
40926
|
-
|
|
40927
|
-
|
|
40928
|
-
|
|
40929
|
-
|
|
40930
|
-
|
|
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
|
+
};
|
|
40931
40350
|
}
|
|
40932
|
-
|
|
40933
|
-
|
|
40934
|
-
|
|
40935
|
-
|
|
40936
|
-
|
|
40937
|
-
|
|
40938
|
-
|
|
40939
|
-
|
|
40940
|
-
|
|
40941
|
-
|
|
40942
|
-
|
|
40943
|
-
|
|
40944
|
-
|
|
40945
|
-
|
|
40946
|
-
|
|
40947
|
-
|
|
40948
|
-
|
|
40949
|
-
|
|
40950
|
-
|
|
40951
|
-
|
|
40351
|
+
});
|
|
40352
|
+
|
|
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);
|
|
40363
|
+
try {
|
|
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)
|
|
40375
|
+
});
|
|
40376
|
+
const hasTools = resp.ok;
|
|
40377
|
+
_toolSupportCache.set(modelName, hasTools);
|
|
40378
|
+
return hasTools;
|
|
40379
|
+
} catch {
|
|
40380
|
+
_toolSupportCache.set(modelName, true);
|
|
40381
|
+
return true;
|
|
40382
|
+
}
|
|
40383
|
+
}
|
|
40384
|
+
function detectSystemSpecs() {
|
|
40385
|
+
let totalRamGB = 0;
|
|
40386
|
+
let availableRamGB = 0;
|
|
40387
|
+
let gpuVramGB = 0;
|
|
40388
|
+
let gpuName = "";
|
|
40389
|
+
try {
|
|
40390
|
+
const memInfo = execSync27("free -b 2>/dev/null || sysctl -n hw.memsize 2>/dev/null", {
|
|
40391
|
+
encoding: "utf8",
|
|
40392
|
+
timeout: 5e3
|
|
40393
|
+
});
|
|
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;
|
|
40399
|
+
}
|
|
40952
40400
|
} else {
|
|
40953
|
-
const
|
|
40954
|
-
if (
|
|
40955
|
-
|
|
40956
|
-
|
|
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
|
-
}
|
|
40401
|
+
const bytes = parseInt(memInfo.trim(), 10);
|
|
40402
|
+
if (!isNaN(bytes)) {
|
|
40403
|
+
totalRamGB = bytes / 1024 ** 3;
|
|
40404
|
+
availableRamGB = totalRamGB * 0.7;
|
|
41002
40405
|
}
|
|
41003
40406
|
}
|
|
40407
|
+
} catch {
|
|
41004
40408
|
}
|
|
41005
|
-
|
|
41006
|
-
|
|
41007
|
-
const
|
|
41008
|
-
|
|
41009
|
-
|
|
41010
|
-
|
|
41011
|
-
|
|
41012
|
-
|
|
41013
|
-
|
|
41014
|
-
|
|
41015
|
-
|
|
41016
|
-
log("python3-pip could not be installed \u2014 moondream-station may be unavailable.");
|
|
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];
|
|
41017
40420
|
}
|
|
41018
40421
|
}
|
|
40422
|
+
} catch {
|
|
41019
40423
|
}
|
|
41020
|
-
|
|
41021
|
-
|
|
41022
|
-
|
|
41023
|
-
|
|
41024
|
-
|
|
41025
|
-
|
|
41026
|
-
|
|
41027
|
-
|
|
41028
|
-
|
|
41029
|
-
|
|
41030
|
-
|
|
41031
|
-
|
|
41032
|
-
|
|
41033
|
-
|
|
41034
|
-
|
|
41035
|
-
|
|
41036
|
-
|
|
41037
|
-
|
|
41038
|
-
|
|
41039
|
-
|
|
41040
|
-
|
|
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
|
+
};
|
|
40430
|
+
}
|
|
40431
|
+
async function detectSystemSpecsAsync() {
|
|
40432
|
+
let totalRamGB = 0;
|
|
40433
|
+
let availableRamGB = 0;
|
|
40434
|
+
let gpuVramGB = 0;
|
|
40435
|
+
let gpuName = "";
|
|
40436
|
+
try {
|
|
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;
|
|
40443
|
+
}
|
|
40444
|
+
} else {
|
|
40445
|
+
const bytes = parseInt(memInfo.trim(), 10);
|
|
40446
|
+
if (!isNaN(bytes)) {
|
|
40447
|
+
totalRamGB = bytes / 1024 ** 3;
|
|
40448
|
+
availableRamGB = totalRamGB * 0.7;
|
|
41041
40449
|
}
|
|
41042
40450
|
}
|
|
40451
|
+
} catch {
|
|
41043
40452
|
}
|
|
41044
|
-
|
|
41045
|
-
|
|
41046
|
-
|
|
41047
|
-
|
|
41048
|
-
|
|
41049
|
-
|
|
41050
|
-
|
|
41051
|
-
|
|
41052
|
-
|
|
41053
|
-
|
|
41054
|
-
|
|
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.");
|
|
41063
|
-
}
|
|
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];
|
|
41064
40464
|
}
|
|
41065
|
-
} catch (err) {
|
|
41066
|
-
log(`moondream-station install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
41067
40465
|
}
|
|
40466
|
+
} catch {
|
|
41068
40467
|
}
|
|
41069
|
-
|
|
41070
|
-
|
|
41071
|
-
|
|
41072
|
-
|
|
41073
|
-
|
|
41074
|
-
|
|
41075
|
-
|
|
41076
|
-
|
|
41077
|
-
|
|
41078
|
-
|
|
41079
|
-
|
|
41080
|
-
|
|
41081
|
-
|
|
41082
|
-
|
|
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.");
|
|
41088
|
-
}
|
|
41089
|
-
} catch (err) {
|
|
41090
|
-
log(`OCR Python stack install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
41091
|
-
}
|
|
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
|
+
};
|
|
40474
|
+
}
|
|
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];
|
|
41092
40482
|
}
|
|
41093
|
-
} else {
|
|
41094
|
-
log("Python venv unavailable \u2014 advanced OCR pipeline will fall back to basic tesseract.");
|
|
41095
40483
|
}
|
|
40484
|
+
return QWEN_VARIANTS.find((v) => v.tag === "qwen3.5:cloud");
|
|
41096
40485
|
}
|
|
41097
|
-
function
|
|
41098
|
-
|
|
41099
|
-
|
|
41100
|
-
|
|
41101
|
-
|
|
41102
|
-
|
|
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);
|
|
41103
40498
|
}
|
|
41104
|
-
|
|
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);
|
|
40507
|
+
}
|
|
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()));
|
|
41105
40514
|
});
|
|
41106
|
-
|
|
41107
|
-
|
|
41108
|
-
|
|
41109
|
-
|
|
41110
|
-
|
|
41111
|
-
|
|
41112
|
-
|
|
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);
|
|
41113
40524
|
}
|
|
41114
|
-
|
|
41115
|
-
|
|
41116
|
-
const
|
|
41117
|
-
|
|
41118
|
-
|
|
41119
|
-
|
|
41120
|
-
|
|
41121
|
-
|
|
41122
|
-
|
|
41123
|
-
|
|
41124
|
-
|
|
41125
|
-
|
|
41126
|
-
|
|
41127
|
-
|
|
41128
|
-
|
|
41129
|
-
|
|
41130
|
-
|
|
41131
|
-
|
|
41132
|
-
|
|
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());
|
|
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("*");
|
|
41133
40553
|
}
|
|
41134
|
-
} catch {
|
|
41135
40554
|
}
|
|
41136
|
-
}
|
|
40555
|
+
};
|
|
40556
|
+
stdin.on("data", onData);
|
|
40557
|
+
});
|
|
40558
|
+
}
|
|
40559
|
+
function ensureCurl() {
|
|
40560
|
+
if (hasCmd("curl"))
|
|
40561
|
+
return true;
|
|
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;
|
|
40567
|
+
}
|
|
40568
|
+
process.stdout.write(` ${c2.cyan("\u25CF")} curl not found. Installing...
|
|
40569
|
+
`);
|
|
40570
|
+
const strategies = [
|
|
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" }
|
|
40577
|
+
];
|
|
40578
|
+
for (const s of strategies) {
|
|
40579
|
+
if (hasCmd(s.check)) {
|
|
41137
40580
|
try {
|
|
41138
|
-
execSync27(
|
|
41139
|
-
if (hasCmd("
|
|
41140
|
-
|
|
40581
|
+
execSync27(s.install, { stdio: "inherit", timeout: 12e4 });
|
|
40582
|
+
if (hasCmd("curl")) {
|
|
40583
|
+
process.stdout.write(` ${c2.green("\u2714")} curl installed via ${s.label}.
|
|
40584
|
+
`);
|
|
41141
40585
|
return true;
|
|
41142
40586
|
}
|
|
41143
40587
|
} catch {
|
|
40588
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} ${s.label} install failed, trying next...
|
|
40589
|
+
`);
|
|
41144
40590
|
}
|
|
41145
40591
|
}
|
|
41146
|
-
|
|
41147
|
-
|
|
41148
|
-
|
|
40592
|
+
}
|
|
40593
|
+
if (plat === "darwin") {
|
|
40594
|
+
try {
|
|
40595
|
+
execSync27("xcode-select --install", { stdio: "inherit", timeout: 3e5 });
|
|
40596
|
+
if (hasCmd("curl"))
|
|
40597
|
+
return true;
|
|
40598
|
+
} catch {
|
|
40599
|
+
}
|
|
40600
|
+
}
|
|
40601
|
+
process.stdout.write(` ${c2.red("\u2716")} Could not install curl. Install it manually and retry.
|
|
40602
|
+
`);
|
|
40603
|
+
return false;
|
|
41149
40604
|
}
|
|
41150
|
-
function
|
|
41151
|
-
|
|
40605
|
+
async function autoInstallOllama(rl) {
|
|
40606
|
+
const plat = platform2();
|
|
40607
|
+
if (plat !== "win32" && !ensureCurl()) {
|
|
40608
|
+
return false;
|
|
40609
|
+
}
|
|
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();
|
|
40616
|
+
}
|
|
40617
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} Unsupported platform: ${plat}
|
|
40618
|
+
`);
|
|
40619
|
+
process.stdout.write(` ${c2.dim("Visit https://ollama.com to install manually.")}
|
|
40620
|
+
|
|
40621
|
+
`);
|
|
40622
|
+
return false;
|
|
41152
40623
|
}
|
|
41153
|
-
|
|
41154
|
-
|
|
41155
|
-
|
|
41156
|
-
|
|
40624
|
+
function installOllamaLinux() {
|
|
40625
|
+
process.stdout.write(`
|
|
40626
|
+
${c2.cyan("\u25CF")} Installing Ollama via official install script...
|
|
40627
|
+
|
|
40628
|
+
`);
|
|
41157
40629
|
try {
|
|
41158
|
-
|
|
41159
|
-
|
|
41160
|
-
|
|
41161
|
-
|
|
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.
|
|
40637
|
+
`);
|
|
40638
|
+
return true;
|
|
40639
|
+
}
|
|
40640
|
+
process.stdout.write(`
|
|
40641
|
+
${c2.yellow("\u26A0")} Install script ran but ollama not found on PATH.
|
|
40642
|
+
`);
|
|
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
|
+
`);
|
|
41162
40648
|
return false;
|
|
41163
40649
|
}
|
|
41164
40650
|
}
|
|
41165
|
-
function
|
|
41166
|
-
|
|
41167
|
-
|
|
41168
|
-
|
|
41169
|
-
|
|
41170
|
-
|
|
41171
|
-
|
|
41172
|
-
|
|
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)...
|
|
40655
|
+
|
|
40656
|
+
`);
|
|
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")) {
|
|
40660
|
+
try {
|
|
40661
|
+
const brewPrefix = existsSync37("/opt/homebrew/bin/brew") ? "/opt/homebrew" : "/usr/local";
|
|
40662
|
+
process.env["PATH"] = `${brewPrefix}/bin:${process.env["PATH"]}`;
|
|
40663
|
+
} catch {
|
|
40664
|
+
}
|
|
40665
|
+
}
|
|
40666
|
+
if (hasCmd("brew")) {
|
|
40667
|
+
process.stdout.write(`
|
|
40668
|
+
${c2.green("\u2714")} Homebrew installed.
|
|
40669
|
+
`);
|
|
40670
|
+
} else {
|
|
40671
|
+
process.stdout.write(`
|
|
40672
|
+
${c2.yellow("\u26A0")} Homebrew install completed but brew not found on PATH.
|
|
40673
|
+
`);
|
|
40674
|
+
process.stdout.write(` ${c2.dim('Try: eval "$(/opt/homebrew/bin/brew shellenv)"')}
|
|
40675
|
+
|
|
40676
|
+
`);
|
|
40677
|
+
return false;
|
|
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;
|
|
40684
|
+
}
|
|
40685
|
+
}
|
|
40686
|
+
process.stdout.write(`
|
|
40687
|
+
${c2.cyan("\u25CF")} Installing Ollama via Homebrew...
|
|
40688
|
+
|
|
40689
|
+
`);
|
|
41173
40690
|
try {
|
|
41174
|
-
|
|
41175
|
-
|
|
41176
|
-
|
|
41177
|
-
headers: { "Content-Type": "application/json" },
|
|
41178
|
-
body: JSON.stringify({ name: modelName }),
|
|
41179
|
-
signal: AbortSignal.timeout(5e3)
|
|
40691
|
+
execSync27("brew install ollama", {
|
|
40692
|
+
stdio: "inherit",
|
|
40693
|
+
timeout: 3e5
|
|
41180
40694
|
});
|
|
41181
|
-
if (
|
|
41182
|
-
|
|
41183
|
-
|
|
41184
|
-
|
|
41185
|
-
return
|
|
41186
|
-
|
|
41187
|
-
|
|
41188
|
-
|
|
41189
|
-
|
|
41190
|
-
|
|
41191
|
-
|
|
41192
|
-
|
|
41193
|
-
|
|
41194
|
-
|
|
41195
|
-
|
|
41196
|
-
return null;
|
|
41197
|
-
const kvBytesPerToken = nLayers * nKVHeads * (keyDim + valDim) * 2;
|
|
41198
|
-
return { kvBytesPerToken, archMax };
|
|
41199
|
-
} catch {
|
|
41200
|
-
return null;
|
|
40695
|
+
if (hasCmd("ollama")) {
|
|
40696
|
+
process.stdout.write(`
|
|
40697
|
+
${c2.green("\u2714")} Ollama installed successfully.
|
|
40698
|
+
`);
|
|
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;
|
|
41201
40710
|
}
|
|
41202
40711
|
}
|
|
41203
|
-
|
|
41204
|
-
|
|
41205
|
-
|
|
40712
|
+
function installOllamaWindows() {
|
|
40713
|
+
process.stdout.write(`
|
|
40714
|
+
${c2.cyan("\u25CF")} Installing Ollama via PowerShell...
|
|
40715
|
+
|
|
40716
|
+
`);
|
|
41206
40717
|
try {
|
|
41207
|
-
|
|
41208
|
-
|
|
41209
|
-
|
|
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
|
|
40718
|
+
execSync27('powershell -Command "irm https://ollama.com/install.ps1 | iex"', {
|
|
40719
|
+
stdio: "inherit",
|
|
40720
|
+
timeout: 3e5
|
|
41221
40721
|
});
|
|
41222
|
-
|
|
41223
|
-
|
|
41224
|
-
|
|
40722
|
+
if (hasCmd("ollama")) {
|
|
40723
|
+
process.stdout.write(`
|
|
40724
|
+
${c2.green("\u2714")} Ollama installed successfully.
|
|
40725
|
+
`);
|
|
40726
|
+
return true;
|
|
40727
|
+
}
|
|
40728
|
+
process.stdout.write(`
|
|
40729
|
+
${c2.yellow("\u26A0")} Install script ran but ollama not found on PATH.
|
|
40730
|
+
`);
|
|
40731
|
+
return false;
|
|
40732
|
+
} catch (err) {
|
|
40733
|
+
process.stdout.write(`
|
|
40734
|
+
${c2.red("\u2716")} Ollama install failed: ${err instanceof Error ? err.message : String(err)}
|
|
40735
|
+
`);
|
|
40736
|
+
return false;
|
|
41225
40737
|
}
|
|
41226
40738
|
}
|
|
41227
|
-
async function
|
|
41228
|
-
|
|
41229
|
-
|
|
40739
|
+
async function ensureOllamaRunning(backendUrl, rl) {
|
|
40740
|
+
const ollamaInstalled = hasCmd("ollama");
|
|
40741
|
+
if (!ollamaInstalled) {
|
|
40742
|
+
if (rl) {
|
|
40743
|
+
process.stdout.write(`
|
|
40744
|
+
${c2.yellow("\u26A0")} Ollama is not installed on this system.
|
|
40745
|
+
`);
|
|
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...
|
|
40753
|
+
`);
|
|
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.
|
|
40761
|
+
|
|
40762
|
+
`);
|
|
40763
|
+
return false;
|
|
40764
|
+
}
|
|
41230
40765
|
}
|
|
41231
|
-
|
|
41232
|
-
|
|
41233
|
-
let sizeGB = 4;
|
|
40766
|
+
process.stdout.write(` ${c2.cyan("\u25CF")} Starting ollama serve...
|
|
40767
|
+
`);
|
|
41234
40768
|
try {
|
|
41235
|
-
const
|
|
41236
|
-
|
|
40769
|
+
const child = spawn19("ollama", ["serve"], { stdio: "ignore", detached: true });
|
|
40770
|
+
child.unref();
|
|
41237
40771
|
} catch {
|
|
40772
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} Could not start ollama serve.
|
|
40773
|
+
|
|
40774
|
+
`);
|
|
40775
|
+
return false;
|
|
41238
40776
|
}
|
|
41239
|
-
|
|
41240
|
-
|
|
40777
|
+
for (let i = 0; i < 5; i++) {
|
|
40778
|
+
await new Promise((resolve36) => setTimeout(resolve36, 2e3));
|
|
41241
40779
|
try {
|
|
41242
|
-
const
|
|
41243
|
-
|
|
41244
|
-
method: "POST",
|
|
41245
|
-
headers: { "Content-Type": "application/json" },
|
|
41246
|
-
body: JSON.stringify({ name: modelName }),
|
|
41247
|
-
signal: AbortSignal.timeout(5e3)
|
|
40780
|
+
const resp = await fetch(`${backendUrl}/api/tags`, {
|
|
40781
|
+
signal: AbortSignal.timeout(3e3)
|
|
41248
40782
|
});
|
|
41249
|
-
if (
|
|
41250
|
-
|
|
41251
|
-
|
|
41252
|
-
|
|
41253
|
-
|
|
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);
|
|
41258
|
-
}
|
|
41259
|
-
}
|
|
40783
|
+
if (resp.ok) {
|
|
40784
|
+
process.stdout.write(` ${c2.green("\u2714")} Ollama is running.
|
|
40785
|
+
|
|
40786
|
+
`);
|
|
40787
|
+
return true;
|
|
41260
40788
|
}
|
|
41261
40789
|
} catch {
|
|
41262
40790
|
}
|
|
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
|
-
}
|
|
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 };
|
|
41275
40791
|
}
|
|
41276
|
-
|
|
40792
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} Ollama started but not responding. Try ${c2.bold("ollama serve")} manually.
|
|
40793
|
+
|
|
40794
|
+
`);
|
|
40795
|
+
return false;
|
|
41277
40796
|
}
|
|
41278
|
-
|
|
40797
|
+
function pullModelWithAutoUpdate(tag) {
|
|
41279
40798
|
try {
|
|
41280
|
-
|
|
41281
|
-
|
|
41282
|
-
|
|
41283
|
-
|
|
41284
|
-
})
|
|
41285
|
-
|
|
41286
|
-
|
|
41287
|
-
|
|
41288
|
-
|
|
41289
|
-
|
|
41290
|
-
|
|
41291
|
-
|
|
41292
|
-
|
|
41293
|
-
|
|
41294
|
-
|
|
41295
|
-
|
|
41296
|
-
|
|
41297
|
-
|
|
41298
|
-
|
|
41299
|
-
|
|
41300
|
-
|
|
41301
|
-
|
|
41302
|
-
|
|
41303
|
-
|
|
41304
|
-
|
|
41305
|
-
|
|
41306
|
-
|
|
41307
|
-
|
|
41308
|
-
|
|
41309
|
-
|
|
41310
|
-
|
|
41311
|
-
|
|
41312
|
-
|
|
40799
|
+
execSync27(`ollama pull ${tag}`, {
|
|
40800
|
+
stdio: "inherit",
|
|
40801
|
+
timeout: 36e5
|
|
40802
|
+
// 1 hour max
|
|
40803
|
+
});
|
|
40804
|
+
} catch (err) {
|
|
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.
|
|
40811
|
+
`);
|
|
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...
|
|
40816
|
+
|
|
40817
|
+
`);
|
|
40818
|
+
try {
|
|
40819
|
+
execSync27("curl -fsSL https://ollama.com/install.sh | sh", {
|
|
40820
|
+
stdio: "inherit",
|
|
40821
|
+
timeout: 3e5
|
|
40822
|
+
// 5 min max for install
|
|
40823
|
+
});
|
|
40824
|
+
process.stdout.write(`
|
|
40825
|
+
${c2.green("\u2714")} Ollama updated successfully.
|
|
40826
|
+
`);
|
|
40827
|
+
process.stdout.write(` ${c2.cyan("\u25CF")} Retrying pull of ${c2.bold(tag)}...
|
|
40828
|
+
|
|
40829
|
+
`);
|
|
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}`);
|
|
40840
|
+
}
|
|
40841
|
+
} else {
|
|
40842
|
+
throw err;
|
|
41313
40843
|
}
|
|
41314
|
-
|
|
41315
|
-
|
|
40844
|
+
}
|
|
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)";
|
|
40885
|
+
}
|
|
40886
|
+
} else {
|
|
40887
|
+
const overshoot = v.sizeGB - budget;
|
|
40888
|
+
note = `need ${overshoot.toFixed(0)} GB more`;
|
|
41316
40889
|
}
|
|
41317
|
-
|
|
41318
|
-
|
|
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;
|
|
41319
40920
|
}
|
|
41320
|
-
|
|
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") {
|
|
41321
40930
|
if (hasCmd("brew")) {
|
|
41322
|
-
console.log(" Installing Neovim via Homebrew...");
|
|
41323
40931
|
try {
|
|
41324
|
-
execSync27("brew install
|
|
41325
|
-
|
|
41326
|
-
|
|
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;
|
|
40937
|
+
}
|
|
41327
40938
|
} catch {
|
|
41328
|
-
console.log(" brew install neovim failed.");
|
|
41329
|
-
return null;
|
|
41330
40939
|
}
|
|
41331
40940
|
}
|
|
41332
|
-
console.log(" Homebrew not found. Install Neovim: brew install neovim");
|
|
41333
|
-
return null;
|
|
41334
40941
|
}
|
|
41335
|
-
|
|
41336
|
-
if (hasCmd(
|
|
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...");
|
|
40942
|
+
for (const s of strategies) {
|
|
40943
|
+
if (hasCmd(s.check)) {
|
|
41347
40944
|
try {
|
|
41348
|
-
execSync27(
|
|
41349
|
-
|
|
41350
|
-
|
|
41351
|
-
|
|
41352
|
-
|
|
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;
|
|
40950
|
+
}
|
|
41353
40951
|
} catch {
|
|
41354
|
-
console.log(" winget install neovim failed.");
|
|
41355
40952
|
}
|
|
41356
40953
|
}
|
|
41357
|
-
console.log(" Install Neovim manually: https://neovim.io");
|
|
41358
|
-
return null;
|
|
41359
40954
|
}
|
|
41360
|
-
|
|
40955
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} Could not install Python3 automatically. Install manually.
|
|
40956
|
+
|
|
40957
|
+
`);
|
|
41361
40958
|
}
|
|
41362
|
-
function
|
|
41363
|
-
const shell = process.env.SHELL ?? "";
|
|
41364
|
-
const rcFile = shell.includes("zsh") ? join54(homedir13(), ".zshrc") : join54(homedir13(), ".bashrc");
|
|
40959
|
+
function checkPythonVenv() {
|
|
41365
40960
|
try {
|
|
41366
|
-
|
|
41367
|
-
|
|
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}`);
|
|
40961
|
+
execSync27("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
|
|
40962
|
+
return true;
|
|
41374
40963
|
} catch {
|
|
41375
|
-
|
|
41376
|
-
}
|
|
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);
|
|
41421
|
-
}
|
|
41422
|
-
return process.stdout.write(data);
|
|
41423
|
-
}
|
|
41424
|
-
function bufferIfOverlay(stream, data) {
|
|
41425
|
-
if (!_overlayActive)
|
|
41426
|
-
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();
|
|
40964
|
+
try {
|
|
40965
|
+
execSync27("python -m venv --help", { stdio: "pipe", timeout: 5e3 });
|
|
41448
40966
|
return true;
|
|
41449
|
-
}
|
|
40967
|
+
} catch {
|
|
40968
|
+
return false;
|
|
40969
|
+
}
|
|
41450
40970
|
}
|
|
41451
40971
|
}
|
|
41452
|
-
function
|
|
41453
|
-
|
|
41454
|
-
|
|
41455
|
-
|
|
41456
|
-
|
|
41457
|
-
|
|
41458
|
-
|
|
41459
|
-
}
|
|
41460
|
-
|
|
41461
|
-
|
|
41462
|
-
|
|
41463
|
-
}
|
|
41464
|
-
if (_onLeaveCallback) {
|
|
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)) {
|
|
41465
40983
|
try {
|
|
41466
|
-
|
|
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;
|
|
40989
|
+
}
|
|
41467
40990
|
} catch {
|
|
41468
40991
|
}
|
|
41469
40992
|
}
|
|
41470
|
-
|
|
41471
|
-
|
|
41472
|
-
|
|
41473
|
-
|
|
41474
|
-
process.stdout.write(data);
|
|
41475
|
-
} else {
|
|
41476
|
-
process.stderr.write(data);
|
|
41477
|
-
}
|
|
41478
|
-
}
|
|
41479
|
-
}
|
|
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;
|
|
41491
|
-
}
|
|
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, "");
|
|
41503
|
-
}
|
|
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}`;
|
|
40993
|
+
}
|
|
40994
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} Could not install python3-venv. Install manually: sudo apt install python3-venv
|
|
40995
|
+
|
|
40996
|
+
`);
|
|
41509
40997
|
}
|
|
41510
|
-
function
|
|
41511
|
-
|
|
41512
|
-
|
|
40998
|
+
async function runSetupWizard(config) {
|
|
40999
|
+
const rl = readline.createInterface({
|
|
41000
|
+
input: process.stdin,
|
|
41001
|
+
output: process.stdout,
|
|
41002
|
+
terminal: true
|
|
41003
|
+
});
|
|
41004
|
+
try {
|
|
41005
|
+
return await doSetup(config, rl);
|
|
41006
|
+
} finally {
|
|
41007
|
+
rl.close();
|
|
41513
41008
|
}
|
|
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
41009
|
}
|
|
41519
|
-
function
|
|
41520
|
-
|
|
41521
|
-
|
|
41522
|
-
|
|
41523
|
-
|
|
41524
|
-
|
|
41525
|
-
|
|
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;
|
|
41031
|
+
}
|
|
41032
|
+
return config.model;
|
|
41526
41033
|
}
|
|
41527
|
-
const
|
|
41528
|
-
|
|
41529
|
-
|
|
41530
|
-
let
|
|
41531
|
-
|
|
41532
|
-
|
|
41533
|
-
|
|
41534
|
-
|
|
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;
|
|
41535
41067
|
} else {
|
|
41536
|
-
|
|
41537
|
-
|
|
41538
|
-
|
|
41539
|
-
|
|
41540
|
-
|
|
41541
|
-
|
|
41542
|
-
|
|
41543
|
-
|
|
41544
|
-
|
|
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 {
|
|
41545
41077
|
}
|
|
41546
41078
|
}
|
|
41079
|
+
if (!testOk) {
|
|
41080
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} Endpoint returned HTTP ${resp.status}
|
|
41081
|
+
`);
|
|
41082
|
+
}
|
|
41547
41083
|
}
|
|
41084
|
+
} catch (err) {
|
|
41085
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} Could not reach endpoint: ${err instanceof Error ? err.message : String(err)}
|
|
41086
|
+
`);
|
|
41548
41087
|
}
|
|
41549
|
-
|
|
41550
|
-
|
|
41551
|
-
|
|
41552
|
-
|
|
41553
|
-
if (!isSkippable(idx) && matchSet.has(idx))
|
|
41554
|
-
return idx;
|
|
41555
|
-
idx += dir;
|
|
41088
|
+
if (!testOk) {
|
|
41089
|
+
if (provider.authRequired && !apiKey) {
|
|
41090
|
+
process.stdout.write(` ${c2.dim(`${provider.label} typically requires an API key.`)}
|
|
41091
|
+
`);
|
|
41556
41092
|
}
|
|
41557
|
-
|
|
41558
|
-
};
|
|
41559
|
-
|
|
41560
|
-
|
|
41561
|
-
|
|
41562
|
-
|
|
41563
|
-
|
|
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
|
-
}
|
|
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;
|
|
41584
41100
|
}
|
|
41585
|
-
|
|
41586
|
-
|
|
41101
|
+
}
|
|
41102
|
+
setConfigValue("backendUrl", cleanUrl);
|
|
41103
|
+
setConfigValue("model", chosenModel);
|
|
41104
|
+
if (apiKey) {
|
|
41105
|
+
setConfigValue("apiKey", apiKey);
|
|
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;
|
|
41121
|
+
}
|
|
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
|
+
`);
|
|
41142
|
+
}
|
|
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");
|
|
41587
41202
|
}
|
|
41588
|
-
|
|
41589
|
-
|
|
41590
|
-
|
|
41591
|
-
|
|
41592
|
-
|
|
41593
|
-
|
|
41594
|
-
|
|
41595
|
-
|
|
41596
|
-
|
|
41597
|
-
scrollOffset = cursorPos;
|
|
41598
|
-
} else if (cursorPos >= scrollOffset + maxVisible) {
|
|
41599
|
-
scrollOffset = cursorPos - maxVisible + 1;
|
|
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();
|
|
41600
41212
|
}
|
|
41601
|
-
const maxOffset = Math.max(0, displayList.length - maxVisible);
|
|
41602
|
-
scrollOffset = Math.max(0, Math.min(maxOffset, scrollOffset));
|
|
41603
41213
|
}
|
|
41604
|
-
|
|
41605
|
-
|
|
41606
|
-
|
|
41607
|
-
|
|
41608
|
-
|
|
41609
|
-
|
|
41610
|
-
|
|
41611
|
-
|
|
41612
|
-
|
|
41613
|
-
|
|
41614
|
-
|
|
41615
|
-
|
|
41616
|
-
|
|
41617
|
-
|
|
41618
|
-
}
|
|
41619
|
-
if (filter) {
|
|
41620
|
-
const count = matchSet.size;
|
|
41621
|
-
lines.push(` ${selectColors.cyan("/")} ${selectColors.bold(filter)} ${selectColors.dim(`(${count} match${count !== 1 ? "es" : ""})`)}`);
|
|
41622
|
-
} else {
|
|
41623
|
-
lines.push(` ${selectColors.dim("Type to filter...")}`);
|
|
41214
|
+
}
|
|
41215
|
+
let models = [];
|
|
41216
|
+
let usingCustomEndpoint = false;
|
|
41217
|
+
try {
|
|
41218
|
+
models = await fetchOllamaModels(config.backendUrl);
|
|
41219
|
+
} catch {
|
|
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;
|
|
41624
41228
|
}
|
|
41625
|
-
|
|
41626
|
-
|
|
41627
|
-
|
|
41628
|
-
|
|
41629
|
-
|
|
41630
|
-
|
|
41631
|
-
|
|
41632
|
-
|
|
41633
|
-
|
|
41634
|
-
|
|
41635
|
-
|
|
41636
|
-
|
|
41637
|
-
|
|
41638
|
-
|
|
41639
|
-
|
|
41640
|
-
|
|
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
|
+
`);
|
|
41641
41249
|
}
|
|
41642
|
-
|
|
41643
|
-
|
|
41644
|
-
|
|
41645
|
-
|
|
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
|
-
}
|
|
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));
|
|
41669
|
-
} else {
|
|
41670
|
-
lines.push(renderRow(item, focused, isActive));
|
|
41250
|
+
} catch {
|
|
41251
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} Could not start Ollama. Try running ${c2.bold("ollama serve")} manually.
|
|
41252
|
+
|
|
41253
|
+
`);
|
|
41671
41254
|
}
|
|
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
41255
|
} else {
|
|
41681
|
-
const
|
|
41682
|
-
|
|
41683
|
-
|
|
41684
|
-
|
|
41685
|
-
|
|
41686
|
-
|
|
41687
|
-
|
|
41688
|
-
|
|
41689
|
-
|
|
41690
|
-
|
|
41691
|
-
|
|
41692
|
-
|
|
41693
|
-
|
|
41694
|
-
|
|
41695
|
-
|
|
41696
|
-
|
|
41697
|
-
|
|
41698
|
-
|
|
41699
|
-
|
|
41700
|
-
|
|
41701
|
-
|
|
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();
|
|
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
|
+
`);
|
|
41786
41277
|
}
|
|
41278
|
+
} catch {
|
|
41279
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} Ollama installed. Start it with: ${c2.bold("ollama serve")}
|
|
41280
|
+
|
|
41281
|
+
`);
|
|
41787
41282
|
}
|
|
41788
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;
|
|
41292
|
+
}
|
|
41293
|
+
}
|
|
41294
|
+
}
|
|
41295
|
+
}
|
|
41296
|
+
if (usingCustomEndpoint) {
|
|
41297
|
+
return config.model;
|
|
41298
|
+
}
|
|
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;
|
|
41305
|
+
}
|
|
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;
|
|
41334
|
+
}
|
|
41335
|
+
} else {
|
|
41336
|
+
process.stdout.write(` ${c2.yellow("\u26A0")} No models found on this system.
|
|
41337
|
+
|
|
41338
|
+
`);
|
|
41339
|
+
}
|
|
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;
|
|
41363
|
+
}
|
|
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") {
|
|
41389
|
+
try {
|
|
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",
|
|
41405
|
+
timeout: 12e4
|
|
41406
|
+
});
|
|
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.`);
|
|
41420
|
+
}
|
|
41421
|
+
}
|
|
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;
|
|
41428
|
+
}
|
|
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;
|
|
41435
|
+
}
|
|
41436
|
+
async function isModelAvailable(config) {
|
|
41437
|
+
try {
|
|
41438
|
+
const models = await fetchModels(config.backendUrl, config.apiKey);
|
|
41439
|
+
return !!findModel(models, config.model);
|
|
41440
|
+
} catch {
|
|
41441
|
+
return false;
|
|
41442
|
+
}
|
|
41443
|
+
}
|
|
41444
|
+
function isFirstRun() {
|
|
41445
|
+
try {
|
|
41446
|
+
return !existsSync37(join54(homedir13(), ".open-agents", "config.json"));
|
|
41447
|
+
} catch {
|
|
41448
|
+
return true;
|
|
41449
|
+
}
|
|
41450
|
+
}
|
|
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 {
|
|
41457
|
+
return false;
|
|
41458
|
+
}
|
|
41459
|
+
}
|
|
41460
|
+
function detectPkgManager() {
|
|
41461
|
+
const plat = process.platform;
|
|
41462
|
+
if (plat === "win32") {
|
|
41463
|
+
if (hasCmd("choco"))
|
|
41464
|
+
return "choco";
|
|
41465
|
+
return null;
|
|
41466
|
+
}
|
|
41467
|
+
if (plat === "darwin") {
|
|
41468
|
+
if (hasCmd("brew"))
|
|
41469
|
+
return "brew";
|
|
41470
|
+
return null;
|
|
41471
|
+
}
|
|
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;
|
|
41481
|
+
}
|
|
41482
|
+
function getVenvDir() {
|
|
41483
|
+
return join54(homedir13(), ".open-agents", "venv");
|
|
41484
|
+
}
|
|
41485
|
+
function hasVenvModule() {
|
|
41486
|
+
try {
|
|
41487
|
+
execSync27("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
|
|
41488
|
+
return true;
|
|
41489
|
+
} catch {
|
|
41490
|
+
return false;
|
|
41491
|
+
}
|
|
41492
|
+
}
|
|
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;
|
|
41504
|
+
}
|
|
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;
|
|
41534
|
+
}
|
|
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";
|
|
41550
|
+
}
|
|
41551
|
+
return "cmd_fail";
|
|
41552
|
+
}
|
|
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;
|
|
41573
|
+
}
|
|
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}.`);
|
|
41616
|
+
} else {
|
|
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`;
|
|
41639
|
+
break;
|
|
41789
41640
|
}
|
|
41790
|
-
|
|
41791
|
-
|
|
41792
|
-
|
|
41793
|
-
|
|
41794
|
-
|
|
41795
|
-
|
|
41796
|
-
|
|
41797
|
-
|
|
41798
|
-
|
|
41799
|
-
cursor = next;
|
|
41800
|
-
render();
|
|
41641
|
+
let ok = false;
|
|
41642
|
+
if (needsSudo) {
|
|
41643
|
+
ok = await sudoInstall(batchCmd, getPassword, log, cachedPasswordRef, 18e4);
|
|
41644
|
+
} else {
|
|
41645
|
+
try {
|
|
41646
|
+
execSync27(batchCmd, { stdio: "pipe", timeout: 18e4 });
|
|
41647
|
+
ok = true;
|
|
41648
|
+
} catch {
|
|
41649
|
+
ok = false;
|
|
41801
41650
|
}
|
|
41802
41651
|
}
|
|
41803
|
-
|
|
41804
|
-
|
|
41805
|
-
|
|
41806
|
-
|
|
41807
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41850
|
-
|
|
41851
|
-
|
|
41852
|
-
|
|
41853
|
-
|
|
41854
|
-
|
|
41855
|
-
|
|
41856
|
-
|
|
41857
|
-
|
|
41858
|
-
|
|
41859
|
-
|
|
41860
|
-
|
|
41861
|
-
|
|
41862
|
-
|
|
41863
|
-
|
|
41864
|
-
|
|
41865
|
-
|
|
41866
|
-
|
|
41867
|
-
|
|
41868
|
-
|
|
41869
|
-
|
|
41870
|
-
|
|
41871
|
-
|
|
41872
|
-
|
|
41873
|
-
|
|
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();
|
|
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";
|
|
41882
41687
|
}
|
|
41883
|
-
}
|
|
41884
|
-
|
|
41885
|
-
|
|
41886
|
-
|
|
41887
|
-
|
|
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.");
|
|
41888
41699
|
}
|
|
41889
|
-
}
|
|
41890
|
-
|
|
41891
|
-
|
|
41892
|
-
|
|
41893
|
-
|
|
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.");
|
|
41719
|
+
}
|
|
41720
|
+
} catch {
|
|
41721
|
+
log("moondream-station install completed.");
|
|
41894
41722
|
}
|
|
41895
|
-
}
|
|
41896
|
-
|
|
41897
|
-
|
|
41898
|
-
|
|
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.");
|
|
41899
41747
|
}
|
|
41900
|
-
}
|
|
41901
|
-
|
|
41902
|
-
|
|
41903
|
-
|
|
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}`;
|
|
41904
41780
|
}
|
|
41905
|
-
|
|
41906
|
-
|
|
41907
|
-
|
|
41908
|
-
render();
|
|
41781
|
+
if (hasCmd("cloudflared")) {
|
|
41782
|
+
log("cloudflared installed.");
|
|
41783
|
+
return true;
|
|
41909
41784
|
}
|
|
41910
|
-
}
|
|
41911
|
-
|
|
41912
|
-
|
|
41913
|
-
|
|
41914
|
-
|
|
41915
|
-
|
|
41916
|
-
|
|
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;
|
|
41928
|
-
}
|
|
41929
|
-
cleanup();
|
|
41930
|
-
resolve36({ confirmed: true, key: items[cursor].key, index: cursor });
|
|
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;
|
|
41931
41792
|
}
|
|
41932
|
-
}
|
|
41933
|
-
|
|
41934
|
-
|
|
41935
|
-
|
|
41936
|
-
|
|
41937
|
-
|
|
41938
|
-
|
|
41939
|
-
|
|
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 });
|
|
41793
|
+
} catch {
|
|
41794
|
+
}
|
|
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;
|
|
41947
41801
|
}
|
|
41948
|
-
}
|
|
41949
|
-
|
|
41950
|
-
|
|
41951
|
-
|
|
41952
|
-
|
|
41953
|
-
|
|
41954
|
-
|
|
41955
|
-
|
|
41956
|
-
|
|
41957
|
-
|
|
41958
|
-
|
|
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);
|
|
41959
41917
|
}
|
|
41960
|
-
scrollOffset = 0;
|
|
41961
|
-
render();
|
|
41962
|
-
} else if (hasBreadcrumbs) {
|
|
41963
|
-
cleanup();
|
|
41964
|
-
resolve36({ confirmed: false, key: "__back__", index: cursor });
|
|
41965
|
-
}
|
|
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;
|
|
41983
|
-
}
|
|
41984
|
-
filter += seq;
|
|
41985
|
-
updateFilter();
|
|
41986
|
-
if (matchSet.size > 0) {
|
|
41987
|
-
const first = findSelectable(0, 1);
|
|
41988
|
-
if (first >= 0)
|
|
41989
|
-
cursor = first;
|
|
41990
41918
|
}
|
|
41991
|
-
scrollOffset = 0;
|
|
41992
|
-
render();
|
|
41993
41919
|
}
|
|
41920
|
+
} catch {
|
|
41994
41921
|
}
|
|
41995
|
-
|
|
41996
|
-
|
|
41997
|
-
|
|
41998
|
-
|
|
41999
|
-
|
|
42000
|
-
|
|
42001
|
-
|
|
42002
|
-
|
|
42003
|
-
|
|
42004
|
-
|
|
42005
|
-
|
|
42006
|
-
|
|
42007
|
-
|
|
42008
|
-
|
|
42009
|
-
|
|
42010
|
-
|
|
42011
|
-
|
|
42012
|
-
|
|
42013
|
-
|
|
42014
|
-
|
|
42015
|
-
|
|
42016
|
-
|
|
42017
|
-
|
|
42018
|
-
|
|
42019
|
-
|
|
42020
|
-
|
|
42021
|
-
|
|
42022
|
-
|
|
42023
|
-
|
|
42024
|
-
|
|
42025
|
-
|
|
42026
|
-
|
|
42027
|
-
|
|
42028
|
-
|
|
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();
|
|
42054
|
-
}
|
|
42055
|
-
}
|
|
42056
|
-
stdin.removeListener("data", onData);
|
|
42057
|
-
stdin.on("data", onInputData);
|
|
42058
|
-
renderInput();
|
|
42059
|
-
});
|
|
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 };
|
|
41936
|
+
}
|
|
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 {
|
|
42060
41956
|
}
|
|
42061
|
-
const
|
|
42062
|
-
|
|
42063
|
-
|
|
42064
|
-
|
|
42065
|
-
|
|
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;
|
|
42066
42020
|
}
|
|
42067
|
-
|
|
42068
|
-
|
|
42069
|
-
"
|
|
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
|
-
|
|
42072
|
-
|
|
42073
|
-
|
|
42074
|
-
|
|
42075
|
-
|
|
42076
|
-
|
|
42077
|
-
|
|
42078
|
-
|
|
42079
|
-
|
|
42080
|
-
|
|
42081
|
-
|
|
42082
|
-
|
|
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
|
|
|
@@ -64895,7 +64870,7 @@ async function startInteractive(config, repoPath) {
|
|
|
64895
64870
|
initOaDirectory(repoRoot);
|
|
64896
64871
|
const savedSettings = resolveSettings(repoRoot);
|
|
64897
64872
|
if (process.stdout.isTTY) {
|
|
64898
|
-
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
|
|
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");
|
|
64899
64874
|
const restoreScreen = () => {
|
|
64900
64875
|
process.stdout.write("\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1015l\x1B[?25h\x1B[?1049l");
|
|
64901
64876
|
};
|
|
@@ -65417,6 +65392,9 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
|
|
|
65417
65392
|
statusBar.handlePointerEvent(type, col, row);
|
|
65418
65393
|
});
|
|
65419
65394
|
process.stdin.pipe(mouseFilter);
|
|
65395
|
+
if (process.stdout.isTTY) {
|
|
65396
|
+
process.stdout.write("\x1B[?1002h\x1B[?1006h");
|
|
65397
|
+
}
|
|
65420
65398
|
process.stdin.on("error", (err) => {
|
|
65421
65399
|
if (err.code === "EIO" || err.code === "ENOTTY")
|
|
65422
65400
|
return;
|