offgrid-ai 0.2.5 → 0.2.7
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/cli.mjs +130 -32
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { homedir, totalmem } from "node:os";
|
|
2
2
|
import { existsSync, statSync, rmSync, readFileSync, appendFileSync, mkdirSync, copyFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { ensureDirs, findLlamaServer, hasHomebrew, DATA_DIR } from "./config.mjs";
|
|
@@ -23,6 +23,7 @@ export async function run(argv) {
|
|
|
23
23
|
if (command === "status") return statusCommand();
|
|
24
24
|
if (command === "stop") return stopCommand(argv.slice(1));
|
|
25
25
|
if (command === "uninstall" || command === "--uninstall") return uninstallCommand(argv.slice(1));
|
|
26
|
+
if (command === "--verbose") return mainFlow(); // verbose flag handled inside onboardFlow
|
|
26
27
|
|
|
27
28
|
throw new Error(`Unknown command: ${command}. Run offgrid-ai help`);
|
|
28
29
|
}
|
|
@@ -488,20 +489,48 @@ async function runningProfiles() {
|
|
|
488
489
|
|
|
489
490
|
// ── Onboarding ──────────────────────────────────────────────────────────────
|
|
490
491
|
|
|
492
|
+
// ── Model recommendations by RAM ───────────────────────────────────────
|
|
493
|
+
// Tier → { lms: [...], ollama: string, label }
|
|
494
|
+
// lms entries are tried in order (first staff-pick match wins, or @quant forces it)
|
|
495
|
+
const MODEL_TIERS = [
|
|
496
|
+
{ maxGB: 8, lms: "google/gemma-4-e2b", ollama: "gemma4:e2b", label: "Gemma 4 E2B (2B effective)" },
|
|
497
|
+
{ maxGB: 16, lms: "google/gemma-4-e4b", ollama: "gemma4:e4b", label: "Gemma 4 E4B (4B effective)" },
|
|
498
|
+
{ maxGB: 32, lms: "qwen/qwen3.5-9b", ollama: "qwen3.5:9b-q4_K_M", label: "Qwen 3.5 9B" },
|
|
499
|
+
{ maxGB: Infinity, lms: "qwen/qwen3.6-35b-a3b", ollama: "qwen3.6:35b-a3b", label: "Qwen 3.6 35B-A3B" },
|
|
500
|
+
];
|
|
501
|
+
|
|
502
|
+
function recommendedModel() {
|
|
503
|
+
const gb = totalmem() / (1024 ** 3);
|
|
504
|
+
const tier = MODEL_TIERS.find(t => gb <= t.maxGB) || MODEL_TIERS[MODEL_TIERS.length - 1];
|
|
505
|
+
return tier;
|
|
506
|
+
}
|
|
507
|
+
|
|
491
508
|
async function onboardFlow() {
|
|
492
509
|
startInteractive("offgrid-ai setup");
|
|
493
510
|
const prompt = createPrompt();
|
|
511
|
+
const verbose = process.argv.includes("--verbose");
|
|
494
512
|
|
|
495
513
|
const { spawn } = await import("node:child_process");
|
|
496
514
|
|
|
497
|
-
/** Run a command
|
|
515
|
+
/** Run a command. Verbose: stream output. Quiet: show only label + result. */
|
|
498
516
|
const run = (cmd, args, label) => new Promise((resolve, reject) => {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
517
|
+
if (verbose) {
|
|
518
|
+
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
519
|
+
child.on("close", (code) => {
|
|
520
|
+
if (code === 0) resolve();
|
|
521
|
+
else reject(new Error(`${label || cmd} exited with code ${code}`));
|
|
522
|
+
});
|
|
523
|
+
child.on("error", (err) => reject(err));
|
|
524
|
+
} else {
|
|
525
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
526
|
+
let stderr = "";
|
|
527
|
+
child.stderr.on("data", (d) => { stderr += d; });
|
|
528
|
+
child.on("close", (code) => {
|
|
529
|
+
if (code === 0) resolve();
|
|
530
|
+
else reject(new Error(stderr.split("\n").filter(l => l.trim()).slice(-3).join("\n") || `${label || cmd} exited with code ${code}`));
|
|
531
|
+
});
|
|
532
|
+
child.on("error", (err) => reject(err));
|
|
533
|
+
}
|
|
505
534
|
});
|
|
506
535
|
try {
|
|
507
536
|
console.log(pc.bold("Welcome to offgrid-ai!"));
|
|
@@ -621,23 +650,37 @@ async function onboardFlow() {
|
|
|
621
650
|
};
|
|
622
651
|
|
|
623
652
|
if (backendChoice === "lmstudio") {
|
|
653
|
+
const model = recommendedModel();
|
|
624
654
|
console.log(pc.cyan("Installing LM Studio via Homebrew..."));
|
|
625
655
|
try {
|
|
626
656
|
await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
|
|
627
657
|
const lmsReady = ensureLmsOnPath();
|
|
628
658
|
console.log(pc.green("✓ LM Studio installed"));
|
|
629
|
-
console.log(pc.yellow("\nDownload your first model:"));
|
|
630
659
|
if (lmsReady) {
|
|
631
|
-
|
|
660
|
+
const download = await prompt.yesNo(`Download a model now? (${model.label})`, true);
|
|
661
|
+
if (download) {
|
|
662
|
+
console.log(pc.cyan(`Downloading ${model.lms} via lms...`));
|
|
663
|
+
try {
|
|
664
|
+
await run("lms", ["get", model.lms, "-y"], `lms get ${model.lms}`);
|
|
665
|
+
console.log(pc.green("✓ Model downloaded."));
|
|
666
|
+
} catch {
|
|
667
|
+
console.log(pc.yellow(`Model download failed. Run manually: lms get ${model.lms}`));
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
console.log(pc.yellow("Download a model later:"));
|
|
671
|
+
console.log(pc.bold(` lms get ${model.lms}`));
|
|
672
|
+
}
|
|
632
673
|
} else {
|
|
633
|
-
console.log(pc.
|
|
674
|
+
console.log(pc.yellow("\nOpen LM Studio once to finish setup, then download a model:"));
|
|
675
|
+
console.log(pc.bold(` lms get ${model.lms}`));
|
|
634
676
|
}
|
|
635
677
|
console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
|
|
636
|
-
} catch
|
|
637
|
-
console.log(pc.red(
|
|
678
|
+
} catch {
|
|
679
|
+
console.log(pc.red("✗ LM Studio installation failed."));
|
|
638
680
|
console.log(pc.dim("Download it manually from https://lmstudio.ai"));
|
|
639
681
|
}
|
|
640
682
|
} else if (backendChoice === "ollama") {
|
|
683
|
+
const model = recommendedModel();
|
|
641
684
|
console.log(pc.cyan("Installing Ollama via Homebrew..."));
|
|
642
685
|
try {
|
|
643
686
|
await run("brew", ["install", "ollama"], "Ollama");
|
|
@@ -648,11 +691,22 @@ async function onboardFlow() {
|
|
|
648
691
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
649
692
|
} catch { /* may already be running */ }
|
|
650
693
|
console.log(pc.green("Ollama is running."));
|
|
651
|
-
|
|
652
|
-
|
|
694
|
+
const download = await prompt.yesNo(`Pull a model now? (${model.label})`, true);
|
|
695
|
+
if (download) {
|
|
696
|
+
console.log(pc.cyan(`Pulling ${model.ollama} via Ollama...`));
|
|
697
|
+
try {
|
|
698
|
+
await run("ollama", ["pull", model.ollama], `ollama pull ${model.ollama}`);
|
|
699
|
+
console.log(pc.green("✓ Model pulled."));
|
|
700
|
+
} catch {
|
|
701
|
+
console.log(pc.yellow(`Model pull failed. Run manually: ollama pull ${model.ollama}`));
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
console.log(pc.yellow("Pull a model later:"));
|
|
705
|
+
console.log(pc.bold(` ollama pull ${model.ollama}`));
|
|
706
|
+
}
|
|
653
707
|
console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
|
|
654
|
-
} catch
|
|
655
|
-
console.log(pc.red(
|
|
708
|
+
} catch {
|
|
709
|
+
console.log(pc.red("✗ Ollama installation failed."));
|
|
656
710
|
console.log(pc.dim("Install it manually from https://ollama.com"));
|
|
657
711
|
}
|
|
658
712
|
} else if (backendChoice === "omlx") {
|
|
@@ -669,21 +723,25 @@ async function onboardFlow() {
|
|
|
669
723
|
console.log(pc.dim("Install manually: brew tap jundot/omlx && brew install omlx"));
|
|
670
724
|
}
|
|
671
725
|
} else if (backendChoice === "all") {
|
|
726
|
+
const model = recommendedModel();
|
|
672
727
|
let installed = [];
|
|
673
728
|
// LM Studio
|
|
674
729
|
console.log(pc.cyan("Installing LM Studio via Homebrew..."));
|
|
730
|
+
let lmsReady = false;
|
|
675
731
|
try {
|
|
676
732
|
await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
|
|
677
|
-
|
|
678
|
-
installed.push(
|
|
733
|
+
lmsReady = ensureLmsOnPath();
|
|
734
|
+
installed.push("LM Studio");
|
|
679
735
|
} catch {
|
|
680
736
|
console.log(pc.yellow("✗ LM Studio installation failed. Download from https://lmstudio.ai"));
|
|
681
737
|
}
|
|
682
738
|
// Ollama
|
|
683
739
|
console.log(pc.cyan("Installing Ollama via Homebrew..."));
|
|
740
|
+
let ollamaInstalled = false;
|
|
684
741
|
try {
|
|
685
742
|
await run("brew", ["install", "ollama"], "Ollama");
|
|
686
743
|
installed.push("Ollama");
|
|
744
|
+
ollamaInstalled = true;
|
|
687
745
|
} catch {
|
|
688
746
|
console.log(pc.yellow("✗ Ollama installation failed. Install manually from https://ollama.com"));
|
|
689
747
|
}
|
|
@@ -696,19 +754,45 @@ async function onboardFlow() {
|
|
|
696
754
|
} catch {
|
|
697
755
|
console.log(pc.yellow("✗ oMLX installation failed. Install manually: brew tap jundot/omlx && brew install omlx"));
|
|
698
756
|
}
|
|
757
|
+
// Auto-download model with best available backend
|
|
699
758
|
if (installed.length > 0) {
|
|
700
759
|
console.log(pc.green(`\n✓ Installed: ${installed.join(", ")}`));
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
760
|
+
if (lmsReady) {
|
|
761
|
+
const download = await prompt.yesNo(`Download a model now? (${model.label})`, true);
|
|
762
|
+
if (download) {
|
|
763
|
+
console.log(pc.cyan(`Downloading ${model.lms} via lms...`));
|
|
764
|
+
try {
|
|
765
|
+
await run("lms", ["get", model.lms, "-y"], `lms get ${model.lms}`);
|
|
766
|
+
console.log(pc.green("✓ Model downloaded."));
|
|
767
|
+
} catch {
|
|
768
|
+
console.log(pc.yellow(`Model download failed. Run manually: lms get ${model.lms}`));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
} else if (ollamaInstalled) {
|
|
772
|
+
// Start Ollama and offer to pull
|
|
773
|
+
console.log(pc.cyan("Starting Ollama..."));
|
|
774
|
+
try { await run("ollama", ["serve"], "Ollama serve"); await new Promise(r => setTimeout(r, 2000)); } catch { /* may already be running */ }
|
|
775
|
+
const download = await prompt.yesNo(`Pull a model now? (${model.label})`, true);
|
|
776
|
+
if (download) {
|
|
777
|
+
console.log(pc.cyan(`Pulling ${model.ollama} via Ollama...`));
|
|
778
|
+
try {
|
|
779
|
+
await run("ollama", ["pull", model.ollama], `ollama pull ${model.ollama}`);
|
|
780
|
+
console.log(pc.green("✓ Model pulled."));
|
|
781
|
+
} catch {
|
|
782
|
+
console.log(pc.yellow(`Model pull failed. Run manually: ollama pull ${model.ollama}`));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
} else {
|
|
786
|
+
console.log(pc.yellow(`Recommended model for your machine (${model.label}):`));
|
|
787
|
+
if (installed.some(i => i.includes("LM Studio"))) {
|
|
788
|
+
console.log(pc.bold(lmsReady ? ` lms get ${model.lms}` : ` Open LM Studio once, then: lms get ${model.lms}`));
|
|
789
|
+
}
|
|
790
|
+
if (installed.includes("Ollama")) {
|
|
791
|
+
console.log(pc.bold(` ollama pull ${model.ollama}`));
|
|
792
|
+
}
|
|
793
|
+
if (installed.includes("oMLX")) {
|
|
794
|
+
console.log(pc.bold(" omlx start"));
|
|
795
|
+
}
|
|
712
796
|
}
|
|
713
797
|
console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
|
|
714
798
|
}
|
|
@@ -794,9 +878,20 @@ async function removeDataDir() {
|
|
|
794
878
|
async function removeSelf() {
|
|
795
879
|
console.log(pc.cyan("\nUninstalling offgrid-ai..."));
|
|
796
880
|
const { spawn: spawnUninstall } = await import("node:child_process");
|
|
881
|
+
const verbose = process.argv.includes("--verbose");
|
|
797
882
|
const runCmd = (cmd, args, label) => new Promise((resolve, reject) => {
|
|
798
|
-
const
|
|
799
|
-
child
|
|
883
|
+
const stdio = verbose ? "inherit" : ["ignore", "pipe", "pipe"];
|
|
884
|
+
const child = spawnUninstall(cmd, args, { stdio });
|
|
885
|
+
if (!verbose) {
|
|
886
|
+
let stderr = "";
|
|
887
|
+
child.stderr?.on("data", (d) => { stderr += d; });
|
|
888
|
+
child.on("close", (code) => {
|
|
889
|
+
if (code === 0) resolve();
|
|
890
|
+
else reject(new Error(stderr.split("\n").filter(l => l.trim()).slice(-3).join("\n") || `${label || cmd} exited with code ${code}`));
|
|
891
|
+
});
|
|
892
|
+
} else {
|
|
893
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${label || cmd} exited with code ${code}`)));
|
|
894
|
+
}
|
|
800
895
|
child.on("error", (err) => reject(err));
|
|
801
896
|
});
|
|
802
897
|
try {
|
|
@@ -847,6 +942,9 @@ Usage:
|
|
|
847
942
|
offgrid-ai help Show this help
|
|
848
943
|
offgrid-ai version Show version
|
|
849
944
|
|
|
945
|
+
Flags:
|
|
946
|
+
--verbose Show install output (brew, lms, ollama, etc.)
|
|
947
|
+
|
|
850
948
|
First run? offgrid-ai walks you through installing everything you need.
|
|
851
949
|
After that, just run it — it finds your models, auto-configures, and launches Pi.`);
|
|
852
950
|
}
|