weclaude 0.0.4

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/cli/wrc.sh +168 -0
  4. package/commands/wrc.md +7 -0
  5. package/config.example.jsonc +75 -0
  6. package/dist/cli/init.js +216 -0
  7. package/dist/cli/init.js.map +1 -0
  8. package/dist/cli/sync.js +130 -0
  9. package/dist/cli/sync.js.map +1 -0
  10. package/dist/daemon/approval.js +366 -0
  11. package/dist/daemon/approval.js.map +1 -0
  12. package/dist/daemon/ask.js +97 -0
  13. package/dist/daemon/ask.js.map +1 -0
  14. package/dist/daemon/cc-bridge.js +173 -0
  15. package/dist/daemon/cc-bridge.js.map +1 -0
  16. package/dist/daemon/claim.js +76 -0
  17. package/dist/daemon/claim.js.map +1 -0
  18. package/dist/daemon/http.js +82 -0
  19. package/dist/daemon/http.js.map +1 -0
  20. package/dist/daemon/inbound.js +145 -0
  21. package/dist/daemon/inbound.js.map +1 -0
  22. package/dist/daemon/index.js +85 -0
  23. package/dist/daemon/index.js.map +1 -0
  24. package/dist/daemon/mirror-bridge.js +539 -0
  25. package/dist/daemon/mirror-bridge.js.map +1 -0
  26. package/dist/daemon/outbound.js +33 -0
  27. package/dist/daemon/outbound.js.map +1 -0
  28. package/dist/daemon/pending.js +27 -0
  29. package/dist/daemon/pending.js.map +1 -0
  30. package/dist/daemon/redact.js +27 -0
  31. package/dist/daemon/redact.js.map +1 -0
  32. package/dist/daemon/session-cache.js +66 -0
  33. package/dist/daemon/session-cache.js.map +1 -0
  34. package/dist/daemon/sessions.js +35 -0
  35. package/dist/daemon/sessions.js.map +1 -0
  36. package/dist/daemon/ws.js +67 -0
  37. package/dist/daemon/ws.js.map +1 -0
  38. package/dist/mcp/server.js +82 -0
  39. package/dist/mcp/server.js.map +1 -0
  40. package/dist/shared/config-writer.js +39 -0
  41. package/dist/shared/config-writer.js.map +1 -0
  42. package/dist/shared/config.js +139 -0
  43. package/dist/shared/config.js.map +1 -0
  44. package/dist/shared/log.js +18 -0
  45. package/dist/shared/log.js.map +1 -0
  46. package/dist/shared/paths.js +6 -0
  47. package/dist/shared/paths.js.map +1 -0
  48. package/docs/DESIGN-INIT.md +125 -0
  49. package/docs/ONBOARDING.md +118 -0
  50. package/hooks/hooks.json +16 -0
  51. package/hooks/pre-tool-use.sh +81 -0
  52. package/launchd/com.cc-wecom.daemon.plist.template +45 -0
  53. package/package.json +77 -0
  54. package/scripts/install.sh +50 -0
  55. package/scripts/uninstall.sh +26 -0
  56. package/systemd/cc-wecom.service.template +17 -0
@@ -0,0 +1,125 @@
1
+ # Init flow 设计
2
+
3
+ ## 目标
4
+
5
+ 让没改过任何 dotfile 的新用户,从 `git clone` 到「在企业微信点按钮放行 Claude 写文件」少于 5 分钟,零手工编辑配置。
6
+
7
+ ## 三个被忽略的难点
8
+
9
+ ### 1. allowFrom 的鸡生蛋
10
+
11
+ 正常运行时 daemon 严格按 `wrc.allowFrom` 白名单转发;用户不在白名单里就被丢弃。但**第一次**用户的 IM userid 还没被任何人知道(你不能预填,你不知道自己企业微信 userid 是多少)。
12
+
13
+ **方案**:daemon 加 `claim` 模式。短窗口内(10 min),匹配指定暗号的第一条消息绕过 allowFrom,把发送方的 principal 同时写进 `defaultChat` 和 `allowFrom`。一次性消费。
14
+
15
+ 为什么不让用户先去后台查 userid 填进 config:
16
+
17
+ - 用户体验断点。打开管理后台找 userid 是个高摩擦动作。
18
+ - 群聊 chatid 在后台根本看不到,必须从消息流里捞。
19
+ - 反正消息进来时 daemon 已经知道 principal;让它自己写比让人抄稳。
20
+
21
+ **为什么不直接关掉 allowFrom 让任何人都能用**:会被任意外部联系人触发 `claude -p` 跑代码。安全模型必须保留白名单,bootstrap 是唯一豁免点,且单次。
22
+
23
+ ### 2. 密钥不能进 dotfile repo
24
+
25
+ 如果 `config.jsonc` 整个塞进 dotfile,`secret` 就会进 git。
26
+
27
+ **方案**:复用 `loadConfig()` 里早就有的 `secrets.json` deep-merge 钩子。init 时把 `botId/secret` 写到 `~/.cc-wecom/secrets.json`,其他写 `config.jsonc`。两份文件运行时合并。`config.jsonc` 可入 dotfiles,`secrets.json` 留本机。
28
+
29
+ ### 3. 配置写入要保留注释
30
+
31
+ `config.example.jsonc` 是带注释的 JSONC,新手要靠注释理解每个字段。如果 init 用 `JSON.parse` + `JSON.stringify` 重写,注释全没。
32
+
33
+ **方案**:用 `jsonc-parser.modify` 做外科级 patch(已经是 `cli/sync.ts` 的依赖)。只动指定 path 的值,其他字节保留。
34
+
35
+ ```ts
36
+ patchJsonc(CONFIG, [
37
+ { path: ["bot", "websocketUrl"], value: "wss://..." },
38
+ { path: ["wrc", "claudeBin"], value: claudeBin },
39
+ ])
40
+ ```
41
+
42
+ ## 三步划分的依据
43
+
44
+ 不是三步随便选的,是**必须**三步:
45
+
46
+ | 步 | 干什么 | 必须独立的原因 |
47
+ | --- | --- | --- |
48
+ | 1 | 写 config + 装 daemon | daemon 起不来后面都不用谈 |
49
+ | 2 | 跨进程 IM 握手 | 必须等真人在另一个客户端发消息,是异步的等待点 |
50
+ | 3 | 端到端 smoke | 验证授权链路和 outbound 链路双向通 |
51
+
52
+ 把第 2 步合并进第 1 步是不行的——daemon 必须先完整启动并连上 WS,才有人接得到那条暗号。
53
+
54
+ 把第 3 步省掉是不行的——hook + MCP + WS + 进程产物链路只有一种验证方式:让 Claude 真的跑一次。装好不演示,第一次真用还是会踩坑。
55
+
56
+ ## 为什么没用 ink
57
+
58
+ 考虑过 ink,否决理由:
59
+
60
+ - 当前仓库纯 TS,无 React,无 JSX,tsconfig 没开 `jsx`。引入 ink → 加 `react`、`@types/react`、改 tsconfig、新建 `.tsx` 文件,体积和复杂度都涨。
61
+ - 这个流程是**严格顺序**的 4 个 prompt + 几段 status 输出,没有并发可视化、没有持久面板、没有键盘焦点。这是 inquirer 的舒适区,不是 ink 的。
62
+ - `@inquirer/prompts` 函数式 API(`await input(...)`、`await select(...)`)和仓库里其他模块的风格一致:纯函数 + 副作用推到边界。
63
+
64
+ 如果以后要做 daemon dashboard / 任务队列实时面板,再上 ink。现在先不上。
65
+
66
+ ## claim 模式的状态机
67
+
68
+ ```
69
+ POST /claim/start
70
+
71
+ ┌──────▼──────┐
72
+ │ ARMED │ phrase, expiresAt
73
+ └──────┬──────┘
74
+
75
+ ┌──────┴──────┬─────────────┐
76
+ │ │ │
77
+ inbound TTL POST /claim/reset
78
+ matches expires │
79
+ │ │ │
80
+ ▼ ▼ ▼
81
+ CLAIMED (cleared) (cleared)
82
+
83
+ └─→ persistClaim(cfg, sourcePath, principal)
84
+ └─→ defaultChat = principal
85
+ allowFrom += principal ← 同时改磁盘 + 改内存
86
+ └─→ ackClaim(client, principal)
87
+ ```
88
+
89
+ `state` 是模块级单例,单实例 daemon 不需要锁。如果以后要多 worker,state 要挪进 daemon 共享存储——但 WS 客户端本身就是单连接的,不会有这个问题。
90
+
91
+ ## 演示选什么 prompt
92
+
93
+ ```
94
+ 1. Bash: echo hello world from cc-wecom
95
+ 2. Bash: sleep 3
96
+ 3. wecom__send_markdown chat="<defaultChat>" content="✅ ..."
97
+ ```
98
+
99
+ 设计取舍:
100
+
101
+ - **必须用 Bash 工具**,因为 `approval.matcher = ".*"` 默认全拦,但 `pre-tool-use.sh` 里对 `Bash` 的只读命令(grep/ls/cat...)有 fast-path 直接 allow。`echo` 不在 fast-path 里,会真触发卡片。
102
+ - **第二次 sleep** 是为了让用户看到「点了一次 ✅,下一次还会问」是默认行为;如果他想免打扰可以点 `✅ N 分钟` 开窗口。
103
+ - **MCP send_markdown** 而不是 `claude` 自己 print:验证的是「主动推送回 IM」这条链路,普通 print 只回流到终端。
104
+
105
+ prompt 里**显式拼出 chat id**而不是依赖 LLM 推断「default chat」,因为:MCP 工具签名要求 `chat` 是显式 string,没有「default」概念。让 LLM 猜是不必要的脆弱性。
106
+
107
+ ## 失败模式
108
+
109
+ | 场景 | init 行为 | 状态可恢复性 |
110
+ | --- | --- | --- |
111
+ | 用户 Ctrl-C 在 prompt 中 | 进程退出,未写任何文件 | ✅ 完全干净 |
112
+ | 写完 config 但 daemon 起不来 | 抛 `daemon did not become ready` | ✅ config 保留,下次 `wrc reload` 即可 |
113
+ | Claim 超时 | 报「超时未收到消息」并退出 | ✅ daemon 还在跑,可手填 config 或重跑 init |
114
+ | 演示 spawn `claude` 失败 | 红字打印 spawn 错误,正常退出 | ✅ 前两步成果都保留 |
115
+
116
+ 没有「半完成」的中间状态需要回滚。
117
+
118
+ ## 后续可能的改进
119
+
120
+ 不在本次范围,但留给未来:
121
+
122
+ - `wrc init --reset` 一键清空(unsync + uninstall daemon + 删 ~/.cc-wecom)
123
+ - claim 暗号支持自定义(防多人共用同一台 daemon 的暗号冲突)
124
+ - 演示步骤后做一次 `wrc status` 的彩色断言,给用户一个「绿色对勾合集」
125
+ - ink 化的 daemon dashboard(独立工具,`wrc tui`)
@@ -0,0 +1,118 @@
1
+ # cc-wecom 上手指南
2
+
3
+ 3 步在一台新机器上把「IM 授权转发 + 远程 CC 控制」跑通。
4
+
5
+ ## 前置
6
+
7
+ - Node ≥ 20
8
+ - 已安装 `claude` 或 `claude-internal`
9
+ - 一个企业微信「智能机器人」:拿到 `botId` 和 `secret`
10
+ - 创建入口:企业微信管理后台 → 应用管理 → 智能机器人
11
+
12
+ ## 步骤
13
+
14
+ ```bash
15
+ # 方式 A:npm 全局安装(推荐)
16
+ npm install -g cc-wecom
17
+ wrc init
18
+
19
+ # 方式 B:免安装一次性试用
20
+ npx cc-wecom init
21
+
22
+ # 方式 C:本地开发
23
+ git clone <repo> && cd cc-wecom
24
+ npm install && npm run build
25
+ ./cli/wrc.sh init
26
+ ```
27
+
28
+ > ⚠️ 卸载时**先**跑 `wrc uninstall`(unsync settings.json + 卸载 daemon),**再** `npm uninstall -g cc-wecom`。否则 launchd/systemd 会反复尝试启动已删除的二进制。
29
+ > `~/.cc-wecom/` 下的 config/secrets/state 不会被清掉,二次安装可无缝复用。
30
+
31
+ ### [1/3] 采集配置
32
+
33
+ 交互式问 4 个值:
34
+
35
+ | 字段 | 写到 |
36
+ | --- | --- |
37
+ | botId | `~/.cc-wecom/secrets.json` |
38
+ | secret | `~/.cc-wecom/secrets.json` |
39
+ | Claude agent | `~/.cc-wecom/config.jsonc` (`wrc.claudeBin` + `sync.targets[0].settingsPath`) |
40
+ | 是否启用 PreToolUse hook | `~/.cc-wecom/config.jsonc` (`approval.enabled`) |
41
+
42
+ Agent 选项:
43
+
44
+ - `claude` → 写到 `~/.claude/settings.json`
45
+ - `claude-internal` → 写到 `~/.claude-internal/settings.json`
46
+ - `custom` → 你给的绝对路径
47
+
48
+ `secrets.json` 和 `config.jsonc` 在 `loadConfig` 里 deep-merge,方便把 `config.jsonc` dotfile 化但 secrets 留在本机。
49
+
50
+ 完成后自动:
51
+ 1. `npx tsc` 编译 dist
52
+ 2. `wrc sync` 把 hook / MCP / env 写进选定的 settings.json
53
+ 3. 安装常驻 daemon(macOS launchd / Linux systemd --user)
54
+ 4. 等 WebSocket 鉴权通过
55
+
56
+ ### [2/3] 绑定默认会话
57
+
58
+ CLI 提示你**在企业微信里**给机器人发:
59
+
60
+ ```
61
+ 将本对话设置为默认会话
62
+ ```
63
+
64
+ Daemon 在内存里临时打开 claim 窗口(10 分钟)。这条消息精确匹配后:
65
+
66
+ - 把发送方 principal(`user:<id>` 或 `chat:<id>`)写到 `defaultChat`
67
+ - 把同一个 principal 加进 `wrc.allowFrom`(去重)
68
+ - 落盘 + 同步内存里的 cfg 对象
69
+ - 给你回一条 markdown ack
70
+
71
+ 这一步是**唯一一次**绕过 `allowFrom` 检查的入口;消费完立刻关闭。后续所有消息严格按 `allowFrom` 鉴权。
72
+
73
+ ### [3/3] 授权转发演示
74
+
75
+ CLI 自动 `spawn claude -p` 跑一条三步指令:
76
+
77
+ ```
78
+ 1. Bash: echo hello world from cc-wecom ← 触发 PreToolUse hook
79
+ 2. Bash: sleep 3 ← 再触发一次
80
+ 3. wecom__send_markdown ← 通过 MCP 主动推送结果
81
+ ```
82
+
83
+ 期望体验:
84
+
85
+ - IM 收到按钮卡片:`授权请求: Bash`
86
+ - 点 ✅ → 卡片就地刷新成 `✅ Bash · 已允许`
87
+ - 3 秒后再来一张卡片(或被 5 分钟自动窗口短路)
88
+ - 最终收到 `✅ cc-wecom 演示完成:hello world`
89
+
90
+ 如果第 1 步选「不开 hook」,会跳过演示。
91
+
92
+ ## 完成后
93
+
94
+ ```bash
95
+ wrc status # daemon + WS 健康
96
+ wrc logs -f # 实时日志
97
+ wrc send <chat> <text> # 主动推消息
98
+ wrc unsync # 卸载 hook/MCP(保留 daemon)
99
+ ```
100
+
101
+ ## 排错
102
+
103
+ | 现象 | 处理 |
104
+ | --- | --- |
105
+ | `daemon: down` | `wrc reload`;看 `~/.cc-wecom/daemon.stderr.log` |
106
+ | 发完暗号 daemon 没反应 | 检查 `wrc status` 的 `wsConnected` 是不是 true;机器人 `secret` 错会卡在 auth |
107
+ | Hook 不触发 | `cat ~/.claude/settings.json | jq .hooks.PreToolUse`;`wrc sync` 重写 |
108
+ | 卡片点了没反应 | 5 秒内才能就地更新;超时是正常情况,决策仍然生效 |
109
+ | MCP 调用 404 | `mcpServers.wecom._managedBy=="cc-wecom"` 应在 settings.json;`wrc sync` 修复 |
110
+
111
+ ## 多机部署
112
+
113
+ `config.jsonc` 可以纳入 dotfiles;`secrets.json` 每台机器独立填。`init` 跑过一次后,第二台机器:
114
+
115
+ ```bash
116
+ cp ~/dotfiles/cc-wecom-config.jsonc ~/.cc-wecom/config.jsonc
117
+ ./cli/wrc.sh init # 跳过覆盖提示,但仍走 claim 步骤拿到本机的 IM principal
118
+ ```
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": ".*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.sh",
10
+ "timeout": 1810
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # cc-wecom PreToolUse hook: forward to local daemon → long-poll → emit decision.
3
+ # Any failure → ask (never break workflow).
4
+ set -uo pipefail
5
+
6
+ DAEMON_URL="${CC_WECOM_DAEMON_URL:-http://127.0.0.1:17890/approve}"
7
+ HOOK_TIMEOUT="${CC_WECOM_HOOK_TIMEOUT:-1810}"
8
+
9
+ emit() {
10
+ local decision="$1" reason="${2:-}"
11
+ printf '%s\n' "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"${decision}\",\"permissionDecisionReason\":\"cc-wecom: ${reason}\"}}"
12
+ exit 0
13
+ }
14
+ ask() { emit "ask" "${1:-bridge unreachable}"; }
15
+
16
+ PAYLOAD=$(cat) || ask "stdin read failed"
17
+ command -v jq >/dev/null 2>&1 || ask "jq missing"
18
+
19
+ SESSION_ID=$(printf '%s' "$PAYLOAD" | jq -r '.session_id // empty')
20
+ TOOL_NAME=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty')
21
+ TOOL_INPUT=$(printf '%s' "$PAYLOAD" | jq -c '.tool_input // {}')
22
+ CWD=$(printf '%s' "$PAYLOAD" | jq -r '.cwd // ""')
23
+ TRANSCRIPT_PATH=$(printf '%s' "$PAYLOAD" | jq -r '.transcript_path // ""')
24
+
25
+ # Bash read-only fast-path: bypass cards for grep / rg etc.
26
+ if [[ "$TOOL_NAME" == "Bash" ]]; then
27
+ CMD=$(printf '%s' "$TOOL_INPUT" | jq -r '.command // ""')
28
+ if [[ "$CMD" =~ ^[[:space:]]*(grep|egrep|fgrep|rg|ls|cat|head|tail|wc|file)([[:space:]]|$) ]] \
29
+ && [[ ! "$CMD" =~ [\;\|\&\>\<\`\$\(] ]]; then
30
+ emit "allow" "read-only bypass"
31
+ fi
32
+ fi
33
+
34
+ # Tail of recent user messages for context on the card.
35
+ # 注意 .message.content 可能是 string 或 content blocks 数组;过滤 tool_result 与
36
+ # Claude Code 注入的 <system-reminder>/<command-*>/<local-command-*> 包裹标签。
37
+ TRANSCRIPT_TAIL=""
38
+ if [[ -n "$TRANSCRIPT_PATH" && -r "$TRANSCRIPT_PATH" ]]; then
39
+ TRANSCRIPT_TAIL=$(tail -n 200 "$TRANSCRIPT_PATH" 2>/dev/null \
40
+ | jq -r '
41
+ select(.type == "user" or .role == "user")
42
+ | select((.isMeta // false) == false)
43
+ | (.message.content // .content) as $c
44
+ | ( if ($c | type) == "string" then $c
45
+ elif ($c | type) == "array" then
46
+ ([ $c[]? | select(.type == "text") | .text ] | join("\n"))
47
+ else "" end )
48
+ | gsub("(?s)<system-reminder>.*?</system-reminder>"; "")
49
+ | gsub("(?s)<command-name>.*?</command-name>"; "")
50
+ | gsub("(?s)<command-message>.*?</command-message>"; "")
51
+ | gsub("(?s)<command-args>.*?</command-args>"; "")
52
+ | gsub("(?s)<local-command-stdout>.*?</local-command-stdout>"; "")
53
+ | gsub("(?s)<local-command-caveat>.*?</local-command-caveat>"; "")
54
+ | gsub("\\s+"; " ")
55
+ | sub("^\\s+"; "") | sub("\\s+$"; "")
56
+ | select(length > 0)
57
+ ' 2>/dev/null \
58
+ | tail -n 3 \
59
+ | head -c 800 || true)
60
+ fi
61
+
62
+ BODY=$(jq -nc \
63
+ --arg sid "$SESSION_ID" \
64
+ --arg tn "$TOOL_NAME" \
65
+ --argjson ti "$TOOL_INPUT" \
66
+ --arg cwd "$CWD" \
67
+ --arg tail "$TRANSCRIPT_TAIL" \
68
+ '{session_id:$sid,tool_name:$tn,tool_input:$ti,cwd:$cwd,transcript_tail:$tail}')
69
+
70
+ RESP=$(curl -sS --max-time "$HOOK_TIMEOUT" \
71
+ -H 'Content-Type: application/json' \
72
+ -d "$BODY" \
73
+ "$DAEMON_URL" 2>/dev/null) || ask "daemon curl failed"
74
+
75
+ DECISION=$(printf '%s' "$RESP" | jq -r '.decision // "ask"' 2>/dev/null) || ask "bad daemon response"
76
+ REASON=$(printf '%s' "$RESP" | jq -r '.reason // ""' 2>/dev/null)
77
+
78
+ case "$DECISION" in
79
+ allow|deny|ask) emit "$DECISION" "$REASON" ;;
80
+ *) ask "unknown decision: $DECISION" ;;
81
+ esac
@@ -0,0 +1,45 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <!-- Generated template — `scripts/install-launchd.sh` materializes the
4
+ real plist into ~/Library/LaunchAgents with absolute paths. -->
5
+ <plist version="1.0">
6
+ <dict>
7
+ <key>Label</key>
8
+ <string>com.cc-wecom.daemon</string>
9
+
10
+ <key>ProgramArguments</key>
11
+ <array>
12
+ <string>__NODE__</string>
13
+ <string>__REPO__/dist/daemon/index.js</string>
14
+ </array>
15
+
16
+ <key>WorkingDirectory</key>
17
+ <string>__REPO__</string>
18
+
19
+ <key>EnvironmentVariables</key>
20
+ <dict>
21
+ <key>PATH</key>
22
+ <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
23
+ <key>NODE_ENV</key>
24
+ <string>production</string>
25
+ </dict>
26
+
27
+ <key>RunAtLoad</key>
28
+ <true/>
29
+ <key>KeepAlive</key>
30
+ <dict>
31
+ <key>SuccessfulExit</key>
32
+ <false/>
33
+ <key>Crashed</key>
34
+ <true/>
35
+ </dict>
36
+
37
+ <key>ThrottleInterval</key>
38
+ <integer>10</integer>
39
+
40
+ <key>StandardOutPath</key>
41
+ <string>__HOME__/.cc-wecom/daemon.stdout.log</string>
42
+ <key>StandardErrorPath</key>
43
+ <string>__HOME__/.cc-wecom/daemon.stderr.log</string>
44
+ </dict>
45
+ </plist>
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "weclaude",
3
+ "version": "0.0.4",
4
+ "description": "WeCom remote control for Claude Code — IM-side approval forwarding + remote /wrc",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "claude",
9
+ "claude-code",
10
+ "wecom",
11
+ "wechat-work",
12
+ "remote-control",
13
+ "approval",
14
+ "mcp"
15
+ ],
16
+ "bin": {
17
+ "wrc": "./cli/wrc.sh",
18
+ "cc-wecom": "./cli/wrc.sh",
19
+ "wrc-daemon": "./dist/daemon/index.js",
20
+ "wrc-mcp": "./dist/mcp/server.js",
21
+ "wrc-init": "./dist/cli/init.js"
22
+ },
23
+ "files": [
24
+ "cli/wrc.sh",
25
+ "dist/cli/init.js",
26
+ "dist/cli/init.js.map",
27
+ "dist/cli/sync.js",
28
+ "dist/cli/sync.js.map",
29
+ "dist/daemon/**/*.js",
30
+ "dist/daemon/**/*.js.map",
31
+ "dist/mcp/**/*.js",
32
+ "dist/mcp/**/*.js.map",
33
+ "dist/shared/**/*.js",
34
+ "dist/shared/**/*.js.map",
35
+ "hooks/pre-tool-use.sh",
36
+ "hooks/hooks.json",
37
+ "scripts/install.sh",
38
+ "scripts/uninstall.sh",
39
+ "launchd/com.cc-wecom.daemon.plist.template",
40
+ "systemd/cc-wecom.service.template",
41
+ "config.example.jsonc",
42
+ "commands/wrc.md",
43
+ "README.md",
44
+ "docs/ONBOARDING.md",
45
+ "docs/DESIGN-INIT.md",
46
+ "LICENSE"
47
+ ],
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.json && chmod +x dist/cli/init.js dist/daemon/index.js dist/mcp/server.js",
50
+ "dev:daemon": "tsx daemon/index.ts",
51
+ "dev:cli": "tsx cli/wrc.ts",
52
+ "typecheck": "tsc --noEmit",
53
+ "prepublishOnly": "rm -rf dist && npm run build",
54
+ "prepack": "test -f dist/daemon/index.js || npm run build"
55
+ },
56
+ "dependencies": {
57
+ "@inquirer/prompts": "^7.2.0",
58
+ "@modelcontextprotocol/sdk": "^1.0.0",
59
+ "@tencent/claude-code-internal": "^1.1.9",
60
+ "@wecom/aibot-node-sdk": "^1.0.6",
61
+ "jsonc-parser": "^3.3.1",
62
+ "pino": "^9.5.0",
63
+ "zod": "^3.23.8"
64
+ },
65
+ "devDependencies": {
66
+ "@types/node": "^22.0.0",
67
+ "tsx": "^4.19.0",
68
+ "typescript": "^5.6.0"
69
+ },
70
+ "engines": {
71
+ "node": ">=20"
72
+ },
73
+ "os": [
74
+ "darwin",
75
+ "linux"
76
+ ]
77
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # Install resident daemon. Auto-detects macOS (launchd) vs Linux (systemd --user).
3
+ set -euo pipefail
4
+
5
+ REPO="$(cd "$(dirname "$0")/.." && pwd)"
6
+ NODE="$(command -v node)"
7
+ HOME_DIR="$HOME"
8
+ LABEL="com.cc-wecom.daemon"
9
+
10
+ [[ -x "$NODE" ]] || { echo "node not found in PATH"; exit 1; }
11
+
12
+ # Build if missing
13
+ if [[ ! -f "$REPO/dist/daemon/index.js" ]]; then
14
+ echo "[install] building..."
15
+ (cd "$REPO" && npm install --silent && npx tsc -p tsconfig.json)
16
+ fi
17
+
18
+ mkdir -p "$HOME_DIR/.cc-wecom"
19
+
20
+ OS="$(uname -s)"
21
+ case "$OS" in
22
+ Darwin)
23
+ PLIST_DST="$HOME_DIR/Library/LaunchAgents/${LABEL}.plist"
24
+ sed \
25
+ -e "s|__NODE__|$NODE|g" \
26
+ -e "s|__REPO__|$REPO|g" \
27
+ -e "s|__HOME__|$HOME_DIR|g" \
28
+ "$REPO/launchd/${LABEL}.plist.template" > "$PLIST_DST"
29
+ launchctl unload "$PLIST_DST" 2>/dev/null || true
30
+ launchctl load -w "$PLIST_DST"
31
+ echo "[install] launchd loaded: $PLIST_DST"
32
+ ;;
33
+ Linux)
34
+ UNIT_DIR="$HOME_DIR/.config/systemd/user"
35
+ mkdir -p "$UNIT_DIR"
36
+ UNIT_DST="$UNIT_DIR/cc-wecom.service"
37
+ sed \
38
+ -e "s|__NODE__|$NODE|g" \
39
+ -e "s|__REPO__|$REPO|g" \
40
+ -e "s|__HOME__|$HOME_DIR|g" \
41
+ "$REPO/systemd/cc-wecom.service.template" > "$UNIT_DST"
42
+ systemctl --user daemon-reload
43
+ systemctl --user enable --now cc-wecom.service
44
+ echo "[install] systemd unit enabled: $UNIT_DST"
45
+ ;;
46
+ *)
47
+ echo "unsupported OS: $OS"; exit 1 ;;
48
+ esac
49
+
50
+ echo "[install] done. Logs at $HOME_DIR/.cc-wecom/daemon.{stdout,stderr,log}"
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ LABEL="com.cc-wecom.daemon"
4
+ HOME_DIR="$HOME"
5
+ OS="$(uname -s)"
6
+
7
+ case "$OS" in
8
+ Darwin)
9
+ PLIST="$HOME_DIR/Library/LaunchAgents/${LABEL}.plist"
10
+ if [[ -f "$PLIST" ]]; then
11
+ launchctl unload "$PLIST" 2>/dev/null || true
12
+ rm -f "$PLIST"
13
+ echo "[uninstall] removed $PLIST"
14
+ fi
15
+ ;;
16
+ Linux)
17
+ UNIT="$HOME_DIR/.config/systemd/user/cc-wecom.service"
18
+ if [[ -f "$UNIT" ]]; then
19
+ systemctl --user disable --now cc-wecom.service 2>/dev/null || true
20
+ rm -f "$UNIT"
21
+ systemctl --user daemon-reload
22
+ echo "[uninstall] removed $UNIT"
23
+ fi
24
+ ;;
25
+ esac
26
+ echo "[uninstall] done."
@@ -0,0 +1,17 @@
1
+ [Unit]
2
+ Description=cc-wecom daemon (WeCom remote control for Claude Code)
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ WorkingDirectory=__REPO__
9
+ ExecStart=__NODE__ __REPO__/dist/daemon/index.js
10
+ Restart=always
11
+ RestartSec=5
12
+ StandardOutput=append:__HOME__/.cc-wecom/daemon.stdout.log
13
+ StandardError=append:__HOME__/.cc-wecom/daemon.stderr.log
14
+ Environment=NODE_ENV=production
15
+
16
+ [Install]
17
+ WantedBy=default.target