ai-lens 0.8.102 → 0.8.103

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
- 3747fae
1
+ 0aee413
package/CHANGELOG.md CHANGED
@@ -2,6 +2,9 @@
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.103 — 2026-06-18
6
+ - fix: `ai-lens status` no longer falsely reports the client as "outdated" on repo-path installs (where hooks run capture.js straight from a checkout that auto-updates on `git pull`). It now reads the client version from the repo the hooks actually run, instead of an unused leftover copy in `~/.ai-lens/client/` — which could be months stale and made the check show ✗ + "run init" even though the live client was current. The stale copy is now noted as ignored. Copy-mode installs are unchanged.
7
+
5
8
  ## 0.8.102 — 2026-06-17
6
9
  - fix: the per-machine `settings.local.json` overlay for committed Claude Code hooks now also writes the absolute launcher form on Windows (matching the main install path), so a repo whose tracked `settings.json` carries `$CLAUDE_PROJECT_DIR` hooks captures correctly on Windows without manual fix-up. `ai-lens status`/`init` now flag any leftover `$CLAUDE_PROJECT_DIR`/`%CLAUDE_PROJECT_DIR%` Claude hook on Windows as outdated so it migrates to the absolute form.
7
10
  - fix: the Windows windowless hook launcher (`ai-lens-hook.ps1`) now sends the event payload to node as raw UTF-8 bytes (Cyrillic prompts and accented paths are no longer mangled by PowerShell's default codepage) and fails open — on any internal error it exits cleanly and writes nothing to stdout, so a launcher hiccup can never break a Cursor hook or disrupt the session.
package/cli/status.js CHANGED
@@ -103,10 +103,12 @@ function expandTilde(pathStr) {
103
103
  }
104
104
 
105
105
  /**
106
- * Detect install mode from the capture.js path in hook commands.
107
- * Returns { ok, summary, detail } for the status output.
106
+ * Resolve the capture.js / launcher path(s) the hooks actually run, tagging each
107
+ * with its install mode. Shared by detectInstallMode and checkClientFiles so both
108
+ * reason about the SAME path the hooks execute (not an unrelated copy).
109
+ * Returns [{ tool, raw, resolved, mode: 'copy' | 'repo-path' }].
108
110
  */
109
- function detectInstallMode(tools) {
111
+ export function resolveHookClientPaths(tools) {
110
112
  // Normalise both sides to forward slashes before comparing — captureCommand
111
113
  // always emits forward-slash paths into the hook command, but join() on
112
114
  // Windows returns backslashes, so a raw startsWith() check would miss every
@@ -116,24 +118,40 @@ function detectInstallMode(tools) {
116
118
  for (const tool of tools) {
117
119
  const cmd = extractHookCommand(tool);
118
120
  if (!cmd) continue;
119
- // Match either a launcher (run.sh / run.cmd) or a legacy capture.js path. The launcher
120
- // always lives in the install dir, so it's by definition copy-mode.
121
- const m = cmd.match(/["']([^"']*(?:capture\.js|run\.(?:sh|cmd)))["']|(\S*(?:capture\.js|run\.(?:sh|cmd)))/);
122
- if (m) paths.push({ tool: tool.name, raw: m[1] || m[2], resolved: expandTilde(m[1] || m[2]).replace(/\\/g, '/') });
121
+ // Match either a launcher (run.sh / run.cmd / ai-lens-hook.ps1) or a capture.js path.
122
+ // A launcher always lives in the install dir, so it's by definition copy-mode.
123
+ const m = cmd.match(/["']([^"']*(?:capture\.js|run\.(?:sh|cmd)|ai-lens-hook\.ps1))["']|(\S*(?:capture\.js|run\.(?:sh|cmd)|ai-lens-hook\.ps1))/);
124
+ if (!m) continue;
125
+ const raw = m[1] || m[2];
126
+ const resolved = expandTilde(raw).replace(/\\/g, '/');
127
+ paths.push({ tool: tool.name, raw, resolved, mode: resolved.startsWith(copyDir) ? 'copy' : 'repo-path' });
128
+ }
129
+ return paths;
130
+ }
131
+
132
+ /** Read the `version` field from a package.json, or null if unreadable. */
133
+ function readPackageVersion(pkgJsonPath) {
134
+ try {
135
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
136
+ return pkg.version || null;
137
+ } catch {
138
+ return null;
123
139
  }
140
+ }
141
+
142
+ /**
143
+ * Detect install mode from the capture.js path in hook commands.
144
+ * Returns { ok, summary, detail } for the status output.
145
+ */
146
+ function detectInstallMode(tools) {
147
+ const paths = resolveHookClientPaths(tools);
124
148
  if (paths.length === 0) {
125
149
  return { ok: null, summary: 'unknown', detail: 'No hook commands found — cannot determine install mode' };
126
150
  }
127
- const modes = paths.map(p => {
128
- if (p.resolved.startsWith(copyDir)) return 'copy';
129
- return 'repo-path';
130
- });
151
+ const modes = paths.map(p => p.mode);
131
152
  const unique = [...new Set(modes)];
132
153
  const mode = unique.length === 1 ? unique[0] : 'mixed';
133
- const detail = paths.map(p => {
134
- const m = p.resolved.startsWith(copyDir) ? 'copy' : 'repo-path';
135
- return ` ${p.tool}: ${m} (${p.raw})`;
136
- }).join('\n');
154
+ const detail = paths.map(p => ` ${p.tool}: ${p.mode} (${p.raw})`).join('\n');
137
155
 
138
156
  if (mode === 'copy') {
139
157
  return { ok: true, summary: 'copy (~/.ai-lens/client/)', detail: `Client files copied to ~/.ai-lens/client/\nUpdate: npx -y ai-lens init --yes\n${detail}` };
@@ -492,7 +510,37 @@ function checkGitIdentity() {
492
510
  return { ok: false, summary: 'not configured', detail: 'Git identity not configured (git config user.email)' };
493
511
  }
494
512
 
495
- function checkClientFiles() {
513
+ export function checkClientFiles(tools = []) {
514
+ // In repo-path mode the hooks run capture.js straight from the repo, so the
515
+ // ~/.ai-lens/client/ copy (if any) is an UNUSED leftover from a prior copy-mode
516
+ // install. Reading that copy's version.json there would falsely flag the client
517
+ // "outdated" while the live client (the repo) is current — so check the version
518
+ // of the path the hooks actually execute.
519
+ const repoHook = resolveHookClientPaths(tools).find(p => p.mode === 'repo-path');
520
+ if (repoHook) {
521
+ // package.json sits one level up from the client/ dir the hook points into.
522
+ const pkgPath = repoHook.resolved.replace(/\/client\/[^/]+$/i, '/package.json');
523
+ const repoVersion = readPackageVersion(pkgPath);
524
+ const copyExists = existsSync(join(homedir(), '.ai-lens', 'client', 'capture.js'));
525
+ let detail = `Hooks run the client from the repo (repo-path mode) — auto-updates on git pull:\n ${repoHook.raw}`;
526
+ detail += repoVersion
527
+ ? `\n Repo client version: ${repoVersion}`
528
+ : `\n Repo client version: unknown (no package.json at ${pkgPath})`;
529
+ if (copyExists) {
530
+ detail += `\n Note: ~/.ai-lens/client/ exists but is UNUSED in repo-path mode (stale leftover from an old copy-install; safe to ignore).`;
531
+ }
532
+ // Healthy as long as the hook-source version is readable. Don't compare against
533
+ // the CLI: `ai-lens status` may run from a global npx CLI while the hooks point
534
+ // at a checkout — that mismatch is expected, not an error. A stale repo checkout
535
+ // surfaces via git (and the version line above), not as a false ✗ here.
536
+ return {
537
+ ok: repoVersion != null,
538
+ summary: repoVersion ? `repo-path (v${repoVersion})` : 'repo-path (version unknown)',
539
+ detail,
540
+ };
541
+ }
542
+
543
+ // Copy-mode (or no hooks resolved): the ~/.ai-lens/client/ copy IS the live client.
496
544
  const dir = join(homedir(), '.ai-lens', 'client');
497
545
  const files = ['capture.js', 'sender.js', 'config.js'];
498
546
  const results = files.map(f => ({ name: f, exists: existsSync(join(dir, f)) }));
@@ -1427,16 +1475,19 @@ export default async function status({ report = false } = {}) {
1427
1475
  // 3. Git identity
1428
1476
  printLine('Git identity', checkGitIdentity());
1429
1477
 
1478
+ // Resolve the tools (incl. project hooks) up front — checkClientFiles needs them
1479
+ // to read the version of the path the hooks actually run (repo-path vs copy).
1480
+ const installedTools = detectInstalledTools();
1481
+ const toolsWithProject = getToolsForCaptureTest();
1482
+
1430
1483
  // 4. Client files
1431
- printLine('Client files', checkClientFiles());
1484
+ printLine('Client files', checkClientFiles(toolsWithProject));
1432
1485
 
1433
1486
  // 5. Config
1434
1487
  const configResult = checkConfig();
1435
1488
  printLine('Config', configResult);
1436
1489
 
1437
1490
  // 6. Hooks: global + project (Cursor then Claude Code; within each: global then project)
1438
- const installedTools = detectInstalledTools();
1439
- const toolsWithProject = getToolsForCaptureTest();
1440
1491
  const isGlobalTool = (tool) => TOOL_CONFIGS.includes(tool);
1441
1492
  const toolLabel = (tool) => (isGlobalTool(tool) ? `${tool.name} (global)` : tool.name);
1442
1493
  const hooksOrder = (a, b) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.102",
3
+ "version": "0.8.103",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {