offgrid-ai 0.6.1 → 0.6.3

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.6.1",
3
+ "version": "0.6.3",
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
@@ -6,6 +6,8 @@ import { homedir } from "node:os";
6
6
  import { execFile } from "node:child_process";
7
7
  import { promisify } from "node:util";
8
8
  import { ensureDirs, loadConfig, saveConfig } from "./config.mjs";
9
+ import { backendFor } from "./backends.mjs";
10
+ import { serverMatchesProfile, serverReady } from "./process.mjs";
9
11
  import { pc, createPrompt, renderRows, renderSection } from "./ui.mjs";
10
12
 
11
13
  const execFileAsync = promisify(execFile);
@@ -137,11 +139,56 @@ export async function linkBenchmarkRepo(prompt) {
137
139
 
138
140
  // ── Create a benchmark run directory ──────────────────────────────────────
139
141
 
142
+ function harnessDisplayName(id) {
143
+ if (id === "pi") return "Pi";
144
+ return String(id).replace(/[-_]+/gu, " ").replace(/\b\w/gu, (char) => char.toUpperCase());
145
+ }
146
+
147
+ function intendedRunnerForProfile(profile) {
148
+ if (!profile) return "your tool";
149
+ const harnessEntries = Object.entries(profile.harnesses ?? {}).filter(([, config]) => config?.enabled !== false);
150
+ const [id] = harnessEntries.find(([key]) => key === "pi") ?? harnessEntries[0] ?? ["pi"];
151
+ return harnessDisplayName(id);
152
+ }
153
+
154
+ async function profileServerStatus(profile) {
155
+ if (!profile) return null;
156
+ const ready = await serverReady(profile.baseUrl).catch(() => false);
157
+ if (!ready) return { running: false };
158
+ const match = await serverMatchesProfile(profile).catch(() => ({ matches: true, reason: "server responded" }));
159
+ return { running: match.matches, reason: match.reason };
160
+ }
161
+
162
+ function printBenchmarkNextSteps({ repoPath, profile, modelId, runnerLabel, serverStatus }) {
163
+ console.log("");
164
+ console.log(pc.bold("Next steps"));
165
+
166
+ let step = 1;
167
+ if (profile) {
168
+ const command = `offgrid-ai run ${profile.id}`;
169
+ if (serverStatus?.running) {
170
+ const reason = serverStatus.reason ? pc.dim(` (${serverStatus.reason})`) : "";
171
+ console.log(` ${step++}. Model server is already running at ${pc.cyan(profile.baseUrl)}${reason}`);
172
+ console.log(` ${pc.dim(`If you still need to open ${runnerLabel}, run: ${command}`)}`);
173
+ } else {
174
+ console.log(` ${step++}. If the model server is not already running, run: ${pc.cyan(command)}`);
175
+ }
176
+ } else {
177
+ console.log(` ${step++}. Open ${runnerLabel} for ${pc.bold(modelId)}`);
178
+ }
179
+
180
+ console.log(` ${step++}. ${pc.cyan(`cd ${repoPath}`)}`);
181
+ console.log(` ${step++}. ${pc.cyan("npm run dev")}`);
182
+ console.log(` ${step}. In the gallery, find this run, copy the prompt, and paste it into ${runnerLabel}`);
183
+ }
184
+
140
185
  async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSource, backendLabel, profile }) {
141
186
  const toolPrompt = buildToolPrompt(benchmark, kind);
142
187
  const now = new Date();
143
188
  const runId = createRunId(now);
144
189
  const modelSlug = slugModelId(modelId);
190
+ const runnerLabel = intendedRunnerForProfile(profile);
191
+ const serverStatus = await profileServerStatus(profile);
145
192
  const runsDir = join(repoPath, "runs");
146
193
  const benchmarkDirectory = join(runsDir, benchmark.id);
147
194
  const modelDirectory = join(benchmarkDirectory, modelSlug);
@@ -166,6 +213,8 @@ async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSo
166
213
  : { metadata: "metadata.json", prompt: "prompt.md", html: "index.html", preview: "preview.png", video: "preview.webm", rawResponse: "response.raw.txt" },
167
214
  runner: {
168
215
  mode: modelSource === "cloud" ? "manual" : "external",
216
+ intendedRunner: profile ? runnerLabel : undefined,
217
+ ...(profile?.harnesses?.pi || runnerLabel === "Pi" ? { tool: "pi" } : {}),
169
218
  ...(modelSource ? { modelSource } : {}),
170
219
  ...(backendLabel ? { backendLabel } : {}),
171
220
  ...(profile?.baseUrl ? { baseUrl: profile.baseUrl } : {}),
@@ -188,18 +237,7 @@ async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSo
188
237
  ["Source", backendLabel || modelSource],
189
238
  ])));
190
239
 
191
- console.log("");
192
- console.log(pc.bold("Next steps"));
193
- if (profile) {
194
- console.log(` 1. ${pc.cyan(`cd ${runDirectory}`)}`);
195
- console.log(` 2. ${pc.cyan(`offgrid-ai run ${profile.id}`)}`);
196
- console.log(` 3. In Pi, paste the prompt from ${pc.cyan("prompt.md")}`);
197
- console.log(` 4. View results: ${pc.cyan(`cd ${repoPath} && npm run dev`)}`);
198
- } else {
199
- console.log(` 1. ${pc.cyan(`cd ${runDirectory}`)}`);
200
- console.log(` 2. Copy the prompt from ${pc.cyan("prompt.md")} into your tool of choice`);
201
- console.log(` 3. View results: ${pc.cyan(`cd ${repoPath} && npm run dev`)}`);
202
- }
240
+ printBenchmarkNextSteps({ repoPath, profile, modelId, runnerLabel, serverStatus });
203
241
 
204
242
  return runDirectory;
205
243
  }
@@ -230,7 +268,6 @@ export async function benchmarkForProfile(profile) {
230
268
  const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
231
269
  if (!selectedBenchmark) return;
232
270
 
233
- const { backendFor } = await import("./backends.mjs");
234
271
  const modelId = profile.modelAlias;
235
272
  const modelSource = profile.providerId === "llama-cpp-mtp" ? "llama-cpp-mtp" : profile.backend === "ollama" ? "ollama" : profile.backend === "omlx" ? "omlx" : "llama-cpp";
236
273
  const backendLabel = backendFor(profile.backend).label;
@@ -269,7 +306,6 @@ export async function benchmarkFlow() {
269
306
  if (!selectedBenchmark) return;
270
307
 
271
308
  const { loadProfiles } = await import("./profiles.mjs");
272
- const { backendFor } = await import("./backends.mjs");
273
309
 
274
310
  const profiles = await loadProfiles();
275
311
  const source = await prompt.choice("Model source", [
package/src/cli.mjs CHANGED
@@ -272,37 +272,88 @@ function buildCatalogItems(normalized) {
272
272
  ];
273
273
  }
274
274
 
275
- function modelSelectOption(item, { runningProfilesNow }) {
275
+ const OPTION_SEPARATOR = pc.dim(" │ ");
276
+ const OPTION_STATUS_WIDTH = 12;
277
+ const OPTION_SOURCE_WIDTH = 14;
278
+
279
+ function optionTag(text, color, width) {
280
+ const padded = String(text).padEnd(width);
281
+ return color ? color(padded) : padded;
282
+ }
283
+
284
+ function optionStatusTag(kind) {
285
+ const statuses = {
286
+ running: ["RUNNING", pc.green],
287
+ ready: ["SET UP", pc.green],
288
+ missing: ["FILE MISSING", pc.red],
289
+ setup: ["NEEDS SETUP", pc.yellow],
290
+ };
291
+ const [text, color] = statuses[kind] ?? [kind, pc.dim];
292
+ return optionTag(text, color, OPTION_STATUS_WIDTH);
293
+ }
294
+
295
+ function optionSourceTag(sourceId, label) {
296
+ const colors = {
297
+ "llama-cpp": pc.cyan,
298
+ "llama-cpp-mtp": pc.blue,
299
+ ollama: pc.green,
300
+ omlx: pc.magenta,
301
+ gguf: pc.cyan,
302
+ };
303
+ return optionTag(label, colors[sourceId] ?? pc.dim, OPTION_SOURCE_WIDTH);
304
+ }
305
+
306
+ function optionLabel({ status, source, name, details = [] }) {
307
+ return [status, source, pc.bold(name), ...details].filter(Boolean).join(OPTION_SEPARATOR);
308
+ }
309
+
310
+ function optionHint(parts) {
311
+ return parts.filter(Boolean).join(" · ");
312
+ }
313
+
314
+ function modelSelectOption(item, { runningProfilesNow, drafters }) {
276
315
  if (item.type === "profile") {
277
316
  const { profile } = item;
317
+ const backend = backendFor(profile.backend);
278
318
  const running = runningProfilesNow.some((r) => r.id === profile.id);
279
319
  const fileMissing = item.fileMissing;
280
- let status;
281
- if (fileMissing) status = pc.red("⚠ missing");
282
- else if (running) status = pc.green("● running");
283
- else status = pc.dim("● ready");
284
- // MTP label: enabled vs available vs not available
320
+ const status = optionStatusTag(fileMissing ? "missing" : running ? "running" : "ready");
321
+ const source = optionSourceTag(profile.backend, backend.label);
322
+ const caps = profile.capabilities ?? {};
285
323
  let mtpLabel;
286
- if (profile.drafterPath) mtpLabel = pc.green("MTP");
287
- else if ((profile.capabilities?.architecture === "gemma4")) mtpLabel = pc.yellow("MTP available");
288
- const ctxLabel = profile.flags?.ctxSize ? `${(profile.flags.ctxSize / 1000).toFixed(0)}k` : null;
289
- const label = [profile.label, status, mtpLabel, ctxLabel].filter(Boolean).join(" ");
290
- return { value: itemKey(item), label, hint: humanCapabilitySummary(profile.capabilities ?? {}) || profile.modelAlias };
324
+ if (profile.drafterPath) mtpLabel = pc.green("MTP on");
325
+ else if (drafters ? matchDrafter(profile.modelPath, drafters) : null) mtpLabel = pc.yellow("MTP available");
326
+ else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP needs drafter");
327
+ const ctxLabel = profile.flags?.ctxSize ? pc.dim(`${(profile.flags.ctxSize / 1000).toFixed(0)}k ctx`) : null;
328
+ const label = optionLabel({ status, source, name: profile.label, details: [ctxLabel, mtpLabel] });
329
+ return { value: itemKey(item), label, hint: optionHint([profile.modelAlias, humanCapabilitySummary(caps), profile.baseUrl]) };
291
330
  }
292
331
  if (item.type === "new") {
293
332
  const { model, drafter } = item;
294
333
  const caps = detectCapabilities(model.path, model.mmprojPath);
295
334
  const mtpAvailable = caps.mtp || Boolean(drafter);
296
335
  let mtpLabel;
297
- if (mtpAvailable) mtpLabel = pc.green("MTP ");
298
- else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP");
299
- const label = [model.label, pc.yellow("needs setup"), mtpLabel, formatBytes(model.sizeBytes)].filter(Boolean).join(" ");
300
- return { value: itemKey(item), label, hint: humanCapabilitySummary(caps) || model.quant };
336
+ if (mtpAvailable) mtpLabel = pc.green("MTP available");
337
+ else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP needs drafter");
338
+ const label = optionLabel({
339
+ status: optionStatusTag("setup"),
340
+ source: optionSourceTag("gguf", "GGUF file"),
341
+ name: model.label,
342
+ details: [pc.dim(formatBytes(model.sizeBytes)), mtpLabel],
343
+ });
344
+ return { value: itemKey(item), label, hint: optionHint([basename(model.path), humanCapabilitySummary(caps), model.quant]) };
301
345
  }
302
346
  // managed
303
347
  const { model, backendId } = item;
304
348
  const backend = BACKENDS[backendId];
305
- return { value: itemKey(item), label: `${model.label} ${pc.dim(`via ${backend.label}`)}`, hint: model.id };
349
+ const details = [model.quant ? pc.dim(model.quant) : null, model.sizeBytes ? pc.dim(formatBytes(model.sizeBytes)) : null];
350
+ const label = optionLabel({
351
+ status: optionStatusTag("setup"),
352
+ source: optionSourceTag(backendId, backend.label),
353
+ name: model.label,
354
+ details,
355
+ });
356
+ return { value: itemKey(item), label, hint: optionHint([model.id, "available through local backend"]) };
306
357
  }
307
358
 
308
359
  function actionsForItem(item) {