@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.
Files changed (54) hide show
  1. package/README.md +146 -73
  2. package/README_EN.md +544 -0
  3. package/bin/telegram-check-update.mjs +10 -0
  4. package/bin/telegram-claude.mjs +10 -0
  5. package/bin/telegram-codex.mjs +10 -0
  6. package/bin/telegram-daemon.mjs +10 -0
  7. package/bin/telegram-opencode.mjs +10 -0
  8. package/bin/telegram-pi.mjs +10 -0
  9. package/bin/telegram-setup.mjs +10 -0
  10. package/dist/bridge/adapter-control.js +19 -12
  11. package/dist/bridge/bridge-adapters.claude.js +219 -115
  12. package/dist/bridge/bridge-adapters.codex.js +532 -99
  13. package/dist/bridge/bridge-adapters.core.js +84 -7
  14. package/dist/bridge/bridge-adapters.opencode.js +202 -55
  15. package/dist/bridge/bridge-adapters.pi.js +23 -18
  16. package/dist/bridge/bridge-adapters.shared.js +9 -9
  17. package/dist/bridge/bridge-session-resume.js +13 -6
  18. package/dist/bridge/bridge-state.js +3 -1
  19. package/dist/bridge/bridge-utils.js +94 -144
  20. package/dist/bridge/claude-hooks.js +60 -3
  21. package/dist/bridge/claude-native-controls.js +19 -12
  22. package/dist/bridge/language-control.js +33 -0
  23. package/dist/bridge/native-terminal-control.js +26 -10
  24. package/dist/bridge/opencode-v2-compat.js +11 -3
  25. package/dist/bridge/wechat-bridge.js +154 -72
  26. package/dist/channels/channel-driver-factory.js +56 -0
  27. package/dist/channels/local/local-channel-port.js +40 -0
  28. package/dist/channels/local/local-driver.js +217 -0
  29. package/dist/channels/telegram/setup-cli.js +12 -0
  30. package/dist/channels/telegram/setup.js +163 -0
  31. package/dist/channels/telegram/telegram-channel-port.js +47 -0
  32. package/dist/channels/telegram/telegram-config.js +66 -0
  33. package/dist/channels/telegram/telegram-driver.js +106 -0
  34. package/dist/channels/telegram/telegram-message.js +41 -0
  35. package/dist/channels/telegram/telegram-transport.js +378 -0
  36. package/dist/channels/wechat/wechat-driver.js +11 -2
  37. package/dist/channels/wecom/wecom-driver.js +8 -1
  38. package/dist/companion/local-companion-start.js +180 -61
  39. package/dist/companion/local-companion.js +27 -7
  40. package/dist/companion/pi-tui-bridge-extension.js +10 -4
  41. package/dist/core/bridge-control.js +3 -0
  42. package/dist/core/bridge-defer.js +3 -2
  43. package/dist/core/bridge-event-forwarder.js +3 -0
  44. package/dist/core/channel-types.js +13 -1
  45. package/dist/daemon/wechat-daemon.js +239 -126
  46. package/dist/i18n/index.js +25 -11
  47. package/dist/i18n/messages-en.js +245 -1
  48. package/dist/i18n/messages-zh.js +245 -1
  49. package/dist/runtime/legacy-adapter-runtime.js +7 -4
  50. package/dist/wechat/channel-config.js +3 -2
  51. package/package.json +14 -4
  52. package/skills/codex-bridge/SKILL.md +35 -0
  53. package/skills/codex-bridge/agents/openai.yaml +4 -0
  54. package/skills/codex-bridge/scripts/install.mjs +28 -0
@@ -4,6 +4,7 @@
4
4
  // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
5
  // must be released under AGPL-3.0-or-later with full source code; see
6
6
  // LICENSE.txt. Network services built on it must offer source to users.
7
+ import { t } from "../i18n/index.js";
7
8
  import fs from "node:fs";
8
9
  import os from "node:os";
9
10
  import path from "node:path";
@@ -424,90 +425,43 @@ export function formatStatusReport(bridgeState, adapterState) {
424
425
  const lastSessionSwitchReason = adapterState.lastSessionSwitchReason ?? adapterState.lastThreadSwitchReason;
425
426
  const formatEpochMs = (value) => typeof value === "number" && Number.isFinite(value)
426
427
  ? new Date(value).toISOString()
427
- : "(none)";
428
+ : t("status.none");
428
429
  return [
429
- `instance_id: ${bridgeState.instanceId}`,
430
- `adapter: ${bridgeState.adapter}`,
431
- `command: ${bridgeState.command}`,
432
- `cwd: ${bridgeState.cwd}`,
433
- `profile: ${bridgeState.profile ?? "(none)"}`,
434
- `bridge_started_at: ${formatEpochMs(bridgeState.bridgeStartedAtMs)}`,
435
- `authorized_user: ${bridgeState.authorizedUserId}`,
436
- `ignored_backlog_count: ${bridgeState.ignoredBacklogCount}`,
437
- `persisted_shared_session_id: ${persistedSharedSessionId ?? "(none)"}`,
438
- `worker_status: ${adapterState.status}`,
439
- `worker_pid: ${adapterState.pid ?? "(unknown)"}`,
440
- `shared_session_id: ${sharedSessionId ?? "(none)"}`,
441
- `last_session_switch_at: ${lastSessionSwitchAt ?? "(none)"}`,
442
- `last_session_switch_source: ${lastSessionSwitchSource ?? "(none)"}`,
443
- `last_session_switch_reason: ${lastSessionSwitchReason ?? "(none)"}`,
444
- `active_turn_id: ${adapterState.activeTurnId ?? "(none)"}`,
445
- `active_turn_origin: ${adapterState.activeTurnOrigin ?? "(none)"}`,
446
- `pending_approval_origin: ${adapterState.pendingApprovalOrigin ?? "(none)"}`,
447
- `pending_user_input_origin: ${adapterState.pendingUserInputOrigin ?? "(none)"}`,
448
- `last_activity_at: ${bridgeState.lastActivityAt ?? "(none)"}`,
449
- `last_input_at: ${adapterState.lastInputAt ?? "(none)"}`,
450
- `last_output_at: ${adapterState.lastOutputAt ?? "(none)"}`,
451
- `pending_confirmation: ${pending ? `${pending.source}:${pending.code}` : "(none)"}`,
452
- `pending_user_input: ${pendingUserInput ? `${pendingUserInput.questions.length} question(s)` : "(none)"}`,
430
+ `${t("status.label.instance_id")}: ${bridgeState.instanceId}`,
431
+ `${t("status.label.adapter")}: ${bridgeState.adapter}`,
432
+ `${t("status.label.command")}: ${bridgeState.command}`,
433
+ `${t("status.label.cwd")}: ${bridgeState.cwd}`,
434
+ `${t("status.label.profile")}: ${bridgeState.profile ?? t("status.none")}`,
435
+ `${t("status.label.bridge_started_at")}: ${formatEpochMs(bridgeState.bridgeStartedAtMs)}`,
436
+ `${t("status.label.authorized_user")}: ${bridgeState.authorizedUserId}`,
437
+ `${t("status.label.ignored_backlog_count")}: ${bridgeState.ignoredBacklogCount}`,
438
+ `${t("status.label.persisted_shared_session_id")}: ${persistedSharedSessionId ?? t("status.none")}`,
439
+ `${t("status.label.worker_status")}: ${adapterState.status}`,
440
+ `${t("status.label.worker_pid")}: ${adapterState.pid ?? t("status.unknown")}`,
441
+ `${t("status.label.shared_session_id")}: ${sharedSessionId ?? t("status.none")}`,
442
+ `${t("status.label.last_session_switch_at")}: ${lastSessionSwitchAt ?? t("status.none")}`,
443
+ `${t("status.label.last_session_switch_source")}: ${lastSessionSwitchSource ?? t("status.none")}`,
444
+ `${t("status.label.last_session_switch_reason")}: ${lastSessionSwitchReason ?? t("status.none")}`,
445
+ `${t("status.label.active_turn_id")}: ${adapterState.activeTurnId ?? t("status.none")}`,
446
+ `${t("status.label.active_turn_origin")}: ${adapterState.activeTurnOrigin ?? t("status.none")}`,
447
+ `${t("status.label.pending_approval_origin")}: ${adapterState.pendingApprovalOrigin ?? t("status.none")}`,
448
+ `${t("status.label.pending_user_input_origin")}: ${adapterState.pendingUserInputOrigin ?? t("status.none")}`,
449
+ `${t("status.label.last_activity_at")}: ${bridgeState.lastActivityAt ?? t("status.none")}`,
450
+ `${t("status.label.last_input_at")}: ${adapterState.lastInputAt ?? t("status.none")}`,
451
+ `${t("status.label.last_output_at")}: ${adapterState.lastOutputAt ?? t("status.none")}`,
452
+ `${t("status.label.pending_confirmation")}: ${pending ? `${pending.source}:${pending.code}` : t("status.none")}`,
453
+ `${t("status.label.pending_user_input")}: ${pendingUserInput ? t("status.questions", { count: pendingUserInput.questions.length }) : t("status.none")}`,
453
454
  ].join("\n");
454
455
  }
455
456
  export function formatSessionSwitchMessage(params) {
456
457
  const shortSessionId = params.sessionId.slice(0, 12);
457
- if (params.adapter === "claude") {
458
- switch (params.reason) {
459
- case "local_follow":
460
- case "local_session_fallback":
461
- case "local_turn":
462
- return `Claude session switched to ${shortSessionId} from the local terminal.`;
463
- case "wechat_resume":
464
- return `Claude session switched to ${shortSessionId} from WeChat.`;
465
- case "startup_restore":
466
- return `Claude restored shared session ${shortSessionId} on startup.`;
467
- default:
468
- return `Claude session switched to ${shortSessionId}.`;
469
- }
470
- }
471
- if (params.adapter === "opencode") {
472
- switch (params.reason) {
473
- case "local_follow":
474
- case "local_session_fallback":
475
- case "local_turn":
476
- return `OpenCode session switched to ${shortSessionId} from the local terminal.`;
477
- case "wechat_resume":
478
- return `OpenCode session switched to ${shortSessionId} from WeChat.`;
479
- case "startup_restore":
480
- return `OpenCode restored shared session ${shortSessionId} on startup.`;
481
- default:
482
- return `OpenCode session switched to ${shortSessionId}.`;
483
- }
484
- }
485
- if (params.adapter === "pi") {
486
- switch (params.reason) {
487
- case "local_follow":
488
- case "local_session_fallback":
489
- case "local_turn":
490
- return `Pi session switched to ${shortSessionId} from the local terminal.`;
491
- case "wechat_resume":
492
- return `Pi session switched to ${shortSessionId} from WeChat.`;
493
- case "startup_restore":
494
- return `Pi restored shared session ${shortSessionId} on startup.`;
495
- default:
496
- return `Pi session switched to ${shortSessionId}.`;
497
- }
498
- }
499
- switch (params.reason) {
500
- case "local_follow":
501
- case "local_session_fallback":
502
- case "local_turn":
503
- return `Codex thread switched to ${shortSessionId} from the local terminal.`;
504
- case "wechat_resume":
505
- return `Codex thread switched to ${shortSessionId} from WeChat.`;
506
- case "startup_restore":
507
- return `Codex restored shared thread ${shortSessionId} on startup.`;
508
- default:
509
- return `Codex thread switched to ${shortSessionId}.`;
510
- }
458
+ const adapter = params.adapter === "claude" ? "Claude" : params.adapter === "opencode" ? "OpenCode" : params.adapter === "pi" ? "Pi" : "Codex";
459
+ const noun = t(params.adapter === "codex" ? "session.thread" : "session.noun");
460
+ const key = ["local_follow", "local_session_fallback", "local_turn"].includes(params.reason)
461
+ ? "session.switch.local"
462
+ : params.reason === "wechat_resume" ? "session.switch.remote"
463
+ : params.reason === "startup_restore" ? "session.switch.restore" : "session.switch.generic";
464
+ return t(key, { adapter, noun, id: shortSessionId });
511
465
  }
512
466
  export function formatThreadSwitchMessage(params) {
513
467
  return formatSessionSwitchMessage({
@@ -519,29 +473,18 @@ export function formatThreadSwitchMessage(params) {
519
473
  }
520
474
  export function formatResumeSessionList(params) {
521
475
  const { adapter, candidates, currentSessionId } = params;
522
- if (candidates.length === 0) {
523
- return adapter === "codex"
524
- ? "No saved Codex threads were found for this working directory."
525
- : adapter === "opencode"
526
- ? "No saved OpenCode sessions were found for this working directory."
527
- : adapter === "pi"
528
- ? "No saved Pi sessions were found for this working directory."
529
- : "No saved Claude sessions were found for this working directory.";
530
- }
531
- const title = adapter === "codex"
532
- ? "Recent Codex threads:"
533
- : adapter === "opencode"
534
- ? "Recent OpenCode sessions:"
535
- : adapter === "pi"
536
- ? "Recent Pi sessions:"
537
- : "Recent Claude sessions:";
476
+ const name = adapter === "codex" ? "Codex" : adapter === "opencode" ? "OpenCode" : adapter === "pi" ? "Pi" : "Claude";
477
+ const noun = t(adapter === "codex" ? "session.threads" : "session.sessions");
478
+ if (candidates.length === 0)
479
+ return t("session.list.empty", { adapter: name, noun });
480
+ const title = t("session.list.header", { adapter: name, noun });
538
481
  return [
539
482
  title,
540
483
  ...candidates.map((candidate, index) => {
541
- const marker = currentSessionId && candidate.sessionId === currentSessionId ? " [current]" : "";
484
+ const marker = currentSessionId && candidate.sessionId === currentSessionId ? t("session.list.current") : "";
542
485
  return `${index + 1}. ${candidate.title} (${candidate.lastUpdatedAt}, ${candidate.sessionId.slice(0, 12)})${marker}`;
543
486
  }),
544
- "Reply with /resume <number> or /resume <unique-id-prefix>.",
487
+ t("session.list.instructions"),
545
488
  ].join("\n");
546
489
  }
547
490
  export function formatResumeThreadList(candidates, currentThreadId) {
@@ -557,13 +500,13 @@ export function formatResumeThreadList(candidates, currentThreadId) {
557
500
  }
558
501
  export function formatMirroredUserInputMessage(adapter, text) {
559
502
  const label = adapter === "codex"
560
- ? "Local Codex input"
503
+ ? t("session.localInput", { adapter: "Codex" })
561
504
  : adapter === "claude"
562
- ? "Local Claude input"
505
+ ? t("session.localInput", { adapter: "Claude" })
563
506
  : adapter === "opencode"
564
- ? "Local OpenCode input"
507
+ ? t("session.localInput", { adapter: "OpenCode" })
565
508
  : adapter === "pi"
566
- ? "Local Pi input"
509
+ ? t("session.localInput", { adapter: "Pi" })
567
510
  : "Local input";
568
511
  return `${label}:\n${truncatePreview(text, 500)}`;
569
512
  }
@@ -783,29 +726,34 @@ export function formatTaskFailedMessage(adapter, text) {
783
726
  : adapter === "pi"
784
727
  ? "Pi"
785
728
  : adapter;
786
- return `${label} task failed:\n${text}`;
729
+ return t("session.taskFailed", { adapter: label, text });
787
730
  }
788
731
  export function formatApprovalMessage(pending, adapterState) {
789
732
  const isClaude = adapterState.kind === "claude";
790
733
  if (isClaude) {
734
+ const detailLabel = !pending.detailLabel || pending.detailLabel === "details"
735
+ ? t("approval.details")
736
+ : ["plan", "command", "path", "pattern", "url"].includes(pending.detailLabel)
737
+ ? t(`approval.label.${pending.detailLabel}`)
738
+ : pending.detailLabel;
791
739
  return [
792
- "Claude permission request.",
793
- ...(pending.toolName ? [`tool: ${pending.toolName}`] : []),
740
+ t("approval.claude.header"),
741
+ ...(pending.toolName ? [`${t("approval.tool")}: ${pending.toolName}`] : []),
794
742
  ...(pending.detailPreview
795
- ? [`${pending.detailLabel ?? "details"}: ${pending.detailPreview}`]
743
+ ? [`${detailLabel}: ${pending.detailPreview}`]
796
744
  : pending.commandPreview
797
- ? [`details: ${pending.commandPreview}`]
745
+ ? [`${t("approval.details")}: ${pending.commandPreview}`]
798
746
  : []),
799
- "Reply with /confirm, confirm, or yes to continue.",
800
- "Reply with /deny, deny, or no to reject.",
747
+ t("approval.confirmInstructions"),
748
+ t("approval.denyInstructions"),
801
749
  ].join("\n");
802
750
  }
803
751
  return [
804
- "CLI approval is required.",
805
- `adapter: ${adapterState.kind}`,
806
- `summary: ${pending.summary}`,
807
- `target: ${pending.commandPreview}`,
808
- "Reply with /confirm or /yes to continue, /deny or /no to reject.",
752
+ t("approval.cli.header"),
753
+ `${t("approval.adapter")}: ${adapterState.kind}`,
754
+ `${t("approval.summary")}: ${pending.summary}`,
755
+ `${t("approval.target")}: ${pending.commandPreview}`,
756
+ t("approval.instructions"),
809
757
  ].join("\n");
810
758
  }
811
759
  export function formatPendingApprovalReminder(pending, adapterState) {
@@ -813,9 +761,9 @@ export function formatPendingApprovalReminder(pending, adapterState) {
813
761
  const target = pending.toolName
814
762
  ? `${pending.toolName}${pending.detailPreview ? ` (${pending.detailPreview})` : ""}`
815
763
  : pending.commandPreview;
816
- return `Approval is pending for ${truncatePreview(target, 140)}. Reply with /confirm, confirm, or yes to continue, or /deny, deny, or no to reject.`;
764
+ return t("approval.pending.claude", { target: truncatePreview(target, 140) });
817
765
  }
818
- return `Approval is pending for ${pending.commandPreview}. Reply with /confirm or /deny.`;
766
+ return t("approval.pending", { target: pending.commandPreview });
819
767
  }
820
768
  function formatUserInputQuestionLabel(question, index) {
821
769
  return `${index + 1}. ${question.header} [id: ${question.id}]`;
@@ -848,7 +796,7 @@ function parseSingleUserInputAnswer(question, rawValue) {
848
796
  const trimmed = normalizeOutput(rawValue).trim();
849
797
  if (!trimmed) {
850
798
  return {
851
- error: `Question "${question.header}" requires an answer.`,
799
+ error: t("input.answer.required", { question: question.header }),
852
800
  };
853
801
  }
854
802
  if (!question.options?.length) {
@@ -882,7 +830,7 @@ function parseSingleUserInputAnswer(question, rawValue) {
882
830
  }
883
831
  else {
884
832
  return {
885
- error: `Question "${question.header}" expects an option number or label.`,
833
+ error: t("input.answer.optionRequired", { question: question.header }),
886
834
  };
887
835
  }
888
836
  }
@@ -892,7 +840,7 @@ function parseSingleUserInputAnswer(question, rawValue) {
892
840
  }
893
841
  if (answers.length === 0) {
894
842
  return {
895
- error: `Question "${question.header}" requires an answer.`,
843
+ error: t("input.answer.required", { question: question.header }),
896
844
  };
897
845
  }
898
846
  return {
@@ -903,7 +851,7 @@ export function parsePendingUserInputAnswerCommand(raw, pending) {
903
851
  const input = normalizeOutput(raw).trim();
904
852
  if (!input) {
905
853
  return {
906
- error: "Reply with /answer followed by your response.",
854
+ error: t("input.answer.empty"),
907
855
  };
908
856
  }
909
857
  const answers = {};
@@ -911,7 +859,7 @@ export function parsePendingUserInputAnswerCommand(raw, pending) {
911
859
  const question = pending.questions[0];
912
860
  if (!question) {
913
861
  return {
914
- error: "No pending user input question was found.",
862
+ error: t("input.answer.missingQuestion"),
915
863
  };
916
864
  }
917
865
  const parsed = parseSingleUserInputAnswer(question, input);
@@ -927,7 +875,7 @@ export function parsePendingUserInputAnswerCommand(raw, pending) {
927
875
  .filter(Boolean);
928
876
  if (segments.length === 0) {
929
877
  return {
930
- error: "Reply with /answer questionId=value; questionId2=value.",
878
+ error: t("input.answer.ordered"),
931
879
  };
932
880
  }
933
881
  for (const segment of segments) {
@@ -942,12 +890,12 @@ export function parsePendingUserInputAnswerCommand(raw, pending) {
942
890
  const question = resolveUserInputQuestion(pending, reference);
943
891
  if (!question) {
944
892
  return {
945
- error: `Unknown question reference: ${reference}`,
893
+ error: t("input.answer.unknownQuestion", { reference }),
946
894
  };
947
895
  }
948
896
  if (answers[question.id]) {
949
897
  return {
950
- error: `Question "${question.id}" was answered more than once.`,
898
+ error: t("input.answer.duplicate", { question: question.id }),
951
899
  };
952
900
  }
953
901
  const parsed = parseSingleUserInputAnswer(question, rawValue);
@@ -959,7 +907,7 @@ export function parsePendingUserInputAnswerCommand(raw, pending) {
959
907
  const missing = pending.questions.filter((question) => !answers[question.id]);
960
908
  if (missing.length > 0) {
961
909
  return {
962
- error: `Missing answers for: ${missing.map((question) => question.id).join(", ")}.`,
910
+ error: t("input.answer.missing", { questions: missing.map((question) => question.id).join(", ") }),
963
911
  };
964
912
  }
965
913
  }
@@ -973,29 +921,31 @@ export function parsePendingUserInputAnswerCommand(raw, pending) {
973
921
  export function formatUserInputRequestMessage(pending, adapterState) {
974
922
  const lines = [
975
923
  pending.summary,
976
- `adapter: ${adapterState.kind}`,
924
+ `${t("approval.adapter")}: ${adapterState.kind}`,
977
925
  ];
978
926
  const hasSecretQuestion = pending.questions.some((question) => question.isSecret);
979
927
  if (hasSecretQuestion) {
980
- lines.push("Warning: this prompt includes secret input upstream, but WeChat replies are not hidden.");
928
+ lines.push(t("input.secretWarning"));
981
929
  }
982
930
  pending.questions.forEach((question, index) => {
983
931
  lines.push("");
984
932
  lines.push(formatUserInputQuestionLabel(question, index));
985
933
  lines.push(question.question);
986
934
  if (question.options?.length) {
987
- lines.push("options:");
935
+ lines.push(t("input.options"));
988
936
  question.options.forEach((option, optionIndex) => {
989
937
  lines.push(` ${optionIndex + 1}. ${option.label} - ${truncatePreview(option.description, 160)}`);
938
+ if (option.preview)
939
+ lines.push(` ${t("input.preview")}\n${truncatePreview(option.preview, 1500)}`);
990
940
  });
991
941
  }
992
942
  if (question.multiple) {
993
- lines.push("multiple selections: allowed (separate values with commas)");
943
+ lines.push(t("input.multiple"));
994
944
  }
995
945
  if (question.isOther) {
996
946
  lines.push(question.customAnswerMode === "value"
997
- ? "custom answer: allowed"
998
- : "custom note: allowed");
947
+ ? t("input.customAnswer")
948
+ : t("input.customNote"));
999
949
  }
1000
950
  });
1001
951
  lines.push("");
@@ -1006,40 +956,40 @@ export function formatUserInputRequestMessage(pending, adapterState) {
1006
956
  }
1007
957
  if (question.options?.length) {
1008
958
  lines.push(question.multiple
1009
- ? "Reply with /answer <option numbers or exact labels separated by commas>."
1010
- : "Reply with /answer <option number or exact label>.");
959
+ ? t("input.answer.multiple")
960
+ : t("input.answer.single"));
1011
961
  if (question.isOther && question.customAnswerMode === "value") {
1012
- lines.push("You may enter a custom answer instead of an option.");
962
+ lines.push(t("input.answer.custom"));
1013
963
  }
1014
964
  else if (question.isOther) {
1015
- lines.push('Add "| your note" to include extra context.');
965
+ lines.push(t("input.answer.note"));
1016
966
  }
1017
967
  }
1018
968
  else {
1019
- lines.push("Reply with /answer <your answer>.");
969
+ lines.push(t("input.answer.text"));
1020
970
  }
1021
971
  }
1022
972
  else {
1023
- lines.push("Reply with /answer questionId=value; questionId2=value.");
1024
- lines.push("You can use question numbers instead of ids.");
1025
- lines.push("For option questions, value can be the option number or exact label.");
1026
- lines.push("For multi-select questions, separate option values with commas.");
973
+ lines.push(t("input.answer.ordered"));
974
+ lines.push(t("input.answer.numbers"));
975
+ lines.push(t("input.answer.optionValue"));
976
+ lines.push(t("input.answer.multipleValue"));
1027
977
  if (pending.questions.some((question) => question.customAnswerMode !== "value")) {
1028
- lines.push('Add "| your note" after an option answer to include extra context.');
978
+ lines.push(t("input.answer.optionNote"));
1029
979
  }
1030
980
  }
1031
- lines.push("Use /stop to interrupt the active turn instead.");
981
+ lines.push(t("input.answer.stop"));
1032
982
  return lines.join("\n");
1033
983
  }
1034
984
  export function formatPendingUserInputReminder(pending) {
1035
985
  if (pending.questions.length === 1) {
1036
986
  const question = pending.questions[0];
1037
987
  if (!question) {
1038
- return "The active CLI is waiting for user input. Reply with /answer and your response, or use /stop to interrupt.";
988
+ return t("input.pending.generic");
1039
989
  }
1040
- return `The active CLI is waiting for user input for ${question.header}. Reply with /answer and your response, or use /stop to interrupt.`;
990
+ return t("input.pending.single", { question: question.header });
1041
991
  }
1042
- 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.`;
992
+ return t("input.pending.multiple", { count: pending.questions.length });
1043
993
  }
1044
994
  export class OutputBatcher {
1045
995
  onFlush;
@@ -1,3 +1,10 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
7
+ import { t } from "../i18n/index.js";
1
8
  import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, isHighRiskShellCommand, isStrictApprovalModeEnabled, isWechatOutboundAttachmentWriteCommand, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
2
9
  export const CLAUDE_WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE = WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
3
10
  const FILE_MUTATION_TOOL_NAMES = new Set([
@@ -41,7 +48,7 @@ export function extractClaudeResumeConversationId(transcriptPath) {
41
48
  const conversationId = fileName.slice(0, -".jsonl".length).trim();
42
49
  return conversationId || null;
43
50
  }
44
- export function buildClaudeHookSettings(command) {
51
+ export function buildClaudeHookSettings(command, env = process.env) {
45
52
  const hook = {
46
53
  hooks: [
47
54
  {
@@ -51,6 +58,9 @@ export function buildClaudeHookSettings(command) {
51
58
  ],
52
59
  };
53
60
  return {
61
+ ...(env.CLI_BRIDGE_STRICT_APPROVAL === "1"
62
+ ? {}
63
+ : { permissions: { allow: ["WebFetch(domain:*)"] } }),
54
64
  hooks: {
55
65
  SessionStart: [hook],
56
66
  SessionEnd: [hook],
@@ -181,7 +191,7 @@ export function buildClaudePermissionApprovalRequest(payload) {
181
191
  const { detailLabel, detailPreview } = summarizeClaudeToolInput(toolName, payload.tool_input);
182
192
  return {
183
193
  source: "cli",
184
- summary: `Claude permission is required for ${toolName}.`,
194
+ summary: t("approval.claude.summary", { tool: toolName }),
185
195
  commandPreview: `${toolName}: ${detailPreview}`,
186
196
  toolName,
187
197
  detailLabel,
@@ -216,7 +226,7 @@ export function getClaudePermissionAutoResponse(payload, env = process.env) {
216
226
  return null;
217
227
  }
218
228
  const toolName = typeof payload.tool_name === "string" ? payload.tool_name.trim() : "";
219
- if (!toolName) {
229
+ if (!toolName || toolName === "AskUserQuestion") {
220
230
  return null;
221
231
  }
222
232
  if (toolName !== "Bash") {
@@ -333,3 +343,50 @@ export function findInjectedClaudePromptIndex(prompt, pendingInputs, nowMs = Dat
333
343
  return candidate.normalizedText === normalizedPrompt;
334
344
  });
335
345
  }
346
+ export function buildClaudeUserInputRequest(payload, requestId) {
347
+ if (payload.tool_name !== "AskUserQuestion" || !Array.isArray(payload.tool_input?.questions)
348
+ || !payload.tool_input.questions.length)
349
+ return null;
350
+ const questions = [];
351
+ for (const [index, raw] of payload.tool_input.questions.entries()) {
352
+ if (!raw || typeof raw !== "object" || typeof raw.question !== "string" || !raw.question.trim()
353
+ || !Array.isArray(raw.options) || !raw.options.length)
354
+ return null;
355
+ const options = [];
356
+ for (const option of raw.options) {
357
+ if (!option || typeof option.label !== "string" || !option.label.trim())
358
+ return null;
359
+ options.push({
360
+ label: option.label,
361
+ description: typeof option.description === "string" ? option.description : "",
362
+ ...(typeof option.preview === "string" ? { preview: option.preview }
363
+ : typeof option.markdown === "string" ? { preview: option.markdown } : {}),
364
+ });
365
+ }
366
+ questions.push({
367
+ id: `q${index + 1}`,
368
+ header: typeof raw.header === "string" ? raw.header : t("input.question.default", { number: index + 1 }),
369
+ question: raw.question,
370
+ multiple: raw.multiSelect === true,
371
+ isOther: true,
372
+ isSecret: false,
373
+ customAnswerMode: "value",
374
+ options,
375
+ });
376
+ }
377
+ return { requestId, summary: t("input.request.claude"), questions };
378
+ }
379
+ export function buildClaudeUserInputHookOutput(payload, request, answers) {
380
+ const mapped = Object.create(null);
381
+ for (const question of request.questions) {
382
+ const values = answers[question.id];
383
+ if (!Array.isArray(values) || !values.length || (!question.multiple && values.length !== 1)
384
+ || values.some((value) => typeof value !== "string" || !value.trim()))
385
+ return null;
386
+ mapped[question.question] = values.join(", ");
387
+ }
388
+ const updatedInput = { ...payload.tool_input, answers: mapped };
389
+ return JSON.stringify({ hookSpecificOutput: payload.hook_event_name === "PermissionRequest"
390
+ ? { hookEventName: "PermissionRequest", decision: { behavior: "allow", updatedInput } }
391
+ : { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput } });
392
+ }
@@ -1,3 +1,10 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
7
+ import { t } from "../i18n/index.js";
1
8
  import { NativeTerminalControl, parseClaudeModelPicker, parseClaudeModelSwitchConfirmation, readClaudePermissionMode } from "./native-terminal-control.js";
2
9
  export class ClaudeNativeControls {
3
10
  previousMode = null;
@@ -9,7 +16,7 @@ export class ClaudeNativeControls {
9
16
  }
10
17
  assertPrompt() {
11
18
  if (readClaudePermissionMode(this.terminal.text) === null) {
12
- throw new Error("Claude must be at an empty native prompt before changing models or plan mode.");
19
+ throw new Error(t("native.claude.emptyPrompt"));
13
20
  }
14
21
  }
15
22
  async listModels() {
@@ -30,7 +37,7 @@ export class ClaudeNativeControls {
30
37
  }
31
38
  const focused = rows.find((row) => row.focused);
32
39
  if (!focused)
33
- throw new Error("Could not identify the focused Claude model. Close the menu and try again.");
40
+ throw new Error(t("native.claude.focusClose"));
34
41
  if (visited.has(focused.index))
35
42
  break;
36
43
  visited.add(focused.index);
@@ -45,7 +52,7 @@ export class ClaudeNativeControls {
45
52
  }
46
53
  const focused = rows.find((row) => row.focused);
47
54
  if (!focused)
48
- throw new Error("Could not identify the focused Claude model.");
55
+ throw new Error(t("native.claude.focus"));
49
56
  if (visitedUp.has(focused.index))
50
57
  break;
51
58
  visitedUp.add(focused.index);
@@ -60,7 +67,7 @@ export class ClaudeNativeControls {
60
67
  async selectModel(id) {
61
68
  const target = JSON.parse(id);
62
69
  if (!Number.isSafeInteger(target.index) || target.index < 1 || typeof target.label !== "string")
63
- throw new Error("Invalid Claude model selection. Send /model again.");
70
+ throw new Error(t("native.claude.invalidModel"));
64
71
  return this.terminal.run(async () => {
65
72
  this.assertPrompt();
66
73
  await this.terminal.openMenu("/model", "Select model");
@@ -68,19 +75,19 @@ export class ClaudeNativeControls {
68
75
  while (true) {
69
76
  const focused = parseClaudeModelPicker(this.terminal.text).find((row) => row.focused);
70
77
  if (!focused)
71
- throw new Error("Could not identify the focused Claude model.");
78
+ throw new Error(t("native.claude.focus"));
72
79
  if (focused.index === target.index) {
73
80
  if (focused.label !== target.label)
74
- throw new Error("The Claude model list changed. Send /model again.");
81
+ throw new Error(t("native.claude.changed"));
75
82
  break;
76
83
  }
77
84
  if (visited.has(focused.index))
78
- throw new Error("The Claude model is no longer selectable. Send /model again.");
85
+ throw new Error(t("native.claude.unavailable"));
79
86
  visited.add(focused.index);
80
87
  await this.terminal.key(focused.index < target.index ? "\u001b[B" : "\u001b[A");
81
88
  }
82
89
  if (!/s to use this session only/i.test(this.terminal.text)) {
83
- throw new Error("This Claude version does not expose session-only model selection. Update Claude Code and try again.");
90
+ throw new Error(t("native.claude.update"));
84
91
  }
85
92
  await this.terminal.submitMenu(undefined, "s");
86
93
  await this.terminal.waitFor(() => parseClaudeModelSwitchConfirmation(this.terminal.text) !== null || readClaudePermissionMode(this.terminal.text) !== null);
@@ -91,7 +98,7 @@ export class ClaudeNativeControls {
91
98
  const matchesTarget = targetLabel.includes(confirmedModel)
92
99
  || (targetLabel.startsWith("default (recommended)") && confirmedModel.endsWith(" (default)"));
93
100
  if (!confirmation.yesFocused || !matchesTarget) {
94
- throw new Error("Claude displayed an unexpected model switch confirmation. Check the visible terminal before trying again.");
101
+ throw new Error(t("native.claude.confirmation"));
95
102
  }
96
103
  await this.terminal.key("\r");
97
104
  await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) !== null);
@@ -99,7 +106,7 @@ export class ClaudeNativeControls {
99
106
  await this.terminal.openMenu("/model", "Select model");
100
107
  const applied = parseClaudeModelPicker(this.terminal.text).find((row) => row.focused);
101
108
  if (!applied || applied.label !== target.label || applied.index !== target.index)
102
- throw new Error("Claude did not confirm the selected model.");
109
+ throw new Error(t("native.claude.notConfirmed"));
103
110
  await this.terminal.closeMenu();
104
111
  return { id, displayName: target.label, isCurrent: true };
105
112
  });
@@ -115,7 +122,7 @@ export class ClaudeNativeControls {
115
122
  if (current === "plan")
116
123
  return true;
117
124
  if (!["default", "acceptEdits", "bypassPermissions", "auto"].includes(current)) {
118
- throw new Error(`Claude cannot restore permission mode ${current} through the native mode switch.`);
125
+ throw new Error(t("native.claude.restoreUnsupported", { mode: current }));
119
126
  }
120
127
  await this.terminal.command("/plan");
121
128
  await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) === "plan");
@@ -133,7 +140,7 @@ export class ClaudeNativeControls {
133
140
  if (mode === target)
134
141
  break;
135
142
  if (!mode || visited.has(mode))
136
- throw new Error(`Claude could not restore ${target}; check the native permission mode.`);
143
+ throw new Error(t("native.claude.restoreFailed", { mode: target }));
137
144
  visited.add(mode);
138
145
  await this.terminal.key("\u001b[Z");
139
146
  await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) !== mode);