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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.mjs +26 -222
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "description": "Privacy-first CLI for running local LLMs — discover, configure, run, benchmark",
5
5
  "author": "Eeshan Srivastava (https://eeshans.com)",
6
6
  "type": "module",
package/src/cli.mjs CHANGED
@@ -153,76 +153,15 @@ export async function mainFlow() {
153
153
  return;
154
154
  }
155
155
 
156
- // 6. Interactive: pick an action
156
+ // 6. Interactive: one command center after onboarding.
157
157
  startInteractive("offgrid-ai");
158
- const prompt = createPrompt();
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
- // ── Explicit model/run commands ─────────────────────────────────────────────
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
- await printModelCatalog(catalog);
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(catalog);
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("Action", [
243
- { value: "inspect", label: "Inspect", hint: "View profile/model details" },
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
- ], "inspect");
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
- const profile = await readProfile(positional[0]);
261
- return await runProfile(profile);
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 Friendly shortcut: pick a model and run it
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