groove-dev 0.14.0 → 0.14.2

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.
@@ -367,8 +367,8 @@ export function createApi(app, daemon) {
367
367
 
368
368
  // --- Skills Marketplace ---
369
369
 
370
- app.get('/api/skills/registry', (req, res) => {
371
- const skills = daemon.skills.getRegistry({
370
+ app.get('/api/skills/registry', async (req, res) => {
371
+ const skills = await daemon.skills.getRegistry({
372
372
  search: req.query.search || '',
373
373
  category: req.query.category || 'all',
374
374
  });
@@ -7,7 +7,19 @@ import { fileURLToPath } from 'url';
7
7
 
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
 
10
- const REMOTE_REGISTRY = 'https://docs.groovedev.ai/skills/registry.json';
10
+ const SKILLS_API = 'https://docs.groovedev.ai/api/v1';
11
+
12
+ // Normalize snake_case API fields to camelCase used by GUI
13
+ function normalize(skill) {
14
+ return {
15
+ ...skill,
16
+ ratingCount: skill.ratingCount ?? skill.rating_count ?? 0,
17
+ contentUrl: skill.contentUrl ?? skill.content_url ?? null,
18
+ authorId: skill.authorId ?? skill.author_id ?? null,
19
+ createdAt: skill.createdAt ?? skill.created_at ?? null,
20
+ updatedAt: skill.updatedAt ?? skill.updated_at ?? null,
21
+ };
22
+ }
11
23
 
12
24
  export class SkillStore {
13
25
  constructor(daemon) {
@@ -22,39 +34,58 @@ export class SkillStore {
22
34
  this.registry = JSON.parse(readFileSync(regPath, 'utf8'));
23
35
  } catch { /* no registry file */ }
24
36
 
25
- // Fetch remote registry in background (auto-update)
37
+ // Fetch full registry from live API in background
26
38
  this._refreshRegistry();
27
39
  }
28
40
 
29
41
  async _refreshRegistry() {
30
42
  try {
31
- const res = await fetch(REMOTE_REGISTRY, { signal: AbortSignal.timeout(5000) });
43
+ const res = await fetch(`${SKILLS_API}/skills?limit=200`, { signal: AbortSignal.timeout(5000) });
32
44
  if (res.ok) {
33
- this.registry = await res.json();
45
+ const data = await res.json();
46
+ this.registry = (data.skills || data).map(normalize);
34
47
  }
35
48
  } catch { /* offline — use bundled */ }
36
49
  }
37
50
 
38
51
  /**
39
- * Get all skills from the registry with installed status.
52
+ * Get skills from the live API, with local fallback.
53
+ * Server handles search + category filtering when online.
40
54
  */
41
- getRegistry(query) {
55
+ async getRegistry(query) {
56
+ // Try live API first — server handles search/filter/sort
57
+ try {
58
+ const params = new URLSearchParams();
59
+ if (query?.search) params.set('search', query.search);
60
+ if (query?.category && query.category !== 'all') params.set('category', query.category);
61
+ params.set('limit', '200');
62
+
63
+ const res = await fetch(`${SKILLS_API}/skills?${params}`, { signal: AbortSignal.timeout(5000) });
64
+ if (res.ok) {
65
+ const data = await res.json();
66
+ const skills = (data.skills || data).map((s) => ({
67
+ ...normalize(s),
68
+ installed: this._isInstalled(s.id),
69
+ }));
70
+ return skills;
71
+ }
72
+ } catch { /* fall through to local */ }
73
+
74
+ // Offline fallback — filter locally from cached registry
42
75
  let skills = this.registry.map((s) => ({
43
76
  ...s,
44
77
  installed: this._isInstalled(s.id),
45
78
  }));
46
79
 
47
- // Search filter
48
80
  if (query?.search) {
49
81
  const q = query.search.toLowerCase();
50
82
  skills = skills.filter((s) =>
51
83
  s.name.toLowerCase().includes(q)
52
84
  || s.description.toLowerCase().includes(q)
53
- || s.tags.some((t) => t.includes(q))
85
+ || (s.tags || []).some((t) => t.includes(q))
54
86
  );
55
87
  }
56
88
 
57
- // Category filter
58
89
  if (query?.category && query.category !== 'all') {
59
90
  skills = skills.filter((s) => s.category === query.category);
60
91
  }
@@ -97,8 +128,8 @@ export class SkillStore {
97
128
  }
98
129
 
99
130
  /**
100
- * Install a skill from the registry.
101
- * Downloads SKILL.md from remote URL, falls back to local Claude plugins.
131
+ * Install a skill.
132
+ * Downloads content from live API, falls back to contentUrl, then local plugins.
102
133
  */
103
134
  async install(skillId) {
104
135
  const entry = this.registry.find((s) => s.id === skillId);
@@ -107,12 +138,21 @@ export class SkillStore {
107
138
 
108
139
  let content = null;
109
140
 
110
- // Try remote download first
111
- if (entry.contentUrl) {
141
+ // Try live API content endpoint first
142
+ try {
143
+ const res = await fetch(`${SKILLS_API}/skills/${skillId}/content`, { signal: AbortSignal.timeout(10000) });
144
+ if (res.ok) {
145
+ const data = await res.json();
146
+ content = data.content;
147
+ }
148
+ } catch { /* fall through */ }
149
+
150
+ // Fall back to contentUrl from registry entry
151
+ if (!content && entry.contentUrl) {
112
152
  try {
113
153
  const res = await fetch(entry.contentUrl, { signal: AbortSignal.timeout(10000) });
114
154
  if (res.ok) content = await res.text();
115
- } catch { /* fall through to local */ }
155
+ } catch { /* fall through */ }
116
156
  }
117
157
 
118
158
  // Fall back to local Claude plugins
@@ -129,6 +169,12 @@ export class SkillStore {
129
169
  mkdirSync(skillDir, { recursive: true });
130
170
  writeFileSync(resolve(skillDir, 'SKILL.md'), content);
131
171
 
172
+ // Track install on server (fire-and-forget, no auth needed)
173
+ fetch(`${SKILLS_API}/skills/${skillId}/install`, {
174
+ method: 'POST',
175
+ signal: AbortSignal.timeout(5000),
176
+ }).catch(() => {});
177
+
132
178
  this.daemon.audit.log('skill.install', { id: skillId, name: entry.name });
133
179
 
134
180
  return { id: skillId, name: entry.name, installed: true };