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.
Files changed (78) hide show
  1. package/.cursor/commands/commit-push.md +18 -0
  2. package/.github/agents/architect.md +26 -0
  3. package/.github/agents/engineer.md +29 -0
  4. package/.github/agents/qa-reviewer.md +31 -0
  5. package/.github/agents/researcher.md +30 -0
  6. package/.github/agents/troubleshooter.md +33 -0
  7. package/.github/copilot-instructions.md +55 -0
  8. package/.github/pull_request_template.md +21 -0
  9. package/.github/workflows/build-binaries.yml +76 -0
  10. package/.github/workflows/ci.yml +70 -0
  11. package/.github/workflows/release-on-main.yml +73 -0
  12. package/.prompts/log.md +9 -0
  13. package/AGENTS.md +84 -0
  14. package/CHANGELOG.md +25 -0
  15. package/CLAUDE.md +130 -0
  16. package/CONTRIBUTING.md +76 -0
  17. package/LICENSE +22 -0
  18. package/README.md +165 -0
  19. package/bin/ai-or-die.js +203 -0
  20. package/docs/.nojekyll +1 -0
  21. package/docs/README.md +37 -0
  22. package/docs/adrs/0000-template.md +35 -0
  23. package/docs/adrs/0001-bridge-base-class.md +53 -0
  24. package/docs/adrs/0002-devtunnels-over-ngrok.md +56 -0
  25. package/docs/adrs/0003-multi-tool-architecture.md +71 -0
  26. package/docs/adrs/0004-cross-platform-support.md +101 -0
  27. package/docs/adrs/0005-single-binary-distribution.md +58 -0
  28. package/docs/agent-instructions/00-philosophy.md +55 -0
  29. package/docs/agent-instructions/01-research-and-web.md +49 -0
  30. package/docs/agent-instructions/02-testing-and-validation.md +63 -0
  31. package/docs/agent-instructions/03-tooling-and-pipelines.md +59 -0
  32. package/docs/architecture/bridge-pattern.md +510 -0
  33. package/docs/architecture/overview.md +216 -0
  34. package/docs/architecture/websocket-protocol.md +609 -0
  35. package/docs/history/README.md +26 -0
  36. package/docs/specs/authentication.md +167 -0
  37. package/docs/specs/bridges.md +210 -0
  38. package/docs/specs/client-app.md +308 -0
  39. package/docs/specs/e2e-testing.md +311 -0
  40. package/docs/specs/server.md +334 -0
  41. package/docs/specs/session-store.md +170 -0
  42. package/docs/specs/usage-analytics.md +342 -0
  43. package/nul +0 -0
  44. package/package.json +54 -0
  45. package/scripts/build-sea.js +187 -0
  46. package/scripts/pty-sea-shim.js +21 -0
  47. package/scripts/publish-both.sh +21 -0
  48. package/scripts/release-pr.sh +73 -0
  49. package/scripts/smoke-test-binary.js +190 -0
  50. package/scripts/validate.ps1 +25 -0
  51. package/scripts/validate.sh +16 -0
  52. package/sea-bootstrap.js +54 -0
  53. package/site/ADVANCED_ANALYTICS.md +174 -0
  54. package/site/index.html +151 -0
  55. package/site/script.js +17 -0
  56. package/site/style.css +60 -0
  57. package/src/base-bridge.js +340 -0
  58. package/src/claude-bridge.js +48 -0
  59. package/src/codex-bridge.js +27 -0
  60. package/src/copilot-bridge.js +29 -0
  61. package/src/gemini-bridge.js +26 -0
  62. package/src/public/app.js +2123 -0
  63. package/src/public/auth.js +244 -0
  64. package/src/public/icon-generator.js +26 -0
  65. package/src/public/icons.js +36 -0
  66. package/src/public/index.html +397 -0
  67. package/src/public/manifest.json +45 -0
  68. package/src/public/plan-detector.js +186 -0
  69. package/src/public/service-worker.js +108 -0
  70. package/src/public/session-manager.js +1124 -0
  71. package/src/public/splits.js +574 -0
  72. package/src/public/style.css +2090 -0
  73. package/src/server.js +1269 -0
  74. package/src/terminal-bridge.js +49 -0
  75. package/src/usage-analytics.js +494 -0
  76. package/src/usage-reader.js +895 -0
  77. package/src/utils/auth.js +123 -0
  78. 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.