ai-lens 0.8.105 → 0.8.107
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 +5 -1
- package/client/ai-lens-hook.ps1 +19 -23
- package/client/capture.js +52 -3
- package/client/run-hidden.vbs +24 -0
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
af32330
|
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.107 — 2026-06-19
|
|
6
|
+
- fix: Cyrillic / non-ASCII in Windows hook payloads is no longer corrupted (it was being dropped as malformed JSON). The windowless launcher now reads the payload as raw bytes straight off the process's stdin and never references PowerShell's `$input` — merely touching `$input` both pre-drains stdin and decodes it through the console's OEM codepage, mangling the UTF-8 BOM and every Cyrillic byte before forwarding. Byte-exact passthrough now; the 0.8.105 read still went through `$input` first. Cursor and Claude Code.
|
|
7
|
+
- fix: the Windows hidden-sender launch now runs detached, so it isn't killed together with the hook's process tree before it finishes starting the sender — closing a rare window where an event could be dropped. The launcher is GUI-subsystem, so detached still adds no console window (no flash).
|
|
8
|
+
|
|
9
|
+
## 0.8.106 — 2026-06-19
|
|
10
|
+
- fix: no more console-window flash on every hook event on Windows (Cursor and Claude Code). The flash was the background sender that `capture.js` launches after each event — a detached console spawn pops a window even when hidden. It's now started through a tiny GUI-subsystem launcher (`run-hidden.vbs`), which has no console at all, so capture stays invisible and still ships reliably. If a corporate policy/AV blocks the Windows Script Host, it automatically falls back to the previous behaviour so events never stop shipping. macOS/Linux unchanged.
|
|
11
|
+
|
|
5
12
|
## 0.8.105 — 2026-06-18
|
|
6
13
|
- fix: Cyrillic (and other non-ASCII) in Claude Code hook payloads is no longer mangled on Windows. The windowless hook launcher read stdin through PowerShell's console codepage (OEM, e.g. CP866 on Russian Windows), corrupting prompts/paths on the way in; it now reads the raw stdin bytes and decodes UTF-8 directly. The earlier 0.8.102 fix only covered the write side, so machines that adopted the windowless launcher saw the corruption reappear.
|
|
7
14
|
|
package/cli/hooks.js
CHANGED
|
@@ -420,7 +420,11 @@ export function listClientFiles(sourceDir = join(__dirname, '..', 'client')) {
|
|
|
420
420
|
// .js — all client modules (sibling imports must all be present).
|
|
421
421
|
// .ps1 — the Windows windowless hook launcher (ai-lens-hook.ps1), used by Cursor AND
|
|
422
422
|
// Claude Code; a missing launcher would break every Windows hook, so ship it too.
|
|
423
|
-
|
|
423
|
+
// .vbs — run-hidden.vbs, the Windows hidden+detached sender launcher capture.js uses
|
|
424
|
+
// to start the sender without a console flash; missing it would reintroduce the flash.
|
|
425
|
+
return readdirSync(sourceDir)
|
|
426
|
+
.filter(f => f.endsWith('.js') || f.endsWith('.ps1') || f.endsWith('.vbs'))
|
|
427
|
+
.sort();
|
|
424
428
|
}
|
|
425
429
|
|
|
426
430
|
/**
|
package/client/ai-lens-hook.ps1
CHANGED
|
@@ -11,18 +11,14 @@
|
|
|
11
11
|
# no flash. It is fail-open: on ANY error it exits 0 and writes NOTHING to stdout (Cursor
|
|
12
12
|
# parses a hook's stdout as JSON — leaking an error there would break the hook).
|
|
13
13
|
#
|
|
14
|
-
# Payload
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# payload is mangled at READ time, before we ever re-encode to UTF-8 on the way
|
|
23
|
-
# to node. Reading OpenStandardInput() as bytes and decoding UTF-8 ourselves
|
|
24
|
-
# bypasses the console codepage entirely. (The 0.8.102 fix only covered the WRITE
|
|
25
|
-
# side; the read side still mangled — observed live as mojibake'd prompts.)
|
|
14
|
+
# Payload: read it as RAW BYTES straight off the process's stdin handle and forward
|
|
15
|
+
# them to node unchanged. Do NOT read it as a string ($input / [Console]::In):
|
|
16
|
+
# Windows PowerShell 5.1 decodes stdin with the console's OEM codepage (e.g. cp866 on
|
|
17
|
+
# RU Windows), which corrupts the UTF-8 BOM and every non-ASCII byte (Cyrillic prompts,
|
|
18
|
+
# accented paths) BEFORE we can forward it — capture.js then sees mangled bytes (the BOM
|
|
19
|
+
# arrives as "´╗┐", Cyrillic as garbage) and drops the event as malformed JSON. Reading
|
|
20
|
+
# OpenStandardInput is byte-exact regardless of codepage. (Both Cursor and Claude Code
|
|
21
|
+
# pipe the payload to this process's OS stdin, so one raw read covers both.)
|
|
26
22
|
#
|
|
27
23
|
# Args: $args[0] = node path, $args[1] = capture.js path.
|
|
28
24
|
|
|
@@ -31,13 +27,15 @@ try {
|
|
|
31
27
|
$node = $args[0]
|
|
32
28
|
$capture = $args[1]
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
# Read ONLY via OpenStandardInput — and never reference the automatic $input
|
|
31
|
+
# variable anywhere in this script: merely mentioning $input makes PowerShell
|
|
32
|
+
# pre-drain the pipeline's stdin into it, which starves this byte-exact read (it
|
|
33
|
+
# then gets 0 bytes and the payload is lost). Verified live: with no $input
|
|
34
|
+
# reference, this reads the caller's UTF-8 stdin byte-for-byte (BOM + Cyrillic
|
|
35
|
+
# intact); with one, the bytes arrive OEM-mangled.
|
|
36
|
+
$ms = New-Object System.IO.MemoryStream
|
|
37
|
+
[Console]::OpenStandardInput().CopyTo($ms)
|
|
38
|
+
$bytes = $ms.ToArray()
|
|
41
39
|
|
|
42
40
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
43
41
|
$psi.FileName = $node
|
|
@@ -47,10 +45,8 @@ try {
|
|
|
47
45
|
$psi.RedirectStandardInput = $true
|
|
48
46
|
|
|
49
47
|
$proc = [System.Diagnostics.Process]::Start($psi)
|
|
50
|
-
# Write the
|
|
51
|
-
#
|
|
52
|
-
# 5.1 mangling non-ASCII content (e.g. Cyrillic prompts, accented paths).
|
|
53
|
-
$bytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
|
|
48
|
+
# Write the raw payload bytes straight to node's stdin via BaseStream — no re-encoding,
|
|
49
|
+
# so the UTF-8 the caller sent reaches capture.js byte-for-byte.
|
|
54
50
|
$proc.StandardInput.BaseStream.Write($bytes, 0, $bytes.Length)
|
|
55
51
|
$proc.StandardInput.BaseStream.Flush()
|
|
56
52
|
$proc.StandardInput.Close()
|
package/client/capture.js
CHANGED
|
@@ -1317,9 +1317,37 @@ export function shouldSpawnSender(lockPath = join(SENDING_DIR, '.sender.lock'),
|
|
|
1317
1317
|
return true;
|
|
1318
1318
|
}
|
|
1319
1319
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1320
|
+
// Decide HOW to launch the background sender — pure, so it's unit-testable.
|
|
1321
|
+
// - Windows + run-hidden.vbs present → wscript.exe run-hidden.vbs <node> <sender>.
|
|
1322
|
+
// wscript is GUI-subsystem (no console) and its Shell.Run(...,0,False) starts the
|
|
1323
|
+
// sender hidden AND independent of the hook's process tree → windowless (no flash)
|
|
1324
|
+
// AND surviving (still ships). `fallback` lets the caller retry detached if wscript
|
|
1325
|
+
// is blocked (corporate AV/policy), trading the flash for not losing data.
|
|
1326
|
+
// - Everywhere else (and the Windows no-vbs fallback) → a direct detached node spawn.
|
|
1327
|
+
// `detached` is load-bearing: it breaks the sender out of the hook's process tree so
|
|
1328
|
+
// it survives the teardown — but on Windows it pops a console window (the flash).
|
|
1329
|
+
export function senderSpawnPlan({ platform, execPath, senderPath, vbsPath, vbsExists }) {
|
|
1330
|
+
if (platform === 'win32' && vbsExists) {
|
|
1331
|
+
return {
|
|
1332
|
+
command: 'wscript.exe',
|
|
1333
|
+
args: [vbsPath, execPath, senderPath],
|
|
1334
|
+
// detached: break wscript out of the hook's process group so it isn't killed
|
|
1335
|
+
// when the hook's process tree is torn down before Shell.Run finishes launching
|
|
1336
|
+
// the sender (which would silently drop the event). wscript is GUI-subsystem, so
|
|
1337
|
+
// detached adds no console window — still no flash.
|
|
1338
|
+
options: { detached: true, windowsHide: true, stdio: 'ignore' },
|
|
1339
|
+
fallback: true,
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
return {
|
|
1343
|
+
command: execPath,
|
|
1344
|
+
args: [senderPath],
|
|
1345
|
+
options: { detached: true, stdio: 'ignore', windowsHide: true },
|
|
1346
|
+
fallback: false,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function spawnSenderDetached(senderPath) {
|
|
1323
1351
|
const child = spawn(process.execPath, [senderPath], {
|
|
1324
1352
|
detached: true,
|
|
1325
1353
|
stdio: 'ignore',
|
|
@@ -1329,6 +1357,27 @@ export function trySpawnSender() {
|
|
|
1329
1357
|
child.unref();
|
|
1330
1358
|
}
|
|
1331
1359
|
|
|
1360
|
+
export function trySpawnSender() {
|
|
1361
|
+
if (!shouldSpawnSender()) return;
|
|
1362
|
+
const senderPath = join(__dirname, 'sender.js');
|
|
1363
|
+
const vbsPath = join(__dirname, 'run-hidden.vbs');
|
|
1364
|
+
const plan = senderSpawnPlan({
|
|
1365
|
+
platform: process.platform,
|
|
1366
|
+
execPath: process.execPath,
|
|
1367
|
+
senderPath,
|
|
1368
|
+
vbsPath,
|
|
1369
|
+
vbsExists: existsSync(vbsPath),
|
|
1370
|
+
});
|
|
1371
|
+
try {
|
|
1372
|
+
const child = spawn(plan.command, plan.args, plan.options);
|
|
1373
|
+
// wscript blocked (AV/policy) → fall back to the detached spawn so we keep shipping.
|
|
1374
|
+
child.on('error', () => { if (plan.fallback) spawnSenderDetached(senderPath); });
|
|
1375
|
+
child.unref();
|
|
1376
|
+
} catch {
|
|
1377
|
+
if (plan.fallback) spawnSenderDetached(senderPath);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1332
1381
|
|
|
1333
1382
|
// =============================================================================
|
|
1334
1383
|
// Main
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
' AI Lens — hidden + detached process launcher (Windows only).
|
|
2
|
+
'
|
|
3
|
+
' Why this exists: capture.js must start the background sender after every event,
|
|
4
|
+
' but Node's spawn can't do both things at once on Windows:
|
|
5
|
+
' - spawn({ detached: true }) -> survives the hook's process-tree teardown, BUT
|
|
6
|
+
' pops a console window even with windowsHide (a flash on every event);
|
|
7
|
+
' - spawn({ detached: false }) -> no window, BUT the sender is killed together with
|
|
8
|
+
' the hook's process tree before it can ship.
|
|
9
|
+
' wscript.exe is GUI-subsystem (it never allocates a console), and WshShell.Run with
|
|
10
|
+
' window style 0 (hidden) + bWaitOnReturn=False launches the target via ShellExecute
|
|
11
|
+
' as an INDEPENDENT process — so the sender is both windowless AND survives. No flash,
|
|
12
|
+
' capture still ships.
|
|
13
|
+
'
|
|
14
|
+
' Args: arg(0) = executable (node), arg(1..n) = its arguments. Each is re-quoted so
|
|
15
|
+
' paths with spaces (e.g. C:\Program Files\nodejs\node.exe) survive.
|
|
16
|
+
Option Explicit
|
|
17
|
+
Dim args, cmd, i
|
|
18
|
+
Set args = WScript.Arguments
|
|
19
|
+
If args.Count = 0 Then WScript.Quit 0
|
|
20
|
+
cmd = ""
|
|
21
|
+
For i = 0 To args.Count - 1
|
|
22
|
+
cmd = cmd & """" & args(i) & """ "
|
|
23
|
+
Next
|
|
24
|
+
CreateObject("WScript.Shell").Run cmd, 0, False
|