offgrid-ai 0.3.4 → 0.3.6

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/install.sh CHANGED
@@ -11,7 +11,8 @@
11
11
  # 1. Checks for Node.js
12
12
  # 2. If not found, installs it via nvm (no sudo needed)
13
13
  # 3. Installs offgrid-ai globally via npm
14
- # 4. Runs offgrid-ai
14
+ # 4. Adds npm global bin to PATH if needed
15
+ # 5. Runs offgrid-ai
15
16
  #
16
17
  # Flags:
17
18
  # --dry-run Show what would happen without making changes
@@ -108,7 +109,7 @@ echo ""
108
109
  printf "${BOLD}Installing offgrid-ai...${RESET}\n"
109
110
  dry npm install -g offgrid-ai
110
111
 
111
- # ── Verify ───────────────────────────────────────────────────────────────────
112
+ # ── Dry-run early exit ──────────────────────────────────────────────────────
112
113
 
113
114
  if $DRY_RUN; then
114
115
  ok "offgrid-ai installed (dry-run)"
@@ -122,23 +123,52 @@ if $DRY_RUN; then
122
123
  exit 0
123
124
  fi
124
125
 
125
- if command -v offgrid-ai &>/dev/null; then
126
- ok "offgrid-ai installed at $(command -v offgrid-ai)"
127
- else
128
- echo ""
129
- warn "offgrid-ai was installed but isn't on your PATH yet."
130
- echo " Restart your terminal and run: offgrid-ai"
131
- echo " Or run: source ~/.nvm/nvm.sh && offgrid-ai"
132
- echo ""
133
- printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
134
- printf "${BOLD}${GREEN} offgrid-ai is ready!${RESET}\n"
135
- printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
136
- echo ""
137
- echo " Run: offgrid-ai"
138
- echo ""
139
- exit 0
126
+ # ── Add npm global bin to PATH if needed ────────────────────────────────────
127
+
128
+ NPM_BIN="$(npm bin -g 2>/dev/null)"
129
+
130
+ if ! command -v offgrid-ai &>/dev/null; then
131
+ if [[ -n "$NPM_BIN" && -x "$NPM_BIN/offgrid-ai" ]]; then
132
+ # Add to current session
133
+ export PATH="$NPM_BIN:$PATH"
134
+ ok "Added $NPM_BIN to PATH for this session"
135
+
136
+ # Add to shell config for future sessions (pick first existing or .zshrc)
137
+ ADDED_TO_RC=false
138
+ for RC_FILE in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile"; do
139
+ if [[ -f "$RC_FILE" || "$RC_FILE" == "$HOME/.zshrc" ]]; then
140
+ if ! grep -qF "$NPM_BIN" "$RC_FILE" 2>/dev/null; then
141
+ echo '' >> "$RC_FILE"
142
+ echo '# Added by offgrid-ai installer' >> "$RC_FILE"
143
+ echo "export PATH=\"$NPM_BIN:\$PATH\"" >> "$RC_FILE"
144
+ ok "Added $NPM_BIN to $RC_FILE"
145
+ ADDED_TO_RC=true
146
+ # Only add to one rc file to avoid duplicates
147
+ break
148
+ fi
149
+ fi
150
+ done
151
+
152
+ if ! $ADDED_TO_RC; then
153
+ warn "$NPM_BIN is already in a shell config file — restart your terminal to use offgrid-ai"
154
+ fi
155
+ else
156
+ echo ""
157
+ warn "offgrid-ai was installed but the binary wasn't found."
158
+ echo " Restart your terminal and run: offgrid-ai"
159
+ echo ""
160
+ printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
161
+ printf "${BOLD}${GREEN} offgrid-ai is ready!${RESET}\n"
162
+ printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
163
+ echo ""
164
+ echo " Run: offgrid-ai"
165
+ echo ""
166
+ exit 0
167
+ fi
140
168
  fi
141
169
 
170
+ ok "offgrid-ai installed at $(command -v offgrid-ai)"
171
+
142
172
  # ── Done ─────────────────────────────────────────────────────────────────────
143
173
 
144
174
  echo ""
@@ -149,7 +179,12 @@ echo ""
149
179
  echo " First run will walk you through setting up everything you need"
150
180
  echo " (llama-server, model backends, Pi)."
151
181
  echo ""
152
- echo " Run: offgrid-ai"
182
+ if command -v offgrid-ai &>/dev/null; then
183
+ echo " Run: offgrid-ai"
184
+ else
185
+ echo " Run: source ~/.zshrc && offgrid-ai"
186
+ echo " (or open a new terminal)"
187
+ fi
153
188
  echo ""
154
189
 
155
190
  if [[ -t 0 ]] && ! $SKIP_RUN; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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",
@@ -29,10 +29,12 @@
29
29
  "scripts": {
30
30
  "start": "node bin/offgrid-ai.mjs",
31
31
  "test": "node --test test/*.mjs",
32
+ "lint": "eslint src/*.mjs bin/*.mjs",
32
33
  "check:privacy": "node scripts/privacy-gate.mjs",
33
34
  "release:check": "bash scripts/release-check.sh",
34
35
  "release:check:fast": "bash scripts/release-check.sh --skip-install --skip-manual",
35
- "prepack": "npm run check:privacy"
36
+ "prepack": "npm run check:privacy",
37
+ "pretest": "npm run lint"
36
38
  },
37
39
  "dependencies": {
38
40
  "@clack/prompts": "^1.4.0",
@@ -47,5 +49,10 @@
47
49
  "llm",
48
50
  "ai"
49
51
  ],
50
- "license": "MIT"
51
- }
52
+ "license": "MIT",
53
+ "devDependencies": {
54
+ "@eslint/js": "^10.0.1",
55
+ "eslint": "^10.4.1",
56
+ "globals": "^17.6.0"
57
+ }
58
+ }
package/src/cli.mjs CHANGED
@@ -1,6 +1,5 @@
1
- import { homedir, totalmem } from "node:os";
1
+ import { totalmem } from "node:os";
2
2
  import { existsSync, statSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
3
  import { ensureDirs, findLlamaServer, hasHomebrew, DATA_DIR } from "./config.mjs";
5
4
  import { scanGgufModels } from "./scan.mjs";
6
5
  import { createProfileFromModel, normalizeProfile } from "./profiles.mjs";
@@ -145,9 +144,8 @@ export async function mainFlow() {
145
144
  console.log(pc.bold("\nSaved profiles"));
146
145
  for (const profile of profiles) {
147
146
  const backend = backendFor(profile.backend);
148
- const running = await isProfileRunning(profile);
149
- const idx = items.length;
150
147
  const colorMap = { "llama-cpp": pc.yellow, "llama-cpp-mtp": pc.blue, "ollama": pc.magenta, "omlx": pc.cyan };
148
+ const running = await isProfileRunning(profile);
151
149
  const c = colorMap[profile.backend] ?? pc.magenta;
152
150
  console.log(` ${running ? pc.green("●") : pc.dim("○")} ${pc.bold(profile.label)} ${c(`[${backend.label}]`)} · ${pc.cyan(profile.modelAlias)}`);
153
151
  }
@@ -294,13 +292,22 @@ async function runProfile(profile, options = {}) {
294
292
  console.log(pc.dim("Use --reuse-existing to reuse this server."));
295
293
  } else if (!ready) {
296
294
  console.log(pc.dim(`Starting ${backend.label} for ${profile.label}...`));
297
- const state = await startServer(profile);
298
- const tail = state?.rawLogPath ? tailFriendly(state.rawLogPath, state.friendlyLogPath) : { stop() {} };
295
+ let state;
299
296
  try {
300
- await waitForReady(profile, state?.pid, state?.rawLogPath);
301
- console.log(pc.green(`[ready] ${profile.baseUrl}/models`));
302
- } finally {
303
- tail.stop();
297
+ state = await startServer(profile);
298
+ const tail = state?.rawLogPath ? tailFriendly(state.rawLogPath, state.friendlyLogPath) : { stop() {} };
299
+ try {
300
+ await waitForReady(profile, state?.pid, state?.rawLogPath);
301
+ console.log(pc.green(`[ready] ${profile.baseUrl}/models`));
302
+ } finally {
303
+ tail.stop();
304
+ }
305
+ } catch (err) {
306
+ // Clean up orphaned server process if startup failed
307
+ if (state?.pid) {
308
+ try { await stopProfile(profile); } catch { /* best effort */ }
309
+ }
310
+ throw err;
304
311
  }
305
312
  }
306
313
  }
@@ -420,7 +427,7 @@ async function removeProfileInteractive(id) {
420
427
 
421
428
  // ── Benchmark (stub) ────────────────────────────────────────────────────────
422
429
 
423
- async function benchmarkFlow(prompt, profiles) {
430
+ async function benchmarkFlow() {
424
431
  console.log(pc.yellow("Benchmark support coming soon."));
425
432
  console.log(pc.dim("This will require the local-llm-visual-benchmark repo."));
426
433
  console.log(pc.dim("For now, start a model with offgrid-ai and run benchmarks manually."));
@@ -598,7 +605,7 @@ async function onboardFlow() {
598
605
  break;
599
606
  }
600
607
  }
601
- } catch (err) {
608
+ } catch {
602
609
  console.log(pc.red(`✗ Homebrew installation failed.`));
603
610
  console.log(pc.dim("Install it manually from https://brew.sh, then run offgrid-ai again."));
604
611
  return;
@@ -775,8 +782,11 @@ async function onboardFlow() {
775
782
  // ── Uninstall ───────────────────────────────────────────────────────────────
776
783
 
777
784
  async function uninstallCommand(argv) {
778
- if (!process.stdin.isTTY) {
779
- // Non-interactive: remove everything
785
+ const { options } = parseOptions(argv);
786
+ const force = options.force || options.f;
787
+
788
+ if (!process.stdin.isTTY || force) {
789
+ // Non-interactive / forced: remove everything
780
790
  await removeDataDir();
781
791
  await removeSelf();
782
792
  return;
package/src/estimate.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { readGgufMetadata } from "./gguf.mjs";
3
- import pc from "picocolors";
4
3
 
5
4
  export function estimateMemory(modelPath, mmprojPath, draftModelPath, flags) {
6
5
  const modelBytes = statSync(modelPath).size;
package/src/process.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { existsSync, openSync } from "node:fs";
3
+ import { openSync } from "node:fs";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
6
  import { LOG_DIR } from "./config.mjs";
package/src/profiles.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { existsSync, statSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { mkdir, readdir, rm, unlink, writeFile, readFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
3
+ import { join } from "node:path";
4
4
  import { PROFILE_DIR, RUN_DIR, LOG_DIR } from "./config.mjs";
5
5
  import { backendFor } from "./backends.mjs";
6
6
  import { computeFlags } from "./autodetect.mjs";
@@ -113,7 +113,7 @@ export async function deleteProfile(id, options = {}) {
113
113
 
114
114
  // ── Normalize / auto-detect ────────────────────────────────────────────────
115
115
 
116
- export function normalizeProfile(profile, modelPath, mmprojPath) {
116
+ export function normalizeProfile(profile) {
117
117
  const backend = backendFor(profile.backend);
118
118
  const flags = {
119
119
  host: "127.0.0.1",
@@ -152,7 +152,7 @@ export async function createProfileFromModel(model, backendId = "llama-cpp") {
152
152
  preset: null, // no presets — auto-detected
153
153
  flags,
154
154
  commandArgv: argv,
155
- }, model.path, model.mmprojPath);
155
+ });
156
156
  }
157
157
 
158
158
  // ── State files (for running servers) ──────────────────────────────────────