@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,426 @@
1
+ /**
2
+ * FAQ Review CLI Command
3
+ *
4
+ * Interactive CLI for human curation of FAQ candidates.
5
+ * Approve, edit, reject, or skip candidates before publishing to KB.
6
+ *
7
+ * Usage:
8
+ * skill faq-review --app total-typescript
9
+ * skill faq-review --app epic-react --stats
10
+ */
11
+
12
+ import { spawnSync } from 'node:child_process'
13
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
14
+ import { tmpdir } from 'node:os'
15
+ import { join } from 'node:path'
16
+ import { confirm, select } from '@inquirer/prompts'
17
+ import {
18
+ type StoredFaqCandidate,
19
+ approveCandidate,
20
+ getPendingCandidates,
21
+ getQueueStats,
22
+ rejectCandidate,
23
+ } from '@skillrecordings/core/faq/review'
24
+ import type { Command } from 'commander'
25
+
26
+ /**
27
+ * Color codes for terminal output
28
+ */
29
+ const COLORS = {
30
+ reset: '\x1b[0m',
31
+ green: '\x1b[32m',
32
+ yellow: '\x1b[33m',
33
+ red: '\x1b[31m',
34
+ blue: '\x1b[34m',
35
+ cyan: '\x1b[36m',
36
+ dim: '\x1b[2m',
37
+ bold: '\x1b[1m',
38
+ } as const
39
+
40
+ /**
41
+ * Word wrap text to specified width
42
+ */
43
+ function wordWrap(text: string, width: number): string {
44
+ const words = text.split(' ')
45
+ const lines: string[] = []
46
+ let currentLine = ''
47
+
48
+ for (const word of words) {
49
+ if (currentLine.length + word.length + 1 <= width) {
50
+ currentLine += (currentLine ? ' ' : '') + word
51
+ } else {
52
+ if (currentLine) lines.push(currentLine)
53
+ currentLine = word
54
+ }
55
+ }
56
+
57
+ if (currentLine) lines.push(currentLine)
58
+ return lines.join('\n')
59
+ }
60
+
61
+ /**
62
+ * Display a single FAQ candidate
63
+ */
64
+ function displayCandidate(
65
+ candidate: StoredFaqCandidate,
66
+ index: number,
67
+ total: number
68
+ ): void {
69
+ console.log('\n' + '═'.repeat(70))
70
+ console.log(
71
+ `${COLORS.bold}FAQ Candidate ${index + 1} of ${total}${COLORS.reset}`
72
+ )
73
+ console.log('═'.repeat(70))
74
+
75
+ // Metadata row
76
+ const confPct = (candidate.confidence * 100).toFixed(0)
77
+ const unchangedPct = (candidate.unchangedRate * 100).toFixed(0)
78
+ console.log(
79
+ `${COLORS.dim}Confidence: ${confPct}% | Cluster: ${candidate.clusterSize} convos | Unchanged: ${unchangedPct}%${COLORS.reset}`
80
+ )
81
+
82
+ if (candidate.suggestedCategory) {
83
+ console.log(
84
+ `${COLORS.dim}Category: ${candidate.suggestedCategory}${COLORS.reset}`
85
+ )
86
+ }
87
+
88
+ if (candidate.tags.length > 0) {
89
+ console.log(
90
+ `${COLORS.dim}Tags: ${candidate.tags.slice(0, 5).join(', ')}${COLORS.reset}`
91
+ )
92
+ }
93
+
94
+ // Question
95
+ console.log(`\n${COLORS.bold}${COLORS.cyan}Question:${COLORS.reset}`)
96
+ console.log(wordWrap(candidate.question, 68))
97
+
98
+ // Answer
99
+ console.log(`\n${COLORS.bold}${COLORS.green}Answer:${COLORS.reset}`)
100
+ console.log(wordWrap(candidate.answer, 68))
101
+
102
+ console.log('\n' + '-'.repeat(70))
103
+ }
104
+
105
+ /**
106
+ * Get editor command
107
+ */
108
+ function getEditor(): string {
109
+ return process.env.EDITOR || process.env.VISUAL || 'nano'
110
+ }
111
+
112
+ /**
113
+ * Open content in editor and return edited content
114
+ */
115
+ function editInEditor(
116
+ question: string,
117
+ answer: string
118
+ ): { question: string; answer: string } | null {
119
+ const editor = getEditor()
120
+ const tmpFile = join(tmpdir(), `faq-edit-${Date.now()}.md`)
121
+
122
+ // Write content to temp file
123
+ const content = `# FAQ Edit
124
+
125
+ ## Question
126
+ ${question}
127
+
128
+ ## Answer
129
+ ${answer}
130
+
131
+ <!--
132
+ Edit the question and answer above.
133
+ Save and close the editor when done.
134
+ The sections are separated by "## Question" and "## Answer" headers.
135
+ -->
136
+ `
137
+
138
+ writeFileSync(tmpFile, content, 'utf-8')
139
+
140
+ try {
141
+ // Open editor (blocking)
142
+ const result = spawnSync(editor, [tmpFile], {
143
+ stdio: 'inherit',
144
+ shell: true,
145
+ })
146
+
147
+ if (result.status !== 0) {
148
+ console.log(`${COLORS.red}Editor exited with error${COLORS.reset}`)
149
+ return null
150
+ }
151
+
152
+ // Read edited content
153
+ const edited = readFileSync(tmpFile, 'utf-8')
154
+
155
+ // Parse sections
156
+ const questionMatch = edited.match(
157
+ /## Question\s*\n([\s\S]*?)(?=\n## Answer|$)/
158
+ )
159
+ const answerMatch = edited.match(/## Answer\s*\n([\s\S]*?)(?=\n<!--|$)/)
160
+
161
+ const editedQuestion = questionMatch?.[1]?.trim()
162
+ const editedAnswer = answerMatch?.[1]?.trim()
163
+
164
+ if (!editedQuestion || !editedAnswer) {
165
+ console.log(
166
+ `${COLORS.red}Could not parse edited content. Please keep the ## headers.${COLORS.reset}`
167
+ )
168
+ return null
169
+ }
170
+
171
+ return {
172
+ question: editedQuestion,
173
+ answer: editedAnswer,
174
+ }
175
+ } finally {
176
+ // Clean up temp file
177
+ if (existsSync(tmpFile)) {
178
+ unlinkSync(tmpFile)
179
+ }
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Run interactive review session
185
+ */
186
+ async function runReviewSession(options: {
187
+ app: string
188
+ }): Promise<void> {
189
+ console.log(`\n${COLORS.bold}📋 FAQ Review Session${COLORS.reset}`)
190
+ console.log(`App: ${options.app}`)
191
+ console.log('Loading candidates...\n')
192
+
193
+ // Load pending candidates
194
+ const candidates = await getPendingCandidates(options.app, 100)
195
+
196
+ if (candidates.length === 0) {
197
+ console.log(
198
+ `${COLORS.yellow}No pending FAQ candidates found.${COLORS.reset}`
199
+ )
200
+ console.log(
201
+ `Run ${COLORS.cyan}skill faq-mine --app ${options.app} --since 30d${COLORS.reset} to generate candidates.`
202
+ )
203
+ return
204
+ }
205
+
206
+ console.log(
207
+ `Found ${COLORS.bold}${candidates.length}${COLORS.reset} pending candidates.`
208
+ )
209
+
210
+ // Session stats
211
+ let approved = 0
212
+ let rejected = 0
213
+ let skipped = 0
214
+ let edited = 0
215
+
216
+ // Review loop
217
+ for (let i = 0; i < candidates.length; i++) {
218
+ const candidate = candidates[i]!
219
+
220
+ displayCandidate(candidate, i, candidates.length)
221
+
222
+ // Get action
223
+ const action = await select({
224
+ message: 'Action:',
225
+ choices: [
226
+ {
227
+ name: `${COLORS.green}[A]pprove${COLORS.reset} - Publish to KB`,
228
+ value: 'approve',
229
+ },
230
+ {
231
+ name: `${COLORS.blue}[E]dit${COLORS.reset} - Edit then publish`,
232
+ value: 'edit',
233
+ },
234
+ {
235
+ name: `${COLORS.red}[R]eject${COLORS.reset} - Won't resurface`,
236
+ value: 'reject',
237
+ },
238
+ {
239
+ name: `${COLORS.dim}[S]kip${COLORS.reset} - Review later`,
240
+ value: 'skip',
241
+ },
242
+ {
243
+ name: `${COLORS.dim}[Q]uit${COLORS.reset} - End session`,
244
+ value: 'quit',
245
+ },
246
+ ],
247
+ })
248
+
249
+ if (action === 'quit') {
250
+ break
251
+ }
252
+
253
+ if (action === 'skip') {
254
+ skipped++
255
+ console.log(`${COLORS.dim}⏭ Skipped${COLORS.reset}`)
256
+ continue
257
+ }
258
+
259
+ if (action === 'reject') {
260
+ const result = await rejectCandidate(
261
+ candidate.id,
262
+ 'Rejected via CLI review'
263
+ )
264
+ if (result.success) {
265
+ rejected++
266
+ console.log(`${COLORS.red}✗ Rejected - won't resurface${COLORS.reset}`)
267
+ } else {
268
+ console.log(
269
+ `${COLORS.red}✗ Failed to reject: ${result.error}${COLORS.reset}`
270
+ )
271
+ }
272
+ continue
273
+ }
274
+
275
+ // approve or edit
276
+ let finalQuestion = candidate.question
277
+ let finalAnswer = candidate.answer
278
+ let wasEdited = false
279
+
280
+ if (action === 'edit') {
281
+ console.log(`\nOpening ${getEditor()}...`)
282
+ const editResult = editInEditor(candidate.question, candidate.answer)
283
+
284
+ if (!editResult) {
285
+ console.log('Edit cancelled. Skipping candidate.')
286
+ skipped++
287
+ continue
288
+ }
289
+
290
+ finalQuestion = editResult.question
291
+ finalAnswer = editResult.answer
292
+ wasEdited =
293
+ finalQuestion !== candidate.question || finalAnswer !== candidate.answer
294
+
295
+ if (wasEdited) {
296
+ console.log(`\n${COLORS.yellow}Content was edited.${COLORS.reset}`)
297
+ console.log(`\n${COLORS.bold}New Question:${COLORS.reset}`)
298
+ console.log(wordWrap(finalQuestion, 68))
299
+ console.log(`\n${COLORS.bold}New Answer:${COLORS.reset}`)
300
+ console.log(wordWrap(finalAnswer, 68))
301
+
302
+ const confirmPublish = await confirm({
303
+ message: 'Publish edited FAQ?',
304
+ default: true,
305
+ })
306
+
307
+ if (!confirmPublish) {
308
+ skipped++
309
+ continue
310
+ }
311
+ }
312
+ }
313
+
314
+ // Publish (approve or edit)
315
+ const result = await approveCandidate(candidate.id, {
316
+ question: wasEdited ? finalQuestion : undefined,
317
+ answer: wasEdited ? finalAnswer : undefined,
318
+ editNotes: wasEdited ? 'Edited during CLI review' : undefined,
319
+ })
320
+
321
+ if (result.success) {
322
+ approved++
323
+ if (wasEdited) edited++
324
+ console.log(
325
+ `${COLORS.green}✓ Published as ${result.articleId}${COLORS.reset}`
326
+ )
327
+ } else {
328
+ console.log(
329
+ `${COLORS.red}✗ Failed to publish: ${result.error}${COLORS.reset}`
330
+ )
331
+ }
332
+ }
333
+
334
+ // Session summary
335
+ console.log('\n' + '═'.repeat(70))
336
+ console.log(`${COLORS.bold}📊 Session Summary${COLORS.reset}`)
337
+ console.log('═'.repeat(70))
338
+ console.log(
339
+ `${COLORS.green}Approved: ${approved}${edited > 0 ? ` (${edited} edited)` : ''}${COLORS.reset}`
340
+ )
341
+ console.log(`${COLORS.red}Rejected: ${rejected}${COLORS.reset}`)
342
+ console.log(`${COLORS.dim}Skipped: ${skipped}${COLORS.reset}`)
343
+ console.log('')
344
+ }
345
+
346
+ /**
347
+ * Display review statistics
348
+ */
349
+ async function showStats(appId: string, json: boolean): Promise<void> {
350
+ const stats = await getQueueStats(appId)
351
+
352
+ if (json) {
353
+ console.log(JSON.stringify(stats, null, 2))
354
+ return
355
+ }
356
+
357
+ console.log(
358
+ `\n${COLORS.bold}📊 Review Queue Statistics for ${appId}${COLORS.reset}`
359
+ )
360
+ console.log('─'.repeat(40))
361
+ console.log(`Pending: ${stats.pending}`)
362
+ console.log(
363
+ `${COLORS.green}Approved: ${stats.approved}${COLORS.reset}`
364
+ )
365
+ console.log(`${COLORS.red}Rejected: ${stats.rejected}${COLORS.reset}`)
366
+ console.log(`Total: ${stats.total}`)
367
+
368
+ if (stats.total > 0 && stats.approved + stats.rejected > 0) {
369
+ const approvalRate = (
370
+ (stats.approved / (stats.approved + stats.rejected)) *
371
+ 100
372
+ ).toFixed(1)
373
+ console.log(`\nApproval rate: ${approvalRate}%`)
374
+ }
375
+
376
+ console.log('')
377
+ }
378
+
379
+ /**
380
+ * Main command handler
381
+ */
382
+ async function faqReview(options: {
383
+ app: string
384
+ stats?: boolean
385
+ json?: boolean
386
+ }): Promise<void> {
387
+ if (!options.app) {
388
+ console.error('Error: --app is required')
389
+ process.exit(1)
390
+ }
391
+
392
+ try {
393
+ if (options.stats) {
394
+ await showStats(options.app, options.json ?? false)
395
+ } else {
396
+ await runReviewSession({
397
+ app: options.app,
398
+ })
399
+ }
400
+ } catch (error) {
401
+ if ((error as any)?.name === 'ExitPromptError') {
402
+ // User pressed Ctrl+C
403
+ console.log('\n\nReview session cancelled.')
404
+ process.exit(0)
405
+ }
406
+
407
+ console.error(
408
+ 'Error:',
409
+ error instanceof Error ? error.message : String(error)
410
+ )
411
+ process.exit(1)
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Register FAQ review commands with Commander
417
+ */
418
+ export function registerFaqReviewCommands(program: Command): void {
419
+ program
420
+ .command('faq-review')
421
+ .description('Interactive review of FAQ candidates')
422
+ .requiredOption('-a, --app <slug>', 'App slug to review (required)')
423
+ .option('--stats', 'Show review statistics instead of interactive review')
424
+ .option('--json', 'Output stats as JSON (use with --stats)')
425
+ .action(faqReview)
426
+ }