@skillrecordings/cli 0.1.0
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/.env.encrypted +0 -0
- package/CHANGELOG.md +35 -0
- package/README.md +214 -0
- package/bin/skill.ts +3 -0
- package/data/tt-archive-dataset.json +1 -0
- package/data/validate-test-dataset.json +97 -0
- package/docs/CLI-AUTH.md +504 -0
- package/package.json +38 -0
- package/preload.ts +18 -0
- package/src/__tests__/init.test.ts +74 -0
- package/src/alignment-test.ts +64 -0
- package/src/check-apps.ts +16 -0
- package/src/commands/auth/decrypt.ts +123 -0
- package/src/commands/auth/encrypt.ts +81 -0
- package/src/commands/auth/index.ts +50 -0
- package/src/commands/auth/keygen.ts +41 -0
- package/src/commands/auth/status.ts +164 -0
- package/src/commands/axiom/forensic.ts +868 -0
- package/src/commands/axiom/index.ts +697 -0
- package/src/commands/build-dataset.ts +311 -0
- package/src/commands/db-status.ts +47 -0
- package/src/commands/deploys.ts +219 -0
- package/src/commands/eval-local/compare.ts +171 -0
- package/src/commands/eval-local/health.ts +212 -0
- package/src/commands/eval-local/index.ts +76 -0
- package/src/commands/eval-local/real-tools.ts +416 -0
- package/src/commands/eval-local/run.ts +1168 -0
- package/src/commands/eval-local/score-production.ts +256 -0
- package/src/commands/eval-local/seed.ts +276 -0
- package/src/commands/eval-pipeline/index.ts +53 -0
- package/src/commands/eval-pipeline/real-tools.ts +492 -0
- package/src/commands/eval-pipeline/run.ts +1316 -0
- package/src/commands/eval-pipeline/seed.ts +395 -0
- package/src/commands/eval-prompt.ts +496 -0
- package/src/commands/eval.test.ts +253 -0
- package/src/commands/eval.ts +108 -0
- package/src/commands/faq-classify.ts +460 -0
- package/src/commands/faq-cluster.ts +135 -0
- package/src/commands/faq-extract.ts +249 -0
- package/src/commands/faq-mine.ts +432 -0
- package/src/commands/faq-review.ts +426 -0
- package/src/commands/front/index.ts +351 -0
- package/src/commands/front/pull-conversations.ts +275 -0
- package/src/commands/front/tags.ts +825 -0
- package/src/commands/front-cache.ts +1277 -0
- package/src/commands/front-stats.ts +75 -0
- package/src/commands/health.test.ts +82 -0
- package/src/commands/health.ts +362 -0
- package/src/commands/init.test.ts +89 -0
- package/src/commands/init.ts +106 -0
- package/src/commands/inngest/client.ts +294 -0
- package/src/commands/inngest/events.ts +296 -0
- package/src/commands/inngest/investigate.ts +382 -0
- package/src/commands/inngest/runs.ts +149 -0
- package/src/commands/inngest/signal.ts +143 -0
- package/src/commands/kb-sync.ts +498 -0
- package/src/commands/memory/find.ts +135 -0
- package/src/commands/memory/get.ts +87 -0
- package/src/commands/memory/index.ts +97 -0
- package/src/commands/memory/stats.ts +163 -0
- package/src/commands/memory/store.ts +49 -0
- package/src/commands/memory/vote.ts +159 -0
- package/src/commands/pipeline.ts +127 -0
- package/src/commands/responses.ts +856 -0
- package/src/commands/tools.ts +293 -0
- package/src/commands/wizard.ts +319 -0
- package/src/index.ts +172 -0
- package/src/lib/crypto.ts +56 -0
- package/src/lib/env-loader.ts +206 -0
- package/src/lib/onepassword.ts +137 -0
- package/src/test-agent-local.ts +115 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI commands for querying Axiom logs and traces
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* skill axiom query "['support-traces'] | where name == 'agent.run' | limit 10"
|
|
6
|
+
* skill axiom agents --app total-typescript --limit 20
|
|
7
|
+
* skill axiom errors --since 1h
|
|
8
|
+
* skill axiom conversation <conversationId>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Axiom } from '@axiomhq/js'
|
|
12
|
+
import type { Command } from 'commander'
|
|
13
|
+
import { registerForensicCommands } from './forensic'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get dataset name from env or default
|
|
17
|
+
*/
|
|
18
|
+
function getDataset(): string {
|
|
19
|
+
return process.env.AXIOM_DATASET || 'support-agent'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get Axiom client (requires AXIOM_TOKEN env var)
|
|
24
|
+
*/
|
|
25
|
+
function getAxiomClient(): Axiom {
|
|
26
|
+
const token = process.env.AXIOM_TOKEN
|
|
27
|
+
if (!token) {
|
|
28
|
+
console.error('AXIOM_TOKEN environment variable is required')
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
return new Axiom({ token })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format duration in milliseconds to human-readable
|
|
36
|
+
*/
|
|
37
|
+
function formatDuration(ms: number): string {
|
|
38
|
+
if (ms < 1000) return `${ms}ms`
|
|
39
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
|
40
|
+
return `${(ms / 60000).toFixed(1)}m`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Format timestamp
|
|
45
|
+
*/
|
|
46
|
+
function formatTime(timestamp: string | Date): string {
|
|
47
|
+
const date = new Date(timestamp)
|
|
48
|
+
return date.toLocaleString('en-US', {
|
|
49
|
+
month: 'short',
|
|
50
|
+
day: 'numeric',
|
|
51
|
+
hour: '2-digit',
|
|
52
|
+
minute: '2-digit',
|
|
53
|
+
second: '2-digit',
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse time range string to start/end dates
|
|
59
|
+
*/
|
|
60
|
+
function parseTimeRange(since: string): { startTime: Date; endTime: Date } {
|
|
61
|
+
const endTime = new Date()
|
|
62
|
+
let startTime: Date
|
|
63
|
+
|
|
64
|
+
// Parse duration strings like "1h", "24h", "7d"
|
|
65
|
+
const match = since.match(/^(\d+)([hmd])$/)
|
|
66
|
+
if (match && match[1] && match[2]) {
|
|
67
|
+
const value = parseInt(match[1], 10)
|
|
68
|
+
const unit = match[2] as 'h' | 'm' | 'd'
|
|
69
|
+
const msPerUnit: Record<'h' | 'm' | 'd', number> = {
|
|
70
|
+
h: 60 * 60 * 1000,
|
|
71
|
+
m: 60 * 1000,
|
|
72
|
+
d: 24 * 60 * 60 * 1000,
|
|
73
|
+
}
|
|
74
|
+
startTime = new Date(endTime.getTime() - value * msPerUnit[unit])
|
|
75
|
+
} else {
|
|
76
|
+
// Try ISO date
|
|
77
|
+
startTime = new Date(since)
|
|
78
|
+
if (isNaN(startTime.getTime())) {
|
|
79
|
+
console.error(
|
|
80
|
+
`Invalid time range: ${since}. Use format like "1h", "24h", "7d" or ISO date.`
|
|
81
|
+
)
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { startTime, endTime }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run a raw APL query
|
|
91
|
+
*/
|
|
92
|
+
async function runQuery(
|
|
93
|
+
apl: string,
|
|
94
|
+
options: { since?: string; json?: boolean }
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const client = getAxiomClient()
|
|
97
|
+
const { startTime, endTime } = parseTimeRange(options.since ?? '24h')
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = await client.query(apl, {
|
|
101
|
+
startTime: startTime.toISOString(),
|
|
102
|
+
endTime: endTime.toISOString(),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if (options.json) {
|
|
106
|
+
console.log(JSON.stringify(result, null, 2))
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Display results in table format
|
|
111
|
+
const matches = result.matches ?? []
|
|
112
|
+
if (matches.length === 0) {
|
|
113
|
+
console.log('No results found')
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(
|
|
118
|
+
`\nFound ${matches.length} results (${result.status?.elapsedTime}ms)`
|
|
119
|
+
)
|
|
120
|
+
console.log('='.repeat(80))
|
|
121
|
+
|
|
122
|
+
for (const match of matches) {
|
|
123
|
+
const data = match.data as Record<string, unknown>
|
|
124
|
+
console.log(`\n[${formatTime(match._time)}]`)
|
|
125
|
+
for (const [key, value] of Object.entries(data)) {
|
|
126
|
+
if (key.startsWith('_')) continue
|
|
127
|
+
const displayValue =
|
|
128
|
+
typeof value === 'object' ? JSON.stringify(value) : value
|
|
129
|
+
console.log(` ${key}: ${displayValue}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(
|
|
134
|
+
'Query failed:',
|
|
135
|
+
error instanceof Error ? error.message : error
|
|
136
|
+
)
|
|
137
|
+
process.exit(1)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* List recent agent runs
|
|
143
|
+
*/
|
|
144
|
+
async function listAgentRuns(options: {
|
|
145
|
+
app?: string
|
|
146
|
+
limit?: number
|
|
147
|
+
since?: string
|
|
148
|
+
json?: boolean
|
|
149
|
+
}): Promise<void> {
|
|
150
|
+
const client = getAxiomClient()
|
|
151
|
+
const limit = options.limit ?? 20
|
|
152
|
+
const { startTime, endTime } = parseTimeRange(options.since ?? '24h')
|
|
153
|
+
|
|
154
|
+
let apl = `['${getDataset()}']
|
|
155
|
+
| where name == 'agent.run'`
|
|
156
|
+
|
|
157
|
+
if (options.app) {
|
|
158
|
+
apl += `
|
|
159
|
+
| where appId == '${options.app}'`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
apl += `
|
|
163
|
+
| sort by _time desc
|
|
164
|
+
| limit ${limit}`
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const result = await client.query(apl, {
|
|
168
|
+
startTime: startTime.toISOString(),
|
|
169
|
+
endTime: endTime.toISOString(),
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const matches = result.matches ?? []
|
|
173
|
+
|
|
174
|
+
if (options.json) {
|
|
175
|
+
console.log(
|
|
176
|
+
JSON.stringify(
|
|
177
|
+
matches.map((m) => m.data),
|
|
178
|
+
null,
|
|
179
|
+
2
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (matches.length === 0) {
|
|
186
|
+
console.log('No agent runs found')
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log('\nRecent Agent Runs')
|
|
191
|
+
console.log('='.repeat(100))
|
|
192
|
+
|
|
193
|
+
for (const match of matches) {
|
|
194
|
+
const d = match.data as Record<string, unknown>
|
|
195
|
+
const time = formatTime(match._time)
|
|
196
|
+
const duration = formatDuration(Number(d.durationMs) || 0)
|
|
197
|
+
const app = d.appId ?? 'unknown'
|
|
198
|
+
const tools = (d.toolCallsCount ?? 0) + ' tools'
|
|
199
|
+
const model = String(d.model ?? '').replace('anthropic/', '')
|
|
200
|
+
const approval = d.requiresApproval ? '!' : d.autoSent ? '+' : '-'
|
|
201
|
+
|
|
202
|
+
console.log(`\n[${time}] ${app} (${duration})`)
|
|
203
|
+
console.log(` Model: ${model} | Tools: ${tools} | Auto: ${approval}`)
|
|
204
|
+
console.log(` Conv: ${d.conversationId}`)
|
|
205
|
+
if (d.customerEmail) console.log(` Customer: ${d.customerEmail}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Summary stats
|
|
209
|
+
const durations = matches.map(
|
|
210
|
+
(m) => Number((m.data as Record<string, unknown>).durationMs) || 0
|
|
211
|
+
)
|
|
212
|
+
const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length
|
|
213
|
+
const autoSent = matches.filter(
|
|
214
|
+
(m) => (m.data as Record<string, unknown>).autoSent
|
|
215
|
+
).length
|
|
216
|
+
const approvals = matches.filter(
|
|
217
|
+
(m) => (m.data as Record<string, unknown>).requiresApproval
|
|
218
|
+
).length
|
|
219
|
+
|
|
220
|
+
console.log('\n' + '-'.repeat(100))
|
|
221
|
+
console.log(
|
|
222
|
+
`Total: ${matches.length} | Avg duration: ${formatDuration(avgDuration)} | Auto-sent: ${autoSent} | Approvals: ${approvals}`
|
|
223
|
+
)
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error(
|
|
226
|
+
'Query failed:',
|
|
227
|
+
error instanceof Error ? error.message : error
|
|
228
|
+
)
|
|
229
|
+
process.exit(1)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* List recent errors
|
|
235
|
+
*/
|
|
236
|
+
async function listErrors(options: {
|
|
237
|
+
since?: string
|
|
238
|
+
limit?: number
|
|
239
|
+
json?: boolean
|
|
240
|
+
}): Promise<void> {
|
|
241
|
+
const client = getAxiomClient()
|
|
242
|
+
const limit = options.limit ?? 50
|
|
243
|
+
const { startTime, endTime } = parseTimeRange(options.since ?? '24h')
|
|
244
|
+
|
|
245
|
+
const apl = `['${getDataset()}']
|
|
246
|
+
| where status == 'error' or error != ''
|
|
247
|
+
| sort by _time desc
|
|
248
|
+
| limit ${limit}`
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const result = await client.query(apl, {
|
|
252
|
+
startTime: startTime.toISOString(),
|
|
253
|
+
endTime: endTime.toISOString(),
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const matches = result.matches ?? []
|
|
257
|
+
|
|
258
|
+
if (options.json) {
|
|
259
|
+
console.log(
|
|
260
|
+
JSON.stringify(
|
|
261
|
+
matches.map((m) => m.data),
|
|
262
|
+
null,
|
|
263
|
+
2
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (matches.length === 0) {
|
|
270
|
+
console.log('No errors found')
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log('\nRecent Errors')
|
|
275
|
+
console.log('='.repeat(100))
|
|
276
|
+
|
|
277
|
+
for (const match of matches) {
|
|
278
|
+
const d = match.data as Record<string, unknown>
|
|
279
|
+
const time = formatTime(match._time)
|
|
280
|
+
const name = d.name ?? 'unknown'
|
|
281
|
+
const error =
|
|
282
|
+
d.error ??
|
|
283
|
+
d.errorStack ??
|
|
284
|
+
d.message ??
|
|
285
|
+
`[${d.level ?? 'error'}] ${d.name ?? 'unknown event'}`
|
|
286
|
+
|
|
287
|
+
console.log(`\n[${time}] ${name}`)
|
|
288
|
+
console.log(` Error: ${String(error).slice(0, 200)}`)
|
|
289
|
+
if (d.conversationId) console.log(` Conv: ${d.conversationId}`)
|
|
290
|
+
if (d.appId) console.log(` App: ${d.appId}`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log('\n' + '-'.repeat(100))
|
|
294
|
+
console.log(`Total errors: ${matches.length}`)
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error(
|
|
297
|
+
'Query failed:',
|
|
298
|
+
error instanceof Error ? error.message : error
|
|
299
|
+
)
|
|
300
|
+
process.exit(1)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get all events for a conversation
|
|
306
|
+
*/
|
|
307
|
+
async function getConversation(
|
|
308
|
+
conversationId: string,
|
|
309
|
+
options: { since?: string; json?: boolean }
|
|
310
|
+
): Promise<void> {
|
|
311
|
+
const client = getAxiomClient()
|
|
312
|
+
const { startTime, endTime } = parseTimeRange(options.since ?? '7d')
|
|
313
|
+
|
|
314
|
+
const apl = `['${getDataset()}']
|
|
315
|
+
| where conversationId == '${conversationId}'
|
|
316
|
+
| sort by _time asc`
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const result = await client.query(apl, {
|
|
320
|
+
startTime: startTime.toISOString(),
|
|
321
|
+
endTime: endTime.toISOString(),
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const matches = result.matches ?? []
|
|
325
|
+
|
|
326
|
+
if (options.json) {
|
|
327
|
+
console.log(
|
|
328
|
+
JSON.stringify(
|
|
329
|
+
matches.map((m) => ({ _time: m._time, ...(m.data as object) })),
|
|
330
|
+
null,
|
|
331
|
+
2
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (matches.length === 0) {
|
|
338
|
+
console.log(`No events found for conversation: ${conversationId}`)
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log(`\nConversation Timeline: ${conversationId}`)
|
|
343
|
+
console.log('='.repeat(100))
|
|
344
|
+
|
|
345
|
+
for (const match of matches) {
|
|
346
|
+
const d = match.data as Record<string, unknown>
|
|
347
|
+
const time = formatTime(match._time)
|
|
348
|
+
const name = d.name ?? d.type ?? 'event'
|
|
349
|
+
const duration = d.durationMs
|
|
350
|
+
? ` (${formatDuration(Number(d.durationMs))})`
|
|
351
|
+
: ''
|
|
352
|
+
|
|
353
|
+
console.log(`\n[${time}] ${name}${duration}`)
|
|
354
|
+
|
|
355
|
+
// Show relevant fields based on event type
|
|
356
|
+
if (d.category) console.log(` Category: ${d.category} (${d.confidence})`)
|
|
357
|
+
if (d.complexity) console.log(` Complexity: ${d.complexity}`)
|
|
358
|
+
if (d.routingType) console.log(` Routing: ${d.routingType}`)
|
|
359
|
+
if (d.model) console.log(` Model: ${d.model}`)
|
|
360
|
+
if (d.toolCallsCount)
|
|
361
|
+
console.log(
|
|
362
|
+
` Tools: ${d.toolCallsCount} (${(d.toolNames as string[])?.join(', ') ?? ''})`
|
|
363
|
+
)
|
|
364
|
+
if (d.memoriesRetrieved) console.log(` Memories: ${d.memoriesRetrieved}`)
|
|
365
|
+
if (d.error) console.log(` Error: ${d.error}`)
|
|
366
|
+
if (d.reasoning)
|
|
367
|
+
console.log(` Reasoning: ${String(d.reasoning).slice(0, 150)}`)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
console.log('\n' + '-'.repeat(100))
|
|
371
|
+
console.log(`Total events: ${matches.length}`)
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error(
|
|
374
|
+
'Query failed:',
|
|
375
|
+
error instanceof Error ? error.message : error
|
|
376
|
+
)
|
|
377
|
+
process.exit(1)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get classification distribution
|
|
383
|
+
*/
|
|
384
|
+
async function getClassificationStats(options: {
|
|
385
|
+
app?: string
|
|
386
|
+
since?: string
|
|
387
|
+
json?: boolean
|
|
388
|
+
}): Promise<void> {
|
|
389
|
+
const client = getAxiomClient()
|
|
390
|
+
const { startTime, endTime } = parseTimeRange(options.since ?? '24h')
|
|
391
|
+
|
|
392
|
+
let apl = `['${getDataset()}']
|
|
393
|
+
| where name == 'classifier.run'`
|
|
394
|
+
|
|
395
|
+
if (options.app) {
|
|
396
|
+
apl += `
|
|
397
|
+
| where appId == '${options.app}'`
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
apl += `
|
|
401
|
+
| summarize count = count() by category, complexity`
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const result = await client.query(apl, {
|
|
405
|
+
startTime: startTime.toISOString(),
|
|
406
|
+
endTime: endTime.toISOString(),
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const buckets = result.buckets?.totals ?? []
|
|
410
|
+
|
|
411
|
+
if (options.json) {
|
|
412
|
+
console.log(JSON.stringify(buckets, null, 2))
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (buckets.length === 0) {
|
|
417
|
+
console.log('No classification data found')
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log('\nClassification Distribution')
|
|
422
|
+
console.log('='.repeat(60))
|
|
423
|
+
|
|
424
|
+
// Group by category
|
|
425
|
+
const byCategory: Record<
|
|
426
|
+
string,
|
|
427
|
+
{ total: number; complexities: Record<string, number> }
|
|
428
|
+
> = {}
|
|
429
|
+
for (const bucket of buckets) {
|
|
430
|
+
const group = bucket.group as Record<string, string>
|
|
431
|
+
const category = group.category ?? 'unknown'
|
|
432
|
+
const complexity = group.complexity ?? 'unknown'
|
|
433
|
+
const count = Number(bucket.aggregations?.[0]?.value ?? 0)
|
|
434
|
+
|
|
435
|
+
if (!byCategory[category]) {
|
|
436
|
+
byCategory[category] = { total: 0, complexities: {} }
|
|
437
|
+
}
|
|
438
|
+
byCategory[category].total += count
|
|
439
|
+
byCategory[category].complexities[complexity] =
|
|
440
|
+
(byCategory[category].complexities[complexity] ?? 0) + count
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Sort by total count
|
|
444
|
+
const sorted = Object.entries(byCategory).sort(
|
|
445
|
+
(a, b) => b[1].total - a[1].total
|
|
446
|
+
)
|
|
447
|
+
const grandTotal = sorted.reduce((sum, [, v]) => sum + v.total, 0)
|
|
448
|
+
|
|
449
|
+
for (const [category, { total, complexities }] of sorted) {
|
|
450
|
+
const pct = ((total / grandTotal) * 100).toFixed(1)
|
|
451
|
+
const complexityStr = Object.entries(complexities)
|
|
452
|
+
.map(([c, n]) => `${c}:${n}`)
|
|
453
|
+
.join(', ')
|
|
454
|
+
console.log(
|
|
455
|
+
`${category.padEnd(25)} ${String(total).padStart(5)} (${pct.padStart(5)}%) [${complexityStr}]`
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
console.log('-'.repeat(60))
|
|
460
|
+
console.log(`Total classifications: ${grandTotal}`)
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error(
|
|
463
|
+
'Query failed:',
|
|
464
|
+
error instanceof Error ? error.message : error
|
|
465
|
+
)
|
|
466
|
+
process.exit(1)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* List workflow step traces (for debugging timeout issues)
|
|
472
|
+
*/
|
|
473
|
+
async function listWorkflowSteps(options: {
|
|
474
|
+
workflow?: string
|
|
475
|
+
conversation?: string
|
|
476
|
+
since?: string
|
|
477
|
+
limit?: number
|
|
478
|
+
json?: boolean
|
|
479
|
+
}): Promise<void> {
|
|
480
|
+
const client = getAxiomClient()
|
|
481
|
+
const limit = options.limit ?? 50
|
|
482
|
+
const { startTime, endTime } = parseTimeRange(options.since ?? '24h')
|
|
483
|
+
|
|
484
|
+
let apl = `['${getDataset()}']
|
|
485
|
+
| where type == 'workflow-step'`
|
|
486
|
+
|
|
487
|
+
if (options.workflow) {
|
|
488
|
+
apl += `
|
|
489
|
+
| where workflowName == '${options.workflow}'`
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (options.conversation) {
|
|
493
|
+
apl += `
|
|
494
|
+
| where conversationId == '${options.conversation}'`
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
apl += `
|
|
498
|
+
| sort by _time desc
|
|
499
|
+
| limit ${limit}`
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const result = await client.query(apl, {
|
|
503
|
+
startTime: startTime.toISOString(),
|
|
504
|
+
endTime: endTime.toISOString(),
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
const matches = result.matches ?? []
|
|
508
|
+
|
|
509
|
+
if (options.json) {
|
|
510
|
+
console.log(
|
|
511
|
+
JSON.stringify(
|
|
512
|
+
matches.map((m) => m.data),
|
|
513
|
+
null,
|
|
514
|
+
2
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (matches.length === 0) {
|
|
521
|
+
console.log('No workflow steps found')
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
console.log('\nWorkflow Steps')
|
|
526
|
+
console.log('='.repeat(100))
|
|
527
|
+
|
|
528
|
+
for (const match of matches) {
|
|
529
|
+
const d = match.data as Record<string, unknown>
|
|
530
|
+
const time = formatTime(match._time)
|
|
531
|
+
const workflow = d.workflowName ?? 'unknown'
|
|
532
|
+
const step = d.stepName ?? 'unknown'
|
|
533
|
+
const duration = formatDuration(Number(d.durationMs) || 0)
|
|
534
|
+
const success = d.success ? '✓' : '✗'
|
|
535
|
+
|
|
536
|
+
console.log(`\n[${time}] ${workflow} > ${step} ${success} (${duration})`)
|
|
537
|
+
if (d.conversationId) console.log(` Conv: ${d.conversationId}`)
|
|
538
|
+
if (d.error) console.log(` Error: ${d.error}`)
|
|
539
|
+
if (d.metadata) console.log(` Meta: ${JSON.stringify(d.metadata)}`)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log('\n' + '-'.repeat(100))
|
|
543
|
+
console.log(`Total: ${matches.length}`)
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error(
|
|
546
|
+
'Query failed:',
|
|
547
|
+
error instanceof Error ? error.message : error
|
|
548
|
+
)
|
|
549
|
+
process.exit(1)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* List approval-related traces (for debugging HITL flow)
|
|
555
|
+
*/
|
|
556
|
+
async function listApprovals(options: {
|
|
557
|
+
since?: string
|
|
558
|
+
limit?: number
|
|
559
|
+
json?: boolean
|
|
560
|
+
}): Promise<void> {
|
|
561
|
+
const client = getAxiomClient()
|
|
562
|
+
const limit = options.limit ?? 30
|
|
563
|
+
const { startTime, endTime } = parseTimeRange(options.since ?? '24h')
|
|
564
|
+
|
|
565
|
+
const apl = `['${getDataset()}']
|
|
566
|
+
| where type in ('approval', 'slack')
|
|
567
|
+
| sort by _time desc
|
|
568
|
+
| limit ${limit}`
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
const result = await client.query(apl, {
|
|
572
|
+
startTime: startTime.toISOString(),
|
|
573
|
+
endTime: endTime.toISOString(),
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
const matches = result.matches ?? []
|
|
577
|
+
|
|
578
|
+
if (options.json) {
|
|
579
|
+
console.log(
|
|
580
|
+
JSON.stringify(
|
|
581
|
+
matches.map((m) => m.data),
|
|
582
|
+
null,
|
|
583
|
+
2
|
|
584
|
+
)
|
|
585
|
+
)
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (matches.length === 0) {
|
|
590
|
+
console.log('No approval traces found')
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
console.log('\nApproval Flow Traces')
|
|
595
|
+
console.log('='.repeat(100))
|
|
596
|
+
|
|
597
|
+
for (const match of matches) {
|
|
598
|
+
const d = match.data as Record<string, unknown>
|
|
599
|
+
const time = formatTime(match._time)
|
|
600
|
+
const name = d.name ?? 'unknown'
|
|
601
|
+
const actionId = d.actionId ?? ''
|
|
602
|
+
const success = d.success !== undefined ? (d.success ? '✓' : '✗') : ''
|
|
603
|
+
const duration = d.durationMs
|
|
604
|
+
? ` (${formatDuration(Number(d.durationMs))})`
|
|
605
|
+
: ''
|
|
606
|
+
|
|
607
|
+
console.log(`\n[${time}] ${name} ${success}${duration}`)
|
|
608
|
+
if (actionId) console.log(` Action: ${actionId}`)
|
|
609
|
+
if (d.actionType) console.log(` Type: ${d.actionType}`)
|
|
610
|
+
if (d.conversationId) console.log(` Conv: ${d.conversationId}`)
|
|
611
|
+
if (d.channel) console.log(` Channel: ${d.channel}`)
|
|
612
|
+
if (d.messageTs) console.log(` Slack TS: ${d.messageTs}`)
|
|
613
|
+
if (d.error) console.log(` Error: ${d.error}`)
|
|
614
|
+
if (d.customerEmail) console.log(` Customer: ${d.customerEmail}`)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
console.log('\n' + '-'.repeat(100))
|
|
618
|
+
console.log(`Total: ${matches.length}`)
|
|
619
|
+
} catch (error) {
|
|
620
|
+
console.error(
|
|
621
|
+
'Query failed:',
|
|
622
|
+
error instanceof Error ? error.message : error
|
|
623
|
+
)
|
|
624
|
+
process.exit(1)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Register Axiom commands with Commander
|
|
630
|
+
*/
|
|
631
|
+
export function registerAxiomCommands(program: Command): void {
|
|
632
|
+
const axiom = program
|
|
633
|
+
.command('axiom')
|
|
634
|
+
.description('Query Axiom logs and traces')
|
|
635
|
+
|
|
636
|
+
axiom
|
|
637
|
+
.command('query')
|
|
638
|
+
.description('Run a raw APL query')
|
|
639
|
+
.argument('<apl>', 'APL query string')
|
|
640
|
+
.option('-s, --since <time>', 'Time range (e.g., 1h, 24h, 7d)', '24h')
|
|
641
|
+
.option('--json', 'Output as JSON')
|
|
642
|
+
.action(runQuery)
|
|
643
|
+
|
|
644
|
+
axiom
|
|
645
|
+
.command('agents')
|
|
646
|
+
.description('List recent agent runs')
|
|
647
|
+
.option('-a, --app <slug>', 'Filter by app')
|
|
648
|
+
.option('-l, --limit <n>', 'Number of results', parseInt)
|
|
649
|
+
.option('-s, --since <time>', 'Time range (e.g., 1h, 24h, 7d)', '24h')
|
|
650
|
+
.option('--json', 'Output as JSON')
|
|
651
|
+
.action(listAgentRuns)
|
|
652
|
+
|
|
653
|
+
axiom
|
|
654
|
+
.command('errors')
|
|
655
|
+
.description('List recent errors')
|
|
656
|
+
.option('-l, --limit <n>', 'Number of results', parseInt)
|
|
657
|
+
.option('-s, --since <time>', 'Time range (e.g., 1h, 24h, 7d)', '24h')
|
|
658
|
+
.option('--json', 'Output as JSON')
|
|
659
|
+
.action(listErrors)
|
|
660
|
+
|
|
661
|
+
axiom
|
|
662
|
+
.command('conversation')
|
|
663
|
+
.description('Get all events for a conversation')
|
|
664
|
+
.argument('<conversationId>', 'Front conversation ID')
|
|
665
|
+
.option('-s, --since <time>', 'Time range (e.g., 1h, 24h, 7d)', '7d')
|
|
666
|
+
.option('--json', 'Output as JSON')
|
|
667
|
+
.action(getConversation)
|
|
668
|
+
|
|
669
|
+
axiom
|
|
670
|
+
.command('classifications')
|
|
671
|
+
.description('Show classification distribution')
|
|
672
|
+
.option('-a, --app <slug>', 'Filter by app')
|
|
673
|
+
.option('-s, --since <time>', 'Time range (e.g., 1h, 24h, 7d)', '24h')
|
|
674
|
+
.option('--json', 'Output as JSON')
|
|
675
|
+
.action(getClassificationStats)
|
|
676
|
+
|
|
677
|
+
axiom
|
|
678
|
+
.command('workflow-steps')
|
|
679
|
+
.description('List workflow step traces (for debugging timeouts)')
|
|
680
|
+
.option('-w, --workflow <name>', 'Filter by workflow name')
|
|
681
|
+
.option('-c, --conversation <id>', 'Filter by conversation ID')
|
|
682
|
+
.option('-l, --limit <n>', 'Number of results', parseInt)
|
|
683
|
+
.option('-s, --since <time>', 'Time range (e.g., 1h, 24h, 7d)', '24h')
|
|
684
|
+
.option('--json', 'Output as JSON')
|
|
685
|
+
.action(listWorkflowSteps)
|
|
686
|
+
|
|
687
|
+
axiom
|
|
688
|
+
.command('approvals')
|
|
689
|
+
.description('List approval flow traces (HITL debugging)')
|
|
690
|
+
.option('-l, --limit <n>', 'Number of results', parseInt)
|
|
691
|
+
.option('-s, --since <time>', 'Time range (e.g., 1h, 24h, 7d)', '24h')
|
|
692
|
+
.option('--json', 'Output as JSON')
|
|
693
|
+
.action(listApprovals)
|
|
694
|
+
|
|
695
|
+
// Register forensic / self-diagnosis queries
|
|
696
|
+
registerForensicCommands(axiom)
|
|
697
|
+
}
|