opencode-provider-litellm 0.3.0 → 0.3.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/package.json +1 -1
- package/src/skills.test.ts +357 -81
- package/src/skills.ts +190 -55
- package/src/types.ts +18 -3
package/package.json
CHANGED
package/src/skills.test.ts
CHANGED
|
@@ -3,13 +3,37 @@ import { tool } from '@opencode-ai/plugin'
|
|
|
3
3
|
import type { PluginConfig, Skill } from './types.js'
|
|
4
4
|
import {
|
|
5
5
|
listSkills,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
listPublicSkills,
|
|
7
|
+
registerSkill,
|
|
8
|
+
enableSkill,
|
|
9
|
+
disableSkill,
|
|
10
|
+
fetchSkillContent,
|
|
8
11
|
createSkillToolDefinitions,
|
|
9
12
|
createSkillsInjector,
|
|
10
13
|
resetSkillsCache,
|
|
11
14
|
} from './skills.js'
|
|
12
15
|
|
|
16
|
+
const mockSkill: Skill = {
|
|
17
|
+
id: 'skill-1',
|
|
18
|
+
name: 'code-review',
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
description: 'Reviews code for best practices',
|
|
21
|
+
source: {
|
|
22
|
+
source: 'git-subdir',
|
|
23
|
+
url: 'https://github.com/org/repo',
|
|
24
|
+
path: 'skills/code-review',
|
|
25
|
+
},
|
|
26
|
+
author: null,
|
|
27
|
+
homepage: null,
|
|
28
|
+
keywords: null,
|
|
29
|
+
category: null,
|
|
30
|
+
domain: null,
|
|
31
|
+
namespace: null,
|
|
32
|
+
enabled: true,
|
|
33
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
34
|
+
updated_at: '2026-01-01T00:00:00Z',
|
|
35
|
+
}
|
|
36
|
+
|
|
13
37
|
describe('listSkills', () => {
|
|
14
38
|
const config: PluginConfig = {
|
|
15
39
|
url: 'https://litellm.example.com',
|
|
@@ -21,24 +45,21 @@ describe('listSkills', () => {
|
|
|
21
45
|
vi.restoreAllMocks()
|
|
22
46
|
})
|
|
23
47
|
|
|
24
|
-
it('returns skills from
|
|
25
|
-
const
|
|
26
|
-
{ id: 'skill-1', name: 'code-review', description: 'Reviews code for best practices', enabled: true },
|
|
27
|
-
{ id: 'skill-2', name: 'security-scan', description: 'Scans for security issues', enabled: true },
|
|
28
|
-
]
|
|
48
|
+
it('returns skills from plugins response', async () => {
|
|
49
|
+
const mockPlugins = { plugins: [mockSkill] }
|
|
29
50
|
|
|
30
51
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
31
52
|
ok: true,
|
|
32
53
|
status: 200,
|
|
33
|
-
json: async () =>
|
|
54
|
+
json: async () => mockPlugins,
|
|
34
55
|
})
|
|
35
56
|
vi.stubGlobal('fetch', mockFetch)
|
|
36
57
|
|
|
37
58
|
const result = await listSkills(config, token)
|
|
38
59
|
|
|
39
|
-
expect(result).toEqual(
|
|
60
|
+
expect(result).toEqual([mockSkill])
|
|
40
61
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
41
|
-
'https://litellm.example.com/
|
|
62
|
+
'https://litellm.example.com/claude-code/plugins',
|
|
42
63
|
expect.objectContaining({
|
|
43
64
|
method: 'GET',
|
|
44
65
|
headers: expect.objectContaining({
|
|
@@ -68,6 +89,18 @@ describe('listSkills', () => {
|
|
|
68
89
|
expect(result).toEqual([])
|
|
69
90
|
})
|
|
70
91
|
|
|
92
|
+
it('returns [] on invalid response format', async () => {
|
|
93
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
94
|
+
ok: true,
|
|
95
|
+
status: 200,
|
|
96
|
+
json: async () => { plugins: null },
|
|
97
|
+
})
|
|
98
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
99
|
+
|
|
100
|
+
const result = await listSkills(config, token)
|
|
101
|
+
expect(result).toEqual([])
|
|
102
|
+
})
|
|
103
|
+
|
|
71
104
|
it('respects timeout (AbortError after 10s)', async () => {
|
|
72
105
|
vi.useFakeTimers()
|
|
73
106
|
|
|
@@ -95,7 +128,48 @@ describe('listSkills', () => {
|
|
|
95
128
|
})
|
|
96
129
|
})
|
|
97
130
|
|
|
98
|
-
describe('
|
|
131
|
+
describe('listPublicSkills', () => {
|
|
132
|
+
const config: PluginConfig = {
|
|
133
|
+
url: 'https://litellm.example.com',
|
|
134
|
+
apiKey: 'test-api-key',
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
vi.restoreAllMocks()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('returns public skills without auth', async () => {
|
|
142
|
+
const mockResponse = { plugins: [mockSkill] }
|
|
143
|
+
|
|
144
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
145
|
+
ok: true,
|
|
146
|
+
status: 200,
|
|
147
|
+
json: async () => mockResponse,
|
|
148
|
+
})
|
|
149
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
150
|
+
|
|
151
|
+
const result = await listPublicSkills(config)
|
|
152
|
+
|
|
153
|
+
expect(result).toEqual([mockSkill])
|
|
154
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
155
|
+
'https://litellm.example.com/public/skill_hub',
|
|
156
|
+
expect.objectContaining({
|
|
157
|
+
method: 'GET',
|
|
158
|
+
signal: expect.any(AbortSignal),
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('returns [] on error', async () => {
|
|
164
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('network error'))
|
|
165
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
166
|
+
|
|
167
|
+
const result = await listPublicSkills(config)
|
|
168
|
+
expect(result).toEqual([])
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('registerSkill', () => {
|
|
99
173
|
const config: PluginConfig = {
|
|
100
174
|
url: 'https://litellm.example.com',
|
|
101
175
|
apiKey: 'test-api-key',
|
|
@@ -110,22 +184,42 @@ describe('createSkill', () => {
|
|
|
110
184
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
111
185
|
ok: true,
|
|
112
186
|
status: 200,
|
|
113
|
-
json: async () => ({
|
|
187
|
+
json: async () => ({
|
|
188
|
+
status: 'success',
|
|
189
|
+
action: 'created',
|
|
190
|
+
plugin: { id: 'new-skill-1', name: 'my-skill' },
|
|
191
|
+
}),
|
|
114
192
|
})
|
|
115
193
|
vi.stubGlobal('fetch', mockFetch)
|
|
116
194
|
|
|
117
|
-
const result = await
|
|
195
|
+
const result = await registerSkill(
|
|
196
|
+
config,
|
|
197
|
+
token,
|
|
198
|
+
'my-skill',
|
|
199
|
+
'https://github.com/org/repo',
|
|
200
|
+
'skills/my-skill',
|
|
201
|
+
'A test skill',
|
|
202
|
+
)
|
|
118
203
|
|
|
119
|
-
expect(result).toBe('Skill "my-skill"
|
|
204
|
+
expect(result).toBe('Skill "my-skill" registered (id: new-skill-1)')
|
|
120
205
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
121
|
-
'https://litellm.example.com/
|
|
206
|
+
'https://litellm.example.com/claude-code/plugins',
|
|
122
207
|
expect.objectContaining({
|
|
123
208
|
method: 'POST',
|
|
124
209
|
headers: expect.objectContaining({
|
|
125
210
|
Authorization: 'Bearer test-token',
|
|
126
211
|
'Content-Type': 'application/json',
|
|
127
212
|
}),
|
|
128
|
-
body: JSON.stringify({
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
name: 'my-skill',
|
|
215
|
+
source: {
|
|
216
|
+
source: 'git-subdir',
|
|
217
|
+
url: 'https://github.com/org/repo',
|
|
218
|
+
path: 'skills/my-skill',
|
|
219
|
+
},
|
|
220
|
+
description: 'A test skill',
|
|
221
|
+
domain: null,
|
|
222
|
+
}),
|
|
129
223
|
signal: expect.any(AbortSignal),
|
|
130
224
|
}),
|
|
131
225
|
)
|
|
@@ -135,9 +229,15 @@ describe('createSkill', () => {
|
|
|
135
229
|
const mockFetch = vi.fn().mockRejectedValue(new Error('connection refused'))
|
|
136
230
|
vi.stubGlobal('fetch', mockFetch)
|
|
137
231
|
|
|
138
|
-
const result = await
|
|
232
|
+
const result = await registerSkill(
|
|
233
|
+
config,
|
|
234
|
+
token,
|
|
235
|
+
'my-skill',
|
|
236
|
+
'https://github.com/org/repo',
|
|
237
|
+
'skills/my-skill',
|
|
238
|
+
)
|
|
139
239
|
|
|
140
|
-
expect(result).toBe('Error
|
|
240
|
+
expect(result).toBe('Error registering skill: connection refused')
|
|
141
241
|
})
|
|
142
242
|
|
|
143
243
|
it('returns error string on non-ok response', async () => {
|
|
@@ -147,43 +247,54 @@ describe('createSkill', () => {
|
|
|
147
247
|
})
|
|
148
248
|
vi.stubGlobal('fetch', mockFetch)
|
|
149
249
|
|
|
150
|
-
const result = await
|
|
250
|
+
const result = await registerSkill(
|
|
251
|
+
config,
|
|
252
|
+
token,
|
|
253
|
+
'my-skill',
|
|
254
|
+
'https://github.com/org/repo',
|
|
255
|
+
'skills/my-skill',
|
|
256
|
+
)
|
|
151
257
|
|
|
152
|
-
expect(result).toContain('Error
|
|
258
|
+
expect(result).toContain('Error registering skill')
|
|
153
259
|
})
|
|
154
260
|
|
|
155
|
-
it('includes
|
|
261
|
+
it('includes domain when provided', async () => {
|
|
156
262
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
157
263
|
ok: true,
|
|
158
264
|
status: 200,
|
|
159
|
-
json: async () => ({ id: 'skill-3', name: '
|
|
265
|
+
json: async () => ({ plugin: { id: 'skill-3', name: 'domain-skill' } }),
|
|
160
266
|
})
|
|
161
267
|
vi.stubGlobal('fetch', mockFetch)
|
|
162
268
|
|
|
163
|
-
await
|
|
269
|
+
await registerSkill(
|
|
164
270
|
config,
|
|
165
271
|
token,
|
|
166
|
-
'
|
|
167
|
-
'
|
|
168
|
-
|
|
169
|
-
'
|
|
272
|
+
'domain-skill',
|
|
273
|
+
'https://github.com/org/repo',
|
|
274
|
+
'skills/domain-skill',
|
|
275
|
+
'A domain skill',
|
|
276
|
+
'Productivity',
|
|
170
277
|
)
|
|
171
278
|
|
|
172
279
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
173
|
-
'https://litellm.example.com/
|
|
280
|
+
'https://litellm.example.com/claude-code/plugins',
|
|
174
281
|
expect.objectContaining({
|
|
175
282
|
body: JSON.stringify({
|
|
176
|
-
name: '
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
283
|
+
name: 'domain-skill',
|
|
284
|
+
source: {
|
|
285
|
+
source: 'git-subdir',
|
|
286
|
+
url: 'https://github.com/org/repo',
|
|
287
|
+
path: 'skills/domain-skill',
|
|
288
|
+
},
|
|
289
|
+
description: 'A domain skill',
|
|
290
|
+
domain: 'Productivity',
|
|
180
291
|
}),
|
|
181
292
|
}),
|
|
182
293
|
)
|
|
183
294
|
})
|
|
184
295
|
})
|
|
185
296
|
|
|
186
|
-
describe('
|
|
297
|
+
describe('enableSkill', () => {
|
|
187
298
|
const config: PluginConfig = {
|
|
188
299
|
url: 'https://litellm.example.com',
|
|
189
300
|
apiKey: 'test-api-key',
|
|
@@ -201,13 +312,13 @@ describe('deleteSkill', () => {
|
|
|
201
312
|
})
|
|
202
313
|
vi.stubGlobal('fetch', mockFetch)
|
|
203
314
|
|
|
204
|
-
const result = await
|
|
315
|
+
const result = await enableSkill(config, token, 'my-skill')
|
|
205
316
|
|
|
206
|
-
expect(result).toBe('Skill "skill
|
|
317
|
+
expect(result).toBe('Skill "my-skill" enabled')
|
|
207
318
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
208
|
-
'https://litellm.example.com/
|
|
319
|
+
'https://litellm.example.com/claude-code/plugins/my-skill/enable',
|
|
209
320
|
expect.objectContaining({
|
|
210
|
-
method: '
|
|
321
|
+
method: 'POST',
|
|
211
322
|
headers: expect.objectContaining({
|
|
212
323
|
Authorization: 'Bearer test-token',
|
|
213
324
|
}),
|
|
@@ -220,9 +331,9 @@ describe('deleteSkill', () => {
|
|
|
220
331
|
const mockFetch = vi.fn().mockRejectedValue(new Error('connection refused'))
|
|
221
332
|
vi.stubGlobal('fetch', mockFetch)
|
|
222
333
|
|
|
223
|
-
const result = await
|
|
334
|
+
const result = await enableSkill(config, token, 'my-skill')
|
|
224
335
|
|
|
225
|
-
expect(result).toBe('Error
|
|
336
|
+
expect(result).toBe('Error enabling skill: connection refused')
|
|
226
337
|
})
|
|
227
338
|
|
|
228
339
|
it('returns error string on non-ok response', async () => {
|
|
@@ -232,9 +343,122 @@ describe('deleteSkill', () => {
|
|
|
232
343
|
})
|
|
233
344
|
vi.stubGlobal('fetch', mockFetch)
|
|
234
345
|
|
|
235
|
-
const result = await
|
|
346
|
+
const result = await enableSkill(config, token, 'my-skill')
|
|
236
347
|
|
|
237
|
-
expect(result).toContain('Error
|
|
348
|
+
expect(result).toContain('Error enabling skill')
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
describe('disableSkill', () => {
|
|
353
|
+
const config: PluginConfig = {
|
|
354
|
+
url: 'https://litellm.example.com',
|
|
355
|
+
apiKey: 'test-api-key',
|
|
356
|
+
}
|
|
357
|
+
const token = 'test-token'
|
|
358
|
+
|
|
359
|
+
beforeEach(() => {
|
|
360
|
+
vi.restoreAllMocks()
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('returns success message', async () => {
|
|
364
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
365
|
+
ok: true,
|
|
366
|
+
status: 200,
|
|
367
|
+
})
|
|
368
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
369
|
+
|
|
370
|
+
const result = await disableSkill(config, token, 'my-skill')
|
|
371
|
+
|
|
372
|
+
expect(result).toBe('Skill "my-skill" disabled')
|
|
373
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
374
|
+
'https://litellm.example.com/claude-code/plugins/my-skill/disable',
|
|
375
|
+
expect.objectContaining({
|
|
376
|
+
method: 'POST',
|
|
377
|
+
headers: expect.objectContaining({
|
|
378
|
+
Authorization: 'Bearer test-token',
|
|
379
|
+
}),
|
|
380
|
+
signal: expect.any(AbortSignal),
|
|
381
|
+
}),
|
|
382
|
+
)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('returns error string on failure', async () => {
|
|
386
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('connection refused'))
|
|
387
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
388
|
+
|
|
389
|
+
const result = await disableSkill(config, token, 'my-skill')
|
|
390
|
+
|
|
391
|
+
expect(result).toBe('Error disabling skill: connection refused')
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('returns error string on non-ok response', async () => {
|
|
395
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
396
|
+
ok: false,
|
|
397
|
+
status: 404,
|
|
398
|
+
})
|
|
399
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
400
|
+
|
|
401
|
+
const result = await disableSkill(config, token, 'my-skill')
|
|
402
|
+
|
|
403
|
+
expect(result).toContain('Error disabling skill')
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe('fetchSkillContent', () => {
|
|
408
|
+
beforeEach(() => {
|
|
409
|
+
vi.restoreAllMocks()
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('returns SKILL.md content from GitHub', async () => {
|
|
413
|
+
const mockContent = '# Test Skill\n\nThis is a test skill.'
|
|
414
|
+
|
|
415
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
416
|
+
ok: true,
|
|
417
|
+
status: 200,
|
|
418
|
+
text: async () => mockContent,
|
|
419
|
+
})
|
|
420
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
421
|
+
|
|
422
|
+
const result = await fetchSkillContent(mockSkill)
|
|
423
|
+
|
|
424
|
+
expect(result).toBe(mockContent)
|
|
425
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
426
|
+
'https://raw.githubusercontent.com/org/repo/master/skills/code-review/SKILL.md',
|
|
427
|
+
expect.any(Object),
|
|
428
|
+
)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('returns null on non-ok response', async () => {
|
|
432
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
433
|
+
ok: false,
|
|
434
|
+
status: 404,
|
|
435
|
+
})
|
|
436
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
437
|
+
|
|
438
|
+
const result = await fetchSkillContent(mockSkill)
|
|
439
|
+
expect(result).toBeNull()
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('returns null on network error', async () => {
|
|
443
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('network error'))
|
|
444
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
445
|
+
|
|
446
|
+
const result = await fetchSkillContent(mockSkill)
|
|
447
|
+
expect(result).toBeNull()
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('returns null for unsupported source type', async () => {
|
|
451
|
+
const mockFetch = vi.fn()
|
|
452
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
453
|
+
|
|
454
|
+
const unsupportedSkill: Skill = {
|
|
455
|
+
...mockSkill,
|
|
456
|
+
source: { source: 'inline', url: 'not-a-git-url' },
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const result = await fetchSkillContent(unsupportedSkill)
|
|
460
|
+
expect(result).toBeNull()
|
|
461
|
+
expect(mockFetch).not.toHaveBeenCalled()
|
|
238
462
|
})
|
|
239
463
|
})
|
|
240
464
|
|
|
@@ -249,41 +473,50 @@ describe('createSkillToolDefinitions', () => {
|
|
|
249
473
|
vi.restoreAllMocks()
|
|
250
474
|
})
|
|
251
475
|
|
|
252
|
-
it('returns
|
|
476
|
+
it('returns 4 tools with correct names', () => {
|
|
253
477
|
const result = createSkillToolDefinitions(config, token)
|
|
254
478
|
|
|
255
|
-
expect(Object.keys(result)).toHaveLength(
|
|
479
|
+
expect(Object.keys(result)).toHaveLength(4)
|
|
256
480
|
expect(result).toHaveProperty('skill_list')
|
|
257
|
-
expect(result).toHaveProperty('
|
|
258
|
-
expect(result).toHaveProperty('
|
|
481
|
+
expect(result).toHaveProperty('skill_register')
|
|
482
|
+
expect(result).toHaveProperty('skill_enable')
|
|
483
|
+
expect(result).toHaveProperty('skill_disable')
|
|
259
484
|
})
|
|
260
485
|
|
|
261
486
|
it('skill_list has correct description', () => {
|
|
262
487
|
const result = createSkillToolDefinitions(config, token)
|
|
263
488
|
|
|
264
|
-
expect(result.skill_list.description).toBe('List all skills registered on the LiteLLM
|
|
489
|
+
expect(result.skill_list.description).toBe('List all skills registered on the LiteLLM Skills Gateway')
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('skill_register has correct description', () => {
|
|
493
|
+
const result = createSkillToolDefinitions(config, token)
|
|
494
|
+
|
|
495
|
+
expect(result.skill_register.description).toBe('Register a new skill on the LiteLLM Skills Gateway pointing to a git source')
|
|
265
496
|
})
|
|
266
497
|
|
|
267
|
-
it('
|
|
498
|
+
it('skill_enable has correct description', () => {
|
|
268
499
|
const result = createSkillToolDefinitions(config, token)
|
|
269
500
|
|
|
270
|
-
expect(result.
|
|
501
|
+
expect(result.skill_enable.description).toBe('Enable (publish) a skill on the LiteLLM Skills Gateway')
|
|
271
502
|
})
|
|
272
503
|
|
|
273
|
-
it('
|
|
504
|
+
it('skill_disable has correct description', () => {
|
|
274
505
|
const result = createSkillToolDefinitions(config, token)
|
|
275
506
|
|
|
276
|
-
expect(result.
|
|
507
|
+
expect(result.skill_disable.description).toBe('Disable (unpublish) a skill on the LiteLLM Skills Gateway')
|
|
277
508
|
})
|
|
278
509
|
|
|
279
510
|
it('skill_list execute returns formatted markdown table', async () => {
|
|
280
511
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
281
512
|
ok: true,
|
|
282
513
|
status: 200,
|
|
283
|
-
json: async () =>
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
514
|
+
json: async () => ({
|
|
515
|
+
plugins: [
|
|
516
|
+
{ ...mockSkill, name: 'code-review', description: 'Reviews code', enabled: true },
|
|
517
|
+
{ ...mockSkill, name: 'security-scan', description: 'Scans security', enabled: false },
|
|
518
|
+
],
|
|
519
|
+
}),
|
|
287
520
|
})
|
|
288
521
|
vi.stubGlobal('fetch', mockFetch)
|
|
289
522
|
|
|
@@ -297,25 +530,47 @@ describe('createSkillToolDefinitions', () => {
|
|
|
297
530
|
expect(output).toContain('Scans security')
|
|
298
531
|
})
|
|
299
532
|
|
|
300
|
-
it('
|
|
533
|
+
it('skill_register execute calls registerSkill', async () => {
|
|
534
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
535
|
+
ok: true,
|
|
536
|
+
status: 200,
|
|
537
|
+
json: async () => ({ plugin: { id: 'new-skill', name: 'new-skill' } }),
|
|
538
|
+
})
|
|
539
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
540
|
+
|
|
541
|
+
const result = createSkillToolDefinitions(config, token)
|
|
542
|
+
|
|
543
|
+
const output = await result.skill_register.execute(
|
|
544
|
+
{
|
|
545
|
+
name: 'new-skill',
|
|
546
|
+
git_url: 'https://github.com/org/repo',
|
|
547
|
+
git_path: 'skills/new-skill',
|
|
548
|
+
description: 'A new skill',
|
|
549
|
+
},
|
|
550
|
+
{} as any,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
expect(output).toBe('Skill "new-skill" registered (id: new-skill)')
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
it('skill_enable execute calls enableSkill', async () => {
|
|
301
557
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
302
558
|
ok: true,
|
|
303
559
|
status: 200,
|
|
304
|
-
json: async () => ({ id: 'skill-new', name: 'new-skill' }),
|
|
305
560
|
})
|
|
306
561
|
vi.stubGlobal('fetch', mockFetch)
|
|
307
562
|
|
|
308
563
|
const result = createSkillToolDefinitions(config, token)
|
|
309
564
|
|
|
310
|
-
const output = await result.
|
|
311
|
-
{ name: '
|
|
565
|
+
const output = await result.skill_enable.execute(
|
|
566
|
+
{ name: 'skill-to-enable' },
|
|
312
567
|
{} as any,
|
|
313
568
|
)
|
|
314
569
|
|
|
315
|
-
expect(output).toBe('Skill "
|
|
570
|
+
expect(output).toBe('Skill "skill-to-enable" enabled')
|
|
316
571
|
})
|
|
317
572
|
|
|
318
|
-
it('
|
|
573
|
+
it('skill_disable execute calls disableSkill', async () => {
|
|
319
574
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
320
575
|
ok: true,
|
|
321
576
|
status: 200,
|
|
@@ -324,12 +579,12 @@ describe('createSkillToolDefinitions', () => {
|
|
|
324
579
|
|
|
325
580
|
const result = createSkillToolDefinitions(config, token)
|
|
326
581
|
|
|
327
|
-
const output = await result.
|
|
328
|
-
{
|
|
582
|
+
const output = await result.skill_disable.execute(
|
|
583
|
+
{ name: 'skill-to-disable' },
|
|
329
584
|
{} as any,
|
|
330
585
|
)
|
|
331
586
|
|
|
332
|
-
expect(output).toBe('Skill "skill-to-
|
|
587
|
+
expect(output).toBe('Skill "skill-to-disable" disabled')
|
|
333
588
|
})
|
|
334
589
|
})
|
|
335
590
|
|
|
@@ -349,10 +604,12 @@ describe('createSkillsInjector', () => {
|
|
|
349
604
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
350
605
|
ok: true,
|
|
351
606
|
status: 200,
|
|
352
|
-
json: async () =>
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
607
|
+
json: async () => ({
|
|
608
|
+
plugins: [
|
|
609
|
+
{ ...mockSkill, name: 'code-review', description: 'Reviews code', enabled: true },
|
|
610
|
+
{ ...mockSkill, name: 'security-scan', description: 'Scans security', enabled: true },
|
|
611
|
+
],
|
|
612
|
+
}),
|
|
356
613
|
})
|
|
357
614
|
vi.stubGlobal('fetch', mockFetch)
|
|
358
615
|
|
|
@@ -373,9 +630,7 @@ describe('createSkillsInjector', () => {
|
|
|
373
630
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
374
631
|
ok: true,
|
|
375
632
|
status: 200,
|
|
376
|
-
json: async () => [
|
|
377
|
-
{ id: 'skill-1', name: 'code-review', description: 'Reviews code', enabled: true },
|
|
378
|
-
],
|
|
633
|
+
json: async () => ({ plugins: [mockSkill] }),
|
|
379
634
|
})
|
|
380
635
|
vi.stubGlobal('fetch', mockFetch)
|
|
381
636
|
|
|
@@ -386,9 +641,7 @@ describe('createSkillsInjector', () => {
|
|
|
386
641
|
|
|
387
642
|
await injector(input, output)
|
|
388
643
|
|
|
389
|
-
// Should not have injected anything — sub-agent sessions are skipped
|
|
390
644
|
expect(output.parts).toEqual([])
|
|
391
|
-
// Should not have called fetch since it returns early
|
|
392
645
|
expect(mockFetch).not.toHaveBeenCalled()
|
|
393
646
|
})
|
|
394
647
|
|
|
@@ -396,9 +649,11 @@ describe('createSkillsInjector', () => {
|
|
|
396
649
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
397
650
|
ok: true,
|
|
398
651
|
status: 200,
|
|
399
|
-
json: async () =>
|
|
400
|
-
|
|
401
|
-
|
|
652
|
+
json: async () => ({
|
|
653
|
+
plugins: [
|
|
654
|
+
{ ...mockSkill, name: 'disabled-skill', description: 'Disabled', enabled: false },
|
|
655
|
+
],
|
|
656
|
+
}),
|
|
402
657
|
})
|
|
403
658
|
vi.stubGlobal('fetch', mockFetch)
|
|
404
659
|
|
|
@@ -412,23 +667,45 @@ describe('createSkillsInjector', () => {
|
|
|
412
667
|
expect(output.parts).toEqual([])
|
|
413
668
|
})
|
|
414
669
|
|
|
670
|
+
it('uses "No description" for null descriptions', async () => {
|
|
671
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
672
|
+
ok: true,
|
|
673
|
+
status: 200,
|
|
674
|
+
json: async () => ({
|
|
675
|
+
plugins: [
|
|
676
|
+
{ ...mockSkill, name: 'no-desc-skill', description: null, enabled: true },
|
|
677
|
+
],
|
|
678
|
+
}),
|
|
679
|
+
})
|
|
680
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
681
|
+
|
|
682
|
+
const injector = createSkillsInjector(config, token)
|
|
683
|
+
|
|
684
|
+
const input = { sessionID: 'main-session' }
|
|
685
|
+
const output = { message: { content: 'Hello' }, parts: [] }
|
|
686
|
+
|
|
687
|
+
await injector(input, output)
|
|
688
|
+
|
|
689
|
+
expect(output.parts[0].text).toContain('<skill name="no-desc-skill">No description</skill>')
|
|
690
|
+
})
|
|
691
|
+
|
|
415
692
|
it('cache TTL works (second call within TTL uses cache)', async () => {
|
|
416
693
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
417
694
|
ok: true,
|
|
418
695
|
status: 200,
|
|
419
|
-
json: async () =>
|
|
420
|
-
|
|
421
|
-
|
|
696
|
+
json: async () => ({
|
|
697
|
+
plugins: [
|
|
698
|
+
{ ...mockSkill, name: 'cached-skill', description: 'Cached', enabled: true },
|
|
699
|
+
],
|
|
700
|
+
}),
|
|
422
701
|
})
|
|
423
702
|
vi.stubGlobal('fetch', mockFetch)
|
|
424
703
|
|
|
425
704
|
const injector = createSkillsInjector(config, token)
|
|
426
705
|
|
|
427
|
-
// First call — should fetch
|
|
428
706
|
await injector({ sessionID: 'session-1' }, { message: {}, parts: [] })
|
|
429
707
|
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
430
708
|
|
|
431
|
-
// Second call — should use cache (fetch not called again)
|
|
432
709
|
await injector({ sessionID: 'session-2' }, { message: {}, parts: [] })
|
|
433
710
|
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
434
711
|
})
|
|
@@ -442,7 +719,6 @@ describe('createSkillsInjector', () => {
|
|
|
442
719
|
const input = { sessionID: 'main-session' }
|
|
443
720
|
const output = { message: { content: 'Hello' }, parts: [] }
|
|
444
721
|
|
|
445
|
-
// Should not throw
|
|
446
722
|
await expect(injector(input, output)).resolves.toBeUndefined()
|
|
447
723
|
expect(output.parts).toEqual([])
|
|
448
724
|
})
|
package/src/skills.ts
CHANGED
|
@@ -15,7 +15,7 @@ export function resetSkillsCache(): void {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Fetches all skills from the LiteLLM
|
|
18
|
+
* Fetches all skills from the LiteLLM Skills Gateway.
|
|
19
19
|
* Returns an empty array on any error (network, 4xx, 5xx, parse failure).
|
|
20
20
|
* Uses a 10s timeout via AbortController.
|
|
21
21
|
*/
|
|
@@ -27,7 +27,7 @@ export async function listSkills(
|
|
|
27
27
|
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
28
28
|
|
|
29
29
|
try {
|
|
30
|
-
const response = await fetch(`${config.url}/
|
|
30
|
+
const response = await fetch(`${config.url}/claude-code/plugins`, {
|
|
31
31
|
method: 'GET',
|
|
32
32
|
headers: {
|
|
33
33
|
Authorization: `Bearer ${token}`,
|
|
@@ -41,11 +41,11 @@ export async function listSkills(
|
|
|
41
41
|
|
|
42
42
|
const body = await response.json()
|
|
43
43
|
|
|
44
|
-
if (!Array.isArray(body)) {
|
|
44
|
+
if (!body || !Array.isArray(body.plugins)) {
|
|
45
45
|
return []
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
return body as Skill[]
|
|
48
|
+
return body.plugins as Skill[]
|
|
49
49
|
} catch {
|
|
50
50
|
return []
|
|
51
51
|
} finally {
|
|
@@ -54,23 +54,55 @@ export async function listSkills(
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* Uses a 10s timeout via AbortController.
|
|
57
|
+
* Fetches only enabled (public) skills from the LiteLLM Skill Hub.
|
|
58
|
+
* No auth required. Useful for discovery without credentials.
|
|
60
59
|
*/
|
|
61
|
-
export async function
|
|
60
|
+
export async function listPublicSkills(config: PluginConfig): Promise<Skill[]> {
|
|
61
|
+
const controller = new AbortController()
|
|
62
|
+
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(`${config.url}/public/skill_hub`, {
|
|
66
|
+
method: 'GET',
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const body = await response.json()
|
|
75
|
+
|
|
76
|
+
if (!body || !Array.isArray(body.plugins)) {
|
|
77
|
+
return []
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return body.plugins as Skill[]
|
|
81
|
+
} catch {
|
|
82
|
+
return []
|
|
83
|
+
} finally {
|
|
84
|
+
clearTimeout(timeoutId)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Registers a new skill on the LiteLLM Skills Gateway.
|
|
90
|
+
* The skill points to a git source containing a SKILL.md file.
|
|
91
|
+
*/
|
|
92
|
+
export async function registerSkill(
|
|
62
93
|
config: PluginConfig,
|
|
63
94
|
token: string,
|
|
64
95
|
name: string,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
96
|
+
gitUrl: string,
|
|
97
|
+
gitPath: string,
|
|
98
|
+
description?: string,
|
|
99
|
+
domain?: string,
|
|
68
100
|
): Promise<string> {
|
|
69
101
|
const controller = new AbortController()
|
|
70
102
|
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
71
103
|
|
|
72
104
|
try {
|
|
73
|
-
const response = await fetch(`${config.url}/
|
|
105
|
+
const response = await fetch(`${config.url}/claude-code/plugins`, {
|
|
74
106
|
method: 'POST',
|
|
75
107
|
headers: {
|
|
76
108
|
Authorization: `Bearer ${token}`,
|
|
@@ -78,44 +110,79 @@ export async function createSkill(
|
|
|
78
110
|
},
|
|
79
111
|
body: JSON.stringify({
|
|
80
112
|
name,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
source: {
|
|
114
|
+
source: 'git-subdir',
|
|
115
|
+
url: gitUrl,
|
|
116
|
+
path: gitPath,
|
|
117
|
+
},
|
|
118
|
+
description: description || null,
|
|
119
|
+
domain: domain || null,
|
|
84
120
|
}),
|
|
85
121
|
signal: controller.signal,
|
|
86
122
|
})
|
|
87
123
|
|
|
88
124
|
if (!response.ok) {
|
|
89
|
-
return `Error
|
|
125
|
+
return `Error registering skill: HTTP ${response.status}`
|
|
90
126
|
}
|
|
91
127
|
|
|
92
128
|
const body = await response.json()
|
|
93
|
-
const id = body
|
|
94
|
-
return `Skill "${name}"
|
|
129
|
+
const id = body?.plugin?.id ?? 'unknown'
|
|
130
|
+
return `Skill "${name}" registered (id: ${id})`
|
|
95
131
|
} catch (error: unknown) {
|
|
96
132
|
const message = error instanceof Error ? error.message : String(error)
|
|
97
|
-
return `Error
|
|
133
|
+
return `Error registering skill: ${message}`
|
|
98
134
|
} finally {
|
|
99
135
|
clearTimeout(timeoutId)
|
|
100
136
|
}
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
/**
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
140
|
+
* Enables (publishes) a skill on the LiteLLM Skills Gateway.
|
|
141
|
+
*/
|
|
142
|
+
export async function enableSkill(
|
|
143
|
+
config: PluginConfig,
|
|
144
|
+
token: string,
|
|
145
|
+
name: string,
|
|
146
|
+
): Promise<string> {
|
|
147
|
+
const controller = new AbortController()
|
|
148
|
+
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(`${config.url}/claude-code/plugins/${name}/enable`, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: {
|
|
154
|
+
Authorization: `Bearer ${token}`,
|
|
155
|
+
},
|
|
156
|
+
signal: controller.signal,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
return `Error enabling skill: HTTP ${response.status}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return `Skill "${name}" enabled`
|
|
164
|
+
} catch (error: unknown) {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
166
|
+
return `Error enabling skill: ${message}`
|
|
167
|
+
} finally {
|
|
168
|
+
clearTimeout(timeoutId)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Disables (unpublishes) a skill on the LiteLLM Skills Gateway.
|
|
107
174
|
*/
|
|
108
|
-
export async function
|
|
175
|
+
export async function disableSkill(
|
|
109
176
|
config: PluginConfig,
|
|
110
177
|
token: string,
|
|
111
|
-
|
|
178
|
+
name: string,
|
|
112
179
|
): Promise<string> {
|
|
113
180
|
const controller = new AbortController()
|
|
114
181
|
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
115
182
|
|
|
116
183
|
try {
|
|
117
|
-
const response = await fetch(`${config.url}/
|
|
118
|
-
method: '
|
|
184
|
+
const response = await fetch(`${config.url}/claude-code/plugins/${name}/disable`, {
|
|
185
|
+
method: 'POST',
|
|
119
186
|
headers: {
|
|
120
187
|
Authorization: `Bearer ${token}`,
|
|
121
188
|
},
|
|
@@ -123,21 +190,83 @@ export async function deleteSkill(
|
|
|
123
190
|
})
|
|
124
191
|
|
|
125
192
|
if (!response.ok) {
|
|
126
|
-
return `Error
|
|
193
|
+
return `Error disabling skill: HTTP ${response.status}`
|
|
127
194
|
}
|
|
128
195
|
|
|
129
|
-
return `Skill "${
|
|
196
|
+
return `Skill "${name}" disabled`
|
|
130
197
|
} catch (error: unknown) {
|
|
131
198
|
const message = error instanceof Error ? error.message : String(error)
|
|
132
|
-
return `Error
|
|
199
|
+
return `Error disabling skill: ${message}`
|
|
133
200
|
} finally {
|
|
134
201
|
clearTimeout(timeoutId)
|
|
135
202
|
}
|
|
136
203
|
}
|
|
137
204
|
|
|
138
205
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
206
|
+
* Fetches the SKILL.md content from a skill's git source.
|
|
207
|
+
* Currently supports GitHub raw URLs.
|
|
208
|
+
*/
|
|
209
|
+
export async function fetchSkillContent(skill: Skill): Promise<string | null> {
|
|
210
|
+
const controller = new AbortController()
|
|
211
|
+
const timeoutId = setTimeout(() => controller.abort(), 10_000)
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const rawUrl = buildRawGitUrl(skill.source)
|
|
215
|
+
if (!rawUrl) return null
|
|
216
|
+
|
|
217
|
+
const response = await fetch(rawUrl, {
|
|
218
|
+
signal: controller.signal,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
if (!response.ok) return null
|
|
222
|
+
|
|
223
|
+
return await response.text()
|
|
224
|
+
} catch {
|
|
225
|
+
return null
|
|
226
|
+
} finally {
|
|
227
|
+
clearTimeout(timeoutId)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Builds a raw git URL for the SKILL.md file from a skill's source.
|
|
233
|
+
* Currently supports GitHub git-subdir sources.
|
|
234
|
+
*/
|
|
235
|
+
function buildRawGitUrl(source: Skill['source']): string | null {
|
|
236
|
+
if (source.source !== 'git-subdir') return null
|
|
237
|
+
|
|
238
|
+
const url = source.url
|
|
239
|
+
if (!url.includes('github.com')) return null
|
|
240
|
+
|
|
241
|
+
const isRaw = url.startsWith('https://raw.githubusercontent.com')
|
|
242
|
+
if (isRaw) {
|
|
243
|
+
const branch = extractBranch(url)
|
|
244
|
+
const path = source.path || ''
|
|
245
|
+
return `https://raw.githubusercontent.com/${url.replace('https://raw.githubusercontent.com/', '').split('/').slice(0, 2).join('/')}/${branch}/${path}/SKILL.md`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const match = url.match(/https:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/.*)?/)
|
|
249
|
+
if (!match) return null
|
|
250
|
+
|
|
251
|
+
const [, owner, repo] = match
|
|
252
|
+
const branch = extractBranch(url) || 'master'
|
|
253
|
+
const path = source.path || ''
|
|
254
|
+
|
|
255
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}/SKILL.md`
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Extracts the branch name from a GitHub URL.
|
|
260
|
+
* Falls back to 'master' if not found.
|
|
261
|
+
*/
|
|
262
|
+
function extractBranch(url: string): string | null {
|
|
263
|
+
const match = url.match(/\/tree\/([^/]+)/)
|
|
264
|
+
return match ? match[1] : null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Creates opencode tool definitions for skill management operations.
|
|
269
|
+
* Returns tools: skill_list, skill_register, skill_enable, skill_disable.
|
|
141
270
|
*/
|
|
142
271
|
export function createSkillToolDefinitions(
|
|
143
272
|
config: PluginConfig,
|
|
@@ -145,7 +274,7 @@ export function createSkillToolDefinitions(
|
|
|
145
274
|
): Record<string, any> {
|
|
146
275
|
return {
|
|
147
276
|
skill_list: tool({
|
|
148
|
-
description: 'List all skills registered on the LiteLLM
|
|
277
|
+
description: 'List all skills registered on the LiteLLM Skills Gateway',
|
|
149
278
|
args: {},
|
|
150
279
|
async execute(_args: Record<string, unknown>, _context: unknown): Promise<string> {
|
|
151
280
|
const skills = await listSkills(config, token)
|
|
@@ -154,12 +283,12 @@ export function createSkillToolDefinitions(
|
|
|
154
283
|
return 'No skills found.'
|
|
155
284
|
}
|
|
156
285
|
|
|
157
|
-
const header = '|
|
|
158
|
-
const sep = '
|
|
286
|
+
const header = '| Name | Description | Enabled | Source |'
|
|
287
|
+
const sep = '|--------|-------------|---------|--------|'
|
|
159
288
|
const rows = skills
|
|
160
289
|
.map(
|
|
161
290
|
(s) =>
|
|
162
|
-
`| ${s.
|
|
291
|
+
`| ${s.name} | ${s.description || '-'} | ${s.enabled ? 'yes' : 'no'} | ${s.source.url} |`,
|
|
163
292
|
)
|
|
164
293
|
.join('\n')
|
|
165
294
|
|
|
@@ -167,37 +296,45 @@ export function createSkillToolDefinitions(
|
|
|
167
296
|
},
|
|
168
297
|
}),
|
|
169
298
|
|
|
170
|
-
|
|
171
|
-
description: '
|
|
299
|
+
skill_register: tool({
|
|
300
|
+
description: 'Register a new skill on the LiteLLM Skills Gateway pointing to a git source',
|
|
172
301
|
args: {
|
|
173
302
|
name: tool.schema.string().describe('Name of the skill'),
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.optional()
|
|
179
|
-
.describe('Input schema for the skill'),
|
|
180
|
-
code: tool.schema.string().optional().describe('Code for the skill'),
|
|
303
|
+
git_url: tool.schema.string().describe('GitHub repository URL containing the skill'),
|
|
304
|
+
git_path: tool.schema.string().describe('Path within the repo to the skill directory (must contain SKILL.md)'),
|
|
305
|
+
description: tool.schema.string().optional().describe('Description of the skill'),
|
|
306
|
+
domain: tool.schema.string().optional().describe('Domain/category for the skill'),
|
|
181
307
|
},
|
|
182
308
|
async execute(args: Record<string, unknown>, _context: unknown): Promise<string> {
|
|
183
|
-
return
|
|
309
|
+
return registerSkill(
|
|
184
310
|
config,
|
|
185
311
|
token,
|
|
186
312
|
args.name as string,
|
|
187
|
-
args.
|
|
188
|
-
args.
|
|
189
|
-
args.
|
|
313
|
+
args.git_url as string,
|
|
314
|
+
args.git_path as string,
|
|
315
|
+
args.description as string | undefined,
|
|
316
|
+
args.domain as string | undefined,
|
|
190
317
|
)
|
|
191
318
|
},
|
|
192
319
|
}),
|
|
193
320
|
|
|
194
|
-
|
|
195
|
-
description: '
|
|
321
|
+
skill_enable: tool({
|
|
322
|
+
description: 'Enable (publish) a skill on the LiteLLM Skills Gateway',
|
|
323
|
+
args: {
|
|
324
|
+
name: tool.schema.string().describe('Name of the skill to enable'),
|
|
325
|
+
},
|
|
326
|
+
async execute(args: Record<string, unknown>, _context: unknown): Promise<string> {
|
|
327
|
+
return enableSkill(config, token, args.name as string)
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
|
|
331
|
+
skill_disable: tool({
|
|
332
|
+
description: 'Disable (unpublish) a skill on the LiteLLM Skills Gateway',
|
|
196
333
|
args: {
|
|
197
|
-
|
|
334
|
+
name: tool.schema.string().describe('Name of the skill to disable'),
|
|
198
335
|
},
|
|
199
336
|
async execute(args: Record<string, unknown>, _context: unknown): Promise<string> {
|
|
200
|
-
return
|
|
337
|
+
return disableSkill(config, token, args.name as string)
|
|
201
338
|
},
|
|
202
339
|
}),
|
|
203
340
|
}
|
|
@@ -216,10 +353,8 @@ export function createSkillsInjector(
|
|
|
216
353
|
output: { message: any; parts: any[] },
|
|
217
354
|
) => Promise<void> {
|
|
218
355
|
return async (input, output) => {
|
|
219
|
-
// Only inject for main agent session — skip ALL sub-agents
|
|
220
356
|
if (input.agent) return
|
|
221
357
|
|
|
222
|
-
// Fetch skills with simple in-memory cache
|
|
223
358
|
let skills: Skill[] = []
|
|
224
359
|
if (skillsCache && Date.now() - skillsCache.timestamp < CACHE_TTL_MS) {
|
|
225
360
|
skills = skillsCache.data
|
|
@@ -232,7 +367,7 @@ export function createSkillsInjector(
|
|
|
232
367
|
if (enabledSkills.length === 0) return
|
|
233
368
|
|
|
234
369
|
const context = enabledSkills
|
|
235
|
-
.map((s) => `<skill name="${s.name}">${s.description}</skill>`)
|
|
370
|
+
.map((s) => `<skill name="${s.name}">${s.description || 'No description'}</skill>`)
|
|
236
371
|
.join('\n')
|
|
237
372
|
|
|
238
373
|
output.parts.push({ type: 'text', text: context })
|
package/src/types.ts
CHANGED
|
@@ -15,12 +15,27 @@ export interface McpTool {
|
|
|
15
15
|
input_schema: Record<string, unknown>
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface SkillSource {
|
|
19
|
+
source: string
|
|
20
|
+
url: string
|
|
21
|
+
path?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
export interface Skill {
|
|
19
25
|
id: string
|
|
20
26
|
name: string
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
version: string
|
|
28
|
+
description: string | null
|
|
29
|
+
source: SkillSource
|
|
30
|
+
author: string | null
|
|
31
|
+
homepage: string | null
|
|
32
|
+
keywords: string | null
|
|
33
|
+
category: string | null
|
|
34
|
+
domain: string | null
|
|
35
|
+
namespace: string | null
|
|
36
|
+
enabled: boolean
|
|
37
|
+
created_at: string
|
|
38
|
+
updated_at: string
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
export interface OpencodeModelConfig {
|