offgrid-ai 0.5.0 → 0.5.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.
- package/package.json +1 -1
- package/src/cli.mjs +39 -44
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -172,54 +172,51 @@ async function modelsCommand(argv) {
|
|
|
172
172
|
return await modelCommandCenter(catalog);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
async function modelCommandCenter(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
async function modelCommandCenter(initialCatalog) {
|
|
176
|
+
if (!process.stdin.isTTY) {
|
|
177
|
+
const normalized = normalizeCatalog(initialCatalog);
|
|
178
|
+
const allItems = buildCatalogItems(normalized);
|
|
179
|
+
if (allItems.length === 0) return;
|
|
180
|
+
// Non-interactive: just show summary
|
|
181
|
+
for (const item of allItems) console.log(item.label);
|
|
182
|
+
return;
|
|
181
183
|
}
|
|
182
|
-
const fileMissingCount = profiles.filter((p) => isProfileFileMissing(p)).length;
|
|
183
|
-
|
|
184
|
-
// Summary card
|
|
185
|
-
const summaryBorder = fileMissingCount > 0 ? pc.red : normalized.newModels.length > 0 ? pc.yellow : pc.dim;
|
|
186
|
-
console.log("\n" + renderCard("Your local AI workspace", renderRows([
|
|
187
|
-
["Setups", `${profiles.length} saved`],
|
|
188
|
-
["Need setup", normalized.newModels.length > 0 ? pc.yellow(`${normalized.newModels.length} model${normalized.newModels.length === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
189
|
-
["Running", runningProfilesNow.length > 0 ? pc.green(String(runningProfilesNow.length)) : pc.dim("none")],
|
|
190
|
-
["File missing", fileMissingCount > 0 ? pc.red(`${fileMissingCount} setup${fileMissingCount === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
191
|
-
]), { formatBorder: summaryBorder }));
|
|
192
|
-
|
|
193
|
-
if (!process.stdin.isTTY) return;
|
|
194
|
-
const allItems = buildCatalogItems(normalized);
|
|
195
|
-
if (allItems.length === 0) return;
|
|
196
184
|
|
|
197
185
|
const prompt = createPrompt();
|
|
198
186
|
try {
|
|
199
187
|
while (true) {
|
|
200
|
-
//
|
|
188
|
+
// Reload catalog each loop to reflect setup/remove changes
|
|
189
|
+
const catalog = await loadModelCatalog();
|
|
190
|
+
const normalized = normalizeCatalog(catalog);
|
|
191
|
+
const allItems = buildCatalogItems(normalized);
|
|
192
|
+
if (allItems.length === 0) { console.log(pc.dim("No models found.")); break; }
|
|
193
|
+
const runningProfilesNow = [];
|
|
194
|
+
for (const profile of normalized.profiles) {
|
|
195
|
+
if (await isProfileRunning(profile)) runningProfilesNow.push(profile);
|
|
196
|
+
}
|
|
197
|
+
const fileMissingCount = normalized.profiles.filter((p) => isProfileFileMissing(p)).length;
|
|
198
|
+
const summaryBorder = fileMissingCount > 0 ? pc.red : normalized.newModels.length > 0 ? pc.yellow : pc.dim;
|
|
199
|
+
console.log("\n" + renderCard("Your local AI workspace", renderRows([
|
|
200
|
+
["Setups", `${normalized.profiles.length} saved`],
|
|
201
|
+
["Need setup", normalized.newModels.length > 0 ? pc.yellow(`${normalized.newModels.length} model${normalized.newModels.length === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
202
|
+
["Running", runningProfilesNow.length > 0 ? pc.green(String(runningProfilesNow.length)) : pc.dim("none")],
|
|
203
|
+
["File missing", fileMissingCount > 0 ? pc.red(`${fileMissingCount} setup${fileMissingCount === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
204
|
+
]), { formatBorder: summaryBorder }));
|
|
205
|
+
|
|
201
206
|
const options = allItems.map((item) => modelSelectOption(item, { runningProfilesNow, drafters: normalized.drafters }));
|
|
202
207
|
const selected = await prompt.choice("Select a model", options);
|
|
203
208
|
if (!selected) break;
|
|
204
209
|
const item = allItems.find((i) => itemKey(i) === selected);
|
|
205
210
|
if (!item) break;
|
|
206
211
|
|
|
207
|
-
// Build contextual actions
|
|
208
212
|
const actions = actionsForItem(item);
|
|
209
213
|
if (actions.length === 1) {
|
|
210
|
-
// Only one action — just do it
|
|
211
214
|
await performAction(prompt, actions[0].value, item);
|
|
212
215
|
} else {
|
|
213
216
|
const action = await prompt.choice(item.label, actions, actions[0].value);
|
|
214
217
|
if (!action) continue;
|
|
215
|
-
if (action === "back") continue;
|
|
216
218
|
await performAction(prompt, action, item);
|
|
217
219
|
}
|
|
218
|
-
// After action, refresh running state and loop back
|
|
219
|
-
runningProfilesNow.length = 0;
|
|
220
|
-
for (const profile of profiles) {
|
|
221
|
-
if (await isProfileRunning(profile)) runningProfilesNow.push(profile);
|
|
222
|
-
}
|
|
223
220
|
}
|
|
224
221
|
} finally {
|
|
225
222
|
prompt.close();
|
|
@@ -287,29 +284,27 @@ function modelSelectOption(item, { runningProfilesNow }) {
|
|
|
287
284
|
const fileMissing = item.fileMissing;
|
|
288
285
|
const caps = profile.capabilities ?? {};
|
|
289
286
|
let status;
|
|
290
|
-
if (fileMissing) status = pc.red("File missing");
|
|
291
|
-
else if (running) status = pc.green("Running");
|
|
292
|
-
else status = pc.dim("Ready");
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
if (profile.
|
|
296
|
-
|
|
297
|
-
return { value: itemKey(item), label: profile.label, hint: detailParts.join(pc.dim(" · ")) };
|
|
287
|
+
if (fileMissing) status = pc.red("⚠ File missing");
|
|
288
|
+
else if (running) status = pc.green("● Running");
|
|
289
|
+
else status = pc.dim("● Ready");
|
|
290
|
+
const labelParts = [profile.label, status];
|
|
291
|
+
if (profile.drafterPath) labelParts.push(pc.green("MTP"));
|
|
292
|
+
if (profile.flags?.ctxSize) labelParts.push(`${(profile.flags.ctxSize / 1000).toFixed(0)}k ctx`);
|
|
293
|
+
return { value: itemKey(item), label: labelParts.join(" "), hint: humanCapabilitySummary(caps) || profile.modelAlias };
|
|
298
294
|
}
|
|
299
295
|
if (item.type === "new") {
|
|
300
296
|
const { model, drafter } = item;
|
|
301
297
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
304
|
-
if (caps.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return { value: itemKey(item), label: model.label, hint: detailParts.join(pc.dim(" · ")) };
|
|
298
|
+
const labelParts = [model.label, pc.yellow("Needs setup")];
|
|
299
|
+
if (caps.mtp || drafter) labelParts.push(pc.green("MTP ✓"));
|
|
300
|
+
else if (caps.architecture === "gemma4") labelParts.push(pc.yellow("MTP"));
|
|
301
|
+
labelParts.push(formatBytes(model.sizeBytes));
|
|
302
|
+
return { value: itemKey(item), label: labelParts.join(" "), hint: humanCapabilitySummary(caps) || model.quant };
|
|
308
303
|
}
|
|
309
304
|
// managed
|
|
310
305
|
const { model, backendId } = item;
|
|
311
306
|
const backend = BACKENDS[backendId];
|
|
312
|
-
return { value: itemKey(item), label: model.label
|
|
307
|
+
return { value: itemKey(item), label: `${model.label} ${pc.dim(`via ${backend.label}`)}`, hint: model.id };
|
|
313
308
|
}
|
|
314
309
|
|
|
315
310
|
function actionsForItem(item) {
|