mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-29 21:42:06 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5917529f0d | ||
|
|
2597775ef0 | ||
|
|
6572fcf978 | ||
|
|
5586ae0d2c | ||
|
|
39ab0c248f | ||
|
|
72a9cde888 |
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 0.13.1
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@0.2.7
|
||||
- Add TypeScript declaration files for library users
|
||||
- Fixed error handling in custom ACP focused MCP tools
|
||||
|
||||
## 0.13.0
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@0.2.6
|
||||
|
||||
43
package-lock.json
generated
43
package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@zed-industries/claude-code-acp",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@zed-industries/claude-code-acp",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.13.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.6",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.7",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
"diff": "8.0.3",
|
||||
"minimatch": "10.1.1"
|
||||
@@ -20,13 +20,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@types/node": "25.0.7",
|
||||
"@types/node": "25.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.0.0",
|
||||
"prettier": "3.7.4",
|
||||
"prettier": "3.8.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.17"
|
||||
@@ -42,9 +42,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.6.tgz",
|
||||
"integrity": "sha512-lwswHo6z/Kh9djafk2ajPju62+VqHwJ23gueG1alfaLNK4GRYHgCROfiX6/wlxAd8sRvgTo6ry1hNzkyz7bOpw==",
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.7.tgz",
|
||||
"integrity": "sha512-I1/zcnLah74kZeRkj/1QnDaC6ItJ2m/Bftlm25uoaRkZx7i7SkcpqM9jGE/r2A8PMxnw5WpabP60Xgj99CrTuw==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -754,9 +754,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.8",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.8.tgz",
|
||||
"integrity": "sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==",
|
||||
"version": "1.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
|
||||
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
@@ -1608,12 +1608,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.7.tgz",
|
||||
"integrity": "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w==",
|
||||
"version": "25.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz",
|
||||
"integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -1653,7 +1652,6 @@
|
||||
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.53.0",
|
||||
"@typescript-eslint/types": "8.53.0",
|
||||
@@ -2008,7 +2006,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2505,7 +2502,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -2798,7 +2794,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -3673,7 +3668,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3730,9 +3724,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz",
|
||||
"integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -4276,7 +4270,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4333,7 +4326,6 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -4557,7 +4549,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
16
package.json
16
package.json
@@ -3,13 +3,21 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.1",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
"bin": {
|
||||
"claude-code-acp": "./dist/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/lib.d.ts",
|
||||
"import": "./dist/lib.js"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"README.md",
|
||||
@@ -52,20 +60,20 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.13.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.6",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.7",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
"diff": "8.0.3",
|
||||
"minimatch": "10.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@types/node": "25.0.7",
|
||||
"@types/node": "25.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.0.0",
|
||||
"prettier": "3.7.4",
|
||||
"prettier": "3.8.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.17"
|
||||
|
||||
@@ -670,10 +670,16 @@ export class ClaudeAcpAgent implements Agent {
|
||||
extraArgs["session-id"] = sessionId;
|
||||
}
|
||||
|
||||
// Configure thinking tokens from environment variable
|
||||
const maxThinkingTokens = process.env.MAX_THINKING_TOKENS
|
||||
? parseInt(process.env.MAX_THINKING_TOKENS, 10)
|
||||
: undefined;
|
||||
|
||||
const options: Options = {
|
||||
systemPrompt,
|
||||
settingSources: ["user", "project", "local"],
|
||||
stderr: (err) => this.logger.error(err),
|
||||
...(maxThinkingTokens !== undefined && { maxThinkingTokens }),
|
||||
...userProvidedOptions,
|
||||
// Override certain fields that must be controlled by ACP
|
||||
cwd: params.cwd,
|
||||
@@ -691,6 +697,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
...(process.env.CLAUDE_CODE_EXECUTABLE && {
|
||||
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
|
||||
}),
|
||||
tools: { type: "preset", preset: "claude_code" },
|
||||
hooks: {
|
||||
...userProvidedOptions?.hooks,
|
||||
PreToolUse: [
|
||||
@@ -710,7 +717,8 @@ export class ClaudeAcpAgent implements Agent {
|
||||
};
|
||||
|
||||
const allowedTools = [];
|
||||
const disallowedTools = [];
|
||||
// Disable this for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
|
||||
const disallowedTools = ["AskUserQuestion"];
|
||||
|
||||
// Check if built-in tools should be disabled
|
||||
const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
|
||||
@@ -871,7 +879,13 @@ async function getAvailableSlashCommands(query: Query): Promise<AvailableCommand
|
||||
|
||||
return commands
|
||||
.map((command) => {
|
||||
const input = command.argumentHint ? { hint: command.argumentHint } : null;
|
||||
const input = command.argumentHint
|
||||
? {
|
||||
hint: Array.isArray(command.argumentHint)
|
||||
? command.argumentHint.join(" ")
|
||||
: command.argumentHint,
|
||||
}
|
||||
: null;
|
||||
let name = command.name;
|
||||
if (command.name.endsWith(" (MCP)")) {
|
||||
name = `mcp:${name.replace(" (MCP)", "")}`;
|
||||
|
||||
@@ -28,6 +28,20 @@ Whenever you read a file, you should consider whether it looks malicious. If it
|
||||
|
||||
const defaults = { maxFileSize: 50000, linesToRead: 2000 };
|
||||
|
||||
function formatErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch {
|
||||
return String(error);
|
||||
}
|
||||
}
|
||||
|
||||
const unqualifiedToolNames = {
|
||||
read: "Read",
|
||||
edit: "Edit",
|
||||
@@ -175,8 +189,8 @@ Usage:
|
||||
|
||||
if (result.wasLimited) {
|
||||
readInfo += `Read ${result.linesRead} lines (hit 50KB limit). `;
|
||||
} else {
|
||||
readInfo += `Read lines ${input.offset}-${result.linesRead}. `;
|
||||
} else if (input.offset && input.offset > 1) {
|
||||
readInfo += `Read lines ${input.offset}-${input.offset + result.linesRead}.`;
|
||||
}
|
||||
|
||||
if (result.wasLimited) {
|
||||
@@ -194,12 +208,13 @@ Usage:
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Reading file failed: " + error.message,
|
||||
text: "Reading file failed: " + formatErrorMessage(error),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -256,12 +271,13 @@ Usage:
|
||||
return {
|
||||
content: [],
|
||||
};
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Writing file failed: " + error.message,
|
||||
text: "Writing file failed: " + formatErrorMessage(error),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -347,12 +363,13 @@ Usage:
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Editing file failed: " + (error?.message ?? String(error)),
|
||||
text: "Editing file failed: " + formatErrorMessage(error),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -394,120 +411,132 @@ Output: Create directory 'foo'`),
|
||||
},
|
||||
},
|
||||
async (input: BashInput, extra) => {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const toolCallId = extra._meta?.["claudecode/toolUseId"];
|
||||
|
||||
if (typeof toolCallId !== "string") {
|
||||
throw new Error("No tool call ID found");
|
||||
}
|
||||
|
||||
if (!agent.clientCapabilities?.terminal || !agent.client.createTerminal) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
const handle = await agent.client.createTerminal({
|
||||
command: input.command,
|
||||
env: [{ name: "CLAUDECODE", value: "1" }],
|
||||
sessionId,
|
||||
outputByteLimit: 32_000,
|
||||
});
|
||||
|
||||
await agent.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId,
|
||||
status: "in_progress",
|
||||
title: input.description,
|
||||
content: [{ type: "terminal", terminalId: handle.id }],
|
||||
},
|
||||
});
|
||||
|
||||
const abortPromise = new Promise((resolve) => {
|
||||
if (extra.signal.aborted) {
|
||||
resolve(null);
|
||||
} else {
|
||||
extra.signal.addEventListener("abort", () => {
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const statusPromise = Promise.race([
|
||||
handle.waitForExit().then((exitStatus) => ({ status: "exited" as const, exitStatus })),
|
||||
abortPromise.then(() => ({ status: "aborted" as const, exitStatus: null })),
|
||||
sleep(input.timeout ?? 2 * 60 * 1000).then(async () => {
|
||||
if (agent.backgroundTerminals[handle.id]?.status === "started") {
|
||||
await handle.kill();
|
||||
}
|
||||
return { status: "timedOut" as const, exitStatus: null };
|
||||
}),
|
||||
]);
|
||||
|
||||
if (input.run_in_background) {
|
||||
agent.backgroundTerminals[handle.id] = {
|
||||
handle,
|
||||
lastOutput: null,
|
||||
status: "started",
|
||||
};
|
||||
|
||||
statusPromise.then(async ({ status, exitStatus }) => {
|
||||
const bgTerm = agent.backgroundTerminals[handle.id];
|
||||
|
||||
if (bgTerm.status !== "started") {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentOutput = await handle.currentOutput();
|
||||
|
||||
agent.backgroundTerminals[handle.id] = {
|
||||
status,
|
||||
pendingOutput: {
|
||||
...currentOutput,
|
||||
output: stripCommonPrefix(bgTerm.lastOutput?.output ?? "", currentOutput.output),
|
||||
exitStatus: exitStatus ?? currentOutput.exitStatus,
|
||||
},
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return handle.release();
|
||||
const toolCallId = extra._meta?.["claudecode/toolUseId"];
|
||||
|
||||
if (typeof toolCallId !== "string") {
|
||||
throw new Error("No tool call ID found");
|
||||
}
|
||||
|
||||
if (!agent.clientCapabilities?.terminal || !agent.client.createTerminal) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
const handle = await agent.client.createTerminal({
|
||||
command: input.command,
|
||||
env: [{ name: "CLAUDECODE", value: "1" }],
|
||||
sessionId,
|
||||
outputByteLimit: 32_000,
|
||||
});
|
||||
|
||||
await agent.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId,
|
||||
status: "in_progress",
|
||||
title: input.description,
|
||||
content: [{ type: "terminal", terminalId: handle.id }],
|
||||
},
|
||||
});
|
||||
|
||||
const abortPromise = new Promise((resolve) => {
|
||||
if (extra.signal.aborted) {
|
||||
resolve(null);
|
||||
} else {
|
||||
extra.signal.addEventListener("abort", () => {
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const statusPromise = Promise.race([
|
||||
handle.waitForExit().then((exitStatus) => ({ status: "exited" as const, exitStatus })),
|
||||
abortPromise.then(() => ({ status: "aborted" as const, exitStatus: null })),
|
||||
sleep(input.timeout ?? 2 * 60 * 1000).then(async () => {
|
||||
if (agent.backgroundTerminals[handle.id]?.status === "started") {
|
||||
await handle.kill();
|
||||
}
|
||||
return { status: "timedOut" as const, exitStatus: null };
|
||||
}),
|
||||
]);
|
||||
|
||||
if (input.run_in_background) {
|
||||
agent.backgroundTerminals[handle.id] = {
|
||||
handle,
|
||||
lastOutput: null,
|
||||
status: "started",
|
||||
};
|
||||
|
||||
statusPromise.then(async ({ status, exitStatus }) => {
|
||||
const bgTerm = agent.backgroundTerminals[handle.id];
|
||||
|
||||
if (bgTerm.status !== "started") {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentOutput = await handle.currentOutput();
|
||||
|
||||
agent.backgroundTerminals[handle.id] = {
|
||||
status,
|
||||
pendingOutput: {
|
||||
...currentOutput,
|
||||
output: stripCommonPrefix(bgTerm.lastOutput?.output ?? "", currentOutput.output),
|
||||
exitStatus: exitStatus ?? currentOutput.exitStatus,
|
||||
},
|
||||
};
|
||||
|
||||
return handle.release();
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Command started in background with id: ${handle.id}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
await using terminal = handle;
|
||||
|
||||
const { status } = await statusPromise;
|
||||
|
||||
if (status === "aborted") {
|
||||
return {
|
||||
content: [{ type: "text", text: "Tool cancelled by user" }],
|
||||
};
|
||||
}
|
||||
|
||||
const output = await terminal.currentOutput();
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: toolCommandOutput(status, output) }],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Command started in background with id: ${handle.id}`,
|
||||
text: "Running bash command failed: " + formatErrorMessage(error),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
await using terminal = handle;
|
||||
|
||||
const { status } = await statusPromise;
|
||||
|
||||
if (status === "aborted") {
|
||||
return {
|
||||
content: [{ type: "text", text: "Tool cancelled by user" }],
|
||||
};
|
||||
}
|
||||
|
||||
const output = await terminal.currentOutput();
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: toolCommandOutput(status, output) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -531,37 +560,49 @@ In sessions with ${acpToolNames.bashOutput} always use it for output from Bash c
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
const bgTerm = agent.backgroundTerminals[input.bash_id];
|
||||
try {
|
||||
const bgTerm = agent.backgroundTerminals[input.bash_id];
|
||||
|
||||
if (!bgTerm) {
|
||||
throw new Error(`Unknown shell ${input.bash_id}`);
|
||||
}
|
||||
if (!bgTerm) {
|
||||
throw new Error(`Unknown shell ${input.bash_id}`);
|
||||
}
|
||||
|
||||
if (bgTerm.status === "started") {
|
||||
const newOutput = await bgTerm.handle.currentOutput();
|
||||
const strippedOutput = stripCommonPrefix(
|
||||
bgTerm.lastOutput?.output ?? "",
|
||||
newOutput.output,
|
||||
);
|
||||
bgTerm.lastOutput = newOutput;
|
||||
if (bgTerm.status === "started") {
|
||||
const newOutput = await bgTerm.handle.currentOutput();
|
||||
const strippedOutput = stripCommonPrefix(
|
||||
bgTerm.lastOutput?.output ?? "",
|
||||
newOutput.output,
|
||||
);
|
||||
bgTerm.lastOutput = newOutput;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: toolCommandOutput(bgTerm.status, {
|
||||
...newOutput,
|
||||
output: strippedOutput,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: toolCommandOutput(bgTerm.status, bgTerm.pendingOutput),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: toolCommandOutput(bgTerm.status, {
|
||||
...newOutput,
|
||||
output: strippedOutput,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: toolCommandOutput(bgTerm.status, bgTerm.pendingOutput),
|
||||
text: "Retrieving bash output failed: " + formatErrorMessage(error),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -588,49 +629,61 @@ In sessions with ${acpToolNames.killShell} always use it instead of KillShell.`,
|
||||
},
|
||||
},
|
||||
async (input: KillShellInput) => {
|
||||
const bgTerm = agent.backgroundTerminals[input.shell_id];
|
||||
try {
|
||||
const bgTerm = agent.backgroundTerminals[input.shell_id];
|
||||
|
||||
if (!bgTerm) {
|
||||
throw new Error(`Unknown shell ${input.shell_id}`);
|
||||
}
|
||||
if (!bgTerm) {
|
||||
throw new Error(`Unknown shell ${input.shell_id}`);
|
||||
}
|
||||
|
||||
switch (bgTerm.status) {
|
||||
case "started": {
|
||||
await bgTerm.handle.kill();
|
||||
const currentOutput = await bgTerm.handle.currentOutput();
|
||||
agent.backgroundTerminals[bgTerm.handle.id] = {
|
||||
status: "killed",
|
||||
pendingOutput: {
|
||||
...currentOutput,
|
||||
output: stripCommonPrefix(bgTerm.lastOutput?.output ?? "", currentOutput.output),
|
||||
switch (bgTerm.status) {
|
||||
case "started": {
|
||||
await bgTerm.handle.kill();
|
||||
const currentOutput = await bgTerm.handle.currentOutput();
|
||||
agent.backgroundTerminals[bgTerm.handle.id] = {
|
||||
status: "killed",
|
||||
pendingOutput: {
|
||||
...currentOutput,
|
||||
output: stripCommonPrefix(bgTerm.lastOutput?.output ?? "", currentOutput.output),
|
||||
},
|
||||
};
|
||||
await bgTerm.handle.release();
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: "Command killed successfully." }],
|
||||
};
|
||||
}
|
||||
case "aborted":
|
||||
return {
|
||||
content: [{ type: "text", text: "Command aborted by user." }],
|
||||
};
|
||||
case "exited":
|
||||
return {
|
||||
content: [{ type: "text", text: "Command had already exited." }],
|
||||
};
|
||||
case "killed":
|
||||
return {
|
||||
content: [{ type: "text", text: "Command was already killed." }],
|
||||
};
|
||||
case "timedOut":
|
||||
return {
|
||||
content: [{ type: "text", text: "Command killed by timeout." }],
|
||||
};
|
||||
default: {
|
||||
unreachable(bgTerm);
|
||||
throw new Error("Unexpected background terminal status");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Killing shell failed: " + formatErrorMessage(error),
|
||||
},
|
||||
};
|
||||
await bgTerm.handle.release();
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: "Command killed successfully." }],
|
||||
};
|
||||
}
|
||||
case "aborted":
|
||||
return {
|
||||
content: [{ type: "text", text: "Command aborted by user." }],
|
||||
};
|
||||
case "exited":
|
||||
return {
|
||||
content: [{ type: "text", text: "Command had already exited." }],
|
||||
};
|
||||
case "killed":
|
||||
return {
|
||||
content: [{ type: "text", text: "Command was already killed." }],
|
||||
};
|
||||
case "timedOut":
|
||||
return {
|
||||
content: [{ type: "text", text: "Command killed by timeout." }],
|
||||
};
|
||||
default: {
|
||||
unreachable(bgTerm);
|
||||
throw new Error("Unexpected background terminal status");
|
||||
}
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: Say hello
|
||||
argument-hint: name
|
||||
argument-hint: [name]
|
||||
---
|
||||
|
||||
Respond with "Hello $1" and nothing else.
|
||||
|
||||
558
src/tests/typescript-declarations.test.ts
Normal file
558
src/tests/typescript-declarations.test.ts
Normal file
@@ -0,0 +1,558 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { spawnSync } from "child_process";
|
||||
|
||||
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
|
||||
"TypeScript declaration files integration",
|
||||
() => {
|
||||
let tempDir: string;
|
||||
let tarballPath: string;
|
||||
const projectRoot = path.resolve(__dirname, "../..");
|
||||
|
||||
// Base configuration templates
|
||||
const basePackageJson = {
|
||||
name: "ts-declaration-test",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
dependencies: {},
|
||||
devDependencies: {
|
||||
typescript: "5.9.3",
|
||||
"@types/node": "25.0.3",
|
||||
},
|
||||
};
|
||||
|
||||
const baseTsConfig = {
|
||||
compilerOptions: {
|
||||
target: "ES2020",
|
||||
module: "NodeNext",
|
||||
moduleResolution: "NodeNext",
|
||||
lib: ["ES2020"],
|
||||
strict: true,
|
||||
esModuleInterop: true,
|
||||
skipLibCheck: true, // Skip checking dependency internals, focus on our types
|
||||
noEmit: true,
|
||||
declaration: false,
|
||||
},
|
||||
include: ["*.ts"],
|
||||
};
|
||||
|
||||
// Build and pack the package once for all tests
|
||||
beforeAll(async () => {
|
||||
// Step 1: Clean dist folder to ensure fresh build
|
||||
const distPath = path.join(projectRoot, "dist");
|
||||
await fs.promises.rm(distPath, { recursive: true, force: true });
|
||||
console.log("Cleaned dist folder");
|
||||
|
||||
console.log("Building package...");
|
||||
|
||||
// Step 2: Build the package
|
||||
const buildResult = spawnSync("npm", ["run", "build"], {
|
||||
cwd: projectRoot,
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (buildResult.status !== 0) {
|
||||
throw new Error(`Build failed: ${buildResult.stderr || buildResult.stdout}`);
|
||||
}
|
||||
|
||||
console.log("Packing package...");
|
||||
|
||||
// Step 3: Pack to create tarball
|
||||
const packResult = spawnSync("npm", ["pack", "--pack-destination", os.tmpdir()], {
|
||||
cwd: projectRoot,
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (packResult.status !== 0) {
|
||||
throw new Error(`Pack failed: ${packResult.stderr || packResult.stdout}`);
|
||||
}
|
||||
|
||||
// Get the tarball filename from stdout (npm pack outputs the filename)
|
||||
const tarballName = packResult.stdout.trim().split("\n").pop();
|
||||
if (!tarballName) {
|
||||
throw new Error("Failed to get tarball name from npm pack output");
|
||||
}
|
||||
tarballPath = path.join(os.tmpdir(), tarballName);
|
||||
|
||||
console.log(`Tarball created at: ${tarballPath}`);
|
||||
}, 60000); // 60 second timeout for build
|
||||
|
||||
// Clean up the tarball after all tests
|
||||
afterAll(async () => {
|
||||
if (tarballPath && fs.existsSync(tarballPath)) {
|
||||
await fs.promises.unlink(tarballPath);
|
||||
console.log("Cleaned up tarball");
|
||||
}
|
||||
});
|
||||
|
||||
// Create fresh temp directory for each test
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "ts-declaration-test-"));
|
||||
});
|
||||
|
||||
// Clean up temp directory after each test
|
||||
afterEach(async () => {
|
||||
if (tempDir) {
|
||||
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to set up a test TypeScript project
|
||||
async function setupTestProject(
|
||||
packageJson: object = basePackageJson,
|
||||
tsconfig: object = baseTsConfig,
|
||||
): Promise<void> {
|
||||
// Write package.json
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "package.json"),
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
);
|
||||
|
||||
// Write tsconfig.json
|
||||
await fs.promises.writeFile(
|
||||
path.join(tempDir, "tsconfig.json"),
|
||||
JSON.stringify(tsconfig, null, 2),
|
||||
);
|
||||
|
||||
// Install all dependencies (TypeScript, @types/node, and the tarball)
|
||||
console.log(`Installing dependencies in ${tempDir}...`);
|
||||
const installResult = spawnSync("npm", ["install", tarballPath], {
|
||||
cwd: tempDir,
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
timeout: 60000, // 60 second timeout
|
||||
});
|
||||
|
||||
if (installResult.status !== 0) {
|
||||
throw new Error(`npm install failed: ${installResult.stderr || installResult.stdout}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to run TypeScript type checking
|
||||
function runTypeCheck(srcDir: string = tempDir): {
|
||||
success: boolean;
|
||||
output: string;
|
||||
} {
|
||||
const result = spawnSync("npx", ["tsc", "--noEmit"], {
|
||||
cwd: srcDir,
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
timeout: 30000, // 30 second timeout
|
||||
});
|
||||
|
||||
return {
|
||||
success: result.status === 0,
|
||||
output: result.stdout + result.stderr,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to write a test TypeScript file
|
||||
async function writeTestFile(filename: string, content: string): Promise<void> {
|
||||
await fs.promises.writeFile(path.join(tempDir, filename), content);
|
||||
}
|
||||
|
||||
// Test 1: Main exports import verification
|
||||
it("should successfully type-check main exports", async () => {
|
||||
await setupTestProject();
|
||||
|
||||
await writeTestFile(
|
||||
"test-main-exports.ts",
|
||||
`
|
||||
import {
|
||||
ClaudeAcpAgent,
|
||||
runAcp,
|
||||
toAcpNotifications,
|
||||
streamEventToAcpNotifications,
|
||||
SettingsManager,
|
||||
createMcpServer,
|
||||
loadManagedSettings,
|
||||
applyEnvironmentSettings,
|
||||
nodeToWebReadable,
|
||||
nodeToWebWritable,
|
||||
Pushable,
|
||||
unreachable,
|
||||
toolInfoFromToolUse,
|
||||
planEntries,
|
||||
toolUpdateFromToolResult,
|
||||
createPreToolUseHook,
|
||||
toolNames,
|
||||
} from "@zed-industries/claude-code-acp";
|
||||
|
||||
// Type-only imports
|
||||
import type {
|
||||
ToolUpdateMeta,
|
||||
NewSessionMeta,
|
||||
ClaudeCodeSettings,
|
||||
PermissionSettings,
|
||||
PermissionDecision,
|
||||
PermissionCheckResult,
|
||||
SettingsManagerOptions,
|
||||
ClaudePlanEntry,
|
||||
} from "@zed-industries/claude-code-acp";
|
||||
|
||||
// Verify exports exist and have expected types
|
||||
const _runAcp: typeof runAcp = runAcp;
|
||||
const _createMcpServer: typeof createMcpServer = createMcpServer;
|
||||
const _toolNames: typeof toolNames = toolNames;
|
||||
`,
|
||||
);
|
||||
|
||||
const result = runTypeCheck();
|
||||
if (!result.success) {
|
||||
console.error("TypeScript errors:", result.output);
|
||||
}
|
||||
expect(result.success).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
// Test 2: Deep imports verification (backwards compatibility)
|
||||
it("should successfully type-check deep imports", async () => {
|
||||
await setupTestProject();
|
||||
|
||||
await writeTestFile(
|
||||
"test-deep-imports.ts",
|
||||
`
|
||||
// Deep import from dist/tools.js
|
||||
import {
|
||||
acpToolNames,
|
||||
EDIT_TOOL_NAMES,
|
||||
ACP_TOOL_NAME_PREFIX,
|
||||
toolInfoFromToolUse,
|
||||
planEntries,
|
||||
} from "@zed-industries/claude-code-acp/dist/tools.js";
|
||||
|
||||
// Deep import from dist/settings.js
|
||||
import {
|
||||
SettingsManager,
|
||||
getManagedSettingsPath,
|
||||
} from "@zed-industries/claude-code-acp/dist/settings.js";
|
||||
|
||||
// Deep import from dist/utils.js
|
||||
import {
|
||||
Pushable,
|
||||
nodeToWebReadable,
|
||||
nodeToWebWritable,
|
||||
loadManagedSettings,
|
||||
} from "@zed-industries/claude-code-acp/dist/utils.js";
|
||||
|
||||
// Verify types work
|
||||
const prefix: string = ACP_TOOL_NAME_PREFIX;
|
||||
const editTools: readonly string[] = EDIT_TOOL_NAMES;
|
||||
`,
|
||||
);
|
||||
|
||||
const result = runTypeCheck();
|
||||
if (!result.success) {
|
||||
console.error("TypeScript errors:", result.output);
|
||||
}
|
||||
expect(result.success).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
// Test 3: SettingsManager type shape verification
|
||||
it("should verify SettingsManager has correct type shape", async () => {
|
||||
await setupTestProject();
|
||||
|
||||
await writeTestFile(
|
||||
"test-settings-manager.ts",
|
||||
`
|
||||
import {
|
||||
SettingsManager,
|
||||
ClaudeCodeSettings,
|
||||
PermissionCheckResult,
|
||||
SettingsManagerOptions
|
||||
} from "@zed-industries/claude-code-acp";
|
||||
|
||||
// Test constructor signature
|
||||
const options: SettingsManagerOptions = {
|
||||
onChange: () => {},
|
||||
logger: { log: console.log, error: console.error },
|
||||
};
|
||||
declare const cwd: string;
|
||||
const manager = new SettingsManager(cwd, options);
|
||||
|
||||
// Test method signatures
|
||||
async function testMethods() {
|
||||
// initialize returns Promise<void>
|
||||
await manager.initialize();
|
||||
|
||||
// checkPermission returns PermissionCheckResult
|
||||
const result: PermissionCheckResult = manager.checkPermission(
|
||||
"mcp__acp__Read",
|
||||
{ file_path: "/some/path" }
|
||||
);
|
||||
|
||||
// Verify decision type
|
||||
const decision: "allow" | "deny" | "ask" = result.decision;
|
||||
const rule: string | undefined = result.rule;
|
||||
const source: "allow" | "deny" | "ask" | undefined = result.source;
|
||||
|
||||
// getSettings returns ClaudeCodeSettings
|
||||
const settings: ClaudeCodeSettings = manager.getSettings();
|
||||
|
||||
// getCwd returns string
|
||||
const currentCwd: string = manager.getCwd();
|
||||
|
||||
// setCwd returns Promise<void>
|
||||
await manager.setCwd("/new/path");
|
||||
|
||||
// dispose returns void
|
||||
manager.dispose();
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const result = runTypeCheck();
|
||||
if (!result.success) {
|
||||
console.error("TypeScript errors:", result.output);
|
||||
}
|
||||
expect(result.success).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
// Test 4: ClaudeAcpAgent instantiation and type verification
|
||||
it("should verify ClaudeAcpAgent has correct type shape", async () => {
|
||||
await setupTestProject();
|
||||
|
||||
await writeTestFile(
|
||||
"test-claude-acp-agent.ts",
|
||||
`
|
||||
import { ClaudeAcpAgent } from "@zed-industries/claude-code-acp";
|
||||
import type { AgentSideConnection } from "@agentclientprotocol/sdk";
|
||||
|
||||
// ClaudeAcpAgent should be a class that can be instantiated
|
||||
declare const mockConnection: AgentSideConnection;
|
||||
declare const mockLogger: { log: (...args: any[]) => void; error: (...args: any[]) => void };
|
||||
|
||||
// Test constructor - accepts AgentSideConnection and optional logger
|
||||
const agent = new ClaudeAcpAgent(mockConnection, mockLogger);
|
||||
|
||||
// Verify it has sessions property
|
||||
const sessions: Record<string, any> = agent.sessions;
|
||||
|
||||
// Verify it has client property
|
||||
const client: AgentSideConnection = agent.client;
|
||||
|
||||
// Verify it has logger property
|
||||
const logger = agent.logger;
|
||||
`,
|
||||
);
|
||||
|
||||
const result = runTypeCheck();
|
||||
if (!result.success) {
|
||||
console.error("TypeScript errors:", result.output);
|
||||
}
|
||||
expect(result.success).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
// Test 5: Type-only exports work correctly
|
||||
it("should verify type-only exports are usable", async () => {
|
||||
await setupTestProject();
|
||||
|
||||
await writeTestFile(
|
||||
"test-type-exports.ts",
|
||||
`
|
||||
import type {
|
||||
ClaudeCodeSettings,
|
||||
PermissionSettings,
|
||||
PermissionDecision,
|
||||
PermissionCheckResult,
|
||||
SettingsManagerOptions,
|
||||
ClaudePlanEntry,
|
||||
ToolUpdateMeta,
|
||||
NewSessionMeta,
|
||||
} from "@zed-industries/claude-code-acp";
|
||||
|
||||
// Test ClaudeCodeSettings shape
|
||||
const settings: ClaudeCodeSettings = {
|
||||
permissions: {
|
||||
allow: ["Read"],
|
||||
deny: ["Read(./.env)"],
|
||||
ask: ["Bash"],
|
||||
additionalDirectories: ["/extra"],
|
||||
defaultMode: "default",
|
||||
},
|
||||
env: {
|
||||
API_KEY: "secret",
|
||||
},
|
||||
};
|
||||
|
||||
// Test PermissionSettings shape
|
||||
const perms: PermissionSettings = {
|
||||
allow: ["Read"],
|
||||
deny: ["Write"],
|
||||
};
|
||||
|
||||
// Test PermissionDecision
|
||||
const decisions: PermissionDecision[] = ["allow", "deny", "ask"];
|
||||
|
||||
// Test ClaudePlanEntry shape
|
||||
const planEntry: ClaudePlanEntry = {
|
||||
content: "Do something",
|
||||
status: "pending",
|
||||
activeForm: "Doing something",
|
||||
};
|
||||
|
||||
// Test valid status values
|
||||
const validStatuses: ClaudePlanEntry["status"][] = [
|
||||
"pending",
|
||||
"in_progress",
|
||||
"completed",
|
||||
];
|
||||
|
||||
// Test ToolUpdateMeta shape
|
||||
const toolMeta: ToolUpdateMeta = {
|
||||
claudeCode: {
|
||||
toolName: "Read",
|
||||
toolResponse: { success: true },
|
||||
},
|
||||
};
|
||||
|
||||
// Test NewSessionMeta shape
|
||||
const sessionMeta: NewSessionMeta = {
|
||||
claudeCode: {
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
`,
|
||||
);
|
||||
|
||||
const result = runTypeCheck();
|
||||
if (!result.success) {
|
||||
console.error("TypeScript errors:", result.output);
|
||||
}
|
||||
expect(result.success).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
// Test 6: Function signatures verification
|
||||
it("should verify function signatures are correct", async () => {
|
||||
await setupTestProject();
|
||||
|
||||
await writeTestFile(
|
||||
"test-function-signatures.ts",
|
||||
`
|
||||
import {
|
||||
runAcp,
|
||||
createMcpServer,
|
||||
toolInfoFromToolUse,
|
||||
planEntries,
|
||||
createPreToolUseHook,
|
||||
loadManagedSettings,
|
||||
applyEnvironmentSettings,
|
||||
SettingsManager,
|
||||
} from "@zed-industries/claude-code-acp";
|
||||
|
||||
import type { ClaudeCodeSettings } from "@zed-industries/claude-code-acp";
|
||||
|
||||
// runAcp should be a function with no parameters that returns void
|
||||
const runAcpType: () => void = runAcp;
|
||||
|
||||
// toolInfoFromToolUse should accept any and return object with title and kind
|
||||
const info = toolInfoFromToolUse({ name: "Read", input: {} });
|
||||
const title: string = info.title;
|
||||
const kind: string = info.kind;
|
||||
|
||||
// planEntries should accept todos array and return array
|
||||
const entries = planEntries({
|
||||
todos: [
|
||||
{ content: "test", status: "pending", activeForm: "testing" }
|
||||
]
|
||||
});
|
||||
// entries should be an array
|
||||
const entriesArray: any[] = entries;
|
||||
|
||||
// createPreToolUseHook should accept SettingsManager and optional logger
|
||||
declare const settingsManager: SettingsManager;
|
||||
const hook = createPreToolUseHook(settingsManager, console);
|
||||
|
||||
// loadManagedSettings should return ClaudeCodeSettings | null
|
||||
const managedSettings: ClaudeCodeSettings | null = loadManagedSettings();
|
||||
|
||||
// applyEnvironmentSettings should accept ClaudeCodeSettings and return void
|
||||
const applyResult: void = applyEnvironmentSettings({ permissions: {} });
|
||||
`,
|
||||
);
|
||||
|
||||
const result = runTypeCheck();
|
||||
if (!result.success) {
|
||||
console.error("TypeScript errors:", result.output);
|
||||
}
|
||||
expect(result.success).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
// Test 7: Pushable generic class verification
|
||||
it("should verify Pushable class works correctly", async () => {
|
||||
await setupTestProject();
|
||||
|
||||
await writeTestFile(
|
||||
"test-pushable.ts",
|
||||
`
|
||||
import { Pushable } from "@zed-industries/claude-code-acp";
|
||||
|
||||
// Pushable should be a generic class
|
||||
const pushable = new Pushable<string>();
|
||||
|
||||
// Should have push method
|
||||
pushable.push("test");
|
||||
|
||||
// Should have end method
|
||||
pushable.end();
|
||||
|
||||
// Should implement AsyncIterable
|
||||
async function consume() {
|
||||
for await (const item of pushable) {
|
||||
const str: string = item;
|
||||
}
|
||||
}
|
||||
|
||||
// Generic type parameter should work
|
||||
const numPushable = new Pushable<number>();
|
||||
numPushable.push(42);
|
||||
|
||||
interface MyType { id: number; name: string; }
|
||||
const customPushable = new Pushable<MyType>();
|
||||
customPushable.push({ id: 1, name: "test" });
|
||||
`,
|
||||
);
|
||||
|
||||
const result = runTypeCheck();
|
||||
if (!result.success) {
|
||||
console.error("TypeScript errors:", result.output);
|
||||
}
|
||||
expect(result.success).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
// Test 8: Verify incorrect types fail
|
||||
it("should fail type-check with incorrect types", async () => {
|
||||
await setupTestProject();
|
||||
|
||||
await writeTestFile(
|
||||
"test-invalid-types.ts",
|
||||
`
|
||||
import { SettingsManager, ClaudeCodeSettings } from "@zed-industries/claude-code-acp";
|
||||
|
||||
// This should fail - SettingsManager constructor requires string cwd
|
||||
// @ts-expect-error - Testing that wrong argument type fails
|
||||
const badManager = new SettingsManager(123);
|
||||
|
||||
// This should fail - ClaudeCodeSettings permissions must be an object
|
||||
const badSettings: ClaudeCodeSettings = {
|
||||
// @ts-expect-error - Testing that wrong type fails
|
||||
permissions: "not-an-object",
|
||||
};
|
||||
`,
|
||||
);
|
||||
|
||||
// This test should PASS because we expect tsc to catch these errors
|
||||
// with @ts-expect-error directives
|
||||
const result = runTypeCheck();
|
||||
if (!result.success) {
|
||||
// If it fails, it means @ts-expect-error didn't catch the error
|
||||
// which could mean the types are too permissive
|
||||
console.error("TypeScript errors (expected @ts-expect-error to catch):", result.output);
|
||||
}
|
||||
expect(result.success).toBe(true);
|
||||
}, 120000);
|
||||
},
|
||||
);
|
||||
@@ -9,7 +9,9 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
Reference in New Issue
Block a user