offgrid-ai 0.5.1 → 0.5.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 +85 -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);
|
|
@@ -209,14 +207,14 @@ async function modelCommandCenter(initialCatalog) {
|
|
|
209
207
|
const item = allItems.find((i) => itemKey(i) === selected);
|
|
210
208
|
if (!item) break;
|
|
211
209
|
|
|
210
|
+
// Show detail card before action menu
|
|
211
|
+
console.log("");
|
|
212
|
+
printItemCard(item, { runningProfilesNow, drafters: normalized.drafters });
|
|
213
|
+
|
|
212
214
|
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
|
-
}
|
|
215
|
+
const action = await prompt.choice(item.label, actions, actions[0].value);
|
|
216
|
+
if (!action) continue;
|
|
217
|
+
await performAction(prompt, action, item);
|
|
220
218
|
}
|
|
221
219
|
} finally {
|
|
222
220
|
prompt.close();
|
|
@@ -282,24 +280,27 @@ function modelSelectOption(item, { runningProfilesNow }) {
|
|
|
282
280
|
const { profile } = item;
|
|
283
281
|
const running = runningProfilesNow.some((r) => r.id === profile.id);
|
|
284
282
|
const fileMissing = item.fileMissing;
|
|
285
|
-
const caps = profile.capabilities ?? {};
|
|
286
283
|
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
|
-
|
|
284
|
+
if (fileMissing) status = pc.red("⚠ missing");
|
|
285
|
+
else if (running) status = pc.green("● running");
|
|
286
|
+
else status = pc.dim("● ready");
|
|
287
|
+
// MTP label: enabled vs available vs not available
|
|
288
|
+
let mtpLabel;
|
|
289
|
+
if (profile.drafterPath) mtpLabel = pc.green("MTP");
|
|
290
|
+
else if ((profile.capabilities?.architecture === "gemma4")) mtpLabel = pc.yellow("MTP available");
|
|
291
|
+
const ctxLabel = profile.flags?.ctxSize ? `${(profile.flags.ctxSize / 1000).toFixed(0)}k` : null;
|
|
292
|
+
const label = [profile.label, status, mtpLabel, ctxLabel].filter(Boolean).join(" ");
|
|
293
|
+
return { value: itemKey(item), label, hint: humanCapabilitySummary(profile.capabilities ?? {}) || profile.modelAlias };
|
|
294
294
|
}
|
|
295
295
|
if (item.type === "new") {
|
|
296
296
|
const { model, drafter } = item;
|
|
297
297
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
298
|
+
const mtpAvailable = caps.mtp || Boolean(drafter);
|
|
299
|
+
let mtpLabel;
|
|
300
|
+
if (mtpAvailable) mtpLabel = pc.green("MTP ✓");
|
|
301
|
+
else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP");
|
|
302
|
+
const label = [model.label, pc.yellow("needs setup"), mtpLabel, formatBytes(model.sizeBytes)].filter(Boolean).join(" ");
|
|
303
|
+
return { value: itemKey(item), label, hint: humanCapabilitySummary(caps) || model.quant };
|
|
303
304
|
}
|
|
304
305
|
// managed
|
|
305
306
|
const { model, backendId } = item;
|
|
@@ -339,6 +340,68 @@ function isProfileFileMissing(profile) {
|
|
|
339
340
|
return !existsSync(profile.modelPath);
|
|
340
341
|
}
|
|
341
342
|
|
|
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");
|
|
364
|
+
}
|
|
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
|
+
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
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
342
405
|
async function performAction(prompt, action, item) {
|
|
343
406
|
if (action === "inspect") {
|
|
344
407
|
if (item.type === "profile") return await printProfileDetails(await readProfile(item.profile.id));
|