leduo-patrol 2.0.1 → 2.2.3
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/README.md +27 -4
- package/dist/server/__tests__/access-key-prompt.test.js +80 -0
- package/dist/server/__tests__/acp-session.test.js +92 -0
- package/dist/server/__tests__/activity-monitor.test.js +13 -1
- package/dist/server/__tests__/claude-cli-session.test.js +17 -0
- package/dist/server/__tests__/pty-runtime.test.js +28 -0
- package/dist/server/__tests__/session-manager.test.js +215 -1
- package/dist/server/access-key-prompt.js +84 -0
- package/dist/server/acp-session.js +476 -0
- package/dist/server/activity-monitor.js +22 -7
- package/dist/server/claude-cli-session.js +57 -12
- package/dist/server/index.js +104 -21
- package/dist/server/launch-mode.js +4 -22
- package/dist/server/pty-runtime.js +28 -0
- package/dist/server/session-manager.js +1117 -121
- package/dist/server/shell-session.js +2 -0
- package/dist/server/startup-preferences.js +45 -0
- package/dist/web/assets/index-B-YXVUoQ.css +1 -0
- package/dist/web/assets/index-Bu0K7QgY.js +21 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -1
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/LICENSE +191 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/README.md +53 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.d.ts +823 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.js +965 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.test.d.ts +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.test.js +839 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.test.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/agent.d.ts +2 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/agent.js +225 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/agent.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/client.d.ts +2 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/client.js +130 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/client.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.d.ts +35 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js +5 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/index.d.ts +27 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/index.js +28 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/index.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts +2870 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.js +3 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.d.ts +5333 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js +1554 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/stream.d.ts +24 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/stream.js +64 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/stream.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/package.json +66 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/schema/schema.json +4125 -0
- package/vendor/claude-code-acp/node_modules/@types/node/LICENSE +21 -0
- package/vendor/claude-code-acp/node_modules/@types/node/README.md +15 -0
- package/vendor/claude-code-acp/node_modules/@types/node/assert/strict.d.ts +105 -0
- package/vendor/claude-code-acp/node_modules/@types/node/assert.d.ts +955 -0
- package/vendor/claude-code-acp/node_modules/@types/node/async_hooks.d.ts +623 -0
- package/vendor/claude-code-acp/node_modules/@types/node/buffer.buffer.d.ts +466 -0
- package/vendor/claude-code-acp/node_modules/@types/node/buffer.d.ts +1810 -0
- package/vendor/claude-code-acp/node_modules/@types/node/child_process.d.ts +1428 -0
- package/vendor/claude-code-acp/node_modules/@types/node/cluster.d.ts +486 -0
- package/vendor/claude-code-acp/node_modules/@types/node/compatibility/iterators.d.ts +21 -0
- package/vendor/claude-code-acp/node_modules/@types/node/console.d.ts +151 -0
- package/vendor/claude-code-acp/node_modules/@types/node/constants.d.ts +20 -0
- package/vendor/claude-code-acp/node_modules/@types/node/crypto.d.ts +4065 -0
- package/vendor/claude-code-acp/node_modules/@types/node/dgram.d.ts +564 -0
- package/vendor/claude-code-acp/node_modules/@types/node/diagnostics_channel.d.ts +576 -0
- package/vendor/claude-code-acp/node_modules/@types/node/dns/promises.d.ts +503 -0
- package/vendor/claude-code-acp/node_modules/@types/node/dns.d.ts +922 -0
- package/vendor/claude-code-acp/node_modules/@types/node/domain.d.ts +166 -0
- package/vendor/claude-code-acp/node_modules/@types/node/events.d.ts +1054 -0
- package/vendor/claude-code-acp/node_modules/@types/node/fs/promises.d.ts +1329 -0
- package/vendor/claude-code-acp/node_modules/@types/node/fs.d.ts +4676 -0
- package/vendor/claude-code-acp/node_modules/@types/node/globals.d.ts +150 -0
- package/vendor/claude-code-acp/node_modules/@types/node/globals.typedarray.d.ts +101 -0
- package/vendor/claude-code-acp/node_modules/@types/node/http.d.ts +2167 -0
- package/vendor/claude-code-acp/node_modules/@types/node/http2.d.ts +2480 -0
- package/vendor/claude-code-acp/node_modules/@types/node/https.d.ts +405 -0
- package/vendor/claude-code-acp/node_modules/@types/node/index.d.ts +115 -0
- package/vendor/claude-code-acp/node_modules/@types/node/inspector/promises.d.ts +41 -0
- package/vendor/claude-code-acp/node_modules/@types/node/inspector.d.ts +224 -0
- package/vendor/claude-code-acp/node_modules/@types/node/inspector.generated.d.ts +4226 -0
- package/vendor/claude-code-acp/node_modules/@types/node/module.d.ts +819 -0
- package/vendor/claude-code-acp/node_modules/@types/node/net.d.ts +933 -0
- package/vendor/claude-code-acp/node_modules/@types/node/os.d.ts +507 -0
- package/vendor/claude-code-acp/node_modules/@types/node/package.json +155 -0
- package/vendor/claude-code-acp/node_modules/@types/node/path/posix.d.ts +8 -0
- package/vendor/claude-code-acp/node_modules/@types/node/path/win32.d.ts +8 -0
- package/vendor/claude-code-acp/node_modules/@types/node/path.d.ts +187 -0
- package/vendor/claude-code-acp/node_modules/@types/node/perf_hooks.d.ts +643 -0
- package/vendor/claude-code-acp/node_modules/@types/node/process.d.ts +2161 -0
- package/vendor/claude-code-acp/node_modules/@types/node/punycode.d.ts +117 -0
- package/vendor/claude-code-acp/node_modules/@types/node/querystring.d.ts +152 -0
- package/vendor/claude-code-acp/node_modules/@types/node/quic.d.ts +910 -0
- package/vendor/claude-code-acp/node_modules/@types/node/readline/promises.d.ts +161 -0
- package/vendor/claude-code-acp/node_modules/@types/node/readline.d.ts +541 -0
- package/vendor/claude-code-acp/node_modules/@types/node/repl.d.ts +415 -0
- package/vendor/claude-code-acp/node_modules/@types/node/sea.d.ts +162 -0
- package/vendor/claude-code-acp/node_modules/@types/node/sqlite.d.ts +955 -0
- package/vendor/claude-code-acp/node_modules/@types/node/stream/consumers.d.ts +38 -0
- package/vendor/claude-code-acp/node_modules/@types/node/stream/promises.d.ts +211 -0
- package/vendor/claude-code-acp/node_modules/@types/node/stream/web.d.ts +296 -0
- package/vendor/claude-code-acp/node_modules/@types/node/stream.d.ts +1760 -0
- package/vendor/claude-code-acp/node_modules/@types/node/string_decoder.d.ts +67 -0
- package/vendor/claude-code-acp/node_modules/@types/node/test/reporters.d.ts +96 -0
- package/vendor/claude-code-acp/node_modules/@types/node/test.d.ts +2240 -0
- package/vendor/claude-code-acp/node_modules/@types/node/timers/promises.d.ts +108 -0
- package/vendor/claude-code-acp/node_modules/@types/node/timers.d.ts +159 -0
- package/vendor/claude-code-acp/node_modules/@types/node/tls.d.ts +1198 -0
- package/vendor/claude-code-acp/node_modules/@types/node/trace_events.d.ts +197 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +462 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/compatibility/float16array.d.ts +71 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +36 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/index.d.ts +117 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.7/compatibility/float16array.d.ts +72 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.7/index.d.ts +117 -0
- package/vendor/claude-code-acp/node_modules/@types/node/tty.d.ts +250 -0
- package/vendor/claude-code-acp/node_modules/@types/node/url.d.ts +519 -0
- package/vendor/claude-code-acp/node_modules/@types/node/util/types.d.ts +558 -0
- package/vendor/claude-code-acp/node_modules/@types/node/util.d.ts +1662 -0
- package/vendor/claude-code-acp/node_modules/@types/node/v8.d.ts +983 -0
- package/vendor/claude-code-acp/node_modules/@types/node/vm.d.ts +1208 -0
- package/vendor/claude-code-acp/node_modules/@types/node/wasi.d.ts +202 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/abortcontroller.d.ts +59 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/blob.d.ts +23 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/console.d.ts +9 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/crypto.d.ts +39 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/domexception.d.ts +68 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/encoding.d.ts +11 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/events.d.ts +106 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/fetch.d.ts +69 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/importmeta.d.ts +13 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/messaging.d.ts +23 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/navigator.d.ts +25 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/performance.d.ts +45 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/storage.d.ts +24 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/streams.d.ts +115 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/timers.d.ts +44 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/url.d.ts +24 -0
- package/vendor/claude-code-acp/node_modules/@types/node/worker_threads.d.ts +717 -0
- package/vendor/claude-code-acp/node_modules/@types/node/zlib.d.ts +618 -0
- package/vendor/claude-code-acp/node_modules/undici-types/LICENSE +21 -0
- package/vendor/claude-code-acp/node_modules/undici-types/README.md +6 -0
- package/vendor/claude-code-acp/node_modules/undici-types/agent.d.ts +32 -0
- package/vendor/claude-code-acp/node_modules/undici-types/api.d.ts +43 -0
- package/vendor/claude-code-acp/node_modules/undici-types/balanced-pool.d.ts +29 -0
- package/vendor/claude-code-acp/node_modules/undici-types/cache-interceptor.d.ts +172 -0
- package/vendor/claude-code-acp/node_modules/undici-types/cache.d.ts +36 -0
- package/vendor/claude-code-acp/node_modules/undici-types/client-stats.d.ts +15 -0
- package/vendor/claude-code-acp/node_modules/undici-types/client.d.ts +108 -0
- package/vendor/claude-code-acp/node_modules/undici-types/connector.d.ts +34 -0
- package/vendor/claude-code-acp/node_modules/undici-types/content-type.d.ts +21 -0
- package/vendor/claude-code-acp/node_modules/undici-types/cookies.d.ts +30 -0
- package/vendor/claude-code-acp/node_modules/undici-types/diagnostics-channel.d.ts +74 -0
- package/vendor/claude-code-acp/node_modules/undici-types/dispatcher.d.ts +276 -0
- package/vendor/claude-code-acp/node_modules/undici-types/env-http-proxy-agent.d.ts +22 -0
- package/vendor/claude-code-acp/node_modules/undici-types/errors.d.ts +161 -0
- package/vendor/claude-code-acp/node_modules/undici-types/eventsource.d.ts +66 -0
- package/vendor/claude-code-acp/node_modules/undici-types/fetch.d.ts +211 -0
- package/vendor/claude-code-acp/node_modules/undici-types/formdata.d.ts +108 -0
- package/vendor/claude-code-acp/node_modules/undici-types/global-dispatcher.d.ts +9 -0
- package/vendor/claude-code-acp/node_modules/undici-types/global-origin.d.ts +7 -0
- package/vendor/claude-code-acp/node_modules/undici-types/h2c-client.d.ts +73 -0
- package/vendor/claude-code-acp/node_modules/undici-types/handlers.d.ts +15 -0
- package/vendor/claude-code-acp/node_modules/undici-types/header.d.ts +160 -0
- package/vendor/claude-code-acp/node_modules/undici-types/index.d.ts +80 -0
- package/vendor/claude-code-acp/node_modules/undici-types/interceptors.d.ts +39 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-agent.d.ts +68 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-call-history.d.ts +111 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-client.d.ts +27 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-errors.d.ts +12 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-interceptor.d.ts +94 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-pool.d.ts +27 -0
- package/vendor/claude-code-acp/node_modules/undici-types/package.json +55 -0
- package/vendor/claude-code-acp/node_modules/undici-types/patch.d.ts +29 -0
- package/vendor/claude-code-acp/node_modules/undici-types/pool-stats.d.ts +19 -0
- package/vendor/claude-code-acp/node_modules/undici-types/pool.d.ts +41 -0
- package/vendor/claude-code-acp/node_modules/undici-types/proxy-agent.d.ts +29 -0
- package/vendor/claude-code-acp/node_modules/undici-types/readable.d.ts +68 -0
- package/vendor/claude-code-acp/node_modules/undici-types/retry-agent.d.ts +8 -0
- package/vendor/claude-code-acp/node_modules/undici-types/retry-handler.d.ts +125 -0
- package/vendor/claude-code-acp/node_modules/undici-types/snapshot-agent.d.ts +109 -0
- package/vendor/claude-code-acp/node_modules/undici-types/util.d.ts +18 -0
- package/vendor/claude-code-acp/node_modules/undici-types/utility.d.ts +7 -0
- package/vendor/claude-code-acp/node_modules/undici-types/webidl.d.ts +341 -0
- package/vendor/claude-code-acp/node_modules/undici-types/websocket.d.ts +186 -0
- package/dist/web/assets/index-B0sSFjwT.css +0 -1
- package/dist/web/assets/index-Cdb0JMLq.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 } });
|
|
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 +
|
|
62
|
-
* assistant +
|
|
63
|
-
* assistant +
|
|
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
|
|
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 "
|
|
94
|
+
return "running";
|
|
80
95
|
}
|
|
81
96
|
if (type === "user") {
|
|
82
97
|
// Local CLI commands (/mcp, /status, /clear, etc.) are already finished
|
|
@@ -3,6 +3,7 @@ import { EventEmitter } from "node:events";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { buildSpawnFailureMessage, ensureDirectoryExistsSync } from "./server-helpers.js";
|
|
6
|
+
import { ensureNodePtySpawnHelperExecutable } from "./pty-runtime.js";
|
|
6
7
|
/**
|
|
7
8
|
* A PTY-backed session that runs the native Claude Code CLI.
|
|
8
9
|
*
|
|
@@ -19,6 +20,7 @@ export class ClaudeCliSession extends EventEmitter {
|
|
|
19
20
|
super();
|
|
20
21
|
this.sessionId = opts.sessionId;
|
|
21
22
|
ensureDirectoryExistsSync(opts.workspacePath, "Session workspace");
|
|
23
|
+
ensureNodePtySpawnHelperExecutable();
|
|
22
24
|
const bin = resolveClaudeBin(opts.claudeBin);
|
|
23
25
|
const args = opts.resume
|
|
24
26
|
? ["--resume", opts.sessionId]
|
|
@@ -26,21 +28,24 @@ export class ClaudeCliSession extends EventEmitter {
|
|
|
26
28
|
if (opts.allowSkipPermissions) {
|
|
27
29
|
args.push("--allow-dangerously-skip-permissions");
|
|
28
30
|
}
|
|
31
|
+
const env = {
|
|
32
|
+
...process.env,
|
|
33
|
+
TERM: "xterm-256color",
|
|
34
|
+
COLORTERM: "truecolor",
|
|
35
|
+
};
|
|
29
36
|
try {
|
|
30
|
-
this.pty =
|
|
31
|
-
name: "xterm-256color",
|
|
32
|
-
cols: opts.cols ?? 80,
|
|
33
|
-
rows: opts.rows ?? 24,
|
|
34
|
-
cwd: opts.workspacePath,
|
|
35
|
-
env: {
|
|
36
|
-
...process.env,
|
|
37
|
-
TERM: "xterm-256color",
|
|
38
|
-
COLORTERM: "truecolor",
|
|
39
|
-
},
|
|
40
|
-
});
|
|
37
|
+
this.pty = spawnClaudePty(buildDirectClaudeLaunch(bin, args), opts.workspacePath, opts.cols ?? 80, opts.rows ?? 24, env);
|
|
41
38
|
}
|
|
42
39
|
catch (error) {
|
|
43
|
-
|
|
40
|
+
if (!shouldRetryClaudeSpawnWithShell(error)) {
|
|
41
|
+
throw new Error(buildSpawnFailureMessage("Claude CLI", bin, opts.workspacePath, error, "Check that the command is installed correctly, or override it with LEDUO_PATROL_CLAUDE_BIN."));
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
this.pty = spawnClaudePty(buildShellWrappedClaudeLaunch(bin, args), opts.workspacePath, opts.cols ?? 80, opts.rows ?? 24, env);
|
|
45
|
+
}
|
|
46
|
+
catch (fallbackError) {
|
|
47
|
+
throw new Error(buildSpawnFailureMessage("Claude CLI", bin, opts.workspacePath, fallbackError, "Direct PTY spawn also failed, even after retrying through a shell wrapper. Check that Claude Code is installed correctly, or override it with LEDUO_PATROL_CLAUDE_BIN."));
|
|
48
|
+
}
|
|
44
49
|
}
|
|
45
50
|
this.pty.onData((data) => {
|
|
46
51
|
this.emit("output", data);
|
|
@@ -70,6 +75,42 @@ export class ClaudeCliSession extends EventEmitter {
|
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
}
|
|
78
|
+
function spawnClaudePty(launch, workspacePath, cols, rows, env) {
|
|
79
|
+
return spawn(launch.command, launch.args, {
|
|
80
|
+
name: "xterm-256color",
|
|
81
|
+
cols,
|
|
82
|
+
rows,
|
|
83
|
+
cwd: workspacePath,
|
|
84
|
+
env,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function buildDirectClaudeLaunch(bin, args) {
|
|
88
|
+
return {
|
|
89
|
+
command: bin,
|
|
90
|
+
args,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function buildShellWrappedClaudeLaunch(bin, args, shellExists = existsSync) {
|
|
94
|
+
return {
|
|
95
|
+
command: resolveClaudeWrapperShell(shellExists),
|
|
96
|
+
args: ["-c", 'exec "$0" "$@"', bin, ...args],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function resolveClaudeWrapperShell(shellExists = existsSync) {
|
|
100
|
+
const candidates = ["/bin/sh", "/bin/bash", "/bin/zsh", "/usr/bin/sh"];
|
|
101
|
+
for (const candidate of candidates) {
|
|
102
|
+
if (shellExists(candidate)) {
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new Error("No compatible shell was found for Claude CLI fallback launch.");
|
|
107
|
+
}
|
|
108
|
+
function shouldRetryClaudeSpawnWithShell(error) {
|
|
109
|
+
if (process.platform === "win32") {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return error instanceof Error && /posix_spawnp failed/i.test(error.message);
|
|
113
|
+
}
|
|
73
114
|
function findExecutableOnPath(command, envPath = process.env.PATH) {
|
|
74
115
|
if (!envPath)
|
|
75
116
|
return null;
|
|
@@ -102,6 +143,10 @@ function resolveClaudeBin(configuredBin, env = process.env) {
|
|
|
102
143
|
throw new Error(`Claude CLI "${candidate}" was not found in PATH. Install Claude Code first, or set LEDUO_PATROL_CLAUDE_BIN=/absolute/path/to/claude.`);
|
|
103
144
|
}
|
|
104
145
|
export const claudeCliSessionTestables = {
|
|
146
|
+
buildDirectClaudeLaunch,
|
|
147
|
+
buildShellWrappedClaudeLaunch,
|
|
105
148
|
findExecutableOnPath,
|
|
106
149
|
resolveClaudeBin,
|
|
150
|
+
resolveClaudeWrapperShell,
|
|
151
|
+
shouldRetryClaudeSpawnWithShell,
|
|
107
152
|
};
|