@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.
- package/bin/skill.mjs +21 -0
- package/dist/chunk-2NCCVTEE.js +22342 -0
- package/dist/chunk-2NCCVTEE.js.map +1 -0
- package/dist/chunk-3E3GYSZR.js +7071 -0
- package/dist/chunk-3E3GYSZR.js.map +1 -0
- package/dist/chunk-F4EM72IH.js +86 -0
- package/dist/chunk-F4EM72IH.js.map +1 -0
- package/dist/chunk-FGP7KUQW.js +432 -0
- package/dist/chunk-FGP7KUQW.js.map +1 -0
- package/dist/chunk-H3D6VCME.js +55 -0
- package/dist/chunk-H3D6VCME.js.map +1 -0
- package/dist/chunk-HK3PEWFD.js +208 -0
- package/dist/chunk-HK3PEWFD.js.map +1 -0
- package/dist/chunk-KEV3QKXP.js +4495 -0
- package/dist/chunk-KEV3QKXP.js.map +1 -0
- package/dist/chunk-MG37YDAK.js +882 -0
- package/dist/chunk-MG37YDAK.js.map +1 -0
- package/dist/chunk-MLNDSBZ4.js +482 -0
- package/dist/chunk-MLNDSBZ4.js.map +1 -0
- package/dist/chunk-N2WIV2JV.js +22 -0
- package/dist/chunk-N2WIV2JV.js.map +1 -0
- package/dist/chunk-PWWRCN5W.js +2067 -0
- package/dist/chunk-PWWRCN5W.js.map +1 -0
- package/dist/chunk-SKHBM3XP.js +7746 -0
- package/dist/chunk-SKHBM3XP.js.map +1 -0
- package/dist/chunk-WFANXVQG.js +64 -0
- package/dist/chunk-WFANXVQG.js.map +1 -0
- package/dist/chunk-WYKL32C3.js +275 -0
- package/dist/chunk-WYKL32C3.js.map +1 -0
- package/dist/chunk-ZNF7XD2S.js +134 -0
- package/dist/chunk-ZNF7XD2S.js.map +1 -0
- package/dist/config-AUAIYDSI.js +20 -0
- package/dist/config-AUAIYDSI.js.map +1 -0
- package/dist/fileFromPath-XN7LXIBI.js +134 -0
- package/dist/fileFromPath-XN7LXIBI.js.map +1 -0
- package/dist/getMachineId-bsd-KW2E7VK3.js +42 -0
- package/dist/getMachineId-bsd-KW2E7VK3.js.map +1 -0
- package/dist/getMachineId-darwin-ROXJUJX5.js +42 -0
- package/dist/getMachineId-darwin-ROXJUJX5.js.map +1 -0
- package/dist/getMachineId-linux-KVZEHQSU.js +34 -0
- package/dist/getMachineId-linux-KVZEHQSU.js.map +1 -0
- package/dist/getMachineId-unsupported-PPRILPPA.js +25 -0
- package/dist/getMachineId-unsupported-PPRILPPA.js.map +1 -0
- package/dist/getMachineId-win-IIF36LEJ.js +44 -0
- package/dist/getMachineId-win-IIF36LEJ.js.map +1 -0
- package/dist/index.js +112703 -0
- package/dist/index.js.map +1 -0
- package/dist/lib-R6DEEJCP.js +7623 -0
- package/dist/lib-R6DEEJCP.js.map +1 -0
- package/dist/pipeline-IAVVAKTU.js +120 -0
- package/dist/pipeline-IAVVAKTU.js.map +1 -0
- package/dist/query-NTP5NVXN.js +25 -0
- package/dist/query-NTP5NVXN.js.map +1 -0
- package/dist/routing-BAEPFB7V.js +390 -0
- package/dist/routing-BAEPFB7V.js.map +1 -0
- package/dist/stripe-lookup-charge-EPRUMZDL.js +56 -0
- package/dist/stripe-lookup-charge-EPRUMZDL.js.map +1 -0
- package/dist/stripe-payment-history-SJPKA63N.js +67 -0
- package/dist/stripe-payment-history-SJPKA63N.js.map +1 -0
- package/dist/stripe-subscription-status-L4Z65GB3.js +58 -0
- package/dist/stripe-subscription-status-L4Z65GB3.js.map +1 -0
- package/dist/stripe-verify-refund-FZDKCIUQ.js +54 -0
- package/dist/stripe-verify-refund-FZDKCIUQ.js.map +1 -0
- package/dist/support-memory-WSG7SDKG.js +10 -0
- package/dist/support-memory-WSG7SDKG.js.map +1 -0
- package/package.json +10 -7
- package/.env.encrypted +0 -0
- package/CHANGELOG.md +0 -35
- package/data/tt-archive-dataset.json +0 -1
- package/data/validate-test-dataset.json +0 -97
- package/docs/CLI-AUTH.md +0 -504
- package/preload.ts +0 -18
- package/src/__tests__/init.test.ts +0 -74
- package/src/alignment-test.ts +0 -64
- package/src/check-apps.ts +0 -16
- package/src/commands/auth/decrypt.ts +0 -123
- package/src/commands/auth/encrypt.ts +0 -81
- package/src/commands/auth/index.ts +0 -50
- package/src/commands/auth/keygen.ts +0 -41
- package/src/commands/auth/status.ts +0 -164
- package/src/commands/axiom/forensic.ts +0 -868
- package/src/commands/axiom/index.ts +0 -697
- package/src/commands/build-dataset.ts +0 -311
- package/src/commands/db-status.ts +0 -47
- package/src/commands/deploys.ts +0 -219
- package/src/commands/eval-local/compare.ts +0 -171
- package/src/commands/eval-local/health.ts +0 -212
- package/src/commands/eval-local/index.ts +0 -76
- package/src/commands/eval-local/real-tools.ts +0 -416
- package/src/commands/eval-local/run.ts +0 -1168
- package/src/commands/eval-local/score-production.ts +0 -256
- package/src/commands/eval-local/seed.ts +0 -276
- package/src/commands/eval-pipeline/index.ts +0 -53
- package/src/commands/eval-pipeline/real-tools.ts +0 -492
- package/src/commands/eval-pipeline/run.ts +0 -1316
- package/src/commands/eval-pipeline/seed.ts +0 -395
- package/src/commands/eval-prompt.ts +0 -496
- package/src/commands/eval.test.ts +0 -253
- package/src/commands/eval.ts +0 -108
- package/src/commands/faq-classify.ts +0 -460
- package/src/commands/faq-cluster.ts +0 -135
- package/src/commands/faq-extract.ts +0 -249
- package/src/commands/faq-mine.ts +0 -432
- package/src/commands/faq-review.ts +0 -426
- package/src/commands/front/index.ts +0 -351
- package/src/commands/front/pull-conversations.ts +0 -275
- package/src/commands/front/tags.ts +0 -825
- package/src/commands/front-cache.ts +0 -1277
- package/src/commands/front-stats.ts +0 -75
- package/src/commands/health.test.ts +0 -82
- package/src/commands/health.ts +0 -362
- package/src/commands/init.test.ts +0 -89
- package/src/commands/init.ts +0 -106
- package/src/commands/inngest/client.ts +0 -294
- package/src/commands/inngest/events.ts +0 -296
- package/src/commands/inngest/investigate.ts +0 -382
- package/src/commands/inngest/runs.ts +0 -149
- package/src/commands/inngest/signal.ts +0 -143
- package/src/commands/kb-sync.ts +0 -498
- package/src/commands/memory/find.ts +0 -135
- package/src/commands/memory/get.ts +0 -87
- package/src/commands/memory/index.ts +0 -97
- package/src/commands/memory/stats.ts +0 -163
- package/src/commands/memory/store.ts +0 -49
- package/src/commands/memory/vote.ts +0 -159
- package/src/commands/pipeline.ts +0 -127
- package/src/commands/responses.ts +0 -856
- package/src/commands/tools.ts +0 -293
- package/src/commands/wizard.ts +0 -319
- package/src/index.ts +0 -172
- package/src/lib/crypto.ts +0 -56
- package/src/lib/env-loader.ts +0 -206
- package/src/lib/onepassword.ts +0 -137
- package/src/test-agent-local.ts +0 -115
- package/tsconfig.json +0 -11
- 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
|
-
}
|
package/src/commands/faq-mine.ts
DELETED
|
@@ -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
|
-
}
|