@mthines/reaper-mcp 0.7.0 → 0.8.0

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/README.md CHANGED
@@ -17,8 +17,7 @@ npx @mthines/reaper-mcp setup
17
17
 
18
18
  # 2. In REAPER: Actions > Load ReaScript > select mcp_bridge.lua > Run
19
19
 
20
- # 3. Install AI mix knowledge in your project
21
- cd your-project
20
+ # 3. Install AI mix knowledge (globally by default, or --project for local)
22
21
  npx @mthines/reaper-mcp install-skills
23
22
 
24
23
  # 4. Open Claude Code — you're ready to mix
@@ -94,19 +93,27 @@ This copies into your REAPER resource folder:
94
93
 
95
94
  You should see in REAPER's console: `MCP Bridge: Started`
96
95
 
97
- ### Step 3: Install AI mix knowledge in your project
96
+ ### Step 3: Install AI mix knowledge
98
97
 
99
98
  ```bash
100
- cd your-music-project
99
+ # Install globally (default) — available from any directory
101
100
  npx @mthines/reaper-mcp install-skills
101
+
102
+ # Or install into a specific project
103
+ cd your-music-project
104
+ npx @mthines/reaper-mcp install-skills --project
102
105
  ```
103
106
 
104
- This creates in your project:
107
+ **Global install** (`--global`, default) installs to `~/.claude/`:
108
+
109
+ - `~/.claude/agents/` — mix engineer subagents (`@mix-engineer`, `@gain-stage`, `@mix-analyzer`, `@master`)
110
+ - `~/.claude/rules/` — architecture and development rules
111
+ - `~/.claude/skills/` — skills like `/learn-plugin`
112
+ - `~/.claude/knowledge/` — plugin knowledge, genre rules, workflows, reference data
113
+
114
+ **Project install** (`--project`) installs to your current directory:
105
115
 
106
- - `.claude/agents/` — mix engineer subagents (`@mix-engineer`, `@gain-stage`, `@mix-analyzer`, `@master`)
107
- - `.claude/rules/` — architecture and development rules
108
- - `.claude/skills/` — skills like `/learn-plugin`
109
- - `knowledge/` — plugin knowledge, genre rules, workflows, reference data
116
+ - `.claude/agents/`, `.claude/rules/`, `.claude/skills/`, `knowledge/` — same as above, scoped to the project
110
117
  - `.mcp.json` — MCP server configuration for Claude Code
111
118
 
112
119
  ### Step 4: Verify
@@ -238,7 +245,7 @@ Checks that the bridge is connected, knowledge is installed, and MCP config exis
238
245
 
239
246
  ## Using the Mix Agents
240
247
 
241
- Once you've run `setup` and `install-skills`, open Claude Code in your project directory. Four specialized mix agents are available:
248
+ Once you've run `setup` and `install-skills`, open Claude Code. Four specialized mix agents are available:
242
249
 
243
250
  ### Available Agents
244
251
 
@@ -440,7 +447,8 @@ The format is `mcp__reaper__{tool_name}`. Once added, Claude Code will run these
440
447
  npx @mthines/reaper-mcp # Start MCP server (default)
441
448
  npx @mthines/reaper-mcp serve # Start MCP server (stdio mode)
442
449
  npx @mthines/reaper-mcp setup # Install Lua bridge + JSFX into REAPER
443
- npx @mthines/reaper-mcp install-skills # Install AI knowledge + agents into your project
450
+ npx @mthines/reaper-mcp install-skills # Install AI knowledge + agents (globally by default)
451
+ npx @mthines/reaper-mcp install-skills --project # Install into current project directory
444
452
  npx @mthines/reaper-mcp doctor # Verify everything is configured
445
453
  npx @mthines/reaper-mcp status # Check bridge connection
446
454
  ```
@@ -454,7 +462,7 @@ reaper-mcp setup
454
462
 
455
463
  ## Claude Code Integration
456
464
 
457
- After `install-skills`, your project has a `.mcp.json`:
465
+ After `install-skills --project`, your project has a `.mcp.json`:
458
466
 
459
467
  ```json
460
468
  {
@@ -0,0 +1,71 @@
1
+ # Knowledge Base
2
+
3
+ Audio engineering knowledge consumed by `reaper-mix-agent` at runtime. Markdown files with YAML frontmatter.
4
+
5
+ ## File Organization
6
+
7
+ ```
8
+ knowledge/
9
+ plugins/{vendor-slug}/{plugin-slug}.md # Plugin knowledge (fx_match, parameters, presets)
10
+ genres/{genre-id}.md # Genre mixing conventions (targets, EQ, compression)
11
+ workflows/{workflow-id}.md # Step-by-step mixing workflows
12
+ reference/{topic}.md # Reference material (frequencies, metering, etc.)
13
+ ```
14
+
15
+ ## How Files Are Loaded
16
+
17
+ `knowledge-loader.ts` in `reaper-mix-agent`:
18
+ - Recursively collects all `.md` files
19
+ - Categorizes by first path segment (`plugins/` → plugin, `genres/` → genre, etc.)
20
+ - Skips files with `_template` in the path
21
+ - Parses frontmatter with a simple line-by-line parser (NOT full YAML)
22
+
23
+ ## Frontmatter Constraints
24
+
25
+ The parser is simple — respect these limitations:
26
+ - Arrays must be single-line: `fx_match: ["Pro-Q 3", "VST3: FabFilter Pro-Q 3"]`
27
+ - No multi-line values or nested objects
28
+ - `#` in frontmatter is treated as a comment — don't use in values
29
+ - Numbers parse automatically; booleans must be `true`/`false` (lowercase)
30
+
31
+ ## Required Frontmatter by Type
32
+
33
+ ### Plugins
34
+ | Field | Required | Description |
35
+ |-------|----------|-------------|
36
+ | `name` | Yes | Display name |
37
+ | `fx_match` | Yes | Array of strings for case-insensitive substring match against REAPER FX list |
38
+ | `category` | Yes | `eq`, `compressor`, `limiter`, `reverb`, `delay`, `gate`, etc. |
39
+ | `vendor` | Yes | Vendor name |
40
+ | `preference` | Yes | 0-100 score (stock: 30-50, third-party: 70-85, industry-standard: 85-95) |
41
+ | `style` | No | `transparent`, `character`, `vintage`, `modern`, `surgical` |
42
+ | `replaces` | No | Array of stock plugin IDs this replaces |
43
+
44
+ ### Genres
45
+ | Field | Required | Description |
46
+ |-------|----------|-------------|
47
+ | `name` | Yes | Genre display name |
48
+ | `id` | Yes | Must match filename (e.g., `rock.md` → `id: rock`) |
49
+ | `lufs_target` | Yes | Array `[-13, -10]` for min/max LUFS |
50
+ | `true_peak` | Yes | dBTP ceiling (e.g., `-1.0`) |
51
+ | `parent` | No | Inherits from parent genre |
52
+
53
+ ### Workflows
54
+ | Field | Required | Description |
55
+ |-------|----------|-------------|
56
+ | `name` | Yes | Workflow display name |
57
+ | `id` | Yes | Must match filename |
58
+
59
+ ## fx_match Pattern Guide
60
+
61
+ Patterns are matched as case-insensitive substrings against REAPER's installed FX names:
62
+ - `"Pro-Q 3"` matches `"VST3: FabFilter Pro-Q 3 (FabFilter)"`, `"AU: Pro-Q 3"`, etc.
63
+ - Include multiple patterns for VST/VST3/AU variants if needed
64
+ - Check REAPER's FX Browser for exact names
65
+
66
+ ## Coupled Code
67
+
68
+ Changes here may require updates in `apps/reaper-mix-agent/`:
69
+ - New knowledge type → update `typeFromPath()` in `knowledge-loader.ts`
70
+ - New frontmatter fields → update consumers in `agent.ts` or `plugin-resolver.ts`
71
+ - New workflow file → should have a matching mode in `modes/`
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: Learn Plugin
3
+ id: learn-plugin
4
+ description: Discover an unknown plugin by reading its parameters, researching online, and creating a knowledge file
5
+ ---
6
+
7
+ # Learn Plugin
8
+
9
+ ## When to Use
10
+
11
+ When you encounter an installed FX plugin that has no knowledge file and you need to understand what it does, how to use it, and what parameters to set. This workflow turns an unknown plugin into a documented, reusable knowledge file.
12
+
13
+ ## Prerequisites
14
+
15
+ - The plugin must be loaded on a track in the current session (so you can read its parameters)
16
+ - You need the track index and FX index of the plugin
17
+
18
+ ## Steps
19
+
20
+ ### Step 1: Read the plugin's parameters
21
+
22
+ ```
23
+ tool: get_fx_parameters
24
+ params:
25
+ trackIndex: N
26
+ fxIndex: M
27
+ ```
28
+
29
+ Examine the parameter list carefully. Parameter names are strong clues:
30
+ - **Frequency, Q, Gain, Bandwidth, Slope** → EQ
31
+ - **Threshold, Ratio, Attack, Release, Knee, Makeup** → Compressor
32
+ - **Ceiling, Lookahead, True Peak** → Limiter
33
+ - **Pre-delay, Decay, Damping, Room Size, Diffusion** → Reverb
34
+ - **Time, Feedback, Mix, Sync, Ping-pong** → Delay
35
+ - **Drive, Saturation, Harmonics, Warmth** → Saturator
36
+ - **Range, Hold, Hysteresis** → Gate
37
+ - **Frequency, Sensitivity, Reduction** → De-esser
38
+ - **Amp Model, Cabinet, Gain** → Amp sim
39
+ - **Width, Mid/Side, Rotation** → Stereo imager
40
+ - **Pitch, Formant, Speed, Correction** → Pitch correction
41
+
42
+ Record the full parameter list with names, current values, and ranges.
43
+
44
+ ### Step 2: Check factory presets
45
+
46
+ ```
47
+ tool: get_fx_preset_list
48
+ params:
49
+ trackIndex: N
50
+ fxIndex: M
51
+ ```
52
+
53
+ Preset names often reveal intended use cases and the plugin's character. Look for:
54
+ - Category hints (e.g., "Vocal", "Drum Bus", "Mastering")
55
+ - Character hints (e.g., "Warm", "Transparent", "Aggressive", "Vintage")
56
+ - Genre hints (e.g., "Hip Hop Vocal", "Rock Guitar")
57
+
58
+ ### Step 3: Research the plugin (if still unclear)
59
+
60
+ If the parameter names and presets don't clearly identify the plugin, do a web search:
61
+
62
+ ```
63
+ Search: "{plugin name}" audio plugin parameters guide
64
+ ```
65
+
66
+ Look for:
67
+ - Official product page (features, description)
68
+ - Manual or parameter reference
69
+ - Professional reviews describing the plugin's character and best use cases
70
+ - Forum discussions about recommended settings
71
+
72
+ ### Step 4: Determine metadata
73
+
74
+ From what you've learned, decide:
75
+
76
+ | Field | How to determine |
77
+ |-------|-----------------|
78
+ | `name` | Full display name as shown in REAPER |
79
+ | `fx_match` | The exact name from REAPER's FX list, plus common variations (VST/VST3/AU) |
80
+ | `category` | Primary category from: eq, compressor, limiter, saturator, reverb, delay, gate, de-esser, amp-sim, channel-strip, stereo-imager, multiband, pitch-correction |
81
+ | `vendor` | The plugin manufacturer |
82
+ | `preference` | 30-50 for free/stock, 55-70 for decent third-party, 70-85 for preferred, 85-95 for industry-standard |
83
+ | `style` | transparent, character, vintage, modern, or surgical |
84
+ | `replaces` | Stock REAPER plugin IDs this can replace (e.g., "rea-eq", "rea-comp") |
85
+
86
+ ### Step 5: Write the knowledge file
87
+
88
+ Create the file at `knowledge/plugins/{vendor-slug}/{plugin-slug}.md` using the template structure:
89
+
90
+ ```markdown
91
+ ---
92
+ name: {Plugin Name}
93
+ fx_match: ["{exact REAPER name}", "{alternate match}"]
94
+ category: {category}
95
+ style: {style}
96
+ vendor: {Vendor Name}
97
+ preference: {score}
98
+ replaces: [{stock plugins if applicable}]
99
+ ---
100
+
101
+ # {Plugin Name}
102
+
103
+ ## What it does
104
+
105
+ {1-2 paragraphs: purpose, character, primary use cases}
106
+
107
+ ## Key parameters by name
108
+
109
+ | Parameter | Range | Description |
110
+ |-----------|-------|-------------|
111
+ | {exact name} | {range} | {description} |
112
+
113
+ ## Recommended settings
114
+
115
+ ### Use case: {instrument or purpose}
116
+
117
+ | Parameter | Value | Why |
118
+ |-----------|-------|-----|
119
+ | {name} | {value} | {reason} |
120
+
121
+ ## Presets worth knowing
122
+
123
+ - **{Preset Name}** — {what it does}
124
+
125
+ ## When to prefer this
126
+
127
+ - {situations where this plugin is the best choice}
128
+ ```
129
+
130
+ ### Step 6: Verify the knowledge file
131
+
132
+ After writing the file, confirm:
133
+ - `fx_match` patterns actually match the installed FX name (case-insensitive substring)
134
+ - All parameter names are EXACT (copy from `get_fx_parameters` output)
135
+ - Category and preference score are appropriate
136
+ - Recommended settings include at least one concrete use case
137
+
138
+ ## Tips
139
+
140
+ - When multiple parameters share a prefix (e.g., "Band 1 Frequency", "Band 2 Frequency"), document the pattern once with "Band N" notation
141
+ - Include the normalized value range (0.0-1.0) alongside the human-readable range when the mapping isn't obvious
142
+ - If the plugin has multiple modes or routing options, document each mode separately
143
+ - For channel-strip plugins with multiple sections (EQ + comp + gate), consider documenting under the most prominent category but mention all capabilities
package/main.js CHANGED
@@ -1456,6 +1456,109 @@ function registerEnvelopeTools(server) {
1456
1456
  return { content: [{ type: "text", text: `Deleted envelope point ${pointIndex}` }] };
1457
1457
  }
1458
1458
  );
1459
+ server.tool(
1460
+ "create_track_envelope",
1461
+ "Create/show an automation envelope on a track. Use envelopeName for built-in envelopes (Volume, Pan, Mute, Width, Trim Volume) or fxIndex+paramIndex for FX parameter envelopes. The envelope is made visible and active.",
1462
+ {
1463
+ trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
1464
+ envelopeName: z14.string().optional().describe('Built-in envelope name: "Volume", "Pan", "Mute", "Width", "Trim Volume"'),
1465
+ fxIndex: z14.coerce.number().int().min(0).optional().describe("FX chain index (for FX parameter envelopes)"),
1466
+ paramIndex: z14.coerce.number().int().min(0).optional().describe("FX parameter index (required if fxIndex provided)")
1467
+ },
1468
+ async ({ trackIndex, envelopeName, fxIndex, paramIndex }) => {
1469
+ const res = await sendCommand("create_track_envelope", {
1470
+ trackIndex,
1471
+ envelopeName,
1472
+ fxIndex,
1473
+ paramIndex
1474
+ });
1475
+ if (!res.success) {
1476
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1477
+ }
1478
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1479
+ }
1480
+ );
1481
+ server.tool(
1482
+ "set_envelope_properties",
1483
+ "Set properties (active, visible, armed) on a track envelope. Requires SWS extension for full support.",
1484
+ {
1485
+ trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
1486
+ envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
1487
+ active: z14.boolean().optional().describe("Set envelope active/inactive"),
1488
+ visible: z14.boolean().optional().describe("Set envelope visible/hidden in arrange view"),
1489
+ armed: z14.boolean().optional().describe("Set envelope armed for writing automation")
1490
+ },
1491
+ async ({ trackIndex, envelopeIndex, active, visible, armed }) => {
1492
+ const res = await sendCommand("set_envelope_properties", {
1493
+ trackIndex,
1494
+ envelopeIndex,
1495
+ active,
1496
+ visible,
1497
+ armed
1498
+ });
1499
+ if (!res.success) {
1500
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1501
+ }
1502
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1503
+ }
1504
+ );
1505
+ server.tool(
1506
+ "clear_envelope",
1507
+ "Delete ALL automation points from an envelope, resetting it to its default state",
1508
+ {
1509
+ trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
1510
+ envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track")
1511
+ },
1512
+ async ({ trackIndex, envelopeIndex }) => {
1513
+ const res = await sendCommand("clear_envelope", { trackIndex, envelopeIndex });
1514
+ if (!res.success) {
1515
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1516
+ }
1517
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1518
+ }
1519
+ );
1520
+ server.tool(
1521
+ "remove_envelope_points",
1522
+ "Delete automation points in a time range from a track envelope. Use to surgically remove a section of automation.",
1523
+ {
1524
+ trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
1525
+ envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
1526
+ timeStart: z14.coerce.number().describe("Start of time range in seconds (inclusive)"),
1527
+ timeEnd: z14.coerce.number().describe("End of time range in seconds (exclusive)")
1528
+ },
1529
+ async ({ trackIndex, envelopeIndex, timeStart, timeEnd }) => {
1530
+ const res = await sendCommand("remove_envelope_points", {
1531
+ trackIndex,
1532
+ envelopeIndex,
1533
+ timeStart,
1534
+ timeEnd
1535
+ });
1536
+ if (!res.success) {
1537
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1538
+ }
1539
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1540
+ }
1541
+ );
1542
+ server.tool(
1543
+ "insert_envelope_points",
1544
+ "Batch insert multiple automation points on a track envelope. Much faster than repeated insert_envelope_point calls.",
1545
+ {
1546
+ trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
1547
+ envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
1548
+ points: z14.string().describe("JSON array of point objects: [{time, value, shape?, tension?}, ...]")
1549
+ },
1550
+ async ({ trackIndex, envelopeIndex, points }) => {
1551
+ const res = await sendCommand("insert_envelope_points", {
1552
+ trackIndex,
1553
+ envelopeIndex,
1554
+ points
1555
+ });
1556
+ if (!res.success) {
1557
+ return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
1558
+ }
1559
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
1560
+ }
1561
+ );
1459
1562
  }
1460
1563
 
1461
1564
  // apps/reaper-mcp-server/src/server.ts
@@ -1669,7 +1772,12 @@ var MCP_TOOL_NAMES = [
1669
1772
  "get_track_envelopes",
1670
1773
  "get_envelope_points",
1671
1774
  "insert_envelope_point",
1672
- "delete_envelope_point"
1775
+ "insert_envelope_points",
1776
+ "delete_envelope_point",
1777
+ "create_track_envelope",
1778
+ "set_envelope_properties",
1779
+ "clear_envelope",
1780
+ "remove_envelope_points"
1673
1781
  ];
1674
1782
  function ensureClaudeSettings(settingsPath) {
1675
1783
  const allowList = MCP_TOOL_NAMES.map((t) => `mcp__reaper__${t}`);
@@ -1708,7 +1816,7 @@ async function setup() {
1708
1816
  } else {
1709
1817
  console.log(` Not found: ${luaSrc}`);
1710
1818
  }
1711
- const effectsDir = getReaperEffectsPath();
1819
+ const effectsDir = join4(getReaperEffectsPath(), "reaper-mcp");
1712
1820
  mkdirSync2(effectsDir, { recursive: true });
1713
1821
  console.log("\nInstalling JSFX analyzers...");
1714
1822
  for (const jsfx of REAPER_ASSETS) {
@@ -1716,7 +1824,7 @@ async function setup() {
1716
1824
  const src = join4(reaperDir, jsfx);
1717
1825
  const dest = join4(effectsDir, jsfx);
1718
1826
  if (installFile(src, dest)) {
1719
- console.log(` Installed: ${jsfx}`);
1827
+ console.log(` Installed: reaper-mcp/${jsfx}`);
1720
1828
  } else {
1721
1829
  console.log(` Not found: ${src}`);
1722
1830
  }
@@ -1729,70 +1837,66 @@ async function setup() {
1729
1837
  console.log(" 4. Run the script (it will keep running in background via defer loop)");
1730
1838
  console.log(" 5. Add reaper-mcp to your Claude Code config (see: npx @mthines/reaper-mcp doctor)");
1731
1839
  }
1732
- async function installSkills() {
1733
- console.log("REAPER MCP \u2014 Install AI Mix Engineer Skills\n");
1734
- const targetDir = process.cwd();
1735
- const globalClaudeDir = join4(homedir2(), ".claude");
1840
+ function parseInstallScope(args) {
1841
+ if (args.includes("--project")) return "project";
1842
+ return "global";
1843
+ }
1844
+ async function installSkills(scope) {
1845
+ console.log(`REAPER MCP \u2014 Install AI Mix Engineer Skills (scope: ${scope})
1846
+ `);
1847
+ const isGlobal = scope === "global";
1848
+ const baseDir = isGlobal ? join4(homedir2(), ".claude") : process.cwd();
1849
+ const claudeDir = isGlobal ? baseDir : join4(baseDir, ".claude");
1736
1850
  const knowledgeSrc = resolveAssetDir(__dirname, "knowledge");
1737
- const knowledgeDest = join4(targetDir, "knowledge");
1738
1851
  if (existsSync2(knowledgeSrc)) {
1739
- const count = copyDirSync(knowledgeSrc, knowledgeDest);
1740
- console.log(`Installed knowledge base: ${count} files \u2192 ${knowledgeDest}`);
1852
+ const dest = join4(baseDir, "knowledge");
1853
+ const count = copyDirSync(knowledgeSrc, dest);
1854
+ console.log(`Installed knowledge base: ${count} files \u2192 ${dest}`);
1741
1855
  } else {
1742
1856
  console.log("Knowledge base not found in package. Skipping.");
1743
1857
  }
1744
1858
  const rulesSrc = resolveAssetDir(__dirname, "claude-rules");
1745
- const rulesDir = join4(targetDir, ".claude", "rules");
1746
1859
  if (existsSync2(rulesSrc)) {
1747
- const count = copyDirSync(rulesSrc, rulesDir);
1748
- console.log(`Installed Claude rules: ${count} files \u2192 ${rulesDir}`);
1860
+ const dest = join4(claudeDir, "rules");
1861
+ const count = copyDirSync(rulesSrc, dest);
1862
+ console.log(`Installed Claude rules: ${count} files \u2192 ${dest}`);
1749
1863
  } else {
1750
1864
  console.log("Claude rules not found in package. Skipping.");
1751
1865
  }
1752
1866
  const skillsSrc = resolveAssetDir(__dirname, "claude-skills");
1753
- const skillsDir = join4(targetDir, ".claude", "skills");
1754
1867
  if (existsSync2(skillsSrc)) {
1755
- const count = copyDirSync(skillsSrc, skillsDir);
1756
- console.log(`Installed Claude skills: ${count} files \u2192 ${skillsDir}`);
1868
+ const dest = join4(claudeDir, "skills");
1869
+ const count = copyDirSync(skillsSrc, dest);
1870
+ console.log(`Installed Claude skills: ${count} files \u2192 ${dest}`);
1757
1871
  } else {
1758
1872
  console.log("Claude skills not found in package. Skipping.");
1759
1873
  }
1760
1874
  const agentsSrc = resolveAssetDir(__dirname, "claude-agents");
1761
- const agentsDir = join4(targetDir, ".claude", "agents");
1762
1875
  if (existsSync2(agentsSrc)) {
1763
- const count = copyDirSync(agentsSrc, agentsDir);
1764
- console.log(`Installed Claude agents: ${count} files \u2192 ${agentsDir}`);
1876
+ const dest = join4(claudeDir, "agents");
1877
+ const count = copyDirSync(agentsSrc, dest);
1878
+ console.log(`Installed Claude agents: ${count} files \u2192 ${dest}`);
1765
1879
  } else {
1766
1880
  console.log("Claude agents not found in package. Skipping.");
1767
1881
  }
1768
- const globalAgentsDir = join4(globalClaudeDir, "agents");
1769
- if (existsSync2(agentsSrc)) {
1770
- const count = copyDirSync(agentsSrc, globalAgentsDir);
1771
- console.log(`Installed Claude agents (global): ${count} files \u2192 ${globalAgentsDir}`);
1772
- }
1773
- const localSettingsPath = join4(targetDir, ".claude", "settings.json");
1774
- const localResult = ensureClaudeSettings(localSettingsPath);
1775
- if (localResult === "created") {
1776
- console.log(`Created Claude settings: ${localSettingsPath}`);
1777
- } else if (localResult === "updated") {
1778
- console.log(`Updated Claude settings with new REAPER tools: ${localSettingsPath}`);
1882
+ const settingsPath = join4(claudeDir, "settings.json");
1883
+ const result = ensureClaudeSettings(settingsPath);
1884
+ if (result === "created") {
1885
+ console.log(`Created Claude settings: ${settingsPath}`);
1886
+ } else if (result === "updated") {
1887
+ console.log(`Updated Claude settings with new REAPER tools: ${settingsPath}`);
1779
1888
  } else {
1780
- console.log(`Claude settings already has all REAPER tools: ${localSettingsPath}`);
1781
- }
1782
- const globalSettingsPath = join4(globalClaudeDir, "settings.json");
1783
- const globalResult = ensureClaudeSettings(globalSettingsPath);
1784
- if (globalResult === "created") {
1785
- console.log(`Created Claude settings (global): ${globalSettingsPath}`);
1786
- } else if (globalResult === "updated") {
1787
- console.log(`Updated Claude settings (global) with new REAPER tools: ${globalSettingsPath}`);
1889
+ console.log(`Claude settings already has all REAPER tools: ${settingsPath}`);
1788
1890
  }
1789
- const mcpJsonPath = join4(targetDir, ".mcp.json");
1790
- if (createMcpJson(mcpJsonPath)) {
1791
- console.log(`
1891
+ if (!isGlobal) {
1892
+ const mcpJsonPath = join4(baseDir, ".mcp.json");
1893
+ if (createMcpJson(mcpJsonPath)) {
1894
+ console.log(`
1792
1895
  Created: ${mcpJsonPath}`);
1793
- } else {
1794
- console.log(`
1896
+ } else {
1897
+ console.log(`
1795
1898
  .mcp.json already exists \u2014 add the reaper server config manually if needed.`);
1899
+ }
1796
1900
  }
1797
1901
  console.log("\nDone! Claude Code now has mix engineer agents, knowledge, and REAPER MCP tools.");
1798
1902
  console.log("All 48 REAPER tools are pre-approved \u2014 agents work autonomously.");
@@ -1806,20 +1910,27 @@ async function doctor() {
1806
1910
  if (!bridgeRunning) {
1807
1911
  console.log(' \u2192 Run "npx @mthines/reaper-mcp setup" then load mcp_bridge.lua in REAPER');
1808
1912
  }
1809
- const agentsExist = existsSync2(join4(process.cwd(), ".claude", "agents"));
1810
- console.log(`Mix agents: ${agentsExist ? "\u2713 Found (.claude/agents/)" : "\u2717 Not installed"}`);
1913
+ const globalClaudeDir = join4(homedir2(), ".claude");
1914
+ const localAgents = existsSync2(join4(process.cwd(), ".claude", "agents"));
1915
+ const globalAgents = existsSync2(join4(globalClaudeDir, "agents"));
1916
+ const agentsExist = localAgents || globalAgents;
1917
+ const agentsLocation = localAgents ? ".claude/agents/" : globalAgents ? "~/.claude/agents/" : "";
1918
+ console.log(`Mix agents: ${agentsExist ? `\u2713 Found (${agentsLocation})` : "\u2717 Not installed"}`);
1811
1919
  if (!agentsExist) {
1812
- console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills" in your project directory');
1920
+ console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
1813
1921
  }
1814
- const knowledgeExists = existsSync2(join4(process.cwd(), "knowledge"));
1815
- console.log(`Knowledge base: ${knowledgeExists ? "\u2713 Found in project" : "\u2717 Not installed"}`);
1922
+ const localKnowledge = existsSync2(join4(process.cwd(), "knowledge"));
1923
+ const globalKnowledge = existsSync2(join4(globalClaudeDir, "knowledge"));
1924
+ const knowledgeExists = localKnowledge || globalKnowledge;
1925
+ const knowledgeLocation = localKnowledge ? "project" : globalKnowledge ? "~/.claude/" : "";
1926
+ console.log(`Knowledge base: ${knowledgeExists ? `\u2713 Found (${knowledgeLocation})` : "\u2717 Not installed"}`);
1816
1927
  if (!knowledgeExists) {
1817
- console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills" in your project directory');
1928
+ console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
1818
1929
  }
1819
1930
  const mcpJsonExists = existsSync2(join4(process.cwd(), ".mcp.json"));
1820
1931
  console.log(`MCP config: ${mcpJsonExists ? "\u2713 .mcp.json found" : "\u2717 .mcp.json missing"}`);
1821
1932
  if (!mcpJsonExists) {
1822
- console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills" to create .mcp.json');
1933
+ console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills --project" to create .mcp.json');
1823
1934
  }
1824
1935
  console.log('\nTo check SWS Extensions, start REAPER and use the "list_available_fx" tool.');
1825
1936
  console.log("SWS provides enhanced plugin discovery and snapshot support.\n");
@@ -1884,7 +1995,7 @@ switch (command) {
1884
1995
  });
1885
1996
  break;
1886
1997
  case "install-skills":
1887
- installSkills().catch((err) => {
1998
+ installSkills(parseInstallScope(process.argv.slice(3))).catch((err) => {
1888
1999
  console.error("Install failed:", err);
1889
2000
  process.exit(1);
1890
2001
  });
@@ -1917,14 +2028,16 @@ Usage:
1917
2028
  npx @mthines/reaper-mcp Start MCP server (stdio mode)
1918
2029
  npx @mthines/reaper-mcp serve Start MCP server (stdio mode)
1919
2030
  npx @mthines/reaper-mcp setup Install Lua bridge + JSFX analyzers into REAPER
1920
- npx @mthines/reaper-mcp install-skills Install AI mix engineer knowledge + agents into your project
2031
+ npx @mthines/reaper-mcp install-skills Install AI knowledge + agents globally (default)
2032
+ npx @mthines/reaper-mcp install-skills --project Install into current project directory
2033
+ npx @mthines/reaper-mcp install-skills --global Install into ~/.claude/ (default)
1921
2034
  npx @mthines/reaper-mcp doctor Check that everything is configured correctly
1922
2035
  npx @mthines/reaper-mcp status Check if Lua bridge is running in REAPER
1923
2036
 
1924
2037
  Quick Start:
1925
2038
  1. npx @mthines/reaper-mcp setup # install REAPER components
1926
2039
  2. Load mcp_bridge.lua in REAPER (Actions > Load ReaScript > Run)
1927
- 3. npx @mthines/reaper-mcp install-skills # install AI knowledge + agents
2040
+ 3. npx @mthines/reaper-mcp install-skills # install AI knowledge + agents (globally)
1928
2041
  4. Open Claude Code \u2014 REAPER tools + mix engineer agents are ready
1929
2042
 
1930
2043
  Tip: install globally for shorter commands:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthines/reaper-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "MCP server for controlling REAPER DAW — real-time mixing, FX control, and frequency analysis for AI agents",
6
6
  "license": "MIT",
@@ -0,0 +1,60 @@
1
+ # REAPER Scripts
2
+
3
+ Files installed INTO the REAPER DAW by the `setup` command. These run inside REAPER's scripting environment.
4
+
5
+ ## Files
6
+
7
+ | File | Language | Purpose |
8
+ |------|----------|---------|
9
+ | `mcp_bridge.lua` | Lua | Persistent bridge: polls for JSON commands, executes ReaScript API, writes responses |
10
+ | `mcp_analyzer.jsfx` | JSFX/EEL2 | Real-time FFT spectrum analyzer, writes to gmem[] |
11
+ | `mcp_lufs_meter.jsfx` | JSFX/EEL2 | LUFS loudness metering |
12
+ | `mcp_correlation_meter.jsfx` | JSFX/EEL2 | Stereo correlation and width analysis |
13
+ | `mcp_crest_factor.jsfx` | JSFX/EEL2 | Crest factor (peak-to-RMS) measurement |
14
+ | `install.sh` | Shell | Manual install helper |
15
+
16
+ ## Lua Bridge (`mcp_bridge.lua`)
17
+
18
+ ### How It Works
19
+ 1. Runs as a persistent `reaper.defer()` loop (polls every ~30ms)
20
+ 2. Reads `command_{uuid}.json` from bridge directory
21
+ 3. Dispatches to handler function in the `handlers` table
22
+ 4. Writes `response_{uuid}.json` with results
23
+ 5. Writes `heartbeat.json` every 1s for liveness detection
24
+
25
+ ### Adding a Handler
26
+ ```lua
27
+ handlers["command_type"] = function(params)
28
+ local result = reaper.SomeApiCall(params.paramName)
29
+ return { field = result }
30
+ end
31
+ ```
32
+
33
+ The command type string must exactly match `CommandType` in `libs/protocol/src/commands.ts`.
34
+
35
+ ### Key Constraints
36
+ - REAPER Lua is sandboxed: **no sockets, no HTTP, no stdin/stdout** — file-based IPC only
37
+ - JSON parsing: uses `CF_Json_Parse` if available (REAPER 7+), falls back to custom Lua parser
38
+ - Track indices are 0-based (same as ReaScript)
39
+ - Volume values: bridge converts between dB (MCP protocol) and linear (ReaScript internally)
40
+ - Always wrap file reads in `pcall` for resilience
41
+
42
+ ## JSFX Meters
43
+
44
+ - Run in REAPER's **audio thread** (not scripting thread)
45
+ - Communicate with Lua via `gmem[]` shared memory
46
+ - Each JSFX uses a unique gmem namespace (e.g., `MCPAnalyzer`, `MCPLufsMeter`)
47
+ - Must pass audio through unmodified (transparent inserts)
48
+ - Auto-inserted by corresponding MCP tools (`read_track_spectrum`, `read_track_lufs`, etc.)
49
+
50
+ ## Testing
51
+
52
+ - **No automated tests possible** — REAPER's Lua/JSFX environment cannot be unit tested outside REAPER
53
+ - Test manually: install bridge, run MCP Inspector, exercise commands
54
+ - Server-side tests mock `sendCommand()` in `bridge.ts`
55
+
56
+ ## Installation
57
+
58
+ Files are copied to `{REAPER_RESOURCE_PATH}/Scripts/` by:
59
+ - `node dist/apps/reaper-mcp-server/main.js setup` (programmatic)
60
+ - `install.sh` (manual)
package/reaper/install.sh CHANGED
@@ -29,11 +29,15 @@ mkdir -p "$SCRIPTS_DIR"
29
29
  cp "$SCRIPT_DIR/mcp_bridge.lua" "$SCRIPTS_DIR/mcp_bridge.lua"
30
30
  echo "Installed: $SCRIPTS_DIR/mcp_bridge.lua"
31
31
 
32
- # Install JSFX analyzer
33
- EFFECTS_DIR="$REAPER_PATH/Effects"
32
+ # Install JSFX analyzers
33
+ EFFECTS_DIR="$REAPER_PATH/Effects/reaper-mcp"
34
34
  mkdir -p "$EFFECTS_DIR"
35
- cp "$SCRIPT_DIR/mcp_analyzer.jsfx" "$EFFECTS_DIR/mcp_analyzer.jsfx"
36
- echo "Installed: $EFFECTS_DIR/mcp_analyzer.jsfx"
35
+ for jsfx in "$SCRIPT_DIR"/*.jsfx; do
36
+ [ -f "$jsfx" ] || continue
37
+ fname="$(basename "$jsfx")"
38
+ cp "$jsfx" "$EFFECTS_DIR/$fname"
39
+ echo "Installed: $EFFECTS_DIR/$fname"
40
+ done
37
41
 
38
42
  # Create bridge data directory
39
43
  BRIDGE_DIR="$SCRIPTS_DIR/mcp_bridge_data"
@@ -11,7 +11,7 @@
11
11
 
12
12
  local POLL_INTERVAL = 0.030 -- 30ms between polls
13
13
  local HEARTBEAT_INTERVAL = 1.0 -- write heartbeat every 1s
14
- local MCP_ANALYZER_FX_NAME = "mcp_analyzer" -- JSFX analyzer name
14
+ local MCP_ANALYZER_FX_NAME = "reaper-mcp/mcp_analyzer" -- JSFX analyzer name
15
15
 
16
16
  -- Determine bridge directory (REAPER resource path / Scripts / mcp_bridge_data)
17
17
  local bridge_dir = reaper.GetResourcePath() .. "/Scripts/mcp_bridge_data/"
@@ -1007,9 +1007,9 @@ end
1007
1007
  -- Phase 4: Custom JSFX analyzer handlers
1008
1008
  -- =============================================================================
1009
1009
 
1010
- local MCP_LUFS_METER_FX_NAME = "mcp_lufs_meter"
1011
- local MCP_CORRELATION_METER_FX_NAME = "mcp_correlation_meter"
1012
- local MCP_CREST_FACTOR_FX_NAME = "mcp_crest_factor"
1010
+ local MCP_LUFS_METER_FX_NAME = "reaper-mcp/mcp_lufs_meter"
1011
+ local MCP_CORRELATION_METER_FX_NAME = "reaper-mcp/mcp_correlation_meter"
1012
+ local MCP_CREST_FACTOR_FX_NAME = "reaper-mcp/mcp_crest_factor"
1013
1013
 
1014
1014
  -- Helper: find or auto-insert a named JSFX on a track.
1015
1015
  -- Returns the FX index (0-based) on success, or nil + error message on failure.
@@ -2396,6 +2396,234 @@ function handlers.delete_envelope_point(params)
2396
2396
  return { success = true, totalPoints = reaper.CountEnvelopePoints(env) }
2397
2397
  end
2398
2398
 
2399
+ function handlers.create_track_envelope(params)
2400
+ local track = reaper.GetTrack(0, params.trackIndex)
2401
+ if not track then return nil, "Track " .. params.trackIndex .. " not found" end
2402
+
2403
+ local env = nil
2404
+ local env_name = nil
2405
+
2406
+ if params.fxIndex ~= nil then
2407
+ -- FX parameter envelope
2408
+ if params.paramIndex == nil then
2409
+ return nil, "paramIndex is required when fxIndex is provided"
2410
+ end
2411
+ local fx_count = reaper.TrackFX_GetNumParms(track)
2412
+ if fx_count == 0 then
2413
+ return nil, "Track " .. params.trackIndex .. " has no FX"
2414
+ end
2415
+ -- create=true to create the envelope if it doesn't exist
2416
+ env = reaper.GetFXEnvelope(track, params.fxIndex, params.paramIndex, true)
2417
+ if not env then
2418
+ return nil, "Failed to create FX parameter envelope (fxIndex=" .. params.fxIndex .. ", paramIndex=" .. params.paramIndex .. ")"
2419
+ end
2420
+ local _, pname = reaper.TrackFX_GetParamName(track, params.fxIndex, params.paramIndex)
2421
+ local _, fname = reaper.TrackFX_GetFXName(track, params.fxIndex)
2422
+ env_name = fname .. " / " .. pname
2423
+ elseif params.envelopeName then
2424
+ -- Built-in envelope: use GetTrackEnvelopeByName + show via state chunk
2425
+ env = reaper.GetTrackEnvelopeByName(track, params.envelopeName)
2426
+ if not env then
2427
+ -- Some envelopes need to be activated first via the track state chunk
2428
+ -- Toggle the envelope visibility via action or chunk editing
2429
+ local env_map = {
2430
+ ["Volume"] = "VOLENV",
2431
+ ["Pan"] = "PANENV",
2432
+ ["Mute"] = "MUTEENV",
2433
+ ["Width"] = "WIDTHENV",
2434
+ ["Trim Volume"] = "VOLENV2",
2435
+ }
2436
+ local chunk_key = env_map[params.envelopeName]
2437
+ if not chunk_key then
2438
+ return nil, "Unknown envelope name: " .. params.envelopeName .. ". Use Volume, Pan, Mute, Width, or Trim Volume"
2439
+ end
2440
+ -- Get track chunk, insert envelope chunk if missing
2441
+ local _, chunk = reaper.GetTrackStateChunk(track, "", false)
2442
+ if not chunk:find(chunk_key) then
2443
+ -- Insert a minimal envelope chunk before the closing >
2444
+ local env_chunk = "\n<" .. chunk_key .. "\nACT 1 -1\nVIS 1 1 1\nLANEHEIGHT 0 0\nARM 0\nDEFSHAPE 0 -1 -1\n>\n"
2445
+ chunk = chunk:gsub("\n>$", env_chunk .. ">")
2446
+ reaper.SetTrackStateChunk(track, chunk, false)
2447
+ else
2448
+ -- Envelope exists in chunk but may be hidden; make it visible
2449
+ chunk = chunk:gsub("(" .. chunk_key .. "[^\n]*\n)ACT 0", "%1ACT 1")
2450
+ chunk = chunk:gsub("(" .. chunk_key .. "[^\n]*\n[^\n]*\n)VIS 0", "%1VIS 1")
2451
+ reaper.SetTrackStateChunk(track, chunk, false)
2452
+ end
2453
+ env = reaper.GetTrackEnvelopeByName(track, params.envelopeName)
2454
+ if not env then
2455
+ return nil, "Failed to create envelope: " .. params.envelopeName
2456
+ end
2457
+ else
2458
+ -- Envelope exists, ensure it's visible and active
2459
+ local _, chunk = reaper.GetEnvelopeStateChunk(env, "", false)
2460
+ chunk = chunk:gsub("ACT 0", "ACT 1")
2461
+ chunk = chunk:gsub("VIS 0", "VIS 1")
2462
+ reaper.SetEnvelopeStateChunk(env, chunk, false)
2463
+ end
2464
+ env_name = params.envelopeName
2465
+ else
2466
+ return nil, "Must provide either envelopeName or fxIndex+paramIndex"
2467
+ end
2468
+
2469
+ -- Find the index of the newly created envelope
2470
+ local env_count = reaper.CountTrackEnvelopes(track)
2471
+ local env_index = -1
2472
+ for i = 0, env_count - 1 do
2473
+ if reaper.GetTrackEnvelope(track, i) == env then
2474
+ env_index = i
2475
+ break
2476
+ end
2477
+ end
2478
+
2479
+ reaper.TrackList_AdjustWindows(false)
2480
+ reaper.UpdateArrange()
2481
+
2482
+ return {
2483
+ trackIndex = params.trackIndex,
2484
+ envelopeIndex = env_index,
2485
+ name = env_name,
2486
+ pointCount = reaper.CountEnvelopePoints(env),
2487
+ }
2488
+ end
2489
+
2490
+ function handlers.set_envelope_properties(params)
2491
+ local track = reaper.GetTrack(0, params.trackIndex)
2492
+ if not track then return nil, "Track " .. params.trackIndex .. " not found" end
2493
+ local env_count = reaper.CountTrackEnvelopes(track)
2494
+ if params.envelopeIndex >= env_count then
2495
+ return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
2496
+ end
2497
+ local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
2498
+
2499
+ if reaper.BR_EnvAlloc then
2500
+ -- SWS extension available: use BR_Env* functions
2501
+ local br_env = reaper.BR_EnvAlloc(env, false)
2502
+ local cur_active, cur_visible, cur_armed = reaper.BR_EnvGetProperties(br_env)
2503
+ local new_active = params.active ~= nil and params.active or cur_active
2504
+ local new_visible = params.visible ~= nil and params.visible or cur_visible
2505
+ local new_armed = params.armed ~= nil and params.armed or cur_armed
2506
+ reaper.BR_EnvSetProperties(br_env, new_active, new_visible, new_armed, false)
2507
+ reaper.BR_EnvFree(br_env, true) -- commit=true
2508
+ else
2509
+ -- Fallback: modify envelope state chunk directly
2510
+ local _, chunk = reaper.GetEnvelopeStateChunk(env, "", false)
2511
+ if params.active ~= nil then
2512
+ local val = params.active and "1" or "0"
2513
+ chunk = chunk:gsub("ACT %d", "ACT " .. val)
2514
+ end
2515
+ if params.visible ~= nil then
2516
+ local val = params.visible and "1" or "0"
2517
+ chunk = chunk:gsub("VIS %d", "VIS " .. val)
2518
+ end
2519
+ if params.armed ~= nil then
2520
+ local val = params.armed and "1" or "0"
2521
+ chunk = chunk:gsub("ARM %d", "ARM " .. val)
2522
+ end
2523
+ reaper.SetEnvelopeStateChunk(env, chunk, false)
2524
+ end
2525
+
2526
+ reaper.TrackList_AdjustWindows(false)
2527
+ reaper.UpdateArrange()
2528
+
2529
+ local _, name = reaper.GetEnvelopeName(env)
2530
+ -- Re-read properties
2531
+ local active, visible, armed = true, true, false
2532
+ if reaper.BR_EnvAlloc then
2533
+ local br_env = reaper.BR_EnvAlloc(env, false)
2534
+ active, visible, armed = reaper.BR_EnvGetProperties(br_env)
2535
+ reaper.BR_EnvFree(br_env, false)
2536
+ end
2537
+ return {
2538
+ trackIndex = params.trackIndex,
2539
+ envelopeIndex = params.envelopeIndex,
2540
+ name = name,
2541
+ active = active,
2542
+ visible = visible,
2543
+ armed = armed,
2544
+ }
2545
+ end
2546
+
2547
+ function handlers.clear_envelope(params)
2548
+ local track = reaper.GetTrack(0, params.trackIndex)
2549
+ if not track then return nil, "Track " .. params.trackIndex .. " not found" end
2550
+ local env_count = reaper.CountTrackEnvelopes(track)
2551
+ if params.envelopeIndex >= env_count then
2552
+ return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
2553
+ end
2554
+ local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
2555
+ local prev_count = reaper.CountEnvelopePoints(env)
2556
+ -- Delete all points by using a range from -infinity to +infinity
2557
+ reaper.DeleteEnvelopePointRange(env, -math.huge, math.huge)
2558
+ reaper.Envelope_SortPoints(env)
2559
+ return {
2560
+ trackIndex = params.trackIndex,
2561
+ envelopeIndex = params.envelopeIndex,
2562
+ deletedPoints = prev_count,
2563
+ totalPoints = reaper.CountEnvelopePoints(env),
2564
+ }
2565
+ end
2566
+
2567
+ function handlers.remove_envelope_points(params)
2568
+ local track = reaper.GetTrack(0, params.trackIndex)
2569
+ if not track then return nil, "Track " .. params.trackIndex .. " not found" end
2570
+ local env_count = reaper.CountTrackEnvelopes(track)
2571
+ if params.envelopeIndex >= env_count then
2572
+ return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
2573
+ end
2574
+ local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
2575
+ local prev_count = reaper.CountEnvelopePoints(env)
2576
+ reaper.DeleteEnvelopePointRange(env, params.timeStart, params.timeEnd)
2577
+ reaper.Envelope_SortPoints(env)
2578
+ local new_count = reaper.CountEnvelopePoints(env)
2579
+ return {
2580
+ trackIndex = params.trackIndex,
2581
+ envelopeIndex = params.envelopeIndex,
2582
+ timeStart = params.timeStart,
2583
+ timeEnd = params.timeEnd,
2584
+ deletedPoints = prev_count - new_count,
2585
+ totalPoints = new_count,
2586
+ }
2587
+ end
2588
+
2589
+ function handlers.insert_envelope_points(params)
2590
+ local track = reaper.GetTrack(0, params.trackIndex)
2591
+ if not track then return nil, "Track " .. params.trackIndex .. " not found" end
2592
+ local env_count = reaper.CountTrackEnvelopes(track)
2593
+ if params.envelopeIndex >= env_count then
2594
+ return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
2595
+ end
2596
+ local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
2597
+
2598
+ -- Parse the points JSON string
2599
+ local points_str = params.points
2600
+ local points = nil
2601
+ if type(points_str) == "string" then
2602
+ points = json_decode(points_str)
2603
+ elseif type(points_str) == "table" then
2604
+ points = points_str
2605
+ end
2606
+ if not points then return nil, "Failed to parse points JSON" end
2607
+
2608
+ local inserted = 0
2609
+ for _, pt in ipairs(points) do
2610
+ if pt.time and pt.value then
2611
+ local shape = pt.shape or 0
2612
+ local tension = pt.tension or 0
2613
+ reaper.InsertEnvelopePoint(env, pt.time, pt.value, shape, tension, false, true)
2614
+ inserted = inserted + 1
2615
+ end
2616
+ end
2617
+
2618
+ reaper.Envelope_SortPoints(env)
2619
+ return {
2620
+ trackIndex = params.trackIndex,
2621
+ envelopeIndex = params.envelopeIndex,
2622
+ insertedPoints = inserted,
2623
+ totalPoints = reaper.CountEnvelopePoints(env),
2624
+ }
2625
+ end
2626
+
2399
2627
  -- =============================================================================
2400
2628
  -- Command dispatcher
2401
2629
  -- =============================================================================