@sudocode-ai/claude-code-acp 0.12.9 → 0.13.1
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 +3 -9
- package/dist/acp-agent.d.ts +194 -0
- package/dist/acp-agent.d.ts.map +1 -0
- package/dist/acp-agent.js +371 -29
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lib.d.ts +7 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/mcp-server.d.ts +21 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +214 -158
- package/dist/settings.d.ts +123 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/tools.d.ts +50 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +62 -39
- package/dist/utils.d.ts +32 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +22 -12
- package/dist/tests/acp-agent.test.js +0 -753
- package/dist/tests/extract-lines.test.js +0 -79
- package/dist/tests/fork-session.test.js +0 -83
- package/dist/tests/replace-and-calculate-location.test.js +0 -266
- package/dist/tests/settings.test.js +0 -462
package/dist/acp-agent.js
CHANGED
|
@@ -20,7 +20,6 @@ export class ClaudeAcpAgent {
|
|
|
20
20
|
this.sessions = {};
|
|
21
21
|
this.client = client;
|
|
22
22
|
this.toolUseCache = {};
|
|
23
|
-
this.fileContentCache = {};
|
|
24
23
|
this.logger = logger ?? console;
|
|
25
24
|
}
|
|
26
25
|
async initialize(request) {
|
|
@@ -55,6 +54,7 @@ export class ClaudeAcpAgent {
|
|
|
55
54
|
},
|
|
56
55
|
sessionCapabilities: {
|
|
57
56
|
fork: {},
|
|
57
|
+
resume: {},
|
|
58
58
|
},
|
|
59
59
|
loadSession: true,
|
|
60
60
|
},
|
|
@@ -82,16 +82,233 @@ export class ClaudeAcpAgent {
|
|
|
82
82
|
* Named unstable_forkSession to match SDK expectations (session/fork routes to this method).
|
|
83
83
|
*/
|
|
84
84
|
async unstable_forkSession(params) {
|
|
85
|
-
//
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
// Get the session directory to track new files
|
|
86
|
+
const sessionDir = this.getSessionDirPath(params.cwd);
|
|
87
|
+
const beforeFiles = new Set(fs.existsSync(sessionDir)
|
|
88
|
+
? fs.readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"))
|
|
89
|
+
: []);
|
|
90
|
+
const result = await this.createSession({
|
|
91
|
+
cwd: params.cwd,
|
|
92
|
+
mcpServers: params.mcpServers ?? [],
|
|
90
93
|
_meta: params._meta,
|
|
91
94
|
}, {
|
|
92
95
|
resume: params.sessionId,
|
|
93
96
|
forkSession: true,
|
|
94
97
|
});
|
|
98
|
+
// Wait briefly for CLI to create the session file
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
100
|
+
// Find the CLI-assigned session ID by looking for new session files
|
|
101
|
+
const cliSessionId = await this.discoverCliSessionId(sessionDir, beforeFiles, result.sessionId);
|
|
102
|
+
if (cliSessionId && cliSessionId !== result.sessionId) {
|
|
103
|
+
// Check if the CLI assigned a non-UUID session ID (e.g., "agent-xxx")
|
|
104
|
+
// If so, we need to extract the internal sessionId from the file
|
|
105
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cliSessionId);
|
|
106
|
+
if (!isUuid) {
|
|
107
|
+
// Read the session file to extract the internal sessionId
|
|
108
|
+
const oldFilePath = path.join(sessionDir, `${cliSessionId}.jsonl`);
|
|
109
|
+
const internalSessionId = this.extractInternalSessionId(oldFilePath);
|
|
110
|
+
if (internalSessionId) {
|
|
111
|
+
this.logger.log(`[claude-code-acp] Fork: extracted internal sessionId ${internalSessionId} from ${cliSessionId}`);
|
|
112
|
+
// Check if target file already exists (CLI reuses session IDs for forks from same parent)
|
|
113
|
+
// If so, generate a new unique session ID to avoid collisions
|
|
114
|
+
let finalSessionId = internalSessionId;
|
|
115
|
+
let newFilePath = path.join(sessionDir, `${finalSessionId}.jsonl`);
|
|
116
|
+
if (fs.existsSync(newFilePath)) {
|
|
117
|
+
// Session ID collision - CLI created a fork with the same internal ID
|
|
118
|
+
// Generate a new UUID and update the file's internal session ID
|
|
119
|
+
finalSessionId = randomUUID();
|
|
120
|
+
newFilePath = path.join(sessionDir, `${finalSessionId}.jsonl`);
|
|
121
|
+
this.logger.log(`[claude-code-acp] Fork: session ID collision detected, using new ID: ${finalSessionId}`);
|
|
122
|
+
// Update the internal session ID in the file before renaming
|
|
123
|
+
this.updateSessionIdInFile(oldFilePath, finalSessionId);
|
|
124
|
+
}
|
|
125
|
+
// Rename the file to match the session ID so CLI can find it
|
|
126
|
+
try {
|
|
127
|
+
fs.renameSync(oldFilePath, newFilePath);
|
|
128
|
+
this.logger.log(`[claude-code-acp] Fork: renamed ${cliSessionId}.jsonl -> ${finalSessionId}.jsonl`);
|
|
129
|
+
// Promote sidechain to full session so it can be resumed/forked again
|
|
130
|
+
this.promoteToFullSession(newFilePath);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
this.logger.error(`[claude-code-acp] Failed to rename session file: ${err}`);
|
|
134
|
+
// Continue anyway - the session might still work
|
|
135
|
+
}
|
|
136
|
+
// Re-register session with the final session ID
|
|
137
|
+
const session = this.sessions[result.sessionId];
|
|
138
|
+
this.sessions[finalSessionId] = session;
|
|
139
|
+
delete this.sessions[result.sessionId];
|
|
140
|
+
return { ...result, sessionId: finalSessionId };
|
|
141
|
+
}
|
|
142
|
+
// Fall through if we couldn't extract the internal ID
|
|
143
|
+
this.logger.error(`[claude-code-acp] Could not extract internal sessionId from ${oldFilePath}`);
|
|
144
|
+
}
|
|
145
|
+
// Re-register session with the CLI's session ID (if it's already a UUID or extraction failed)
|
|
146
|
+
this.logger.log(`[claude-code-acp] Fork: remapping session ${result.sessionId} -> ${cliSessionId}`);
|
|
147
|
+
this.sessions[cliSessionId] = this.sessions[result.sessionId];
|
|
148
|
+
delete this.sessions[result.sessionId];
|
|
149
|
+
return { ...result, sessionId: cliSessionId };
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get the directory where session files are stored for a given cwd.
|
|
155
|
+
*/
|
|
156
|
+
getSessionDirPath(cwd) {
|
|
157
|
+
const realCwd = fs.realpathSync(cwd);
|
|
158
|
+
const cwdHash = realCwd.replace(/[/_]/g, "-");
|
|
159
|
+
return path.join(os.homedir(), ".claude", "projects", cwdHash);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Extract the internal sessionId from a session JSONL file.
|
|
163
|
+
* The CLI stores the actual session ID inside the file, which may differ from the filename.
|
|
164
|
+
* For forked sessions, the filename is "agent-xxx" but the internal sessionId is a UUID.
|
|
165
|
+
*/
|
|
166
|
+
extractInternalSessionId(filePath) {
|
|
167
|
+
try {
|
|
168
|
+
if (!fs.existsSync(filePath)) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
172
|
+
const firstLine = content.split("\n").find((line) => line.trim().length > 0);
|
|
173
|
+
if (!firstLine) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const parsed = JSON.parse(firstLine);
|
|
177
|
+
if (parsed.sessionId && typeof parsed.sessionId === "string") {
|
|
178
|
+
// Verify it's a UUID format
|
|
179
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(parsed.sessionId);
|
|
180
|
+
if (isUuid) {
|
|
181
|
+
return parsed.sessionId;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
this.logger.error(`[claude-code-acp] Failed to extract sessionId from ${filePath}: ${err}`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Promote a sidechain session to a regular session by modifying the session file.
|
|
193
|
+
* Forked sessions have "isSidechain": true which prevents them from being resumed.
|
|
194
|
+
* This method changes it to false so the session can be resumed/forked again.
|
|
195
|
+
*/
|
|
196
|
+
promoteToFullSession(filePath) {
|
|
197
|
+
try {
|
|
198
|
+
if (!fs.existsSync(filePath)) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
202
|
+
const lines = content.split("\n");
|
|
203
|
+
const modifiedLines = [];
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
if (!line.trim()) {
|
|
206
|
+
modifiedLines.push(line);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(line);
|
|
211
|
+
// Change isSidechain from true to false
|
|
212
|
+
if (parsed.isSidechain === true) {
|
|
213
|
+
parsed.isSidechain = false;
|
|
214
|
+
}
|
|
215
|
+
modifiedLines.push(JSON.stringify(parsed));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Keep line as-is if it can't be parsed
|
|
219
|
+
modifiedLines.push(line);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
fs.writeFileSync(filePath, modifiedLines.join("\n"), "utf-8");
|
|
223
|
+
this.logger.log(`[claude-code-acp] Promoted sidechain to full session: ${filePath}`);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
this.logger.error(`[claude-code-acp] Failed to promote session: ${err}`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Update the sessionId in all lines of a session JSONL file.
|
|
233
|
+
* This is used when we need to assign a new unique session ID to avoid collisions.
|
|
234
|
+
*/
|
|
235
|
+
updateSessionIdInFile(filePath, newSessionId) {
|
|
236
|
+
try {
|
|
237
|
+
if (!fs.existsSync(filePath)) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
241
|
+
const lines = content.split("\n");
|
|
242
|
+
const modifiedLines = [];
|
|
243
|
+
for (const line of lines) {
|
|
244
|
+
if (!line.trim()) {
|
|
245
|
+
modifiedLines.push(line);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(line);
|
|
250
|
+
// Update the sessionId in each line
|
|
251
|
+
if (parsed.sessionId && typeof parsed.sessionId === "string") {
|
|
252
|
+
parsed.sessionId = newSessionId;
|
|
253
|
+
}
|
|
254
|
+
modifiedLines.push(JSON.stringify(parsed));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Keep line as-is if it can't be parsed
|
|
258
|
+
modifiedLines.push(line);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
fs.writeFileSync(filePath, modifiedLines.join("\n"), "utf-8");
|
|
262
|
+
this.logger.log(`[claude-code-acp] Updated session ID in file: ${filePath}`);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
this.logger.error(`[claude-code-acp] Failed to update session ID in file: ${err}`);
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Discover the CLI-assigned session ID by looking for new session files.
|
|
272
|
+
* Returns the CLI's session ID if found, or the original sessionId if not.
|
|
273
|
+
*/
|
|
274
|
+
async discoverCliSessionId(sessionDir, beforeFiles, fallbackId, timeout = 2000) {
|
|
275
|
+
const start = Date.now();
|
|
276
|
+
// Pattern for CLI-assigned fork session IDs (agent-xxxxxxx)
|
|
277
|
+
const agentPattern = /^agent-[a-f0-9]+\.jsonl$/;
|
|
278
|
+
while (Date.now() - start < timeout) {
|
|
279
|
+
if (fs.existsSync(sessionDir)) {
|
|
280
|
+
const currentFiles = fs.readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
|
|
281
|
+
// Only look for new files that match the agent-xxx pattern
|
|
282
|
+
// This prevents picking up renamed UUID files from previous forks
|
|
283
|
+
const newFiles = currentFiles.filter((f) => !beforeFiles.has(f) && agentPattern.test(f));
|
|
284
|
+
if (newFiles.length === 1) {
|
|
285
|
+
// Found exactly one new agent session file - this is our fork
|
|
286
|
+
this.logger.log(`[claude-code-acp] Discovered fork session file: ${newFiles[0]}`);
|
|
287
|
+
return newFiles[0].replace(".jsonl", "");
|
|
288
|
+
}
|
|
289
|
+
else if (newFiles.length > 1) {
|
|
290
|
+
// Multiple new agent files - try to find the most recent one
|
|
291
|
+
let newestFile = "";
|
|
292
|
+
let newestMtime = 0;
|
|
293
|
+
for (const file of newFiles) {
|
|
294
|
+
const filePath = path.join(sessionDir, file);
|
|
295
|
+
const stat = fs.statSync(filePath);
|
|
296
|
+
if (stat.mtimeMs > newestMtime) {
|
|
297
|
+
newestMtime = stat.mtimeMs;
|
|
298
|
+
newestFile = file;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (newestFile) {
|
|
302
|
+
this.logger.log(`[claude-code-acp] Discovered fork session file (newest of ${newFiles.length}): ${newestFile}`);
|
|
303
|
+
return newestFile.replace(".jsonl", "");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
308
|
+
}
|
|
309
|
+
// Timeout - return fallback
|
|
310
|
+
this.logger.log(`[claude-code-acp] Could not discover CLI session ID, using fallback: ${fallbackId}`);
|
|
311
|
+
return fallbackId;
|
|
95
312
|
}
|
|
96
313
|
/**
|
|
97
314
|
* Alias for unstable_forkSession for convenience.
|
|
@@ -191,7 +408,7 @@ export class ClaudeAcpAgent {
|
|
|
191
408
|
break;
|
|
192
409
|
}
|
|
193
410
|
case "stream_event": {
|
|
194
|
-
for (const notification of streamEventToAcpNotifications(message, params.sessionId, this.toolUseCache, this.
|
|
411
|
+
for (const notification of streamEventToAcpNotifications(message, params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
195
412
|
await this.client.sessionUpdate(notification);
|
|
196
413
|
}
|
|
197
414
|
break;
|
|
@@ -233,7 +450,7 @@ export class ClaudeAcpAgent {
|
|
|
233
450
|
? // Handled by stream events above
|
|
234
451
|
message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
|
|
235
452
|
: message.message.content;
|
|
236
|
-
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, this.toolUseCache, this.
|
|
453
|
+
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
237
454
|
await this.client.sessionUpdate(notification);
|
|
238
455
|
}
|
|
239
456
|
break;
|
|
@@ -256,6 +473,106 @@ export class ClaudeAcpAgent {
|
|
|
256
473
|
this.sessions[params.sessionId].cancelled = true;
|
|
257
474
|
await this.sessions[params.sessionId].query.interrupt();
|
|
258
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Handle extension methods from the client.
|
|
478
|
+
*
|
|
479
|
+
* Currently supports:
|
|
480
|
+
* - `_session/flush`: Flush a session to disk for fork-with-flush support
|
|
481
|
+
*/
|
|
482
|
+
async extMethod(method, params) {
|
|
483
|
+
if (method === "_session/flush") {
|
|
484
|
+
return this.handleSessionFlush(params);
|
|
485
|
+
}
|
|
486
|
+
throw RequestError.methodNotFound(method);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Flush a session to disk by aborting its query subprocess.
|
|
490
|
+
*
|
|
491
|
+
* This is used by the fork-with-flush mechanism to ensure session data
|
|
492
|
+
* is persisted to disk before forking. When the Claude SDK subprocess
|
|
493
|
+
* exits (via abort), it writes the session data to:
|
|
494
|
+
* ~/.claude/projects/<cwd-hash>/<sessionId>.jsonl
|
|
495
|
+
*
|
|
496
|
+
* After this method completes, the session is removed from memory and
|
|
497
|
+
* must be reloaded via loadSession() to continue using it.
|
|
498
|
+
*/
|
|
499
|
+
async handleSessionFlush(params) {
|
|
500
|
+
const { sessionId, persistTimeout = 5000 } = params;
|
|
501
|
+
const session = this.sessions[sessionId];
|
|
502
|
+
if (!session) {
|
|
503
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
// Step 1: Mark session as cancelled to stop processing
|
|
507
|
+
session.cancelled = true;
|
|
508
|
+
// Step 2: Interrupt any ongoing query work
|
|
509
|
+
await session.query.interrupt();
|
|
510
|
+
// Step 3: End the input stream to signal no more input
|
|
511
|
+
session.input.end();
|
|
512
|
+
// Step 4: Abort the session using the AbortController
|
|
513
|
+
// This forces the Claude SDK subprocess to exit, which triggers disk persistence
|
|
514
|
+
session.abortController.abort();
|
|
515
|
+
// Step 5: Wait for the session file to appear on disk
|
|
516
|
+
// Use stored sessionFilePath for forked sessions (where filename differs from sessionId)
|
|
517
|
+
const sessionFilePath = session.sessionFilePath ?? this.getSessionFilePath(sessionId, session.cwd);
|
|
518
|
+
this.logger.log(`[claude-code-acp] Waiting for session file at: ${sessionFilePath}`);
|
|
519
|
+
this.logger.log(`[claude-code-acp] Session cwd: ${session.cwd}`);
|
|
520
|
+
const persisted = await this.waitForSessionFile(sessionFilePath, persistTimeout);
|
|
521
|
+
if (!persisted) {
|
|
522
|
+
this.logger.error(`[claude-code-acp] Session file not found at ${sessionFilePath} after ${persistTimeout}ms`);
|
|
523
|
+
// Check if file exists at the path
|
|
524
|
+
const exists = fs.existsSync(sessionFilePath);
|
|
525
|
+
this.logger.error(`[claude-code-acp] File exists check: ${exists}`);
|
|
526
|
+
// Still remove the session from memory
|
|
527
|
+
delete this.sessions[sessionId];
|
|
528
|
+
return { success: false, error: `Session file not created within timeout` };
|
|
529
|
+
}
|
|
530
|
+
// Step 6: Remove session from our map
|
|
531
|
+
// The client will call loadSession() to reload it from disk
|
|
532
|
+
delete this.sessions[sessionId];
|
|
533
|
+
this.logger.log(`[claude-code-acp] Session ${sessionId} flushed to disk at ${sessionFilePath}`);
|
|
534
|
+
return { success: true, filePath: sessionFilePath };
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
this.logger.error(`[claude-code-acp] Failed to flush session ${sessionId}:`, error);
|
|
538
|
+
// Clean up session on error
|
|
539
|
+
delete this.sessions[sessionId];
|
|
540
|
+
return { success: false, error: String(error) };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Get the file path where Claude Code stores session data.
|
|
545
|
+
*
|
|
546
|
+
* Claude Code stores sessions at:
|
|
547
|
+
* ~/.claude/projects/<cwd-hash>/<sessionId>.jsonl
|
|
548
|
+
*
|
|
549
|
+
* Where <cwd-hash> is the cwd with `/` replaced by `-`
|
|
550
|
+
* Note: We resolve the real path to handle macOS symlinks like /var -> /private/var
|
|
551
|
+
*/
|
|
552
|
+
getSessionFilePath(sessionId, cwd) {
|
|
553
|
+
// Resolve the real path to handle macOS symlinks like /var -> /private/var
|
|
554
|
+
const realCwd = fs.realpathSync(cwd);
|
|
555
|
+
// Claude Code replaces both / and _ with - in the cwd hash
|
|
556
|
+
const cwdHash = realCwd.replace(/[/_]/g, "-");
|
|
557
|
+
return path.join(os.homedir(), ".claude", "projects", cwdHash, `${sessionId}.jsonl`);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Wait for a session file to appear on disk.
|
|
561
|
+
*
|
|
562
|
+
* @param filePath - Path to the session file
|
|
563
|
+
* @param timeout - Maximum time to wait in milliseconds
|
|
564
|
+
* @returns true if file appears, false if timeout
|
|
565
|
+
*/
|
|
566
|
+
async waitForSessionFile(filePath, timeout) {
|
|
567
|
+
const start = Date.now();
|
|
568
|
+
while (Date.now() - start < timeout) {
|
|
569
|
+
if (fs.existsSync(filePath)) {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
259
576
|
async unstable_setSessionModel(params) {
|
|
260
577
|
if (!this.sessions[params.sessionId]) {
|
|
261
578
|
throw new Error("Session not found");
|
|
@@ -287,14 +604,10 @@ export class ClaudeAcpAgent {
|
|
|
287
604
|
}
|
|
288
605
|
async readTextFile(params) {
|
|
289
606
|
const response = await this.client.readTextFile(params);
|
|
290
|
-
if (!params.limit && !params.line) {
|
|
291
|
-
this.fileContentCache[params.path] = response.content;
|
|
292
|
-
}
|
|
293
607
|
return response;
|
|
294
608
|
}
|
|
295
609
|
async writeTextFile(params) {
|
|
296
610
|
const response = await this.client.writeTextFile(params);
|
|
297
|
-
this.fileContentCache[params.path] = params.content;
|
|
298
611
|
return response;
|
|
299
612
|
}
|
|
300
613
|
canUseTool(sessionId) {
|
|
@@ -322,7 +635,7 @@ export class ClaudeAcpAgent {
|
|
|
322
635
|
toolCall: {
|
|
323
636
|
toolCallId: toolUseID,
|
|
324
637
|
rawInput: toolInput,
|
|
325
|
-
title: toolInfoFromToolUse({ name: toolName, input: toolInput }
|
|
638
|
+
title: toolInfoFromToolUse({ name: toolName, input: toolInput }).title,
|
|
326
639
|
},
|
|
327
640
|
});
|
|
328
641
|
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
@@ -378,7 +691,7 @@ export class ClaudeAcpAgent {
|
|
|
378
691
|
toolCall: {
|
|
379
692
|
toolCallId: toolUseID,
|
|
380
693
|
rawInput: toolInput,
|
|
381
|
-
title: toolInfoFromToolUse({ name: toolName, input: toolInput }
|
|
694
|
+
title: toolInfoFromToolUse({ name: toolName, input: toolInput }).title,
|
|
382
695
|
},
|
|
383
696
|
});
|
|
384
697
|
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
@@ -416,9 +729,18 @@ export class ClaudeAcpAgent {
|
|
|
416
729
|
};
|
|
417
730
|
}
|
|
418
731
|
async createSession(params, creationOpts = {}) {
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
|
|
732
|
+
// We want to create a new session id unless it is resume,
|
|
733
|
+
// but not resume + forkSession.
|
|
734
|
+
let sessionId;
|
|
735
|
+
if (creationOpts.forkSession) {
|
|
736
|
+
sessionId = randomUUID();
|
|
737
|
+
}
|
|
738
|
+
else if (creationOpts.resume) {
|
|
739
|
+
sessionId = creationOpts.resume;
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
sessionId = randomUUID();
|
|
743
|
+
}
|
|
422
744
|
const input = new Pushable();
|
|
423
745
|
const settingsManager = new SettingsManager(params.cwd, {
|
|
424
746
|
logger: this.logger,
|
|
@@ -473,15 +795,21 @@ export class ClaudeAcpAgent {
|
|
|
473
795
|
// Extract options from _meta if provided
|
|
474
796
|
const userProvidedOptions = params._meta?.claudeCode?.options;
|
|
475
797
|
const extraArgs = { ...userProvidedOptions?.extraArgs };
|
|
476
|
-
if (creationOpts?.resume === undefined) {
|
|
798
|
+
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
|
|
477
799
|
// Set our own session id if not resuming an existing session.
|
|
478
|
-
//
|
|
800
|
+
// Note: For forked sessions (resume + fork), Claude CLI assigns its own session ID
|
|
801
|
+
// which means chain forking (fork of a fork) is not currently supported.
|
|
479
802
|
extraArgs["session-id"] = sessionId;
|
|
480
803
|
}
|
|
804
|
+
// Configure thinking tokens from environment variable
|
|
805
|
+
const maxThinkingTokens = process.env.MAX_THINKING_TOKENS
|
|
806
|
+
? parseInt(process.env.MAX_THINKING_TOKENS, 10)
|
|
807
|
+
: undefined;
|
|
481
808
|
const options = {
|
|
482
809
|
systemPrompt,
|
|
483
810
|
settingSources: ["user", "project", "local"],
|
|
484
811
|
stderr: (err) => this.logger.error(err),
|
|
812
|
+
...(maxThinkingTokens !== undefined && { maxThinkingTokens }),
|
|
485
813
|
...userProvidedOptions,
|
|
486
814
|
// Override certain fields that must be controlled by ACP
|
|
487
815
|
cwd: params.cwd,
|
|
@@ -499,6 +827,7 @@ export class ClaudeAcpAgent {
|
|
|
499
827
|
...(process.env.CLAUDE_CODE_EXECUTABLE && {
|
|
500
828
|
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
|
|
501
829
|
}),
|
|
830
|
+
tools: { type: "preset", preset: "claude_code" },
|
|
502
831
|
hooks: {
|
|
503
832
|
...userProvidedOptions?.hooks,
|
|
504
833
|
PreToolUse: [
|
|
@@ -517,7 +846,8 @@ export class ClaudeAcpAgent {
|
|
|
517
846
|
...creationOpts,
|
|
518
847
|
};
|
|
519
848
|
const allowedTools = [];
|
|
520
|
-
|
|
849
|
+
// Disable this for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
|
|
850
|
+
const disallowedTools = ["AskUserQuestion"];
|
|
521
851
|
// Check if built-in tools should be disabled
|
|
522
852
|
const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
|
|
523
853
|
if (!disableBuiltInTools) {
|
|
@@ -543,11 +873,15 @@ export class ClaudeAcpAgent {
|
|
|
543
873
|
if (disallowedTools.length > 0) {
|
|
544
874
|
options.disallowedTools = disallowedTools;
|
|
545
875
|
}
|
|
546
|
-
//
|
|
547
|
-
const
|
|
548
|
-
|
|
876
|
+
// Create our own AbortController for session management
|
|
877
|
+
const sessionAbortController = new AbortController();
|
|
878
|
+
// Handle abort controller from meta options (user can still provide one)
|
|
879
|
+
const userAbortController = userProvidedOptions?.abortController;
|
|
880
|
+
if (userAbortController?.signal.aborted) {
|
|
549
881
|
throw new Error("Cancelled");
|
|
550
882
|
}
|
|
883
|
+
// Pass the abort controller to the query options
|
|
884
|
+
options.abortController = sessionAbortController;
|
|
551
885
|
const q = query({
|
|
552
886
|
prompt: input,
|
|
553
887
|
options,
|
|
@@ -558,6 +892,8 @@ export class ClaudeAcpAgent {
|
|
|
558
892
|
cancelled: false,
|
|
559
893
|
permissionMode,
|
|
560
894
|
settingsManager,
|
|
895
|
+
abortController: sessionAbortController,
|
|
896
|
+
cwd: params.cwd,
|
|
561
897
|
};
|
|
562
898
|
const availableCommands = await getAvailableSlashCommands(q);
|
|
563
899
|
const models = await getAvailableModels(q);
|
|
@@ -639,7 +975,13 @@ async function getAvailableSlashCommands(query) {
|
|
|
639
975
|
const commands = await query.supportedCommands();
|
|
640
976
|
return commands
|
|
641
977
|
.map((command) => {
|
|
642
|
-
const input = command.argumentHint
|
|
978
|
+
const input = command.argumentHint
|
|
979
|
+
? {
|
|
980
|
+
hint: Array.isArray(command.argumentHint)
|
|
981
|
+
? command.argumentHint.join(" ")
|
|
982
|
+
: command.argumentHint,
|
|
983
|
+
}
|
|
984
|
+
: null;
|
|
643
985
|
let name = command.name;
|
|
644
986
|
if (command.name.endsWith(" (MCP)")) {
|
|
645
987
|
name = `mcp:${name.replace(" (MCP)", "")}`;
|
|
@@ -750,7 +1092,7 @@ export function promptToClaude(prompt) {
|
|
|
750
1092
|
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
751
1093
|
* Only handles text, image, and thinking chunks for now.
|
|
752
1094
|
*/
|
|
753
|
-
export function toAcpNotifications(content, role, sessionId, toolUseCache,
|
|
1095
|
+
export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger) {
|
|
754
1096
|
if (typeof content === "string") {
|
|
755
1097
|
return [
|
|
756
1098
|
{
|
|
@@ -857,7 +1199,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, fileC
|
|
|
857
1199
|
sessionUpdate: "tool_call",
|
|
858
1200
|
rawInput,
|
|
859
1201
|
status: "pending",
|
|
860
|
-
...toolInfoFromToolUse(chunk
|
|
1202
|
+
...toolInfoFromToolUse(chunk),
|
|
861
1203
|
};
|
|
862
1204
|
}
|
|
863
1205
|
break;
|
|
@@ -908,13 +1250,13 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, fileC
|
|
|
908
1250
|
}
|
|
909
1251
|
return output;
|
|
910
1252
|
}
|
|
911
|
-
export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
1253
|
+
export function streamEventToAcpNotifications(message, sessionId, toolUseCache, client, logger) {
|
|
912
1254
|
const event = message.event;
|
|
913
1255
|
switch (event.type) {
|
|
914
1256
|
case "content_block_start":
|
|
915
|
-
return toAcpNotifications([event.content_block], "assistant", sessionId, toolUseCache,
|
|
1257
|
+
return toAcpNotifications([event.content_block], "assistant", sessionId, toolUseCache, client, logger);
|
|
916
1258
|
case "content_block_delta":
|
|
917
|
-
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache,
|
|
1259
|
+
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger);
|
|
918
1260
|
// No content
|
|
919
1261
|
case "message_start":
|
|
920
1262
|
case "message_delta":
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ClaudeAcpAgent, runAcp, toAcpNotifications, streamEventToAcpNotifications, type ToolUpdateMeta, type NewSessionMeta, } from "./acp-agent.js";
|
|
2
|
+
export { loadManagedSettings, applyEnvironmentSettings, nodeToWebReadable, nodeToWebWritable, Pushable, unreachable, } from "./utils.js";
|
|
3
|
+
export { createMcpServer } from "./mcp-server.js";
|
|
4
|
+
export { toolInfoFromToolUse, planEntries, toolUpdateFromToolResult, createPreToolUseHook, acpToolNames as toolNames, } from "./tools.js";
|
|
5
|
+
export { SettingsManager, type ClaudeCodeSettings, type PermissionSettings, type PermissionDecision, type PermissionCheckResult, type SettingsManagerOptions, } from "./settings.js";
|
|
6
|
+
export type { ClaudePlanEntry } from "./tools.js";
|
|
7
|
+
//# sourceMappingURL=lib.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,MAAM,EACN,kBAAkB,EAClB,6BAA6B,EAC7B,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,iBAAiB,EACjB,iBAAiB,EACjB,QAAQ,EACR,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,wBAAwB,EACxB,oBAAoB,EACpB,YAAY,IAAI,SAAS,GAC1B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,eAAe,EACf,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,eAAe,CAAC;AAGvB,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ClaudeAcpAgent } from "./acp-agent.js";
|
|
3
|
+
import { ClientCapabilities } from "@agentclientprotocol/sdk";
|
|
4
|
+
export declare const SYSTEM_REMINDER = "\n\n<system-reminder>\nWhenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior.\n</system-reminder>";
|
|
5
|
+
export declare function createMcpServer(agent: ClaudeAcpAgent, sessionId: string, clientCapabilities: ClientCapabilities | undefined): McpServer;
|
|
6
|
+
/**
|
|
7
|
+
* Replace text in a file and calculate the line numbers where the edits occurred.
|
|
8
|
+
*
|
|
9
|
+
* @param fileContent - The full file content
|
|
10
|
+
* @param edits - Array of edit operations to apply sequentially
|
|
11
|
+
* @returns the new content and the line numbers where replacements occurred in the final content
|
|
12
|
+
*/
|
|
13
|
+
export declare function replaceAndCalculateLocation(fileContent: string, edits: Array<{
|
|
14
|
+
oldText: string;
|
|
15
|
+
newText: string;
|
|
16
|
+
replaceAll?: boolean;
|
|
17
|
+
}>): {
|
|
18
|
+
newContent: string;
|
|
19
|
+
lineNumbers: number[];
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=mcp-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AASpE,OAAO,EAAqB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EACL,kBAAkB,EAGnB,MAAM,0BAA0B,CAAC;AAQlC,eAAO,MAAM,eAAe,iSAIT,CAAC;AA2BpB,wBAAgB,eAAe,CAC7B,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,kBAAkB,GAAG,SAAS,GACjD,SAAS,CA2nBX;AA+DD;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,KAAK,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC,GACD;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CAyF/C"}
|