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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.mjs +62 -118
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { homedir, totalmem } from "node:os";
2
- import { existsSync, statSync, rmSync, readFileSync, appendFileSync, mkdirSync, copyFileSync } 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";
@@ -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 one in LM Studio or start Ollama, then run offgrid-ai.");
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("Download a model in LM Studio (https://lmstudio.ai), start Ollama, or install oMLX."));
70
- console.log(pc.dim("Then run offgrid-ai again."));
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 lmsAppBundle = "/Applications/LM Studio.app/Contents/Resources/app/.webpack/lms";
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
- if (lmsReady) {
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
- }
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.cyan("\nStarting Ollama..."));
689
- try {
690
- await run("ollama", ["serve"], "Ollama serve");
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 server:"));
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 (err) {
722
- console.log(pc.red(`✗ oMLX installation failed.`));
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
- 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
- }
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() {