weacpx 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -39
- package/config.example.json +4 -1
- package/dist/bridge/bridge-main.js +264 -30
- package/dist/cli.js +1767 -718
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Weacpx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
连接微信与 acpx 协议,让 Claude Code / Codex 成为你口袋里的 24/7 伙伴。
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/weacpx)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://zread.ai/gadzan/weacpx)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## Why Weacpx?
|
|
13
|
+
|
|
14
|
+
在 Agent-First 的开发模式下,编码任务必须依托顶级 Agents,🙅♀️不要通过 openclaw 去开发,现在有一个更好的方案。Weacpx 通过微信提供一个轻量化的远程入口,随时随地通过手机驱动你的顶级 Agents。
|
|
15
|
+
|
|
16
|
+
Weacpx 的核心价值主张很简单:
|
|
17
|
+
|
|
18
|
+
**随时随地访问** — 只要你有微信,就能控制你的 Agent。无需 VPN、Web 界面或复杂的云服务配置。
|
|
19
|
+
|
|
20
|
+
**统一的会话管理** — 通过 acpx 协议,weacpx 让你在微信里管理多个 Agent 会话(Codex、Claude Code 等),就像在本地终端一样。创建、切换、查询状态,全部通过简单的斜杠命令完成,这是其它简单基于 ACP 实现的远控 agent 所不具备的。
|
|
21
|
+
|
|
22
|
+
**轻量守护进程** — weacpx 作为后台守护进程运行,资源占用极低。不用启动一个臃肿 openclaw,不用担心在工作机器上使用会占用资源。启动、停止、查看状态都通过简单的 CLI 命令完成。
|
|
23
|
+
|
|
24
|
+
**权限可控** — 可以即时通过微信修改 agent 的权限,无论是 YOLO 还是只读。
|
|
4
25
|
|
|
5
26
|
## 安装前准备
|
|
6
27
|
|
|
7
28
|
开始前,至少需要:
|
|
8
29
|
|
|
9
30
|
- Node.js 22+ 或 Bun
|
|
10
|
-
- 一个可用的微信登录环境
|
|
11
31
|
- Claude Code 或 Codex
|
|
32
|
+
- 装了微信的手机
|
|
12
33
|
|
|
13
34
|
> `weacpx` 基于 `weixin-agent-sdk` 与 `acpx` 实现。
|
|
14
35
|
> 正常情况下,不需要再额外全局安装 `acpx`。
|
|
@@ -252,12 +273,6 @@ bun run dev
|
|
|
252
273
|
- `~/.weacpx/runtime/stdout.log`
|
|
253
274
|
- `~/.weacpx/runtime/stderr.log`
|
|
254
275
|
|
|
255
|
-
常用环境变量:
|
|
256
|
-
|
|
257
|
-
- `WEACPX_CONFIG`
|
|
258
|
-
- `WEACPX_STATE`
|
|
259
|
-
- `WEACPX_WEIXIN_SDK`
|
|
260
|
-
|
|
261
276
|
### Transport 权限配置
|
|
262
277
|
|
|
263
278
|
`config.json` 中的 `transport` 支持以下权限字段:
|
|
@@ -304,36 +319,6 @@ bun run dev
|
|
|
304
319
|
|
|
305
320
|
## 注意事项
|
|
306
321
|
|
|
307
|
-
### `dry-run`
|
|
308
|
-
|
|
309
|
-
`dry-run` 会复用同一套 router、session service、transport,只是把微信消息换成终端输入,适合本地排查。
|
|
310
|
-
|
|
311
|
-
示例:
|
|
312
|
-
|
|
313
|
-
```bash
|
|
314
|
-
bun run dry-run --chat-key wx:test -- \
|
|
315
|
-
"/agent add codex" \
|
|
316
|
-
"/ws new backend -d /absolute/path/to/backend" \
|
|
317
|
-
"/ss new demo -a codex --ws backend" \
|
|
318
|
-
"/status"
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### 如果 `/ss new` 失败
|
|
322
|
-
|
|
323
|
-
当前最常见的问题仍然是底层 `acpx` named session 的运行时恢复,不一定是 `weacpx` 本身的逻辑问题。
|
|
324
|
-
|
|
325
|
-
可以先在本地创建一个 named session,再在微信里 attach:
|
|
326
|
-
|
|
327
|
-
```bash
|
|
328
|
-
./node_modules/.bin/acpx --verbose --cwd /absolute/workspace/path codex sessions new --name existing-demo
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
然后在微信里:
|
|
332
|
-
|
|
333
|
-
```text
|
|
334
|
-
/ss attach demo -a codex --ws backend --name existing-demo
|
|
335
|
-
```
|
|
336
|
-
|
|
337
322
|
### Adapter mode 参考
|
|
338
323
|
|
|
339
324
|
`acpx set-mode` / 计划中的 `/mode <id>` 本质上都是给底层 ACP session 发送 `session/set_mode`。
|
|
@@ -354,6 +339,22 @@ bun run dry-run --chat-key wx:test -- \
|
|
|
354
339
|
- 对其他 adapter,不要在 `weacpx` 里写死候选值;最好把 `/mode <id>` 设计成透传,由 adapter 自己决定是否接受。
|
|
355
340
|
- 如果某个 adapter 后续补充了官方 mode 文档,再把它们补进这里。
|
|
356
341
|
|
|
342
|
+
### 如果 `/ss new` 失败
|
|
343
|
+
|
|
344
|
+
当前最常见的问题仍然是底层 `acpx` named session 的运行时恢复,不一定是 `weacpx` 本身的逻辑问题。
|
|
345
|
+
|
|
346
|
+
可以先在本地创建一个 named session,再在微信里 attach:
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
./node_modules/.bin/acpx --verbose --cwd /absolute/workspace/path codex sessions new --name existing-demo
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
然后在微信里:
|
|
353
|
+
|
|
354
|
+
```text
|
|
355
|
+
/ss attach demo -a codex --ws backend --name existing-demo
|
|
356
|
+
```
|
|
357
|
+
|
|
357
358
|
## 更多文档
|
|
358
359
|
|
|
359
360
|
- 配置参考:[docs/config-reference.md](./docs/config-reference.md)
|
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"
|
|
@@ -46,6 +46,16 @@ var __export = (target, all) => {
|
|
|
46
46
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
47
47
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
48
48
|
|
|
49
|
+
// src/transport/acpx-bridge/acpx-bridge-protocol.ts
|
|
50
|
+
function encodeBridgeRequest(request) {
|
|
51
|
+
return `${JSON.stringify(request)}
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
function encodeBridgePromptSegmentEvent(event) {
|
|
55
|
+
return `${JSON.stringify(event)}
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
// src/transport/prompt-output.ts
|
|
50
60
|
function getPromptText(result) {
|
|
51
61
|
const stdoutOutput = extractPromptOutput(result.stdout);
|
|
@@ -193,21 +203,102 @@ var init_spawn_command = __esm(() => {
|
|
|
193
203
|
SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
|
|
194
204
|
});
|
|
195
205
|
|
|
206
|
+
// src/transport/streaming-prompt.ts
|
|
207
|
+
function createStreamingPromptState() {
|
|
208
|
+
return {
|
|
209
|
+
buffer: "",
|
|
210
|
+
segments: [],
|
|
211
|
+
hasAgentMessage: false,
|
|
212
|
+
pendingLine: "",
|
|
213
|
+
finalize() {
|
|
214
|
+
if (this.pendingLine.trim().length > 0) {
|
|
215
|
+
parseStreamingChunks(this, this.pendingLine);
|
|
216
|
+
}
|
|
217
|
+
const remaining = this.buffer.trim();
|
|
218
|
+
this.buffer = "";
|
|
219
|
+
this.pendingLine = "";
|
|
220
|
+
return remaining;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function parseStreamingDataChunk(state, chunk) {
|
|
225
|
+
state.pendingLine += chunk;
|
|
226
|
+
let boundary;
|
|
227
|
+
while ((boundary = state.pendingLine.indexOf(`
|
|
228
|
+
`)) !== -1) {
|
|
229
|
+
const line = state.pendingLine.slice(0, boundary);
|
|
230
|
+
state.pendingLine = state.pendingLine.slice(boundary + 1);
|
|
231
|
+
parseStreamingChunks(state, line);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function parseStreamingChunks(state, line) {
|
|
235
|
+
const trimmed = line.trim();
|
|
236
|
+
if (trimmed.length === 0)
|
|
237
|
+
return;
|
|
238
|
+
let event;
|
|
239
|
+
try {
|
|
240
|
+
event = JSON.parse(trimmed);
|
|
241
|
+
} catch {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
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";
|
|
245
|
+
if (!isMessageChunk)
|
|
246
|
+
return;
|
|
247
|
+
state.hasAgentMessage = true;
|
|
248
|
+
const chunk = event.params.update.content.text ?? "";
|
|
249
|
+
if (chunk.length === 0)
|
|
250
|
+
return;
|
|
251
|
+
state.buffer += chunk;
|
|
252
|
+
let boundary;
|
|
253
|
+
while ((boundary = state.buffer.indexOf(`
|
|
254
|
+
|
|
255
|
+
`)) !== -1) {
|
|
256
|
+
const segment = state.buffer.slice(0, boundary).trim();
|
|
257
|
+
state.buffer = state.buffer.slice(boundary + 2);
|
|
258
|
+
if (segment.length > 0) {
|
|
259
|
+
state.segments.push(segment);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
196
264
|
// src/bridge/bridge-main.ts
|
|
197
265
|
import { createInterface } from "node:readline";
|
|
198
266
|
|
|
267
|
+
// src/bridge/bridge-env.ts
|
|
268
|
+
function normalizeBridgePermissionMode(value) {
|
|
269
|
+
return value === "approve-reads" || value === "deny-all" || value === "approve-all" ? value : "approve-all";
|
|
270
|
+
}
|
|
271
|
+
function normalizeBridgeNonInteractivePermissions(value) {
|
|
272
|
+
return value === "deny" || value === "fail" ? value : "deny";
|
|
273
|
+
}
|
|
274
|
+
|
|
199
275
|
// src/bridge/bridge-server.ts
|
|
200
276
|
init_prompt_output();
|
|
201
277
|
|
|
278
|
+
class BridgeInvalidRequestError extends Error {
|
|
279
|
+
}
|
|
280
|
+
var BRIDGE_METHODS = new Set([
|
|
281
|
+
"ping",
|
|
282
|
+
"shutdown",
|
|
283
|
+
"updatePermissionPolicy",
|
|
284
|
+
"hasSession",
|
|
285
|
+
"ensureSession",
|
|
286
|
+
"prompt",
|
|
287
|
+
"setMode",
|
|
288
|
+
"cancel"
|
|
289
|
+
]);
|
|
290
|
+
|
|
202
291
|
class BridgeServer {
|
|
203
292
|
runtime;
|
|
204
293
|
constructor(runtime) {
|
|
205
294
|
this.runtime = runtime;
|
|
206
295
|
}
|
|
207
|
-
async handleLine(line) {
|
|
208
|
-
|
|
296
|
+
async handleLine(line, writeLine) {
|
|
297
|
+
let requestId = extractRequestId(line);
|
|
209
298
|
try {
|
|
210
|
-
const
|
|
299
|
+
const request = parseBridgeRequest(line);
|
|
300
|
+
requestId = request.id;
|
|
301
|
+
const result = await this.dispatch(request.id, request.method, request.params, writeLine);
|
|
211
302
|
return `${JSON.stringify({
|
|
212
303
|
id: request.id,
|
|
213
304
|
ok: true,
|
|
@@ -217,10 +308,10 @@ class BridgeServer {
|
|
|
217
308
|
} catch (error) {
|
|
218
309
|
const message = error instanceof Error ? error.message : String(error);
|
|
219
310
|
return `${JSON.stringify({
|
|
220
|
-
id:
|
|
311
|
+
id: requestId,
|
|
221
312
|
ok: false,
|
|
222
313
|
error: {
|
|
223
|
-
code: "BRIDGE_INTERNAL_ERROR",
|
|
314
|
+
code: error instanceof BridgeInvalidRequestError ? "BRIDGE_INVALID_REQUEST" : "BRIDGE_INTERNAL_ERROR",
|
|
224
315
|
message,
|
|
225
316
|
...error instanceof PromptCommandError ? {
|
|
226
317
|
details: {
|
|
@@ -234,54 +325,132 @@ class BridgeServer {
|
|
|
234
325
|
`;
|
|
235
326
|
}
|
|
236
327
|
}
|
|
237
|
-
async dispatch(method, params) {
|
|
328
|
+
async dispatch(requestId, method, params, writeLine) {
|
|
238
329
|
switch (method) {
|
|
239
330
|
case "ping":
|
|
240
331
|
return {};
|
|
241
332
|
case "shutdown":
|
|
242
333
|
return await this.runtime.shutdown();
|
|
334
|
+
case "updatePermissionPolicy":
|
|
335
|
+
return await this.runtime.updatePermissionPolicy({
|
|
336
|
+
permissionMode: requirePermissionMode(params, "permissionMode"),
|
|
337
|
+
nonInteractivePermissions: requireNonInteractivePermissions(params, "nonInteractivePermissions")
|
|
338
|
+
});
|
|
243
339
|
case "hasSession":
|
|
244
340
|
return await this.runtime.hasSession({
|
|
245
|
-
agent:
|
|
341
|
+
agent: requireString(params, "agent"),
|
|
246
342
|
agentCommand: asOptionalString(params.agentCommand),
|
|
247
|
-
cwd:
|
|
248
|
-
name:
|
|
343
|
+
cwd: requireString(params, "cwd"),
|
|
344
|
+
name: requireString(params, "name")
|
|
249
345
|
});
|
|
250
346
|
case "ensureSession":
|
|
251
347
|
return await this.runtime.ensureSession({
|
|
252
|
-
agent:
|
|
348
|
+
agent: requireString(params, "agent"),
|
|
253
349
|
agentCommand: asOptionalString(params.agentCommand),
|
|
254
|
-
cwd:
|
|
255
|
-
name:
|
|
350
|
+
cwd: requireString(params, "cwd"),
|
|
351
|
+
name: requireString(params, "name")
|
|
256
352
|
});
|
|
257
353
|
case "prompt":
|
|
258
354
|
return await this.runtime.prompt({
|
|
259
|
-
agent:
|
|
355
|
+
agent: requireString(params, "agent"),
|
|
260
356
|
agentCommand: asOptionalString(params.agentCommand),
|
|
261
|
-
cwd:
|
|
262
|
-
name:
|
|
263
|
-
text:
|
|
357
|
+
cwd: requireString(params, "cwd"),
|
|
358
|
+
name: requireString(params, "name"),
|
|
359
|
+
text: requireString(params, "text")
|
|
360
|
+
}, (event) => {
|
|
361
|
+
if (event.type === "prompt.segment") {
|
|
362
|
+
writeLine?.(encodeBridgePromptSegmentEvent({
|
|
363
|
+
id: requestId,
|
|
364
|
+
event: "prompt.segment",
|
|
365
|
+
text: event.text
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
264
368
|
});
|
|
265
369
|
case "setMode":
|
|
266
370
|
return await this.runtime.setMode({
|
|
267
|
-
agent:
|
|
371
|
+
agent: requireString(params, "agent"),
|
|
268
372
|
agentCommand: asOptionalString(params.agentCommand),
|
|
269
|
-
cwd:
|
|
270
|
-
name:
|
|
271
|
-
modeId:
|
|
373
|
+
cwd: requireString(params, "cwd"),
|
|
374
|
+
name: requireString(params, "name"),
|
|
375
|
+
modeId: requireString(params, "modeId")
|
|
272
376
|
});
|
|
273
377
|
case "cancel":
|
|
274
378
|
return await this.runtime.cancel({
|
|
275
|
-
agent:
|
|
379
|
+
agent: requireString(params, "agent"),
|
|
276
380
|
agentCommand: asOptionalString(params.agentCommand),
|
|
277
|
-
cwd:
|
|
278
|
-
name:
|
|
381
|
+
cwd: requireString(params, "cwd"),
|
|
382
|
+
name: requireString(params, "name")
|
|
279
383
|
});
|
|
280
384
|
default:
|
|
281
385
|
throw new Error(`unsupported bridge method: ${method}`);
|
|
282
386
|
}
|
|
283
387
|
}
|
|
284
388
|
}
|
|
389
|
+
function extractRequestId(line) {
|
|
390
|
+
try {
|
|
391
|
+
const raw = JSON.parse(line);
|
|
392
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
393
|
+
return "unknown";
|
|
394
|
+
}
|
|
395
|
+
const id = raw.id;
|
|
396
|
+
return typeof id === "string" && id.length > 0 ? id : "unknown";
|
|
397
|
+
} catch {
|
|
398
|
+
return "unknown";
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function parseBridgeRequest(line) {
|
|
402
|
+
let raw;
|
|
403
|
+
try {
|
|
404
|
+
raw = JSON.parse(line);
|
|
405
|
+
} catch {
|
|
406
|
+
throw new BridgeInvalidRequestError("request must be valid JSON");
|
|
407
|
+
}
|
|
408
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
409
|
+
throw new BridgeInvalidRequestError("request must be a JSON object");
|
|
410
|
+
}
|
|
411
|
+
const request = raw;
|
|
412
|
+
const id = request.id;
|
|
413
|
+
const method = request.method;
|
|
414
|
+
const params = request.params;
|
|
415
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
416
|
+
throw new BridgeInvalidRequestError("id must be a non-empty string");
|
|
417
|
+
}
|
|
418
|
+
if (typeof method !== "string" || method.length === 0) {
|
|
419
|
+
throw new BridgeInvalidRequestError("method must be a non-empty string");
|
|
420
|
+
}
|
|
421
|
+
if (!BRIDGE_METHODS.has(method)) {
|
|
422
|
+
throw new BridgeInvalidRequestError(`unsupported bridge method: ${method}`);
|
|
423
|
+
}
|
|
424
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
425
|
+
throw new BridgeInvalidRequestError("params must be an object");
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
id,
|
|
429
|
+
method,
|
|
430
|
+
params
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function requireString(params, key) {
|
|
434
|
+
const value = params[key];
|
|
435
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
436
|
+
throw new BridgeInvalidRequestError(`${key} must be a non-empty string`);
|
|
437
|
+
}
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
function requirePermissionMode(params, key) {
|
|
441
|
+
const value = params[key];
|
|
442
|
+
if (value === "approve-all" || value === "approve-reads" || value === "deny-all") {
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
throw new BridgeInvalidRequestError(`${key} must be approve-all, approve-reads, or deny-all`);
|
|
446
|
+
}
|
|
447
|
+
function requireNonInteractivePermissions(params, key) {
|
|
448
|
+
const value = params[key];
|
|
449
|
+
if (value === "deny" || value === "fail") {
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
throw new BridgeInvalidRequestError(`${key} must be deny or fail`);
|
|
453
|
+
}
|
|
285
454
|
function asOptionalString(value) {
|
|
286
455
|
if (typeof value !== "string" || value.length === 0) {
|
|
287
456
|
return;
|
|
@@ -300,11 +469,18 @@ class BridgeRuntime {
|
|
|
300
469
|
run;
|
|
301
470
|
runSessionCreate;
|
|
302
471
|
options;
|
|
303
|
-
|
|
472
|
+
runPromptCommand;
|
|
473
|
+
constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner, options = {}, runPromptCommand = defaultPromptRunner) {
|
|
304
474
|
this.command = command;
|
|
305
475
|
this.run = run;
|
|
306
476
|
this.runSessionCreate = runSessionCreate;
|
|
307
477
|
this.options = options;
|
|
478
|
+
this.runPromptCommand = runPromptCommand;
|
|
479
|
+
}
|
|
480
|
+
async updatePermissionPolicy(policy) {
|
|
481
|
+
this.options.permissionMode = policy.permissionMode;
|
|
482
|
+
this.options.nonInteractivePermissions = policy.nonInteractivePermissions;
|
|
483
|
+
return {};
|
|
308
484
|
}
|
|
309
485
|
async hasSession(input) {
|
|
310
486
|
const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
|
|
@@ -338,14 +514,14 @@ class BridgeRuntime {
|
|
|
338
514
|
}
|
|
339
515
|
return {};
|
|
340
516
|
}
|
|
341
|
-
async prompt(input) {
|
|
517
|
+
async prompt(input, onEvent) {
|
|
342
518
|
const spawnSpec = resolveSpawnCommand(this.command, this.buildPromptArgs(input, [
|
|
343
519
|
"prompt",
|
|
344
520
|
"-s",
|
|
345
521
|
input.name,
|
|
346
522
|
input.text
|
|
347
523
|
]));
|
|
348
|
-
const result = await this.run(spawnSpec.command, spawnSpec.args);
|
|
524
|
+
const result = onEvent ? await this.runPromptCommand(spawnSpec.command, spawnSpec.args, onEvent) : await this.run(spawnSpec.command, spawnSpec.args);
|
|
349
525
|
return { text: getPromptText(result) };
|
|
350
526
|
}
|
|
351
527
|
async setMode(input) {
|
|
@@ -408,7 +584,7 @@ class BridgeRuntime {
|
|
|
408
584
|
}
|
|
409
585
|
buildPermissionArgs() {
|
|
410
586
|
const permissionMode = this.options.permissionMode ?? "approve-all";
|
|
411
|
-
const nonInteractivePermissions = this.options.nonInteractivePermissions ?? "
|
|
587
|
+
const nonInteractivePermissions = this.options.nonInteractivePermissions ?? "deny";
|
|
412
588
|
const modeFlag = permissionMode === "approve-reads" ? "--approve-reads" : permissionMode === "deny-all" ? "--deny-all" : "--approve-all";
|
|
413
589
|
return [modeFlag, "--non-interactive-permissions", nonInteractivePermissions];
|
|
414
590
|
}
|
|
@@ -430,6 +606,62 @@ async function defaultRunner(command, args) {
|
|
|
430
606
|
});
|
|
431
607
|
});
|
|
432
608
|
}
|
|
609
|
+
async function runStreamingPrompt(command, args, onEvent, options = {}) {
|
|
610
|
+
const spawnPrompt = options.spawnPrompt ?? ((spawnCommand, spawnArgs) => spawn(spawnCommand, spawnArgs, { stdio: ["ignore", "pipe", "pipe"] }));
|
|
611
|
+
const setIntervalFn = options.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
|
|
612
|
+
const clearIntervalFn = options.clearIntervalFn ?? ((timer) => clearInterval(timer));
|
|
613
|
+
const maxSegmentWaitMs = options.maxSegmentWaitMs ?? 30000;
|
|
614
|
+
const flushCheckIntervalMs = options.flushCheckIntervalMs ?? 5000;
|
|
615
|
+
const now = options.now ?? (() => Date.now());
|
|
616
|
+
return await new Promise((resolve, reject) => {
|
|
617
|
+
const child = spawnPrompt(command, args);
|
|
618
|
+
let stdout = "";
|
|
619
|
+
let stderr = "";
|
|
620
|
+
const state = createStreamingPromptState();
|
|
621
|
+
let lastReplyAt = now();
|
|
622
|
+
const flushBuffer = () => {
|
|
623
|
+
const remaining = state.buffer.trim();
|
|
624
|
+
if (remaining.length > 0) {
|
|
625
|
+
state.buffer = "";
|
|
626
|
+
onEvent?.({ type: "prompt.segment", text: remaining });
|
|
627
|
+
lastReplyAt = now();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
const timer = setIntervalFn(() => {
|
|
631
|
+
if (state.buffer.trim().length > 0 && now() - lastReplyAt >= maxSegmentWaitMs) {
|
|
632
|
+
flushBuffer();
|
|
633
|
+
}
|
|
634
|
+
}, flushCheckIntervalMs);
|
|
635
|
+
child.stdout.setEncoding("utf8");
|
|
636
|
+
child.stdout.on("data", (chunk) => {
|
|
637
|
+
const text = String(chunk);
|
|
638
|
+
stdout += text;
|
|
639
|
+
parseStreamingDataChunk(state, text);
|
|
640
|
+
for (const segment of state.segments.splice(0)) {
|
|
641
|
+
onEvent?.({ type: "prompt.segment", text: segment });
|
|
642
|
+
lastReplyAt = now();
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
child.stderr.on("data", (chunk) => {
|
|
646
|
+
stderr += String(chunk);
|
|
647
|
+
});
|
|
648
|
+
child.on("error", (error) => {
|
|
649
|
+
clearIntervalFn(timer);
|
|
650
|
+
reject(error);
|
|
651
|
+
});
|
|
652
|
+
child.on("close", (code) => {
|
|
653
|
+
clearIntervalFn(timer);
|
|
654
|
+
const remaining = state.finalize();
|
|
655
|
+
if (remaining.length > 0) {
|
|
656
|
+
onEvent?.({ type: "prompt.segment", text: remaining });
|
|
657
|
+
}
|
|
658
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
async function defaultPromptRunner(command, args, onEvent) {
|
|
663
|
+
return await runStreamingPrompt(command, args, onEvent);
|
|
664
|
+
}
|
|
433
665
|
async function shellSessionCreateRunner(command, args, cwd) {
|
|
434
666
|
const helperPath = fileURLToPath(new URL("../../scripts/acpx-session-new-helper.sh", import.meta.url));
|
|
435
667
|
return await new Promise((resolve, reject) => {
|
|
@@ -453,14 +685,16 @@ async function shellSessionCreateRunner(command, args, cwd) {
|
|
|
453
685
|
|
|
454
686
|
// src/bridge/bridge-main.ts
|
|
455
687
|
var server = new BridgeServer(new BridgeRuntime(process.env.WEACPX_BRIDGE_ACPX_COMMAND ?? "acpx", undefined, undefined, {
|
|
456
|
-
permissionMode: process.env.WEACPX_BRIDGE_PERMISSION_MODE
|
|
457
|
-
nonInteractivePermissions: process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS
|
|
688
|
+
permissionMode: normalizeBridgePermissionMode(process.env.WEACPX_BRIDGE_PERMISSION_MODE),
|
|
689
|
+
nonInteractivePermissions: normalizeBridgeNonInteractivePermissions(process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS)
|
|
458
690
|
}));
|
|
459
691
|
var input = createInterface({
|
|
460
692
|
input: process.stdin,
|
|
461
693
|
crlfDelay: Infinity
|
|
462
694
|
});
|
|
463
695
|
for await (const line of input) {
|
|
464
|
-
const response = await server.handleLine(line)
|
|
696
|
+
const response = await server.handleLine(line, (chunk) => {
|
|
697
|
+
process.stdout.write(chunk);
|
|
698
|
+
});
|
|
465
699
|
process.stdout.write(response);
|
|
466
700
|
}
|