deepseek-harness-skill-mcp 0.1.0
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/LICENSE +201 -0
- package/README.md +152 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +675 -0
- package/package.json +70 -0
- package/src/client/controller.js +41 -0
- package/src/client/index.jsx +262 -0
- package/src/client/locales.js +65 -0
- package/src/client/styles.css +34 -0
- package/src/index.js +54 -0
- package/src/skills.js +187 -0
- package/src/store.js +216 -0
- package/src/web.js +78 -0
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deepseek-harness-skill-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Skill path and MCP server management for DeepSeek Harness Web",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/civilization-os/dsh-skill-mcp.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/civilization-os/dsh-skill-mcp#readme",
|
|
11
|
+
"bugs": "https://github.com/civilization-os/dsh-skill-mcp/issues",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public",
|
|
14
|
+
"provenance": true,
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/index.js",
|
|
20
|
+
"./client": "./lib/client.js",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"lib",
|
|
26
|
+
"cordis.patch.yml",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"dsh": {
|
|
31
|
+
"bundle": {
|
|
32
|
+
"patch": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"client": {
|
|
35
|
+
"platform": "web",
|
|
36
|
+
"inject": [
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "node --test tests/*.test.js",
|
|
43
|
+
"build": "node scripts/build.js",
|
|
44
|
+
"web": "node scripts/web.js",
|
|
45
|
+
"setup": "node scripts/setup.js"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.19.0"
|
|
49
|
+
},
|
|
50
|
+
"packageManager": "pnpm@10.33.0",
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@deepseek-ai/dsh-mcp-client": "0.1.2-rc.1",
|
|
53
|
+
"@deepseek-ai/dsh-skill-filesystem": "0.1.2-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-tools": "0.1.2-rc.1",
|
|
55
|
+
"@deepseek-ai/schemastery": "3.18.2",
|
|
56
|
+
"yaml": "^2.9.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@deepseek-ai/cordis": ">=4.0.2"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
63
|
+
"@deepseek-ai/dsh-skill": "0.1.2-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
|
|
65
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
66
|
+
"esbuild": "^0.28.2",
|
|
67
|
+
"react": "^19.2.8",
|
|
68
|
+
"react-dom": "^19.2.8"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** One observable per mounted plugin; drafts stay in the settings component. */
|
|
2
|
+
export class ExtensionsController {
|
|
3
|
+
state = { extensions: [], sources: [], skills: [], revision: '', loading: false, saving: false, error: '', saved: false }
|
|
4
|
+
listeners = new Set()
|
|
5
|
+
lifetime = new AbortController()
|
|
6
|
+
generation = 0
|
|
7
|
+
silentPending = false
|
|
8
|
+
constructor(call) { this.call = call }
|
|
9
|
+
getSnapshot = () => this.state
|
|
10
|
+
subscribe = listener => { this.listeners.add(listener); return () => this.listeners.delete(listener) }
|
|
11
|
+
publish(patch) {
|
|
12
|
+
if (this.lifetime.signal.aborted) return
|
|
13
|
+
this.state = { ...this.state, ...patch }
|
|
14
|
+
for (const listener of this.listeners) listener()
|
|
15
|
+
}
|
|
16
|
+
async request(endpoint, args = {}, options = {}) {
|
|
17
|
+
if (this.state.saving || this.lifetime.signal.aborted) return false
|
|
18
|
+
const silent = endpoint === 'list' && options.silent === true
|
|
19
|
+
if (silent && (this.state.loading || this.silentPending)) return false
|
|
20
|
+
if (silent) this.silentPending = true
|
|
21
|
+
const generation = ++this.generation
|
|
22
|
+
const saving = endpoint !== 'list'
|
|
23
|
+
if (!silent) this.publish({ loading: !saving, saving, error: '', saved: false })
|
|
24
|
+
try {
|
|
25
|
+
const result = await this.call(endpoint, { ...args, revision: this.state.revision }, this.lifetime.signal)
|
|
26
|
+
if (this.lifetime.signal.aborted || generation !== this.generation) return false
|
|
27
|
+
if (!result.ok) {
|
|
28
|
+
if (!silent) this.publish({ error: result.error.code === 'extensions/conflict' ? 'conflict' : 'failed', loading: false, saving: false })
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
this.publish(silent ? result.value : { ...result.value, loading: false, saving: false, saved: saving })
|
|
32
|
+
return true
|
|
33
|
+
} catch {
|
|
34
|
+
if (!silent && generation === this.generation) this.publish({ error: 'failed', loading: false, saving: false })
|
|
35
|
+
return false
|
|
36
|
+
} finally {
|
|
37
|
+
if (silent) this.silentPending = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
dispose() { this.lifetime.abort(); this.listeners.clear() }
|
|
41
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/** Skill and MCP settings sections registered through the public Slots and Connection services. */
|
|
2
|
+
import { useEffect, useId, useState } from 'react'
|
|
3
|
+
import { ExtensionsController } from './controller.js'
|
|
4
|
+
import { zh, en } from './locales.js'
|
|
5
|
+
import css from './styles.css'
|
|
6
|
+
|
|
7
|
+
export const inject = ['slots', 'locale', 'connection']
|
|
8
|
+
const namespace = 'settings.extension-manager'
|
|
9
|
+
const settingsPollIntervalMs = 3000
|
|
10
|
+
|
|
11
|
+
export function apply(ctx) {
|
|
12
|
+
const controller = new ExtensionsController((endpoint, args, signal) => ctx.connection.rpc.call('/extensions', endpoint, args, signal))
|
|
13
|
+
ctx.effect(() => ctx.locale.register(namespace, { zh, en }))
|
|
14
|
+
ctx.effect(() => () => controller.dispose())
|
|
15
|
+
ctx.effect(() => {
|
|
16
|
+
const style = document.createElement('style')
|
|
17
|
+
style.textContent = css
|
|
18
|
+
document.head.append(style)
|
|
19
|
+
return () => style.remove()
|
|
20
|
+
})
|
|
21
|
+
ctx.on('connection/reset', () => { void controller.request('list') })
|
|
22
|
+
const t = ctx.locale.bind(namespace)
|
|
23
|
+
const face = { hooks: { manager: controller }, request: (endpoint, args, options) => controller.request(endpoint, args, options) }
|
|
24
|
+
// The two sections share one controller and managed patch; each kind gets its own settings page.
|
|
25
|
+
const section = (id, kind) => ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
26
|
+
name: 'settings.section', id, order: kind === 'skill' ? 16 : 17,
|
|
27
|
+
label: () => t(kind === 'skill' ? 'navSkill' : 'navMcp'), locale: namespace,
|
|
28
|
+
inject: () => face,
|
|
29
|
+
}, kind === 'skill' ? SkillSection : McpSection))
|
|
30
|
+
section('skill-manager', 'skill')
|
|
31
|
+
section('mcp-manager', 'mcp')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function SkillSection({ t, useManager, request }) {
|
|
35
|
+
const state = useManager(value => value)
|
|
36
|
+
const [form, setForm] = useState('')
|
|
37
|
+
const [query, setQuery] = useState('')
|
|
38
|
+
const [groupBy, setGroupBy] = useState('path')
|
|
39
|
+
const busy = state.loading || state.saving
|
|
40
|
+
const sources = state.sources ?? []
|
|
41
|
+
const skills = state.skills ?? []
|
|
42
|
+
const visible = skills.filter(skill => {
|
|
43
|
+
const source = sources.find(item => item.id === skill.sourceId)
|
|
44
|
+
return `${skill.name} ${skill.description} ${source?.location ?? ''} ${source?.group ?? ''}`.toLowerCase().includes(query.toLowerCase())
|
|
45
|
+
})
|
|
46
|
+
const counts = {
|
|
47
|
+
all: skills.length,
|
|
48
|
+
automatic: skills.filter(skill => skill.valid && skill.modelInvocable && skill.sourceEnabled && !skill.shadowed).length,
|
|
49
|
+
manual: skills.filter(skill => skill.valid && !skill.modelInvocable && skill.userInvocable && skill.sourceEnabled).length,
|
|
50
|
+
invalid: skills.filter(skill => !skill.valid).length,
|
|
51
|
+
}
|
|
52
|
+
useLiveRefresh(request)
|
|
53
|
+
return <section className="dsh-ext dsh-skill-manager">
|
|
54
|
+
<header className="dsh-ext-heading"><div><h2>{t('navSkill')}</h2><p>{t('skillIntro')}</p></div>
|
|
55
|
+
<div className="dsh-ext-heading-actions"><button type="button" disabled={busy || !state.revision} onClick={() => setForm('source')}>{t('add')}</button>
|
|
56
|
+
<button type="button" disabled={busy || sources.length === 0} onClick={() => setForm('skill')}>{t('newSkill')}</button>
|
|
57
|
+
<button type="button" disabled={busy} onClick={() => request('list')}>{t('refresh')}</button></div></header>
|
|
58
|
+
<PageState state={state} t={t} />
|
|
59
|
+
<div className="dsh-skill-stats">
|
|
60
|
+
{Object.entries(counts).map(([key, value]) => <div key={key}><strong>{value}</strong><span>{t(`stat_${key}`)}</span></div>)}
|
|
61
|
+
</div>
|
|
62
|
+
<label className="dsh-skill-search"><span className="dsh-visually-hidden">{t('search')}</span>
|
|
63
|
+
<input type="search" value={query} onChange={event => setQuery(event.target.value)} placeholder={t('searchPlaceholder')} /></label>
|
|
64
|
+
<div className="dsh-skill-grouping" role="group" aria-label={t('groupView')}>
|
|
65
|
+
<span>{t('groupView')}</span><button type="button" aria-pressed={groupBy === 'path'} onClick={() => setGroupBy('path')}>{t('groupByPath')}</button>
|
|
66
|
+
<button type="button" aria-pressed={groupBy === 'custom'} onClick={() => setGroupBy('custom')}>{t('groupByCustom')}</button>
|
|
67
|
+
</div>
|
|
68
|
+
{form === 'source' && <AddForm kind="skill" t={t} busy={busy} onCancel={() => setForm('')} onSave={async args => {
|
|
69
|
+
if (await request('add-skill', args)) setForm('')
|
|
70
|
+
}} />}
|
|
71
|
+
{form === 'skill' && <NewSkillForm sources={sources} t={t} busy={busy} onCancel={() => setForm('')} onSave={async args => {
|
|
72
|
+
if (await request('create-skill', args)) setForm('')
|
|
73
|
+
}} />}
|
|
74
|
+
{sources.length === 0 && <div className="dsh-ext-card dsh-ext-empty"><strong>{t('emptySkills')}</strong><p>{t('emptySkillsHint')}</p></div>}
|
|
75
|
+
<div className="dsh-skill-sources">{groupBy === 'path' ? sources.map(source => <SkillPath key={source.id} source={source} skills={skills} visible={visible} query={query} t={t} busy={busy} request={request} />)
|
|
76
|
+
: groupSources(sources.filter(source => !query || visible.some(skill => skill.sourceId === source.id))).map(group => <section className="dsh-skill-user-group" key={group.name}>
|
|
77
|
+
<header><h3>{group.name || t('ungrouped')} <span className="dsh-ext-count">{group.sources.reduce((total, source) => total + skills.filter(skill => skill.sourceId === source.id).length, 0)}</span></h3></header>
|
|
78
|
+
<div>{group.sources.map(source => <SkillPath key={source.id} source={source} skills={skills} visible={visible} query={query} t={t} busy={busy} request={request} nested />)}</div>
|
|
79
|
+
</section>)}</div>
|
|
80
|
+
<p className="dsh-ext-footnote">{t('skillFootnote')}</p>
|
|
81
|
+
</section>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function groupSources(sources) {
|
|
85
|
+
const groups = new Map()
|
|
86
|
+
for (const source of sources) {
|
|
87
|
+
const key = source.group || ''
|
|
88
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
89
|
+
groups.get(key).push(source)
|
|
90
|
+
}
|
|
91
|
+
return [...groups].map(([name, entries]) => ({ name, sources: entries }))
|
|
92
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function SkillPath({ source, skills, visible, query, t, busy, request, nested = false }) {
|
|
96
|
+
const rows = visible.filter(skill => skill.sourceId === source.id)
|
|
97
|
+
if (query && rows.length === 0) return null
|
|
98
|
+
return <section className={`dsh-skill-source${nested ? ' dsh-skill-source-nested' : ''}`}>
|
|
99
|
+
<header><div className="dsh-skill-source-title"><span className={`dsh-ext-status ${source.enabled ? 'dsh-ext-status-available' : ''}`} aria-hidden="true" />
|
|
100
|
+
<div><h3>{source.location} <span className="dsh-ext-count">{skills.filter(skill => skill.sourceId === source.id).length}</span></h3>
|
|
101
|
+
<small>{source.group ? `${t('customGroup')}: ${source.group}` : t('ungrouped')}</small></div></div>
|
|
102
|
+
<button type="button" role="switch" aria-checked={source.enabled} disabled={busy}
|
|
103
|
+
onClick={() => request('enable', { id: source.id, enabled: !source.enabled })}>{t(source.enabled ? 'disable' : 'enable')}</button></header>
|
|
104
|
+
<GroupEditor source={source} t={t} busy={busy} request={request} />
|
|
105
|
+
{source.issue && <p className="dsh-ext-notice">{t(`issue_${source.issue}`)}</p>}
|
|
106
|
+
{rows.length === 0 ? <div className="dsh-ext-empty"><p>{t('sourceEmpty')}</p></div>
|
|
107
|
+
: <ul>{rows.map(skill => <SkillRow key={skill.relativePath} skill={skill} t={t} busy={busy} request={request} />)}</ul>}
|
|
108
|
+
</section>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function GroupEditor({ source, t, busy, request }) {
|
|
112
|
+
const [group, setGroup] = useState(source.group)
|
|
113
|
+
useEffect(() => setGroup(source.group), [source.group])
|
|
114
|
+
return <form className="dsh-skill-group-editor" onSubmit={event => {
|
|
115
|
+
event.preventDefault()
|
|
116
|
+
void request('set-skill-group', { id: source.id, group })
|
|
117
|
+
}}><label>{t('customGroup')}<input value={group} maxLength={64} disabled={busy} onChange={event => setGroup(event.target.value)} placeholder={t('groupPlaceholder')} /></label>
|
|
118
|
+
<button type="submit" disabled={busy || group.trim() === source.group}>{t('saveGroup')}</button></form>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function McpSection({ t, useManager, request }) {
|
|
122
|
+
const state = useManager(value => value)
|
|
123
|
+
const [form, setForm] = useState(false)
|
|
124
|
+
const busy = state.loading || state.saving
|
|
125
|
+
const rows = state.extensions.filter(row => row.kind === 'mcp')
|
|
126
|
+
useLiveRefresh(request)
|
|
127
|
+
return <section className="dsh-ext">
|
|
128
|
+
<header className="dsh-ext-heading"><div><h2>{t('navMcp')}</h2><p>{t('mcpIntro')}</p></div>
|
|
129
|
+
<button type="button" disabled={busy} onClick={() => request('list')}>{t('refresh')}</button></header>
|
|
130
|
+
<PageState state={state} t={t} />
|
|
131
|
+
<section className="dsh-ext-card">
|
|
132
|
+
<header><h3>{t('mcp')} <span className="dsh-ext-count">{rows.length}</span></h3>
|
|
133
|
+
<button type="button" disabled={busy || !state.revision} onClick={() => setForm(true)}>{t('addMcp')}</button></header>
|
|
134
|
+
{rows.length === 0 && <div className="dsh-ext-empty"><strong>{t('emptyMcp')}</strong><p>{t('emptyMcpHint')}</p></div>}
|
|
135
|
+
<ul>{rows.map(row => <li key={row.id}><div className="dsh-ext-details"><strong>{row.id}</strong><code>{row.location}</code><McpRuntime row={row} t={t} /></div>
|
|
136
|
+
<button type="button" role="switch" aria-checked={row.enabled} aria-label={`${t(row.enabled ? 'disable' : 'enable')} ${row.id}`} disabled={busy}
|
|
137
|
+
onClick={() => request('enable', { id: row.id, enabled: !row.enabled })}>{t(row.enabled ? 'disable' : 'enable')}</button></li>)}</ul>
|
|
138
|
+
{form && <AddForm kind="mcp" t={t} busy={busy} onCancel={() => setForm(false)} onSave={async args => {
|
|
139
|
+
if (await request('add-mcp', args)) setForm(false)
|
|
140
|
+
}} />}
|
|
141
|
+
</section>
|
|
142
|
+
<p className="dsh-ext-footnote">{t('mcpFootnote')}</p>
|
|
143
|
+
</section>
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function useLiveRefresh(request) {
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
void request('list')
|
|
149
|
+
let timer
|
|
150
|
+
const poll = () => {
|
|
151
|
+
if (document.visibilityState === 'visible') void request('list', {}, { silent: true })
|
|
152
|
+
timer = window.setTimeout(poll, settingsPollIntervalMs)
|
|
153
|
+
}
|
|
154
|
+
const refreshWhenVisible = () => {
|
|
155
|
+
if (document.visibilityState === 'visible') void request('list', {}, { silent: true })
|
|
156
|
+
}
|
|
157
|
+
timer = window.setTimeout(poll, settingsPollIntervalMs)
|
|
158
|
+
document.addEventListener('visibilitychange', refreshWhenVisible)
|
|
159
|
+
return () => {
|
|
160
|
+
window.clearTimeout(timer)
|
|
161
|
+
document.removeEventListener('visibilitychange', refreshWhenVisible)
|
|
162
|
+
}
|
|
163
|
+
}, [request])
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function PageState({ state, t }) {
|
|
167
|
+
return <>{state.loading && <p role="status">{t('loading')}</p>}
|
|
168
|
+
{state.error && <p className="dsh-ext-notice" role="alert">{t(state.error)}</p>}
|
|
169
|
+
{state.saved && <p className="dsh-ext-notice" role="status">{t('saved')}</p>}</>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function SkillRow({ skill, t, busy, request }) {
|
|
173
|
+
const state = !skill.valid ? 'invalid' : !skill.sourceEnabled || (!skill.modelInvocable && !skill.userInvocable)
|
|
174
|
+
? 'disabled' : skill.shadowed ? 'shadowed' : !skill.modelInvocable ? 'manual' : 'available'
|
|
175
|
+
const invocation = skill.modelInvocable && skill.userInvocable ? 'both'
|
|
176
|
+
: skill.modelInvocable ? 'modelOnly' : skill.userInvocable ? 'manualOnly' : 'none'
|
|
177
|
+
const statusLabel = !skill.valid ? 'skillStatusInvalid' : !skill.sourceEnabled ? 'skillStatusSourceDisabled'
|
|
178
|
+
: skill.shadowed ? 'skillStatusShadowed' : ''
|
|
179
|
+
const update = patch => request('set-skill-invocation', {
|
|
180
|
+
sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision,
|
|
181
|
+
modelInvocable: skill.modelInvocable, userInvocable: skill.userInvocable, ...patch,
|
|
182
|
+
})
|
|
183
|
+
return <li className="dsh-skill-row"><div className="dsh-skill-row-main">
|
|
184
|
+
<div className="dsh-skill-name"><span className={`dsh-ext-status dsh-ext-status-${state}`} aria-hidden="true" /><strong>{skill.name}</strong>
|
|
185
|
+
<span className="dsh-skill-badge">{t(`format_${skill.format}`)}</span><span className="dsh-skill-badge">{t(`invocation_${invocation}`)}</span>
|
|
186
|
+
{statusLabel && <span className={`dsh-skill-badge dsh-skill-badge-${state}`}>{t(statusLabel)}</span>}</div>
|
|
187
|
+
<p>{skill.description || t('noDescription')}</p><code>{skill.relativePath}</code>
|
|
188
|
+
<details><summary>{t('details')}</summary><div className="dsh-skill-detail">
|
|
189
|
+
{skill.whenToUse && <p><strong>{t('whenToUse')}</strong>{skill.whenToUse}</p>}
|
|
190
|
+
{skill.issues.length > 0 && <div><strong>{t('diagnostics')}</strong><ul>{skill.issues.map(issue => <li key={issue}>{t(`issue_${issue}`)}</li>)}</ul></div>}
|
|
191
|
+
<div className="dsh-skill-invocation"><strong>{t('invocation')}</strong>
|
|
192
|
+
<label><input type="checkbox" checked={skill.modelInvocable} disabled={busy || !skill.valid} onChange={event => update({ modelInvocable: event.target.checked })} /> {t('modelInvocable')}</label>
|
|
193
|
+
<label><input type="checkbox" checked={skill.userInvocable} disabled={busy || !skill.valid} onChange={event => update({ userInvocable: event.target.checked })} /> {t('userInvocable')}</label></div>
|
|
194
|
+
<div><strong>{t('resources')}</strong>{skill.resources.length
|
|
195
|
+
? <ul className="dsh-skill-resources">{skill.resources.map(path => <li key={path}><code>{path}</code></li>)}</ul>
|
|
196
|
+
: <p>{t('noResources')}</p>}</div>
|
|
197
|
+
</div></details>
|
|
198
|
+
</div></li>
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function McpRuntime({ row, t }) {
|
|
202
|
+
const tools = row.tools ?? []
|
|
203
|
+
const state = !row.enabled ? 'disabled' : tools.length ? 'available' : 'waiting'
|
|
204
|
+
const prefix = `mcp__${row.id}__`
|
|
205
|
+
return <div className="dsh-ext-runtime">
|
|
206
|
+
<span className={`dsh-ext-status dsh-ext-status-${state}`} aria-hidden="true" />
|
|
207
|
+
<span className="dsh-ext-status-label">{t(`status_${state}`)}</span>
|
|
208
|
+
{tools.length > 0 && <details><summary>{t('tools')} · {tools.length}</summary>
|
|
209
|
+
<ul className="dsh-ext-tools">{tools.map(name => <li key={name}><code title={name}>{name.startsWith(prefix) ? name.slice(prefix.length) : name}</code></li>)}</ul>
|
|
210
|
+
</details>}
|
|
211
|
+
</div>
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function NewSkillForm({ sources, t, busy, onSave, onCancel }) {
|
|
215
|
+
const prefix = useId()
|
|
216
|
+
return <form className="dsh-ext-form dsh-skill-create" onSubmit={event => {
|
|
217
|
+
event.preventDefault()
|
|
218
|
+
const data = new FormData(event.currentTarget)
|
|
219
|
+
void onSave({
|
|
220
|
+
sourceId: data.get('sourceId'), name: data.get('name'), description: data.get('description'),
|
|
221
|
+
whenToUse: data.get('whenToUse'), modelInvocable: data.has('modelInvocable'),
|
|
222
|
+
userInvocable: data.has('userInvocable'), structure: data.get('structure'),
|
|
223
|
+
})
|
|
224
|
+
}}><fieldset disabled={busy}>
|
|
225
|
+
<label htmlFor={`${prefix}-sourceId`}>{t('source')}<select id={`${prefix}-sourceId`} name="sourceId">
|
|
226
|
+
{sources.map(source => <option key={source.id} value={source.id}>{source.id}</option>)}</select></label>
|
|
227
|
+
<label htmlFor={`${prefix}-name`}>{t('skillName')}<input id={`${prefix}-name`} name="name" required pattern="[a-z0-9]+(?:-[a-z0-9]+)*" autoComplete="off" /></label>
|
|
228
|
+
<label className="dsh-ext-span" htmlFor={`${prefix}-description`}>{t('description')}<textarea id={`${prefix}-description`} name="description" rows={3} required /></label>
|
|
229
|
+
<label className="dsh-ext-span" htmlFor={`${prefix}-whenToUse`}>{t('whenToUse')}<textarea id={`${prefix}-whenToUse`} name="whenToUse" rows={2} /></label>
|
|
230
|
+
<label htmlFor={`${prefix}-structure`}>{t('structure')}<select id={`${prefix}-structure`} name="structure">
|
|
231
|
+
<option value="minimal">{t('structureMinimal')}</option><option value="standard">{t('structureStandard')}</option></select></label>
|
|
232
|
+
<div className="dsh-skill-create-flags"><span>{t('invocation')}</span>
|
|
233
|
+
<label><input type="checkbox" name="modelInvocable" defaultChecked /> {t('modelInvocable')}</label>
|
|
234
|
+
<label><input type="checkbox" name="userInvocable" defaultChecked /> {t('userInvocable')}</label></div>
|
|
235
|
+
<div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'create')}</button></div>
|
|
236
|
+
</fieldset></form>
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function AddForm({ kind, t, busy, onSave, onCancel }) {
|
|
240
|
+
const prefix = useId()
|
|
241
|
+
const [transport, setTransport] = useState('stdio')
|
|
242
|
+
const field = (name, required = true, multiline = false) => <label htmlFor={`${prefix}-${name}`}>{t(name)}{multiline
|
|
243
|
+
? <textarea id={`${prefix}-${name}`} name={name} rows={3} />
|
|
244
|
+
: <input id={`${prefix}-${name}`} name={name} required={required} autoComplete="off" {...(name === 'id' ? { pattern: '[A-Za-z0-9_\\-]{1,32}', maxLength: 32 } : {})} />}</label>
|
|
245
|
+
return <form className="dsh-ext-form" onSubmit={event => {
|
|
246
|
+
event.preventDefault()
|
|
247
|
+
const data = Object.fromEntries(new FormData(event.currentTarget))
|
|
248
|
+
const args = kind === 'skill' ? { directory: data.directory, group: data.group ?? '' } : {
|
|
249
|
+
id: data.id, configuration: JSON.stringify(transport === 'stdio'
|
|
250
|
+
? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
|
|
251
|
+
: { transport, url: data.url }),
|
|
252
|
+
}
|
|
253
|
+
void onSave(args)
|
|
254
|
+
}}><fieldset disabled={busy}>{kind === 'skill' ? <>{field('directory')} {field('group', false)}</> : <>{field('id')}
|
|
255
|
+
<label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
|
|
256
|
+
<option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option></select></label>
|
|
257
|
+
{transport === 'stdio' ? <>{field('command')}{field('args', false, true)}{field('cwd', false)}</> : field('url')}
|
|
258
|
+
<p>{t('secret')}</p>
|
|
259
|
+
</>}
|
|
260
|
+
<div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'save')}</button></div>
|
|
261
|
+
</fieldset></form>
|
|
262
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Both dictionaries use the same key vocabulary; checked by tests. */
|
|
2
|
+
export const zh = {
|
|
3
|
+
navSkill: 'Skill 管理', navMcp: 'MCP 管理',
|
|
4
|
+
skillIntro: '配置一个或多个 Skill 路径;每个路径下可以包含多个技能。', mcpIntro: '管理外部工具服务器,让它们在当前配置中可用。',
|
|
5
|
+
skills: 'Skill 路径', mcp: 'MCP 服务器', refresh: '刷新', add: '添加路径', addMcp: '添加服务器', newSkill: '新建 Skill',
|
|
6
|
+
emptySkills: '还没有配置 Skill 路径', emptySkillsHint: '指定一个本地 skills 目录,目录下的各个子目录就是技能。',
|
|
7
|
+
emptyMcp: '还没有添加 MCP 服务器', emptyMcpHint: '通过本地命令或 HTTP 地址连接外部工具。',
|
|
8
|
+
id: '标识', directory: 'Skill 路径', group: '用户分组(可选)', transport: '配置类型', stdio: 'stdio', http: 'HTTP',
|
|
9
|
+
command: '命令', args: '参数(每行一个)', cwd: '工作目录(可选)', url: '服务器地址',
|
|
10
|
+
cancel: '取消', save: '添加', create: '创建', saving: '保存中…', loading: '加载中…',
|
|
11
|
+
enabled: '配置已启用', disabled: '已禁用', enable: '启用', disable: '禁用',
|
|
12
|
+
saved: '更改已保存。', failed: '操作失败,请检查输入、连接或服务端日志后重试。',
|
|
13
|
+
conflict: '配置已被修改,请刷新后重试。未提交的表单内容已保留。',
|
|
14
|
+
skillFootnote: '新路径默认禁用。启用后,Skill provider 会扫描该路径下的所有技能。配置变更会自动生效。',
|
|
15
|
+
mcpFootnote: '新服务器默认禁用。启用后会执行所填命令或连接所填地址。配置变更会自动生效。',
|
|
16
|
+
secret: '请勿填写密钥或带令牌的地址。此版本暂不支持认证参数。',
|
|
17
|
+
tools: '工具列表', status_available: '工具可用', status_waiting: '等待发现工具', status_disabled: '已禁用',
|
|
18
|
+
skillHint: '启用后由 Skill provider 扫描;每个配置项对应一个包含多个技能的目录。',
|
|
19
|
+
stat_all: '全部技能', stat_automatic: '模型可用', stat_manual: '仅手动', stat_invalid: '配置异常',
|
|
20
|
+
search: '搜索', searchPlaceholder: '搜索名称、描述、路径或分组…', sourceEmpty: '这个路径下还没有发现 Skill。',
|
|
21
|
+
groupView: '分组方式', groupByPath: '按路径', groupByCustom: '按用户分组', customGroup: '用户分组', ungrouped: '未分组',
|
|
22
|
+
groupPlaceholder: '例如:工作、个人、开发', saveGroup: '保存分组',
|
|
23
|
+
noDescription: '没有可用的描述', details: '查看详情', whenToUse: '适用场景', diagnostics: '格式诊断',
|
|
24
|
+
invocation: '调用权限', modelInvocable: '允许模型自动选择', userInvocable: '允许用户通过 /name 调用',
|
|
25
|
+
invocation_both: '自动 + 手动', invocation_modelOnly: '仅模型', invocation_manualOnly: '仅手动', invocation_none: '完全禁用',
|
|
26
|
+
format_bundle: '目录', format_flat: '单文件', skillStatusInvalid: '格式异常', skillStatusSourceDisabled: '来源已禁用', skillStatusShadowed: '被同名覆盖',
|
|
27
|
+
resources: '资源文件', noResources: '没有附加资源', source: '保存来源', skillName: 'Skill 名称', description: '描述',
|
|
28
|
+
structure: '目录结构', structureMinimal: '仅 SKILL.md', structureStandard: '标准目录(scripts / references / assets)',
|
|
29
|
+
'issue_missing-frontmatter': '缺少 YAML frontmatter', 'issue_invalid-yaml': 'YAML 格式错误',
|
|
30
|
+
'issue_invalid-frontmatter': 'frontmatter 必须是对象', 'issue_invalid-name': 'name 必须使用 kebab-case',
|
|
31
|
+
'issue_missing-description': '缺少 description', 'issue_invalid-invocation': '调用权限字段不是有效布尔值',
|
|
32
|
+
issue_unreadable: '文件无法读取', 'issue_source-unreadable': 'Skill 路径无法读取',
|
|
33
|
+
}
|
|
34
|
+
export const en = {
|
|
35
|
+
navSkill: 'Skills', navMcp: 'MCP',
|
|
36
|
+
skillIntro: 'Configure one or more Skill paths; each path can contain multiple skills.', mcpIntro: 'Manage external tool servers for this configuration.',
|
|
37
|
+
skills: 'Skill paths', mcp: 'MCP servers', refresh: 'Refresh', add: 'Add path', addMcp: 'Add server', newSkill: 'New Skill',
|
|
38
|
+
emptySkills: 'No Skill paths configured', emptySkillsHint: 'Choose a local skills directory whose children contain the individual skills.',
|
|
39
|
+
emptyMcp: 'No MCP servers yet', emptyMcpHint: 'Connect external tools with a local command or HTTP endpoint.',
|
|
40
|
+
id: 'Identifier', directory: 'Skill path', group: 'User group (optional)', transport: 'Type', stdio: 'stdio', http: 'HTTP',
|
|
41
|
+
command: 'Command', args: 'Arguments (one per line)', cwd: 'Working directory (optional)', url: 'Server URL',
|
|
42
|
+
cancel: 'Cancel', save: 'Add', create: 'Create', saving: 'Saving…', loading: 'Loading…',
|
|
43
|
+
enabled: 'Configured enabled', disabled: 'Disabled', enable: 'Enable', disable: 'Disable',
|
|
44
|
+
saved: 'Changes saved.', failed: 'Operation failed. Check input, connection or server logs and retry.',
|
|
45
|
+
conflict: 'Configuration changed. Refresh and retry; your draft has been retained.',
|
|
46
|
+
skillFootnote: 'New paths are disabled. Once enabled, the Skill provider scans every skill below them.',
|
|
47
|
+
mcpFootnote: 'New servers are disabled. Enabling one runs its command or connects to its endpoint; changes apply automatically.',
|
|
48
|
+
secret: 'Do not enter secrets or token-bearing URLs. Authentication fields are not supported yet.',
|
|
49
|
+
tools: 'Tools', status_available: 'Tools available', status_waiting: 'Waiting for tools', status_disabled: 'Disabled',
|
|
50
|
+
skillHint: 'The Skill provider scans enabled paths; every configured path contains multiple skills.',
|
|
51
|
+
stat_all: 'All skills', stat_automatic: 'Model enabled', stat_manual: 'Manual only', stat_invalid: 'Invalid',
|
|
52
|
+
search: 'Search', searchPlaceholder: 'Search name, description, path or group…', sourceEmpty: 'No Skills were found in this path.',
|
|
53
|
+
groupView: 'Group view', groupByPath: 'By path', groupByCustom: 'By user group', customGroup: 'User group', ungrouped: 'Ungrouped',
|
|
54
|
+
groupPlaceholder: 'For example: Work, Personal, Development', saveGroup: 'Save group',
|
|
55
|
+
noDescription: 'No usable description', details: 'View details', whenToUse: 'When to use', diagnostics: 'Diagnostics',
|
|
56
|
+
invocation: 'Invocation', modelInvocable: 'Allow model selection', userInvocable: 'Allow user /name invocation',
|
|
57
|
+
invocation_both: 'Model + user', invocation_modelOnly: 'Model only', invocation_manualOnly: 'Manual only', invocation_none: 'Fully disabled',
|
|
58
|
+
format_bundle: 'Bundle', format_flat: 'Flat file', skillStatusInvalid: 'Invalid', skillStatusSourceDisabled: 'Source disabled', skillStatusShadowed: 'Shadowed',
|
|
59
|
+
resources: 'Resource files', noResources: 'No additional resources', source: 'Source', skillName: 'Skill name', description: 'Description',
|
|
60
|
+
structure: 'Structure', structureMinimal: 'SKILL.md only', structureStandard: 'Standard folders (scripts / references / assets)',
|
|
61
|
+
'issue_missing-frontmatter': 'Missing YAML frontmatter', 'issue_invalid-yaml': 'Invalid YAML',
|
|
62
|
+
'issue_invalid-frontmatter': 'Frontmatter must be an object', 'issue_invalid-name': 'name must use kebab-case',
|
|
63
|
+
'issue_missing-description': 'Missing description', 'issue_invalid-invocation': 'Invocation field is not a valid boolean',
|
|
64
|
+
issue_unreadable: 'File cannot be read', 'issue_source-unreadable': 'Skill path cannot be read',
|
|
65
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.dsh-ext{color:var(--dsw-alias-label-primary);max-width:840px;padding:4px 0 28px;font:inherit}
|
|
2
|
+
.dsh-ext-heading,.dsh-ext-card>header{display:flex;justify-content:space-between;align-items:center;gap:16px}
|
|
3
|
+
.dsh-ext h2{font-size:22px;letter-spacing:-.4px;margin:0 0 8px}.dsh-ext h3{font-size:15px;margin:0;display:flex;align-items:center;gap:8px}
|
|
4
|
+
.dsh-ext p,.dsh-ext small{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.6}.dsh-ext-heading p{margin:0}
|
|
5
|
+
.dsh-ext button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 13px;white-space:nowrap}
|
|
6
|
+
.dsh-ext button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-ext button:disabled{opacity:.5;cursor:default}
|
|
7
|
+
.dsh-ext :is(button,input,select,textarea):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}
|
|
8
|
+
.dsh-ext-card{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;margin-top:20px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
9
|
+
.dsh-ext-card>header{padding:18px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext-count{font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary)}
|
|
10
|
+
.dsh-ext-empty{padding:32px 20px}.dsh-ext-empty strong{font-size:14px;font-weight:500}.dsh-ext-empty p{margin:6px 0 0}
|
|
11
|
+
.dsh-ext ul{list-style:none;padding:0;margin:0}.dsh-ext li{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext li:last-child{border:0}
|
|
12
|
+
.dsh-ext-details{display:flex;flex-direction:column;gap:5px;min-width:0}.dsh-ext-details strong{font-size:14px}.dsh-ext-details code{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}
|
|
13
|
+
.dsh-ext-runtime{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
14
|
+
.dsh-ext-status{width:7px;height:7px;border-radius:50%;background:#858585;box-shadow:0 0 0 3px color-mix(in srgb,#858585 16%,transparent)}
|
|
15
|
+
.dsh-ext-status-available{background:#38a169;box-shadow:0 0 0 3px color-mix(in srgb,#38a169 18%,transparent)}.dsh-ext-status-waiting{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}
|
|
16
|
+
.dsh-ext-runtime details{width:100%;margin-top:2px}.dsh-ext-runtime summary{width:max-content;cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none}.dsh-ext-runtime summary:hover{color:var(--dsw-alias-label-primary)}
|
|
17
|
+
.dsh-ext-runtime .dsh-ext-tools{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.dsh-ext-runtime .dsh-ext-tools li{display:block;border:0;padding:4px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-3)}
|
|
18
|
+
.dsh-ext-form{padding:20px;border-top:1px solid var(--dsw-alias-border-l4)}.dsh-ext fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:1fr 1fr;gap:16px}.dsh-ext label{display:flex;flex-direction:column;gap:7px;font-size:13px}
|
|
19
|
+
.dsh-ext input,.dsh-ext select,.dsh-ext textarea{box-sizing:border-box;min-width:0;width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4);border-radius:8px;padding:10px}.dsh-ext textarea{resize:vertical}
|
|
20
|
+
.dsh-ext-actions{display:flex;justify-content:flex-end;gap:8px;grid-column:1/-1}.dsh-ext fieldset>p{grid-column:1/-1;margin:0}.dsh-ext-notice{padding:12px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:10px}.dsh-ext-footnote{padding:0 4px}
|
|
21
|
+
.dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
22
|
+
.dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
23
|
+
.dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}
|
|
24
|
+
.dsh-skill-grouping{display:flex;align-items:center;justify-content:flex-end;gap:6px;margin-top:10px}.dsh-skill-grouping>span{margin-right:4px;font-size:12px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-grouping button{padding:5px 10px}.dsh-skill-grouping button[aria-pressed=true]{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 13%,transparent)}
|
|
25
|
+
.dsh-skill-sources{display:flex;flex-direction:column;gap:14px;margin-top:14px}.dsh-skill-source{border:1px solid var(--dsw-alias-border-l4);border-radius:15px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}.dsh-skill-source>header{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:16px 18px;border-bottom:1px solid var(--dsw-alias-border-l4)}
|
|
26
|
+
.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title h3{margin-bottom:5px;overflow-wrap:anywhere}.dsh-skill-source-title small{display:block;font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}
|
|
27
|
+
.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px;padding:10px 18px;border-bottom:1px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1)}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}
|
|
28
|
+
.dsh-skill-user-group{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}.dsh-skill-user-group>header{padding:15px 18px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-skill-user-group>div{display:flex;flex-direction:column;gap:10px;padding:10px}.dsh-skill-source-nested{border-radius:11px;background:var(--dsw-alias-bg-layer-1)}
|
|
29
|
+
.dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
30
|
+
.dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}
|
|
31
|
+
.dsh-skill-row details{margin-top:9px}.dsh-skill-row summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-skill-detail{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dsh-skill-detail>p{grid-column:1/-1;margin:0}.dsh-skill-detail p strong,.dsh-skill-detail>div>strong{display:block;margin-bottom:5px;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
32
|
+
.dsh-skill-invocation{display:flex;flex-direction:column;gap:7px}.dsh-skill-invocation label,.dsh-skill-create-flags label{display:flex;flex-direction:row;align-items:center;gap:5px}.dsh-ext input[type=checkbox]{width:auto;min-width:auto}.dsh-skill-detail ul,.dsh-skill-detail li{display:block!important;margin:0;padding:0!important;border:0!important}.dsh-skill-resources{display:flex!important;flex-wrap:wrap!important;gap:5px}.dsh-skill-resources li{padding:3px 6px!important;border-radius:5px!important;background:var(--dsw-alias-bg-layer-3)!important}
|
|
33
|
+
.dsh-ext-span{grid-column:1/-1}.dsh-skill-create-flags{display:flex;flex-direction:column;gap:7px;font-size:13px}.dsh-skill-create-flags>span{margin-bottom:1px}.dsh-skill-source>.dsh-ext-notice{margin:12px 18px}.dsh-skill-source>.dsh-ext-empty{padding:18px}
|
|
34
|
+
@media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-grouping{justify-content:flex-start}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Skill and MCP configuration tools for a profile-owned managed patch. */
|
|
2
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
|
+
import { ExtensionStore } from './store.js'
|
|
7
|
+
import { createWebHandler } from './web.js'
|
|
8
|
+
|
|
9
|
+
export const name = 'extension-manager'
|
|
10
|
+
export const inject = ['tools', 'skills']
|
|
11
|
+
export const Config = Schema.object({ patchPath: Schema.string().default('') })
|
|
12
|
+
|
|
13
|
+
const string = { type: 'string', required: true }
|
|
14
|
+
const output = {
|
|
15
|
+
schema: { type: 'object', additionalProperties: true, properties: {} },
|
|
16
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Register management operations. MCP processes are started only by the profile loader. */
|
|
20
|
+
export async function apply(ctx, config) {
|
|
21
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
22
|
+
const store = new ExtensionStore(config.patchPath || join(home, 'profiles', 'web', 'cordis.patch.yml'))
|
|
23
|
+
await store.read()
|
|
24
|
+
ctx.inject(['connection'], web => {
|
|
25
|
+
web.connection.rpc.handle('/extensions', createWebHandler(store, ctx))
|
|
26
|
+
})
|
|
27
|
+
const register = (name, description, parameters, execute) => ctx.tools.register(defineTool({ name, description, parameters, output, execute }))
|
|
28
|
+
|
|
29
|
+
register('extensions_list', 'List managed Skill sources and MCP configurations; configured enabled state is not connection health.', {}, async () => ({
|
|
30
|
+
extensions: (await store.read()).map(row => ({
|
|
31
|
+
id: row.id, kind: row.config.serverName ? 'mcp' : 'skill', enabled: !row.disabled,
|
|
32
|
+
...(row.config.transport ? { transport: row.config.transport } : { directory: row.config.customSkillDirs[0] }),
|
|
33
|
+
})),
|
|
34
|
+
}))
|
|
35
|
+
register('extensions_add_skill', 'Add an existing root containing skill directories. Added sources are disabled until explicitly enabled.', {
|
|
36
|
+
id: string, directory: string,
|
|
37
|
+
}, (args, exec) => store.addSkill(args.id, args.directory, exec.signal))
|
|
38
|
+
register('extensions_add_mcp', 'Add a disabled MCP server. configuration is JSON with transport and command/args/cwd or url. Do not supply secrets in arguments.', {
|
|
39
|
+
id: string, configuration: string,
|
|
40
|
+
}, (args, exec) => {
|
|
41
|
+
const input = JSON.parse(args.configuration)
|
|
42
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('configuration must be a JSON object.')
|
|
43
|
+
return store.addMcp(args.id, input, exec.signal)
|
|
44
|
+
})
|
|
45
|
+
register('extensions_set_enabled', 'Enable or disable a managed extension. Enabling MCP authorizes its configured executable or endpoint to run when the profile reloads. Live profiles reload automatically; startup profiles require restart.', {
|
|
46
|
+
id: string, enabled: { type: 'boolean', required: true },
|
|
47
|
+
}, (args, exec) => store.setEnabled(args.id, args.enabled, exec.signal))
|
|
48
|
+
register('extensions_inspect', 'Inspect actual Skill catalog and visible MCP tool names for the caller. Zero tools does not establish connection failure. Does not start a separate connection probe.', {
|
|
49
|
+
cwd: string,
|
|
50
|
+
}, async (args, exec) => ({
|
|
51
|
+
skills: await ctx.skills.snapshot({ cwd: args.cwd, scope: exec.agent, signal: exec.signal }),
|
|
52
|
+
mcpTools: ctx.tools.schemas(exec.agent).map(tool => tool.name).filter(name => name.startsWith('mcp__')),
|
|
53
|
+
}))
|
|
54
|
+
}
|