@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.
Files changed (73) hide show
  1. package/.env.encrypted +0 -0
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +214 -0
  4. package/bin/skill.ts +3 -0
  5. package/data/tt-archive-dataset.json +1 -0
  6. package/data/validate-test-dataset.json +97 -0
  7. package/docs/CLI-AUTH.md +504 -0
  8. package/package.json +38 -0
  9. package/preload.ts +18 -0
  10. package/src/__tests__/init.test.ts +74 -0
  11. package/src/alignment-test.ts +64 -0
  12. package/src/check-apps.ts +16 -0
  13. package/src/commands/auth/decrypt.ts +123 -0
  14. package/src/commands/auth/encrypt.ts +81 -0
  15. package/src/commands/auth/index.ts +50 -0
  16. package/src/commands/auth/keygen.ts +41 -0
  17. package/src/commands/auth/status.ts +164 -0
  18. package/src/commands/axiom/forensic.ts +868 -0
  19. package/src/commands/axiom/index.ts +697 -0
  20. package/src/commands/build-dataset.ts +311 -0
  21. package/src/commands/db-status.ts +47 -0
  22. package/src/commands/deploys.ts +219 -0
  23. package/src/commands/eval-local/compare.ts +171 -0
  24. package/src/commands/eval-local/health.ts +212 -0
  25. package/src/commands/eval-local/index.ts +76 -0
  26. package/src/commands/eval-local/real-tools.ts +416 -0
  27. package/src/commands/eval-local/run.ts +1168 -0
  28. package/src/commands/eval-local/score-production.ts +256 -0
  29. package/src/commands/eval-local/seed.ts +276 -0
  30. package/src/commands/eval-pipeline/index.ts +53 -0
  31. package/src/commands/eval-pipeline/real-tools.ts +492 -0
  32. package/src/commands/eval-pipeline/run.ts +1316 -0
  33. package/src/commands/eval-pipeline/seed.ts +395 -0
  34. package/src/commands/eval-prompt.ts +496 -0
  35. package/src/commands/eval.test.ts +253 -0
  36. package/src/commands/eval.ts +108 -0
  37. package/src/commands/faq-classify.ts +460 -0
  38. package/src/commands/faq-cluster.ts +135 -0
  39. package/src/commands/faq-extract.ts +249 -0
  40. package/src/commands/faq-mine.ts +432 -0
  41. package/src/commands/faq-review.ts +426 -0
  42. package/src/commands/front/index.ts +351 -0
  43. package/src/commands/front/pull-conversations.ts +275 -0
  44. package/src/commands/front/tags.ts +825 -0
  45. package/src/commands/front-cache.ts +1277 -0
  46. package/src/commands/front-stats.ts +75 -0
  47. package/src/commands/health.test.ts +82 -0
  48. package/src/commands/health.ts +362 -0
  49. package/src/commands/init.test.ts +89 -0
  50. package/src/commands/init.ts +106 -0
  51. package/src/commands/inngest/client.ts +294 -0
  52. package/src/commands/inngest/events.ts +296 -0
  53. package/src/commands/inngest/investigate.ts +382 -0
  54. package/src/commands/inngest/runs.ts +149 -0
  55. package/src/commands/inngest/signal.ts +143 -0
  56. package/src/commands/kb-sync.ts +498 -0
  57. package/src/commands/memory/find.ts +135 -0
  58. package/src/commands/memory/get.ts +87 -0
  59. package/src/commands/memory/index.ts +97 -0
  60. package/src/commands/memory/stats.ts +163 -0
  61. package/src/commands/memory/store.ts +49 -0
  62. package/src/commands/memory/vote.ts +159 -0
  63. package/src/commands/pipeline.ts +127 -0
  64. package/src/commands/responses.ts +856 -0
  65. package/src/commands/tools.ts +293 -0
  66. package/src/commands/wizard.ts +319 -0
  67. package/src/index.ts +172 -0
  68. package/src/lib/crypto.ts +56 -0
  69. package/src/lib/env-loader.ts +206 -0
  70. package/src/lib/onepassword.ts +137 -0
  71. package/src/test-agent-local.ts +115 -0
  72. package/tsconfig.json +11 -0
  73. package/vitest.config.ts +10 -0
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Front CLI commands for debugging and investigation
3
+ *
4
+ * Provides direct access to Front API for:
5
+ * - Fetching messages (body, author, recipients)
6
+ * - Fetching conversations with message history
7
+ * - Listing and looking up teammates
8
+ * - Comparing webhook data vs API data
9
+ */
10
+
11
+ import { createInstrumentedFrontClient } from '@skillrecordings/core/front/instrumented-client'
12
+ import type {
13
+ Message as FrontMessage,
14
+ MessageList,
15
+ } from '@skillrecordings/front-sdk'
16
+ import type { Command } from 'commander'
17
+ import { registerCacheCommand } from '../front-cache'
18
+ import { registerPullCommand } from './pull-conversations'
19
+ import { registerTagCommands } from './tags'
20
+
21
+ type Message = FrontMessage
22
+
23
+ /**
24
+ * Get Front API client from environment (instrumented)
25
+ */
26
+ function getFrontClient() {
27
+ const apiToken = process.env.FRONT_API_TOKEN
28
+ if (!apiToken) {
29
+ throw new Error('FRONT_API_TOKEN environment variable is required')
30
+ }
31
+ return createInstrumentedFrontClient({ apiToken })
32
+ }
33
+
34
+ /**
35
+ * Get Front SDK client from environment (full typed client)
36
+ */
37
+ function getFrontSdkClient() {
38
+ const apiToken = process.env.FRONT_API_TOKEN
39
+ if (!apiToken) {
40
+ throw new Error('FRONT_API_TOKEN environment variable is required')
41
+ }
42
+ return createInstrumentedFrontClient({ apiToken })
43
+ }
44
+
45
+ /**
46
+ * Format timestamp to human-readable
47
+ */
48
+ function formatTimestamp(ts: number): string {
49
+ return new Date(ts * 1000).toLocaleString('en-US', {
50
+ month: 'short',
51
+ day: 'numeric',
52
+ hour: '2-digit',
53
+ minute: '2-digit',
54
+ })
55
+ }
56
+
57
+ /**
58
+ * Truncate string with ellipsis
59
+ */
60
+ function truncate(str: string, len: number): string {
61
+ if (str.length <= len) return str
62
+ return str.slice(0, len - 3) + '...'
63
+ }
64
+
65
+ /**
66
+ * Normalize Front resource ID or URL to ID
67
+ */
68
+ function normalizeId(idOrUrl: string): string {
69
+ return idOrUrl.startsWith('http') ? idOrUrl.split('/').pop()! : idOrUrl
70
+ }
71
+
72
+ /**
73
+ * Command: skill front message <id>
74
+ * Fetch full message details from Front API
75
+ */
76
+ async function getMessage(
77
+ id: string,
78
+ options: { json?: boolean }
79
+ ): Promise<void> {
80
+ try {
81
+ const front = getFrontClient()
82
+ const message = await front.messages.get(normalizeId(id))
83
+
84
+ if (options.json) {
85
+ console.log(JSON.stringify(message, null, 2))
86
+ return
87
+ }
88
+
89
+ console.log('\n📧 Message Details:')
90
+ console.log(` ID: ${message.id}`)
91
+ console.log(` Type: ${message.type}`)
92
+ console.log(` Subject: ${message.subject || '(none)'}`)
93
+ console.log(` Created: ${formatTimestamp(message.created_at)}`)
94
+
95
+ if (message.author) {
96
+ console.log(` Author: ${message.author.email || message.author.id}`)
97
+ }
98
+
99
+ console.log('\n📬 Recipients:')
100
+ for (const r of message.recipients) {
101
+ console.log(` ${r.role}: ${r.handle}`)
102
+ }
103
+
104
+ console.log('\n📝 Body:')
105
+ // Strip HTML and show preview
106
+ const textBody =
107
+ message.text ||
108
+ message.body
109
+ .replace(/<[^>]*>/g, ' ')
110
+ .replace(/\s+/g, ' ')
111
+ .trim()
112
+ console.log(
113
+ ` Length: ${message.body.length} chars (HTML), ${textBody.length} chars (text)`
114
+ )
115
+ console.log(` Preview: ${truncate(textBody, 500)}`)
116
+
117
+ if (message.attachments && message.attachments.length > 0) {
118
+ console.log(`\n📎 Attachments: ${message.attachments.length}`)
119
+ for (const a of message.attachments) {
120
+ console.log(` - ${a.filename} (${a.content_type})`)
121
+ }
122
+ }
123
+
124
+ console.log('')
125
+ } catch (error) {
126
+ if (options.json) {
127
+ console.error(
128
+ JSON.stringify({
129
+ error: error instanceof Error ? error.message : 'Unknown error',
130
+ })
131
+ )
132
+ } else {
133
+ console.error(
134
+ 'Error:',
135
+ error instanceof Error ? error.message : 'Unknown error'
136
+ )
137
+ }
138
+ process.exit(1)
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Command: skill front conversation <id>
144
+ * Fetch conversation details and optionally messages
145
+ */
146
+ async function getConversation(
147
+ id: string,
148
+ options: { json?: boolean; messages?: boolean }
149
+ ): Promise<void> {
150
+ try {
151
+ const front = getFrontClient()
152
+ const conversation = await front.conversations.get(normalizeId(id))
153
+
154
+ // Fetch messages if requested
155
+ let messages: Message[] | undefined
156
+ if (options.messages) {
157
+ const messageList = (await front.conversations.listMessages(
158
+ normalizeId(id)
159
+ )) as MessageList
160
+ messages = messageList._results ?? []
161
+ }
162
+
163
+ if (options.json) {
164
+ console.log(JSON.stringify({ conversation, messages }, null, 2))
165
+ return
166
+ }
167
+
168
+ console.log('\n💬 Conversation Details:')
169
+ console.log(` ID: ${conversation.id}`)
170
+ console.log(` Subject: ${conversation.subject || '(none)'}`)
171
+ console.log(` Status: ${conversation.status}`)
172
+ console.log(` Created: ${formatTimestamp(conversation.created_at)}`)
173
+
174
+ if (conversation.recipient) {
175
+ console.log(` Recipient: ${conversation.recipient.handle}`)
176
+ }
177
+
178
+ if (conversation.assignee) {
179
+ console.log(` Assignee: ${conversation.assignee.email}`)
180
+ }
181
+
182
+ if (conversation.tags && conversation.tags.length > 0) {
183
+ console.log(
184
+ ` Tags: ${conversation.tags.map((t: { name: string }) => t.name).join(', ')}`
185
+ )
186
+ }
187
+
188
+ if (options.messages && messages) {
189
+ console.log(`\n📨 Messages (${messages.length}):`)
190
+ console.log('-'.repeat(80))
191
+
192
+ for (const msg of messages) {
193
+ const direction = msg.is_inbound ? '← IN' : '→ OUT'
194
+ const author = msg.author?.email || 'unknown'
195
+ const time = formatTimestamp(msg.created_at)
196
+ const textBody =
197
+ msg.text ||
198
+ msg.body
199
+ .replace(/<[^>]*>/g, ' ')
200
+ .replace(/\s+/g, ' ')
201
+ .trim()
202
+
203
+ console.log(`\n[${direction}] ${time} - ${author}`)
204
+ console.log(` ${truncate(textBody, 200)}`)
205
+ }
206
+ } else if (!options.messages) {
207
+ console.log('\n (use --messages to see message history)')
208
+ }
209
+
210
+ console.log('')
211
+ } catch (error) {
212
+ if (options.json) {
213
+ console.error(
214
+ JSON.stringify({
215
+ error: error instanceof Error ? error.message : 'Unknown error',
216
+ })
217
+ )
218
+ } else {
219
+ console.error(
220
+ 'Error:',
221
+ error instanceof Error ? error.message : 'Unknown error'
222
+ )
223
+ }
224
+ process.exit(1)
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Command: skill front teammates
230
+ * List all teammates in the workspace
231
+ */
232
+ async function listTeammates(options: { json?: boolean }): Promise<void> {
233
+ try {
234
+ const front = getFrontSdkClient()
235
+ const result = await front.teammates.list()
236
+
237
+ if (options.json) {
238
+ console.log(JSON.stringify(result._results, null, 2))
239
+ return
240
+ }
241
+
242
+ console.log('\n👥 Teammates:')
243
+ console.log('-'.repeat(60))
244
+
245
+ for (const teammate of result._results) {
246
+ const available = teammate.is_available ? '✓' : '✗'
247
+ console.log(` ${available} ${teammate.id}`)
248
+ console.log(` Email: ${teammate.email}`)
249
+ if (teammate.first_name || teammate.last_name) {
250
+ console.log(
251
+ ` Name: ${teammate.first_name || ''} ${teammate.last_name || ''}`.trim()
252
+ )
253
+ }
254
+ if (teammate.username) {
255
+ console.log(` Username: ${teammate.username}`)
256
+ }
257
+ console.log('')
258
+ }
259
+ } catch (error) {
260
+ console.error(
261
+ 'Error:',
262
+ error instanceof Error ? error.message : 'Unknown error'
263
+ )
264
+ process.exit(1)
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Command: skill front teammate <id>
270
+ * Get a specific teammate by ID
271
+ */
272
+ async function getTeammate(
273
+ id: string,
274
+ options: { json?: boolean }
275
+ ): Promise<void> {
276
+ try {
277
+ const front = getFrontSdkClient()
278
+ const teammate = await front.teammates.get(id)
279
+
280
+ if (options.json) {
281
+ console.log(JSON.stringify(teammate, null, 2))
282
+ return
283
+ }
284
+
285
+ console.log('\n👤 Teammate Details:')
286
+ console.log(` ID: ${teammate.id}`)
287
+ console.log(` Email: ${teammate.email}`)
288
+ if (teammate.first_name || teammate.last_name) {
289
+ console.log(
290
+ ` Name: ${teammate.first_name || ''} ${teammate.last_name || ''}`.trim()
291
+ )
292
+ }
293
+ if (teammate.username) {
294
+ console.log(` Username: ${teammate.username}`)
295
+ }
296
+ console.log(` Available: ${teammate.is_available ? 'Yes' : 'No'}`)
297
+ console.log('')
298
+ } catch (error) {
299
+ console.error(
300
+ 'Error:',
301
+ error instanceof Error ? error.message : 'Unknown error'
302
+ )
303
+ process.exit(1)
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Register Front commands with Commander
309
+ */
310
+ export function registerFrontCommands(program: Command): void {
311
+ const front = program
312
+ .command('front')
313
+ .description('Front API commands for debugging')
314
+
315
+ front
316
+ .command('message')
317
+ .description('Get message details from Front API')
318
+ .argument('<id>', 'Message ID (e.g., msg_xxx)')
319
+ .option('--json', 'Output as JSON')
320
+ .action(getMessage)
321
+
322
+ front
323
+ .command('conversation')
324
+ .description('Get conversation details from Front API')
325
+ .argument('<id>', 'Conversation ID (e.g., cnv_xxx)')
326
+ .option('--json', 'Output as JSON')
327
+ .option('-m, --messages', 'Include message history')
328
+ .action(getConversation)
329
+
330
+ front
331
+ .command('teammates')
332
+ .description('List all teammates in the workspace')
333
+ .option('--json', 'Output as JSON')
334
+ .action(listTeammates)
335
+
336
+ front
337
+ .command('teammate')
338
+ .description('Get teammate details by ID')
339
+ .argument('<id>', 'Teammate ID (e.g., tea_xxx or username)')
340
+ .option('--json', 'Output as JSON')
341
+ .action(getTeammate)
342
+
343
+ // Register pull command for building eval datasets
344
+ registerPullCommand(front)
345
+
346
+ // Register tag management commands
347
+ registerTagCommands(front)
348
+
349
+ // Register cache command for DuckDB sync
350
+ registerCacheCommand(front)
351
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Pull conversations from Front for eval dataset
3
+ *
4
+ * Usage:
5
+ * skill front pull --inbox <inbox_id> --limit 100 --output data/front-conversations.json
6
+ */
7
+
8
+ import { writeFileSync } from 'fs'
9
+ import { createInstrumentedFrontClient } from '@skillrecordings/core/front/instrumented-client'
10
+ import type { Command } from 'commander'
11
+
12
+ interface PullOptions {
13
+ inbox?: string
14
+ limit?: number
15
+ output?: string
16
+ filter?: string
17
+ json?: boolean
18
+ }
19
+
20
+ interface FrontConversation {
21
+ id: string
22
+ subject: string
23
+ status: string
24
+ created_at: number
25
+ last_message_at?: number
26
+ tags: Array<{ id: string; name: string }>
27
+ recipient?: { handle: string; name?: string }
28
+ assignee?: { email: string }
29
+ }
30
+
31
+ interface FrontMessage {
32
+ id: string
33
+ type: string
34
+ is_inbound: boolean
35
+ created_at: number
36
+ subject?: string
37
+ body?: string
38
+ text?: string
39
+ author?: { email?: string; name?: string }
40
+ }
41
+
42
+ interface EvalSample {
43
+ id: string
44
+ conversationId: string
45
+ subject: string
46
+ customerEmail: string
47
+ status: string
48
+ tags: string[]
49
+ triggerMessage: {
50
+ id: string
51
+ subject: string
52
+ body: string
53
+ timestamp: number
54
+ }
55
+ conversationHistory: Array<{
56
+ direction: 'in' | 'out'
57
+ body: string
58
+ timestamp: number
59
+ author?: string
60
+ }>
61
+ category: string // inferred from tags/content
62
+ }
63
+
64
+ export async function pullConversations(options: PullOptions): Promise<void> {
65
+ const { inbox, limit = 50, output, filter, json = false } = options
66
+
67
+ const frontToken = process.env.FRONT_API_TOKEN
68
+ if (!frontToken) {
69
+ console.error('Error: FRONT_API_TOKEN environment variable required')
70
+ process.exit(1)
71
+ }
72
+
73
+ const front = createInstrumentedFrontClient({ apiToken: frontToken })
74
+
75
+ try {
76
+ // If no inbox specified, list available inboxes
77
+ if (!inbox) {
78
+ console.log('Fetching available inboxes...\n')
79
+ const inboxesData = (await front.raw.get('/inboxes')) as {
80
+ _results: Array<{ id: string; name: string; address?: string }>
81
+ }
82
+
83
+ console.log('Available inboxes:')
84
+ for (const ib of inboxesData._results || []) {
85
+ console.log(` ${ib.id}: ${ib.name} (${ib.address || 'no address'})`)
86
+ }
87
+ console.log(
88
+ '\nUse --inbox <id> to pull conversations from a specific inbox'
89
+ )
90
+ return
91
+ }
92
+
93
+ console.log(`Pulling conversations from inbox ${inbox}...`)
94
+
95
+ // Get conversations from inbox
96
+ let allConversations: FrontConversation[] = []
97
+ let nextUrl: string | null = `/inboxes/${inbox}/conversations?limit=50`
98
+
99
+ while (nextUrl && allConversations.length < limit) {
100
+ const data = (await front.raw.get(nextUrl)) as {
101
+ _results: FrontConversation[]
102
+ _pagination?: { next?: string }
103
+ }
104
+
105
+ allConversations = allConversations.concat(data._results || [])
106
+ nextUrl = data._pagination?.next || null
107
+
108
+ process.stdout.write(
109
+ `\r Fetched ${allConversations.length} conversations...`
110
+ )
111
+ }
112
+
113
+ allConversations = allConversations.slice(0, limit)
114
+ console.log(`\n Total: ${allConversations.length} conversations`)
115
+
116
+ // Filter if specified
117
+ if (filter) {
118
+ const filterLower = filter.toLowerCase()
119
+ allConversations = allConversations.filter((c) => {
120
+ const subject = (c.subject || '').toLowerCase()
121
+ const tags = c.tags.map((t) => t.name.toLowerCase()).join(' ')
122
+ return subject.includes(filterLower) || tags.includes(filterLower)
123
+ })
124
+ console.log(
125
+ ` After filter "${filter}": ${allConversations.length} conversations`
126
+ )
127
+ }
128
+
129
+ // Build eval samples
130
+ console.log('\nFetching message details...')
131
+ const samples: EvalSample[] = []
132
+ let processed = 0
133
+
134
+ for (const conv of allConversations) {
135
+ processed++
136
+ process.stdout.write(
137
+ `\r Processing ${processed}/${allConversations.length}...`
138
+ )
139
+
140
+ try {
141
+ // Get messages for this conversation
142
+ const messagesData = (await front.raw.get(
143
+ `/conversations/${conv.id}/messages`
144
+ )) as { _results: FrontMessage[] }
145
+ const messages = messagesData._results || []
146
+
147
+ // Find the most recent inbound message as trigger
148
+ const inboundMessages = messages
149
+ .filter((m) => m.is_inbound)
150
+ .sort((a, b) => b.created_at - a.created_at)
151
+
152
+ const triggerMessage = inboundMessages[0]
153
+ if (!triggerMessage) continue // Skip if no inbound messages
154
+
155
+ // Extract body text
156
+ const bodyText =
157
+ triggerMessage.text ||
158
+ triggerMessage.body
159
+ ?.replace(/<[^>]*>/g, ' ')
160
+ .replace(/\s+/g, ' ')
161
+ .trim() ||
162
+ ''
163
+
164
+ // Skip very short messages
165
+ if (bodyText.length < 20) continue
166
+
167
+ // Build conversation history
168
+ const history = messages
169
+ .sort((a, b) => a.created_at - b.created_at)
170
+ .map((m) => ({
171
+ direction: (m.is_inbound ? 'in' : 'out') as 'in' | 'out',
172
+ body:
173
+ m.text ||
174
+ m.body
175
+ ?.replace(/<[^>]*>/g, ' ')
176
+ .replace(/\s+/g, ' ')
177
+ .trim() ||
178
+ '',
179
+ timestamp: m.created_at,
180
+ author: m.author?.email,
181
+ }))
182
+
183
+ // Infer category from tags/subject
184
+ const tagNames = conv.tags.map((t) => t.name.toLowerCase()).join(' ')
185
+ const subject = (conv.subject || '').toLowerCase()
186
+ let category = 'general'
187
+
188
+ if (tagNames.includes('refund') || subject.includes('refund'))
189
+ category = 'refund'
190
+ else if (
191
+ tagNames.includes('access') ||
192
+ subject.includes('login') ||
193
+ subject.includes('access')
194
+ )
195
+ category = 'access'
196
+ else if (
197
+ tagNames.includes('technical') ||
198
+ subject.includes('error') ||
199
+ subject.includes('bug')
200
+ )
201
+ category = 'technical'
202
+ else if (subject.includes('feedback') || subject.includes('suggestion'))
203
+ category = 'feedback'
204
+ else if (
205
+ subject.includes('partnership') ||
206
+ subject.includes('collaborate')
207
+ )
208
+ category = 'business'
209
+
210
+ samples.push({
211
+ id: conv.id,
212
+ conversationId: conv.id,
213
+ subject: conv.subject || '(no subject)',
214
+ customerEmail: conv.recipient?.handle || 'unknown',
215
+ status: conv.status,
216
+ tags: conv.tags.map((t) => t.name),
217
+ triggerMessage: {
218
+ id: triggerMessage.id,
219
+ subject: triggerMessage.subject || conv.subject || '',
220
+ body: bodyText,
221
+ timestamp: triggerMessage.created_at,
222
+ },
223
+ conversationHistory: history,
224
+ category,
225
+ })
226
+
227
+ // Rate limit
228
+ await new Promise((r) => setTimeout(r, 100))
229
+ } catch (err) {
230
+ // Skip failed conversations
231
+ continue
232
+ }
233
+ }
234
+
235
+ console.log(`\n\nBuilt ${samples.length} eval samples`)
236
+
237
+ // Category breakdown
238
+ const byCategory: Record<string, number> = {}
239
+ for (const s of samples) {
240
+ byCategory[s.category] = (byCategory[s.category] || 0) + 1
241
+ }
242
+ console.log('\nBy category:')
243
+ for (const [cat, count] of Object.entries(byCategory).sort(
244
+ (a, b) => b[1] - a[1]
245
+ )) {
246
+ console.log(` ${cat}: ${count}`)
247
+ }
248
+
249
+ // Output
250
+ if (output) {
251
+ writeFileSync(output, JSON.stringify(samples, null, 2))
252
+ console.log(`\nSaved to ${output}`)
253
+ } else if (json) {
254
+ console.log(JSON.stringify(samples, null, 2))
255
+ }
256
+ } catch (error) {
257
+ console.error(
258
+ '\nError:',
259
+ error instanceof Error ? error.message : 'Unknown error'
260
+ )
261
+ process.exit(1)
262
+ }
263
+ }
264
+
265
+ export function registerPullCommand(parent: Command): void {
266
+ parent
267
+ .command('pull')
268
+ .description('Pull conversations from Front for eval dataset')
269
+ .option('-i, --inbox <id>', 'Inbox ID to pull from')
270
+ .option('-l, --limit <n>', 'Max conversations to pull', parseInt)
271
+ .option('-o, --output <file>', 'Output file path')
272
+ .option('-f, --filter <term>', 'Filter by subject/tag containing term')
273
+ .option('--json', 'JSON output')
274
+ .action(pullConversations)
275
+ }