offgrid-ai 0.7.2 → 0.7.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Privacy-first CLI for running local LLMs — discover, configure, run, benchmark",
5
5
  "author": "Eeshan Srivastava (https://eeshans.com)",
6
6
  "type": "module",
package/src/benchmark.mjs CHANGED
@@ -28,17 +28,7 @@ export function createRunId(date = new Date()) {
28
28
  }
29
29
 
30
30
  export function buildToolPrompt(benchmark, kind) {
31
- if (kind === "data-science") return benchmark.prompt;
32
- return [
33
- "Create a complete, self-contained HTML file for the request below.",
34
- "Write the file as `index.html` in the current working directory.",
35
- "Do not create any folders, do not infer a filesystem path, and do not print the HTML in chat.",
36
- "",
37
- "The HTML must include all CSS and JavaScript inline and must not depend on external network assets.",
38
- "After building the page, run a visual QA pass with agent-browser or Playwright: open the saved index.html, inspect the rendered result, and fix any obvious layout, animation, console, or viewport issues before you finish.",
39
- "",
40
- benchmark.prompt,
41
- ].join("\n");
31
+ return benchmark.prompt;
42
32
  }
43
33
 
44
34
  export async function loadBenchmarks(benchDir) {
@@ -9,6 +9,8 @@ import { buildCatalogItems, createManagedProfile, itemKey, loadModelCatalog, nor
9
9
  import { modelSelectOption, modelNameWidth, printGgufModelDetails, printManagedModelDetails, printWorkspaceHeader, printBenchmarkLine, printProfileDetails } from "../model-presenters.mjs";
10
10
  import { runProfile } from "./run.mjs";
11
11
 
12
+ const { stripVTControlCharacters } = await import("node:util");
13
+
12
14
  export async function modelsCommand(argv) {
13
15
  await ensureDirs();
14
16
  const catalog = await loadModelCatalog();
@@ -62,33 +64,64 @@ export async function modelCommandCenter(initialCatalog) {
62
64
  }
63
65
  }
64
66
 
65
-
67
+ function formatActions(rawActions) {
68
+ const sep = pc.dim(" │ ");
69
+ const maxName = Math.max(...rawActions.map((a) => stripVTControlCharacters(a.name).length));
70
+ const width = Math.max(17, maxName + 2);
71
+ return rawActions.map((a) => {
72
+ const name = a.dimmed ? pc.dim(pc.strikethrough(a.name.padEnd(width).slice(0, width))) : pc.bold(a.name.padEnd(width).slice(0, width));
73
+ const desc = a.dimmed ? pc.red("file not found") : pc.dim(a.desc);
74
+ return { value: a.value, label: name + sep + desc };
75
+ });
76
+ }
66
77
 
67
78
  function actionsForItem(item) {
79
+ const missing = item.type === "profile" && item.fileMissing;
68
80
  if (item.type === "profile") {
69
- const actions = [
70
- { value: "run", label: "Start chatting", hint: "Launch and open Pi" },
71
- { value: "reconfigure", label: "Reconfigure", hint: "Change context, MTP, settings" },
72
- { value: "inspect", label: "Details", hint: "Paths, ports, flags" },
81
+ const available = [
82
+ { value: "inspect", name: "Details", desc: "Paths, ports, flags" },
73
83
  ];
74
- const backend = backendFor(item.profile.backend);
75
- if (backend.type === "local-server" || backend.type === "managed-server") actions.push({ value: "benchmark", label: "Benchmark", hint: "Prepare a benchmark run" });
76
- if (!item.fileMissing) actions.push({ value: "remove", label: "Remove", hint: "Delete this setup" });
77
- return actions;
84
+ if (!missing) {
85
+ available.unshift(
86
+ { value: "run", name: "Start chatting", desc: "Launch and open Pi" },
87
+ { value: "reconfigure", name: "Reconfigure", desc: "Change context, MTP, settings" },
88
+ );
89
+ const backend = backendFor(item.profile.backend);
90
+ if (backend.type === "local-server" || backend.type === "managed-server") {
91
+ available.push({ value: "benchmark", name: "Benchmark", desc: "Prepare a benchmark run" });
92
+ }
93
+ }
94
+ available.push({ value: "remove", name: "Remove", desc: missing ? "Delete this broken setup" : "Delete this setup" });
95
+ if (missing) {
96
+ available.unshift(
97
+ { value: "run", name: "Start chatting", desc: "Launch and open Pi", dimmed: true },
98
+ { value: "reconfigure", name: "Reconfigure", desc: "Change context, MTP, settings", dimmed: true },
99
+ );
100
+ const backend = backendFor(item.profile.backend);
101
+ if (backend.type === "local-server" || backend.type === "managed-server") {
102
+ available.push({ value: "benchmark", name: "Benchmark", desc: "Prepare a benchmark run", dimmed: true });
103
+ }
104
+ }
105
+ return formatActions(available);
78
106
  }
79
107
  if (item.type === "new") {
80
- return [
81
- { value: "setup", label: "Set up", hint: "Configure and save" },
82
- { value: "inspect", label: "Details", hint: "Model info" },
83
- ];
108
+ return formatActions([
109
+ { value: "setup", name: "Set up", desc: "Configure and save" },
110
+ { value: "inspect", name: "Details", desc: "Model info" },
111
+ ]);
84
112
  }
85
- return [
86
- { value: "setup", label: "Set up", hint: `Connect via ${BACKENDS[item.backendId].label}` },
87
- { value: "inspect", label: "Details", hint: "Model info" },
88
- ];
113
+ return formatActions([
114
+ { value: "setup", name: "Set up", desc: `Connect via ${BACKENDS[item.backendId].label}` },
115
+ { value: "inspect", name: "Details", desc: "Model info" },
116
+ ]);
89
117
  }
90
118
 
91
119
  async function performAction(prompt, action, item) {
120
+ const missing = item.type === "profile" && item.fileMissing;
121
+ if (missing && ["run", "reconfigure", "benchmark"].includes(action)) {
122
+ console.log(pc.red("This model's file is no longer on disk. Remove the setup or move the file back."));
123
+ return;
124
+ }
92
125
  if (action === "inspect") {
93
126
  if (item.type === "profile") return await printProfileDetails(await readProfile(item.profile.id));
94
127
  if (item.type === "managed") return printManagedModelDetails(item.model, BACKENDS[item.backendId]);
@@ -169,4 +202,4 @@ async function removeProfileInteractive(id) {
169
202
  await removeFromPiConfig(profile);
170
203
  await deleteProfile(id);
171
204
  console.log(pc.green(`Removed ${profile.label} (${profile.id})`));
172
- }
205
+ }
@@ -25,7 +25,7 @@ function optionPad(text, color, width) {
25
25
  function optionStatusTag(kind) {
26
26
  const statuses = {
27
27
  running: ["RUNNING", pc.green],
28
- ready: ["READY", pc.green],
28
+ ready: ["READY", pc.blue],
29
29
  missing: ["MISSING", pc.red],
30
30
  setup: ["SETUP", pc.yellow],
31
31
  };
@@ -130,13 +130,14 @@ export function printWorkspaceHeader(normalized, runningProfilesNow) {
130
130
  const setupCount = normalized.newModels.length + normalized.managedItems.length;
131
131
 
132
132
  const countParts = [];
133
- if (runningCount > 0) countParts.push(`${runningCount} running`);
134
- if (readyCount > 0) countParts.push(pc.green(`${readyCount} model${readyCount === 1 ? "" : "s"} ready`));
133
+ if (runningCount > 0) countParts.push(pc.green(`${runningCount} running`));
134
+ if (readyCount > 0) countParts.push(pc.blue(`${readyCount} model${readyCount === 1 ? "" : "s"} ready`));
135
135
  if (missingCount > 0) countParts.push(pc.red(`${missingCount} model${missingCount === 1 ? "" : "s"} missing`));
136
136
  if (setupCount > 0) countParts.push(pc.yellow(`${setupCount} model${setupCount === 1 ? "" : "s"} need${setupCount === 1 ? "s" : ""} setup`));
137
137
 
138
- console.log(pc.dim(` ${countParts.join(" · ")}`));
138
+ console.log(` ${countParts.join(pc.dim(" · "))}`);
139
139
  console.log(pc.dim(` Profiles: ${DATA_DIR}`));
140
+ console.log(pc.dim(" ─────────────────────────────────────────────────────────"));
140
141
  }
141
142
 
142
143
  export async function printBenchmarkLine() {
@@ -155,7 +156,7 @@ export async function printProfileDetails(profile) {
155
156
  const fileMissing = !isManaged && isProfileFileMissing(profile);
156
157
  console.log("\n" + renderSection("Model overview", renderRows([
157
158
  ["Name", pc.bold(profile.label)],
158
- ["Status", fileMissing ? pc.red("File missing") : running ? pc.green("Running now") : "Ready"],
159
+ ["Status", fileMissing ? pc.red("File missing") : running ? pc.green("Running now") : pc.blue("Ready")],
159
160
  ["Details", profileDetailParts(profile, { fileMissing }).join(pc.dim(" · "))],
160
161
  ["Server", fileMissing ? pc.red(profile.baseUrl) : profile.baseUrl],
161
162
  ])));