offgrid-ai 0.3.12 → 0.3.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
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
@@ -13,6 +13,7 @@ import { pc, formatBytes, renderRows, renderSection, startInteractive, createPro
13
13
  import { checkForUpdate, currentPackageVersion, detectInvocation, updateCommand, runUpdateCommand } from "./updates.mjs";
14
14
  import { removeInstallerPathEntries } from "./shell-path.mjs";
15
15
  import { configureLocalProfile } from "./profile-setup.mjs";
16
+ import { buildPrettyCommand } from "./command.mjs";
16
17
 
17
18
  // ── Entry point ────────────────────────────────────────────────────────────
18
19
 
@@ -201,15 +202,21 @@ export async function mainFlow() {
201
202
  }
202
203
 
203
204
  // Pick what to do
204
- const action = await prompt.choice("What next?", [
205
- { value: "run", label: "Run a model", hint: "Start server and launch Pi" },
206
- ...(profiles.length > 0 ? [{ value: "manage", label: "Manage profiles", hint: "Sync, remove, or inspect" }] : []),
207
- { value: "benchmark", label: "Benchmark", hint: "Run a benchmark prompt" },
208
- ], "run");
209
-
210
- if (action === "run") return await pickAndRun(prompt, profiles, newModels, managedItems);
211
- if (action === "manage") return await manageProfiles(prompt, profiles);
212
- if (action === "benchmark") return await benchmarkFlow(prompt, profiles);
205
+ while (true) {
206
+ const action = await prompt.choice("What next?", [
207
+ { value: "run", label: "Run a model", hint: "Start server and launch Pi" },
208
+ ...(profiles.length > 0 ? [{ value: "manage", label: "Manage profiles", hint: "Sync, remove, or inspect" }] : []),
209
+ { value: "benchmark", label: "Benchmark", hint: "Run a benchmark prompt" },
210
+ ], "run");
211
+
212
+ if (action === "run") return await pickAndRun(prompt, profiles, newModels, managedItems);
213
+ if (action === "manage") {
214
+ const result = await manageProfiles(prompt, profiles);
215
+ if (result === "back") continue;
216
+ return result;
217
+ }
218
+ if (action === "benchmark") return await benchmarkFlow(prompt, profiles);
219
+ }
213
220
  } finally {
214
221
  prompt.close();
215
222
  }
@@ -384,58 +391,63 @@ async function runProfile(profile, options = {}) {
384
391
  // ── Manage profiles ─────────────────────────────────────────────────────────
385
392
 
386
393
  async function manageProfiles(prompt, profiles) {
387
- const choices = profiles.map((p) => ({
388
- value: p.id,
389
- label: p.label,
390
- hint: `${p.modelAlias} · ${p.baseUrl}`,
391
- }));
392
- choices.push({ value: "__back", label: "← Back" });
394
+ while (true) {
395
+ const choices = profiles.map((p) => ({
396
+ value: p.id,
397
+ label: p.label,
398
+ hint: `${p.modelAlias} · ${p.baseUrl}`,
399
+ }));
400
+ choices.push({ value: "__back", label: "← Back" });
393
401
 
394
- const selected = await prompt.choice("Which profile?", choices, choices[0].value);
395
- if (selected === "__back") return;
402
+ const selected = await prompt.choice("Which profile?", choices, choices[0].value);
403
+ if (selected === "__back") return "back";
396
404
 
397
- const profile = await readProfile(selected);
398
- const backend = backendFor(profile.backend);
399
- const isManaged = backend.type === "managed-server";
405
+ const profile = await readProfile(selected);
406
+ const backend = backendFor(profile.backend);
407
+ const isManaged = backend.type === "managed-server";
408
+ const piConfigured = await hasPiModel(profile);
400
409
 
401
- // Show profile details
402
- console.log("");
403
- console.log(renderSection("Profile", renderRows([
404
- ["ID", pc.cyan(profile.id)],
405
- ["Label", pc.bold(profile.label)],
406
- ["Backend", backend.label],
407
- ["Endpoint", pc.green(profile.baseUrl)],
408
- ...(!isManaged ? [
409
- ["Model", profile.modelPath ?? "unknown"],
410
- ["MMProj", profile.mmprojPath ?? "none"],
411
- ["Memory", existsSync(profile.modelPath) ? formatBytes(statSync(profile.modelPath).size) : "unknown"],
412
- ] : []),
413
- ["Alias", pc.cyan(profile.modelAlias)],
414
- ["Pi", (await hasPiModel(profile)) ? pc.green("configured") : pc.yellow("not synced")],
415
- ])));
416
-
417
- if (!isManaged && profile.commandArgv) {
410
+ // Show profile details
418
411
  console.log("");
419
- console.log(pc.bold("Auto-detected flags"));
420
- console.log(pc.dim(profile.commandArgv.join(" ")));
421
- }
422
-
423
- const action = await prompt.choice("Action", [
424
- { value: "sync", label: "Sync Pi config", hint: "Update ~/.pi/agent/models.json" },
425
- { value: "run", label: "Run", hint: "Start server + Pi" },
426
- ...(isManaged ? [] : [{ value: "server", label: "Server only", hint: "Start server, no harness" }]),
427
- { value: "remove", label: "Remove", hint: "Delete profile + Pi config" },
428
- { value: "__back", label: "← Back" },
429
- ], "sync");
412
+ console.log(renderSection("Profile", renderRows([
413
+ ["ID", pc.cyan(profile.id)],
414
+ ["Label", pc.bold(profile.label)],
415
+ ["Backend", backend.label],
416
+ ["Endpoint", pc.green(profile.baseUrl)],
417
+ ...(!isManaged ? [
418
+ ["Model", profile.modelPath ?? "unknown"],
419
+ ["MMProj", profile.mmprojPath ?? "none"],
420
+ ["Memory", existsSync(profile.modelPath) ? formatBytes(statSync(profile.modelPath).size) : "unknown"],
421
+ ] : []),
422
+ ["Alias", pc.cyan(profile.modelAlias)],
423
+ ["Pi", piConfigured ? pc.green("configured") : pc.yellow("not synced")],
424
+ ])));
425
+
426
+ if (!isManaged && profile.commandArgv) {
427
+ console.log("");
428
+ console.log(pc.bold("llama-server command"));
429
+ console.log(pc.dim(buildPrettyCommand(profile)));
430
+ }
430
431
 
431
- if (action === "sync") {
432
- await syncPiConfig(profile);
433
- } else if (action === "run") {
434
- return await runProfile(profile);
435
- } else if (action === "server") {
436
- return await runProfile(profile, { with: "server" });
437
- } else if (action === "remove") {
438
- await removeProfileInteractive(profile.id);
432
+ const action = await prompt.choice("Action", [
433
+ { 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" },
434
+ { value: "run", label: "Run", hint: "Start server + Pi" },
435
+ ...(isManaged ? [] : [{ value: "server", label: "Server only", hint: "Start server, no harness" }]),
436
+ { value: "remove", label: "Remove", hint: "Delete profile + Pi config" },
437
+ { value: "__back", label: "← Back", hint: "Choose another profile" },
438
+ ], "sync");
439
+
440
+ if (action === "__back") continue;
441
+ if (action === "sync") {
442
+ await syncPiConfig(profile);
443
+ continue;
444
+ }
445
+ if (action === "run") return await runProfile(profile);
446
+ if (action === "server") return await runProfile(profile, { with: "server" });
447
+ if (action === "remove") {
448
+ await removeProfileInteractive(profile.id);
449
+ return;
450
+ }
439
451
  }
440
452
  }
441
453
 
@@ -0,0 +1,21 @@
1
+ export function buildPrettyCommand(profile, binary = "llama-server") {
2
+ const argv = profile.commandArgv ?? [];
3
+ const lines = [`${quoteShell(binary)} \\`];
4
+ for (let i = 0; i < argv.length; i++) {
5
+ const arg = argv[i];
6
+ const next = argv[i + 1];
7
+ const hasValue = arg.startsWith("--") && next && !next.startsWith("--");
8
+ if (hasValue) {
9
+ lines.push(` ${arg} ${quoteShell(next)}${i + 2 < argv.length ? " \\" : ""}`);
10
+ i += 1;
11
+ } else {
12
+ lines.push(` ${arg}${i + 1 < argv.length ? " \\" : ""}`);
13
+ }
14
+ }
15
+ return lines.join("\n");
16
+ }
17
+
18
+ export function quoteShell(value) {
19
+ const text = String(value);
20
+ return /^[A-Za-z0-9_/@%+=:,.-]+$/u.test(text) ? text : `'${text.replace(/'/gu, `'"'"'`)}'`;
21
+ }