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.
Files changed (3) hide show
  1. package/README.md +37 -21
  2. package/index.mjs +102 -2
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,33 +1,45 @@
1
- # SwarmPath Claude Code Bridge v2.4.1
1
+ # SwarmPath Claude Code Bridge v2.5
2
2
 
3
- 手机远程控制 Mac 本地 Claude Code — 一行命令连接,自动登录,无需手动粘贴 Token。
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
- 手机 SwarmPath Chat SwarmPath 服务器 Mac 本地
23
- | | |
24
- | |<-- WebSocket --------| bridge 启动 + 自动登录
25
- | |--- 配对码 A3X7 ----->| 终端显示
26
- |-- /connect A3X7 -------->| 配对成功 |
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.0.0",
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
  }