leduo-patrol 2.2.1 → 2.2.4

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 (179) hide show
  1. package/README.md +11 -2
  2. package/dist/server/__tests__/acp-session.test.js +115 -0
  3. package/dist/server/__tests__/activity-monitor.test.js +13 -1
  4. package/dist/server/__tests__/session-manager.test.js +380 -1
  5. package/dist/server/acp-session.js +476 -0
  6. package/dist/server/activity-monitor.js +22 -7
  7. package/dist/server/index.js +57 -1
  8. package/dist/server/session-manager.js +1301 -121
  9. package/dist/web/assets/index-Bll9nc_X.js +21 -0
  10. package/dist/web/assets/index-y1qgSOLv.css +1 -0
  11. package/dist/web/index.html +2 -2
  12. package/package.json +3 -1
  13. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/LICENSE +191 -0
  14. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/README.md +53 -0
  15. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.d.ts +823 -0
  16. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.js +965 -0
  17. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.js.map +1 -0
  18. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.test.d.ts +1 -0
  19. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.test.js +839 -0
  20. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.test.js.map +1 -0
  21. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/agent.d.ts +2 -0
  22. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/agent.js +225 -0
  23. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/agent.js.map +1 -0
  24. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/client.d.ts +2 -0
  25. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/client.js +130 -0
  26. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/client.js.map +1 -0
  27. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.d.ts +35 -0
  28. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js +5 -0
  29. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js.map +1 -0
  30. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/index.d.ts +27 -0
  31. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/index.js +28 -0
  32. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/index.js.map +1 -0
  33. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts +2870 -0
  34. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.js +3 -0
  35. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.js.map +1 -0
  36. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.d.ts +5333 -0
  37. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js +1554 -0
  38. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js.map +1 -0
  39. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/stream.d.ts +24 -0
  40. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/stream.js +64 -0
  41. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/stream.js.map +1 -0
  42. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/package.json +66 -0
  43. package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/schema/schema.json +4125 -0
  44. package/vendor/claude-code-acp/node_modules/@types/node/LICENSE +21 -0
  45. package/vendor/claude-code-acp/node_modules/@types/node/README.md +15 -0
  46. package/vendor/claude-code-acp/node_modules/@types/node/assert/strict.d.ts +105 -0
  47. package/vendor/claude-code-acp/node_modules/@types/node/assert.d.ts +955 -0
  48. package/vendor/claude-code-acp/node_modules/@types/node/async_hooks.d.ts +623 -0
  49. package/vendor/claude-code-acp/node_modules/@types/node/buffer.buffer.d.ts +466 -0
  50. package/vendor/claude-code-acp/node_modules/@types/node/buffer.d.ts +1810 -0
  51. package/vendor/claude-code-acp/node_modules/@types/node/child_process.d.ts +1428 -0
  52. package/vendor/claude-code-acp/node_modules/@types/node/cluster.d.ts +486 -0
  53. package/vendor/claude-code-acp/node_modules/@types/node/compatibility/iterators.d.ts +21 -0
  54. package/vendor/claude-code-acp/node_modules/@types/node/console.d.ts +151 -0
  55. package/vendor/claude-code-acp/node_modules/@types/node/constants.d.ts +20 -0
  56. package/vendor/claude-code-acp/node_modules/@types/node/crypto.d.ts +4065 -0
  57. package/vendor/claude-code-acp/node_modules/@types/node/dgram.d.ts +564 -0
  58. package/vendor/claude-code-acp/node_modules/@types/node/diagnostics_channel.d.ts +576 -0
  59. package/vendor/claude-code-acp/node_modules/@types/node/dns/promises.d.ts +503 -0
  60. package/vendor/claude-code-acp/node_modules/@types/node/dns.d.ts +922 -0
  61. package/vendor/claude-code-acp/node_modules/@types/node/domain.d.ts +166 -0
  62. package/vendor/claude-code-acp/node_modules/@types/node/events.d.ts +1054 -0
  63. package/vendor/claude-code-acp/node_modules/@types/node/fs/promises.d.ts +1329 -0
  64. package/vendor/claude-code-acp/node_modules/@types/node/fs.d.ts +4676 -0
  65. package/vendor/claude-code-acp/node_modules/@types/node/globals.d.ts +150 -0
  66. package/vendor/claude-code-acp/node_modules/@types/node/globals.typedarray.d.ts +101 -0
  67. package/vendor/claude-code-acp/node_modules/@types/node/http.d.ts +2167 -0
  68. package/vendor/claude-code-acp/node_modules/@types/node/http2.d.ts +2480 -0
  69. package/vendor/claude-code-acp/node_modules/@types/node/https.d.ts +405 -0
  70. package/vendor/claude-code-acp/node_modules/@types/node/index.d.ts +115 -0
  71. package/vendor/claude-code-acp/node_modules/@types/node/inspector/promises.d.ts +41 -0
  72. package/vendor/claude-code-acp/node_modules/@types/node/inspector.d.ts +224 -0
  73. package/vendor/claude-code-acp/node_modules/@types/node/inspector.generated.d.ts +4226 -0
  74. package/vendor/claude-code-acp/node_modules/@types/node/module.d.ts +819 -0
  75. package/vendor/claude-code-acp/node_modules/@types/node/net.d.ts +933 -0
  76. package/vendor/claude-code-acp/node_modules/@types/node/os.d.ts +507 -0
  77. package/vendor/claude-code-acp/node_modules/@types/node/package.json +155 -0
  78. package/vendor/claude-code-acp/node_modules/@types/node/path/posix.d.ts +8 -0
  79. package/vendor/claude-code-acp/node_modules/@types/node/path/win32.d.ts +8 -0
  80. package/vendor/claude-code-acp/node_modules/@types/node/path.d.ts +187 -0
  81. package/vendor/claude-code-acp/node_modules/@types/node/perf_hooks.d.ts +643 -0
  82. package/vendor/claude-code-acp/node_modules/@types/node/process.d.ts +2161 -0
  83. package/vendor/claude-code-acp/node_modules/@types/node/punycode.d.ts +117 -0
  84. package/vendor/claude-code-acp/node_modules/@types/node/querystring.d.ts +152 -0
  85. package/vendor/claude-code-acp/node_modules/@types/node/quic.d.ts +910 -0
  86. package/vendor/claude-code-acp/node_modules/@types/node/readline/promises.d.ts +161 -0
  87. package/vendor/claude-code-acp/node_modules/@types/node/readline.d.ts +541 -0
  88. package/vendor/claude-code-acp/node_modules/@types/node/repl.d.ts +415 -0
  89. package/vendor/claude-code-acp/node_modules/@types/node/sea.d.ts +162 -0
  90. package/vendor/claude-code-acp/node_modules/@types/node/sqlite.d.ts +955 -0
  91. package/vendor/claude-code-acp/node_modules/@types/node/stream/consumers.d.ts +38 -0
  92. package/vendor/claude-code-acp/node_modules/@types/node/stream/promises.d.ts +211 -0
  93. package/vendor/claude-code-acp/node_modules/@types/node/stream/web.d.ts +296 -0
  94. package/vendor/claude-code-acp/node_modules/@types/node/stream.d.ts +1760 -0
  95. package/vendor/claude-code-acp/node_modules/@types/node/string_decoder.d.ts +67 -0
  96. package/vendor/claude-code-acp/node_modules/@types/node/test/reporters.d.ts +96 -0
  97. package/vendor/claude-code-acp/node_modules/@types/node/test.d.ts +2240 -0
  98. package/vendor/claude-code-acp/node_modules/@types/node/timers/promises.d.ts +108 -0
  99. package/vendor/claude-code-acp/node_modules/@types/node/timers.d.ts +159 -0
  100. package/vendor/claude-code-acp/node_modules/@types/node/tls.d.ts +1198 -0
  101. package/vendor/claude-code-acp/node_modules/@types/node/trace_events.d.ts +197 -0
  102. package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +462 -0
  103. package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/compatibility/float16array.d.ts +71 -0
  104. package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +36 -0
  105. package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/index.d.ts +117 -0
  106. package/vendor/claude-code-acp/node_modules/@types/node/ts5.7/compatibility/float16array.d.ts +72 -0
  107. package/vendor/claude-code-acp/node_modules/@types/node/ts5.7/index.d.ts +117 -0
  108. package/vendor/claude-code-acp/node_modules/@types/node/tty.d.ts +250 -0
  109. package/vendor/claude-code-acp/node_modules/@types/node/url.d.ts +519 -0
  110. package/vendor/claude-code-acp/node_modules/@types/node/util/types.d.ts +558 -0
  111. package/vendor/claude-code-acp/node_modules/@types/node/util.d.ts +1662 -0
  112. package/vendor/claude-code-acp/node_modules/@types/node/v8.d.ts +983 -0
  113. package/vendor/claude-code-acp/node_modules/@types/node/vm.d.ts +1208 -0
  114. package/vendor/claude-code-acp/node_modules/@types/node/wasi.d.ts +202 -0
  115. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/abortcontroller.d.ts +59 -0
  116. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/blob.d.ts +23 -0
  117. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/console.d.ts +9 -0
  118. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/crypto.d.ts +39 -0
  119. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/domexception.d.ts +68 -0
  120. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/encoding.d.ts +11 -0
  121. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/events.d.ts +106 -0
  122. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/fetch.d.ts +69 -0
  123. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/importmeta.d.ts +13 -0
  124. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/messaging.d.ts +23 -0
  125. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/navigator.d.ts +25 -0
  126. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/performance.d.ts +45 -0
  127. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/storage.d.ts +24 -0
  128. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/streams.d.ts +115 -0
  129. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/timers.d.ts +44 -0
  130. package/vendor/claude-code-acp/node_modules/@types/node/web-globals/url.d.ts +24 -0
  131. package/vendor/claude-code-acp/node_modules/@types/node/worker_threads.d.ts +717 -0
  132. package/vendor/claude-code-acp/node_modules/@types/node/zlib.d.ts +618 -0
  133. package/vendor/claude-code-acp/node_modules/undici-types/LICENSE +21 -0
  134. package/vendor/claude-code-acp/node_modules/undici-types/README.md +6 -0
  135. package/vendor/claude-code-acp/node_modules/undici-types/agent.d.ts +32 -0
  136. package/vendor/claude-code-acp/node_modules/undici-types/api.d.ts +43 -0
  137. package/vendor/claude-code-acp/node_modules/undici-types/balanced-pool.d.ts +29 -0
  138. package/vendor/claude-code-acp/node_modules/undici-types/cache-interceptor.d.ts +172 -0
  139. package/vendor/claude-code-acp/node_modules/undici-types/cache.d.ts +36 -0
  140. package/vendor/claude-code-acp/node_modules/undici-types/client-stats.d.ts +15 -0
  141. package/vendor/claude-code-acp/node_modules/undici-types/client.d.ts +108 -0
  142. package/vendor/claude-code-acp/node_modules/undici-types/connector.d.ts +34 -0
  143. package/vendor/claude-code-acp/node_modules/undici-types/content-type.d.ts +21 -0
  144. package/vendor/claude-code-acp/node_modules/undici-types/cookies.d.ts +30 -0
  145. package/vendor/claude-code-acp/node_modules/undici-types/diagnostics-channel.d.ts +74 -0
  146. package/vendor/claude-code-acp/node_modules/undici-types/dispatcher.d.ts +276 -0
  147. package/vendor/claude-code-acp/node_modules/undici-types/env-http-proxy-agent.d.ts +22 -0
  148. package/vendor/claude-code-acp/node_modules/undici-types/errors.d.ts +161 -0
  149. package/vendor/claude-code-acp/node_modules/undici-types/eventsource.d.ts +66 -0
  150. package/vendor/claude-code-acp/node_modules/undici-types/fetch.d.ts +211 -0
  151. package/vendor/claude-code-acp/node_modules/undici-types/formdata.d.ts +108 -0
  152. package/vendor/claude-code-acp/node_modules/undici-types/global-dispatcher.d.ts +9 -0
  153. package/vendor/claude-code-acp/node_modules/undici-types/global-origin.d.ts +7 -0
  154. package/vendor/claude-code-acp/node_modules/undici-types/h2c-client.d.ts +73 -0
  155. package/vendor/claude-code-acp/node_modules/undici-types/handlers.d.ts +15 -0
  156. package/vendor/claude-code-acp/node_modules/undici-types/header.d.ts +160 -0
  157. package/vendor/claude-code-acp/node_modules/undici-types/index.d.ts +80 -0
  158. package/vendor/claude-code-acp/node_modules/undici-types/interceptors.d.ts +39 -0
  159. package/vendor/claude-code-acp/node_modules/undici-types/mock-agent.d.ts +68 -0
  160. package/vendor/claude-code-acp/node_modules/undici-types/mock-call-history.d.ts +111 -0
  161. package/vendor/claude-code-acp/node_modules/undici-types/mock-client.d.ts +27 -0
  162. package/vendor/claude-code-acp/node_modules/undici-types/mock-errors.d.ts +12 -0
  163. package/vendor/claude-code-acp/node_modules/undici-types/mock-interceptor.d.ts +94 -0
  164. package/vendor/claude-code-acp/node_modules/undici-types/mock-pool.d.ts +27 -0
  165. package/vendor/claude-code-acp/node_modules/undici-types/package.json +55 -0
  166. package/vendor/claude-code-acp/node_modules/undici-types/patch.d.ts +29 -0
  167. package/vendor/claude-code-acp/node_modules/undici-types/pool-stats.d.ts +19 -0
  168. package/vendor/claude-code-acp/node_modules/undici-types/pool.d.ts +41 -0
  169. package/vendor/claude-code-acp/node_modules/undici-types/proxy-agent.d.ts +29 -0
  170. package/vendor/claude-code-acp/node_modules/undici-types/readable.d.ts +68 -0
  171. package/vendor/claude-code-acp/node_modules/undici-types/retry-agent.d.ts +8 -0
  172. package/vendor/claude-code-acp/node_modules/undici-types/retry-handler.d.ts +125 -0
  173. package/vendor/claude-code-acp/node_modules/undici-types/snapshot-agent.d.ts +109 -0
  174. package/vendor/claude-code-acp/node_modules/undici-types/util.d.ts +18 -0
  175. package/vendor/claude-code-acp/node_modules/undici-types/utility.d.ts +7 -0
  176. package/vendor/claude-code-acp/node_modules/undici-types/webidl.d.ts +341 -0
  177. package/vendor/claude-code-acp/node_modules/undici-types/websocket.d.ts +186 -0
  178. package/dist/web/assets/index-B5Dh2E8j.css +0 -1
  179. package/dist/web/assets/index-xPPPaEde.js +0 -13
@@ -0,0 +1,476 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as childProcess from "node:child_process";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { Readable, Writable } from "node:stream";
6
+ import * as acp from "@agentclientprotocol/sdk";
7
+ import { buildSpawnFailureMessage } from "./server-helpers.js";
8
+ export const acpSessionTestables = {
9
+ spawnAgent(agentBinPath, options) {
10
+ return childProcess.spawn(agentBinPath, [], options);
11
+ },
12
+ };
13
+ export class ClaudeAcpSession {
14
+ workspacePath;
15
+ agentBinPath;
16
+ claudeBin;
17
+ onEvent;
18
+ pendingPermissions = new Map();
19
+ pendingQuestions = new Map();
20
+ agentProcess = null;
21
+ connection = null;
22
+ sessionId = null;
23
+ activePrompt = false;
24
+ disposing = false;
25
+ connectPromise = null;
26
+ sessionPromise = null;
27
+ currentModeId = null;
28
+ drainTimer = null;
29
+ drainResolve = null;
30
+ static DRAIN_QUIET_MS = 200;
31
+ constructor(options) {
32
+ this.workspacePath = options.workspacePath;
33
+ this.agentBinPath = options.agentBinPath;
34
+ this.claudeBin = options.claudeBin;
35
+ this.onEvent = options.onEvent;
36
+ }
37
+ async connect() {
38
+ if (this.connectPromise) {
39
+ await this.connectPromise;
40
+ return;
41
+ }
42
+ if (this.connection) {
43
+ this.emitReady();
44
+ return;
45
+ }
46
+ this.connectPromise = (async () => {
47
+ await mkdir(this.workspacePath, { recursive: true });
48
+ const agentEnv = {
49
+ ...process.env,
50
+ ...(this.claudeBin ? { CLAUDE_CODE_EXECUTABLE: this.claudeBin } : undefined),
51
+ };
52
+ try {
53
+ this.agentProcess = acpSessionTestables.spawnAgent(this.agentBinPath, {
54
+ cwd: this.workspacePath,
55
+ env: agentEnv,
56
+ stdio: ["pipe", "pipe", "pipe"],
57
+ });
58
+ }
59
+ catch (error) {
60
+ throw new Error(this.buildAgentSpawnFailureMessage(error));
61
+ }
62
+ let startupComplete = false;
63
+ let processHandled = false;
64
+ let rejectStartup = null;
65
+ const startupFailure = new Promise((_, reject) => {
66
+ rejectStartup = reject;
67
+ });
68
+ const handleAgentFailure = (error) => {
69
+ if (processHandled) {
70
+ return;
71
+ }
72
+ processHandled = true;
73
+ this.connection = null;
74
+ this.sessionId = null;
75
+ this.sessionPromise = null;
76
+ this.connectPromise = null;
77
+ this.currentModeId = null;
78
+ this.activePrompt = false;
79
+ this.clearDrain();
80
+ this.rejectPendingPermissions(new Error("Permission request cancelled because ACP agent stopped."));
81
+ this.rejectPendingQuestions(new Error("Question cancelled because ACP agent stopped."));
82
+ this.agentProcess = null;
83
+ if (this.disposing) {
84
+ this.disposing = false;
85
+ if (!startupComplete) {
86
+ rejectStartup?.(error);
87
+ rejectStartup = null;
88
+ }
89
+ return;
90
+ }
91
+ if (!startupComplete) {
92
+ rejectStartup?.(error);
93
+ rejectStartup = null;
94
+ return;
95
+ }
96
+ this.onEvent({
97
+ type: "error",
98
+ payload: { message: error.message, fatal: true },
99
+ });
100
+ };
101
+ this.agentProcess.stderr.on("data", (chunk) => {
102
+ const message = chunk.toString().trim();
103
+ if (!message || this.shouldIgnoreAgentStderr(message) || this.shouldIgnoreToolOutputLog(message)) {
104
+ return;
105
+ }
106
+ this.onEvent({ type: "error", payload: { message, fatal: false } });
107
+ });
108
+ this.agentProcess.on("error", (error) => {
109
+ handleAgentFailure(new Error(this.buildAgentSpawnFailureMessage(error)));
110
+ });
111
+ this.agentProcess.on("exit", (code, signal) => {
112
+ handleAgentFailure(new Error(`Claude ACP agent exited (${code ?? "null"} / ${signal ?? "null"}).`));
113
+ });
114
+ const input = Writable.toWeb(this.agentProcess.stdin);
115
+ const output = Readable.toWeb(this.agentProcess.stdout);
116
+ const stream = acp.ndJsonStream(input, output);
117
+ const client = {
118
+ requestPermission: async (params) => this.handlePermissionRequest(params),
119
+ sessionUpdate: async (params) => {
120
+ const update = params.update;
121
+ if (update.sessionUpdate === "current_mode_update" && typeof update.currentModeId === "string") {
122
+ this.currentModeId = update.currentModeId;
123
+ }
124
+ this.onEvent({ type: "session_update", payload: params.update });
125
+ this.resetDrainTimer();
126
+ },
127
+ readTextFile: async (params) => this.handleReadTextFile(params),
128
+ writeTextFile: async (params) => this.handleWriteTextFile(params),
129
+ extMethod: async (method, params) => this.handleExtMethod(method, params),
130
+ };
131
+ this.connection = new acp.ClientSideConnection(() => client, stream);
132
+ await Promise.race([
133
+ this.connection.initialize({
134
+ protocolVersion: acp.PROTOCOL_VERSION,
135
+ clientCapabilities: {
136
+ fs: { readTextFile: true, writeTextFile: true },
137
+ _meta: {
138
+ extensions: [
139
+ {
140
+ method: "leduo/ask_question",
141
+ description: "Ask the user a question with optional multiple-choice options. " +
142
+ "Params: { question: string, options?: Array<{ id: string, label: string }>, allowCustomAnswer?: boolean }. " +
143
+ "Returns: { answer: string }.",
144
+ },
145
+ ],
146
+ },
147
+ },
148
+ }),
149
+ startupFailure,
150
+ ]);
151
+ startupComplete = true;
152
+ rejectStartup = null;
153
+ this.emitReady();
154
+ })();
155
+ try {
156
+ await this.connectPromise;
157
+ }
158
+ catch (error) {
159
+ this.connectPromise = null;
160
+ throw error;
161
+ }
162
+ }
163
+ async ensureSession() {
164
+ if (this.sessionPromise) {
165
+ return await this.sessionPromise;
166
+ }
167
+ if (!this.connection) {
168
+ await this.connect();
169
+ }
170
+ if (this.sessionId || !this.connection) {
171
+ return this.sessionId;
172
+ }
173
+ this.sessionPromise = (async () => {
174
+ const response = await this.connection.newSession({
175
+ cwd: this.workspacePath,
176
+ mcpServers: [],
177
+ });
178
+ this.sessionId = response.sessionId;
179
+ this.currentModeId = null;
180
+ this.onEvent({
181
+ type: "session_created",
182
+ payload: {
183
+ sessionId: response.sessionId,
184
+ modes: response.modes?.availableModes.map((mode) => mode.id) ?? [],
185
+ configOptions: response.configOptions ?? [],
186
+ },
187
+ });
188
+ return this.sessionId;
189
+ })();
190
+ try {
191
+ return await this.sessionPromise;
192
+ }
193
+ catch (error) {
194
+ this.sessionPromise = null;
195
+ throw error;
196
+ }
197
+ }
198
+ async loadSession(existingSessionId) {
199
+ if (!this.connection) {
200
+ await this.connect();
201
+ }
202
+ if (!this.connection) {
203
+ throw new Error("ACP connection is not available.");
204
+ }
205
+ this.sessionId = existingSessionId;
206
+ this.sessionPromise = Promise.resolve(existingSessionId);
207
+ const response = await this.connection.loadSession({
208
+ sessionId: existingSessionId,
209
+ cwd: this.workspacePath,
210
+ mcpServers: [],
211
+ });
212
+ this.currentModeId = response.modes?.currentModeId ?? null;
213
+ this.onEvent({
214
+ type: "session_restored",
215
+ payload: {
216
+ sessionId: existingSessionId,
217
+ modes: response.modes?.availableModes.map((mode) => mode.id) ?? [],
218
+ configOptions: response.configOptions ?? [],
219
+ },
220
+ });
221
+ return existingSessionId;
222
+ }
223
+ async findRestorableSession(preferredSessionId) {
224
+ if (!this.connection) {
225
+ await this.connect();
226
+ }
227
+ if (!this.connection) {
228
+ throw new Error("ACP connection is not available.");
229
+ }
230
+ const response = await this.connection.unstable_listSessions({
231
+ cwd: this.workspacePath,
232
+ });
233
+ if (preferredSessionId) {
234
+ const exactMatch = response.sessions.find((session) => session.sessionId === preferredSessionId);
235
+ return exactMatch?.sessionId ?? null;
236
+ }
237
+ return response.sessions[0]?.sessionId ?? null;
238
+ }
239
+ async prompt(text, images) {
240
+ const sessionId = await this.ensureSession();
241
+ if (!this.connection || !sessionId) {
242
+ throw new Error("ACP session is not available.");
243
+ }
244
+ if (this.activePrompt) {
245
+ throw new Error("Another Claude prompt is still running.");
246
+ }
247
+ this.activePrompt = true;
248
+ const promptId = randomUUID();
249
+ this.onEvent({ type: "prompt_started", payload: { promptId, text, images } });
250
+ try {
251
+ const promptContent = [];
252
+ if (images && images.length > 0) {
253
+ for (const img of images) {
254
+ promptContent.push({ type: "image", data: img.data, mimeType: img.mimeType });
255
+ }
256
+ }
257
+ promptContent.push({ type: "text", text });
258
+ const response = await this.connection.prompt({
259
+ sessionId,
260
+ messageId: randomUUID(),
261
+ prompt: promptContent,
262
+ });
263
+ await this.waitForDrain();
264
+ this.onEvent({
265
+ type: "prompt_finished",
266
+ payload: { promptId, stopReason: response.stopReason },
267
+ });
268
+ }
269
+ finally {
270
+ this.activePrompt = false;
271
+ }
272
+ }
273
+ async setMode(modeId) {
274
+ const sessionId = await this.ensureSession();
275
+ if (!this.connection || !sessionId || !modeId || this.currentModeId === modeId) {
276
+ return;
277
+ }
278
+ await this.connection.setSessionMode({
279
+ sessionId,
280
+ modeId,
281
+ });
282
+ this.currentModeId = modeId;
283
+ }
284
+ async cancel() {
285
+ if (!this.connection || !this.sessionId || !this.activePrompt) {
286
+ return;
287
+ }
288
+ await this.connection.cancel({ sessionId: this.sessionId });
289
+ }
290
+ async resolvePermission(requestId, optionId, note) {
291
+ const pending = this.pendingPermissions.get(requestId);
292
+ if (!pending) {
293
+ throw new Error("Permission request was not found or already resolved.");
294
+ }
295
+ pending.resolve({
296
+ outcome: {
297
+ outcome: "selected",
298
+ optionId,
299
+ _meta: note && note.trim() ? { note: note.trim() } : undefined,
300
+ },
301
+ });
302
+ this.pendingPermissions.delete(requestId);
303
+ this.onEvent({ type: "permission_resolved", payload: { requestId, optionId } });
304
+ }
305
+ async answerQuestion(questionId, answer) {
306
+ const pending = this.pendingQuestions.get(questionId);
307
+ if (!pending) {
308
+ throw new Error("Question was not found or already answered.");
309
+ }
310
+ pending.resolve({ answer });
311
+ this.pendingQuestions.delete(questionId);
312
+ this.onEvent({ type: "question_answered", payload: { questionId, answer } });
313
+ }
314
+ async dispose() {
315
+ this.disposing = true;
316
+ this.clearDrain();
317
+ this.rejectPendingPermissions(new Error("Client disconnected."));
318
+ this.rejectPendingQuestions(new Error("Client disconnected."));
319
+ if (this.agentProcess && !this.agentProcess.killed) {
320
+ this.agentProcess.kill();
321
+ }
322
+ this.agentProcess = null;
323
+ this.connection = null;
324
+ this.sessionId = null;
325
+ this.sessionPromise = null;
326
+ this.connectPromise = null;
327
+ this.currentModeId = null;
328
+ this.activePrompt = false;
329
+ }
330
+ emitReady() {
331
+ this.onEvent({
332
+ type: "ready",
333
+ payload: { workspacePath: this.workspacePath, agentConnected: true },
334
+ });
335
+ }
336
+ buildAgentSpawnFailureMessage(error) {
337
+ const hint = (error instanceof Error
338
+ && "code" in error
339
+ && error.code === "EAGAIN")
340
+ ? "The OS temporarily refused to start a new process (EAGAIN). Try again and check system process/thread limits or other running agent processes."
341
+ : "Check that LEDUO_PATROL_AGENT_BIN points to a valid ACP agent, or that the bundled claude-code-acp agent is executable.";
342
+ return buildSpawnFailureMessage("Claude ACP agent", this.agentBinPath, this.workspacePath, error, hint);
343
+ }
344
+ waitForDrain() {
345
+ return new Promise((resolve) => {
346
+ this.drainResolve = resolve;
347
+ this.drainTimer = setTimeout(() => {
348
+ this.drainResolve = null;
349
+ this.drainTimer = null;
350
+ resolve();
351
+ }, ClaudeAcpSession.DRAIN_QUIET_MS);
352
+ });
353
+ }
354
+ resetDrainTimer() {
355
+ if (this.drainTimer && this.drainResolve) {
356
+ clearTimeout(this.drainTimer);
357
+ const resolve = this.drainResolve;
358
+ this.drainTimer = setTimeout(() => {
359
+ this.drainResolve = null;
360
+ this.drainTimer = null;
361
+ resolve();
362
+ }, ClaudeAcpSession.DRAIN_QUIET_MS);
363
+ }
364
+ }
365
+ clearDrain() {
366
+ if (this.drainTimer) {
367
+ clearTimeout(this.drainTimer);
368
+ this.drainTimer = null;
369
+ }
370
+ if (this.drainResolve) {
371
+ this.drainResolve();
372
+ this.drainResolve = null;
373
+ }
374
+ }
375
+ shouldIgnoreAgentStderr(message) {
376
+ return (message.includes("Error handling notification") &&
377
+ message.includes("method: 'session/update'") &&
378
+ message.includes("message: 'Invalid params'")) || isMissingPostToolHookMessage(message) || message.includes("<local-command-stdout>");
379
+ }
380
+ shouldIgnoreToolOutputLog(message) {
381
+ const normalized = message.trim();
382
+ return normalized.startsWith('[{"index":') || normalized.startsWith("[{\"index\":");
383
+ }
384
+ async handlePermissionRequest(params) {
385
+ const requestId = randomUUID();
386
+ this.onEvent({
387
+ type: "permission_requested",
388
+ payload: {
389
+ requestId,
390
+ toolCall: params.toolCall,
391
+ options: params.options,
392
+ },
393
+ });
394
+ return await new Promise((resolve, reject) => {
395
+ this.pendingPermissions.set(requestId, { resolve, reject });
396
+ });
397
+ }
398
+ rejectPendingPermissions(reason) {
399
+ for (const pending of this.pendingPermissions.values()) {
400
+ pending.reject(reason);
401
+ }
402
+ this.pendingPermissions.clear();
403
+ }
404
+ rejectPendingQuestions(reason) {
405
+ for (const pending of this.pendingQuestions.values()) {
406
+ pending.reject(reason);
407
+ }
408
+ this.pendingQuestions.clear();
409
+ }
410
+ async handleExtMethod(method, params) {
411
+ if (method === "leduo/ask_question") {
412
+ return await this.handleAskQuestion(params);
413
+ }
414
+ throw new Error(`Unknown extension method: ${method}`);
415
+ }
416
+ async handleAskQuestion(params) {
417
+ const questionId = randomUUID();
418
+ const question = typeof params.question === "string" ? params.question : "";
419
+ const rawOptions = Array.isArray(params.options) ? params.options : [];
420
+ const options = rawOptions
421
+ .map((opt) => {
422
+ if (opt && typeof opt === "object" && !Array.isArray(opt)) {
423
+ const record = opt;
424
+ return {
425
+ id: typeof record.id === "string" ? record.id : "",
426
+ label: typeof record.label === "string" ? record.label : "",
427
+ };
428
+ }
429
+ return null;
430
+ })
431
+ .filter((opt) => opt !== null && opt.id !== "" && opt.label !== "");
432
+ const allowCustomAnswer = params.allowCustomAnswer === true;
433
+ this.onEvent({
434
+ type: "question_requested",
435
+ payload: { questionId, question, options, allowCustomAnswer },
436
+ });
437
+ return await new Promise((resolve, reject) => {
438
+ this.pendingQuestions.set(questionId, { resolve, reject });
439
+ });
440
+ }
441
+ resolveWorkspacePath(targetPath) {
442
+ const absolutePath = path.resolve(this.workspacePath, targetPath);
443
+ const relativePath = path.relative(this.workspacePath, absolutePath);
444
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
445
+ throw new Error(`Refusing to access file outside workspace: ${targetPath}`);
446
+ }
447
+ return absolutePath;
448
+ }
449
+ async handleReadTextFile(params) {
450
+ const filePath = path.isAbsolute(params.path)
451
+ ? params.path
452
+ : this.resolveWorkspacePath(params.path);
453
+ const content = await readFile(filePath, "utf8");
454
+ if (params.line != null || params.limit != null) {
455
+ const lines = content.split("\n");
456
+ const offset = (params.line ?? 1) - 1;
457
+ const limit = params.limit ?? lines.length;
458
+ const start = Math.max(0, offset);
459
+ const end = Math.min(lines.length, start + limit);
460
+ return { content: lines.slice(start, end).join("\n") };
461
+ }
462
+ return { content };
463
+ }
464
+ async handleWriteTextFile(params) {
465
+ const filePath = path.isAbsolute(params.path)
466
+ ? params.path
467
+ : this.resolveWorkspacePath(params.path);
468
+ const dirName = path.dirname(filePath);
469
+ await mkdir(dirName, { recursive: true });
470
+ await writeFile(filePath, params.content, "utf8");
471
+ return {};
472
+ }
473
+ }
474
+ function isMissingPostToolHookMessage(message) {
475
+ return message.includes("No onPostToolUseHook found for tool use ID:");
476
+ }
@@ -10,6 +10,15 @@ const POLL_INTERVAL_MS = 2000;
10
10
  const ACTIVITY_TYPES = new Set(["assistant", "user", "progress"]);
11
11
  /** Types to skip when scanning for the last meaningful entry. */
12
12
  const SKIP_TYPES = new Set(["last-prompt", "system", "file-history-snapshot", "queue-operation"]);
13
+ function extractAssistantContentTypes(entry) {
14
+ const content = entry.message?.content;
15
+ if (!Array.isArray(content)) {
16
+ return [];
17
+ }
18
+ return content
19
+ .map((block) => (block && typeof block === "object" ? block.type : undefined))
20
+ .filter((type) => typeof type === "string");
21
+ }
13
22
  /**
14
23
  * Detect if a user entry represents a local CLI command (not a real user message to Claude).
15
24
  * Local commands have content containing `<command-name>`, `<local-command-stdout>`, or `<local-command-caveat>`.
@@ -58,9 +67,10 @@ export function detectClearCommand(entry) {
58
67
  * Given a parsed JSONL entry, return the activity state.
59
68
  *
60
69
  * Rules:
61
- * assistant + no stop_reason (null / undefined) running
62
- * assistant + stop_reason "tool_use" pending
63
- * assistant + stop_reason "end_turn"|"stop_sequence" → completed
70
+ * assistant + tool_use content / stop_reason pending
71
+ * assistant + thinking content only running
72
+ * assistant + text content only → completed
73
+ * assistant + explicit terminal stop_reason → completed
64
74
  * user (local command / meta) → completed
65
75
  * user (real message) → running
66
76
  * system + subtype "local_command" → completed
@@ -70,13 +80,18 @@ export function detectClearCommand(entry) {
70
80
  export function determineActivityState(entry) {
71
81
  const { type } = entry;
72
82
  if (type === "assistant") {
83
+ const contentTypes = extractAssistantContentTypes(entry);
73
84
  const stopReason = entry.message?.stop_reason;
74
- if (stopReason == null)
75
- return "running";
76
- if (stopReason === "tool_use")
85
+ if (stopReason === "tool_use" || contentTypes.includes("tool_use"))
77
86
  return "pending";
87
+ if (stopReason != null)
88
+ return "completed";
89
+ if (contentTypes.includes("thinking"))
90
+ return "running";
91
+ if (contentTypes.includes("text"))
92
+ return "completed";
78
93
  // end_turn, stop_sequence, max_tokens, etc. – treat as completed
79
- return "completed";
94
+ return "running";
80
95
  }
81
96
  if (type === "user") {
82
97
  // Local CLI commands (/mcp, /status, /clear, etc.) are already finished
@@ -2,6 +2,7 @@
2
2
  import express from "express";
3
3
  import { access, readdir } from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { createRequire } from "node:module";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { createServer } from "node:http";
7
8
  import { userInfo } from "node:os";
@@ -35,6 +36,7 @@ const listenHost = bindMode === "local" ? "127.0.0.1" : "0.0.0.0";
35
36
  const launchHost = bindMode === "local" ? "127.0.0.1" : pickPreferredLanIp();
36
37
  const launchUser = userInfo().username;
37
38
  const claudeBin = process.env.LEDUO_PATROL_CLAUDE_BIN?.trim() || undefined;
39
+ const agentBinPath = resolveAgentBinPath();
38
40
  const accessKey = await resolveAccessKey();
39
41
  const enableShell = parseBooleanFlag(process.env.LEDUO_ENABLE_SHELL, true);
40
42
  const allowSkipPermissions = process.env.LEDUO_PATROL_ALLOW_SKIP_PERMISSIONS === "true";
@@ -44,6 +46,7 @@ const wss = new WebSocketServer({ server, path: "/ws" });
44
46
  const sessionManager = new SessionManager({
45
47
  allowedRoots,
46
48
  claudeBin,
49
+ agentBinPath,
47
50
  allowSkipPermissions,
48
51
  });
49
52
  await sessionManager.initialize();
@@ -87,11 +90,29 @@ app.get("/api/config", (_req, res) => {
87
90
  launchHost,
88
91
  launchUser,
89
92
  allowSkipPermissions,
93
+ availableSessionEngines: sessionManager.getAvailableEngines(),
94
+ defaultSessionEngine: "cli",
90
95
  });
91
96
  });
92
97
  app.get("/api/state", (_req, res) => {
93
98
  res.json(sessionManager.getStateSnapshot());
94
99
  });
100
+ app.get("/api/session-history", (req, res) => {
101
+ try {
102
+ const clientSessionId = typeof req.query.clientSessionId === "string" ? req.query.clientSessionId : "";
103
+ const before = Number(req.query.before ?? 0);
104
+ const limit = Number(req.query.limit ?? 120);
105
+ if (!clientSessionId) {
106
+ throw new Error("clientSessionId is required");
107
+ }
108
+ res.json(sessionManager.getSessionHistory(clientSessionId, before, limit));
109
+ }
110
+ catch (error) {
111
+ res.status(400).json({
112
+ message: formatError(error),
113
+ });
114
+ }
115
+ });
95
116
  app.get("/api/session-diff/files", async (req, res) => {
96
117
  try {
97
118
  const clientSessionId = typeof req.query.clientSessionId === "string" ? req.query.clientSessionId : "";
@@ -197,7 +218,28 @@ wss.on("connection", (socket, request) => {
197
218
  });
198
219
  break;
199
220
  case "create_session":
200
- await sessionManager.createSession(message.payload.workspacePath, message.payload.title, message.payload.allowSkipPermissions);
221
+ await sessionManager.createSession(message.payload.workspacePath, message.payload.title, message.payload.allowSkipPermissions, message.payload.engine ?? "cli");
222
+ break;
223
+ case "switch_engine":
224
+ await sessionManager.switchEngine(message.payload.clientSessionId, message.payload.engine);
225
+ break;
226
+ case "prompt":
227
+ await sessionManager.prompt(message.payload.clientSessionId, message.payload.text, message.payload.modeId, message.payload.images);
228
+ break;
229
+ case "set_mode":
230
+ await sessionManager.setSessionMode(message.payload.clientSessionId, message.payload.modeId);
231
+ break;
232
+ case "cancel":
233
+ await sessionManager.cancel(message.payload.clientSessionId);
234
+ break;
235
+ case "remove_queued_prompt":
236
+ await sessionManager.removeQueuedPrompt(message.payload.clientSessionId, message.payload.promptId);
237
+ break;
238
+ case "permission":
239
+ await sessionManager.resolvePermission(message.payload.clientSessionId, message.payload.requestId, message.payload.optionId, message.payload.note);
240
+ break;
241
+ case "answer_question":
242
+ await sessionManager.answerQuestion(message.payload.clientSessionId, message.payload.questionId, message.payload.answer);
201
243
  break;
202
244
  case "close_session":
203
245
  await sessionManager.closeSession(message.payload.clientSessionId);
@@ -331,3 +373,17 @@ function parseBooleanFlag(rawValue, defaultValue) {
331
373
  }
332
374
  return defaultValue;
333
375
  }
376
+ function resolveAgentBinPath() {
377
+ if (process.env.LEDUO_PATROL_AGENT_BIN?.trim()) {
378
+ return process.env.LEDUO_PATROL_AGENT_BIN.trim();
379
+ }
380
+ const require = createRequire(import.meta.url);
381
+ try {
382
+ const pkgPath = require.resolve("@zed-industries/claude-code-acp/package.json");
383
+ const pkgDir = path.dirname(pkgPath);
384
+ return path.join(pkgDir, "dist", "index.js");
385
+ }
386
+ catch {
387
+ return undefined;
388
+ }
389
+ }