ai-lens 0.8.43 → 0.8.44

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
- eea31dd
1
+ 7043044
package/cli/hooks.js CHANGED
@@ -193,22 +193,23 @@ const CLAUDE_CODE_HOOKS = {
193
193
  SubagentStop: () => ({ matcher: '', hooks: [{ type: 'command', command: captureCommand(true, true) }] }),
194
194
  };
195
195
 
196
- // Use tilde path (~/.ai-lens/...) so settings are portable (no absolute paths).
196
+ // Use absolute path Cursor does not expand ~ in hook commands (it passes the
197
+ // literal string to Node, which resolves it relative to CWD).
197
198
  const CURSOR_HOOKS = {
198
- sessionStart: () => ({ command: cursorCaptureCommand(true) }),
199
- beforeSubmitPrompt: () => ({ command: cursorCaptureCommand(true) }),
200
- postToolUse: () => ({ command: cursorCaptureCommand(true) }),
201
- postToolUseFailure: () => ({ command: cursorCaptureCommand(true) }),
202
- afterFileEdit: () => ({ command: cursorCaptureCommand(true) }),
203
- afterShellExecution: () => ({ command: cursorCaptureCommand(true) }),
204
- afterMCPExecution: () => ({ command: cursorCaptureCommand(true) }),
205
- subagentStart: () => ({ command: cursorCaptureCommand(true) }),
206
- subagentStop: () => ({ command: cursorCaptureCommand(true) }),
207
- preCompact: () => ({ command: cursorCaptureCommand(true) }),
208
- afterAgentResponse: () => ({ command: cursorCaptureCommand(true) }),
209
- afterAgentThought: () => ({ command: cursorCaptureCommand(true) }),
210
- stop: () => ({ command: cursorCaptureCommand(true) }),
211
- sessionEnd: () => ({ command: cursorCaptureCommand(true) }),
199
+ sessionStart: () => ({ command: cursorCaptureCommand(false) }),
200
+ beforeSubmitPrompt: () => ({ command: cursorCaptureCommand(false) }),
201
+ postToolUse: () => ({ command: cursorCaptureCommand(false) }),
202
+ postToolUseFailure: () => ({ command: cursorCaptureCommand(false) }),
203
+ afterFileEdit: () => ({ command: cursorCaptureCommand(false) }),
204
+ afterShellExecution: () => ({ command: cursorCaptureCommand(false) }),
205
+ afterMCPExecution: () => ({ command: cursorCaptureCommand(false) }),
206
+ subagentStart: () => ({ command: cursorCaptureCommand(false) }),
207
+ subagentStop: () => ({ command: cursorCaptureCommand(false) }),
208
+ preCompact: () => ({ command: cursorCaptureCommand(false) }),
209
+ afterAgentResponse: () => ({ command: cursorCaptureCommand(false) }),
210
+ afterAgentThought: () => ({ command: cursorCaptureCommand(false) }),
211
+ stop: () => ({ command: cursorCaptureCommand(false) }),
212
+ sessionEnd: () => ({ command: cursorCaptureCommand(false) }),
212
213
  };
213
214
 
214
215
  // Same as CURSOR_HOOKS but command uses ~/.ai-lens/... (for --project-hooks)
package/client/capture.js CHANGED
@@ -191,6 +191,55 @@ function getCachedSessionPath(sessionId) {
191
191
  try { return readFileSync(dst, 'utf-8').trim() || null; } catch { return null; }
192
192
  }
193
193
 
194
+ // =============================================================================
195
+ // Project Path Refinement from Tool Events
196
+ // =============================================================================
197
+
198
+ /**
199
+ * Extract an absolute file path from tool input.
200
+ * Covers Read/Edit/Write (file_path), Glob/Grep (path).
201
+ */
202
+ function extractFilePath(toolInput) {
203
+ if (!toolInput || typeof toolInput !== 'object') return null;
204
+ const p = toolInput.file_path || toolInput.path;
205
+ return (typeof p === 'string' && p.startsWith('/')) ? p : null;
206
+ }
207
+
208
+ /**
209
+ * Walk up from a path to find the nearest .git directory.
210
+ * Returns the git root (parent of .git) or null.
211
+ */
212
+ function findGitRoot(filePath) {
213
+ let dir = dirname(filePath);
214
+ while (dir && dir !== '/' && dir.length > 1) {
215
+ try {
216
+ if (existsSync(join(dir, '.git'))) return dir;
217
+ } catch {}
218
+ const parent = dirname(dir);
219
+ if (parent === dir) break;
220
+ dir = parent;
221
+ }
222
+ return null;
223
+ }
224
+
225
+ /**
226
+ * Refine project_path using file paths from tool events.
227
+ * Picks the deepest (most specific) git root — correct for nested repos
228
+ * like etl-pipelines/bk-reports-automation/.
229
+ * Only refines "downward" (more specific), never upward.
230
+ */
231
+ function refineProjectPath(event, projectPath, sessionId) {
232
+ const toolInput = event.tool_input || event.input;
233
+ const filePath = extractFilePath(toolInput);
234
+ if (!filePath) return projectPath;
235
+ const gitRoot = findGitRoot(filePath);
236
+ if (gitRoot && (!projectPath || gitRoot.length > projectPath.length)) {
237
+ if (sessionId) cacheSessionPath(sessionId, gitRoot);
238
+ return gitRoot;
239
+ }
240
+ return projectPath;
241
+ }
242
+
194
243
  // =============================================================================
195
244
  // Deduplication: drop consecutive identical event types per session
196
245
  // =============================================================================
@@ -354,6 +403,11 @@ function normalizeClaudeCode(event) {
354
403
  data = { hook: hookType };
355
404
  }
356
405
 
406
+ // Refine project_path from file paths in tool events (ANL-518)
407
+ if (hookType === 'PostToolUse' || hookType === 'PostToolUseFailure') {
408
+ projectPath = refineProjectPath(event, projectPath, sessionId);
409
+ }
410
+
357
411
  if (event.permission_mode) {
358
412
  data.permission_mode = event.permission_mode;
359
413
  }
@@ -523,6 +577,15 @@ function normalizeCursor(event) {
523
577
  data = { hook_event_name: hookName };
524
578
  }
525
579
 
580
+ // Refine project_path from file paths in tool events (ANL-518)
581
+ if (hookName === 'postToolUse' || hookName === 'postToolUseFailure' || hookName === 'afterFileEdit') {
582
+ // Cursor passes file_path at top level for afterFileEdit
583
+ const cursorEvent = hookName === 'afterFileEdit' && event.file_path
584
+ ? { ...event, tool_input: { file_path: event.file_path } }
585
+ : event;
586
+ projectPath = refineProjectPath(cursorEvent, projectPath, sessionId);
587
+ }
588
+
526
589
  if (event.permission_mode) {
527
590
  data.permission_mode = event.permission_mode;
528
591
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.43",
3
+ "version": "0.8.44",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {