ai-or-die 0.1.0
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/.cursor/commands/commit-push.md +18 -0
- package/.github/agents/architect.md +26 -0
- package/.github/agents/engineer.md +29 -0
- package/.github/agents/qa-reviewer.md +31 -0
- package/.github/agents/researcher.md +30 -0
- package/.github/agents/troubleshooter.md +33 -0
- package/.github/copilot-instructions.md +55 -0
- package/.github/pull_request_template.md +21 -0
- package/.github/workflows/build-binaries.yml +76 -0
- package/.github/workflows/ci.yml +70 -0
- package/.github/workflows/release-on-main.yml +73 -0
- package/.prompts/log.md +9 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +25 -0
- package/CLAUDE.md +130 -0
- package/CONTRIBUTING.md +76 -0
- package/LICENSE +22 -0
- package/README.md +165 -0
- package/bin/ai-or-die.js +203 -0
- package/docs/.nojekyll +1 -0
- package/docs/README.md +37 -0
- package/docs/adrs/0000-template.md +35 -0
- package/docs/adrs/0001-bridge-base-class.md +53 -0
- package/docs/adrs/0002-devtunnels-over-ngrok.md +56 -0
- package/docs/adrs/0003-multi-tool-architecture.md +71 -0
- package/docs/adrs/0004-cross-platform-support.md +101 -0
- package/docs/adrs/0005-single-binary-distribution.md +58 -0
- package/docs/agent-instructions/00-philosophy.md +55 -0
- package/docs/agent-instructions/01-research-and-web.md +49 -0
- package/docs/agent-instructions/02-testing-and-validation.md +63 -0
- package/docs/agent-instructions/03-tooling-and-pipelines.md +59 -0
- package/docs/architecture/bridge-pattern.md +510 -0
- package/docs/architecture/overview.md +216 -0
- package/docs/architecture/websocket-protocol.md +609 -0
- package/docs/history/README.md +26 -0
- package/docs/specs/authentication.md +167 -0
- package/docs/specs/bridges.md +210 -0
- package/docs/specs/client-app.md +308 -0
- package/docs/specs/e2e-testing.md +311 -0
- package/docs/specs/server.md +334 -0
- package/docs/specs/session-store.md +170 -0
- package/docs/specs/usage-analytics.md +342 -0
- package/nul +0 -0
- package/package.json +54 -0
- package/scripts/build-sea.js +187 -0
- package/scripts/pty-sea-shim.js +21 -0
- package/scripts/publish-both.sh +21 -0
- package/scripts/release-pr.sh +73 -0
- package/scripts/smoke-test-binary.js +190 -0
- package/scripts/validate.ps1 +25 -0
- package/scripts/validate.sh +16 -0
- package/sea-bootstrap.js +54 -0
- package/site/ADVANCED_ANALYTICS.md +174 -0
- package/site/index.html +151 -0
- package/site/script.js +17 -0
- package/site/style.css +60 -0
- package/src/base-bridge.js +340 -0
- package/src/claude-bridge.js +48 -0
- package/src/codex-bridge.js +27 -0
- package/src/copilot-bridge.js +29 -0
- package/src/gemini-bridge.js +26 -0
- package/src/public/app.js +2123 -0
- package/src/public/auth.js +244 -0
- package/src/public/icon-generator.js +26 -0
- package/src/public/icons.js +36 -0
- package/src/public/index.html +397 -0
- package/src/public/manifest.json +45 -0
- package/src/public/plan-detector.js +186 -0
- package/src/public/service-worker.js +108 -0
- package/src/public/session-manager.js +1124 -0
- package/src/public/splits.js +574 -0
- package/src/public/style.css +2090 -0
- package/src/server.js +1269 -0
- package/src/terminal-bridge.js +49 -0
- package/src/usage-analytics.js +494 -0
- package/src/usage-reader.js +895 -0
- package/src/utils/auth.js +123 -0
- package/src/utils/session-store.js +181 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# Bridge Pattern
|
|
2
|
+
|
|
3
|
+
The bridge pattern is the core extensible architecture that allows ai-or-die to support multiple CLI tools through a uniform interface. Each CLI tool is wrapped in a bridge class that manages process spawning, I/O streaming, terminal resizing, and graceful shutdown.
|
|
4
|
+
|
|
5
|
+
## Bridge Class Structure
|
|
6
|
+
|
|
7
|
+
Every bridge class follows the same structural pattern. The codebase currently has three implementations:
|
|
8
|
+
|
|
9
|
+
- **ClaudeBridge** (`src/claude-bridge.js`) -- wraps the `claude` CLI
|
|
10
|
+
- **CodexBridge** (`src/codex-bridge.js`) -- wraps the `codex` CLI
|
|
11
|
+
- **AgentBridge** (`src/agent-bridge.js`) -- wraps the `cursor-agent` CLI
|
|
12
|
+
|
|
13
|
+
All three share an identical public interface:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
class Bridge {
|
|
17
|
+
constructor() // Find the CLI command on disk
|
|
18
|
+
findCommand() -> string // Platform-aware command discovery
|
|
19
|
+
commandExists(command) -> boolean // Check if command is in PATH
|
|
20
|
+
startSession(sessionId, options) -> session
|
|
21
|
+
sendInput(sessionId, data) -> void
|
|
22
|
+
resize(sessionId, cols, rows) -> void
|
|
23
|
+
stopSession(sessionId) -> void
|
|
24
|
+
getSession(sessionId) -> session | undefined
|
|
25
|
+
getAllSessions() -> session[]
|
|
26
|
+
cleanup() -> void // Stop all sessions
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Internal State
|
|
31
|
+
|
|
32
|
+
Each bridge maintains a `Map<sessionId, session>` where a session object contains:
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
{
|
|
36
|
+
process: ptyProcess, // The node-pty IPty instance
|
|
37
|
+
workingDir: string, // Working directory the process was spawned in
|
|
38
|
+
created: Date, // When the session was created
|
|
39
|
+
active: boolean, // Whether the process is still running
|
|
40
|
+
killTimeout: number // Timer ID for the SIGKILL escalation
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How to Add a New CLI Tool
|
|
45
|
+
|
|
46
|
+
Adding a new CLI tool requires changes in four places. This section walks through adding a hypothetical "Gemini" CLI as an example.
|
|
47
|
+
|
|
48
|
+
### Step 1: Create the Bridge
|
|
49
|
+
|
|
50
|
+
Create `src/gemini-bridge.js` following the established pattern:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
const { spawn } = require('node-pty');
|
|
54
|
+
const path = require('path');
|
|
55
|
+
const fs = require('fs');
|
|
56
|
+
|
|
57
|
+
class GeminiBridge {
|
|
58
|
+
constructor() {
|
|
59
|
+
this.sessions = new Map();
|
|
60
|
+
this.geminiCommand = this.findGeminiCommand();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
findGeminiCommand() {
|
|
64
|
+
const possibleCommands = [
|
|
65
|
+
path.join(process.env.HOME || '/', '.gemini', 'local', 'gemini'),
|
|
66
|
+
'gemini',
|
|
67
|
+
path.join(process.env.HOME || '/', '.local', 'bin', 'gemini'),
|
|
68
|
+
'/usr/local/bin/gemini',
|
|
69
|
+
'/usr/bin/gemini'
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
for (const cmd of possibleCommands) {
|
|
73
|
+
try {
|
|
74
|
+
if (fs.existsSync(cmd) || this.commandExists(cmd)) {
|
|
75
|
+
console.log(`Found Gemini command at: ${cmd}`);
|
|
76
|
+
return cmd;
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.error('Gemini command not found, using default "gemini"');
|
|
84
|
+
return 'gemini';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
commandExists(command) {
|
|
88
|
+
try {
|
|
89
|
+
require('child_process').execFileSync('which', [command], { stdio: 'ignore' });
|
|
90
|
+
return true;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async startSession(sessionId, options = {}) {
|
|
97
|
+
if (this.sessions.has(sessionId)) {
|
|
98
|
+
throw new Error(`Session ${sessionId} already exists`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const {
|
|
102
|
+
workingDir = process.cwd(),
|
|
103
|
+
onOutput = () => {},
|
|
104
|
+
onExit = () => {},
|
|
105
|
+
onError = () => {},
|
|
106
|
+
cols = 80,
|
|
107
|
+
rows = 24
|
|
108
|
+
} = options;
|
|
109
|
+
|
|
110
|
+
const args = []; // Add any CLI-specific flags here
|
|
111
|
+
const geminiProcess = spawn(this.geminiCommand, args, {
|
|
112
|
+
cwd: workingDir,
|
|
113
|
+
env: {
|
|
114
|
+
...process.env,
|
|
115
|
+
TERM: 'xterm-256color',
|
|
116
|
+
FORCE_COLOR: '1',
|
|
117
|
+
COLORTERM: 'truecolor'
|
|
118
|
+
},
|
|
119
|
+
cols,
|
|
120
|
+
rows,
|
|
121
|
+
name: 'xterm-color'
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const session = {
|
|
125
|
+
process: geminiProcess,
|
|
126
|
+
workingDir,
|
|
127
|
+
created: new Date(),
|
|
128
|
+
active: true,
|
|
129
|
+
killTimeout: null
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
this.sessions.set(sessionId, session);
|
|
133
|
+
|
|
134
|
+
let dataBuffer = '';
|
|
135
|
+
|
|
136
|
+
geminiProcess.onData((data) => {
|
|
137
|
+
dataBuffer += data;
|
|
138
|
+
if (dataBuffer.length > 10000) {
|
|
139
|
+
dataBuffer = dataBuffer.slice(-5000);
|
|
140
|
+
}
|
|
141
|
+
onOutput(data);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
geminiProcess.onExit((exitCode, signal) => {
|
|
145
|
+
if (session.killTimeout) {
|
|
146
|
+
clearTimeout(session.killTimeout);
|
|
147
|
+
session.killTimeout = null;
|
|
148
|
+
}
|
|
149
|
+
session.active = false;
|
|
150
|
+
this.sessions.delete(sessionId);
|
|
151
|
+
onExit(exitCode, signal);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
geminiProcess.on('error', (error) => {
|
|
155
|
+
if (session.killTimeout) {
|
|
156
|
+
clearTimeout(session.killTimeout);
|
|
157
|
+
session.killTimeout = null;
|
|
158
|
+
}
|
|
159
|
+
session.active = false;
|
|
160
|
+
this.sessions.delete(sessionId);
|
|
161
|
+
onError(error);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return session;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async sendInput(sessionId, data) {
|
|
168
|
+
const session = this.sessions.get(sessionId);
|
|
169
|
+
if (!session || !session.active) {
|
|
170
|
+
throw new Error(`Session ${sessionId} not found or not active`);
|
|
171
|
+
}
|
|
172
|
+
session.process.write(data);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async resize(sessionId, cols, rows) {
|
|
176
|
+
const session = this.sessions.get(sessionId);
|
|
177
|
+
if (!session || !session.active) {
|
|
178
|
+
throw new Error(`Session ${sessionId} not found or not active`);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
session.process.resize(cols, rows);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.warn(`Failed to resize session ${sessionId}:`, error.message);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async stopSession(sessionId) {
|
|
188
|
+
const session = this.sessions.get(sessionId);
|
|
189
|
+
if (!session) return;
|
|
190
|
+
|
|
191
|
+
if (session.killTimeout) {
|
|
192
|
+
clearTimeout(session.killTimeout);
|
|
193
|
+
session.killTimeout = null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (session.active && session.process) {
|
|
197
|
+
session.process.kill('SIGTERM');
|
|
198
|
+
session.killTimeout = setTimeout(() => {
|
|
199
|
+
if (session.active && session.process) {
|
|
200
|
+
session.process.kill('SIGKILL');
|
|
201
|
+
}
|
|
202
|
+
}, 5000);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
session.active = false;
|
|
206
|
+
this.sessions.delete(sessionId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getSession(sessionId) {
|
|
210
|
+
return this.sessions.get(sessionId);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
getAllSessions() {
|
|
214
|
+
return Array.from(this.sessions.entries()).map(([id, session]) => ({
|
|
215
|
+
id,
|
|
216
|
+
workingDir: session.workingDir,
|
|
217
|
+
created: session.created,
|
|
218
|
+
active: session.active
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async cleanup() {
|
|
223
|
+
for (const sessionId of this.sessions.keys()) {
|
|
224
|
+
await this.stopSession(sessionId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = GeminiBridge;
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Step 2: Register the Bridge in the Server
|
|
233
|
+
|
|
234
|
+
In `src/server.js`, import and instantiate the bridge in the `ClaudeCodeWebServer` constructor:
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
const GeminiBridge = require('./gemini-bridge');
|
|
238
|
+
|
|
239
|
+
// Inside constructor:
|
|
240
|
+
this.geminiBridge = new GeminiBridge();
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Step 3: Add WebSocket Message Handlers
|
|
244
|
+
|
|
245
|
+
Add three things to the server's message handling:
|
|
246
|
+
|
|
247
|
+
**a) A new `start_gemini` case in `handleMessage()`:**
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
case 'start_gemini':
|
|
251
|
+
await this.startGemini(wsId, data.options || {});
|
|
252
|
+
break;
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**b) Route `input`, `resize`, and `stop` to the new bridge when `session.agent === 'gemini'`:**
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
// In the 'input' handler:
|
|
259
|
+
if (session.agent === 'gemini') {
|
|
260
|
+
await this.geminiBridge.sendInput(wsInfo.claudeSessionId, data.data);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// In the 'resize' handler:
|
|
264
|
+
if (session.agent === 'gemini') {
|
|
265
|
+
await this.geminiBridge.resize(wsInfo.claudeSessionId, data.cols, data.rows);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// In the 'stop' handler:
|
|
269
|
+
if (session?.agent === 'gemini') {
|
|
270
|
+
await this.stopGemini(wsInfo.claudeSessionId);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**c) Implement `startGemini()` and `stopGemini()` methods** following the same pattern as `startClaude()`/`stopClaude()`. The key details:
|
|
275
|
+
|
|
276
|
+
- Set `session.agent = 'gemini'` when starting
|
|
277
|
+
- Broadcast `{ type: 'gemini_started', sessionId }` on success
|
|
278
|
+
- Broadcast `{ type: 'gemini_stopped' }` on stop
|
|
279
|
+
- Wire up `onOutput`, `onExit`, and `onError` callbacks
|
|
280
|
+
|
|
281
|
+
**d) Handle cleanup in `close()`:**
|
|
282
|
+
|
|
283
|
+
```javascript
|
|
284
|
+
if (session.agent === 'gemini') {
|
|
285
|
+
this.geminiBridge.stopSession(sessionId);
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Step 4: Add UI Card
|
|
290
|
+
|
|
291
|
+
In the client-side JavaScript (`src/public/app.js`), add a launch card for the new tool. The client sends `{ type: 'start_gemini', options: {} }` over the WebSocket and listens for `gemini_started` and `gemini_stopped` messages.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Platform-Aware Command Resolution
|
|
296
|
+
|
|
297
|
+
### Search Strategy
|
|
298
|
+
|
|
299
|
+
Each bridge's `findCommand()` method tries multiple locations in order:
|
|
300
|
+
|
|
301
|
+
1. **Tool-specific home directory** -- e.g., `~/.claude/local/claude`, `~/.codex/local/codex`
|
|
302
|
+
2. **Bare command name** -- relies on `PATH` resolution (e.g., `claude`, `codex`)
|
|
303
|
+
3. **Alternative names** -- e.g., `claude-code` as a fallback for `claude`
|
|
304
|
+
4. **User-local bin** -- `~/.local/bin/{command}`
|
|
305
|
+
5. **System-wide locations** -- `/usr/local/bin/{command}`, `/usr/bin/{command}`
|
|
306
|
+
|
|
307
|
+
### PATH Lookup
|
|
308
|
+
|
|
309
|
+
The `commandExists()` method uses `which` to check if a command is available in the system PATH:
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
commandExists(command) {
|
|
313
|
+
try {
|
|
314
|
+
require('child_process').execFileSync('which', [command], { stdio: 'ignore' });
|
|
315
|
+
return true;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
On Windows, this would need to use `where` instead of `which`. The current implementation targets Linux and macOS.
|
|
323
|
+
|
|
324
|
+
### Home Directory Resolution
|
|
325
|
+
|
|
326
|
+
All bridges use `process.env.HOME` for home directory resolution, with a fallback to `/`:
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
path.join(process.env.HOME || '/', '.claude', 'local', 'claude')
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
On Windows systems, `os.homedir()` would be the more reliable approach.
|
|
333
|
+
|
|
334
|
+
### Fallback Behavior
|
|
335
|
+
|
|
336
|
+
If no command is found at any of the searched locations, the bridge falls back to the bare command name (e.g., `'claude'`). This means the process will fail to spawn at runtime with a descriptive error, rather than failing silently at startup.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Process Lifecycle
|
|
341
|
+
|
|
342
|
+
```mermaid
|
|
343
|
+
stateDiagram-v2
|
|
344
|
+
[*] --> Discovered: constructor() / findCommand()
|
|
345
|
+
Discovered --> Spawning: startSession()
|
|
346
|
+
Spawning --> Running: node-pty spawn success
|
|
347
|
+
Spawning --> Error: spawn failure
|
|
348
|
+
Running --> Running: onData / sendInput / resize
|
|
349
|
+
Running --> Terminating: stopSession() / SIGTERM
|
|
350
|
+
Terminating --> Dead: Process exits within 5s
|
|
351
|
+
Terminating --> ForceKill: 5s timeout
|
|
352
|
+
ForceKill --> Dead: SIGKILL
|
|
353
|
+
Running --> Dead: Natural exit (onExit)
|
|
354
|
+
Running --> Dead: Process error (onError)
|
|
355
|
+
Error --> [*]
|
|
356
|
+
Dead --> [*]: Session removed from Map
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Spawn (`startSession`)
|
|
360
|
+
|
|
361
|
+
The bridge spawns the CLI process using `node-pty`:
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
const process = spawn(this.command, args, {
|
|
365
|
+
cwd: workingDir,
|
|
366
|
+
env: {
|
|
367
|
+
...process.env,
|
|
368
|
+
TERM: 'xterm-256color',
|
|
369
|
+
FORCE_COLOR: '1',
|
|
370
|
+
COLORTERM: 'truecolor'
|
|
371
|
+
},
|
|
372
|
+
cols,
|
|
373
|
+
rows,
|
|
374
|
+
name: 'xterm-color'
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Three callbacks are registered on the process:
|
|
379
|
+
|
|
380
|
+
| Callback | Trigger | Action |
|
|
381
|
+
|----------|---------|--------|
|
|
382
|
+
| `onData(data)` | CLI writes to stdout/stderr | Forward to `onOutput` callback; append to rolling buffer |
|
|
383
|
+
| `onExit(code, signal)` | Process exits | Mark session inactive; remove from map; call `onExit` callback |
|
|
384
|
+
| `on('error', err)` | Spawn or runtime error | Mark session inactive; remove from map; call `onError` callback |
|
|
385
|
+
|
|
386
|
+
### Data Streaming (`onData`)
|
|
387
|
+
|
|
388
|
+
Each bridge maintains a rolling `dataBuffer` (capped at 10,000 characters, trimmed to the last 5,000) for internal use such as prompt detection. The raw output data is passed through to the server's `onOutput` callback without transformation.
|
|
389
|
+
|
|
390
|
+
### Input (`sendInput`)
|
|
391
|
+
|
|
392
|
+
Input is written directly to the PTY process:
|
|
393
|
+
|
|
394
|
+
```javascript
|
|
395
|
+
session.process.write(data);
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
This is a synchronous write to the pseudo-terminal's master side. The data flows to the CLI process's stdin.
|
|
399
|
+
|
|
400
|
+
### Resize
|
|
401
|
+
|
|
402
|
+
Terminal dimensions are updated on the PTY:
|
|
403
|
+
|
|
404
|
+
```javascript
|
|
405
|
+
session.process.resize(cols, rows);
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
This sends a `SIGWINCH` signal to the child process, causing it to re-query its terminal dimensions.
|
|
409
|
+
|
|
410
|
+
### Graceful Shutdown (`stopSession`)
|
|
411
|
+
|
|
412
|
+
Shutdown follows a two-phase approach:
|
|
413
|
+
|
|
414
|
+
1. **SIGTERM** -- Politely ask the process to terminate
|
|
415
|
+
2. **Wait 5 seconds** -- Give the process time to clean up
|
|
416
|
+
3. **SIGKILL** -- Force-kill if the process is still alive
|
|
417
|
+
|
|
418
|
+
```javascript
|
|
419
|
+
session.process.kill('SIGTERM');
|
|
420
|
+
session.killTimeout = setTimeout(() => {
|
|
421
|
+
if (session.active && session.process) {
|
|
422
|
+
session.process.kill('SIGKILL');
|
|
423
|
+
}
|
|
424
|
+
}, 5000);
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
If the process exits naturally before the timeout, the `onExit` handler clears the kill timeout.
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## node-pty Configuration
|
|
432
|
+
|
|
433
|
+
### Terminal Type
|
|
434
|
+
|
|
435
|
+
All bridges configure the PTY with:
|
|
436
|
+
|
|
437
|
+
```javascript
|
|
438
|
+
{
|
|
439
|
+
TERM: 'xterm-256color', // Terminal type for full color support
|
|
440
|
+
FORCE_COLOR: '1', // Force color output in CLIs that check
|
|
441
|
+
COLORTERM: 'truecolor', // Advertise 24-bit color support
|
|
442
|
+
name: 'xterm-color' // PTY name
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
This ensures CLI tools produce rich, colorized output that maps correctly to the xterm.js terminal in the browser.
|
|
447
|
+
|
|
448
|
+
### Default Dimensions
|
|
449
|
+
|
|
450
|
+
If not specified by the client, terminals default to 80 columns by 24 rows -- the standard VT100 size.
|
|
451
|
+
|
|
452
|
+
### Environment Inheritance
|
|
453
|
+
|
|
454
|
+
The PTY inherits the full server process environment (`...process.env`) with the terminal-specific variables overlaid. This means the CLI tools have access to API keys, PATH, and other environment variables set in the server's context.
|
|
455
|
+
|
|
456
|
+
### Windows Support (ConPTY)
|
|
457
|
+
|
|
458
|
+
The `node-pty` library uses ConPTY on Windows automatically. However, the current command discovery logic (`which`, `HOME`, Unix paths) is Linux/macOS-oriented. Windows support would require:
|
|
459
|
+
|
|
460
|
+
- Using `where` instead of `which` in `commandExists()`
|
|
461
|
+
- Using `os.homedir()` instead of `process.env.HOME`
|
|
462
|
+
- Adding Windows-specific search paths (e.g., `%APPDATA%`, `%LOCALAPPDATA%`)
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Trust Prompt Auto-Accept (Claude-Specific)
|
|
467
|
+
|
|
468
|
+
The `ClaudeBridge` has unique behavior not present in other bridges: it automatically handles Claude's workspace trust prompt.
|
|
469
|
+
|
|
470
|
+
When Claude CLI is launched in a new directory, it asks:
|
|
471
|
+
|
|
472
|
+
```
|
|
473
|
+
Do you trust the files in this folder?
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
The bridge detects this prompt in the output buffer and automatically sends a carriage return to accept it:
|
|
477
|
+
|
|
478
|
+
```javascript
|
|
479
|
+
// Buffer data to check for trust prompt
|
|
480
|
+
dataBuffer += data;
|
|
481
|
+
|
|
482
|
+
// Check for trust prompt and auto-accept it
|
|
483
|
+
if (!trustPromptHandled && dataBuffer.includes('Do you trust the files in this folder?')) {
|
|
484
|
+
trustPromptHandled = true;
|
|
485
|
+
setTimeout(() => {
|
|
486
|
+
claudeProcess.write('\r');
|
|
487
|
+
}, 500);
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Key details:
|
|
492
|
+
- The flag `trustPromptHandled` ensures this only fires once per session
|
|
493
|
+
- A 500ms delay is added before sending the keystroke to ensure the prompt UI is fully rendered
|
|
494
|
+
- The `\r` (carriage return) confirms the default selection ("Enter to confirm")
|
|
495
|
+
- Other bridges do not need this because their CLIs do not have a trust prompt
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Data Buffer Management
|
|
500
|
+
|
|
501
|
+
Each bridge maintains a per-session rolling buffer for internal prompt detection:
|
|
502
|
+
|
|
503
|
+
```javascript
|
|
504
|
+
dataBuffer += data;
|
|
505
|
+
if (dataBuffer.length > 10000) {
|
|
506
|
+
dataBuffer = dataBuffer.slice(-5000);
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
This is separate from the server-level `outputBuffer` (which stores the last 1000 output chunks for client reconnection replay). The bridge-level buffer is only used for pattern matching within the bridge itself (e.g., trust prompt detection in ClaudeBridge).
|