@josephyan/qingflow-cli 1.1.1 → 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.
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@1.1.1
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.1.1 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@1.1.2 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
@@ -12,7 +12,9 @@ const PKG_BY_BIN = {
12
12
  function resolvePackageModule(metaUrl, ...segments) {
13
13
  const scriptPath = fileURLToPath(metaUrl);
14
14
  const scriptDir = path.dirname(scriptPath);
15
- const direct = path.join(scriptDir, ...segments);
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);
16
18
  if (fs.existsSync(direct)) {
17
19
  return pathToFileURL(direct).href;
18
20
  }
@@ -31,6 +33,11 @@ function resolvePackageModule(metaUrl, ...segments) {
31
33
  throw new Error(`Cannot locate ${segments.join("/")} from ${scriptPath}`);
32
34
  }
33
35
 
34
- const { getPackageRoot, spawnServer } = await import(resolvePackageModule(import.meta.url, "lib", "runtime.mjs"));
36
+ const { getPackageRoot, spawnServer, runSkillsCli } = await import(resolvePackageModule(import.meta.url, "lib", "runtime.mjs"));
35
37
  const packageRoot = getPackageRoot(import.meta.url);
36
- 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
+ }
@@ -60,29 +60,359 @@ export function getCodexHome() {
60
60
  return path.join(home, ".codex");
61
61
  }
62
62
 
63
- 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) {
64
103
  const skillsSrc = path.join(packageRoot, "skills");
65
104
  if (!fs.existsSync(skillsSrc)) {
66
- return { installed: [], skipped: true, destination: null };
105
+ return [];
67
106
  }
68
107
 
69
- const codexHome = getCodexHome();
70
- const skillsDestRoot = path.join(codexHome, "skills");
71
- 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
+ }
72
122
 
73
- const installed = [];
74
- for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
75
- if (!entry.isDirectory()) {
76
- 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" };
77
138
  }
78
- const src = path.join(skillsSrc, entry.name);
79
- const dest = path.join(skillsDestRoot, entry.name);
80
139
  fs.rmSync(dest, { recursive: true, force: true });
140
+ }
141
+
142
+ if (mode === "copy") {
81
143
  fs.cpSync(src, dest, { recursive: true });
82
- installed.push(entry.name);
144
+ return { status: "installed" };
145
+ }
146
+
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(", ")}`);
83
220
  }
84
221
 
85
- return { installed, skipped: false, destination: skillsDestRoot };
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
+ }
86
416
  }
87
417
 
88
418
  export function getVenvDir(packageRoot) {
@@ -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.1.1",
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.1.1"
7
+ version = "1.1.2"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -9,6 +9,7 @@ from ..errors import QingflowApiError
9
9
  from ..public_surface import cli_public_tool_spec_from_namespace
10
10
  from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
11
11
  from ..tools.ai_builder_tools import _attach_builder_apply_envelope
12
+ from ..version import get_cli_version, get_cli_version_info
12
13
  from .context import CliContext, build_cli_context
13
14
  from .formatters import emit_json_result, emit_text_result
14
15
  from .commands import register_all_commands
@@ -34,7 +35,10 @@ def build_parser() -> argparse.ArgumentParser:
34
35
  parser = _QingflowArgumentParser(prog="qingflow", description="Qingflow CLI")
35
36
  parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
36
37
  parser.add_argument("--json", action="store_true", help="输出 JSON")
38
+ parser.add_argument("--version", action="store_true", help="输出 Qingflow CLI 版本")
37
39
  subparsers = parser.add_subparsers(dest="command", required=True)
40
+ version_parser = subparsers.add_parser("version", help="输出 Qingflow CLI 版本")
41
+ version_parser.set_defaults(handler=_handle_version, format_hint="version")
38
42
  register_all_commands(subparsers)
39
43
  return parser
40
44
 
@@ -54,6 +58,8 @@ def run(
54
58
  err = stderr or sys.stderr
55
59
  parser = build_parser()
56
60
  normalized_argv = _normalize_global_args(list(argv) if argv is not None else sys.argv[1:])
61
+ if "--version" in normalized_argv:
62
+ return _emit_version(json_mode=_should_force_json_output_argv_for_version(normalized_argv), stdout=out)
57
63
  try:
58
64
  args = parser.parse_args(normalized_argv)
59
65
  except _CliArgumentError as exc:
@@ -76,6 +82,8 @@ def run(
76
82
  setattr(args, "_stdin", sys.stdin)
77
83
  setattr(args, "_stdout_stream", out)
78
84
  setattr(args, "_stderr_stream", err)
85
+ if getattr(args, "command", "") == "version":
86
+ return _emit_version(json_mode=bool(args.json), stdout=out)
79
87
  handler = getattr(args, "handler", None)
80
88
  if handler is None:
81
89
  parser.print_help(out)
@@ -121,6 +129,10 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
121
129
  global_args.append(token)
122
130
  index += 1
123
131
  continue
132
+ if token == "--version":
133
+ global_args.append(token)
134
+ index += 1
135
+ continue
124
136
  if token == "--profile":
125
137
  global_args.append(token)
126
138
  if index + 1 >= len(argv):
@@ -138,6 +150,35 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
138
150
  return global_args + remaining
139
151
 
140
152
 
153
+ def _handle_version(_args: argparse.Namespace, _context: CliContext) -> dict[str, Any]:
154
+ info = get_cli_version_info()
155
+ return {
156
+ "ok": True,
157
+ "status": "success",
158
+ **info,
159
+ }
160
+
161
+
162
+ def _emit_version(*, json_mode: bool, stdout: TextIO) -> int:
163
+ version = get_cli_version()
164
+ if json_mode:
165
+ emit_json_result(
166
+ {
167
+ "ok": True,
168
+ "status": "success",
169
+ **get_cli_version_info(),
170
+ },
171
+ stream=stdout,
172
+ )
173
+ else:
174
+ stdout.write(f"{version}\n")
175
+ return 0
176
+
177
+
178
+ def _should_force_json_output_argv_for_version(argv: list[str]) -> bool:
179
+ return "--json" in argv
180
+
181
+
141
182
  def _should_force_json_output(args: argparse.Namespace) -> bool:
142
183
  if bool(getattr(args, "force_json_output", False)):
143
184
  return True
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shutil
6
+ import sys
7
+ from importlib import metadata
8
+ from pathlib import Path
9
+
10
+
11
+ def get_cli_version() -> str:
12
+ package_json_version = _find_package_json_version()
13
+ if package_json_version:
14
+ return package_json_version
15
+ try:
16
+ return metadata.version("qingflow-mcp")
17
+ except metadata.PackageNotFoundError:
18
+ return "0+local"
19
+
20
+
21
+ def get_cli_version_info() -> dict[str, str | None]:
22
+ package_root = _find_package_root()
23
+ return {
24
+ "version": get_cli_version(),
25
+ "package": _find_package_name(package_root) or "@qingflow-tech/qingflow-cli",
26
+ "executable_path": _resolve_executable_path(),
27
+ "command_path": shutil.which("qingflow"),
28
+ "package_root": str(package_root) if package_root is not None else None,
29
+ "skill_version": _find_skill_version(package_root),
30
+ }
31
+
32
+
33
+ def _find_package_json_version() -> str | None:
34
+ package_root = _find_package_root()
35
+ if package_root is None:
36
+ return None
37
+ try:
38
+ payload = json.loads((package_root / "package.json").read_text(encoding="utf-8"))
39
+ except (OSError, json.JSONDecodeError):
40
+ return None
41
+ version = str(payload.get("version") or "")
42
+ return version or None
43
+
44
+
45
+ def _find_package_root() -> Path | None:
46
+ current = Path(__file__).resolve()
47
+ for parent in current.parents:
48
+ package_json = parent / "package.json"
49
+ if not package_json.exists():
50
+ continue
51
+ try:
52
+ payload = json.loads(package_json.read_text(encoding="utf-8"))
53
+ except (OSError, json.JSONDecodeError):
54
+ continue
55
+ name = str(payload.get("name") or "")
56
+ version = str(payload.get("version") or "")
57
+ if version and name in {
58
+ "qingflow-mcp-workspace",
59
+ "@qingflow-tech/qingflow-cli",
60
+ "@qingflow-tech/qingflow-app-user-mcp",
61
+ "@qingflow-tech/qingflow-app-builder-mcp",
62
+ "@josephyan/qingflow-cli",
63
+ "@josephyan/qingflow-app-user-mcp",
64
+ "@josephyan/qingflow-app-builder-mcp",
65
+ }:
66
+ return parent
67
+ return None
68
+
69
+
70
+ def _find_package_name(package_root: Path | None) -> str | None:
71
+ if package_root is None:
72
+ return None
73
+ try:
74
+ payload = json.loads((package_root / "package.json").read_text(encoding="utf-8"))
75
+ except (OSError, json.JSONDecodeError):
76
+ return None
77
+ name = str(payload.get("name") or "")
78
+ return name or None
79
+
80
+
81
+ def _resolve_executable_path() -> str | None:
82
+ if not sys.argv:
83
+ return None
84
+ raw = str(sys.argv[0] or "").strip()
85
+ if not raw:
86
+ return None
87
+ try:
88
+ return str(Path(raw).resolve())
89
+ except OSError:
90
+ return raw
91
+
92
+
93
+ def _find_skill_version(package_root: Path | None) -> str | None:
94
+ if package_root is None:
95
+ return None
96
+ candidates = [
97
+ package_root / "skills" / "qingflow-cli" / "SKILL.md",
98
+ package_root / "skill" / "qingflow-cli" / "SKILL.md",
99
+ ]
100
+ for skill_file in candidates:
101
+ if not skill_file.exists():
102
+ continue
103
+ try:
104
+ text = skill_file.read_text(encoding="utf-8")
105
+ except OSError:
106
+ continue
107
+ match = re.search(r"Skill\s*版本\**[::]\s*`?([^`\s))]+)", text)
108
+ if match:
109
+ return match.group(1).strip()
110
+ return None