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 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.16.3",
4
- "description": "Privacy-first CLI for running local LLMs — discover, configure, run, benchmark",
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 src/benchmark/*.mjs scripts/*.mjs bin/*.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.\n\nFor benchmarks, run offgrid-ai benchmark to prepare a visual or data-science benchmark run.", { formatBorder: pc.magenta }));
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
  }
@@ -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"));
@@ -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, printWorkspaceHeader, printBenchmarkLine, printProfileDetails } from "../model-presenters.mjs";
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
- const catalog = initialCatalog.newModels ? initialCatalog : await loadModelCatalog();
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 color = backendColors[backendId] ?? pc.dim;
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.bold(pc.yellow(`Needs setup (${setupItems.length})`))}`, items: groupItems });
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", "benchmark"].includes(action)) {
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
  }
@@ -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";
@@ -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, and benchmark directory slugs.
7
+ // API calls, profile IDs, and Pi config matching.
8
8
  // The returned `display` is the human-readable string shown in pickers, details,
9
- // and benchmark metadata.
9
+
10
10
 
11
11
  // ── Known model families ────────────────────────────────────────────────
12
12
  //
@@ -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 = pc.dim(" ");
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
- ["Command", pc.dim(script)],
270
- ], { columns: Math.min(process.stdout.columns ?? 120, 140) }));
259
+ ]));
260
+ console.log("");
261
+ console.log(pc.dim(script));
271
262
  }
272
263
  }
273
264
  }