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,170 @@
|
|
|
1
|
+
# SessionStore Specification
|
|
2
|
+
|
|
3
|
+
Source: `src/utils/session-store.js`
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`SessionStore` provides atomic, file-based persistence for session data. It serializes the server's in-memory `Map<sessionId, Session>` to JSON on disk and restores it on startup.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Storage Path
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
~/.claude-code-web/sessions.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The directory `~/.claude-code-web/` is created on initialization (`mkdir -p` equivalent) if it does not exist. Future versions will migrate to `~/.ai-or-die/sessions.json`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Constructor
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
new SessionStore()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Sets `this.storageDir` to `path.join(os.homedir(), '.claude-code-web')`.
|
|
28
|
+
- Sets `this.sessionsFile` to `path.join(this.storageDir, 'sessions.json')`.
|
|
29
|
+
- Calls `initializeStorage()` to ensure the directory exists.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Methods
|
|
34
|
+
|
|
35
|
+
### `saveSessions(sessions: Map) => Promise<boolean>`
|
|
36
|
+
|
|
37
|
+
Serializes the session Map to disk.
|
|
38
|
+
|
|
39
|
+
**Process:**
|
|
40
|
+
|
|
41
|
+
1. Ensures `storageDir` exists via `fs.mkdir(recursive: true)`.
|
|
42
|
+
2. Converts the `Map` to an array of plain objects, applying these transformations:
|
|
43
|
+
- `active` is always set to `false` (processes cannot survive restarts).
|
|
44
|
+
- `connections` is serialized as an empty array (WebSocket references are not persistable).
|
|
45
|
+
- `outputBuffer` is truncated to the **last 100 lines** to limit file size.
|
|
46
|
+
- `sessionStartTime` and `sessionUsage` are preserved if present, otherwise default values are used.
|
|
47
|
+
3. Wraps the array in an envelope:
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"version": "1.0",
|
|
51
|
+
"savedAt": "2026-02-05T10:00:00.000Z",
|
|
52
|
+
"sessions": [ ... ]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
4. Writes to a temp file (`sessions.json.tmp`) first.
|
|
56
|
+
5. Renames temp file to `sessions.json` (atomic write to prevent corruption on crash).
|
|
57
|
+
|
|
58
|
+
**Returns:** `true` on success, `false` on error.
|
|
59
|
+
|
|
60
|
+
### `loadSessions() => Promise<Map>`
|
|
61
|
+
|
|
62
|
+
Restores sessions from disk into an in-memory `Map`.
|
|
63
|
+
|
|
64
|
+
**Process:**
|
|
65
|
+
|
|
66
|
+
1. Checks file existence via `fs.access`.
|
|
67
|
+
2. Reads file contents as UTF-8.
|
|
68
|
+
3. Handles empty/whitespace-only files by returning an empty `Map`.
|
|
69
|
+
4. Parses JSON with error recovery:
|
|
70
|
+
- On parse failure, renames the corrupted file to `sessions.json.corrupted.<timestamp>` for forensics, then returns an empty `Map`.
|
|
71
|
+
5. Validates the parsed structure:
|
|
72
|
+
- Must be a non-null object with a `sessions` array property.
|
|
73
|
+
- Returns an empty `Map` if the structure is invalid.
|
|
74
|
+
6. Checks the `savedAt` timestamp -- if older than **7 days**, the data is considered stale and an empty `Map` is returned.
|
|
75
|
+
7. For each session entry:
|
|
76
|
+
- Skips entries without an `id` field.
|
|
77
|
+
- Deserializes `created` and `lastActivity` back to `Date` objects.
|
|
78
|
+
- Forces `active = false`.
|
|
79
|
+
- Converts `connections` back to an empty `Set`.
|
|
80
|
+
- Restores `outputBuffer` (defaults to `[]`).
|
|
81
|
+
- Sets `maxBufferSize` to `1000`.
|
|
82
|
+
- Restores `usageData` if available.
|
|
83
|
+
|
|
84
|
+
**Returns:** `Map<sessionId, Session>`.
|
|
85
|
+
|
|
86
|
+
**Error handling:** If the file does not exist (`ENOENT`), silently returns an empty `Map`. Other errors are logged to stderr.
|
|
87
|
+
|
|
88
|
+
### `clearOldSessions() => Promise<boolean>`
|
|
89
|
+
|
|
90
|
+
Deletes the `sessions.json` file entirely.
|
|
91
|
+
|
|
92
|
+
### `getSessionMetadata() => Promise<Object>`
|
|
93
|
+
|
|
94
|
+
Returns metadata about the persistence file without loading full session data.
|
|
95
|
+
|
|
96
|
+
**Success response:**
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"exists": true,
|
|
100
|
+
"savedAt": "2026-02-05T10:00:00.000Z",
|
|
101
|
+
"sessionCount": 5,
|
|
102
|
+
"fileSize": 12345,
|
|
103
|
+
"version": "1.0"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Failure response:**
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"exists": false,
|
|
111
|
+
"error": "ENOENT: no such file or directory"
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Serialized Session Schema
|
|
118
|
+
|
|
119
|
+
Each session in the `sessions` array has this shape:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"id": "uuid",
|
|
124
|
+
"name": "Session 2/5/2026, 10:30:00 AM",
|
|
125
|
+
"created": "2026-02-05T10:30:00.000Z",
|
|
126
|
+
"lastActivity": "2026-02-05T11:00:00.000Z",
|
|
127
|
+
"workingDir": "/home/user/project",
|
|
128
|
+
"active": false,
|
|
129
|
+
"outputBuffer": ["line1", "line2", "...up to 100"],
|
|
130
|
+
"connections": [],
|
|
131
|
+
"lastAccessed": 1738750800000,
|
|
132
|
+
"sessionStartTime": "2026-02-05T10:30:00.000Z",
|
|
133
|
+
"sessionUsage": {
|
|
134
|
+
"requests": 0,
|
|
135
|
+
"inputTokens": 0,
|
|
136
|
+
"outputTokens": 0,
|
|
137
|
+
"cacheTokens": 0,
|
|
138
|
+
"totalCost": 0,
|
|
139
|
+
"models": {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Integration with Server
|
|
147
|
+
|
|
148
|
+
The server calls `SessionStore` in these contexts:
|
|
149
|
+
|
|
150
|
+
| Trigger | Method |
|
|
151
|
+
|---------|--------|
|
|
152
|
+
| Server startup | `loadSessions()` -- restores sessions into `claudeSessions` Map |
|
|
153
|
+
| Every 30 seconds | `saveSessions()` via `setInterval` |
|
|
154
|
+
| After session create | `saveSessions()` |
|
|
155
|
+
| After session delete | `saveSessions()` |
|
|
156
|
+
| On `SIGINT` / `SIGTERM` | `saveSessions()` via `handleShutdown()` |
|
|
157
|
+
| On `beforeExit` | `saveSessions()` |
|
|
158
|
+
| `GET /api/sessions/persistence` | `getSessionMetadata()` |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Corruption Recovery
|
|
163
|
+
|
|
164
|
+
The store handles corruption gracefully:
|
|
165
|
+
|
|
166
|
+
1. **Empty file** -- Detected by checking `!data || !data.trim()`. Returns empty Map.
|
|
167
|
+
2. **Invalid JSON** -- Caught by `JSON.parse` try/catch. The corrupted file is renamed with a `.corrupted.<timestamp>` suffix for manual inspection, then returns empty Map.
|
|
168
|
+
3. **Invalid structure** -- If parsed data is not an object or lacks a `sessions` array, returns empty Map.
|
|
169
|
+
4. **Stale data** -- Sessions older than 7 days are discarded entirely.
|
|
170
|
+
5. **Invalid session entries** -- Individual entries without an `id` field are silently skipped.
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Usage Analytics Specification
|
|
2
|
+
|
|
3
|
+
Two classes work together to track, analyze, and predict API usage: `UsageReader` reads raw JSONL log files from the Claude CLI, and `UsageAnalytics` performs session windowing, burn rate analysis, and depletion predictions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## UsageReader
|
|
8
|
+
|
|
9
|
+
Source: `src/usage-reader.js`
|
|
10
|
+
|
|
11
|
+
### Overview
|
|
12
|
+
|
|
13
|
+
Reads usage data from Claude CLI's JSONL log files at `~/.claude/projects/`. Each project directory contains per-conversation JSONL files with one JSON object per line, recording every API request and response.
|
|
14
|
+
|
|
15
|
+
### Constructor
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
new UsageReader(sessionDurationHours = 5)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `claudeProjectsPath`: `~/.claude/projects/`
|
|
22
|
+
- Cache with a 5-second TTL for `getUsageStats()`.
|
|
23
|
+
- `sessionDurationHours`: configurable window size (default 5 hours).
|
|
24
|
+
|
|
25
|
+
### JSONL File Discovery
|
|
26
|
+
|
|
27
|
+
`findJsonlFiles(onlyRecent = false)`:
|
|
28
|
+
- Recursively scans all project directories under `~/.claude/projects/`.
|
|
29
|
+
- Collects all `.jsonl` files.
|
|
30
|
+
- When `onlyRecent = true`, filters to files modified within the last 24 hours.
|
|
31
|
+
|
|
32
|
+
### Entry Parsing
|
|
33
|
+
|
|
34
|
+
`readJsonlFile(filePath, cutoffTime)`:
|
|
35
|
+
- Streams the file line by line via `readline`.
|
|
36
|
+
- Parses each line as JSON. Malformed lines are silently ignored.
|
|
37
|
+
- Filters by `cutoffTime` (only entries with `timestamp >= cutoffTime`).
|
|
38
|
+
- Deduplicates entries within each file using a hash of `message_id:request_id`.
|
|
39
|
+
- Extracts only assistant messages with usage data (`entry.type === 'assistant'` or `entry.message.role === 'assistant'`).
|
|
40
|
+
|
|
41
|
+
### Extracted Entry Fields
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
{
|
|
45
|
+
timestamp: string,
|
|
46
|
+
model: 'opus' | 'sonnet' | 'haiku' | 'unknown',
|
|
47
|
+
inputTokens: number,
|
|
48
|
+
outputTokens: number,
|
|
49
|
+
cacheCreationTokens: number,
|
|
50
|
+
cacheReadTokens: number,
|
|
51
|
+
totalCost: number,
|
|
52
|
+
sessionId: string,
|
|
53
|
+
messageId: string,
|
|
54
|
+
requestId: string
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Model Normalization
|
|
59
|
+
|
|
60
|
+
`normalizeModelName(model)`:
|
|
61
|
+
- Any model string containing `"opus"` -> `"opus"`
|
|
62
|
+
- Any model string containing `"sonnet"` -> `"sonnet"`
|
|
63
|
+
- Any model string containing `"haiku"` -> `"haiku"`
|
|
64
|
+
- Otherwise -> `"unknown"`
|
|
65
|
+
|
|
66
|
+
### Pricing
|
|
67
|
+
|
|
68
|
+
Costs are calculated per-token when `usage.total_cost` is not available:
|
|
69
|
+
|
|
70
|
+
| Model | Input (per 1M tokens) | Output (per 1M tokens) | Cache Creation | Cache Read |
|
|
71
|
+
|-------|----------------------|------------------------|----------------|------------|
|
|
72
|
+
| Opus | $15.00 | $75.00 | Same as input | 10% of input |
|
|
73
|
+
| Sonnet | $3.00 | $15.00 | Same as input | 10% of input |
|
|
74
|
+
| Haiku | $0.25 | $1.25 | Same as input | 10% of input |
|
|
75
|
+
|
|
76
|
+
When `usage.total_cost` is present:
|
|
77
|
+
- If the value is > 1, it is assumed to be in cents and divided by 100.
|
|
78
|
+
- Otherwise, used as-is (dollars).
|
|
79
|
+
|
|
80
|
+
### Session Boundary Detection
|
|
81
|
+
|
|
82
|
+
`getDailySessionBoundaries()`:
|
|
83
|
+
|
|
84
|
+
1. Gets all entries for the current calendar day (midnight to 23:59:59).
|
|
85
|
+
2. Sorts chronologically.
|
|
86
|
+
3. Groups into sessions: a new session starts when an entry falls outside the current session's window (start + `sessionDurationHours`).
|
|
87
|
+
4. Each session start is rounded down to the nearest hour.
|
|
88
|
+
5. Session end is `startTime + sessionDurationHours` or midnight, whichever is earlier.
|
|
89
|
+
6. Returns array of `{ sessionNumber, startTime, endTime, sessionId }`.
|
|
90
|
+
|
|
91
|
+
`getCurrentSession()`:
|
|
92
|
+
- Calls `getDailySessionBoundaries()` and returns the session containing `now`, or `null`.
|
|
93
|
+
|
|
94
|
+
### Key Methods
|
|
95
|
+
|
|
96
|
+
#### `getUsageStats(hoursBack = 24)`
|
|
97
|
+
|
|
98
|
+
Returns aggregated statistics for the given lookback window. Cached for 5 seconds.
|
|
99
|
+
|
|
100
|
+
**Response:**
|
|
101
|
+
```js
|
|
102
|
+
{
|
|
103
|
+
requests: number,
|
|
104
|
+
totalTokens: number, // inputTokens + outputTokens (no cache tokens)
|
|
105
|
+
inputTokens: number,
|
|
106
|
+
outputTokens: number,
|
|
107
|
+
cacheCreationTokens: number,
|
|
108
|
+
cacheReadTokens: number,
|
|
109
|
+
cacheTokens: number, // cacheCreation + cacheRead
|
|
110
|
+
totalCost: number,
|
|
111
|
+
periodHours: number,
|
|
112
|
+
firstEntry: string,
|
|
113
|
+
lastEntry: string,
|
|
114
|
+
models: { [model]: { requests, inputTokens, outputTokens, cost } },
|
|
115
|
+
hourlyRate: number,
|
|
116
|
+
projectedDaily: number,
|
|
117
|
+
tokensPerHour: number,
|
|
118
|
+
costPerHour: number,
|
|
119
|
+
requestPercentage: number,
|
|
120
|
+
tokenPercentage: number
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### `getCurrentSessionStats()`
|
|
125
|
+
|
|
126
|
+
Returns statistics for the current active session window.
|
|
127
|
+
|
|
128
|
+
**Response:**
|
|
129
|
+
```js
|
|
130
|
+
{
|
|
131
|
+
requests: number,
|
|
132
|
+
inputTokens: number,
|
|
133
|
+
outputTokens: number,
|
|
134
|
+
cacheCreationTokens: number,
|
|
135
|
+
cacheReadTokens: number,
|
|
136
|
+
cacheTokens: number,
|
|
137
|
+
totalTokens: number,
|
|
138
|
+
totalCost: number,
|
|
139
|
+
models: { ... },
|
|
140
|
+
sessionStartTime: string, // ISO timestamp
|
|
141
|
+
lastUpdate: string,
|
|
142
|
+
sessionId: string,
|
|
143
|
+
sessionNumber: number, // 1-indexed within the day
|
|
144
|
+
isExpired: boolean,
|
|
145
|
+
remainingTokens: null
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### `calculateBurnRate(minutes = 60)`
|
|
150
|
+
|
|
151
|
+
Calculates token consumption rate over the given time window.
|
|
152
|
+
|
|
153
|
+
**Response:**
|
|
154
|
+
```js
|
|
155
|
+
{
|
|
156
|
+
rate: number, // tokens per minute
|
|
157
|
+
confidence: number, // 0.0 to 1.0, based on data points (min(count/10, 1))
|
|
158
|
+
dataPoints: number
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### `detectOverlappingSessions()`
|
|
163
|
+
|
|
164
|
+
Looks back `2 * sessionDurationHours`, groups entries into sessions by time gaps, and identifies overlapping session windows. Returns the session list (not the overlap pairs, which are stored in `this.overlappingSessions`).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## UsageAnalytics
|
|
169
|
+
|
|
170
|
+
Source: `src/usage-analytics.js`
|
|
171
|
+
|
|
172
|
+
Extends `EventEmitter`. Provides real-time analytics, burn rate tracking with trend analysis, and depletion predictions.
|
|
173
|
+
|
|
174
|
+
### Constructor
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
new UsageAnalytics(options)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| Option | Default | Description |
|
|
181
|
+
|--------|---------|-------------|
|
|
182
|
+
| `sessionDurationHours` | `5` | Session window size |
|
|
183
|
+
| `confidenceThreshold` | `0.95` | Minimum confidence for predictions |
|
|
184
|
+
| `burnRateWindow` | `60` | Minutes of data for burn rate calculation |
|
|
185
|
+
| `updateInterval` | `10000` | Milliseconds between analytics updates |
|
|
186
|
+
| `plan` | `'custom'` | Subscription plan type |
|
|
187
|
+
| `customCostLimit` | `76.89` | Dollar limit for custom plans |
|
|
188
|
+
|
|
189
|
+
### Plan Limits
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
{
|
|
193
|
+
'pro': { tokens: 19000, cost: 18.00, messages: 250, algorithm: 'fixed' },
|
|
194
|
+
'max5': { tokens: 88000, cost: 35.00, messages: 1000, algorithm: 'fixed' },
|
|
195
|
+
'max20': { tokens: 220000, cost: 140.00, messages: 2000, algorithm: 'fixed' },
|
|
196
|
+
'custom': { tokens: null, cost: 76.89, messages: 1019, algorithm: 'p90' }
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Legacy aliases `claude-pro`, `claude-max5`, and `claude-max20` are maintained for backwards compatibility with the same values.
|
|
201
|
+
|
|
202
|
+
### Token Limit Resolution
|
|
203
|
+
|
|
204
|
+
`getTokenLimit()`:
|
|
205
|
+
- For `fixed` algorithm plans: returns `plan.tokens` directly.
|
|
206
|
+
- For `p90` algorithm plans: returns the calculated P90 value, or `188,026` as the default fallback.
|
|
207
|
+
|
|
208
|
+
### P90 Calculation
|
|
209
|
+
|
|
210
|
+
`calculateP90Limit(historicalSessions)`:
|
|
211
|
+
- Requires at least 10 historical sessions.
|
|
212
|
+
- Sorts all session token counts ascending.
|
|
213
|
+
- Returns the value at the 90th percentile index.
|
|
214
|
+
- Emits `p90-calculated` event.
|
|
215
|
+
|
|
216
|
+
### Burn Rate Analysis
|
|
217
|
+
|
|
218
|
+
`calculateBurnRate()`:
|
|
219
|
+
|
|
220
|
+
1. Sorts recent usage data by timestamp.
|
|
221
|
+
2. Calculates token consumption rates over multiple time windows: 5, 10, 15, 30, and 60 minutes.
|
|
222
|
+
3. Each window's rate is weighted by data density (`min(dataPoints / 10, 1)`).
|
|
223
|
+
4. The final burn rate is a weighted average across all windows.
|
|
224
|
+
5. Only counts input + output tokens (excludes cache tokens).
|
|
225
|
+
|
|
226
|
+
`analyzeTrend()`:
|
|
227
|
+
|
|
228
|
+
Compares the average burn rate from the first half vs. the second half of the history:
|
|
229
|
+
- Change > +15%: `'increasing'`
|
|
230
|
+
- Change < -15%: `'decreasing'`
|
|
231
|
+
- Otherwise: `'stable'`
|
|
232
|
+
|
|
233
|
+
Requires at least 5 data points. Burn rate history is kept for the last hour.
|
|
234
|
+
|
|
235
|
+
### Depletion Predictions
|
|
236
|
+
|
|
237
|
+
`updatePredictions()`:
|
|
238
|
+
|
|
239
|
+
1. Gets the current session and its token limit.
|
|
240
|
+
2. Calculates `remaining = limit - used`.
|
|
241
|
+
3. If remaining <= 0: depletion is now, confidence = 1.
|
|
242
|
+
4. Otherwise: `minutesToDepletion = remaining / currentBurnRate`.
|
|
243
|
+
5. Adjusts for velocity trend:
|
|
244
|
+
- `'increasing'`: pulls depletion 10% sooner.
|
|
245
|
+
- `'decreasing'`: pushes depletion 10% later.
|
|
246
|
+
|
|
247
|
+
### Confidence Scoring
|
|
248
|
+
|
|
249
|
+
`calculateConfidence()` combines three factors:
|
|
250
|
+
|
|
251
|
+
| Factor | Weight | Score Calculation |
|
|
252
|
+
|--------|--------|-------------------|
|
|
253
|
+
| Data quantity | 0.3 | `min(recentUsage.length / 20, 1)` |
|
|
254
|
+
| Rate consistency | 0.4 | `1 - coefficient_of_variation` (requires 4+ history points) |
|
|
255
|
+
| Trend stability | 0.3 | `1.0` if stable, `0.7` if trending |
|
|
256
|
+
|
|
257
|
+
### Session Management
|
|
258
|
+
|
|
259
|
+
`startSession(sessionId, startTime)`:
|
|
260
|
+
- Creates a session object with `startTime`, calculated `endTime`, and zero usage counters.
|
|
261
|
+
- Stores in `activeSessions` Map.
|
|
262
|
+
- Updates rolling windows.
|
|
263
|
+
- Emits `session-started`.
|
|
264
|
+
|
|
265
|
+
`updateRollingWindows()`:
|
|
266
|
+
- Clears existing windows.
|
|
267
|
+
- Creates a window for each active session that started within the last `sessionDurationHours`.
|
|
268
|
+
- Each window tracks total tokens, cost, and remaining tokens.
|
|
269
|
+
|
|
270
|
+
`cleanup()`:
|
|
271
|
+
- Removes expired sessions (where `endTime < now`) to `sessionHistory`.
|
|
272
|
+
- Trims `sessionHistory` to the last 24 hours.
|
|
273
|
+
|
|
274
|
+
### Events
|
|
275
|
+
|
|
276
|
+
| Event | Payload | Trigger |
|
|
277
|
+
|-------|---------|---------|
|
|
278
|
+
| `usage-update` | Usage entry | New data point added |
|
|
279
|
+
| `session-started` | Session object | Session tracking started |
|
|
280
|
+
| `windows-updated` | Window array | Rolling windows recalculated |
|
|
281
|
+
| `burn-rate-updated` | `{ rate, trend, confidence }` | Burn rate recalculated |
|
|
282
|
+
| `prediction-updated` | `{ depletionTime, confidence, remaining, burnRate }` | Prediction refreshed |
|
|
283
|
+
| `p90-calculated` | `{ limit, sampleSize, confidence }` | P90 limit computed |
|
|
284
|
+
| `plan-changed` | Plan type string | User changed plan |
|
|
285
|
+
|
|
286
|
+
### Comprehensive Analytics Response
|
|
287
|
+
|
|
288
|
+
`getAnalytics()` returns the full state:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
{
|
|
292
|
+
currentSession: {
|
|
293
|
+
id, startTime, endTime, tokens, remaining, percentUsed
|
|
294
|
+
},
|
|
295
|
+
burnRate: {
|
|
296
|
+
current: number, // tokens per minute
|
|
297
|
+
trend: 'stable' | 'increasing' | 'decreasing',
|
|
298
|
+
history: [{ timestamp, rate }] // last 10 data points
|
|
299
|
+
},
|
|
300
|
+
predictions: {
|
|
301
|
+
depletionTime: Date | null,
|
|
302
|
+
confidence: number, // 0.0 to 1.0
|
|
303
|
+
minutesRemaining: number | null
|
|
304
|
+
},
|
|
305
|
+
plan: {
|
|
306
|
+
type: string,
|
|
307
|
+
limits: { tokens, cost, messages, algorithm },
|
|
308
|
+
p90Limit: number | null
|
|
309
|
+
},
|
|
310
|
+
windows: [ ... ],
|
|
311
|
+
activeSessions: [ { id, startTime, endTime, isActive, tokens } ]
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Session Timer (Server Integration)
|
|
318
|
+
|
|
319
|
+
Source: `src/server.js`, `handleGetUsage()`
|
|
320
|
+
|
|
321
|
+
The server combines `UsageReader` and `UsageAnalytics` data into a session timer object sent via the `usage_update` WebSocket message:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
{
|
|
325
|
+
startTime: string, // ISO timestamp of session start
|
|
326
|
+
elapsed: number, // milliseconds since session start
|
|
327
|
+
remaining: number, // milliseconds until session expires
|
|
328
|
+
formatted: "HH:MM:SS", // elapsed time formatted
|
|
329
|
+
remainingFormatted: "HH:MM", // remaining time formatted
|
|
330
|
+
hours: number,
|
|
331
|
+
minutes: number,
|
|
332
|
+
seconds: number,
|
|
333
|
+
remainingMs: number,
|
|
334
|
+
sessionDurationHours: number, // configured window size (e.g. 5)
|
|
335
|
+
sessionNumber: number, // 1-indexed session within the day
|
|
336
|
+
isExpired: boolean,
|
|
337
|
+
burnRate: number, // tokens per minute
|
|
338
|
+
burnRateConfidence: number,
|
|
339
|
+
depletionTime: Date | null,
|
|
340
|
+
depletionConfidence: number
|
|
341
|
+
}
|
|
342
|
+
```
|
package/nul
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-or-die",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Universal AI coding terminal — Claude, Copilot, Gemini & more in your browser",
|
|
5
|
+
"main": "src/server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ai-or-die": "./bin/ai-or-die.js",
|
|
8
|
+
"aiordie": "./bin/ai-or-die.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/ai-or-die.js",
|
|
12
|
+
"dev": "node bin/ai-or-die.js --dev",
|
|
13
|
+
"test": "mocha --exit test/*.test.js",
|
|
14
|
+
"build:bundle": "node scripts/build-sea.js bundle",
|
|
15
|
+
"build:sea": "node scripts/build-sea.js",
|
|
16
|
+
"release:pr": "bash scripts/release-pr.sh"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ai-or-die",
|
|
20
|
+
"aiordie",
|
|
21
|
+
"ai",
|
|
22
|
+
"terminal",
|
|
23
|
+
"claude",
|
|
24
|
+
"copilot",
|
|
25
|
+
"gemini",
|
|
26
|
+
"web",
|
|
27
|
+
"browser",
|
|
28
|
+
"remote"
|
|
29
|
+
],
|
|
30
|
+
"author": "Animesh Kundu",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"commander": "^12.1.0",
|
|
34
|
+
"cors": "^2.8.5",
|
|
35
|
+
"express": "^4.19.2",
|
|
36
|
+
"@lydell/node-pty": "1.2.0-beta.10",
|
|
37
|
+
"open": "^10.1.0",
|
|
38
|
+
"uuid": "^10.0.0",
|
|
39
|
+
"ws": "^8.18.0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22.0.0"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/animeshkundu/ai-or-die.git"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/animeshkundu/ai-or-die#readme",
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"esbuild": "^0.24.0",
|
|
51
|
+
"mocha": "^11.7.1",
|
|
52
|
+
"postject": "^1.0.0-alpha.6"
|
|
53
|
+
}
|
|
54
|
+
}
|