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.
- package/docs/specs/bridges.md +11 -6
- package/docs/specs/e2e-testing.md +1 -0
- package/package.json +1 -1
- package/src/base-bridge.js +17 -8
- package/src/copilot-bridge.js +2 -1
- package/src/gemini-bridge.js +2 -1
- package/src/public/app.js +79 -4
- package/src/public/index.html +12 -0
- package/src/public/style.css +31 -0
- package/src/server.js +24 -17
package/docs/specs/bridges.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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
package/src/base-bridge.js
CHANGED
|
@@ -198,20 +198,17 @@ class BaseBridge {
|
|
|
198
198
|
|
|
199
199
|
const env = {
|
|
200
200
|
...process.env,
|
|
201
|
-
TERM:
|
|
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:
|
|
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
|
-
|
|
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) => {
|
package/src/copilot-bridge.js
CHANGED
package/src/gemini-bridge.js
CHANGED
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({
|
|
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 {
|
package/src/public/index.html
CHANGED
|
@@ -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>
|
package/src/public/style.css
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
394
|
-
gemini: { alias: this.aliases.gemini, available: this.geminiBridge.isAvailable(), hasDangerousMode:
|
|
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;
|