cli-wechat-bridge 1.0.5 → 1.0.7
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 +126 -366
- package/dist/bridge/bridge-adapters.claude.js +127 -6
- package/dist/bridge/bridge-adapters.codex.js +219 -7
- package/dist/bridge/bridge-adapters.core.js +13 -5
- package/dist/bridge/bridge-adapters.opencode.js +143 -24
- package/dist/bridge/bridge-adapters.shared.js +217 -6
- package/dist/bridge/bridge-process-reaper.js +40 -1
- package/dist/bridge/bridge-state.js +6 -3
- package/dist/bridge/bridge-utils.js +49 -2
- package/dist/bridge/claude-hooks.js +64 -3
- package/dist/bridge/wechat-bridge.js +69 -3
- package/dist/commands/check-update.js +1 -1
- package/dist/companion/local-companion-start.js +27 -2
- package/dist/companion/local-companion.js +21 -4
- package/dist/daemon/wechat-daemon.js +277 -21
- package/package.json +5 -6
- package/dist/companion/codex-panel-link.js +0 -72
- package/dist/companion/codex-panel.js +0 -179
- package/dist/media/media-types.js +0 -1
- package/dist/wechat/standalone-bot.js +0 -355
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
2
|
import { OPENCODE_SERVER_HOST, OPENCODE_SERVER_READY_TIMEOUT_MS, OPENCODE_SSE_RECONNECT_DELAY_MS, OPENCODE_SESSION_IDLE_SETTLE_MS, OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS, buildCliEnvironment, isRecord, describeUnknownError, resolveSpawnTarget, reserveLocalPort, waitForTcpPort, delay, } from "./bridge-adapters.shared.js";
|
|
3
3
|
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
4
|
-
import { buildOneTimeCode, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
|
|
4
|
+
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, buildOneTimeCode, isWechatOutboundAttachmentMutationTool, isWechatOutboundAttachmentWriteCommand, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
|
|
5
5
|
const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPENCODE_DEBUG ?? "");
|
|
6
6
|
const OPENCODE_DUPLICATE_EVENT_TTL_MS = 150;
|
|
7
7
|
const OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS = 30_000;
|
|
8
8
|
const OPENCODE_RECENT_LOCAL_PROMPT_TTL_MS = 10_000;
|
|
9
9
|
const OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS = 5_000;
|
|
10
|
+
const OPENCODE_TUI_SELECT_RETRY_DELAYS_MS = [250, 750, 1_500];
|
|
10
11
|
/* ------------------------------------------------------------------ */
|
|
11
12
|
/* Adapter */
|
|
12
13
|
/* ------------------------------------------------------------------ */
|
|
@@ -82,7 +83,7 @@ export class OpenCodeServerAdapter {
|
|
|
82
83
|
this.startSseListener();
|
|
83
84
|
if (this.options.renderMode === "companion") {
|
|
84
85
|
await this.startNativeClient();
|
|
85
|
-
await this.syncVisibleSessionToShared({ force: true });
|
|
86
|
+
await this.syncVisibleSessionToShared({ force: true, retry: true });
|
|
86
87
|
}
|
|
87
88
|
else {
|
|
88
89
|
this.state.pid = serverProcess.pid;
|
|
@@ -177,6 +178,7 @@ export class OpenCodeServerAdapter {
|
|
|
177
178
|
clearTrackedTurn: true,
|
|
178
179
|
syncVisible: true,
|
|
179
180
|
forceVisibleSync: true,
|
|
181
|
+
retryVisibleSync: true,
|
|
180
182
|
});
|
|
181
183
|
}
|
|
182
184
|
async interrupt() {
|
|
@@ -365,7 +367,7 @@ export class OpenCodeServerAdapter {
|
|
|
365
367
|
if (this.nativeProcess) {
|
|
366
368
|
return;
|
|
367
369
|
}
|
|
368
|
-
const env =
|
|
370
|
+
const env = this.buildNativeClientEnv();
|
|
369
371
|
const attachArgs = await this.buildNativeAttachArgs();
|
|
370
372
|
const target = resolveSpawnTarget(this.options.command, this.options.kind, { env });
|
|
371
373
|
const startedAt = nowIso();
|
|
@@ -410,6 +412,7 @@ export class OpenCodeServerAdapter {
|
|
|
410
412
|
}
|
|
411
413
|
async buildNativeAttachArgs() {
|
|
412
414
|
const args = ["attach", this.getServerUrl()];
|
|
415
|
+
args.push("--dir", this.options.cwd);
|
|
413
416
|
const sessionId = this.activeSessionId;
|
|
414
417
|
if (sessionId && (await this.hasSession(sessionId))) {
|
|
415
418
|
args.push("--session", sessionId);
|
|
@@ -417,6 +420,16 @@ export class OpenCodeServerAdapter {
|
|
|
417
420
|
args.push(...(this.options.extraCliArgs ?? []));
|
|
418
421
|
return args;
|
|
419
422
|
}
|
|
423
|
+
buildNativeClientEnv() {
|
|
424
|
+
const env = buildCliEnvironment(this.options.kind);
|
|
425
|
+
if (this.activeSessionId) {
|
|
426
|
+
env.OPENCODE_ROUTE = JSON.stringify({
|
|
427
|
+
type: "session",
|
|
428
|
+
sessionID: this.activeSessionId,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return env;
|
|
432
|
+
}
|
|
420
433
|
async createSdkClient() {
|
|
421
434
|
try {
|
|
422
435
|
const { createOpencodeClient } = await import("@opencode-ai/sdk/v2");
|
|
@@ -441,6 +454,21 @@ export class OpenCodeServerAdapter {
|
|
|
441
454
|
if (!this.client) {
|
|
442
455
|
return;
|
|
443
456
|
}
|
|
457
|
+
if (this.options.sessionStartMode === "new") {
|
|
458
|
+
const session = this.unwrapOrThrow(await this.client.session.create({
|
|
459
|
+
directory: this.options.cwd,
|
|
460
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
461
|
+
}));
|
|
462
|
+
this.switchSharedSession(session, {
|
|
463
|
+
source: "local",
|
|
464
|
+
reason: "local_follow",
|
|
465
|
+
syncVisible: false,
|
|
466
|
+
});
|
|
467
|
+
this.state.lastSessionSwitchAt = undefined;
|
|
468
|
+
this.state.lastSessionSwitchSource = undefined;
|
|
469
|
+
this.state.lastSessionSwitchReason = undefined;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
444
472
|
let listedSessions = [];
|
|
445
473
|
try {
|
|
446
474
|
const result = await this.client.session.list();
|
|
@@ -801,6 +829,11 @@ export class OpenCodeServerAdapter {
|
|
|
801
829
|
return;
|
|
802
830
|
}
|
|
803
831
|
this.clearWechatWorkingNotice();
|
|
832
|
+
const denyMessage = this.getWechatOutboundAttachmentPermissionDenyMessage(properties, pendingPermission);
|
|
833
|
+
if (denyMessage) {
|
|
834
|
+
this.rejectPermission(pendingPermission, denyMessage);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
804
837
|
const approval = this.toPendingApproval(pendingPermission);
|
|
805
838
|
this.pendingPermission = pendingPermission;
|
|
806
839
|
this.state.pendingApproval = approval;
|
|
@@ -817,6 +850,57 @@ export class OpenCodeServerAdapter {
|
|
|
817
850
|
this.state.pendingApproval = null;
|
|
818
851
|
this.state.pendingApprovalOrigin = undefined;
|
|
819
852
|
}
|
|
853
|
+
getWechatOutboundAttachmentPermissionDenyMessage(properties, pendingPermission) {
|
|
854
|
+
const metadata = isRecord(properties.metadata) ? properties.metadata : {};
|
|
855
|
+
const command = this.extractPermissionCommand(properties, metadata);
|
|
856
|
+
if (isWechatOutboundAttachmentWriteCommand(command)) {
|
|
857
|
+
return WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
|
|
858
|
+
}
|
|
859
|
+
const targetPath = [
|
|
860
|
+
command,
|
|
861
|
+
typeof metadata.path === "string" ? metadata.path : "",
|
|
862
|
+
typeof metadata.file === "string" ? metadata.file : "",
|
|
863
|
+
typeof metadata.filePath === "string" ? metadata.filePath : "",
|
|
864
|
+
typeof metadata.filepath === "string" ? metadata.filepath : "",
|
|
865
|
+
typeof metadata.parentDir === "string" ? metadata.parentDir : "",
|
|
866
|
+
typeof metadata.target === "string" ? metadata.target : "",
|
|
867
|
+
typeof metadata.detail === "string" ? metadata.detail : "",
|
|
868
|
+
Array.isArray(properties.patterns)
|
|
869
|
+
? properties.patterns.filter((value) => typeof value === "string").join(" ")
|
|
870
|
+
: "",
|
|
871
|
+
typeof properties.pattern === "string" ? properties.pattern : "",
|
|
872
|
+
].join(" ");
|
|
873
|
+
return isWechatOutboundAttachmentMutationTool(pendingPermission.request.toolName, targetPath)
|
|
874
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
875
|
+
: null;
|
|
876
|
+
}
|
|
877
|
+
rejectPermission(pendingPermission, denyMessage) {
|
|
878
|
+
const client = this.client;
|
|
879
|
+
if (!client) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
void (async () => {
|
|
883
|
+
try {
|
|
884
|
+
const result = await client.permission.respond({
|
|
885
|
+
sessionID: pendingPermission.sessionId,
|
|
886
|
+
permissionID: pendingPermission.permissionId,
|
|
887
|
+
directory: this.options.cwd,
|
|
888
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
889
|
+
response: "reject",
|
|
890
|
+
});
|
|
891
|
+
if (result.error !== undefined) {
|
|
892
|
+
throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
catch (err) {
|
|
896
|
+
this.emit({
|
|
897
|
+
type: "stderr",
|
|
898
|
+
text: `${denyMessage}\nFailed to reject permission: ${describeUnknownError(err)}`,
|
|
899
|
+
timestamp: nowIso(),
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
})();
|
|
903
|
+
}
|
|
820
904
|
toPendingApproval(pendingPermission) {
|
|
821
905
|
return {
|
|
822
906
|
...pendingPermission.request,
|
|
@@ -834,24 +918,14 @@ export class OpenCodeServerAdapter {
|
|
|
834
918
|
if (!sessionId || !permissionId) {
|
|
835
919
|
return null;
|
|
836
920
|
}
|
|
837
|
-
const toolName =
|
|
838
|
-
? properties.type
|
|
839
|
-
: typeof properties.permission === "string"
|
|
840
|
-
? properties.permission
|
|
841
|
-
: undefined;
|
|
921
|
+
const toolName = this.extractPermissionToolName(properties);
|
|
842
922
|
const title = typeof properties.title === "string"
|
|
843
923
|
? properties.title
|
|
844
924
|
: typeof properties.permission === "string"
|
|
845
925
|
? `Permission request: ${properties.permission}`
|
|
846
926
|
: undefined;
|
|
847
927
|
const metadata = isRecord(properties.metadata) ? properties.metadata : {};
|
|
848
|
-
const command =
|
|
849
|
-
? metadata.command
|
|
850
|
-
: typeof metadata.detail === "string"
|
|
851
|
-
? metadata.detail
|
|
852
|
-
: Array.isArray(properties.patterns)
|
|
853
|
-
? properties.patterns.filter((value) => typeof value === "string").join(", ")
|
|
854
|
-
: undefined;
|
|
928
|
+
const command = this.extractPermissionCommand(properties, metadata);
|
|
855
929
|
return {
|
|
856
930
|
sessionId,
|
|
857
931
|
permissionId,
|
|
@@ -869,6 +943,26 @@ export class OpenCodeServerAdapter {
|
|
|
869
943
|
},
|
|
870
944
|
};
|
|
871
945
|
}
|
|
946
|
+
extractPermissionToolName(properties) {
|
|
947
|
+
return typeof properties.type === "string"
|
|
948
|
+
? properties.type
|
|
949
|
+
: typeof properties.permission === "string"
|
|
950
|
+
? properties.permission
|
|
951
|
+
: undefined;
|
|
952
|
+
}
|
|
953
|
+
extractPermissionCommand(properties, metadata) {
|
|
954
|
+
if (typeof metadata.command === "string") {
|
|
955
|
+
return metadata.command;
|
|
956
|
+
}
|
|
957
|
+
if (typeof metadata.detail === "string") {
|
|
958
|
+
return metadata.detail;
|
|
959
|
+
}
|
|
960
|
+
if (Array.isArray(properties.patterns)) {
|
|
961
|
+
const patterns = properties.patterns.filter((value) => typeof value === "string");
|
|
962
|
+
return patterns.length > 0 ? patterns.join(", ") : undefined;
|
|
963
|
+
}
|
|
964
|
+
return undefined;
|
|
965
|
+
}
|
|
872
966
|
handleSessionCreated(properties) {
|
|
873
967
|
if (!isRecord(properties)) {
|
|
874
968
|
return;
|
|
@@ -1788,7 +1882,9 @@ export class OpenCodeServerAdapter {
|
|
|
1788
1882
|
this.recordSessionSwitch(sessionId, options.source, options.reason, options.notify);
|
|
1789
1883
|
}
|
|
1790
1884
|
if (options.syncVisible !== false && (changed || options.forceVisibleSync)) {
|
|
1791
|
-
void this.syncVisibleSessionSelection(typeof session === "string" ? { id: session } : session
|
|
1885
|
+
void this.syncVisibleSessionSelection(typeof session === "string" ? { id: session } : session, {
|
|
1886
|
+
retry: options.retryVisibleSync,
|
|
1887
|
+
});
|
|
1792
1888
|
}
|
|
1793
1889
|
return changed;
|
|
1794
1890
|
}
|
|
@@ -1799,7 +1895,7 @@ export class OpenCodeServerAdapter {
|
|
|
1799
1895
|
await this.syncVisibleSessionSelection({
|
|
1800
1896
|
id: this.activeSessionId,
|
|
1801
1897
|
workspaceID: this.activeWorkspaceId ?? undefined,
|
|
1802
|
-
}, { force: options.force });
|
|
1898
|
+
}, { force: options.force, retry: options.retry });
|
|
1803
1899
|
}
|
|
1804
1900
|
async syncVisibleSessionSelection(session, options = {}) {
|
|
1805
1901
|
if (!this.client?.tui ||
|
|
@@ -1810,13 +1906,9 @@ export class OpenCodeServerAdapter {
|
|
|
1810
1906
|
this.suppressedTuiSessionSelectId = session.id;
|
|
1811
1907
|
this.logDebug(`[opencode-adapter:tui] selectSession session=${session.id} workspace=${session.workspaceID ?? this.activeWorkspaceId ?? "(none)"}`);
|
|
1812
1908
|
try {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
sessionID: session.id,
|
|
1817
|
-
});
|
|
1818
|
-
if (result.error !== undefined) {
|
|
1819
|
-
throw result.error;
|
|
1909
|
+
await this.sendVisibleSessionSelection(session);
|
|
1910
|
+
if (options.retry === true) {
|
|
1911
|
+
this.scheduleVisibleSessionSelectionRetries(session);
|
|
1820
1912
|
}
|
|
1821
1913
|
}
|
|
1822
1914
|
catch (err) {
|
|
@@ -1826,6 +1918,33 @@ export class OpenCodeServerAdapter {
|
|
|
1826
1918
|
this.logDebug(`[opencode-adapter:tui] selectSession failed for ${session.id}: ${describeUnknownError(err)}`);
|
|
1827
1919
|
}
|
|
1828
1920
|
}
|
|
1921
|
+
async sendVisibleSessionSelection(session) {
|
|
1922
|
+
if (!this.client?.tui) {
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
const result = await this.client.tui.selectSession({
|
|
1926
|
+
directory: this.options.cwd,
|
|
1927
|
+
workspace: session.workspaceID ?? this.activeWorkspaceId ?? undefined,
|
|
1928
|
+
sessionID: session.id,
|
|
1929
|
+
});
|
|
1930
|
+
if (result.error !== undefined) {
|
|
1931
|
+
throw result.error;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
scheduleVisibleSessionSelectionRetries(session) {
|
|
1935
|
+
for (const retryDelayMs of OPENCODE_TUI_SELECT_RETRY_DELAYS_MS) {
|
|
1936
|
+
setTimeout(() => {
|
|
1937
|
+
if (this.shuttingDown ||
|
|
1938
|
+
this.options.renderMode !== "companion" ||
|
|
1939
|
+
this.activeSessionId !== session.id) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
void this.sendVisibleSessionSelection(session).catch((err) => {
|
|
1943
|
+
this.logDebug(`[opencode-adapter:tui] selectSession retry failed for ${session.id}: ${describeUnknownError(err)}`);
|
|
1944
|
+
});
|
|
1945
|
+
}, retryDelayMs).unref?.();
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1829
1948
|
isLocalSessionNavigationCommand(command) {
|
|
1830
1949
|
if (!command) {
|
|
1831
1950
|
return false;
|
|
@@ -2,13 +2,10 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { spawn as spawnPty } from "node-pty";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { buildClaudeFailureMessage, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeResumeConversationId, findInjectedClaudePromptIndex, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
10
|
-
import { detectCliApproval, isHighRiskShellCommand, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
11
|
-
import { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
|
|
7
|
+
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
|
|
8
|
+
import { getNotificationThreadId, isRecord, } from "./bridge-adapter-common.js";
|
|
12
9
|
export { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
|
|
13
10
|
export const DEFAULT_COLS = 120;
|
|
14
11
|
export const DEFAULT_ROWS = 30;
|
|
@@ -152,8 +149,220 @@ export function buildCodexApprovalRequest(method, params) {
|
|
|
152
149
|
commandPreview: truncatePreview(preview, 180),
|
|
153
150
|
};
|
|
154
151
|
}
|
|
152
|
+
if (method === "item/permissions/requestApproval") {
|
|
153
|
+
const reason = typeof params.reason === "string" ? params.reason : "";
|
|
154
|
+
const preview = summarizeCodexPermissionsRequest(params.permissions);
|
|
155
|
+
return {
|
|
156
|
+
source: "cli",
|
|
157
|
+
summary: reason
|
|
158
|
+
? `Codex needs approval before granting extra permissions: ${truncatePreview(reason, 160)}`
|
|
159
|
+
: "Codex needs approval before granting extra permissions.",
|
|
160
|
+
commandPreview: truncatePreview(preview, 180),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
export function getCodexWechatOutboundAttachmentDenyMessage(method, params) {
|
|
166
|
+
if (!isRecord(params)) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (method === "item/commandExecution/requestApproval") {
|
|
170
|
+
return isWechatOutboundAttachmentWriteCommand(params.command) ||
|
|
171
|
+
containsWechatOutboundAttachmentPathDeep(params.additionalPermissions)
|
|
172
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
173
|
+
: null;
|
|
174
|
+
}
|
|
175
|
+
if (method === "item/fileChange/requestApproval") {
|
|
176
|
+
return containsWechatOutboundAttachmentPath(params.grantRoot)
|
|
177
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
178
|
+
: null;
|
|
179
|
+
}
|
|
180
|
+
if (method === "item/permissions/requestApproval") {
|
|
181
|
+
return containsWechatOutboundAttachmentPathDeep(params.permissions)
|
|
182
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
183
|
+
: null;
|
|
184
|
+
}
|
|
155
185
|
return null;
|
|
156
186
|
}
|
|
187
|
+
function commandApprovalAllowsAccept(availableDecisions) {
|
|
188
|
+
if (!Array.isArray(availableDecisions)) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return availableDecisions.some((decision) => decision === "accept");
|
|
192
|
+
}
|
|
193
|
+
function normalizePermissionPath(pathValue) {
|
|
194
|
+
const normalized = pathValue.trim().replace(/\\/g, "/").toLowerCase();
|
|
195
|
+
return normalized === "/" ? normalized : normalized.replace(/\/+$/g, "");
|
|
196
|
+
}
|
|
197
|
+
function isHighRiskPermissionPath(pathValue) {
|
|
198
|
+
const normalized = normalizePermissionPath(pathValue);
|
|
199
|
+
if (!normalized) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (normalized === "/" || /^[a-z]:$/i.test(normalized)) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return /^[a-z]:(?:\/windows|\/program files(?: \(x86\))?|\/programdata)(?:\/|$)/i.test(normalized);
|
|
206
|
+
}
|
|
207
|
+
function collectPermissionPaths(value, output) {
|
|
208
|
+
if (typeof value === "string") {
|
|
209
|
+
output.push(value);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (Array.isArray(value)) {
|
|
213
|
+
for (const item of value) {
|
|
214
|
+
collectPermissionPaths(item, output);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!isRecord(value)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (typeof value.path === "string") {
|
|
222
|
+
output.push(value.path);
|
|
223
|
+
}
|
|
224
|
+
if (typeof value.pattern === "string") {
|
|
225
|
+
output.push(value.pattern);
|
|
226
|
+
}
|
|
227
|
+
for (const item of Object.values(value)) {
|
|
228
|
+
collectPermissionPaths(item, output);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function hasRootSpecialPermissionPath(value) {
|
|
232
|
+
if (Array.isArray(value)) {
|
|
233
|
+
return value.some(hasRootSpecialPermissionPath);
|
|
234
|
+
}
|
|
235
|
+
if (!isRecord(value)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
if ((value.kind === "root" || value.kind === "Root") ||
|
|
239
|
+
(value.value === "root" || value.value === "Root")) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
return Object.values(value).some(hasRootSpecialPermissionPath);
|
|
243
|
+
}
|
|
244
|
+
function containsHighRiskPermissionTarget(value) {
|
|
245
|
+
if (hasRootSpecialPermissionPath(value)) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
const paths = [];
|
|
249
|
+
collectPermissionPaths(value, paths);
|
|
250
|
+
return paths.some(isHighRiskPermissionPath);
|
|
251
|
+
}
|
|
252
|
+
export function getCodexApprovalAutoResponse(method, params) {
|
|
253
|
+
if (method === "item/commandExecution/requestApproval") {
|
|
254
|
+
const command = typeof params.command === "string" ? params.command : "";
|
|
255
|
+
if (!commandApprovalAllowsAccept(params.availableDecisions)) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
if (command && isHighRiskShellCommand(command)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (containsHighRiskPermissionTarget(params.additionalPermissions)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
result: { decision: "accept" },
|
|
266
|
+
reason: command
|
|
267
|
+
? `low-risk command ${truncatePreview(command, 120)}`
|
|
268
|
+
: "low-risk command approval",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (method === "item/fileChange/requestApproval") {
|
|
272
|
+
if (containsHighRiskPermissionTarget(params.grantRoot)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
result: { decision: "accept" },
|
|
277
|
+
reason: "low-risk file change approval",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (method === "item/permissions/requestApproval") {
|
|
281
|
+
if (containsHighRiskPermissionTarget(params.permissions)) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
result: buildCodexPermissionsRequestApprovalResponse(params, "confirm", {
|
|
286
|
+
strictAutoReview: true,
|
|
287
|
+
}),
|
|
288
|
+
reason: "low-risk permission grant",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function collectStringValues(value) {
|
|
294
|
+
return Array.isArray(value)
|
|
295
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
296
|
+
: [];
|
|
297
|
+
}
|
|
298
|
+
function summarizeCodexPermissionsRequest(permissions) {
|
|
299
|
+
if (!isRecord(permissions)) {
|
|
300
|
+
return "Additional permissions requested.";
|
|
301
|
+
}
|
|
302
|
+
const parts = [];
|
|
303
|
+
if (isRecord(permissions.network) && permissions.network.enabled === true) {
|
|
304
|
+
parts.push("network access");
|
|
305
|
+
}
|
|
306
|
+
if (isRecord(permissions.fileSystem)) {
|
|
307
|
+
const readPaths = collectStringValues(permissions.fileSystem.read);
|
|
308
|
+
const writePaths = collectStringValues(permissions.fileSystem.write);
|
|
309
|
+
if (readPaths.length > 0) {
|
|
310
|
+
parts.push(`read: ${readPaths.join(", ")}`);
|
|
311
|
+
}
|
|
312
|
+
if (writePaths.length > 0) {
|
|
313
|
+
parts.push(`write: ${writePaths.join(", ")}`);
|
|
314
|
+
}
|
|
315
|
+
if (Array.isArray(permissions.fileSystem.entries) && permissions.fileSystem.entries.length > 0) {
|
|
316
|
+
parts.push(`filesystem entries: ${permissions.fileSystem.entries.length}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return parts.length > 0 ? parts.join("; ") : "Additional permissions requested.";
|
|
320
|
+
}
|
|
321
|
+
function clonePermissionObject(value) {
|
|
322
|
+
if (!isRecord(value)) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return JSON.parse(JSON.stringify(value));
|
|
326
|
+
}
|
|
327
|
+
export function buildCodexPermissionsRequestApprovalResponse(params, action = "deny", options = {}) {
|
|
328
|
+
const permissions = {};
|
|
329
|
+
if (action === "confirm" && isRecord(params) && isRecord(params.permissions)) {
|
|
330
|
+
const network = clonePermissionObject(params.permissions.network);
|
|
331
|
+
const fileSystem = clonePermissionObject(params.permissions.fileSystem);
|
|
332
|
+
if (network) {
|
|
333
|
+
permissions.network = network;
|
|
334
|
+
}
|
|
335
|
+
if (fileSystem) {
|
|
336
|
+
permissions.fileSystem = fileSystem;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const response = {
|
|
340
|
+
permissions,
|
|
341
|
+
scope: "turn",
|
|
342
|
+
};
|
|
343
|
+
if (options.strictAutoReview) {
|
|
344
|
+
response.strictAutoReview = true;
|
|
345
|
+
}
|
|
346
|
+
return response;
|
|
347
|
+
}
|
|
348
|
+
export function buildCodexMcpServerElicitationDeclineResponse() {
|
|
349
|
+
return {
|
|
350
|
+
action: "decline",
|
|
351
|
+
content: null,
|
|
352
|
+
_meta: null,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
export function buildCodexDynamicToolCallFailureResponse() {
|
|
356
|
+
return {
|
|
357
|
+
contentItems: [
|
|
358
|
+
{
|
|
359
|
+
type: "inputText",
|
|
360
|
+
text: "Dynamic tool calls are not supported by the WeChat bridge.",
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
success: false,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
157
366
|
export function buildCodexUserInputRequest(params) {
|
|
158
367
|
if (!isRecord(params) || !Array.isArray(params.questions)) {
|
|
159
368
|
return null;
|
|
@@ -287,6 +496,7 @@ export function shouldRecoverCodexStaleBusyState(params) {
|
|
|
287
496
|
!params.pendingTurnStart &&
|
|
288
497
|
!params.hasActiveTurn &&
|
|
289
498
|
!params.hasPendingApproval &&
|
|
499
|
+
!params.hasPendingUserInput &&
|
|
290
500
|
!params.activeTurnId);
|
|
291
501
|
}
|
|
292
502
|
export function shouldAutoCompleteCodexWechatTurnAfterFinalReply(params) {
|
|
@@ -295,6 +505,7 @@ export function shouldAutoCompleteCodexWechatTurnAfterFinalReply(params) {
|
|
|
295
505
|
params.activeTurnOrigin === "wechat" &&
|
|
296
506
|
!params.pendingTurnStart &&
|
|
297
507
|
!params.hasPendingApproval &&
|
|
508
|
+
!params.hasPendingUserInput &&
|
|
298
509
|
params.hasFinalOutput &&
|
|
299
510
|
!params.hasCompletedTurn &&
|
|
300
511
|
typeof params.lastActivityAtMs === "number" &&
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
const PEER_BRIDGE_EXIT_TIMEOUT_MS = 4_000;
|
|
3
4
|
const PEER_BRIDGE_EXIT_POLL_MS = 100;
|
|
4
5
|
function isPidAlive(pid) {
|
|
@@ -24,7 +25,30 @@ function isRecord(value) {
|
|
|
24
25
|
}
|
|
25
26
|
export function isWechatBridgeCommandLine(commandLine) {
|
|
26
27
|
return (/wechat-bridge\.(?:ts|js|mjs)/i.test(commandLine) &&
|
|
27
|
-
/(?:^|\s)--adapter(
|
|
28
|
+
/(?:^|\s|["'])--adapter(?:(?:["']?\s)|=)/i.test(commandLine));
|
|
29
|
+
}
|
|
30
|
+
export function isWechatDaemonCommandLine(commandLine) {
|
|
31
|
+
return /wechat-daemon\.(?:ts|js|mjs)/i.test(commandLine);
|
|
32
|
+
}
|
|
33
|
+
function normalizeComparablePath(filePath) {
|
|
34
|
+
const normalized = path.resolve(filePath);
|
|
35
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
36
|
+
}
|
|
37
|
+
function extractCommandLineOption(commandLine, optionName) {
|
|
38
|
+
const escaped = optionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
39
|
+
const pattern = new RegExp(`(?:^|\\s|["'])--${escaped}(?:(?:["']?\\s+)|=)(?:"([^"]+)"|'([^']+)'|(\\S+))`, "i");
|
|
40
|
+
const match = pattern.exec(commandLine);
|
|
41
|
+
return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
|
|
42
|
+
}
|
|
43
|
+
export function isWechatDaemonCommandLineForCwd(commandLine, cwd) {
|
|
44
|
+
if (!isWechatDaemonCommandLine(commandLine)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const commandCwd = extractCommandLineOption(commandLine, "cwd");
|
|
48
|
+
if (!commandCwd) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return normalizeComparablePath(commandCwd) === normalizeComparablePath(cwd);
|
|
28
52
|
}
|
|
29
53
|
/**
|
|
30
54
|
* Detects `opencode serve` command lines spawned by the bridge adapter.
|
|
@@ -165,6 +189,21 @@ export function listPeerBridgeProcesses(currentPid = process.pid) {
|
|
|
165
189
|
? listWindowsBridgeProcesses(currentPid)
|
|
166
190
|
: listPosixBridgeProcesses(currentPid);
|
|
167
191
|
}
|
|
192
|
+
export function getProcessRecordByPid(pid, currentPid = process.pid) {
|
|
193
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === currentPid) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return listAllProcessesRaw(currentPid).find((record) => record.pid === pid) ?? null;
|
|
197
|
+
}
|
|
198
|
+
export function listWechatDaemonProcesses(params = {}) {
|
|
199
|
+
const currentPid = params.currentPid ?? process.pid;
|
|
200
|
+
const excludePids = new Set(params.excludePids ?? []);
|
|
201
|
+
return listAllProcessesRaw(currentPid)
|
|
202
|
+
.filter((record) => !excludePids.has(record.pid))
|
|
203
|
+
.filter((record) => params.cwd
|
|
204
|
+
? isWechatDaemonCommandLineForCwd(record.commandLine, params.cwd)
|
|
205
|
+
: isWechatDaemonCommandLine(record.commandLine));
|
|
206
|
+
}
|
|
168
207
|
/**
|
|
169
208
|
* List all managed OpenCode child processes (`serve` and `attach`) whose parent
|
|
170
209
|
* PID is no longer alive. These are orphaned processes left behind when a bridge
|
|
@@ -169,14 +169,17 @@ export class BridgeStateStore {
|
|
|
169
169
|
lifecycle: options.lifecycle,
|
|
170
170
|
};
|
|
171
171
|
this.acquireLock();
|
|
172
|
+
const shouldRestoreSessionState = options.sessionStartMode !== "new";
|
|
172
173
|
const persisted = this.readStateFile();
|
|
173
|
-
const persistedSharedSessionId = resolveRestorableSharedSessionId(persisted, options);
|
|
174
|
-
const persistedResumeConversationId =
|
|
174
|
+
const persistedSharedSessionId = resolveRestorableSharedSessionId(shouldRestoreSessionState ? persisted : null, options);
|
|
175
|
+
const persistedResumeConversationId = shouldRestoreSessionState &&
|
|
176
|
+
options.adapter === "claude" &&
|
|
175
177
|
persisted?.cwd === options.cwd &&
|
|
176
178
|
typeof persisted.resumeConversationId === "string"
|
|
177
179
|
? persisted.resumeConversationId
|
|
178
180
|
: undefined;
|
|
179
|
-
const persistedTranscriptPath =
|
|
181
|
+
const persistedTranscriptPath = shouldRestoreSessionState &&
|
|
182
|
+
options.adapter === "claude" &&
|
|
180
183
|
persisted?.cwd === options.cwd &&
|
|
181
184
|
typeof persisted.transcriptPath === "string"
|
|
182
185
|
? persisted.transcriptPath
|