@williamzujkowski/live-coding-music-mcp 2.0.0 → 4.0.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.
@@ -1,58 +1,46 @@
1
1
  /**
2
2
  * history domain — undo/redo stacks and the pattern history browser.
3
3
  *
4
- * Owns (5 tools): undo, redo, list_history, restore_history,
5
- * compare_patterns. Post-consolidation target per the #110 audit:
6
- * single `history` tool with an `action` enum. For now we keep the
7
- * individual verbs.
4
+ * Owns one consolidated tool (`history(action)`) with undo/redo/list/
5
+ * restore/compare actions.
8
6
  *
9
7
  * The history module mutates the three state arrays (undoStack,
10
- * redoStack, historyStack) passed through ctx.history. The outer
11
- * server pushes onto these arrays on write/append/insert/replace/clear,
12
- * so all mutations share the same underlying storage.
8
+ * redoStack, historyStack) reached through ctx.getHistory(). The outer
9
+ * server pushes onto these arrays on every edit_pattern call, so all
10
+ * mutations share the same underlying storage.
13
11
  */
14
- export const tools = [
15
- {
16
- name: 'undo',
17
- description: 'Undo last action',
18
- inputSchema: { type: 'object', properties: {} },
19
- },
20
- {
21
- name: 'redo',
22
- description: 'Redo action',
23
- inputSchema: { type: 'object', properties: {} },
24
- },
25
- {
26
- name: 'list_history',
27
- description: 'List recent pattern history with timestamps and previews',
28
- inputSchema: {
29
- type: 'object',
30
- properties: {
31
- limit: { type: 'number', description: 'Maximum entries to return (default: 10)' },
32
- },
33
- },
34
- },
35
- {
36
- name: 'restore_history',
37
- description: 'Restore a previous pattern from history by ID',
38
- inputSchema: {
39
- type: 'object',
40
- properties: {
41
- id: { type: 'number', description: 'History entry ID to restore' },
42
- },
43
- required: ['id'],
44
- },
12
+ const SESSION_ID_PROP = {
13
+ session_id: {
14
+ type: 'string',
15
+ description: 'Optional session ID (#108). Omit to use default session. Note: undo/redo/history stacks are currently server-wide; only the read/write target changes.',
45
16
  },
17
+ };
18
+ export const tools = [
46
19
  {
47
- name: 'compare_patterns',
48
- description: 'Compare two patterns from history showing differences',
20
+ name: 'history',
21
+ description: 'Navigate or inspect the pattern edit history. ' +
22
+ 'action=undo reverts the last edit on the targeted session. ' +
23
+ 'action=redo replays a previously-undone edit. ' +
24
+ 'action=list returns recent entries with timestamps and previews (limit defaults to 10). ' +
25
+ 'action=restore jumps the editor to a specific entry by id (current pattern goes on the undo stack). ' +
26
+ 'action=compare diffs two entries by id (or one entry vs current pattern). ' +
27
+ 'Example: history({ action: "list", limit: 5 }) — recent edits, newest first. ' +
28
+ 'For on-disk saved patterns use pattern_store — history deals with the in-memory edit timeline of the current session.',
49
29
  inputSchema: {
50
30
  type: 'object',
51
31
  properties: {
52
- id1: { type: 'number', description: 'First pattern ID' },
53
- id2: { type: 'number', description: 'Second pattern ID (default: current pattern)' },
32
+ action: {
33
+ type: 'string',
34
+ enum: ['undo', 'redo', 'list', 'restore', 'compare'],
35
+ description: 'Which history operation to perform',
36
+ },
37
+ limit: { type: 'number', description: 'Maximum entries (action=list, default 10)' },
38
+ id: { type: 'number', description: 'History entry ID (action=restore)' },
39
+ id1: { type: 'number', description: 'First entry ID (action=compare)' },
40
+ id2: { type: 'number', description: 'Second entry ID (action=compare, default: current pattern)' },
41
+ ...SESSION_ID_PROP,
54
42
  },
55
- required: ['id1'],
43
+ required: ['action'],
56
44
  },
57
45
  },
58
46
  ];
@@ -104,90 +92,110 @@ function summarizeDiff(a, b) {
104
92
  }
105
93
  return { linesAdded: added, linesRemoved: removed, linesChanged: changed, charsDiff: b.length - a.length };
106
94
  }
95
+ async function doUndo(ctx, sid) {
96
+ const { undoStack, redoStack, maxHistory } = ctx.getHistory(sid);
97
+ if (!sid && !ctx.isInitialized())
98
+ return 'Browser not initialized. Run init first.';
99
+ if (undoStack.length === 0)
100
+ return 'Nothing to undo';
101
+ const controller = ctx.getController(sid);
102
+ const current = await controller.getCurrentPattern();
103
+ redoStack.push(current);
104
+ if (redoStack.length > maxHistory)
105
+ redoStack.shift();
106
+ const previous = undoStack.pop();
107
+ await controller.writePattern(previous);
108
+ return 'Undone';
109
+ }
110
+ async function doRedo(ctx, sid) {
111
+ const { undoStack, redoStack, maxHistory } = ctx.getHistory(sid);
112
+ if (!sid && !ctx.isInitialized())
113
+ return 'Browser not initialized. Run init first.';
114
+ if (redoStack.length === 0)
115
+ return 'Nothing to redo';
116
+ const controller = ctx.getController(sid);
117
+ const current = await controller.getCurrentPattern();
118
+ undoStack.push(current);
119
+ if (undoStack.length > maxHistory)
120
+ undoStack.shift();
121
+ const next = redoStack.pop();
122
+ await controller.writePattern(next);
123
+ return 'Redone';
124
+ }
125
+ function doList(args, ctx, sid) {
126
+ const { historyStack } = ctx.getHistory(sid);
127
+ if (historyStack.length === 0) {
128
+ return 'No pattern history yet. Make some edits to build history.';
129
+ }
130
+ const limit = args?.limit || 10;
131
+ const recent = historyStack.slice(-limit).reverse();
132
+ return {
133
+ count: historyStack.length,
134
+ showing: recent.length,
135
+ entries: recent.map(e => ({
136
+ id: e.id,
137
+ preview: e.pattern.substring(0, 60) + (e.pattern.length > 60 ? '...' : ''),
138
+ chars: e.pattern.length,
139
+ action: e.action,
140
+ timestamp: formatTimeAgo(e.timestamp),
141
+ })),
142
+ };
143
+ }
144
+ async function doRestore(args, ctx, sid) {
145
+ const { undoStack, historyStack, maxHistory } = ctx.getHistory(sid);
146
+ if (!sid && !ctx.isInitialized())
147
+ return 'Browser not initialized. Run init first.';
148
+ const controller = ctx.getController(sid);
149
+ const entry = historyStack.find(e => e.id === args.id);
150
+ if (!entry) {
151
+ return `History entry #${args.id} not found. Use history({ action: "list" }) to see available entries.`;
152
+ }
153
+ const current = await controller.getCurrentPattern();
154
+ undoStack.push(current);
155
+ if (undoStack.length > maxHistory)
156
+ undoStack.shift();
157
+ await controller.writePattern(entry.pattern);
158
+ return `Restored pattern from history #${args.id} (${formatTimeAgo(entry.timestamp)})`;
159
+ }
160
+ async function doCompare(args, ctx, sid) {
161
+ const { historyStack } = ctx.getHistory(sid);
162
+ const first = historyStack.find(e => e.id === args.id1);
163
+ if (!first)
164
+ return `History entry #${args.id1} not found.`;
165
+ let second;
166
+ let label;
167
+ if (args.id2) {
168
+ const b = historyStack.find(e => e.id === args.id2);
169
+ if (!b)
170
+ return `History entry #${args.id2} not found.`;
171
+ second = b.pattern;
172
+ label = `#${args.id2}`;
173
+ }
174
+ else {
175
+ second = await ctx.getCurrentPatternSafe(sid);
176
+ label = 'current';
177
+ }
178
+ return {
179
+ pattern1: { id: args.id1, chars: first.pattern.length },
180
+ pattern2: { id: label, chars: second.length },
181
+ diff: generateDiff(first.pattern, second),
182
+ summary: summarizeDiff(first.pattern, second),
183
+ };
184
+ }
107
185
  export async function execute(name, args, ctx) {
108
- const { undoStack, redoStack, historyStack, maxHistory } = ctx.history;
186
+ const sid = args?.session_id;
109
187
  switch (name) {
110
- case 'undo': {
111
- if (!ctx.isInitialized())
112
- return 'Browser not initialized. Run init first.';
113
- if (undoStack.length === 0)
114
- return 'Nothing to undo';
115
- const current = await ctx.controller.getCurrentPattern();
116
- redoStack.push(current);
117
- if (redoStack.length > maxHistory)
118
- redoStack.shift();
119
- const previous = undoStack.pop();
120
- await ctx.controller.writePattern(previous);
121
- return 'Undone';
122
- }
123
- case 'redo': {
124
- if (!ctx.isInitialized())
125
- return 'Browser not initialized. Run init first.';
126
- if (redoStack.length === 0)
127
- return 'Nothing to redo';
128
- const current = await ctx.controller.getCurrentPattern();
129
- undoStack.push(current);
130
- if (undoStack.length > maxHistory)
131
- undoStack.shift();
132
- const next = redoStack.pop();
133
- await ctx.controller.writePattern(next);
134
- return 'Redone';
135
- }
136
- case 'list_history': {
137
- if (historyStack.length === 0) {
138
- return 'No pattern history yet. Make some edits to build history.';
139
- }
140
- const limit = args?.limit || 10;
141
- const recent = historyStack.slice(-limit).reverse();
142
- return {
143
- count: historyStack.length,
144
- showing: recent.length,
145
- entries: recent.map(e => ({
146
- id: e.id,
147
- preview: e.pattern.substring(0, 60) + (e.pattern.length > 60 ? '...' : ''),
148
- chars: e.pattern.length,
149
- action: e.action,
150
- timestamp: formatTimeAgo(e.timestamp),
151
- })),
152
- };
153
- }
154
- case 'restore_history': {
155
- if (!ctx.isInitialized())
156
- return 'Browser not initialized. Run init first.';
157
- const entry = historyStack.find(e => e.id === args.id);
158
- if (!entry) {
159
- return `History entry #${args.id} not found. Use list_history to see available entries.`;
160
- }
161
- const current = await ctx.controller.getCurrentPattern();
162
- undoStack.push(current);
163
- if (undoStack.length > maxHistory)
164
- undoStack.shift();
165
- await ctx.controller.writePattern(entry.pattern);
166
- return `Restored pattern from history #${args.id} (${formatTimeAgo(entry.timestamp)})`;
167
- }
168
- case 'compare_patterns': {
169
- const first = historyStack.find(e => e.id === args.id1);
170
- if (!first)
171
- return `History entry #${args.id1} not found.`;
172
- let second;
173
- let label;
174
- if (args.id2) {
175
- const b = historyStack.find(e => e.id === args.id2);
176
- if (!b)
177
- return `History entry #${args.id2} not found.`;
178
- second = b.pattern;
179
- label = `#${args.id2}`;
180
- }
181
- else {
182
- second = await ctx.getCurrentPatternSafe();
183
- label = 'current';
188
+ case 'history': {
189
+ const action = args?.action;
190
+ switch (action) {
191
+ case 'undo': return await doUndo(ctx, sid);
192
+ case 'redo': return await doRedo(ctx, sid);
193
+ case 'list': return doList(args, ctx, sid);
194
+ case 'restore': return await doRestore(args, ctx, sid);
195
+ case 'compare': return await doCompare(args, ctx, sid);
196
+ default:
197
+ throw new Error(`Invalid action: ${action}. Must be one of: undo, redo, list, restore, compare`);
184
198
  }
185
- return {
186
- pattern1: { id: args.id1, chars: first.pattern.length },
187
- pattern2: { id: label, chars: second.length },
188
- diff: generateDiff(first.pattern, second),
189
- summary: summarizeDiff(first.pattern, second),
190
- };
191
199
  }
192
200
  default:
193
201
  throw new Error(`history module does not handle tool: ${name}`);
@@ -1,41 +1,56 @@
1
1
  /**
2
- * playback domain — transport controls (play, pause, stop).
2
+ * playback domain — transport controls.
3
3
  *
4
- * Owns (3 tools): play, pause, stop. Post-consolidation target
5
- * (#110 audit, #120 epic): single `playback` tool with action enum.
6
- * For now we keep the individual verbs and only extract them out of
7
- * server.ts.
8
- *
9
- * Initialization checks happen upstream via requiresInitialization()
10
- * in StrudelMCPServer — handlers here assume the browser is ready.
4
+ * Owns one consolidated tool (`playback(action)`) with play/pause/stop.
11
5
  */
12
- export const tools = [
13
- {
14
- name: 'play',
15
- description: 'Start playing pattern',
16
- inputSchema: { type: 'object', properties: {} },
17
- },
18
- {
19
- name: 'pause',
20
- description: 'Pause playback',
21
- inputSchema: { type: 'object', properties: {} },
6
+ const SESSION_ID_PROP = {
7
+ session_id: {
8
+ type: 'string',
9
+ description: 'Optional session ID (#108). Omit to use default session.',
22
10
  },
11
+ };
12
+ export const tools = [
23
13
  {
24
- name: 'stop',
25
- description: 'Stop playback',
26
- inputSchema: { type: 'object', properties: {} },
14
+ name: 'playback',
15
+ description: 'Control transport on the current session. ' +
16
+ 'action=play starts the editor pattern. ' +
17
+ 'action=pause stops without resetting clock. ' +
18
+ 'action=stop ends playback. ' +
19
+ 'Example: playback({ action: "play" }). ' +
20
+ 'For pattern editing use edit_pattern; for tempo use set_tempo.',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ action: {
25
+ type: 'string',
26
+ enum: ['play', 'pause', 'stop'],
27
+ description: 'Transport action',
28
+ },
29
+ ...SESSION_ID_PROP,
30
+ },
31
+ required: ['action'],
32
+ },
27
33
  },
28
34
  ];
29
35
  export const toolNames = new Set(tools.map(t => t.name));
30
- export async function execute(name, _args, ctx) {
31
- switch (name) {
36
+ export async function execute(name, args, ctx) {
37
+ const sid = args?.session_id;
38
+ if (!sid && !ctx.isInitialized()) {
39
+ return 'Browser not initialized. Run init first.';
40
+ }
41
+ const controller = ctx.getController(sid);
42
+ if (name !== 'playback') {
43
+ throw new Error(`playback module does not handle tool: ${name}`);
44
+ }
45
+ const action = args?.action;
46
+ switch (action) {
32
47
  case 'play':
33
- return await ctx.controller.play();
48
+ return await controller.play();
34
49
  case 'pause':
35
50
  case 'stop':
36
- return await ctx.controller.stop();
51
+ return await controller.stop();
37
52
  default:
38
- throw new Error(`playback module does not handle tool: ${name}`);
53
+ throw new Error(`Invalid action: ${action}. Must be one of: play, pause, stop`);
39
54
  }
40
55
  }
41
56
  export const playbackModule = { tools, toolNames, execute };
@@ -1,111 +1,111 @@
1
1
  /**
2
2
  * session domain — multi-session lifecycle management.
3
3
  *
4
- * Owns (4 tools): create_session, destroy_session, list_sessions,
5
- * switch_session. Per the #110 audit, these collapse into a single
6
- * `session` tool with an `action` enum post-split. `init` stays
7
- * separate — it's the global bootstrap, not a session operation.
8
- *
9
- * `show_browser` also stays in server.ts for now until it moves into
10
- * a `browser_window` module (per audit) alongside `screenshot`.
4
+ * Owns one consolidated tool (`session(action)`) with create/destroy/
5
+ * list/switch actions. `init` stays separate — it's the global
6
+ * bootstrap, not a session operation.
11
7
  */
12
8
  import { InputValidator } from '../../utils/InputValidator.js';
13
9
  export const tools = [
14
10
  {
15
- name: 'create_session',
16
- description: 'Create a new isolated Strudel browser session. Sessions share one browser but have isolated contexts.',
17
- inputSchema: {
18
- type: 'object',
19
- properties: { session_id: { type: 'string', description: 'Unique identifier for the session' } },
20
- required: ['session_id'],
21
- },
22
- },
23
- {
24
- name: 'destroy_session',
25
- description: 'Close and destroy a Strudel session, releasing its resources.',
26
- inputSchema: {
27
- type: 'object',
28
- properties: { session_id: { type: 'string', description: 'Session identifier to destroy' } },
29
- required: ['session_id'],
30
- },
31
- },
32
- {
33
- name: 'list_sessions',
34
- description: 'List all active Strudel sessions with their metadata.',
35
- inputSchema: { type: 'object', properties: {} },
36
- },
37
- {
38
- name: 'switch_session',
39
- description: 'Change the default session used by other tools.',
11
+ name: 'session',
12
+ description: 'Manage isolated Strudel browser sessions (multi-session, #108). ' +
13
+ 'action=create starts a new named session (sessions share one browser but isolated contexts). ' +
14
+ 'action=destroy closes a named session and releases its resources. ' +
15
+ 'action=list returns metadata for all active sessions (id, created, last_activity, is_playing, is_default). ' +
16
+ 'action=switch changes the default session that subsequent tool calls route to when no session_id is passed. ' +
17
+ 'Example: session({ action: "create", session_id: "live-set-1" }). ' +
18
+ 'For the on-disk pattern catalog use pattern_store(action=list) — session(action=list) lists *runtime* sessions, not saved patterns.',
40
19
  inputSchema: {
41
20
  type: 'object',
42
- properties: { session_id: { type: 'string', description: 'Session identifier to set as default' } },
43
- required: ['session_id'],
21
+ properties: {
22
+ action: { type: 'string', enum: ['create', 'destroy', 'list', 'switch'], description: 'Session lifecycle action' },
23
+ session_id: { type: 'string', description: 'Session identifier (required for create/destroy/switch)' },
24
+ },
25
+ required: ['action'],
44
26
  },
45
27
  },
46
28
  ];
47
29
  export const toolNames = new Set(tools.map(t => t.name));
48
- export async function execute(name, args, ctx) {
30
+ async function doCreate(args, ctx) {
31
+ const sm = ctx.sessionManager;
32
+ InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
33
+ try {
34
+ await sm.createSession(args.session_id);
35
+ return {
36
+ success: true,
37
+ session_id: args.session_id,
38
+ message: `Session '${args.session_id}' created successfully`,
39
+ total_sessions: sm.getSessionCount(),
40
+ max_sessions: sm.getMaxSessions(),
41
+ };
42
+ }
43
+ catch (error) {
44
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
45
+ }
46
+ }
47
+ async function doDestroy(args, ctx) {
49
48
  const sm = ctx.sessionManager;
49
+ InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
50
+ try {
51
+ await sm.destroySession(args.session_id);
52
+ // Drop the session's history bundle (#179) and audio capture service
53
+ // (#180) so a future session with the same id starts fresh.
54
+ ctx.dropHistory(args.session_id);
55
+ ctx.dropAudioCaptureService(args.session_id);
56
+ return {
57
+ success: true,
58
+ session_id: args.session_id,
59
+ message: `Session '${args.session_id}' destroyed`,
60
+ remaining_sessions: sm.getSessionCount(),
61
+ };
62
+ }
63
+ catch (error) {
64
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
65
+ }
66
+ }
67
+ function doList(ctx) {
68
+ const sm = ctx.sessionManager;
69
+ const info = sm.getSessionsInfo();
70
+ return {
71
+ count: info.length,
72
+ max_sessions: sm.getMaxSessions(),
73
+ default_session: sm.getDefaultSessionId(),
74
+ sessions: info.map(s => ({
75
+ id: s.id,
76
+ created: s.created.toISOString(),
77
+ last_activity: s.lastActivity.toISOString(),
78
+ is_playing: s.isPlaying,
79
+ is_default: s.id === sm.getDefaultSessionId(),
80
+ })),
81
+ };
82
+ }
83
+ function doSwitch(args, ctx) {
84
+ const sm = ctx.sessionManager;
85
+ InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
86
+ try {
87
+ sm.setDefaultSession(args.session_id);
88
+ return {
89
+ success: true,
90
+ default_session: args.session_id,
91
+ message: `Default session switched to '${args.session_id}'`,
92
+ };
93
+ }
94
+ catch (error) {
95
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
96
+ }
97
+ }
98
+ export async function execute(name, args, ctx) {
50
99
  switch (name) {
51
- case 'create_session': {
52
- InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
53
- try {
54
- await sm.createSession(args.session_id);
55
- return {
56
- success: true,
57
- session_id: args.session_id,
58
- message: `Session '${args.session_id}' created successfully`,
59
- total_sessions: sm.getSessionCount(),
60
- max_sessions: sm.getMaxSessions(),
61
- };
62
- }
63
- catch (error) {
64
- return { success: false, error: error instanceof Error ? error.message : String(error) };
65
- }
66
- }
67
- case 'destroy_session': {
68
- InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
69
- try {
70
- await sm.destroySession(args.session_id);
71
- return {
72
- success: true,
73
- session_id: args.session_id,
74
- message: `Session '${args.session_id}' destroyed`,
75
- remaining_sessions: sm.getSessionCount(),
76
- };
77
- }
78
- catch (error) {
79
- return { success: false, error: error instanceof Error ? error.message : String(error) };
80
- }
81
- }
82
- case 'list_sessions': {
83
- const info = sm.getSessionsInfo();
84
- return {
85
- count: info.length,
86
- max_sessions: sm.getMaxSessions(),
87
- default_session: sm.getDefaultSessionId(),
88
- sessions: info.map(s => ({
89
- id: s.id,
90
- created: s.created.toISOString(),
91
- last_activity: s.lastActivity.toISOString(),
92
- is_playing: s.isPlaying,
93
- is_default: s.id === sm.getDefaultSessionId(),
94
- })),
95
- };
96
- }
97
- case 'switch_session': {
98
- InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
99
- try {
100
- sm.setDefaultSession(args.session_id);
101
- return {
102
- success: true,
103
- default_session: args.session_id,
104
- message: `Default session switched to '${args.session_id}'`,
105
- };
106
- }
107
- catch (error) {
108
- return { success: false, error: error instanceof Error ? error.message : String(error) };
100
+ case 'session': {
101
+ const a = args?.action;
102
+ switch (a) {
103
+ case 'create': return await doCreate(args, ctx);
104
+ case 'destroy': return await doDestroy(args, ctx);
105
+ case 'list': return doList(ctx);
106
+ case 'switch': return doSwitch(args, ctx);
107
+ default:
108
+ throw new Error(`Invalid action: ${a}. Must be one of: create, destroy, list, switch`);
109
109
  }
110
110
  }
111
111
  default: