ai-or-die 0.1.49 → 0.1.52

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
@@ -18,7 +18,7 @@ const program = new Command();
18
18
  program
19
19
  .name('ai-or-die')
20
20
  .description('ai-or-die — Universal AI coding terminal')
21
- .version('0.1.0')
21
+ .version(require(path.join(__dirname, '..', 'package.json')).version)
22
22
  .option('-p, --port <number>', 'port to run the server on', '7777')
23
23
  .option('--no-open', 'do not automatically open browser')
24
24
  .option('--auth <token>', 'authentication token for secure access')
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "ai-or-die",
3
- "version": "0.1.49",
3
+ "version": "0.1.52",
4
4
  "description": "Universal AI coding terminal — Claude, Copilot, Gemini & more in your browser",
5
5
  "main": "src/server.js",
6
6
  "bin": {
7
- "ai-or-die": "bin/ai-or-die.js",
8
- "aiordie": "bin/ai-or-die.js"
7
+ "ai-or-die": "./bin/ai-or-die.js",
8
+ "aiordie": "./bin/ai-or-die.js"
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node bin/supervisor.js",
package/src/public/app.js CHANGED
@@ -16,7 +16,10 @@ class ClaudeCodeWebInterface {
16
16
  this.currentFolderPath = null;
17
17
  this.claudeSessions = [];
18
18
  this.isCreatingNewSession = false;
19
- this.isMobile = this.detectMobile();
19
+ Object.defineProperty(this, 'isMobile', {
20
+ get: () => this.detectMobile(),
21
+ configurable: true
22
+ });
20
23
  this.currentMode = 'chat';
21
24
  this._overlayExplicitlyHidden = false;
22
25
  this.planDetector = null;
@@ -250,10 +253,12 @@ class ClaudeCodeWebInterface {
250
253
 
251
254
  // Network change handlers
252
255
  window.addEventListener('online', () => {
256
+ if (window.feedback) window.feedback.success('Back online — reconnecting...');
253
257
  if (this.socket?.readyState !== WebSocket.OPEN) this.reconnect();
254
258
  });
255
259
  window.addEventListener('offline', () => {
256
260
  this.updateStatus('Offline');
261
+ if (window.feedback) window.feedback.warning('Connection lost — you are offline', { duration: 0 });
257
262
  });
258
263
 
259
264
  window.addEventListener('resize', () => {
@@ -344,7 +349,7 @@ class ClaudeCodeWebInterface {
344
349
 
345
350
  // Skip preventDefault for scrollable containers that handle their own scroll
346
351
  const target = e.target;
347
- if (target && (target.closest('.xterm-viewport') || target.closest('.modal-body') || target.closest('.extra-keys-bar'))) {
352
+ if (target && (target.closest('.xterm-viewport') || target.closest('.modal-body') || target.closest('.extra-keys-bar') || target.closest('.fb-file-list') || target.closest('.fb-content') || target.closest('.fb-preview-container'))) {
348
353
  lastY = y;
349
354
  return;
350
355
  }
@@ -725,19 +730,13 @@ class ClaudeCodeWebInterface {
725
730
 
726
731
  if (heightDiff > threshold && !this._keyboardOpen) {
727
732
  this._keyboardOpen = true;
733
+ // Apply class immediately — CSS transitions handle visual smoothing
734
+ document.body.classList.add('keyboard-open');
728
735
  this._adjustTerminalForKeyboard(currentHeight);
729
- // Debounce body class to prevent flicker during keyboard animation
730
- clearTimeout(classDebounceTimer);
731
- classDebounceTimer = setTimeout(() => {
732
- document.body.classList.add('keyboard-open');
733
- }, 300);
734
736
  } else if (heightDiff <= threshold && this._keyboardOpen) {
735
737
  this._keyboardOpen = false;
738
+ document.body.classList.remove('keyboard-open');
736
739
  this._restoreTerminalFromKeyboard();
737
- clearTimeout(classDebounceTimer);
738
- classDebounceTimer = setTimeout(() => {
739
- document.body.classList.remove('keyboard-open');
740
- }, 300);
741
740
  }
742
741
  };
743
742
 
@@ -817,8 +816,10 @@ class ClaudeCodeWebInterface {
817
816
  document.documentElement.style.setProperty('--visual-viewport-height', availableHeight + 'px');
818
817
  const termEl = document.getElementById('terminal');
819
818
  if (termEl) {
819
+ // Force reflow after show() so offsetHeight is accurate
820
820
  const extraKeysHeight = this.extraKeys?.container?.offsetHeight || 44;
821
- termEl.style.height = (availableHeight - extraKeysHeight) + 'px';
821
+ void extraKeysHeight; // ensure reflow read is not optimized away
822
+ termEl.style.height = (availableHeight - (this.extraKeys?.container?.offsetHeight || 44)) + 'px';
822
823
  if (this.fitAddon) this.fitAddon.fit();
823
824
  }
824
825
  }
@@ -1639,12 +1640,12 @@ class ClaudeCodeWebInterface {
1639
1640
  // but still use backoff to avoid thundering herd
1640
1641
  if (this._serverRestarting) {
1641
1642
  this.updateStatus('Restarting \u2014 reconnecting\u2026');
1642
- const restartBackoff = Math.min(2000 * Math.pow(1.5, this._restartReconnectAttempts || 0), 15000);
1643
+ const restartBackoff = Math.min(2000 * Math.pow(1.5, this._restartReconnectAttempts || 0), 15000) * (0.7 + Math.random() * 0.6);
1643
1644
  this._restartReconnectAttempts = (this._restartReconnectAttempts || 0) + 1;
1644
1645
  setTimeout(() => this.reconnect(), restartBackoff);
1645
1646
  } else if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) {
1646
- this.updateStatus('Reconnecting...');
1647
- setTimeout(() => this.reconnect(), Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts), 30000));
1647
+ this.updateStatus('Reconnecting (' + (this.reconnectAttempts + 1) + '/' + this.maxReconnectAttempts + ')...');
1648
+ setTimeout(() => this.reconnect(), Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts), 30000) * (0.7 + Math.random() * 0.6));
1648
1649
  this.reconnectAttempts++;
1649
1650
  } else {
1650
1651
  this.updateStatus('Disconnected');
@@ -1675,6 +1676,14 @@ class ClaudeCodeWebInterface {
1675
1676
  clearInterval(this._safariPollInterval);
1676
1677
  this._safariPollInterval = null;
1677
1678
  }
1679
+ if (this._heartbeatTimer) {
1680
+ clearInterval(this._heartbeatTimer);
1681
+ this._heartbeatTimer = null;
1682
+ }
1683
+ if (this.usageUpdateTimer) {
1684
+ clearInterval(this.usageUpdateTimer);
1685
+ this.usageUpdateTimer = null;
1686
+ }
1678
1687
  }
1679
1688
 
1680
1689
  reconnect() {
@@ -2831,10 +2840,7 @@ class ClaudeCodeWebInterface {
2831
2840
  activeSocket = resolved.socket;
2832
2841
 
2833
2842
  // Position menu: bottom sheet on mobile, cursor-anchored on desktop
2834
- // Use current viewport width instead of constructor-time isMobile
2835
- // so tablet rotation is handled correctly (820px matches CSS breakpoint)
2836
- const isMobileViewport = window.innerWidth <= 820;
2837
- if (isMobileViewport) {
2843
+ if (this.isMobile) {
2838
2844
  menu.style.left = '';
2839
2845
  menu.style.top = '';
2840
2846
  menu.style.display = 'block';
@@ -3322,7 +3328,8 @@ class ClaudeCodeWebInterface {
3322
3328
  }
3323
3329
 
3324
3330
  startHeartbeat() {
3325
- setInterval(() => {
3331
+ if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
3332
+ this._heartbeatTimer = setInterval(() => {
3326
3333
  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
3327
3334
  this.send({ type: 'ping' });
3328
3335
  }
@@ -4258,7 +4265,14 @@ class ClaudeCodeWebInterface {
4258
4265
  });
4259
4266
  html += '</ul>';
4260
4267
  }
4261
- return typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(html) : html;
4268
+ if (typeof DOMPurify !== 'undefined') {
4269
+ return DOMPurify.sanitize(html);
4270
+ } else {
4271
+ // Safe fallback: strip all HTML, render as plain text
4272
+ const temp = document.createElement('div');
4273
+ temp.textContent = html;
4274
+ return temp.innerHTML;
4275
+ }
4262
4276
  }
4263
4277
 
4264
4278
  async _loadPlanLibraries() {
@@ -4492,7 +4506,7 @@ class ClaudeCodeWebInterface {
4492
4506
  // Container is already visible by default
4493
4507
 
4494
4508
  // Check if mobile screen
4495
- const isMobile = window.innerWidth <= 820;
4509
+ const isMobile = this.isMobile;
4496
4510
  const isSmallMobile = window.innerWidth <= 480;
4497
4511
 
4498
4512
  // Format tokens (K/M notation)
@@ -4632,8 +4646,8 @@ class ClaudeCodeWebInterface {
4632
4646
  if (totalTokens > 0) {
4633
4647
  const opusPercent = (opusTokens / totalTokens) * 100;
4634
4648
  const sonnetPercent = (sonnetTokens / totalTokens) * 100;
4635
- const isMobile = window.innerWidth <= 820;
4636
-
4649
+ const isMobile = this.isMobile;
4650
+
4637
4651
  // Use short names on mobile, full names on desktop
4638
4652
  const opusName = isMobile ? 'O' : 'Opus';
4639
4653
  const sonnetName = isMobile ? 'S' : 'Sonnet';
@@ -4655,7 +4669,7 @@ class ClaudeCodeWebInterface {
4655
4669
  }
4656
4670
  } else {
4657
4671
  // No active session or expired session - show zeros
4658
- const isMobile = window.innerWidth <= 820;
4672
+ const isMobile = this.isMobile;
4659
4673
 
4660
4674
  document.getElementById('usageTitle').textContent = '0h 0m';
4661
4675
  document.getElementById('usageTokens').textContent = isMobile ? '0%' : '0';
@@ -1,5 +1,5 @@
1
1
  .extra-keys-bar {
2
- display: none;
2
+ display: flex;
3
3
  flex-direction: column;
4
4
  position: fixed;
5
5
  bottom: 0;
@@ -8,10 +8,18 @@
8
8
  background: var(--surface-secondary);
9
9
  border-top: 1px solid var(--border-default);
10
10
  z-index: var(--z-sticky);
11
+ max-height: 0;
12
+ opacity: 0;
13
+ overflow: hidden;
14
+ transition: max-height 0.15s ease-out, opacity 0.15s ease-out;
15
+ pointer-events: none;
11
16
  }
12
17
 
13
18
  .extra-keys-bar.visible {
14
- display: flex;
19
+ max-height: 120px;
20
+ opacity: 1;
21
+ overflow: visible;
22
+ pointer-events: auto;
15
23
  }
16
24
 
17
25
  .extra-keys-row {
@@ -23,12 +31,19 @@
23
31
  padding: 2px 8px;
24
32
  align-items: center;
25
33
  -webkit-overflow-scrolling: touch;
34
+ max-height: 60px;
35
+ opacity: 1;
36
+ transition: max-height 0.15s ease-out, opacity 0.15s ease-out, padding 0.15s ease-out;
26
37
  }
27
38
 
28
39
  .extra-keys-row::-webkit-scrollbar { display: none; }
29
40
 
30
41
  .extra-keys-row-hidden {
31
- display: none;
42
+ max-height: 0;
43
+ opacity: 0;
44
+ overflow: hidden;
45
+ padding: 0;
46
+ transition: max-height 0.15s ease-out, opacity 0.15s ease-out, padding 0.15s ease-out;
32
47
  }
33
48
 
34
49
  .extra-key {
@@ -70,6 +85,20 @@
70
85
  opacity: 0.8;
71
86
  }
72
87
 
88
+ .extra-key-clipboard {
89
+ background: var(--surface-tertiary);
90
+ border-color: var(--accent-default);
91
+ border-width: 1.5px;
92
+ color: var(--accent-default);
93
+ font-weight: 600;
94
+ }
95
+
96
+ .extra-key-clipboard:active {
97
+ background: var(--accent-soft);
98
+ border-color: var(--accent-default);
99
+ color: var(--text-inverse, var(--text-primary));
100
+ }
101
+
73
102
  .extra-key-dismiss:active {
74
103
  background: var(--accent-soft);
75
104
  border-color: var(--accent-default);
@@ -18,6 +18,8 @@ class ExtraKeys {
18
18
  this.container.setAttribute('aria-label', 'Terminal modifier keys');
19
19
 
20
20
  const row1Keys = [
21
+ { label: 'Cp', title: 'Copy selection', handler: 'copy' },
22
+ { label: 'Pst', title: 'Paste clipboard', handler: 'paste' },
21
23
  { label: 'Tab', data: '\t' },
22
24
  { label: 'Ctrl', modifier: 'ctrl' },
23
25
  { label: 'Alt', modifier: 'alt' },
@@ -67,6 +69,12 @@ class ExtraKeys {
67
69
 
68
70
  this._resizeHandler = () => this._updateRow2Visibility();
69
71
  window.addEventListener('resize', this._resizeHandler);
72
+ window.addEventListener('orientationchange', () => {
73
+ setTimeout(() => this._updateRow2Visibility(), 300);
74
+ });
75
+ if (window.visualViewport) {
76
+ window.visualViewport.addEventListener('resize', () => this._updateRow2Visibility());
77
+ }
70
78
  }
71
79
 
72
80
  _buildRow(keys) {
@@ -82,6 +90,16 @@ class ExtraKeys {
82
90
  if (key.dismiss) {
83
91
  btn.classList.add('extra-key-dismiss');
84
92
  btn.addEventListener('click', () => this._dismiss());
93
+ } else if (key.handler) {
94
+ btn.classList.add('extra-key-clipboard');
95
+ if (key.title) btn.setAttribute('title', key.title);
96
+ btn.addEventListener('click', () => {
97
+ if (key.handler === 'copy') {
98
+ this._handleCopy();
99
+ } else if (key.handler === 'paste') {
100
+ this._handlePaste();
101
+ }
102
+ });
85
103
  } else if (key.modifier) {
86
104
  btn.classList.add('extra-key-modifier');
87
105
  btn.dataset.modifier = key.modifier;
@@ -104,6 +122,34 @@ class ExtraKeys {
104
122
  }
105
123
  }
106
124
 
125
+ async _handleCopy() {
126
+ if ('vibrate' in navigator) try { navigator.vibrate(10); } catch (_) {}
127
+ const selection = this.app && this.app.terminal ? this.app.terminal.getSelection() : '';
128
+ if (!selection) {
129
+ if (window.feedback) window.feedback.warning('No text selected');
130
+ return;
131
+ }
132
+ try {
133
+ await navigator.clipboard.writeText(selection);
134
+ if (window.feedback) window.feedback.success('Copied');
135
+ } catch (err) {
136
+ if (window.feedback) window.feedback.warning('Clipboard access denied');
137
+ }
138
+ }
139
+
140
+ async _handlePaste() {
141
+ if ('vibrate' in navigator) try { navigator.vibrate(10); } catch (_) {}
142
+ try {
143
+ const text = await navigator.clipboard.readText();
144
+ if (text && this.app && this.app.send) {
145
+ this.app.send({ type: 'input', data: text });
146
+ if (window.feedback) window.feedback.success('Pasted');
147
+ }
148
+ } catch (err) {
149
+ if (window.feedback) window.feedback.warning('Clipboard access denied');
150
+ }
151
+ }
152
+
107
153
  _sendKey(data) {
108
154
  if (!this.app) return;
109
155
  if ('vibrate' in navigator) try { navigator.vibrate(10); } catch (_) {}
@@ -202,9 +248,12 @@ class ExtraKeys {
202
248
 
203
249
  _updateRow2Visibility() {
204
250
  if (!this._row2 || !this._visible) return;
205
- const termEl = document.getElementById('terminal');
206
- const height = termEl ? termEl.offsetHeight : window.innerHeight;
207
- if (height > 400) {
251
+ const vpHeight = window.visualViewport
252
+ ? window.visualViewport.height
253
+ : window.innerHeight;
254
+ const isLandscape = window.innerWidth > window.innerHeight;
255
+ const threshold = isLandscape ? 280 : 400;
256
+ if (vpHeight > threshold) {
208
257
  this._row2.classList.remove('extra-keys-row-hidden');
209
258
  } else {
210
259
  this._row2.classList.add('extra-keys-row-hidden');
@@ -51,11 +51,15 @@
51
51
  <link rel="stylesheet" href="components/cards.css">
52
52
  <link rel="stylesheet" href="components/menus.css">
53
53
  <link rel="stylesheet" href="components/notifications.css">
54
- <link rel="stylesheet" href="components/file-browser.css">
55
- <link rel="stylesheet" href="components/banner-base.css">
56
- <link rel="stylesheet" href="components/vscode-tunnel.css">
54
+ <link rel="stylesheet" href="components/file-browser.css" media="print" onload="this.media='all'">
55
+ <noscript><link rel="stylesheet" href="components/file-browser.css"></noscript>
56
+ <link rel="stylesheet" href="components/banner-base.css" media="print" onload="this.media='all'">
57
+ <noscript><link rel="stylesheet" href="components/banner-base.css"></noscript>
58
+ <link rel="stylesheet" href="components/vscode-tunnel.css" media="print" onload="this.media='all'">
59
+ <noscript><link rel="stylesheet" href="components/vscode-tunnel.css"></noscript>
57
60
  <link rel="stylesheet" href="components/feedback.css">
58
- <link rel="stylesheet" href="components/voice-input.css">
61
+ <link rel="stylesheet" href="components/voice-input.css" media="print" onload="this.media='all'">
62
+ <noscript><link rel="stylesheet" href="components/voice-input.css"></noscript>
59
63
  <link rel="stylesheet" href="components/input-overlay.css">
60
64
  <link rel="stylesheet" href="components/bottom-nav.css">
61
65
  <link rel="stylesheet" href="components/extra-keys.css">
@@ -64,7 +68,7 @@
64
68
  <link rel="preconnect" href="https://fonts.googleapis.com">
65
69
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
66
70
  <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
67
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
71
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
68
72
  <script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script>
69
73
  <script src="https://unpkg.com/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
70
74
  <script src="https://unpkg.com/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js"></script>
@@ -1,6 +1,6 @@
1
1
  // Bump this version when urlsToCache entries are added or removed.
2
2
  // Content changes to existing files are handled by the network-first fetch strategy.
3
- const CACHE_NAME = 'ai-or-die-v8';
3
+ const CACHE_NAME = 'ai-or-die-v9';
4
4
  const urlsToCache = [
5
5
  '/',
6
6
  '/index.html',
@@ -31,19 +31,38 @@ const urlsToCache = [
31
31
  '/splits.js',
32
32
  '/icons.js',
33
33
  '/components/extra-keys.css',
34
- '/extra-keys.js'
34
+ '/components/file-browser.css',
35
+ '/components/banner-base.css',
36
+ '/components/vscode-tunnel.css',
37
+ '/components/feedback.css',
38
+ '/components/voice-input.css',
39
+ '/components/input-overlay.css',
40
+ '/extra-keys.js',
41
+ '/file-browser.js',
42
+ '/file-editor.js',
43
+ '/voice-handler.js',
44
+ '/image-handler.js',
45
+ '/input-overlay.js',
46
+ '/feedback-manager.js'
35
47
  ];
36
48
 
37
49
  // Install event - cache resources
38
50
  self.addEventListener('install', event => {
39
51
  event.waitUntil(
40
52
  caches.open(CACHE_NAME)
41
- .then(cache => {
53
+ .then(async cache => {
42
54
  console.log('Opened cache');
43
- return cache.addAll(urlsToCache);
55
+ // Use individual cache.add() calls so one failure doesn't block the rest
56
+ const results = await Promise.allSettled(
57
+ urlsToCache.map(url => cache.add(url))
58
+ );
59
+ const failed = results.filter(r => r.status === 'rejected');
60
+ if (failed.length > 0) {
61
+ console.warn(`Failed to cache ${failed.length} of ${urlsToCache.length} resources`);
62
+ }
44
63
  })
45
64
  .catch(err => {
46
- console.error('Failed to cache resources:', err);
65
+ console.error('Failed to open cache:', err);
47
66
  })
48
67
  );
49
68
  });
@@ -92,6 +111,24 @@ self.addEventListener('fetch', event => {
92
111
  return;
93
112
  }
94
113
 
114
+ // Cache-first for versioned CDN assets (immutable, pinned to specific versions)
115
+ const knownCDNs = ['unpkg.com', 'cdnjs.cloudflare.com', 'cdn.jsdelivr.net', 'fonts.googleapis.com', 'fonts.gstatic.com'];
116
+ if (knownCDNs.some(cdn => url.hostname.includes(cdn))) {
117
+ event.respondWith(
118
+ caches.match(request).then(cached => {
119
+ if (cached) return cached;
120
+ return fetch(request).then(response => {
121
+ if (response && response.status === 200) {
122
+ const clone = response.clone();
123
+ caches.open(CACHE_NAME).then(cache => cache.put(request, clone));
124
+ }
125
+ return response;
126
+ });
127
+ })
128
+ );
129
+ return;
130
+ }
131
+
95
132
  // For static assets, try network first, fall back to cache
96
133
  event.respondWith(
97
134
  fetch(request)
@@ -412,7 +412,7 @@ class SessionTabManager {
412
412
  }
413
413
 
414
414
  updateTabOverflow() {
415
- const isMobile = window.innerWidth <= 820;
415
+ const isMobile = this.claudeInterface.isMobile;
416
416
  const overflowWrapper = document.getElementById('tabOverflowWrapper');
417
417
  const overflowCount = document.querySelector('.tab-overflow-count');
418
418
 
@@ -580,7 +580,7 @@ class SessionTabManager {
580
580
  });
581
581
 
582
582
  // Reorder tabs based on the initial timestamps (mobile only)
583
- if (window.innerWidth <= 820) {
583
+ if (this.claudeInterface.isMobile) {
584
584
  this.reorderTabsByLastAccessed();
585
585
  }
586
586
 
@@ -741,7 +741,7 @@ class SessionTabManager {
741
741
  this.updateTabHistory(sessionId);
742
742
  }
743
743
 
744
- if (window.innerWidth <= 820) {
744
+ if (this.claudeInterface.isMobile) {
745
745
  const tabIndex = this.getOrderedTabIds().indexOf(sessionId);
746
746
  if (tabIndex >= 2) this.reorderTabsByLastAccessed();
747
747
  }
package/src/stt-engine.js CHANGED
@@ -21,6 +21,7 @@ class SttEngine {
21
21
  this._currentRequest = null;
22
22
  this._requestIdCounter = 0;
23
23
  this._restartAttempts = 0;
24
+ this._lastSpawnError = null;
24
25
  this._initPromise = null;
25
26
  this._modelManager = new ModelManager({
26
27
  modelsDir: options.modelsDir
@@ -116,7 +117,10 @@ class SttEngine {
116
117
  }
117
118
 
118
119
  if (msg.type === 'error') {
119
- console.error('[stt-engine] Worker model load error:', msg.message);
120
+ console.error('[stt-engine] Worker error:', msg.message);
121
+ if (msg.message && msg.message.includes('sherpa-onnx-node')) {
122
+ this._lastSpawnError = 'MODULE_NOT_FOUND';
123
+ }
120
124
  this._status = 'unavailable';
121
125
  return;
122
126
  }
@@ -154,6 +158,13 @@ class SttEngine {
154
158
 
155
159
  this._worker = null;
156
160
 
161
+ // Don't retry if the dependency is fundamentally missing
162
+ if (this._lastSpawnError === 'MODULE_NOT_FOUND') {
163
+ console.error('[stt-engine] sherpa-onnx-node not installed — STT unavailable. Install with: npm install sherpa-onnx-node');
164
+ this._status = 'unavailable';
165
+ return;
166
+ }
167
+
157
168
  // Give up after too many consecutive failures
158
169
  if (this._restartAttempts >= MAX_RESTART_ATTEMPTS) {
159
170
  console.error(`[stt-engine] Max restart attempts (${MAX_RESTART_ATTEMPTS}) reached, giving up`);
@@ -201,6 +212,7 @@ class SttEngine {
201
212
  this._worker = worker;
202
213
  this._status = 'ready';
203
214
  this._restartAttempts = 0;
215
+ this._lastSpawnError = null;
204
216
 
205
217
  worker.on('message', (m) => this._onWorkerMessage(m));
206
218
  worker.on('exit', (c) => this._onWorkerExit(c));
@@ -218,6 +230,10 @@ class SttEngine {
218
230
  const onError = (err) => {
219
231
  worker.off('message', onReady);
220
232
  worker.off('error', onError);
233
+ // Tag dependency errors so _onWorkerExit can skip futile retries
234
+ if (err.code === 'MODULE_NOT_FOUND' || (err.message && err.message.includes('sherpa-onnx-node'))) {
235
+ this._lastSpawnError = 'MODULE_NOT_FOUND';
236
+ }
221
237
  reject(err);
222
238
  };
223
239
 
package/src/stt-worker.js CHANGED
@@ -29,7 +29,16 @@ if (os.platform() === 'win32') {
29
29
  }
30
30
 
31
31
  // Now safe to require sherpa-onnx-node
32
- const { OfflineRecognizer } = require('sherpa-onnx-node');
32
+ let OfflineRecognizer;
33
+ try {
34
+ ({ OfflineRecognizer } = require('sherpa-onnx-node'));
35
+ } catch (err) {
36
+ parentPort.postMessage({
37
+ type: 'error',
38
+ message: `sherpa-onnx-node is not installed. Install it with: npm install sherpa-onnx-node\n(Original error: ${err.message})`
39
+ });
40
+ process.exit(1);
41
+ }
33
42
 
34
43
  const modelDir = workerData.modelDir;
35
44
  const numThreads = workerData.numThreads || Math.min(4, os.cpus().length);
package/CHANGELOG.md DELETED
@@ -1,25 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [0.1.0] - 2025-02-06
9
-
10
- ### Added
11
- - Multi-tool support: Claude, GitHub Copilot, Google Gemini, OpenAI Codex, and raw Terminal
12
- - BaseBridge architecture for cross-platform CLI tool management
13
- - Dynamic tool card UI — automatically detects installed tools
14
- - Microsoft Dev Tunnels integration for secure remote access
15
- - Real-time terminal streaming via xterm.js and WebSocket
16
- - Multi-session support with persistent tabs
17
- - Token-based authentication (auto-generated by default)
18
- - Session persistence across server restarts
19
- - Cross-platform support (Linux and Windows via ConPTY)
20
- - Progressive Web App (PWA) with installable manifest
21
- - Mobile-responsive design with touch-optimized controls
22
- - Comprehensive documentation in docs/ (architecture, specs, ADRs, agent instructions)
23
- - AI agent infrastructure with 5 specialized personas
24
- - CI/CD pipeline with cross-platform test matrix
25
- - Validation scripts for Linux and Windows