swarmpath-claudecode-bridge 1.0.0 → 1.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 +37 -21
- package/index.mjs +102 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,33 +1,45 @@
|
|
|
1
|
-
# SwarmPath Claude Code Bridge v2.
|
|
1
|
+
# SwarmPath Claude Code Bridge v2.5
|
|
2
2
|
|
|
3
|
-
手机远程控制 Mac 本地 Claude Code —
|
|
3
|
+
手机远程控制 Mac 本地 Claude Code — 一行命令连接,自动登录,支持 Chat + Terminal 双模式。
|
|
4
4
|
|
|
5
5
|
## 快速开始
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# 第一步:配置(仅首次)
|
|
9
|
-
npx swarmpath-claudecode-bridge --setup
|
|
9
|
+
npx swarmpath-claudecode-bridge@latest --setup
|
|
10
10
|
|
|
11
11
|
# 第二步:启动(任意项目目录下)
|
|
12
12
|
cd ~/my-project
|
|
13
|
-
npx swarmpath-claudecode-bridge
|
|
13
|
+
npx swarmpath-claudecode-bridge@latest
|
|
14
14
|
|
|
15
15
|
# 第三步:手机 SwarmPath Chat 输入配对码
|
|
16
16
|
/connect XXXXXX
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
npm 包地址:https://www.npmjs.com/package/swarmpath-claudecode-bridge
|
|
20
|
+
|
|
21
|
+
## 双模式
|
|
22
|
+
|
|
23
|
+
### Chat 模式(默认)
|
|
24
|
+
|
|
25
|
+
在 Bridge session 中发消息,Claude Code 以 `claude -p` 非交互模式执行,流式返回结果。适合手机端使用。
|
|
26
|
+
|
|
27
|
+
### Terminal 模式(v2.5 新增)
|
|
28
|
+
|
|
29
|
+
点击 header 栏的 `>_` 按钮切换到 Terminal 模式,在网页中嵌入真正的终端(xterm.js),通过 PTY 双向通信,获得和本地终端完全一致的 Claude Code 体验:
|
|
30
|
+
|
|
31
|
+
- 完整交互式 TUI(进度条、颜色、动画)
|
|
32
|
+
- 支持 `/` 命令(`/help`、`/compact` 等)
|
|
33
|
+
- 窗口 resize 自适应
|
|
34
|
+
- 建议在桌面端使用(手机屏幕较小)
|
|
20
35
|
|
|
21
36
|
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|-- 发送消息 ------------->|-- ws 转发 ------------>|
|
|
29
|
-
| | | claude -p "..." --resume
|
|
30
|
-
|<-- 流式回显 -------------|<-- ws 流式回传 --------|
|
|
37
|
+
手机/桌面 xterm.js SwarmPath 服务器 Mac 本地
|
|
38
|
+
| | |
|
|
39
|
+
|-- ws {input} --------------->|-- ws terminal_input ->|
|
|
40
|
+
| | | node-pty → claude 交互模式
|
|
41
|
+
|<-- ws {data} ----------------|<-- ws terminal_data --|
|
|
42
|
+
|-- ws {resize} -------------->|-- ws terminal_resize >|
|
|
31
43
|
```
|
|
32
44
|
|
|
33
45
|
## 安装 & 配置
|
|
@@ -35,13 +47,13 @@ npx swarmpath-claudecode-bridge
|
|
|
35
47
|
### 前置条件
|
|
36
48
|
|
|
37
49
|
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)(终端输入 `claude --version` 验证)
|
|
38
|
-
- Node.js 18
|
|
50
|
+
- Node.js 18+(Terminal 模式需要编译 `node-pty` 原生模块)
|
|
39
51
|
- SwarmPath Chat 账号
|
|
40
52
|
|
|
41
53
|
### 首次配置
|
|
42
54
|
|
|
43
55
|
```bash
|
|
44
|
-
npx swarmpath-claudecode-bridge --setup
|
|
56
|
+
npx swarmpath-claudecode-bridge@latest --setup
|
|
45
57
|
```
|
|
46
58
|
|
|
47
59
|
交互式输入:
|
|
@@ -64,10 +76,10 @@ SwarmPath 服务器地址 [wss://www.swarmpathchat.com]:
|
|
|
64
76
|
|
|
65
77
|
```bash
|
|
66
78
|
# 当前目录
|
|
67
|
-
npx swarmpath-claudecode-bridge
|
|
79
|
+
npx swarmpath-claudecode-bridge@latest
|
|
68
80
|
|
|
69
81
|
# 指定项目目录
|
|
70
|
-
npx swarmpath-claudecode-bridge --cwd /Users/apple/my-project
|
|
82
|
+
npx swarmpath-claudecode-bridge@latest --cwd /Users/apple/my-project
|
|
71
83
|
```
|
|
72
84
|
|
|
73
85
|
输出:
|
|
@@ -94,13 +106,16 @@ npx swarmpath-claudecode-bridge --cwd /Users/apple/my-project
|
|
|
94
106
|
/connect F8USY4
|
|
95
107
|
```
|
|
96
108
|
|
|
97
|
-
###
|
|
109
|
+
### Chat 模式对话
|
|
98
110
|
|
|
99
111
|
在 Bridge session 中直接发消息:
|
|
100
112
|
- `帮我看看当前目录有什么文件`
|
|
101
113
|
- `帮我写一个 hello.py`
|
|
102
114
|
- `运行测试`
|
|
103
|
-
|
|
115
|
+
|
|
116
|
+
### Terminal 模式
|
|
117
|
+
|
|
118
|
+
点击 header 右侧 `>_` 图标切换到 Terminal 模式,直接使用 Claude Code 交互式界面。
|
|
104
119
|
|
|
105
120
|
### 切换模型
|
|
106
121
|
|
|
@@ -139,13 +154,14 @@ export SWARMPATH_TOKEN=eyJ...
|
|
|
139
154
|
|
|
140
155
|
| 特性 | 说明 |
|
|
141
156
|
|------|------|
|
|
157
|
+
| **Chat + Terminal 双模式** | Chat 模式适合手机,Terminal 模式提供完整 TUI 体验 |
|
|
142
158
|
| **自动登录** | 首次 `--setup` 后,后续启动自动用存储的凭据登录 |
|
|
143
159
|
| **Token 自动续期** | 每 12 分钟自动刷新,无需手动干预 |
|
|
144
160
|
| **断线重连** | WebSocket 断开后自动重连(指数退避 5s → 60s) |
|
|
145
161
|
| **Token 过期自动恢复** | 4001 断开时自动重新登录并重连 |
|
|
146
162
|
| **长对话上下文** | `--resume` 复用 Claude session,多轮对话保持上下文 |
|
|
147
163
|
| **远程文件浏览** | 资源管理器远程浏览 Mac 文件(只读) |
|
|
148
|
-
| **`/cd` 切目录** |
|
|
164
|
+
| **`/cd` 切目录** | 动态切换工作目录,自动重置 session |
|
|
149
165
|
|
|
150
166
|
## 安全
|
|
151
167
|
|
package/index.mjs
CHANGED
|
@@ -10,13 +10,17 @@
|
|
|
10
10
|
* npx swarmpath-claudecode-bridge --server wss://... --token <jwt> # Manual token mode
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { spawn } from 'child_process';
|
|
13
|
+
import { spawn, execSync as _execSync } from 'child_process';
|
|
14
14
|
import { resolve, join, extname } from 'path';
|
|
15
15
|
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
16
16
|
import { homedir } from 'os';
|
|
17
17
|
import { createInterface } from 'readline';
|
|
18
18
|
import WebSocket from 'ws';
|
|
19
19
|
|
|
20
|
+
// node-pty for terminal mode (optional — gracefully degrade if not available)
|
|
21
|
+
let pty = null;
|
|
22
|
+
try { pty = (await import('node-pty')).default || (await import('node-pty')); } catch {};
|
|
23
|
+
|
|
20
24
|
// ---------------------------------------------------------------------------
|
|
21
25
|
// Config file
|
|
22
26
|
// ---------------------------------------------------------------------------
|
|
@@ -176,6 +180,9 @@ let activeRequestId = null;
|
|
|
176
180
|
let claudeSessionId = null;
|
|
177
181
|
let shuttingDown = false;
|
|
178
182
|
|
|
183
|
+
/** Terminal PTY sessions: requestId → pty instance */
|
|
184
|
+
const terminalSessions = new Map();
|
|
185
|
+
|
|
179
186
|
const HEARTBEAT_MS = 30_000;
|
|
180
187
|
const RECONNECT_MS = 5_000;
|
|
181
188
|
const MAX_RECONNECT_MS = 60_000;
|
|
@@ -238,6 +245,7 @@ function connect() {
|
|
|
238
245
|
// If token expired, auto-refresh and reconnect
|
|
239
246
|
if (code === 4001 && !shuttingDown) {
|
|
240
247
|
console.log('🔑 Token 过期,尝试自动续期...');
|
|
248
|
+
cfg.accessToken = null; // Clear expired token so ensureToken fetches a new one
|
|
241
249
|
try {
|
|
242
250
|
TOKEN = await ensureToken(cfg);
|
|
243
251
|
console.log('🔑 续期成功,重新连接...');
|
|
@@ -306,6 +314,22 @@ function handleMessage(msg) {
|
|
|
306
314
|
handleFileRequest(msg);
|
|
307
315
|
break;
|
|
308
316
|
|
|
317
|
+
case 'terminal_start':
|
|
318
|
+
handleTerminalStart(msg);
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case 'terminal_input':
|
|
322
|
+
handleTerminalInput(msg);
|
|
323
|
+
break;
|
|
324
|
+
|
|
325
|
+
case 'terminal_resize':
|
|
326
|
+
handleTerminalResize(msg);
|
|
327
|
+
break;
|
|
328
|
+
|
|
329
|
+
case 'terminal_stop':
|
|
330
|
+
handleTerminalStop(msg);
|
|
331
|
+
break;
|
|
332
|
+
|
|
309
333
|
case 'error':
|
|
310
334
|
console.error('Server error:', msg.message);
|
|
311
335
|
break;
|
|
@@ -387,6 +411,7 @@ function executePrompt(requestId, prompt, model) {
|
|
|
387
411
|
activeChild = null;
|
|
388
412
|
activeRequestId = null;
|
|
389
413
|
}
|
|
414
|
+
if (code !== 0 && stderrBuf.trim()) console.error(` ⚠ stderr [${requestId}]: ${stderrBuf.trim().slice(0, 200)}`);
|
|
390
415
|
console.log(` ✓ 完成 [${requestId}] (exit=${code})`);
|
|
391
416
|
});
|
|
392
417
|
|
|
@@ -420,6 +445,76 @@ function processClaudeEvent(requestId, event, hasSentDelta) {
|
|
|
420
445
|
return false;
|
|
421
446
|
}
|
|
422
447
|
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// Terminal PTY operations
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
function handleTerminalStart(msg) {
|
|
452
|
+
const { requestId, cols, rows } = msg;
|
|
453
|
+
if (!pty) {
|
|
454
|
+
wsSend({ type: 'terminal_exit', requestId, exitCode: -1, error: 'node-pty not available' });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// Kill existing terminal for this requestId if any
|
|
458
|
+
if (terminalSessions.has(requestId)) {
|
|
459
|
+
try { terminalSessions.get(requestId).kill(); } catch {}
|
|
460
|
+
terminalSessions.delete(requestId);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Resolve full path to claude — node-pty doesn't search PATH like shell
|
|
464
|
+
let claudePath = 'claude';
|
|
465
|
+
try {
|
|
466
|
+
claudePath = _execSync('which claude', { encoding: 'utf-8' }).trim() || 'claude';
|
|
467
|
+
} catch {}
|
|
468
|
+
|
|
469
|
+
console.log(` 🖥 Terminal start [${requestId}] (${cols}x${rows}) → ${claudePath}`);
|
|
470
|
+
let term;
|
|
471
|
+
try {
|
|
472
|
+
term = pty.spawn(claudePath, [], {
|
|
473
|
+
name: 'xterm-256color',
|
|
474
|
+
cols: cols || 120,
|
|
475
|
+
rows: rows || 30,
|
|
476
|
+
cwd: CWD,
|
|
477
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
478
|
+
});
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error(` 🖥 Terminal spawn failed: ${err.message}`);
|
|
481
|
+
wsSend({ type: 'terminal_exit', requestId, exitCode: -1, error: err.message });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
terminalSessions.set(requestId, term);
|
|
485
|
+
|
|
486
|
+
term.onData((data) => {
|
|
487
|
+
wsSend({ type: 'terminal_data', requestId, data });
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
term.onExit(({ exitCode }) => {
|
|
491
|
+
console.log(` 🖥 Terminal exit [${requestId}] (code=${exitCode})`);
|
|
492
|
+
terminalSessions.delete(requestId);
|
|
493
|
+
wsSend({ type: 'terminal_exit', requestId, exitCode });
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function handleTerminalInput(msg) {
|
|
498
|
+
const term = terminalSessions.get(msg.requestId);
|
|
499
|
+
if (term) term.write(msg.data);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function handleTerminalResize(msg) {
|
|
503
|
+
const term = terminalSessions.get(msg.requestId);
|
|
504
|
+
if (term && msg.cols && msg.rows) {
|
|
505
|
+
try { term.resize(msg.cols, msg.rows); } catch {}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function handleTerminalStop(msg) {
|
|
510
|
+
const term = terminalSessions.get(msg.requestId);
|
|
511
|
+
if (term) {
|
|
512
|
+
console.log(` 🖥 Terminal stop [${msg.requestId}]`);
|
|
513
|
+
try { term.kill(); } catch {}
|
|
514
|
+
terminalSessions.delete(msg.requestId);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
423
518
|
// ---------------------------------------------------------------------------
|
|
424
519
|
// File operations
|
|
425
520
|
// ---------------------------------------------------------------------------
|
|
@@ -441,8 +536,9 @@ function handleFileRequest(msg) {
|
|
|
441
536
|
const st = statSync(newCwd);
|
|
442
537
|
if (!st.isDirectory()) throw new Error('Not a directory');
|
|
443
538
|
CWD = newCwd;
|
|
539
|
+
claudeSessionId = null; // Reset session — old session is tied to previous cwd
|
|
444
540
|
wsSend({ type: 'cwd', cwd: CWD });
|
|
445
|
-
console.log(` 📂 切换目录: ${CWD}`);
|
|
541
|
+
console.log(` 📂 切换目录: ${CWD} (session reset)`);
|
|
446
542
|
wsSend({ type: 'file_response', requestId, data: { cwd: CWD } });
|
|
447
543
|
} else if (action === 'tree') {
|
|
448
544
|
const tree = buildTree(CWD, '', msg.depth || 3, !!msg.showHidden);
|
|
@@ -511,6 +607,10 @@ function shutdown() {
|
|
|
511
607
|
activeChild.kill('SIGTERM');
|
|
512
608
|
setTimeout(() => { if (activeChild && !activeChild.killed) activeChild.kill('SIGKILL'); }, 2000);
|
|
513
609
|
}
|
|
610
|
+
for (const [, term] of terminalSessions) {
|
|
611
|
+
try { term.kill(); } catch {}
|
|
612
|
+
}
|
|
613
|
+
terminalSessions.clear();
|
|
514
614
|
if (ws) ws.close(1000, 'Shutdown');
|
|
515
615
|
setTimeout(() => process.exit(0), 3000);
|
|
516
616
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarmpath-claudecode-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Connect local Claude Code to SwarmPath Chat — control your Mac from your phone",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
|
+
"node-pty": "^1.2.0-beta.12",
|
|
22
23
|
"ws": "^8.19.0"
|
|
23
24
|
}
|
|
24
25
|
}
|