cli-wechat-bridge 1.1.7 → 1.1.8
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 +448 -434
- package/bin/_run-entry.mjs +23 -5
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +29 -1
- package/dist/bridge/bridge-adapters.codex.js +112 -7
- package/dist/bridge/bridge-adapters.core.js +19 -1
- package/dist/bridge/bridge-adapters.opencode.js +150 -1
- package/dist/bridge/bridge-adapters.pi.js +39 -0
- package/dist/bridge/claude-native-controls.js +133 -0
- package/dist/bridge/native-terminal-control.js +253 -0
- package/dist/bridge/wechat-bridge.js +182 -113
- package/dist/companion/codex-remote-client.js +22 -2
- package/dist/companion/codex-visible-client-link.js +101 -0
- package/dist/companion/local-companion-start.js +7 -2
- package/dist/companion/local-companion.js +17 -0
- package/dist/companion/pi-tui-bridge-extension.js +56 -3
- package/dist/core/bridge-control.js +8 -0
- package/dist/core/conversation-routing.js +48 -0
- package/dist/daemon/wechat-daemon.js +120 -74
- package/dist/i18n/messages-en.js +1 -1
- package/dist/i18n/messages-zh.js +1 -1
- package/dist/runtime/legacy-adapter-runtime.js +15 -0
- package/dist/runtime/runtime-types.js +3 -0
- package/dist/utils/version-checker.js +11 -11
- package/dist/wechat/wechat-transport.js +96 -58
- package/package.json +108 -106
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
|
@@ -23,12 +23,37 @@ export function parseCodexVisibleSwitchRequest(value) {
|
|
|
23
23
|
threadId: value.threadId,
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
export function parseCodexVisibleControlRequest(value) {
|
|
27
|
+
const switchRequest = parseCodexVisibleSwitchRequest(value);
|
|
28
|
+
if (switchRequest) {
|
|
29
|
+
return switchRequest;
|
|
30
|
+
}
|
|
31
|
+
if (!isRecord(value)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (value.type !== "shutdown" ||
|
|
35
|
+
typeof value.id !== "string" ||
|
|
36
|
+
typeof value.token !== "string") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
type: "shutdown",
|
|
41
|
+
id: value.id,
|
|
42
|
+
token: value.token,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
26
45
|
export function sendCodexVisibleSwitchResponse(socket, response) {
|
|
27
46
|
if (socket.destroyed || socket.writableEnded) {
|
|
28
47
|
return;
|
|
29
48
|
}
|
|
30
49
|
socket.end(`${JSON.stringify(response)}\n`);
|
|
31
50
|
}
|
|
51
|
+
export function sendCodexVisibleShutdownResponse(socket, response) {
|
|
52
|
+
if (socket.destroyed || socket.writableEnded) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
socket.end(`${JSON.stringify(response)}\n`);
|
|
56
|
+
}
|
|
32
57
|
function resolveCodexControlEndpoint(cwd, instanceId) {
|
|
33
58
|
const endpoint = readLocalCompanionEndpoint(cwd, { adapter: "codex" });
|
|
34
59
|
if (!endpoint || endpoint.kind !== "codex" || endpoint.instanceId !== instanceId) {
|
|
@@ -120,3 +145,79 @@ export async function requestCodexVisibleThreadSwitch(params) {
|
|
|
120
145
|
});
|
|
121
146
|
});
|
|
122
147
|
}
|
|
148
|
+
export async function requestCodexVisibleClientShutdown(params) {
|
|
149
|
+
const endpoint = resolveCodexControlEndpoint(params.cwd, params.instanceId);
|
|
150
|
+
const requestId = crypto.randomUUID();
|
|
151
|
+
const timeoutMs = params.timeoutMs ?? 5_000;
|
|
152
|
+
await new Promise((resolve, reject) => {
|
|
153
|
+
const socket = net.connect({
|
|
154
|
+
host: CODEX_VISIBLE_CONTROL_HOST,
|
|
155
|
+
port: endpoint.codexControlPort,
|
|
156
|
+
});
|
|
157
|
+
let buffer = "";
|
|
158
|
+
let settled = false;
|
|
159
|
+
const finish = (error) => {
|
|
160
|
+
if (settled) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
settled = true;
|
|
164
|
+
socket.destroy();
|
|
165
|
+
if (error) {
|
|
166
|
+
reject(error);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
resolve();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
socket.setEncoding("utf8");
|
|
173
|
+
socket.setTimeout(timeoutMs);
|
|
174
|
+
socket.once("connect", () => {
|
|
175
|
+
const request = {
|
|
176
|
+
type: "shutdown",
|
|
177
|
+
id: requestId,
|
|
178
|
+
token: endpoint.codexControlToken,
|
|
179
|
+
};
|
|
180
|
+
socket.write(`${JSON.stringify(request)}\n`);
|
|
181
|
+
});
|
|
182
|
+
socket.on("data", (chunk) => {
|
|
183
|
+
buffer += chunk;
|
|
184
|
+
if (buffer.length > 65_536) {
|
|
185
|
+
finish(new Error("The visible Codex client returned an oversized shutdown response."));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
189
|
+
if (newlineIndex < 0) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
193
|
+
let parsed;
|
|
194
|
+
try {
|
|
195
|
+
parsed = JSON.parse(line);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
finish(new Error("The visible Codex client returned an invalid shutdown response."));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!isRecord(parsed) || parsed.type !== "shutdown_result" || parsed.id !== requestId) {
|
|
202
|
+
finish(new Error("The visible Codex client returned an unexpected shutdown response."));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (parsed.ok !== true) {
|
|
206
|
+
finish(new Error(typeof parsed.error === "string"
|
|
207
|
+
? parsed.error
|
|
208
|
+
: "The visible Codex client could not shut down."));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
finish();
|
|
212
|
+
});
|
|
213
|
+
socket.once("timeout", () => {
|
|
214
|
+
finish(new Error("Timed out waiting for the visible Codex client to shut down."));
|
|
215
|
+
});
|
|
216
|
+
socket.once("error", (error) => finish(error));
|
|
217
|
+
socket.once("close", () => {
|
|
218
|
+
if (!settled) {
|
|
219
|
+
finish(new Error("The visible Codex client closed the shutdown connection."));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
@@ -39,8 +39,13 @@ export function formatSwitchFailureMessage(cwd) {
|
|
|
39
39
|
export function formatRestartUnhealthyMessage(cwd) {
|
|
40
40
|
return `Detected unhealthy companion state for ${cwd}. Restarting bridge...`;
|
|
41
41
|
}
|
|
42
|
-
export function defaultSessionStartMode(
|
|
43
|
-
|
|
42
|
+
export function defaultSessionStartMode(_adapter) {
|
|
43
|
+
// Codex 0.155+ persists app-server threads lazily, so launching the visible
|
|
44
|
+
// client with a freshly prepared (not yet persisted) thread id crashes its
|
|
45
|
+
// bootstrap with "no rollout found". Start every adapter fresh by default;
|
|
46
|
+
// explicit --session-start-mode restore remains available for deliberate
|
|
47
|
+
// restores of already persisted threads.
|
|
48
|
+
return "new";
|
|
44
49
|
}
|
|
45
50
|
export function decideLaunchAction(input) {
|
|
46
51
|
if (input.lockShouldAutoReclaim) {
|
|
@@ -231,6 +231,23 @@ export async function runLocalCompanion(options) {
|
|
|
231
231
|
publishState();
|
|
232
232
|
sendResponse(socket, message.id, true);
|
|
233
233
|
break;
|
|
234
|
+
case "list_models":
|
|
235
|
+
if (!adapter.listModels)
|
|
236
|
+
throw new Error("Model listing is not supported by this companion.");
|
|
237
|
+
sendResponse(socket, message.id, true, await adapter.listModels());
|
|
238
|
+
break;
|
|
239
|
+
case "select_model":
|
|
240
|
+
if (!adapter.selectModel)
|
|
241
|
+
throw new Error("Model selection is not supported by this companion.");
|
|
242
|
+
sendResponse(socket, message.id, true, await adapter.selectModel(message.payload.modelId));
|
|
243
|
+
publishState();
|
|
244
|
+
break;
|
|
245
|
+
case "set_plan_mode":
|
|
246
|
+
if (!adapter.setPlanMode)
|
|
247
|
+
throw new Error("Plan mode is not supported by this companion.");
|
|
248
|
+
sendResponse(socket, message.id, true, await adapter.setPlanMode(message.payload.enabled));
|
|
249
|
+
publishState();
|
|
250
|
+
break;
|
|
234
251
|
case "interrupt":
|
|
235
252
|
sendResponse(socket, message.id, true, await adapter.interrupt());
|
|
236
253
|
break;
|
|
@@ -167,7 +167,7 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
167
167
|
}
|
|
168
168
|
},
|
|
169
169
|
});
|
|
170
|
-
const executeCommand = (command) => {
|
|
170
|
+
const executeCommand = async (command) => {
|
|
171
171
|
try {
|
|
172
172
|
if (command.type === "prompt" && typeof command.text === "string") {
|
|
173
173
|
if (!latestContext?.isIdle()) {
|
|
@@ -180,6 +180,59 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
180
180
|
latestContext?.abort();
|
|
181
181
|
writeFrame({ type: "response", id: command.id, success: true });
|
|
182
182
|
}
|
|
183
|
+
else if (command.type === "list_models") {
|
|
184
|
+
if (!latestContext) {
|
|
185
|
+
throw new Error("Pi session context is not ready.");
|
|
186
|
+
}
|
|
187
|
+
const models = latestContext.scopedModels.length > 0
|
|
188
|
+
? latestContext.scopedModels.map((entry) => entry.model)
|
|
189
|
+
: latestContext.modelRegistry.getAvailable();
|
|
190
|
+
writeFrame({
|
|
191
|
+
type: "response",
|
|
192
|
+
id: command.id,
|
|
193
|
+
success: true,
|
|
194
|
+
data: {
|
|
195
|
+
models: models.map((model) => ({
|
|
196
|
+
id: `${model.provider}/${model.id}`,
|
|
197
|
+
displayName: model.name
|
|
198
|
+
? `${model.name} (${model.provider})`
|
|
199
|
+
: `${model.id} (${model.provider})`,
|
|
200
|
+
isCurrent: latestContext?.model?.provider === model.provider &&
|
|
201
|
+
latestContext.model.id === model.id,
|
|
202
|
+
})),
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else if (command.type === "select_model" && typeof command.modelId === "string") {
|
|
207
|
+
if (!latestContext?.isIdle()) {
|
|
208
|
+
throw new Error("Pi TUI is already processing a turn.");
|
|
209
|
+
}
|
|
210
|
+
const models = latestContext.scopedModels.length > 0
|
|
211
|
+
? latestContext.scopedModels.map((entry) => entry.model)
|
|
212
|
+
: latestContext.modelRegistry.getAvailable();
|
|
213
|
+
const model = models.find((candidate) => `${candidate.provider}/${candidate.id}` === command.modelId);
|
|
214
|
+
if (!model) {
|
|
215
|
+
throw new Error(`Pi model is no longer available: ${command.modelId}`);
|
|
216
|
+
}
|
|
217
|
+
const selected = await pi.setModel(model);
|
|
218
|
+
if (!selected) {
|
|
219
|
+
throw new Error(`Pi could not switch to model: ${command.modelId}`);
|
|
220
|
+
}
|
|
221
|
+
writeFrame({
|
|
222
|
+
type: "response",
|
|
223
|
+
id: command.id,
|
|
224
|
+
success: true,
|
|
225
|
+
data: {
|
|
226
|
+
model: {
|
|
227
|
+
id: `${model.provider}/${model.id}`,
|
|
228
|
+
displayName: model.name
|
|
229
|
+
? `${model.name} (${model.provider})`
|
|
230
|
+
: `${model.id} (${model.provider})`,
|
|
231
|
+
isCurrent: true,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
183
236
|
else if (command.type === "new_session" && typeof command.id === "string") {
|
|
184
237
|
pi.sendUserMessage(`/${INTERNAL_NEW_COMMAND} ${command.id}`, {
|
|
185
238
|
expandPromptTemplates: true,
|
|
@@ -245,7 +298,7 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
245
298
|
try {
|
|
246
299
|
const parsed = JSON.parse(line);
|
|
247
300
|
if (isRecord(parsed)) {
|
|
248
|
-
dispatchCommand(parsed);
|
|
301
|
+
void dispatchCommand(parsed);
|
|
249
302
|
}
|
|
250
303
|
}
|
|
251
304
|
catch {
|
|
@@ -260,7 +313,7 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
260
313
|
latestContext = context;
|
|
261
314
|
writeSessionState(context, typeof event.reason === "string" ? event.reason : "startup");
|
|
262
315
|
for (const command of queuedCommands.splice(0)) {
|
|
263
|
-
executeCommand(command);
|
|
316
|
+
void executeCommand(command);
|
|
264
317
|
}
|
|
265
318
|
});
|
|
266
319
|
pi.on("input", (event, context) => {
|
|
@@ -26,6 +26,14 @@ export function parseBridgeControlCommand(text) {
|
|
|
26
26
|
return { type: "deny" };
|
|
27
27
|
case "/answer":
|
|
28
28
|
return argument ? { type: "answer", raw: argument } : null;
|
|
29
|
+
case "/model":
|
|
30
|
+
return { type: "model", target: argument || undefined };
|
|
31
|
+
case "/plan":
|
|
32
|
+
if (!argument || argument.toLowerCase() === "on")
|
|
33
|
+
return { type: "plan", enabled: true };
|
|
34
|
+
if (argument.toLowerCase() === "off")
|
|
35
|
+
return { type: "plan", enabled: false };
|
|
36
|
+
return null;
|
|
29
37
|
default:
|
|
30
38
|
return null;
|
|
31
39
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
/**
|
|
3
|
+
* Keeps the reply target scoped to one inbound async call chain. Different
|
|
4
|
+
* conversations may run concurrently without overwriting each other's target.
|
|
5
|
+
*/
|
|
6
|
+
export class InboundConversationContext {
|
|
7
|
+
storage = new AsyncLocalStorage();
|
|
8
|
+
run(conversation, callback) {
|
|
9
|
+
return this.storage.run(conversation, callback);
|
|
10
|
+
}
|
|
11
|
+
get() {
|
|
12
|
+
return this.storage.getStore();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function tryBeginTurn(state, task, conversation) {
|
|
16
|
+
if (state.activeTask) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const lease = {
|
|
20
|
+
task,
|
|
21
|
+
previousConversation: state.activeConversation,
|
|
22
|
+
};
|
|
23
|
+
state.activeTask = task;
|
|
24
|
+
if (conversation) {
|
|
25
|
+
state.activeConversation = conversation;
|
|
26
|
+
state.lastConversation = conversation;
|
|
27
|
+
}
|
|
28
|
+
return lease;
|
|
29
|
+
}
|
|
30
|
+
export function rollbackTurn(state, lease) {
|
|
31
|
+
if (state.activeTask !== lease.task) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
state.activeTask = null;
|
|
35
|
+
state.activeConversation = lease.previousConversation;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
export function clearTurn(state, expectedTask) {
|
|
39
|
+
if (expectedTask && state.activeTask !== expectedTask) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
state.activeTask = null;
|
|
43
|
+
state.activeConversation = undefined;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
export function resolveConversationTarget(params) {
|
|
47
|
+
return params.active ?? params.last ?? params.fallback;
|
|
48
|
+
}
|