@unlinearity/cli-wechat-bridge 1.0.7 → 1.1.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/LICENSE.txt +661 -21
- package/README.md +49 -2
- package/dist/bridge/bridge-adapters.claude.js +194 -5
- package/dist/bridge/bridge-adapters.codex.js +84 -37
- package/dist/bridge/bridge-adapters.core.js +58 -10
- package/dist/bridge/bridge-adapters.opencode.js +20 -12
- package/dist/bridge/bridge-adapters.shared.js +101 -1
- package/dist/bridge/bridge-adapters.shell.js +1 -1
- package/dist/bridge/bridge-process-reaper.js +7 -2
- package/dist/bridge/bridge-utils.js +40 -14
- package/dist/bridge/claude-hooks.js +14 -16
- package/dist/bridge/wechat-bridge.js +59 -10
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion.js +1 -0
- package/dist/daemon/daemon-link.js +6 -0
- package/dist/daemon/emoji-bindings.js +111 -0
- package/dist/daemon/wechat-daemon.js +197 -61
- package/dist/i18n/index.js +32 -0
- package/dist/i18n/messages-en.js +48 -0
- package/dist/i18n/messages-zh.js +48 -0
- package/dist/runtime/legacy-adapter-runtime.js +3 -0
- package/dist/wechat/channel-config.js +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<a href="https://www.npmjs.com/package/cli-wechat-bridge"><img alt="npm downloads" src="https://img.shields.io/npm/dm/cli-wechat-bridge?label=Downloads&style=for-the-badge&logo=npm&color=16a34a&labelColor=1c1917"></a>
|
|
11
11
|
<a href="https://www.npmjs.com/package/@unlinearity/cli-wechat-bridge"><img alt="scoped npm downloads" src="https://img.shields.io/npm/dm/@unlinearity/cli-wechat-bridge?label=Scoped%20downloads&style=for-the-badge&logo=npm&color=15803d&labelColor=1c1917"></a>
|
|
12
12
|
<a href="https://www.npmjs.com/package/cli-wechat-bridge">
|
|
13
|
-
<img alt="License" src="https://img.shields.io/
|
|
13
|
+
<img alt="License" src="https://img.shields.io/badge/License-AGPL--3.0-7c3aed?style=for-the-badge&labelColor=1c1917">
|
|
14
14
|
</a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
@@ -305,9 +305,44 @@ wechat-claude-start --model sonnet --dangerously-skip-permissions
|
|
|
305
305
|
| `/new` 或 `/new-session` | OpenCode 模式下新建 session |
|
|
306
306
|
| `/confirm` / `/deny` | 处理 CLI 权限请求;需要一次性 code 的请求会在消息中提示具体确认格式 |
|
|
307
307
|
| `/daemon-stop` | daemon 模式下停止常驻进程 |
|
|
308
|
+
| `/bindings` | 查看当前所有表情绑定 |
|
|
309
|
+
| `/bind [表情] /命令` | 绑定表情到指定命令 |
|
|
310
|
+
| `/unbind [表情]` | 解除指定表情的绑定 |
|
|
308
311
|
|
|
309
312
|
说明:微信侧 `/resume` 目前暂时保持禁用;需要切换 Codex / Claude / OpenCode 会话时,优先在本地 companion 中使用原生 `/resume`、`/new` 或对应 CLI 命令,微信会跟随本地活动会话。
|
|
310
313
|
|
|
314
|
+
### 表情绑定
|
|
315
|
+
|
|
316
|
+
Daemon 模式支持将微信表情映射为命令,在微信中发送表情即可快速触发操作。(不过注意,第一次启动相关cli的时候,最好不要带消息,等启动完成再“表情+文本”快速给指定 cli 发送消息。
|
|
317
|
+
|
|
318
|
+

|
|
319
|
+
|
|
320
|
+
**默认绑定:**
|
|
321
|
+
|
|
322
|
+
| 表情 | 命令 | 说明 |
|
|
323
|
+
| --- | --- | --- |
|
|
324
|
+
| `[OK]` | `/confirm` | 批准权限请求 |
|
|
325
|
+
| `[闭嘴]` | `/stop` | 中断当前任务 |
|
|
326
|
+
| `[拥抱]` | `/claude` | 切换到 Claude Code |
|
|
327
|
+
| `[强]` | `/codex` | 切换到 Codex |
|
|
328
|
+
| `[胜利]` | `/opencode` | 切换到 OpenCode |
|
|
329
|
+
| `[再见]` | `/daemon-stop` | 停止 daemon |
|
|
330
|
+
|
|
331
|
+
**管理命令:**
|
|
332
|
+
|
|
333
|
+
```
|
|
334
|
+
/bindings 查看当前所有绑定
|
|
335
|
+
/bind [表情] /命令 绑定表情到命令,如 /bind [微笑] /status
|
|
336
|
+
/unbind [表情] 解除绑定,如 /unbind [微笑]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**触发规则:**
|
|
340
|
+
|
|
341
|
+
- 表情必须出现在消息开头才会触发(如 `[OK]` 触发,`你好[OK]` 不触发);
|
|
342
|
+
- 如果表情后面还有文本(如 `[拥抱]帮我写个脚本`),会先执行命令再将剩余文本作为消息转发;
|
|
343
|
+
- 大小写不敏感(`[OK]` 和 `[ok]` 等价);
|
|
344
|
+
- 修改后立即生效并持久化到 `~/.cli-bridge/emoji-bindings.json`,重启 daemon 后保留。
|
|
345
|
+
|
|
311
346
|
## 工作区模型
|
|
312
347
|
|
|
313
348
|
本项目采用“当前目录即当前工作区”的模型:
|
|
@@ -390,7 +425,19 @@ npm install -g cli-wechat-bridge@latest
|
|
|
390
425
|
|
|
391
426
|
## License
|
|
392
427
|
|
|
393
|
-
|
|
428
|
+
本项目采用**双协议**授权:
|
|
429
|
+
|
|
430
|
+
**开源协议:[AGPL-3.0](LICENSE.txt)**
|
|
431
|
+
|
|
432
|
+
- 个人使用、学习、研究:完全免费
|
|
433
|
+
- 修改和衍生作品必须以相同协议(AGPL-3.0)开源
|
|
434
|
+
- 通过网络提供基于本项目的服务,也必须公开完整源代码
|
|
435
|
+
|
|
436
|
+
**商业授权**
|
|
437
|
+
|
|
438
|
+
如果你希望在闭源商业产品中使用本项目(不公开你的源代码),需要获得商业许可。请联系作者获取商业授权方案:
|
|
439
|
+
|
|
440
|
+
- GitHub: [@UNLINEARITY](https://github.com/UNLINEARITY)
|
|
394
441
|
|
|
395
442
|
<p align="center">
|
|
396
443
|
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=UNLINEARITY/CLI-WeChat-Bridge&type=timeline&legend=top-left">
|
|
@@ -3,9 +3,10 @@ import net from "node:net";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { buildLocalCompanionToken } from "../companion/local-companion-link.js";
|
|
6
|
+
import { t } from "../i18n/index.js";
|
|
6
7
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
7
8
|
import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, getClaudePermissionAutoResponse, getClaudeWechatOutboundAttachmentDenyMessage, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
8
|
-
import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
9
|
+
import { detectCliApproval, isThinkingForwardEnabled, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
9
10
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
10
11
|
import * as shared from "./bridge-adapters.shared.js";
|
|
11
12
|
const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, delay, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
|
|
@@ -90,6 +91,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
90
91
|
localTerminalInputListener = null;
|
|
91
92
|
resizeListener = null;
|
|
92
93
|
settingsFilePath = null;
|
|
94
|
+
hookErrorLogPath = null;
|
|
95
|
+
hookReceivedCount = 0;
|
|
96
|
+
hookHealthCheckTimer = null;
|
|
93
97
|
pendingHookApprovals = new Map();
|
|
94
98
|
recoveringInvalidResume = false;
|
|
95
99
|
workingNoticeTimer = null;
|
|
@@ -98,6 +102,10 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
98
102
|
lastCompactCompletionAtMs = 0;
|
|
99
103
|
startupOutputBuffer = "";
|
|
100
104
|
hasAutoConfirmedWorkspaceTrustPrompt = false;
|
|
105
|
+
lastAutoApprovedPayload = null;
|
|
106
|
+
transcriptPollTimer = null;
|
|
107
|
+
transcriptTailOffset = 0;
|
|
108
|
+
polledTranscriptSessionId = null;
|
|
101
109
|
constructor(options) {
|
|
102
110
|
super(options);
|
|
103
111
|
const shouldRestoreInitialSession = options.sessionStartMode !== "new";
|
|
@@ -163,7 +171,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
163
171
|
throw new Error("claude is still working. Wait for the current reply or use /stop.");
|
|
164
172
|
}
|
|
165
173
|
if (this.pendingApproval) {
|
|
166
|
-
throw new Error("A Claude approval request is pending. Reply with /confirm
|
|
174
|
+
throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
|
|
167
175
|
}
|
|
168
176
|
const normalizedText = normalizeOutput(text).trim();
|
|
169
177
|
this.pendingInjectedInputs.push({
|
|
@@ -178,6 +186,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
178
186
|
this.pendingCliApprovalHints = null;
|
|
179
187
|
this.clearWechatWorkingNotice(true);
|
|
180
188
|
this.setStatus("busy");
|
|
189
|
+
this.startTranscriptThinkingWatch();
|
|
181
190
|
this.writeToPty(this.buildRemoteInputPayload(text));
|
|
182
191
|
await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
|
|
183
192
|
this.writeToPty("\r");
|
|
@@ -200,6 +209,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
200
209
|
this.pendingCliApprovalHints = null;
|
|
201
210
|
this.flushPendingClaudeHookApprovals();
|
|
202
211
|
this.writeToPty("\u0003");
|
|
212
|
+
this.scheduleTaskComplete(shared.INTERRUPT_SETTLE_DELAY_MS);
|
|
203
213
|
return true;
|
|
204
214
|
}
|
|
205
215
|
async reset() {
|
|
@@ -247,11 +257,34 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
247
257
|
this.writeToPty(input);
|
|
248
258
|
return true;
|
|
249
259
|
}
|
|
260
|
+
async resolveAllApprovals(action) {
|
|
261
|
+
let count = 0;
|
|
262
|
+
for (const requestId of Array.from(this.pendingHookApprovals.keys())) {
|
|
263
|
+
const pending = this.pendingHookApprovals.get(requestId);
|
|
264
|
+
if (pending) {
|
|
265
|
+
this.pendingHookApprovals.delete(requestId);
|
|
266
|
+
this.respondToClaudeHook(pending.socket, requestId, buildClaudePermissionDecisionHookOutput(action));
|
|
267
|
+
count++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (count > 0) {
|
|
271
|
+
this.clearWechatWorkingNotice();
|
|
272
|
+
this.pendingCliApprovalHints = null;
|
|
273
|
+
this.pendingApproval = null;
|
|
274
|
+
this.state.pendingApproval = null;
|
|
275
|
+
this.state.pendingApprovalOrigin = undefined;
|
|
276
|
+
this.setStatus("busy");
|
|
277
|
+
return count;
|
|
278
|
+
}
|
|
279
|
+
const ok = await this.resolveApproval(action);
|
|
280
|
+
return ok ? 1 : 0;
|
|
281
|
+
}
|
|
250
282
|
async dispose() {
|
|
251
283
|
this.detachLocalTerminal();
|
|
252
284
|
this.clearWechatWorkingNotice(true);
|
|
253
285
|
this.pendingCliApprovalHints = null;
|
|
254
286
|
this.flushPendingClaudeHookApprovals();
|
|
287
|
+
this.stopTranscriptThinkingWatch();
|
|
255
288
|
await super.dispose();
|
|
256
289
|
await this.stopHookServer();
|
|
257
290
|
}
|
|
@@ -270,6 +303,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
270
303
|
afterStart() {
|
|
271
304
|
this.attachLocalTerminal();
|
|
272
305
|
this.resizePtyToTerminal();
|
|
306
|
+
this.startHookHealthCheck();
|
|
273
307
|
}
|
|
274
308
|
handleData(rawText) {
|
|
275
309
|
this.renderLocalOutput(rawText);
|
|
@@ -446,12 +480,15 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
446
480
|
const hookEntryPath = fs.existsSync(sourceHookEntryPath)
|
|
447
481
|
? sourceHookEntryPath
|
|
448
482
|
: path.join(MODULE_DIR, "claude-hook.js");
|
|
483
|
+
const hookErrorLogPath = path.join(runtimeDir, "hook-error.log");
|
|
484
|
+
this.hookErrorLogPath = hookErrorLogPath;
|
|
449
485
|
fs.writeFileSync(hookScriptPath, buildClaudeHookScript({
|
|
450
486
|
platform: process.platform,
|
|
451
487
|
runtimeExecPath: process.execPath,
|
|
452
488
|
hookEntryPath,
|
|
453
489
|
hookPort: this.hookPort,
|
|
454
490
|
hookToken: this.hookToken,
|
|
491
|
+
hookErrorLogPath,
|
|
455
492
|
}), "utf8");
|
|
456
493
|
if (process.platform !== "win32") {
|
|
457
494
|
fs.chmodSync(hookScriptPath, 0o755);
|
|
@@ -500,7 +537,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
500
537
|
return;
|
|
501
538
|
}
|
|
502
539
|
try {
|
|
503
|
-
this.pty.resize(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
|
|
540
|
+
this.pty.resize?.(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
|
|
504
541
|
}
|
|
505
542
|
catch {
|
|
506
543
|
// Best effort resize sync.
|
|
@@ -546,6 +583,34 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
546
583
|
this.workingNoticeSent = false;
|
|
547
584
|
}
|
|
548
585
|
}
|
|
586
|
+
startHookHealthCheck() {
|
|
587
|
+
this.hookReceivedCount = 0;
|
|
588
|
+
this.clearHookHealthCheck();
|
|
589
|
+
this.hookHealthCheckTimer = setTimeout(() => {
|
|
590
|
+
this.hookHealthCheckTimer = null;
|
|
591
|
+
if (this.hookReceivedCount === 0 && !this.shuttingDown) {
|
|
592
|
+
const logHint = this.hookErrorLogPath
|
|
593
|
+
? t("hook.healthCheck.logHint", { logPath: this.hookErrorLogPath })
|
|
594
|
+
: "";
|
|
595
|
+
this.emit({
|
|
596
|
+
type: "stdout",
|
|
597
|
+
text: [
|
|
598
|
+
t("hook.healthCheck.warning"),
|
|
599
|
+
logHint,
|
|
600
|
+
t("hook.healthCheck.fixes"),
|
|
601
|
+
].filter(Boolean).join("\n"),
|
|
602
|
+
timestamp: nowIso(),
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}, 15_000);
|
|
606
|
+
this.hookHealthCheckTimer.unref?.();
|
|
607
|
+
}
|
|
608
|
+
clearHookHealthCheck() {
|
|
609
|
+
if (this.hookHealthCheckTimer) {
|
|
610
|
+
clearTimeout(this.hookHealthCheckTimer);
|
|
611
|
+
this.hookHealthCheckTimer = null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
549
614
|
emitClaudeNotice(text, level = "info") {
|
|
550
615
|
const normalized = normalizeOutput(text).trim();
|
|
551
616
|
if (!normalized) {
|
|
@@ -566,6 +631,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
566
631
|
}
|
|
567
632
|
return `${CLAUDE_BRACKETED_PASTE_START}${normalizedText}${CLAUDE_BRACKETED_PASTE_END}`;
|
|
568
633
|
}
|
|
634
|
+
// Fallback heuristic for older Claude Code versions that lack PostCompact hooks.
|
|
635
|
+
// The structured PostCompact hook event (handled in handleClaudeHookEnvelope) is
|
|
636
|
+
// the reliable signal; this regex match serves as a best-effort fallback.
|
|
569
637
|
shouldTreatClaudeOutputAsCompactCompletion(text) {
|
|
570
638
|
if (this.state.status !== "busy" &&
|
|
571
639
|
this.state.status !== "awaiting_approval" &&
|
|
@@ -664,6 +732,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
664
732
|
this.currentPreview = "(idle)";
|
|
665
733
|
}
|
|
666
734
|
handleClaudeHookEnvelope(params) {
|
|
735
|
+
this.hookReceivedCount++;
|
|
736
|
+
this.clearHookHealthCheck();
|
|
667
737
|
const payload = parseClaudeHookPayload(params.rawPayload);
|
|
668
738
|
if (!payload?.hook_event_name) {
|
|
669
739
|
this.respondToClaudeHook(params.socket, params.requestId);
|
|
@@ -682,8 +752,32 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
682
752
|
this.handleClaudePermissionRequest(params.requestId, payload, params.socket);
|
|
683
753
|
return;
|
|
684
754
|
case "Notification":
|
|
685
|
-
if (payload.notification_type === "permission_prompt"
|
|
686
|
-
this.
|
|
755
|
+
if (payload.notification_type === "permission_prompt") {
|
|
756
|
+
if (this.pendingApproval) {
|
|
757
|
+
this.setStatus("awaiting_approval", "Claude approval is required.");
|
|
758
|
+
}
|
|
759
|
+
else if (this.lastAutoApprovedPayload) {
|
|
760
|
+
// Auto-approval response failed to reach Claude Code (socket closed before delivery).
|
|
761
|
+
// Fall back to forwarding the approval request to WeChat.
|
|
762
|
+
const fallbackPayload = this.lastAutoApprovedPayload;
|
|
763
|
+
this.lastAutoApprovedPayload = null;
|
|
764
|
+
const request = buildClaudePermissionApprovalRequest(fallbackPayload);
|
|
765
|
+
this.pendingApproval = {
|
|
766
|
+
...request,
|
|
767
|
+
requestId: undefined,
|
|
768
|
+
confirmInput: this.pendingCliApprovalHints?.confirmInput,
|
|
769
|
+
denyInput: this.pendingCliApprovalHints?.denyInput,
|
|
770
|
+
};
|
|
771
|
+
this.pendingCliApprovalHints = null;
|
|
772
|
+
this.state.pendingApproval = this.pendingApproval;
|
|
773
|
+
this.state.pendingApprovalOrigin = this.state.activeTurnOrigin;
|
|
774
|
+
this.setStatus("awaiting_approval", "Claude auto-approval failed, forwarding to WeChat.");
|
|
775
|
+
this.emit({
|
|
776
|
+
type: "approval_required",
|
|
777
|
+
request: this.pendingApproval,
|
|
778
|
+
timestamp: nowIso(),
|
|
779
|
+
});
|
|
780
|
+
}
|
|
687
781
|
}
|
|
688
782
|
this.respondToClaudeHook(params.socket, params.requestId);
|
|
689
783
|
return;
|
|
@@ -695,6 +789,10 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
695
789
|
this.handleClaudeStopFailure(payload);
|
|
696
790
|
this.respondToClaudeHook(params.socket, params.requestId);
|
|
697
791
|
return;
|
|
792
|
+
case "PostCompact":
|
|
793
|
+
this.completeClaudeCompact();
|
|
794
|
+
this.respondToClaudeHook(params.socket, params.requestId);
|
|
795
|
+
return;
|
|
698
796
|
default:
|
|
699
797
|
this.respondToClaudeHook(params.socket, params.requestId);
|
|
700
798
|
return;
|
|
@@ -732,6 +830,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
732
830
|
this.state.resumeConversationId = nextResumeConversationId ?? undefined;
|
|
733
831
|
this.transcriptPath = nextTranscriptPath;
|
|
734
832
|
this.state.transcriptPath = nextTranscriptPath ?? undefined;
|
|
833
|
+
this.startTranscriptThinkingWatch();
|
|
735
834
|
if (previousRuntimeSessionId === payload.session_id) {
|
|
736
835
|
return;
|
|
737
836
|
}
|
|
@@ -770,6 +869,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
770
869
|
this.pendingCliApprovalHints = null;
|
|
771
870
|
this.clearWechatWorkingNotice(true);
|
|
772
871
|
this.setStatus("busy");
|
|
872
|
+
this.startTranscriptThinkingWatch();
|
|
773
873
|
this.emit({
|
|
774
874
|
type: "mirrored_user_input",
|
|
775
875
|
text: prompt,
|
|
@@ -809,6 +909,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
809
909
|
}
|
|
810
910
|
handleClaudePermissionRequest(requestId, payload, socket) {
|
|
811
911
|
this.clearWechatWorkingNotice();
|
|
912
|
+
this.lastAutoApprovedPayload = null;
|
|
812
913
|
const denyMessage = getClaudeWechatOutboundAttachmentDenyMessage(payload);
|
|
813
914
|
if (denyMessage) {
|
|
814
915
|
this.pendingApproval = null;
|
|
@@ -825,6 +926,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
825
926
|
if (!this.pendingApproval && !this.state.pendingApproval) {
|
|
826
927
|
this.setStatus("busy", `Claude approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
|
|
827
928
|
}
|
|
929
|
+
this.lastAutoApprovedPayload = payload;
|
|
828
930
|
this.respondToClaudeHook(socket, requestId, buildClaudePermissionDecisionHookOutput(autoResponse.action));
|
|
829
931
|
return;
|
|
830
932
|
}
|
|
@@ -890,6 +992,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
890
992
|
handleClaudeStop(payload) {
|
|
891
993
|
this.clearWechatWorkingNotice(true);
|
|
892
994
|
this.pendingCliApprovalHints = null;
|
|
995
|
+
this.lastAutoApprovedPayload = null;
|
|
893
996
|
this.flushPendingClaudeHookApprovals();
|
|
894
997
|
this.pendingApproval = null;
|
|
895
998
|
this.state.pendingApproval = null;
|
|
@@ -907,11 +1010,97 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
907
1010
|
summary: this.currentPreview,
|
|
908
1011
|
timestamp: nowIso(),
|
|
909
1012
|
});
|
|
1013
|
+
this.stopTranscriptThinkingWatch();
|
|
910
1014
|
this.currentPreview = "(idle)";
|
|
911
1015
|
}
|
|
912
1016
|
handleClaudeStopFailure(payload) {
|
|
1017
|
+
this.lastAutoApprovedPayload = null;
|
|
1018
|
+
this.stopTranscriptThinkingWatch();
|
|
913
1019
|
this.failClaudeTurn(buildClaudeFailureMessage(payload));
|
|
914
1020
|
}
|
|
1021
|
+
startTranscriptThinkingWatch() {
|
|
1022
|
+
this.stopTranscriptThinkingWatch();
|
|
1023
|
+
if (!this.transcriptPath) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (!isThinkingForwardEnabled()) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
this.transcriptTailOffset = 0;
|
|
1030
|
+
try {
|
|
1031
|
+
const stat = fs.statSync(this.transcriptPath);
|
|
1032
|
+
this.transcriptTailOffset = stat.size;
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const watchPath = this.transcriptPath;
|
|
1038
|
+
const sessionId = this.runtimeSessionId;
|
|
1039
|
+
this.polledTranscriptSessionId = sessionId;
|
|
1040
|
+
this.transcriptPollTimer = setInterval(() => {
|
|
1041
|
+
if (this.shuttingDown) {
|
|
1042
|
+
this.stopTranscriptThinkingWatch();
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (this.polledTranscriptSessionId !== this.runtimeSessionId) {
|
|
1046
|
+
this.stopTranscriptThinkingWatch();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
const stat = fs.statSync(watchPath);
|
|
1051
|
+
if (stat.size <= this.transcriptTailOffset) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const fd = fs.openSync(watchPath, "r");
|
|
1055
|
+
const buf = Buffer.alloc(stat.size - this.transcriptTailOffset);
|
|
1056
|
+
fs.readSync(fd, buf, 0, buf.length, this.transcriptTailOffset);
|
|
1057
|
+
fs.closeSync(fd);
|
|
1058
|
+
this.transcriptTailOffset = stat.size;
|
|
1059
|
+
const newText = buf.toString("utf8");
|
|
1060
|
+
const lines = newText.split(/\r?\n/).filter(Boolean);
|
|
1061
|
+
for (const line of lines) {
|
|
1062
|
+
let parsed;
|
|
1063
|
+
try {
|
|
1064
|
+
parsed = JSON.parse(line);
|
|
1065
|
+
}
|
|
1066
|
+
catch {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (!parsed ||
|
|
1070
|
+
parsed.type !== "assistant" ||
|
|
1071
|
+
!Array.isArray(parsed.message?.content)) {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
for (const block of parsed.message.content) {
|
|
1075
|
+
if (block.type === "thinking" &&
|
|
1076
|
+
typeof block.thinking === "string" &&
|
|
1077
|
+
block.thinking.trim()) {
|
|
1078
|
+
const thinking = normalizeOutput(block.thinking).trim();
|
|
1079
|
+
if (thinking) {
|
|
1080
|
+
this.emit({
|
|
1081
|
+
type: "thinking",
|
|
1082
|
+
text: thinking,
|
|
1083
|
+
timestamp: nowIso(),
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
// File may be temporarily locked or deleted; silently retry next poll.
|
|
1092
|
+
}
|
|
1093
|
+
}, 800);
|
|
1094
|
+
this.transcriptPollTimer.unref?.();
|
|
1095
|
+
}
|
|
1096
|
+
stopTranscriptThinkingWatch() {
|
|
1097
|
+
if (this.transcriptPollTimer) {
|
|
1098
|
+
clearInterval(this.transcriptPollTimer);
|
|
1099
|
+
this.transcriptPollTimer = null;
|
|
1100
|
+
}
|
|
1101
|
+
this.transcriptTailOffset = 0;
|
|
1102
|
+
this.polledTranscriptSessionId = null;
|
|
1103
|
+
}
|
|
915
1104
|
respondToClaudeHook(socket, requestId, stdout) {
|
|
916
1105
|
try {
|
|
917
1106
|
socket.end(`${JSON.stringify({ requestId, stdout })}\n`);
|