ai-or-die 0.1.13 → 0.1.15

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.
@@ -35,7 +35,9 @@ Every bridge implements the same public API:
35
35
  ```js
36
36
  {
37
37
  workingDir: string, // defaults to process.cwd()
38
- dangerouslySkipPermissions: boolean, // defaults to false (Claude/Codex only)
38
+ dangerouslySkipPermissions: boolean, // defaults to false (Claude, Codex, Copilot, Gemini)
39
+ cols: number, // defaults to 80
40
+ rows: number, // defaults to 24
39
41
  onOutput: (data: string) => void,
40
42
  onExit: (code: number, signal: number) => void,
41
43
  onError: (error: Error) => void,
@@ -188,23 +190,26 @@ Each concrete bridge would extend `BaseBridge` and provide:
188
190
  - `buildArgs(options)` -- returns the CLI arguments array
189
191
  - `onDataHook(data, buffer)` -- optional per-bridge output processing (e.g., trust prompt handling)
190
192
 
191
- ### CopilotBridge (Planned)
193
+ ### CopilotBridge
192
194
 
193
195
  - Command: `copilot`
194
- - Installation: `npm install -g @github/copilot`
196
+ - Installation: `npm install -g @github/copilot`, `winget install GitHub.Copilot`
197
+ - Dangerous flag: `--yolo` (auto-approve all tool executions)
195
198
  - Search paths: standard locations following the same pattern as existing bridges
196
199
 
197
- ### GeminiBridge (Planned)
200
+ ### GeminiBridge
198
201
 
199
202
  - Command: `gemini`
200
203
  - Installation: `npm install -g @google/gemini-cli`
204
+ - Dangerous flag: `--yolo` (disable sandbox, auto-approve commands)
201
205
  - Search paths: standard locations following the same pattern as existing bridges
202
206
 
203
- ### TerminalBridge (Planned)
207
+ ### TerminalBridge
204
208
 
205
209
  Opens a raw shell session rather than an AI agent.
206
210
 
207
211
  - Linux/macOS: spawns `$SHELL` (defaults to `/bin/bash`)
208
- - Windows: spawns `powershell.exe` or `cmd.exe`
212
+ - Windows: spawns PowerShell 7 (`pwsh`) or falls back to `$COMSPEC`
213
+ - Async shell resolution via `resolveFullPathAsync()`
209
214
  - No `dangerouslySkipPermissions` or AI-specific flags
210
215
  - Useful for running manual commands alongside agent sessions
@@ -190,6 +190,7 @@ function sendAndWait(ws, message, expectedType, timeoutMs) { ... }
190
190
  | Stop terminal session | Send `stop` -> receive `exit` message |
191
191
  | Cannot start two tools in same session | Start terminal, then start terminal again -> receive `error` |
192
192
  | Echo unique marker through terminal (cross-platform) | Start terminal -> drain initial output -> send `echo MARKER` -> verify marker in collected output -> stop |
193
+ | Start terminal with custom cols/rows | Send `start_terminal` with cols=132, rows=50 -> verify terminal starts and responds to echo |
193
194
 
194
195
  ### 3.6 Input/Output Round-Trip (`describe('I/O round-trip')`)
195
196
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-or-die",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Universal AI coding terminal — Claude, Copilot, Gemini & more in your browser",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -198,20 +198,17 @@ class BaseBridge {
198
198
 
199
199
  const env = {
200
200
  ...process.env,
201
- TERM: this.isWindows ? 'xterm' : 'xterm-256color',
202
- FORCE_COLOR: '1'
201
+ TERM: 'xterm-256color',
202
+ FORCE_COLOR: '1',
203
+ COLORTERM: 'truecolor'
203
204
  };
204
- if (!this.isWindows) {
205
- env.COLORTERM = 'truecolor';
206
- }
207
205
 
208
206
  const ptyProcess = spawn(this.command, args, {
209
207
  cwd: workingDir,
210
208
  env,
211
209
  cols,
212
210
  rows,
213
- name: this.isWindows ? 'xterm' : 'xterm-color',
214
- useConpty: this.isWindows
211
+ name: 'xterm-256color'
215
212
  });
216
213
 
217
214
  const session = {
@@ -238,6 +235,8 @@ class BaseBridge {
238
235
  }, SPAWN_TIMEOUT_MS);
239
236
 
240
237
  let dataBuffer = '';
238
+ let outputBatch = '';
239
+ let flushTimer = null;
241
240
 
242
241
  ptyProcess.onData((data) => {
243
242
  if (!receivedLifeSign) {
@@ -258,7 +257,17 @@ class BaseBridge {
258
257
  dataBuffer = dataBuffer.slice(-5000);
259
258
  }
260
259
 
261
- onOutput(data);
260
+ // Batch output: coalesce PTY data chunks from the same I/O cycle
261
+ // setImmediate flushes on the next tick — no arbitrary time boundary
262
+ // that could split ANSI escape sequences or multi-byte UTF-8 characters
263
+ outputBatch += data;
264
+ if (!flushTimer) {
265
+ flushTimer = setImmediate(() => {
266
+ onOutput(outputBatch);
267
+ outputBatch = '';
268
+ flushTimer = null;
269
+ });
270
+ }
262
271
  });
263
272
 
264
273
  ptyProcess.onExit((exitCode, signal) => {
@@ -21,7 +21,8 @@ class CopilotBridge extends BaseBridge {
21
21
  '{HOME}\\AppData\\Roaming\\npm\\copilot'
22
22
  ]
23
23
  },
24
- defaultCommand: 'copilot'
24
+ defaultCommand: 'copilot',
25
+ dangerousFlag: '--yolo'
25
26
  });
26
27
  }
27
28
  }
@@ -18,7 +18,8 @@ class GeminiBridge extends BaseBridge {
18
18
  '{HOME}\\AppData\\Roaming\\npm\\gemini'
19
19
  ]
20
20
  },
21
- defaultCommand: 'gemini'
21
+ defaultCommand: 'gemini',
22
+ dangerousFlag: '--yolo'
22
23
  });
23
24
  }
24
25
  }
package/src/public/app.js CHANGED
@@ -338,6 +338,7 @@ class ClaudeCodeWebInterface {
338
338
 
339
339
  this.terminal.open(document.getElementById('terminal'));
340
340
  this.fitTerminal();
341
+ this.setupTerminalContextMenu();
341
342
 
342
343
  this.terminal.onData((data) => {
343
344
  if (this.socket && this.socket.readyState === WebSocket.OPEN) {
@@ -912,7 +913,12 @@ class ClaudeCodeWebInterface {
912
913
  }
913
914
  }, 45000);
914
915
 
915
- this.send({ type: `start_${toolId}`, options });
916
+ this.send({
917
+ type: `start_${toolId}`,
918
+ options,
919
+ cols: this.terminal ? this.terminal.cols : 80,
920
+ rows: this.terminal ? this.terminal.rows : 24
921
+ });
916
922
  }
917
923
 
918
924
  clearTerminal() {
@@ -937,7 +943,13 @@ class ClaudeCodeWebInterface {
937
943
  if (this.fitAddon) {
938
944
  try {
939
945
  this.fitAddon.fit();
940
-
946
+
947
+ // Subtract 2 rows to account for tab bar / header not included in fit calculation
948
+ const adjustedRows = Math.max(1, this.terminal.rows - 2);
949
+ if (adjustedRows !== this.terminal.rows) {
950
+ this.terminal.resize(this.terminal.cols, adjustedRows);
951
+ }
952
+
941
953
  // On mobile, ensure terminal doesn't exceed viewport width
942
954
  if (this.isMobile) {
943
955
  const terminalElement = document.querySelector('.xterm');
@@ -959,6 +971,66 @@ class ClaudeCodeWebInterface {
959
971
  }
960
972
  }
961
973
 
974
+ setupTerminalContextMenu() {
975
+ const menu = document.getElementById('termContextMenu');
976
+ if (!menu) return;
977
+
978
+ const termEl = document.getElementById('terminal');
979
+ termEl.addEventListener('contextmenu', (e) => {
980
+ e.preventDefault();
981
+ e.stopPropagation();
982
+
983
+ // Position menu at cursor
984
+ menu.style.left = e.clientX + 'px';
985
+ menu.style.top = e.clientY + 'px';
986
+ menu.style.display = 'block';
987
+
988
+ // Disable copy if no selection
989
+ const copyItem = menu.querySelector('[data-action="copy"]');
990
+ if (copyItem) {
991
+ const hasSelection = this.terminal.hasSelection();
992
+ copyItem.classList.toggle('disabled', !hasSelection);
993
+ }
994
+ });
995
+
996
+ // Handle menu item clicks
997
+ menu.addEventListener('click', async (e) => {
998
+ const action = e.target.dataset.action;
999
+ if (!action) return;
1000
+ menu.style.display = 'none';
1001
+
1002
+ switch (action) {
1003
+ case 'copy': {
1004
+ const sel = this.terminal.getSelection();
1005
+ if (sel) await navigator.clipboard.writeText(sel);
1006
+ break;
1007
+ }
1008
+ case 'paste': {
1009
+ try {
1010
+ const text = await navigator.clipboard.readText();
1011
+ if (text && this.socket && this.socket.readyState === WebSocket.OPEN) {
1012
+ this.send({ type: 'input', data: text });
1013
+ }
1014
+ } catch { /* clipboard permission denied */ }
1015
+ break;
1016
+ }
1017
+ case 'selectAll':
1018
+ this.terminal.selectAll();
1019
+ break;
1020
+ case 'clear':
1021
+ this.terminal.clear();
1022
+ break;
1023
+ }
1024
+ this.terminal.focus();
1025
+ });
1026
+
1027
+ // Dismiss menu on click outside or scroll
1028
+ document.addEventListener('click', (e) => {
1029
+ if (!menu.contains(e.target)) menu.style.display = 'none';
1030
+ });
1031
+ document.addEventListener('keydown', () => { menu.style.display = 'none'; });
1032
+ }
1033
+
962
1034
  updateStatus(status) {
963
1035
  // Status display removed with header - status now shown in tabs
964
1036
  console.log('Status:', status);
@@ -1011,6 +1083,7 @@ class ClaudeCodeWebInterface {
1011
1083
  const themeSelect = document.getElementById('themeSelect');
1012
1084
  if (themeSelect) themeSelect.value = settings.theme === 'light' ? 'light' : 'dark';
1013
1085
  document.getElementById('showTokenStats').checked = settings.showTokenStats;
1086
+ document.getElementById('dangerousMode').checked = settings.dangerousMode || false;
1014
1087
  }
1015
1088
 
1016
1089
  hideSettings() {
@@ -1026,7 +1099,8 @@ class ClaudeCodeWebInterface {
1026
1099
  const defaults = {
1027
1100
  fontSize: 14,
1028
1101
  showTokenStats: true,
1029
- theme: 'dark'
1102
+ theme: 'dark',
1103
+ dangerousMode: false
1030
1104
  };
1031
1105
 
1032
1106
  try {
@@ -1042,7 +1116,8 @@ class ClaudeCodeWebInterface {
1042
1116
  const settings = {
1043
1117
  fontSize: parseInt(document.getElementById('fontSize').value),
1044
1118
  showTokenStats: document.getElementById('showTokenStats').checked,
1045
- theme: (document.getElementById('themeSelect')?.value) || 'dark'
1119
+ theme: (document.getElementById('themeSelect')?.value) || 'dark',
1120
+ dangerousMode: document.getElementById('dangerousMode').checked
1046
1121
  };
1047
1122
 
1048
1123
  try {
@@ -107,6 +107,13 @@
107
107
  <div class="terminal-container" id="terminalContainer" data-view="single">
108
108
  <div class="terminal-wrapper">
109
109
  <div id="terminal"></div>
110
+ <div id="termContextMenu" class="term-context-menu" style="display:none">
111
+ <div class="ctx-item" data-action="copy">Copy</div>
112
+ <div class="ctx-item" data-action="paste">Paste</div>
113
+ <div class="ctx-sep"></div>
114
+ <div class="ctx-item" data-action="selectAll">Select All</div>
115
+ <div class="ctx-item" data-action="clear">Clear</div>
116
+ </div>
110
117
  </div>
111
118
  </div>
112
119
  </main>
@@ -156,6 +163,11 @@
156
163
  <label for="showTokenStats">Show Token Stats:</label>
157
164
  <input type="checkbox" id="showTokenStats" checked>
158
165
  </div>
166
+ <div class="setting-group">
167
+ <label for="dangerousMode">Autonomous Mode:</label>
168
+ <input type="checkbox" id="dangerousMode">
169
+ <small style="display:block;color:#e8a838;margin-top:4px">Skips permission prompts for Claude, Copilot, Gemini, Codex. Use in trusted environments only.</small>
170
+ </div>
159
171
  </div>
160
172
  <div class="modal-footer">
161
173
  <button class="btn btn-primary" id="saveSettingsBtn">Save Settings</button>
@@ -1260,6 +1260,37 @@ body {
1260
1260
  background: var(--border-hover);
1261
1261
  }
1262
1262
 
1263
+ /* Terminal context menu */
1264
+ .term-context-menu {
1265
+ position: fixed;
1266
+ z-index: 1000;
1267
+ background: var(--bg-secondary);
1268
+ border: 1px solid var(--border);
1269
+ border-radius: 8px;
1270
+ padding: 4px 0;
1271
+ min-width: 140px;
1272
+ box-shadow: 0 8px 24px rgba(0,0,0,.4);
1273
+ }
1274
+ .ctx-item {
1275
+ padding: 6px 16px;
1276
+ cursor: pointer;
1277
+ font-size: 13px;
1278
+ color: var(--text-primary);
1279
+ }
1280
+ .ctx-item:hover {
1281
+ background: var(--accent);
1282
+ color: #fff;
1283
+ }
1284
+ .ctx-item.disabled {
1285
+ opacity: .4;
1286
+ pointer-events: none;
1287
+ }
1288
+ .ctx-sep {
1289
+ height: 1px;
1290
+ background: var(--border);
1291
+ margin: 4px 0;
1292
+ }
1293
+
1263
1294
  /* Folder Browser Modal */
1264
1295
  .folder-browser-modal {
1265
1296
  display: none;
package/src/server.js CHANGED
@@ -94,9 +94,7 @@ class ClaudeCodeWebServer {
94
94
  }
95
95
 
96
96
  async saveSessionsToDisk() {
97
- if (this.claudeSessions.size > 0) {
98
- await this.sessionStore.saveSessions(this.claudeSessions);
99
- }
97
+ await this.sessionStore.saveSessions(this.claudeSessions);
100
98
  }
101
99
 
102
100
  async handleShutdown() {
@@ -345,7 +343,7 @@ class ClaudeCodeWebServer {
345
343
  });
346
344
 
347
345
  // Delete a Claude session
348
- this.app.delete('/api/sessions/:sessionId', (req, res) => {
346
+ this.app.delete('/api/sessions/:sessionId', async (req, res) => {
349
347
  const sessionId = req.params.sessionId;
350
348
  const session = this.claudeSessions.get(sessionId);
351
349
 
@@ -374,10 +372,10 @@ class ClaudeCodeWebServer {
374
372
  });
375
373
 
376
374
  this.claudeSessions.delete(sessionId);
377
-
378
- // Save sessions after deletion
379
- this.saveSessionsToDisk();
380
-
375
+
376
+ // Save sessions after deletion — await to ensure persistence
377
+ await this.saveSessionsToDisk();
378
+
381
379
  res.json({ success: true, message: 'Session deleted' });
382
380
  });
383
381
 
@@ -390,8 +388,8 @@ class ClaudeCodeWebServer {
390
388
  tools: {
391
389
  claude: { alias: this.aliases.claude, available: this.claudeBridge.isAvailable(), hasDangerousMode: true },
392
390
  codex: { alias: this.aliases.codex, available: this.codexBridge.isAvailable(), hasDangerousMode: true },
393
- copilot: { alias: this.aliases.copilot, available: this.copilotBridge.isAvailable(), hasDangerousMode: false },
394
- gemini: { alias: this.aliases.gemini, available: this.geminiBridge.isAvailable(), hasDangerousMode: false },
391
+ copilot: { alias: this.aliases.copilot, available: this.copilotBridge.isAvailable(), hasDangerousMode: true },
392
+ gemini: { alias: this.aliases.gemini, available: this.geminiBridge.isAvailable(), hasDangerousMode: true },
395
393
  terminal: { alias: this.aliases.terminal, available: this.terminalBridge.isAvailable(), hasDangerousMode: false }
396
394
  }
397
395
  });
@@ -645,8 +643,15 @@ class ClaudeCodeWebServer {
645
643
  server = http.createServer(this.app);
646
644
  }
647
645
 
648
- this.wss = new WebSocket.Server({
646
+ this.wss = new WebSocket.Server({
649
647
  server,
648
+ perMessageDeflate: {
649
+ serverNoContextTakeover: true,
650
+ clientNoContextTakeover: true,
651
+ serverMaxWindowBits: 13,
652
+ clientMaxWindowBits: 13,
653
+ zlibDeflateOptions: { level: 6 }
654
+ },
650
655
  verifyClient: (info) => {
651
656
  if (!this.noAuth && this.auth) {
652
657
  const url = new URL(info.req.url, 'ws://localhost');
@@ -753,19 +758,19 @@ class ClaudeCodeWebServer {
753
758
  break;
754
759
 
755
760
  case 'start_claude':
756
- await this.startToolSession(wsId, 'claude', this.claudeBridge, data.options || {});
761
+ await this.startToolSession(wsId, 'claude', this.claudeBridge, data.options || {}, data.cols, data.rows);
757
762
  break;
758
763
  case 'start_codex':
759
- await this.startToolSession(wsId, 'codex', this.codexBridge, data.options || {});
764
+ await this.startToolSession(wsId, 'codex', this.codexBridge, data.options || {}, data.cols, data.rows);
760
765
  break;
761
766
  case 'start_copilot':
762
- await this.startToolSession(wsId, 'copilot', this.copilotBridge, data.options || {});
767
+ await this.startToolSession(wsId, 'copilot', this.copilotBridge, data.options || {}, data.cols, data.rows);
763
768
  break;
764
769
  case 'start_gemini':
765
- await this.startToolSession(wsId, 'gemini', this.geminiBridge, data.options || {});
770
+ await this.startToolSession(wsId, 'gemini', this.geminiBridge, data.options || {}, data.cols, data.rows);
766
771
  break;
767
772
  case 'start_terminal':
768
- await this.startToolSession(wsId, 'terminal', this.terminalBridge, data.options || {});
773
+ await this.startToolSession(wsId, 'terminal', this.terminalBridge, data.options || {}, data.cols, data.rows);
769
774
  break;
770
775
 
771
776
  case 'input':
@@ -969,7 +974,7 @@ class ClaudeCodeWebServer {
969
974
  return bridges[agentType] || null;
970
975
  }
971
976
 
972
- async startToolSession(wsId, toolName, bridge, options) {
977
+ async startToolSession(wsId, toolName, bridge, options, cols, rows) {
973
978
  const wsInfo = this.webSocketConnections.get(wsId);
974
979
  if (!wsInfo) {
975
980
  console.warn(`startToolSession(${toolName}): wsInfo not found for wsId=${wsId}`);
@@ -1021,6 +1026,8 @@ class ClaudeCodeWebServer {
1021
1026
  console.log(`startToolSession(${toolName}): spawning in session ${sessionId}, workingDir=${session.workingDir}`);
1022
1027
  await bridge.startSession(sessionId, {
1023
1028
  workingDir: session.workingDir,
1029
+ cols: cols || 80,
1030
+ rows: rows || 24,
1024
1031
  onOutput: (data) => {
1025
1032
  const currentSession = this.claudeSessions.get(sessionId);
1026
1033
  if (!currentSession) return;