coding-tool-x 3.8.7 → 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 (133) hide show
  1. package/dist/web/assets/{AgentsDrawer-C7ePByZx.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-Ccl4rhdz.js → ChatHistoryDrawer-CrfS0KYV.js} +1 -1
  5. package/dist/web/assets/{CollapseItem-MxG_UD4o.js → CollapseItem-ClMsJuUu.js} +1 -1
  6. package/dist/web/assets/{CommandsDrawer-DWbaxHRZ.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-CKMFsfEF.js → ConfigTemplatesDrawer-BqM32Im7.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplatesPanel-Cvbnkvpt.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-DA-dR_vP.js → FavoritesDrawer-DDyWUBJR.js} +1 -1
  13. package/dist/web/assets/Forward-DLCL7aMY.js +1 -0
  14. package/dist/web/assets/{GatewayConvertDrawer-402_raGb.js → GatewayConvertDrawer-Rigtt_5J.js} +1 -1
  15. package/dist/web/assets/Home-tCSKmSHV.js +1 -0
  16. package/dist/web/assets/{MarkdownEditor-BvkDECwH.js → MarkdownEditor-BMMUufSc.js} +1 -1
  17. package/dist/web/assets/McpDrawer-CmE-d7VW.js +1 -0
  18. package/dist/web/assets/{McpFormDrawer-DPMnyt5L.js → McpFormDrawer-ByvS18od.js} +1 -1
  19. package/dist/web/assets/{PluginManager-FKVeugr4.js → PluginManager-C4kJlpVX.js} +1 -1
  20. package/dist/web/assets/{PluginsDrawer-MvM1R55d.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-lMjttbkD.js → SettingsDrawer-BITzjQLl.js} +1 -1
  32. package/dist/web/assets/SkillManager-D9jkNuRM.js +1 -0
  33. package/dist/web/assets/{SkillsDrawer-DF6JsSyj.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-BaZkZqPP.js → SpeedTestDrawer-Bx1bYf5i.js} +1 -1
  37. package/dist/web/assets/Upload-CzC4rGI4.js +1 -0
  38. package/dist/web/assets/{WorkspaceDrawer-BFSwZ5yU.js → WorkspaceDrawer-eujtWpoq.js} +1 -1
  39. package/dist/web/assets/{WorkspaceManager-BHL1uYct.js → WorkspaceManager-CQ-8Ej68.js} +1 -1
  40. package/dist/web/assets/{WorkspacePanel-B5PooBtg.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-BohDDAy_.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-D-DDpbnI.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/driver-factories/api.js +22 -6
  107. package/src/shared/driver-factories/projects.js +8 -0
  108. package/dist/web/assets/Analytics-BVG6ehFv.js +0 -1
  109. package/dist/web/assets/AssetPathField-BZI9NvEr.js +0 -1
  110. package/dist/web/assets/ConfigExportDrawer-DrawKwxE.js +0 -1
  111. package/dist/web/assets/ConfigTemplates-AiRPv2cd.js +0 -1
  112. package/dist/web/assets/Home-CTF8ZWKW.js +0 -1
  113. package/dist/web/assets/McpDrawer-DZGHkuDm.js +0 -1
  114. package/dist/web/assets/PluginsPanel-CKuBs38B.js +0 -1
  115. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +0 -1
  116. package/dist/web/assets/ProjectList-677RLZ8O.css +0 -1
  117. package/dist/web/assets/ProjectList-tsjwjXV1.js +0 -1
  118. package/dist/web/assets/PromptsDrawer-B-X9ci8n.js +0 -1
  119. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +0 -1
  120. package/dist/web/assets/RecentSessionsDrawer-wLktMF_c.js +0 -1
  121. package/dist/web/assets/SessionCard-DkJPy0TY.js +0 -1
  122. package/dist/web/assets/SessionList-BpYRz1cX.css +0 -1
  123. package/dist/web/assets/SessionList-Gzhh4FOZ.js +0 -1
  124. package/dist/web/assets/SkillManager-CzFxWxx-.js +0 -1
  125. package/dist/web/assets/SkillsPanel--lLEts9y.js +0 -1
  126. package/dist/web/assets/Upload-BEQ-3Kqq.js +0 -1
  127. package/dist/web/assets/headers-cf-QapUq.js +0 -1
  128. package/dist/web/assets/index-BFInlZGI.js +0 -2
  129. package/dist/web/assets/platforms-CBkB45JP.js +0 -1
  130. package/dist/web/assets/platforms-FTpjxAT6.js +0 -1
  131. package/dist/web/assets/sessions-Cmy31slD.js +0 -1
  132. package/dist/web/assets/sessions-yC_oVVh8.js +0 -1
  133. package/dist/web/assets/use-message-DoLM8C1C.js +0 -1
@@ -339,11 +339,67 @@ function parseSession(filePath) {
339
339
  };
340
340
  }
341
341
 
342
+ /**
343
+ * Read only the beginning of a rollout for list views. Codex rollouts can be
344
+ * hundreds of megabytes; this helper must never call readJSONL/parseSession.
345
+ */
346
+ function parseSessionSummary(filePath, { sessionId = null, mtimeMs = null } = {}) {
347
+ let fd;
348
+ try {
349
+ fd = fs.openSync(filePath, 'r');
350
+ const buffer = Buffer.alloc(64 * 1024);
351
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
352
+ const lines = buffer.subarray(0, bytesRead).toString('utf8')
353
+ .split(/\r?\n/)
354
+ .filter(line => line.trim())
355
+ .map(line => {
356
+ try { return JSON.parse(line); } catch (_) { return null; }
357
+ })
358
+ .filter(Boolean);
359
+ const meta = extractSessionMeta(lines);
360
+ if (!meta) return null;
361
+ const firstUser = lines.find(line => {
362
+ const payload = line.type === 'response_item' ? line.payload : null;
363
+ return payload?.type === 'message' && payload.role === 'user' && Array.isArray(payload.content);
364
+ });
365
+ const content = firstUser?.payload?.content
366
+ ?.map(block => block?.text || block?.input_text || '')
367
+ .join('\n')
368
+ .trim() || null;
369
+ const nativeSessionId = typeof meta.sessionId === 'string' && meta.sessionId.trim()
370
+ ? meta.sessionId.trim()
371
+ : sessionId;
372
+ const projectName = extractCodexProjectName(meta);
373
+ return {
374
+ session: {
375
+ sessionId: nativeSessionId,
376
+ projectName,
377
+ projectDisplayName: projectName,
378
+ projectFullPath: typeof meta.cwd === 'string' && meta.cwd.trim() ? meta.cwd.trim() : null,
379
+ firstMessage: content,
380
+ gitBranch: meta.git?.branch || null,
381
+ provider: meta.provider || null,
382
+ model: null,
383
+ startedAt: meta.timestamp ? new Date(meta.timestamp).getTime() : null,
384
+ updatedAt: mtimeMs,
385
+ extraJson: JSON.stringify({ cwd: meta.cwd || null })
386
+ }
387
+ };
388
+ } catch (_) {
389
+ return null;
390
+ } finally {
391
+ if (fd !== undefined) {
392
+ try { fs.closeSync(fd); } catch (_) {}
393
+ }
394
+ }
395
+ }
396
+
342
397
  module.exports = {
343
398
  readJSONL,
344
399
  extractSessionMeta,
345
400
  extractMessages,
346
401
  extractTokenUsage,
347
402
  parseSession,
348
- parseSessionMeta
403
+ parseSessionMeta,
404
+ parseSessionSummary
349
405
  };
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { getCodexDir } = require('./config');
6
- const { parseSession } = require('./parser');
6
+ const { parseSession, parseSessionSummary } = require('./parser');
7
7
 
8
8
  function configure({ pathContext } = {}) {
9
9
  require('./config').configure?.({ pathContext });
@@ -90,6 +90,12 @@ async function inventory({ projectsDir } = {}) {
90
90
  }));
91
91
  }
92
92
 
93
+ async function summarize(descriptor) {
94
+ const parsed = parseSessionSummary(descriptor.filePath, descriptor);
95
+ if (!parsed?.session) return { sessionId: descriptor.sessionId, projectName: descriptor.projectHint || 'unknown' };
96
+ return parsed;
97
+ }
98
+
93
99
  /**
94
100
  * Parse a Codex session file.
95
101
  */
@@ -169,4 +175,4 @@ async function parse(descriptor) {
169
175
  return { session, messages: normalizedMessages };
170
176
  }
171
177
 
172
- module.exports = { configure, inventory, parse, extractCodexProjectName };
178
+ module.exports = { configure, inventory, summarize, parse, extractCodexProjectName };
@@ -250,7 +250,7 @@ function normalizeSession(codexSession) {
250
250
  */
251
251
  async function getProjects(options = {}) {
252
252
  if (!fs.existsSync(getSessionsDir())) return [];
253
- const projects = await getSessionHistoryIndex({ ...options, projectsDir: getSessionsDir() }).listProjects('codex', options);
253
+ const projects = await getSessionHistoryIndex({ ...options, projectsDir: getSessionsDir() }).getAllProjects('codex', options);
254
254
  const savedOrder = getProjectOrder();
255
255
  if (savedOrder.length === 0) return projects;
256
256
 
@@ -260,6 +260,11 @@ async function getProjects(options = {}) {
260
260
  return [...ordered, ...byName.values()];
261
261
  }
262
262
 
263
+ async function getProjectsPage(options = {}) {
264
+ return getSessionHistoryIndex({ ...options, projectsDir: getSessionsDir() })
265
+ .listProjectsPage('codex', options);
266
+ }
267
+
263
268
  /**
264
269
  * 根据项目名获取会话列表(归一化格式)
265
270
  * @param {string} projectName - 项目名称
@@ -284,6 +289,21 @@ async function getSessionsByProject(projectName, options = {}) {
284
289
  return [...byId.values(), ...ordered];
285
290
  }
286
291
 
292
+ async function getSessionsPage(projectName, options = {}) {
293
+ const payload = await getSessionHistoryIndex().listSessionsPage('codex', projectName, options);
294
+ const forkRelations = require('../claude/sessions-implementation').getForkRelations();
295
+ const aliases = require('../../../server/services/alias').loadAliases();
296
+ return {
297
+ ...payload,
298
+ sessions: payload.sessions.map(session => ({
299
+ ...session,
300
+ mtime: new Date(session.mtime).toISOString(),
301
+ forkedFrom: forkRelations[session.sessionId] || null,
302
+ alias: aliases[session.sessionId] || null
303
+ }))
304
+ };
305
+ }
306
+
287
307
  /**
288
308
  * 根据 sessionId 获取会话(归一化格式)
289
309
  * @param {string} sessionId - 会话 ID
@@ -302,8 +322,8 @@ async function getSessionById(sessionId) {
302
322
  * @param {string} keyword - 搜索关键词
303
323
  * @returns {Array} 搜索结果
304
324
  */
305
- async function searchSessions(keyword) {
306
- return getSessionHistoryIndex().searchSessions('codex', keyword);
325
+ async function searchSessions(keyword, options = {}) {
326
+ return getSessionHistoryIndex().searchSessions('codex', keyword, options);
307
327
  }
308
328
 
309
329
  /**
@@ -825,7 +845,9 @@ module.exports = {
825
845
  scanSessionFiles,
826
846
  getAllSessions,
827
847
  getProjects,
848
+ getProjectsPage,
828
849
  getSessionsByProject,
850
+ getSessionsPage,
829
851
  getSessionById,
830
852
  searchSessions,
831
853
  normalizeSession,
@@ -11,9 +11,10 @@ function createDriver(context = {}) {
11
11
  servicePath: './codex/sessions-implementation',
12
12
  localServicePath: '../platforms/drivers/codex/sessions-implementation',
13
13
  adapterLocalPath: '../platforms/drivers/codex/session-history-adapter',
14
- adapterMethods: { inventory: 'inventory', parse: 'parse' },
14
+ adapterMethods: { inventory: 'inventory', summarize: 'summarize', parse: 'parse' },
15
15
  methods: {
16
16
  listSessions: 'getSessionsByProject',
17
+ listSessionsPage: 'getSessionsPage',
17
18
  recent: 'getRecentSessions',
18
19
  search: 'searchSessions',
19
20
  getSessionById: 'getSessionById',
@@ -25,6 +26,22 @@ function createDriver(context = {}) {
25
26
  status: 'getSessionStatus',
26
27
  messages: 'getSessionMessages'
27
28
  },
29
+ customMethods: {
30
+ searchAcrossProjects: (service, requestOrKeyword, limit, options = {}) => {
31
+ if (requestOrKeyword && typeof requestOrKeyword === 'object' && !Array.isArray(requestOrKeyword)) {
32
+ const request = requestOrKeyword;
33
+ const query = request.query || {};
34
+ return service.searchSessions(query.keyword || query.q || '', {
35
+ ...request,
36
+ ...options,
37
+ projectName: query.projectName || query.project || null,
38
+ limit: Number.parseInt(query.limit, 10) || 35,
39
+ contextLength: Number(query.context) || 35
40
+ });
41
+ }
42
+ return service.searchSessions(requestOrKeyword, { ...options, limit });
43
+ }
44
+ },
28
45
  onSuccess: operation => {
29
46
  if (['delete', 'fork', 'saveSessionOrder'].includes(operation)) {
30
47
  context.sessionHistoryIndex?.invalidateSource('codex');
@@ -0,0 +1,396 @@
1
+ 'use strict';
2
+
3
+ const { createDriver: createNativeConfigDriver } = require('./native-config');
4
+ const { createDriver: createProjectsDriver } = require('./projects');
5
+ const { createDriver: createSessionsDriver } = require('./sessions');
6
+ const { listProfiles, listProfilePlugins, listProfileCapabilities, listProfileMcp, listProfilePrompts, listPlugins, upsertProfilePatchRow, deleteProfilePatchRow } = require('./common');
7
+ const { listSkills, getSkill, createSkill, updateSkill, deleteSkill } = require('./resources');
8
+ const { installPlugin, uninstallPlugin, updatePlugin } = require('./plugin-manager');
9
+ const { createDriver: createChannelsDriver } = require('./channels');
10
+ const { createDriver: createProxyDriver } = require('./proxy');
11
+ const { createDriver: createStatisticsDriver } = require('./statistics');
12
+
13
+ function requiredId(value, label) {
14
+ const id = String(value || '').trim();
15
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(id)) throw new Error(`${label} must be a safe identifier`);
16
+ return id;
17
+ }
18
+
19
+ function profilePatchOptions(request = {}) {
20
+ const body = request.body || {};
21
+ return { expectedRevision: body.expectedRevision };
22
+ }
23
+
24
+ function mcpConfigFromRequest(request = {}) {
25
+ const body = request.body || {};
26
+ const source = body.config && typeof body.config === 'object' && !Array.isArray(body.config) ? body.config : body;
27
+ const config = { ...source };
28
+ delete config.id;
29
+ delete config.name;
30
+ delete config.profile;
31
+ delete config.expectedRevision;
32
+ delete config.enabled;
33
+ delete config.disabled;
34
+ delete config.scope;
35
+ delete config.cwd;
36
+ if (!['stdio', 'streamable-http'].includes(config.transport)) throw new Error('MCP transport must be stdio or streamable-http');
37
+ if (!/^[A-Za-z0-9_-]{1,32}$/.test(String(config.serverName || '').trim())) throw new Error('MCP serverName must be 1-32 ASCII letters, digits, underscores, or hyphens');
38
+ return config;
39
+ }
40
+
41
+ function promptConfigFromRequest(request = {}) {
42
+ const body = request.body || {};
43
+ const source = body.config && typeof body.config === 'object' && !Array.isArray(body.config) ? body.config : body;
44
+ const config = { ...source };
45
+ delete config.id;
46
+ delete config.name;
47
+ delete config.profile;
48
+ delete config.expectedRevision;
49
+ delete config.enabled;
50
+ delete config.disabled;
51
+ delete config.scope;
52
+ delete config.cwd;
53
+ return config;
54
+ }
55
+
56
+ function patchResult(context, request, profileName, operation, patch) {
57
+ return {
58
+ profile: profileName,
59
+ operation,
60
+ patch,
61
+ mcp: listProfileMcp(context, profileName),
62
+ prompts: listProfilePrompts(context, profileName)
63
+ };
64
+ }
65
+
66
+ function requirePatchResult(patch, profileName) {
67
+ if (patch) return patch;
68
+ const error = new Error(`DSH profile not found: ${profileName}`);
69
+ error.statusCode = 404;
70
+ throw error;
71
+ }
72
+
73
+ function resultFor(context = {}, status, data, error, cause) {
74
+ const route = context.route || {};
75
+ const result = {
76
+ status,
77
+ platform: context.platform || 'dsh',
78
+ capability: route.capability || context.capability || 'api',
79
+ operation: route.operation || context.operation
80
+ };
81
+ if (status === 'ok') result.data = data;
82
+ if (error) result.error = error instanceof Error ? error.message : String(error);
83
+ if (cause) Object.defineProperty(result, 'cause', { value: cause, enumerable: false });
84
+ return result;
85
+ }
86
+
87
+ function resolveProfileName(context = {}, requested = '') {
88
+ const profiles = listProfiles(context);
89
+ const requestedName = String(requested || '').trim();
90
+ if (requestedName) return requestedName;
91
+ const envProfile = String(process.env.DSH_PROFILE || '').trim();
92
+ if (envProfile && profiles.some(profile => profile.name === envProfile)) return envProfile;
93
+ return profiles.find(profile => profile.name === 'default')?.name || profiles[0]?.name || null;
94
+ }
95
+
96
+ function profileNameForRequest(context = {}, request = {}) {
97
+ return resolveProfileName(
98
+ context,
99
+ request.profile
100
+ || request.query?.profile
101
+ || request.body?.profile
102
+ || request.params?.profileName
103
+ );
104
+ }
105
+
106
+ function dshMcpConfigToSpec(config = {}) {
107
+ const transport = config.transport === 'streamable-http' ? 'streamable_http' : config.transport || 'stdio';
108
+ const spec = { type: transport };
109
+ if (transport === 'stdio') {
110
+ if (config.command) spec.command = config.command;
111
+ if (Array.isArray(config.args)) spec.args = [...config.args];
112
+ if (config.env && typeof config.env === 'object') spec.env = { ...config.env };
113
+ } else {
114
+ if (config.url) spec.url = config.url;
115
+ if (config.headers && typeof config.headers === 'object') spec.headers = { ...config.headers };
116
+ }
117
+ return spec;
118
+ }
119
+
120
+ function dshMcpSpecToConfig(spec = {}, serverName) {
121
+ const type = spec.type === 'streamable_http' ? 'streamable-http' : spec.type || 'stdio';
122
+ if (!['stdio', 'streamable-http'].includes(type)) {
123
+ throw new Error(`DSH MCP does not support transport: ${spec.type || type}`);
124
+ }
125
+ const config = { serverName, transport: type };
126
+ if (type === 'stdio') {
127
+ if (spec.command) config.command = spec.command;
128
+ if (Array.isArray(spec.args)) config.args = [...spec.args];
129
+ if (spec.env && typeof spec.env === 'object') config.env = { ...spec.env };
130
+ } else {
131
+ if (spec.url) config.url = spec.url;
132
+ if (spec.headers && typeof spec.headers === 'object') config.headers = { ...spec.headers };
133
+ }
134
+ return config;
135
+ }
136
+
137
+ function readDshMcpEntries(context = {}) {
138
+ const profile = resolveProfileName(context);
139
+ if (!profile) return {};
140
+ const result = listProfileMcp(context, profile);
141
+ const entries = {};
142
+ for (const server of result?.servers || []) {
143
+ if (!server?.id || server.disabled) continue;
144
+ entries[server.id] = dshMcpConfigToSpec(server.config || {});
145
+ }
146
+ return entries;
147
+ }
148
+
149
+ function promptContentFromDshConfig(config = {}) {
150
+ for (const key of ['content', 'prompt', 'text']) {
151
+ if (typeof config[key] === 'string') return config[key];
152
+ }
153
+ return [config.personaPrefix, config.personaSuffix]
154
+ .filter(value => typeof value === 'string' && value.trim())
155
+ .join('\n')
156
+ .trim();
157
+ }
158
+
159
+ function readDshPrompt(context = {}) {
160
+ const profile = resolveProfileName(context);
161
+ if (!profile) return '';
162
+ const result = listProfilePrompts(context, profile);
163
+ const prompt = [...(result?.prompts || [])]
164
+ .filter(entry => entry && entry.disabled !== true)
165
+ .pop();
166
+ return promptContentFromDshConfig(prompt?.config || {});
167
+ }
168
+
169
+ function dshCapabilityContext(context, capability, operation) {
170
+ return { ...context, capability, operation };
171
+ }
172
+
173
+ function createDriver(context = {}) {
174
+ const native = createNativeConfigDriver(context);
175
+ const projects = createProjectsDriver(context);
176
+ const sessions = createSessionsDriver(context);
177
+ const channels = createChannelsDriver(context);
178
+ const proxy = createProxyDriver(context);
179
+ const statistics = createStatisticsDriver(context);
180
+ const driver = {
181
+ ...projects,
182
+ ...sessions,
183
+ platform: 'dsh',
184
+ capability: context.capability || 'api'
185
+ };
186
+
187
+ // Keep the aggregate DSH API driver compatible with the manifest consistency
188
+ // contract. Actual descriptor requests are still dispatched by capability.
189
+ for (const operation of ['list', 'current', 'enabled', 'create', 'update', 'remove', 'applyToSettings', 'sync', 'order', 'models', 'probeModels', 'speedTest', 'speedTestAll']) {
190
+ driver[operation] = (...args) => channels[operation](...args);
191
+ }
192
+ for (const operation of ['status', 'start', 'stop']) {
193
+ driver[operation] = (...args) => proxy[operation](...args);
194
+ }
195
+ for (const operation of ['summary', 'today', 'daily']) {
196
+ driver[operation] = (...args) => statistics[operation](...args);
197
+ }
198
+
199
+ driver.getConfig = async request => resultFor(request, 'ok', native.getConfig());
200
+ driver.updateConfig = async request => {
201
+ try {
202
+ return resultFor(request, 'ok', native.updateConfig(request));
203
+ } catch (error) {
204
+ return resultFor(request, error.statusCode === 409 ? 'conflict' : 'failed', undefined, error, error);
205
+ }
206
+ };
207
+ driver.getConfigCapabilities = async request => resultFor(request, 'ok', native.getConfigCapabilities());
208
+ driver.getConfigAuthProviders = async request => resultFor(request, 'ok', native.getConfigAuthProviders());
209
+ driver.getConfigResources = async request => resultFor(request, 'ok', native.getConfigResources(request));
210
+ driver.listProfiles = async request => resultFor(request, 'ok', { profiles: listProfiles(context) });
211
+ driver.listPlugins = request => resultFor(request, 'ok', listPlugins(context));
212
+ driver.listProfilePlugins = async (request = {}) => {
213
+ const profile = request.params?.profileName;
214
+ const result = listProfilePlugins(context, profile);
215
+ return result ? resultFor(request, 'ok', result) : resultFor(request, 'unsupported');
216
+ };
217
+ driver.listProfileCapabilities = async (request = {}) => {
218
+ const profile = request.params?.profileName;
219
+ const result = listProfileCapabilities(context, profile);
220
+ return result ? resultFor(request, 'ok', result) : resultFor(request, 'unsupported');
221
+ };
222
+ driver.listProfileMcp = async (request = {}) => {
223
+ const profile = request.params?.profileName;
224
+ const result = listProfileMcp(context, profile);
225
+ return result ? resultFor(request, 'ok', result) : resultFor(request, 'unsupported');
226
+ };
227
+ driver.listProfilePrompts = async (request = {}) => {
228
+ const profile = request.params?.profileName;
229
+ const result = listProfilePrompts(context, profile);
230
+ return result ? resultFor(request, 'ok', result) : resultFor(request, 'unsupported');
231
+ };
232
+ driver.installPlugin = async (request = {}) => {
233
+ const result = await installPlugin(context, request);
234
+ return resultFor(request, 'ok', result);
235
+ };
236
+ driver.uninstallPlugin = async (request = {}) => {
237
+ const result = await uninstallPlugin(context, request);
238
+ return resultFor(request, 'ok', result);
239
+ };
240
+ driver.updatePlugin = async (request = {}) => {
241
+ const result = await updatePlugin(context, request);
242
+ return resultFor(request, 'ok', result);
243
+ };
244
+ driver.upsertMcp = async (request = {}) => {
245
+ const profileName = request.params?.profileName;
246
+ const body = request.body || {};
247
+ const config = mcpConfigFromRequest(request);
248
+ const serverName = String(config.serverName).trim();
249
+ const id = requiredId(request.params?.serverId || body.id || `mcp-${serverName}`, 'MCP id');
250
+ const entry = {
251
+ id,
252
+ name: '@deepseek-ai/dsh-mcp-client',
253
+ ...(body.disabled === true || body.enabled === false ? { disabled: true } : {}),
254
+ config
255
+ };
256
+ const patch = requirePatchResult(
257
+ upsertProfilePatchRow(context, profileName, entry, profilePatchOptions(request)),
258
+ profileName
259
+ );
260
+ return resultFor(request, 'ok', patchResult(context, request, profileName, 'upsertMcp', patch));
261
+ };
262
+ driver.deleteMcp = async (request = {}) => {
263
+ const profileName = request.params?.profileName;
264
+ const body = request.body || {};
265
+ const id = requiredId(request.params?.serverId || body.id, 'MCP id');
266
+ const patch = requirePatchResult(
267
+ deleteProfilePatchRow(context, profileName, id, profilePatchOptions(request)),
268
+ profileName
269
+ );
270
+ return resultFor(request, 'ok', patchResult(context, request, profileName, 'deleteMcp', patch));
271
+ };
272
+ driver.upsertPrompt = async (request = {}) => {
273
+ const profileName = request.params?.profileName;
274
+ const body = request.body || {};
275
+ const id = requiredId(request.params?.promptId || body.id, 'prompt id');
276
+ const patch = requirePatchResult(
277
+ upsertProfilePatchRow(context, profileName, {
278
+ id,
279
+ name: '@deepseek-ai/dsh-system-prompt',
280
+ ...(body.disabled === true || body.enabled === false ? { disabled: true } : {}),
281
+ config: promptConfigFromRequest(request)
282
+ }, profilePatchOptions(request)),
283
+ profileName
284
+ );
285
+ return resultFor(request, 'ok', patchResult(context, request, profileName, 'upsertPrompt', patch));
286
+ };
287
+ driver.deletePrompt = async (request = {}) => {
288
+ const profileName = request.params?.profileName;
289
+ const body = request.body || {};
290
+ const id = requiredId(request.params?.promptId || body.id, 'prompt id');
291
+ const patch = requirePatchResult(
292
+ deleteProfilePatchRow(context, profileName, id, profilePatchOptions(request)),
293
+ profileName
294
+ );
295
+ return resultFor(request, 'ok', patchResult(context, request, profileName, 'deletePrompt', patch));
296
+ };
297
+
298
+ if (context.capability === 'mcp') {
299
+ const mcpContext = operation => dshCapabilityContext(context, 'mcp', operation);
300
+ driver.entries = () => resultFor(mcpContext('entries'), 'ok', readDshMcpEntries(context));
301
+ driver.read = () => resultFor(mcpContext('read'), 'ok', { mcpServers: readDshMcpEntries(context) });
302
+ driver.normalize = spec => resultFor(mcpContext('normalize'), 'ok', spec);
303
+ driver.sync = async server => {
304
+ const profileName = profileNameForRequest(context, server);
305
+ const id = String(server?.id || '').trim();
306
+ if (!id) throw new Error('MCP server id is required');
307
+ const patch = requirePatchResult(
308
+ upsertProfilePatchRow(context, profileName, {
309
+ id,
310
+ name: '@deepseek-ai/dsh-mcp-client',
311
+ config: dshMcpSpecToConfig(server.server || {}, id)
312
+ }),
313
+ profileName
314
+ );
315
+ return resultFor(mcpContext('sync'), 'ok', patch);
316
+ };
317
+ driver.remove = async serverId => {
318
+ const profileName = profileNameForRequest(context);
319
+ const id = String(serverId || '').trim();
320
+ if (!id) throw new Error('MCP server id is required');
321
+ if (!profileName) return resultFor(mcpContext('remove'), 'ok', true);
322
+ const patch = requirePatchResult(deleteProfilePatchRow(context, profileName, id), profileName);
323
+ return resultFor(mcpContext('remove'), 'ok', patch);
324
+ };
325
+ driver.import = async servers => {
326
+ const entries = readDshMcpEntries(context);
327
+ let count = 0;
328
+ for (const [id, spec] of Object.entries(entries)) {
329
+ if (servers[id]) {
330
+ servers[id].apps = { ...(servers[id].apps || {}), dsh: true };
331
+ continue;
332
+ }
333
+ const now = Date.now();
334
+ servers[id] = {
335
+ id,
336
+ name: id,
337
+ server: spec,
338
+ apps: { dsh: true },
339
+ createdAt: now,
340
+ updatedAt: now
341
+ };
342
+ count++;
343
+ }
344
+ return resultFor(mcpContext('import'), 'ok', count);
345
+ };
346
+ }
347
+
348
+ if (context.capability === 'prompts') {
349
+ const promptContext = operation => dshCapabilityContext(context, 'prompts', operation);
350
+ driver.read = () => resultFor(promptContext('read'), 'ok', readDshPrompt(context));
351
+ driver.write = async content => {
352
+ const profileName = profileNameForRequest(context);
353
+ if (!profileName) throw new Error('No DSH profile is available');
354
+ if (typeof content !== 'string') throw new Error('Prompt text must be a string');
355
+ const patch = requirePatchResult(
356
+ upsertProfilePatchRow(context, profileName, {
357
+ id: 'system-prompt',
358
+ name: '@deepseek-ai/dsh-system-prompt',
359
+ config: { personaPrefix: content }
360
+ }),
361
+ profileName
362
+ );
363
+ return resultFor(promptContext('write'), 'ok', patch);
364
+ };
365
+ driver.remove = async () => {
366
+ const profileName = profileNameForRequest(context);
367
+ if (!profileName) return resultFor(promptContext('remove'), 'ok', true);
368
+ const patch = requirePatchResult(
369
+ deleteProfilePatchRow(context, profileName, 'system-prompt'),
370
+ profileName
371
+ );
372
+ return resultFor(promptContext('remove'), 'ok', patch);
373
+ };
374
+ }
375
+
376
+ driver.listSkills = async (request = {}) => resultFor(request, 'ok', listSkills(context, {
377
+ profile: request.query?.profile,
378
+ cwd: request.query?.cwd,
379
+ scope: request.query?.scope,
380
+ includeContent: request.query?.includeContent === '1'
381
+ }));
382
+ driver.getSkill = async (request = {}) => {
383
+ const result = getSkill(context, request.query?.profile, request.params?.skillName, {
384
+ cwd: request.query?.cwd,
385
+ scope: request.query?.scope
386
+ });
387
+ return result ? resultFor(request, 'ok', result) : resultFor(request, 'unsupported');
388
+ };
389
+ driver.createSkill = async (request = {}) => resultFor(request, 'ok', createSkill(context, request));
390
+ driver.updateSkill = async (request = {}) => resultFor(request, 'ok', updateSkill(context, request));
391
+ driver.deleteSkill = async (request = {}) => resultFor(request, 'ok', deleteSkill(context, request));
392
+
393
+ return driver;
394
+ }
395
+
396
+ module.exports = { createDriver };