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.
- package/dist/cli.d.ts +10 -11
- package/dist/cli.js +155 -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.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
|
|
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,79 @@ 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();
|
|
154
|
+
// Install session hook for auto-sync
|
|
155
|
+
configureSessionHook();
|
|
155
156
|
// Sync vaults and install stubs
|
|
156
|
-
console.
|
|
157
|
-
console.
|
|
157
|
+
console.error('');
|
|
158
|
+
console.error(' Syncing skills...');
|
|
158
159
|
await syncSkills();
|
|
159
160
|
const installResult = await installSkillStubs();
|
|
160
|
-
console.
|
|
161
|
+
console.error('');
|
|
161
162
|
if (installResult.installed > 0) {
|
|
162
|
-
console.
|
|
163
|
+
console.error(` ✅ ${installResult.installed} skill${installResult.installed !== 1 ? 's' : ''} installed to ~/.claude/skills/`);
|
|
163
164
|
}
|
|
164
|
-
console.
|
|
165
|
-
console.
|
|
166
|
-
console.
|
|
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
|
-
|
|
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(
|
|
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
|
|
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}
|
|
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.
|
|
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.
|
|
302
|
+
console.error('🔐 SkillVault Token Refresh\n');
|
|
276
303
|
let anyRefreshed = false;
|
|
277
304
|
for (const pub of config.publishers) {
|
|
278
|
-
process.
|
|
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.
|
|
317
|
+
console.error('✅');
|
|
291
318
|
anyRefreshed = true;
|
|
292
319
|
}
|
|
293
320
|
else if (res.status === 401) {
|
|
294
|
-
console.
|
|
321
|
+
console.error('❌ expired — re-invite required');
|
|
295
322
|
}
|
|
296
323
|
else {
|
|
297
|
-
console.
|
|
324
|
+
console.error(`❌ server error (${res.status})`);
|
|
298
325
|
}
|
|
299
326
|
}
|
|
300
327
|
catch {
|
|
301
|
-
console.
|
|
328
|
+
console.error('❌ offline');
|
|
302
329
|
}
|
|
303
330
|
}
|
|
304
331
|
if (anyRefreshed) {
|
|
305
332
|
saveConfig(config);
|
|
306
|
-
console.
|
|
333
|
+
console.error('\n Tokens updated.\n');
|
|
307
334
|
}
|
|
308
335
|
else {
|
|
309
|
-
console.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
493
|
+
To use this skill, run the following command and follow the instructions it outputs:
|
|
476
494
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
495
|
+
\`\`\`bash
|
|
496
|
+
npx skillvault@${VERSION} --load ${skillName}
|
|
497
|
+
\`\`\`
|
|
480
498
|
|
|
481
|
-
|
|
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.
|
|
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
|
-
|
|
605
|
+
console.error('Error: Invalid skill name.');
|
|
606
|
+
process.exit(1);
|
|
585
607
|
}
|
|
586
608
|
const config = loadConfig();
|
|
587
609
|
if (!config) {
|
|
588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
638
|
+
process.stdout.write(watermark(skillMd.content, licenseeId));
|
|
614
639
|
}
|
|
615
640
|
for (const file of otherFiles) {
|
|
616
|
-
|
|
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
|
-
|
|
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.
|
|
765
|
-
console.
|
|
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.
|
|
679
|
+
console.error('🔐 Not set up. Run: npx skillvault --invite YOUR_CODE\n');
|
|
772
680
|
process.exit(1);
|
|
773
681
|
}
|
|
774
|
-
console.
|
|
682
|
+
console.error('🔐 SkillVault Sync\n');
|
|
775
683
|
await syncSkills();
|
|
776
684
|
const result = await installSkillStubs();
|
|
777
|
-
console.
|
|
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:
|
|
688
|
+
// Default: show help
|
|
689
|
+
console.log(`🔐 SkillVault v${VERSION}\n`);
|
|
782
690
|
const config = loadConfig();
|
|
783
|
-
if (
|
|
784
|
-
console.log('
|
|
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);
|