@szc-ft/mcp-szcd-client 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@szc-ft/mcp-szcd-client",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "MCP client for szcd component library - auto-configures AI coding tools with MCP server, skills, agents and commands",
5
5
  "keywords": [
6
6
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "szcd-component-helper",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "szcd 组件库 MCP 助手 — 查询组件信息、匹配需求、生成代码",
5
5
  "mcpServers": {
6
6
  "szcd-component-helper": {
@@ -0,0 +1,260 @@
1
+ /**
2
+ * codex.js — OpenAI Codex CLI 兼容逻辑
3
+ *
4
+ * 导出 setupCodex(deps) 供 postinstall.js 调用。
5
+ *
6
+ * Codex CLI 关键事实(来源:github.com/openai/codex codex-rs/cli/src/mcp_cmd.rs +
7
+ * developers.openai.ac.cn/codex/skills + 本机 codex-cli 0.142.0 实测):
8
+ * - 安装:npm i -g @openai/codex(提供 `codex` 命令)
9
+ * - 配置文件:$CODEX_HOME/config.toml(默认 ~/.codex/config.toml),TOML 格式
10
+ * - MCP server 节段:[mcp_servers.<NAME>],扁平字段,无 transport 子表、无 type/enabled:
11
+ * · stdio:command = "...",可选 args/env
12
+ * · streamable_http:url = "...",可选 bearer_token_env_var
13
+ * CLI 通过是否存在 url/command 自动识别 transport 类型
14
+ * - 管理子命令:codex mcp { list | get | add | remove }
15
+ * - Skill 目录(用户级):$HOME/.agents/skills/<skill-name>/SKILL.md(不是 ~/.codex/skills/,
16
+ * 与 Codex CLI 自己的 home 分离;同一 .agents/skills/ 也被其它走 "open agent skills 规范"
17
+ * 的客户端共享)。Codex 还会读仓库级 .agents/skills,但 postinstall 不应碰用户仓库。
18
+ * - Skill 格式:目录形式,必含 SKILL.md(YAML frontmatter: name/description + Markdown 正文),
19
+ * 可选 scripts/、references/、assets/、agents/openai.yaml 等子目录。无需 CLI 注册,
20
+ * 放文件即生效;变更后重启 Codex。
21
+ *
22
+ * 本实现策略:
23
+ * 1. MCP:优先 spawn `codex mcp remove` 再 `codex mcp add`(CLI 自己做 TOML 合并 + 幂等)
24
+ * 2. MCP fallback:找不到 `codex` 时直接写 config.toml(扁平 TOML,与 CLI 序列化一致)
25
+ * 3. Skill:调用 deps.copySkill(...) 把 standard-skill/* 整目录拷到 ~/.agents/skills/
26
+ */
27
+
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import os from "node:os";
31
+ import { execSync } from "node:child_process";
32
+ import { getSketchMcpServerUrl, ADDITIONAL_MCP_SERVERS } from "./common.js";
33
+
34
+ // ==================== 路径工具 ====================
35
+
36
+ function getHomeDir() {
37
+ if (os.platform() === "win32") {
38
+ return process.env.USERPROFILE || process.env.HOME || os.homedir();
39
+ }
40
+ return process.env.HOME || os.homedir();
41
+ }
42
+
43
+ function getCodexHome() {
44
+ return process.env.CODEX_HOME || path.join(getHomeDir(), ".codex");
45
+ }
46
+
47
+ function getCodexConfigPath() {
48
+ return path.join(getCodexHome(), "config.toml");
49
+ }
50
+
51
+ function isCodexCliAvailable() {
52
+ try {
53
+ execSync("codex --version", { stdio: "pipe", timeout: 5000 });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ // ==================== TOML 序列化(最小子集,够本场景用) ====================
61
+
62
+ /**
63
+ * 把单个 mcp server 渲染成 TOML(与 codex CLI 实际序列化格式一致:扁平、无 transport 子表)。
64
+ *
65
+ * stdio:
66
+ * [mcp_servers.NAME]
67
+ * command = "..."
68
+ * args = ["...", "..."]
69
+ *
70
+ * streamable_http:
71
+ * [mcp_servers.NAME]
72
+ * url = "https://..."
73
+ *
74
+ * 注:codex CLI 0.142 实测产物不写 `type`、`enabled`、`transport`,
75
+ * 由它根据是否有 url/command 字段自动识别。手写 TOML 必须与 CLI 一致,
76
+ * 否则 CLI 下次 `mcp add/remove` 重写时会丢失字段。
77
+ */
78
+ function renderStdioServerToml(name, command, args = []) {
79
+ const lines = [
80
+ `[mcp_servers.${name}]`,
81
+ `command = ${tomlString(command)}`,
82
+ ];
83
+ if (args.length) {
84
+ lines.push(`args = [${args.map(tomlString).join(", ")}]`);
85
+ }
86
+ return lines.join("\n");
87
+ }
88
+
89
+ function renderHttpServerToml(name, url, bearerTokenEnvVar) {
90
+ const lines = [
91
+ `[mcp_servers.${name}]`,
92
+ `url = ${tomlString(url)}`,
93
+ ];
94
+ if (bearerTokenEnvVar) {
95
+ lines.push(`bearer_token_env_var = ${tomlString(bearerTokenEnvVar)}`);
96
+ }
97
+ return lines.join("\n");
98
+ }
99
+
100
+ function tomlString(s) {
101
+ // basic TOML string: 双引号 + 转义反斜杠/双引号/换行
102
+ return '"' + String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n") + '"';
103
+ }
104
+
105
+ // ==================== 文件级合并(CLI 不可用时回退) ====================
106
+
107
+ /**
108
+ * 读取已有 config.toml,移除本包负责的 [mcp_servers.<name>] / [mcp_servers.<name>.transport]
109
+ * 整段(直到下一个 [...]),然后追加新的渲染结果。
110
+ *
111
+ * 不引入完整 TOML 解析器(用户其它 server / 顶层 settings 用纯文本保留,最稳)。
112
+ */
113
+ function upsertCodexConfig(configPath, ourServers) {
114
+ let existing = "";
115
+ if (fs.existsSync(configPath)) {
116
+ existing = fs.readFileSync(configPath, "utf8");
117
+ }
118
+
119
+ // 移除我们要写的每个 server 的旧 [mcp_servers.NAME] 整段(直到下一个 [...] 或文件末)
120
+ for (const s of ourServers) {
121
+ const sectionRegex = new RegExp(
122
+ `(^|\\n)\\[mcp_servers\\.${escapeRegex(s.name)}\\][^\\n]*\\n` +
123
+ `(?:(?!\\n\\[)[\\s\\S])*?(?=\\n\\[|$)`,
124
+ "g"
125
+ );
126
+ existing = existing.replace(sectionRegex, "");
127
+ }
128
+ // 收尾:折叠多余空行 / 保证末尾有换行
129
+ existing = existing.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "").trimEnd();
130
+
131
+ const newBlocks = ourServers.map(s => {
132
+ if (s.kind === "stdio") return renderStdioServerToml(s.name, s.command, s.args);
133
+ return renderHttpServerToml(s.name, s.url, s.bearerTokenEnvVar);
134
+ }).join("\n\n");
135
+
136
+ const sep = existing.length ? "\n\n" : "";
137
+ return existing + sep + newBlocks + "\n";
138
+ }
139
+
140
+ function escapeRegex(s) {
141
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
142
+ }
143
+
144
+ // ==================== CLI 优先路径 ====================
145
+
146
+ function addServerViaCli(server) {
147
+ // codex mcp add NAME --url URL (HTTP)
148
+ // codex mcp add NAME [--env K=V]... -- COMMAND ARGS (stdio)
149
+ let cmd;
150
+ if (server.kind === "http") {
151
+ cmd = `codex mcp add ${shellEscape(server.name)} --url ${shellEscape(server.url)}`;
152
+ } else {
153
+ const args = (server.args || []).map(shellEscape).join(" ");
154
+ cmd = `codex mcp add ${shellEscape(server.name)} -- ${shellEscape(server.command)}${args ? " " + args : ""}`;
155
+ }
156
+ try {
157
+ // 先 remove 再 add,保证幂等(add 在已存在时会报错)
158
+ execSync(`codex mcp remove ${shellEscape(server.name)}`, { stdio: "pipe", timeout: 8000 });
159
+ } catch { /* 没有就忽略 */ }
160
+ try {
161
+ execSync(cmd, { stdio: "pipe", timeout: 10000 });
162
+ return { ok: true, via: "cli" };
163
+ } catch (e) {
164
+ return { ok: false, via: "cli", error: String(e.stderr || e.message).slice(0, 300) };
165
+ }
166
+ }
167
+
168
+ function shellEscape(s) {
169
+ // 简单版:含空格/特殊字符的串用单引号包,单引号本身转义为 '"'"'
170
+ if (/^[A-Za-z0-9_\-./:@=]+$/.test(s)) return s;
171
+ return `'${String(s).replace(/'/g, `'"'"'`)}'`;
172
+ }
173
+
174
+ // ==================== Skill 复制 ====================
175
+
176
+ function getCodexSkillsDirectory() {
177
+ return path.join(getHomeDir(), ".agents", "skills");
178
+ }
179
+
180
+ function copySkillsToCodex(deps) {
181
+ if (typeof deps.discoverSkills !== "function" || typeof deps.copySkill !== "function") {
182
+ console.log("ℹ️ deps.discoverSkills / deps.copySkill not provided — skipping Codex skill copy");
183
+ return { ok: false, skipped: true };
184
+ }
185
+ const skillsRoot = getCodexSkillsDirectory();
186
+ const skills = deps.discoverSkills();
187
+ const results = [];
188
+ for (const skill of skills) {
189
+ const skillDir = path.join(skillsRoot, skill.name);
190
+ try {
191
+ deps.ensureDirectory?.(skillDir) ?? fs.mkdirSync(skillDir, { recursive: true });
192
+ const dest = path.join(skillDir, "SKILL.md");
193
+ deps.copySkill(skill.sourcePath, dest);
194
+ console.log(`✓ Copied skill "${skill.name}" to Codex (${skillDir})`);
195
+ results.push({ name: skill.name, ok: true, path: skillDir });
196
+ } catch (e) {
197
+ console.warn(`⚠️ Failed to copy skill "${skill.name}" to Codex: ${e.message}`);
198
+ results.push({ name: skill.name, ok: false, error: e.message });
199
+ }
200
+ }
201
+ return { ok: results.every(r => r.ok), results, skillsRoot };
202
+ }
203
+
204
+ // ==================== 主入口 ====================
205
+
206
+ export function setupCodex(deps) {
207
+ // 1) 构造我们要注册的两个 server
208
+ const httpUrl = (deps.getMcpServerUrl?.() || "http://localhost:3456") + "/mcp";
209
+ const serverName = deps.getMcpServerName?.() || "szcd-component-helper";
210
+
211
+ const ourServers = [
212
+ // szcd 主 server:HTTP(与其它 IDE 一致;本地 server 无鉴权,故不带 bearer)
213
+ { kind: "http", name: serverName, url: httpUrl },
214
+ // sketch-mcp-server:stdio
215
+ ...ADDITIONAL_MCP_SERVERS.filter(s => s.type === "stdio").map(s => ({
216
+ kind: "stdio", name: s.name, command: s.command, args: s.args || [],
217
+ })),
218
+ ];
219
+
220
+ // 2) MCP 注册
221
+ const mcpResult = registerMcpServers(ourServers);
222
+
223
+ // 3) Skill 复制(独立于 MCP 注册成败,文件操作本身基本不会失败)
224
+ const skillResult = copySkillsToCodex(deps);
225
+
226
+ return { mcp: mcpResult, skill: skillResult };
227
+ }
228
+
229
+ function registerMcpServers(ourServers) {
230
+ const cliAvailable = isCodexCliAvailable();
231
+ if (cliAvailable) {
232
+ console.log("✓ codex CLI detected — registering MCP servers via `codex mcp add`");
233
+ const cliResults = [];
234
+ for (const s of ourServers) {
235
+ const r = addServerViaCli(s);
236
+ cliResults.push({ name: s.name, ...r });
237
+ if (r.ok) console.log(` ✓ codex mcp add ${s.name} (${s.kind})`);
238
+ else console.warn(` ⚠️ codex mcp add ${s.name} failed: ${r.error}`);
239
+ }
240
+ const allOk = cliResults.every(r => r.ok);
241
+ if (allOk) return { ok: true, via: "cli", servers: cliResults };
242
+ console.log("→ CLI partial failure, falling back to direct config.toml edit");
243
+ } else {
244
+ console.log("ℹ️ codex CLI not installed (you can install via `npm i -g @openai/codex`)");
245
+ console.log(" Writing config.toml directly so it works once Codex is installed.");
246
+ }
247
+
248
+ // 回退:直接写 config.toml
249
+ const configPath = getCodexConfigPath();
250
+ try {
251
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
252
+ const merged = upsertCodexConfig(configPath, ourServers);
253
+ fs.writeFileSync(configPath, merged);
254
+ console.log(`✓ Wrote Codex MCP config: ${configPath}`);
255
+ return { ok: true, via: "config-toml", configPath, servers: ourServers.map(s => s.name) };
256
+ } catch (e) {
257
+ console.warn(`⚠️ Failed to write ${configPath}: ${e.message}`);
258
+ return { ok: false, error: e.message };
259
+ }
260
+ }
@@ -103,24 +103,33 @@ function installWindows(browser) {
103
103
  const lnkPath = path.join(desktop, `${browser.name} DevSession.lnk`);
104
104
 
105
105
  // PowerShell 创建 .lnk
106
- // 注意:PowerShell -Command 参数里的双引号、反斜杠都得转义
107
- const ps = [
108
- `$s = (New-Object -ComObject WScript.Shell).CreateShortcut('${lnkPath.replace(/'/g, "''")}')`,
109
- `$s.TargetPath = '${browser.exe.replace(/'/g, "''")}'`,
110
- `$s.Arguments = '${args.replace(/'/g, "''")}'`,
111
- `$s.WorkingDirectory = '${path.dirname(browser.exe).replace(/'/g, "''")}'`,
112
- `$s.IconLocation = '${browser.exe.replace(/'/g, "''")},0'`,
113
- `$s.Description = '${browser.name} CDP dev mode (port ${CDP_PORT}) — used by szcd local-browser-test'`,
106
+ // 注意:Arguments 字符串里含 "C:/..." 这种裸双引号;为彻底避开 shell 引号嵌套,
107
+ // PowerShell -EncodedCommand:脚本块以 UTF-16LE 序列化后 base64 传入,
108
+ // 不再依赖 cmd / bash 的引号转义规则。
109
+ const psScript = [
110
+ `$s = (New-Object -ComObject WScript.Shell).CreateShortcut([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64utf8(lnkPath)}')))`,
111
+ `$s.TargetPath = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64utf8(browser.exe)}'))`,
112
+ `$s.Arguments = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64utf8(args)}'))`,
113
+ `$s.WorkingDirectory = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64utf8(path.dirname(browser.exe))}'))`,
114
+ `$s.IconLocation = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64utf8(browser.exe + ",0")}'))`,
115
+ `$s.Description = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64utf8(browser.name + " CDP dev mode (port " + CDP_PORT + ") — used by szcd local-browser-test")}'))`,
114
116
  `$s.Save()`,
115
117
  ].join("; ");
118
+ // -EncodedCommand 要求脚本块本身是 UTF-16LE base64
119
+ const encoded = Buffer.from(psScript, "utf16le").toString("base64");
116
120
  try {
117
- execSync(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${ps}"`, { stdio: "ignore" });
121
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encoded}`, { stdio: "ignore" });
118
122
  return { ok: true, path: lnkPath };
119
123
  } catch (e) {
120
124
  return { ok: false, error: e.message };
121
125
  }
122
126
  }
123
127
 
128
+ // 字符串 → UTF-8 bytes → base64,给 PowerShell 内联反序列化用
129
+ function b64utf8(s) {
130
+ return Buffer.from(String(s), "utf8").toString("base64");
131
+ }
132
+
124
133
  function installMac(browser) {
125
134
  const dataDir = getUserDataDir(browser.id, "darwin");
126
135
  const appDir = path.join(os.homedir(), "Applications");
@@ -24,6 +24,7 @@ import { setupClaudeCode } from "./lib/claude-code.js";
24
24
  import { setupOpenCode } from "./lib/opencode.js";
25
25
  import { setupQwenCode } from "./lib/qwen-code.js";
26
26
  import { setupQoder } from "./lib/qoder.js";
27
+ import { setupCodex } from "./lib/codex.js";
27
28
  import { setupDevBrowserShortcut } from "./lib/dev-browser-shortcut.js";
28
29
 
29
30
  // 递归守卫在 common.js 加载时已执行
@@ -228,6 +229,10 @@ function main() {
228
229
  console.log("\n🔧 Setting up Qoder CLI compatibility...\n");
229
230
  const qoderResult = setupQoder(deps);
230
231
 
232
+ // ---- OpenAI Codex CLI ----
233
+ console.log("\n🔧 Setting up Codex CLI compatibility...\n");
234
+ setupCodex(deps);
235
+
231
236
  // ---- Dev Browser Shortcut(local-browser-test 一键启动) ----
232
237
  console.log("\n🔧 Setting up dev-browser shortcut for local-browser-test...\n");
233
238
  setupDevBrowserShortcut(deps);