offgrid-ai 0.4.0 → 0.4.2

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 +58 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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
@@ -9,7 +9,7 @@ import { startServer, stopProfile, waitForReady, serverReady, serverMatchesProfi
9
9
  import { syncPiConfig, removeFromPiConfig, hasPiModel, launchPi, hasPi } from "./harness-pi.mjs";
10
10
  import { tailFriendly } from "./logs.mjs";
11
11
  import { estimateMemory } from "./estimate.mjs";
12
- import { pc, formatBytes, renderRows, renderSection, renderCard, humanCapabilitySummary, statusText, startInteractive, createPrompt, parseOptions } from "./ui.mjs";
12
+ import { pc, formatBytes, renderRows, renderSection, renderCard, humanCapabilitySummary, startInteractive, createPrompt, parseOptions } from "./ui.mjs";
13
13
  import { checkForUpdate, currentPackageVersion, detectInvocation, updateCommand, runUpdateCommand } from "./updates.mjs";
14
14
  import { removeInstallerPathEntries } from "./shell-path.mjs";
15
15
  import { configureLocalProfile } from "./profile-setup.mjs";
@@ -241,12 +241,16 @@ 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
+
246
+ const summaryBorder = fileMissingCount > 0 ? pc.red : newModels.length > 0 ? pc.yellow : pc.dim;
244
247
  console.log("\n" + renderCard("Your local AI workspace", renderRows([
245
- ["Ready to chat", pc.green(`${profiles.length} saved setup${profiles.length === 1 ? "" : "s"}`)],
246
- ["Need setup", newModels.length > 0 ? pc.yellow(`${newModels.length} downloaded model${newModels.length === 1 ? "" : "s"}`) : pc.green("none")],
247
- ["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 }));
248
+ ["Setups", `${profiles.length} saved`],
249
+ ["Need setup", newModels.length > 0 ? pc.yellow(`${newModels.length} model${newModels.length === 1 ? "" : "s"}`) : pc.dim("none")],
250
+ ["Running", runningProfilesNow.length > 0 ? pc.green(String(runningProfilesNow.length)) : pc.dim("none")],
251
+ ["File missing", fileMissingCount > 0 ? pc.red(`${fileMissingCount} setup${fileMissingCount === 1 ? "" : "s"}`) : pc.dim("none")],
252
+ ["Next step", fileMissingCount > 0 ? pc.red("Remove or fix missing setups") : profiles.length > 0 ? "Start chatting" : newModels.length > 0 ? pc.yellow("Set up a downloaded model") : "Download a model"],
253
+ ]), { formatBorder: summaryBorder }));
250
254
 
251
255
  console.log("\n" + pc.bold("Ready to chat"));
252
256
  if (profiles.length === 0) {
@@ -255,14 +259,15 @@ async function printModelCatalog({ profiles, newModels, managedItems }, items =
255
259
  for (const profile of profiles) {
256
260
  const running = runningProfilesNow.some((item) => item.id === profile.id);
257
261
  const piConfigured = await hasPiModel(profile);
262
+ const fileMissing = isProfileFileMissing(profile);
258
263
  const num = itemNumber((item) => item.type === "profile" && item.profile.id === profile.id);
259
- console.log(profileCatalogCard(num, profile, { running, piConfigured }));
264
+ console.log(profileCatalogCard(num, profile, { running, piConfigured, fileMissing }));
260
265
  }
261
266
  }
262
267
 
263
- console.log("\n" + pc.bold("Downloaded, needs one-time setup"));
268
+ console.log("\n" + pc.bold("Needs one-time setup"));
264
269
  if (newModels.length === 0) {
265
- console.log(renderCard("All set", "Every downloaded local model already has a saved setup.", { formatBorder: pc.green }));
270
+ console.log(renderCard("All set", "Every downloaded local model already has a saved setup.", { formatBorder: pc.dim }));
266
271
  } else {
267
272
  for (const model of newModels.slice(0, 20)) {
268
273
  const caps = detectCapabilities(model.path, model.mmprojPath);
@@ -285,21 +290,36 @@ async function printModelCatalog({ profiles, newModels, managedItems }, items =
285
290
  }
286
291
  }
287
292
 
288
- function profileCatalogCard(num, profile, { running, piConfigured }) {
293
+ function isProfileFileMissing(profile) {
294
+ const backend = backendFor(profile.backend);
295
+ if (backend.type === "managed-server") return false;
296
+ if (!profile.modelPath) return true; // no path recorded means we can't verify
297
+ return !existsSync(profile.modelPath);
298
+ }
299
+
300
+ function profileCatalogCard(num, profile, { running, piConfigured, fileMissing }) {
289
301
  const backend = backendFor(profile.backend);
290
302
  const caps = profile.capabilities ?? {};
291
- const status = running ? statusText("running", "Running now") : statusText("ready", "Ready to chat");
303
+ let status;
304
+ if (fileMissing) {
305
+ status = pc.red("File missing");
306
+ } else if (running) {
307
+ status = pc.green("Running now");
308
+ } else {
309
+ status = "Ready";
310
+ }
311
+ const border = fileMissing ? pc.red : running ? pc.green : pc.dim;
292
312
  return renderCard(`${num}. ${profile.label}`, renderRows([
293
313
  ["Status", status],
294
- ["Good for", humanCapabilitySummary(caps)],
295
- ["Pi", piConfigured ? pc.green("Synced") : pc.yellow("Needs sync")],
314
+ ["Good for", fileMissing ? pc.red("Model file not found") : humanCapabilitySummary(caps)],
315
+ ["Pi", piConfigured ? pc.dim("synced") : pc.yellow("Needs sync")],
296
316
  ["Runs with", backend.label],
297
- ]), { formatBorder: running ? pc.green : pc.cyan });
317
+ ]), { formatBorder: border });
298
318
  }
299
319
 
300
320
  function downloadedModelCard(num, model, caps) {
301
321
  return renderCard(`${num}. ${model.label}`, renderRows([
302
- ["Status", statusText("warning", "Needs one-time setup")],
322
+ ["Status", pc.yellow("Needs setup")],
303
323
  ["Good for", humanCapabilitySummary(caps)],
304
324
  ["Size", formatBytes(model.sizeBytes)],
305
325
  ]), { formatBorder: pc.yellow });
@@ -307,16 +327,16 @@ function downloadedModelCard(num, model, caps) {
307
327
 
308
328
  function managedModelCard(num, model, backend) {
309
329
  return renderCard(`${num}. ${model.label}`, renderRows([
310
- ["Status", statusText("info", `Local model via ${backend.label}`)],
330
+ ["Status", pc.dim(`Via ${backend.label}`)],
311
331
  ["Runs with", backend.label],
312
- ["Model ID", pc.cyan(model.id)],
332
+ ["Model ID", model.id],
313
333
  ...(model.quant ? [["Size/type", model.quant]] : []),
314
- ]), { formatBorder: pc.magenta });
334
+ ]), { formatBorder: pc.dim });
315
335
  }
316
336
 
317
337
  function modelCatalogItems({ profiles, newModels, managedItems }) {
318
338
  return [
319
- ...profiles.map((profile) => ({ type: "profile", profile, label: profile.label, hint: `${profile.modelAlias} · ${profile.baseUrl}` })),
339
+ ...profiles.map((profile) => ({ type: "profile", profile, label: profile.label, hint: `${profile.modelAlias} · ${profile.baseUrl}`, fileMissing: isProfileFileMissing(profile) })),
320
340
  ...newModels.map((model) => ({ type: "new", model, label: model.label, hint: `${model.quant ?? "GGUF"} · ${formatBytes(model.sizeBytes)}` })),
321
341
  ...managedItems.map(({ model, backendId }) => ({ type: "managed", model, backendId, label: model.label, hint: BACKENDS[backendId].label })),
322
342
  ];
@@ -389,26 +409,34 @@ async function printProfileDetails(profile) {
389
409
  const isManaged = backend.type === "managed-server";
390
410
  const piConfigured = await hasPiModel(profile);
391
411
  const running = await isProfileRunning(profile);
412
+ const fileMissing = !isManaged && isProfileFileMissing(profile);
413
+ const statusRow = fileMissing
414
+ ? pc.red("File missing")
415
+ : running ? pc.green("Running now") : "Ready";
392
416
  console.log("\n" + renderSection("Model overview", renderRows([
393
417
  ["Name", pc.bold(profile.label)],
394
- ["Status", running ? pc.green("Running now") : pc.green("Ready to chat")],
395
- ["Good for", humanCapabilitySummary(profile.capabilities ?? {})],
396
- ["Pi", piConfigured ? pc.green("Synced") : pc.yellow("Needs sync")],
397
- ["Server", pc.green(profile.baseUrl)],
418
+ ["Status", statusRow],
419
+ ["Good for", fileMissing ? pc.red("Model file not found — remove or fix this setup") : humanCapabilitySummary(profile.capabilities ?? {})],
420
+ ["Pi", piConfigured ? pc.dim("synced") : pc.yellow("Needs sync")],
421
+ ["Server", fileMissing ? pc.red(profile.baseUrl) : profile.baseUrl],
398
422
  ])));
399
423
 
400
424
  console.log("\n" + renderSection("Model details", renderRows([
401
- ["Setup ID", pc.cyan(profile.id)],
425
+ ["Setup ID", profile.id],
402
426
  ["Runs with", backend.label],
403
- ["Model alias", pc.cyan(profile.modelAlias)],
427
+ ["Model alias", profile.modelAlias],
404
428
  ...(profile.capabilities ? [["Detected", capabilitySummary(profile.capabilities)]] : []),
405
429
  ...(!isManaged ? [
406
- ["Local file", profile.modelPath ?? "unknown"],
407
- ["Vision file", profile.mmprojPath ?? "none"],
430
+ ["Local file", fileMissing ? pc.red(`${profile.modelPath} (not found)`) : profile.modelPath ?? "unknown"],
431
+ ["Vision file", profile.mmprojPath ? (existsSync(profile.mmprojPath) ? profile.mmprojPath : pc.red(`${profile.mmprojPath} (not found)`)) : "none"],
408
432
  ["Model size", profile.modelPath && existsSync(profile.modelPath) ? formatBytes(statSync(profile.modelPath).size) : "unknown"],
409
433
  ] : []),
410
434
  ]), { columns: 110 }));
411
435
 
436
+ if (fileMissing) {
437
+ console.log("\n" + pc.red("⚠ This model's file is no longer on disk. Remove this setup or move the file back."));
438
+ }
439
+
412
440
  if (!isManaged && profile.commandArgv) {
413
441
  const commandArgv = await readCommandArgv(profile);
414
442
  console.log("\n" + renderSection("llama-server command", pc.dim(buildPrettyCommand({ ...profile, commandArgv })), { columns: 120 }));
@@ -633,9 +661,9 @@ async function statusCommand() {
633
661
  if (running.length === 0) {
634
662
  console.log(renderCard("Status", renderRows([
635
663
  ["Running now", pc.dim("none")],
636
- ["Ready setups", profiles.length > 0 ? pc.green(String(profiles.length)) : pc.dim("none")],
637
- ["Next step", profiles.length > 0 ? "Run offgrid-ai to start chatting" : "Run offgrid-ai to set up a model"],
638
- ]), { formatBorder: profiles.length > 0 ? pc.cyan : pc.yellow }));
664
+ ["Ready setups", profiles.length > 0 ? String(profiles.length) : pc.dim("none")],
665
+ ["Next step", profiles.length > 0 ? "Run offgrid-ai to start chatting" : pc.yellow("Run offgrid-ai to set up a model")],
666
+ ]), { formatBorder: pc.dim }));
639
667
  return;
640
668
  }
641
669