@unlinearity/cli-wechat-bridge 1.2.1 → 1.2.3
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 +146 -73
- package/README_EN.md +544 -0
- package/bin/telegram-check-update.mjs +10 -0
- package/bin/telegram-claude.mjs +10 -0
- package/bin/telegram-codex.mjs +10 -0
- package/bin/telegram-daemon.mjs +10 -0
- package/bin/telegram-opencode.mjs +10 -0
- package/bin/telegram-pi.mjs +10 -0
- package/bin/telegram-setup.mjs +10 -0
- package/dist/bridge/adapter-control.js +19 -12
- package/dist/bridge/bridge-adapters.claude.js +219 -115
- package/dist/bridge/bridge-adapters.codex.js +532 -99
- package/dist/bridge/bridge-adapters.core.js +84 -7
- package/dist/bridge/bridge-adapters.opencode.js +202 -55
- package/dist/bridge/bridge-adapters.pi.js +23 -18
- package/dist/bridge/bridge-adapters.shared.js +9 -9
- package/dist/bridge/bridge-session-resume.js +13 -6
- package/dist/bridge/bridge-state.js +3 -1
- package/dist/bridge/bridge-utils.js +94 -144
- package/dist/bridge/claude-hooks.js +60 -3
- package/dist/bridge/claude-native-controls.js +19 -12
- package/dist/bridge/language-control.js +33 -0
- package/dist/bridge/native-terminal-control.js +26 -10
- package/dist/bridge/opencode-v2-compat.js +11 -3
- package/dist/bridge/wechat-bridge.js +154 -72
- package/dist/channels/channel-driver-factory.js +56 -0
- package/dist/channels/local/local-channel-port.js +40 -0
- package/dist/channels/local/local-driver.js +217 -0
- package/dist/channels/telegram/setup-cli.js +12 -0
- package/dist/channels/telegram/setup.js +163 -0
- package/dist/channels/telegram/telegram-channel-port.js +47 -0
- package/dist/channels/telegram/telegram-config.js +66 -0
- package/dist/channels/telegram/telegram-driver.js +106 -0
- package/dist/channels/telegram/telegram-message.js +41 -0
- package/dist/channels/telegram/telegram-transport.js +378 -0
- package/dist/channels/wechat/wechat-driver.js +11 -2
- package/dist/channels/wecom/wecom-driver.js +8 -1
- package/dist/companion/local-companion-start.js +180 -61
- package/dist/companion/local-companion.js +27 -7
- package/dist/companion/pi-tui-bridge-extension.js +10 -4
- package/dist/core/bridge-control.js +3 -0
- package/dist/core/bridge-defer.js +3 -2
- package/dist/core/bridge-event-forwarder.js +3 -0
- package/dist/core/channel-types.js +13 -1
- package/dist/daemon/wechat-daemon.js +239 -126
- package/dist/i18n/index.js +25 -11
- package/dist/i18n/messages-en.js +245 -1
- package/dist/i18n/messages-zh.js +245 -1
- package/dist/runtime/legacy-adapter-runtime.js +7 -4
- package/dist/wechat/channel-config.js +3 -2
- package/package.json +14 -4
- package/skills/codex-bridge/SKILL.md +35 -0
- package/skills/codex-bridge/agents/openai.yaml +4 -0
- package/skills/codex-bridge/scripts/install.mjs +28 -0
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
6
6
|
// must be released under AGPL-3.0-or-later with full source code; see
|
|
7
7
|
// LICENSE.txt. Network services built on it must offer source to users.
|
|
8
|
+
import fs from "node:fs";
|
|
8
9
|
import net from "node:net";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import { spawn } from "node:child_process";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { BRIDGE_LOG_FILE } from "../wechat/channel-config.js";
|
|
13
|
+
import { BRIDGE_LOG_FILE, getWorkspaceChannelPaths } from "../wechat/channel-config.js";
|
|
14
|
+
import { resolveCodexDaemonControlSocketPath, } from "../bridge/bridge-adapters.codex.js";
|
|
13
15
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
14
16
|
import { ensureWecomAccount } from "../channels/wecom/setup.js";
|
|
17
|
+
import { ensureTelegramAccount } from "../channels/telegram/setup.js";
|
|
18
|
+
import { getBridgeChannelDisplayName, isBridgeChannelId, normalizeBridgeChannelId, } from "../core/channel-types.js";
|
|
15
19
|
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
16
20
|
import { readBridgeLockFile, shouldAutoReclaimBridgeLock, } from "../bridge/bridge-state.js";
|
|
17
21
|
import { clearLocalCompanionOccupancy, clearLocalCompanionEndpoint, readLocalCompanionEndpoint, } from "./local-companion-link.js";
|
|
@@ -23,8 +27,8 @@ const MODULE_DIR = path.dirname(MODULE_FILE);
|
|
|
23
27
|
const RUNTIME_ENTRY_EXTENSION = path.extname(MODULE_FILE) === ".ts" ? ".ts" : ".js";
|
|
24
28
|
const DEFAULT_WAIT_TIMEOUT_MS = 15_000;
|
|
25
29
|
const DEFAULT_ADAPTER = "codex";
|
|
26
|
-
function log(
|
|
27
|
-
process.stderr.write(`[${channelId}-${adapter}] ${message}\n`);
|
|
30
|
+
function log(options, message) {
|
|
31
|
+
process.stderr.write(`[${options.channelId ?? "wechat"}-${options.adapter}] ${message}\n`);
|
|
28
32
|
}
|
|
29
33
|
export function normalizeComparablePath(cwd) {
|
|
30
34
|
const normalized = path.resolve(cwd);
|
|
@@ -115,6 +119,8 @@ export function parseCliArgs(argv) {
|
|
|
115
119
|
let timeoutMs = DEFAULT_WAIT_TIMEOUT_MS;
|
|
116
120
|
let sessionStartMode;
|
|
117
121
|
let channelId = "wechat";
|
|
122
|
+
let attachCurrentSession = false;
|
|
123
|
+
let codexThreadId;
|
|
118
124
|
const cliArgs = [];
|
|
119
125
|
for (let i = 0; i < argv.length; i += 1) {
|
|
120
126
|
const arg = argv[i];
|
|
@@ -134,15 +140,14 @@ export function parseCliArgs(argv) {
|
|
|
134
140
|
process.stdout.write([
|
|
135
141
|
`Usage: ${commandName} [--cwd <path>] [--profile <name-or-path>] [--timeout-ms <ms>] [--session-start-mode <restore|new>] [...${helpAdapter} args]`,
|
|
136
142
|
"",
|
|
137
|
-
`Ensures the ${channelId
|
|
143
|
+
`Ensures the ${getBridgeChannelDisplayName(channelId)} runtime for the current directory, then opens or reuses the visible CLI.`,
|
|
138
144
|
"If a same-workspace daemon is running, the command delegates to that daemon.",
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
: `${adapterLabel} starts a fresh session by default.`,
|
|
142
|
-
helpAdapter === "codex"
|
|
143
|
-
? "Use --session-start-mode new to explicitly start a fresh session."
|
|
144
|
-
: "Use --session-start-mode restore to explicitly restore the previous session.",
|
|
145
|
+
`${adapterLabel} starts a fresh session by default.`,
|
|
146
|
+
"Use --session-start-mode restore to explicitly restore the previous session.",
|
|
145
147
|
"Use --doctor to check the selected adapter and workspace without starting it.",
|
|
148
|
+
helpAdapter === "codex"
|
|
149
|
+
? `Use --attach-current-session to attach ${getBridgeChannelDisplayName(channelId)} to the live Codex daemon session in this directory (Codex >= 0.162 shared background server), instead of opening a visible client.`
|
|
150
|
+
: "",
|
|
146
151
|
"Without a daemon, the transient runtime is companion-bound and stops when the visible CLI closes.",
|
|
147
152
|
"Unknown arguments are forwarded to the visible CLI client.",
|
|
148
153
|
"",
|
|
@@ -153,7 +158,7 @@ export function parseCliArgs(argv) {
|
|
|
153
158
|
continue;
|
|
154
159
|
}
|
|
155
160
|
if (arg === "--channel") {
|
|
156
|
-
if (!next ||
|
|
161
|
+
if (!isBridgeChannelId(next) || next === "local") {
|
|
157
162
|
throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
|
|
158
163
|
}
|
|
159
164
|
channelId = next;
|
|
@@ -204,8 +209,23 @@ export function parseCliArgs(argv) {
|
|
|
204
209
|
i += 1;
|
|
205
210
|
continue;
|
|
206
211
|
}
|
|
212
|
+
if (arg === "--attach-current-session") {
|
|
213
|
+
attachCurrentSession = true;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (arg === "--codex-thread-id") {
|
|
217
|
+
if (!next) {
|
|
218
|
+
throw new Error("--codex-thread-id requires a value");
|
|
219
|
+
}
|
|
220
|
+
codexThreadId = next;
|
|
221
|
+
i += 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
207
224
|
cliArgs.push(arg);
|
|
208
225
|
}
|
|
226
|
+
if (attachCurrentSession && adapter !== "codex") {
|
|
227
|
+
throw new Error("--attach-current-session requires the codex adapter");
|
|
228
|
+
}
|
|
209
229
|
return {
|
|
210
230
|
adapter,
|
|
211
231
|
cwd,
|
|
@@ -214,6 +234,8 @@ export function parseCliArgs(argv) {
|
|
|
214
234
|
sessionStartMode: sessionStartMode ?? defaultSessionStartMode(adapter),
|
|
215
235
|
cliArgs,
|
|
216
236
|
channelId,
|
|
237
|
+
attachCurrentSession,
|
|
238
|
+
codexThreadId,
|
|
217
239
|
};
|
|
218
240
|
}
|
|
219
241
|
function isPidAlive(pid) {
|
|
@@ -238,9 +260,9 @@ async function waitForProcessExit(pid, timeoutMs) {
|
|
|
238
260
|
}
|
|
239
261
|
return !isPidAlive(pid);
|
|
240
262
|
}
|
|
241
|
-
async function stopExistingBridge(lock,
|
|
263
|
+
async function stopExistingBridge(lock, options) {
|
|
242
264
|
const { pid, cwd } = lock;
|
|
243
|
-
log(
|
|
265
|
+
log(options, `Stopping existing bridge for ${cwd} (pid=${pid})...`);
|
|
244
266
|
try {
|
|
245
267
|
process.kill(pid);
|
|
246
268
|
}
|
|
@@ -264,7 +286,7 @@ async function stopExistingBridge(lock, requestedAdapter) {
|
|
|
264
286
|
else {
|
|
265
287
|
clearLocalCompanionEndpoint(cwd);
|
|
266
288
|
}
|
|
267
|
-
log(
|
|
289
|
+
log(options, `Cleared stale local companion endpoint for previous workspace ${cwd}.`);
|
|
268
290
|
}
|
|
269
291
|
async function isEndpointReachable(endpoint) {
|
|
270
292
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
@@ -289,7 +311,8 @@ async function isEndpointReachable(endpoint) {
|
|
|
289
311
|
socket.once("error", () => finish(false));
|
|
290
312
|
});
|
|
291
313
|
}
|
|
292
|
-
async function readUsableEndpoint(
|
|
314
|
+
async function readUsableEndpoint(options) {
|
|
315
|
+
const { cwd, adapter } = options;
|
|
293
316
|
const endpoint = readLocalCompanionEndpoint(cwd, { adapter });
|
|
294
317
|
if (!endpoint || endpoint.kind !== adapter) {
|
|
295
318
|
return { endpoint: null };
|
|
@@ -298,7 +321,7 @@ async function readUsableEndpoint(cwd, adapter) {
|
|
|
298
321
|
return { endpoint };
|
|
299
322
|
}
|
|
300
323
|
clearLocalCompanionEndpoint(cwd, endpoint.instanceId, { adapter });
|
|
301
|
-
log(
|
|
324
|
+
log(options, `Removed stale local companion endpoint for ${cwd}.`);
|
|
302
325
|
return { endpoint: null };
|
|
303
326
|
}
|
|
304
327
|
function isCompanionAlive(endpoint) {
|
|
@@ -314,21 +337,31 @@ function isCompanionAlive(endpoint) {
|
|
|
314
337
|
return false;
|
|
315
338
|
}
|
|
316
339
|
export function buildBackgroundBridgeArgs(entryPath, options) {
|
|
317
|
-
|
|
340
|
+
// The attach bridge outlives this launcher (no visible client keeps the
|
|
341
|
+
// foreground alive), so it must not be companion-bound to the launcher pid.
|
|
342
|
+
const lifecycle = options.attachCurrentSession ? "persistent" : "companion_bound";
|
|
318
343
|
const args = ["--no-warnings"];
|
|
319
344
|
if (path.extname(entryPath) === ".ts") {
|
|
320
345
|
args.push("--experimental-strip-types");
|
|
321
346
|
}
|
|
322
347
|
args.push(entryPath, "--adapter", options.adapter, "--cwd", options.cwd, "--lifecycle", lifecycle);
|
|
323
|
-
if (options.channelId
|
|
324
|
-
args.push("--channel",
|
|
348
|
+
if (options.channelId) {
|
|
349
|
+
args.push("--channel", options.channelId);
|
|
325
350
|
}
|
|
326
|
-
|
|
327
|
-
|
|
351
|
+
// Attach discovery owns thread selection; do not restore stale bridge state.
|
|
352
|
+
const sessionStartMode = options.attachCurrentSession ? "new" : options.sessionStartMode;
|
|
353
|
+
if (sessionStartMode !== "restore") {
|
|
354
|
+
args.push("--session-start-mode", sessionStartMode);
|
|
328
355
|
}
|
|
329
356
|
if (options.profile) {
|
|
330
357
|
args.push("--profile", options.profile);
|
|
331
358
|
}
|
|
359
|
+
if (options.attachCurrentSession) {
|
|
360
|
+
args.push("--codex-attach-daemon");
|
|
361
|
+
if (options.codexThreadId) {
|
|
362
|
+
args.push("--codex-thread-id", options.codexThreadId);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
332
365
|
return args;
|
|
333
366
|
}
|
|
334
367
|
function startBridgeInBackground(options) {
|
|
@@ -348,10 +381,11 @@ function startBridgeInBackground(options) {
|
|
|
348
381
|
});
|
|
349
382
|
child.unref();
|
|
350
383
|
}
|
|
351
|
-
async function waitForEndpoint(
|
|
384
|
+
async function waitForEndpoint(options) {
|
|
385
|
+
const { cwd, adapter, timeoutMs } = options;
|
|
352
386
|
const deadline = Date.now() + timeoutMs;
|
|
353
387
|
while (Date.now() < deadline) {
|
|
354
|
-
const result = await readUsableEndpoint(
|
|
388
|
+
const result = await readUsableEndpoint(options);
|
|
355
389
|
if (result.endpoint) {
|
|
356
390
|
return result.endpoint;
|
|
357
391
|
}
|
|
@@ -361,25 +395,25 @@ async function waitForEndpoint(cwd, adapter, timeoutMs) {
|
|
|
361
395
|
}
|
|
362
396
|
async function ensureBridgeReady(options) {
|
|
363
397
|
// If the lock is absent or the lock-holding process is dead, do NOT trust a
|
|
364
|
-
// leftover endpoint. The
|
|
398
|
+
// leftover endpoint. The channel transport may have crashed while
|
|
365
399
|
// the opencode server kept running. Starting only the panel would leave no
|
|
366
|
-
// bridge to
|
|
400
|
+
// bridge to receive channel messages.
|
|
367
401
|
const lock = readBridgeLockFile();
|
|
368
402
|
const lockProcessAlive = lock ? isPidAlive(lock.pid) : false;
|
|
369
403
|
if (!lock || !lockProcessAlive) {
|
|
370
404
|
if (lock && !lockProcessAlive) {
|
|
371
|
-
log(options
|
|
405
|
+
log(options, `Found stale lock for ${options.cwd} (pid=${lock.pid} dead). Clearing.`);
|
|
372
406
|
clearLocalCompanionEndpoint(options.cwd, undefined, {
|
|
373
407
|
adapter: options.adapter,
|
|
374
408
|
});
|
|
375
409
|
}
|
|
376
|
-
log(options
|
|
410
|
+
log(options, `Starting bridge in background for ${options.cwd}...`);
|
|
377
411
|
startBridgeInBackground(options);
|
|
378
|
-
await waitForEndpoint(options
|
|
412
|
+
await waitForEndpoint(options);
|
|
379
413
|
return { shouldOpenVisibleClient: true };
|
|
380
414
|
}
|
|
381
415
|
// Lock is held by a live process; decide whether to reuse, switch, or replace it.
|
|
382
|
-
const endpointResult = await readUsableEndpoint(options
|
|
416
|
+
const endpointResult = await readUsableEndpoint(options);
|
|
383
417
|
const decision = decideLaunchAction({
|
|
384
418
|
requestedAdapter: options.adapter,
|
|
385
419
|
requestedCwd: options.cwd,
|
|
@@ -391,31 +425,31 @@ async function ensureBridgeReady(options) {
|
|
|
391
425
|
sessionStartMode: options.sessionStartMode,
|
|
392
426
|
requestedChannelId: options.channelId,
|
|
393
427
|
});
|
|
394
|
-
log(options
|
|
428
|
+
log(options, decision.message);
|
|
395
429
|
if (decision.kind === "already_active") {
|
|
396
430
|
return { shouldOpenVisibleClient: false };
|
|
397
431
|
}
|
|
398
432
|
if (decision.kind === "open_companion") {
|
|
399
433
|
if (!endpointResult.endpoint) {
|
|
400
|
-
await waitForEndpoint(options
|
|
434
|
+
await waitForEndpoint(options);
|
|
401
435
|
}
|
|
402
436
|
return { shouldOpenVisibleClient: true };
|
|
403
437
|
}
|
|
404
438
|
if (decision.kind === "switch_workspace") {
|
|
405
439
|
try {
|
|
406
|
-
await stopExistingBridge(lock, options
|
|
440
|
+
await stopExistingBridge(lock, options);
|
|
407
441
|
}
|
|
408
442
|
catch (error) {
|
|
409
|
-
log(options
|
|
443
|
+
log(options, decision.failureMessage);
|
|
410
444
|
throw error;
|
|
411
445
|
}
|
|
412
446
|
}
|
|
413
447
|
else {
|
|
414
|
-
await stopExistingBridge(lock, options
|
|
448
|
+
await stopExistingBridge(lock, options);
|
|
415
449
|
}
|
|
416
|
-
log(options
|
|
450
|
+
log(options, `Starting replacement bridge in background for ${options.cwd}...`);
|
|
417
451
|
startBridgeInBackground(options);
|
|
418
|
-
await waitForEndpoint(options
|
|
452
|
+
await waitForEndpoint(options);
|
|
419
453
|
return { shouldOpenVisibleClient: true };
|
|
420
454
|
}
|
|
421
455
|
// The daemon's ensure_slot can legitimately take a while: opening a visible
|
|
@@ -460,10 +494,10 @@ export async function tryDelegateToDaemon(options, deps = {}) {
|
|
|
460
494
|
// launch: fall back to a local companion-bound bridge so the user still
|
|
461
495
|
// gets a working terminal instead of an error for work the daemon may
|
|
462
496
|
// still complete.
|
|
463
|
-
log(options
|
|
497
|
+
log(options, `Daemon delegation failed (${response.error ?? "unknown error"}). Falling back to a local bridge.`);
|
|
464
498
|
return false;
|
|
465
499
|
}
|
|
466
|
-
log(options
|
|
500
|
+
log(options, `Delegated to running ${requestedChannel}-daemon for ${options.cwd}.`);
|
|
467
501
|
return true;
|
|
468
502
|
}
|
|
469
503
|
export async function runVisibleClient(options, runners = {}) {
|
|
@@ -487,17 +521,30 @@ export async function ensureCompanionStartWechatCredentials(adapter, ensureCrede
|
|
|
487
521
|
await ensureCredentials({
|
|
488
522
|
requireUserId: true,
|
|
489
523
|
validateExisting: true,
|
|
490
|
-
log: (message) => log(adapter, message),
|
|
524
|
+
log: (message) => log({ adapter, channelId: "wechat" }, message),
|
|
491
525
|
});
|
|
492
526
|
}
|
|
493
|
-
|
|
494
|
-
|
|
527
|
+
const channelCredentialSetups = {
|
|
528
|
+
wechat: async (options) => {
|
|
529
|
+
await ensureCompanionStartWechatCredentials(options.adapter);
|
|
530
|
+
},
|
|
531
|
+
wecom: async (options) => {
|
|
495
532
|
await ensureWecomAccount({
|
|
496
|
-
log: (message) => log(options
|
|
533
|
+
log: (message) => log(options, message),
|
|
534
|
+
});
|
|
535
|
+
},
|
|
536
|
+
telegram: async (options) => {
|
|
537
|
+
await ensureTelegramAccount({
|
|
538
|
+
log: (message) => log(options, message),
|
|
497
539
|
});
|
|
498
|
-
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
export async function ensureCompanionStartChannelCredentials(options) {
|
|
543
|
+
const channelId = options.channelId ?? "wechat";
|
|
544
|
+
if (channelId === "local") {
|
|
545
|
+
throw new Error("The local channel does not support companion launchers.");
|
|
499
546
|
}
|
|
500
|
-
await
|
|
547
|
+
await channelCredentialSetups[channelId](options);
|
|
501
548
|
}
|
|
502
549
|
export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
|
|
503
550
|
if (argv.includes("--doctor")) {
|
|
@@ -507,6 +554,9 @@ export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
|
|
|
507
554
|
}
|
|
508
555
|
const options = parseCliArgs(argv);
|
|
509
556
|
await ensureCompanionStartChannelCredentials(options);
|
|
557
|
+
if (options.attachCurrentSession) {
|
|
558
|
+
return await runAttachCurrentSession(options);
|
|
559
|
+
}
|
|
510
560
|
if (await tryDelegateToDaemon(options)) {
|
|
511
561
|
return 0;
|
|
512
562
|
}
|
|
@@ -516,29 +566,98 @@ export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
|
|
|
516
566
|
}
|
|
517
567
|
return await runVisibleClient(options);
|
|
518
568
|
}
|
|
569
|
+
export function resolveAttachedBridgeThreadId(state, lock, options) {
|
|
570
|
+
if (!lock || !state || typeof state !== "object") {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const parsed = state;
|
|
574
|
+
const channelId = options.channelId ?? "wechat";
|
|
575
|
+
if (lock.adapter !== "codex" ||
|
|
576
|
+
!isSameWorkspaceCwd(lock.cwd, options.cwd) ||
|
|
577
|
+
(lock.channelId ?? "wechat") !== channelId ||
|
|
578
|
+
parsed.instanceId !== lock.instanceId ||
|
|
579
|
+
parsed.adapter !== "codex" ||
|
|
580
|
+
typeof parsed.cwd !== "string" ||
|
|
581
|
+
!isSameWorkspaceCwd(parsed.cwd, options.cwd) ||
|
|
582
|
+
(parsed.channelId ?? "wechat") !== channelId ||
|
|
583
|
+
typeof parsed.sharedThreadId !== "string" ||
|
|
584
|
+
!parsed.sharedThreadId ||
|
|
585
|
+
(options.codexThreadId && parsed.sharedThreadId !== options.codexThreadId)) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
return parsed.sharedThreadId;
|
|
589
|
+
}
|
|
590
|
+
export function formatAttachedBridgeMessage(options, sharedThreadId) {
|
|
591
|
+
return `Attached Codex daemon thread ${sharedThreadId.slice(0, 8)} for ${options.cwd}. ${getBridgeChannelDisplayName(options.channelId ?? "wechat")} forwarding is active; the visible Codex session keeps running unchanged.`;
|
|
592
|
+
}
|
|
593
|
+
async function waitForAttachedBridgeState(options) {
|
|
594
|
+
const { cwd, timeoutMs } = options;
|
|
595
|
+
const stateFile = getWorkspaceChannelPaths(cwd).stateFile;
|
|
596
|
+
const deadline = Date.now() + timeoutMs;
|
|
597
|
+
while (Date.now() < deadline) {
|
|
598
|
+
try {
|
|
599
|
+
const raw = fs.readFileSync(stateFile, "utf8");
|
|
600
|
+
const parsed = JSON.parse(raw);
|
|
601
|
+
const lock = readBridgeLockFile();
|
|
602
|
+
const sharedThreadId = resolveAttachedBridgeThreadId(parsed, lock, options);
|
|
603
|
+
if (sharedThreadId && lock && isPidAlive(lock.pid)) {
|
|
604
|
+
return { sharedThreadId };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
// Not written yet; keep polling until the deadline.
|
|
609
|
+
}
|
|
610
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
611
|
+
}
|
|
612
|
+
throw new Error(`Timed out waiting for the attach bridge to bind a Codex thread for ${cwd}. Check ${BRIDGE_LOG_FILE}.`);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Attach flow: bind the selected channel to the live Codex shared-daemon session in the
|
|
616
|
+
* workspace. No visible client is opened — the owning Codex TUI stays as-is —
|
|
617
|
+
* so readiness is the persisted bridge state carrying the attached thread id.
|
|
618
|
+
*/
|
|
619
|
+
async function runAttachCurrentSession(options) {
|
|
620
|
+
const daemonEndpoint = readDaemonEndpoint();
|
|
621
|
+
if (daemonEndpoint && (await isDaemonEndpointAlive(daemonEndpoint))) {
|
|
622
|
+
const channel = daemonEndpoint.channelId ?? "wechat";
|
|
623
|
+
throw new Error(`A ${channel}-daemon is running for ${daemonEndpoint.cwd}. Attach mode needs the workspace bridge lock; stop the daemon first or use its slots.`);
|
|
624
|
+
}
|
|
625
|
+
const socketPath = resolveCodexDaemonControlSocketPath();
|
|
626
|
+
if (!fs.existsSync(socketPath)) {
|
|
627
|
+
throw new Error(`No shared Codex app-server daemon socket at ${socketPath}. Attach mode requires Codex >= 0.162 with its shared background server (the default). Sessions started with --no-daemon, --oss, or a --profile, and desktop-app embedded sessions, cannot be attached.`);
|
|
628
|
+
}
|
|
629
|
+
const lock = readBridgeLockFile();
|
|
630
|
+
if (lock) {
|
|
631
|
+
if (isPidAlive(lock.pid)) {
|
|
632
|
+
await stopExistingBridge(lock, options);
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
clearLocalCompanionEndpoint(options.cwd, undefined, { adapter: options.adapter });
|
|
636
|
+
log(options, `Cleared stale bridge lock and endpoint for ${lock.cwd}.`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
log(options, `Starting attach bridge in background for ${options.cwd}...`);
|
|
640
|
+
startBridgeInBackground(options);
|
|
641
|
+
const state = await waitForAttachedBridgeState(options);
|
|
642
|
+
log(options, formatAttachedBridgeMessage(options, state.sharedThreadId));
|
|
643
|
+
return 0;
|
|
644
|
+
}
|
|
519
645
|
export async function main(argv = process.argv.slice(2)) {
|
|
520
646
|
try {
|
|
521
647
|
const exitCode = await runLocalCompanionStart(argv);
|
|
522
648
|
process.exit(exitCode);
|
|
523
649
|
}
|
|
524
650
|
catch (error) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const channelId = (
|
|
534
|
-
|
|
535
|
-
return parseCliArgs(argv).channelId ?? "wechat";
|
|
536
|
-
}
|
|
537
|
-
catch {
|
|
538
|
-
return "wechat";
|
|
539
|
-
}
|
|
540
|
-
})();
|
|
541
|
-
log(adapter, error instanceof Error ? error.message : String(error), channelId);
|
|
651
|
+
// Argument validation may fail; retain the wrapper's channel and adapter
|
|
652
|
+
// without running the failing parser again.
|
|
653
|
+
const adapterIndex = argv.lastIndexOf("--adapter");
|
|
654
|
+
const adapterValue = adapterIndex >= 0 ? argv[adapterIndex + 1] : undefined;
|
|
655
|
+
const adapter = adapterValue && ["codex", "claude", "opencode", "pi"].includes(adapterValue)
|
|
656
|
+
? adapterValue
|
|
657
|
+
: DEFAULT_ADAPTER;
|
|
658
|
+
const channelIndex = argv.lastIndexOf("--channel");
|
|
659
|
+
const channelId = normalizeBridgeChannelId(channelIndex >= 0 ? argv[channelIndex + 1] : undefined);
|
|
660
|
+
log({ adapter, channelId }, error instanceof Error ? error.message : String(error));
|
|
542
661
|
process.exit(1);
|
|
543
662
|
}
|
|
544
663
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// LICENSE.txt. Network services built on it must offer source to users.
|
|
8
8
|
import net from "node:net";
|
|
9
9
|
import path from "node:path";
|
|
10
|
+
import { parseLocale, setLocale, t } from "../i18n/index.js";
|
|
10
11
|
import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
|
|
11
12
|
import { LOCAL_COMPANION_RECONNECT_GRACE_MS, } from "../bridge/bridge-adapters.shared.js";
|
|
12
13
|
import { runCodexRemoteClientFromEndpoint } from "./codex-remote-client.js";
|
|
@@ -170,7 +171,7 @@ export async function runLocalCompanion(options) {
|
|
|
170
171
|
}
|
|
171
172
|
sendLocalCompanionMessage(activeSocket, {
|
|
172
173
|
type: "state",
|
|
173
|
-
state: adapter.getState(),
|
|
174
|
+
state: { ...adapter.getState(), supportsLocaleUpdates: true },
|
|
174
175
|
});
|
|
175
176
|
};
|
|
176
177
|
const sendResponse = (socket, id, ok, result, error) => {
|
|
@@ -211,6 +212,15 @@ export async function runLocalCompanion(options) {
|
|
|
211
212
|
const handleBridgeRequest = async (socket, message) => {
|
|
212
213
|
try {
|
|
213
214
|
switch (message.payload.command) {
|
|
215
|
+
case "set_locale": {
|
|
216
|
+
const locale = parseLocale(message.payload.locale);
|
|
217
|
+
if (!locale)
|
|
218
|
+
throw new Error(t("language.usage"));
|
|
219
|
+
setLocale(locale);
|
|
220
|
+
process.env.CLI_BRIDGE_LANG = locale;
|
|
221
|
+
sendResponse(socket, message.id, true);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
214
224
|
case "send_input":
|
|
215
225
|
await adapter.sendInput(message.payload.text);
|
|
216
226
|
sendResponse(socket, message.id, true);
|
|
@@ -239,18 +249,18 @@ export async function runLocalCompanion(options) {
|
|
|
239
249
|
break;
|
|
240
250
|
case "list_models":
|
|
241
251
|
if (!adapter.listModels)
|
|
242
|
-
throw new Error("
|
|
252
|
+
throw new Error(t("companion.modelsUnsupported"));
|
|
243
253
|
sendResponse(socket, message.id, true, await adapter.listModels());
|
|
244
254
|
break;
|
|
245
255
|
case "select_model":
|
|
246
256
|
if (!adapter.selectModel)
|
|
247
|
-
throw new Error("
|
|
257
|
+
throw new Error(t("companion.modelUnsupported"));
|
|
248
258
|
sendResponse(socket, message.id, true, await adapter.selectModel(message.payload.modelId));
|
|
249
259
|
publishState();
|
|
250
260
|
break;
|
|
251
261
|
case "set_plan_mode":
|
|
252
262
|
if (!adapter.setPlanMode)
|
|
253
|
-
throw new Error("
|
|
263
|
+
throw new Error(t("companion.planUnsupported"));
|
|
254
264
|
sendResponse(socket, message.id, true, await adapter.setPlanMode(message.payload.enabled));
|
|
255
265
|
publishState();
|
|
256
266
|
break;
|
|
@@ -263,10 +273,14 @@ export async function runLocalCompanion(options) {
|
|
|
263
273
|
sendResponse(socket, message.id, true);
|
|
264
274
|
break;
|
|
265
275
|
case "resolve_approval":
|
|
266
|
-
sendResponse(socket, message.id, true,
|
|
276
|
+
sendResponse(socket, message.id, true, message.payload.requestId === undefined
|
|
277
|
+
? false
|
|
278
|
+
: await adapter.resolveApproval(message.payload.action, message.payload.requestId));
|
|
267
279
|
break;
|
|
268
280
|
case "submit_user_input":
|
|
269
|
-
sendResponse(socket, message.id, true,
|
|
281
|
+
sendResponse(socket, message.id, true, adapter.getState().supportsUserInputRequestIds && !message.payload.requestId
|
|
282
|
+
? false
|
|
283
|
+
: await adapter.submitUserInput(message.payload.answers, message.payload.requestId));
|
|
270
284
|
break;
|
|
271
285
|
case "dispose":
|
|
272
286
|
sendResponse(socket, message.id, true);
|
|
@@ -308,6 +322,11 @@ export async function runLocalCompanion(options) {
|
|
|
308
322
|
localDetach = attachLocalCompanionMessageListener(socket, (message) => {
|
|
309
323
|
if (!helloAcknowledged) {
|
|
310
324
|
if (message.type === "hello_ack") {
|
|
325
|
+
const locale = parseLocale(message.locale);
|
|
326
|
+
if (locale) {
|
|
327
|
+
setLocale(locale);
|
|
328
|
+
process.env.CLI_BRIDGE_LANG = locale;
|
|
329
|
+
}
|
|
311
330
|
helloAcknowledged = true;
|
|
312
331
|
closeReason = null;
|
|
313
332
|
activeSocket = socket;
|
|
@@ -356,6 +375,7 @@ export async function runLocalCompanion(options) {
|
|
|
356
375
|
type: "hello",
|
|
357
376
|
token: endpoint.token,
|
|
358
377
|
companionPid: process.pid,
|
|
378
|
+
supportsLocaleUpdates: true,
|
|
359
379
|
});
|
|
360
380
|
});
|
|
361
381
|
socket.once("error", (error) => {
|
|
@@ -402,13 +422,13 @@ export async function runLocalCompanion(options) {
|
|
|
402
422
|
}
|
|
403
423
|
};
|
|
404
424
|
adapter.setEventSink((event) => {
|
|
425
|
+
publishState();
|
|
405
426
|
if (activeSocket) {
|
|
406
427
|
sendLocalCompanionMessage(activeSocket, {
|
|
407
428
|
type: "event",
|
|
408
429
|
event,
|
|
409
430
|
});
|
|
410
431
|
}
|
|
411
|
-
publishState();
|
|
412
432
|
if (event.type === "fatal_error") {
|
|
413
433
|
void closeCompanion(1, "fatal_error");
|
|
414
434
|
return;
|
|
@@ -330,11 +330,14 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
330
330
|
});
|
|
331
331
|
pi.on("message_end", (event, context) => {
|
|
332
332
|
latestContext = context;
|
|
333
|
+
const message = isRecord(event.message) ? event.message : {};
|
|
334
|
+
if (message.role !== "assistant") {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
333
337
|
const text = extractAssistantText(event.message);
|
|
334
|
-
if (!text) {
|
|
338
|
+
if (!text && message.stopReason !== "error") {
|
|
335
339
|
return;
|
|
336
340
|
}
|
|
337
|
-
const message = isRecord(event.message) ? event.message : {};
|
|
338
341
|
writeFrame({
|
|
339
342
|
type: "assistant_message",
|
|
340
343
|
text,
|
|
@@ -346,9 +349,12 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
346
349
|
latestContext = context;
|
|
347
350
|
writeFrame({ type: "agent_start" });
|
|
348
351
|
});
|
|
349
|
-
pi.on("agent_settled", (
|
|
352
|
+
pi.on("agent_settled", (event, context) => {
|
|
350
353
|
latestContext = context;
|
|
351
354
|
writeSessionState(context, "settled");
|
|
352
|
-
writeFrame({
|
|
355
|
+
writeFrame({
|
|
356
|
+
type: "agent_settled",
|
|
357
|
+
...(typeof event.aborted === "boolean" ? { aborted: event.aborted } : {}),
|
|
358
|
+
});
|
|
353
359
|
});
|
|
354
360
|
}
|
|
@@ -7,6 +7,9 @@ export function parseBridgeControlCommand(text) {
|
|
|
7
7
|
const command = rawCommand.toLowerCase();
|
|
8
8
|
const argument = rest.join(" ").trim();
|
|
9
9
|
switch (command) {
|
|
10
|
+
case "/lang":
|
|
11
|
+
case "/language":
|
|
12
|
+
return { type: "language", target: argument || undefined };
|
|
10
13
|
case "/status":
|
|
11
14
|
return { type: "status" };
|
|
12
15
|
case "/resume":
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t } from "../i18n/index.js";
|
|
1
2
|
export function shouldDeferCodexInboundMessage(params) {
|
|
2
3
|
return (params.adapter === "codex" &&
|
|
3
4
|
!params.hasPendingConfirmation &&
|
|
@@ -18,8 +19,8 @@ export function canDrainDeferredCodexInboundQueue(params) {
|
|
|
18
19
|
params.status !== "awaiting_input");
|
|
19
20
|
}
|
|
20
21
|
export function formatDeferredCodexInboundQueueMessage(queuePosition) {
|
|
21
|
-
return
|
|
22
|
+
return t("bridge.deferred", { position: queuePosition });
|
|
22
23
|
}
|
|
23
24
|
export function isRetryableDeferredCodexDrainError(errorText) {
|
|
24
|
-
return /still working|approval request is pending|waiting for local terminal input
|
|
25
|
+
return /still working|approval request is pending|waiting for local terminal input|仍在运行|有待审批请求|正在等待本地终端输入/i.test(errorText);
|
|
25
26
|
}
|
|
@@ -19,6 +19,9 @@ export async function forwardBridgeEvent(event, handlers) {
|
|
|
19
19
|
case "thinking":
|
|
20
20
|
await handlers.thinking?.(event);
|
|
21
21
|
return;
|
|
22
|
+
case "approval_state_changed":
|
|
23
|
+
await handlers.approvalStateChanged?.(event);
|
|
24
|
+
return;
|
|
22
25
|
case "approval_required":
|
|
23
26
|
await handlers.approvalRequired?.(event);
|
|
24
27
|
return;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
export const BRIDGE_CHANNEL_DISPLAY_NAMES = {
|
|
2
|
+
wechat: "WeChat",
|
|
3
|
+
wecom: "WeCom",
|
|
4
|
+
telegram: "Telegram",
|
|
5
|
+
local: "Local",
|
|
6
|
+
};
|
|
7
|
+
export function isBridgeChannelId(value) {
|
|
8
|
+
return typeof value === "string" && Object.hasOwn(BRIDGE_CHANNEL_DISPLAY_NAMES, value);
|
|
9
|
+
}
|
|
10
|
+
export function getBridgeChannelDisplayName(channelId) {
|
|
11
|
+
return BRIDGE_CHANNEL_DISPLAY_NAMES[channelId];
|
|
12
|
+
}
|
|
1
13
|
export function normalizeBridgeChannelId(value) {
|
|
2
|
-
return value
|
|
14
|
+
return isBridgeChannelId(value) ? value : "wechat";
|
|
3
15
|
}
|