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 +1 -1
- package/src/backends.mjs +0 -14
- package/src/cli.mjs +58 -31
- package/src/ui.mjs +0 -23
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 } 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,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: "
|
|
593
|
-
{ value: "lmstudio", label: "LM Studio", hint: "
|
|
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
|
|
621
|
-
|
|
622
|
-
|
|
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
|
|
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)("
|
|
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
|
|
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:
|
|
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]) => {
|