ai-lens 0.8.94 → 0.8.96

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
- 1b043cc
1
+ 549ce90
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.96 — 2026-06-17
6
+ - 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
+
8
+ ## 0.8.95 — 2026-06-17
9
+ - fix: on Windows, Cursor hooks no longer flash a console window on every event (including new-session start) — the hook command is now wrapped in `conhost.exe --headless` (Windows 10 1809+), the same windowless form Claude Code hooks already use. Older Windows builds and macOS/Linux are unchanged. Re-run `ai-lens init` to apply
10
+ - feat: `ai-lens init` now warns when the configured `projects` filter does not cover the workspace you're setting up — a stale filter silently drops all capture (hooks look configured but record nothing)
11
+ - feat: the Claude Code history-import offer now previews how much there is to import ("Found N sessions from A to B") before asking, instead of a bare yes/no
12
+
5
13
  ## 0.8.94 — 2026-06-16
6
14
  - feat: capture 8 more Claude Code events that were previously ignored — turn failures (session-limit / prompt-too-long / server-overload messages), notifications, permission requests, post-compact summaries, task created/completed, instructions loaded, and slash-command expansions. Re-run `ai-lens init` to install them; they're collected and forwarded only (no dashboard changes yet)
7
15
 
package/cli/hooks.js CHANGED
@@ -375,9 +375,13 @@ 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
+ // windowless: on Windows ≥1809 wrap node in `conhost.exe --headless` so Cursor
379
+ // doesn't flash a console window on every event (incl. session start). `& ` still
380
+ // works in front of conhost; the detector strips `& ` then the conhost prefix, so
381
+ // the form stays recognised as current (no churn). Mac/old-Windows: unchanged.
378
382
  const cmd = customPath != null
379
- ? captureCommand({ useTilde: false, rawPath: true, customPath, ctx, shell })
380
- : captureCommand({ useTilde: effectiveUseTilde, ctx, shell });
383
+ ? captureCommand({ rawPath: true, customPath, windowless: true, ctx, shell })
384
+ : captureCommand({ useTilde: effectiveUseTilde, windowless: true, ctx, shell });
381
385
  return platform === 'win32' ? `& ${cmd}` : cmd;
382
386
  }
383
387
 
@@ -1084,9 +1088,26 @@ function isCurrentAiLensHook(entry, expected, opts = {}) {
1084
1088
  // Exception (allowPlatformRewrite, set for untracked/per-machine files): a
1085
1089
  // $CLAUDE_PROJECT_DIR/%CLAUDE_PROJECT_DIR% hook written for the OTHER OS won't
1086
1090
  // expand on this platform, so flag it outdated to let init rewrite it.
1091
+ //
1092
+ // Windowless exception (same allowPlatformRewrite gate): when the freshly
1093
+ // regenerated `expected` command carries the `conhost.exe --headless` wrapper
1094
+ // (Cursor/Claude on conhost-capable Windows) but the stored command does NOT,
1095
+ // flag it outdated so init upgrades it — otherwise a pre-conhost install stays
1096
+ // `current` forever and keeps flashing a console window on every event. We
1097
+ // compare ONLY the windowless dimension (not the whole command), so legitimate
1098
+ // path/node/install-mode variation never false-flags. `expected` already bakes
1099
+ // in tool+platform+conhost-support (makeCursorHookDefs/makeClaudeHookDefs pass
1100
+ // windowless:true; makeCodexHookDefs does not), so this self-scopes: Codex,
1101
+ // macOS, and old Windows produce a non-conhost `expected` and never trip it.
1102
+ // Committed (tracked) files are exempt via allowPlatformRewrite — they carry one
1103
+ // OS-agnostic syntax and can't bake a Windows-only wrapper; the per-machine
1104
+ // overlay provides the windowless path.
1087
1105
  const { platform = process.platform, allowPlatformRewrite = false } = opts;
1106
+ const expectedCmd = expected?.command ?? expected?.hooks?.[0]?.command ?? '';
1107
+ const expectedWindowless = isConhostHeadlessCommand(expectedCmd);
1088
1108
  const ok = (cmd) => isAcceptableHookCommand(cmd)
1089
- && !(allowPlatformRewrite && isWrongPlatformProjectDirCommand(cmd, platform));
1109
+ && !(allowPlatformRewrite && isWrongPlatformProjectDirCommand(cmd, platform))
1110
+ && !(allowPlatformRewrite && expectedWindowless && !isConhostHeadlessCommand(cmd));
1090
1111
  // Flat format (Cursor): single command per entry.
1091
1112
  if (entry?.command != null) {
1092
1113
  return ok(entry.command);
@@ -92,6 +92,31 @@ export function resolveCutoff({ days, since, from }, now = new Date()) {
92
92
  return new Date(now.getTime() - d * DAY_MS).toISOString();
93
93
  }
94
94
 
95
+ /**
96
+ * Cheap preview for the init import offer — how many local transcripts (≈ sessions)
97
+ * fall within the window and their date span, by file mtime (no file reads). Lets
98
+ * init show "Found N sessions from A to B" before asking to import.
99
+ * @returns {{ count: number, earliest: string|null, latest: string|null }}
100
+ */
101
+ export function previewClaudeCode({ days = 90, dir = PROJECTS_DIR } = {}, now = new Date()) {
102
+ if (!existsSync(dir)) return { count: 0, earliest: null, latest: null };
103
+ const cutoffMs = Date.parse(resolveCutoff({ days }, now));
104
+ let count = 0, min = Infinity, max = -Infinity;
105
+ for (const f of walkJsonl(dir)) {
106
+ let m;
107
+ try { m = statSync(f).mtimeMs; } catch { continue; }
108
+ if (m < cutoffMs) continue;
109
+ count++;
110
+ if (m < min) min = m;
111
+ if (m > max) max = m;
112
+ }
113
+ return {
114
+ count,
115
+ earliest: count ? new Date(min).toISOString() : null,
116
+ latest: count ? new Date(max).toISOString() : null,
117
+ };
118
+ }
119
+
95
120
  /**
96
121
  * Resolve the INCLUSIVE lower bound from `--from` (or null when open-ended).
97
122
  * Unlike the file-level `cutoff` (which only decides whether to read a file),
package/cli/init.js CHANGED
@@ -596,6 +596,27 @@ export default async function init() {
596
596
  info(' Tracking: all projects');
597
597
  }
598
598
 
599
+ // Loud guard: a narrow `projects` filter that does NOT cover this workspace silently
600
+ // drops ALL capture (project_filter) — hooks look configured but record nothing. This
601
+ // bites when init inherits a stale `projects` from an old config (a leftover test path).
602
+ // Warn against the cwd (the workspace init is being run from).
603
+ if (projects) {
604
+ const cwd = resolve(process.cwd());
605
+ const norm = (p) => p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
606
+ const cwdN = norm(cwd);
607
+ const covered = projects.split(',').map(norm).some(m => m && (cwdN === m || cwdN.startsWith(m + '/')));
608
+ if (!covered) {
609
+ blank();
610
+ warn(` ⚠ Projects filter does not include this workspace — events from here will be DROPPED.`);
611
+ warn(` filter: ${projects}`);
612
+ warn(` here: ${cwd}`);
613
+ info(` Capture will look configured but record nothing. Fix: re-run with \`--projects all\``);
614
+ info(` (track everything), or edit ~/.ai-lens/config.json and remove/adjust "projects".`);
615
+ info(` (Ignore if you deliberately track only other paths.)`);
616
+ blank();
617
+ }
618
+ }
619
+
599
620
  // Build new config in memory — saved after "Proceed?" confirmation
600
621
  const newConfig = { ...currentConfig, serverUrl, projects };
601
622
 
@@ -1256,10 +1277,28 @@ async function maybeOfferImportHistory(flags) {
1256
1277
  if (flags.noImport) return;
1257
1278
  if (!existsSync(join(homedir(), '.claude', 'projects'))) return;
1258
1279
 
1280
+ // Preview so the offer isn't a blind yes/no: count of local transcripts (≈ sessions)
1281
+ // and their date span within the 90d window (cheap mtime scan, no file reads).
1282
+ let preview = null;
1283
+ try {
1284
+ const mod = await import('./import/claude-code.js');
1285
+ if (mod.previewClaudeCode) preview = mod.previewClaudeCode({ days: 90 });
1286
+ } catch { /* preview is best-effort */ }
1287
+ if (preview && preview.count === 0) {
1288
+ info(' No local Claude Code history in the last 90 days — nothing to import.');
1289
+ return;
1290
+ }
1291
+ const day = (iso) => (iso || '').slice(0, 10);
1292
+ const previewLine = preview
1293
+ ? `Found ${preview.count} Claude Code session${preview.count === 1 ? '' : 's'} from ${day(preview.earliest)} to ${day(preview.latest)} (last 90d).`
1294
+ : 'Local Claude Code history found.';
1295
+
1259
1296
  let run = flags.importHistory || flags.yes;
1260
- if (!run) {
1297
+ if (run) {
1298
+ info(` ${previewLine}`);
1299
+ } else {
1261
1300
  try {
1262
- const answer = (await ask('Import your local Claude Code history now? (Y/n) ')).toLowerCase();
1301
+ const answer = (await ask(`${previewLine} Import now? (Y/n) `)).toLowerCase();
1263
1302
  run = answer === '' || answer === 'y' || answer === 'yes';
1264
1303
  } catch { run = false; }
1265
1304
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.94",
3
+ "version": "0.8.96",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {