ai-lens 0.8.96 → 0.8.98

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
- 549ce90
1
+ 5d8b7f1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
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.98 — 2026-06-17
6
+ - fix: Cursor hooks on Windows no longer use the `conhost.exe --headless` wrapper — it made Cursor report a "JSON Parse Error" on every hook (conhost emits terminal escape codes that Cursor tries to parse as the hook's output) and broke capture. Cursor returns to the plain working form; machines that briefly got the conhost'd Cursor hook are auto-downgraded on the next `ai-lens init`/`/setup`. Claude Code keeps the windowless wrapper (it tolerates the output). The minor Cursor console flash on Windows is upstream Cursor behaviour
7
+
8
+ ## 0.8.97 — 2026-06-17
9
+ - fix: `ai-lens init` no longer leaves an invalid `{ "version": 1 }` (no hooks) in `~/.cursor/hooks.json` when migrating to project hooks — Cursor flagged such a file as an error and stopped running ALL hooks, silently killing capture even when project hooks were correct. init now deletes the empty file, and repairs machines already left in this state on the next run
10
+
5
11
  ## 0.8.96 — 2026-06-17
6
12
  - fix: `ai-lens status` now reports a Windows Cursor (or Claude Code) hook that lacks the windowless `conhost.exe --headless` wrapper as outdated, so a normal re-run of `ai-lens init` (or `/setup`) upgrades it in place. Previously the console-flash fix from 0.8.95 only applied to brand-new installs; existing hooks were considered up-to-date and never rewritten. macOS/Linux, older Windows, and Codex hooks are unaffected
7
13
 
package/cli/hooks.js CHANGED
@@ -375,13 +375,17 @@ export function cursorCaptureCommand(opts = {}) {
375
375
  // hint when building the underlying command (avoids the cmd.exe `call` prefix
376
376
  // which PowerShell doesn't understand).
377
377
  const shell = platform === 'win32' ? 'powershell' : null;
378
- // windowless: on Windows ≥1809 wrap node in `conhost.exe --headless` so Cursor
379
- // doesn't flash a console window on every event (incl. session start). `& ` still
380
- // works in front of conhost; the detector strips `& ` then the conhost prefix, so
381
- // the form stays recognised as current (no churn). Mac/old-Windows: unchanged.
378
+ // NOTE: Cursor deliberately does NOT get the `conhost.exe --headless` windowless
379
+ // wrapper (unlike Claude Code). conhost runs a ConPTY and emits VT escape sequences
380
+ // (e.g. ESC[?9001h win32-input-mode) onto the hook's output stream, and Cursor
381
+ // parses a hook's stdout as JSON "JSON Parse Error … is not valid JSON", which
382
+ // breaks the hook. Claude Code tolerates non-JSON hook stdout, so it keeps conhost.
383
+ // The residual Cursor console flash on Windows is upstream (how Cursor spawns the
384
+ // hook process) and is the lesser evil vs a broken hook. (ADR 0003 §3.2 deferred
385
+ // Cursor+conhost as unverified; verified incompatible — reverted here.)
382
386
  const cmd = customPath != null
383
- ? captureCommand({ rawPath: true, customPath, windowless: true, ctx, shell })
384
- : captureCommand({ useTilde: effectiveUseTilde, windowless: true, ctx, shell });
387
+ ? captureCommand({ rawPath: true, customPath, ctx, shell })
388
+ : captureCommand({ useTilde: effectiveUseTilde, ctx, shell });
385
389
  return platform === 'win32' ? `& ${cmd}` : cmd;
386
390
  }
387
391
 
@@ -1089,25 +1093,28 @@ function isCurrentAiLensHook(entry, expected, opts = {}) {
1089
1093
  // $CLAUDE_PROJECT_DIR/%CLAUDE_PROJECT_DIR% hook written for the OTHER OS won't
1090
1094
  // expand on this platform, so flag it outdated to let init rewrite it.
1091
1095
  //
1092
- // Windowless exception (same allowPlatformRewrite gate): when the freshly
1093
- // regenerated `expected` command carries the `conhost.exe --headless` wrapper
1094
- // (Cursor/Claude on conhost-capable Windows) but the stored command does NOT,
1095
- // flag it outdated so init upgrades it otherwise a pre-conhost install stays
1096
- // `current` forever and keeps flashing a console window on every event. We
1097
- // compare ONLY the windowless dimension (not the whole command), so legitimate
1098
- // path/node/install-mode variation never false-flags. `expected` already bakes
1099
- // in tool+platform+conhost-support (makeCursorHookDefs/makeClaudeHookDefs pass
1100
- // windowless:true; makeCodexHookDefs does not), so this self-scopes: Codex,
1101
- // macOS, and old Windows produce a non-conhost `expected` and never trip it.
1102
- // Committed (tracked) files are exempt via allowPlatformRewrite — they carry one
1103
- // OS-agnostic syntax and can't bake a Windows-only wrapper; the per-machine
1104
- // overlay provides the windowless path.
1096
+ // Windowless exception (same allowPlatformRewrite gate): flag a hook outdated when
1097
+ // its stored `conhost.exe --headless` wrapper DISAGREES with the freshly regenerated
1098
+ // `expected` command in EITHER direction so init re-syncs it:
1099
+ // - expected has conhost, stored doesn't ADD it (Claude on conhost-capable
1100
+ // Windows that predates the windowless form);
1101
+ // - expected has NO conhost, stored DOES REMOVE it (Cursor: conhost is
1102
+ // incompatible its ConPTY VT output breaks Cursor's JSON parse of hook stdout
1103
+ // so we downgrade machines that got the short-lived conhost'd Cursor form).
1104
+ // We compare ONLY the windowless dimension (not the whole command), so legitimate
1105
+ // path/node/install-mode variation never false-flags. `expected` already bakes in
1106
+ // tool+platform+conhost-support (makeClaudeHookDefs passes windowless:true;
1107
+ // makeCursorHookDefs and makeCodexHookDefs do NOT), so this self-scopes: Codex,
1108
+ // Cursor, macOS, and old Windows produce a non-conhost `expected`, and only Claude
1109
+ // on conhost-capable Windows produces a conhost `expected`. Committed (tracked)
1110
+ // files are exempt via allowPlatformRewrite — they carry one OS-agnostic syntax
1111
+ // and can't bake a Windows-only wrapper; the per-machine overlay provides it.
1105
1112
  const { platform = process.platform, allowPlatformRewrite = false } = opts;
1106
1113
  const expectedCmd = expected?.command ?? expected?.hooks?.[0]?.command ?? '';
1107
1114
  const expectedWindowless = isConhostHeadlessCommand(expectedCmd);
1108
1115
  const ok = (cmd) => isAcceptableHookCommand(cmd)
1109
1116
  && !(allowPlatformRewrite && isWrongPlatformProjectDirCommand(cmd, platform))
1110
- && !(allowPlatformRewrite && expectedWindowless && !isConhostHeadlessCommand(cmd));
1117
+ && !(allowPlatformRewrite && expectedWindowless !== isConhostHeadlessCommand(cmd));
1111
1118
  // Flat format (Cursor): single command per entry.
1112
1119
  if (entry?.command != null) {
1113
1120
  return ok(entry.command);
@@ -1312,8 +1319,15 @@ export function buildStrippedConfig(tool, existingConfig) {
1312
1319
  // If no hooks remain, clean up
1313
1320
  if (Object.keys(base.hooks).length === 0) {
1314
1321
  delete base.hooks;
1315
- // If other settings remain (shared config like settings.json), keep them
1316
- if (Object.keys(base).length > 0) {
1322
+ // Keep the file only if REAL user settings remain — i.e. keys beyond the tool's
1323
+ // OWN scaffolding (e.g. Cursor's `version: 1`, which we wrote ourselves). Leaving
1324
+ // a Cursor hooks.json as just `{ "version": 1 }` is an invalid hookless config:
1325
+ // Cursor flags it as an error and stops running hooks. In that case return null so
1326
+ // the caller deletes the file. Real settings (Claude settings.json env/statusLine,
1327
+ // not in topLevelFields) are still preserved.
1328
+ const scaffolding = new Set(Object.keys(tool.topLevelFields || {}));
1329
+ const meaningful = Object.keys(base).filter((k) => !scaffolding.has(k));
1330
+ if (meaningful.length > 0) {
1317
1331
  return base;
1318
1332
  }
1319
1333
  return null;
@@ -1800,7 +1814,19 @@ export function cleanupOppositeScope(activeTools) {
1800
1814
  try { config = JSON.parse(raw); } catch { continue; }
1801
1815
 
1802
1816
  const hooks = config.hooks;
1803
- if (!hooks || typeof hooks !== 'object') continue;
1817
+ if (!hooks || typeof hooks !== 'object') {
1818
+ // Repair: an older CLI could strip the opposite-scope hooks but leave behind an
1819
+ // invalid hookless scaffolding file (e.g. Cursor's `{ "version": 1 }`), which the
1820
+ // tool then flags as an error and stops running hooks. If a non-shared opposite
1821
+ // file has no hooks and only our own scaffolding keys, delete it.
1822
+ const scaffold = new Set(Object.keys(global.topLevelFields || {}));
1823
+ const onlyScaffold = Object.keys(config).every((k) => scaffold.has(k));
1824
+ if (!global.sharedConfig && onlyScaffold) {
1825
+ try { unlinkSync(oppositeConfigPath); } catch { /* already gone */ }
1826
+ results.push({ path: oppositeConfigPath, action: 'removed', toolName: oppositeToolName });
1827
+ }
1828
+ continue;
1829
+ }
1804
1830
 
1805
1831
  let hasAiLens = false;
1806
1832
  for (const entries of Object.values(hooks)) {
@@ -1816,8 +1842,18 @@ export function cleanupOppositeScope(activeTools) {
1816
1842
  if (stripped) {
1817
1843
  writeHooksConfig({ configPath: oppositeConfigPath }, stripped);
1818
1844
  results.push({ path: oppositeConfigPath, action: 'cleaned', toolName: oppositeToolName });
1845
+ } else if (global.sharedConfig) {
1846
+ // Shared config (Claude settings.json): drop the hooks but keep the file —
1847
+ // it may carry the user's other settings.
1848
+ writeHooksConfig({ configPath: oppositeConfigPath }, {});
1849
+ results.push({ path: oppositeConfigPath, action: 'cleaned', toolName: oppositeToolName });
1850
+ } else {
1851
+ // Non-shared (Cursor/Codex): the file held only AI Lens hooks plus our own
1852
+ // scaffolding (e.g. Cursor's `version: 1`). Delete it rather than leave an
1853
+ // invalid hookless `{ "version": 1 }` that breaks the tool's hook loading.
1854
+ try { unlinkSync(oppositeConfigPath); } catch { /* already gone */ }
1855
+ results.push({ path: oppositeConfigPath, action: 'removed', toolName: oppositeToolName });
1819
1856
  }
1820
- // Don't delete the file — global settings.json may have other settings
1821
1857
  }
1822
1858
  return results;
1823
1859
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.96",
3
+ "version": "0.8.98",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {