weacpx 0.1.7 → 0.2.1

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
@@ -4,6 +4,7 @@
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/weacpx?style=flat-square)](https://www.npmjs.com/package/weacpx)
6
6
  [![Node.js Version](https://img.shields.io/node/v/weacpx?style=flat-square)](https://nodejs.org)
7
+ [![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat-square&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/gadzan/weacpx)
7
8
  [![License](https://img.shields.io/npm/l/weacpx?style=flat-square)](./LICENSE)
8
9
 
9
10
  ![weacpx logo](assets/weacpx.jpg)
@@ -70,7 +71,9 @@ bun add -g weacpx
70
71
  hello
71
72
  ```
72
73
 
73
- 如果任务比较长,`weacpx` 会优先把 Agent 的中间回复分段发回微信,而不是一直等到最后一条结果。
74
+ 如果任务比较长,`weacpx` 在支持流式中间回复的 transport(当前主要是 `acpx-cli`)下,会优先把 Agent 的中间回复分段发回微信,而不是一直等到最后一条结果。
75
+
76
+ 如果你更想要“一次性只回最终结果”,可以配置全局默认 `wechat.replyMode`,或在当前会话里用 `/replymode final` 临时覆盖。
74
77
 
75
78
  如果你是从源码仓库直接使用:
76
79
 
@@ -95,14 +98,55 @@ bun run dev
95
98
  - `weacpx start`
96
99
  - `weacpx status`
97
100
  - `weacpx stop`
101
+ - `weacpx doctor`
102
+ - `weacpx version`
98
103
 
99
- 说明:
104
+ 其他说明:
100
105
 
101
106
  - `run` 前台运行,适合调试
102
107
  - `start` 后台启动
103
108
  - `status` 查看后台状态、PID、配置路径和日志路径
104
109
  - `stop` 停止后台实例
105
110
  - `logout` 清除本机已保存的微信登录凭证;如果当前没有已登录账号,会直接提示
111
+ - `doctor` 运行诊断,默认检查 config / runtime / daemon / wechat / acpx / bridge
112
+ - `version` 输出当前安装的 `weacpx` 版本号,可用于排查环境或确认升级是否生效
113
+
114
+ ### doctor 诊断
115
+
116
+ `weacpx doctor` 用来快速检查本机环境是否能正常运行。
117
+
118
+ ```bash
119
+ weacpx doctor
120
+ weacpx doctor --verbose
121
+ weacpx doctor --smoke
122
+ weacpx doctor --smoke --agent codex --workspace backend
123
+ ```
124
+
125
+ 说明:
126
+
127
+ - `--verbose` 会展开每个检查的技术细节,方便定位问题
128
+ - `--smoke` 会额外执行一次真实 transport 级别的最小 prompt 检查
129
+ - `--agent` / `--workspace` 只影响 `--smoke`,不会改变默认诊断检查
130
+ - `--smoke` 可能留下一个临时的底层 `acpx` session;这是为了换取真实链路验证能力
131
+ - 如果不传 `--smoke`,相关 smoke 检查会被标记为 `SKIP`
132
+
133
+ ### version 查看版本
134
+
135
+ `weacpx version` 用来输出当前安装的 CLI 版本号。
136
+
137
+ ```bash
138
+ weacpx version
139
+ weacpx --version
140
+ weacpx -v
141
+ ```
142
+
143
+ 说明:
144
+
145
+ - 三种写法都会输出同一个版本号
146
+ - 适合在升级后确认当前生效的是哪个版本
147
+ - 排查用户环境问题时,建议先附上这里输出的版本信息
148
+
149
+ ### logout 退出登录
106
150
 
107
151
  说明:
108
152
 
@@ -169,14 +213,18 @@ bun run dev
169
213
  | 命令 | 说明 |
170
214
  |------|------|
171
215
  | `/sessions` / `/session` / `/ss` | 查看当前已添加的会话 |
172
- | `/ss <agent> -d <path>` | 新建会话(自动按目录名推导并创建或复用 workspace,再创建或复用 session) |
173
- | `/ss new <agent> -d <path>` | 强制新建会话 |
216
+ | `/ss <agent> (-d <path> | --ws <name>)` | 新建会话;传 `-d` 时按目录自动创建或复用 workspace,传 `--ws` 时复用已注册 workspace |
217
+ | `/ss new <agent> (-d <path> | --ws <name>)` | 强制新建会话;`--ws` 只复用已注册 workspace |
174
218
  | `/ss new <alias> -a <name> --ws <name>` | 强制新建会话,并指定 agent 和 workspace |
175
219
  | `/ss attach <alias> -a <name> --ws <name> --name <transport-session>` | 恢复已存在的会话 |
176
220
  | `/use <alias>` | 切换当前会话 |
177
221
  | `/status` | 查看当前会话状态 |
178
222
  | `/mode` | 查看当前会话已保存的 mode |
179
223
  | `/mode <id>` | 设置当前会话 mode,例如 `/mode plan` |
224
+ | `/replymode` | 查看当前会话的回复输出模式(全局默认 / 当前覆盖 / 实际生效) |
225
+ | `/replymode stream` | 当前逻辑会话使用流式回复 |
226
+ | `/replymode final` | 当前逻辑会话只发送最终文本结果 |
227
+ | `/replymode reset` | 清除当前逻辑会话覆盖,回退到全局默认 |
180
228
  | `/session reset` | 重置当前会话上下文,保留 alias/agent/workspace,但重新绑定到一个新的后端 session |
181
229
  | `/clear` | `/session reset` 的快捷别名 |
182
230
  | `/cancel` | 取消当前会话 |
@@ -185,13 +233,45 @@ bun run dev
185
233
  说明:
186
234
 
187
235
  - `/ss <agent> -d <path>` 是最常用入口,会自动按目录名推导并创建或复用 workspace,再创建或复用 session
188
- - `/ss new <agent> -d <path>` 表示强制新建 session
236
+ - `/ss <agent> --ws <name>` 会直接复用已注册 workspace,再创建或复用 session
237
+ - `/ss new <agent> (-d <path> | --ws <name>)` 表示强制新建 session
189
238
  - `/use <alias>` 用来切换当前会话
190
239
  - `/mode` 会显示当前逻辑会话里保存的 mode;如果还没设置过,会显示“未设置”
191
240
  - `/mode <id>` 会把 mode 透传给底层 `acpx set-mode`,成功后再写回当前逻辑会话
241
+ - `/replymode` 修改的是**当前逻辑会话**的 reply mode override,不是底层 transport session 的全局属性
242
+ - `wechat.replyMode` 是全局默认值;`/replymode reset` 会回退到这个默认值
243
+ - `final` 只影响微信侧是否实时发送文本流式片段,不改变 acpx transport 本身的生成方式
192
244
  - `/session reset` 和 `/clear` 会保留当前逻辑会话名,但重新创建一个新的后端 session,从空上下文重新开始
193
245
  - 非 `/` 开头的文本会发送到当前 session
194
246
 
247
+ ### 配置命令
248
+
249
+ `/config` 用来查看和修改一小部分**受支持的配置字段**,不是任意 JSON 编辑器。
250
+
251
+ | 命令 | 说明 |
252
+ |------|------|
253
+ | `/config` | 查看当前支持通过微信修改的配置路径 |
254
+ | `/config set <path> <value>` | 修改一个受支持的配置值 |
255
+
256
+ 常见示例:
257
+
258
+ ```text
259
+ /config
260
+ /config set wechat.replyMode final
261
+ /config set logging.level debug
262
+ /config set transport.permissionMode approve-reads
263
+ /config set workspaces.backend.description backend repo
264
+ ```
265
+
266
+ 说明:
267
+
268
+ - `/config` 只允许修改白名单字段,不支持任意路径写入
269
+ - `agents.<name>.*` / `workspaces.<name>.*` 这类动态路径要求目标已经存在,不会自动创建
270
+ - `/config set wechat.replyMode final` 修改的是**全局默认回复模式**
271
+ - `/replymode final` 修改的是**当前逻辑会话覆盖**
272
+ - 成功修改后会立即写回 `~/.weacpx/config.json`
273
+ - 更完整的边界和支持字段,请参考 [docs/config-command.md](./docs/config-command.md)
274
+
195
275
  ### 权限策略
196
276
 
197
277
  `weacpx` 支持直接在微信里查看和切换 `acpx` 的权限策略。
@@ -203,7 +283,6 @@ bun run dev
203
283
  | `/pm set read` | 切到 `approve-reads` |
204
284
  | `/pm set deny` | 切到 `deny-all` |
205
285
  | `/pm auto` | 查看当前非交互策略 |
206
- | `/pm auto allow` | 切到 `allow` |
207
286
  | `/pm auto deny` | 切到 `deny` |
208
287
  | `/pm auto fail` | 切到 `fail` |
209
288
 
@@ -282,7 +361,7 @@ bun run dev
282
361
  "type": "acpx-bridge",
283
362
  "sessionInitTimeoutMs": 120000,
284
363
  "permissionMode": "approve-all",
285
- "nonInteractivePermissions": "fail"
364
+ "nonInteractivePermissions": "deny"
286
365
  }
287
366
  }
288
367
  ```
@@ -290,8 +369,8 @@ bun run dev
290
369
  说明:
291
370
 
292
371
  - `permissionMode`: `approve-all`、`approve-reads`、`deny-all`
293
- - `nonInteractivePermissions`: `allow`、`deny`、`fail`
294
- - 默认值分别是 `approve-all` 和 `fail`
372
+ - `nonInteractivePermissions`: `deny`、`fail`
373
+ - 默认值分别是 `approve-all` 和 `deny`
295
374
  - 也可以直接在微信里通过 `/pm` 和 `/pm auto` 修改
296
375
 
297
376
  ### 日志配置
@@ -3,7 +3,7 @@
3
3
  "type": "acpx-bridge",
4
4
  "sessionInitTimeoutMs": 120000,
5
5
  "permissionMode": "approve-all",
6
- "nonInteractivePermissions": "fail"
6
+ "nonInteractivePermissions": "deny"
7
7
  },
8
8
  "logging": {
9
9
  "level": "info",
@@ -11,6 +11,9 @@
11
11
  "maxFiles": 5,
12
12
  "retentionDays": 7
13
13
  },
14
+ "wechat": {
15
+ "replyMode": "stream"
16
+ },
14
17
  "agents": {
15
18
  "codex": {
16
19
  "driver": "codex"
@@ -44,8 +44,19 @@ var __export = (target, all) => {
44
44
  });
45
45
  };
46
46
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
+ var __promiseAll = (args) => Promise.all(args);
47
48
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
48
49
 
50
+ // src/transport/acpx-bridge/acpx-bridge-protocol.ts
51
+ function encodeBridgeRequest(request) {
52
+ return `${JSON.stringify(request)}
53
+ `;
54
+ }
55
+ function encodeBridgePromptSegmentEvent(event) {
56
+ return `${JSON.stringify(event)}
57
+ `;
58
+ }
59
+
49
60
  // src/transport/prompt-output.ts
50
61
  function getPromptText(result) {
51
62
  const stdoutOutput = extractPromptOutput(result.stdout);
@@ -193,9 +204,75 @@ var init_spawn_command = __esm(() => {
193
204
  SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
194
205
  });
195
206
 
207
+ // src/transport/streaming-prompt.ts
208
+ function createStreamingPromptState() {
209
+ return {
210
+ buffer: "",
211
+ segments: [],
212
+ hasAgentMessage: false,
213
+ pendingLine: "",
214
+ finalize() {
215
+ if (this.pendingLine.trim().length > 0) {
216
+ parseStreamingChunks(this, this.pendingLine);
217
+ }
218
+ const remaining = this.buffer.trim();
219
+ this.buffer = "";
220
+ this.pendingLine = "";
221
+ return remaining;
222
+ }
223
+ };
224
+ }
225
+ function parseStreamingDataChunk(state, chunk) {
226
+ state.pendingLine += chunk;
227
+ let boundary;
228
+ while ((boundary = state.pendingLine.indexOf(`
229
+ `)) !== -1) {
230
+ const line = state.pendingLine.slice(0, boundary);
231
+ state.pendingLine = state.pendingLine.slice(boundary + 1);
232
+ parseStreamingChunks(state, line);
233
+ }
234
+ }
235
+ function parseStreamingChunks(state, line) {
236
+ const trimmed = line.trim();
237
+ if (trimmed.length === 0)
238
+ return;
239
+ let event;
240
+ try {
241
+ event = JSON.parse(trimmed);
242
+ } catch {
243
+ return;
244
+ }
245
+ 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";
246
+ if (!isMessageChunk)
247
+ return;
248
+ state.hasAgentMessage = true;
249
+ const chunk = event.params.update.content.text ?? "";
250
+ if (chunk.length === 0)
251
+ return;
252
+ state.buffer += chunk;
253
+ let boundary;
254
+ while ((boundary = state.buffer.indexOf(`
255
+
256
+ `)) !== -1) {
257
+ const segment = state.buffer.slice(0, boundary).trim();
258
+ state.buffer = state.buffer.slice(boundary + 2);
259
+ if (segment.length > 0) {
260
+ state.segments.push(segment);
261
+ }
262
+ }
263
+ }
264
+
196
265
  // src/bridge/bridge-main.ts
197
266
  import { createInterface } from "node:readline";
198
267
 
268
+ // src/bridge/bridge-env.ts
269
+ function normalizeBridgePermissionMode(value) {
270
+ return value === "approve-reads" || value === "deny-all" || value === "approve-all" ? value : "approve-all";
271
+ }
272
+ function normalizeBridgeNonInteractivePermissions(value) {
273
+ return value === "deny" || value === "fail" ? value : "deny";
274
+ }
275
+
199
276
  // src/bridge/bridge-server.ts
200
277
  init_prompt_output();
201
278
 
@@ -204,6 +281,7 @@ class BridgeInvalidRequestError extends Error {
204
281
  var BRIDGE_METHODS = new Set([
205
282
  "ping",
206
283
  "shutdown",
284
+ "updatePermissionPolicy",
207
285
  "hasSession",
208
286
  "ensureSession",
209
287
  "prompt",
@@ -216,12 +294,12 @@ class BridgeServer {
216
294
  constructor(runtime) {
217
295
  this.runtime = runtime;
218
296
  }
219
- async handleLine(line) {
297
+ async handleLine(line, writeLine) {
220
298
  let requestId = extractRequestId(line);
221
299
  try {
222
300
  const request = parseBridgeRequest(line);
223
301
  requestId = request.id;
224
- const result = await this.dispatch(request.method, request.params);
302
+ const result = await this.dispatch(request.id, request.method, request.params, writeLine);
225
303
  return `${JSON.stringify({
226
304
  id: request.id,
227
305
  ok: true,
@@ -248,12 +326,17 @@ class BridgeServer {
248
326
  `;
249
327
  }
250
328
  }
251
- async dispatch(method, params) {
329
+ async dispatch(requestId, method, params, writeLine) {
252
330
  switch (method) {
253
331
  case "ping":
254
332
  return {};
255
333
  case "shutdown":
256
334
  return await this.runtime.shutdown();
335
+ case "updatePermissionPolicy":
336
+ return await this.runtime.updatePermissionPolicy({
337
+ permissionMode: requirePermissionMode(params, "permissionMode"),
338
+ nonInteractivePermissions: requireNonInteractivePermissions(params, "nonInteractivePermissions")
339
+ });
257
340
  case "hasSession":
258
341
  return await this.runtime.hasSession({
259
342
  agent: requireString(params, "agent"),
@@ -275,6 +358,14 @@ class BridgeServer {
275
358
  cwd: requireString(params, "cwd"),
276
359
  name: requireString(params, "name"),
277
360
  text: requireString(params, "text")
361
+ }, (event) => {
362
+ if (event.type === "prompt.segment") {
363
+ writeLine?.(encodeBridgePromptSegmentEvent({
364
+ id: requestId,
365
+ event: "prompt.segment",
366
+ text: event.text
367
+ }));
368
+ }
278
369
  });
279
370
  case "setMode":
280
371
  return await this.runtime.setMode({
@@ -347,6 +438,20 @@ function requireString(params, key) {
347
438
  }
348
439
  return value;
349
440
  }
441
+ function requirePermissionMode(params, key) {
442
+ const value = params[key];
443
+ if (value === "approve-all" || value === "approve-reads" || value === "deny-all") {
444
+ return value;
445
+ }
446
+ throw new BridgeInvalidRequestError(`${key} must be approve-all, approve-reads, or deny-all`);
447
+ }
448
+ function requireNonInteractivePermissions(params, key) {
449
+ const value = params[key];
450
+ if (value === "deny" || value === "fail") {
451
+ return value;
452
+ }
453
+ throw new BridgeInvalidRequestError(`${key} must be deny or fail`);
454
+ }
350
455
  function asOptionalString(value) {
351
456
  if (typeof value !== "string" || value.length === 0) {
352
457
  return;
@@ -357,19 +462,28 @@ function asOptionalString(value) {
357
462
  // src/bridge/bridge-runtime.ts
358
463
  init_spawn_command();
359
464
  init_prompt_output();
465
+ import { copyFile, readdir } from "node:fs/promises";
466
+ import { homedir } from "node:os";
467
+ import { join } from "node:path";
360
468
  import { spawn } from "node:child_process";
361
- import { fileURLToPath } from "node:url";
362
469
 
363
470
  class BridgeRuntime {
364
471
  command;
365
472
  run;
366
473
  runSessionCreate;
367
474
  options;
368
- constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner, options = {}) {
475
+ runPromptCommand;
476
+ constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner, options = {}, runPromptCommand = defaultPromptRunner) {
369
477
  this.command = command;
370
478
  this.run = run;
371
479
  this.runSessionCreate = runSessionCreate;
372
480
  this.options = options;
481
+ this.runPromptCommand = runPromptCommand;
482
+ }
483
+ async updatePermissionPolicy(policy) {
484
+ this.options.permissionMode = policy.permissionMode;
485
+ this.options.nonInteractivePermissions = policy.nonInteractivePermissions;
486
+ return {};
373
487
  }
374
488
  async hasSession(input) {
375
489
  const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
@@ -397,20 +511,27 @@ class BridgeRuntime {
397
511
  return {};
398
512
  }
399
513
  const createSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "new", "--name", input.name]));
400
- const createdWithHelper = await this.runSessionCreate(createSpec.command, createSpec.args, input.cwd);
401
- if (createdWithHelper.code !== 0) {
402
- throw new Error(createdWithHelper.stderr || createdWithHelper.stdout || ensured.stderr || ensured.stdout || "failed to create session");
514
+ const created = await this.runSessionCreate(createSpec.command, createSpec.args, input.cwd);
515
+ if (created.code === 0) {
516
+ return {};
403
517
  }
404
- return {};
518
+ const output = created.stderr || created.stdout || "";
519
+ if (output.includes("EPERM") && await tryRepairAcpxSessionIndex()) {
520
+ const repaired = await this.run(existingSpec.command, existingSpec.args);
521
+ if (repaired.code === 0) {
522
+ return {};
523
+ }
524
+ }
525
+ throw new Error(output || ensured.stderr || ensured.stdout || "failed to create session");
405
526
  }
406
- async prompt(input) {
527
+ async prompt(input, onEvent) {
407
528
  const spawnSpec = resolveSpawnCommand(this.command, this.buildPromptArgs(input, [
408
529
  "prompt",
409
530
  "-s",
410
531
  input.name,
411
532
  input.text
412
533
  ]));
413
- const result = await this.run(spawnSpec.command, spawnSpec.args);
534
+ const result = onEvent ? await this.runPromptCommand(spawnSpec.command, spawnSpec.args, onEvent) : await this.run(spawnSpec.command, spawnSpec.args);
414
535
  return { text: getPromptText(result) };
415
536
  }
416
537
  async setMode(input) {
@@ -473,7 +594,7 @@ class BridgeRuntime {
473
594
  }
474
595
  buildPermissionArgs() {
475
596
  const permissionMode = this.options.permissionMode ?? "approve-all";
476
- const nonInteractivePermissions = this.options.nonInteractivePermissions ?? "fail";
597
+ const nonInteractivePermissions = this.options.nonInteractivePermissions ?? "deny";
477
598
  const modeFlag = permissionMode === "approve-reads" ? "--approve-reads" : permissionMode === "deny-all" ? "--deny-all" : "--approve-all";
478
599
  return [modeFlag, "--non-interactive-permissions", nonInteractivePermissions];
479
600
  }
@@ -495,10 +616,66 @@ async function defaultRunner(command, args) {
495
616
  });
496
617
  });
497
618
  }
619
+ async function runStreamingPrompt(command, args, onEvent, options = {}) {
620
+ const spawnPrompt = options.spawnPrompt ?? ((spawnCommand, spawnArgs) => spawn(spawnCommand, spawnArgs, { stdio: ["ignore", "pipe", "pipe"] }));
621
+ const setIntervalFn = options.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
622
+ const clearIntervalFn = options.clearIntervalFn ?? ((timer) => clearInterval(timer));
623
+ const maxSegmentWaitMs = options.maxSegmentWaitMs ?? 30000;
624
+ const flushCheckIntervalMs = options.flushCheckIntervalMs ?? 5000;
625
+ const now = options.now ?? (() => Date.now());
626
+ return await new Promise((resolve, reject) => {
627
+ const child = spawnPrompt(command, args);
628
+ let stdout = "";
629
+ let stderr = "";
630
+ const state = createStreamingPromptState();
631
+ let lastReplyAt = now();
632
+ const flushBuffer = () => {
633
+ const remaining = state.buffer.trim();
634
+ if (remaining.length > 0) {
635
+ state.buffer = "";
636
+ onEvent?.({ type: "prompt.segment", text: remaining });
637
+ lastReplyAt = now();
638
+ }
639
+ };
640
+ const timer = setIntervalFn(() => {
641
+ if (state.buffer.trim().length > 0 && now() - lastReplyAt >= maxSegmentWaitMs) {
642
+ flushBuffer();
643
+ }
644
+ }, flushCheckIntervalMs);
645
+ child.stdout.setEncoding("utf8");
646
+ child.stdout.on("data", (chunk) => {
647
+ const text = String(chunk);
648
+ stdout += text;
649
+ parseStreamingDataChunk(state, text);
650
+ for (const segment of state.segments.splice(0)) {
651
+ onEvent?.({ type: "prompt.segment", text: segment });
652
+ lastReplyAt = now();
653
+ }
654
+ });
655
+ child.stderr.on("data", (chunk) => {
656
+ stderr += String(chunk);
657
+ });
658
+ child.on("error", (error) => {
659
+ clearIntervalFn(timer);
660
+ reject(error);
661
+ });
662
+ child.on("close", (code) => {
663
+ clearIntervalFn(timer);
664
+ const remaining = state.finalize();
665
+ if (remaining.length > 0) {
666
+ onEvent?.({ type: "prompt.segment", text: remaining });
667
+ }
668
+ resolve({ code: code ?? 1, stdout, stderr });
669
+ });
670
+ });
671
+ }
672
+ async function defaultPromptRunner(command, args, onEvent) {
673
+ return await runStreamingPrompt(command, args, onEvent);
674
+ }
498
675
  async function shellSessionCreateRunner(command, args, cwd) {
499
- const helperPath = fileURLToPath(new URL("../../scripts/acpx-session-new-helper.sh", import.meta.url));
500
676
  return await new Promise((resolve, reject) => {
501
- const child = spawn("/bin/zsh", [helperPath, command, cwd, ...args], {
677
+ const child = spawn(command, args, {
678
+ cwd,
502
679
  stdio: ["ignore", "pipe", "pipe"]
503
680
  });
504
681
  let stdout = "";
@@ -515,17 +692,58 @@ async function shellSessionCreateRunner(command, args, cwd) {
515
692
  });
516
693
  });
517
694
  }
695
+ async function tryRepairAcpxSessionIndex() {
696
+ if (process.platform !== "win32") {
697
+ return false;
698
+ }
699
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir();
700
+ if (!home) {
701
+ return false;
702
+ }
703
+ const sessionsDir = join(home, ".acpx", "sessions");
704
+ const indexPath = join(sessionsDir, "index.json");
705
+ let files;
706
+ try {
707
+ files = await readdir(sessionsDir);
708
+ } catch {
709
+ return false;
710
+ }
711
+ const tmpFiles = files.filter((f) => f.startsWith("index.json.") && f.endsWith(".tmp"));
712
+ if (tmpFiles.length === 0) {
713
+ return false;
714
+ }
715
+ let latestTmp = "";
716
+ let latestTime = 0;
717
+ for (const f of tmpFiles) {
718
+ const match = f.match(/^index\.json\.\d+\.(\d+)\.tmp$/);
719
+ if (match && Number(match[1]) > latestTime) {
720
+ latestTime = Number(match[1]);
721
+ latestTmp = f;
722
+ }
723
+ }
724
+ if (!latestTmp) {
725
+ return false;
726
+ }
727
+ try {
728
+ await copyFile(join(sessionsDir, latestTmp), indexPath);
729
+ return true;
730
+ } catch {
731
+ return false;
732
+ }
733
+ }
518
734
 
519
735
  // src/bridge/bridge-main.ts
520
736
  var server = new BridgeServer(new BridgeRuntime(process.env.WEACPX_BRIDGE_ACPX_COMMAND ?? "acpx", undefined, undefined, {
521
- permissionMode: process.env.WEACPX_BRIDGE_PERMISSION_MODE === "approve-reads" || process.env.WEACPX_BRIDGE_PERMISSION_MODE === "deny-all" ? process.env.WEACPX_BRIDGE_PERMISSION_MODE : "approve-all",
522
- nonInteractivePermissions: process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS === "deny" ? "deny" : "fail"
737
+ permissionMode: normalizeBridgePermissionMode(process.env.WEACPX_BRIDGE_PERMISSION_MODE),
738
+ nonInteractivePermissions: normalizeBridgeNonInteractivePermissions(process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS)
523
739
  }));
524
740
  var input = createInterface({
525
741
  input: process.stdin,
526
742
  crlfDelay: Infinity
527
743
  });
528
744
  for await (const line of input) {
529
- const response = await server.handleLine(line);
745
+ const response = await server.handleLine(line, (chunk) => {
746
+ process.stdout.write(chunk);
747
+ });
530
748
  process.stdout.write(response);
531
749
  }