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.
- package/dist/cli.d.ts +10 -11
- package/dist/cli.js +118 -246
- 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,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,
|
|
476
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
565
|
+
console.error('Error: Invalid skill name.');
|
|
566
|
+
process.exit(1);
|
|
582
567
|
}
|
|
583
568
|
const config = loadConfig();
|
|
584
569
|
if (!config) {
|
|
585
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
598
|
+
process.stdout.write(watermark(skillMd.content, licenseeId));
|
|
611
599
|
}
|
|
612
600
|
for (const file of otherFiles) {
|
|
613
|
-
|
|
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
|
-
|
|
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.
|
|
762
|
-
console.
|
|
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.
|
|
639
|
+
console.error('🔐 Not set up. Run: npx skillvault --invite YOUR_CODE\n');
|
|
769
640
|
process.exit(1);
|
|
770
641
|
}
|
|
771
|
-
console.
|
|
642
|
+
console.error('🔐 SkillVault Sync\n');
|
|
772
643
|
await syncSkills();
|
|
773
644
|
const result = await installSkillStubs();
|
|
774
|
-
console.
|
|
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:
|
|
648
|
+
// Default: show help
|
|
649
|
+
console.log(`🔐 SkillVault v${VERSION}\n`);
|
|
779
650
|
const config = loadConfig();
|
|
780
|
-
if (
|
|
781
|
-
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 {
|
|
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);
|