skillvault 0.1.0 → 0.4.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 +14 -9
- package/dist/cli.js +538 -119
- package/package.json +3 -3
package/dist/cli.d.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* SkillVault Agent —
|
|
3
|
+
* SkillVault Agent — Secure skill distribution for Claude Code.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* npx skillvault --invite CODE #
|
|
7
|
-
* npx skillvault
|
|
8
|
-
*
|
|
6
|
+
* npx skillvault --invite CODE # Setup / add publisher
|
|
7
|
+
* npx skillvault --status # Show publishers & skills
|
|
8
|
+
* npx skillvault --refresh # Re-authenticate tokens + sync
|
|
9
|
+
* npx skillvault --sync # Sync vaults + install stubs
|
|
10
|
+
* npx skillvault # Start MCP server (after setup)
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
* 1. Redeems invite code
|
|
12
|
-
* 2.
|
|
13
|
-
* 3.
|
|
14
|
-
* 4.
|
|
12
|
+
* How it works:
|
|
13
|
+
* 1. Redeems invite code → gets publisher token
|
|
14
|
+
* 2. Downloads encrypted skill vaults to ~/.skillvault/vaults/
|
|
15
|
+
* 3. Installs stub SKILL.md files in ~/.claude/skills/ (Claude discovers these)
|
|
16
|
+
* 4. Starts MCP server exposing skillvault_load tool
|
|
17
|
+
* 5. When Claude triggers a skill, it calls skillvault_load → decrypts on demand
|
|
18
|
+
* 6. Decrypted content lives only in conversation context — never on disk
|
|
19
|
+
* 7. License is validated on every load — revoked license = dead skill
|
|
15
20
|
*/
|
|
16
21
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,49 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* SkillVault Agent —
|
|
3
|
+
* SkillVault Agent — Secure skill distribution for Claude Code.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* npx skillvault --invite CODE #
|
|
7
|
-
* npx skillvault
|
|
8
|
-
*
|
|
6
|
+
* npx skillvault --invite CODE # Setup / add publisher
|
|
7
|
+
* npx skillvault --status # Show publishers & skills
|
|
8
|
+
* npx skillvault --refresh # Re-authenticate tokens + sync
|
|
9
|
+
* npx skillvault --sync # Sync vaults + install stubs
|
|
10
|
+
* npx skillvault # Start MCP server (after setup)
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
* 1. Redeems invite code
|
|
12
|
-
* 2.
|
|
13
|
-
* 3.
|
|
14
|
-
* 4.
|
|
12
|
+
* How it works:
|
|
13
|
+
* 1. Redeems invite code → gets publisher token
|
|
14
|
+
* 2. Downloads encrypted skill vaults to ~/.skillvault/vaults/
|
|
15
|
+
* 3. Installs stub SKILL.md files in ~/.claude/skills/ (Claude discovers these)
|
|
16
|
+
* 4. Starts MCP server exposing skillvault_load tool
|
|
17
|
+
* 5. When Claude triggers a skill, it calls skillvault_load → decrypts on demand
|
|
18
|
+
* 6. Decrypted content lives only in conversation context — never on disk
|
|
19
|
+
* 7. License is validated on every load — revoked license = dead skill
|
|
15
20
|
*/
|
|
16
21
|
import { createServer } from 'node:http';
|
|
17
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
|
|
18
23
|
import { join } from 'node:path';
|
|
19
|
-
import { execFile } from 'node:child_process';
|
|
20
|
-
import { promisify } from 'node:util';
|
|
21
24
|
import { createDecipheriv, createPublicKey, diffieHellman, hkdfSync, generateKeyPairSync, } from 'node:crypto';
|
|
22
|
-
const
|
|
25
|
+
const VERSION = '0.4.0';
|
|
23
26
|
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
24
27
|
const API_URL = process.env.SKILLVAULT_API_URL || 'https://api.getskillvault.com';
|
|
25
28
|
const MCP_PORT = parseInt(process.env.SKILLVAULT_MCP_PORT || '9877', 10);
|
|
26
29
|
const CONFIG_DIR = join(HOME, '.skillvault');
|
|
27
30
|
const CONFIG_PATH = join(CONFIG_DIR, 'agent-config.json');
|
|
28
31
|
const VAULT_DIR = join(CONFIG_DIR, 'vaults');
|
|
32
|
+
const SKILLS_DIR = join(HOME, '.claude', 'skills');
|
|
29
33
|
// ── CLI Argument Parsing ──
|
|
30
34
|
const args = process.argv.slice(2);
|
|
31
35
|
const inviteIdx = args.indexOf('--invite');
|
|
32
36
|
const inviteCode = inviteIdx >= 0 ? args[inviteIdx + 1] : null;
|
|
33
37
|
const helpFlag = args.includes('--help') || args.includes('-h');
|
|
34
38
|
const versionFlag = args.includes('--version') || args.includes('-v');
|
|
39
|
+
const statusFlag = args.includes('--status');
|
|
40
|
+
const refreshFlag = args.includes('--refresh');
|
|
41
|
+
const syncFlag = args.includes('--sync');
|
|
35
42
|
if (versionFlag) {
|
|
36
|
-
console.log(
|
|
43
|
+
console.log(`skillvault ${VERSION}`);
|
|
37
44
|
process.exit(0);
|
|
38
45
|
}
|
|
39
46
|
if (helpFlag) {
|
|
40
47
|
console.log(`
|
|
41
|
-
SkillVault
|
|
48
|
+
SkillVault — Secure skill distribution for Claude Code
|
|
42
49
|
|
|
43
50
|
Usage:
|
|
44
|
-
npx skillvault --invite CODE
|
|
45
|
-
npx skillvault
|
|
51
|
+
npx skillvault --invite CODE Setup or add a new publisher
|
|
52
|
+
npx skillvault --status Show all publishers, skills, and statuses
|
|
53
|
+
npx skillvault --refresh Re-authenticate expired tokens + sync skills
|
|
54
|
+
npx skillvault --sync Sync vaults and install skill stubs
|
|
55
|
+
npx skillvault Start MCP server (default, after setup)
|
|
46
56
|
npx skillvault --help Show this help
|
|
57
|
+
npx skillvault --version Show version
|
|
58
|
+
|
|
59
|
+
Skills are encrypted at rest. When Claude Code triggers a skill, the
|
|
60
|
+
MCP server decrypts it on demand and returns the content to Claude's
|
|
61
|
+
session. The decrypted content is never written to disk.
|
|
47
62
|
|
|
48
63
|
Environment:
|
|
49
64
|
SKILLVAULT_API_URL Server URL (default: https://api.getskillvault.com)
|
|
@@ -54,7 +69,24 @@ if (helpFlag) {
|
|
|
54
69
|
function loadConfig() {
|
|
55
70
|
try {
|
|
56
71
|
if (existsSync(CONFIG_PATH)) {
|
|
57
|
-
|
|
72
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
73
|
+
if (raw.token && !raw.publishers) {
|
|
74
|
+
const migrated = {
|
|
75
|
+
customer_token: raw.token,
|
|
76
|
+
customer_email: raw.email || null,
|
|
77
|
+
publishers: [{
|
|
78
|
+
id: raw.publisher_id,
|
|
79
|
+
name: raw.publisher_id,
|
|
80
|
+
token: raw.token,
|
|
81
|
+
added_at: raw.setup_at || new Date().toISOString(),
|
|
82
|
+
}],
|
|
83
|
+
api_url: raw.api_url || API_URL,
|
|
84
|
+
setup_at: raw.setup_at || new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
saveConfig(migrated);
|
|
87
|
+
return migrated;
|
|
88
|
+
}
|
|
89
|
+
return raw;
|
|
58
90
|
}
|
|
59
91
|
}
|
|
60
92
|
catch { }
|
|
@@ -64,11 +96,10 @@ function saveConfig(config) {
|
|
|
64
96
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
65
97
|
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
66
98
|
}
|
|
67
|
-
// ── Setup: Redeem Invite + Configure MCP ──
|
|
99
|
+
// ── Setup: Redeem Invite + Configure MCP + Sync ──
|
|
68
100
|
async function setup(code) {
|
|
69
101
|
console.log('🔐 SkillVault Setup');
|
|
70
102
|
console.log(` Redeeming invite code: ${code}`);
|
|
71
|
-
// Redeem invite code for a token
|
|
72
103
|
const response = await fetch(`${API_URL}/auth/companion/token`, {
|
|
73
104
|
method: 'POST',
|
|
74
105
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -86,15 +117,58 @@ async function setup(code) {
|
|
|
86
117
|
if (data.capabilities.length > 0) {
|
|
87
118
|
console.log(` 📦 Skills: ${data.capabilities.join(', ')}`);
|
|
88
119
|
}
|
|
89
|
-
|
|
90
|
-
|
|
120
|
+
const existingConfig = loadConfig();
|
|
121
|
+
const publisherEntry = {
|
|
122
|
+
id: data.publisher_id,
|
|
123
|
+
name: data.publisher_name || data.publisher_id,
|
|
91
124
|
token: data.token,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
125
|
+
added_at: new Date().toISOString(),
|
|
126
|
+
};
|
|
127
|
+
if (existingConfig) {
|
|
128
|
+
const existingIdx = existingConfig.publishers.findIndex(p => p.id === data.publisher_id);
|
|
129
|
+
if (existingIdx >= 0) {
|
|
130
|
+
existingConfig.publishers[existingIdx] = publisherEntry;
|
|
131
|
+
console.log(` 🔄 Updated publisher: ${publisherEntry.name}`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
existingConfig.publishers.push(publisherEntry);
|
|
135
|
+
console.log(` ➕ Added publisher: ${publisherEntry.name}. You now have skills from ${existingConfig.publishers.length} publishers.`);
|
|
136
|
+
}
|
|
137
|
+
if (data.customer_token)
|
|
138
|
+
existingConfig.customer_token = data.customer_token;
|
|
139
|
+
if (data.email)
|
|
140
|
+
existingConfig.customer_email = data.email;
|
|
141
|
+
saveConfig(existingConfig);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
saveConfig({
|
|
145
|
+
customer_token: data.customer_token || data.token,
|
|
146
|
+
customer_email: data.email,
|
|
147
|
+
publishers: [publisherEntry],
|
|
148
|
+
api_url: API_URL,
|
|
149
|
+
setup_at: new Date().toISOString(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
mkdirSync(join(VAULT_DIR, data.publisher_id), { recursive: true });
|
|
153
|
+
// Configure MCP server in Claude Code
|
|
154
|
+
configureMCP();
|
|
155
|
+
// Sync vaults and install stubs
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(' Syncing skills...');
|
|
158
|
+
await syncSkills();
|
|
159
|
+
const installResult = await installSkillStubs();
|
|
160
|
+
console.log('');
|
|
161
|
+
if (installResult.installed > 0) {
|
|
162
|
+
console.log(` ✅ ${installResult.installed} skill${installResult.installed !== 1 ? 's' : ''} ready`);
|
|
163
|
+
}
|
|
164
|
+
console.log(' Setup complete! Start the agent with: npx skillvault');
|
|
165
|
+
console.log(' Then restart Claude Code to discover your skills.');
|
|
166
|
+
console.log('');
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Configure Claude Code to connect to the SkillVault MCP server.
|
|
170
|
+
*/
|
|
171
|
+
function configureMCP() {
|
|
98
172
|
const mcpConfigPath = join(HOME, '.claude', '.mcp.json');
|
|
99
173
|
try {
|
|
100
174
|
let mcpConfig = {};
|
|
@@ -111,17 +185,318 @@ async function setup(code) {
|
|
|
111
185
|
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), { mode: 0o600 });
|
|
112
186
|
console.log(` ✅ Claude Code MCP configured`);
|
|
113
187
|
}
|
|
114
|
-
catch
|
|
188
|
+
catch {
|
|
115
189
|
console.error(` ⚠️ Failed to configure MCP — manually add to ~/.claude/.mcp.json`);
|
|
116
190
|
}
|
|
117
|
-
|
|
118
|
-
|
|
191
|
+
}
|
|
192
|
+
async function showStatus() {
|
|
193
|
+
const config = loadConfig();
|
|
194
|
+
if (!config) {
|
|
195
|
+
console.log('🔐 SkillVault Agent\n');
|
|
196
|
+
console.log(' Not set up yet. Run:');
|
|
197
|
+
console.log(' npx skillvault --invite YOUR_CODE\n');
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
console.log('🔐 SkillVault Status\n');
|
|
201
|
+
if (config.customer_email)
|
|
202
|
+
console.log(` Account: ${config.customer_email}`);
|
|
203
|
+
console.log(` Config: ${CONFIG_PATH}`);
|
|
204
|
+
console.log(` Skills: ${SKILLS_DIR} (stubs — encrypted at rest)`);
|
|
205
|
+
console.log(` Server: ${config.api_url}`);
|
|
119
206
|
console.log('');
|
|
120
|
-
|
|
121
|
-
|
|
207
|
+
let skills = [];
|
|
208
|
+
let online = false;
|
|
209
|
+
try {
|
|
210
|
+
const token = config.customer_token || (config.publishers.length > 0 ? config.publishers[0].token : null);
|
|
211
|
+
if (token) {
|
|
212
|
+
const res = await fetch(`${config.api_url}/customer/skills`, {
|
|
213
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
214
|
+
signal: AbortSignal.timeout(5000),
|
|
215
|
+
});
|
|
216
|
+
if (res.ok) {
|
|
217
|
+
const data = await res.json();
|
|
218
|
+
skills = data.skills || [];
|
|
219
|
+
online = true;
|
|
220
|
+
}
|
|
221
|
+
else if (res.status === 401) {
|
|
222
|
+
console.log(' ⚠️ Session expired. Run: npx skillvault --refresh\n');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch { }
|
|
227
|
+
console.log(' Publishers:');
|
|
228
|
+
console.log(' ' + '-'.repeat(60));
|
|
229
|
+
console.log(` ${'Name'.padEnd(25)} ${'Skills'.padEnd(10)} ${'Status'.padEnd(15)}`);
|
|
230
|
+
console.log(' ' + '-'.repeat(60));
|
|
231
|
+
let totalSkills = 0;
|
|
232
|
+
for (const pub of config.publishers) {
|
|
233
|
+
const pubSkills = skills.filter(s => s.publisher_id === pub.id);
|
|
234
|
+
let localVaultCount = 0;
|
|
235
|
+
const pubVaultDir = join(VAULT_DIR, pub.id);
|
|
236
|
+
if (existsSync(pubVaultDir)) {
|
|
237
|
+
try {
|
|
238
|
+
localVaultCount = readdirSync(pubVaultDir).filter(f => f.endsWith('.vault')).length;
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
}
|
|
242
|
+
const displayCount = online ? pubSkills.length : localVaultCount;
|
|
243
|
+
totalSkills += displayCount;
|
|
244
|
+
const status = online ? 'connected' : 'offline (cached)';
|
|
245
|
+
console.log(` ${pub.name.padEnd(25)} ${String(displayCount).padEnd(10)} ${status.padEnd(15)}`);
|
|
246
|
+
}
|
|
247
|
+
console.log(' ' + '-'.repeat(60));
|
|
248
|
+
if (online && skills.length > 0) {
|
|
249
|
+
console.log('\n Skills:');
|
|
250
|
+
console.log(' ' + '-'.repeat(70));
|
|
251
|
+
console.log(` ${'Skill'.padEnd(25)} ${'Publisher'.padEnd(20)} ${'Status'.padEnd(12)} ${'Expires'.padEnd(12)}`);
|
|
252
|
+
console.log(' ' + '-'.repeat(70));
|
|
253
|
+
for (const skill of skills) {
|
|
254
|
+
const pubName = config.publishers.find(p => p.id === skill.publisher_id)?.name || skill.publisher_name || skill.publisher_id;
|
|
255
|
+
const expires = skill.expires_at ? new Date(skill.expires_at).toLocaleDateString() : 'never';
|
|
256
|
+
console.log(` ${skill.skill_name.padEnd(25)} ${pubName.padEnd(20)} ${skill.status.padEnd(12)} ${expires.padEnd(12)}`);
|
|
257
|
+
}
|
|
258
|
+
console.log(' ' + '-'.repeat(70));
|
|
259
|
+
}
|
|
260
|
+
const publisherCount = config.publishers.length;
|
|
261
|
+
const allActive = skills.length > 0 && skills.every(s => s.status === 'active');
|
|
262
|
+
const statusSuffix = online ? (allActive ? ', all active' : '') : ' (offline)';
|
|
263
|
+
console.log(`\n Total: ${totalSkills} skill${totalSkills !== 1 ? 's' : ''} from ${publisherCount} publisher${publisherCount !== 1 ? 's' : ''}${statusSuffix}`);
|
|
122
264
|
console.log('');
|
|
123
265
|
}
|
|
124
|
-
// ──
|
|
266
|
+
// ── Refresh ──
|
|
267
|
+
async function refreshTokens() {
|
|
268
|
+
const config = loadConfig();
|
|
269
|
+
if (!config) {
|
|
270
|
+
console.log('🔐 SkillVault Agent\n');
|
|
271
|
+
console.log(' Not set up yet. Run:');
|
|
272
|
+
console.log(' npx skillvault --invite YOUR_CODE\n');
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
console.log('🔐 SkillVault Token Refresh\n');
|
|
276
|
+
let anyRefreshed = false;
|
|
277
|
+
for (const pub of config.publishers) {
|
|
278
|
+
process.stdout.write(` Refreshing ${pub.name}... `);
|
|
279
|
+
try {
|
|
280
|
+
const res = await fetch(`${config.api_url}/auth/companion/refresh`, {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${pub.token}` },
|
|
283
|
+
signal: AbortSignal.timeout(10000),
|
|
284
|
+
});
|
|
285
|
+
if (res.ok) {
|
|
286
|
+
const data = await res.json();
|
|
287
|
+
pub.token = data.token;
|
|
288
|
+
if (data.customer_token)
|
|
289
|
+
config.customer_token = data.customer_token;
|
|
290
|
+
console.log('✅ refreshed');
|
|
291
|
+
anyRefreshed = true;
|
|
292
|
+
}
|
|
293
|
+
else if (res.status === 401) {
|
|
294
|
+
console.log('❌ expired — re-invite required');
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
console.log(`❌ server error (${res.status})`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
console.log('❌ offline');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (anyRefreshed) {
|
|
305
|
+
saveConfig(config);
|
|
306
|
+
console.log('\n Tokens updated.\n');
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
console.log('\n No tokens refreshed. You may need to run: npx skillvault --invite CODE\n');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function syncSkills() {
|
|
313
|
+
const config = loadConfig();
|
|
314
|
+
if (!config || config.publishers.length === 0) {
|
|
315
|
+
return { synced: 0, errors: ['No config or publishers found'] };
|
|
316
|
+
}
|
|
317
|
+
let totalSynced = 0;
|
|
318
|
+
const errors = [];
|
|
319
|
+
for (const pub of config.publishers) {
|
|
320
|
+
const pubVaultDir = join(VAULT_DIR, pub.id);
|
|
321
|
+
mkdirSync(pubVaultDir, { recursive: true });
|
|
322
|
+
let skills = [];
|
|
323
|
+
try {
|
|
324
|
+
const res = await fetch(`${config.api_url}/skills?publisher_id=${encodeURIComponent(pub.id)}`, {
|
|
325
|
+
headers: { 'Authorization': `Bearer ${pub.token}` },
|
|
326
|
+
signal: AbortSignal.timeout(10000),
|
|
327
|
+
});
|
|
328
|
+
if (!res.ok) {
|
|
329
|
+
errors.push(`${pub.name}: ${res.status === 401 ? 'auth expired' : `failed (${res.status})`}`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const data = await res.json();
|
|
333
|
+
skills = data.skills || [];
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
errors.push(`${pub.name}: ${err instanceof Error ? err.message : 'fetch failed'}`);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const remoteSkillNames = new Set(skills.map(s => s.skill_name));
|
|
340
|
+
// Revocation: remove stubs for skills no longer in the remote list
|
|
341
|
+
try {
|
|
342
|
+
if (existsSync(pubVaultDir)) {
|
|
343
|
+
const localVaults = readdirSync(pubVaultDir).filter(f => f.endsWith('.vault'));
|
|
344
|
+
for (const vaultFile of localVaults) {
|
|
345
|
+
const localSkillName = vaultFile.replace(/\.vault$/, '');
|
|
346
|
+
if (!remoteSkillNames.has(localSkillName)) {
|
|
347
|
+
console.log(`[sync] Grant revoked: "${localSkillName}" from ${pub.name}`);
|
|
348
|
+
const skillDir = join(SKILLS_DIR, localSkillName);
|
|
349
|
+
try {
|
|
350
|
+
if (existsSync(skillDir) && existsSync(join(skillDir, 'manifest.json'))) {
|
|
351
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
352
|
+
console.log(`[sync] Removed stub: ~/.claude/skills/${localSkillName}/`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch { }
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch { }
|
|
361
|
+
// Download missing or updated vaults + write skill metadata for stubs
|
|
362
|
+
let pubSynced = 0;
|
|
363
|
+
for (const skill of skills) {
|
|
364
|
+
const vaultPath = join(pubVaultDir, `${skill.skill_name}.vault`);
|
|
365
|
+
const vaultExists = existsSync(vaultPath);
|
|
366
|
+
let needsDownload = !vaultExists;
|
|
367
|
+
if (vaultExists && skill.vault_hash) {
|
|
368
|
+
const hashPath = vaultPath + '.hash';
|
|
369
|
+
try {
|
|
370
|
+
if (existsSync(hashPath)) {
|
|
371
|
+
const localHash = readFileSync(hashPath, 'utf8').trim();
|
|
372
|
+
if (localHash !== skill.vault_hash) {
|
|
373
|
+
needsDownload = true;
|
|
374
|
+
console.log(`[sync] Update available: "${skill.skill_name}" from ${pub.name}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch { }
|
|
379
|
+
}
|
|
380
|
+
if (!needsDownload)
|
|
381
|
+
continue;
|
|
382
|
+
const capabilityName = skill.capability_name || skill.skill_name;
|
|
383
|
+
try {
|
|
384
|
+
let dlRes = await fetch(`${config.api_url}/skills/download?capability=${encodeURIComponent(capabilityName)}`, { signal: AbortSignal.timeout(15000) });
|
|
385
|
+
if (!dlRes.ok) {
|
|
386
|
+
dlRes = await fetch(`${config.api_url}/skills/download?capability=${encodeURIComponent(capabilityName)}`, { headers: { 'Authorization': `Bearer ${pub.token}` }, signal: AbortSignal.timeout(15000) });
|
|
387
|
+
}
|
|
388
|
+
if (!dlRes.ok) {
|
|
389
|
+
errors.push(`${pub.name}/${skill.skill_name}: download failed (${dlRes.status})`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const dlData = await dlRes.json();
|
|
393
|
+
const vaultBuffer = Buffer.from(dlData.vault_data, 'base64');
|
|
394
|
+
writeFileSync(vaultPath, vaultBuffer, { mode: 0o600 });
|
|
395
|
+
if (dlData.vault_hash)
|
|
396
|
+
writeFileSync(vaultPath + '.hash', dlData.vault_hash, { mode: 0o600 });
|
|
397
|
+
// Store skill metadata for stub generation
|
|
398
|
+
writeFileSync(vaultPath + '.meta', JSON.stringify({
|
|
399
|
+
skill_name: skill.skill_name,
|
|
400
|
+
description: skill.description || '',
|
|
401
|
+
capability_name: skill.capability_name,
|
|
402
|
+
version: dlData.version || skill.version || '0.0.0',
|
|
403
|
+
publisher_name: pub.name,
|
|
404
|
+
publisher_id: pub.id,
|
|
405
|
+
}), { mode: 0o600 });
|
|
406
|
+
pubSynced++;
|
|
407
|
+
console.log(`[sync] ${vaultExists ? 'Updated' : 'Downloaded'}: "${skill.skill_name}" from ${pub.name}`);
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
errors.push(`${pub.name}/${skill.skill_name}: ${err instanceof Error ? err.message : 'download error'}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (pubSynced > 0)
|
|
414
|
+
console.log(`[sync] Synced ${pubSynced} vault${pubSynced !== 1 ? 's' : ''} from ${pub.name}`);
|
|
415
|
+
totalSynced += pubSynced;
|
|
416
|
+
}
|
|
417
|
+
return { synced: totalSynced, errors };
|
|
418
|
+
}
|
|
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
|
+
async function installSkillStubs() {
|
|
425
|
+
const config = loadConfig();
|
|
426
|
+
if (!config || config.publishers.length === 0) {
|
|
427
|
+
return { installed: 0, skipped: 0, errors: ['No config or publishers found'] };
|
|
428
|
+
}
|
|
429
|
+
let installed = 0;
|
|
430
|
+
let skipped = 0;
|
|
431
|
+
const errors = [];
|
|
432
|
+
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
433
|
+
for (const pub of config.publishers) {
|
|
434
|
+
const pubVaultDir = join(VAULT_DIR, pub.id);
|
|
435
|
+
if (!existsSync(pubVaultDir))
|
|
436
|
+
continue;
|
|
437
|
+
const vaultFiles = readdirSync(pubVaultDir).filter(f => f.endsWith('.vault'));
|
|
438
|
+
for (const vaultFile of vaultFiles) {
|
|
439
|
+
const skillName = vaultFile.replace(/\.vault$/, '');
|
|
440
|
+
const vaultPath = join(pubVaultDir, vaultFile);
|
|
441
|
+
const skillDir = join(SKILLS_DIR, skillName);
|
|
442
|
+
const manifestPath = join(skillDir, 'manifest.json');
|
|
443
|
+
const hashPath = vaultPath + '.hash';
|
|
444
|
+
// Check if stub is already up to date
|
|
445
|
+
if (existsSync(manifestPath) && existsSync(hashPath)) {
|
|
446
|
+
try {
|
|
447
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
448
|
+
const currentHash = readFileSync(hashPath, 'utf8').trim();
|
|
449
|
+
if (manifest.vault_hash === currentHash) {
|
|
450
|
+
skipped++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch { }
|
|
455
|
+
}
|
|
456
|
+
// Read skill metadata (written during sync)
|
|
457
|
+
let meta = { skill_name: skillName, description: '', publisher_name: pub.name };
|
|
458
|
+
const metaPath = vaultPath + '.meta';
|
|
459
|
+
try {
|
|
460
|
+
if (existsSync(metaPath))
|
|
461
|
+
meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
462
|
+
}
|
|
463
|
+
catch { }
|
|
464
|
+
// Write stub SKILL.md
|
|
465
|
+
mkdirSync(skillDir, { recursive: true });
|
|
466
|
+
const stub = `---
|
|
467
|
+
name: ${skillName}
|
|
468
|
+
description: "${(meta.description || '').replace(/"/g, '\\"')}"
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
# ${skillName}
|
|
472
|
+
|
|
473
|
+
This is an encrypted SkillVault skill from **${meta.publisher_name || pub.name}**.
|
|
474
|
+
|
|
475
|
+
To use this skill, call the \`skillvault_load\` MCP tool with skill_name "${skillName}".
|
|
476
|
+
The tool will decrypt and return the full skill instructions for you to follow.
|
|
477
|
+
|
|
478
|
+
Example: Use the skillvault_load tool with skill_name "${skillName}" to load the instructions, then follow them to complete the user's request.
|
|
479
|
+
`;
|
|
480
|
+
writeFileSync(join(skillDir, 'SKILL.md'), stub, { mode: 0o600 });
|
|
481
|
+
// Write manifest
|
|
482
|
+
const vaultHash = existsSync(hashPath) ? readFileSync(hashPath, 'utf8').trim() : '';
|
|
483
|
+
writeFileSync(manifestPath, JSON.stringify({
|
|
484
|
+
publisher: meta.publisher_name || pub.name,
|
|
485
|
+
publisher_id: pub.id,
|
|
486
|
+
skill_name: skillName,
|
|
487
|
+
capability_name: meta.capability_name || `skill/${skillName}`,
|
|
488
|
+
version: meta.version || '0.0.0',
|
|
489
|
+
vault_hash: vaultHash,
|
|
490
|
+
installed_at: new Date().toISOString(),
|
|
491
|
+
encrypted: true,
|
|
492
|
+
}, null, 2), { mode: 0o600 });
|
|
493
|
+
installed++;
|
|
494
|
+
console.log(`[install] Stub installed: "${skillName}" → ~/.claude/skills/${skillName}/`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { installed, skipped, errors };
|
|
498
|
+
}
|
|
499
|
+
// ── Vault Decryption (in-memory only) ──
|
|
125
500
|
const VAULT_MAGIC = Buffer.from([0x00, 0x53, 0x56, 0x54]);
|
|
126
501
|
function decryptVault(data, cek) {
|
|
127
502
|
let offset = 0;
|
|
@@ -148,24 +523,31 @@ function decryptVault(data, cek) {
|
|
|
148
523
|
dec.setAuthTag(authTag);
|
|
149
524
|
dec.setAAD(metadataJSON);
|
|
150
525
|
const payload = Buffer.concat([dec.update(encPayload), dec.final()]);
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return
|
|
526
|
+
const metadata = JSON.parse(metadataJSON.toString('utf8'));
|
|
527
|
+
const files = manifest.map(entry => ({
|
|
528
|
+
path: entry.path,
|
|
529
|
+
content: payload.subarray(entry.offset, entry.offset + entry.size).toString('utf8'),
|
|
530
|
+
}));
|
|
531
|
+
return { metadata, files };
|
|
157
532
|
}
|
|
158
|
-
|
|
533
|
+
function resolveSkillPublisher(skillName, config) {
|
|
534
|
+
for (const pub of config.publishers) {
|
|
535
|
+
const vaultPath = join(VAULT_DIR, pub.id, `${skillName}.vault`);
|
|
536
|
+
if (existsSync(vaultPath))
|
|
537
|
+
return { publisher: pub, vaultPath };
|
|
538
|
+
}
|
|
539
|
+
const legacyPath = join(VAULT_DIR, `${skillName}.vault`);
|
|
540
|
+
if (existsSync(legacyPath) && config.publishers.length > 0) {
|
|
541
|
+
return { publisher: config.publishers[0], vaultPath: legacyPath };
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
async function fetchCEK(skillName, publisherToken) {
|
|
159
546
|
const kp = generateKeyPairSync('x25519');
|
|
160
547
|
const pub = kp.publicKey.export({ type: 'spki', format: 'der' }).toString('base64');
|
|
161
|
-
const config = loadConfig();
|
|
162
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
163
|
-
if (config?.token) {
|
|
164
|
-
headers['Authorization'] = `Bearer ${config.token}`;
|
|
165
|
-
}
|
|
166
548
|
const res = await fetch(`${API_URL}/v1/skills/${skillName}/cek`, {
|
|
167
549
|
method: 'POST',
|
|
168
|
-
headers,
|
|
550
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${publisherToken}` },
|
|
169
551
|
body: JSON.stringify({ companion_public_key: pub }),
|
|
170
552
|
});
|
|
171
553
|
if (!res.ok)
|
|
@@ -190,102 +572,86 @@ function watermark(content, id) {
|
|
|
190
572
|
const mark = BigInt('0x' + hex).toString(4).split('').map(d => zw[d]).join('');
|
|
191
573
|
return content.split('\n').map((l, i) => (i > 0 && i % 5 === 0 ? l + mark : l)).join('\n');
|
|
192
574
|
}
|
|
193
|
-
// ──
|
|
194
|
-
function findClaude() {
|
|
195
|
-
const paths = ['/usr/local/bin/claude', '/opt/homebrew/bin/claude', join(HOME, '.claude', 'bin', 'claude'), join(HOME, '.local', 'bin', 'claude')];
|
|
196
|
-
for (const p of paths)
|
|
197
|
-
if (existsSync(p))
|
|
198
|
-
return p;
|
|
199
|
-
try {
|
|
200
|
-
const { execSync } = require('node:child_process');
|
|
201
|
-
return execSync('which claude', { encoding: 'utf8', timeout: 3000 }).trim() || null;
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// ── Input Validation ──
|
|
575
|
+
// ── On-Demand Skill Loading (MCP tool handler) ──
|
|
208
576
|
function validateSkillName(name) {
|
|
209
577
|
return /^[a-zA-Z0-9_-]+$/.test(name) && name.length > 0 && name.length <= 128;
|
|
210
578
|
}
|
|
211
|
-
async function
|
|
212
|
-
// CRITICAL: Sanitize skill_name to prevent path traversal and URL injection
|
|
579
|
+
async function loadSkill(skillName) {
|
|
213
580
|
if (!validateSkillName(skillName)) {
|
|
214
|
-
return { success: false,
|
|
581
|
+
return { success: false, content: '', error: 'Invalid skill name. Only letters, numbers, hyphens, and underscores allowed.' };
|
|
215
582
|
}
|
|
216
583
|
const config = loadConfig();
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
584
|
+
if (!config) {
|
|
585
|
+
return { success: false, content: '', error: 'Not configured. Run: npx skillvault --invite CODE' };
|
|
586
|
+
}
|
|
587
|
+
const resolved = resolveSkillPublisher(skillName, config);
|
|
588
|
+
if (!resolved) {
|
|
589
|
+
return { success: false, content: '', error: `Vault not found for "${skillName}". Run: npx skillvault --sync` };
|
|
222
590
|
}
|
|
223
|
-
|
|
591
|
+
const licenseeId = config.customer_email || 'unknown';
|
|
592
|
+
// Fetch CEK — validates license on every load
|
|
224
593
|
let cek;
|
|
225
594
|
try {
|
|
226
|
-
cek = await fetchCEK(skillName);
|
|
595
|
+
cek = await fetchCEK(skillName, resolved.publisher.token);
|
|
227
596
|
}
|
|
228
597
|
catch (err) {
|
|
229
|
-
return { success: false,
|
|
598
|
+
return { success: false, content: '', error: `License check failed: ${err instanceof Error ? err.message : 'unknown'}. Your license may have been revoked.` };
|
|
230
599
|
}
|
|
231
|
-
// Decrypt
|
|
232
|
-
let content;
|
|
600
|
+
// Decrypt in memory — content never touches disk
|
|
233
601
|
try {
|
|
234
|
-
const vaultData = readFileSync(vaultPath);
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
finally {
|
|
602
|
+
const vaultData = readFileSync(resolved.vaultPath);
|
|
603
|
+
const vault = decryptVault(vaultData, cek);
|
|
238
604
|
cek.fill(0);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const { stdout } = await execFileAsync(claudePath, ['-p', prompt], { timeout: 120_000, maxBuffer: 10 * 1024 * 1024 });
|
|
246
|
-
content = ''; // zero-fill
|
|
247
|
-
return { success: true, output: stdout };
|
|
605
|
+
// Combine all files into a single response, with SKILL.md first
|
|
606
|
+
const skillMd = vault.files.find(f => f.path === 'SKILL.md');
|
|
607
|
+
const otherFiles = vault.files.filter(f => f.path !== 'SKILL.md');
|
|
608
|
+
let content = '';
|
|
609
|
+
if (skillMd) {
|
|
610
|
+
content = watermark(skillMd.content, licenseeId);
|
|
248
611
|
}
|
|
249
|
-
|
|
250
|
-
content
|
|
251
|
-
return { success: false, output: '', error: err instanceof Error ? err.message : 'Execution failed' };
|
|
612
|
+
for (const file of otherFiles) {
|
|
613
|
+
content += `\n\n---\n# File: ${file.path}\n\n${watermark(file.content, licenseeId)}`;
|
|
252
614
|
}
|
|
615
|
+
return { success: true, content };
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
cek.fill(0);
|
|
619
|
+
return { success: false, content: '', error: `Decryption failed: ${err instanceof Error ? err.message : 'unknown'}` };
|
|
253
620
|
}
|
|
254
|
-
// Fallback: return skill description without full content
|
|
255
|
-
content = '';
|
|
256
|
-
return { success: true, output: `Skill "${skillName}" is ready but Claude CLI was not found for secure execution. Install Claude Code to enable protected skill execution.` };
|
|
257
621
|
}
|
|
258
622
|
function rpcOk(id, result) { return JSON.stringify({ jsonrpc: '2.0', id, result }); }
|
|
259
623
|
function rpcErr(id, code, msg) { return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message: msg } }); }
|
|
260
624
|
async function handleRPC(req) {
|
|
261
625
|
switch (req.method) {
|
|
262
626
|
case 'initialize':
|
|
263
|
-
return rpcOk(req.id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'skillvault', version:
|
|
627
|
+
return rpcOk(req.id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'skillvault', version: VERSION } });
|
|
264
628
|
case 'tools/list':
|
|
265
629
|
return rpcOk(req.id, {
|
|
266
630
|
tools: [{
|
|
267
|
-
name: '
|
|
268
|
-
description: '
|
|
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.',
|
|
269
633
|
inputSchema: {
|
|
270
634
|
type: 'object',
|
|
271
635
|
properties: {
|
|
272
|
-
skill_name: { type: 'string', description: 'Name of the skill to
|
|
273
|
-
request: { type: 'string', description: 'What to accomplish with this skill' },
|
|
636
|
+
skill_name: { type: 'string', description: 'Name of the skill to load (from the stub SKILL.md)' },
|
|
274
637
|
},
|
|
275
|
-
required: ['skill_name'
|
|
638
|
+
required: ['skill_name'],
|
|
276
639
|
},
|
|
277
640
|
}],
|
|
278
641
|
});
|
|
279
642
|
case 'tools/call': {
|
|
280
643
|
const name = req.params?.name;
|
|
281
|
-
const { skill_name
|
|
282
|
-
if (name !== '
|
|
644
|
+
const { skill_name } = req.params?.arguments || {};
|
|
645
|
+
if (name !== 'skillvault_load')
|
|
283
646
|
return rpcErr(req.id, -32601, `Unknown tool: ${name}`);
|
|
284
|
-
if (!skill_name
|
|
285
|
-
return rpcErr(req.id, -32602, 'skill_name
|
|
286
|
-
console.log(`[MCP]
|
|
287
|
-
const result = await
|
|
288
|
-
return rpcOk(req.id, {
|
|
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
|
+
});
|
|
289
655
|
}
|
|
290
656
|
case 'notifications/initialized': return '';
|
|
291
657
|
default: return rpcErr(req.id || 0, -32601, `Unknown method: ${req.method}`);
|
|
@@ -293,7 +659,6 @@ async function handleRPC(req) {
|
|
|
293
659
|
}
|
|
294
660
|
function startServer() {
|
|
295
661
|
const server = createServer(async (req, res) => {
|
|
296
|
-
// No CORS headers — MCP server is local-only, no browser access allowed
|
|
297
662
|
if (req.method === 'OPTIONS') {
|
|
298
663
|
res.writeHead(403);
|
|
299
664
|
res.end();
|
|
@@ -306,15 +671,11 @@ function startServer() {
|
|
|
306
671
|
}
|
|
307
672
|
const chunks = [];
|
|
308
673
|
let size = 0;
|
|
309
|
-
const MAX_BODY = 1024 * 1024;
|
|
310
|
-
req.on('data', (c) => {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
chunks.push(c);
|
|
317
|
-
});
|
|
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); });
|
|
318
679
|
req.on('end', async () => {
|
|
319
680
|
if (size > MAX_BODY) {
|
|
320
681
|
res.writeHead(413);
|
|
@@ -341,8 +702,37 @@ function startServer() {
|
|
|
341
702
|
});
|
|
342
703
|
server.listen(MCP_PORT, '127.0.0.1', () => {
|
|
343
704
|
console.log(`🔐 SkillVault MCP server running on 127.0.0.1:${MCP_PORT}`);
|
|
344
|
-
|
|
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.`);
|
|
345
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);
|
|
346
736
|
});
|
|
347
737
|
server.on('error', (err) => {
|
|
348
738
|
if (err.code === 'EADDRINUSE') {
|
|
@@ -356,10 +746,39 @@ function startServer() {
|
|
|
356
746
|
async function main() {
|
|
357
747
|
if (inviteCode) {
|
|
358
748
|
await setup(inviteCode);
|
|
749
|
+
if (!statusFlag && !refreshFlag)
|
|
750
|
+
process.exit(0);
|
|
751
|
+
}
|
|
752
|
+
if (statusFlag) {
|
|
753
|
+
await showStatus();
|
|
754
|
+
process.exit(0);
|
|
755
|
+
}
|
|
756
|
+
if (refreshFlag) {
|
|
757
|
+
await refreshTokens();
|
|
758
|
+
await syncSkills();
|
|
759
|
+
const result = await installSkillStubs();
|
|
760
|
+
if (result.installed > 0)
|
|
761
|
+
console.log(` ✅ ${result.installed} stub${result.installed !== 1 ? 's' : ''} updated`);
|
|
762
|
+
console.log('');
|
|
763
|
+
process.exit(0);
|
|
359
764
|
}
|
|
765
|
+
if (syncFlag) {
|
|
766
|
+
const config = loadConfig();
|
|
767
|
+
if (!config) {
|
|
768
|
+
console.log('🔐 Not set up. Run: npx skillvault --invite YOUR_CODE\n');
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
console.log('🔐 SkillVault Sync\n');
|
|
772
|
+
await syncSkills();
|
|
773
|
+
const result = await installSkillStubs();
|
|
774
|
+
console.log(`\n ✅ ${result.installed} installed, ${result.skipped} up to date`);
|
|
775
|
+
console.log('');
|
|
776
|
+
process.exit(0);
|
|
777
|
+
}
|
|
778
|
+
// Default: start MCP server
|
|
360
779
|
const config = loadConfig();
|
|
361
780
|
if (!config) {
|
|
362
|
-
console.log('🔐 SkillVault
|
|
781
|
+
console.log('🔐 SkillVault\n');
|
|
363
782
|
console.log(' Not set up yet. Run:');
|
|
364
783
|
console.log(' npx skillvault --invite YOUR_CODE\n');
|
|
365
784
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillvault",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "SkillVault
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "SkillVault — secure skill distribution for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"skillvault": "dist/cli.js"
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
],
|
|
16
16
|
"keywords": [
|
|
17
17
|
"skillvault",
|
|
18
|
-
"mcp",
|
|
19
18
|
"claude",
|
|
20
19
|
"claude-code",
|
|
21
20
|
"ai-skills",
|
|
21
|
+
"skill-distribution",
|
|
22
22
|
"encrypted-skills"
|
|
23
23
|
],
|
|
24
24
|
"license": "MIT"
|