skillvault 0.4.1 → 0.5.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 (3) hide show
  1. package/dist/cli.d.ts +10 -11
  2. package/dist/cli.js +117 -248
  3. package/package.json +1 -1
package/dist/cli.d.ts CHANGED
@@ -1,21 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * SkillVault Agent — Secure skill distribution for Claude Code.
3
+ * SkillVault — Secure skill distribution for Claude Code.
4
4
  *
5
5
  * Usage:
6
- * npx skillvault --invite CODE # Setup / add publisher
6
+ * npx skillvault --invite CODE # Setup: redeem invite + sync + install
7
+ * npx skillvault --load SKILL # Decrypt a skill (used by Claude Code)
7
8
  * npx skillvault --status # Show publishers & skills
8
9
  * npx skillvault --refresh # Re-authenticate tokens + sync
9
- * npx skillvault --sync # Sync vaults + install stubs
10
- * npx skillvault # Start MCP server (after setup)
10
+ * npx skillvault --sync # Sync vaults + update stubs
11
11
  *
12
12
  * How it works:
13
- * 1. Redeems invite codegets publisher token
14
- * 2. Downloads encrypted skill vaults to ~/.skillvault/vaults/
15
- * 3. Installs stub SKILL.md files in ~/.claude/skills/ (Claude discovers these)
16
- * 4. Starts MCP server exposing skillvault_load tool
17
- * 5. When Claude triggers a skill, it calls skillvault_load → decrypts on demand
18
- * 6. Decrypted content lives only in conversation context — never on disk
19
- * 7. License is validated on every load — revoked license = dead skill
13
+ * 1. Customer runs --invite oncevaults downloaded, stubs installed
14
+ * 2. Claude Code discovers stub SKILL.md files in ~/.claude/skills/
15
+ * 3. When a skill is triggered, Claude runs: npx skillvault --load <name>
16
+ * 4. The CLI decrypts the vault (license checked) and outputs to stdout
17
+ * 5. Claude reads the output and follows the instructions
18
+ * 6. Decrypted content is never written to disk
20
19
  */
21
20
  export {};
package/dist/cli.js CHANGED
@@ -1,31 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * SkillVault Agent — Secure skill distribution for Claude Code.
3
+ * SkillVault — Secure skill distribution for Claude Code.
4
4
  *
5
5
  * Usage:
6
- * npx skillvault --invite CODE # Setup / add publisher
6
+ * npx skillvault --invite CODE # Setup: redeem invite + sync + install
7
+ * npx skillvault --load SKILL # Decrypt a skill (used by Claude Code)
7
8
  * npx skillvault --status # Show publishers & skills
8
9
  * npx skillvault --refresh # Re-authenticate tokens + sync
9
- * npx skillvault --sync # Sync vaults + install stubs
10
- * npx skillvault # Start MCP server (after setup)
10
+ * npx skillvault --sync # Sync vaults + update stubs
11
11
  *
12
12
  * How it works:
13
- * 1. Redeems invite codegets publisher token
14
- * 2. Downloads encrypted skill vaults to ~/.skillvault/vaults/
15
- * 3. Installs stub SKILL.md files in ~/.claude/skills/ (Claude discovers these)
16
- * 4. Starts MCP server exposing skillvault_load tool
17
- * 5. When Claude triggers a skill, it calls skillvault_load → decrypts on demand
18
- * 6. Decrypted content lives only in conversation context — never on disk
19
- * 7. License is validated on every load — revoked license = dead skill
13
+ * 1. Customer runs --invite oncevaults downloaded, stubs installed
14
+ * 2. Claude Code discovers stub SKILL.md files in ~/.claude/skills/
15
+ * 3. When a skill is triggered, Claude runs: npx skillvault --load <name>
16
+ * 4. The CLI decrypts the vault (license checked) and outputs to stdout
17
+ * 5. Claude reads the output and follows the instructions
18
+ * 6. Decrypted content is never written to disk
20
19
  */
21
- import { createServer } from 'node:http';
22
20
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
23
21
  import { join } from 'node:path';
24
22
  import { createDecipheriv, createPublicKey, diffieHellman, hkdfSync, generateKeyPairSync, } from 'node:crypto';
25
- const VERSION = '0.4.1';
23
+ const VERSION = '0.5.0';
26
24
  const HOME = process.env.HOME || process.env.USERPROFILE || '~';
27
25
  const API_URL = process.env.SKILLVAULT_API_URL || 'https://api.getskillvault.com';
28
- const MCP_PORT = parseInt(process.env.SKILLVAULT_MCP_PORT || '9877', 10);
29
26
  const CONFIG_DIR = join(HOME, '.skillvault');
30
27
  const CONFIG_PATH = join(CONFIG_DIR, 'agent-config.json');
31
28
  const VAULT_DIR = join(CONFIG_DIR, 'vaults');
@@ -34,6 +31,8 @@ const SKILLS_DIR = join(HOME, '.claude', 'skills');
34
31
  const args = process.argv.slice(2);
35
32
  const inviteIdx = args.indexOf('--invite');
36
33
  const inviteCode = inviteIdx >= 0 ? args[inviteIdx + 1] : null;
34
+ const loadIdx = args.indexOf('--load');
35
+ const loadSkillName = loadIdx >= 0 ? args[loadIdx + 1] : null;
37
36
  const helpFlag = args.includes('--help') || args.includes('-h');
38
37
  const versionFlag = args.includes('--version') || args.includes('-v');
39
38
  const statusFlag = args.includes('--status');
@@ -45,24 +44,24 @@ if (versionFlag) {
45
44
  }
46
45
  if (helpFlag) {
47
46
  console.log(`
48
- SkillVault — Secure skill distribution for Claude Code
47
+ SkillVault v${VERSION} — Secure skill distribution for Claude Code
49
48
 
50
49
  Usage:
51
- npx skillvault --invite CODE Setup or add a new publisher
52
- npx skillvault --status Show all publishers, skills, and statuses
53
- npx skillvault --refresh Re-authenticate expired tokens + sync skills
54
- npx skillvault --sync Sync vaults and install skill stubs
55
- npx skillvault Start MCP server (default, after setup)
56
- npx skillvault --help Show this help
57
- npx skillvault --version Show version
50
+ npx skillvault --invite CODE Setup: redeem invite, sync, install skills
51
+ npx skillvault --load SKILL Decrypt and output a skill (used by Claude)
52
+ npx skillvault --status Show publishers, skills, and statuses
53
+ npx skillvault --refresh Re-authenticate tokens + sync skills
54
+ npx skillvault --sync Sync vaults and update skill stubs
55
+ npx skillvault --help Show this help
56
+ npx skillvault --version Show version
58
57
 
59
- Skills are encrypted at rest. When Claude Code triggers a skill, the
60
- MCP server decrypts it on demand and returns the content to Claude's
61
- session. The decrypted content is never written to disk.
58
+ Skills are encrypted at rest in ~/.skillvault/vaults/. When Claude Code
59
+ triggers a skill, it runs \`npx skillvault --load <name>\` to decrypt on
60
+ demand. The decrypted content goes to stdout — never written to disk.
61
+ License is validated on every load.
62
62
 
63
63
  Environment:
64
64
  SKILLVAULT_API_URL Server URL (default: https://api.getskillvault.com)
65
- SKILLVAULT_MCP_PORT MCP server port (default: 9877)
66
65
  `);
67
66
  process.exit(0);
68
67
  }
@@ -96,10 +95,10 @@ function saveConfig(config) {
96
95
  mkdirSync(CONFIG_DIR, { recursive: true });
97
96
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
98
97
  }
99
- // ── Setup: Redeem Invite + Configure MCP + Sync ──
98
+ // ── Setup: Redeem Invite + Sync + Install ──
100
99
  async function setup(code) {
101
- console.log('🔐 SkillVault Setup');
102
- console.log(` Redeeming invite code: ${code}`);
100
+ console.error('🔐 SkillVault Setup');
101
+ console.error(` Redeeming invite code: ${code}`);
103
102
  const response = await fetch(`${API_URL}/auth/companion/token`, {
104
103
  method: 'POST',
105
104
  headers: { 'Content-Type': 'application/json' },
@@ -111,11 +110,11 @@ async function setup(code) {
111
110
  process.exit(1);
112
111
  }
113
112
  const data = await response.json();
114
- console.log(` ✅ Authenticated`);
113
+ console.error(` ✅ Authenticated`);
115
114
  if (data.email)
116
- console.log(` 📧 ${data.email}`);
115
+ console.error(` 📧 ${data.email}`);
117
116
  if (data.capabilities.length > 0) {
118
- console.log(` 📦 Skills: ${data.capabilities.join(', ')}`);
117
+ console.error(` 📦 Skills: ${data.capabilities.join(', ')}`);
119
118
  }
120
119
  const existingConfig = loadConfig();
121
120
  const publisherEntry = {
@@ -128,11 +127,11 @@ async function setup(code) {
128
127
  const existingIdx = existingConfig.publishers.findIndex(p => p.id === data.publisher_id);
129
128
  if (existingIdx >= 0) {
130
129
  existingConfig.publishers[existingIdx] = publisherEntry;
131
- console.log(` 🔄 Updated publisher: ${publisherEntry.name}`);
130
+ console.error(` 🔄 Updated publisher: ${publisherEntry.name}`);
132
131
  }
133
132
  else {
134
133
  existingConfig.publishers.push(publisherEntry);
135
- console.log(` ➕ Added publisher: ${publisherEntry.name}. You now have skills from ${existingConfig.publishers.length} publishers.`);
134
+ console.error(` ➕ Added publisher: ${publisherEntry.name}`);
136
135
  }
137
136
  if (data.customer_token)
138
137
  existingConfig.customer_token = data.customer_token;
@@ -150,49 +149,39 @@ async function setup(code) {
150
149
  });
151
150
  }
152
151
  mkdirSync(join(VAULT_DIR, data.publisher_id), { recursive: true });
153
- // Configure MCP server in Claude Code
154
- configureMCP();
152
+ // Clean up legacy MCP config if present
153
+ cleanupMCPConfig();
155
154
  // Sync vaults and install stubs
156
- console.log('');
157
- console.log(' Syncing skills...');
155
+ console.error('');
156
+ console.error(' Syncing skills...');
158
157
  await syncSkills();
159
158
  const installResult = await installSkillStubs();
160
- console.log('');
159
+ console.error('');
161
160
  if (installResult.installed > 0) {
162
- console.log(` ✅ ${installResult.installed} skill${installResult.installed !== 1 ? 's' : ''} ready`);
161
+ console.error(` ✅ ${installResult.installed} skill${installResult.installed !== 1 ? 's' : ''} installed to ~/.claude/skills/`);
163
162
  }
164
- console.log(' Setup complete! Start the agent with: npx skillvault');
165
- console.log(' Then restart Claude Code to discover your skills.');
166
- console.log('');
163
+ console.error(' Setup complete! Restart Claude Code to use your skills.');
164
+ console.error('');
167
165
  }
168
- /**
169
- * Configure Claude Code to connect to the SkillVault MCP server.
170
- */
171
- function configureMCP() {
166
+ /** Remove legacy MCP server config from ~/.claude/.mcp.json */
167
+ function cleanupMCPConfig() {
172
168
  const mcpConfigPath = join(HOME, '.claude', '.mcp.json');
173
169
  try {
174
- let mcpConfig = {};
175
170
  if (existsSync(mcpConfigPath)) {
176
- mcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf8'));
171
+ const mcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf8'));
172
+ if (mcpConfig.mcpServers?.skillvault) {
173
+ delete mcpConfig.mcpServers.skillvault;
174
+ writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), { mode: 0o600 });
175
+ console.error(' 🧹 Removed legacy MCP config');
176
+ }
177
177
  }
178
- if (!mcpConfig.mcpServers)
179
- mcpConfig.mcpServers = {};
180
- mcpConfig.mcpServers.skillvault = {
181
- type: 'url',
182
- url: `http://127.0.0.1:${MCP_PORT}/mcp`,
183
- };
184
- mkdirSync(join(HOME, '.claude'), { recursive: true });
185
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), { mode: 0o600 });
186
- console.log(` ✅ Claude Code MCP configured`);
187
- }
188
- catch {
189
- console.error(` ⚠️ Failed to configure MCP — manually add to ~/.claude/.mcp.json`);
190
178
  }
179
+ catch { }
191
180
  }
192
181
  async function showStatus() {
193
182
  const config = loadConfig();
194
183
  if (!config) {
195
- console.log('🔐 SkillVault Agent\n');
184
+ console.log('🔐 SkillVault\n');
196
185
  console.log(' Not set up yet. Run:');
197
186
  console.log(' npx skillvault --invite YOUR_CODE\n');
198
187
  process.exit(1);
@@ -201,7 +190,7 @@ async function showStatus() {
201
190
  if (config.customer_email)
202
191
  console.log(` Account: ${config.customer_email}`);
203
192
  console.log(` Config: ${CONFIG_PATH}`);
204
- console.log(` Skills: ${SKILLS_DIR} (stubs — encrypted at rest)`);
193
+ console.log(` Skills: ${SKILLS_DIR}`);
205
194
  console.log(` Server: ${config.api_url}`);
206
195
  console.log('');
207
196
  let skills = [];
@@ -267,15 +256,13 @@ async function showStatus() {
267
256
  async function refreshTokens() {
268
257
  const config = loadConfig();
269
258
  if (!config) {
270
- console.log('🔐 SkillVault Agent\n');
271
- console.log(' Not set up yet. Run:');
272
- console.log(' npx skillvault --invite YOUR_CODE\n');
259
+ console.error('🔐 SkillVault\n Not set up. Run: npx skillvault --invite YOUR_CODE\n');
273
260
  process.exit(1);
274
261
  }
275
- console.log('🔐 SkillVault Token Refresh\n');
262
+ console.error('🔐 SkillVault Token Refresh\n');
276
263
  let anyRefreshed = false;
277
264
  for (const pub of config.publishers) {
278
- process.stdout.write(` Refreshing ${pub.name}... `);
265
+ process.stderr.write(` Refreshing ${pub.name}... `);
279
266
  try {
280
267
  const res = await fetch(`${config.api_url}/auth/companion/refresh`, {
281
268
  method: 'POST',
@@ -287,26 +274,26 @@ async function refreshTokens() {
287
274
  pub.token = data.token;
288
275
  if (data.customer_token)
289
276
  config.customer_token = data.customer_token;
290
- console.log('✅ refreshed');
277
+ console.error('✅');
291
278
  anyRefreshed = true;
292
279
  }
293
280
  else if (res.status === 401) {
294
- console.log('❌ expired — re-invite required');
281
+ console.error('❌ expired — re-invite required');
295
282
  }
296
283
  else {
297
- console.log(`❌ server error (${res.status})`);
284
+ console.error(`❌ server error (${res.status})`);
298
285
  }
299
286
  }
300
287
  catch {
301
- console.log('❌ offline');
288
+ console.error('❌ offline');
302
289
  }
303
290
  }
304
291
  if (anyRefreshed) {
305
292
  saveConfig(config);
306
- console.log('\n Tokens updated.\n');
293
+ console.error('\n Tokens updated.\n');
307
294
  }
308
295
  else {
309
- console.log('\n No tokens refreshed. You may need to run: npx skillvault --invite CODE\n');
296
+ console.error('\n No tokens refreshed.\n');
310
297
  }
311
298
  }
312
299
  async function syncSkills() {
@@ -337,19 +324,19 @@ async function syncSkills() {
337
324
  continue;
338
325
  }
339
326
  const remoteSkillNames = new Set(skills.map(s => s.skill_name));
340
- // Revocation: remove stubs for skills no longer in the remote list
327
+ // Revocation: remove stubs for skills no longer in remote list
341
328
  try {
342
329
  if (existsSync(pubVaultDir)) {
343
330
  const localVaults = readdirSync(pubVaultDir).filter(f => f.endsWith('.vault'));
344
331
  for (const vaultFile of localVaults) {
345
332
  const localSkillName = vaultFile.replace(/\.vault$/, '');
346
333
  if (!remoteSkillNames.has(localSkillName)) {
347
- console.log(`[sync] Grant revoked: "${localSkillName}" from ${pub.name}`);
334
+ console.error(`[sync] Grant revoked: "${localSkillName}" from ${pub.name}`);
348
335
  const skillDir = join(SKILLS_DIR, localSkillName);
349
336
  try {
350
337
  if (existsSync(skillDir) && existsSync(join(skillDir, 'manifest.json'))) {
351
338
  rmSync(skillDir, { recursive: true, force: true });
352
- console.log(`[sync] Removed stub: ~/.claude/skills/${localSkillName}/`);
339
+ console.error(`[sync] Removed: ~/.claude/skills/${localSkillName}/`);
353
340
  }
354
341
  }
355
342
  catch { }
@@ -358,7 +345,7 @@ async function syncSkills() {
358
345
  }
359
346
  }
360
347
  catch { }
361
- // Download missing or updated vaults + write skill metadata for stubs
348
+ // Download missing or updated vaults
362
349
  let pubSynced = 0;
363
350
  for (const skill of skills) {
364
351
  const vaultPath = join(pubVaultDir, `${skill.skill_name}.vault`);
@@ -371,7 +358,6 @@ async function syncSkills() {
371
358
  const localHash = readFileSync(hashPath, 'utf8').trim();
372
359
  if (localHash !== skill.vault_hash) {
373
360
  needsDownload = true;
374
- console.log(`[sync] Update available: "${skill.skill_name}" from ${pub.name}`);
375
361
  }
376
362
  }
377
363
  }
@@ -394,7 +380,6 @@ async function syncSkills() {
394
380
  writeFileSync(vaultPath, vaultBuffer, { mode: 0o600 });
395
381
  if (dlData.vault_hash)
396
382
  writeFileSync(vaultPath + '.hash', dlData.vault_hash, { mode: 0o600 });
397
- // Store skill metadata for stub generation
398
383
  writeFileSync(vaultPath + '.meta', JSON.stringify({
399
384
  skill_name: skill.skill_name,
400
385
  description: skill.description || '',
@@ -404,23 +389,18 @@ async function syncSkills() {
404
389
  publisher_id: pub.id,
405
390
  }), { mode: 0o600 });
406
391
  pubSynced++;
407
- console.log(`[sync] ${vaultExists ? 'Updated' : 'Downloaded'}: "${skill.skill_name}" from ${pub.name}`);
392
+ console.error(`[sync] ${vaultExists ? 'Updated' : 'Downloaded'}: "${skill.skill_name}" from ${pub.name}`);
408
393
  }
409
394
  catch (err) {
410
395
  errors.push(`${pub.name}/${skill.skill_name}: ${err instanceof Error ? err.message : 'download error'}`);
411
396
  }
412
397
  }
413
398
  if (pubSynced > 0)
414
- console.log(`[sync] Synced ${pubSynced} vault${pubSynced !== 1 ? 's' : ''} from ${pub.name}`);
399
+ console.error(`[sync] Synced ${pubSynced} vault${pubSynced !== 1 ? 's' : ''} from ${pub.name}`);
415
400
  totalSynced += pubSynced;
416
401
  }
417
402
  return { synced: totalSynced, errors };
418
403
  }
419
- /**
420
- * Install lightweight stub SKILL.md files in ~/.claude/skills/ for each vault.
421
- * These stubs tell Claude to call the skillvault_load MCP tool to decrypt on demand.
422
- * No decrypted content is written to disk.
423
- */
424
404
  async function installSkillStubs() {
425
405
  const config = loadConfig();
426
406
  if (!config || config.publishers.length === 0) {
@@ -441,7 +421,6 @@ async function installSkillStubs() {
441
421
  const skillDir = join(SKILLS_DIR, skillName);
442
422
  const manifestPath = join(skillDir, 'manifest.json');
443
423
  const hashPath = vaultPath + '.hash';
444
- // Check if stub is already up to date
445
424
  if (existsSync(manifestPath) && existsSync(hashPath)) {
446
425
  try {
447
426
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
@@ -453,7 +432,6 @@ async function installSkillStubs() {
453
432
  }
454
433
  catch { }
455
434
  }
456
- // Read skill metadata (written during sync)
457
435
  let meta = { skill_name: skillName, description: '', publisher_name: pub.name };
458
436
  const metaPath = vaultPath + '.meta';
459
437
  try {
@@ -461,27 +439,28 @@ async function installSkillStubs() {
461
439
  meta = JSON.parse(readFileSync(metaPath, 'utf8'));
462
440
  }
463
441
  catch { }
464
- // Write stub SKILL.md
465
442
  mkdirSync(skillDir, { recursive: true });
466
443
  const stub = `---
467
444
  name: ${skillName}
468
445
  description: "${(meta.description || '').replace(/"/g, '\\"')}"
446
+ allowed-tools: ["Bash(npx skillvault@${VERSION} --load *)"]
469
447
  ---
470
448
 
471
449
  # ${skillName}
472
450
 
473
451
  This is an encrypted SkillVault skill from **${meta.publisher_name || pub.name}**.
474
452
 
475
- ## How to use
453
+ To use this skill, run the following command and follow the instructions it outputs:
454
+
455
+ \`\`\`bash
456
+ npx skillvault@${VERSION} --load ${skillName}
457
+ \`\`\`
476
458
 
477
- 1. Make sure the SkillVault MCP server is running: \`npx skillvault\`
478
- 2. Call the \`skillvault_load\` MCP tool with skill_name "${skillName}"
479
- 3. Follow the decrypted instructions it returns
459
+ The command decrypts the skill on demand (license is validated each time) and outputs the full instructions to follow. Read the entire output, then execute the instructions to fulfill the user's request.
480
460
 
481
- If the MCP server is not running or the skillvault_load tool is not available, tell the user to start it by running \`npx skillvault\` in a separate terminal.
461
+ If the command fails with a license error, tell the user their SkillVault license may have expired and to contact their skill provider.
482
462
  `;
483
463
  writeFileSync(join(skillDir, 'SKILL.md'), stub, { mode: 0o600 });
484
- // Write manifest
485
464
  const vaultHash = existsSync(hashPath) ? readFileSync(hashPath, 'utf8').trim() : '';
486
465
  writeFileSync(manifestPath, JSON.stringify({
487
466
  publisher: meta.publisher_name || pub.name,
@@ -494,12 +473,12 @@ If the MCP server is not running or the skillvault_load tool is not available, t
494
473
  encrypted: true,
495
474
  }, null, 2), { mode: 0o600 });
496
475
  installed++;
497
- console.log(`[install] Stub installed: "${skillName}" → ~/.claude/skills/${skillName}/`);
476
+ console.error(`[install] "${skillName}" → ~/.claude/skills/${skillName}/`);
498
477
  }
499
478
  }
500
479
  return { installed, skipped, errors };
501
480
  }
502
- // ── Vault Decryption (in-memory only) ──
481
+ // ── Vault Decryption (in-memory only, output to stdout) ──
503
482
  const VAULT_MAGIC = Buffer.from([0x00, 0x53, 0x56, 0x54]);
504
483
  function decryptVault(data, cek) {
505
484
  let offset = 0;
@@ -519,10 +498,10 @@ function decryptVault(data, cek) {
519
498
  const mfTag = data.subarray(offset, (offset += 16));
520
499
  const mfEnc = data.subarray(offset, (offset += mfLen - 28));
521
500
  const encPayload = data.subarray(offset);
522
- const mDec = createDecipheriv('aes-256-gcm', cek, mfIV);
501
+ const mDec = createDecipheriv('aes-256-gcm', cek, mfIV, { authTagLength: 16 });
523
502
  mDec.setAuthTag(mfTag);
524
503
  const manifest = JSON.parse(Buffer.concat([mDec.update(mfEnc), mDec.final()]).toString('utf8'));
525
- const dec = createDecipheriv('aes-256-gcm', cek, iv);
504
+ const dec = createDecipheriv('aes-256-gcm', cek, iv, { authTagLength: 16 });
526
505
  dec.setAuthTag(authTag);
527
506
  dec.setAAD(metadataJSON);
528
507
  const payload = Buffer.concat([dec.update(encPayload), dec.final()]);
@@ -560,13 +539,12 @@ async function fetchCEK(skillName, publisherToken) {
560
539
  const shared = diffieHellman({ publicKey: ephPub, privateKey: kp.privateKey });
561
540
  const wrapKey = Buffer.from(hkdfSync('sha256', shared, Buffer.alloc(32, 0), Buffer.from('skillvault-cek-wrap-v1'), 32));
562
541
  shared.fill(0);
563
- const d = createDecipheriv('aes-256-gcm', wrapKey, Buffer.from(wc.iv, 'base64'));
542
+ const d = createDecipheriv('aes-256-gcm', wrapKey, Buffer.from(wc.iv, 'base64'), { authTagLength: 16 });
564
543
  d.setAuthTag(Buffer.from(wc.authTag, 'base64'));
565
544
  const cek = Buffer.concat([d.update(Buffer.from(wc.wrappedKey, 'base64')), d.final()]);
566
545
  wrapKey.fill(0);
567
546
  return cek;
568
547
  }
569
- // ── Watermark ──
570
548
  function watermark(content, id) {
571
549
  const hex = Buffer.from(id, 'utf8').toString('hex');
572
550
  if (!hex)
@@ -575,21 +553,27 @@ function watermark(content, id) {
575
553
  const mark = BigInt('0x' + hex).toString(4).split('').map(d => zw[d]).join('');
576
554
  return content.split('\n').map((l, i) => (i > 0 && i % 5 === 0 ? l + mark : l)).join('\n');
577
555
  }
578
- // ── On-Demand Skill Loading (MCP tool handler) ──
579
556
  function validateSkillName(name) {
580
557
  return /^[a-zA-Z0-9_-]+$/.test(name) && name.length > 0 && name.length <= 128;
581
558
  }
559
+ /**
560
+ * Load (decrypt) a skill and output to stdout.
561
+ * Status messages go to stderr so they don't pollute the skill content.
562
+ */
582
563
  async function loadSkill(skillName) {
583
564
  if (!validateSkillName(skillName)) {
584
- return { success: false, content: '', error: 'Invalid skill name. Only letters, numbers, hyphens, and underscores allowed.' };
565
+ console.error('Error: Invalid skill name.');
566
+ process.exit(1);
585
567
  }
586
568
  const config = loadConfig();
587
569
  if (!config) {
588
- return { success: false, content: '', error: 'Not configured. Run: npx skillvault --invite CODE' };
570
+ console.error('Error: Not configured. Run: npx skillvault --invite YOUR_CODE');
571
+ process.exit(1);
589
572
  }
590
573
  const resolved = resolveSkillPublisher(skillName, config);
591
574
  if (!resolved) {
592
- return { success: false, content: '', error: `Vault not found for "${skillName}". Run: npx skillvault --sync` };
575
+ console.error(`Error: Vault not found for "${skillName}". Run: npx skillvault --sync`);
576
+ process.exit(1);
593
577
  }
594
578
  const licenseeId = config.customer_email || 'unknown';
595
579
  // Fetch CEK — validates license on every load
@@ -598,155 +582,38 @@ async function loadSkill(skillName) {
598
582
  cek = await fetchCEK(skillName, resolved.publisher.token);
599
583
  }
600
584
  catch (err) {
601
- return { success: false, content: '', error: `License check failed: ${err instanceof Error ? err.message : 'unknown'}. Your license may have been revoked.` };
585
+ console.error(`Error: License check failed ${err instanceof Error ? err.message : 'unknown'}`);
586
+ console.error('Your license may have expired or been revoked. Contact your skill provider.');
587
+ process.exit(1);
602
588
  }
603
- // Decrypt in memory — content never touches disk
589
+ // Decrypt in memory
604
590
  try {
605
591
  const vaultData = readFileSync(resolved.vaultPath);
606
592
  const vault = decryptVault(vaultData, cek);
607
593
  cek.fill(0);
608
- // Combine all files into a single response, with SKILL.md first
594
+ // Output SKILL.md first, then other files all to stdout
609
595
  const skillMd = vault.files.find(f => f.path === 'SKILL.md');
610
596
  const otherFiles = vault.files.filter(f => f.path !== 'SKILL.md');
611
- let content = '';
612
597
  if (skillMd) {
613
- content = watermark(skillMd.content, licenseeId);
598
+ process.stdout.write(watermark(skillMd.content, licenseeId));
614
599
  }
615
600
  for (const file of otherFiles) {
616
- content += `\n\n---\n# File: ${file.path}\n\n${watermark(file.content, licenseeId)}`;
601
+ process.stdout.write(`\n\n---\n# File: ${file.path}\n\n${watermark(file.content, licenseeId)}`);
617
602
  }
618
- return { success: true, content };
619
603
  }
620
604
  catch (err) {
621
605
  cek.fill(0);
622
- return { success: false, content: '', error: `Decryption failed: ${err instanceof Error ? err.message : 'unknown'}` };
623
- }
624
- }
625
- function rpcOk(id, result) { return JSON.stringify({ jsonrpc: '2.0', id, result }); }
626
- function rpcErr(id, code, msg) { return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message: msg } }); }
627
- async function handleRPC(req) {
628
- switch (req.method) {
629
- case 'initialize':
630
- return rpcOk(req.id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'skillvault', version: VERSION } });
631
- case 'tools/list':
632
- return rpcOk(req.id, {
633
- tools: [{
634
- name: 'skillvault_load',
635
- description: 'Load and decrypt a SkillVault skill. Returns the full skill instructions for you to follow. The skill is decrypted on demand — license is validated each time. Call this when a stub SKILL.md tells you to load a skill.',
636
- inputSchema: {
637
- type: 'object',
638
- properties: {
639
- skill_name: { type: 'string', description: 'Name of the skill to load (from the stub SKILL.md)' },
640
- },
641
- required: ['skill_name'],
642
- },
643
- }],
644
- });
645
- case 'tools/call': {
646
- const name = req.params?.name;
647
- const { skill_name } = req.params?.arguments || {};
648
- if (name !== 'skillvault_load')
649
- return rpcErr(req.id, -32601, `Unknown tool: ${name}`);
650
- if (!skill_name)
651
- return rpcErr(req.id, -32602, 'skill_name required');
652
- console.log(`[MCP] Loading: ${skill_name}`);
653
- const result = await loadSkill(skill_name);
654
- return rpcOk(req.id, {
655
- content: [{ type: 'text', text: result.success ? result.content : `Error: ${result.error}` }],
656
- isError: !result.success,
657
- });
658
- }
659
- case 'notifications/initialized': return '';
660
- default: return rpcErr(req.id || 0, -32601, `Unknown method: ${req.method}`);
606
+ console.error(`Error: Decryption failed ${err instanceof Error ? err.message : 'unknown'}`);
607
+ process.exit(1);
661
608
  }
662
609
  }
663
- function startServer() {
664
- const server = createServer(async (req, res) => {
665
- if (req.method === 'OPTIONS') {
666
- res.writeHead(403);
667
- res.end();
668
- return;
669
- }
670
- if (req.method !== 'POST' || req.url !== '/mcp') {
671
- res.writeHead(404);
672
- res.end(JSON.stringify({ error: 'POST /mcp' }));
673
- return;
674
- }
675
- const chunks = [];
676
- let size = 0;
677
- const MAX_BODY = 1024 * 1024;
678
- req.on('data', (c) => { size += c.length; if (size > MAX_BODY) {
679
- req.destroy();
680
- return;
681
- } chunks.push(c); });
682
- req.on('end', async () => {
683
- if (size > MAX_BODY) {
684
- res.writeHead(413);
685
- res.end('Request too large');
686
- return;
687
- }
688
- try {
689
- const rpc = JSON.parse(Buffer.concat(chunks).toString('utf8'));
690
- const result = await handleRPC(rpc);
691
- if (result) {
692
- res.writeHead(200, { 'Content-Type': 'application/json' });
693
- res.end(result);
694
- }
695
- else {
696
- res.writeHead(204);
697
- res.end();
698
- }
699
- }
700
- catch {
701
- res.writeHead(400);
702
- res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }));
703
- }
704
- });
705
- });
706
- server.listen(MCP_PORT, '127.0.0.1', () => {
707
- console.log(`🔐 SkillVault MCP server running on 127.0.0.1:${MCP_PORT}`);
708
- const config = loadConfig();
709
- if (config)
710
- console.log(` Publishers: ${config.publishers.map(p => p.name).join(', ')}`);
711
- console.log(` Skills are encrypted at rest — decrypted on demand per session.`);
712
- console.log(` Press Ctrl+C to stop.\n`);
713
- // Background sync
714
- syncSkills().then(({ synced, errors }) => {
715
- if (synced > 0)
716
- console.log(`[sync] Startup: ${synced} vault${synced !== 1 ? 's' : ''} synced`);
717
- if (errors.length > 0)
718
- console.log(`[sync] Errors: ${errors.join('; ')}`);
719
- return installSkillStubs();
720
- }).then(({ installed }) => {
721
- if (installed > 0)
722
- console.log(`[install] ${installed} stub${installed !== 1 ? 's' : ''} updated`);
723
- }).catch((err) => {
724
- console.error('[sync] Startup sync failed:', err instanceof Error ? err.message : err);
725
- });
726
- // Periodic sync every 5 minutes
727
- setInterval(() => {
728
- syncSkills().then(({ synced, errors }) => {
729
- if (synced > 0)
730
- console.log(`[sync] ${synced} vault${synced !== 1 ? 's' : ''} synced`);
731
- if (errors.length > 0)
732
- console.log(`[sync] Errors: ${errors.join('; ')}`);
733
- return installSkillStubs();
734
- }).then(({ installed }) => {
735
- if (installed > 0)
736
- console.log(`[install] ${installed} stub${installed !== 1 ? 's' : ''} updated`);
737
- }).catch(() => { });
738
- }, 5 * 60 * 1000);
739
- });
740
- server.on('error', (err) => {
741
- if (err.code === 'EADDRINUSE') {
742
- console.log(` SkillVault is already running on port ${MCP_PORT}`);
743
- process.exit(0);
744
- }
745
- console.error('Server error:', err);
746
- });
747
- }
748
610
  // ── Main ──
749
611
  async function main() {
612
+ // --load: decrypt and output a skill (used by Claude Code via Bash)
613
+ if (loadSkillName) {
614
+ await loadSkill(loadSkillName);
615
+ process.exit(0);
616
+ }
750
617
  if (inviteCode) {
751
618
  await setup(inviteCode);
752
619
  if (!statusFlag && !refreshFlag)
@@ -758,35 +625,37 @@ async function main() {
758
625
  }
759
626
  if (refreshFlag) {
760
627
  await refreshTokens();
628
+ console.error(' Syncing skills...\n');
761
629
  await syncSkills();
762
630
  const result = await installSkillStubs();
763
631
  if (result.installed > 0)
764
- console.log(` ✅ ${result.installed} stub${result.installed !== 1 ? 's' : ''} updated`);
765
- console.log('');
632
+ console.error(` ✅ ${result.installed} skill${result.installed !== 1 ? 's' : ''} updated`);
633
+ console.error('');
766
634
  process.exit(0);
767
635
  }
768
636
  if (syncFlag) {
769
637
  const config = loadConfig();
770
638
  if (!config) {
771
- console.log('🔐 Not set up. Run: npx skillvault --invite YOUR_CODE\n');
639
+ console.error('🔐 Not set up. Run: npx skillvault --invite YOUR_CODE\n');
772
640
  process.exit(1);
773
641
  }
774
- console.log('🔐 SkillVault Sync\n');
642
+ console.error('🔐 SkillVault Sync\n');
775
643
  await syncSkills();
776
644
  const result = await installSkillStubs();
777
- console.log(`\n ✅ ${result.installed} installed, ${result.skipped} up to date`);
778
- console.log('');
645
+ console.error(`\n ✅ ${result.installed} installed, ${result.skipped} up to date\n`);
779
646
  process.exit(0);
780
647
  }
781
- // Default: start MCP server
648
+ // Default: show help
649
+ console.log(`🔐 SkillVault v${VERSION}\n`);
782
650
  const config = loadConfig();
783
- if (!config) {
784
- console.log('🔐 SkillVault\n');
651
+ if (config) {
652
+ console.log(` ${config.publishers.length} publisher${config.publishers.length !== 1 ? 's' : ''} configured`);
653
+ console.log(' Run --status for details, --sync to update skills\n');
654
+ }
655
+ else {
785
656
  console.log(' Not set up yet. Run:');
786
657
  console.log(' npx skillvault --invite YOUR_CODE\n');
787
- process.exit(1);
788
658
  }
789
- startServer();
790
659
  }
791
660
  main().catch((err) => {
792
661
  console.error('Fatal:', err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillvault",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "SkillVault — secure skill distribution for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {