ai-lens 0.8.61 → 0.8.63
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/client/capture.js +78 -5
- package/client/codex.js +53 -3
- package/package.json +4 -2
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
5128852
|
package/client/capture.js
CHANGED
|
@@ -494,14 +494,68 @@ function extractFilePath(toolInput) {
|
|
|
494
494
|
}
|
|
495
495
|
|
|
496
496
|
/**
|
|
497
|
-
*
|
|
498
|
-
*
|
|
497
|
+
* Given a directory that contains a `.git` entry, return the real repo root.
|
|
498
|
+
* If `.git` is a directory, the input is already the repo root. If `.git` is
|
|
499
|
+
* a FILE (git worktree — contains "gitdir: <path-to-main>/.git/worktrees/<n>")
|
|
500
|
+
* we follow the pointer up to the main `.git` directory and return its parent
|
|
501
|
+
* so worktree sessions attribute to the main repo rather than a per-branch
|
|
502
|
+
* pseudo-project (e.g. `agent-a8d9bb19`, `ANL-689`).
|
|
503
|
+
*
|
|
504
|
+
* Submodules also use a `.git` file, but its gitdir points under
|
|
505
|
+
* `<super>/.git/modules/<name>`. We intentionally skip those so edits inside
|
|
506
|
+
* a submodule keep attributing to the submodule, not the super-project —
|
|
507
|
+
* that's outside the scope of ANL-729.
|
|
508
|
+
*
|
|
509
|
+
* Never returns a path "outside" the filesystem; on any parse failure falls
|
|
510
|
+
* back to the original dir.
|
|
511
|
+
*/
|
|
512
|
+
function resolveWorktreeToMainRepo(dir) {
|
|
513
|
+
try {
|
|
514
|
+
const gitEntry = join(dir, '.git');
|
|
515
|
+
const st = statSync(gitEntry);
|
|
516
|
+
if (st.isDirectory()) return dir;
|
|
517
|
+
if (!st.isFile()) return dir;
|
|
518
|
+
|
|
519
|
+
const contents = readFileSync(gitEntry, 'utf-8');
|
|
520
|
+
const match = contents.match(/^\s*gitdir:\s*(.+?)\s*$/m);
|
|
521
|
+
if (!match) return dir;
|
|
522
|
+
|
|
523
|
+
let gitdir = match[1];
|
|
524
|
+
// Resolve relative pointers against the worktree dir. POSIX-only check
|
|
525
|
+
// (`startsWith('/')`) — AI Lens runs on macOS/Linux dev machines, Windows
|
|
526
|
+
// isn't a supported client target.
|
|
527
|
+
if (!gitdir.startsWith('/')) gitdir = join(dir, gitdir);
|
|
528
|
+
|
|
529
|
+
// Only real worktrees: `<main>/.git/worktrees/<name>`. Submodules
|
|
530
|
+
// (`<super>/.git/modules/<name>`) are left alone on purpose.
|
|
531
|
+
if (!gitdir.includes('/.git/worktrees/')) return dir;
|
|
532
|
+
|
|
533
|
+
// Walk up until we hit the directory literally named `.git`; its parent
|
|
534
|
+
// is the main repo. Depth is bounded: gitdir is `<main>/.git/worktrees/<n>`
|
|
535
|
+
// so the `.git` segment is at most 2 levels up — 4 gives a safety margin.
|
|
536
|
+
let cur = gitdir;
|
|
537
|
+
for (let i = 0; i < 4; i++) {
|
|
538
|
+
const parent = dirname(cur);
|
|
539
|
+
if (parent === cur) break;
|
|
540
|
+
if (cur.endsWith('/.git')) return parent;
|
|
541
|
+
cur = parent;
|
|
542
|
+
}
|
|
543
|
+
return dir;
|
|
544
|
+
} catch {
|
|
545
|
+
return dir;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Walk up from a path to find the nearest .git entry.
|
|
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.
|
|
499
553
|
*/
|
|
500
554
|
function findGitRoot(filePath) {
|
|
501
555
|
let dir = dirname(filePath);
|
|
502
556
|
while (dir && dir !== '/' && dir.length > 1) {
|
|
503
557
|
try {
|
|
504
|
-
if (existsSync(join(dir, '.git'))) return dir;
|
|
558
|
+
if (existsSync(join(dir, '.git'))) return resolveWorktreeToMainRepo(dir);
|
|
505
559
|
} catch {}
|
|
506
560
|
const parent = dirname(dir);
|
|
507
561
|
if (parent === dir) break;
|
|
@@ -510,6 +564,22 @@ function findGitRoot(filePath) {
|
|
|
510
564
|
return null;
|
|
511
565
|
}
|
|
512
566
|
|
|
567
|
+
/**
|
|
568
|
+
* If `dir` is a git worktree checkout, return the main repo root; otherwise
|
|
569
|
+
* return `dir` unchanged. Used at session intake where we have a launcher cwd
|
|
570
|
+
* (SessionStart / workspace_roots) and want to avoid attributing the session
|
|
571
|
+
* to a worktree branch name.
|
|
572
|
+
*/
|
|
573
|
+
function canonicalizeProjectPath(dir) {
|
|
574
|
+
if (!dir || typeof dir !== 'string') return dir;
|
|
575
|
+
try {
|
|
576
|
+
if (existsSync(join(dir, '.git'))) {
|
|
577
|
+
return resolveWorktreeToMainRepo(dir);
|
|
578
|
+
}
|
|
579
|
+
} catch {}
|
|
580
|
+
return dir;
|
|
581
|
+
}
|
|
582
|
+
|
|
513
583
|
/**
|
|
514
584
|
* Refine project_path using file paths from tool events.
|
|
515
585
|
* Picks the deepest (most specific) git root — correct for nested repos
|
|
@@ -617,8 +687,9 @@ function normalizeClaudeCode(event) {
|
|
|
617
687
|
let type = CLAUDE_CODE_TYPE_MAP[hookType] || hookType;
|
|
618
688
|
const timestamp = new Date().toISOString();
|
|
619
689
|
|
|
620
|
-
// Extract project path from cwd (SessionStart) or cache
|
|
621
|
-
|
|
690
|
+
// Extract project path from cwd (SessionStart) or cache. Canonicalize
|
|
691
|
+
// worktree checkouts to the main repo root (ANL-599 follow-up).
|
|
692
|
+
let projectPath = event.cwd ? canonicalizeProjectPath(event.cwd) : null;
|
|
622
693
|
if (projectPath && sessionId) {
|
|
623
694
|
cacheSessionPath(sessionId, projectPath);
|
|
624
695
|
} else if (sessionId) {
|
|
@@ -826,6 +897,8 @@ function normalizeCursor(event) {
|
|
|
826
897
|
const type = CURSOR_TYPE_MAP[hookName] || hookName;
|
|
827
898
|
const timestamp = new Date().toISOString();
|
|
828
899
|
let projectPath = pickWorkspaceRoot(event.workspace_roots);
|
|
900
|
+
// Canonicalize worktree checkouts to main repo root (ANL-599 follow-up).
|
|
901
|
+
if (projectPath) projectPath = canonicalizeProjectPath(projectPath);
|
|
829
902
|
// Guard: null sessionId would cache/lookup under key "null", contaminating unrelated sessions
|
|
830
903
|
if (projectPath && sessionId) {
|
|
831
904
|
cacheSessionPath(sessionId, projectPath);
|
package/client/codex.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, realpathSync } from 'node:fs';
|
|
1
|
+
import { existsSync, realpathSync, statSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { toNumberOrNull } from './token-usage.js';
|
|
@@ -139,13 +139,59 @@ function extractFilePath(input) {
|
|
|
139
139
|
return null;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// Mirrors `resolveWorktreeToMainRepo` in capture.js (ANL-729). If this
|
|
143
|
+
// changes in one place, change it in the other — we keep the copy inline
|
|
144
|
+
// instead of a shared module to avoid cross-file churn in this pipeline.
|
|
145
|
+
function resolveWorktreeToMainRepo(dir) {
|
|
146
|
+
try {
|
|
147
|
+
const gitEntry = join(dir, '.git');
|
|
148
|
+
const st = statSync(gitEntry);
|
|
149
|
+
if (st.isDirectory()) return dir;
|
|
150
|
+
if (!st.isFile()) return dir;
|
|
151
|
+
|
|
152
|
+
const contents = readFileSync(gitEntry, 'utf-8');
|
|
153
|
+
const match = contents.match(/^\s*gitdir:\s*(.+?)\s*$/m);
|
|
154
|
+
if (!match) return dir;
|
|
155
|
+
|
|
156
|
+
let gitdir = match[1];
|
|
157
|
+
// POSIX-only (`startsWith('/')`) — AI Lens clients are macOS/Linux.
|
|
158
|
+
if (!gitdir.startsWith('/')) gitdir = join(dir, gitdir);
|
|
159
|
+
|
|
160
|
+
// Only real worktrees (`<main>/.git/worktrees/<name>`). Submodules
|
|
161
|
+
// (`<super>/.git/modules/<name>`) are left alone.
|
|
162
|
+
if (!gitdir.includes('/.git/worktrees/')) return dir;
|
|
163
|
+
|
|
164
|
+
// `.git` segment sits 2 levels above the gitdir; 4 is a safety margin.
|
|
165
|
+
let cur = gitdir;
|
|
166
|
+
for (let i = 0; i < 4; i++) {
|
|
167
|
+
const parent = dirname(cur);
|
|
168
|
+
if (parent === cur) break;
|
|
169
|
+
if (cur.endsWith('/.git')) return parent;
|
|
170
|
+
cur = parent;
|
|
171
|
+
}
|
|
172
|
+
return dir;
|
|
173
|
+
} catch {
|
|
174
|
+
return dir;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function canonicalizeProjectPath(dir) {
|
|
179
|
+
if (!dir || typeof dir !== 'string') return dir;
|
|
180
|
+
try {
|
|
181
|
+
if (existsSync(join(dir, '.git'))) {
|
|
182
|
+
return resolveWorktreeToMainRepo(dir);
|
|
183
|
+
}
|
|
184
|
+
} catch {}
|
|
185
|
+
return dir;
|
|
186
|
+
}
|
|
187
|
+
|
|
142
188
|
function findGitRoot(filePath) {
|
|
143
189
|
if (!filePath || typeof filePath !== 'string' || !filePath.startsWith('/')) return null;
|
|
144
190
|
|
|
145
191
|
let dir = filePath;
|
|
146
192
|
while (dir && dir !== '/' && dir.length > 1) {
|
|
147
193
|
try {
|
|
148
|
-
if (existsSync(join(dir, '.git'))) return dir;
|
|
194
|
+
if (existsSync(join(dir, '.git'))) return resolveWorktreeToMainRepo(dir);
|
|
149
195
|
} catch {}
|
|
150
196
|
|
|
151
197
|
const parent = dirname(dir);
|
|
@@ -259,6 +305,10 @@ function buildSpecificToolEvent(stream, tool, input, parsedOutput, timestamp, ra
|
|
|
259
305
|
{
|
|
260
306
|
command: input?.cmd || input?.command || null,
|
|
261
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,
|
|
262
312
|
},
|
|
263
313
|
raw,
|
|
264
314
|
);
|
|
@@ -327,7 +377,7 @@ export function normalizeCodexSessionEntries(record, state, streamKey = 'default
|
|
|
327
377
|
|
|
328
378
|
stream.sessionId = logicalSessionId;
|
|
329
379
|
stream.rawSessionId = sessionId;
|
|
330
|
-
stream.projectPath = cwd;
|
|
380
|
+
stream.projectPath = canonicalizeProjectPath(cwd);
|
|
331
381
|
stream.hasActivity = false;
|
|
332
382
|
stream.model = null;
|
|
333
383
|
events.push(buildUnifiedEvent(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-lens",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.63",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Centralized session analytics for AI coding tools",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"prepare": "git rev-parse --short HEAD > .commithash 2>/dev/null || true",
|
|
17
|
+
"prepare": "git rev-parse --short HEAD > .commithash 2>/dev/null || true; git config core.hooksPath .githooks 2>/dev/null || true",
|
|
18
|
+
"version": "node scripts/check-changelog.js && git add CHANGELOG.md",
|
|
19
|
+
"release": "./scripts/release-cli.sh",
|
|
18
20
|
"init": "node bin/ai-lens.js init",
|
|
19
21
|
"remove": "node bin/ai-lens.js remove",
|
|
20
22
|
"status": "node bin/ai-lens.js status",
|