ai-lens 0.8.62 → 0.8.66

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
- ee4f839
1
+ 695b8c8
package/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ History of changes to the `ai-lens` CLI package on npm. New entries go on top. Format: `## X.Y.Z — YYYY-MM-DD`, followed by user-facing bullets.
4
+
5
+ ## 0.8.66 — 2026-05-26
6
+ - chore: republish — the npm registry was stuck on 0.8.63 while two patch releases (0.8.64 Codex `apply_patch` multi-file fix and 0.8.65 raw-field preservation on large events) were tagged but never published. Anyone running `npx -y ai-lens init --yes` will now actually receive those fixes.
7
+ - chore: `CHANGELOG.md` now ships inside the published npm package, so it's visible on the npm registry page alongside the README.
8
+
9
+ ## 0.8.65 — 2026-05-26
10
+ - fix: events larger than 10 KB no longer lose their `raw` field. The sender used to silently strip `raw` from any event that didn't fit a 10 KB POST — this hit 17–19% of PostToolUse for active developers, up to 85% of MCPExecution on Cursor, and 61% of UserPromptSubmit on bots with large system prompts. The sender now bundles small events into ~64 KB POSTs and ships any single large event in its own request, untouched. Only `client/sender.js` changed — server limits stay as they were.
11
+
12
+ ## 0.8.64 — 2026-04-29
13
+ - fix: Codex `apply_patch` now persists ALL touched files, not just the first one. Multi-file patches (the common refactor pattern) used to be truncated — `files_modified`, `file_paths_mentioned` on the stats card, and scope/owner attribution all showed only the first file from the patch.
14
+
15
+ ## 0.8.63 — 2026-04-28
16
+ - fix: Codex `exec_command` now records `exit_code` on the `ShellExecution` event. Without it, analysis systematically missed failed tests, builds, and CLI commands — the stats card believed there were no shell errors.
17
+
18
+ ## 0.8.62 — 2026-04-24
19
+ - fix: sessions inside a git worktree are now attributed to the main repository name instead of the worktree branch (names like `agent-a8d9bb19`, `ANL-689` no longer appear as separate "projects"). Regression ANL-729, fixed for Claude Code, Cursor, and Codex.
20
+ - fix: sessions inside a git submodule stay attached to the submodule itself and no longer collapse into the super-project.
21
+
22
+ ## 0.8.61 — 2026-04-14
23
+ - fix: codex-watcher no longer leaks CPU/memory on long-running sessions (ANL-595)
24
+ - perf: new Codex events are read via byte offset instead of re-reading the whole file
25
+ - fix: watcher state is now persisted across restarts — the synthetic SessionEnd is not lost when the process dies mid-rotation
26
+ - fix: the watcher shuts down cleanly when `codexEnabled` is turned off in config
27
+
28
+ ## 0.8.60 and earlier
29
+ Entries were not kept — history can be reconstructed from `git log` and `npm view ai-lens time`.
package/client/codex.js CHANGED
@@ -305,6 +305,10 @@ function buildSpecificToolEvent(stream, tool, input, parsedOutput, timestamp, ra
305
305
  {
306
306
  command: input?.cmd || input?.command || null,
307
307
  result: parsedOutput.result,
308
+ // parseToolOutput already extracts exit_code from parsed.metadata —
309
+ // include it so build-session-stats can detect failed shell commands
310
+ // by exact signal rather than falling back to the output-text heuristic.
311
+ exit_code: parsedOutput.exitCode ?? null,
308
312
  },
309
313
  raw,
310
314
  );
@@ -326,15 +330,20 @@ function buildSpecificToolEvent(stream, tool, input, parsedOutput, timestamp, ra
326
330
  }
327
331
 
328
332
  if (tool === 'apply_patch' && wasSuccessful) {
329
- const file = extractPatchFiles(input)[0] || null;
330
- if (!file) return null;
333
+ const files = extractPatchFiles(input);
334
+ if (files.length === 0) return null;
331
335
  return buildUnifiedEvent(
332
336
  stream,
333
337
  'FileEdit',
334
338
  timestamp,
335
339
  {
336
- file,
337
- file_path: file,
340
+ // Legacy single-file aliases (file, file_path) — kept so any consumer
341
+ // that only reads the first path still works on the new event shape.
342
+ file: files[0],
343
+ file_path: files[0],
344
+ // Full deduplicated array of every file mentioned in the patch.
345
+ // extractPatchFiles already returns >=1 entry by this branch.
346
+ file_paths: files,
338
347
  },
339
348
  raw,
340
349
  );
package/client/redact.js CHANGED
@@ -50,7 +50,9 @@ const PATTERNS = [
50
50
  },
51
51
 
52
52
  // Generic sk- key (must come after sk-ant- and sk-proj-)
53
- { type: 'API_KEY', re: /sk-[A-Za-z0-9_-]{32,}/g },
53
+ // 16+ chars catches LiteLLM virtual keys (22 chars) and most other sk-
54
+ // formats; OpenAI 48-char keys still match. Lowered from 32 after leak.
55
+ { type: 'API_KEY', re: /sk-[A-Za-z0-9_-]{16,}/g },
54
56
 
55
57
  // Stripe secret key (live only — test keys intentionally not redacted)
56
58
  { type: 'STRIPE_SECRET', re: /sk_live_[A-Za-z0-9]{24,}/g },
@@ -120,6 +122,17 @@ const PATTERNS = [
120
122
  replacer: (m, prefix, _val) => `${prefix}[REDACTED:KEY_VALUE]`,
121
123
  },
122
124
 
125
+ // Form-input "value" fields next to a "Password" / "Token" / "API Key" /
126
+ // "Secret" field name. Catches browser_fill_form / form_input MCP payloads
127
+ // where the literal password sits in `value`, not `password`:
128
+ // {"name":"Password","value":"<password>"}
129
+ // {"fields":[{"name":"Password","type":"textbox","value":"…"}]}
130
+ {
131
+ type: 'FORM_VALUE',
132
+ re: /("name"\s*:\s*"(?:[Pp]assword|[Tt]oken|[Ss]ecret|API\s?Key)[^"]{0,40}"[^}]{0,200}?"value"\s*:\s*")([^"]{4,})/g,
133
+ replacer: (m, prefix, _val) => `${prefix}[REDACTED:FORM_VALUE]`,
134
+ },
135
+
123
136
  // Russian key-value pairs: пароль=..., секрет: ..., токен: ..., ключ: ...
124
137
  // Separate pattern because \b does not work with Cyrillic (\w is ASCII-only)
125
138
  {
package/client/sender.js CHANGED
@@ -61,7 +61,13 @@ export function rotateLog(logPath = LOG_PATH, maxAgeDays = LOG_MAX_AGE_DAYS) {
61
61
  }
62
62
 
63
63
  export const MAX_QUEUE_SIZE = 10_000;
64
- export const MAX_CHUNK_BYTES = 10 * 1024; // 10 KB per POST — small enough to retry cheaply on flaky networks
64
+ // Bundling threshold only when we have many small events, pack them into
65
+ // POSTs of roughly this size. A single event larger than the target is sent
66
+ // as its own chunk untouched (see chunkEvents). The earlier 10 KB cap
67
+ // silently stripped `raw` from any event over the limit (PostToolUse with
68
+ // large MCP outputs, UserPromptSubmit with bot system prompts), which broke
69
+ // downstream analysis. Server accepts up to express.json's 50 MB anyway.
70
+ export const BATCH_TARGET_BYTES = 64 * 1024;
65
71
  export const LOCK_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
66
72
  export const SENDER_BACKOFF_MS = 30_000;
67
73
 
@@ -546,13 +552,15 @@ export function groupByDeveloper(events, hasAuthToken = false) {
546
552
  }
547
553
 
548
554
  // =============================================================================
549
- // chunkEvents (unchanged)
555
+ // chunkEvents
550
556
  // =============================================================================
551
557
 
552
558
  /**
553
- * Split an array of events into chunks that fit within MAX_CHUNK_BYTES.
559
+ * Bundle events into POSTs targeting `maxBytes`. A single event larger than
560
+ * the target gets its own chunk — we never split or strip it. The server
561
+ * accepts up to 50 MB and rejects events > 4 MB on its own.
554
562
  */
555
- export function chunkEvents(events, maxBytes = MAX_CHUNK_BYTES) {
563
+ export function chunkEvents(events, maxBytes = BATCH_TARGET_BYTES) {
556
564
  const chunks = [];
557
565
  let chunk = [];
558
566
  let chunkSize = 2; // opening '[' + closing ']'
@@ -576,38 +584,6 @@ export function chunkEvents(events, maxBytes = MAX_CHUNK_BYTES) {
576
584
  return chunks;
577
585
  }
578
586
 
579
- // =============================================================================
580
- // filterOversized (unchanged)
581
- // =============================================================================
582
-
583
- /**
584
- * Filter out oversized events, salvaging those with a `raw` field by stripping it.
585
- */
586
- export function filterOversized(batch, maxBytes = MAX_CHUNK_BYTES) {
587
- const sendable = [];
588
- const droppedIds = new Set();
589
- for (const evt of batch) {
590
- let evtBytes = Buffer.byteLength(JSON.stringify(evt));
591
- // Work on a shallow copy — never mutate the input array element.
592
- // The original object in pending/ must not be modified.
593
- let candidate = evt;
594
- if (evtBytes > maxBytes && evt.raw !== undefined) {
595
- const originalBytes = evtBytes;
596
- candidate = { ...evt };
597
- delete candidate.raw;
598
- evtBytes = Buffer.byteLength(JSON.stringify(candidate));
599
- log({ msg: 'stripped-raw-oversized', event_id: evt.event_id, type: evt.type, original_bytes: originalBytes, stripped_bytes: evtBytes });
600
- }
601
- if (evtBytes > maxBytes) {
602
- log({ msg: 'skip-oversized', event_id: evt.event_id, type: evt.type, bytes: evtBytes, limit: maxBytes });
603
- if (evt.event_id) droppedIds.add(evt.event_id);
604
- } else {
605
- sendable.push(candidate);
606
- }
607
- }
608
- return { sendable, droppedIds };
609
- }
610
-
611
587
  // =============================================================================
612
588
  // HTTP
613
589
  // =============================================================================
@@ -797,9 +773,7 @@ async function main() {
797
773
 
798
774
  try {
799
775
  for (const { identity, events: batch } of byDeveloper.values()) {
800
- const { sendable, droppedIds } = filterOversized(batch);
801
- for (const id of droppedIds) sentEventIds.add(id);
802
- const chunks = chunkEvents(sendable);
776
+ const chunks = chunkEvents(batch);
803
777
  let totalReceived = 0;
804
778
  for (const chunk of chunks) {
805
779
  const result = await postEvents(serverUrl, chunk, identity, hasAuthToken ? authToken : null, { lockPath });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.62",
3
+ "version": "0.8.66",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {
@@ -11,7 +11,8 @@
11
11
  "cli/",
12
12
  "client/",
13
13
  ".commithash",
14
- "README.md"
14
+ "README.md",
15
+ "CHANGELOG.md"
15
16
  ],
16
17
  "scripts": {
17
18
  "prepare": "git rev-parse --short HEAD > .commithash 2>/dev/null || true; git config core.hooksPath .githooks 2>/dev/null || true",