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 +88 -9
- package/config.example.json +4 -1
- package/dist/bridge/bridge-main.js +235 -17
- package/dist/cli.js +2469 -520
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/weacpx)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
|
+
[](https://zread.ai/gadzan/weacpx)
|
|
7
8
|
[](./LICENSE)
|
|
8
9
|
|
|
9
10
|

|
|
@@ -70,7 +71,9 @@ bun add -g weacpx
|
|
|
70
71
|
hello
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
如果任务比较长,`weacpx`
|
|
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
|
|
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
|
|
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": "
|
|
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`: `
|
|
294
|
-
- 默认值分别是 `approve-all` 和 `
|
|
372
|
+
- `nonInteractivePermissions`: `deny`、`fail`
|
|
373
|
+
- 默认值分别是 `approve-all` 和 `deny`
|
|
295
374
|
- 也可以直接在微信里通过 `/pm` 和 `/pm auto` 修改
|
|
296
375
|
|
|
297
376
|
### 日志配置
|
package/config.example.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"type": "acpx-bridge",
|
|
4
4
|
"sessionInitTimeoutMs": 120000,
|
|
5
5
|
"permissionMode": "approve-all",
|
|
6
|
-
"nonInteractivePermissions": "
|
|
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
|
-
|
|
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
|
|
401
|
-
if (
|
|
402
|
-
|
|
514
|
+
const created = await this.runSessionCreate(createSpec.command, createSpec.args, input.cwd);
|
|
515
|
+
if (created.code === 0) {
|
|
516
|
+
return {};
|
|
403
517
|
}
|
|
404
|
-
|
|
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 ?? "
|
|
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(
|
|
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
|
|
522
|
-
nonInteractivePermissions: process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS
|
|
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
|
}
|