voicemode-channel 0.1.4 → 0.2.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 +39 -8
- package/dist/auth.js +42 -35
- package/dist/credentials.js +1 -1
- package/dist/gateway.js +5 -2
- package/dist/index.js +66 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ User hears TTS response <- Channel reply tool <----------------------+
|
|
|
17
17
|
### Claude Code plugin
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
+
claude plugin marketplace add mbailey/claude-plugins
|
|
20
21
|
claude plugin install voicemode-channel@mbailey
|
|
21
22
|
```
|
|
22
23
|
|
|
@@ -63,13 +64,13 @@ Open **[app.voicemode.dev](https://app.voicemode.dev)** on your phone or browser
|
|
|
63
64
|
|
|
64
65
|
## Configuration
|
|
65
66
|
|
|
66
|
-
| Environment Variable
|
|
67
|
-
|
|
68
|
-
| `VOICEMODE_CHANNEL_ENABLED`
|
|
69
|
-
| `VOICEMODE_CHANNEL_DEBUG`
|
|
70
|
-
| `VOICEMODE_CONNECT_WS_URL`
|
|
71
|
-
| `VOICEMODE_AGENT_NAME`
|
|
72
|
-
| `VOICEMODE_AGENT_DISPLAY_NAME` | `Claude Code`
|
|
67
|
+
| Environment Variable | Default | Description |
|
|
68
|
+
| ------------------------------ | ------------------------ | --------------------------------------------------------------------------- |
|
|
69
|
+
| `VOICEMODE_CHANNEL_ENABLED` | `false` | **Required.** Must be `true` to enable. Server exits immediately otherwise. |
|
|
70
|
+
| `VOICEMODE_CHANNEL_DEBUG` | `false` | Enable debug logging |
|
|
71
|
+
| `VOICEMODE_CONNECT_WS_URL` | `wss://voicemode.dev/ws` | WebSocket gateway URL |
|
|
72
|
+
| `VOICEMODE_AGENT_NAME` | `voicemode` | Agent identity for gateway registration |
|
|
73
|
+
| `VOICEMODE_AGENT_DISPLAY_NAME` | `Claude Code` | Display name shown to callers |
|
|
73
74
|
|
|
74
75
|
## How it works
|
|
75
76
|
|
|
@@ -81,6 +82,7 @@ This plugin provides an MCP server that declares the experimental `claude/channe
|
|
|
81
82
|
4. Provides a `reply` tool for Claude to send responses back
|
|
82
83
|
|
|
83
84
|
Channel events appear in Claude's session as:
|
|
85
|
+
|
|
84
86
|
```
|
|
85
87
|
<channel source="voicemode-channel" caller="NAME">TRANSCRIPT</channel>
|
|
86
88
|
```
|
|
@@ -88,20 +90,24 @@ Channel events appear in Claude's session as:
|
|
|
88
90
|
## Troubleshooting
|
|
89
91
|
|
|
90
92
|
**Channel not connecting**
|
|
93
|
+
|
|
91
94
|
- Ensure `VOICEMODE_CHANNEL_ENABLED=true` is set
|
|
92
95
|
- Check credentials exist: `voicemode-channel auth status`
|
|
93
96
|
- Re-authenticate: `voicemode-channel auth login`
|
|
94
97
|
- Enable debug logging: `VOICEMODE_CHANNEL_DEBUG=true`
|
|
95
98
|
|
|
96
99
|
**No audio on caller's device**
|
|
100
|
+
|
|
97
101
|
- Confirm you're signed into [app.voicemode.dev](https://app.voicemode.dev) with the same account
|
|
98
102
|
- Check that Claude is using the `reply` tool (not a plain text response)
|
|
99
103
|
|
|
100
104
|
**Plugin not found after install**
|
|
105
|
+
|
|
101
106
|
- Verify Claude Code v2.1.80+ is installed: `claude --version`
|
|
102
107
|
- Reinstall: `claude plugin install voicemode-channel@mbailey`
|
|
103
108
|
|
|
104
109
|
**Hook timeout on startup**
|
|
110
|
+
|
|
105
111
|
- The SessionStart hook installs npm dependencies -- this may take a moment on first run
|
|
106
112
|
- Subsequent starts use the cached install and are fast
|
|
107
113
|
|
|
@@ -114,12 +120,37 @@ cd voicemode-channel
|
|
|
114
120
|
npm install
|
|
115
121
|
|
|
116
122
|
# Build
|
|
117
|
-
|
|
123
|
+
make build
|
|
118
124
|
|
|
119
125
|
# Test with --plugin-dir
|
|
120
126
|
VOICEMODE_CHANNEL_ENABLED=true claude --plugin-dir . --dangerously-load-development-channels server:voicemode-channel
|
|
121
127
|
```
|
|
122
128
|
|
|
129
|
+
### Testing with mcptools
|
|
130
|
+
|
|
131
|
+
[mcptools](https://github.com/f/mcptools) provides an interactive shell for testing MCP servers (`brew install mcptools`):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
make shell
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Example session:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
mcp > tools # List available tools
|
|
141
|
+
mcp > status # Check connection state
|
|
142
|
+
mcp > reply {"text":"hello from cli"} # Send a voice reply
|
|
143
|
+
mcp > profile # View agent profile
|
|
144
|
+
mcp > profile {"voice":"af_sky"} # Update profile fields
|
|
145
|
+
mcp > /q # Quit
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The MCP Inspector web UI is also available:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
make inspect
|
|
152
|
+
```
|
|
153
|
+
|
|
123
154
|
## Status
|
|
124
155
|
|
|
125
156
|
Research preview. Requires Claude Code v2.1.80+ with channel support.
|
package/dist/auth.js
CHANGED
|
@@ -12,7 +12,6 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
12
12
|
import { createServer } from 'node:http';
|
|
13
13
|
import { execFile } from 'node:child_process';
|
|
14
14
|
import { platform } from 'node:os';
|
|
15
|
-
import { createConnection } from 'node:net';
|
|
16
15
|
import { AUTH0_DOMAIN, AUTH0_CLIENT_ID, save_credentials, } from './credentials.js';
|
|
17
16
|
// ---------------------------------------------------------------------------
|
|
18
17
|
// Auth0 OAuth parameters (matching Python CLI)
|
|
@@ -38,23 +37,27 @@ function generate_pkce_params() {
|
|
|
38
37
|
// ---------------------------------------------------------------------------
|
|
39
38
|
// Port selection
|
|
40
39
|
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Try to listen on a port, resolving with the server if successful.
|
|
42
|
+
* Eliminates TOCTOU race by using listen() directly instead of checking first.
|
|
43
|
+
*/
|
|
44
|
+
function try_listen(server, port) {
|
|
42
45
|
return new Promise((resolve) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
server.once('error', (err) => {
|
|
47
|
+
if (err.code === 'EADDRINUSE')
|
|
48
|
+
resolve(false);
|
|
49
|
+
else
|
|
50
|
+
resolve(false);
|
|
51
|
+
});
|
|
52
|
+
server.listen(port, '127.0.0.1', () => resolve(true));
|
|
46
53
|
});
|
|
47
54
|
}
|
|
48
|
-
async function find_available_port() {
|
|
49
|
-
for (let port = CALLBACK_PORT_START; port <= CALLBACK_PORT_END; port++) {
|
|
50
|
-
if (await is_port_available(port))
|
|
51
|
-
return port;
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
55
|
// ---------------------------------------------------------------------------
|
|
56
56
|
// Callback HTML page
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
|
+
function escape_html(s) {
|
|
59
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
60
|
+
}
|
|
58
61
|
function callback_page(success, error_message = '') {
|
|
59
62
|
const icon_bg = success ? '#3fb950' : '#f85149';
|
|
60
63
|
const icon_svg = success
|
|
@@ -63,7 +66,7 @@ function callback_page(success, error_message = '') {
|
|
|
63
66
|
const heading = success ? 'Authentication Successful' : 'Authentication Failed';
|
|
64
67
|
const message = success
|
|
65
68
|
? 'You can close this window and return to the terminal.'
|
|
66
|
-
: (error_message ? `Error: ${error_message}` : 'Something went wrong.');
|
|
69
|
+
: (error_message ? `Error: ${escape_html(error_message)}` : 'Something went wrong.');
|
|
67
70
|
return `<!DOCTYPE html>
|
|
68
71
|
<html lang="en">
|
|
69
72
|
<head>
|
|
@@ -195,22 +198,15 @@ function build_authorize_url(redirect_uri, pkce, state) {
|
|
|
195
198
|
// Main login flow
|
|
196
199
|
// ---------------------------------------------------------------------------
|
|
197
200
|
export async function login(log) {
|
|
198
|
-
//
|
|
199
|
-
const port = await find_available_port();
|
|
200
|
-
if (port === null) {
|
|
201
|
-
throw new Error(`No available ports in range ${CALLBACK_PORT_START}-${CALLBACK_PORT_END}. ` +
|
|
202
|
-
'Please close applications using these ports and try again.');
|
|
203
|
-
}
|
|
204
|
-
// Generate PKCE parameters and state
|
|
201
|
+
// Generate PKCE parameters and state upfront (needed for the request handler)
|
|
205
202
|
const pkce = generate_pkce_params();
|
|
206
203
|
const state = randomBytes(16).toString('base64url');
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
const auth_url = build_authorize_url(redirect_uri, pkce, state);
|
|
210
|
-
// Start callback server and wait for the authorization code
|
|
204
|
+
// Create the server with request handler and find a port by trying to listen directly.
|
|
205
|
+
// The server stays bound to the port the entire time -- no TOCTOU race.
|
|
211
206
|
const result = await new Promise((resolve, reject) => {
|
|
212
|
-
|
|
213
|
-
|
|
207
|
+
let bound_port = 0;
|
|
208
|
+
const callback_server = createServer((req, res) => {
|
|
209
|
+
const url = new URL(req.url ?? '/', `http://localhost:${bound_port}`);
|
|
214
210
|
if (url.pathname !== '/callback') {
|
|
215
211
|
res.writeHead(404);
|
|
216
212
|
res.end();
|
|
@@ -236,7 +232,7 @@ export async function login(log) {
|
|
|
236
232
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
237
233
|
res.end(callback_page(true));
|
|
238
234
|
cleanup();
|
|
239
|
-
resolve({ code, state: url.searchParams.get('state') });
|
|
235
|
+
resolve({ code, state: url.searchParams.get('state'), port: bound_port });
|
|
240
236
|
});
|
|
241
237
|
const timeout_timer = setTimeout(() => {
|
|
242
238
|
cleanup();
|
|
@@ -244,10 +240,24 @@ export async function login(log) {
|
|
|
244
240
|
}, CALLBACK_TIMEOUT_MS);
|
|
245
241
|
function cleanup() {
|
|
246
242
|
clearTimeout(timeout_timer);
|
|
247
|
-
|
|
243
|
+
callback_server.close();
|
|
248
244
|
}
|
|
249
|
-
|
|
250
|
-
|
|
245
|
+
// Try ports sequentially until one binds
|
|
246
|
+
async function bind_port() {
|
|
247
|
+
for (let p = CALLBACK_PORT_START; p <= CALLBACK_PORT_END; p++) {
|
|
248
|
+
if (await try_listen(callback_server, p)) {
|
|
249
|
+
bound_port = p;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
cleanup();
|
|
254
|
+
reject(new Error(`No available ports in range ${CALLBACK_PORT_START}-${CALLBACK_PORT_END}. ` +
|
|
255
|
+
'Please close applications using these ports and try again.'));
|
|
256
|
+
}
|
|
257
|
+
bind_port().then(() => {
|
|
258
|
+
const redirect_uri = `http://localhost:${bound_port}/callback`;
|
|
259
|
+
const auth_url = build_authorize_url(redirect_uri, pkce, state);
|
|
260
|
+
log(`Callback server listening on port ${bound_port}`);
|
|
251
261
|
// Try to open browser
|
|
252
262
|
const opened = open_browser(auth_url);
|
|
253
263
|
if (!opened || !process.env.DISPLAY && !process.env.BROWSER && platform() === 'linux') {
|
|
@@ -258,11 +268,7 @@ export async function login(log) {
|
|
|
258
268
|
log('Opening browser for authentication...');
|
|
259
269
|
}
|
|
260
270
|
log('Waiting for authentication (up to 5 minutes)...');
|
|
261
|
-
});
|
|
262
|
-
server.on('error', (err) => {
|
|
263
|
-
cleanup();
|
|
264
|
-
reject(new Error(`Callback server error: ${err.message}`));
|
|
265
|
-
});
|
|
271
|
+
}).catch(reject);
|
|
266
272
|
});
|
|
267
273
|
if (result === null) {
|
|
268
274
|
throw new Error('Authentication timed out. Please try again.');
|
|
@@ -273,6 +279,7 @@ export async function login(log) {
|
|
|
273
279
|
}
|
|
274
280
|
log('Authorization code received, exchanging for tokens...');
|
|
275
281
|
// Exchange code for tokens
|
|
282
|
+
const redirect_uri = `http://localhost:${result.port}/callback`;
|
|
276
283
|
const token_response = await exchange_code_for_tokens(result.code, pkce.code_verifier, redirect_uri);
|
|
277
284
|
const expires_in = token_response.expires_in ?? 3600;
|
|
278
285
|
// Fetch user info (optional, non-fatal)
|
package/dist/credentials.js
CHANGED
|
@@ -35,7 +35,7 @@ export function load_credentials() {
|
|
|
35
35
|
export function save_credentials(creds) {
|
|
36
36
|
try {
|
|
37
37
|
const dir = join(homedir(), '.voicemode');
|
|
38
|
-
mkdirSync(dir, { recursive: true });
|
|
38
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
39
39
|
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
40
40
|
}
|
|
41
41
|
catch {
|
package/dist/gateway.js
CHANGED
|
@@ -23,6 +23,9 @@ import { get_valid_token } from './credentials.js';
|
|
|
23
23
|
// Configuration
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
const WS_URL = process.env.VOICEMODE_CONNECT_WS_URL ?? 'wss://voicemode.dev/ws';
|
|
26
|
+
if (WS_URL.startsWith('ws://')) {
|
|
27
|
+
process.stderr.write('[voicemode-channel] WARNING: Gateway URL uses unencrypted ws:// -- tokens will be sent in cleartext\n');
|
|
28
|
+
}
|
|
26
29
|
const HEARTBEAT_INTERVAL_MS = 25_000;
|
|
27
30
|
const HEARTBEAT_LIVENESS_TIMEOUT_MS = 60_000; // Force-close if no pong received within this window
|
|
28
31
|
const INITIAL_RETRY_DELAY_MS = 1_000;
|
|
@@ -287,8 +290,8 @@ export class GatewayClient extends EventEmitter {
|
|
|
287
290
|
const display_name = profile?.display_name ?? process.env.VOICEMODE_AGENT_DISPLAY_NAME ?? 'Claude Code';
|
|
288
291
|
const presence = profile?.presence ?? 'available';
|
|
289
292
|
const host = hostname();
|
|
290
|
-
const project_path = process.cwd();
|
|
291
|
-
const context = profile?.context ?? get_project_context(
|
|
293
|
+
const project_path = basename(process.cwd()); // Send basename only -- don't leak full filesystem path
|
|
294
|
+
const context = profile?.context ?? get_project_context(process.cwd());
|
|
292
295
|
const user_entry = {
|
|
293
296
|
name: agent_name,
|
|
294
297
|
host,
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,9 @@ try {
|
|
|
38
38
|
continue;
|
|
39
39
|
const key = trimmed.slice(0, eq).trim();
|
|
40
40
|
const value = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
41
|
+
// Only allow VOICEMODE_ prefixed keys to prevent env var injection
|
|
42
|
+
if (!key.startsWith('VOICEMODE_'))
|
|
43
|
+
continue;
|
|
41
44
|
// Don't override existing env vars
|
|
42
45
|
if (!(key in process.env)) {
|
|
43
46
|
process.env[key] = value;
|
|
@@ -116,6 +119,7 @@ if (process.env.VOICEMODE_CHANNEL_ENABLED !== 'true') {
|
|
|
116
119
|
process.stderr.write('VoiceMode channel server disabled. Set VOICEMODE_CHANNEL_ENABLED=true to enable.\n');
|
|
117
120
|
process.exit(0);
|
|
118
121
|
}
|
|
122
|
+
const WS_URL = process.env.VOICEMODE_CONNECT_WS_URL ?? 'wss://voicemode.dev/ws';
|
|
119
123
|
const CHANNEL_NAME = 'voicemode-channel';
|
|
120
124
|
const CHANNEL_VERSION = '0.1.4';
|
|
121
125
|
const INSTRUCTIONS = [
|
|
@@ -183,6 +187,15 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
183
187
|
required: ['text'],
|
|
184
188
|
},
|
|
185
189
|
},
|
|
190
|
+
{
|
|
191
|
+
name: 'status',
|
|
192
|
+
description: 'Check the current status of the VoiceMode channel. ' +
|
|
193
|
+
'Returns connection state, gateway URL, session IDs, auth info, and current profile.',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
186
199
|
{
|
|
187
200
|
name: 'profile',
|
|
188
201
|
description: 'Get or update the agent profile. ' +
|
|
@@ -220,6 +233,9 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
220
233
|
});
|
|
221
234
|
mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
222
235
|
const { name, arguments: args } = request.params;
|
|
236
|
+
if (name === 'status') {
|
|
237
|
+
return handle_status_tool();
|
|
238
|
+
}
|
|
223
239
|
if (name === 'profile') {
|
|
224
240
|
return handle_profile_tool(args);
|
|
225
241
|
}
|
|
@@ -232,6 +248,50 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
232
248
|
};
|
|
233
249
|
});
|
|
234
250
|
// ---------------------------------------------------------------------------
|
|
251
|
+
// Tool handler: status
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
function handle_status_tool() {
|
|
254
|
+
const lines = [];
|
|
255
|
+
// Connection state
|
|
256
|
+
const state = gateway?.state ?? 'disconnected';
|
|
257
|
+
lines.push(`Connection: ${state}`);
|
|
258
|
+
lines.push(`Gateway: ${WS_URL}`);
|
|
259
|
+
// Session IDs
|
|
260
|
+
const server_session = gateway?.session_id ?? 'none';
|
|
261
|
+
const agent_session = gateway?.agent_session_id ?? 'none';
|
|
262
|
+
lines.push(`Session: ${server_session} (server) / ${agent_session} (agent)`);
|
|
263
|
+
lines.push('');
|
|
264
|
+
// Auth info
|
|
265
|
+
const creds = load_credentials();
|
|
266
|
+
if (creds) {
|
|
267
|
+
const expired = is_expired(creds);
|
|
268
|
+
const expires_date = new Date(creds.expires_at * 1000);
|
|
269
|
+
const expires_str = expires_date.toISOString().slice(0, 16);
|
|
270
|
+
lines.push(`Auth: authenticated`);
|
|
271
|
+
const name = creds.user_info?.name;
|
|
272
|
+
const email = creds.user_info?.email;
|
|
273
|
+
if (name || email) {
|
|
274
|
+
const parts = [name, email ? `(${email})` : null].filter(Boolean).join(' ');
|
|
275
|
+
lines.push(`User: ${parts}`);
|
|
276
|
+
}
|
|
277
|
+
lines.push(`Token: ${expired ? 'expired' : 'valid'} (expires ${expires_str})`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
lines.push('Auth: not authenticated');
|
|
281
|
+
}
|
|
282
|
+
lines.push('');
|
|
283
|
+
// Profile
|
|
284
|
+
lines.push(`Profile: set`);
|
|
285
|
+
lines.push(` Name: ${currentProfile.name}`);
|
|
286
|
+
lines.push(` Display: ${currentProfile.display_name}`);
|
|
287
|
+
lines.push(` Context: ${currentProfile.context ?? 'none'}`);
|
|
288
|
+
lines.push(` Voice: ${currentProfile.voice ?? 'default'}`);
|
|
289
|
+
lines.push(` Presence: ${currentProfile.presence}`);
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
235
295
|
// Tool handler: reply
|
|
236
296
|
// ---------------------------------------------------------------------------
|
|
237
297
|
function handle_reply_tool(args) {
|
|
@@ -293,15 +353,16 @@ function handle_profile_tool(args) {
|
|
|
293
353
|
}],
|
|
294
354
|
};
|
|
295
355
|
}
|
|
296
|
-
// Merge provided fields into current profile
|
|
356
|
+
// Merge provided fields into current profile (cap at 200 chars to prevent abuse)
|
|
357
|
+
const cap = (s) => s.slice(0, 200);
|
|
297
358
|
if (typeof args.name === 'string')
|
|
298
|
-
currentProfile.name = args.name;
|
|
359
|
+
currentProfile.name = cap(args.name);
|
|
299
360
|
if (typeof args.display_name === 'string')
|
|
300
|
-
currentProfile.display_name = args.display_name;
|
|
361
|
+
currentProfile.display_name = cap(args.display_name);
|
|
301
362
|
if (typeof args.context === 'string')
|
|
302
|
-
currentProfile.context = args.context;
|
|
363
|
+
currentProfile.context = cap(args.context);
|
|
303
364
|
if (typeof args.voice === 'string')
|
|
304
|
-
currentProfile.voice = args.voice;
|
|
365
|
+
currentProfile.voice = cap(args.voice);
|
|
305
366
|
if (args.presence === 'available' || args.presence === 'busy' || args.presence === 'away')
|
|
306
367
|
currentProfile.presence = args.presence;
|
|
307
368
|
log(`Profile updated: ${JSON.stringify(currentProfile)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voicemode-channel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "VoiceMode Channel - inbound voice calls to Claude Code via VoiceMode Connect",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"ws": "^8.18.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^
|
|
23
|
+
"@types/node": "^25.5.2",
|
|
24
24
|
"@types/ws": "^8.5.0",
|
|
25
25
|
"tsx": "^4.19.0",
|
|
26
|
-
"typescript": "^
|
|
26
|
+
"typescript": "^6.0.2"
|
|
27
27
|
}
|
|
28
28
|
}
|