dsh-plugin-capabilities 0.2.1 → 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/README.md +1 -1
- package/lib/client.js +111 -15
- package/lib/client.js.map +3 -3
- package/lib/index.js +24 -2
- package/lib/index.js.map +2 -2
- package/market/mcp.json +325 -27
- package/market/skills.json +534 -4
- package/package.json +1 -1
- package/src/client/MarketTab.tsx +116 -9
- package/src/client/css.ts +10 -1
- package/src/client/index.ts +13 -0
- package/src/client/locales.ts +8 -0
- package/src/market.test.ts +14 -3
- package/src/market.ts +40 -0
package/src/client/MarketTab.tsx
CHANGED
|
@@ -48,6 +48,7 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
48
48
|
const [busyId, setBusyId] = useState<string | null>(null)
|
|
49
49
|
const [outcome, setOutcome] = useState<{ ok: boolean; text: string } | null>(null)
|
|
50
50
|
const [confirmUninstall, setConfirmUninstall] = useState<{ kind: 'root' | 'mcp'; id: string; name: string } | null>(null)
|
|
51
|
+
const [detail, setDetail] = useState<{ kind: Segment; id: string } | null>(null)
|
|
51
52
|
const [reload, setReload] = useState(0)
|
|
52
53
|
|
|
53
54
|
const refreshSkills = useCallback((): void => {
|
|
@@ -128,6 +129,15 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
128
129
|
const pick = <T,>(base: T, zh: T | undefined): T => (zhUi() && zh !== undefined ? zh : base)
|
|
129
130
|
const title = (entry: { name: string; nameZh?: string }): string => pick(entry.name, entry.nameZh)
|
|
130
131
|
const desc = (entry: { description: string; descriptionZh?: string }): string => pick(entry.description, entry.descriptionZh)
|
|
132
|
+
const longText = (entry: { description: string; descriptionZh?: string; detail?: string; detailZh?: string }): string =>
|
|
133
|
+
entry.detail !== undefined ? pick(entry.detail, entry.detailZh) : desc(entry)
|
|
134
|
+
|
|
135
|
+
const cardStop = (event: { stopPropagation(): void }): void => { event.stopPropagation() }
|
|
136
|
+
|
|
137
|
+
// The card list refreshes on install/uninstall, so the detail modal resolves
|
|
138
|
+
// its entry by id each render and follows the fresh installed state.
|
|
139
|
+
const detailRepo = detail !== null && detail.kind === 'skills' && repos !== null ? repos.find(repo => repo.id === detail.id) ?? null : null
|
|
140
|
+
const detailServer = detail !== null && detail.kind === 'mcp' && servers !== null ? servers.find(server => server.id === detail.id) ?? null : null
|
|
131
141
|
|
|
132
142
|
return (
|
|
133
143
|
<div className="dpc-section">
|
|
@@ -172,7 +182,7 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
172
182
|
{repos !== null && repos.length > 0 && (
|
|
173
183
|
<ul className="dpc-cards">
|
|
174
184
|
{repos.map((repo) => (
|
|
175
|
-
<li className="dpc-card" key={repo.id}>
|
|
185
|
+
<li className="dpc-card" key={repo.id} onClick={() => setDetail({ kind: 'skills', id: repo.id })}>
|
|
176
186
|
<div className="dpc-cardTop">
|
|
177
187
|
<strong className="dpc-cardTitle" title={repo.url}>{title(repo)}</strong>
|
|
178
188
|
{repo.skillCount !== undefined && <span className="dpc-tag">{t('marketSkillCount').replace('{n}', String(repo.skillCount))}</span>}
|
|
@@ -180,14 +190,15 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
180
190
|
</div>
|
|
181
191
|
<p className="dpc-cardDesc">{desc(repo)}</p>
|
|
182
192
|
<div className="dpc-cardRow">
|
|
183
|
-
<
|
|
193
|
+
<button type="button" className="dpc-link" onClick={() => setDetail({ kind: 'skills', id: repo.id })}>{t('marketDetail')}</button>
|
|
194
|
+
<a className="dpc-link" href={repo.homepage ?? repo.url} target="_blank" rel="noreferrer" onClick={cardStop}>{t('marketHome')}</a>
|
|
184
195
|
<span className="dpc-spacer" />
|
|
185
196
|
{repo.installedId !== null ? (
|
|
186
|
-
<Button variant="ghost" size="sm" disabled={busyId !== null} onClick={() => setConfirmUninstall({ kind: 'root', id: repo.installedId as string, name: title(repo) })}>
|
|
197
|
+
<Button variant="ghost" size="sm" disabled={busyId !== null} onClick={(event) => { event.stopPropagation(); setConfirmUninstall({ kind: 'root', id: repo.installedId as string, name: title(repo) }) }}>
|
|
187
198
|
{t('marketUninstall')}
|
|
188
199
|
</Button>
|
|
189
200
|
) : (
|
|
190
|
-
<Button variant="primary" size="sm" disabled={busyId !== null} onClick={() => void doInstallRepo(repo)}>
|
|
201
|
+
<Button variant="primary" size="sm" disabled={busyId !== null} onClick={(event) => { event.stopPropagation(); void doInstallRepo(repo) }}>
|
|
191
202
|
{busyId === repo.id ? t('marketInstalling') : t('marketInstall')}
|
|
192
203
|
</Button>
|
|
193
204
|
)}
|
|
@@ -207,7 +218,7 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
207
218
|
{servers !== null && servers.length > 0 && (
|
|
208
219
|
<ul className="dpc-cards">
|
|
209
220
|
{servers.map((server) => (
|
|
210
|
-
<li className="dpc-card" key={server.id}>
|
|
221
|
+
<li className="dpc-card" key={server.id} onClick={() => setDetail({ kind: 'mcp', id: server.id })}>
|
|
211
222
|
<div className="dpc-cardTop">
|
|
212
223
|
<strong className="dpc-cardTitle" title={server.id}>{title(server)}</strong>
|
|
213
224
|
<span className="dpc-tag">{server.transport}</span>
|
|
@@ -219,20 +230,21 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
219
230
|
<p className="dpc-envHint">{t('marketNeedsEnv')}: {server.envKeys.join(', ')}</p>
|
|
220
231
|
)}
|
|
221
232
|
<div className="dpc-cardRow">
|
|
222
|
-
<
|
|
233
|
+
<button type="button" className="dpc-link" onClick={() => setDetail({ kind: 'mcp', id: server.id })}>{t('marketDetail')}</button>
|
|
234
|
+
<a className="dpc-link" href={server.homepage} target="_blank" rel="noreferrer" onClick={cardStop}>{t('marketHome')}</a>
|
|
223
235
|
<span className="dpc-spacer" />
|
|
224
236
|
{server.installed ? (
|
|
225
|
-
<Button variant="ghost" size="sm" disabled={busyId !== null} onClick={() => void mcp.list().then(
|
|
237
|
+
<Button variant="ghost" size="sm" disabled={busyId !== null} onClick={(event) => { event.stopPropagation(); void mcp.list().then(
|
|
226
238
|
(body) => {
|
|
227
239
|
const row = body.servers.find(item => item.serverName === server.id)
|
|
228
240
|
if (row !== undefined) setConfirmUninstall({ kind: 'mcp', id: row.id, name: server.id })
|
|
229
241
|
},
|
|
230
242
|
(error: Error) => setOutcome({ ok: false, text: `${t('failed')}: ${String(error.message ?? error)}` }),
|
|
231
|
-
)}>
|
|
243
|
+
) }}>
|
|
232
244
|
{t('marketUninstall')}
|
|
233
245
|
</Button>
|
|
234
246
|
) : (
|
|
235
|
-
<Button variant="primary" size="sm" disabled={busyId !== null} onClick={() => void doInstallMcp(server)}>
|
|
247
|
+
<Button variant="primary" size="sm" disabled={busyId !== null} onClick={(event) => { event.stopPropagation(); void doInstallMcp(server) }}>
|
|
236
248
|
{busyId === server.id ? t('marketInstalling') : t('marketAdd')}
|
|
237
249
|
</Button>
|
|
238
250
|
)}
|
|
@@ -244,6 +256,101 @@ export function MarketTab(props: { t: Translate; market: MarketInjected; mcp: Mc
|
|
|
244
256
|
</>
|
|
245
257
|
)}
|
|
246
258
|
|
|
259
|
+
<Modal
|
|
260
|
+
open={detailRepo !== null || detailServer !== null}
|
|
261
|
+
onClose={() => setDetail(null)}
|
|
262
|
+
title={detailRepo !== null ? title(detailRepo) : detailServer !== null ? title(detailServer) : ''}
|
|
263
|
+
footer={
|
|
264
|
+
<>
|
|
265
|
+
{detailRepo !== null && (
|
|
266
|
+
<a className="dpc-link" href={detailRepo.homepage ?? detailRepo.url} target="_blank" rel="noreferrer">{t('marketHome')}</a>
|
|
267
|
+
)}
|
|
268
|
+
{detailServer !== null && (
|
|
269
|
+
<a className="dpc-link" href={detailServer.homepage} target="_blank" rel="noreferrer">{t('marketHome')}</a>
|
|
270
|
+
)}
|
|
271
|
+
<span className="dpc-spacer" />
|
|
272
|
+
{detailRepo !== null && (detailRepo.installedId !== null ? (
|
|
273
|
+
<Button variant="ghost" disabled={busyId !== null} onClick={() => { setConfirmUninstall({ kind: 'root', id: detailRepo.installedId as string, name: title(detailRepo) }); setDetail(null) }}>
|
|
274
|
+
{t('marketUninstall')}
|
|
275
|
+
</Button>
|
|
276
|
+
) : (
|
|
277
|
+
<Button variant="primary" disabled={busyId !== null} onClick={() => void doInstallRepo(detailRepo)}>
|
|
278
|
+
{busyId === detailRepo.id ? t('marketInstalling') : t('marketInstall')}
|
|
279
|
+
</Button>
|
|
280
|
+
))}
|
|
281
|
+
{detailServer !== null && (detailServer.installed ? (
|
|
282
|
+
<Button variant="ghost" disabled={busyId !== null} onClick={() => void mcp.list().then(
|
|
283
|
+
(body) => {
|
|
284
|
+
const row = body.servers.find(item => item.serverName === detailServer.id)
|
|
285
|
+
if (row !== undefined) { setConfirmUninstall({ kind: 'mcp', id: row.id, name: title(detailServer) }); setDetail(null) }
|
|
286
|
+
},
|
|
287
|
+
(error: Error) => setOutcome({ ok: false, text: `${t('failed')}: ${String(error.message ?? error)}` }),
|
|
288
|
+
)}>
|
|
289
|
+
{t('marketUninstall')}
|
|
290
|
+
</Button>
|
|
291
|
+
) : (
|
|
292
|
+
<Button variant="primary" disabled={busyId !== null} onClick={() => void doInstallMcp(detailServer)}>
|
|
293
|
+
{busyId === detailServer.id ? t('marketInstalling') : t('marketAdd')}
|
|
294
|
+
</Button>
|
|
295
|
+
))}
|
|
296
|
+
</>
|
|
297
|
+
}
|
|
298
|
+
>
|
|
299
|
+
{detailRepo !== null && (
|
|
300
|
+
<>
|
|
301
|
+
<p className="dpc-detailDesc">{longText(detailRepo)}</p>
|
|
302
|
+
{detailRepo.skills !== undefined && detailRepo.skills.length > 0 && (
|
|
303
|
+
<div className="dpc-detailSection">
|
|
304
|
+
<div className="dpc-detailLabel">{t('marketSkillListLabel')}</div>
|
|
305
|
+
<ul className="dpc-skillList">
|
|
306
|
+
{detailRepo.skills.map(item => (
|
|
307
|
+
<li className="dpc-skillItem" key={item.name}>
|
|
308
|
+
<code className="dpc-skillName">{item.name}</code>
|
|
309
|
+
{item.description !== undefined && (
|
|
310
|
+
<span className="dpc-skillDesc">{pick(item.description, item.descriptionZh)}</span>
|
|
311
|
+
)}
|
|
312
|
+
</li>
|
|
313
|
+
))}
|
|
314
|
+
</ul>
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
</>
|
|
318
|
+
)}
|
|
319
|
+
{detailServer !== null && (
|
|
320
|
+
<>
|
|
321
|
+
<p className="dpc-detailDesc">{longText(detailServer)}</p>
|
|
322
|
+
{detailServer.command !== undefined && (
|
|
323
|
+
<div className="dpc-detailSection">
|
|
324
|
+
<div className="dpc-detailLabel">{t('marketCommandLabel')}</div>
|
|
325
|
+
<code className="dpc-code">{[detailServer.command, ...(detailServer.args ?? [])].join(' ')}</code>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
{detailServer.transport === 'streamable-http' && detailServer.url !== undefined && (
|
|
329
|
+
<div className="dpc-detailSection">
|
|
330
|
+
<div className="dpc-detailLabel">{t('marketCommandLabel')}</div>
|
|
331
|
+
<code className="dpc-code">{detailServer.url}</code>
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
{detailServer.envKeys !== undefined && detailServer.envKeys.length > 0 && (
|
|
335
|
+
<div className="dpc-detailSection">
|
|
336
|
+
<div className="dpc-detailLabel">{t('marketNeedsEnv')}</div>
|
|
337
|
+
<div className="dpc-detailTags">
|
|
338
|
+
{detailServer.envKeys.map(key => <span className="dpc-tag" key={key}>{key}</span>)}
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
{detailServer.tools !== undefined && detailServer.tools.length > 0 && (
|
|
343
|
+
<div className="dpc-detailSection">
|
|
344
|
+
<div className="dpc-detailLabel">{t('marketToolsLabel')}</div>
|
|
345
|
+
<div className="dpc-detailTags">
|
|
346
|
+
{detailServer.tools.map(name => <span className="dpc-tag" key={name}>{name}</span>)}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
)}
|
|
350
|
+
</>
|
|
351
|
+
)}
|
|
352
|
+
</Modal>
|
|
353
|
+
|
|
247
354
|
<Modal
|
|
248
355
|
open={confirmUninstall !== null}
|
|
249
356
|
onClose={() => setConfirmUninstall(null)}
|
package/src/client/css.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const CSS = `
|
|
|
26
26
|
.dpc-refresh:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
|
|
27
27
|
.dpc-empty{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
|
|
28
28
|
.dpc-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:stretch;gap:10px;margin:0;padding:0;list-style:none}
|
|
29
|
-
.dpc-card{display:flex;flex-direction:column;gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3);padding:12px 14px}
|
|
29
|
+
.dpc-card{display:flex;flex-direction:column;gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3);padding:12px 14px;cursor:pointer}
|
|
30
30
|
.dpc-card:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
31
31
|
.dpc-cardTop{display:flex;align-items:center;gap:8px}
|
|
32
32
|
.dpc-cardTitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:20px;font-weight:600;font-family:var(--ds-font-family-code)}
|
|
@@ -108,5 +108,14 @@ export const CSS = `
|
|
|
108
108
|
.dpc-format .dpc-form{margin-top:8px}
|
|
109
109
|
.dpc-formatHint{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
110
110
|
.dpc-code{margin:4px 0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:8px 10px;overflow-x:auto;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-family:var(--ds-font-family-code);font-size:11px;line-height:17px;white-space:pre}
|
|
111
|
+
/* Market detail modal. */
|
|
112
|
+
.dpc-detailDesc{margin:0;font-size:13px;line-height:20px}
|
|
113
|
+
.dpc-detailSection{margin-top:12px;display:flex;flex-direction:column;gap:6px}
|
|
114
|
+
.dpc-detailLabel{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}
|
|
115
|
+
.dpc-detailTags{display:flex;flex-wrap:wrap;gap:4px}
|
|
116
|
+
.dpc-skillList{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}
|
|
117
|
+
.dpc-skillItem{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:18px;min-width:0}
|
|
118
|
+
.dpc-skillName{font-family:var(--ds-font-family-code);color:var(--dsw-alias-label-primary);white-space:nowrap;flex:none}
|
|
119
|
+
.dpc-skillDesc{color:var(--dsw-alias-label-secondary);min-width:0}
|
|
111
120
|
@media(max-width:680px){.dpc-cards{grid-template-columns:minmax(0,1fr)}}
|
|
112
121
|
`
|
package/src/client/index.ts
CHANGED
|
@@ -131,6 +131,13 @@ export function apply(ctx: CapabilitiesClientContext): void {
|
|
|
131
131
|
})
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/** One skill inside a market repository's inventory, as the browser sees it. */
|
|
135
|
+
export interface MarketSkillItemView {
|
|
136
|
+
name: string
|
|
137
|
+
description?: string
|
|
138
|
+
descriptionZh?: string
|
|
139
|
+
}
|
|
140
|
+
|
|
134
141
|
/** One skills-market repository as the browser sees it. */
|
|
135
142
|
export interface MarketRepoView {
|
|
136
143
|
id: string
|
|
@@ -138,9 +145,12 @@ export interface MarketRepoView {
|
|
|
138
145
|
nameZh?: string
|
|
139
146
|
description: string
|
|
140
147
|
descriptionZh?: string
|
|
148
|
+
detail?: string
|
|
149
|
+
detailZh?: string
|
|
141
150
|
url: string
|
|
142
151
|
homepage?: string
|
|
143
152
|
skillCount?: number
|
|
153
|
+
skills?: MarketSkillItemView[]
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
/** One MCP-market server as the browser sees it. */
|
|
@@ -158,4 +168,7 @@ export interface MarketServerView {
|
|
|
158
168
|
homepage: string
|
|
159
169
|
category?: string
|
|
160
170
|
runtime?: string
|
|
171
|
+
tools?: string[]
|
|
172
|
+
detail?: string
|
|
173
|
+
detailZh?: string
|
|
161
174
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -125,6 +125,10 @@ export const zh = {
|
|
|
125
125
|
marketRepoInstalled: '仓库已安装,技能稍候出现在「技能」页列表。',
|
|
126
126
|
marketUninstallConfirm: '确认移除?',
|
|
127
127
|
marketUninstallRootWarn: '将取消扫描该仓库并删除下载的副本(移除后可重新安装)。',
|
|
128
|
+
marketDetail: '详情',
|
|
129
|
+
marketSkillListLabel: '技能清单',
|
|
130
|
+
marketToolsLabel: '提供工具',
|
|
131
|
+
marketCommandLabel: '启动命令',
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
export const en = {
|
|
@@ -252,4 +256,8 @@ export const en = {
|
|
|
252
256
|
marketRepoInstalled: 'Repository installed — its skills appear on the Skills tab shortly.',
|
|
253
257
|
marketUninstallConfirm: 'Uninstall?',
|
|
254
258
|
marketUninstallRootWarn: 'Stops scanning the repository and deletes the downloaded copy (you can reinstall later).',
|
|
259
|
+
marketDetail: 'Details',
|
|
260
|
+
marketSkillListLabel: 'Skills included',
|
|
261
|
+
marketToolsLabel: 'Tools provided',
|
|
262
|
+
marketCommandLabel: 'Launch command',
|
|
255
263
|
}
|
package/src/market.test.ts
CHANGED
|
@@ -8,10 +8,13 @@ const okJson = (body: unknown): typeof fetch =>
|
|
|
8
8
|
describe('loadMarketIndex', () => {
|
|
9
9
|
it('prefers a valid remote index', async () => {
|
|
10
10
|
const index = await loadMarketIndex('skills', {
|
|
11
|
-
fetcher: okJson({ repos: [{ id: 'r1', name: 'Repo', description: 'd', url: 'https://github.com/a/b' }] }),
|
|
11
|
+
fetcher: okJson({ repos: [{ id: 'r1', name: 'Repo', description: 'd', url: 'https://github.com/a/b', detail: 'long', detailZh: '长文', skills: [{ name: 's1', description: 'first', descriptionZh: '第一个' }, { name: 's2' }, 'junk'] }] }),
|
|
12
12
|
})
|
|
13
13
|
expect(index?.source).toBe('remote')
|
|
14
14
|
expect(index?.skills?.[0]?.id).toBe('r1')
|
|
15
|
+
expect(index?.skills?.[0]?.detail).toBe('long')
|
|
16
|
+
expect(index?.skills?.[0]?.detailZh).toBe('长文')
|
|
17
|
+
expect(index?.skills?.[0]?.skills).toEqual([{ name: 's1', description: 'first', descriptionZh: '第一个' }, { name: 's2' }])
|
|
15
18
|
})
|
|
16
19
|
|
|
17
20
|
it('falls back to the bundled snapshot when remote is unreachable or invalid', async () => {
|
|
@@ -29,7 +32,7 @@ describe('loadMarketIndex', () => {
|
|
|
29
32
|
const index = await loadMarketIndex('mcp', {
|
|
30
33
|
fetcher: okJson({
|
|
31
34
|
servers: [
|
|
32
|
-
{ id: 'ok', name: 'OK', description: 'd', transport: 'stdio', command: 'npx', args: ['-y', 'x'], envKeys: ['K'], homepage: 'https://x' },
|
|
35
|
+
{ id: 'ok', name: 'OK', description: 'd', transport: 'stdio', command: 'npx', args: ['-y', 'x'], envKeys: ['K'], homepage: 'https://x', tools: ['t1'], detail: 'long' },
|
|
33
36
|
{ id: 'no-command', name: 'Bad', description: 'd', transport: 'stdio', homepage: 'https://x' },
|
|
34
37
|
{ id: 'no-home', name: 'Bad2', description: 'd', transport: 'stdio', command: 'npx' },
|
|
35
38
|
'not an object',
|
|
@@ -38,7 +41,7 @@ describe('loadMarketIndex', () => {
|
|
|
38
41
|
})
|
|
39
42
|
expect(index?.source).toBe('remote')
|
|
40
43
|
expect(index?.servers).toHaveLength(1)
|
|
41
|
-
expect(index?.servers?.[0]).toMatchObject({ id: 'ok', envKeys: ['K'] })
|
|
44
|
+
expect(index?.servers?.[0]).toMatchObject({ id: 'ok', envKeys: ['K'], tools: ['t1'], detail: 'long' })
|
|
42
45
|
})
|
|
43
46
|
|
|
44
47
|
it('ships parseable bundled snapshots with expected top-level shape', () => {
|
|
@@ -54,4 +57,12 @@ describe('loadMarketIndex', () => {
|
|
|
54
57
|
const index = await loadMarketIndex('mcp', { fetcher: okJson({ servers: [{ id: 'http1', name: 'H', description: 'd', transport: 'streamable-http', url: 'https://e/mcp', homepage: 'https://x' }] }) })
|
|
55
58
|
expect(index?.servers?.[0]).toMatchObject({ transport: 'streamable-http', url: 'https://e/mcp' })
|
|
56
59
|
})
|
|
60
|
+
|
|
61
|
+
it('bundles skill and tool inventories for the market detail view', async () => {
|
|
62
|
+
const failing = (async () => { throw new Error('offline') }) as unknown as typeof fetch
|
|
63
|
+
const skills = await loadMarketIndex('skills', { fetcher: failing })
|
|
64
|
+
expect(skills?.skills?.some(repo => (repo.skills?.length ?? 0) > 0)).toBe(true)
|
|
65
|
+
const mcp = await loadMarketIndex('mcp', { fetcher: failing })
|
|
66
|
+
expect(mcp?.servers?.some(server => (server.tools?.length ?? 0) > 0)).toBe(true)
|
|
67
|
+
})
|
|
57
68
|
})
|
package/src/market.ts
CHANGED
|
@@ -13,6 +13,13 @@ import { fileURLToPath } from 'node:url'
|
|
|
13
13
|
/** Which catalog a request wants. */
|
|
14
14
|
export type MarketKind = 'skills' | 'mcp'
|
|
15
15
|
|
|
16
|
+
/** One skill inside a market repository's inventory. */
|
|
17
|
+
export interface MarketSkillItem {
|
|
18
|
+
name: string
|
|
19
|
+
description?: string
|
|
20
|
+
descriptionZh?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
/** One installable skill repository in the skills index. */
|
|
17
24
|
export interface MarketSkillRepo {
|
|
18
25
|
id: string
|
|
@@ -20,9 +27,14 @@ export interface MarketSkillRepo {
|
|
|
20
27
|
nameZh?: string
|
|
21
28
|
description: string
|
|
22
29
|
descriptionZh?: string
|
|
30
|
+
/** Long-form intro for the detail view (falls back to description). */
|
|
31
|
+
detail?: string
|
|
32
|
+
detailZh?: string
|
|
23
33
|
url: string
|
|
24
34
|
homepage?: string
|
|
25
35
|
skillCount?: number
|
|
36
|
+
/** Skill inventory for the market detail view. */
|
|
37
|
+
skills?: MarketSkillItem[]
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
/** One installable server in the MCP index. */
|
|
@@ -41,6 +53,11 @@ export interface MarketMcpServer {
|
|
|
41
53
|
category?: string
|
|
42
54
|
/** Runtime hint shown next to stdio commands (npx / uvx). */
|
|
43
55
|
runtime?: string
|
|
56
|
+
/** Tool names the server exposes, for the market detail view. */
|
|
57
|
+
tools?: string[]
|
|
58
|
+
/** Long-form intro for the detail view (falls back to description). */
|
|
59
|
+
detail?: string
|
|
60
|
+
detailZh?: string
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
export interface MarketIndex {
|
|
@@ -62,6 +79,22 @@ export function bundledMarketPath(kind: MarketKind): string {
|
|
|
62
79
|
const isString = (value: unknown): value is string => typeof value === 'string' && value !== ''
|
|
63
80
|
const isStringArray = (value: unknown): value is string[] => Array.isArray(value) && value.every(isString)
|
|
64
81
|
|
|
82
|
+
function parseSkillItems(value: unknown): MarketSkillItem[] | null {
|
|
83
|
+
if (!Array.isArray(value)) return null
|
|
84
|
+
const out: MarketSkillItem[] = []
|
|
85
|
+
for (const item of value) {
|
|
86
|
+
if (typeof item !== 'object' || item === null) continue
|
|
87
|
+
const record = item as Record<string, unknown>
|
|
88
|
+
if (!isString(record.name)) continue
|
|
89
|
+
out.push({
|
|
90
|
+
name: record.name,
|
|
91
|
+
...isString(record.description) ? { description: record.description } : {},
|
|
92
|
+
...isString(record.descriptionZh) ? { descriptionZh: record.descriptionZh } : {},
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
return out.length > 0 ? out : null
|
|
96
|
+
}
|
|
97
|
+
|
|
65
98
|
function parseSkillsIndex(parsed: unknown): MarketSkillRepo[] | null {
|
|
66
99
|
if (typeof parsed !== 'object' || parsed === null) return null
|
|
67
100
|
const repos = (parsed as { repos?: unknown }).repos
|
|
@@ -71,15 +104,19 @@ function parseSkillsIndex(parsed: unknown): MarketSkillRepo[] | null {
|
|
|
71
104
|
if (typeof entry !== 'object' || entry === null) continue
|
|
72
105
|
const record = entry as Record<string, unknown>
|
|
73
106
|
if (!isString(record.id) || !isString(record.name) || !isString(record.description) || !isString(record.url)) continue
|
|
107
|
+
const skills = parseSkillItems(record.skills)
|
|
74
108
|
out.push({
|
|
75
109
|
id: record.id,
|
|
76
110
|
name: record.name,
|
|
77
111
|
...isString(record.nameZh) ? { nameZh: record.nameZh } : {},
|
|
78
112
|
description: record.description,
|
|
79
113
|
...isString(record.descriptionZh) ? { descriptionZh: record.descriptionZh } : {},
|
|
114
|
+
...isString(record.detail) ? { detail: record.detail } : {},
|
|
115
|
+
...isString(record.detailZh) ? { detailZh: record.detailZh } : {},
|
|
80
116
|
url: record.url,
|
|
81
117
|
...isString(record.homepage) ? { homepage: record.homepage } : {},
|
|
82
118
|
...(typeof record.skillCount === 'number' ? { skillCount: record.skillCount } : {}),
|
|
119
|
+
...(skills !== null ? { skills } : {}),
|
|
83
120
|
})
|
|
84
121
|
}
|
|
85
122
|
return out.length > 0 ? out : null
|
|
@@ -111,6 +148,9 @@ function parseMcpIndex(parsed: unknown): MarketMcpServer[] | null {
|
|
|
111
148
|
homepage: record.homepage,
|
|
112
149
|
...isString(record.category) ? { category: record.category } : {},
|
|
113
150
|
...isString(record.runtime) ? { runtime: record.runtime } : {},
|
|
151
|
+
...isStringArray(record.tools) ? { tools: record.tools } : {},
|
|
152
|
+
...isString(record.detail) ? { detail: record.detail } : {},
|
|
153
|
+
...isString(record.detailZh) ? { detailZh: record.detailZh } : {},
|
|
114
154
|
})
|
|
115
155
|
}
|
|
116
156
|
return out.length > 0 ? out : null
|