@unlinearity/cli-wechat-bridge 1.2.2 → 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 +131 -72
- 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/bridge-adapters.codex.js +325 -16
- package/dist/bridge/bridge-state.js +3 -1
- package/dist/bridge/wechat-bridge.js +97 -30
- package/dist/channels/channel-driver-factory.js +12 -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/companion/local-companion-start.js +180 -61
- package/dist/core/channel-types.js +13 -5
- package/dist/daemon/wechat-daemon.js +51 -16
- package/dist/i18n/messages-en.js +1 -0
- package/dist/i18n/messages-zh.js +1 -0
- 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
|
}
|
|
@@ -1,7 +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
|
-
|
|
3
|
-
return "wecom";
|
|
4
|
-
if (value === "local")
|
|
5
|
-
return "local";
|
|
6
|
-
return "wechat";
|
|
14
|
+
return isBridgeChannelId(value) ? value : "wechat";
|
|
7
15
|
}
|
|
@@ -39,11 +39,15 @@ import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
|
|
|
39
39
|
import { isDirectModuleRun } from "../core/direct-run.js";
|
|
40
40
|
import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
|
|
41
41
|
import { ensureWecomAccount } from "../channels/wecom/setup.js";
|
|
42
|
+
import { ensureTelegramAccount } from "../channels/telegram/setup.js";
|
|
43
|
+
import { TelegramTransport } from "../channels/telegram/telegram-transport.js";
|
|
44
|
+
import { TelegramChannelPort } from "../channels/telegram/telegram-channel-port.js";
|
|
42
45
|
import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
|
|
43
46
|
import { WecomTransport } from "../channels/wecom/wecom-transport.js";
|
|
44
47
|
import { createChannelDriver } from "../channels/channel-driver-factory.js";
|
|
45
48
|
import { LocalChannelPort } from "../channels/local/local-channel-port.js";
|
|
46
49
|
import { getLocalChannelFiles, LOCAL_CHANNEL_OPERATOR_ID, } from "../channels/local/local-driver.js";
|
|
50
|
+
import { getBridgeChannelDisplayName, isBridgeChannelId, normalizeBridgeChannelId, } from "../core/channel-types.js";
|
|
47
51
|
import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
48
52
|
import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION, isPidAlive, readDaemonEndpoint, sendDaemonRequest, sendDaemonResponse, writeDaemonEndpoint, } from "./daemon-link.js";
|
|
49
53
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
@@ -65,11 +69,7 @@ const DAEMON_ADAPTERS = ["codex", "claude", "opencode", "pi"];
|
|
|
65
69
|
function getCliChannelId(argv = process.argv.slice(2)) {
|
|
66
70
|
const index = argv.indexOf("--channel");
|
|
67
71
|
const value = index >= 0 ? argv[index + 1] : undefined;
|
|
68
|
-
|
|
69
|
-
return "wecom";
|
|
70
|
-
if (value === "local")
|
|
71
|
-
return "local";
|
|
72
|
-
return "wechat";
|
|
72
|
+
return normalizeBridgeChannelId(value);
|
|
73
73
|
}
|
|
74
74
|
function log(message) {
|
|
75
75
|
process.stderr.write(`[${getCliChannelId()}-daemon] ${message}\n`);
|
|
@@ -141,7 +141,7 @@ export function parseDaemonCliArgs(argv) {
|
|
|
141
141
|
}
|
|
142
142
|
const next = argv[i + 1];
|
|
143
143
|
if (arg === "--help" || arg === "-h") {
|
|
144
|
-
const channelDisplayName = channelId
|
|
144
|
+
const channelDisplayName = getBridgeChannelDisplayName(channelId);
|
|
145
145
|
process.stdout.write([
|
|
146
146
|
`Usage: ${channelId}-daemon [--cwd <path>] [--adapter <codex|claude|opencode|pi>] [--profile <name-or-path>] [--no-open]`,
|
|
147
147
|
"",
|
|
@@ -160,7 +160,7 @@ export function parseDaemonCliArgs(argv) {
|
|
|
160
160
|
continue;
|
|
161
161
|
}
|
|
162
162
|
if (arg === "--channel") {
|
|
163
|
-
if (!
|
|
163
|
+
if (!isBridgeChannelId(next)) {
|
|
164
164
|
throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
|
|
165
165
|
}
|
|
166
166
|
channelId = next;
|
|
@@ -573,6 +573,7 @@ export class WechatDaemon {
|
|
|
573
573
|
channelDriver;
|
|
574
574
|
channelId;
|
|
575
575
|
wecomTransport;
|
|
576
|
+
telegramTransport;
|
|
576
577
|
localFiles;
|
|
577
578
|
inboundConversationContext = new InboundConversationContext();
|
|
578
579
|
fallbackConversation;
|
|
@@ -601,6 +602,7 @@ export class WechatDaemon {
|
|
|
601
602
|
this.transport = params.transport;
|
|
602
603
|
this.channelId = params.channelId ?? "wechat";
|
|
603
604
|
this.wecomTransport = params.wecomTransport ?? null;
|
|
605
|
+
this.telegramTransport = params.telegramTransport ?? null;
|
|
604
606
|
this.localFiles = params.localFiles ?? null;
|
|
605
607
|
this.deps = params.deps ?? {};
|
|
606
608
|
this.channelDriver = createChannelDriver({
|
|
@@ -609,6 +611,7 @@ export class WechatDaemon {
|
|
|
609
611
|
accountId: params.accountId,
|
|
610
612
|
wechatTransport: this.transport,
|
|
611
613
|
wecomTransport: this.wecomTransport,
|
|
614
|
+
telegramTransport: this.telegramTransport,
|
|
612
615
|
localFiles: this.localFiles,
|
|
613
616
|
buildWechatInboundPrompt: (text, attachments) => buildWechatInboundPrompt(text, attachments.filter((attachment) => attachment.kind === "image" || attachment.kind === "file")),
|
|
614
617
|
log: (message) => this.daemonLog(message),
|
|
@@ -893,6 +896,12 @@ export class WechatDaemon {
|
|
|
893
896
|
catch {
|
|
894
897
|
// Best effort shutdown.
|
|
895
898
|
}
|
|
899
|
+
try {
|
|
900
|
+
await this.telegramTransport?.stop();
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
// Best effort shutdown.
|
|
904
|
+
}
|
|
896
905
|
clearDaemonEndpoint();
|
|
897
906
|
for (const slot of this.slots.values()) {
|
|
898
907
|
try {
|
|
@@ -1402,6 +1411,16 @@ export class WechatDaemon {
|
|
|
1402
1411
|
},
|
|
1403
1412
|
});
|
|
1404
1413
|
}
|
|
1414
|
+
if (this.channelId === "telegram") {
|
|
1415
|
+
return new TelegramChannelPort({
|
|
1416
|
+
transport: this.telegramTransport,
|
|
1417
|
+
sendText: (target, text, kind) => this.queueWechatMessage(this.authorizedUserId, text, toWechatSendContext(kind), target),
|
|
1418
|
+
prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
|
|
1419
|
+
onEmptyVisibleReply: (currentAdapter, rawText) => {
|
|
1420
|
+
appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
|
|
1421
|
+
},
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1405
1424
|
if (this.channelId === "local") {
|
|
1406
1425
|
return new LocalChannelPort({
|
|
1407
1426
|
sendText: (recipientId, text, context) => this.queueWechatMessage(recipientId, text, context),
|
|
@@ -2415,19 +2434,26 @@ export async function runDaemon(options) {
|
|
|
2415
2434
|
const wecomAccount = options.channelId === "wecom"
|
|
2416
2435
|
? await ensureWecomAccount({ log })
|
|
2417
2436
|
: null;
|
|
2437
|
+
const telegramAccount = options.channelId === "telegram"
|
|
2438
|
+
? await ensureTelegramAccount({ log })
|
|
2439
|
+
: null;
|
|
2418
2440
|
const credentials = wecomAccount
|
|
2419
2441
|
? { userId: wecomAccount.operatorUserId }
|
|
2420
|
-
:
|
|
2421
|
-
? { userId:
|
|
2422
|
-
:
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2442
|
+
: telegramAccount
|
|
2443
|
+
? { userId: telegramAccount.operatorUserId }
|
|
2444
|
+
: options.channelId === "local"
|
|
2445
|
+
? { userId: LOCAL_CHANNEL_OPERATOR_ID }
|
|
2446
|
+
: await ensureWechatCredentials({
|
|
2447
|
+
requireUserId: true,
|
|
2448
|
+
validateExisting: true,
|
|
2449
|
+
log,
|
|
2450
|
+
});
|
|
2427
2451
|
if (!credentials.userId) {
|
|
2428
2452
|
throw new Error(options.channelId === "wecom"
|
|
2429
2453
|
? "Saved WeCom credentials are missing operatorUserId."
|
|
2430
|
-
:
|
|
2454
|
+
: options.channelId === "telegram"
|
|
2455
|
+
? "Saved Telegram credentials are missing operatorUserId."
|
|
2456
|
+
: "Saved WeChat credentials are missing userId.");
|
|
2431
2457
|
}
|
|
2432
2458
|
const daemon = new WechatDaemon({
|
|
2433
2459
|
cwd: options.cwd,
|
|
@@ -2437,7 +2463,7 @@ export async function runDaemon(options) {
|
|
|
2437
2463
|
? new WeChatTransport({ log, logError })
|
|
2438
2464
|
: null,
|
|
2439
2465
|
channelId: options.channelId,
|
|
2440
|
-
accountId: wecomAccount?.botId,
|
|
2466
|
+
accountId: wecomAccount?.botId ?? telegramAccount?.botUsername,
|
|
2441
2467
|
wecomTransport: wecomAccount
|
|
2442
2468
|
? new WecomTransport({
|
|
2443
2469
|
account: wecomAccount,
|
|
@@ -2447,6 +2473,15 @@ export async function runDaemon(options) {
|
|
|
2447
2473
|
},
|
|
2448
2474
|
})
|
|
2449
2475
|
: null,
|
|
2476
|
+
telegramTransport: telegramAccount
|
|
2477
|
+
? new TelegramTransport({
|
|
2478
|
+
account: telegramAccount,
|
|
2479
|
+
logger: {
|
|
2480
|
+
log: (message) => log(message),
|
|
2481
|
+
error: (message) => logError(message),
|
|
2482
|
+
},
|
|
2483
|
+
})
|
|
2484
|
+
: null,
|
|
2450
2485
|
localFiles: options.channelId === "local"
|
|
2451
2486
|
? getLocalChannelFiles(options.cwd)
|
|
2452
2487
|
: null,
|
package/dist/i18n/messages-en.js
CHANGED
|
@@ -337,6 +337,7 @@ export const messages = {
|
|
|
337
337
|
"status.questions": "{count} question(s)",
|
|
338
338
|
"operator.wechat": "the configured WeChat owner",
|
|
339
339
|
"operator.wecom": "the paired WeCom operator",
|
|
340
|
+
"operator.telegram": "the paired Telegram operator",
|
|
340
341
|
"operator.local": "the local operator",
|
|
341
342
|
"status.label.instance_id": "instance_id",
|
|
342
343
|
"status.label.adapter": "adapter",
|
package/dist/i18n/messages-zh.js
CHANGED
|
@@ -337,6 +337,7 @@ export const messages = {
|
|
|
337
337
|
"status.questions": "{count} 个问题",
|
|
338
338
|
"operator.wechat": "已配置的微信操作者",
|
|
339
339
|
"operator.wecom": "已配对的企业微信操作者",
|
|
340
|
+
"operator.telegram": "已配对的 Telegram 操作者",
|
|
340
341
|
"operator.local": "本地操作者",
|
|
341
342
|
"status.label.instance_id": "实例 ID",
|
|
342
343
|
"status.label.adapter": "适配器",
|