offgrid-ai 0.3.15 → 0.3.16
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 +26 -222
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -153,76 +153,15 @@ export async function mainFlow() {
|
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
// 6. Interactive:
|
|
156
|
+
// 6. Interactive: one command center after onboarding.
|
|
157
157
|
startInteractive("offgrid-ai");
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
// Show what we found
|
|
161
|
-
const profiledPaths = new Set(profiles.map((p) => p.modelPath).filter(Boolean));
|
|
162
|
-
const newModels = ggufModels.filter((m) => !profiledPaths.has(m.path));
|
|
163
|
-
|
|
164
|
-
// Managed backend models
|
|
165
|
-
const managedItems = [];
|
|
166
|
-
for (const { backendId, models } of managedModels) {
|
|
167
|
-
const profiledAliases = new Set(
|
|
168
|
-
profiles.filter((p) => p.backend === backendId).map((p) => backendId === "ollama" ? `ollama:${p.ollamaModel ?? p.modelAlias}` : `omlx:${p.omlxModel ?? p.modelAlias}`)
|
|
169
|
-
);
|
|
170
|
-
for (const model of models) {
|
|
171
|
-
if (!profiledAliases.has(`${backendId}:${model.id}`)) {
|
|
172
|
-
managedItems.push({ model, backendId });
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Show what we found
|
|
178
|
-
if (profiles.length > 0) {
|
|
179
|
-
console.log(pc.bold("\nSaved profiles"));
|
|
180
|
-
for (const profile of profiles) {
|
|
181
|
-
const backend = backendFor(profile.backend);
|
|
182
|
-
const colorMap = { "llama-cpp": pc.yellow, "llama-cpp-mtp": pc.blue, "ollama": pc.magenta, "omlx": pc.cyan };
|
|
183
|
-
const running = await isProfileRunning(profile);
|
|
184
|
-
const c = colorMap[profile.backend] ?? pc.magenta;
|
|
185
|
-
console.log(` ${running ? pc.green("●") : pc.dim("○")} ${pc.bold(profile.label)} ${c(`[${backend.label}]`)} · ${pc.cyan(profile.modelAlias)}`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (newModels.length > 0) {
|
|
189
|
-
console.log(pc.bold("\nNew models"));
|
|
190
|
-
for (const model of newModels.slice(0, 10)) {
|
|
191
|
-
console.log(` ${pc.cyan(model.label)} ${pc.dim(model.quant ?? "")} · ${pc.dim(formatBytes(model.sizeBytes))}`);
|
|
192
|
-
}
|
|
193
|
-
if (newModels.length > 10) console.log(pc.dim(` ... and ${newModels.length - 10} more`));
|
|
194
|
-
}
|
|
195
|
-
for (const { backendId, models } of managedModels) {
|
|
196
|
-
if (models.length > 0) {
|
|
197
|
-
const be = BACKENDS[backendId];
|
|
198
|
-
console.log(pc.bold(`\n${be.label} models`));
|
|
199
|
-
for (const model of models.slice(0, 5)) {
|
|
200
|
-
console.log(` ${pc.cyan(model.label)}`);
|
|
201
|
-
}
|
|
202
|
-
if (models.length > 5) console.log(pc.dim(` ... and ${models.length - 5} more`));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Pick what to do
|
|
207
|
-
const action = await prompt.choice("What next?", [
|
|
208
|
-
{ value: "run", label: "Run a model", hint: "Start server and launch Pi" },
|
|
209
|
-
...(profiles.length > 0 ? [{ value: "manage", label: "Manage profiles", hint: "Sync, remove, or inspect" }] : []),
|
|
210
|
-
{ value: "benchmark", label: "Benchmark", hint: "Run a benchmark prompt" },
|
|
211
|
-
], "run");
|
|
212
|
-
|
|
213
|
-
if (action === "run") return await pickAndRun(prompt, profiles, newModels, managedItems);
|
|
214
|
-
if (action === "manage") return await manageProfiles(prompt, profiles);
|
|
215
|
-
if (action === "benchmark") return await benchmarkFlow(prompt, profiles);
|
|
216
|
-
} finally {
|
|
217
|
-
prompt.close();
|
|
218
|
-
}
|
|
158
|
+
return await modelCommandCenter({ profiles, ggufModels, managedModels });
|
|
219
159
|
}
|
|
220
160
|
|
|
221
|
-
// ──
|
|
161
|
+
// ── Model command center ────────────────────────────────────────────────────
|
|
222
162
|
|
|
223
163
|
async function modelsCommand(argv) {
|
|
224
164
|
await ensureDirs();
|
|
225
|
-
if (process.stdin.isTTY) startInteractive("offgrid-ai models");
|
|
226
165
|
const catalog = await loadModelCatalog();
|
|
227
166
|
|
|
228
167
|
if (argv[0]) {
|
|
@@ -231,20 +170,28 @@ async function modelsCommand(argv) {
|
|
|
231
170
|
return;
|
|
232
171
|
}
|
|
233
172
|
|
|
234
|
-
|
|
173
|
+
if (process.stdin.isTTY) startInteractive("offgrid-ai");
|
|
174
|
+
return await modelCommandCenter(catalog);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function modelCommandCenter(catalog) {
|
|
178
|
+
const normalized = normalizeCatalog(catalog);
|
|
179
|
+
await printModelCatalog(normalized);
|
|
235
180
|
if (!process.stdin.isTTY) return;
|
|
236
181
|
|
|
237
|
-
const items = modelCatalogItems(
|
|
182
|
+
const items = modelCatalogItems(normalized);
|
|
238
183
|
if (items.length === 0) return;
|
|
239
184
|
|
|
240
185
|
const prompt = createPrompt();
|
|
241
186
|
try {
|
|
242
|
-
const action = await prompt.choice("
|
|
243
|
-
{ value: "inspect", label: "Inspect", hint: "View
|
|
187
|
+
const action = await prompt.choice("What do you want to do?", [
|
|
188
|
+
{ value: "inspect", label: "Inspect", hint: "View details" },
|
|
244
189
|
{ value: "setup", label: "Set up / sync", hint: "Create profile or sync Pi" },
|
|
245
190
|
{ value: "run", label: "Run", hint: "Start server and launch Pi" },
|
|
191
|
+
{ value: "benchmark", label: "Benchmark", hint: "Coming soon: local benchmark project" },
|
|
246
192
|
{ value: "remove", label: "Remove", hint: "Delete a saved profile" },
|
|
247
|
-
], "
|
|
193
|
+
], "run");
|
|
194
|
+
if (action === "benchmark") return await benchmarkFlow();
|
|
248
195
|
const item = await chooseCatalogItem(prompt, items, action);
|
|
249
196
|
if (!item) return;
|
|
250
197
|
return await handleCatalogAction(prompt, action, item);
|
|
@@ -256,21 +203,9 @@ async function modelsCommand(argv) {
|
|
|
256
203
|
async function runCommand(argv) {
|
|
257
204
|
await ensureDirs();
|
|
258
205
|
const { positional } = parseOptions(argv);
|
|
259
|
-
if (positional[0])
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const catalog = await loadModelCatalog();
|
|
265
|
-
if (!process.stdin.isTTY) throw new Error("Run requires a profile id in non-interactive mode: offgrid-ai run <profile>");
|
|
266
|
-
startInteractive("offgrid-ai run");
|
|
267
|
-
await printModelCatalog(catalog);
|
|
268
|
-
const prompt = createPrompt();
|
|
269
|
-
try {
|
|
270
|
-
return await pickAndRun(prompt, catalog.profiles, catalog.newModels, catalog.managedItems);
|
|
271
|
-
} finally {
|
|
272
|
-
prompt.close();
|
|
273
|
-
}
|
|
206
|
+
if (!positional[0]) return await mainFlow();
|
|
207
|
+
const profile = await readProfile(positional[0]);
|
|
208
|
+
return await runProfile(profile);
|
|
274
209
|
}
|
|
275
210
|
|
|
276
211
|
async function loadModelCatalog() {
|
|
@@ -279,6 +214,12 @@ async function loadModelCatalog() {
|
|
|
279
214
|
scanGgufModels(),
|
|
280
215
|
scanManagedModels(),
|
|
281
216
|
]);
|
|
217
|
+
return normalizeCatalog({ profiles, ggufModels, managedModels });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeCatalog(catalog) {
|
|
221
|
+
if (catalog.newModels && catalog.managedItems) return catalog;
|
|
222
|
+
const { profiles, ggufModels, managedModels } = catalog;
|
|
282
223
|
const profiledPaths = new Set(profiles.map((p) => p.modelPath).filter(Boolean));
|
|
283
224
|
const newModels = ggufModels.filter((m) => !profiledPaths.has(m.path));
|
|
284
225
|
const managedItems = [];
|
|
@@ -442,91 +383,6 @@ function createManagedProfile(model, backendId) {
|
|
|
442
383
|
});
|
|
443
384
|
}
|
|
444
385
|
|
|
445
|
-
// ── Pick and run ────────────────────────────────────────────────────────────
|
|
446
|
-
|
|
447
|
-
async function pickAndRun(prompt, profiles, newModels, managedItems) {
|
|
448
|
-
// If there's exactly one profile and it's already running, offer to connect or start fresh
|
|
449
|
-
const choices = [];
|
|
450
|
-
|
|
451
|
-
// Existing profiles
|
|
452
|
-
for (const profile of profiles) {
|
|
453
|
-
const running = await isProfileRunning(profile);
|
|
454
|
-
const backend = backendFor(profile.backend);
|
|
455
|
-
const colorMap = { "llama-cpp": pc.yellow, "llama-cpp-mtp": pc.blue, "ollama": pc.magenta, "omlx": pc.cyan };
|
|
456
|
-
const c = colorMap[profile.backend] ?? pc.magenta;
|
|
457
|
-
choices.push({
|
|
458
|
-
value: `profile:${profile.id}`,
|
|
459
|
-
label: `${running ? pc.green("● ") : ""}${profile.label}`,
|
|
460
|
-
hint: `${c(backend.label)} · ${profile.modelAlias} · ${profile.baseUrl}`,
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// New GGUF models
|
|
465
|
-
for (const model of newModels.slice(0, 20)) {
|
|
466
|
-
choices.push({
|
|
467
|
-
value: `new:${model.path}`,
|
|
468
|
-
label: model.label,
|
|
469
|
-
hint: `${model.quant ?? "GGUF"} · ${formatBytes(model.sizeBytes)}`,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Managed models
|
|
474
|
-
for (const { model, backendId } of managedItems) {
|
|
475
|
-
const be = BACKENDS[backendId];
|
|
476
|
-
choices.push({
|
|
477
|
-
value: `managed:${backendId}:${model.id}`,
|
|
478
|
-
label: model.label,
|
|
479
|
-
hint: `${be.label}`,
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (choices.length === 0) {
|
|
484
|
-
console.log(pc.yellow("No models available."));
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const selected = await prompt.choice("Pick a model", choices, choices[0].value);
|
|
489
|
-
|
|
490
|
-
if (selected.startsWith("profile:")) {
|
|
491
|
-
const id = selected.slice("profile:".length);
|
|
492
|
-
const profile = await readProfile(id);
|
|
493
|
-
return await runProfile(profile);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (selected.startsWith("new:")) {
|
|
497
|
-
const modelPath = selected.slice("new:".length);
|
|
498
|
-
const model = newModels.find((m) => m.path === modelPath);
|
|
499
|
-
if (!model) throw new Error("Model not found.");
|
|
500
|
-
const profile = await createProfileFromModel(model);
|
|
501
|
-
const configured = await configureLocalProfile(prompt, profile);
|
|
502
|
-
if (!configured) return;
|
|
503
|
-
await saveProfile(configured);
|
|
504
|
-
console.log(pc.green(`Saved profile: ${configured.label}`));
|
|
505
|
-
await syncPiConfig(configured);
|
|
506
|
-
return await runProfile(configured);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (selected.startsWith("managed:")) {
|
|
510
|
-
const managedSelection = selected.slice("managed:".length);
|
|
511
|
-
const separator = managedSelection.indexOf(":");
|
|
512
|
-
const backendId = separator === -1 ? managedSelection : managedSelection.slice(0, separator);
|
|
513
|
-
const modelId = separator === -1 ? "" : managedSelection.slice(separator + 1);
|
|
514
|
-
const model = managedItems.find((m) => m.model.id === modelId && m.backendId === backendId)?.model;
|
|
515
|
-
if (!model) throw new Error("Model not found.");
|
|
516
|
-
const profile = normalizeProfile({
|
|
517
|
-
id: model.id.replace(/[^a-z0-9._-]+/gi, "-").toLowerCase(),
|
|
518
|
-
label: model.label,
|
|
519
|
-
backend: backendId,
|
|
520
|
-
modelAlias: model.aliasSuggestion,
|
|
521
|
-
...(backendId === "ollama" ? { ollamaModel: model.id } : {}),
|
|
522
|
-
...(backendId === "omlx" ? { omlxModel: model.id } : {}),
|
|
523
|
-
});
|
|
524
|
-
await saveProfile(profile);
|
|
525
|
-
await syncPiConfig(profile);
|
|
526
|
-
return await runProfile(profile);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
386
|
async function runProfile(profile, options = {}) {
|
|
531
387
|
const backend = backendFor(profile.backend);
|
|
532
388
|
const withHarness = options.with ?? "pi";
|
|
@@ -608,56 +464,6 @@ async function runProfile(profile, options = {}) {
|
|
|
608
464
|
}
|
|
609
465
|
}
|
|
610
466
|
|
|
611
|
-
// ── Manage profiles ─────────────────────────────────────────────────────────
|
|
612
|
-
|
|
613
|
-
async function manageProfiles(prompt, profiles) {
|
|
614
|
-
const choices = profiles.map((p) => ({
|
|
615
|
-
value: p.id,
|
|
616
|
-
label: p.label,
|
|
617
|
-
hint: `${p.modelAlias} · ${p.baseUrl}`,
|
|
618
|
-
}));
|
|
619
|
-
|
|
620
|
-
const selected = await prompt.choice("Which profile?", choices, choices[0].value);
|
|
621
|
-
const profile = await readProfile(selected);
|
|
622
|
-
const backend = backendFor(profile.backend);
|
|
623
|
-
const isManaged = backend.type === "managed-server";
|
|
624
|
-
const piConfigured = await hasPiModel(profile);
|
|
625
|
-
|
|
626
|
-
// Show profile details
|
|
627
|
-
console.log("");
|
|
628
|
-
console.log(renderSection("Profile", renderRows([
|
|
629
|
-
["ID", pc.cyan(profile.id)],
|
|
630
|
-
["Label", pc.bold(profile.label)],
|
|
631
|
-
["Backend", backend.label],
|
|
632
|
-
["Endpoint", pc.green(profile.baseUrl)],
|
|
633
|
-
...(!isManaged ? [
|
|
634
|
-
["Model", profile.modelPath ?? "unknown"],
|
|
635
|
-
["MMProj", profile.mmprojPath ?? "none"],
|
|
636
|
-
["Memory", existsSync(profile.modelPath) ? formatBytes(statSync(profile.modelPath).size) : "unknown"],
|
|
637
|
-
] : []),
|
|
638
|
-
["Alias", pc.cyan(profile.modelAlias)],
|
|
639
|
-
["Pi", piConfigured ? pc.green("configured") : pc.yellow("not synced")],
|
|
640
|
-
])));
|
|
641
|
-
|
|
642
|
-
if (!isManaged && profile.commandArgv) {
|
|
643
|
-
console.log("");
|
|
644
|
-
console.log(pc.bold("llama-server command"));
|
|
645
|
-
console.log(pc.dim(buildPrettyCommand(profile)));
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const action = await prompt.choice("Action", [
|
|
649
|
-
{ value: "sync", label: piConfigured ? `${pc.green("✓")} Pi config synced` : "Sync Pi config", hint: piConfigured ? "Already in ~/.pi/agent/models.json" : "Update ~/.pi/agent/models.json" },
|
|
650
|
-
{ value: "run", label: "Run", hint: "Start server + Pi" },
|
|
651
|
-
...(isManaged ? [] : [{ value: "server", label: "Server only", hint: "Start server, no harness" }]),
|
|
652
|
-
{ value: "remove", label: "Remove", hint: "Delete profile + Pi config" },
|
|
653
|
-
], "sync");
|
|
654
|
-
|
|
655
|
-
if (action === "sync") return await syncPiConfig(profile);
|
|
656
|
-
if (action === "run") return await runProfile(profile);
|
|
657
|
-
if (action === "server") return await runProfile(profile, { with: "server" });
|
|
658
|
-
if (action === "remove") return await removeProfileInteractive(profile.id);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
467
|
async function removeProfileInteractive(id) {
|
|
662
468
|
const profile = await readProfile(id);
|
|
663
469
|
if (!process.stdin.isTTY) {
|
|
@@ -1207,9 +1013,7 @@ function printHelp() {
|
|
|
1207
1013
|
console.log(`${pc.bold("offgrid-ai")} — privacy-first local LLM runner
|
|
1208
1014
|
|
|
1209
1015
|
Usage:
|
|
1210
|
-
offgrid-ai
|
|
1211
|
-
offgrid-ai models List, inspect, set up, sync, or remove models
|
|
1212
|
-
offgrid-ai run Pick and run a model (or: offgrid-ai run <profile>)
|
|
1016
|
+
offgrid-ai Command center: inspect, set up, run, benchmark, or remove models
|
|
1213
1017
|
offgrid-ai status Show running local models
|
|
1214
1018
|
offgrid-ai stop Stop a running server (or: offgrid-ai stop <id>)
|
|
1215
1019
|
offgrid-ai uninstall Remove offgrid-ai, clean up PATH, optionally keep profiles
|