ai-lens 0.8.22 → 0.8.24

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
- 23120cb
1
+ 39d3d60
package/cli/hooks.js CHANGED
@@ -106,15 +106,17 @@ 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
- const base = nodePath === '/usr/bin/env node'
109
+ return 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
+ }
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;
118
120
  }
119
121
 
120
122
  // ---------------------------------------------------------------------------
@@ -169,20 +171,20 @@ const CLAUDE_CODE_HOOKS = {
169
171
  };
170
172
 
171
173
  const CURSOR_HOOKS = {
172
- sessionStart: () => ({ command: captureCommand() }),
173
- beforeSubmitPrompt: () => ({ command: captureCommand() }),
174
- postToolUse: () => ({ command: captureCommand() }),
175
- postToolUseFailure: () => ({ command: captureCommand() }),
176
- afterFileEdit: () => ({ command: captureCommand() }),
177
- afterShellExecution: () => ({ command: captureCommand() }),
178
- afterMCPExecution: () => ({ command: captureCommand() }),
179
- subagentStart: () => ({ command: captureCommand() }),
180
- subagentStop: () => ({ command: captureCommand() }),
181
- preCompact: () => ({ command: captureCommand() }),
182
- afterAgentResponse: () => ({ command: captureCommand() }),
183
- afterAgentThought: () => ({ command: captureCommand() }),
184
- stop: () => ({ command: captureCommand() }),
185
- 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() }),
186
188
  };
187
189
 
188
190
  export const TOOL_CONFIGS = [
@@ -224,7 +226,11 @@ export function isAiLensHook(entry) {
224
226
  }
225
227
 
226
228
  function isCurrentAiLensHook(entry, expected) {
227
- 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
228
234
  // Normalize separators so hooks installed before path-normalization fix still match
229
235
  const norm = s => (s || '').replace(/\\/g, '/');
230
236
  // Flat format (Cursor)
package/cli/init.js CHANGED
@@ -326,7 +326,6 @@ export default async function init() {
326
326
  // Banner
327
327
  heading(`AI Lens — Init v${version} (${commit})`);
328
328
  detail(`capture.js: ${CAPTURE_PATH}`);
329
- blank();
330
329
 
331
330
  // Detect installed tools
332
331
  heading('Detecting installed AI tools...');
@@ -342,7 +341,6 @@ export default async function init() {
342
341
  for (const tool of tools) {
343
342
  success(` Found: ${tool.name} (${tool.dirPath})`);
344
343
  }
345
- blank();
346
344
 
347
345
  // Configuration
348
346
  heading('Configuration');
@@ -408,7 +406,6 @@ export default async function init() {
408
406
 
409
407
  // Build new config in memory — saved after "Proceed?" confirmation
410
408
  const newConfig = { ...currentConfig, serverUrl, projects };
411
- blank();
412
409
 
413
410
  // Install client files to ~/.ai-lens/client/
414
411
  heading('Installing client files...');
@@ -420,8 +417,6 @@ export default async function init() {
420
417
  return;
421
418
  }
422
419
 
423
- blank();
424
-
425
420
  // Authentication
426
421
  heading('Authentication');
427
422
  if (currentConfig.authToken) {
@@ -476,7 +471,6 @@ export default async function init() {
476
471
  info(' Or re-run init when Auth0 is configured on the server.');
477
472
  }
478
473
  }
479
- blank();
480
474
 
481
475
  // Analyze each tool
482
476
  heading('Analyzing hook configurations...');
@@ -529,7 +523,6 @@ export default async function init() {
529
523
  const plan = describePlan(tool, analysis);
530
524
  if (!plan) continue;
531
525
  info(` ${plan.description}`);
532
- detail(`hooks: ${plan.hooks.join(', ')}`);
533
526
  }
534
527
  blank();
535
528
 
@@ -541,7 +534,6 @@ export default async function init() {
541
534
  return;
542
535
  }
543
536
  }
544
- blank();
545
537
 
546
538
  // Apply
547
539
  heading('Applying changes...');
@@ -567,7 +559,6 @@ export default async function init() {
567
559
  // Persist config only after hooks are written — avoids inconsistency where
568
560
  // config has new serverUrl/projects but hooks still use old commands.
569
561
  saveLensConfig(newConfig);
570
- blank();
571
562
 
572
563
  // Verify hooks were written correctly
573
564
  heading('Verifying hooks...');
@@ -596,7 +587,6 @@ export default async function init() {
596
587
  }
597
588
  }
598
589
  }
599
- blank();
600
590
 
601
591
  // Summary
602
592
  heading('Summary');
@@ -610,7 +600,6 @@ export default async function init() {
610
600
  if (results.some(r => !r.ok) || verifyFailed) {
611
601
  process.exitCode = 1;
612
602
  }
613
- blank();
614
603
  }
615
604
 
616
605
  // Enable hooks if globally disabled (e.g. disableAllHooks in settings.local.json)
@@ -728,7 +717,6 @@ export default async function init() {
728
717
  } else {
729
718
  info(' Skipped');
730
719
  }
731
- blank();
732
720
  }
733
721
 
734
722
  // Cursor MCP
@@ -754,7 +742,6 @@ export default async function init() {
754
742
  } else {
755
743
  info(' Skipped');
756
744
  }
757
- blank();
758
745
  }
759
746
 
760
747
  // Quick verification
@@ -828,7 +815,7 @@ export default async function init() {
828
815
  'X-Auth-Token': finalConfig.authToken,
829
816
  };
830
817
  if (gitEmail) headers['X-Developer-Git-Email'] = gitEmail;
831
- if (gitName) headers['X-Developer-Name'] = gitName;
818
+ if (gitName) headers['X-Developer-Name'] = encodeURIComponent(gitName);
832
819
 
833
820
  const parsed = new URL(`${verifyUrl}/api/events`);
834
821
  const isHttps = parsed.protocol === 'https:';
package/cli/status.js CHANGED
@@ -188,14 +188,21 @@ function checkCaptureRun(installedTools) {
188
188
  let shellOk = false;
189
189
  let shellDetail = '';
190
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
- });
191
+ // On Windows, Cursor runs hooks via PowerShell, but shell:true uses cmd.exe.
192
+ // Use powershell.exe explicitly so the `& "..."` call-operator syntax works.
193
+ const isWin = process.platform === 'win32';
194
+ const shellResult = spawnSync(
195
+ isWin ? 'powershell.exe' : command,
196
+ isWin ? ['-NoProfile', '-Command', command] : [],
197
+ {
198
+ input: shellEvent,
199
+ shell: !isWin,
200
+ encoding: 'utf-8',
201
+ timeout: 10_000,
202
+ env: { ...process.env, AI_LENS_PROJECTS: join(homedir(), '.ai-lens-status-check-nonexistent') },
203
+ windowsHide: true,
204
+ },
205
+ );
199
206
  if (shellResult.error) throw shellResult.error;
200
207
  shellOk = shellResult.status === 0;
201
208
  shellDetail = shellOk ? 'exit 0' : `Exit code: ${shellResult.status}\nError: ${(shellResult.stderr || '').trim() || '(no stderr)'}`;
@@ -692,7 +699,7 @@ async function checkE2e(serverUrl, authToken) {
692
699
  'Content-Type': 'application/json',
693
700
  'X-Auth-Token': authToken,
694
701
  ...(email && { 'X-Developer-Git-Email': email }),
695
- ...(name && { 'X-Developer-Name': name }),
702
+ ...(name && { 'X-Developer-Name': encodeURIComponent(name) }),
696
703
  },
697
704
  body: JSON.stringify(testEvent),
698
705
  signal: AbortSignal.timeout(8000),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.22",
3
+ "version": "0.8.24",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {