ai-lens 0.8.80 → 0.8.81

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
- a6ad00e
1
+ 0533332
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
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.81 — 2026-06-05
6
+ - improve: `ai-lens status` now tells a stale error from an active one — a past send failure no longer shows red once sending has recovered, and the time and code of the last error are shown (e.g. "recovered — last error 1d ago (ECONNRESET)").
7
+ - improve: `ai-lens status` retries the server health check a few times before reporting "unreachable", so a one-off network blip no longer flags the server as down.
8
+ - improve: `ai-lens status --report` now includes your config and hook files (Cursor / Claude Code / Codex, both global and project scope), with the auth token masked, so setup issues can be diagnosed remotely without sending your local status file.
9
+
5
10
  ## 0.8.80 — 2026-06-04
6
11
  - fix: `ai-lens import claude-code` now caps long prompts (so a huge pasted prompt is no longer dropped by the server), captures prompts from messages that mix text with images/documents (shown as a safe `[N attachment(s)]` placeholder — never the image data), and avoids a stale session "end" time on transcripts that are still being written.
7
12
 
package/cli/status.js CHANGED
@@ -619,40 +619,69 @@ function checkSenderLog() {
619
619
  return { ok: false, summary: `error reading log: ${err.message}`, detail: `Error: ${err.message}` };
620
620
  }
621
621
 
622
- // Aggregate across all log entries for observability
622
+ // Aggregate across all log entries for observability. Counts are all-time, but
623
+ // we also track recency (last error ts + a 24h window) so a single old failure
624
+ // doesn't flag the check red forever — see `recovered` below.
623
625
  let sentOk = 0; // total events successfully delivered to server
624
626
  let failedCount = 0; // total failed send attempts (connection/auth errors)
627
+ let recentFailed = 0; // failed sends within the last 24h
625
628
  let rollbackCount = 0; // total rollbacks (partial + full) — events re-queued for retry
626
629
  let lastSend = null;
630
+ let lastSendMs = 0;
631
+ let lastError = null; // ts of the most recent failure/error
632
+ let lastErrorMs = 0;
633
+ let lastErrorCode = null; // connection/error code of that last failure
627
634
  let hasErrors = false;
635
+ const DAY_MS = 24 * 60 * 60 * 1000;
636
+ const now = Date.now();
628
637
 
629
638
  for (const line of lines) {
630
639
  try {
631
640
  const entry = JSON.parse(line);
641
+ const tsMs = entry.ts ? new Date(entry.ts).getTime() : NaN;
632
642
  if (entry.msg === 'sent') {
633
643
  lastSend = entry.ts;
644
+ if (!isNaN(tsMs) && tsMs > lastSendMs) lastSendMs = tsMs;
634
645
  sentOk += parseInt(entry.events, 10) || 0;
635
646
  }
636
647
  if (entry.msg === 'failed' || entry.msg === 'error' || entry.msg === 'auth-failed') {
637
648
  hasErrors = true;
638
649
  if (entry.msg === 'failed') failedCount++;
650
+ if (!isNaN(tsMs)) {
651
+ if (tsMs > lastErrorMs) {
652
+ lastErrorMs = tsMs;
653
+ lastError = entry.ts;
654
+ lastErrorCode = entry.cause?.code || entry.code || entry.error || null;
655
+ }
656
+ if (entry.msg === 'failed' && now - tsMs <= DAY_MS) recentFailed++;
657
+ }
639
658
  }
640
659
  if (entry.msg === 'rollback' || entry.msg === 'partial-rollback') rollbackCount++;
641
660
  } catch { /* non-JSON line */ }
642
661
  }
643
662
 
663
+ // Recovered = the last successful send is newer than the last error, so the
664
+ // failures are historical noise rather than an active problem. Only treat
665
+ // errors as active (red) when nothing has succeeded since the last one.
666
+ const recovered = lastErrorMs > 0 && lastSendMs > lastErrorMs;
667
+ const activeErrors = hasErrors && !recovered;
668
+
644
669
  const stats = [];
645
670
  if (sentOk > 0) stats.push(`${sentOk} events sent`);
646
- if (failedCount > 0) stats.push(`${failedCount} failed`);
671
+ if (failedCount > 0) stats.push(`${failedCount} failed${recentFailed > 0 ? `, ${recentFailed} in 24h` : ''}`);
647
672
  if (rollbackCount > 0) stats.push(`${rollbackCount} rollbacks`);
648
673
 
674
+ const errNote = lastError
675
+ ? `last error ${relativeTime(lastError)}${lastErrorCode ? ` (${lastErrorCode})` : ''}`
676
+ : 'has errors';
677
+
649
678
  let summary;
650
679
  if (lastSend) {
651
680
  summary = `last send ${relativeTime(lastSend)}`;
652
681
  if (stats.length > 0) summary += ` (${stats.join(', ')})`;
653
- if (hasErrors) summary += ', has errors';
682
+ if (hasErrors) summary += recovered ? `, recovered — ${errNote}` : `, ${errNote}`;
654
683
  } else if (hasErrors) {
655
- summary = 'errors in log';
684
+ summary = `errors in log — ${errNote}`;
656
685
  if (stats.length > 0) summary += ` (${stats.join(', ')})`;
657
686
  } else {
658
687
  summary = `${lines.length} entries`;
@@ -660,10 +689,11 @@ function checkSenderLog() {
660
689
  }
661
690
 
662
691
  const last20 = lines.slice(-20);
692
+ const state = activeErrors ? 'ACTIVE ERRORS' : recovered ? 'recovered' : 'healthy';
663
693
  return {
664
- ok: !hasErrors,
694
+ ok: !activeErrors,
665
695
  summary,
666
- detail: `Log: ${LOG_PATH}\nTotal entries: ${lines.length}\nCounters: sent_ok=${sentOk}, failed=${failedCount}, rollbacks=${rollbackCount}\nLast 20 entries:\n${last20.join('\n')}`,
696
+ detail: `Log: ${LOG_PATH}\nTotal entries: ${lines.length}\nCounters: sent_ok=${sentOk}, failed=${failedCount} (${recentFailed} in 24h), rollbacks=${rollbackCount}\nLast send: ${lastSend || '(never)'}\nLast error: ${lastError || '(none)'}${lastErrorCode ? ` (${lastErrorCode})` : ''}\nState: ${state}\nLast 20 entries:\n${last20.join('\n')}`,
667
697
  };
668
698
  }
669
699
 
@@ -816,31 +846,46 @@ async function checkServer(serverUrl) {
816
846
  }
817
847
 
818
848
  const url = `${serverUrl}/api/health`;
819
- const start = Date.now();
820
- try {
821
- const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
822
- const latency = Date.now() - start;
823
- const body = await res.text();
824
- if (res.ok) {
849
+ // A single probe times out on a transient network blip (VPN reconnect, brief
850
+ // packet loss) and flags the server red even when it's actually fine. Retry a
851
+ // few times with a short backoff before the verdict; an HTTP response (even an
852
+ // error status) short-circuits — the server answered, no point retrying.
853
+ const ATTEMPTS = 3;
854
+ const PER_ATTEMPT_TIMEOUT = 5000;
855
+ const codes = [];
856
+ const attemptLog = [];
857
+ for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
858
+ const start = Date.now();
859
+ try {
860
+ const res = await fetch(url, { signal: AbortSignal.timeout(PER_ATTEMPT_TIMEOUT) });
861
+ const latency = Date.now() - start;
862
+ const body = await res.text();
863
+ if (res.ok) {
864
+ const note = attempt > 1 ? `, ${attempt}/${ATTEMPTS} attempts` : '';
865
+ return {
866
+ ok: true,
867
+ summary: `reachable (${latency}ms${note})`,
868
+ detail: `URL: ${url}\nStatus: ${res.status}\nLatency: ${latency}ms\nAttempt: ${attempt}/${ATTEMPTS}\nResponse: ${body}`,
869
+ };
870
+ }
825
871
  return {
826
- ok: true,
827
- summary: `reachable (${latency}ms)`,
872
+ ok: false,
873
+ summary: `HTTP ${res.status} (${latency}ms)`,
828
874
  detail: `URL: ${url}\nStatus: ${res.status}\nLatency: ${latency}ms\nResponse: ${body}`,
829
875
  };
876
+ } catch (err) {
877
+ const latency = Date.now() - start;
878
+ const code = err.code || err.cause?.code || err.message;
879
+ codes.push(code);
880
+ attemptLog.push(`#${attempt}: ${code} (${latency}ms)`);
881
+ if (attempt < ATTEMPTS) await new Promise(r => setTimeout(r, 1000));
830
882
  }
831
- return {
832
- ok: false,
833
- summary: `HTTP ${res.status} (${latency}ms)`,
834
- detail: `URL: ${url}\nStatus: ${res.status}\nLatency: ${latency}ms\nResponse: ${body}`,
835
- };
836
- } catch (err) {
837
- const latency = Date.now() - start;
838
- return {
839
- ok: false,
840
- summary: `unreachable (${err.code || err.cause?.code || err.message})`,
841
- detail: `URL: ${url}\nError: ${err.message}\nLatency: ${latency}ms`,
842
- };
843
883
  }
884
+ return {
885
+ ok: false,
886
+ summary: `unreachable (${codes[codes.length - 1]}, ${ATTEMPTS} attempts)`,
887
+ detail: `URL: ${url}\nUnreachable after ${ATTEMPTS} attempts:\n${attemptLog.join('\n')}`,
888
+ };
844
889
  }
845
890
 
846
891
  async function checkToken(serverUrl, authToken) {
@@ -1097,7 +1142,50 @@ function buildReport(results, timestamp, warnings = [], allTools = TOOL_CONFIGS)
1097
1142
  // Report mode: POST structured status to server
1098
1143
  // ---------------------------------------------------------------------------
1099
1144
 
1100
- async function sendStatusReport(results, warnings, clientVersion, clientCommit, serverUrl, authToken) {
1145
+ // AI Lens config (~/.ai-lens/config.json) with the auth token masked, or null if
1146
+ // absent/unreadable. Mirrors the masked dump buildReport writes to the local file.
1147
+ function maskedAiLensConfig() {
1148
+ try {
1149
+ const raw = readFileSync(join(homedir(), '.ai-lens', 'config.json'), 'utf-8');
1150
+ const config = JSON.parse(raw);
1151
+ if (config.authToken) config.authToken = maskToken(config.authToken);
1152
+ return config;
1153
+ } catch {
1154
+ return null;
1155
+ }
1156
+ }
1157
+
1158
+ // Hook-config file contents for every tool (Cursor/Claude Code/Codex), both
1159
+ // global and project scope, so a single remote report is enough to diagnose hook
1160
+ // wiring (actual command form, scope, $CLAUDE_PROJECT_DIR vs %VAR%, missing file)
1161
+ // without asking the developer for their local ~/ai-lens-status.txt.
1162
+ function collectHookConfigs(allTools) {
1163
+ const entries = [];
1164
+ const readHooks = (tool, scope, path) => {
1165
+ let hooks = null, error = null;
1166
+ try {
1167
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
1168
+ hooks = parsed.hooks ?? null;
1169
+ if (hooks === null) error = 'no hooks section';
1170
+ } catch (err) {
1171
+ error = err.code === 'ENOENT' ? 'not found' : err.message;
1172
+ }
1173
+ entries.push({ tool: tool.name, scope, path, hooks, error });
1174
+ };
1175
+ for (const tool of allTools) {
1176
+ const scope = TOOL_CONFIGS.includes(tool) ? 'global' : 'project';
1177
+ readHooks(tool, scope, tool.configPath);
1178
+ // Claude Code keeps a separate settings.local.json (personal/project override)
1179
+ // that can carry or disable hooks — capture it when present.
1180
+ if (tool.name.startsWith('Claude Code') && tool.dirPath) {
1181
+ const localPath = join(tool.dirPath, 'settings.local.json');
1182
+ if (existsSync(localPath)) readHooks(tool, `${scope}-local`, localPath);
1183
+ }
1184
+ }
1185
+ return entries;
1186
+ }
1187
+
1188
+ async function sendStatusReport(results, warnings, clientVersion, clientCommit, serverUrl, authToken, allTools = TOOL_CONFIGS) {
1101
1189
  if (!serverUrl || !authToken) return;
1102
1190
 
1103
1191
  const payload = {
@@ -1108,6 +1196,12 @@ async function sendStatusReport(results, warnings, clientVersion, clientCommit,
1108
1196
  os: `${process.platform} ${osRelease()} ${osArch()}`,
1109
1197
  checks: results.map(({ label, ok, summary, detail }) => ({ label, ok, summary, detail })),
1110
1198
  warnings: warnings.map(({ msg, action }) => ({ msg, action })),
1199
+ // Config + hook dumps (token masked) — the local-file-only sections, now also
1200
+ // sent so remote triage doesn't need the developer's ~/ai-lens-status.txt.
1201
+ configs: {
1202
+ ai_lens: maskedAiLensConfig(),
1203
+ hooks: collectHookConfigs(allTools),
1204
+ },
1111
1205
  };
1112
1206
 
1113
1207
  try {
@@ -1332,16 +1426,18 @@ export default async function status({ report = false } = {}) {
1332
1426
  }
1333
1427
  }
1334
1428
 
1429
+ // Global TOOL_CONFIGS (always listed, even if not installed) + project tools.
1430
+ // Used by both report modes: the server JSON config dump and the local text file.
1431
+ const allToolsForReport = [...TOOL_CONFIGS, ...toolsWithProject.filter(t => !TOOL_CONFIGS.includes(t))];
1432
+
1335
1433
  if (report) {
1336
1434
  // --report mode: same on-screen output as normal status, but POST the
1337
1435
  // structured JSON to the server instead of writing the local text file.
1338
- await sendStatusReport(results, warnings, version, commit, serverUrl, authToken);
1436
+ await sendStatusReport(results, warnings, version, commit, serverUrl, authToken, allToolsForReport);
1339
1437
  blank();
1340
1438
  } else {
1341
1439
  // Normal mode: write text report file
1342
1440
  const timestamp = new Date().toISOString();
1343
- // Merge global TOOL_CONFIGS (always listed, even if not installed) with project tools
1344
- const allToolsForReport = [...TOOL_CONFIGS, ...toolsWithProject.filter(t => !TOOL_CONFIGS.includes(t))];
1345
1441
  const reportText = buildReport(results, timestamp, warnings, allToolsForReport);
1346
1442
  try {
1347
1443
  writeFileSync(REPORT_PATH, reportText);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-lens",
3
- "version": "0.8.80",
3
+ "version": "0.8.81",
4
4
  "type": "module",
5
5
  "description": "Centralized session analytics for AI coding tools",
6
6
  "bin": {