cli-wechat-bridge 1.1.2 → 1.1.4

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.
Files changed (46) hide show
  1. package/README.md +129 -150
  2. package/bin/_run-entry.mjs +12 -0
  3. package/bin/wechat-claude-start.mjs +7 -2
  4. package/bin/wechat-claude.mjs +1 -1
  5. package/bin/wechat-codex-start.mjs +7 -2
  6. package/bin/wechat-codex.mjs +1 -1
  7. package/bin/wechat-opencode-start.mjs +7 -2
  8. package/bin/wechat-opencode.mjs +1 -1
  9. package/bin/wechat-pi-start.mjs +10 -0
  10. package/bin/wechat-pi.mjs +5 -0
  11. package/dist/bridge/bridge-adapter-common.js +2 -0
  12. package/dist/bridge/bridge-adapters.claude.js +21 -1
  13. package/dist/bridge/bridge-adapters.codex.js +85 -6
  14. package/dist/bridge/bridge-adapters.core.js +112 -6
  15. package/dist/bridge/bridge-adapters.js +5 -3
  16. package/dist/bridge/bridge-adapters.opencode.js +390 -52
  17. package/dist/bridge/bridge-adapters.pi.js +707 -0
  18. package/dist/bridge/bridge-adapters.shared.js +22 -102
  19. package/dist/bridge/bridge-final-reply.js +17 -2
  20. package/dist/bridge/bridge-process-reaper.js +14 -4
  21. package/dist/bridge/bridge-state.js +42 -6
  22. package/dist/bridge/bridge-utils.js +94 -237
  23. package/dist/bridge/wechat-bridge.js +77 -131
  24. package/dist/bridge/wechat-forwarding.js +58 -0
  25. package/dist/companion/codex-remote-client.js +20 -6
  26. package/dist/companion/local-companion-link.js +13 -3
  27. package/dist/companion/local-companion-start.js +64 -16
  28. package/dist/companion/local-companion.js +8 -5
  29. package/dist/companion/pi-tui-bridge-extension.js +256 -0
  30. package/dist/daemon/daemon-link.js +25 -3
  31. package/dist/daemon/wechat-daemon.js +264 -77
  32. package/dist/i18n/index.js +0 -3
  33. package/dist/i18n/messages-en.js +4 -5
  34. package/dist/i18n/messages-zh.js +4 -5
  35. package/dist/utils/atomic-file.js +15 -0
  36. package/dist/utils/doctor.js +27 -11
  37. package/dist/wechat/channel-config.js +0 -2
  38. package/dist/wechat/setup.js +5 -9
  39. package/dist/wechat/wechat-transport.js +14 -8
  40. package/package.json +113 -102
  41. package/bin/wechat-bridge-claude.mjs +0 -5
  42. package/bin/wechat-bridge-codex.mjs +0 -5
  43. package/bin/wechat-bridge-opencode.mjs +0 -5
  44. package/bin/wechat-bridge-shell.mjs +0 -5
  45. package/bin/wechat-bridge.mjs +0 -5
  46. 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
- ".psd1",
92
- ".psm1",
93
- ".py",
94
- ".rb",
95
- ".rs",
96
- ".scala",
86
+ ".vbs",
87
+ ".vbe",
88
+ ".jse",
89
+ ".wsf",
90
+ ".wsh",
91
+ // Unix one-click script launchers and installable packages
97
92
  ".sh",
98
- ".swift",
99
- ".ts",
100
- ".tsx",
101
- ".vb",
102
- ".zsh",
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();
@@ -802,22 +627,19 @@ export function formatMirroredUserInputMessage(adapter, text) {
802
627
  ? "Local Claude input"
803
628
  : adapter === "opencode"
804
629
  ? "Local OpenCode input"
805
- : "Local input";
630
+ : adapter === "pi"
631
+ ? "Local Pi input"
632
+ : "Local input";
806
633
  return `${label}:\n${truncatePreview(text, 500)}`;
807
634
  }
808
- export function formatFinalReplyMessage(adapter, text) {
809
- if (adapter === "claude" || adapter === "codex" || adapter === "opencode") {
810
- return text;
811
- }
812
- // After the early return above, only "shell" remains.
813
- const label = adapter === "codex" ? "Codex" : adapter === "claude" ? "Claude" : adapter === "opencode" ? "OpenCode" : adapter;
814
- return `${label} final reply:\n${text}`;
635
+ export function formatFinalReplyMessage(_adapter, text) {
636
+ return text;
815
637
  }
816
638
  const OPENCODE_WORKING_NOTICE_RE = /^OpenCode is still working on:\s*$/i;
817
639
  const OPENCODE_TRANSIENT_NOTICE_RES = [
818
640
  /^Bridge error: opencode companion is not connected\./i,
819
641
  /^OpenCode companion is not connected(?: for bridge workspace)?:?$/i,
820
- /^Run "wechat-(?:bridge-opencode|opencode(?:-start)?)".*$/i,
642
+ /^Run "wechat-opencode".*$/i,
821
643
  /^OpenCode session switched to \S+ from the local terminal\.$/i,
822
644
  /^Local OpenCode input:\s*$/i,
823
645
  ];
@@ -912,7 +734,7 @@ export function sanitizeWechatFinalReplyText(adapter, text) {
912
734
  const resolved = tail || cleaned;
913
735
  return isOpenCodeReasoningResidue(resolved) ? "" : resolved;
914
736
  }
915
- function extractInlineWechatAttachments(text) {
737
+ export function extractInlineWechatAttachments(text) {
916
738
  const sanitized = text
917
739
  .replace(/\\\n\s*/g, "\\")
918
740
  .replace(/\.\s*\n?\s*([A-Za-z0-9]{2,8})(?=\?)/g, ".$1")
@@ -1000,8 +822,9 @@ function inferInlineWechatAttachmentKind(filePath) {
1000
822
  if (INLINE_VOICE_EXTENSIONS.has(extension)) {
1001
823
  return "voice";
1002
824
  }
1003
- // Keep ordinary local files auto-sendable, but avoid turning common
1004
- // source/script references in prose into unintended WeChat uploads.
825
+ // Ordinary local files stay auto-sendable; only directly executable /
826
+ // installer types are excluded so a path mentioned in prose cannot
827
+ // accidentally ship a runnable binary to a WeChat contact.
1005
828
  if (!extension || INLINE_REFERENCE_ONLY_FILE_EXTENSIONS.has(extension)) {
1006
829
  return null;
1007
830
  }
@@ -1016,7 +839,15 @@ function cleanupVisibleWechatReplyText(text) {
1016
839
  .trim();
1017
840
  }
1018
841
  export function formatTaskFailedMessage(adapter, text) {
1019
- const label = adapter === "codex" ? "Codex" : adapter === "claude" ? "Claude" : adapter === "opencode" ? "OpenCode" : adapter;
842
+ const label = adapter === "codex"
843
+ ? "Codex"
844
+ : adapter === "claude"
845
+ ? "Claude"
846
+ : adapter === "opencode"
847
+ ? "OpenCode"
848
+ : adapter === "pi"
849
+ ? "Pi"
850
+ : adapter;
1020
851
  return `${label} task failed:\n${text}`;
1021
852
  }
1022
853
  export function formatApprovalMessage(pending, adapterState) {
@@ -1035,7 +866,7 @@ export function formatApprovalMessage(pending, adapterState) {
1035
866
  ].join("\n");
1036
867
  }
1037
868
  return [
1038
- `${pending.source === "shell" ? "Shell" : "CLI"} approval is required.`,
869
+ "CLI approval is required.",
1039
870
  `adapter: ${adapterState.kind}`,
1040
871
  `summary: ${pending.summary}`,
1041
872
  `target: ${pending.commandPreview}`,
@@ -1090,22 +921,35 @@ function parseSingleUserInputAnswer(question, rawValue) {
1090
921
  answers: [trimmed],
1091
922
  };
1092
923
  }
1093
- const separatorIndex = trimmed.indexOf("|");
924
+ const customAnswerMode = question.customAnswerMode ?? "note";
925
+ const separatorIndex = customAnswerMode === "note" ? trimmed.indexOf("|") : -1;
1094
926
  const selection = separatorIndex >= 0 ? trimmed.slice(0, separatorIndex).trim() : trimmed;
1095
927
  let note = separatorIndex >= 0 ? trimmed.slice(separatorIndex + 1).trim() : "";
1096
928
  const answers = [];
1097
929
  if (selection) {
1098
- const selectedLabel = resolveUserInputOptionLabel(question, selection);
1099
- if (selectedLabel) {
1100
- answers.push(selectedLabel);
1101
- }
1102
- else if (question.isOther) {
1103
- note = note ? `${selection}; ${note}` : selection;
1104
- }
1105
- else {
1106
- return {
1107
- error: `Question "${question.header}" expects an option number or label.`,
1108
- };
930
+ const selections = question.multiple
931
+ ? selection.split(",").map((value) => value.trim()).filter(Boolean)
932
+ : [selection];
933
+ for (const value of selections) {
934
+ const selectedLabel = resolveUserInputOptionLabel(question, value);
935
+ if (selectedLabel) {
936
+ if (!answers.includes(selectedLabel)) {
937
+ answers.push(selectedLabel);
938
+ }
939
+ }
940
+ else if (question.isOther && customAnswerMode === "value") {
941
+ if (!answers.includes(value)) {
942
+ answers.push(value);
943
+ }
944
+ }
945
+ else if (question.isOther) {
946
+ note = note ? `${value}; ${note}` : value;
947
+ }
948
+ else {
949
+ return {
950
+ error: `Question "${question.header}" expects an option number or label.`,
951
+ };
952
+ }
1109
953
  }
1110
954
  }
1111
955
  if (note) {
@@ -1210,8 +1054,13 @@ export function formatUserInputRequestMessage(pending, adapterState) {
1210
1054
  lines.push(` ${optionIndex + 1}. ${option.label} - ${truncatePreview(option.description, 160)}`);
1211
1055
  });
1212
1056
  }
1057
+ if (question.multiple) {
1058
+ lines.push("multiple selections: allowed (separate values with commas)");
1059
+ }
1213
1060
  if (question.isOther) {
1214
- lines.push("custom note: allowed");
1061
+ lines.push(question.customAnswerMode === "value"
1062
+ ? "custom answer: allowed"
1063
+ : "custom note: allowed");
1215
1064
  }
1216
1065
  });
1217
1066
  lines.push("");
@@ -1221,8 +1070,13 @@ export function formatUserInputRequestMessage(pending, adapterState) {
1221
1070
  return lines.join("\n");
1222
1071
  }
1223
1072
  if (question.options?.length) {
1224
- lines.push("Reply with /answer <option number or exact label>.");
1225
- if (question.isOther) {
1073
+ lines.push(question.multiple
1074
+ ? "Reply with /answer <option numbers or exact labels separated by commas>."
1075
+ : "Reply with /answer <option number or exact label>.");
1076
+ if (question.isOther && question.customAnswerMode === "value") {
1077
+ lines.push("You may enter a custom answer instead of an option.");
1078
+ }
1079
+ else if (question.isOther) {
1226
1080
  lines.push('Add "| your note" to include extra context.');
1227
1081
  }
1228
1082
  }
@@ -1234,7 +1088,10 @@ export function formatUserInputRequestMessage(pending, adapterState) {
1234
1088
  lines.push("Reply with /answer questionId=value; questionId2=value.");
1235
1089
  lines.push("You can use question numbers instead of ids.");
1236
1090
  lines.push("For option questions, value can be the option number or exact label.");
1237
- lines.push('Add "| your note" after an option answer to include extra context.');
1091
+ lines.push("For multi-select questions, separate option values with commas.");
1092
+ if (pending.questions.some((question) => question.customAnswerMode !== "value")) {
1093
+ lines.push('Add "| your note" after an option answer to include extra context.');
1094
+ }
1238
1095
  }
1239
1096
  lines.push("Use /stop to interrupt the active turn instead.");
1240
1097
  return lines.join("\n");
@@ -1243,11 +1100,11 @@ export function formatPendingUserInputReminder(pending) {
1243
1100
  if (pending.questions.length === 1) {
1244
1101
  const question = pending.questions[0];
1245
1102
  if (!question) {
1246
- return "Codex is waiting for user input. Reply with /answer and your response, or use /stop to interrupt.";
1103
+ return "The active CLI is waiting for user input. Reply with /answer and your response, or use /stop to interrupt.";
1247
1104
  }
1248
- return `Codex is waiting for user input for ${question.header}. Reply with /answer and your response, or use /stop to interrupt.`;
1105
+ return `The active CLI is waiting for user input for ${question.header}. Reply with /answer and your response, or use /stop to interrupt.`;
1249
1106
  }
1250
- return `Codex is waiting for answers to ${pending.questions.length} questions. Reply with /answer questionId=value; questionId2=value, or use /stop to interrupt.`;
1107
+ return `The active CLI is waiting for answers to ${pending.questions.length} questions. Reply with /answer questionId=value; questionId2=value, or use /stop to interrupt.`;
1251
1108
  }
1252
1109
  export class OutputBatcher {
1253
1110
  onFlush;