ai-lens 0.8.49 → 0.8.52
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/.commithash +1 -1
- package/README.md +133 -62
- package/cli/hooks.js +12 -3
- package/cli/init.js +99 -28
- package/cli/remove.js +18 -0
- package/cli/status.js +55 -5
- package/client/capture.js +497 -39
- package/client/codex-watcher.js +524 -0
- package/client/codex.js +570 -0
- package/client/config.js +59 -10
- package/client/sender.js +34 -0
- package/client/token-usage.js +47 -0
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
8553371
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# AI Lens
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Analytics for AI coding sessions. Captures hook events from Claude Code, Cursor,
|
|
4
|
+
and Codex, normalizes them to a unified format, queues locally, and ships to a
|
|
5
|
+
centralized server with a web dashboard and MCP integration.
|
|
4
6
|
|
|
5
7
|
```
|
|
6
8
|
Hook fires → capture.js → normalize → queue.jsonl → sender.js → POST /api/events → server → dashboard
|
|
@@ -15,9 +17,11 @@ npx -y ai-lens init
|
|
|
15
17
|
```
|
|
16
18
|
|
|
17
19
|
This will:
|
|
18
|
-
1. Detect installed AI tools (Claude Code, Cursor)
|
|
20
|
+
1. Detect installed AI tools (Claude Code, Cursor, Codex)
|
|
19
21
|
2. Copy client files to `~/.ai-lens/client/`
|
|
20
22
|
3. Configure hooks in `~/.claude/settings.json` and/or `~/.cursor/hooks.json`
|
|
23
|
+
4. Start the Codex watcher for user-level and project-local Codex sessions
|
|
24
|
+
5. Register the MCP server for in-editor analytics (optional)
|
|
21
25
|
|
|
22
26
|
Re-running is safe — it updates outdated hooks and skips current ones.
|
|
23
27
|
|
|
@@ -26,19 +30,38 @@ Re-running is safe — it updates outdated hooks and skips current ones.
|
|
|
26
30
|
To write hooks into the project directory (`.cursor/hooks.json` and `.claude/settings.json`) instead of global `~/.cursor/` and `~/.claude/`, run from the project root:
|
|
27
31
|
|
|
28
32
|
```bash
|
|
29
|
-
npx
|
|
33
|
+
npx -y ai-lens init --server https://ai-lens.rantsports.com --no-mcp --project-hooks
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
Add `--use-repo-path` to run `capture.js` directly from the package (repo or npx cache) instead of copying to `~/.ai-lens/client/`. Useful when the repo is next to the workspace.
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
### CLI commands
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
```bash
|
|
41
|
+
npx ai-lens init # Setup wizard — detect tools, install hooks, configure MCP
|
|
42
|
+
npx ai-lens status # Run health checks and generate a diagnostic report
|
|
43
|
+
npx ai-lens remove # Remove hooks, client files, and MCP config
|
|
44
|
+
npx ai-lens version # Show installed version
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### CLI options
|
|
48
|
+
|
|
49
|
+
| Flag | Description |
|
|
50
|
+
|------|-------------|
|
|
51
|
+
| `--server URL` | Server URL (default: `http://localhost:3000`) |
|
|
52
|
+
| `--yes`, `-y` | Non-interactive mode, accept all defaults |
|
|
53
|
+
| `--no-mcp` | Skip MCP server registration |
|
|
54
|
+
| `--mcp-scope SCOPE` | MCP scope: `user` (default), `local`, or `project` |
|
|
55
|
+
| `--projects LIST` | Comma-separated project paths to monitor (default: all) |
|
|
56
|
+
| `--project-hooks` | Write hooks into project directory instead of global config |
|
|
57
|
+
| `--use-repo-path` | Run capture.js from package path instead of copying to `~/.ai-lens/client/` |
|
|
58
|
+
|
|
59
|
+
### Environment variables (client)
|
|
37
60
|
|
|
38
61
|
```bash
|
|
39
62
|
# In your shell profile (~/.zshrc, ~/.bashrc)
|
|
40
|
-
export AI_LENS_SERVER_URL=
|
|
41
|
-
export AI_LENS_PROJECTS="~/
|
|
63
|
+
export AI_LENS_SERVER_URL=https://ai-lens.rantsports.com
|
|
64
|
+
export AI_LENS_PROJECTS="~/meta/, ~/meta-cursor/" # optional, default: all
|
|
42
65
|
```
|
|
43
66
|
|
|
44
67
|
<details>
|
|
@@ -76,6 +99,14 @@ export AI_LENS_PROJECTS="~/work/, ~/projects/" # optional, default: all
|
|
|
76
99
|
}
|
|
77
100
|
```
|
|
78
101
|
|
|
102
|
+
**Codex** — no hook file. Run `ai-lens init` to start the local watcher, or start it manually:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
node ~/.ai-lens/client/codex-watcher.js
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The watcher tails the user-level Codex directory (`~/.codex`) and any project-local `.codex` directories found under `AI_LENS_PROJECTS`. It respects `AI_LENS_PROJECTS` the same way as Claude Code and Cursor: only sessions whose `cwd` is inside a configured monitored root are sent.
|
|
109
|
+
|
|
79
110
|
</details>
|
|
80
111
|
|
|
81
112
|
## Server Setup
|
|
@@ -87,46 +118,94 @@ docker compose up -d
|
|
|
87
118
|
```
|
|
88
119
|
|
|
89
120
|
Starts three containers:
|
|
90
|
-
- **nginx** — reverse proxy with basic auth, port `13300`
|
|
91
|
-
- **app** — Node.js Express server with dashboard
|
|
92
|
-
- **postgres** — PostgreSQL 16 database
|
|
93
121
|
|
|
94
|
-
|
|
122
|
+
| Container | Image | Purpose |
|
|
123
|
+
|-----------|-------|---------|
|
|
124
|
+
| **app** | `ai-lens/app` | API server + web dashboard (port 3000) |
|
|
125
|
+
| **postgres** | `postgres:16-alpine` | PostgreSQL 16 database |
|
|
126
|
+
| **analyzer** | `ai-lens/analyzer` | Background session analyzer (needs `claude login`) |
|
|
127
|
+
|
|
128
|
+
Dashboard: https://ai-lens.rantsports.com
|
|
95
129
|
|
|
96
|
-
|
|
130
|
+
Images are stored in ECR (`267996409571.dkr.ecr.eu-north-1.amazonaws.com/ai-lens/`) and mirrored to GHCR (`ghcr.io/r-ms/ai-lens/`) on every push to `main`.
|
|
97
131
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
|
101
|
-
|
|
132
|
+
### Environment variables (server)
|
|
133
|
+
|
|
134
|
+
| Variable | Default | Description |
|
|
135
|
+
|----------|---------|-------------|
|
|
136
|
+
| `PORT` | `3000` | Server port |
|
|
137
|
+
| `DATABASE_URL` | _(required)_ | PostgreSQL connection string |
|
|
138
|
+
| `POSTGRES_PASSWORD` | `ailens` | PostgreSQL password (docker compose) |
|
|
139
|
+
| `ANALYSIS_INTERVAL` | `3600` | Seconds between analysis runs |
|
|
140
|
+
| `AI_LENS_ADMIN_SECRET` | _(none)_ | Admin secret for auth token management |
|
|
141
|
+
| `OPENAI_API_KEY` | _(none)_ | OpenAI API key for FAISS vector search; text search works without it |
|
|
142
|
+
| `TEAMS_CONFIG` | _(none)_ | JSON config for team definitions |
|
|
143
|
+
| `CORS_ALLOWED_ORIGINS` | _(none)_ | Allowed CORS origins |
|
|
144
|
+
|
|
145
|
+
#### Auth0 SSO
|
|
146
|
+
|
|
147
|
+
| Variable | Description |
|
|
148
|
+
|----------|-------------|
|
|
149
|
+
| `AUTH0_DOMAIN` | Auth0 tenant domain |
|
|
150
|
+
| `AUTH0_CLIENT_ID` | Auth0 SPA client ID |
|
|
151
|
+
| `AUTH0_AUDIENCE` | Auth0 API audience identifier |
|
|
152
|
+
| `AUTH0_ALLOWED_DOMAIN` | Restrict login to a specific email domain |
|
|
153
|
+
| `AUTH0_CLI_CLIENT_ID` | Auth0 Native app client ID for device code flow |
|
|
154
|
+
| `MCP_SERVER_URL` | Public server URL for MCP OAuth callbacks |
|
|
155
|
+
|
|
156
|
+
Without Auth0, the server uses git email headers for identity (personal mode).
|
|
102
157
|
|
|
103
158
|
### Local development
|
|
104
159
|
|
|
105
160
|
```bash
|
|
106
|
-
|
|
107
|
-
npm
|
|
161
|
+
docker compose up postgres -d
|
|
162
|
+
npm install
|
|
163
|
+
DATABASE_URL=postgresql://ailens:ailens@localhost:5432/ailens npm start
|
|
108
164
|
```
|
|
109
165
|
|
|
110
|
-
SQLite is used when `DATABASE_URL` is not set. PostgreSQL is used in Docker via `DATABASE_URL=postgresql://...`.
|
|
111
|
-
|
|
112
166
|
## Dashboard
|
|
113
167
|
|
|
114
|
-
React + TypeScript SPA with
|
|
168
|
+
React + TypeScript SPA with:
|
|
169
|
+
- Organization-wide KPIs and adoption trends
|
|
170
|
+
- Team and developer breakdowns
|
|
171
|
+
- Session timelines with tool usage
|
|
172
|
+
- AI-generated session and team analyses
|
|
173
|
+
- Token usage by model
|
|
174
|
+
- MCP server and skill distribution
|
|
175
|
+
- Knowledge base and recurring problems
|
|
115
176
|
|
|
116
177
|
```bash
|
|
117
178
|
cd dashboard
|
|
118
179
|
npm install
|
|
119
180
|
npm run dev # Vite dev server with HMR (proxies API to localhost:3000)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
Production build (served by Express as static files):
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
npm run build:dashboard
|
|
181
|
+
npm run build # Production build (served by Express as static files)
|
|
126
182
|
```
|
|
127
183
|
|
|
128
184
|
Tech: Vite, Tailwind CSS, Nivo charts, TanStack Query, react-router-dom.
|
|
129
185
|
|
|
186
|
+
## MCP Tools
|
|
187
|
+
|
|
188
|
+
When MCP is enabled during `npx ai-lens init`, these tools become available inside Claude Code and Cursor:
|
|
189
|
+
|
|
190
|
+
| Tool | Description |
|
|
191
|
+
|------|-------------|
|
|
192
|
+
| `who_am_i` | Identify yourself by git email — returns your developer profile and team(s) |
|
|
193
|
+
| `get_overview` | Organization-wide KPIs: active developers, adoption rate, AI hours, MCP and skill distribution |
|
|
194
|
+
| `list_teams` | List all teams with member counts, adoption rate, and AI hours |
|
|
195
|
+
| `get_team` | Team detail: KPIs, members, tasks, activity trend, MCP and skill distribution |
|
|
196
|
+
| `get_team_analysis` | AI-generated team analysis: achievements, recurring problems, recommendations |
|
|
197
|
+
| `get_developer` | Developer profile: sessions, AI hours, tasks, MCP and skill usage, team comparison |
|
|
198
|
+
| `get_mcp_distribution` | MCP server usage across the organization |
|
|
199
|
+
| `get_chain` | Session chain with compact event timeline, plan mode segments, and timing |
|
|
200
|
+
| `get_events` | Full event data for specific event IDs |
|
|
201
|
+
| `get_chain_analysis` | AI-generated chain analysis: tasks, problems, tool errors, unanswered questions |
|
|
202
|
+
| `request_analysis` | Manually trigger analysis for a specific session chain |
|
|
203
|
+
| `get_token_usage` | Token usage statistics grouped by model (input/output/cache tokens) |
|
|
204
|
+
| `knowhow_search` | Search the team knowledge base built from session analyses |
|
|
205
|
+
| `knowhow_update` | Add or update a knowledge base entry |
|
|
206
|
+
| `export_developer_tips` | Export personalized tips as a Markdown document |
|
|
207
|
+
| `search` | Natural language search across sessions, tasks, and projects |
|
|
208
|
+
|
|
130
209
|
## API
|
|
131
210
|
|
|
132
211
|
### `POST /api/events`
|
|
@@ -134,7 +213,7 @@ Tech: Vite, Tailwind CSS, Nivo charts, TanStack Query, react-router-dom.
|
|
|
134
213
|
Batch insert events. Deduplicates by `event_id` (ON CONFLICT DO NOTHING) — safe to re-send.
|
|
135
214
|
|
|
136
215
|
```
|
|
137
|
-
Headers: X-Developer-Git-Email, X-Developer-Name,
|
|
216
|
+
Headers: X-Developer-Git-Email, X-Developer-Name, X-Auth-Token
|
|
138
217
|
Body: [{ source, session_id, type, project_path, timestamp, data, raw, event_id }]
|
|
139
218
|
Response: { received, skipped, deduplicated }
|
|
140
219
|
```
|
|
@@ -153,38 +232,36 @@ List all developers.
|
|
|
153
232
|
|
|
154
233
|
### `GET /api/dashboard/*`
|
|
155
234
|
|
|
156
|
-
Aggregate endpoints for dashboard charts
|
|
235
|
+
Aggregate endpoints for dashboard charts: overview, teams, developers, tokens, MCP distribution, developer activity, knowledge base, problems, company/team/developer analyses.
|
|
157
236
|
|
|
158
237
|
## Event Types
|
|
159
238
|
|
|
160
239
|
| Type | Source | Description |
|
|
161
240
|
|------|--------|-------------|
|
|
162
|
-
| `SessionStart` |
|
|
163
|
-
| `SessionEnd` |
|
|
164
|
-
| `UserPromptSubmit` |
|
|
165
|
-
| `PostToolUse` |
|
|
166
|
-
| `PostToolUseFailure` |
|
|
167
|
-
| `Stop` |
|
|
168
|
-
| `PreCompact` |
|
|
241
|
+
| `SessionStart` | All | Session opened |
|
|
242
|
+
| `SessionEnd` | All | Session closed |
|
|
243
|
+
| `UserPromptSubmit` | All | User sent a prompt |
|
|
244
|
+
| `PostToolUse` | All | Tool execution completed |
|
|
245
|
+
| `PostToolUseFailure` | All | Tool execution failed |
|
|
246
|
+
| `Stop` | All | Agent stopped |
|
|
247
|
+
| `PreCompact` | All | Context compaction triggered |
|
|
169
248
|
| `PlanModeStart` | Claude Code | Entered plan mode |
|
|
170
249
|
| `PlanModeEnd` | Claude Code | Exited plan mode (plan content in raw payload) |
|
|
171
|
-
| `SubagentStart` |
|
|
172
|
-
| `SubagentStop` |
|
|
250
|
+
| `SubagentStart` | All | Subagent spawned |
|
|
251
|
+
| `SubagentStop` | All | Subagent finished |
|
|
173
252
|
| `FileEdit` | Cursor | File edited |
|
|
174
253
|
| `ShellExecution` | Cursor | Shell command executed |
|
|
175
254
|
| `MCPExecution` | Cursor | MCP tool executed |
|
|
176
|
-
| `AgentResponse` | Cursor | Agent response |
|
|
255
|
+
| `AgentResponse` | Cursor | Agent response (includes token usage in raw payload) |
|
|
177
256
|
| `AgentThought` | Cursor | Agent reasoning |
|
|
178
257
|
|
|
179
|
-
##
|
|
258
|
+
## Supported Tools
|
|
180
259
|
|
|
181
|
-
|
|
|
182
|
-
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
|
|
|
186
|
-
| `AI_LENS_AUTH_TOKEN` | `collector:secret-collector-token-2026-ai-lens` | Client auth (`user:password`) |
|
|
187
|
-
| `AI_LENS_PROJECTS` | _(all)_ | Comma-separated project paths to monitor (`~` supported) |
|
|
260
|
+
| Tool | Hook mechanism |
|
|
261
|
+
|------|---------------|
|
|
262
|
+
| **Claude Code** | Hooks via `~/.claude/settings.json` |
|
|
263
|
+
| **Cursor** | Hooks via `~/.cursor/hooks.json` |
|
|
264
|
+
| **Codex** | File watcher on `~/.codex` and project-local `.codex` directories |
|
|
188
265
|
|
|
189
266
|
## Client Data
|
|
190
267
|
|
|
@@ -193,41 +270,35 @@ Stored in `~/.ai-lens/`:
|
|
|
193
270
|
| File | Purpose |
|
|
194
271
|
|------|---------|
|
|
195
272
|
| `client/` | Installed client files (capture.js, sender.js, config.js) |
|
|
273
|
+
| `config.json` | Server URL, auth token, project list |
|
|
196
274
|
| `queue.jsonl` | Pending events |
|
|
197
275
|
| `queue.sending.jsonl` | Events being sent (atomic rename as mutex) |
|
|
198
276
|
| `sender.log` | Sender activity log |
|
|
277
|
+
| `capture.log` | Capture drop log (normalization failures, write errors) |
|
|
199
278
|
| `session-paths.json` | Session-to-project path cache |
|
|
200
279
|
|
|
201
280
|
## Development
|
|
202
281
|
|
|
203
282
|
```bash
|
|
204
|
-
npm test # Run all tests (vitest,
|
|
283
|
+
npm test # Run all tests (vitest, 683 tests)
|
|
205
284
|
npm run test:watch # Watch mode
|
|
206
285
|
npm run dev:dashboard # Dashboard dev server
|
|
207
286
|
```
|
|
208
287
|
|
|
209
|
-
Tests use
|
|
288
|
+
Tests require PostgreSQL — set `DATABASE_URL` or use `docker compose up postgres -d` (test DB `ailens_test` is created automatically).
|
|
210
289
|
|
|
211
290
|
## Deployment
|
|
212
291
|
|
|
213
292
|
GitLab CI (`.gitlab-ci.yml`) on push to `main`:
|
|
214
293
|
|
|
215
|
-
1. `
|
|
216
|
-
2. `
|
|
217
|
-
3.
|
|
218
|
-
|
|
219
|
-
## Data Migration
|
|
220
|
-
|
|
221
|
-
Sync local SQLite data to a remote PostgreSQL server:
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
node scripts/sync-to-remote.js # Default remote
|
|
225
|
-
node scripts/sync-to-remote.js http://custom:13300 # Custom URL
|
|
226
|
-
```
|
|
294
|
+
1. **build-app** — builds `ai-lens/app` Docker image, pushes to ECR + GHCR
|
|
295
|
+
2. **build-analyzer** — builds `ai-lens/analyzer` Docker image, pushes to ECR + GHCR
|
|
296
|
+
3. **deploy** — zero-downtime rolling deploy to production (scale up new container, health check, remove old)
|
|
227
297
|
|
|
228
|
-
|
|
298
|
+
Build jobs trigger only when relevant files change (Dockerfile, server/**, dashboard/**, etc.).
|
|
229
299
|
|
|
230
300
|
## Requirements
|
|
231
301
|
|
|
232
302
|
- Node.js 20+
|
|
233
303
|
- Docker + Docker Compose (for production deployment)
|
|
304
|
+
- PostgreSQL 16 (for local development without Docker)
|
package/cli/hooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readFileSync, writeFileSync, copyFileSync, renameSync, mkdirSync, rmSync, unlinkSync, chmodSync } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, writeFileSync, copyFileSync, renameSync, mkdirSync, rmSync, unlinkSync, chmodSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -145,7 +145,16 @@ export function cursorCaptureCommand(useTilde = false, customPath = null) {
|
|
|
145
145
|
// Client file installation
|
|
146
146
|
// ---------------------------------------------------------------------------
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Enumerate every .js file that ships with the package's client/ directory.
|
|
150
|
+
* Dynamic discovery (rather than a hardcoded CLIENT_FILES list) guarantees
|
|
151
|
+
* that any new client/*.js file added to the repo is automatically installed,
|
|
152
|
+
* so a forgotten list entry can never produce a broken install that crashes
|
|
153
|
+
* every hook with ERR_MODULE_NOT_FOUND on a missing sibling import.
|
|
154
|
+
*/
|
|
155
|
+
export function listClientFiles(sourceDir = join(__dirname, '..', 'client')) {
|
|
156
|
+
return readdirSync(sourceDir).filter(f => f.endsWith('.js')).sort();
|
|
157
|
+
}
|
|
149
158
|
|
|
150
159
|
/**
|
|
151
160
|
* Copy client/ files from the package source to ~/.ai-lens/client/.
|
|
@@ -155,7 +164,7 @@ export function installClientFiles() {
|
|
|
155
164
|
const sourceDir = join(__dirname, '..', 'client');
|
|
156
165
|
mkdirSync(CLIENT_INSTALL_DIR, { recursive: true });
|
|
157
166
|
|
|
158
|
-
for (const file of
|
|
167
|
+
for (const file of listClientFiles(sourceDir)) {
|
|
159
168
|
copyFileSync(join(sourceDir, file), join(CLIENT_INSTALL_DIR, file));
|
|
160
169
|
}
|
|
161
170
|
|
package/cli/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import { execSync, spawn } from 'node:child_process';
|
|
3
3
|
import { existsSync, copyFileSync, readdirSync } from 'node:fs';
|
|
4
|
-
import { join, resolve, relative } from 'node:path';
|
|
4
|
+
import { join, resolve, relative, dirname } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { request as httpRequest } from 'node:http';
|
|
7
7
|
import { request as httpsRequest } from 'node:https';
|
|
@@ -67,7 +67,7 @@ function getJson(url) {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function postJson(url, body) {
|
|
70
|
+
function postJson(url, body, timeoutMs = 15_000) {
|
|
71
71
|
return new Promise((resolve, reject) => {
|
|
72
72
|
const parsed = new URL(url);
|
|
73
73
|
const isHttps = parsed.protocol === 'https:';
|
|
@@ -82,7 +82,7 @@ function postJson(url, body) {
|
|
|
82
82
|
'Content-Type': 'application/json',
|
|
83
83
|
'Content-Length': Buffer.byteLength(data),
|
|
84
84
|
},
|
|
85
|
-
timeout:
|
|
85
|
+
timeout: timeoutMs,
|
|
86
86
|
};
|
|
87
87
|
const req = requestFn(options, (res) => {
|
|
88
88
|
let buf = '';
|
|
@@ -107,7 +107,7 @@ function postJson(url, body) {
|
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function postForm(url, params) {
|
|
110
|
+
function postForm(url, params, timeoutMs = 15_000) {
|
|
111
111
|
return new Promise((resolve, reject) => {
|
|
112
112
|
const parsed = new URL(url);
|
|
113
113
|
const isHttps = parsed.protocol === 'https:';
|
|
@@ -122,7 +122,7 @@ function postForm(url, params) {
|
|
|
122
122
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
123
123
|
'Content-Length': Buffer.byteLength(data),
|
|
124
124
|
},
|
|
125
|
-
timeout:
|
|
125
|
+
timeout: timeoutMs,
|
|
126
126
|
};
|
|
127
127
|
const req = requestFn(options, (res) => {
|
|
128
128
|
let buf = '';
|
|
@@ -146,6 +146,39 @@ function sleep(ms) {
|
|
|
146
146
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function isTransientNetError(err) {
|
|
150
|
+
const msg = `${err && err.code ? err.code : ''} ${err && err.message ? err.message : err}`;
|
|
151
|
+
return /ECONNRESET|EPIPE|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|ECONNREFUSED|socket hang up/i.test(String(msg));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Retry fn on flaky TLS/proxy resets (common during long Auth0 device polls). */
|
|
155
|
+
async function withRetries(fn, { attempts = 5, baseDelayMs = 2000 } = {}) {
|
|
156
|
+
let lastErr;
|
|
157
|
+
for (let i = 1; i <= attempts; i++) {
|
|
158
|
+
try {
|
|
159
|
+
return await fn();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
lastErr = err;
|
|
162
|
+
if (!isTransientNetError(err) || i === attempts) throw err;
|
|
163
|
+
await sleep(baseDelayMs * i);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw lastErr;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function startCodexWatcher(watcherPath) {
|
|
170
|
+
if (!existsSync(watcherPath)) return false;
|
|
171
|
+
if (process.env.AI_LENS_TEST_NO_DETACHED_SPAWN === '1') return true;
|
|
172
|
+
const child = spawn(process.execPath, [watcherPath], {
|
|
173
|
+
detached: true,
|
|
174
|
+
stdio: 'ignore',
|
|
175
|
+
windowsHide: true,
|
|
176
|
+
});
|
|
177
|
+
child.on('error', () => {});
|
|
178
|
+
child.unref();
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
149
182
|
async function deviceCodeAuth(serverUrl) {
|
|
150
183
|
// 1. Fetch Auth0 config from server
|
|
151
184
|
let config;
|
|
@@ -206,17 +239,24 @@ async function deviceCodeAuth(serverUrl) {
|
|
|
206
239
|
while (Date.now() < deadline) {
|
|
207
240
|
await sleep(interval * 1000);
|
|
208
241
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
242
|
+
// Auth0 may keep this request open longer than a typical HTTP call; 15s caused false "Request timed out".
|
|
243
|
+
const tokenResp = await withRetries(
|
|
244
|
+
() => postForm(`https://${domain}/oauth/token`, {
|
|
245
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
246
|
+
client_id: cliClientId,
|
|
247
|
+
device_code,
|
|
248
|
+
}, 120_000),
|
|
249
|
+
{ attempts: 6, baseDelayMs: 2000 },
|
|
250
|
+
);
|
|
214
251
|
|
|
215
252
|
if (tokenResp.status === 200 && tokenResp.data.id_token) {
|
|
216
253
|
// 5. Exchange JWT for personal token
|
|
217
|
-
const result = await
|
|
218
|
-
|
|
219
|
-
|
|
254
|
+
const result = await withRetries(
|
|
255
|
+
() => postJson(`${serverUrl}/api/auth/device-token`, {
|
|
256
|
+
jwt: tokenResp.data.id_token,
|
|
257
|
+
}, 120_000),
|
|
258
|
+
{ attempts: 5, baseDelayMs: 2000 },
|
|
259
|
+
);
|
|
220
260
|
if (!result?.token) throw new Error('Server returned no token — contact your admin');
|
|
221
261
|
return result;
|
|
222
262
|
}
|
|
@@ -386,14 +426,12 @@ export default async function init() {
|
|
|
386
426
|
}
|
|
387
427
|
|
|
388
428
|
if (tools.length === 0) {
|
|
389
|
-
warn('No
|
|
390
|
-
info('
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
for (const tool of tools) {
|
|
396
|
-
success(` Found: ${tool.name} (${tool.dirPath})`);
|
|
429
|
+
warn('No Claude Code or Cursor installation detected.');
|
|
430
|
+
info('Continuing with AI Lens client install, config, and Codex watcher setup.');
|
|
431
|
+
} else {
|
|
432
|
+
for (const tool of tools) {
|
|
433
|
+
success(` Found: ${tool.name} (${tool.dirPath})`);
|
|
434
|
+
}
|
|
397
435
|
}
|
|
398
436
|
|
|
399
437
|
// Configuration
|
|
@@ -419,19 +457,20 @@ export default async function init() {
|
|
|
419
457
|
|
|
420
458
|
// Project filter
|
|
421
459
|
const currentProjects = currentConfig.projects || null;
|
|
460
|
+
const projectHooksDefault = flags.projectHooks ? resolve(process.cwd()) : null;
|
|
422
461
|
let projects;
|
|
423
462
|
if (flags.projects) {
|
|
424
463
|
projects = flags.projects;
|
|
425
464
|
} else if (auto) {
|
|
426
|
-
projects = currentProjects;
|
|
465
|
+
projects = currentProjects || projectHooksDefault;
|
|
427
466
|
} else {
|
|
428
|
-
const projectsDefault = currentProjects || 'all';
|
|
467
|
+
const projectsDefault = currentProjects || projectHooksDefault || 'all';
|
|
429
468
|
const projectsInput = await ask(
|
|
430
469
|
`Projects to track (comma-separated, ~ supported, Enter = ${projectsDefault}): `,
|
|
431
470
|
);
|
|
432
471
|
projects = (projectsInput && projectsInput.trim() && projectsInput.trim().toLowerCase() !== 'all')
|
|
433
472
|
? projectsInput
|
|
434
|
-
: null;
|
|
473
|
+
: (projectHooksDefault || null);
|
|
435
474
|
}
|
|
436
475
|
// Guard: non-string (e.g. array from corrupt config) → treat as unset
|
|
437
476
|
if (projects && typeof projects !== 'string') {
|
|
@@ -498,10 +537,11 @@ export default async function init() {
|
|
|
498
537
|
try {
|
|
499
538
|
authResult = await deviceCodeAuth(serverUrl);
|
|
500
539
|
} catch (err) {
|
|
501
|
-
|
|
540
|
+
const msg = (err && err.message) ? err.message : String(err);
|
|
541
|
+
if (msg.includes('not configured')) {
|
|
502
542
|
warn(` Auth not configured on server — personal mode (events sent via git identity)`);
|
|
503
543
|
} else {
|
|
504
|
-
warn(` Authentication failed: ${
|
|
544
|
+
warn(` Authentication failed: ${msg}`);
|
|
505
545
|
warn(` Run "npx -y ai-lens init" again later to authenticate`);
|
|
506
546
|
}
|
|
507
547
|
}
|
|
@@ -530,8 +570,12 @@ export default async function init() {
|
|
|
530
570
|
}
|
|
531
571
|
}
|
|
532
572
|
|
|
533
|
-
if (flags.noHooks) {
|
|
534
|
-
|
|
573
|
+
if (flags.noHooks || tools.length === 0) {
|
|
574
|
+
if (tools.length === 0 && !flags.noHooks) {
|
|
575
|
+
info('No Claude Code or Cursor hooks to configure in this environment.');
|
|
576
|
+
} else if (flags.noHooks) {
|
|
577
|
+
info('--no-hooks: skipping hook configuration and MCP setup');
|
|
578
|
+
}
|
|
535
579
|
saveLensConfig(newConfig);
|
|
536
580
|
} else {
|
|
537
581
|
// Analyze each tool
|
|
@@ -727,6 +771,33 @@ export default async function init() {
|
|
|
727
771
|
warn(` Migration skipped: ${err.message}`);
|
|
728
772
|
}
|
|
729
773
|
|
|
774
|
+
heading('Codex');
|
|
775
|
+
let enableCodex = currentConfig.codexEnabled === true;
|
|
776
|
+
if (auto) {
|
|
777
|
+
enableCodex = true;
|
|
778
|
+
} else {
|
|
779
|
+
const defaultLabel = enableCodex ? 'Y/n' : 'y/N';
|
|
780
|
+
const answer = await ask(` Enable Codex session tracking? [${defaultLabel}] `);
|
|
781
|
+
if (answer) {
|
|
782
|
+
enableCodex = ['y', 'yes'].includes(answer.toLowerCase());
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
newConfig.codexEnabled = enableCodex;
|
|
786
|
+
saveLensConfig(newConfig);
|
|
787
|
+
|
|
788
|
+
if (enableCodex) {
|
|
789
|
+
const watcherPath = flags.useRepoPath
|
|
790
|
+
? join(dirname(REPO_CAPTURE_PATH), 'codex-watcher.js')
|
|
791
|
+
: join(dirname(CAPTURE_PATH), 'codex-watcher.js');
|
|
792
|
+
if (startCodexWatcher(watcherPath)) {
|
|
793
|
+
success(' Codex watcher started');
|
|
794
|
+
} else {
|
|
795
|
+
warn(` Codex watcher not started: missing ${watcherPath}`);
|
|
796
|
+
}
|
|
797
|
+
} else {
|
|
798
|
+
info(' Codex tracking disabled');
|
|
799
|
+
}
|
|
800
|
+
|
|
730
801
|
// Flush any pending events with the new config (e.g. backlog from previous 401/network errors)
|
|
731
802
|
const senderPath = join(homedir(), '.ai-lens', 'client', 'sender.js');
|
|
732
803
|
if (existsSync(senderPath)) {
|
package/cli/remove.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
buildStrippedConfig, writeHooksConfig, removeClientFiles, getVersionInfo,
|
|
13
13
|
cleanupLegacyHooks, cleanupEmptyMcpJson, removeCursorMcp,
|
|
14
14
|
} from './hooks.js';
|
|
15
|
+
import { stopCodexWatcher } from '../client/codex-watcher.js';
|
|
15
16
|
|
|
16
17
|
function ask(question) {
|
|
17
18
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -121,6 +122,23 @@ export default async function remove() {
|
|
|
121
122
|
}
|
|
122
123
|
blank();
|
|
123
124
|
|
|
125
|
+
// Stop Codex watcher before deleting ~/.ai-lens files so the process does not
|
|
126
|
+
// keep running against removed paths or leave a stale lock behind.
|
|
127
|
+
heading('Stopping Codex watcher...');
|
|
128
|
+
try {
|
|
129
|
+
const watcherResult = await stopCodexWatcher();
|
|
130
|
+
if (watcherResult.previousState === 'active') {
|
|
131
|
+
success(` Codex watcher stopped (pid ${watcherResult.pid}${watcherResult.forced ? ', forced' : ''})`);
|
|
132
|
+
} else if (watcherResult.previousState === 'missing') {
|
|
133
|
+
info(' Codex watcher not running');
|
|
134
|
+
} else {
|
|
135
|
+
success(` Codex watcher lock cleaned up (${watcherResult.previousState})`);
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
error(` Failed to stop Codex watcher: ${err.message}`);
|
|
139
|
+
}
|
|
140
|
+
blank();
|
|
141
|
+
|
|
124
142
|
// Remove MCP servers
|
|
125
143
|
heading('Removing MCP servers...');
|
|
126
144
|
|