offgrid-ai 0.2.1 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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/backends.mjs CHANGED
@@ -57,20 +57,6 @@ export function backendFor(backendId) {
57
57
  return backend;
58
58
  }
59
59
 
60
- export function inferBackendId(modelOrProfile) {
61
- const haystack = [
62
- modelOrProfile?.path,
63
- modelOrProfile?.modelPath,
64
- modelOrProfile?.label,
65
- modelOrProfile?.modelAlias,
66
- modelOrProfile?.id,
67
- modelOrProfile?.providerId,
68
- modelOrProfile?.backend,
69
- ].filter(Boolean).join(" ").toLowerCase();
70
- if (haystack.includes("mtp")) return "llama-cpp-mtp";
71
- return "llama-cpp";
72
- }
73
-
74
60
  export async function backendBinaryFor(backendId) {
75
61
  const backend = BACKENDS[backendId ?? "llama-cpp"];
76
62
  if (backend.type === "managed-server") return null;
package/src/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { homedir } from "node:os";
2
- import { existsSync, rmSync } from "node:fs";
2
+ import { existsSync, statSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { ensureDirs, findLlamaServer, hasHomebrew, DATA_DIR } from "./config.mjs";
5
5
  import { scanGgufModels } from "./scan.mjs";
@@ -76,23 +76,11 @@ export async function mainFlow() {
76
76
  return;
77
77
  }
78
78
 
79
- // 4. Interactive: pick an action
79
+ // 6. Interactive: pick an action
80
80
  startInteractive("offgrid-ai");
81
81
  const prompt = createPrompt();
82
82
  try {
83
- // Build items list
84
- const items = [];
85
-
86
- // Existing profiles (quick run)
87
- if (profiles.length > 0) {
88
- for (const profile of profiles) {
89
- const running = await isProfileRunning(profile);
90
- const backend = backendFor(profile.backend);
91
- items.push({ type: "profile", profile, running });
92
- }
93
- }
94
-
95
- // New GGUF models (auto-setup)
83
+ // Show what we found
96
84
  const profiledPaths = new Set(profiles.map((p) => p.modelPath).filter(Boolean));
97
85
  const newModels = ggufModels.filter((m) => !profiledPaths.has(m.path));
98
86
 
@@ -592,6 +580,7 @@ async function onboardFlow() {
592
580
  { value: "ollama", label: "Ollama", hint: "brew install ollama — models download on demand" },
593
581
  { value: "lmstudio", label: "LM Studio", hint: "brew install --cask lm-studio — visual model browser" },
594
582
  { value: "omlx", label: "oMLX", hint: "brew tap jundot/omlx && brew install omlx — Apple Silicon optimized" },
583
+ { value: "all", label: "Install all three", hint: "Ollama + LM Studio + oMLX" },
595
584
  { value: "skip", label: "Skip for now", hint: "I'll set up models myself" },
596
585
  ], "ollama");
597
586
 
@@ -639,6 +628,39 @@ async function onboardFlow() {
639
628
  console.log(pc.red(`Failed to install oMLX: ${err.message}`));
640
629
  console.log(pc.dim("Install it manually: brew tap jundot/omlx && brew install omlx"));
641
630
  }
631
+ } else if (backendChoice === "all") {
632
+ let installed = [];
633
+ // Ollama
634
+ console.log(pc.cyan("Installing Ollama via Homebrew..."));
635
+ try {
636
+ await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
637
+ installed.push("Ollama");
638
+ } catch {
639
+ console.log(pc.yellow("Ollama installation failed. Install manually from https://ollama.com"));
640
+ }
641
+ // LM Studio
642
+ console.log(pc.cyan("Installing LM Studio via Homebrew..."));
643
+ try {
644
+ await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
645
+ installed.push("LM Studio");
646
+ } catch {
647
+ console.log(pc.yellow("LM Studio installation failed. Download from https://lmstudio.ai"));
648
+ }
649
+ // oMLX
650
+ console.log(pc.cyan("Installing oMLX via Homebrew..."));
651
+ try {
652
+ await promisify(execFile)("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], { stdio: "inherit" });
653
+ await promisify(execFile)("brew", ["install", "omlx"], { stdio: "inherit" });
654
+ installed.push("oMLX");
655
+ } catch {
656
+ console.log(pc.yellow("oMLX installation failed. Install manually: brew tap jundot/omlx && brew install omlx"));
657
+ }
658
+ if (installed.length > 0) {
659
+ console.log(pc.green(`\n✓ Installed: ${installed.join(", ")}`));
660
+ console.log(pc.yellow("Next steps:"));
661
+ console.log(pc.bold(" ollama pull gemma3:4b"));
662
+ console.log(pc.dim("Or open LM Studio to browse models, or run: omlx start"));
663
+ }
642
664
  } else {
643
665
  console.log(pc.dim("Run offgrid-ai again when you've set up a model backend."));
644
666
  }
@@ -657,7 +679,7 @@ async function uninstallCommand(argv) {
657
679
  if (!process.stdin.isTTY) {
658
680
  // Non-interactive: remove everything
659
681
  await removeDataDir();
660
- removeSelf();
682
+ await removeSelf();
661
683
  return;
662
684
  }
663
685
 
package/src/ui.mjs CHANGED
@@ -4,20 +4,6 @@ import pc from "picocolors";
4
4
  export { pc };
5
5
  export { pc as colors };
6
6
 
7
- export function printHelp() {
8
- console.log(`${pc.bold("offgrid-ai")} — privacy-first local LLM runner\n`);
9
- console.log("Usage:");
10
- console.log(" offgrid-ai Pick a model and run");
11
- console.log(" offgrid-ai models List profiles and models");
12
- console.log(" offgrid-ai run [id] Run a profile (start server + launch Pi)");
13
- console.log(" offgrid-ai stop [id] Stop a running server");
14
- console.log(" offgrid-ai benchmark Run a benchmark prompt");
15
- console.log("");
16
- console.log(pc.bold("Run modes (--with):"));
17
- console.log(" pi Launch Pi with the selected model (default)");
18
- console.log(" server Start server only, no harness");
19
- }
20
-
21
7
  export function formatBytes(bytes) {
22
8
  if (!Number.isFinite(bytes)) return "unknown";
23
9
  const units = ["B", "KB", "MB", "GB", "TB"];
@@ -63,15 +49,6 @@ function handleCancel(value) {
63
49
  return value;
64
50
  }
65
51
 
66
- export function relativeTime(date) {
67
- const ms = Date.now() - date.getTime();
68
- const abs = Math.abs(ms);
69
- for (const [label, size] of [["day", 86400000], ["hour", 3600000], ["minute", 60000], ["second", 1000]]) {
70
- if (abs >= size) { const v = Math.round(abs / size); return `${v} ${label}${v === 1 ? "" : "s"} ago`; }
71
- }
72
- return "just now";
73
- }
74
-
75
52
  export function renderRows(rows) {
76
53
  const width = Math.max(...rows.map(([key]) => pc.strip(String(key)).length));
77
54
  return rows.map(([key, value]) => {