@skillrecordings/cli 0.1.0 → 0.2.1

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 (136) hide show
  1. package/bin/skill.mjs +21 -0
  2. package/dist/chunk-2NCCVTEE.js +22342 -0
  3. package/dist/chunk-2NCCVTEE.js.map +1 -0
  4. package/dist/chunk-3E3GYSZR.js +7071 -0
  5. package/dist/chunk-3E3GYSZR.js.map +1 -0
  6. package/dist/chunk-F4EM72IH.js +86 -0
  7. package/dist/chunk-F4EM72IH.js.map +1 -0
  8. package/dist/chunk-FGP7KUQW.js +432 -0
  9. package/dist/chunk-FGP7KUQW.js.map +1 -0
  10. package/dist/chunk-H3D6VCME.js +55 -0
  11. package/dist/chunk-H3D6VCME.js.map +1 -0
  12. package/dist/chunk-HK3PEWFD.js +208 -0
  13. package/dist/chunk-HK3PEWFD.js.map +1 -0
  14. package/dist/chunk-KEV3QKXP.js +4495 -0
  15. package/dist/chunk-KEV3QKXP.js.map +1 -0
  16. package/dist/chunk-MG37YDAK.js +882 -0
  17. package/dist/chunk-MG37YDAK.js.map +1 -0
  18. package/dist/chunk-MLNDSBZ4.js +482 -0
  19. package/dist/chunk-MLNDSBZ4.js.map +1 -0
  20. package/dist/chunk-N2WIV2JV.js +22 -0
  21. package/dist/chunk-N2WIV2JV.js.map +1 -0
  22. package/dist/chunk-PWWRCN5W.js +2067 -0
  23. package/dist/chunk-PWWRCN5W.js.map +1 -0
  24. package/dist/chunk-SKHBM3XP.js +7746 -0
  25. package/dist/chunk-SKHBM3XP.js.map +1 -0
  26. package/dist/chunk-WFANXVQG.js +64 -0
  27. package/dist/chunk-WFANXVQG.js.map +1 -0
  28. package/dist/chunk-WYKL32C3.js +275 -0
  29. package/dist/chunk-WYKL32C3.js.map +1 -0
  30. package/dist/chunk-ZNF7XD2S.js +134 -0
  31. package/dist/chunk-ZNF7XD2S.js.map +1 -0
  32. package/dist/config-AUAIYDSI.js +20 -0
  33. package/dist/config-AUAIYDSI.js.map +1 -0
  34. package/dist/fileFromPath-XN7LXIBI.js +134 -0
  35. package/dist/fileFromPath-XN7LXIBI.js.map +1 -0
  36. package/dist/getMachineId-bsd-KW2E7VK3.js +42 -0
  37. package/dist/getMachineId-bsd-KW2E7VK3.js.map +1 -0
  38. package/dist/getMachineId-darwin-ROXJUJX5.js +42 -0
  39. package/dist/getMachineId-darwin-ROXJUJX5.js.map +1 -0
  40. package/dist/getMachineId-linux-KVZEHQSU.js +34 -0
  41. package/dist/getMachineId-linux-KVZEHQSU.js.map +1 -0
  42. package/dist/getMachineId-unsupported-PPRILPPA.js +25 -0
  43. package/dist/getMachineId-unsupported-PPRILPPA.js.map +1 -0
  44. package/dist/getMachineId-win-IIF36LEJ.js +44 -0
  45. package/dist/getMachineId-win-IIF36LEJ.js.map +1 -0
  46. package/dist/index.js +112703 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/lib-R6DEEJCP.js +7623 -0
  49. package/dist/lib-R6DEEJCP.js.map +1 -0
  50. package/dist/pipeline-IAVVAKTU.js +120 -0
  51. package/dist/pipeline-IAVVAKTU.js.map +1 -0
  52. package/dist/query-NTP5NVXN.js +25 -0
  53. package/dist/query-NTP5NVXN.js.map +1 -0
  54. package/dist/routing-BAEPFB7V.js +390 -0
  55. package/dist/routing-BAEPFB7V.js.map +1 -0
  56. package/dist/stripe-lookup-charge-EPRUMZDL.js +56 -0
  57. package/dist/stripe-lookup-charge-EPRUMZDL.js.map +1 -0
  58. package/dist/stripe-payment-history-SJPKA63N.js +67 -0
  59. package/dist/stripe-payment-history-SJPKA63N.js.map +1 -0
  60. package/dist/stripe-subscription-status-L4Z65GB3.js +58 -0
  61. package/dist/stripe-subscription-status-L4Z65GB3.js.map +1 -0
  62. package/dist/stripe-verify-refund-FZDKCIUQ.js +54 -0
  63. package/dist/stripe-verify-refund-FZDKCIUQ.js.map +1 -0
  64. package/dist/support-memory-WSG7SDKG.js +10 -0
  65. package/dist/support-memory-WSG7SDKG.js.map +1 -0
  66. package/package.json +10 -7
  67. package/.env.encrypted +0 -0
  68. package/CHANGELOG.md +0 -35
  69. package/data/tt-archive-dataset.json +0 -1
  70. package/data/validate-test-dataset.json +0 -97
  71. package/docs/CLI-AUTH.md +0 -504
  72. package/preload.ts +0 -18
  73. package/src/__tests__/init.test.ts +0 -74
  74. package/src/alignment-test.ts +0 -64
  75. package/src/check-apps.ts +0 -16
  76. package/src/commands/auth/decrypt.ts +0 -123
  77. package/src/commands/auth/encrypt.ts +0 -81
  78. package/src/commands/auth/index.ts +0 -50
  79. package/src/commands/auth/keygen.ts +0 -41
  80. package/src/commands/auth/status.ts +0 -164
  81. package/src/commands/axiom/forensic.ts +0 -868
  82. package/src/commands/axiom/index.ts +0 -697
  83. package/src/commands/build-dataset.ts +0 -311
  84. package/src/commands/db-status.ts +0 -47
  85. package/src/commands/deploys.ts +0 -219
  86. package/src/commands/eval-local/compare.ts +0 -171
  87. package/src/commands/eval-local/health.ts +0 -212
  88. package/src/commands/eval-local/index.ts +0 -76
  89. package/src/commands/eval-local/real-tools.ts +0 -416
  90. package/src/commands/eval-local/run.ts +0 -1168
  91. package/src/commands/eval-local/score-production.ts +0 -256
  92. package/src/commands/eval-local/seed.ts +0 -276
  93. package/src/commands/eval-pipeline/index.ts +0 -53
  94. package/src/commands/eval-pipeline/real-tools.ts +0 -492
  95. package/src/commands/eval-pipeline/run.ts +0 -1316
  96. package/src/commands/eval-pipeline/seed.ts +0 -395
  97. package/src/commands/eval-prompt.ts +0 -496
  98. package/src/commands/eval.test.ts +0 -253
  99. package/src/commands/eval.ts +0 -108
  100. package/src/commands/faq-classify.ts +0 -460
  101. package/src/commands/faq-cluster.ts +0 -135
  102. package/src/commands/faq-extract.ts +0 -249
  103. package/src/commands/faq-mine.ts +0 -432
  104. package/src/commands/faq-review.ts +0 -426
  105. package/src/commands/front/index.ts +0 -351
  106. package/src/commands/front/pull-conversations.ts +0 -275
  107. package/src/commands/front/tags.ts +0 -825
  108. package/src/commands/front-cache.ts +0 -1277
  109. package/src/commands/front-stats.ts +0 -75
  110. package/src/commands/health.test.ts +0 -82
  111. package/src/commands/health.ts +0 -362
  112. package/src/commands/init.test.ts +0 -89
  113. package/src/commands/init.ts +0 -106
  114. package/src/commands/inngest/client.ts +0 -294
  115. package/src/commands/inngest/events.ts +0 -296
  116. package/src/commands/inngest/investigate.ts +0 -382
  117. package/src/commands/inngest/runs.ts +0 -149
  118. package/src/commands/inngest/signal.ts +0 -143
  119. package/src/commands/kb-sync.ts +0 -498
  120. package/src/commands/memory/find.ts +0 -135
  121. package/src/commands/memory/get.ts +0 -87
  122. package/src/commands/memory/index.ts +0 -97
  123. package/src/commands/memory/stats.ts +0 -163
  124. package/src/commands/memory/store.ts +0 -49
  125. package/src/commands/memory/vote.ts +0 -159
  126. package/src/commands/pipeline.ts +0 -127
  127. package/src/commands/responses.ts +0 -856
  128. package/src/commands/tools.ts +0 -293
  129. package/src/commands/wizard.ts +0 -319
  130. package/src/index.ts +0 -172
  131. package/src/lib/crypto.ts +0 -56
  132. package/src/lib/env-loader.ts +0 -206
  133. package/src/lib/onepassword.ts +0 -137
  134. package/src/test-agent-local.ts +0 -115
  135. package/tsconfig.json +0 -11
  136. package/vitest.config.ts +0 -10
@@ -1,249 +0,0 @@
1
- /**
2
- * FAQ Extraction CLI Command
3
- *
4
- * Extracts FAQ candidates from clustered conversations.
5
- * Part of Phase 1.3 of the FAQ Mining pipeline.
6
- *
7
- * Usage:
8
- * bun src/index.ts faq-extract
9
- * bun src/index.ts faq-extract --app total-typescript --push-redis
10
- * bun src/index.ts faq-extract --dry-run
11
- * bun src/index.ts faq-extract --version v2
12
- */
13
-
14
- import { existsSync } from 'fs'
15
- import { join, resolve } from 'path'
16
- import { createDuckDBSource } from '@skillrecordings/core/faq/duckdb-source'
17
- import {
18
- type ExtractionOptions,
19
- extractFaqCandidates,
20
- } from '@skillrecordings/core/faq/extractor'
21
- import { closeDb } from '@skillrecordings/database'
22
- import type { Command } from 'commander'
23
-
24
- /** Project root */
25
- const PROJECT_ROOT = resolve(__dirname, '../../../..')
26
-
27
- /** Default paths */
28
- const DEFAULT_CLUSTERING_PATH = join(
29
- PROJECT_ROOT,
30
- 'artifacts/phase-1/clustering/v1/clustering-result.json'
31
- )
32
- const DEFAULT_GOLDEN_PATH = join(
33
- PROJECT_ROOT,
34
- 'artifacts/phase-0/golden/latest/responses.json'
35
- )
36
- const DEFAULT_OUTPUT_PATH = join(PROJECT_ROOT, 'artifacts/phase-1/extraction')
37
- const DEFAULT_CACHE_PATH = `${process.env.HOME}/skill/data/front-cache.db`
38
-
39
- /**
40
- * Validate required files exist
41
- */
42
- function validatePaths(clusteringPath: string, goldenPath?: string): void {
43
- if (!existsSync(clusteringPath)) {
44
- throw new Error(
45
- `Clustering result not found at ${clusteringPath}\n` +
46
- 'Run `bun src/index.ts faq-cluster` first to generate clustering.'
47
- )
48
- }
49
-
50
- if (goldenPath && !existsSync(goldenPath)) {
51
- console.warn(`⚠️ Golden responses not found at ${goldenPath}`)
52
- console.warn(' Golden matching will be disabled.')
53
- }
54
- }
55
-
56
- /**
57
- * Main command handler
58
- */
59
- async function faqExtract(options: {
60
- clusteringPath?: string
61
- goldenPath?: string
62
- outputPath?: string
63
- cachePath?: string
64
- outputVersion?: string
65
- minClusterSize?: number
66
- topN?: number
67
- dedupThreshold?: number
68
- pushRedis?: boolean
69
- app?: string
70
- dryRun?: boolean
71
- json?: boolean
72
- filters?: boolean
73
- }): Promise<void> {
74
- const clusteringPath = options.clusteringPath ?? DEFAULT_CLUSTERING_PATH
75
- const goldenPath = options.goldenPath ?? DEFAULT_GOLDEN_PATH
76
- const outputPath = options.outputPath ?? DEFAULT_OUTPUT_PATH
77
- const cachePath = options.cachePath ?? DEFAULT_CACHE_PATH
78
- const version = options.outputVersion ?? 'v1'
79
-
80
- const applyFilters = options.filters ?? true
81
-
82
- console.log('🔬 FAQ Extraction Pipeline')
83
- console.log('='.repeat(60))
84
- console.log(` Clustering: ${clusteringPath}`)
85
- console.log(` Golden: ${goldenPath}`)
86
- console.log(` Output: ${outputPath}`)
87
- console.log(` DuckDB cache: ${cachePath}`)
88
- console.log(` Version: ${version}`)
89
- console.log(` Apply filters: ${applyFilters}`)
90
- console.log(` Push to Redis: ${options.pushRedis ?? false}`)
91
- console.log(` Dry run: ${options.dryRun ?? false}`)
92
- console.log('')
93
-
94
- // Validate paths
95
- validatePaths(clusteringPath, goldenPath)
96
-
97
- // Check DuckDB cache
98
- if (!existsSync(cachePath)) {
99
- throw new Error(
100
- `DuckDB cache not found at ${cachePath}\n` +
101
- 'Run `bun src/index.ts front-cache sync` first to populate cache.'
102
- )
103
- }
104
-
105
- let source
106
-
107
- try {
108
- // Create DuckDB source
109
- console.log('📦 Connecting to DuckDB cache...')
110
- source = await createDuckDBSource({ dbPath: cachePath })
111
-
112
- // Get source stats
113
- const stats = await source.getStats?.()
114
- if (stats) {
115
- console.log(
116
- ` ${stats.totalConversations.toLocaleString()} conversations in cache`
117
- )
118
- }
119
-
120
- // Run extraction
121
- const extractionOptions: ExtractionOptions = {
122
- clusteringPath,
123
- goldenPath: existsSync(goldenPath) ? goldenPath : undefined,
124
- source,
125
- outputPath,
126
- version,
127
- minClusterSize: options.minClusterSize ?? 3,
128
- topN: options.topN ?? 5,
129
- dedupThreshold: options.dedupThreshold ?? 0.85,
130
- pushToRedis: options.pushRedis ?? false,
131
- appId: options.app,
132
- dryRun: options.dryRun ?? false,
133
- applyFilters,
134
- }
135
-
136
- const result = await extractFaqCandidates(extractionOptions)
137
-
138
- // JSON output
139
- if (options.json) {
140
- console.log('\n📋 JSON Output:')
141
- console.log(JSON.stringify(result.stats, null, 2))
142
- }
143
-
144
- // Check acceptance criteria
145
- console.log('\n✅ Acceptance Criteria Check:')
146
- const highConfidence = result.stats.highConfidenceCount
147
- const target = 50
148
-
149
- if (highConfidence >= target) {
150
- console.log(
151
- ` ✅ ${highConfidence} candidates with confidence ≥ 0.7 (target: ${target})`
152
- )
153
- } else {
154
- console.log(
155
- ` ⚠️ Only ${highConfidence} candidates with confidence ≥ 0.7 (target: ${target})`
156
- )
157
- console.log(
158
- ' Consider lowering --min-cluster-size or --dedup-threshold'
159
- )
160
- }
161
-
162
- console.log(
163
- ` ✅ Golden match rate: ${(result.stats.goldenMatchRate * 100).toFixed(1)}%`
164
- )
165
- console.log(
166
- ` ✅ Deduplication working: ${result.stats.deduplicatedCount} removed`
167
- )
168
-
169
- if (!options.dryRun) {
170
- console.log(`\n✅ Extraction complete!`)
171
- console.log(` Artifacts written to: ${join(outputPath, version)}`)
172
-
173
- if (options.pushRedis && options.app) {
174
- console.log(
175
- ` Candidates pushed to Redis queue: faq:pending:${options.app}`
176
- )
177
- }
178
- }
179
- } catch (error) {
180
- console.error(
181
- '\n❌ Error:',
182
- error instanceof Error ? error.message : String(error)
183
- )
184
- process.exit(1)
185
- } finally {
186
- if (source?.close) {
187
- await source.close()
188
- }
189
- await closeDb()
190
- }
191
- }
192
-
193
- /**
194
- * Register FAQ extraction commands with Commander
195
- */
196
- export function registerFaqExtractCommands(program: Command): void {
197
- program
198
- .command('faq-extract')
199
- .description('Extract FAQ candidates from clustered conversations')
200
- .option(
201
- '--clustering-path <path>',
202
- 'Path to clustering result file',
203
- DEFAULT_CLUSTERING_PATH
204
- )
205
- .option(
206
- '--golden-path <path>',
207
- 'Path to golden responses file',
208
- DEFAULT_GOLDEN_PATH
209
- )
210
- .option(
211
- '--output-path <path>',
212
- 'Path to write extraction artifacts',
213
- DEFAULT_OUTPUT_PATH
214
- )
215
- .option(
216
- '--cache-path <path>',
217
- 'Path to DuckDB cache file',
218
- DEFAULT_CACHE_PATH
219
- )
220
- .option(
221
- '--output-version <version>',
222
- 'Version tag for output (e.g., v1, v2)',
223
- 'v1'
224
- )
225
- .option(
226
- '--min-cluster-size <n>',
227
- 'Minimum cluster size to process (default: 3)',
228
- (val: string) => parseInt(val, 10)
229
- )
230
- .option(
231
- '--top-n <n>',
232
- 'Number of representative conversations per cluster (default: 5)',
233
- (val: string) => parseInt(val, 10)
234
- )
235
- .option(
236
- '--dedup-threshold <n>',
237
- 'Similarity threshold for deduplication (default: 0.85)',
238
- (val: string) => parseFloat(val)
239
- )
240
- .option('--push-redis', 'Push candidates to Redis review queue')
241
- .option(
242
- '-a, --app <slug>',
243
- 'App ID for Redis queue (required with --push-redis)'
244
- )
245
- .option('-d, --dry-run', 'Show summary without writing artifacts')
246
- .option('--json', 'Output stats as JSON')
247
- .option('--no-filters', 'Disable preprocessing filters (for comparison)')
248
- .action(faqExtract)
249
- }
@@ -1,432 +0,0 @@
1
- /**
2
- * FAQ Mining CLI Command
3
- *
4
- * Mines resolved support conversations for FAQ candidates.
5
- * Uses semantic clustering to identify recurring questions.
6
- *
7
- * Usage:
8
- * skill faq-mine --app total-typescript --since 30d
9
- * skill faq-mine --app epic-react --since 7d --unchanged-only
10
- * skill faq-mine --app total-typescript --since 90d --json
11
- * skill faq-mine --app epic-web --since 30d --export faq-candidates.json
12
- */
13
-
14
- import { writeFileSync } from 'fs'
15
- import {
16
- type FaqCandidate,
17
- type MineResult,
18
- filterAutoSurfaceCandidates,
19
- mineConversations,
20
- mineFaqCandidates,
21
- } from '@skillrecordings/core/faq'
22
- import { createDuckDBSource } from '@skillrecordings/core/faq/duckdb-source'
23
- import type { DataSource } from '@skillrecordings/core/faq/types'
24
- import { closeDb } from '@skillrecordings/database'
25
- import type { Command } from 'commander'
26
-
27
- /**
28
- * Format timestamp for display
29
- */
30
- function formatDate(date: Date): string {
31
- return date.toLocaleString('en-US', {
32
- month: 'short',
33
- day: 'numeric',
34
- hour: '2-digit',
35
- minute: '2-digit',
36
- })
37
- }
38
-
39
- /**
40
- * Truncate string with ellipsis
41
- */
42
- function truncate(str: string, len: number): string {
43
- if (!str) return ''
44
- if (str.length <= len) return str
45
- return str.slice(0, len - 3) + '...'
46
- }
47
-
48
- /**
49
- * Color codes for terminal output
50
- */
51
- const COLORS = {
52
- reset: '\x1b[0m',
53
- green: '\x1b[32m',
54
- yellow: '\x1b[33m',
55
- blue: '\x1b[34m',
56
- cyan: '\x1b[36m',
57
- dim: '\x1b[2m',
58
- bold: '\x1b[1m',
59
- } as const
60
-
61
- /**
62
- * Display human-readable mining results
63
- */
64
- function displayResults(result: MineResult): void {
65
- console.log(`\n${COLORS.bold}📚 FAQ Mining Results${COLORS.reset}`)
66
- console.log('='.repeat(60))
67
-
68
- // Stats
69
- console.log(`\n${COLORS.cyan}Statistics:${COLORS.reset}`)
70
- console.log(` Total conversations: ${result.stats.totalConversations}`)
71
- console.log(` Clustered: ${result.stats.clusteredConversations}`)
72
- console.log(` Clusters formed: ${result.stats.clusterCount}`)
73
- console.log(` FAQ candidates: ${result.stats.candidateCount}`)
74
- console.log(
75
- ` Avg cluster size: ${result.stats.averageClusterSize.toFixed(1)}`
76
- )
77
- console.log(
78
- ` ${COLORS.green}Avg unchanged rate: ${(result.stats.averageUnchangedRate * 100).toFixed(1)}%${COLORS.reset}`
79
- )
80
-
81
- // Clusters
82
- if (result.clusters.length > 0) {
83
- console.log(`\n${COLORS.bold}📊 Clusters:${COLORS.reset}`)
84
- console.log('-'.repeat(60))
85
-
86
- for (const cluster of result.clusters.slice(0, 10)) {
87
- const unchangedPct = (cluster.unchangedRate * 100).toFixed(0)
88
- console.log(
89
- `\n${COLORS.cyan}Cluster ${cluster.id.slice(0, 8)}${COLORS.reset} (${cluster.conversations.length} convos, ${unchangedPct}% unchanged)`
90
- )
91
- console.log(
92
- ` ${COLORS.dim}Centroid: ${truncate(cluster.centroid, 150)}${COLORS.reset}`
93
- )
94
- console.log(
95
- ` ${COLORS.dim}Period: ${formatDate(cluster.oldest)} - ${formatDate(cluster.mostRecent)}${COLORS.reset}`
96
- )
97
- }
98
-
99
- if (result.clusters.length > 10) {
100
- console.log(
101
- `\n ${COLORS.dim}... and ${result.clusters.length - 10} more clusters${COLORS.reset}`
102
- )
103
- }
104
- }
105
-
106
- // Top candidates
107
- if (result.candidates.length > 0) {
108
- console.log(`\n${COLORS.bold}🏆 Top FAQ Candidates:${COLORS.reset}`)
109
- console.log('-'.repeat(60))
110
-
111
- // Filter to auto-surface candidates
112
- const autoSurface = filterAutoSurfaceCandidates(result.candidates)
113
-
114
- const displayCandidates =
115
- autoSurface.length > 0
116
- ? autoSurface.slice(0, 10)
117
- : result.candidates.slice(0, 10)
118
-
119
- const label =
120
- autoSurface.length > 0
121
- ? `(${autoSurface.length} auto-surface ready)`
122
- : '(no auto-surface candidates)'
123
-
124
- console.log(`${COLORS.dim}${label}${COLORS.reset}\n`)
125
-
126
- for (const [i, candidate] of displayCandidates.entries()) {
127
- if (!candidate) continue
128
-
129
- const confPct = (candidate.confidence * 100).toFixed(0)
130
- const unchangedPct = (candidate.unchangedRate * 100).toFixed(0)
131
-
132
- console.log(
133
- `${COLORS.bold}#${i + 1}${COLORS.reset} ${COLORS.dim}Confidence: ${confPct}% | ${candidate.clusterSize} occurrences | ${unchangedPct}% unchanged${COLORS.reset}`
134
- )
135
- console.log(
136
- ` ${COLORS.bold}Q:${COLORS.reset} ${truncate(candidate.question, 200)}`
137
- )
138
- console.log(
139
- ` ${COLORS.green}A:${COLORS.reset} ${truncate(candidate.answer, 300)}`
140
- )
141
- if (candidate.suggestedCategory) {
142
- console.log(
143
- ` ${COLORS.cyan}Category: ${candidate.suggestedCategory}${COLORS.reset}`
144
- )
145
- }
146
- if (candidate.tags.length > 0) {
147
- console.log(
148
- ` ${COLORS.dim}Tags: ${candidate.tags.slice(0, 5).join(', ')}${COLORS.reset}`
149
- )
150
- }
151
- console.log('')
152
- }
153
- }
154
-
155
- console.log('')
156
- }
157
-
158
- /** Default DuckDB cache path */
159
- const DEFAULT_CACHE_PATH = `${process.env.HOME}/skill/data/front-cache.db`
160
-
161
- /**
162
- * Create data source based on --source flag.
163
- */
164
- async function createSource(
165
- sourceType: 'cache' | 'front' | undefined,
166
- cachePath?: string
167
- ): Promise<DataSource | undefined> {
168
- if (sourceType === 'cache') {
169
- const dbPath = cachePath ?? DEFAULT_CACHE_PATH
170
- console.log(`📦 Using DuckDB cache: ${dbPath}`)
171
- return createDuckDBSource({ dbPath })
172
- }
173
-
174
- // Default to Front API (undefined means use existing behavior)
175
- return undefined
176
- }
177
-
178
- /**
179
- * Main command handler
180
- */
181
- async function faqMine(options: {
182
- app: string
183
- since: string
184
- limit?: number
185
- unchangedOnly?: boolean
186
- clusterThreshold?: number
187
- json?: boolean
188
- export?: string
189
- raw?: boolean
190
- source?: 'cache' | 'front'
191
- cachePath?: string
192
- dryRun?: boolean
193
- }): Promise<void> {
194
- if (!options.app) {
195
- console.error('Error: --app is required')
196
- process.exit(1)
197
- }
198
-
199
- if (!options.since) {
200
- console.error('Error: --since is required (e.g., 30d, 7d, 90d)')
201
- process.exit(1)
202
- }
203
-
204
- let source: DataSource | undefined
205
-
206
- try {
207
- // Create data source
208
- source = await createSource(options.source ?? 'cache', options.cachePath)
209
- // Dry run mode: show stats and sample data
210
- if (options.dryRun) {
211
- console.log(`\n🧪 DRY RUN MODE - ${options.app}`)
212
- console.log(` Source: ${source?.name ?? 'front'}`)
213
- console.log(` Since: ${options.since}`)
214
- console.log(` Limit: ${options.limit ?? 500}`)
215
-
216
- if (source?.getStats) {
217
- const stats = await source.getStats()
218
- console.log(`\n📊 Cache Statistics:`)
219
- console.log(
220
- ` Total conversations: ${stats.totalConversations.toLocaleString()}`
221
- )
222
- console.log(
223
- ` Filtered (matching criteria): ${stats.filteredConversations.toLocaleString()}`
224
- )
225
- console.log(
226
- ` Total messages: ${stats.totalMessages.toLocaleString()}`
227
- )
228
- console.log(` Inboxes: ${stats.inboxCount}`)
229
- if (stats.dateRange.oldest && stats.dateRange.newest) {
230
- console.log(
231
- ` Date range: ${stats.dateRange.oldest.toLocaleDateString()} - ${stats.dateRange.newest.toLocaleDateString()}`
232
- )
233
- }
234
- }
235
-
236
- // Fetch a small sample
237
- console.log(`\n📝 Sample conversations (limit 5):`)
238
- const sample = await mineConversations({
239
- appId: options.app,
240
- since: options.since,
241
- limit: 5,
242
- unchangedOnly: options.unchangedOnly ?? false,
243
- source,
244
- })
245
-
246
- for (const conv of sample) {
247
- console.log(`\n [${conv.conversationId}]`)
248
- console.log(` Q: ${truncate(conv.question, 100)}`)
249
- console.log(` A: ${truncate(conv.answer, 100)}`)
250
- console.log(` Tags: ${conv.tags.slice(0, 5).join(', ')}`)
251
- }
252
-
253
- console.log(
254
- `\n✅ Dry run complete. ${sample.length} sample conversations loaded.`
255
- )
256
- return
257
- }
258
-
259
- // Raw mode: just export Q&A pairs without clustering
260
- if (options.raw) {
261
- console.log(`📚 Mining raw Q&A pairs for ${options.app}...`)
262
- console.log(` Source: ${source?.name ?? 'front'}`)
263
- console.log(` Since: ${options.since}`)
264
- console.log(` Unchanged only: ${options.unchangedOnly ?? false}`)
265
-
266
- const conversations = await mineConversations({
267
- appId: options.app,
268
- since: options.since,
269
- limit: options.limit ?? 500,
270
- unchangedOnly: options.unchangedOnly ?? false,
271
- source,
272
- })
273
-
274
- const rawData = {
275
- generatedAt: new Date().toISOString(),
276
- options: {
277
- appId: options.app,
278
- since: options.since,
279
- unchangedOnly: options.unchangedOnly ?? false,
280
- },
281
- stats: {
282
- total: conversations.length,
283
- },
284
- conversations: conversations.map((c) => ({
285
- conversationId: c.conversationId,
286
- question: c.question,
287
- answer: c.answer,
288
- subject: c.subject,
289
- tags: c.tags,
290
- wasUnchanged: c.wasUnchanged,
291
- resolvedAt: c.resolvedAt.toISOString(),
292
- })),
293
- }
294
-
295
- if (options.export) {
296
- writeFileSync(options.export, JSON.stringify(rawData, null, 2), 'utf-8')
297
- console.log(
298
- `\n✅ Exported ${conversations.length} raw Q&A pairs to ${options.export}`
299
- )
300
- } else {
301
- console.log(JSON.stringify(rawData, null, 2))
302
- }
303
-
304
- return
305
- }
306
-
307
- const result = await mineFaqCandidates({
308
- appId: options.app,
309
- since: options.since,
310
- limit: options.limit ?? 500,
311
- unchangedOnly: options.unchangedOnly ?? false,
312
- clusterThreshold: options.clusterThreshold,
313
- source,
314
- })
315
-
316
- // JSON output
317
- if (options.json) {
318
- // Convert dates to ISO strings for JSON
319
- const jsonResult = {
320
- ...result,
321
- conversations: result.conversations.map((c) => ({
322
- ...c,
323
- resolvedAt: c.resolvedAt.toISOString(),
324
- _raw: undefined, // Don't include raw data in JSON
325
- })),
326
- clusters: result.clusters.map((c) => ({
327
- ...c,
328
- mostRecent: c.mostRecent.toISOString(),
329
- oldest: c.oldest.toISOString(),
330
- conversations: c.conversations.map((conv) => ({
331
- conversationId: conv.conversationId,
332
- question: conv.question.slice(0, 200),
333
- wasUnchanged: conv.wasUnchanged,
334
- })),
335
- })),
336
- candidates: result.candidates.map((c) => ({
337
- ...c,
338
- generatedAt: c.generatedAt.toISOString(),
339
- })),
340
- }
341
- console.log(JSON.stringify(jsonResult, null, 2))
342
- return
343
- }
344
-
345
- // Export to file
346
- if (options.export) {
347
- const exportData = {
348
- generatedAt: new Date().toISOString(),
349
- options: {
350
- appId: options.app,
351
- since: options.since,
352
- unchangedOnly: options.unchangedOnly,
353
- },
354
- stats: result.stats,
355
- candidates: result.candidates.map((c) => ({
356
- id: c.id,
357
- question: c.question,
358
- answer: c.answer,
359
- clusterSize: c.clusterSize,
360
- unchangedRate: c.unchangedRate,
361
- confidence: c.confidence,
362
- suggestedCategory: c.suggestedCategory,
363
- tags: c.tags,
364
- generatedAt: c.generatedAt.toISOString(),
365
- })),
366
- }
367
- writeFileSync(
368
- options.export,
369
- JSON.stringify(exportData, null, 2),
370
- 'utf-8'
371
- )
372
- console.log(
373
- `\n✅ Exported ${result.candidates.length} FAQ candidates to ${options.export}`
374
- )
375
- return
376
- }
377
-
378
- // Human-readable output
379
- displayResults(result)
380
- } catch (error) {
381
- console.error(
382
- 'Error:',
383
- error instanceof Error ? error.message : String(error)
384
- )
385
- process.exit(1)
386
- } finally {
387
- // Close data source if needed
388
- if (source?.close) {
389
- await source.close()
390
- }
391
- await closeDb()
392
- }
393
- }
394
-
395
- /**
396
- * Register FAQ mining commands with Commander
397
- */
398
- export function registerFaqMineCommands(program: Command): void {
399
- program
400
- .command('faq-mine')
401
- .description('Mine FAQ candidates from resolved support conversations')
402
- .requiredOption('-a, --app <slug>', 'App slug to mine from (required)')
403
- .requiredOption(
404
- '-s, --since <duration>',
405
- 'Time window to mine (e.g., 30d, 7d, 90d)'
406
- )
407
- .option(
408
- '-l, --limit <n>',
409
- 'Maximum conversations to process (default: 500)',
410
- parseInt
411
- )
412
- .option(
413
- '-u, --unchanged-only',
414
- 'Only include conversations where draft was sent unchanged'
415
- )
416
- .option(
417
- '--cluster-threshold <n>',
418
- 'Similarity threshold for clustering (default: 0.75)',
419
- parseFloat
420
- )
421
- .option('--json', 'Output as JSON')
422
- .option('-e, --export <file>', 'Export candidates to file')
423
- .option('-r, --raw', 'Export raw Q&A pairs without clustering (faster)')
424
- .option(
425
- '--source <type>',
426
- 'Data source: cache (DuckDB, default) or front (live API)',
427
- 'cache'
428
- )
429
- .option('--cache-path <path>', 'Path to DuckDB cache file')
430
- .option('-d, --dry-run', 'Show stats and sample data without full mining')
431
- .action(faqMine)
432
- }