@unlinearity/cli-wechat-bridge 1.2.0 → 1.2.2
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 +25 -10
- package/bin/wechat-check-update.mjs +0 -0
- package/bin/wechat-claude.mjs +0 -0
- package/bin/wechat-codex.mjs +0 -0
- package/bin/wechat-daemon.mjs +0 -0
- package/bin/wechat-opencode.mjs +0 -0
- package/bin/wechat-pi.mjs +0 -0
- package/bin/wechat-setup.mjs +0 -0
- package/bin/wecom-check-update.mjs +0 -0
- package/bin/wecom-claude.mjs +0 -0
- package/bin/wecom-codex.mjs +0 -0
- package/bin/wecom-daemon.mjs +0 -0
- package/bin/wecom-opencode.mjs +0 -0
- package/bin/wecom-pi.mjs +0 -0
- package/bin/wecom-setup.mjs +0 -0
- package/dist/bridge/adapter-control.js +19 -12
- package/dist/bridge/bridge-adapters.claude.js +219 -115
- package/dist/bridge/bridge-adapters.codex.js +295 -90
- package/dist/bridge/bridge-adapters.core.js +84 -7
- package/dist/bridge/bridge-adapters.opencode.js +344 -74
- package/dist/bridge/bridge-adapters.pi.js +23 -18
- package/dist/bridge/bridge-adapters.shared.js +9 -9
- package/dist/bridge/bridge-session-resume.js +13 -6
- package/dist/bridge/bridge-utils.js +94 -144
- package/dist/bridge/claude-hooks.js +60 -3
- package/dist/bridge/claude-native-controls.js +32 -13
- package/dist/bridge/codex-visible-thread-proxy.js +140 -0
- package/dist/bridge/language-control.js +33 -0
- package/dist/bridge/native-terminal-control.js +38 -10
- package/dist/bridge/opencode-v2-compat.js +458 -0
- package/dist/bridge/wechat-bridge.js +57 -42
- package/dist/channels/channel-driver-factory.js +44 -0
- package/dist/channels/local/local-channel-port.js +40 -0
- package/dist/channels/local/local-driver.js +217 -0
- package/dist/channels/wechat/wechat-driver.js +11 -2
- package/dist/channels/wecom/wecom-driver.js +8 -1
- package/dist/companion/local-companion.js +27 -7
- package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
- package/dist/companion/pi-tui-bridge-extension.js +10 -4
- package/dist/core/bridge-control.js +3 -0
- package/dist/core/bridge-defer.js +3 -2
- package/dist/core/bridge-event-forwarder.js +3 -0
- package/dist/core/channel-types.js +5 -1
- package/dist/daemon/wechat-daemon.js +202 -124
- package/dist/i18n/index.js +25 -11
- package/dist/i18n/messages-en.js +244 -1
- package/dist/i18n/messages-zh.js +244 -1
- package/dist/runtime/legacy-adapter-runtime.js +7 -4
- package/dist/wechat/channel-config.js +3 -2
- package/package.json +4 -2
|
@@ -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
|
-
: "
|
|
428
|
+
: t("status.none");
|
|
428
429
|
return [
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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 ? "
|
|
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
|
-
"
|
|
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
|
-
? "
|
|
503
|
+
? t("session.localInput", { adapter: "Codex" })
|
|
561
504
|
: adapter === "claude"
|
|
562
|
-
? "
|
|
505
|
+
? t("session.localInput", { adapter: "Claude" })
|
|
563
506
|
: adapter === "opencode"
|
|
564
|
-
? "
|
|
507
|
+
? t("session.localInput", { adapter: "OpenCode" })
|
|
565
508
|
: adapter === "pi"
|
|
566
|
-
? "
|
|
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
|
|
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
|
-
"
|
|
793
|
-
...(pending.toolName ? [
|
|
740
|
+
t("approval.claude.header"),
|
|
741
|
+
...(pending.toolName ? [`${t("approval.tool")}: ${pending.toolName}`] : []),
|
|
794
742
|
...(pending.detailPreview
|
|
795
|
-
? [`${
|
|
743
|
+
? [`${detailLabel}: ${pending.detailPreview}`]
|
|
796
744
|
: pending.commandPreview
|
|
797
|
-
? [
|
|
745
|
+
? [`${t("approval.details")}: ${pending.commandPreview}`]
|
|
798
746
|
: []),
|
|
799
|
-
"
|
|
800
|
-
"
|
|
747
|
+
t("approval.confirmInstructions"),
|
|
748
|
+
t("approval.denyInstructions"),
|
|
801
749
|
].join("\n");
|
|
802
750
|
}
|
|
803
751
|
return [
|
|
804
|
-
"
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
"
|
|
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
|
|
764
|
+
return t("approval.pending.claude", { target: truncatePreview(target, 140) });
|
|
817
765
|
}
|
|
818
|
-
return
|
|
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:
|
|
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:
|
|
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:
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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:
|
|
893
|
+
error: t("input.answer.unknownQuestion", { reference }),
|
|
946
894
|
};
|
|
947
895
|
}
|
|
948
896
|
if (answers[question.id]) {
|
|
949
897
|
return {
|
|
950
|
-
error:
|
|
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:
|
|
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
|
-
|
|
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("
|
|
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
|
|
943
|
+
lines.push(t("input.multiple"));
|
|
994
944
|
}
|
|
995
945
|
if (question.isOther) {
|
|
996
946
|
lines.push(question.customAnswerMode === "value"
|
|
997
|
-
? "
|
|
998
|
-
: "
|
|
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
|
-
? "
|
|
1010
|
-
: "
|
|
959
|
+
? t("input.answer.multiple")
|
|
960
|
+
: t("input.answer.single"));
|
|
1011
961
|
if (question.isOther && question.customAnswerMode === "value") {
|
|
1012
|
-
lines.push("
|
|
962
|
+
lines.push(t("input.answer.custom"));
|
|
1013
963
|
}
|
|
1014
964
|
else if (question.isOther) {
|
|
1015
|
-
lines.push(
|
|
965
|
+
lines.push(t("input.answer.note"));
|
|
1016
966
|
}
|
|
1017
967
|
}
|
|
1018
968
|
else {
|
|
1019
|
-
lines.push("
|
|
969
|
+
lines.push(t("input.answer.text"));
|
|
1020
970
|
}
|
|
1021
971
|
}
|
|
1022
972
|
else {
|
|
1023
|
-
lines.push("
|
|
1024
|
-
lines.push("
|
|
1025
|
-
lines.push("
|
|
1026
|
-
lines.push("
|
|
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(
|
|
978
|
+
lines.push(t("input.answer.optionNote"));
|
|
1029
979
|
}
|
|
1030
980
|
}
|
|
1031
|
-
lines.push("
|
|
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 "
|
|
988
|
+
return t("input.pending.generic");
|
|
1039
989
|
}
|
|
1040
|
-
return
|
|
990
|
+
return t("input.pending.single", { question: question.header });
|
|
1041
991
|
}
|
|
1042
|
-
return
|
|
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:
|
|
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,4 +1,11 @@
|
|
|
1
|
-
|
|
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";
|
|
8
|
+
import { NativeTerminalControl, parseClaudeModelPicker, parseClaudeModelSwitchConfirmation, readClaudePermissionMode } from "./native-terminal-control.js";
|
|
2
9
|
export class ClaudeNativeControls {
|
|
3
10
|
previousMode = null;
|
|
4
11
|
terminal;
|
|
@@ -9,7 +16,7 @@ export class ClaudeNativeControls {
|
|
|
9
16
|
}
|
|
10
17
|
assertPrompt() {
|
|
11
18
|
if (readClaudePermissionMode(this.terminal.text) === null) {
|
|
12
|
-
throw new Error("
|
|
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("
|
|
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("
|
|
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("
|
|
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,26 +75,38 @@ 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("
|
|
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("
|
|
81
|
+
throw new Error(t("native.claude.changed"));
|
|
75
82
|
break;
|
|
76
83
|
}
|
|
77
84
|
if (visited.has(focused.index))
|
|
78
|
-
throw new Error("
|
|
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("
|
|
90
|
+
throw new Error(t("native.claude.update"));
|
|
84
91
|
}
|
|
85
92
|
await this.terminal.submitMenu(undefined, "s");
|
|
86
|
-
await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) !== null);
|
|
93
|
+
await this.terminal.waitFor(() => parseClaudeModelSwitchConfirmation(this.terminal.text) !== null || readClaudePermissionMode(this.terminal.text) !== null);
|
|
94
|
+
const confirmation = parseClaudeModelSwitchConfirmation(this.terminal.text);
|
|
95
|
+
if (confirmation) {
|
|
96
|
+
const targetLabel = target.label.toLowerCase();
|
|
97
|
+
const confirmedModel = confirmation.model.toLowerCase();
|
|
98
|
+
const matchesTarget = targetLabel.includes(confirmedModel)
|
|
99
|
+
|| (targetLabel.startsWith("default (recommended)") && confirmedModel.endsWith(" (default)"));
|
|
100
|
+
if (!confirmation.yesFocused || !matchesTarget) {
|
|
101
|
+
throw new Error(t("native.claude.confirmation"));
|
|
102
|
+
}
|
|
103
|
+
await this.terminal.key("\r");
|
|
104
|
+
await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) !== null);
|
|
105
|
+
}
|
|
87
106
|
await this.terminal.openMenu("/model", "Select model");
|
|
88
107
|
const applied = parseClaudeModelPicker(this.terminal.text).find((row) => row.focused);
|
|
89
108
|
if (!applied || applied.label !== target.label || applied.index !== target.index)
|
|
90
|
-
throw new Error("
|
|
109
|
+
throw new Error(t("native.claude.notConfirmed"));
|
|
91
110
|
await this.terminal.closeMenu();
|
|
92
111
|
return { id, displayName: target.label, isCurrent: true };
|
|
93
112
|
});
|
|
@@ -103,7 +122,7 @@ export class ClaudeNativeControls {
|
|
|
103
122
|
if (current === "plan")
|
|
104
123
|
return true;
|
|
105
124
|
if (!["default", "acceptEdits", "bypassPermissions", "auto"].includes(current)) {
|
|
106
|
-
throw new Error(
|
|
125
|
+
throw new Error(t("native.claude.restoreUnsupported", { mode: current }));
|
|
107
126
|
}
|
|
108
127
|
await this.terminal.command("/plan");
|
|
109
128
|
await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) === "plan");
|
|
@@ -121,7 +140,7 @@ export class ClaudeNativeControls {
|
|
|
121
140
|
if (mode === target)
|
|
122
141
|
break;
|
|
123
142
|
if (!mode || visited.has(mode))
|
|
124
|
-
throw new Error(
|
|
143
|
+
throw new Error(t("native.claude.restoreFailed", { mode: target }));
|
|
125
144
|
visited.add(mode);
|
|
126
145
|
await this.terminal.key("\u001b[Z");
|
|
127
146
|
await this.terminal.waitFor(() => readClaudePermissionMode(this.terminal.text) !== mode);
|