livepilot 1.10.7 → 1.10.8

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 (122) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/README.md +11 -9
  3. package/bin/livepilot.js +146 -28
  4. package/installer/install.js +117 -11
  5. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  6. package/m4l_device/livepilot_bridge.js +1 -1
  7. package/mcp_server/__init__.py +1 -1
  8. package/mcp_server/atlas/__init__.py +39 -7
  9. package/mcp_server/atlas/tools.py +56 -15
  10. package/mcp_server/composer/layer_planner.py +27 -0
  11. package/mcp_server/composer/prompt_parser.py +15 -6
  12. package/mcp_server/connection.py +11 -3
  13. package/mcp_server/corpus/__init__.py +14 -4
  14. package/mcp_server/m4l_bridge.py +48 -7
  15. package/mcp_server/runtime/execution_router.py +16 -2
  16. package/mcp_server/runtime/remote_commands.py +6 -0
  17. package/mcp_server/sample_engine/models.py +22 -3
  18. package/mcp_server/semantic_moves/__init__.py +1 -0
  19. package/mcp_server/semantic_moves/compiler.py +9 -1
  20. package/mcp_server/semantic_moves/device_creation_compilers.py +47 -0
  21. package/mcp_server/semantic_moves/mix_compilers.py +170 -0
  22. package/mcp_server/semantic_moves/mix_moves.py +1 -1
  23. package/mcp_server/semantic_moves/models.py +5 -0
  24. package/mcp_server/semantic_moves/tools.py +15 -4
  25. package/mcp_server/server.py +7 -3
  26. package/mcp_server/services/singletons.py +68 -0
  27. package/mcp_server/splice_client/client.py +29 -8
  28. package/mcp_server/tools/analyzer.py +7 -6
  29. package/mcp_server/tools/clips.py +1 -1
  30. package/mcp_server/tools/midi_io.py +10 -0
  31. package/mcp_server/tools/tracks.py +1 -1
  32. package/mcp_server/tools/transport.py +1 -1
  33. package/mcp_server/translation_engine/tools.py +8 -4
  34. package/package.json +25 -3
  35. package/remote_script/LivePilot/__init__.py +29 -9
  36. package/remote_script/LivePilot/arrangement.py +12 -2
  37. package/remote_script/LivePilot/browser.py +16 -6
  38. package/remote_script/LivePilot/devices.py +10 -5
  39. package/remote_script/LivePilot/notes.py +13 -2
  40. package/remote_script/LivePilot/server.py +51 -13
  41. package/remote_script/LivePilot/version_detect.py +7 -4
  42. package/server.json +20 -0
  43. package/.claude-plugin/marketplace.json +0 -21
  44. package/.mcp.json.disabled +0 -9
  45. package/.mcpbignore +0 -60
  46. package/AGENTS.md +0 -46
  47. package/BUGS.md +0 -1570
  48. package/CODE_OF_CONDUCT.md +0 -27
  49. package/CONTRIBUTING.md +0 -131
  50. package/SECURITY.md +0 -48
  51. package/livepilot/.Codex-plugin/plugin.json +0 -8
  52. package/livepilot/.claude-plugin/plugin.json +0 -8
  53. package/livepilot/agents/livepilot-producer/AGENT.md +0 -313
  54. package/livepilot/commands/arrange.md +0 -47
  55. package/livepilot/commands/beat.md +0 -77
  56. package/livepilot/commands/evaluate.md +0 -49
  57. package/livepilot/commands/memory.md +0 -22
  58. package/livepilot/commands/mix.md +0 -44
  59. package/livepilot/commands/perform.md +0 -42
  60. package/livepilot/commands/session.md +0 -13
  61. package/livepilot/commands/sounddesign.md +0 -43
  62. package/livepilot/skills/livepilot-arrangement/SKILL.md +0 -155
  63. package/livepilot/skills/livepilot-composition-engine/SKILL.md +0 -107
  64. package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +0 -97
  65. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +0 -102
  66. package/livepilot/skills/livepilot-core/SKILL.md +0 -184
  67. package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +0 -831
  68. package/livepilot/skills/livepilot-core/references/automation-atlas.md +0 -272
  69. package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +0 -110
  70. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +0 -687
  71. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +0 -753
  72. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +0 -525
  73. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +0 -402
  74. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +0 -963
  75. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +0 -874
  76. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +0 -571
  77. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +0 -714
  78. package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +0 -953
  79. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +0 -34
  80. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +0 -204
  81. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +0 -173
  82. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +0 -211
  83. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +0 -188
  84. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +0 -162
  85. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +0 -229
  86. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +0 -243
  87. package/livepilot/skills/livepilot-core/references/m4l-devices.md +0 -352
  88. package/livepilot/skills/livepilot-core/references/memory-guide.md +0 -107
  89. package/livepilot/skills/livepilot-core/references/midi-recipes.md +0 -402
  90. package/livepilot/skills/livepilot-core/references/mixing-patterns.md +0 -578
  91. package/livepilot/skills/livepilot-core/references/overview.md +0 -290
  92. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +0 -724
  93. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +0 -140
  94. package/livepilot/skills/livepilot-core/references/sound-design.md +0 -393
  95. package/livepilot/skills/livepilot-devices/SKILL.md +0 -169
  96. package/livepilot/skills/livepilot-evaluation/SKILL.md +0 -156
  97. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +0 -118
  98. package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +0 -121
  99. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +0 -110
  100. package/livepilot/skills/livepilot-mix-engine/SKILL.md +0 -123
  101. package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +0 -143
  102. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +0 -105
  103. package/livepilot/skills/livepilot-mixing/SKILL.md +0 -157
  104. package/livepilot/skills/livepilot-notes/SKILL.md +0 -130
  105. package/livepilot/skills/livepilot-performance-engine/SKILL.md +0 -122
  106. package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +0 -98
  107. package/livepilot/skills/livepilot-release/SKILL.md +0 -130
  108. package/livepilot/skills/livepilot-sample-engine/SKILL.md +0 -105
  109. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +0 -87
  110. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +0 -51
  111. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +0 -131
  112. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +0 -168
  113. package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +0 -119
  114. package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +0 -118
  115. package/livepilot/skills/livepilot-wonder/SKILL.md +0 -79
  116. package/m4l_device/LivePilot_Analyzer.amxd.pre-presentation-backup +0 -0
  117. package/m4l_device/LivePilot_Analyzer.maxpat +0 -2705
  118. package/m4l_device/LivePilot_Analyzer.maxproj +0 -53
  119. package/manifest.json +0 -91
  120. package/mcp_server/splice_client/protos/app_pb2.pyi +0 -1153
  121. package/scripts/generate_tool_catalog.py +0 -106
  122. package/scripts/sync_metadata.py +0 -349
@@ -1,106 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Generate tool catalog from live runtime metadata.
3
-
4
- Produces a markdown tool catalog validated against mcp.list_tools().
5
- This is the single source of truth — hand-edited catalogs are replaced.
6
-
7
- Usage: python3 scripts/generate_tool_catalog.py > docs/manual/tool-catalog-generated.md
8
- """
9
-
10
- import asyncio
11
- import inspect
12
- import sys
13
- from collections import defaultdict
14
- from pathlib import Path
15
-
16
- ROOT = Path(__file__).resolve().parent.parent
17
- sys.path.insert(0, str(ROOT))
18
-
19
-
20
- def get_tools() -> list[dict]:
21
- """Get all registered tools with metadata."""
22
- from mcp_server.server import mcp
23
-
24
- tools_raw = asyncio.run(mcp.list_tools())
25
- tools = []
26
- for t in tools_raw:
27
- # Get the module path to determine domain
28
- func = t.fn if hasattr(t, "fn") else None
29
- module = ""
30
- if func:
31
- module = func.__module__ if hasattr(func, "__module__") else ""
32
-
33
- # Get parameter names
34
- params = []
35
- if func:
36
- sig = inspect.signature(func)
37
- for name, param in sig.parameters.items():
38
- if name == "ctx":
39
- continue
40
- required = param.default is inspect.Parameter.empty
41
- params.append({"name": name, "required": required})
42
-
43
- tools.append({
44
- "name": t.name,
45
- "description": t.description[:120] if hasattr(t, "description") and t.description else "",
46
- "module": module,
47
- "params": params,
48
- })
49
-
50
- return tools
51
-
52
-
53
- def infer_domain(module: str) -> str:
54
- """Infer domain from module path using the same module-layout rule as
55
- ``scripts/sync_metadata.py``:
56
- - ``mcp_server.<X>.<...>`` → ``<X>``
57
- - ``mcp_server.tools.<Y>`` → ``<Y>``
58
- The display name is Title-Cased; underscores become spaces.
59
- """
60
- parts = module.split(".")
61
- if len(parts) < 2 or parts[0] != "mcp_server":
62
- return "Other"
63
- if parts[1] == "tools":
64
- domain_key = parts[2] if len(parts) > 2 else "other"
65
- else:
66
- domain_key = parts[1]
67
- return domain_key.replace("_", " ").title()
68
-
69
-
70
- def main():
71
- tools = get_tools()
72
- total = len(tools)
73
-
74
- # Group by domain
75
- domains = defaultdict(list)
76
- for t in tools:
77
- domain = infer_domain(t["module"])
78
- domains[domain].append(t)
79
-
80
- print(f"# LivePilot — Full Tool Catalog (Generated)")
81
- print()
82
- print(f"{total} tools across {len(domains)} domains.")
83
- print()
84
- print("> Auto-generated from `mcp.list_tools()`. Do not hand-edit.")
85
- print("> Regenerate: `python3 scripts/generate_tool_catalog.py`")
86
- print()
87
- print("---")
88
- print()
89
-
90
- for domain in sorted(domains.keys()):
91
- tool_list = sorted(domains[domain], key=lambda t: t["name"])
92
- print(f"## {domain} ({len(tool_list)})")
93
- print()
94
- print("| Tool | Description |")
95
- print("|------|-------------|")
96
- for t in tool_list:
97
- desc = t["description"].split("\n")[0].strip()
98
- print(f"| `{t['name']}` | {desc} |")
99
- print()
100
-
101
- print(f"---")
102
- print(f"*Generated from {total} registered tools.*")
103
-
104
-
105
- if __name__ == "__main__":
106
- main()
@@ -1,349 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Metadata sync — single source of truth for version, tool count, and domain count.
3
-
4
- Reads version from package.json, tool count from test_tools_contract.py,
5
- derives domain count + list from mcp_server source layout, and verifies all
6
- known locations are in sync.
7
-
8
- Usage:
9
- python scripts/sync_metadata.py --check # verify, exit 1 if stale
10
- python scripts/sync_metadata.py --fix # auto-fix stale references
11
- """
12
-
13
- import json
14
- import re
15
- import sys
16
- from pathlib import Path
17
-
18
- ROOT = Path(__file__).resolve().parents[1]
19
- TOOLS_ROOT = ROOT / "mcp_server"
20
-
21
-
22
- def get_version() -> str:
23
- """Read version from package.json (source of truth)."""
24
- pkg = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
25
- return pkg["version"]
26
-
27
-
28
- def get_tool_count() -> int:
29
- """Read tool count from test_tools_contract.py assertion."""
30
- src = (ROOT / "tests" / "test_tools_contract.py").read_text(encoding="utf-8")
31
- match = re.search(r"assert len\(tools\) == (\d+)", src)
32
- if match:
33
- return int(match.group(1))
34
- raise ValueError("Could not find tool count assertion in test_tools_contract.py")
35
-
36
-
37
- def get_domains() -> tuple[int, list[str]]:
38
- """Derive the set of tool domains from mcp_server source layout.
39
-
40
- A domain is:
41
- - the subdirectory name for ``mcp_server/<X>/...`` files containing ``@mcp.tool()``
42
- - the file stem for ``mcp_server/tools/<Y>.py`` files
43
-
44
- Returns (count, sorted list of names).
45
- """
46
- domains: set[str] = set()
47
- for py in TOOLS_ROOT.rglob("*.py"):
48
- try:
49
- content = py.read_text(encoding="utf-8")
50
- except (OSError, UnicodeDecodeError):
51
- continue
52
- if "@mcp.tool()" not in content:
53
- continue
54
- rel_parts = py.relative_to(TOOLS_ROOT).parts
55
- if len(rel_parts) < 2:
56
- # Top-level file (e.g., server.py). No such file currently registers
57
- # tools; if one does, it would need an explicit domain assignment.
58
- continue
59
- if rel_parts[0] == "tools":
60
- domains.add(py.stem)
61
- else:
62
- domains.add(rel_parts[0])
63
- return len(domains), sorted(domains)
64
-
65
-
66
- # Files that must contain the version string
67
- VERSION_FILES = [
68
- "package.json",
69
- "server.json",
70
- "manifest.json",
71
- "livepilot/.claude-plugin/plugin.json",
72
- "livepilot/.Codex-plugin/plugin.json",
73
- ".claude-plugin/marketplace.json",
74
- "mcp_server/__init__.py",
75
- "remote_script/LivePilot/__init__.py",
76
- "CLAUDE.md",
77
- "AGENTS.md",
78
- "livepilot/skills/livepilot-core/references/overview.md",
79
- "docs/M4L_BRIDGE.md",
80
- ]
81
-
82
- # Files that must contain the tool count
83
- TOOL_COUNT_FILES = [
84
- "README.md",
85
- "package.json",
86
- "server.json",
87
- "CLAUDE.md",
88
- "AGENTS.md",
89
- "CONTRIBUTING.md",
90
- "livepilot/.claude-plugin/plugin.json",
91
- "livepilot/.Codex-plugin/plugin.json",
92
- "livepilot/skills/livepilot-core/SKILL.md",
93
- "livepilot/skills/livepilot-core/references/overview.md",
94
- "docs/manual/index.md",
95
- "docs/manual/tool-reference.md",
96
- "docs/manual/tool-catalog.md",
97
- ]
98
-
99
- # Files that must contain the current domain count ("N domains").
100
- DOMAIN_COUNT_FILES = [
101
- "README.md",
102
- "package.json",
103
- "manifest.json",
104
- "CLAUDE.md",
105
- "AGENTS.md",
106
- "livepilot/.claude-plugin/plugin.json",
107
- "livepilot/.Codex-plugin/plugin.json",
108
- ".claude-plugin/marketplace.json",
109
- "livepilot/skills/livepilot-core/SKILL.md",
110
- "livepilot/skills/livepilot-core/references/overview.md",
111
- "livepilot/skills/livepilot-release/SKILL.md",
112
- "docs/manual/index.md",
113
- "docs/manual/tool-catalog.md",
114
- "docs/manual/tool-catalog-generated.md",
115
- "tests/test_tools_contract.py",
116
- ]
117
-
118
- # Files that enumerate the domain list inline as ``N domains: a, b, c, ...``.
119
- # Each file's enumeration must match the derived domain set exactly.
120
- DOMAIN_LIST_FILES = [
121
- "CLAUDE.md",
122
- "livepilot/skills/livepilot-release/SKILL.md",
123
- ]
124
-
125
-
126
- def check_version(version: str) -> list[str]:
127
- """Check all version files for staleness."""
128
- issues = []
129
- for rel_path in VERSION_FILES:
130
- path = ROOT / rel_path
131
- if not path.exists():
132
- continue
133
- content = path.read_text(encoding="utf-8")
134
- if version not in content:
135
- # Find what version IS there
136
- old = re.search(r"1\.\d+\.\d+", content)
137
- old_ver = old.group(0) if old else "???"
138
- if old_ver != version:
139
- issues.append(f" {rel_path}: has {old_ver}, expected {version}")
140
- return issues
141
-
142
-
143
- def check_tool_count(count: int) -> list[str]:
144
- """Check all tool count files for staleness."""
145
- issues = []
146
- count_str = str(count)
147
- for rel_path in TOOL_COUNT_FILES:
148
- path = ROOT / rel_path
149
- if not path.exists():
150
- continue
151
- content = path.read_text(encoding="utf-8")
152
- # Look for "N tools" pattern
153
- matches = re.findall(r"(\d+)\s+tools", content)
154
- for m in matches:
155
- if m != count_str and int(m) > 250: # ignore subset counts like "210 tools"
156
- issues.append(f" {rel_path}: has '{m} tools', expected '{count_str} tools'")
157
- break
158
- return issues
159
-
160
-
161
- def check_domain_count(count: int) -> list[str]:
162
- """Check all domain-count files for stale numbers."""
163
- issues = []
164
- count_str = str(count)
165
- for rel_path in DOMAIN_COUNT_FILES:
166
- path = ROOT / rel_path
167
- if not path.exists():
168
- continue
169
- content = path.read_text(encoding="utf-8")
170
- matches = re.findall(r"(\d+)\s+domains?\b", content)
171
- for m in matches:
172
- # Filter historical CHANGELOG-style subset counts (e.g., "5 domains",
173
- # "17 domains"). Active claim has always been >= 40.
174
- if m != count_str and int(m) > 35:
175
- issues.append(
176
- f" {rel_path}: has '{m} domains', expected '{count_str} domains'"
177
- )
178
- break
179
- return issues
180
-
181
-
182
- def check_domain_list(domains: list[str]) -> list[str]:
183
- """Verify each DOMAIN_LIST_FILES file enumerates exactly the derived domain set."""
184
- issues = []
185
- domain_set = set(domains)
186
- for rel_path in DOMAIN_LIST_FILES:
187
- path = ROOT / rel_path
188
- if not path.exists():
189
- continue
190
- content = path.read_text(encoding="utf-8")
191
- # Match "<N> domains: a, b, c, ..." up to the first period or newline.
192
- # Allow trailing markdown bold markers after "domains" (e.g. ``**43 domains**:``).
193
- match = re.search(r"\d+\s+domains?\**\s*[:\-]\s*([^.\n]+)", content)
194
- if not match:
195
- issues.append(
196
- f" {rel_path}: no 'N domains: ...' inline list found to verify"
197
- )
198
- continue
199
- raw_names = (n.strip() for n in match.group(1).split(","))
200
- listed = {re.sub(r"[^a-z0-9_]", "", n.lower()) for n in raw_names}
201
- listed.discard("")
202
- missing = domain_set - listed
203
- extra = listed - domain_set
204
- if missing:
205
- issues.append(
206
- f" {rel_path}: inline list missing {len(missing)} domain(s) — {', '.join(sorted(missing))}"
207
- )
208
- if extra:
209
- issues.append(
210
- f" {rel_path}: inline list has {len(extra)} unknown domain(s) — {', '.join(sorted(extra))}"
211
- )
212
- return issues
213
-
214
-
215
- def _fix_count(
216
- count: int, files: list[str], noun: str, threshold: int
217
- ) -> list[str]:
218
- """Replace every stale ``<N> <noun>(s)`` in *files* with ``<count> <noun>(s)``.
219
-
220
- Only substitutes where ``N != count`` and ``N > threshold``; this mirrors the
221
- filtering in the corresponding ``check_*`` function so historical/subset
222
- numbers are never rewritten.
223
- """
224
- fixed: list[str] = []
225
- count_str = str(count)
226
- pattern = re.compile(rf"(\d+)(\s+{re.escape(noun)}s?)\b")
227
- for rel_path in files:
228
- path = ROOT / rel_path
229
- if not path.exists():
230
- continue
231
- content = path.read_text(encoding="utf-8")
232
- seen_old: list[str] = []
233
-
234
- def replace(match: "re.Match[str]") -> str:
235
- old = match.group(1)
236
- if old != count_str and int(old) > threshold:
237
- seen_old.append(old)
238
- return f"{count_str}{match.group(2)}"
239
- return match.group(0)
240
-
241
- new_content = pattern.sub(replace, content)
242
- if seen_old:
243
- path.write_text(new_content, encoding="utf-8")
244
- fixed.append(f" {rel_path}: {noun} count {seen_old[0]} → {count_str}")
245
- return fixed
246
-
247
-
248
- def fix_tool_count(count: int) -> list[str]:
249
- return _fix_count(count, TOOL_COUNT_FILES, "tool", threshold=250)
250
-
251
-
252
- def fix_domain_count(count: int) -> list[str]:
253
- return _fix_count(count, DOMAIN_COUNT_FILES, "domain", threshold=35)
254
-
255
-
256
- def fix_domain_list(domains: list[str]) -> list[str]:
257
- """Append missing domain names to each DOMAIN_LIST_FILES inline enumeration.
258
-
259
- Extra (unknown) entries are never auto-removed — the script only adds, so an
260
- accidental pattern miss can't silently delete a legitimate entry.
261
- """
262
- fixed: list[str] = []
263
- pattern = re.compile(r"(\d+\s+domains?\**\s*[:\-]\s*)([^.\n]+)(\.|\n)")
264
- for rel_path in DOMAIN_LIST_FILES:
265
- path = ROOT / rel_path
266
- if not path.exists():
267
- continue
268
- content = path.read_text(encoding="utf-8")
269
- match = pattern.search(content)
270
- if not match:
271
- continue
272
- listed_raw = match.group(2)
273
- listed = {
274
- re.sub(r"[^a-z0-9_]", "", n.strip().lower())
275
- for n in listed_raw.split(",")
276
- }
277
- listed.discard("")
278
- missing = [d for d in domains if d not in listed]
279
- if not missing:
280
- continue
281
- new_list = listed_raw.rstrip() + ", " + ", ".join(missing)
282
- new_content = content[: match.start(2)] + new_list + content[match.end(2) :]
283
- path.write_text(new_content, encoding="utf-8")
284
- fixed.append(
285
- f" {rel_path}: appended {len(missing)} domain(s) — {', '.join(missing)}"
286
- )
287
- return fixed
288
-
289
-
290
- def main():
291
- mode = sys.argv[1] if len(sys.argv) > 1 else "--check"
292
-
293
- version = get_version()
294
- tool_count = get_tool_count()
295
- domain_count, domains = get_domains()
296
-
297
- print(
298
- f"Source of truth: version={version}, tools={tool_count}, domains={domain_count}"
299
- )
300
-
301
- if mode == "--fix":
302
- fixed = (
303
- fix_tool_count(tool_count)
304
- + fix_domain_count(domain_count)
305
- + fix_domain_list(domains)
306
- )
307
- if fixed:
308
- print(f"\nFixed {len(fixed)} reference(s):")
309
- for f in fixed:
310
- print(f)
311
- else:
312
- print("\nNothing to fix automatically.")
313
-
314
- remaining = (
315
- check_version(version)
316
- + check_tool_count(tool_count)
317
- + check_domain_count(domain_count)
318
- + check_domain_list(domains)
319
- )
320
- if remaining:
321
- print(f"\n{len(remaining)} issue(s) remain (manual fix required):")
322
- for issue in remaining:
323
- print(issue)
324
- print(
325
- "\nNote: --fix covers tool/domain counts and missing domain list entries. "
326
- "Version strings and extra list entries must be fixed by hand."
327
- )
328
- sys.exit(1)
329
- print("\nAll metadata in sync.")
330
- sys.exit(0)
331
-
332
- # --check mode (default)
333
- all_issues = (
334
- check_version(version)
335
- + check_tool_count(tool_count)
336
- + check_domain_count(domain_count)
337
- + check_domain_list(domains)
338
- )
339
- if all_issues:
340
- print(f"\nFound {len(all_issues)} stale reference(s):")
341
- for issue in all_issues:
342
- print(issue)
343
- sys.exit(1)
344
- print("All metadata in sync.")
345
- sys.exit(0)
346
-
347
-
348
- if __name__ == "__main__":
349
- main()