livepilot 1.10.7 → 1.10.9

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 (135) hide show
  1. package/CHANGELOG.md +254 -0
  2. package/README.md +19 -17
  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/evaluation/fabric.py +62 -1
  15. package/mcp_server/m4l_bridge.py +63 -12
  16. package/mcp_server/project_brain/automation_graph.py +23 -1
  17. package/mcp_server/project_brain/builder.py +2 -0
  18. package/mcp_server/project_brain/models.py +20 -1
  19. package/mcp_server/project_brain/tools.py +10 -3
  20. package/mcp_server/runtime/execution_router.py +16 -2
  21. package/mcp_server/runtime/remote_commands.py +6 -0
  22. package/mcp_server/sample_engine/models.py +22 -3
  23. package/mcp_server/semantic_moves/__init__.py +1 -0
  24. package/mcp_server/semantic_moves/compiler.py +9 -1
  25. package/mcp_server/semantic_moves/device_creation_compilers.py +47 -0
  26. package/mcp_server/semantic_moves/mix_compilers.py +170 -0
  27. package/mcp_server/semantic_moves/mix_moves.py +1 -1
  28. package/mcp_server/semantic_moves/models.py +5 -0
  29. package/mcp_server/semantic_moves/tools.py +154 -35
  30. package/mcp_server/server.py +147 -17
  31. package/mcp_server/services/singletons.py +68 -0
  32. package/mcp_server/session_continuity/models.py +13 -0
  33. package/mcp_server/session_continuity/tools.py +2 -0
  34. package/mcp_server/session_continuity/tracker.py +93 -0
  35. package/mcp_server/splice_client/client.py +29 -8
  36. package/mcp_server/tools/_analyzer_engine/__init__.py +39 -0
  37. package/mcp_server/tools/_analyzer_engine/context.py +103 -0
  38. package/mcp_server/tools/_analyzer_engine/flucoma.py +23 -0
  39. package/mcp_server/tools/_analyzer_engine/sample.py +122 -0
  40. package/mcp_server/tools/_motif_engine.py +19 -4
  41. package/mcp_server/tools/analyzer.py +25 -180
  42. package/mcp_server/tools/clips.py +240 -2
  43. package/mcp_server/tools/midi_io.py +10 -0
  44. package/mcp_server/tools/tracks.py +1 -1
  45. package/mcp_server/tools/transport.py +59 -4
  46. package/mcp_server/translation_engine/tools.py +8 -4
  47. package/package.json +25 -3
  48. package/remote_script/LivePilot/__init__.py +36 -9
  49. package/remote_script/LivePilot/arrangement.py +12 -2
  50. package/remote_script/LivePilot/browser.py +16 -6
  51. package/remote_script/LivePilot/devices.py +10 -5
  52. package/remote_script/LivePilot/notes.py +13 -2
  53. package/remote_script/LivePilot/server.py +51 -13
  54. package/remote_script/LivePilot/version_detect.py +7 -4
  55. package/server.json +20 -0
  56. package/.claude-plugin/marketplace.json +0 -21
  57. package/.mcp.json.disabled +0 -9
  58. package/.mcpbignore +0 -60
  59. package/AGENTS.md +0 -46
  60. package/BUGS.md +0 -1570
  61. package/CODE_OF_CONDUCT.md +0 -27
  62. package/CONTRIBUTING.md +0 -131
  63. package/SECURITY.md +0 -48
  64. package/livepilot/.Codex-plugin/plugin.json +0 -8
  65. package/livepilot/.claude-plugin/plugin.json +0 -8
  66. package/livepilot/agents/livepilot-producer/AGENT.md +0 -313
  67. package/livepilot/commands/arrange.md +0 -47
  68. package/livepilot/commands/beat.md +0 -77
  69. package/livepilot/commands/evaluate.md +0 -49
  70. package/livepilot/commands/memory.md +0 -22
  71. package/livepilot/commands/mix.md +0 -44
  72. package/livepilot/commands/perform.md +0 -42
  73. package/livepilot/commands/session.md +0 -13
  74. package/livepilot/commands/sounddesign.md +0 -43
  75. package/livepilot/skills/livepilot-arrangement/SKILL.md +0 -155
  76. package/livepilot/skills/livepilot-composition-engine/SKILL.md +0 -107
  77. package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +0 -97
  78. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +0 -102
  79. package/livepilot/skills/livepilot-core/SKILL.md +0 -184
  80. package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +0 -831
  81. package/livepilot/skills/livepilot-core/references/automation-atlas.md +0 -272
  82. package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +0 -110
  83. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +0 -687
  84. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +0 -753
  85. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +0 -525
  86. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +0 -402
  87. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +0 -963
  88. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +0 -874
  89. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +0 -571
  90. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +0 -714
  91. package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +0 -953
  92. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +0 -34
  93. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +0 -204
  94. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +0 -173
  95. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +0 -211
  96. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +0 -188
  97. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +0 -162
  98. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +0 -229
  99. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +0 -243
  100. package/livepilot/skills/livepilot-core/references/m4l-devices.md +0 -352
  101. package/livepilot/skills/livepilot-core/references/memory-guide.md +0 -107
  102. package/livepilot/skills/livepilot-core/references/midi-recipes.md +0 -402
  103. package/livepilot/skills/livepilot-core/references/mixing-patterns.md +0 -578
  104. package/livepilot/skills/livepilot-core/references/overview.md +0 -290
  105. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +0 -724
  106. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +0 -140
  107. package/livepilot/skills/livepilot-core/references/sound-design.md +0 -393
  108. package/livepilot/skills/livepilot-devices/SKILL.md +0 -169
  109. package/livepilot/skills/livepilot-evaluation/SKILL.md +0 -156
  110. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +0 -118
  111. package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +0 -121
  112. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +0 -110
  113. package/livepilot/skills/livepilot-mix-engine/SKILL.md +0 -123
  114. package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +0 -143
  115. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +0 -105
  116. package/livepilot/skills/livepilot-mixing/SKILL.md +0 -157
  117. package/livepilot/skills/livepilot-notes/SKILL.md +0 -130
  118. package/livepilot/skills/livepilot-performance-engine/SKILL.md +0 -122
  119. package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +0 -98
  120. package/livepilot/skills/livepilot-release/SKILL.md +0 -130
  121. package/livepilot/skills/livepilot-sample-engine/SKILL.md +0 -105
  122. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +0 -87
  123. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +0 -51
  124. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +0 -131
  125. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +0 -168
  126. package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +0 -119
  127. package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +0 -118
  128. package/livepilot/skills/livepilot-wonder/SKILL.md +0 -79
  129. package/m4l_device/LivePilot_Analyzer.amxd.pre-presentation-backup +0 -0
  130. package/m4l_device/LivePilot_Analyzer.maxpat +0 -2705
  131. package/m4l_device/LivePilot_Analyzer.maxproj +0 -53
  132. package/manifest.json +0 -91
  133. package/mcp_server/splice_client/protos/app_pb2.pyi +0 -1153
  134. package/scripts/generate_tool_catalog.py +0 -106
  135. 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()