@josephyan/qingflow-cli 0.2.0-beta.55

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 (79) hide show
  1. package/README.md +30 -0
  2. package/docs/local-agent-install.md +235 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +204 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +5 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +547 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +985 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +8243 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +184 -0
  21. package/src/qingflow_mcp/cli/commands/common.py +47 -0
  22. package/src/qingflow_mcp/cli/commands/imports.py +86 -0
  23. package/src/qingflow_mcp/cli/commands/record.py +202 -0
  24. package/src/qingflow_mcp/cli/commands/task.py +87 -0
  25. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  26. package/src/qingflow_mcp/cli/context.py +48 -0
  27. package/src/qingflow_mcp/cli/formatters.py +269 -0
  28. package/src/qingflow_mcp/cli/json_io.py +50 -0
  29. package/src/qingflow_mcp/cli/main.py +147 -0
  30. package/src/qingflow_mcp/config.py +221 -0
  31. package/src/qingflow_mcp/errors.py +66 -0
  32. package/src/qingflow_mcp/import_store.py +121 -0
  33. package/src/qingflow_mcp/json_types.py +18 -0
  34. package/src/qingflow_mcp/list_type_labels.py +76 -0
  35. package/src/qingflow_mcp/server.py +211 -0
  36. package/src/qingflow_mcp/server_app_builder.py +387 -0
  37. package/src/qingflow_mcp/server_app_user.py +317 -0
  38. package/src/qingflow_mcp/session_store.py +289 -0
  39. package/src/qingflow_mcp/solution/__init__.py +6 -0
  40. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  41. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  42. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  43. package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
  44. package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
  45. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  46. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  47. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  48. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  49. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  50. package/src/qingflow_mcp/solution/design_session.py +222 -0
  51. package/src/qingflow_mcp/solution/design_store.py +100 -0
  52. package/src/qingflow_mcp/solution/executor.py +2339 -0
  53. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  54. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  55. package/src/qingflow_mcp/solution/run_store.py +244 -0
  56. package/src/qingflow_mcp/solution/spec_models.py +853 -0
  57. package/src/qingflow_mcp/tools/__init__.py +1 -0
  58. package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
  59. package/src/qingflow_mcp/tools/app_tools.py +850 -0
  60. package/src/qingflow_mcp/tools/approval_tools.py +833 -0
  61. package/src/qingflow_mcp/tools/auth_tools.py +697 -0
  62. package/src/qingflow_mcp/tools/base.py +81 -0
  63. package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
  64. package/src/qingflow_mcp/tools/directory_tools.py +648 -0
  65. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  66. package/src/qingflow_mcp/tools/file_tools.py +385 -0
  67. package/src/qingflow_mcp/tools/import_tools.py +1971 -0
  68. package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
  69. package/src/qingflow_mcp/tools/package_tools.py +240 -0
  70. package/src/qingflow_mcp/tools/portal_tools.py +131 -0
  71. package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
  72. package/src/qingflow_mcp/tools/record_tools.py +12739 -0
  73. package/src/qingflow_mcp/tools/role_tools.py +94 -0
  74. package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
  75. package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
  76. package/src/qingflow_mcp/tools/task_tools.py +843 -0
  77. package/src/qingflow_mcp/tools/view_tools.py +280 -0
  78. package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
  79. package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from copy import deepcopy
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timezone
8
+ from hashlib import sha256
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from ..config import get_mcp_home
13
+
14
+
15
+ def get_bootstrap_runs_path() -> Path:
16
+ custom_home = os.getenv("QINGFLOW_MCP_BOOTSTRAP_HOME")
17
+ if custom_home:
18
+ return Path(custom_home).expanduser()
19
+ return get_mcp_home() / "bootstrap-runs"
20
+
21
+
22
+ def fingerprint_payload(payload: dict[str, Any]) -> str:
23
+ text = json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
24
+ return sha256(text.encode("utf-8")).hexdigest()
25
+
26
+
27
+ def utc_now() -> str:
28
+ return datetime.now(timezone.utc).isoformat()
29
+
30
+
31
+ def storage_file_stem(value: str) -> str:
32
+ safe = sanitize_key(value).strip("_") or "item"
33
+ safe = safe[:80].rstrip("_") or "item"
34
+ digest = sha256(value.encode("utf-8")).hexdigest()[:12]
35
+ return f"{safe}--{digest}"
36
+
37
+
38
+ def resolve_storage_path(base_dir: Path, *, key: str, id_field: str) -> Path:
39
+ preferred = base_dir / f"{storage_file_stem(key)}.json"
40
+ if preferred.exists():
41
+ return preferred
42
+ legacy = base_dir / f"{sanitize_key(key)}.json"
43
+ if not legacy.exists():
44
+ return preferred
45
+ try:
46
+ payload = json.loads(legacy.read_text(encoding="utf-8"))
47
+ except Exception:
48
+ return preferred
49
+ if payload.get(id_field) == key:
50
+ return legacy
51
+ return preferred
52
+
53
+
54
+ @dataclass(slots=True)
55
+ class RunArtifactStore:
56
+ path: Path
57
+ data: dict[str, Any]
58
+
59
+ @classmethod
60
+ def open(
61
+ cls,
62
+ *,
63
+ idempotency_key: str,
64
+ normalized_solution_spec: dict[str, Any],
65
+ request_fingerprint: str,
66
+ run_label: str | None,
67
+ initial_artifacts: dict[str, Any] | None = None,
68
+ ) -> "RunArtifactStore":
69
+ base_dir = get_bootstrap_runs_path()
70
+ base_dir.mkdir(parents=True, exist_ok=True)
71
+ path = resolve_storage_path(base_dir, key=idempotency_key, id_field="idempotency_key")
72
+ if path.exists():
73
+ data = json.loads(path.read_text(encoding="utf-8"))
74
+ stored_key = data.get("idempotency_key")
75
+ if stored_key != idempotency_key:
76
+ raise ValueError(f"existing run artifact at '{path}' belongs to '{stored_key}', not '{idempotency_key}'")
77
+ existing_fingerprint = data.get("request_fingerprint")
78
+ if isinstance(initial_artifacts, dict) and (not existing_fingerprint or existing_fingerprint == request_fingerprint):
79
+ merged_artifacts = _merge_nested_dicts(data.get("artifacts", {}), initial_artifacts)
80
+ if merged_artifacts != data.get("artifacts", {}):
81
+ data["artifacts"] = merged_artifacts
82
+ data["updated_at"] = utc_now()
83
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
84
+ else:
85
+ data = {
86
+ "idempotency_key": idempotency_key,
87
+ "request_fingerprint": request_fingerprint,
88
+ "normalized_solution_spec": normalized_solution_spec,
89
+ "run_label": run_label,
90
+ "artifacts": deepcopy(initial_artifacts)
91
+ if isinstance(initial_artifacts, dict)
92
+ else {
93
+ "package": {},
94
+ "roles": {},
95
+ "apps": {},
96
+ "views": {},
97
+ "charts": {},
98
+ "records": {},
99
+ "portal": {},
100
+ "navigation": {},
101
+ "field_maps": {},
102
+ },
103
+ "steps": {},
104
+ "errors": [],
105
+ "status": "pending",
106
+ "created_at": utc_now(),
107
+ "updated_at": utc_now(),
108
+ }
109
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
110
+ return cls(path=path, data=data)
111
+
112
+ def ensure_apply_fingerprint(self, request_fingerprint: str) -> None:
113
+ existing = self.data.get("request_fingerprint")
114
+ if existing and existing != request_fingerprint:
115
+ raise ValueError("idempotency_key already exists with a different request fingerprint")
116
+
117
+ def record_step_started(self, step_name: str, request_fingerprint: str, debug_context: dict[str, Any] | None = None) -> None:
118
+ step = self.data["steps"].get(step_name, {})
119
+ step.update(
120
+ {
121
+ "status": "running",
122
+ "request_fingerprint": request_fingerprint,
123
+ "started_at": step.get("started_at") or utc_now(),
124
+ "finished_at": None,
125
+ "error": None,
126
+ "debug_context": deepcopy(debug_context) if debug_context is not None else step.get("debug_context"),
127
+ }
128
+ )
129
+ self.data["steps"][step_name] = step
130
+ self.data["status"] = "running"
131
+ self._flush()
132
+
133
+ def record_step_completed(
134
+ self,
135
+ step_name: str,
136
+ artifact_keys: dict[str, Any] | None = None,
137
+ result: dict[str, Any] | None = None,
138
+ debug_context: dict[str, Any] | None = None,
139
+ ) -> None:
140
+ step = self.data["steps"].setdefault(step_name, {})
141
+ step.update(
142
+ {
143
+ "status": "completed",
144
+ "artifact_keys": artifact_keys or step.get("artifact_keys") or {},
145
+ "result": result or step.get("result") or {},
146
+ "finished_at": utc_now(),
147
+ "error": None,
148
+ "debug_context": deepcopy(debug_context) if debug_context is not None else step.get("debug_context"),
149
+ }
150
+ )
151
+ self.data["errors"] = [entry for entry in self.data.get("errors", []) if entry.get("step_name") != step_name]
152
+ self._flush()
153
+
154
+ def record_step_failed(self, step_name: str, error: Any, debug_context: dict[str, Any] | None = None) -> None:
155
+ error_text, error_payload = _normalize_error_payload(error)
156
+ step = self.data["steps"].setdefault(step_name, {})
157
+ step.update(
158
+ {
159
+ "status": "failed",
160
+ "finished_at": utc_now(),
161
+ "error": error_text,
162
+ "debug_context": deepcopy(debug_context) if debug_context is not None else step.get("debug_context"),
163
+ }
164
+ )
165
+ if error_payload is not None:
166
+ step["error_payload"] = deepcopy(error_payload)
167
+ entry = {
168
+ "step_name": step_name,
169
+ "error": error_text,
170
+ "at": utc_now(),
171
+ "debug_context": deepcopy(debug_context) if debug_context is not None else step.get("debug_context"),
172
+ }
173
+ if error_payload is not None:
174
+ entry["error_payload"] = deepcopy(error_payload)
175
+ if isinstance(error_payload.get("category"), str):
176
+ entry["category"] = error_payload["category"]
177
+ if error_payload.get("details") is not None:
178
+ entry["detail"] = deepcopy(error_payload["details"])
179
+ elif error_payload.get("message") is not None:
180
+ entry["detail"] = deepcopy(error_payload["message"])
181
+ self.data.setdefault("errors", []).append(entry)
182
+ self.data["status"] = "failed"
183
+ self._flush()
184
+
185
+ def set_artifact(self, section: str, key: str, value: Any) -> None:
186
+ self.data["artifacts"].setdefault(section, {})
187
+ self.data["artifacts"][section][key] = deepcopy(value)
188
+ self._flush()
189
+
190
+ def get_artifact(self, section: str, key: str, default: Any = None) -> Any:
191
+ return self.data.get("artifacts", {}).get(section, {}).get(key, default)
192
+
193
+ def get_step_status(self, step_name: str) -> str | None:
194
+ step = self.data.get("steps", {}).get(step_name)
195
+ return step.get("status") if step else None
196
+
197
+ def should_run(self, step_name: str, *, force: bool = False) -> bool:
198
+ if force:
199
+ return True
200
+ return self.get_step_status(step_name) != "completed"
201
+
202
+ def mark_finished(self, *, status: str) -> None:
203
+ self.data["status"] = status
204
+ self._flush()
205
+
206
+ def summary(self) -> dict[str, Any]:
207
+ return {
208
+ "artifacts": deepcopy(self.data.get("artifacts", {})),
209
+ "step_results": deepcopy(self.data.get("steps", {})),
210
+ "errors": deepcopy(self.data.get("errors", [])),
211
+ "status": self.data.get("status", "pending"),
212
+ "run_path": str(self.path),
213
+ }
214
+
215
+ def _flush(self) -> None:
216
+ self.data["updated_at"] = utc_now()
217
+ self.path.write_text(json.dumps(self.data, ensure_ascii=False, indent=2), encoding="utf-8")
218
+
219
+
220
+ def sanitize_key(value: str) -> str:
221
+ return "".join(ch if ch.isalnum() or ch in ("-", "_") else "_" for ch in value)
222
+
223
+
224
+ def _merge_nested_dicts(base: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
225
+ merged = deepcopy(base)
226
+ for key, value in incoming.items():
227
+ if isinstance(value, dict) and isinstance(merged.get(key), dict):
228
+ merged[key] = _merge_nested_dicts(merged[key], value)
229
+ else:
230
+ merged[key] = deepcopy(value)
231
+ return merged
232
+
233
+
234
+ def _normalize_error_payload(error: Any) -> tuple[str, dict[str, Any] | None]:
235
+ if isinstance(error, dict):
236
+ return json.dumps(error, ensure_ascii=False), deepcopy(error)
237
+ text = str(error)
238
+ try:
239
+ parsed = json.loads(text)
240
+ except Exception:
241
+ return text, None
242
+ if isinstance(parsed, dict):
243
+ return text, parsed
244
+ return text, None