remote-claude 0.2.4 → 0.2.6
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/.env.example +4 -0
- package/README.md +26 -0
- package/init.sh +3 -0
- package/package.json +3 -2
- package/remote_claude.py +41 -0
- package/scripts/report_install.py +86 -0
- package/server/server.py +19 -6
- package/stats/collector.py +48 -12
package/.env.example
CHANGED
package/README.md
CHANGED
|
@@ -231,6 +231,32 @@ remote-claude lark status # 查看状态
|
|
|
231
231
|
|
|
232
232
|
飞书中与机器人对话,可用命令:`/menu`、`/attach`、`/detach`、`/list`、`/help` 等。
|
|
233
233
|
|
|
234
|
+
## 高级配置
|
|
235
|
+
|
|
236
|
+
在 `~/.remote-claude/.env` 中可配置以下选项:
|
|
237
|
+
|
|
238
|
+
| 配置项 | 默认值 | 说明 |
|
|
239
|
+
|--------|--------|------|
|
|
240
|
+
| `CLAUDE_COMMAND` | `claude` | 启动 Claude CLI 的命令 |
|
|
241
|
+
| `FEISHU_APP_ID` | — | 飞书应用 ID |
|
|
242
|
+
| `FEISHU_APP_SECRET` | — | 飞书应用密钥 |
|
|
243
|
+
| `ENABLE_USER_WHITELIST` | `false` | 是否启用用户白名单 |
|
|
244
|
+
| `ALLOWED_USERS` | — | 白名单用户 ID,逗号分隔 |
|
|
245
|
+
|
|
246
|
+
### 自定义 Claude CLI 命令
|
|
247
|
+
|
|
248
|
+
若你的 Claude CLI 安装方式不同,启动命令不是 `claude`,可通过 `CLAUDE_COMMAND` 指定:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# ~/.remote-claude/.env
|
|
252
|
+
|
|
253
|
+
# 使用两段式命令(如 ccr code)
|
|
254
|
+
CLAUDE_COMMAND=ccr code
|
|
255
|
+
|
|
256
|
+
# 使用绝对路径
|
|
257
|
+
CLAUDE_COMMAND=/usr/local/bin/claude
|
|
258
|
+
```
|
|
259
|
+
|
|
234
260
|
## 系统要求
|
|
235
261
|
|
|
236
262
|
- **操作系统**: macOS 或 Linux
|
package/init.sh
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remote-claude",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "双端共享 Claude CLI 工具",
|
|
5
5
|
"bin": {
|
|
6
6
|
"remote-claude": "bin/remote-claude",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin/",
|
|
15
|
-
"scripts
|
|
15
|
+
"scripts/*.sh",
|
|
16
|
+
"scripts/*.py",
|
|
16
17
|
"init.sh",
|
|
17
18
|
"remote_claude.py",
|
|
18
19
|
"server/*.py",
|
package/remote_claude.py
CHANGED
|
@@ -387,6 +387,43 @@ def cmd_stats(args):
|
|
|
387
387
|
return 0
|
|
388
388
|
|
|
389
389
|
|
|
390
|
+
def cmd_update(args):
|
|
391
|
+
"""更新 remote-claude 到最新版本"""
|
|
392
|
+
import subprocess as _sp
|
|
393
|
+
|
|
394
|
+
git_dir = SCRIPT_DIR / ".git"
|
|
395
|
+
if git_dir.exists():
|
|
396
|
+
# 源码安装:git pull 更新
|
|
397
|
+
print(f"检测到源码安装({SCRIPT_DIR})")
|
|
398
|
+
print("正在更新...")
|
|
399
|
+
result = _sp.run(["git", "pull"], cwd=SCRIPT_DIR)
|
|
400
|
+
if result.returncode != 0:
|
|
401
|
+
print("❌ git pull 失败")
|
|
402
|
+
return 1
|
|
403
|
+
# 同步 Python 依赖
|
|
404
|
+
_sp.run(["uv", "sync"], cwd=SCRIPT_DIR)
|
|
405
|
+
print("✅ 更新完成")
|
|
406
|
+
else:
|
|
407
|
+
# npm 安装:区分本地和全局
|
|
408
|
+
install_dir_str = str(SCRIPT_DIR)
|
|
409
|
+
if "node_modules" in install_dir_str:
|
|
410
|
+
# 本地 npm 安装:找到项目根目录(node_modules 的上两级)
|
|
411
|
+
project_root = SCRIPT_DIR.parent.parent
|
|
412
|
+
print(f"检测到 npm 本地安装({project_root})")
|
|
413
|
+
print("正在更新...")
|
|
414
|
+
result = _sp.run(["npm", "install", "remote-claude@latest"], cwd=project_root)
|
|
415
|
+
else:
|
|
416
|
+
# 全局 npm 安装
|
|
417
|
+
print("检测到 npm 全局安装")
|
|
418
|
+
print("正在更新...")
|
|
419
|
+
result = _sp.run(["npm", "install", "-g", "remote-claude@latest"])
|
|
420
|
+
if result.returncode != 0:
|
|
421
|
+
print("❌ npm 更新失败")
|
|
422
|
+
return 1
|
|
423
|
+
print("✅ 更新完成,请重启终端使新版本生效")
|
|
424
|
+
return 0
|
|
425
|
+
|
|
426
|
+
|
|
390
427
|
def cmd_lark(args):
|
|
391
428
|
"""飞书客户端管理(兼容旧命令)"""
|
|
392
429
|
# 如果没有子命令,默认显示状态或启动
|
|
@@ -531,6 +568,10 @@ def main():
|
|
|
531
568
|
)
|
|
532
569
|
stats_parser.set_defaults(func=cmd_stats)
|
|
533
570
|
|
|
571
|
+
# update 命令
|
|
572
|
+
update_parser = subparsers.add_parser("update", help="更新 remote-claude 到最新版本")
|
|
573
|
+
update_parser.set_defaults(func=cmd_update)
|
|
574
|
+
|
|
534
575
|
args = parser.parse_args()
|
|
535
576
|
|
|
536
577
|
if args.command is None:
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
init_install 上报脚本
|
|
3
|
+
|
|
4
|
+
每次 init.sh 执行时调用(npm 安装和源码安装均覆盖),发送 init_install 事件到 Mixpanel。
|
|
5
|
+
仅依赖标准库,不需要 mixpanel 包,失败静默。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import platform
|
|
11
|
+
import urllib.request
|
|
12
|
+
import uuid
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_TOKEN = 'c4d804fc1fe4337132e4da90fdb690c9'
|
|
16
|
+
_USER_DIR = Path.home() / '.remote-claude'
|
|
17
|
+
_ID_FILE = _USER_DIR / 'machine-id'
|
|
18
|
+
_REPORTED_VERSION_FILE = _USER_DIR / 'init_install_version'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_machine_id() -> str:
|
|
22
|
+
if _ID_FILE.exists():
|
|
23
|
+
try:
|
|
24
|
+
mid = _ID_FILE.read_text().strip()
|
|
25
|
+
if mid:
|
|
26
|
+
return mid
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
mid = str(uuid.uuid4())
|
|
30
|
+
try:
|
|
31
|
+
_USER_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
_ID_FILE.write_text(mid)
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
return mid
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_version() -> str:
|
|
39
|
+
try:
|
|
40
|
+
pkg = Path(__file__).parent.parent / 'package.json'
|
|
41
|
+
return json.loads(pkg.read_text()).get('version', 'unknown')
|
|
42
|
+
except Exception:
|
|
43
|
+
return 'unknown'
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main() -> None:
|
|
47
|
+
machine_id = _get_machine_id()
|
|
48
|
+
version = _get_version()
|
|
49
|
+
|
|
50
|
+
# 每个版本只上报一次
|
|
51
|
+
try:
|
|
52
|
+
if _REPORTED_VERSION_FILE.exists() and _REPORTED_VERSION_FILE.read_text().strip() == version:
|
|
53
|
+
return
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
props = {
|
|
57
|
+
'token': _TOKEN,
|
|
58
|
+
'distinct_id': machine_id,
|
|
59
|
+
'version': version,
|
|
60
|
+
'hostname': platform.node(),
|
|
61
|
+
'os': f'{platform.system()} {platform.release()}',
|
|
62
|
+
'python': platform.python_version(),
|
|
63
|
+
}
|
|
64
|
+
data = base64.b64encode(json.dumps([{
|
|
65
|
+
'event': 'init_install',
|
|
66
|
+
'properties': props,
|
|
67
|
+
}]).encode()).decode()
|
|
68
|
+
req = urllib.request.Request(
|
|
69
|
+
'https://api.mixpanel.com/track',
|
|
70
|
+
data=f'data={data}'.encode(),
|
|
71
|
+
method='POST',
|
|
72
|
+
)
|
|
73
|
+
urllib.request.urlopen(req, timeout=10)
|
|
74
|
+
|
|
75
|
+
# 记录已上报版本
|
|
76
|
+
try:
|
|
77
|
+
_REPORTED_VERSION_FILE.write_text(version)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == '__main__':
|
|
83
|
+
try:
|
|
84
|
+
main()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
package/server/server.py
CHANGED
|
@@ -35,9 +35,16 @@ from utils.protocol import (
|
|
|
35
35
|
)
|
|
36
36
|
from utils.session import (
|
|
37
37
|
get_socket_path, get_pid_file, ensure_socket_dir,
|
|
38
|
-
generate_client_id, cleanup_session, _safe_filename
|
|
38
|
+
generate_client_id, cleanup_session, _safe_filename, get_env_file
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
+
# 加载用户 .env 配置(支持 CLAUDE_COMMAND 等)
|
|
42
|
+
try:
|
|
43
|
+
from dotenv import load_dotenv
|
|
44
|
+
load_dotenv(get_env_file())
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
41
48
|
try:
|
|
42
49
|
from stats import track as _track_stats
|
|
43
50
|
except Exception:
|
|
@@ -533,9 +540,11 @@ class ProxyServer:
|
|
|
533
540
|
"""Proxy Server"""
|
|
534
541
|
|
|
535
542
|
def __init__(self, session_name: str, claude_args: list = None,
|
|
543
|
+
claude_cmd: str = "claude",
|
|
536
544
|
debug_screen: bool = False, debug_verbose: bool = False):
|
|
537
545
|
self.session_name = session_name
|
|
538
546
|
self.claude_args = claude_args or []
|
|
547
|
+
self.claude_cmd = claude_cmd
|
|
539
548
|
self.debug_screen = debug_screen
|
|
540
549
|
self.debug_verbose = debug_verbose
|
|
541
550
|
self.socket_path = get_socket_path(session_name)
|
|
@@ -638,7 +647,9 @@ class ProxyServer:
|
|
|
638
647
|
# 清除 tmux 标识变量(PTY 数据不经过 tmux,不应让 Claude CLI 误判终端环境)
|
|
639
648
|
child_env.pop('TMUX', None)
|
|
640
649
|
child_env.pop('TMUX_PANE', None)
|
|
641
|
-
|
|
650
|
+
import shlex as _shlex
|
|
651
|
+
_cmd_parts = _shlex.split(self.claude_cmd)
|
|
652
|
+
os.execvpe(_cmd_parts[0], _cmd_parts + self.claude_args, child_env)
|
|
642
653
|
os._exit(1) # execvpe 失败时兜底退出
|
|
643
654
|
else:
|
|
644
655
|
# 父进程
|
|
@@ -801,10 +812,11 @@ class ProxyServer:
|
|
|
801
812
|
|
|
802
813
|
|
|
803
814
|
def run_server(session_name: str, claude_args: list = None,
|
|
815
|
+
claude_cmd: str = "claude",
|
|
804
816
|
debug_screen: bool = False, debug_verbose: bool = False):
|
|
805
817
|
"""运行服务器"""
|
|
806
|
-
server = ProxyServer(session_name, claude_args,
|
|
807
|
-
debug_verbose=debug_verbose)
|
|
818
|
+
server = ProxyServer(session_name, claude_args, claude_cmd=claude_cmd,
|
|
819
|
+
debug_screen=debug_screen, debug_verbose=debug_verbose)
|
|
808
820
|
|
|
809
821
|
# 信号处理
|
|
810
822
|
def signal_handler(signum, frame):
|
|
@@ -832,5 +844,6 @@ if __name__ == "__main__":
|
|
|
832
844
|
help="debug 日志输出完整诊断信息(indicator、repr 等)")
|
|
833
845
|
args = parser.parse_args()
|
|
834
846
|
|
|
835
|
-
|
|
836
|
-
|
|
847
|
+
claude_cmd = os.environ.get("CLAUDE_COMMAND", "claude")
|
|
848
|
+
run_server(args.session_name, args.claude_args, claude_cmd=claude_cmd,
|
|
849
|
+
debug_screen=args.debug_screen, debug_verbose=args.debug_verbose)
|
package/stats/collector.py
CHANGED
|
@@ -7,6 +7,7 @@ StatsCollector:事件收集器
|
|
|
7
7
|
- 所有异常均被捕获,绝不影响主流程
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import logging
|
|
10
11
|
import os
|
|
11
12
|
import sqlite3
|
|
12
13
|
import threading
|
|
@@ -15,6 +16,8 @@ from collections import deque
|
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Optional
|
|
17
18
|
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
18
21
|
from .machine import get_machine_id, get_machine_info
|
|
19
22
|
|
|
20
23
|
# SQLite 数据库路径(持久化,不受 /tmp 清理影响)
|
|
@@ -321,16 +324,49 @@ class StatsCollector:
|
|
|
321
324
|
|
|
322
325
|
def report_install(self) -> None:
|
|
323
326
|
"""首次运行时上报 install 事件和 user profile"""
|
|
324
|
-
if not self.
|
|
327
|
+
if not self._is_first_run or not self._mp_token:
|
|
325
328
|
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
329
|
+
|
|
330
|
+
machine_info = get_machine_info()
|
|
331
|
+
machine_id = self._machine_id
|
|
332
|
+
mp_token = self._mp_token
|
|
333
|
+
mp = self._mp # 可能为 None(mixpanel 包未安装时)
|
|
334
|
+
|
|
335
|
+
def _send() -> None:
|
|
336
|
+
try:
|
|
337
|
+
import base64
|
|
338
|
+
import json
|
|
339
|
+
import urllib.request
|
|
340
|
+
|
|
341
|
+
props = {**machine_info, 'token': mp_token, 'distinct_id': machine_id}
|
|
342
|
+
data = base64.b64encode(json.dumps([{
|
|
343
|
+
'event': 'install',
|
|
344
|
+
'properties': props,
|
|
345
|
+
}]).encode()).decode()
|
|
346
|
+
req = urllib.request.Request(
|
|
347
|
+
'https://api.mixpanel.com/track',
|
|
348
|
+
data=f'data={data}'.encode(),
|
|
349
|
+
method='POST',
|
|
350
|
+
)
|
|
351
|
+
urllib.request.urlopen(req, timeout=10)
|
|
352
|
+
logger.debug('install event sent via urllib (machine_id=%s)', machine_id)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.warning('install event failed: %s', e)
|
|
355
|
+
|
|
356
|
+
# 同时更新 user profile(仅 SDK 可用时)
|
|
357
|
+
if mp:
|
|
358
|
+
try:
|
|
359
|
+
import datetime
|
|
360
|
+
mp.people_set(machine_id, {
|
|
361
|
+
'$name': machine_info['hostname'],
|
|
362
|
+
**machine_info,
|
|
363
|
+
'first_seen': datetime.datetime.now().isoformat(),
|
|
364
|
+
})
|
|
365
|
+
logger.debug('people_set sent via mixpanel SDK')
|
|
366
|
+
except Exception as e:
|
|
367
|
+
logger.warning('people_set failed: %s', e)
|
|
368
|
+
else:
|
|
369
|
+
logger.debug('mixpanel SDK not available, skipping people_set')
|
|
370
|
+
|
|
371
|
+
t = threading.Thread(target=_send, daemon=True)
|
|
372
|
+
t.start()
|