linkshell-cli 0.2.83 → 0.2.85

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.
@@ -71,14 +71,71 @@ interface TerminalInstance {
71
71
  interface PendingPermission {
72
72
  terminalId: string;
73
73
  timeout: ReturnType<typeof setTimeout>;
74
- resolve: (decision: "allow" | "deny") => void;
74
+ permissionSuggestions: unknown[];
75
+ resolve: (decision: HookPermissionDecision) => void;
75
76
  }
76
77
 
78
+ interface HookPermissionDecision {
79
+ behavior: "allow" | "deny";
80
+ updatedPermissions?: unknown[];
81
+ message?: string;
82
+ interrupt?: boolean;
83
+ }
84
+
85
+ type HookPermissionChoice =
86
+ | "allow"
87
+ | "deny"
88
+ | {
89
+ outcome: "allow" | "deny" | "cancelled";
90
+ optionId?: string;
91
+ };
92
+
77
93
  function isLinkShellHookEntry(entry: unknown, marker: string): boolean {
78
94
  const raw = JSON.stringify(entry);
79
95
  return raw.includes(`/hook?m=${marker}`);
80
96
  }
81
97
 
98
+ function stringifyHookInput(value: unknown): string {
99
+ if (typeof value === "string") return value.slice(0, 1200);
100
+ if (typeof value === "object" && value) {
101
+ try {
102
+ return JSON.stringify(value, null, 2).slice(0, 1200);
103
+ } catch {
104
+ return String(value).slice(0, 1200);
105
+ }
106
+ }
107
+ return "";
108
+ }
109
+
110
+ function hookPermissionSuggestions(event: Record<string, unknown>): unknown[] {
111
+ if (isCodexPermissionRequest(event)) return [];
112
+ const snake = event.permission_suggestions;
113
+ const camel = event.permissionSuggestions;
114
+ if (Array.isArray(snake)) return snake;
115
+ if (Array.isArray(camel)) return camel;
116
+ return [];
117
+ }
118
+
119
+ function isCodexPermissionRequest(event: Record<string, unknown>): boolean {
120
+ if (typeof event.turn_id === "string" || typeof event.turnId === "string") return true;
121
+ const transcriptPath = event.transcript_path ?? event.transcriptPath;
122
+ return typeof transcriptPath === "string" && transcriptPath.includes("/.codex/");
123
+ }
124
+
125
+ function hookPermissionOptions(suggestions: unknown[]): Array<{
126
+ id: string;
127
+ label: string;
128
+ kind: "allow" | "deny" | "other";
129
+ }> {
130
+ return [
131
+ { id: "deny", label: "拒绝", kind: "deny" },
132
+ { id: "allow_once", label: "允许一次", kind: "allow" },
133
+ ...(suggestions.length > 0
134
+ ? [{ id: "allow_always" as const, label: "始终允许", kind: "allow" as const }]
135
+ : []),
136
+ ];
137
+ }
138
+
82
139
  function getPairingGatewayParam(gatewayHttpUrl: string): string | undefined {
83
140
  try {
84
141
  const url = new URL(gatewayHttpUrl);
@@ -593,9 +650,11 @@ export class BridgeSession {
593
650
  }
594
651
  case "agent.permission.response": {
595
652
  const p = parseTypedPayload("agent.permission.response", envelope.payload);
596
- const decision = p.outcome === "allow" ? "allow" : "deny";
597
- if (this.resolvePendingPermission(p.requestId, decision)) {
598
- this.log(`agent permission response for hook ${p.requestId}: ${decision}`);
653
+ if (this.resolvePendingPermission(p.requestId, {
654
+ outcome: p.outcome,
655
+ optionId: p.optionId,
656
+ })) {
657
+ this.log(`agent permission response for hook ${p.requestId}: ${p.outcome}:${p.optionId ?? "default"}`);
599
658
  break;
600
659
  }
601
660
  if (!this.agentSession) {
@@ -1073,6 +1132,7 @@ export class BridgeSession {
1073
1132
  // PermissionRequest: hold connection, wait for user decision from mobile app
1074
1133
  if (hookName === "PermissionRequest") {
1075
1134
  const requestId = `pr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1135
+ const permissionSuggestions = hookPermissionSuggestions(event);
1076
1136
  const timeout = setTimeout(() => {
1077
1137
  if (this.resolvePendingPermission(requestId, "deny")) {
1078
1138
  this.log(`permission request ${requestId} timed out`);
@@ -1082,12 +1142,13 @@ export class BridgeSession {
1082
1142
  this.pendingPermissions.set(requestId, {
1083
1143
  terminalId,
1084
1144
  timeout,
1145
+ permissionSuggestions,
1085
1146
  resolve: (decision) => {
1086
1147
  if (res.writableEnded) return;
1087
1148
  const responseJson = JSON.stringify({
1088
1149
  hookSpecificOutput: {
1089
1150
  hookEventName: "PermissionRequest",
1090
- decision: { behavior: decision },
1151
+ decision,
1091
1152
  },
1092
1153
  });
1093
1154
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -1482,16 +1543,8 @@ export class BridgeSession {
1482
1543
  requestId: string,
1483
1544
  ): void {
1484
1545
  const toolName = (event.tool_name ?? event.toolName) as string | undefined;
1485
- const toolInput =
1486
- typeof event.tool_input === "object" && event.tool_input
1487
- ? JSON.stringify(event.tool_input).slice(0, 1200)
1488
- : typeof event.toolInput === "object" && event.toolInput
1489
- ? JSON.stringify(event.toolInput).slice(0, 1200)
1490
- : typeof event.tool_input === "string"
1491
- ? event.tool_input.slice(0, 1200)
1492
- : typeof event.toolInput === "string"
1493
- ? event.toolInput.slice(0, 1200)
1494
- : "";
1546
+ const toolInput = stringifyHookInput(event.tool_input ?? event.toolInput);
1547
+ const suggestions = hookPermissionSuggestions(event);
1495
1548
  const context =
1496
1549
  typeof event.permission_prompt === "string"
1497
1550
  ? event.permission_prompt
@@ -1507,10 +1560,7 @@ export class BridgeSession {
1507
1560
  toolName,
1508
1561
  toolInput,
1509
1562
  context,
1510
- options: [
1511
- { id: "deny", label: "拒绝", kind: "deny" },
1512
- { id: "allow", label: "允许", kind: "allow" },
1513
- ],
1563
+ options: hookPermissionOptions(suggestions),
1514
1564
  },
1515
1565
  }));
1516
1566
  }
@@ -1592,12 +1642,12 @@ export class BridgeSession {
1592
1642
  }
1593
1643
  }
1594
1644
 
1595
- private resolvePendingPermission(requestId: string, decision: "allow" | "deny"): boolean {
1645
+ private resolvePendingPermission(requestId: string, choice: HookPermissionChoice): boolean {
1596
1646
  const pending = this.pendingPermissions.get(requestId);
1597
1647
  if (!pending) return false;
1598
1648
  this.pendingPermissions.delete(requestId);
1599
1649
  clearTimeout(pending.timeout);
1600
- pending.resolve(decision);
1650
+ pending.resolve(this.formatHookPermissionDecision(pending, choice));
1601
1651
 
1602
1652
  const stack = this.permissionStacks.get(pending.terminalId);
1603
1653
  if (stack) {
@@ -1608,6 +1658,26 @@ export class BridgeSession {
1608
1658
  return true;
1609
1659
  }
1610
1660
 
1661
+ private formatHookPermissionDecision(
1662
+ permission: PendingPermission,
1663
+ choice: HookPermissionChoice,
1664
+ ): HookPermissionDecision {
1665
+ const outcome = typeof choice === "string" ? choice : choice.outcome;
1666
+ const optionId = typeof choice === "string" ? undefined : choice.optionId;
1667
+ if (outcome === "allow") {
1668
+ return {
1669
+ behavior: "allow",
1670
+ ...(optionId === "allow_always" && permission.permissionSuggestions.length > 0
1671
+ ? { updatedPermissions: permission.permissionSuggestions }
1672
+ : {}),
1673
+ };
1674
+ }
1675
+ return {
1676
+ behavior: "deny",
1677
+ message: outcome === "cancelled" ? "Permission request cancelled." : "Permission denied by user.",
1678
+ };
1679
+ }
1680
+
1611
1681
  private sendPermissionSnapshot(
1612
1682
  terminalId: string,
1613
1683
  phase: string,