@roj-ai/cli 0.0.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 (49) hide show
  1. package/dist/client.d.ts +8 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +28 -0
  4. package/dist/commands/answer.d.ts +3 -0
  5. package/dist/commands/answer.d.ts.map +1 -0
  6. package/dist/commands/answer.js +15 -0
  7. package/dist/commands/debug-format.d.ts +12 -0
  8. package/dist/commands/debug-format.d.ts.map +1 -0
  9. package/dist/commands/debug-format.js +262 -0
  10. package/dist/commands/debug.d.ts +14 -0
  11. package/dist/commands/debug.d.ts.map +1 -0
  12. package/dist/commands/debug.js +187 -0
  13. package/dist/commands/messages.d.ts +4 -0
  14. package/dist/commands/messages.d.ts.map +1 -0
  15. package/dist/commands/messages.js +77 -0
  16. package/dist/commands/presets.d.ts +3 -0
  17. package/dist/commands/presets.d.ts.map +1 -0
  18. package/dist/commands/presets.js +14 -0
  19. package/dist/commands/sessions.d.ts +6 -0
  20. package/dist/commands/sessions.d.ts.map +1 -0
  21. package/dist/commands/sessions.js +50 -0
  22. package/dist/main.d.ts +2 -0
  23. package/dist/main.d.ts.map +1 -0
  24. package/dist/main.js +277 -0
  25. package/dist/repl/formatter.d.ts +16 -0
  26. package/dist/repl/formatter.d.ts.map +1 -0
  27. package/dist/repl/formatter.js +72 -0
  28. package/dist/repl/repl.d.ts +3 -0
  29. package/dist/repl/repl.d.ts.map +1 -0
  30. package/dist/repl/repl.js +212 -0
  31. package/dist/repl/session-picker.d.ts +6 -0
  32. package/dist/repl/session-picker.d.ts.map +1 -0
  33. package/dist/repl/session-picker.js +60 -0
  34. package/dist/unwrap.d.ts +4 -0
  35. package/dist/unwrap.d.ts.map +1 -0
  36. package/dist/unwrap.js +6 -0
  37. package/package.json +27 -0
  38. package/src/client.ts +41 -0
  39. package/src/commands/answer.ts +27 -0
  40. package/src/commands/debug-format.ts +299 -0
  41. package/src/commands/debug.ts +347 -0
  42. package/src/commands/messages.ts +92 -0
  43. package/src/commands/presets.ts +22 -0
  44. package/src/commands/sessions.ts +73 -0
  45. package/src/main.ts +319 -0
  46. package/src/repl/formatter.ts +84 -0
  47. package/src/repl/repl.ts +246 -0
  48. package/src/repl/session-picker.ts +81 -0
  49. package/src/unwrap.ts +8 -0
@@ -0,0 +1,6 @@
1
+ import { SessionId } from '@roj-ai/shared';
2
+ import type { RpcClient } from '@roj-ai/shared/rpc';
3
+ import * as readline from 'node:readline';
4
+ export declare function pickSession(client: RpcClient, rl: readline.Interface): Promise<SessionId>;
5
+ export declare function createNewSession(client: RpcClient, rl: readline.Interface): Promise<SessionId>;
6
+ //# sourceMappingURL=session-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-picker.d.ts","sourceRoot":"","sources":["../../src/repl/session-picker.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAiBzC,wBAAsB,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CA8B/F;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CA4BpG"}
@@ -0,0 +1,60 @@
1
+ import { SessionId } from '@roj-ai/shared';
2
+ import { unwrap } from '../unwrap.js';
3
+ const DIM = '\x1b[2m';
4
+ const BOLD = '\x1b[1m';
5
+ const RESET = '\x1b[0m';
6
+ function ask(rl, question) {
7
+ return new Promise(resolve => {
8
+ rl.question(question, resolve);
9
+ });
10
+ }
11
+ function isSessionMetadata(data) {
12
+ return typeof data === 'object' && data !== null && 'sessionId' in data && 'presetId' in data;
13
+ }
14
+ export async function pickSession(client, rl) {
15
+ const { sessions: rawSessions } = unwrap(await client.call('sessions.list', { status: 'active', limit: 20, order: 'desc' }));
16
+ const sessions = rawSessions.filter(isSessionMetadata);
17
+ console.log();
18
+ console.log(`${BOLD}Active sessions:${RESET}`);
19
+ for (let i = 0; i < sessions.length; i++) {
20
+ const s = sessions[i];
21
+ const date = new Date(s.createdAt).toLocaleString();
22
+ const name = s.name ? ` - ${s.name}` : '';
23
+ console.log(` ${BOLD}${i + 1})${RESET} ${s.sessionId.slice(0, 8)}... ${DIM}[${s.presetId}]${RESET}${name} ${DIM}${date}${RESET}`);
24
+ }
25
+ console.log(` ${BOLD}n)${RESET} Create new session`);
26
+ console.log();
27
+ const choice = await ask(rl, 'Select session: ');
28
+ if (choice.toLowerCase() === 'n') {
29
+ return createNewSession(client, rl);
30
+ }
31
+ const index = parseInt(choice, 10) - 1;
32
+ if (index >= 0 && index < sessions.length) {
33
+ return sessions[index].sessionId;
34
+ }
35
+ console.log('Invalid choice, try again.');
36
+ return pickSession(client, rl);
37
+ }
38
+ export async function createNewSession(client, rl) {
39
+ const { presets } = unwrap(await client.call('presets.list', {}));
40
+ if (presets.length === 0) {
41
+ throw new Error('No presets available on the server.');
42
+ }
43
+ console.log();
44
+ console.log(`${BOLD}Available presets:${RESET}`);
45
+ for (let i = 0; i < presets.length; i++) {
46
+ const p = presets[i];
47
+ const desc = p.description ? ` - ${p.description}` : '';
48
+ console.log(` ${BOLD}${i + 1})${RESET} ${p.name}${DIM}${desc}${RESET}`);
49
+ }
50
+ console.log();
51
+ const choice = await ask(rl, 'Select preset: ');
52
+ const index = parseInt(choice, 10) - 1;
53
+ if (index < 0 || index >= presets.length) {
54
+ console.log('Invalid choice, try again.');
55
+ return createNewSession(client, rl);
56
+ }
57
+ const { sessionId } = unwrap(await client.call('sessions.create', { presetId: presets[index].id }));
58
+ console.log(`Session created: ${sessionId}`);
59
+ return SessionId(sessionId);
60
+ }
@@ -0,0 +1,4 @@
1
+ import type { Result } from '@roj-ai/shared/rpc';
2
+ import type { RpcErrorInfo } from '@roj-ai/shared/rpc';
3
+ export declare function unwrap<T>(result: Result<T, RpcErrorInfo>): T;
4
+ //# sourceMappingURL=unwrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unwrap.d.ts","sourceRoot":"","sources":["../src/unwrap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,YAAY,CAAC,GAAG,CAAC,CAG5D"}
package/dist/unwrap.js ADDED
@@ -0,0 +1,6 @@
1
+ import { RpcError } from '@roj-ai/shared/rpc';
2
+ export function unwrap(result) {
3
+ if (result.ok)
4
+ return result.value;
5
+ throw new RpcError(0, result.error);
6
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@roj-ai/cli",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "main": "./src/main.ts",
6
+ "files": [
7
+ "src",
8
+ "dist"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "dev": "bun run src/main.ts",
15
+ "build": "tsc",
16
+ "type-check": "tsc --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@roj-ai/sdk": "^0.0.2",
20
+ "@roj-ai/shared": "^0.0.2",
21
+ "@roj-ai/transport": "^0.0.2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/bun": "latest",
25
+ "typescript": "^5.7.2"
26
+ }
27
+ }
package/src/client.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { RpcClient } from '@roj-ai/shared/rpc'
2
+ import type { ProtocolDef, RawMessageListener } from '@roj-ai/transport'
3
+ import { bunWebSocketFactory } from '@roj-ai/transport/bun'
4
+ import { ClientConnection } from '@roj-ai/transport/client'
5
+
6
+ export function createCliClient(baseUrl: string): RpcClient {
7
+ return new RpcClient(baseUrl)
8
+ }
9
+
10
+ export type WsConnection = ClientConnection<ProtocolDef, ProtocolDef>
11
+
12
+ export type NotificationHandlers = Record<string, (payload: unknown) => void>
13
+
14
+ export function connectWebSocket(
15
+ baseUrl: string,
16
+ sessionId: string,
17
+ handlers: NotificationHandlers,
18
+ ): WsConnection {
19
+ const url = new URL('/ws/spa', baseUrl)
20
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
21
+ url.searchParams.set('sessionId', sessionId)
22
+
23
+ const connection = new ClientConnection<ProtocolDef, ProtocolDef>({
24
+ url: url.toString(),
25
+ wsFactory: bunWebSocketFactory,
26
+ reconnect: {
27
+ baseDelayMs: 1000,
28
+ maxDelayMs: 10000,
29
+ maxAttempts: 10,
30
+ },
31
+ })
32
+
33
+ const listener: RawMessageListener = (type, payload) => {
34
+ const handler = handlers[type]
35
+ if (handler) {
36
+ handler(payload)
37
+ }
38
+ }
39
+ connection.setRawMessageListener(listener)
40
+ return connection
41
+ }
@@ -0,0 +1,27 @@
1
+ import { AgentId, ChatMessageId } from '@roj-ai/shared'
2
+ import type { RpcClient } from '@roj-ai/shared/rpc'
3
+ import { unwrap } from '../unwrap.js'
4
+
5
+ export async function answerCommand(
6
+ client: RpcClient,
7
+ sessionId: string,
8
+ questionId: string,
9
+ answer: string,
10
+ json: boolean,
11
+ ): Promise<void> {
12
+ const session = unwrap(await client.call('sessions.get', { sessionId }))
13
+ if (!session.entryAgentId) {
14
+ console.error('No entry agent found for session.')
15
+ process.exit(1)
16
+ }
17
+ const result = unwrap(
18
+ await client.call('user-chat.answerQuestion', { sessionId, agentId: AgentId(session.entryAgentId), questionId: ChatMessageId(questionId), answer }),
19
+ )
20
+
21
+ if (json) {
22
+ console.log(JSON.stringify(result))
23
+ return
24
+ }
25
+
26
+ console.log('Answer submitted.')
27
+ }
@@ -0,0 +1,299 @@
1
+ import type { LLMCallLogEntry } from '@roj-ai/sdk'
2
+ import type { DomainEvent } from '@roj-ai/sdk'
3
+ import type {
4
+ AgentTreeNode,
5
+ ConversationMessageView,
6
+ GetAgentDetailResponse,
7
+ GetMetricsResponse,
8
+ GlobalMailboxMessage,
9
+ TimelineItem,
10
+ } from '@roj-ai/shared'
11
+
12
+ // ANSI color codes
13
+ const RESET = '\x1b[0m'
14
+ const BOLD = '\x1b[1m'
15
+ const DIM = '\x1b[2m'
16
+ const CYAN = '\x1b[36m'
17
+ const GREEN = '\x1b[32m'
18
+ const YELLOW = '\x1b[33m'
19
+ const RED = '\x1b[31m'
20
+ const MAGENTA = '\x1b[35m'
21
+
22
+ export function truncate(str: string, maxLen: number): string {
23
+ if (str.length <= maxLen) return str
24
+ return str.slice(0, maxLen - 1) + '\u2026'
25
+ }
26
+
27
+ function formatDuration(ms: number | undefined): string {
28
+ if (ms === undefined) return '-'
29
+ if (ms < 1000) return `${ms}ms`
30
+ return `${(ms / 1000).toFixed(1)}s`
31
+ }
32
+
33
+ function formatTimestamp(ts: number): string {
34
+ return new Date(ts).toLocaleTimeString()
35
+ }
36
+
37
+ function statusColor(status: string): string {
38
+ switch (status) {
39
+ case 'idle':
40
+ case 'success':
41
+ return GREEN
42
+ case 'thinking':
43
+ case 'responding':
44
+ case 'running':
45
+ return YELLOW
46
+ case 'error':
47
+ return RED
48
+ default:
49
+ return ''
50
+ }
51
+ }
52
+
53
+ // ============================================================================
54
+ // Agent Tree
55
+ // ============================================================================
56
+
57
+ export function formatAgentTree(nodes: AgentTreeNode[]): string {
58
+ const lines: string[] = []
59
+ for (let i = 0; i < nodes.length; i++) {
60
+ const isLast = i === nodes.length - 1
61
+ formatTreeNode(lines, nodes[i], '', isLast)
62
+ }
63
+ return lines.join('\n')
64
+ }
65
+
66
+ function formatCost(cost: number): string {
67
+ if (cost >= 1) return `$${cost.toFixed(2)}`
68
+ return `$${cost.toFixed(4)}`
69
+ }
70
+
71
+ function formatTreeNode(lines: string[], node: AgentTreeNode, prefix: string, isLast: boolean): void {
72
+ const connector = isLast ? '\u2514\u2500\u2500 ' : '\u251c\u2500\u2500 '
73
+ const childPrefix = prefix + (isLast ? ' ' : '\u2502 ')
74
+ const color = statusColor(node.status)
75
+
76
+ // Line 1: name + status
77
+ lines.push(`${prefix}${connector}${BOLD}${node.definitionName}${RESET} ${color}[${node.status}]${RESET}`)
78
+
79
+ // Line 2: cost + badges + id
80
+ const details: string[] = []
81
+ if (node.cost > 0) details.push(`${GREEN}${formatCost(node.cost)}${RESET}`)
82
+ if (node.isExecuting) details.push(`${YELLOW}executing${RESET}`)
83
+ if (node.mailboxCount > 0) details.push(`${CYAN}${node.mailboxCount} msgs${RESET}`)
84
+ if (node.pendingToolCalls > 0) details.push(`${MAGENTA}${node.pendingToolCalls} tools${RESET}`)
85
+ const detailStr = details.length > 0 ? details.join(` ${DIM}\u00b7${RESET} `) + ' ' : ''
86
+ lines.push(`${childPrefix}${detailStr}${DIM}${node.id}${RESET}`)
87
+
88
+ // Children
89
+ for (let i = 0; i < node.children.length; i++) {
90
+ const childIsLast = i === node.children.length - 1
91
+ formatTreeNode(lines, node.children[i], childPrefix, childIsLast)
92
+ }
93
+ }
94
+
95
+ // ============================================================================
96
+ // Agent Detail
97
+ // ============================================================================
98
+
99
+ export function formatAgentDetail(detail: GetAgentDetailResponse): string {
100
+ const lines: string[] = []
101
+ const color = statusColor(detail.status)
102
+
103
+ // Header
104
+ lines.push(`${BOLD}Agent: ${detail.definitionName}${RESET}`)
105
+ lines.push(` ID: ${detail.id}`)
106
+ lines.push(` Status: ${color}${detail.status}${RESET}`)
107
+ lines.push(` Parent: ${detail.parentId ?? 'none'}`)
108
+
109
+ // Mailbox
110
+ lines.push('')
111
+ lines.push(`${BOLD}Mailbox${RESET} (${detail.mailbox.length} messages)`)
112
+ if (detail.mailbox.length === 0) {
113
+ lines.push(` ${DIM}(empty)${RESET}`)
114
+ } else {
115
+ for (const msg of detail.mailbox) {
116
+ const consumed = msg.consumed ? `${DIM}[consumed]${RESET}` : `${YELLOW}[pending]${RESET}`
117
+ lines.push(` ${msg.from} ${consumed}: ${truncate(msg.content, 80)}`)
118
+ }
119
+ }
120
+
121
+ // Conversation
122
+ lines.push('')
123
+ lines.push(`${BOLD}Conversation${RESET} (${detail.conversationHistory.length} messages)`)
124
+ if (detail.conversationHistory.length === 0) {
125
+ lines.push(` ${DIM}(empty)${RESET}`)
126
+ } else {
127
+ for (const msg of detail.conversationHistory) {
128
+ lines.push(formatConversationMessage(msg))
129
+ }
130
+ }
131
+
132
+ // Pending Tool Calls
133
+ if (detail.pendingToolCalls.length > 0) {
134
+ lines.push('')
135
+ lines.push(`${BOLD}Pending Tool Calls${RESET} (${detail.pendingToolCalls.length})`)
136
+ for (const tc of detail.pendingToolCalls) {
137
+ lines.push(` ${MAGENTA}${tc.name}${RESET} [${tc.status}] ${DIM}${tc.id}${RESET}`)
138
+ if (tc.input) {
139
+ lines.push(` Input: ${truncate(JSON.stringify(tc.input), 100)}`)
140
+ }
141
+ }
142
+ }
143
+
144
+ return lines.join('\n')
145
+ }
146
+
147
+ function formatConversationMessage(msg: ConversationMessageView): string {
148
+ switch (msg.role) {
149
+ case 'user':
150
+ return ` ${GREEN}user:${RESET} ${truncate(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content), 100)}`
151
+ case 'assistant': {
152
+ const content = msg.content ? truncate(msg.content, 100) : ''
153
+ const toolsInfo = msg.toolCalls?.length ? ` ${DIM}[${msg.toolCalls.length} tool calls]${RESET}` : ''
154
+ return ` ${CYAN}assistant:${RESET} ${content}${toolsInfo}`
155
+ }
156
+ case 'tool': {
157
+ const errTag = msg.isError ? `${RED}[error]${RESET} ` : ''
158
+ return ` ${MAGENTA}tool(${msg.toolCallId}):${RESET} ${errTag}${truncate(msg.content, 100)}`
159
+ }
160
+ case 'system':
161
+ return ` ${DIM}system: ${truncate(msg.content, 100)}${RESET}`
162
+ }
163
+ }
164
+
165
+ // ============================================================================
166
+ // Timeline
167
+ // ============================================================================
168
+
169
+ export function formatTimelineItem(item: TimelineItem): string {
170
+ const status = `${statusColor(item.status)}${item.status}${RESET}`
171
+ const duration = formatDuration(item.durationMs)
172
+ const time = formatTimestamp(item.startedAt)
173
+
174
+ switch (item.type) {
175
+ case 'llm': {
176
+ const model = item.model ?? '-'
177
+ const tokens = item.promptTokens !== undefined ? `${item.promptTokens}/${item.completionTokens}` : '-'
178
+ return `${time} ${CYAN}llm${RESET} ${item.agentName.padEnd(20)} ${status.padEnd(20)} ${duration.padStart(8)} ${model} ${tokens}`
179
+ }
180
+ case 'tool': {
181
+ const toolName = item.toolName ?? '-'
182
+ return `${time} ${MAGENTA}tool${RESET} ${item.agentName.padEnd(20)} ${status.padEnd(20)} ${duration.padStart(8)} ${toolName}`
183
+ }
184
+ case 'compaction': {
185
+ const info = item.originalTokens !== undefined
186
+ ? `${item.originalTokens} -> ${item.compactedTokens} tokens, ${item.messagesRemoved} msgs removed`
187
+ : ''
188
+ return `${time} ${YELLOW}compact${RESET} ${item.agentName.padEnd(20)} ${status.padEnd(20)} ${DIM}${info}${RESET}`
189
+ }
190
+ }
191
+ }
192
+
193
+ // ============================================================================
194
+ // Mailbox
195
+ // ============================================================================
196
+
197
+ export function formatMailboxMessage(msg: GlobalMailboxMessage): string {
198
+ const status = msg.consumed ? `${DIM}[consumed]${RESET}` : `${YELLOW}[pending]${RESET}`
199
+ const time = formatTimestamp(msg.timestamp)
200
+ return `${time} ${msg.fromAgentName} -> ${msg.toAgentName} ${status} ${truncate(msg.content, 60)}`
201
+ }
202
+
203
+ // ============================================================================
204
+ // Metrics
205
+ // ============================================================================
206
+
207
+ export function formatMetrics(metrics: GetMetricsResponse): string {
208
+ const lines: string[] = []
209
+ lines.push(`${BOLD}Session Metrics${RESET}`)
210
+ lines.push(` Total tokens: ${metrics.totalTokens.toLocaleString()}`)
211
+ lines.push(` Prompt tokens: ${metrics.promptTokens.toLocaleString()}`)
212
+ lines.push(` Completion tokens: ${metrics.completionTokens.toLocaleString()}`)
213
+ if (metrics.totalCost !== undefined) {
214
+ lines.push(` Cost: $${metrics.totalCost.toFixed(4)}`)
215
+ }
216
+ lines.push(` LLM calls: ${metrics.llmCalls}`)
217
+ lines.push(` Tool calls: ${metrics.toolCalls}`)
218
+ lines.push(` Agent count: ${metrics.agentCount}`)
219
+ lines.push(` Duration: ${formatDuration(metrics.durationMs)}`)
220
+ return lines.join('\n')
221
+ }
222
+
223
+ // ============================================================================
224
+ // LLM Call Detail
225
+ // ============================================================================
226
+
227
+ export function formatLLMCallDetail(call: LLMCallLogEntry): string {
228
+ const lines: string[] = []
229
+
230
+ lines.push(`${BOLD}LLM Call: ${call.id}${RESET}`)
231
+ lines.push(` Agent: ${call.agentId}`)
232
+ lines.push(` Status: ${statusColor(call.status)}${call.status}${RESET}`)
233
+ lines.push(` Created: ${new Date(call.createdAt).toLocaleString()}`)
234
+ if (call.durationMs !== undefined) {
235
+ lines.push(` Duration: ${formatDuration(call.durationMs)}`)
236
+ }
237
+
238
+ // Request
239
+ lines.push('')
240
+ lines.push(`${BOLD}Request${RESET}`)
241
+ lines.push(` Model: ${call.request.model}`)
242
+ lines.push(` Messages: ${call.request.messages.length}`)
243
+ lines.push(` Tools: ${call.request.toolsCount}`)
244
+ if (call.request.systemPrompt) {
245
+ lines.push(` System: ${truncate(call.request.systemPrompt, 100)}`)
246
+ }
247
+
248
+ // Response
249
+ if (call.response) {
250
+ lines.push('')
251
+ lines.push(`${BOLD}Response${RESET}`)
252
+ lines.push(` Finish: ${call.response.finishReason}`)
253
+ if (call.response.content) {
254
+ lines.push(` Content: ${truncate(call.response.content, 200)}`)
255
+ }
256
+ if (call.response.toolCalls.length > 0) {
257
+ lines.push(` Tool calls: ${call.response.toolCalls.length}`)
258
+ for (const tc of call.response.toolCalls) {
259
+ lines.push(` ${MAGENTA}${tc.name}${RESET} ${DIM}${tc.id}${RESET}`)
260
+ }
261
+ }
262
+ }
263
+
264
+ // Metrics
265
+ if (call.metrics) {
266
+ lines.push('')
267
+ lines.push(`${BOLD}Metrics${RESET}`)
268
+ lines.push(` Prompt: ${call.metrics.promptTokens.toLocaleString()} tokens`)
269
+ lines.push(` Completion: ${call.metrics.completionTokens.toLocaleString()} tokens`)
270
+ lines.push(` Total: ${call.metrics.totalTokens.toLocaleString()} tokens`)
271
+ if (call.metrics.cost !== undefined) {
272
+ lines.push(` Cost: $${call.metrics.cost.toFixed(4)}`)
273
+ }
274
+ lines.push(` Latency: ${formatDuration(call.metrics.latencyMs)}`)
275
+ }
276
+
277
+ // Error
278
+ if (call.error) {
279
+ lines.push('')
280
+ lines.push(`${RED}${BOLD}Error${RESET}`)
281
+ lines.push(` ${RED}${call.error.type}: ${call.error.message}${RESET}`)
282
+ }
283
+
284
+ return lines.join('\n')
285
+ }
286
+
287
+ // ============================================================================
288
+ // Event Row
289
+ // ============================================================================
290
+
291
+ export function formatEventRow(event: DomainEvent, index: number): string[] {
292
+ const agentId = 'agentId' in event ? String(event.agentId) : '-'
293
+ return [
294
+ String(index),
295
+ event.type,
296
+ truncate(agentId, 24),
297
+ formatTimestamp(event.timestamp),
298
+ ]
299
+ }