@lijian-ui/dsh-skill-manage 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/lib/index.js ADDED
@@ -0,0 +1,1215 @@
1
+ import Schema from "@deepseek-ai/schemastery";
2
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
+ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
4
+ import { z } from "zod";
5
+ import { access, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import { basename, dirname, join, resolve } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
9
+ import { parse } from "yaml";
10
+ //#region src/skill-files.ts
11
+ const DISABLED_SUFFIX = ".disabled";
12
+ const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
13
+ async function pathExists(path) {
14
+ try {
15
+ await access(path);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ async function findProjectRoot(cwd) {
22
+ let current = resolve(cwd);
23
+ while (true) {
24
+ if (await pathExists(join(current, ".git"))) return current;
25
+ const parent = dirname(current);
26
+ if (parent === current) return resolve(cwd);
27
+ current = parent;
28
+ }
29
+ }
30
+ function parseFrontmatter(raw) {
31
+ const text = raw.trimStart();
32
+ if (!text.startsWith("---")) return void 0;
33
+ const firstEnd = text.indexOf("\n");
34
+ if (firstEnd === -1) return void 0;
35
+ const closing = text.indexOf("\n---", firstEnd + 1);
36
+ const fmEnd = closing === -1 ? text.length : closing;
37
+ const fm = text.slice(3, fmEnd);
38
+ let body = "";
39
+ if (closing !== -1) {
40
+ const at = text.indexOf("\n", closing + 3);
41
+ if (at !== -1) body = text.slice(at + 1);
42
+ }
43
+ const pick = (key) => {
44
+ const m = new RegExp("^" + key + ":\\s*(.+)$", "m").exec(fm);
45
+ if (m === null) return void 0;
46
+ return m[1].trim().replace(/^["']|["']$/g, "");
47
+ };
48
+ const name = pick("name");
49
+ if (name === void 0 || !SKILL_NAME_RE.test(name)) return void 0;
50
+ return {
51
+ name,
52
+ description: pick("description") ?? "",
53
+ whenToUse: pick("whenToUse"),
54
+ body: body.trim()
55
+ };
56
+ }
57
+ function validateFrontmatter(raw) {
58
+ const text = raw.trimStart();
59
+ if (!text.startsWith("---")) return {
60
+ ok: false,
61
+ error: "缺少 YAML frontmatter(文件必须以 --- 开头)"
62
+ };
63
+ const firstEnd = text.indexOf("\n");
64
+ if (firstEnd === -1) return {
65
+ ok: false,
66
+ error: "frontmatter 未闭合"
67
+ };
68
+ const closing = text.indexOf("\n---", firstEnd + 1);
69
+ if (closing === -1) return {
70
+ ok: false,
71
+ error: "frontmatter 未闭合(缺少结尾的 ---)"
72
+ };
73
+ const fm = text.slice(firstEnd + 1, closing);
74
+ let data;
75
+ try {
76
+ data = parse(fm);
77
+ } catch (error) {
78
+ return {
79
+ ok: false,
80
+ error: "frontmatter 不是合法的 YAML:" + (error instanceof Error ? error.message : String(error))
81
+ };
82
+ }
83
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return {
84
+ ok: false,
85
+ error: "frontmatter 必须是键值对(YAML 映射)"
86
+ };
87
+ const obj = data;
88
+ for (const key of [
89
+ "disableModelInvocation",
90
+ "modelInvocable",
91
+ "userInvocable"
92
+ ]) if (key in obj) return {
93
+ ok: false,
94
+ error: "不支持旧字段 \"" + key + "\",请改用 disable-model-invocation / user-invocable"
95
+ };
96
+ const name = obj.name;
97
+ if (typeof name !== "string" || name.length === 0) return {
98
+ ok: false,
99
+ error: "frontmatter 缺少 name(必须是非空字符串)"
100
+ };
101
+ if (!SKILL_NAME_RE.test(name)) return {
102
+ ok: false,
103
+ error: "技能名 \"" + name + "\" 不符合命名规则(仅小写字母、数字与连字符,如 my-skill)"
104
+ };
105
+ const description = obj.description;
106
+ if (typeof description !== "string" || description.trim().length === 0) return {
107
+ ok: false,
108
+ error: "frontmatter 缺少 description(必须是非空字符串)"
109
+ };
110
+ const whenToUse = obj.whenToUse;
111
+ if (whenToUse !== void 0 && typeof whenToUse !== "string") return {
112
+ ok: false,
113
+ error: "whenToUse 必须是字符串"
114
+ };
115
+ for (const key of ["disable-model-invocation", "user-invocable"]) {
116
+ const value = obj[key];
117
+ if (value !== void 0) {
118
+ const lower = String(value).toLowerCase();
119
+ if (![
120
+ "true",
121
+ "false",
122
+ "yes",
123
+ "no",
124
+ "on",
125
+ "off",
126
+ "1",
127
+ "0"
128
+ ].includes(lower)) return {
129
+ ok: false,
130
+ error: key + " 必须是布尔值"
131
+ };
132
+ }
133
+ }
134
+ if (obj.metadata !== void 0 && (typeof obj.metadata !== "object" || obj.metadata === null || Array.isArray(obj.metadata))) return {
135
+ ok: false,
136
+ error: "metadata 必须是对象"
137
+ };
138
+ return {
139
+ ok: true,
140
+ skill: {
141
+ name,
142
+ description,
143
+ whenToUse: typeof whenToUse === "string" ? whenToUse : void 0,
144
+ body: ""
145
+ }
146
+ };
147
+ }
148
+ async function collectSkillEntries(roots) {
149
+ const entries = [];
150
+ for (const root of roots) {
151
+ let items;
152
+ try {
153
+ items = await readdir(root.path, { withFileTypes: true });
154
+ } catch {
155
+ continue;
156
+ }
157
+ for (const item of items) if (item.isDirectory() || item.isSymbolicLink() && (await stat(join(root.path, item.name)).catch(() => void 0))?.isDirectory() === true) {
158
+ const md = join(root.path, item.name, "SKILL.md");
159
+ const disabled = md + DISABLED_SUFFIX;
160
+ if (await pathExists(md)) {
161
+ const parsed = parseFrontmatter(await readFile(md, "utf8").catch(() => ""));
162
+ entries.push({
163
+ name: parsed?.name ?? item.name,
164
+ description: parsed?.description ?? "",
165
+ whenToUse: parsed?.whenToUse,
166
+ enabled: true,
167
+ kind: "bundle",
168
+ file: md,
169
+ dirBundle: true,
170
+ source: root.source,
171
+ projectRoot: root.projectRoot
172
+ });
173
+ } else if (await pathExists(disabled)) {
174
+ const parsed = parseFrontmatter(await readFile(disabled, "utf8").catch(() => ""));
175
+ entries.push({
176
+ name: parsed?.name ?? item.name,
177
+ description: parsed?.description ?? "",
178
+ whenToUse: parsed?.whenToUse,
179
+ enabled: false,
180
+ kind: "bundle",
181
+ file: disabled,
182
+ dirBundle: true,
183
+ source: root.source,
184
+ projectRoot: root.projectRoot
185
+ });
186
+ }
187
+ } else if (item.isFile()) {
188
+ if (item.name.endsWith(".md.disabled")) {
189
+ const file = join(root.path, item.name);
190
+ const parsed = parseFrontmatter(await readFile(file, "utf8").catch(() => ""));
191
+ entries.push({
192
+ name: parsed?.name ?? item.name.slice(0, -12),
193
+ description: parsed?.description ?? "",
194
+ whenToUse: parsed?.whenToUse,
195
+ enabled: false,
196
+ kind: "flat",
197
+ file,
198
+ dirBundle: false,
199
+ source: root.source,
200
+ projectRoot: root.projectRoot
201
+ });
202
+ } else if (item.name.endsWith(".md")) {
203
+ const file = join(root.path, item.name);
204
+ const parsed = parseFrontmatter(await readFile(file, "utf8"));
205
+ entries.push({
206
+ name: parsed?.name ?? item.name.slice(0, -3),
207
+ description: parsed?.description ?? "",
208
+ whenToUse: parsed?.whenToUse,
209
+ enabled: true,
210
+ kind: "flat",
211
+ file,
212
+ dirBundle: false,
213
+ source: root.source,
214
+ projectRoot: root.projectRoot
215
+ });
216
+ }
217
+ }
218
+ }
219
+ return entries;
220
+ }
221
+ function winnerEntry(entries, name) {
222
+ const matches = entries.filter((entry) => entry.name === name);
223
+ if (matches.length === 0) return void 0;
224
+ matches.sort((a, b) => sourceRank(a.source) - sourceRank(b.source));
225
+ return matches[0];
226
+ }
227
+ function sourceRank(source) {
228
+ switch (source) {
229
+ case "project-dsh": return 1;
230
+ case "project-agents": return 2;
231
+ case "user-dsh": return 3;
232
+ case "user-agents": return 4;
233
+ default: return 9;
234
+ }
235
+ }
236
+ //#endregion
237
+ //#region src/scope.ts
238
+ function workspaceSkillRoot(projectRoot) {
239
+ return join(projectRoot, ".dsh", "skills");
240
+ }
241
+ function scopeRootOf(target, dshHome) {
242
+ return target === null || target === void 0 ? join(dshHome, "skills") : workspaceSkillRoot(target);
243
+ }
244
+ async function normalizeWorkspaces(paths) {
245
+ const seen = /* @__PURE__ */ new Set();
246
+ const result = [];
247
+ for (const raw of paths) {
248
+ if (typeof raw !== "string" || raw.trim() === "") continue;
249
+ const absolute = resolve(raw.trim());
250
+ const info = await stat(absolute).catch(() => void 0);
251
+ if (info === void 0 || !info.isDirectory()) throw new Error("工作区不存在或不是目录:\"" + raw + "\"");
252
+ const project = await findProjectRoot(absolute);
253
+ const key = process.platform === "win32" ? project.toLowerCase() : project;
254
+ if (seen.has(key)) continue;
255
+ seen.add(key);
256
+ result.push(project);
257
+ }
258
+ return result;
259
+ }
260
+ async function normalizeWorkspace(raw) {
261
+ const list = await normalizeWorkspaces([raw]);
262
+ if (list.length === 0) throw new Error("至少需要指定一个存在的工作区");
263
+ return list[0];
264
+ }
265
+ function isBusyError(error) {
266
+ return error !== null && typeof error === "object" && [
267
+ "EPERM",
268
+ "EBUSY",
269
+ "EACCES",
270
+ "ENOTEMPTY"
271
+ ].includes(error.code ?? "");
272
+ }
273
+ async function removeRetry(path) {
274
+ for (let attempt = 0; attempt < 5; attempt++) try {
275
+ await rm(path, {
276
+ recursive: true,
277
+ force: true
278
+ });
279
+ return true;
280
+ } catch (error) {
281
+ if (!isBusyError(error)) throw error;
282
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 300 * (attempt + 1)));
283
+ }
284
+ return false;
285
+ }
286
+ async function migrateEntry(entry, targetRoot, mode) {
287
+ const sourceDir = entry.dirBundle ? dirname(entry.file) : entry.file;
288
+ const target = entry.dirBundle ? join(targetRoot, entry.name) : join(targetRoot, basename(entry.file));
289
+ if (resolve(sourceDir) === resolve(target)) throw new Error("技能 \"" + entry.name + "\" 已在此作用域中");
290
+ if (await pathExists(target)) throw new Error("目标位置已存在同名技能:\"" + target + "\"");
291
+ if (!await pathExists(sourceDir)) throw new Error("技能 \"" + entry.name + "\" 的源文件不存在:" + sourceDir);
292
+ await mkdir(targetRoot, { recursive: true });
293
+ if (mode === "move") try {
294
+ await rename(sourceDir, target);
295
+ return { target };
296
+ } catch (error) {
297
+ const code = error.code;
298
+ if (![
299
+ "EXDEV",
300
+ "EBUSY",
301
+ "EPERM",
302
+ "EACCES"
303
+ ].includes(code)) throw new Error("移动技能文件失败:" + (error instanceof Error ? error.message : String(error)));
304
+ }
305
+ const staging = join(targetRoot, ".dsh-skill-staging-" + process.pid + "-" + Math.random().toString(36).slice(2, 8));
306
+ try {
307
+ if (entry.dirBundle) {
308
+ await cp(sourceDir, staging, { recursive: true });
309
+ await rename(staging, target);
310
+ } else {
311
+ await mkdir(staging, { recursive: true });
312
+ const stagedFile = join(staging, basename(entry.file));
313
+ await cp(entry.file, stagedFile);
314
+ await rename(stagedFile, target);
315
+ await rm(staging, {
316
+ recursive: true,
317
+ force: true
318
+ }).catch(() => {});
319
+ }
320
+ } catch (error) {
321
+ await rm(staging, {
322
+ recursive: true,
323
+ force: true
324
+ }).catch(() => {});
325
+ throw new Error("复制技能文件失败(已回滚):" + (error instanceof Error ? error.message : String(error)));
326
+ }
327
+ if (mode === "move") try {
328
+ if (!await removeRetry(sourceDir)) throw new Error("源文件删除超时");
329
+ } catch (error) {
330
+ await rm(target, {
331
+ recursive: true,
332
+ force: true
333
+ }).catch(() => {});
334
+ throw new Error("技能 \"" + entry.name + "\" 已复制到目标,但无法删除源文件(可能被占用),已回滚新副本:" + (error instanceof Error ? error.message : String(error)));
335
+ }
336
+ return { target };
337
+ }
338
+ async function batchMigrateEntries(items, targetRoot, mode) {
339
+ const results = [];
340
+ for (const item of items) try {
341
+ await migrateEntry(item, targetRoot, mode);
342
+ results.push({
343
+ name: item.name,
344
+ ok: true
345
+ });
346
+ } catch (error) {
347
+ results.push({
348
+ name: item.name,
349
+ ok: false,
350
+ error: error instanceof Error ? error.message : String(error)
351
+ });
352
+ }
353
+ return results;
354
+ }
355
+ //#endregion
356
+ //#region src/remote.ts
357
+ const SERVICE = "skillManage";
358
+ const PACKAGE = "@lijian-ui/dsh-skill-manage";
359
+ const sessionIdSchema = z.string().optional();
360
+ const scopeSchema = z.object({
361
+ kind: z.enum(["global", "workspace"]),
362
+ path: z.string().optional(),
363
+ label: z.string().optional()
364
+ });
365
+ const skillSummarySchema = z.object({
366
+ name: z.string(),
367
+ description: z.string(),
368
+ whenToUse: z.string().optional(),
369
+ provider: z.string(),
370
+ source: z.string(),
371
+ enabled: z.boolean(),
372
+ modelInvocable: z.boolean(),
373
+ userInvocable: z.boolean(),
374
+ scope: scopeSchema.optional()
375
+ });
376
+ const listResultSchema = z.object({ skills: z.array(skillSummarySchema) });
377
+ const workspacesResultSchema = z.object({ workspaces: z.array(z.object({
378
+ path: z.string(),
379
+ label: z.string(),
380
+ sessions: z.number()
381
+ })) });
382
+ const resourceBaseSchema = z.object({
383
+ kind: z.string(),
384
+ path: z.string().optional(),
385
+ url: z.string().optional(),
386
+ description: z.string().optional()
387
+ }).optional();
388
+ const skillContentSchema = z.object({
389
+ name: z.string(),
390
+ description: z.string(),
391
+ content: z.string(),
392
+ provider: z.string(),
393
+ whenToUse: z.string().optional(),
394
+ path: z.string().optional(),
395
+ resourceBase: resourceBaseSchema
396
+ }).nullable();
397
+ const setEnabledResultSchema = z.object({
398
+ name: z.string(),
399
+ enabled: z.boolean()
400
+ });
401
+ const deleteSkillResultSchema = z.object({ name: z.string() });
402
+ const migratePayloadSchema = z.object({
403
+ target: z.string().nullable(),
404
+ mode: z.enum(["copy", "move"])
405
+ });
406
+ const migrateResultSchema = z.object({
407
+ name: z.string(),
408
+ scope: scopeSchema
409
+ });
410
+ const batchMigratePayloadSchema = z.object({
411
+ from: z.string().nullable(),
412
+ targets: z.array(z.string().nullable()).min(1),
413
+ mode: z.enum(["copy", "move"]),
414
+ names: z.array(z.string())
415
+ });
416
+ const batchMigrateResultSchema = z.object({ results: z.array(z.object({
417
+ name: z.string(),
418
+ target: z.string().nullable().optional(),
419
+ ok: z.boolean(),
420
+ error: z.string().optional()
421
+ })) });
422
+ const addFileSchema = z.object({
423
+ path: z.string(),
424
+ base64: z.string()
425
+ });
426
+ const addPayloadSchema = z.object({
427
+ kind: z.enum(["bundle", "flat"]),
428
+ files: z.array(addFileSchema).min(1),
429
+ workspace: z.string().nullable().optional()
430
+ });
431
+ const addResultSchema = z.object({
432
+ name: z.string(),
433
+ kind: z.enum(["bundle", "flat"]),
434
+ scope: scopeSchema
435
+ });
436
+ const MANIFEST = {
437
+ package: PACKAGE,
438
+ face: "host",
439
+ schemas: [],
440
+ invocations: [
441
+ {
442
+ id: `${PACKAGE}#${SERVICE}/list`,
443
+ service: SERVICE,
444
+ namespace: SERVICE,
445
+ method: "list",
446
+ invocation: { kind: "direct" },
447
+ parameters: [{
448
+ name: "sessionId",
449
+ wire: "sessionId",
450
+ source: "json",
451
+ acceptsUndefined: true,
452
+ codec: {
453
+ mode: "strict",
454
+ typeSymbol: `${PACKAGE}#sessionId`,
455
+ schema: sessionIdSchema
456
+ }
457
+ }],
458
+ result: {
459
+ mode: "strict",
460
+ typeSymbol: `${PACKAGE}#SkillListResult`,
461
+ schema: listResultSchema
462
+ }
463
+ },
464
+ {
465
+ id: `${PACKAGE}#${SERVICE}/workspaces`,
466
+ service: SERVICE,
467
+ namespace: SERVICE,
468
+ method: "workspaces",
469
+ invocation: { kind: "direct" },
470
+ parameters: [],
471
+ result: {
472
+ mode: "strict",
473
+ typeSymbol: `${PACKAGE}#WorkspacesResult`,
474
+ schema: workspacesResultSchema
475
+ }
476
+ },
477
+ {
478
+ id: `${PACKAGE}#${SERVICE}/content`,
479
+ service: SERVICE,
480
+ namespace: SERVICE,
481
+ method: "content",
482
+ invocation: { kind: "direct" },
483
+ parameters: [{
484
+ name: "name",
485
+ wire: "name",
486
+ source: "json",
487
+ codec: {
488
+ mode: "strict",
489
+ typeSymbol: `${PACKAGE}#SkillName`,
490
+ schema: z.string()
491
+ }
492
+ }, {
493
+ name: "sessionId",
494
+ wire: "sessionId",
495
+ source: "json",
496
+ acceptsUndefined: true,
497
+ codec: {
498
+ mode: "strict",
499
+ typeSymbol: `${PACKAGE}#sessionId`,
500
+ schema: sessionIdSchema
501
+ }
502
+ }],
503
+ result: {
504
+ mode: "strict",
505
+ typeSymbol: `${PACKAGE}#SkillContent`,
506
+ schema: skillContentSchema
507
+ }
508
+ },
509
+ {
510
+ id: `${PACKAGE}#${SERVICE}/setEnabled`,
511
+ service: SERVICE,
512
+ namespace: SERVICE,
513
+ method: "setEnabled",
514
+ invocation: { kind: "direct" },
515
+ parameters: [
516
+ {
517
+ name: "name",
518
+ wire: "name",
519
+ source: "json",
520
+ codec: {
521
+ mode: "strict",
522
+ typeSymbol: `${PACKAGE}#SkillName`,
523
+ schema: z.string()
524
+ }
525
+ },
526
+ {
527
+ name: "sessionId",
528
+ wire: "sessionId",
529
+ source: "json",
530
+ acceptsUndefined: true,
531
+ codec: {
532
+ mode: "strict",
533
+ typeSymbol: `${PACKAGE}#sessionId`,
534
+ schema: sessionIdSchema
535
+ }
536
+ },
537
+ {
538
+ name: "enabled",
539
+ wire: "enabled",
540
+ source: "json",
541
+ codec: {
542
+ mode: "strict",
543
+ typeSymbol: `${PACKAGE}#EnabledFlag`,
544
+ schema: z.boolean()
545
+ }
546
+ }
547
+ ],
548
+ result: {
549
+ mode: "strict",
550
+ typeSymbol: `${PACKAGE}#SetEnabledResult`,
551
+ schema: setEnabledResultSchema
552
+ }
553
+ },
554
+ {
555
+ id: `${PACKAGE}#${SERVICE}/migrate`,
556
+ service: SERVICE,
557
+ namespace: SERVICE,
558
+ method: "migrate",
559
+ invocation: { kind: "direct" },
560
+ parameters: [
561
+ {
562
+ name: "name",
563
+ wire: "name",
564
+ source: "json",
565
+ codec: {
566
+ mode: "strict",
567
+ typeSymbol: `${PACKAGE}#SkillName`,
568
+ schema: z.string()
569
+ }
570
+ },
571
+ {
572
+ name: "sessionId",
573
+ wire: "sessionId",
574
+ source: "json",
575
+ acceptsUndefined: true,
576
+ codec: {
577
+ mode: "strict",
578
+ typeSymbol: `${PACKAGE}#sessionId`,
579
+ schema: sessionIdSchema
580
+ }
581
+ },
582
+ {
583
+ name: "payload",
584
+ wire: "payload",
585
+ source: "json",
586
+ codec: {
587
+ mode: "strict",
588
+ typeSymbol: `${PACKAGE}#MigratePayload`,
589
+ schema: migratePayloadSchema
590
+ }
591
+ }
592
+ ],
593
+ result: {
594
+ mode: "strict",
595
+ typeSymbol: `${PACKAGE}#MigrateResult`,
596
+ schema: migrateResultSchema
597
+ }
598
+ },
599
+ {
600
+ id: `${PACKAGE}#${SERVICE}/batchMigrate`,
601
+ service: SERVICE,
602
+ namespace: SERVICE,
603
+ method: "batchMigrate",
604
+ invocation: { kind: "direct" },
605
+ parameters: [{
606
+ name: "sessionId",
607
+ wire: "sessionId",
608
+ source: "json",
609
+ acceptsUndefined: true,
610
+ codec: {
611
+ mode: "strict",
612
+ typeSymbol: `${PACKAGE}#sessionId`,
613
+ schema: sessionIdSchema
614
+ }
615
+ }, {
616
+ name: "payload",
617
+ wire: "payload",
618
+ source: "json",
619
+ codec: {
620
+ mode: "strict",
621
+ typeSymbol: `${PACKAGE}#BatchMigratePayload`,
622
+ schema: batchMigratePayloadSchema
623
+ }
624
+ }],
625
+ result: {
626
+ mode: "strict",
627
+ typeSymbol: `${PACKAGE}#BatchMigrateResult`,
628
+ schema: batchMigrateResultSchema
629
+ }
630
+ },
631
+ {
632
+ id: `${PACKAGE}#${SERVICE}/deleteSkill`,
633
+ service: SERVICE,
634
+ namespace: SERVICE,
635
+ method: "deleteSkill",
636
+ invocation: { kind: "direct" },
637
+ parameters: [{
638
+ name: "name",
639
+ wire: "name",
640
+ source: "json",
641
+ codec: {
642
+ mode: "strict",
643
+ typeSymbol: `${PACKAGE}#SkillName`,
644
+ schema: z.string()
645
+ }
646
+ }, {
647
+ name: "sessionId",
648
+ wire: "sessionId",
649
+ source: "json",
650
+ acceptsUndefined: true,
651
+ codec: {
652
+ mode: "strict",
653
+ typeSymbol: `${PACKAGE}#sessionId`,
654
+ schema: sessionIdSchema
655
+ }
656
+ }],
657
+ result: {
658
+ mode: "strict",
659
+ typeSymbol: `${PACKAGE}#DeleteSkillResult`,
660
+ schema: deleteSkillResultSchema
661
+ }
662
+ },
663
+ {
664
+ id: `${PACKAGE}#${SERVICE}/addSkill`,
665
+ service: SERVICE,
666
+ namespace: SERVICE,
667
+ method: "addSkill",
668
+ invocation: { kind: "direct" },
669
+ parameters: [{
670
+ name: "sessionId",
671
+ wire: "sessionId",
672
+ source: "json",
673
+ acceptsUndefined: true,
674
+ codec: {
675
+ mode: "strict",
676
+ typeSymbol: `${PACKAGE}#sessionId`,
677
+ schema: sessionIdSchema
678
+ }
679
+ }, {
680
+ name: "payload",
681
+ wire: "payload",
682
+ source: "json",
683
+ codec: {
684
+ mode: "strict",
685
+ typeSymbol: `${PACKAGE}#AddPayload`,
686
+ schema: addPayloadSchema
687
+ }
688
+ }],
689
+ result: {
690
+ mode: "strict",
691
+ typeSymbol: `${PACKAGE}#AddResult`,
692
+ schema: addResultSchema
693
+ }
694
+ }
695
+ ],
696
+ model: {
697
+ services: [],
698
+ events: [],
699
+ objects: []
700
+ }
701
+ };
702
+ const MAX_ADD_FILES = 200;
703
+ const MAX_ADD_TOTAL_BYTES = 8388608;
704
+ var SkillManageApi = class extends TypertRemoteService {
705
+ constructor(ctx) {
706
+ super(ctx, SERVICE);
707
+ }
708
+ registryFor(sessionId) {
709
+ const live = sessionId === void 0 ? void 0 : this.ctx.agents.get(sessionId);
710
+ if (live !== void 0) {
711
+ const result = this.ctx.get("agentPresets")?.serviceFor?.(live, "skills");
712
+ if (result !== void 0) return result;
713
+ }
714
+ return this.ctx.skills;
715
+ }
716
+ viewFor(sessionId) {
717
+ const registry = this.registryFor(sessionId);
718
+ const session = sessionId === void 0 ? void 0 : this.ctx.sessions.get(sessionId);
719
+ const scope = sessionId === void 0 ? void 0 : this.ctx.agents.get(sessionId);
720
+ return {
721
+ registry,
722
+ cwd: session?.header?.cwd,
723
+ scope
724
+ };
725
+ }
726
+ homes() {
727
+ return {
728
+ dshHome: resolveDshHome(),
729
+ agentsHome: resolve(process.env.DSH_AGENTS_HOME?.trim() ? process.env.DSH_AGENTS_HOME : join(homedir(), ".agents"))
730
+ };
731
+ }
732
+ isWithin(baseDir, candidate) {
733
+ if (typeof candidate !== "string" || candidate === "") return false;
734
+ const base = resolve(baseDir);
735
+ const value = resolve(candidate);
736
+ if (value === base) return true;
737
+ const b = process.platform === "win32" ? base.toLowerCase() : base;
738
+ const v = process.platform === "win32" ? value.toLowerCase() : value;
739
+ const sep = process.platform === "win32" ? "\\" : "/";
740
+ return v.startsWith(b.endsWith(sep) ? b : b + sep);
741
+ }
742
+ async allRoots() {
743
+ const { dshHome, agentsHome } = this.homes();
744
+ const roots = [];
745
+ const seen = /* @__PURE__ */ new Set();
746
+ const push = (path, source, projectRoot) => {
747
+ const normalized = resolve(path);
748
+ const key = process.platform === "win32" ? normalized.toLowerCase() : normalized;
749
+ if (seen.has(key)) return;
750
+ seen.add(key);
751
+ roots.push({
752
+ path,
753
+ source,
754
+ projectRoot
755
+ });
756
+ };
757
+ push(join(dshHome, "skills"), "user-dsh", void 0);
758
+ push(join(agentsHome, "skills"), "user-agents", void 0);
759
+ for (const workspace of (await this.workspaces()).workspaces) {
760
+ push(join(workspace.path, ".dsh", "skills"), "project-dsh", workspace.path);
761
+ push(join(workspace.path, ".agents", "skills"), "project-agents", workspace.path);
762
+ }
763
+ return roots;
764
+ }
765
+ async fileEntriesAll() {
766
+ return collectSkillEntries(await this.allRoots());
767
+ }
768
+ workspaceOfPath(path, roots) {
769
+ if (typeof path !== "string" || path === "") return void 0;
770
+ for (const root of roots) {
771
+ if (root.projectRoot === void 0) continue;
772
+ if (this.isWithin(root.path, path)) return root.projectRoot;
773
+ }
774
+ }
775
+ scopeForEntry(entry) {
776
+ if (entry.projectRoot !== void 0) return {
777
+ kind: "workspace",
778
+ path: entry.projectRoot,
779
+ label: basename(entry.projectRoot) || entry.projectRoot
780
+ };
781
+ return { kind: "global" };
782
+ }
783
+ scopeForTarget(targetRoot, targetProject) {
784
+ const { dshHome } = this.homes();
785
+ if (resolve(targetRoot) === resolve(join(dshHome, "skills"))) return { kind: "global" };
786
+ return {
787
+ kind: "workspace",
788
+ path: targetProject ?? void 0,
789
+ label: targetProject !== void 0 && targetProject !== null ? basename(targetProject) || targetProject : void 0
790
+ };
791
+ }
792
+ async list(sessionId) {
793
+ const { registry, cwd, scope } = this.viewFor(sessionId);
794
+ const roots = await this.allRoots();
795
+ const listed = await registry.list({
796
+ cwd,
797
+ scope
798
+ });
799
+ const skills = [];
800
+ const seen = /* @__PURE__ */ new Set();
801
+ const seenKey = (name, scopePath) => name + "\0" + (scopePath ?? "global");
802
+ for (const skill of listed) {
803
+ if (this.workspaceOfPath(skill.path, roots) !== void 0) continue;
804
+ const source = skill.source ?? (skill.provider === "runtime" ? "runtime" : "");
805
+ skills.push({
806
+ name: skill.name,
807
+ description: skill.description,
808
+ ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
809
+ provider: skill.provider,
810
+ source,
811
+ enabled: true,
812
+ modelInvocable: skill.invocation.modelInvocable,
813
+ userInvocable: skill.invocation.userInvocable,
814
+ scope: { kind: "global" }
815
+ });
816
+ seen.add(seenKey(skill.name, "global"));
817
+ }
818
+ for (const entry of await collectSkillEntries(roots)) {
819
+ const scopePath = entry.projectRoot ?? "global";
820
+ if (seen.has(seenKey(entry.name, scopePath))) continue;
821
+ seen.add(seenKey(entry.name, scopePath));
822
+ skills.push({
823
+ name: entry.name,
824
+ description: entry.description,
825
+ ...entry.whenToUse === void 0 ? {} : { whenToUse: entry.whenToUse },
826
+ provider: "filesystem",
827
+ source: entry.source,
828
+ enabled: entry.enabled,
829
+ modelInvocable: false,
830
+ userInvocable: false,
831
+ scope: this.scopeForEntry(entry)
832
+ });
833
+ }
834
+ return { skills };
835
+ }
836
+ async workspaces() {
837
+ const map = /* @__PURE__ */ new Map();
838
+ const keyOf = (path) => process.platform === "win32" ? path.toLowerCase() : path;
839
+ const add = async (path, label, sessions) => {
840
+ if (typeof path !== "string" || path === "") return;
841
+ let project;
842
+ try {
843
+ project = await findProjectRoot(resolve(path));
844
+ } catch {
845
+ return;
846
+ }
847
+ const key = keyOf(project);
848
+ if (map.has(key)) return;
849
+ map.set(key, {
850
+ path: project,
851
+ label: label || basename(project) || project,
852
+ sessions: sessions ?? 0
853
+ });
854
+ };
855
+ try {
856
+ const registry = this.ctx.get("workspaceRegistry");
857
+ if (registry !== void 0 && typeof registry.list === "function") for (const workspace of registry.list()) {
858
+ try {
859
+ if (await workspace.status() !== "ok") continue;
860
+ } catch {}
861
+ await add(workspace.path, workspace.title, Array.isArray(workspace.sessionIds) ? workspace.sessionIds.length : 0);
862
+ }
863
+ } catch {}
864
+ try {
865
+ for (const session of this.ctx.sessions.list()) {
866
+ const cwd = session.header?.cwd;
867
+ if (cwd === void 0 || cwd === "") continue;
868
+ await add(resolve(cwd), void 0, 1);
869
+ }
870
+ } catch {}
871
+ return { workspaces: [...map.values()].sort((a, b) => a.label.localeCompare(b.label) || a.path.localeCompare(b.path)) };
872
+ }
873
+ async locate(name, sessionId) {
874
+ const { registry, cwd, scope } = this.viewFor(sessionId);
875
+ const skill = await registry.get(name, {
876
+ cwd,
877
+ scope
878
+ });
879
+ if (skill !== void 0 && this.workspaceOfPath(skill.path, await this.allRoots()) === void 0) return {
880
+ kind: "live",
881
+ skill
882
+ };
883
+ const entry = winnerEntry(await this.fileEntriesAll(), name);
884
+ if (entry !== void 0) return {
885
+ kind: "file",
886
+ entry
887
+ };
888
+ return { kind: "missing" };
889
+ }
890
+ async content(name, sessionId) {
891
+ const located = await this.locate(name, sessionId);
892
+ if (located.kind === "missing") return null;
893
+ if (located.kind === "file") {
894
+ const raw = await readFile(located.entry.file, "utf8");
895
+ return {
896
+ name: located.entry.name,
897
+ description: located.entry.description,
898
+ content: raw,
899
+ provider: "filesystem",
900
+ path: located.entry.file
901
+ };
902
+ }
903
+ const skill = located.skill;
904
+ return {
905
+ name: skill.name,
906
+ description: skill.description,
907
+ content: skill.content,
908
+ provider: skill.provider,
909
+ ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
910
+ ...skill.path === void 0 ? {} : { path: skill.path },
911
+ ...skill.resourceBase === void 0 ? {} : { resourceBase: skill.resourceBase }
912
+ };
913
+ }
914
+ assertEditable(skill) {
915
+ if (skill.source === "bundled") throw new Error("技能 \"" + skill.name + "\" 随部署附带,不可修改");
916
+ if (typeof skill.path !== "string" || skill.path.length === 0) throw new Error("技能 \"" + skill.name + "\" 没有可修改的文件");
917
+ }
918
+ async setEnabled(name, sessionId, enabled) {
919
+ const located = await this.locate(name, sessionId);
920
+ if (located.kind === "missing") throw new Error("技能 \"" + name + "\" 不存在");
921
+ if (located.kind === "live") {
922
+ const skill = located.skill;
923
+ this.assertEditable(skill);
924
+ if (enabled) return {
925
+ name,
926
+ enabled: true
927
+ };
928
+ const target = skill.path + DISABLED_SUFFIX;
929
+ if (await pathExists(target)) throw new Error("目标文件已存在:" + target);
930
+ await rename(skill.path, target);
931
+ return {
932
+ name,
933
+ enabled: false
934
+ };
935
+ }
936
+ const entry = located.entry;
937
+ if (enabled === entry.enabled) return {
938
+ name,
939
+ enabled
940
+ };
941
+ const target = enabled ? entry.file.slice(0, -9) : entry.file + DISABLED_SUFFIX;
942
+ if (await pathExists(target)) throw new Error("目标文件已存在:" + target);
943
+ await rename(entry.file, target);
944
+ return {
945
+ name,
946
+ enabled
947
+ };
948
+ }
949
+ async deleteSkill(name, sessionId) {
950
+ const located = await this.locate(name, sessionId);
951
+ if (located.kind === "missing") throw new Error("技能 \"" + name + "\" 不存在");
952
+ if (located.kind === "live") {
953
+ const skill = located.skill;
954
+ this.assertEditable(skill);
955
+ if (basename(skill.path) === "SKILL.md") await rm(dirname(skill.path), {
956
+ recursive: true,
957
+ force: true
958
+ });
959
+ else await rm(skill.path, { force: true });
960
+ return { name };
961
+ }
962
+ const entry = located.entry;
963
+ if (entry.dirBundle) await rm(dirname(entry.file), {
964
+ recursive: true,
965
+ force: true
966
+ });
967
+ else await rm(entry.file, { force: true });
968
+ return { name };
969
+ }
970
+ async migratableEntry(name, sessionId) {
971
+ const entry = winnerEntry(await this.fileEntriesAll(), name);
972
+ if (entry !== void 0) return entry;
973
+ const located = await this.locate(name, sessionId);
974
+ if (located.kind !== "live") return void 0;
975
+ const skill = located.skill;
976
+ if (typeof skill.path !== "string" || skill.path === "") return void 0;
977
+ const { dshHome, agentsHome } = this.homes();
978
+ if (![join(dshHome, "skills"), join(agentsHome, "skills")].some((root) => this.isWithin(root, skill.path))) return void 0;
979
+ this.assertEditable(skill);
980
+ return {
981
+ name: skill.name,
982
+ file: skill.path,
983
+ dirBundle: basename(skill.path) === "SKILL.md",
984
+ enabled: true,
985
+ source: skill.source ?? "user-dsh"
986
+ };
987
+ }
988
+ async migrate(name, sessionId, payload) {
989
+ const { target: rawTarget, mode } = payload;
990
+ const { dshHome } = this.homes();
991
+ const targetProject = rawTarget === null || rawTarget === void 0 ? null : await normalizeWorkspace(rawTarget);
992
+ const targetRoot = scopeRootOf(targetProject, dshHome);
993
+ const entry = await this.migratableEntry(name, sessionId);
994
+ if (entry === void 0) throw new Error("技能 \"" + name + "\" 没有可迁移的文件(随部署附带或运行时内置的技能不可迁移)");
995
+ await migrateEntry(entry, targetRoot, mode);
996
+ return {
997
+ name,
998
+ scope: this.scopeForTarget(targetRoot, targetProject)
999
+ };
1000
+ }
1001
+ async batchMigrate(sessionId, payload) {
1002
+ const { from: rawFrom, targets: rawTargets, mode, names } = payload;
1003
+ if (mode === "move" && rawTargets.length > 1) throw new Error("移动模式只能选择一个目标作用域(多个目标请改用复制)");
1004
+ const { dshHome, agentsHome } = this.homes();
1005
+ const fromProject = rawFrom === null || rawFrom === void 0 ? null : await normalizeWorkspace(rawFrom);
1006
+ const fromRoots = fromProject === null ? [{
1007
+ path: join(dshHome, "skills"),
1008
+ source: "user-dsh"
1009
+ }, {
1010
+ path: join(agentsHome, "skills"),
1011
+ source: "user-agents"
1012
+ }] : [{
1013
+ path: workspaceSkillRoot(fromProject),
1014
+ source: "project-dsh",
1015
+ projectRoot: fromProject
1016
+ }];
1017
+ const byName = /* @__PURE__ */ new Map();
1018
+ for (const entry of await collectSkillEntries(fromRoots)) if (!byName.has(entry.name)) byName.set(entry.name, entry);
1019
+ const chosen = [];
1020
+ const results = [];
1021
+ for (const name of names) {
1022
+ const entry = byName.get(name);
1023
+ if (entry === void 0) results.push({
1024
+ name,
1025
+ ok: false,
1026
+ error: "技能 \"" + name + "\" 不在源作用域中"
1027
+ });
1028
+ else chosen.push(entry);
1029
+ }
1030
+ for (const rawTarget of rawTargets) {
1031
+ const targetRoot = scopeRootOf(rawTarget === null || rawTarget === void 0 ? null : await normalizeWorkspace(rawTarget), dshHome);
1032
+ if (fromRoots.some((root) => resolve(root.path) === resolve(targetRoot))) {
1033
+ for (const entry of chosen) results.push({
1034
+ name: entry.name,
1035
+ target: rawTarget ?? null,
1036
+ ok: false,
1037
+ error: "目标作用域与源作用域相同"
1038
+ });
1039
+ continue;
1040
+ }
1041
+ for (const item of await batchMigrateEntries(chosen, targetRoot, mode)) results.push({
1042
+ name: item.name,
1043
+ target: rawTarget ?? null,
1044
+ ok: item.ok,
1045
+ ...item.error === void 0 ? {} : { error: item.error }
1046
+ });
1047
+ }
1048
+ return { results };
1049
+ }
1050
+ async addSkill(sessionId, payload) {
1051
+ const { kind, files, workspace: rawWorkspace } = payload;
1052
+ if (files.length > MAX_ADD_FILES) throw new Error("文件数量过多(最多 200 个)");
1053
+ const decoded = files.map((file) => {
1054
+ const data = Buffer.from(file.base64, "base64");
1055
+ if (data.length === 0 && file.base64.length > 0) throw new Error("文件内容解码失败:" + file.path);
1056
+ return {
1057
+ path: file.path.replaceAll("\\", "/"),
1058
+ data
1059
+ };
1060
+ });
1061
+ if (decoded.reduce((sum, file) => sum + file.data.length, 0) > MAX_ADD_TOTAL_BYTES) throw new Error("技能总大小超过 8MB 上限");
1062
+ for (const file of decoded) if (file.path.startsWith("/") || file.path.split("/").some((segment) => segment === ".." || segment === ".")) throw new Error("非法文件路径:" + file.path);
1063
+ const { dshHome } = this.homes();
1064
+ let targetProject;
1065
+ let targetRoot;
1066
+ if (rawWorkspace === void 0 || rawWorkspace === null || rawWorkspace === "") targetRoot = join(dshHome, "skills");
1067
+ else {
1068
+ targetProject = await normalizeWorkspace(rawWorkspace);
1069
+ targetRoot = workspaceSkillRoot(targetProject);
1070
+ }
1071
+ let name;
1072
+ let writes;
1073
+ if (kind === "bundle") {
1074
+ const tops = new Set(decoded.map((file) => file.path.split("/")[0]));
1075
+ if (tops.size !== 1 || decoded.some((file) => file.path.split("/").length < 2)) throw new Error("技能文件夹结构不正确:所有文件应位于同一个文件夹内");
1076
+ const top = [...tops][0];
1077
+ const skillFile = decoded.find((file) => file.path === top + "/SKILL.md");
1078
+ if (skillFile === void 0) throw new Error("技能文件夹缺少顶层的 SKILL.md 文件");
1079
+ const validation = validateFrontmatter(skillFile.data.toString("utf8"));
1080
+ if (!validation.ok) throw new Error("技能格式不符合要求:" + validation.error);
1081
+ name = validation.skill.name;
1082
+ writes = decoded.map((file) => ({
1083
+ relative: file.path.slice(top.length + 1),
1084
+ data: file.data
1085
+ }));
1086
+ } else {
1087
+ if (decoded.length !== 1) throw new Error("单个技能文件一次只能添加一个");
1088
+ const file = decoded[0];
1089
+ const flatName = file.path.split("/").filter(Boolean).pop() ?? "";
1090
+ if (!flatName.toLowerCase().endsWith(".md")) throw new Error("技能文件必须是 .md 文件");
1091
+ const validation = validateFrontmatter(file.data.toString("utf8"));
1092
+ if (!validation.ok) throw new Error("技能格式不符合要求:" + validation.error);
1093
+ name = validation.skill.name;
1094
+ writes = [{
1095
+ relative: flatName,
1096
+ data: file.data
1097
+ }];
1098
+ }
1099
+ const existing = winnerEntry(await this.fileEntriesAll(), name);
1100
+ if (existing !== void 0) throw new Error("同名技能 \"" + name + "\" 已存在(" + (existing.enabled ? "已启用" : "已停用") + ",位于 " + (existing.projectRoot !== void 0 ? existing.projectRoot : "全局用户根") + ")");
1101
+ const { registry, cwd, scope } = this.viewFor(sessionId);
1102
+ if ((await registry.list({
1103
+ cwd,
1104
+ scope
1105
+ })).some((skill) => skill.name === name)) throw new Error("同名技能 \"" + name + "\" 已存在");
1106
+ const target = kind === "bundle" ? join(targetRoot, name) : join(targetRoot, writes[0].relative);
1107
+ const staging = join(targetRoot, ".dsh-skill-staging-" + process.pid + "-" + Math.random().toString(36).slice(2, 8));
1108
+ try {
1109
+ if (kind === "bundle") {
1110
+ for (const write of writes) {
1111
+ const filePath = join(staging, write.relative);
1112
+ await mkdir(dirname(filePath), { recursive: true });
1113
+ await writeFile(filePath, write.data);
1114
+ }
1115
+ await rename(staging, target);
1116
+ } else {
1117
+ await mkdir(staging, { recursive: true });
1118
+ const stagedFile = join(staging, writes[0].relative);
1119
+ await writeFile(stagedFile, writes[0].data);
1120
+ await rename(stagedFile, target);
1121
+ await rm(staging, {
1122
+ recursive: true,
1123
+ force: true
1124
+ }).catch(() => {});
1125
+ }
1126
+ } catch (error) {
1127
+ await rm(staging, {
1128
+ recursive: true,
1129
+ force: true
1130
+ }).catch(() => {});
1131
+ await rm(target, {
1132
+ recursive: true,
1133
+ force: true
1134
+ }).catch(() => {});
1135
+ throw new Error("写入技能文件失败(已回滚):" + (error instanceof Error ? error.message : String(error)));
1136
+ }
1137
+ if (!await this.waitForDiscovery(name, sessionId, targetProject ?? cwd)) {
1138
+ await rm(target, {
1139
+ recursive: true,
1140
+ force: true
1141
+ }).catch(() => {});
1142
+ throw new Error("DSH 未接受该技能(格式校验未通过),已回滚。请检查 frontmatter 后重试");
1143
+ }
1144
+ return {
1145
+ name,
1146
+ kind,
1147
+ scope: targetProject !== void 0 && targetProject !== null ? {
1148
+ kind: "workspace",
1149
+ path: targetProject,
1150
+ label: basename(targetProject) || targetProject
1151
+ } : { kind: "global" }
1152
+ };
1153
+ }
1154
+ async waitForDiscovery(name, sessionId, probeCwd) {
1155
+ const { registry, scope } = this.viewFor(sessionId);
1156
+ for (let attempt = 0; attempt < 12; attempt++) {
1157
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
1158
+ try {
1159
+ if (await registry.get(name, {
1160
+ cwd: probeCwd,
1161
+ scope
1162
+ }) !== void 0) return true;
1163
+ } catch {
1164
+ return true;
1165
+ }
1166
+ }
1167
+ return false;
1168
+ }
1169
+ };
1170
+ function registerSkillManageRemote(ctx) {
1171
+ new SkillManageApi(ctx);
1172
+ ctx.effect(() => ctx.typert.register(MANIFEST), "skill-manage: typert manifest");
1173
+ }
1174
+ //#endregion
1175
+ //#region src/index.ts
1176
+ const inject = [
1177
+ "typert",
1178
+ "settings",
1179
+ "skills",
1180
+ "sessions",
1181
+ "agents"
1182
+ ];
1183
+ const NS = settingsNamespace("skill-manage");
1184
+ const Config = Schema.object({});
1185
+ function apply(ctx) {
1186
+ installConsoleLoggerExporter(ctx);
1187
+ installSettingsSection(ctx, NS, Config, {}, {
1188
+ setSource: () => {},
1189
+ onChange: () => {}
1190
+ });
1191
+ registerSkillManageRemote(ctx);
1192
+ }
1193
+ function installConsoleLoggerExporter(ctx) {
1194
+ try {
1195
+ ctx.root.logger?.exporter?.({
1196
+ colors: 0,
1197
+ export: (message) => {
1198
+ const { name = "skill-manage", type = "log", args = [] } = message;
1199
+ const line = `[${name}] ${args.map((arg) => arg instanceof Error ? arg.stack ?? arg.message : typeof arg === "string" ? arg : (() => {
1200
+ try {
1201
+ return JSON.stringify(arg);
1202
+ } catch {
1203
+ return String(arg);
1204
+ }
1205
+ })()).join(" ")}`;
1206
+ if (type === "error") console.error(line);
1207
+ else if (type === "warn") console.warn(line);
1208
+ else if (type === "debug") console.debug(line);
1209
+ else console.info(line);
1210
+ }
1211
+ });
1212
+ } catch {}
1213
+ }
1214
+ //#endregion
1215
+ export { Config, apply, inject };