ai-lens 0.8.42 → 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 +1 -1
- package/cli/hooks.js +16 -15
- package/cli/status.js +31 -6
- package/client/capture.js +63 -0
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
|
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(
|
|
199
|
-
beforeSubmitPrompt: () => ({ command: cursorCaptureCommand(
|
|
200
|
-
postToolUse: () => ({ command: cursorCaptureCommand(
|
|
201
|
-
postToolUseFailure: () => ({ command: cursorCaptureCommand(
|
|
202
|
-
afterFileEdit: () => ({ command: cursorCaptureCommand(
|
|
203
|
-
afterShellExecution: () => ({ command: cursorCaptureCommand(
|
|
204
|
-
afterMCPExecution: () => ({ command: cursorCaptureCommand(
|
|
205
|
-
subagentStart: () => ({ command: cursorCaptureCommand(
|
|
206
|
-
subagentStop: () => ({ command: cursorCaptureCommand(
|
|
207
|
-
preCompact: () => ({ command: cursorCaptureCommand(
|
|
208
|
-
afterAgentResponse: () => ({ command: cursorCaptureCommand(
|
|
209
|
-
afterAgentThought: () => ({ command: cursorCaptureCommand(
|
|
210
|
-
stop: () => ({ command: cursorCaptureCommand(
|
|
211
|
-
sessionEnd: () => ({ command: cursorCaptureCommand(
|
|
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/cli/status.js
CHANGED
|
@@ -847,7 +847,7 @@ function buildWarnings(results, { hasEverCaptured, lastCaptureMs, serverSessionC
|
|
|
847
847
|
// Report file generation
|
|
848
848
|
// ---------------------------------------------------------------------------
|
|
849
849
|
|
|
850
|
-
function buildReport(results, timestamp, warnings = []) {
|
|
850
|
+
function buildReport(results, timestamp, warnings = [], allTools = TOOL_CONFIGS) {
|
|
851
851
|
const lines = [
|
|
852
852
|
`AI Lens Status Report`,
|
|
853
853
|
`Generated: ${timestamp}`,
|
|
@@ -890,8 +890,8 @@ function buildReport(results, timestamp, warnings = []) {
|
|
|
890
890
|
}
|
|
891
891
|
lines.push('');
|
|
892
892
|
|
|
893
|
-
// Full hook configs
|
|
894
|
-
for (const tool of
|
|
893
|
+
// Full hook configs (global + project)
|
|
894
|
+
for (const tool of allTools) {
|
|
895
895
|
lines.push(`${'='.repeat(60)}`);
|
|
896
896
|
lines.push(`${tool.name} hook config (${tool.configPath}):`);
|
|
897
897
|
try {
|
|
@@ -1028,15 +1028,24 @@ export default async function status() {
|
|
|
1028
1028
|
// 6. Hooks: global + project (Cursor then Claude Code; within each: global then project)
|
|
1029
1029
|
const installedTools = detectInstalledTools();
|
|
1030
1030
|
const toolsWithProject = getToolsForCaptureTest();
|
|
1031
|
-
const
|
|
1031
|
+
const isGlobalTool = (tool) => TOOL_CONFIGS.includes(tool);
|
|
1032
|
+
const toolLabel = (tool) => (isGlobalTool(tool) ? `${tool.name} (global)` : tool.name);
|
|
1032
1033
|
const hooksOrder = (a, b) => {
|
|
1033
1034
|
const nameA = toolLabel(a), nameB = toolLabel(b);
|
|
1034
1035
|
const cursorFirst = (n) => (n.startsWith('Cursor') ? 0 : 1);
|
|
1035
1036
|
const globalFirst = (n) => (n.includes('(global)') ? 0 : 1);
|
|
1036
1037
|
return cursorFirst(nameA) - cursorFirst(nameB) || globalFirst(nameA) - globalFirst(nameB);
|
|
1037
1038
|
};
|
|
1039
|
+
// Track which scopes have current hooks for mode detection
|
|
1040
|
+
let hasGlobalHooks = false;
|
|
1041
|
+
let hasProjectHooks = false;
|
|
1038
1042
|
for (const tool of toolsWithProject.slice().sort(hooksOrder)) {
|
|
1039
|
-
|
|
1043
|
+
const hookResult = checkHooks(tool);
|
|
1044
|
+
printLine(toolLabel(tool), hookResult);
|
|
1045
|
+
if (hookResult.ok === true) {
|
|
1046
|
+
if (isGlobalTool(tool)) hasGlobalHooks = true;
|
|
1047
|
+
else hasProjectHooks = true;
|
|
1048
|
+
}
|
|
1040
1049
|
}
|
|
1041
1050
|
// Global tools not installed (no ~/.cursor or ~/.claude) — same order: Cursor then Claude Code
|
|
1042
1051
|
const notInstalled = TOOL_CONFIGS.filter(t => !installedTools.includes(t));
|
|
@@ -1045,6 +1054,20 @@ export default async function status() {
|
|
|
1045
1054
|
printLine(toolLabel(tool), r);
|
|
1046
1055
|
}
|
|
1047
1056
|
|
|
1057
|
+
// 6b. Hook mode: global, project, both, or none
|
|
1058
|
+
const hookMode = hasGlobalHooks && hasProjectHooks ? 'global + project (may double-fire!)'
|
|
1059
|
+
: hasProjectHooks ? 'project'
|
|
1060
|
+
: hasGlobalHooks ? 'global'
|
|
1061
|
+
: 'none';
|
|
1062
|
+
const hookModeOk = hasGlobalHooks && hasProjectHooks ? null
|
|
1063
|
+
: (hasGlobalHooks || hasProjectHooks) ? true
|
|
1064
|
+
: false;
|
|
1065
|
+
printLine('Hook mode', {
|
|
1066
|
+
ok: hookModeOk,
|
|
1067
|
+
summary: hookMode,
|
|
1068
|
+
detail: `Global hooks active: ${hasGlobalHooks}\nProject hooks active: ${hasProjectHooks}${hasGlobalHooks && hasProjectHooks ? '\nWarning: both global and project hooks are active — events may be captured twice. Run init with --project-hooks to consolidate.' : ''}`,
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1048
1071
|
// 7. Queue (before capture test so test event doesn't show as pending)
|
|
1049
1072
|
const queueResult = checkQueue();
|
|
1050
1073
|
printLine('Queue', queueResult);
|
|
@@ -1103,7 +1126,9 @@ export default async function status() {
|
|
|
1103
1126
|
|
|
1104
1127
|
// Write report file
|
|
1105
1128
|
const timestamp = new Date().toISOString();
|
|
1106
|
-
|
|
1129
|
+
// Merge global TOOL_CONFIGS (always listed, even if not installed) with project tools
|
|
1130
|
+
const allToolsForReport = [...TOOL_CONFIGS, ...toolsWithProject.filter(t => !TOOL_CONFIGS.includes(t))];
|
|
1131
|
+
const report = buildReport(results, timestamp, warnings, allToolsForReport);
|
|
1107
1132
|
try {
|
|
1108
1133
|
writeFileSync(REPORT_PATH, report);
|
|
1109
1134
|
blank();
|
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
|
}
|