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.
- package/package.json +1 -1
- package/src/cli.mjs +58 -30
package/package.json
CHANGED
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,
|
|
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
|
-
["
|
|
246
|
-
["Need setup", newModels.length > 0 ? pc.yellow(`${newModels.length}
|
|
247
|
-
["Running
|
|
248
|
-
["
|
|
249
|
-
|
|
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("
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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:
|
|
317
|
+
]), { formatBorder: border });
|
|
298
318
|
}
|
|
299
319
|
|
|
300
320
|
function downloadedModelCard(num, model, caps) {
|
|
301
321
|
return renderCard(`${num}. ${model.label}`, renderRows([
|
|
302
|
-
["Status",
|
|
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",
|
|
330
|
+
["Status", pc.dim(`Via ${backend.label}`)],
|
|
311
331
|
["Runs with", backend.label],
|
|
312
|
-
["Model ID",
|
|
332
|
+
["Model ID", model.id],
|
|
313
333
|
...(model.quant ? [["Size/type", model.quant]] : []),
|
|
314
|
-
]), { formatBorder: pc.
|
|
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",
|
|
395
|
-
["Good for", humanCapabilitySummary(profile.capabilities ?? {})],
|
|
396
|
-
["Pi", piConfigured ? pc.
|
|
397
|
-
["Server", pc.
|
|
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",
|
|
425
|
+
["Setup ID", profile.id],
|
|
402
426
|
["Runs with", backend.label],
|
|
403
|
-
["Model alias",
|
|
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
|
|
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 ?
|
|
637
|
-
["Next step", profiles.length > 0 ? "Run offgrid-ai to start chatting" : "Run offgrid-ai to set up a model"],
|
|
638
|
-
]), { formatBorder:
|
|
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
|
|