skillvault 0.4.1 → 0.5.1

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 +155 -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.1';
23
+ const VERSION = '0.5.1';
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,79 @@ 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();
154
+ // Install session hook for auto-sync
155
+ configureSessionHook();
155
156
  // Sync vaults and install stubs
156
- console.log('');
157
- console.log(' Syncing skills...');
157
+ console.error('');
158
+ console.error(' Syncing skills...');
158
159
  await syncSkills();
159
160
  const installResult = await installSkillStubs();
160
- console.log('');
161
+ console.error('');
161
162
  if (installResult.installed > 0) {
162
- console.log(` ✅ ${installResult.installed} skill${installResult.installed !== 1 ? 's' : ''} ready`);
163
+ console.error(` ✅ ${installResult.installed} skill${installResult.installed !== 1 ? 's' : ''} installed to ~/.claude/skills/`);
163
164
  }
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('');
165
+ console.error(' Setup complete! Restart Claude Code to use your skills.');
166
+ console.error(' Skills will auto-sync at the start of each Claude Code session.');
167
+ console.error('');
167
168
  }
168
- /**
169
- * Configure Claude Code to connect to the SkillVault MCP server.
170
- */
171
- function configureMCP() {
169
+ /** Remove legacy MCP server config from ~/.claude/.mcp.json */
170
+ function cleanupMCPConfig() {
172
171
  const mcpConfigPath = join(HOME, '.claude', '.mcp.json');
173
172
  try {
174
- let mcpConfig = {};
175
173
  if (existsSync(mcpConfigPath)) {
176
- mcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf8'));
174
+ const mcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf8'));
175
+ if (mcpConfig.mcpServers?.skillvault) {
176
+ delete mcpConfig.mcpServers.skillvault;
177
+ writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), { mode: 0o600 });
178
+ console.error(' 🧹 Removed legacy MCP config');
179
+ }
180
+ }
181
+ }
182
+ catch { }
183
+ }
184
+ /**
185
+ * Install a SessionStart hook in Claude Code settings so skills auto-sync
186
+ * at the start of each session. This discovers new skills from all
187
+ * existing publishers without the customer doing anything.
188
+ */
189
+ function configureSessionHook() {
190
+ const settingsPath = join(HOME, '.claude', 'settings.json');
191
+ try {
192
+ let settings = {};
193
+ if (existsSync(settingsPath)) {
194
+ settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
195
+ }
196
+ if (!settings.hooks)
197
+ settings.hooks = {};
198
+ if (!settings.hooks.SessionStart)
199
+ settings.hooks.SessionStart = [];
200
+ // Check if we already have a skillvault sync hook
201
+ const hasHook = settings.hooks.SessionStart.some((group) => group.matcher === 'startup' &&
202
+ group.hooks?.some((h) => h.command?.includes('skillvault')));
203
+ if (!hasHook) {
204
+ settings.hooks.SessionStart.push({
205
+ matcher: 'startup',
206
+ hooks: [{
207
+ type: 'command',
208
+ command: `npx skillvault@${VERSION} --sync`,
209
+ timeout: 30,
210
+ }],
211
+ });
212
+ mkdirSync(join(HOME, '.claude'), { recursive: true });
213
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
214
+ console.error(' ✅ Auto-sync hook installed');
177
215
  }
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
216
  }
188
217
  catch {
189
- console.error(` ⚠️ Failed to configure MCPmanually add to ~/.claude/.mcp.json`);
218
+ console.error(' ⚠️ Could not install auto-sync hook run npx skillvault --sync manually');
190
219
  }
191
220
  }
192
221
  async function showStatus() {
193
222
  const config = loadConfig();
194
223
  if (!config) {
195
- console.log('🔐 SkillVault Agent\n');
224
+ console.log('🔐 SkillVault\n');
196
225
  console.log(' Not set up yet. Run:');
197
226
  console.log(' npx skillvault --invite YOUR_CODE\n');
198
227
  process.exit(1);
@@ -201,7 +230,7 @@ async function showStatus() {
201
230
  if (config.customer_email)
202
231
  console.log(` Account: ${config.customer_email}`);
203
232
  console.log(` Config: ${CONFIG_PATH}`);
204
- console.log(` Skills: ${SKILLS_DIR} (stubs — encrypted at rest)`);
233
+ console.log(` Skills: ${SKILLS_DIR}`);
205
234
  console.log(` Server: ${config.api_url}`);
206
235
  console.log('');
207
236
  let skills = [];
@@ -267,15 +296,13 @@ async function showStatus() {
267
296
  async function refreshTokens() {
268
297
  const config = loadConfig();
269
298
  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');
299
+ console.error('🔐 SkillVault\n Not set up. Run: npx skillvault --invite YOUR_CODE\n');
273
300
  process.exit(1);
274
301
  }
275
- console.log('🔐 SkillVault Token Refresh\n');
302
+ console.error('🔐 SkillVault Token Refresh\n');
276
303
  let anyRefreshed = false;
277
304
  for (const pub of config.publishers) {
278
- process.stdout.write(` Refreshing ${pub.name}... `);
305
+ process.stderr.write(` Refreshing ${pub.name}... `);
279
306
  try {
280
307
  const res = await fetch(`${config.api_url}/auth/companion/refresh`, {
281
308
  method: 'POST',
@@ -287,26 +314,26 @@ async function refreshTokens() {
287
314
  pub.token = data.token;
288
315
  if (data.customer_token)
289
316
  config.customer_token = data.customer_token;
290
- console.log('✅ refreshed');
317
+ console.error('✅');
291
318
  anyRefreshed = true;
292
319
  }
293
320
  else if (res.status === 401) {
294
- console.log('❌ expired — re-invite required');
321
+ console.error('❌ expired — re-invite required');
295
322
  }
296
323
  else {
297
- console.log(`❌ server error (${res.status})`);
324
+ console.error(`❌ server error (${res.status})`);
298
325
  }
299
326
  }
300
327
  catch {
301
- console.log('❌ offline');
328
+ console.error('❌ offline');
302
329
  }
303
330
  }
304
331
  if (anyRefreshed) {
305
332
  saveConfig(config);
306
- console.log('\n Tokens updated.\n');
333
+ console.error('\n Tokens updated.\n');
307
334
  }
308
335
  else {
309
- console.log('\n No tokens refreshed. You may need to run: npx skillvault --invite CODE\n');
336
+ console.error('\n No tokens refreshed.\n');
310
337
  }
311
338
  }
312
339
  async function syncSkills() {
@@ -337,19 +364,19 @@ async function syncSkills() {
337
364
  continue;
338
365
  }
339
366
  const remoteSkillNames = new Set(skills.map(s => s.skill_name));
340
- // Revocation: remove stubs for skills no longer in the remote list
367
+ // Revocation: remove stubs for skills no longer in remote list
341
368
  try {
342
369
  if (existsSync(pubVaultDir)) {
343
370
  const localVaults = readdirSync(pubVaultDir).filter(f => f.endsWith('.vault'));
344
371
  for (const vaultFile of localVaults) {
345
372
  const localSkillName = vaultFile.replace(/\.vault$/, '');
346
373
  if (!remoteSkillNames.has(localSkillName)) {
347
- console.log(`[sync] Grant revoked: "${localSkillName}" from ${pub.name}`);
374
+ console.error(`[sync] Grant revoked: "${localSkillName}" from ${pub.name}`);
348
375
  const skillDir = join(SKILLS_DIR, localSkillName);
349
376
  try {
350
377
  if (existsSync(skillDir) && existsSync(join(skillDir, 'manifest.json'))) {
351
378
  rmSync(skillDir, { recursive: true, force: true });
352
- console.log(`[sync] Removed stub: ~/.claude/skills/${localSkillName}/`);
379
+ console.error(`[sync] Removed: ~/.claude/skills/${localSkillName}/`);
353
380
  }
354
381
  }
355
382
  catch { }
@@ -358,7 +385,7 @@ async function syncSkills() {
358
385
  }
359
386
  }
360
387
  catch { }
361
- // Download missing or updated vaults + write skill metadata for stubs
388
+ // Download missing or updated vaults
362
389
  let pubSynced = 0;
363
390
  for (const skill of skills) {
364
391
  const vaultPath = join(pubVaultDir, `${skill.skill_name}.vault`);
@@ -371,7 +398,6 @@ async function syncSkills() {
371
398
  const localHash = readFileSync(hashPath, 'utf8').trim();
372
399
  if (localHash !== skill.vault_hash) {
373
400
  needsDownload = true;
374
- console.log(`[sync] Update available: "${skill.skill_name}" from ${pub.name}`);
375
401
  }
376
402
  }
377
403
  }
@@ -394,7 +420,6 @@ async function syncSkills() {
394
420
  writeFileSync(vaultPath, vaultBuffer, { mode: 0o600 });
395
421
  if (dlData.vault_hash)
396
422
  writeFileSync(vaultPath + '.hash', dlData.vault_hash, { mode: 0o600 });
397
- // Store skill metadata for stub generation
398
423
  writeFileSync(vaultPath + '.meta', JSON.stringify({
399
424
  skill_name: skill.skill_name,
400
425
  description: skill.description || '',
@@ -404,23 +429,18 @@ async function syncSkills() {
404
429
  publisher_id: pub.id,
405
430
  }), { mode: 0o600 });
406
431
  pubSynced++;
407
- console.log(`[sync] ${vaultExists ? 'Updated' : 'Downloaded'}: "${skill.skill_name}" from ${pub.name}`);
432
+ console.error(`[sync] ${vaultExists ? 'Updated' : 'Downloaded'}: "${skill.skill_name}" from ${pub.name}`);
408
433
  }
409
434
  catch (err) {
410
435
  errors.push(`${pub.name}/${skill.skill_name}: ${err instanceof Error ? err.message : 'download error'}`);
411
436
  }
412
437
  }
413
438
  if (pubSynced > 0)
414
- console.log(`[sync] Synced ${pubSynced} vault${pubSynced !== 1 ? 's' : ''} from ${pub.name}`);
439
+ console.error(`[sync] Synced ${pubSynced} vault${pubSynced !== 1 ? 's' : ''} from ${pub.name}`);
415
440
  totalSynced += pubSynced;
416
441
  }
417
442
  return { synced: totalSynced, errors };
418
443
  }
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
444
  async function installSkillStubs() {
425
445
  const config = loadConfig();
426
446
  if (!config || config.publishers.length === 0) {
@@ -441,7 +461,6 @@ async function installSkillStubs() {
441
461
  const skillDir = join(SKILLS_DIR, skillName);
442
462
  const manifestPath = join(skillDir, 'manifest.json');
443
463
  const hashPath = vaultPath + '.hash';
444
- // Check if stub is already up to date
445
464
  if (existsSync(manifestPath) && existsSync(hashPath)) {
446
465
  try {
447
466
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
@@ -453,7 +472,6 @@ async function installSkillStubs() {
453
472
  }
454
473
  catch { }
455
474
  }
456
- // Read skill metadata (written during sync)
457
475
  let meta = { skill_name: skillName, description: '', publisher_name: pub.name };
458
476
  const metaPath = vaultPath + '.meta';
459
477
  try {
@@ -461,27 +479,28 @@ async function installSkillStubs() {
461
479
  meta = JSON.parse(readFileSync(metaPath, 'utf8'));
462
480
  }
463
481
  catch { }
464
- // Write stub SKILL.md
465
482
  mkdirSync(skillDir, { recursive: true });
466
483
  const stub = `---
467
484
  name: ${skillName}
468
485
  description: "${(meta.description || '').replace(/"/g, '\\"')}"
486
+ allowed-tools: ["Bash(npx skillvault@${VERSION} --load *)"]
469
487
  ---
470
488
 
471
489
  # ${skillName}
472
490
 
473
491
  This is an encrypted SkillVault skill from **${meta.publisher_name || pub.name}**.
474
492
 
475
- ## How to use
493
+ To use this skill, run the following command and follow the instructions it outputs:
476
494
 
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
495
+ \`\`\`bash
496
+ npx skillvault@${VERSION} --load ${skillName}
497
+ \`\`\`
480
498
 
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.
499
+ 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.
500
+
501
+ If the command fails with a license error, tell the user their SkillVault license may have expired and to contact their skill provider.
482
502
  `;
483
503
  writeFileSync(join(skillDir, 'SKILL.md'), stub, { mode: 0o600 });
484
- // Write manifest
485
504
  const vaultHash = existsSync(hashPath) ? readFileSync(hashPath, 'utf8').trim() : '';
486
505
  writeFileSync(manifestPath, JSON.stringify({
487
506
  publisher: meta.publisher_name || pub.name,
@@ -494,12 +513,12 @@ If the MCP server is not running or the skillvault_load tool is not available, t
494
513
  encrypted: true,
495
514
  }, null, 2), { mode: 0o600 });
496
515
  installed++;
497
- console.log(`[install] Stub installed: "${skillName}" → ~/.claude/skills/${skillName}/`);
516
+ console.error(`[install] "${skillName}" → ~/.claude/skills/${skillName}/`);
498
517
  }
499
518
  }
500
519
  return { installed, skipped, errors };
501
520
  }
502
- // ── Vault Decryption (in-memory only) ──
521
+ // ── Vault Decryption (in-memory only, output to stdout) ──
503
522
  const VAULT_MAGIC = Buffer.from([0x00, 0x53, 0x56, 0x54]);
504
523
  function decryptVault(data, cek) {
505
524
  let offset = 0;
@@ -519,10 +538,10 @@ function decryptVault(data, cek) {
519
538
  const mfTag = data.subarray(offset, (offset += 16));
520
539
  const mfEnc = data.subarray(offset, (offset += mfLen - 28));
521
540
  const encPayload = data.subarray(offset);
522
- const mDec = createDecipheriv('aes-256-gcm', cek, mfIV);
541
+ const mDec = createDecipheriv('aes-256-gcm', cek, mfIV, { authTagLength: 16 });
523
542
  mDec.setAuthTag(mfTag);
524
543
  const manifest = JSON.parse(Buffer.concat([mDec.update(mfEnc), mDec.final()]).toString('utf8'));
525
- const dec = createDecipheriv('aes-256-gcm', cek, iv);
544
+ const dec = createDecipheriv('aes-256-gcm', cek, iv, { authTagLength: 16 });
526
545
  dec.setAuthTag(authTag);
527
546
  dec.setAAD(metadataJSON);
528
547
  const payload = Buffer.concat([dec.update(encPayload), dec.final()]);
@@ -560,13 +579,12 @@ async function fetchCEK(skillName, publisherToken) {
560
579
  const shared = diffieHellman({ publicKey: ephPub, privateKey: kp.privateKey });
561
580
  const wrapKey = Buffer.from(hkdfSync('sha256', shared, Buffer.alloc(32, 0), Buffer.from('skillvault-cek-wrap-v1'), 32));
562
581
  shared.fill(0);
563
- const d = createDecipheriv('aes-256-gcm', wrapKey, Buffer.from(wc.iv, 'base64'));
582
+ const d = createDecipheriv('aes-256-gcm', wrapKey, Buffer.from(wc.iv, 'base64'), { authTagLength: 16 });
564
583
  d.setAuthTag(Buffer.from(wc.authTag, 'base64'));
565
584
  const cek = Buffer.concat([d.update(Buffer.from(wc.wrappedKey, 'base64')), d.final()]);
566
585
  wrapKey.fill(0);
567
586
  return cek;
568
587
  }
569
- // ── Watermark ──
570
588
  function watermark(content, id) {
571
589
  const hex = Buffer.from(id, 'utf8').toString('hex');
572
590
  if (!hex)
@@ -575,21 +593,27 @@ function watermark(content, id) {
575
593
  const mark = BigInt('0x' + hex).toString(4).split('').map(d => zw[d]).join('');
576
594
  return content.split('\n').map((l, i) => (i > 0 && i % 5 === 0 ? l + mark : l)).join('\n');
577
595
  }
578
- // ── On-Demand Skill Loading (MCP tool handler) ──
579
596
  function validateSkillName(name) {
580
597
  return /^[a-zA-Z0-9_-]+$/.test(name) && name.length > 0 && name.length <= 128;
581
598
  }
599
+ /**
600
+ * Load (decrypt) a skill and output to stdout.
601
+ * Status messages go to stderr so they don't pollute the skill content.
602
+ */
582
603
  async function loadSkill(skillName) {
583
604
  if (!validateSkillName(skillName)) {
584
- return { success: false, content: '', error: 'Invalid skill name. Only letters, numbers, hyphens, and underscores allowed.' };
605
+ console.error('Error: Invalid skill name.');
606
+ process.exit(1);
585
607
  }
586
608
  const config = loadConfig();
587
609
  if (!config) {
588
- return { success: false, content: '', error: 'Not configured. Run: npx skillvault --invite CODE' };
610
+ console.error('Error: Not configured. Run: npx skillvault --invite YOUR_CODE');
611
+ process.exit(1);
589
612
  }
590
613
  const resolved = resolveSkillPublisher(skillName, config);
591
614
  if (!resolved) {
592
- return { success: false, content: '', error: `Vault not found for "${skillName}". Run: npx skillvault --sync` };
615
+ console.error(`Error: Vault not found for "${skillName}". Run: npx skillvault --sync`);
616
+ process.exit(1);
593
617
  }
594
618
  const licenseeId = config.customer_email || 'unknown';
595
619
  // Fetch CEK — validates license on every load
@@ -598,155 +622,38 @@ async function loadSkill(skillName) {
598
622
  cek = await fetchCEK(skillName, resolved.publisher.token);
599
623
  }
600
624
  catch (err) {
601
- return { success: false, content: '', error: `License check failed: ${err instanceof Error ? err.message : 'unknown'}. Your license may have been revoked.` };
625
+ console.error(`Error: License check failed ${err instanceof Error ? err.message : 'unknown'}`);
626
+ console.error('Your license may have expired or been revoked. Contact your skill provider.');
627
+ process.exit(1);
602
628
  }
603
- // Decrypt in memory — content never touches disk
629
+ // Decrypt in memory
604
630
  try {
605
631
  const vaultData = readFileSync(resolved.vaultPath);
606
632
  const vault = decryptVault(vaultData, cek);
607
633
  cek.fill(0);
608
- // Combine all files into a single response, with SKILL.md first
634
+ // Output SKILL.md first, then other files all to stdout
609
635
  const skillMd = vault.files.find(f => f.path === 'SKILL.md');
610
636
  const otherFiles = vault.files.filter(f => f.path !== 'SKILL.md');
611
- let content = '';
612
637
  if (skillMd) {
613
- content = watermark(skillMd.content, licenseeId);
638
+ process.stdout.write(watermark(skillMd.content, licenseeId));
614
639
  }
615
640
  for (const file of otherFiles) {
616
- content += `\n\n---\n# File: ${file.path}\n\n${watermark(file.content, licenseeId)}`;
641
+ process.stdout.write(`\n\n---\n# File: ${file.path}\n\n${watermark(file.content, licenseeId)}`);
617
642
  }
618
- return { success: true, content };
619
643
  }
620
644
  catch (err) {
621
645
  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}`);
646
+ console.error(`Error: Decryption failed ${err instanceof Error ? err.message : 'unknown'}`);
647
+ process.exit(1);
661
648
  }
662
649
  }
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
650
  // ── Main ──
749
651
  async function main() {
652
+ // --load: decrypt and output a skill (used by Claude Code via Bash)
653
+ if (loadSkillName) {
654
+ await loadSkill(loadSkillName);
655
+ process.exit(0);
656
+ }
750
657
  if (inviteCode) {
751
658
  await setup(inviteCode);
752
659
  if (!statusFlag && !refreshFlag)
@@ -758,35 +665,37 @@ async function main() {
758
665
  }
759
666
  if (refreshFlag) {
760
667
  await refreshTokens();
668
+ console.error(' Syncing skills...\n');
761
669
  await syncSkills();
762
670
  const result = await installSkillStubs();
763
671
  if (result.installed > 0)
764
- console.log(` ✅ ${result.installed} stub${result.installed !== 1 ? 's' : ''} updated`);
765
- console.log('');
672
+ console.error(` ✅ ${result.installed} skill${result.installed !== 1 ? 's' : ''} updated`);
673
+ console.error('');
766
674
  process.exit(0);
767
675
  }
768
676
  if (syncFlag) {
769
677
  const config = loadConfig();
770
678
  if (!config) {
771
- console.log('🔐 Not set up. Run: npx skillvault --invite YOUR_CODE\n');
679
+ console.error('🔐 Not set up. Run: npx skillvault --invite YOUR_CODE\n');
772
680
  process.exit(1);
773
681
  }
774
- console.log('🔐 SkillVault Sync\n');
682
+ console.error('🔐 SkillVault Sync\n');
775
683
  await syncSkills();
776
684
  const result = await installSkillStubs();
777
- console.log(`\n ✅ ${result.installed} installed, ${result.skipped} up to date`);
778
- console.log('');
685
+ console.error(`\n ✅ ${result.installed} installed, ${result.skipped} up to date\n`);
779
686
  process.exit(0);
780
687
  }
781
- // Default: start MCP server
688
+ // Default: show help
689
+ console.log(`🔐 SkillVault v${VERSION}\n`);
782
690
  const config = loadConfig();
783
- if (!config) {
784
- console.log('🔐 SkillVault\n');
691
+ if (config) {
692
+ console.log(` ${config.publishers.length} publisher${config.publishers.length !== 1 ? 's' : ''} configured`);
693
+ console.log(' Run --status for details, --sync to update skills\n');
694
+ }
695
+ else {
785
696
  console.log(' Not set up yet. Run:');
786
697
  console.log(' npx skillvault --invite YOUR_CODE\n');
787
- process.exit(1);
788
698
  }
789
- startServer();
790
699
  }
791
700
  main().catch((err) => {
792
701
  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.1",
4
4
  "description": "SkillVault — secure skill distribution for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {