@josephyan/qingflow-cli 1.0.10 → 1.1.1

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 (65) hide show
  1. package/README.md +3 -3
  2. package/npm/bin/qingflow.mjs +32 -1
  3. package/npm/lib/runtime.mjs +43 -2
  4. package/package.json +1 -1
  5. package/pyproject.toml +2 -1
  6. package/skills/qingflow-cli/SKILL.md +440 -0
  7. package/skills/qingflow-cli/manifest.yaml +10 -0
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +94 -0
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +485 -0
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md +237 -0
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_MATCH_RULES.md +137 -0
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md +263 -0
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +304 -0
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md +41 -0
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +139 -0
  16. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +84 -0
  17. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +129 -0
  18. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +195 -0
  19. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +159 -0
  20. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +20 -0
  21. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md +176 -0
  22. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +163 -0
  23. package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +107 -0
  24. package/skills/qingflow-cli/reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md +151 -0
  25. package/skills/qingflow-cli/reference/_batch_schema_complex.json +18 -0
  26. package/skills/qingflow-cli/reference/_batch_schema_scalar.json +17 -0
  27. package/skills/qingflow-cli/reference/charts_remove.example.json +1 -0
  28. package/skills/qingflow-cli/reference/charts_reorder.example.json +1 -0
  29. package/skills/qingflow-cli/reference/charts_upsert_bar.example.json +8 -0
  30. package/skills/qingflow-cli/reference/charts_upsert_dashboard_starter.example.json +37 -0
  31. package/skills/qingflow-cli/reference/charts_upsert_minimal.example.json +13 -0
  32. package/skills/qingflow-cli/reference/portal_sections_all_types.example.json +131 -0
  33. package/skills/qingflow-cli/reference/portal_sections_five_types.example.json +126 -0
  34. package/skills/qingflow-cli/reference/portal_sections_standard_workbench.example.json +128 -0
  35. package/skills/qingflow-cli/reference/schema_add_fields_minimal.example.json +7 -0
  36. package/skills/qingflow-cli/reference/schema_apply_add_fields_all_types.json +78 -0
  37. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +7 -0
  38. package/skills/qingflow-cli/scripts/builder-package-from-app-list.py +140 -0
  39. package/skills/qingflow-cli/scripts/find-app-by-keyword.py +132 -0
  40. package/skills/qingflow-cli/scripts/validate_qingflow_output_files.py +87 -0
  41. package/src/qingflow_mcp/__init__.py +1 -1
  42. package/src/qingflow_mcp/builder_facade/models.py +532 -48
  43. package/src/qingflow_mcp/builder_facade/service.py +9194 -2384
  44. package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
  45. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  46. package/src/qingflow_mcp/cli/commands/builder.py +354 -56
  47. package/src/qingflow_mcp/cli/commands/record.py +89 -4
  48. package/src/qingflow_mcp/cli/formatters.py +53 -15
  49. package/src/qingflow_mcp/cli/main.py +204 -3
  50. package/src/qingflow_mcp/public_surface.py +11 -8
  51. package/src/qingflow_mcp/response_trim.py +185 -46
  52. package/src/qingflow_mcp/server.py +18 -15
  53. package/src/qingflow_mcp/server_app_builder.py +108 -30
  54. package/src/qingflow_mcp/server_app_user.py +20 -21
  55. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  56. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  57. package/src/qingflow_mcp/solution/executor.py +3 -133
  58. package/src/qingflow_mcp/tools/ai_builder_tools.py +2617 -440
  59. package/src/qingflow_mcp/tools/app_tools.py +53 -8
  60. package/src/qingflow_mcp/tools/package_tools.py +16 -2
  61. package/src/qingflow_mcp/tools/record_tools.py +3408 -599
  62. package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
  63. package/src/qingflow_mcp/tools/solution_tools.py +30 -2
  64. package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
  65. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -13,7 +13,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
13
13
  record_subparsers = parser.add_subparsers(
14
14
  dest="record_command",
15
15
  required=True,
16
- metavar="{schema,list,access,get,insert,update,delete,code-block-run}",
16
+ metavar="{schema,list,access,get,logs,insert,update,delete,member-candidates,department-candidates,code-block-run}",
17
17
  )
18
18
 
19
19
  schema = record_subparsers.add_parser("schema", help="读取记录相关表结构")
@@ -47,6 +47,28 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
47
47
  schema_code_block.add_argument("--app-key", required=True)
48
48
  schema_code_block.set_defaults(handler=_handle_schema_code_block, format_hint="")
49
49
 
50
+ member_candidates = record_subparsers.add_parser("member-candidates", help="读取成员字段候选项")
51
+ member_candidates.add_argument("--app-key", required=True)
52
+ member_candidates.add_argument("--field-id", type=int, required=True)
53
+ member_candidates.add_argument("--keyword", default="")
54
+ member_candidates.add_argument("--page-num", type=int, default=1)
55
+ member_candidates.add_argument("--page-size", type=int, default=20)
56
+ member_candidates.add_argument("--record-id")
57
+ member_candidates.add_argument("--workflow-node-id", type=int)
58
+ member_candidates.add_argument("--fields-file")
59
+ member_candidates.set_defaults(handler=_handle_member_candidates, format_hint="")
60
+
61
+ department_candidates = record_subparsers.add_parser("department-candidates", help="读取部门字段候选项")
62
+ department_candidates.add_argument("--app-key", required=True)
63
+ department_candidates.add_argument("--field-id", type=int, required=True)
64
+ department_candidates.add_argument("--keyword", default="")
65
+ department_candidates.add_argument("--page-num", type=int, default=1)
66
+ department_candidates.add_argument("--page-size", type=int, default=20)
67
+ department_candidates.add_argument("--record-id")
68
+ department_candidates.add_argument("--workflow-node-id", type=int)
69
+ department_candidates.add_argument("--fields-file")
70
+ department_candidates.set_defaults(handler=_handle_department_candidates, format_hint="")
71
+
50
72
  list_parser = record_subparsers.add_parser("list", help="列出记录")
51
73
  list_parser.add_argument("--app-key", required=True)
52
74
  list_parser.add_argument("--column", dest="columns", action="append", type=int, default=[])
@@ -56,7 +78,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
56
78
  list_parser.add_argument("--query-fields-file")
57
79
  list_parser.add_argument("--where-file")
58
80
  list_parser.add_argument("--order-by-file")
59
- list_parser.add_argument("--limit", type=int, default=20)
60
81
  list_parser.add_argument("--page", type=int, default=1)
61
82
  list_parser.add_argument("--view-id")
62
83
  list_parser.add_argument("--list-type", dest="legacy_list_type", type=int, help=argparse.SUPPRESS)
@@ -81,9 +102,16 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
81
102
  get.add_argument("--view-id")
82
103
  get.set_defaults(handler=_handle_get, format_hint="record_get")
83
104
 
105
+ logs = record_subparsers.add_parser("logs", help="读取单条记录全量日志并写入本地 JSONL")
106
+ logs.add_argument("--app-key", required=True)
107
+ logs.add_argument("--record-id", required=True)
108
+ logs.add_argument("--view-id")
109
+ logs.set_defaults(handler=_handle_logs, format_hint="record_logs")
110
+
84
111
  insert = record_subparsers.add_parser("insert", help="新增记录")
85
112
  insert.add_argument("--app-key", required=True)
86
- insert.add_argument("--fields-file", required=True)
113
+ insert.add_argument("--fields-file", help=argparse.SUPPRESS)
114
+ insert.add_argument("--items-file")
87
115
  insert.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
88
116
  insert.set_defaults(handler=_handle_insert, format_hint="")
89
117
 
@@ -207,6 +235,38 @@ def _handle_schema_code_block(args: argparse.Namespace, context: CliContext) ->
207
235
  return context.code_block.record_code_block_schema_get_public(profile=args.profile, app_key=args.app_key)
208
236
 
209
237
 
238
+ def _candidate_context_fields(args: argparse.Namespace) -> dict[str, Any]:
239
+ return load_object_arg(args.fields_file, option_name="--fields-file") or {}
240
+
241
+
242
+ def _handle_member_candidates(args: argparse.Namespace, context: CliContext) -> dict:
243
+ return context.record.record_member_candidates(
244
+ profile=args.profile,
245
+ app_key=args.app_key,
246
+ field_id=args.field_id,
247
+ record_id=args.record_id,
248
+ workflow_node_id=args.workflow_node_id,
249
+ fields=_candidate_context_fields(args),
250
+ keyword=args.keyword,
251
+ page_num=args.page_num,
252
+ page_size=args.page_size,
253
+ )
254
+
255
+
256
+ def _handle_department_candidates(args: argparse.Namespace, context: CliContext) -> dict:
257
+ return context.record.record_department_candidates(
258
+ profile=args.profile,
259
+ app_key=args.app_key,
260
+ field_id=args.field_id,
261
+ record_id=args.record_id,
262
+ workflow_node_id=args.workflow_node_id,
263
+ fields=_candidate_context_fields(args),
264
+ keyword=args.keyword,
265
+ page_num=args.page_num,
266
+ page_size=args.page_size,
267
+ )
268
+
269
+
210
270
  def _validate_public_view_selector(
211
271
  *,
212
272
  view_id: str | None,
@@ -248,7 +308,6 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
248
308
  query_fields=_query_fields(args),
249
309
  where=load_list_arg(args.where_file, option_name="--where-file"),
250
310
  order_by=load_list_arg(args.order_by_file, option_name="--order-by-file"),
251
- limit=args.limit,
252
311
  page=args.page,
253
312
  view_id=args.view_id,
254
313
  )
@@ -275,7 +334,33 @@ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
275
334
  )
276
335
 
277
336
 
337
+ def _handle_logs(args: argparse.Namespace, context: CliContext) -> dict:
338
+ return context.record.record_logs_get(
339
+ profile=args.profile,
340
+ app_key=args.app_key,
341
+ record_id=args.record_id,
342
+ view_id=args.view_id,
343
+ )
344
+
345
+
278
346
  def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
347
+ if args.items_file:
348
+ if args.fields_file:
349
+ raise_config_error(
350
+ "record insert batch mode does not accept --fields-file.",
351
+ fix_hint="Use `record insert --app-key APP_KEY --items-file ITEMS.json` for batch inserts.",
352
+ )
353
+ return context.record.record_insert_public(
354
+ profile=args.profile,
355
+ app_key=args.app_key,
356
+ items=require_list_arg(args.items_file, option_name="--items-file"),
357
+ verify_write=bool(args.verify_write),
358
+ )
359
+ if not args.fields_file:
360
+ raise_config_error(
361
+ "record insert requires --items-file.",
362
+ fix_hint="Use `record insert --app-key APP_KEY --items-file ITEMS.json`; a single insert is one item in the JSON array.",
363
+ )
279
364
  return context.record.record_insert_public(
280
365
  profile=args.profile,
281
366
  app_key=args.app_key,
@@ -190,20 +190,9 @@ def _format_record_list(result: dict[str, Any]) -> str:
190
190
  lines.append(f"- query: {lookup.get('query')}")
191
191
  lines.append(f"- confidence: {lookup.get('confidence')}")
192
192
  lines.append(f"- next_action: {lookup.get('next_action')}")
193
- lines.append(f"- reported_total: {lookup.get('reported_total')}")
194
- candidates = lookup.get("candidates") if isinstance(lookup.get("candidates"), list) else []
195
- for candidate in candidates[:5]:
196
- if not isinstance(candidate, dict):
197
- continue
198
- title = candidate.get("title") or "-"
199
- record_id = candidate.get("record_id") or "-"
200
- score = candidate.get("score")
201
- lines.append(f" - {record_id} | score={score} | {title}")
202
- matches = candidate.get("matched_fields") if isinstance(candidate.get("matched_fields"), list) else []
203
- if matches:
204
- match = matches[0]
205
- if isinstance(match, dict):
206
- lines.append(f" match: {match.get('title')}={match.get('value')}")
193
+ lines.append(f"- total_count: {lookup.get('total_count')}")
194
+ lines.append(f"- returned_count: {lookup.get('returned_count')}")
195
+ lines.append(f"- truncated: {lookup.get('truncated')}")
207
196
  lines.append(f"Returned Records: {len(items)}")
208
197
  for item in items[:10]:
209
198
  if isinstance(item, dict):
@@ -256,6 +245,15 @@ def _format_record_get(result: dict[str, Any]) -> str:
256
245
  media_items = media_assets.get("items") if isinstance(media_assets.get("items"), list) else []
257
246
  downloaded_media = [item for item in media_items if isinstance(item, dict) and item.get("access_status") == "downloaded"]
258
247
  failed_media = [item for item in media_items if isinstance(item, dict) and item.get("access_status") != "downloaded"]
248
+ file_assets = result.get("file_assets") if isinstance(result.get("file_assets"), dict) else {}
249
+ file_items = file_assets.get("items") if isinstance(file_assets.get("items"), list) else []
250
+ downloaded_files = [item for item in file_items if isinstance(item, dict) and item.get("access_status") == "downloaded"]
251
+ failed_files = [item for item in file_items if isinstance(item, dict) and item.get("access_status") != "downloaded"]
252
+ extracted_files = [
253
+ item
254
+ for item in downloaded_files
255
+ if isinstance(item.get("extraction"), dict) and item["extraction"].get("status") == "ok"
256
+ ]
259
257
  associated_resources = result.get("associated_resources") if isinstance(result.get("associated_resources"), list) else []
260
258
  unavailable_context = result.get("unavailable_context") if isinstance(result.get("unavailable_context"), list) else []
261
259
  lines = [
@@ -267,17 +265,26 @@ def _format_record_get(result: dict[str, Any]) -> str:
267
265
  f"Data logs: {data_logs.get('status') or '-'} / loaded={data_logs.get('items_loaded')}",
268
266
  f"Workflow logs: {workflow_logs.get('status') or '-'} / loaded={workflow_logs.get('items_loaded')}",
269
267
  f"Media assets: {media_assets.get('status') or '-'} / downloaded={len(downloaded_media)} / failed={len(failed_media)}",
268
+ f"File assets: {file_assets.get('status') or '-'} / downloaded={len(downloaded_files)} / extracted={len(extracted_files)} / failed={len(failed_files)}",
270
269
  f"Associated resources: {len(associated_resources)}",
271
270
  f"Unavailable contexts: {len(unavailable_context)}",
272
271
  ]
273
272
  if media_assets.get("local_dir"):
274
273
  lines.append(f"Media dir: {media_assets.get('local_dir')}")
274
+ if file_assets.get("local_dir"):
275
+ lines.append(f"File dir: {file_assets.get('local_dir')}")
275
276
  if failed_media:
276
277
  failure_counts: dict[str, int] = {}
277
278
  for item in failed_media:
278
279
  key = f"{item.get('access_status') or 'unknown'} via {item.get('download_strategy') or 'unknown'}"
279
280
  failure_counts[key] = failure_counts.get(key, 0) + 1
280
281
  lines.append("Media failures: " + ", ".join(f"{key}={count}" for key, count in sorted(failure_counts.items())))
282
+ if failed_files:
283
+ failure_counts = {}
284
+ for item in failed_files:
285
+ key = f"{item.get('access_status') or 'unknown'} via {item.get('download_strategy') or 'unknown'}"
286
+ failure_counts[key] = failure_counts.get(key, 0) + 1
287
+ lines.append("File failures: " + ", ".join(f"{key}={count}" for key, count in sorted(failure_counts.items())))
281
288
  summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
282
289
  if summary.get("text"):
283
290
  lines.append(f"Summary: {summary.get('text')}")
@@ -285,6 +292,37 @@ def _format_record_get(result: dict[str, Any]) -> str:
285
292
  return "\n".join(lines) + "\n"
286
293
 
287
294
 
295
+ def _format_record_logs(result: dict[str, Any]) -> str:
296
+ app = result.get("app") if isinstance(result.get("app"), dict) else {}
297
+ view = result.get("view") if isinstance(result.get("view"), dict) else {}
298
+ record = result.get("record") if isinstance(result.get("record"), dict) else {}
299
+ data_logs = result.get("data_logs") if isinstance(result.get("data_logs"), dict) else {}
300
+ workflow_logs = result.get("workflow_logs") if isinstance(result.get("workflow_logs"), dict) else {}
301
+ integrity = result.get("context_integrity") if isinstance(result.get("context_integrity"), dict) else {}
302
+ lines = [
303
+ f"Status: {result.get('status') or '-'}",
304
+ f"App: {app.get('app_name') or app.get('app_key') or '-'} ({app.get('app_key') or '-'})",
305
+ f"View: {view.get('name') or view.get('view_id') or '-'}",
306
+ f"Record: {record.get('title') or '-'} ({record.get('record_id') or '-'})",
307
+ f"Data logs: {data_logs.get('status') or '-'} / count={data_logs.get('items_count')} / pages={data_logs.get('pages_fetched')} / complete={data_logs.get('complete')}",
308
+ f"Workflow logs: {workflow_logs.get('status') or '-'} / count={workflow_logs.get('items_count')} / pages={workflow_logs.get('pages_fetched')} / complete={workflow_logs.get('complete')}",
309
+ f"Safe for full log conclusion: {integrity.get('safe_for_full_log_conclusion')}",
310
+ ]
311
+ if result.get("local_dir"):
312
+ lines.append(f"Local dir: {result.get('local_dir')}")
313
+ if data_logs.get("local_path"):
314
+ lines.append(f"Data logs file: {data_logs.get('local_path')}")
315
+ if workflow_logs.get("local_path"):
316
+ lines.append(f"Workflow logs file: {workflow_logs.get('local_path')}")
317
+ if result.get("summary_path"):
318
+ lines.append(f"Summary file: {result.get('summary_path')}")
319
+ _append_warnings(lines, result.get("warnings"))
320
+ unavailable = result.get("unavailable_context") if isinstance(result.get("unavailable_context"), list) else []
321
+ if unavailable:
322
+ lines.append(f"Unavailable contexts: {len(unavailable)}")
323
+ return "\n".join(lines) + "\n"
324
+
325
+
288
326
  def _format_task_list(result: dict[str, Any]) -> str:
289
327
  data = result.get("data") if isinstance(result.get("data"), dict) else {}
290
328
  items = data.get("items") if isinstance(data.get("items"), list) else []
@@ -781,11 +819,11 @@ _FORMATTERS = {
781
819
  "workspace_get": _format_workspace_get,
782
820
  "workspace_select": _format_workspace_select,
783
821
  "app_list": _format_app_items,
784
- "app_search": _format_app_items,
785
822
  "app_get": _format_app_get,
786
823
  "record_list": _format_record_list,
787
824
  "record_access": _format_record_access,
788
825
  "record_get": _format_record_get,
826
+ "record_logs": _format_record_logs,
789
827
  "task_list": _format_task_list,
790
828
  "task_workbench": _format_task_workbench,
791
829
  "task_get": _format_task_get,
@@ -8,6 +8,7 @@ from typing import Any, Callable, TextIO
8
8
  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
+ from ..tools.ai_builder_tools import _attach_builder_apply_envelope
11
12
  from .context import CliContext, build_cli_context
12
13
  from .formatters import emit_json_result, emit_text_result
13
14
  from .commands import register_all_commands
@@ -16,8 +17,21 @@ from .commands import register_all_commands
16
17
  Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]
17
18
 
18
19
 
20
+ class _CliArgumentError(Exception):
21
+ def __init__(self, *, prog: str, message: str, usage: str) -> None:
22
+ super().__init__(message)
23
+ self.prog = prog
24
+ self.message = message
25
+ self.usage = usage
26
+
27
+
28
+ class _QingflowArgumentParser(argparse.ArgumentParser):
29
+ def error(self, message: str) -> None:
30
+ raise _CliArgumentError(prog=self.prog, message=message, usage=self.format_usage())
31
+
32
+
19
33
  def build_parser() -> argparse.ArgumentParser:
20
- parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI")
34
+ parser = _QingflowArgumentParser(prog="qingflow", description="Qingflow CLI")
21
35
  parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
22
36
  parser.add_argument("--json", action="store_true", help="输出 JSON")
23
37
  subparsers = parser.add_subparsers(dest="command", required=True)
@@ -42,6 +56,21 @@ def run(
42
56
  normalized_argv = _normalize_global_args(list(argv) if argv is not None else sys.argv[1:])
43
57
  try:
44
58
  args = parser.parse_args(normalized_argv)
59
+ except _CliArgumentError as exc:
60
+ if _should_force_json_output_argv(normalized_argv):
61
+ payload = {
62
+ "category": "config",
63
+ "status": "failed",
64
+ "error_code": "ARGUMENT_ERROR",
65
+ "message": exc.message,
66
+ "details": {"usage": exc.usage.strip(), "prog": exc.prog},
67
+ }
68
+ payload = _maybe_attach_builder_apply_error_envelope_from_argv(normalized_argv, payload)
69
+ emit_json_result(payload, stream=out)
70
+ return 2
71
+ err.write(exc.usage)
72
+ err.write(f"{exc.prog}: error: {exc.message}\n")
73
+ return 2
45
74
  except SystemExit as exc:
46
75
  return int(exc.code or 0)
47
76
  setattr(args, "_stdin", sys.stdin)
@@ -51,8 +80,10 @@ def run(
51
80
  if handler is None:
52
81
  parser.print_help(out)
53
82
  return 2
54
- context = context_factory()
55
83
  try:
84
+ if _should_force_json_output(args):
85
+ setattr(args, "json", True)
86
+ context = context_factory()
56
87
  if not bool(args.json):
57
88
  _emit_cli_effective_context_notice(args, context, stream=err)
58
89
  result = handler(args, context)
@@ -60,12 +91,15 @@ def run(
60
91
  return int(exc.code or 0)
61
92
  except RuntimeError as exc:
62
93
  payload = trim_error_response(_parse_error_payload(exc))
94
+ payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
63
95
  return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
64
96
  except QingflowApiError as exc:
65
97
  payload = trim_error_response(exc.to_dict())
98
+ payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
66
99
  return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
67
100
  finally:
68
- context.close()
101
+ if "context" in locals():
102
+ context.close()
69
103
 
70
104
  exit_code = _result_exit_code(result)
71
105
  trimmed_result = trim_public_response(resolve_cli_tool_name(args), result) if isinstance(result, dict) else result
@@ -104,6 +138,173 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
104
138
  return global_args + remaining
105
139
 
106
140
 
141
+ def _should_force_json_output(args: argparse.Namespace) -> bool:
142
+ if bool(getattr(args, "force_json_output", False)):
143
+ return True
144
+ if (
145
+ getattr(args, "command", "") == "builder"
146
+ and getattr(args, "builder_app_command", "") == "repair-code-blocks"
147
+ and bool(getattr(args, "apply", False))
148
+ ):
149
+ return True
150
+ return False
151
+
152
+
153
+ def _should_force_json_output_argv(argv: list[str]) -> bool:
154
+ tokens = _strip_global_args(argv)
155
+ if not tokens or tokens[0] not in {"builder", "build"}:
156
+ return False
157
+ if len(tokens) < 3:
158
+ return False
159
+ section = tokens[1]
160
+ action = tokens[2]
161
+ if section in {"package", "button", "associated-resource", "associated-resources", "portal", "schema", "layout", "views", "flow", "charts"}:
162
+ return action == "apply"
163
+ if section == "publish":
164
+ return action == "verify"
165
+ if section == "app":
166
+ if action == "release-edit-lock-if-mine":
167
+ return True
168
+ if action == "repair-code-blocks":
169
+ return "--apply" in tokens
170
+ return False
171
+
172
+
173
+ def _maybe_attach_builder_apply_error_envelope_from_args(args: argparse.Namespace, payload: dict[str, Any]) -> dict[str, Any]:
174
+ operation = _builder_apply_operation_from_args(args)
175
+ if not operation:
176
+ return payload
177
+ enriched = dict(payload)
178
+ enriched.setdefault("status", "failed")
179
+ enriched.setdefault("ok", False)
180
+ enriched.setdefault("write_executed", False)
181
+ enriched.setdefault("safe_to_retry", False)
182
+ _copy_arg_identity(enriched, args)
183
+ return _attach_builder_apply_envelope(operation, enriched)
184
+
185
+
186
+ def _maybe_attach_builder_apply_error_envelope_from_argv(argv: list[str], payload: dict[str, Any]) -> dict[str, Any]:
187
+ operation = _builder_apply_operation_from_argv(argv)
188
+ if not operation:
189
+ return payload
190
+ enriched = dict(payload)
191
+ enriched.setdefault("status", "failed")
192
+ enriched.setdefault("ok", False)
193
+ enriched.setdefault("write_executed", False)
194
+ enriched.setdefault("safe_to_retry", False)
195
+ _copy_argv_identity(enriched, argv)
196
+ return _attach_builder_apply_envelope(operation, enriched)
197
+
198
+
199
+ def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
200
+ if getattr(args, "command", "") not in {"builder", "build"}:
201
+ return None
202
+ section = str(getattr(args, "builder_command", "") or "")
203
+ if section == "package" and getattr(args, "builder_package_command", "") == "apply":
204
+ return "package_apply"
205
+ if section == "button" and getattr(args, "builder_button_command", "") == "apply":
206
+ return "app_custom_buttons_apply"
207
+ if section in {"associated-resource", "associated-resources"} and getattr(args, "builder_associated_resource_command", "") == "apply":
208
+ return "app_associated_resources_apply"
209
+ if section == "portal" and getattr(args, "builder_portal_command", "") == "apply":
210
+ return "portal_apply"
211
+ if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
212
+ return "app_schema_apply"
213
+ if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
214
+ return "app_layout_apply"
215
+ if section == "views" and getattr(args, "builder_views_command", "") == "apply":
216
+ return "app_views_apply"
217
+ if section == "flow" and getattr(args, "builder_flow_command", "") == "apply":
218
+ return "app_flow_apply"
219
+ if section == "charts" and getattr(args, "builder_charts_command", "") == "apply":
220
+ return "app_charts_apply"
221
+ if section == "publish" and getattr(args, "builder_publish_command", "") == "verify":
222
+ return "app_publish_verify"
223
+ return None
224
+
225
+
226
+ def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
227
+ tokens = _strip_global_args(argv)
228
+ if not tokens or tokens[0] not in {"builder", "build"} or len(tokens) < 3:
229
+ return None
230
+ section = tokens[1]
231
+ action = tokens[2]
232
+ if action != "apply" and not (section == "publish" and action == "verify"):
233
+ return None
234
+ mapping = {
235
+ "package": "package_apply",
236
+ "button": "app_custom_buttons_apply",
237
+ "associated-resource": "app_associated_resources_apply",
238
+ "associated-resources": "app_associated_resources_apply",
239
+ "portal": "portal_apply",
240
+ "schema": "app_schema_apply",
241
+ "layout": "app_layout_apply",
242
+ "views": "app_views_apply",
243
+ "flow": "app_flow_apply",
244
+ "charts": "app_charts_apply",
245
+ "publish": "app_publish_verify",
246
+ }
247
+ return mapping.get(section)
248
+
249
+
250
+ def _copy_arg_identity(payload: dict[str, Any], args: argparse.Namespace) -> None:
251
+ for attr, key in (
252
+ ("app_key", "app_key"),
253
+ ("app_name", "app_name"),
254
+ ("app_title", "app_title"),
255
+ ("package_id", "package_id"),
256
+ ("dash_key", "dash_key"),
257
+ ("dash_name", "dash_name"),
258
+ ):
259
+ value = getattr(args, attr, None)
260
+ if value not in (None, ""):
261
+ payload.setdefault(key, value)
262
+
263
+
264
+ def _copy_argv_identity(payload: dict[str, Any], argv: list[str]) -> None:
265
+ tokens = _strip_global_args(argv)
266
+ option_to_key = {
267
+ "--app-key": "app_key",
268
+ "--app-name": "app_name",
269
+ "--app-title": "app_title",
270
+ "--package-id": "package_id",
271
+ "--dash-key": "dash_key",
272
+ "--dash-name": "dash_name",
273
+ }
274
+ index = 0
275
+ while index < len(tokens):
276
+ token = tokens[index]
277
+ if token in option_to_key and index + 1 < len(tokens):
278
+ payload.setdefault(option_to_key[token], tokens[index + 1])
279
+ index += 2
280
+ continue
281
+ for option, key in option_to_key.items():
282
+ prefix = f"{option}="
283
+ if token.startswith(prefix):
284
+ payload.setdefault(key, token[len(prefix) :])
285
+ break
286
+ index += 1
287
+
288
+
289
+ def _strip_global_args(argv: list[str]) -> list[str]:
290
+ stripped: list[str] = []
291
+ index = 0
292
+ while index < len(argv):
293
+ token = argv[index]
294
+ if token == "--json":
295
+ index += 1
296
+ continue
297
+ if token == "--profile":
298
+ index += 2
299
+ continue
300
+ if token.startswith("--profile="):
301
+ index += 1
302
+ continue
303
+ stripped.append(token)
304
+ index += 1
305
+ return stripped
306
+
307
+
107
308
  def _parse_error_payload(exc: RuntimeError) -> dict[str, Any]:
108
309
  raw = str(exc)
109
310
  try:
@@ -38,7 +38,6 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
38
38
  PublicToolSpec(USER_DOMAIN, "workspace_get", ("workspace_get",), ("workspace", "get")),
39
39
  PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
40
40
  PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
41
- PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
42
41
  PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
43
42
  PublicToolSpec(USER_DOMAIN, "portal_list", ("portal_list",), ("portal", "list"), cli_show_effective_context=True),
44
43
  PublicToolSpec(USER_DOMAIN, "portal_get", ("portal_get",), ("portal", "get"), cli_show_effective_context=True),
@@ -78,12 +77,13 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
78
77
  ("record_code_block_schema_get_public",),
79
78
  ("record", "schema", "code-block"),
80
79
  ),
81
- PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), cli_public=False),
82
- PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), cli_public=False),
80
+ PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), ("record", "member-candidates")),
81
+ PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), ("record", "department-candidates")),
83
82
  PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze"), mcp_public=False),
84
83
  PublicToolSpec(USER_DOMAIN, "record_list", ("record_list",), ("record", "list"), cli_show_effective_context=True),
85
84
  PublicToolSpec(USER_DOMAIN, "record_access", ("record_access",), ("record", "access"), cli_show_effective_context=True),
86
85
  PublicToolSpec(USER_DOMAIN, "record_get", ("record_get_public",), ("record", "get"), cli_show_effective_context=True),
86
+ PublicToolSpec(USER_DOMAIN, "record_logs_get", ("record_logs_get",), ("record", "logs"), cli_show_effective_context=True),
87
87
  PublicToolSpec(USER_DOMAIN, "record_insert", ("record_insert_public",), ("record", "insert"), cli_show_effective_context=True, cli_context_write=True),
88
88
  PublicToolSpec(USER_DOMAIN, "record_update", ("record_update_public",), ("record", "update"), cli_show_effective_context=True, cli_context_write=True),
89
89
  PublicToolSpec(USER_DOMAIN, "record_delete", ("record_delete_public",), ("record", "delete"), cli_show_effective_context=True, cli_context_write=True),
@@ -127,6 +127,8 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
127
127
  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),
128
128
  PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
129
129
  PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
130
+ PublicToolSpec(BUILDER_DOMAIN, "workspace_icon_catalog_get", ("workspace_icon_catalog_get",), ("builder", "icon", "catalog"), has_contract=True, cli_show_effective_context=True),
131
+ PublicToolSpec(BUILDER_DOMAIN, "package_list", ("package_list",), ("builder", "package", "list"), has_contract=True, cli_show_effective_context=True),
130
132
  PublicToolSpec(BUILDER_DOMAIN, "package_get", ("package_get",), ("builder", "package", "get"), has_contract=True, cli_show_effective_context=True),
131
133
  PublicToolSpec(BUILDER_DOMAIN, "package_apply", ("package_apply",), ("builder", "package", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
132
134
  PublicToolSpec(BUILDER_DOMAIN, "solution_install", ("solution_install",), ("builder", "solution", "install"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
@@ -136,11 +138,10 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
136
138
  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),
137
139
  PublicToolSpec(BUILDER_DOMAIN, "app_resolve", ("app_resolve",), ("builder", "app", "resolve"), has_contract=True, cli_show_effective_context=True),
138
140
  PublicToolSpec(BUILDER_DOMAIN, "button_style_catalog_get", ("button_style_catalog_get",), ("builder", "button", "catalog"), has_contract=True, cli_show_effective_context=True),
139
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_list", ("app_custom_button_list",), ("builder", "button", "list"), has_contract=True, cli_show_effective_context=True),
140
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_get", ("app_custom_button_get",), ("builder", "button", "get"), has_contract=True, cli_show_effective_context=True),
141
- 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),
142
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_update", ("app_custom_button_update",), ("builder", "button", "update"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
143
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_delete", ("app_custom_button_delete",), ("builder", "button", "delete"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
141
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_buttons", ("app_get_buttons",), ("builder", "button", "get"), has_contract=True, cli_show_effective_context=True),
142
+ PublicToolSpec(BUILDER_DOMAIN, "app_custom_buttons_apply", ("app_custom_buttons_apply",), ("builder", "button", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
143
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_associated_resources", ("app_get_associated_resources",), ("builder", "associated-resource", "get"), has_contract=True, cli_show_effective_context=True),
144
+ PublicToolSpec(BUILDER_DOMAIN, "app_associated_resources_apply", ("app_associated_resources_apply",), ("builder", "associated-resource", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
144
145
  PublicToolSpec(BUILDER_DOMAIN, "app_get", ("app_get",), ("builder", "app", "get", "summary"), has_contract=True, cli_show_effective_context=True),
145
146
  PublicToolSpec(BUILDER_DOMAIN, "app_get_fields", ("app_get_fields",), ("builder", "app", "get", "fields"), has_contract=True, cli_show_effective_context=True),
146
147
  PublicToolSpec(BUILDER_DOMAIN, "app_repair_code_blocks", ("app_repair_code_blocks",), ("builder", "app", "repair-code-blocks"), has_contract=True, cli_show_effective_context=True),
@@ -154,6 +155,8 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
154
155
  PublicToolSpec(BUILDER_DOMAIN, "chart_get", ("chart_get",), ("builder", "chart", "get"), has_contract=True, cli_show_effective_context=True),
155
156
  PublicToolSpec(BUILDER_DOMAIN, "app_schema_apply", ("app_schema_apply",), ("builder", "schema", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
156
157
  PublicToolSpec(BUILDER_DOMAIN, "app_layout_apply", ("app_layout_apply",), ("builder", "layout", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
158
+ PublicToolSpec(BUILDER_DOMAIN, "app_flow_get_schema", ("app_flow_get_schema",), ("builder", "flow", "schema"), has_contract=True, cli_show_effective_context=True),
159
+ PublicToolSpec(BUILDER_DOMAIN, "app_flow_get", ("app_flow_get",), ("builder", "flow", "get"), has_contract=True, cli_show_effective_context=True),
157
160
  PublicToolSpec(BUILDER_DOMAIN, "app_flow_apply", ("app_flow_apply",), ("builder", "flow", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
158
161
  PublicToolSpec(BUILDER_DOMAIN, "app_views_apply", ("app_views_apply",), ("builder", "views", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
159
162
  PublicToolSpec(BUILDER_DOMAIN, "app_charts_apply", ("app_charts_apply",), ("builder", "charts", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),