rms-devremote 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.
Files changed (68) hide show
  1. package/README.md +154 -0
  2. package/dist/commands/attach.d.ts +2 -0
  3. package/dist/commands/attach.js +10 -0
  4. package/dist/commands/check.d.ts +2 -0
  5. package/dist/commands/check.js +210 -0
  6. package/dist/commands/clean.d.ts +2 -0
  7. package/dist/commands/clean.js +177 -0
  8. package/dist/commands/dashboard.d.ts +2 -0
  9. package/dist/commands/dashboard.js +57 -0
  10. package/dist/commands/link.d.ts +2 -0
  11. package/dist/commands/link.js +112 -0
  12. package/dist/commands/ping.d.ts +2 -0
  13. package/dist/commands/ping.js +21 -0
  14. package/dist/commands/setup.d.ts +2 -0
  15. package/dist/commands/setup.js +54 -0
  16. package/dist/commands/status.d.ts +2 -0
  17. package/dist/commands/status.js +65 -0
  18. package/dist/commands/unlink.d.ts +2 -0
  19. package/dist/commands/unlink.js +53 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +55 -0
  22. package/dist/server/auth.d.ts +6 -0
  23. package/dist/server/auth.js +32 -0
  24. package/dist/server/frontend.d.ts +4 -0
  25. package/dist/server/frontend.js +886 -0
  26. package/dist/server/index.d.ts +1 -0
  27. package/dist/server/index.js +283 -0
  28. package/dist/server/terminal.d.ts +14 -0
  29. package/dist/server/terminal.js +43 -0
  30. package/dist/services/battery-worker.d.ts +1 -0
  31. package/dist/services/battery-worker.js +2 -0
  32. package/dist/services/battery.d.ts +27 -0
  33. package/dist/services/battery.js +152 -0
  34. package/dist/services/config.d.ts +63 -0
  35. package/dist/services/config.js +84 -0
  36. package/dist/services/docker.d.ts +25 -0
  37. package/dist/services/docker.js +75 -0
  38. package/dist/services/hooks.d.ts +15 -0
  39. package/dist/services/hooks.js +111 -0
  40. package/dist/services/ntfy.d.ts +19 -0
  41. package/dist/services/ntfy.js +63 -0
  42. package/dist/services/process.d.ts +30 -0
  43. package/dist/services/process.js +90 -0
  44. package/dist/services/proxy-worker.d.ts +1 -0
  45. package/dist/services/proxy-worker.js +12 -0
  46. package/dist/services/proxy.d.ts +4 -0
  47. package/dist/services/proxy.js +195 -0
  48. package/dist/services/shell.d.ts +22 -0
  49. package/dist/services/shell.js +47 -0
  50. package/dist/services/tmux.d.ts +30 -0
  51. package/dist/services/tmux.js +74 -0
  52. package/dist/services/ttyd.d.ts +28 -0
  53. package/dist/services/ttyd.js +71 -0
  54. package/dist/setup-server/routes.d.ts +4 -0
  55. package/dist/setup-server/routes.js +177 -0
  56. package/dist/setup-server/server.d.ts +4 -0
  57. package/dist/setup-server/server.js +32 -0
  58. package/docker/docker-compose.yml +24 -0
  59. package/docker/ntfy/server.yml +6 -0
  60. package/package.json +61 -0
  61. package/scripts/claude-remote.sh +583 -0
  62. package/scripts/hooks/notify.sh +68 -0
  63. package/scripts/notify.sh +54 -0
  64. package/scripts/startup.sh +29 -0
  65. package/scripts/update-check.sh +25 -0
  66. package/src/setup-server/public/index.html +21 -0
  67. package/src/setup-server/public/setup.css +475 -0
  68. package/src/setup-server/public/setup.js +687 -0
@@ -0,0 +1,47 @@
1
+ import { execFileSync, execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ const execFileAsync = promisify(execFile);
4
+ /**
5
+ * Synchronously execute a command with arguments.
6
+ * Uses execFileSync to prevent shell injection.
7
+ * Returns trimmed stdout as a string.
8
+ */
9
+ export function run(cmd, args) {
10
+ const result = execFileSync(cmd, args, { encoding: 'utf8' });
11
+ return result.trim();
12
+ }
13
+ /**
14
+ * Asynchronously execute a command with arguments.
15
+ * Uses execFile (promisified) to prevent shell injection.
16
+ * Returns trimmed stdout as a string.
17
+ */
18
+ export async function runAsync(cmd, args) {
19
+ const { stdout } = await execFileAsync(cmd, args, { encoding: 'utf8' });
20
+ return stdout.trim();
21
+ }
22
+ /**
23
+ * Check whether a command exists in PATH.
24
+ * Returns true if found, false otherwise.
25
+ */
26
+ export function which(cmd) {
27
+ try {
28
+ run('which', [cmd]);
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ /**
36
+ * Check whether a given port is currently in use.
37
+ * Uses lsof to inspect open files/sockets.
38
+ */
39
+ export function isPortInUse(port) {
40
+ try {
41
+ run('lsof', ['-iTCP:' + port, '-sTCP:LISTEN', '-t']);
42
+ return true;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Check whether tmux is installed in PATH.
3
+ */
4
+ export declare function isTmuxInstalled(): boolean;
5
+ /**
6
+ * Check whether the devremote tmux session exists.
7
+ */
8
+ export declare function sessionExists(): boolean;
9
+ /**
10
+ * Create the devremote tmux session in detached mode.
11
+ */
12
+ export declare function createSession(): void;
13
+ /**
14
+ * Attach to the devremote tmux session.
15
+ * Spawns tmux with stdio: 'inherit' and waits for the child to exit.
16
+ */
17
+ export declare function attachSession(): void;
18
+ /**
19
+ * Kill a tmux session by name (defaults to devremote).
20
+ */
21
+ export declare function killSession(name?: string): void;
22
+ /**
23
+ * List all tmux sessions that include the devremote session name.
24
+ * Returns an array of matching session name strings.
25
+ */
26
+ export declare function listSessions(): string[];
27
+ /**
28
+ * Return the configured session name.
29
+ */
30
+ export declare function getSessionName(): string;
@@ -0,0 +1,74 @@
1
+ import { spawn } from 'child_process';
2
+ import { run } from './shell.js';
3
+ import { which } from './shell.js';
4
+ const SESSION_NAME = 'devremote';
5
+ /**
6
+ * Check whether tmux is installed in PATH.
7
+ */
8
+ export function isTmuxInstalled() {
9
+ return which('tmux');
10
+ }
11
+ /**
12
+ * Check whether the devremote tmux session exists.
13
+ */
14
+ export function sessionExists() {
15
+ try {
16
+ run('tmux', ['has-session', '-t', SESSION_NAME]);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ /**
24
+ * Create the devremote tmux session in detached mode.
25
+ */
26
+ export function createSession() {
27
+ run('tmux', ['new-session', '-d', '-s', SESSION_NAME]);
28
+ }
29
+ /**
30
+ * Attach to the devremote tmux session.
31
+ * Spawns tmux with stdio: 'inherit' and waits for the child to exit.
32
+ */
33
+ export function attachSession() {
34
+ const child = spawn('tmux', ['attach-session', '-t', SESSION_NAME], {
35
+ stdio: 'inherit',
36
+ });
37
+ child.on('exit', (code) => {
38
+ process.exit(code ?? 0);
39
+ });
40
+ }
41
+ /**
42
+ * Kill a tmux session by name (defaults to devremote).
43
+ */
44
+ export function killSession(name) {
45
+ const target = name ?? SESSION_NAME;
46
+ try {
47
+ run('tmux', ['kill-session', '-t', target]);
48
+ }
49
+ catch {
50
+ // Session may not exist — silently ignore
51
+ }
52
+ }
53
+ /**
54
+ * List all tmux sessions that include the devremote session name.
55
+ * Returns an array of matching session name strings.
56
+ */
57
+ export function listSessions() {
58
+ try {
59
+ const output = run('tmux', ['list-sessions', '-F', '#{session_name}']);
60
+ return output
61
+ .split('\n')
62
+ .map((s) => s.trim())
63
+ .filter((s) => s.includes(SESSION_NAME));
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ }
69
+ /**
70
+ * Return the configured session name.
71
+ */
72
+ export function getSessionName() {
73
+ return SESSION_NAME;
74
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Check whether the devremote server is currently running.
3
+ */
4
+ export declare function isServerRunning(): boolean;
5
+ /**
6
+ * Return the PID of the server process, or null.
7
+ */
8
+ export declare function getServerPid(): number | null;
9
+ /**
10
+ * Check whether the server port is available.
11
+ */
12
+ export declare function checkPortAvailable(): {
13
+ available: boolean;
14
+ blockingPid?: number;
15
+ };
16
+ /**
17
+ * Start the devremote terminal server as a detached process.
18
+ * Returns the PID.
19
+ */
20
+ export declare function startServer(): number;
21
+ /**
22
+ * Stop the devremote server.
23
+ */
24
+ export declare function stopServer(): void;
25
+ export declare const isTtydRunning: typeof isServerRunning;
26
+ export declare const startTtyd: typeof startServer;
27
+ export declare const stopTtyd: typeof stopServer;
28
+ export declare const getTtydPid: typeof getServerPid;
@@ -0,0 +1,71 @@
1
+ import { spawn } from 'child_process';
2
+ import { resolve, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { isPortInUse } from './shell.js';
5
+ import { findProcessByPort } from './process.js';
6
+ import { readConfig } from './config.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ /**
10
+ * Check whether the devremote server is currently running.
11
+ */
12
+ export function isServerRunning() {
13
+ const config = readConfig();
14
+ return isPortInUse(config.ttyd.port);
15
+ }
16
+ /**
17
+ * Return the PID of the server process, or null.
18
+ */
19
+ export function getServerPid() {
20
+ const config = readConfig();
21
+ return findProcessByPort(config.ttyd.port);
22
+ }
23
+ /**
24
+ * Check whether the server port is available.
25
+ */
26
+ export function checkPortAvailable() {
27
+ const config = readConfig();
28
+ const pid = findProcessByPort(config.ttyd.port);
29
+ if (pid === null) {
30
+ return { available: true };
31
+ }
32
+ return { available: false, blockingPid: pid };
33
+ }
34
+ /**
35
+ * Start the devremote terminal server as a detached process.
36
+ * Returns the PID.
37
+ */
38
+ export function startServer() {
39
+ const serverEntry = resolve(__dirname, '..', 'server', 'index.js');
40
+ const child = spawn('node', [serverEntry], {
41
+ detached: true,
42
+ stdio: 'ignore',
43
+ env: { ...process.env },
44
+ });
45
+ child.unref();
46
+ const pid = child.pid;
47
+ if (pid === undefined) {
48
+ throw new Error('Failed to start devremote server: no PID returned');
49
+ }
50
+ return pid;
51
+ }
52
+ /**
53
+ * Stop the devremote server.
54
+ */
55
+ export function stopServer() {
56
+ const config = readConfig();
57
+ const pid = findProcessByPort(config.ttyd.port);
58
+ if (pid !== null) {
59
+ try {
60
+ process.kill(pid, 'SIGTERM');
61
+ }
62
+ catch {
63
+ // Already dead
64
+ }
65
+ }
66
+ }
67
+ // Backwards-compatible aliases
68
+ export const isTtydRunning = isServerRunning;
69
+ export const startTtyd = startServer;
70
+ export const stopTtyd = stopServer;
71
+ export const getTtydPid = getServerPid;
@@ -0,0 +1,4 @@
1
+ import type { Express } from 'express';
2
+ import type { WebSocketServer } from 'ws';
3
+ export declare function broadcast(wss: WebSocketServer, type: string, data?: unknown): void;
4
+ export declare function setupRoutes(app: Express, wss: WebSocketServer): void;
@@ -0,0 +1,177 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { resolve, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { copyFileSync, mkdirSync, writeFileSync, existsSync } from 'fs';
5
+ import { hashSync } from 'bcryptjs';
6
+ import { ensureDataDir, writeConfig, writeEnv, DOCKER_COMPOSE_PATH, NTFY_CONFIG_PATH, NOTIFY_SCRIPT_PATH, HOOKS_PATH, PIN_HASH_PATH, } from '../services/config.js';
7
+ import { composeUp } from '../services/docker.js';
8
+ import { createAdminUser, sendNotification } from '../services/ntfy.js';
9
+ import { which } from '../services/shell.js';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ // ---------------------------------------------------------------------------
13
+ // Broadcast helper
14
+ // ---------------------------------------------------------------------------
15
+ export function broadcast(wss, type, data = {}) {
16
+ const message = JSON.stringify({ type, data });
17
+ wss.clients.forEach((client) => {
18
+ if (client.readyState === 1 /* OPEN */) {
19
+ client.send(message);
20
+ }
21
+ });
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Route setup
25
+ // ---------------------------------------------------------------------------
26
+ export function setupRoutes(app, wss) {
27
+ // GET /api/prereqs — check system dependencies
28
+ app.get('/api/prereqs', (_req, res) => {
29
+ res.json({
30
+ docker: which('docker'),
31
+ tmux: which('tmux'),
32
+ ttyd: which('ttyd'),
33
+ claude: which('claude'),
34
+ jq: which('jq'),
35
+ });
36
+ });
37
+ // POST /api/setup — run setup steps
38
+ app.post('/api/setup', async (req, res) => {
39
+ try {
40
+ const { terminalDomain, notifyDomain, tunnelToken, ttydPassword, email, pin, } = req.body;
41
+ // Step 1: Preparation
42
+ broadcast(wss, 'status', { message: 'Preparation des fichiers...' });
43
+ // Step 2: Ensure data directory
44
+ ensureDataDir();
45
+ // Step 3: Generate ntfy credentials
46
+ const ntfyTopic = `devremote-${randomBytes(6).toString('hex')}`;
47
+ const ntfyPassword = randomBytes(8).toString('hex');
48
+ // Step 4: Write config.json
49
+ writeConfig({
50
+ domains: {
51
+ terminal: terminalDomain,
52
+ notify: notifyDomain,
53
+ },
54
+ ttyd: {
55
+ port: 7681,
56
+ },
57
+ ntfy: {
58
+ port: 2586,
59
+ },
60
+ battery: {
61
+ lowThreshold: 20,
62
+ checkInterval: 60,
63
+ },
64
+ });
65
+ // Step 5: Write .env
66
+ writeEnv({
67
+ CLOUDFLARE_TUNNEL_TOKEN: tunnelToken,
68
+ TERMINAL_DOMAIN: terminalDomain,
69
+ NOTIFY_DOMAIN: notifyDomain,
70
+ TTYD_USER: email,
71
+ TTYD_PASSWORD: ttydPassword,
72
+ NTFY_TOPIC: ntfyTopic,
73
+ NTFY_ADMIN_PASSWORD: ntfyPassword,
74
+ NTFY_PORT: '2586',
75
+ });
76
+ // Step 6: Copy docker-compose.yml
77
+ const repoDockerDir = resolve(__dirname, '..', '..', 'docker');
78
+ const srcCompose = resolve(repoDockerDir, 'docker-compose.yml');
79
+ copyFileSync(srcCompose, DOCKER_COMPOSE_PATH);
80
+ // Step 7: Write ntfy/server.yml with actual domain
81
+ const ntfyDir = resolve(NTFY_CONFIG_PATH, '..');
82
+ if (!existsSync(ntfyDir)) {
83
+ mkdirSync(ntfyDir, { recursive: true });
84
+ }
85
+ const ntfyServerYml = [
86
+ `# ntfy server configuration`,
87
+ `base-url: https://${notifyDomain}`,
88
+ `listen-http: ":80"`,
89
+ `auth-default-access: "deny-all"`,
90
+ `behind-proxy: true`,
91
+ `cache-file: "/var/cache/ntfy/cache.db"`,
92
+ `auth-file: "/var/lib/ntfy/user.db"`,
93
+ `enable-web: false`,
94
+ ].join('\n') + '\n';
95
+ writeFileSync(NTFY_CONFIG_PATH, ntfyServerYml, 'utf8');
96
+ // Step 8: Copy notify.sh and chmod 755
97
+ const repoScriptsDir = resolve(__dirname, '..', '..', 'scripts');
98
+ const srcNotify = resolve(repoScriptsDir, 'notify.sh');
99
+ copyFileSync(srcNotify, NOTIFY_SCRIPT_PATH);
100
+ // chmod 755
101
+ const { chmodSync } = await import('fs');
102
+ chmodSync(NOTIFY_SCRIPT_PATH, 0o755);
103
+ // Step 9: Write hooks.json template
104
+ const hooksTemplate = {
105
+ hooks: {
106
+ PreToolUse: [
107
+ {
108
+ matcher: '',
109
+ hooks: [
110
+ {
111
+ type: 'command',
112
+ command: `${NOTIFY_SCRIPT_PATH} tool-use`,
113
+ },
114
+ ],
115
+ },
116
+ ],
117
+ Notification: [
118
+ {
119
+ matcher: '',
120
+ hooks: [
121
+ {
122
+ type: 'command',
123
+ command: `${NOTIFY_SCRIPT_PATH} notification`,
124
+ },
125
+ ],
126
+ },
127
+ ],
128
+ Stop: [
129
+ {
130
+ matcher: '',
131
+ hooks: [
132
+ {
133
+ type: 'command',
134
+ command: `${NOTIFY_SCRIPT_PATH} stop`,
135
+ },
136
+ ],
137
+ },
138
+ ],
139
+ },
140
+ };
141
+ writeFileSync(HOOKS_PATH, JSON.stringify(hooksTemplate, null, 2), 'utf8');
142
+ // Step 10: Hash PIN and save
143
+ const pinHash = hashSync(pin, 10);
144
+ writeFileSync(PIN_HASH_PATH, pinHash, 'utf8');
145
+ // Step 11: Start Docker
146
+ broadcast(wss, 'status', { message: 'Demarrage Docker...' });
147
+ composeUp();
148
+ // Step 12: Wait 5 seconds
149
+ await new Promise((r) => setTimeout(r, 5000));
150
+ // Step 13: Create ntfy admin user
151
+ broadcast(wss, 'status', { message: 'Creation utilisateur ntfy...' });
152
+ createAdminUser(ntfyPassword);
153
+ // Step 14: Done
154
+ broadcast(wss, 'complete', {});
155
+ res.json({ ok: true, ntfyTopic, ntfyPassword, notifyDomain });
156
+ }
157
+ catch (err) {
158
+ const message = err instanceof Error ? err.message : String(err);
159
+ broadcast(wss, 'error', { message });
160
+ res.status(500).json({ ok: false, error: message });
161
+ }
162
+ });
163
+ // POST /api/test-notification — send a test notification
164
+ app.post('/api/test-notification', async (_req, res) => {
165
+ try {
166
+ const ok = await sendNotification('Test rms-devremote -- les notifications marchent !');
167
+ res.json({ ok });
168
+ }
169
+ catch {
170
+ res.json({ ok: false });
171
+ }
172
+ });
173
+ // POST /api/done — signal setup complete
174
+ app.post('/api/done', (_req, res) => {
175
+ res.json({ ok: true });
176
+ });
177
+ }
@@ -0,0 +1,4 @@
1
+ export declare function getSetupUrl(): string;
2
+ export declare function startSetupServer(): Promise<{
3
+ close: () => void;
4
+ }>;
@@ -0,0 +1,32 @@
1
+ import { createServer } from 'http';
2
+ import { resolve, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import express from 'express';
5
+ import { WebSocketServer } from 'ws';
6
+ import { setupRoutes } from './routes.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const PORT = 3456;
10
+ export function getSetupUrl() {
11
+ return `http://localhost:${PORT}`;
12
+ }
13
+ export async function startSetupServer() {
14
+ const app = express();
15
+ app.use(express.json());
16
+ // Serve static files from src/setup-server/public/
17
+ // When running from dist/, go up two levels to reach the source tree
18
+ const publicDir = resolve(__dirname, '..', '..', 'src', 'setup-server', 'public');
19
+ app.use(express.static(publicDir));
20
+ const httpServer = createServer(app);
21
+ const wss = new WebSocketServer({ server: httpServer });
22
+ setupRoutes(app, wss);
23
+ await new Promise((resolvePromise) => {
24
+ httpServer.listen(PORT, '127.0.0.1', () => resolvePromise());
25
+ });
26
+ return {
27
+ close: () => {
28
+ httpServer.close();
29
+ wss.close();
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,24 @@
1
+ services:
2
+ tunnel:
3
+ image: cloudflare/cloudflared:latest
4
+ container_name: devremote-tunnel
5
+ restart: unless-stopped
6
+ command: tunnel run
7
+ environment:
8
+ - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
9
+
10
+ ntfy:
11
+ image: binwiederhier/ntfy:latest
12
+ container_name: devremote-ntfy
13
+ restart: unless-stopped
14
+ command: serve
15
+ ports:
16
+ - "127.0.0.1:${NTFY_PORT:-2586}:80"
17
+ volumes:
18
+ - ./ntfy/server.yml:/etc/ntfy/server.yml:ro
19
+ - ntfy-cache:/var/cache/ntfy
20
+ - ntfy-auth:/var/lib/ntfy
21
+
22
+ volumes:
23
+ ntfy-cache:
24
+ ntfy-auth:
@@ -0,0 +1,6 @@
1
+ listen-http: ":80"
2
+ auth-default-access: "deny-all"
3
+ behind-proxy: true
4
+ cache-file: "/var/cache/ntfy/cache.db"
5
+ auth-file: "/var/lib/ntfy/user.db"
6
+ enable-web: false
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "rms-devremote",
3
+ "version": "3.0.0",
4
+ "description": "Control your terminal remotely from your phone — mobile PWA with push notifications and zero open ports",
5
+ "type": "module",
6
+ "bin": {
7
+ "rms-devremote": "./dist/index.js",
8
+ "rdr": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "docker/",
13
+ "scripts/",
14
+ "src/setup-server/public/"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsc --watch",
19
+ "start": "node dist/index.js"
20
+ },
21
+ "keywords": [
22
+ "terminal",
23
+ "remote",
24
+ "tmux",
25
+ "mobile",
26
+ "pwa",
27
+ "xterm",
28
+ "claude",
29
+ "notifications"
30
+ ],
31
+ "author": "RMS Tech Studio",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/RMS-Tech-Studio/claude-remote.git"
36
+ },
37
+ "engines": {
38
+ "node": ">=20.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@clack/prompts": "^1.1.0",
42
+ "@xterm/addon-fit": "^0.11.0",
43
+ "@xterm/addon-web-links": "^0.12.0",
44
+ "@xterm/xterm": "^6.0.0",
45
+ "bcryptjs": "^3.0.3",
46
+ "chalk": "^5.6.2",
47
+ "commander": "^14.0.3",
48
+ "express": "^5.2.1",
49
+ "jsonwebtoken": "^9.0.3",
50
+ "node-pty": "^1.1.0",
51
+ "ws": "^8.20.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/bcryptjs": "^3.0.0",
55
+ "@types/express": "^5.0.6",
56
+ "@types/jsonwebtoken": "^9.0.10",
57
+ "@types/node": "^25.5.0",
58
+ "@types/ws": "^8.18.1",
59
+ "typescript": "^6.0.2"
60
+ }
61
+ }