livepilot 1.10.6 → 1.10.7
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/.claude-plugin/marketplace.json +3 -3
- package/.mcp.json.disabled +9 -0
- package/.mcpbignore +3 -0
- package/AGENTS.md +3 -3
- package/BUGS.md +1570 -0
- package/CHANGELOG.md +42 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +7 -7
- package/bin/livepilot.js +28 -8
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +4 -4
- package/livepilot/skills/livepilot-core/references/overview.md +2 -2
- package/livepilot/skills/livepilot-release/SKILL.md +8 -8
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/LivePilot_Analyzer.amxd.pre-presentation-backup +0 -0
- package/m4l_device/LivePilot_Analyzer.maxproj +53 -0
- package/m4l_device/livepilot_bridge.js +214 -2
- package/manifest.json +3 -3
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +93 -26
- package/mcp_server/creative_constraints/tools.py +206 -33
- package/mcp_server/experiment/engine.py +7 -9
- package/mcp_server/hook_hunter/analyzer.py +62 -9
- package/mcp_server/hook_hunter/tools.py +60 -9
- package/mcp_server/m4l_bridge.py +21 -6
- package/mcp_server/musical_intelligence/detectors.py +32 -0
- package/mcp_server/performance_engine/tools.py +112 -29
- package/mcp_server/preview_studio/engine.py +89 -8
- package/mcp_server/preview_studio/tools.py +22 -6
- package/mcp_server/project_brain/automation_graph.py +71 -19
- package/mcp_server/project_brain/builder.py +2 -0
- package/mcp_server/project_brain/tools.py +55 -5
- package/mcp_server/reference_engine/profile_builder.py +129 -3
- package/mcp_server/reference_engine/tools.py +47 -6
- package/mcp_server/runtime/execution_router.py +50 -0
- package/mcp_server/runtime/mcp_dispatch.py +75 -3
- package/mcp_server/runtime/remote_commands.py +4 -2
- package/mcp_server/sample_engine/analyzer.py +131 -4
- package/mcp_server/sample_engine/critics.py +29 -8
- package/mcp_server/sample_engine/models.py +20 -1
- package/mcp_server/sample_engine/tools.py +48 -14
- package/mcp_server/semantic_moves/sound_design_compilers.py +22 -59
- package/mcp_server/semantic_moves/transition_compilers.py +12 -19
- package/mcp_server/server.py +68 -2
- package/mcp_server/session_continuity/models.py +4 -0
- package/mcp_server/session_continuity/tracker.py +14 -1
- package/mcp_server/song_brain/builder.py +110 -12
- package/mcp_server/song_brain/tools.py +77 -13
- package/mcp_server/sound_design/tools.py +112 -1
- package/mcp_server/stuckness_detector/detector.py +90 -0
- package/mcp_server/stuckness_detector/tools.py +41 -0
- package/mcp_server/tools/_agent_os_engine/critics.py +24 -0
- package/mcp_server/tools/_composition_engine/__init__.py +2 -2
- package/mcp_server/tools/_composition_engine/harmony.py +90 -0
- package/mcp_server/tools/_composition_engine/sections.py +47 -4
- package/mcp_server/tools/_harmony_engine.py +52 -8
- package/mcp_server/tools/_research_engine.py +98 -19
- package/mcp_server/tools/_theory_engine.py +138 -9
- package/mcp_server/tools/agent_os.py +20 -3
- package/mcp_server/tools/analyzer.py +98 -0
- package/mcp_server/tools/clips.py +45 -0
- package/mcp_server/tools/composition.py +66 -23
- package/mcp_server/tools/devices.py +22 -1
- package/mcp_server/tools/harmony.py +115 -14
- package/mcp_server/tools/midi_io.py +13 -1
- package/mcp_server/tools/mixing.py +35 -1
- package/mcp_server/tools/motif.py +49 -3
- package/mcp_server/tools/research.py +24 -0
- package/mcp_server/tools/theory.py +108 -16
- package/mcp_server/transition_engine/critics.py +18 -11
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +57 -2
- package/remote_script/LivePilot/clips.py +69 -0
- package/remote_script/LivePilot/mixing.py +117 -0
- package/remote_script/LivePilot/router.py +13 -1
- package/scripts/generate_tool_catalog.py +13 -38
- package/scripts/sync_metadata.py +231 -14
package/scripts/sync_metadata.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Metadata sync — single source of truth for version and
|
|
2
|
+
"""Metadata sync — single source of truth for version, tool count, and domain count.
|
|
3
3
|
|
|
4
4
|
Reads version from package.json, tool count from test_tools_contract.py,
|
|
5
|
-
|
|
5
|
+
derives domain count + list from mcp_server source layout, and verifies all
|
|
6
|
+
known locations are in sync.
|
|
6
7
|
|
|
7
8
|
Usage:
|
|
8
9
|
python scripts/sync_metadata.py --check # verify, exit 1 if stale
|
|
@@ -15,6 +16,7 @@ import sys
|
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
18
|
ROOT = Path(__file__).resolve().parents[1]
|
|
19
|
+
TOOLS_ROOT = ROOT / "mcp_server"
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def get_version() -> str:
|
|
@@ -32,6 +34,35 @@ def get_tool_count() -> int:
|
|
|
32
34
|
raise ValueError("Could not find tool count assertion in test_tools_contract.py")
|
|
33
35
|
|
|
34
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
|
+
|
|
35
66
|
# Files that must contain the version string
|
|
36
67
|
VERSION_FILES = [
|
|
37
68
|
"package.json",
|
|
@@ -65,6 +96,32 @@ TOOL_COUNT_FILES = [
|
|
|
65
96
|
"docs/manual/tool-catalog.md",
|
|
66
97
|
]
|
|
67
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
|
+
|
|
68
125
|
|
|
69
126
|
def check_version(version: str) -> list[str]:
|
|
70
127
|
"""Check all version files for staleness."""
|
|
@@ -101,31 +158,191 @@ def check_tool_count(count: int) -> list[str]:
|
|
|
101
158
|
return issues
|
|
102
159
|
|
|
103
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
|
+
|
|
104
290
|
def main():
|
|
105
291
|
mode = sys.argv[1] if len(sys.argv) > 1 else "--check"
|
|
106
292
|
|
|
107
293
|
version = get_version()
|
|
108
294
|
tool_count = get_tool_count()
|
|
295
|
+
domain_count, domains = get_domains()
|
|
109
296
|
|
|
110
|
-
print(
|
|
297
|
+
print(
|
|
298
|
+
f"Source of truth: version={version}, tools={tool_count}, domains={domain_count}"
|
|
299
|
+
)
|
|
111
300
|
|
|
112
|
-
|
|
113
|
-
|
|
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.")
|
|
114
313
|
|
|
115
|
-
|
|
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)
|
|
116
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
|
+
)
|
|
117
339
|
if all_issues:
|
|
118
340
|
print(f"\nFound {len(all_issues)} stale reference(s):")
|
|
119
341
|
for issue in all_issues:
|
|
120
342
|
print(issue)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
print("\n--fix mode not yet implemented. Fix manually.")
|
|
125
|
-
sys.exit(1)
|
|
126
|
-
else:
|
|
127
|
-
print("All metadata in sync.")
|
|
128
|
-
sys.exit(0)
|
|
343
|
+
sys.exit(1)
|
|
344
|
+
print("All metadata in sync.")
|
|
345
|
+
sys.exit(0)
|
|
129
346
|
|
|
130
347
|
|
|
131
348
|
if __name__ == "__main__":
|