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 +53 -18
- package/package.json +11 -4
- package/src/cli.mjs +24 -14
- package/src/estimate.mjs +0 -1
- package/src/process.mjs +1 -1
- package/src/profiles.mjs +4 -4
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.
|
|
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
|
-
# ──
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
298
|
-
const tail = state?.rawLogPath ? tailFriendly(state.rawLogPath, state.friendlyLogPath) : { stop() {} };
|
|
295
|
+
let state;
|
|
299
296
|
try {
|
|
300
|
-
await
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
779
|
-
|
|
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
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 {
|
|
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
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, readdir, rm, unlink, writeFile, readFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
}
|
|
155
|
+
});
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// ── State files (for running servers) ──────────────────────────────────────
|