@williamzujkowski/live-coding-music-mcp 1.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 +21 -0
- package/README.md +1735 -0
- package/dist/AudioAnalyzer.js +678 -0
- package/dist/PatternStore.js +169 -0
- package/dist/StrudelController.js +676 -0
- package/dist/__tests__/MusicTheory.test.js +245 -0
- package/dist/__tests__/PatternGenerator.test.js +337 -0
- package/dist/__tests__/PatternStore.test.js +153 -0
- package/dist/__tests__/utils/MockPlaywright.js +130 -0
- package/dist/index.js +21 -0
- package/dist/server/EnhancedMCPServer.js +654 -0
- package/dist/server/EnhancedMCPServerFixed.js +2506 -0
- package/dist/server/server.js +580 -0
- package/dist/server/tools/ai.js +530 -0
- package/dist/server/tools/analysis.js +132 -0
- package/dist/server/tools/capture.js +184 -0
- package/dist/server/tools/diagnostics.js +96 -0
- package/dist/server/tools/editor.js +128 -0
- package/dist/server/tools/generate.js +218 -0
- package/dist/server/tools/history.js +196 -0
- package/dist/server/tools/playback.js +41 -0
- package/dist/server/tools/session.js +115 -0
- package/dist/server/tools/storage.js +77 -0
- package/dist/server/tools/transform.js +399 -0
- package/dist/server/tools/types.js +9 -0
- package/dist/services/AudioCaptureService.js +279 -0
- package/dist/services/GeminiService.js +790 -0
- package/dist/services/MIDIExportService.js +412 -0
- package/dist/services/MusicTheory.js +186 -0
- package/dist/services/PatternGenerator.js +647 -0
- package/dist/services/SessionManager.js +316 -0
- package/dist/services/StrudelEngine.js +374 -0
- package/dist/services/__mocks__/StrudelEngine.js +157 -0
- package/dist/types/AudioAnalysis.js +4 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/ErrorRecovery.js +248 -0
- package/dist/utils/InputValidator.js +281 -0
- package/dist/utils/Logger.js +19 -0
- package/dist/utils/PatternValidator.js +241 -0
- package/dist/utils/PerformanceMonitor.js +128 -0
- package/package.json +86 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* history domain — undo/redo stacks and the pattern history browser.
|
|
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.
|
|
8
|
+
*
|
|
9
|
+
* 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.
|
|
13
|
+
*/
|
|
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
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'compare_patterns',
|
|
48
|
+
description: 'Compare two patterns from history showing differences',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
id1: { type: 'number', description: 'First pattern ID' },
|
|
53
|
+
id2: { type: 'number', description: 'Second pattern ID (default: current pattern)' },
|
|
54
|
+
},
|
|
55
|
+
required: ['id1'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
export const toolNames = new Set(tools.map(t => t.name));
|
|
60
|
+
function formatTimeAgo(date) {
|
|
61
|
+
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
|
|
62
|
+
if (seconds < 60)
|
|
63
|
+
return `${seconds}s ago`;
|
|
64
|
+
if (seconds < 3600)
|
|
65
|
+
return `${Math.floor(seconds / 60)}m ago`;
|
|
66
|
+
if (seconds < 86400)
|
|
67
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
68
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
69
|
+
}
|
|
70
|
+
function generateDiff(a, b) {
|
|
71
|
+
const l1 = a.split('\n');
|
|
72
|
+
const l2 = b.split('\n');
|
|
73
|
+
const out = [];
|
|
74
|
+
const max = Math.max(l1.length, l2.length);
|
|
75
|
+
for (let i = 0; i < max; i++) {
|
|
76
|
+
const x = l1[i] || '';
|
|
77
|
+
const y = l2[i] || '';
|
|
78
|
+
if (x === y) {
|
|
79
|
+
out.push(` ${x}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
if (x)
|
|
83
|
+
out.push(`- ${x}`);
|
|
84
|
+
if (y)
|
|
85
|
+
out.push(`+ ${y}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function summarizeDiff(a, b) {
|
|
91
|
+
const l1 = a.split('\n');
|
|
92
|
+
const l2 = b.split('\n');
|
|
93
|
+
let added = 0, removed = 0, changed = 0;
|
|
94
|
+
const max = Math.max(l1.length, l2.length);
|
|
95
|
+
for (let i = 0; i < max; i++) {
|
|
96
|
+
const x = l1[i];
|
|
97
|
+
const y = l2[i];
|
|
98
|
+
if (x === undefined && y !== undefined)
|
|
99
|
+
added++;
|
|
100
|
+
else if (x !== undefined && y === undefined)
|
|
101
|
+
removed++;
|
|
102
|
+
else if (x !== y)
|
|
103
|
+
changed++;
|
|
104
|
+
}
|
|
105
|
+
return { linesAdded: added, linesRemoved: removed, linesChanged: changed, charsDiff: b.length - a.length };
|
|
106
|
+
}
|
|
107
|
+
export async function execute(name, args, ctx) {
|
|
108
|
+
const { undoStack, redoStack, historyStack, maxHistory } = ctx.history;
|
|
109
|
+
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';
|
|
184
|
+
}
|
|
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
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
throw new Error(`history module does not handle tool: ${name}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export const historyModule = { tools, toolNames, execute };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* playback domain — transport controls (play, pause, stop).
|
|
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.
|
|
11
|
+
*/
|
|
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: {} },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'stop',
|
|
25
|
+
description: 'Stop playback',
|
|
26
|
+
inputSchema: { type: 'object', properties: {} },
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
export const toolNames = new Set(tools.map(t => t.name));
|
|
30
|
+
export async function execute(name, _args, ctx) {
|
|
31
|
+
switch (name) {
|
|
32
|
+
case 'play':
|
|
33
|
+
return await ctx.controller.play();
|
|
34
|
+
case 'pause':
|
|
35
|
+
case 'stop':
|
|
36
|
+
return await ctx.controller.stop();
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`playback module does not handle tool: ${name}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export const playbackModule = { tools, toolNames, execute };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session domain — multi-session lifecycle management.
|
|
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`.
|
|
11
|
+
*/
|
|
12
|
+
import { InputValidator } from '../../utils/InputValidator.js';
|
|
13
|
+
export const tools = [
|
|
14
|
+
{
|
|
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.',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: { session_id: { type: 'string', description: 'Session identifier to set as default' } },
|
|
43
|
+
required: ['session_id'],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
export const toolNames = new Set(tools.map(t => t.name));
|
|
48
|
+
export async function execute(name, args, ctx) {
|
|
49
|
+
const sm = ctx.sessionManager;
|
|
50
|
+
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) };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
throw new Error(`session module does not handle tool: ${name}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export const sessionModule = { tools, toolNames, execute };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* storage domain — pattern persistence (save, load, list).
|
|
3
|
+
*
|
|
4
|
+
* Owns (3 tools): save, load, list. Post-consolidation target per the
|
|
5
|
+
* #110 audit: single `pattern_store` tool with `action` enum + params,
|
|
6
|
+
* which also resolves the `list` / `list_sessions` naming collision.
|
|
7
|
+
* For now we keep the individual verbs.
|
|
8
|
+
*/
|
|
9
|
+
import { InputValidator } from '../../utils/InputValidator.js';
|
|
10
|
+
export const tools = [
|
|
11
|
+
{
|
|
12
|
+
name: 'save',
|
|
13
|
+
description: 'Save pattern with metadata',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
name: { type: 'string', description: 'Pattern name' },
|
|
18
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
19
|
+
},
|
|
20
|
+
required: ['name'],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'load',
|
|
25
|
+
description: 'Load saved pattern',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
name: { type: 'string', description: 'Pattern name' },
|
|
30
|
+
},
|
|
31
|
+
required: ['name'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'list',
|
|
36
|
+
description: 'List saved patterns',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
tag: { type: 'string', description: 'Filter by tag' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
export const toolNames = new Set(tools.map(t => t.name));
|
|
46
|
+
export async function execute(name, args, ctx) {
|
|
47
|
+
switch (name) {
|
|
48
|
+
case 'save': {
|
|
49
|
+
InputValidator.validateStringLength(args.name, 'name', 255, false);
|
|
50
|
+
const toSave = await ctx.getCurrentPatternSafe();
|
|
51
|
+
if (!toSave) {
|
|
52
|
+
return 'No pattern to save';
|
|
53
|
+
}
|
|
54
|
+
await ctx.store.save(args.name, toSave, args.tags || []);
|
|
55
|
+
return `Pattern saved as "${args.name}"`;
|
|
56
|
+
}
|
|
57
|
+
case 'load': {
|
|
58
|
+
InputValidator.validateStringLength(args.name, 'name', 255, false);
|
|
59
|
+
const saved = await ctx.store.load(args.name);
|
|
60
|
+
if (saved) {
|
|
61
|
+
await ctx.writePatternSafe(saved.content);
|
|
62
|
+
return `Loaded pattern "${args.name}"`;
|
|
63
|
+
}
|
|
64
|
+
return `Pattern "${args.name}" not found`;
|
|
65
|
+
}
|
|
66
|
+
case 'list': {
|
|
67
|
+
if (args?.tag) {
|
|
68
|
+
InputValidator.validateStringLength(args.tag, 'tag', 100, false);
|
|
69
|
+
}
|
|
70
|
+
const patterns = await ctx.store.list(args?.tag);
|
|
71
|
+
return patterns.map(p => `• ${p.name} [${p.tags.join(', ')}] - ${p.timestamp}`).join('\n') || 'No patterns found';
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`storage module does not handle tool: ${name}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export const storageModule = { tools, toolNames, execute };
|