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.
Files changed (78) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.mcp.json.disabled +9 -0
  3. package/.mcpbignore +3 -0
  4. package/AGENTS.md +3 -3
  5. package/BUGS.md +1570 -0
  6. package/CHANGELOG.md +42 -0
  7. package/CONTRIBUTING.md +1 -1
  8. package/README.md +7 -7
  9. package/bin/livepilot.js +28 -8
  10. package/livepilot/.Codex-plugin/plugin.json +2 -2
  11. package/livepilot/.claude-plugin/plugin.json +2 -2
  12. package/livepilot/skills/livepilot-core/SKILL.md +4 -4
  13. package/livepilot/skills/livepilot-core/references/overview.md +2 -2
  14. package/livepilot/skills/livepilot-release/SKILL.md +8 -8
  15. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  16. package/m4l_device/LivePilot_Analyzer.amxd.pre-presentation-backup +0 -0
  17. package/m4l_device/LivePilot_Analyzer.maxproj +53 -0
  18. package/m4l_device/livepilot_bridge.js +214 -2
  19. package/manifest.json +3 -3
  20. package/mcp_server/__init__.py +1 -1
  21. package/mcp_server/atlas/__init__.py +93 -26
  22. package/mcp_server/creative_constraints/tools.py +206 -33
  23. package/mcp_server/experiment/engine.py +7 -9
  24. package/mcp_server/hook_hunter/analyzer.py +62 -9
  25. package/mcp_server/hook_hunter/tools.py +60 -9
  26. package/mcp_server/m4l_bridge.py +21 -6
  27. package/mcp_server/musical_intelligence/detectors.py +32 -0
  28. package/mcp_server/performance_engine/tools.py +112 -29
  29. package/mcp_server/preview_studio/engine.py +89 -8
  30. package/mcp_server/preview_studio/tools.py +22 -6
  31. package/mcp_server/project_brain/automation_graph.py +71 -19
  32. package/mcp_server/project_brain/builder.py +2 -0
  33. package/mcp_server/project_brain/tools.py +55 -5
  34. package/mcp_server/reference_engine/profile_builder.py +129 -3
  35. package/mcp_server/reference_engine/tools.py +47 -6
  36. package/mcp_server/runtime/execution_router.py +50 -0
  37. package/mcp_server/runtime/mcp_dispatch.py +75 -3
  38. package/mcp_server/runtime/remote_commands.py +4 -2
  39. package/mcp_server/sample_engine/analyzer.py +131 -4
  40. package/mcp_server/sample_engine/critics.py +29 -8
  41. package/mcp_server/sample_engine/models.py +20 -1
  42. package/mcp_server/sample_engine/tools.py +48 -14
  43. package/mcp_server/semantic_moves/sound_design_compilers.py +22 -59
  44. package/mcp_server/semantic_moves/transition_compilers.py +12 -19
  45. package/mcp_server/server.py +68 -2
  46. package/mcp_server/session_continuity/models.py +4 -0
  47. package/mcp_server/session_continuity/tracker.py +14 -1
  48. package/mcp_server/song_brain/builder.py +110 -12
  49. package/mcp_server/song_brain/tools.py +77 -13
  50. package/mcp_server/sound_design/tools.py +112 -1
  51. package/mcp_server/stuckness_detector/detector.py +90 -0
  52. package/mcp_server/stuckness_detector/tools.py +41 -0
  53. package/mcp_server/tools/_agent_os_engine/critics.py +24 -0
  54. package/mcp_server/tools/_composition_engine/__init__.py +2 -2
  55. package/mcp_server/tools/_composition_engine/harmony.py +90 -0
  56. package/mcp_server/tools/_composition_engine/sections.py +47 -4
  57. package/mcp_server/tools/_harmony_engine.py +52 -8
  58. package/mcp_server/tools/_research_engine.py +98 -19
  59. package/mcp_server/tools/_theory_engine.py +138 -9
  60. package/mcp_server/tools/agent_os.py +20 -3
  61. package/mcp_server/tools/analyzer.py +98 -0
  62. package/mcp_server/tools/clips.py +45 -0
  63. package/mcp_server/tools/composition.py +66 -23
  64. package/mcp_server/tools/devices.py +22 -1
  65. package/mcp_server/tools/harmony.py +115 -14
  66. package/mcp_server/tools/midi_io.py +13 -1
  67. package/mcp_server/tools/mixing.py +35 -1
  68. package/mcp_server/tools/motif.py +49 -3
  69. package/mcp_server/tools/research.py +24 -0
  70. package/mcp_server/tools/theory.py +108 -16
  71. package/mcp_server/transition_engine/critics.py +18 -11
  72. package/package.json +2 -2
  73. package/remote_script/LivePilot/__init__.py +57 -2
  74. package/remote_script/LivePilot/clips.py +69 -0
  75. package/remote_script/LivePilot/mixing.py +117 -0
  76. package/remote_script/LivePilot/router.py +13 -1
  77. package/scripts/generate_tool_catalog.py +13 -38
  78. package/scripts/sync_metadata.py +231 -14
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env python3
2
- """Metadata sync — single source of truth for version and tool count.
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
- and verifies all known locations are in sync.
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(f"Source of truth: version={version}, tools={tool_count}")
297
+ print(
298
+ f"Source of truth: version={version}, tools={tool_count}, domains={domain_count}"
299
+ )
111
300
 
112
- version_issues = check_version(version)
113
- count_issues = check_tool_count(tool_count)
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
- all_issues = version_issues + count_issues
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
- if mode == "--check":
122
- sys.exit(1)
123
- elif mode == "--fix":
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__":