skillvault 0.5.3 → 0.7.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.js +168 -20
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
22
|
import { createDecipheriv, createPublicKey, diffieHellman, hkdfSync, generateKeyPairSync, } from 'node:crypto';
|
|
23
|
-
const VERSION = '0.
|
|
23
|
+
const VERSION = '0.7.1';
|
|
24
24
|
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
25
25
|
const API_URL = process.env.SKILLVAULT_API_URL || 'https://api.getskillvault.com';
|
|
26
26
|
const CONFIG_DIR = join(HOME, '.skillvault');
|
|
@@ -107,6 +107,19 @@ async function setup(code) {
|
|
|
107
107
|
if (!response.ok) {
|
|
108
108
|
const err = await response.json().catch(() => ({ message: response.statusText }));
|
|
109
109
|
console.error(` ❌ Failed: ${err.message}`);
|
|
110
|
+
console.error('');
|
|
111
|
+
if (err.message.includes('404') || err.message.includes('not found')) {
|
|
112
|
+
console.error(' The invite code was not found. Check that you entered it correctly.');
|
|
113
|
+
console.error(' Invite codes are 8 characters (e.g. A1B2C3D4).');
|
|
114
|
+
}
|
|
115
|
+
else if (err.message.includes('400') || err.message.includes('expired')) {
|
|
116
|
+
console.error(' This invite code has already been used or has expired.');
|
|
117
|
+
console.error(' Ask the publisher for a new invite code.');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.error(' Could not reach the SkillVault server. Check your internet connection.');
|
|
121
|
+
console.error(` Server: ${API_URL}`);
|
|
122
|
+
}
|
|
110
123
|
process.exit(1);
|
|
111
124
|
}
|
|
112
125
|
const data = await response.json();
|
|
@@ -235,24 +248,33 @@ async function showStatus() {
|
|
|
235
248
|
console.log('');
|
|
236
249
|
let skills = [];
|
|
237
250
|
let online = false;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const res = await fetch(`${config.api_url}/
|
|
242
|
-
headers: { 'Authorization': `Bearer ${token}` },
|
|
251
|
+
// Fetch skills from each publisher using companion tokens
|
|
252
|
+
for (const pub of config.publishers) {
|
|
253
|
+
try {
|
|
254
|
+
const res = await fetch(`${config.api_url}/agent/skills?publisher_id=${encodeURIComponent(pub.id)}`, {
|
|
255
|
+
headers: { 'Authorization': `Bearer ${pub.token}` },
|
|
243
256
|
signal: AbortSignal.timeout(5000),
|
|
244
257
|
});
|
|
245
258
|
if (res.ok) {
|
|
246
259
|
const data = await res.json();
|
|
247
|
-
|
|
260
|
+
for (const s of (data.skills || [])) {
|
|
261
|
+
skills.push({
|
|
262
|
+
skill_name: s.skill_name,
|
|
263
|
+
publisher_id: pub.id,
|
|
264
|
+
publisher_name: pub.name,
|
|
265
|
+
status: s.status || 'active',
|
|
266
|
+
expires_at: null,
|
|
267
|
+
last_used: null,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
248
270
|
online = true;
|
|
249
271
|
}
|
|
250
272
|
else if (res.status === 401) {
|
|
251
|
-
console.log(
|
|
273
|
+
console.log(` ⚠️ Token expired for ${pub.name}. Run: npx skillvault --refresh\n`);
|
|
252
274
|
}
|
|
253
275
|
}
|
|
276
|
+
catch { }
|
|
254
277
|
}
|
|
255
|
-
catch { }
|
|
256
278
|
console.log(' Publishers:');
|
|
257
279
|
console.log(' ' + '-'.repeat(60));
|
|
258
280
|
console.log(` ${'Name'.padEnd(25)} ${'Skills'.padEnd(10)} ${'Status'.padEnd(15)}`);
|
|
@@ -318,14 +340,14 @@ async function refreshTokens() {
|
|
|
318
340
|
anyRefreshed = true;
|
|
319
341
|
}
|
|
320
342
|
else if (res.status === 401) {
|
|
321
|
-
console.error('❌ expired —
|
|
343
|
+
console.error('❌ expired — ask the publisher for a new invite code');
|
|
322
344
|
}
|
|
323
345
|
else {
|
|
324
|
-
console.error(`❌ server error (${res.status})
|
|
346
|
+
console.error(`❌ server error (${res.status}). Try again later or contact the publisher.`);
|
|
325
347
|
}
|
|
326
348
|
}
|
|
327
349
|
catch {
|
|
328
|
-
console.error('❌ offline');
|
|
350
|
+
console.error('❌ offline — check your internet connection');
|
|
329
351
|
}
|
|
330
352
|
}
|
|
331
353
|
if (anyRefreshed) {
|
|
@@ -348,12 +370,12 @@ async function syncSkills() {
|
|
|
348
370
|
mkdirSync(pubVaultDir, { recursive: true });
|
|
349
371
|
let skills = [];
|
|
350
372
|
try {
|
|
351
|
-
const res = await fetch(`${config.api_url}/skills?publisher_id=${encodeURIComponent(pub.id)}`, {
|
|
373
|
+
const res = await fetch(`${config.api_url}/agent/skills?publisher_id=${encodeURIComponent(pub.id)}`, {
|
|
352
374
|
headers: { 'Authorization': `Bearer ${pub.token}` },
|
|
353
375
|
signal: AbortSignal.timeout(10000),
|
|
354
376
|
});
|
|
355
377
|
if (!res.ok) {
|
|
356
|
-
errors.push(`${pub.name}: ${res.status === 401 ? 'auth expired' : `
|
|
378
|
+
errors.push(`${pub.name}: ${res.status === 401 ? 'auth expired — run npx skillvault --refresh' : `server returned ${res.status}`}`);
|
|
357
379
|
continue;
|
|
358
380
|
}
|
|
359
381
|
const data = await res.json();
|
|
@@ -642,25 +664,128 @@ function watermark(content, id) {
|
|
|
642
664
|
function validateSkillName(name) {
|
|
643
665
|
return /^[a-zA-Z0-9_-]+$/.test(name) && name.length > 0 && name.length <= 128;
|
|
644
666
|
}
|
|
667
|
+
/**
|
|
668
|
+
* Quick sync for a single skill — checks for vault update before decrypting.
|
|
669
|
+
* Returns true if the vault was updated. Status goes to stderr.
|
|
670
|
+
*/
|
|
671
|
+
async function syncSingleSkill(skillName, pub, config) {
|
|
672
|
+
try {
|
|
673
|
+
const capabilityName = `skill/${skillName.toLowerCase()}`;
|
|
674
|
+
const res = await fetch(`${config.api_url}/skills/check-update?capability=${encodeURIComponent(capabilityName)}¤t_version=0.0.0`, { signal: AbortSignal.timeout(5000) });
|
|
675
|
+
if (!res.ok)
|
|
676
|
+
return false;
|
|
677
|
+
const data = await res.json();
|
|
678
|
+
// Check if local vault hash matches
|
|
679
|
+
const vaultPath = join(VAULT_DIR, pub.id, `${skillName}.vault`);
|
|
680
|
+
const hashPath = vaultPath + '.hash';
|
|
681
|
+
if (existsSync(hashPath) && data.vault_hash) {
|
|
682
|
+
const localHash = readFileSync(hashPath, 'utf8').trim();
|
|
683
|
+
if (localHash === data.vault_hash)
|
|
684
|
+
return false; // already up to date
|
|
685
|
+
}
|
|
686
|
+
// Download updated vault
|
|
687
|
+
const dlRes = await fetch(`${config.api_url}/skills/download?capability=${encodeURIComponent(capabilityName)}`, { headers: { 'Authorization': `Bearer ${pub.token}` }, signal: AbortSignal.timeout(15000) });
|
|
688
|
+
if (!dlRes.ok)
|
|
689
|
+
return false;
|
|
690
|
+
const dlData = await dlRes.json();
|
|
691
|
+
const vaultBuffer = Buffer.from(dlData.vault_data, 'base64');
|
|
692
|
+
mkdirSync(join(VAULT_DIR, pub.id), { recursive: true });
|
|
693
|
+
writeFileSync(vaultPath, vaultBuffer, { mode: 0o600 });
|
|
694
|
+
if (dlData.vault_hash)
|
|
695
|
+
writeFileSync(hashPath, dlData.vault_hash, { mode: 0o600 });
|
|
696
|
+
writeFileSync(vaultPath + '.meta', JSON.stringify({
|
|
697
|
+
skill_name: skillName,
|
|
698
|
+
description: '',
|
|
699
|
+
capability_name: capabilityName,
|
|
700
|
+
version: dlData.version || data.latest_version,
|
|
701
|
+
publisher_name: pub.name,
|
|
702
|
+
publisher_id: pub.id,
|
|
703
|
+
}), { mode: 0o600 });
|
|
704
|
+
console.error(`[sync] Updated "${skillName}" to v${dlData.version || data.latest_version}`);
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return false; // sync failure is non-fatal — use existing vault
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Background sync for all skills across all publishers.
|
|
713
|
+
* Discovers new skills the customer has been granted since last sync.
|
|
714
|
+
* Runs async — doesn't block the load operation.
|
|
715
|
+
*/
|
|
716
|
+
async function backgroundSyncAll(config) {
|
|
717
|
+
try {
|
|
718
|
+
await syncSkills();
|
|
719
|
+
await installSkillStubs();
|
|
720
|
+
}
|
|
721
|
+
catch { } // non-fatal
|
|
722
|
+
}
|
|
645
723
|
/**
|
|
646
724
|
* Load (decrypt) a skill and output to stdout.
|
|
725
|
+
* Syncs the requested skill first to ensure latest version.
|
|
726
|
+
* Triggers background sync for all other skills.
|
|
647
727
|
* Status messages go to stderr so they don't pollute the skill content.
|
|
648
728
|
*/
|
|
649
729
|
async function loadSkill(skillName) {
|
|
650
730
|
if (!validateSkillName(skillName)) {
|
|
651
|
-
console.error('Error: Invalid skill name.');
|
|
731
|
+
console.error('Error: Invalid skill name. Skill names can only contain letters, numbers, hyphens, and underscores (max 128 chars).');
|
|
732
|
+
console.error('Example: npx skillvault --load my-skill-name');
|
|
652
733
|
process.exit(1);
|
|
653
734
|
}
|
|
654
735
|
const config = loadConfig();
|
|
655
736
|
if (!config) {
|
|
656
|
-
console.error('Error:
|
|
737
|
+
console.error('Error: SkillVault is not configured on this machine.');
|
|
738
|
+
console.error('');
|
|
739
|
+
console.error('To set up, you need an invite code from a skill publisher.');
|
|
740
|
+
console.error('Run: npx skillvault --invite YOUR_INVITE_CODE');
|
|
741
|
+
console.error('');
|
|
742
|
+
console.error('If you already set up SkillVault, the config file may be missing:');
|
|
743
|
+
console.error(` Expected: ${CONFIG_PATH}`);
|
|
657
744
|
process.exit(1);
|
|
658
745
|
}
|
|
659
|
-
|
|
746
|
+
// Pre-load sync: ensure we have the latest vault for this skill
|
|
747
|
+
let resolved = resolveSkillPublisher(skillName, config);
|
|
748
|
+
if (resolved) {
|
|
749
|
+
await syncSingleSkill(skillName, resolved.publisher, config);
|
|
750
|
+
// Re-resolve in case the vault was just downloaded
|
|
751
|
+
resolved = resolveSkillPublisher(skillName, config);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
// Skill not found locally — try a full sync first (may be a newly granted skill)
|
|
755
|
+
console.error(`[sync] Skill "${skillName}" not found locally, syncing...`);
|
|
756
|
+
await syncSkills();
|
|
757
|
+
await installSkillStubs();
|
|
758
|
+
resolved = resolveSkillPublisher(skillName, config);
|
|
759
|
+
}
|
|
660
760
|
if (!resolved) {
|
|
661
|
-
console.error(`Error:
|
|
761
|
+
console.error(`Error: Skill "${skillName}" not found after syncing with server.`);
|
|
762
|
+
console.error('');
|
|
763
|
+
console.error('Possible causes:');
|
|
764
|
+
console.error(' 1. You don\'t have a license for this skill — ask the publisher for an invite');
|
|
765
|
+
console.error(' 2. The skill name is misspelled — check the exact name with: npx skillvault --status');
|
|
766
|
+
console.error(' 3. Your token expired — refresh with: npx skillvault --refresh');
|
|
767
|
+
console.error('');
|
|
768
|
+
console.error('Available skills on this machine:');
|
|
769
|
+
const localConfig = loadConfig();
|
|
770
|
+
if (localConfig) {
|
|
771
|
+
for (const pub of localConfig.publishers) {
|
|
772
|
+
const pubVaultDir = join(VAULT_DIR, pub.id);
|
|
773
|
+
try {
|
|
774
|
+
if (existsSync(pubVaultDir)) {
|
|
775
|
+
const vaults = readdirSync(pubVaultDir).filter(f => f.endsWith('.vault'));
|
|
776
|
+
for (const v of vaults)
|
|
777
|
+
console.error(` - ${v.replace('.vault', '')} (from ${pub.name})`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch { }
|
|
781
|
+
}
|
|
782
|
+
if (localConfig.publishers.length === 0)
|
|
783
|
+
console.error(' (none — no publishers configured)');
|
|
784
|
+
}
|
|
662
785
|
process.exit(1);
|
|
663
786
|
}
|
|
787
|
+
// Kick off background sync for all other skills (non-blocking)
|
|
788
|
+
backgroundSyncAll(config).catch(() => { });
|
|
664
789
|
const licenseeId = config.customer_email || 'unknown';
|
|
665
790
|
// Fetch CEK — validates license on every load
|
|
666
791
|
let cek;
|
|
@@ -668,8 +793,27 @@ async function loadSkill(skillName) {
|
|
|
668
793
|
cek = await fetchCEK(skillName, resolved.publisher.token);
|
|
669
794
|
}
|
|
670
795
|
catch (err) {
|
|
671
|
-
|
|
672
|
-
console.error(
|
|
796
|
+
const errMsg = err instanceof Error ? err.message : 'unknown';
|
|
797
|
+
console.error(`Error: License check failed for "${skillName}" — ${errMsg}`);
|
|
798
|
+
console.error('');
|
|
799
|
+
if (errMsg.includes('403') || errMsg.includes('no_license')) {
|
|
800
|
+
console.error('Your license for this skill has been revoked or expired.');
|
|
801
|
+
console.error('Contact the skill publisher to request a new license.');
|
|
802
|
+
}
|
|
803
|
+
else if (errMsg.includes('401')) {
|
|
804
|
+
console.error('Your authentication token has expired.');
|
|
805
|
+
console.error('Fix: npx skillvault --refresh');
|
|
806
|
+
}
|
|
807
|
+
else if (errMsg.includes('fetch') || errMsg.includes('ECONNREFUSED')) {
|
|
808
|
+
console.error('Could not reach the SkillVault server. Check your internet connection.');
|
|
809
|
+
console.error(`Server: ${API_URL}`);
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
console.error('The server rejected the license check. This could mean:');
|
|
813
|
+
console.error(' - Your license was revoked');
|
|
814
|
+
console.error(' - Your token expired (fix: npx skillvault --refresh)');
|
|
815
|
+
console.error(' - The server is temporarily unavailable');
|
|
816
|
+
}
|
|
673
817
|
process.exit(1);
|
|
674
818
|
}
|
|
675
819
|
// Decrypt in memory
|
|
@@ -690,6 +834,10 @@ async function loadSkill(skillName) {
|
|
|
690
834
|
catch (err) {
|
|
691
835
|
cek.fill(0);
|
|
692
836
|
console.error(`Error: Decryption failed — ${err instanceof Error ? err.message : 'unknown'}`);
|
|
837
|
+
console.error('');
|
|
838
|
+
console.error('The vault file may be corrupted or the CEK may not match.');
|
|
839
|
+
console.error('Try re-syncing: npx skillvault --sync');
|
|
840
|
+
console.error('If the problem persists, the skill may need to be republished by the publisher.');
|
|
693
841
|
process.exit(1);
|
|
694
842
|
}
|
|
695
843
|
}
|