offgrid-ai 0.2.7 → 0.2.9
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 +62 -118
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir, totalmem } from "node:os";
|
|
2
|
-
import { existsSync, statSync, rmSync
|
|
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";
|
|
@@ -60,14 +60,40 @@ export async function mainFlow() {
|
|
|
60
60
|
// Fall through — they can still use managed backends
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// 4. No models found at all (but backends exist)
|
|
63
|
+
// 4. No models found at all (but backends may exist)
|
|
64
64
|
if (!hasAnyModels && profiles.length === 0) {
|
|
65
65
|
if (!process.stdin.isTTY) {
|
|
66
|
-
throw new Error("No models found. Download
|
|
66
|
+
throw new Error("No models found. Download a model, then run offgrid-ai.");
|
|
67
67
|
}
|
|
68
68
|
console.log(pc.yellow("No models found."));
|
|
69
|
-
console.log(pc.dim("
|
|
70
|
-
|
|
69
|
+
console.log(pc.dim("You need to download a model to use offgrid-ai.\n"));
|
|
70
|
+
// Detect which backends are installed
|
|
71
|
+
const ollamaInstalled = await hasOllamaInstalled();
|
|
72
|
+
const omlxInstalled = await hasOmlxInstalled();
|
|
73
|
+
const lmStudioInstalled = existsSync("/Applications/LM Studio.app");
|
|
74
|
+
const hasBackends = llamaBinary || ollamaInstalled || omlxInstalled || lmStudioInstalled;
|
|
75
|
+
if (hasBackends) {
|
|
76
|
+
console.log(pc.bold("Backend status:"));
|
|
77
|
+
console.log(` ${lmStudioInstalled ? pc.green("✓") : pc.red("✗")} LM Studio ${lmStudioInstalled ? "— installed" : "— not installed"}`);
|
|
78
|
+
console.log(` ${ollamaInstalled ? pc.green("✓") : pc.red("✗")} Ollama ${ollamaInstalled ? "— installed" : "— not installed"}`);
|
|
79
|
+
console.log(` ${omlxInstalled ? pc.green("✓") : pc.red("✗")} oMLX ${omlxInstalled ? "— installed" : "— not installed"}`);
|
|
80
|
+
console.log(` ${llamaBinary ? pc.green("✓") : pc.red("✗")} llama-server ${llamaBinary ? "— installed" : "— not installed"}`);
|
|
81
|
+
console.log();
|
|
82
|
+
const model = recommendedModel();
|
|
83
|
+
console.log(pc.bold("Next step — download a model:"));
|
|
84
|
+
if (lmStudioInstalled) {
|
|
85
|
+
console.log(" Open LM Studio → browse models → download");
|
|
86
|
+
console.log(pc.dim(` Recommended: ${model.label}`));
|
|
87
|
+
}
|
|
88
|
+
if (ollamaInstalled) {
|
|
89
|
+
console.log(pc.bold(` ollama pull ${model.ollama}`));
|
|
90
|
+
}
|
|
91
|
+
if (omlxInstalled) {
|
|
92
|
+
console.log(pc.bold(" omlx start"));
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
console.log(pc.dim("Run offgrid-ai to install a backend and download a model."));
|
|
96
|
+
}
|
|
71
97
|
return;
|
|
72
98
|
}
|
|
73
99
|
|
|
@@ -622,88 +648,28 @@ async function onboardFlow() {
|
|
|
622
648
|
{ value: "skip", label: "Skip for now", hint: "I'll set up models myself" },
|
|
623
649
|
], "lmstudio");
|
|
624
650
|
|
|
625
|
-
const
|
|
626
|
-
const lmsBin = join(homedir(), ".lmstudio", "bin");
|
|
627
|
-
|
|
628
|
-
const ensureLmsOnPath = () => {
|
|
629
|
-
const lmsDest = join(lmsBin, "lms");
|
|
630
|
-
// Bootstrap lms from app bundle if not yet in ~/.lmstudio/bin
|
|
631
|
-
if (!existsSync(lmsDest) && existsSync(lmsAppBundle)) {
|
|
632
|
-
mkdirSync(lmsBin, { recursive: true });
|
|
633
|
-
copyFileSync(lmsAppBundle, lmsDest);
|
|
634
|
-
}
|
|
635
|
-
if (!existsSync(lmsDest)) {
|
|
636
|
-
console.log(pc.yellow(" Note: lms CLI will be available after opening LM Studio once."));
|
|
637
|
-
return false;
|
|
638
|
-
}
|
|
639
|
-
if (process.env.PATH.split(":").includes(lmsBin)) return true;
|
|
640
|
-
process.env.PATH = `${lmsBin}:${process.env.PATH}`;
|
|
641
|
-
const profileFiles = [join(homedir(), ".zshrc"), join(homedir(), ".bash_profile")];
|
|
642
|
-
const line = `export PATH="$PATH:$HOME/.lmstudio/bin"`;
|
|
643
|
-
for (const f of profileFiles) {
|
|
644
|
-
if (!existsSync(f)) continue;
|
|
645
|
-
const content = readFileSync(f, "utf8");
|
|
646
|
-
if (content.includes(".lmstudio/bin")) continue;
|
|
647
|
-
appendFileSync(f, `\n${line}\n`);
|
|
648
|
-
}
|
|
649
|
-
return true;
|
|
650
|
-
};
|
|
651
|
+
const model = recommendedModel();
|
|
651
652
|
|
|
652
653
|
if (backendChoice === "lmstudio") {
|
|
653
|
-
const model = recommendedModel();
|
|
654
654
|
console.log(pc.cyan("Installing LM Studio via Homebrew..."));
|
|
655
655
|
try {
|
|
656
656
|
await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
|
|
657
|
-
const lmsReady = ensureLmsOnPath();
|
|
658
657
|
console.log(pc.green("✓ LM Studio installed"));
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
}
|
|
673
|
-
} else {
|
|
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}`));
|
|
676
|
-
}
|
|
658
|
+
console.log(pc.yellow("\nOpen LM Studio and download a model to get started."));
|
|
659
|
+
console.log(pc.dim(`Recommended for your machine: ${model.label}`));
|
|
677
660
|
console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
|
|
678
661
|
} catch {
|
|
679
662
|
console.log(pc.red("✗ LM Studio installation failed."));
|
|
680
663
|
console.log(pc.dim("Download it manually from https://lmstudio.ai"));
|
|
681
664
|
}
|
|
682
665
|
} else if (backendChoice === "ollama") {
|
|
683
|
-
const model = recommendedModel();
|
|
684
666
|
console.log(pc.cyan("Installing Ollama via Homebrew..."));
|
|
685
667
|
try {
|
|
686
668
|
await run("brew", ["install", "ollama"], "Ollama");
|
|
687
669
|
console.log(pc.green("✓ Ollama installed"));
|
|
688
|
-
console.log(pc.
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
692
|
-
} catch { /* may already be running */ }
|
|
693
|
-
console.log(pc.green("Ollama is running."));
|
|
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
|
-
}
|
|
670
|
+
console.log(pc.yellow("\nStart Ollama and pull a model:"));
|
|
671
|
+
console.log(pc.bold(` ollama serve \u0026 ollama pull ${model.ollama}`));
|
|
672
|
+
console.log(pc.dim(`Recommended for your machine: ${model.label}`));
|
|
707
673
|
console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
|
|
708
674
|
} catch {
|
|
709
675
|
console.log(pc.red("✗ Ollama installation failed."));
|
|
@@ -715,33 +681,29 @@ async function onboardFlow() {
|
|
|
715
681
|
await run("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], "oMLX tap");
|
|
716
682
|
await run("brew", ["install", "omlx"], "oMLX");
|
|
717
683
|
console.log(pc.green("✓ oMLX installed"));
|
|
718
|
-
console.log(pc.yellow("\nStart oMLX
|
|
684
|
+
console.log(pc.yellow("\nStart oMLX and download a model:"));
|
|
719
685
|
console.log(pc.bold(" omlx start"));
|
|
686
|
+
console.log(pc.dim(`Recommended for your machine: ${model.label}`));
|
|
720
687
|
console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
|
|
721
|
-
} catch
|
|
722
|
-
console.log(pc.red(
|
|
688
|
+
} catch {
|
|
689
|
+
console.log(pc.red("✗ oMLX installation failed."));
|
|
723
690
|
console.log(pc.dim("Install manually: brew tap jundot/omlx && brew install omlx"));
|
|
724
691
|
}
|
|
725
692
|
} else if (backendChoice === "all") {
|
|
726
|
-
const model = recommendedModel();
|
|
727
693
|
let installed = [];
|
|
728
694
|
// LM Studio
|
|
729
695
|
console.log(pc.cyan("Installing LM Studio via Homebrew..."));
|
|
730
|
-
let lmsReady = false;
|
|
731
696
|
try {
|
|
732
697
|
await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
|
|
733
|
-
lmsReady = ensureLmsOnPath();
|
|
734
698
|
installed.push("LM Studio");
|
|
735
699
|
} catch {
|
|
736
700
|
console.log(pc.yellow("✗ LM Studio installation failed. Download from https://lmstudio.ai"));
|
|
737
701
|
}
|
|
738
702
|
// Ollama
|
|
739
703
|
console.log(pc.cyan("Installing Ollama via Homebrew..."));
|
|
740
|
-
let ollamaInstalled = false;
|
|
741
704
|
try {
|
|
742
705
|
await run("brew", ["install", "ollama"], "Ollama");
|
|
743
706
|
installed.push("Ollama");
|
|
744
|
-
ollamaInstalled = true;
|
|
745
707
|
} catch {
|
|
746
708
|
console.log(pc.yellow("✗ Ollama installation failed. Install manually from https://ollama.com"));
|
|
747
709
|
}
|
|
@@ -754,47 +716,9 @@ async function onboardFlow() {
|
|
|
754
716
|
} catch {
|
|
755
717
|
console.log(pc.yellow("✗ oMLX installation failed. Install manually: brew tap jundot/omlx && brew install omlx"));
|
|
756
718
|
}
|
|
757
|
-
// Auto-download model with best available backend
|
|
758
719
|
if (installed.length > 0) {
|
|
759
720
|
console.log(pc.green(`\n✓ Installed: ${installed.join(", ")}`));
|
|
760
|
-
|
|
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
|
-
}
|
|
796
|
-
}
|
|
797
|
-
console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
|
|
721
|
+
console.log(pc.dim(`Recommended for your machine (${(totalmem() / (1024 ** 3)).toFixed(0)}GB RAM): ${model.label}`));
|
|
798
722
|
}
|
|
799
723
|
} else {
|
|
800
724
|
console.log(pc.dim("Run offgrid-ai again when you've set up a model backend."));
|
|
@@ -904,6 +828,26 @@ async function removeSelf() {
|
|
|
904
828
|
}
|
|
905
829
|
}
|
|
906
830
|
|
|
831
|
+
// ── Backend install detection (for status display) ────────────────────────
|
|
832
|
+
|
|
833
|
+
async function hasOllamaInstalled() {
|
|
834
|
+
try {
|
|
835
|
+
const { promisify } = await import("node:util");
|
|
836
|
+
const { execFile } = await import("node:child_process");
|
|
837
|
+
await promisify(execFile)("which", ["ollama"]);
|
|
838
|
+
return true;
|
|
839
|
+
} catch { return false; }
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function hasOmlxInstalled() {
|
|
843
|
+
try {
|
|
844
|
+
const { promisify } = await import("node:util");
|
|
845
|
+
const { execFile } = await import("node:child_process");
|
|
846
|
+
await promisify(execFile)("which", ["omlx"]);
|
|
847
|
+
return true;
|
|
848
|
+
} catch { return false; }
|
|
849
|
+
}
|
|
850
|
+
|
|
907
851
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
908
852
|
|
|
909
853
|
async function scanManagedModels() {
|