@qingflow-tech/qingflow-app-builder-mcp 1.0.2 → 1.0.4

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 (53) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +9 -3
  3. package/npm/lib/runtime.mjs +10 -3
  4. package/package.json +1 -1
  5. package/pyproject.toml +1 -1
  6. package/skills/qingflow-app-builder/SKILL.md +88 -184
  7. package/skills/qingflow-app-builder/references/create-app.md +15 -34
  8. package/skills/qingflow-app-builder/references/gotchas.md +3 -3
  9. package/skills/qingflow-app-builder/references/solution-playbooks.md +1 -2
  10. package/skills/qingflow-app-builder/references/tool-selection.md +9 -10
  11. package/src/qingflow_mcp/__init__.py +33 -1
  12. package/src/qingflow_mcp/backend_client.py +109 -0
  13. package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +58 -9
  15. package/src/qingflow_mcp/builder_facade/service.py +1711 -240
  16. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  17. package/src/qingflow_mcp/cli/commands/app.py +47 -1
  18. package/src/qingflow_mcp/cli/commands/auth.py +63 -0
  19. package/src/qingflow_mcp/cli/commands/builder.py +11 -3
  20. package/src/qingflow_mcp/cli/commands/exports.py +111 -0
  21. package/src/qingflow_mcp/cli/commands/record.py +5 -5
  22. package/src/qingflow_mcp/cli/commands/task.py +701 -27
  23. package/src/qingflow_mcp/cli/commands/workspace.py +84 -0
  24. package/src/qingflow_mcp/cli/context.py +3 -0
  25. package/src/qingflow_mcp/cli/formatters.py +424 -50
  26. package/src/qingflow_mcp/cli/interaction.py +72 -0
  27. package/src/qingflow_mcp/cli/main.py +11 -1
  28. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  29. package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
  30. package/src/qingflow_mcp/config.py +1 -1
  31. package/src/qingflow_mcp/errors.py +4 -4
  32. package/src/qingflow_mcp/export_store.py +14 -0
  33. package/src/qingflow_mcp/id_utils.py +49 -0
  34. package/src/qingflow_mcp/public_surface.py +16 -1
  35. package/src/qingflow_mcp/response_trim.py +394 -9
  36. package/src/qingflow_mcp/server.py +26 -0
  37. package/src/qingflow_mcp/server_app_builder.py +15 -1
  38. package/src/qingflow_mcp/server_app_user.py +113 -0
  39. package/src/qingflow_mcp/session_store.py +126 -21
  40. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  41. package/src/qingflow_mcp/solution/executor.py +2 -2
  42. package/src/qingflow_mcp/tools/ai_builder_tools.py +107 -34
  43. package/src/qingflow_mcp/tools/app_tools.py +1 -0
  44. package/src/qingflow_mcp/tools/auth_tools.py +243 -9
  45. package/src/qingflow_mcp/tools/base.py +6 -2
  46. package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
  47. package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
  48. package/src/qingflow_mcp/tools/export_tools.py +1565 -0
  49. package/src/qingflow_mcp/tools/import_tools.py +78 -4
  50. package/src/qingflow_mcp/tools/record_tools.py +551 -165
  51. package/src/qingflow_mcp/tools/resource_read_tools.py +154 -33
  52. package/src/qingflow_mcp/tools/task_context_tools.py +917 -141
  53. package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
@@ -44,6 +44,9 @@ def run(
44
44
  args = parser.parse_args(normalized_argv)
45
45
  except SystemExit as exc:
46
46
  return int(exc.code or 0)
47
+ setattr(args, "_stdin", sys.stdin)
48
+ setattr(args, "_stdout_stream", out)
49
+ setattr(args, "_stderr_stream", err)
47
50
  handler = getattr(args, "handler", None)
48
51
  if handler is None:
49
52
  parser.print_help(out)
@@ -53,6 +56,8 @@ def run(
53
56
  if not bool(args.json):
54
57
  _emit_cli_effective_context_notice(args, context, stream=err)
55
58
  result = handler(args, context)
59
+ except SystemExit as exc:
60
+ return int(exc.code or 0)
56
61
  except RuntimeError as exc:
57
62
  payload = trim_error_response(_parse_error_payload(exc))
58
63
  return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
@@ -152,6 +157,7 @@ def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliCon
152
157
  spec = cli_public_tool_spec_from_namespace(args)
153
158
  if spec is None or not spec.cli_show_effective_context:
154
159
  return
160
+ hide_context_line = bool(getattr(args, "hide_effective_context_line", False))
155
161
  sessions = getattr(context, "sessions", None)
156
162
  if sessions is None or not hasattr(sessions, "get_profile"):
157
163
  return
@@ -168,9 +174,13 @@ def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliCon
168
174
  workspace_label = f"{workspace_name} ({workspace_id})"
169
175
  else:
170
176
  workspace_label = str(workspace_id)
171
- lines = [f"Context: profile={profile_name} workspace={workspace_label}"]
177
+ lines: list[str] = []
178
+ if not hide_context_line:
179
+ lines.append(f"Context: profile={profile_name} workspace={workspace_label}")
172
180
  if spec.cli_context_write and profile_name == "default":
173
181
  lines.append("Warning: using default profile for a workspace-sensitive write command")
182
+ if not lines:
183
+ return
174
184
  stream.write("\n".join(lines) + "\n")
175
185
 
176
186
 
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from Crypto.Cipher import PKCS1_v1_5
8
+ from Crypto.PublicKey import RSA
9
+
10
+ from ..backend_client import BackendClient
11
+ from ..config import get_default_base_url, get_timeout_seconds, normalize_base_url
12
+ from ..errors import QingflowApiError
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class QingflowNativeLoginResult:
17
+ token: str
18
+ user_info: dict[str, Any]
19
+ login_token: str | None = None
20
+ flow: str = "qingflow_password"
21
+
22
+
23
+ class QingflowNativeLoginHelper:
24
+ def __init__(self, *, backend: BackendClient | None = None) -> None:
25
+ self._owns_backend = backend is None
26
+ self._backend = backend or BackendClient(timeout=get_timeout_seconds())
27
+
28
+ def close(self) -> None:
29
+ if self._owns_backend:
30
+ self._backend.close()
31
+
32
+ def login_with_password(
33
+ self,
34
+ *,
35
+ base_url: str | None,
36
+ email: str,
37
+ password: str,
38
+ ) -> QingflowNativeLoginResult:
39
+ normalized_base_url = normalize_base_url(base_url) or get_default_base_url()
40
+ normalized_email = str(email or "").strip()
41
+ normalized_password = str(password or "")
42
+ if not normalized_base_url:
43
+ raise QingflowApiError.config_error("base_url is required or configure default_base_url")
44
+ if not normalized_email:
45
+ raise QingflowApiError.config_error("email is required for Qingflow account login")
46
+ if not normalized_password:
47
+ raise QingflowApiError.config_error("password is required for Qingflow account login")
48
+
49
+ pubkey_payload = self._backend.public_request("GET", normalized_base_url, "/user/pubkey", qf_version=None)
50
+ pubkey = self._extract_pubkey(pubkey_payload)
51
+ encrypted_password = _encrypt_password(normalized_password, pubkey)
52
+ login_payload = self._backend.public_request(
53
+ "POST",
54
+ normalized_base_url,
55
+ "/user/login",
56
+ json_body={"email": normalized_email, "password": encrypted_password},
57
+ qf_version=None,
58
+ )
59
+ if not isinstance(login_payload, dict):
60
+ raise QingflowApiError(category="auth", message="Qingflow login did not return a valid response")
61
+
62
+ token = str(login_payload.get("token") or "").strip()
63
+ login_token = str(login_payload.get("loginToken") or "").strip() or None
64
+ if not token:
65
+ if login_token:
66
+ raise QingflowApiError(
67
+ category="auth",
68
+ message=(
69
+ "Qingflow account login requires additional security verification. "
70
+ "CLI password login currently does not complete the loginToken verification step."
71
+ ),
72
+ details={"login_token_present": True},
73
+ )
74
+ raise QingflowApiError(
75
+ category="auth",
76
+ message="Qingflow login succeeded but did not return a token",
77
+ )
78
+
79
+ user_info = login_payload.get("userInfo")
80
+ if not isinstance(user_info, dict):
81
+ user_info = {}
82
+ return QingflowNativeLoginResult(
83
+ token=token,
84
+ login_token=login_token,
85
+ user_info=user_info,
86
+ )
87
+
88
+ def _extract_pubkey(self, payload: Any) -> str:
89
+ if not isinstance(payload, dict):
90
+ raise QingflowApiError(category="auth", message="Qingflow pubkey response is invalid")
91
+ pubkey = str(payload.get("pubkey") or "").strip()
92
+ if not pubkey:
93
+ raise QingflowApiError(category="auth", message="Qingflow pubkey response did not include pubkey")
94
+ return pubkey
95
+
96
+
97
+ def login_with_qingflow_password(
98
+ *,
99
+ base_url: str | None,
100
+ email: str,
101
+ password: str,
102
+ ) -> QingflowNativeLoginResult:
103
+ helper = QingflowNativeLoginHelper()
104
+ try:
105
+ return helper.login_with_password(base_url=base_url, email=email, password=password)
106
+ finally:
107
+ helper.close()
108
+
109
+
110
+ def _encrypt_password(password: str, pubkey: str) -> str:
111
+ public_key = RSA.import_key(
112
+ "-----BEGIN PUBLIC KEY-----\n" + pubkey.strip() + "\n-----END PUBLIC KEY-----\n"
113
+ )
114
+ cipher = PKCS1_v1_5.new(public_key)
115
+ encrypted = cipher.encrypt(password.encode("utf-8"))
116
+ return base64.b64encode(encrypted).decode("ascii")
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import select
5
+ import shutil
6
+ import textwrap
7
+ from dataclasses import dataclass
8
+ from typing import Generic, Sequence, TextIO, TypeVar
9
+
10
+ try:
11
+ import termios
12
+ import tty
13
+ except ImportError: # pragma: no cover - non-POSIX fallback
14
+ termios = None
15
+ tty = None
16
+
17
+
18
+ T = TypeVar("T")
19
+ RAW_TTY_NEWLINE = "\r\n"
20
+ # Give terminal escape sequences a slightly roomier window so arrow keys
21
+ # still parse correctly when an outer Node/npm launcher adds a bit of PTY lag.
22
+ ESCAPE_SEQUENCE_TIMEOUT_SECONDS = 0.2
23
+ ESCAPE_SEQUENCE_FOLLOWUP_TIMEOUT_SECONDS = 0.02
24
+
25
+
26
+ @dataclass(slots=True)
27
+ class SelectionOption(Generic[T]):
28
+ value: T
29
+ label: str
30
+ hint: str | None = None
31
+
32
+
33
+ def select_option(
34
+ *,
35
+ title: str,
36
+ options: Sequence[SelectionOption[T]],
37
+ input_stream: TextIO,
38
+ output_stream: TextIO,
39
+ page_size: int = 8,
40
+ ) -> T | None:
41
+ if not options:
42
+ return None
43
+ if len(options) == 1:
44
+ return options[0].value
45
+ if not _supports_raw_selection(input_stream=input_stream, output_stream=output_stream):
46
+ return _select_option_via_prompt(title=title, options=options, input_stream=input_stream, output_stream=output_stream)
47
+ return _select_option_via_raw_terminal(
48
+ title=title,
49
+ options=options,
50
+ input_stream=input_stream,
51
+ output_stream=output_stream,
52
+ page_size=max(3, page_size),
53
+ )
54
+
55
+
56
+ def _supports_raw_selection(*, input_stream: TextIO, output_stream: TextIO) -> bool:
57
+ if termios is None or tty is None:
58
+ return False
59
+ if not bool(getattr(input_stream, "isatty", lambda: False)()) or not bool(getattr(output_stream, "isatty", lambda: False)()):
60
+ return False
61
+ return hasattr(input_stream, "fileno")
62
+
63
+
64
+ def _select_option_via_prompt(
65
+ *,
66
+ title: str,
67
+ options: Sequence[SelectionOption[T]],
68
+ input_stream: TextIO,
69
+ output_stream: TextIO,
70
+ ) -> T | None:
71
+ output_stream.write(title + "\n")
72
+ for index, option in enumerate(options, start=1):
73
+ suffix = f" ({option.hint})" if option.hint else ""
74
+ output_stream.write(f"{index}. {option.label}{suffix}\n")
75
+ output_stream.write("请输入编号并回车,留空取消: ")
76
+ output_stream.flush()
77
+ line = input_stream.readline()
78
+ selected = str(line or "").strip()
79
+ if not selected:
80
+ return None
81
+ if not selected.isdigit():
82
+ return None
83
+ index = int(selected) - 1
84
+ if index < 0 or index >= len(options):
85
+ return None
86
+ return options[index].value
87
+
88
+
89
+ def _select_option_via_raw_terminal(
90
+ *,
91
+ title: str,
92
+ options: Sequence[SelectionOption[T]],
93
+ input_stream: TextIO,
94
+ output_stream: TextIO,
95
+ page_size: int,
96
+ ) -> T | None:
97
+ fd = input_stream.fileno()
98
+ original_mode = termios.tcgetattr(fd)
99
+ selected_index = 0
100
+ output_stream.write("\x1b[?1049h\x1b[?25l")
101
+ output_stream.flush()
102
+ try:
103
+ tty.setraw(fd)
104
+ while True:
105
+ _render_options(
106
+ title=title,
107
+ options=options,
108
+ selected_index=selected_index,
109
+ output_stream=output_stream,
110
+ page_size=page_size,
111
+ )
112
+ key = _read_key(input_stream)
113
+ if key in ("\r", "\n"):
114
+ return options[selected_index].value
115
+ if key in ("\x03", "\x1b", "q", "Q"):
116
+ return None
117
+ if key in ("\x1b[A", "k", "K"):
118
+ selected_index = (selected_index - 1) % len(options)
119
+ continue
120
+ if key in ("\x1b[B", "j", "J"):
121
+ selected_index = (selected_index + 1) % len(options)
122
+ finally:
123
+ termios.tcsetattr(fd, termios.TCSADRAIN, original_mode)
124
+ output_stream.write("\x1b[?25h\x1b[?1049l")
125
+ output_stream.flush()
126
+
127
+
128
+ def _render_options(
129
+ *,
130
+ title: str,
131
+ options: Sequence[SelectionOption[object]],
132
+ selected_index: int,
133
+ output_stream: TextIO,
134
+ page_size: int,
135
+ ) -> None:
136
+ terminal_width = shutil.get_terminal_size((100, 20)).columns
137
+ total = len(options)
138
+ page_start = max(0, min(selected_index - page_size // 2, max(total - page_size, 0)))
139
+ visible = options[page_start: page_start + page_size]
140
+ lines = _render_multiline_text(title, width=terminal_width)
141
+ lines.extend(
142
+ [
143
+ "↑/↓ 或 j/k 选择,Enter 确认,q / Esc 取消",
144
+ "",
145
+ ]
146
+ )
147
+ for offset, option in enumerate(visible, start=page_start):
148
+ marker = ">" if offset == selected_index else " "
149
+ suffix = f" [{option.hint}]" if option.hint else ""
150
+ lines.append(_truncate_line(f"{marker} {option.label}{suffix}", width=terminal_width))
151
+ if total > page_size:
152
+ lines.append("")
153
+ lines.append(f"{selected_index + 1}/{total}")
154
+ output_stream.write("\x1b[2J\x1b[H")
155
+ output_stream.write(RAW_TTY_NEWLINE.join(lines))
156
+ output_stream.flush()
157
+
158
+
159
+ def _truncate_line(text: str, *, width: int) -> str:
160
+ if width <= 0 or len(text) <= width:
161
+ return text
162
+ if width <= 1:
163
+ return text[:width]
164
+ return text[: width - 1] + "…"
165
+
166
+
167
+ def _render_multiline_text(text: str, *, width: int) -> list[str]:
168
+ parts = text.splitlines() or [text]
169
+ rendered: list[str] = []
170
+ wrap_width = max(1, width)
171
+ for part in parts:
172
+ if not part:
173
+ rendered.append("")
174
+ continue
175
+ initial_indent = ""
176
+ subsequent_indent = ""
177
+ stripped = part.lstrip()
178
+ if stripped.startswith("- "):
179
+ leading_spaces = len(part) - len(stripped)
180
+ initial_indent = part[:leading_spaces] + "- "
181
+ subsequent_indent = part[:leading_spaces] + " "
182
+ content = stripped[2:]
183
+ else:
184
+ content = part
185
+ wrapped = textwrap.wrap(
186
+ content,
187
+ width=wrap_width,
188
+ initial_indent=initial_indent,
189
+ subsequent_indent=subsequent_indent,
190
+ replace_whitespace=False,
191
+ drop_whitespace=False,
192
+ break_long_words=True,
193
+ )
194
+ rendered.extend(wrapped or [""])
195
+ return rendered
196
+
197
+
198
+ def _read_key(input_stream: TextIO) -> str:
199
+ fd = input_stream.fileno()
200
+ first_bytes = os.read(fd, 1)
201
+ if not first_bytes:
202
+ return ""
203
+ first = first_bytes.decode("utf-8", errors="ignore")
204
+ if first != "\x1b":
205
+ return first
206
+ chunks = [first]
207
+ if not select.select([fd], [], [], ESCAPE_SEQUENCE_TIMEOUT_SECONDS)[0]:
208
+ return first
209
+ while True:
210
+ chunk = os.read(fd, 1).decode("utf-8", errors="ignore")
211
+ if not chunk:
212
+ break
213
+ chunks.append(chunk)
214
+ if len(chunks) >= 3:
215
+ break
216
+ if not select.select([fd], [], [], ESCAPE_SEQUENCE_FOLLOWUP_TIMEOUT_SECONDS)[0]:
217
+ break
218
+ return "".join(chunks)
@@ -22,7 +22,7 @@ DEFAULT_REPOSITORY_AUTHOR_NAME = "qingflow-mcp"
22
22
  DEFAULT_REPOSITORY_AUTHOR_EMAIL = "qingflow-mcp@local.invalid"
23
23
  DEFAULT_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY = "tokenKey"
24
24
  DEFAULT_CREDIT_USAGE_RECORD_PATH = "/user/credit/usage"
25
- DEFAULT_MCPORTER_CONFIG_PATH = "/root/.openclaw/workspace/config/mcporter.json"
25
+ DEFAULT_MCPORTER_CONFIG_PATH = "~/.openclaw/workspace/config/mcporter.json"
26
26
 
27
27
 
28
28
  def get_mcp_home() -> Path:
@@ -43,19 +43,19 @@ class QingflowApiError(Exception):
43
43
  def auth_required(cls, profile: str) -> "QingflowApiError":
44
44
  return cls(
45
45
  category="auth",
46
- message=f"Profile '{profile}' is not logged in. Run auth_use_credential first.",
46
+ message=f"Profile '{profile}' is not logged in. Run auth login or auth_use_credential first.",
47
47
  )
48
48
 
49
49
  @classmethod
50
50
  def workspace_not_selected(cls, profile: str) -> "QingflowApiError":
51
51
  return cls(
52
52
  category="workspace",
53
- message=f"WORKSPACE_NOT_SELECTED: profile '{profile}' has no workspace from auth context. Re-run auth_use_credential.",
53
+ message=f"WORKSPACE_NOT_SELECTED: profile '{profile}' has no workspace from auth context. Re-run auth login or auth_use_credential.",
54
54
  )
55
55
 
56
56
  @classmethod
57
- def config_error(cls, message: str) -> "QingflowApiError":
58
- return cls(category="config", message=message)
57
+ def config_error(cls, message: str, *, details: JSONObject | None = None) -> "QingflowApiError":
58
+ return cls(category="config", message=message, details=details)
59
59
 
60
60
  @classmethod
61
61
  def not_supported(cls, message: str) -> "QingflowApiError":
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import timedelta
4
+ from pathlib import Path
5
+
6
+ from .import_store import _JsonEntryStore, _store_dir
7
+
8
+
9
+ class ExportJobStore(_JsonEntryStore):
10
+ def __init__(self, base_dir: Path | None = None, *, ttl_seconds: int = 24 * 3600) -> None:
11
+ super().__init__(
12
+ base_dir=base_dir or _store_dir("QINGFLOW_MCP_EXPORT_JOB_HOME", "export-jobs"),
13
+ ttl=timedelta(seconds=ttl_seconds),
14
+ )
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .errors import QingflowApiError
6
+
7
+
8
+ JS_MAX_SAFE_INTEGER = 9_007_199_254_740_991
9
+
10
+
11
+ def stringify_backend_id(value: Any) -> str | None:
12
+ """Return an exact public id string for backend-originated identifiers."""
13
+ if value in (None, ""):
14
+ return None
15
+ if isinstance(value, bool):
16
+ return None
17
+ text = str(value).strip()
18
+ return text or None
19
+
20
+
21
+ def normalize_positive_id_text(value: Any, *, field_name: str) -> str:
22
+ """Normalize a user-supplied id while rejecting JS-unsafe numeric input."""
23
+ if value in (None, "") or isinstance(value, bool):
24
+ raise QingflowApiError.config_error(f"{field_name} must be positive")
25
+ if isinstance(value, int):
26
+ if value <= 0:
27
+ raise QingflowApiError.config_error(f"{field_name} must be positive")
28
+ if value > JS_MAX_SAFE_INTEGER:
29
+ raise QingflowApiError.config_error(
30
+ f"{field_name} exceeds JavaScript's safe integer range; pass it as a string to avoid precision loss"
31
+ )
32
+ return str(value)
33
+ if isinstance(value, str):
34
+ text = value.strip()
35
+ if not text.isdecimal() or int(text) <= 0:
36
+ raise QingflowApiError.config_error(f"{field_name} must be a positive integer string")
37
+ return text
38
+ raise QingflowApiError.config_error(f"{field_name} must be a positive integer string")
39
+
40
+
41
+ def normalize_positive_id_int(value: Any, *, field_name: str) -> int:
42
+ """Normalize an id to Python int after the public boundary preserves it as text."""
43
+ return int(normalize_positive_id_text(value, field_name=field_name))
44
+
45
+
46
+ def ids_equal(left: Any, right: Any) -> bool:
47
+ left_text = stringify_backend_id(left)
48
+ right_text = stringify_backend_id(right)
49
+ return left_text is not None and right_text is not None and left_text == right_text
@@ -30,10 +30,13 @@ def tool_key(domain: str, tool_name: str) -> str:
30
30
 
31
31
 
32
32
  USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
33
+ PublicToolSpec(USER_DOMAIN, "auth_login", cli_route=("auth", "login"), mcp_public=False),
33
34
  PublicToolSpec(USER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("auth", "use-credential")),
34
35
  PublicToolSpec(USER_DOMAIN, "auth_whoami", ("auth_whoami",), ("auth", "whoami")),
35
36
  PublicToolSpec(USER_DOMAIN, "auth_logout", ("auth_logout",), ("auth", "logout")),
36
37
  PublicToolSpec(USER_DOMAIN, "workspace_list", ("workspace_list",), ("workspace", "list")),
38
+ PublicToolSpec(USER_DOMAIN, "workspace_get", ("workspace_get",), ("workspace", "get")),
39
+ PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
37
40
  PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
38
41
  PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
39
42
  PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
@@ -88,11 +91,21 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
88
91
  PublicToolSpec(USER_DOMAIN, "record_import_repair_local", ("record_import_repair_local",), ("import", "repair")),
89
92
  PublicToolSpec(USER_DOMAIN, "record_import_start", ("record_import_start",), ("import", "start")),
90
93
  PublicToolSpec(USER_DOMAIN, "record_import_status_get", ("record_import_status_get",), ("import", "status")),
94
+ PublicToolSpec(USER_DOMAIN, "record_export_start", ("record_export_start",), ("export", "start"), cli_show_effective_context=True),
95
+ PublicToolSpec(USER_DOMAIN, "record_export_status_get", ("record_export_status_get",), ("export", "status"), cli_show_effective_context=True),
96
+ PublicToolSpec(USER_DOMAIN, "record_export_get", ("record_export_get",), ("export", "get"), cli_show_effective_context=True),
97
+ PublicToolSpec(USER_DOMAIN, "record_export_direct", ("record_export_direct",), ("export", "direct"), cli_show_effective_context=True),
91
98
  PublicToolSpec(USER_DOMAIN, "record_code_block_run", ("record_code_block_run",), ("record", "code-block-run"), cli_show_effective_context=True, cli_context_write=True),
92
99
  PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list"), cli_show_effective_context=True),
93
100
  PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get"), cli_show_effective_context=True),
94
101
  PublicToolSpec(USER_DOMAIN, "task_action_execute", ("task_action_execute",), ("task", "action"), cli_show_effective_context=True, cli_context_write=True),
95
- PublicToolSpec(USER_DOMAIN, "task_associated_report_detail_get", ("task_associated_report_detail_get",), cli_public=False, cli_show_effective_context=True),
102
+ PublicToolSpec(
103
+ USER_DOMAIN,
104
+ "task_associated_report_detail_get",
105
+ ("task_associated_report_detail_get",),
106
+ ("task", "report"),
107
+ cli_show_effective_context=True,
108
+ ),
96
109
  PublicToolSpec(USER_DOMAIN, "task_workflow_log_get", ("task_workflow_log_get",), ("task", "log"), cli_show_effective_context=True),
97
110
  PublicToolSpec(USER_DOMAIN, "directory_search", ("directory_search",), cli_public=False),
98
111
  PublicToolSpec(USER_DOMAIN, "directory_list_internal_users", ("directory_list_internal_users",), cli_public=False),
@@ -109,6 +122,7 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
109
122
  PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
110
123
  PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
111
124
  PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), ("builder", "workspace", "list"), cli_public=False),
125
+ PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",), ("builder", "workspace", "get"), cli_public=False),
112
126
  PublicToolSpec(BUILDER_DOMAIN, "file_upload_local", ("file_upload_local",), ("builder", "file", "upload-local"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
113
127
  PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
114
128
  PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
@@ -120,6 +134,7 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
120
134
  PublicToolSpec(BUILDER_DOMAIN, "role_create", ("role_create",), ("builder", "role", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
121
135
  PublicToolSpec(BUILDER_DOMAIN, "app_release_edit_lock_if_mine", ("app_release_edit_lock_if_mine",), ("builder", "app", "release-edit-lock-if-mine"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
122
136
  PublicToolSpec(BUILDER_DOMAIN, "app_resolve", ("app_resolve",), ("builder", "app", "resolve"), has_contract=True, cli_show_effective_context=True),
137
+ PublicToolSpec(BUILDER_DOMAIN, "button_style_catalog_get", ("button_style_catalog_get",), ("builder", "button", "catalog"), has_contract=True, cli_show_effective_context=True),
123
138
  PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_list", ("app_custom_button_list",), ("builder", "button", "list"), has_contract=True, cli_show_effective_context=True),
124
139
  PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_get", ("app_custom_button_get",), ("builder", "button", "get"), has_contract=True, cli_show_effective_context=True),
125
140
  PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_create", ("app_custom_button_create",), ("builder", "button", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),