@josephyan/qingflow-app-builder-mcp 0.2.0-beta.1000 → 0.2.0-beta.1001

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-app-builder-mcp@0.2.0-beta.1000
6
+ npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1001
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1000 qingflow-app-builder-mcp
12
+ npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1001 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-builder-mcp",
3
- "version": "0.2.0-beta.1000",
3
+ "version": "0.2.0-beta.1001",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution workflows.",
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 = "0.2.0b1000"
7
+ version = "0.2.0b1001"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b1000"
8
+ _FALLBACK_VERSION = "0.2.0b1001"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -6,6 +6,8 @@ import json
6
6
  from ..context import CliContext
7
7
  from ..terminal_ui import SelectionOption, select_option
8
8
 
9
+ _INTERACTIVE_SELECTION_UNAVAILABLE = object()
10
+
9
11
 
10
12
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
11
13
  parser = subparsers.add_parser("workspace", help="工作区")
@@ -23,7 +25,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
23
25
 
24
26
  select_parser = workspace_subparsers.add_parser("select", help="切换当前工作区")
25
27
  select_parser.add_argument("--ws-id", type=int, default=0, help="不传时在交互终端中选择工作区")
26
- select_parser.set_defaults(handler=_handle_select, format_hint="workspace_get")
28
+ select_parser.set_defaults(handler=_handle_select, format_hint="workspace_select")
27
29
 
28
30
 
29
31
  def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
@@ -45,7 +47,7 @@ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
45
47
  def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
46
48
  if int(args.ws_id or 0) <= 0:
47
49
  selected_ws_id = _choose_workspace_interactively(args, context)
48
- if selected_ws_id is None:
50
+ if selected_ws_id is _INTERACTIVE_SELECTION_UNAVAILABLE:
49
51
  raise RuntimeError(
50
52
  json.dumps(
51
53
  {
@@ -55,6 +57,8 @@ def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
55
57
  ensure_ascii=False,
56
58
  )
57
59
  )
60
+ if selected_ws_id is None:
61
+ return {"status": "cancelled", "message": "已取消"}
58
62
  args.ws_id = selected_ws_id
59
63
  return context.workspace.workspace_select(
60
64
  profile=args.profile,
@@ -62,13 +66,13 @@ def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
62
66
  )
63
67
 
64
68
 
65
- def _choose_workspace_interactively(args: argparse.Namespace, context: CliContext) -> int | None:
69
+ def _choose_workspace_interactively(args: argparse.Namespace, context: CliContext) -> int | None | object:
66
70
  input_stream = getattr(args, "_stdin", None)
67
71
  output_stream = getattr(args, "_stderr_stream", None)
68
72
  if input_stream is None or output_stream is None:
69
- return None
73
+ return _INTERACTIVE_SELECTION_UNAVAILABLE
70
74
  if not bool(getattr(input_stream, "isatty", lambda: False)()):
71
- return None
75
+ return _INTERACTIVE_SELECTION_UNAVAILABLE
72
76
  page = context.workspace.workspace_list(
73
77
  profile=args.profile,
74
78
  page_num=1,
@@ -77,7 +81,7 @@ def _choose_workspace_interactively(args: argparse.Namespace, context: CliContex
77
81
  ).get("page")
78
82
  items = page.get("list") if isinstance(page, dict) and isinstance(page.get("list"), list) else []
79
83
  if not items:
80
- return None
84
+ return _INTERACTIVE_SELECTION_UNAVAILABLE
81
85
  current_ws_id = None
82
86
  sessions = getattr(context, "sessions", None)
83
87
  if sessions is not None and hasattr(sessions, "get_profile"):
@@ -106,6 +106,21 @@ def _format_workspace_get(result: dict[str, Any]) -> str:
106
106
  return "\n".join(lines) + "\n"
107
107
 
108
108
 
109
+ def _format_workspace_select(result: dict[str, Any]) -> str:
110
+ status = str(result.get("status") or "").lower()
111
+ if status == "cancelled":
112
+ return str(result.get("message") or "已取消") + "\n"
113
+
114
+ workspace = result.get("workspace") if isinstance(result.get("workspace"), dict) else {}
115
+ workspace_name = workspace.get("workspaceName") or workspace.get("wsName") or result.get("selected", {}).get("workspace_name") if isinstance(result.get("selected"), dict) else None
116
+ lines = [
117
+ f"已切换到: {workspace_name or '-'} ({workspace.get('wsId') or result.get('ws_id') or '-'})",
118
+ f"QF Version: {result.get('qf_version') or workspace.get('systemVersion') or '-'}",
119
+ ]
120
+ _append_warnings(lines, result.get("warnings"))
121
+ return "\n".join(lines) + "\n"
122
+
123
+
109
124
  def _format_app_items(result: dict[str, Any]) -> str:
110
125
  items = result.get("items")
111
126
  if not isinstance(items, list):
@@ -559,6 +574,7 @@ _FORMATTERS = {
559
574
  "auth_whoami": _format_whoami,
560
575
  "workspace_list": _format_workspace_list,
561
576
  "workspace_get": _format_workspace_get,
577
+ "workspace_select": _format_workspace_select,
562
578
  "app_list": _format_app_items,
563
579
  "app_search": _format_app_items,
564
580
  "app_get": _format_app_get,
@@ -16,6 +16,8 @@ except ImportError: # pragma: no cover - non-POSIX fallback
16
16
 
17
17
  T = TypeVar("T")
18
18
  RAW_TTY_NEWLINE = "\r\n"
19
+ ESCAPE_SEQUENCE_TIMEOUT_SECONDS = 0.05
20
+ ESCAPE_SEQUENCE_FOLLOWUP_TIMEOUT_SECONDS = 0.005
19
21
 
20
22
 
21
23
  @dataclass(slots=True)
@@ -158,16 +160,23 @@ def _truncate_line(text: str, *, width: int) -> str:
158
160
 
159
161
 
160
162
  def _read_key(input_stream: TextIO) -> str:
161
- first = input_stream.read(1)
163
+ fd = input_stream.fileno()
164
+ first_bytes = os.read(fd, 1)
165
+ if not first_bytes:
166
+ return ""
167
+ first = first_bytes.decode("utf-8", errors="ignore")
162
168
  if first != "\x1b":
163
169
  return first
164
170
  chunks = [first]
165
- fd = input_stream.fileno()
166
- while select.select([fd], [], [], 0.02)[0]:
171
+ if not select.select([fd], [], [], ESCAPE_SEQUENCE_TIMEOUT_SECONDS)[0]:
172
+ return first
173
+ while True:
167
174
  chunk = os.read(fd, 1).decode("utf-8", errors="ignore")
168
175
  if not chunk:
169
176
  break
170
177
  chunks.append(chunk)
171
178
  if len(chunks) >= 3:
172
179
  break
180
+ if not select.select([fd], [], [], ESCAPE_SEQUENCE_FOLLOWUP_TIMEOUT_SECONDS)[0]:
181
+ break
173
182
  return "".join(chunks)