coding-tool-x 3.8.6 → 3.8.8

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.
Files changed (134) hide show
  1. package/dist/web/assets/{AgentsDrawer-Db9wAyaZ.js → AgentsDrawer-DRQuwi0m.js} +1 -1
  2. package/dist/web/assets/Analytics-Cf5KwEdj.js +1 -0
  3. package/dist/web/assets/AssetPathField-DyniM0pF.js +1 -0
  4. package/dist/web/assets/{ChatHistoryDrawer-CwzJHyNP.js → ChatHistoryDrawer-CrfS0KYV.js} +1 -1
  5. package/dist/web/assets/{CollapseItem-CTCfSiue.js → CollapseItem-ClMsJuUu.js} +1 -1
  6. package/dist/web/assets/{CommandsDrawer-vDghB1U9.js → CommandsDrawer-CzHelf56.js} +1 -1
  7. package/dist/web/assets/ConfigExportDrawer-BEZ8wTby.js +1 -0
  8. package/dist/web/assets/ConfigTemplates-Crg3xWe5.js +1 -0
  9. package/dist/web/assets/{ConfigTemplatesDrawer-CbsJp76X.js → ConfigTemplatesDrawer-BqM32Im7.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplatesPanel-liMSTh7s.js → ConfigTemplatesPanel-lTdutC8H.js} +1 -1
  11. package/dist/web/assets/{Divider-D28g1gwj.js → Divider-DsdbkHrG.js} +1 -1
  12. package/dist/web/assets/{FavoritesDrawer-D6QSiYaL.js → FavoritesDrawer-DDyWUBJR.js} +1 -1
  13. package/dist/web/assets/Forward-DLCL7aMY.js +1 -0
  14. package/dist/web/assets/{GatewayConvertDrawer-RvET6iNG.js → GatewayConvertDrawer-Rigtt_5J.js} +1 -1
  15. package/dist/web/assets/Home-tCSKmSHV.js +1 -0
  16. package/dist/web/assets/{MarkdownEditor-CYmLGYju.js → MarkdownEditor-BMMUufSc.js} +1 -1
  17. package/dist/web/assets/McpDrawer-CmE-d7VW.js +1 -0
  18. package/dist/web/assets/{McpFormDrawer-BcTlgSpN.js → McpFormDrawer-ByvS18od.js} +1 -1
  19. package/dist/web/assets/{PluginManager-Dh2o0xkZ.js → PluginManager-C4kJlpVX.js} +1 -1
  20. package/dist/web/assets/{PluginsDrawer-deKQ5u3I.js → PluginsDrawer-05R4k15x.js} +1 -1
  21. package/dist/web/assets/PluginsPanel-BCoEs7Og.js +1 -0
  22. package/dist/web/assets/PluginsPanel-CzGMK-Ap.css +1 -0
  23. package/dist/web/assets/ProjectList-BJt1OIaI.js +1 -0
  24. package/dist/web/assets/ProjectList-DG3yu8xZ.css +1 -0
  25. package/dist/web/assets/PromptsDrawer-DnwBf_oW.js +1 -0
  26. package/dist/web/assets/RecentSessionsDrawer-B7d-38wH.js +1 -0
  27. package/dist/web/assets/RecentSessionsDrawer-yrAD4p1A.css +1 -0
  28. package/dist/web/assets/SessionCard-BUgjHQwX.js +1 -0
  29. package/dist/web/assets/SessionList-B-bXZRFk.js +1 -0
  30. package/dist/web/assets/SessionList-qG_XpPBJ.css +1 -0
  31. package/dist/web/assets/{SettingsDrawer-u_vwUuPS.js → SettingsDrawer-BITzjQLl.js} +1 -1
  32. package/dist/web/assets/SkillManager-D9jkNuRM.js +1 -0
  33. package/dist/web/assets/{SkillsDrawer-CkF3BpyC.js → SkillsDrawer-0Z4IefxC.js} +1 -1
  34. package/dist/web/assets/SkillsPanel-BKIVOj7n.js +1 -0
  35. package/dist/web/assets/{SkillsPanel-B3Nm5j32.css → SkillsPanel-C4ke56nF.css} +1 -1
  36. package/dist/web/assets/{SpeedTestDrawer-BgfG1Ked.js → SpeedTestDrawer-Bx1bYf5i.js} +1 -1
  37. package/dist/web/assets/Upload-CzC4rGI4.js +1 -0
  38. package/dist/web/assets/{WorkspaceDrawer-Br5CeCzL.js → WorkspaceDrawer-eujtWpoq.js} +1 -1
  39. package/dist/web/assets/{WorkspaceManager-B2O8-qIw.js → WorkspaceManager-CQ-8Ej68.js} +1 -1
  40. package/dist/web/assets/{WorkspacePanel-5oDKit3c.js → WorkspacePanel-Cql5qE2R.js} +1 -1
  41. package/dist/web/assets/headers-BqDmkMjy.js +1 -0
  42. package/dist/web/assets/{icons-B2Vg-IOC.js → icons-DmFe4b5j.js} +1 -1
  43. package/dist/web/assets/index-Cmmwj3Fx.js +2 -0
  44. package/dist/web/assets/{index-Bi1MQHYX.css → index-Cnbe696C.css} +1 -1
  45. package/dist/web/assets/{light-Cp0t7v32.js → light-0eavxNvt.js} +1 -1
  46. package/dist/web/assets/platforms-BmzzwnSw.js +1 -0
  47. package/dist/web/assets/platforms-DWXgmy2w.js +1 -0
  48. package/dist/web/assets/session-visibility-BAmxh3cK.js +1 -0
  49. package/dist/web/assets/sessions-D4yQtfKb.js +1 -0
  50. package/dist/web/assets/sessions-DqlbocLJ.js +1 -0
  51. package/dist/web/assets/use-message-DqCwefG6.js +1 -0
  52. package/dist/web/assets/{useResponsiveDrawer-jbmqZuJs.js → useResponsiveDrawer-DZd5rNk5.js} +1 -1
  53. package/dist/web/index.html +7 -7
  54. package/package.json +1 -1
  55. package/src/commands/search.js +17 -2
  56. package/src/config/paths.js +2 -0
  57. package/src/platforms/driver-registry.js +9 -1
  58. package/src/platforms/drivers/claude/session-history-adapter.js +72 -1
  59. package/src/platforms/drivers/claude/sessions-implementation.js +49 -7
  60. package/src/platforms/drivers/claude/sessions.js +30 -6
  61. package/src/platforms/drivers/codex/parser.js +57 -1
  62. package/src/platforms/drivers/codex/session-history-adapter.js +8 -2
  63. package/src/platforms/drivers/codex/sessions-implementation.js +25 -3
  64. package/src/platforms/drivers/codex/sessions.js +18 -1
  65. package/src/platforms/drivers/dsh/api-operations.js +396 -0
  66. package/src/platforms/drivers/dsh/channels-implementation.js +458 -0
  67. package/src/platforms/drivers/dsh/channels.js +191 -0
  68. package/src/platforms/drivers/dsh/common.js +571 -0
  69. package/src/platforms/drivers/dsh/native-config.js +93 -0
  70. package/src/platforms/drivers/dsh/native-logs.js +116 -0
  71. package/src/platforms/drivers/dsh/plugin-manager.js +117 -0
  72. package/src/platforms/drivers/dsh/projects.js +23 -0
  73. package/src/platforms/drivers/dsh/proxy-implementation.js +260 -0
  74. package/src/platforms/drivers/dsh/proxy.js +23 -0
  75. package/src/platforms/drivers/dsh/resources.js +346 -0
  76. package/src/platforms/drivers/dsh/sessions.js +587 -0
  77. package/src/platforms/drivers/dsh/statistics-implementation.js +238 -0
  78. package/src/platforms/drivers/dsh/statistics.js +24 -0
  79. package/src/platforms/drivers/gemini/session-history-adapter.js +49 -1
  80. package/src/platforms/drivers/gemini/sessions-implementation.js +46 -1
  81. package/src/platforms/drivers/gemini/sessions.js +2 -1
  82. package/src/platforms/drivers/omp/session-history-adapter.js +42 -1
  83. package/src/platforms/drivers/omp/sessions-implementation.js +51 -3
  84. package/src/platforms/drivers/omp/sessions.js +33 -2
  85. package/src/platforms/drivers/opencode/sessions-implementation.js +89 -9
  86. package/src/platforms/drivers/opencode/sessions.js +37 -1
  87. package/src/platforms/manifest-schema.js +43 -3
  88. package/src/platforms/manifests/dsh.json +542 -0
  89. package/src/platforms/path-resolver.js +2 -0
  90. package/src/platforms/platform-path-context.js +12 -2
  91. package/src/platforms/registry.js +16 -1
  92. package/src/platforms/runtime.js +5 -0
  93. package/src/platforms/session-history-adapters.js +1 -0
  94. package/src/server/api/platform-api-config.js +23 -2
  95. package/src/server/api/platform-route-factory.js +9 -2
  96. package/src/server/api/platforms.js +19 -1
  97. package/src/server/api/plugins.js +108 -13
  98. package/src/server/services/config-registry-service.js +10 -1
  99. package/src/server/services/config-templates-service.js +1 -0
  100. package/src/server/services/plugins-service.js +474 -103
  101. package/src/server/services/session-history-index.js +900 -140
  102. package/src/server/services/session-history-worker.js +3 -1
  103. package/src/server/services/skill-service.js +214 -7
  104. package/src/server/services/usage-log-utils.js +1 -0
  105. package/src/server/services/workspace-service.js +39 -10
  106. package/src/shared/channel-driver.js +80 -5
  107. package/src/shared/driver-factories/api.js +22 -6
  108. package/src/shared/driver-factories/projects.js +8 -0
  109. package/dist/web/assets/Analytics-C4gMzv7g.js +0 -1
  110. package/dist/web/assets/AssetPathField-Bd_nMFu3.js +0 -1
  111. package/dist/web/assets/ConfigExportDrawer-C-XDvlHc.js +0 -1
  112. package/dist/web/assets/ConfigTemplates-CYY8XRED.js +0 -1
  113. package/dist/web/assets/Home-P8eseZcf.js +0 -1
  114. package/dist/web/assets/McpDrawer-Ch3bkKIq.js +0 -1
  115. package/dist/web/assets/PluginsPanel-BofsgQgj.js +0 -1
  116. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +0 -1
  117. package/dist/web/assets/ProjectList-677RLZ8O.css +0 -1
  118. package/dist/web/assets/ProjectList-DhSWJHTA.js +0 -1
  119. package/dist/web/assets/PromptsDrawer-Cfb2FeeU.js +0 -1
  120. package/dist/web/assets/RecentSessionsDrawer-3eXamJhr.js +0 -1
  121. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +0 -1
  122. package/dist/web/assets/SessionCard-CW0eHURI.js +0 -1
  123. package/dist/web/assets/SessionList-BpYRz1cX.css +0 -1
  124. package/dist/web/assets/SessionList-DQfyDGWU.js +0 -1
  125. package/dist/web/assets/SkillManager-DHMprJLA.js +0 -1
  126. package/dist/web/assets/SkillsPanel-DWE4pFGx.js +0 -1
  127. package/dist/web/assets/Upload-CNwaGOws.js +0 -1
  128. package/dist/web/assets/headers-cf-QapUq.js +0 -1
  129. package/dist/web/assets/index-BMAUPq5K.js +0 -2
  130. package/dist/web/assets/platforms-CBkB45JP.js +0 -1
  131. package/dist/web/assets/platforms-FTpjxAT6.js +0 -1
  132. package/dist/web/assets/sessions-Cmy31slD.js +0 -1
  133. package/dist/web/assets/sessions-yC_oVVh8.js +0 -1
  134. package/dist/web/assets/use-message-DoLM8C1C.js +0 -1
@@ -0,0 +1,346 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const yaml = require('js-yaml');
7
+
8
+ const {
9
+ isObject,
10
+ redactSecrets,
11
+ readProfilePatchContributions,
12
+ resolvePaths,
13
+ writeAtomic
14
+ } = require('./common');
15
+
16
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
17
+ const SKILL_FILESYSTEM_PLUGIN = '@deepseek-ai/dsh-skill-filesystem';
18
+
19
+ const ROOT_RANKS = Object.freeze({
20
+ 'project-dsh': 100,
21
+ 'project-agents': 200,
22
+ custom: 300,
23
+ 'user-dsh': 400,
24
+ 'user-agents': 500,
25
+ bundled: 600
26
+ });
27
+
28
+ function resolveSkillConfig(context = {}, profileName) {
29
+ const config = {};
30
+ if (!profileName) return config;
31
+ const info = readProfilePatchContributions(context, profileName);
32
+ if (!info) return config;
33
+ for (const { rows } of info.contributions) {
34
+ for (const row of rows) {
35
+ if (row.name !== SKILL_FILESYSTEM_PLUGIN || !isObject(row.config)) continue;
36
+ Object.assign(config, row.config);
37
+ }
38
+ }
39
+ return config;
40
+ }
41
+
42
+ function findProjectRoot(cwd) {
43
+ let current = path.resolve(cwd || process.cwd());
44
+ try {
45
+ if (!fs.statSync(current).isDirectory()) current = path.dirname(current);
46
+ } catch (_) {}
47
+ while (true) {
48
+ if (fs.existsSync(path.join(current, '.git'))) return current;
49
+ const parent = path.dirname(current);
50
+ if (parent === current) return path.resolve(cwd || process.cwd());
51
+ current = parent;
52
+ }
53
+ }
54
+
55
+ function pathIsReadableDirectory(value) {
56
+ try { return fs.statSync(value).isDirectory(); } catch (_) { return false; }
57
+ }
58
+
59
+ function addRoot(roots, rootPath, source) {
60
+ if (typeof rootPath !== 'string' || !rootPath.trim()) return;
61
+ const resolved = path.resolve(rootPath);
62
+ if (roots.some(root => root.path === resolved)) return;
63
+ roots.push({ path: resolved, source, rank: ROOT_RANKS[source] || ROOT_RANKS.custom });
64
+ }
65
+
66
+ function resolveSkillRoots(context = {}, options = {}) {
67
+ const paths = resolvePaths(context);
68
+ const profileName = options.profile || null;
69
+ const skillConfig = resolveSkillConfig(context, profileName);
70
+ const includeDefaultRoots = skillConfig.includeDefaultRoots !== false;
71
+ const roots = [];
72
+ const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
73
+
74
+ if (includeDefaultRoots) {
75
+ const projectRoot = findProjectRoot(cwd);
76
+ addRoot(roots, path.join(projectRoot, '.dsh', 'skills'), 'project-dsh');
77
+ addRoot(roots, path.join(projectRoot, '.agents', 'skills'), 'project-agents');
78
+ }
79
+
80
+ const customSkillDirs = Array.isArray(skillConfig.customSkillDirs)
81
+ ? skillConfig.customSkillDirs
82
+ : [];
83
+ for (const root of customSkillDirs) addRoot(roots, root, 'custom');
84
+
85
+ if (includeDefaultRoots) {
86
+ addRoot(roots, path.join(skillConfig.dshHome || paths.dir, 'skills'), 'user-dsh');
87
+ addRoot(roots, path.join(skillConfig.agentsHome || process.env.DSH_AGENTS_HOME || path.join(os.homedir(), '.agents'), 'skills'), 'user-agents');
88
+ }
89
+
90
+ const bundledSkillDir = skillConfig.bundledSkillDir || process.env.DSH_BUNDLED_SKILL_DIR;
91
+ if (bundledSkillDir) addRoot(roots, bundledSkillDir, 'bundled');
92
+
93
+ return { cwd, profile: profileName, roots, config: skillConfig };
94
+ }
95
+
96
+ function readSkillFrontmatter(filePath) {
97
+ const raw = fs.readFileSync(filePath, 'utf8');
98
+ const firstLine = raw.startsWith('\uFEFF') ? raw.slice(1) : raw;
99
+ if (!firstLine.startsWith('---\n') && !firstLine.startsWith('---\r\n')) return null;
100
+ const match = /^(?:---\r?\n)([\s\S]*?)(?:\r?\n---\r?\n|\r?\n---$)/.exec(firstLine);
101
+ if (!match) return null;
102
+ const parsed = yaml.load(match[1], { schema: yaml.SAFE_SCHEMA });
103
+ if (!isObject(parsed)) return null;
104
+ const name = typeof parsed.name === 'string' ? parsed.name.trim() : '';
105
+ const description = typeof parsed.description === 'string' ? parsed.description.trim() : '';
106
+ if (!SKILL_NAME.test(name) || !description) return null;
107
+ const disableModelInvocation = parseFrontmatterBoolean(parsed['disable-model-invocation'], false);
108
+ const userInvocable = parseFrontmatterBoolean(parsed['user-invocable'], true);
109
+ const bodyStart = match[0].length;
110
+ const content = firstLine.slice(bodyStart).trim();
111
+ return {
112
+ name,
113
+ description,
114
+ ...(typeof parsed.whenToUse === 'string' && parsed.whenToUse.trim()
115
+ ? { whenToUse: parsed.whenToUse.trim() }
116
+ : {}),
117
+ invocation: { modelInvocable: !disableModelInvocation, userInvocable },
118
+ ...(isObject(parsed.metadata) ? { metadata: redactSecrets(parsed.metadata) } : {}),
119
+ content
120
+ };
121
+ }
122
+
123
+ function parseFrontmatterBoolean(value, fallback) {
124
+ if (value === undefined) return fallback;
125
+ if (value === true || value === 1) return true;
126
+ if (value === false || value === 0) return false;
127
+ if (typeof value === 'string') {
128
+ if (['true', 'yes', 'on', '1'].includes(value.toLowerCase())) return true;
129
+ if (['false', 'no', 'off', '0'].includes(value.toLowerCase())) return false;
130
+ }
131
+ return fallback;
132
+ }
133
+
134
+ function directoryEntryType(fullPath, entry) {
135
+ if (entry.isDirectory()) return 'directory';
136
+ if (entry.isFile()) return 'file';
137
+ if (!entry.isSymbolicLink()) return null;
138
+ try {
139
+ const stat = fs.statSync(fullPath);
140
+ if (stat.isDirectory()) return 'directory';
141
+ if (stat.isFile()) return 'file';
142
+ } catch (_) {}
143
+ return null;
144
+ }
145
+
146
+ function discoverRoot(root) {
147
+ if (!pathIsReadableDirectory(root.path)) return [];
148
+ let entries;
149
+ try {
150
+ entries = fs.readdirSync(root.path, { withFileTypes: true });
151
+ } catch (_) {
152
+ return [];
153
+ }
154
+ const skills = [];
155
+ for (const entry of entries) {
156
+ if (root.source === 'user-dsh' && entry.name === '.system') continue;
157
+ const fullPath = path.join(root.path, entry.name);
158
+ const type = directoryEntryType(fullPath, entry);
159
+ const filePath = type === 'directory'
160
+ ? path.join(fullPath, 'SKILL.md')
161
+ : type === 'file' && entry.name.endsWith('.md') ? fullPath : null;
162
+ if (!filePath || !fs.existsSync(filePath)) continue;
163
+ let parsed;
164
+ try { parsed = readSkillFrontmatter(filePath); } catch (_) { parsed = null; }
165
+ if (!parsed) continue;
166
+ skills.push({
167
+ ...parsed,
168
+ path: filePath,
169
+ source: root.source,
170
+ rank: root.rank,
171
+ resourceBase: path.dirname(filePath)
172
+ });
173
+ }
174
+ return skills;
175
+ }
176
+
177
+ function publicSkill(skill, includeContent) {
178
+ const result = {
179
+ name: skill.name,
180
+ description: skill.description,
181
+ ...(skill.whenToUse ? { whenToUse: skill.whenToUse } : {}),
182
+ invocation: skill.invocation,
183
+ source: skill.source,
184
+ path: skill.path,
185
+ resourceBase: skill.resourceBase,
186
+ ...(skill.metadata ? { metadata: skill.metadata } : {})
187
+ };
188
+ if (includeContent) result.content = skill.content;
189
+ return result;
190
+ }
191
+
192
+ function listSkills(context = {}, options = {}) {
193
+ const resolved = resolveSkillRoots(context, options);
194
+ const roots = options.scope === 'project'
195
+ ? resolved.roots.filter(root => root.source.startsWith('project-'))
196
+ : options.scope === 'user'
197
+ ? resolved.roots.filter(root => !root.source.startsWith('project-'))
198
+ : resolved.roots;
199
+ const candidates = roots.flatMap(discoverRoot)
200
+ .sort((left, right) => left.rank - right.rank || left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
201
+ const winners = new Map();
202
+ for (const candidate of candidates) {
203
+ if (!winners.has(candidate.name)) winners.set(candidate.name, candidate);
204
+ }
205
+ const includeContent = options.includeContent === true;
206
+ return {
207
+ profile: resolved.profile,
208
+ cwd: resolved.cwd,
209
+ roots: roots.map(root => ({
210
+ path: root.path,
211
+ source: root.source,
212
+ exists: pathIsReadableDirectory(root.path)
213
+ })),
214
+ skills: [...winners.values()]
215
+ .sort((left, right) => left.name.localeCompare(right.name))
216
+ .map(skill => publicSkill(skill, includeContent)),
217
+ management: 'ctx',
218
+ note: 'skill 文件由 DSH skill-filesystem 发现和加载;ctx 管理 user/project skill 文件,不接管 DSH 运行时注册。'
219
+ };
220
+ }
221
+
222
+ function getSkill(context = {}, profileName, skillName, options = {}) {
223
+ const result = listSkills(context, { ...options, profile: profileName, includeContent: true });
224
+ const skill = result.skills.find(entry => entry.name === skillName);
225
+ return skill ? { ...result, skill } : null;
226
+ }
227
+
228
+ function resolveManagedSkillRoot(context = {}, options = {}) {
229
+ const scope = options.scope || 'user';
230
+ const resolved = resolveSkillRoots(context, { profile: options.profile, cwd: options.cwd });
231
+ const source = scope === 'project' ? 'project-dsh' : scope === 'user' ? 'user-dsh' : null;
232
+ if (!source) throw new Error('DSH skill scope must be user or project');
233
+ const root = resolved.roots.find(entry => entry.source === source);
234
+ if (!root) throw new Error(`DSH ${scope} skill root is unavailable`);
235
+ return { ...root, cwd: resolved.cwd, scope };
236
+ }
237
+
238
+ function isInside(root, candidate) {
239
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
240
+ return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
241
+ }
242
+
243
+ function assertManagedSkillPath(root, filePath) {
244
+ if (!isInside(root.path, filePath)) throw new Error('DSH skill path escapes its managed root');
245
+ const parent = path.dirname(filePath);
246
+ if (fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink()) throw new Error('DSH skill symlinks cannot be managed');
247
+ if (fs.existsSync(parent) && fs.lstatSync(parent).isSymbolicLink()) throw new Error('DSH skill symlink directories cannot be managed');
248
+ }
249
+
250
+ function normalizeSkillName(value) {
251
+ const name = String(value || '').trim();
252
+ if (!SKILL_NAME.test(name)) throw new Error('DSH skill name must be lowercase kebab-case');
253
+ return name;
254
+ }
255
+
256
+ function skillDocument(body, name) {
257
+ const description = String(body.description || '').trim();
258
+ if (!description) throw new Error('DSH skill description is required');
259
+ const content = String(body.content || '').trim();
260
+ const frontmatter = {
261
+ name,
262
+ description,
263
+ ...(body.whenToUse ? { whenToUse: String(body.whenToUse).trim() } : {}),
264
+ ...(body.userInvocable === false ? { 'user-invocable': false } : {}),
265
+ ...(body.modelInvocable === false ? { 'disable-model-invocation': true } : {}),
266
+ ...(isObject(body.metadata) ? { metadata: body.metadata } : {})
267
+ };
268
+ return `---\n${yaml.dump(frontmatter, { noRefs: true, lineWidth: 120 })}---\n\n${content}\n`;
269
+ }
270
+
271
+ function findManagedSkill(context = {}, name, options = {}) {
272
+ const result = listSkills(context, { profile: options.profile, cwd: options.cwd });
273
+ const skill = result.skills.find(entry => entry.name === name);
274
+ if (!skill) return null;
275
+ const roots = resolveSkillRoots(context, { profile: options.profile, cwd: options.cwd }).roots;
276
+ const root = roots.find(entry => isInside(entry.path, skill.path));
277
+ if (!root || !['project-dsh', 'user-dsh'].includes(root.source)) return null;
278
+ assertManagedSkillPath(root, skill.path);
279
+ return { skill, root };
280
+ }
281
+
282
+ function createSkill(context = {}, request = {}) {
283
+ const body = request.body || {};
284
+ const name = normalizeSkillName(body.name);
285
+ const root = resolveManagedSkillRoot(context, body);
286
+ const directory = path.join(root.path, name);
287
+ const filePath = path.join(directory, 'SKILL.md');
288
+ assertManagedSkillPath(root, filePath);
289
+ if (fs.existsSync(filePath) && body.overwrite !== true) {
290
+ const error = new Error(`DSH skill already exists: ${name}`);
291
+ error.statusCode = 409;
292
+ throw error;
293
+ }
294
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
295
+ writeAtomic(filePath, skillDocument(body, name), 0o600);
296
+ return getSkill(context, body.profile, name, { cwd: body.cwd });
297
+ }
298
+
299
+ function updateSkill(context = {}, request = {}) {
300
+ const body = request.body || {};
301
+ const name = normalizeSkillName(request.params?.skillName || body.name);
302
+ const existing = findManagedSkill(context, name, body);
303
+ const root = existing?.root || resolveManagedSkillRoot(context, body);
304
+ const filePath = existing?.skill.path || path.join(root.path, name, 'SKILL.md');
305
+ assertManagedSkillPath(root, filePath);
306
+ if (!existing && !fs.existsSync(filePath)) {
307
+ const error = new Error(`DSH skill not found: ${name}`);
308
+ error.statusCode = 404;
309
+ throw error;
310
+ }
311
+ const current = existing?.skill || getSkill(context, body.profile, name, { cwd: body.cwd })?.skill;
312
+ writeAtomic(filePath, skillDocument({
313
+ ...current,
314
+ ...body,
315
+ metadata: body.metadata === undefined ? current?.metadata : body.metadata
316
+ }, name), 0o600);
317
+ return getSkill(context, body.profile, name, { cwd: body.cwd });
318
+ }
319
+
320
+ function deleteSkill(context = {}, request = {}) {
321
+ const body = request.body || {};
322
+ const name = normalizeSkillName(request.params?.skillName || body.name);
323
+ const existing = findManagedSkill(context, name, body);
324
+ if (!existing) {
325
+ const error = new Error(`DSH skill not found or is not ctx-managed: ${name}`);
326
+ error.statusCode = 404;
327
+ throw error;
328
+ }
329
+ const filePath = existing.skill.path;
330
+ assertManagedSkillPath(existing.root, filePath);
331
+ if (path.basename(filePath).toLowerCase() === 'skill.md' && path.basename(path.dirname(filePath)) === name) {
332
+ fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
333
+ } else {
334
+ fs.unlinkSync(filePath);
335
+ }
336
+ return { name, removed: true, scope: existing.root.source === 'user-dsh' ? 'user' : 'project' };
337
+ }
338
+
339
+ module.exports = {
340
+ resolveSkillRoots,
341
+ listSkills,
342
+ getSkill,
343
+ createSkill,
344
+ updateSkill,
345
+ deleteSkill
346
+ };