@josephyan/qingflow-cli 1.0.11 → 1.1.2

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 (67) hide show
  1. package/README.md +3 -3
  2. package/npm/bin/qingflow.mjs +40 -2
  3. package/npm/lib/runtime.mjs +386 -15
  4. package/npm/scripts/postinstall.mjs +7 -2
  5. package/package.json +1 -1
  6. package/pyproject.toml +1 -1
  7. package/skills/qingflow-cli/SKILL.md +440 -0
  8. package/skills/qingflow-cli/manifest.yaml +10 -0
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +485 -0
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md +237 -0
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_MATCH_RULES.md +137 -0
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md +263 -0
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +304 -0
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md +41 -0
  16. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
  17. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
  18. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
  19. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
  20. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
  21. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +20 -0
  22. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
  23. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +163 -0
  24. package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +107 -0
  25. package/skills/qingflow-cli/reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +151 -0
  26. package/skills/qingflow-cli/reference/_batch_schema_complex.json +18 -0
  27. package/skills/qingflow-cli/reference/_batch_schema_scalar.json +17 -0
  28. package/skills/qingflow-cli/reference/charts_remove.example.json +1 -0
  29. package/skills/qingflow-cli/reference/charts_reorder.example.json +1 -0
  30. package/skills/qingflow-cli/reference/charts_upsert_bar.example.json +8 -0
  31. package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +37 -0
  32. package/skills/qingflow-cli/reference/charts_upsert_minimal.example.json +13 -0
  33. package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +131 -0
  34. package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +126 -0
  35. package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +128 -0
  36. package/skills/qingflow-cli/reference/schema_add_fields_minimal.example.json +7 -0
  37. package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +78 -0
  38. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +7 -0
  39. package/skills/qingflow-cli/scripts/builder-package-from-app-list.py +140 -0
  40. package/skills/qingflow-cli/scripts/find-app-by-keyword.py +132 -0
  41. package/skills/qingflow-cli/scripts/validate_qingflow_output_files.py +87 -0
  42. package/src/qingflow_mcp/__init__.py +1 -1
  43. package/src/qingflow_mcp/builder_facade/models.py +532 -48
  44. package/src/qingflow_mcp/builder_facade/service.py +9194 -2384
  45. package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
  46. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  47. package/src/qingflow_mcp/cli/commands/builder.py +354 -56
  48. package/src/qingflow_mcp/cli/commands/record.py +89 -2
  49. package/src/qingflow_mcp/cli/formatters.py +32 -1
  50. package/src/qingflow_mcp/cli/main.py +245 -3
  51. package/src/qingflow_mcp/public_surface.py +11 -8
  52. package/src/qingflow_mcp/response_trim.py +143 -14
  53. package/src/qingflow_mcp/server.py +15 -12
  54. package/src/qingflow_mcp/server_app_builder.py +108 -30
  55. package/src/qingflow_mcp/server_app_user.py +17 -18
  56. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  57. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  58. package/src/qingflow_mcp/solution/executor.py +3 -133
  59. package/src/qingflow_mcp/tools/ai_builder_tools.py +2617 -440
  60. package/src/qingflow_mcp/tools/app_tools.py +53 -8
  61. package/src/qingflow_mcp/tools/package_tools.py +16 -2
  62. package/src/qingflow_mcp/tools/record_tools.py +2095 -176
  63. package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
  64. package/src/qingflow_mcp/tools/solution_tools.py +30 -2
  65. package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
  66. package/src/qingflow_mcp/version.py +110 -0
  67. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@1.0.11
6
+ npm install @josephyan/qingflow-cli@1.1.2
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@1.0.11 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@1.1.2 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
@@ -22,7 +22,7 @@ This package bootstraps a local Python runtime on first install and then starts
22
22
 
23
23
  Bundled skills:
24
24
 
25
- - none
25
+ - `skills/qingflow-cli`
26
26
 
27
27
  Note:
28
28
 
@@ -1,5 +1,43 @@
1
1
  #!/usr/bin/env node
2
- import { spawnServer, getPackageRoot } from "../lib/runtime.mjs";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
5
 
6
+ const PKG_BY_BIN = {
7
+ qingflow: "qingflow-cli",
8
+ "qingflow-app-user-mcp": "qingflow-app-user-mcp",
9
+ "qingflow-app-builder-mcp": "qingflow-app-builder-mcp",
10
+ };
11
+
12
+ function resolvePackageModule(metaUrl, ...segments) {
13
+ const scriptPath = fileURLToPath(metaUrl);
14
+ const scriptDir = path.dirname(scriptPath);
15
+ // bin files live in <pkg>/npm/bin/, runtime.mjs lives in the sibling <pkg>/npm/lib/,
16
+ // so step up one level before joining the requested segments.
17
+ const direct = path.join(scriptDir, "..", ...segments);
18
+ if (fs.existsSync(direct)) {
19
+ return pathToFileURL(direct).href;
20
+ }
21
+ if (path.basename(scriptDir) === ".bin") {
22
+ const binName = path.basename(scriptPath);
23
+ const pkgName = PKG_BY_BIN[binName];
24
+ if (!pkgName) {
25
+ throw new Error(`Unknown qingflow command: ${binName}`);
26
+ }
27
+ const scope = process.env.QINGFLOW_NPM_SCOPE || "@josephyan";
28
+ const fromBin = path.join(scriptDir, "..", scope, pkgName, "npm", ...segments);
29
+ if (fs.existsSync(fromBin)) {
30
+ return pathToFileURL(fromBin).href;
31
+ }
32
+ }
33
+ throw new Error(`Cannot locate ${segments.join("/")} from ${scriptPath}`);
34
+ }
35
+
36
+ const { getPackageRoot, spawnServer, runSkillsCli } = await import(resolvePackageModule(import.meta.url, "lib", "runtime.mjs"));
4
37
  const packageRoot = getPackageRoot(import.meta.url);
5
- spawnServer(packageRoot, process.argv.slice(2), "qingflow", { allowRuntimeBootstrap: true, stdio: "inherit" });
38
+ const args = process.argv.slice(2);
39
+ if (args[0] === "skills") {
40
+ runSkillsCli(packageRoot, args.slice(1));
41
+ } else {
42
+ spawnServer(packageRoot, args, "qingflow", { allowRuntimeBootstrap: true, stdio: "inherit" });
43
+ }
@@ -5,6 +5,12 @@ import { fileURLToPath } from "node:url";
5
5
 
6
6
  const WINDOWS = process.platform === "win32";
7
7
 
8
+ const PKG_BY_BIN = {
9
+ qingflow: "qingflow-cli",
10
+ "qingflow-app-user-mcp": "qingflow-app-user-mcp",
11
+ "qingflow-app-builder-mcp": "qingflow-app-builder-mcp",
12
+ };
13
+
8
14
  function runChecked(command, args, options = {}) {
9
15
  const result = spawnSync(command, args, {
10
16
  encoding: "utf8",
@@ -28,7 +34,18 @@ function commandWorks(command, args) {
28
34
  }
29
35
 
30
36
  export function getPackageRoot(metaUrl) {
31
- return path.resolve(path.dirname(fileURLToPath(metaUrl)), "..", "..");
37
+ const scriptPath = fileURLToPath(metaUrl);
38
+ const scriptDir = path.dirname(scriptPath);
39
+ if (path.basename(scriptDir) === ".bin") {
40
+ const binName = path.basename(scriptPath);
41
+ const pkgName = PKG_BY_BIN[binName];
42
+ if (!pkgName) {
43
+ throw new Error(`Unknown qingflow command: ${binName}`);
44
+ }
45
+ const scope = process.env.QINGFLOW_NPM_SCOPE || "@josephyan";
46
+ return path.join(scriptDir, "..", scope, pkgName);
47
+ }
48
+ return path.resolve(scriptDir, "..", "..");
32
49
  }
33
50
 
34
51
  export function getCodexHome() {
@@ -43,29 +60,359 @@ export function getCodexHome() {
43
60
  return path.join(home, ".codex");
44
61
  }
45
62
 
46
- export function installBundledSkills(packageRoot) {
63
+ function getHomeDir() {
64
+ const home = process.env.HOME || process.env.USERPROFILE;
65
+ if (!home) {
66
+ throw new Error("Cannot resolve user home because HOME is not set.");
67
+ }
68
+ return home;
69
+ }
70
+
71
+ function getAgentSkillDest(agent, scope, cwd = process.cwd()) {
72
+ const normalizedAgent = agent.trim().toLowerCase();
73
+ const normalizedScope = scope.trim().toLowerCase();
74
+ const home = getHomeDir();
75
+ if (!["user", "project"].includes(normalizedScope)) {
76
+ throw new Error(`Unsupported skills scope '${scope}'. Expected 'user' or 'project'.`);
77
+ }
78
+
79
+ const projectRoots = {
80
+ codex: [cwd, ".codex", "skills"],
81
+ claude: [cwd, ".claude", "skills"],
82
+ "claude-code": [cwd, ".claude", "skills"],
83
+ cursor: [cwd, ".cursor", "skills"],
84
+ generic: [cwd, ".agents", "skills"],
85
+ };
86
+ const userRoots = {
87
+ codex: [process.env.CODEX_HOME?.trim() || path.join(home, ".codex"), "skills"],
88
+ claude: [home, ".claude", "skills"],
89
+ "claude-code": [home, ".claude", "skills"],
90
+ cursor: [home, ".cursor", "skills"],
91
+ generic: [home, ".agents", "skills"],
92
+ };
93
+
94
+ const roots = normalizedScope === "project" ? projectRoots : userRoots;
95
+ const parts = roots[normalizedAgent];
96
+ if (!parts) {
97
+ throw new Error(`Unsupported skills agent '${agent}'. Expected one of: codex, claude, claude-code, cursor, generic.`);
98
+ }
99
+ return path.resolve(...parts);
100
+ }
101
+
102
+ export function listBundledSkills(packageRoot) {
47
103
  const skillsSrc = path.join(packageRoot, "skills");
48
104
  if (!fs.existsSync(skillsSrc)) {
49
- return { installed: [], skipped: true, destination: null };
105
+ return [];
50
106
  }
51
107
 
52
- const codexHome = getCodexHome();
53
- const skillsDestRoot = path.join(codexHome, "skills");
54
- fs.mkdirSync(skillsDestRoot, { recursive: true });
108
+ return fs
109
+ .readdirSync(skillsSrc, { withFileTypes: true })
110
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(skillsSrc, entry.name, "SKILL.md")))
111
+ .map((entry) => entry.name)
112
+ .sort();
113
+ }
114
+
115
+ function sameRealPath(a, b) {
116
+ try {
117
+ return fs.realpathSync(a) === fs.realpathSync(b);
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
55
122
 
56
- const installed = [];
57
- for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
58
- if (!entry.isDirectory()) {
59
- continue;
123
+ function installOneSkill(src, dest, { force, mode }) {
124
+ let existingStat = null;
125
+ try {
126
+ existingStat = fs.lstatSync(dest);
127
+ } catch (error) {
128
+ if (error.code !== "ENOENT") {
129
+ throw error;
130
+ }
131
+ }
132
+ if (existingStat) {
133
+ if (existingStat.isSymbolicLink() && sameRealPath(dest, src)) {
134
+ return { status: "unchanged" };
135
+ }
136
+ if (!force) {
137
+ return { status: "conflict" };
60
138
  }
61
- const src = path.join(skillsSrc, entry.name);
62
- const dest = path.join(skillsDestRoot, entry.name);
63
139
  fs.rmSync(dest, { recursive: true, force: true });
140
+ }
141
+
142
+ if (mode === "copy") {
64
143
  fs.cpSync(src, dest, { recursive: true });
65
- installed.push(entry.name);
144
+ return { status: "installed" };
66
145
  }
67
146
 
68
- return { installed, skipped: false, destination: skillsDestRoot };
147
+ fs.symlinkSync(src, dest, "dir");
148
+ return { status: "installed" };
149
+ }
150
+
151
+ function writeSkillProvenance(skillsDestRoot, skillName, payload) {
152
+ const provenanceRoot = path.join(skillsDestRoot, ".qingflow-skill-sources");
153
+ fs.mkdirSync(provenanceRoot, { recursive: true });
154
+ fs.writeFileSync(path.join(provenanceRoot, `${skillName}.json`), `${JSON.stringify(payload, null, 2)}\n`);
155
+ }
156
+
157
+ function sleepMs(ms) {
158
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
159
+ }
160
+
161
+ function withSkillInstallLock(skillsDestRoot, callback) {
162
+ const lockDir = path.join(skillsDestRoot, ".qingflow-skill-install.lock");
163
+ const startedAt = Date.now();
164
+ while (true) {
165
+ try {
166
+ fs.mkdirSync(lockDir);
167
+ break;
168
+ } catch (error) {
169
+ if (error.code !== "EEXIST") {
170
+ throw error;
171
+ }
172
+ try {
173
+ const stat = fs.statSync(lockDir);
174
+ if (Date.now() - stat.mtimeMs > 300_000) {
175
+ fs.rmSync(lockDir, { recursive: true, force: true });
176
+ continue;
177
+ }
178
+ } catch {
179
+ continue;
180
+ }
181
+ if (Date.now() - startedAt > 30_000) {
182
+ throw new Error(`Timed out waiting for skill install lock: ${lockDir}`);
183
+ }
184
+ sleepMs(100);
185
+ }
186
+ }
187
+ try {
188
+ return callback();
189
+ } finally {
190
+ fs.rmSync(lockDir, { recursive: true, force: true });
191
+ }
192
+ }
193
+
194
+ export function installBundledSkills(
195
+ packageRoot,
196
+ {
197
+ agent = "codex",
198
+ scope = "user",
199
+ mode = "symlink",
200
+ force = false,
201
+ skills = [],
202
+ cwd = process.cwd(),
203
+ } = {},
204
+ ) {
205
+ const skillsSrc = path.join(packageRoot, "skills");
206
+ if (!fs.existsSync(skillsSrc)) {
207
+ return { installed: [], unchanged: [], conflicts: [], skipped: true, destination: null };
208
+ }
209
+
210
+ const validModes = new Set(["symlink", "copy"]);
211
+ if (!validModes.has(mode)) {
212
+ throw new Error(`Unsupported skills install mode '${mode}'. Expected 'symlink' or 'copy'.`);
213
+ }
214
+
215
+ const available = listBundledSkills(packageRoot);
216
+ const wanted = skills.length ? skills : available;
217
+ const unknown = wanted.filter((skillName) => !available.includes(skillName));
218
+ if (unknown.length) {
219
+ throw new Error(`Unknown bundled skill(s): ${unknown.join(", ")}. Available: ${available.join(", ")}`);
220
+ }
221
+
222
+ const skillsDestRoot = getAgentSkillDest(agent, scope, cwd);
223
+ fs.mkdirSync(skillsDestRoot, { recursive: true });
224
+
225
+ const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
226
+ const result = {
227
+ installed: [],
228
+ unchanged: [],
229
+ conflicts: [],
230
+ skipped: false,
231
+ destination: skillsDestRoot,
232
+ agent,
233
+ scope,
234
+ mode,
235
+ };
236
+
237
+ return withSkillInstallLock(skillsDestRoot, () => {
238
+ for (const skillName of wanted) {
239
+ const src = path.join(skillsSrc, skillName);
240
+ const dest = path.join(skillsDestRoot, skillName);
241
+ const installResult = installOneSkill(src, dest, { force, mode });
242
+ result[installResult.status === "conflict" ? "conflicts" : installResult.status].push(skillName);
243
+ if (installResult.status !== "conflict") {
244
+ writeSkillProvenance(skillsDestRoot, skillName, {
245
+ package: packageJson.name ?? null,
246
+ version: packageJson.version ?? null,
247
+ skill: skillName,
248
+ source: src,
249
+ destination: dest,
250
+ agent,
251
+ scope,
252
+ mode,
253
+ installed_at: new Date().toISOString(),
254
+ });
255
+ }
256
+ }
257
+
258
+ return result;
259
+ });
260
+ }
261
+
262
+ function parseSkillsCliArgs(args) {
263
+ const parsed = {
264
+ command: "help",
265
+ agent: "codex",
266
+ scope: "user",
267
+ mode: "symlink",
268
+ force: false,
269
+ json: false,
270
+ skills: [],
271
+ };
272
+ const rest = [...args];
273
+ if (rest.length && !rest[0].startsWith("-")) {
274
+ parsed.command = rest.shift();
275
+ }
276
+ for (let index = 0; index < rest.length; index += 1) {
277
+ const arg = rest[index];
278
+ const next = () => {
279
+ index += 1;
280
+ if (index >= rest.length) {
281
+ throw new Error(`Missing value for ${arg}`);
282
+ }
283
+ return rest[index];
284
+ };
285
+ switch (arg) {
286
+ case "--agent":
287
+ case "-a":
288
+ parsed.agent = next();
289
+ break;
290
+ case "--scope":
291
+ case "-s":
292
+ parsed.scope = next();
293
+ break;
294
+ case "--mode":
295
+ case "-m":
296
+ parsed.mode = next();
297
+ break;
298
+ case "--skill":
299
+ parsed.skills.push(next());
300
+ break;
301
+ case "--all":
302
+ parsed.agent = "all";
303
+ break;
304
+ case "--force":
305
+ case "-f":
306
+ parsed.force = true;
307
+ break;
308
+ case "--copy":
309
+ parsed.mode = "copy";
310
+ break;
311
+ case "--json":
312
+ parsed.json = true;
313
+ break;
314
+ case "--help":
315
+ case "-h":
316
+ parsed.command = "help";
317
+ break;
318
+ default:
319
+ if (arg.startsWith("-")) {
320
+ throw new Error(`Unknown option ${arg}`);
321
+ }
322
+ parsed.skills.push(arg);
323
+ break;
324
+ }
325
+ }
326
+ return parsed;
327
+ }
328
+
329
+ function printSkillsHelp() {
330
+ console.log(`Qingflow bundled skills
331
+
332
+ Usage:
333
+ qingflow-skills list [--json]
334
+ qingflow-skills install [--agent codex|claude|claude-code|cursor|generic|all] [--scope user|project] [--mode symlink|copy] [--skill name] [--force] [--json]
335
+
336
+ Examples:
337
+ qingflow-skills list
338
+ qingflow-skills install --agent codex --scope user
339
+ qingflow-skills install --agent claude-code --scope project --copy
340
+ qingflow-skills install --agent all --scope project
341
+
342
+ Defaults:
343
+ --agent codex
344
+ --scope user
345
+ --mode symlink
346
+
347
+ Existing skills are not overwritten unless --force is provided.`);
348
+ }
349
+
350
+ function printInstallSummary(result) {
351
+ console.log(`[qingflow-skills] destination: ${result.destination}`);
352
+ if (result.installed.length) {
353
+ console.log(`[qingflow-skills] installed: ${result.installed.join(", ")}`);
354
+ }
355
+ if (result.unchanged.length) {
356
+ console.log(`[qingflow-skills] unchanged: ${result.unchanged.join(", ")}`);
357
+ }
358
+ if (result.conflicts.length) {
359
+ console.log(`[qingflow-skills] conflicts: ${result.conflicts.join(", ")}`);
360
+ console.log("[qingflow-skills] Re-run with --force to replace conflicting skills.");
361
+ }
362
+ }
363
+
364
+ export function runSkillsCli(packageRoot, args) {
365
+ let parsed;
366
+ try {
367
+ parsed = parseSkillsCliArgs(args);
368
+ if (parsed.command === "help") {
369
+ printSkillsHelp();
370
+ return;
371
+ }
372
+ if (parsed.command === "list") {
373
+ const skills = listBundledSkills(packageRoot);
374
+ if (parsed.json) {
375
+ console.log(JSON.stringify({ skills }, null, 2));
376
+ } else {
377
+ for (const skill of skills) {
378
+ console.log(skill);
379
+ }
380
+ }
381
+ return;
382
+ }
383
+ if (parsed.command !== "install") {
384
+ throw new Error(`Unknown qingflow skills command '${parsed.command}'.`);
385
+ }
386
+
387
+ const agents = parsed.agent === "all" ? ["codex", "claude-code", "cursor", "generic"] : [parsed.agent];
388
+ const results = agents.map((agent) =>
389
+ installBundledSkills(packageRoot, {
390
+ agent,
391
+ scope: parsed.scope,
392
+ mode: parsed.mode,
393
+ force: parsed.force,
394
+ skills: parsed.skills,
395
+ }),
396
+ );
397
+ if (parsed.json) {
398
+ console.log(JSON.stringify({ results }, null, 2));
399
+ } else {
400
+ for (const result of results) {
401
+ printInstallSummary(result);
402
+ }
403
+ }
404
+ if (results.some((result) => result.conflicts.length)) {
405
+ process.exit(2);
406
+ }
407
+ } catch (error) {
408
+ if (parsed?.json) {
409
+ console.error(JSON.stringify({ error: error.message }, null, 2));
410
+ } else {
411
+ console.error(`[qingflow-skills] ${error.message}`);
412
+ console.error("Run 'qingflow-skills --help' for usage.");
413
+ }
414
+ process.exit(1);
415
+ }
69
416
  }
70
417
 
71
418
  export function getVenvDir(packageRoot) {
@@ -189,6 +536,17 @@ function getVenvPip(packageRoot) {
189
536
  : path.join(getVenvDir(packageRoot), "bin", "pip");
190
537
  }
191
538
 
539
+ function findOfflineProjectWheel(findLinksDir) {
540
+ const wheels = fs
541
+ .readdirSync(findLinksDir)
542
+ .filter((name) => /^qingflow_mcp-.*\.whl$/i.test(name))
543
+ .sort();
544
+ if (wheels.length === 0) {
545
+ throw new Error(`Offline install expected a qingflow_mcp wheel in ${findLinksDir}`);
546
+ }
547
+ return path.join(findLinksDir, wheels[wheels.length - 1]);
548
+ }
549
+
192
550
  export function findPython() {
193
551
  const preferred = process.env.QINGFLOW_MCP_PYTHON?.trim();
194
552
  const candidates = preferred
@@ -237,7 +595,20 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
237
595
  }
238
596
 
239
597
  const pip = getVenvPip(packageRoot);
240
- runChecked(pip, ["install", "--disable-pip-version-check", "."], { cwd: packageRoot });
598
+ const pipArgs = ["install", "--disable-pip-version-check"];
599
+ const offlineFindLinks = process.env.QINGFLOW_MCP_PIP_FIND_LINKS?.trim();
600
+ if (process.env.QINGFLOW_MCP_PIP_NO_INDEX === "1") {
601
+ pipArgs.push("--no-index");
602
+ }
603
+ if (offlineFindLinks) {
604
+ pipArgs.push("--find-links", offlineFindLinks);
605
+ }
606
+ if (process.env.QINGFLOW_MCP_PIP_NO_INDEX === "1" && offlineFindLinks) {
607
+ pipArgs.push(findOfflineProjectWheel(offlineFindLinks));
608
+ } else {
609
+ pipArgs.push(".");
610
+ }
611
+ runChecked(pip, pipArgs, { cwd: packageRoot });
241
612
 
242
613
  fs.writeFileSync(
243
614
  stampPath,
@@ -6,9 +6,14 @@ try {
6
6
  console.log("[qingflow-mcp] Bootstrapping Python runtime...");
7
7
  ensurePythonEnv(packageRoot, { commandName: "qingflow" });
8
8
  console.log("[qingflow-mcp] Python runtime is ready.");
9
- const skills = installBundledSkills(packageRoot);
9
+ const skills = installBundledSkills(packageRoot, { force: true });
10
10
  if (!skills.skipped) {
11
- console.log(`[qingflow-mcp] Installed skills to ${skills.destination}: ${skills.installed.join(", ")}`);
11
+ const changed = [
12
+ skills.installed.length ? `installed=${skills.installed.join(",")}` : "",
13
+ skills.unchanged.length ? `unchanged=${skills.unchanged.join(",")}` : "",
14
+ skills.conflicts.length ? `conflicts=${skills.conflicts.join(",")}` : "",
15
+ ].filter(Boolean).join(" ");
16
+ console.log(`[qingflow-mcp] Installed skills to ${skills.destination}: ${changed || "none"}`);
12
17
  }
13
18
  } catch (error) {
14
19
  console.error(`[qingflow-mcp] postinstall failed: ${error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "1.0.11",
3
+ "version": "1.1.2",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.0.11"
7
+ version = "1.1.2"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"