loki-mode 6.75.2 → 6.76.0
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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +672 -0
- package/autonomy/run.sh +1 -1
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/mcp/magic_tools.py +471 -0
- package/mcp/server.py +13 -0
- package/package.json +1 -1
- package/references/magic-modules-patterns.md +634 -0
- package/skills/00-index.md +1 -0
- package/skills/magic-modules.md +205 -0
- package/web-app/dist/assets/{AdminPage-D4QSV6Zi.js → AdminPage-DwVUK4v9.js} +1 -1
- package/web-app/dist/assets/{Avatar-88MlpLO5.js → Avatar-B7gqhcg3.js} +1 -1
- package/web-app/dist/assets/{Badge-DbGjLr4i.js → Badge-DA3xNJAS.js} +1 -1
- package/web-app/dist/assets/{Button-sp_FVGZj.js → Button-BPXURLaK.js} +1 -1
- package/web-app/dist/assets/{ComparePage-p2ENnfa7.js → ComparePage-B0JQMhKG.js} +1 -1
- package/web-app/dist/assets/GitHubIssuesPanel-D38-fy29.js +12 -0
- package/web-app/dist/assets/{GitHubPRsPanel-Bi_yrcAE.js → GitHubPRsPanel-DLPcW3N0.js} +2 -2
- package/web-app/dist/assets/{HomePage-BB83YPiX.js → HomePage-CzeoS2V_.js} +3 -3
- package/web-app/dist/assets/{LoginPage-BXUudCJ9.js → LoginPage-DqCzxsfx.js} +1 -1
- package/web-app/dist/assets/MagicPage-CBLqpa55.js +31 -0
- package/web-app/dist/assets/{MetricsPage-CX0Ahy-_.js → MetricsPage-CPYQR0zr.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-C4JqatEk.js → NotFoundPage-B62u4iCs.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-t5J2XAJT.js → ProjectPage-DNujSl6j.js} +67 -72
- package/web-app/dist/assets/{ProjectsPage-Bzpz1clk.js → ProjectsPage-uHG7kxB-.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-y_yl8FvH.js → SettingsPage-BaQJbOgL.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-B7d6pzMq.js → ShowcasePage-DQR_e-kg.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-C4tR33KU.js → SystemSettingsPage-C_Q_1WK4.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-DIOCfZIP.js → TeamsPage-DOFErDqX.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DlKyapXX.js → TemplatesPage-Ty72hILN.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Czg-ZC2k.js → TerminalOutput-DqOVnR1p.js} +7 -12
- package/web-app/dist/assets/{activity-h1wU9a0L.js → activity-BgBZ4s4c.js} +1 -1
- package/web-app/dist/assets/{bell-Bu8lsWOp.js → bell-C-UezVWi.js} +1 -1
- package/web-app/dist/assets/{bot-rWO7KjkQ.js → bot-D70fEnm5.js} +1 -1
- package/web-app/dist/assets/{check-BWp8L5Cy.js → check-CBohulxQ.js} +1 -1
- package/web-app/dist/assets/{chevron-left-Bw4I1yGm.js → chevron-left-C-emzUhB.js} +1 -1
- package/web-app/dist/assets/{circle-alert-C37PKXiC.js → circle-alert-8SRY0_GX.js} +1 -1
- package/web-app/dist/assets/{clock-DDScLol4.js → clock-mfq4XnPQ.js} +1 -1
- package/web-app/dist/assets/{cloud-DaYKPLaM.js → cloud-DpRM7T8t.js} +1 -1
- package/web-app/dist/assets/code-xml-1N2Ui-4c.js +6 -0
- package/web-app/dist/assets/{copy-DKIRv0VK.js → copy-LXquTgzI.js} +1 -1
- package/web-app/dist/assets/{database-CYZBHz51.js → database-S1dyXnuT.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CydJu0kl.js → dollar-sign-CRqk0dW5.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DqZ9gpdv.js → file-code-corner-B99CwY_6.js} +1 -1
- package/web-app/dist/assets/{file-plus-CzeFJWp3.js → file-plus-DZ5qnz5b.js} +1 -1
- package/web-app/dist/assets/{folder-open-4YWk08dP.js → folder-open-DBCm7yuF.js} +1 -1
- package/web-app/dist/assets/{git-commit-horizontal-wbqFPNID.js → git-commit-horizontal-DM1ERuNd.js} +1 -1
- package/web-app/dist/assets/{globe-Cby-g5Yb.js → globe-B7xEJSL_.js} +1 -1
- package/web-app/dist/assets/{hammer-BNScgGdp.js → hammer-Cgi3LTuS.js} +1 -1
- package/web-app/dist/assets/{index-6Z4B0I6r.js → index-BN52-GQT.js} +22 -17
- package/web-app/dist/assets/index-BfZSDej1.css +1 -0
- package/web-app/dist/assets/{layers-XfssQc5V.js → layers-Bi8RPIBC.js} +1 -1
- package/web-app/dist/assets/{lightbulb-EhnzRw7M.js → lightbulb-Doc_n8JX.js} +1 -1
- package/web-app/dist/assets/{loader-circle-BA0QIVGA.js → loader-circle-BB932A7A.js} +1 -1
- package/web-app/dist/assets/{lock-BABtHe6K.js → lock-Bt6gpMrs.js} +1 -1
- package/web-app/dist/assets/{mail-Dokiey5S.js → mail-BuzAu1IP.js} +1 -1
- package/web-app/dist/assets/{package-DbJyS1Ft.js → package-BE5FHxQ8.js} +1 -1
- package/web-app/dist/assets/{plus-BcAN8Kaj.js → plus-CNqABexN.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-B3dG1-Sb.js → refresh-cw-34B13ztx.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-Cs1Phctm.js → rotate-ccw-CrD2QB29.js} +1 -1
- package/web-app/dist/assets/{save-DsrNCZrP.js → save-DsJcqdnI.js} +1 -1
- package/web-app/dist/assets/{server-CpN2GX4G.js → server-BcgRMArA.js} +1 -1
- package/web-app/dist/assets/{shield-alert-CKJ1pzCz.js → shield-alert-DLYLdVJ0.js} +1 -1
- package/web-app/dist/assets/{trash-2-C9vZqTqw.js → trash-2-Cc-VTvzt.js} +1 -1
- package/web-app/dist/assets/{trending-down-BNLTrF5P.js → trending-down-CrDpO2a_.js} +1 -1
- package/web-app/dist/assets/{trending-up-DmFIdVOc.js → trending-up-CNVsmM3G.js} +1 -1
- package/web-app/dist/assets/upload-LuDuB7Wc.js +6 -0
- package/web-app/dist/assets/{usePolling-vUlY-o6P.js → usePolling-C8rvc-CG.js} +1 -1
- package/web-app/dist/assets/{user-Dh00W8De.js → user-BT79cI-o.js} +1 -1
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +120 -0
- package/web-app/dist/assets/GitHubIssuesPanel-DBbBTG9w.js +0 -17
- package/web-app/dist/assets/index-CVM4A1Fw.css +0 -1
package/autonomy/run.sh
CHANGED
|
@@ -5732,8 +5732,8 @@ run_doc_staleness_check() {
|
|
|
5732
5732
|
# Checks README, documentation freshness, and package API docs
|
|
5733
5733
|
# ============================================================================
|
|
5734
5734
|
|
|
5735
|
+
# shellcheck disable=SC2120
|
|
5735
5736
|
run_doc_quality_gate() {
|
|
5736
|
-
# shellcheck disable=SC2120
|
|
5737
5737
|
local project_dir="${1:-${TARGET_DIR:-.}}"
|
|
5738
5738
|
local score=100
|
|
5739
5739
|
local issues=()
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""MCP tools for Magic Modules.
|
|
2
|
+
|
|
3
|
+
Exposes loki magic functionality to AI coding assistants via the MCP
|
|
4
|
+
protocol. Tools delegate to the magic/ package where possible and fall
|
|
5
|
+
back to the `loki magic` CLI when direct imports are unavailable.
|
|
6
|
+
|
|
7
|
+
Registration pattern:
|
|
8
|
+
from mcp.magic_tools import register_magic_tools
|
|
9
|
+
register_magic_tools(mcp_server) # Called from mcp/server.py
|
|
10
|
+
|
|
11
|
+
All tools are resilient: they catch exceptions and return structured
|
|
12
|
+
error dicts rather than raising, so an MCP client always receives a
|
|
13
|
+
well-formed JSON response.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import subprocess
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Regex used to validate component names. Mirrors the rule stated in the
|
|
23
|
+
# tool docstrings so callers get a consistent, early error.
|
|
24
|
+
_NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")
|
|
25
|
+
|
|
26
|
+
# Default timeout (seconds) for CLI subprocess calls. Generation involves
|
|
27
|
+
# an LLM round-trip so it can legitimately take a while; we still cap it
|
|
28
|
+
# to avoid hanging the MCP server indefinitely.
|
|
29
|
+
_CLI_TIMEOUT = 600
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _error(message: str, **extra: Any) -> Dict[str, Any]:
|
|
33
|
+
"""Return a structured error dict."""
|
|
34
|
+
result: Dict[str, Any] = {"ok": False, "error": message}
|
|
35
|
+
result.update(extra)
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ok(**fields: Any) -> Dict[str, Any]:
|
|
40
|
+
"""Return a structured success dict."""
|
|
41
|
+
result: Dict[str, Any] = {"ok": True}
|
|
42
|
+
result.update(fields)
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _validate_name(name: str) -> Optional[str]:
|
|
47
|
+
"""Return an error message if name is invalid, else None."""
|
|
48
|
+
if not isinstance(name, str) or not name:
|
|
49
|
+
return "name must be a non-empty string"
|
|
50
|
+
if not _NAME_RE.match(name):
|
|
51
|
+
return (
|
|
52
|
+
"name must match ^[a-zA-Z][a-zA-Z0-9_-]*$ "
|
|
53
|
+
"(letters, digits, underscore, hyphen; must start with a letter)"
|
|
54
|
+
)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _run_loki(args: List[str], timeout: int = _CLI_TIMEOUT) -> Dict[str, Any]:
|
|
59
|
+
"""Invoke the `loki` CLI and return a structured result.
|
|
60
|
+
|
|
61
|
+
The CLI is expected to emit JSON on stdout for machine consumers. If
|
|
62
|
+
stdout is not valid JSON we surface both stdout and stderr in the
|
|
63
|
+
error response so callers can diagnose.
|
|
64
|
+
"""
|
|
65
|
+
cmd = ["loki", *args]
|
|
66
|
+
try:
|
|
67
|
+
proc = subprocess.run(
|
|
68
|
+
cmd,
|
|
69
|
+
capture_output=True,
|
|
70
|
+
text=True,
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
check=False,
|
|
73
|
+
)
|
|
74
|
+
except FileNotFoundError:
|
|
75
|
+
return _error("loki CLI not found on PATH", command=cmd)
|
|
76
|
+
except subprocess.TimeoutExpired:
|
|
77
|
+
return _error(
|
|
78
|
+
"loki CLI timed out", command=cmd, timeout_seconds=timeout
|
|
79
|
+
)
|
|
80
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
81
|
+
return _error(f"failed to invoke loki CLI: {exc}", command=cmd)
|
|
82
|
+
|
|
83
|
+
stdout = (proc.stdout or "").strip()
|
|
84
|
+
stderr = (proc.stderr or "").strip()
|
|
85
|
+
|
|
86
|
+
if proc.returncode != 0:
|
|
87
|
+
return _error(
|
|
88
|
+
f"loki CLI exited with code {proc.returncode}",
|
|
89
|
+
command=cmd,
|
|
90
|
+
stdout=stdout,
|
|
91
|
+
stderr=stderr,
|
|
92
|
+
returncode=proc.returncode,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Try to decode JSON first; fall back to raw stdout if not JSON.
|
|
96
|
+
if stdout:
|
|
97
|
+
try:
|
|
98
|
+
parsed = json.loads(stdout)
|
|
99
|
+
if isinstance(parsed, dict):
|
|
100
|
+
parsed.setdefault("ok", True)
|
|
101
|
+
return parsed
|
|
102
|
+
return _ok(result=parsed)
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
return _ok(stdout=stdout, stderr=stderr)
|
|
105
|
+
|
|
106
|
+
return _ok(stdout=stdout, stderr=stderr)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Tool implementations
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def loki_magic_generate(
|
|
115
|
+
name: str,
|
|
116
|
+
description: str = "",
|
|
117
|
+
target: str = "react",
|
|
118
|
+
placement: str = "",
|
|
119
|
+
tags: Optional[List[str]] = None,
|
|
120
|
+
) -> Dict[str, Any]:
|
|
121
|
+
"""Generate a new component from a description.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
name: Component name (must match ^[a-zA-Z][a-zA-Z0-9_-]*$).
|
|
125
|
+
description: Natural-language description of what the component does.
|
|
126
|
+
target: 'react' | 'webcomponent' | 'both'.
|
|
127
|
+
placement: Optional file path where the component should be placed.
|
|
128
|
+
tags: List of tags for registry search.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
A dict with keys:
|
|
132
|
+
ok (bool), name (str), spec_path (str),
|
|
133
|
+
react_path (str, if target includes react),
|
|
134
|
+
webcomponent_path (str, if target includes webcomponent),
|
|
135
|
+
version (str), debate_passed (bool)
|
|
136
|
+
On failure: {"ok": False, "error": "..."}.
|
|
137
|
+
"""
|
|
138
|
+
err = _validate_name(name)
|
|
139
|
+
if err:
|
|
140
|
+
return _error(err, name=name)
|
|
141
|
+
|
|
142
|
+
allowed_targets = {"react", "webcomponent", "both"}
|
|
143
|
+
if target not in allowed_targets:
|
|
144
|
+
return _error(
|
|
145
|
+
f"target must be one of {sorted(allowed_targets)}",
|
|
146
|
+
target=target,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if tags is not None and not isinstance(tags, list):
|
|
150
|
+
return _error("tags must be a list of strings", tags=tags)
|
|
151
|
+
|
|
152
|
+
args: List[str] = ["magic", "generate", name, "--target", target]
|
|
153
|
+
if description:
|
|
154
|
+
args.extend(["--description", description])
|
|
155
|
+
if placement:
|
|
156
|
+
args.extend(["--placement", placement])
|
|
157
|
+
if tags:
|
|
158
|
+
args.extend(["--tags", ",".join(str(t) for t in tags)])
|
|
159
|
+
|
|
160
|
+
return _run_loki(args)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def loki_magic_list(
|
|
164
|
+
query: str = "",
|
|
165
|
+
tags: Optional[List[str]] = None,
|
|
166
|
+
target: Optional[str] = None,
|
|
167
|
+
) -> Dict[str, Any]:
|
|
168
|
+
"""List / search registered components.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
query: Substring match against component names.
|
|
172
|
+
tags: Filter by tags (AND logic).
|
|
173
|
+
target: Filter by target framework.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
{"ok": True, "count": int, "components": [...]} on success,
|
|
177
|
+
or a structured error dict on failure.
|
|
178
|
+
"""
|
|
179
|
+
if tags is not None and not isinstance(tags, list):
|
|
180
|
+
return _error("tags must be a list of strings", tags=tags)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Prefer direct Python API when available.
|
|
184
|
+
from magic.core.registry import ComponentRegistry # type: ignore
|
|
185
|
+
except ImportError:
|
|
186
|
+
# Fall back to CLI if the magic package is not importable.
|
|
187
|
+
args: List[str] = ["magic", "list", "--json"]
|
|
188
|
+
if query:
|
|
189
|
+
args.extend(["--query", query])
|
|
190
|
+
if tags:
|
|
191
|
+
args.extend(["--tags", ",".join(str(t) for t in tags)])
|
|
192
|
+
if target:
|
|
193
|
+
args.extend(["--target", target])
|
|
194
|
+
return _run_loki(args)
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
return _error(f"failed to import magic registry: {exc}")
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
reg = ComponentRegistry(".")
|
|
200
|
+
results = reg.search(query=query, tags=tags, target=target)
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
return _error(f"registry search failed: {exc}")
|
|
203
|
+
|
|
204
|
+
components = list(results) if results is not None else []
|
|
205
|
+
return _ok(count=len(components), components=components)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def loki_magic_get(name: str) -> Dict[str, Any]:
|
|
209
|
+
"""Fetch details for a specific component.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
name: Component name (must match ^[a-zA-Z][a-zA-Z0-9_-]*$).
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
{"ok": True, "component": {...}} on success,
|
|
216
|
+
or a structured error dict on failure.
|
|
217
|
+
"""
|
|
218
|
+
err = _validate_name(name)
|
|
219
|
+
if err:
|
|
220
|
+
return _error(err, name=name)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
from magic.core.registry import ComponentRegistry # type: ignore
|
|
224
|
+
except ImportError:
|
|
225
|
+
return _run_loki(["magic", "get", name, "--json"])
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
return _error(f"failed to import magic registry: {exc}")
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
reg = ComponentRegistry(".")
|
|
231
|
+
# Prefer a .get() method if the registry exposes one; otherwise
|
|
232
|
+
# fall back to searching by exact name.
|
|
233
|
+
component: Any = None
|
|
234
|
+
if hasattr(reg, "get"):
|
|
235
|
+
component = reg.get(name)
|
|
236
|
+
elif hasattr(reg, "find"):
|
|
237
|
+
component = reg.find(name)
|
|
238
|
+
else:
|
|
239
|
+
results = reg.search(query=name)
|
|
240
|
+
for item in (results or []):
|
|
241
|
+
if isinstance(item, dict) and item.get("name") == name:
|
|
242
|
+
component = item
|
|
243
|
+
break
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
return _error(f"registry lookup failed: {exc}", name=name)
|
|
246
|
+
|
|
247
|
+
if component is None:
|
|
248
|
+
return _error(f"component not found: {name}", name=name)
|
|
249
|
+
return _ok(component=component)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def loki_magic_update(
|
|
253
|
+
name: str,
|
|
254
|
+
spec_update: str = "",
|
|
255
|
+
force: bool = False,
|
|
256
|
+
) -> Dict[str, Any]:
|
|
257
|
+
"""Update a component when its spec has changed.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
name: Component name.
|
|
261
|
+
spec_update: Optional updated description / spec text to apply
|
|
262
|
+
before regeneration.
|
|
263
|
+
force: If False (default), only regenerates when the spec hash
|
|
264
|
+
has diverged. If True, always regenerates.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
A dict describing the update result on success, or a structured
|
|
268
|
+
error dict on failure.
|
|
269
|
+
"""
|
|
270
|
+
err = _validate_name(name)
|
|
271
|
+
if err:
|
|
272
|
+
return _error(err, name=name)
|
|
273
|
+
|
|
274
|
+
args: List[str] = ["magic", "update", name]
|
|
275
|
+
if spec_update:
|
|
276
|
+
args.extend(["--spec-update", spec_update])
|
|
277
|
+
if force:
|
|
278
|
+
args.append("--force")
|
|
279
|
+
args.append("--json")
|
|
280
|
+
|
|
281
|
+
return _run_loki(args)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def loki_magic_debate(
|
|
285
|
+
name: str,
|
|
286
|
+
rounds: int = 3,
|
|
287
|
+
personas: Optional[List[str]] = None,
|
|
288
|
+
) -> Dict[str, Any]:
|
|
289
|
+
"""Run multi-persona debate on an existing component.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
name: Component name.
|
|
293
|
+
rounds: Number of debate rounds (default 3).
|
|
294
|
+
personas: Optional list of persona identifiers. When omitted,
|
|
295
|
+
the debate runner uses its default persona set.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Debate result including critiques and (if applicable) refined
|
|
299
|
+
code, or a structured error dict on failure.
|
|
300
|
+
"""
|
|
301
|
+
err = _validate_name(name)
|
|
302
|
+
if err:
|
|
303
|
+
return _error(err, name=name)
|
|
304
|
+
|
|
305
|
+
if not isinstance(rounds, int) or rounds < 1:
|
|
306
|
+
return _error("rounds must be a positive integer", rounds=rounds)
|
|
307
|
+
|
|
308
|
+
if personas is not None and not isinstance(personas, list):
|
|
309
|
+
return _error("personas must be a list of strings", personas=personas)
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
from magic.core.debate import DebateRunner # type: ignore
|
|
313
|
+
except ImportError:
|
|
314
|
+
args: List[str] = ["magic", "debate", name, "--rounds", str(rounds)]
|
|
315
|
+
if personas:
|
|
316
|
+
args.extend(["--personas", ",".join(str(p) for p in personas)])
|
|
317
|
+
args.append("--json")
|
|
318
|
+
return _run_loki(args)
|
|
319
|
+
except Exception as exc:
|
|
320
|
+
return _error(f"failed to import debate runner: {exc}")
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
runner_kwargs: Dict[str, Any] = {"rounds": rounds}
|
|
324
|
+
if personas:
|
|
325
|
+
runner_kwargs["personas"] = personas
|
|
326
|
+
# DebateRunner's exact constructor signature is defined by the
|
|
327
|
+
# magic package. We pass the workspace root positionally and
|
|
328
|
+
# forward debate params as kwargs, staying tolerant of minor
|
|
329
|
+
# signature differences.
|
|
330
|
+
try:
|
|
331
|
+
runner = DebateRunner(".", **runner_kwargs)
|
|
332
|
+
except TypeError:
|
|
333
|
+
runner = DebateRunner(".")
|
|
334
|
+
for attr, value in runner_kwargs.items():
|
|
335
|
+
try:
|
|
336
|
+
setattr(runner, attr, value)
|
|
337
|
+
except Exception:
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
if hasattr(runner, "run"):
|
|
341
|
+
result = runner.run(name)
|
|
342
|
+
elif hasattr(runner, "debate"):
|
|
343
|
+
result = runner.debate(name)
|
|
344
|
+
else:
|
|
345
|
+
return _error(
|
|
346
|
+
"DebateRunner exposes neither run() nor debate()",
|
|
347
|
+
name=name,
|
|
348
|
+
)
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
return _error(f"debate failed: {exc}", name=name)
|
|
351
|
+
|
|
352
|
+
if isinstance(result, dict):
|
|
353
|
+
result.setdefault("ok", True)
|
|
354
|
+
return result
|
|
355
|
+
return _ok(result=result)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def loki_magic_tokens_extract() -> Dict[str, Any]:
|
|
359
|
+
"""Extract design tokens from the current codebase.
|
|
360
|
+
|
|
361
|
+
Returns observed colors, spacing, typography, etc. Observation is
|
|
362
|
+
non-destructive: the registry is not modified (save=False).
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
from magic.core.design_tokens import DesignTokens # type: ignore
|
|
366
|
+
except ImportError:
|
|
367
|
+
return _run_loki(["magic", "tokens", "extract", "--json"])
|
|
368
|
+
except Exception as exc:
|
|
369
|
+
return _error(f"failed to import design tokens module: {exc}")
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
dt = DesignTokens(".")
|
|
373
|
+
observed = dt.extract_from_codebase(save=False)
|
|
374
|
+
except Exception as exc:
|
|
375
|
+
return _error(f"token extraction failed: {exc}")
|
|
376
|
+
|
|
377
|
+
if isinstance(observed, dict):
|
|
378
|
+
observed.setdefault("ok", True)
|
|
379
|
+
return observed
|
|
380
|
+
return _ok(tokens=observed)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def loki_magic_stats() -> Dict[str, Any]:
|
|
384
|
+
"""Registry stats: total components, per-target counts, debate pass rate.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
A dict of statistics on success, or a structured error dict on
|
|
388
|
+
failure.
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
from magic.core.registry import ComponentRegistry # type: ignore
|
|
392
|
+
except ImportError:
|
|
393
|
+
return _run_loki(["magic", "stats", "--json"])
|
|
394
|
+
except Exception as exc:
|
|
395
|
+
return _error(f"failed to import magic registry: {exc}")
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
stats = ComponentRegistry(".").stats()
|
|
399
|
+
except Exception as exc:
|
|
400
|
+
return _error(f"failed to compute stats: {exc}")
|
|
401
|
+
|
|
402
|
+
if isinstance(stats, dict):
|
|
403
|
+
stats.setdefault("ok", True)
|
|
404
|
+
return stats
|
|
405
|
+
return _ok(stats=stats)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# Public registration entry point
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# Tuple of (callable, public-tool-name). Tool names are stable and match
|
|
414
|
+
# the CLI verbs so MCP clients can predict them.
|
|
415
|
+
_TOOLS = (
|
|
416
|
+
(loki_magic_generate, "loki_magic_generate"),
|
|
417
|
+
(loki_magic_list, "loki_magic_list"),
|
|
418
|
+
(loki_magic_get, "loki_magic_get"),
|
|
419
|
+
(loki_magic_update, "loki_magic_update"),
|
|
420
|
+
(loki_magic_debate, "loki_magic_debate"),
|
|
421
|
+
(loki_magic_tokens_extract, "loki_magic_tokens_extract"),
|
|
422
|
+
(loki_magic_stats, "loki_magic_stats"),
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def register_magic_tools(mcp_server: Any) -> List[str]:
|
|
427
|
+
"""Wire the module's functions into a FastMCP server instance.
|
|
428
|
+
|
|
429
|
+
Usage from mcp/server.py:
|
|
430
|
+
from mcp.magic_tools import register_magic_tools
|
|
431
|
+
register_magic_tools(mcp)
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
mcp_server: A FastMCP-compatible server instance exposing a
|
|
435
|
+
`.tool()` decorator factory.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
The list of tool names that were successfully registered.
|
|
439
|
+
"""
|
|
440
|
+
if mcp_server is None:
|
|
441
|
+
raise ValueError("mcp_server must not be None")
|
|
442
|
+
|
|
443
|
+
if not hasattr(mcp_server, "tool"):
|
|
444
|
+
raise TypeError(
|
|
445
|
+
"mcp_server does not expose a .tool() method; "
|
|
446
|
+
"expected a FastMCP-compatible instance"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
registered: List[str] = []
|
|
450
|
+
for func, tool_name in _TOOLS:
|
|
451
|
+
try:
|
|
452
|
+
mcp_server.tool()(func)
|
|
453
|
+
registered.append(tool_name)
|
|
454
|
+
except Exception:
|
|
455
|
+
# Don't let a single bad registration break the rest. The
|
|
456
|
+
# integration pass can inspect the returned list to confirm
|
|
457
|
+
# which tools made it through.
|
|
458
|
+
continue
|
|
459
|
+
return registered
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
__all__ = [
|
|
463
|
+
"loki_magic_generate",
|
|
464
|
+
"loki_magic_list",
|
|
465
|
+
"loki_magic_get",
|
|
466
|
+
"loki_magic_update",
|
|
467
|
+
"loki_magic_debate",
|
|
468
|
+
"loki_magic_tokens_extract",
|
|
469
|
+
"loki_magic_stats",
|
|
470
|
+
"register_magic_tools",
|
|
471
|
+
]
|
package/mcp/server.py
CHANGED
|
@@ -1954,6 +1954,19 @@ async def loki_phase_report() -> str:
|
|
|
1954
1954
|
Use loki_state_get and loki_task_queue_list to gather data."""
|
|
1955
1955
|
|
|
1956
1956
|
|
|
1957
|
+
# ============================================================
|
|
1958
|
+
# MAGIC MODULES TOOLS (spec-driven component generation)
|
|
1959
|
+
# ============================================================
|
|
1960
|
+
|
|
1961
|
+
try:
|
|
1962
|
+
from mcp.magic_tools import register_magic_tools
|
|
1963
|
+
register_magic_tools(mcp)
|
|
1964
|
+
except Exception as _magic_err:
|
|
1965
|
+
# Magic Modules is optional; log and continue if unavailable
|
|
1966
|
+
import sys as _sys
|
|
1967
|
+
print(f"[warn] magic_tools registration skipped: {_magic_err}", file=_sys.stderr)
|
|
1968
|
+
|
|
1969
|
+
|
|
1957
1970
|
# ============================================================
|
|
1958
1971
|
# MAIN
|
|
1959
1972
|
# ============================================================
|