@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,395 @@
1
+ /**
2
+ * Seed command for eval-pipeline
3
+ * Populates MySQL and Qdrant with test fixtures for honest pipeline evals
4
+ */
5
+
6
+ import { join } from 'path'
7
+ import { readFile, readdir } from 'fs/promises'
8
+ import { glob } from 'glob'
9
+ import matter from 'gray-matter'
10
+
11
+ interface SeedOptions {
12
+ clean?: boolean
13
+ fixtures?: string
14
+ json?: boolean
15
+ }
16
+
17
+ interface SeedResult {
18
+ apps: number
19
+ trustScores: number
20
+ customers: number
21
+ knowledge: number
22
+ embeddings: number
23
+ scenarios: number
24
+ }
25
+
26
+ interface KnowledgeDoc {
27
+ id: string
28
+ content: string
29
+ type: string
30
+ app: string
31
+ tags: string[]
32
+ title: string
33
+ filePath: string
34
+ }
35
+
36
+ interface QdrantPoint {
37
+ id: string
38
+ vector: number[]
39
+ payload: Record<string, unknown>
40
+ }
41
+
42
+ function generateUUID(): string {
43
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
44
+ const r = (Math.random() * 16) | 0
45
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
46
+ return v.toString(16)
47
+ })
48
+ }
49
+
50
+ export async function seed(options: SeedOptions): Promise<void> {
51
+ const fixturesPath = options.fixtures || 'fixtures'
52
+
53
+ if (!options.json) {
54
+ console.log('\n🌱 Seeding eval-pipeline environment...\n')
55
+ }
56
+
57
+ const result: SeedResult = {
58
+ apps: 0,
59
+ trustScores: 0,
60
+ customers: 0,
61
+ knowledge: 0,
62
+ embeddings: 0,
63
+ scenarios: 0,
64
+ }
65
+
66
+ try {
67
+ // Get MySQL connection
68
+ const mysql = await import('mysql2/promise')
69
+ const connection = await mysql.createConnection({
70
+ host: process.env.MYSQL_HOST || 'localhost',
71
+ port: parseInt(process.env.MYSQL_PORT || '3306'),
72
+ user: process.env.MYSQL_USER || 'eval_user',
73
+ password: process.env.MYSQL_PASSWORD || 'eval_pass',
74
+ database: process.env.MYSQL_DATABASE || 'support_eval',
75
+ })
76
+
77
+ if (options.clean) {
78
+ if (!options.json) console.log('🧹 Cleaning existing data...')
79
+ await cleanDatabase(connection)
80
+ await cleanQdrant()
81
+ }
82
+
83
+ // 1. Seed apps
84
+ if (!options.json) console.log('📦 Seeding apps...')
85
+ const apps = await loadJsonFiles(join(fixturesPath, 'apps'))
86
+ result.apps = await seedApps(connection, apps)
87
+
88
+ // 2. Count trust scores seeded with apps
89
+ const [trustRows] = await connection.execute(
90
+ 'SELECT COUNT(*) as count FROM SUPPORT_trust_scores'
91
+ )
92
+ result.trustScores = (trustRows as any)[0].count
93
+
94
+ // 3. Load customer fixtures (used by mock integration, not stored in DB)
95
+ if (!options.json) console.log('👥 Loading customer fixtures...')
96
+ const customers = await loadJsonFiles(join(fixturesPath, 'customers'))
97
+ result.customers = customers.length
98
+
99
+ // 4. Seed knowledge base with embeddings
100
+ if (!options.json) console.log('📚 Seeding knowledge base...')
101
+ const knowledge = await loadKnowledgeFiles(join(fixturesPath, 'knowledge'))
102
+ result.knowledge = knowledge.length
103
+ result.embeddings = await seedKnowledgeBase(knowledge, !options.json)
104
+
105
+ // 5. Count scenarios
106
+ const scenarioFiles = await glob(join(fixturesPath, 'scenarios/**/*.json'))
107
+ result.scenarios = scenarioFiles.length
108
+
109
+ await connection.end()
110
+
111
+ if (options.json) {
112
+ console.log(JSON.stringify({ success: true, result }, null, 2))
113
+ } else {
114
+ console.log('\n✅ Seeding complete!\n')
115
+ console.log(` Apps: ${result.apps}`)
116
+ console.log(` Trust Scores: ${result.trustScores}`)
117
+ console.log(` Customers: ${result.customers} (fixture files)`)
118
+ console.log(` Knowledge: ${result.knowledge} documents`)
119
+ console.log(` Embeddings: ${result.embeddings}`)
120
+ console.log(` Scenarios: ${result.scenarios}\n`)
121
+ }
122
+ } catch (error) {
123
+ if (options.json) {
124
+ console.log(
125
+ JSON.stringify({
126
+ success: false,
127
+ error: error instanceof Error ? error.message : 'Unknown error',
128
+ })
129
+ )
130
+ } else {
131
+ console.error('❌ Seeding failed:', error)
132
+ }
133
+ process.exit(1)
134
+ }
135
+ }
136
+
137
+ async function cleanDatabase(connection: any): Promise<void> {
138
+ // Disable foreign key checks temporarily
139
+ await connection.execute('SET FOREIGN_KEY_CHECKS = 0')
140
+
141
+ const tables = [
142
+ 'SUPPORT_dead_letter_queue',
143
+ 'SUPPORT_trust_scores',
144
+ 'SUPPORT_audit_log',
145
+ 'SUPPORT_approval_requests',
146
+ 'SUPPORT_actions',
147
+ 'SUPPORT_conversations',
148
+ 'SUPPORT_apps',
149
+ ]
150
+
151
+ for (const table of tables) {
152
+ try {
153
+ await connection.execute(`TRUNCATE TABLE ${table}`)
154
+ } catch {
155
+ // Table might not exist yet, ignore
156
+ }
157
+ }
158
+
159
+ await connection.execute('SET FOREIGN_KEY_CHECKS = 1')
160
+ }
161
+
162
+ async function cleanQdrant(): Promise<void> {
163
+ const qdrantUrl = process.env.QDRANT_URL || 'http://localhost:6333'
164
+ const collection = process.env.QDRANT_COLLECTION || 'knowledge'
165
+
166
+ try {
167
+ await fetch(`${qdrantUrl}/collections/${collection}`, {
168
+ method: 'DELETE',
169
+ })
170
+ } catch {
171
+ // Collection might not exist, ignore
172
+ }
173
+ }
174
+
175
+ async function loadJsonFiles(dirPath: string): Promise<any[]> {
176
+ try {
177
+ const files = await readdir(dirPath)
178
+ const jsonFiles = files.filter((f) => f.endsWith('.json'))
179
+
180
+ const items = await Promise.all(
181
+ jsonFiles.map(async (file) => {
182
+ const content = await readFile(join(dirPath, file), 'utf-8')
183
+ return JSON.parse(content)
184
+ })
185
+ )
186
+
187
+ return items
188
+ } catch {
189
+ return []
190
+ }
191
+ }
192
+
193
+ async function loadKnowledgeFiles(basePath: string): Promise<KnowledgeDoc[]> {
194
+ const files = await glob(join(basePath, '**/*.md'))
195
+ const docs: KnowledgeDoc[] = []
196
+
197
+ for (const filePath of files) {
198
+ const content = await readFile(filePath, 'utf-8')
199
+ const { data: frontmatter, content: body } = matter(content)
200
+
201
+ // Extract title from first markdown heading or filename
202
+ const titleMatch = body.match(/^#\s+(.+)$/m)
203
+ const title = titleMatch?.[1]
204
+ ?? filePath.split('/').pop()?.replace('.md', '')
205
+ ?? 'Untitled'
206
+
207
+ docs.push({
208
+ id: generateUUID(),
209
+ content: body.trim(),
210
+ type: frontmatter.type || 'general',
211
+ app: frontmatter.app || 'unknown',
212
+ tags: frontmatter.tags || [],
213
+ title,
214
+ filePath,
215
+ })
216
+ }
217
+
218
+ return docs
219
+ }
220
+
221
+ async function seedApps(connection: any, apps: any[]): Promise<number> {
222
+ for (const app of apps) {
223
+ await connection.execute(
224
+ `INSERT INTO SUPPORT_apps (
225
+ id, slug, name, front_inbox_id, instructor_teammate_id,
226
+ stripe_account_id, stripe_connected, integration_base_url,
227
+ webhook_secret, capabilities, auto_approve_refund_days, auto_approve_transfer_days
228
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
229
+ ON DUPLICATE KEY UPDATE
230
+ name = VALUES(name),
231
+ integration_base_url = VALUES(integration_base_url),
232
+ capabilities = VALUES(capabilities)`,
233
+ [
234
+ app.id,
235
+ app.slug,
236
+ app.name,
237
+ app.front_inbox_id,
238
+ app.instructor_teammate_id || null,
239
+ app.stripe_account_id || null,
240
+ app.stripe_connected || false,
241
+ app.integration_base_url,
242
+ app.webhook_secret,
243
+ JSON.stringify(app.capabilities || []),
244
+ app.auto_approve_refund_days || 30,
245
+ app.auto_approve_transfer_days || 14,
246
+ ]
247
+ )
248
+
249
+ // Seed default trust scores for this app
250
+ const categories = ['refund', 'access', 'technical', 'general', 'transfer']
251
+ for (const category of categories) {
252
+ const id = `ts_${app.id}_${category}`
253
+ await connection.execute(
254
+ `INSERT INTO SUPPORT_trust_scores (id, app_id, category, trust_score, sample_count)
255
+ VALUES (?, ?, ?, 0.75, 25)
256
+ ON DUPLICATE KEY UPDATE id = id`,
257
+ [id, app.id, category]
258
+ )
259
+ }
260
+ }
261
+
262
+ return apps.length
263
+ }
264
+
265
+ async function seedKnowledgeBase(
266
+ docs: KnowledgeDoc[],
267
+ showProgress: boolean
268
+ ): Promise<number> {
269
+ if (docs.length === 0) return 0
270
+
271
+ const qdrantUrl = process.env.QDRANT_URL || 'http://localhost:6333'
272
+ const collection = process.env.QDRANT_COLLECTION || 'knowledge'
273
+ const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434'
274
+ const embeddingModel = process.env.EMBEDDING_MODEL || 'mxbai-embed-large'
275
+
276
+ // Determine vector size based on model
277
+ const vectorSize = embeddingModel.includes('mxbai') ? 1024 : 768
278
+
279
+ // Ensure Qdrant collection exists
280
+ const existsRes = await fetch(`${qdrantUrl}/collections/${collection}`)
281
+
282
+ if (existsRes.status === 404) {
283
+ const createRes = await fetch(`${qdrantUrl}/collections/${collection}`, {
284
+ method: 'PUT',
285
+ headers: { 'Content-Type': 'application/json' },
286
+ body: JSON.stringify({
287
+ vectors: {
288
+ size: vectorSize,
289
+ distance: 'Cosine',
290
+ },
291
+ }),
292
+ })
293
+
294
+ if (!createRes.ok) {
295
+ const error = await createRes.text()
296
+ throw new Error(`Failed to create Qdrant collection: ${error}`)
297
+ }
298
+ }
299
+
300
+ // Check if Ollama is available
301
+ try {
302
+ const healthRes = await fetch(`${ollamaUrl}/api/tags`)
303
+ if (!healthRes.ok) {
304
+ throw new Error('Ollama is not responding')
305
+ }
306
+ } catch (error) {
307
+ throw new Error(
308
+ `Ollama not available at ${ollamaUrl}. ` +
309
+ 'Make sure Ollama is running: ollama serve'
310
+ )
311
+ }
312
+
313
+ let embeddedCount = 0
314
+ const batchSize = 5
315
+ const points: QdrantPoint[] = []
316
+
317
+ for (let i = 0; i < docs.length; i++) {
318
+ const doc = docs[i]
319
+ if (!doc) continue
320
+
321
+ try {
322
+ // Generate embedding via Ollama
323
+ const embedRes = await fetch(`${ollamaUrl}/api/embed`, {
324
+ method: 'POST',
325
+ headers: { 'Content-Type': 'application/json' },
326
+ body: JSON.stringify({
327
+ model: embeddingModel,
328
+ input: doc.content,
329
+ }),
330
+ })
331
+
332
+ if (!embedRes.ok) {
333
+ const error = await embedRes.text()
334
+ throw new Error(`Embedding failed: ${error}`)
335
+ }
336
+
337
+ const embedData = (await embedRes.json()) as {
338
+ embeddings?: number[][]
339
+ embedding?: number[]
340
+ }
341
+ const vector = embedData.embeddings?.[0] ?? embedData.embedding
342
+
343
+ if (!vector) {
344
+ throw new Error('No embedding returned from Ollama')
345
+ }
346
+
347
+ points.push({
348
+ id: doc.id,
349
+ vector,
350
+ payload: {
351
+ content: doc.content,
352
+ title: doc.title,
353
+ type: doc.type,
354
+ app: doc.app,
355
+ tags: doc.tags,
356
+ },
357
+ })
358
+
359
+ embeddedCount++
360
+
361
+ if (showProgress) {
362
+ process.stdout.write(`\r Embedded: ${embeddedCount}/${docs.length}`)
363
+ }
364
+
365
+ // Batch upsert every N documents or at the end
366
+ if (points.length >= batchSize || i === docs.length - 1) {
367
+ const upsertRes = await fetch(
368
+ `${qdrantUrl}/collections/${collection}/points`,
369
+ {
370
+ method: 'PUT',
371
+ headers: { 'Content-Type': 'application/json' },
372
+ body: JSON.stringify({ points }),
373
+ }
374
+ )
375
+
376
+ if (!upsertRes.ok) {
377
+ const error = await upsertRes.text()
378
+ throw new Error(`Failed to upsert points: ${error}`)
379
+ }
380
+
381
+ points.length = 0 // Clear batch
382
+ }
383
+ } catch (error) {
384
+ if (showProgress) {
385
+ console.error(`\n Failed to embed ${doc.title}:`, error)
386
+ }
387
+ }
388
+ }
389
+
390
+ if (showProgress) {
391
+ console.log('') // New line after progress
392
+ }
393
+
394
+ return embeddedCount
395
+ }