@josephyan/qingflow-cli 0.2.0-beta.1000

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 (92) hide show
  1. package/README.md +31 -0
  2. package/docs/local-agent-install.md +309 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +346 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +37 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +649 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +1846 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +16502 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +112 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +539 -0
  21. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  22. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  23. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  24. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  25. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  26. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  27. package/src/qingflow_mcp/cli/commands/task.py +141 -0
  28. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  29. package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
  30. package/src/qingflow_mcp/cli/context.py +60 -0
  31. package/src/qingflow_mcp/cli/formatters.py +573 -0
  32. package/src/qingflow_mcp/cli/json_io.py +50 -0
  33. package/src/qingflow_mcp/cli/main.py +186 -0
  34. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  35. package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
  36. package/src/qingflow_mcp/config.py +407 -0
  37. package/src/qingflow_mcp/errors.py +66 -0
  38. package/src/qingflow_mcp/id_utils.py +49 -0
  39. package/src/qingflow_mcp/import_store.py +121 -0
  40. package/src/qingflow_mcp/json_types.py +18 -0
  41. package/src/qingflow_mcp/list_type_labels.py +76 -0
  42. package/src/qingflow_mcp/public_surface.py +243 -0
  43. package/src/qingflow_mcp/repository_store.py +71 -0
  44. package/src/qingflow_mcp/response_trim.py +841 -0
  45. package/src/qingflow_mcp/server.py +216 -0
  46. package/src/qingflow_mcp/server_app_builder.py +543 -0
  47. package/src/qingflow_mcp/server_app_user.py +386 -0
  48. package/src/qingflow_mcp/session_store.py +369 -0
  49. package/src/qingflow_mcp/solution/__init__.py +6 -0
  50. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  51. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  52. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  53. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  54. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  55. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  56. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  57. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  58. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  59. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  60. package/src/qingflow_mcp/solution/design_session.py +222 -0
  61. package/src/qingflow_mcp/solution/design_store.py +100 -0
  62. package/src/qingflow_mcp/solution/executor.py +2398 -0
  63. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  64. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  65. package/src/qingflow_mcp/solution/run_store.py +244 -0
  66. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  67. package/src/qingflow_mcp/tools/__init__.py +1 -0
  68. package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
  69. package/src/qingflow_mcp/tools/app_tools.py +926 -0
  70. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  71. package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
  72. package/src/qingflow_mcp/tools/base.py +281 -0
  73. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  74. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  75. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  76. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  77. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  78. package/src/qingflow_mcp/tools/import_tools.py +2223 -0
  79. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  80. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  81. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  82. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  83. package/src/qingflow_mcp/tools/record_tools.py +14291 -0
  84. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  85. package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
  86. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  87. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  88. package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
  89. package/src/qingflow_mcp/tools/task_tools.py +889 -0
  90. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  91. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  92. package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Qingflow CLI
2
+
3
+ Install:
4
+
5
+ ```bash
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.1000
7
+ ```
8
+
9
+ Run:
10
+
11
+ ```bash
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.1000 qingflow
13
+ ```
14
+
15
+ Environment:
16
+
17
+ - `QINGFLOW_MCP_DEFAULT_BASE_URL`
18
+ - `QINGFLOW_MCP_DEFAULT_QF_VERSION`
19
+ - `QINGFLOW_MCP_HOME`
20
+
21
+ This package bootstraps a local Python runtime on first install and then starts the `qingflow` command line interface.
22
+
23
+ Bundled skills:
24
+
25
+ - none
26
+
27
+ Note:
28
+
29
+ - The skill files are included in the npm package.
30
+ - On install, the package copies them to `$CODEX_HOME/skills` (or `~/.codex/skills` if `CODEX_HOME` is unset).
31
+ - If a stdio MCP client reports `Transport closed`, delete `.npm-python`, reinstall the package, and make sure CLI/user/builder packages are on the same version. The stdio entrypoints refuse runtime bootstrap so install logs never corrupt MCP stdout.
@@ -0,0 +1,309 @@
1
+ # 本地智能体安装
2
+
3
+ 这个目录下现在有三个本地命令入口:
4
+
5
+ 1. 统一 CLI:`qingflow`
6
+ 2. 记录/待办优先的 `qingflow-app-user-mcp`
7
+ 3. 精简 builder 的 `qingflow-app-builder-mcp`
8
+
9
+ ## 本地鉴权推荐方案
10
+
11
+ 本地模式现在推荐优先使用 `credential` 建立会话,而不是直接注入 `token`。
12
+
13
+ 推荐链路:
14
+
15
+ 1. createClaw 或其它本地宿主为当前实例保存 `credential`
16
+ 2. 本地 MCP 调用 `auth_use_credential`
17
+ 3. MCP 用该 `credential` 请求 apaas `/mcp/auth/context`
18
+ 4. 解析并保存 `token / wsId / qfVersion / uid`
19
+ 5. 业务工具直接使用这份上下文
20
+
21
+ `auth_use_credential` 是本地唯一鉴权主路径。
22
+
23
+ 补充说明:
24
+
25
+ - 对 stdio MCP 来说,主路径仍然只有 `auth_use_credential`。
26
+ - 如果你是在终端里直接使用 `qingflow` CLI,可以额外使用 `qingflow auth login` 作为“人类登录”入口;默认会提示轻流邮箱和隐藏密码,拿到 `token` 后建立本地 CLI 会话。
27
+ - 也就是说,这次新增的是 CLI 的登录入口,不是给 MCP 增加第二套会话模型。
28
+
29
+ ## npm 安装器适用场景
30
+
31
+ 适合这类本地 agent / gateway:
32
+
33
+ - Claude Desktop
34
+ - Cline / Roo / Cursor 这类本地 MCP 客户端
35
+ - OpenClaw 风格的本地 agent 容器或本地 gateway
36
+ - 任何支持 `command + args + env` 的 stdio MCP 客户端
37
+
38
+ ## 前置条件
39
+
40
+ - Node.js >= 16.16
41
+ - Python >= 3.11
42
+ - 安装过程中可以访问 PyPI
43
+
44
+ 如果机器上没有默认 `python3` / `python`,可以先设置:
45
+
46
+ ```bash
47
+ export QINGFLOW_MCP_PYTHON=/path/to/python3.11
48
+ ```
49
+
50
+ ## 安装方式
51
+
52
+ ### 方式 1:在源码目录预热运行环境
53
+
54
+ ```bash
55
+ cd qingflow-support/mcp-server
56
+ npm install
57
+ ```
58
+
59
+ 这个模式适合你已经有源码 checkout,只想让当前目录具备本地 agent 可调用的运行时。
60
+
61
+ ### 方式 2:全局安装到当前机器
62
+
63
+ ```bash
64
+ cd qingflow-support/mcp-server
65
+ npm install -g .
66
+ ```
67
+
68
+ ### 方式 3:安装到某个本地 agent workspace
69
+
70
+ ```bash
71
+ npm install /absolute/path/to/qingflow-support/mcp-server
72
+ ```
73
+
74
+ ### 方式 4:离线分发 tgz 安装包
75
+
76
+ 先在源码目录打包:
77
+
78
+ ```bash
79
+ cd qingflow-support/mcp-server
80
+ npm run pack:npm
81
+ ```
82
+
83
+ 会生成:
84
+
85
+ ```bash
86
+ dist/npm/qingflow-tech-qingflow-cli-<version>.tgz
87
+ dist/npm/qingflow-tech-qingflow-app-user-mcp-<version>.tgz
88
+ dist/npm/qingflow-tech-qingflow-app-builder-mcp-<version>.tgz
89
+ ```
90
+
91
+ 然后在目标机器安装:
92
+
93
+ ```bash
94
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-cli-<version>.tgz
95
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-app-user-mcp-<version>.tgz
96
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-app-builder-mcp-<version>.tgz
97
+ ```
98
+
99
+ 安装时会自动:
100
+
101
+ 1. 创建 `.npm-python/`
102
+ 2. 在其中建立 Python 虚拟环境
103
+ 3. 执行 `pip install .`
104
+ 4. 在安装位置暴露 `qingflow`、`qingflow-app-user-mcp`、`qingflow-app-builder-mcp` 命令
105
+
106
+ ## 本地验证
107
+
108
+ 如果你在源码目录执行了 `npm install`,可直接这样启动:
109
+
110
+ ```bash
111
+ cd qingflow-support/mcp-server
112
+ node ./npm/bin/qingflow.mjs --help
113
+ node ./npm/bin/qingflow-app-user-mcp.mjs
114
+ node ./npm/bin/qingflow-app-builder-mcp.mjs
115
+ ```
116
+
117
+ 如果你是全局安装:
118
+
119
+ ```bash
120
+ qingflow --help
121
+ qingflow-app-user-mcp
122
+ qingflow-app-builder-mcp
123
+ ```
124
+
125
+ 如果你是把包安装到了某个本地 agent workspace,命令通常位于:
126
+
127
+ ```bash
128
+ /absolute/path/to/agent-workspace/node_modules/.bin/qingflow
129
+ /absolute/path/to/agent-workspace/node_modules/.bin/qingflow-app-user-mcp
130
+ /absolute/path/to/agent-workspace/node_modules/.bin/qingflow-app-builder-mcp
131
+ ```
132
+
133
+ 如果你是从 tgz 安装到某个空目录,命令通常位于:
134
+
135
+ ```bash
136
+ /absolute/path/to/install-dir/node_modules/.bin/qingflow
137
+ /absolute/path/to/install-dir/node_modules/.bin/qingflow-app-user-mcp
138
+ /absolute/path/to/install-dir/node_modules/.bin/qingflow-app-builder-mcp
139
+ ```
140
+
141
+ 这是 stdio MCP server,正常情况下不会输出欢迎信息,而是等待客户端连接。
142
+
143
+ ## 客户端配置
144
+
145
+ ### 通用 stdio MCP 客户端
146
+
147
+ 如果你直接使用源码 checkout:
148
+
149
+ ```json
150
+ {
151
+ "mcpServers": {
152
+ "qingflow": {
153
+ "command": "node",
154
+ "args": [
155
+ "/absolute/path/to/qingflow-support/mcp-server/npm/bin/qingflow-app-user-mcp.mjs"
156
+ ],
157
+ "env": {
158
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
159
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
160
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
161
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
162
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
163
+ }
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ 如果你已经全局安装:
170
+
171
+ ```json
172
+ {
173
+ "mcpServers": {
174
+ "qingflow": {
175
+ "command": "qingflow-app-user-mcp",
176
+ "args": [],
177
+ "env": {
178
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
179
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
180
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
181
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
182
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
183
+ }
184
+ }
185
+ }
186
+ }
187
+ ```
188
+
189
+ 如果你把包安装到了某个本地 agent workspace:
190
+
191
+ ```json
192
+ {
193
+ "mcpServers": {
194
+ "qingflow": {
195
+ "command": "/absolute/path/to/agent-workspace/node_modules/.bin/qingflow-app-user-mcp",
196
+ "args": [],
197
+ "env": {
198
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
199
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
200
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
201
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
202
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
203
+ }
204
+ }
205
+ }
206
+ }
207
+ ```
208
+
209
+ ### 使用 npx
210
+
211
+ 如果不做全局安装,也可以直接运行独立包:
212
+
213
+ ```json
214
+ {
215
+ "mcpServers": {
216
+ "qingflow-user": {
217
+ "command": "npx",
218
+ "args": [
219
+ "-y",
220
+ "@josephyan/qingflow-app-user-mcp"
221
+ ],
222
+ "env": {
223
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
224
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
225
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
226
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
227
+ }
228
+ },
229
+ "qingflow-builder": {
230
+ "command": "npx",
231
+ "args": [
232
+ "-y",
233
+ "@josephyan/qingflow-app-builder-mcp"
234
+ ],
235
+ "env": {
236
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
237
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
238
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
239
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
240
+ }
241
+ }
242
+ }
243
+ }
244
+ ```
245
+
246
+ 说明:
247
+ - 源码目录 `npm install` 不会把命令加到全局 PATH;这种模式请用 `node ./npm/bin/qingflow.mjs`、`node ./npm/bin/qingflow-app-user-mcp.mjs` 或 `node ./npm/bin/qingflow-app-builder-mcp.mjs`
248
+ - `npx` 方式适合临时安装或容器化本地 agent
249
+ - 全局安装方式更适合长期固定使用的本机开发环境
250
+ - 计费接口使用当前登录会话的 `token` 与 `wsId` 请求头,可通过 `QINGFLOW_MCP_CREDIT_APAAS_BASE_URL/PATH` 覆盖调用记录接口地址
251
+
252
+ ## 排障
253
+
254
+ 如果安装失败,优先检查:
255
+
256
+ 1. `node -v`
257
+ 2. `python3 --version`
258
+ 3. `pip` 是否能访问 PyPI
259
+ 4. 是否设置了错误的 `QINGFLOW_MCP_PYTHON`
260
+
261
+ 如果需要重装 Python 侧运行环境,可以删掉:
262
+
263
+ ```bash
264
+ rm -rf .npm-python
265
+ ```
266
+
267
+ 然后重新执行:
268
+
269
+ ```bash
270
+ npm install
271
+ ```
272
+
273
+ 如果 MCP 客户端一调用工具就报 `Transport closed`,优先检查这几件事:
274
+
275
+ 1. 不要混用不同版本的 `@josephyan/qingflow-cli`、`@josephyan/qingflow-app-user-mcp`、`@josephyan/qingflow-app-builder-mcp`
276
+ 2. 删除安装目录下的 `.npm-python`
277
+ 3. 重新执行 `npm install` 或重新安装对应 tgz/npm 包
278
+ 4. 再启动 MCP 客户端
279
+
280
+ 现在 stdio MCP 入口会拒绝在启动瞬间“边启动边重建 Python 运行时”,因为安装日志一旦写进 stdout,就会破坏 MCP 握手并表现成 `Transport closed`。如果运行时缺失或版本不一致,入口会直接报错并提示重装,而不是静默自修复。
281
+
282
+ ## createClaw 本地接入示例
283
+
284
+ 如果 createClaw 已经为当前本地实例保存了 `credential`,推荐在首次建链时调用:
285
+
286
+ ```bash
287
+ qingflow auth use-credential \
288
+ --base-url https://qingflow.com/api \
289
+ --credential-stdin
290
+ ```
291
+
292
+ 然后把 `credential` 写到 stdin。
293
+
294
+ 等价 MCP 工具调用参数:
295
+
296
+ ```json
297
+ {
298
+ "profile": "default",
299
+ "base_url": "https://qingflow.com/api",
300
+ "credential": "1602853_277941",
301
+ "persist": false
302
+ }
303
+ ```
304
+
305
+ 说明:
306
+
307
+ - 本地会把解析后的 `token` 和原始 `credential` 写入 profile 文件,用于后续 CLI 命令恢复会话
308
+ - `persist=true` 时,本地还会优先把解析后的 `token` 和原始 `credential` 同步写入系统 keychain
309
+ - 当前工作区以 `/mcp/auth/context` 返回的 `wsId` 为准,不再通过本地 MCP 显式切换
package/entry_point.py ADDED
@@ -0,0 +1,13 @@
1
+ import sys
2
+ import os
3
+
4
+ # Ensure the 'src' directory is in the Python search path
5
+ # PyInstaller will pick up everything in src/qingflow_mcp as the package
6
+ src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "src"))
7
+ if src_path not in sys.path:
8
+ sys.path.insert(0, src_path)
9
+
10
+ from qingflow_mcp.server import main
11
+
12
+ if __name__ == "__main__":
13
+ main()
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { spawnServer, getPackageRoot } from "../lib/runtime.mjs";
3
+
4
+ const packageRoot = getPackageRoot(import.meta.url);
5
+ spawnServer(packageRoot, process.argv.slice(2), "qingflow", { allowRuntimeBootstrap: true, stdio: "inherit" });
@@ -0,0 +1,346 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const WINDOWS = process.platform === "win32";
7
+
8
+ function runChecked(command, args, options = {}) {
9
+ const result = spawnSync(command, args, {
10
+ encoding: "utf8",
11
+ stdio: ["ignore", "pipe", "pipe"],
12
+ ...options,
13
+ });
14
+ if (result.error) {
15
+ throw result.error;
16
+ }
17
+ if (result.status !== 0) {
18
+ const details = [result.stderr, result.stdout].filter((value) => typeof value === "string" && value.trim()).join("\n");
19
+ throw new Error(details ? `Command failed: ${command} ${args.join(" ")}\n${details}` : `Command failed: ${command} ${args.join(" ")}`);
20
+ }
21
+ }
22
+
23
+ function commandWorks(command, args) {
24
+ const result = spawnSync(command, args, {
25
+ stdio: "ignore",
26
+ });
27
+ return result.status === 0;
28
+ }
29
+
30
+ export function getPackageRoot(metaUrl) {
31
+ return path.resolve(path.dirname(fileURLToPath(metaUrl)), "..", "..");
32
+ }
33
+
34
+ export function getCodexHome() {
35
+ const configured = process.env.CODEX_HOME?.trim();
36
+ if (configured) {
37
+ return path.resolve(configured);
38
+ }
39
+ const home = process.env.HOME || process.env.USERPROFILE;
40
+ if (!home) {
41
+ throw new Error("Cannot resolve CODEX_HOME because HOME is not set.");
42
+ }
43
+ return path.join(home, ".codex");
44
+ }
45
+
46
+ export function installBundledSkills(packageRoot) {
47
+ const skillsSrc = path.join(packageRoot, "skills");
48
+ if (!fs.existsSync(skillsSrc)) {
49
+ return { installed: [], skipped: true, destination: null };
50
+ }
51
+
52
+ const codexHome = getCodexHome();
53
+ const skillsDestRoot = path.join(codexHome, "skills");
54
+ fs.mkdirSync(skillsDestRoot, { recursive: true });
55
+
56
+ const installed = [];
57
+ for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
58
+ if (!entry.isDirectory()) {
59
+ continue;
60
+ }
61
+ const src = path.join(skillsSrc, entry.name);
62
+ const dest = path.join(skillsDestRoot, entry.name);
63
+ fs.rmSync(dest, { recursive: true, force: true });
64
+ fs.cpSync(src, dest, { recursive: true });
65
+ installed.push(entry.name);
66
+ }
67
+
68
+ return { installed, skipped: false, destination: skillsDestRoot };
69
+ }
70
+
71
+ export function getVenvDir(packageRoot) {
72
+ return path.join(packageRoot, ".npm-python");
73
+ }
74
+
75
+ export function getVenvPython(packageRoot) {
76
+ return WINDOWS
77
+ ? path.join(getVenvDir(packageRoot), "Scripts", "python.exe")
78
+ : path.join(getVenvDir(packageRoot), "bin", "python");
79
+ }
80
+
81
+ export function getVenvServerCommand(packageRoot, commandName = "qingflow-mcp") {
82
+ return WINDOWS
83
+ ? path.join(getVenvDir(packageRoot), "Scripts", `${commandName}.exe`)
84
+ : path.join(getVenvDir(packageRoot), "bin", commandName);
85
+ }
86
+
87
+ function readPackageVersion(packageRoot) {
88
+ const packageJsonPath = path.join(packageRoot, "package.json");
89
+ try {
90
+ const payload = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
91
+ return typeof payload.version === "string" && payload.version.trim() ? payload.version.trim() : null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ function readBootstrapStamp(stampPath) {
98
+ if (!fs.existsSync(stampPath)) {
99
+ return { exists: false, version: null };
100
+ }
101
+ try {
102
+ const payload = JSON.parse(fs.readFileSync(stampPath, "utf8"));
103
+ const version = typeof payload.package_version === "string" && payload.package_version.trim() ? payload.package_version.trim() : null;
104
+ return { exists: true, version };
105
+ } catch {
106
+ return { exists: true, version: null };
107
+ }
108
+ }
109
+
110
+ export function inspectPythonEnv(packageRoot, commandName = "qingflow-mcp") {
111
+ const venvDir = getVenvDir(packageRoot);
112
+ const serverCommand = getVenvServerCommand(packageRoot, commandName);
113
+ const stampPath = path.join(venvDir, ".bootstrap.json");
114
+ const packageVersion = readPackageVersion(packageRoot);
115
+ const stamp = readBootstrapStamp(stampPath);
116
+ const problems = [];
117
+
118
+ if (!packageVersion) {
119
+ problems.push("package-version-missing");
120
+ }
121
+ if (!fs.existsSync(serverCommand)) {
122
+ problems.push("server-command-missing");
123
+ }
124
+ if (!stamp.exists) {
125
+ problems.push("bootstrap-stamp-missing");
126
+ } else if (!stamp.version) {
127
+ problems.push("bootstrap-stamp-invalid");
128
+ } else if (packageVersion && stamp.version !== packageVersion) {
129
+ problems.push("bootstrap-version-mismatch");
130
+ }
131
+
132
+ return {
133
+ ready: problems.length === 0,
134
+ packageVersion,
135
+ stampVersion: stamp.version,
136
+ stampExists: stamp.exists,
137
+ stampPath,
138
+ serverCommand,
139
+ venvDir,
140
+ problems,
141
+ };
142
+ }
143
+
144
+ function formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap = false } = {}) {
145
+ const problemLines = [];
146
+ for (const problem of runtime.problems) {
147
+ switch (problem) {
148
+ case "package-version-missing":
149
+ problemLines.push("- package.json is missing a valid version field");
150
+ break;
151
+ case "server-command-missing":
152
+ problemLines.push(`- missing Python entrypoint: ${runtime.serverCommand}`);
153
+ break;
154
+ case "bootstrap-stamp-missing":
155
+ problemLines.push(`- missing bootstrap stamp: ${runtime.stampPath}`);
156
+ break;
157
+ case "bootstrap-stamp-invalid":
158
+ problemLines.push(`- bootstrap stamp is unreadable or invalid: ${runtime.stampPath}`);
159
+ break;
160
+ case "bootstrap-version-mismatch":
161
+ problemLines.push(
162
+ `- bootstrap version mismatch: package=${runtime.packageVersion ?? "unknown"}, runtime=${runtime.stampVersion ?? "unknown"}`
163
+ );
164
+ break;
165
+ default:
166
+ problemLines.push(`- ${problem}`);
167
+ break;
168
+ }
169
+ }
170
+
171
+ const action = allowRuntimeBootstrap
172
+ ? "Delete .npm-python and retry, or rerun npm install to rebuild the embedded Python runtime."
173
+ : "Delete .npm-python and rerun npm install, or reinstall the npm package before starting the MCP server.";
174
+
175
+ const bootstrapNote = allowRuntimeBootstrap
176
+ ? ""
177
+ : "\nRuntime bootstrap is disabled for stdio MCP entrypoints so install logs can never corrupt the MCP stdout transport.";
178
+
179
+ return [
180
+ `[qingflow-mcp] Python runtime for ${commandName} is not ready.`,
181
+ ...problemLines,
182
+ action + bootstrapNote,
183
+ ].join("\n");
184
+ }
185
+
186
+ function getVenvPip(packageRoot) {
187
+ return WINDOWS
188
+ ? path.join(getVenvDir(packageRoot), "Scripts", "pip.exe")
189
+ : path.join(getVenvDir(packageRoot), "bin", "pip");
190
+ }
191
+
192
+ export function findPython() {
193
+ const preferred = process.env.QINGFLOW_MCP_PYTHON?.trim();
194
+ const candidates = preferred
195
+ ? [{ command: preferred, args: [], label: preferred }]
196
+ : WINDOWS
197
+ ? [
198
+ { command: "py", args: ["-3", "-V"], label: "py -3" },
199
+ { command: "python", args: ["-V"], label: "python" },
200
+ { command: "python3", args: ["-V"], label: "python3" },
201
+ ]
202
+ : [
203
+ { command: "python3", args: ["-V"], label: "python3" },
204
+ { command: "python", args: ["-V"], label: "python" },
205
+ ];
206
+
207
+ for (const candidate of candidates) {
208
+ if (commandWorks(candidate.command, candidate.args)) {
209
+ if (candidate.command === "py") {
210
+ return { command: "py", args: ["-3"], label: candidate.label };
211
+ }
212
+ return { command: candidate.command, args: [], label: candidate.label };
213
+ }
214
+ }
215
+
216
+ throw new Error(
217
+ "Python 3.11+ was not found. Set QINGFLOW_MCP_PYTHON to a Python 3 executable before running npm install."
218
+ );
219
+ }
220
+
221
+ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qingflow-mcp" } = {}) {
222
+ const python = findPython();
223
+ const runtime = inspectPythonEnv(packageRoot, commandName);
224
+ const venvPython = getVenvPython(packageRoot);
225
+ const { packageVersion, serverCommand, stampPath, venvDir, stampVersion } = runtime;
226
+
227
+ if (!force && fs.existsSync(serverCommand) && fs.existsSync(stampPath) && stampVersion && stampVersion === packageVersion) {
228
+ return serverCommand;
229
+ }
230
+
231
+ if (force && fs.existsSync(venvDir)) {
232
+ fs.rmSync(venvDir, { recursive: true, force: true });
233
+ }
234
+
235
+ if (!fs.existsSync(venvPython)) {
236
+ runChecked(python.command, [...python.args, "-m", "venv", venvDir], { cwd: packageRoot });
237
+ }
238
+
239
+ const pip = getVenvPip(packageRoot);
240
+ runChecked(pip, ["install", "--disable-pip-version-check", "."], { cwd: packageRoot });
241
+
242
+ fs.writeFileSync(
243
+ stampPath,
244
+ JSON.stringify(
245
+ {
246
+ installed_at: new Date().toISOString(),
247
+ installer: "npm",
248
+ package_version: packageVersion,
249
+ },
250
+ null,
251
+ 2
252
+ )
253
+ );
254
+
255
+ if (!fs.existsSync(serverCommand)) {
256
+ throw new Error(`Bootstrap finished but ${serverCommand} was not created.`);
257
+ }
258
+
259
+ return serverCommand;
260
+ }
261
+
262
+ function proxyStreams(child) {
263
+ if (process.stdin.readable && child.stdin) {
264
+ process.stdin.pipe(child.stdin);
265
+ child.stdin.on("error", (error) => {
266
+ if (error.code !== "EPIPE") {
267
+ console.error(`[qingflow-mcp] Failed to forward stdin: ${error.message}`);
268
+ }
269
+ });
270
+ } else if (child.stdin) {
271
+ child.stdin.end();
272
+ }
273
+
274
+ if (child.stdout) {
275
+ child.stdout.pipe(process.stdout);
276
+ }
277
+ if (child.stderr) {
278
+ child.stderr.pipe(process.stderr);
279
+ }
280
+ }
281
+
282
+ function forwardSignal(child, signal) {
283
+ process.on(signal, () => {
284
+ if (!child.killed) {
285
+ child.kill(signal);
286
+ }
287
+ });
288
+ }
289
+
290
+ export function spawnServer(
291
+ packageRoot,
292
+ args,
293
+ commandName = "qingflow-mcp",
294
+ { allowRuntimeBootstrap = false, stdio = "proxy" } = {},
295
+ ) {
296
+ let runtime = inspectPythonEnv(packageRoot, commandName);
297
+ let serverCommand = runtime.serverCommand;
298
+
299
+ if (!runtime.ready) {
300
+ if (!allowRuntimeBootstrap) {
301
+ console.error(formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap }));
302
+ process.exit(1);
303
+ return;
304
+ }
305
+
306
+ try {
307
+ serverCommand = ensurePythonEnv(packageRoot, { commandName });
308
+ runtime = inspectPythonEnv(packageRoot, commandName);
309
+ } catch (error) {
310
+ console.error(`[qingflow-mcp] Failed to prepare Python runtime for ${commandName}: ${error.message}`);
311
+ process.exit(1);
312
+ return;
313
+ }
314
+
315
+ if (!runtime.ready) {
316
+ console.error(formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap }));
317
+ process.exit(1);
318
+ return;
319
+ }
320
+ }
321
+
322
+ const child = spawn(serverCommand, args, {
323
+ stdio: stdio === "inherit" ? "inherit" : ["pipe", "pipe", "pipe"],
324
+ env: process.env,
325
+ windowsHide: true,
326
+ });
327
+
328
+ if (stdio !== "inherit") {
329
+ proxyStreams(child);
330
+ }
331
+ forwardSignal(child, "SIGINT");
332
+ forwardSignal(child, "SIGTERM");
333
+
334
+ child.on("close", (code, signal) => {
335
+ if (signal) {
336
+ process.kill(process.pid, signal);
337
+ return;
338
+ }
339
+ process.exit(code ?? 0);
340
+ });
341
+
342
+ child.on("error", (error) => {
343
+ console.error(`[qingflow-mcp] Failed to start server: ${error.message}`);
344
+ process.exit(1);
345
+ });
346
+ }