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