ai-lens 0.8.68 → 0.8.70
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 +28 -4
- package/cli/init.js +33 -7
- package/cli/status.js +22 -3
- package/client/capture.js +3 -3
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
0912a07
|
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.70 — 2026-05-28
|
|
6
|
+
- diag: `sender-spawn-failed`, `codex-watcher-spawn-failed` and `queue-write-failed` entries in the capture log now record the OS `error.code` (e.g. EMFILE, EACCES). `ai-lens status` surfaces a per-category code breakdown so these failures can be diagnosed centrally instead of guessing from a bare count. The raw error message (which may contain local paths) stays on your machine — only the short code travels in the status report.
|
|
7
|
+
|
|
8
|
+
## 0.8.69 — 2026-05-27
|
|
9
|
+
- feat: per-machine launcher (`~/.ai-lens/client/run.sh` / `run.cmd`) now also accepts an optional script path as its first argument. With no args it still execs the sibling `capture.js` (default install), but `~/.ai-lens/client/run.sh path/to/some/capture.js` execs that script with the launcher's resolved node — letting bootstrap-style workflows (e.g. meta-cursor) route through the launcher for proper node resolution while keeping `capture.js` under git in the workspace repo.
|
|
10
|
+
- feat: new `--install-launcher` flag for `init`. Forces launcher installation even when `--no-hooks` is set, so the meta-cursor setup skill can wire up `~/.ai-lens/client/run.sh` without touching the static hook templates in the workspace repo.
|
|
11
|
+
|
|
5
12
|
## 0.8.68 — 2026-05-27
|
|
6
13
|
- fix: hooks now resolve node from nvm, asdf, fnm, volta and n in addition to Homebrew. Previously `init` fell back to `/usr/bin/env node`, which made GUI Cursor on macOS silently lose every event because launchd's minimal PATH doesn't include node. Affected users with custom node installs (e.g. `/Users/<you>/node/bin/node`) saw hundreds of dropped events with no visible error.
|
|
7
14
|
- feat: `init` now installs a per-machine launcher (`~/.ai-lens/client/run.sh` on macOS/Linux, `run.cmd` on Windows) with the resolved node path baked in. Hook commands invoke the launcher directly, so they no longer depend on the GUI app's PATH and survive `brew upgrade node`.
|
package/cli/hooks.js
CHANGED
|
@@ -242,8 +242,20 @@ export function launcherFilename(platform = process.platform) {
|
|
|
242
242
|
|
|
243
243
|
/**
|
|
244
244
|
* Write a per-machine launcher script with the resolved node path baked in.
|
|
245
|
-
*
|
|
246
|
-
*
|
|
245
|
+
*
|
|
246
|
+
* The launcher acts as a node-resolution shim that supports two invocation modes:
|
|
247
|
+
*
|
|
248
|
+
* 1. No arguments — run the sibling capture.js (the default install flow where
|
|
249
|
+
* both the launcher and capture.js live in ~/.ai-lens/client/).
|
|
250
|
+
* 2. Script path as $1 — run that script with the resolved node. This is the
|
|
251
|
+
* repo-path mode (e.g. meta-cursor static hooks pointing at
|
|
252
|
+
* `internal/analytics/ai-lens/client/capture.js`) where capture.js auto-
|
|
253
|
+
* updates via `git pull` and only the node binary needs per-machine baking.
|
|
254
|
+
*
|
|
255
|
+
* In both cases the launcher is independent of HOME / USERPROFILE / cwd — for
|
|
256
|
+
* mode 1 the path is derived from `dirname $0`; for mode 2 it's whatever the
|
|
257
|
+
* caller passes through (typically a workspace-relative path that resolves
|
|
258
|
+
* against the cwd Cursor/Claude Code set when firing the hook).
|
|
247
259
|
*
|
|
248
260
|
* @param {object} opts
|
|
249
261
|
* @param {string} opts.clientDir — directory where the launcher (and capture.js) live
|
|
@@ -256,7 +268,13 @@ export function writeLauncher({ clientDir = CLIENT_INSTALL_DIR, nodePath, platfo
|
|
|
256
268
|
|
|
257
269
|
if (platform === 'win32') {
|
|
258
270
|
const escaped = nodePath.replace(/"/g, '""');
|
|
259
|
-
const content =
|
|
271
|
+
const content =
|
|
272
|
+
'@echo off\r\n'
|
|
273
|
+
+ 'if "%~1"=="" goto default\r\n'
|
|
274
|
+
+ `"${escaped}" %*\r\n`
|
|
275
|
+
+ 'goto :eof\r\n'
|
|
276
|
+
+ ':default\r\n'
|
|
277
|
+
+ `"${escaped}" "%~dp0capture.js"\r\n`;
|
|
260
278
|
const target = join(clientDir, 'run.cmd');
|
|
261
279
|
writeFileSync(target, content);
|
|
262
280
|
return target;
|
|
@@ -265,8 +283,14 @@ export function writeLauncher({ clientDir = CLIENT_INSTALL_DIR, nodePath, platfo
|
|
|
265
283
|
const escaped = shellEscape(nodePath, 'linux'); // POSIX single-quote escape
|
|
266
284
|
const content =
|
|
267
285
|
'#!/bin/sh\n'
|
|
286
|
+
+ `# AI Lens per-machine launcher. Two modes:\n`
|
|
287
|
+
+ `# $0 → exec node on sibling capture.js (default install)\n`
|
|
288
|
+
+ `# $0 <script> → exec node on <script> with remaining args (repo-path mode)\n`
|
|
289
|
+
+ 'if [ $# -gt 0 ]; then\n'
|
|
290
|
+
+ ` exec ${escaped} "$@"\n`
|
|
291
|
+
+ 'fi\n'
|
|
268
292
|
+ 'DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)\n'
|
|
269
|
-
+ `exec ${escaped} "$DIR/capture.js"
|
|
293
|
+
+ `exec ${escaped} "$DIR/capture.js"\n`;
|
|
270
294
|
const target = join(clientDir, 'run.sh');
|
|
271
295
|
writeFileSync(target, content);
|
|
272
296
|
try { chmodSync(target, 0o755); } catch { /* best effort */ }
|
package/cli/init.js
CHANGED
|
@@ -20,8 +20,9 @@ import {
|
|
|
20
20
|
getClaudeCodeHookDefsWithPath, getCursorHookDefsWithPath,
|
|
21
21
|
cleanupLegacyHooks, cleanupOppositeScope, cleanupEmptyMcpJson, addCursorMcp, removeCursorMcp,
|
|
22
22
|
checkHooksDisabled, enableHooks,
|
|
23
|
-
findStableNodePath, isVersionPinnedNodePath,
|
|
23
|
+
findStableNodePath, isVersionPinnedNodePath, writeLauncher,
|
|
24
24
|
} from './hooks.js';
|
|
25
|
+
import { mkdirSync } from 'node:fs';
|
|
25
26
|
import { scanNestedClaudeProjects, summarizeNestedProjects } from './scan.js';
|
|
26
27
|
|
|
27
28
|
function ask(question) {
|
|
@@ -351,6 +352,9 @@ function getInitArgs() {
|
|
|
351
352
|
case '--use-repo-path':
|
|
352
353
|
flags.useRepoPath = true;
|
|
353
354
|
break;
|
|
355
|
+
case '--install-launcher':
|
|
356
|
+
flags.installLauncher = true;
|
|
357
|
+
break;
|
|
354
358
|
case '--mcp-scope':
|
|
355
359
|
if (i + 1 < args.length) flags.mcpScope = args[++i];
|
|
356
360
|
else process.stderr.write('Warning: --mcp-scope requires a value\n');
|
|
@@ -359,7 +363,7 @@ function getInitArgs() {
|
|
|
359
363
|
const a = args[i];
|
|
360
364
|
if (a.startsWith('-')) {
|
|
361
365
|
process.stderr.write(`Warning: unknown flag "${a}" — did you mean --${a.replace(/^-+/, '')}?\n`);
|
|
362
|
-
} else if (['server', 'projects', 'yes', 'no-mcp', 'no-hooks', 'project-hooks', 'use-repo-path', 'mcp-scope'].includes(a)) {
|
|
366
|
+
} else if (['server', 'projects', 'yes', 'no-mcp', 'no-hooks', 'project-hooks', 'use-repo-path', 'install-launcher', 'mcp-scope'].includes(a)) {
|
|
363
367
|
process.stderr.write(`Warning: unexpected argument "${a}" — did you mean --${a}?\n`);
|
|
364
368
|
}
|
|
365
369
|
}
|
|
@@ -387,11 +391,17 @@ export default async function init() {
|
|
|
387
391
|
//
|
|
388
392
|
// --project-hooks ALWAYS writes hooks regardless of whether ~/.cursor or
|
|
389
393
|
// ~/.claude exist globally — so willWriteHooks must consider it.
|
|
394
|
+
//
|
|
395
|
+
// --install-launcher (added 0.8.69) forces launcher installation even when
|
|
396
|
+
// --no-hooks. Used by bootstrap workflows like meta-cursor where static hook
|
|
397
|
+
// templates point at ~/.ai-lens/client/run.sh but init must NOT overwrite
|
|
398
|
+
// those templates with the dynamic hook form.
|
|
390
399
|
const globalTools = detectInstalledTools();
|
|
391
400
|
const willWriteHooks = !flags.noHooks && (globalTools.length > 0 || !!flags.projectHooks);
|
|
401
|
+
const willInstallLauncher = willWriteHooks || !!flags.installLauncher;
|
|
392
402
|
let nodeResolution = null;
|
|
393
403
|
let ctx = null;
|
|
394
|
-
if (
|
|
404
|
+
if (willInstallLauncher) {
|
|
395
405
|
nodeResolution = findStableNodePath();
|
|
396
406
|
if (!nodeResolution) {
|
|
397
407
|
error('Could not find any node binary on this system.');
|
|
@@ -406,7 +416,9 @@ export default async function init() {
|
|
|
406
416
|
} else {
|
|
407
417
|
detail(` Resolved node: ${nodeResolution.path} (${nodeResolution.source})`);
|
|
408
418
|
}
|
|
409
|
-
|
|
419
|
+
if (willWriteHooks) {
|
|
420
|
+
ctx = { nodeResolution, platform: process.platform, clientDir: join(homedir(), '.ai-lens', 'client') };
|
|
421
|
+
}
|
|
410
422
|
}
|
|
411
423
|
|
|
412
424
|
// Detect installed tools — re-detect with ctx now that it's available.
|
|
@@ -545,9 +557,8 @@ export default async function init() {
|
|
|
545
557
|
// Build new config in memory — saved after "Proceed?" confirmation
|
|
546
558
|
const newConfig = { ...currentConfig, serverUrl, projects };
|
|
547
559
|
|
|
548
|
-
// Install client files to ~/.ai-lens/client/ (skip when --use-repo-path
|
|
549
|
-
//
|
|
550
|
-
// — --no-hooks installs leave it out (and don't require a resolved node path).
|
|
560
|
+
// Install client files to ~/.ai-lens/client/ (skip when --use-repo-path because
|
|
561
|
+
// capture.js comes from the monorepo / package source in that mode).
|
|
551
562
|
if (!flags.useRepoPath) {
|
|
552
563
|
heading('Installing client files...');
|
|
553
564
|
try {
|
|
@@ -564,6 +575,21 @@ export default async function init() {
|
|
|
564
575
|
error(` Failed to install client files: ${err.message}`);
|
|
565
576
|
return;
|
|
566
577
|
}
|
|
578
|
+
} else if (willInstallLauncher) {
|
|
579
|
+
// --use-repo-path with --install-launcher: keep capture.js out of the install
|
|
580
|
+
// dir (it lives in the monorepo, auto-updates via git pull) but still create
|
|
581
|
+
// the per-machine launcher so static hooks can route through it for proper
|
|
582
|
+
// node resolution. The meta-cursor bootstrap is the primary consumer.
|
|
583
|
+
heading('Installing launcher (--install-launcher) ...');
|
|
584
|
+
try {
|
|
585
|
+
const clientDir = join(homedir(), '.ai-lens', 'client');
|
|
586
|
+
mkdirSync(clientDir, { recursive: true });
|
|
587
|
+
writeLauncher({ clientDir, nodePath: nodeResolution.path, platform: process.platform });
|
|
588
|
+
success(' Wrote per-machine launcher to ~/.ai-lens/client/');
|
|
589
|
+
} catch (err) {
|
|
590
|
+
error(` Failed to write launcher: ${err.message}`);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
567
593
|
} else {
|
|
568
594
|
detail(' Skipping client install (--use-repo-path: using package copy).');
|
|
569
595
|
}
|
package/cli/status.js
CHANGED
|
@@ -725,8 +725,13 @@ function checkCaptureLog() {
|
|
|
725
725
|
return { ok: false, summary: `error reading log: ${err.message}`, detail: `Error: ${err.message}` };
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
-
// Count entries by category (reason for drops, msg for errors)
|
|
728
|
+
// Count entries by category (reason for drops, msg for errors). For error
|
|
729
|
+
// entries that carry an `error.code` (spawn/queue failures), also tally the
|
|
730
|
+
// code distribution per category so it reaches the server via client_reports
|
|
731
|
+
// — the count alone can't tell EMFILE from EACCES from ENOENT. The raw
|
|
732
|
+
// error.message strings stay local (may contain paths); only the code travels.
|
|
729
733
|
const counts = {};
|
|
734
|
+
const codesByCategory = {};
|
|
730
735
|
let lastTs = null;
|
|
731
736
|
let hasErrors = false;
|
|
732
737
|
for (const line of lines) {
|
|
@@ -734,24 +739,38 @@ function checkCaptureLog() {
|
|
|
734
739
|
const entry = JSON.parse(line);
|
|
735
740
|
const category = entry.reason || entry.msg || 'unknown';
|
|
736
741
|
counts[category] = (counts[category] || 0) + 1;
|
|
742
|
+
if (entry.code) {
|
|
743
|
+
(codesByCategory[category] ??= {});
|
|
744
|
+
codesByCategory[category][entry.code] = (codesByCategory[category][entry.code] || 0) + 1;
|
|
745
|
+
}
|
|
737
746
|
lastTs = entry.ts;
|
|
738
747
|
if (entry.msg) hasErrors = true;
|
|
739
748
|
} catch { /* non-JSON line */ }
|
|
740
749
|
}
|
|
741
750
|
|
|
742
751
|
const total = lines.length;
|
|
743
|
-
const breakdown = Object.entries(counts).map(([r, n]) =>
|
|
752
|
+
const breakdown = Object.entries(counts).map(([r, n]) => {
|
|
753
|
+
const codes = codesByCategory[r];
|
|
754
|
+
if (!codes) return `${r}: ${n}`;
|
|
755
|
+
const codeStr = Object.entries(codes).map(([c, cn]) => `${c}: ${cn}`).join(', ');
|
|
756
|
+
return `${r}: ${n} [${codeStr}]`;
|
|
757
|
+
}).join(', ');
|
|
744
758
|
|
|
745
759
|
let summary = `${total} entries`;
|
|
746
760
|
if (breakdown) summary += ` (${breakdown})`;
|
|
747
761
|
if (lastTs) summary += `, last ${relativeTime(lastTs)}`;
|
|
748
762
|
|
|
763
|
+
// Include a compact code-distribution block in detail too — survives even if
|
|
764
|
+
// the "last 10 entries" happen to be benign project_filter lines.
|
|
765
|
+
const codeLines = Object.entries(codesByCategory)
|
|
766
|
+
.map(([cat, codes]) => ` ${cat}: ${Object.entries(codes).map(([c, n]) => `${c}=${n}`).join(', ')}`);
|
|
767
|
+
const codeBlock = codeLines.length ? `\n\nError codes by category:\n${codeLines.join('\n')}` : '';
|
|
749
768
|
const last10 = lines.slice(-10);
|
|
750
769
|
|
|
751
770
|
return {
|
|
752
771
|
ok: !hasErrors,
|
|
753
772
|
summary,
|
|
754
|
-
detail: `Log: ${CAPTURE_LOG_PATH}\nTotal: ${total}\n\nLast 10 entries:\n${last10.join('\n')}`,
|
|
773
|
+
detail: `Log: ${CAPTURE_LOG_PATH}\nTotal: ${total}${codeBlock}\n\nLast 10 entries:\n${last10.join('\n')}`,
|
|
755
774
|
};
|
|
756
775
|
}
|
|
757
776
|
|
package/client/capture.js
CHANGED
|
@@ -1263,7 +1263,7 @@ async function main() {
|
|
|
1263
1263
|
writeToSpool(ev);
|
|
1264
1264
|
if (ev === primary) primaryWritten = true;
|
|
1265
1265
|
} catch (err) {
|
|
1266
|
-
captureLog({ msg: 'queue-write-failed', error: err.message, type: ev.type, session_id: ev.session_id });
|
|
1266
|
+
captureLog({ msg: 'queue-write-failed', error: err.message, code: err.code, type: ev.type, session_id: ev.session_id });
|
|
1267
1267
|
// If the primary failed, propagate the failure so the hook exits non-zero
|
|
1268
1268
|
// (and dedup is NOT committed). If a per-call event failed, log and keep
|
|
1269
1269
|
// going — losing one TokenUsage row is better than dropping the whole
|
|
@@ -1312,14 +1312,14 @@ async function main() {
|
|
|
1312
1312
|
try {
|
|
1313
1313
|
trySpawnSender();
|
|
1314
1314
|
} catch (err) {
|
|
1315
|
-
captureLog({ msg: 'sender-spawn-failed', error: err.message });
|
|
1315
|
+
captureLog({ msg: 'sender-spawn-failed', error: err.message, code: err.code });
|
|
1316
1316
|
// event is queued — sender will be spawned on next capture
|
|
1317
1317
|
}
|
|
1318
1318
|
|
|
1319
1319
|
try {
|
|
1320
1320
|
trySpawnCodexWatcher({ replayExisting: shouldReplayCodexHistory(primary) });
|
|
1321
1321
|
} catch (err) {
|
|
1322
|
-
captureLog({ msg: 'codex-watcher-spawn-failed', error: err.message });
|
|
1322
|
+
captureLog({ msg: 'codex-watcher-spawn-failed', error: err.message, code: err.code });
|
|
1323
1323
|
}
|
|
1324
1324
|
}
|
|
1325
1325
|
|