@unlinearity/cli-wechat-bridge 1.0.4 → 1.0.6
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 +136 -25
- package/bin/wechat-daemon.mjs +5 -0
- package/dist/bridge/bridge-adapters.claude.js +33 -3
- package/dist/bridge/bridge-adapters.codex.js +216 -6
- package/dist/bridge/bridge-adapters.core.js +46 -6
- package/dist/bridge/bridge-adapters.opencode.js +79 -13
- package/dist/bridge/bridge-adapters.shared.js +215 -1
- package/dist/bridge/bridge-controller.js +6 -2
- package/dist/bridge/bridge-utils.js +92 -9
- package/dist/bridge/claude-hooks.js +64 -3
- package/dist/bridge/wechat-bridge.js +102 -8
- package/dist/companion/codex-remote-client.js +8 -4
- package/dist/companion/local-companion-link.js +56 -26
- package/dist/companion/local-companion-start.js +48 -5
- package/dist/companion/local-companion.js +3 -1
- package/dist/daemon/daemon-link.js +175 -0
- package/dist/daemon/wechat-daemon.js +1202 -0
- package/dist/wechat/channel-config.js +126 -33
- package/dist/wechat/standalone-bot.js +5 -6
- package/dist/wechat/wechat-channel.js +21 -10
- package/dist/wechat/wechat-transport.js +291 -9
- package/package.json +62 -98
|
@@ -1,7 +1,7 @@
|
|
|
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, containsWechatOutboundAttachmentPath, 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;
|
|
@@ -801,6 +801,11 @@ export class OpenCodeServerAdapter {
|
|
|
801
801
|
return;
|
|
802
802
|
}
|
|
803
803
|
this.clearWechatWorkingNotice();
|
|
804
|
+
const denyMessage = this.getWechatOutboundAttachmentPermissionDenyMessage(properties, pendingPermission);
|
|
805
|
+
if (denyMessage) {
|
|
806
|
+
this.rejectPermission(pendingPermission, denyMessage);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
804
809
|
const approval = this.toPendingApproval(pendingPermission);
|
|
805
810
|
this.pendingPermission = pendingPermission;
|
|
806
811
|
this.state.pendingApproval = approval;
|
|
@@ -817,6 +822,57 @@ export class OpenCodeServerAdapter {
|
|
|
817
822
|
this.state.pendingApproval = null;
|
|
818
823
|
this.state.pendingApprovalOrigin = undefined;
|
|
819
824
|
}
|
|
825
|
+
getWechatOutboundAttachmentPermissionDenyMessage(properties, pendingPermission) {
|
|
826
|
+
const metadata = isRecord(properties.metadata) ? properties.metadata : {};
|
|
827
|
+
const command = this.extractPermissionCommand(properties, metadata);
|
|
828
|
+
if (isWechatOutboundAttachmentWriteCommand(command)) {
|
|
829
|
+
return WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
|
|
830
|
+
}
|
|
831
|
+
const targetPath = [
|
|
832
|
+
command,
|
|
833
|
+
typeof metadata.path === "string" ? metadata.path : "",
|
|
834
|
+
typeof metadata.file === "string" ? metadata.file : "",
|
|
835
|
+
typeof metadata.filePath === "string" ? metadata.filePath : "",
|
|
836
|
+
typeof metadata.filepath === "string" ? metadata.filepath : "",
|
|
837
|
+
typeof metadata.parentDir === "string" ? metadata.parentDir : "",
|
|
838
|
+
typeof metadata.target === "string" ? metadata.target : "",
|
|
839
|
+
typeof metadata.detail === "string" ? metadata.detail : "",
|
|
840
|
+
Array.isArray(properties.patterns)
|
|
841
|
+
? properties.patterns.filter((value) => typeof value === "string").join(" ")
|
|
842
|
+
: "",
|
|
843
|
+
typeof properties.pattern === "string" ? properties.pattern : "",
|
|
844
|
+
].join(" ");
|
|
845
|
+
return isWechatOutboundAttachmentMutationTool(pendingPermission.request.toolName, targetPath)
|
|
846
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
847
|
+
: null;
|
|
848
|
+
}
|
|
849
|
+
rejectPermission(pendingPermission, denyMessage) {
|
|
850
|
+
const client = this.client;
|
|
851
|
+
if (!client) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
void (async () => {
|
|
855
|
+
try {
|
|
856
|
+
const result = await client.permission.respond({
|
|
857
|
+
sessionID: pendingPermission.sessionId,
|
|
858
|
+
permissionID: pendingPermission.permissionId,
|
|
859
|
+
directory: this.options.cwd,
|
|
860
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
861
|
+
response: "reject",
|
|
862
|
+
});
|
|
863
|
+
if (result.error !== undefined) {
|
|
864
|
+
throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (err) {
|
|
868
|
+
this.emit({
|
|
869
|
+
type: "stderr",
|
|
870
|
+
text: `${denyMessage}\nFailed to reject permission: ${describeUnknownError(err)}`,
|
|
871
|
+
timestamp: nowIso(),
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
})();
|
|
875
|
+
}
|
|
820
876
|
toPendingApproval(pendingPermission) {
|
|
821
877
|
return {
|
|
822
878
|
...pendingPermission.request,
|
|
@@ -834,24 +890,14 @@ export class OpenCodeServerAdapter {
|
|
|
834
890
|
if (!sessionId || !permissionId) {
|
|
835
891
|
return null;
|
|
836
892
|
}
|
|
837
|
-
const toolName =
|
|
838
|
-
? properties.type
|
|
839
|
-
: typeof properties.permission === "string"
|
|
840
|
-
? properties.permission
|
|
841
|
-
: undefined;
|
|
893
|
+
const toolName = this.extractPermissionToolName(properties);
|
|
842
894
|
const title = typeof properties.title === "string"
|
|
843
895
|
? properties.title
|
|
844
896
|
: typeof properties.permission === "string"
|
|
845
897
|
? `Permission request: ${properties.permission}`
|
|
846
898
|
: undefined;
|
|
847
899
|
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;
|
|
900
|
+
const command = this.extractPermissionCommand(properties, metadata);
|
|
855
901
|
return {
|
|
856
902
|
sessionId,
|
|
857
903
|
permissionId,
|
|
@@ -869,6 +915,26 @@ export class OpenCodeServerAdapter {
|
|
|
869
915
|
},
|
|
870
916
|
};
|
|
871
917
|
}
|
|
918
|
+
extractPermissionToolName(properties) {
|
|
919
|
+
return typeof properties.type === "string"
|
|
920
|
+
? properties.type
|
|
921
|
+
: typeof properties.permission === "string"
|
|
922
|
+
? properties.permission
|
|
923
|
+
: undefined;
|
|
924
|
+
}
|
|
925
|
+
extractPermissionCommand(properties, metadata) {
|
|
926
|
+
if (typeof metadata.command === "string") {
|
|
927
|
+
return metadata.command;
|
|
928
|
+
}
|
|
929
|
+
if (typeof metadata.detail === "string") {
|
|
930
|
+
return metadata.detail;
|
|
931
|
+
}
|
|
932
|
+
if (Array.isArray(properties.patterns)) {
|
|
933
|
+
const patterns = properties.patterns.filter((value) => typeof value === "string");
|
|
934
|
+
return patterns.length > 0 ? patterns.join(", ") : undefined;
|
|
935
|
+
}
|
|
936
|
+
return undefined;
|
|
937
|
+
}
|
|
872
938
|
handleSessionCreated(properties) {
|
|
873
939
|
if (!isRecord(properties)) {
|
|
874
940
|
return;
|
|
@@ -7,7 +7,7 @@ import { spawn as spawnPty } from "node-pty";
|
|
|
7
7
|
import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, sendLocalCompanionMessage, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
8
8
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
9
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";
|
|
10
|
+
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, detectCliApproval, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
11
11
|
import { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
|
|
12
12
|
export { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
|
|
13
13
|
export const DEFAULT_COLS = 120;
|
|
@@ -152,8 +152,220 @@ export function buildCodexApprovalRequest(method, params) {
|
|
|
152
152
|
commandPreview: truncatePreview(preview, 180),
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
+
if (method === "item/permissions/requestApproval") {
|
|
156
|
+
const reason = typeof params.reason === "string" ? params.reason : "";
|
|
157
|
+
const preview = summarizeCodexPermissionsRequest(params.permissions);
|
|
158
|
+
return {
|
|
159
|
+
source: "cli",
|
|
160
|
+
summary: reason
|
|
161
|
+
? `Codex needs approval before granting extra permissions: ${truncatePreview(reason, 160)}`
|
|
162
|
+
: "Codex needs approval before granting extra permissions.",
|
|
163
|
+
commandPreview: truncatePreview(preview, 180),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
export function getCodexWechatOutboundAttachmentDenyMessage(method, params) {
|
|
169
|
+
if (!isRecord(params)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (method === "item/commandExecution/requestApproval") {
|
|
173
|
+
return isWechatOutboundAttachmentWriteCommand(params.command) ||
|
|
174
|
+
containsWechatOutboundAttachmentPathDeep(params.additionalPermissions)
|
|
175
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
176
|
+
: null;
|
|
177
|
+
}
|
|
178
|
+
if (method === "item/fileChange/requestApproval") {
|
|
179
|
+
return containsWechatOutboundAttachmentPath(params.grantRoot)
|
|
180
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
181
|
+
: null;
|
|
182
|
+
}
|
|
183
|
+
if (method === "item/permissions/requestApproval") {
|
|
184
|
+
return containsWechatOutboundAttachmentPathDeep(params.permissions)
|
|
185
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
186
|
+
: null;
|
|
187
|
+
}
|
|
155
188
|
return null;
|
|
156
189
|
}
|
|
190
|
+
function commandApprovalAllowsAccept(availableDecisions) {
|
|
191
|
+
if (!Array.isArray(availableDecisions)) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return availableDecisions.some((decision) => decision === "accept");
|
|
195
|
+
}
|
|
196
|
+
function normalizePermissionPath(pathValue) {
|
|
197
|
+
const normalized = pathValue.trim().replace(/\\/g, "/").toLowerCase();
|
|
198
|
+
return normalized === "/" ? normalized : normalized.replace(/\/+$/g, "");
|
|
199
|
+
}
|
|
200
|
+
function isHighRiskPermissionPath(pathValue) {
|
|
201
|
+
const normalized = normalizePermissionPath(pathValue);
|
|
202
|
+
if (!normalized) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (normalized === "/" || /^[a-z]:$/i.test(normalized)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return /^[a-z]:(?:\/windows|\/program files(?: \(x86\))?|\/programdata)(?:\/|$)/i.test(normalized);
|
|
209
|
+
}
|
|
210
|
+
function collectPermissionPaths(value, output) {
|
|
211
|
+
if (typeof value === "string") {
|
|
212
|
+
output.push(value);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
for (const item of value) {
|
|
217
|
+
collectPermissionPaths(item, output);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!isRecord(value)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (typeof value.path === "string") {
|
|
225
|
+
output.push(value.path);
|
|
226
|
+
}
|
|
227
|
+
if (typeof value.pattern === "string") {
|
|
228
|
+
output.push(value.pattern);
|
|
229
|
+
}
|
|
230
|
+
for (const item of Object.values(value)) {
|
|
231
|
+
collectPermissionPaths(item, output);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function hasRootSpecialPermissionPath(value) {
|
|
235
|
+
if (Array.isArray(value)) {
|
|
236
|
+
return value.some(hasRootSpecialPermissionPath);
|
|
237
|
+
}
|
|
238
|
+
if (!isRecord(value)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if ((value.kind === "root" || value.kind === "Root") ||
|
|
242
|
+
(value.value === "root" || value.value === "Root")) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return Object.values(value).some(hasRootSpecialPermissionPath);
|
|
246
|
+
}
|
|
247
|
+
function containsHighRiskPermissionTarget(value) {
|
|
248
|
+
if (hasRootSpecialPermissionPath(value)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
const paths = [];
|
|
252
|
+
collectPermissionPaths(value, paths);
|
|
253
|
+
return paths.some(isHighRiskPermissionPath);
|
|
254
|
+
}
|
|
255
|
+
export function getCodexApprovalAutoResponse(method, params) {
|
|
256
|
+
if (method === "item/commandExecution/requestApproval") {
|
|
257
|
+
const command = typeof params.command === "string" ? params.command : "";
|
|
258
|
+
if (!commandApprovalAllowsAccept(params.availableDecisions)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (command && isHighRiskShellCommand(command)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
if (containsHighRiskPermissionTarget(params.additionalPermissions)) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
result: { decision: "accept" },
|
|
269
|
+
reason: command
|
|
270
|
+
? `low-risk command ${truncatePreview(command, 120)}`
|
|
271
|
+
: "low-risk command approval",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (method === "item/fileChange/requestApproval") {
|
|
275
|
+
if (containsHighRiskPermissionTarget(params.grantRoot)) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
result: { decision: "accept" },
|
|
280
|
+
reason: "low-risk file change approval",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (method === "item/permissions/requestApproval") {
|
|
284
|
+
if (containsHighRiskPermissionTarget(params.permissions)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
result: buildCodexPermissionsRequestApprovalResponse(params, "confirm", {
|
|
289
|
+
strictAutoReview: true,
|
|
290
|
+
}),
|
|
291
|
+
reason: "low-risk permission grant",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
function collectStringValues(value) {
|
|
297
|
+
return Array.isArray(value)
|
|
298
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
299
|
+
: [];
|
|
300
|
+
}
|
|
301
|
+
function summarizeCodexPermissionsRequest(permissions) {
|
|
302
|
+
if (!isRecord(permissions)) {
|
|
303
|
+
return "Additional permissions requested.";
|
|
304
|
+
}
|
|
305
|
+
const parts = [];
|
|
306
|
+
if (isRecord(permissions.network) && permissions.network.enabled === true) {
|
|
307
|
+
parts.push("network access");
|
|
308
|
+
}
|
|
309
|
+
if (isRecord(permissions.fileSystem)) {
|
|
310
|
+
const readPaths = collectStringValues(permissions.fileSystem.read);
|
|
311
|
+
const writePaths = collectStringValues(permissions.fileSystem.write);
|
|
312
|
+
if (readPaths.length > 0) {
|
|
313
|
+
parts.push(`read: ${readPaths.join(", ")}`);
|
|
314
|
+
}
|
|
315
|
+
if (writePaths.length > 0) {
|
|
316
|
+
parts.push(`write: ${writePaths.join(", ")}`);
|
|
317
|
+
}
|
|
318
|
+
if (Array.isArray(permissions.fileSystem.entries) && permissions.fileSystem.entries.length > 0) {
|
|
319
|
+
parts.push(`filesystem entries: ${permissions.fileSystem.entries.length}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return parts.length > 0 ? parts.join("; ") : "Additional permissions requested.";
|
|
323
|
+
}
|
|
324
|
+
function clonePermissionObject(value) {
|
|
325
|
+
if (!isRecord(value)) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return JSON.parse(JSON.stringify(value));
|
|
329
|
+
}
|
|
330
|
+
export function buildCodexPermissionsRequestApprovalResponse(params, action = "deny", options = {}) {
|
|
331
|
+
const permissions = {};
|
|
332
|
+
if (action === "confirm" && isRecord(params) && isRecord(params.permissions)) {
|
|
333
|
+
const network = clonePermissionObject(params.permissions.network);
|
|
334
|
+
const fileSystem = clonePermissionObject(params.permissions.fileSystem);
|
|
335
|
+
if (network) {
|
|
336
|
+
permissions.network = network;
|
|
337
|
+
}
|
|
338
|
+
if (fileSystem) {
|
|
339
|
+
permissions.fileSystem = fileSystem;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const response = {
|
|
343
|
+
permissions,
|
|
344
|
+
scope: "turn",
|
|
345
|
+
};
|
|
346
|
+
if (options.strictAutoReview) {
|
|
347
|
+
response.strictAutoReview = true;
|
|
348
|
+
}
|
|
349
|
+
return response;
|
|
350
|
+
}
|
|
351
|
+
export function buildCodexMcpServerElicitationDeclineResponse() {
|
|
352
|
+
return {
|
|
353
|
+
action: "decline",
|
|
354
|
+
content: null,
|
|
355
|
+
_meta: null,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
export function buildCodexDynamicToolCallFailureResponse() {
|
|
359
|
+
return {
|
|
360
|
+
contentItems: [
|
|
361
|
+
{
|
|
362
|
+
type: "inputText",
|
|
363
|
+
text: "Dynamic tool calls are not supported by the WeChat bridge.",
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
success: false,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
157
369
|
export function buildCodexUserInputRequest(params) {
|
|
158
370
|
if (!isRecord(params) || !Array.isArray(params.questions)) {
|
|
159
371
|
return null;
|
|
@@ -287,6 +499,7 @@ export function shouldRecoverCodexStaleBusyState(params) {
|
|
|
287
499
|
!params.pendingTurnStart &&
|
|
288
500
|
!params.hasActiveTurn &&
|
|
289
501
|
!params.hasPendingApproval &&
|
|
502
|
+
!params.hasPendingUserInput &&
|
|
290
503
|
!params.activeTurnId);
|
|
291
504
|
}
|
|
292
505
|
export function shouldAutoCompleteCodexWechatTurnAfterFinalReply(params) {
|
|
@@ -295,6 +508,7 @@ export function shouldAutoCompleteCodexWechatTurnAfterFinalReply(params) {
|
|
|
295
508
|
params.activeTurnOrigin === "wechat" &&
|
|
296
509
|
!params.pendingTurnStart &&
|
|
297
510
|
!params.hasPendingApproval &&
|
|
511
|
+
!params.hasPendingUserInput &&
|
|
298
512
|
params.hasFinalOutput &&
|
|
299
513
|
!params.hasCompletedTurn &&
|
|
300
514
|
typeof params.lastActivityAtMs === "number" &&
|
|
@@ -17,7 +17,9 @@ export class BridgeController {
|
|
|
17
17
|
this.clearLocalClientEndpoint();
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
const existing = readLocalCompanionEndpoint(this.cwd
|
|
20
|
+
const existing = readLocalCompanionEndpoint(this.cwd, {
|
|
21
|
+
adapter: endpoint.kind,
|
|
22
|
+
});
|
|
21
23
|
const adapterState = this.adapter.getState();
|
|
22
24
|
const nextEndpoint = existing?.instanceId === endpoint.instanceId
|
|
23
25
|
? {
|
|
@@ -38,7 +40,9 @@ export class BridgeController {
|
|
|
38
40
|
writeLocalCompanionEndpoint(nextEndpoint);
|
|
39
41
|
}
|
|
40
42
|
clearLocalClientEndpoint() {
|
|
41
|
-
clearLocalCompanionEndpoint(this.cwd, this.endpointInstanceId ?? undefined
|
|
43
|
+
clearLocalCompanionEndpoint(this.cwd, this.endpointInstanceId ?? undefined, {
|
|
44
|
+
adapter: this.adapter.getState().kind,
|
|
45
|
+
});
|
|
42
46
|
this.endpointInstanceId = null;
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -16,10 +16,12 @@ const WECHAT_ATTACHMENT_PROMPT_PREFIX = [
|
|
|
16
16
|
"[WeChat bridge note]",
|
|
17
17
|
"Your final reply will be forwarded back to a WeChat chat.",
|
|
18
18
|
"If the user asks you to send a local file or media to WeChat and you know the local path, do not say that you lack a WeChat sending tool.",
|
|
19
|
-
"For a real send request,
|
|
19
|
+
"For a real send request, locate the original local file path and reference that path directly in the attachment block.",
|
|
20
|
+
"Do not create, copy, move, or write files into ~/.claude/channels/wechat, ~/.cli-bridge, or any outbound-attachments directory.",
|
|
21
|
+
"Do not call Write, cp, copy, xcopy, robocopy, mv, move, or shell redirection to stage outbound files for WeChat.",
|
|
20
22
|
"Put any brief visible reply text first, then end the message with exactly one trailing block like:",
|
|
21
23
|
"```wechat-attachments",
|
|
22
|
-
"file C:\\Users\\name\\Desktop\\document.
|
|
24
|
+
"file C:\\Users\\name\\Desktop\\document.docx",
|
|
23
25
|
"```",
|
|
24
26
|
"Valid kinds: image, file, video, voice.",
|
|
25
27
|
"Use `file` for PDFs and ordinary documents. Only include files you truly intend to upload.",
|
|
@@ -27,6 +29,10 @@ const WECHAT_ATTACHMENT_PROMPT_PREFIX = [
|
|
|
27
29
|
"[User request]",
|
|
28
30
|
].join("\n");
|
|
29
31
|
const WECHAT_ATTACHMENT_BLOCK_RE = /\n```wechat-attachments[ \t]*\n([\s\S]*?)\n```[ \t]*$/;
|
|
32
|
+
export const WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE = "The WeChat bridge does not use outbound attachment directories. Do not create or copy files under .claude/channels/wechat/outbound-attachments or .cli-bridge/outbound-attachments. To send a file, put the original absolute local file path in the final ```wechat-attachments``` block.";
|
|
33
|
+
const WECHAT_OUTBOUND_ATTACHMENT_PATH_RE = /(?:^|\/)(?:(?:\.claude\/channels\/wechat\/|\.cli-bridge\/)?outbound-attachments)(?:\/|$)/i;
|
|
34
|
+
const WECHAT_OUTBOUND_ATTACHMENT_WRITE_COMMAND_RE = /\b(cp|copy|copy-item|xcopy|robocopy|mv|move|move-item|mkdir|md|new-item|ni|set-content|add-content|out-file|write-output|touch)\b|>\s*["']?[^&|]*outbound-attachments/i;
|
|
35
|
+
const WECHAT_OUTBOUND_ATTACHMENT_MUTATION_TOOL_RE = /^(?:write|edit|multiedit|notebookedit|patch|create|mkdir|move|copy|file[_-]?change|external_directory)$/i;
|
|
30
36
|
const WECHAT_ATTACHMENT_KINDS = ["image", "file", "video", "voice"];
|
|
31
37
|
const INLINE_IMAGE_EXTENSIONS = new Set([
|
|
32
38
|
".png",
|
|
@@ -211,15 +217,88 @@ export function shouldInjectWechatAttachmentPrompt(text) {
|
|
|
211
217
|
mentionsLocalPath ||
|
|
212
218
|
looksLikeShortSendCommand);
|
|
213
219
|
}
|
|
214
|
-
export function
|
|
215
|
-
if (
|
|
216
|
-
return
|
|
220
|
+
export function containsWechatOutboundAttachmentPath(value) {
|
|
221
|
+
if (typeof value !== "string") {
|
|
222
|
+
return false;
|
|
217
223
|
}
|
|
218
|
-
const normalized =
|
|
219
|
-
|
|
220
|
-
|
|
224
|
+
const normalized = value.trim().replace(/\\/g, "/");
|
|
225
|
+
return WECHAT_OUTBOUND_ATTACHMENT_PATH_RE.test(normalized);
|
|
226
|
+
}
|
|
227
|
+
export function containsWechatOutboundAttachmentPathDeep(value) {
|
|
228
|
+
const seen = new WeakSet();
|
|
229
|
+
const visit = (candidate) => {
|
|
230
|
+
if (containsWechatOutboundAttachmentPath(candidate)) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (!candidate || typeof candidate !== "object") {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (seen.has(candidate)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
seen.add(candidate);
|
|
240
|
+
if (Array.isArray(candidate)) {
|
|
241
|
+
return candidate.some((item) => visit(item));
|
|
242
|
+
}
|
|
243
|
+
return Object.values(candidate).some((item) => visit(item));
|
|
244
|
+
};
|
|
245
|
+
return visit(value);
|
|
246
|
+
}
|
|
247
|
+
export function isWechatOutboundAttachmentWriteCommand(command) {
|
|
248
|
+
return (typeof command === "string" &&
|
|
249
|
+
containsWechatOutboundAttachmentPath(command) &&
|
|
250
|
+
WECHAT_OUTBOUND_ATTACHMENT_WRITE_COMMAND_RE.test(command));
|
|
251
|
+
}
|
|
252
|
+
export function isWechatOutboundAttachmentMutationTool(toolName, targetPath) {
|
|
253
|
+
return (typeof toolName === "string" &&
|
|
254
|
+
WECHAT_OUTBOUND_ATTACHMENT_MUTATION_TOOL_RE.test(toolName.trim()) &&
|
|
255
|
+
containsWechatOutboundAttachmentPath(targetPath));
|
|
256
|
+
}
|
|
257
|
+
function formatPromptByteSize(bytes) {
|
|
258
|
+
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (bytes >= 1024 * 1024) {
|
|
262
|
+
return `${(bytes / (1024 * 1024)).toFixed(bytes >= 100 * 1024 * 1024 ? 0 : 1)} MB`;
|
|
263
|
+
}
|
|
264
|
+
if (bytes >= 1024) {
|
|
265
|
+
return `${(bytes / 1024).toFixed(bytes >= 100 * 1024 ? 0 : 1)} KB`;
|
|
266
|
+
}
|
|
267
|
+
return `${bytes} B`;
|
|
268
|
+
}
|
|
269
|
+
function formatWechatInboundAttachmentPrompt(attachments) {
|
|
270
|
+
const lines = [
|
|
271
|
+
"[WeChat inbound attachments]",
|
|
272
|
+
"The user sent attachment files through WeChat. They have been saved locally. Read the local paths directly if the user's request requires inspecting them.",
|
|
273
|
+
];
|
|
274
|
+
attachments.forEach((attachment, index) => {
|
|
275
|
+
const sizeText = formatPromptByteSize(attachment.sizeBytes);
|
|
276
|
+
const metadata = [
|
|
277
|
+
`kind=${attachment.kind}`,
|
|
278
|
+
attachment.fileName ? `name=${attachment.fileName}` : "",
|
|
279
|
+
sizeText ? `size=${sizeText}` : "",
|
|
280
|
+
].filter(Boolean);
|
|
281
|
+
lines.push(`${index + 1}. ${metadata.join(" ")} path=${attachment.path}`);
|
|
282
|
+
});
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
export function buildWechatInboundPrompt(text, attachments = []) {
|
|
286
|
+
const trimmedAttachments = attachments.filter((attachment) => attachment.path.trim());
|
|
287
|
+
if (!trimmedAttachments.length) {
|
|
288
|
+
if (!shouldInjectWechatAttachmentPrompt(text)) {
|
|
289
|
+
return text;
|
|
290
|
+
}
|
|
291
|
+
const normalized = normalizeOutput(text).trim();
|
|
292
|
+
if (!normalized) {
|
|
293
|
+
return text;
|
|
294
|
+
}
|
|
295
|
+
return `${WECHAT_ATTACHMENT_PROMPT_PREFIX}\n${normalized}`;
|
|
221
296
|
}
|
|
222
|
-
|
|
297
|
+
const baseText = normalizeOutput(text).trim() || "Received WeChat attachment(s).";
|
|
298
|
+
const userPrompt = shouldInjectWechatAttachmentPrompt(baseText)
|
|
299
|
+
? `${WECHAT_ATTACHMENT_PROMPT_PREFIX}\n${baseText.trim()}`
|
|
300
|
+
: baseText;
|
|
301
|
+
return `${userPrompt.trim()}\n\n${formatWechatInboundAttachmentPrompt(trimmedAttachments)}`;
|
|
223
302
|
}
|
|
224
303
|
export function parseWechatFinalReply(text) {
|
|
225
304
|
const normalized = normalizeOutput(text);
|
|
@@ -320,7 +399,11 @@ const HIGH_RISK_PATTERNS = [
|
|
|
320
399
|
/\b(?:invoke-expression|iex)\b/i,
|
|
321
400
|
/\bcurl\b.*\|\s*(?:iex|powershell)\b/i,
|
|
322
401
|
/\binvoke-webrequest\b.*\|\s*(?:iex|powershell)\b/i,
|
|
402
|
+
/\brm\b\s+/i,
|
|
323
403
|
/\brm\b\s+-[A-Za-z-]*r[A-Za-z-]*/i,
|
|
404
|
+
/\bfind\b[^\r\n]*\s-delete\b/i,
|
|
405
|
+
/\bfind\b[^\r\n]*\s-exec\s+(?:rm|rmdir|del|erase|remove-item)\b/i,
|
|
406
|
+
/\bxargs\b[^\r\n]*\b(?:rm|rmdir|del|erase|remove-item)\b/i,
|
|
324
407
|
/\bsudo\b/i,
|
|
325
408
|
/\bmkfs(?:\.\w+)?\b/i,
|
|
326
409
|
/\bdd\b/i,
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { normalizeOutput, truncatePreview } from "./bridge-utils.js";
|
|
1
|
+
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, isHighRiskShellCommand, isWechatOutboundAttachmentWriteCommand, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
|
|
2
|
+
export const CLAUDE_WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE = WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
|
|
3
|
+
const FILE_MUTATION_TOOL_NAMES = new Set([
|
|
4
|
+
"Write",
|
|
5
|
+
"Edit",
|
|
6
|
+
"MultiEdit",
|
|
7
|
+
"NotebookEdit",
|
|
8
|
+
]);
|
|
9
|
+
const LOW_RISK_READ_TOOL_NAMES = new Set([
|
|
10
|
+
"Glob",
|
|
11
|
+
"Grep",
|
|
12
|
+
"LS",
|
|
13
|
+
"NotebookRead",
|
|
14
|
+
"Read",
|
|
15
|
+
"TodoRead",
|
|
16
|
+
]);
|
|
2
17
|
function quoteWindowsCommandArg(value) {
|
|
3
18
|
return `"${value.replace(/"/g, '""')}"`;
|
|
4
19
|
}
|
|
@@ -166,14 +181,60 @@ export function buildClaudePermissionApprovalRequest(payload) {
|
|
|
166
181
|
detailPreview,
|
|
167
182
|
};
|
|
168
183
|
}
|
|
169
|
-
|
|
184
|
+
function getClaudeToolInputString(payload, key) {
|
|
185
|
+
const value = payload.tool_input?.[key];
|
|
186
|
+
return typeof value === "string" ? value : "";
|
|
187
|
+
}
|
|
188
|
+
export function getClaudeWechatOutboundAttachmentDenyMessage(payload) {
|
|
189
|
+
const toolName = typeof payload.tool_name === "string" ? payload.tool_name.trim() : "";
|
|
190
|
+
if (!toolName) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (FILE_MUTATION_TOOL_NAMES.has(toolName)) {
|
|
194
|
+
const filePath = getClaudeToolInputString(payload, "file_path");
|
|
195
|
+
return containsWechatOutboundAttachmentPath(filePath)
|
|
196
|
+
? CLAUDE_WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
197
|
+
: null;
|
|
198
|
+
}
|
|
199
|
+
if (toolName === "Bash") {
|
|
200
|
+
const command = getClaudeToolInputString(payload, "command");
|
|
201
|
+
if (isWechatOutboundAttachmentWriteCommand(command)) {
|
|
202
|
+
return CLAUDE_WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
export function getClaudePermissionAutoResponse(payload) {
|
|
208
|
+
const toolName = typeof payload.tool_name === "string" ? payload.tool_name.trim() : "";
|
|
209
|
+
if (!toolName) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (LOW_RISK_READ_TOOL_NAMES.has(toolName)) {
|
|
213
|
+
return {
|
|
214
|
+
action: "confirm",
|
|
215
|
+
reason: `low-risk ${toolName} permission`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (toolName !== "Bash") {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const command = getClaudeToolInputString(payload, "command").trim();
|
|
222
|
+
if (!command || isHighRiskShellCommand(command)) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
action: "confirm",
|
|
227
|
+
reason: `low-risk command ${truncatePreview(command, 120)}`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function buildClaudePermissionDecisionHookOutput(action, denyMessage = "Permission denied from WeChat bridge.") {
|
|
170
231
|
const decision = action === "confirm"
|
|
171
232
|
? {
|
|
172
233
|
behavior: "allow",
|
|
173
234
|
}
|
|
174
235
|
: {
|
|
175
236
|
behavior: "deny",
|
|
176
|
-
message:
|
|
237
|
+
message: denyMessage,
|
|
177
238
|
interrupt: false,
|
|
178
239
|
};
|
|
179
240
|
return JSON.stringify({
|