@josephyan/qingflow-cli 0.2.0-beta.999 → 1.0.5

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 (36) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/src/qingflow_mcp/__init__.py +1 -1
  5. package/src/qingflow_mcp/backend_client.py +109 -0
  6. package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
  7. package/src/qingflow_mcp/builder_facade/models.py +44 -5
  8. package/src/qingflow_mcp/builder_facade/service.py +21 -8
  9. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  10. package/src/qingflow_mcp/cli/commands/app.py +47 -1
  11. package/src/qingflow_mcp/cli/commands/builder.py +7 -0
  12. package/src/qingflow_mcp/cli/commands/exports.py +111 -0
  13. package/src/qingflow_mcp/cli/commands/record.py +20 -0
  14. package/src/qingflow_mcp/cli/commands/task.py +644 -22
  15. package/src/qingflow_mcp/cli/commands/workspace.py +49 -50
  16. package/src/qingflow_mcp/cli/context.py +3 -0
  17. package/src/qingflow_mcp/cli/formatters.py +139 -4
  18. package/src/qingflow_mcp/cli/interaction.py +72 -0
  19. package/src/qingflow_mcp/cli/main.py +2 -0
  20. package/src/qingflow_mcp/cli/terminal_ui.py +55 -9
  21. package/src/qingflow_mcp/errors.py +2 -2
  22. package/src/qingflow_mcp/export_store.py +14 -0
  23. package/src/qingflow_mcp/public_surface.py +6 -0
  24. package/src/qingflow_mcp/response_trim.py +40 -1
  25. package/src/qingflow_mcp/server.py +22 -0
  26. package/src/qingflow_mcp/server_app_builder.py +4 -0
  27. package/src/qingflow_mcp/server_app_user.py +104 -8
  28. package/src/qingflow_mcp/session_store.py +57 -6
  29. package/src/qingflow_mcp/tools/ai_builder_tools.py +59 -16
  30. package/src/qingflow_mcp/tools/auth_tools.py +26 -0
  31. package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
  32. package/src/qingflow_mcp/tools/export_tools.py +1565 -0
  33. package/src/qingflow_mcp/tools/import_tools.py +42 -2
  34. package/src/qingflow_mcp/tools/record_tools.py +551 -16
  35. package/src/qingflow_mcp/tools/resource_read_tools.py +40 -1
  36. package/src/qingflow_mcp/tools/task_context_tools.py +26 -8
@@ -96,6 +96,9 @@ class ResourceReadTools(ToolBase):
96
96
  )
97
97
  )
98
98
  if system_view is not None:
99
+ export_capability = _view_export_capability_payload(
100
+ supported=_export_supported_for_view_type(system_view["view_type"])
101
+ )
99
102
  return self._run(
100
103
  profile,
101
104
  lambda session_profile, _context: {
@@ -106,11 +109,17 @@ class ResourceReadTools(ToolBase):
106
109
  {
107
110
  "code": "VIEW_APP_KEY_UNRESOLVED",
108
111
  "message": f"view_get could not resolve app_key for system view `{view_id}`; keep using the app_key from the parent app context.",
109
- }
112
+ },
113
+ {
114
+ "code": "VIEW_EXPORT_APP_CONTEXT_REQUIRED",
115
+ "message": f"view_get supports exporting `{view_id}`, but the export call still needs the parent app context `app_key`.",
116
+ },
110
117
  ],
111
118
  "verification": {
112
119
  "view_exists": True,
113
120
  "descriptor_only": True,
121
+ "export_route_supported": export_capability["supported"],
122
+ "export_permission_verified": export_capability["permission_verified"],
114
123
  },
115
124
  "data": {
116
125
  "app_key": None,
@@ -120,11 +129,13 @@ class ResourceReadTools(ToolBase):
120
129
  "view_type": system_view["view_type"],
121
130
  "visible_columns": [],
122
131
  "analysis_supported": system_view["analysis_supported"],
132
+ "export_capability": export_capability,
123
133
  },
124
134
  },
125
135
  )
126
136
 
127
137
  def runner(session_profile, context):
138
+ raw_view_type = None
128
139
  warnings: list[JSONObject] = []
129
140
  verification = {
130
141
  "view_exists": True,
@@ -150,6 +161,11 @@ class ResourceReadTools(ToolBase):
150
161
  str(base_info.get("viewgraphType") or "").strip()
151
162
  or str(config.get("viewgraphType") or config.get("viewType") or "").strip()
152
163
  )
164
+ export_capability = _view_export_capability_payload(
165
+ supported=_export_supported_for_view_type(raw_view_type or None)
166
+ )
167
+ verification["export_route_supported"] = export_capability["supported"]
168
+ verification["export_permission_verified"] = export_capability["permission_verified"]
153
169
  resolved_app_key = str(base_info.get("appKey") or config.get("appKey") or "").strip() or None
154
170
  if not resolved_app_key:
155
171
  resolved_app_key = self._resolve_app_key_from_view_form(context=context, view_key=view_key)
@@ -165,6 +181,13 @@ class ResourceReadTools(ToolBase):
165
181
  "message": f"view_get could not resolve app_key for `{view_id}` from view metadata; keep using the app_key from the parent app or portal context.",
166
182
  }
167
183
  )
184
+ if export_capability["supported"]:
185
+ warnings.append(
186
+ {
187
+ "code": "VIEW_EXPORT_APP_CONTEXT_REQUIRED",
188
+ "message": f"view_get supports exporting `{view_id}`, but the export call still needs an explicit `app_key` from the parent app context.",
189
+ }
190
+ )
168
191
  return {
169
192
  "profile": profile,
170
193
  "ws_id": session_profile.selected_ws_id,
@@ -186,6 +209,7 @@ class ResourceReadTools(ToolBase):
186
209
  if str(item.get("queTitle") or item.get("title") or "").strip()
187
210
  ],
188
211
  "analysis_supported": _analysis_supported_for_view_type(raw_view_type or None),
212
+ "export_capability": export_capability,
189
213
  },
190
214
  }
191
215
 
@@ -464,6 +488,21 @@ def _lookup_system_view_descriptor(view_id: str) -> dict[str, Any] | None:
464
488
  return None
465
489
 
466
490
 
491
+ def _view_export_capability_payload(*, supported: bool) -> JSONObject:
492
+ return {
493
+ "supported": supported,
494
+ "tool": "record_export_start",
495
+ "format": "xlsx",
496
+ "async": True,
497
+ "requires_app_key": True,
498
+ "permission_verified": False,
499
+ }
500
+
501
+
502
+ def _export_supported_for_view_type(view_type: str | None) -> bool:
503
+ return _analysis_supported_for_view_type(view_type)
504
+
505
+
467
506
  def _normalize_view_type(view_type: Any) -> str | None:
468
507
  value = str(view_type or "").strip()
469
508
  if not value:
@@ -1762,6 +1762,7 @@ class TaskContextTools(ToolBase):
1762
1762
  "apply_time": record.get("apply_time"),
1763
1763
  "last_update_time": record.get("last_update_time"),
1764
1764
  "core_fields": self._task_record_core_fields(record.get("answers") or []),
1765
+ "all_fields": self._task_record_all_fields(record.get("answers") or []),
1765
1766
  },
1766
1767
  "available_actions": available_actions,
1767
1768
  "editable_fields": [
@@ -1921,9 +1922,21 @@ class TaskContextTools(ToolBase):
1921
1922
  return None
1922
1923
 
1923
1924
  def _task_record_core_fields(self, answers: Any, *, limit: int = 12) -> dict[str, Any]:
1925
+ return self._task_record_field_map(answers, limit=limit, truncate_text=160)
1926
+
1927
+ def _task_record_all_fields(self, answers: Any) -> dict[str, Any]:
1928
+ return self._task_record_field_map(answers, limit=None, truncate_text=None)
1929
+
1930
+ def _task_record_field_map(
1931
+ self,
1932
+ answers: Any,
1933
+ *,
1934
+ limit: int | None,
1935
+ truncate_text: int | None,
1936
+ ) -> dict[str, Any]:
1924
1937
  if not isinstance(answers, list):
1925
1938
  return {}
1926
- core_fields: dict[str, Any] = {}
1939
+ field_map: dict[str, Any] = {}
1927
1940
  for answer in answers:
1928
1941
  if not isinstance(answer, dict):
1929
1942
  continue
@@ -1943,19 +1956,24 @@ class TaskContextTools(ToolBase):
1943
1956
  value = values[0] if len(values) == 1 else values
1944
1957
  if value in (None, "", []):
1945
1958
  continue
1946
- core_fields[str(title)] = self._compact_task_value(value)
1947
- if len(core_fields) >= limit:
1959
+ field_map[str(title)] = self._compact_task_value(value, truncate_text=truncate_text)
1960
+ if limit is not None and len(field_map) >= limit:
1948
1961
  break
1949
- return core_fields
1962
+ return field_map
1950
1963
 
1951
- def _compact_task_value(self, value: Any) -> Any:
1964
+ def _compact_task_value(self, value: Any, *, truncate_text: int | None = 160) -> Any:
1952
1965
  if isinstance(value, list):
1953
- return [self._compact_task_value(item) for item in value[:8]]
1966
+ items = [self._compact_task_value(item, truncate_text=truncate_text) for item in value]
1967
+ if truncate_text is not None:
1968
+ return items[:8]
1969
+ return items
1954
1970
  text = re.sub(r"<[^>]+>", " ", str(value))
1955
1971
  text = re.sub(r"\s+", " ", text).strip()
1956
- if len(text) <= 160:
1972
+ if truncate_text is None or len(text) <= truncate_text:
1957
1973
  return text
1958
- return text[:157].rstrip() + "..."
1974
+ if truncate_text <= 3:
1975
+ return text[:truncate_text]
1976
+ return text[: truncate_text - 3].rstrip() + "..."
1959
1977
 
1960
1978
  def _compact_task_editable_field(self, field: dict[str, Any], update_schema: dict[str, Any]) -> dict[str, Any]:
1961
1979
  payload_template = update_schema.get("payload_template") if isinstance(update_schema.get("payload_template"), dict) else {}