offgrid-ai 0.3.31 → 0.4.1

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 +39 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.3.31",
3
+ "version": "0.4.1",
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
@@ -241,12 +241,15 @@ async function printModelCatalog({ profiles, newModels, managedItems }, items =
241
241
  if (await isProfileRunning(profile)) runningProfilesNow.push(profile);
242
242
  }
243
243
 
244
+ const fileMissingCount = profiles.filter((p) => isProfileFileMissing(p)).length;
245
+
244
246
  console.log("\n" + renderCard("Your local AI workspace", renderRows([
245
247
  ["Ready to chat", pc.green(`${profiles.length} saved setup${profiles.length === 1 ? "" : "s"}`)],
246
248
  ["Need setup", newModels.length > 0 ? pc.yellow(`${newModels.length} downloaded model${newModels.length === 1 ? "" : "s"}`) : pc.green("none")],
247
249
  ["Running now", runningProfilesNow.length > 0 ? pc.green(String(runningProfilesNow.length)) : pc.dim("none")],
248
- ["Next step", profiles.length > 0 ? "Start chatting" : newModels.length > 0 ? "Set up a downloaded model" : "Download a model"],
249
- ]), { formatBorder: pc.cyan }));
250
+ ["File missing", fileMissingCount > 0 ? pc.red(`${fileMissingCount} setup${fileMissingCount === 1 ? "" : "s"}`) : pc.green("none")],
251
+ ["Next step", fileMissingCount > 0 ? pc.red("Remove or fix missing setups") : profiles.length > 0 ? "Start chatting" : newModels.length > 0 ? "Set up a downloaded model" : "Download a model"],
252
+ ]), { formatBorder: fileMissingCount > 0 ? pc.yellow : pc.cyan }));
250
253
 
251
254
  console.log("\n" + pc.bold("Ready to chat"));
252
255
  if (profiles.length === 0) {
@@ -255,8 +258,9 @@ async function printModelCatalog({ profiles, newModels, managedItems }, items =
255
258
  for (const profile of profiles) {
256
259
  const running = runningProfilesNow.some((item) => item.id === profile.id);
257
260
  const piConfigured = await hasPiModel(profile);
261
+ const fileMissing = isProfileFileMissing(profile);
258
262
  const num = itemNumber((item) => item.type === "profile" && item.profile.id === profile.id);
259
- console.log(profileCatalogCard(num, profile, { running, piConfigured }));
263
+ console.log(profileCatalogCard(num, profile, { running, piConfigured, fileMissing }));
260
264
  }
261
265
  }
262
266
 
@@ -285,16 +289,30 @@ async function printModelCatalog({ profiles, newModels, managedItems }, items =
285
289
  }
286
290
  }
287
291
 
288
- function profileCatalogCard(num, profile, { running, piConfigured }) {
292
+ function isProfileFileMissing(profile) {
293
+ const backend = backendFor(profile.backend);
294
+ if (backend.type === "managed-server") return false;
295
+ if (!profile.modelPath) return true; // no path recorded means we can't verify
296
+ return !existsSync(profile.modelPath);
297
+ }
298
+
299
+ function profileCatalogCard(num, profile, { running, piConfigured, fileMissing }) {
289
300
  const backend = backendFor(profile.backend);
290
301
  const caps = profile.capabilities ?? {};
291
- const status = running ? statusText("running", "Running now") : statusText("ready", "Ready to chat");
302
+ let status;
303
+ if (fileMissing) {
304
+ status = pc.red("File missing");
305
+ } else if (running) {
306
+ status = statusText("running", "Running now");
307
+ } else {
308
+ status = statusText("ready", "Ready to chat");
309
+ }
292
310
  return renderCard(`${num}. ${profile.label}`, renderRows([
293
311
  ["Status", status],
294
- ["Good for", humanCapabilitySummary(caps)],
312
+ ["Good for", fileMissing ? pc.red("Model file not found") : humanCapabilitySummary(caps)],
295
313
  ["Pi", piConfigured ? pc.green("Synced") : pc.yellow("Needs sync")],
296
314
  ["Runs with", backend.label],
297
- ]), { formatBorder: running ? pc.green : pc.cyan });
315
+ ]), { formatBorder: fileMissing ? pc.red : running ? pc.green : pc.cyan });
298
316
  }
299
317
 
300
318
  function downloadedModelCard(num, model, caps) {
@@ -316,7 +334,7 @@ function managedModelCard(num, model, backend) {
316
334
 
317
335
  function modelCatalogItems({ profiles, newModels, managedItems }) {
318
336
  return [
319
- ...profiles.map((profile) => ({ type: "profile", profile, label: profile.label, hint: `${profile.modelAlias} · ${profile.baseUrl}` })),
337
+ ...profiles.map((profile) => ({ type: "profile", profile, label: profile.label, hint: `${profile.modelAlias} · ${profile.baseUrl}`, fileMissing: isProfileFileMissing(profile) })),
320
338
  ...newModels.map((model) => ({ type: "new", model, label: model.label, hint: `${model.quant ?? "GGUF"} · ${formatBytes(model.sizeBytes)}` })),
321
339
  ...managedItems.map(({ model, backendId }) => ({ type: "managed", model, backendId, label: model.label, hint: BACKENDS[backendId].label })),
322
340
  ];
@@ -389,12 +407,16 @@ async function printProfileDetails(profile) {
389
407
  const isManaged = backend.type === "managed-server";
390
408
  const piConfigured = await hasPiModel(profile);
391
409
  const running = await isProfileRunning(profile);
410
+ const fileMissing = !isManaged && isProfileFileMissing(profile);
411
+ const statusRow = fileMissing
412
+ ? pc.red("File missing")
413
+ : running ? pc.green("Running now") : pc.green("Ready to chat");
392
414
  console.log("\n" + renderSection("Model overview", renderRows([
393
415
  ["Name", pc.bold(profile.label)],
394
- ["Status", running ? pc.green("Running now") : pc.green("Ready to chat")],
395
- ["Good for", humanCapabilitySummary(profile.capabilities ?? {})],
416
+ ["Status", statusRow],
417
+ ["Good for", fileMissing ? pc.red("Model file not found — remove or fix this setup") : humanCapabilitySummary(profile.capabilities ?? {})],
396
418
  ["Pi", piConfigured ? pc.green("Synced") : pc.yellow("Needs sync")],
397
- ["Server", pc.green(profile.baseUrl)],
419
+ ["Server", fileMissing ? pc.red(profile.baseUrl) : pc.green(profile.baseUrl)],
398
420
  ])));
399
421
 
400
422
  console.log("\n" + renderSection("Model details", renderRows([
@@ -403,12 +425,16 @@ async function printProfileDetails(profile) {
403
425
  ["Model alias", pc.cyan(profile.modelAlias)],
404
426
  ...(profile.capabilities ? [["Detected", capabilitySummary(profile.capabilities)]] : []),
405
427
  ...(!isManaged ? [
406
- ["Local file", profile.modelPath ?? "unknown"],
407
- ["Vision file", profile.mmprojPath ?? "none"],
428
+ ["Local file", fileMissing ? pc.red(`${profile.modelPath} (not found)`) : profile.modelPath ?? "unknown"],
429
+ ["Vision file", profile.mmprojPath ? (existsSync(profile.mmprojPath) ? profile.mmprojPath : pc.red(`${profile.mmprojPath} (not found)`)) : "none"],
408
430
  ["Model size", profile.modelPath && existsSync(profile.modelPath) ? formatBytes(statSync(profile.modelPath).size) : "unknown"],
409
431
  ] : []),
410
432
  ]), { columns: 110 }));
411
433
 
434
+ if (fileMissing) {
435
+ console.log("\n" + pc.red("⚠ This model's file is no longer on disk. Remove this setup or move the file back."));
436
+ }
437
+
412
438
  if (!isManaged && profile.commandArgv) {
413
439
  const commandArgv = await readCommandArgv(profile);
414
440
  console.log("\n" + renderSection("llama-server command", pc.dim(buildPrettyCommand({ ...profile, commandArgv })), { columns: 120 }));