@unlinearity/cli-wechat-bridge 1.1.3 → 1.1.5
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 +110 -140
- package/bin/wechat-claude.mjs +1 -1
- package/bin/wechat-codex.mjs +1 -1
- package/bin/wechat-opencode.mjs +1 -1
- package/bin/wechat-pi.mjs +1 -1
- package/dist/bridge/bridge-adapters.claude.js +448 -11
- package/dist/bridge/bridge-adapters.codex.js +262 -11
- package/dist/bridge/bridge-adapters.core.js +76 -4
- package/dist/bridge/bridge-adapters.js +0 -3
- package/dist/bridge/bridge-adapters.opencode.js +497 -49
- package/dist/bridge/bridge-adapters.pi.js +44 -8
- package/dist/bridge/bridge-adapters.shared.js +13 -100
- package/dist/bridge/bridge-controller.js +3 -0
- package/dist/bridge/bridge-final-reply.js +17 -2
- package/dist/bridge/bridge-process-reaper.js +14 -4
- package/dist/bridge/bridge-session-resume.js +151 -0
- package/dist/bridge/bridge-state.js +41 -6
- package/dist/bridge/bridge-utils.js +63 -229
- package/dist/bridge/claude-hooks.js +1 -0
- package/dist/bridge/wechat-bridge.js +93 -140
- package/dist/bridge/wechat-forwarding.js +58 -0
- package/dist/companion/codex-remote-client.js +309 -36
- package/dist/companion/codex-visible-client-link.js +122 -0
- package/dist/companion/local-companion-link.js +33 -2
- package/dist/companion/local-companion-start.js +44 -13
- package/dist/companion/local-companion.js +6 -3
- package/dist/companion/opencode-tui-bridge-plugin.js +73 -0
- package/dist/companion/pi-tui-bridge-extension.js +42 -3
- package/dist/daemon/daemon-link.js +25 -3
- package/dist/daemon/wechat-daemon.js +168 -56
- package/dist/i18n/index.js +0 -3
- package/dist/i18n/messages-en.js +2 -3
- package/dist/i18n/messages-zh.js +2 -3
- package/dist/utils/atomic-file.js +15 -0
- package/dist/utils/doctor.js +23 -11
- package/dist/wechat/channel-config.js +0 -2
- package/dist/wechat/setup.js +5 -11
- package/dist/wechat/wechat-transport.js +14 -8
- package/package.json +5 -12
- package/scripts/ensure-node-pty-permissions.mjs +73 -0
- package/bin/wechat-bridge-claude.mjs +0 -5
- package/bin/wechat-bridge-codex.mjs +0 -5
- package/bin/wechat-bridge-opencode.mjs +0 -5
- package/bin/wechat-bridge-pi.mjs +0 -5
- package/bin/wechat-bridge-shell.mjs +0 -5
- package/bin/wechat-bridge.mjs +0 -5
- package/bin/wechat-claude-start.mjs +0 -5
- package/bin/wechat-codex-start.mjs +0 -5
- package/bin/wechat-opencode-start.mjs +0 -5
- package/bin/wechat-pi-start.mjs +0 -5
- package/dist/bridge/bridge-adapters.shell.js +0 -363
|
@@ -61,45 +61,40 @@ const INLINE_VOICE_EXTENSIONS = new Set([
|
|
|
61
61
|
".ogg",
|
|
62
62
|
".aac",
|
|
63
63
|
]);
|
|
64
|
+
// Directly executable / installer file types are never auto-uploaded from a
|
|
65
|
+
// path mentioned in a reply: handing these to WeChat contacts by accident is
|
|
66
|
+
// a real hazard. Everything else — source code, documents, archives, data
|
|
67
|
+
// files — stays auto-sendable so common workflows keep working.
|
|
64
68
|
const INLINE_REFERENCE_ONLY_FILE_EXTENSIONS = new Set([
|
|
69
|
+
// Windows binaries and installers
|
|
70
|
+
".exe",
|
|
71
|
+
".dll",
|
|
72
|
+
".sys",
|
|
73
|
+
".msi",
|
|
74
|
+
".msp",
|
|
75
|
+
".scr",
|
|
76
|
+
".com",
|
|
77
|
+
".pif",
|
|
78
|
+
".cpl",
|
|
79
|
+
".ocx",
|
|
80
|
+
".hta",
|
|
81
|
+
".jar",
|
|
82
|
+
// Windows one-click script launchers
|
|
65
83
|
".bat",
|
|
66
|
-
".c",
|
|
67
|
-
".cc",
|
|
68
|
-
".cjs",
|
|
69
84
|
".cmd",
|
|
70
|
-
".cpp",
|
|
71
|
-
".cs",
|
|
72
|
-
".cts",
|
|
73
|
-
".cxx",
|
|
74
|
-
".go",
|
|
75
|
-
".h",
|
|
76
|
-
".hh",
|
|
77
|
-
".hpp",
|
|
78
|
-
".java",
|
|
79
|
-
".js",
|
|
80
|
-
".jsx",
|
|
81
|
-
".kt",
|
|
82
|
-
".kts",
|
|
83
|
-
".lua",
|
|
84
|
-
".m",
|
|
85
|
-
".mjs",
|
|
86
|
-
".mm",
|
|
87
|
-
".mts",
|
|
88
|
-
".php",
|
|
89
|
-
".pl",
|
|
90
85
|
".ps1",
|
|
91
|
-
".
|
|
92
|
-
".
|
|
93
|
-
".
|
|
94
|
-
".
|
|
95
|
-
".
|
|
96
|
-
|
|
86
|
+
".vbs",
|
|
87
|
+
".vbe",
|
|
88
|
+
".jse",
|
|
89
|
+
".wsf",
|
|
90
|
+
".wsh",
|
|
91
|
+
// Unix one-click script launchers and installable packages
|
|
97
92
|
".sh",
|
|
98
|
-
".
|
|
99
|
-
".
|
|
100
|
-
".
|
|
101
|
-
".
|
|
102
|
-
".
|
|
93
|
+
".bash",
|
|
94
|
+
".deb",
|
|
95
|
+
".rpm",
|
|
96
|
+
".apk",
|
|
97
|
+
".dmg",
|
|
103
98
|
]);
|
|
104
99
|
const INLINE_MAAS_URL_RE = /https?:\/\/[^\s]*?\/([A-Za-z]:\\.+?(?:\.\s*[A-Za-z0-9]{2,8})+)(?:\?[^\n]*)?/g;
|
|
105
100
|
const INLINE_WINDOWS_PATH_RE = /(^|[^\w])`?([A-Za-z]:\\(?:[^\\/:*?"<>|\r\n`]+\\)*[^\\/:*?"<>|\r\n`]+?(?:\.\s*[A-Za-z0-9]{2,8})+)`?(?=$|[^\w])/gm;
|
|
@@ -451,176 +446,6 @@ export function isHighRiskShellCommand(command) {
|
|
|
451
446
|
}
|
|
452
447
|
return HIGH_RISK_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
453
448
|
}
|
|
454
|
-
const ALWAYS_INTERACTIVE_SHELL_COMMANDS = new Set([
|
|
455
|
-
"ftp",
|
|
456
|
-
"htop",
|
|
457
|
-
"irb",
|
|
458
|
-
"less",
|
|
459
|
-
"more",
|
|
460
|
-
"mongosh",
|
|
461
|
-
"mysql",
|
|
462
|
-
"nano",
|
|
463
|
-
"nvim",
|
|
464
|
-
"psql",
|
|
465
|
-
"redis-cli",
|
|
466
|
-
"screen",
|
|
467
|
-
"sftp",
|
|
468
|
-
"sqlite3",
|
|
469
|
-
"ssh",
|
|
470
|
-
"telnet",
|
|
471
|
-
"tmux",
|
|
472
|
-
"top",
|
|
473
|
-
"vi",
|
|
474
|
-
"vim",
|
|
475
|
-
"watch",
|
|
476
|
-
]);
|
|
477
|
-
function tokenizeShellCommand(command, maxTokens = 16) {
|
|
478
|
-
const tokens = [];
|
|
479
|
-
let current = "";
|
|
480
|
-
let quote = null;
|
|
481
|
-
for (const char of command.trim()) {
|
|
482
|
-
if (quote) {
|
|
483
|
-
if (char === quote) {
|
|
484
|
-
quote = null;
|
|
485
|
-
}
|
|
486
|
-
else {
|
|
487
|
-
current += char;
|
|
488
|
-
}
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
if (char === '"' || char === "'") {
|
|
492
|
-
quote = char;
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
if (/\s/u.test(char)) {
|
|
496
|
-
if (current) {
|
|
497
|
-
tokens.push(current);
|
|
498
|
-
current = "";
|
|
499
|
-
if (tokens.length >= maxTokens) {
|
|
500
|
-
return tokens;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
current += char;
|
|
506
|
-
}
|
|
507
|
-
if (current) {
|
|
508
|
-
tokens.push(current);
|
|
509
|
-
}
|
|
510
|
-
return tokens;
|
|
511
|
-
}
|
|
512
|
-
function normalizeShellExecutableToken(token) {
|
|
513
|
-
const trimmed = token.trim().replace(/^["']|["']$/g, "");
|
|
514
|
-
if (!trimmed) {
|
|
515
|
-
return "";
|
|
516
|
-
}
|
|
517
|
-
return path.parse(trimmed).name.toLowerCase();
|
|
518
|
-
}
|
|
519
|
-
function findCommandFlagIndex(args, supportedFlags) {
|
|
520
|
-
return args.findIndex((arg) => supportedFlags.includes(arg.toLowerCase()));
|
|
521
|
-
}
|
|
522
|
-
function hasScriptLikeArg(args) {
|
|
523
|
-
return args.some((arg) => Boolean(arg) && !arg.startsWith("-") && !arg.startsWith("/"));
|
|
524
|
-
}
|
|
525
|
-
function hasAnyCommandFlag(args, supportedFlags) {
|
|
526
|
-
return findCommandFlagIndex(args, supportedFlags) >= 0;
|
|
527
|
-
}
|
|
528
|
-
function buildInteractiveShellCommandMessage(executable, suggestion) {
|
|
529
|
-
return `Interactive command "${executable}" is not supported in shell mode yet. This shell bridge currently only supports non-interactive commands and scripts. ${suggestion}`;
|
|
530
|
-
}
|
|
531
|
-
export function getInteractiveShellCommandRejectionMessage(command) {
|
|
532
|
-
const tokens = tokenizeShellCommand(command);
|
|
533
|
-
if (!tokens.length) {
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
const firstToken = tokens[0];
|
|
537
|
-
if (!firstToken) {
|
|
538
|
-
return null;
|
|
539
|
-
}
|
|
540
|
-
const executable = normalizeShellExecutableToken(firstToken);
|
|
541
|
-
const args = tokens.slice(1);
|
|
542
|
-
const lowerArgs = args.map((arg) => arg.toLowerCase());
|
|
543
|
-
if (!executable) {
|
|
544
|
-
return null;
|
|
545
|
-
}
|
|
546
|
-
if (ALWAYS_INTERACTIVE_SHELL_COMMANDS.has(executable)) {
|
|
547
|
-
return buildInteractiveShellCommandMessage(executable, "Run a non-interactive command or script instead.");
|
|
548
|
-
}
|
|
549
|
-
switch (executable) {
|
|
550
|
-
case "python":
|
|
551
|
-
case "python3":
|
|
552
|
-
case "py": {
|
|
553
|
-
if (!args.length) {
|
|
554
|
-
return buildInteractiveShellCommandMessage(executable, 'Try "python script.py" or "python -c \\"...\\"" instead.');
|
|
555
|
-
}
|
|
556
|
-
if (lowerArgs.includes("-i") || lowerArgs.includes("--interactive")) {
|
|
557
|
-
return buildInteractiveShellCommandMessage(executable, 'Try "python script.py" or "python -c \\"...\\"" instead.');
|
|
558
|
-
}
|
|
559
|
-
if (hasAnyCommandFlag(lowerArgs, ["-c", "-h", "--help", "-v", "--version"])) {
|
|
560
|
-
return null;
|
|
561
|
-
}
|
|
562
|
-
const moduleFlagIndex = findCommandFlagIndex(lowerArgs, ["-m"]);
|
|
563
|
-
if (moduleFlagIndex >= 0) {
|
|
564
|
-
return moduleFlagIndex < args.length - 1
|
|
565
|
-
? null
|
|
566
|
-
: buildInteractiveShellCommandMessage(executable, 'Try "python script.py" or "python -m module_name" instead.');
|
|
567
|
-
}
|
|
568
|
-
return hasScriptLikeArg(args)
|
|
569
|
-
? null
|
|
570
|
-
: buildInteractiveShellCommandMessage(executable, 'Try "python script.py" or "python -c \\"...\\"" instead.');
|
|
571
|
-
}
|
|
572
|
-
case "node":
|
|
573
|
-
if (!args.length || lowerArgs.includes("-i") || lowerArgs.includes("--interactive")) {
|
|
574
|
-
return buildInteractiveShellCommandMessage(executable, 'Try "node script.js" or "node -e \\"...\\"" instead.');
|
|
575
|
-
}
|
|
576
|
-
if (hasAnyCommandFlag(lowerArgs, ["-e", "--eval", "-p", "--print", "-h", "--help", "-v", "--version"])) {
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
return hasScriptLikeArg(args)
|
|
580
|
-
? null
|
|
581
|
-
: buildInteractiveShellCommandMessage(executable, 'Try "node script.js" or "node -e \\"...\\"" instead.');
|
|
582
|
-
case "cmd":
|
|
583
|
-
if (lowerArgs.includes("/?")) {
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
if (lowerArgs.includes("/k") || !lowerArgs.includes("/c")) {
|
|
587
|
-
return buildInteractiveShellCommandMessage(executable, 'Try "cmd /c <command>" or run the command directly instead.');
|
|
588
|
-
}
|
|
589
|
-
return null;
|
|
590
|
-
case "powershell":
|
|
591
|
-
case "pwsh":
|
|
592
|
-
if (!args.length ||
|
|
593
|
-
lowerArgs.includes("-noexit") ||
|
|
594
|
-
lowerArgs.includes("-nologo") && args.length === 1) {
|
|
595
|
-
return buildInteractiveShellCommandMessage(executable, `Try "${executable} -Command \\"...\\"" or "${executable} -File script.ps1" instead.`);
|
|
596
|
-
}
|
|
597
|
-
if (hasAnyCommandFlag(lowerArgs, ["-c", "-command", "-enc", "-encodedcommand", "-f", "-file", "-h", "-help", "-v", "-version", "-?"])) {
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
return hasScriptLikeArg(args)
|
|
601
|
-
? null
|
|
602
|
-
: buildInteractiveShellCommandMessage(executable, `Try "${executable} -Command \\"...\\"" or "${executable} -File script.ps1" instead.`);
|
|
603
|
-
case "bash":
|
|
604
|
-
case "dash":
|
|
605
|
-
case "ksh":
|
|
606
|
-
case "sh":
|
|
607
|
-
case "zsh":
|
|
608
|
-
if (!args.length || lowerArgs.includes("-i")) {
|
|
609
|
-
return buildInteractiveShellCommandMessage(executable, `Try "${executable} -c '...'" or "${executable} script.sh" instead.`);
|
|
610
|
-
}
|
|
611
|
-
if (findCommandFlagIndex(lowerArgs, ["-c", "-lc"]) >= 0) {
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
if (hasAnyCommandFlag(lowerArgs, ["-h", "--help", "--version"])) {
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
return hasScriptLikeArg(args)
|
|
618
|
-
? null
|
|
619
|
-
: buildInteractiveShellCommandMessage(executable, `Try "${executable} -c '...'" or "${executable} script.sh" instead.`);
|
|
620
|
-
default:
|
|
621
|
-
return null;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
449
|
export function detectCliApproval(text) {
|
|
625
450
|
const normalized = normalizeOutput(text);
|
|
626
451
|
const compact = normalized.replace(/\s+/g, " ").trim();
|
|
@@ -743,6 +568,20 @@ export function formatSessionSwitchMessage(params) {
|
|
|
743
568
|
return `OpenCode session switched to ${shortSessionId}.`;
|
|
744
569
|
}
|
|
745
570
|
}
|
|
571
|
+
if (params.adapter === "pi") {
|
|
572
|
+
switch (params.reason) {
|
|
573
|
+
case "local_follow":
|
|
574
|
+
case "local_session_fallback":
|
|
575
|
+
case "local_turn":
|
|
576
|
+
return `Pi session switched to ${shortSessionId} from the local terminal.`;
|
|
577
|
+
case "wechat_resume":
|
|
578
|
+
return `Pi session switched to ${shortSessionId} from WeChat.`;
|
|
579
|
+
case "startup_restore":
|
|
580
|
+
return `Pi restored shared session ${shortSessionId} on startup.`;
|
|
581
|
+
default:
|
|
582
|
+
return `Pi session switched to ${shortSessionId}.`;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
746
585
|
switch (params.reason) {
|
|
747
586
|
case "local_follow":
|
|
748
587
|
case "local_session_fallback":
|
|
@@ -771,17 +610,24 @@ export function formatResumeSessionList(params) {
|
|
|
771
610
|
? "No saved Codex threads were found for this working directory."
|
|
772
611
|
: adapter === "opencode"
|
|
773
612
|
? "No saved OpenCode sessions were found for this working directory."
|
|
774
|
-
:
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
613
|
+
: adapter === "pi"
|
|
614
|
+
? "No saved Pi sessions were found for this working directory."
|
|
615
|
+
: "No saved Claude sessions were found for this working directory.";
|
|
616
|
+
}
|
|
617
|
+
const title = adapter === "codex"
|
|
618
|
+
? "Recent Codex threads:"
|
|
619
|
+
: adapter === "opencode"
|
|
620
|
+
? "Recent OpenCode sessions:"
|
|
621
|
+
: adapter === "pi"
|
|
622
|
+
? "Recent Pi sessions:"
|
|
623
|
+
: "Recent Claude sessions:";
|
|
778
624
|
return [
|
|
779
625
|
title,
|
|
780
626
|
...candidates.map((candidate, index) => {
|
|
781
627
|
const marker = currentSessionId && candidate.sessionId === currentSessionId ? " [current]" : "";
|
|
782
628
|
return `${index + 1}. ${candidate.title} (${candidate.lastUpdatedAt}, ${candidate.sessionId.slice(0, 12)})${marker}`;
|
|
783
629
|
}),
|
|
784
|
-
|
|
630
|
+
"Reply with /resume <number> or /resume <unique-id-prefix>.",
|
|
785
631
|
].join("\n");
|
|
786
632
|
}
|
|
787
633
|
export function formatResumeThreadList(candidates, currentThreadId) {
|
|
@@ -807,27 +653,14 @@ export function formatMirroredUserInputMessage(adapter, text) {
|
|
|
807
653
|
: "Local input";
|
|
808
654
|
return `${label}:\n${truncatePreview(text, 500)}`;
|
|
809
655
|
}
|
|
810
|
-
export function formatFinalReplyMessage(
|
|
811
|
-
|
|
812
|
-
return text;
|
|
813
|
-
}
|
|
814
|
-
// After the early return above, only "shell" remains.
|
|
815
|
-
const label = adapter === "codex"
|
|
816
|
-
? "Codex"
|
|
817
|
-
: adapter === "claude"
|
|
818
|
-
? "Claude"
|
|
819
|
-
: adapter === "opencode"
|
|
820
|
-
? "OpenCode"
|
|
821
|
-
: adapter === "pi"
|
|
822
|
-
? "Pi"
|
|
823
|
-
: adapter;
|
|
824
|
-
return `${label} final reply:\n${text}`;
|
|
656
|
+
export function formatFinalReplyMessage(_adapter, text) {
|
|
657
|
+
return text;
|
|
825
658
|
}
|
|
826
659
|
const OPENCODE_WORKING_NOTICE_RE = /^OpenCode is still working on:\s*$/i;
|
|
827
660
|
const OPENCODE_TRANSIENT_NOTICE_RES = [
|
|
828
661
|
/^Bridge error: opencode companion is not connected\./i,
|
|
829
662
|
/^OpenCode companion is not connected(?: for bridge workspace)?:?$/i,
|
|
830
|
-
/^Run "wechat-
|
|
663
|
+
/^Run "wechat-opencode".*$/i,
|
|
831
664
|
/^OpenCode session switched to \S+ from the local terminal\.$/i,
|
|
832
665
|
/^Local OpenCode input:\s*$/i,
|
|
833
666
|
];
|
|
@@ -922,7 +755,7 @@ export function sanitizeWechatFinalReplyText(adapter, text) {
|
|
|
922
755
|
const resolved = tail || cleaned;
|
|
923
756
|
return isOpenCodeReasoningResidue(resolved) ? "" : resolved;
|
|
924
757
|
}
|
|
925
|
-
function extractInlineWechatAttachments(text) {
|
|
758
|
+
export function extractInlineWechatAttachments(text) {
|
|
926
759
|
const sanitized = text
|
|
927
760
|
.replace(/\\\n\s*/g, "\\")
|
|
928
761
|
.replace(/\.\s*\n?\s*([A-Za-z0-9]{2,8})(?=\?)/g, ".$1")
|
|
@@ -1010,8 +843,9 @@ function inferInlineWechatAttachmentKind(filePath) {
|
|
|
1010
843
|
if (INLINE_VOICE_EXTENSIONS.has(extension)) {
|
|
1011
844
|
return "voice";
|
|
1012
845
|
}
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
846
|
+
// Ordinary local files stay auto-sendable; only directly executable /
|
|
847
|
+
// installer types are excluded so a path mentioned in prose cannot
|
|
848
|
+
// accidentally ship a runnable binary to a WeChat contact.
|
|
1015
849
|
if (!extension || INLINE_REFERENCE_ONLY_FILE_EXTENSIONS.has(extension)) {
|
|
1016
850
|
return null;
|
|
1017
851
|
}
|
|
@@ -1053,7 +887,7 @@ export function formatApprovalMessage(pending, adapterState) {
|
|
|
1053
887
|
].join("\n");
|
|
1054
888
|
}
|
|
1055
889
|
return [
|
|
1056
|
-
|
|
890
|
+
"CLI approval is required.",
|
|
1057
891
|
`adapter: ${adapterState.kind}`,
|
|
1058
892
|
`summary: ${pending.summary}`,
|
|
1059
893
|
`target: ${pending.commandPreview}`,
|