offgrid-ai 0.16.3 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/package.json +3 -11
- package/src/cli.mjs +1 -4
- package/src/commands/main.mjs +23 -1
- package/src/commands/models.mjs +105 -32
- package/src/commands/run.mjs +1 -4
- package/src/config.mjs +16 -1
- package/src/model-name.mjs +2 -2
- package/src/model-presenters.mjs +4 -13
- package/src/profile-setup.mjs +206 -49
- package/src/ui.mjs +8 -8
- package/src/benchmark/finalize.mjs +0 -169
- package/src/benchmark/flow.mjs +0 -240
- package/src/benchmark/metrics.mjs +0 -107
- package/src/benchmark/prepare.mjs +0 -118
- package/src/benchmark/repo.mjs +0 -77
- package/src/benchmark/sdk-runner.mjs +0 -363
- package/src/benchmark/shared.mjs +0 -46
- package/src/benchmark.mjs +0 -12
- package/src/commands/benchmark.mjs +0 -4
package/README.md
CHANGED
|
@@ -77,14 +77,12 @@ Pick a model from the list and press Enter. offgrid-ai configures the rest and o
|
|
|
77
77
|
offgrid-ai # primary entry-point for the CLI
|
|
78
78
|
offgrid-ai status # see if any model is running
|
|
79
79
|
offgrid-ai stop # stop the running model
|
|
80
|
-
offgrid-ai benchmark # run a benchmark paired with my local llm benchmark runner
|
|
81
80
|
offgrid-ai uninstall # remove offgrid-ai
|
|
82
81
|
```
|
|
83
82
|
|
|
84
83
|
## What can I do with it?
|
|
85
84
|
|
|
86
85
|
- **Chat with local models** — you download the models yourself, and then offgrid-ai helps configure and run then
|
|
87
|
-
- **Run benchmarks** — compare how different models perform on creative or data-science tasks. Pairs with my other [local llm benchmark runner](https://github.com/eeshansrivastava89/local-llm-visual-benchmark)
|
|
88
86
|
- **Keep data private** — everything runs on your machine without any cloud connections
|
|
89
87
|
|
|
90
88
|
## Need help?
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "offgrid-ai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Privacy-first CLI for running local LLMs — discover, configure, run,
|
|
3
|
+
"version": "0.17.0",
|
|
4
|
+
"description": "Privacy-first CLI for running local LLMs — discover, configure, run, and chat",
|
|
5
5
|
"author": "Eeshan Srivastava (https://eeshans.com)",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"bin/*.mjs",
|
|
12
12
|
"src/*.mjs",
|
|
13
13
|
"src/commands/*.mjs",
|
|
14
|
-
"src/benchmark/*.mjs",
|
|
15
14
|
"resources/*.py",
|
|
16
15
|
"resources/recommendations.json",
|
|
17
16
|
"install.sh"
|
|
@@ -34,7 +33,7 @@
|
|
|
34
33
|
"start": "node bin/offgrid-ai.mjs",
|
|
35
34
|
"test": "node --test test/*.mjs",
|
|
36
35
|
"test:integration": "OFFGRID_INTEGRATION=1 node --test test/integration/*.mjs",
|
|
37
|
-
"lint": "eslint src/*.mjs src/commands/*.mjs
|
|
36
|
+
"lint": "eslint src/*.mjs src/commands/*.mjs scripts/*.mjs bin/*.mjs",
|
|
38
37
|
"check:privacy": "node scripts/privacy-gate.mjs",
|
|
39
38
|
"release:check": "bash scripts/release-check.sh",
|
|
40
39
|
"release:check:fast": "bash scripts/release-check.sh --skip-install --skip-manual",
|
|
@@ -42,9 +41,6 @@
|
|
|
42
41
|
"pretest": "npm run lint"
|
|
43
42
|
},
|
|
44
43
|
"dependencies": {
|
|
45
|
-
"@earendil-works/pi-agent-core": "^0.80.3",
|
|
46
|
-
"@earendil-works/pi-ai": "^0.80.3",
|
|
47
|
-
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
48
44
|
"@inquirer/prompts": "^8.5.2",
|
|
49
45
|
"picocolors": "^1.1.0"
|
|
50
46
|
},
|
|
@@ -61,9 +57,5 @@
|
|
|
61
57
|
"@eslint/js": "^10.0.1",
|
|
62
58
|
"eslint": "^10.4.1",
|
|
63
59
|
"globals": "^17.6.0"
|
|
64
|
-
},
|
|
65
|
-
"allowScripts": {
|
|
66
|
-
"@google/genai": true,
|
|
67
|
-
"protobufjs": true
|
|
68
60
|
}
|
|
69
61
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import { modelsCommand } from "./commands/models.mjs";
|
|
|
5
5
|
import { runCommand } from "./commands/run.mjs";
|
|
6
6
|
import { statusCommand } from "./commands/status.mjs";
|
|
7
7
|
import { stopCommand } from "./commands/stop.mjs";
|
|
8
|
-
import { benchmarkCommand } from "./commands/benchmark.mjs";
|
|
9
8
|
import { uninstallCommand } from "./commands/uninstall.mjs";
|
|
10
9
|
|
|
11
10
|
async function offerUpdate(argv) {
|
|
@@ -45,7 +44,6 @@ export async function run(argv) {
|
|
|
45
44
|
if (command === "run") return runCommand(argv.slice(1));
|
|
46
45
|
if (command === "status") return statusCommand();
|
|
47
46
|
if (command === "stop") return stopCommand(argv.slice(1));
|
|
48
|
-
if (command === "benchmark") return benchmarkCommand();
|
|
49
47
|
if (command === "uninstall" || command === "--uninstall") return uninstallCommand(argv.slice(1));
|
|
50
48
|
if (command === "--verbose") return mainFlow();
|
|
51
49
|
|
|
@@ -69,10 +67,9 @@ function printHelp() {
|
|
|
69
67
|
["Start", pc.bold("offgrid-ai")],
|
|
70
68
|
["Status", "offgrid-ai status"],
|
|
71
69
|
["Stop", "offgrid-ai stop"],
|
|
72
|
-
["Benchmark", "offgrid-ai benchmark"],
|
|
73
70
|
["Uninstall", "offgrid-ai uninstall"],
|
|
74
71
|
["Version", "offgrid-ai version"],
|
|
75
72
|
]), { formatBorder: pc.cyan }));
|
|
76
|
-
console.log("\n" + renderCard("How it works", "Run offgrid-ai, choose a local model, and start chatting in Pi.\n\nFirst run walks you through missing tools. After that, offgrid-ai remembers your model setup
|
|
73
|
+
console.log("\n" + renderCard("How it works", "Run offgrid-ai, choose a local model, and start chatting in Pi.\n\nFirst run walks you through missing tools. After that, offgrid-ai remembers your model setup.", { formatBorder: pc.magenta }));
|
|
77
74
|
console.log("\n" + pc.dim("Tip: use --verbose only when you want detailed install output."));
|
|
78
75
|
}
|
package/src/commands/main.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { offerManagedLlamaRuntimeUpdate } from "../runtime.mjs";
|
|
|
7
7
|
import { offerManagedOmlxUpdate, hasOmlx } from "../omlx-runtime.mjs";
|
|
8
8
|
import { hasLmStudioInstalled, scanManagedModels } from "../managed.mjs";
|
|
9
9
|
import { recommendedModel } from "../recommendations.mjs";
|
|
10
|
-
import { pc, startInteractive, createPrompt } from "../ui.mjs";
|
|
10
|
+
import { pc, startInteractive, createPrompt, renderCard } from "../ui.mjs";
|
|
11
11
|
import { onboardFlow } from "./onboard.mjs";
|
|
12
12
|
import { modelCommandCenter } from "./models.mjs";
|
|
13
13
|
import { statusCommand } from "./status.mjs";
|
|
@@ -58,9 +58,31 @@ export async function mainFlow() {
|
|
|
58
58
|
if (!process.stdin.isTTY) return await statusCommand();
|
|
59
59
|
|
|
60
60
|
startInteractive("offgrid-ai");
|
|
61
|
+
printStatusHeader({ llamaBinary, managedModels, piInstalled, omlxInstalled: await hasOmlx(), profiles });
|
|
62
|
+
console.log(pc.dim(" How to get models — offgrid-ai finds them on disk after you download:"));
|
|
63
|
+
console.log(pc.dim(" LM Studio Open LM Studio app, browse and download"));
|
|
64
|
+
console.log(pc.dim(" oMLX Open oMLX app, browse and download"));
|
|
65
|
+
console.log(pc.dim(" HuggingFace hf download mlx-community/gemma-4-e2b-it-4bit"));
|
|
66
|
+
console.log("");
|
|
61
67
|
return await modelCommandCenter({ profiles, ggufModels, managedModels, drafters });
|
|
62
68
|
}
|
|
63
69
|
|
|
70
|
+
function printStatusHeader({ llamaBinary, managedModels, piInstalled, omlxInstalled, profiles }) {
|
|
71
|
+
const omlxServerUp = managedModels.some((m) => m.backendId === "omlx" && m.status === "ok");
|
|
72
|
+
const parts = [
|
|
73
|
+
llamaBinary ? pc.green("llama.cpp ✓") : pc.red("llama.cpp ✗"),
|
|
74
|
+
];
|
|
75
|
+
if (omlxInstalled) {
|
|
76
|
+
parts.push(omlxServerUp ? pc.green("oMLX ✓ server up") : pc.yellow("oMLX ✓ server down"));
|
|
77
|
+
} else {
|
|
78
|
+
parts.push(pc.red("oMLX ✗"));
|
|
79
|
+
}
|
|
80
|
+
parts.push(piInstalled ? pc.green("Pi ✓") : pc.red("Pi ✗"));
|
|
81
|
+
if (profiles.length > 0) parts.push(pc.dim(`${profiles.length} model${profiles.length === 1 ? "" : "s"}`));
|
|
82
|
+
console.log(renderCard("offgrid-ai", parts.join(pc.dim(" · ")), { formatBorder: pc.cyan }));
|
|
83
|
+
console.log("");
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
async function printNoModelsHelp(llamaBinary) {
|
|
65
87
|
console.log(pc.yellow("No models found."));
|
|
66
88
|
console.log(pc.dim("You need to download a model to use offgrid-ai.\n"));
|
package/src/commands/models.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { ensureDirs } from "../config.mjs";
|
|
1
|
+
import { ensureDirs, getModelScanDirs, addModelScanDir, removeModelScanDir, DEFAULT_MODEL_DIRS, findLlamaServer, HF_HUB_DIR } from "../config.mjs";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import { backendFor, BACKENDS } from "../backends.mjs";
|
|
3
6
|
import { createProfileFromModel, readProfile, saveProfile, deleteProfile, profileJsonPath } from "../profiles.mjs";
|
|
4
7
|
import { isProfileRunning, isProfileServerUp, modelAvailableOnServer, stopProfile } from "../process.mjs";
|
|
5
|
-
import { syncPiConfig, removeFromPiConfig } from "../harness-pi.mjs";
|
|
8
|
+
import { syncPiConfig, removeFromPiConfig, hasPi } from "../harness-pi.mjs";
|
|
9
|
+
import { hasOmlx } from "../omlx-runtime.mjs";
|
|
6
10
|
import { configureLocalProfile, configureManagedProfile } from "../profile-setup.mjs";
|
|
7
|
-
import { pc, startInteractive, createPrompt, modelSelect } from "../ui.mjs";
|
|
11
|
+
import { pc, startInteractive, createPrompt, modelSelect, renderCard, renderRows } from "../ui.mjs";
|
|
8
12
|
import { buildCatalogItems, createManagedProfile, itemKey, loadModelCatalog, normalizeCatalog } from "../model-catalog.mjs";
|
|
9
|
-
import { modelSelectOption, modelNameWidth, inferBackendId, formatSourceLabel, discoverySourceForItem, printGgufModelDetails, printManagedModelDetails,
|
|
13
|
+
import { modelSelectOption, modelNameWidth, inferBackendId, formatSourceLabel, discoverySourceForItem, printGgufModelDetails, printManagedModelDetails, printProfileDetails } from "../model-presenters.mjs";
|
|
10
14
|
import { runProfile } from "./run.mjs";
|
|
11
15
|
|
|
12
16
|
const { stripVTControlCharacters } = await import("node:util");
|
|
@@ -31,7 +35,19 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
let catalog = initialCatalog.newModels ? initialCatalog : await loadModelCatalog();
|
|
39
|
+
|
|
40
|
+
while (true) {
|
|
41
|
+
const result = await showModelPicker(catalog);
|
|
42
|
+
if (result === "rescan") {
|
|
43
|
+
catalog = await loadModelCatalog();
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function showModelPicker(catalog) {
|
|
35
51
|
const normalized = normalizeCatalog(catalog);
|
|
36
52
|
const allItems = buildCatalogItems(normalized);
|
|
37
53
|
if (allItems.length === 0) {
|
|
@@ -50,9 +66,6 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
50
66
|
if (!(await modelAvailableOnServer(profile))) modelMissingIds.add(profile.id);
|
|
51
67
|
}
|
|
52
68
|
}
|
|
53
|
-
printWorkspaceHeader(normalized, runningProfilesNow, modelMissingIds);
|
|
54
|
-
await printBenchmarkLine();
|
|
55
|
-
|
|
56
69
|
const nameWidth = modelNameWidth(allItems);
|
|
57
70
|
|
|
58
71
|
const statusFor = (item) => {
|
|
@@ -82,15 +95,10 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
const groups = [];
|
|
85
|
-
const backendColors = {
|
|
86
|
-
"llama-cpp": pc.cyan,
|
|
87
|
-
omlx: pc.magenta,
|
|
88
|
-
};
|
|
89
98
|
for (const { backendId, sourceId, items } of byBackend.values()) {
|
|
90
99
|
const backendLabel = backendFor(backendId)?.label ?? backendId;
|
|
91
100
|
const sourceLabel = formatSourceLabel(sourceId);
|
|
92
|
-
const
|
|
93
|
-
const sep = `Inference: ${pc.bold(color(backendLabel))} ${pc.dim("|")} Source: ${sourceLabel} (${items.length})`;
|
|
101
|
+
const sep = ` ${pc.dim(backendLabel + " · " + sourceLabel + " (" + items.length + ")")}`;
|
|
94
102
|
const groupItems = items.map((item) => {
|
|
95
103
|
const opt = modelSelectOption(item, { runningProfilesNow, modelMissingIds, nameWidth, compact: true });
|
|
96
104
|
return { value: opt.value, label: opt.label, description: opt.description };
|
|
@@ -103,13 +111,21 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
103
111
|
const opt = modelSelectOption(item, { runningProfilesNow, modelMissingIds, nameWidth, compact: true });
|
|
104
112
|
return { value: opt.value, label: opt.label, description: opt.description };
|
|
105
113
|
});
|
|
106
|
-
groups.push({ separator: ` ${pc.
|
|
114
|
+
groups.push({ separator: ` ${pc.yellow("Needs setup (" + setupItems.length + ")")}`, items: groupItems });
|
|
107
115
|
}
|
|
108
116
|
|
|
117
|
+
groups.push({ separator: " ", items: [{ value: "__settings__", label: `${pc.dim("○")} ${pc.cyan("⚙ Status & settings")}` }] });
|
|
118
|
+
|
|
109
119
|
const prompt = createPrompt();
|
|
110
120
|
try {
|
|
111
121
|
const selected = await modelSelect("Select a model", groups, { pageSize: 20 });
|
|
112
122
|
if (!selected) return;
|
|
123
|
+
|
|
124
|
+
if (selected === "__settings__") {
|
|
125
|
+
await settingsFlow(prompt);
|
|
126
|
+
return "rescan";
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
const item = allItems.find((candidate) => itemKey(candidate) === selected);
|
|
114
130
|
if (!item) return;
|
|
115
131
|
|
|
@@ -144,10 +160,6 @@ function actionsForItem(item) {
|
|
|
144
160
|
{ value: "run", name: "Start chatting", desc: "Launch and open Pi" },
|
|
145
161
|
{ value: "reconfigure", name: "Reconfigure", desc: "Change context, MTP, settings" },
|
|
146
162
|
);
|
|
147
|
-
const backend = backendFor(item.profile.backend);
|
|
148
|
-
if (backend.type === "local-server" || backend.type === "managed-server") {
|
|
149
|
-
available.push({ value: "benchmark", name: "Benchmark", desc: "Prepare a benchmark run" });
|
|
150
|
-
}
|
|
151
163
|
}
|
|
152
164
|
available.push({ value: "remove", name: "Remove", desc: missing ? "Delete this broken setup" : "Delete this setup" });
|
|
153
165
|
if (missing) {
|
|
@@ -155,10 +167,6 @@ function actionsForItem(item) {
|
|
|
155
167
|
{ value: "run", name: "Start chatting", desc: "Launch and open Pi", dimmed: true },
|
|
156
168
|
{ value: "reconfigure", name: "Reconfigure", desc: "Change context, MTP, settings", dimmed: true },
|
|
157
169
|
);
|
|
158
|
-
const backend = backendFor(item.profile.backend);
|
|
159
|
-
if (backend.type === "local-server" || backend.type === "managed-server") {
|
|
160
|
-
available.push({ value: "benchmark", name: "Benchmark", desc: "Prepare a benchmark run", dimmed: true });
|
|
161
|
-
}
|
|
162
170
|
}
|
|
163
171
|
return formatActions(available);
|
|
164
172
|
}
|
|
@@ -176,7 +184,7 @@ function actionsForItem(item) {
|
|
|
176
184
|
|
|
177
185
|
async function performAction(prompt, action, item) {
|
|
178
186
|
const missing = item.type === "profile" && item.fileMissing;
|
|
179
|
-
if (missing && ["run", "reconfigure"
|
|
187
|
+
if (missing && ["run", "reconfigure"].includes(action)) {
|
|
180
188
|
console.log(pc.red("This model's file is no longer on disk. Remove the setup or move the file back."));
|
|
181
189
|
return;
|
|
182
190
|
}
|
|
@@ -185,14 +193,6 @@ async function performAction(prompt, action, item) {
|
|
|
185
193
|
if (item.type === "managed") return printManagedModelDetails(item.model, BACKENDS[item.backendId]);
|
|
186
194
|
return printGgufModelDetails(item.model, item.drafter);
|
|
187
195
|
}
|
|
188
|
-
if (action === "benchmark") {
|
|
189
|
-
if (item.type === "profile") {
|
|
190
|
-
const { benchmarkForProfile } = await import("../benchmark.mjs");
|
|
191
|
-
return await benchmarkForProfile(await readProfile(item.profile.id));
|
|
192
|
-
}
|
|
193
|
-
const { benchmarkFlow } = await import("../benchmark.mjs");
|
|
194
|
-
return await benchmarkFlow();
|
|
195
|
-
}
|
|
196
196
|
if (action === "run") return await runItem(item);
|
|
197
197
|
if (action === "reconfigure" || action === "setup") return await setupItem(prompt, item);
|
|
198
198
|
if (action === "remove" && item.type === "profile") return await removeProfileInteractive(item.profile.id);
|
|
@@ -255,4 +255,77 @@ async function removeProfileInteractive(id) {
|
|
|
255
255
|
await removeFromPiConfig(profile);
|
|
256
256
|
await deleteProfile(id);
|
|
257
257
|
console.log(pc.green(`Removed ${profile.label} (${profile.id})`));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Settings & discovery path management ───────────────────────────────────
|
|
261
|
+
|
|
262
|
+
async function settingsFlow(prompt) {
|
|
263
|
+
while (true) {
|
|
264
|
+
const llamaBinary = await findLlamaServer();
|
|
265
|
+
const omlxInstalled = await hasOmlx();
|
|
266
|
+
const piInstalled = await hasPi();
|
|
267
|
+
|
|
268
|
+
let omlxServerUp = false;
|
|
269
|
+
if (omlxInstalled) {
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch("http://127.0.0.1:8000/v1/models", { signal: AbortSignal.timeout(2000) });
|
|
272
|
+
omlxServerUp = res.ok;
|
|
273
|
+
} catch { /* server down */ }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log(renderCard("Runtime status", renderRows([
|
|
278
|
+
["llama.cpp", llamaBinary ? pc.green("✓ ") + pc.dim(llamaBinary) : pc.red("✗ not found")],
|
|
279
|
+
["oMLX", omlxInstalled ? (omlxServerUp ? pc.green("✓ server up") : pc.yellow("✓ installed · server down")) : pc.red("✗ not found")],
|
|
280
|
+
["Pi", piInstalled ? pc.green("✓ installed") : pc.red("✗ not found")],
|
|
281
|
+
]), { formatBorder: pc.cyan }));
|
|
282
|
+
|
|
283
|
+
const scanDirs = await getModelScanDirs();
|
|
284
|
+
const defaultSet = new Set(DEFAULT_MODEL_DIRS);
|
|
285
|
+
const pathLabels = new Map([
|
|
286
|
+
[join(homedir(), ".lmstudio", "models"), "LM Studio downloads"],
|
|
287
|
+
[join(homedir(), ".omlx", "models"), "oMLX downloads"],
|
|
288
|
+
[HF_HUB_DIR, "HuggingFace CLI downloads"],
|
|
289
|
+
]);
|
|
290
|
+
const pathRows = scanDirs.map((dir) => {
|
|
291
|
+
const exists = existsSync(dir);
|
|
292
|
+
const isBuiltin = defaultSet.has(dir);
|
|
293
|
+
const desc = pathLabels.get(dir);
|
|
294
|
+
const label = `${exists ? pc.green("✓") : pc.red("✗")} ${dir}`;
|
|
295
|
+
const tags = [desc, isBuiltin ? "built-in" : "custom"].filter(Boolean).join(pc.dim(" · "));
|
|
296
|
+
return [label, pc.dim(tags)];
|
|
297
|
+
});
|
|
298
|
+
console.log("");
|
|
299
|
+
console.log(renderCard("Discovery paths", renderRows(pathRows), { formatBorder: pc.magenta }));
|
|
300
|
+
|
|
301
|
+
const customDirs = scanDirs.filter((d) => !defaultSet.has(d));
|
|
302
|
+
const choices = [
|
|
303
|
+
{ value: "add", label: "Add discovery path" },
|
|
304
|
+
...(customDirs.length > 0 ? [{ value: "remove", label: "Remove discovery path" }] : []),
|
|
305
|
+
{ value: "back", label: "Back to models" },
|
|
306
|
+
];
|
|
307
|
+
const action = await prompt.choice("Settings", choices, "back");
|
|
308
|
+
|
|
309
|
+
if (!action || action === "back") return;
|
|
310
|
+
|
|
311
|
+
if (action === "add") {
|
|
312
|
+
const dir = await prompt.text("Path to model directory", "");
|
|
313
|
+
if (!dir || !dir.trim()) continue;
|
|
314
|
+
const cleanDir = dir.trim();
|
|
315
|
+
if (!existsSync(cleanDir)) {
|
|
316
|
+
console.log(pc.red(`Directory not found: ${cleanDir}`));
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
await addModelScanDir(cleanDir);
|
|
320
|
+
console.log(pc.green(`Added: ${cleanDir}`));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (action === "remove") {
|
|
324
|
+
const removeChoices = customDirs.map((d) => ({ value: d, label: d }));
|
|
325
|
+
const toRemove = await prompt.choice("Remove path", removeChoices);
|
|
326
|
+
if (!toRemove) continue;
|
|
327
|
+
await removeModelScanDir(toRemove);
|
|
328
|
+
console.log(pc.green(`Removed: ${toRemove}`));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
258
331
|
}
|
package/src/commands/run.mjs
CHANGED
|
@@ -113,6 +113,7 @@ async function launchHarness(profile, options, isManaged, withHarness, backend)
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
if (!(await hasPiModel(profile))) await syncPiConfig(profile);
|
|
116
|
+
|
|
116
117
|
try {
|
|
117
118
|
await launchPi(profile);
|
|
118
119
|
} finally {
|
|
@@ -121,10 +122,6 @@ async function launchHarness(profile, options, isManaged, withHarness, backend)
|
|
|
121
122
|
const result = await stopProfile(profile);
|
|
122
123
|
console.log(result.stopped ? pc.green(`[stop] ${result.message}`) : pc.dim(`[stop] ${result.message}`));
|
|
123
124
|
} else {
|
|
124
|
-
// Managed-server backends (oMLX): unload the model from the
|
|
125
|
-
// server's memory via its HTTP API. The server itself stays running
|
|
126
|
-
// (offgrid-ai doesn't manage it), but the model is released — same UX
|
|
127
|
-
// as local-server backends where stopProfile kills the process.
|
|
128
125
|
const result = await unloadModelFromServer(profile);
|
|
129
126
|
if (result.unloaded) {
|
|
130
127
|
console.log(pc.green(`[unload] ${backend.label}: model unloaded`));
|
package/src/config.mjs
CHANGED
|
@@ -26,6 +26,7 @@ export const HF_HUB_DIR = process.env.HF_HUB_CACHE
|
|
|
26
26
|
|
|
27
27
|
export const DEFAULT_MODEL_DIRS = [
|
|
28
28
|
join(homedir(), ".lmstudio", "models"),
|
|
29
|
+
join(homedir(), ".omlx", "models"),
|
|
29
30
|
HF_HUB_DIR,
|
|
30
31
|
];
|
|
31
32
|
|
|
@@ -47,7 +48,6 @@ const CONFIG_PATH = join(DATA_DIR, "config.json");
|
|
|
47
48
|
|
|
48
49
|
const DEFAULT_CONFIG = {
|
|
49
50
|
modelScanDirs: [],
|
|
50
|
-
benchmarkRepoPath: null,
|
|
51
51
|
binaryOverrides: {},
|
|
52
52
|
};
|
|
53
53
|
|
|
@@ -78,6 +78,21 @@ export async function getModelScanDirs() {
|
|
|
78
78
|
return [...DEFAULT_MODEL_DIRS, ...config.modelScanDirs].filter((dir, i, arr) => arr.indexOf(dir) === i);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
export async function addModelScanDir(dir) {
|
|
82
|
+
const config = await loadConfig();
|
|
83
|
+
config.modelScanDirs ??= [];
|
|
84
|
+
if (!config.modelScanDirs.includes(dir)) {
|
|
85
|
+
config.modelScanDirs.push(dir);
|
|
86
|
+
await saveConfig(config);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function removeModelScanDir(dir) {
|
|
91
|
+
const config = await loadConfig();
|
|
92
|
+
config.modelScanDirs = (config.modelScanDirs ?? []).filter((d) => d !== dir);
|
|
93
|
+
await saveConfig(config);
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
// ── Binary discovery ──────────────────────────────────────────────────────
|
|
82
97
|
|
|
83
98
|
import { execFile } from "node:child_process";
|
package/src/model-name.mjs
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// No other function should format, title-case, or dissect a model name.
|
|
5
5
|
//
|
|
6
6
|
// The returned `id` is always the raw identifier (untouched) and is used for
|
|
7
|
-
// API calls, profile IDs, Pi config matching
|
|
7
|
+
// API calls, profile IDs, and Pi config matching.
|
|
8
8
|
// The returned `display` is the human-readable string shown in pickers, details,
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
|
|
11
11
|
// ── Known model families ────────────────────────────────────────────────
|
|
12
12
|
//
|
package/src/model-presenters.mjs
CHANGED
|
@@ -7,9 +7,8 @@ import { pc, formatBytes, renderSectionRows } from "./ui.mjs";
|
|
|
7
7
|
import { capabilitySummary, ggufDetailParts, isProfileFileMissing, profileDetailParts } from "./model-summary.mjs";
|
|
8
8
|
import { itemKey } from "./model-catalog.mjs";
|
|
9
9
|
import { DATA_DIR } from "./config.mjs";
|
|
10
|
-
import { findBenchmarkRepo } from "./benchmark.mjs";
|
|
11
10
|
|
|
12
|
-
const OPTION_SEPARATOR =
|
|
11
|
+
const OPTION_SEPARATOR = " ";
|
|
13
12
|
const OPTION_STATUS_WIDTH = 12;
|
|
14
13
|
const OPTION_BACKEND_WIDTH = 14;
|
|
15
14
|
const OPTION_SOURCE_WIDTH = 14;
|
|
@@ -218,15 +217,6 @@ export function printWorkspaceHeader(normalized, runningProfilesNow, modelMissin
|
|
|
218
217
|
console.log(pc.dim(" ─────────────────────────────────────────────────────────"));
|
|
219
218
|
}
|
|
220
219
|
|
|
221
|
-
export async function printBenchmarkLine() {
|
|
222
|
-
const repoPath = await findBenchmarkRepo();
|
|
223
|
-
if (repoPath) {
|
|
224
|
-
console.log(pc.green(" ✓") + " local-llm-visual-benchmark linked");
|
|
225
|
-
} else {
|
|
226
|
-
console.log(pc.yellow(" ○") + " to run benchmarks, pair with " + pc.cyan("local-llm-visual-benchmark"));
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
220
|
export async function printProfileDetails(profile) {
|
|
231
221
|
const backend = backendFor(profile.backend);
|
|
232
222
|
const isManaged = backend.type === "managed-server";
|
|
@@ -266,8 +256,9 @@ export async function printProfileDetails(profile) {
|
|
|
266
256
|
const scriptPath = join(profileDir(profile.id), "start.sh");
|
|
267
257
|
console.log("\n" + renderSectionRows("Server command", [
|
|
268
258
|
["Run manually", pc.cyan(`bash ${scriptPath}`)],
|
|
269
|
-
|
|
270
|
-
|
|
259
|
+
]));
|
|
260
|
+
console.log("");
|
|
261
|
+
console.log(pc.dim(script));
|
|
271
262
|
}
|
|
272
263
|
}
|
|
273
264
|
}
|