@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.
- package/README.md +30 -0
- package/docs/local-agent-install.md +235 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +204 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +547 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +985 -0
- package/src/qingflow_mcp/builder_facade/service.py +8243 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +184 -0
- package/src/qingflow_mcp/cli/commands/common.py +47 -0
- package/src/qingflow_mcp/cli/commands/imports.py +86 -0
- package/src/qingflow_mcp/cli/commands/record.py +202 -0
- package/src/qingflow_mcp/cli/commands/task.py +87 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +269 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +147 -0
- package/src/qingflow_mcp/config.py +221 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/server.py +211 -0
- package/src/qingflow_mcp/server_app_builder.py +387 -0
- package/src/qingflow_mcp/server_app_user.py +317 -0
- package/src/qingflow_mcp/session_store.py +289 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2339 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +853 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
- package/src/qingflow_mcp/tools/app_tools.py +850 -0
- package/src/qingflow_mcp/tools/approval_tools.py +833 -0
- package/src/qingflow_mcp/tools/auth_tools.py +697 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
- package/src/qingflow_mcp/tools/directory_tools.py +648 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +385 -0
- package/src/qingflow_mcp/tools/import_tools.py +1971 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +240 -0
- package/src/qingflow_mcp/tools/portal_tools.py +131 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
- package/src/qingflow_mcp/tools/record_tools.py +12739 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
- package/src/qingflow_mcp/tools/task_tools.py +843 -0
- package/src/qingflow_mcp/tools/view_tools.py +280 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
- 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
|