@unlinearity/cli-wechat-bridge 1.1.6 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +661 -661
- package/README.md +468 -475
- package/bin/_run-entry.mjs +29 -6
- package/bin/wechat-check-update.mjs +6 -1
- package/bin/wechat-claude.mjs +6 -1
- package/bin/wechat-codex.mjs +6 -1
- package/bin/wechat-daemon.mjs +6 -1
- package/bin/wechat-opencode.mjs +6 -1
- package/bin/wechat-pi.mjs +6 -1
- package/bin/wechat-setup.mjs +6 -1
- package/bin/wecom-check-update.mjs +10 -0
- package/bin/wecom-claude.mjs +10 -0
- package/bin/wecom-codex.mjs +10 -0
- package/bin/wecom-daemon.mjs +10 -0
- package/bin/wecom-opencode.mjs +10 -0
- package/bin/wecom-pi.mjs +10 -0
- package/bin/wecom-setup.mjs +10 -0
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +161 -4
- package/dist/bridge/bridge-adapters.codex.js +206 -14
- package/dist/bridge/bridge-adapters.core.js +25 -1
- package/dist/bridge/bridge-adapters.opencode.js +298 -20
- package/dist/bridge/bridge-adapters.pi.js +45 -0
- package/dist/bridge/bridge-adapters.shared.js +6 -0
- package/dist/bridge/bridge-controller.js +6 -0
- package/dist/bridge/bridge-final-reply.js +6 -0
- package/dist/bridge/bridge-process-reaper.js +41 -2
- package/dist/bridge/bridge-state.js +16 -3
- package/dist/bridge/bridge-utils.js +6 -31
- package/dist/bridge/claude-hook.js +6 -0
- package/dist/bridge/claude-native-controls.js +145 -0
- package/dist/bridge/codex-visible-thread-proxy.js +140 -0
- package/dist/bridge/native-terminal-control.js +271 -0
- package/dist/bridge/opencode-v2-compat.js +450 -0
- package/dist/bridge/wechat-bridge.js +380 -171
- package/dist/bridge/wechat-forwarding.js +6 -0
- package/dist/bridge/wechat-outbound-queue.js +6 -0
- package/dist/channels/wechat/bridge-final-reply.js +13 -22
- package/dist/channels/wechat/index.js +6 -0
- package/dist/channels/wechat/wechat-driver.js +191 -0
- package/dist/channels/wechat/wechat-forwarding.js +18 -0
- package/dist/channels/wechat/wechat-outbound-queue.js +49 -3
- package/dist/channels/wecom/bridge-final-reply.js +32 -0
- package/dist/channels/wecom/setup-cli.js +12 -0
- package/dist/channels/wecom/setup.js +182 -0
- package/dist/channels/wecom/wecom-channel-port.js +45 -0
- package/dist/channels/wecom/wecom-config.js +72 -0
- package/dist/channels/wecom/wecom-driver.js +106 -0
- package/dist/channels/wecom/wecom-media.js +147 -0
- package/dist/channels/wecom/wecom-message.js +149 -0
- package/dist/channels/wecom/wecom-transport.js +314 -0
- package/dist/commands/check-update.js +6 -0
- package/dist/companion/codex-remote-client.js +30 -3
- package/dist/companion/codex-visible-client-link.js +107 -0
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion-start.js +62 -11
- package/dist/companion/local-companion.js +25 -1
- package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
- package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
- package/dist/companion/pi-tui-bridge-extension.js +62 -3
- package/dist/core/bridge-control.js +10 -0
- package/dist/core/bridge-defer.js +25 -0
- package/dist/core/channel-driver.js +1 -0
- package/dist/core/channel-types.js +3 -1
- package/dist/core/conversation-routing.js +58 -0
- package/dist/core/direct-run.js +23 -0
- package/dist/core/inbound-message-claims.js +67 -0
- package/dist/core/outbound-target.js +32 -0
- package/dist/core/text-utils.js +6 -0
- package/dist/core/turn-coordinator.js +85 -0
- package/dist/daemon/daemon-link.js +9 -0
- package/dist/daemon/emoji-bindings.js +6 -0
- package/dist/daemon/wechat-daemon.js +675 -122
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages-en.js +9 -1
- package/dist/i18n/messages-zh.js +9 -1
- package/dist/runtime/create-runtime-host.js +6 -0
- package/dist/runtime/legacy-adapter-runtime.js +15 -0
- package/dist/runtime/runtime-types.js +3 -0
- package/dist/utils/atomic-file.js +6 -0
- package/dist/utils/doctor.js +36 -10
- package/dist/utils/version-checker.js +17 -11
- package/dist/wechat/channel-config.js +6 -0
- package/dist/wechat/setup.js +119 -13
- package/dist/wechat/wechat-channel.js +6 -0
- package/dist/wechat/wechat-transport.js +169 -58
- package/package.json +16 -4
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
8
|
import { randomBytes } from "node:crypto";
|
|
3
9
|
import fs from "node:fs";
|
|
@@ -6,6 +12,7 @@ import path from "node:path";
|
|
|
6
12
|
import { fileURLToPath } from "node:url";
|
|
7
13
|
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, OPENCODE_HTTP_READY_TIMEOUT_MS, OPENCODE_HTTP_READY_PROBE_TIMEOUT_MS, OPENCODE_HTTP_READY_PROBE_INTERVAL_MS, buildCliEnvironment, isRecord, describeUnknownError, resolveSpawnTarget, reserveLocalPort, waitForTcpPort, delay, } from "./bridge-adapters.shared.js";
|
|
8
14
|
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
15
|
+
import { createOpenCodeV2CompatClient } from "./opencode-v2-compat.js";
|
|
9
16
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
10
17
|
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, buildOneTimeCode, isWechatOutboundAttachmentMutationTool, isWechatOutboundAttachmentWriteCommand, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
|
|
11
18
|
const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPENCODE_DEBUG ?? "");
|
|
@@ -18,7 +25,11 @@ const TUI_SESSION_SELECT_SUPPRESSION_TTL_MS = 5_000;
|
|
|
18
25
|
const OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS = 5_000;
|
|
19
26
|
const OPENCODE_TUI_SELECT_RETRY_DELAYS_MS = [250, 750, 1_500];
|
|
20
27
|
const OPENCODE_MINIMUM_SUPPORTED_VERSION = "1.18.0";
|
|
21
|
-
const OPENCODE_MAXIMUM_SUPPORTED_MAJOR =
|
|
28
|
+
const OPENCODE_MAXIMUM_SUPPORTED_MAJOR = 3;
|
|
29
|
+
const OPENCODE_VERSION_PROBE_TIMEOUT_MS = 5_000;
|
|
30
|
+
export function parseOpenCodeCliVersion(output) {
|
|
31
|
+
return /\bv?(\d+\.\d+\.\d+)\b/.exec(output)?.[1] ?? null;
|
|
32
|
+
}
|
|
22
33
|
const OPENCODE_TUI_ROUTE_READY_TIMEOUT_MS = 10_000;
|
|
23
34
|
const OPENCODE_TUI_ROUTE_MAX_BUFFER_SIZE = 1024 * 1024;
|
|
24
35
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
@@ -77,6 +88,8 @@ export class OpenCodeServerAdapter {
|
|
|
77
88
|
tuiRoutePort = 0;
|
|
78
89
|
tuiRouteConfigDir = null;
|
|
79
90
|
tuiRouteConfigPath = null;
|
|
91
|
+
tuiRoutePluginPath = null;
|
|
92
|
+
openCodeMajor = 1;
|
|
80
93
|
visibleTuiSessionId = null;
|
|
81
94
|
tuiRouteStateReceived = false;
|
|
82
95
|
expectedVisibleTuiSessionId = null;
|
|
@@ -85,6 +98,139 @@ export class OpenCodeServerAdapter {
|
|
|
85
98
|
pendingLocalSessionCreateFollowUntilMs = 0;
|
|
86
99
|
pendingPermission = null;
|
|
87
100
|
pendingQuestion = null;
|
|
101
|
+
previousAgentBySession = new Map();
|
|
102
|
+
async listModels() {
|
|
103
|
+
this.assertRemoteControlReady();
|
|
104
|
+
const session = await this.ensureControlSession();
|
|
105
|
+
const models = await this.listControlModels();
|
|
106
|
+
return models.map((model) => ({
|
|
107
|
+
id: model.id,
|
|
108
|
+
displayName: model.displayName,
|
|
109
|
+
isCurrent: session.model?.providerID === model.providerID &&
|
|
110
|
+
session.model.id === model.modelID,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
async selectModel(modelId) {
|
|
114
|
+
this.assertRemoteControlReady();
|
|
115
|
+
const session = await this.ensureControlSession();
|
|
116
|
+
const models = await this.listControlModels();
|
|
117
|
+
const target = models.find((model) => model.id === modelId);
|
|
118
|
+
if (!target) {
|
|
119
|
+
throw new Error("This OpenCode model is no longer available. Send /model again.");
|
|
120
|
+
}
|
|
121
|
+
const currentVariant = session.model?.variant;
|
|
122
|
+
const variant = currentVariant && currentVariant !== "default" && target.variants.includes(currentVariant)
|
|
123
|
+
? currentVariant
|
|
124
|
+
: "default";
|
|
125
|
+
const result = await this.client.v2.session.switchModel({
|
|
126
|
+
sessionID: session.id,
|
|
127
|
+
model: {
|
|
128
|
+
providerID: target.providerID,
|
|
129
|
+
id: target.modelID,
|
|
130
|
+
variant,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
this.unwrapOrThrow(result);
|
|
134
|
+
const updated = await this.getSessionForCurrentDirectory(session.id);
|
|
135
|
+
if (!updated ||
|
|
136
|
+
updated.model?.providerID !== target.providerID ||
|
|
137
|
+
updated.model.id !== target.modelID ||
|
|
138
|
+
(updated.model.variant ?? "default") !== variant) {
|
|
139
|
+
throw new Error("OpenCode did not confirm the selected model.");
|
|
140
|
+
}
|
|
141
|
+
return { id: target.id, displayName: target.displayName, isCurrent: true };
|
|
142
|
+
}
|
|
143
|
+
async setPlanMode(enabled) {
|
|
144
|
+
this.assertRemoteControlReady();
|
|
145
|
+
const session = await this.ensureControlSession();
|
|
146
|
+
const agents = await this.listPrimaryAgents();
|
|
147
|
+
const planAgent = agents.find((agent) => agent.name === "plan");
|
|
148
|
+
if (!planAgent) {
|
|
149
|
+
throw new Error("OpenCode primary agent 'plan' is not available.");
|
|
150
|
+
}
|
|
151
|
+
const currentAgent = session.agent ?? agents[0]?.name;
|
|
152
|
+
if (!currentAgent) {
|
|
153
|
+
throw new Error("OpenCode did not return any primary agents.");
|
|
154
|
+
}
|
|
155
|
+
if (enabled && currentAgent === "plan") {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (!enabled && currentAgent !== "plan") {
|
|
159
|
+
this.previousAgentBySession.delete(session.id);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const previousAgent = this.previousAgentBySession.get(session.id);
|
|
163
|
+
const target = enabled
|
|
164
|
+
? "plan"
|
|
165
|
+
: agents.some((agent) => agent.name === previousAgent)
|
|
166
|
+
? previousAgent
|
|
167
|
+
: agents.find((agent) => agent.name === "build")?.name ??
|
|
168
|
+
agents.find((agent) => agent.name !== "plan")?.name;
|
|
169
|
+
if (!target) {
|
|
170
|
+
throw new Error("OpenCode did not return an agent that can replace plan mode.");
|
|
171
|
+
}
|
|
172
|
+
const result = await this.client.v2.session.switchAgent({
|
|
173
|
+
sessionID: session.id,
|
|
174
|
+
agent: target,
|
|
175
|
+
});
|
|
176
|
+
this.unwrapOrThrow(result);
|
|
177
|
+
const updated = await this.getSessionForCurrentDirectory(session.id);
|
|
178
|
+
if (!updated || updated.agent !== target) {
|
|
179
|
+
throw new Error("OpenCode did not confirm the selected agent.");
|
|
180
|
+
}
|
|
181
|
+
if (enabled) {
|
|
182
|
+
this.previousAgentBySession.set(session.id, currentAgent);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
this.previousAgentBySession.delete(session.id);
|
|
186
|
+
}
|
|
187
|
+
return enabled;
|
|
188
|
+
}
|
|
189
|
+
assertRemoteControlReady() {
|
|
190
|
+
if (!this.client) {
|
|
191
|
+
throw new Error("OpenCode is not running.");
|
|
192
|
+
}
|
|
193
|
+
if (this.state.status === "busy") {
|
|
194
|
+
throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
|
|
195
|
+
}
|
|
196
|
+
if (this.pendingPermission) {
|
|
197
|
+
throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
|
|
198
|
+
}
|
|
199
|
+
if (this.pendingQuestion) {
|
|
200
|
+
throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async ensureControlSession() {
|
|
204
|
+
const session = await this.ensureSession();
|
|
205
|
+
if (session.id !== this.activeSessionId) {
|
|
206
|
+
this.switchSharedSession(session, {
|
|
207
|
+
source: "wechat",
|
|
208
|
+
reason: "wechat_resume",
|
|
209
|
+
syncVisible: true,
|
|
210
|
+
forceVisibleSync: true,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return session;
|
|
214
|
+
}
|
|
215
|
+
async listPrimaryAgents() {
|
|
216
|
+
if (!this.client)
|
|
217
|
+
throw new Error("OpenCode is not running.");
|
|
218
|
+
const agents = this.unwrapOrThrow(await this.client.app.agents({
|
|
219
|
+
directory: this.options.cwd,
|
|
220
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
221
|
+
}, { signal: AbortSignal.timeout(10_000) }));
|
|
222
|
+
return agents.filter((agent) => agent.mode !== "subagent" && !agent.hidden);
|
|
223
|
+
}
|
|
224
|
+
async listControlModels() {
|
|
225
|
+
if (!this.client)
|
|
226
|
+
throw new Error("OpenCode is not running.");
|
|
227
|
+
const providers = this.unwrapOrThrow(await this.client.provider.list({ directory: this.options.cwd, workspace: this.activeWorkspaceId ?? undefined }, { signal: AbortSignal.timeout(10_000) }));
|
|
228
|
+
return providers.all.filter((provider) => providers.connected.includes(provider.id)).flatMap((provider) => Object.entries(provider.models).filter(([id, model]) => model.status !== "deprecated" && !(provider.id === "opencode" && id.includes("-nano"))).map(([id, model]) => ({
|
|
229
|
+
id: `${provider.id}/${id}`, displayName: `${model.name || id} (${provider.name})`,
|
|
230
|
+
providerID: provider.id, modelID: id,
|
|
231
|
+
variants: Object.keys(model.variants ?? {}),
|
|
232
|
+
})));
|
|
233
|
+
}
|
|
88
234
|
constructor(options) {
|
|
89
235
|
this.options = options;
|
|
90
236
|
this.state = {
|
|
@@ -111,6 +257,7 @@ export class OpenCodeServerAdapter {
|
|
|
111
257
|
this.shuttingDown = false;
|
|
112
258
|
this.setStatus("starting", "Starting OpenCode companion...");
|
|
113
259
|
try {
|
|
260
|
+
await this.probeOpenCodeVersion();
|
|
114
261
|
this.serverPort = await reserveLocalPort();
|
|
115
262
|
const serverProcess = await this.startServerProcess();
|
|
116
263
|
await waitForTcpPort(OPENCODE_SERVER_HOST, this.serverPort, OPENCODE_SERVER_READY_TIMEOUT_MS);
|
|
@@ -170,10 +317,21 @@ export class OpenCodeServerAdapter {
|
|
|
170
317
|
this.recordPendingWechatPromptMirrorSuppression(session.id, normalized);
|
|
171
318
|
this.beginTrackedTurn(normalized, "wechat");
|
|
172
319
|
try {
|
|
320
|
+
const selection = await this.getSessionForCurrentDirectory(session.id) ?? session;
|
|
173
321
|
const result = await this.client.session.promptAsync({
|
|
174
322
|
sessionID: session.id,
|
|
175
323
|
directory: this.options.cwd,
|
|
176
324
|
workspace: session.workspaceID ?? this.activeWorkspaceId ?? undefined,
|
|
325
|
+
...(selection.agent ? { agent: selection.agent } : {}),
|
|
326
|
+
...(selection.model
|
|
327
|
+
? {
|
|
328
|
+
model: {
|
|
329
|
+
providerID: selection.model.providerID,
|
|
330
|
+
modelID: selection.model.id,
|
|
331
|
+
},
|
|
332
|
+
variant: selection.model.variant ?? "default",
|
|
333
|
+
}
|
|
334
|
+
: {}),
|
|
177
335
|
parts: [{ type: "text", text: normalized }],
|
|
178
336
|
});
|
|
179
337
|
if (result.error !== undefined) {
|
|
@@ -411,6 +569,7 @@ export class OpenCodeServerAdapter {
|
|
|
411
569
|
this.pendingLocalSessionCreateFollowUntilMs = 0;
|
|
412
570
|
this.clearObservedMessageTracking();
|
|
413
571
|
this.recentSdkEventObservations.clear();
|
|
572
|
+
this.previousAgentBySession.clear();
|
|
414
573
|
this.outputBatcher.clear();
|
|
415
574
|
this.clearStreamedPartState();
|
|
416
575
|
this.clearPendingPermissionState();
|
|
@@ -481,13 +640,28 @@ export class OpenCodeServerAdapter {
|
|
|
481
640
|
resolve(address.port);
|
|
482
641
|
});
|
|
483
642
|
});
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const pluginPath = path.join(MODULE_DIR, "..", "companion",
|
|
643
|
+
const pluginName = this.openCodeMajor >= 2
|
|
644
|
+
? "opencode-v2-tui-bridge-plugin"
|
|
645
|
+
: "opencode-tui-bridge-plugin";
|
|
646
|
+
const pluginPath = path.join(MODULE_DIR, "..", "companion", `${pluginName}${RUNTIME_ENTRY_EXTENSION}`);
|
|
488
647
|
if (!fs.existsSync(pluginPath)) {
|
|
489
648
|
throw new Error(`OpenCode TUI bridge plugin was not found: ${pluginPath}`);
|
|
490
649
|
}
|
|
650
|
+
const workspaceDir = ensureWorkspaceChannelDir(this.options.cwd).workspaceDir;
|
|
651
|
+
const configDir = fs.mkdtempSync(path.join(workspaceDir, "opencode-tui-route-"));
|
|
652
|
+
this.tuiRouteConfigDir = configDir;
|
|
653
|
+
if (this.openCodeMajor >= 2) {
|
|
654
|
+
fs.copyFileSync(pluginPath, path.join(configDir, `tui${RUNTIME_ENTRY_EXTENSION}`));
|
|
655
|
+
fs.writeFileSync(path.join(configDir, "package.json"), `${JSON.stringify({
|
|
656
|
+
name: "cli-wechat-bridge-opencode-tui-route",
|
|
657
|
+
private: true,
|
|
658
|
+
type: "module",
|
|
659
|
+
}, null, 2)}\n`, "utf8");
|
|
660
|
+
this.tuiRoutePluginPath = configDir;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
this.tuiRoutePluginPath = pluginPath;
|
|
664
|
+
const configPath = path.join(configDir, "tui.json");
|
|
491
665
|
fs.writeFileSync(configPath, `${JSON.stringify({
|
|
492
666
|
plugin: [
|
|
493
667
|
[
|
|
@@ -499,7 +673,6 @@ export class OpenCodeServerAdapter {
|
|
|
499
673
|
],
|
|
500
674
|
],
|
|
501
675
|
}, null, 2)}\n`, "utf8");
|
|
502
|
-
this.tuiRouteConfigDir = configDir;
|
|
503
676
|
this.tuiRouteConfigPath = configPath;
|
|
504
677
|
}
|
|
505
678
|
acceptTuiRouteSocket(socket) {
|
|
@@ -614,6 +787,7 @@ export class OpenCodeServerAdapter {
|
|
|
614
787
|
const configDir = this.tuiRouteConfigDir;
|
|
615
788
|
this.tuiRouteConfigDir = null;
|
|
616
789
|
this.tuiRouteConfigPath = null;
|
|
790
|
+
this.tuiRoutePluginPath = null;
|
|
617
791
|
this.tuiRoutePort = 0;
|
|
618
792
|
this.tuiRouteToken = "";
|
|
619
793
|
this.tuiRouteStateReceived = false;
|
|
@@ -630,6 +804,50 @@ export class OpenCodeServerAdapter {
|
|
|
630
804
|
}
|
|
631
805
|
}
|
|
632
806
|
}
|
|
807
|
+
async probeOpenCodeVersion() {
|
|
808
|
+
const env = buildCliEnvironment(this.options.kind);
|
|
809
|
+
const target = resolveSpawnTarget(this.options.command, this.options.kind, { env });
|
|
810
|
+
const version = await new Promise((resolve, reject) => {
|
|
811
|
+
const child = spawnChildProcess(target.file, [...target.args, "--version"], {
|
|
812
|
+
cwd: this.options.cwd,
|
|
813
|
+
env,
|
|
814
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
815
|
+
windowsHide: true,
|
|
816
|
+
});
|
|
817
|
+
let output = "";
|
|
818
|
+
let settled = false;
|
|
819
|
+
const finish = (error) => {
|
|
820
|
+
if (settled)
|
|
821
|
+
return;
|
|
822
|
+
settled = true;
|
|
823
|
+
clearTimeout(timer);
|
|
824
|
+
if (error)
|
|
825
|
+
reject(error);
|
|
826
|
+
else
|
|
827
|
+
resolve(output);
|
|
828
|
+
};
|
|
829
|
+
const timer = setTimeout(() => {
|
|
830
|
+
if (child.pid != null) {
|
|
831
|
+
killProcessTreeSync(child.pid);
|
|
832
|
+
}
|
|
833
|
+
finish(new Error("Timed out checking the OpenCode CLI version."));
|
|
834
|
+
}, OPENCODE_VERSION_PROBE_TIMEOUT_MS);
|
|
835
|
+
child.stdout?.on("data", (chunk) => { output += chunk.toString("utf8"); });
|
|
836
|
+
child.stderr?.on("data", (chunk) => { output += chunk.toString("utf8"); });
|
|
837
|
+
child.once("error", (error) => finish(error));
|
|
838
|
+
child.once("exit", (code) => {
|
|
839
|
+
if (code === 0)
|
|
840
|
+
finish();
|
|
841
|
+
else
|
|
842
|
+
finish(new Error(`OpenCode --version exited with code ${code ?? "unknown"}.`));
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
const parsed = parseOpenCodeCliVersion(version);
|
|
846
|
+
if (!parsed || !this.isSupportedOpenCodeVersion(parsed)) {
|
|
847
|
+
throw new Error(`Unsupported OpenCode version ${parsed ?? (version.trim() || "unknown")}. Install OpenCode >=${OPENCODE_MINIMUM_SUPPORTED_VERSION} <${OPENCODE_MAXIMUM_SUPPORTED_MAJOR}.0.0.`);
|
|
848
|
+
}
|
|
849
|
+
this.openCodeMajor = Number(parsed.split(".")[0]);
|
|
850
|
+
}
|
|
633
851
|
async startServerProcess() {
|
|
634
852
|
const env = buildCliEnvironment(this.options.kind);
|
|
635
853
|
this.serverPassword = randomBytes(24).toString("hex");
|
|
@@ -735,8 +953,9 @@ export class OpenCodeServerAdapter {
|
|
|
735
953
|
if ((this.options.extraCliArgs ?? []).includes("--pure")) {
|
|
736
954
|
throw new Error("OpenCode --pure cannot be used with the WeChat bridge because local session following requires the managed TUI plugin.");
|
|
737
955
|
}
|
|
738
|
-
const args =
|
|
739
|
-
|
|
956
|
+
const args = this.openCodeMajor >= 2
|
|
957
|
+
? ["--server", this.getServerUrl()]
|
|
958
|
+
: ["attach", this.getServerUrl(), "--dir", this.options.cwd];
|
|
740
959
|
const sessionId = this.activeSessionId;
|
|
741
960
|
if (sessionId && (await this.hasSession(sessionId))) {
|
|
742
961
|
args.push("--session", sessionId);
|
|
@@ -746,7 +965,37 @@ export class OpenCodeServerAdapter {
|
|
|
746
965
|
}
|
|
747
966
|
buildNativeClientEnv() {
|
|
748
967
|
const env = buildCliEnvironment(this.options.kind);
|
|
749
|
-
if (this.
|
|
968
|
+
if (this.openCodeMajor >= 2 && this.tuiRoutePluginPath) {
|
|
969
|
+
let existingConfig = {};
|
|
970
|
+
if (env.OPENCODE_CLI_CONFIG_CONTENT) {
|
|
971
|
+
try {
|
|
972
|
+
const parsed = JSON.parse(env.OPENCODE_CLI_CONFIG_CONTENT);
|
|
973
|
+
if (isRecord(parsed))
|
|
974
|
+
existingConfig = parsed;
|
|
975
|
+
}
|
|
976
|
+
catch (error) {
|
|
977
|
+
throw new Error(`OpenCode cannot merge the managed TUI plugin into invalid OPENCODE_CLI_CONFIG_CONTENT: ${describeUnknownError(error)}`, { cause: error });
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const existingPlugins = Array.isArray(existingConfig.plugins)
|
|
981
|
+
? existingConfig.plugins
|
|
982
|
+
: [];
|
|
983
|
+
env.OPENCODE_CLI_CONFIG_CONTENT = JSON.stringify({
|
|
984
|
+
...existingConfig,
|
|
985
|
+
plugins: [
|
|
986
|
+
...existingPlugins,
|
|
987
|
+
{
|
|
988
|
+
package: this.tuiRoutePluginPath,
|
|
989
|
+
options: {
|
|
990
|
+
port: this.tuiRoutePort,
|
|
991
|
+
token: this.tuiRouteToken,
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
});
|
|
996
|
+
env.OPENCODE_PASSWORD = this.serverPassword ?? "";
|
|
997
|
+
}
|
|
998
|
+
else if (this.tuiRouteConfigDir && this.tuiRouteConfigPath) {
|
|
750
999
|
if (env.OPENCODE_CONFIG_DIR && env.OPENCODE_TUI_CONFIG) {
|
|
751
1000
|
throw new Error("OpenCode cannot inject the managed TUI session plugin while both OPENCODE_CONFIG_DIR and OPENCODE_TUI_CONFIG are set.");
|
|
752
1001
|
}
|
|
@@ -760,7 +1009,7 @@ export class OpenCodeServerAdapter {
|
|
|
760
1009
|
if (this.serverPassword) {
|
|
761
1010
|
env.OPENCODE_SERVER_PASSWORD = this.serverPassword;
|
|
762
1011
|
}
|
|
763
|
-
if (this.activeSessionId) {
|
|
1012
|
+
if (this.activeSessionId && this.openCodeMajor < 2) {
|
|
764
1013
|
env.OPENCODE_ROUTE = JSON.stringify({
|
|
765
1014
|
type: "session",
|
|
766
1015
|
sessionID: this.activeSessionId,
|
|
@@ -777,6 +1026,14 @@ export class OpenCodeServerAdapter {
|
|
|
777
1026
|
}
|
|
778
1027
|
async createSdkClient() {
|
|
779
1028
|
try {
|
|
1029
|
+
if (this.openCodeMajor >= 2) {
|
|
1030
|
+
this.client = await createOpenCodeV2CompatClient({
|
|
1031
|
+
baseUrl: `http://${OPENCODE_SERVER_HOST}:${this.serverPort}`,
|
|
1032
|
+
directory: this.options.cwd,
|
|
1033
|
+
headers: this.buildServerAuthHeaders(),
|
|
1034
|
+
});
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
780
1037
|
const { createOpencodeClient } = await import("@opencode-ai/sdk/v2");
|
|
781
1038
|
this.client = createOpencodeClient({
|
|
782
1039
|
baseUrl: `http://${OPENCODE_SERVER_HOST}:${this.serverPort}`,
|
|
@@ -786,7 +1043,8 @@ export class OpenCodeServerAdapter {
|
|
|
786
1043
|
});
|
|
787
1044
|
}
|
|
788
1045
|
catch (err) {
|
|
789
|
-
|
|
1046
|
+
const packageName = this.openCodeMajor >= 2 ? "@opencode/client" : "@opencode-ai/sdk";
|
|
1047
|
+
throw new Error(`Failed to load ${packageName}. Make sure it is installed: ${describeUnknownError(err)}`, { cause: err });
|
|
790
1048
|
}
|
|
791
1049
|
}
|
|
792
1050
|
async checkHealth() {
|
|
@@ -801,17 +1059,24 @@ export class OpenCodeServerAdapter {
|
|
|
801
1059
|
let lastError;
|
|
802
1060
|
while (Date.now() < deadline) {
|
|
803
1061
|
try {
|
|
804
|
-
const
|
|
1062
|
+
const healthPath = this.openCodeMajor >= 2 ? "/api/info" : "/global/health";
|
|
1063
|
+
const response = await fetch(`${baseUrl}${healthPath}`, {
|
|
805
1064
|
headers: this.buildServerAuthHeaders(),
|
|
806
1065
|
signal: AbortSignal.timeout(OPENCODE_HTTP_READY_PROBE_TIMEOUT_MS),
|
|
807
1066
|
});
|
|
808
1067
|
if (response.ok) {
|
|
809
1068
|
const health = await response.json();
|
|
810
|
-
|
|
1069
|
+
const version = isRecord(health) && typeof health.version === "string"
|
|
1070
|
+
? health.version
|
|
1071
|
+
: undefined;
|
|
1072
|
+
const healthy = this.openCodeMajor >= 2
|
|
1073
|
+
? Boolean(version)
|
|
1074
|
+
: isRecord(health) && health.healthy === true;
|
|
1075
|
+
if (!healthy || !version) {
|
|
811
1076
|
lastError = new Error("OpenCode health check returned an invalid response.");
|
|
812
1077
|
}
|
|
813
|
-
else if (!this.isSupportedOpenCodeVersion(
|
|
814
|
-
throw new Error(`Unsupported OpenCode version ${
|
|
1078
|
+
else if (!this.isSupportedOpenCodeVersion(version)) {
|
|
1079
|
+
throw new Error(`Unsupported OpenCode version ${version}. Install OpenCode >=${OPENCODE_MINIMUM_SUPPORTED_VERSION} <${OPENCODE_MAXIMUM_SUPPORTED_MAJOR}.0.0.`);
|
|
815
1080
|
}
|
|
816
1081
|
else {
|
|
817
1082
|
return;
|
|
@@ -948,10 +1213,9 @@ export class OpenCodeServerAdapter {
|
|
|
948
1213
|
return;
|
|
949
1214
|
}
|
|
950
1215
|
this.sseAbortController = new AbortController();
|
|
951
|
-
const sseLoops =
|
|
952
|
-
this.runSseLoop("event")
|
|
953
|
-
this.runSseLoop("global-event")
|
|
954
|
-
];
|
|
1216
|
+
const sseLoops = this.openCodeMajor >= 2
|
|
1217
|
+
? [this.runSseLoop("event")]
|
|
1218
|
+
: [this.runSseLoop("event"), this.runSseLoop("global-event")];
|
|
955
1219
|
this.sseLoopPromise = Promise.all(sseLoops).then(() => undefined);
|
|
956
1220
|
}
|
|
957
1221
|
async runSseLoop(streamName) {
|
|
@@ -1535,7 +1799,11 @@ export class OpenCodeServerAdapter {
|
|
|
1535
1799
|
return;
|
|
1536
1800
|
}
|
|
1537
1801
|
const sessionId = this.extractSessionId(properties);
|
|
1538
|
-
if (!sessionId
|
|
1802
|
+
if (!sessionId) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
this.previousAgentBySession.delete(sessionId);
|
|
1806
|
+
if (sessionId !== this.activeSessionId) {
|
|
1539
1807
|
return;
|
|
1540
1808
|
}
|
|
1541
1809
|
this.clearTrackedTurnForLocalSessionSwitch();
|
|
@@ -2590,6 +2858,16 @@ export class OpenCodeServerAdapter {
|
|
|
2590
2858
|
}
|
|
2591
2859
|
}
|
|
2592
2860
|
async sendVisibleSessionSelection(session) {
|
|
2861
|
+
if (this.openCodeMajor >= 2) {
|
|
2862
|
+
if (!this.tuiRouteSocket || this.tuiRouteSocket.destroyed) {
|
|
2863
|
+
throw new Error("The OpenCode 2 TUI route bridge is not connected.");
|
|
2864
|
+
}
|
|
2865
|
+
this.tuiRouteSocket.write(`${JSON.stringify({
|
|
2866
|
+
type: "select_session",
|
|
2867
|
+
sessionId: session.id,
|
|
2868
|
+
})}\n`);
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2593
2871
|
if (!this.client?.tui) {
|
|
2594
2872
|
return;
|
|
2595
2873
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import crypto from "node:crypto";
|
|
2
8
|
import fs from "node:fs";
|
|
3
9
|
import net from "node:net";
|
|
@@ -356,6 +362,41 @@ export class PiTuiAdapter {
|
|
|
356
362
|
this.pendingSessionSwitch = null;
|
|
357
363
|
}
|
|
358
364
|
}
|
|
365
|
+
async listModels() {
|
|
366
|
+
if (this.state.status === "busy") {
|
|
367
|
+
throw new Error("Pi is still working. Wait for the current turn before listing models.");
|
|
368
|
+
}
|
|
369
|
+
const response = await this.sendCommand("list_models");
|
|
370
|
+
if (!isRecord(response.data) || !Array.isArray(response.data.models)) {
|
|
371
|
+
throw new Error("Pi TUI returned an invalid model list.");
|
|
372
|
+
}
|
|
373
|
+
return response.data.models
|
|
374
|
+
.filter((model) => isRecord(model))
|
|
375
|
+
.filter((model) => typeof model.id === "string" && typeof model.displayName === "string")
|
|
376
|
+
.map((model) => ({
|
|
377
|
+
id: model.id,
|
|
378
|
+
displayName: model.displayName,
|
|
379
|
+
isCurrent: model.isCurrent === true,
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
async selectModel(modelId) {
|
|
383
|
+
if (this.state.status === "busy") {
|
|
384
|
+
throw new Error("Pi is still working. Wait for the current turn before switching models.");
|
|
385
|
+
}
|
|
386
|
+
const response = await this.sendCommand("select_model", { modelId });
|
|
387
|
+
if (!isRecord(response.data) || !isRecord(response.data.model)) {
|
|
388
|
+
throw new Error("Pi TUI returned an invalid selected model.");
|
|
389
|
+
}
|
|
390
|
+
const model = response.data.model;
|
|
391
|
+
if (typeof model.id !== "string" || typeof model.displayName !== "string") {
|
|
392
|
+
throw new Error("Pi TUI returned an invalid selected model.");
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
id: model.id,
|
|
396
|
+
displayName: model.displayName,
|
|
397
|
+
isCurrent: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
359
400
|
async interrupt() {
|
|
360
401
|
if (!this.socket || this.state.status !== "busy") {
|
|
361
402
|
return false;
|
|
@@ -569,6 +610,10 @@ export class PiTuiAdapter {
|
|
|
569
610
|
else if (this.currentAssistantText) {
|
|
570
611
|
this.emit({ type: "final_reply", text: this.currentAssistantText, timestamp: nowIso() });
|
|
571
612
|
}
|
|
613
|
+
// Mirror the other adapters: a settled turn always reports completion so
|
|
614
|
+
// bridge/daemon ownership state is released even when the turn produced
|
|
615
|
+
// no visible reply.
|
|
616
|
+
this.emit({ type: "task_complete", timestamp: nowIso() });
|
|
572
617
|
this.state.lastOutputAt = nowIso();
|
|
573
618
|
this.clearActiveTurn();
|
|
574
619
|
this.setStatus("idle");
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import fs from "node:fs";
|
|
2
8
|
import os from "node:os";
|
|
3
9
|
import path from "node:path";
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import { clearLocalCompanionEndpoint, readLocalCompanionEndpoint, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
2
8
|
import { hasLocalClientEndpointProvider } from "../runtime/runtime-types.js";
|
|
3
9
|
export class BridgeController {
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
/** @deprecated Import from ../channels/wechat/bridge-final-reply.ts instead. */
|
|
2
8
|
export * from "../channels/wechat/bridge-final-reply.js";
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
1
7
|
import { spawnSync } from "node:child_process";
|
|
2
8
|
import path from "node:path";
|
|
3
9
|
const PEER_BRIDGE_EXIT_TIMEOUT_MS = 4_000;
|
|
@@ -113,7 +119,7 @@ function normalizeBridgeProcessRecord(value) {
|
|
|
113
119
|
}
|
|
114
120
|
return record;
|
|
115
121
|
}
|
|
116
|
-
|
|
122
|
+
function parseWindowsProcessProbeRecords(stdout) {
|
|
117
123
|
const trimmed = stdout.trim();
|
|
118
124
|
if (!trimmed) {
|
|
119
125
|
return [];
|
|
@@ -128,7 +134,13 @@ export function parseWindowsBridgeProcessProbeOutput(stdout, currentPid = proces
|
|
|
128
134
|
const values = Array.isArray(parsed) ? parsed : [parsed];
|
|
129
135
|
return values
|
|
130
136
|
.map(normalizeBridgeProcessRecord)
|
|
131
|
-
.filter((record) => Boolean(record))
|
|
137
|
+
.filter((record) => Boolean(record));
|
|
138
|
+
}
|
|
139
|
+
export function parseWindowsProcessRecordProbeOutput(stdout, currentPid = process.pid) {
|
|
140
|
+
return (parseWindowsProcessProbeRecords(stdout).find((record) => record.pid !== currentPid) ?? null);
|
|
141
|
+
}
|
|
142
|
+
export function parseWindowsBridgeProcessProbeOutput(stdout, currentPid = process.pid) {
|
|
143
|
+
return parseWindowsProcessProbeRecords(stdout)
|
|
132
144
|
.filter((record) => record.pid !== currentPid && isWechatBridgeCommandLine(record.commandLine));
|
|
133
145
|
}
|
|
134
146
|
export function parsePosixBridgeProcessProbeOutput(stdout, currentPid = process.pid) {
|
|
@@ -195,10 +207,37 @@ export function listPeerBridgeProcesses(currentPid = process.pid) {
|
|
|
195
207
|
? listWindowsBridgeProcesses(currentPid)
|
|
196
208
|
: listPosixBridgeProcesses(currentPid);
|
|
197
209
|
}
|
|
210
|
+
function getWindowsProcessRecordByPid(pid, currentPid = process.pid) {
|
|
211
|
+
const probe = spawnSync("powershell.exe", [
|
|
212
|
+
"-NoLogo",
|
|
213
|
+
"-NoProfile",
|
|
214
|
+
"-NonInteractive",
|
|
215
|
+
"-ExecutionPolicy",
|
|
216
|
+
"Bypass",
|
|
217
|
+
"-Command",
|
|
218
|
+
[
|
|
219
|
+
"$ErrorActionPreference='Stop';",
|
|
220
|
+
`Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}'`,
|
|
221
|
+
"| Select-Object ProcessId,ParentProcessId,Name,CommandLine",
|
|
222
|
+
"| ConvertTo-Json -Compress",
|
|
223
|
+
].join(" "),
|
|
224
|
+
], {
|
|
225
|
+
encoding: "utf8",
|
|
226
|
+
windowsHide: true,
|
|
227
|
+
timeout: 8_000,
|
|
228
|
+
});
|
|
229
|
+
if (probe.status !== 0 || typeof probe.stdout !== "string") {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return parseWindowsProcessRecordProbeOutput(probe.stdout, currentPid);
|
|
233
|
+
}
|
|
198
234
|
export function getProcessRecordByPid(pid, currentPid = process.pid) {
|
|
199
235
|
if (!Number.isInteger(pid) || pid <= 0 || pid === currentPid) {
|
|
200
236
|
return null;
|
|
201
237
|
}
|
|
238
|
+
if (process.platform === "win32") {
|
|
239
|
+
return getWindowsProcessRecordByPid(pid, currentPid);
|
|
240
|
+
}
|
|
202
241
|
return listAllProcessesRaw(currentPid).find((record) => record.pid === pid) ?? null;
|
|
203
242
|
}
|
|
204
243
|
export function listWechatDaemonProcesses(params = {}) {
|