@josephyan/qingflow-cli 0.2.0-beta.73 → 0.2.0-beta.75

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.
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .config import get_repository_metadata_dir
10
+
11
+
12
+ def _utc_now() -> str:
13
+ return datetime.now(timezone.utc).isoformat()
14
+
15
+
16
+ def _safe_key(value: str) -> str:
17
+ keep: list[str] = []
18
+ for char in value:
19
+ if char.isalnum() or char in {"-", "_"}:
20
+ keep.append(char)
21
+ else:
22
+ keep.append("_")
23
+ normalized = "".join(keep).strip("_")
24
+ return normalized or "repository"
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class RepositoryMetadataStore:
29
+ base_dir: Path | None = None
30
+ _dir: Path = field(init=False, repr=False)
31
+
32
+ def __post_init__(self) -> None:
33
+ self._dir = self.base_dir or get_repository_metadata_dir()
34
+ self._dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ def put(self, repo_name: str, payload: dict[str, Any]) -> dict[str, Any]:
37
+ now = _utc_now()
38
+ existing = self.get(repo_name) or {}
39
+ data = dict(existing)
40
+ data.update(payload)
41
+ data["repo_name"] = repo_name
42
+ data["created_at"] = existing.get("created_at") or now
43
+ data["updated_at"] = now
44
+ self._path(repo_name).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
45
+ return data
46
+
47
+ def get(self, repo_name: str) -> dict[str, Any] | None:
48
+ path = self._path(repo_name)
49
+ if not path.exists():
50
+ return None
51
+ try:
52
+ payload = json.loads(path.read_text(encoding="utf-8"))
53
+ except (OSError, json.JSONDecodeError):
54
+ path.unlink(missing_ok=True)
55
+ return None
56
+ return payload if isinstance(payload, dict) else None
57
+
58
+ def list(self) -> list[dict[str, Any]]:
59
+ entries: list[dict[str, Any]] = []
60
+ for path in sorted(self._dir.glob("*.json")):
61
+ try:
62
+ payload = json.loads(path.read_text(encoding="utf-8"))
63
+ except (OSError, json.JSONDecodeError):
64
+ continue
65
+ if isinstance(payload, dict):
66
+ entries.append(payload)
67
+ entries.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True)
68
+ return entries
69
+
70
+ def _path(self, repo_name: str) -> Path:
71
+ return self._dir / f"{_safe_key(repo_name)}.json"
@@ -5,11 +5,17 @@ from copy import deepcopy
5
5
  from functools import wraps
6
6
  from typing import Any, Callable
7
7
 
8
+ from .public_surface import (
9
+ BUILDER_DOMAIN,
10
+ USER_DOMAIN,
11
+ cli_trim_key_from_namespace,
12
+ server_method_map,
13
+ tool_key,
14
+ )
15
+
8
16
 
9
17
  JSONObject = dict[str, Any]
10
18
  TransformFn = Callable[[JSONObject], None]
11
- USER_DOMAIN = "user"
12
- BUILDER_DOMAIN = "builder"
13
19
 
14
20
 
15
21
  COMMON_SUCCESS_DROP_TOP = {
@@ -47,10 +53,6 @@ COMMON_ERROR_DROP_TOP = {
47
53
  SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
48
54
 
49
55
 
50
- def _tool_key(domain: str, tool_name: str) -> str:
51
- return f"{domain}:{tool_name}"
52
-
53
-
54
56
  def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict[str, Any]:
55
57
  if not isinstance(payload, dict):
56
58
  return payload
@@ -100,246 +102,11 @@ def wrap_trimmed_methods(instance: object, method_map: dict[str, str]) -> object
100
102
 
101
103
 
102
104
  def resolve_cli_tool_name(args: Any) -> str | None:
103
- command = getattr(args, "command", None)
104
- if command == "auth":
105
- return {
106
- "login": _tool_key(USER_DOMAIN, "auth_login"),
107
- "use-token": _tool_key(USER_DOMAIN, "auth_use_token"),
108
- "whoami": _tool_key(USER_DOMAIN, "auth_whoami"),
109
- "logout": _tool_key(USER_DOMAIN, "auth_logout"),
110
- }.get(getattr(args, "auth_command", None))
111
- if command == "workspace":
112
- return {
113
- "list": _tool_key(USER_DOMAIN, "workspace_list"),
114
- "select": _tool_key(USER_DOMAIN, "workspace_select"),
115
- }.get(getattr(args, "workspace_command", None))
116
- if command == "app":
117
- return {
118
- "list": _tool_key(USER_DOMAIN, "app_list"),
119
- "search": _tool_key(USER_DOMAIN, "app_search"),
120
- "get": _tool_key(USER_DOMAIN, "app_get"),
121
- }.get(getattr(args, "app_command", None))
122
- if command == "portal":
123
- return {
124
- "list": _tool_key(USER_DOMAIN, "portal_list"),
125
- "get": _tool_key(USER_DOMAIN, "portal_get"),
126
- }.get(getattr(args, "portal_command", None))
127
- if command == "view":
128
- return {
129
- "get": _tool_key(USER_DOMAIN, "view_get"),
130
- }.get(getattr(args, "view_command", None))
131
- if command == "chart":
132
- return {
133
- "get": _tool_key(USER_DOMAIN, "chart_get"),
134
- }.get(getattr(args, "chart_command", None))
135
- if command == "record":
136
- record_command = getattr(args, "record_command", None)
137
- if record_command == "schema":
138
- return {
139
- "applicant": _tool_key(USER_DOMAIN, "record_schema_get"),
140
- "browse": _tool_key(USER_DOMAIN, "record_browse_schema_get"),
141
- "insert": _tool_key(USER_DOMAIN, "record_insert_schema_get"),
142
- "update": _tool_key(USER_DOMAIN, "record_update_schema_get"),
143
- "import": _tool_key(USER_DOMAIN, "record_import_schema_get"),
144
- "code-block": _tool_key(USER_DOMAIN, "record_code_block_schema_get"),
145
- }.get(getattr(args, "record_schema_command", None))
146
- return {
147
- "list": _tool_key(USER_DOMAIN, "record_list"),
148
- "get": _tool_key(USER_DOMAIN, "record_get"),
149
- "insert": _tool_key(USER_DOMAIN, "record_insert"),
150
- "update": _tool_key(USER_DOMAIN, "record_update"),
151
- "delete": _tool_key(USER_DOMAIN, "record_delete"),
152
- "analyze": _tool_key(USER_DOMAIN, "record_analyze"),
153
- "code-block-run": _tool_key(USER_DOMAIN, "record_code_block_run"),
154
- }.get(record_command)
155
- if command == "import":
156
- return {
157
- "template": _tool_key(USER_DOMAIN, "record_import_template_get"),
158
- "verify": _tool_key(USER_DOMAIN, "record_import_verify"),
159
- "repair": _tool_key(USER_DOMAIN, "record_import_repair_local"),
160
- "start": _tool_key(USER_DOMAIN, "record_import_start"),
161
- "status": _tool_key(USER_DOMAIN, "record_import_status_get"),
162
- }.get(getattr(args, "import_command", None))
163
- if command == "task":
164
- return {
165
- "list": _tool_key(USER_DOMAIN, "task_list"),
166
- "get": _tool_key(USER_DOMAIN, "task_get"),
167
- "action": _tool_key(USER_DOMAIN, "task_action_execute"),
168
- "log": _tool_key(USER_DOMAIN, "task_workflow_log_get"),
169
- }.get(getattr(args, "task_command", None))
170
- if command not in {"builder", "build"}:
171
- return None
172
- builder_command = getattr(args, "builder_command", None)
173
- if builder_command == "file":
174
- return {
175
- "upload-local": _tool_key(BUILDER_DOMAIN, "file_upload_local"),
176
- }.get(getattr(args, "builder_file_command", None))
177
- if builder_command == "feedback":
178
- return {
179
- "submit": _tool_key(BUILDER_DOMAIN, "feedback_submit"),
180
- }.get(getattr(args, "builder_feedback_command", None))
181
- if builder_command == "contract":
182
- return _tool_key(BUILDER_DOMAIN, "builder_tool_contract")
183
- if builder_command == "member":
184
- return {
185
- "search": _tool_key(BUILDER_DOMAIN, "member_search"),
186
- }.get(getattr(args, "builder_member_command", None))
187
- if builder_command == "role":
188
- return {
189
- "search": _tool_key(BUILDER_DOMAIN, "role_search"),
190
- "create": _tool_key(BUILDER_DOMAIN, "role_create"),
191
- }.get(getattr(args, "builder_role_command", None))
192
- if builder_command == "package":
193
- return {
194
- "list": _tool_key(BUILDER_DOMAIN, "package_list"),
195
- "resolve": _tool_key(BUILDER_DOMAIN, "package_resolve"),
196
- "create": _tool_key(BUILDER_DOMAIN, "package_create"),
197
- "attach-app": _tool_key(BUILDER_DOMAIN, "package_attach_app"),
198
- }.get(getattr(args, "builder_package_command", None))
199
- if builder_command == "app":
200
- app_command = getattr(args, "builder_app_command", None)
201
- if app_command == "get":
202
- return {
203
- "summary": _tool_key(BUILDER_DOMAIN, "app_get"),
204
- "fields": _tool_key(BUILDER_DOMAIN, "app_get_fields"),
205
- "layout": _tool_key(BUILDER_DOMAIN, "app_get_layout"),
206
- "views": _tool_key(BUILDER_DOMAIN, "app_get_views"),
207
- "flow": _tool_key(BUILDER_DOMAIN, "app_get_flow"),
208
- "charts": _tool_key(BUILDER_DOMAIN, "app_get_charts"),
209
- }.get(getattr(args, "builder_app_get_section", "summary"))
210
- return {
211
- "resolve": _tool_key(BUILDER_DOMAIN, "app_resolve"),
212
- "release-edit-lock-if-mine": _tool_key(BUILDER_DOMAIN, "app_release_edit_lock_if_mine"),
213
- }.get(app_command)
214
- if builder_command == "button":
215
- return {
216
- "list": _tool_key(BUILDER_DOMAIN, "app_custom_button_list"),
217
- "get": _tool_key(BUILDER_DOMAIN, "app_custom_button_get"),
218
- "create": _tool_key(BUILDER_DOMAIN, "app_custom_button_create"),
219
- "update": _tool_key(BUILDER_DOMAIN, "app_custom_button_update"),
220
- "delete": _tool_key(BUILDER_DOMAIN, "app_custom_button_delete"),
221
- }.get(getattr(args, "builder_button_command", None))
222
- if builder_command == "portal":
223
- return {
224
- "list": _tool_key(BUILDER_DOMAIN, "portal_list"),
225
- "get": _tool_key(BUILDER_DOMAIN, "portal_get"),
226
- "apply": _tool_key(BUILDER_DOMAIN, "portal_apply"),
227
- }.get(getattr(args, "builder_portal_command", None))
228
- if builder_command == "view":
229
- return {
230
- "get": _tool_key(BUILDER_DOMAIN, "view_get"),
231
- }.get(getattr(args, "builder_view_command", None))
232
- if builder_command == "chart":
233
- return {
234
- "get": _tool_key(BUILDER_DOMAIN, "chart_get"),
235
- }.get(getattr(args, "builder_chart_command", None))
236
- if builder_command == "schema":
237
- return {"apply": _tool_key(BUILDER_DOMAIN, "app_schema_apply")}.get(getattr(args, "builder_schema_command", None))
238
- if builder_command == "layout":
239
- return {"apply": _tool_key(BUILDER_DOMAIN, "app_layout_apply")}.get(getattr(args, "builder_layout_command", None))
240
- if builder_command == "views":
241
- return {"apply": _tool_key(BUILDER_DOMAIN, "app_views_apply")}.get(getattr(args, "builder_views_command", None))
242
- if builder_command == "flow":
243
- return {"apply": _tool_key(BUILDER_DOMAIN, "app_flow_apply")}.get(getattr(args, "builder_flow_command", None))
244
- if builder_command == "charts":
245
- return {"apply": _tool_key(BUILDER_DOMAIN, "app_charts_apply")}.get(getattr(args, "builder_charts_command", None))
246
- if builder_command == "publish":
247
- return {"verify": _tool_key(BUILDER_DOMAIN, "app_publish_verify")}.get(getattr(args, "builder_publish_command", None))
248
- return None
249
-
250
-
251
- USER_SERVER_METHOD_MAP = {
252
- "auth_login": _tool_key(USER_DOMAIN, "auth_login"),
253
- "auth_use_token": _tool_key(USER_DOMAIN, "auth_use_token"),
254
- "auth_whoami": _tool_key(USER_DOMAIN, "auth_whoami"),
255
- "auth_logout": _tool_key(USER_DOMAIN, "auth_logout"),
256
- "workspace_list": _tool_key(USER_DOMAIN, "workspace_list"),
257
- "workspace_select": _tool_key(USER_DOMAIN, "workspace_select"),
258
- "app_list": _tool_key(USER_DOMAIN, "app_list"),
259
- "app_search": _tool_key(USER_DOMAIN, "app_search"),
260
- "app_get": _tool_key(USER_DOMAIN, "app_get"),
261
- "portal_list": _tool_key(USER_DOMAIN, "portal_list"),
262
- "portal_get": _tool_key(USER_DOMAIN, "portal_get"),
263
- "view_get": _tool_key(USER_DOMAIN, "view_get"),
264
- "chart_get": _tool_key(USER_DOMAIN, "chart_get"),
265
- "file_get_upload_info": _tool_key(USER_DOMAIN, "file_get_upload_info"),
266
- "file_upload_local": _tool_key(USER_DOMAIN, "file_upload_local"),
267
- "feedback_submit": _tool_key(USER_DOMAIN, "feedback_submit"),
268
- "record_import_schema_get": _tool_key(USER_DOMAIN, "record_import_schema_get"),
269
- "record_import_template_get": _tool_key(USER_DOMAIN, "record_import_template_get"),
270
- "record_import_verify": _tool_key(USER_DOMAIN, "record_import_verify"),
271
- "record_import_repair_local": _tool_key(USER_DOMAIN, "record_import_repair_local"),
272
- "record_import_start": _tool_key(USER_DOMAIN, "record_import_start"),
273
- "record_import_status_get": _tool_key(USER_DOMAIN, "record_import_status_get"),
274
- "record_insert_schema_get_public": _tool_key(USER_DOMAIN, "record_insert_schema_get"),
275
- "record_member_candidates": _tool_key(USER_DOMAIN, "record_member_candidates"),
276
- "record_department_candidates": _tool_key(USER_DOMAIN, "record_department_candidates"),
277
- "record_analyze": _tool_key(USER_DOMAIN, "record_analyze"),
278
- "record_list": _tool_key(USER_DOMAIN, "record_list"),
279
- "record_get_public": _tool_key(USER_DOMAIN, "record_get"),
280
- "record_browse_schema_get_public": _tool_key(USER_DOMAIN, "record_browse_schema_get"),
281
- "record_update_schema_get_public": _tool_key(USER_DOMAIN, "record_update_schema_get"),
282
- "record_insert_public": _tool_key(USER_DOMAIN, "record_insert"),
283
- "record_update_public": _tool_key(USER_DOMAIN, "record_update"),
284
- "record_delete_public": _tool_key(USER_DOMAIN, "record_delete"),
285
- "record_code_block_schema_get_public": _tool_key(USER_DOMAIN, "record_code_block_schema_get"),
286
- "record_code_block_run": _tool_key(USER_DOMAIN, "record_code_block_run"),
287
- "task_list": _tool_key(USER_DOMAIN, "task_list"),
288
- "task_get": _tool_key(USER_DOMAIN, "task_get"),
289
- "task_action_execute": _tool_key(USER_DOMAIN, "task_action_execute"),
290
- "task_associated_report_detail_get": _tool_key(USER_DOMAIN, "task_associated_report_detail_get"),
291
- "task_workflow_log_get": _tool_key(USER_DOMAIN, "task_workflow_log_get"),
292
- "directory_search": _tool_key(USER_DOMAIN, "directory_search"),
293
- "directory_list_internal_users": _tool_key(USER_DOMAIN, "directory_list_internal_users"),
294
- "directory_list_all_internal_users": _tool_key(USER_DOMAIN, "directory_list_all_internal_users"),
295
- "directory_list_internal_departments": _tool_key(USER_DOMAIN, "directory_list_internal_departments"),
296
- "directory_list_all_departments": _tool_key(USER_DOMAIN, "directory_list_all_departments"),
297
- "directory_list_sub_departments": _tool_key(USER_DOMAIN, "directory_list_sub_departments"),
298
- "directory_list_external_members": _tool_key(USER_DOMAIN, "directory_list_external_members"),
299
- }
105
+ return cli_trim_key_from_namespace(args)
300
106
 
301
- BUILDER_SERVER_METHOD_MAP = {
302
- "auth_login": _tool_key(BUILDER_DOMAIN, "auth_login"),
303
- "auth_use_token": _tool_key(BUILDER_DOMAIN, "auth_use_token"),
304
- "auth_whoami": _tool_key(BUILDER_DOMAIN, "auth_whoami"),
305
- "auth_logout": _tool_key(BUILDER_DOMAIN, "auth_logout"),
306
- "workspace_list": _tool_key(BUILDER_DOMAIN, "workspace_list"),
307
- "workspace_select": _tool_key(BUILDER_DOMAIN, "workspace_select"),
308
- "file_upload_local": _tool_key(BUILDER_DOMAIN, "file_upload_local"),
309
- "feedback_submit": _tool_key(BUILDER_DOMAIN, "feedback_submit"),
310
- "package_list": _tool_key(BUILDER_DOMAIN, "package_list"),
311
- "package_resolve": _tool_key(BUILDER_DOMAIN, "package_resolve"),
312
- "builder_tool_contract": _tool_key(BUILDER_DOMAIN, "builder_tool_contract"),
313
- "package_create": _tool_key(BUILDER_DOMAIN, "package_create"),
314
- "member_search": _tool_key(BUILDER_DOMAIN, "member_search"),
315
- "role_search": _tool_key(BUILDER_DOMAIN, "role_search"),
316
- "role_create": _tool_key(BUILDER_DOMAIN, "role_create"),
317
- "package_attach_app": _tool_key(BUILDER_DOMAIN, "package_attach_app"),
318
- "app_release_edit_lock_if_mine": _tool_key(BUILDER_DOMAIN, "app_release_edit_lock_if_mine"),
319
- "app_resolve": _tool_key(BUILDER_DOMAIN, "app_resolve"),
320
- "app_custom_button_list": _tool_key(BUILDER_DOMAIN, "app_custom_button_list"),
321
- "app_custom_button_get": _tool_key(BUILDER_DOMAIN, "app_custom_button_get"),
322
- "app_custom_button_create": _tool_key(BUILDER_DOMAIN, "app_custom_button_create"),
323
- "app_custom_button_update": _tool_key(BUILDER_DOMAIN, "app_custom_button_update"),
324
- "app_custom_button_delete": _tool_key(BUILDER_DOMAIN, "app_custom_button_delete"),
325
- "app_get": _tool_key(BUILDER_DOMAIN, "app_get"),
326
- "app_get_fields": _tool_key(BUILDER_DOMAIN, "app_get_fields"),
327
- "app_get_layout": _tool_key(BUILDER_DOMAIN, "app_get_layout"),
328
- "app_get_views": _tool_key(BUILDER_DOMAIN, "app_get_views"),
329
- "app_get_flow": _tool_key(BUILDER_DOMAIN, "app_get_flow"),
330
- "app_get_charts": _tool_key(BUILDER_DOMAIN, "app_get_charts"),
331
- "portal_list": _tool_key(BUILDER_DOMAIN, "portal_list"),
332
- "portal_get": _tool_key(BUILDER_DOMAIN, "portal_get"),
333
- "view_get": _tool_key(BUILDER_DOMAIN, "view_get"),
334
- "chart_get": _tool_key(BUILDER_DOMAIN, "chart_get"),
335
- "app_schema_apply": _tool_key(BUILDER_DOMAIN, "app_schema_apply"),
336
- "app_layout_apply": _tool_key(BUILDER_DOMAIN, "app_layout_apply"),
337
- "app_flow_apply": _tool_key(BUILDER_DOMAIN, "app_flow_apply"),
338
- "app_views_apply": _tool_key(BUILDER_DOMAIN, "app_views_apply"),
339
- "app_charts_apply": _tool_key(BUILDER_DOMAIN, "app_charts_apply"),
340
- "portal_apply": _tool_key(BUILDER_DOMAIN, "portal_apply"),
341
- "app_publish_verify": _tool_key(BUILDER_DOMAIN, "app_publish_verify"),
342
- }
107
+
108
+ USER_SERVER_METHOD_MAP = server_method_map(USER_DOMAIN)
109
+ BUILDER_SERVER_METHOD_MAP = server_method_map(BUILDER_DOMAIN)
343
110
 
344
111
 
345
112
  def _wrap_callable(original: Callable[..., Any], tool_name: str) -> Callable[..., Any]:
@@ -600,7 +367,7 @@ def _trim_builder_list_like(payload: JSONObject) -> None:
600
367
  def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform: TransformFn) -> None:
601
368
  for domain in domains:
602
369
  for name in names:
603
- SUCCESS_POLICY_BY_TOOL[_tool_key(domain, name)] = transform
370
+ SUCCESS_POLICY_BY_TOOL[tool_key(domain, name)] = transform
604
371
 
605
372
 
606
373
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_login", "auth_use_token", "auth_whoami"), _trim_auth_payload)
@@ -609,6 +376,7 @@ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_works
609
376
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_select",), _trim_workspace_select)
610
377
  _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
611
378
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
379
+ _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
612
380
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("portal_list", "portal_get", "view_get", "chart_get"), _trim_builder_list_like)
613
381
  _register_policy((USER_DOMAIN,), ("file_get_upload_info",), _trim_file_upload_info)
614
382
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("file_upload_local",), _trim_file_upload_local)
@@ -645,6 +413,7 @@ _register_policy(
645
413
  (USER_DOMAIN,),
646
414
  (
647
415
  "task_get",
416
+ "task_save_only",
648
417
  "task_action_execute",
649
418
  "task_associated_report_detail_get",
650
419
  "task_workflow_log_get",
@@ -680,6 +449,7 @@ _register_policy(
680
449
  "package_list",
681
450
  "package_resolve",
682
451
  "builder_tool_contract",
452
+ "solution_install",
683
453
  "package_create",
684
454
  "member_search",
685
455
  "role_search",
@@ -693,6 +463,7 @@ _register_policy(
693
463
  "app_custom_button_update",
694
464
  "app_custom_button_delete",
695
465
  "app_get_fields",
466
+ "app_repair_code_blocks",
696
467
  "app_get_layout",
697
468
  "app_get_views",
698
469
  "app_get_flow",
@@ -34,10 +34,12 @@ def build_builder_server() -> FastMCP:
34
34
  "`feedback_submit` is always available as a cross-cutting helper when the current capability is unsupported, awkward, or still cannot satisfy the user's need after reasonable use; it does not require Qingflow login or workspace selection, and it should be called only after explicit user confirmation. "
35
35
  "Follow the resource path resolve -> summary read -> apply -> attach -> publish_verify. "
36
36
  "Use builder_tool_contract when you need a machine-readable contract, aliases, allowed enums, or a minimal valid example for a public builder tool. "
37
+ "Use solution_install when the user explicitly wants to install a packaged solution/template by solution_key, optionally copying bundled demo data. "
37
38
  "If creating a new package may be appropriate, ask the user to confirm package creation before calling package_create; otherwise use package_resolve/package_list and app_resolve to locate resources, "
38
- "app_get/app_get_fields/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for configuration reads, "
39
+ "app_get/app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for configuration reads, "
39
40
  "member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
40
41
  "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates are replace-only and publish=false only guarantees draft/base-info updates, and flow should use publish=false whenever you only want draft/precheck behavior. "
42
+ "For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
41
43
  "Use package_attach_app to attach apps to packages, and app_publish_verify for explicit final publish verification. "
42
44
  "For workflow edits, keep the public builder surface on stable linear flows only: start/approve/fill/copy/webhook/end. Branch and condition nodes are intentionally disabled because the backend workflow route is not front-end stable for those node types. Declare node assignees and editable fields explicitly. "
43
45
  "If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
@@ -198,6 +200,20 @@ def build_builder_server() -> FastMCP:
198
200
  ) -> dict:
199
201
  return ai_builder.package_create(profile=profile, package_name=package_name, icon=icon, color=color)
200
202
 
203
+ @server.tool()
204
+ def solution_install(
205
+ profile: str = DEFAULT_PROFILE,
206
+ solution_key: str = "",
207
+ being_copy_data: bool = True,
208
+ solution_source: str = "solutionDetail",
209
+ ) -> dict:
210
+ return ai_builder.solution_install(
211
+ profile=profile,
212
+ solution_key=solution_key,
213
+ being_copy_data=being_copy_data,
214
+ solution_source=solution_source,
215
+ )
216
+
201
217
  @server.tool()
202
218
  def member_search(
203
219
  profile: str = DEFAULT_PROFILE,
@@ -318,6 +334,15 @@ def build_builder_server() -> FastMCP:
318
334
  def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
319
335
  return ai_builder.app_get_fields(profile=profile, app_key=app_key)
320
336
 
337
+ @server.tool()
338
+ def app_repair_code_blocks(
339
+ profile: str = DEFAULT_PROFILE,
340
+ app_key: str = "",
341
+ field: str | None = None,
342
+ apply: bool = False,
343
+ ) -> dict:
344
+ return ai_builder.app_repair_code_blocks(profile=profile, app_key=app_key, field=field, apply=apply)
345
+
321
346
  @server.tool()
322
347
  def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
323
348
  return ai_builder.app_get_layout(profile=profile, app_key=app_key)
@@ -117,6 +117,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
117
117
 
118
118
  - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
119
119
  - Member and department fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to `record_member_candidates` or `record_department_candidates` when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
120
+ - When candidate browsing must match a real update/write scope, pass `record_id`, `workflow_node_id`, and any pending `fields` context to the candidate tool; otherwise the candidate result is only a static applicant-node preview.
120
121
  - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
121
122
 
122
123
  ## Code Block Path
@@ -143,11 +144,14 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
143
144
 
144
145
  ## Task Workflow Path
145
146
 
146
- `task_list -> task_get -> task_action_execute`
147
+ `task_list -> task_get -> task_save_only / task_action_execute`
147
148
 
148
149
  - Use `task_associated_report_detail_get` for associated view or report details.
149
150
  - Use `task_workflow_log_get` for full workflow log history.
150
151
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
152
+ - Treat `task_action_execute` as the tool-level action enum surface; the current task's real actions are only the ones listed in `task_get.capabilities.available_actions`.
153
+ - Use `task_save_only` when the user wants to save editable field changes on the current node without advancing the workflow.
154
+ - `save_only` is exposed only when the backend current-node `editableQueIds` signal returns a non-empty result; MCP no longer infers `save_only` from local schema reconstruction.
151
155
 
152
156
  ## Time Handling
153
157