ai-lens 0.8.100 → 0.8.102
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/CHANGELOG.md +7 -0
- package/cli/hooks.js +32 -18
- package/cli/init.js +12 -4
- package/client/ai-lens-hook.ps1 +28 -17
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3747fae
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
History of changes to the `ai-lens` CLI package on npm. New entries go on top. Format: `## X.Y.Z — YYYY-MM-DD`, followed by user-facing bullets.
|
|
4
4
|
|
|
5
|
+
## 0.8.102 — 2026-06-17
|
|
6
|
+
- fix: the per-machine `settings.local.json` overlay for committed Claude Code hooks now also writes the absolute launcher form on Windows (matching the main install path), so a repo whose tracked `settings.json` carries `$CLAUDE_PROJECT_DIR` hooks captures correctly on Windows without manual fix-up. `ai-lens status`/`init` now flag any leftover `$CLAUDE_PROJECT_DIR`/`%CLAUDE_PROJECT_DIR%` Claude hook on Windows as outdated so it migrates to the absolute form.
|
|
7
|
+
- fix: the Windows windowless hook launcher (`ai-lens-hook.ps1`) now sends the event payload to node as raw UTF-8 bytes (Cyrillic prompts and accented paths are no longer mangled by PowerShell's default codepage) and fails open — on any internal error it exits cleanly and writes nothing to stdout, so a launcher hiccup can never break a Cursor hook or disrupt the session.
|
|
8
|
+
|
|
9
|
+
## 0.8.101 — 2026-06-17
|
|
10
|
+
- fix: Claude Code hooks on Windows now use an absolute path to capture.js instead of `$CLAUDE_PROJECT_DIR`, which Claude Code does not expand on Windows (the v0.8.100 form failed to find the launcher and, when Cursor also ran the hook, produced a visible error). Capture is restored. Existing installs migrate on the next `ai-lens init` / `/setup`. macOS/Linux keep `$CLAUDE_PROJECT_DIR` (expanded there). Note: a brief console window can still flash on Claude Code hooks on Windows — that's an upstream Claude Code behaviour (it launches the hook shell without hiding the window) and can't be fixed from the hook side
|
|
11
|
+
|
|
5
12
|
## 0.8.100 — 2026-06-17
|
|
6
13
|
- fix: Claude Code hooks on Windows now actually capture. The previous windowless wrapper (`conhost.exe --headless`) hid the console window but silently swallowed the hook's stdin, so the event payload never reached AI Lens and nothing was recorded. Claude Code now uses the same windowless launcher as Cursor (`ai-lens-hook.ps1`), which both suppresses the window and delivers the payload. Existing installs migrate on the next `ai-lens init` / `/setup`. macOS/Linux unchanged. (The launcher was renamed from `cursor-ai-lens-hook.ps1` since it now serves both tools.)
|
|
7
14
|
|
package/cli/hooks.js
CHANGED
|
@@ -316,16 +316,13 @@ export function captureCommand(opts = {}) {
|
|
|
316
316
|
// binary or /home/<user> path. Windows Claude Code shells via cmd.exe (%VAR%);
|
|
317
317
|
// POSIX shells use $VAR.
|
|
318
318
|
if (projectDirRelPath != null) {
|
|
319
|
+
// POSIX/macOS only: bare node + $CLAUDE_PROJECT_DIR (the shell expands $VAR; the
|
|
320
|
+
// path stays machine-agnostic for committed hooks + cwd-robust). On WINDOWS this
|
|
321
|
+
// form must NOT be used — Claude Code's hook spawn (Git Bash / PowerShell) does NOT
|
|
322
|
+
// expand $CLAUDE_PROJECT_DIR nor %CLAUDE_PROJECT_DIR% (verified live: both reached
|
|
323
|
+
// the shell literally and the path failed). Callers therefore route Windows Claude
|
|
324
|
+
// hooks to the absolute getClaudeCodeHookDefsWithPath form instead of this one.
|
|
319
325
|
const rel = projectDirRelPath.replace(/\\/g, '/').replace(/^\.?\/+/, '');
|
|
320
|
-
if (winLauncher) {
|
|
321
|
-
// Windowless form via the launcher. Uses $CLAUDE_PROJECT_DIR — Claude Code
|
|
322
|
-
// substitutes that variable itself before exec on every OS. The launcher ships
|
|
323
|
-
// next to capture.js, so it resolves under the same $CLAUDE_PROJECT_DIR path.
|
|
324
|
-
// node stays bare (committed/machine-agnostic form) — the launcher's
|
|
325
|
-
// ProcessStartInfo resolves it via PATH (Claude Code requires node on PATH).
|
|
326
|
-
const launcherRel = rel.replace(/[^/]*$/, 'ai-lens-hook.ps1');
|
|
327
|
-
return `powershell -NoProfile -ExecutionPolicy Bypass -File "$CLAUDE_PROJECT_DIR/${launcherRel}" node "$CLAUDE_PROJECT_DIR/${rel}"`;
|
|
328
|
-
}
|
|
329
326
|
const dir = isWin ? '%CLAUDE_PROJECT_DIR%' : '$CLAUDE_PROJECT_DIR';
|
|
330
327
|
return `node "${dir}/${rel}"`;
|
|
331
328
|
}
|
|
@@ -926,14 +923,17 @@ function isAcceptableHookCommand(cmd) {
|
|
|
926
923
|
export function isWrongPlatformProjectDirCommand(cmd, platform = process.platform) {
|
|
927
924
|
if (!isClaudeProjectDirCommand(cmd)) return false;
|
|
928
925
|
const n = (cmd || '').replace(/\\/g, '/');
|
|
929
|
-
// The
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
926
|
+
// The legacy conhost.exe --headless form is unambiguously a Windows command and uses
|
|
927
|
+
// $CLAUDE_PROJECT_DIR: correct on win32, wrong elsewhere. (Migration off conhost is
|
|
928
|
+
// driven separately by the wrapper-kind check in isCurrentAiLensHook.)
|
|
929
|
+
if (isConhostHeadlessCommand(n)) return platform !== 'win32';
|
|
930
|
+
// On Windows, NEITHER $CLAUDE_PROJECT_DIR nor %CLAUDE_PROJECT_DIR% actually expands in
|
|
931
|
+
// Claude Code's hook spawn (verified live) — so ANY variable-bearing form is wrong on
|
|
932
|
+
// win32 and must migrate to the absolute path form (incl. a stale $VAR-launcher or a
|
|
933
|
+
// %VAR% overlay). On POSIX the shell expands $CLAUDE_PROJECT_DIR, so $VAR is the only
|
|
934
|
+
// correct form there.
|
|
935
|
+
if (platform === 'win32') return true;
|
|
936
|
+
return !n.includes('$CLAUDE_PROJECT_DIR');
|
|
937
937
|
}
|
|
938
938
|
|
|
939
939
|
// Leading `conhost.exe --headless ` windowless wrapper, with the trailing space so a
|
|
@@ -1036,13 +1036,27 @@ export function analyzeClaudeLocalOverlay(tool, opts = {}) {
|
|
|
1036
1036
|
if (!rel) return { applicable: false, reason: 'could not extract capture.js path from hook command' };
|
|
1037
1037
|
|
|
1038
1038
|
const localPath = claudeLocalSettingsPath(tool);
|
|
1039
|
+
// Committed-file → per-machine overlay mirror. On Windows $CLAUDE_PROJECT_DIR/%VAR% does
|
|
1040
|
+
// NOT expand in Claude Code's hook spawn, so the overlay (like init's main path) writes
|
|
1041
|
+
// the absolute windowless-launcher form — rel resolved against the project root, node
|
|
1042
|
+
// resolved on this machine. POSIX keeps the $CLAUDE_PROJECT_DIR form (the shell expands it).
|
|
1043
|
+
const overlayCtx = { ...(opts.ctx ?? {}), platform };
|
|
1044
|
+
let overlayHookDefs;
|
|
1045
|
+
if (platform === 'win32') {
|
|
1046
|
+
const projectRoot = dirname(dirname(tool.configPath));
|
|
1047
|
+
const absCapture = join(projectRoot, rel).replace(/\\/g, '/');
|
|
1048
|
+
const nodeResolution = overlayCtx.nodeResolution ?? findStableNodePath({ platform });
|
|
1049
|
+
overlayHookDefs = getClaudeCodeHookDefsWithPath(absCapture, { ...overlayCtx, nodeResolution });
|
|
1050
|
+
} else {
|
|
1051
|
+
overlayHookDefs = getClaudeCodeHookDefsWithProjectDir(rel, overlayCtx);
|
|
1052
|
+
}
|
|
1039
1053
|
const localTool = {
|
|
1040
1054
|
...tool,
|
|
1041
1055
|
name: `${tool.name} (settings.local.json overlay)`,
|
|
1042
1056
|
configPath: localPath,
|
|
1043
1057
|
// Never backup/rename a user's settings.local.json wholesale on parse errors.
|
|
1044
1058
|
sharedConfig: true,
|
|
1045
|
-
hookDefs:
|
|
1059
|
+
hookDefs: overlayHookDefs,
|
|
1046
1060
|
};
|
|
1047
1061
|
const overlayAnalysis = analyzeToolHooks(localTool, { platform, allowPlatformRewrite: true });
|
|
1048
1062
|
return {
|
package/cli/init.js
CHANGED
|
@@ -182,9 +182,13 @@ function makeNestedClaudeTool(projectDir, capturePathInHooks, ctx = null) {
|
|
|
182
182
|
// cwd. Absolute/tilde paths keep the legacy <node> <capture.js> form.
|
|
183
183
|
const rel = capturePathInHooks.replace(/\\/g, '/');
|
|
184
184
|
const isRelative = !rel.startsWith('/') && !rel.startsWith('~') && !/^[a-zA-Z]:\//.test(rel);
|
|
185
|
-
|
|
185
|
+
// $CLAUDE_PROJECT_DIR is expanded by the shell on POSIX but NOT on Windows (Claude's
|
|
186
|
+
// hook spawn takes it literally) — so on Windows resolve rel to an absolute path
|
|
187
|
+
// against the nested project root instead of using the projectDir form.
|
|
188
|
+
const useProjectDir = isRelative && /(?:^|\/)ai-lens\//.test(rel) && ctx?.platform !== 'win32';
|
|
189
|
+
tool.hookDefs = useProjectDir
|
|
186
190
|
? getClaudeCodeHookDefsWithProjectDir(rel, ctx)
|
|
187
|
-
: getClaudeCodeHookDefsWithPath(capturePathInHooks, ctx);
|
|
191
|
+
: getClaudeCodeHookDefsWithPath(isRelative ? join(projectDir, rel).replace(/\\/g, '/') : capturePathInHooks, ctx);
|
|
188
192
|
}
|
|
189
193
|
return tool;
|
|
190
194
|
}
|
|
@@ -513,9 +517,13 @@ export default async function init() {
|
|
|
513
517
|
: repoPathAbs.replace(/\\/g, '/');
|
|
514
518
|
for (const tool of tools) {
|
|
515
519
|
if (tool.name.startsWith('Claude Code')) {
|
|
516
|
-
|
|
520
|
+
// On Windows, Claude Code's hook spawn does NOT expand $CLAUDE_PROJECT_DIR (Git
|
|
521
|
+
// Bash / PowerShell take it literally), so the projectDir form can't resolve —
|
|
522
|
+
// use the resolved absolute repo path instead (per-machine, so absolute is fine).
|
|
523
|
+
// POSIX/macOS keep $CLAUDE_PROJECT_DIR (cwd-robust + the shell expands it).
|
|
524
|
+
tool.hookDefs = (claudeProjectDirRel && ctx?.platform !== 'win32')
|
|
517
525
|
? getClaudeCodeHookDefsWithProjectDir(claudeProjectDirRel, ctx)
|
|
518
|
-
: getClaudeCodeHookDefsWithPath(pathInHooks, ctx);
|
|
526
|
+
: getClaudeCodeHookDefsWithPath(claudeProjectDirRel ? repoPathAbs.replace(/\\/g, '/') : pathInHooks, ctx);
|
|
519
527
|
}
|
|
520
528
|
else if (tool.name.startsWith('Cursor')) tool.hookDefs = getCursorHookDefsWithPath(pathInHooks, ctx);
|
|
521
529
|
else if (tool.name.startsWith('Codex')) tool.hookDefs = getCodexHookDefsWithPath(codexPathInHooks, ctx);
|
package/client/ai-lens-hook.ps1
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
# never reaches capture.js (it reads empty stdin and drops the event).
|
|
9
9
|
# This launcher instead starts node via ProcessStartInfo with CreateNoWindow=$true (no
|
|
10
10
|
# window) and RedirectStandardInput (real stdin forwarded) — so capture works AND there's
|
|
11
|
-
# no flash
|
|
11
|
+
# no flash. It is fail-open: on ANY error it exits 0 and writes NOTHING to stdout (Cursor
|
|
12
|
+
# parses a hook's stdout as JSON — leaking an error there would break the hook).
|
|
12
13
|
#
|
|
13
14
|
# Payload source differs by caller, so read BOTH:
|
|
14
15
|
# - Cursor pipes it through the PowerShell pipeline → $input.
|
|
@@ -18,22 +19,32 @@
|
|
|
18
19
|
# Args: $args[0] = node path, $args[1] = capture.js path.
|
|
19
20
|
|
|
20
21
|
$ErrorActionPreference = 'Stop'
|
|
21
|
-
|
|
22
|
-
$
|
|
22
|
+
try {
|
|
23
|
+
$node = $args[0]
|
|
24
|
+
$capture = $args[1]
|
|
23
25
|
|
|
24
|
-
$payload = @($input) -join "`n"
|
|
25
|
-
if ([string]::IsNullOrEmpty($payload)) {
|
|
26
|
-
|
|
27
|
-
}
|
|
26
|
+
$payload = @($input) -join "`n"
|
|
27
|
+
if ([string]::IsNullOrEmpty($payload)) {
|
|
28
|
+
$payload = [Console]::In.ReadToEnd()
|
|
29
|
+
}
|
|
28
30
|
|
|
29
|
-
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
30
|
-
$psi.FileName = $node
|
|
31
|
-
$psi.Arguments = '"' + $capture + '"'
|
|
32
|
-
$psi.UseShellExecute = $false
|
|
33
|
-
$psi.CreateNoWindow = $true
|
|
34
|
-
$psi.RedirectStandardInput = $true
|
|
31
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
32
|
+
$psi.FileName = $node
|
|
33
|
+
$psi.Arguments = '"' + $capture + '"'
|
|
34
|
+
$psi.UseShellExecute = $false
|
|
35
|
+
$psi.CreateNoWindow = $true
|
|
36
|
+
$psi.RedirectStandardInput = $true
|
|
35
37
|
|
|
36
|
-
$proc = [System.Diagnostics.Process]::Start($psi)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
$proc = [System.Diagnostics.Process]::Start($psi)
|
|
39
|
+
# Write the payload as raw UTF-8 bytes to node's stdin (capture.js reads UTF-8). Going
|
|
40
|
+
# through BaseStream avoids the StreamWriter's default codepage on Windows PowerShell
|
|
41
|
+
# 5.1 mangling non-ASCII content (e.g. Cyrillic prompts, accented paths).
|
|
42
|
+
$bytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
|
|
43
|
+
$proc.StandardInput.BaseStream.Write($bytes, 0, $bytes.Length)
|
|
44
|
+
$proc.StandardInput.BaseStream.Flush()
|
|
45
|
+
$proc.StandardInput.Close()
|
|
46
|
+
$proc.WaitForExit()
|
|
47
|
+
} catch {
|
|
48
|
+
# Fail-open: never disrupt the session, never emit non-JSON to the hook's stdout.
|
|
49
|
+
exit 0
|
|
50
|
+
}
|