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.
- package/dist/cli.d.ts +10 -11
- package/dist/cli.js +117 -248
- 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
|
|
3
|
+
* SkillVault — Secure skill distribution for Claude Code.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* npx skillvault --invite CODE # Setup
|
|
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 +
|
|
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.
|
|
14
|
-
* 2.
|
|
15
|
-
* 3.
|
|
16
|
-
* 4.
|
|
17
|
-
* 5.
|
|
18
|
-
* 6. Decrypted content
|
|
19
|
-
* 7. License is validated on every load — revoked license = dead skill
|
|
13
|
+
* 1. Customer runs --invite once → vaults 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
|
|
3
|
+
* SkillVault — Secure skill distribution for Claude Code.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* npx skillvault --invite CODE # Setup
|
|
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 +
|
|
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.
|
|
14
|
-
* 2.
|
|
15
|
-
* 3.
|
|
16
|
-
* 4.
|
|
17
|
-
* 5.
|
|
18
|
-
* 6. Decrypted content
|
|
19
|
-
* 7. License is validated on every load — revoked license = dead skill
|
|
13
|
+
* 1. Customer runs --invite once → vaults 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.
|
|
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
|
|
52
|
-
npx skillvault --
|
|
53
|
-
npx skillvault --
|
|
54
|
-
npx skillvault --
|
|
55
|
-
npx skillvault
|
|
56
|
-
npx skillvault --help
|
|
57
|
-
npx skillvault --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
|
|
60
|
-
|
|
61
|
-
|
|
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 +
|
|
98
|
+
// ── Setup: Redeem Invite + Sync + Install ──
|
|
100
99
|
async function setup(code) {
|
|
101
|
-
console.
|
|
102
|
-
console.
|
|
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.
|
|
113
|
+
console.error(` ✅ Authenticated`);
|
|
115
114
|
if (data.email)
|
|
116
|
-
console.
|
|
115
|
+
console.error(` 📧 ${data.email}`);
|
|
117
116
|
if (data.capabilities.length > 0) {
|
|
118
|
-
console.
|
|
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.
|
|
130
|
+
console.error(` 🔄 Updated publisher: ${publisherEntry.name}`);
|
|
132
131
|
}
|
|
133
132
|
else {
|
|
134
133
|
existingConfig.publishers.push(publisherEntry);
|
|
135
|
-
console.
|
|
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
|
-
//
|
|
154
|
-
|
|
152
|
+
// Clean up legacy MCP config if present
|
|
153
|
+
cleanupMCPConfig();
|
|
155
154
|
// Sync vaults and install stubs
|
|
156
|
-
console.
|
|
157
|
-
console.
|
|
155
|
+
console.error('');
|
|
156
|
+
console.error(' Syncing skills...');
|
|
158
157
|
await syncSkills();
|
|
159
158
|
const installResult = await installSkillStubs();
|
|
160
|
-
console.
|
|
159
|
+
console.error('');
|
|
161
160
|
if (installResult.installed > 0) {
|
|
162
|
-
console.
|
|
161
|
+
console.error(` ✅ ${installResult.installed} skill${installResult.installed !== 1 ? 's' : ''} installed to ~/.claude/skills/`);
|
|
163
162
|
}
|
|
164
|
-
console.
|
|
165
|
-
console.
|
|
166
|
-
console.log('');
|
|
163
|
+
console.error(' ✅ Setup complete! Restart Claude Code to use your skills.');
|
|
164
|
+
console.error('');
|
|
167
165
|
}
|
|
168
|
-
/**
|
|
169
|
-
|
|
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
|
|
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}
|
|
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.
|
|
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.
|
|
262
|
+
console.error('🔐 SkillVault Token Refresh\n');
|
|
276
263
|
let anyRefreshed = false;
|
|
277
264
|
for (const pub of config.publishers) {
|
|
278
|
-
process.
|
|
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.
|
|
277
|
+
console.error('✅');
|
|
291
278
|
anyRefreshed = true;
|
|
292
279
|
}
|
|
293
280
|
else if (res.status === 401) {
|
|
294
|
-
console.
|
|
281
|
+
console.error('❌ expired — re-invite required');
|
|
295
282
|
}
|
|
296
283
|
else {
|
|
297
|
-
console.
|
|
284
|
+
console.error(`❌ server error (${res.status})`);
|
|
298
285
|
}
|
|
299
286
|
}
|
|
300
287
|
catch {
|
|
301
|
-
console.
|
|
288
|
+
console.error('❌ offline');
|
|
302
289
|
}
|
|
303
290
|
}
|
|
304
291
|
if (anyRefreshed) {
|
|
305
292
|
saveConfig(config);
|
|
306
|
-
console.
|
|
293
|
+
console.error('\n Tokens updated.\n');
|
|
307
294
|
}
|
|
308
295
|
else {
|
|
309
|
-
console.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
565
|
+
console.error('Error: Invalid skill name.');
|
|
566
|
+
process.exit(1);
|
|
585
567
|
}
|
|
586
568
|
const config = loadConfig();
|
|
587
569
|
if (!config) {
|
|
588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
598
|
+
process.stdout.write(watermark(skillMd.content, licenseeId));
|
|
614
599
|
}
|
|
615
600
|
for (const file of otherFiles) {
|
|
616
|
-
|
|
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
|
-
|
|
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.
|
|
765
|
-
console.
|
|
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.
|
|
639
|
+
console.error('🔐 Not set up. Run: npx skillvault --invite YOUR_CODE\n');
|
|
772
640
|
process.exit(1);
|
|
773
641
|
}
|
|
774
|
-
console.
|
|
642
|
+
console.error('🔐 SkillVault Sync\n');
|
|
775
643
|
await syncSkills();
|
|
776
644
|
const result = await installSkillStubs();
|
|
777
|
-
console.
|
|
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:
|
|
648
|
+
// Default: show help
|
|
649
|
+
console.log(`🔐 SkillVault v${VERSION}\n`);
|
|
782
650
|
const config = loadConfig();
|
|
783
|
-
if (
|
|
784
|
-
console.log('
|
|
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);
|