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 CHANGED
@@ -1 +1 @@
1
- 0a107d0
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
- return readdirSync(sourceDir).filter(f => f.endsWith('.js') || f.endsWith('.ps1')).sort();
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
  /**
@@ -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 source differs by caller, so read BOTH:
15
- # - Cursor pipes it through the PowerShell pipeline $input.
16
- # - Claude Code pipes it to the process's OS stdin read it as raw bytes.
17
- # Read $input first; if empty (Claude Code invocation), read the OS stdin stream.
18
- #
19
- # Why raw bytes (not [Console]::In.ReadToEnd()): [Console]::In decodes stdin via
20
- # [Console]::InputEncoding, which on Windows PowerShell 5.1 defaults to the OEM
21
- # codepage (e.g. CP866 on Russian Windows) so Cyrillic/non-ASCII in the JSON
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
- $payload = @($input) -join "`n"
35
- if ([string]::IsNullOrEmpty($payload)) {
36
- $stdin = [Console]::OpenStandardInput()
37
- $ms = New-Object System.IO.MemoryStream
38
- $stdin.CopyTo($ms)
39
- $payload = [System.Text.Encoding]::UTF8.GetString($ms.ToArray())
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 payload as raw UTF-8 bytes to node's stdin (capture.js reads UTF-8). Going
51
- # through BaseStream avoids the StreamWriter's default codepage on Windows PowerShell
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
- export function trySpawnSender() {
1321
- if (!shouldSpawnSender()) return;
1322
- const senderPath = join(__dirname, 'sender.js');
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.105",
3
+ "version": "0.8.107",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {