ai-or-die 0.1.70 → 0.1.71

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/bin/ai-or-die.js CHANGED
@@ -12,6 +12,7 @@ try {
12
12
  open = openModule.default || openModule;
13
13
  } catch { open = null; }
14
14
  const { ClaudeCodeWebServer } = require('../src/server');
15
+ const { isBun } = require('../src/utils/runtime');
15
16
 
16
17
  const program = new Command();
17
18
 
@@ -35,10 +36,14 @@ program
35
36
  .option('--terminal-alias <name>', 'display alias for Terminal (default: env TERMINAL_ALIAS or "Terminal")')
36
37
  .option('--tunnel', 'enable dev tunnel (requires devtunnel CLI installed)')
37
38
  .option('--tunnel-allow-anonymous', 'allow anonymous access to dev tunnel')
38
- .option('--stt', 'enable local speech-to-text (downloads ~670MB Parakeet V3 model on first use)')
39
+ .option('--no-stt', 'disable local speech-to-text (on by default; downloads ~670MB Parakeet V3 model on first use)')
39
40
  .option('--stt-endpoint <url>', 'use external STT endpoint (OpenAI-compatible)')
40
41
  .option('--stt-model-dir <path>', 'custom directory for STT model files')
41
- .option('--stt-threads <number>', 'CPU threads for STT inference (default: auto, max 4)');
42
+ .option('--stt-threads <number>', 'CPU threads for STT inference (default: auto, max 4)')
43
+ .option('--no-sticky-notes', 'disable per-tab AI session summaries + auto tab titles (on by default)')
44
+ .option('--sticky-notes-model-dir <path>', 'custom directory for the sticky-note model file')
45
+ .option('--sticky-notes-model <url>', 'override the sticky-note model GGUF download URL')
46
+ .option('--sticky-notes-threads <number>', 'CPU threads for sticky-note inference (default: auto, max 4)');
42
47
 
43
48
  // Auto-open is OFF by default and opt-in via --open. Legacy callers may still pass
44
49
  // --no-open (the old opt-out flag); filter it out so it parses harmlessly as a no-op.
@@ -64,6 +69,22 @@ async function main() {
64
69
  process.exit(1);
65
70
  }
66
71
 
72
+ // Bun: limited support. The app continues to run, but two native
73
+ // incompatibilities apply (both externally confirmed, neither fixable here):
74
+ // • node-llama-cpp's N-API addon crashes Bun (NAPI FATAL ERROR, exit 133),
75
+ // so the sticky-note model is force-disabled (server + engine self-gate).
76
+ // • node-pty cannot read the PTY master under Bun (oven-sh/bun#25822) — the
77
+ // terminal may "start" but never show a prompt (it hangs).
78
+ // STT still works under Bun. For a guaranteed-working terminal, use Node.js.
79
+ if (isBun()) {
80
+ const bunVer = (process.versions && process.versions.bun) || 'unknown';
81
+ console.log(`\n\x1b[33m⚠ Running under Bun ${bunVer} — limited support. Continuing with sticky-notes disabled.\x1b[0m`);
82
+ console.log(' • Sticky-note summaries are disabled under Bun (node-llama-cpp crashes Bun’s N-API).');
83
+ console.log(' • Heads-up: terminal output can hang under Bun (node-pty/#25822).');
84
+ console.log(' If the prompt never appears, run with Node.js instead:');
85
+ console.log(` \x1b[1mnode ${path.relative(process.cwd(), __filename) || 'bin/ai-or-die.js'} ${process.argv.slice(2).join(' ')}\x1b[0m\n`);
86
+ }
87
+
67
88
  // Handle authentication logic
68
89
  // Tunnel mode disables auth — the tunnel itself controls access
69
90
  let authToken = null;
@@ -93,10 +114,15 @@ async function main() {
93
114
  geminiAlias: options.geminiAlias || process.env.GEMINI_ALIAS || 'Gemini',
94
115
  terminalAlias: options.terminalAlias || process.env.TERMINAL_ALIAS || 'Terminal',
95
116
  folderMode: true, // Always use folder mode
96
- stt: options.stt || !!process.env.STT_ENABLED,
117
+ stt: options.stt !== false && process.env.STT_DISABLED !== '1',
97
118
  sttEndpoint: options.sttEndpoint || process.env.STT_ENDPOINT,
98
119
  sttModelDir: options.sttModelDir || process.env.AI_OR_DIE_MODELS_DIR,
99
120
  sttThreads: options.sttThreads || process.env.STT_THREADS,
121
+ // Per-tab AI session summaries (on by default; --no-sticky-notes disables).
122
+ stickyNotes: options.stickyNotes !== false && process.env.STICKY_NOTES_DISABLED !== '1',
123
+ stickyNotesModelDir: options.stickyNotesModelDir || process.env.STICKY_NOTES_MODEL_DIR,
124
+ stickyNotesModel: options.stickyNotesModel || process.env.STICKY_NOTES_MODEL,
125
+ stickyNotesThreads: options.stickyNotesThreads || process.env.STICKY_NOTES_THREADS,
100
126
  };
101
127
 
102
128
  console.log('Starting ai-or-die...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-or-die",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "description": "Universal AI coding terminal — Claude, Copilot, Gemini & more in your browser",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -41,6 +41,7 @@
41
41
  "dependencies": {
42
42
  "@lydell/node-pty": "1.2.0-beta.10",
43
43
  "@vscode/ripgrep": "^1.18.0",
44
+ "@xterm/headless": "^6.0.0",
44
45
  "chokidar": "^5.0.0",
45
46
  "commander": "^12.1.0",
46
47
  "cors": "^2.8.5",
@@ -66,5 +67,8 @@
66
67
  "jsdom": "^24.1.3",
67
68
  "mocha": "^11.7.1",
68
69
  "postject": "^1.0.0-alpha.6"
70
+ },
71
+ "optionalDependencies": {
72
+ "node-llama-cpp": "^3.18.1"
69
73
  }
70
74
  }
@@ -8,6 +8,22 @@ const os = require('os');
8
8
  const PTY_WRITE_CHUNK_SIZE = 4096;
9
9
  /** Inter-chunk delay in ms — allows ConPTY buffer to drain */
10
10
  const PTY_WRITE_CHUNK_DELAY_MS = 10;
11
+ /**
12
+ * Grace window (ms) during which a read EAGAIN with no life-sign yet is treated
13
+ * as a benign transient startup blip and swallowed. After this, a *sustained*
14
+ * EAGAIN flood with no output is treated as a real failure and surfaced (rather
15
+ * than hanging silently until the 30s spawn watchdog). Node delivers its first
16
+ * PTY output well within this window; Bun's permanent read EAGAIN does not.
17
+ */
18
+ const PTY_EAGAIN_GRACE_MS = 3000;
19
+ /**
20
+ * Minimum number of pre-life-sign EAGAIN errors before a session is treated as a
21
+ * sustained-failure (the Bun + node-pty read loop fires EAGAIN continuously,
22
+ * forever). Set well above any plausible Node startup count (node-pty emits
23
+ * "EAGAIN twice at first"), so a stray late EAGAIN on a slow Node session can
24
+ * never trip a false teardown — worst case it falls through to the 30s watchdog.
25
+ */
26
+ const PTY_EAGAIN_FAIL_THRESHOLD = 50;
11
27
 
12
28
  class BaseBridge {
13
29
  constructor(toolName, options = {}) {
@@ -28,6 +44,40 @@ class BaseBridge {
28
44
  this._commandReady = this.initCommand();
29
45
  }
30
46
 
47
+ /**
48
+ * True when a PTY 'error' is a read EAGAIN ("resource temporarily unavailable").
49
+ * @param {Error & {code?: string}} error
50
+ * @returns {boolean}
51
+ */
52
+ static isEagainError(error) {
53
+ return !!(error && (error.code === 'EAGAIN' || (error.message && error.message.includes('EAGAIN'))));
54
+ }
55
+
56
+ /**
57
+ * Decide whether a PTY 'error' is a benign, swallowable transient read EAGAIN.
58
+ *
59
+ * Swallow when the error is EAGAIN AND any of: a life-sign already arrived
60
+ * (post-startup blip); we are still inside the startup grace window; or the
61
+ * EAGAIN count has not yet reached the sustained-failure threshold. Only a
62
+ * *sustained* EAGAIN flood with no life-sign past the grace window (the Bun +
63
+ * node-pty read failure, oven-sh/bun#25822, where the master never delivers
64
+ * data) is surfaced — so the session tears down + the client gets feedback
65
+ * instead of an infinite hang, while a stray late EAGAIN on a slow Node
66
+ * session is never enough to false-fail it.
67
+ *
68
+ * @param {Error & {code?: string}} error
69
+ * @param {boolean} receivedLifeSign - true once any onData/onExit fired
70
+ * @param {number} elapsedMs - ms since the PTY was spawned
71
+ * @param {number} eagainCount - count of EAGAIN errors seen so far (incl. this one)
72
+ * @returns {boolean} true → swallow (return early); false → handle the error
73
+ */
74
+ static shouldSwallowTransientEagain(error, receivedLifeSign, elapsedMs, eagainCount) {
75
+ if (!BaseBridge.isEagainError(error)) return false;
76
+ if (receivedLifeSign) return true;
77
+ if (elapsedMs < PTY_EAGAIN_GRACE_MS) return true;
78
+ return eagainCount < PTY_EAGAIN_FAIL_THRESHOLD;
79
+ }
80
+
31
81
  /**
32
82
  * Async command discovery — runs where/which without blocking the event loop.
33
83
  * Called automatically from the constructor; await bridge._commandReady to ensure it's done.
@@ -264,9 +314,16 @@ class BaseBridge {
264
314
 
265
315
  // Spawn watchdog: if no data, exit, or error arrives within 30s, treat as failure
266
316
  let receivedLifeSign = false;
317
+ const ptyStartedAt = Date.now();
318
+ let eagainCount = 0;
319
+ // One-shot guard so the watchdog + error paths can each tear the session
320
+ // down + call onError at most once (a sustained EAGAIN flood fires the
321
+ // error handler repeatedly).
322
+ let terminalFailureHandled = false;
267
323
  const SPAWN_TIMEOUT_MS = 30000;
268
324
  const spawnWatchdog = setTimeout(() => {
269
- if (!receivedLifeSign && session.active) {
325
+ if (!receivedLifeSign && session.active && !terminalFailureHandled) {
326
+ terminalFailureHandled = true;
270
327
  console.error(`${this.toolName} session ${sessionId}: no response within ${SPAWN_TIMEOUT_MS}ms, treating as spawn failure`);
271
328
  session.active = false;
272
329
  this.sessions.delete(sessionId);
@@ -340,6 +397,26 @@ class BaseBridge {
340
397
  this._addPtyDisposable(session, onExitDisposable);
341
398
 
342
399
  const errorHandler = (error) => {
400
+ // read EAGAIN ("resource temporarily unavailable") is a known transient
401
+ // PTY-startup condition under Node — node-pty's own socket 'error'
402
+ // handler ignores it ("fs.ReadStream gets EAGAIN twice at first") and
403
+ // keeps the master fd alive. We attach a *second* 'error' listener on the
404
+ // same socket, so we swallow the same transient blips: any EAGAIN after a
405
+ // life-sign (output/exit already arrived), within a short startup grace
406
+ // window, or below the sustained-failure threshold. That stops a benign
407
+ // EAGAIN from tearing the session down and surfacing a fatal "Connection
408
+ // Error" that makes the client retry + double-spawn.
409
+ //
410
+ // But a *sustained* EAGAIN flood with no life-sign is NOT transient — it
411
+ // is the Bun + node-pty read failure (oven-sh/bun#25822), where the PTY
412
+ // master never delivers data. Swallowing it forever turns a dead session
413
+ // into a silent hang until the 30s watchdog. Once the grace window passes
414
+ // AND the EAGAIN count crosses the threshold (with no life-sign), fall
415
+ // through so the error surfaces (client gets feedback / can reconnect).
416
+ if (BaseBridge.isEagainError(error)) eagainCount++;
417
+ if (BaseBridge.shouldSwallowTransientEagain(error, receivedLifeSign, Date.now() - ptyStartedAt, eagainCount)) {
418
+ return;
419
+ }
343
420
  if (!receivedLifeSign) {
344
421
  receivedLifeSign = true;
345
422
  clearTimeout(spawnWatchdog);
@@ -349,12 +426,22 @@ class BaseBridge {
349
426
  if (error.message && error.message.includes('read EIO')) {
350
427
  return;
351
428
  }
429
+ // One-shot: a sustained EAGAIN flood (or the watchdog) must not tear the
430
+ // session down or call onError more than once.
431
+ if (terminalFailureHandled) {
432
+ return;
433
+ }
434
+ terminalFailureHandled = true;
352
435
  console.error(`${this.toolName} session ${sessionId} error:`, error);
353
436
  if (session.killTimeout) {
354
437
  clearTimeout(session.killTimeout);
355
438
  session.killTimeout = null;
356
439
  }
357
440
  this._disposePtyDisposables(session, sessionId);
441
+ // Kill the PTY child so a failed session (e.g. a Bun EAGAIN flood, where
442
+ // the shell is alive but unreadable) doesn't leak as a zombie process /
443
+ // FD — mirrors the spawn-watchdog teardown. Harmless if already dead.
444
+ try { ptyProcess.kill(); } catch (e) { /* ignore — may already be dead */ }
358
445
  if (this.sessions.has(sessionId)) {
359
446
  session.active = false;
360
447
  this.sessions.delete(sessionId);