offgrid-ai 0.2.0 → 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.0",
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
 
@@ -589,23 +577,24 @@ 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?", [
592
- { value: "ollama", label: "Ollama", hint: "Easiest way — models download on demand" },
593
- { value: "lmstudio", label: "LM Studio", hint: "Visual model browser (opens download page)" },
594
- { value: "omlx", label: "oMLX", hint: "Apple Silicon optimized" },
580
+ { value: "ollama", label: "Ollama", hint: "brew install ollama — models download on demand" },
581
+ { value: "lmstudio", label: "LM Studio", hint: "brew install --cask lm-studio visual model browser" },
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
 
587
+ const { execFile } = await import("node:child_process");
588
+ const { promisify } = await import("node:util");
589
+
598
590
  if (backendChoice === "ollama") {
599
591
  console.log(pc.cyan("Installing Ollama via Homebrew..."));
600
- const { execFile } = await import("node:child_process");
601
- const { promisify } = await import("node:util");
602
592
  try {
603
593
  await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
604
594
  console.log(pc.green("✓ Ollama installed"));
605
595
  console.log(pc.cyan("\nStarting Ollama..."));
606
596
  try {
607
597
  await promisify(execFile)("ollama", ["serve"], { stdio: "ignore", detached: true });
608
- // Give it a moment to start
609
598
  await new Promise((resolve) => setTimeout(resolve, 2000));
610
599
  } catch { /* may already be running */ }
611
600
  console.log(pc.green("Ollama is running."));
@@ -617,22 +606,60 @@ async function onboardFlow() {
617
606
  console.log(pc.dim("Install it manually from https://ollama.com"));
618
607
  }
619
608
  } else if (backendChoice === "lmstudio") {
620
- console.log(pc.cyan("LM Studio needs to be installed manually."));
621
- console.log(pc.bold("\n Download LM Studio: https://lmstudio.ai"));
622
- console.log(pc.dim("Then browse and download models inside LM Studio, and run offgrid-ai again."));
609
+ console.log(pc.cyan("Installing LM Studio via Homebrew..."));
610
+ try {
611
+ await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
612
+ console.log(pc.green("✓ LM Studio installed"));
613
+ console.log(pc.yellow("\nOpen LM Studio to browse and download models, then run offgrid-ai again."));
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
+ }
623
618
  } else if (backendChoice === "omlx") {
624
- console.log(pc.cyan("Installing oMLX via pip..."));
625
- const { execFile } = await import("node:child_process");
626
- const { promisify } = await import("node:util");
619
+ console.log(pc.cyan("Installing oMLX via Homebrew..."));
627
620
  try {
628
- await promisify(execFile)("pip3", ["install", "omlx"], { stdio: "inherit" });
621
+ await promisify(execFile)("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], { stdio: "inherit" });
622
+ await promisify(execFile)("brew", ["install", "omlx"], { stdio: "inherit" });
629
623
  console.log(pc.green("✓ oMLX installed"));
630
624
  console.log(pc.yellow("\nStart oMLX server:"));
631
- console.log(pc.bold(" omlx serve"));
625
+ console.log(pc.bold(" omlx start"));
632
626
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
633
627
  } catch (err) {
634
628
  console.log(pc.red(`Failed to install oMLX: ${err.message}`));
635
- console.log(pc.dim("Install it manually: pip3 install omlx"));
629
+ console.log(pc.dim("Install it manually: brew tap jundot/omlx && brew install omlx"));
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"));
636
663
  }
637
664
  } else {
638
665
  console.log(pc.dim("Run offgrid-ai again when you've set up a model backend."));
@@ -652,7 +679,7 @@ async function uninstallCommand(argv) {
652
679
  if (!process.stdin.isTTY) {
653
680
  // Non-interactive: remove everything
654
681
  await removeDataDir();
655
- removeSelf();
682
+ await removeSelf();
656
683
  return;
657
684
  }
658
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]) => {