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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.mjs +130 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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,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, stream output to terminal, throw on failure. */
515
+ /** Run a command. Verbose: stream output. Quiet: show only label + result. */
498
516
  const run = (cmd, args, label) => new Promise((resolve, reject) => {
499
- const child = spawn(cmd, args, { stdio: "inherit" });
500
- child.on("close", (code) => {
501
- if (code === 0) resolve();
502
- else reject(new Error(`${label || cmd} exited with code ${code}`));
503
- });
504
- child.on("error", (err) => reject(err));
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
- console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
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.bold(" Open LM Studio once, then: lms get qwen/qwen3.5-9b"));
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 (err) {
637
- console.log(pc.red(`✗ LM Studio installation failed.`));
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
- console.log(pc.yellow("\nPull your first model:"));
652
- console.log(pc.bold(" ollama pull gemma3:4b"));
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 (err) {
655
- console.log(pc.red(`✗ Ollama installation failed.`));
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
- const lmsReady = ensureLmsOnPath();
678
- installed.push(lmsReady ? "LM Studio" : "LM Studio (open app once for lms CLI)");
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
- console.log(pc.yellow("Next steps — download your first model:"));
702
- const hasLms = installed.some(i => i.includes("LM Studio"));
703
- if (hasLms) {
704
- const lmsReady = installed.some(i => i === "LM Studio");
705
- console.log(pc.bold(lmsReady ? " lms get qwen/qwen3.5-9b" : " Open LM Studio once, then: lms get qwen/qwen3.5-9b"));
706
- }
707
- if (installed.includes("Ollama")) {
708
- console.log(pc.bold(" ollama pull gemma3:4b"));
709
- }
710
- if (installed.includes("oMLX")) {
711
- console.log(pc.bold(" omlx start"));
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 child = spawnUninstall(cmd, args, { stdio: "inherit" });
799
- child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${label || cmd} exited with code ${code}`)));
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
  }