@josephyan/qingflow-app-user-mcp 0.2.0-beta.1000 → 0.2.0-beta.1002

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-user-mcp@0.2.0-beta.1000
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.1002
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.1000 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.1002 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.1000",
3
+ "version": "0.2.0-beta.1002",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory 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.0b1002"
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.0b1002"
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,10 @@ except ImportError: # pragma: no cover - non-POSIX fallback
16
16
 
17
17
  T = TypeVar("T")
18
18
  RAW_TTY_NEWLINE = "\r\n"
19
+ # Give terminal escape sequences a slightly roomier window so arrow keys
20
+ # still parse correctly when an outer Node/npm launcher adds a bit of PTY lag.
21
+ ESCAPE_SEQUENCE_TIMEOUT_SECONDS = 0.2
22
+ ESCAPE_SEQUENCE_FOLLOWUP_TIMEOUT_SECONDS = 0.02
19
23
 
20
24
 
21
25
  @dataclass(slots=True)
@@ -158,16 +162,23 @@ def _truncate_line(text: str, *, width: int) -> str:
158
162
 
159
163
 
160
164
  def _read_key(input_stream: TextIO) -> str:
161
- first = input_stream.read(1)
165
+ fd = input_stream.fileno()
166
+ first_bytes = os.read(fd, 1)
167
+ if not first_bytes:
168
+ return ""
169
+ first = first_bytes.decode("utf-8", errors="ignore")
162
170
  if first != "\x1b":
163
171
  return first
164
172
  chunks = [first]
165
- fd = input_stream.fileno()
166
- while select.select([fd], [], [], 0.02)[0]:
173
+ if not select.select([fd], [], [], ESCAPE_SEQUENCE_TIMEOUT_SECONDS)[0]:
174
+ return first
175
+ while True:
167
176
  chunk = os.read(fd, 1).decode("utf-8", errors="ignore")
168
177
  if not chunk:
169
178
  break
170
179
  chunks.append(chunk)
171
180
  if len(chunks) >= 3:
172
181
  break
182
+ if not select.select([fd], [], [], ESCAPE_SEQUENCE_FOLLOWUP_TIMEOUT_SECONDS)[0]:
183
+ break
173
184
  return "".join(chunks)