offgrid-ai 0.2.6 → 0.2.8

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 +39 -110
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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
- import { homedir } from "node:os";
2
- import { existsSync, statSync, rmSync, readFileSync, appendFileSync, mkdirSync, copyFileSync } from "node:fs";
1
+ import { homedir, totalmem } from "node:os";
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";
@@ -489,6 +489,22 @@ async function runningProfiles() {
489
489
 
490
490
  // ── Onboarding ──────────────────────────────────────────────────────────────
491
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
+
492
508
  async function onboardFlow() {
493
509
  startInteractive("offgrid-ai setup");
494
510
  const prompt = createPrompt();
@@ -606,57 +622,16 @@ async function onboardFlow() {
606
622
  { value: "skip", label: "Skip for now", hint: "I'll set up models myself" },
607
623
  ], "lmstudio");
608
624
 
609
- const lmsAppBundle = "/Applications/LM Studio.app/Contents/Resources/app/.webpack/lms";
610
- const lmsBin = join(homedir(), ".lmstudio", "bin");
611
-
612
- const ensureLmsOnPath = () => {
613
- const lmsDest = join(lmsBin, "lms");
614
- // Bootstrap lms from app bundle if not yet in ~/.lmstudio/bin
615
- if (!existsSync(lmsDest) && existsSync(lmsAppBundle)) {
616
- mkdirSync(lmsBin, { recursive: true });
617
- copyFileSync(lmsAppBundle, lmsDest);
618
- }
619
- if (!existsSync(lmsDest)) {
620
- console.log(pc.yellow(" Note: lms CLI will be available after opening LM Studio once."));
621
- return false;
622
- }
623
- if (process.env.PATH.split(":").includes(lmsBin)) return true;
624
- process.env.PATH = `${lmsBin}:${process.env.PATH}`;
625
- const profileFiles = [join(homedir(), ".zshrc"), join(homedir(), ".bash_profile")];
626
- const line = `export PATH="$PATH:$HOME/.lmstudio/bin"`;
627
- for (const f of profileFiles) {
628
- if (!existsSync(f)) continue;
629
- const content = readFileSync(f, "utf8");
630
- if (content.includes(".lmstudio/bin")) continue;
631
- appendFileSync(f, `\n${line}\n`);
632
- }
633
- return true;
634
- };
625
+ const model = recommendedModel();
635
626
 
636
627
  if (backendChoice === "lmstudio") {
637
628
  console.log(pc.cyan("Installing LM Studio via Homebrew..."));
638
629
  try {
639
630
  await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
640
- const lmsReady = ensureLmsOnPath();
641
631
  console.log(pc.green("✓ LM Studio installed"));
642
- if (lmsReady) {
643
- const download = await prompt.yesNo("Download a model now? (qwen3.5-9b)", true);
644
- if (download) {
645
- console.log(pc.cyan("Downloading qwen/qwen3.5-9b via lms..."));
646
- try {
647
- await run("lms", ["get", "qwen/qwen3.5-9b", "-y"], "lms get qwen/qwen3.5-9b");
648
- console.log(pc.green("✓ Model downloaded."));
649
- } catch {
650
- console.log(pc.yellow("Model download failed. Run manually: lms get qwen/qwen3.5-9b"));
651
- }
652
- } else {
653
- console.log(pc.yellow("Download a model later:"));
654
- console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
655
- }
656
- } else {
657
- console.log(pc.yellow("\nOpen LM Studio once to finish setup, then download a model:"));
658
- console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
659
- }
632
+ console.log(pc.yellow("\nOpen LM Studio, set up the app, and download a model."));
633
+ console.log(pc.dim(`Recommended for your machine: ${model.label}`));
634
+ console.log(pc.bold(` lms get ${model.lms}`));
660
635
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
661
636
  } catch {
662
637
  console.log(pc.red("✗ LM Studio installation failed."));
@@ -667,25 +642,9 @@ async function onboardFlow() {
667
642
  try {
668
643
  await run("brew", ["install", "ollama"], "Ollama");
669
644
  console.log(pc.green("✓ Ollama installed"));
670
- console.log(pc.cyan("\nStarting Ollama..."));
671
- try {
672
- await run("ollama", ["serve"], "Ollama serve");
673
- await new Promise((resolve) => setTimeout(resolve, 2000));
674
- } catch { /* may already be running */ }
675
- console.log(pc.green("Ollama is running."));
676
- const download = await prompt.yesNo("Pull a model now? (gemma3:4b)", true);
677
- if (download) {
678
- console.log(pc.cyan("Pulling gemma3:4b via Ollama..."));
679
- try {
680
- await run("ollama", ["pull", "gemma3:4b"], "ollama pull gemma3:4b");
681
- console.log(pc.green("✓ Model pulled."));
682
- } catch {
683
- console.log(pc.yellow("Model pull failed. Run manually: ollama pull gemma3:4b"));
684
- }
685
- } else {
686
- console.log(pc.yellow("Pull a model later:"));
687
- console.log(pc.bold(" ollama pull gemma3:4b"));
688
- }
645
+ console.log(pc.yellow("\nStart Ollama and pull a model:"));
646
+ console.log(pc.bold(` ollama serve \u0026 ollama pull ${model.ollama}`));
647
+ console.log(pc.dim(`Recommended for your machine: ${model.label}`));
689
648
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
690
649
  } catch {
691
650
  console.log(pc.red("✗ Ollama installation failed."));
@@ -697,32 +656,29 @@ async function onboardFlow() {
697
656
  await run("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], "oMLX tap");
698
657
  await run("brew", ["install", "omlx"], "oMLX");
699
658
  console.log(pc.green("✓ oMLX installed"));
700
- console.log(pc.yellow("\nStart oMLX server:"));
659
+ console.log(pc.yellow("\nStart oMLX and download a model:"));
701
660
  console.log(pc.bold(" omlx start"));
661
+ console.log(pc.dim(`Recommended for your machine: ${model.label}`));
702
662
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
703
- } catch (err) {
704
- console.log(pc.red(`✗ oMLX installation failed.`));
663
+ } catch {
664
+ console.log(pc.red("✗ oMLX installation failed."));
705
665
  console.log(pc.dim("Install manually: brew tap jundot/omlx && brew install omlx"));
706
666
  }
707
667
  } else if (backendChoice === "all") {
708
668
  let installed = [];
709
669
  // LM Studio
710
670
  console.log(pc.cyan("Installing LM Studio via Homebrew..."));
711
- let lmsReady = false;
712
671
  try {
713
672
  await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
714
- lmsReady = ensureLmsOnPath();
715
673
  installed.push("LM Studio");
716
674
  } catch {
717
675
  console.log(pc.yellow("✗ LM Studio installation failed. Download from https://lmstudio.ai"));
718
676
  }
719
677
  // Ollama
720
678
  console.log(pc.cyan("Installing Ollama via Homebrew..."));
721
- let ollamaInstalled = false;
722
679
  try {
723
680
  await run("brew", ["install", "ollama"], "Ollama");
724
681
  installed.push("Ollama");
725
- ollamaInstalled = true;
726
682
  } catch {
727
683
  console.log(pc.yellow("✗ Ollama installation failed. Install manually from https://ollama.com"));
728
684
  }
@@ -735,45 +691,18 @@ async function onboardFlow() {
735
691
  } catch {
736
692
  console.log(pc.yellow("✗ oMLX installation failed. Install manually: brew tap jundot/omlx && brew install omlx"));
737
693
  }
738
- // Auto-download model with best available backend
739
694
  if (installed.length > 0) {
740
695
  console.log(pc.green(`\n✓ Installed: ${installed.join(", ")}`));
741
- if (lmsReady) {
742
- const download = await prompt.yesNo("Download a model now? (qwen3.5-9b)", true);
743
- if (download) {
744
- console.log(pc.cyan("Downloading qwen/qwen3.5-9b via lms..."));
745
- try {
746
- await run("lms", ["get", "qwen/qwen3.5-9b", "-y"], "lms get qwen/qwen3.5-9b");
747
- console.log(pc.green("✓ Model downloaded."));
748
- } catch {
749
- console.log(pc.yellow("Model download failed. Run manually: lms get qwen/qwen3.5-9b"));
750
- }
751
- }
752
- } else if (ollamaInstalled) {
753
- // Start Ollama and offer to pull
754
- console.log(pc.cyan("Starting Ollama..."));
755
- try { await run("ollama", ["serve"], "Ollama serve"); await new Promise(r => setTimeout(r, 2000)); } catch { /* may already be running */ }
756
- const download = await prompt.yesNo("Pull a model now? (gemma3:4b)", true);
757
- if (download) {
758
- console.log(pc.cyan("Pulling gemma3:4b via Ollama..."));
759
- try {
760
- await run("ollama", ["pull", "gemma3:4b"], "ollama pull gemma3:4b");
761
- console.log(pc.green("✓ Model pulled."));
762
- } catch {
763
- console.log(pc.yellow("Model pull failed. Run manually: ollama pull gemma3:4b"));
764
- }
765
- }
766
- } else {
767
- console.log(pc.yellow("Next steps — download your first model:"));
768
- if (installed.some(i => i.includes("LM Studio"))) {
769
- console.log(pc.bold(lmsReady ? " lms get qwen/qwen3.5-9b" : " Open LM Studio once, then: lms get qwen/qwen3.5-9b"));
770
- }
771
- if (installed.includes("Ollama")) {
772
- console.log(pc.bold(" ollama pull gemma3:4b"));
773
- }
774
- if (installed.includes("oMLX")) {
775
- console.log(pc.bold(" omlx start"));
776
- }
696
+ console.log(pc.yellow("\nDownload a model to get started:"));
697
+ console.log(pc.dim(`Recommended for your machine (${(totalmem() / (1024 ** 3)).toFixed(0)}GB RAM): ${model.label}`));
698
+ if (installed.includes("LM Studio")) {
699
+ console.log(pc.bold(` LM Studio lms get ${model.lms}`));
700
+ }
701
+ if (installed.includes("Ollama")) {
702
+ console.log(pc.bold(` Ollama → ollama pull ${model.ollama}`));
703
+ }
704
+ if (installed.includes("oMLX")) {
705
+ console.log(pc.bold(" oMLX → omlx start"));
777
706
  }
778
707
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
779
708
  }