linkshell-cli 0.2.64 → 0.2.66

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.
Files changed (37) hide show
  1. package/README.md +3 -0
  2. package/dist/cli/src/index.js +28 -1
  3. package/dist/cli/src/index.js.map +1 -1
  4. package/dist/cli/src/runtime/acp/acp-client.d.ts +41 -0
  5. package/dist/cli/src/runtime/acp/acp-client.js +102 -0
  6. package/dist/cli/src/runtime/acp/acp-client.js.map +1 -0
  7. package/dist/cli/src/runtime/acp/agent-session.d.ts +42 -0
  8. package/dist/cli/src/runtime/acp/agent-session.js +492 -0
  9. package/dist/cli/src/runtime/acp/agent-session.js.map +1 -0
  10. package/dist/cli/src/runtime/acp/json-rpc.d.ts +21 -0
  11. package/dist/cli/src/runtime/acp/json-rpc.js +150 -0
  12. package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -0
  13. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +13 -0
  14. package/dist/cli/src/runtime/acp/provider-resolver.js +22 -0
  15. package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -0
  16. package/dist/cli/src/runtime/bridge-session.d.ts +9 -0
  17. package/dist/cli/src/runtime/bridge-session.js +163 -41
  18. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  19. package/dist/cli/src/utils/daemon.d.ts +6 -0
  20. package/dist/cli/src/utils/daemon.js +22 -0
  21. package/dist/cli/src/utils/daemon.js.map +1 -1
  22. package/dist/cli/src/utils/keep-awake.d.ts +6 -0
  23. package/dist/cli/src/utils/keep-awake.js +48 -0
  24. package/dist/cli/src/utils/keep-awake.js.map +1 -0
  25. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  26. package/dist/shared-protocol/src/index.d.ts +1108 -54
  27. package/dist/shared-protocol/src/index.js +112 -1
  28. package/dist/shared-protocol/src/index.js.map +1 -1
  29. package/package.json +4 -4
  30. package/src/index.ts +38 -1
  31. package/src/runtime/acp/acp-client.ts +133 -0
  32. package/src/runtime/acp/agent-session.ts +589 -0
  33. package/src/runtime/acp/json-rpc.ts +177 -0
  34. package/src/runtime/acp/provider-resolver.ts +37 -0
  35. package/src/runtime/bridge-session.ts +189 -41
  36. package/src/utils/daemon.ts +28 -0
  37. package/src/utils/keep-awake.ts +61 -0
@@ -0,0 +1,150 @@
1
+ import { spawn } from "node:child_process";
2
+ export class JsonRpcStdioTransport {
3
+ command;
4
+ framing;
5
+ onNotification;
6
+ onRequest;
7
+ onExit;
8
+ child;
9
+ nextId = 1;
10
+ pending = new Map();
11
+ buffer = "";
12
+ constructor(command, framing, onNotification, onRequest, onExit) {
13
+ this.command = command;
14
+ this.framing = framing;
15
+ this.onNotification = onNotification;
16
+ this.onRequest = onRequest;
17
+ this.onExit = onExit;
18
+ }
19
+ start(cwd) {
20
+ if (this.child)
21
+ return;
22
+ this.child = spawn(this.command, {
23
+ cwd,
24
+ shell: true,
25
+ stdio: ["pipe", "pipe", "pipe"],
26
+ env: process.env,
27
+ });
28
+ this.child.stdout.setEncoding("utf8");
29
+ this.child.stderr.setEncoding("utf8");
30
+ this.child.stdout.on("data", (chunk) => this.read(chunk));
31
+ this.child.stderr.on("data", (chunk) => {
32
+ const trimmed = chunk.trim();
33
+ if (trimmed)
34
+ process.stderr.write(`[agent:stderr] ${trimmed}\n`);
35
+ });
36
+ this.child.on("error", (error) => this.failAll(error.message));
37
+ this.child.on("exit", (code, signal) => {
38
+ this.failAll(`ACP agent exited (code=${code ?? "null"}, signal=${signal ?? "null"})`);
39
+ });
40
+ }
41
+ request(method, params, timeoutMs = 30_000) {
42
+ if (!this.child || this.child.stdin.destroyed) {
43
+ return Promise.reject(new Error("ACP agent is not running"));
44
+ }
45
+ const id = this.nextId++;
46
+ const message = { jsonrpc: "2.0", id, method, params };
47
+ return new Promise((resolve, reject) => {
48
+ const timer = setTimeout(() => {
49
+ this.pending.delete(id);
50
+ reject(new Error(`ACP request timed out: ${method}`));
51
+ }, timeoutMs);
52
+ this.pending.set(id, { resolve, reject, timer });
53
+ this.write(message);
54
+ });
55
+ }
56
+ notify(method, params) {
57
+ this.write({ jsonrpc: "2.0", method, params });
58
+ }
59
+ stop() {
60
+ const child = this.child;
61
+ this.child = undefined;
62
+ if (child && !child.killed)
63
+ child.kill("SIGTERM");
64
+ this.failAll("ACP transport stopped");
65
+ }
66
+ write(message) {
67
+ const raw = JSON.stringify(message);
68
+ if (this.framing === "newline") {
69
+ this.child?.stdin.write(`${raw}\n`);
70
+ return;
71
+ }
72
+ this.child?.stdin.write(`Content-Length: ${Buffer.byteLength(raw, "utf8")}\r\n\r\n${raw}`);
73
+ }
74
+ read(chunk) {
75
+ this.buffer += chunk;
76
+ while (this.buffer.length > 0) {
77
+ const contentLengthMatch = this.buffer.match(/^Content-Length:\s*(\d+)\r?\n/i);
78
+ if (contentLengthMatch) {
79
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
80
+ const altHeaderEnd = this.buffer.indexOf("\n\n");
81
+ const end = headerEnd >= 0 ? headerEnd + 4 : altHeaderEnd >= 0 ? altHeaderEnd + 2 : -1;
82
+ if (end < 0)
83
+ return;
84
+ const length = Number(contentLengthMatch[1]);
85
+ if (this.buffer.length < end + length)
86
+ return;
87
+ const raw = this.buffer.slice(end, end + length);
88
+ this.buffer = this.buffer.slice(end + length);
89
+ this.dispatch(raw);
90
+ continue;
91
+ }
92
+ const newline = this.buffer.indexOf("\n");
93
+ if (newline < 0)
94
+ return;
95
+ const raw = this.buffer.slice(0, newline).trim();
96
+ this.buffer = this.buffer.slice(newline + 1);
97
+ if (raw)
98
+ this.dispatch(raw);
99
+ }
100
+ }
101
+ dispatch(raw) {
102
+ let message;
103
+ try {
104
+ message = JSON.parse(raw);
105
+ }
106
+ catch {
107
+ return;
108
+ }
109
+ if ("id" in message && ("result" in message || "error" in message)) {
110
+ const pending = this.pending.get(message.id);
111
+ if (!pending)
112
+ return;
113
+ this.pending.delete(message.id);
114
+ clearTimeout(pending.timer);
115
+ const response = message;
116
+ if (response.error)
117
+ pending.reject(new Error(response.error.message));
118
+ else
119
+ pending.resolve(response.result);
120
+ return;
121
+ }
122
+ if ("method" in message && "id" in message) {
123
+ Promise.resolve(this.onRequest(message.method, message.params))
124
+ .then((result) => this.write({ jsonrpc: "2.0", id: message.id, result }))
125
+ .catch((error) => {
126
+ this.write({
127
+ jsonrpc: "2.0",
128
+ id: message.id,
129
+ error: {
130
+ code: -32000,
131
+ message: error instanceof Error ? error.message : String(error),
132
+ },
133
+ });
134
+ });
135
+ return;
136
+ }
137
+ if ("method" in message) {
138
+ this.onNotification(message.method, message.params);
139
+ }
140
+ }
141
+ failAll(message) {
142
+ this.onExit(message);
143
+ for (const [, pending] of this.pending) {
144
+ clearTimeout(pending.timer);
145
+ pending.reject(new Error(message));
146
+ }
147
+ this.pending.clear();
148
+ }
149
+ }
150
+ //# sourceMappingURL=json-rpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-rpc.js","sourceRoot":"","sources":["../../../../../src/runtime/acp/json-rpc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export type AgentProvider = "codex" | "claude" | "custom";
2
+ export type AgentProtocol = "acp" | "codex-app-server";
3
+ export type AgentFraming = "content-length" | "newline";
4
+ export interface AgentCommandConfig {
5
+ command: string;
6
+ provider: AgentProvider;
7
+ protocol: AgentProtocol;
8
+ framing: AgentFraming;
9
+ }
10
+ export declare function resolveAgentCommand(input: {
11
+ provider: AgentProvider;
12
+ command?: string;
13
+ }): AgentCommandConfig | null;
@@ -0,0 +1,22 @@
1
+ export function resolveAgentCommand(input) {
2
+ const explicit = input.command?.trim();
3
+ if (explicit) {
4
+ const isCodexAppServer = /\bcodex\b/.test(explicit) && /\bapp-server\b/.test(explicit);
5
+ return {
6
+ provider: input.provider,
7
+ command: explicit,
8
+ protocol: isCodexAppServer ? "codex-app-server" : "acp",
9
+ framing: isCodexAppServer ? "newline" : "content-length",
10
+ };
11
+ }
12
+ if (input.provider === "codex") {
13
+ return {
14
+ provider: "codex",
15
+ command: "codex app-server --listen stdio://",
16
+ protocol: "codex-app-server",
17
+ framing: "newline",
18
+ };
19
+ }
20
+ return null;
21
+ }
22
+ //# sourceMappingURL=provider-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-resolver.js","sourceRoot":"","sources":["../../../../../src/runtime/acp/provider-resolver.ts"],"names":[],"mappings":"AAWA,MAAM,UAAU,mBAAmB,CAAC,KAGnC;IACC,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;IACvC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,gBAAgB,GAAG,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvF,OAAO;YACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,OAAO,EAAE,QAAQ;YACjB,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,KAAK;YACvD,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB;SACzD,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAC/B,OAAO;YACL,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,oCAAoC;YAC7C,QAAQ,EAAE,kBAAkB;YAC5B,OAAO,EAAE,SAAS;SACnB,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import type { ProviderConfig } from "../providers.js";
2
+ import type { AgentProvider } from "./acp/provider-resolver.js";
2
3
  export interface BridgeSessionOptions {
3
4
  gatewayUrl: string;
4
5
  gatewayHttpUrl: string;
@@ -12,6 +13,10 @@ export interface BridgeSessionOptions {
12
13
  screen?: boolean;
13
14
  providerConfig: ProviderConfig;
14
15
  authToken?: string;
16
+ keepAwake?: boolean;
17
+ agentUi?: boolean;
18
+ agentProvider?: AgentProvider;
19
+ agentCommand?: string;
15
20
  }
16
21
  export declare class BridgeSession {
17
22
  private readonly options;
@@ -30,6 +35,8 @@ export declare class BridgeSession {
30
35
  private screenCapture;
31
36
  private screenShare;
32
37
  private tunnelSockets;
38
+ private keepAwake;
39
+ private agentSession;
33
40
  constructor(options: BridgeSessionOptions);
34
41
  private log;
35
42
  start(): Promise<void>;
@@ -61,6 +68,8 @@ export declare class BridgeSession {
61
68
  private autoResolvePending;
62
69
  /** Drain all pending permissions for a terminal (session ended, stop, etc.) */
63
70
  private drainPendingPermissions;
71
+ private resolvePendingPermission;
72
+ private sendPermissionSnapshot;
64
73
  private cleanupHookServer;
65
74
  /** Remove hook entries containing our marker from a JSON config file */
66
75
  private removeHookEntries;
@@ -10,11 +10,19 @@ import { ScrollbackBuffer } from "./scrollback.js";
10
10
  import { ScreenFallback } from "./screen-fallback.js";
11
11
  import { ScreenShare } from "./screen-share.js";
12
12
  import { getLanIp } from "../utils/lan-ip.js";
13
+ import { startKeepAwake } from "../utils/keep-awake.js";
14
+ import { AgentSessionProxy } from "./acp/agent-session.js";
13
15
  const HEARTBEAT_INTERVAL = 15_000;
14
16
  const RECONNECT_BASE_DELAY = 1_000;
15
17
  const RECONNECT_MAX_DELAY = 30_000;
16
18
  const RECONNECT_MAX_ATTEMPTS = 20;
17
19
  const DEFAULT_TERMINAL_ID = "default";
20
+ const HOOK_BODY_LIMIT = 256 * 1024;
21
+ const PERMISSION_REQUEST_TIMEOUT_MS = Number(process.env.LINKSHELL_PERMISSION_TIMEOUT_MS ?? 5 * 60_000);
22
+ function isLinkShellHookEntry(entry) {
23
+ const raw = JSON.stringify(entry);
24
+ return /\/hook\?m=lsh-[^"'\s]+/.test(raw);
25
+ }
18
26
  function getPairingGatewayParam(gatewayHttpUrl) {
19
27
  try {
20
28
  const url = new URL(gatewayHttpUrl);
@@ -64,6 +72,12 @@ function resolvePairingGateway(gatewayHttpUrl, pairingGateway) {
64
72
  }
65
73
  }
66
74
  }
75
+ function normalizeAgentProvider(provider) {
76
+ if (provider === "claude" || provider === "custom") {
77
+ return provider;
78
+ }
79
+ return "codex";
80
+ }
67
81
  export class BridgeSession {
68
82
  options;
69
83
  socket;
@@ -82,6 +96,8 @@ export class BridgeSession {
82
96
  screenCapture;
83
97
  screenShare;
84
98
  tunnelSockets = new Map();
99
+ keepAwake;
100
+ agentSession;
85
101
  constructor(options) {
86
102
  this.options = options;
87
103
  this.sessionId = options.sessionId ?? "";
@@ -96,6 +112,24 @@ export class BridgeSession {
96
112
  if (!this.sessionId) {
97
113
  await this.createPairing();
98
114
  }
115
+ if (this.options.keepAwake) {
116
+ this.keepAwake = startKeepAwake();
117
+ }
118
+ else {
119
+ process.stderr.write("[bridge] keep-awake disabled\n");
120
+ }
121
+ if (this.options.agentUi) {
122
+ const agentProvider = normalizeAgentProvider(this.options.agentProvider ?? this.options.providerConfig.provider);
123
+ this.agentSession = new AgentSessionProxy({
124
+ sessionId: this.sessionId,
125
+ cwd: process.cwd(),
126
+ provider: agentProvider,
127
+ command: this.options.agentCommand,
128
+ verbose: this.options.verbose,
129
+ send: (envelope) => this.send(envelope),
130
+ });
131
+ process.stderr.write("[bridge] agent GUI channel enabled\n");
132
+ }
99
133
  await this.spawnTerminal(DEFAULT_TERMINAL_ID, process.cwd());
100
134
  this.connectGateway();
101
135
  }
@@ -180,7 +214,14 @@ export class BridgeSession {
180
214
  this.startHeartbeat();
181
215
  });
182
216
  this.socket.on("message", (data) => {
183
- const envelope = parseEnvelope(data.toString());
217
+ let envelope;
218
+ try {
219
+ envelope = parseEnvelope(data.toString());
220
+ }
221
+ catch (err) {
222
+ this.log(`invalid gateway message ignored: ${err}`);
223
+ return;
224
+ }
184
225
  this.log(`recv ${envelope.type}${envelope.seq !== undefined ? ` seq=${envelope.seq}` : ""}`);
185
226
  this.handleMessage(envelope).catch((err) => {
186
227
  this.log(`handleMessage error: ${err}`);
@@ -374,7 +415,7 @@ export class BridgeSession {
374
415
  const p = parseTypedPayload("session.resume", envelope.payload);
375
416
  // Replay all terminals
376
417
  for (const [termId, term] of this.terminals) {
377
- this.replayFrom(termId, term, p.lastAckedSeq);
418
+ this.replayFrom(termId, term, p.lastAckedSeqByTerminal[termId] ?? p.lastAckedSeq);
378
419
  }
379
420
  // Also send terminal list so client knows what's available
380
421
  this.sendTerminalList();
@@ -401,6 +442,35 @@ export class BridgeSession {
401
442
  this.screenShare?.handleIceCandidate(p.candidate, p.sdpMid, p.sdpMLineIndex);
402
443
  break;
403
444
  }
445
+ case "agent.initialize":
446
+ case "agent.session.new":
447
+ case "agent.session.load":
448
+ case "agent.session.list":
449
+ case "agent.prompt":
450
+ case "agent.cancel":
451
+ case "agent.permission.response": {
452
+ if (!this.agentSession) {
453
+ this.send(createEnvelope({
454
+ type: "agent.capabilities",
455
+ sessionId: this.sessionId,
456
+ payload: {
457
+ enabled: false,
458
+ provider: normalizeAgentProvider(this.options.agentProvider ?? this.options.providerConfig.provider),
459
+ error: "Agent GUI is not enabled. Start CLI with --agent-ui.",
460
+ supportsSessionList: false,
461
+ supportsSessionLoad: false,
462
+ supportsImages: false,
463
+ supportsAudio: false,
464
+ supportsPermission: false,
465
+ supportsPlan: false,
466
+ supportsCancel: false,
467
+ },
468
+ }));
469
+ break;
470
+ }
471
+ await this.agentSession.handleEnvelope(envelope);
472
+ break;
473
+ }
404
474
  case "file.upload": {
405
475
  const p = parseTypedPayload("file.upload", envelope.payload);
406
476
  const ext = p.filename.split(".").pop() || "png";
@@ -415,22 +485,8 @@ export class BridgeSession {
415
485
  }
416
486
  case "permission.decision": {
417
487
  const p = envelope.payload;
418
- const resolve = this.pendingPermissions.get(p.requestId);
419
- if (resolve) {
420
- this.pendingPermissions.delete(p.requestId);
421
- resolve(p.decision);
488
+ if (this.resolvePendingPermission(p.requestId, p.decision)) {
422
489
  this.log(`permission decision for ${p.requestId}: ${p.decision}`);
423
- // Pop from permission stack
424
- if (p.decision === "allow" || p.decision === "deny") {
425
- const stack = this.permissionStacks.get(tid);
426
- if (stack) {
427
- const idx = stack.findIndex((s) => s.requestId === p.requestId);
428
- if (idx >= 0)
429
- stack.splice(idx, 1);
430
- if (stack.length === 0)
431
- this.permissionStacks.delete(tid);
432
- }
433
- }
434
490
  }
435
491
  else {
436
492
  this.log(`no pending permission for ${p.requestId}`);
@@ -740,8 +796,21 @@ export class BridgeSession {
740
796
  return;
741
797
  }
742
798
  let body = "";
743
- req.on("data", (chunk) => { body += chunk.toString(); });
799
+ let bodyTooLarge = false;
800
+ req.on("data", (chunk) => {
801
+ if (bodyTooLarge)
802
+ return;
803
+ body += chunk.toString();
804
+ if (Buffer.byteLength(body, "utf8") > HOOK_BODY_LIMIT) {
805
+ bodyTooLarge = true;
806
+ res.writeHead(413);
807
+ res.end("payload too large");
808
+ req.destroy();
809
+ }
810
+ });
744
811
  req.on("end", () => {
812
+ if (bodyTooLarge || res.writableEnded)
813
+ return;
745
814
  this.log(`hook body (${body.length} bytes): ${body.slice(0, 200)}`);
746
815
  try {
747
816
  const event = JSON.parse(body);
@@ -749,15 +818,27 @@ export class BridgeSession {
749
818
  // PermissionRequest: hold connection, wait for user decision from mobile app
750
819
  if (hookName === "PermissionRequest") {
751
820
  const requestId = `pr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
752
- this.pendingPermissions.set(requestId, (decision) => {
753
- const responseJson = JSON.stringify({
754
- hookSpecificOutput: {
755
- hookEventName: "PermissionRequest",
756
- decision: { behavior: decision },
757
- },
758
- });
759
- res.writeHead(200, { "Content-Type": "application/json" });
760
- res.end(responseJson);
821
+ const timeout = setTimeout(() => {
822
+ if (this.resolvePendingPermission(requestId, "deny")) {
823
+ this.log(`permission request ${requestId} timed out`);
824
+ this.sendPermissionSnapshot(terminalId, "thinking", "permission timed out");
825
+ }
826
+ }, PERMISSION_REQUEST_TIMEOUT_MS);
827
+ this.pendingPermissions.set(requestId, {
828
+ terminalId,
829
+ timeout,
830
+ resolve: (decision) => {
831
+ if (res.writableEnded)
832
+ return;
833
+ const responseJson = JSON.stringify({
834
+ hookSpecificOutput: {
835
+ hookEventName: "PermissionRequest",
836
+ decision: { behavior: decision },
837
+ },
838
+ });
839
+ res.writeHead(200, { "Content-Type": "application/json" });
840
+ res.end(responseJson);
841
+ },
761
842
  });
762
843
  // Send status with requestId so app can route decision back
763
844
  this.handleHookEvent(terminalId, event, provider, requestId);
@@ -814,7 +895,14 @@ export class BridgeSession {
814
895
  }
815
896
  catch { /* doesn't exist yet */ }
816
897
  const hookEntry = { matcher: "", hooks: [{ type: "command", command: curlCmd, timeout: 5 }] };
817
- const permissionEntry = { matcher: "", hooks: [{ type: "command", command: curlCmd, timeout: 86400 }] };
898
+ const permissionEntry = {
899
+ matcher: "",
900
+ hooks: [{
901
+ type: "command",
902
+ command: curlCmd,
903
+ timeout: Math.ceil((PERMISSION_REQUEST_TIMEOUT_MS + 30_000) / 1000),
904
+ }],
905
+ };
818
906
  const hookEvents = {
819
907
  PreToolUse: hookEntry,
820
908
  PostToolUse: hookEntry,
@@ -829,7 +917,7 @@ export class BridgeSession {
829
917
  for (const [eventName, entry] of Object.entries(hookEvents)) {
830
918
  let arr = Array.isArray(existingHooks[eventName]) ? existingHooks[eventName] : [];
831
919
  // Remove any dead linkshell hook entries (from previous instances)
832
- arr = arr.filter((e) => !JSON.stringify(e).includes("/hook"));
920
+ arr = arr.filter((e) => !isLinkShellHookEntry(e));
833
921
  arr.push(entry);
834
922
  existingHooks[eventName] = arr;
835
923
  }
@@ -884,7 +972,7 @@ export class BridgeSession {
884
972
  const existingHooks = existing.hooks ?? {};
885
973
  for (const [eventName, entry] of Object.entries(hookEvents)) {
886
974
  let arr = Array.isArray(existingHooks[eventName]) ? existingHooks[eventName] : [];
887
- arr = arr.filter((e) => !JSON.stringify(e).includes("/hook"));
975
+ arr = arr.filter((e) => !isLinkShellHookEntry(e));
888
976
  arr.push(entry);
889
977
  existingHooks[eventName] = arr;
890
978
  }
@@ -914,7 +1002,7 @@ export class BridgeSession {
914
1002
  const existingHooks = (existing.hooks ?? {});
915
1003
  for (const [eventName, entry] of Object.entries(hookEvents)) {
916
1004
  let arr = Array.isArray(existingHooks[eventName]) ? existingHooks[eventName] : [];
917
- arr = arr.filter((e) => !JSON.stringify(e).includes("/hook"));
1005
+ arr = arr.filter((e) => !isLinkShellHookEntry(e));
918
1006
  arr.push(entry);
919
1007
  existingHooks[eventName] = arr;
920
1008
  }
@@ -949,7 +1037,7 @@ export class BridgeSession {
949
1037
  const existingHooks = existing.hooks ?? {};
950
1038
  for (const [eventName, entry] of Object.entries(hookEvents)) {
951
1039
  let arr = Array.isArray(existingHooks[eventName]) ? existingHooks[eventName] : [];
952
- arr = arr.filter((e) => !JSON.stringify(e).includes("/hook"));
1040
+ arr = arr.filter((e) => !isLinkShellHookEntry(e));
953
1041
  arr.push(entry);
954
1042
  existingHooks[eventName] = arr;
955
1043
  }
@@ -1180,10 +1268,7 @@ export class BridgeSession {
1180
1268
  }
1181
1269
  /** Auto-resolve a single pending permission (user acted in terminal) */
1182
1270
  autoResolvePending(requestId) {
1183
- const resolve = this.pendingPermissions.get(requestId);
1184
- if (resolve) {
1185
- this.pendingPermissions.delete(requestId);
1186
- resolve("allow");
1271
+ if (this.resolvePendingPermission(requestId, "allow")) {
1187
1272
  this.log(`auto-resolved pending permission ${requestId} (user acted in terminal)`);
1188
1273
  }
1189
1274
  }
@@ -1192,15 +1277,48 @@ export class BridgeSession {
1192
1277
  const stack = this.permissionStacks.get(terminalId);
1193
1278
  if (!stack)
1194
1279
  return;
1195
- for (const entry of stack) {
1196
- const resolve = this.pendingPermissions.get(entry.requestId);
1197
- if (resolve) {
1198
- this.pendingPermissions.delete(entry.requestId);
1199
- resolve("deny");
1280
+ for (const entry of [...stack]) {
1281
+ if (this.resolvePendingPermission(entry.requestId, "deny")) {
1200
1282
  this.log(`drained pending permission ${entry.requestId}`);
1201
1283
  }
1202
1284
  }
1203
1285
  }
1286
+ resolvePendingPermission(requestId, decision) {
1287
+ const pending = this.pendingPermissions.get(requestId);
1288
+ if (!pending)
1289
+ return false;
1290
+ this.pendingPermissions.delete(requestId);
1291
+ clearTimeout(pending.timeout);
1292
+ pending.resolve(decision);
1293
+ const stack = this.permissionStacks.get(pending.terminalId);
1294
+ if (stack) {
1295
+ const idx = stack.findIndex((entry) => entry.requestId === requestId);
1296
+ if (idx >= 0)
1297
+ stack.splice(idx, 1);
1298
+ if (stack.length === 0)
1299
+ this.permissionStacks.delete(pending.terminalId);
1300
+ }
1301
+ return true;
1302
+ }
1303
+ sendPermissionSnapshot(terminalId, phase, summary) {
1304
+ const stack = this.permissionStacks.get(terminalId);
1305
+ const topPermission = stack && stack.length > 0 ? stack[stack.length - 1] : undefined;
1306
+ const pendingPermissionCount = stack?.length ?? 0;
1307
+ const term = this.terminals.get(terminalId);
1308
+ const seq = term ? term.statusSeq++ : 0;
1309
+ this.send(createEnvelope({
1310
+ type: "terminal.status",
1311
+ sessionId: this.sessionId,
1312
+ terminalId,
1313
+ payload: {
1314
+ phase,
1315
+ seq,
1316
+ ...(summary && { summary }),
1317
+ ...(topPermission && { topPermission }),
1318
+ ...(pendingPermissionCount > 0 && { pendingPermissionCount }),
1319
+ },
1320
+ }));
1321
+ }
1204
1322
  cleanupHookServer(term) {
1205
1323
  // Drain any pending permission requests for this terminal
1206
1324
  this.drainPendingPermissions(term.id);
@@ -1374,6 +1492,10 @@ export class BridgeSession {
1374
1492
  this.exited = true;
1375
1493
  this.stopHeartbeat();
1376
1494
  this.stopScreenCapture();
1495
+ this.agentSession?.stop();
1496
+ this.agentSession = undefined;
1497
+ this.keepAwake?.stop();
1498
+ this.keepAwake = undefined;
1377
1499
  if (this.reconnectTimer) {
1378
1500
  clearTimeout(this.reconnectTimer);
1379
1501
  this.reconnectTimer = undefined;