@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.
- package/.env.encrypted +0 -0
- package/CHANGELOG.md +35 -0
- package/README.md +214 -0
- package/bin/skill.ts +3 -0
- package/data/tt-archive-dataset.json +1 -0
- package/data/validate-test-dataset.json +97 -0
- package/docs/CLI-AUTH.md +504 -0
- package/package.json +38 -0
- package/preload.ts +18 -0
- package/src/__tests__/init.test.ts +74 -0
- package/src/alignment-test.ts +64 -0
- package/src/check-apps.ts +16 -0
- package/src/commands/auth/decrypt.ts +123 -0
- package/src/commands/auth/encrypt.ts +81 -0
- package/src/commands/auth/index.ts +50 -0
- package/src/commands/auth/keygen.ts +41 -0
- package/src/commands/auth/status.ts +164 -0
- package/src/commands/axiom/forensic.ts +868 -0
- package/src/commands/axiom/index.ts +697 -0
- package/src/commands/build-dataset.ts +311 -0
- package/src/commands/db-status.ts +47 -0
- package/src/commands/deploys.ts +219 -0
- package/src/commands/eval-local/compare.ts +171 -0
- package/src/commands/eval-local/health.ts +212 -0
- package/src/commands/eval-local/index.ts +76 -0
- package/src/commands/eval-local/real-tools.ts +416 -0
- package/src/commands/eval-local/run.ts +1168 -0
- package/src/commands/eval-local/score-production.ts +256 -0
- package/src/commands/eval-local/seed.ts +276 -0
- package/src/commands/eval-pipeline/index.ts +53 -0
- package/src/commands/eval-pipeline/real-tools.ts +492 -0
- package/src/commands/eval-pipeline/run.ts +1316 -0
- package/src/commands/eval-pipeline/seed.ts +395 -0
- package/src/commands/eval-prompt.ts +496 -0
- package/src/commands/eval.test.ts +253 -0
- package/src/commands/eval.ts +108 -0
- package/src/commands/faq-classify.ts +460 -0
- package/src/commands/faq-cluster.ts +135 -0
- package/src/commands/faq-extract.ts +249 -0
- package/src/commands/faq-mine.ts +432 -0
- package/src/commands/faq-review.ts +426 -0
- package/src/commands/front/index.ts +351 -0
- package/src/commands/front/pull-conversations.ts +275 -0
- package/src/commands/front/tags.ts +825 -0
- package/src/commands/front-cache.ts +1277 -0
- package/src/commands/front-stats.ts +75 -0
- package/src/commands/health.test.ts +82 -0
- package/src/commands/health.ts +362 -0
- package/src/commands/init.test.ts +89 -0
- package/src/commands/init.ts +106 -0
- package/src/commands/inngest/client.ts +294 -0
- package/src/commands/inngest/events.ts +296 -0
- package/src/commands/inngest/investigate.ts +382 -0
- package/src/commands/inngest/runs.ts +149 -0
- package/src/commands/inngest/signal.ts +143 -0
- package/src/commands/kb-sync.ts +498 -0
- package/src/commands/memory/find.ts +135 -0
- package/src/commands/memory/get.ts +87 -0
- package/src/commands/memory/index.ts +97 -0
- package/src/commands/memory/stats.ts +163 -0
- package/src/commands/memory/store.ts +49 -0
- package/src/commands/memory/vote.ts +159 -0
- package/src/commands/pipeline.ts +127 -0
- package/src/commands/responses.ts +856 -0
- package/src/commands/tools.ts +293 -0
- package/src/commands/wizard.ts +319 -0
- package/src/index.ts +172 -0
- package/src/lib/crypto.ts +56 -0
- package/src/lib/env-loader.ts +206 -0
- package/src/lib/onepassword.ts +137 -0
- package/src/test-agent-local.ts +115 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +10 -0
package/docs/CLI-AUTH.md
ADDED
|
@@ -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()
|