ai-lens 0.8.100 → 0.8.103
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 +10 -0
- package/cli/hooks.js +32 -18
- package/cli/init.js +12 -4
- package/cli/status.js +70 -19
- package/client/ai-lens-hook.ps1 +28 -17
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
0aee413
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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.103 — 2026-06-18
|
|
6
|
+
- fix: `ai-lens status` no longer falsely reports the client as "outdated" on repo-path installs (where hooks run capture.js straight from a checkout that auto-updates on `git pull`). It now reads the client version from the repo the hooks actually run, instead of an unused leftover copy in `~/.ai-lens/client/` — which could be months stale and made the check show ✗ + "run init" even though the live client was current. The stale copy is now noted as ignored. Copy-mode installs are unchanged.
|
|
7
|
+
|
|
8
|
+
## 0.8.102 — 2026-06-17
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
|
|
12
|
+
## 0.8.101 — 2026-06-17
|
|
13
|
+
- 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
|
|
14
|
+
|
|
5
15
|
## 0.8.100 — 2026-06-17
|
|
6
16
|
- 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
17
|
|
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/cli/status.js
CHANGED
|
@@ -103,10 +103,12 @@ function expandTilde(pathStr) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
106
|
+
* Resolve the capture.js / launcher path(s) the hooks actually run, tagging each
|
|
107
|
+
* with its install mode. Shared by detectInstallMode and checkClientFiles so both
|
|
108
|
+
* reason about the SAME path the hooks execute (not an unrelated copy).
|
|
109
|
+
* Returns [{ tool, raw, resolved, mode: 'copy' | 'repo-path' }].
|
|
108
110
|
*/
|
|
109
|
-
function
|
|
111
|
+
export function resolveHookClientPaths(tools) {
|
|
110
112
|
// Normalise both sides to forward slashes before comparing — captureCommand
|
|
111
113
|
// always emits forward-slash paths into the hook command, but join() on
|
|
112
114
|
// Windows returns backslashes, so a raw startsWith() check would miss every
|
|
@@ -116,24 +118,40 @@ function detectInstallMode(tools) {
|
|
|
116
118
|
for (const tool of tools) {
|
|
117
119
|
const cmd = extractHookCommand(tool);
|
|
118
120
|
if (!cmd) continue;
|
|
119
|
-
// Match either a launcher (run.sh / run.cmd) or a
|
|
120
|
-
// always lives in the install dir, so it's by definition copy-mode.
|
|
121
|
-
const m = cmd.match(/["']([^"']*(?:capture\.js|run\.(?:sh|cmd)))["']|(\S*(?:capture\.js|run\.(?:sh|cmd)))/);
|
|
122
|
-
if (m)
|
|
121
|
+
// Match either a launcher (run.sh / run.cmd / ai-lens-hook.ps1) or a capture.js path.
|
|
122
|
+
// A launcher always lives in the install dir, so it's by definition copy-mode.
|
|
123
|
+
const m = cmd.match(/["']([^"']*(?:capture\.js|run\.(?:sh|cmd)|ai-lens-hook\.ps1))["']|(\S*(?:capture\.js|run\.(?:sh|cmd)|ai-lens-hook\.ps1))/);
|
|
124
|
+
if (!m) continue;
|
|
125
|
+
const raw = m[1] || m[2];
|
|
126
|
+
const resolved = expandTilde(raw).replace(/\\/g, '/');
|
|
127
|
+
paths.push({ tool: tool.name, raw, resolved, mode: resolved.startsWith(copyDir) ? 'copy' : 'repo-path' });
|
|
128
|
+
}
|
|
129
|
+
return paths;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Read the `version` field from a package.json, or null if unreadable. */
|
|
133
|
+
function readPackageVersion(pkgJsonPath) {
|
|
134
|
+
try {
|
|
135
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
136
|
+
return pkg.version || null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
123
139
|
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Detect install mode from the capture.js path in hook commands.
|
|
144
|
+
* Returns { ok, summary, detail } for the status output.
|
|
145
|
+
*/
|
|
146
|
+
function detectInstallMode(tools) {
|
|
147
|
+
const paths = resolveHookClientPaths(tools);
|
|
124
148
|
if (paths.length === 0) {
|
|
125
149
|
return { ok: null, summary: 'unknown', detail: 'No hook commands found — cannot determine install mode' };
|
|
126
150
|
}
|
|
127
|
-
const modes = paths.map(p =>
|
|
128
|
-
if (p.resolved.startsWith(copyDir)) return 'copy';
|
|
129
|
-
return 'repo-path';
|
|
130
|
-
});
|
|
151
|
+
const modes = paths.map(p => p.mode);
|
|
131
152
|
const unique = [...new Set(modes)];
|
|
132
153
|
const mode = unique.length === 1 ? unique[0] : 'mixed';
|
|
133
|
-
const detail = paths.map(p => {
|
|
134
|
-
const m = p.resolved.startsWith(copyDir) ? 'copy' : 'repo-path';
|
|
135
|
-
return ` ${p.tool}: ${m} (${p.raw})`;
|
|
136
|
-
}).join('\n');
|
|
154
|
+
const detail = paths.map(p => ` ${p.tool}: ${p.mode} (${p.raw})`).join('\n');
|
|
137
155
|
|
|
138
156
|
if (mode === 'copy') {
|
|
139
157
|
return { ok: true, summary: 'copy (~/.ai-lens/client/)', detail: `Client files copied to ~/.ai-lens/client/\nUpdate: npx -y ai-lens init --yes\n${detail}` };
|
|
@@ -492,7 +510,37 @@ function checkGitIdentity() {
|
|
|
492
510
|
return { ok: false, summary: 'not configured', detail: 'Git identity not configured (git config user.email)' };
|
|
493
511
|
}
|
|
494
512
|
|
|
495
|
-
function checkClientFiles() {
|
|
513
|
+
export function checkClientFiles(tools = []) {
|
|
514
|
+
// In repo-path mode the hooks run capture.js straight from the repo, so the
|
|
515
|
+
// ~/.ai-lens/client/ copy (if any) is an UNUSED leftover from a prior copy-mode
|
|
516
|
+
// install. Reading that copy's version.json there would falsely flag the client
|
|
517
|
+
// "outdated" while the live client (the repo) is current — so check the version
|
|
518
|
+
// of the path the hooks actually execute.
|
|
519
|
+
const repoHook = resolveHookClientPaths(tools).find(p => p.mode === 'repo-path');
|
|
520
|
+
if (repoHook) {
|
|
521
|
+
// package.json sits one level up from the client/ dir the hook points into.
|
|
522
|
+
const pkgPath = repoHook.resolved.replace(/\/client\/[^/]+$/i, '/package.json');
|
|
523
|
+
const repoVersion = readPackageVersion(pkgPath);
|
|
524
|
+
const copyExists = existsSync(join(homedir(), '.ai-lens', 'client', 'capture.js'));
|
|
525
|
+
let detail = `Hooks run the client from the repo (repo-path mode) — auto-updates on git pull:\n ${repoHook.raw}`;
|
|
526
|
+
detail += repoVersion
|
|
527
|
+
? `\n Repo client version: ${repoVersion}`
|
|
528
|
+
: `\n Repo client version: unknown (no package.json at ${pkgPath})`;
|
|
529
|
+
if (copyExists) {
|
|
530
|
+
detail += `\n Note: ~/.ai-lens/client/ exists but is UNUSED in repo-path mode (stale leftover from an old copy-install; safe to ignore).`;
|
|
531
|
+
}
|
|
532
|
+
// Healthy as long as the hook-source version is readable. Don't compare against
|
|
533
|
+
// the CLI: `ai-lens status` may run from a global npx CLI while the hooks point
|
|
534
|
+
// at a checkout — that mismatch is expected, not an error. A stale repo checkout
|
|
535
|
+
// surfaces via git (and the version line above), not as a false ✗ here.
|
|
536
|
+
return {
|
|
537
|
+
ok: repoVersion != null,
|
|
538
|
+
summary: repoVersion ? `repo-path (v${repoVersion})` : 'repo-path (version unknown)',
|
|
539
|
+
detail,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Copy-mode (or no hooks resolved): the ~/.ai-lens/client/ copy IS the live client.
|
|
496
544
|
const dir = join(homedir(), '.ai-lens', 'client');
|
|
497
545
|
const files = ['capture.js', 'sender.js', 'config.js'];
|
|
498
546
|
const results = files.map(f => ({ name: f, exists: existsSync(join(dir, f)) }));
|
|
@@ -1427,16 +1475,19 @@ export default async function status({ report = false } = {}) {
|
|
|
1427
1475
|
// 3. Git identity
|
|
1428
1476
|
printLine('Git identity', checkGitIdentity());
|
|
1429
1477
|
|
|
1478
|
+
// Resolve the tools (incl. project hooks) up front — checkClientFiles needs them
|
|
1479
|
+
// to read the version of the path the hooks actually run (repo-path vs copy).
|
|
1480
|
+
const installedTools = detectInstalledTools();
|
|
1481
|
+
const toolsWithProject = getToolsForCaptureTest();
|
|
1482
|
+
|
|
1430
1483
|
// 4. Client files
|
|
1431
|
-
printLine('Client files', checkClientFiles());
|
|
1484
|
+
printLine('Client files', checkClientFiles(toolsWithProject));
|
|
1432
1485
|
|
|
1433
1486
|
// 5. Config
|
|
1434
1487
|
const configResult = checkConfig();
|
|
1435
1488
|
printLine('Config', configResult);
|
|
1436
1489
|
|
|
1437
1490
|
// 6. Hooks: global + project (Cursor then Claude Code; within each: global then project)
|
|
1438
|
-
const installedTools = detectInstalledTools();
|
|
1439
|
-
const toolsWithProject = getToolsForCaptureTest();
|
|
1440
1491
|
const isGlobalTool = (tool) => TOOL_CONFIGS.includes(tool);
|
|
1441
1492
|
const toolLabel = (tool) => (isGlobalTool(tool) ? `${tool.name} (global)` : tool.name);
|
|
1442
1493
|
const hooksOrder = (a, b) => {
|
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
|
+
}
|