offgrid-ai 0.5.1 → 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.
- package/package.json +1 -1
- package/src/cli.mjs +92 -22
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -177,7 +177,6 @@ async function modelCommandCenter(initialCatalog) {
|
|
|
177
177
|
const normalized = normalizeCatalog(initialCatalog);
|
|
178
178
|
const allItems = buildCatalogItems(normalized);
|
|
179
179
|
if (allItems.length === 0) return;
|
|
180
|
-
// Non-interactive: just show summary
|
|
181
180
|
for (const item of allItems) console.log(item.label);
|
|
182
181
|
return;
|
|
183
182
|
}
|
|
@@ -185,7 +184,6 @@ async function modelCommandCenter(initialCatalog) {
|
|
|
185
184
|
const prompt = createPrompt();
|
|
186
185
|
try {
|
|
187
186
|
while (true) {
|
|
188
|
-
// Reload catalog each loop to reflect setup/remove changes
|
|
189
187
|
const catalog = await loadModelCatalog();
|
|
190
188
|
const normalized = normalizeCatalog(catalog);
|
|
191
189
|
const allItems = buildCatalogItems(normalized);
|
|
@@ -203,6 +201,9 @@ async function modelCommandCenter(initialCatalog) {
|
|
|
203
201
|
["File missing", fileMissingCount > 0 ? pc.red(`${fileMissingCount} setup${fileMissingCount === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
204
202
|
]), { formatBorder: summaryBorder }));
|
|
205
203
|
|
|
204
|
+
// Show model cards for all items
|
|
205
|
+
printModelCards(allItems, { runningProfilesNow, drafters: normalized.drafters });
|
|
206
|
+
|
|
206
207
|
const options = allItems.map((item) => modelSelectOption(item, { runningProfilesNow, drafters: normalized.drafters }));
|
|
207
208
|
const selected = await prompt.choice("Select a model", options);
|
|
208
209
|
if (!selected) break;
|
|
@@ -210,13 +211,9 @@ async function modelCommandCenter(initialCatalog) {
|
|
|
210
211
|
if (!item) break;
|
|
211
212
|
|
|
212
213
|
const actions = actionsForItem(item);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const action = await prompt.choice(item.label, actions, actions[0].value);
|
|
217
|
-
if (!action) continue;
|
|
218
|
-
await performAction(prompt, action, item);
|
|
219
|
-
}
|
|
214
|
+
const action = await prompt.choice(item.label, actions, actions[0].value);
|
|
215
|
+
if (!action) continue;
|
|
216
|
+
await performAction(prompt, action, item);
|
|
220
217
|
}
|
|
221
218
|
} finally {
|
|
222
219
|
prompt.close();
|
|
@@ -282,24 +279,27 @@ function modelSelectOption(item, { runningProfilesNow }) {
|
|
|
282
279
|
const { profile } = item;
|
|
283
280
|
const running = runningProfilesNow.some((r) => r.id === profile.id);
|
|
284
281
|
const fileMissing = item.fileMissing;
|
|
285
|
-
const caps = profile.capabilities ?? {};
|
|
286
282
|
let status;
|
|
287
|
-
if (fileMissing) status = pc.red("⚠
|
|
288
|
-
else if (running) status = pc.green("●
|
|
289
|
-
else status = pc.dim("●
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (profile.
|
|
293
|
-
|
|
283
|
+
if (fileMissing) status = pc.red("⚠ missing");
|
|
284
|
+
else if (running) status = pc.green("● running");
|
|
285
|
+
else status = pc.dim("● ready");
|
|
286
|
+
// MTP label: enabled vs available vs not available
|
|
287
|
+
let mtpLabel;
|
|
288
|
+
if (profile.drafterPath) mtpLabel = pc.green("MTP");
|
|
289
|
+
else if ((profile.capabilities?.architecture === "gemma4")) mtpLabel = pc.yellow("MTP available");
|
|
290
|
+
const ctxLabel = profile.flags?.ctxSize ? `${(profile.flags.ctxSize / 1000).toFixed(0)}k` : null;
|
|
291
|
+
const label = [profile.label, status, mtpLabel, ctxLabel].filter(Boolean).join(" ");
|
|
292
|
+
return { value: itemKey(item), label, hint: humanCapabilitySummary(profile.capabilities ?? {}) || profile.modelAlias };
|
|
294
293
|
}
|
|
295
294
|
if (item.type === "new") {
|
|
296
295
|
const { model, drafter } = item;
|
|
297
296
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
297
|
+
const mtpAvailable = caps.mtp || Boolean(drafter);
|
|
298
|
+
let mtpLabel;
|
|
299
|
+
if (mtpAvailable) mtpLabel = pc.green("MTP ✓");
|
|
300
|
+
else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP");
|
|
301
|
+
const label = [model.label, pc.yellow("needs setup"), mtpLabel, formatBytes(model.sizeBytes)].filter(Boolean).join(" ");
|
|
302
|
+
return { value: itemKey(item), label, hint: humanCapabilitySummary(caps) || model.quant };
|
|
303
303
|
}
|
|
304
304
|
// managed
|
|
305
305
|
const { model, backendId } = item;
|
|
@@ -339,6 +339,76 @@ function isProfileFileMissing(profile) {
|
|
|
339
339
|
return !existsSync(profile.modelPath);
|
|
340
340
|
}
|
|
341
341
|
|
|
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 }));
|
|
375
|
+
}
|
|
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 }));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
342
412
|
async function performAction(prompt, action, item) {
|
|
343
413
|
if (action === "inspect") {
|
|
344
414
|
if (item.type === "profile") return await printProfileDetails(await readProfile(item.profile.id));
|