weacpx 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gadzan Mak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # weacpx
2
+
3
+ 通过微信 ClawBot 远程控制通过 `acpx` 控制 Claude Code、Codex 等 Agents。
4
+
5
+ ## weacpx 是什么
6
+
7
+ `weacpx` 基于以下组件工作:
8
+
9
+ - `weixin-agent-sdk`
10
+ - `acpx`
11
+ - `acpx` 已支持的 agent driver,或自定义 ACP agent
12
+
13
+ 它适合这样的场景:
14
+
15
+ - 你已经在本机使用 `acpx`
16
+ - 你希望通过微信远程发起或继续一个 agent 会话
17
+ - 你希望在手机上完成常见的会话切换、目录切换和对话操作
18
+
19
+ ## 安装前准备
20
+
21
+ 开始前,至少需要:
22
+
23
+ - Node.js 22+
24
+ - Bun
25
+ - 一个可用的微信登录环境
26
+ - 本机可以运行 `acpx` 及其目标 agent
27
+
28
+ 正常情况下,不需要再额外全局安装 `acpx`。
29
+
30
+ ## 安装
31
+
32
+ 全局安装:
33
+
34
+ ```bash
35
+ # 使用 NPM 全局安装
36
+ npm install -g weacpx
37
+ # 或使用 bun 全局安装
38
+ bun add -g weacpx
39
+ ```
40
+
41
+ 如果你是从源码仓库直接使用,请先安装依赖并构建:
42
+
43
+ ```bash
44
+ bun install
45
+ bun run dev
46
+ ```
47
+
48
+ ## 快速开始
49
+
50
+ 第一次使用建议按这个顺序:
51
+
52
+ 1. 登录微信
53
+ 2. 启动服务
54
+ 3. 在微信里创建会话并开始对话
55
+
56
+ 如果你是全局安装版本:
57
+
58
+ ```bash
59
+ weacpx login
60
+ weacpx start
61
+ weacpx status
62
+ ```
63
+
64
+ 如果你是在仓库里本地运行:
65
+
66
+ ```bash
67
+ bun run login
68
+ bun run dev
69
+ ```
70
+
71
+ `weacpx login` 和 `bun run login` 都会在终端里显示二维码。
72
+
73
+ 启动后,在微信里先发:
74
+
75
+ ```text
76
+ /ss codex -d /absolute/path/to/your/repo
77
+
78
+ /help
79
+ ```
80
+
81
+ 第一行的意思是:开启或挂在一个会话,并切换到该会话。使用 Codex,并指定工作目录为 `/absolute/path/to/your/repo`。
82
+ 第二行的意思是:查看帮助信息。
83
+
84
+ 然后就可以直接发普通消息,例如:
85
+
86
+ ```text
87
+ hello
88
+ ```
89
+
90
+ 普通文本会默认发送到当前选中的 session。
91
+
92
+ ## CLI 命令
93
+
94
+ 常用命令:
95
+
96
+ - `weacpx login`
97
+ - `weacpx run`
98
+ - `weacpx start`
99
+ - `weacpx status`
100
+ - `weacpx stop`
101
+
102
+ 说明:
103
+
104
+ - `run` 前台运行,适合调试
105
+ - `start` 后台启动
106
+ - `status` 查看后台状态、PID、配置路径和日志路径
107
+ - `stop` 停止后台实例
108
+
109
+ ## 微信中如何使用
110
+
111
+ ### Agent
112
+
113
+ 内置 `codex` 与 `claude` 两个常见模板,也支持添加你自己的 agent。
114
+
115
+ - `/agents` 查看当前已添加的 agent
116
+ - `/agent add codex` 添加 codex agent
117
+ - `/agent add claude` 添加 claude agent
118
+ - `/agent rm <name>` 删除 agent
119
+
120
+ 说明:
121
+
122
+ - 内置 `codex` 和 `claude` 走 `acpx` 的 driver alias,通常不需要额外写 `agent.command`
123
+ - 如果你接入的是自定义 agent,再考虑显式配置 `agent.command`
124
+
125
+ ### Workspace 工作目录
126
+
127
+ - `/workspaces` 或 `/workspace` 或 `/ws` 可查看当前已添加的工作目录
128
+ - `/ws new <name> -d <path>` 添加工作目录,`-d` 后面接的是目录在电脑的绝对路径
129
+ - `/workspace rm <name>` 删除工作目录
130
+
131
+ 说明:
132
+
133
+ - `/ws new` 会先校验路径是否存在
134
+ - Windows 路径里如果有空格,请给 `-d` 或 `--cwd` 的值加引号,例如:`/ws new backend -d "E:\my projects\backend"`
135
+
136
+ ### Session 会话
137
+
138
+ - `/sessions` 或 `/session` 或 `/ss` 可查看当前已添加的会话
139
+ - `/ss <agent> -d <path>` 新建会话,会自动按目录名推导并创建或复用 workspace,再创建或复用 session
140
+ - `/ss new <agent> -d <path>` 强制新建会话
141
+ - `/ss new <alias> -a <name> --ws <name>` 强制新建会话,并指定 agent 和 workspace
142
+ - `/ss attach <alias> -a <name> --ws <name> --name <transport-session>` 恢复已存在的会话
143
+ - `/use <alias>` 切换当前会话
144
+ - `/status` 查看当前会话状态
145
+ - `/cancel` 取消当前会话
146
+ - `/stop` 停止当前会话
147
+
148
+ 说明:
149
+
150
+ - `/ss <agent> -d <path>` 是最常用入口,会自动按目录名推导并创建或复用 workspace,再创建或复用 session
151
+ - `/ss new <agent> -d <path>` 表示强制新建 session
152
+ - `/use <alias>` 用来切换当前会话
153
+ - 非 `/` 开头的文本会发送到当前 session
154
+
155
+ ### 推荐工作流
156
+
157
+ 新建一个可聊天的会话:
158
+
159
+ ```text
160
+ /ss codex -d /absolute/path/to/backend
161
+ 修一下最近这个接口超时问题
162
+ ```
163
+
164
+ 在同一个聊天里切换多个会话:
165
+
166
+ ```text
167
+ /ss new codex -d /absolute/path/to/backend
168
+ /ss
169
+ /use backend:codex
170
+ 看一下接口日志
171
+ /use backend:codex-2
172
+ 看一下前端报错
173
+ ```
174
+
175
+ 删除不再需要的资源:
176
+
177
+ ```text
178
+ /agent rm claude
179
+ /workspace rm old-repo
180
+ ```
181
+
182
+ ## 配置与运行文件
183
+
184
+ 默认文件位置:
185
+
186
+ - 配置文件:`~/.weacpx/config.json`
187
+ - 状态文件:`~/.weacpx/state.json`
188
+ - 运行日志:`~/.weacpx/runtime/app.log`
189
+
190
+ 后台运行时还会使用:
191
+
192
+ - `~/.weacpx/runtime/daemon.pid`
193
+ - `~/.weacpx/runtime/status.json`
194
+ - `~/.weacpx/runtime/stdout.log`
195
+ - `~/.weacpx/runtime/stderr.log`
196
+
197
+ 常用环境变量:
198
+
199
+ - `WEACPX_CONFIG`
200
+ - `WEACPX_STATE`
201
+ - `WEACPX_WEIXIN_SDK`
202
+
203
+ ### 日志配置
204
+
205
+ `config.json` 支持可选的 `logging` 配置:
206
+
207
+ ```json
208
+ {
209
+ "logging": {
210
+ "level": "info",
211
+ "maxSizeBytes": 2097152,
212
+ "maxFiles": 5,
213
+ "retentionDays": 7
214
+ }
215
+ }
216
+ ```
217
+
218
+ 说明:
219
+
220
+ - `level`: `error`、`info`、`debug`
221
+ - `maxSizeBytes`: 单个 `app.log` 文件达到上限后会轮转
222
+ - `maxFiles`: 最多保留多少个轮转文件
223
+ - `retentionDays`: 每次启动时会清理超过保留天数的旧轮转日志
224
+
225
+ ## 注意事项
226
+
227
+ ### `dry-run`
228
+
229
+ `dry-run` 会复用同一套 router、session service、transport,只是把微信消息换成终端输入,适合本地排查。
230
+
231
+ 示例:
232
+
233
+ ```bash
234
+ bun run dry-run --chat-key wx:test -- \
235
+ "/agent add codex" \
236
+ "/ws new backend -d /absolute/path/to/backend" \
237
+ "/ss new demo -a codex --ws backend" \
238
+ "/status"
239
+ ```
240
+
241
+ ### 如果 `/ss new` 失败
242
+
243
+ 当前最常见的问题仍然是底层 `acpx` named session 的运行时恢复,不一定是 `weacpx` 本身的逻辑问题。
244
+
245
+ 可以先在本地创建一个 named session,再在微信里 attach:
246
+
247
+ ```bash
248
+ ./node_modules/.bin/acpx --verbose --cwd /absolute/workspace/path codex sessions new --name existing-demo
249
+ ```
250
+
251
+ 然后在微信里:
252
+
253
+ ```text
254
+ /ss attach demo -a codex --ws backend --name existing-demo
255
+ ```
256
+
257
+ ## 更多文档
258
+
259
+ - 配置参考:[docs/config-reference.md](./docs/config-reference.md)
260
+ - 测试说明:[docs/testing.md](./docs/testing.md)
261
+ - 开发与贡献:[docs/development.md](./docs/development.md)
@@ -0,0 +1,30 @@
1
+ {
2
+ "transport": {
3
+ "type": "acpx-bridge",
4
+ "sessionInitTimeoutMs": 120000
5
+ },
6
+ "logging": {
7
+ "level": "info",
8
+ "maxSizeBytes": 2097152,
9
+ "maxFiles": 5,
10
+ "retentionDays": 7
11
+ },
12
+ "agents": {
13
+ "codex": {
14
+ "driver": "codex"
15
+ },
16
+ "claude": {
17
+ "driver": "claude"
18
+ }
19
+ },
20
+ "workspaces": {
21
+ "backend": {
22
+ "cwd": "/absolute/path/to/backend",
23
+ "description": "backend repo"
24
+ },
25
+ "weacpx-console": {
26
+ "cwd": "/Users/your-name/Projects/weacpx/.worktrees/weacpx-console-v1",
27
+ "description": "weacpx console worktree"
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,229 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
+
31
+ // src/bridge/bridge-main.ts
32
+ import { createInterface } from "node:readline";
33
+
34
+ // src/bridge/bridge-server.ts
35
+ class BridgeServer {
36
+ runtime;
37
+ constructor(runtime) {
38
+ this.runtime = runtime;
39
+ }
40
+ async handleLine(line) {
41
+ const request = JSON.parse(line);
42
+ try {
43
+ const result = await this.dispatch(request.method, request.params);
44
+ return `${JSON.stringify({
45
+ id: request.id,
46
+ ok: true,
47
+ result
48
+ })}
49
+ `;
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ return `${JSON.stringify({
53
+ id: request.id,
54
+ ok: false,
55
+ error: {
56
+ code: "BRIDGE_INTERNAL_ERROR",
57
+ message
58
+ }
59
+ })}
60
+ `;
61
+ }
62
+ }
63
+ async dispatch(method, params) {
64
+ switch (method) {
65
+ case "ping":
66
+ return {};
67
+ case "shutdown":
68
+ return await this.runtime.shutdown();
69
+ case "hasSession":
70
+ return await this.runtime.hasSession({
71
+ agent: String(params.agent),
72
+ agentCommand: asOptionalString(params.agentCommand),
73
+ cwd: String(params.cwd),
74
+ name: String(params.name)
75
+ });
76
+ case "ensureSession":
77
+ return await this.runtime.ensureSession({
78
+ agent: String(params.agent),
79
+ agentCommand: asOptionalString(params.agentCommand),
80
+ cwd: String(params.cwd),
81
+ name: String(params.name)
82
+ });
83
+ case "prompt":
84
+ return await this.runtime.prompt({
85
+ agent: String(params.agent),
86
+ agentCommand: asOptionalString(params.agentCommand),
87
+ cwd: String(params.cwd),
88
+ name: String(params.name),
89
+ text: String(params.text)
90
+ });
91
+ case "cancel":
92
+ return await this.runtime.cancel({
93
+ agent: String(params.agent),
94
+ agentCommand: asOptionalString(params.agentCommand),
95
+ cwd: String(params.cwd),
96
+ name: String(params.name)
97
+ });
98
+ default:
99
+ throw new Error(`unsupported bridge method: ${method}`);
100
+ }
101
+ }
102
+ }
103
+ function asOptionalString(value) {
104
+ if (typeof value !== "string" || value.length === 0) {
105
+ return;
106
+ }
107
+ return value;
108
+ }
109
+
110
+ // src/bridge/bridge-runtime.ts
111
+ import { spawn } from "node:child_process";
112
+ import { fileURLToPath } from "node:url";
113
+
114
+ class BridgeRuntime {
115
+ command;
116
+ run;
117
+ runSessionCreate;
118
+ constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner) {
119
+ this.command = command;
120
+ this.run = run;
121
+ this.runSessionCreate = runSessionCreate;
122
+ }
123
+ async hasSession(input) {
124
+ const result = await this.run(this.command, this.buildSessionArgs(input, [
125
+ "sessions",
126
+ "show",
127
+ input.name
128
+ ]));
129
+ return { exists: result.code === 0 };
130
+ }
131
+ async ensureSession(input) {
132
+ const ensured = await this.run(this.command, this.buildSessionArgs(input, ["sessions", "ensure", "--name", input.name]));
133
+ if (ensured.code === 0) {
134
+ return {};
135
+ }
136
+ const existing = await this.run(this.command, this.buildSessionArgs(input, ["sessions", "show", input.name]));
137
+ if (existing.code === 0) {
138
+ return {};
139
+ }
140
+ const createdWithHelper = await this.runSessionCreate(this.command, this.buildSessionArgs(input, ["sessions", "new", "--name", input.name]), input.cwd);
141
+ if (createdWithHelper.code !== 0) {
142
+ throw new Error(createdWithHelper.stderr || createdWithHelper.stdout || ensured.stderr || ensured.stdout || "failed to create session");
143
+ }
144
+ return {};
145
+ }
146
+ async prompt(input) {
147
+ const result = await this.run(this.command, this.buildSessionArgs(input, [
148
+ "prompt",
149
+ "-s",
150
+ input.name,
151
+ input.text
152
+ ]));
153
+ if (result.code !== 0) {
154
+ throw new Error(result.stderr || result.stdout || "prompt failed");
155
+ }
156
+ return { text: result.stdout.trim() };
157
+ }
158
+ async cancel(input) {
159
+ const result = await this.run(this.command, this.buildSessionArgs(input, [
160
+ "cancel",
161
+ "-s",
162
+ input.name
163
+ ]));
164
+ if (result.code !== 0) {
165
+ throw new Error(result.stderr || result.stdout || "cancel failed");
166
+ }
167
+ return {
168
+ cancelled: true,
169
+ message: result.stdout.trim()
170
+ };
171
+ }
172
+ async shutdown() {
173
+ return {};
174
+ }
175
+ buildSessionArgs(input, tail) {
176
+ if (input.agentCommand) {
177
+ return ["--format", "quiet", "--cwd", input.cwd, "--agent", input.agentCommand, ...tail];
178
+ }
179
+ return ["--format", "quiet", "--cwd", input.cwd, input.agent, ...tail];
180
+ }
181
+ }
182
+ async function defaultRunner(command, args) {
183
+ return await new Promise((resolve, reject) => {
184
+ const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
185
+ let stdout = "";
186
+ let stderr = "";
187
+ child.stdout.on("data", (chunk) => {
188
+ stdout += String(chunk);
189
+ });
190
+ child.stderr.on("data", (chunk) => {
191
+ stderr += String(chunk);
192
+ });
193
+ child.on("error", reject);
194
+ child.on("close", (code) => {
195
+ resolve({ code: code ?? 1, stdout, stderr });
196
+ });
197
+ });
198
+ }
199
+ async function shellSessionCreateRunner(command, args, cwd) {
200
+ const helperPath = fileURLToPath(new URL("../../scripts/acpx-session-new-helper.sh", import.meta.url));
201
+ return await new Promise((resolve, reject) => {
202
+ const child = spawn("/bin/zsh", [helperPath, command, cwd, ...args], {
203
+ stdio: ["ignore", "pipe", "pipe"]
204
+ });
205
+ let stdout = "";
206
+ let stderr = "";
207
+ child.stdout.on("data", (chunk) => {
208
+ stdout += String(chunk);
209
+ });
210
+ child.stderr.on("data", (chunk) => {
211
+ stderr += String(chunk);
212
+ });
213
+ child.on("error", reject);
214
+ child.on("close", (code) => {
215
+ resolve({ code: code ?? 1, stdout, stderr });
216
+ });
217
+ });
218
+ }
219
+
220
+ // src/bridge/bridge-main.ts
221
+ var server = new BridgeServer(new BridgeRuntime(process.env.WEACPX_BRIDGE_ACPX_COMMAND ?? "acpx"));
222
+ var input = createInterface({
223
+ input: process.stdin,
224
+ crlfDelay: Infinity
225
+ });
226
+ for await (const line of input) {
227
+ const response = await server.handleLine(line);
228
+ process.stdout.write(response);
229
+ }