ai-lens 0.8.21 → 0.8.23

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
- 9f6f7a9
1
+ 9446d54
package/cli/hooks.js CHANGED
@@ -111,6 +111,14 @@ export function captureCommand() {
111
111
  : `${shellEscape(nodePath.replace(/\\/g, '/'))} ${escaped}`;
112
112
  }
113
113
 
114
+ // Cursor on Windows executes hooks via PowerShell, which treats a quoted path like
115
+ // "node.exe" as a string expression, not a command invocation. The & (call) operator
116
+ // is required. Claude Code uses bash or cmd.exe where & is not needed (and breaks bash).
117
+ export function cursorCaptureCommand() {
118
+ const cmd = captureCommand();
119
+ return process.platform === 'win32' ? `& ${cmd}` : cmd;
120
+ }
121
+
114
122
  // ---------------------------------------------------------------------------
115
123
  // Client file installation
116
124
  // ---------------------------------------------------------------------------
@@ -163,20 +171,20 @@ const CLAUDE_CODE_HOOKS = {
163
171
  };
164
172
 
165
173
  const CURSOR_HOOKS = {
166
- sessionStart: () => ({ command: captureCommand() }),
167
- beforeSubmitPrompt: () => ({ command: captureCommand() }),
168
- postToolUse: () => ({ command: captureCommand() }),
169
- postToolUseFailure: () => ({ command: captureCommand() }),
170
- afterFileEdit: () => ({ command: captureCommand() }),
171
- afterShellExecution: () => ({ command: captureCommand() }),
172
- afterMCPExecution: () => ({ command: captureCommand() }),
173
- subagentStart: () => ({ command: captureCommand() }),
174
- subagentStop: () => ({ command: captureCommand() }),
175
- preCompact: () => ({ command: captureCommand() }),
176
- afterAgentResponse: () => ({ command: captureCommand() }),
177
- afterAgentThought: () => ({ command: captureCommand() }),
178
- stop: () => ({ command: captureCommand() }),
179
- sessionEnd: () => ({ command: captureCommand() }),
174
+ sessionStart: () => ({ command: cursorCaptureCommand() }),
175
+ beforeSubmitPrompt: () => ({ command: cursorCaptureCommand() }),
176
+ postToolUse: () => ({ command: cursorCaptureCommand() }),
177
+ postToolUseFailure: () => ({ command: cursorCaptureCommand() }),
178
+ afterFileEdit: () => ({ command: cursorCaptureCommand() }),
179
+ afterShellExecution: () => ({ command: cursorCaptureCommand() }),
180
+ afterMCPExecution: () => ({ command: cursorCaptureCommand() }),
181
+ subagentStart: () => ({ command: cursorCaptureCommand() }),
182
+ subagentStop: () => ({ command: cursorCaptureCommand() }),
183
+ preCompact: () => ({ command: cursorCaptureCommand() }),
184
+ afterAgentResponse: () => ({ command: cursorCaptureCommand() }),
185
+ afterAgentThought: () => ({ command: cursorCaptureCommand() }),
186
+ stop: () => ({ command: cursorCaptureCommand() }),
187
+ sessionEnd: () => ({ command: cursorCaptureCommand() }),
180
188
  };
181
189
 
182
190
  export const TOOL_CONFIGS = [
@@ -218,7 +226,11 @@ export function isAiLensHook(entry) {
218
226
  }
219
227
 
220
228
  function isCurrentAiLensHook(entry, expected) {
221
- const expected_cmd = captureCommand();
229
+ // Extract expected command from the hook definition (supports per-tool commands,
230
+ // e.g. Cursor uses & prefix on Windows for PowerShell, Claude Code does not).
231
+ const expected_cmd = expected?.command
232
+ || expected?.hooks?.[0]?.command
233
+ || captureCommand(); // fallback
222
234
  // Normalize separators so hooks installed before path-normalization fix still match
223
235
  const norm = s => (s || '').replace(/\\/g, '/');
224
236
  // Flat format (Cursor)
package/cli/status.js CHANGED
@@ -98,8 +98,10 @@ function validateHookCommandPaths(tool) {
98
98
  }
99
99
 
100
100
  // Extract node path (first token). Skip if /usr/bin/env node (node resolved via PATH)
101
- if (!command.startsWith('/usr/bin/env node')) {
102
- const nodeMatch = command.match(/^["']([^"']+)["']|^(\S+)/);
101
+ // Strip "& " prefix (PowerShell call operator, added on Windows) before matching.
102
+ const cmdForNode = command.replace(/^& /, '');
103
+ if (!cmdForNode.startsWith('/usr/bin/env node')) {
104
+ const nodeMatch = cmdForNode.match(/^["']([^"']+)["']|^(\S+)/);
103
105
  if (nodeMatch) {
104
106
  const nodePath = nodeMatch[1] || nodeMatch[2];
105
107
  if (nodePath !== 'node' && !existsSync(nodePath)) {
@@ -174,21 +176,72 @@ function checkCaptureRun(installedTools) {
174
176
  }
175
177
  } catch { /* best effort */ }
176
178
 
177
- toolResults.push({ name, ok: exitOk, cmd: testCmd, exitDetail, captureNote });
179
+ // Shell command test: execute the actual hook command string through the shell
180
+ // to catch path/quoting/slash issues that direct spawn bypasses.
181
+ // This simulates how Claude Code / Cursor actually invoke hooks on the OS.
182
+ const shellSessionId = 'status-shell-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7);
183
+ const shellEvent = JSON.stringify({
184
+ hook_event_name: 'Stop',
185
+ session_id: shellSessionId,
186
+ stop_reason: 'test',
187
+ });
188
+ let shellOk = false;
189
+ let shellDetail = '';
190
+ try {
191
+ const shellResult = spawnSync(command, {
192
+ input: shellEvent,
193
+ shell: true,
194
+ encoding: 'utf-8',
195
+ timeout: 10_000,
196
+ env: { ...process.env, AI_LENS_PROJECTS: join(homedir(), '.ai-lens-status-check-nonexistent') },
197
+ windowsHide: true,
198
+ });
199
+ if (shellResult.error) throw shellResult.error;
200
+ shellOk = shellResult.status === 0;
201
+ shellDetail = shellOk ? 'exit 0' : `Exit code: ${shellResult.status}\nError: ${(shellResult.stderr || '').trim() || '(no stderr)'}`;
202
+ } catch (err) {
203
+ shellDetail = `Exit code: N/A\nError: ${err.message}`;
204
+ }
205
+
206
+ let shellCaptureNote = '';
207
+ try {
208
+ if (existsSync(CAPTURE_LOG_PATH)) {
209
+ const logLines = readFileSync(CAPTURE_LOG_PATH, 'utf-8').split(/\r?\n/).filter(Boolean);
210
+ for (let i = logLines.length - 1; i >= 0; i--) {
211
+ try {
212
+ const entry = JSON.parse(logLines[i]);
213
+ if (entry.session_id === shellSessionId) {
214
+ shellCaptureNote = entry.reason || entry.msg || 'unknown';
215
+ break;
216
+ }
217
+ } catch { /* skip unparseable lines */ }
218
+ }
219
+ }
220
+ } catch { /* best effort */ }
221
+
222
+ toolResults.push({ name, ok: exitOk, cmd: testCmd, exitDetail, captureNote, shellOk, shellCmd: command, shellDetail, shellCaptureNote });
178
223
  }
179
224
 
180
- const allOk = toolResults.every(r => r.ok);
181
- const failedTools = toolResults.filter(r => !r.ok).map(r => r.name);
225
+ const allOk = toolResults.every(r => r.ok && r.shellOk);
226
+ const failedTools = toolResults.filter(r => !r.ok || !r.shellOk).map(r => r.name);
182
227
  const summaryParts = toolResults.map(r => {
183
- const status = r.ok ? 'OK' : 'FAILED';
184
- return r.captureNote ? `${r.name}: ${status} (${r.captureNote})` : `${r.name}: ${status}`;
228
+ const directStatus = r.ok ? 'OK' : 'FAILED';
229
+ const shellStatus = r.shellOk ? 'OK' : 'FAILED';
230
+ const parts = [`${r.name}: ${directStatus}`];
231
+ if (!r.shellOk) parts[0] += `, shell: ${shellStatus}`;
232
+ if (r.captureNote) parts[0] += ` (${r.captureNote})`;
233
+ return parts[0];
185
234
  });
186
235
  const summary = allOk
187
236
  ? `capture runs OK (${summaryParts.join(', ')})`
188
237
  : `capture failed for: ${failedTools.join(', ')}`;
189
- const detail = toolResults.map(r =>
190
- `[${r.name}]\n Ran: ${r.cmd}\n Result: ${r.exitDetail}${r.captureNote ? `\n Capture log: ${r.captureNote}` : ''}`
191
- ).join('\n\n');
238
+ const detail = toolResults.map(r => {
239
+ let text = `[${r.name}]\n Ran: ${r.cmd}\n Result: ${r.exitDetail}`;
240
+ if (r.captureNote) text += `\n Capture log: ${r.captureNote}`;
241
+ text += `\n Shell: ${r.shellCmd} < (test event)\n Shell result: ${r.shellDetail}`;
242
+ if (r.shellCaptureNote) text += `\n Shell capture log: ${r.shellCaptureNote}`;
243
+ return text;
244
+ }).join('\n\n');
192
245
 
193
246
  return { ok: allOk, summary, detail };
194
247
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.21",
3
+ "version": "0.8.23",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {