claude-remote 0.3.0 → 0.4.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 +51 -3
- package/package.json +1 -1
- package/server.js +180 -17
package/README.md
CHANGED
|
@@ -59,9 +59,56 @@ claude-remote /path/to/project --resume abc123
|
|
|
59
59
|
PORT=8080 claude-remote
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
### 3.
|
|
62
|
+
### 3. 安全认证
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
`claude-remote` 内置 Token 鉴权机制,防止未授权的客户端控制你的 Claude Code。
|
|
65
|
+
|
|
66
|
+
#### 工作原理
|
|
67
|
+
|
|
68
|
+
1. **首次启动时自动生成 Token** —— 桥接器启动后会自动生成一个随机 Token,持久化到 `~/.claude-remote-token` 文件,并在启动 banner 中显示
|
|
69
|
+
2. **客户端必须提供 Token** —— App 在添加/编辑服务器时填入 Token,连接时通过 `hello` 握手消息发送给服务器
|
|
70
|
+
3. **服务器验证 Token** —— 使用 `crypto.timingSafeEqual` 安全比对,防止时序攻击。验证通过后才发送会话数据
|
|
71
|
+
4. **认证超时** —— 连接后 5 秒内未完成认证将被自动断开(close code 4002)
|
|
72
|
+
5. **认证失败** —— Token 不匹配时连接立即关闭(close code 4001),App 会弹出编辑对话框提示修正
|
|
73
|
+
|
|
74
|
+
#### Token 来源优先级
|
|
75
|
+
|
|
76
|
+
| 优先级 | 来源 | 说明 |
|
|
77
|
+
|--------|------|------|
|
|
78
|
+
| 1 | `--token <value>` | CLI 参数,最高优先级 |
|
|
79
|
+
| 2 | `CLAUDE_REMOTE_TOKEN` 环境变量 | 适合 CI/脚本场景 |
|
|
80
|
+
| 3 | `~/.claude-remote-token` 文件 | 自动生成并持久化,首次启动时创建 |
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 使用自定义 Token
|
|
84
|
+
claude-remote --token my-secret-token
|
|
85
|
+
|
|
86
|
+
# 通过环境变量
|
|
87
|
+
CLAUDE_REMOTE_TOKEN=my-secret-token claude-remote
|
|
88
|
+
|
|
89
|
+
# 默认行为:自动生成并保存到 ~/.claude-remote-token
|
|
90
|
+
claude-remote
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### 禁用认证
|
|
94
|
+
|
|
95
|
+
> ⚠️ 仅在安全网络环境下使用,如本地开发或 VPN 内网。
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# CLI 参数
|
|
99
|
+
claude-remote --no-auth
|
|
100
|
+
|
|
101
|
+
# 环境变量
|
|
102
|
+
NO_AUTH=1 claude-remote
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### App 端配置
|
|
106
|
+
|
|
107
|
+
在连接中心添加或编辑服务器时,填入 Token 字段即可。Token 输入框支持密码显示/隐藏切换。认证失败时 App 会自动弹出编辑对话框,方便修正 Token。
|
|
108
|
+
|
|
109
|
+
### 4. App 连接
|
|
110
|
+
|
|
111
|
+
安装 Android App 后,在连接中心添加服务器地址连接。根据你的网络环境,有以下三种方式:
|
|
65
112
|
|
|
66
113
|
#### 方式一:局域网直连
|
|
67
114
|
|
|
@@ -107,7 +154,7 @@ wss://xxx.trycloudflare.com
|
|
|
107
154
|
|
|
108
155
|
> 适合在外网或移动网络下远程控制,支持 HTTPS/WSS 加密。
|
|
109
156
|
|
|
110
|
-
###
|
|
157
|
+
### 5. 编译 Android App
|
|
111
158
|
|
|
112
159
|
```bash
|
|
113
160
|
cd app
|
|
@@ -143,6 +190,7 @@ npx tauri android dev
|
|
|
143
190
|
| 工具调用追踪 | 折叠式 step group,shimmer 加载动效 |
|
|
144
191
|
| 权限队列 | 批量审批,计数器显示剩余数量 |
|
|
145
192
|
| 模式切换 | 四种模式一键切换,彩色状态标识 |
|
|
193
|
+
| Token 鉴权 | 自动生成 Token,timingSafeEqual 安全比对,防止未授权访问 |
|
|
146
194
|
| 对话压缩 | /compact 带 spinner 遮罩,压缩完自动恢复 |
|
|
147
195
|
| 斜杠命令菜单 | 输入 `/` 弹出命令面板 |
|
|
148
196
|
| 模型切换 | 底部面板选择,toast 即时反馈 |
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const pty = require('node-pty');
|
|
6
6
|
const { WebSocketServer, WebSocket } = require('ws');
|
|
7
|
+
const crypto = require('crypto');
|
|
7
8
|
const { execSync } = require('child_process');
|
|
8
9
|
|
|
9
10
|
// --- CLI argument parsing ---
|
|
@@ -26,6 +27,8 @@ const BLOCKED_FLAGS = new Set([
|
|
|
26
27
|
'--help', '-h', // exits immediately
|
|
27
28
|
'--init-only', // exits immediately
|
|
28
29
|
'--maintenance', // exits immediately
|
|
30
|
+
'--token', // bridge-only: auth token
|
|
31
|
+
'--no-auth', // bridge-only: disable auth
|
|
29
32
|
]);
|
|
30
33
|
|
|
31
34
|
// Flags that consume the next argument as a value
|
|
@@ -42,6 +45,7 @@ const FLAGS_WITH_VALUE = new Set([
|
|
|
42
45
|
'--output-format', '--input-format', '--json-schema',
|
|
43
46
|
'--max-budget-usd', '--max-turns', '--fallback-model',
|
|
44
47
|
'--permission-prompt-tool',
|
|
48
|
+
'--token', // bridge-only: auth token
|
|
45
49
|
]);
|
|
46
50
|
|
|
47
51
|
function parseCliArgs(argv) {
|
|
@@ -49,6 +53,8 @@ function parseCliArgs(argv) {
|
|
|
49
53
|
let cwd = null;
|
|
50
54
|
const claudeArgs = [];
|
|
51
55
|
const blocked = [];
|
|
56
|
+
let token = null;
|
|
57
|
+
let noAuth = false;
|
|
52
58
|
|
|
53
59
|
let i = 0;
|
|
54
60
|
while (i < rawArgs.length) {
|
|
@@ -75,6 +81,25 @@ function parseCliArgs(argv) {
|
|
|
75
81
|
const eqIdx = arg.indexOf('=');
|
|
76
82
|
const flagName = eqIdx > 0 ? arg.substring(0, eqIdx) : arg;
|
|
77
83
|
|
|
84
|
+
// Intercept bridge-only flags
|
|
85
|
+
if (flagName === '--token') {
|
|
86
|
+
if (eqIdx > 0) {
|
|
87
|
+
token = arg.substring(eqIdx + 1);
|
|
88
|
+
} else if (i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('-')) {
|
|
89
|
+
i++;
|
|
90
|
+
token = rawArgs[i];
|
|
91
|
+
} else {
|
|
92
|
+
token = '';
|
|
93
|
+
}
|
|
94
|
+
i++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (flagName === '--no-auth') {
|
|
98
|
+
noAuth = true;
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
78
103
|
if (BLOCKED_FLAGS.has(flagName)) {
|
|
79
104
|
blocked.push(flagName);
|
|
80
105
|
if (eqIdx > 0) {
|
|
@@ -96,10 +121,36 @@ function parseCliArgs(argv) {
|
|
|
96
121
|
i++;
|
|
97
122
|
}
|
|
98
123
|
|
|
99
|
-
return { cwd: cwd || process.cwd(), claudeArgs, blocked };
|
|
124
|
+
return { cwd: cwd || process.cwd(), claudeArgs, blocked, token, noAuth };
|
|
100
125
|
}
|
|
101
126
|
|
|
102
|
-
const { cwd: _parsedCwd, claudeArgs: CLAUDE_EXTRA_ARGS, blocked: _blockedArgs } = parseCliArgs(process.argv);
|
|
127
|
+
const { cwd: _parsedCwd, claudeArgs: CLAUDE_EXTRA_ARGS, blocked: _blockedArgs, token: _cliToken, noAuth: _cliNoAuth } = parseCliArgs(process.argv);
|
|
128
|
+
|
|
129
|
+
// --- Auth token resolution ---
|
|
130
|
+
const AUTH_TOKEN_ENV_VAR = 'CLAUDE_REMOTE_TOKEN';
|
|
131
|
+
const LEGACY_AUTH_TOKEN_ENV_VAR = 'TOKEN';
|
|
132
|
+
const AUTH_DISABLED = _cliNoAuth || process.env.NO_AUTH === '1';
|
|
133
|
+
const TOKEN_FILE = path.join(os.homedir(), '.claude-remote-token');
|
|
134
|
+
const UNUSED_LEGACY_TOKEN_ENV = !!process.env[LEGACY_AUTH_TOKEN_ENV_VAR] && !process.env[AUTH_TOKEN_ENV_VAR];
|
|
135
|
+
|
|
136
|
+
function resolveAuthToken() {
|
|
137
|
+
if (AUTH_DISABLED) return null;
|
|
138
|
+
// 1. CLI --token
|
|
139
|
+
if (_cliToken) return _cliToken;
|
|
140
|
+
// 2. Namespaced env
|
|
141
|
+
if (process.env[AUTH_TOKEN_ENV_VAR]) return process.env[AUTH_TOKEN_ENV_VAR];
|
|
142
|
+
// 3. Persisted file
|
|
143
|
+
try {
|
|
144
|
+
const saved = fs.readFileSync(TOKEN_FILE, 'utf8').trim();
|
|
145
|
+
if (saved) return saved;
|
|
146
|
+
} catch {}
|
|
147
|
+
// 4. Generate and persist
|
|
148
|
+
const generated = crypto.randomBytes(24).toString('base64url');
|
|
149
|
+
try { fs.writeFileSync(TOKEN_FILE, generated + '\n', { mode: 0o600 }); } catch {}
|
|
150
|
+
return generated;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const AUTH_TOKEN = resolveAuthToken();
|
|
103
154
|
|
|
104
155
|
// --- Config ---
|
|
105
156
|
const PORT = parseInt(process.env.PORT || '3100', 10);
|
|
@@ -107,6 +158,11 @@ let CWD = _parsedCwd;
|
|
|
107
158
|
const CLAUDE_HOME = path.join(os.homedir(), '.claude');
|
|
108
159
|
const CLAUDE_STATE_FILE = path.join(os.homedir(), '.claude.json');
|
|
109
160
|
const PROJECTS_DIR = path.join(CLAUDE_HOME, 'projects');
|
|
161
|
+
const AUTH_HELLO_TIMEOUT_MS = 5000;
|
|
162
|
+
const WS_CLOSE_AUTH_FAILED = 4001;
|
|
163
|
+
const WS_CLOSE_AUTH_TIMEOUT = 4002;
|
|
164
|
+
const WS_CLOSE_REASON_AUTH_FAILED = 'auth_failed';
|
|
165
|
+
const WS_CLOSE_REASON_AUTH_TIMEOUT = 'auth_timeout';
|
|
110
166
|
|
|
111
167
|
// --- State ---
|
|
112
168
|
let claudeProc = null;
|
|
@@ -129,6 +185,7 @@ let tailCatchingUp = false; // true while reading historical transcript content
|
|
|
129
185
|
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
130
186
|
const LEGACY_REPLAY_DELAY_MS = 1500;
|
|
131
187
|
const IMAGE_UPLOAD_TTL_MS = 15 * 60 * 1000;
|
|
188
|
+
const IMAGE_UPLOAD_DIR = path.join(CLAUDE_HOME, 'remote-uploads');
|
|
132
189
|
let turnStateVersion = 0;
|
|
133
190
|
let turnState = {
|
|
134
191
|
phase: 'idle',
|
|
@@ -161,6 +218,10 @@ function wsLabel(ws) {
|
|
|
161
218
|
return `ws#${ws && ws._bridgeId ? ws._bridgeId : '?'}${clientId}`;
|
|
162
219
|
}
|
|
163
220
|
|
|
221
|
+
function isAuthenticatedClient(ws) {
|
|
222
|
+
return !!ws && ws.readyState === WebSocket.OPEN && !!ws._authenticated;
|
|
223
|
+
}
|
|
224
|
+
|
|
164
225
|
function sendWs(ws, msg, context = '') {
|
|
165
226
|
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
166
227
|
ws.send(JSON.stringify(msg));
|
|
@@ -399,7 +460,7 @@ const server = http.createServer((req, res) => {
|
|
|
399
460
|
}
|
|
400
461
|
|
|
401
462
|
// No WebUI clients → fall back to terminal prompt
|
|
402
|
-
const clients = [...wss.clients].filter(
|
|
463
|
+
const clients = [...wss.clients].filter(isAuthenticatedClient);
|
|
403
464
|
if (clients.length === 0) {
|
|
404
465
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
405
466
|
res.end(JSON.stringify({ decision: 'ask' }));
|
|
@@ -505,7 +566,7 @@ function broadcast(msg) {
|
|
|
505
566
|
const raw = JSON.stringify(msg);
|
|
506
567
|
const recipients = [];
|
|
507
568
|
for (const ws of wss.clients) {
|
|
508
|
-
if (ws
|
|
569
|
+
if (isAuthenticatedClient(ws)) {
|
|
509
570
|
ws.send(raw);
|
|
510
571
|
recipients.push(wsLabel(ws));
|
|
511
572
|
}
|
|
@@ -566,14 +627,18 @@ function cleanupImageUpload(uploadId) {
|
|
|
566
627
|
|
|
567
628
|
function cleanupClientUploads(ws) {
|
|
568
629
|
for (const [uploadId, upload] of pendingImageUploads) {
|
|
569
|
-
if (upload.owner === ws) cleanupImageUpload(uploadId);
|
|
630
|
+
if (upload.owner === ws && !upload.submitted) cleanupImageUpload(uploadId);
|
|
570
631
|
}
|
|
571
632
|
}
|
|
572
633
|
|
|
573
634
|
function createTempImageFile(buffer, mediaType, uploadId) {
|
|
574
|
-
const
|
|
635
|
+
const isLinux = process.platform !== 'win32' && process.platform !== 'darwin';
|
|
636
|
+
const tmpDir = isLinux
|
|
637
|
+
? IMAGE_UPLOAD_DIR
|
|
638
|
+
: (process.env.CLAUDE_CODE_TMPDIR || os.tmpdir());
|
|
575
639
|
const type = String(mediaType || 'image/png').toLowerCase();
|
|
576
640
|
const ext = type.includes('jpeg') || type.includes('jpg') ? '.jpg' : '.png';
|
|
641
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
577
642
|
const tmpFile = path.join(tmpDir, `bridge_upload_${uploadId}_${Date.now()}${ext}`);
|
|
578
643
|
fs.writeFileSync(tmpFile, buffer);
|
|
579
644
|
return tmpFile;
|
|
@@ -588,11 +653,7 @@ setInterval(() => {
|
|
|
588
653
|
}
|
|
589
654
|
}, 60 * 1000).unref();
|
|
590
655
|
|
|
591
|
-
|
|
592
|
-
ws._bridgeId = ++nextWsId;
|
|
593
|
-
ws._clientInstanceId = '';
|
|
594
|
-
log(`WS connected: ${wsLabel(ws)} remote=${req.socket.remoteAddress || '?'} ua=${JSON.stringify(req.headers['user-agent'] || '')}`);
|
|
595
|
-
|
|
656
|
+
function sendInitialMessages(ws) {
|
|
596
657
|
sendWs(ws, {
|
|
597
658
|
type: 'status',
|
|
598
659
|
status: claudeProc ? 'running' : 'starting',
|
|
@@ -610,20 +671,86 @@ wss.on('connection', (ws, req) => {
|
|
|
610
671
|
lastSeq: latestEventSeq(),
|
|
611
672
|
}, 'initial');
|
|
612
673
|
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function sendAuthOk(ws) {
|
|
677
|
+
sendWs(ws, {
|
|
678
|
+
type: 'auth_ok',
|
|
679
|
+
authRequired: !AUTH_DISABLED,
|
|
680
|
+
}, 'auth_ok');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
wss.on('connection', (ws, req) => {
|
|
684
|
+
ws._bridgeId = ++nextWsId;
|
|
685
|
+
ws._clientInstanceId = '';
|
|
686
|
+
ws._authenticated = AUTH_DISABLED; // skip auth if disabled
|
|
687
|
+
ws._authTimer = null;
|
|
688
|
+
log(`WS connected: ${wsLabel(ws)} remote=${req.socket.remoteAddress || '?'} ua=${JSON.stringify(req.headers['user-agent'] || '')} authRequired=${!AUTH_DISABLED}`);
|
|
689
|
+
|
|
690
|
+
// If auth is disabled, send initial messages immediately
|
|
691
|
+
if (AUTH_DISABLED) {
|
|
692
|
+
sendAuthOk(ws);
|
|
693
|
+
sendInitialMessages(ws);
|
|
694
|
+
} else {
|
|
695
|
+
ws._authTimer = setTimeout(() => {
|
|
696
|
+
if (ws.readyState !== WebSocket.OPEN || ws._authenticated) return;
|
|
697
|
+
log(`Auth timeout for ${wsLabel(ws)}`);
|
|
698
|
+
ws.close(WS_CLOSE_AUTH_TIMEOUT, WS_CLOSE_REASON_AUTH_TIMEOUT);
|
|
699
|
+
}, AUTH_HELLO_TIMEOUT_MS);
|
|
700
|
+
}
|
|
613
701
|
|
|
614
702
|
// New clients should explicitly request a resume window. Keep a delayed
|
|
615
703
|
// full replay fallback so older clients still work.
|
|
616
704
|
ws._resumeHandled = false;
|
|
617
|
-
ws._legacyReplayTimer =
|
|
618
|
-
|
|
619
|
-
ws.
|
|
620
|
-
|
|
621
|
-
|
|
705
|
+
ws._legacyReplayTimer = null;
|
|
706
|
+
if (AUTH_DISABLED) {
|
|
707
|
+
ws._legacyReplayTimer = setTimeout(() => {
|
|
708
|
+
if (ws.readyState !== WebSocket.OPEN || ws._resumeHandled) return;
|
|
709
|
+
ws._resumeHandled = true;
|
|
710
|
+
sendReplay(ws, null);
|
|
711
|
+
}, LEGACY_REPLAY_DELAY_MS);
|
|
712
|
+
}
|
|
622
713
|
|
|
623
714
|
ws.on('message', async (raw) => {
|
|
624
715
|
let msg;
|
|
625
716
|
try { msg = JSON.parse(raw); } catch { return; }
|
|
626
717
|
|
|
718
|
+
// --- Authentication gate ---
|
|
719
|
+
if (!ws._authenticated) {
|
|
720
|
+
if (msg.type !== 'hello') return; // ignore non-hello when not authenticated
|
|
721
|
+
ws._clientInstanceId = String(msg.clientInstanceId || ws._clientInstanceId || '');
|
|
722
|
+
log(`WS hello from ${wsLabel(ws)} page=${JSON.stringify(msg.page || '')} ua=${JSON.stringify(msg.userAgent || '')}`);
|
|
723
|
+
|
|
724
|
+
const clientToken = String(msg.token || '');
|
|
725
|
+
if (!AUTH_TOKEN || !clientToken) {
|
|
726
|
+
log(`Auth failed for ${wsLabel(ws)}: missing token`);
|
|
727
|
+
ws.close(WS_CLOSE_AUTH_FAILED, WS_CLOSE_REASON_AUTH_FAILED);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const a = Buffer.from(AUTH_TOKEN, 'utf8');
|
|
731
|
+
const b = Buffer.from(clientToken, 'utf8');
|
|
732
|
+
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
|
|
733
|
+
log(`Auth failed for ${wsLabel(ws)}: invalid token`);
|
|
734
|
+
ws.close(WS_CLOSE_AUTH_FAILED, WS_CLOSE_REASON_AUTH_FAILED);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
ws._authenticated = true;
|
|
738
|
+
if (ws._authTimer) {
|
|
739
|
+
clearTimeout(ws._authTimer);
|
|
740
|
+
ws._authTimer = null;
|
|
741
|
+
}
|
|
742
|
+
log(`Auth OK for ${wsLabel(ws)}`);
|
|
743
|
+
|
|
744
|
+
sendAuthOk(ws);
|
|
745
|
+
sendInitialMessages(ws);
|
|
746
|
+
ws._legacyReplayTimer = setTimeout(() => {
|
|
747
|
+
if (ws.readyState !== WebSocket.OPEN || ws._resumeHandled) return;
|
|
748
|
+
ws._resumeHandled = true;
|
|
749
|
+
sendReplay(ws, null);
|
|
750
|
+
}, LEGACY_REPLAY_DELAY_MS);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
627
754
|
switch (msg.type) {
|
|
628
755
|
case 'hello':
|
|
629
756
|
ws._clientInstanceId = String(msg.clientInstanceId || ws._clientInstanceId || '');
|
|
@@ -860,13 +987,16 @@ wss.on('connection', (ws, req) => {
|
|
|
860
987
|
break;
|
|
861
988
|
}
|
|
862
989
|
try {
|
|
990
|
+
const shouldCleanupAfterSubmit = process.platform === 'win32' || process.platform === 'darwin';
|
|
863
991
|
handlePreparedImageUpload({
|
|
864
992
|
tmpFile: upload.tmpFile,
|
|
865
993
|
mediaType: upload.mediaType,
|
|
866
994
|
text: msg.text || '',
|
|
867
995
|
logLabel: upload.name || uploadId,
|
|
868
|
-
onCleanup: () => cleanupImageUpload(uploadId),
|
|
996
|
+
onCleanup: shouldCleanupAfterSubmit ? () => cleanupImageUpload(uploadId) : null,
|
|
869
997
|
});
|
|
998
|
+
upload.submitted = true;
|
|
999
|
+
upload.updatedAt = Date.now();
|
|
870
1000
|
setTurnState('running', { reason: 'image_submit' });
|
|
871
1001
|
sendUploadStatus(ws, uploadId, 'submitted');
|
|
872
1002
|
} catch (err) {
|
|
@@ -932,6 +1062,10 @@ wss.on('connection', (ws, req) => {
|
|
|
932
1062
|
});
|
|
933
1063
|
|
|
934
1064
|
ws.on('close', () => {
|
|
1065
|
+
if (ws._authTimer) {
|
|
1066
|
+
clearTimeout(ws._authTimer);
|
|
1067
|
+
ws._authTimer = null;
|
|
1068
|
+
}
|
|
935
1069
|
if (ws._legacyReplayTimer) {
|
|
936
1070
|
clearTimeout(ws._legacyReplayTimer);
|
|
937
1071
|
ws._legacyReplayTimer = null;
|
|
@@ -1493,11 +1627,27 @@ function handlePreparedImageUpload({ tmpFile, mediaType, text, logLabel = '', on
|
|
|
1493
1627
|
|
|
1494
1628
|
const isWin = process.platform === 'win32';
|
|
1495
1629
|
const isMac = process.platform === 'darwin';
|
|
1630
|
+
const isLinux = !isWin && !isMac;
|
|
1496
1631
|
|
|
1497
1632
|
try {
|
|
1498
1633
|
const stat = fs.statSync(tmpFile);
|
|
1499
1634
|
log(`Image ready: ${logLabel || path.basename(tmpFile)} (${stat.size} bytes)`);
|
|
1500
1635
|
|
|
1636
|
+
if (isLinux) {
|
|
1637
|
+
const quotedPath = JSON.stringify(tmpFile);
|
|
1638
|
+
const trimmedText = (text || '').trim();
|
|
1639
|
+
const prompt = trimmedText
|
|
1640
|
+
? `Please analyze the image at this path: ${quotedPath}\n\nUser note:\n${trimmedText}`
|
|
1641
|
+
: `Analyze this image: ${quotedPath}`;
|
|
1642
|
+
|
|
1643
|
+
claudeProc.write(prompt);
|
|
1644
|
+
setTimeout(() => {
|
|
1645
|
+
if (claudeProc) claudeProc.write('\r');
|
|
1646
|
+
log(`Sent Linux image path prompt: ${tmpFile}`);
|
|
1647
|
+
}, 150);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1501
1651
|
if (isWin) {
|
|
1502
1652
|
const psCmd = `Add-Type -AssemblyName System.Drawing; Add-Type -AssemblyName System.Windows.Forms; $img = [System.Drawing.Image]::FromFile('${tmpFile.replace(/'/g, "''")}'); [System.Windows.Forms.Clipboard]::SetImage($img); $img.Dispose()`;
|
|
1503
1653
|
execSync(`powershell -NoProfile -STA -Command "${psCmd}"`, { timeout: 10000 });
|
|
@@ -1563,6 +1713,11 @@ function handleImageUpload(msg) {
|
|
|
1563
1713
|
mediaType: msg.mediaType,
|
|
1564
1714
|
text: msg.text || '',
|
|
1565
1715
|
});
|
|
1716
|
+
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
1717
|
+
setTimeout(() => {
|
|
1718
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1719
|
+
}, IMAGE_UPLOAD_TTL_MS).unref();
|
|
1720
|
+
}
|
|
1566
1721
|
setTurnState('running', { reason: 'legacy_image_upload' });
|
|
1567
1722
|
} catch (err) {
|
|
1568
1723
|
log(`Image upload error: ${err.message}`);
|
|
@@ -1684,6 +1839,14 @@ server.listen(PORT, '0.0.0.0', () => {
|
|
|
1684
1839
|
CWD: ${CWD}
|
|
1685
1840
|
Log: ${LOG_FILE}
|
|
1686
1841
|
`;
|
|
1842
|
+
if (AUTH_DISABLED) {
|
|
1843
|
+
banner += ` Auth: DISABLED (no authentication)\n`;
|
|
1844
|
+
} else {
|
|
1845
|
+
banner += ` Token: ${AUTH_TOKEN}\n`;
|
|
1846
|
+
}
|
|
1847
|
+
if (UNUSED_LEGACY_TOKEN_ENV) {
|
|
1848
|
+
banner += ` Note: Ignoring legacy ${LEGACY_AUTH_TOKEN_ENV_VAR}; use ${AUTH_TOKEN_ENV_VAR} instead\n`;
|
|
1849
|
+
}
|
|
1687
1850
|
if (CLAUDE_EXTRA_ARGS.length > 0) {
|
|
1688
1851
|
banner += ` Args: claude ${CLAUDE_EXTRA_ARGS.join(' ')}\n`;
|
|
1689
1852
|
}
|