offgrid-ai 0.8.6 → 0.8.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.
package/install.sh CHANGED
@@ -27,6 +27,7 @@ set -euo pipefail
27
27
 
28
28
  DRY_RUN=false
29
29
  SKIP_RUN=false
30
+ DEFAULT_RC="${DEFAULT_RC:-}" # allow callers to override target rc file
30
31
 
31
32
  for arg in "$@"; do
32
33
  case "$arg" in
@@ -153,13 +154,12 @@ if [[ -n "$NPM_BIN" && -x "$NPM_BIN/offgrid-ai" ]]; then
153
154
  else
154
155
  # Not on PATH — add it
155
156
  export PATH="$NPM_BIN:$PATH"
156
- ok "Added $NPM_BIN to PATH for this session"
157
157
  ok "offgrid-ai ${INSTALLED_VERSION:+v${INSTALLED_VERSION} }installed"
158
158
 
159
159
  # Add to shell config for future sessions (pick first existing or .zshrc)
160
160
  ADDED_TO_RC=false
161
161
  # Respect user's shell preference; default to .zshrc on macOS
162
- [[ "$OSTYPE" == darwin* ]] && [[ -z "$DEFAULT_RC" ]] && DEFAULT_RC="$HOME/.zshrc"
162
+ [[ "$OSTYPE" == darwin* ]] && [[ -z "${DEFAULT_RC}" ]] && DEFAULT_RC="$HOME/.zshrc"
163
163
  RC_CANDIDATES=("${DEFAULT_RC:-}" "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile")
164
164
  for RC_FILE in "${RC_CANDIDATES[@]}"; do
165
165
  [[ -z "$RC_FILE" ]] && continue
@@ -174,7 +174,13 @@ if [[ -n "$NPM_BIN" && -x "$NPM_BIN/offgrid-ai" ]]; then
174
174
  fi
175
175
  done
176
176
 
177
- if ! $ADDED_TO_RC; then
177
+ if $ADDED_TO_RC; then
178
+ echo ""
179
+ echo "To use it right now, run:"
180
+ echo " source ${RC_FILE}"
181
+ echo ""
182
+ echo "Or open a new terminal window/tab."
183
+ else
178
184
  warn "$NPM_BIN is already in a shell config file — restart your terminal to use offgrid-ai"
179
185
  fi
180
186
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.8.6",
3
+ "version": "0.8.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/benchmark.mjs CHANGED
@@ -262,7 +262,36 @@ function formatToolCall(toolCall) {
262
262
  return `[toolCall] ${toolCall.name}${summary}`;
263
263
  }
264
264
 
265
- function renderStreamEvent(parsed, state) {
265
+ function formatTokens(n) {
266
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
267
+ if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
268
+ return String(Math.round(n));
269
+ }
270
+
271
+ function estimatedTokensFromText(text) {
272
+ // Simple heuristic: ~4 chars per token for code/English.
273
+ return Math.max(1, Math.ceil(text.length / 4));
274
+ }
275
+
276
+ function clearStatusLine() {
277
+ if (process.stdout.isTTY) {
278
+ process.stdout.write("\r\x1b[K");
279
+ }
280
+ }
281
+
282
+ function printStatusLine(text) {
283
+ if (process.stdout.isTTY) {
284
+ process.stdout.write(`\r\x1b[K${text}`);
285
+ }
286
+ }
287
+
288
+ function printFinalLine(text) {
289
+ clearStatusLine();
290
+ console.log(text);
291
+ }
292
+
293
+ function renderStreamEvent(parsed, state, opts = {}) {
294
+ const verbose = Boolean(opts.verbose);
266
295
  const type = parsed.type;
267
296
 
268
297
  switch (type) {
@@ -274,7 +303,11 @@ function renderStreamEvent(parsed, state) {
274
303
  break;
275
304
  case "turn_start": {
276
305
  state.turn += 1;
277
- console.log(BENCH_COLORS.info(`\n[turn ${state.turn}]`));
306
+ state.status.mode = "thinking";
307
+ state.status.toolName = null;
308
+ state.status.bytes = 0;
309
+ state.status.tokens = 0;
310
+ printFinalLine(BENCH_COLORS.info(`[turn ${state.turn}]`));
278
311
  break;
279
312
  }
280
313
  case "message_start": {
@@ -289,15 +322,21 @@ function renderStreamEvent(parsed, state) {
289
322
  if (!evt) return;
290
323
  const subtype = String(evt.type ?? "").replace(/_/gu, "");
291
324
  if (subtype === "thinkingstart" || subtype === "thinkingdelta") {
292
- process.stdout.write(BENCH_COLORS.thinking(evt.delta || ""));
325
+ if (verbose) process.stdout.write(BENCH_COLORS.thinking(evt.delta || ""));
326
+ state.status.mode = "thinking";
327
+ updateStatusFromDelta(state, evt.delta);
293
328
  } else if (subtype === "textstart" || subtype === "textdelta") {
294
- process.stdout.write(BENCH_COLORS.text(evt.delta || ""));
329
+ if (verbose) process.stdout.write(BENCH_COLORS.text(evt.delta || ""));
330
+ state.status.mode = "text";
331
+ updateStatusFromDelta(state, evt.delta);
295
332
  } else if (subtype === "toolcallstart") {
296
- console.log(BENCH_COLORS.tool("\n[tool_call_start]"));
333
+ if (!verbose) printFinalLine(BENCH_COLORS.tool("[tool_call_start]"));
297
334
  } else if (subtype === "toolcalldelta") {
298
- process.stdout.write(BENCH_COLORS.tool(evt.delta || ""));
335
+ if (verbose) process.stdout.write(BENCH_COLORS.tool(evt.delta || ""));
336
+ state.status.mode = "tool";
337
+ updateStatusFromDelta(state, evt.delta);
299
338
  } else if (subtype === "toolcallend") {
300
- console.log(BENCH_COLORS.tool("[tool_call_end]"));
339
+ if (!verbose) printFinalLine(BENCH_COLORS.tool("[tool_call_end]"));
301
340
  }
302
341
  break;
303
342
  }
@@ -306,36 +345,76 @@ function renderStreamEvent(parsed, state) {
306
345
  if (msg?.role === "assistant" && Array.isArray(msg.content)) {
307
346
  for (const item of msg.content) {
308
347
  if (item.type === "toolCall") {
309
- console.log(BENCH_COLORS.tool(`\n${formatToolCall(item)}`));
348
+ const toolLine = formatToolCall(item);
349
+ state.status.toolName = item.name;
350
+ if (!verbose) printFinalLine(BENCH_COLORS.tool(toolLine));
310
351
  }
311
352
  }
312
353
  }
313
354
  break;
314
355
  }
315
356
  case "tool_execution_start":
316
- console.log(BENCH_COLORS.tool(`\n[exec] ${parsed.toolName}`));
357
+ state.status.mode = "exec";
358
+ state.status.toolName = parsed.toolName;
359
+ state.status.bytes = 0;
360
+ state.status.tokens = 0;
361
+ printFinalLine(BENCH_COLORS.tool(`[exec] ${parsed.toolName}`));
317
362
  break;
318
- case "tool_execution_update":
363
+ case "tool_execution_update": {
319
364
  if (parsed.content) {
320
- process.stdout.write(BENCH_COLORS.toolOutput(parsed.content));
365
+ if (verbose) process.stdout.write(BENCH_COLORS.toolOutput(parsed.content));
366
+ state.status.mode = "exec";
367
+ updateStatusFromDelta(state, parsed.content);
321
368
  }
322
369
  break;
370
+ }
323
371
  case "tool_execution_end":
324
- console.log(BENCH_COLORS.tool(`[exec done] ${parsed.toolName}`));
372
+ printFinalLine(BENCH_COLORS.tool(`[exec done] ${state.status.toolName || parsed.toolName}`));
325
373
  break;
326
374
  case "toolResult": {
327
375
  const errorFlag = parsed.isError ? BENCH_COLORS.error(" error") : "";
328
- console.log(BENCH_COLORS.tool(`\n[result] ${parsed.toolName}${errorFlag}`));
376
+ printFinalLine(BENCH_COLORS.tool(`[result] ${parsed.toolName}${errorFlag}`));
377
+ break;
378
+ }
379
+ case "turn_end": {
380
+ const usage = parsed.message?.usage;
381
+ if (usage) {
382
+ const exact = usage.output ?? usage.totalTokens ?? 0;
383
+ printFinalLine(BENCH_COLORS.info(`[turn ${state.turn}] completed · ${formatTokens(exact)} tokens`));
384
+ } else {
385
+ printFinalLine(BENCH_COLORS.info(`[turn ${state.turn}] completed`));
386
+ }
329
387
  break;
330
388
  }
331
389
  case "agent_end":
332
- console.log(BENCH_COLORS.dim("\n[agent_end]"));
390
+ clearStatusLine();
391
+ console.log(BENCH_COLORS.dim("[agent_end]"));
333
392
  break;
334
393
  default:
335
394
  break;
336
395
  }
337
396
  }
338
397
 
398
+ function updateStatusFromDelta(state, delta) {
399
+ if (!delta) return;
400
+ state.status.bytes += Buffer.byteLength(delta, "utf8");
401
+ state.status.tokens = estimatedTokensFromText(String(state.status.bytes));
402
+ const label = state.status.toolName ? ` · ${state.status.toolName}` : "";
403
+ const modeLabel = state.status.mode === "thinking" ? "thinking" : state.status.mode === "text" ? "text" : state.status.mode === "tool" ? "tool" : "exec";
404
+ const bytes = formatBytes(state.status.bytes);
405
+ const tokens = formatTokens(state.status.tokens);
406
+ printStatusLine(BENCH_COLORS.dim(`[turn ${state.turn}] ${modeLabel}${label} · ${bytes} (~${tokens} tokens)`));
407
+ }
408
+
409
+ function formatBytes(bytes) {
410
+ if (!Number.isFinite(bytes)) return "unknown";
411
+ const units = ["B", "KB", "MB", "GB", "TB"];
412
+ let size = bytes;
413
+ let unit = 0;
414
+ while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit += 1; }
415
+ return `${size.toFixed(unit === 0 ? 0 : 2)} ${units[unit]}`;
416
+ }
417
+
339
418
  export function piModelString(profile) {
340
419
  return profile.harnesses?.pi?.model ?? `${profile.providerId}/${profile.modelAlias}`;
341
420
  }
@@ -382,7 +461,8 @@ export async function runBenchmarkInPi(profile, runDirectory, { signal } = {}) {
382
461
  const streamHandle = await openFileHandle(streamPath, "w");
383
462
  const stderrHandle = await openFileHandle(stderrPath, "w");
384
463
 
385
- const renderState = { turn: 0 };
464
+ const verbose = Boolean(process.env.OFFGRID_BENCHMARK_VERBOSE);
465
+ const renderState = { turn: 0, status: { mode: "idle", toolName: null, bytes: 0, tokens: 0 } };
386
466
 
387
467
  function appendResponse(text) {
388
468
  responseBuffer += text;
@@ -436,7 +516,7 @@ export async function runBenchmarkInPi(profile, runDirectory, { signal } = {}) {
436
516
  const timestamp = extractTimestamp(parsed);
437
517
  updateTimeBounds(timestamp);
438
518
 
439
- renderStreamEvent(parsed, renderState);
519
+ renderStreamEvent(parsed, renderState, { verbose });
440
520
 
441
521
  if (parsed.type === "session" || parsed.type === "agent_start") {
442
522
  if (timestamp && runStartMs === null) runStartMs = timestamp;
@@ -17,12 +17,6 @@ if (!prefix) {
17
17
  process.exit(0);
18
18
  }
19
19
 
20
- if (isHermesPrefix(prefix, process.env.HOME)) {
21
- console.log("offgrid-ai installed with a Hermes-managed npm prefix.");
22
- console.log("Not adding Hermes Node to PATH automatically. Use your normal Node/npm, or run the offgrid-ai install script.");
23
- process.exit(0);
24
- }
25
-
26
20
  const npmBin = join(prefix, "bin");
27
21
  const marker = "# Added by offgrid-ai installer";
28
22
  const pathLine = `export PATH="${npmBin}:$PATH"`;
@@ -100,12 +94,6 @@ function currentPackageVersion() {
100
94
  }
101
95
  }
102
96
 
103
- function isHermesPrefix(prefix, home) {
104
- const normalized = prefix.replace(/\\/gu, "/");
105
- if (normalized.includes("/.hermes/")) return true;
106
- return Boolean(home && normalized === `${home.replace(/\\/gu, "/")}/.hermes/node`);
107
- }
108
-
109
97
  function isLikelyGlobalPrefix(prefix, home) {
110
98
  if (!prefix || !home) return false;
111
99
  const normalized = prefix.replace(/\\/gu, "/");