swarmpath-claudecode-bridge 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.
Files changed (3) hide show
  1. package/README.md +160 -0
  2. package/index.mjs +527 -0
  3. package/package.json +24 -0
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # SwarmPath Claude Code Bridge v2.4.1
2
+
3
+ 手机远程控制 Mac 本地 Claude Code — 一行命令连接,自动登录,无需手动粘贴 Token。
4
+
5
+ ## 快速开始
6
+
7
+ ```bash
8
+ # 第一步:配置(仅首次)
9
+ npx swarmpath-claudecode-bridge --setup
10
+
11
+ # 第二步:启动(任意项目目录下)
12
+ cd ~/my-project
13
+ npx swarmpath-claudecode-bridge
14
+
15
+ # 第三步:手机 SwarmPath Chat 输入配对码
16
+ /connect XXXXXX
17
+ ```
18
+
19
+ ## 原理
20
+
21
+ ```
22
+ 手机 SwarmPath Chat SwarmPath 服务器 Mac 本地
23
+ | | |
24
+ | |<-- WebSocket --------| bridge 启动 + 自动登录
25
+ | |--- 配对码 A3X7 ----->| 终端显示
26
+ |-- /connect A3X7 -------->| 配对成功 |
27
+ | | |
28
+ |-- 发送消息 ------------->|-- ws 转发 ------------>|
29
+ | | | claude -p "..." --resume
30
+ |<-- 流式回显 -------------|<-- ws 流式回传 --------|
31
+ ```
32
+
33
+ ## 安装 & 配置
34
+
35
+ ### 前置条件
36
+
37
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)(终端输入 `claude --version` 验证)
38
+ - Node.js 18+
39
+ - SwarmPath Chat 账号
40
+
41
+ ### 首次配置
42
+
43
+ ```bash
44
+ npx swarmpath-claudecode-bridge --setup
45
+ ```
46
+
47
+ 交互式输入:
48
+ ```
49
+ 🔧 SwarmPath Claude Code Bridge — 初始配置
50
+
51
+ SwarmPath 服务器地址 [wss://www.swarmpathchat.com]:
52
+ 用户名: admin
53
+ 密码: ****
54
+
55
+ ✅ 配置已保存到 ~/.swarmpath-bridge.json
56
+ ✅ 登录成功!
57
+ ```
58
+
59
+ 配置保存到 `~/.swarmpath-bridge.json`,后续启动自动读取,无需重复配置。
60
+
61
+ ## 使用
62
+
63
+ ### 启动
64
+
65
+ ```bash
66
+ # 当前目录
67
+ npx swarmpath-claudecode-bridge
68
+
69
+ # 指定项目目录
70
+ npx swarmpath-claudecode-bridge --cwd /Users/apple/my-project
71
+ ```
72
+
73
+ 输出:
74
+ ```
75
+ 🌐 SwarmPath Claude Code Bridge
76
+ 📁 Working directory: /Users/apple/my-project
77
+ 🔑 自动登录成功
78
+
79
+ 🔗 Connecting to wss://www.swarmpathchat.com ...
80
+ ✅ Connected to SwarmPath server
81
+
82
+ ═══════════════════════════════════════════
83
+ 配对码 (Pairing Code): F8USY4
84
+ 有效期: 300 秒
85
+ ═══════════════════════════════════════════
86
+
87
+ 在 SwarmPath Chat 中输入: /connect F8USY4
88
+ ```
89
+
90
+ ### 手机端配对
91
+
92
+ 在 SwarmPath Chat 任意对话中输入:
93
+ ```
94
+ /connect F8USY4
95
+ ```
96
+
97
+ ### 对话
98
+
99
+ 在 Bridge session 中直接发消息:
100
+ - `帮我看看当前目录有什么文件`
101
+ - `帮我写一个 hello.py`
102
+ - `运行测试`
103
+ - `帮我把这个项目的架构图绘制出来`
104
+
105
+ ### 切换模型
106
+
107
+ 点击 header 模型名(Sonnet/Opus/Haiku)切换 Claude Code 使用的模型。
108
+
109
+ ### 切换工作目录
110
+
111
+ ```
112
+ /cd /Users/apple/another-project
113
+ /cd ..
114
+ ```
115
+
116
+ ### 重新连接
117
+
118
+ bridge 重启后,在同一个 Bridge session 中输入新配对码即可重连(不创建新 session,历史保留):
119
+ ```
120
+ /connect XXXXXX
121
+ ```
122
+
123
+ ## 参数
124
+
125
+ | 参数 | 说明 |
126
+ |------|------|
127
+ | `--setup` | 交互式首次配置 |
128
+ | `--cwd <path>` | 指定工作目录(默认当前目录) |
129
+ | `--server <url>` | 覆盖服务器地址 |
130
+ | `--token <jwt>` | 手动指定 token(跳过自动登录) |
131
+
132
+ 环境变量:
133
+ ```bash
134
+ export SWARMPATH_SERVER=wss://www.swarmpathchat.com
135
+ export SWARMPATH_TOKEN=eyJ...
136
+ ```
137
+
138
+ ## 特性
139
+
140
+ | 特性 | 说明 |
141
+ |------|------|
142
+ | **自动登录** | 首次 `--setup` 后,后续启动自动用存储的凭据登录 |
143
+ | **Token 自动续期** | 每 12 分钟自动刷新,无需手动干预 |
144
+ | **断线重连** | WebSocket 断开后自动重连(指数退避 5s → 60s) |
145
+ | **Token 过期自动恢复** | 4001 断开时自动重新登录并重连 |
146
+ | **长对话上下文** | `--resume` 复用 Claude session,多轮对话保持上下文 |
147
+ | **远程文件浏览** | 资源管理器远程浏览 Mac 文件(只读) |
148
+ | **`/cd` 切目录** | 动态切换工作目录,无需重启 |
149
+
150
+ ## 安全
151
+
152
+ - 凭据存储在本地 `~/.swarmpath-bridge.json`(权限 600 建议)
153
+ - 配对码 6 位加密随机,5 分钟有效,一次性
154
+ - 所有通信通过 WSS 加密
155
+ - 服务器仅转发,不存储文件内容
156
+ - 远程资源管理器只读
157
+
158
+ ## 退出
159
+
160
+ 终端按 `Ctrl+C` 优雅退出。
package/index.mjs ADDED
@@ -0,0 +1,527 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SwarmPath Claude Code Bridge
4
+ * Connect local Claude Code to SwarmPath Chat — control your Mac from your phone.
5
+ *
6
+ * Usage:
7
+ * npx swarmpath-claudecode-bridge --setup # First-time setup
8
+ * npx swarmpath-claudecode-bridge # Start (auto-login)
9
+ * npx swarmpath-claudecode-bridge --cwd /path/to/project # Specify working directory
10
+ * npx swarmpath-claudecode-bridge --server wss://... --token <jwt> # Manual token mode
11
+ */
12
+
13
+ import { spawn } from 'child_process';
14
+ import { resolve, join, extname } from 'path';
15
+ import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
16
+ import { homedir } from 'os';
17
+ import { createInterface } from 'readline';
18
+ import WebSocket from 'ws';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Config file
22
+ // ---------------------------------------------------------------------------
23
+ const CONFIG_PATH = join(homedir(), '.swarmpath-bridge.json');
24
+
25
+ function loadConfig() {
26
+ try {
27
+ if (existsSync(CONFIG_PATH)) return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
28
+ } catch {}
29
+ return {};
30
+ }
31
+
32
+ function saveConfig(cfg) {
33
+ writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf-8');
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Interactive setup
38
+ // ---------------------------------------------------------------------------
39
+ async function runSetup() {
40
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
41
+ const ask = (q) => new Promise(r => rl.question(q, r));
42
+
43
+ console.log('\n🔧 SwarmPath Claude Code Bridge — 初始配置\n');
44
+
45
+ const cfg = loadConfig();
46
+ const server = await ask(`SwarmPath 服务器地址 [${cfg.server || 'wss://www.swarmpathchat.com'}]: `);
47
+ const username = await ask(`用户名 [${cfg.username || ''}]: `);
48
+ const password = await ask('密码: ');
49
+
50
+ cfg.server = server.trim() || cfg.server || 'wss://www.swarmpathchat.com';
51
+ cfg.username = username.trim() || cfg.username || '';
52
+ cfg.password = password || cfg.password || '';
53
+
54
+ saveConfig(cfg);
55
+ console.log(`\n✅ 配置已保存到 ${CONFIG_PATH}`);
56
+
57
+ // Test login
58
+ try {
59
+ const tokens = await login(cfg.server, cfg.username, cfg.password);
60
+ cfg.accessToken = tokens.accessToken;
61
+ cfg.refreshToken = tokens.refreshToken;
62
+ saveConfig(cfg);
63
+ console.log('✅ 登录成功!\n');
64
+ console.log('现在可以运行:');
65
+ console.log(' npx swarmpath-claudecode-bridge');
66
+ console.log(' npx swarmpath-claudecode-bridge --cwd /path/to/project\n');
67
+ } catch (err) {
68
+ console.error(`❌ 登录失败: ${err.message}\n`);
69
+ }
70
+
71
+ rl.close();
72
+ process.exit(0);
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Auth: login & refresh
77
+ // ---------------------------------------------------------------------------
78
+ async function login(serverWs, username, password) {
79
+ const httpUrl = serverWs.replace('wss://', 'https://').replace('ws://', 'http://');
80
+ const res = await fetch(`${httpUrl}/auth/login`, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ username, password }),
84
+ });
85
+ if (!res.ok) {
86
+ const body = await res.json().catch(() => ({}));
87
+ throw new Error(body.error || `HTTP ${res.status}`);
88
+ }
89
+ return res.json();
90
+ }
91
+
92
+ async function refreshToken(serverWs, rToken) {
93
+ const httpUrl = serverWs.replace('wss://', 'https://').replace('ws://', 'http://');
94
+ const res = await fetch(`${httpUrl}/auth/refresh`, {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({ refreshToken: rToken }),
98
+ });
99
+ if (!res.ok) throw new Error('Refresh failed');
100
+ return res.json();
101
+ }
102
+
103
+ async function ensureToken(cfg) {
104
+ // Try existing access token
105
+ if (cfg.accessToken) return cfg.accessToken;
106
+
107
+ // Try refresh
108
+ if (cfg.refreshToken) {
109
+ try {
110
+ const tokens = await refreshToken(cfg.server, cfg.refreshToken);
111
+ cfg.accessToken = tokens.accessToken;
112
+ if (tokens.refreshToken) cfg.refreshToken = tokens.refreshToken;
113
+ saveConfig(cfg);
114
+ return cfg.accessToken;
115
+ } catch {}
116
+ }
117
+
118
+ // Re-login with stored credentials
119
+ if (cfg.username && cfg.password) {
120
+ const tokens = await login(cfg.server, cfg.username, cfg.password);
121
+ cfg.accessToken = tokens.accessToken;
122
+ cfg.refreshToken = tokens.refreshToken;
123
+ saveConfig(cfg);
124
+ return cfg.accessToken;
125
+ }
126
+
127
+ throw new Error('No valid credentials. Run: npx swarmpath-claudecode-bridge --setup');
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Parse CLI args
132
+ // ---------------------------------------------------------------------------
133
+ const cliArgs = process.argv.slice(2);
134
+ function getArg(name) {
135
+ const idx = cliArgs.indexOf(`--${name}`);
136
+ return idx >= 0 && idx + 1 < cliArgs.length ? cliArgs[idx + 1] : null;
137
+ }
138
+ const hasFlag = (name) => cliArgs.includes(`--${name}`);
139
+
140
+ // Handle --setup
141
+ if (hasFlag('setup')) {
142
+ await runSetup();
143
+ }
144
+
145
+ // Resolve config
146
+ const cfg = loadConfig();
147
+ const SERVER = getArg('server') || cfg.server || process.env.SWARMPATH_SERVER;
148
+ let TOKEN = getArg('token') || process.env.SWARMPATH_TOKEN;
149
+ let CWD = getArg('cwd') || process.cwd();
150
+
151
+ if (!SERVER) {
152
+ console.error('❌ 未配置服务器。请先运行: npx swarmpath-claudecode-bridge --setup');
153
+ process.exit(1);
154
+ }
155
+
156
+ // Auto-login if no token provided
157
+ if (!TOKEN) {
158
+ try {
159
+ TOKEN = await ensureToken(cfg);
160
+ console.log('🔑 自动登录成功');
161
+ } catch (err) {
162
+ console.error(`❌ ${err.message}`);
163
+ process.exit(1);
164
+ }
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // State
169
+ // ---------------------------------------------------------------------------
170
+ let ws = null;
171
+ let reconnectTimer = null;
172
+ let heartbeatTimer = null;
173
+ let tokenRefreshTimer = null;
174
+ let activeChild = null;
175
+ let activeRequestId = null;
176
+ let claudeSessionId = null;
177
+ let shuttingDown = false;
178
+
179
+ const HEARTBEAT_MS = 30_000;
180
+ const RECONNECT_MS = 5_000;
181
+ const MAX_RECONNECT_MS = 60_000;
182
+ const TOKEN_REFRESH_MS = 12 * 60 * 1000; // Refresh token every 12 min (before 15-min expiry)
183
+ let reconnectDelay = RECONNECT_MS;
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Token auto-refresh
187
+ // ---------------------------------------------------------------------------
188
+ function startTokenRefresh() {
189
+ clearInterval(tokenRefreshTimer);
190
+ tokenRefreshTimer = setInterval(async () => {
191
+ try {
192
+ TOKEN = await ensureToken(cfg);
193
+ console.log('🔑 Token 已自动续期');
194
+ } catch (err) {
195
+ console.error('⚠️ Token 续期失败:', err.message);
196
+ }
197
+ }, TOKEN_REFRESH_MS);
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // WebSocket connection
202
+ // ---------------------------------------------------------------------------
203
+ function connect() {
204
+ const cwdParam = encodeURIComponent(CWD);
205
+ const url = `${SERVER}/ws/bridge?token=${TOKEN}&cwd=${cwdParam}`;
206
+
207
+ console.log(`\n🔗 Connecting to ${SERVER} ...`);
208
+ ws = new WebSocket(url);
209
+
210
+ ws.on('open', () => {
211
+ console.log('✅ Connected to SwarmPath server');
212
+ reconnectDelay = RECONNECT_MS;
213
+
214
+ clearInterval(heartbeatTimer);
215
+ heartbeatTimer = setInterval(() => {
216
+ if (ws?.readyState === WebSocket.OPEN) {
217
+ ws.send(JSON.stringify({ type: 'heartbeat' }));
218
+ }
219
+ }, HEARTBEAT_MS);
220
+
221
+ ws.send(JSON.stringify({ type: 'cwd', cwd: CWD }));
222
+ });
223
+
224
+ ws.on('message', (raw) => {
225
+ try {
226
+ const msg = JSON.parse(raw.toString());
227
+ handleMessage(msg);
228
+ } catch (e) {
229
+ console.error('Failed to parse message:', e.message);
230
+ }
231
+ });
232
+
233
+ ws.on('close', async (code, reason) => {
234
+ const reasonStr = reason?.toString() || 'none';
235
+ console.log(`🔌 Disconnected (code=${code}, reason=${reasonStr})`);
236
+ clearInterval(heartbeatTimer);
237
+
238
+ // If token expired, auto-refresh and reconnect
239
+ if (code === 4001 && !shuttingDown) {
240
+ console.log('🔑 Token 过期,尝试自动续期...');
241
+ try {
242
+ TOKEN = await ensureToken(cfg);
243
+ console.log('🔑 续期成功,重新连接...');
244
+ reconnectDelay = RECONNECT_MS;
245
+ setTimeout(() => connect(), 1000);
246
+ return;
247
+ } catch (err) {
248
+ console.error('❌ 续期失败:', err.message);
249
+ }
250
+ }
251
+
252
+ if (!shuttingDown) scheduleReconnect();
253
+ });
254
+
255
+ ws.on('error', (err) => {
256
+ console.error('WebSocket error:', err.message);
257
+ });
258
+ }
259
+
260
+ function scheduleReconnect() {
261
+ if (reconnectTimer) return;
262
+ console.log(`⏳ Reconnecting in ${reconnectDelay / 1000}s ...`);
263
+ reconnectTimer = setTimeout(() => {
264
+ reconnectTimer = null;
265
+ reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_MS);
266
+ connect();
267
+ }, reconnectDelay);
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Message handling
272
+ // ---------------------------------------------------------------------------
273
+ function handleMessage(msg) {
274
+ switch (msg.type) {
275
+ case 'paired_code':
276
+ console.log('');
277
+ console.log('═══════════════════════════════════════════');
278
+ console.log(` 配对码 (Pairing Code): ${msg.code}`);
279
+ console.log(` 有效期: ${msg.expiresIn || 300} 秒`);
280
+ console.log('═══════════════════════════════════════════');
281
+ console.log('');
282
+ console.log('在 SwarmPath Chat 中输入: /connect ' + msg.code);
283
+ console.log('');
284
+ break;
285
+
286
+ case 'paired':
287
+ console.log(`🤝 配对成功! (bridgeId: ${msg.bridgeId})`);
288
+ console.log(`📁 工作目录: ${CWD}`);
289
+ console.log('等待指令...\n');
290
+ break;
291
+
292
+ case 'prompt':
293
+ console.log(`📨 收到指令 [${msg.requestId}]${msg.model ? ' (' + msg.model + ')' : ''}: ${msg.prompt.slice(0, 80)}${msg.prompt.length > 80 ? '...' : ''}`);
294
+ executePrompt(msg.requestId, msg.prompt, msg.model);
295
+ break;
296
+
297
+ case 'abort':
298
+ console.log(`⛔ 收到中止指令 [${msg.requestId}]`);
299
+ if (activeChild && activeRequestId === msg.requestId) {
300
+ activeChild.kill('SIGTERM');
301
+ setTimeout(() => { if (activeChild && !activeChild.killed) activeChild.kill('SIGKILL'); }, 3000);
302
+ }
303
+ break;
304
+
305
+ case 'file_request':
306
+ handleFileRequest(msg);
307
+ break;
308
+
309
+ case 'error':
310
+ console.error('Server error:', msg.message);
311
+ break;
312
+ }
313
+ }
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // Execute Claude CLI
317
+ // ---------------------------------------------------------------------------
318
+ function executePrompt(requestId, prompt, model) {
319
+ if (activeChild && activeRequestId) {
320
+ const prevReqId = activeRequestId;
321
+ activeChild.kill('SIGTERM');
322
+ wsSend({ type: 'stream_error', requestId: prevReqId, error: 'Superseded by new request' });
323
+ }
324
+
325
+ activeRequestId = requestId;
326
+
327
+ const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions'];
328
+ if (claudeSessionId) args.push('--resume', claudeSessionId);
329
+ if (model) args.push('--model', model);
330
+
331
+ const child = spawn('claude', args, {
332
+ cwd: CWD,
333
+ stdio: ['pipe', 'pipe', 'pipe'],
334
+ env: { ...process.env },
335
+ });
336
+
337
+ activeChild = child;
338
+ wsSend({ type: 'stream_start', requestId });
339
+
340
+ let buffer = '';
341
+ let hasSentDelta = false;
342
+
343
+ child.stdout.on('data', (chunk) => {
344
+ buffer += chunk.toString();
345
+ const lines = buffer.split('\n');
346
+ buffer = lines.pop() || '';
347
+
348
+ for (const line of lines) {
349
+ if (!line.trim()) continue;
350
+ try {
351
+ const event = JSON.parse(line);
352
+ if (event.session_id && !claudeSessionId) {
353
+ claudeSessionId = event.session_id;
354
+ console.log(` 📎 Session ID: ${claudeSessionId}`);
355
+ }
356
+ hasSentDelta = processClaudeEvent(requestId, event, hasSentDelta) || hasSentDelta;
357
+ } catch {
358
+ if (line.trim()) {
359
+ wsSend({ type: 'stream_delta', requestId, content: line + '\n' });
360
+ hasSentDelta = true;
361
+ }
362
+ }
363
+ }
364
+ });
365
+
366
+ const MAX_STDERR = 64 * 1024;
367
+ let stderrBuf = '';
368
+ child.stderr.on('data', (chunk) => {
369
+ if (stderrBuf.length < MAX_STDERR) {
370
+ stderrBuf += chunk.toString();
371
+ if (stderrBuf.length > MAX_STDERR) stderrBuf = stderrBuf.slice(0, MAX_STDERR);
372
+ }
373
+ });
374
+
375
+ child.on('close', (code) => {
376
+ if (buffer.trim()) {
377
+ try {
378
+ const event = JSON.parse(buffer);
379
+ hasSentDelta = processClaudeEvent(requestId, event, hasSentDelta) || hasSentDelta;
380
+ } catch {
381
+ if (buffer.trim()) wsSend({ type: 'stream_delta', requestId, content: buffer });
382
+ }
383
+ }
384
+ if (activeRequestId === requestId) {
385
+ if (code === 0) wsSend({ type: 'stream_done', requestId });
386
+ else wsSend({ type: 'stream_error', requestId, error: stderrBuf.trim() || `Claude exited with code ${code}` });
387
+ activeChild = null;
388
+ activeRequestId = null;
389
+ }
390
+ console.log(` ✓ 完成 [${requestId}] (exit=${code})`);
391
+ });
392
+
393
+ child.on('error', (err) => {
394
+ console.error(` ✗ 失败 [${requestId}]:`, err.message);
395
+ wsSend({ type: 'stream_error', requestId, error: err.message });
396
+ if (activeRequestId === requestId) { activeChild = null; activeRequestId = null; }
397
+ });
398
+ }
399
+
400
+ function processClaudeEvent(requestId, event, hasSentDelta) {
401
+ if (event.type === 'content_block_delta' && event.delta?.text) {
402
+ wsSend({ type: 'stream_delta', requestId, content: event.delta.text });
403
+ return true;
404
+ }
405
+ if (event.type === 'assistant' && event.message?.content) {
406
+ let sent = false;
407
+ for (const block of event.message.content) {
408
+ if (block.type === 'text' && block.text) {
409
+ if (hasSentDelta) wsSend({ type: 'stream_delta', requestId, content: '\n\n' });
410
+ wsSend({ type: 'stream_delta', requestId, content: block.text });
411
+ sent = true;
412
+ }
413
+ }
414
+ return sent || hasSentDelta;
415
+ }
416
+ if (event.type === 'result' && !hasSentDelta) {
417
+ const text = typeof event.result === 'string' ? event.result : event.result?.text || '';
418
+ if (text) { wsSend({ type: 'stream_delta', requestId, content: text }); return true; }
419
+ }
420
+ return false;
421
+ }
422
+
423
+ // ---------------------------------------------------------------------------
424
+ // File operations
425
+ // ---------------------------------------------------------------------------
426
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.next', '__pycache__', '.cache', 'dist', '.DS_Store']);
427
+ const MIME_MAP = {
428
+ '.js': 'text/javascript', '.mjs': 'text/javascript', '.ts': 'text/plain', '.tsx': 'text/plain',
429
+ '.json': 'application/json', '.md': 'text/markdown', '.txt': 'text/plain', '.html': 'text/html',
430
+ '.css': 'text/css', '.py': 'text/plain', '.sh': 'text/plain', '.yaml': 'text/yaml', '.yml': 'text/yaml',
431
+ '.xml': 'text/xml', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg',
432
+ '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.pdf': 'application/pdf',
433
+ };
434
+
435
+ function handleFileRequest(msg) {
436
+ const { requestId, action } = msg;
437
+ try {
438
+ if (action === 'cd') {
439
+ const target = msg.path || '..';
440
+ const newCwd = resolve(CWD, target);
441
+ const st = statSync(newCwd);
442
+ if (!st.isDirectory()) throw new Error('Not a directory');
443
+ CWD = newCwd;
444
+ wsSend({ type: 'cwd', cwd: CWD });
445
+ console.log(` 📂 切换目录: ${CWD}`);
446
+ wsSend({ type: 'file_response', requestId, data: { cwd: CWD } });
447
+ } else if (action === 'tree') {
448
+ const tree = buildTree(CWD, '', msg.depth || 3, !!msg.showHidden);
449
+ wsSend({ type: 'file_response', requestId, data: { cwd: CWD, tree } });
450
+ } else if (action === 'read') {
451
+ const relPath = msg.path;
452
+ if (!relPath) throw new Error('path required');
453
+ const fullPath = resolve(CWD, relPath);
454
+ if (!fullPath.startsWith(resolve(CWD) + '/') && fullPath !== resolve(CWD)) throw new Error('Access denied');
455
+ const ext = extname(fullPath).toLowerCase();
456
+ const mime = MIME_MAP[ext] || 'application/octet-stream';
457
+ const isText = mime.startsWith('text/') || mime === 'application/json' || mime === 'text/markdown';
458
+ if (isText) {
459
+ wsSend({ type: 'file_response', requestId, data: { content: readFileSync(fullPath, 'utf-8'), mimeType: mime, encoding: 'utf8' } });
460
+ } else {
461
+ const buf = readFileSync(fullPath);
462
+ if (buf.length > 2 * 1024 * 1024) throw new Error('File too large');
463
+ wsSend({ type: 'file_response', requestId, data: { content: buf.toString('base64'), mimeType: mime, encoding: 'base64' } });
464
+ }
465
+ } else {
466
+ throw new Error(`Unknown action: ${action}`);
467
+ }
468
+ } catch (err) {
469
+ wsSend({ type: 'file_response', requestId, error: err.message || String(err) });
470
+ }
471
+ }
472
+
473
+ function buildTree(dir, relPrefix, maxDepth, showHidden, depth = 0) {
474
+ if (depth >= maxDepth) return [];
475
+ const entries = [];
476
+ try {
477
+ for (const item of readdirSync(dir, { withFileTypes: true })) {
478
+ if (!showHidden && item.name.startsWith('.')) continue;
479
+ if (SKIP_DIRS.has(item.name)) continue;
480
+ const relPath = relPrefix ? relPrefix + '/' + item.name : item.name;
481
+ if (item.isDirectory()) {
482
+ entries.push({ name: item.name, path: relPath, isDir: true, children: buildTree(join(dir, item.name), relPath, maxDepth, showHidden, depth + 1) });
483
+ } else {
484
+ try { entries.push({ name: item.name, path: relPath, isDir: false, size: statSync(join(dir, item.name)).size }); }
485
+ catch { entries.push({ name: item.name, path: relPath, isDir: false, size: 0 }); }
486
+ }
487
+ }
488
+ } catch {}
489
+ entries.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1));
490
+ return entries;
491
+ }
492
+
493
+ // ---------------------------------------------------------------------------
494
+ // Helpers
495
+ // ---------------------------------------------------------------------------
496
+ function wsSend(data) {
497
+ if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(data));
498
+ }
499
+
500
+ // ---------------------------------------------------------------------------
501
+ // Graceful shutdown
502
+ // ---------------------------------------------------------------------------
503
+ function shutdown() {
504
+ if (shuttingDown) return;
505
+ shuttingDown = true;
506
+ console.log('\n🛑 Shutting down...');
507
+ clearInterval(heartbeatTimer);
508
+ clearInterval(tokenRefreshTimer);
509
+ clearTimeout(reconnectTimer);
510
+ if (activeChild) {
511
+ activeChild.kill('SIGTERM');
512
+ setTimeout(() => { if (activeChild && !activeChild.killed) activeChild.kill('SIGKILL'); }, 2000);
513
+ }
514
+ if (ws) ws.close(1000, 'Shutdown');
515
+ setTimeout(() => process.exit(0), 3000);
516
+ }
517
+
518
+ process.on('SIGINT', shutdown);
519
+ process.on('SIGTERM', shutdown);
520
+
521
+ // ---------------------------------------------------------------------------
522
+ // Start
523
+ // ---------------------------------------------------------------------------
524
+ console.log('🌐 SwarmPath Claude Code Bridge');
525
+ console.log(`📁 Working directory: ${CWD}`);
526
+ startTokenRefresh();
527
+ connect();
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "swarmpath-claudecode-bridge",
3
+ "version": "1.0.0",
4
+ "description": "Connect local Claude Code to SwarmPath Chat — control your Mac from your phone",
5
+ "type": "module",
6
+ "bin": {
7
+ "swarmpath-claudecode-bridge": "./index.mjs"
8
+ },
9
+ "keywords": [
10
+ "claude-code",
11
+ "swarmpath",
12
+ "bridge",
13
+ "remote-control",
14
+ "ai-agent"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/SwarmPathAI/swarmpath-agent-chat"
19
+ },
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "ws": "^8.19.0"
23
+ }
24
+ }