offgrid-ai 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -59,6 +59,7 @@ curl -fsSL https://raw.githubusercontent.com/eeshansrivastava89/offgrid-ai/main/
59
59
 
60
60
  | Backend | Type | Auto-detected |
61
61
  |---|---|---|
62
+ | **LM Studio** | Visual model browser + CLI (`lms`) | ✓ models in `~/.lmstudio/models/` |
62
63
  | **llama.cpp** | Local server | ✓ GGUF models in `~/.lmstudio/models/` |
63
64
  | **llama.cpp MTP** | Local server (speculative decoding) | ✓ MTP detected from model metadata |
64
65
  | **Ollama** | Managed server | ✓ via `localhost:11434` |
@@ -70,7 +71,10 @@ When you run `offgrid-ai` for the first time on a fresh machine:
70
71
 
71
72
  1. **Homebrew** — Required. Offered to install if missing.
72
73
  2. **llama-server** — Required for GGUF models. Offered to install via Homebrew.
73
- 3. **Model backend** — At least one is needed: LM Studio, Ollama, or oMLX.
74
+ 3. **Model backend** — At least one is needed (LM Studio recommended):
75
+ - **LM Studio** — visual model browser + `lms` CLI, download models with `lms get qwen/qwen3.5-9b`
76
+ - **Ollama** — models download on demand with `ollama pull`
77
+ - **oMLX** — Apple Silicon optimized
74
78
  4. **Models** — If no models found, tells you where to get them.
75
79
 
76
80
  Subsequent runs skip everything that's already installed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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, readFileSync, appendFileSync } 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
 
@@ -589,16 +577,45 @@ async function onboardFlow() {
589
577
  console.log(pc.dim("You need at least one model backend to use offgrid-ai.\n"));
590
578
 
591
579
  const backendChoice = await prompt.choice("Install a model backend?", [
580
+ { value: "lmstudio", label: "LM Studio (recommended)", hint: "brew install --cask lm-studio — visual model browser + CLI" },
592
581
  { value: "ollama", label: "Ollama", hint: "brew install ollama — models download on demand" },
593
- { 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: "LM Studio + Ollama + oMLX" },
595
584
  { value: "skip", label: "Skip for now", hint: "I'll set up models myself" },
596
- ], "ollama");
585
+ ], "lmstudio");
597
586
 
598
587
  const { execFile } = await import("node:child_process");
599
588
  const { promisify } = await import("node:util");
600
589
 
601
- if (backendChoice === "ollama") {
590
+ const ensureLmsOnPath = () => {
591
+ const lmsBin = join(homedir(), ".lmstudio", "bin");
592
+ if (!existsSync(join(lmsBin, "lms"))) return;
593
+ if (process.env.PATH.split(":").includes(lmsBin)) return;
594
+ process.env.PATH = `${lmsBin}:${process.env.PATH}`;
595
+ const profileFiles = [join(homedir(), ".zshrc"), join(homedir(), ".bash_profile")];
596
+ const line = `export PATH="$PATH:$HOME/.lmstudio/bin"`;
597
+ for (const f of profileFiles) {
598
+ if (!existsSync(f)) continue;
599
+ const content = readFileSync(f, "utf8");
600
+ if (content.includes(".lmstudio/bin")) continue;
601
+ appendFileSync(f, `\n${line}\n`);
602
+ }
603
+ };
604
+
605
+ if (backendChoice === "lmstudio") {
606
+ console.log(pc.cyan("Installing LM Studio via Homebrew..."));
607
+ try {
608
+ await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
609
+ ensureLmsOnPath();
610
+ console.log(pc.green("✓ LM Studio installed"));
611
+ console.log(pc.yellow("\nDownload your first model:"));
612
+ console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
613
+ console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
614
+ } catch (err) {
615
+ console.log(pc.red(`Failed to install LM Studio: ${err.message}`));
616
+ console.log(pc.dim("Download it manually from https://lmstudio.ai"));
617
+ }
618
+ } else if (backendChoice === "ollama") {
602
619
  console.log(pc.cyan("Installing Ollama via Homebrew..."));
603
620
  try {
604
621
  await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
@@ -616,16 +633,6 @@ async function onboardFlow() {
616
633
  console.log(pc.red(`Failed to install Ollama: ${err.message}`));
617
634
  console.log(pc.dim("Install it manually from https://ollama.com"));
618
635
  }
619
- } else if (backendChoice === "lmstudio") {
620
- console.log(pc.cyan("Installing LM Studio via Homebrew..."));
621
- try {
622
- await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
623
- console.log(pc.green("✓ LM Studio installed"));
624
- console.log(pc.yellow("\nOpen LM Studio to browse and download models, then run offgrid-ai again."));
625
- } catch (err) {
626
- console.log(pc.red(`Failed to install LM Studio: ${err.message}`));
627
- console.log(pc.dim("Download it manually from https://lmstudio.ai"));
628
- }
629
636
  } else if (backendChoice === "omlx") {
630
637
  console.log(pc.cyan("Installing oMLX via Homebrew..."));
631
638
  try {
@@ -639,6 +646,48 @@ async function onboardFlow() {
639
646
  console.log(pc.red(`Failed to install oMLX: ${err.message}`));
640
647
  console.log(pc.dim("Install it manually: brew tap jundot/omlx && brew install omlx"));
641
648
  }
649
+ } else if (backendChoice === "all") {
650
+ let installed = [];
651
+ // LM Studio
652
+ console.log(pc.cyan("Installing LM Studio via Homebrew..."));
653
+ try {
654
+ await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
655
+ ensureLmsOnPath();
656
+ installed.push("LM Studio");
657
+ } catch {
658
+ console.log(pc.yellow("LM Studio installation failed. Download from https://lmstudio.ai"));
659
+ }
660
+ // Ollama
661
+ console.log(pc.cyan("Installing Ollama via Homebrew..."));
662
+ try {
663
+ await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
664
+ installed.push("Ollama");
665
+ } catch {
666
+ console.log(pc.yellow("Ollama installation failed. Install manually from https://ollama.com"));
667
+ }
668
+ // oMLX
669
+ console.log(pc.cyan("Installing oMLX via Homebrew..."));
670
+ try {
671
+ await promisify(execFile)("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], { stdio: "inherit" });
672
+ await promisify(execFile)("brew", ["install", "omlx"], { stdio: "inherit" });
673
+ installed.push("oMLX");
674
+ } catch {
675
+ console.log(pc.yellow("oMLX installation failed. Install manually: brew tap jundot/omlx && brew install omlx"));
676
+ }
677
+ if (installed.length > 0) {
678
+ console.log(pc.green(`\n✓ Installed: ${installed.join(", ")}`));
679
+ console.log(pc.yellow("Next steps — download your first model:"));
680
+ if (installed.includes("LM Studio")) {
681
+ console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
682
+ }
683
+ if (installed.includes("Ollama")) {
684
+ console.log(pc.bold(" ollama pull gemma3:4b"));
685
+ }
686
+ if (installed.includes("oMLX")) {
687
+ console.log(pc.bold(" omlx start"));
688
+ }
689
+ console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
690
+ }
642
691
  } else {
643
692
  console.log(pc.dim("Run offgrid-ai again when you've set up a model backend."));
644
693
  }
@@ -657,7 +706,7 @@ async function uninstallCommand(argv) {
657
706
  if (!process.stdin.isTTY) {
658
707
  // Non-interactive: remove everything
659
708
  await removeDataDir();
660
- removeSelf();
709
+ await removeSelf();
661
710
  return;
662
711
  }
663
712
 
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]) => {