weacpx 0.1.1 → 0.1.3

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,31 +1,17 @@
1
1
  # weacpx
2
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
- - 你希望在手机上完成常见的会话切换、目录切换和对话操作
3
+ 使用微信 ClawBot 随时随地通过 `acpx` 控制 Claude Code、Codex 等 Agents。
18
4
 
19
5
  ## 安装前准备
20
6
 
21
7
  开始前,至少需要:
22
8
 
23
- - Node.js 22+
24
- - Bun
9
+ - Node.js 22+ 或 Bun
25
10
  - 一个可用的微信登录环境
26
- - 本机可以运行 `acpx` 及其目标 agent
11
+ - Claude Code Codex
27
12
 
28
- 正常情况下,不需要再额外全局安装 `acpx`。
13
+ > `weacpx` 基于 `weixin-agent-sdk` 与 `acpx` 实现。
14
+ > 正常情况下,不需要再额外全局安装 `acpx`。
29
15
 
30
16
  ## 安装
31
17
 
@@ -34,17 +20,10 @@
34
20
  ```bash
35
21
  # 使用 NPM 全局安装
36
22
  npm install -g weacpx
37
- # 或使用 bun 全局安装
23
+ # 或使用 Bun 全局安装
38
24
  bun add -g weacpx
39
25
  ```
40
26
 
41
- 如果你是从源码仓库直接使用,请先安装依赖并构建:
42
-
43
- ```bash
44
- bun install
45
- bun run dev
46
- ```
47
-
48
27
  ## 快速开始
49
28
 
50
29
  第一次使用建议按这个顺序:
@@ -58,19 +37,9 @@ bun run dev
58
37
  ```bash
59
38
  weacpx login
60
39
  weacpx start
61
- weacpx status
62
40
  ```
63
41
 
64
- 如果你是在仓库里本地运行:
65
-
66
- ```bash
67
- bun run login
68
- bun run dev
69
- ```
70
-
71
- `weacpx login` 和 `bun run login` 都会在终端里显示二维码。
72
-
73
- 启动后,在微信里先发:
42
+ `weacpx login` 会在终端里显示二维码,使用微信扫描登录。`weacpx start` 启动后,在微信里发:
74
43
 
75
44
  ```text
76
45
  /ss codex -d /absolute/path/to/your/repo
@@ -78,8 +47,9 @@ bun run dev
78
47
  /help
79
48
  ```
80
49
 
81
- 第一行的意思是:开启或挂在一个会话,并切换到该会话。使用 Codex,并指定工作目录为 `/absolute/path/to/your/repo`。
82
- 第二行的意思是:查看帮助信息。
50
+ `/ss codex -d /absolute/path/to/your/repo`:开启或挂在一个会话,并切换到该会话。使用 Codex,并指定工作目录为 `/absolute/path/to/your/repo`。
51
+
52
+ `/help` 查看帮助信息。
83
53
 
84
54
  然后就可以直接发普通消息,例如:
85
55
 
@@ -87,6 +57,17 @@ bun run dev
87
57
  hello
88
58
  ```
89
59
 
60
+ 如果你是从源码仓库直接使用:
61
+
62
+ ```bash
63
+ # 先安装依赖
64
+ bun install
65
+ # 登录微信
66
+ bun run login
67
+ # 启动服务
68
+ bun run dev
69
+ ```
70
+
90
71
  普通文本会默认发送到当前选中的 session。
91
72
 
92
73
  ## CLI 命令
@@ -106,27 +87,55 @@ hello
106
87
  - `status` 查看后台状态、PID、配置路径和日志路径
107
88
  - `stop` 停止后台实例
108
89
 
109
- ## 微信中如何使用
90
+ ## 微信中使用说明
110
91
 
111
- ### Agent
92
+ ### 管理 Agent
112
93
 
113
- 内置 `codex` 与 `claude` 两个常见模板,也支持添加你自己的 agent。
94
+ 内置 `codex` 与 `claude` 两个常见 agent,也支持添加你自己的 agents
114
95
 
115
- - `/agents` 查看当前已添加的 agent
116
- - `/agent add codex` 添加 codex agent
117
- - `/agent add claude` 添加 claude agent
118
- - `/agent rm <name>` 删除 agent
96
+ | 命令 | 说明 |
97
+ |------|------|
98
+ | `/agents` | 查看当前已添加的 agent |
99
+ | `/agent add codex` | 添加 codex agent |
100
+ | `/agent add claude` | 添加 claude agent |
101
+ | `/agent rm <name>` | 删除 agent |
119
102
 
120
103
  说明:
121
104
 
122
105
  - 内置 `codex` 和 `claude` 走 `acpx` 的 driver alias,通常不需要额外写 `agent.command`
123
106
  - 如果你接入的是自定义 agent,再考虑显式配置 `agent.command`
124
107
 
108
+ `config.json` 中的 `agent.command` 用于显式指定 agent 的原始启动命令,完整字段如下:
109
+
110
+ | 字段 | 类型 | 必填 | 说明 |
111
+ |------|------|------|------|
112
+ | `driver` | `string` | 是 | agent 驱动类型,传递给 acpx 的第一位置参数 |
113
+ | `command` | `string` | 否 | 显式指定自定义 agent 的原始命令。不填则使用 acpx 默认行为 |
114
+
115
+ 示例 — 配置一个自定义 agent:
116
+
117
+ ```json
118
+ {
119
+ "agents": {
120
+ "my-agent": {
121
+ "driver": "codex",
122
+ "command": "/path/to/acpx codex --arg1 value1"
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ - 内置 `codex` 和 `claude` 建议只写 `driver`,让 `acpx` 自己解析对应 alias
129
+ - `command` 主要用于自定义 agent,不建议给内置 driver 手写原始命令
130
+ - 旧版 `codex` raw command 配置会被自动忽略,回退为 `acpx codex ...`
131
+
125
132
  ### Workspace 工作目录
126
133
 
127
- - `/workspaces` `/workspace` 或 `/ws` 可查看当前已添加的工作目录
128
- - `/ws new <name> -d <path>` 添加工作目录,`-d` 后面接的是目录在电脑的绝对路径
129
- - `/workspace rm <name>` 删除工作目录
134
+ | 命令 | 说明 |
135
+ |------|------|
136
+ | `/workspaces` / `/workspace` / `/ws` | 查看当前已添加的工作目录 |
137
+ | `/ws new <name> -d <path>` | 添加工作目录,`-d` 后面接的是目录在电脑的绝对路径 |
138
+ | `/workspace rm <name>` | 删除工作目录 |
130
139
 
131
140
  说明:
132
141
 
@@ -135,15 +144,17 @@ hello
135
144
 
136
145
  ### Session 会话
137
146
 
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
+ | `/sessions` / `/session` / `/ss` | 查看当前已添加的会话 |
150
+ | `/ss <agent> -d <path>` | 新建会话(自动按目录名推导并创建或复用 workspace,再创建或复用 session) |
151
+ | `/ss new <agent> -d <path>` | 强制新建会话 |
152
+ | `/ss new <alias> -a <name> --ws <name>` | 强制新建会话,并指定 agent 和 workspace |
153
+ | `/ss attach <alias> -a <name> --ws <name> --name <transport-session>` | 恢复已存在的会话 |
154
+ | `/use <alias>` | 切换当前会话 |
155
+ | `/status` | 查看当前会话状态 |
156
+ | `/cancel` | 取消当前会话 |
157
+ | `/stop` | 停止当前会话 |
147
158
 
148
159
  说明:
149
160
 
@@ -28,6 +28,138 @@ var __export = (target, all) => {
28
28
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
29
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
30
 
31
+ // src/process/spawn-command.ts
32
+ function resolveSpawnCommand(command, args) {
33
+ if (SCRIPT_FILE_PATTERN.test(command)) {
34
+ return {
35
+ command: process.execPath,
36
+ args: [command, ...args]
37
+ };
38
+ }
39
+ return { command, args };
40
+ }
41
+ var SCRIPT_FILE_PATTERN;
42
+ var init_spawn_command = __esm(() => {
43
+ SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
44
+ });
45
+
46
+ // src/transport/prompt-output.ts
47
+ function getPromptText(result) {
48
+ const stdoutOutput = extractPromptOutput(result.stdout);
49
+ if (result.code === 0) {
50
+ return sanitizePromptText(stdoutOutput.text);
51
+ }
52
+ const preferredError = extractPromptFailureMessage(result);
53
+ if (preferredError) {
54
+ throw new Error(preferredError);
55
+ }
56
+ const stderrOutput = extractPromptOutput(result.stderr);
57
+ const partialReply = [stdoutOutput, stderrOutput].filter((output) => output.hasAgentMessage && output.text.length > 0).map((output) => sanitizePromptText(output.text)).find((text) => text.length > 0);
58
+ if (partialReply) {
59
+ return partialReply;
60
+ }
61
+ throw new Error(`command failed with exit code ${result.code}`);
62
+ }
63
+ function normalizeCommandError(result) {
64
+ const preferredError = extractPromptFailureMessage(result);
65
+ if (preferredError) {
66
+ return preferredError;
67
+ }
68
+ return result.stdout.trim() || null;
69
+ }
70
+ function extractPromptFailureMessage(result) {
71
+ const rpcMessages = extractJsonRpcErrorMessages(result.stderr).concat(extractJsonRpcErrorMessages(result.stdout)).filter((message) => message.length > 0);
72
+ const preferredMessage = [...rpcMessages].reverse().find((message) => message !== "Resource not found");
73
+ if (preferredMessage) {
74
+ return preferredMessage;
75
+ }
76
+ if (rpcMessages.length > 0) {
77
+ return rpcMessages[rpcMessages.length - 1] ?? null;
78
+ }
79
+ const stderrText = result.stderr.trim();
80
+ if (stderrText.length > 0) {
81
+ return stderrText;
82
+ }
83
+ return null;
84
+ }
85
+ function extractPromptOutput(output) {
86
+ const lines = output.split(`
87
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
88
+ const messageSegments = [];
89
+ let currentSegment = "";
90
+ let hasAgentMessage = false;
91
+ for (const line of lines) {
92
+ try {
93
+ const event = JSON.parse(line);
94
+ const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
95
+ if (isMessageChunk) {
96
+ hasAgentMessage = true;
97
+ const chunk = event.params.update.content.text ?? "";
98
+ if (chunk.length > 0) {
99
+ currentSegment += chunk;
100
+ }
101
+ continue;
102
+ }
103
+ if (currentSegment.trim().length > 0) {
104
+ messageSegments.push(currentSegment.trim());
105
+ }
106
+ currentSegment = "";
107
+ } catch {
108
+ if (currentSegment.trim().length > 0) {
109
+ messageSegments.push(currentSegment.trim());
110
+ currentSegment = "";
111
+ }
112
+ }
113
+ }
114
+ if (currentSegment.trim().length > 0) {
115
+ messageSegments.push(currentSegment.trim());
116
+ }
117
+ if (messageSegments.length > 0) {
118
+ return {
119
+ text: messageSegments[messageSegments.length - 1],
120
+ hasAgentMessage
121
+ };
122
+ }
123
+ return {
124
+ text: output.trim(),
125
+ hasAgentMessage
126
+ };
127
+ }
128
+ function sanitizePromptText(text) {
129
+ const trimmed = text.trim();
130
+ const paragraphs = trimmed.split(/\n\s*\n/);
131
+ if (paragraphs.length < 2) {
132
+ return trimmed;
133
+ }
134
+ const firstParagraph = paragraphs[0].trim().replace(/\s+/g, " ").toLowerCase();
135
+ if (!looksLikeWorkflowPreamble(firstParagraph)) {
136
+ return trimmed;
137
+ }
138
+ return paragraphs.slice(1).join(`
139
+
140
+ `).trim();
141
+ }
142
+ function looksLikeWorkflowPreamble(paragraph) {
143
+ if (!paragraph.startsWith("using ")) {
144
+ return false;
145
+ }
146
+ return paragraph.includes("using-superpowers") || paragraph.includes("repo workflow requirement") || paragraph.includes("workflow requirement") || paragraph.includes("before responding") || paragraph.includes("skill check");
147
+ }
148
+ function extractJsonRpcErrorMessages(output) {
149
+ return output.split(`
150
+ `).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
151
+ try {
152
+ const payload = JSON.parse(line);
153
+ if (typeof payload.error?.message === "string" && payload.error.message.length > 0) {
154
+ return [payload.error.message];
155
+ }
156
+ } catch {
157
+ return [];
158
+ }
159
+ return [];
160
+ });
161
+ }
162
+
31
163
  // src/bridge/bridge-main.ts
32
164
  import { createInterface } from "node:readline";
33
165
 
@@ -108,6 +240,7 @@ function asOptionalString(value) {
108
240
  }
109
241
 
110
242
  // src/bridge/bridge-runtime.ts
243
+ init_spawn_command();
111
244
  import { spawn } from "node:child_process";
112
245
  import { fileURLToPath } from "node:url";
113
246
 
@@ -121,46 +254,54 @@ class BridgeRuntime {
121
254
  this.runSessionCreate = runSessionCreate;
122
255
  }
123
256
  async hasSession(input) {
124
- const result = await this.run(this.command, this.buildSessionArgs(input, [
257
+ const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
125
258
  "sessions",
126
259
  "show",
127
260
  input.name
128
261
  ]));
262
+ const result = await this.run(spawnSpec.command, spawnSpec.args);
129
263
  return { exists: result.code === 0 };
130
264
  }
131
265
  async ensureSession(input) {
132
- const ensured = await this.run(this.command, this.buildSessionArgs(input, ["sessions", "ensure", "--name", input.name]));
266
+ const ensuredSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
267
+ "sessions",
268
+ "ensure",
269
+ "--name",
270
+ input.name
271
+ ]));
272
+ const ensured = await this.run(ensuredSpec.command, ensuredSpec.args);
133
273
  if (ensured.code === 0) {
134
274
  return {};
135
275
  }
136
- const existing = await this.run(this.command, this.buildSessionArgs(input, ["sessions", "show", input.name]));
276
+ const existingSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "show", input.name]));
277
+ const existing = await this.run(existingSpec.command, existingSpec.args);
137
278
  if (existing.code === 0) {
138
279
  return {};
139
280
  }
140
- const createdWithHelper = await this.runSessionCreate(this.command, this.buildSessionArgs(input, ["sessions", "new", "--name", input.name]), input.cwd);
281
+ const createSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "new", "--name", input.name]));
282
+ const createdWithHelper = await this.runSessionCreate(createSpec.command, createSpec.args, input.cwd);
141
283
  if (createdWithHelper.code !== 0) {
142
284
  throw new Error(createdWithHelper.stderr || createdWithHelper.stdout || ensured.stderr || ensured.stdout || "failed to create session");
143
285
  }
144
286
  return {};
145
287
  }
146
288
  async prompt(input) {
147
- const result = await this.run(this.command, this.buildSessionArgs(input, [
289
+ const spawnSpec = resolveSpawnCommand(this.command, this.buildPromptArgs(input, [
148
290
  "prompt",
149
291
  "-s",
150
292
  input.name,
151
293
  input.text
152
294
  ]));
153
- if (result.code !== 0) {
154
- throw new Error(result.stderr || result.stdout || "prompt failed");
155
- }
156
- return { text: result.stdout.trim() };
295
+ const result = await this.run(spawnSpec.command, spawnSpec.args);
296
+ return { text: getPromptText(result) };
157
297
  }
158
298
  async cancel(input) {
159
- const result = await this.run(this.command, this.buildSessionArgs(input, [
299
+ const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
160
300
  "cancel",
161
301
  "-s",
162
302
  input.name
163
303
  ]));
304
+ const result = await this.run(spawnSpec.command, spawnSpec.args);
164
305
  if (result.code !== 0) {
165
306
  throw new Error(result.stderr || result.stdout || "cancel failed");
166
307
  }
@@ -178,6 +319,12 @@ class BridgeRuntime {
178
319
  }
179
320
  return ["--format", "quiet", "--cwd", input.cwd, input.agent, ...tail];
180
321
  }
322
+ buildPromptArgs(input, tail) {
323
+ if (input.agentCommand) {
324
+ return ["--format", "json", "--json-strict", "--cwd", input.cwd, "--agent", input.agentCommand, ...tail];
325
+ }
326
+ return ["--format", "json", "--json-strict", "--cwd", input.cwd, input.agent, ...tail];
327
+ }
181
328
  }
182
329
  async function defaultRunner(command, args) {
183
330
  return await new Promise((resolve, reject) => {
package/dist/cli.js CHANGED
@@ -1440,6 +1440,9 @@ function parseCommand(input) {
1440
1440
  }
1441
1441
  return { kind: "session.attach", alias, agent, workspace, transportSession };
1442
1442
  }
1443
+ if (command.startsWith("/") && isRecognizedCommand(command)) {
1444
+ return { kind: "invalid", text: trimmed, recognizedCommand: command };
1445
+ }
1443
1446
  return { kind: "prompt", text: trimmed };
1444
1447
  }
1445
1448
  function hasAnyFlag(parts, flags) {
@@ -1462,6 +1465,9 @@ function normalizeCommand(command) {
1462
1465
  return "/cancel";
1463
1466
  return command;
1464
1467
  }
1468
+ function isRecognizedCommand(command) {
1469
+ return RECOGNIZED_COMMANDS.has(command);
1470
+ }
1465
1471
  function tokenizeCommand(input) {
1466
1472
  const tokens = [];
1467
1473
  let current = "";
@@ -1493,10 +1499,26 @@ function tokenizeCommand(input) {
1493
1499
  }
1494
1500
  return tokens;
1495
1501
  }
1502
+ var RECOGNIZED_COMMANDS;
1503
+ var init_parse_command = __esm(() => {
1504
+ RECOGNIZED_COMMANDS = new Set([
1505
+ "/help",
1506
+ "/agents",
1507
+ "/workspaces",
1508
+ "/sessions",
1509
+ "/status",
1510
+ "/cancel",
1511
+ "/session",
1512
+ "/workspace",
1513
+ "/use",
1514
+ "/agent"
1515
+ ]);
1516
+ });
1496
1517
 
1497
1518
  // src/commands/command-router.ts
1498
1519
  import { access } from "node:fs/promises";
1499
1520
  import { basename as basename2, normalize } from "node:path";
1521
+ import { homedir } from "node:os";
1500
1522
 
1501
1523
  class CommandRouter {
1502
1524
  sessions;
@@ -1520,6 +1542,19 @@ class CommandRouter {
1520
1542
  });
1521
1543
  return await this.executeCommand(chatKey, command.kind, startedAt, async () => {
1522
1544
  switch (command.kind) {
1545
+ case "invalid":
1546
+ return {
1547
+ text: [
1548
+ "无法识别的命令格式。",
1549
+ "",
1550
+ "正确的会话创建格式:",
1551
+ "/session new <别名> --agent <Agent名> --ws <工作区名>",
1552
+ "",
1553
+ "例如:",
1554
+ "/session new demo --agent claude --ws weacpx"
1555
+ ].join(`
1556
+ `)
1557
+ };
1523
1558
  case "help":
1524
1559
  return { text: renderHelpText() };
1525
1560
  case "agents":
@@ -1553,10 +1588,11 @@ class CommandRouter {
1553
1588
  if (!this.config || !this.configStore) {
1554
1589
  return { text: "当前没有加载可写入的配置。" };
1555
1590
  }
1556
- if (!await pathExists(command.cwd)) {
1591
+ const wsCwd = normalizePathForWorkspace(command.cwd);
1592
+ if (!await pathExists(wsCwd)) {
1557
1593
  return { text: `工作区路径不存在:${command.cwd}` };
1558
1594
  }
1559
- const updated = await this.configStore.upsertWorkspace(command.name, command.cwd);
1595
+ const updated = await this.configStore.upsertWorkspace(command.name, wsCwd);
1560
1596
  this.replaceConfig(updated);
1561
1597
  return { text: `工作区「${command.name}」已保存` };
1562
1598
  }
@@ -1755,7 +1791,17 @@ class CommandRouter {
1755
1791
  `)
1756
1792
  };
1757
1793
  }
1758
- throw error;
1794
+ if (!isPartialPromptOutputError(message)) {
1795
+ throw error;
1796
+ }
1797
+ return {
1798
+ text: [
1799
+ `当前会话「${session.alias}」执行中断,未收到最终回复。`,
1800
+ "请直接重试;如果长时间无响应,可先发送 /cancel 后再重试。",
1801
+ `错误信息:${summarizeTransportError(message)}`
1802
+ ].join(`
1803
+ `)
1804
+ };
1759
1805
  }
1760
1806
  renderSessionCreationError(session, error) {
1761
1807
  const message = error instanceof Error ? error.message : String(error);
@@ -1891,7 +1937,8 @@ async function pathExists(path) {
1891
1937
  }
1892
1938
  }
1893
1939
  function normalizePathForWorkspace(path) {
1894
- return normalize(path);
1940
+ const expanded = path.startsWith("~") ? homedir() + path.slice(1) : path;
1941
+ return normalize(expanded);
1895
1942
  }
1896
1943
  function sameWorkspacePath(left, right) {
1897
1944
  const normalizedLeft = normalizePathForWorkspace(left);
@@ -1901,9 +1948,16 @@ function sameWorkspacePath(left, right) {
1901
1948
  }
1902
1949
  return normalizedLeft === normalizedRight;
1903
1950
  }
1951
+ function summarizeTransportError(message) {
1952
+ return message.replace(/\s+/g, " ").trim().slice(0, 200);
1953
+ }
1954
+ function isPartialPromptOutputError(message) {
1955
+ return message.includes("未收到最终回复");
1956
+ }
1904
1957
  var init_command_router = __esm(() => {
1905
1958
  init_agent_templates();
1906
1959
  init_app_logger();
1960
+ init_parse_command();
1907
1961
  });
1908
1962
 
1909
1963
  // src/config/resolve-agent-command.ts
@@ -2108,7 +2162,7 @@ var init_ensure_config = __esm(() => {
2108
2162
 
2109
2163
  // src/config/resolve-acpx-command.ts
2110
2164
  import { readFileSync } from "node:fs";
2111
- import { dirname as dirname6, resolve } from "node:path";
2165
+ import { posix, win32 } from "node:path";
2112
2166
  import { createRequire as createRequire2 } from "node:module";
2113
2167
  function resolveAcpxCommand(options = {}) {
2114
2168
  if (options.configuredCommand) {
@@ -2120,12 +2174,11 @@ function resolveAcpxCommand(options = {}) {
2120
2174
  try {
2121
2175
  const packageJsonPath = resolvePackageJson("acpx/package.json");
2122
2176
  const pkg = readPackageJson(packageJsonPath);
2177
+ const pathApi = platform === "win32" ? win32 : posix;
2178
+ const packageDir = pathApi.dirname(packageJsonPath);
2123
2179
  const binPath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && typeof pkg.bin.acpx === "string" ? pkg.bin.acpx : null;
2124
2180
  if (binPath) {
2125
- if (platform === "win32") {
2126
- return resolve(dirname6(packageJsonPath), "../.bin/acpx.exe");
2127
- }
2128
- return resolve(dirname6(packageJsonPath), binPath);
2181
+ return pathApi.resolve(packageDir, binPath);
2129
2182
  }
2130
2183
  } catch {}
2131
2184
  return "acpx";
@@ -2275,7 +2328,7 @@ function createEmptyState() {
2275
2328
 
2276
2329
  // src/state/state-store.ts
2277
2330
  import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
2278
- import { dirname as dirname7 } from "node:path";
2331
+ import { dirname as dirname6 } from "node:path";
2279
2332
 
2280
2333
  class StateStore {
2281
2334
  path;
@@ -2297,7 +2350,7 @@ class StateStore {
2297
2350
  }
2298
2351
  }
2299
2352
  async save(state) {
2300
- await mkdir7(dirname7(this.path), { recursive: true });
2353
+ await mkdir7(dirname6(this.path), { recursive: true });
2301
2354
  await writeFile5(this.path, JSON.stringify(state, null, 2));
2302
2355
  }
2303
2356
  }
@@ -2320,17 +2373,27 @@ async function runConsole(paths, deps) {
2320
2373
  configPath: paths.configPath,
2321
2374
  statePath: paths.statePath
2322
2375
  });
2323
- heartbeatTimer = setIntervalFn(() => deps.daemonRuntime?.heartbeat(), deps.heartbeatIntervalMs ?? 30000);
2376
+ heartbeatTimer = setIntervalFn(() => {
2377
+ deps.daemonRuntime?.heartbeat().catch(() => {});
2378
+ }, deps.heartbeatIntervalMs ?? 30000);
2324
2379
  }
2325
2380
  await sdk.start(runtime.agent);
2326
2381
  } finally {
2382
+ let disposeError = null;
2327
2383
  if (heartbeatTimer !== null) {
2328
2384
  clearIntervalFn(heartbeatTimer);
2329
2385
  }
2330
- await runtime.dispose();
2386
+ try {
2387
+ await runtime.dispose();
2388
+ } catch (error) {
2389
+ disposeError = error;
2390
+ }
2331
2391
  if (deps.daemonRuntime) {
2332
2392
  await deps.daemonRuntime.stop();
2333
2393
  }
2394
+ if (disposeError) {
2395
+ throw disposeError;
2396
+ }
2334
2397
  }
2335
2398
  }
2336
2399
 
@@ -2355,8 +2418,8 @@ class AcpxBridgeClient {
2355
2418
  request(method, params) {
2356
2419
  const id = String(this.nextId);
2357
2420
  this.nextId += 1;
2358
- return awaitable((resolve2, reject) => {
2359
- this.pending.set(id, { resolve: resolve2, reject });
2421
+ return awaitable((resolve, reject) => {
2422
+ this.pending.set(id, { resolve, reject });
2360
2423
  this.writeLine(encodeBridgeRequest({
2361
2424
  id,
2362
2425
  method,
@@ -2377,6 +2440,13 @@ class AcpxBridgeClient {
2377
2440
  }
2378
2441
  pending.reject(new Error(response.error.message));
2379
2442
  }
2443
+ handleExit(error) {
2444
+ const pendingRequests = [...this.pending.values()];
2445
+ this.pending.clear();
2446
+ for (const pending of pendingRequests) {
2447
+ pending.reject(error);
2448
+ }
2449
+ }
2380
2450
  }
2381
2451
  function buildBridgeSpawnSpec(options) {
2382
2452
  if (options.execPath.endsWith("bun")) {
@@ -2416,6 +2486,10 @@ async function spawnAcpxBridgeClient(options = {}) {
2416
2486
  });
2417
2487
  child.on("exit", () => {
2418
2488
  output.close();
2489
+ client.handleExit(new Error("bridge process exited before responding"));
2490
+ });
2491
+ child.on("error", (error) => {
2492
+ client.handleExit(error);
2419
2493
  });
2420
2494
  client.waitUntilReady = async () => {
2421
2495
  await client.request("ping", {});
@@ -2432,8 +2506,8 @@ async function spawnAcpxBridgeClient(options = {}) {
2432
2506
  return client;
2433
2507
  }
2434
2508
  function awaitable(executor) {
2435
- return new Promise((resolve2, reject) => {
2436
- executor(resolve2, reject);
2509
+ return new Promise((resolve, reject) => {
2510
+ executor(resolve, reject);
2437
2511
  });
2438
2512
  }
2439
2513
  var init_acpx_bridge_client = () => {};
@@ -2476,14 +2550,146 @@ class AcpxBridgeTransport {
2476
2550
  }
2477
2551
  }
2478
2552
 
2553
+ // src/process/spawn-command.ts
2554
+ function resolveSpawnCommand(command, args) {
2555
+ if (SCRIPT_FILE_PATTERN.test(command)) {
2556
+ return {
2557
+ command: process.execPath,
2558
+ args: [command, ...args]
2559
+ };
2560
+ }
2561
+ return { command, args };
2562
+ }
2563
+ var SCRIPT_FILE_PATTERN;
2564
+ var init_spawn_command = __esm(() => {
2565
+ SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
2566
+ });
2567
+
2568
+ // src/transport/prompt-output.ts
2569
+ function getPromptText(result) {
2570
+ const stdoutOutput = extractPromptOutput(result.stdout);
2571
+ if (result.code === 0) {
2572
+ return sanitizePromptText(stdoutOutput.text);
2573
+ }
2574
+ const preferredError = extractPromptFailureMessage(result);
2575
+ if (preferredError) {
2576
+ throw new Error(preferredError);
2577
+ }
2578
+ const stderrOutput = extractPromptOutput(result.stderr);
2579
+ const partialReply = [stdoutOutput, stderrOutput].filter((output) => output.hasAgentMessage && output.text.length > 0).map((output) => sanitizePromptText(output.text)).find((text) => text.length > 0);
2580
+ if (partialReply) {
2581
+ return partialReply;
2582
+ }
2583
+ throw new Error(`command failed with exit code ${result.code}`);
2584
+ }
2585
+ function normalizeCommandError(result) {
2586
+ const preferredError = extractPromptFailureMessage(result);
2587
+ if (preferredError) {
2588
+ return preferredError;
2589
+ }
2590
+ return result.stdout.trim() || null;
2591
+ }
2592
+ function extractPromptFailureMessage(result) {
2593
+ const rpcMessages = extractJsonRpcErrorMessages(result.stderr).concat(extractJsonRpcErrorMessages(result.stdout)).filter((message) => message.length > 0);
2594
+ const preferredMessage = [...rpcMessages].reverse().find((message) => message !== "Resource not found");
2595
+ if (preferredMessage) {
2596
+ return preferredMessage;
2597
+ }
2598
+ if (rpcMessages.length > 0) {
2599
+ return rpcMessages[rpcMessages.length - 1] ?? null;
2600
+ }
2601
+ const stderrText = result.stderr.trim();
2602
+ if (stderrText.length > 0) {
2603
+ return stderrText;
2604
+ }
2605
+ return null;
2606
+ }
2607
+ function extractPromptOutput(output) {
2608
+ const lines = output.split(`
2609
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
2610
+ const messageSegments = [];
2611
+ let currentSegment = "";
2612
+ let hasAgentMessage = false;
2613
+ for (const line of lines) {
2614
+ try {
2615
+ const event = JSON.parse(line);
2616
+ const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
2617
+ if (isMessageChunk) {
2618
+ hasAgentMessage = true;
2619
+ const chunk = event.params.update.content.text ?? "";
2620
+ if (chunk.length > 0) {
2621
+ currentSegment += chunk;
2622
+ }
2623
+ continue;
2624
+ }
2625
+ if (currentSegment.trim().length > 0) {
2626
+ messageSegments.push(currentSegment.trim());
2627
+ }
2628
+ currentSegment = "";
2629
+ } catch {
2630
+ if (currentSegment.trim().length > 0) {
2631
+ messageSegments.push(currentSegment.trim());
2632
+ currentSegment = "";
2633
+ }
2634
+ }
2635
+ }
2636
+ if (currentSegment.trim().length > 0) {
2637
+ messageSegments.push(currentSegment.trim());
2638
+ }
2639
+ if (messageSegments.length > 0) {
2640
+ return {
2641
+ text: messageSegments[messageSegments.length - 1],
2642
+ hasAgentMessage
2643
+ };
2644
+ }
2645
+ return {
2646
+ text: output.trim(),
2647
+ hasAgentMessage
2648
+ };
2649
+ }
2650
+ function sanitizePromptText(text) {
2651
+ const trimmed = text.trim();
2652
+ const paragraphs = trimmed.split(/\n\s*\n/);
2653
+ if (paragraphs.length < 2) {
2654
+ return trimmed;
2655
+ }
2656
+ const firstParagraph = paragraphs[0].trim().replace(/\s+/g, " ").toLowerCase();
2657
+ if (!looksLikeWorkflowPreamble(firstParagraph)) {
2658
+ return trimmed;
2659
+ }
2660
+ return paragraphs.slice(1).join(`
2661
+
2662
+ `).trim();
2663
+ }
2664
+ function looksLikeWorkflowPreamble(paragraph) {
2665
+ if (!paragraph.startsWith("using ")) {
2666
+ return false;
2667
+ }
2668
+ return paragraph.includes("using-superpowers") || paragraph.includes("repo workflow requirement") || paragraph.includes("workflow requirement") || paragraph.includes("before responding") || paragraph.includes("skill check");
2669
+ }
2670
+ function extractJsonRpcErrorMessages(output) {
2671
+ return output.split(`
2672
+ `).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
2673
+ try {
2674
+ const payload = JSON.parse(line);
2675
+ if (typeof payload.error?.message === "string" && payload.error.message.length > 0) {
2676
+ return [payload.error.message];
2677
+ }
2678
+ } catch {
2679
+ return [];
2680
+ }
2681
+ return [];
2682
+ });
2683
+ }
2684
+
2479
2685
  // src/transport/acpx-cli/node-pty-helper.ts
2480
2686
  import { chmod as chmodFs } from "node:fs/promises";
2481
- import { dirname as dirname8, join as join3 } from "node:path";
2687
+ import { dirname as dirname7, join as join3 } from "node:path";
2482
2688
  function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
2483
2689
  if (platform === "win32") {
2484
2690
  return null;
2485
2691
  }
2486
- return join3(dirname8(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
2692
+ return join3(dirname7(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
2487
2693
  }
2488
2694
  async function ensureNodePtyHelperExecutable(helperPath, chmod = chmodFs) {
2489
2695
  if (!helperPath) {
@@ -2505,8 +2711,9 @@ import { createRequire as createRequire3 } from "node:module";
2505
2711
  import { spawn as spawn3 } from "node:child_process";
2506
2712
  import { spawn as spawnPty } from "node-pty";
2507
2713
  async function defaultRunner(command, args, options) {
2508
- return await new Promise((resolve2, reject) => {
2509
- const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
2714
+ return await new Promise((resolve, reject) => {
2715
+ const spawnSpec = resolveSpawnCommand(command, args);
2716
+ const child = spawn3(spawnSpec.command, spawnSpec.args, { stdio: ["ignore", "pipe", "pipe"] });
2510
2717
  let stdout = "";
2511
2718
  let stderr = "";
2512
2719
  const timeoutId = options?.timeoutMs ? setTimeout(() => {
@@ -2527,15 +2734,16 @@ async function defaultRunner(command, args, options) {
2527
2734
  child.on("close", (code) => {
2528
2735
  if (timeoutId)
2529
2736
  clearTimeout(timeoutId);
2530
- resolve2({ code: code ?? 1, stdout, stderr });
2737
+ resolve({ code: code ?? 1, stdout, stderr });
2531
2738
  });
2532
2739
  });
2533
2740
  }
2534
2741
  async function defaultPtyRunner(command, args, options) {
2535
2742
  const helperPath = resolveNodePtyHelperPath(require3.resolve("node-pty/package.json"), process.platform, process.arch);
2536
2743
  await ensureNodePtyHelperExecutable(helperPath);
2537
- return await new Promise((resolve2, reject) => {
2538
- const child = spawnPty(command, args, {
2744
+ return await new Promise((resolve, reject) => {
2745
+ const spawnSpec = resolveSpawnCommand(command, args);
2746
+ const child = spawnPty(spawnSpec.command, spawnSpec.args, {
2539
2747
  name: "xterm-color",
2540
2748
  cols: 80,
2541
2749
  rows: 24,
@@ -2553,7 +2761,7 @@ async function defaultPtyRunner(command, args, options) {
2553
2761
  child.onExit(({ exitCode }) => {
2554
2762
  if (timeoutId)
2555
2763
  clearTimeout(timeoutId);
2556
- resolve2({ code: exitCode, stdout: output, stderr: "" });
2764
+ resolve({ code: exitCode, stdout: output, stderr: "" });
2557
2765
  });
2558
2766
  });
2559
2767
  }
@@ -2582,8 +2790,8 @@ class AcpxCliTransport {
2582
2790
  });
2583
2791
  }
2584
2792
  async prompt(session, text) {
2585
- const output = await this.run(this.buildPromptArgs(session, text));
2586
- return { text: sanitizePromptText(extractPromptText(output)) };
2793
+ const result = await this.runCommand(this.command, this.buildPromptArgs(session, text));
2794
+ return { text: getPromptText(result) };
2587
2795
  }
2588
2796
  async cancel(session) {
2589
2797
  const output = await this.run(this.buildArgs(session, [
@@ -2624,12 +2832,13 @@ class AcpxCliTransport {
2624
2832
  return result.stdout;
2625
2833
  }
2626
2834
  async runCommandWithTimeout(runner, args, options) {
2835
+ const spawnSpec = resolveSpawnCommand(this.command, args);
2627
2836
  if (!options?.timeoutMs) {
2628
- return await runner(this.command, args, undefined);
2837
+ return await runner(spawnSpec.command, spawnSpec.args, undefined);
2629
2838
  }
2630
2839
  let timeoutId;
2631
2840
  return await Promise.race([
2632
- runner(this.command, args, options).finally(() => {
2841
+ runner(spawnSpec.command, spawnSpec.args, options).finally(() => {
2633
2842
  if (timeoutId)
2634
2843
  clearTimeout(timeoutId);
2635
2844
  }),
@@ -2672,88 +2881,9 @@ function renderCommandForError(args) {
2672
2881
  }
2673
2882
  return rendered.join(" ");
2674
2883
  }
2675
- function extractPromptText(output) {
2676
- const lines = output.split(`
2677
- `).map((line) => line.trim()).filter((line) => line.length > 0);
2678
- const messageSegments = [];
2679
- let currentSegment = "";
2680
- for (const line of lines) {
2681
- try {
2682
- const event = JSON.parse(line);
2683
- const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
2684
- if (isMessageChunk) {
2685
- const chunk = event.params.update.content.text ?? "";
2686
- if (chunk.length > 0) {
2687
- currentSegment += chunk;
2688
- }
2689
- continue;
2690
- }
2691
- if (currentSegment.trim().length > 0) {
2692
- messageSegments.push(currentSegment.trim());
2693
- }
2694
- currentSegment = "";
2695
- } catch {
2696
- if (currentSegment.trim().length > 0) {
2697
- messageSegments.push(currentSegment.trim());
2698
- currentSegment = "";
2699
- }
2700
- }
2701
- }
2702
- if (currentSegment.trim().length > 0) {
2703
- messageSegments.push(currentSegment.trim());
2704
- }
2705
- if (messageSegments.length > 0) {
2706
- return messageSegments[messageSegments.length - 1];
2707
- }
2708
- return output.trim();
2709
- }
2710
- function sanitizePromptText(text) {
2711
- const trimmed = text.trim();
2712
- const paragraphs = trimmed.split(/\n\s*\n/);
2713
- if (paragraphs.length < 2) {
2714
- return trimmed;
2715
- }
2716
- const firstParagraph = paragraphs[0].trim().replace(/\s+/g, " ").toLowerCase();
2717
- if (!looksLikeWorkflowPreamble(firstParagraph)) {
2718
- return trimmed;
2719
- }
2720
- return paragraphs.slice(1).join(`
2721
-
2722
- `).trim();
2723
- }
2724
- function looksLikeWorkflowPreamble(paragraph) {
2725
- if (!paragraph.startsWith("using ")) {
2726
- return false;
2727
- }
2728
- return paragraph.includes("using-superpowers") || paragraph.includes("repo workflow requirement") || paragraph.includes("workflow requirement") || paragraph.includes("before responding") || paragraph.includes("skill check");
2729
- }
2730
- function normalizeCommandError(result) {
2731
- const rpcMessages = extractJsonRpcErrorMessages(result.stderr).concat(extractJsonRpcErrorMessages(result.stdout)).filter((message) => message.length > 0);
2732
- const preferredMessage = [...rpcMessages].reverse().find((message) => message !== "Resource not found");
2733
- if (preferredMessage) {
2734
- return preferredMessage;
2735
- }
2736
- if (rpcMessages.length > 0) {
2737
- return rpcMessages[rpcMessages.length - 1] ?? null;
2738
- }
2739
- return result.stderr.trim() || result.stdout.trim() || null;
2740
- }
2741
- function extractJsonRpcErrorMessages(output) {
2742
- return output.split(`
2743
- `).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
2744
- try {
2745
- const payload = JSON.parse(line);
2746
- if (typeof payload.error?.message === "string" && payload.error.message.length > 0) {
2747
- return [payload.error.message];
2748
- }
2749
- } catch {
2750
- return [];
2751
- }
2752
- return [];
2753
- });
2754
- }
2755
2884
  var require3;
2756
2885
  var init_acpx_cli_transport = __esm(() => {
2886
+ init_spawn_command();
2757
2887
  init_node_pty_helper();
2758
2888
  require3 = createRequire3(import.meta.url);
2759
2889
  });
@@ -2765,8 +2895,8 @@ __export(exports_main, {
2765
2895
  main: () => main2,
2766
2896
  buildApp: () => buildApp
2767
2897
  });
2768
- import { homedir } from "node:os";
2769
- import { dirname as dirname9, join as join4 } from "node:path";
2898
+ import { homedir as homedir2 } from "node:os";
2899
+ import { dirname as dirname8, join as join4 } from "node:path";
2770
2900
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2771
2901
  async function buildApp(paths, deps = {}) {
2772
2902
  await ensureConfigExists(paths.configPath);
@@ -2826,7 +2956,7 @@ async function main2() {
2826
2956
  }
2827
2957
  }
2828
2958
  function resolveRuntimePaths() {
2829
- const home = process.env.HOME ?? homedir();
2959
+ const home = process.env.HOME ?? homedir2();
2830
2960
  if (!home) {
2831
2961
  throw new Error("Unable to resolve the current user home directory");
2832
2962
  }
@@ -2842,7 +2972,7 @@ function resolveBridgeEntryPath() {
2842
2972
  return fileURLToPath2(new URL("./bridge/bridge-main.ts", import.meta.url));
2843
2973
  }
2844
2974
  function resolveAppLogPath(configPath) {
2845
- const rootDir = dirname9(configPath);
2975
+ const rootDir = dirname8(configPath);
2846
2976
  const runtimeDir = join4(rootDir, "runtime");
2847
2977
  return join4(runtimeDir, "app.log");
2848
2978
  }
@@ -2862,7 +2992,7 @@ var init_main = __esm(async () => {
2862
2992
  });
2863
2993
 
2864
2994
  // src/cli.ts
2865
- import { homedir as homedir2 } from "node:os";
2995
+ import { homedir as homedir3 } from "node:os";
2866
2996
  import { sep } from "node:path";
2867
2997
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2868
2998
 
@@ -3107,6 +3237,7 @@ async function spawnWindowsHiddenProcess(request) {
3107
3237
  return;
3108
3238
  }
3109
3239
  settled = true;
3240
+ child.stdout?.destroy();
3110
3241
  child.unref();
3111
3242
  resolve(pid);
3112
3243
  });
@@ -3322,7 +3453,7 @@ function createDefaultController() {
3322
3453
  });
3323
3454
  }
3324
3455
  function requireHome() {
3325
- const home = process.env.HOME ?? homedir2();
3456
+ const home = process.env.HOME ?? homedir3();
3326
3457
  if (!home) {
3327
3458
  throw new Error("Unable to resolve the current user home directory");
3328
3459
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weacpx",
3
3
  "description": "用微信远程控制 `acpx` 会话的控制台, 底层基于 `weixin-agent-sdk` 与 `acpx`",
4
- "version": "0.1.1",
4
+ "version": "0.1.3",
5
5
  "main": "index.js",
6
6
  "directories": {
7
7
  "doc": "docs",