@znmin/claude-remote-cli 0.2.6 → 0.2.7

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 CHANGED
@@ -25,10 +25,10 @@ npm install -g git+https://github.com/znmin/claude-remote-cli.git
25
25
  也可以从已下载的分发包安装:
26
26
 
27
27
  ```bash
28
- npm install -g ./znmin-claude-remote-cli-0.2.6.tgz
28
+ npm install -g ./znmin-claude-remote-cli-0.2.7.tgz
29
29
  ```
30
30
 
31
- 若系统 npm 全局目录不可写,可使用 `npm install -g --prefix "$HOME/.local" ./znmin-claude-remote-cli-0.2.6.tgz`,并确保 `~/.local/bin` 在 PATH 中;卸载时也使用相同的 `--prefix`。
31
+ 若系统 npm 全局目录不可写,可使用 `npm install -g --prefix "$HOME/.local" ./znmin-claude-remote-cli-0.2.7.tgz`,并确保 `~/.local/bin` 在 PATH 中;卸载时也使用相同的 `--prefix`。
32
32
 
33
33
  npm 中不带作用域的 `claude-remote-cli` 属于其他项目。本项目使用独立的 `@znmin/claude-remote-cli` 作用域包名;请安装完整包名,或使用上述 GitHub 地址及本项目分发的 `.tgz` 文件。
34
34
 
@@ -131,6 +131,7 @@ claude-remote configure my-server --mode local
131
131
  - 浏览器元素选择器在多次快照间保持不变;快照包含 iframe(选择器形如 `frame=1 >> …`)和开放的 Shadow DOM 元素;截图支持整页和单个元素。
132
132
  - 本地项目的 `CLAUDE.md`、`.claude/CLAUDE.md`、`CLAUDE.local.md` 在启动时读取并放入系统提示(合计最多 64 KiB),不再依赖模型自行读取。
133
133
  - 关闭终端窗口(SIGHUP)或被 `kill`(SIGTERM)时也会清理浏览器、后台命令、SSH 隧道和服务器临时文件。
134
+ - 0.2.7 起,SSH 断线等原因导致远程 Claude 异常结束时,会自动关闭它遗留的鼠标上报、焦点事件、括号粘贴和扩展键盘等终端模式,避免移动鼠标或按键时输出乱码。若仍显示异常(例如停留在全屏界面),可运行 `reset`。
134
135
  - 启动时浏览器检查与 SSH 连接并行,浏览器已就绪时不再调用 Playwright 安装程序;服务器上的版本检查和配置写入合并为一次往返。
135
136
 
136
137
  Git、npm、pnpm、Python、Go、Docker、数据库 CLI、`gh` 等共用本地终端,不需要逐项适配,仍需在本机安装并具备必要的登录、连接配置及权限。终端继承启动用户环境和登录 shell,默认工作目录为当前项目。长任务使用后台进程,再查询日志和停止;退出会话会停止仍受本工具管理的进程组,自行 daemonize 或逃离原进程组的程序不在自动清理保证内。
@@ -235,7 +236,7 @@ npm test
235
236
  CLAUDE_REMOTE_INTEGRATION=1 CLAUDE_REMOTE_BROWSER_TEST=1 npm test
236
237
 
237
238
  npm pack
238
- npm install -g ./znmin-claude-remote-cli-0.2.6.tgz
239
+ npm install -g ./znmin-claude-remote-cli-0.2.7.tgz
239
240
 
240
241
  # 确定包名、版本和使用条款后,使用有发布权限的 npm 账号发布
241
242
  npm login
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@znmin/claude-remote-cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Run Claude Code on an SSH server with local filesystem, terminal and browser tools, or remote bidirectional sync",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/client.js CHANGED
@@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import { createHash, randomUUID } from 'node:crypto';
5
- import { execute, exists, quote, readJSON, shellJoin, which, withLock, writeJSON } from './system.js';
5
+ import { execute, exists, quote, readJSON, restoreTerminal, shellJoin, which, withLock, writeJSON } from './system.js';
6
6
  import { ensureMutagen } from './installer.js';
7
7
  import { ensureRemoteClaude, remoteEnvironment as prefix } from './remote-claude.js';
8
8
  import { sshArgs } from './ssh.js';
@@ -67,6 +67,7 @@ export class Client {
67
67
  if (this.env.CLAUDE_REMOTE_DEBUG === '1') this.log(...messages);
68
68
  };
69
69
  this.tty = options.tty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
70
+ this.restoreTerminal = options.restoreTerminal || (() => restoreTerminal());
70
71
  }
71
72
 
72
73
  async configure(input) {
@@ -204,6 +205,8 @@ export class Client {
204
205
  try {
205
206
  code = (await this.remote(config, prefix + 'cd -- ' + remotePath(item.remote) +
206
207
  ' && exec ' + shellJoin([config.claude, ...extra]), true)).code;
208
+ // A dropped connection leaves the remote UI's terminal modes switched on.
209
+ if (code !== 0) this.restoreTerminal();
207
210
  } finally {
208
211
  try { await this.flush(item.name); }
209
212
  catch (error) {
package/src/local-mode.js CHANGED
@@ -169,7 +169,10 @@ export async function launchLocal(client, config, extra, dependencies = {}) {
169
169
  const script = remoteEnvironment + 'cd -- ' + remotePath(remoteDirectory) + ' || exit 1; ' +
170
170
  'trap ' + quote('rm -f -- ' + remotePath(configFile) + ' ' + remotePath(promptFile)) + ' EXIT; ' +
171
171
  shellJoin([config.claude, ...args]);
172
- return (await remote(script, { interactive: true, check: false })).code;
172
+ const code = (await remote(script, { interactive: true, check: false })).code;
173
+ // A dropped connection leaves the remote UI's terminal modes switched on.
174
+ if (code !== 0) client.restoreTerminal();
175
+ return code;
173
176
  } finally {
174
177
  removeCleanup();
175
178
  await cleanup();
package/src/system.js CHANGED
@@ -57,7 +57,7 @@ export function onCleanup(callback) {
57
57
  return () => cleanups.delete(callback);
58
58
  }
59
59
 
60
- export function terminate(signal, { exit = code => process.exit(code), graceMs = 10000 } = {}) {
60
+ export function terminate(signal, { exit = code => process.exit(code), graceMs = 10000, terminal = process.stdout } = {}) {
61
61
  if (terminating) return terminating;
62
62
  const code = 128 + (os.constants.signals[signal] || 15);
63
63
  terminating = (async () => {
@@ -71,11 +71,33 @@ export function terminate(signal, { exit = code => process.exit(code), graceMs =
71
71
  for (const child of trackedChildren) {
72
72
  try { child.kill('SIGTERM'); } catch { /* already gone */ }
73
73
  }
74
+ restoreTerminal(terminal);
74
75
  exit(code);
75
76
  })();
76
77
  return terminating;
77
78
  }
78
79
 
80
+ // A remote full-screen program that loses its connection cannot switch off the
81
+ // terminal modes it enabled, so mouse moves, focus changes and keys show up as
82
+ // stray escape codes. Every sequence here only turns a mode off and is a no-op
83
+ // when the mode is already off. The alternate screen (?1049l) is deliberately
84
+ // left alone: leaving it when not active can move the cursor on some terminals.
85
+ export const TERMINAL_RESET = [
86
+ '\x1b[?1000l', '\x1b[?1002l', '\x1b[?1003l', '\x1b[?1005l', '\x1b[?1006l', '\x1b[?1015l', // mouse reporting
87
+ '\x1b[?1004l', // focus in/out events
88
+ '\x1b[?2004l', // bracketed paste
89
+ '\x1b[=0;1u', '\x1b[<99u', // kitty keyboard protocol: clear flags, pop pushed levels
90
+ '\x1b[>4m', // xterm modifyOtherKeys
91
+ '\x1b[?1l', '\x1b>', // normal cursor keys and keypad
92
+ '\x1b[0m', '\x1b[?25h' // plain text attributes, visible cursor
93
+ ].join('');
94
+
95
+ export function restoreTerminal(stream = process.stdout) {
96
+ if (!stream?.isTTY) return false;
97
+ try { stream.write(TERMINAL_RESET); return true; }
98
+ catch { return false; } // The terminal may already be gone (SIGHUP).
99
+ }
100
+
79
101
  export function installSignalHandlers() {
80
102
  for (const signal of ['SIGHUP', 'SIGTERM']) process.on(signal, () => { terminate(signal); });
81
103
  }