ai-lens 0.8.111 → 0.8.113
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 +5 -0
- package/cli/init.js +28 -1
- package/client/capture.js +79 -4
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
f10ad46
|
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.113 — 2026-06-24
|
|
6
|
+
- feat: track working-directory changes mid-session (Claude Code `CwdChanged`) so events are attributed to the project you `cd` into, not the one you started in
|
|
7
|
+
- feat: capture the assistant's response text and thinking from the Claude Code transcript, not just token counts — the full turn-by-turn dialogue is now recorded
|
|
8
|
+
|
|
9
|
+
## 0.8.112 — 2026-06-23
|
|
10
|
+
- feat: re-running `init --yes` no longer changes your MCP registration. On an already-set-up install it's left exactly as-is — if MCP is registered it stays (same scope), if it isn't it's left off — instead of being removed-and-re-added at user scope (which could migrate its scope, force it on, or drop it if the re-add failed). Fresh installs still register MCP; use `init --mcp-only` to deliberately (re)register.
|
|
11
|
+
|
|
5
12
|
## 0.8.111 — 2026-06-23
|
|
6
13
|
- fix(status): case-fold project-filter-mismatch buckets on Windows
|
|
7
14
|
- fix(client): tolerate Cursor Windows mojibake paths in project_filter
|
package/cli/hooks.js
CHANGED
|
@@ -502,6 +502,11 @@ const CLAUDE_HOOK_SPEC = {
|
|
|
502
502
|
TaskCompleted: { matcher: '' },
|
|
503
503
|
InstructionsLoaded: { matcher: '' },
|
|
504
504
|
UserPromptExpansion: { matcher: '' },
|
|
505
|
+
// Project attribution: fires when the session's cwd changes mid-flight.
|
|
506
|
+
// capture.js updates the session→project cache from new_cwd so subsequent
|
|
507
|
+
// events attribute to the right project (cwd otherwise only arrives at
|
|
508
|
+
// SessionStart). Metric-neutral server-side (OBSERVABILITY_EVENT_TYPES).
|
|
509
|
+
CwdChanged: { matcher: '' },
|
|
505
510
|
};
|
|
506
511
|
|
|
507
512
|
const CURSOR_HOOK_NAMES = [
|
package/cli/init.js
CHANGED
|
@@ -66,6 +66,24 @@ export function importMode(flags = {}) {
|
|
|
66
66
|
return 'prompt'; // interactive: ask
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// How the FULL init flow should handle the MCP registration. Returns:
|
|
70
|
+
// 'skip' — never touch MCP (today's --no-mcp behavior; existing reg left intact)
|
|
71
|
+
// 'preserve' — existing install under --yes: leave MCP exactly as-is (no add/remove/migrate)
|
|
72
|
+
// 'setup' — fresh install under --yes, or interactive: register (onboarding / prompt)
|
|
73
|
+
//
|
|
74
|
+
// On an EXISTING install under --yes, MCP is ALWAYS preserved — neither --project-hooks
|
|
75
|
+
// nor --mcp-scope re-registers it. This stops `setupMcpServers` (which removes at all
|
|
76
|
+
// scopes then re-adds at one) from migrating an existing local/project registration to
|
|
77
|
+
// `user`, force-adding MCP for someone who never had it, or leaving MCP removed-but-not-
|
|
78
|
+
// re-added when the `claude mcp add` fails. The only explicit (re)register path is the
|
|
79
|
+
// separate `--mcp-only` branch, which never reaches this gate.
|
|
80
|
+
// "Existing install" = a prior auth token in config (read before this run authenticates).
|
|
81
|
+
export function mcpSetupAction({ noMcp, auto, hasToken } = {}) {
|
|
82
|
+
if (noMcp) return 'skip';
|
|
83
|
+
if (auto && hasToken) return 'preserve';
|
|
84
|
+
return 'setup';
|
|
85
|
+
}
|
|
86
|
+
|
|
69
87
|
function getJson(url) {
|
|
70
88
|
return new Promise((resolve, reject) => {
|
|
71
89
|
const parsed = new URL(url);
|
|
@@ -1305,14 +1323,23 @@ export default async function init() {
|
|
|
1305
1323
|
// --project-hooks installs hooks at project scope, so mirror that for the MCP:
|
|
1306
1324
|
// default the scope to local and target the project .cursor/mcp.json (an explicit
|
|
1307
1325
|
// --mcp-scope still wins; setupMcpServers maps scope → Cursor target).
|
|
1308
|
-
|
|
1326
|
+
const mcpAction = mcpSetupAction({
|
|
1327
|
+
noMcp: flags.noMcp,
|
|
1328
|
+
auto,
|
|
1329
|
+
hasToken: Boolean(currentConfig.authToken),
|
|
1330
|
+
});
|
|
1331
|
+
if (mcpAction === 'setup') {
|
|
1309
1332
|
await setupMcpServers(serverUrl, {
|
|
1310
1333
|
auto,
|
|
1311
1334
|
mcpScope: flags.mcpScope,
|
|
1312
1335
|
forcedScope: (flags.projectHooks && !flags.mcpScope) ? 'local' : null,
|
|
1313
1336
|
projectRoot: flags.projectHooks ? resolve(process.cwd()) : null,
|
|
1314
1337
|
});
|
|
1338
|
+
} else if (mcpAction === 'preserve') {
|
|
1339
|
+
// Existing install under --yes: leave the current MCP registration untouched.
|
|
1340
|
+
info(' MCP: сохранён как есть (для (пере)регистрации — npx -y ai-lens init --mcp-only)');
|
|
1315
1341
|
}
|
|
1342
|
+
// 'skip' (--no-mcp) → do nothing; an existing registration is left intact.
|
|
1316
1343
|
|
|
1317
1344
|
// Quick verification
|
|
1318
1345
|
heading('Verification');
|
package/client/capture.js
CHANGED
|
@@ -419,6 +419,24 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
|
|
|
419
419
|
if (!model || model === '<synthetic>') continue;
|
|
420
420
|
const usage = message.usage;
|
|
421
421
|
if (!usage || typeof usage !== 'object') continue;
|
|
422
|
+
// Also lift the assistant's own text + thinking blocks from this call's
|
|
423
|
+
// content. They live ONLY in the transcript (no hook payload carries the
|
|
424
|
+
// intermediate assistant text between tool calls), so the caller emits
|
|
425
|
+
// them as AssistantText / AssistantThinking content events alongside
|
|
426
|
+
// TokenUsage — closing the one dialogue gap. Concatenate multiple blocks
|
|
427
|
+
// of the same kind in order.
|
|
428
|
+
let text = '';
|
|
429
|
+
let thinking = '';
|
|
430
|
+
if (Array.isArray(message.content)) {
|
|
431
|
+
for (const block of message.content) {
|
|
432
|
+
if (!block || typeof block !== 'object') continue;
|
|
433
|
+
if (block.type === 'text' && typeof block.text === 'string' && block.text) {
|
|
434
|
+
text += (text ? '\n' : '') + block.text;
|
|
435
|
+
} else if (block.type === 'thinking' && typeof block.thinking === 'string' && block.thinking) {
|
|
436
|
+
thinking += (thinking ? '\n' : '') + block.thinking;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
422
440
|
// Capture each call as its own entry — the caller emits one unified
|
|
423
441
|
// TokenUsage event per entry, matching Cursor/Codex per-call granularity.
|
|
424
442
|
calls.push({
|
|
@@ -431,6 +449,8 @@ function extractNewClaudeApiCallsFromTranscript(transcriptPath) {
|
|
|
431
449
|
model,
|
|
432
450
|
timestamp: typeof parsed.timestamp === 'string' ? parsed.timestamp : null,
|
|
433
451
|
uuid: typeof parsed.uuid === 'string' ? parsed.uuid : null,
|
|
452
|
+
text: text || null,
|
|
453
|
+
thinking: thinking || null,
|
|
434
454
|
});
|
|
435
455
|
}
|
|
436
456
|
} catch (err) {
|
|
@@ -751,6 +771,22 @@ function normalizeClaudeCode(event) {
|
|
|
751
771
|
case 'SessionStart':
|
|
752
772
|
data = { cwd: event.cwd };
|
|
753
773
|
break;
|
|
774
|
+
case 'CwdChanged': {
|
|
775
|
+
// The session's working dir changed mid-flight. cwd otherwise only
|
|
776
|
+
// arrives at SessionStart, so refresh the session→project cache from
|
|
777
|
+
// new_cwd and attribute THIS event to the new project. Canonicalizes
|
|
778
|
+
// worktree checkouts to the main repo root like SessionStart does.
|
|
779
|
+
const newCwd = event.new_cwd || event.cwd || null;
|
|
780
|
+
data = { old_cwd: event.old_cwd || null, new_cwd: newCwd };
|
|
781
|
+
if (newCwd) {
|
|
782
|
+
const newPath = canonicalizeProjectPath(newCwd);
|
|
783
|
+
if (newPath) {
|
|
784
|
+
projectPath = newPath;
|
|
785
|
+
if (sessionId) cacheSessionPath(sessionId, newPath);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
754
790
|
case 'UserPromptSubmit':
|
|
755
791
|
data = { prompt: truncate(event.prompt || '', TRUNCATION_LIMITS.userPrompt) };
|
|
756
792
|
break;
|
|
@@ -933,7 +969,37 @@ function normalizeClaudeCode(event) {
|
|
|
933
969
|
},
|
|
934
970
|
raw: buildTokenUsageRaw({ source_uuid: call.uuid }, call.usage, call.model),
|
|
935
971
|
}));
|
|
936
|
-
|
|
972
|
+
// Assistant dialogue content (text + thinking) for the same calls. Stored
|
|
973
|
+
// for dialogue/search; metric-neutral server-side (ASSISTANT_CONTENT_TYPES).
|
|
974
|
+
// data.* is truncated for display; raw.* keeps the full text (redacted
|
|
975
|
+
// server-side), mirroring how raw preserves full content elsewhere.
|
|
976
|
+
const contentEvents = [];
|
|
977
|
+
for (const call of calls) {
|
|
978
|
+
const base = {
|
|
979
|
+
event_id: null,
|
|
980
|
+
source: 'claude_code',
|
|
981
|
+
session_id: sessionId,
|
|
982
|
+
project_path: projectPath,
|
|
983
|
+
timestamp: call.timestamp || timestamp,
|
|
984
|
+
};
|
|
985
|
+
if (call.text) {
|
|
986
|
+
contentEvents.push({
|
|
987
|
+
...base,
|
|
988
|
+
type: 'AssistantText',
|
|
989
|
+
data: { text: truncate(call.text, TRUNCATION_LIMITS.agentResponse), model: call.model },
|
|
990
|
+
raw: { source_uuid: call.uuid, model: call.model, text: call.text },
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
if (call.thinking) {
|
|
994
|
+
contentEvents.push({
|
|
995
|
+
...base,
|
|
996
|
+
type: 'AssistantThinking',
|
|
997
|
+
data: { thinking: truncate(call.thinking, TRUNCATION_LIMITS.agentThought), model: call.model },
|
|
998
|
+
raw: { source_uuid: call.uuid, model: call.model, thinking: call.thinking },
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const result = [primary, ...tokenEvents, ...contentEvents];
|
|
937
1003
|
// Attach the commit callback as a non-enumerable property on the returned
|
|
938
1004
|
// array so it survives through normalizeEvent() without leaking into
|
|
939
1005
|
// iteration (for...of, .map, writeToSpool's {...spread}) or into tests
|
|
@@ -1610,12 +1676,18 @@ async function main() {
|
|
|
1610
1676
|
for (let i = 1; i < events.length; i++) {
|
|
1611
1677
|
const ev = events[i];
|
|
1612
1678
|
const sourceUuid = ev.raw && ev.raw.source_uuid;
|
|
1679
|
+
// Per-type kind keeps the three call-derived events (TokenUsage +
|
|
1680
|
+
// AssistantText + AssistantThinking) that share one assistant-line uuid
|
|
1681
|
+
// from colliding on a single id (which would dedup all but one away).
|
|
1682
|
+
const kind = ev.type === 'AssistantText' ? 'assistanttext'
|
|
1683
|
+
: ev.type === 'AssistantThinking' ? 'assistantthinking'
|
|
1684
|
+
: 'tokenusage';
|
|
1613
1685
|
if (sourceUuid) {
|
|
1614
|
-
ev.event_id = deterministicEventId(`claude_code
|
|
1686
|
+
ev.event_id = deterministicEventId(`claude_code:${kind}:${sourceUuid}`);
|
|
1615
1687
|
} else {
|
|
1616
1688
|
// Fallback: stdin hash + per-event index. Should never be needed because
|
|
1617
1689
|
// Claude Code transcripts always include uuid per record.
|
|
1618
|
-
ev.event_id = deterministicEventId(`${input}
|
|
1690
|
+
ev.event_id = deterministicEventId(`${input}:${kind}:${i}`);
|
|
1619
1691
|
}
|
|
1620
1692
|
}
|
|
1621
1693
|
|
|
@@ -1640,7 +1712,10 @@ async function main() {
|
|
|
1640
1712
|
// the next Stop re-reads the same delta (server-side dedup on event_id
|
|
1641
1713
|
// handles the already-succeeded rows).
|
|
1642
1714
|
if (ev === primary) process.exit(1);
|
|
1643
|
-
|
|
1715
|
+
// Any transcript-derived event (TokenUsage / AssistantText /
|
|
1716
|
+
// AssistantThinking) failing means we must NOT advance the cursor, so the
|
|
1717
|
+
// next Stop re-reads the delta. Server-side ON CONFLICT dedups what landed.
|
|
1718
|
+
if (ev !== primary) allTokenUsageWritten = false;
|
|
1644
1719
|
}
|
|
1645
1720
|
}
|
|
1646
1721
|
|