sidekick-shared 0.13.2

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.
Files changed (126) hide show
  1. package/README.md +92 -0
  2. package/dist/aggregation/EventAggregator.d.ts +172 -0
  3. package/dist/aggregation/EventAggregator.js +1443 -0
  4. package/dist/aggregation/FrequencyTracker.d.ts +42 -0
  5. package/dist/aggregation/FrequencyTracker.js +73 -0
  6. package/dist/aggregation/HeatmapTracker.d.ts +40 -0
  7. package/dist/aggregation/HeatmapTracker.js +93 -0
  8. package/dist/aggregation/PatternExtractor.d.ts +51 -0
  9. package/dist/aggregation/PatternExtractor.js +171 -0
  10. package/dist/aggregation/snapshot.d.ts +64 -0
  11. package/dist/aggregation/snapshot.js +151 -0
  12. package/dist/aggregation/types.d.ts +121 -0
  13. package/dist/aggregation/types.js +6 -0
  14. package/dist/context/composer.d.ts +31 -0
  15. package/dist/context/composer.js +72 -0
  16. package/dist/credentials.d.ts +23 -0
  17. package/dist/credentials.js +96 -0
  18. package/dist/formatters/eventHighlighter.d.ts +30 -0
  19. package/dist/formatters/eventHighlighter.js +217 -0
  20. package/dist/formatters/noiseClassifier.d.ts +73 -0
  21. package/dist/formatters/noiseClassifier.js +226 -0
  22. package/dist/formatters/sessionDump.d.ts +38 -0
  23. package/dist/formatters/sessionDump.js +313 -0
  24. package/dist/formatters/toolSummary.d.ts +23 -0
  25. package/dist/formatters/toolSummary.js +230 -0
  26. package/dist/index.d.ts +85 -0
  27. package/dist/index.js +182 -0
  28. package/dist/parsers/changelogParser.d.ts +25 -0
  29. package/dist/parsers/changelogParser.js +74 -0
  30. package/dist/parsers/codexParser.d.ts +76 -0
  31. package/dist/parsers/codexParser.js +653 -0
  32. package/dist/parsers/debugLogParser.d.ts +63 -0
  33. package/dist/parsers/debugLogParser.js +164 -0
  34. package/dist/parsers/jsonl.d.ts +45 -0
  35. package/dist/parsers/jsonl.js +57 -0
  36. package/dist/parsers/openCodeParser.d.ts +64 -0
  37. package/dist/parsers/openCodeParser.js +581 -0
  38. package/dist/parsers/planExtractor.d.ts +63 -0
  39. package/dist/parsers/planExtractor.js +330 -0
  40. package/dist/parsers/sessionActivityDetector.d.ts +31 -0
  41. package/dist/parsers/sessionActivityDetector.js +184 -0
  42. package/dist/parsers/sessionPathResolver.d.ts +230 -0
  43. package/dist/parsers/sessionPathResolver.js +753 -0
  44. package/dist/parsers/subagentScanner.d.ts +43 -0
  45. package/dist/parsers/subagentScanner.js +366 -0
  46. package/dist/parsers/subagentTraceParser.d.ts +58 -0
  47. package/dist/parsers/subagentTraceParser.js +346 -0
  48. package/dist/paths.d.ts +38 -0
  49. package/dist/paths.js +107 -0
  50. package/dist/phrases.d.ts +52 -0
  51. package/dist/phrases.js +1333 -0
  52. package/dist/providers/claudeCode.d.ts +48 -0
  53. package/dist/providers/claudeCode.js +465 -0
  54. package/dist/providers/codex.d.ts +57 -0
  55. package/dist/providers/codex.js +944 -0
  56. package/dist/providers/codexDatabase.d.ts +37 -0
  57. package/dist/providers/codexDatabase.js +148 -0
  58. package/dist/providers/detect.d.ts +16 -0
  59. package/dist/providers/detect.js +162 -0
  60. package/dist/providers/openCode.d.ts +70 -0
  61. package/dist/providers/openCode.js +1524 -0
  62. package/dist/providers/openCodeDatabase.d.ts +87 -0
  63. package/dist/providers/openCodeDatabase.js +232 -0
  64. package/dist/providers/types.d.ts +154 -0
  65. package/dist/providers/types.js +12 -0
  66. package/dist/quota.d.ts +34 -0
  67. package/dist/quota.js +80 -0
  68. package/dist/readers/decisions.d.ts +10 -0
  69. package/dist/readers/decisions.js +27 -0
  70. package/dist/readers/handoff.d.ts +4 -0
  71. package/dist/readers/handoff.js +51 -0
  72. package/dist/readers/helpers.d.ts +7 -0
  73. package/dist/readers/helpers.js +52 -0
  74. package/dist/readers/history.d.ts +5 -0
  75. package/dist/readers/history.js +12 -0
  76. package/dist/readers/notes.d.ts +10 -0
  77. package/dist/readers/notes.js +46 -0
  78. package/dist/readers/plans.d.ts +35 -0
  79. package/dist/readers/plans.js +247 -0
  80. package/dist/readers/tasks.d.ts +8 -0
  81. package/dist/readers/tasks.js +22 -0
  82. package/dist/report/htmlHelpers.d.ts +18 -0
  83. package/dist/report/htmlHelpers.js +166 -0
  84. package/dist/report/htmlReportGenerator.d.ts +11 -0
  85. package/dist/report/htmlReportGenerator.js +650 -0
  86. package/dist/report/index.d.ts +8 -0
  87. package/dist/report/index.js +16 -0
  88. package/dist/report/logo.d.ts +2 -0
  89. package/dist/report/logo.js +5 -0
  90. package/dist/report/openBrowser.d.ts +5 -0
  91. package/dist/report/openBrowser.js +22 -0
  92. package/dist/report/transcriptParser.d.ts +12 -0
  93. package/dist/report/transcriptParser.js +177 -0
  94. package/dist/report/types.d.ts +43 -0
  95. package/dist/report/types.js +5 -0
  96. package/dist/search/advancedFilter.d.ts +62 -0
  97. package/dist/search/advancedFilter.js +201 -0
  98. package/dist/search/sessionSearch.d.ts +16 -0
  99. package/dist/search/sessionSearch.js +93 -0
  100. package/dist/types/codex.d.ts +276 -0
  101. package/dist/types/codex.js +14 -0
  102. package/dist/types/decisionLog.d.ts +23 -0
  103. package/dist/types/decisionLog.js +8 -0
  104. package/dist/types/historicalData.d.ts +74 -0
  105. package/dist/types/historicalData.js +17 -0
  106. package/dist/types/knowledgeNote.d.ts +40 -0
  107. package/dist/types/knowledgeNote.js +18 -0
  108. package/dist/types/opencode.d.ts +268 -0
  109. package/dist/types/opencode.js +13 -0
  110. package/dist/types/plan.d.ts +49 -0
  111. package/dist/types/plan.js +10 -0
  112. package/dist/types/sessionEvent.d.ts +562 -0
  113. package/dist/types/sessionEvent.js +11 -0
  114. package/dist/types/taskPersistence.d.ts +33 -0
  115. package/dist/types/taskPersistence.js +16 -0
  116. package/dist/watchers/eventBridge.d.ts +19 -0
  117. package/dist/watchers/eventBridge.js +162 -0
  118. package/dist/watchers/factory.d.ts +15 -0
  119. package/dist/watchers/factory.js +85 -0
  120. package/dist/watchers/jsonlWatcher.d.ts +30 -0
  121. package/dist/watchers/jsonlWatcher.js +444 -0
  122. package/dist/watchers/sqliteWatcher.d.ts +30 -0
  123. package/dist/watchers/sqliteWatcher.js +278 -0
  124. package/dist/watchers/types.d.ts +60 -0
  125. package/dist/watchers/types.js +5 -0
  126. package/package.json +31 -0
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ /**
3
+ * Noise classification pipeline for session events.
4
+ *
5
+ * Multi-layer classification system that filters system noise, sidechains,
6
+ * synthetic messages, and soft noise (system-reminder tags, empty outputs).
7
+ * Inspired by tail-claude's noise filtering.
8
+ *
9
+ * @module formatters/noiseClassifier
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.isHardNoise = isHardNoise;
13
+ exports.isHardNoiseFollowEvent = isHardNoiseFollowEvent;
14
+ exports.getSoftNoiseReason = getSoftNoiseReason;
15
+ exports.classifyMessage = classifyMessage;
16
+ exports.classifyFollowEvent = classifyFollowEvent;
17
+ exports.shouldMergeWithPrevious = shouldMergeWithPrevious;
18
+ exports.classifyNoise = classifyNoise;
19
+ // ── Hard Noise Detection ──
20
+ /** Hard noise types that should be dropped entirely from display. */
21
+ const HARD_NOISE_EVENT_TYPES = new Set([
22
+ 'file-history-snapshot',
23
+ 'queue-operation',
24
+ 'progress',
25
+ ]);
26
+ const SYNTHETIC_MODEL_PREFIX = '<synthetic>';
27
+ /**
28
+ * Determines if an event is "hard noise" and should be dropped entirely.
29
+ *
30
+ * Hard noise includes:
31
+ * - System-type events (non-content)
32
+ * - Specific infrastructure event types (file-history-snapshot, queue-operation, progress)
33
+ * - Sidechain/subagent events (when filtering to main conversation)
34
+ * - Synthetic model entries (model starts with `<synthetic>`)
35
+ */
36
+ function isHardNoise(event) {
37
+ // Sidechain events are noise for the main timeline
38
+ if (event.isSidechain)
39
+ return true;
40
+ // Check for known noise event types in raw data
41
+ const rawType = event.type;
42
+ if (HARD_NOISE_EVENT_TYPES.has(rawType))
43
+ return true;
44
+ // Synthetic model entries
45
+ if (event.message?.model?.startsWith(SYNTHETIC_MODEL_PREFIX))
46
+ return true;
47
+ return false;
48
+ }
49
+ /**
50
+ * FollowEvent variant of hard noise detection.
51
+ */
52
+ function isHardNoiseFollowEvent(event) {
53
+ // System events that are just infrastructure
54
+ if (event.type === 'system') {
55
+ const summary = event.summary?.toLowerCase() ?? '';
56
+ // Keep "Session ended" type system events
57
+ if (summary.includes('session ended') || summary.includes('session started')) {
58
+ return false;
59
+ }
60
+ // Token count events from Codex are noise for timeline display
61
+ if (summary.startsWith('tokens:') || summary.startsWith('model:')) {
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+ // ── Soft Noise Detection ──
68
+ const SYSTEM_REMINDER_PATTERN = /<system-reminder>[\s\S]*?<\/system-reminder>/;
69
+ const COMMAND_CAVEAT_PATTERNS = [
70
+ /IMPORTANT:.*?(?:never|always|must|should)/i,
71
+ /Note:.*?(?:do not|don't|avoid)/i,
72
+ ];
73
+ const INTERRUPTION_MARKERS = [
74
+ 'interrupted by user',
75
+ 'operation cancelled',
76
+ 'aborted',
77
+ ];
78
+ /**
79
+ * Checks for soft noise that can be hidden but not dropped.
80
+ *
81
+ * Soft noise includes:
82
+ * - system-reminder XML tags in content
83
+ * - Command caveats (IMPORTANT/Note instructions)
84
+ * - Empty tool outputs
85
+ * - User interruption markers
86
+ *
87
+ * @returns A reason string if soft noise is detected, null otherwise.
88
+ */
89
+ function getSoftNoiseReason(event) {
90
+ const content = extractContent(event);
91
+ if (content && SYSTEM_REMINDER_PATTERN.test(content)) {
92
+ return 'system-reminder';
93
+ }
94
+ for (const pattern of COMMAND_CAVEAT_PATTERNS) {
95
+ if (content && pattern.test(content)) {
96
+ return 'command-caveat';
97
+ }
98
+ }
99
+ // Empty tool results
100
+ if (event.type === 'tool_result') {
101
+ const output = event.result?.output;
102
+ if (output === '' || output === undefined || output === null) {
103
+ return 'empty-tool-output';
104
+ }
105
+ }
106
+ // Interruption markers
107
+ if (content) {
108
+ for (const marker of INTERRUPTION_MARKERS) {
109
+ if (content.toLowerCase().includes(marker)) {
110
+ return 'interruption';
111
+ }
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ // ── Message Classification ──
117
+ /**
118
+ * Classifies a SessionEvent into semantic message categories.
119
+ *
120
+ * Categories:
121
+ * - `user` — human prompts
122
+ * - `ai` — assistant responses (text or tool calls)
123
+ * - `system` — tool results, compaction, infrastructure
124
+ * - `teammate` — teammate/subagent messages (from `<teammate-message>` blocks)
125
+ * - `compact` — compaction/summary events
126
+ */
127
+ function classifyMessage(event) {
128
+ switch (event.type) {
129
+ case 'user': {
130
+ const content = extractContent(event);
131
+ // Check for teammate messages
132
+ if (content && content.includes('<teammate-message>')) {
133
+ return 'teammate';
134
+ }
135
+ return 'user';
136
+ }
137
+ case 'assistant':
138
+ return 'ai';
139
+ case 'summary':
140
+ return 'compact';
141
+ case 'tool_use':
142
+ return 'ai';
143
+ case 'tool_result':
144
+ return 'system';
145
+ default:
146
+ return 'system';
147
+ }
148
+ }
149
+ /**
150
+ * FollowEvent variant of message classification.
151
+ */
152
+ function classifyFollowEvent(event) {
153
+ switch (event.type) {
154
+ case 'user':
155
+ return 'user';
156
+ case 'assistant':
157
+ return 'ai';
158
+ case 'tool_use':
159
+ return 'ai';
160
+ case 'tool_result':
161
+ return 'system';
162
+ case 'summary':
163
+ return 'compact';
164
+ case 'system':
165
+ return 'system';
166
+ default:
167
+ return 'system';
168
+ }
169
+ }
170
+ // ── Merge Detection ──
171
+ /**
172
+ * Determines if the current event should be merged with the previous one.
173
+ *
174
+ * Consecutive assistant text messages (without tool calls between them)
175
+ * can be merged into a single display entry.
176
+ */
177
+ function shouldMergeWithPrevious(current, previous) {
178
+ if (!previous)
179
+ return false;
180
+ // Merge consecutive assistant text messages
181
+ if (current.type === 'assistant' && previous.type === 'assistant') {
182
+ // Don't merge if either has tool_use content blocks
183
+ const currentContent = current.message?.content;
184
+ const previousContent = previous.message?.content;
185
+ if (hasToolUseBlocks(currentContent) || hasToolUseBlocks(previousContent)) {
186
+ return false;
187
+ }
188
+ return true;
189
+ }
190
+ return false;
191
+ }
192
+ // ── Full Classification ──
193
+ /**
194
+ * Performs full noise classification on a SessionEvent.
195
+ * Combines hard noise detection, soft noise detection, and message classification.
196
+ */
197
+ function classifyNoise(event) {
198
+ return {
199
+ isHardNoise: isHardNoise(event),
200
+ softNoiseReason: getSoftNoiseReason(event),
201
+ messageClassification: classifyMessage(event),
202
+ };
203
+ }
204
+ // ── Helpers ──
205
+ function extractContent(event) {
206
+ const content = event.message?.content;
207
+ if (!content)
208
+ return null;
209
+ if (typeof content === 'string')
210
+ return content;
211
+ if (Array.isArray(content)) {
212
+ const texts = [];
213
+ for (const block of content) {
214
+ if (block.type === 'text' && typeof block.text === 'string') {
215
+ texts.push(block.text);
216
+ }
217
+ }
218
+ return texts.length > 0 ? texts.join('\n') : null;
219
+ }
220
+ return null;
221
+ }
222
+ function hasToolUseBlocks(content) {
223
+ if (!Array.isArray(content))
224
+ return false;
225
+ return content.some(b => b.type === 'tool_use');
226
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Session dump formatters — text, markdown, and JSON output.
3
+ *
4
+ * Extracted from sidekick-cli's dump command so both the CLI and VS Code
5
+ * extension can produce identical session reports from AggregatedMetrics.
6
+ *
7
+ * @module formatters/sessionDump
8
+ */
9
+ import type { AggregatedMetrics } from '../aggregation/types';
10
+ /** Options for text and markdown formatters. */
11
+ export interface SessionDumpOptions {
12
+ /** Terminal width for text output (default 120). */
13
+ width?: number;
14
+ /** Show noise-classified events (default false hides them). */
15
+ expand?: boolean;
16
+ /** Session file name to display in markdown summary table. */
17
+ sessionFileName?: string;
18
+ }
19
+ /**
20
+ * Serialize aggregated metrics as pretty-printed JSON.
21
+ */
22
+ export declare function formatSessionJson(metrics: AggregatedMetrics): string;
23
+ /**
24
+ * Format aggregated metrics as a plain-text report.
25
+ */
26
+ export declare function formatSessionText(metrics: AggregatedMetrics, options?: SessionDumpOptions): string;
27
+ /**
28
+ * Format aggregated metrics as a markdown report.
29
+ */
30
+ export declare function formatSessionMarkdown(metrics: AggregatedMetrics, options?: SessionDumpOptions): string;
31
+ /** @internal Exported for tests. */
32
+ export declare function fmtTokens(n: number): string;
33
+ /** @internal Exported for tests. */
34
+ export declare function fmtCost(cost: number): string;
35
+ /** @internal Exported for tests. */
36
+ export declare function formatTimestamp(iso: string): string;
37
+ /** @internal Exported for tests. */
38
+ export declare function formatDuration(start: string | null, end: string | null): string;
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ /**
3
+ * Session dump formatters — text, markdown, and JSON output.
4
+ *
5
+ * Extracted from sidekick-cli's dump command so both the CLI and VS Code
6
+ * extension can produce identical session reports from AggregatedMetrics.
7
+ *
8
+ * @module formatters/sessionDump
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.formatSessionJson = formatSessionJson;
12
+ exports.formatSessionText = formatSessionText;
13
+ exports.formatSessionMarkdown = formatSessionMarkdown;
14
+ exports.fmtTokens = fmtTokens;
15
+ exports.fmtCost = fmtCost;
16
+ exports.formatTimestamp = formatTimestamp;
17
+ exports.formatDuration = formatDuration;
18
+ // ── JSON output ──
19
+ /**
20
+ * Serialize aggregated metrics as pretty-printed JSON.
21
+ */
22
+ function formatSessionJson(metrics) {
23
+ return JSON.stringify(metrics, null, 2) + '\n';
24
+ }
25
+ // ── Text output ──
26
+ /**
27
+ * Format aggregated metrics as a plain-text report.
28
+ */
29
+ function formatSessionText(metrics, options = {}) {
30
+ const width = options.width ?? 120;
31
+ const expand = options.expand ?? false;
32
+ const lines = [];
33
+ const hr = '\u2500'.repeat(Math.min(width, 80));
34
+ // Header
35
+ lines.push(hr);
36
+ lines.push(formatHeader(metrics));
37
+ lines.push(hr);
38
+ lines.push('');
39
+ // Token summary
40
+ lines.push(formatTokenSummary(metrics));
41
+ lines.push('');
42
+ // Model stats
43
+ if (metrics.modelStats.length > 0) {
44
+ lines.push('Models:');
45
+ for (const m of metrics.modelStats) {
46
+ lines.push(` ${m.model}: ${m.calls} calls, ${fmtTokens(m.tokens)} tokens, ${fmtCost(m.cost)}`);
47
+ }
48
+ lines.push('');
49
+ }
50
+ // Tool stats
51
+ if (metrics.toolStats.length > 0) {
52
+ lines.push('Tools:');
53
+ const sorted = [...metrics.toolStats].sort((a, b) => (b.successCount + b.failureCount) - (a.successCount + a.failureCount));
54
+ for (const t of sorted) {
55
+ const total = t.successCount + t.failureCount;
56
+ const failStr = t.failureCount > 0 ? ` (${t.failureCount} failed)` : '';
57
+ const avgMs = t.completedCount > 0 ? Math.round(t.totalDuration / t.completedCount) : 0;
58
+ const durationStr = avgMs > 0 ? ` avg ${avgMs}ms` : '';
59
+ lines.push(` ${t.name}: ${total}${failStr}${durationStr}`);
60
+ }
61
+ lines.push('');
62
+ }
63
+ // Compaction / truncation
64
+ if (metrics.compactionCount > 0 || metrics.truncationCount > 0) {
65
+ const parts = [];
66
+ if (metrics.compactionCount > 0)
67
+ parts.push(`${metrics.compactionCount} compaction(s)`);
68
+ if (metrics.truncationCount > 0)
69
+ parts.push(`${metrics.truncationCount} truncation(s)`);
70
+ lines.push(`Context: ${parts.join(', ')}`);
71
+ lines.push('');
72
+ }
73
+ // Timeline
74
+ lines.push(hr);
75
+ lines.push('Timeline:');
76
+ lines.push(hr);
77
+ const filteredEvents = expand
78
+ ? metrics.timeline
79
+ : metrics.timeline.filter(e => e.noiseLevel !== 'noise');
80
+ if (filteredEvents.length === 0) {
81
+ lines.push(' (no events)');
82
+ }
83
+ else {
84
+ for (const event of filteredEvents) {
85
+ const ts = formatTimestamp(event.timestamp);
86
+ const icon = getTimelineEventIcon(event.type);
87
+ const summary = truncateToWidth(event.description, width - ts.length - 4);
88
+ lines.push(`${ts} ${icon} ${summary}`);
89
+ }
90
+ }
91
+ lines.push('');
92
+ return lines.join('\n');
93
+ }
94
+ // ── Markdown output ──
95
+ /**
96
+ * Format aggregated metrics as a markdown report.
97
+ */
98
+ function formatSessionMarkdown(metrics, options = {}) {
99
+ const expand = options.expand ?? false;
100
+ const sessionFileName = options.sessionFileName;
101
+ const lines = [];
102
+ // Title
103
+ lines.push('# Session Report');
104
+ lines.push('');
105
+ // Summary
106
+ lines.push('## Summary');
107
+ lines.push('');
108
+ lines.push(`| Metric | Value |`);
109
+ lines.push(`|--------|-------|`);
110
+ if (sessionFileName) {
111
+ lines.push(`| Session file | \`${sessionFileName}\` |`);
112
+ }
113
+ lines.push(`| Started | ${metrics.sessionStartTime || 'N/A'} |`);
114
+ lines.push(`| Last event | ${metrics.lastEventTime || 'N/A'} |`);
115
+ lines.push(`| Duration | ${formatDuration(metrics.sessionStartTime, metrics.lastEventTime)} |`);
116
+ lines.push(`| Messages | ${metrics.messageCount} |`);
117
+ lines.push(`| Events | ${metrics.eventCount} |`);
118
+ lines.push(`| Model | ${metrics.currentModel || 'N/A'} |`);
119
+ lines.push(`| Provider | ${metrics.providerId || 'N/A'} |`);
120
+ lines.push('');
121
+ // Tokens
122
+ lines.push('## Tokens');
123
+ lines.push('');
124
+ lines.push(`| Category | Count |`);
125
+ lines.push(`|----------|-------|`);
126
+ lines.push(`| Input | ${fmtTokens(metrics.tokens.inputTokens)} |`);
127
+ lines.push(`| Output | ${fmtTokens(metrics.tokens.outputTokens)} |`);
128
+ lines.push(`| Cache write | ${fmtTokens(metrics.tokens.cacheWriteTokens)} |`);
129
+ lines.push(`| Cache read | ${fmtTokens(metrics.tokens.cacheReadTokens)} |`);
130
+ lines.push(`| Total | ${fmtTokens(metrics.tokens.inputTokens + metrics.tokens.outputTokens)} |`);
131
+ if (metrics.tokens.reportedCost > 0) {
132
+ lines.push(`| Cost | ${fmtCost(metrics.tokens.reportedCost)} |`);
133
+ }
134
+ lines.push('');
135
+ // Model stats
136
+ if (metrics.modelStats.length > 0) {
137
+ lines.push('## Models');
138
+ lines.push('');
139
+ lines.push(`| Model | Calls | Tokens | Cost |`);
140
+ lines.push(`|-------|-------|--------|------|`);
141
+ for (const m of metrics.modelStats) {
142
+ lines.push(`| ${m.model} | ${m.calls} | ${fmtTokens(m.tokens)} | ${fmtCost(m.cost)} |`);
143
+ }
144
+ lines.push('');
145
+ }
146
+ // Tool calls
147
+ if (metrics.toolStats.length > 0) {
148
+ lines.push('## Tool Calls');
149
+ lines.push('');
150
+ lines.push(`| Tool | Total | Failed | Avg Duration |`);
151
+ lines.push(`|------|-------|--------|--------------|`);
152
+ const sorted = [...metrics.toolStats].sort((a, b) => (b.successCount + b.failureCount) - (a.successCount + a.failureCount));
153
+ for (const t of sorted) {
154
+ const total = t.successCount + t.failureCount;
155
+ const avgMs = t.completedCount > 0 ? Math.round(t.totalDuration / t.completedCount) : 0;
156
+ lines.push(`| ${t.name} | ${total} | ${t.failureCount} | ${avgMs > 0 ? `${avgMs}ms` : '-'} |`);
157
+ }
158
+ lines.push('');
159
+ }
160
+ // Context events
161
+ if (metrics.compactionCount > 0 || metrics.truncationCount > 0) {
162
+ lines.push('## Context Management');
163
+ lines.push('');
164
+ if (metrics.compactionCount > 0) {
165
+ lines.push(`- **Compactions:** ${metrics.compactionCount}`);
166
+ for (const c of metrics.compactionEvents) {
167
+ lines.push(` - At ${formatTimestamp(c.timestamp instanceof Date ? c.timestamp.toISOString() : String(c.timestamp))}: ${fmtTokens(c.contextBefore)} -> ${fmtTokens(c.contextAfter)} tokens`);
168
+ }
169
+ }
170
+ if (metrics.truncationCount > 0) {
171
+ lines.push(`- **Truncations:** ${metrics.truncationCount}`);
172
+ }
173
+ lines.push('');
174
+ }
175
+ // Subagents
176
+ if (metrics.subagents.length > 0) {
177
+ lines.push('## Subagents');
178
+ lines.push('');
179
+ lines.push(`| Type | Description | Status | Duration |`);
180
+ lines.push(`|------|-------------|--------|----------|`);
181
+ for (const s of metrics.subagents) {
182
+ const dur = s.durationMs ? `${Math.round(s.durationMs / 1000)}s` : '-';
183
+ lines.push(`| ${s.subagentType} | ${s.description} | ${s.status} | ${dur} |`);
184
+ }
185
+ lines.push('');
186
+ }
187
+ // Timeline
188
+ lines.push('## Timeline');
189
+ lines.push('');
190
+ const filteredEvents = expand
191
+ ? metrics.timeline
192
+ : metrics.timeline.filter(e => e.noiseLevel !== 'noise');
193
+ if (filteredEvents.length === 0) {
194
+ lines.push('_(no events)_');
195
+ }
196
+ else {
197
+ lines.push('```');
198
+ for (const event of filteredEvents) {
199
+ const ts = formatTimestamp(event.timestamp);
200
+ const icon = getTimelineEventIcon(event.type);
201
+ lines.push(`${ts} ${icon} ${event.description}`);
202
+ }
203
+ lines.push('```');
204
+ }
205
+ lines.push('');
206
+ return lines.join('\n');
207
+ }
208
+ // ── Formatting helpers ──
209
+ function formatHeader(metrics) {
210
+ const parts = [];
211
+ if (metrics.providerId)
212
+ parts.push(metrics.providerId);
213
+ if (metrics.currentModel)
214
+ parts.push(metrics.currentModel);
215
+ const duration = formatDuration(metrics.sessionStartTime, metrics.lastEventTime);
216
+ if (duration !== 'N/A')
217
+ parts.push(duration);
218
+ parts.push(`${metrics.messageCount} messages`);
219
+ parts.push(`${metrics.eventCount} events`);
220
+ return parts.join(' | ');
221
+ }
222
+ function formatTokenSummary(metrics) {
223
+ const t = metrics.tokens;
224
+ const total = t.inputTokens + t.outputTokens;
225
+ const parts = [
226
+ `Tokens: ${fmtTokens(total)} total`,
227
+ `(${fmtTokens(t.inputTokens)} in`,
228
+ `${fmtTokens(t.outputTokens)} out`,
229
+ ];
230
+ if (t.cacheReadTokens > 0)
231
+ parts.push(`${fmtTokens(t.cacheReadTokens)} cache-read`);
232
+ if (t.cacheWriteTokens > 0)
233
+ parts.push(`${fmtTokens(t.cacheWriteTokens)} cache-write`);
234
+ const line = parts.join(', ');
235
+ if (t.reportedCost > 0)
236
+ return `${line}) Cost: ${fmtCost(t.reportedCost)}`;
237
+ return line + ')';
238
+ }
239
+ /** @internal Exported for tests. */
240
+ function fmtTokens(n) {
241
+ if (n >= 1_000_000)
242
+ return `${(n / 1_000_000).toFixed(1)}M`;
243
+ if (n >= 1_000)
244
+ return `${(n / 1_000).toFixed(1)}k`;
245
+ return String(n);
246
+ }
247
+ /** @internal Exported for tests. */
248
+ function fmtCost(cost) {
249
+ if (cost <= 0)
250
+ return '$0.00';
251
+ if (cost < 0.01)
252
+ return `$${cost.toFixed(4)}`;
253
+ return `$${cost.toFixed(2)}`;
254
+ }
255
+ /** @internal Exported for tests. */
256
+ function formatTimestamp(iso) {
257
+ try {
258
+ const d = new Date(iso);
259
+ if (isNaN(d.getTime()))
260
+ return '??:??:??';
261
+ const h = d.getHours().toString().padStart(2, '0');
262
+ const m = d.getMinutes().toString().padStart(2, '0');
263
+ const s = d.getSeconds().toString().padStart(2, '0');
264
+ return `${h}:${m}:${s}`;
265
+ }
266
+ catch {
267
+ return '??:??:??';
268
+ }
269
+ }
270
+ /** @internal Exported for tests. */
271
+ function formatDuration(start, end) {
272
+ if (!start || !end)
273
+ return 'N/A';
274
+ try {
275
+ const ms = new Date(end).getTime() - new Date(start).getTime();
276
+ if (ms < 0)
277
+ return 'N/A';
278
+ const totalSec = Math.floor(ms / 1000);
279
+ const hours = Math.floor(totalSec / 3600);
280
+ const minutes = Math.floor((totalSec % 3600) / 60);
281
+ const seconds = totalSec % 60;
282
+ if (hours > 0)
283
+ return `${hours}h ${minutes}m ${seconds}s`;
284
+ if (minutes > 0)
285
+ return `${minutes}m ${seconds}s`;
286
+ return `${seconds}s`;
287
+ }
288
+ catch {
289
+ return 'N/A';
290
+ }
291
+ }
292
+ /** Map TimelineEvent.type to a display icon. */
293
+ function getTimelineEventIcon(type) {
294
+ switch (type) {
295
+ case 'user_prompt': return '\u25b6'; // right-pointing triangle
296
+ case 'assistant_response': return '\u2726'; // four-pointed star
297
+ case 'tool_call': return '\u2699'; // gear
298
+ case 'tool_result': return '\u2190'; // left arrow
299
+ case 'compaction': return '\u21bb'; // clockwise arrow
300
+ case 'error': return '\u2718'; // heavy ballot X
301
+ case 'session_start': return '\u25cf'; // black circle
302
+ case 'session_end': return '\u25cb'; // white circle
303
+ default: return ' ';
304
+ }
305
+ }
306
+ function truncateToWidth(text, maxLen) {
307
+ if (maxLen < 10)
308
+ maxLen = 10;
309
+ const clean = text.replace(/\s+/g, ' ').trim();
310
+ if (clean.length <= maxLen)
311
+ return clean;
312
+ return clean.substring(0, maxLen - 3) + '...';
313
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Rich tool summary formatters.
3
+ *
4
+ * Registry of per-tool formatters that produce contextual one-liner summaries.
5
+ * Replaces the generic field-fallback `summarizeToolInput()` with tool-specific
6
+ * formatting inspired by tail-claude's 19+ tool formatters.
7
+ *
8
+ * @module formatters/toolSummary
9
+ */
10
+ declare function truncate(text: string, maxLen: number): string;
11
+ /**
12
+ * Produces a contextual one-liner summary for a tool call.
13
+ *
14
+ * Uses a registry of per-tool formatters for known tools (Read, Write, Edit,
15
+ * Bash, Grep, Glob, Task, WebFetch, WebSearch, etc.) and falls back to
16
+ * generic field extraction for unknown tools.
17
+ *
18
+ * @param toolName - The canonical tool name (e.g., "Read", "Bash")
19
+ * @param input - The tool's input parameters
20
+ * @returns A human-readable one-liner summary (may be empty string)
21
+ */
22
+ export declare function formatToolSummary(toolName: string, input: Record<string, unknown>): string;
23
+ export { truncate };