@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.
Files changed (51) hide show
  1. package/README.md +110 -140
  2. package/bin/wechat-claude.mjs +1 -1
  3. package/bin/wechat-codex.mjs +1 -1
  4. package/bin/wechat-opencode.mjs +1 -1
  5. package/bin/wechat-pi.mjs +1 -1
  6. package/dist/bridge/bridge-adapters.claude.js +448 -11
  7. package/dist/bridge/bridge-adapters.codex.js +262 -11
  8. package/dist/bridge/bridge-adapters.core.js +76 -4
  9. package/dist/bridge/bridge-adapters.js +0 -3
  10. package/dist/bridge/bridge-adapters.opencode.js +497 -49
  11. package/dist/bridge/bridge-adapters.pi.js +44 -8
  12. package/dist/bridge/bridge-adapters.shared.js +13 -100
  13. package/dist/bridge/bridge-controller.js +3 -0
  14. package/dist/bridge/bridge-final-reply.js +17 -2
  15. package/dist/bridge/bridge-process-reaper.js +14 -4
  16. package/dist/bridge/bridge-session-resume.js +151 -0
  17. package/dist/bridge/bridge-state.js +41 -6
  18. package/dist/bridge/bridge-utils.js +63 -229
  19. package/dist/bridge/claude-hooks.js +1 -0
  20. package/dist/bridge/wechat-bridge.js +93 -140
  21. package/dist/bridge/wechat-forwarding.js +58 -0
  22. package/dist/companion/codex-remote-client.js +309 -36
  23. package/dist/companion/codex-visible-client-link.js +122 -0
  24. package/dist/companion/local-companion-link.js +33 -2
  25. package/dist/companion/local-companion-start.js +44 -13
  26. package/dist/companion/local-companion.js +6 -3
  27. package/dist/companion/opencode-tui-bridge-plugin.js +73 -0
  28. package/dist/companion/pi-tui-bridge-extension.js +42 -3
  29. package/dist/daemon/daemon-link.js +25 -3
  30. package/dist/daemon/wechat-daemon.js +168 -56
  31. package/dist/i18n/index.js +0 -3
  32. package/dist/i18n/messages-en.js +2 -3
  33. package/dist/i18n/messages-zh.js +2 -3
  34. package/dist/utils/atomic-file.js +15 -0
  35. package/dist/utils/doctor.js +23 -11
  36. package/dist/wechat/channel-config.js +0 -2
  37. package/dist/wechat/setup.js +5 -11
  38. package/dist/wechat/wechat-transport.js +14 -8
  39. package/package.json +5 -12
  40. package/scripts/ensure-node-pty-permissions.mjs +73 -0
  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-pi.mjs +0 -5
  45. package/bin/wechat-bridge-shell.mjs +0 -5
  46. package/bin/wechat-bridge.mjs +0 -5
  47. package/bin/wechat-claude-start.mjs +0 -5
  48. package/bin/wechat-codex-start.mjs +0 -5
  49. package/bin/wechat-opencode-start.mjs +0 -5
  50. package/bin/wechat-pi-start.mjs +0 -5
  51. 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();
@@ -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
- : "No saved sessions were found for this working directory.";
775
- }
776
- const title = adapter === "codex" ? "Recent Codex threads:" : adapter === "opencode" ? "Recent OpenCode sessions:" : "Recent sessions:";
777
- const resumeTargetLabel = adapter === "codex" ? "threadId" : "sessionId";
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
- `Reply with /resume <number> or /resume <${resumeTargetLabel}>.`,
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(adapter, text) {
811
- if (adapter === "claude" || adapter === "codex" || adapter === "opencode" || adapter === "pi") {
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-(?:bridge-opencode|opencode(?:-start)?)".*$/i,
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
- // Keep ordinary local files auto-sendable, but avoid turning common
1014
- // source/script references in prose into unintended WeChat uploads.
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
- `${pending.source === "shell" ? "Shell" : "CLI"} approval is required.`,
890
+ "CLI approval is required.",
1057
891
  `adapter: ${adapterState.kind}`,
1058
892
  `summary: ${pending.summary}`,
1059
893
  `target: ${pending.commandPreview}`,
@@ -53,6 +53,7 @@ export function buildClaudeHookSettings(command) {
53
53
  return {
54
54
  hooks: {
55
55
  SessionStart: [hook],
56
+ SessionEnd: [hook],
56
57
  UserPromptSubmit: [hook],
57
58
  PermissionRequest: [hook],
58
59
  Notification: [