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,311 @@
|
|
|
1
|
+
# E2E Testing Specification
|
|
2
|
+
|
|
3
|
+
This document outlines the end-to-end testing strategy for the ai-or-die web application, covering framework selection, architectural decisions, and the full test plan.
|
|
4
|
+
|
|
5
|
+
## 1. Research Findings
|
|
6
|
+
|
|
7
|
+
### 1.1 Testing WebSocket-Based Node.js Apps
|
|
8
|
+
|
|
9
|
+
**Recommended approach: Mocha + native `ws` client.**
|
|
10
|
+
|
|
11
|
+
The `ws` npm package (already a project dependency) doubles as both server and client library. For Mocha-based integration tests, the pattern is straightforward:
|
|
12
|
+
|
|
13
|
+
- Start the server in a `before()` hook via the `ClaudeCodeWebServer` class
|
|
14
|
+
- Create `ws` client connections in each test
|
|
15
|
+
- Tear down in `after()` hooks
|
|
16
|
+
|
|
17
|
+
This avoids adding any new dependencies. The `ws` client supports the same event model as the browser `WebSocket` API, making test code transferable.
|
|
18
|
+
|
|
19
|
+
Alternative approaches considered and rejected:
|
|
20
|
+
- **jest-websocket-mock** -- Jest-specific, doesn't fit our Mocha setup
|
|
21
|
+
- **mock-socket** -- Adds a mock layer we don't need; we want real server integration
|
|
22
|
+
- **MSW (Mock Service Worker)** -- Overkill for server-side WebSocket testing
|
|
23
|
+
|
|
24
|
+
**Sources:**
|
|
25
|
+
- [Integration testing WebSocket server in Node.JS (Medium)](https://medium.com/@basavarajkn/integration-testing-websocket-server-in-node-js-2997d107414c)
|
|
26
|
+
- [WebSocket Tests: Complete Guide 2025 (VideoSDK)](https://www.videosdk.live/developer-hub/websocket/websocket-tests)
|
|
27
|
+
- [ws GitHub repository](https://github.com/websockets/ws)
|
|
28
|
+
|
|
29
|
+
### 1.2 Testing xterm.js Terminal Emulation E2E
|
|
30
|
+
|
|
31
|
+
xterm.js itself uses Playwright for its integration test suite, running tests against the browser-rendered terminal canvas. For our use case, however, the critical path is **server-side**: data flows from the spawned CLI process through node-pty, over WebSocket, to the client. The xterm.js rendering layer is a display concern.
|
|
32
|
+
|
|
33
|
+
**Our strategy splits into two tiers:**
|
|
34
|
+
|
|
35
|
+
| Tier | What it tests | Tool |
|
|
36
|
+
|------|--------------|------|
|
|
37
|
+
| Server E2E | Server boot, WebSocket protocol, session lifecycle, PTY I/O | Mocha + `ws` client |
|
|
38
|
+
| Browser E2E (future) | xterm.js rendering, keyboard input, UI interactions | Playwright |
|
|
39
|
+
|
|
40
|
+
For the initial test suite, Tier 1 (server E2E) provides the highest coverage-to-effort ratio. It validates the entire backend pipeline without requiring a browser.
|
|
41
|
+
|
|
42
|
+
**Sources:**
|
|
43
|
+
- [xterm.js Contributing Wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing)
|
|
44
|
+
- [Playwright E2E Guide 2026 (DeviQA)](https://www.deviqa.com/blog/guide-to-playwright-end-to-end-testing-in-2025/)
|
|
45
|
+
|
|
46
|
+
### 1.3 Testing node-pty Without Real CLI Tools
|
|
47
|
+
|
|
48
|
+
The key insight: **we don't need to mock node-pty at all**. The `TerminalBridge` already spawns the user's default shell (bash on Linux, PowerShell on Windows). A shell is a perfectly valid "mock tool" -- we can:
|
|
49
|
+
|
|
50
|
+
1. Start a terminal session (spawns bash/powershell)
|
|
51
|
+
2. Send `echo "test input"` as input
|
|
52
|
+
3. Verify the echoed output appears in the WebSocket stream
|
|
53
|
+
|
|
54
|
+
This tests the full node-pty pipeline (spawn, write, read, resize, kill) using a real pseudo-terminal, without needing Claude/Copilot/Gemini installed. The `TerminalBridge.isAvailable()` always returns `true` since a shell is always present.
|
|
55
|
+
|
|
56
|
+
For CI environments where specific tool bridges need validation, lightweight mock scripts can simulate CLI behavior:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
#!/bin/bash
|
|
60
|
+
# mock-tool.sh -- Simulates a CLI tool
|
|
61
|
+
echo "Mock Tool v1.0"
|
|
62
|
+
while IFS= read -r line; do
|
|
63
|
+
echo "Response: $line"
|
|
64
|
+
done
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Sources:**
|
|
68
|
+
- [node-pty GitHub (microsoft/node-pty)](https://github.com/microsoft/node-pty)
|
|
69
|
+
- [Node.js PTY Deep Dive (w3tutorials)](https://www.w3tutorials.net/blog/nodejs-pty/)
|
|
70
|
+
|
|
71
|
+
### 1.4 Express + WebSocket E2E Best Practices (2025/2026)
|
|
72
|
+
|
|
73
|
+
Current best practices for testing Express + WebSocket servers:
|
|
74
|
+
|
|
75
|
+
1. **Decouple server from port binding** -- The `ClaudeCodeWebServer` class already supports this via `start()` returning a promise. Use port 0 for auto-assignment in tests.
|
|
76
|
+
2. **Lifecycle in hooks** -- `before()` starts server, `after()` calls `close()`. Keep server instances per `describe` block to avoid cross-contamination.
|
|
77
|
+
3. **Async-first** -- Mocha 11.x natively handles async/await and returned promises.
|
|
78
|
+
4. **Timeout management** -- WebSocket tests need longer timeouts (5-10s) for PTY spawn + output propagation.
|
|
79
|
+
5. **Parallel safety** -- Each test suite uses a unique port (0 = OS-assigned) and isolated session state.
|
|
80
|
+
|
|
81
|
+
**Sources:**
|
|
82
|
+
- [Fullstack Open: E2E Testing with Playwright](https://fullstackopen.com/en/part5/end_to_end_testing_playwright/)
|
|
83
|
+
- [Testing Socket.io With Mocha and Chai](https://alexzywiak.github.io/testing-socket-io-with-mocha-and-chai/index.html)
|
|
84
|
+
|
|
85
|
+
### 1.5 Can Playwright/Puppeteer Test xterm.js Terminals?
|
|
86
|
+
|
|
87
|
+
**Yes, but with caveats.**
|
|
88
|
+
|
|
89
|
+
xterm.js renders to a `<canvas>` element (via its WebGL or Canvas renderer), which means standard DOM selectors don't work for reading terminal content. Approaches:
|
|
90
|
+
|
|
91
|
+
- **xterm.js API access via `page.evaluate()`** -- Expose the `Terminal` instance on `window`, then call `terminal.buffer.active.getLine(row).translateToString()` to read terminal content programmatically.
|
|
92
|
+
- **Keyboard simulation** -- Playwright's `page.keyboard.type()` sends keystrokes that xterm.js captures.
|
|
93
|
+
- **Screenshot comparison** -- Visual regression testing via `page.screenshot()` comparisons.
|
|
94
|
+
|
|
95
|
+
For the initial test suite, browser-level tests are **deferred**. The server E2E tests with `ws` clients cover the critical data path. Browser tests can be added later as a Playwright layer when UI regression testing becomes a priority.
|
|
96
|
+
|
|
97
|
+
**Sources:**
|
|
98
|
+
- [Playwright GitHub](https://github.com/microsoft/playwright)
|
|
99
|
+
- [xterm.js GitHub](https://github.com/xtermjs/xterm.js)
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 2. Test Architecture
|
|
104
|
+
|
|
105
|
+
### 2.1 Design Principles
|
|
106
|
+
|
|
107
|
+
- **No new dependencies** -- Use Mocha (devDependency), `ws` and `assert` (both already available)
|
|
108
|
+
- **Real server, real PTY** -- No mocking of node-pty or WebSocket; test the actual stack
|
|
109
|
+
- **Mock the tools, not the infrastructure** -- Use bash/echo as the "tool" instead of Claude/Copilot
|
|
110
|
+
- **Cross-platform** -- Tests must pass on Linux and Windows CI; use `TerminalBridge` (shell) as the universal tool
|
|
111
|
+
- **Deterministic** -- Avoid timing-dependent assertions; use event-driven waits with timeouts
|
|
112
|
+
- **Isolated** -- Each test suite starts its own server on an ephemeral port
|
|
113
|
+
|
|
114
|
+
### 2.2 File Structure
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
test/
|
|
118
|
+
session-store.test.js # Existing: unit tests for SessionStore
|
|
119
|
+
claude-bridge.test.js # Existing: unit tests for ClaudeBridge
|
|
120
|
+
server-alias.test.js # Existing: unit tests for server aliases
|
|
121
|
+
e2e.test.js # NEW: server E2E tests (this spec)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2.3 Helper Utilities (embedded in test file)
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
// Wait for a specific WebSocket message type
|
|
128
|
+
function waitForMessage(ws, type, timeoutMs = 5000) { ... }
|
|
129
|
+
|
|
130
|
+
// Create an authenticated WebSocket connection
|
|
131
|
+
function connectWs(port, token, sessionId) { ... }
|
|
132
|
+
|
|
133
|
+
// Send a typed message and await a specific response type
|
|
134
|
+
function sendAndWait(ws, message, expectedType, timeoutMs) { ... }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 3. Test Plan
|
|
140
|
+
|
|
141
|
+
### 3.1 Server Boot & Health (`describe('Server lifecycle')`)
|
|
142
|
+
|
|
143
|
+
| Test | What it validates |
|
|
144
|
+
|------|-------------------|
|
|
145
|
+
| Server starts on ephemeral port | `ClaudeCodeWebServer.start()` resolves; port > 0 |
|
|
146
|
+
| Health endpoint returns OK | `GET /api/health` -> `{ status: 'ok' }` |
|
|
147
|
+
| Config endpoint returns tool info | `GET /api/config` -> contains `tools.terminal.available: true` |
|
|
148
|
+
| Server shuts down cleanly | `server.close()` completes without error |
|
|
149
|
+
|
|
150
|
+
### 3.2 Authentication (`describe('Authentication')`)
|
|
151
|
+
|
|
152
|
+
| Test | What it validates |
|
|
153
|
+
|------|-------------------|
|
|
154
|
+
| Auth-enabled: valid token accepted | `GET /api/health` with `Bearer <token>` -> 200 |
|
|
155
|
+
| Auth-enabled: invalid token rejected | `GET /api/health` with wrong token -> 401 |
|
|
156
|
+
| Auth-enabled: missing token rejected | `GET /api/health` with no auth -> 401 |
|
|
157
|
+
| Auth-enabled: WS with valid token connects | WS `?token=<token>` -> receives `connected` message |
|
|
158
|
+
| Auth-enabled: WS with invalid token rejected | WS `?token=wrong` -> connection refused |
|
|
159
|
+
| No-auth mode: all requests accepted | `noAuth: true` -> health endpoint accessible without token |
|
|
160
|
+
|
|
161
|
+
### 3.3 WebSocket Connection (`describe('WebSocket connection')`)
|
|
162
|
+
|
|
163
|
+
| Test | What it validates |
|
|
164
|
+
|------|-------------------|
|
|
165
|
+
| Connection receives `connected` message | First message is `{ type: 'connected', connectionId: <uuid> }` |
|
|
166
|
+
| Ping/pong works | Send `{ type: 'ping' }` -> receive `{ type: 'pong' }` |
|
|
167
|
+
| Clean disconnect | Close WS -> no server errors |
|
|
168
|
+
|
|
169
|
+
### 3.4 Session Management (`describe('Session management')`)
|
|
170
|
+
|
|
171
|
+
| Test | What it validates |
|
|
172
|
+
|------|-------------------|
|
|
173
|
+
| Create session via WS | Send `create_session` -> receive `session_created` with `sessionId` |
|
|
174
|
+
| Create session via REST | `POST /api/sessions/create` -> 200 with `sessionId` |
|
|
175
|
+
| List sessions | `GET /api/sessions/list` -> includes created session |
|
|
176
|
+
| Join existing session | Send `join_session` with valid ID -> receive `session_joined` with output buffer |
|
|
177
|
+
| Join non-existent session | Send `join_session` with bad ID -> receive `error` |
|
|
178
|
+
| Leave session | Send `leave_session` -> receive `session_left` |
|
|
179
|
+
| Delete session via REST | `DELETE /api/sessions/:id` -> 200, session gone from list |
|
|
180
|
+
| Delete non-existent session | `DELETE /api/sessions/bad-id` -> 404 |
|
|
181
|
+
|
|
182
|
+
### 3.5 Tool Session Lifecycle (`describe('Terminal tool session')`)
|
|
183
|
+
|
|
184
|
+
| Test | What it validates |
|
|
185
|
+
|------|-------------------|
|
|
186
|
+
| Start terminal session | Send `start_terminal` -> receive `terminal_started` |
|
|
187
|
+
| Terminal produces output | After start, receive `output` messages (shell prompt) |
|
|
188
|
+
| Send input, receive output | Send `echo hello` -> output contains `hello` |
|
|
189
|
+
| Resize terminal | Send `resize` with new cols/rows -> no error |
|
|
190
|
+
| Stop terminal session | Send `stop` -> receive `exit` message |
|
|
191
|
+
| Cannot start two tools in same session | Start terminal, then start terminal again -> receive `error` |
|
|
192
|
+
| Echo unique marker through terminal (cross-platform) | Start terminal -> drain initial output -> send `echo MARKER` -> verify marker in collected output -> stop |
|
|
193
|
+
|
|
194
|
+
### 3.6 Input/Output Round-Trip (`describe('I/O round-trip')`)
|
|
195
|
+
|
|
196
|
+
| Test | What it validates |
|
|
197
|
+
|------|-------------------|
|
|
198
|
+
| Echo command round-trip | Send `echo MARKER_STRING` -> output stream contains `MARKER_STRING` |
|
|
199
|
+
| Multi-line output | Send command producing multiple lines -> all lines received |
|
|
200
|
+
| Special characters | Send `echo "hello world & <test>"` -> output preserves content |
|
|
201
|
+
| Output buffer replay | Join session -> `session_joined` includes prior output in buffer |
|
|
202
|
+
|
|
203
|
+
### 3.7 Multi-Session (`describe('Multi-session management')`)
|
|
204
|
+
|
|
205
|
+
| Test | What it validates |
|
|
206
|
+
|------|-------------------|
|
|
207
|
+
| Create multiple sessions | Create 3 sessions -> list shows 3 |
|
|
208
|
+
| Sessions are isolated | Start terminal in session A, send input -> session B gets no output |
|
|
209
|
+
| Switch sessions | Leave session A, join session B -> receive B's buffer |
|
|
210
|
+
| Delete active session | Delete session with running terminal -> process stops, session removed |
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 4. Cross-Platform Considerations
|
|
215
|
+
|
|
216
|
+
### 4.1 Shell Differences
|
|
217
|
+
|
|
218
|
+
| Aspect | Linux/macOS | Windows |
|
|
219
|
+
|--------|-------------|---------|
|
|
220
|
+
| Default shell | bash/zsh | PowerShell/cmd |
|
|
221
|
+
| Echo command | `echo hello` | `echo hello` (works in both) |
|
|
222
|
+
| Line endings | `\n` | `\r\n` |
|
|
223
|
+
| Exit command | `exit` | `exit` |
|
|
224
|
+
| Prompt detection | `$` or `#` | `PS C:\>` or `>` |
|
|
225
|
+
|
|
226
|
+
The test suite uses `echo` for I/O validation, which works identically across shells. Output assertions use substring matching (`.includes()`) rather than exact string comparison to handle prompt/formatting differences.
|
|
227
|
+
|
|
228
|
+
### 4.2 CI Matrix
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
# Example GitHub Actions matrix
|
|
232
|
+
strategy:
|
|
233
|
+
matrix:
|
|
234
|
+
os: [ubuntu-latest, windows-latest]
|
|
235
|
+
node: [18, 20, 22]
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 4.3 Timeout Handling
|
|
239
|
+
|
|
240
|
+
PTY process startup varies by platform. Windows ConPTY is slower than Unix PTY. Tests use:
|
|
241
|
+
- 5s default message timeout
|
|
242
|
+
- 10s timeout for PTY output propagation
|
|
243
|
+
- 30s Mocha suite timeout
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 5. Future Enhancements
|
|
248
|
+
|
|
249
|
+
### 5.1 Browser E2E with Playwright (Tier 2)
|
|
250
|
+
|
|
251
|
+
When browser-level testing is needed:
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
// Example Playwright test structure
|
|
255
|
+
test('terminal displays tool output', async ({ page }) => {
|
|
256
|
+
await page.goto(`http://localhost:${port}?token=${authToken}`);
|
|
257
|
+
|
|
258
|
+
// Wait for xterm.js canvas to render
|
|
259
|
+
await page.waitForSelector('.xterm-screen canvas');
|
|
260
|
+
|
|
261
|
+
// Read terminal content via xterm API
|
|
262
|
+
const content = await page.evaluate(() => {
|
|
263
|
+
const term = window.__terminal__;
|
|
264
|
+
const buffer = term.buffer.active;
|
|
265
|
+
let text = '';
|
|
266
|
+
for (let i = 0; i < buffer.cursorY + 1; i++) {
|
|
267
|
+
text += buffer.getLine(i).translateToString(true) + '\n';
|
|
268
|
+
}
|
|
269
|
+
return text;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(content).toContain('expected output');
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### 5.2 Mock Tool Scripts
|
|
277
|
+
|
|
278
|
+
For testing specific tool bridge behaviors (trust prompts, dangerous mode flags):
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
#!/bin/bash
|
|
282
|
+
# test/fixtures/mock-claude.sh
|
|
283
|
+
echo "Claude Code v1.0.0 (mock)"
|
|
284
|
+
echo "Do you trust the files in this folder?"
|
|
285
|
+
read -r response
|
|
286
|
+
echo "Trust accepted: $response"
|
|
287
|
+
while IFS= read -r line; do
|
|
288
|
+
echo "Assistant: I received '$line'"
|
|
289
|
+
done
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 5.3 Performance/Load Testing
|
|
293
|
+
|
|
294
|
+
- Concurrent WebSocket connections (50-100 simultaneous)
|
|
295
|
+
- Session creation throughput
|
|
296
|
+
- Output streaming latency under load
|
|
297
|
+
- Memory usage with many active PTY sessions
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## 6. Dependencies
|
|
302
|
+
|
|
303
|
+
No new dependencies required.
|
|
304
|
+
|
|
305
|
+
| Package | Role | Status |
|
|
306
|
+
|---------|------|--------|
|
|
307
|
+
| `mocha` | Test runner | Already in devDependencies |
|
|
308
|
+
| `assert` | Assertions | Node.js built-in |
|
|
309
|
+
| `ws` | WebSocket client in tests | Already in dependencies |
|
|
310
|
+
| `http` | HTTP client for REST endpoints | Node.js built-in |
|
|
311
|
+
| `node-pty` | PTY spawning (via bridges) | Already in dependencies |
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# Server Specification
|
|
2
|
+
|
|
3
|
+
Source: `src/server.js` -- class `ClaudeCodeWebServer`
|
|
4
|
+
|
|
5
|
+
## Constructor
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
new ClaudeCodeWebServer(options)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| Option | Type | Default | Description |
|
|
12
|
+
|--------|------|---------|-------------|
|
|
13
|
+
| `port` | number | `7777` | HTTP/HTTPS listen port |
|
|
14
|
+
| `auth` | string | `undefined` | Bearer token for authentication; when set, all HTTP and WebSocket requests must provide it |
|
|
15
|
+
| `noAuth` | boolean | `false` | Disable authentication entirely (`--disable-auth`) |
|
|
16
|
+
| `dev` | boolean | `false` | Enable verbose console logging |
|
|
17
|
+
| `https` | boolean | `false` | Start an HTTPS server instead of HTTP |
|
|
18
|
+
| `cert` | string | -- | Path to PEM certificate file (required when `https` is true) |
|
|
19
|
+
| `key` | string | -- | Path to PEM private key file (required when `https` is true) |
|
|
20
|
+
| `folderMode` | boolean | `true` | Enable folder-selection UI (defaults to true; always enabled in current CLI) |
|
|
21
|
+
| `sessionHours` | number | `5` | Session window duration in hours; also read from `CLAUDE_SESSION_HOURS` env |
|
|
22
|
+
| `plan` | string | `'max20'` | Subscription plan type (`pro`, `max5`, `max20`, `custom`); also read from `CLAUDE_PLAN` env |
|
|
23
|
+
| `customCostLimit` | number | `50.00` | Dollar cost limit for custom plans; also read from `CLAUDE_COST_LIMIT` env |
|
|
24
|
+
| `claudeAlias` | string | `'Claude'` | UI display name for Claude agent; also read from `CLAUDE_ALIAS` env |
|
|
25
|
+
| `codexAlias` | string | `'Codex'` | UI display name for Codex agent; also read from `CODEX_ALIAS` env |
|
|
26
|
+
| `agentAlias` | string | `'Cursor'` | UI display name for the third agent; also read from `AGENT_ALIAS` env |
|
|
27
|
+
|
|
28
|
+
### Internal State
|
|
29
|
+
|
|
30
|
+
| Property | Type | Description |
|
|
31
|
+
|----------|------|-------------|
|
|
32
|
+
| `claudeSessions` | `Map<string, Session>` | All sessions keyed by UUID |
|
|
33
|
+
| `webSocketConnections` | `Map<string, WsInfo>` | Active WebSocket connections keyed by UUID |
|
|
34
|
+
| `claudeBridge` | `ClaudeBridge` | Bridge instance managing Claude CLI processes |
|
|
35
|
+
| `codexBridge` | `CodexBridge` | Bridge instance managing Codex CLI processes |
|
|
36
|
+
| `agentBridge` | `AgentBridge` | Bridge instance managing Cursor Agent processes |
|
|
37
|
+
| `sessionStore` | `SessionStore` | Persistence layer for session data |
|
|
38
|
+
| `usageReader` | `UsageReader` | Reads JSONL usage logs from `~/.claude/projects/` |
|
|
39
|
+
| `usageAnalytics` | `UsageAnalytics` | Calculates burn rate, predictions, plan limits |
|
|
40
|
+
| `selectedWorkingDir` | string \| null | Currently selected working directory via folder browser |
|
|
41
|
+
| `baseFolder` | string | `process.cwd()` at startup -- root for path validation |
|
|
42
|
+
| `isShuttingDown` | boolean | Prevents duplicate shutdown sequences |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## REST API Endpoints
|
|
47
|
+
|
|
48
|
+
### Public (pre-auth)
|
|
49
|
+
|
|
50
|
+
#### `GET /auth-status`
|
|
51
|
+
Returns whether authentication is required.
|
|
52
|
+
|
|
53
|
+
**Response:**
|
|
54
|
+
```json
|
|
55
|
+
{ "authRequired": true, "authenticated": false }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### `POST /auth-verify`
|
|
59
|
+
Validates a token against the server's configured auth token.
|
|
60
|
+
|
|
61
|
+
**Request body:** `{ "token": "..." }`
|
|
62
|
+
**Success:** `{ "valid": true }`
|
|
63
|
+
**Failure (401):** `{ "valid": false, "error": "Invalid token" }`
|
|
64
|
+
|
|
65
|
+
### Protected (behind auth middleware when auth is enabled)
|
|
66
|
+
|
|
67
|
+
#### `GET /api/health`
|
|
68
|
+
Health check.
|
|
69
|
+
|
|
70
|
+
**Response:**
|
|
71
|
+
```json
|
|
72
|
+
{ "status": "ok", "claudeSessions": 3, "activeConnections": 2 }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### `GET /api/sessions/list`
|
|
76
|
+
List all sessions with metadata.
|
|
77
|
+
|
|
78
|
+
**Response:**
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"sessions": [
|
|
82
|
+
{
|
|
83
|
+
"id": "uuid",
|
|
84
|
+
"name": "Session 2/5/2026, 10:30:00 AM",
|
|
85
|
+
"created": "2026-02-05T10:30:00.000Z",
|
|
86
|
+
"active": true,
|
|
87
|
+
"workingDir": "/home/user/project",
|
|
88
|
+
"connectedClients": 1,
|
|
89
|
+
"lastActivity": "2026-02-05T11:00:00.000Z"
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### `POST /api/sessions/create`
|
|
96
|
+
Create a new session.
|
|
97
|
+
|
|
98
|
+
**Request body:** `{ "name": "...", "workingDir": "/path" }`
|
|
99
|
+
- `workingDir` is validated against `baseFolder`.
|
|
100
|
+
- Falls back to `selectedWorkingDir` or `baseFolder` when omitted.
|
|
101
|
+
|
|
102
|
+
**Response:**
|
|
103
|
+
```json
|
|
104
|
+
{ "success": true, "sessionId": "uuid", "session": { "id": "...", "name": "...", "workingDir": "..." } }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### `GET /api/sessions/:sessionId`
|
|
108
|
+
Get details of a single session.
|
|
109
|
+
|
|
110
|
+
**Response:** Session object with `id`, `name`, `created`, `active`, `workingDir`, `connectedClients`, `lastActivity`.
|
|
111
|
+
|
|
112
|
+
**404** when session ID is unknown.
|
|
113
|
+
|
|
114
|
+
#### `DELETE /api/sessions/:sessionId`
|
|
115
|
+
Delete a session. Stops the running agent process (if any), sends `session_deleted` to all connected WebSocket clients, and removes the session from the in-memory Map and disk persistence.
|
|
116
|
+
|
|
117
|
+
**Response:** `{ "success": true, "message": "Session deleted" }`
|
|
118
|
+
|
|
119
|
+
#### `GET /api/config`
|
|
120
|
+
Returns server configuration relevant to the client.
|
|
121
|
+
|
|
122
|
+
**Response:**
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"folderMode": true,
|
|
126
|
+
"selectedWorkingDir": "/home/user/project",
|
|
127
|
+
"baseFolder": "/home/user",
|
|
128
|
+
"aliases": { "claude": "Claude", "codex": "Codex", "agent": "Cursor" }
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `GET /api/folders`
|
|
133
|
+
Browse directories within `baseFolder`.
|
|
134
|
+
|
|
135
|
+
**Query params:**
|
|
136
|
+
- `path` -- directory to list (default: `baseFolder`)
|
|
137
|
+
- `showHidden` -- `"true"` to include dotfiles
|
|
138
|
+
|
|
139
|
+
**Response:**
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"currentPath": "/home/user/projects",
|
|
143
|
+
"parentPath": "/home/user",
|
|
144
|
+
"folders": [ { "name": "repo", "path": "/home/user/projects/repo", "isDirectory": true } ],
|
|
145
|
+
"home": "/home/user",
|
|
146
|
+
"baseFolder": "/home/user"
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### `POST /api/set-working-dir`
|
|
151
|
+
Set the server-wide working directory for new sessions.
|
|
152
|
+
|
|
153
|
+
**Request body:** `{ "path": "/absolute/path" }`
|
|
154
|
+
**Response:** `{ "success": true, "workingDir": "/absolute/path" }`
|
|
155
|
+
|
|
156
|
+
#### `POST /api/folders/select`
|
|
157
|
+
Alias for `POST /api/set-working-dir` with identical behavior.
|
|
158
|
+
|
|
159
|
+
#### `POST /api/create-folder`
|
|
160
|
+
Create a new directory inside an allowed parent.
|
|
161
|
+
|
|
162
|
+
**Request body:** `{ "parentPath": "/base/path", "folderName": "new-dir" }`
|
|
163
|
+
- `folderName` must not contain `/` or `\`.
|
|
164
|
+
- Both `parentPath` and resulting path are validated.
|
|
165
|
+
|
|
166
|
+
**Response:** `{ "success": true, "path": "/base/path/new-dir", "message": "..." }`
|
|
167
|
+
|
|
168
|
+
#### `POST /api/close-session`
|
|
169
|
+
Clears `selectedWorkingDir` back to `null`.
|
|
170
|
+
|
|
171
|
+
**Response:** `{ "success": true, "message": "Working directory cleared" }`
|
|
172
|
+
|
|
173
|
+
#### `GET /api/sessions/persistence`
|
|
174
|
+
Returns persistence metadata from `SessionStore.getSessionMetadata()`.
|
|
175
|
+
|
|
176
|
+
**Response:**
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"exists": true,
|
|
180
|
+
"savedAt": "2026-02-05T10:00:00.000Z",
|
|
181
|
+
"sessionCount": 3,
|
|
182
|
+
"fileSize": 4096,
|
|
183
|
+
"version": "1.0",
|
|
184
|
+
"currentSessions": 3,
|
|
185
|
+
"autoSaveEnabled": true,
|
|
186
|
+
"autoSaveInterval": 30000
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### `GET /`
|
|
191
|
+
Serves `src/public/index.html`.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## WebSocket
|
|
196
|
+
|
|
197
|
+
### Setup
|
|
198
|
+
|
|
199
|
+
The WebSocket server (`ws.Server`) is attached to the same HTTP(S) server instance. A `verifyClient` callback checks the `?token=` query parameter against `this.auth` when authentication is enabled.
|
|
200
|
+
|
|
201
|
+
### Connection Lifecycle
|
|
202
|
+
|
|
203
|
+
1. Client connects. Server assigns a `wsId` (UUID), stores it in `webSocketConnections`, and sends `{ type: "connected", connectionId: wsId }`.
|
|
204
|
+
2. If `?sessionId=` is present in the URL and the session exists, the server auto-joins that session.
|
|
205
|
+
3. On close or error, the connection is removed from `webSocketConnections` and its session's `connections` Set.
|
|
206
|
+
|
|
207
|
+
### Message Protocol
|
|
208
|
+
|
|
209
|
+
All messages are JSON. The `type` field determines the handler.
|
|
210
|
+
|
|
211
|
+
| Client Message | Description |
|
|
212
|
+
|---------------|-------------|
|
|
213
|
+
| `create_session` | Create a new session and join it. Fields: `name`, `workingDir`. |
|
|
214
|
+
| `join_session` | Join an existing session. Fields: `sessionId`. Replays the last 200 lines of the output buffer. |
|
|
215
|
+
| `leave_session` | Disconnect from current session without stopping the agent. |
|
|
216
|
+
| `start_claude` | Launch Claude CLI in the current session. Fields: `options` (optional). Pre-checks tool availability. |
|
|
217
|
+
| `start_codex` | Launch Codex CLI in the current session. Fields: `options` (optional). Pre-checks tool availability. |
|
|
218
|
+
| `start_copilot` | Launch Copilot CLI in the current session. Fields: `options` (optional). Pre-checks tool availability. |
|
|
219
|
+
| `start_gemini` | Launch Gemini CLI in the current session. Fields: `options` (optional). Pre-checks tool availability. |
|
|
220
|
+
| `start_terminal` | Launch a terminal shell in the current session. Fields: `options` (optional). Pre-checks tool availability. |
|
|
221
|
+
|
|
222
|
+
**`start_*` Error Handling**: All tool start messages go through `startToolSession`, which sends an `error` message for every failure path:
|
|
223
|
+
|
|
224
|
+
| Condition | Error message |
|
|
225
|
+
|-----------|--------------|
|
|
226
|
+
| No session joined | "No session joined. Please create or join a session first." |
|
|
227
|
+
| Session not in map | "Session not found. It may have been deleted. Please create a new session." |
|
|
228
|
+
| Agent already running | "An agent is already running in this session" |
|
|
229
|
+
| Tool not available | "{tool} is not available. Please ensure the {tool} CLI is installed..." |
|
|
230
|
+
| Spawn failure | "Failed to start {tool}: {error}" |
|
|
231
|
+
|
|
232
|
+
| `input` | Send raw terminal input to the running agent. Fields: `data`. |
|
|
233
|
+
| `resize` | Resize the pty. Fields: `cols`, `rows`. |
|
|
234
|
+
| `stop` | Terminate the running agent process. |
|
|
235
|
+
| `ping` | Keep-alive. Server responds with `{ type: "pong" }`. |
|
|
236
|
+
| `get_usage` | Request usage statistics. Server responds with `usage_update`. |
|
|
237
|
+
|
|
238
|
+
| Server Message | Description |
|
|
239
|
+
|---------------|-------------|
|
|
240
|
+
| `connected` | Initial connection acknowledgment with `connectionId`. |
|
|
241
|
+
| `session_created` | Session successfully created. Fields: `sessionId`, `sessionName`, `workingDir`. |
|
|
242
|
+
| `session_joined` | Joined an existing session. Fields: `sessionId`, `sessionName`, `workingDir`, `active`, `outputBuffer`. |
|
|
243
|
+
| `session_left` | Successfully left the session. Fields: `sessionId`. |
|
|
244
|
+
| `session_deleted` | Session was deleted by another client or via REST API. |
|
|
245
|
+
| `claude_started` / `codex_started` / `agent_started` | Agent process launched. |
|
|
246
|
+
| `claude_stopped` / `codex_stopped` / `agent_stopped` | Agent process terminated. |
|
|
247
|
+
| `output` | Terminal output data from the agent. Fields: `data`. |
|
|
248
|
+
| `exit` | Agent process exited. Fields: `code`, `signal`. |
|
|
249
|
+
| `error` | Error message. Fields: `message`. |
|
|
250
|
+
| `info` | Informational message (e.g., "No agent is running"). |
|
|
251
|
+
| `pong` | Response to `ping`. |
|
|
252
|
+
| `usage_update` | Usage statistics payload (see Usage Analytics spec). |
|
|
253
|
+
|
|
254
|
+
### Broadcasting
|
|
255
|
+
|
|
256
|
+
`broadcastToSession(sessionId, data)` iterates the session's `connections` Set, verifies each WebSocket is open and belongs to the correct session, then sends the JSON-serialized message.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Session Management
|
|
261
|
+
|
|
262
|
+
### In-Memory Session Object
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
{
|
|
266
|
+
id: 'uuid',
|
|
267
|
+
name: 'Session 2/5/2026, 10:30:00 AM',
|
|
268
|
+
created: Date,
|
|
269
|
+
lastActivity: Date,
|
|
270
|
+
active: false, // true when an agent process is running
|
|
271
|
+
agent: null, // 'claude' | 'codex' | 'agent' | null
|
|
272
|
+
workingDir: '/path',
|
|
273
|
+
connections: Set<wsId>, // WebSocket connection IDs
|
|
274
|
+
outputBuffer: [], // Rolling buffer of terminal output strings
|
|
275
|
+
maxBufferSize: 1000, // Max items in outputBuffer
|
|
276
|
+
sessionStartTime: Date|null, // Set on first agent start
|
|
277
|
+
sessionUsage: { // Aggregated token/cost tracking
|
|
278
|
+
requests: 0,
|
|
279
|
+
inputTokens: 0,
|
|
280
|
+
outputTokens: 0,
|
|
281
|
+
cacheTokens: 0,
|
|
282
|
+
totalCost: 0,
|
|
283
|
+
models: {}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Auto-Save
|
|
289
|
+
|
|
290
|
+
Sessions are persisted to disk via `SessionStore` every 30 seconds (`setInterval`). The `saveSessionsToDisk()` method also fires:
|
|
291
|
+
- After session creation
|
|
292
|
+
- After session deletion
|
|
293
|
+
- On `beforeExit`
|
|
294
|
+
- During graceful shutdown
|
|
295
|
+
|
|
296
|
+
### Session Restoration
|
|
297
|
+
|
|
298
|
+
On startup, `loadPersistedSessions()` loads sessions from disk. All sessions are restored with `active: false` and empty `connections` Sets since pty processes do not survive restarts.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Graceful Shutdown
|
|
303
|
+
|
|
304
|
+
Registered on `SIGINT` and `SIGTERM`. The `handleShutdown()` method:
|
|
305
|
+
|
|
306
|
+
1. Sets `isShuttingDown = true` to prevent re-entry.
|
|
307
|
+
2. Saves all sessions to disk.
|
|
308
|
+
3. Clears the auto-save interval.
|
|
309
|
+
4. Calls `close()` which:
|
|
310
|
+
- Saves sessions again.
|
|
311
|
+
- Closes the WebSocket server.
|
|
312
|
+
- Closes the HTTP server.
|
|
313
|
+
- Stops all active agent processes (routing to the correct bridge based on `session.agent`).
|
|
314
|
+
- Clears `claudeSessions` and `webSocketConnections` Maps.
|
|
315
|
+
5. Calls `process.exit(0)`.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Path Validation
|
|
320
|
+
|
|
321
|
+
Two methods enforce directory traversal prevention:
|
|
322
|
+
|
|
323
|
+
- **`isPathWithinBase(targetPath)`** -- Resolves `targetPath` and checks that it starts with the resolved `baseFolder`.
|
|
324
|
+
- **`validatePath(targetPath)`** -- Returns `{ valid: true, path: resolvedPath }` or `{ valid: false, error: '...' }`.
|
|
325
|
+
|
|
326
|
+
Every endpoint that accepts a filesystem path (`/api/folders`, `/api/set-working-dir`, `/api/create-folder`, `/api/sessions/create`, etc.) calls `validatePath()` before performing any I/O. The `baseFolder` is always `process.cwd()` at server startup time.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Static Assets and PWA
|
|
331
|
+
|
|
332
|
+
- `express.static` serves `src/public/`.
|
|
333
|
+
- `manifest.json` is served with `Content-Type: application/manifest+json`.
|
|
334
|
+
- Dynamic SVG icons are generated at `/icon-{16,32,144,180,192,512}.png` with monospace "CC" text on a dark background, served as `image/svg+xml` with a 1-year cache header.
|