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 +1 -1
- package/CHANGELOG.md +5 -0
- package/cli/status.js +126 -30
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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 +=
|
|
682
|
+
if (hasErrors) summary += recovered ? `, recovered — ${errNote}` : `, ${errNote}`;
|
|
654
683
|
} else if (hasErrors) {
|
|
655
|
-
summary =
|
|
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: !
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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:
|
|
827
|
-
summary: `
|
|
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
|
-
|
|
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);
|