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 CHANGED
@@ -1 +1 @@
1
- b5da117
1
+ 8553371
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # AI Lens
2
2
 
3
- Hook-based analytics for AI coding sessions. Captures events from Claude Code and Cursor, normalizes them to a unified format, queues locally, and ships to a centralized server with a web dashboard.
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 git+ssh://git@rantsports.gitlab.yandexcloud.net:ai-first-workspace/internal/analytics/ai-lens.git init --server https://ai-lens.rantsports.com --no-mcp --project-hooks
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
- Hooks use the path `~/.ai-lens/client/capture.js` by default (or the package path with `--use-repo-path`); the config can be committed to the repo.
38
+ ### CLI commands
35
39
 
36
- Configure the server URL and optionally filter projects:
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=http://your-server:13300
41
- export AI_LENS_PROJECTS="~/work/, ~/projects/" # optional, default: all
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
- Dashboard: `http://your-server:13300`
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
- Default credentials:
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
- | User | Password | Purpose |
99
- |------|----------|---------|
100
- | `collector` | `secret-collector-token-2026-ai-lens` | Client sender (automatic via `AI_LENS_AUTH_TOKEN`) |
101
- | `meta` | `meta` | Browser / dashboard access |
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
- npm install --prefix server # Install server deps (auto-runs via prestart)
107
- npm start # Express on port 3000, SQLite at ~/.ai-lens-server/data.db
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 session timelines, tool breakdowns, adoption trends, and per-developer analytics.
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, Authorization: Basic <base64>
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 (stats, trends, tool usage, etc.).
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` | Both | Session opened |
163
- | `SessionEnd` | Both | Session closed |
164
- | `UserPromptSubmit` | Both | User sent a prompt |
165
- | `PostToolUse` | Both | Tool execution completed |
166
- | `PostToolUseFailure` | Both | Tool execution failed |
167
- | `Stop` | Both | Agent stopped |
168
- | `PreCompact` | Both | Context compaction triggered |
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` | Both | Subagent spawned |
172
- | `SubagentStop` | Both | Subagent finished |
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
- ## Environment Variables
258
+ ## Supported Tools
180
259
 
181
- | Variable | Default | Description |
182
- |----------|---------|-------------|
183
- | `PORT` | `3000` (local), `13300` (Docker) | Server port |
184
- | `DATABASE_URL` | _(unset = SQLite)_ | PostgreSQL connection string |
185
- | `AI_LENS_SERVER_URL` | `http://localhost:3000` | Client server endpoint |
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, 204 tests)
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 in-memory SQLite via `initTestDb()`.
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. `rsync` to deploy host
216
- 2. `docker compose down && docker compose up -d --build`
217
- 3. Health check
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
- Safe to re-run deduplicates by `event_id`.
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
- const CLIENT_FILES = ['capture.js', 'sender.js', 'config.js', 'redact.js'];
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 CLIENT_FILES) {
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: 15_000,
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: 15_000,
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
- const tokenResp = await postForm(`https://${domain}/oauth/token`, {
210
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
211
- client_id: cliClientId,
212
- device_code,
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 postJson(`${serverUrl}/api/auth/device-token`, {
218
- jwt: tokenResp.data.id_token,
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 supported AI tools detected.');
390
- info('Looked for ~/.claude/ and ~/.cursor/ directories.');
391
- info('Install Claude Code or Cursor, then re-run: npx -y ai-lens init');
392
- return;
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
- if (err.message.includes('not configured')) {
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: ${err.message}`);
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
- info('--no-hooks: skipping hook configuration and MCP setup');
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