cli-wechat-bridge 1.1.4 → 1.1.6
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 +153 -131
- package/bin/_run-entry.mjs +20 -17
- package/dist/bridge/bridge-adapters.claude.js +429 -11
- package/dist/bridge/bridge-adapters.codex.js +441 -22
- package/dist/bridge/bridge-adapters.core.js +2 -1
- package/dist/bridge/bridge-adapters.opencode.js +392 -53
- package/dist/bridge/bridge-adapters.pi.js +33 -7
- package/dist/bridge/bridge-adapters.shared.js +2 -1
- package/dist/bridge/bridge-controller.js +3 -0
- package/dist/bridge/bridge-final-reply.js +2 -65
- package/dist/bridge/bridge-session-resume.js +151 -0
- package/dist/bridge/bridge-state.js +1 -1
- package/dist/bridge/bridge-utils.js +30 -101
- package/dist/bridge/claude-hooks.js +1 -0
- package/dist/bridge/wechat-bridge.js +256 -211
- package/dist/bridge/wechat-forwarding.js +2 -58
- package/dist/bridge/wechat-outbound-queue.js +2 -0
- package/dist/channels/wechat/bridge-final-reply.js +65 -0
- package/dist/channels/wechat/channel-message.js +24 -0
- package/dist/channels/wechat/index.js +5 -0
- package/dist/channels/wechat/wechat-channel-port.js +67 -0
- package/dist/channels/wechat/wechat-forwarding.js +79 -0
- package/dist/channels/wechat/wechat-outbound-queue.js +72 -0
- package/dist/companion/codex-remote-client.js +291 -32
- package/dist/companion/codex-visible-client-link.js +122 -0
- package/dist/companion/local-companion-link.js +26 -0
- package/dist/companion/local-companion-start.js +0 -1
- package/dist/companion/opencode-tui-bridge-plugin.js +73 -0
- package/dist/companion/pi-tui-bridge-extension.js +42 -3
- package/dist/core/bridge-control.js +32 -0
- package/dist/core/bridge-event-forwarder.js +50 -0
- package/dist/core/bridge-message-router.js +32 -0
- package/dist/core/channel-types.js +1 -0
- package/dist/core/text-utils.js +63 -0
- package/dist/daemon/wechat-daemon.js +242 -159
- package/dist/i18n/messages-en.js +2 -2
- package/dist/i18n/messages-zh.js +2 -2
- package/package.json +105 -109
- package/bin/wechat-claude-start.mjs +0 -10
- package/bin/wechat-codex-start.mjs +0 -10
- package/bin/wechat-opencode-start.mjs +0 -10
- package/bin/wechat-pi-start.mjs +0 -10
|
@@ -2,11 +2,13 @@ import fs from "node:fs";
|
|
|
2
2
|
import net from "node:net";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { buildLocalCompanionToken } from "../companion/local-companion-link.js";
|
|
6
7
|
import { t } from "../i18n/index.js";
|
|
7
8
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
8
9
|
import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, getClaudePermissionAutoResponse, getClaudeWechatOutboundAttachmentDenyMessage, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
9
|
-
import { detectCliApproval, isThinkingForwardEnabled,
|
|
10
|
+
import { detectCliApproval, isThinkingForwardEnabled, } from "./bridge-utils.js";
|
|
11
|
+
import { normalizeOutput, nowIso, truncatePreview } from "../core/text-utils.js";
|
|
10
12
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
11
13
|
import * as shared from "./bridge-adapters.shared.js";
|
|
12
14
|
const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, delay, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
|
|
@@ -17,6 +19,10 @@ const CLAUDE_BRACKETED_PASTE_START = "\u001b[200~";
|
|
|
17
19
|
const CLAUDE_BRACKETED_PASTE_END = "\u001b[201~";
|
|
18
20
|
const CLAUDE_REMOTE_ENTER_DELAY_MS = 40;
|
|
19
21
|
const CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT = 4_000;
|
|
22
|
+
const CLAUDE_SESSION_METADATA_READ_BYTES = 64 * 1024;
|
|
23
|
+
const CLAUDE_MAX_SANITIZED_PROJECT_PATH = 200;
|
|
24
|
+
const CLAUDE_RESUME_TIMEOUT_MS = 30_000;
|
|
25
|
+
const CLAUDE_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
20
26
|
function isClaudeWorkspaceTrustPrompt(text) {
|
|
21
27
|
const compact = text.replace(/\s+/g, " ").trim();
|
|
22
28
|
return (/Accessing workspace:/i.test(compact) &&
|
|
@@ -27,6 +33,229 @@ function isClaudeWorkspaceTrustPrompt(text) {
|
|
|
27
33
|
function isRecord(value) {
|
|
28
34
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
29
35
|
}
|
|
36
|
+
function expandHomePath(value) {
|
|
37
|
+
if (value === "~") {
|
|
38
|
+
return os.homedir();
|
|
39
|
+
}
|
|
40
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
41
|
+
return path.join(os.homedir(), value.slice(2));
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function normalizeComparablePath(value) {
|
|
46
|
+
const resolved = path.resolve(value);
|
|
47
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
48
|
+
}
|
|
49
|
+
function isSameClaudePath(left, right) {
|
|
50
|
+
return normalizeComparablePath(left) === normalizeComparablePath(right);
|
|
51
|
+
}
|
|
52
|
+
function getClaudeConfigDirectory(env) {
|
|
53
|
+
const custom = env.CLAUDE_CONFIG_DIR;
|
|
54
|
+
return custom
|
|
55
|
+
? path.resolve(expandHomePath(custom))
|
|
56
|
+
: path.join(env.USERPROFILE || env.HOME || os.homedir(), ".claude");
|
|
57
|
+
}
|
|
58
|
+
function sanitizeClaudeProjectPath(cwd) {
|
|
59
|
+
return path.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
60
|
+
}
|
|
61
|
+
export function buildClaudeProjectSessionDirectory(cwd, env = process.env) {
|
|
62
|
+
return path.join(getClaudeConfigDirectory(env), "projects", sanitizeClaudeProjectPath(cwd));
|
|
63
|
+
}
|
|
64
|
+
function readClaudeSessionSegments(filePath) {
|
|
65
|
+
const stat = fs.statSync(filePath);
|
|
66
|
+
const fd = fs.openSync(filePath, "r");
|
|
67
|
+
try {
|
|
68
|
+
const headLength = Math.min(stat.size, CLAUDE_SESSION_METADATA_READ_BYTES);
|
|
69
|
+
const headBuffer = Buffer.alloc(headLength);
|
|
70
|
+
fs.readSync(fd, headBuffer, 0, headLength, 0);
|
|
71
|
+
const tailStart = Math.max(0, stat.size - CLAUDE_SESSION_METADATA_READ_BYTES);
|
|
72
|
+
const tailLength = stat.size - tailStart;
|
|
73
|
+
const tailBuffer = Buffer.alloc(tailLength);
|
|
74
|
+
fs.readSync(fd, tailBuffer, 0, tailLength, tailStart);
|
|
75
|
+
return [headBuffer.toString("utf8"), tailBuffer.toString("utf8")];
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
fs.closeSync(fd);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function extractClaudeUserMessageText(value) {
|
|
82
|
+
if (typeof value === "string") {
|
|
83
|
+
return normalizeOutput(value).trim();
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(value)) {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
return normalizeOutput(value
|
|
89
|
+
.filter((item) => isRecord(item))
|
|
90
|
+
.filter((item) => item.type === "text" && typeof item.text === "string")
|
|
91
|
+
.map((item) => item.text)
|
|
92
|
+
.join("\n")).trim();
|
|
93
|
+
}
|
|
94
|
+
function readClaudeSessionMetadata(filePath) {
|
|
95
|
+
const metadata = { lastTimestampMs: 0 };
|
|
96
|
+
const seenLines = new Set();
|
|
97
|
+
for (const segment of readClaudeSessionSegments(filePath)) {
|
|
98
|
+
for (const rawLine of segment.split(/\r?\n/)) {
|
|
99
|
+
const line = rawLine.trim();
|
|
100
|
+
if (!line || seenLines.has(line)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
seenLines.add(line);
|
|
104
|
+
try {
|
|
105
|
+
const entry = JSON.parse(line);
|
|
106
|
+
if (!isRecord(entry)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (typeof entry.cwd === "string") {
|
|
110
|
+
metadata.cwd = entry.cwd;
|
|
111
|
+
}
|
|
112
|
+
if (entry.isSidechain === false) {
|
|
113
|
+
metadata.isSidechain = false;
|
|
114
|
+
}
|
|
115
|
+
else if (entry.isSidechain === true && metadata.isSidechain === undefined) {
|
|
116
|
+
metadata.isSidechain = true;
|
|
117
|
+
}
|
|
118
|
+
if (entry.type === "custom-title" && typeof entry.customTitle === "string") {
|
|
119
|
+
metadata.customTitle = normalizeOutput(entry.customTitle).trim();
|
|
120
|
+
}
|
|
121
|
+
else if (entry.type === "ai-title" && typeof entry.aiTitle === "string") {
|
|
122
|
+
metadata.aiTitle = normalizeOutput(entry.aiTitle).trim();
|
|
123
|
+
}
|
|
124
|
+
else if (entry.type === "last-prompt" && typeof entry.lastPrompt === "string") {
|
|
125
|
+
metadata.lastPrompt = normalizeOutput(entry.lastPrompt).trim();
|
|
126
|
+
}
|
|
127
|
+
else if (entry.type === "summary" && typeof entry.summary === "string") {
|
|
128
|
+
metadata.summary = normalizeOutput(entry.summary).trim();
|
|
129
|
+
}
|
|
130
|
+
else if (!metadata.firstPrompt &&
|
|
131
|
+
entry.type === "user" &&
|
|
132
|
+
isRecord(entry.message)) {
|
|
133
|
+
metadata.firstPrompt = extractClaudeUserMessageText(entry.message.content);
|
|
134
|
+
}
|
|
135
|
+
if (typeof entry.timestamp === "string") {
|
|
136
|
+
const timestampMs = Date.parse(entry.timestamp);
|
|
137
|
+
if (Number.isFinite(timestampMs)) {
|
|
138
|
+
metadata.lastTimestampMs = Math.max(metadata.lastTimestampMs, timestampMs);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Head/tail segments can begin or end with a partial JSONL line.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return metadata;
|
|
148
|
+
}
|
|
149
|
+
function readClaudeSessionIndex(projectDir, cwd) {
|
|
150
|
+
const indexPath = path.join(projectDir, "sessions-index.json");
|
|
151
|
+
if (!fs.existsSync(indexPath)) {
|
|
152
|
+
return new Map();
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(fs.readFileSync(indexPath, "utf8"));
|
|
156
|
+
if (!isRecord(parsed)) {
|
|
157
|
+
return new Map();
|
|
158
|
+
}
|
|
159
|
+
if (typeof parsed.originalPath === "string" && !isSameClaudePath(parsed.originalPath, cwd)) {
|
|
160
|
+
return new Map();
|
|
161
|
+
}
|
|
162
|
+
const entries = Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
163
|
+
return new Map(entries
|
|
164
|
+
.filter((entry) => isRecord(entry))
|
|
165
|
+
.filter((entry) => typeof entry.sessionId === "string")
|
|
166
|
+
.map((entry) => [entry.sessionId, entry]));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return new Map();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function findClaudeProjectSessionDirectories(cwd, env) {
|
|
173
|
+
const projectsDir = path.join(getClaudeConfigDirectory(env), "projects");
|
|
174
|
+
const direct = buildClaudeProjectSessionDirectory(cwd, env);
|
|
175
|
+
const results = new Set();
|
|
176
|
+
if (fs.existsSync(direct)) {
|
|
177
|
+
results.add(direct);
|
|
178
|
+
}
|
|
179
|
+
const sanitized = sanitizeClaudeProjectPath(cwd);
|
|
180
|
+
if (sanitized.length <= CLAUDE_MAX_SANITIZED_PROJECT_PATH || !fs.existsSync(projectsDir)) {
|
|
181
|
+
return [...results];
|
|
182
|
+
}
|
|
183
|
+
const prefix = sanitized.slice(0, CLAUDE_MAX_SANITIZED_PROJECT_PATH);
|
|
184
|
+
for (const entry of fs.readdirSync(projectsDir, { withFileTypes: true })) {
|
|
185
|
+
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
|
186
|
+
results.add(path.join(projectsDir, entry.name));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return [...results];
|
|
190
|
+
}
|
|
191
|
+
function summarizeClaudeSessionFile(filePath, cwd, indexEntry) {
|
|
192
|
+
const sessionId = path.basename(filePath, ".jsonl");
|
|
193
|
+
if (!CLAUDE_SESSION_ID_RE.test(sessionId)) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const metadata = readClaudeSessionMetadata(filePath);
|
|
198
|
+
const belongsToCwd = (typeof indexEntry?.projectPath === "string" &&
|
|
199
|
+
isSameClaudePath(indexEntry.projectPath, cwd)) ||
|
|
200
|
+
(typeof metadata.cwd === "string" && isSameClaudePath(metadata.cwd, cwd));
|
|
201
|
+
if (!belongsToCwd || indexEntry?.isSidechain === true || metadata.isSidechain === true) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const indexedModifiedMs = typeof indexEntry?.modified === "string" ? Date.parse(indexEntry.modified) : 0;
|
|
205
|
+
const updatedMs = metadata.lastTimestampMs ||
|
|
206
|
+
(Number.isFinite(indexedModifiedMs) ? indexedModifiedMs : 0) ||
|
|
207
|
+
fs.statSync(filePath).mtimeMs;
|
|
208
|
+
const title = metadata.customTitle ||
|
|
209
|
+
indexEntry?.customTitle ||
|
|
210
|
+
metadata.aiTitle ||
|
|
211
|
+
metadata.lastPrompt ||
|
|
212
|
+
indexEntry?.summary ||
|
|
213
|
+
metadata.summary ||
|
|
214
|
+
indexEntry?.firstPrompt ||
|
|
215
|
+
metadata.firstPrompt;
|
|
216
|
+
if (!title) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
sessionId,
|
|
221
|
+
title: truncatePreview(title, 120),
|
|
222
|
+
lastUpdatedAt: new Date(updatedMs).toISOString(),
|
|
223
|
+
source: "claude",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export function listClaudeResumeSessions(cwd, limit = 10, env = process.env) {
|
|
231
|
+
const candidates = [];
|
|
232
|
+
for (const projectDir of findClaudeProjectSessionDirectories(cwd, env)) {
|
|
233
|
+
const index = readClaudeSessionIndex(projectDir, cwd);
|
|
234
|
+
for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
|
|
235
|
+
if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".jsonl")) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const sessionId = path.basename(entry.name, ".jsonl");
|
|
239
|
+
const candidate = summarizeClaudeSessionFile(path.join(projectDir, entry.name), cwd, index.get(sessionId));
|
|
240
|
+
if (candidate) {
|
|
241
|
+
candidates.push(candidate);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return candidates
|
|
246
|
+
.sort((left, right) => Date.parse(right.lastUpdatedAt) - Date.parse(left.lastUpdatedAt))
|
|
247
|
+
.slice(0, Math.max(1, limit));
|
|
248
|
+
}
|
|
249
|
+
function findClaudeResumeSessionFile(cwd, sessionId, env = process.env) {
|
|
250
|
+
for (const projectDir of findClaudeProjectSessionDirectories(cwd, env)) {
|
|
251
|
+
const filePath = path.join(projectDir, `${sessionId}.jsonl`);
|
|
252
|
+
if (fs.existsSync(filePath) &&
|
|
253
|
+
summarizeClaudeSessionFile(filePath, cwd, readClaudeSessionIndex(projectDir, cwd).get(sessionId))) {
|
|
254
|
+
return filePath;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
30
259
|
export function normalizeClaudeProjectConfigKey(cwd) {
|
|
31
260
|
return path.resolve(cwd).replace(/\\/g, "/");
|
|
32
261
|
}
|
|
@@ -106,6 +335,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
106
335
|
transcriptPollTimer = null;
|
|
107
336
|
transcriptTailOffset = 0;
|
|
108
337
|
polledTranscriptSessionId = null;
|
|
338
|
+
pendingResume = null;
|
|
339
|
+
localResumeSessionEndObserved = false;
|
|
109
340
|
constructor(options) {
|
|
110
341
|
super(options);
|
|
111
342
|
const shouldRestoreInitialSession = options.sessionStartMode !== "new";
|
|
@@ -192,11 +423,104 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
192
423
|
this.writeToPty("\r");
|
|
193
424
|
this.armWechatWorkingNotice();
|
|
194
425
|
}
|
|
195
|
-
async listResumeSessions(
|
|
196
|
-
|
|
426
|
+
async listResumeSessions(limit = 10) {
|
|
427
|
+
return listClaudeResumeSessions(this.options.cwd, limit);
|
|
428
|
+
}
|
|
429
|
+
async resumeSession(sessionId) {
|
|
430
|
+
if (!this.pty) {
|
|
431
|
+
throw new Error("Claude is not running yet.");
|
|
432
|
+
}
|
|
433
|
+
if (this.pendingResume) {
|
|
434
|
+
throw new Error("Claude is already switching sessions.");
|
|
435
|
+
}
|
|
436
|
+
if (this.pendingApproval) {
|
|
437
|
+
throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
|
|
438
|
+
}
|
|
439
|
+
if (this.state.status === "busy" || this.state.status === "awaiting_approval") {
|
|
440
|
+
throw new Error("Claude is still working. Wait for the current reply or use /stop.");
|
|
441
|
+
}
|
|
442
|
+
if (this.state.status !== "idle") {
|
|
443
|
+
throw new Error(`Claude cannot switch sessions while its status is ${this.state.status}.`);
|
|
444
|
+
}
|
|
445
|
+
const targetConversationId = sessionId.trim();
|
|
446
|
+
if (!CLAUDE_SESSION_ID_RE.test(targetConversationId)) {
|
|
447
|
+
throw new Error(`Invalid Claude session ID: ${targetConversationId}`);
|
|
448
|
+
}
|
|
449
|
+
if (!this.resolveClaudeResumeSessionFile(targetConversationId)) {
|
|
450
|
+
throw new Error(`No Claude session ${targetConversationId} was found for ${this.options.cwd}.`);
|
|
451
|
+
}
|
|
452
|
+
if (this.isClaudeSessionActiveElsewhere(targetConversationId)) {
|
|
453
|
+
throw new Error(`Claude session ${targetConversationId} is active in another Claude process. Stop it before resuming from WeChat.`);
|
|
454
|
+
}
|
|
455
|
+
this.setStatus("starting", `Switching Claude to session ${targetConversationId.slice(0, 12)}...`);
|
|
456
|
+
const completion = new Promise((resolve, reject) => {
|
|
457
|
+
const timer = setTimeout(() => {
|
|
458
|
+
if (this.pendingResume?.targetConversationId !== targetConversationId) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const phase = this.pendingResume.sessionEndObserved
|
|
462
|
+
? " after the previous session ended"
|
|
463
|
+
: "";
|
|
464
|
+
this.pendingResume = null;
|
|
465
|
+
this.setStatus("idle");
|
|
466
|
+
reject(new Error(`Timed out waiting for Claude to resume session ${targetConversationId}${phase}.`));
|
|
467
|
+
}, CLAUDE_RESUME_TIMEOUT_MS);
|
|
468
|
+
this.pendingResume = {
|
|
469
|
+
targetConversationId,
|
|
470
|
+
resolve,
|
|
471
|
+
reject,
|
|
472
|
+
timer,
|
|
473
|
+
sessionEndObserved: false,
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
this.writeToPty(`/resume ${targetConversationId}`);
|
|
477
|
+
await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
|
|
478
|
+
this.writeToPty("\r");
|
|
479
|
+
await completion;
|
|
480
|
+
}
|
|
481
|
+
isClaudeSessionActiveElsewhere(sessionId) {
|
|
482
|
+
if (sessionId === this.resumeConversationId) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const env = shared.buildCliEnvironment("claude");
|
|
487
|
+
const target = shared.resolveSpawnTarget(this.options.command, "claude", { env });
|
|
488
|
+
const result = spawnSync(target.file, [...target.args, "agents", "--json", "--cwd", this.options.cwd], {
|
|
489
|
+
cwd: this.options.cwd,
|
|
490
|
+
env,
|
|
491
|
+
encoding: "utf8",
|
|
492
|
+
windowsHide: true,
|
|
493
|
+
timeout: 5_000,
|
|
494
|
+
});
|
|
495
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
const parsed = JSON.parse(result.stdout);
|
|
499
|
+
const sessions = Array.isArray(parsed)
|
|
500
|
+
? parsed
|
|
501
|
+
: isRecord(parsed) && Array.isArray(parsed.sessions)
|
|
502
|
+
? parsed.sessions
|
|
503
|
+
: [];
|
|
504
|
+
return sessions.some((session) => {
|
|
505
|
+
if (!isRecord(session)) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
const candidateId = typeof session.id === "string"
|
|
509
|
+
? session.id
|
|
510
|
+
: typeof session.sessionId === "string"
|
|
511
|
+
? session.sessionId
|
|
512
|
+
: typeof session.session_id === "string"
|
|
513
|
+
? session.session_id
|
|
514
|
+
: "";
|
|
515
|
+
return candidateId === sessionId;
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
197
521
|
}
|
|
198
|
-
|
|
199
|
-
|
|
522
|
+
resolveClaudeResumeSessionFile(sessionId) {
|
|
523
|
+
return findClaudeResumeSessionFile(this.options.cwd, sessionId);
|
|
200
524
|
}
|
|
201
525
|
async interrupt() {
|
|
202
526
|
if (!this.pty) {
|
|
@@ -228,6 +552,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
228
552
|
return true;
|
|
229
553
|
}
|
|
230
554
|
async reset() {
|
|
555
|
+
this.rejectPendingResume(new Error("Claude was reset during session switching."));
|
|
231
556
|
this.clearWechatWorkingNotice(true);
|
|
232
557
|
this.pendingCliApprovalHints = null;
|
|
233
558
|
this.runtimeSessionId = null;
|
|
@@ -295,6 +620,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
295
620
|
return ok ? 1 : 0;
|
|
296
621
|
}
|
|
297
622
|
async dispose() {
|
|
623
|
+
this.rejectPendingResume(new Error("Claude is shutting down during session switching."));
|
|
298
624
|
this.detachLocalTerminal();
|
|
299
625
|
this.clearWechatWorkingNotice(true);
|
|
300
626
|
this.pendingCliApprovalHints = null;
|
|
@@ -326,6 +652,16 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
326
652
|
if (!text) {
|
|
327
653
|
return;
|
|
328
654
|
}
|
|
655
|
+
if (this.pendingResume &&
|
|
656
|
+
(isClaudeInvalidResumeError(text) ||
|
|
657
|
+
/Session .+ was not found|Found \d+ sessions matching|Failed to resume/i.test(text))) {
|
|
658
|
+
const failureLine = text
|
|
659
|
+
.split("\n")
|
|
660
|
+
.map((line) => line.trim())
|
|
661
|
+
.find((line) => /not found|matching|failed to resume/i.test(line));
|
|
662
|
+
this.rejectPendingResume(new Error(failureLine || "Claude could not resume the requested session."));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
329
665
|
if (this.resumeConversationId &&
|
|
330
666
|
!this.hasAcceptedInput &&
|
|
331
667
|
!this.recoveringInvalidResume &&
|
|
@@ -385,6 +721,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
385
721
|
this.detachLocalTerminal();
|
|
386
722
|
this.clearWechatWorkingNotice(true);
|
|
387
723
|
this.pendingCliApprovalHints = null;
|
|
724
|
+
this.rejectPendingResume(new Error("Claude exited before the requested session switch completed."));
|
|
388
725
|
void this.stopHookServer();
|
|
389
726
|
if (this.recoveringInvalidResume && !this.shuttingDown) {
|
|
390
727
|
this.clearCompletionTimer();
|
|
@@ -772,6 +1109,10 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
772
1109
|
this.handleClaudeSessionStart(payload);
|
|
773
1110
|
this.respondToClaudeHook(params.socket, params.requestId);
|
|
774
1111
|
return;
|
|
1112
|
+
case "SessionEnd":
|
|
1113
|
+
this.handleClaudeSessionEnd(payload);
|
|
1114
|
+
this.respondToClaudeHook(params.socket, params.requestId);
|
|
1115
|
+
return;
|
|
775
1116
|
case "UserPromptSubmit":
|
|
776
1117
|
this.handleClaudeUserPromptSubmit(payload);
|
|
777
1118
|
this.respondToClaudeHook(params.socket, params.requestId);
|
|
@@ -836,6 +1177,17 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
836
1177
|
? payload.transcript_path.trim()
|
|
837
1178
|
: null;
|
|
838
1179
|
const nextResumeConversationId = extractClaudeResumeConversationId(nextTranscriptPath ?? undefined);
|
|
1180
|
+
const resumedConversationId = nextResumeConversationId ?? payload.session_id;
|
|
1181
|
+
const pendingResume = this.pendingResume;
|
|
1182
|
+
const pendingResumeMatches = payload.source === "resume" &&
|
|
1183
|
+
pendingResume !== null &&
|
|
1184
|
+
pendingResume.targetConversationId === resumedConversationId;
|
|
1185
|
+
const isInteractiveResume = payload.source === "resume" && Boolean(previousRuntimeSessionId);
|
|
1186
|
+
if (isInteractiveResume &&
|
|
1187
|
+
!pendingResumeMatches &&
|
|
1188
|
+
!this.localResumeSessionEndObserved) {
|
|
1189
|
+
this.failClaudeTurn("The active WeChat task was interrupted because the local Claude terminal switched sessions.");
|
|
1190
|
+
}
|
|
839
1191
|
const compactedByTranscriptRotation = Boolean(this.transcriptPath) &&
|
|
840
1192
|
Boolean(nextTranscriptPath) &&
|
|
841
1193
|
this.transcriptPath !== nextTranscriptPath &&
|
|
@@ -859,16 +1211,33 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
859
1211
|
this.transcriptPath = nextTranscriptPath;
|
|
860
1212
|
this.state.transcriptPath = nextTranscriptPath ?? undefined;
|
|
861
1213
|
this.startTranscriptThinkingWatch();
|
|
862
|
-
if (previousRuntimeSessionId === payload.session_id) {
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
1214
|
const timestamp = nowIso();
|
|
866
|
-
const
|
|
1215
|
+
const isStartupRestore = !previousRuntimeSessionId &&
|
|
867
1216
|
(payload.source === "resume" ||
|
|
868
1217
|
(nextResumeConversationId !== null &&
|
|
869
1218
|
nextResumeConversationId === previousResumeConversationId));
|
|
870
|
-
const source =
|
|
871
|
-
|
|
1219
|
+
const source = pendingResumeMatches
|
|
1220
|
+
? "wechat"
|
|
1221
|
+
: isStartupRestore
|
|
1222
|
+
? "restore"
|
|
1223
|
+
: "local";
|
|
1224
|
+
const reason = pendingResumeMatches
|
|
1225
|
+
? "wechat_resume"
|
|
1226
|
+
: isStartupRestore
|
|
1227
|
+
? "startup_restore"
|
|
1228
|
+
: "local_follow";
|
|
1229
|
+
this.localResumeSessionEndObserved = false;
|
|
1230
|
+
if (pendingResume &&
|
|
1231
|
+
payload.source === "resume" &&
|
|
1232
|
+
!pendingResumeMatches) {
|
|
1233
|
+
this.rejectPendingResume(new Error(`Claude resumed unexpected session ${resumedConversationId}; expected ${pendingResume.targetConversationId}.`));
|
|
1234
|
+
}
|
|
1235
|
+
if (previousRuntimeSessionId === payload.session_id) {
|
|
1236
|
+
if (pendingResumeMatches) {
|
|
1237
|
+
this.resolvePendingResume();
|
|
1238
|
+
}
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
872
1241
|
this.state.lastSessionSwitchAt = timestamp;
|
|
873
1242
|
this.state.lastSessionSwitchSource = source;
|
|
874
1243
|
this.state.lastSessionSwitchReason = reason;
|
|
@@ -879,6 +1248,40 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
879
1248
|
reason,
|
|
880
1249
|
timestamp,
|
|
881
1250
|
});
|
|
1251
|
+
if (pendingResumeMatches) {
|
|
1252
|
+
this.resolvePendingResume();
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
handleClaudeSessionEnd(payload) {
|
|
1256
|
+
if (payload.reason !== "resume") {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (this.pendingResume) {
|
|
1260
|
+
this.pendingResume.sessionEndObserved = true;
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
this.localResumeSessionEndObserved = true;
|
|
1264
|
+
this.failClaudeTurn("The active WeChat task was interrupted because the local Claude terminal switched sessions.");
|
|
1265
|
+
}
|
|
1266
|
+
resolvePendingResume() {
|
|
1267
|
+
const pending = this.pendingResume;
|
|
1268
|
+
if (!pending) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
this.pendingResume = null;
|
|
1272
|
+
clearTimeout(pending.timer);
|
|
1273
|
+
this.setStatus("idle");
|
|
1274
|
+
pending.resolve();
|
|
1275
|
+
}
|
|
1276
|
+
rejectPendingResume(error) {
|
|
1277
|
+
const pending = this.pendingResume;
|
|
1278
|
+
if (!pending) {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
this.pendingResume = null;
|
|
1282
|
+
clearTimeout(pending.timer);
|
|
1283
|
+
this.setStatus("idle");
|
|
1284
|
+
pending.reject(error);
|
|
882
1285
|
}
|
|
883
1286
|
handleClaudeUserPromptSubmit(payload) {
|
|
884
1287
|
const prompt = typeof payload.prompt === "string" ? normalizeOutput(payload.prompt).trim() : "";
|
|
@@ -1028,6 +1431,16 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
1028
1431
|
normalizeClaudeAssistantMessage(payload));
|
|
1029
1432
|
}
|
|
1030
1433
|
handleClaudeStop(payload) {
|
|
1434
|
+
if (payload.session_id &&
|
|
1435
|
+
this.runtimeSessionId &&
|
|
1436
|
+
payload.session_id !== this.runtimeSessionId) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (!this.hasAcceptedInput &&
|
|
1440
|
+
this.state.activeTurnOrigin === undefined &&
|
|
1441
|
+
this.currentPreview === "(idle)") {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1031
1444
|
this.clearWechatWorkingNotice(true);
|
|
1032
1445
|
this.pendingCliApprovalHints = null;
|
|
1033
1446
|
this.lastAutoApprovedPayload = null;
|
|
@@ -1052,6 +1465,11 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
1052
1465
|
this.currentPreview = "(idle)";
|
|
1053
1466
|
}
|
|
1054
1467
|
handleClaudeStopFailure(payload) {
|
|
1468
|
+
if (payload.session_id &&
|
|
1469
|
+
this.runtimeSessionId &&
|
|
1470
|
+
payload.session_id !== this.runtimeSessionId) {
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1055
1473
|
this.lastAutoApprovedPayload = null;
|
|
1056
1474
|
this.stopTranscriptThinkingWatch();
|
|
1057
1475
|
this.failClaudeTurn(buildClaudeFailureMessage(payload));
|