@yottameta/yotta-dev-mcp-plugin 0.1.1 → 0.2.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.
- package/.agents/plugins/marketplace.json +3 -3
- package/.claude-plugin/marketplace.json +3 -3
- package/package.json +1 -1
- package/plugin.json +2 -2
- package/skills/yotta-dev-mcp/SKILL.md +24 -9
- package/skills/yotta-dev-mcp/references/adapters.md +69 -0
- package/skills/yotta-dev-mcp/references/architecture-contract.md +256 -0
- package/skills/yotta-dev-mcp/references/tools.md +182 -3
- package/skills/yotta-dev-mcp/scripts/dev_adapters.py +609 -0
- package/skills/yotta-dev-mcp/scripts/dev_architecture.py +379 -0
- package/skills/yotta-dev-mcp/scripts/dev_common.py +155 -0
- package/skills/yotta-dev-mcp/scripts/dev_contract.py +830 -0
- package/skills/yotta-dev-mcp/scripts/dev_engine.py +328 -210
- package/skills/yotta-dev-mcp/scripts/dev_impact.py +556 -0
- package/skills/yotta-dev-mcp/scripts/dev_mcp_doctor.py +768 -0
- package/skills/yotta-dev-mcp/scripts/dev_model.py +447 -0
- package/skills/yotta-dev-mcp/scripts/dev_selftest.py +544 -0
- package/skills/yotta-dev-mcp/scripts/dev_verify.py +450 -0
- package/skills/yotta-dev-mcp/scripts/yotta_dev_mcp.py +246 -7
- package/skills/yotta-dev-mcp/server.json +3 -3
|
@@ -8,7 +8,6 @@ tool explicitly documents a write.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
|
-
import ast
|
|
12
11
|
import json
|
|
13
12
|
import math
|
|
14
13
|
import os
|
|
@@ -18,133 +17,37 @@ import subprocess
|
|
|
18
17
|
import sys
|
|
19
18
|
from pathlib import Path
|
|
20
19
|
|
|
20
|
+
import dev_contract
|
|
21
|
+
from dev_adapters import run_adapter as _run_adapter_impl
|
|
22
|
+
from dev_architecture import architecture_review as _architecture_review_impl
|
|
23
|
+
from dev_common import (
|
|
24
|
+
ANSI_RE, ERROR_RE, MAX_FILE_BYTES, VERIFY_LEVELS, _iter_files, _json_safe,
|
|
25
|
+
_language, _read_text, _rel,
|
|
26
|
+
_frontmatter_name, _frontmatter_version, source_exts,
|
|
27
|
+
)
|
|
28
|
+
from dev_impact import impact_analysis as _impact_analysis_impl
|
|
29
|
+
from dev_mcp_doctor import (
|
|
30
|
+
default_config_paths as _default_config_paths_impl,
|
|
31
|
+
default_skill_dirs as _default_skill_dirs_impl,
|
|
32
|
+
mcp_doctor as _mcp_doctor_impl,
|
|
33
|
+
)
|
|
34
|
+
from dev_verify import verify_change as _verify_change_impl
|
|
35
|
+
from dev_selftest import self_test
|
|
36
|
+
from dev_model import (
|
|
37
|
+
_classify_python_import,
|
|
38
|
+
_repo_map_js,
|
|
39
|
+
_repo_map_python,
|
|
40
|
+
system_model as _system_model_impl,
|
|
41
|
+
)
|
|
21
42
|
from dev_rules import REVIEW_RULES
|
|
22
43
|
|
|
23
|
-
VERSION = "0.
|
|
44
|
+
VERSION = "0.2.1"
|
|
24
45
|
|
|
25
|
-
IGNORE_DIRS = {
|
|
26
|
-
".git", ".hg", ".svn", "node_modules", "__pycache__", ".venv", "venv",
|
|
27
|
-
"dist", "build", ".next", ".nuxt", ".cache", ".tmp", ".tmp2",
|
|
28
|
-
}
|
|
29
|
-
SOURCE_EXTS = {
|
|
30
|
-
".py", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".sh", ".ps1",
|
|
31
|
-
".go", ".rs", ".java", ".kt", ".kts", ".rb", ".php",
|
|
32
|
-
}
|
|
33
|
-
TEXT_EXTS = SOURCE_EXTS | {".json", ".md", ".txt", ".yml", ".yaml", ".toml", ".ini", ".cfg"}
|
|
34
|
-
MAX_FILE_BYTES = 2 * 1024 * 1024
|
|
35
46
|
|
|
36
|
-
ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
|
37
|
-
ERROR_RE = re.compile(
|
|
38
|
-
r"(?i)(traceback|exception|\berror\b|\bfailed\b|\bfail\b|fatal|panic|"
|
|
39
|
-
r"assertion|npm err!|\berr\b|\bwarn(?:ing)?\b)"
|
|
40
|
-
)
|
|
41
47
|
|
|
42
48
|
|
|
43
|
-
def _json_safe(value):
|
|
44
|
-
if isinstance(value, Path):
|
|
45
|
-
return str(value)
|
|
46
|
-
if isinstance(value, dict):
|
|
47
|
-
return {str(k): _json_safe(v) for k, v in value.items()}
|
|
48
|
-
if isinstance(value, list):
|
|
49
|
-
return [_json_safe(v) for v in value]
|
|
50
|
-
return value
|
|
51
49
|
|
|
52
50
|
|
|
53
|
-
def _read_text(path):
|
|
54
|
-
path = Path(path)
|
|
55
|
-
if path.stat().st_size > MAX_FILE_BYTES:
|
|
56
|
-
raise ValueError("文件超过大小上限: %s" % path)
|
|
57
|
-
data = path.read_bytes()
|
|
58
|
-
if b"\x00" in data[:4096]:
|
|
59
|
-
raise ValueError("二进制文件不参与文本扫描: %s" % path)
|
|
60
|
-
return data.decode("utf-8", errors="replace")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _iter_files(root, extensions=None, max_files=5000, all_files=False):
|
|
64
|
-
root = Path(root)
|
|
65
|
-
extensions = set(extensions or TEXT_EXTS)
|
|
66
|
-
count = 0
|
|
67
|
-
for current, dirs, files in os.walk(str(root)):
|
|
68
|
-
dirs[:] = sorted(d for d in dirs if d not in IGNORE_DIRS)
|
|
69
|
-
for name in sorted(files):
|
|
70
|
-
path = Path(current) / name
|
|
71
|
-
if path.is_symlink():
|
|
72
|
-
continue
|
|
73
|
-
if not all_files and extensions and path.suffix.lower() not in extensions:
|
|
74
|
-
continue
|
|
75
|
-
if path.stat().st_size > MAX_FILE_BYTES:
|
|
76
|
-
continue
|
|
77
|
-
yield path
|
|
78
|
-
count += 1
|
|
79
|
-
if count >= max_files:
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _rel(root, path):
|
|
84
|
-
try:
|
|
85
|
-
return str(Path(path).resolve().relative_to(Path(root).resolve())).replace("\\", "/")
|
|
86
|
-
except ValueError:
|
|
87
|
-
return str(Path(path)).replace("\\", "/")
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _language(path):
|
|
91
|
-
ext = Path(path).suffix.lower()
|
|
92
|
-
if ext == ".py":
|
|
93
|
-
return "python"
|
|
94
|
-
if ext in (".js", ".jsx", ".mjs", ".cjs"):
|
|
95
|
-
return "javascript"
|
|
96
|
-
if ext in (".ts", ".tsx"):
|
|
97
|
-
return "typescript"
|
|
98
|
-
if ext in (".sh", ".ps1"):
|
|
99
|
-
return "shell"
|
|
100
|
-
return ext.lstrip(".") or "text"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _resolve_python_import(source_rel, node):
|
|
104
|
-
source = Path(source_rel)
|
|
105
|
-
if isinstance(node, ast.Import):
|
|
106
|
-
return [alias.name for alias in node.names]
|
|
107
|
-
if not isinstance(node, ast.ImportFrom):
|
|
108
|
-
return []
|
|
109
|
-
if node.level:
|
|
110
|
-
parts = list(source.with_suffix("").parts[:-1])
|
|
111
|
-
if node.level > 1:
|
|
112
|
-
parts = parts[:-(node.level - 1)] if len(parts) >= node.level - 1 else []
|
|
113
|
-
prefix = "/".join(parts)
|
|
114
|
-
module = node.module or ""
|
|
115
|
-
target = (prefix + "/" + module.replace(".", "/")).strip("/")
|
|
116
|
-
return [target + ".py" if target else source_rel]
|
|
117
|
-
if node.module:
|
|
118
|
-
return [node.module]
|
|
119
|
-
return []
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def _repo_map_python(path, rel):
|
|
123
|
-
text = _read_text(path)
|
|
124
|
-
imports = []
|
|
125
|
-
try:
|
|
126
|
-
tree = ast.parse(text)
|
|
127
|
-
except SyntaxError:
|
|
128
|
-
return imports
|
|
129
|
-
for node in ast.walk(tree):
|
|
130
|
-
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
131
|
-
for target in _resolve_python_import(rel, node):
|
|
132
|
-
imports.append({"source": rel, "target": target, "line": getattr(node, "lineno", 1)})
|
|
133
|
-
return imports
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _repo_map_js(path, rel):
|
|
137
|
-
text = _read_text(path)
|
|
138
|
-
imports = []
|
|
139
|
-
patterns = [
|
|
140
|
-
re.compile(r"""(?:from\s+|import\s*\()\s*['"]([^'"]+)['"]"""),
|
|
141
|
-
re.compile(r"""require\s*\(\s*['"]([^'"]+)['"]\s*\)"""),
|
|
142
|
-
]
|
|
143
|
-
for lineno, line in enumerate(text.splitlines(), 1):
|
|
144
|
-
for pattern in patterns:
|
|
145
|
-
for match in pattern.finditer(line):
|
|
146
|
-
imports.append({"source": rel, "target": match.group(1), "line": lineno})
|
|
147
|
-
return imports
|
|
148
51
|
|
|
149
52
|
|
|
150
53
|
def repo_map(path, max_files=2000):
|
|
@@ -161,6 +64,7 @@ def repo_map(path, max_files=2000):
|
|
|
161
64
|
if len(files) > max_files:
|
|
162
65
|
truncated = True
|
|
163
66
|
files = files[:max_files]
|
|
67
|
+
module_set = {_rel(root, file_path) for file_path in files}
|
|
164
68
|
for file_path in files:
|
|
165
69
|
rel = _rel(root, file_path)
|
|
166
70
|
try:
|
|
@@ -170,7 +74,11 @@ def repo_map(path, max_files=2000):
|
|
|
170
74
|
language = _language(file_path)
|
|
171
75
|
modules.append({"path": rel, "language": language, "lines": len(text.splitlines())})
|
|
172
76
|
if file_path.suffix.lower() == ".py":
|
|
173
|
-
|
|
77
|
+
for raw in _repo_map_python(file_path, rel):
|
|
78
|
+
_, target = _classify_python_import(
|
|
79
|
+
raw["target"], rel, module_set, relative=raw.get("relative", False)
|
|
80
|
+
)
|
|
81
|
+
imports.append({"source": rel, "target": target, "line": raw["line"]})
|
|
174
82
|
elif file_path.suffix.lower() in (".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"):
|
|
175
83
|
imports.extend(_repo_map_js(file_path, rel))
|
|
176
84
|
if (
|
|
@@ -191,10 +99,6 @@ def repo_map(path, max_files=2000):
|
|
|
191
99
|
}
|
|
192
100
|
|
|
193
101
|
|
|
194
|
-
def source_exts():
|
|
195
|
-
return set(SOURCE_EXTS)
|
|
196
|
-
|
|
197
|
-
|
|
198
102
|
def _classify_match(line, query):
|
|
199
103
|
escaped = re.escape(query)
|
|
200
104
|
if re.search(r"^\s*(?:async\s+)?def\s+%s\b" % escaped, line):
|
|
@@ -384,92 +288,20 @@ def review_diff(diff_text=None, path=None, base=None, max_findings=200):
|
|
|
384
288
|
return {"files": files, "findings": findings[:max_findings], "truncated": truncated}
|
|
385
289
|
|
|
386
290
|
|
|
387
|
-
def _frontmatter_version(text):
|
|
388
|
-
match = re.search(r"(?m)^version:\s*[\"']?([^\"'\r\n]+)", text)
|
|
389
|
-
return match.group(1).strip() if match else None
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
def _frontmatter_name(text):
|
|
393
|
-
match = re.search(r"(?m)^name:\s*[\"']?([^\"'\r\n]+)", text)
|
|
394
|
-
return match.group(1).strip() if match else None
|
|
395
|
-
|
|
396
|
-
|
|
397
291
|
def _default_skill_dirs():
|
|
398
|
-
|
|
399
|
-
candidates = [
|
|
400
|
-
home / ".codex" / "skills",
|
|
401
|
-
home / ".claude" / "skills",
|
|
402
|
-
home / ".cursor" / "skills",
|
|
403
|
-
home / ".config" / "opencode" / "skills",
|
|
404
|
-
]
|
|
405
|
-
codex_home = os.environ.get("CODEX_HOME")
|
|
406
|
-
if codex_home:
|
|
407
|
-
candidates.insert(0, Path(codex_home) / "skills")
|
|
408
|
-
claude_home = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
409
|
-
if claude_home:
|
|
410
|
-
candidates.insert(0, Path(claude_home) / "skills")
|
|
411
|
-
xdg_home = os.environ.get("XDG_CONFIG_HOME")
|
|
412
|
-
if xdg_home:
|
|
413
|
-
candidates.insert(0, Path(xdg_home) / "opencode" / "skills")
|
|
414
|
-
return candidates
|
|
292
|
+
return _default_skill_dirs_impl()
|
|
415
293
|
|
|
416
294
|
|
|
417
295
|
def _default_config_paths():
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
def mcp_doctor(skills_dirs=None, config_paths=None):
|
|
429
|
-
skills = []
|
|
430
|
-
issues = []
|
|
431
|
-
for directory in (skills_dirs or _default_skill_dirs()):
|
|
432
|
-
root = Path(directory)
|
|
433
|
-
if not root.is_dir():
|
|
434
|
-
continue
|
|
435
|
-
for child in sorted(root.iterdir(), key=lambda item: item.name):
|
|
436
|
-
skill_file = child / "SKILL.md"
|
|
437
|
-
if not child.is_dir() or not skill_file.is_file():
|
|
438
|
-
continue
|
|
439
|
-
try:
|
|
440
|
-
text = _read_text(skill_file)
|
|
441
|
-
except (OSError, ValueError) as exc:
|
|
442
|
-
issues.append("%s: %s" % (skill_file, exc))
|
|
443
|
-
continue
|
|
444
|
-
skills.append({
|
|
445
|
-
"name": _frontmatter_name(text) or child.name,
|
|
446
|
-
"version": _frontmatter_version(text),
|
|
447
|
-
"path": str(skill_file),
|
|
448
|
-
})
|
|
449
|
-
mcp_configs = []
|
|
450
|
-
for config_path in (config_paths or _default_config_paths()):
|
|
451
|
-
path = Path(config_path)
|
|
452
|
-
if not path.is_file():
|
|
453
|
-
continue
|
|
454
|
-
try:
|
|
455
|
-
payload = json.loads(_read_text(path))
|
|
456
|
-
except Exception as exc: # noqa: BLE001
|
|
457
|
-
issues.append("%s: %s" % (path, exc))
|
|
458
|
-
continue
|
|
459
|
-
servers = payload.get("mcpServers") if isinstance(payload, dict) else None
|
|
460
|
-
mcp_configs.append({
|
|
461
|
-
"path": str(path),
|
|
462
|
-
"servers": sorted(servers.keys()) if isinstance(servers, dict) else [],
|
|
463
|
-
})
|
|
464
|
-
skills.sort(key=lambda item: (item["name"], item["path"]))
|
|
465
|
-
mcp_configs.sort(key=lambda item: item["path"])
|
|
466
|
-
return {
|
|
467
|
-
"skills": skills,
|
|
468
|
-
"mcp_configs": mcp_configs,
|
|
469
|
-
"issues": issues,
|
|
470
|
-
"checked_skills": len(skills),
|
|
471
|
-
"checked_configs": len(mcp_configs),
|
|
472
|
-
}
|
|
296
|
+
return _default_config_paths_impl()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def mcp_doctor(skills_dirs=None, config_paths=None, include_defaults=None):
|
|
300
|
+
return _mcp_doctor_impl(
|
|
301
|
+
skills_dirs=skills_dirs,
|
|
302
|
+
config_paths=config_paths,
|
|
303
|
+
include_defaults=include_defaults,
|
|
304
|
+
)
|
|
473
305
|
|
|
474
306
|
|
|
475
307
|
SECRET_KEY_RE = re.compile(
|
|
@@ -478,6 +310,35 @@ SECRET_KEY_RE = re.compile(
|
|
|
478
310
|
AWS_KEY_RE = re.compile(r"\bAKIA[0-9A-Z]{16}\b")
|
|
479
311
|
PRIVATE_KEY_RE = re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")
|
|
480
312
|
HIGH_ENTROPY_RE = re.compile(r"[A-Za-z0-9_+/=\-]{32,}")
|
|
313
|
+
HASH_CONTEXT_RE = re.compile(r"(?i)(sha1|sha256|sha512|hash|checksum|digest|integrity)")
|
|
314
|
+
HEX_TOKEN_RE = re.compile(r"^[0-9a-fA-F]+$")
|
|
315
|
+
PATH_HINT_RE = re.compile(r"(?i)(?:[a-z]:[\\/]|\\\\|https?://|file://)")
|
|
316
|
+
URL_HINT_RE = re.compile(r"(?i)(?:file|https?)://")
|
|
317
|
+
PERCENT_ESCAPE_RE = re.compile(r"%[0-9A-Fa-f]{2}")
|
|
318
|
+
HASH_PREFIX_RE = re.compile(r"(?i)^(?:sha1|sha256|sha512|md5)[=:]([0-9a-fA-F]{32,128})$")
|
|
319
|
+
FILE_SUFFIX_RE = re.compile(
|
|
320
|
+
r"(?i)\.(exe|dll|sys|py|js|ts|tsx|json|md|txt|log|whl|tar|gz|zip|png|jpg|jpeg|svg)"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _high_entropy_noise(line, token, start, end):
|
|
325
|
+
"""Return True for common non-secret high-entropy noise."""
|
|
326
|
+
if HASH_PREFIX_RE.match(token):
|
|
327
|
+
return True
|
|
328
|
+
if PERCENT_ESCAPE_RE.search(token) and URL_HINT_RE.search(line):
|
|
329
|
+
return True
|
|
330
|
+
if start > 0 and line[start - 1] == "%":
|
|
331
|
+
return True
|
|
332
|
+
if HEX_TOKEN_RE.match(token) and len(token) in (32, 40, 64, 128) and HASH_CONTEXT_RE.search(line):
|
|
333
|
+
return True
|
|
334
|
+
window = line[max(0, start - 16):min(len(line), end + 16)]
|
|
335
|
+
if PATH_HINT_RE.search(window):
|
|
336
|
+
return True
|
|
337
|
+
if URL_HINT_RE.search(line) and PERCENT_ESCAPE_RE.search(window):
|
|
338
|
+
return True
|
|
339
|
+
if FILE_SUFFIX_RE.match(line[end:end + 8]):
|
|
340
|
+
return True
|
|
341
|
+
return False
|
|
481
342
|
|
|
482
343
|
|
|
483
344
|
def _entropy(value):
|
|
@@ -506,7 +367,9 @@ def scan_secrets(path=None, text=None, max_findings=200, include_git_history=Fal
|
|
|
506
367
|
root = Path(path)
|
|
507
368
|
if not root.exists():
|
|
508
369
|
raise ValueError("路径不存在: %s" % path)
|
|
509
|
-
files = [root] if root.is_file() else list(
|
|
370
|
+
files = [root] if root.is_file() else list(
|
|
371
|
+
_iter_files(root, all_files=True, ignore_temp=False)
|
|
372
|
+
)
|
|
510
373
|
base = root.parent if root.is_file() else root
|
|
511
374
|
for file_path in files:
|
|
512
375
|
try:
|
|
@@ -542,7 +405,11 @@ def scan_secrets(path=None, text=None, max_findings=200, include_git_history=Fal
|
|
|
542
405
|
})
|
|
543
406
|
for match in HIGH_ENTROPY_RE.finditer(line):
|
|
544
407
|
token = match.group(0)
|
|
545
|
-
if
|
|
408
|
+
if (
|
|
409
|
+
_entropy(token) >= 4.0
|
|
410
|
+
and not PRIVATE_KEY_RE.search(line)
|
|
411
|
+
and not _high_entropy_noise(line, token, match.start(), match.end())
|
|
412
|
+
):
|
|
546
413
|
findings.append({
|
|
547
414
|
"path": rel, "line": line_no, "rule": "high-entropy-token",
|
|
548
415
|
"severity": "medium", "evidence": _redact(token),
|
|
@@ -956,9 +823,165 @@ def workflow_state(root, action="read", date=None, text=None, file=None, apply=F
|
|
|
956
823
|
}
|
|
957
824
|
|
|
958
825
|
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def system_model(path, max_files=2000, contract_file=None):
|
|
929
|
+
"""Build the deterministic system model through the stable engine facade."""
|
|
930
|
+
return _system_model_impl(
|
|
931
|
+
path, max_files=max_files, contract_file=contract_file,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def architecture_review(path, max_files=2000, contract_file=None):
|
|
936
|
+
"""Review architecture through the stable engine facade."""
|
|
937
|
+
return _architecture_review_impl(
|
|
938
|
+
path, max_files=max_files, contract_file=contract_file,
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def impact_analysis(path, changed_files=None, diff=None, symbols=None, depth=3,
|
|
943
|
+
max_files=2000, contract_file=None):
|
|
944
|
+
"""Build the change impact cone through the stable engine facade."""
|
|
945
|
+
return _impact_analysis_impl(
|
|
946
|
+
path, changed_files=changed_files, diff=diff, symbols=symbols,
|
|
947
|
+
depth=depth, max_files=max_files, contract_file=contract_file,
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def verify_change(path, changed_files=None, diff=None, symbols=None, depth=3,
|
|
952
|
+
levels=None, allow_execute=False, timeout=120,
|
|
953
|
+
max_files=2000, contract_file=None, policy_file=None):
|
|
954
|
+
"""Run the verification ladder through the stable engine facade."""
|
|
955
|
+
return _verify_change_impl(
|
|
956
|
+
path, changed_files=changed_files, diff=diff, symbols=symbols,
|
|
957
|
+
depth=depth, levels=levels, allow_execute=allow_execute, timeout=timeout,
|
|
958
|
+
max_files=max_files, contract_file=contract_file, policy_file=policy_file,
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def run_adapter(path, action="list", adapter=None, allow_execute=False, timeout=120,
|
|
963
|
+
max_chars=120000, token_budget=None, target="."):
|
|
964
|
+
"""Probe or explicitly run one optional external adapter.
|
|
965
|
+
|
|
966
|
+
The implementation lives in dev_adapters.py; this facade keeps the public
|
|
967
|
+
dispatch signature and fail-closed default visible to self_test.
|
|
968
|
+
"""
|
|
969
|
+
return _run_adapter_impl(
|
|
970
|
+
path, action=action, adapter=adapter, allow_execute=allow_execute,
|
|
971
|
+
timeout=timeout, max_chars=max_chars, token_budget=token_budget,
|
|
972
|
+
target=target,
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
|
|
959
976
|
def dispatch(name, arguments):
|
|
960
977
|
handlers = {
|
|
961
978
|
"repo_map": repo_map,
|
|
979
|
+
"system_model": system_model,
|
|
980
|
+
"architecture_review": architecture_review,
|
|
981
|
+
"impact_analysis": impact_analysis,
|
|
982
|
+
"verify_change": verify_change,
|
|
983
|
+
"self_test": self_test,
|
|
984
|
+
"run_adapter": run_adapter,
|
|
962
985
|
"find_code": find_code,
|
|
963
986
|
"compress_output": compress_output,
|
|
964
987
|
"review_code": review_code,
|
|
@@ -982,6 +1005,62 @@ def main():
|
|
|
982
1005
|
sub = parser.add_subparsers(dest="command")
|
|
983
1006
|
repo = sub.add_parser("repo-map")
|
|
984
1007
|
repo.add_argument("path")
|
|
1008
|
+
model = sub.add_parser("system-model")
|
|
1009
|
+
model.add_argument("path")
|
|
1010
|
+
model.add_argument("--contract", help="contract path relative to the repository root")
|
|
1011
|
+
review = sub.add_parser("architecture-review")
|
|
1012
|
+
review.add_argument("path")
|
|
1013
|
+
review.add_argument("--contract", help="contract path relative to the repository root")
|
|
1014
|
+
impact = sub.add_parser("impact-analysis")
|
|
1015
|
+
impact.add_argument("path")
|
|
1016
|
+
impact.add_argument("--changed", action="append",
|
|
1017
|
+
help="repository-relative changed file (repeatable)")
|
|
1018
|
+
impact.add_argument("--diff-file", help="read a unified diff from this file")
|
|
1019
|
+
impact.add_argument("--symbol", action="append",
|
|
1020
|
+
help="target symbol whose definition site is the change (repeatable)")
|
|
1021
|
+
impact.add_argument("--depth", type=int, default=3,
|
|
1022
|
+
help="reverse-dependency depth, 1-10 (default 3)")
|
|
1023
|
+
impact.add_argument("--contract", help="contract path relative to the repository root")
|
|
1024
|
+
verify = sub.add_parser("verify-change")
|
|
1025
|
+
verify.add_argument("path")
|
|
1026
|
+
verify.add_argument("--changed", action="append",
|
|
1027
|
+
help="repository-relative changed file (repeatable)")
|
|
1028
|
+
verify.add_argument("--diff-file", help="read a unified diff from this file")
|
|
1029
|
+
verify.add_argument("--symbol", action="append",
|
|
1030
|
+
help="target symbol whose definition site is the change (repeatable)")
|
|
1031
|
+
verify.add_argument("--level", action="append", choices=VERIFY_LEVELS,
|
|
1032
|
+
help="additional verification level (repeatable)")
|
|
1033
|
+
verify.add_argument("--depth", type=int, default=3,
|
|
1034
|
+
help="reverse-dependency depth, 1-10 (default 3)")
|
|
1035
|
+
verify.add_argument("--contract", help="contract path relative to the repository root")
|
|
1036
|
+
verify.add_argument("--policy-file", help="verification policy path relative to the root")
|
|
1037
|
+
verify.add_argument("--allow-execute", action="store_true",
|
|
1038
|
+
help="run whitelisted L2-L4 policy checks")
|
|
1039
|
+
verify.add_argument("--timeout", type=int, default=120,
|
|
1040
|
+
help="upper bound for each check in seconds")
|
|
1041
|
+
self_test_parser = sub.add_parser("self-test")
|
|
1042
|
+
self_test_parser.add_argument("path")
|
|
1043
|
+
self_test_parser.add_argument("--mode", choices=("auto", "source", "installed"),
|
|
1044
|
+
default="auto")
|
|
1045
|
+
self_test_parser.add_argument("--allow-execute", action="store_true",
|
|
1046
|
+
help="run the target test suite")
|
|
1047
|
+
self_test_parser.add_argument("--timeout", type=int, default=120,
|
|
1048
|
+
help="test timeout in seconds")
|
|
1049
|
+
adapter = sub.add_parser("adapter")
|
|
1050
|
+
adapter.add_argument("path")
|
|
1051
|
+
adapter.add_argument("--action", choices=("list", "run"), default="list")
|
|
1052
|
+
adapter.add_argument("--adapter",
|
|
1053
|
+
choices=("import-linter", "dependency-cruiser", "repomix"))
|
|
1054
|
+
adapter.add_argument("--allow-execute", action="store_true",
|
|
1055
|
+
help="run the selected adapter; required for action=run")
|
|
1056
|
+
adapter.add_argument("--timeout", type=int, default=120,
|
|
1057
|
+
help="adapter timeout in seconds")
|
|
1058
|
+
adapter.add_argument("--max-chars", type=int, default=120000,
|
|
1059
|
+
help="Repomix output cap")
|
|
1060
|
+
adapter.add_argument("--token-budget", type=int,
|
|
1061
|
+
help="optional Repomix token budget")
|
|
1062
|
+
adapter.add_argument("--target", default=".",
|
|
1063
|
+
help="repository-relative adapter target, default .")
|
|
985
1064
|
find = sub.add_parser("find-code")
|
|
986
1065
|
find.add_argument("path")
|
|
987
1066
|
find.add_argument("query")
|
|
@@ -992,9 +1071,44 @@ def main():
|
|
|
992
1071
|
doctor = sub.add_parser("mcp-doctor")
|
|
993
1072
|
doctor.add_argument("--skills-dir", action="append")
|
|
994
1073
|
doctor.add_argument("--config", action="append")
|
|
1074
|
+
doctor.add_argument("--include-defaults", action="store_true",
|
|
1075
|
+
help="scan built-in host registry in addition to --config paths")
|
|
995
1076
|
args = parser.parse_args()
|
|
996
1077
|
if args.command == "repo-map":
|
|
997
1078
|
result = repo_map(args.path)
|
|
1079
|
+
elif args.command == "system-model":
|
|
1080
|
+
result = system_model(args.path, contract_file=args.contract)
|
|
1081
|
+
elif args.command == "architecture-review":
|
|
1082
|
+
result = architecture_review(args.path, contract_file=args.contract)
|
|
1083
|
+
elif args.command == "impact-analysis":
|
|
1084
|
+
diff_text = None
|
|
1085
|
+
if args.diff_file:
|
|
1086
|
+
diff_text = Path(args.diff_file).read_text(encoding="utf-8")
|
|
1087
|
+
result = impact_analysis(args.path, changed_files=args.changed, diff=diff_text,
|
|
1088
|
+
symbols=args.symbol, depth=args.depth,
|
|
1089
|
+
contract_file=args.contract)
|
|
1090
|
+
elif args.command == "verify-change":
|
|
1091
|
+
diff_text = None
|
|
1092
|
+
if args.diff_file:
|
|
1093
|
+
diff_text = Path(args.diff_file).read_text(encoding="utf-8")
|
|
1094
|
+
result = verify_change(
|
|
1095
|
+
args.path, changed_files=args.changed, diff=diff_text,
|
|
1096
|
+
symbols=args.symbol, depth=args.depth, levels=args.level,
|
|
1097
|
+
allow_execute=args.allow_execute, timeout=args.timeout,
|
|
1098
|
+
contract_file=args.contract, policy_file=args.policy_file,
|
|
1099
|
+
)
|
|
1100
|
+
elif args.command == "self-test":
|
|
1101
|
+
result = self_test(
|
|
1102
|
+
args.path, mode=args.mode, allow_execute=args.allow_execute,
|
|
1103
|
+
timeout=args.timeout,
|
|
1104
|
+
)
|
|
1105
|
+
elif args.command == "adapter":
|
|
1106
|
+
result = run_adapter(
|
|
1107
|
+
args.path, action=args.action, adapter=args.adapter,
|
|
1108
|
+
allow_execute=args.allow_execute, timeout=args.timeout,
|
|
1109
|
+
max_chars=args.max_chars, token_budget=args.token_budget,
|
|
1110
|
+
target=args.target,
|
|
1111
|
+
)
|
|
998
1112
|
elif args.command == "find-code":
|
|
999
1113
|
result = find_code(args.path, args.query)
|
|
1000
1114
|
elif args.command == "compress-output":
|
|
@@ -1002,7 +1116,11 @@ def main():
|
|
|
1002
1116
|
elif args.command == "review-code":
|
|
1003
1117
|
result = review_code(args.path)
|
|
1004
1118
|
elif args.command == "mcp-doctor":
|
|
1005
|
-
result = mcp_doctor(
|
|
1119
|
+
result = mcp_doctor(
|
|
1120
|
+
skills_dirs=args.skills_dir,
|
|
1121
|
+
config_paths=args.config,
|
|
1122
|
+
include_defaults=args.include_defaults,
|
|
1123
|
+
)
|
|
1006
1124
|
else:
|
|
1007
1125
|
parser.print_help()
|
|
1008
1126
|
return 2
|