ai-lens 0.8.21 → 0.8.22

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
+ 23120cb
package/cli/hooks.js CHANGED
@@ -106,9 +106,15 @@ export function captureCommand() {
106
106
  const capturePath = CAPTURE_PATH.replace(/\\/g, '/');
107
107
  const escaped = shellEscape(capturePath);
108
108
  // /usr/bin/env doesn't need shell-escaping, but named paths do
109
- return nodePath === '/usr/bin/env node'
109
+ const base = nodePath === '/usr/bin/env node'
110
110
  ? `/usr/bin/env node ${escaped}`
111
111
  : `${shellEscape(nodePath.replace(/\\/g, '/'))} ${escaped}`;
112
+ // On Windows, prefix with "& " so the command works in both PowerShell and cmd.exe.
113
+ // PowerShell treats a quoted path like "node.exe" as a string expression, not a command;
114
+ // the & (call) operator is required. In cmd.exe, & is a command separator — the empty
115
+ // first part is a no-op, and the real command runs as the second part.
116
+ if (process.platform === 'win32') return `& ${base}`;
117
+ return base;
112
118
  }
113
119
 
114
120
  // ---------------------------------------------------------------------------
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.22",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {