@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,16 @@
1
+ import '../preload'
2
+ import { database, AppsTable } from '@skillrecordings/database'
3
+
4
+ async function main() {
5
+ const apps = await database.select().from(AppsTable)
6
+ console.log('\nšŸ“¦ Registered Apps:\n')
7
+ for (const app of apps) {
8
+ console.log(` ${app.slug} (${app.name})`)
9
+ console.log(` Instructor: ${app.instructor_teammate_id || 'āŒ NOT SET'}`)
10
+ console.log(` Front Inbox: ${app.front_inbox_id}`)
11
+ console.log(` Capabilities: ${JSON.stringify(app.capabilities)}`)
12
+ console.log()
13
+ }
14
+ process.exit(0)
15
+ }
16
+ main()
@@ -0,0 +1,123 @@
1
+ import path from 'path'
2
+ import fs from 'fs/promises'
3
+ import { decrypt } from '../../lib/crypto'
4
+ import { isServiceAccountConfigured, readSecret } from '../../lib/onepassword'
5
+
6
+ interface DecryptOptions {
7
+ output?: string
8
+ identity?: string
9
+ json?: boolean
10
+ }
11
+
12
+ /**
13
+ * Decrypt a file with an age private key
14
+ * @param input - Path to encrypted file (.age)
15
+ * @param options - Command options
16
+ */
17
+ export async function decryptAction(
18
+ input: string,
19
+ options: DecryptOptions
20
+ ): Promise<void> {
21
+ try {
22
+ // Resolve identity key using priority: --identity flag > AGE_SECRET_KEY env > 1Password
23
+ let privateKey: string | undefined
24
+
25
+ if (options.identity) {
26
+ // If starts with AGE-SECRET-KEY, use directly
27
+ if (options.identity.startsWith('AGE-SECRET-KEY-')) {
28
+ privateKey = options.identity
29
+ }
30
+ // If starts with op://, use 1Password
31
+ else if (options.identity.startsWith('op://')) {
32
+ if (isServiceAccountConfigured()) {
33
+ privateKey = await readSecret(options.identity)
34
+ } else {
35
+ const error =
36
+ '1Password reference provided but OP_SERVICE_ACCOUNT_TOKEN not set'
37
+ if (options.json) {
38
+ console.log(JSON.stringify({ success: false, error }))
39
+ } else {
40
+ console.error(`Error: ${error}`)
41
+ }
42
+ process.exit(1)
43
+ }
44
+ }
45
+ // Otherwise treat as file path
46
+ else {
47
+ privateKey = (await fs.readFile(options.identity, 'utf-8')).trim()
48
+ }
49
+ }
50
+
51
+ // Fall back to AGE_SECRET_KEY env var
52
+ if (!privateKey) {
53
+ privateKey = process.env.AGE_SECRET_KEY
54
+ }
55
+
56
+ if (!privateKey) {
57
+ const error =
58
+ 'No private key specified. Use --identity <key|file|op://ref> or set AGE_SECRET_KEY environment variable.'
59
+ if (options.json) {
60
+ console.log(JSON.stringify({ success: false, error }))
61
+ } else {
62
+ console.error(`Error: ${error}`)
63
+ }
64
+ process.exit(1)
65
+ }
66
+
67
+ // Validate private key format
68
+ if (!privateKey.startsWith('AGE-SECRET-KEY-')) {
69
+ const error = `Invalid private key format. Expected AGE-SECRET-KEY-1..., got: ${privateKey.slice(0, 15)}...`
70
+ if (options.json) {
71
+ console.log(JSON.stringify({ success: false, error }))
72
+ } else {
73
+ console.error(`Error: ${error}`)
74
+ }
75
+ process.exit(1)
76
+ }
77
+
78
+ // Read encrypted file
79
+ const encryptedData = await fs.readFile(input)
80
+
81
+ // Decrypt
82
+ const decrypted = await decrypt(encryptedData, privateKey)
83
+
84
+ // Determine output
85
+ if (options.output) {
86
+ // Write to file
87
+ await fs.writeFile(options.output, decrypted, 'utf-8')
88
+
89
+ if (options.json) {
90
+ console.log(
91
+ JSON.stringify({
92
+ success: true,
93
+ input: path.resolve(input),
94
+ output: path.resolve(options.output),
95
+ })
96
+ )
97
+ } else {
98
+ console.log(`āœ“ Decrypted ${input} → ${options.output}`)
99
+ }
100
+ } else {
101
+ // Write to stdout
102
+ if (options.json) {
103
+ console.log(
104
+ JSON.stringify({
105
+ success: true,
106
+ input: path.resolve(input),
107
+ content: decrypted,
108
+ })
109
+ )
110
+ } else {
111
+ console.log(decrypted)
112
+ }
113
+ }
114
+ } catch (err) {
115
+ const error = err instanceof Error ? err.message : String(err)
116
+ if (options.json) {
117
+ console.log(JSON.stringify({ success: false, error }))
118
+ } else {
119
+ console.error(`Error: ${error}`)
120
+ }
121
+ process.exit(1)
122
+ }
123
+ }
@@ -0,0 +1,81 @@
1
+ import path from 'path'
2
+ import fs from 'fs/promises'
3
+ import { encrypt } from '../../lib/crypto'
4
+
5
+ interface EncryptOptions {
6
+ output?: string
7
+ recipient?: string
8
+ json?: boolean
9
+ }
10
+
11
+ /**
12
+ * Encrypt a file with an age public key
13
+ * @param input - Path to input file
14
+ * @param options - Command options
15
+ */
16
+ export async function encryptAction(
17
+ input: string,
18
+ options: EncryptOptions
19
+ ): Promise<void> {
20
+ try {
21
+ // Get recipient key from --recipient or AGE_PUBLIC_KEY env
22
+ const recipientKey = options.recipient || process.env.AGE_PUBLIC_KEY
23
+
24
+ if (!recipientKey) {
25
+ const error =
26
+ 'No recipient key specified. Use --recipient or set AGE_PUBLIC_KEY environment variable.'
27
+ if (options.json) {
28
+ console.log(JSON.stringify({ success: false, error }))
29
+ } else {
30
+ console.error(`Error: ${error}`)
31
+ }
32
+ process.exit(1)
33
+ }
34
+
35
+ // Validate recipient key format (age1...)
36
+ if (!recipientKey.startsWith('age1')) {
37
+ const error = `Invalid recipient key format. Expected age1..., got: ${recipientKey.slice(0, 10)}...`
38
+ if (options.json) {
39
+ console.log(JSON.stringify({ success: false, error }))
40
+ } else {
41
+ console.error(`Error: ${error}`)
42
+ }
43
+ process.exit(1)
44
+ }
45
+
46
+ // Read input file
47
+ const data = await fs.readFile(input, 'utf-8')
48
+
49
+ // Encrypt
50
+ const encrypted = await encrypt(data, recipientKey)
51
+
52
+ // Determine output path
53
+ const outputPath = options.output || `${input}.age`
54
+
55
+ // Write encrypted data
56
+ await fs.writeFile(outputPath, encrypted)
57
+
58
+ // Output success
59
+ if (options.json) {
60
+ console.log(
61
+ JSON.stringify({
62
+ success: true,
63
+ input: path.resolve(input),
64
+ output: path.resolve(outputPath),
65
+ recipientKey: recipientKey.slice(0, 10) + '...',
66
+ })
67
+ )
68
+ } else {
69
+ console.log(`āœ“ Encrypted ${input} → ${outputPath}`)
70
+ console.log(` Recipient: ${recipientKey.slice(0, 10)}...`)
71
+ }
72
+ } catch (err) {
73
+ const error = err instanceof Error ? err.message : String(err)
74
+ if (options.json) {
75
+ console.log(JSON.stringify({ success: false, error }))
76
+ } else {
77
+ console.error(`Error: ${error}`)
78
+ }
79
+ process.exit(1)
80
+ }
81
+ }
@@ -0,0 +1,50 @@
1
+ import type { Command } from 'commander'
2
+ import { decryptAction } from './decrypt'
3
+ import { encryptAction } from './encrypt'
4
+ import { keygen } from './keygen'
5
+ import { statusAction } from './status'
6
+
7
+ /**
8
+ * Register auth commands with Commander
9
+ */
10
+ export function registerAuthCommands(program: Command): void {
11
+ const auth = program
12
+ .command('auth')
13
+ .description('Manage encrypted secrets for CLI distribution')
14
+
15
+ auth
16
+ .command('keygen')
17
+ .description('Generate age keypair for encryption')
18
+ .option('--output <path>', 'Write keypair to file')
19
+ .option('--json', 'Output as JSON')
20
+ .action(async (options) => {
21
+ await keygen(options)
22
+ })
23
+
24
+ auth
25
+ .command('encrypt')
26
+ .description('Encrypt a file with age public key')
27
+ .argument('<input>', 'Input file path')
28
+ .option('--output <path>', 'Output file path (default: <input>.age)')
29
+ .option('--recipient <key>', 'Age public key (or read from AGE_PUBLIC_KEY)')
30
+ .option('--json', 'Output as JSON')
31
+ .action(encryptAction)
32
+
33
+ auth
34
+ .command('decrypt')
35
+ .description('Decrypt a file with age private key')
36
+ .argument('<input>', 'Input file path (.age)')
37
+ .option('--output <path>', 'Output file path (default: stdout)')
38
+ .option(
39
+ '--identity <key>',
40
+ 'Age private key, file path, or 1Password ref (op://...)'
41
+ )
42
+ .option('--json', 'Output as JSON')
43
+ .action(decryptAction)
44
+
45
+ auth
46
+ .command('status')
47
+ .description('Check encryption setup status')
48
+ .option('--json', 'Output as JSON')
49
+ .action(statusAction)
50
+ }
@@ -0,0 +1,41 @@
1
+ import { writeFileSync } from 'fs'
2
+ import { generateKeypair } from '../../lib/crypto'
3
+
4
+ interface KeygenOptions {
5
+ json?: boolean
6
+ output?: string
7
+ }
8
+
9
+ /**
10
+ * Generate age keypair for encryption
11
+ */
12
+ export async function keygen(options: KeygenOptions = {}) {
13
+ const { publicKey, privateKey } = await generateKeypair()
14
+
15
+ if (options.json) {
16
+ // JSON output mode
17
+ console.log(JSON.stringify({ publicKey, privateKey }, null, 2))
18
+ } else if (options.output) {
19
+ // Write to file
20
+ const content = `# Age keypair generated by skill CLI
21
+ # Public key (share this):
22
+ ${publicKey}
23
+
24
+ # Private key (KEEP SECRET):
25
+ ${privateKey}
26
+ `
27
+ writeFileSync(options.output, content, 'utf-8')
28
+ console.log(`Keypair written to ${options.output}`)
29
+ console.error(
30
+ `\nāš ļø WARNING: Private key written to disk. Keep ${options.output} secure!`
31
+ )
32
+ } else {
33
+ // Normal mode: public to stdout, private to stderr with warning
34
+ console.log(publicKey)
35
+ console.error('\nāš ļø Private key (KEEP SECRET):')
36
+ console.error(privateKey)
37
+ console.error(
38
+ '\nāš ļø Store this private key securely. Anyone with it can decrypt your data.'
39
+ )
40
+ }
41
+ }
@@ -0,0 +1,164 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { isServiceAccountConfigured } from '../../lib/onepassword.js'
4
+
5
+ interface StatusOptions {
6
+ json?: boolean
7
+ }
8
+
9
+ interface AuthStatus {
10
+ envLocal: {
11
+ exists: boolean
12
+ path: string
13
+ }
14
+ envEncrypted: {
15
+ exists: boolean
16
+ path: string
17
+ }
18
+ ageSecretKey: {
19
+ configured: boolean
20
+ masked?: string
21
+ }
22
+ opServiceAccountToken: {
23
+ configured: boolean
24
+ }
25
+ envSource: 'local' | 'encrypted' | 'none'
26
+ }
27
+
28
+ /**
29
+ * Check if a file exists
30
+ */
31
+ async function fileExists(filePath: string): Promise<boolean> {
32
+ try {
33
+ await fs.access(filePath)
34
+ return true
35
+ } catch {
36
+ return false
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Mask AGE secret key for display
42
+ * Shows AGE-SECRET-KEY-1XXX...XXX format
43
+ */
44
+ function maskAgeKey(key: string): string {
45
+ if (!key.startsWith('AGE-SECRET-KEY-1')) {
46
+ return 'INVALID_FORMAT'
47
+ }
48
+ const prefix = key.slice(0, 19) // AGE-SECRET-KEY-1XXX
49
+ const suffix = key.slice(-3) // XXX
50
+ return `${prefix}...${suffix}`
51
+ }
52
+
53
+ /**
54
+ * Determine which env source would be used based on priority
55
+ */
56
+ function determineEnvSource(
57
+ localExists: boolean,
58
+ encryptedExists: boolean,
59
+ ageKeyConfigured: boolean
60
+ ): 'local' | 'encrypted' | 'none' {
61
+ // Priority from env-loader.ts:
62
+ // 1. Local .env files
63
+ // 2. Encrypted .env.encrypted (if AGE_SECRET_KEY is set)
64
+ // 3. None
65
+
66
+ if (localExists) {
67
+ return 'local'
68
+ }
69
+
70
+ if (encryptedExists && ageKeyConfigured) {
71
+ return 'encrypted'
72
+ }
73
+
74
+ return 'none'
75
+ }
76
+
77
+ /**
78
+ * Check auth configuration status
79
+ */
80
+ export async function statusAction(options: StatusOptions): Promise<void> {
81
+ const cliDir = path.resolve(import.meta.dirname, '../../..')
82
+
83
+ // Check .env.local
84
+ const envLocalPath = path.join(cliDir, '.env.local')
85
+ const envLocalExists = await fileExists(envLocalPath)
86
+
87
+ // Check .env.encrypted
88
+ const envEncryptedPath = path.join(cliDir, '.env.encrypted')
89
+ const envEncryptedExists = await fileExists(envEncryptedPath)
90
+
91
+ // Check AGE_SECRET_KEY
92
+ const ageSecretKey = process.env.AGE_SECRET_KEY
93
+ const ageKeyConfigured = Boolean(ageSecretKey)
94
+
95
+ // Check OP_SERVICE_ACCOUNT_TOKEN
96
+ const opConfigured = isServiceAccountConfigured()
97
+
98
+ // Determine env source
99
+ const envSource = determineEnvSource(
100
+ envLocalExists,
101
+ envEncryptedExists,
102
+ ageKeyConfigured
103
+ )
104
+
105
+ const status: AuthStatus = {
106
+ envLocal: {
107
+ exists: envLocalExists,
108
+ path: envLocalPath,
109
+ },
110
+ envEncrypted: {
111
+ exists: envEncryptedExists,
112
+ path: envEncryptedPath,
113
+ },
114
+ ageSecretKey: {
115
+ configured: ageKeyConfigured,
116
+ masked:
117
+ ageKeyConfigured && ageSecretKey ? maskAgeKey(ageSecretKey) : undefined,
118
+ },
119
+ opServiceAccountToken: {
120
+ configured: opConfigured,
121
+ },
122
+ envSource,
123
+ }
124
+
125
+ if (options.json) {
126
+ console.log(JSON.stringify(status, null, 2))
127
+ return
128
+ }
129
+
130
+ // Human-readable output
131
+ console.log('Auth Configuration Status\n')
132
+
133
+ console.log('Environment Files:')
134
+ console.log(
135
+ ` .env.local: ${envLocalExists ? 'āœ“' : 'āœ—'} ${envLocalExists ? '(exists)' : '(not found)'}`
136
+ )
137
+ console.log(` Path: ${envLocalPath}`)
138
+ console.log(
139
+ ` .env.encrypted: ${envEncryptedExists ? 'āœ“' : 'āœ—'} ${envEncryptedExists ? '(exists)' : '(not found)'}`
140
+ )
141
+ console.log(` Path: ${envEncryptedPath}`)
142
+
143
+ console.log('\nEnvironment Variables:')
144
+ console.log(
145
+ ` AGE_SECRET_KEY: ${ageKeyConfigured ? 'āœ“' : 'āœ—'} ${ageKeyConfigured ? `(${status.ageSecretKey.masked})` : '(not set)'}`
146
+ )
147
+ console.log(
148
+ ` OP_SERVICE_ACCOUNT_TOKEN: ${opConfigured ? 'āœ“' : 'āœ—'} ${opConfigured ? '(configured)' : '(not set)'}`
149
+ )
150
+
151
+ console.log('\nEnv Source Priority:')
152
+ if (envSource === 'local') {
153
+ console.log(' → Using local .env.local file')
154
+ } else if (envSource === 'encrypted') {
155
+ console.log(' → Using encrypted .env.encrypted file')
156
+ } else {
157
+ console.log(' → No env source available')
158
+ console.log('\n⚠ No environment configuration found.')
159
+ console.log('\nOptions:')
160
+ console.log(' 1. Create .env.local with your secrets')
161
+ console.log(' 2. Use encrypted .env.encrypted (requires AGE_SECRET_KEY)')
162
+ console.log('\nSee docs/ENV.md for setup instructions.')
163
+ }
164
+ }