@williamzujkowski/live-coding-music-mcp 1.0.0 → 3.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.
- package/LICENSE +661 -21
- package/README.md +228 -233
- package/dist/AudioAnalyzer.js +0 -1
- package/dist/PatternStore.js +0 -1
- package/dist/index.js +0 -0
- package/dist/server/resources.js +175 -0
- package/dist/server/server.js +226 -348
- package/dist/server/tools/ai.js +78 -22
- package/dist/server/tools/analysis.js +266 -73
- package/dist/server/tools/capture.js +67 -20
- package/dist/server/tools/compose.js +159 -0
- package/dist/server/tools/diagnostics.js +112 -60
- package/dist/server/tools/editor.js +130 -66
- package/dist/server/tools/generate.js +190 -74
- package/dist/server/tools/history.js +149 -88
- package/dist/server/tools/playback.js +49 -18
- package/dist/server/tools/session.js +108 -70
- package/dist/server/tools/storage.js +86 -30
- package/dist/server/tools/transform.js +250 -116
- package/dist/server/tools/types.js +66 -1
- package/dist/services/AudioCaptureService.js +4 -3
- package/dist/services/GeminiService.js +1 -1
- package/dist/services/MIDIExportService.js +2 -4
- package/dist/services/SessionManager.js +4 -2
- package/dist/services/StrudelEngine.js +24 -126
- package/dist/services/StrudelEngineHelpers.js +151 -0
- package/package.json +2 -2
- package/dist/__tests__/MusicTheory.test.js +0 -245
- package/dist/__tests__/PatternGenerator.test.js +0 -337
- package/dist/__tests__/PatternStore.test.js +0 -153
- package/dist/__tests__/utils/MockPlaywright.js +0 -130
- package/dist/server/EnhancedMCPServer.js +0 -654
- package/dist/server/EnhancedMCPServerFixed.js +0 -2506
|
@@ -11,20 +11,53 @@
|
|
|
11
11
|
* server pushes onto these arrays on write/append/insert/replace/clear,
|
|
12
12
|
* so all mutations share the same underlying storage.
|
|
13
13
|
*/
|
|
14
|
+
const SESSION_ID_PROP = {
|
|
15
|
+
session_id: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
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.',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
14
20
|
export const tools = [
|
|
21
|
+
{
|
|
22
|
+
name: 'history',
|
|
23
|
+
description: 'Navigate or inspect the pattern edit history. ' +
|
|
24
|
+
'action=undo reverts the last edit on the targeted session. ' +
|
|
25
|
+
'action=redo replays a previously-undone edit. ' +
|
|
26
|
+
'action=list returns recent entries with timestamps and previews (limit defaults to 10). ' +
|
|
27
|
+
'action=restore jumps the editor to a specific entry by id (current pattern goes on the undo stack). ' +
|
|
28
|
+
'action=compare diffs two entries by id (or one entry vs current pattern). ' +
|
|
29
|
+
'Example: history({ action: "list", limit: 5 }) — recent edits, newest first. ' +
|
|
30
|
+
'For on-disk saved patterns use pattern_store — history deals with the in-memory edit timeline of the current session.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
action: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
enum: ['undo', 'redo', 'list', 'restore', 'compare'],
|
|
37
|
+
description: 'Which history operation to perform',
|
|
38
|
+
},
|
|
39
|
+
limit: { type: 'number', description: 'Maximum entries (action=list, default 10)' },
|
|
40
|
+
id: { type: 'number', description: 'History entry ID (action=restore)' },
|
|
41
|
+
id1: { type: 'number', description: 'First entry ID (action=compare)' },
|
|
42
|
+
id2: { type: 'number', description: 'Second entry ID (action=compare, default: current pattern)' },
|
|
43
|
+
...SESSION_ID_PROP,
|
|
44
|
+
},
|
|
45
|
+
required: ['action'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
15
48
|
{
|
|
16
49
|
name: 'undo',
|
|
17
|
-
description: 'Undo last action',
|
|
18
|
-
inputSchema: { type: 'object', properties: {} },
|
|
50
|
+
description: '[DEPRECATED — use history({ action: "undo" }) instead] Undo last action',
|
|
51
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
19
52
|
},
|
|
20
53
|
{
|
|
21
54
|
name: 'redo',
|
|
22
|
-
description: 'Redo action',
|
|
23
|
-
inputSchema: { type: 'object', properties: {} },
|
|
55
|
+
description: '[DEPRECATED — use history({ action: "redo" }) instead] Redo action',
|
|
56
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
24
57
|
},
|
|
25
58
|
{
|
|
26
59
|
name: 'list_history',
|
|
27
|
-
description: 'List recent pattern history with timestamps and previews',
|
|
60
|
+
description: '[DEPRECATED — use history({ action: "list" }) instead] List recent pattern history with timestamps and previews',
|
|
28
61
|
inputSchema: {
|
|
29
62
|
type: 'object',
|
|
30
63
|
properties: {
|
|
@@ -34,23 +67,25 @@ export const tools = [
|
|
|
34
67
|
},
|
|
35
68
|
{
|
|
36
69
|
name: 'restore_history',
|
|
37
|
-
description: 'Restore a previous pattern from history by ID',
|
|
70
|
+
description: '[DEPRECATED — use history({ action: "restore" }) instead] Restore a previous pattern from history by ID',
|
|
38
71
|
inputSchema: {
|
|
39
72
|
type: 'object',
|
|
40
73
|
properties: {
|
|
41
74
|
id: { type: 'number', description: 'History entry ID to restore' },
|
|
75
|
+
...SESSION_ID_PROP,
|
|
42
76
|
},
|
|
43
77
|
required: ['id'],
|
|
44
78
|
},
|
|
45
79
|
},
|
|
46
80
|
{
|
|
47
81
|
name: 'compare_patterns',
|
|
48
|
-
description: 'Compare two patterns from history showing differences',
|
|
82
|
+
description: '[DEPRECATED — use history({ action: "compare" }) instead] Compare two patterns from history showing differences',
|
|
49
83
|
inputSchema: {
|
|
50
84
|
type: 'object',
|
|
51
85
|
properties: {
|
|
52
86
|
id1: { type: 'number', description: 'First pattern ID' },
|
|
53
87
|
id2: { type: 'number', description: 'Second pattern ID (default: current pattern)' },
|
|
88
|
+
...SESSION_ID_PROP,
|
|
54
89
|
},
|
|
55
90
|
required: ['id1'],
|
|
56
91
|
},
|
|
@@ -104,91 +139,117 @@ function summarizeDiff(a, b) {
|
|
|
104
139
|
}
|
|
105
140
|
return { linesAdded: added, linesRemoved: removed, linesChanged: changed, charsDiff: b.length - a.length };
|
|
106
141
|
}
|
|
142
|
+
async function doUndo(ctx, sid) {
|
|
143
|
+
const { undoStack, redoStack, maxHistory } = ctx.getHistory(sid);
|
|
144
|
+
if (!sid && !ctx.isInitialized())
|
|
145
|
+
return 'Browser not initialized. Run init first.';
|
|
146
|
+
if (undoStack.length === 0)
|
|
147
|
+
return 'Nothing to undo';
|
|
148
|
+
const controller = ctx.getController(sid);
|
|
149
|
+
const current = await controller.getCurrentPattern();
|
|
150
|
+
redoStack.push(current);
|
|
151
|
+
if (redoStack.length > maxHistory)
|
|
152
|
+
redoStack.shift();
|
|
153
|
+
const previous = undoStack.pop();
|
|
154
|
+
await controller.writePattern(previous);
|
|
155
|
+
return 'Undone';
|
|
156
|
+
}
|
|
157
|
+
async function doRedo(ctx, sid) {
|
|
158
|
+
const { undoStack, redoStack, maxHistory } = ctx.getHistory(sid);
|
|
159
|
+
if (!sid && !ctx.isInitialized())
|
|
160
|
+
return 'Browser not initialized. Run init first.';
|
|
161
|
+
if (redoStack.length === 0)
|
|
162
|
+
return 'Nothing to redo';
|
|
163
|
+
const controller = ctx.getController(sid);
|
|
164
|
+
const current = await controller.getCurrentPattern();
|
|
165
|
+
undoStack.push(current);
|
|
166
|
+
if (undoStack.length > maxHistory)
|
|
167
|
+
undoStack.shift();
|
|
168
|
+
const next = redoStack.pop();
|
|
169
|
+
await controller.writePattern(next);
|
|
170
|
+
return 'Redone';
|
|
171
|
+
}
|
|
172
|
+
function doList(args, ctx, sid) {
|
|
173
|
+
const { historyStack } = ctx.getHistory(sid);
|
|
174
|
+
if (historyStack.length === 0) {
|
|
175
|
+
return 'No pattern history yet. Make some edits to build history.';
|
|
176
|
+
}
|
|
177
|
+
const limit = args?.limit || 10;
|
|
178
|
+
const recent = historyStack.slice(-limit).reverse();
|
|
179
|
+
return {
|
|
180
|
+
count: historyStack.length,
|
|
181
|
+
showing: recent.length,
|
|
182
|
+
entries: recent.map(e => ({
|
|
183
|
+
id: e.id,
|
|
184
|
+
preview: e.pattern.substring(0, 60) + (e.pattern.length > 60 ? '...' : ''),
|
|
185
|
+
chars: e.pattern.length,
|
|
186
|
+
action: e.action,
|
|
187
|
+
timestamp: formatTimeAgo(e.timestamp),
|
|
188
|
+
})),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function doRestore(args, ctx, sid) {
|
|
192
|
+
const { undoStack, historyStack, maxHistory } = ctx.getHistory(sid);
|
|
193
|
+
if (!sid && !ctx.isInitialized())
|
|
194
|
+
return 'Browser not initialized. Run init first.';
|
|
195
|
+
const controller = ctx.getController(sid);
|
|
196
|
+
const entry = historyStack.find(e => e.id === args.id);
|
|
197
|
+
if (!entry) {
|
|
198
|
+
return `History entry #${args.id} not found. Use history({ action: "list" }) to see available entries.`;
|
|
199
|
+
}
|
|
200
|
+
const current = await controller.getCurrentPattern();
|
|
201
|
+
undoStack.push(current);
|
|
202
|
+
if (undoStack.length > maxHistory)
|
|
203
|
+
undoStack.shift();
|
|
204
|
+
await controller.writePattern(entry.pattern);
|
|
205
|
+
return `Restored pattern from history #${args.id} (${formatTimeAgo(entry.timestamp)})`;
|
|
206
|
+
}
|
|
207
|
+
async function doCompare(args, ctx, sid) {
|
|
208
|
+
const { historyStack } = ctx.getHistory(sid);
|
|
209
|
+
const first = historyStack.find(e => e.id === args.id1);
|
|
210
|
+
if (!first)
|
|
211
|
+
return `History entry #${args.id1} not found.`;
|
|
212
|
+
let second;
|
|
213
|
+
let label;
|
|
214
|
+
if (args.id2) {
|
|
215
|
+
const b = historyStack.find(e => e.id === args.id2);
|
|
216
|
+
if (!b)
|
|
217
|
+
return `History entry #${args.id2} not found.`;
|
|
218
|
+
second = b.pattern;
|
|
219
|
+
label = `#${args.id2}`;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
second = await ctx.getCurrentPatternSafe(sid);
|
|
223
|
+
label = 'current';
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
pattern1: { id: args.id1, chars: first.pattern.length },
|
|
227
|
+
pattern2: { id: label, chars: second.length },
|
|
228
|
+
diff: generateDiff(first.pattern, second),
|
|
229
|
+
summary: summarizeDiff(first.pattern, second),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
107
232
|
export async function execute(name, args, ctx) {
|
|
108
|
-
const
|
|
233
|
+
const sid = args?.session_id;
|
|
109
234
|
switch (name) {
|
|
110
|
-
case '
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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';
|
|
235
|
+
case 'history': {
|
|
236
|
+
const action = args?.action;
|
|
237
|
+
switch (action) {
|
|
238
|
+
case 'undo': return await doUndo(ctx, sid);
|
|
239
|
+
case 'redo': return await doRedo(ctx, sid);
|
|
240
|
+
case 'list': return doList(args, ctx, sid);
|
|
241
|
+
case 'restore': return await doRestore(args, ctx, sid);
|
|
242
|
+
case 'compare': return await doCompare(args, ctx, sid);
|
|
243
|
+
default:
|
|
244
|
+
throw new Error(`Invalid action: ${action}. Must be one of: undo, redo, list, restore, compare`);
|
|
184
245
|
}
|
|
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
246
|
}
|
|
247
|
+
// Deprecated aliases — forward to consolidated handler.
|
|
248
|
+
case 'undo': return await doUndo(ctx, sid);
|
|
249
|
+
case 'redo': return await doRedo(ctx, sid);
|
|
250
|
+
case 'list_history': return doList(args, ctx, sid);
|
|
251
|
+
case 'restore_history': return await doRestore(args, ctx, sid);
|
|
252
|
+
case 'compare_patterns': return await doCompare(args, ctx, sid);
|
|
192
253
|
default:
|
|
193
254
|
throw new Error(`history module does not handle tool: ${name}`);
|
|
194
255
|
}
|
|
@@ -1,41 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* playback domain — transport controls
|
|
2
|
+
* playback domain — transport controls.
|
|
3
3
|
*
|
|
4
|
-
* Owns
|
|
5
|
-
* (
|
|
6
|
-
* For now we keep the individual verbs and only extract them out of
|
|
7
|
-
* server.ts.
|
|
4
|
+
* Owns one consolidated tool (`playback(action)`) plus three deprecated
|
|
5
|
+
* aliases (play, pause, stop) per #120 / #152.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Aliases forward to `playback` and will be removed in a future release
|
|
8
|
+
* per the deprecation policy.
|
|
11
9
|
*/
|
|
10
|
+
const SESSION_ID_PROP = {
|
|
11
|
+
session_id: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'Optional session ID (#108). Omit to use default session.',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
12
16
|
export const tools = [
|
|
17
|
+
{
|
|
18
|
+
name: 'playback',
|
|
19
|
+
description: 'Control transport on the current session. ' +
|
|
20
|
+
'action=play starts the editor pattern. ' +
|
|
21
|
+
'action=pause stops without resetting clock. ' +
|
|
22
|
+
'action=stop ends playback. ' +
|
|
23
|
+
'Example: playback({ action: "play" }). ' +
|
|
24
|
+
'For pattern editing use edit_pattern; for tempo use set_tempo.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
action: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
enum: ['play', 'pause', 'stop'],
|
|
31
|
+
description: 'Transport action',
|
|
32
|
+
},
|
|
33
|
+
...SESSION_ID_PROP,
|
|
34
|
+
},
|
|
35
|
+
required: ['action'],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
13
38
|
{
|
|
14
39
|
name: 'play',
|
|
15
|
-
description: 'Start playing pattern',
|
|
16
|
-
inputSchema: { type: 'object', properties: {} },
|
|
40
|
+
description: '[DEPRECATED — use playback({ action: "play" }) instead] Start playing pattern',
|
|
41
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
17
42
|
},
|
|
18
43
|
{
|
|
19
44
|
name: 'pause',
|
|
20
|
-
description: 'Pause playback',
|
|
21
|
-
inputSchema: { type: 'object', properties: {} },
|
|
45
|
+
description: '[DEPRECATED — use playback({ action: "pause" }) instead] Pause playback',
|
|
46
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
22
47
|
},
|
|
23
48
|
{
|
|
24
49
|
name: 'stop',
|
|
25
|
-
description: 'Stop playback',
|
|
26
|
-
inputSchema: { type: 'object', properties: {} },
|
|
50
|
+
description: '[DEPRECATED — use playback({ action: "stop" }) instead] Stop playback',
|
|
51
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
27
52
|
},
|
|
28
53
|
];
|
|
29
54
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
30
|
-
export async function execute(name,
|
|
31
|
-
|
|
55
|
+
export async function execute(name, args, ctx) {
|
|
56
|
+
const sid = args?.session_id;
|
|
57
|
+
if (!sid && !ctx.isInitialized()) {
|
|
58
|
+
return 'Browser not initialized. Run init first.';
|
|
59
|
+
}
|
|
60
|
+
const controller = ctx.getController(sid);
|
|
61
|
+
const action = name === 'playback' ? args?.action : name;
|
|
62
|
+
switch (action) {
|
|
32
63
|
case 'play':
|
|
33
|
-
return await
|
|
64
|
+
return await controller.play();
|
|
34
65
|
case 'pause':
|
|
35
66
|
case 'stop':
|
|
36
|
-
return await
|
|
67
|
+
return await controller.stop();
|
|
37
68
|
default:
|
|
38
|
-
throw new Error(`
|
|
69
|
+
throw new Error(`Invalid action: ${action}. Must be one of: play, pause, stop`);
|
|
39
70
|
}
|
|
40
71
|
}
|
|
41
72
|
export const playbackModule = { tools, toolNames, execute };
|
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session domain — multi-session lifecycle management.
|
|
3
3
|
*
|
|
4
|
-
* Owns (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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)`) plus four deprecated
|
|
5
|
+
* aliases (create_session, destroy_session, list_sessions, switch_session)
|
|
6
|
+
* per #120 / #158. `init` stays separate — it's the global bootstrap,
|
|
7
|
+
* not a session operation.
|
|
11
8
|
*/
|
|
12
9
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
13
10
|
export const tools = [
|
|
11
|
+
{
|
|
12
|
+
name: 'session',
|
|
13
|
+
description: 'Manage isolated Strudel browser sessions (multi-session, #108). ' +
|
|
14
|
+
'action=create starts a new named session (sessions share one browser but isolated contexts). ' +
|
|
15
|
+
'action=destroy closes a named session and releases its resources. ' +
|
|
16
|
+
'action=list returns metadata for all active sessions (id, created, last_activity, is_playing, is_default). ' +
|
|
17
|
+
'action=switch changes the default session that subsequent tool calls route to when no session_id is passed. ' +
|
|
18
|
+
'Example: session({ action: "create", session_id: "live-set-1" }). ' +
|
|
19
|
+
'For the on-disk pattern catalog use pattern_store(action=list) — session(action=list) lists *runtime* sessions, not saved patterns.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
action: { type: 'string', enum: ['create', 'destroy', 'list', 'switch'], description: 'Session lifecycle action' },
|
|
24
|
+
session_id: { type: 'string', description: 'Session identifier (required for create/destroy/switch)' },
|
|
25
|
+
},
|
|
26
|
+
required: ['action'],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
14
29
|
{
|
|
15
30
|
name: 'create_session',
|
|
16
|
-
description: '
|
|
31
|
+
description: '[DEPRECATED — use session({ action: "create" }) instead] Create a new isolated Strudel browser session.',
|
|
17
32
|
inputSchema: {
|
|
18
33
|
type: 'object',
|
|
19
34
|
properties: { session_id: { type: 'string', description: 'Unique identifier for the session' } },
|
|
@@ -22,7 +37,7 @@ export const tools = [
|
|
|
22
37
|
},
|
|
23
38
|
{
|
|
24
39
|
name: 'destroy_session',
|
|
25
|
-
description: 'Close and destroy a Strudel session
|
|
40
|
+
description: '[DEPRECATED — use session({ action: "destroy" }) instead] Close and destroy a Strudel session.',
|
|
26
41
|
inputSchema: {
|
|
27
42
|
type: 'object',
|
|
28
43
|
properties: { session_id: { type: 'string', description: 'Session identifier to destroy' } },
|
|
@@ -31,12 +46,12 @@ export const tools = [
|
|
|
31
46
|
},
|
|
32
47
|
{
|
|
33
48
|
name: 'list_sessions',
|
|
34
|
-
description: 'List all active Strudel sessions
|
|
49
|
+
description: '[DEPRECATED — use session({ action: "list" }) instead] List all active Strudel sessions.',
|
|
35
50
|
inputSchema: { type: 'object', properties: {} },
|
|
36
51
|
},
|
|
37
52
|
{
|
|
38
53
|
name: 'switch_session',
|
|
39
|
-
description: '
|
|
54
|
+
description: '[DEPRECATED — use session({ action: "switch" }) instead] Change the default session.',
|
|
40
55
|
inputSchema: {
|
|
41
56
|
type: 'object',
|
|
42
57
|
properties: { session_id: { type: 'string', description: 'Session identifier to set as default' } },
|
|
@@ -45,69 +60,92 @@ export const tools = [
|
|
|
45
60
|
},
|
|
46
61
|
];
|
|
47
62
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
48
|
-
|
|
63
|
+
async function doCreate(args, ctx) {
|
|
64
|
+
const sm = ctx.sessionManager;
|
|
65
|
+
InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
|
|
66
|
+
try {
|
|
67
|
+
await sm.createSession(args.session_id);
|
|
68
|
+
return {
|
|
69
|
+
success: true,
|
|
70
|
+
session_id: args.session_id,
|
|
71
|
+
message: `Session '${args.session_id}' created successfully`,
|
|
72
|
+
total_sessions: sm.getSessionCount(),
|
|
73
|
+
max_sessions: sm.getMaxSessions(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function doDestroy(args, ctx) {
|
|
81
|
+
const sm = ctx.sessionManager;
|
|
82
|
+
InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
|
|
83
|
+
try {
|
|
84
|
+
await sm.destroySession(args.session_id);
|
|
85
|
+
// Drop the session's history bundle (#179) and audio capture service
|
|
86
|
+
// (#180) so a future session with the same id starts fresh.
|
|
87
|
+
ctx.dropHistory(args.session_id);
|
|
88
|
+
ctx.dropAudioCaptureService(args.session_id);
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
session_id: args.session_id,
|
|
92
|
+
message: `Session '${args.session_id}' destroyed`,
|
|
93
|
+
remaining_sessions: sm.getSessionCount(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function doList(ctx) {
|
|
49
101
|
const sm = ctx.sessionManager;
|
|
102
|
+
const info = sm.getSessionsInfo();
|
|
103
|
+
return {
|
|
104
|
+
count: info.length,
|
|
105
|
+
max_sessions: sm.getMaxSessions(),
|
|
106
|
+
default_session: sm.getDefaultSessionId(),
|
|
107
|
+
sessions: info.map(s => ({
|
|
108
|
+
id: s.id,
|
|
109
|
+
created: s.created.toISOString(),
|
|
110
|
+
last_activity: s.lastActivity.toISOString(),
|
|
111
|
+
is_playing: s.isPlaying,
|
|
112
|
+
is_default: s.id === sm.getDefaultSessionId(),
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function doSwitch(args, ctx) {
|
|
117
|
+
const sm = ctx.sessionManager;
|
|
118
|
+
InputValidator.validateStringLength(args.session_id, 'session_id', 100, false);
|
|
119
|
+
try {
|
|
120
|
+
sm.setDefaultSession(args.session_id);
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
default_session: args.session_id,
|
|
124
|
+
message: `Default session switched to '${args.session_id}'`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export async function execute(name, args, ctx) {
|
|
50
132
|
switch (name) {
|
|
51
|
-
case '
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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) };
|
|
133
|
+
case 'session': {
|
|
134
|
+
const a = args?.action;
|
|
135
|
+
switch (a) {
|
|
136
|
+
case 'create': return await doCreate(args, ctx);
|
|
137
|
+
case 'destroy': return await doDestroy(args, ctx);
|
|
138
|
+
case 'list': return doList(ctx);
|
|
139
|
+
case 'switch': return doSwitch(args, ctx);
|
|
140
|
+
default:
|
|
141
|
+
throw new Error(`Invalid action: ${a}. Must be one of: create, destroy, list, switch`);
|
|
109
142
|
}
|
|
110
143
|
}
|
|
144
|
+
// Deprecated aliases.
|
|
145
|
+
case 'create_session': return await doCreate(args, ctx);
|
|
146
|
+
case 'destroy_session': return await doDestroy(args, ctx);
|
|
147
|
+
case 'list_sessions': return doList(ctx);
|
|
148
|
+
case 'switch_session': return doSwitch(args, ctx);
|
|
111
149
|
default:
|
|
112
150
|
throw new Error(`session module does not handle tool: ${name}`);
|
|
113
151
|
}
|