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 +1 -1
- package/CHANGELOG.md +6 -0
- package/cli/hooks.js +60 -24
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
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,
|
|
384
|
-
: captureCommand({ useTilde: effectiveUseTilde,
|
|
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):
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
// windowless
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
//
|
|
1104
|
-
//
|
|
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
|
|
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
|
-
//
|
|
1316
|
-
|
|
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')
|
|
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
|
}
|