chore(deps): upgrade claude-code-acp to v0.8.4 and mark express dependencies as optional

This commit is contained in:
Mohamed Boudra
2025-10-26 08:57:57 +01:00
parent 1d8e86985b
commit b1faab72ce
4 changed files with 191 additions and 112 deletions
+72
View File
@@ -130,5 +130,77 @@
"createdAt": "2025-10-26T07:29:32.156Z", "createdAt": "2025-10-26T07:29:32.156Z",
"lastActivityAt": "2025-10-26T07:29:40.533Z", "lastActivityAt": "2025-10-26T07:29:40.533Z",
"cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-Gi719a" "cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-Gi719a"
},
{
"id": "94e1e306-5837-4ce3-9a09-09f1c8ae2678",
"title": "Agent 94e1e306",
"sessionId": "0bc2cfed-8898-4834-8827-23283ba231cf",
"options": {
"type": "claude",
"sessionId": "0bc2cfed-8898-4834-8827-23283ba231cf"
},
"createdAt": "2025-10-26T07:41:24.824Z",
"lastActivityAt": "2025-10-26T07:41:34.736Z",
"cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-c77wpz"
},
{
"id": "686a1db9-e41c-4d4b-b0ab-6ae7e573bbd3",
"title": "Agent 686a1db9",
"sessionId": "c62d4b37-df4b-4c73-8afb-9c8a994cd1c9",
"options": {
"type": "claude",
"sessionId": "c62d4b37-df4b-4c73-8afb-9c8a994cd1c9"
},
"createdAt": "2025-10-26T07:42:30.564Z",
"lastActivityAt": "2025-10-26T07:42:39.893Z",
"cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-m0yPgj"
},
{
"id": "badd51fa-7f43-453a-802e-8504944cc388",
"title": "Agent badd51fa",
"sessionId": "09459e2b-92be-4535-9109-64f0f3eac5d5",
"options": {
"type": "claude",
"sessionId": "09459e2b-92be-4535-9109-64f0f3eac5d5"
},
"createdAt": "2025-10-26T07:49:57.618Z",
"lastActivityAt": "2025-10-26T07:50:05.977Z",
"cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-JNRc9I"
},
{
"id": "26610309-c9e2-4a33-94c1-c4afe7ec439b",
"title": "Agent 26610309",
"sessionId": "a73bcefc-d829-4286-b192-a6a5c04927c0",
"options": {
"type": "claude",
"sessionId": "a73bcefc-d829-4286-b192-a6a5c04927c0"
},
"createdAt": "2025-10-26T07:51:04.294Z",
"lastActivityAt": "2025-10-26T07:51:15.950Z",
"cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-3t9vQe"
},
{
"id": "4c4f71e5-cb60-4040-9911-746258b43d1f",
"title": "Agent 4c4f71e5",
"sessionId": "99af8673-ba9a-4fe1-bb96-19ea54760cfe",
"options": {
"type": "claude",
"sessionId": "99af8673-ba9a-4fe1-bb96-19ea54760cfe"
},
"createdAt": "2025-10-26T07:52:50.829Z",
"lastActivityAt": "2025-10-26T07:53:02.229Z",
"cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-UOhobf"
},
{
"id": "835f1dc5-1398-4743-ad97-f9ebae232cc1",
"title": "Agent 835f1dc5",
"sessionId": "90231d13-4f40-435d-aec5-ce43d76ac98f",
"options": {
"type": "claude",
"sessionId": "90231d13-4f40-435d-aec5-ce43d76ac98f"
},
"createdAt": "2025-10-26T07:57:16.797Z",
"lastActivityAt": "2025-10-26T07:57:29.704Z",
"cwd": "/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/acp-test-GAWCrz"
} }
] ]
+1 -1
View File
@@ -19,7 +19,7 @@
"dependencies": { "dependencies": {
"@agentclientprotocol/sdk": "^0.4.9", "@agentclientprotocol/sdk": "^0.4.9",
"@ai-sdk/openai": "^2.0.52", "@ai-sdk/openai": "^2.0.52",
"@boudra/claude-code-acp": "^0.8.3", "@boudra/claude-code-acp": "^0.8.4",
"@deepgram/sdk": "^3.4.0", "@deepgram/sdk": "^3.4.0",
"@modelcontextprotocol/sdk": "^1.20.1", "@modelcontextprotocol/sdk": "^1.20.1",
"@openrouter/ai-sdk-provider": "^1.2.0", "@openrouter/ai-sdk-provider": "^1.2.0",
@@ -19,85 +19,51 @@ describe("AgentManager", () => {
await rm(tmpDir, { recursive: true, force: true }); await rm(tmpDir, { recursive: true, force: true });
}); });
it( it("should create file after accepting plan", async () => {
"should create file after accepting plan", let permissionRequest: RequestPermissionRequest | null = null;
async () => { let requestId: string | null = null;
let permissionRequest: RequestPermissionRequest | null = null; const testFile = join(tmpDir, "test.txt");
let requestId: string | null = null;
const testFile = join(tmpDir, "test.txt");
const agentId = await manager.createAgent({ const agentId = await manager.createAgent({
cwd: tmpDir, cwd: tmpDir,
initialMode: "plan", initialMode: "plan",
}); });
const unsubscribe = manager.subscribeToUpdates( const unsubscribe = manager.subscribeToUpdates(
agentId, agentId,
(update: AgentUpdate) => { (update: AgentUpdate) => {
const notification: AgentNotification = update.notification; const notification: AgentNotification = update.notification;
if (notification.type === "permission") { if (notification.type === "permission") {
permissionRequest = notification.request; permissionRequest = notification.request;
requestId = notification.requestId; requestId = notification.requestId;
}
} }
}
);
try {
await manager.sendPrompt(
agentId,
"Create a file called test.txt with the content 'hello world'"
); );
try { let attempts = 0;
await manager.sendPrompt( while (!permissionRequest && attempts < 40) {
agentId, await new Promise((resolve) => setTimeout(resolve, 500));
"Create a file called test.txt with the content 'hello world'" attempts++;
);
let attempts = 0;
while (!permissionRequest && attempts < 40) {
await new Promise((resolve) => setTimeout(resolve, 500));
attempts++;
}
expect(permissionRequest).toBeDefined();
expect(requestId).toBeDefined();
const acceptOption = permissionRequest!.options.find(
(o) => o.kind === "allow_once" || o.kind === "allow_always"
);
expect(acceptOption).toBeDefined();
manager.respondToPermission(agentId, requestId!, acceptOption!.optionId);
let status = manager.getAgentStatus(agentId);
attempts = 0;
while (status !== "completed" && status !== "failed" && attempts < 60) {
await new Promise((resolve) => setTimeout(resolve, 1000));
status = manager.getAgentStatus(agentId);
attempts++;
}
expect(status).toBe("completed");
const content = await readFile(testFile, "utf-8");
expect(content).toContain("hello world");
const agentInfo = manager.listAgents().find((a) => a.id === agentId);
expect(agentInfo?.currentModeId).toBe("acceptEdits");
} finally {
unsubscribe();
await manager.killAgent(agentId);
} }
},
120000
);
it( expect(permissionRequest).toBeDefined();
"should load persisted agent and send new prompt", expect(requestId).toBeDefined();
async () => {
const agentId = await manager.createAgent({
cwd: tmpDir,
});
await manager.sendPrompt(agentId, "echo 'first message'"); const acceptOption = permissionRequest!.options.find(
(o) => o.kind === "allow_once" || o.kind === "allow_always"
);
expect(acceptOption).toBeDefined();
manager.respondToPermission(agentId, requestId!, acceptOption!.optionId);
let status = manager.getAgentStatus(agentId); let status = manager.getAgentStatus(agentId);
let attempts = 0; attempts = 0;
while (status !== "completed" && status !== "failed" && attempts < 60) { while (status !== "completed" && status !== "failed" && attempts < 60) {
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
status = manager.getAgentStatus(agentId); status = manager.getAgentStatus(agentId);
@@ -106,45 +72,82 @@ describe("AgentManager", () => {
expect(status).toBe("completed"); expect(status).toBe("completed");
const agentBeforeKill = manager.listAgents().find((a) => a.id === agentId); const content = await readFile(testFile, "utf-8");
const claudeSessionId = manager.getClaudeSessionId(agentId); expect(content).toContain("hello world");
console.log("Before kill - ACP Session ID:", agentBeforeKill?.sessionId);
console.log("Before kill - Claude Session ID:", claudeSessionId);
console.log("Working directory:", tmpDir);
const agentInfo = manager.listAgents().find((a) => a.id === agentId);
expect(agentInfo?.currentModeId).toBe("acceptEdits");
} finally {
unsubscribe();
await manager.killAgent(agentId); await manager.killAgent(agentId);
}
}, 120000);
const newManager = new AgentManager(); it.only("should load persisted agent and send new prompt", async () => {
await newManager.initialize(); const agentId = await manager.createAgent({
cwd: tmpDir,
});
const agents = newManager.listAgents(); await manager.sendPrompt(agentId, "echo 'first message'");
const loadedAgent = agents.find((a) => a.id === agentId);
const loadedClaudeSessionId = newManager.getClaudeSessionId(agentId);
console.log("After reload - ACP Session ID:", loadedAgent?.sessionId); let status = manager.getAgentStatus(agentId);
console.log("After reload - Claude Session ID:", loadedClaudeSessionId); let attempts = 0;
while (status !== "completed" && status !== "failed" && attempts < 60) {
await new Promise((resolve) => setTimeout(resolve, 1000));
status = manager.getAgentStatus(agentId);
attempts++;
}
expect(loadedAgent).toBeDefined(); expect(status).toBe("completed");
expect(loadedAgent?.id).toBe(agentId);
expect(loadedAgent?.cwd).toBe(tmpDir);
await newManager.sendPrompt(agentId, "echo 'second message'"); const agentBeforeKill = manager.listAgents().find((a) => a.id === agentId);
const claudeSessionId = manager.getClaudeSessionId(agentId);
console.log("Before kill - ACP Session ID:", agentBeforeKill?.sessionId);
console.log("Before kill - Claude Session ID:", claudeSessionId);
console.log("Working directory:", tmpDir);
await manager.killAgent(agentId);
const newManager = new AgentManager();
await newManager.initialize();
const agents = newManager.listAgents();
const loadedAgent = agents.find((a) => a.id === agentId);
const loadedClaudeSessionId = newManager.getClaudeSessionId(agentId);
console.log("After reload - ACP Session ID:", loadedAgent?.sessionId);
console.log("After reload - Claude Session ID:", loadedClaudeSessionId);
expect(loadedAgent).toBeDefined();
expect(loadedAgent?.id).toBe(agentId);
expect(loadedAgent?.cwd).toBe(tmpDir);
await newManager.initializeAgentAndGetHistory(agentId);
await new Promise((resolve) => setTimeout(resolve, 2000));
const persistedUpdates = newManager.getAgentUpdates(agentId);
console.log(
"Persisted updates:",
JSON.stringify(persistedUpdates, null, 2)
);
await newManager.sendPrompt(agentId, "echo 'second message'");
status = newManager.getAgentStatus(agentId);
attempts = 0;
while (status !== "completed" && status !== "failed" && attempts < 60) {
await new Promise((resolve) => setTimeout(resolve, 1000));
status = newManager.getAgentStatus(agentId); status = newManager.getAgentStatus(agentId);
attempts = 0; attempts++;
while (status !== "completed" && status !== "failed" && attempts < 60) { }
await new Promise((resolve) => setTimeout(resolve, 1000));
status = newManager.getAgentStatus(agentId);
attempts++;
}
expect(status).toBe("completed"); expect(status).toBe("completed");
const finalUpdates = newManager.getAgentUpdates(agentId); const finalUpdates = newManager.getAgentUpdates(agentId);
expect(finalUpdates.length).toBeGreaterThan(0); expect(finalUpdates.length).toBeGreaterThan(0);
await newManager.killAgent(agentId); console.log("Final updates:", finalUpdates);
},
120000 await newManager.killAgent(agentId);
); }, 120000);
}); });
+17 -13
View File
@@ -62,9 +62,7 @@ interface ManagedAgent {
type UpdateAgentCallback = ( type UpdateAgentCallback = (
agentId: string, agentId: string,
updateFn: ( updateFn: (agent: ManagedAgent) => boolean | void | Promise<boolean | void>,
agent: ManagedAgent
) => boolean | void | Promise<boolean | void>,
options?: { sessionId: string | null } options?: { sessionId: string | null }
) => Promise<void>; ) => Promise<void>;
@@ -113,7 +111,10 @@ class ACPClient implements Client {
async sessionUpdate(params: SessionNotification): Promise<void> { async sessionUpdate(params: SessionNotification): Promise<void> {
if (params.update.sessionUpdate === "agent_message_chunk") { if (params.update.sessionUpdate === "agent_message_chunk") {
console.log(`[Agent ${this.agentId}] Message chunk update:`, JSON.stringify(params, null, 2)); console.log(
`[Agent ${this.agentId}] Message chunk update:`,
JSON.stringify(params, null, 2)
);
} }
// Check if this update contains a Claude session ID // Check if this update contains a Claude session ID
@@ -349,13 +350,15 @@ export class AgentManager {
} }
// Convert prompt to ContentBlock array if it's a string // Convert prompt to ContentBlock array if it's a string
const contentBlocks: ContentBlock[] = typeof prompt === "string" const contentBlocks: ContentBlock[] =
? [{ type: "text", text: prompt }] typeof prompt === "string" ? [{ type: "text", text: prompt }] : prompt;
: prompt;
// Extract text for user message notification (use first text block) // Extract text for user message notification (use first text block)
const firstTextBlock = contentBlocks.find(block => block.type === "text"); const firstTextBlock = contentBlocks.find((block) => block.type === "text");
const userMessageText = firstTextBlock && "text" in firstTextBlock ? firstTextBlock.text : "[message with attachments]"; const userMessageText =
firstTextBlock && "text" in firstTextBlock
? firstTextBlock.text
: "[message with attachments]";
// Emit user message notification // Emit user message notification
const userMessageUpdate: AgentUpdate = { const userMessageUpdate: AgentUpdate = {
@@ -540,6 +543,7 @@ export class AgentManager {
if (runtime) { if (runtime) {
setTimeout(() => { setTimeout(() => {
if (!runtime.process.killed) { if (!runtime.process.killed) {
console.log(`[Agent ${agentId}] Force killing process`);
runtime.process.kill("SIGKILL"); runtime.process.kill("SIGKILL");
} }
}, 5000); }, 5000);
@@ -898,9 +902,7 @@ export class AgentManager {
private async updateAgent( private async updateAgent(
agentId: string, agentId: string,
updateFn: ( updateFn: (agent: ManagedAgent) => boolean | void | Promise<boolean | void>,
agent: ManagedAgent
) => boolean | void | Promise<boolean | void>,
options?: { sessionId: string | null } options?: { sessionId: string | null }
): Promise<void> { ): Promise<void> {
const agent = this.agents.get(agentId); const agent = this.agents.get(agentId);
@@ -1185,7 +1187,9 @@ export class AgentManager {
const runtime = this.getRuntime(agent); const runtime = this.getRuntime(agent);
if (runtime && runtime.currentModeId !== newModeId) { if (runtime && runtime.currentModeId !== newModeId) {
console.log(`[Agent ${agentId}] Mode changed: ${runtime.currentModeId} -> ${newModeId}`); console.log(
`[Agent ${agentId}] Mode changed: ${runtime.currentModeId} -> ${newModeId}`
);
runtime.currentModeId = newModeId; runtime.currentModeId = newModeId;
} }
} }