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.
- package/dist/cli/src/runtime/acp/agent-session.js +10 -2
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.js +11 -2
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +1 -0
- package/dist/cli/src/runtime/bridge-session.js +68 -19
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/runtime/acp/agent-session.ts +12 -2
- package/src/runtime/acp/agent-workspace.ts +13 -2
- package/src/runtime/bridge-session.ts +91 -21
|
@@ -71,14 +71,71 @@ interface TerminalInstance {
|
|
|
71
71
|
interface PendingPermission {
|
|
72
72
|
terminalId: string;
|
|
73
73
|
timeout: ReturnType<typeof setTimeout>;
|
|
74
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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,
|