cli-wechat-bridge 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +661 -661
- package/README.md +461 -434
- 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 +6 -1
- package/bin/wecom-claude.mjs +6 -1
- package/bin/wecom-codex.mjs +6 -1
- package/bin/wecom-daemon.mjs +6 -1
- package/bin/wecom-opencode.mjs +6 -1
- package/bin/wecom-pi.mjs +6 -1
- package/bin/wecom-setup.mjs +6 -1
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +35 -1
- package/dist/bridge/bridge-adapters.codex.js +118 -7
- package/dist/bridge/bridge-adapters.core.js +25 -1
- package/dist/bridge/bridge-adapters.opencode.js +156 -1
- 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 +6 -0
- package/dist/bridge/bridge-state.js +6 -0
- package/dist/bridge/bridge-utils.js +6 -31
- package/dist/bridge/claude-hook.js +6 -0
- package/dist/bridge/claude-native-controls.js +133 -0
- package/dist/bridge/native-terminal-control.js +259 -0
- package/dist/bridge/wechat-bridge.js +250 -274
- 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 +39 -0
- package/dist/channels/wecom/bridge-final-reply.js +6 -0
- package/dist/channels/wecom/setup-cli.js +6 -0
- package/dist/channels/wecom/setup.js +6 -0
- package/dist/channels/wecom/wecom-config.js +6 -0
- package/dist/channels/wecom/wecom-driver.js +106 -0
- package/dist/channels/wecom/wecom-media.js +6 -0
- package/dist/channels/wecom/wecom-transport.js +6 -0
- package/dist/commands/check-update.js +6 -0
- package/dist/companion/codex-remote-client.js +28 -2
- 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 +13 -2
- package/dist/companion/local-companion.js +23 -0
- package/dist/companion/opencode-tui-bridge-plugin.js +6 -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/conversation-routing.js +58 -0
- package/dist/core/direct-run.js +6 -0
- package/dist/core/inbound-message-claims.js +6 -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 +6 -0
- package/dist/daemon/emoji-bindings.js +6 -0
- package/dist/daemon/wechat-daemon.js +529 -205
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages-en.js +7 -1
- package/dist/i18n/messages-zh.js +7 -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 +6 -0
- package/dist/utils/version-checker.js +17 -11
- package/dist/wechat/channel-config.js +6 -0
- package/dist/wechat/setup.js +6 -0
- package/dist/wechat/wechat-channel.js +6 -0
- package/dist/wechat/wechat-transport.js +169 -58
- package/package.json +108 -106
- 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 net from "node:net";
|
|
2
8
|
import path from "node:path";
|
|
3
9
|
import { spawn as spawnPty } from "node-pty";
|
|
@@ -15,7 +21,7 @@ const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, b
|
|
|
15
21
|
* the script path contains spaces: node-pty quotes the already-quoted path,
|
|
16
22
|
* making cmd.exe treat the literal quotes as part of the filename.
|
|
17
23
|
*/
|
|
18
|
-
function unwrapCmdExeForPty(rawTarget) {
|
|
24
|
+
export function unwrapCmdExeForPty(rawTarget) {
|
|
19
25
|
if (process.platform !== "win32") {
|
|
20
26
|
return rawTarget;
|
|
21
27
|
}
|
|
@@ -166,6 +172,15 @@ export class LocalCompanionProxyAdapter {
|
|
|
166
172
|
command: "create_session",
|
|
167
173
|
});
|
|
168
174
|
}
|
|
175
|
+
async listModels() {
|
|
176
|
+
return await this.sendRequest({ command: "list_models" });
|
|
177
|
+
}
|
|
178
|
+
async selectModel(modelId) {
|
|
179
|
+
return await this.sendRequest({ command: "select_model", modelId });
|
|
180
|
+
}
|
|
181
|
+
async setPlanMode(enabled) {
|
|
182
|
+
return await this.sendRequest({ command: "set_plan_mode", enabled });
|
|
183
|
+
}
|
|
169
184
|
async interrupt() {
|
|
170
185
|
const result = await this.sendRequest({
|
|
171
186
|
command: "interrupt",
|
|
@@ -612,6 +627,15 @@ export class AbstractPtyAdapter {
|
|
|
612
627
|
setEventSink(sink) {
|
|
613
628
|
this.eventSink = sink;
|
|
614
629
|
}
|
|
630
|
+
async listModels() {
|
|
631
|
+
throw new Error(`/${this.options.kind} model selection is not available.`);
|
|
632
|
+
}
|
|
633
|
+
async selectModel(_modelId) {
|
|
634
|
+
throw new Error(`/${this.options.kind} model selection is not available.`);
|
|
635
|
+
}
|
|
636
|
+
async setPlanMode(_enabled) {
|
|
637
|
+
throw new Error(`/${this.options.kind} plan mode is not available.`);
|
|
638
|
+
}
|
|
615
639
|
async start() {
|
|
616
640
|
if (this.pty) {
|
|
617
641
|
return;
|
|
@@ -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";
|
|
@@ -85,6 +91,139 @@ export class OpenCodeServerAdapter {
|
|
|
85
91
|
pendingLocalSessionCreateFollowUntilMs = 0;
|
|
86
92
|
pendingPermission = null;
|
|
87
93
|
pendingQuestion = null;
|
|
94
|
+
previousAgentBySession = new Map();
|
|
95
|
+
async listModels() {
|
|
96
|
+
this.assertRemoteControlReady();
|
|
97
|
+
const session = await this.ensureControlSession();
|
|
98
|
+
const models = await this.listControlModels();
|
|
99
|
+
return models.map((model) => ({
|
|
100
|
+
id: model.id,
|
|
101
|
+
displayName: model.displayName,
|
|
102
|
+
isCurrent: session.model?.providerID === model.providerID &&
|
|
103
|
+
session.model.id === model.modelID,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
async selectModel(modelId) {
|
|
107
|
+
this.assertRemoteControlReady();
|
|
108
|
+
const session = await this.ensureControlSession();
|
|
109
|
+
const models = await this.listControlModels();
|
|
110
|
+
const target = models.find((model) => model.id === modelId);
|
|
111
|
+
if (!target) {
|
|
112
|
+
throw new Error("This OpenCode model is no longer available. Send /model again.");
|
|
113
|
+
}
|
|
114
|
+
const currentVariant = session.model?.variant;
|
|
115
|
+
const variant = currentVariant && currentVariant !== "default" && target.variants.includes(currentVariant)
|
|
116
|
+
? currentVariant
|
|
117
|
+
: "default";
|
|
118
|
+
const result = await this.client.v2.session.switchModel({
|
|
119
|
+
sessionID: session.id,
|
|
120
|
+
model: {
|
|
121
|
+
providerID: target.providerID,
|
|
122
|
+
id: target.modelID,
|
|
123
|
+
variant,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
this.unwrapOrThrow(result);
|
|
127
|
+
const updated = await this.getSessionForCurrentDirectory(session.id);
|
|
128
|
+
if (!updated ||
|
|
129
|
+
updated.model?.providerID !== target.providerID ||
|
|
130
|
+
updated.model.id !== target.modelID ||
|
|
131
|
+
(updated.model.variant ?? "default") !== variant) {
|
|
132
|
+
throw new Error("OpenCode did not confirm the selected model.");
|
|
133
|
+
}
|
|
134
|
+
return { id: target.id, displayName: target.displayName, isCurrent: true };
|
|
135
|
+
}
|
|
136
|
+
async setPlanMode(enabled) {
|
|
137
|
+
this.assertRemoteControlReady();
|
|
138
|
+
const session = await this.ensureControlSession();
|
|
139
|
+
const agents = await this.listPrimaryAgents();
|
|
140
|
+
const planAgent = agents.find((agent) => agent.name === "plan");
|
|
141
|
+
if (!planAgent) {
|
|
142
|
+
throw new Error("OpenCode primary agent 'plan' is not available.");
|
|
143
|
+
}
|
|
144
|
+
const currentAgent = session.agent ?? agents[0]?.name;
|
|
145
|
+
if (!currentAgent) {
|
|
146
|
+
throw new Error("OpenCode did not return any primary agents.");
|
|
147
|
+
}
|
|
148
|
+
if (enabled && currentAgent === "plan") {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (!enabled && currentAgent !== "plan") {
|
|
152
|
+
this.previousAgentBySession.delete(session.id);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const previousAgent = this.previousAgentBySession.get(session.id);
|
|
156
|
+
const target = enabled
|
|
157
|
+
? "plan"
|
|
158
|
+
: agents.some((agent) => agent.name === previousAgent)
|
|
159
|
+
? previousAgent
|
|
160
|
+
: agents.find((agent) => agent.name === "build")?.name ??
|
|
161
|
+
agents.find((agent) => agent.name !== "plan")?.name;
|
|
162
|
+
if (!target) {
|
|
163
|
+
throw new Error("OpenCode did not return an agent that can replace plan mode.");
|
|
164
|
+
}
|
|
165
|
+
const result = await this.client.v2.session.switchAgent({
|
|
166
|
+
sessionID: session.id,
|
|
167
|
+
agent: target,
|
|
168
|
+
});
|
|
169
|
+
this.unwrapOrThrow(result);
|
|
170
|
+
const updated = await this.getSessionForCurrentDirectory(session.id);
|
|
171
|
+
if (!updated || updated.agent !== target) {
|
|
172
|
+
throw new Error("OpenCode did not confirm the selected agent.");
|
|
173
|
+
}
|
|
174
|
+
if (enabled) {
|
|
175
|
+
this.previousAgentBySession.set(session.id, currentAgent);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.previousAgentBySession.delete(session.id);
|
|
179
|
+
}
|
|
180
|
+
return enabled;
|
|
181
|
+
}
|
|
182
|
+
assertRemoteControlReady() {
|
|
183
|
+
if (!this.client) {
|
|
184
|
+
throw new Error("OpenCode is not running.");
|
|
185
|
+
}
|
|
186
|
+
if (this.state.status === "busy") {
|
|
187
|
+
throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
|
|
188
|
+
}
|
|
189
|
+
if (this.pendingPermission) {
|
|
190
|
+
throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
|
|
191
|
+
}
|
|
192
|
+
if (this.pendingQuestion) {
|
|
193
|
+
throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async ensureControlSession() {
|
|
197
|
+
const session = await this.ensureSession();
|
|
198
|
+
if (session.id !== this.activeSessionId) {
|
|
199
|
+
this.switchSharedSession(session, {
|
|
200
|
+
source: "wechat",
|
|
201
|
+
reason: "wechat_resume",
|
|
202
|
+
syncVisible: true,
|
|
203
|
+
forceVisibleSync: true,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return session;
|
|
207
|
+
}
|
|
208
|
+
async listPrimaryAgents() {
|
|
209
|
+
if (!this.client)
|
|
210
|
+
throw new Error("OpenCode is not running.");
|
|
211
|
+
const agents = this.unwrapOrThrow(await this.client.app.agents({
|
|
212
|
+
directory: this.options.cwd,
|
|
213
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
214
|
+
}, { signal: AbortSignal.timeout(10_000) }));
|
|
215
|
+
return agents.filter((agent) => agent.mode !== "subagent" && !agent.hidden);
|
|
216
|
+
}
|
|
217
|
+
async listControlModels() {
|
|
218
|
+
if (!this.client)
|
|
219
|
+
throw new Error("OpenCode is not running.");
|
|
220
|
+
const providers = this.unwrapOrThrow(await this.client.provider.list({ directory: this.options.cwd, workspace: this.activeWorkspaceId ?? undefined }, { signal: AbortSignal.timeout(10_000) }));
|
|
221
|
+
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]) => ({
|
|
222
|
+
id: `${provider.id}/${id}`, displayName: `${model.name || id} (${provider.name})`,
|
|
223
|
+
providerID: provider.id, modelID: id,
|
|
224
|
+
variants: Object.keys(model.variants ?? {}),
|
|
225
|
+
})));
|
|
226
|
+
}
|
|
88
227
|
constructor(options) {
|
|
89
228
|
this.options = options;
|
|
90
229
|
this.state = {
|
|
@@ -170,10 +309,21 @@ export class OpenCodeServerAdapter {
|
|
|
170
309
|
this.recordPendingWechatPromptMirrorSuppression(session.id, normalized);
|
|
171
310
|
this.beginTrackedTurn(normalized, "wechat");
|
|
172
311
|
try {
|
|
312
|
+
const selection = await this.getSessionForCurrentDirectory(session.id) ?? session;
|
|
173
313
|
const result = await this.client.session.promptAsync({
|
|
174
314
|
sessionID: session.id,
|
|
175
315
|
directory: this.options.cwd,
|
|
176
316
|
workspace: session.workspaceID ?? this.activeWorkspaceId ?? undefined,
|
|
317
|
+
...(selection.agent ? { agent: selection.agent } : {}),
|
|
318
|
+
...(selection.model
|
|
319
|
+
? {
|
|
320
|
+
model: {
|
|
321
|
+
providerID: selection.model.providerID,
|
|
322
|
+
modelID: selection.model.id,
|
|
323
|
+
},
|
|
324
|
+
variant: selection.model.variant ?? "default",
|
|
325
|
+
}
|
|
326
|
+
: {}),
|
|
177
327
|
parts: [{ type: "text", text: normalized }],
|
|
178
328
|
});
|
|
179
329
|
if (result.error !== undefined) {
|
|
@@ -411,6 +561,7 @@ export class OpenCodeServerAdapter {
|
|
|
411
561
|
this.pendingLocalSessionCreateFollowUntilMs = 0;
|
|
412
562
|
this.clearObservedMessageTracking();
|
|
413
563
|
this.recentSdkEventObservations.clear();
|
|
564
|
+
this.previousAgentBySession.clear();
|
|
414
565
|
this.outputBatcher.clear();
|
|
415
566
|
this.clearStreamedPartState();
|
|
416
567
|
this.clearPendingPermissionState();
|
|
@@ -1535,7 +1686,11 @@ export class OpenCodeServerAdapter {
|
|
|
1535
1686
|
return;
|
|
1536
1687
|
}
|
|
1537
1688
|
const sessionId = this.extractSessionId(properties);
|
|
1538
|
-
if (!sessionId
|
|
1689
|
+
if (!sessionId) {
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
this.previousAgentBySession.delete(sessionId);
|
|
1693
|
+
if (sessionId !== this.activeSessionId) {
|
|
1539
1694
|
return;
|
|
1540
1695
|
}
|
|
1541
1696
|
this.clearTrackedTurnForLocalSessionSwitch();
|
|
@@ -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;
|
|
@@ -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 { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureWorkspaceChannelDir, ensureChannelDataDir, } from "../wechat/channel-config.js";
|
|
3
9
|
import { buildInstanceId } from "../core/text-utils.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 fs from "node:fs";
|
|
2
8
|
import os from "node:os";
|
|
3
9
|
import path from "node:path";
|
|
@@ -1113,37 +1119,6 @@ export function isStrictApprovalModeEnabled(env = process.env) {
|
|
|
1113
1119
|
const value = env.CLI_BRIDGE_STRICT_APPROVAL?.trim().toLowerCase();
|
|
1114
1120
|
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
1115
1121
|
}
|
|
1116
|
-
// Outbound WeChat text size cap shared by the streaming OutputBatcher and the
|
|
1117
|
-
// final-reply forwarder. Long replies sent as a single sendmessage call can be
|
|
1118
|
-
// rejected by the server, which previously made long final replies vanish.
|
|
1119
|
-
export const WECHAT_TEXT_CHUNK_MAX_CHARS = 1_200;
|
|
1120
|
-
export function splitWechatTextIntoChunks(text, maxChars = WECHAT_TEXT_CHUNK_MAX_CHARS) {
|
|
1121
|
-
const normalized = text.trim();
|
|
1122
|
-
if (!normalized) {
|
|
1123
|
-
return [];
|
|
1124
|
-
}
|
|
1125
|
-
if (normalized.length <= maxChars) {
|
|
1126
|
-
return [normalized];
|
|
1127
|
-
}
|
|
1128
|
-
const chunks = [];
|
|
1129
|
-
let remaining = normalized;
|
|
1130
|
-
while (remaining.length > maxChars) {
|
|
1131
|
-
// Prefer breaking at a newline reasonably close to the cap so paragraphs
|
|
1132
|
-
// stay intact; fall back to a hard split.
|
|
1133
|
-
const window = remaining.slice(0, maxChars + 1);
|
|
1134
|
-
const newlineIndex = window.lastIndexOf("\n");
|
|
1135
|
-
const splitIndex = newlineIndex > maxChars / 2 ? newlineIndex : maxChars;
|
|
1136
|
-
const chunk = remaining.slice(0, splitIndex).trim();
|
|
1137
|
-
if (chunk) {
|
|
1138
|
-
chunks.push(chunk);
|
|
1139
|
-
}
|
|
1140
|
-
remaining = remaining.slice(splitIndex).trim();
|
|
1141
|
-
}
|
|
1142
|
-
if (remaining) {
|
|
1143
|
-
chunks.push(remaining);
|
|
1144
|
-
}
|
|
1145
|
-
return chunks;
|
|
1146
|
-
}
|
|
1147
1122
|
export function shouldDropStartupBacklogMessage(createdAtMs, bridgeStartedAtMs, graceMs = MESSAGE_START_GRACE_MS) {
|
|
1148
1123
|
// A missing or unparsable timestamp must not drop the message: treat it as
|
|
1149
1124
|
// fresh and let normal processing continue.
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
4
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
5
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
6
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
7
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
2
8
|
import crypto from "node:crypto";
|
|
3
9
|
import net from "node:net";
|
|
4
10
|
async function readStdin() {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { NativeTerminalControl, parseClaudeModelPicker, readClaudePermissionMode } from "./native-terminal-control.js";
|
|
2
|
+
export class ClaudeNativeControls {
|
|
3
|
+
previousMode = null;
|
|
4
|
+
terminal;
|
|
5
|
+
session;
|
|
6
|
+
constructor(terminal, session) {
|
|
7
|
+
this.terminal = terminal;
|
|
8
|
+
this.session = session;
|
|
9
|
+
}
|
|
10
|
+
assertPrompt() {
|
|
11
|
+
if (readClaudePermissionMode(this.terminal.text) === null) {
|
|
12
|
+
throw new Error("Claude must be at an empty native prompt before changing models or plan mode.");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async listModels() {
|
|
16
|
+
return this.terminal.run(async () => {
|
|
17
|
+
this.assertPrompt();
|
|
18
|
+
await this.terminal.openMenu("/model", "Select model");
|
|
19
|
+
const options = new Map();
|
|
20
|
+
const visited = new Set();
|
|
21
|
+
const initial = parseClaudeModelPicker(this.terminal.text).find((option) => option.focused)?.index;
|
|
22
|
+
while (true) {
|
|
23
|
+
const rows = parseClaudeModelPicker(this.terminal.text);
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
options.set(row.index, {
|
|
26
|
+
id: JSON.stringify({ index: row.index, label: row.label }),
|
|
27
|
+
displayName: row.label,
|
|
28
|
+
isCurrent: row.current || row.index === initial,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const focused = rows.find((row) => row.focused);
|
|
32
|
+
if (!focused)
|
|
33
|
+
throw new Error("Could not identify the focused Claude model. Close the menu and try again.");
|
|
34
|
+
if (visited.has(focused.index))
|
|
35
|
+
break;
|
|
36
|
+
visited.add(focused.index);
|
|
37
|
+
await this.terminal.key("\u001b[B");
|
|
38
|
+
}
|
|
39
|
+
// A clamped selector can start in the middle; walk upwards to capture earlier hidden rows too.
|
|
40
|
+
const visitedUp = new Set();
|
|
41
|
+
while (true) {
|
|
42
|
+
const rows = parseClaudeModelPicker(this.terminal.text);
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
options.set(row.index, { id: JSON.stringify({ index: row.index, label: row.label }), displayName: row.label, isCurrent: row.current || row.index === initial });
|
|
45
|
+
}
|
|
46
|
+
const focused = rows.find((row) => row.focused);
|
|
47
|
+
if (!focused)
|
|
48
|
+
throw new Error("Could not identify the focused Claude model.");
|
|
49
|
+
if (visitedUp.has(focused.index))
|
|
50
|
+
break;
|
|
51
|
+
visitedUp.add(focused.index);
|
|
52
|
+
if (focused.index === 1)
|
|
53
|
+
break;
|
|
54
|
+
await this.terminal.key("\u001b[A");
|
|
55
|
+
}
|
|
56
|
+
await this.terminal.closeMenu();
|
|
57
|
+
return [...options.entries()].filter(([index]) => visited.has(index) || visitedUp.has(index)).sort(([a], [b]) => a - b).map(([, option]) => option);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async selectModel(id) {
|
|
61
|
+
const target = JSON.parse(id);
|
|
62
|
+
if (!Number.isSafeInteger(target.index) || target.index < 1 || typeof target.label !== "string")
|
|
63
|
+
throw new Error("Invalid Claude model selection. Send /model again.");
|
|
64
|
+
return this.terminal.run(async () => {
|
|
65
|
+
this.assertPrompt();
|
|
66
|
+
await this.terminal.openMenu("/model", "Select model");
|
|
67
|
+
const visited = new Set();
|
|
68
|
+
while (true) {
|
|
69
|
+
const focused = parseClaudeModelPicker(this.terminal.text).find((row) => row.focused);
|
|
70
|
+
if (!focused)
|
|
71
|
+
throw new Error("Could not identify the focused Claude model.");
|
|
72
|
+
if (focused.index === target.index) {
|
|
73
|
+
if (focused.label !== target.label)
|
|
74
|
+
throw new Error("The Claude model list changed. Send /model again.");
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
if (visited.has(focused.index))
|
|
78
|
+
throw new Error("The Claude model is no longer selectable. Send /model again.");
|
|
79
|
+
visited.add(focused.index);
|
|
80
|
+
await this.terminal.key(focused.index < target.index ? "\u001b[B" : "\u001b[A");
|
|
81
|
+
}
|
|
82
|
+
if (!/s to use this session only/i.test(this.terminal.text)) {
|
|
83
|
+
throw new Error("This Claude version does not expose session-only model selection. Update Claude Code and try again.");
|
|
84
|
+
}
|
|
85
|
+
await this.terminal.submitMenu(undefined, "s");
|
|
86
|
+
await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) !== null);
|
|
87
|
+
await this.terminal.openMenu("/model", "Select model");
|
|
88
|
+
const applied = parseClaudeModelPicker(this.terminal.text).find((row) => row.focused);
|
|
89
|
+
if (!applied || applied.label !== target.label || applied.index !== target.index)
|
|
90
|
+
throw new Error("Claude did not confirm the selected model.");
|
|
91
|
+
await this.terminal.closeMenu();
|
|
92
|
+
return { id, displayName: target.label, isCurrent: true };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async setPlanMode(enabled) {
|
|
96
|
+
return this.terminal.run(async () => {
|
|
97
|
+
this.assertPrompt();
|
|
98
|
+
const current = readClaudePermissionMode(this.terminal.text);
|
|
99
|
+
const session = this.session();
|
|
100
|
+
if (this.previousMode?.session !== session)
|
|
101
|
+
this.previousMode = null;
|
|
102
|
+
if (enabled) {
|
|
103
|
+
if (current === "plan")
|
|
104
|
+
return true;
|
|
105
|
+
if (!["default", "acceptEdits", "bypassPermissions", "auto"].includes(current)) {
|
|
106
|
+
throw new Error(`Claude cannot restore permission mode ${current} through the native mode switch.`);
|
|
107
|
+
}
|
|
108
|
+
await this.terminal.command("/plan");
|
|
109
|
+
await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) === "plan");
|
|
110
|
+
this.previousMode = { session, mode: current };
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (current !== "plan") {
|
|
114
|
+
this.previousMode = null;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const target = this.previousMode?.mode ?? "default";
|
|
118
|
+
const visited = new Set();
|
|
119
|
+
while (true) {
|
|
120
|
+
const mode = readClaudePermissionMode(this.terminal.text);
|
|
121
|
+
if (mode === target)
|
|
122
|
+
break;
|
|
123
|
+
if (!mode || visited.has(mode))
|
|
124
|
+
throw new Error(`Claude could not restore ${target}; check the native permission mode.`);
|
|
125
|
+
visited.add(mode);
|
|
126
|
+
await this.terminal.key("\u001b[Z");
|
|
127
|
+
await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) !== mode);
|
|
128
|
+
}
|
|
129
|
+
this.previousMode = null;
|
|
130
|
+
return false;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|