@morehao/dsh-skills-console 0.4.0 → 0.5.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 +44 -20
- package/client.js +1299 -248
- package/lib/core/hash.js +13 -1
- package/lib/core/intro-store.js +156 -0
- package/lib/core/intro.js +430 -0
- package/lib/core/inventory.js +45 -17
- package/lib/core/invocation.js +578 -0
- package/lib/core/paths.js +2 -0
- package/lib/core/remote.js +103 -1
- package/lib/core/service.js +1103 -9
- package/lib/core/state.js +106 -6
- package/lib/index.js +16 -0
- package/lib/invocation-support.json +46 -0
- package/lib/routes.js +60 -2
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
package/lib/core/state.js
CHANGED
|
@@ -7,11 +7,14 @@ import path from 'node:path';
|
|
|
7
7
|
* - v2:`installs[].enabled` + `exports[]`(只给其他 agent 的显式导出)+ `shadows[]`(DSH 扫描根里的停用标记)。
|
|
8
8
|
* - v3:`installs[].enabled` 是**唯一**启停开关;`shadows[]` 退休,`legacyShadows[]` 只放
|
|
9
9
|
* 迁移时删不掉的旧桩与 orphan(v0.4.0 起启停由每个 live agent scope 的 policy provider 负责)。
|
|
10
|
+
* - v4:`installs[].mode`(auto / manual / hidden)成为调用策略;`enabled` 只保留给 legacy
|
|
11
|
+
* `dsh-off`(旧版 DSH-only 停用),manual / hidden 不再借助 provider。`manual` 记录
|
|
12
|
+
* frontmatter 的原始值与来源,`hidden` 记录 A′ park 的 marker / 内容哈希。
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* 迁移分三步:`upgradeStateV1` / `upgradeStateV2` / `upgradeStateV3` 只做**纯结构**改写
|
|
15
|
+
* (可测、无副作用),删链/删旧桩/剪悬空导出这类要做 IO 的事在 `migrations.js` 里按 manifest 执行。
|
|
13
16
|
*/
|
|
14
|
-
export const STATE_VERSION =
|
|
17
|
+
export const STATE_VERSION = 4;
|
|
15
18
|
|
|
16
19
|
export function emptyState() {
|
|
17
20
|
return {
|
|
@@ -150,6 +153,37 @@ export function upgradeStateV2(value, { now = new Date().toISOString() } = {}) {
|
|
|
150
153
|
return next;
|
|
151
154
|
}
|
|
152
155
|
|
|
156
|
+
const INVOCATION_MODES = new Set(['auto', 'manual', 'hidden']);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* v3 → v4 的结构升级。**不读写任何文件**:
|
|
160
|
+
* - 老记录没有 `mode` → `auto`(行为与 v3 完全一致);
|
|
161
|
+
* - `enabled === false` → 额外 `legacy: 'dsh-off'`,仍由 provider 隐藏,绝不静默改成 manual;
|
|
162
|
+
* - `manual` / `hidden` 只做形状占位(null),真实值由后续写操作填充。
|
|
163
|
+
*
|
|
164
|
+
* 不写 `migration`:v1→v2→v3 的 migration 是文件侧待办清单,v4 只是字段迁移,没有 IO。
|
|
165
|
+
*/
|
|
166
|
+
export function upgradeStateV3(value) {
|
|
167
|
+
const installs = (Array.isArray(value.installs) ? value.installs : []).map((item) => {
|
|
168
|
+
const next = isPlainObject(item) ? { ...item } : {};
|
|
169
|
+
if (!INVOCATION_MODES.has(next.mode)) next.mode = 'auto';
|
|
170
|
+
if (next.mode === 'auto') {
|
|
171
|
+
if (next.enabled === false) next.legacy = 'dsh-off';
|
|
172
|
+
else delete next.legacy;
|
|
173
|
+
} else {
|
|
174
|
+
// manual / hidden 都不再依赖 provider:enabled 必须是 true,否则会双重隐藏。
|
|
175
|
+
next.enabled = true;
|
|
176
|
+
delete next.legacy;
|
|
177
|
+
}
|
|
178
|
+
if (next.mode === 'auto') {
|
|
179
|
+
next.manual = null;
|
|
180
|
+
next.hidden = null;
|
|
181
|
+
}
|
|
182
|
+
return next;
|
|
183
|
+
});
|
|
184
|
+
return { ...value, version: 4, installs };
|
|
185
|
+
}
|
|
186
|
+
|
|
153
187
|
function validateV3(state) {
|
|
154
188
|
for (const key of ['subscriptions', 'installs', 'exports', 'legacyShadows']) {
|
|
155
189
|
if (!Array.isArray(state[key])) return { ok: false, reason: `state.${key} is not an array` };
|
|
@@ -202,6 +236,69 @@ function validateV3(state) {
|
|
|
202
236
|
return { ok: true, state };
|
|
203
237
|
}
|
|
204
238
|
|
|
239
|
+
/**
|
|
240
|
+
* v4 的形状校验。只修「缺省即可推断」的字段(mode 缺省 auto、hidden 的固定文件名),
|
|
241
|
+
* 不猜冲突语义:mode 写错、manual 缺 origin / original 一律只读,避免拿坏状态去写文件。
|
|
242
|
+
*/
|
|
243
|
+
function validateV4(state) {
|
|
244
|
+
for (const row of state.installs) {
|
|
245
|
+
if (row.mode === undefined) row.mode = 'auto';
|
|
246
|
+
if (!INVOCATION_MODES.has(row.mode)) {
|
|
247
|
+
return { ok: false, reason: `state.installs[] ${row.name} has invalid mode ${String(row.mode)}` };
|
|
248
|
+
}
|
|
249
|
+
if (row.legacy !== undefined && row.legacy !== 'dsh-off') {
|
|
250
|
+
return { ok: false, reason: `state.installs[] ${row.name} has invalid legacy ${String(row.legacy)}` };
|
|
251
|
+
}
|
|
252
|
+
if (row.mode === 'auto') {
|
|
253
|
+
row.manual = null;
|
|
254
|
+
row.hidden = null;
|
|
255
|
+
if (row.legacy === 'dsh-off') row.enabled = false;
|
|
256
|
+
else delete row.legacy;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
row.enabled = true;
|
|
260
|
+
delete row.legacy;
|
|
261
|
+
if (row.mode === 'manual') {
|
|
262
|
+
row.hidden = null;
|
|
263
|
+
if (!isPlainObject(row.manual)) {
|
|
264
|
+
return { ok: false, reason: `state.installs[] ${row.name} manual metadata is missing` };
|
|
265
|
+
}
|
|
266
|
+
if (row.manual.origin !== 'plugin' && row.manual.origin !== 'author') {
|
|
267
|
+
return { ok: false, reason: `state.installs[] ${row.name} manual.origin is invalid` };
|
|
268
|
+
}
|
|
269
|
+
if (!['absent', 'true', 'false'].includes(row.manual.original)) {
|
|
270
|
+
return { ok: false, reason: `state.installs[] ${row.name} manual.original is invalid` };
|
|
271
|
+
}
|
|
272
|
+
if (typeof row.manual.appliedAt !== 'string' || !row.manual.appliedAt) {
|
|
273
|
+
row.manual.appliedAt = new Date().toISOString();
|
|
274
|
+
}
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// hidden
|
|
278
|
+
if (!isPlainObject(row.hidden)) {
|
|
279
|
+
return { ok: false, reason: `state.installs[] ${row.name} hidden metadata is missing` };
|
|
280
|
+
}
|
|
281
|
+
if (row.hidden.previousMode !== 'auto' && row.hidden.previousMode !== 'manual') {
|
|
282
|
+
return { ok: false, reason: `state.installs[] ${row.name} hidden.previousMode is invalid` };
|
|
283
|
+
}
|
|
284
|
+
if (typeof row.hidden.disabledFile !== 'string' || !row.hidden.disabledFile) {
|
|
285
|
+
row.hidden.disabledFile = 'SKILL.md.disabled';
|
|
286
|
+
}
|
|
287
|
+
if (typeof row.hidden.marker !== 'string' || !row.hidden.marker) {
|
|
288
|
+
row.hidden.marker = '.dsh-skills-console.park.json';
|
|
289
|
+
}
|
|
290
|
+
if (row.hidden.canonicalHash !== undefined && row.hidden.canonicalHash !== null
|
|
291
|
+
&& typeof row.hidden.canonicalHash !== 'string') {
|
|
292
|
+
return { ok: false, reason: `state.installs[] ${row.name} hidden.canonicalHash is invalid` };
|
|
293
|
+
}
|
|
294
|
+
if (typeof row.hidden.at !== 'string' || !row.hidden.at) row.hidden.at = new Date().toISOString();
|
|
295
|
+
if (row.manual !== undefined && row.manual !== null && !isPlainObject(row.manual)) {
|
|
296
|
+
return { ok: false, reason: `state.installs[] ${row.name} manual metadata is invalid` };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { ok: true, state };
|
|
300
|
+
}
|
|
301
|
+
|
|
205
302
|
export function validateState(value) {
|
|
206
303
|
if (!isPlainObject(value)) return { ok: false, reason: 'state is not an object' };
|
|
207
304
|
if (typeof value.version !== 'number' || !Number.isInteger(value.version)) {
|
|
@@ -229,8 +326,9 @@ export function validateState(value) {
|
|
|
229
326
|
}
|
|
230
327
|
|
|
231
328
|
let upgraded;
|
|
232
|
-
if (value.version === 1) upgraded = upgradeStateV2(upgradeStateV1(value));
|
|
233
|
-
else if (value.version === 2) upgraded = upgradeStateV2(value);
|
|
329
|
+
if (value.version === 1) upgraded = upgradeStateV3(upgradeStateV2(upgradeStateV1(value)));
|
|
330
|
+
else if (value.version === 2) upgraded = upgradeStateV3(upgradeStateV2(value));
|
|
331
|
+
else if (value.version === 3) upgraded = upgradeStateV3(value);
|
|
234
332
|
else upgraded = value;
|
|
235
333
|
|
|
236
334
|
const state = { ...emptyState(), ...upgraded };
|
|
@@ -238,7 +336,9 @@ export function validateState(value) {
|
|
|
238
336
|
// v1 的顶层 links[] 在 v2 升级时已拆成 exports[] + migration.legacyLinks;
|
|
239
337
|
// 旧版本只搬家没删原字段,v3 里读到就剪掉,不再往外写。
|
|
240
338
|
delete state.links;
|
|
241
|
-
const
|
|
339
|
+
const v3 = validateV3(state);
|
|
340
|
+
if (!v3.ok) return v3;
|
|
341
|
+
const verdict = validateV4(v3.state);
|
|
242
342
|
if (!verdict.ok) return verdict;
|
|
243
343
|
state.version = STATE_VERSION;
|
|
244
344
|
return { ok: true, state: verdict.state };
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import { createPaths } from './core/paths.js';
|
|
|
4
4
|
import { createRegistry } from './core/agents.js';
|
|
5
5
|
import { createPolicyProvider, installPolicyProviders } from './core/policy.js';
|
|
6
6
|
import { createService } from './core/service.js';
|
|
7
|
+
import { normalizeIntroConfig } from './core/intro.js';
|
|
8
|
+
import { createIntroStore } from './core/intro-store.js';
|
|
7
9
|
import { StateStore } from './core/state.js';
|
|
8
10
|
import { createRoutes, ROUTE_PREFIX } from './routes.js';
|
|
9
11
|
|
|
@@ -86,6 +88,20 @@ export function apply(ctx, config = {}) {
|
|
|
86
88
|
workspacesProvider,
|
|
87
89
|
invalidatePolicies: () => policies.invalidate(),
|
|
88
90
|
policyStatus: () => policies.status(),
|
|
91
|
+
// 技能介绍:走宿主已挂载的模型服务(可选服务,缺失时接口 501,其他功能不受影响)
|
|
92
|
+
introStore: createIntroStore({ paths, logger }),
|
|
93
|
+
introOptions: normalizeIntroConfig(config.intro || {}),
|
|
94
|
+
getLlm: () => ctx.get('llm'),
|
|
95
|
+
getDefaultModel: () => {
|
|
96
|
+
const defaults = ctx.get('agentDefaultModel');
|
|
97
|
+
if (!defaults?.currentSelection) return null;
|
|
98
|
+
try {
|
|
99
|
+
return defaults.currentSelection();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger('intro.default-model.failed', { message: error?.message });
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
89
105
|
});
|
|
90
106
|
|
|
91
107
|
// 迁移必须尽早跑:GUI 一打开就该是「停用 = 真不可见」,而不是等用户点到某一行才发现。
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generatedAt": "2026-10-26",
|
|
3
|
+
"note": "只承诺已确认的客户端;`disable-model-invocation` 是客户端扩展,不是 Agent Skills 规范字段。",
|
|
4
|
+
"clients": [
|
|
5
|
+
{
|
|
6
|
+
"id": "dsh",
|
|
7
|
+
"label": "DSH",
|
|
8
|
+
"manual": "yes",
|
|
9
|
+
"hidden": "yes",
|
|
10
|
+
"verifiedAt": "2026-10-26",
|
|
11
|
+
"source": "宿主 SKILL.md frontmatter + 扫描根"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "claude",
|
|
15
|
+
"label": "Claude Code",
|
|
16
|
+
"manual": "yes",
|
|
17
|
+
"hidden": "yes",
|
|
18
|
+
"verifiedAt": "2026-10-26",
|
|
19
|
+
"source": "Claude Code skills frontmatter"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "cursor",
|
|
23
|
+
"label": "Cursor",
|
|
24
|
+
"manual": "yes",
|
|
25
|
+
"hidden": "yes",
|
|
26
|
+
"verifiedAt": "2026-10-26",
|
|
27
|
+
"source": "Cursor skills frontmatter"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "opencode",
|
|
31
|
+
"label": "OpenCode",
|
|
32
|
+
"manual": "version",
|
|
33
|
+
"hidden": "yes",
|
|
34
|
+
"verifiedAt": null,
|
|
35
|
+
"source": "版本相关,未逐版验证"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "other",
|
|
39
|
+
"label": "其他 / 未验证",
|
|
40
|
+
"manual": "unverified",
|
|
41
|
+
"hidden": "spec",
|
|
42
|
+
"verifiedAt": null,
|
|
43
|
+
"source": "目录里没有 SKILL.md 即不符合规范发现"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -73,12 +73,23 @@ function matchRoute(method, pathname) {
|
|
|
73
73
|
if (segments[0] === 'skills' && segments.length === 3 && segments[2] === 'files' && method === 'GET') {
|
|
74
74
|
return { name: 'skill.files', params: { name: decodeURIComponent(segments[1]) } };
|
|
75
75
|
}
|
|
76
|
+
// 技能介绍(模型生成):GET 只读缓存,POST 才真正调模型
|
|
77
|
+
if (segments[0] === 'skills' && segments.length === 3 && segments[2] === 'intro') {
|
|
78
|
+
if (method === 'GET') return { name: 'skill.intro.get', params: { name: decodeURIComponent(segments[1]) } };
|
|
79
|
+
if (method === 'POST') return { name: 'skill.intro.generate', params: { name: decodeURIComponent(segments[1]) } };
|
|
80
|
+
}
|
|
76
81
|
if (segments[0] === 'skills' && segments.length === 3 && segments[2] === 'adopt' && method === 'POST') {
|
|
77
82
|
return { name: 'skill.adopt', params: { name: decodeURIComponent(segments[1]) } };
|
|
78
83
|
}
|
|
79
84
|
if (segments[0] === 'skills' && segments.length === 3 && segments[2] === 'mode' && method === 'POST') {
|
|
80
85
|
return { name: 'skill.mode', params: { name: decodeURIComponent(segments[1]) } };
|
|
81
86
|
}
|
|
87
|
+
if (segments[0] === 'skills' && segments.length === 3 && segments[2] === 'invocation' && method === 'POST') {
|
|
88
|
+
return { name: 'skill.invocation', params: { name: decodeURIComponent(segments[1]) } };
|
|
89
|
+
}
|
|
90
|
+
if (segments[0] === 'skills' && segments.length === 3 && segments[2] === 'unpark' && method === 'POST') {
|
|
91
|
+
return { name: 'skill.unpark', params: { name: decodeURIComponent(segments[1]) } };
|
|
92
|
+
}
|
|
82
93
|
if (segments[0] === 'skills' && segments.length === 2 && method === 'DELETE') {
|
|
83
94
|
return { name: 'skill.remove', params: { name: decodeURIComponent(segments[1]) } };
|
|
84
95
|
}
|
|
@@ -91,6 +102,13 @@ function matchRoute(method, pathname) {
|
|
|
91
102
|
if (segments.length === 3 && segments[2] === 'refresh' && method === 'POST') {
|
|
92
103
|
return { name: 'subscriptions.refresh', params: { id: decodeURIComponent(segments[1]) } };
|
|
93
104
|
}
|
|
105
|
+
// 订阅候选的介绍:路径由服务端按快照反查,客户端只给订阅 id + 条目名
|
|
106
|
+
if (segments.length === 5 && segments[2] === 'entries' && segments[4] === 'intro') {
|
|
107
|
+
const id = decodeURIComponent(segments[1]);
|
|
108
|
+
const entryName = decodeURIComponent(segments[3]);
|
|
109
|
+
if (method === 'GET') return { name: 'subscription.intro.get', params: { id, name: entryName } };
|
|
110
|
+
if (method === 'POST') return { name: 'subscription.intro.generate', params: { id, name: entryName } };
|
|
111
|
+
}
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
if (segments[0] === 'updates') {
|
|
@@ -123,13 +141,24 @@ export function createRoutes({ service, logger }) {
|
|
|
123
141
|
return;
|
|
124
142
|
}
|
|
125
143
|
|
|
144
|
+
// 客户端断开(关抽屉 / 关页面)就取消模型调用:摘要是一次性辅助请求,没必要跑完再丢。
|
|
145
|
+
// 测试里的 fake res 没有 on/off,所以要按能力探测。
|
|
146
|
+
const abort = new AbortController();
|
|
147
|
+
const onClose = () => {
|
|
148
|
+
if (!res.writableEnded) abort.abort();
|
|
149
|
+
};
|
|
150
|
+
if (typeof res.on === 'function') res.on('close', onClose);
|
|
151
|
+
|
|
126
152
|
try {
|
|
127
153
|
service.reloadState();
|
|
128
154
|
const body = method === 'GET' ? {} : await readBody(req);
|
|
129
155
|
const query = Object.fromEntries(url.searchParams.entries());
|
|
130
|
-
const result = await dispatch(route, { body, query, service });
|
|
156
|
+
const result = await dispatch(route, { body, query, service, signal: abort.signal });
|
|
157
|
+
// 生成期间客户端可能已经走了:往已关闭的响应写会抛 ERR_STREAM_WRITE_AFTER_END
|
|
158
|
+
if (res.writableEnded || res.destroyed) return;
|
|
131
159
|
sendJson(res, 200, result);
|
|
132
160
|
} catch (error) {
|
|
161
|
+
if (res.writableEnded || res.destroyed) return;
|
|
133
162
|
if (error instanceof StateReadOnlyError) {
|
|
134
163
|
sendJson(res, 409, {
|
|
135
164
|
ok: false,
|
|
@@ -146,11 +175,13 @@ export function createRoutes({ service, logger }) {
|
|
|
146
175
|
}
|
|
147
176
|
logger('route.error', { message: error?.message, stack: String(error?.stack || '').slice(0, 800) });
|
|
148
177
|
sendJson(res, 500, { ok: false, code: 'error.skills.internal', message: error?.message || 'internal error' });
|
|
178
|
+
} finally {
|
|
179
|
+
if (typeof res.off === 'function') res.off('close', onClose);
|
|
149
180
|
}
|
|
150
181
|
};
|
|
151
182
|
}
|
|
152
183
|
|
|
153
|
-
async function dispatch(route, { body, query, service }) {
|
|
184
|
+
async function dispatch(route, { body, query, service, signal = null }) {
|
|
154
185
|
switch (route.name) {
|
|
155
186
|
case 'state': {
|
|
156
187
|
const state = await service.getState({ cwd: query.cwd || body.cwd, workspace: query.workspace });
|
|
@@ -174,6 +205,29 @@ async function dispatch(route, { body, query, service }) {
|
|
|
174
205
|
return service.listSkillBody({ name: route.params.name, path: query.path });
|
|
175
206
|
case 'skill.files':
|
|
176
207
|
return service.listSkillFiles({ name: route.params.name });
|
|
208
|
+
case 'skill.intro.get':
|
|
209
|
+
return service.getSkillIntro({ name: route.params.name, lang: query.lang });
|
|
210
|
+
case 'subscription.intro.get':
|
|
211
|
+
return service.getRemoteIntro({
|
|
212
|
+
subscriptionId: route.params.id,
|
|
213
|
+
name: route.params.name,
|
|
214
|
+
lang: query.lang,
|
|
215
|
+
});
|
|
216
|
+
case 'subscription.intro.generate':
|
|
217
|
+
return service.generateRemoteIntro({
|
|
218
|
+
subscriptionId: route.params.id,
|
|
219
|
+
name: route.params.name,
|
|
220
|
+
lang: body.lang ?? query.lang,
|
|
221
|
+
force: body.force === true,
|
|
222
|
+
signal,
|
|
223
|
+
});
|
|
224
|
+
case 'skill.intro.generate':
|
|
225
|
+
return service.generateSkillIntro({
|
|
226
|
+
name: route.params.name,
|
|
227
|
+
lang: body.lang ?? query.lang,
|
|
228
|
+
force: body.force === true,
|
|
229
|
+
signal,
|
|
230
|
+
});
|
|
177
231
|
case 'skill.adopt':
|
|
178
232
|
return service.adopt({ name: route.params.name });
|
|
179
233
|
case 'skill.mode':
|
|
@@ -183,6 +237,10 @@ async function dispatch(route, { body, query, service }) {
|
|
|
183
237
|
scope: body.scope,
|
|
184
238
|
projectRoot: body.projectRoot,
|
|
185
239
|
});
|
|
240
|
+
case 'skill.invocation':
|
|
241
|
+
return service.setInvocation({ name: route.params.name, mode: body.mode });
|
|
242
|
+
case 'skill.unpark':
|
|
243
|
+
return service.unpark({ name: route.params.name, keep: body.keep || null });
|
|
186
244
|
case 'skill.remove':
|
|
187
245
|
return service.remove({ name: route.params.name });
|
|
188
246
|
case 'subscriptions.add':
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Skills Console",
|
|
4
|
-
"description": "Install, update, remove
|
|
4
|
+
"description": "Install, update, remove and manage invocation policies for local agent skills. Skill bodies stay untouched; only Manual-only adds one marker line."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morehao/dsh-skills-console",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "统一管理本地技能的安装、更新、删除与接入,让同一份技能在多个 Agent 间复用。A skill manager for DeepSeek Harness: install, update, remove, link and toggle agent skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "morehao <morehao@qq.com>",
|