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.
Files changed (2) hide show
  1. package/dist/cli.js +168 -20
  2. 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.5.3';
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
- try {
239
- const token = config.customer_token || (config.publishers.length > 0 ? config.publishers[0].token : null);
240
- if (token) {
241
- const res = await fetch(`${config.api_url}/customer/skills`, {
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
- skills = data.skills || [];
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(' ⚠️ Session expired. Run: npx skillvault --refresh\n');
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 — re-invite required');
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' : `failed (${res.status})`}`);
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)}&current_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: Not configured. Run: npx skillvault --invite YOUR_CODE');
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
- const resolved = resolveSkillPublisher(skillName, config);
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: Vault not found for "${skillName}". Run: npx skillvault --sync`);
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
- console.error(`Error: License check failed — ${err instanceof Error ? err.message : 'unknown'}`);
672
- console.error('Your license may have expired or been revoked. Contact your skill provider.');
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillvault",
3
- "version": "0.5.3",
3
+ "version": "0.7.1",
4
4
  "description": "SkillVault — secure skill distribution for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {