@unlinearity/cli-wechat-bridge 1.0.5 → 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.
@@ -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 = typeof properties.type === "string"
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 = typeof metadata.command === "string"
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" &&
@@ -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, prefer locating and sending the file directly instead of opening or reading it unless the user explicitly asked for that.",
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.pdf",
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,6 +217,43 @@ export function shouldInjectWechatAttachmentPrompt(text) {
211
217
  mentionsLocalPath ||
212
218
  looksLikeShortSendCommand);
213
219
  }
220
+ export function containsWechatOutboundAttachmentPath(value) {
221
+ if (typeof value !== "string") {
222
+ return false;
223
+ }
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
+ }
214
257
  function formatPromptByteSize(bytes) {
215
258
  if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) {
216
259
  return null;
@@ -356,7 +399,11 @@ const HIGH_RISK_PATTERNS = [
356
399
  /\b(?:invoke-expression|iex)\b/i,
357
400
  /\bcurl\b.*\|\s*(?:iex|powershell)\b/i,
358
401
  /\binvoke-webrequest\b.*\|\s*(?:iex|powershell)\b/i,
402
+ /\brm\b\s+/i,
359
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,
360
407
  /\bsudo\b/i,
361
408
  /\bmkfs(?:\.\w+)?\b/i,
362
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
- export function buildClaudePermissionDecisionHookOutput(action) {
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: "Permission denied from WeChat bridge.",
237
+ message: denyMessage,
177
238
  interrupt: false,
178
239
  };
179
240
  return JSON.stringify({