offgrid-ai 0.5.2 → 0.5.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.mjs +70 -62
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.5.2",
3
+ "version": "0.5.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/cli.mjs CHANGED
@@ -201,16 +201,16 @@ async function modelCommandCenter(initialCatalog) {
201
201
  ["File missing", fileMissingCount > 0 ? pc.red(`${fileMissingCount} setup${fileMissingCount === 1 ? "" : "s"}`) : pc.dim("none")],
202
202
  ]), { formatBorder: summaryBorder }));
203
203
 
204
+ // Show model cards for all items
205
+ printModelCards(allItems, { runningProfilesNow, drafters: normalized.drafters });
206
+
204
207
  const options = allItems.map((item) => modelSelectOption(item, { runningProfilesNow, drafters: normalized.drafters }));
208
+ options.push({ value: "__exit__", label: pc.dim("Exit"), hint: "Close offgrid-ai" });
205
209
  const selected = await prompt.choice("Select a model", options);
206
- if (!selected) break;
210
+ if (!selected || selected === "__exit__") break;
207
211
  const item = allItems.find((i) => itemKey(i) === selected);
208
212
  if (!item) break;
209
213
 
210
- // Show detail card before action menu
211
- console.log("");
212
- printItemCard(item, { runningProfilesNow, drafters: normalized.drafters });
213
-
214
214
  const actions = actionsForItem(item);
215
215
  const action = await prompt.choice(item.label, actions, actions[0].value);
216
216
  if (!action) continue;
@@ -340,65 +340,73 @@ function isProfileFileMissing(profile) {
340
340
  return !existsSync(profile.modelPath);
341
341
  }
342
342
 
343
- function printItemCard(item, { runningProfilesNow, drafters }) {
344
- if (item.type === "profile") {
345
- const { profile } = item;
346
- const backend = backendFor(profile.backend);
347
- const isManaged = backend.type === "managed-server";
348
- const running = runningProfilesNow.some((r) => r.id === profile.id);
349
- const fileMissing = isProfileFileMissing(profile);
350
- const caps = profile.capabilities ?? {};
351
- let status;
352
- if (fileMissing) status = pc.red("File missing");
353
- else if (running) status = pc.green("Running now");
354
- else status = "Ready";
355
- const border = fileMissing ? pc.red : running ? pc.green : pc.dim;
356
- // MTP: distinguish enabled vs available
357
- let mtpLabel;
358
- if (profile.drafterPath) {
359
- mtpLabel = pc.green(`enabled (drafter: ${basename(profile.drafterPath)})`);
360
- } else if (drafters ? matchDrafter(profile.modelPath, drafters) : null) {
361
- mtpLabel = pc.yellow("available reconfigure to enable");
362
- } else if (caps.architecture === "gemma4") {
363
- mtpLabel = pc.yellow("drafter not found");
343
+ function printModelCards(items, { runningProfilesNow, drafters }) {
344
+ // Group items by type for section headers
345
+ const profiles = items.filter((i) => i.type === "profile");
346
+ const newModels = items.filter((i) => i.type === "new");
347
+ const managed = items.filter((i) => i.type === "managed");
348
+
349
+ if (profiles.length > 0) {
350
+ console.log("\n" + pc.bold("Ready to chat"));
351
+ for (const item of profiles) {
352
+ const { profile } = item;
353
+ const backend = backendFor(profile.backend);
354
+ const running = runningProfilesNow.some((r) => r.id === profile.id);
355
+ const fileMissing = item.fileMissing;
356
+ const caps = profile.capabilities ?? {};
357
+ let status;
358
+ if (fileMissing) status = pc.red("File missing");
359
+ else if (running) status = pc.green("Running now");
360
+ else status = "Ready";
361
+ const border = fileMissing ? pc.red : running ? pc.green : pc.dim;
362
+ // MTP: enabled vs available
363
+ let mtpLabel;
364
+ if (profile.drafterPath) mtpLabel = pc.green("MTP enabled");
365
+ else if (drafters ? matchDrafter(profile.modelPath, drafters) : null) mtpLabel = pc.yellow("MTP available");
366
+ else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP: needs drafter");
367
+ const detailParts = [fileMissing ? pc.red("File not found") : humanCapabilitySummary(caps)];
368
+ if (mtpLabel) detailParts.push(mtpLabel);
369
+ const ctxLabel = profile.flags?.ctxSize ? `${(profile.flags.ctxSize / 1000).toFixed(0)}k ctx` : null;
370
+ if (ctxLabel) detailParts.push(ctxLabel);
371
+ console.log(renderCard(profile.label, renderRows([
372
+ ["Status", status],
373
+ ["Details", detailParts.join(pc.dim(" · "))],
374
+ ["Runs with", backend.label],
375
+ ]), { formatBorder: border }));
364
376
  }
365
- const detailParts = [fileMissing ? pc.red("File not found") : humanCapabilitySummary(caps)];
366
- if (mtpLabel) detailParts.push(mtpLabel);
367
- const ctxLabel = profile.flags?.ctxSize ? `${(profile.flags.ctxSize / 1000).toFixed(0)}k ctx` : null;
368
- if (ctxLabel) detailParts.push(ctxLabel);
369
- const rows = [
370
- ["Status", status],
371
- ["Details", detailParts.join(pc.dim(" · "))],
372
- ["Runs with", backend.label],
373
- ];
374
- if (!isManaged && profile.modelPath) {
375
- rows.push(["File", fileMissing ? pc.red(`${profile.modelPath} (not found)`) : profile.modelPath]);
377
+ }
378
+
379
+ if (newModels.length > 0) {
380
+ console.log("\n" + pc.bold("Needs setup"));
381
+ for (const item of newModels) {
382
+ const { model, drafter } = item;
383
+ const caps = detectCapabilities(model.path, model.mmprojPath);
384
+ const mtpAvailable = caps.mtp || Boolean(drafter);
385
+ const mtpLabel = mtpAvailable
386
+ ? pc.green("MTP ✓")
387
+ : (caps.architecture === "gemma4")
388
+ ? pc.yellow("MTP: needs drafter")
389
+ : null;
390
+ const detailParts = [humanCapabilitySummary(caps)];
391
+ if (mtpLabel) detailParts.push(mtpLabel);
392
+ detailParts.push(formatBytes(model.sizeBytes));
393
+ console.log(renderCard(model.label, renderRows([
394
+ ["Status", pc.yellow("Needs setup")],
395
+ ["Details", detailParts.join(pc.dim(" · "))],
396
+ ]), { formatBorder: pc.yellow }));
397
+ }
398
+ }
399
+
400
+ for (const beId of ["ollama", "omlx"]) {
401
+ const managedForBackend = managed.filter((i) => i.backendId === beId);
402
+ if (managedForBackend.length === 0) continue;
403
+ const be = BACKENDS[beId];
404
+ console.log("\n" + pc.bold(`Via ${be.label}`));
405
+ for (const item of managedForBackend) {
406
+ console.log(renderCard(item.model.label, renderRows([
407
+ ["Details", [item.model.id, item.model.quant].filter(Boolean).join(pc.dim(" · "))],
408
+ ]), { formatBorder: pc.dim }));
376
409
  }
377
- console.log(renderCard(profile.label, renderRows(rows), { formatBorder: border }));
378
- } else if (item.type === "new") {
379
- const { model, drafter } = item;
380
- const caps = detectCapabilities(model.path, model.mmprojPath);
381
- const mtpAvailable = caps.mtp || Boolean(drafter);
382
- const mtpLabel = mtpAvailable
383
- ? pc.green("MTP ✓")
384
- : (caps.architecture === "gemma4")
385
- ? pc.yellow("MTP: needs drafter")
386
- : null;
387
- const detailParts = [humanCapabilitySummary(caps)];
388
- if (mtpLabel) detailParts.push(mtpLabel);
389
- detailParts.push(formatBytes(model.sizeBytes));
390
- console.log(renderCard(model.label, renderRows([
391
- ["Status", pc.yellow("Needs setup")],
392
- ["Details", detailParts.join(pc.dim(" · "))],
393
- ]), { formatBorder: pc.yellow }));
394
- } else {
395
- // managed
396
- const { model, backendId } = item;
397
- const backend = BACKENDS[backendId];
398
- console.log(renderCard(model.label, renderRows([
399
- ["Status", pc.dim(`Via ${backend.label}`)],
400
- ["Details", [model.id, model.quant].filter(Boolean).join(pc.dim(" · "))],
401
- ]), { formatBorder: pc.dim }));
402
410
  }
403
411
  }
404
412