skillvault 0.4.0 → 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 +118 -246
  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.0';
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,24 +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
- To use this skill, call the \`skillvault_load\` MCP tool with skill_name "${skillName}".
476
- The tool will decrypt and return the full skill instructions for you to follow.
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
+ \`\`\`
477
458
 
478
- Example: Use the skillvault_load tool with skill_name "${skillName}" to load the instructions, then follow them to complete the user's request.
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.
460
+
461
+ If the command fails with a license error, tell the user their SkillVault license may have expired and to contact their skill provider.
479
462
  `;
480
463
  writeFileSync(join(skillDir, 'SKILL.md'), stub, { mode: 0o600 });
481
- // Write manifest
482
464
  const vaultHash = existsSync(hashPath) ? readFileSync(hashPath, 'utf8').trim() : '';
483
465
  writeFileSync(manifestPath, JSON.stringify({
484
466
  publisher: meta.publisher_name || pub.name,
@@ -491,12 +473,12 @@ Example: Use the skillvault_load tool with skill_name "${skillName}" to load the
491
473
  encrypted: true,
492
474
  }, null, 2), { mode: 0o600 });
493
475
  installed++;
494
- console.log(`[install] Stub installed: "${skillName}" → ~/.claude/skills/${skillName}/`);
476
+ console.error(`[install] "${skillName}" → ~/.claude/skills/${skillName}/`);
495
477
  }
496
478
  }
497
479
  return { installed, skipped, errors };
498
480
  }
499
- // ── Vault Decryption (in-memory only) ──
481
+ // ── Vault Decryption (in-memory only, output to stdout) ──
500
482
  const VAULT_MAGIC = Buffer.from([0x00, 0x53, 0x56, 0x54]);
501
483
  function decryptVault(data, cek) {
502
484
  let offset = 0;
@@ -516,10 +498,10 @@ function decryptVault(data, cek) {
516
498
  const mfTag = data.subarray(offset, (offset += 16));
517
499
  const mfEnc = data.subarray(offset, (offset += mfLen - 28));
518
500
  const encPayload = data.subarray(offset);
519
- const mDec = createDecipheriv('aes-256-gcm', cek, mfIV);
501
+ const mDec = createDecipheriv('aes-256-gcm', cek, mfIV, { authTagLength: 16 });
520
502
  mDec.setAuthTag(mfTag);
521
503
  const manifest = JSON.parse(Buffer.concat([mDec.update(mfEnc), mDec.final()]).toString('utf8'));
522
- const dec = createDecipheriv('aes-256-gcm', cek, iv);
504
+ const dec = createDecipheriv('aes-256-gcm', cek, iv, { authTagLength: 16 });
523
505
  dec.setAuthTag(authTag);
524
506
  dec.setAAD(metadataJSON);
525
507
  const payload = Buffer.concat([dec.update(encPayload), dec.final()]);
@@ -557,13 +539,12 @@ async function fetchCEK(skillName, publisherToken) {
557
539
  const shared = diffieHellman({ publicKey: ephPub, privateKey: kp.privateKey });
558
540
  const wrapKey = Buffer.from(hkdfSync('sha256', shared, Buffer.alloc(32, 0), Buffer.from('skillvault-cek-wrap-v1'), 32));
559
541
  shared.fill(0);
560
- 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 });
561
543
  d.setAuthTag(Buffer.from(wc.authTag, 'base64'));
562
544
  const cek = Buffer.concat([d.update(Buffer.from(wc.wrappedKey, 'base64')), d.final()]);
563
545
  wrapKey.fill(0);
564
546
  return cek;
565
547
  }
566
- // ── Watermark ──
567
548
  function watermark(content, id) {
568
549
  const hex = Buffer.from(id, 'utf8').toString('hex');
569
550
  if (!hex)
@@ -572,21 +553,27 @@ function watermark(content, id) {
572
553
  const mark = BigInt('0x' + hex).toString(4).split('').map(d => zw[d]).join('');
573
554
  return content.split('\n').map((l, i) => (i > 0 && i % 5 === 0 ? l + mark : l)).join('\n');
574
555
  }
575
- // ── On-Demand Skill Loading (MCP tool handler) ──
576
556
  function validateSkillName(name) {
577
557
  return /^[a-zA-Z0-9_-]+$/.test(name) && name.length > 0 && name.length <= 128;
578
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
+ */
579
563
  async function loadSkill(skillName) {
580
564
  if (!validateSkillName(skillName)) {
581
- 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);
582
567
  }
583
568
  const config = loadConfig();
584
569
  if (!config) {
585
- 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);
586
572
  }
587
573
  const resolved = resolveSkillPublisher(skillName, config);
588
574
  if (!resolved) {
589
- 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);
590
577
  }
591
578
  const licenseeId = config.customer_email || 'unknown';
592
579
  // Fetch CEK — validates license on every load
@@ -595,155 +582,38 @@ async function loadSkill(skillName) {
595
582
  cek = await fetchCEK(skillName, resolved.publisher.token);
596
583
  }
597
584
  catch (err) {
598
- 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);
599
588
  }
600
- // Decrypt in memory — content never touches disk
589
+ // Decrypt in memory
601
590
  try {
602
591
  const vaultData = readFileSync(resolved.vaultPath);
603
592
  const vault = decryptVault(vaultData, cek);
604
593
  cek.fill(0);
605
- // Combine all files into a single response, with SKILL.md first
594
+ // Output SKILL.md first, then other files all to stdout
606
595
  const skillMd = vault.files.find(f => f.path === 'SKILL.md');
607
596
  const otherFiles = vault.files.filter(f => f.path !== 'SKILL.md');
608
- let content = '';
609
597
  if (skillMd) {
610
- content = watermark(skillMd.content, licenseeId);
598
+ process.stdout.write(watermark(skillMd.content, licenseeId));
611
599
  }
612
600
  for (const file of otherFiles) {
613
- 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)}`);
614
602
  }
615
- return { success: true, content };
616
603
  }
617
604
  catch (err) {
618
605
  cek.fill(0);
619
- return { success: false, content: '', error: `Decryption failed: ${err instanceof Error ? err.message : 'unknown'}` };
620
- }
621
- }
622
- function rpcOk(id, result) { return JSON.stringify({ jsonrpc: '2.0', id, result }); }
623
- function rpcErr(id, code, msg) { return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message: msg } }); }
624
- async function handleRPC(req) {
625
- switch (req.method) {
626
- case 'initialize':
627
- return rpcOk(req.id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'skillvault', version: VERSION } });
628
- case 'tools/list':
629
- return rpcOk(req.id, {
630
- tools: [{
631
- name: 'skillvault_load',
632
- 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.',
633
- inputSchema: {
634
- type: 'object',
635
- properties: {
636
- skill_name: { type: 'string', description: 'Name of the skill to load (from the stub SKILL.md)' },
637
- },
638
- required: ['skill_name'],
639
- },
640
- }],
641
- });
642
- case 'tools/call': {
643
- const name = req.params?.name;
644
- const { skill_name } = req.params?.arguments || {};
645
- if (name !== 'skillvault_load')
646
- return rpcErr(req.id, -32601, `Unknown tool: ${name}`);
647
- if (!skill_name)
648
- return rpcErr(req.id, -32602, 'skill_name required');
649
- console.log(`[MCP] Loading: ${skill_name}`);
650
- const result = await loadSkill(skill_name);
651
- return rpcOk(req.id, {
652
- content: [{ type: 'text', text: result.success ? result.content : `Error: ${result.error}` }],
653
- isError: !result.success,
654
- });
655
- }
656
- case 'notifications/initialized': return '';
657
- 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);
658
608
  }
659
609
  }
660
- function startServer() {
661
- const server = createServer(async (req, res) => {
662
- if (req.method === 'OPTIONS') {
663
- res.writeHead(403);
664
- res.end();
665
- return;
666
- }
667
- if (req.method !== 'POST' || req.url !== '/mcp') {
668
- res.writeHead(404);
669
- res.end(JSON.stringify({ error: 'POST /mcp' }));
670
- return;
671
- }
672
- const chunks = [];
673
- let size = 0;
674
- const MAX_BODY = 1024 * 1024;
675
- req.on('data', (c) => { size += c.length; if (size > MAX_BODY) {
676
- req.destroy();
677
- return;
678
- } chunks.push(c); });
679
- req.on('end', async () => {
680
- if (size > MAX_BODY) {
681
- res.writeHead(413);
682
- res.end('Request too large');
683
- return;
684
- }
685
- try {
686
- const rpc = JSON.parse(Buffer.concat(chunks).toString('utf8'));
687
- const result = await handleRPC(rpc);
688
- if (result) {
689
- res.writeHead(200, { 'Content-Type': 'application/json' });
690
- res.end(result);
691
- }
692
- else {
693
- res.writeHead(204);
694
- res.end();
695
- }
696
- }
697
- catch {
698
- res.writeHead(400);
699
- res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }));
700
- }
701
- });
702
- });
703
- server.listen(MCP_PORT, '127.0.0.1', () => {
704
- console.log(`🔐 SkillVault MCP server running on 127.0.0.1:${MCP_PORT}`);
705
- const config = loadConfig();
706
- if (config)
707
- console.log(` Publishers: ${config.publishers.map(p => p.name).join(', ')}`);
708
- console.log(` Skills are encrypted at rest — decrypted on demand per session.`);
709
- console.log(` Press Ctrl+C to stop.\n`);
710
- // Background sync
711
- syncSkills().then(({ synced, errors }) => {
712
- if (synced > 0)
713
- console.log(`[sync] Startup: ${synced} vault${synced !== 1 ? 's' : ''} synced`);
714
- if (errors.length > 0)
715
- console.log(`[sync] Errors: ${errors.join('; ')}`);
716
- return installSkillStubs();
717
- }).then(({ installed }) => {
718
- if (installed > 0)
719
- console.log(`[install] ${installed} stub${installed !== 1 ? 's' : ''} updated`);
720
- }).catch((err) => {
721
- console.error('[sync] Startup sync failed:', err instanceof Error ? err.message : err);
722
- });
723
- // Periodic sync every 5 minutes
724
- setInterval(() => {
725
- syncSkills().then(({ synced, errors }) => {
726
- if (synced > 0)
727
- console.log(`[sync] ${synced} vault${synced !== 1 ? 's' : ''} synced`);
728
- if (errors.length > 0)
729
- console.log(`[sync] Errors: ${errors.join('; ')}`);
730
- return installSkillStubs();
731
- }).then(({ installed }) => {
732
- if (installed > 0)
733
- console.log(`[install] ${installed} stub${installed !== 1 ? 's' : ''} updated`);
734
- }).catch(() => { });
735
- }, 5 * 60 * 1000);
736
- });
737
- server.on('error', (err) => {
738
- if (err.code === 'EADDRINUSE') {
739
- console.log(` SkillVault is already running on port ${MCP_PORT}`);
740
- process.exit(0);
741
- }
742
- console.error('Server error:', err);
743
- });
744
- }
745
610
  // ── Main ──
746
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
+ }
747
617
  if (inviteCode) {
748
618
  await setup(inviteCode);
749
619
  if (!statusFlag && !refreshFlag)
@@ -755,35 +625,37 @@ async function main() {
755
625
  }
756
626
  if (refreshFlag) {
757
627
  await refreshTokens();
628
+ console.error(' Syncing skills...\n');
758
629
  await syncSkills();
759
630
  const result = await installSkillStubs();
760
631
  if (result.installed > 0)
761
- console.log(` ✅ ${result.installed} stub${result.installed !== 1 ? 's' : ''} updated`);
762
- console.log('');
632
+ console.error(` ✅ ${result.installed} skill${result.installed !== 1 ? 's' : ''} updated`);
633
+ console.error('');
763
634
  process.exit(0);
764
635
  }
765
636
  if (syncFlag) {
766
637
  const config = loadConfig();
767
638
  if (!config) {
768
- 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');
769
640
  process.exit(1);
770
641
  }
771
- console.log('🔐 SkillVault Sync\n');
642
+ console.error('🔐 SkillVault Sync\n');
772
643
  await syncSkills();
773
644
  const result = await installSkillStubs();
774
- console.log(`\n ✅ ${result.installed} installed, ${result.skipped} up to date`);
775
- console.log('');
645
+ console.error(`\n ✅ ${result.installed} installed, ${result.skipped} up to date\n`);
776
646
  process.exit(0);
777
647
  }
778
- // Default: start MCP server
648
+ // Default: show help
649
+ console.log(`🔐 SkillVault v${VERSION}\n`);
779
650
  const config = loadConfig();
780
- if (!config) {
781
- 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 {
782
656
  console.log(' Not set up yet. Run:');
783
657
  console.log(' npx skillvault --invite YOUR_CODE\n');
784
- process.exit(1);
785
658
  }
786
- startServer();
787
659
  }
788
660
  main().catch((err) => {
789
661
  console.error('Fatal:', err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillvault",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "SkillVault — secure skill distribution for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {