@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.
@@ -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.1.1"
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
- imports.extend(_repo_map_python(file_path, rel))
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
- home = Path.home()
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
- home = Path.home()
419
- return [
420
- home / ".codex" / "config.json",
421
- home / ".codex" / "mcp.json",
422
- home / ".config" / "opencode" / "opencode.json",
423
- home / ".claude" / "settings.json",
424
- home / ".cursor" / "mcp.json",
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(_iter_files(root, all_files=True))
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 _entropy(token) >= 4.0 and not PRIVATE_KEY_RE.search(line):
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(skills_dirs=args.skills_dir, config_paths=args.config)
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