weacpx 0.1.5 → 0.1.7

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 CHANGED
@@ -1,14 +1,34 @@
1
- # weacpx
1
+ # Weacpx
2
2
 
3
- 使用微信 ClawBot 随时随地通过 `acpx` 控制 Claude CodeCodex Agents。
3
+ 连接微信与 acpx 协议,让 Claude Code / Codex 成为你口袋里的 24/7 伙伴。
4
+
5
+ [![npm](https://img.shields.io/npm/v/weacpx?style=flat-square)](https://www.npmjs.com/package/weacpx)
6
+ [![Node.js Version](https://img.shields.io/node/v/weacpx?style=flat-square)](https://nodejs.org)
7
+ [![License](https://img.shields.io/npm/l/weacpx?style=flat-square)](./LICENSE)
8
+
9
+ ![weacpx logo](assets/weacpx.jpg)
10
+
11
+ ## Why Weacpx?
12
+
13
+ 在 Agent-First 的开发模式下,编码任务必须依托顶级 Agents,🙅‍♀️不要通过 openclaw 去开发,现在有一个更好的方案。Weacpx 通过微信提供一个轻量化的远程入口,随时随地通过手机驱动你的顶级 Agents。
14
+
15
+ Weacpx 的核心价值主张很简单:
16
+
17
+ **随时随地访问** — 只要你有微信,就能控制你的 Agent。无需 VPN、Web 界面或复杂的云服务配置。
18
+
19
+ **统一的会话管理** — 通过 acpx 协议,weacpx 让你在微信里管理多个 Agent 会话(Codex、Claude Code 等),就像在本地终端一样。创建、切换、查询状态,全部通过简单的斜杠命令完成,这是其它简单基于 ACP 实现的远控 agent 所不具备的。
20
+
21
+ **轻量守护进程** — weacpx 作为后台守护进程运行,资源占用极低。不用启动一个臃肿 openclaw,不用担心在工作机器上使用会占用资源。启动、停止、查看状态都通过简单的 CLI 命令完成。
22
+
23
+ **权限可控** — 可以即时通过微信修改 agent 的权限,无论是 YOLO 还是只读。
4
24
 
5
25
  ## 安装前准备
6
26
 
7
27
  开始前,至少需要:
8
28
 
9
29
  - Node.js 22+ 或 Bun
10
- - 一个可用的微信登录环境
11
30
  - Claude Code 或 Codex
31
+ - 装了微信的手机
12
32
 
13
33
  > `weacpx` 基于 `weixin-agent-sdk` 与 `acpx` 实现。
14
34
  > 正常情况下,不需要再额外全局安装 `acpx`。
@@ -155,6 +175,8 @@ bun run dev
155
175
  | `/ss attach <alias> -a <name> --ws <name> --name <transport-session>` | 恢复已存在的会话 |
156
176
  | `/use <alias>` | 切换当前会话 |
157
177
  | `/status` | 查看当前会话状态 |
178
+ | `/mode` | 查看当前会话已保存的 mode |
179
+ | `/mode <id>` | 设置当前会话 mode,例如 `/mode plan` |
158
180
  | `/session reset` | 重置当前会话上下文,保留 alias/agent/workspace,但重新绑定到一个新的后端 session |
159
181
  | `/clear` | `/session reset` 的快捷别名 |
160
182
  | `/cancel` | 取消当前会话 |
@@ -165,6 +187,8 @@ bun run dev
165
187
  - `/ss <agent> -d <path>` 是最常用入口,会自动按目录名推导并创建或复用 workspace,再创建或复用 session
166
188
  - `/ss new <agent> -d <path>` 表示强制新建 session
167
189
  - `/use <alias>` 用来切换当前会话
190
+ - `/mode` 会显示当前逻辑会话里保存的 mode;如果还没设置过,会显示“未设置”
191
+ - `/mode <id>` 会把 mode 透传给底层 `acpx set-mode`,成功后再写回当前逻辑会话
168
192
  - `/session reset` 和 `/clear` 会保留当前逻辑会话名,但重新创建一个新的后端 session,从空上下文重新开始
169
193
  - 非 `/` 开头的文本会发送到当前 session
170
194
 
@@ -248,12 +272,6 @@ bun run dev
248
272
  - `~/.weacpx/runtime/stdout.log`
249
273
  - `~/.weacpx/runtime/stderr.log`
250
274
 
251
- 常用环境变量:
252
-
253
- - `WEACPX_CONFIG`
254
- - `WEACPX_STATE`
255
- - `WEACPX_WEIXIN_SDK`
256
-
257
275
  ### Transport 权限配置
258
276
 
259
277
  `config.json` 中的 `transport` 支持以下权限字段:
@@ -300,19 +318,25 @@ bun run dev
300
318
 
301
319
  ## 注意事项
302
320
 
303
- ### `dry-run`
321
+ ### Adapter mode 参考
304
322
 
305
- `dry-run` 会复用同一套 router、session service、transport,只是把微信消息换成终端输入,适合本地排查。
323
+ `acpx set-mode` / 计划中的 `/mode <id>` 本质上都是给底层 ACP session 发送 `session/set_mode`。
324
+ 这里的 `<id>` 不是 `weacpx` 或 `acpx` 统一规定的枚举,而是**各 adapter 自己定义**的值;填错时通常会收到 adapter 返回的 `Invalid params` 一类错误。
306
325
 
307
- 示例:
326
+ 基于 `acpx` 内置 adapter 文档和各上游公开文档,当前能确认的信息如下:
308
327
 
309
- ```bash
310
- bun run dry-run --chat-key wx:test -- \
311
- "/agent add codex" \
312
- "/ws new backend -d /absolute/path/to/backend" \
313
- "/ss new demo -a codex --ws backend" \
314
- "/status"
315
- ```
328
+ | adapter | 已确认可用的 mode id | 说明 |
329
+ |------|------|------|
330
+ | `codex` | `plan` | `acpx` 自身示例明确使用过 `acpx codex set-mode plan`。`codex-acp` 还暴露了 `mode` 运行时配置项,但上游目前没有公开一份完整、稳定的 mode id 列表。 |
331
+ | `cursor` | `agent`、`plan`、`ask` | Cursor 官方文档/更新日志公开提到 `Plan mode`、`Ask mode`;Cursor 官方论坛在 ACP `session/configure` 示例中展示过 `availableModes` `agent` / `plan` / `ask`。 |
332
+ | 其他内置 adapter | 暂无公开、稳定的 mode id 列表 | 包括 `claude`、`copilot`、`gemini`、`qoder`、`qwen`、`kimi`、`kiro`、`iflow`、`opencode`、`trae`、`droid`、`kilocode` 等。即使某些产品本身有“Ask / Agent / Plan”之类概念,其 ACP `set-mode` 可接受的精确字符串也往往没有在官方文档中写死。 |
333
+
334
+ 建议:
335
+
336
+ - 对 `codex`,优先把 `plan` 当作已知可用值。
337
+ - 对 `cursor`,优先使用 `agent`、`plan`、`ask`。
338
+ - 对其他 adapter,不要在 `weacpx` 里写死候选值;最好把 `/mode <id>` 设计成透传,由 adapter 自己决定是否接受。
339
+ - 如果某个 adapter 后续补充了官方 mode 文档,再把它们补进这里。
316
340
 
317
341
  ### 如果 `/ss new` 失败
318
342
 
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "transport": {
3
3
  "type": "acpx-bridge",
4
- "sessionInitTimeoutMs": 120000
4
+ "sessionInitTimeoutMs": 120000,
5
+ "permissionMode": "approve-all",
6
+ "nonInteractivePermissions": "fail"
5
7
  },
6
8
  "logging": {
7
9
  "level": "info",
@@ -4,25 +4,43 @@ var __getProtoOf = Object.getPrototypeOf;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
7
12
  var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
8
20
  target = mod != null ? __create(__getProtoOf(mod)) : {};
9
21
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
22
  for (let key of __getOwnPropNames(mod))
11
23
  if (!__hasOwnProp.call(to, key))
12
24
  __defProp(to, key, {
13
- get: () => mod[key],
25
+ get: __accessProp.bind(mod, key),
14
26
  enumerable: true
15
27
  });
28
+ if (canCache)
29
+ cache.set(mod, to);
16
30
  return to;
17
31
  };
18
32
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __returnValue = (v) => v;
34
+ function __exportSetter(name, newValue) {
35
+ this[name] = __returnValue.bind(null, newValue);
36
+ }
19
37
  var __export = (target, all) => {
20
38
  for (var name in all)
21
39
  __defProp(target, name, {
22
40
  get: all[name],
23
41
  enumerable: true,
24
42
  configurable: true,
25
- set: (newValue) => all[name] = () => newValue
43
+ set: __exportSetter.bind(all, name)
26
44
  });
27
45
  };
28
46
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -181,14 +199,28 @@ import { createInterface } from "node:readline";
181
199
  // src/bridge/bridge-server.ts
182
200
  init_prompt_output();
183
201
 
202
+ class BridgeInvalidRequestError extends Error {
203
+ }
204
+ var BRIDGE_METHODS = new Set([
205
+ "ping",
206
+ "shutdown",
207
+ "hasSession",
208
+ "ensureSession",
209
+ "prompt",
210
+ "setMode",
211
+ "cancel"
212
+ ]);
213
+
184
214
  class BridgeServer {
185
215
  runtime;
186
216
  constructor(runtime) {
187
217
  this.runtime = runtime;
188
218
  }
189
219
  async handleLine(line) {
190
- const request = JSON.parse(line);
220
+ let requestId = extractRequestId(line);
191
221
  try {
222
+ const request = parseBridgeRequest(line);
223
+ requestId = request.id;
192
224
  const result = await this.dispatch(request.method, request.params);
193
225
  return `${JSON.stringify({
194
226
  id: request.id,
@@ -199,10 +231,10 @@ class BridgeServer {
199
231
  } catch (error) {
200
232
  const message = error instanceof Error ? error.message : String(error);
201
233
  return `${JSON.stringify({
202
- id: request.id,
234
+ id: requestId,
203
235
  ok: false,
204
236
  error: {
205
- code: "BRIDGE_INTERNAL_ERROR",
237
+ code: error instanceof BridgeInvalidRequestError ? "BRIDGE_INVALID_REQUEST" : "BRIDGE_INTERNAL_ERROR",
206
238
  message,
207
239
  ...error instanceof PromptCommandError ? {
208
240
  details: {
@@ -224,38 +256,97 @@ class BridgeServer {
224
256
  return await this.runtime.shutdown();
225
257
  case "hasSession":
226
258
  return await this.runtime.hasSession({
227
- agent: String(params.agent),
259
+ agent: requireString(params, "agent"),
228
260
  agentCommand: asOptionalString(params.agentCommand),
229
- cwd: String(params.cwd),
230
- name: String(params.name)
261
+ cwd: requireString(params, "cwd"),
262
+ name: requireString(params, "name")
231
263
  });
232
264
  case "ensureSession":
233
265
  return await this.runtime.ensureSession({
234
- agent: String(params.agent),
266
+ agent: requireString(params, "agent"),
235
267
  agentCommand: asOptionalString(params.agentCommand),
236
- cwd: String(params.cwd),
237
- name: String(params.name)
268
+ cwd: requireString(params, "cwd"),
269
+ name: requireString(params, "name")
238
270
  });
239
271
  case "prompt":
240
272
  return await this.runtime.prompt({
241
- agent: String(params.agent),
273
+ agent: requireString(params, "agent"),
274
+ agentCommand: asOptionalString(params.agentCommand),
275
+ cwd: requireString(params, "cwd"),
276
+ name: requireString(params, "name"),
277
+ text: requireString(params, "text")
278
+ });
279
+ case "setMode":
280
+ return await this.runtime.setMode({
281
+ agent: requireString(params, "agent"),
242
282
  agentCommand: asOptionalString(params.agentCommand),
243
- cwd: String(params.cwd),
244
- name: String(params.name),
245
- text: String(params.text)
283
+ cwd: requireString(params, "cwd"),
284
+ name: requireString(params, "name"),
285
+ modeId: requireString(params, "modeId")
246
286
  });
247
287
  case "cancel":
248
288
  return await this.runtime.cancel({
249
- agent: String(params.agent),
289
+ agent: requireString(params, "agent"),
250
290
  agentCommand: asOptionalString(params.agentCommand),
251
- cwd: String(params.cwd),
252
- name: String(params.name)
291
+ cwd: requireString(params, "cwd"),
292
+ name: requireString(params, "name")
253
293
  });
254
294
  default:
255
295
  throw new Error(`unsupported bridge method: ${method}`);
256
296
  }
257
297
  }
258
298
  }
299
+ function extractRequestId(line) {
300
+ try {
301
+ const raw = JSON.parse(line);
302
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
303
+ return "unknown";
304
+ }
305
+ const id = raw.id;
306
+ return typeof id === "string" && id.length > 0 ? id : "unknown";
307
+ } catch {
308
+ return "unknown";
309
+ }
310
+ }
311
+ function parseBridgeRequest(line) {
312
+ let raw;
313
+ try {
314
+ raw = JSON.parse(line);
315
+ } catch {
316
+ throw new BridgeInvalidRequestError("request must be valid JSON");
317
+ }
318
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
319
+ throw new BridgeInvalidRequestError("request must be a JSON object");
320
+ }
321
+ const request = raw;
322
+ const id = request.id;
323
+ const method = request.method;
324
+ const params = request.params;
325
+ if (typeof id !== "string" || id.length === 0) {
326
+ throw new BridgeInvalidRequestError("id must be a non-empty string");
327
+ }
328
+ if (typeof method !== "string" || method.length === 0) {
329
+ throw new BridgeInvalidRequestError("method must be a non-empty string");
330
+ }
331
+ if (!BRIDGE_METHODS.has(method)) {
332
+ throw new BridgeInvalidRequestError(`unsupported bridge method: ${method}`);
333
+ }
334
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
335
+ throw new BridgeInvalidRequestError("params must be an object");
336
+ }
337
+ return {
338
+ id,
339
+ method,
340
+ params
341
+ };
342
+ }
343
+ function requireString(params, key) {
344
+ const value = params[key];
345
+ if (typeof value !== "string" || value.length === 0) {
346
+ throw new BridgeInvalidRequestError(`${key} must be a non-empty string`);
347
+ }
348
+ return value;
349
+ }
259
350
  function asOptionalString(value) {
260
351
  if (typeof value !== "string" || value.length === 0) {
261
352
  return;
@@ -322,6 +413,19 @@ class BridgeRuntime {
322
413
  const result = await this.run(spawnSpec.command, spawnSpec.args);
323
414
  return { text: getPromptText(result) };
324
415
  }
416
+ async setMode(input) {
417
+ const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
418
+ "set-mode",
419
+ "-s",
420
+ input.name,
421
+ input.modeId
422
+ ]));
423
+ const result = await this.run(spawnSpec.command, spawnSpec.args);
424
+ if (result.code !== 0) {
425
+ throw new Error(result.stderr || result.stdout || "set-mode failed");
426
+ }
427
+ return {};
428
+ }
325
429
  async cancel(input) {
326
430
  const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
327
431
  "cancel",