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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Remote Claude 飞书客户端配置
2
2
 
3
+ # Claude CLI 命令(可选,默认 claude)
4
+ # 支持多词命令,如:ccr code、/usr/local/bin/claude
5
+ # CLAUDE_COMMAND=claude
6
+
3
7
  # 飞书应用配置(必填)
4
8
  # 在飞书开发者后台创建应用获取: https://open.feishu.cn/app
5
9
  FEISHU_APP_ID=cli_xxxxx
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
@@ -369,6 +369,9 @@ install_dependencies() {
369
369
  fi
370
370
 
371
371
  print_success "依赖安装完成"
372
+
373
+ # 上报 init_install 事件(后台执行,不阻塞,失败静默)
374
+ python3 scripts/report_install.py &>/dev/null &
372
375
  }
373
376
 
374
377
  # 配置飞书环境
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-claude",
3
- "version": "0.2.4",
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
- os.execvpe("claude", ["claude"] + self.claude_args, child_env)
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, debug_screen=debug_screen,
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
- run_server(args.session_name, args.claude_args, debug_screen=args.debug_screen,
836
- debug_verbose=args.debug_verbose)
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)
@@ -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._mp or not self._is_first_run:
327
+ if not self._is_first_run or not self._mp_token:
325
328
  return
326
- try:
327
- machine_info = get_machine_info()
328
- import datetime
329
- self._mp.people_set(self._machine_id, {
330
- '$name': machine_info['hostname'],
331
- **machine_info,
332
- 'first_seen': datetime.datetime.now().isoformat(),
333
- })
334
- self._mp_track('install', machine_info)
335
- except Exception:
336
- pass
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()