@unlinearity/cli-wechat-bridge 1.0.6 → 1.0.7
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 +111 -354
- package/dist/bridge/bridge-adapters.claude.js +94 -3
- package/dist/bridge/bridge-adapters.codex.js +4 -2
- package/dist/bridge/bridge-adapters.core.js +13 -5
- package/dist/bridge/bridge-adapters.opencode.js +65 -12
- package/dist/bridge/bridge-adapters.shared.js +3 -6
- package/dist/bridge/bridge-process-reaper.js +40 -1
- package/dist/bridge/bridge-state.js +6 -3
- package/dist/bridge/wechat-bridge.js +11 -1
- package/dist/commands/check-update.js +1 -1
- package/dist/companion/local-companion-start.js +27 -2
- package/dist/companion/local-companion.js +21 -4
- package/dist/daemon/wechat-daemon.js +276 -20
- package/package.json +1 -3
- package/dist/companion/codex-panel-link.js +0 -72
- package/dist/companion/codex-panel.js +0 -179
- package/dist/media/media-types.js +0 -1
- package/dist/wechat/standalone-bot.js +0 -355
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import net from "node:net";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { buildLocalCompanionToken } from "../companion/local-companion-link.js";
|
|
5
6
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
@@ -14,6 +15,69 @@ const CLAUDE_COMPACT_DEDUP_MS = 2_000;
|
|
|
14
15
|
const CLAUDE_BRACKETED_PASTE_START = "\u001b[200~";
|
|
15
16
|
const CLAUDE_BRACKETED_PASTE_END = "\u001b[201~";
|
|
16
17
|
const CLAUDE_REMOTE_ENTER_DELAY_MS = 40;
|
|
18
|
+
const CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT = 4_000;
|
|
19
|
+
function isClaudeWorkspaceTrustPrompt(text) {
|
|
20
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
21
|
+
return (/Accessing workspace:/i.test(compact) &&
|
|
22
|
+
/Quick safety check:/i.test(compact) &&
|
|
23
|
+
/project you created or one you trust/i.test(compact) &&
|
|
24
|
+
/I trust this folder/i.test(compact));
|
|
25
|
+
}
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
export function normalizeClaudeProjectConfigKey(cwd) {
|
|
30
|
+
return path.resolve(cwd).replace(/\\/g, "/");
|
|
31
|
+
}
|
|
32
|
+
export function ensureClaudeWorkspaceTrustAccepted(cwd, homeDir = process.env.USERPROFILE || process.env.HOME || os.homedir()) {
|
|
33
|
+
const configPath = path.join(homeDir, ".claude.json");
|
|
34
|
+
let config = {};
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(configPath)) {
|
|
37
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
38
|
+
config = raw.trim() ? JSON.parse(raw) : {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (!isRecord(config)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const projectKey = normalizeClaudeProjectConfigKey(cwd);
|
|
48
|
+
const projects = isRecord(config.projects) ? config.projects : {};
|
|
49
|
+
const currentProject = isRecord(projects[projectKey])
|
|
50
|
+
? projects[projectKey]
|
|
51
|
+
: {};
|
|
52
|
+
if (currentProject.hasTrustDialogAccepted === true) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const nextConfig = {
|
|
56
|
+
...config,
|
|
57
|
+
projects: {
|
|
58
|
+
...projects,
|
|
59
|
+
[projectKey]: {
|
|
60
|
+
...currentProject,
|
|
61
|
+
hasTrustDialogAccepted: true,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const tempPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
66
|
+
try {
|
|
67
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
68
|
+
fs.renameSync(tempPath, configPath);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
try {
|
|
73
|
+
fs.rmSync(tempPath, { force: true });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Best effort cleanup.
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
17
81
|
export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
18
82
|
hookServer = null;
|
|
19
83
|
hookPort = null;
|
|
@@ -32,11 +96,20 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
32
96
|
workingNoticeSent = false;
|
|
33
97
|
workingNoticeDelayMs = CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS;
|
|
34
98
|
lastCompactCompletionAtMs = 0;
|
|
99
|
+
startupOutputBuffer = "";
|
|
100
|
+
hasAutoConfirmedWorkspaceTrustPrompt = false;
|
|
35
101
|
constructor(options) {
|
|
36
102
|
super(options);
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
|
|
103
|
+
const shouldRestoreInitialSession = options.sessionStartMode !== "new";
|
|
104
|
+
this.runtimeSessionId = shouldRestoreInitialSession
|
|
105
|
+
? options.initialSharedSessionId ?? options.initialSharedThreadId ?? null
|
|
106
|
+
: null;
|
|
107
|
+
this.resumeConversationId = shouldRestoreInitialSession
|
|
108
|
+
? options.initialResumeConversationId ?? null
|
|
109
|
+
: null;
|
|
110
|
+
this.transcriptPath = shouldRestoreInitialSession
|
|
111
|
+
? options.initialTranscriptPath ?? null
|
|
112
|
+
: null;
|
|
40
113
|
if (this.runtimeSessionId) {
|
|
41
114
|
this.state.sharedSessionId = this.runtimeSessionId;
|
|
42
115
|
this.state.activeRuntimeSessionId = this.runtimeSessionId;
|
|
@@ -52,6 +125,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
52
125
|
if (this.pty) {
|
|
53
126
|
return;
|
|
54
127
|
}
|
|
128
|
+
this.startupOutputBuffer = "";
|
|
129
|
+
this.hasAutoConfirmedWorkspaceTrustPrompt = false;
|
|
130
|
+
ensureClaudeWorkspaceTrustAccepted(this.options.cwd);
|
|
55
131
|
// Validate transcript file exists before launching Claude CLI.
|
|
56
132
|
// After a compact, the old transcript is deleted and the persisted
|
|
57
133
|
// resumeConversationId becomes invalid, causing --resume to crash.
|
|
@@ -209,6 +285,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
209
285
|
return;
|
|
210
286
|
}
|
|
211
287
|
this.state.lastOutputAt = nowIso();
|
|
288
|
+
if (this.maybeAutoConfirmWorkspaceTrustPrompt(text)) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
212
291
|
if (this.shouldTreatClaudeOutputAsCompactCompletion(text)) {
|
|
213
292
|
this.completeClaudeCompact();
|
|
214
293
|
return;
|
|
@@ -241,6 +320,18 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
241
320
|
return;
|
|
242
321
|
}
|
|
243
322
|
}
|
|
323
|
+
maybeAutoConfirmWorkspaceTrustPrompt(text) {
|
|
324
|
+
if (this.hasAcceptedInput || this.hasAutoConfirmedWorkspaceTrustPrompt) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
this.startupOutputBuffer = `${this.startupOutputBuffer}${text}`.slice(-CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT);
|
|
328
|
+
if (!isClaudeWorkspaceTrustPrompt(this.startupOutputBuffer)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
this.hasAutoConfirmedWorkspaceTrustPrompt = true;
|
|
332
|
+
this.writeToPty("\r");
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
244
335
|
handleExit(exitCode) {
|
|
245
336
|
this.detachLocalTerminal();
|
|
246
337
|
this.clearWechatWorkingNotice(true);
|
|
@@ -7,7 +7,7 @@ import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
|
7
7
|
import * as shared from "./bridge-adapters.shared.js";
|
|
8
8
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
9
9
|
import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
|
|
10
|
-
const { CODEX_APP_SERVER_HOST,
|
|
10
|
+
const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_READY_TIMEOUT_MS, CODEX_FINAL_REPLY_SETTLE_DELAY_MS, CODEX_RECENT_SESSION_KEY_LIMIT, CODEX_RPC_CONNECT_RETRY_MS, CODEX_RPC_RECONNECT_TIMEOUT_MS, CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS, CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS, CODEX_SESSION_POLL_INTERVAL_MS, CODEX_STARTUP_WARMUP_MS, CODEX_THREAD_SIGNAL_TTL_MS, INTERRUPT_SETTLE_DELAY_MS, appendBoundedLog, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexDynamicToolCallFailureResponse, buildCodexMcpServerElicitationDeclineResponse, buildCodexPermissionsRequestApprovalResponse, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getCodexApprovalAutoResponse, getCodexWechatOutboundAttachmentDenyMessage, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
|
|
11
11
|
const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
|
|
12
12
|
export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
13
13
|
runtimeKind = "codex_runtime_host";
|
|
@@ -64,7 +64,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
64
64
|
localClientInstanceId = `${process.pid}-${Date.now().toString(36)}`;
|
|
65
65
|
constructor(options) {
|
|
66
66
|
super(options);
|
|
67
|
-
this.resumeThreadId = options.
|
|
67
|
+
this.resumeThreadId = options.sessionStartMode === "new"
|
|
68
|
+
? null
|
|
69
|
+
: options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
|
|
68
70
|
if (this.resumeThreadId && options.renderMode !== "panel") {
|
|
69
71
|
this.state.sharedSessionId = this.resumeThreadId;
|
|
70
72
|
this.state.sharedThreadId = this.resumeThreadId;
|
|
@@ -20,21 +20,29 @@ export class LocalCompanionProxyAdapter {
|
|
|
20
20
|
reconnectShutdownTimer = null;
|
|
21
21
|
constructor(options) {
|
|
22
22
|
this.options = options;
|
|
23
|
+
const shouldRestoreInitialSession = options.sessionStartMode !== "new";
|
|
24
|
+
const initialSharedSessionId = shouldRestoreInitialSession
|
|
25
|
+
? options.initialSharedSessionId ?? options.initialSharedThreadId
|
|
26
|
+
: undefined;
|
|
23
27
|
this.state = {
|
|
24
28
|
kind: options.kind,
|
|
25
29
|
status: "stopped",
|
|
26
30
|
cwd: options.cwd,
|
|
27
31
|
command: options.command,
|
|
28
32
|
profile: options.profile,
|
|
29
|
-
sharedSessionId:
|
|
33
|
+
sharedSessionId: initialSharedSessionId,
|
|
30
34
|
sharedThreadId: options.kind === "codex" || options.kind === "opencode"
|
|
31
|
-
?
|
|
35
|
+
? initialSharedSessionId
|
|
32
36
|
: undefined,
|
|
33
37
|
activeRuntimeSessionId: options.kind === "claude" || options.kind === "opencode"
|
|
34
|
-
?
|
|
38
|
+
? initialSharedSessionId
|
|
39
|
+
: undefined,
|
|
40
|
+
resumeConversationId: shouldRestoreInitialSession && options.kind === "claude"
|
|
41
|
+
? options.initialResumeConversationId
|
|
42
|
+
: undefined,
|
|
43
|
+
transcriptPath: shouldRestoreInitialSession && options.kind === "claude"
|
|
44
|
+
? options.initialTranscriptPath
|
|
35
45
|
: undefined,
|
|
36
|
-
resumeConversationId: options.kind === "claude" ? options.initialResumeConversationId : undefined,
|
|
37
|
-
transcriptPath: options.kind === "claude" ? options.initialTranscriptPath : undefined,
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
48
|
setEventSink(sink) {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
2
|
import { OPENCODE_SERVER_HOST, OPENCODE_SERVER_READY_TIMEOUT_MS, OPENCODE_SSE_RECONNECT_DELAY_MS, OPENCODE_SESSION_IDLE_SETTLE_MS, OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS, buildCliEnvironment, isRecord, describeUnknownError, resolveSpawnTarget, reserveLocalPort, waitForTcpPort, delay, } from "./bridge-adapters.shared.js";
|
|
3
3
|
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
4
|
-
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE,
|
|
4
|
+
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, buildOneTimeCode, isWechatOutboundAttachmentMutationTool, isWechatOutboundAttachmentWriteCommand, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
|
|
5
5
|
const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPENCODE_DEBUG ?? "");
|
|
6
6
|
const OPENCODE_DUPLICATE_EVENT_TTL_MS = 150;
|
|
7
7
|
const OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS = 30_000;
|
|
8
8
|
const OPENCODE_RECENT_LOCAL_PROMPT_TTL_MS = 10_000;
|
|
9
9
|
const OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS = 5_000;
|
|
10
|
+
const OPENCODE_TUI_SELECT_RETRY_DELAYS_MS = [250, 750, 1_500];
|
|
10
11
|
/* ------------------------------------------------------------------ */
|
|
11
12
|
/* Adapter */
|
|
12
13
|
/* ------------------------------------------------------------------ */
|
|
@@ -82,7 +83,7 @@ export class OpenCodeServerAdapter {
|
|
|
82
83
|
this.startSseListener();
|
|
83
84
|
if (this.options.renderMode === "companion") {
|
|
84
85
|
await this.startNativeClient();
|
|
85
|
-
await this.syncVisibleSessionToShared({ force: true });
|
|
86
|
+
await this.syncVisibleSessionToShared({ force: true, retry: true });
|
|
86
87
|
}
|
|
87
88
|
else {
|
|
88
89
|
this.state.pid = serverProcess.pid;
|
|
@@ -177,6 +178,7 @@ export class OpenCodeServerAdapter {
|
|
|
177
178
|
clearTrackedTurn: true,
|
|
178
179
|
syncVisible: true,
|
|
179
180
|
forceVisibleSync: true,
|
|
181
|
+
retryVisibleSync: true,
|
|
180
182
|
});
|
|
181
183
|
}
|
|
182
184
|
async interrupt() {
|
|
@@ -365,7 +367,7 @@ export class OpenCodeServerAdapter {
|
|
|
365
367
|
if (this.nativeProcess) {
|
|
366
368
|
return;
|
|
367
369
|
}
|
|
368
|
-
const env =
|
|
370
|
+
const env = this.buildNativeClientEnv();
|
|
369
371
|
const attachArgs = await this.buildNativeAttachArgs();
|
|
370
372
|
const target = resolveSpawnTarget(this.options.command, this.options.kind, { env });
|
|
371
373
|
const startedAt = nowIso();
|
|
@@ -410,6 +412,7 @@ export class OpenCodeServerAdapter {
|
|
|
410
412
|
}
|
|
411
413
|
async buildNativeAttachArgs() {
|
|
412
414
|
const args = ["attach", this.getServerUrl()];
|
|
415
|
+
args.push("--dir", this.options.cwd);
|
|
413
416
|
const sessionId = this.activeSessionId;
|
|
414
417
|
if (sessionId && (await this.hasSession(sessionId))) {
|
|
415
418
|
args.push("--session", sessionId);
|
|
@@ -417,6 +420,16 @@ export class OpenCodeServerAdapter {
|
|
|
417
420
|
args.push(...(this.options.extraCliArgs ?? []));
|
|
418
421
|
return args;
|
|
419
422
|
}
|
|
423
|
+
buildNativeClientEnv() {
|
|
424
|
+
const env = buildCliEnvironment(this.options.kind);
|
|
425
|
+
if (this.activeSessionId) {
|
|
426
|
+
env.OPENCODE_ROUTE = JSON.stringify({
|
|
427
|
+
type: "session",
|
|
428
|
+
sessionID: this.activeSessionId,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return env;
|
|
432
|
+
}
|
|
420
433
|
async createSdkClient() {
|
|
421
434
|
try {
|
|
422
435
|
const { createOpencodeClient } = await import("@opencode-ai/sdk/v2");
|
|
@@ -441,6 +454,21 @@ export class OpenCodeServerAdapter {
|
|
|
441
454
|
if (!this.client) {
|
|
442
455
|
return;
|
|
443
456
|
}
|
|
457
|
+
if (this.options.sessionStartMode === "new") {
|
|
458
|
+
const session = this.unwrapOrThrow(await this.client.session.create({
|
|
459
|
+
directory: this.options.cwd,
|
|
460
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
461
|
+
}));
|
|
462
|
+
this.switchSharedSession(session, {
|
|
463
|
+
source: "local",
|
|
464
|
+
reason: "local_follow",
|
|
465
|
+
syncVisible: false,
|
|
466
|
+
});
|
|
467
|
+
this.state.lastSessionSwitchAt = undefined;
|
|
468
|
+
this.state.lastSessionSwitchSource = undefined;
|
|
469
|
+
this.state.lastSessionSwitchReason = undefined;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
444
472
|
let listedSessions = [];
|
|
445
473
|
try {
|
|
446
474
|
const result = await this.client.session.list();
|
|
@@ -1854,7 +1882,9 @@ export class OpenCodeServerAdapter {
|
|
|
1854
1882
|
this.recordSessionSwitch(sessionId, options.source, options.reason, options.notify);
|
|
1855
1883
|
}
|
|
1856
1884
|
if (options.syncVisible !== false && (changed || options.forceVisibleSync)) {
|
|
1857
|
-
void this.syncVisibleSessionSelection(typeof session === "string" ? { id: session } : session
|
|
1885
|
+
void this.syncVisibleSessionSelection(typeof session === "string" ? { id: session } : session, {
|
|
1886
|
+
retry: options.retryVisibleSync,
|
|
1887
|
+
});
|
|
1858
1888
|
}
|
|
1859
1889
|
return changed;
|
|
1860
1890
|
}
|
|
@@ -1865,7 +1895,7 @@ export class OpenCodeServerAdapter {
|
|
|
1865
1895
|
await this.syncVisibleSessionSelection({
|
|
1866
1896
|
id: this.activeSessionId,
|
|
1867
1897
|
workspaceID: this.activeWorkspaceId ?? undefined,
|
|
1868
|
-
}, { force: options.force });
|
|
1898
|
+
}, { force: options.force, retry: options.retry });
|
|
1869
1899
|
}
|
|
1870
1900
|
async syncVisibleSessionSelection(session, options = {}) {
|
|
1871
1901
|
if (!this.client?.tui ||
|
|
@@ -1876,13 +1906,9 @@ export class OpenCodeServerAdapter {
|
|
|
1876
1906
|
this.suppressedTuiSessionSelectId = session.id;
|
|
1877
1907
|
this.logDebug(`[opencode-adapter:tui] selectSession session=${session.id} workspace=${session.workspaceID ?? this.activeWorkspaceId ?? "(none)"}`);
|
|
1878
1908
|
try {
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
sessionID: session.id,
|
|
1883
|
-
});
|
|
1884
|
-
if (result.error !== undefined) {
|
|
1885
|
-
throw result.error;
|
|
1909
|
+
await this.sendVisibleSessionSelection(session);
|
|
1910
|
+
if (options.retry === true) {
|
|
1911
|
+
this.scheduleVisibleSessionSelectionRetries(session);
|
|
1886
1912
|
}
|
|
1887
1913
|
}
|
|
1888
1914
|
catch (err) {
|
|
@@ -1892,6 +1918,33 @@ export class OpenCodeServerAdapter {
|
|
|
1892
1918
|
this.logDebug(`[opencode-adapter:tui] selectSession failed for ${session.id}: ${describeUnknownError(err)}`);
|
|
1893
1919
|
}
|
|
1894
1920
|
}
|
|
1921
|
+
async sendVisibleSessionSelection(session) {
|
|
1922
|
+
if (!this.client?.tui) {
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
const result = await this.client.tui.selectSession({
|
|
1926
|
+
directory: this.options.cwd,
|
|
1927
|
+
workspace: session.workspaceID ?? this.activeWorkspaceId ?? undefined,
|
|
1928
|
+
sessionID: session.id,
|
|
1929
|
+
});
|
|
1930
|
+
if (result.error !== undefined) {
|
|
1931
|
+
throw result.error;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
scheduleVisibleSessionSelectionRetries(session) {
|
|
1935
|
+
for (const retryDelayMs of OPENCODE_TUI_SELECT_RETRY_DELAYS_MS) {
|
|
1936
|
+
setTimeout(() => {
|
|
1937
|
+
if (this.shuttingDown ||
|
|
1938
|
+
this.options.renderMode !== "companion" ||
|
|
1939
|
+
this.activeSessionId !== session.id) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
void this.sendVisibleSessionSelection(session).catch((err) => {
|
|
1943
|
+
this.logDebug(`[opencode-adapter:tui] selectSession retry failed for ${session.id}: ${describeUnknownError(err)}`);
|
|
1944
|
+
});
|
|
1945
|
+
}, retryDelayMs).unref?.();
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1895
1948
|
isLocalSessionNavigationCommand(command) {
|
|
1896
1949
|
if (!command) {
|
|
1897
1950
|
return false;
|
|
@@ -2,13 +2,10 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { spawn as spawnPty } from "node-pty";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { buildClaudeFailureMessage, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeResumeConversationId, findInjectedClaudePromptIndex, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
10
|
-
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, detectCliApproval, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
11
|
-
import { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
|
|
7
|
+
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
|
|
8
|
+
import { getNotificationThreadId, isRecord, } from "./bridge-adapter-common.js";
|
|
12
9
|
export { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
|
|
13
10
|
export const DEFAULT_COLS = 120;
|
|
14
11
|
export const DEFAULT_ROWS = 30;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
const PEER_BRIDGE_EXIT_TIMEOUT_MS = 4_000;
|
|
3
4
|
const PEER_BRIDGE_EXIT_POLL_MS = 100;
|
|
4
5
|
function isPidAlive(pid) {
|
|
@@ -24,7 +25,30 @@ function isRecord(value) {
|
|
|
24
25
|
}
|
|
25
26
|
export function isWechatBridgeCommandLine(commandLine) {
|
|
26
27
|
return (/wechat-bridge\.(?:ts|js|mjs)/i.test(commandLine) &&
|
|
27
|
-
/(?:^|\s)--adapter(
|
|
28
|
+
/(?:^|\s|["'])--adapter(?:(?:["']?\s)|=)/i.test(commandLine));
|
|
29
|
+
}
|
|
30
|
+
export function isWechatDaemonCommandLine(commandLine) {
|
|
31
|
+
return /wechat-daemon\.(?:ts|js|mjs)/i.test(commandLine);
|
|
32
|
+
}
|
|
33
|
+
function normalizeComparablePath(filePath) {
|
|
34
|
+
const normalized = path.resolve(filePath);
|
|
35
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
36
|
+
}
|
|
37
|
+
function extractCommandLineOption(commandLine, optionName) {
|
|
38
|
+
const escaped = optionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
39
|
+
const pattern = new RegExp(`(?:^|\\s|["'])--${escaped}(?:(?:["']?\\s+)|=)(?:"([^"]+)"|'([^']+)'|(\\S+))`, "i");
|
|
40
|
+
const match = pattern.exec(commandLine);
|
|
41
|
+
return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
|
|
42
|
+
}
|
|
43
|
+
export function isWechatDaemonCommandLineForCwd(commandLine, cwd) {
|
|
44
|
+
if (!isWechatDaemonCommandLine(commandLine)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const commandCwd = extractCommandLineOption(commandLine, "cwd");
|
|
48
|
+
if (!commandCwd) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return normalizeComparablePath(commandCwd) === normalizeComparablePath(cwd);
|
|
28
52
|
}
|
|
29
53
|
/**
|
|
30
54
|
* Detects `opencode serve` command lines spawned by the bridge adapter.
|
|
@@ -165,6 +189,21 @@ export function listPeerBridgeProcesses(currentPid = process.pid) {
|
|
|
165
189
|
? listWindowsBridgeProcesses(currentPid)
|
|
166
190
|
: listPosixBridgeProcesses(currentPid);
|
|
167
191
|
}
|
|
192
|
+
export function getProcessRecordByPid(pid, currentPid = process.pid) {
|
|
193
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === currentPid) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return listAllProcessesRaw(currentPid).find((record) => record.pid === pid) ?? null;
|
|
197
|
+
}
|
|
198
|
+
export function listWechatDaemonProcesses(params = {}) {
|
|
199
|
+
const currentPid = params.currentPid ?? process.pid;
|
|
200
|
+
const excludePids = new Set(params.excludePids ?? []);
|
|
201
|
+
return listAllProcessesRaw(currentPid)
|
|
202
|
+
.filter((record) => !excludePids.has(record.pid))
|
|
203
|
+
.filter((record) => params.cwd
|
|
204
|
+
? isWechatDaemonCommandLineForCwd(record.commandLine, params.cwd)
|
|
205
|
+
: isWechatDaemonCommandLine(record.commandLine));
|
|
206
|
+
}
|
|
168
207
|
/**
|
|
169
208
|
* List all managed OpenCode child processes (`serve` and `attach`) whose parent
|
|
170
209
|
* PID is no longer alive. These are orphaned processes left behind when a bridge
|
|
@@ -169,14 +169,17 @@ export class BridgeStateStore {
|
|
|
169
169
|
lifecycle: options.lifecycle,
|
|
170
170
|
};
|
|
171
171
|
this.acquireLock();
|
|
172
|
+
const shouldRestoreSessionState = options.sessionStartMode !== "new";
|
|
172
173
|
const persisted = this.readStateFile();
|
|
173
|
-
const persistedSharedSessionId = resolveRestorableSharedSessionId(persisted, options);
|
|
174
|
-
const persistedResumeConversationId =
|
|
174
|
+
const persistedSharedSessionId = resolveRestorableSharedSessionId(shouldRestoreSessionState ? persisted : null, options);
|
|
175
|
+
const persistedResumeConversationId = shouldRestoreSessionState &&
|
|
176
|
+
options.adapter === "claude" &&
|
|
175
177
|
persisted?.cwd === options.cwd &&
|
|
176
178
|
typeof persisted.resumeConversationId === "string"
|
|
177
179
|
? persisted.resumeConversationId
|
|
178
180
|
: undefined;
|
|
179
|
-
const persistedTranscriptPath =
|
|
181
|
+
const persistedTranscriptPath = shouldRestoreSessionState &&
|
|
182
|
+
options.adapter === "claude" &&
|
|
180
183
|
persisted?.cwd === options.cwd &&
|
|
181
184
|
typeof persisted.transcriptPath === "string"
|
|
182
185
|
? persisted.transcriptPath
|
|
@@ -151,6 +151,7 @@ export function parseCliArgs(argv) {
|
|
|
151
151
|
let cwd = process.cwd();
|
|
152
152
|
let profile;
|
|
153
153
|
let lifecycle = "persistent";
|
|
154
|
+
let sessionStartMode = "restore";
|
|
154
155
|
for (let i = 0; i < argv.length; i += 1) {
|
|
155
156
|
const arg = argv[i];
|
|
156
157
|
const next = argv[i + 1];
|
|
@@ -190,6 +191,13 @@ export function parseCliArgs(argv) {
|
|
|
190
191
|
lifecycle = next;
|
|
191
192
|
i += 1;
|
|
192
193
|
break;
|
|
194
|
+
case "--session-start-mode":
|
|
195
|
+
if (!next || !["restore", "new"].includes(next)) {
|
|
196
|
+
throw new Error(`Invalid session start mode: ${next ?? "(missing)"}`);
|
|
197
|
+
}
|
|
198
|
+
sessionStartMode = next;
|
|
199
|
+
i += 1;
|
|
200
|
+
break;
|
|
193
201
|
case "--shutdown-on-parent-exit":
|
|
194
202
|
lifecycle = "companion_bound";
|
|
195
203
|
break;
|
|
@@ -211,11 +219,12 @@ export function parseCliArgs(argv) {
|
|
|
211
219
|
cwd,
|
|
212
220
|
profile,
|
|
213
221
|
lifecycle,
|
|
222
|
+
sessionStartMode,
|
|
214
223
|
};
|
|
215
224
|
}
|
|
216
225
|
function printUsageAndExit() {
|
|
217
226
|
process.stdout.write([
|
|
218
|
-
"Usage: wechat-bridge --adapter <codex|claude|opencode|shell> [--cmd <executable>] [--cwd <path>] [--profile <name-or-path>] [--lifecycle <persistent|companion_bound>]",
|
|
227
|
+
"Usage: wechat-bridge --adapter <codex|claude|opencode|shell> [--cmd <executable>] [--cwd <path>] [--profile <name-or-path>] [--lifecycle <persistent|companion_bound>] [--session-start-mode <restore|new>]",
|
|
219
228
|
"",
|
|
220
229
|
"Examples:",
|
|
221
230
|
" wechat-bridge-codex",
|
|
@@ -305,6 +314,7 @@ async function main() {
|
|
|
305
314
|
cwd: options.cwd,
|
|
306
315
|
profile: options.profile,
|
|
307
316
|
lifecycle: options.lifecycle,
|
|
317
|
+
sessionStartMode: options.sessionStartMode,
|
|
308
318
|
initialSharedSessionId: stateStore.getState().sharedSessionId ?? stateStore.getState().sharedThreadId,
|
|
309
319
|
initialResumeConversationId: stateStore.getState().resumeConversationId,
|
|
310
320
|
initialTranscriptPath: stateStore.getState().transcriptPath,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { checkForUpdate, getCurrentVersion,
|
|
2
|
+
import { checkForUpdate, getCurrentVersion, } from "../utils/version-checker.js";
|
|
3
3
|
async function main() {
|
|
4
4
|
const currentVersion = await getCurrentVersion();
|
|
5
5
|
console.log(`CLI WeChat Bridge Version Check`);
|
|
@@ -37,6 +37,9 @@ export function formatSwitchFailureMessage(cwd) {
|
|
|
37
37
|
export function formatRestartUnhealthyMessage(cwd) {
|
|
38
38
|
return `Detected unhealthy companion state for ${cwd}. Restarting bridge...`;
|
|
39
39
|
}
|
|
40
|
+
export function defaultSessionStartMode(adapter) {
|
|
41
|
+
return adapter === "claude" || adapter === "opencode" ? "new" : "restore";
|
|
42
|
+
}
|
|
40
43
|
export function decideLaunchAction(input) {
|
|
41
44
|
if (input.lockShouldAutoReclaim) {
|
|
42
45
|
return {
|
|
@@ -55,6 +58,13 @@ export function decideLaunchAction(input) {
|
|
|
55
58
|
failureMessage: formatSwitchFailureMessage(input.runningLock.cwd),
|
|
56
59
|
};
|
|
57
60
|
}
|
|
61
|
+
if (input.sessionStartMode === "new" &&
|
|
62
|
+
(input.requestedAdapter === "claude" || input.requestedAdapter === "opencode")) {
|
|
63
|
+
return {
|
|
64
|
+
kind: "start_bridge",
|
|
65
|
+
message: `Starting a fresh ${input.requestedAdapter} session for ${input.requestedCwd}...`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
58
68
|
if (!input.endpoint || !input.endpointIsReachable) {
|
|
59
69
|
return {
|
|
60
70
|
kind: "restart_unhealthy",
|
|
@@ -99,7 +109,8 @@ export function parseCliArgs(argv) {
|
|
|
99
109
|
" wechat-opencode-start [--cwd <path>] [--profile <name-or-path>] [--timeout-ms <ms>] [...opencode args]",
|
|
100
110
|
" local-companion-start [--adapter <codex|claude|opencode>] [--cwd <path>] [--profile <name-or-path>] [--timeout-ms <ms>] [...cli args]",
|
|
101
111
|
"",
|
|
102
|
-
"Starts
|
|
112
|
+
"Starts a bridge for the current directory, waits for the local endpoint, then opens the visible companion or panel.",
|
|
113
|
+
"Claude and OpenCode launchers start a fresh CLI session by default.",
|
|
103
114
|
"All adapters are companion-bound: closing the companion/panel also stops the bridge.",
|
|
104
115
|
"Unknown arguments are forwarded to the visible CLI client.",
|
|
105
116
|
"",
|
|
@@ -144,7 +155,14 @@ export function parseCliArgs(argv) {
|
|
|
144
155
|
}
|
|
145
156
|
cliArgs.push(arg);
|
|
146
157
|
}
|
|
147
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
adapter,
|
|
160
|
+
cwd,
|
|
161
|
+
profile,
|
|
162
|
+
timeoutMs,
|
|
163
|
+
sessionStartMode: defaultSessionStartMode(adapter),
|
|
164
|
+
cliArgs,
|
|
165
|
+
};
|
|
148
166
|
}
|
|
149
167
|
function isPidAlive(pid) {
|
|
150
168
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
@@ -247,6 +265,9 @@ export function buildBackgroundBridgeArgs(entryPath, options) {
|
|
|
247
265
|
args.push("--experimental-strip-types");
|
|
248
266
|
}
|
|
249
267
|
args.push(entryPath, "--adapter", options.adapter, "--cwd", options.cwd, "--lifecycle", lifecycle);
|
|
268
|
+
if (options.sessionStartMode !== "restore") {
|
|
269
|
+
args.push("--session-start-mode", options.sessionStartMode);
|
|
270
|
+
}
|
|
250
271
|
if (options.profile) {
|
|
251
272
|
args.push("--profile", options.profile);
|
|
252
273
|
}
|
|
@@ -304,6 +325,7 @@ async function ensureBridgeReady(options) {
|
|
|
304
325
|
endpoint: endpointResult.endpoint,
|
|
305
326
|
endpointIsReachable: Boolean(endpointResult.endpoint),
|
|
306
327
|
companionIsAlive: isCompanionAlive(endpointResult.endpoint),
|
|
328
|
+
sessionStartMode: options.sessionStartMode,
|
|
307
329
|
});
|
|
308
330
|
log(options.adapter, decision.message);
|
|
309
331
|
if (decision.kind === "already_active") {
|
|
@@ -355,6 +377,8 @@ export async function tryDelegateToDaemon(options, deps = {}) {
|
|
|
355
377
|
profile: options.profile,
|
|
356
378
|
cliArgs: options.cliArgs,
|
|
357
379
|
openVisible: true,
|
|
380
|
+
sessionStartMode: options.sessionStartMode,
|
|
381
|
+
reuseExistingVisible: true,
|
|
358
382
|
});
|
|
359
383
|
if (!response.ok) {
|
|
360
384
|
throw new Error(response.error);
|
|
@@ -374,6 +398,7 @@ export async function runVisibleClient(options, runners = {}) {
|
|
|
374
398
|
return await (runners.localCompanion ?? runLocalCompanion)({
|
|
375
399
|
adapter: options.adapter,
|
|
376
400
|
cwd: options.cwd,
|
|
401
|
+
sessionStartMode: options.sessionStartMode,
|
|
377
402
|
cliArgs: options.cliArgs,
|
|
378
403
|
});
|
|
379
404
|
}
|
|
@@ -13,6 +13,7 @@ function log(adapter, message) {
|
|
|
13
13
|
function parseCliArgs(argv) {
|
|
14
14
|
let adapter = null;
|
|
15
15
|
let cwd = process.cwd();
|
|
16
|
+
let sessionStartMode = "restore";
|
|
16
17
|
const cliArgs = [];
|
|
17
18
|
for (let i = 0; i < argv.length; i += 1) {
|
|
18
19
|
const arg = argv[i];
|
|
@@ -46,12 +47,20 @@ function parseCliArgs(argv) {
|
|
|
46
47
|
i += 1;
|
|
47
48
|
continue;
|
|
48
49
|
}
|
|
50
|
+
if (arg === "--session-start-mode") {
|
|
51
|
+
if (!next || !["restore", "new"].includes(next)) {
|
|
52
|
+
throw new Error(`Invalid session start mode: ${next ?? "(missing)"}`);
|
|
53
|
+
}
|
|
54
|
+
sessionStartMode = next;
|
|
55
|
+
i += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
49
58
|
cliArgs.push(arg);
|
|
50
59
|
}
|
|
51
60
|
if (!adapter) {
|
|
52
61
|
throw new Error("Missing required --adapter <codex|claude|opencode>");
|
|
53
62
|
}
|
|
54
|
-
return { adapter, cwd, cliArgs };
|
|
63
|
+
return { adapter, cwd, sessionStartMode, cliArgs };
|
|
55
64
|
}
|
|
56
65
|
function delay(ms) {
|
|
57
66
|
if (ms <= 0) {
|
|
@@ -79,14 +88,22 @@ export async function runLocalCompanion(options) {
|
|
|
79
88
|
extraCliArgs: options.cliArgs,
|
|
80
89
|
});
|
|
81
90
|
}
|
|
91
|
+
const shouldRestoreInitialSession = options.sessionStartMode !== "new";
|
|
82
92
|
const adapter = createBridgeAdapter({
|
|
83
93
|
kind: initialEndpoint.kind,
|
|
84
94
|
command: initialEndpoint.command,
|
|
85
95
|
cwd: initialEndpoint.cwd,
|
|
86
96
|
profile: initialEndpoint.profile,
|
|
87
|
-
initialSharedSessionId:
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
initialSharedSessionId: shouldRestoreInitialSession
|
|
98
|
+
? initialEndpoint.sharedSessionId ?? initialEndpoint.sharedThreadId
|
|
99
|
+
: undefined,
|
|
100
|
+
initialResumeConversationId: shouldRestoreInitialSession
|
|
101
|
+
? initialEndpoint.resumeConversationId
|
|
102
|
+
: undefined,
|
|
103
|
+
initialTranscriptPath: shouldRestoreInitialSession
|
|
104
|
+
? initialEndpoint.transcriptPath
|
|
105
|
+
: undefined,
|
|
106
|
+
sessionStartMode: options.sessionStartMode,
|
|
90
107
|
renderMode: initialEndpoint.kind === "codex" ? "panel" : "companion",
|
|
91
108
|
extraCliArgs: options.cliArgs,
|
|
92
109
|
});
|