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 +1 -1
- package/cli/hooks.js +27 -15
- package/cli/status.js +63 -10
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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:
|
|
167
|
-
beforeSubmitPrompt: () => ({ command:
|
|
168
|
-
postToolUse: () => ({ command:
|
|
169
|
-
postToolUseFailure: () => ({ command:
|
|
170
|
-
afterFileEdit: () => ({ command:
|
|
171
|
-
afterShellExecution: () => ({ command:
|
|
172
|
-
afterMCPExecution: () => ({ command:
|
|
173
|
-
subagentStart: () => ({ command:
|
|
174
|
-
subagentStop: () => ({ command:
|
|
175
|
-
preCompact: () => ({ command:
|
|
176
|
-
afterAgentResponse: () => ({ command:
|
|
177
|
-
afterAgentThought: () => ({ command:
|
|
178
|
-
stop: () => ({ command:
|
|
179
|
-
sessionEnd: () => ({ command:
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
|
184
|
-
|
|
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}
|
|
191
|
-
|
|
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
|
}
|