@znmin/claude-remote-cli 0.3.1 → 0.3.2
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 +7 -6
- package/package.json +2 -2
- package/src/cli.js +23 -7
- package/src/local/browser.js +66 -10
- package/src/local/server.js +1 -1
- package/src/local-mode.js +1 -1
package/README.md
CHANGED
|
@@ -25,14 +25,14 @@ 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.3.
|
|
28
|
+
npm install -g ./znmin-claude-remote-cli-0.3.2.tgz
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
若系统 npm 全局目录不可写,可使用 `npm install -g --prefix "$HOME/.local" ./znmin-claude-remote-cli-0.3.
|
|
31
|
+
若系统 npm 全局目录不可写,可使用 `npm install -g --prefix "$HOME/.local" ./znmin-claude-remote-cli-0.3.2.tgz`,并确保 `~/.local/bin` 在 PATH 中;卸载时也使用相同的 `--prefix`。
|
|
32
32
|
|
|
33
33
|
npm 中不带作用域的 `claude-remote-cli` 属于其他项目。本项目使用独立的 `@znmin/claude-remote-cli` 作用域包名;请安装完整包名,或使用上述 GitHub 地址及本项目分发的 `.tgz` 文件。
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
依赖按模式准备:本地模式优先使用本机已安装的 Google Chrome,其次 Microsoft Edge(Windows 10/11 自带),都没有时首次启动下载 Playwright Chromium;服务器模式首次启动准备 Mutagen 0.18.1。Mutagen 复用 PATH 中的匹配版本,或从官方 GitHub Release 下载并校验内置 SHA-256。浏览器和同步组件需要网络和额外磁盘空间;npm 安装本身不强制下载这些二进制文件。
|
|
36
36
|
|
|
37
37
|
### Windows(0.3.0)
|
|
38
38
|
|
|
@@ -160,7 +160,7 @@ Git、npm、pnpm、Python、Go、Docker、数据库 CLI、`gh` 等共用本地
|
|
|
160
160
|
|
|
161
161
|
0.2.5 起,普通公开资料查询优先使用服务器上的 Claude 原生 `WebSearch` / `WebFetch`,不打开本地浏览器;服务器的文件、终端和浏览器执行工具仍不开放。原生搜索/网页读取是否可用取决于服务器 Claude 的账号、服务商和策略;如果不可用或查询失败,会先说明并征求用户意见,再回退本地浏览器。本地开发页面、localhost/局域网地址、交互测试、截图和交互登录仍使用本地浏览器,不交给服务器 WebFetch。该选择通过工具白名单及提示词约定实现,不是对模型每次路由选择的硬性拦截。
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
浏览器使用独立窗口和临时上下文,即使使用本机 Chrome/Edge 也不读取日常的书签、登录态和 Cookie。默认依次选择本机 Chrome、Edge,都没有时启动本地模式下载 Playwright Chromium 并缓存;可用 `CLAUDE_REMOTE_BROWSER` 指定。自动选择的本机浏览器无法启动时会提示并改用 Chromium。收到浏览器工具调用时才打开窗口。可以打开本机 localhost,填写表单、点击、截图和读取控制台错误。
|
|
164
164
|
|
|
165
165
|
```bash
|
|
166
166
|
claude-remote install-browser
|
|
@@ -187,7 +187,7 @@ claude-remote flush # 等待当前项目完成一次同步
|
|
|
187
187
|
claude-remote stop # 停止当前目录在当前配置服务器上的同步
|
|
188
188
|
claude-remote stop --all # 停止本工具的所有同步会话
|
|
189
189
|
claude-remote stop --name 名称 # 按 status 中的名称停止旧服务器上的映射
|
|
190
|
-
claude-remote doctor # 检查本地安装(含 Node 版本、SSH
|
|
190
|
+
claude-remote doctor # 检查本地安装(含 Node 版本、SSH 工具、使用的本地浏览器)和服务器配置,不连接服务器
|
|
191
191
|
claude-remote install-sync # 手动重试同步工具准备
|
|
192
192
|
claude-remote --help
|
|
193
193
|
```
|
|
@@ -229,6 +229,7 @@ CLAUDE_REMOTE_DEBUG=1 claude
|
|
|
229
229
|
| 变量 | 用途 |
|
|
230
230
|
| --- | --- |
|
|
231
231
|
| `CLAUDE_REMOTE_MODE` | 临时选择 `local` 或 `remote` |
|
|
232
|
+
| `CLAUDE_REMOTE_BROWSER` | 本地浏览器:`chrome`、`msedge` 或 `chromium`(Playwright 自带);未设置时按 Chrome、Edge、Chromium 顺序自动选择 |
|
|
232
233
|
| `CLAUDE_REMOTE_HEADLESS` | `1` 无窗口浏览器,`0` 可见窗口,覆盖配置 |
|
|
233
234
|
| `CLAUDE_REMOTE_HOST` | 临时 SSH 别名或 user@host |
|
|
234
235
|
| `CLAUDE_REMOTE_PORT` | 临时 SSH 端口 |
|
|
@@ -256,7 +257,7 @@ npm run test:windows # Windows 上运行的客户端测试(CI 在 windows-la
|
|
|
256
257
|
CLAUDE_REMOTE_INTEGRATION=1 CLAUDE_REMOTE_BROWSER_TEST=1 npm test
|
|
257
258
|
|
|
258
259
|
npm pack
|
|
259
|
-
npm install -g ./znmin-claude-remote-cli-0.3.
|
|
260
|
+
npm install -g ./znmin-claude-remote-cli-0.3.2.tgz
|
|
260
261
|
|
|
261
262
|
# 确定包名、版本和使用条款后,使用有发布权限的 npm 账号发布
|
|
262
263
|
npm login
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@znmin/claude-remote-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Run Claude Code on an SSH server from macOS, Linux or Windows with local filesystem, terminal and browser tools, or remote bidirectional sync",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"postinstall": "node scripts/postinstall.js",
|
|
32
32
|
"lint": "eslint .",
|
|
33
33
|
"test": "node --test test/*.test.js",
|
|
34
|
-
"test:windows": "node --test test/windows.test.js test/local-tools.test.js",
|
|
34
|
+
"test:windows": "node --test test/windows.test.js test/local-tools.test.js test/browser-choice.test.js",
|
|
35
35
|
"prepublishOnly": "npm run lint && npm test"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
package/src/cli.js
CHANGED
|
@@ -68,6 +68,16 @@ export function executionOptions(args) {
|
|
|
68
68
|
return { mode, forwarded };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Mirrors Client: configure saves new setups as local; a saved server without
|
|
72
|
+
// a mode predates local mode and stays remote.
|
|
73
|
+
export function defaultModeLabel(config, env = {}) {
|
|
74
|
+
if (env.CLAUDE_REMOTE_MODE) return `${env.CLAUDE_REMOTE_MODE}(环境变量 CLAUDE_REMOTE_MODE)`;
|
|
75
|
+
if (config.mode) return config.mode;
|
|
76
|
+
if (config.host) return 'remote(旧配置)';
|
|
77
|
+
if (env.CLAUDE_REMOTE_HOST) return 'remote(未保存配置)';
|
|
78
|
+
return 'local(配置服务器后的默认值)';
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
export async function main(asClaude, args = process.argv.slice(2), client = new Client()) {
|
|
72
82
|
if (asClaude) {
|
|
73
83
|
const options = executionOptions(args);
|
|
@@ -114,8 +124,9 @@ export async function main(asClaude, args = process.argv.slice(2), client = new
|
|
|
114
124
|
}
|
|
115
125
|
if (action === 'install-sync') { console.log('同步工具:' + await ensureMutagen({ env: client.env })); return 0; }
|
|
116
126
|
if (action === 'install-browser') {
|
|
117
|
-
const { installBrowser } = await import('./local/browser.js');
|
|
118
|
-
await installBrowser(client.log, { debug: client.debug
|
|
127
|
+
const { browserName, installBrowser } = await import('./local/browser.js');
|
|
128
|
+
const choice = await installBrowser(client.log, { debug: client.debug, env: client.env });
|
|
129
|
+
console.log(`浏览器已就绪:${browserName(choice)}(${choice.file})${choice.kind === 'chromium' ? '' : ',无需下载'}`); return 0;
|
|
119
130
|
}
|
|
120
131
|
if (action === 'doctor') {
|
|
121
132
|
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
@@ -131,16 +142,21 @@ export async function main(asClaude, args = process.argv.slice(2), client = new
|
|
|
131
142
|
for (const line of await conflictReport(client.env)) console.log(line);
|
|
132
143
|
}
|
|
133
144
|
try {
|
|
134
|
-
const { browserReady } = await import('./local/browser.js');
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
const { browserName, browserReady, chooseBrowser } = await import('./local/browser.js');
|
|
146
|
+
const choice = await chooseBrowser(client.env);
|
|
147
|
+
if (choice.kind !== 'chromium') console.log(`本地浏览器:${browserName(choice)}(${choice.file}),无需下载`);
|
|
148
|
+
else {
|
|
149
|
+
const { chromium } = await import('playwright');
|
|
150
|
+
const ready = await browserReady();
|
|
151
|
+
console.log(`本地浏览器:${browserName(choice)},${ready ? chromium.executablePath() : '尚未准备(本地模式首次启动或 claude-remote install-browser 时下载)'}` +
|
|
152
|
+
(choice.explicit ? '' : ';安装 Chrome 或 Edge 后可直接使用'));
|
|
153
|
+
}
|
|
138
154
|
} catch (error) { console.log('本地浏览器:无法检查(' + error.message + ')'); }
|
|
139
155
|
const cached = cachedBinary(client.env);
|
|
140
156
|
console.log(`同步工具:${client.env.CLAUDE_REMOTE_MUTAGEN || (await exists(cached) ? cached : await which('mutagen', client.env.PATH)) || '首次使用时自动下载'}(目标版本 ${VERSION})`);
|
|
141
157
|
const config = await readJSON(client.configFile, {});
|
|
142
158
|
console.log(`配置:${client.configFile}\n服务器:${client.env.CLAUDE_REMOTE_HOST || config.host || '尚未配置'}`);
|
|
143
|
-
console.log(`默认模式:${client.env
|
|
159
|
+
console.log(`默认模式:${defaultModeLabel(config, client.env)}`);
|
|
144
160
|
return 0;
|
|
145
161
|
}
|
|
146
162
|
throw new Error(`未知命令 ${action}。运行 claude-remote --help 查看用法。`);
|
package/src/local/browser.js
CHANGED
|
@@ -21,7 +21,48 @@ export async function browserReady() {
|
|
|
21
21
|
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const BROWSER_NAMES = { chrome: 'Google Chrome', msedge: 'Microsoft Edge', chromium: 'Chromium(Playwright 自带)' };
|
|
25
|
+
|
|
26
|
+
// Standard install locations of Chrome and Edge. Windows checks per-machine
|
|
27
|
+
// and per-user installs; Edge ships with Windows 10/11.
|
|
28
|
+
export function systemBrowserCandidates(kind, env = process.env, platform = process.platform, home = os.homedir()) {
|
|
29
|
+
if (platform === 'win32') {
|
|
30
|
+
const relative = kind === 'chrome' ? ['Google', 'Chrome', 'Application', 'chrome.exe'] : ['Microsoft', 'Edge', 'Application', 'msedge.exe'];
|
|
31
|
+
const bases = [env.ProgramFiles, env.ProgramW6432, env['ProgramFiles(x86)'], env.LOCALAPPDATA].filter(Boolean);
|
|
32
|
+
return [...new Set(bases.map(base => path.win32.join(base, ...relative)))];
|
|
33
|
+
}
|
|
34
|
+
if (platform === 'darwin') {
|
|
35
|
+
const app = kind === 'chrome' ? 'Google Chrome' : 'Microsoft Edge';
|
|
36
|
+
return ['/Applications', path.join(home, 'Applications')].map(directory => path.join(directory, app + '.app', 'Contents', 'MacOS', app));
|
|
37
|
+
}
|
|
38
|
+
return kind === 'chrome' ? ['/opt/google/chrome/chrome'] : ['/opt/microsoft/msedge/msedge'];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// CLAUDE_REMOTE_BROWSER picks one explicitly; otherwise an installed Chrome,
|
|
42
|
+
// then Edge, and only without either the downloaded Chromium.
|
|
43
|
+
export async function chooseBrowser(env = process.env, { platform = process.platform, home = os.homedir(),
|
|
44
|
+
fileExists = file => exists(file).catch(() => false) } = {}) {
|
|
45
|
+
const wanted = (env.CLAUDE_REMOTE_BROWSER || '').trim().toLowerCase();
|
|
46
|
+
if (wanted && !(wanted in BROWSER_NAMES)) throw new Error('CLAUDE_REMOTE_BROWSER 只能是 chrome、msedge 或 chromium。');
|
|
47
|
+
if (wanted === 'chromium') return { kind: 'chromium', explicit: true };
|
|
48
|
+
for (const kind of wanted ? [wanted] : ['chrome', 'msedge'])
|
|
49
|
+
for (const file of systemBrowserCandidates(kind, env, platform, home))
|
|
50
|
+
if (await fileExists(file)) return { kind, file, explicit: Boolean(wanted) };
|
|
51
|
+
if (wanted) throw new Error(`找不到 ${BROWSER_NAMES[wanted]}。请安装后重试,或去掉 CLAUDE_REMOTE_BROWSER 改为自动选择。`);
|
|
52
|
+
return { kind: 'chromium', explicit: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const browserName = choice => BROWSER_NAMES[choice.kind];
|
|
56
|
+
|
|
57
|
+
// Returns the browser to launch. A system browser needs no download.
|
|
58
|
+
export async function installBrowser(log = console.error, { debug = () => {}, force = false, env = process.env } = {}) {
|
|
59
|
+
const choice = await chooseBrowser(env);
|
|
60
|
+
if (choice.kind !== 'chromium') { debug(`使用系统浏览器:${browserName(choice)}(${choice.file})`); return choice; }
|
|
61
|
+
await installChromium(log, { debug, force });
|
|
62
|
+
return { ...choice, file: chromium.executablePath() };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function installChromium(log = console.error, { debug = () => {}, force = false } = {}) {
|
|
25
66
|
if (!force && await browserReady()) { debug('浏览器缓存可用:' + chromium.executablePath()); return; }
|
|
26
67
|
if (force) preparation = null;
|
|
27
68
|
if (!preparation) preparation = (async () => {
|
|
@@ -80,19 +121,34 @@ function collectElements({ limit, textLimit }) {
|
|
|
80
121
|
}
|
|
81
122
|
|
|
82
123
|
export class Browser {
|
|
83
|
-
constructor({ headless = false, log = console.error } = {}) {
|
|
84
|
-
this.headless = headless; this.log = log; this.pages = new Map(); this.logs = new Map();
|
|
124
|
+
constructor({ headless = false, log = console.error, env = process.env } = {}) {
|
|
125
|
+
this.headless = headless; this.log = log; this.env = env; this.pages = new Map(); this.logs = new Map();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async launchChromium() {
|
|
129
|
+
await installChromium(this.log);
|
|
130
|
+
try { return await chromium.launch({ headless: this.headless }); }
|
|
131
|
+
catch (error) {
|
|
132
|
+
// The cache marker may be stale (e.g. browsers removed by hand): reinstall once.
|
|
133
|
+
if (!/Executable doesn't exist|executable/i.test(error.message)) throw error;
|
|
134
|
+
await installChromium(this.log, { force: true });
|
|
135
|
+
return chromium.launch({ headless: this.headless });
|
|
136
|
+
}
|
|
85
137
|
}
|
|
86
138
|
|
|
87
139
|
async start() {
|
|
88
140
|
if (!this.starting) this.starting = (async () => {
|
|
89
|
-
await installBrowser(this.log);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
141
|
+
const choice = await installBrowser(this.log, { env: this.env });
|
|
142
|
+
if (choice.kind === 'chromium') this.browser = await this.launchChromium();
|
|
143
|
+
else {
|
|
144
|
+
// Each launch uses a fresh temporary profile, never the user's own.
|
|
145
|
+
try { this.browser = await chromium.launch({ headless: this.headless, executablePath: choice.file }); }
|
|
146
|
+
catch (error) {
|
|
147
|
+
const reason = error.message.split('\n')[0];
|
|
148
|
+
if (choice.explicit) throw new Error(`无法启动 ${browserName(choice)}:${reason}`);
|
|
149
|
+
this.log(`无法启动 ${browserName(choice)},改用 Playwright 自带 Chromium:${reason}`);
|
|
150
|
+
this.browser = await this.launchChromium();
|
|
151
|
+
}
|
|
96
152
|
}
|
|
97
153
|
this.browser.on('disconnected', () => { this.starting = null; this.context = null; this.browser = null; this.pages.clear(); this.logs.clear(); });
|
|
98
154
|
this.context = await this.browser.newContext({ viewport: { width: 1280, height: 800 } });
|
package/src/local/server.js
CHANGED
|
@@ -44,7 +44,7 @@ const selector = z.string().min(1).max(4000);
|
|
|
44
44
|
export async function startLocalServer({ root, env = process.env, headless = false, browser, log = console.error } = {}) {
|
|
45
45
|
const workspace = new Workspace(await fs.realpath(root));
|
|
46
46
|
const processes = new Processes(workspace, env);
|
|
47
|
-
browser ||= new Browser({ headless, log });
|
|
47
|
+
browser ||= new Browser({ headless, log, env });
|
|
48
48
|
const token = randomBytes(32).toString('hex');
|
|
49
49
|
const transports = new Set();
|
|
50
50
|
let closing = false;
|
package/src/local-mode.js
CHANGED
|
@@ -129,7 +129,7 @@ export async function launchLocal(client, config, extra, dependencies = {}) {
|
|
|
129
129
|
const prompt = localPrompt(root, { instructions: await projectInstructions(root), appended: extractAppendedPrompts(extra).prompts,
|
|
130
130
|
platform: client.platform });
|
|
131
131
|
// Prepare the browser while SSH connects; it is only needed before Claude starts.
|
|
132
|
-
const browserReady = (dependencies.installBrowser || installBrowser)(client.log, { debug: client.debug });
|
|
132
|
+
const browserReady = (dependencies.installBrowser || installBrowser)(client.log, { debug: client.debug, env: client.env });
|
|
133
133
|
browserReady.catch(() => {}); // Reported when awaited below.
|
|
134
134
|
let gateway, tunnel, configFile, promptFile, cleaning;
|
|
135
135
|
const hash = createHash('sha256').update(config.client_id + '\0' + root).digest('hex').slice(0, 24);
|