@leterhong/multichat 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.agents/plugins/marketplace.json +32 -0
  2. package/.agents/plugins/plugins/developer-toolkit/.codex-plugin/plugin.json +24 -0
  3. package/.agents/plugins/plugins/developer-toolkit/skills/code-review/SKILL.md +9 -0
  4. package/.agents/plugins/plugins/developer-toolkit/skills/code-review/agents/openai.yaml +3 -0
  5. package/.agents/plugins/plugins/developer-toolkit/skills/release-notes/SKILL.md +9 -0
  6. package/.agents/plugins/plugins/developer-toolkit/skills/release-notes/agents/openai.yaml +3 -0
  7. package/.agents/plugins/plugins/developer-toolkit/skills/security-review/SKILL.md +9 -0
  8. package/.agents/plugins/plugins/developer-toolkit/skills/security-review/agents/openai.yaml +3 -0
  9. package/.agents/plugins/plugins/developer-toolkit/skills/test-plan/SKILL.md +9 -0
  10. package/.agents/plugins/plugins/developer-toolkit/skills/test-plan/agents/openai.yaml +3 -0
  11. package/.agents/plugins/plugins/writing-toolkit/.codex-plugin/plugin.json +24 -0
  12. package/.agents/plugins/plugins/writing-toolkit/skills/meeting-notes/SKILL.md +9 -0
  13. package/.agents/plugins/plugins/writing-toolkit/skills/meeting-notes/agents/openai.yaml +3 -0
  14. package/.agents/plugins/plugins/writing-toolkit/skills/resume-tune/SKILL.md +9 -0
  15. package/.agents/plugins/plugins/writing-toolkit/skills/resume-tune/agents/openai.yaml +3 -0
  16. package/.agents/plugins/plugins/writing-toolkit/skills/weekly-report/SKILL.md +9 -0
  17. package/.agents/plugins/plugins/writing-toolkit/skills/weekly-report/agents/openai.yaml +3 -0
  18. package/.agents/skills/.gitkeep +1 -0
  19. package/.agents/skills/wanghong-handwritten-ppt/SKILL.md +172 -0
  20. package/.agents/skills/wanghong-handwritten-ppt/assets/HTML-PPT-LICENSE +21 -0
  21. package/.agents/skills/wanghong-handwritten-ppt/assets/NEAT-ANNOTATIONS-LICENSE +21 -0
  22. package/.agents/skills/wanghong-handwritten-ppt/assets/animations.css +138 -0
  23. package/.agents/skills/wanghong-handwritten-ppt/assets/base.css +150 -0
  24. package/.agents/skills/wanghong-handwritten-ppt/assets/deepseek-example.css +893 -0
  25. package/.agents/skills/wanghong-handwritten-ppt/assets/neat-annotations.css +152 -0
  26. package/.agents/skills/wanghong-handwritten-ppt/assets/preview-cover.png +0 -0
  27. package/.agents/skills/wanghong-handwritten-ppt/assets/preview-diagram.png +0 -0
  28. package/.agents/skills/wanghong-handwritten-ppt/assets/preview-summary.png +0 -0
  29. package/.agents/skills/wanghong-handwritten-ppt/assets/preview-table.png +0 -0
  30. package/.agents/skills/wanghong-handwritten-ppt/assets/runtime.js +960 -0
  31. package/.agents/skills/wanghong-handwritten-ppt/assets/template.css +198 -0
  32. package/.agents/skills/wanghong-handwritten-ppt/evals/evals.json +20 -0
  33. package/.agents/skills/wanghong-handwritten-ppt/examples/deepseek-v4-flash/index.html +414 -0
  34. package/.agents/skills/wanghong-handwritten-ppt/examples/deepseek-v4-flash/style.css +897 -0
  35. package/.agents/skills/wanghong-handwritten-ppt/references/prompts.md +170 -0
  36. package/.agents/skills/wanghong-handwritten-ppt/references/style-guide.md +133 -0
  37. package/.agents/skills/wanghong-handwritten-ppt/scripts/check_deck.py +41 -0
  38. package/.agents/skills/wanghong-handwritten-ppt/scripts/prepare_render_html.py +137 -0
  39. package/.agents/skills/wanghong-handwritten-ppt/scripts/render.sh +110 -0
  40. package/.agents/skills/wanghong-handwritten-ppt/templates/deck.html +94 -0
  41. package/.agents/skills/wanghong-handwritten-ppt/tests/test_font_contract.py +73 -0
  42. package/LICENSE +21 -0
  43. package/README.md +186 -0
  44. package/backend/adapters/README.md +77 -0
  45. package/backend/adapters/anthropic.ts +115 -0
  46. package/backend/adapters/base.ts +47 -0
  47. package/backend/adapters/index.ts +41 -0
  48. package/backend/adapters/lmstudio.ts +7 -0
  49. package/backend/adapters/moonshot.ts +7 -0
  50. package/backend/adapters/ollama.ts +7 -0
  51. package/backend/adapters/openai-compatible.ts +51 -0
  52. package/backend/adapters/openai.ts +7 -0
  53. package/backend/adapters/qwen.ts +112 -0
  54. package/backend/adapters/wenxin.ts +136 -0
  55. package/backend/adapters/zhipu.ts +93 -0
  56. package/backend/dist/adapters/anthropic.js +130 -0
  57. package/backend/dist/adapters/anthropic.js.map +1 -0
  58. package/backend/dist/adapters/base.js +38 -0
  59. package/backend/dist/adapters/base.js.map +1 -0
  60. package/backend/dist/adapters/index.js +39 -0
  61. package/backend/dist/adapters/index.js.map +1 -0
  62. package/backend/dist/adapters/lmstudio.js +8 -0
  63. package/backend/dist/adapters/lmstudio.js.map +1 -0
  64. package/backend/dist/adapters/moonshot.js +8 -0
  65. package/backend/dist/adapters/moonshot.js.map +1 -0
  66. package/backend/dist/adapters/ollama.js +8 -0
  67. package/backend/dist/adapters/ollama.js.map +1 -0
  68. package/backend/dist/adapters/openai-compatible.js +47 -0
  69. package/backend/dist/adapters/openai-compatible.js.map +1 -0
  70. package/backend/dist/adapters/openai.js +8 -0
  71. package/backend/dist/adapters/openai.js.map +1 -0
  72. package/backend/dist/adapters/qwen.js +103 -0
  73. package/backend/dist/adapters/qwen.js.map +1 -0
  74. package/backend/dist/adapters/wenxin.js +114 -0
  75. package/backend/dist/adapters/wenxin.js.map +1 -0
  76. package/backend/dist/adapters/zhipu.js +86 -0
  77. package/backend/dist/adapters/zhipu.js.map +1 -0
  78. package/backend/dist/extensions/demo-mcp-server.js +75 -0
  79. package/backend/dist/extensions/demo-mcp-server.js.map +1 -0
  80. package/backend/dist/extensions/importer.js +828 -0
  81. package/backend/dist/extensions/importer.js.map +1 -0
  82. package/backend/dist/extensions/manager.js +1154 -0
  83. package/backend/dist/extensions/manager.js.map +1 -0
  84. package/backend/dist/lib/catalog.js +30 -0
  85. package/backend/dist/lib/catalog.js.map +1 -0
  86. package/backend/dist/lib/context.js +59 -0
  87. package/backend/dist/lib/context.js.map +1 -0
  88. package/backend/dist/lib/errors.js +75 -0
  89. package/backend/dist/lib/errors.js.map +1 -0
  90. package/backend/dist/lib/provider-store.js +116 -0
  91. package/backend/dist/lib/provider-store.js.map +1 -0
  92. package/backend/dist/lib/redact.js +11 -0
  93. package/backend/dist/lib/redact.js.map +1 -0
  94. package/backend/dist/lib/runtime-store.js +28 -0
  95. package/backend/dist/lib/runtime-store.js.map +1 -0
  96. package/backend/dist/lib/sqlite-store.js +218 -0
  97. package/backend/dist/lib/sqlite-store.js.map +1 -0
  98. package/backend/dist/lib/sse.js +46 -0
  99. package/backend/dist/lib/sse.js.map +1 -0
  100. package/backend/dist/lib/ssrf.js +131 -0
  101. package/backend/dist/lib/ssrf.js.map +1 -0
  102. package/backend/dist/lib/store.js +84 -0
  103. package/backend/dist/lib/store.js.map +1 -0
  104. package/backend/dist/lib/usage.js +219 -0
  105. package/backend/dist/lib/usage.js.map +1 -0
  106. package/backend/dist/lib/util.js +38 -0
  107. package/backend/dist/lib/util.js.map +1 -0
  108. package/backend/dist/lib/workspace-store.js +152 -0
  109. package/backend/dist/lib/workspace-store.js.map +1 -0
  110. package/backend/dist/mcp.js +429 -0
  111. package/backend/dist/mcp.js.map +1 -0
  112. package/backend/dist/routes/agents.js +51 -0
  113. package/backend/dist/routes/agents.js.map +1 -0
  114. package/backend/dist/routes/assistants.js +32 -0
  115. package/backend/dist/routes/assistants.js.map +1 -0
  116. package/backend/dist/routes/chat.js +697 -0
  117. package/backend/dist/routes/chat.js.map +1 -0
  118. package/backend/dist/routes/control-plane.js +200 -0
  119. package/backend/dist/routes/control-plane.js.map +1 -0
  120. package/backend/dist/routes/conversations.js +66 -0
  121. package/backend/dist/routes/conversations.js.map +1 -0
  122. package/backend/dist/routes/extensions-import.js +22 -0
  123. package/backend/dist/routes/extensions-import.js.map +1 -0
  124. package/backend/dist/routes/import.js +85 -0
  125. package/backend/dist/routes/import.js.map +1 -0
  126. package/backend/dist/routes/mcp-servers.js +56 -0
  127. package/backend/dist/routes/mcp-servers.js.map +1 -0
  128. package/backend/dist/routes/meta.js +37 -0
  129. package/backend/dist/routes/meta.js.map +1 -0
  130. package/backend/dist/routes/mock.js +28 -0
  131. package/backend/dist/routes/mock.js.map +1 -0
  132. package/backend/dist/routes/plugins.js +41 -0
  133. package/backend/dist/routes/plugins.js.map +1 -0
  134. package/backend/dist/routes/providers.js +197 -0
  135. package/backend/dist/routes/providers.js.map +1 -0
  136. package/backend/dist/routes/runs.js +57 -0
  137. package/backend/dist/routes/runs.js.map +1 -0
  138. package/backend/dist/routes/skills.js +73 -0
  139. package/backend/dist/routes/skills.js.map +1 -0
  140. package/backend/dist/routes/tools.js +40 -0
  141. package/backend/dist/routes/tools.js.map +1 -0
  142. package/backend/dist/routes/usage.js +14 -0
  143. package/backend/dist/routes/usage.js.map +1 -0
  144. package/backend/dist/routes/workspaces.js +158 -0
  145. package/backend/dist/routes/workspaces.js.map +1 -0
  146. package/backend/dist/runtime/agent.js +518 -0
  147. package/backend/dist/runtime/agent.js.map +1 -0
  148. package/backend/dist/server.js +103 -0
  149. package/backend/dist/server.js.map +1 -0
  150. package/backend/dist/types.js +3 -0
  151. package/backend/dist/types.js.map +1 -0
  152. package/backend/extensions/demo-mcp-server.js +70 -0
  153. package/backend/extensions/importer.ts +709 -0
  154. package/backend/extensions/manager.ts +1055 -0
  155. package/backend/lib/catalog.ts +28 -0
  156. package/backend/lib/context.ts +61 -0
  157. package/backend/lib/errors.ts +78 -0
  158. package/backend/lib/provider-store.ts +116 -0
  159. package/backend/lib/redact.ts +9 -0
  160. package/backend/lib/runtime-store.ts +38 -0
  161. package/backend/lib/sqlite-store.ts +220 -0
  162. package/backend/lib/sse.ts +42 -0
  163. package/backend/lib/ssrf.ts +120 -0
  164. package/backend/lib/store.ts +79 -0
  165. package/backend/lib/usage.ts +212 -0
  166. package/backend/lib/util.ts +31 -0
  167. package/backend/lib/workspace-store.ts +150 -0
  168. package/backend/mcp.ts +417 -0
  169. package/backend/package.json +34 -0
  170. package/backend/prompts.json +82 -0
  171. package/backend/routes/agents.ts +55 -0
  172. package/backend/routes/assistants.ts +30 -0
  173. package/backend/routes/chat.ts +660 -0
  174. package/backend/routes/control-plane.ts +165 -0
  175. package/backend/routes/conversations.ts +62 -0
  176. package/backend/routes/extensions-import.ts +15 -0
  177. package/backend/routes/import.ts +72 -0
  178. package/backend/routes/mcp-servers.ts +36 -0
  179. package/backend/routes/meta.ts +38 -0
  180. package/backend/routes/mock.ts +25 -0
  181. package/backend/routes/plugins.ts +27 -0
  182. package/backend/routes/providers.ts +166 -0
  183. package/backend/routes/runs.ts +55 -0
  184. package/backend/routes/skills.ts +52 -0
  185. package/backend/routes/tools.ts +40 -0
  186. package/backend/routes/usage.ts +13 -0
  187. package/backend/routes/workspaces.ts +124 -0
  188. package/backend/runtime/agent.ts +474 -0
  189. package/backend/server.ts +106 -0
  190. package/backend/types.ts +126 -0
  191. package/bin/multichat.mjs +535 -0
  192. package/frontend/dist/assets/agentPicker-C39VzmYa.js +13 -0
  193. package/frontend/dist/assets/compare-BpaGD32J.js +1 -0
  194. package/frontend/dist/assets/conversations-COuLmg99.js +7 -0
  195. package/frontend/dist/assets/data-ZH7Dux1K.js +1 -0
  196. package/frontend/dist/assets/extensionImport-CnTWKtWm.js +28 -0
  197. package/frontend/dist/assets/importExport-B3mB-9zs.js +70 -0
  198. package/frontend/dist/assets/index-CASVCOUO.js +34 -0
  199. package/frontend/dist/assets/index-DSvbjNLc.css +1 -0
  200. package/frontend/dist/assets/init-ZPK483vB.js +1 -0
  201. package/frontend/dist/assets/jsx-runtime-ZSsbr0oW.js +9 -0
  202. package/frontend/dist/assets/modal-CgHocv3n.js +2 -0
  203. package/frontend/dist/assets/modelPicker-EUmmVnbj.js +14 -0
  204. package/frontend/dist/assets/pluginsUI-B6jGUQkx.js +16 -0
  205. package/frontend/dist/assets/render-DgAg1K8A.js +1 -0
  206. package/frontend/dist/assets/send-BJhZ_5YA.js +14 -0
  207. package/frontend/dist/assets/settings-DdnYwZl-.js +2 -0
  208. package/frontend/dist/assets/shell-DFV2khQl.js +1 -0
  209. package/frontend/dist/assets/workbench-BY2J4vWC.js +41 -0
  210. package/frontend/dist/icon-maskable.svg +4 -0
  211. package/frontend/dist/icon.svg +4 -0
  212. package/frontend/dist/index.html +21 -0
  213. package/frontend/dist/manifest.webmanifest +26 -0
  214. package/package.json +58 -0
@@ -0,0 +1,1055 @@
1
+ 'use strict';
2
+
3
+ // Standard extension registry for Agent Skills, MCP servers and Codex plugins.
4
+ //
5
+ // The three concepts intentionally remain separate:
6
+ // - Skill: a directory whose entrypoint is SKILL.md.
7
+ // - MCP server: an independently configured connection discovered at runtime.
8
+ // - Plugin: a package rooted at .codex-plugin/plugin.json which may bundle
9
+ // skills, MCP config and other host-specific components.
10
+ //
11
+ // Built-in repository assets are read from .agents/. User-managed assets live
12
+ // below DATA_DIR/extensions so installations persist across app restarts.
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+ const { spawnSync } = require('child_process');
18
+ const ctx = require('../lib/context');
19
+ const { AppError } = require('../lib/errors');
20
+ import type { JsonRecord, McpServerRecord, RequestOptions } from '../types';
21
+
22
+ interface ExtensionState {
23
+ skills: Record<string, JsonRecord>;
24
+ plugins: Record<string, JsonRecord>;
25
+ mcp: Record<string, JsonRecord>;
26
+ }
27
+
28
+ const PROJECT_ROOT = path.resolve(process.env.MULTICHAT_PROJECT_ROOT || path.join(ctx.BACKEND_ROOT, '..'));
29
+ const MANAGED_ROOT = path.join(ctx.DATA_DIR, 'extensions');
30
+ const MANAGED_SKILLS_ROOT = path.join(MANAGED_ROOT, 'skills');
31
+ const MANAGED_PLUGINS_ROOT = path.join(MANAGED_ROOT, 'plugins');
32
+ const MANAGED_BUILTIN_ROOT = path.join(MANAGED_ROOT, 'builtin');
33
+ const DEMO_MCP_SOURCE = path.join(__dirname, 'demo-mcp-server.js');
34
+ const DEMO_MCP_ENTRY = path.join(MANAGED_BUILTIN_ROOT, 'demo-mcp-server.js');
35
+ const REPO_SKILLS_ROOT = path.join(PROJECT_ROOT, '.agents', 'skills');
36
+ const REPO_MARKETPLACE = path.join(PROJECT_ROOT, '.agents', 'plugins', 'marketplace.json');
37
+ const USER_SKILLS_ROOT = path.join(os.homedir(), '.agents', 'skills');
38
+ const MANAGED_MARKETPLACE = path.join(MANAGED_PLUGINS_ROOT, 'marketplace.json');
39
+ const CODEX_CONFIG = path.join(PROJECT_ROOT, '.codex', 'config.toml');
40
+ const STATE_FILE = 'extensions_state.json';
41
+ const MCP_FILE = 'mcp_servers.json';
42
+ const MANAGED_TOML_START = '# >>> MultiChat managed MCP servers';
43
+ const MANAGED_TOML_END = '# <<< MultiChat managed MCP servers';
44
+ const toolCache = new Map();
45
+
46
+ function readJson<T>(file: string, fallback: T): T {
47
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
48
+ catch { return fallback; }
49
+ }
50
+
51
+ function jsonClone(value) { return JSON.parse(JSON.stringify(value)); }
52
+
53
+ function state(): ExtensionState {
54
+ const value = ctx.store.read(STATE_FILE, {}) as Partial<ExtensionState>;
55
+ value.skills ||= {};
56
+ value.plugins ||= {};
57
+ value.mcp ||= {};
58
+ return value as ExtensionState;
59
+ }
60
+
61
+ function saveState(value) { ctx.store.write(STATE_FILE, value); }
62
+
63
+ function slug(value, label = 'id') {
64
+ const result = String(value || '').trim().toLowerCase()
65
+ .replace(/[^a-z0-9]+/g, '-')
66
+ .replace(/^-+|-+$/g, '')
67
+ .replace(/-{2,}/g, '-');
68
+ if (!result || result.length > 64) throw new AppError('INVALID_PACKAGE', `${label} 必须是 1-64 位小写字母、数字或连字符`);
69
+ return result;
70
+ }
71
+
72
+ function isInside(root, candidate) {
73
+ const rr = path.resolve(root);
74
+ const cc = path.resolve(candidate);
75
+ return cc === rr || cc.startsWith(rr + path.sep);
76
+ }
77
+
78
+ function yamlScalar(raw) {
79
+ let value = String(raw || '').trim();
80
+ if (!value) return '';
81
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
82
+ if (value[0] === '"') {
83
+ try { return JSON.parse(value); } catch { return value.slice(1, -1); }
84
+ }
85
+ return value.slice(1, -1).replace(/''/g, "'");
86
+ }
87
+ value = value.replace(/\s+#.*$/, '').trim();
88
+ return value;
89
+ }
90
+
91
+ function parseSkillMarkdown(contents: string, file: string) {
92
+ const text = String(contents || '').replace(/^\uFEFF/, '');
93
+ const match = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)([\s\S]*)$/.exec(text);
94
+ if (!match) throw new AppError('INVALID_PACKAGE', `SKILL.md 缺少 YAML frontmatter:${file}`);
95
+ const metadata: JsonRecord = {};
96
+ const lines = match[1].split(/\r?\n/);
97
+ for (let i = 0; i < lines.length; i += 1) {
98
+ const row = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(lines[i]);
99
+ if (!row) continue;
100
+ if (/^[>|][+-]?$/.test(row[2])) {
101
+ const parts = [];
102
+ while (i + 1 < lines.length && /^\s+/.test(lines[i + 1])) parts.push(lines[++i].trim());
103
+ metadata[row[1]] = parts.join(row[2].startsWith('>') ? ' ' : '\n');
104
+ } else metadata[row[1]] = yamlScalar(row[2]);
105
+ }
106
+ if (!metadata.name || !metadata.description) {
107
+ throw new AppError('INVALID_PACKAGE', `SKILL.md 必须包含 name 与 description:${file}`);
108
+ }
109
+ return { metadata, instructions: match[2].trim() };
110
+ }
111
+
112
+ function skillMetadataId(metadata: JsonRecord) {
113
+ const raw = metadata && metadata.metadata;
114
+ if (raw && typeof raw === 'object') {
115
+ return raw['multichat-id'] || raw.multichatId || metadata.name;
116
+ }
117
+ if (typeof raw === 'string' && raw.trim()) {
118
+ try {
119
+ const parsed = JSON.parse(raw);
120
+ return parsed['multichat-id'] || parsed.multichatId || metadata.name;
121
+ } catch {
122
+ // `metadata` is an optional interoperability field. Invalid or
123
+ // non-JSON metadata must not prevent an otherwise valid Skill loading.
124
+ }
125
+ }
126
+ return metadata.name;
127
+ }
128
+
129
+ function readSkill(skillDir, source) {
130
+ const file = path.join(skillDir, 'SKILL.md');
131
+ if (!fs.existsSync(file)) return null;
132
+ try {
133
+ const parsed = parseSkillMarkdown(fs.readFileSync(file, 'utf8'), file);
134
+ const id = String(skillMetadataId(parsed.metadata)).trim();
135
+ const st = fs.statSync(file);
136
+ const currentState = state();
137
+ const stateKey = path.resolve(file).toLowerCase();
138
+ const saved = currentState.skills[stateKey] || {};
139
+ return {
140
+ key: `skill_${crypto.createHash('sha256').update(path.resolve(file).toLowerCase()).digest('hex').slice(0, 16)}`,
141
+ id,
142
+ name: String(parsed.metadata.name),
143
+ description: String(parsed.metadata.description),
144
+ instructions: parsed.instructions,
145
+ type: 'workflow',
146
+ enabled: saved.enabled !== false && source.enabled !== false,
147
+ managed: source.kind === 'managed' || source.kind === 'repo' || source.kind === 'plugin',
148
+ scope: source.scope,
149
+ source,
150
+ path: file,
151
+ directory: skillDir,
152
+ modifiedAt: st.mtime.toISOString(),
153
+ resources: ['scripts', 'references', 'assets'].filter(name => fs.existsSync(path.join(skillDir, name))),
154
+ targets: source.kind === 'plugin' ? ['multichat'] : ['multichat', 'codex'],
155
+ };
156
+ } catch (error) {
157
+ return {
158
+ key: `skill_${crypto.createHash('sha256').update(path.resolve(file).toLowerCase()).digest('hex').slice(0, 16)}`,
159
+ id: path.basename(skillDir),
160
+ name: path.basename(skillDir),
161
+ description: error.message,
162
+ instructions: '',
163
+ type: 'workflow',
164
+ enabled: false,
165
+ managed: source.kind !== 'user',
166
+ scope: source.scope,
167
+ source,
168
+ path: file,
169
+ directory: skillDir,
170
+ invalid: true,
171
+ error: error.message,
172
+ resources: [],
173
+ targets: source.kind === 'plugin' ? ['multichat'] : ['multichat', 'codex'],
174
+ };
175
+ }
176
+ }
177
+
178
+ function scanSkillRoot(root, source) {
179
+ if (!root || !fs.existsSync(root)) return [];
180
+ const out = [];
181
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
182
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
183
+ const item = readSkill(path.join(root, entry.name), source);
184
+ if (item) out.push(item);
185
+ }
186
+ return out;
187
+ }
188
+
189
+ function marketplaceSources() {
190
+ return [
191
+ { file: REPO_MARKETPLACE, kind: 'repo', scope: 'project' },
192
+ { file: MANAGED_MARKETPLACE, kind: 'managed', scope: 'global' },
193
+ ].filter(item => fs.existsSync(item.file));
194
+ }
195
+
196
+ function resolveMarketplaceEntry(source, entry) {
197
+ if (!entry || typeof entry !== 'object' || !entry.name || !entry.source) return null;
198
+ if (entry.source.source !== 'local' || typeof entry.source.path !== 'string') return null;
199
+ const root = path.dirname(source.file);
200
+ const pluginDir = path.resolve(root, entry.source.path);
201
+ if (!isInside(root, pluginDir)) return null;
202
+ const manifestFile = path.join(pluginDir, '.codex-plugin', 'plugin.json');
203
+ if (!fs.existsSync(manifestFile)) return null;
204
+ const manifest = readJson(manifestFile, null);
205
+ if (!manifest || typeof manifest !== 'object' || !manifest.name) return null;
206
+ return { source, entry, pluginDir, manifestFile, manifest };
207
+ }
208
+
209
+ function resolvePluginComponentPath(pluginDir, configured, fallback) {
210
+ if (configured === undefined || configured === null) return path.join(pluginDir, fallback);
211
+ if (typeof configured !== 'string' || !configured.startsWith('./')) return null;
212
+ const resolved = path.resolve(pluginDir, configured);
213
+ return isInside(pluginDir, resolved) ? resolved : null;
214
+ }
215
+
216
+ function mcpServerMap(value: unknown): JsonRecord {
217
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
218
+ const object = value as JsonRecord;
219
+ if (object.mcpServers && typeof object.mcpServers === 'object') return object.mcpServers;
220
+ if (object.mcp_servers && typeof object.mcp_servers === 'object') return object.mcp_servers;
221
+ const entries = Object.entries(object).filter(([, item]) => item && typeof item === 'object' && !Array.isArray(item) && (item.command || item.url || item.type));
222
+ return Object.fromEntries(entries);
223
+ }
224
+
225
+ function pluginRecords() {
226
+ const out = [];
227
+ const seen = new Set();
228
+ const currentState = state();
229
+ for (const source of marketplaceSources()) {
230
+ const marketplace = readJson<JsonRecord>(source.file, {});
231
+ for (const entry of Array.isArray(marketplace.plugins) ? marketplace.plugins : []) {
232
+ const resolved = resolveMarketplaceEntry(source, entry);
233
+ if (!resolved) continue;
234
+ const key = `${source.file}:${resolved.manifest.name}`;
235
+ if (seen.has(key)) continue;
236
+ seen.add(key);
237
+ const { manifest, pluginDir, manifestFile } = resolved;
238
+ const pluginState = currentState.plugins[key] || {};
239
+ const publicKey = `plugin_${crypto.createHash('sha256').update(key.toLowerCase()).digest('hex').slice(0, 16)}`;
240
+ const skillsPath = resolvePluginComponentPath(pluginDir, manifest.skills, 'skills');
241
+ const mcpPath = typeof manifest.mcpServers === 'object'
242
+ ? null
243
+ : resolvePluginComponentPath(pluginDir, manifest.mcpServers, '.mcp.json');
244
+ const countDirs = name => {
245
+ const dir = path.join(pluginDir, name);
246
+ if (!fs.existsSync(dir)) return 0;
247
+ return fs.readdirSync(dir, { withFileTypes: true }).filter(item => item.isDirectory() && !item.name.startsWith('.')).length;
248
+ };
249
+ const mcpJson = mcpPath && fs.existsSync(mcpPath) ? readJson(mcpPath, {}) : {};
250
+ const serverMap = typeof manifest.mcpServers === 'object' ? mcpServerMap(manifest.mcpServers) : mcpServerMap(mcpJson);
251
+ const components = {
252
+ skills: scanSkillRoot(skillsPath, { kind: 'plugin', scope: source.scope, plugin: manifest.name, root: pluginDir }).length,
253
+ mcpServers: serverMap && typeof serverMap === 'object' ? Object.keys(serverMap).length : 0,
254
+ agents: countDirs('agents'),
255
+ commands: countDirs('commands'),
256
+ hooks: fs.existsSync(path.join(pluginDir, 'hooks', 'hooks.json')) || fs.existsSync(path.join(pluginDir, 'hooks.json')) ? 1 : 0,
257
+ };
258
+ out.push({
259
+ key: publicKey,
260
+ id: String(manifest.name),
261
+ name: manifest.interface?.displayName || manifest.name,
262
+ version: manifest.version || '0.0.0',
263
+ description: manifest.interface?.shortDescription || manifest.description || '',
264
+ author: manifest.author?.name || manifest.author || '',
265
+ category: resolved.entry.category || manifest.interface?.category || 'Other',
266
+ enabled: pluginState.enabled !== false,
267
+ installed: pluginState.imported === true,
268
+ imported: pluginState.imported === true,
269
+ removable: pluginState.imported === true && source.kind === 'repo',
270
+ availableInProject: true,
271
+ managed: true,
272
+ source: { kind: source.kind, scope: source.scope, marketplace: source.file, path: pluginDir },
273
+ path: pluginDir,
274
+ manifestPath: manifestFile,
275
+ components,
276
+ policy: resolved.entry.policy || {},
277
+ invalid: !skillsPath || (typeof manifest.mcpServers === 'string' && !mcpPath),
278
+ _key: key,
279
+ _manifest: manifest,
280
+ _skillsPath: skillsPath,
281
+ _mcpPath: mcpPath,
282
+ });
283
+ }
284
+ }
285
+ return out;
286
+ }
287
+
288
+ function listPlugins() {
289
+ return pluginRecords().map(({ _key, _manifest, _skillsPath, _mcpPath, ...item }) => item);
290
+ }
291
+
292
+ function listSkills() {
293
+ const out = [];
294
+ out.push(...scanSkillRoot(MANAGED_SKILLS_ROOT, { kind: 'managed', scope: 'global', root: MANAGED_SKILLS_ROOT }));
295
+ out.push(...scanSkillRoot(REPO_SKILLS_ROOT, { kind: 'repo', scope: 'project', root: REPO_SKILLS_ROOT }));
296
+ if (path.resolve(USER_SKILLS_ROOT) !== path.resolve(MANAGED_SKILLS_ROOT)) {
297
+ out.push(...scanSkillRoot(USER_SKILLS_ROOT, { kind: 'user', scope: 'user', root: USER_SKILLS_ROOT }));
298
+ }
299
+ for (const plugin of pluginRecords()) {
300
+ out.push(...scanSkillRoot(plugin._skillsPath, {
301
+ kind: 'plugin',
302
+ scope: plugin.source.scope,
303
+ plugin: plugin.id,
304
+ root: plugin.path,
305
+ enabled: plugin.enabled,
306
+ }));
307
+ }
308
+ const seen = new Set();
309
+ return out.filter(item => {
310
+ const key = `${item.source.kind}:${item.source.plugin || ''}:${item.path}`.toLowerCase();
311
+ if (seen.has(key)) return false;
312
+ seen.add(key);
313
+ return true;
314
+ }).sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
315
+ }
316
+
317
+ function findSkill(id) {
318
+ const matches = listSkills().filter(item => item.key === id || item.id === id || item.name === id);
319
+ if (!matches.length) throw new AppError('NOT_FOUND', 'skill not found');
320
+ if (matches.length > 1 && !matches.some(item => item.key === id)) {
321
+ throw new AppError('CONFLICT', `Skill 标识 ${id} 对应多个来源,请使用 source-qualified key`);
322
+ }
323
+ const exact = matches.find(item => item.key === id);
324
+ if (exact) return exact;
325
+ return matches[0];
326
+ }
327
+
328
+ function formatSkill({ id, name, description, instructions }) {
329
+ const metadata = JSON.stringify({ 'multichat-id': id });
330
+ return `---\nname: ${JSON.stringify(name)}\ndescription: ${JSON.stringify(description)}\nmetadata: ${metadata}\n---\n\n${String(instructions || '').trim()}\n`;
331
+ }
332
+
333
+ function createSkill(input) {
334
+ const name = slug(input.name, 'skill name');
335
+ const id = slug(input.id || name, 'skill id');
336
+ const description = String(input.description || '').trim();
337
+ const instructions = String(input.instructions || '').trim();
338
+ if (!description || !instructions) throw new AppError('INVALID_PACKAGE', 'description 与 instructions 不能为空');
339
+ const dir = path.join(REPO_SKILLS_ROOT, name);
340
+ if (fs.existsSync(dir)) throw new AppError('CONFLICT', 'skill already exists');
341
+ fs.mkdirSync(dir, { recursive: true });
342
+ fs.writeFileSync(path.join(dir, 'SKILL.md'), formatSkill({ id, name, description, instructions }), 'utf8');
343
+ return readSkill(dir, { kind: 'repo', scope: 'project', root: REPO_SKILLS_ROOT });
344
+ }
345
+
346
+ function updateSkill(id, input) {
347
+ const item = findSkill(id);
348
+ if (!['managed', 'repo'].includes(item.source.kind)) throw new AppError('PERMISSION_DENIED', '此 Skill 由用户目录或插件提供,请在来源处修改');
349
+ const name = slug(input.name || item.name, 'skill name');
350
+ const description = String(input.description ?? item.description).trim();
351
+ const instructions = String(input.instructions ?? item.instructions).trim();
352
+ if (!name || !description || !instructions) throw new AppError('INVALID_PACKAGE', 'name、description 与 instructions 不能为空');
353
+ fs.writeFileSync(item.path, formatSkill({ id: item.id, name, description, instructions }), 'utf8');
354
+ return readSkill(item.directory, item.source);
355
+ }
356
+
357
+ function setSkillEnabled(id, enabled) {
358
+ const item = findSkill(id);
359
+ const value = state();
360
+ const key = path.resolve(item.path).toLowerCase();
361
+ value.skills[key] = { ...(value.skills[key] || {}), enabled: !!enabled };
362
+ saveState(value);
363
+ return { ok: true, enabled: !!enabled };
364
+ }
365
+
366
+ function deleteSkill(id) {
367
+ const item = findSkill(id);
368
+ if (!['managed', 'repo'].includes(item.source.kind)) throw new AppError('PERMISSION_DENIED', '只能删除 MultiChat 托管或当前项目内的 Skill');
369
+ if (!isInside(item.source.root, item.directory)) throw new AppError('PERMISSION_DENIED', 'skill path escaped managed root');
370
+ fs.rmSync(item.directory, { recursive: true, force: false });
371
+ const agents = ctx.store.read(ctx.AGENT_FILE, []);
372
+ let changed = false;
373
+ for (const agent of agents) {
374
+ const next = (agent.skillIds || []).filter(skillId => skillId !== item.id);
375
+ const nextRefs = (agent.skillRefs || []).filter(skillRef => skillRef !== item.key && skillRef !== item.id);
376
+ if (next.length !== (agent.skillIds || []).length) { agent.skillIds = next; changed = true; }
377
+ if (nextRefs.length !== (agent.skillRefs || []).length) { agent.skillRefs = nextRefs; changed = true; }
378
+ }
379
+ if (changed) ctx.store.write(ctx.AGENT_FILE, agents);
380
+ return { ok: true };
381
+ }
382
+
383
+ function gitDiff(target) {
384
+ const rel = path.relative(PROJECT_ROOT, target);
385
+ if (rel.startsWith('..') || path.isAbsolute(rel)) return { tracked: false, status: 'external', diff: '' };
386
+ const status = spawnSync('git', ['status', '--short', '--', rel], { cwd: PROJECT_ROOT, encoding: 'utf8', windowsHide: true });
387
+ if (status.error || status.status !== 0) {
388
+ const reason = status.error?.message || String(status.stderr || '').trim() || 'Git status unavailable';
389
+ return { tracked: false, status: 'unavailable', diff: '', error: reason.slice(0, 500) };
390
+ }
391
+ const diffResult = spawnSync('git', ['diff', '--no-ext-diff', '--', rel], { cwd: PROJECT_ROOT, encoding: 'utf8', windowsHide: true });
392
+ if (diffResult.error || diffResult.status !== 0) {
393
+ const reason = diffResult.error?.message || String(diffResult.stderr || '').trim() || 'Git diff unavailable';
394
+ return { tracked: false, status: 'unavailable', diff: '', error: reason.slice(0, 500) };
395
+ }
396
+ let diff = diffResult.stdout || '';
397
+ const statusText = String(status.stdout || '').trim();
398
+ if (!diff && statusText.startsWith('??') && fs.existsSync(target)) {
399
+ const files = [];
400
+ const walk = current => {
401
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
402
+ if (files.length >= 80 || entry.name === 'node_modules' || entry.name === '.git') continue;
403
+ const absolute = path.join(current, entry.name);
404
+ if (entry.isDirectory()) walk(absolute);
405
+ else if (entry.isFile() && fs.statSync(absolute).size <= 256 * 1024) files.push(absolute);
406
+ }
407
+ };
408
+ if (fs.statSync(target).isFile()) files.push(target); else walk(target);
409
+ const chunks = [];
410
+ let remaining = 120_000;
411
+ for (const file of files) {
412
+ if (remaining <= 0) break;
413
+ let contents;
414
+ try { contents = fs.readFileSync(file, 'utf8'); } catch { continue; }
415
+ if (contents.includes('\0')) continue;
416
+ const header = `+++ ${path.relative(PROJECT_ROOT, file).replace(/\\/g, '/')}`;
417
+ const added = contents.split(/\r?\n/).map(line => `+${line}`).join('\n');
418
+ const chunk = `${header}\n${added}`.slice(0, remaining);
419
+ chunks.push(chunk); remaining -= chunk.length;
420
+ }
421
+ diff = chunks.join('\n\n');
422
+ }
423
+ return { tracked: !statusText.startsWith('??'), status: statusText || 'clean', diff };
424
+ }
425
+
426
+ function pluginDiff(id) {
427
+ const matches = pluginRecords().filter(plugin => plugin.key === id || plugin.id === id);
428
+ if (matches.length > 1 && !matches.some(plugin => plugin.key === id)) throw new AppError('CONFLICT', `插件 ${id} 来自多个 marketplace,请使用 source-qualified key`);
429
+ const item = matches.find(plugin => plugin.key === id) || matches[0];
430
+ if (!item) throw new AppError('NOT_FOUND', 'plugin not found');
431
+ return gitDiff(item.path);
432
+ }
433
+
434
+ function setPluginEnabled(id, enabled) {
435
+ const matches = pluginRecords().filter(plugin => plugin.key === id || plugin.id === id);
436
+ if (matches.length > 1 && !matches.some(plugin => plugin.key === id)) throw new AppError('CONFLICT', `插件 ${id} 来自多个 marketplace,请使用 source-qualified key`);
437
+ const item = matches.find(plugin => plugin.key === id) || matches[0];
438
+ if (!item) throw new AppError('NOT_FOUND', 'plugin not found');
439
+ const value = state();
440
+ value.plugins[item._key] = { ...(value.plugins[item._key] || {}), enabled: !!enabled };
441
+ saveState(value);
442
+ ctx.closeAllMcpClients();
443
+ toolCache.clear();
444
+ return { ok: true, enabled: !!enabled };
445
+ }
446
+
447
+ function markPluginImported(id) {
448
+ const matches = pluginRecords().filter(plugin => plugin.key === id || plugin.id === id);
449
+ const item = matches.find(plugin => plugin.key === id) || matches[0];
450
+ if (!item) throw new AppError('NOT_FOUND', 'plugin not found');
451
+ const value = state();
452
+ value.plugins[item._key] = { ...(value.plugins[item._key] || {}), imported: true, enabled: false };
453
+ saveState(value);
454
+ ctx.closeAllMcpClients();
455
+ toolCache.clear();
456
+ return listPlugins().find(plugin => plugin.key === item.key);
457
+ }
458
+
459
+ function writeJsonAtomic(file, value) {
460
+ fs.mkdirSync(path.dirname(file), { recursive: true });
461
+ const temp = `${file}.multichat-${process.pid}-${crypto.randomBytes(5).toString('hex')}.tmp`;
462
+ try {
463
+ fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' });
464
+ fs.renameSync(temp, file);
465
+ } finally {
466
+ if (fs.existsSync(temp)) fs.rmSync(temp, { force: true });
467
+ }
468
+ }
469
+
470
+ function deleteImportedPlugin(id) {
471
+ const matches = pluginRecords().filter(plugin => plugin.key === id || plugin.id === id);
472
+ const item = matches.find(plugin => plugin.key === id) || matches[0];
473
+ if (!item) throw new AppError('NOT_FOUND', 'plugin not found');
474
+ const currentState = state();
475
+ const pluginState = currentState.plugins[item._key] || {};
476
+ const repoPluginRoot = path.join(path.dirname(REPO_MARKETPLACE), 'plugins');
477
+ if (pluginState.imported !== true || item.source.kind !== 'repo' || !isInside(repoPluginRoot, item.path) || path.resolve(item.path) === path.resolve(repoPluginRoot)) {
478
+ throw new AppError('PERMISSION_DENIED', '只能卸载通过 MultiChat 导入的项目插件');
479
+ }
480
+ const marketplace = readJson(REPO_MARKETPLACE, null);
481
+ if (!marketplace || !Array.isArray(marketplace.plugins)) throw new AppError('INVALID_PACKAGE', 'project marketplace.json is invalid');
482
+ const nextMarketplace = { ...marketplace, plugins: marketplace.plugins.filter(entry => entry?.name !== item.id) };
483
+ const allSkills = listSkills();
484
+ const pluginSkills = allSkills.filter(skill => skill.source?.kind === 'plugin' && isInside(item.path, skill.path));
485
+ const pluginSkillKeys = new Set(pluginSkills.map(skill => skill.key));
486
+ const otherSkillIds = new Set(allSkills.filter(skill => !pluginSkillKeys.has(skill.key)).map(skill => skill.id));
487
+ const removableLegacySkillIds = new Set(pluginSkills.map(skill => skill.id).filter(skillId => !otherSkillIds.has(skillId)));
488
+ const pluginMcpIds = pluginMcpRecords().filter(server => server.source?.pluginKey === item.key).map(server => server.id);
489
+ const nextState = jsonClone(currentState);
490
+ delete nextState.plugins[item._key];
491
+ for (const key of Object.keys(nextState.skills)) {
492
+ if (key.startsWith(path.resolve(item.path).toLowerCase() + path.sep)) delete nextState.skills[key];
493
+ }
494
+ for (const serverId of pluginMcpIds) delete nextState.mcp[serverId];
495
+
496
+ const originalAgents = ctx.store.read(ctx.AGENT_FILE, []);
497
+ const nextAgents = jsonClone(Array.isArray(originalAgents) ? originalAgents : []);
498
+ const removedMcpIds = new Set(pluginMcpIds);
499
+ let agentsChanged = false;
500
+ for (const agent of nextAgents) {
501
+ const currentSkillRefs = Array.isArray(agent.skillRefs) ? agent.skillRefs : [];
502
+ const currentSkillIds = Array.isArray(agent.skillIds) ? agent.skillIds : [];
503
+ const currentMcpIds = Array.isArray(agent.mcpServerIds) ? agent.mcpServerIds : [];
504
+ const nextSkillRefs = currentSkillRefs.filter(ref => !pluginSkillKeys.has(ref) && !removableLegacySkillIds.has(ref));
505
+ const nextSkillIds = currentSkillIds.filter(ref => !pluginSkillKeys.has(ref) && !removableLegacySkillIds.has(ref));
506
+ const nextMcpIds = currentMcpIds.filter(ref => !removedMcpIds.has(ref));
507
+ if (nextSkillRefs.length !== currentSkillRefs.length) { agent.skillRefs = nextSkillRefs; agentsChanged = true; }
508
+ if (nextSkillIds.length !== currentSkillIds.length) { agent.skillIds = nextSkillIds; agentsChanged = true; }
509
+ if (nextMcpIds.length !== currentMcpIds.length) { agent.mcpServerIds = nextMcpIds; agentsChanged = true; }
510
+ }
511
+
512
+ const backup = path.join(repoPluginRoot, `.multichat-remove-${process.pid}-${Date.now()}-${crypto.randomBytes(5).toString('hex')}`);
513
+ fs.renameSync(item.path, backup);
514
+ try {
515
+ writeJsonAtomic(REPO_MARKETPLACE, nextMarketplace);
516
+ saveState(nextState);
517
+ if (agentsChanged) ctx.store.write(ctx.AGENT_FILE, nextAgents);
518
+ } catch (error) {
519
+ const rollbackFailures = [];
520
+ const rollback = (label, action) => {
521
+ try { action(); }
522
+ catch (rollbackError) { rollbackFailures.push(`${label}: ${rollbackError.message}`); }
523
+ };
524
+ rollback('plugin package', () => {
525
+ if (!fs.existsSync(item.path) && fs.existsSync(backup)) fs.renameSync(backup, item.path);
526
+ });
527
+ rollback('marketplace', () => writeJsonAtomic(REPO_MARKETPLACE, marketplace));
528
+ rollback('extension state', () => saveState(currentState));
529
+ if (agentsChanged) rollback('agent references', () => ctx.store.write(ctx.AGENT_FILE, originalAgents));
530
+ if (rollbackFailures.length) error.rollbackFailures = rollbackFailures;
531
+ throw error;
532
+ }
533
+ try { fs.rmSync(backup, { recursive: true, force: true }); }
534
+ catch {
535
+ // All durable registries are committed. A stale hidden backup is safer
536
+ // than reporting failure after a successful transactional uninstall.
537
+ }
538
+ try { ctx.closeAllMcpClients(); } catch { /* uninstall is already committed */ }
539
+ toolCache.clear();
540
+ return { ok: true, id: item.id };
541
+ }
542
+
543
+ function standaloneMcpRecords(): McpServerRecord[] {
544
+ const rows = ctx.store.read(MCP_FILE, []) as unknown;
545
+ return Array.isArray(rows) ? rows : [];
546
+ }
547
+
548
+ function normalizeMcpInput(input: McpServerRecord, previous: McpServerRecord = {}): McpServerRecord {
549
+ const id = slug(input.id || previous.id || input.name, 'server id');
550
+ const transport = (input.transport || previous.transport) === 'http' || input.url ? 'http' : 'stdio';
551
+ const record: McpServerRecord = {
552
+ ...previous,
553
+ id,
554
+ name: String(input.name || previous.name || id).trim(),
555
+ description: String(input.description ?? previous.description ?? '').trim(),
556
+ transport,
557
+ enabled: input.enabled === undefined ? previous.enabled !== false : input.enabled !== false,
558
+ trustLevel: input.trustLevel === undefined ? (previous.trustLevel || 'untrusted') : (input.trustLevel === 'trusted' ? 'trusted' : 'untrusted'),
559
+ allowPrivate: input.allowPrivate === undefined ? previous.allowPrivate === true : input.allowPrivate === true,
560
+ targets: Array.isArray(input.targets) ? input.targets.filter(value => ['multichat', 'codex'].includes(value)) : (previous.targets || ['multichat', 'codex']),
561
+ source: previous.source || input.source || { kind: 'managed', scope: 'project' },
562
+ };
563
+ if (transport === 'stdio') {
564
+ record.command = String(input.command || previous.command || '').trim();
565
+ if (!record.command) throw new AppError('INVALID_PACKAGE', 'STDIO server 必须提供 command');
566
+ record.args = Array.isArray(input.args) ? input.args.map((value, index) => {
567
+ const text = String(value);
568
+ if ((text === '***' || /=\*\*\*$/.test(text)) && previous.args?.[index]) return String(previous.args[index]);
569
+ return text;
570
+ }) : (previous.args || []);
571
+ record.cwd = String(input.cwd ?? previous.cwd ?? '').trim();
572
+ record.env = input.env && typeof input.env === 'object' && !Array.isArray(input.env) ? Object.fromEntries(Object.entries(input.env).map(([key, value]) => [String(key), String(value)])) : (previous.env || {});
573
+ delete record.url;
574
+ delete record.headers;
575
+ delete record.bearerTokenEnvVar;
576
+ } else {
577
+ const requestedUrl = String(input.url || '').trim();
578
+ record.url = requestedUrl.includes('***') && previous.url ? previous.url : String(requestedUrl || previous.url || '').trim();
579
+ if (!/^https?:\/\//i.test(record.url)) throw new AppError('INVALID_PACKAGE', 'HTTP server 必须提供 http(s) URL');
580
+ const parsedUrl = new URL(record.url);
581
+ if (parsedUrl.username || parsedUrl.password) throw new AppError('INVALID_PACKAGE', 'URL 中不能包含用户名或密码,请改用 bearer token 环境变量');
582
+ record.bearerTokenEnvVar = String(input.bearerTokenEnvVar ?? previous.bearerTokenEnvVar ?? '').trim();
583
+ record.headers = input.headers && typeof input.headers === 'object' && !Array.isArray(input.headers) ? Object.fromEntries(Object.entries(input.headers).map(([key, value]) => [String(key), String(value)])) : (previous.headers || {});
584
+ delete record.command;
585
+ delete record.args;
586
+ delete record.cwd;
587
+ delete record.env;
588
+ }
589
+ if ((record.targets || []).includes('codex')) {
590
+ if (record.transport === 'stdio' && redactArgs(record.args || []).some((value, index) => value !== String(record.args[index]))) {
591
+ throw new AppError('INVALID_PACKAGE', '面向 Codex 的 MCP 不能把 token/secret/password 写进 args,请改用环境变量');
592
+ }
593
+ if (record.transport === 'http' && Object.keys(record.headers || {}).length) {
594
+ throw new AppError('INVALID_PACKAGE', '面向 Codex 的 HTTP MCP 不能保存静态 headers,请使用 bearer token 环境变量');
595
+ }
596
+ }
597
+ record.updatedAt = new Date().toISOString();
598
+ return record;
599
+ }
600
+
601
+ function expandEnvironmentTemplate(value) {
602
+ let missing = false;
603
+ const expanded = String(value).replace(/\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
604
+ if (process.env[name] === undefined) {
605
+ missing = true;
606
+ return '';
607
+ }
608
+ return process.env[name];
609
+ });
610
+ return { value: expanded, missing };
611
+ }
612
+
613
+ function expandEnvironmentMap(value: unknown): Record<string, string> {
614
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
615
+ const out: Record<string, string> = {};
616
+ for (const [key, item] of Object.entries(value)) {
617
+ const expanded = expandEnvironmentTemplate(item);
618
+ if (!expanded.missing) out[String(key)] = expanded.value;
619
+ }
620
+ return out;
621
+ }
622
+
623
+ function pluginMcpRecords(): McpServerRecord[] {
624
+ const out: McpServerRecord[] = [];
625
+ const currentState = state();
626
+ for (const plugin of pluginRecords()) {
627
+ let map: JsonRecord = {};
628
+ if (typeof plugin._manifest.mcpServers === 'object') map = mcpServerMap(plugin._manifest.mcpServers);
629
+ else if (plugin._mcpPath && fs.existsSync(plugin._mcpPath)) {
630
+ const json = readJson(plugin._mcpPath, {});
631
+ map = mcpServerMap(json);
632
+ }
633
+ for (const [name, raw] of Object.entries(map || {})) {
634
+ if (!raw || typeof raw !== 'object') continue;
635
+ const id = `${plugin.key}.${name}`;
636
+ const policy = currentState.mcp[id] || {};
637
+ const pluginRootValue = value => String(value).replaceAll('${PLUGIN_ROOT}', plugin.path);
638
+ const pluginMap = value => {
639
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
640
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [String(key), pluginRootValue(item)]));
641
+ };
642
+ const record: McpServerRecord = {
643
+ id,
644
+ name,
645
+ description: `由插件 ${plugin.name} 提供`,
646
+ transport: raw.type === 'http' || raw.url ? 'http' : 'stdio',
647
+ command: raw.command,
648
+ args: Array.isArray(raw.args) ? raw.args.map(value => String(value).replaceAll('${PLUGIN_ROOT}', plugin.path)) : [],
649
+ cwd: String(raw.cwd || plugin.path).replaceAll('${PLUGIN_ROOT}', plugin.path),
650
+ env: pluginMap(raw.env),
651
+ url: raw.url,
652
+ headers: pluginMap(raw.headers || raw.http_headers),
653
+ bearerTokenEnvVar: raw.bearer_token_env_var || raw.bearerTokenEnvVar || '',
654
+ enabled: plugin.enabled && policy.enabled !== false,
655
+ trustLevel: policy.trustLevel === 'trusted' ? 'trusted' : 'untrusted',
656
+ targets: Array.isArray(policy.targets) ? policy.targets.filter(value => value === 'multichat') : ['multichat'],
657
+ allowPrivate: policy.allowPrivate === true,
658
+ source: { kind: 'plugin', scope: plugin.source.scope, plugin: plugin.id, pluginKey: plugin.key, path: plugin._mcpPath || plugin.manifestPath },
659
+ };
660
+ out.push(record);
661
+ }
662
+ }
663
+ return out;
664
+ }
665
+
666
+ function privateMcpRecords(): McpServerRecord[] {
667
+ return [...standaloneMcpRecords(), ...pluginMcpRecords()];
668
+ }
669
+
670
+ function redactUrl(value) {
671
+ if (!value) return value;
672
+ try {
673
+ const url = new URL(value);
674
+ if (url.username) url.username = 'redacted';
675
+ if (url.password) url.password = 'redacted';
676
+ for (const key of [...url.searchParams.keys()]) url.searchParams.set(key, '***');
677
+ return url.toString();
678
+ } catch { return String(value).replace(/([?&][^=]+)=([^&]+)/g, '$1=***'); }
679
+ }
680
+
681
+ function redactArgs(values) {
682
+ const out = [];
683
+ let redactNext = false;
684
+ for (const raw of values || []) {
685
+ const value = String(raw);
686
+ if (redactNext) { out.push('***'); redactNext = false; continue; }
687
+ if (/^--?(?:api[-_]?key|token|secret|password|authorization)$/i.test(value)) {
688
+ out.push(value); redactNext = true; continue;
689
+ }
690
+ out.push(value.replace(/(--?(?:api[-_]?key|token|secret|password|authorization)=).+/i, '$1***'));
691
+ }
692
+ return out;
693
+ }
694
+
695
+ function publicMcp(record: McpServerRecord) {
696
+ const cached = toolCache.get(record.id);
697
+ return {
698
+ id: record.id,
699
+ name: record.name,
700
+ description: record.description || '',
701
+ transport: record.transport,
702
+ command: record.command,
703
+ args: redactArgs(record.args || []),
704
+ cwd: record.cwd || '',
705
+ url: redactUrl(record.url),
706
+ envKeys: Object.keys(record.env || {}),
707
+ headerKeys: Object.keys(record.headers || {}),
708
+ bearerTokenEnvVar: record.bearerTokenEnvVar || '',
709
+ enabled: record.enabled !== false,
710
+ trustLevel: record.trustLevel || 'untrusted',
711
+ allowPrivate: record.allowPrivate === true,
712
+ targets: record.targets || [],
713
+ source: record.source || { kind: 'managed', scope: 'project' },
714
+ tools: cached?.tools || [],
715
+ status: cached?.status || 'unknown',
716
+ error: cached?.error || null,
717
+ instructions: cached?.instructions || '',
718
+ serverInfo: cached?.serverInfo || null,
719
+ lastCheckedAt: cached?.checkedAt || null,
720
+ };
721
+ }
722
+
723
+ function listMcpServers() { return privateMcpRecords().map(publicMcp); }
724
+
725
+ function findPrivateMcp(id) {
726
+ const record = privateMcpRecords().find(item => item.id === id);
727
+ if (!record) throw new AppError('NOT_FOUND', 'MCP server not found');
728
+ return record;
729
+ }
730
+
731
+ function createMcpServer(input) {
732
+ const rows = standaloneMcpRecords();
733
+ const record = normalizeMcpInput(input);
734
+ if (rows.some(item => item.id === record.id) || pluginMcpRecords().some(item => item.id === record.id)) throw new AppError('CONFLICT', 'MCP server already exists');
735
+ rows.push(record);
736
+ ctx.store.write(MCP_FILE, rows);
737
+ syncCodexMcpConfig();
738
+ return publicMcp(record);
739
+ }
740
+
741
+ // Import several standalone servers in one registry write. The importer has
742
+ // already performed package inspection, but this function repeats the runtime
743
+ // normalization and conflict checks so the final write cannot partially apply.
744
+ // Imported servers deliberately remain disabled and untrusted until the user
745
+ // reviews and enables them from the MCP page.
746
+ function importMcpServers(inputs, conflictPolicy = 'reject') {
747
+ if (!Array.isArray(inputs) || !inputs.length || inputs.length > 50) {
748
+ throw new AppError('INVALID_PACKAGE', 'MCP import must contain 1-50 servers');
749
+ }
750
+ const rows = standaloneMcpRecords();
751
+ const pluginIds = new Set(pluginMcpRecords().map(item => item.id));
752
+ const ids = new Set();
753
+ const normalized = inputs.map(input => {
754
+ const record = normalizeMcpInput({
755
+ ...input,
756
+ enabled: false,
757
+ trustLevel: 'untrusted',
758
+ allowPrivate: false,
759
+ source: { kind: 'managed', scope: 'project', imported: true },
760
+ });
761
+ if (ids.has(record.id)) throw new AppError('CONFLICT', `duplicate MCP server id: ${record.id}`);
762
+ ids.add(record.id);
763
+ if (pluginIds.has(record.id)) throw new AppError('PERMISSION_DENIED', `MCP server ${record.id} is provided by a plugin and cannot be replaced`);
764
+ return record;
765
+ });
766
+ const conflicts = normalized.filter(record => rows.some(item => item.id === record.id));
767
+ const protectedConflicts = conflicts.filter(record => rows.find(item => item.id === record.id)?.source?.kind !== 'managed');
768
+ if (protectedConflicts.length) {
769
+ throw new AppError('PERMISSION_DENIED', `protected MCP server cannot be replaced: ${protectedConflicts.map(item => item.id).join(', ')}`);
770
+ }
771
+ if (conflicts.length && conflictPolicy !== 'replace') {
772
+ throw new AppError('CONFLICT', `MCP server already exists: ${conflicts.map(item => item.id).join(', ')}`);
773
+ }
774
+ const replacedIds = new Set(normalized.map(item => item.id));
775
+ const previous = rows.filter(item => replacedIds.has(item.id));
776
+ const next = [...rows.filter(item => !replacedIds.has(item.id)), ...normalized];
777
+ ctx.store.write(MCP_FILE, next);
778
+ try { syncCodexMcpConfig(); }
779
+ catch (error) {
780
+ ctx.store.write(MCP_FILE, rows);
781
+ try { syncCodexMcpConfig(); } catch { /* preserve the original error */ }
782
+ throw error;
783
+ }
784
+ for (const record of previous) ctx.closeMcpClient(mcpLaunch(record));
785
+ for (const id of replacedIds) toolCache.delete(id);
786
+ return normalized.map(publicMcp);
787
+ }
788
+
789
+ function updateMcpServer(id, input) {
790
+ const rows = standaloneMcpRecords();
791
+ const index = rows.findIndex(item => item.id === id);
792
+ if (index < 0) {
793
+ const pluginRecord = pluginMcpRecords().find(item => item.id === id);
794
+ if (!pluginRecord) throw new AppError('NOT_FOUND', 'MCP server not found');
795
+ const allowed = new Set(['enabled', 'trustLevel', 'targets', 'allowPrivate']);
796
+ if (Object.keys(input).some(key => !allowed.has(key))) throw new AppError('PERMISSION_DENIED', '插件内 MCP server 只能修改启停、信任和作用域策略');
797
+ const value = state();
798
+ value.mcp[id] = {
799
+ ...(value.mcp[id] || {}),
800
+ ...(input.enabled === undefined ? {} : { enabled: input.enabled !== false }),
801
+ ...(input.trustLevel === undefined ? {} : { trustLevel: input.trustLevel === 'trusted' ? 'trusted' : 'untrusted' }),
802
+ ...(input.targets === undefined ? {} : { targets: Array.isArray(input.targets) && input.targets.includes('multichat') ? ['multichat'] : [] }),
803
+ ...(input.allowPrivate === undefined ? {} : { allowPrivate: input.allowPrivate === true }),
804
+ };
805
+ saveState(value);
806
+ ctx.closeMcpClient(mcpLaunch(pluginRecord));
807
+ toolCache.delete(id);
808
+ return publicMcp(pluginMcpRecords().find(item => item.id === id));
809
+ }
810
+ const previousLaunch = mcpLaunch(rows[index]);
811
+ rows[index] = normalizeMcpInput({ ...input, id }, rows[index]);
812
+ ctx.store.write(MCP_FILE, rows);
813
+ ctx.closeMcpClient(previousLaunch);
814
+ toolCache.delete(id);
815
+ syncCodexMcpConfig();
816
+ return publicMcp(rows[index]);
817
+ }
818
+
819
+ function deleteMcpServer(id) {
820
+ const rows = standaloneMcpRecords();
821
+ const previous = rows.find(item => item.id === id);
822
+ const next = rows.filter(item => item.id !== id);
823
+ if (next.length === rows.length) throw new AppError('PERMISSION_DENIED', '插件内 MCP server 不能单独删除');
824
+ ctx.store.write(MCP_FILE, next);
825
+ if (previous) ctx.closeMcpClient(mcpLaunch(previous));
826
+ toolCache.delete(id);
827
+ syncCodexMcpConfig();
828
+ return { ok: true };
829
+ }
830
+
831
+ function mcpLaunch(record: McpServerRecord) {
832
+ if (record.transport === 'http') {
833
+ const headers = expandEnvironmentMap(record.headers);
834
+ if (record.bearerTokenEnvVar && process.env[record.bearerTokenEnvVar]) headers.Authorization = `Bearer ${process.env[record.bearerTokenEnvVar]}`;
835
+ return {
836
+ url: record.url,
837
+ headers,
838
+ fetchImpl: (url, options) => ctx.safeFetch(url, options, record.allowPrivate === true),
839
+ };
840
+ }
841
+ return {
842
+ command: record.command,
843
+ args: record.args || [],
844
+ cwd: record.cwd ? (path.isAbsolute(record.cwd) ? record.cwd : path.resolve(PROJECT_ROOT, record.cwd)) : undefined,
845
+ env: expandEnvironmentMap(record.env),
846
+ };
847
+ }
848
+
849
+ async function discoverMcpTools(id: string, force = false, options: RequestOptions = {}) {
850
+ const record = findPrivateMcp(id);
851
+ if (!record.enabled) throw new AppError('PERMISSION_DENIED', 'MCP server is disabled');
852
+ if (!force && toolCache.get(id)?.status === 'ready') return toolCache.get(id);
853
+ try {
854
+ const client = ctx.getMcpClient(mcpLaunch(record));
855
+ const tools = (await client.listTools(options)).map(tool => ({
856
+ name: String(tool.name || ''),
857
+ description: String(tool.description || ''),
858
+ inputSchema: tool.inputSchema || { type: 'object', properties: {} },
859
+ annotations: tool.annotations || {},
860
+ }));
861
+ const value = { status: 'ready', tools, checkedAt: new Date().toISOString(), error: null, instructions: client.instructions || '', serverInfo: client.serverInfo || null };
862
+ toolCache.set(id, value);
863
+ return value;
864
+ } catch (error) {
865
+ const value = { status: 'error', tools: [], checkedAt: new Date().toISOString(), error: error.message };
866
+ toolCache.set(id, value);
867
+ throw new AppError('MCP_CONNECTION_FAILED', error.message);
868
+ }
869
+ }
870
+
871
+ function toolFunctionName(serverId, toolName) {
872
+ const clean = value => String(value).replace(/[^A-Za-z0-9_-]/g, '_');
873
+ const raw = `mcp__${clean(serverId)}__${clean(toolName)}`;
874
+ if (raw.length <= 64) return raw;
875
+ const suffix = crypto.createHash('sha256').update(`${serverId}\0${toolName}`).digest('hex').slice(0, 10);
876
+ return `${raw.slice(0, 53)}_${suffix}`;
877
+ }
878
+
879
+ async function runtimeMcpTools(serverIds: string[], options: RequestOptions = {}) {
880
+ const ids = new Set(Array.isArray(serverIds) ? serverIds : []);
881
+ const definitions = [];
882
+ const calls = new Map();
883
+ const errors = [];
884
+ const records = privateMcpRecords().filter(item => item.enabled && ids.has(item.id) && (item.targets || []).includes('multichat'));
885
+ const discoveredRecords = await Promise.all(records.map(async record => {
886
+ try { return { record, discovered: await discoverMcpTools(record.id, false, options) }; }
887
+ catch (error) {
888
+ if (options.signal?.aborted) throw error;
889
+ return { record, error };
890
+ }
891
+ }));
892
+ for (const { record, discovered, error } of discoveredRecords) {
893
+ if (error) { errors.push({ serverId: record.id, name: record.name, error: error.message }); continue; }
894
+ for (const tool of discovered.tools) {
895
+ const name = toolFunctionName(record.id, tool.name);
896
+ definitions.push({ type: 'function', function: { name, description: `${tool.description || tool.name} [MCP: ${record.name}]`, parameters: tool.inputSchema || { type: 'object', properties: {} } } });
897
+ calls.set(name, {
898
+ skill: {
899
+ id: `mcp:${record.id}:${tool.name}`,
900
+ name: tool.name,
901
+ description: tool.description || '',
902
+ type: 'mcp',
903
+ enabled: true,
904
+ config: { mcp: mcpLaunch(record), tool: tool.name, permissions: ['external'], trustLevel: record.trustLevel || 'untrusted' },
905
+ },
906
+ });
907
+ }
908
+ }
909
+ return { definitions, calls, errors };
910
+ }
911
+
912
+ function tomlString(value) { return JSON.stringify(String(value)); }
913
+ function tomlKey(value) { return /^[A-Za-z0-9_-]+$/.test(value) ? value : tomlString(value); }
914
+ function tomlArray(values) { return `[${values.map(tomlString).join(', ')}]`; }
915
+
916
+ function codexMcpBlock() {
917
+ const lines = [MANAGED_TOML_START, '# Generated from MultiChat MCP registry. Edit servers in MultiChat, not in this block.'];
918
+ for (const record of standaloneMcpRecords().filter(item => item.enabled && (item.targets || []).includes('codex'))) {
919
+ lines.push('', `[mcp_servers.${tomlKey(record.id)}]`);
920
+ if (record.transport === 'http') {
921
+ lines.push(`url = ${tomlString(record.url)}`);
922
+ if (record.bearerTokenEnvVar) lines.push(`bearer_token_env_var = ${tomlString(record.bearerTokenEnvVar)}`);
923
+ } else {
924
+ lines.push(`command = ${tomlString(record.command)}`);
925
+ if ((record.args || []).length) lines.push(`args = ${tomlArray(record.args)}`);
926
+ if (record.cwd) lines.push(`cwd = ${tomlString(record.cwd)}`);
927
+ if (Object.keys(record.env || {}).length) lines.push(`env_vars = ${tomlArray(Object.keys(record.env))}`);
928
+ }
929
+ lines.push('enabled = true');
930
+ lines.push(`default_tools_approval_mode = ${tomlString(record.trustLevel === 'trusted' ? 'auto' : 'prompt')}`);
931
+ }
932
+ lines.push('', MANAGED_TOML_END);
933
+ return lines.join('\n');
934
+ }
935
+
936
+ function syncCodexMcpConfig() {
937
+ fs.mkdirSync(path.dirname(CODEX_CONFIG), { recursive: true });
938
+ const existing = fs.existsSync(CODEX_CONFIG) ? fs.readFileSync(CODEX_CONFIG, 'utf8') : '';
939
+ const pattern = new RegExp(`${MANAGED_TOML_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${MANAGED_TOML_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*`, 'g');
940
+ const unmanaged = existing.replace(pattern, '').trimEnd();
941
+ const next = `${unmanaged}${unmanaged ? '\n\n' : ''}${codexMcpBlock()}\n`;
942
+ fs.writeFileSync(CODEX_CONFIG, next, 'utf8');
943
+ return { ok: true, path: CODEX_CONFIG, servers: standaloneMcpRecords().filter(item => item.enabled && (item.targets || []).includes('codex')).length };
944
+ }
945
+
946
+ function migrateLegacyAgents() {
947
+ const agents = ctx.store.read(ctx.AGENT_FILE, []);
948
+ if (!Array.isArray(agents) || !agents.length) return;
949
+ const skills = listSkills();
950
+ const builtinTools = ctx.store.read(ctx.SKILL_FILE, []).filter(item => !['prompt', 'mcp'].includes(item.type));
951
+ const builtinToolIds = new Set(builtinTools.map(item => item.id));
952
+ let changed = false;
953
+ for (const agent of agents) {
954
+ if (!Array.isArray(agent.skillRefs)) {
955
+ agent.skillRefs = (agent.skillIds || []).map(id => skills.find(skill => skill.id === id)?.key).filter(Boolean);
956
+ changed = true;
957
+ }
958
+ const legacyToolIds = (agent.skillIds || []).filter(id => builtinToolIds.has(id));
959
+ if (!Array.isArray(agent.toolIds)) {
960
+ agent.toolIds = legacyToolIds;
961
+ changed = true;
962
+ } else {
963
+ const mergedToolIds = [...new Set([...agent.toolIds, ...legacyToolIds])];
964
+ if (mergedToolIds.length !== agent.toolIds.length) {
965
+ agent.toolIds = mergedToolIds;
966
+ changed = true;
967
+ }
968
+ }
969
+ if (legacyToolIds.length) {
970
+ agent.skillIds = (agent.skillIds || []).filter(id => !builtinToolIds.has(id));
971
+ changed = true;
972
+ }
973
+ if (!Array.isArray(agent.mcpServerIds)) { agent.mcpServerIds = []; changed = true; }
974
+ if ((agent.skillIds || []).some(id => id === 'sk_mcp_time' || id === 'sk_mcp_readfile')) {
975
+ if (!agent.mcpServerIds.includes('multichat-demo')) agent.mcpServerIds.push('multichat-demo');
976
+ agent.skillIds = agent.skillIds.filter(id => id !== 'sk_mcp_time' && id !== 'sk_mcp_readfile');
977
+ agent.name = agent.name === 'MCP 演示助手' ? 'MCP 示例助手' : agent.name;
978
+ agent.description = '接入标准 MCP server 的示例智能体,可获取当前时间并查看项目摘要。';
979
+ agent.systemPrompt = '你是一个接入了 MCP 工具的助手。需要时间时调用 get_current_time;需要了解当前项目时调用 get_project_summary。';
980
+ changed = true;
981
+ }
982
+ }
983
+ if (changed) ctx.store.write(ctx.AGENT_FILE, agents);
984
+ }
985
+
986
+ function ensureDefaults() {
987
+ fs.mkdirSync(MANAGED_SKILLS_ROOT, { recursive: true });
988
+ fs.mkdirSync(MANAGED_PLUGINS_ROOT, { recursive: true });
989
+ fs.mkdirSync(MANAGED_BUILTIN_ROOT, { recursive: true });
990
+ fs.mkdirSync(REPO_SKILLS_ROOT, { recursive: true });
991
+ // Keep the bundled demo server in the persistent data directory. A CLI package
992
+ // may live in an npm cache, while DATA_DIR is stable across downloads/upgrades.
993
+ fs.copyFileSync(DEMO_MCP_SOURCE, DEMO_MCP_ENTRY);
994
+ const existingServers = ctx.store.read(MCP_FILE, null) as McpServerRecord[] | null;
995
+ if (!existingServers) {
996
+ ctx.store.write(MCP_FILE, [{
997
+ id: 'multichat-demo',
998
+ name: 'MultiChat 示例工具',
999
+ description: '标准 MCP STDIO 示例:提供当前时间和项目摘要两个只读工具。',
1000
+ transport: 'stdio',
1001
+ command: process.execPath,
1002
+ args: [DEMO_MCP_ENTRY],
1003
+ cwd: PROJECT_ROOT,
1004
+ env: {},
1005
+ enabled: true,
1006
+ trustLevel: 'trusted',
1007
+ targets: ['multichat', 'codex'],
1008
+ source: { kind: 'builtin', scope: 'project' },
1009
+ updatedAt: new Date().toISOString(),
1010
+ }]);
1011
+ } else {
1012
+ const demo = existingServers.find(server => server.id === 'multichat-demo' && server.source?.kind === 'builtin');
1013
+ if (demo && (demo.command !== process.execPath || demo.args?.[0] !== DEMO_MCP_ENTRY || demo.cwd !== PROJECT_ROOT)) {
1014
+ demo.command = process.execPath;
1015
+ demo.args = [DEMO_MCP_ENTRY];
1016
+ demo.cwd = PROJECT_ROOT;
1017
+ demo.updatedAt = new Date().toISOString();
1018
+ ctx.store.write(MCP_FILE, existingServers);
1019
+ }
1020
+ }
1021
+ if (!ctx.store.read(STATE_FILE, null)) saveState(state());
1022
+ migrateLegacyAgents();
1023
+ syncCodexMcpConfig();
1024
+ }
1025
+
1026
+ module.exports = {
1027
+ PROJECT_ROOT,
1028
+ MANAGED_ROOT,
1029
+ REPO_MARKETPLACE,
1030
+ CODEX_CONFIG,
1031
+ parseSkillMarkdown,
1032
+ mcpServerMap,
1033
+ listSkills,
1034
+ findSkill,
1035
+ createSkill,
1036
+ updateSkill,
1037
+ setSkillEnabled,
1038
+ deleteSkill,
1039
+ gitDiff,
1040
+ listPlugins,
1041
+ setPluginEnabled,
1042
+ markPluginImported,
1043
+ deleteImportedPlugin,
1044
+ pluginDiff,
1045
+ listMcpServers,
1046
+ createMcpServer,
1047
+ importMcpServers,
1048
+ updateMcpServer,
1049
+ deleteMcpServer,
1050
+ discoverMcpTools,
1051
+ runtimeMcpTools,
1052
+ syncCodexMcpConfig,
1053
+ ensureDefaults,
1054
+ _test: { expandEnvironmentMap },
1055
+ };