groove-dev 0.14.1 → 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
|
|
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
|
|
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(
|
|
43
|
+
const res = await fetch(`${SKILLS_API}/skills?limit=200`, { signal: AbortSignal.timeout(5000) });
|
|
32
44
|
if (res.ok) {
|
|
33
|
-
|
|
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
|
|
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
|
|
101
|
-
* Downloads
|
|
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
|
|
111
|
-
|
|
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
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "Open-source agent orchestration layer for AI coding tools. GUI dashboard, multi-agent coordination, zero cold-start (Journalist), infinite sessions (adaptive context rotation), AI Project Manager, Quick Launch. Works with Claude Code, Codex, Gemini CLI, Ollama.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -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
|
|
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
|
|
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(
|
|
43
|
+
const res = await fetch(`${SKILLS_API}/skills?limit=200`, { signal: AbortSignal.timeout(5000) });
|
|
32
44
|
if (res.ok) {
|
|
33
|
-
|
|
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
|
|
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
|
|
101
|
-
* Downloads
|
|
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
|
|
111
|
-
|
|
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
|
|
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 };
|