@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,504 @@
1
+ # CLI Auth - Encrypted Secrets Distribution
2
+
3
+ Age encryption system for distributing CLI secrets to team members via 1Password.
4
+
5
+ ## Overview
6
+
7
+ The auth system uses [age encryption](https://github.com/FiloSottile/age) to distribute encrypted environment files to team members. Admins encrypt secrets once, commit the encrypted file to git, and store the decryption key in 1Password. Team members with the 1Password Service Account token file get **automatic, transparent decryption** - they just run `skill` and it works.
8
+
9
+ **Key benefits:**
10
+ - Encrypted file committed to git (`.env.encrypted`)
11
+ - **Zero-config for team** - just drop `~/.op-token` and run CLI
12
+ - Automatic decryption via 1Password integration
13
+ - Key rotation without redistributing encrypted files
14
+ - Local `.env.local` overrides encrypted file for development
15
+
16
+ ## Quick Start
17
+
18
+ ### Team Member Setup (Zero-Config)
19
+
20
+ 1. Get `~/.op-token` file from admin (contains 1Password Service Account token)
21
+ 2. Place it at `~/.op-token`:
22
+ ```bash
23
+ # File contents:
24
+ export OP_SERVICE_ACCOUNT_TOKEN="ops_xxx..."
25
+ ```
26
+ 3. **That's it** - run any `skill` command:
27
+ ```bash
28
+ skill db-status # Just works - secrets auto-loaded from 1Password
29
+ ```
30
+
31
+ The CLI automatically:
32
+ 1. Detects `~/.op-token` exists
33
+ 2. Reads the service account token
34
+ 3. Fetches the age private key from 1Password (`op://Support/skill-cli-age-key/private_key`)
35
+ 4. Decrypts `.env.encrypted` on the fly
36
+ 5. Injects secrets into the environment
37
+
38
+ ### Local Development (Priority Override)
39
+
40
+ If you have a local `.env.local` file, it takes priority over the encrypted file:
41
+
42
+ ```bash
43
+ # packages/cli/.env.local exists? → used directly, no decryption needed
44
+ # packages/cli/.env.local missing? → auto-decrypt .env.encrypted via 1Password
45
+ ```
46
+
47
+ This means:
48
+ - Your local overrides always win
49
+ - You can modify values without affecting team
50
+ - Production secrets stay in sync via encrypted file
51
+
52
+ ### Admin Setup (First Time)
53
+
54
+ 1. Generate keypair:
55
+ ```bash
56
+ skill auth keygen
57
+ ```
58
+ 2. Store private key in 1Password:
59
+ - Vault: `Support`
60
+ - Item: `skill-cli-age-key`
61
+ - Field: `private_key`
62
+ 3. Encrypt current secrets:
63
+ ```bash
64
+ skill auth encrypt .env.local --recipient <public-key> --output .env.encrypted
65
+ ```
66
+ 4. Commit `.env.encrypted` to git
67
+ 5. Create and distribute `~/.op-token` file with service account token
68
+
69
+ ## Commands Reference
70
+
71
+ ### `skill auth keygen`
72
+
73
+ Generate an age encryption keypair.
74
+
75
+ ```bash
76
+ # Output to stdout (public key) and stderr (private key with warning)
77
+ skill auth keygen
78
+
79
+ # Save to file
80
+ skill auth keygen --output keypair.txt
81
+
82
+ # JSON output
83
+ skill auth keygen --json
84
+ ```
85
+
86
+ **Output:**
87
+ - Public key: `age1...` (share this)
88
+ - Private key: `AGE-SECRET-KEY-1...` (KEEP SECRET)
89
+
90
+ **Options:**
91
+ - `--output <path>` - Write keypair to file
92
+ - `--json` - Output as JSON
93
+
94
+ **Security:** Private key is written to stderr with warnings in normal mode. Store it securely in 1Password.
95
+
96
+ ### `skill auth encrypt`
97
+
98
+ Encrypt a file with an age public key.
99
+
100
+ ```bash
101
+ # Encrypt using AGE_PUBLIC_KEY env var
102
+ skill auth encrypt .env.local
103
+
104
+ # Specify recipient key explicitly
105
+ skill auth encrypt .env.local --recipient age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
106
+
107
+ # Custom output path
108
+ skill auth encrypt .env.local --output secrets.age
109
+
110
+ # JSON output
111
+ skill auth encrypt .env.local --json
112
+ ```
113
+
114
+ **Arguments:**
115
+ - `<input>` - Path to file to encrypt
116
+
117
+ **Options:**
118
+ - `--output <path>` - Output file path (default: `<input>.age`)
119
+ - `--recipient <key>` - Age public key (or use `AGE_PUBLIC_KEY` env var)
120
+ - `--json` - Output as JSON
121
+
122
+ **Environment:**
123
+ - `AGE_PUBLIC_KEY` - Default recipient key if `--recipient` not provided
124
+
125
+ **Exit codes:**
126
+ - `0` - Success
127
+ - `1` - Error (missing recipient, invalid key format, file not found)
128
+
129
+ ### `skill auth decrypt`
130
+
131
+ Decrypt a file with an age private key.
132
+
133
+ ```bash
134
+ # Decrypt to stdout (using AGE_SECRET_KEY env var)
135
+ skill auth decrypt .env.local.age
136
+
137
+ # Decrypt to file
138
+ skill auth decrypt .env.local.age --output .env.local
139
+
140
+ # Use 1Password reference (automatic with OP_SERVICE_ACCOUNT_TOKEN set)
141
+ skill auth decrypt .env.local.age --identity "op://Private/cli-age-key/private_key"
142
+
143
+ # Use private key from file
144
+ skill auth decrypt .env.local.age --identity /path/to/key.txt
145
+
146
+ # Use private key directly
147
+ skill auth decrypt .env.local.age --identity "AGE-SECRET-KEY-1..."
148
+
149
+ # JSON output
150
+ skill auth decrypt .env.local.age --json
151
+ ```
152
+
153
+ **Arguments:**
154
+ - `<input>` - Path to encrypted file (`.age`)
155
+
156
+ **Options:**
157
+ - `--output <path>` - Output file path (default: stdout)
158
+ - `--identity <key>` - Private key, file path, or 1Password reference (`op://...`)
159
+ - `--json` - Output as JSON
160
+
161
+ **Identity resolution priority:**
162
+ 1. `--identity` flag value
163
+ 2. `AGE_SECRET_KEY` environment variable
164
+ 3. Error if none provided
165
+
166
+ **Environment:**
167
+ - `AGE_SECRET_KEY` - Default private key or 1Password reference
168
+ - `OP_SERVICE_ACCOUNT_TOKEN` - Required for 1Password references
169
+
170
+ **Exit codes:**
171
+ - `0` - Success
172
+ - `1` - Error (missing identity, invalid key format, decryption failed)
173
+
174
+ ### `skill auth status`
175
+
176
+ Check encryption setup status.
177
+
178
+ ```bash
179
+ skill auth status
180
+ skill auth status --json
181
+ ```
182
+
183
+ **Status checks:**
184
+ - Is 1Password CLI (`op`) installed?
185
+ - Is `OP_SERVICE_ACCOUNT_TOKEN` set?
186
+ - Can we read age keys from 1Password?
187
+ - Are `AGE_PUBLIC_KEY` and `AGE_SECRET_KEY` configured?
188
+
189
+ **Options:**
190
+ - `--json` - Output as JSON
191
+
192
+ **Note:** This command is a placeholder and will be implemented by another worker.
193
+
194
+ ## Distribution Workflow
195
+
196
+ ### Admin: Initial Setup
197
+
198
+ 1. **Generate keypair:**
199
+ ```bash
200
+ skill auth keygen --json > keypair.json
201
+ ```
202
+
203
+ 2. **Store private key in 1Password:**
204
+ - Vault: `Support` (or your team's shared vault)
205
+ - Item name: `skill-cli-age-key`
206
+ - Add field: `private_key` with value `AGE-SECRET-KEY-1...`
207
+ - Reference becomes: `op://Support/skill-cli-age-key/private_key`
208
+
209
+ 3. **Encrypt secrets:**
210
+ ```bash
211
+ cd packages/cli
212
+ skill auth encrypt .env.local --recipient <public-key> --output .env.encrypted
213
+ ```
214
+
215
+ 4. **Commit encrypted file:**
216
+ ```bash
217
+ git add .env.encrypted
218
+ git commit -m "Add encrypted env for team distribution"
219
+ ```
220
+
221
+ 5. **Create ~/.op-token template:**
222
+ ```bash
223
+ # Create file with service account token
224
+ echo 'export OP_SERVICE_ACCOUNT_TOKEN="ops_xxx..."' > op-token-template.txt
225
+ ```
226
+
227
+ 6. **Distribute token file securely:**
228
+ - Share `op-token-template.txt` via secure channel (1Password itself, secure Slack DM)
229
+ - Team members save as `~/.op-token`
230
+
231
+ ### Team: Zero-Config Setup
232
+
233
+ 1. **Get the token file from admin** (via secure channel)
234
+
235
+ 2. **Save as ~/.op-token:**
236
+ ```bash
237
+ # File should contain:
238
+ export OP_SERVICE_ACCOUNT_TOKEN="ops_xxx..."
239
+ ```
240
+
241
+ 3. **Use the CLI** - that's it:
242
+ ```bash
243
+ skill db-status # Secrets auto-loaded
244
+ skill front message abc123 # Just works
245
+ ```
246
+
247
+ ### How Auto-Decryption Works
248
+
249
+ The CLI's env loader (`src/lib/env-loader.ts`) follows this priority:
250
+
251
+ 1. **Local .env.local exists?** → Use it directly (no decryption)
252
+ 2. **~/.op-token exists?** → Auto-decrypt `.env.encrypted`:
253
+ - Parse token from `~/.op-token`
254
+ - Set `OP_SERVICE_ACCOUNT_TOKEN` in environment
255
+ - Fetch age private key from `op://Support/skill-cli-age-key/private_key`
256
+ - Decrypt `.env.encrypted` on the fly
257
+ - Inject secrets into `process.env`
258
+ 3. **AGE_SECRET_KEY set?** → Decrypt using that key directly
259
+ 4. **None of the above?** → Error with clear instructions
260
+
261
+ ## 1Password Service Account Setup
262
+
263
+ Service accounts enable headless authentication for CLI and team automation.
264
+
265
+ ### Create Service Account
266
+
267
+ 1. **Open 1Password:**
268
+ - Go to Integrations > Service Accounts
269
+ - Click "Create Service Account"
270
+ - Name it: "skill-cli" or "CLI Secrets Distribution"
271
+
272
+ 2. **Configure Access:**
273
+ - Grant read access to the `Support` vault (or wherever age key is stored)
274
+ - Note: Service accounts can't use 2FA or sign in to apps
275
+
276
+ 3. **Generate Token:**
277
+ - Copy the token (`ops_xxx...`)
278
+ - Store it securely (only shown once)
279
+
280
+ ### Create ~/.op-token File
281
+
282
+ The CLI auto-loads tokens from `~/.op-token` - no shell profile changes needed.
283
+
284
+ ```bash
285
+ # Create the token file
286
+ cat > ~/.op-token << 'EOF'
287
+ export OP_SERVICE_ACCOUNT_TOKEN="ops_xxx..."
288
+ EOF
289
+
290
+ # Secure the file
291
+ chmod 600 ~/.op-token
292
+ ```
293
+
294
+ **Why this file format?**
295
+ - Can be sourced manually: `source ~/.op-token`
296
+ - Auto-parsed by CLI without sourcing
297
+ - Same format works in shell profiles if you prefer that
298
+
299
+ ### Store Age Key in 1Password
300
+
301
+ 1. **Create Item:**
302
+ - Vault: `Support` (must be accessible to service account)
303
+ - Item type: "Secure Note" or "Password"
304
+ - Title: `skill-cli-age-key`
305
+
306
+ 2. **Add Private Key Field:**
307
+ - Add custom field: `private_key`
308
+ - Value: `AGE-SECRET-KEY-1...`
309
+
310
+ 3. **Verify Reference:**
311
+ - Reference: `op://Support/skill-cli-age-key/private_key`
312
+ - Test: `source ~/.op-token && op read "op://Support/skill-cli-age-key/private_key"`
313
+
314
+ ### Troubleshooting 1Password
315
+
316
+ **"op: command not found"**
317
+ - Install 1Password CLI: https://developer.1password.com/docs/cli/get-started/
318
+ - Linux: `curl -sS https://downloads.1password.com/linux/keys/1password.asc | sudo gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg`
319
+
320
+ **CLI not loading secrets automatically**
321
+ - Check `~/.op-token` exists and has correct format
322
+ - Verify token starts with `ops_`
323
+ - Test manually: `source ~/.op-token && op whoami`
324
+
325
+ **"Failed to read secret from 1Password"**
326
+ - Check token has access to vault: `op vault list`
327
+ - Verify item exists: `op item get skill-cli-age-key --vault Support`
328
+ - Test read directly: `op read "op://Support/skill-cli-age-key/private_key"`
329
+
330
+ **"No accounts configured for use with 1Password CLI"**
331
+ - This means `OP_SERVICE_ACCOUNT_TOKEN` isn't set
332
+ - Check `~/.op-token` file exists and is readable
333
+ - Verify the token value is correct (single line, no extra quotes)
334
+
335
+ ## Key Rotation
336
+
337
+ Rotate keys periodically or when team members leave.
338
+
339
+ ### 1. Generate New Keypair
340
+
341
+ ```bash
342
+ skill auth keygen --json > new-keypair.json
343
+ ```
344
+
345
+ ### 2. Update 1Password Vault
346
+
347
+ - Open "cli-age-key" item in 1Password
348
+ - Update `private_key` field with new private key
349
+ - Keep old key in history for emergency decryption
350
+
351
+ ### 3. Re-encrypt All Secrets
352
+
353
+ ```bash
354
+ # Update AGE_PUBLIC_KEY in env
355
+ export AGE_PUBLIC_KEY="<new-public-key>"
356
+
357
+ # Re-encrypt all secret files
358
+ skill auth encrypt .env.local --output .env.local.age
359
+ skill auth encrypt apps/web/.env.local --output apps/web/.env.local.age
360
+ skill auth encrypt apps/slack/.env.local --output apps/slack/.env.local.age
361
+ ```
362
+
363
+ ### 4. Distribute New Encrypted Files
364
+
365
+ - Commit updated `.age` files to git
366
+ - Or share via your distribution channel
367
+ - Team members can decrypt with same 1Password reference (key updated in vault)
368
+
369
+ ### 5. Notify Team
370
+
371
+ Send notification with:
372
+ - Date of key rotation
373
+ - Reason (routine, security incident, team change)
374
+ - Action required (pull latest `.age` files, re-decrypt)
375
+
376
+ ## Troubleshooting
377
+
378
+ ### "Found .env.encrypted but cannot decrypt"
379
+
380
+ **Cause:** No decryption key available. Neither `~/.op-token` nor `AGE_SECRET_KEY` is configured.
381
+
382
+ **Fix (recommended - use 1Password):**
383
+ 1. Get `~/.op-token` file from admin
384
+ 2. Place at `~/.op-token`
385
+ 3. Retry the CLI command
386
+
387
+ **Fix (alternative - manual key):**
388
+ ```bash
389
+ export AGE_SECRET_KEY="AGE-SECRET-KEY-1..."
390
+ ```
391
+
392
+ ### "No recipient key specified"
393
+
394
+ **Cause:** `AGE_PUBLIC_KEY` env var not set and `--recipient` not provided.
395
+
396
+ **Fix:**
397
+ ```bash
398
+ export AGE_PUBLIC_KEY="age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
399
+ # Or use --recipient flag
400
+ ```
401
+
402
+ ### "No private key specified"
403
+
404
+ **Cause:** `AGE_SECRET_KEY` env var not set and `--identity` not provided.
405
+
406
+ **Fix:**
407
+ ```bash
408
+ export AGE_SECRET_KEY="op://Support/skill-cli-age-key/private_key"
409
+ # Or use --identity flag
410
+ ```
411
+
412
+ ### "Invalid recipient key format"
413
+
414
+ **Cause:** Public key doesn't start with `age1`.
415
+
416
+ **Fix:** Generate new keypair with `skill auth keygen` or verify key was copied correctly.
417
+
418
+ ### "Invalid private key format"
419
+
420
+ **Cause:** Private key doesn't start with `AGE-SECRET-KEY-1`.
421
+
422
+ **Fix:** Generate new keypair with `skill auth keygen` or verify key was copied correctly.
423
+
424
+ ### "1Password reference provided but OP_SERVICE_ACCOUNT_TOKEN not set"
425
+
426
+ **Cause:** Using `op://` reference without setting service account token.
427
+
428
+ **Fix:**
429
+ ```bash
430
+ # Create ~/.op-token (preferred)
431
+ echo 'export OP_SERVICE_ACCOUNT_TOKEN="ops_xxx"' > ~/.op-token
432
+
433
+ # Or export directly
434
+ export OP_SERVICE_ACCOUNT_TOKEN="ops_xxx"
435
+ ```
436
+
437
+ ### "Failed to read secret from 1Password"
438
+
439
+ **Cause:** Invalid reference, vault access issue, or 1Password CLI not authenticated.
440
+
441
+ **Fix:**
442
+ 1. Verify reference format: `op://VaultName/ItemName/FieldName`
443
+ 2. Test direct read: `source ~/.op-token && op read "op://Support/skill-cli-age-key/private_key"`
444
+ 3. Check vault access: `op vault list`
445
+ 4. Verify service account has read access to the vault
446
+
447
+ ### Decryption fails with "bad ciphertext"
448
+
449
+ **Cause:** Private key doesn't match the public key used for encryption.
450
+
451
+ **Fix:**
452
+ - Verify you're using the correct private key
453
+ - Check if key rotation occurred (get new encrypted file)
454
+ - Ensure private key wasn't truncated or modified
455
+
456
+ ### "~/.op-token exists but not working"
457
+
458
+ **Cause:** File format incorrect or token expired.
459
+
460
+ **Fix:**
461
+ 1. Check file format:
462
+ ```bash
463
+ cat ~/.op-token
464
+ # Should be: export OP_SERVICE_ACCOUNT_TOKEN="ops_..."
465
+ ```
466
+ 2. Test token manually:
467
+ ```bash
468
+ source ~/.op-token && op whoami
469
+ ```
470
+ 3. Get new token from admin if expired
471
+
472
+ ## Security Best Practices
473
+
474
+ 1. **Never commit private keys**
475
+ - Private keys go in 1Password only
476
+ - Use `.env.local` (gitignored) for local development
477
+ - Use `op://` references in config files
478
+
479
+ 2. **Rotate keys regularly**
480
+ - Rotate keys every 90 days or when team changes
481
+ - Keep rotation history for emergency decryption
482
+ - Document rotation dates
483
+
484
+ 3. **Limit service account access**
485
+ - Grant read-only access to specific vaults
486
+ - Create separate service accounts for different purposes
487
+ - Audit access logs regularly
488
+
489
+ 4. **Verify encrypted files**
490
+ - Test decryption after encryption
491
+ - Verify file integrity before distribution
492
+ - Keep backups of encrypted files
493
+
494
+ 5. **Monitor access**
495
+ - Use 1Password audit logs to track secret access
496
+ - Set up alerts for suspicious activity
497
+ - Review service account usage monthly
498
+
499
+ ## Related Documentation
500
+
501
+ - [CLI README](../README.md) - Overview of all CLI commands
502
+ - [1Password Service Accounts](https://developer.1password.com/docs/service-accounts/) - Official docs
503
+ - [age encryption](https://github.com/FiloSottile/age) - Encryption tool used
504
+ - [Environment Setup](../../../docs/ENV.md) - Repository environment configuration
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@skillrecordings/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "skill": "./bin/skill.ts"
7
+ },
8
+ "scripts": {
9
+ "dev": "bun src/index.ts",
10
+ "check-types": "tsc --noEmit",
11
+ "test": "vitest --run"
12
+ },
13
+ "devDependencies": {
14
+ "@repo/typescript-config": "*",
15
+ "@types/node": "^22.15.3",
16
+ "typescript": "5.9.2"
17
+ },
18
+ "dependencies": {
19
+ "@axiomhq/js": "^1.3.1",
20
+ "@inquirer/prompts": "^8.2.0",
21
+ "@skillrecordings/core": "workspace:*",
22
+ "@skillrecordings/database": "workspace:*",
23
+ "@skillrecordings/front-sdk": "workspace:*",
24
+ "@skillrecordings/memory": "workspace:*",
25
+ "@skillrecordings/sdk": "workspace:*",
26
+ "age-encryption": "^0.3.0",
27
+ "ai": "^6.0.49",
28
+ "commander": "^12.1.0",
29
+ "dotenv-flow": "^4.1.0",
30
+ "glob": "^13.0.0",
31
+ "gray-matter": "^4.0.3",
32
+ "mysql2": "^3.16.1",
33
+ "zod": "^4.3.5"
34
+ },
35
+ "optionalDependencies": {
36
+ "duckdb": "^1.4.3"
37
+ }
38
+ }
package/preload.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { loadSecrets } from './src/lib/env-loader.js'
2
+
3
+ // Load env from the CLI package directory before anything else
4
+ // Don't crash for commands that don't need secrets (--help, auth, etc.)
5
+ const noSecretsNeeded = process.argv.some((a) =>
6
+ ['--help', '-h', '--version', '-V', 'auth'].includes(a)
7
+ )
8
+
9
+ try {
10
+ await loadSecrets()
11
+ } catch (err) {
12
+ if (noSecretsNeeded) {
13
+ // Silently continue - these commands don't need secrets
14
+ } else {
15
+ console.error(err instanceof Error ? err.message : String(err))
16
+ process.exit(1)
17
+ }
18
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import * as crypto from 'node:crypto'
3
+
4
+ /**
5
+ * Test helper: Generate webhook secret
6
+ * Should generate a 64-character hex string (32 bytes)
7
+ */
8
+ function generateWebhookSecret(): string {
9
+ return crypto.randomBytes(32).toString('hex')
10
+ }
11
+
12
+ describe('generateWebhookSecret', () => {
13
+ it('generates a 64-character hex string', () => {
14
+ const secret = generateWebhookSecret()
15
+
16
+ expect(secret).toHaveLength(64)
17
+ expect(secret).toMatch(/^[0-9a-f]{64}$/)
18
+ })
19
+
20
+ it('generates unique secrets on each call', () => {
21
+ const secret1 = generateWebhookSecret()
22
+ const secret2 = generateWebhookSecret()
23
+
24
+ expect(secret1).not.toBe(secret2)
25
+ })
26
+
27
+ it('generates valid hex characters only', () => {
28
+ const secret = generateWebhookSecret()
29
+
30
+ // Should contain only 0-9 and a-f
31
+ expect(secret).toMatch(/^[0-9a-f]+$/)
32
+ })
33
+ })
34
+
35
+ describe('init command (placeholder)', () => {
36
+ beforeEach(() => {
37
+ vi.clearAllMocks()
38
+ })
39
+
40
+ it('should accept app name as argument', async () => {
41
+ // TODO: Test when init command is fully implemented
42
+ // This is a placeholder for testing command parsing
43
+ const appName = 'my-app'
44
+ expect(appName).toBe('my-app')
45
+ })
46
+
47
+ it('should work with --org option', async () => {
48
+ // TODO: Test when init command is fully implemented
49
+ // This is a placeholder for testing --org flag
50
+ const org = 'my-org'
51
+ expect(org).toBe('my-org')
52
+ })
53
+
54
+ it('should output webhook URL format', () => {
55
+ // TODO: Test when init command is fully implemented
56
+ // Expected format: https://support.skillrecordings.com/api/webhooks/front/{appId}
57
+ const appId = 'test-app'
58
+ const webhookUrl = `https://support.skillrecordings.com/api/webhooks/front/${appId}`
59
+
60
+ expect(webhookUrl).toContain('/api/webhooks/front/')
61
+ expect(webhookUrl).toContain(appId)
62
+ })
63
+
64
+ it('should output .env example with webhook secret', () => {
65
+ // TODO: Test when init command is fully implemented
66
+ // Expected format:
67
+ // FRONT_WEBHOOK_SECRET=<secret>
68
+ const secret = generateWebhookSecret()
69
+ const envExample = `FRONT_WEBHOOK_SECRET=${secret}`
70
+
71
+ expect(envExample).toContain('FRONT_WEBHOOK_SECRET=')
72
+ expect(envExample).toContain(secret)
73
+ })
74
+ })
@@ -0,0 +1,64 @@
1
+ import '../preload'
2
+ import { runSupportAgent } from '@skillrecordings/core/agent'
3
+ import { database } from '@skillrecordings/database'
4
+ import { IntegrationClient } from '@skillrecordings/sdk/client'
5
+ import { readFileSync } from 'fs'
6
+
7
+ interface Sample {
8
+ triggerMessage: { subject: string; body: string }
9
+ agentResponse: { text: string }
10
+ app: string
11
+ }
12
+
13
+ async function main() {
14
+ const dataset: Sample[] = JSON.parse(readFileSync('data/eval-dataset.json', 'utf-8'))
15
+
16
+ // Pick samples with leakage patterns
17
+ const leakyOnes = dataset.filter(s =>
18
+ s.agentResponse.text.includes('No instructor routing') ||
19
+ s.agentResponse.text.includes("can't route") ||
20
+ s.agentResponse.text.includes('Per my guidelines')
21
+ ).slice(0, 3)
22
+
23
+ for (const sample of leakyOnes) {
24
+ const appSlug = sample.app === 'unknown' ? 'ai-hero' : sample.app
25
+ const app = await database.query.AppsTable.findFirst({
26
+ where: (apps, { eq }) => eq(apps.slug, appSlug),
27
+ })
28
+
29
+ if (!app) continue
30
+
31
+ const client = new IntegrationClient({
32
+ baseUrl: app.integration_base_url,
33
+ webhookSecret: app.webhook_secret,
34
+ })
35
+
36
+ console.log('\n' + '='.repeat(60))
37
+ console.log('SUBJECT:', sample.triggerMessage.subject.slice(0, 60))
38
+ console.log('PROD (leaky):', sample.agentResponse.text.slice(0, 120) + '...')
39
+
40
+ try {
41
+ const result = await runSupportAgent({
42
+ message: sample.triggerMessage.body,
43
+ conversationHistory: [],
44
+ customerContext: { email: '[EMAIL]' },
45
+ appId: appSlug,
46
+ model: 'anthropic/claude-haiku-4-5',
47
+ integrationClient: client,
48
+ appConfig: {
49
+ instructor_teammate_id: app.instructor_teammate_id || undefined,
50
+ stripeAccountId: app.stripe_account_id || undefined,
51
+ },
52
+ })
53
+
54
+ console.log('LOCAL:', result.response?.slice(0, 120) || '(no response - GOOD)')
55
+ console.log('TOOLS:', result.toolCalls.map(t => t.name).join(', ') || '(none)')
56
+ console.log('IMPROVED?:', !result.response ? '✅ Yes (silent)' : (result.response.includes('instructor') ? '❌ No' : '✅ Yes'))
57
+ } catch (e: any) {
58
+ console.log('ERROR:', e.message)
59
+ }
60
+ }
61
+
62
+ process.exit(0)
63
+ }
64
+ main()