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,571 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const yaml = require('js-yaml');
7
+
8
+ // DSH's patch language deliberately keeps `!!js` expressions unevaluated until
9
+ // the harness boots. The control plane only needs the static entry shape, so
10
+ // preserve the expression as text instead of executing it or rejecting it.
11
+ class DshJsExpression {
12
+ constructor(value) {
13
+ this.value = value;
14
+ }
15
+ }
16
+
17
+ const DSH_YAML_SCHEMA = yaml.DEFAULT_SCHEMA.extend([
18
+ new yaml.Type('tag:yaml.org,2002:js', {
19
+ kind: 'scalar',
20
+ instanceOf: DshJsExpression,
21
+ construct: value => `!!js ${value}`,
22
+ represent: value => value.value
23
+ })
24
+ ]);
25
+
26
+ function clone(value) {
27
+ if (value === undefined) return undefined;
28
+ return JSON.parse(JSON.stringify(value));
29
+ }
30
+
31
+ function resolvePaths(context = {}) {
32
+ const paths = context.paths || context.pathContext?.native || {};
33
+ const home = path.resolve(paths.dir || paths.home || context.pathContext?.home || process.env.DSH_HOME || path.join(require('os').homedir(), '.dsh'));
34
+ const resolved = {
35
+ dir: home,
36
+ settings: paths.settings || path.join(home, 'settings.yaml'),
37
+ credentials: paths.credentials || path.join(home, '.credentials.yaml'),
38
+ sessions: paths.sessions || path.join(home, 'sessions'),
39
+ profiles: paths.profiles || path.join(home, 'profiles'),
40
+ patch: paths.patch || path.join(home, 'cordis.patch.yml')
41
+ };
42
+ const installAnchor = paths.installAnchor || context.installAnchor || process.env.DSH_INSTALL_ANCHOR;
43
+ const installRoot = paths.installRoot || context.installRoot || process.env.DSH_INSTALL_ROOT;
44
+ if (installAnchor) resolved.installAnchor = installAnchor;
45
+ if (installRoot) resolved.installRoot = installRoot;
46
+ return resolved;
47
+ }
48
+
49
+ function isObject(value) {
50
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
51
+ }
52
+
53
+ function isSecretKey(key) {
54
+ return /^(?:api[-_]?key|access[-_]?token|refresh[-_]?token|token|secret|password|authorization|credentials?)$/i.test(String(key || ''));
55
+ }
56
+
57
+ function redactSecrets(value, key = '') {
58
+ if (isSecretKey(key)) return '[REDACTED]';
59
+ if (Array.isArray(value)) return value.map(entry => redactSecrets(entry));
60
+ if (!isObject(value)) return value;
61
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
62
+ entryKey,
63
+ redactSecrets(entryValue, entryKey)
64
+ ]));
65
+ }
66
+
67
+ function redactPatchConfig(value, key = '') {
68
+ if (isSecretKey(key)) return '[REDACTED]';
69
+ if (key === 'env' || key === 'headers') {
70
+ if (!isObject(value)) return value;
71
+ return Object.fromEntries(Object.keys(value).map(entryKey => [entryKey, '[REDACTED]']));
72
+ }
73
+ if (typeof value === 'string' && /^!!js\s+.*(?:process\.env|credentials?|secret|token|password)/i.test(value)) {
74
+ return '[EXPRESSION]';
75
+ }
76
+ if (key === 'url' && typeof value === 'string') {
77
+ try {
78
+ const parsed = new URL(value);
79
+ if (parsed.username) parsed.username = '[REDACTED]';
80
+ if (parsed.password) parsed.password = '[REDACTED]';
81
+ for (const name of [...parsed.searchParams.keys()]) {
82
+ if (isSecretKey(name)) parsed.searchParams.set(name, '[REDACTED]');
83
+ }
84
+ return parsed.toString();
85
+ } catch (_) {
86
+ return value;
87
+ }
88
+ }
89
+ if (Array.isArray(value)) return value.map(entry => redactPatchConfig(entry));
90
+ if (!isObject(value)) return value;
91
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
92
+ entryKey,
93
+ redactPatchConfig(entryValue, entryKey)
94
+ ]));
95
+ }
96
+
97
+ function readYamlFile(filePath, fallback = {}) {
98
+ if (!fs.existsSync(filePath)) return clone(fallback);
99
+ const content = fs.readFileSync(filePath, 'utf8');
100
+ if (!content.trim()) return clone(fallback);
101
+ const parsed = yaml.load(content, { schema: DSH_YAML_SCHEMA });
102
+ return parsed === undefined || parsed === null ? clone(fallback) : parsed;
103
+ }
104
+
105
+ function markJsExpressions(value) {
106
+ if (Array.isArray(value)) return value.map(markJsExpressions);
107
+ if (isObject(value)) return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, markJsExpressions(entry)]));
108
+ if (typeof value === 'string' && /^!!js\s+\S/.test(value)) return new DshJsExpression(value.replace(/^!!js\s+/, ''));
109
+ return value;
110
+ }
111
+
112
+ function readJsonFile(filePath, fallback = null) {
113
+ try {
114
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
115
+ } catch (error) {
116
+ if (error && error.code === 'ENOENT' && fallback !== undefined) return clone(fallback);
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ function fileRevision(filePath) {
122
+ try {
123
+ const stat = fs.statSync(filePath);
124
+ return `${stat.dev}:${stat.ino}:${stat.size}:${Math.round(stat.mtimeMs)}`;
125
+ } catch (error) {
126
+ if (error && error.code === 'ENOENT') return null;
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ function writeAtomic(filePath, content, mode) {
132
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
133
+ const temporary = `${filePath}.ctx-tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
134
+ try {
135
+ fs.writeFileSync(temporary, content, { encoding: 'utf8', mode });
136
+ if (mode !== undefined) fs.chmodSync(temporary, mode);
137
+ fs.renameSync(temporary, filePath);
138
+ } finally {
139
+ try { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); } catch (_) {}
140
+ }
141
+ }
142
+
143
+ function mergeSettings(existing, namespace, patch, replace = false) {
144
+ if (!isObject(existing)) throw new Error('DSH settings document must be a mapping');
145
+ if (typeof namespace !== 'string' || !namespace.trim()) throw new Error('settings namespace is required');
146
+ if (!isObject(patch)) throw new Error('settings patch must be a mapping');
147
+ const result = clone(existing);
148
+ const current = isObject(result[namespace]) ? result[namespace] : {};
149
+ result[namespace] = replace ? clone(patch) : { ...current, ...clone(patch) };
150
+ return result;
151
+ }
152
+
153
+ function dumpYaml(value) {
154
+ return yaml.dump(markJsExpressions(value), { schema: DSH_YAML_SCHEMA, noRefs: true, lineWidth: 120 });
155
+ }
156
+
157
+ function credentialMetadata(filePath) {
158
+ if (!fs.existsSync(filePath)) return { path: filePath, exists: false, refs: [], records: [] };
159
+ const parsed = readYamlFile(filePath, {});
160
+ if (!isObject(parsed)) throw new Error('DSH credentials document must be a mapping');
161
+ const mode = fs.statSync(filePath).mode & 0o777;
162
+ const refs = isObject(parsed.refs) ? Object.keys(parsed.refs) : [];
163
+ const records = isObject(parsed.records) ? Object.keys(parsed.records) : [];
164
+ return {
165
+ path: filePath,
166
+ exists: true,
167
+ mode,
168
+ secure: (mode & 0o077) === 0,
169
+ version: parsed.version === undefined ? null : parsed.version,
170
+ refs,
171
+ records
172
+ };
173
+ }
174
+
175
+ function safeProfileName(value) {
176
+ const name = String(value || '').trim();
177
+ if (!name || name === '.' || name === '..' || name.includes('/') || name.includes('\\') || name.includes('\0')) {
178
+ throw new Error('Invalid DSH profile name');
179
+ }
180
+ return name;
181
+ }
182
+
183
+ function readProfileManifest(paths, profileName) {
184
+ const name = safeProfileName(profileName);
185
+ const profileDir = path.join(paths.profiles, name);
186
+ const resolved = path.resolve(profileDir);
187
+ if (!resolved.startsWith(`${path.resolve(paths.profiles)}${path.sep}`)) throw new Error('Invalid DSH profile path');
188
+ const manifestPath = path.join(profileDir, 'package.json');
189
+ const manifest = readJsonFile(manifestPath, null);
190
+ if (!manifest) return null;
191
+ return { name, profileDir, manifestPath, manifest };
192
+ }
193
+
194
+ function resolvePackageDir(profileDir, packageName, paths = {}) {
195
+ if (typeof packageName !== 'string' || !packageName.trim()) return null;
196
+ const roots = [
197
+ path.resolve(profileDir, 'node_modules'),
198
+ path.resolve(paths.profiles || path.dirname(profileDir), 'node_modules'),
199
+ paths.installAnchor ? path.resolve(path.dirname(paths.installAnchor), 'node_modules') : null,
200
+ paths.installRoot ? path.resolve(paths.installRoot, 'node_modules') : null
201
+ ].filter(Boolean);
202
+ for (const root of [...new Set(roots)]) {
203
+ const target = path.resolve(root, ...packageName.split('/'));
204
+ if (!target.startsWith(`${root}${path.sep}`)) continue;
205
+ try {
206
+ if (fs.statSync(target).isDirectory()) return target;
207
+ } catch (_) {}
208
+ }
209
+ return null;
210
+ }
211
+
212
+ function listProfiles(context = {}) {
213
+ const paths = resolvePaths(context);
214
+ if (!fs.existsSync(paths.profiles)) return [];
215
+ return fs.readdirSync(paths.profiles, { withFileTypes: true })
216
+ .filter(entry => entry.isDirectory())
217
+ .map(entry => {
218
+ const profile = readProfileManifest(paths, entry.name);
219
+ const manifest = profile?.manifest || {};
220
+ const profileConfig = isObject(manifest.dsh?.profile) ? manifest.dsh.profile : {};
221
+ return {
222
+ name: entry.name,
223
+ path: path.join(paths.profiles, entry.name),
224
+ manifestPath: profile?.manifestPath || path.join(paths.profiles, entry.name, 'package.json'),
225
+ exists: Boolean(profile),
226
+ bundles: Array.isArray(profileConfig.bundles) ? [...profileConfig.bundles] : [],
227
+ patchReload: profileConfig.patchReload || null,
228
+ dependencies: isObject(manifest.dependencies) ? { ...manifest.dependencies } : {}
229
+ };
230
+ })
231
+ .sort((left, right) => left.name.localeCompare(right.name));
232
+ }
233
+
234
+ function listProfilePlugins(context = {}, profileName) {
235
+ const paths = resolvePaths(context);
236
+ const profile = readProfileManifest(paths, profileName);
237
+ if (!profile) return null;
238
+ const manifest = profile.manifest || {};
239
+ const profileConfig = isObject(manifest.dsh?.profile) ? manifest.dsh.profile : {};
240
+ const bundleNames = Array.isArray(profileConfig.bundles) ? profileConfig.bundles : [];
241
+ const dependencyNames = Object.keys(isObject(manifest.dependencies) ? manifest.dependencies : {});
242
+ const names = [...new Set([...bundleNames, ...dependencyNames])];
243
+ const plugins = names.map(name => {
244
+ const packageDir = resolvePackageDir(profile.profileDir, name, paths);
245
+ const packageJsonPath = packageDir ? path.join(packageDir, 'package.json') : null;
246
+ const packageJson = packageJsonPath ? readJsonFile(packageJsonPath, {}) : {};
247
+ const bundle = isObject(packageJson.dsh?.bundle) ? packageJson.dsh.bundle : null;
248
+ const patchPath = bundle?.patch && packageDir ? path.resolve(packageDir, bundle.patch) : null;
249
+ const patchExists = Boolean(patchPath && fs.existsSync(patchPath));
250
+ return {
251
+ name,
252
+ requestedVersion: manifest.dependencies?.[name] || manifest.devDependencies?.[name] || null,
253
+ version: packageJson.version || null,
254
+ installed: Boolean(packageDir),
255
+ bundle: Boolean(bundle),
256
+ patch: bundle?.patch || null,
257
+ patchPath,
258
+ patchExists,
259
+ inBundleList: bundleNames.includes(name),
260
+ management: 'ctx+dsh-plugin'
261
+ };
262
+ });
263
+ return {
264
+ profile: profile.name,
265
+ profilePath: profile.profileDir,
266
+ manifestPath: profile.manifestPath,
267
+ patchPath: path.join(profile.profileDir, 'cordis.patch.yml'),
268
+ patchReload: profileConfig.patchReload || null,
269
+ plugins
270
+ };
271
+ }
272
+
273
+ function profilePatchFiles(context = {}, profileName) {
274
+ const paths = resolvePaths(context);
275
+ const pluginInfo = listProfilePlugins(context, profileName);
276
+ if (!pluginInfo) return null;
277
+ const profile = readProfileManifest(paths, profileName);
278
+ const bundleFiles = pluginInfo.plugins
279
+ .filter(plugin => plugin.inBundleList && plugin.patchPath)
280
+ .map(plugin => plugin.patchPath);
281
+ const files = [...new Set([
282
+ ...bundleFiles,
283
+ path.join(profile.profileDir, 'cordis.patch.yml'),
284
+ paths.patch
285
+ ])];
286
+ return { paths, profile, pluginInfo, files };
287
+ }
288
+
289
+ function readProfilePatchContributions(context = {}, profileName) {
290
+ const info = profilePatchFiles(context, profileName);
291
+ if (!info) return null;
292
+ const contributions = [];
293
+ for (const filePath of info.files) {
294
+ if (!filePath || !fs.existsSync(filePath)) continue;
295
+ const parsed = readYamlFile(filePath, []);
296
+ contributions.push({
297
+ path: filePath,
298
+ rows: collectPatchRows(parsed, [], filePath, { includeConfig: true })
299
+ });
300
+ }
301
+ return { ...info, contributions };
302
+ }
303
+
304
+ function patchEntryId(entry) {
305
+ return isObject(entry) && typeof entry.id === 'string' && entry.id.trim() ? entry.id.trim() : null;
306
+ }
307
+
308
+ function patchRowsFromDocument(document) {
309
+ const rows = [];
310
+ const visit = value => {
311
+ if (Array.isArray(value)) {
312
+ for (const entry of value) visit(entry);
313
+ return;
314
+ }
315
+ if (!isObject(value)) return;
316
+ for (const operation of ['insert', 'replace', 'delete', 'update']) {
317
+ const entries = Array.isArray(value[operation]) ? value[operation] : [];
318
+ for (const entry of entries) {
319
+ if (isObject(entry)) rows.push({ operation, entry });
320
+ }
321
+ }
322
+ if (patchEntryId(value)) rows.push({ operation: 'row', entry: value });
323
+ };
324
+ visit(document);
325
+ return rows;
326
+ }
327
+
328
+ function ensurePatchDocument(document) {
329
+ if (document === undefined || document === null) return [];
330
+ if (!Array.isArray(document)) throw new Error('DSH profile patch must be a top-level YAML array');
331
+ return document;
332
+ }
333
+
334
+ function assertPatchRevision(filePath, expectedRevision) {
335
+ const currentRevision = fileRevision(filePath);
336
+ if (expectedRevision !== undefined && expectedRevision !== currentRevision) {
337
+ const error = new Error('DSH profile patch changed on disk; reload before updating');
338
+ error.statusCode = 409;
339
+ throw error;
340
+ }
341
+ return currentRevision;
342
+ }
343
+
344
+ function updateProfilePatch(context = {}, profileName, mutator, options = {}) {
345
+ const info = profilePatchFiles(context, profileName);
346
+ if (!info) return null;
347
+ const patchPath = path.join(info.profile.profileDir, 'cordis.patch.yml');
348
+ const expectedRevision = options.expectedRevision;
349
+ assertPatchRevision(patchPath, expectedRevision);
350
+ const current = ensurePatchDocument(readYamlFile(patchPath, []));
351
+ const next = mutator(clone(current), {
352
+ patchPath,
353
+ currentRevision: fileRevision(patchPath),
354
+ composedRows: info.files.flatMap(filePath => {
355
+ if (!fs.existsSync(filePath)) return [];
356
+ return patchRowsFromDocument(readYamlFile(filePath, [])).map(row => ({ ...row, source: filePath }));
357
+ })
358
+ });
359
+ if (!Array.isArray(next)) throw new Error('DSH profile patch update must return an array');
360
+ writeAtomic(patchPath, dumpYaml(next), 0o600);
361
+ return { profile: profileName, patchPath, revision: fileRevision(patchPath) };
362
+ }
363
+
364
+ function removeManagedRows(document, predicate) {
365
+ const next = [];
366
+ for (const item of document) {
367
+ if (!isObject(item)) {
368
+ next.push(item);
369
+ continue;
370
+ }
371
+ let changed = false;
372
+ const copy = { ...item };
373
+ for (const operation of ['insert', 'replace', 'delete', 'update']) {
374
+ if (!Array.isArray(copy[operation])) continue;
375
+ const kept = copy[operation].filter(entry => {
376
+ const match = isObject(entry) && predicate(entry);
377
+ if (match) changed = true;
378
+ return !match;
379
+ });
380
+ if (kept.length) copy[operation] = kept;
381
+ else delete copy[operation];
382
+ }
383
+ if (patchEntryId(copy) && predicate(copy)) {
384
+ changed = true;
385
+ continue;
386
+ }
387
+ if (!changed || Object.keys(copy).length) next.push(copy);
388
+ }
389
+ return next;
390
+ }
391
+
392
+ function upsertProfilePatchRow(context = {}, profileName, entry, options = {}) {
393
+ const id = patchEntryId(entry);
394
+ if (!id) throw new Error('DSH patch entry id is required');
395
+ return updateProfilePatch(context, profileName, (document, meta) => {
396
+ const hasComposedRow = meta.composedRows.some(row => patchEntryId(row.entry) === id);
397
+ const withoutOwn = removeManagedRows(document, row => patchEntryId(row) === id);
398
+ if (hasComposedRow) withoutOwn.push(entry);
399
+ else withoutOwn.push({ insert: [entry] });
400
+ return withoutOwn;
401
+ }, options);
402
+ }
403
+
404
+ function deleteProfilePatchRow(context = {}, profileName, id, options = {}) {
405
+ const rowId = patchEntryId({ id });
406
+ if (!rowId) throw new Error('DSH patch entry id is required');
407
+ return updateProfilePatch(context, profileName, (document, meta) => {
408
+ const hasComposedRow = meta.composedRows.some(row => patchEntryId(row.entry) === rowId);
409
+ const hasNonLocalRow = meta.composedRows.some(row => patchEntryId(row.entry) === rowId && row.source !== meta.patchPath);
410
+ const withoutOwn = removeManagedRows(document, row => patchEntryId(row) === rowId);
411
+ if (hasComposedRow && hasNonLocalRow) {
412
+ withoutOwn.push({ delete: [{ id: rowId }] });
413
+ }
414
+ return withoutOwn;
415
+ }, options);
416
+ }
417
+
418
+ function collectPatchRows(value, rows = [], source = null, options = {}) {
419
+ const pushRow = (operation, entry) => {
420
+ const row = {
421
+ operation,
422
+ id: entry.id || null,
423
+ name: entry.name || null,
424
+ disabled: entry.disabled === true,
425
+ hasConfig: Object.prototype.hasOwnProperty.call(entry, 'config'),
426
+ source
427
+ };
428
+ if (options.includeConfig && row.hasConfig) row.config = redactPatchConfig(entry.config);
429
+ rows.push(row);
430
+ };
431
+ if (Array.isArray(value)) {
432
+ for (const entry of value) collectPatchRows(entry, rows, source, options);
433
+ return rows;
434
+ }
435
+ if (!isObject(value)) return rows;
436
+ for (const operation of ['insert', 'replace', 'delete', 'update']) {
437
+ const entries = Array.isArray(value[operation]) ? value[operation] : [];
438
+ for (const entry of entries) {
439
+ if (isObject(entry)) pushRow(operation, entry);
440
+ }
441
+ }
442
+ if (value.id || value.name) {
443
+ pushRow('row', value);
444
+ }
445
+ for (const [key, nested] of Object.entries(value)) {
446
+ if (!['insert', 'replace', 'delete', 'update', 'config'].includes(key)) collectPatchRows(nested, rows, source, options);
447
+ }
448
+ return rows;
449
+ }
450
+
451
+ function effectivePatchRows(info) {
452
+ const byId = new Map();
453
+ for (const contribution of info.contributions) {
454
+ for (const row of contribution.rows) {
455
+ if (!row.id) continue;
456
+ if (row.operation === 'delete') {
457
+ byId.delete(row.id);
458
+ } else {
459
+ byId.set(row.id, row);
460
+ }
461
+ }
462
+ }
463
+ return [...byId.values()];
464
+ }
465
+
466
+ function listProfileCapabilities(context = {}, profileName) {
467
+ const info = readProfilePatchContributions(context, profileName);
468
+ if (!info) return null;
469
+ return {
470
+ profile: info.pluginInfo.profile,
471
+ bundles: info.pluginInfo.plugins.filter(plugin => plugin.inBundleList).map(plugin => plugin.name),
472
+ contributions: info.contributions.map(({ path: source, rows }) => ({
473
+ path: source,
474
+ rows: rows.map(({ config, ...row }) => row)
475
+ })),
476
+ management: 'ctx',
477
+ note: 'ctx 通过 dsh plugin 管理依赖安装、移除和更新;DSH 进程及 profile 运行时仍由 dsh 负责。'
478
+ };
479
+ }
480
+
481
+ function isMcpPatchRow(row) {
482
+ const config = isObject(row.config) ? row.config : {};
483
+ return row.name === '@deepseek-ai/dsh-mcp-client'
484
+ || (typeof config.serverName === 'string' && typeof config.transport === 'string');
485
+ }
486
+
487
+ function isPromptPatchRow(row) {
488
+ return row.id === 'system-prompt'
489
+ || row.name === '@deepseek-ai/dsh-system-prompt'
490
+ || /prompt/i.test(String(row.id || ''))
491
+ || /prompt/i.test(String(row.name || ''));
492
+ }
493
+
494
+ function listProfileMcp(context = {}, profileName) {
495
+ const info = readProfilePatchContributions(context, profileName);
496
+ if (!info) return null;
497
+ const servers = effectivePatchRows(info)
498
+ .filter(isMcpPatchRow)
499
+ .map(row => ({
500
+ id: row.id,
501
+ name: row.name,
502
+ operation: row.operation,
503
+ disabled: row.disabled,
504
+ enabled: !row.disabled,
505
+ source: row.source,
506
+ config: row.config || {}
507
+ }));
508
+ return {
509
+ profile: info.pluginInfo.profile,
510
+ servers,
511
+ management: 'ctx',
512
+ note: 'ctx 管理 MCP 的 profile patch 配置;连接、重连和实际调用由 dsh-mcp-client 运行时管理。'
513
+ };
514
+ }
515
+
516
+ function listProfilePrompts(context = {}, profileName) {
517
+ const info = readProfilePatchContributions(context, profileName);
518
+ if (!info) return null;
519
+ const prompts = effectivePatchRows(info)
520
+ .filter(isPromptPatchRow)
521
+ .map(row => ({
522
+ id: row.id,
523
+ name: row.name,
524
+ operation: row.operation,
525
+ disabled: row.disabled,
526
+ enabled: !row.disabled,
527
+ source: row.source,
528
+ config: row.config || {}
529
+ }));
530
+ return {
531
+ profile: info.pluginInfo.profile,
532
+ prompts,
533
+ management: 'ctx',
534
+ note: 'ctx 管理 prompt 的 profile patch 配置;section 注册与组装仍由 dsh-system-prompt 运行时完成。'
535
+ };
536
+ }
537
+
538
+ function listPlugins(context = {}) {
539
+ return {
540
+ profiles: listProfiles(context).map(profile => listProfilePlugins(context, profile.name)).filter(Boolean),
541
+ management: 'ctx+dsh-plugin',
542
+ note: 'ctx 通过 dsh plugin 管理插件依赖;插件运行生命周期和 profile 重启仍由 dsh 负责。'
543
+ };
544
+ }
545
+
546
+ module.exports = {
547
+ clone,
548
+ resolvePaths,
549
+ isObject,
550
+ readYamlFile,
551
+ readJsonFile,
552
+ fileRevision,
553
+ writeAtomic,
554
+ mergeSettings,
555
+ credentialMetadata,
556
+ listProfiles,
557
+ listProfilePlugins,
558
+ listProfileCapabilities,
559
+ listProfileMcp,
560
+ listProfilePrompts,
561
+ listPlugins,
562
+ profilePatchFiles,
563
+ readProfilePatchContributions,
564
+ dumpYaml,
565
+ updateProfilePatch,
566
+ upsertProfilePatchRow,
567
+ deleteProfilePatchRow,
568
+ safeProfileName,
569
+ redactSecrets,
570
+ redactPatchConfig
571
+ };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { resolvePaths, readYamlFile, writeAtomic, dumpYaml, mergeSettings, fileRevision, credentialMetadata, redactSecrets, listProfiles, listProfilePlugins, listProfileMcp, listProfilePrompts } = require('./common');
5
+ const { resolveSkillRoots } = require('./resources');
6
+
7
+ function createDriver(context = {}) {
8
+ const paths = resolvePaths(context);
9
+ return {
10
+ platform: 'dsh',
11
+ capability: 'nativeConfig',
12
+ getConfig() {
13
+ return {
14
+ settings: {
15
+ path: paths.settings,
16
+ exists: fs.existsSync(paths.settings),
17
+ revision: fileRevision(paths.settings),
18
+ namespaces: redactSecrets(readYamlFile(paths.settings, {}))
19
+ },
20
+ credentials: credentialMetadata(paths.credentials),
21
+ sessions: {
22
+ root: paths.sessions,
23
+ exists: fs.existsSync(paths.sessions)
24
+ },
25
+ home: paths.dir
26
+ };
27
+ },
28
+ updateConfig(request = {}) {
29
+ const body = request.body || request;
30
+ const currentRevision = fileRevision(paths.settings);
31
+ if (body.expectedRevision !== undefined && body.expectedRevision !== currentRevision) {
32
+ const error = new Error('DSH settings changed on disk; reload before updating');
33
+ error.statusCode = 409;
34
+ throw error;
35
+ }
36
+ const current = readYamlFile(paths.settings, {});
37
+ const next = mergeSettings(current, body.namespace, body.patch, body.replace === true);
38
+ writeAtomic(paths.settings, dumpYaml(next), 0o600);
39
+ return this.getConfig();
40
+ },
41
+ getConfigCapabilities() {
42
+ return {
43
+ platform: 'dsh',
44
+ settings: { read: true, update: true, patch: true },
45
+ credentials: { readMetadata: true, values: false, update: false },
46
+ sessions: { read: true, write: false },
47
+ skills: { inspect: true, read: true, create: true, update: true, remove: true },
48
+ plugins: { inspect: true, install: true, remove: true, update: true, enable: false, disable: false },
49
+ mcp: { inspect: true, create: true, update: true, remove: true, connect: false },
50
+ prompts: { inspect: true, create: true, update: true, remove: true }
51
+ };
52
+ },
53
+ getConfigResources(request = {}) {
54
+ const profileName = request.query?.profile || null;
55
+ const skillRoots = resolveSkillRoots(context, {
56
+ profile: profileName,
57
+ cwd: request.query?.cwd
58
+ });
59
+ const profiles = listProfiles(context).map(profile => {
60
+ const plugins = listProfilePlugins(context, profile.name);
61
+ const mcp = listProfileMcp(context, profile.name);
62
+ const prompts = listProfilePrompts(context, profile.name);
63
+ return {
64
+ name: profile.name,
65
+ pluginCount: plugins?.plugins?.length || 0,
66
+ mcpCount: mcp?.servers?.length || 0,
67
+ promptCount: prompts?.prompts?.length || 0
68
+ };
69
+ });
70
+ return {
71
+ platform: 'dsh',
72
+ paths: { ...paths },
73
+ profiles,
74
+ skills: {
75
+ read: true,
76
+ create: true,
77
+ update: true,
78
+ remove: true,
79
+ roots: skillRoots.roots.map(root => ({ path: root.path, source: root.source }))
80
+ },
81
+ plugins: { inspect: true, management: 'ctx+dsh-plugin' },
82
+ mcp: { inspect: true, management: 'ctx-patch' },
83
+ prompts: { inspect: true, management: 'ctx-patch' }
84
+ };
85
+ },
86
+ getConfigAuthProviders() {
87
+ const metadata = credentialMetadata(paths.credentials);
88
+ return { platform: 'dsh', credentials: metadata, providers: metadata.refs };
89
+ }
90
+ };
91
+ }
92
+
93
+ module.exports = { createDriver };