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,167 @@
|
|
|
1
|
+
# Authentication Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
ai-or-die provides optional token-based authentication for both HTTP and WebSocket connections. Authentication is managed by two independent classes: a server-side `AuthManager` utility and a client-side `AuthManager` in the browser.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Server-Side Auth
|
|
10
|
+
|
|
11
|
+
### AuthManager Class
|
|
12
|
+
|
|
13
|
+
Source: `src/utils/auth.js`
|
|
14
|
+
|
|
15
|
+
A utility class for token management, rate limiting, and Express middleware generation. While the class is available, the server (`src/server.js`) currently implements auth inline rather than delegating to this class.
|
|
16
|
+
|
|
17
|
+
#### Token Management
|
|
18
|
+
|
|
19
|
+
| Method | Description |
|
|
20
|
+
|--------|-------------|
|
|
21
|
+
| `generateToken()` | Generates a 32-byte random hex string via `crypto.randomBytes(32).toString('hex')` |
|
|
22
|
+
| `validateToken(token)` | Checks if `token` is in the internal `Set` |
|
|
23
|
+
| `addToken(token)` | Adds a token to the `Set` |
|
|
24
|
+
| `removeToken(token)` | Removes a token from the `Set` |
|
|
25
|
+
| `clearTokens()` | Clears all stored tokens |
|
|
26
|
+
|
|
27
|
+
#### Middleware Factory
|
|
28
|
+
|
|
29
|
+
`createMiddleware(requiredToken)` returns an Express middleware that:
|
|
30
|
+
|
|
31
|
+
1. If `requiredToken` is falsy, calls `next()` (no auth required).
|
|
32
|
+
2. Extracts the token from:
|
|
33
|
+
- `Authorization: Bearer <token>` header, or
|
|
34
|
+
- `?token=<token>` query parameter.
|
|
35
|
+
3. Compares against `requiredToken`.
|
|
36
|
+
4. Returns `401 Unauthorized` with `{ error: "Unauthorized", message: "Valid authentication token required" }` on mismatch.
|
|
37
|
+
|
|
38
|
+
#### WebSocket Validator
|
|
39
|
+
|
|
40
|
+
`createWebSocketValidator(requiredToken)` returns a function compatible with the `ws` library's `verifyClient` callback:
|
|
41
|
+
|
|
42
|
+
1. If `requiredToken` is falsy, returns `true` (no auth required).
|
|
43
|
+
2. Parses the URL from `info.req.url`.
|
|
44
|
+
3. Extracts `?token=` query parameter.
|
|
45
|
+
4. Returns `token === requiredToken`.
|
|
46
|
+
|
|
47
|
+
#### Rate Limiting
|
|
48
|
+
|
|
49
|
+
`rateLimit(identifier, maxRequests = 100, windowMs = 60000)`:
|
|
50
|
+
|
|
51
|
+
1. Tracks request timestamps per `identifier` (typically IP address).
|
|
52
|
+
2. Filters to requests within the current window.
|
|
53
|
+
3. Returns `false` if the limit is exceeded, `true` otherwise.
|
|
54
|
+
4. Pushes the current timestamp on success.
|
|
55
|
+
|
|
56
|
+
`createRateLimitMiddleware(maxRequests = 100, windowMs = 60000)` returns Express middleware that:
|
|
57
|
+
- Identifies the client by `req.ip || req.connection.remoteAddress`.
|
|
58
|
+
- Returns `429 Too Many Requests` with `retryAfter` header when the limit is hit.
|
|
59
|
+
|
|
60
|
+
`cleanupRateLimit()`:
|
|
61
|
+
- Removes entries older than 1 hour from the rate limiter `Map`.
|
|
62
|
+
- Should be called periodically to prevent memory growth.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### Server Auth Implementation
|
|
67
|
+
|
|
68
|
+
Source: `src/server.js` (inline in `setupExpress()`)
|
|
69
|
+
|
|
70
|
+
The server implements authentication directly rather than using the `AuthManager` class:
|
|
71
|
+
|
|
72
|
+
1. **Pre-auth endpoints** -- `/auth-status` and `/auth-verify` are registered before the auth middleware, making them accessible without a token.
|
|
73
|
+
|
|
74
|
+
2. **Auth middleware** -- Registered conditionally when `!this.noAuth && this.auth`:
|
|
75
|
+
```js
|
|
76
|
+
const token = req.headers.authorization || req.query.token;
|
|
77
|
+
if (token !== `Bearer ${this.auth}` && token !== this.auth) {
|
|
78
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
This accepts either:
|
|
82
|
+
- `Authorization: Bearer <token>` header
|
|
83
|
+
- `?token=<token>` query parameter (raw token, no Bearer prefix)
|
|
84
|
+
- `Authorization: <token>` header (raw token, matched directly)
|
|
85
|
+
|
|
86
|
+
3. **WebSocket auth** -- The `verifyClient` callback on the `ws.Server`:
|
|
87
|
+
```js
|
|
88
|
+
verifyClient: (info) => {
|
|
89
|
+
if (!this.noAuth && this.auth) {
|
|
90
|
+
const url = new URL(info.req.url, 'ws://localhost');
|
|
91
|
+
const token = url.searchParams.get('token');
|
|
92
|
+
return token === this.auth;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### CLI Auth Flow
|
|
101
|
+
|
|
102
|
+
Source: `bin/cc-web.js`
|
|
103
|
+
|
|
104
|
+
| CLI Flag | Behavior |
|
|
105
|
+
|----------|----------|
|
|
106
|
+
| `--auth <token>` | Use the provided string as the auth token |
|
|
107
|
+
| `--disable-auth` | Set `noAuth = true`; no token is required |
|
|
108
|
+
| _(neither flag)_ | Auto-generate a random 10-character token using a charset that excludes ambiguous characters (`0`, `O`, `1`, `l`, `I`) |
|
|
109
|
+
|
|
110
|
+
The random token generator:
|
|
111
|
+
```js
|
|
112
|
+
const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
|
|
113
|
+
```
|
|
114
|
+
This produces tokens that are easy to read and transcribe (e.g., `kP7nVm3Qxt`).
|
|
115
|
+
|
|
116
|
+
The generated token is printed to the terminal with bold yellow ANSI formatting for visibility.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Client-Side Auth
|
|
121
|
+
|
|
122
|
+
### AuthManager (Browser)
|
|
123
|
+
|
|
124
|
+
Source: `src/public/auth.js`
|
|
125
|
+
|
|
126
|
+
A global singleton instantiated as `window.authManager`.
|
|
127
|
+
|
|
128
|
+
#### Token Storage
|
|
129
|
+
|
|
130
|
+
Tokens are stored in `sessionStorage` under the key `cc-web-token`. This means:
|
|
131
|
+
- The token persists across page reloads within the same tab.
|
|
132
|
+
- The token is cleared when the tab/browser is closed.
|
|
133
|
+
- Each browser tab can have its own independent auth state.
|
|
134
|
+
|
|
135
|
+
#### Initialization Flow
|
|
136
|
+
|
|
137
|
+
`initialize()` is called on page load:
|
|
138
|
+
|
|
139
|
+
1. Calls `GET /auth-status` to check if auth is required.
|
|
140
|
+
2. If auth is required and no stored token exists, calls `showLoginPrompt()`.
|
|
141
|
+
3. If auth is required and a token exists, calls `POST /auth-verify` to validate it.
|
|
142
|
+
4. If validation fails, clears the stored token and shows the login prompt.
|
|
143
|
+
5. Returns `true` if authenticated (or auth not required), `false` if the login prompt was shown.
|
|
144
|
+
|
|
145
|
+
#### Login Prompt
|
|
146
|
+
|
|
147
|
+
`showLoginPrompt()` creates a full-screen overlay (`z-index: 10000`) with:
|
|
148
|
+
- A password input field for the access token.
|
|
149
|
+
- A submit button that calls `verifyToken()`.
|
|
150
|
+
- Error display for invalid tokens.
|
|
151
|
+
- Visual feedback during verification (input disabled, button text changes to "Authenticating...").
|
|
152
|
+
- On success: overlay is removed and `window.location.reload()` triggers full re-initialization.
|
|
153
|
+
- On failure: error message is shown, form is re-enabled.
|
|
154
|
+
|
|
155
|
+
#### Helper Methods
|
|
156
|
+
|
|
157
|
+
| Method | Description |
|
|
158
|
+
|--------|-------------|
|
|
159
|
+
| `getAuthHeaders()` | Returns `{ Authorization: "Bearer <token>" }` or empty object |
|
|
160
|
+
| `getWebSocketUrl(baseUrl)` | Appends `?token=<token>` (or `&token=<token>`) to the WebSocket URL |
|
|
161
|
+
| `logout()` | Clears `sessionStorage`, reloads the page |
|
|
162
|
+
|
|
163
|
+
#### Integration Points
|
|
164
|
+
|
|
165
|
+
- `ClaudeCodeWebInterface.authFetch(url, options)` merges `authManager.getAuthHeaders()` into every fetch request.
|
|
166
|
+
- WebSocket connections use `authManager.getWebSocketUrl()` to append the token to the connection URL.
|
|
167
|
+
- `SessionTabManager.loadSessions()` and `closeSession()` include auth headers via `window.authManager.getAuthHeaders()`.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Bridge Specification
|
|
2
|
+
|
|
3
|
+
Bridges manage the spawning, I/O, and lifecycle of CLI agent processes via `node-pty`. Each bridge class owns a `Map<sessionId, BridgeSession>` of active pty sessions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Common Interface
|
|
8
|
+
|
|
9
|
+
Every bridge implements the same public API:
|
|
10
|
+
|
|
11
|
+
| Method | Signature | Description |
|
|
12
|
+
|--------|-----------|-------------|
|
|
13
|
+
| `startSession` | `(sessionId, options) => Promise<BridgeSession>` | Spawn the CLI process in a pty and wire up output/exit/error callbacks. Throws if `sessionId` already exists. |
|
|
14
|
+
| `sendInput` | `(sessionId, data) => Promise<void>` | Write raw data to the pty stdin. Throws if session is missing or inactive. |
|
|
15
|
+
| `resize` | `(sessionId, cols, rows) => Promise<void>` | Resize the pty dimensions. Logs a warning on failure instead of throwing. |
|
|
16
|
+
| `stopSession` | `(sessionId) => Promise<void>` | Send `SIGTERM`; after a 5-second grace period, send `SIGKILL`. Removes the session from the internal Map. |
|
|
17
|
+
| `getSession` | `(sessionId) => BridgeSession \| undefined` | Return the raw session object. |
|
|
18
|
+
| `getAllSessions` | `() => Array<{ id, workingDir, created, active }>` | List all sessions with metadata. |
|
|
19
|
+
| `cleanup` | `() => Promise<void>` | Stop all active sessions. |
|
|
20
|
+
|
|
21
|
+
### BridgeSession Object
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
{
|
|
25
|
+
process: IPty, // node-pty process handle
|
|
26
|
+
workingDir: string, // cwd the process was started in
|
|
27
|
+
created: Date,
|
|
28
|
+
active: boolean,
|
|
29
|
+
killTimeout: Timeout|null // handle for the SIGKILL escalation timer
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### startSession Options
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
{
|
|
37
|
+
workingDir: string, // defaults to process.cwd()
|
|
38
|
+
dangerouslySkipPermissions: boolean, // defaults to false (Claude/Codex only)
|
|
39
|
+
onOutput: (data: string) => void,
|
|
40
|
+
onExit: (code: number, signal: number) => void,
|
|
41
|
+
onError: (error: Error) => void,
|
|
42
|
+
cols: number, // defaults to 80
|
|
43
|
+
rows: number // defaults to 24
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### PTY Environment
|
|
48
|
+
|
|
49
|
+
All bridges spawn with these environment variables:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
TERM=xterm-256color
|
|
53
|
+
FORCE_COLOR=1
|
|
54
|
+
COLORTERM=truecolor
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The pty name is set to `xterm-color`.
|
|
58
|
+
|
|
59
|
+
### Command Discovery
|
|
60
|
+
|
|
61
|
+
Each bridge's constructor calls `findCommand()` which iterates through platform-specific candidate paths. Discovery works differently depending on whether the candidate is an absolute path or a bare command name:
|
|
62
|
+
|
|
63
|
+
- **Absolute paths:** Checked via `fs.existsSync()` only. On Windows, candidates are expanded with `.exe` and `.cmd` suffixes (e.g., `C:\Users\foo\.claude\local\claude` also checks `claude.exe` and `claude.cmd`).
|
|
64
|
+
- **Bare command names:** Checked via `commandExists()`, which runs `which` (Linux/macOS) or `where` (Windows) with a 5-second timeout to prevent hangs on systems with large PATH or network-mapped drives.
|
|
65
|
+
|
|
66
|
+
The first match wins. If none are found, a fallback default command name is used (e.g., `'claude'`).
|
|
67
|
+
|
|
68
|
+
### Spawn Watchdog
|
|
69
|
+
|
|
70
|
+
After a PTY process is spawned, a 30-second watchdog timer starts. If no data, exit, or error event fires within that period, the process is killed and an error is reported to the caller. This prevents zombie processes on Windows where ConPTY initialization can hang silently. The watchdog is cleared on the first `onData`, `onExit`, or `on('error')` event.
|
|
71
|
+
|
|
72
|
+
### Availability Check
|
|
73
|
+
|
|
74
|
+
`isAvailable()` returns `true` if the resolved command differs from the fallback default (meaning a specific path was found), or if the fallback command exists on PATH. The server checks `isAvailable()` before attempting to spawn a tool session, returning an immediate error to the client if the CLI is not installed.
|
|
75
|
+
|
|
76
|
+
The result is cached for 60 seconds (`_availableCache` / `_availableCacheTime`) to avoid repeated synchronous `where`/`which` calls that block the Node.js event loop. On Windows, `where.exe` can take several seconds when scanning large PATH variables or network-mapped drives, which would stall all WebSocket message processing during concurrent session starts.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## ClaudeBridge
|
|
81
|
+
|
|
82
|
+
Source: `src/claude-bridge.js`
|
|
83
|
+
|
|
84
|
+
### Command Search Paths
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
/home/ec2-user/.claude/local/claude
|
|
88
|
+
claude (PATH lookup)
|
|
89
|
+
claude-code (PATH lookup)
|
|
90
|
+
~/.claude/local/claude
|
|
91
|
+
~/.local/bin/claude
|
|
92
|
+
/usr/local/bin/claude
|
|
93
|
+
/usr/bin/claude
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Fallback: `'claude'`
|
|
97
|
+
|
|
98
|
+
### CLI Arguments
|
|
99
|
+
|
|
100
|
+
| Flag | Condition |
|
|
101
|
+
|------|-----------|
|
|
102
|
+
| `--dangerously-skip-permissions` | When `options.dangerouslySkipPermissions` is `true` |
|
|
103
|
+
|
|
104
|
+
### Trust Prompt Auto-Accept
|
|
105
|
+
|
|
106
|
+
The bridge monitors output for the string `"Do you trust the files in this folder?"`. On first detection, it sends `\r` (Enter) after a 500ms delay to auto-confirm the default trust option. This is tracked via a `trustPromptHandled` boolean to prevent duplicate submissions.
|
|
107
|
+
|
|
108
|
+
### Output Buffer
|
|
109
|
+
|
|
110
|
+
A rolling `dataBuffer` (max 10,000 chars, trimmed to last 5,000) is maintained for trust prompt detection. This buffer is internal to the bridge and separate from the server-level `outputBuffer`.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## CodexBridge
|
|
115
|
+
|
|
116
|
+
Source: `src/codex-bridge.js`
|
|
117
|
+
|
|
118
|
+
### Command Search Paths
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
~/.codex/local/codex
|
|
122
|
+
codex (PATH lookup)
|
|
123
|
+
codex-code (PATH lookup)
|
|
124
|
+
~/.local/bin/codex
|
|
125
|
+
/usr/local/bin/codex
|
|
126
|
+
/usr/bin/codex
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Fallback: `'codex'`
|
|
130
|
+
|
|
131
|
+
### CLI Arguments
|
|
132
|
+
|
|
133
|
+
| Flag | Condition |
|
|
134
|
+
|------|-----------|
|
|
135
|
+
| `--dangerously-bypass-approvals-and-sandbox` | When `options.dangerouslySkipPermissions` is `true` |
|
|
136
|
+
|
|
137
|
+
### Notes
|
|
138
|
+
|
|
139
|
+
- No trust prompt handling (Codex does not prompt for folder trust).
|
|
140
|
+
- Maintains an internal `dataBuffer` (same 10,000/5,000 limits) for future prompt detection.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## AgentBridge
|
|
145
|
+
|
|
146
|
+
Source: `src/agent-bridge.js`
|
|
147
|
+
|
|
148
|
+
### Command Search Paths
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
~/.cursor/local/cursor-agent
|
|
152
|
+
cursor-agent (PATH lookup)
|
|
153
|
+
~/.local/bin/cursor-agent
|
|
154
|
+
/usr/local/bin/cursor-agent
|
|
155
|
+
/usr/bin/cursor-agent
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Fallback: `'cursor-agent'`
|
|
159
|
+
|
|
160
|
+
### CLI Arguments
|
|
161
|
+
|
|
162
|
+
None. The agent is spawned with an empty args array.
|
|
163
|
+
|
|
164
|
+
### Notes
|
|
165
|
+
|
|
166
|
+
- No `dangerouslySkipPermissions` option. The AgentBridge `startSession` does not destructure or use that option.
|
|
167
|
+
- Maintains an internal `dataBuffer` (same 10,000/5,000 limits) for future prompt detection.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Planned Bridges
|
|
172
|
+
|
|
173
|
+
The following bridges are planned but not yet implemented. They should follow the same common interface described above.
|
|
174
|
+
|
|
175
|
+
### BaseBridge (Planned)
|
|
176
|
+
|
|
177
|
+
A shared base class that extracts the duplicated logic from ClaudeBridge, CodexBridge, and AgentBridge:
|
|
178
|
+
|
|
179
|
+
- Cross-platform command discovery (`find*Command`, `commandExists`)
|
|
180
|
+
- Session lifecycle management (`startSession`, `stopSession`, `sendInput`, `resize`)
|
|
181
|
+
- Output buffering with configurable limits
|
|
182
|
+
- Kill timeout escalation (SIGTERM then SIGKILL after 5s)
|
|
183
|
+
- PTY environment setup
|
|
184
|
+
|
|
185
|
+
Each concrete bridge would extend `BaseBridge` and provide:
|
|
186
|
+
- `commandSearchPaths` -- ordered array of paths to search
|
|
187
|
+
- `fallbackCommand` -- default command name
|
|
188
|
+
- `buildArgs(options)` -- returns the CLI arguments array
|
|
189
|
+
- `onDataHook(data, buffer)` -- optional per-bridge output processing (e.g., trust prompt handling)
|
|
190
|
+
|
|
191
|
+
### CopilotBridge (Planned)
|
|
192
|
+
|
|
193
|
+
- Command: `copilot`
|
|
194
|
+
- Installation: `npm install -g @github/copilot`
|
|
195
|
+
- Search paths: standard locations following the same pattern as existing bridges
|
|
196
|
+
|
|
197
|
+
### GeminiBridge (Planned)
|
|
198
|
+
|
|
199
|
+
- Command: `gemini`
|
|
200
|
+
- Installation: `npm install -g @google/gemini-cli`
|
|
201
|
+
- Search paths: standard locations following the same pattern as existing bridges
|
|
202
|
+
|
|
203
|
+
### TerminalBridge (Planned)
|
|
204
|
+
|
|
205
|
+
Opens a raw shell session rather than an AI agent.
|
|
206
|
+
|
|
207
|
+
- Linux/macOS: spawns `$SHELL` (defaults to `/bin/bash`)
|
|
208
|
+
- Windows: spawns `powershell.exe` or `cmd.exe`
|
|
209
|
+
- No `dangerouslySkipPermissions` or AI-specific flags
|
|
210
|
+
- Useful for running manual commands alongside agent sessions
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Client Application Specification
|
|
2
|
+
|
|
3
|
+
The frontend is a single-page application served from `src/public/`. It runs entirely in the browser with no build step -- all JavaScript is loaded directly via `<script>` tags.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Dependencies
|
|
8
|
+
|
|
9
|
+
| Library | Version | Source | Purpose |
|
|
10
|
+
|---------|---------|--------|---------|
|
|
11
|
+
| xterm.js | 5.3.0 | unpkg CDN | Terminal emulator component |
|
|
12
|
+
| xterm-addon-fit | 0.8.0 | unpkg CDN | Auto-fit terminal to container |
|
|
13
|
+
| xterm-addon-web-links | 0.9.0 | unpkg CDN | Clickable URLs in terminal output |
|
|
14
|
+
| JetBrains Mono | -- | Google Fonts | Monospace font for terminal |
|
|
15
|
+
| Inter | -- | Google Fonts | UI font for headers, tabs, controls |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ClaudeCodeWebInterface
|
|
20
|
+
|
|
21
|
+
Source: `src/public/app.js` (~2100 lines)
|
|
22
|
+
|
|
23
|
+
The main application controller. Instantiated once on page load.
|
|
24
|
+
|
|
25
|
+
### Constructor Properties
|
|
26
|
+
|
|
27
|
+
| Property | Type | Description |
|
|
28
|
+
|----------|------|-------------|
|
|
29
|
+
| `terminal` | Terminal | xterm.js instance |
|
|
30
|
+
| `fitAddon` | FitAddon | Auto-resize addon |
|
|
31
|
+
| `webLinksAddon` | WebLinksAddon | Clickable URL addon |
|
|
32
|
+
| `socket` | WebSocket | Active WebSocket connection |
|
|
33
|
+
| `connectionId` | string | Server-assigned connection UUID |
|
|
34
|
+
| `currentClaudeSessionId` | string | Currently joined session ID |
|
|
35
|
+
| `currentClaudeSessionName` | string | Currently joined session name |
|
|
36
|
+
| `reconnectAttempts` | number | Counter for exponential backoff |
|
|
37
|
+
| `maxReconnectAttempts` | number | 5 |
|
|
38
|
+
| `reconnectDelay` | number | 1000ms base delay |
|
|
39
|
+
| `folderMode` | boolean | Always `true` |
|
|
40
|
+
| `currentFolderPath` | string | Current path in folder browser |
|
|
41
|
+
| `claudeSessions` | Array | Cached session list from server |
|
|
42
|
+
| `isCreatingNewSession` | boolean | Flag for new-session folder picker flow |
|
|
43
|
+
| `isMobile` | boolean | Detected via `detectMobile()` |
|
|
44
|
+
| `currentMode` | string | `'chat'` |
|
|
45
|
+
| `planDetector` | PlanDetector | Plan mode detection instance |
|
|
46
|
+
| `aliases` | Object | `{ claude: 'Claude', codex: 'Codex' }` -- populated from `/api/config` |
|
|
47
|
+
| `sessionTabManager` | SessionTabManager | Tab bar controller |
|
|
48
|
+
| `usageStats` | Object | Latest usage data |
|
|
49
|
+
| `sessionTimer` | Object | Session timer data from server |
|
|
50
|
+
| `sessionTimerInterval` | Interval | Client-side timer tick |
|
|
51
|
+
| `splitContainer` | Object | Split/tile view controller |
|
|
52
|
+
|
|
53
|
+
### Initialization Flow
|
|
54
|
+
|
|
55
|
+
`init()` performs these steps in order:
|
|
56
|
+
|
|
57
|
+
1. Call `window.authManager.initialize()`. If it returns `false`, the login prompt is displayed and initialization halts.
|
|
58
|
+
2. Fetch `/api/config` to get folder mode, aliases, and base folder.
|
|
59
|
+
3. Set up the terminal (xterm.js with fit addon and web links addon).
|
|
60
|
+
4. Establish WebSocket connection.
|
|
61
|
+
5. Set up UI event handlers (folder browser, session controls, resize observer).
|
|
62
|
+
6. Initialize `SessionTabManager`.
|
|
63
|
+
7. Initialize `PlanDetector`.
|
|
64
|
+
8. Load existing sessions from the server.
|
|
65
|
+
9. Start usage polling interval.
|
|
66
|
+
|
|
67
|
+
### WebSocket Management
|
|
68
|
+
|
|
69
|
+
- **Connection:** Constructs the URL with the session token via `authManager.getWebSocketUrl()`. Reconnects automatically with exponential backoff up to `maxReconnectAttempts`.
|
|
70
|
+
- **Message handling:** Routes incoming messages by `type` field to appropriate handlers (output rendering, session state updates, usage updates, etc.).
|
|
71
|
+
- **Output rendering:** Writes raw terminal data directly to xterm.js via `terminal.write(data)`. Also feeds data to `planDetector.processOutput(data)` and `sessionTabManager.markSessionActivity()`.
|
|
72
|
+
|
|
73
|
+
### Terminal Configuration
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
{
|
|
77
|
+
cursorBlink: true,
|
|
78
|
+
theme: {
|
|
79
|
+
background: '#0d1117',
|
|
80
|
+
foreground: '#f0f6fc',
|
|
81
|
+
cursor: '#f0f6fc',
|
|
82
|
+
// Full 16-color ANSI palette configured
|
|
83
|
+
},
|
|
84
|
+
fontFamily: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace",
|
|
85
|
+
fontSize: 14, // 13 on mobile
|
|
86
|
+
lineHeight: 1.2,
|
|
87
|
+
scrollback: 10000,
|
|
88
|
+
allowProposedApi: true
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The terminal auto-resizes via `FitAddon` triggered by a `ResizeObserver` on the terminal container, debounced to prevent excessive resize messages.
|
|
93
|
+
|
|
94
|
+
### Folder Browser
|
|
95
|
+
|
|
96
|
+
A modal dialog for selecting working directories:
|
|
97
|
+
- Fetches directory listings from `GET /api/folders?path=...`.
|
|
98
|
+
- Supports navigating up to parent (within `baseFolder` bounds).
|
|
99
|
+
- Supports creating new folders via `POST /api/create-folder`.
|
|
100
|
+
- On selection, either creates a new session or sets the working directory.
|
|
101
|
+
|
|
102
|
+
### Agent Start Controls
|
|
103
|
+
|
|
104
|
+
When a session has no running agent, the UI presents buttons to start:
|
|
105
|
+
- **Claude** -- sends `{ type: "start_claude" }`
|
|
106
|
+
- **Codex** -- sends `{ type: "start_codex" }`
|
|
107
|
+
- **Copilot** -- sends `{ type: "start_copilot" }`
|
|
108
|
+
- **Gemini** -- sends `{ type: "start_gemini" }`
|
|
109
|
+
- **Terminal** -- sends `{ type: "start_terminal" }`
|
|
110
|
+
|
|
111
|
+
Each button's label uses the configured alias from `/api/config`. Buttons are disabled for tools that are not available on the server.
|
|
112
|
+
|
|
113
|
+
A 45-second client-side timeout acts as a safety net: if no `_started`, `error`, or `exit` message arrives from the server within that window, the loading spinner is replaced with an error message. This prevents the UI from getting permanently stuck if the server fails to respond (e.g., due to a process hang on Windows).
|
|
114
|
+
|
|
115
|
+
### Usage Dashboard
|
|
116
|
+
|
|
117
|
+
Polls usage data via the WebSocket `get_usage` message at regular intervals. Displays:
|
|
118
|
+
- Session timer (elapsed/remaining in the current session window)
|
|
119
|
+
- Token consumption (input, output, cache)
|
|
120
|
+
- Cost tracking
|
|
121
|
+
- Burn rate and depletion predictions
|
|
122
|
+
- Plan information
|
|
123
|
+
|
|
124
|
+
### Authenticated Fetch
|
|
125
|
+
|
|
126
|
+
`authFetch(url, options)` wraps `fetch()` to automatically include auth headers:
|
|
127
|
+
```js
|
|
128
|
+
const authHeaders = window.authManager.getAuthHeaders();
|
|
129
|
+
const mergedOptions = {
|
|
130
|
+
...options,
|
|
131
|
+
headers: { ...authHeaders, ...(options.headers || {}) }
|
|
132
|
+
};
|
|
133
|
+
return fetch(url, mergedOptions);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## SessionTabManager
|
|
139
|
+
|
|
140
|
+
Source: `src/public/session-manager.js` (~1125 lines)
|
|
141
|
+
|
|
142
|
+
Manages the browser-style tab bar for multi-session support.
|
|
143
|
+
|
|
144
|
+
### State
|
|
145
|
+
|
|
146
|
+
| Property | Type | Description |
|
|
147
|
+
|----------|------|-------------|
|
|
148
|
+
| `tabs` | `Map<sessionId, HTMLElement>` | Tab DOM elements |
|
|
149
|
+
| `activeSessions` | `Map<sessionId, SessionData>` | Session metadata |
|
|
150
|
+
| `activeTabId` | string | Currently active tab's session ID |
|
|
151
|
+
| `tabOrder` | Array | Visual ordering of tab IDs |
|
|
152
|
+
| `tabHistory` | Array | Most-recently-used ordering (max 50 entries) |
|
|
153
|
+
| `notificationsEnabled` | boolean | Whether desktop notifications are permitted |
|
|
154
|
+
|
|
155
|
+
### Tab Operations
|
|
156
|
+
|
|
157
|
+
| Operation | Trigger |
|
|
158
|
+
|-----------|---------|
|
|
159
|
+
| Create | "New Tab" button, Ctrl/Cmd+T |
|
|
160
|
+
| Close | Close button, Ctrl/Cmd+W, middle-click |
|
|
161
|
+
| Switch | Tab click, Ctrl/Cmd+Tab (next), Ctrl/Cmd+Shift+Tab (prev), Alt+1-9 |
|
|
162
|
+
| Rename | Double-click tab |
|
|
163
|
+
| Reorder | Drag and drop |
|
|
164
|
+
| Close Others | Right-click context menu |
|
|
165
|
+
|
|
166
|
+
### Tab Display Name Resolution
|
|
167
|
+
|
|
168
|
+
1. If the session name is customized (does not start with "Session " containing ":"), use it.
|
|
169
|
+
2. Otherwise, extract the last path component of `workingDir` as the folder name.
|
|
170
|
+
3. Fall back to the default session name.
|
|
171
|
+
|
|
172
|
+
### Drag and Drop
|
|
173
|
+
|
|
174
|
+
Tabs are `draggable="true"`. Reordering uses these events:
|
|
175
|
+
- `dragstart` -- stores the session ID and adds a `.dragging` class.
|
|
176
|
+
- `dragover` -- calculates insertion point via `getDragAfterElement()` (finds the closest tab element based on mouse X position).
|
|
177
|
+
- `dragend` -- syncs the visual order to `tabOrder` and updates overflow menus.
|
|
178
|
+
|
|
179
|
+
### Mobile Behavior
|
|
180
|
+
|
|
181
|
+
On viewports <= 768px wide:
|
|
182
|
+
- Only the first 2 tabs are visible.
|
|
183
|
+
- An overflow dropdown shows remaining tabs with a count badge.
|
|
184
|
+
- Tabs are automatically reordered by `lastAccessed` timestamp so the most recently used tabs are always visible.
|
|
185
|
+
|
|
186
|
+
### Status Indicators
|
|
187
|
+
|
|
188
|
+
Each tab has a status dot with states:
|
|
189
|
+
- **idle** -- default, no activity
|
|
190
|
+
- **active** -- agent is producing output (adds `.pulse` animation)
|
|
191
|
+
- **error** -- an error occurred in the session
|
|
192
|
+
- **unread** -- output completed in a background tab (blue indicator)
|
|
193
|
+
|
|
194
|
+
### Unread Detection
|
|
195
|
+
|
|
196
|
+
When a session transitions from `active` to `idle` in a background tab (not the currently viewed tab), it is marked as unread. This happens via two mechanisms:
|
|
197
|
+
|
|
198
|
+
1. **Work completion timeout (90s):** If no new output arrives for 90 seconds while the status was `active`, the tab is marked as unread and a notification is sent.
|
|
199
|
+
2. **Command completion patterns:** Output is checked against regex patterns for common completion indicators:
|
|
200
|
+
- `build successful`, `compilation finished`, `tests passed`
|
|
201
|
+
- `deployment complete`, `npm install completed`
|
|
202
|
+
- `successfully compiled`, `Done in X.Xs`
|
|
203
|
+
|
|
204
|
+
### Desktop Notifications
|
|
205
|
+
|
|
206
|
+
- Requests permission on first load (deferred by 2 seconds).
|
|
207
|
+
- Shows a prompt banner if permission is `"default"`.
|
|
208
|
+
- Sends `Notification` when the page is not visible and the event is for a background tab.
|
|
209
|
+
- Falls back to in-page toast notifications + vibration on mobile.
|
|
210
|
+
- Notifications auto-close after 5 seconds.
|
|
211
|
+
- Clicking a notification switches to the relevant tab and focuses the window.
|
|
212
|
+
|
|
213
|
+
### Keyboard Shortcuts
|
|
214
|
+
|
|
215
|
+
| Shortcut | Action |
|
|
216
|
+
|----------|--------|
|
|
217
|
+
| Ctrl/Cmd + T | New tab |
|
|
218
|
+
| Ctrl/Cmd + W | Close current tab |
|
|
219
|
+
| Ctrl/Cmd + Tab | Next tab (uses MRU history first, falls back to visual order) |
|
|
220
|
+
| Ctrl/Cmd + Shift + Tab | Previous tab (visual order) |
|
|
221
|
+
| Alt + 1-9 | Switch to tab by index |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## PlanDetector
|
|
226
|
+
|
|
227
|
+
Source: `src/public/plan-detector.js` (~186 lines)
|
|
228
|
+
|
|
229
|
+
Monitors terminal output for plan mode activation and plan content.
|
|
230
|
+
|
|
231
|
+
### Detection Markers
|
|
232
|
+
|
|
233
|
+
**Plan mode start indicators:**
|
|
234
|
+
- `"Plan mode is active"`
|
|
235
|
+
- `"you MUST NOT make any edits"`
|
|
236
|
+
- `"present your plan by calling the ExitPlanMode tool"`
|
|
237
|
+
- `"Starting plan mode"`
|
|
238
|
+
|
|
239
|
+
**Completed plan patterns:**
|
|
240
|
+
- `## Implementation Plan:`
|
|
241
|
+
- `### <number>. ` (numbered sections)
|
|
242
|
+
- `## Plan:`
|
|
243
|
+
- `### Plan Overview`
|
|
244
|
+
- `## Proposed Solution:`
|
|
245
|
+
|
|
246
|
+
**Plan mode end indicators:**
|
|
247
|
+
- `"User has approved your plan"`
|
|
248
|
+
- `"You can now start coding"`
|
|
249
|
+
- `"Plan mode exited"`
|
|
250
|
+
- `"Exiting plan mode"`
|
|
251
|
+
|
|
252
|
+
### Processing Flow
|
|
253
|
+
|
|
254
|
+
1. Raw output is appended to an internal buffer (max 10,000 entries, trimmed to 5,000).
|
|
255
|
+
2. Recent text is extracted with ANSI codes stripped.
|
|
256
|
+
3. Checks are performed in order: plan start, completed plan content, plan end.
|
|
257
|
+
4. Callbacks `onPlanModeChange(isActive)` and `onPlanDetected(plan)` fire on state changes.
|
|
258
|
+
|
|
259
|
+
### Plan Extraction
|
|
260
|
+
|
|
261
|
+
Three strategies are attempted in order:
|
|
262
|
+
1. Match `## Implementation Plan:` to a terminal prompt indicator.
|
|
263
|
+
2. Match a structured plan header with 2+ `###` subsections.
|
|
264
|
+
3. Match any plan-like content in the last 5,000 characters.
|
|
265
|
+
|
|
266
|
+
The extracted plan text is cleaned of ANSI codes and normalized line endings.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## AuthManager (Client)
|
|
271
|
+
|
|
272
|
+
Source: `src/public/auth.js` (~245 lines)
|
|
273
|
+
|
|
274
|
+
See the [Authentication Specification](authentication.md) for full details. Key points:
|
|
275
|
+
|
|
276
|
+
- Global singleton at `window.authManager`.
|
|
277
|
+
- Token persisted in `sessionStorage` (per-tab, cleared on browser close).
|
|
278
|
+
- Full-screen login overlay with password input.
|
|
279
|
+
- `getAuthHeaders()` returns `{ Authorization: "Bearer <token>" }`.
|
|
280
|
+
- `getWebSocketUrl(baseUrl)` appends `?token=<token>` to WebSocket URLs.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## PWA Support
|
|
285
|
+
|
|
286
|
+
### Service Worker
|
|
287
|
+
|
|
288
|
+
Source: `src/public/service-worker.js`
|
|
289
|
+
|
|
290
|
+
- **Cache name:** `claude-code-web-v1`
|
|
291
|
+
- **Precached resources:** `/`, `/index.html`, `/style.css`, `/app.js`, `/session-manager.js`, `/plan-detector.js`
|
|
292
|
+
- **Strategy for API/WebSocket routes:** Network only, with a 503 offline fallback.
|
|
293
|
+
- **Strategy for static assets:** Network first, cache on success, fall back to cache when offline.
|
|
294
|
+
- Activates immediately via `skipWaiting()` + `clients.claim()`.
|
|
295
|
+
- Cleans up old caches on activation.
|
|
296
|
+
|
|
297
|
+
### Manifest
|
|
298
|
+
|
|
299
|
+
Source: `src/public/manifest.json`
|
|
300
|
+
|
|
301
|
+
Provides installable PWA metadata. Icons are dynamically generated SVGs served by the Express server at `/icon-{size}.png`.
|
|
302
|
+
|
|
303
|
+
### Dynamic Icon Generation
|
|
304
|
+
|
|
305
|
+
The server generates SVG icons at sizes 16, 32, 144, 180, 192, and 512 pixels:
|
|
306
|
+
- Dark background (`#1a1a1a`) with rounded corners.
|
|
307
|
+
- Monospace "CC" text in orange (`#ff6b00`).
|
|
308
|
+
- Served as `image/svg+xml` with a 1-year `Cache-Control`.
|