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 +5 -1
- package/package.json +1 -1
- package/src/backends.mjs +0 -14
- package/src/cli.mjs +78 -29
- package/src/ui.mjs +0 -23
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
|
|
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
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
|
-
//
|
|
79
|
+
// 6. Interactive: pick an action
|
|
80
80
|
startInteractive("offgrid-ai");
|
|
81
81
|
const prompt = createPrompt();
|
|
82
82
|
try {
|
|
83
|
-
//
|
|
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
|
-
], "
|
|
585
|
+
], "lmstudio");
|
|
597
586
|
|
|
598
587
|
const { execFile } = await import("node:child_process");
|
|
599
588
|
const { promisify } = await import("node:util");
|
|
600
589
|
|
|
601
|
-
|
|
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]) => {
|