ai-lens 0.8.89 → 0.8.91
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 +8 -0
- package/cli/hooks.js +113 -0
- package/cli/import/claude-code.js +2 -2
- package/cli/import.js +1 -1
- package/cli/init.js +35 -1
- package/cli/remove.js +11 -1
- package/cli/status.js +40 -2
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
8894c21
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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.91 — 2026-06-11
|
|
6
|
+
- 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
|
+
- 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
|
|
8
|
+
- improve: `ai-lens remove` also cleans AI Lens hooks out of `.claude/settings.local.json`
|
|
9
|
+
|
|
10
|
+
## 0.8.90 — 2026-06-11
|
|
11
|
+
- improve: the default import window for `ai-lens import claude-code` (and the import offer inside `init`) is now 90 days instead of 30 — machines with a longer Claude Code transcript retention get their full recent history. Auto-analysis still covers only the last 30 days; older sessions enter metrics and cost rollups.
|
|
12
|
+
|
|
5
13
|
## 0.8.89 — 2026-06-11
|
|
6
14
|
- improve: `ai-lens import claude-code` (and the import step inside `init`) now shows a live progress spinner — transcripts scanned, events collected, batches shipping — instead of sitting silent for minutes on a large history. Terminal-only; piped/CI output is unchanged.
|
|
7
15
|
|
package/cli/hooks.js
CHANGED
|
@@ -967,6 +967,119 @@ function isGitTracked(filePath) {
|
|
|
967
967
|
}
|
|
968
968
|
}
|
|
969
969
|
|
|
970
|
+
// ---------------------------------------------------------------------------
|
|
971
|
+
// Claude Code settings.local.json platform overlay
|
|
972
|
+
// ---------------------------------------------------------------------------
|
|
973
|
+
// A COMMITTED .claude/settings.json can carry only ONE CLAUDE_PROJECT_DIR syntax —
|
|
974
|
+
// $VAR (POSIX sh) or %VAR% (Windows cmd.exe). Claude Code hands hook commands to the
|
|
975
|
+
// native OS shell, so on the other OS the variable never expands and the hook
|
|
976
|
+
// silently captures nothing. Upstream declined to fix or even document this
|
|
977
|
+
// (anthropic/claude-code#24710, closed as not planned). Our fix: leave the committed
|
|
978
|
+
// file alone (the anti-churn rule above) and mirror the hooks in the running OS's
|
|
979
|
+
// form into .claude/settings.local.json — Claude Code's per-machine layer, which it
|
|
980
|
+
// MERGES with settings.json at runtime and auto-gitignores. Both entries end up
|
|
981
|
+
// registered: the wrong-form one dies silently, the overlay captures. If upstream
|
|
982
|
+
// ever expands $VAR on Windows too, the double-fire collapses in the server's
|
|
983
|
+
// content-hash dedup.
|
|
984
|
+
|
|
985
|
+
/** Workspace-root-relative capture.js path out of a CLAUDE_PROJECT_DIR hook command. */
|
|
986
|
+
export function extractProjectDirRelPath(cmd) {
|
|
987
|
+
if (!cmd) return null;
|
|
988
|
+
const n = String(cmd).replace(/\\/g, '/');
|
|
989
|
+
const m = n.match(/[$%]CLAUDE_PROJECT_DIR%?\/([^"']*ai-lens\/client\/capture\.js)/);
|
|
990
|
+
return m ? m[1] : null;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** First AI Lens CLAUDE_PROJECT_DIR hook command in a Claude settings file, or null. */
|
|
994
|
+
function findClaudeProjectDirCommand(configPath) {
|
|
995
|
+
try {
|
|
996
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
997
|
+
if (!config.hooks || typeof config.hooks !== 'object') return null;
|
|
998
|
+
for (const entries of Object.values(config.hooks)) {
|
|
999
|
+
if (!Array.isArray(entries)) continue;
|
|
1000
|
+
for (const entry of entries) {
|
|
1001
|
+
if (!Array.isArray(entry?.hooks)) continue;
|
|
1002
|
+
for (const h of entry.hooks) {
|
|
1003
|
+
if (isClaudeProjectDirCommand(h?.command || '')) return h.command;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
} catch { /* unreadable / malformed — no overlay decision possible */ }
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export function claudeLocalSettingsPath(tool) {
|
|
1012
|
+
return join(dirname(tool.configPath), 'settings.local.json');
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Decide whether a Claude Code settings file needs the settings.local.json platform
|
|
1017
|
+
* overlay on this machine.
|
|
1018
|
+
*
|
|
1019
|
+
* applicable ⇔ the file has AI Lens CLAUDE_PROJECT_DIR hooks in the OTHER OS's
|
|
1020
|
+
* syntax AND is git-tracked (an untracked file is simply rewritten in place by the
|
|
1021
|
+
* allowPlatformRewrite path instead — no overlay needed).
|
|
1022
|
+
*
|
|
1023
|
+
* @param {object} tool — Claude Code tool config ({ configPath, ... })
|
|
1024
|
+
* @param {object} [opts] — { platform, tracked, ctx } overrides (tests)
|
|
1025
|
+
*/
|
|
1026
|
+
export function analyzeClaudeLocalOverlay(tool, opts = {}) {
|
|
1027
|
+
const platform = opts.platform ?? process.platform;
|
|
1028
|
+
const cmd = findClaudeProjectDirCommand(tool.configPath);
|
|
1029
|
+
if (!cmd) return { applicable: false, reason: 'no CLAUDE_PROJECT_DIR ai-lens hooks' };
|
|
1030
|
+
if (!isWrongPlatformProjectDirCommand(cmd, platform)) {
|
|
1031
|
+
return { applicable: false, reason: 'platform syntax already correct' };
|
|
1032
|
+
}
|
|
1033
|
+
const tracked = opts.tracked ?? isGitTracked(tool.configPath);
|
|
1034
|
+
if (!tracked) {
|
|
1035
|
+
return { applicable: false, reason: 'untracked — init rewrites it in place' };
|
|
1036
|
+
}
|
|
1037
|
+
const rel = extractProjectDirRelPath(cmd);
|
|
1038
|
+
if (!rel) return { applicable: false, reason: 'could not extract capture.js path from hook command' };
|
|
1039
|
+
|
|
1040
|
+
const localPath = claudeLocalSettingsPath(tool);
|
|
1041
|
+
const localTool = {
|
|
1042
|
+
...tool,
|
|
1043
|
+
name: `${tool.name} (settings.local.json overlay)`,
|
|
1044
|
+
configPath: localPath,
|
|
1045
|
+
// Never backup/rename a user's settings.local.json wholesale on parse errors.
|
|
1046
|
+
sharedConfig: true,
|
|
1047
|
+
hookDefs: getClaudeCodeHookDefsWithProjectDir(rel, { ...(opts.ctx ?? {}), platform }),
|
|
1048
|
+
};
|
|
1049
|
+
const overlayAnalysis = analyzeToolHooks(localTool, { platform, allowPlatformRewrite: true });
|
|
1050
|
+
return {
|
|
1051
|
+
applicable: true,
|
|
1052
|
+
rel,
|
|
1053
|
+
localPath,
|
|
1054
|
+
localTool,
|
|
1055
|
+
overlayCurrent: overlayAnalysis.status === 'current',
|
|
1056
|
+
overlayStatus: overlayAnalysis.status,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Write (or refresh) the platform overlay decided by analyzeClaudeLocalOverlay.
|
|
1062
|
+
* Merges into the existing settings.local.json, preserving everything that isn't an
|
|
1063
|
+
* AI Lens hook. A malformed existing local file is left untouched (never destroy a
|
|
1064
|
+
* user's local settings to install our hooks).
|
|
1065
|
+
*/
|
|
1066
|
+
export function writeClaudeLocalOverlay(tool, opts = {}) {
|
|
1067
|
+
const a = opts.analysis ?? analyzeClaudeLocalOverlay(tool, opts);
|
|
1068
|
+
if (!a.applicable) return { written: false, reason: a.reason };
|
|
1069
|
+
if (a.overlayCurrent) return { written: false, reason: 'overlay already current', localPath: a.localPath };
|
|
1070
|
+
let existing = null;
|
|
1071
|
+
if (existsSync(a.localPath)) {
|
|
1072
|
+
try {
|
|
1073
|
+
existing = JSON.parse(readFileSync(a.localPath, 'utf-8'));
|
|
1074
|
+
} catch {
|
|
1075
|
+
return { written: false, reason: `${a.localPath} is malformed — fix or remove it first`, localPath: a.localPath };
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const merged = buildMergedConfig(a.localTool, existing);
|
|
1079
|
+
writeHooksConfig(a.localTool, merged);
|
|
1080
|
+
return { written: true, localPath: a.localPath };
|
|
1081
|
+
}
|
|
1082
|
+
|
|
970
1083
|
function isCurrentAiLensHook(entry, expected, opts = {}) {
|
|
971
1084
|
// "Current" = a GUI-safe install (launcher OR absolute-node capture.js) OR a
|
|
972
1085
|
// committed Claude Code $CLAUDE_PROJECT_DIR project hook. We do NOT require an exact
|
|
@@ -88,7 +88,7 @@ export function resolveCutoff({ days, since, from }, now = new Date()) {
|
|
|
88
88
|
const lower = from || since;
|
|
89
89
|
if (lower) return new Date(Date.parse(lower)).toISOString();
|
|
90
90
|
if (days === 0) return new Date(0).toISOString();
|
|
91
|
-
const d = Number.isInteger(days) && days >= 0 ? days :
|
|
91
|
+
const d = Number.isInteger(days) && days >= 0 ? days : 90;
|
|
92
92
|
return new Date(now.getTime() - d * DAY_MS).toISOString();
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -284,7 +284,7 @@ async function drainSpool({ timeoutMs = 120_000 } = {}) {
|
|
|
284
284
|
|
|
285
285
|
export default async function importClaudeCode(flags) {
|
|
286
286
|
const {
|
|
287
|
-
days =
|
|
287
|
+
days = 90, since = null, from = null, to = null, dryRun = false, projects = null,
|
|
288
288
|
noRedact = false, analysisMaxAgeDays: amAgeFlag = 30,
|
|
289
289
|
} = flags;
|
|
290
290
|
const analysisMaxAgeDays = Number.isInteger(amAgeFlag) && amAgeFlag >= 0 ? amAgeFlag : 30;
|
package/cli/import.js
CHANGED
|
@@ -17,7 +17,7 @@ const INT_KEYS = new Set(['days', 'analysisMaxAgeDays']);
|
|
|
17
17
|
* REAL import instead of a preview.)
|
|
18
18
|
*/
|
|
19
19
|
export function parseFlags(argv) {
|
|
20
|
-
const flags = { days:
|
|
20
|
+
const flags = { days: 90, since: null, from: null, to: null, dryRun: false, projects: null, noRedact: false, analysisMaxAgeDays: 30 };
|
|
21
21
|
const errors = [];
|
|
22
22
|
for (let i = 0; i < argv.length; i++) {
|
|
23
23
|
const arg = argv[i];
|
package/cli/init.js
CHANGED
|
@@ -15,6 +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
19
|
installClientFiles, readLensConfig, saveLensConfig, getVersionInfo,
|
|
19
20
|
getClaudeCodeHookDefsWithPath, getClaudeCodeHookDefsWithProjectDir, getCursorHookDefsWithPath, getCodexHookDefsWithPath,
|
|
20
21
|
cleanupLegacyHooks, cleanupOppositeScope, cleanupEmptyMcpJson, addCursorMcp, removeCursorMcp,
|
|
@@ -988,6 +989,35 @@ export default async function init() {
|
|
|
988
989
|
}
|
|
989
990
|
}
|
|
990
991
|
|
|
992
|
+
// Cross-platform gap for COMMITTED Claude Code project hooks: a git-tracked
|
|
993
|
+
// .claude/settings.json carries one CLAUDE_PROJECT_DIR syntax, and on the other OS
|
|
994
|
+
// the hook silently never fires (cmd.exe vs sh; upstream anthropic/claude-code#24710
|
|
995
|
+
// closed as not planned). When the committed form doesn't match this OS, mirror the
|
|
996
|
+
// hooks into .claude/settings.local.json — Claude Code's per-machine layer
|
|
997
|
+
// (auto-gitignored, merged with settings.json at runtime). Runs regardless of
|
|
998
|
+
// --project-hooks: the committed file arrives with the clone, not via init.
|
|
999
|
+
if (!flags.noHooks) {
|
|
1000
|
+
try {
|
|
1001
|
+
const overlayProjectRoot = resolve(process.cwd());
|
|
1002
|
+
const overlayClaudeTool = getClaudeCodeToolConfig(overlayProjectRoot, 'Claude Code (project)', ctx);
|
|
1003
|
+
if (overlayClaudeTool && existsSync(overlayClaudeTool.configPath)) {
|
|
1004
|
+
const overlay = analyzeClaudeLocalOverlay(overlayClaudeTool);
|
|
1005
|
+
if (overlay.applicable && !overlay.overlayCurrent) {
|
|
1006
|
+
const r = writeClaudeLocalOverlay(overlayClaudeTool, { analysis: overlay });
|
|
1007
|
+
if (r.written) {
|
|
1008
|
+
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)`);
|
|
1009
|
+
} else {
|
|
1010
|
+
warn(` Claude Code: platform overlay not written — ${r.reason}`);
|
|
1011
|
+
}
|
|
1012
|
+
} else if (overlay.applicable && overlay.overlayCurrent) {
|
|
1013
|
+
info(' Claude Code: settings.local.json platform overlay already in place.');
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
warn(` Claude Code platform overlay check failed: ${err.message}`);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
991
1021
|
// Migrate legacy queue.jsonl → spool format (after user confirmation)
|
|
992
1022
|
try {
|
|
993
1023
|
const { migrated, errors: migErrors } = migrateIfNeeded();
|
|
@@ -1251,7 +1281,11 @@ async function maybeOfferImportHistory(flags) {
|
|
|
1251
1281
|
heading('Importing local history');
|
|
1252
1282
|
try {
|
|
1253
1283
|
const { default: importClaudeCode } = await import('./import/claude-code.js');
|
|
1254
|
-
|
|
1284
|
+
// 90 days, not 30: Claude Code's local retention (~30d default) bounds the
|
|
1285
|
+
// volume anyway, but machines with longer retention get their full recent
|
|
1286
|
+
// history. LLM-analysis cost stays capped by the importer's
|
|
1287
|
+
// analysisMaxAgeDays=30 — older sessions land in metrics/cost rollups only.
|
|
1288
|
+
await importClaudeCode({ days: 90 }); // server URL + git identity were just configured
|
|
1255
1289
|
} catch (err) {
|
|
1256
1290
|
error(` Import failed: ${err.message}`);
|
|
1257
1291
|
info(' You can retry later: `npx -y ai-lens import claude-code`');
|
package/cli/remove.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from './logger.js';
|
|
10
10
|
import {
|
|
11
11
|
detectInstalledTools, getCursorToolConfig, getClaudeCodeToolConfig, getCodexToolConfig, analyzeToolHooks,
|
|
12
|
-
buildStrippedConfig, writeHooksConfig, removeClientFiles, getVersionInfo,
|
|
12
|
+
buildStrippedConfig, writeHooksConfig, removeClientFiles, getVersionInfo, claudeLocalSettingsPath,
|
|
13
13
|
cleanupLegacyHooks, cleanupEmptyMcpJson, removeCursorMcp, removeCodexHookTrust,
|
|
14
14
|
} from './hooks.js';
|
|
15
15
|
|
|
@@ -45,6 +45,16 @@ export default async function remove() {
|
|
|
45
45
|
tools = tools.filter(t => t.name !== 'Claude Code' || t.configPath !== projectClaude.configPath);
|
|
46
46
|
tools.push(projectClaude);
|
|
47
47
|
}
|
|
48
|
+
// The settings.local.json platform overlay (written by init for committed
|
|
49
|
+
// cross-platform hooks) holds AI Lens hooks of its own — strip it like any
|
|
50
|
+
// other config so remove leaves no live capture behind. sharedConfig: never
|
|
51
|
+
// delete or backup-rename the user's whole local settings file.
|
|
52
|
+
if (projectClaude) {
|
|
53
|
+
const localOverlayPath = claudeLocalSettingsPath(projectClaude);
|
|
54
|
+
if (existsSync(localOverlayPath)) {
|
|
55
|
+
tools.push({ ...projectClaude, name: 'Claude Code (project local overlay)', configPath: localOverlayPath, sharedConfig: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
48
58
|
const projectCodex = getCodexToolConfig(projectRoot, 'Codex (project)');
|
|
49
59
|
if (projectCodex && existsSync(projectCodex.configPath)) {
|
|
50
60
|
tools = tools.filter(t => t.name !== 'Codex' || t.configPath !== projectCodex.configPath);
|
package/cli/status.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync, writeFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { execSync, spawnSync } from 'node:child_process';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
4
|
import { homedir, release as osRelease, arch as osArch } from 'node:os';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
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 } from './hooks.js';
|
|
10
|
+
import { getVersionInfo, readLensConfig, detectInstalledTools, getCursorToolConfig, getClaudeCodeToolConfig, getCodexToolConfig, analyzeToolHooks, checkHooksDisabled, verifyCodexHookTrust, CAPTURE_PATH, TOOL_CONFIGS, isClaudeProjectDirCommand, analyzeClaudeLocalOverlay, extractProjectDirRelPath } 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';
|
|
@@ -544,6 +544,44 @@ function checkHooks(tool) {
|
|
|
544
544
|
};
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
+
// CLAUDE_PROJECT_DIR project hooks: the var only expands at Claude Code runtime, so
|
|
548
|
+
// literal-path validation below would false-flag them. Validate the two things that
|
|
549
|
+
// actually break: capture.js missing at the project-relative path, and the committed
|
|
550
|
+
// file carrying the OTHER OS's variable syntax (which never expands here \u2014
|
|
551
|
+
// anthropic/claude-code#24710). The latter is healthy when the settings.local.json
|
|
552
|
+
// platform overlay is in place (written by init), red with a fix hint when it isn't.
|
|
553
|
+
if (mapped.ok === true && tool.dirPath && dirname(tool.dirPath) !== homedir()) {
|
|
554
|
+
const cmd = extractHookCommand(tool);
|
|
555
|
+
if (cmd && isClaudeProjectDirCommand(cmd)) {
|
|
556
|
+
const overlay = analyzeClaudeLocalOverlay(tool);
|
|
557
|
+
if (overlay.applicable) {
|
|
558
|
+
if (overlay.overlayCurrent) {
|
|
559
|
+
return {
|
|
560
|
+
ok: true,
|
|
561
|
+
summary: 'hooks current (settings.local.json platform overlay)',
|
|
562
|
+
detail: `${detail}\n\nCommitted settings.json uses the other OS's CLAUDE_PROJECT_DIR syntax (never fires here); this machine captures via the overlay in ${overlay.localPath}.`,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
ok: false,
|
|
567
|
+
summary: "committed hooks use the other OS's CLAUDE_PROJECT_DIR syntax \u2014 never fire here",
|
|
568
|
+
detail: `${detail}\n\nThe committed .claude/settings.json hooks use the other OS's variable syntax, which this OS's shell does not expand (anthropic/claude-code#24710).\nFix: npx -y ai-lens init (writes this OS's form to .claude/settings.local.json \u2014 per-machine, not committed)`,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
// Correct platform form: resolve the project-relative path for a real existence check.
|
|
572
|
+
const rel = extractProjectDirRelPath(cmd);
|
|
573
|
+
const projectRoot = dirname(tool.dirPath);
|
|
574
|
+
if (rel && !existsSync(join(projectRoot, rel))) {
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
summary: 'hooks current BUT capture.js missing in project',
|
|
578
|
+
detail: `${detail}\n\nCommand path issues:\n \u2717 capture.js not found at: ${join(projectRoot, rel)}`,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
return { ok: true, summary: mapped.label, detail };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
547
585
|
// Path validation: when hooks are present, verify command paths exist
|
|
548
586
|
if (mapped.ok === true) {
|
|
549
587
|
const pathIssues = validateHookCommandPaths(tool);
|