ai-lens 0.8.91 → 0.8.93
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 +76 -1
- package/cli/init.js +11 -2
- package/cli/status.js +8 -1
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
255d9a3
|
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.93 — 2026-06-15
|
|
6
|
+
- fix: on Windows, committed Claude Code project hooks no longer flash a console window that steals focus on every event — the hook command is wrapped in `conhost.exe --headless` (Windows 10 1809+), which runs capture with no visible window while still capturing every event. Older Windows builds and macOS/Linux are unchanged
|
|
7
|
+
|
|
8
|
+
## 0.8.92 — 2026-06-11
|
|
9
|
+
- fix: when global `~/.claude` hooks are active, `ai-lens init` no longer also installs the `.claude/settings.local.json` platform overlay (and removes a previously-written one) — global hooks already capture every project, and registering both made every hook event fire capture twice
|
|
10
|
+
- improve: `ai-lens status` shows committed other-OS project hooks as healthy ("capture covered by global hooks") when global hooks are doing the work, instead of a false warning
|
|
11
|
+
|
|
5
12
|
## 0.8.91 — 2026-06-11
|
|
6
13
|
- fix: Claude Code hooks committed to a repo now work on both Windows and macOS/Linux. A committed `.claude/settings.json` can only carry one OS's `CLAUDE_PROJECT_DIR` syntax, so on the other OS hooks silently captured nothing; `ai-lens init` now writes the running OS's form into `.claude/settings.local.json` (per-machine, auto-gitignored) without touching the shared file
|
|
7
14
|
- improve: `ai-lens status` correctly diagnoses committed project hooks — green when the platform overlay is in place, a clear fix hint when it's missing, and no more false "command path missing" on healthy setups
|
package/cli/hooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readFileSync, writeFileSync, copyFileSync, renameSync, mkdirSync, rmSync, unlinkSync, chmodSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
3
|
+
import { homedir, release } from 'node:os';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
6
|
|
|
@@ -302,6 +302,22 @@ export function writeLauncher({ clientDir = CLIENT_INSTALL_DIR, nodePath, platfo
|
|
|
302
302
|
// Hook command construction
|
|
303
303
|
// ---------------------------------------------------------------------------
|
|
304
304
|
|
|
305
|
+
// `conhost.exe --headless` runs a console program with no visible window — the
|
|
306
|
+
// fix for Windows hooks flashing/stealing focus on every event (Claude Code spawns
|
|
307
|
+
// the hook process without CREATE_NO_WINDOW; upstream anthropics/claude-code#61051).
|
|
308
|
+
// Unlike wscript/`-WindowStyle Hidden`, it keeps stdin wired, so capture.js still
|
|
309
|
+
// receives the hook payload. `--headless` requires the modern conhost shipped in
|
|
310
|
+
// Windows 10 1809 (build 17763); on older builds the flag errors, so we gate on it
|
|
311
|
+
// and fall back to the bare-node form (still flashes, but captures).
|
|
312
|
+
const WIN_HEADLESS_MIN_BUILD = 17763;
|
|
313
|
+
|
|
314
|
+
export function supportsConhostHeadless(osRelease = release()) {
|
|
315
|
+
// os.release() on Windows looks like '10.0.22631'. Build = the third segment.
|
|
316
|
+
const m = /^\d+\.\d+\.(\d+)/.exec(String(osRelease || ''));
|
|
317
|
+
if (!m) return false;
|
|
318
|
+
return Number(m[1]) >= WIN_HEADLESS_MIN_BUILD;
|
|
319
|
+
}
|
|
320
|
+
|
|
305
321
|
// Lazy default ctx for callers that don't pass one (legacy tests, TOOL_CONFIGS at
|
|
306
322
|
// import-time). Resolver isn't called at module load — only when captureCommand runs.
|
|
307
323
|
function resolveDefaultCtx() {
|
|
@@ -310,6 +326,7 @@ function resolveDefaultCtx() {
|
|
|
310
326
|
nodeResolution,
|
|
311
327
|
platform: process.platform,
|
|
312
328
|
clientDir: CLIENT_INSTALL_DIR,
|
|
329
|
+
conhost: process.platform === 'win32' && supportsConhostHeadless(),
|
|
313
330
|
};
|
|
314
331
|
}
|
|
315
332
|
|
|
@@ -340,6 +357,9 @@ export function captureCommand(opts = {}) {
|
|
|
340
357
|
const clientDir = ctx.clientDir ?? CLIENT_INSTALL_DIR;
|
|
341
358
|
const nodeResolution = ctx.nodeResolution;
|
|
342
359
|
const isWin = platform === 'win32';
|
|
360
|
+
// Windows-only windowless wrapper (see supportsConhostHeadless). Resolved per-ctx
|
|
361
|
+
// so a build < 1809 falls back to the bare form. Never applied off Windows.
|
|
362
|
+
const conhost = isWin && (ctx.conhost ?? supportsConhostHeadless());
|
|
343
363
|
// shell hint distinguishes cmd.exe (Claude Code) from PowerShell (Cursor) on
|
|
344
364
|
// Windows — the two need different escaping for paths with spaces.
|
|
345
365
|
const isPS = shell === 'powershell';
|
|
@@ -354,6 +374,13 @@ export function captureCommand(opts = {}) {
|
|
|
354
374
|
// POSIX shells use $VAR.
|
|
355
375
|
if (projectDirRelPath != null) {
|
|
356
376
|
const rel = projectDirRelPath.replace(/\\/g, '/').replace(/^\.?\/+/, '');
|
|
377
|
+
if (conhost) {
|
|
378
|
+
// Windowless form. conhost is the launched image (no cmd.exe shell), so the
|
|
379
|
+
// path must use $CLAUDE_PROJECT_DIR — Claude Code substitutes that variable
|
|
380
|
+
// itself before exec on every OS (the %VAR% form would survive unexpanded
|
|
381
|
+
// since there's no shell to expand it). Verified on Windows 11 / cc 2.1.177.
|
|
382
|
+
return `conhost.exe --headless node "$CLAUDE_PROJECT_DIR/${rel}"`;
|
|
383
|
+
}
|
|
357
384
|
const dir = isWin ? '%CLAUDE_PROJECT_DIR%' : '$CLAUDE_PROJECT_DIR';
|
|
358
385
|
return `node "${dir}/${rel}"`;
|
|
359
386
|
}
|
|
@@ -945,10 +972,21 @@ function isAcceptableHookCommand(cmd) {
|
|
|
945
972
|
export function isWrongPlatformProjectDirCommand(cmd, platform = process.platform) {
|
|
946
973
|
if (!isClaudeProjectDirCommand(cmd)) return false;
|
|
947
974
|
const n = (cmd || '').replace(/\\/g, '/');
|
|
975
|
+
// The conhost windowless form is unambiguously a Windows command (conhost.exe is
|
|
976
|
+
// Windows-only) and deliberately uses $CLAUDE_PROJECT_DIR — Claude Code substitutes
|
|
977
|
+
// that itself, so it expands on Windows too. It is therefore correct on win32 and
|
|
978
|
+
// wrong (won't run) anywhere else; the %VAR% rule below doesn't apply to it.
|
|
979
|
+
if (isConhostHeadlessCommand(n)) return platform !== 'win32';
|
|
948
980
|
const correctVar = platform === 'win32' ? '%CLAUDE_PROJECT_DIR%' : '$CLAUDE_PROJECT_DIR';
|
|
949
981
|
return !n.includes(correctVar);
|
|
950
982
|
}
|
|
951
983
|
|
|
984
|
+
// `conhost.exe --headless …` windowless wrapper (Windows). Tolerates an optional
|
|
985
|
+
// `.exe` and arbitrary spacing between the two tokens.
|
|
986
|
+
export function isConhostHeadlessCommand(cmd) {
|
|
987
|
+
return /(^|[\\/\s])conhost(\.exe)?\s+--headless\b/i.test(String(cmd || '').replace(/\\/g, '/'));
|
|
988
|
+
}
|
|
989
|
+
|
|
952
990
|
// Whether a hook-config file is committed (tracked) in git. The anti-churn rule
|
|
953
991
|
// (treat both $ and % CLAUDE_PROJECT_DIR forms as current) exists ONLY to keep a
|
|
954
992
|
// COMMITTED cross-platform hook file from being flipped to one OS's syntax and
|
|
@@ -1080,6 +1118,43 @@ export function writeClaudeLocalOverlay(tool, opts = {}) {
|
|
|
1080
1118
|
return { written: true, localPath: a.localPath };
|
|
1081
1119
|
}
|
|
1082
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* Whether GLOBAL Claude Code hooks (~/.claude/settings.json) are active on this
|
|
1123
|
+
* machine — current AI Lens hooks and not disabled. When they are, the
|
|
1124
|
+
* settings.local.json platform overlay is redundant: global hooks fire for every
|
|
1125
|
+
* project, and registering both would double-capture each event (collapsed by the
|
|
1126
|
+
* server's content-hash dedup, but pointless client work).
|
|
1127
|
+
* @param {object} [globalTool] — override for tests (defaults to the real ~/.claude tool)
|
|
1128
|
+
*/
|
|
1129
|
+
export function globalClaudeHooksActive(globalTool = TOOL_CONFIGS.find(t => t.name === 'Claude Code')) {
|
|
1130
|
+
if (!globalTool || !existsSync(globalTool.configPath)) return false;
|
|
1131
|
+
if (analyzeToolHooks(globalTool).status !== 'current') return false;
|
|
1132
|
+
return checkHooksDisabled(globalTool).length === 0;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Strip AI Lens hooks from the settings.local.json overlay (e.g. after global
|
|
1137
|
+
* Claude Code hooks were installed and the overlay became a double-capture source).
|
|
1138
|
+
* Preserves everything else in the file; refuses a malformed file.
|
|
1139
|
+
*/
|
|
1140
|
+
export function removeClaudeLocalOverlay(tool) {
|
|
1141
|
+
const localPath = claudeLocalSettingsPath(tool);
|
|
1142
|
+
if (!existsSync(localPath)) return { removed: false, reason: 'no settings.local.json' };
|
|
1143
|
+
let config;
|
|
1144
|
+
try {
|
|
1145
|
+
config = JSON.parse(readFileSync(localPath, 'utf-8'));
|
|
1146
|
+
} catch {
|
|
1147
|
+
return { removed: false, reason: `${localPath} is malformed`, localPath };
|
|
1148
|
+
}
|
|
1149
|
+
const hasAiLens = config.hooks && typeof config.hooks === 'object'
|
|
1150
|
+
&& Object.values(config.hooks).some(en => Array.isArray(en) && en.some(e => isAiLensHook(e)));
|
|
1151
|
+
if (!hasAiLens) return { removed: false, reason: 'no AI Lens hooks in overlay', localPath };
|
|
1152
|
+
const probe = { ...tool, configPath: localPath, sharedConfig: true };
|
|
1153
|
+
const stripped = buildStrippedConfig(probe, config);
|
|
1154
|
+
writeHooksConfig(probe, stripped ?? {});
|
|
1155
|
+
return { removed: true, localPath };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1083
1158
|
function isCurrentAiLensHook(entry, expected, opts = {}) {
|
|
1084
1159
|
// "Current" = a GUI-safe install (launcher OR absolute-node capture.js) OR a
|
|
1085
1160
|
// committed Claude Code $CLAUDE_PROJECT_DIR project hook. We do NOT require an exact
|
package/cli/init.js
CHANGED
|
@@ -15,7 +15,7 @@ import { migrateIfNeeded } from '../client/sender.js';
|
|
|
15
15
|
import {
|
|
16
16
|
CAPTURE_PATH, REPO_CAPTURE_PATH, detectInstalledTools, getCursorToolConfig, getClaudeCodeToolConfig, getCodexToolConfig,
|
|
17
17
|
analyzeToolHooks, buildMergedConfig, writeHooksConfig, describePlan, enableCodexHookTrust,
|
|
18
|
-
analyzeClaudeLocalOverlay, writeClaudeLocalOverlay,
|
|
18
|
+
analyzeClaudeLocalOverlay, writeClaudeLocalOverlay, removeClaudeLocalOverlay, globalClaudeHooksActive,
|
|
19
19
|
installClientFiles, readLensConfig, saveLensConfig, getVersionInfo,
|
|
20
20
|
getClaudeCodeHookDefsWithPath, getClaudeCodeHookDefsWithProjectDir, getCursorHookDefsWithPath, getCodexHookDefsWithPath,
|
|
21
21
|
cleanupLegacyHooks, cleanupOppositeScope, cleanupEmptyMcpJson, addCursorMcp, removeCursorMcp,
|
|
@@ -1002,7 +1002,16 @@ export default async function init() {
|
|
|
1002
1002
|
const overlayClaudeTool = getClaudeCodeToolConfig(overlayProjectRoot, 'Claude Code (project)', ctx);
|
|
1003
1003
|
if (overlayClaudeTool && existsSync(overlayClaudeTool.configPath)) {
|
|
1004
1004
|
const overlay = analyzeClaudeLocalOverlay(overlayClaudeTool);
|
|
1005
|
-
if (overlay.applicable &&
|
|
1005
|
+
if (overlay.applicable && globalClaudeHooksActive()) {
|
|
1006
|
+
// Global ~/.claude hooks fire for every project — an overlay would only
|
|
1007
|
+
// double-register capture. Make sure it's absent rather than present.
|
|
1008
|
+
const rm = removeClaudeLocalOverlay(overlayClaudeTool);
|
|
1009
|
+
if (rm.removed) {
|
|
1010
|
+
success(` Claude Code: global hooks cover this project — removed the redundant overlay from ${rm.localPath}`);
|
|
1011
|
+
} else {
|
|
1012
|
+
info(' Claude Code: global hooks cover this project — settings.local.json overlay not needed.');
|
|
1013
|
+
}
|
|
1014
|
+
} else if (overlay.applicable && !overlay.overlayCurrent) {
|
|
1006
1015
|
const r = writeClaudeLocalOverlay(overlayClaudeTool, { analysis: overlay });
|
|
1007
1016
|
if (r.written) {
|
|
1008
1017
|
success(` Claude Code: committed project hooks use the other OS's CLAUDE_PROJECT_DIR syntax — wrote this OS's form to ${r.localPath} (per-machine, not committed)`);
|
package/cli/status.js
CHANGED
|
@@ -7,7 +7,7 @@ import tls from 'node:tls';
|
|
|
7
7
|
|
|
8
8
|
import { TLS_TRUST_CODES, tlsCodeOf, tlsVerdictSummary, issuerName } from '../client/tls-trust.js';
|
|
9
9
|
|
|
10
|
-
import { getVersionInfo, readLensConfig, detectInstalledTools, getCursorToolConfig, getClaudeCodeToolConfig, getCodexToolConfig, analyzeToolHooks, checkHooksDisabled, verifyCodexHookTrust, CAPTURE_PATH, TOOL_CONFIGS, isClaudeProjectDirCommand, analyzeClaudeLocalOverlay, extractProjectDirRelPath } from './hooks.js';
|
|
10
|
+
import { getVersionInfo, readLensConfig, detectInstalledTools, getCursorToolConfig, getClaudeCodeToolConfig, getCodexToolConfig, analyzeToolHooks, checkHooksDisabled, verifyCodexHookTrust, CAPTURE_PATH, TOOL_CONFIGS, isClaudeProjectDirCommand, analyzeClaudeLocalOverlay, extractProjectDirRelPath, globalClaudeHooksActive } from './hooks.js';
|
|
11
11
|
import { DATA_DIR, PENDING_DIR, SENDING_DIR, SESSION_PATHS_DIR, LOG_PATH, CAPTURE_LOG_PATH, LAST_STATUS_REPORT_PATH, getGitIdentity, getMonitoredProjects } from '../client/config.js';
|
|
12
12
|
import { isLockStale } from '../client/sender.js';
|
|
13
13
|
import { initLogger, info, success, warn, error, heading, blank } from './logger.js';
|
|
@@ -555,6 +555,13 @@ function checkHooks(tool) {
|
|
|
555
555
|
if (cmd && isClaudeProjectDirCommand(cmd)) {
|
|
556
556
|
const overlay = analyzeClaudeLocalOverlay(tool);
|
|
557
557
|
if (overlay.applicable) {
|
|
558
|
+
if (globalClaudeHooksActive()) {
|
|
559
|
+
return {
|
|
560
|
+
ok: true,
|
|
561
|
+
summary: 'committed hooks are for the other OS \u2014 capture covered by global hooks',
|
|
562
|
+
detail: `${detail}\n\nCommitted settings.json uses the other OS's CLAUDE_PROJECT_DIR syntax (never fires here), but global ~/.claude hooks are active and capture every project \u2014 no overlay needed.`,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
558
565
|
if (overlay.overlayCurrent) {
|
|
559
566
|
return {
|
|
560
567
|
ok: true,
|