@znmin/claude-remote-cli 0.2.3 → 0.2.5
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 +19 -7
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -2
- package/src/cli.js +4 -2
- package/src/client.js +13 -6
- package/src/installer.js +1 -1
- package/src/local/browser.js +3 -2
- package/src/local/server.js +1 -1
- package/src/local-mode.js +7 -4
- package/src/ssh-auth.js +5 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# claude-remote-cli
|
|
2
2
|
|
|
3
|
-
在项目目录运行 `claude`,让 Claude Code 主进程运行在 SSH
|
|
3
|
+
在项目目录运行 `claude`,让 Claude Code 主进程运行在 SSH 服务器,文件、终端和交互式浏览器操作在本机执行,普通公开资料搜索与网页读取使用服务器原生工具;也保留服务器执行和目录双向同步模式。
|
|
4
4
|
|
|
5
5
|
本项目是独立的 SSH/同步封装,不是 Anthropic 官方客户端。它不提供服务器、Claude 账号或 API 服务。
|
|
6
6
|
|
|
@@ -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.
|
|
28
|
+
npm install -g ./znmin-claude-remote-cli-0.2.5.tgz
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
若系统 npm 全局目录不可写,可使用 `npm install -g --prefix "$HOME/.local" ./znmin-claude-remote-cli-0.2.
|
|
31
|
+
若系统 npm 全局目录不可写,可使用 `npm install -g --prefix "$HOME/.local" ./znmin-claude-remote-cli-0.2.5.tgz`,并确保 `~/.local/bin` 在 PATH 中;卸载时也使用相同的 `--prefix`。
|
|
32
32
|
|
|
33
33
|
npm 中不带作用域的 `claude-remote-cli` 属于其他项目。本项目使用独立的 `@znmin/claude-remote-cli` 作用域包名;请安装完整包名,或使用上述 GitHub 地址及本项目分发的 `.tgz` 文件。
|
|
34
34
|
|
|
@@ -129,6 +129,8 @@ Git、npm、pnpm、Python、Go、Docker、数据库 CLI、`gh` 等共用本地
|
|
|
129
129
|
|
|
130
130
|
文件工具限定项目内,拒绝路径穿越和指向项目外的符号链接;修改已有文件需要精确旧文本或读取时的 SHA-256。终端具有启动用户的正常系统权限,项目工作目录不是操作系统沙箱。本地模式可使用本机 Git worktree。
|
|
131
131
|
|
|
132
|
+
0.2.5 起,普通公开资料查询优先使用服务器上的 Claude 原生 `WebSearch` / `WebFetch`,不打开本地浏览器;服务器的文件、终端和浏览器执行工具仍不开放。原生搜索/网页读取是否可用取决于服务器 Claude 的账号、服务商和策略;如果不可用或查询失败,会先说明并征求用户意见,再回退本地浏览器。本地开发页面、localhost/局域网地址、交互测试、截图和交互登录仍使用本地浏览器,不交给服务器 WebFetch。该选择通过工具白名单及提示词约定实现,不是对模型每次路由选择的硬性拦截。
|
|
133
|
+
|
|
132
134
|
浏览器使用独立窗口和临时上下文,不读取日常 Chrome 的登录态。启动本地模式时准备相应浏览器版本,后续使用缓存;收到浏览器工具调用时才打开窗口。可以打开本机 localhost,填写表单、点击、截图和读取控制台错误。
|
|
133
135
|
|
|
134
136
|
```bash
|
|
@@ -142,7 +144,7 @@ Linux 上仍需安装 Playwright 所需的系统共享库,有窗口模式还
|
|
|
142
144
|
|
|
143
145
|
本地模式要求服务器允许 SSH TCP 转发,Claude Code 版本至少 **2.1.248**;已有版本过旧时会提示在服务器运行 `claude update`。每个会话创建仅监听本机回环地址的工具服务、随机令牌和 SSH 反向隧道;令牌通过 SSH 标准输入写入服务器 0600 临时配置,不出现在命令行参数或同步项目中。退出时清理配置、连接和浏览器。
|
|
144
146
|
|
|
145
|
-
启动时使用 `--restricted`、`--tools ""`、`--strict-mcp-config`,限定工具和设置来源、关闭普通 hooks。0.2.2 起恢复 `/config`、`/model`、`/help` 等内置交互命令,不再使用禁用整个命令菜单的 `--disable-slash-commands`;通过 `disableSkillShellExecution` 和 `disableBundledSkills` 单独关闭技能内联 shell 和内置技能工作流。恢复菜单不代表所有依赖服务器项目目录或原生工具的命令都适用于本地模式。设置面板修改的是服务器上的 Claude 配置;受限模式仍只加载托管配置和本次 `--settings`,不会恢复读取普通用户/项目 settings.json,因此偏好设置的跨会话持久化仍受该限制影响,可用 `claude --model sonnet` 明确指定启动模型。保留服务器上的订阅认证,不使用跳过 OAuth 的 `--bare
|
|
147
|
+
启动时使用 `--restricted`、`--tools "WebSearch,WebFetch"`、`--strict-mcp-config`,限定工具和设置来源、关闭普通 hooks。0.2.2 起恢复 `/config`、`/model`、`/help` 等内置交互命令,不再使用禁用整个命令菜单的 `--disable-slash-commands`;通过 `disableSkillShellExecution` 和 `disableBundledSkills` 单独关闭技能内联 shell 和内置技能工作流。恢复菜单不代表所有依赖服务器项目目录或原生工具的命令都适用于本地模式。设置面板修改的是服务器上的 Claude 配置;受限模式仍只加载托管配置和本次 `--settings`,不会恢复读取普通用户/项目 settings.json,因此偏好设置的跨会话持久化仍受该限制影响,可用 `claude --model sonnet` 明确指定启动模型。保留服务器上的订阅认证,不使用跳过 OAuth 的 `--bare`。本地连接失效时不会将文件或命令操作退回服务器执行。支持 `--continue`、`--resume`、`--model`、`--effort`、`-p` 和输入/输出格式;暂不接受替换工具配置、加载插件、切换云会话等参数。
|
|
146
148
|
|
|
147
149
|
这是工具路由约束,不是对 Claude Code 主进程的系统隔离。认证、会话和日志仍在服务器,管理员管理策略继续生效。文件内容、命令结果和截图会经过服务器交给模型,不整体同步不代表数据不离开本机。
|
|
148
150
|
|
|
@@ -181,6 +183,16 @@ CLAUDE_REMOTE_HOST=another-server CLAUDE_REMOTE_PORT=2222 claude
|
|
|
181
183
|
- 不允许同步整个本机用户目录或磁盘根目录。不会同步用户目录中的 Claude 登录信息;远程使用自己的登录状态和工具配置。
|
|
182
184
|
- 本版不提供 tmux 会话恢复。网络中断后文件同步会尝试重连,远程 Claude 进程是否继续运行取决于服务器的进程行为。
|
|
183
185
|
|
|
186
|
+
## 运行提示与调试
|
|
187
|
+
|
|
188
|
+
正常使用时,只显示需要等待或操作的提示,例如首次下载组件、SSH 验证、服务器策略修复结果,以及错误。已配置好的本地模式直接进入 Claude,不重复显示模式说明、项目路径或浏览器缓存检查。服务器同步模式会简短提示“正在同步…”。`configure`、`setup-ssh`、`doctor` 等显式管理命令仍显示操作结果;Claude 自身的界面和 SSH 验证提示不受影响。
|
|
189
|
+
|
|
190
|
+
需要查看连接目标、项目路径、密钥配置过程、缓存检查、同步工具过程输出和错误堆栈时,临时启用:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
CLAUDE_REMOTE_DEBUG=1 claude
|
|
194
|
+
```
|
|
195
|
+
|
|
184
196
|
## 高级环境变量
|
|
185
197
|
|
|
186
198
|
| 变量 | 用途 |
|
|
@@ -194,7 +206,7 @@ CLAUDE_REMOTE_HOST=another-server CLAUDE_REMOTE_PORT=2222 claude
|
|
|
194
206
|
| `CLAUDE_REMOTE_MUTAGEN` | 使用自行准备的 Mutagen 可执行文件绝对路径(应为 0.18.1,并放置配套代理包) |
|
|
195
207
|
| `CLAUDE_REMOTE_PREPARE_SYNC=1` | npm 安装阶段提前准备同步工具,默认按需准备 |
|
|
196
208
|
| `CLAUDE_REMOTE_SKIP_DOWNLOAD=1` | 跳过安装阶段的同步工具准备 |
|
|
197
|
-
| `CLAUDE_REMOTE_DEBUG=1` |
|
|
209
|
+
| `CLAUDE_REMOTE_DEBUG=1` | 显示详细启动过程和错误堆栈 |
|
|
198
210
|
|
|
199
211
|
`npm install --ignore-scripts` 也受支持,首次使用各模式时准备对应组件。无法访问 GitHub 时可自行准备 Mutagen 并使用 `CLAUDE_REMOTE_MUTAGEN`。浏览器缓存由 Playwright 管理,支持其 `PLAYWRIGHT_BROWSERS_PATH` 环境变量。
|
|
200
212
|
|
|
@@ -210,7 +222,7 @@ npm test
|
|
|
210
222
|
CLAUDE_REMOTE_INTEGRATION=1 CLAUDE_REMOTE_BROWSER_TEST=1 npm test
|
|
211
223
|
|
|
212
224
|
npm pack
|
|
213
|
-
npm install -g ./znmin-claude-remote-cli-0.2.
|
|
225
|
+
npm install -g ./znmin-claude-remote-cli-0.2.5.tgz
|
|
214
226
|
|
|
215
227
|
# 确定包名、版本和使用条款后,使用有发布权限的 npm 账号发布
|
|
216
228
|
npm login
|
|
@@ -219,6 +231,6 @@ npm publish --access public
|
|
|
219
231
|
|
|
220
232
|
GitHub 仓库名为 `claude-remote-cli`,npm 包名为 `@znmin/claude-remote-cli`,命令名为 `claude` 和 `claude-remote`。发布账号必须拥有 npm 用户作用域 `@znmin`,或拥有同名 npm 组织的发布权限;GitHub 身份不会自动授予 npm 权限。当前 `license` 为 `UNLICENSED`,尚未指定开源许可证。
|
|
221
233
|
|
|
222
|
-
测试覆盖真实 MCP HTTP 调用、本机文件与进程、受控浏览器交互、本地真实同步、模拟 SSH/Claude
|
|
234
|
+
测试覆盖真实 MCP HTTP 调用、本机文件与进程、受控浏览器交互、本地真实同步、模拟 SSH/Claude 的完整启动清理链路,以及官方安装流程的隔离模拟。真实服务器完整启动、Claude 模型端到端调用、Linux 和其他 CPU 架构尚未完成实测。
|
|
223
235
|
|
|
224
236
|
参考:[npm 命令入口配置](https://docs.npmjs.com/cli/configuring-npm/package-json#bin)、[Mutagen 同步机制](https://mutagen.io/documentation/synchronization/)、[Mutagen SSH 支持](https://mutagen.io/documentation/transports/ssh/)。
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -3,10 +3,9 @@ import { ensureMutagen } from '../src/installer.js';
|
|
|
3
3
|
if (process.env.CLAUDE_REMOTE_PREPARE_SYNC === '1' && process.env.CLAUDE_REMOTE_SKIP_DOWNLOAD !== '1') {
|
|
4
4
|
try {
|
|
5
5
|
await ensureMutagen();
|
|
6
|
-
console.log('claude 已安装。运行 claude-remote configure 用户名@服务器,或直接运行 claude 开始配置。');
|
|
7
6
|
} catch (error) {
|
|
8
7
|
// Offline installation and npm --ignore-scripts remain supported: retry at first use.
|
|
9
8
|
console.warn(`CLI 已安装,同步工具准备未完成:${error.message}\n首次连接时会重试。`);
|
|
10
9
|
}
|
|
11
10
|
}
|
|
12
|
-
console.log('
|
|
11
|
+
console.log('安装完成,运行 claude 开始。');
|
package/src/cli.js
CHANGED
|
@@ -29,6 +29,7 @@ configure 可选:--mode local|remote、--headless/--headed、--root 远程根
|
|
|
29
29
|
新配置默认使用本地执行;旧配置保持服务器模式,直到显式切换。
|
|
30
30
|
服务器缺少默认 Claude Code 时会自动安装;首次启动需完成认证。
|
|
31
31
|
此工具不提供服务器或 Claude 账号。
|
|
32
|
+
详细诊断:CLAUDE_REMOTE_DEBUG=1 claude
|
|
32
33
|
仅服务器同步模式会在退出后继续后台同步;停止请运行 claude-remote stop。
|
|
33
34
|
`;
|
|
34
35
|
|
|
@@ -101,7 +102,8 @@ export async function main(asClaude, args = process.argv.slice(2), client = new
|
|
|
101
102
|
const { values, positionals } = parseArgs({ args: rest, allowPositionals: true,
|
|
102
103
|
options: { 'enable-pubkey': { type: 'boolean' } } });
|
|
103
104
|
if (positionals.length) throw new Error('用法:claude-remote setup-ssh [--enable-pubkey]');
|
|
104
|
-
await client.ensureSSH(client, await client.settings(), { enablePubkey: Boolean(values['enable-pubkey']) });
|
|
105
|
+
await client.ensureSSH(client, await client.settings(), { enablePubkey: Boolean(values['enable-pubkey']) });
|
|
106
|
+
client.log('SSH 免密登录可用。'); return 0;
|
|
105
107
|
}
|
|
106
108
|
if (rest.length) throw new Error(`${action} 不接受额外参数。`);
|
|
107
109
|
if (action === 'status') {
|
|
@@ -113,7 +115,7 @@ export async function main(asClaude, args = process.argv.slice(2), client = new
|
|
|
113
115
|
if (action === 'install-sync') { console.log('同步工具:' + await ensureMutagen({ env: client.env })); return 0; }
|
|
114
116
|
if (action === 'install-browser') {
|
|
115
117
|
const { installBrowser } = await import('./local/browser.js');
|
|
116
|
-
await installBrowser(client.log); console.log('
|
|
118
|
+
await installBrowser(client.log, { debug: client.debug }); console.log('浏览器已就绪。'); return 0;
|
|
117
119
|
}
|
|
118
120
|
if (action === 'doctor') {
|
|
119
121
|
console.log(`Node.js:${process.version}\n平台:${process.platform}/${process.arch}`);
|
package/src/client.js
CHANGED
|
@@ -63,6 +63,9 @@ export class Client {
|
|
|
63
63
|
this.ensureMutagen = options.ensureMutagen || ensureMutagen;
|
|
64
64
|
this.log = options.log || console.error;
|
|
65
65
|
this.warn = options.warn || console.error;
|
|
66
|
+
this.debug = (...messages) => {
|
|
67
|
+
if (this.env.CLAUDE_REMOTE_DEBUG === '1') this.log(...messages);
|
|
68
|
+
};
|
|
66
69
|
this.tty = options.tty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -74,8 +77,10 @@ export class Client {
|
|
|
74
77
|
ignores: [...DEFAULTS.ignores, ...(input.ignores || [])],
|
|
75
78
|
client_id: old.client_id || randomUUID().replaceAll('-', '').slice(0, 12) });
|
|
76
79
|
await writeJSON(this.configFile, config);
|
|
77
|
-
this.log(
|
|
78
|
-
this.
|
|
80
|
+
this.log(`已保存:${config.host}(${config.mode === 'local' ? '本地执行' : '远程同步'})。`);
|
|
81
|
+
this.debug(`配置文件:${this.configFile}`);
|
|
82
|
+
if (old.host && (old.host !== config.host || (old.mode || 'remote') !== config.mode))
|
|
83
|
+
this.log('停止旧同步:claude-remote stop --all');
|
|
79
84
|
return config;
|
|
80
85
|
});
|
|
81
86
|
}
|
|
@@ -150,6 +155,7 @@ export class Client {
|
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
async launch(config, extra) {
|
|
158
|
+
this.debug(`连接:${config.host}${config.port ? ':' + config.port : ''},模式:${config.mode || 'remote'}`);
|
|
153
159
|
await this.ensureSSH(this, config);
|
|
154
160
|
if (config.mode === 'local') {
|
|
155
161
|
const { launchLocal } = await import('./local-mode.js');
|
|
@@ -178,21 +184,22 @@ export class Client {
|
|
|
178
184
|
await this.remote(config, ensureRemoteClaude(config.claude) +
|
|
179
185
|
'mkdir -p -- ' + remotePath(item.remote), false, { timeout: 600000 });
|
|
180
186
|
if (existing.some(session => session.name === item.name)) {
|
|
181
|
-
await this.sync(['resume', item.name], { timeout: 60000, capture: !this.tty });
|
|
187
|
+
await this.sync(['resume', item.name], { timeout: 60000, capture: this.env.CLAUDE_REMOTE_DEBUG !== '1' || !this.tty });
|
|
182
188
|
} else {
|
|
183
189
|
const endpoint = config.host + ':' + (config.port != null ? config.port + ':' : '') +
|
|
184
190
|
(item.remote.startsWith('/') ? item.remote : '~/' + item.remote);
|
|
185
191
|
const args = ['create', '--name', item.name, '--label', 'claude-remote=true', '--mode', 'two-way-safe',
|
|
186
192
|
'--no-global-configuration', '--no-ignore-vcs', '--watch-polling-interval', '2'];
|
|
187
193
|
for (const pattern of config.ignores) args.push('--ignore', pattern);
|
|
188
|
-
await this.sync([...args, item.local, endpoint], { timeout: 180000, capture: !this.tty });
|
|
194
|
+
await this.sync([...args, item.local, endpoint], { timeout: 180000, capture: this.env.CLAUDE_REMOTE_DEBUG !== '1' || !this.tty });
|
|
189
195
|
}
|
|
190
196
|
registry[item.name] = item;
|
|
191
197
|
await writeJSON(this.registryFile, registry);
|
|
192
198
|
});
|
|
193
|
-
this.
|
|
199
|
+
this.debug(`本地:${item.local}\n远程:${config.host}:${item.remote}`);
|
|
200
|
+
this.log('正在同步…');
|
|
194
201
|
await this.flush(item.name);
|
|
195
|
-
this.
|
|
202
|
+
this.debug('同步完成,启动远程 Claude Code。退出后继续后台同步;停止请运行 claude-remote stop。');
|
|
196
203
|
let code = 1;
|
|
197
204
|
try {
|
|
198
205
|
code = (await this.remote(config, prefix + 'cd -- ' + remotePath(item.remote) +
|
package/src/installer.js
CHANGED
|
@@ -64,7 +64,7 @@ export async function ensureMutagen({ env = process.env, forceDownload = false }
|
|
|
64
64
|
const temporary = await fs.mkdtemp(path.join(path.dirname(destination), '.mutagen-install-'));
|
|
65
65
|
try {
|
|
66
66
|
const archive = path.join(temporary, 'download.tar.gz');
|
|
67
|
-
console.error(
|
|
67
|
+
console.error('正在下载同步工具…');
|
|
68
68
|
await download(`https://github.com/mutagen-io/mutagen/releases/download/v${VERSION}/${archiveName}`, archive);
|
|
69
69
|
await verifyChecksum(archive, checksum);
|
|
70
70
|
// Extract only the two required files from the authenticated archive.
|
package/src/local/browser.js
CHANGED
|
@@ -8,13 +8,14 @@ import { execute, exists, withLock } from '../system.js';
|
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
let preparation;
|
|
10
10
|
|
|
11
|
-
export async function installBrowser(log = console.error) {
|
|
11
|
+
export async function installBrowser(log = console.error, { debug = () => {} } = {}) {
|
|
12
12
|
if (!preparation) preparation = (async () => {
|
|
13
13
|
const cli = path.join(path.dirname(require.resolve('playwright/package.json')), 'cli.js');
|
|
14
14
|
// Keep the lock outside Playwright's cache: its installer replaces browser directories.
|
|
15
15
|
const lock = path.join(process.env.CLAUDE_REMOTE_CACHE_DIR || path.join(os.homedir(), '.cache', 'claude-remote'), 'browser-install');
|
|
16
16
|
await withLock(lock, async () => {
|
|
17
|
-
log('
|
|
17
|
+
if (!await exists(chromium.executablePath())) log('正在下载浏览器…');
|
|
18
|
+
else debug('检查浏览器缓存:' + chromium.executablePath());
|
|
18
19
|
// The CLI checks completion markers and prepares headed AND headless binaries.
|
|
19
20
|
const result = await execute(process.execPath, [cli, 'install', 'chromium'], { capture: true, timeout: 600000 });
|
|
20
21
|
if (!await exists(chromium.executablePath())) throw new Error('浏览器安装未完成:' + result.stderr);
|
package/src/local/server.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function startLocalServer({ root, env = process.env, headless = fal
|
|
|
66
66
|
register('list_processes', 'List processes owned by this tool session, not all system processes.', {}, () => processes.list(), true);
|
|
67
67
|
register('stop_process', 'Stop a process group started by this tool session, including a development server.', { job_id: z.string() },
|
|
68
68
|
args => processes.stop(args.job_id));
|
|
69
|
-
register('browser_open', 'Open a page in a dedicated LOCAL browser. localhost is the developer computer. Does not attach to existing browser profiles.', {
|
|
69
|
+
register('browser_open', 'Open a page in a dedicated LOCAL browser for local development, interactive UI testing, screenshots or interactive sign-in. For ordinary public research use server WebSearch/WebFetch; ask the user before falling back to this browser. localhost is the developer computer. Does not attach to existing browser profiles.', {
|
|
70
70
|
url: z.string().url().or(z.literal('about:blank')).default('about:blank')
|
|
71
71
|
}, args => browser.open(args.url));
|
|
72
72
|
register('browser_snapshot', 'Inspect a LOCAL browser page and get visible text and selectors for interactive elements.', { page_id: pageId },
|
package/src/local-mode.js
CHANGED
|
@@ -38,7 +38,7 @@ export function validateLocalArguments(args) {
|
|
|
38
38
|
|
|
39
39
|
export function localClaudeArguments(configFile, root, extra) {
|
|
40
40
|
validateLocalArguments(extra);
|
|
41
|
-
return ['--restricted', '--tools', '', '--strict-mcp-config', '--mcp-config', configFile,
|
|
41
|
+
return ['--restricted', '--tools', 'WebSearch,WebFetch', '--strict-mcp-config', '--mcp-config', configFile,
|
|
42
42
|
// Keep built-in UI commands (/config, /model, /help) available. Disable
|
|
43
43
|
// skill shell expansion separately instead of disabling the entire menu.
|
|
44
44
|
'--setting-sources', '', '--settings', JSON.stringify({ disableAllHooks: true,
|
|
@@ -46,8 +46,11 @@ export function localClaudeArguments(configFile, root, extra) {
|
|
|
46
46
|
'--append-system-prompt', `All project operations run on the developer's LOCAL computer through the local MCP server. ` +
|
|
47
47
|
`The LOCAL project root is ${JSON.stringify(root)}. The server working directory is only for session metadata, not project files. ` +
|
|
48
48
|
'Call workspace_info before working. Use only local tools for file access, shell commands and browser operations. ' +
|
|
49
|
+
'For public information research, use the server-side WebSearch and WebFetch tools first; do not open the local browser for ordinary searches or reading public documentation. ' +
|
|
50
|
+
'Use the LOCAL browser for local development pages, localhost/loopback or developer LAN addresses, interactive UI testing, screenshots, or pages requiring interactive sign-in. ' +
|
|
51
|
+
'Never send local development URLs to server-side WebFetch. If WebSearch or WebFetch is unavailable or fails for ordinary public research, report it and ask before using the local browser as a fallback. ' +
|
|
49
52
|
'Read LOCAL CLAUDE.md with read_file if present. Use background commands for development servers, inspect their logs, and stop owned processes when finished. ' +
|
|
50
|
-
'If the local connection fails, report it and stop
|
|
53
|
+
'If the local connection fails, report it and stop local project operations; never fall back to server filesystem, shell or browser tools.', ...extra];
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
export function supportsLocalMode(output) {
|
|
@@ -63,7 +66,7 @@ export async function launchLocal(client, config, extra, dependencies = {}) {
|
|
|
63
66
|
const root = await fs.realpath(client.cwd), home = await fs.realpath(client.home);
|
|
64
67
|
if (root === home || root === path.parse(root).root) throw new Error('请进入具体项目目录,不允许以用户目录或磁盘根目录作为项目。');
|
|
65
68
|
const prepareBrowser = dependencies.installBrowser || installBrowser;
|
|
66
|
-
await prepareBrowser(client.log);
|
|
69
|
+
await prepareBrowser(client.log, { debug: client.debug });
|
|
67
70
|
const gateway = await (dependencies.startLocalServer || startLocalServer)({ root, env: client.env,
|
|
68
71
|
headless: client.env.CLAUDE_REMOTE_HEADLESS === undefined ? Boolean(config.browserHeadless) : client.env.CLAUDE_REMOTE_HEADLESS === '1', log: client.log });
|
|
69
72
|
let tunnel, configFile;
|
|
@@ -82,7 +85,7 @@ export async function launchLocal(client, config, extra, dependencies = {}) {
|
|
|
82
85
|
headers: { Authorization: 'Bearer ' + gateway.token }, timeout: 180000 } } };
|
|
83
86
|
await remote('set -e; umask 077; mkdir -p -- ' + remotePath(remoteDirectory) + '; cat > ' + remotePath(configFile),
|
|
84
87
|
{ input: JSON.stringify(mcp) });
|
|
85
|
-
client.
|
|
88
|
+
client.debug(`本地执行模式:${root}\n文件、命令和浏览器在本机运行;项目文件不通过本模式同步。`);
|
|
86
89
|
const args = localClaudeArguments(path.posix.basename(configFile), root, extra);
|
|
87
90
|
// Keep the parent shell so EXIT can remove the token-bearing temporary config.
|
|
88
91
|
const script = remoteEnvironment + 'cd -- ' + remotePath(remoteDirectory) + ' || exit 1; ' +
|
package/src/ssh-auth.js
CHANGED
|
@@ -39,7 +39,7 @@ export async function ensureSSH(client, config, { enablePubkey = false } = {}) {
|
|
|
39
39
|
const temporary = await fs.mkdtemp(path.join(base, 'ccra-'));
|
|
40
40
|
const socket = path.join(temporary, 'ssh');
|
|
41
41
|
try {
|
|
42
|
-
client.log('
|
|
42
|
+
client.log('请按提示完成 SSH 验证。');
|
|
43
43
|
// Authenticate once; subsequent installation reuses this private socket.
|
|
44
44
|
// Do not capture authentication output or handle passwords in JavaScript.
|
|
45
45
|
await run(['-M', '-S', socket, '-o', 'ControlPersist=no', '-o', 'ControlMaster=yes',
|
|
@@ -47,7 +47,7 @@ export async function ensureSSH(client, config, { enablePubkey = false } = {}) {
|
|
|
47
47
|
const inspect = await run(['-S', socket, config.host, 'sh -c ' + quote(sshPolicyScript({ enable: enablePubkey }))],
|
|
48
48
|
{ capture: true });
|
|
49
49
|
const policy = parseSSHPolicy(inspect.stdout);
|
|
50
|
-
if (policy.backup) client.log('
|
|
50
|
+
if (policy.backup) client.log('已开启公钥登录。配置备份:' + policy.backup);
|
|
51
51
|
if (policy.pubkeyauthentication === 'no')
|
|
52
52
|
throw new Error('服务器关闭了公钥登录(PubkeyAuthentication no),尚未生成或添加密钥。' +
|
|
53
53
|
(policy.uid === '0' ? '如需开启,请运行:claude-remote setup-ssh --enable-pubkey(备份配置、校验后重新加载,保留密码登录)。' :
|
|
@@ -58,14 +58,14 @@ export async function ensureSSH(client, config, { enablePubkey = false } = {}) {
|
|
|
58
58
|
',不允许仅凭公钥登录;请联系管理员确认认证策略。未修改该策略,尚未生成或添加密钥。');
|
|
59
59
|
if (policy.status !== 'ok') {
|
|
60
60
|
if (enablePubkey) throw new Error('无法读取服务器有效 SSH 配置,未自动修改服务器策略;请联系管理员检查。');
|
|
61
|
-
client.
|
|
61
|
+
client.debug('无法读取服务器 SSH 策略,尝试普通公钥授权。');
|
|
62
62
|
}
|
|
63
63
|
const id = createHash('sha256').update(config.host + '\0' + (config.port ?? '')).digest('hex').slice(0, 20);
|
|
64
64
|
const key = path.join(directory, 'id_ed25519_' + id);
|
|
65
65
|
await fs.chmod(directory, 0o700);
|
|
66
66
|
if (!await exists(key)) {
|
|
67
67
|
if (await exists(key + '.pub')) throw new Error('专用 SSH 私钥缺失但公钥仍存在,请检查:' + key);
|
|
68
|
-
client.
|
|
68
|
+
client.debug('创建专用 SSH 密钥:' + key);
|
|
69
69
|
await client.execute('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', 'claude-remote-cli', '-f', key],
|
|
70
70
|
{ env: client.env, capture: true, timeout: 30000 });
|
|
71
71
|
}
|
|
@@ -91,7 +91,7 @@ export async function ensureSSH(client, config, { enablePubkey = false } = {}) {
|
|
|
91
91
|
});
|
|
92
92
|
if (!previous.includes(block)) await fs.appendFile(configFile, block, { mode: 0o600 });
|
|
93
93
|
if (!await probe()) throw new Error('密钥已授权,但默认 SSH 配置验证失败。请检查 ~/.ssh/config 的 IdentityFile 和 IdentitiesOnly 设置。');
|
|
94
|
-
client.
|
|
94
|
+
client.debug('SSH 免密登录已配置并验证成功。');
|
|
95
95
|
} finally {
|
|
96
96
|
await run(['-S', socket, '-O', 'exit', config.host], { capture: true, check: false, timeout: 5000 }).catch(() => {});
|
|
97
97
|
await fs.rm(temporary, { recursive: true, force: true });
|