ai-lens 0.8.63 → 0.8.67
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/CHANGELOG.md +32 -0
- package/client/capture.js +29 -19
- package/client/codex.js +9 -4
- package/client/redact.js +20 -3
- package/client/sender.js +13 -39
- package/package.json +3 -2
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
cf6dbc7
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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.67 — 2026-05-26
|
|
6
|
+
- fix: sessions launched from a subdirectory of a repo (e.g. `cd scripts/asr-worker && claude`) now attribute to the repo root instead of the subdirectory. Previously the dashboard showed deep subfolders as if they were the project — a session that ran 74 events inside `meetings-lens/scripts/asr-worker` and 7 at the repo root was attributed to `asr-worker`. SessionStart cwd is now walked up to the nearest `.git` like file-path refinement already did.
|
|
7
|
+
|
|
8
|
+
## 0.8.66 — 2026-05-26
|
|
9
|
+
- 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.
|
|
10
|
+
- chore: `CHANGELOG.md` now ships inside the published npm package, so it's visible on the npm registry page alongside the README.
|
|
11
|
+
|
|
12
|
+
## 0.8.65 — 2026-05-26
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
## 0.8.64 — 2026-04-29
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
18
|
+
## 0.8.63 — 2026-04-28
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
21
|
+
## 0.8.62 — 2026-04-24
|
|
22
|
+
- 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.
|
|
23
|
+
- fix: sessions inside a git submodule stay attached to the submodule itself and no longer collapse into the super-project.
|
|
24
|
+
|
|
25
|
+
## 0.8.61 — 2026-04-14
|
|
26
|
+
- fix: codex-watcher no longer leaks CPU/memory on long-running sessions (ANL-595)
|
|
27
|
+
- perf: new Codex events are read via byte offset instead of re-reading the whole file
|
|
28
|
+
- fix: watcher state is now persisted across restarts — the synthetic SessionEnd is not lost when the process dies mid-rotation
|
|
29
|
+
- fix: the watcher shuts down cleanly when `codexEnabled` is turned off in config
|
|
30
|
+
|
|
31
|
+
## 0.8.60 and earlier
|
|
32
|
+
Entries were not kept — history can be reconstructed from `git log` and `npm view ai-lens time`.
|
package/client/capture.js
CHANGED
|
@@ -547,37 +547,47 @@ function resolveWorktreeToMainRepo(dir) {
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
/**
|
|
550
|
-
* Walk up from a
|
|
550
|
+
* Walk up from a directory to find the nearest .git entry.
|
|
551
551
|
* Returns the repo root (parent of a `.git` directory, or the main repo root
|
|
552
|
-
* when `.git` is a worktree-pointer file) or null.
|
|
552
|
+
* when `.git` is a worktree-pointer file) or null when no `.git` is found.
|
|
553
553
|
*/
|
|
554
|
-
function
|
|
555
|
-
let
|
|
556
|
-
while (
|
|
554
|
+
function findGitRootFromDir(dir) {
|
|
555
|
+
let cur = dir;
|
|
556
|
+
while (cur && cur !== '/' && cur.length > 1) {
|
|
557
557
|
try {
|
|
558
|
-
if (existsSync(join(
|
|
558
|
+
if (existsSync(join(cur, '.git'))) return resolveWorktreeToMainRepo(cur);
|
|
559
559
|
} catch {}
|
|
560
|
-
const parent = dirname(
|
|
561
|
-
if (parent ===
|
|
562
|
-
|
|
560
|
+
const parent = dirname(cur);
|
|
561
|
+
if (parent === cur) break;
|
|
562
|
+
cur = parent;
|
|
563
563
|
}
|
|
564
564
|
return null;
|
|
565
565
|
}
|
|
566
566
|
|
|
567
567
|
/**
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
|
|
571
|
-
|
|
568
|
+
* Walk up from a file path to find the nearest .git entry.
|
|
569
|
+
* Returns the repo root or null when no `.git` is found.
|
|
570
|
+
*/
|
|
571
|
+
function findGitRoot(filePath) {
|
|
572
|
+
return findGitRootFromDir(dirname(filePath));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Resolve a launcher cwd (SessionStart / workspace_roots) to its repo root.
|
|
577
|
+
*
|
|
578
|
+
* Two cases are handled:
|
|
579
|
+
* 1. `dir` is a worktree checkout (`.git` is a pointer file) — roll up to the
|
|
580
|
+
* main repo so per-project dashboards don't fragment per worktree branch.
|
|
581
|
+
* 2. `dir` is a subdirectory of a repo (e.g. user did `cd scripts/asr-worker`
|
|
582
|
+
* before launching Claude Code) — walk up to the nearest `.git`, otherwise
|
|
583
|
+
* a deep subfolder would dominate the session's representative path and
|
|
584
|
+
* hide that the work belongs to the parent repo.
|
|
585
|
+
*
|
|
586
|
+
* Returns `dir` unchanged when no `.git` is found anywhere up the tree.
|
|
572
587
|
*/
|
|
573
588
|
function canonicalizeProjectPath(dir) {
|
|
574
589
|
if (!dir || typeof dir !== 'string') return dir;
|
|
575
|
-
|
|
576
|
-
if (existsSync(join(dir, '.git'))) {
|
|
577
|
-
return resolveWorktreeToMainRepo(dir);
|
|
578
|
-
}
|
|
579
|
-
} catch {}
|
|
580
|
-
return dir;
|
|
590
|
+
return findGitRootFromDir(dir) || dir;
|
|
581
591
|
}
|
|
582
592
|
|
|
583
593
|
/**
|
package/client/codex.js
CHANGED
|
@@ -330,15 +330,20 @@ function buildSpecificToolEvent(stream, tool, input, parsedOutput, timestamp, ra
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
if (tool === 'apply_patch' && wasSuccessful) {
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
333
|
+
const files = extractPatchFiles(input);
|
|
334
|
+
if (files.length === 0) return null;
|
|
335
335
|
return buildUnifiedEvent(
|
|
336
336
|
stream,
|
|
337
337
|
'FileEdit',
|
|
338
338
|
timestamp,
|
|
339
339
|
{
|
|
340
|
-
file,
|
|
341
|
-
|
|
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,
|
|
342
347
|
},
|
|
343
348
|
raw,
|
|
344
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
|
-
|
|
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 },
|
|
@@ -88,8 +90,12 @@ const PATTERNS = [
|
|
|
88
90
|
// Meilisearch master/API key (mc_ + 32+ hex chars)
|
|
89
91
|
{ type: 'MEILISEARCH_KEY', re: /\bmc_[0-9a-f]{32,}\b/g },
|
|
90
92
|
|
|
91
|
-
// Connection string password (://user:password@host) — redacts password only
|
|
92
|
-
|
|
93
|
+
// Connection string password (://user:password@host) — redacts password only.
|
|
94
|
+
// Tightened: exclude JSON structural chars from the user/password classes so
|
|
95
|
+
// a stringified JSON payload (e.g. MCP `tool_response` kept as a single
|
|
96
|
+
// string leaf) can't be matched across field boundaries. See the same note
|
|
97
|
+
// in server/utils/redact.js.
|
|
98
|
+
{ type: 'CONNECTION_STRING', re: /:\/\/([^:@\s"',\[\]{}']+):([^@\s"',\[\]{}']{3,})@/g, replacer: (m, user, _pw) => `://${user}:[REDACTED:CONNECTION_STRING]@` },
|
|
93
99
|
|
|
94
100
|
// MySQL/mysqldump CLI -p<password> syntax (mysql -u user -pSECRET dbname)
|
|
95
101
|
{ type: 'MYSQL_PASSWORD', re: /(\bmysql\w*\b[^\n]*\s-p)(\S{8,})/g, replacer: (m, prefix, _pw) => `${prefix}[REDACTED:MYSQL_PASSWORD]` },
|
|
@@ -120,6 +126,17 @@ const PATTERNS = [
|
|
|
120
126
|
replacer: (m, prefix, _val) => `${prefix}[REDACTED:KEY_VALUE]`,
|
|
121
127
|
},
|
|
122
128
|
|
|
129
|
+
// Form-input "value" fields next to a "Password" / "Token" / "API Key" /
|
|
130
|
+
// "Secret" field name. Catches browser_fill_form / form_input MCP payloads
|
|
131
|
+
// where the literal password sits in `value`, not `password`:
|
|
132
|
+
// {"name":"Password","value":"<password>"}
|
|
133
|
+
// {"fields":[{"name":"Password","type":"textbox","value":"…"}]}
|
|
134
|
+
{
|
|
135
|
+
type: 'FORM_VALUE',
|
|
136
|
+
re: /("name"\s*:\s*"(?:[Pp]assword|[Tt]oken|[Ss]ecret|API\s?Key)[^"]{0,40}"[^}]{0,200}?"value"\s*:\s*")([^"]{4,})/g,
|
|
137
|
+
replacer: (m, prefix, _val) => `${prefix}[REDACTED:FORM_VALUE]`,
|
|
138
|
+
},
|
|
139
|
+
|
|
123
140
|
// Russian key-value pairs: пароль=..., секрет: ..., токен: ..., ключ: ...
|
|
124
141
|
// Separate pattern because \b does not work with Cyrillic (\w is ASCII-only)
|
|
125
142
|
{
|
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
|
-
|
|
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
|
|
555
|
+
// chunkEvents
|
|
550
556
|
// =============================================================================
|
|
551
557
|
|
|
552
558
|
/**
|
|
553
|
-
*
|
|
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 =
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.8.67",
|
|
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",
|