mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
Compare commits
1 Commits
v0.2.4
...
code-block
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abae47de80 |
@@ -1,6 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
*.min.js
|
||||
*.config.js
|
||||
.github/
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2020": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-console": "off",
|
||||
"no-constant-condition": "off",
|
||||
"default-case": "error",
|
||||
"prefer-const": "error",
|
||||
"no-var": "error",
|
||||
"eqeqeq": ["error", "always"],
|
||||
"curly": ["error", "all"]
|
||||
},
|
||||
"ignorePatterns": ["dist", "node_modules", "*.js"]
|
||||
}
|
||||
45
.github/workflows/ci.yml
vendored
45
.github/workflows/ci.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: "ci-${{ github.ref }}"
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: latest
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check formatting
|
||||
run: npm run format:check
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:run
|
||||
23
.github/workflows/publish.yml
vendored
23
.github/workflows/publish.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Publish Package to npmjs
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: npm ci
|
||||
# Add back provenance once repo is public
|
||||
- run: npm publish # --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
54
README.md
54
README.md
@@ -1,53 +1,3 @@
|
||||
# ACP adapter for Claude Code
|
||||
An ACP-compatible coding agent powered by the Claude Code SDK
|
||||
|
||||
[](https://www.npmjs.com/package/@zed-industries/claude-code-acp)
|
||||
|
||||
Use [Claude Code](https://www.anthropic.com/claude-code) from [ACP-compatible](https://agentclientprotocol.com) clients such as [Zed](https://zed.dev)!
|
||||
|
||||
This tool implements an ACP agent by using the official [Claude Code SDK](https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-overview), supporting:
|
||||
|
||||
- Context @-mentions
|
||||
- Images
|
||||
- Tool calls (with permission requests)
|
||||
- Following
|
||||
- Edit review
|
||||
- TODO lists
|
||||
- Interactive (and background) terminals
|
||||
- Custom [Slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands)
|
||||
- Client MCP servers
|
||||
|
||||
Learn more about the [Agent Client Protocol](https://agentclientprotocol.com/).
|
||||
|
||||
## How to use
|
||||
|
||||
### Zed
|
||||
|
||||
The latest version of Zed can already use this adapter out of the box.
|
||||
|
||||
To use Claude Code, open the Agent Panel and click "New Claude Code Thread" from the `+` button menu in the top-right:
|
||||
|
||||
https://github.com/user-attachments/assets/ddce66c7-79ac-47a3-ad59-4a6a3ca74903
|
||||
|
||||
Read the docs on [External Agent](https://zed.dev/docs/ai/external-agents) support.
|
||||
|
||||
### Other clients
|
||||
|
||||
Setup instructions for other clients are coming soon. Feel free to [submit a PR](https://github.com/zed-industries/claude-code-acp/pulls) to add yours!
|
||||
|
||||
#### Installation
|
||||
|
||||
Install the adapter from `npm`:
|
||||
|
||||
```bash
|
||||
$ npm install @zed-industries/claude-code-acp
|
||||
```
|
||||
|
||||
You can then use `claude-code-acp` as a regular ACP agent:
|
||||
|
||||
```
|
||||
$ ANTHROPIC_API_KEY=sk-... claude-code-acp
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
This wrapper lets you use the real version of Claude Code from the IDE of your choice.
|
||||
|
||||
589
package-lock.json
generated
589
package-lock.json
generated
@@ -1,45 +1,37 @@
|
||||
{
|
||||
"name": "@zed-industries/claude-code-acp",
|
||||
"version": "0.2.4",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@zed-industries/claude-code-acp",
|
||||
"version": "0.2.4",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-code": "^1.0.100",
|
||||
"@anthropic-ai/claude-code": "^1.0.96",
|
||||
"@modelcontextprotocol/sdk": "^1.17.4",
|
||||
"@types/express": "^5.0.3",
|
||||
"@zed-industries/agent-client-protocol": "0.2.0-alpha.4",
|
||||
"diff": "^8.0.2",
|
||||
"@zed-industries/agent-client-protocol": "0.1.2",
|
||||
"express": "^5.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"uuid": "11.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"claude-code-acp": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.42.0",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"eslint": "^8.56.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.0",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code": {
|
||||
"version": "1.0.100",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.100.tgz",
|
||||
"integrity": "sha512-b4FRo3t46kPawzE8pT7nxtDc9NdYPTFaPoGGns/ZNqsqRu2jKMiSjjt5DiFvfaBZA8Tnt2qXbv4nd1vqR9Ru8Q==",
|
||||
"version": "1.0.96",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.96.tgz",
|
||||
"integrity": "sha512-xnxhYzuh6PYlMcw56REMQiGMW20WaLLOvG8L8TObq70zhNKs3dro7nhYwHRe1c2ubTr20oIJK0aSkyD2BpO8nA==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"bin": {
|
||||
"claude": "cli.js"
|
||||
@@ -564,30 +556,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
|
||||
@@ -614,30 +582,6 @@
|
||||
"node": ">=10.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/module-importer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
@@ -1407,251 +1351,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz",
|
||||
"integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.42.0",
|
||||
"@typescript-eslint/type-utils": "8.42.0",
|
||||
"@typescript-eslint/utils": "8.42.0",
|
||||
"@typescript-eslint/visitor-keys": "8.42.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz",
|
||||
"integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.42.0",
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/typescript-estree": "8.42.0",
|
||||
"@typescript-eslint/visitor-keys": "8.42.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz",
|
||||
"integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.42.0",
|
||||
"@typescript-eslint/types": "^8.42.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz",
|
||||
"integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/visitor-keys": "8.42.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz",
|
||||
"integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz",
|
||||
"integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/typescript-estree": "8.42.0",
|
||||
"@typescript-eslint/utils": "8.42.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz",
|
||||
"integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz",
|
||||
"integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.42.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.42.0",
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/visitor-keys": "8.42.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz",
|
||||
"integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.42.0",
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"@typescript-eslint/typescript-estree": "8.42.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz",
|
||||
"integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.42.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
@@ -1797,9 +1496,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@zed-industries/agent-client-protocol": {
|
||||
"version": "0.2.0-alpha.4",
|
||||
"resolved": "https://registry.npmjs.org/@zed-industries/agent-client-protocol/-/agent-client-protocol-0.2.0-alpha.4.tgz",
|
||||
"integrity": "sha512-l/MdK0MrvEb55/U3wAtAN/MC04YK+N6vNyakNgLsK2JTcmG54n45gXa0OZ08lYUmEvLe0shdu1dDtzlOV/Luxw==",
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@zed-industries/agent-client-protocol/-/agent-client-protocol-0.1.2.tgz",
|
||||
"integrity": "sha512-A1RCN7Uf09vngbhBVLiZENenKYOYD2OGiILLGXPaSBloDbem8xmLaDmrKQuYl0kjLJ1DcWmVaHUv97n1l5vJ6w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"zod": "^3.0.0"
|
||||
@@ -1948,26 +1647,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
@@ -2216,9 +1903,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
|
||||
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
@@ -2421,19 +2109,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz",
|
||||
"integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||
@@ -2464,30 +2139,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
||||
@@ -2584,12 +2235,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
|
||||
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.5.tgz",
|
||||
"integrity": "sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
@@ -2665,36 +2316,6 @@
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
@@ -2756,19 +2377,6 @@
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||
@@ -2946,30 +2554,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
@@ -3163,16 +2747,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-path-inside": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||
@@ -3336,43 +2910,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
@@ -3395,19 +2932,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
@@ -3707,22 +3241,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -3949,19 +3467,6 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||
@@ -4273,19 +3778,6 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -4305,19 +3797,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
@@ -4362,16 +3841,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node/node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
31
package.json
31
package.json
@@ -3,33 +3,19 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bin": {
|
||||
"claude-code-acp": "./dist/index.js"
|
||||
},
|
||||
"version": "0.2.4",
|
||||
"version": "0.1.0",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
|
||||
"main": "dist/index.js",
|
||||
"main": "dist/acp-agent.js",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist/",
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "npm run build && npm run start",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"lint:fix": "eslint src --ext .ts --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"check": "npm run lint && npm run format:check",
|
||||
"test": "vitest",
|
||||
"test:integration": "RUN_INTEGRATION_TESTS=true vitest run",
|
||||
"test:run": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"prepublishOnly": "npm run build"
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -43,11 +29,10 @@
|
||||
"author": "Zed Industries",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-code": "^1.0.100",
|
||||
"@anthropic-ai/claude-code": "^1.0.96",
|
||||
"@modelcontextprotocol/sdk": "^1.17.4",
|
||||
"@types/express": "^5.0.3",
|
||||
"@zed-industries/agent-client-protocol": "0.2.0-alpha.4",
|
||||
"diff": "^8.0.2",
|
||||
"@zed-industries/agent-client-protocol": "0.1.2",
|
||||
"express": "^5.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"uuid": "11.1.0"
|
||||
@@ -56,12 +41,8 @@
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.42.0",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"eslint": "^8.56.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.0",
|
||||
"vitest": "^3.2.4"
|
||||
|
||||
259
src/acp-agent.ts
259
src/acp-agent.ts
@@ -2,40 +2,44 @@ import {
|
||||
Agent,
|
||||
AgentSideConnection,
|
||||
AuthenticateRequest,
|
||||
AvailableCommand,
|
||||
CancelNotification,
|
||||
ClientCapabilities,
|
||||
Client,
|
||||
InitializeRequest,
|
||||
InitializeResponse,
|
||||
LoadSessionRequest,
|
||||
NewSessionRequest,
|
||||
NewSessionResponse,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
ReadTextFileRequest,
|
||||
ReadTextFileResponse,
|
||||
RequestError,
|
||||
TerminalHandle,
|
||||
TerminalOutputResponse,
|
||||
WriteTextFileRequest,
|
||||
WriteTextFileResponse,
|
||||
ToolCallContent,
|
||||
ToolKind,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
import {
|
||||
McpServerConfig,
|
||||
Options,
|
||||
Query,
|
||||
query,
|
||||
SDKAssistantMessage,
|
||||
SDKMessage,
|
||||
SDKUserMessage,
|
||||
} from "@anthropic-ai/claude-code";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import * as readline from "node:readline";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import { nodeToWebReadable, nodeToWebWritable, Pushable, sleep, unreachable } from "./utils.js";
|
||||
import {
|
||||
nodeToWebReadable,
|
||||
nodeToWebWritable,
|
||||
Pushable,
|
||||
unreachable,
|
||||
} from "./utils.js";
|
||||
import { ContentBlock } from "@zed-industries/agent-client-protocol";
|
||||
import { SessionNotification } from "@zed-industries/agent-client-protocol";
|
||||
import { createMcpServer } from "./mcp-server.js";
|
||||
import { AddressInfo } from "node:net";
|
||||
import { toolInfoFromToolUse, planEntries, toolUpdateFromToolResult } from "./tools.js";
|
||||
import {
|
||||
toolInfoFromToolUse,
|
||||
planEntries,
|
||||
toolUpdateFromToolResult,
|
||||
} from "./tools.js";
|
||||
|
||||
type Session = {
|
||||
query: Query;
|
||||
@@ -43,49 +47,25 @@ type Session = {
|
||||
cancelled: boolean;
|
||||
};
|
||||
|
||||
type BackgroundTerminal =
|
||||
| {
|
||||
handle: TerminalHandle;
|
||||
status: "started";
|
||||
lastOutput: TerminalOutputResponse | null;
|
||||
}
|
||||
| {
|
||||
status: "aborted" | "exited" | "killed" | "timedOut";
|
||||
pendingOutput: TerminalOutputResponse;
|
||||
};
|
||||
|
||||
type ToolUseCache = {
|
||||
[key: string]: { type: "tool_use"; id: string; name: string; input: any };
|
||||
};
|
||||
|
||||
// Implement the ACP Agent interface
|
||||
export class ClaudeAcpAgent implements Agent {
|
||||
sessions: {
|
||||
[key: string]: Session;
|
||||
};
|
||||
client: AgentSideConnection;
|
||||
toolUseCache: ToolUseCache;
|
||||
fileContentCache: { [key: string]: string };
|
||||
backgroundTerminals: { [key: string]: BackgroundTerminal } = {};
|
||||
clientCapabilities?: ClientCapabilities;
|
||||
client: Client;
|
||||
toolUseCache: { [key: string]: any };
|
||||
|
||||
constructor(client: AgentSideConnection) {
|
||||
constructor(client: Client) {
|
||||
this.sessions = {};
|
||||
this.client = client;
|
||||
this.toolUseCache = {};
|
||||
this.fileContentCache = {};
|
||||
}
|
||||
|
||||
async initialize(request: InitializeRequest): Promise<InitializeResponse> {
|
||||
this.clientCapabilities = request.clientCapabilities;
|
||||
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
|
||||
return {
|
||||
protocolVersion: 1,
|
||||
// todo!()
|
||||
agentCapabilities: {
|
||||
promptCapabilities: {
|
||||
image: true,
|
||||
embeddedContext: true,
|
||||
},
|
||||
promptCapabilities: { image: true, embeddedContext: true },
|
||||
},
|
||||
authMethods: [
|
||||
{
|
||||
@@ -102,8 +82,8 @@ export class ClaudeAcpAgent implements Agent {
|
||||
};
|
||||
}
|
||||
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
|
||||
const sessionId = uuidv7();
|
||||
const input = new Pushable<SDKUserMessage>();
|
||||
let sessionId = uuidv7();
|
||||
let input = new Pushable<SDKUserMessage>();
|
||||
|
||||
const mcpServers: Record<string, McpServerConfig> = {};
|
||||
if (Array.isArray(params.mcpServers)) {
|
||||
@@ -119,8 +99,9 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
}
|
||||
|
||||
const server = await createMcpServer(this, sessionId, this.clientCapabilities);
|
||||
const address = server.address() as AddressInfo;
|
||||
// todo!() auth
|
||||
let server = await createMcpServer(this, sessionId);
|
||||
let address = server.address() as AddressInfo;
|
||||
mcpServers["acp"] = {
|
||||
type: "http",
|
||||
url: "http://127.0.0.1:" + address.port + "/mcp",
|
||||
@@ -129,41 +110,17 @@ export class ClaudeAcpAgent implements Agent {
|
||||
},
|
||||
};
|
||||
|
||||
const options: Options = {
|
||||
cwd: params.cwd,
|
||||
mcpServers,
|
||||
permissionPromptToolName: "mcp__acp__permission",
|
||||
stderr: (err) => console.error(err),
|
||||
// note: although not documented by the types, passing an absolute path
|
||||
// here works to find zed's managed node version.
|
||||
executable: process.execPath as any,
|
||||
};
|
||||
|
||||
const allowedTools = [];
|
||||
const disallowedTools = [];
|
||||
if (this.clientCapabilities?.fs?.readTextFile) {
|
||||
allowedTools.push("mcp__acp__read");
|
||||
disallowedTools.push("Read");
|
||||
}
|
||||
if (this.clientCapabilities?.fs?.writeTextFile) {
|
||||
allowedTools.push("mcp__acp__write");
|
||||
disallowedTools.push("Write", "Edit", "MultiEdit");
|
||||
}
|
||||
if (this.clientCapabilities?.terminal) {
|
||||
allowedTools.push("mcp__acp__BashOutput", "mcp__acp__KillBash");
|
||||
disallowedTools.push("Bash", "BashOutput", "KillBash");
|
||||
}
|
||||
|
||||
if (allowedTools.length > 0) {
|
||||
options.allowedTools = allowedTools;
|
||||
}
|
||||
if (disallowedTools.length > 0) {
|
||||
options.disallowedTools = disallowedTools;
|
||||
}
|
||||
|
||||
const q = query({
|
||||
let q = query({
|
||||
prompt: input,
|
||||
options,
|
||||
options: {
|
||||
cwd: params.cwd,
|
||||
mcpServers,
|
||||
allowedTools: ["mcp__acp__read"],
|
||||
disallowedTools: ["Read", "Write", "Edit", "MultiEdit"],
|
||||
strictMcpConfig: true,
|
||||
permissionPromptToolName: "mcp__acp__permission",
|
||||
stderr: (err) => console.error(err),
|
||||
},
|
||||
});
|
||||
this.sessions[sessionId] = {
|
||||
query: q,
|
||||
@@ -171,14 +128,12 @@ export class ClaudeAcpAgent implements Agent {
|
||||
cancelled: false,
|
||||
};
|
||||
|
||||
const availableCommands = await availableSlashCommands(q);
|
||||
return {
|
||||
sessionId,
|
||||
availableCommands,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(_params: AuthenticateRequest): Promise<void> {
|
||||
async authenticate(params: AuthenticateRequest): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
@@ -193,7 +148,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
|
||||
input.push(promptToClaude(params));
|
||||
while (true) {
|
||||
const { value: message, done } = await query.next();
|
||||
let { value: message, done } = await query.next();
|
||||
if (done || !message) {
|
||||
if (this.sessions[params.sessionId].cancelled) {
|
||||
return { stopReason: "cancelled" };
|
||||
@@ -231,8 +186,8 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}
|
||||
|
||||
if (
|
||||
message.message.model === "<synthetic>" &&
|
||||
message.message.content.length === 1 &&
|
||||
message.message.model == "<synthetic>" &&
|
||||
message.message.content.length == 1 &&
|
||||
message.message.content[0].text.includes("Please run /login")
|
||||
) {
|
||||
throw RequestError.authRequired();
|
||||
@@ -241,7 +196,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
message,
|
||||
params.sessionId,
|
||||
this.toolUseCache,
|
||||
this.fileContentCache,
|
||||
)) {
|
||||
await this.client.sessionUpdate(notification);
|
||||
}
|
||||
@@ -261,88 +215,6 @@ export class ClaudeAcpAgent implements Agent {
|
||||
this.sessions[params.sessionId].cancelled = true;
|
||||
await this.sessions[params.sessionId].query.interrupt();
|
||||
}
|
||||
|
||||
async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
const response = await this.client.readTextFile(params);
|
||||
if (!params.limit && !params.line) {
|
||||
this.fileContentCache[params.path] = response.content;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
||||
const response = await this.client.writeTextFile(params);
|
||||
this.fileContentCache[params.path] = params.content;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
async function availableSlashCommands(query: Query): Promise<AvailableCommand[]> {
|
||||
const UNSUPPORTED_COMMANDS = [
|
||||
"add-dir",
|
||||
"agents", // Modal
|
||||
"bashes", // Modal
|
||||
"bug", // Modal
|
||||
"clear", // Escape Codes
|
||||
"compact", // Not supported via SDK?
|
||||
"config", // Modal
|
||||
"context", // Escape Codes
|
||||
"cost", // Escape Codes
|
||||
"doctor", // Escape Codes
|
||||
"exit",
|
||||
"export", // Modal
|
||||
"help", // Modal
|
||||
"hooks", // Modal
|
||||
"ide", // Modal
|
||||
"install-github-app", // Modal
|
||||
"login",
|
||||
"logout",
|
||||
"memory",
|
||||
"mcp",
|
||||
"migrate-installer", // Modal
|
||||
"model", // Not supported via SDK?
|
||||
"output-style", // Modal
|
||||
"output-style:new", // Modal
|
||||
"permissions", // Modal
|
||||
"privacy-settings",
|
||||
"release-notes", // Escape Codes
|
||||
"resume",
|
||||
"status", // Not supported via SDK?
|
||||
"statusline", // Not needed
|
||||
"terminal-setup", // Not needed
|
||||
"todos", // Escape Codes
|
||||
"vim", // Not needed
|
||||
];
|
||||
|
||||
const commands = await Promise.race([
|
||||
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
|
||||
(query as any).supportedCommands(),
|
||||
sleep(10000).then(() => {
|
||||
if (
|
||||
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
|
||||
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
|
||||
) {
|
||||
throw RequestError.authRequired();
|
||||
}
|
||||
throw new Error(
|
||||
"Failed to intialize Claude Code.\n\nThis may be caused by incorrect MCP server configuration, try disabling them.",
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return commands
|
||||
.map((command: { name: string; description: string; argumentHint: string }) => {
|
||||
const input = command.argumentHint ? { hint: command.argumentHint } : null;
|
||||
return {
|
||||
name: command.name,
|
||||
description: command.description || "",
|
||||
input,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(command: AvailableCommand) =>
|
||||
!(command.name.match(/\(MCP\)/) || UNSUPPORTED_COMMANDS.includes(command.name)),
|
||||
);
|
||||
}
|
||||
|
||||
function formatUriAsLink(uri: string): string {
|
||||
@@ -440,14 +312,13 @@ function promptToClaude(prompt: PromptRequest): SDKUserMessage {
|
||||
export function toAcpNotifications(
|
||||
message: SDKAssistantMessage | SDKUserMessage,
|
||||
sessionId: string,
|
||||
toolUseCache: ToolUseCache,
|
||||
fileContentCache: { [key: string]: string },
|
||||
toolUseCache: { [key: string]: any },
|
||||
): SessionNotification[] {
|
||||
const chunks = message.message.content as ContentChunk[];
|
||||
const output = [];
|
||||
let chunks = message.message.content as ContentChunk[];
|
||||
let output = [];
|
||||
// Only handle the first chunk for streaming; extend as needed for batching
|
||||
for (const chunk of chunks) {
|
||||
let update: SessionNotification["update"] | null = null;
|
||||
let update: SessionNotification["update"];
|
||||
switch (chunk.type) {
|
||||
case "text":
|
||||
update = {
|
||||
@@ -464,7 +335,8 @@ export function toAcpNotifications(
|
||||
content: {
|
||||
type: "image",
|
||||
data: chunk.source.type === "base64" ? chunk.source.data : "",
|
||||
mimeType: chunk.source.type === "base64" ? chunk.source.media_type : "",
|
||||
mimeType:
|
||||
chunk.source.type === "base64" ? chunk.source.media_type : "",
|
||||
uri: chunk.source.type === "url" ? chunk.source.url : undefined,
|
||||
},
|
||||
};
|
||||
@@ -480,7 +352,7 @@ export function toAcpNotifications(
|
||||
break;
|
||||
case "tool_use":
|
||||
toolUseCache[chunk.id] = chunk;
|
||||
if (chunk.name === "TodoWrite") {
|
||||
if (chunk.name == "TodoWrite") {
|
||||
update = {
|
||||
sessionUpdate: "plan",
|
||||
entries: planEntries(chunk.input),
|
||||
@@ -491,37 +363,24 @@ export function toAcpNotifications(
|
||||
sessionUpdate: "tool_call",
|
||||
rawInput: chunk.input,
|
||||
status: "pending",
|
||||
...toolInfoFromToolUse(chunk, fileContentCache),
|
||||
...toolInfoFromToolUse(chunk),
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "tool_result": {
|
||||
const toolUse = toolUseCache[chunk.tool_use_id];
|
||||
if (!toolUse) {
|
||||
console.error(
|
||||
`[claude-code-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (toolUse.name !== "TodoWrite") {
|
||||
update = {
|
||||
toolCallId: chunk.tool_use_id,
|
||||
sessionUpdate: "tool_call_update",
|
||||
status: chunk.is_error ? "failed" : "completed",
|
||||
...toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id]),
|
||||
};
|
||||
}
|
||||
case "tool_result":
|
||||
update = {
|
||||
toolCallId: chunk.tool_use_id,
|
||||
sessionUpdate: "tool_call_update",
|
||||
status: chunk.is_error ? "failed" : "completed",
|
||||
...toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id]),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error("unhandled chunk type: " + chunk.type);
|
||||
}
|
||||
if (update) {
|
||||
output.push({ sessionId, update });
|
||||
}
|
||||
output.push({ sessionId, update });
|
||||
}
|
||||
|
||||
return output;
|
||||
@@ -537,7 +396,7 @@ export function runAcp() {
|
||||
|
||||
type ContentChunk =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "tool_use"; id: string; name: string; input: any }
|
||||
| { type: "tool_use"; id: string; name: string; input: any } // input is serde_json::Value, so use any or unknown
|
||||
| {
|
||||
type: "tool_result";
|
||||
content: string;
|
||||
|
||||
13
src/index.ts
13
src/index.ts
@@ -1,16 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// stdout is used to send messages to the client
|
||||
// we redirect everything else to stderr to make sure it doesn't interfere with ACP
|
||||
console.log = console.error;
|
||||
console.info = console.error;
|
||||
console.warn = console.error;
|
||||
console.debug = console.error;
|
||||
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
console.error("Unhandled Rejection at:", promise, "reason:", reason);
|
||||
});
|
||||
|
||||
import { runAcp as runAcp } from "./acp-agent.js";
|
||||
runAcp();
|
||||
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import express from "express";
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { z } from "zod";
|
||||
import { Server } from "node:http";
|
||||
import { ClaudeAcpAgent } from "./acp-agent.js";
|
||||
import { ClientCapabilities, TerminalOutputResponse } from "@zed-industries/agent-client-protocol";
|
||||
import * as diff from "diff";
|
||||
|
||||
import { sleep, unreachable } from "./utils.js";
|
||||
|
||||
export const SYSTEM_REMINDER = `
|
||||
|
||||
<system-reminder>
|
||||
Whenever 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.
|
||||
</system-reminder>`;
|
||||
|
||||
export function createMcpServer(
|
||||
agent: ClaudeAcpAgent,
|
||||
sessionId: string,
|
||||
clientCapabilities: ClientCapabilities | undefined,
|
||||
): Promise<Server> {
|
||||
// Create MCP server
|
||||
const server = new McpServer({
|
||||
@@ -27,163 +16,173 @@ export function createMcpServer(
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
if (clientCapabilities?.fs?.readTextFile) {
|
||||
server.registerTool(
|
||||
"read",
|
||||
{
|
||||
title: "Read",
|
||||
description: `Reads the content of the given file in the project.
|
||||
server.registerTool(
|
||||
"read",
|
||||
{
|
||||
title: "Read",
|
||||
description: `Reads the content of the given file in the project.
|
||||
|
||||
Never attempt to read a path that hasn't been previously mentioned.
|
||||
|
||||
In sessions with mcp__acp__read always use it instead of Read as it contains the most up-to-date contents.`,
|
||||
inputSchema: {
|
||||
abs_path: z.string().describe("The absolute path to the file to read."),
|
||||
offset: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Which line to start reading from. Omit to start from the beginning."),
|
||||
limit: z.number().optional().describe("How many lines to read. Omit for the whole file."),
|
||||
},
|
||||
annotations: {
|
||||
title: "Read file",
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
inputSchema: {
|
||||
abs_path: z.string().describe("The absolute path to the file to read."),
|
||||
offset: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe(
|
||||
"Which line to start reading from. Omit to start from the beginning.",
|
||||
),
|
||||
limit: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("How many lines to read. Omit for the whole file."),
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
const content = await agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
limit: input.limit,
|
||||
line: input.offset,
|
||||
});
|
||||
|
||||
annotations: {
|
||||
title: "Read file",
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: content.content + SYSTEM_REMINDER,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Reading file failed: " + error.message,
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
let content = await agent.client.readTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
limit: input.limit,
|
||||
line: input.offset,
|
||||
});
|
||||
|
||||
if (clientCapabilities?.fs?.writeTextFile) {
|
||||
server.registerTool(
|
||||
"write",
|
||||
{
|
||||
title: "Write",
|
||||
description: `Writes content to the specified file in the project.
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
content.content +
|
||||
`
|
||||
|
||||
<system-reminder>
|
||||
Whenever 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.
|
||||
</system-reminder>`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Reading file failed: " + error.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"write",
|
||||
{
|
||||
title: "Write",
|
||||
description: `Writes content to the specified file in the project.
|
||||
|
||||
In sessions with mcp__acp__write always use it instead of Write as it will
|
||||
allow the user to conveniently review changes.`,
|
||||
inputSchema: {
|
||||
abs_path: z.string().describe("The absolute path to the file to write"),
|
||||
content: z.string().describe("The full content to write"),
|
||||
},
|
||||
annotations: {
|
||||
title: "Write file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
inputSchema: {
|
||||
abs_path: z.string().describe("The absolute path to the file to write"),
|
||||
content: z.string().describe("The full content to write"),
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
await agent.writeTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
content: input.content,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [],
|
||||
};
|
||||
} catch (error: any) {
|
||||
annotations: {
|
||||
title: "Write file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Writing file failed: " + error.message,
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
let content = await agent.client.writeTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
content: input.content,
|
||||
});
|
||||
|
||||
server.registerTool(
|
||||
"edit",
|
||||
{
|
||||
title: "Edit",
|
||||
description: `Edit a file.
|
||||
return {
|
||||
content: [],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Writing file failed: " + error.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"edit",
|
||||
{
|
||||
title: "Edit",
|
||||
description: `Edit a file.
|
||||
|
||||
In sessions with mcp__acp__edit always use it instead of Edit as it will
|
||||
allow the user to conveniently review changes.
|
||||
|
||||
File editing instructions:
|
||||
- The \`old_string\` param must match existing file content, including indentation.
|
||||
- The \`old_string\` param must come from the actual file, not an outline.
|
||||
- The \`old_string\` section must not be empty.
|
||||
- The \`old_text\` param must match existing file content, including indentation.
|
||||
- The \`old_text\` param must come from the actual file, not an outline.
|
||||
- The \`old_text\` section must not be empty.
|
||||
- Be minimal with replacements:
|
||||
- For unique lines, include only those lines.
|
||||
- For non-unique lines, include enough context to identify them.
|
||||
- Do not escape quotes, newlines, or other characters.
|
||||
- Only edit the specified file.
|
||||
- If the \`old_string\` value isn't found in the file, the edit won't be applied. The tool will fail and must be retried.`,
|
||||
inputSchema: {
|
||||
abs_path: z.string().describe("The absolute path to the file to read."),
|
||||
old_string: z.string().describe("The old text to replace (must be unique in the file)"),
|
||||
new_string: z.string().describe("The new text."),
|
||||
},
|
||||
annotations: {
|
||||
title: "Edit file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
- Only edit the specified file.`,
|
||||
inputSchema: {
|
||||
abs_path: z.string().describe("The absolute path to the file to read."),
|
||||
old_text: z
|
||||
.string()
|
||||
.describe("The old text to replace (must be unique in the file)"),
|
||||
new_text: z.string().describe("The new text."),
|
||||
},
|
||||
async (input) => {
|
||||
annotations: {
|
||||
title: "Edit file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
@@ -196,112 +195,25 @@ File editing instructions:
|
||||
};
|
||||
}
|
||||
|
||||
const { content } = await agent.readTextFile({
|
||||
let { content } = await agent.client.readTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
});
|
||||
|
||||
const { newContent } = replaceAndCalculateLocation(content, [
|
||||
{
|
||||
oldText: input.old_string,
|
||||
newText: input.new_string,
|
||||
replaceAll: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const patch = diff.createPatch(input.abs_path, content, newContent);
|
||||
|
||||
await agent.writeTextFile({
|
||||
sessionId,
|
||||
path: input.abs_path,
|
||||
content: newContent,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
const { newContent, lineNumbers } = replaceAndCalculateLocation(
|
||||
content,
|
||||
[
|
||||
{
|
||||
type: "text",
|
||||
text: patch,
|
||||
oldText: input.old_text,
|
||||
newText: input.new_text,
|
||||
replaceAll: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"multi-edit",
|
||||
{
|
||||
title: "Multi Edit",
|
||||
description: `Edit a file with multiple sequential edits.
|
||||
|
||||
In sessions with mcp__acp__multi-edit always use it instead of MultiEdit as it will
|
||||
allow the user to conveniently review changes.
|
||||
|
||||
File editing instructions:
|
||||
- The \`old_string\` param must match existing file content, including indentation.
|
||||
- The \`old_string\` param must come from the actual file, not an outline.
|
||||
- The \`old_string\` section must not be empty.
|
||||
- Be minimal with replacements:
|
||||
- For unique lines, include only those lines.
|
||||
- For non-unique lines, include enough context to identify them, unless you're using \`replace_all\`
|
||||
- Do not escape quotes, newlines, or other characters.
|
||||
- If any of the provided \`old_string\` values aren't found in the file, no edits will be applied. The tool will fail and must be retried.`,
|
||||
inputSchema: {
|
||||
file_path: z.string().describe("The absolute path to the file to modify"),
|
||||
edits: z
|
||||
.array(
|
||||
z.object({
|
||||
old_string: z.string().describe("The text to replace"),
|
||||
new_string: z.string().describe("The text to replace it with"),
|
||||
replace_all: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Replace all occurrences of old_string (default false)"),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.describe("Array of edit operations to perform sequentially on the file"),
|
||||
},
|
||||
annotations: {
|
||||
title: "Multi Edit file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const { content } = await agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
});
|
||||
|
||||
const { newContent } = replaceAndCalculateLocation(
|
||||
content,
|
||||
input.edits.map((edit) => ({
|
||||
oldText: edit.old_string,
|
||||
newText: edit.new_string,
|
||||
replaceAll: edit.replace_all ?? false,
|
||||
})),
|
||||
);
|
||||
|
||||
const patch = diff.createPatch(input.file_path, content, newContent);
|
||||
|
||||
await agent.writeTextFile({
|
||||
await agent.client.writeTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
path: input.abs_path,
|
||||
content: newContent,
|
||||
});
|
||||
|
||||
@@ -309,261 +221,103 @@ File editing instructions:
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: patch,
|
||||
text: JSON.stringify({ lineNumbers }),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (agent.clientCapabilities?.terminal) {
|
||||
server.registerTool(
|
||||
"Bash",
|
||||
{
|
||||
title: "Bash",
|
||||
description: "Executes a bash command",
|
||||
inputSchema: {
|
||||
command: z.string().describe("The bash command to execute as a one-liner"),
|
||||
timeout_ms: z
|
||||
.number()
|
||||
.default(2 * 60 * 1000)
|
||||
.describe("Optional timeout in milliseconds"),
|
||||
run_in_background: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe(
|
||||
"When set to true, the command is started in the background. The tool returns an `id` that can be used with the `mcp__acp__BashOutput` tool to retrieve the current output, or the `mcp__acp__KillBash` tool to stop it early.",
|
||||
),
|
||||
},
|
||||
},
|
||||
async (input, 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,
|
||||
sessionId,
|
||||
outputByteLimit: 32_000,
|
||||
});
|
||||
|
||||
await agent.client.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId,
|
||||
status: "in_progress",
|
||||
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_ms).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();
|
||||
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [{ type: "text", text: toolCommandOutput(status, output) }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Editing file failed: " + error.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"multi-edit",
|
||||
{
|
||||
title: "Multi Edit",
|
||||
description: `Edit a file with multiple sequential edits.`,
|
||||
inputSchema: {
|
||||
file_path: z
|
||||
.string()
|
||||
.describe("The absolute path to the file to modify"),
|
||||
edits: z
|
||||
.array(
|
||||
z.object({
|
||||
old_string: z.string().describe("The text to replace"),
|
||||
new_string: z.string().describe("The text to replace it with"),
|
||||
replace_all: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"Replace all occurrences of old_string (default false)",
|
||||
),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.describe(
|
||||
"Array of edit operations to perform sequentially on the file",
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"BashOutput",
|
||||
{
|
||||
title: "BashOutput",
|
||||
description:
|
||||
"Returns the current output and exit status of a background bash command by its id. Includes only new output since last invocation.",
|
||||
inputSchema: {
|
||||
id: z
|
||||
.string()
|
||||
.describe("The id of the background bash command as returned by `mcp__acp__Bash`"),
|
||||
},
|
||||
annotations: {
|
||||
title: "Multi Edit file",
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
openWorldHint: false,
|
||||
idempotentHint: false,
|
||||
},
|
||||
async (input) => {
|
||||
const bgTerm = agent.backgroundTerminals[input.id];
|
||||
},
|
||||
async (input) => {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (!bgTerm) {
|
||||
throw new Error(`Unknown shell ${input.id}`);
|
||||
}
|
||||
let { content } = await agent.client.readTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
});
|
||||
|
||||
if (bgTerm.status === "started") {
|
||||
const newOutput = await bgTerm.handle.currentOutput();
|
||||
const strippedOutput = stripCommonPrefix(
|
||||
bgTerm.lastOutput?.output ?? "",
|
||||
newOutput.output,
|
||||
);
|
||||
bgTerm.lastOutput = newOutput;
|
||||
const { newContent, lineNumbers } = replaceAndCalculateLocation(
|
||||
content,
|
||||
input.edits.map((edit) => ({
|
||||
oldText: edit.old_string,
|
||||
newText: edit.new_string,
|
||||
replaceAll: edit.replace_all ?? false,
|
||||
})),
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: toolCommandOutput(bgTerm.status, {
|
||||
...newOutput,
|
||||
output: strippedOutput,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: toolCommandOutput(bgTerm.status, bgTerm.pendingOutput),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
await agent.client.writeTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
content: newContent,
|
||||
});
|
||||
|
||||
server.registerTool(
|
||||
"KillBash",
|
||||
{
|
||||
title: "KillBash",
|
||||
description: "Stops a background command by its id",
|
||||
inputSchema: {
|
||||
id: z
|
||||
.string()
|
||||
.describe("The id of the background bash command as returned by `mcp__acp__Bash`"),
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
const bgTerm = agent.backgroundTerminals[input.id];
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ lineNumbers }),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
if (!bgTerm) {
|
||||
throw new Error(`Unknown shell ${input.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),
|
||||
},
|
||||
};
|
||||
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: {
|
||||
return unreachable(bgTerm);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const alwaysAllowedTools: { [key: string]: boolean } = {};
|
||||
let alwaysAllowedTools: { [key: string]: boolean } = {};
|
||||
server.registerTool(
|
||||
"permission",
|
||||
{
|
||||
@@ -603,7 +357,7 @@ File editing instructions:
|
||||
],
|
||||
};
|
||||
}
|
||||
const response = await agent.client.requestPermission({
|
||||
let response = await agent.client.requestPermission({
|
||||
options: [
|
||||
{
|
||||
kind: "allow_always",
|
||||
@@ -620,10 +374,11 @@ File editing instructions:
|
||||
},
|
||||
});
|
||||
if (
|
||||
response.outcome?.outcome === "selected" &&
|
||||
(response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")
|
||||
response.outcome?.outcome == "selected" &&
|
||||
(response.outcome.optionId == "allow" ||
|
||||
response.outcome.optionId == "allow_always")
|
||||
) {
|
||||
if (response.outcome.optionId === "allow_always") {
|
||||
if (response.outcome.optionId == "allow_always") {
|
||||
alwaysAllowedTools[input.tool_name] = true;
|
||||
}
|
||||
return {
|
||||
@@ -658,9 +413,10 @@ File editing instructions:
|
||||
|
||||
app.post("/mcp", async (req, res) => {
|
||||
try {
|
||||
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
});
|
||||
const transport: StreamableHTTPServerTransport =
|
||||
new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
});
|
||||
res.on("close", () => {
|
||||
transport.close();
|
||||
server.close();
|
||||
@@ -673,7 +429,7 @@ File editing instructions:
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32603,
|
||||
message: `Internal server error: ${error}`,
|
||||
message: "Internal server error",
|
||||
},
|
||||
id: null,
|
||||
});
|
||||
@@ -692,67 +448,6 @@ File editing instructions:
|
||||
});
|
||||
}
|
||||
|
||||
function stripCommonPrefix(a: string, b: string): string {
|
||||
let i = 0;
|
||||
while (i < a.length && i < b.length && a[i] === b[i]) {
|
||||
i++;
|
||||
}
|
||||
return b.slice(i);
|
||||
}
|
||||
|
||||
function toolCommandOutput(
|
||||
status: "started" | "aborted" | "exited" | "killed" | "timedOut",
|
||||
output: TerminalOutputResponse,
|
||||
): string {
|
||||
const { exitStatus, output: commandOutput, truncated } = output;
|
||||
|
||||
let toolOutput = "";
|
||||
|
||||
switch (status) {
|
||||
case "started":
|
||||
case "exited": {
|
||||
if (exitStatus && (exitStatus.exitCode ?? null) === null) {
|
||||
toolOutput += `Interrupted by the user. `;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "killed":
|
||||
toolOutput += `Killed. `;
|
||||
break;
|
||||
case "timedOut":
|
||||
toolOutput += `Timed out. `;
|
||||
break;
|
||||
case "aborted":
|
||||
break;
|
||||
default: {
|
||||
const unreachable: never = status;
|
||||
return unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
if (exitStatus) {
|
||||
if (typeof exitStatus.exitCode === "number") {
|
||||
toolOutput += `Exited with code ${exitStatus.exitCode}.`;
|
||||
}
|
||||
|
||||
if (typeof exitStatus.signal === "string") {
|
||||
toolOutput += `Signal \`${exitStatus.signal}\`. `;
|
||||
}
|
||||
|
||||
toolOutput += "Final output:\n\n";
|
||||
} else {
|
||||
toolOutput += "New output:\n\n";
|
||||
}
|
||||
|
||||
toolOutput += commandOutput;
|
||||
|
||||
if (truncated) {
|
||||
toolOutput += `\n\nCommand output was too long, so it was truncated to ${commandOutput.length} bytes.`;
|
||||
}
|
||||
|
||||
return toolOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace text in a file and calculate the line numbers where the edits occurred.
|
||||
*
|
||||
@@ -779,7 +474,7 @@ export function replaceAndCalculateLocation(
|
||||
for (const edit of edits) {
|
||||
// Skip empty oldText
|
||||
if (edit.oldText === "") {
|
||||
throw new Error(`The provided \`old_string\` is empty.\n\nNo edits were applied.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (edit.replaceAll) {
|
||||
@@ -790,14 +485,7 @@ export function replaceAndCalculateLocation(
|
||||
|
||||
while (true) {
|
||||
const index = currentContent.indexOf(edit.oldText, searchIndex);
|
||||
if (index === -1) {
|
||||
if (searchIndex === 0) {
|
||||
throw new Error(
|
||||
`The provided \`old_string\` does not appear in the file: "${edit.oldText}".\n\nNo edits were applied.`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (index === -1) break;
|
||||
|
||||
// Add content before the match
|
||||
parts.push(currentContent.substring(lastIndex, index));
|
||||
@@ -817,11 +505,7 @@ export function replaceAndCalculateLocation(
|
||||
} else {
|
||||
// Replace first occurrence only
|
||||
const index = currentContent.indexOf(edit.oldText);
|
||||
if (index === -1) {
|
||||
throw new Error(
|
||||
`The provided \`old_string\` does not appear in the file: "${edit.oldText}".\n\nNo edits were applied.`,
|
||||
);
|
||||
} else {
|
||||
if (index !== -1) {
|
||||
const marker = `${markerPrefix}${markerCounter++}__`;
|
||||
markers.push(marker);
|
||||
currentContent =
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
description: 10 * 3 = 30
|
||||
---
|
||||
|
||||
What is 10 \* 3. Only respond with the number.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
description: Say hello
|
||||
argument-hint: [name]
|
||||
---
|
||||
|
||||
Respond with "Hello $1" and nothing else.
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Agent,
|
||||
Client,
|
||||
ClientSideConnection,
|
||||
NewSessionResponse,
|
||||
ReadTextFileRequest,
|
||||
ReadTextFileResponse,
|
||||
RequestPermissionRequest,
|
||||
@@ -14,168 +13,89 @@ import {
|
||||
WriteTextFileResponse,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
|
||||
import { markdownEscape, toolInfoFromToolUse, toolUpdateFromToolResult } from "../tools.js";
|
||||
import { toolInfoFromToolUse, toolUpdateFromToolResult } from "../tools.js";
|
||||
import { toAcpNotifications } from "../acp-agent.js";
|
||||
import { SDKAssistantMessage } from "@anthropic-ai/claude-code";
|
||||
|
||||
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration", () => {
|
||||
let child: ReturnType<typeof spawn>;
|
||||
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
|
||||
"ACP subprocess integration",
|
||||
() => {
|
||||
let child: ReturnType<typeof spawn>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const valid = spawnSync("tsc", { stdio: "inherit" });
|
||||
if (valid.status) {
|
||||
throw new Error("failed to compile");
|
||||
}
|
||||
// Start the subprocess
|
||||
child = spawn("npm", ["run", "--silent", "dev"], {
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
env: process.env,
|
||||
beforeAll(async () => {
|
||||
let valid = spawnSync("tsc", { stdio: "inherit" });
|
||||
if (valid.status) {
|
||||
throw new Error("failed to compile");
|
||||
}
|
||||
// Start the subprocess
|
||||
child = spawn("npm", ["run", "--silent", "dev"], {
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
env: process.env,
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
console.error("Error starting subprocess:", error);
|
||||
});
|
||||
child.on("exit", (exit) => {
|
||||
console.error("Exited with", exit);
|
||||
});
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
console.error("Error starting subprocess:", error);
|
||||
|
||||
afterAll(() => {
|
||||
child.kill();
|
||||
});
|
||||
child.on("exit", (exit) => {
|
||||
console.error("Exited with", exit);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
child.kill();
|
||||
});
|
||||
class TestClient implements Client {
|
||||
agent: Agent;
|
||||
|
||||
class TestClient implements Client {
|
||||
agent: Agent;
|
||||
files: Map<string, string> = new Map();
|
||||
receivedText: string = "";
|
||||
|
||||
constructor(agent: Agent) {
|
||||
this.agent = agent;
|
||||
}
|
||||
|
||||
takeReceivedText() {
|
||||
const text = this.receivedText;
|
||||
this.receivedText = "";
|
||||
return text;
|
||||
}
|
||||
|
||||
async requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
||||
const optionId = params.options.find((p) => p.kind === "allow_once")!.optionId;
|
||||
|
||||
return { outcome: { outcome: "selected", optionId } };
|
||||
}
|
||||
|
||||
async sessionUpdate(params: SessionNotification): Promise<void> {
|
||||
console.error("RECEIVED", JSON.stringify(params, null, 4));
|
||||
|
||||
if (
|
||||
params.update.sessionUpdate === "agent_message_chunk" &&
|
||||
params.update.content.type === "text"
|
||||
) {
|
||||
this.receivedText += params.update.content.text;
|
||||
constructor(agent: Agent) {
|
||||
this.agent = agent;
|
||||
}
|
||||
requestPermission(
|
||||
params: RequestPermissionRequest,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
async sessionUpdate(params: SessionNotification): Promise<void> {
|
||||
console.error("RECEVIED", params);
|
||||
}
|
||||
writeTextFile(
|
||||
params: WriteTextFileRequest,
|
||||
): Promise<WriteTextFileResponse> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
async readTextFile(
|
||||
params: ReadTextFileRequest,
|
||||
): Promise<ReadTextFileResponse> {
|
||||
return {
|
||||
content:
|
||||
"This project attempts to take over the world by solving the Goldbach conjecture.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
||||
this.files.set(params.path, params.content);
|
||||
return null;
|
||||
}
|
||||
|
||||
async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
||||
const content = this.files.get(params.path) ?? "";
|
||||
return {
|
||||
content,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function setupTestSession(cwd: string): Promise<{
|
||||
client: TestClient;
|
||||
connection: ClientSideConnection;
|
||||
newSessionResponse: NewSessionResponse;
|
||||
}> {
|
||||
let client;
|
||||
const connection = new ClientSideConnection(
|
||||
(agent) => {
|
||||
client = new TestClient(agent);
|
||||
return client;
|
||||
},
|
||||
nodeToWebWritable(child.stdin!),
|
||||
nodeToWebReadable(child.stdout!),
|
||||
);
|
||||
|
||||
await connection.initialize({
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: {
|
||||
fs: {
|
||||
readTextFile: true,
|
||||
writeTextFile: true,
|
||||
it("should connect to the ACP subprocess", async () => {
|
||||
const connection = new ClientSideConnection(
|
||||
(agent) => {
|
||||
return new TestClient(agent);
|
||||
},
|
||||
},
|
||||
});
|
||||
nodeToWebWritable(child.stdin!),
|
||||
nodeToWebReadable(child.stdout!),
|
||||
);
|
||||
|
||||
const newSessionResponse = await connection.newSession({
|
||||
cwd,
|
||||
mcpServers: [],
|
||||
});
|
||||
|
||||
return { client: client!, connection, newSessionResponse };
|
||||
}
|
||||
|
||||
it("should connect to the ACP subprocess", async () => {
|
||||
const { client, connection, newSessionResponse } = await setupTestSession("./");
|
||||
|
||||
await connection.prompt({
|
||||
prompt: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Hello",
|
||||
},
|
||||
],
|
||||
sessionId: newSessionResponse.sessionId,
|
||||
});
|
||||
|
||||
expect(client.takeReceivedText()).not.toEqual("");
|
||||
}, 30000);
|
||||
|
||||
it("should include available commands", async () => {
|
||||
const { client, connection, newSessionResponse } = await setupTestSession(__dirname);
|
||||
|
||||
expect(newSessionResponse.availableCommands).toContainEqual({
|
||||
name: "quick-math",
|
||||
description: "10 * 3 = 30 (project)",
|
||||
input: null,
|
||||
});
|
||||
expect(newSessionResponse.availableCommands).toContainEqual({
|
||||
name: "say-hello",
|
||||
description: "Say hello (project)",
|
||||
input: { hint: "[name]" },
|
||||
});
|
||||
|
||||
await connection.prompt({
|
||||
prompt: [
|
||||
{
|
||||
type: "text",
|
||||
text: "/quick-math",
|
||||
},
|
||||
],
|
||||
sessionId: newSessionResponse.sessionId,
|
||||
});
|
||||
|
||||
expect(client.takeReceivedText()).toContain("30");
|
||||
|
||||
await connection.prompt({
|
||||
prompt: [
|
||||
{
|
||||
type: "text",
|
||||
text: "/say-hello GPT-5",
|
||||
},
|
||||
],
|
||||
sessionId: newSessionResponse.sessionId,
|
||||
});
|
||||
|
||||
expect(client.takeReceivedText()).toContain("Hello GPT-5");
|
||||
}, 30000);
|
||||
});
|
||||
let session = await connection.newSession({ cwd: "./", mcpServers: [] });
|
||||
await connection.prompt({
|
||||
prompt: [
|
||||
{
|
||||
type: "text",
|
||||
text: "What does this project do? Read the README.md file.",
|
||||
},
|
||||
],
|
||||
sessionId: session.sessionId,
|
||||
});
|
||||
console.log("DONE AWAITING");
|
||||
}, 20000);
|
||||
},
|
||||
);
|
||||
|
||||
describe("tool conversions", () => {
|
||||
it("should handle Bash nicely", () => {
|
||||
@@ -189,7 +109,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "execute",
|
||||
title: "`rm README.md.rm`",
|
||||
content: [
|
||||
@@ -214,9 +134,9 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "search",
|
||||
title: "Find `*/**.ts`",
|
||||
title: "Find */**.ts",
|
||||
content: [],
|
||||
locations: [],
|
||||
});
|
||||
@@ -235,7 +155,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "think",
|
||||
title: "Handle user's work request",
|
||||
content: [
|
||||
@@ -256,15 +176,15 @@ describe("tool conversions", () => {
|
||||
id: "toolu_01EEqsX7Eb9hpx87KAHVPTey",
|
||||
name: "LS",
|
||||
input: {
|
||||
path: "/Users/test/github/claude-code-acp",
|
||||
path: "/Users/benbrandt/github/claude-code-acp",
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "search",
|
||||
title: "List the `/Users/test/github/claude-code-acp` directory's contents",
|
||||
title: "/Users/benbrandt/github/claude-code-acp",
|
||||
content: [],
|
||||
locations: [],
|
||||
locations: [{ path: "/Users/benbrandt/github/claude-code-acp" }],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -278,7 +198,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "search",
|
||||
title: 'grep ".*"',
|
||||
content: [],
|
||||
@@ -291,12 +211,12 @@ describe("tool conversions", () => {
|
||||
id: "toolu_01ABC123XYZ789",
|
||||
name: "Write",
|
||||
input: {
|
||||
file_path: "/Users/test/project/example.txt",
|
||||
abs_path: "/Users/test/project/example.txt",
|
||||
content: "Hello, World!\nThis is test content.",
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "edit",
|
||||
title: "Write /Users/test/project/example.txt",
|
||||
content: [
|
||||
@@ -322,7 +242,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "edit",
|
||||
title: "Write /Users/test/project/config.json",
|
||||
content: [
|
||||
@@ -343,14 +263,19 @@ describe("tool conversions", () => {
|
||||
id: "toolu_01MNO456PQR789",
|
||||
name: "Read",
|
||||
input: {
|
||||
file_path: "/Users/test/project/readme.md",
|
||||
abs_path: "/Users/test/project/readme.md",
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read File",
|
||||
content: [],
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: "/Users/test/project/readme.md" },
|
||||
},
|
||||
],
|
||||
locations: [{ path: "/Users/test/project/readme.md", line: 0 }],
|
||||
});
|
||||
});
|
||||
@@ -365,7 +290,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/data.json",
|
||||
content: [],
|
||||
@@ -384,7 +309,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/large.txt (1 - 100)",
|
||||
content: [],
|
||||
@@ -404,7 +329,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/large.txt (51 - 150)",
|
||||
content: [],
|
||||
@@ -423,7 +348,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "read",
|
||||
title: "Read /Users/test/project/large.txt (from line 201)",
|
||||
content: [],
|
||||
@@ -443,7 +368,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "fetch",
|
||||
title: "Fetch https://agentclientprotocol.com",
|
||||
content: [
|
||||
@@ -468,7 +393,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "fetch",
|
||||
title: '"agentclientprotocol.com"',
|
||||
content: [],
|
||||
@@ -476,7 +401,7 @@ describe("tool conversions", () => {
|
||||
});
|
||||
|
||||
it("should handle KillBash entries", () => {
|
||||
const tool_use = {
|
||||
let tool_use = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01PhLms5fuvmdjy2bb6dfUKT",
|
||||
name: "KillBash",
|
||||
@@ -485,7 +410,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "execute",
|
||||
title: `Kill Process`,
|
||||
content: [],
|
||||
@@ -493,7 +418,7 @@ describe("tool conversions", () => {
|
||||
});
|
||||
|
||||
it("should handle BashOutput entries", () => {
|
||||
const tool_use = {
|
||||
let tool_use = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01SJUWPtj1QspgANgtpqGPuN",
|
||||
name: "BashOutput",
|
||||
@@ -502,7 +427,7 @@ describe("tool conversions", () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
|
||||
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
||||
kind: "execute",
|
||||
title: `Tail Logs`,
|
||||
content: [],
|
||||
@@ -550,7 +475,8 @@ describe("tool conversions", () => {
|
||||
activeForm: "Adding concurrent access tests",
|
||||
},
|
||||
{
|
||||
content: "Add tests for Each function with various data types",
|
||||
content:
|
||||
"Add tests for Each function with various data types",
|
||||
status: "pending",
|
||||
activeForm: "Adding Each function tests",
|
||||
},
|
||||
@@ -586,7 +512,7 @@ describe("tool conversions", () => {
|
||||
session_id: "d056596f-e328-41e9-badd-b07122ae5227",
|
||||
uuid: "b7c3330c-de8f-4bba-ac53-68c7f76ffeb5",
|
||||
};
|
||||
expect(toAcpNotifications(received, "test", {}, {})).toStrictEqual([
|
||||
expect(toAcpNotifications(received, "test", {})).toStrictEqual([
|
||||
{
|
||||
sessionId: "test",
|
||||
update: {
|
||||
@@ -638,80 +564,140 @@ describe("tool conversions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should show full diff for multi edit tool", () => {
|
||||
it("should extract locations from edit tool results", () => {
|
||||
const toolUse = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01ABC123",
|
||||
name: "edit",
|
||||
input: {
|
||||
abs_path: "/Users/test/project/example.txt",
|
||||
old_text: "old content",
|
||||
new_text: "new content",
|
||||
},
|
||||
};
|
||||
|
||||
const toolResult = {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ lineNumbers: [5, 10, 15] }),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const update = toolUpdateFromToolResult(toolResult, toolUse);
|
||||
|
||||
expect(update.locations).toEqual([
|
||||
{ path: "/Users/test/project/example.txt", line: 5 },
|
||||
{ path: "/Users/test/project/example.txt", line: 10 },
|
||||
{ path: "/Users/test/project/example.txt", line: 15 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should extract locations from multi-edit tool results", () => {
|
||||
const toolUse = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01DEF456",
|
||||
name: "mcp__acp__multi-edit",
|
||||
name: "multi-edit",
|
||||
input: {
|
||||
file_path: "/Users/test/project/config.json",
|
||||
edits: [
|
||||
{
|
||||
old_string: 'version": 1',
|
||||
new_string: 'version": 2',
|
||||
old_string: "version: 1.0",
|
||||
new_string: "version: 2.0",
|
||||
replace_all: false,
|
||||
},
|
||||
{
|
||||
old_string: 'enabled": false',
|
||||
new_string: 'enabled": true',
|
||||
old_string: "enabled: false",
|
||||
new_string: "enabled: true",
|
||||
replace_all: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const fileCache: { [key: string]: string } = {
|
||||
"/Users/test/project/config.json": JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
filler: "filler",
|
||||
enabled: false,
|
||||
},
|
||||
null,
|
||||
4,
|
||||
),
|
||||
};
|
||||
|
||||
expect(toolInfoFromToolUse(toolUse, fileCache)).toEqual({
|
||||
const toolResult = {
|
||||
content: [
|
||||
{
|
||||
newText: `{
|
||||
"version": 2,
|
||||
"filler": "filler",
|
||||
"enabled": true
|
||||
}`,
|
||||
oldText: `{
|
||||
"version": 1,
|
||||
"filler": "filler",
|
||||
"enabled": false
|
||||
}`,
|
||||
path: "/Users/test/project/config.json",
|
||||
type: "diff",
|
||||
type: "text",
|
||||
text: JSON.stringify({ lineNumbers: [2, 8, 12, 20] }),
|
||||
},
|
||||
],
|
||||
kind: "edit",
|
||||
locations: [
|
||||
{
|
||||
path: "/Users/test/project/config.json",
|
||||
line: 1,
|
||||
},
|
||||
{
|
||||
path: "/Users/test/project/config.json",
|
||||
line: 3,
|
||||
},
|
||||
],
|
||||
title: "Edit /Users/test/project/config.json",
|
||||
});
|
||||
};
|
||||
|
||||
const update = toolUpdateFromToolResult(toolResult, toolUse);
|
||||
|
||||
expect(update.locations).toEqual([
|
||||
{ path: "/Users/test/project/config.json", line: 2 },
|
||||
{ path: "/Users/test/project/config.json", line: 8 },
|
||||
{ path: "/Users/test/project/config.json", line: 12 },
|
||||
{ path: "/Users/test/project/config.json", line: 20 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return empty update for successful edit result", () => {
|
||||
it("should handle mcp__acp__edit tool results", () => {
|
||||
const toolUse = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01GHI789",
|
||||
name: "mcp__acp__edit",
|
||||
input: {
|
||||
abs_path: "/Users/test/project/main.py",
|
||||
old_text: "def hello():",
|
||||
new_text: "def hello_world():",
|
||||
},
|
||||
};
|
||||
|
||||
const toolResult = {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ lineNumbers: [42] }),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const update = toolUpdateFromToolResult(toolResult, toolUse);
|
||||
|
||||
expect(update.locations).toEqual([
|
||||
{ path: "/Users/test/project/main.py", line: 42 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle empty locations from edit tools", () => {
|
||||
const toolUse = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01JKL012",
|
||||
name: "edit",
|
||||
input: {
|
||||
abs_path: "/Users/test/project/notfound.txt",
|
||||
old_text: "nonexistent",
|
||||
new_text: "replacement",
|
||||
},
|
||||
};
|
||||
|
||||
const toolResult = {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({ lineNumbers: [] }),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const update = toolUpdateFromToolResult(toolResult, toolUse);
|
||||
|
||||
expect(update.locations).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle malformed tool results gracefully", () => {
|
||||
const toolUse = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01MNO345",
|
||||
name: "mcp__acp__edit",
|
||||
name: "edit",
|
||||
input: {
|
||||
abs_path: "/Users/test/project/test.txt",
|
||||
old_string: "old",
|
||||
new_string: "new",
|
||||
old_text: "old",
|
||||
new_text: "new",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -722,9 +708,6 @@ describe("tool conversions", () => {
|
||||
text: "not valid json",
|
||||
},
|
||||
],
|
||||
tool_use_id: "test",
|
||||
is_error: false,
|
||||
type: "tool_result" as const,
|
||||
};
|
||||
|
||||
const update = toolUpdateFromToolResult(toolResult, toolUse);
|
||||
@@ -732,50 +715,4 @@ describe("tool conversions", () => {
|
||||
// Should return empty object when parsing fails
|
||||
expect(update).toEqual({});
|
||||
});
|
||||
|
||||
it("should return content update for edit failure", () => {
|
||||
const toolUse = {
|
||||
type: "tool_use",
|
||||
id: "toolu_01MNO345",
|
||||
name: "mcp__acp__edit",
|
||||
input: {
|
||||
abs_path: "/Users/test/project/test.txt",
|
||||
old_string: "old",
|
||||
new_string: "new",
|
||||
},
|
||||
};
|
||||
|
||||
const toolResult = {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Failed to find `old_string`",
|
||||
},
|
||||
],
|
||||
tool_use_id: "test",
|
||||
is_error: true,
|
||||
type: "tool_result" as const,
|
||||
};
|
||||
|
||||
const update = toolUpdateFromToolResult(toolResult, toolUse);
|
||||
|
||||
// Should return empty object when parsing fails
|
||||
expect(update).toEqual({
|
||||
content: [
|
||||
{ content: { type: "text", text: "Failed to find `old_string`" }, type: "content" },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("escape markdown", () => {
|
||||
it("should escape markdown characters", () => {
|
||||
let text = "Hello *world*!";
|
||||
let escaped = markdownEscape(text);
|
||||
expect(escaped).toEqual("```\nHello *world*!\n```");
|
||||
|
||||
text = "for example:\n```markdown\nHello *world*!\n```\n";
|
||||
escaped = markdownEscape(text);
|
||||
expect(escaped).toEqual("````\nfor example:\n```markdown\nHello *world*!\n```\n````");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,13 @@ import { replaceAndCalculateLocation } from "../mcp-server.js";
|
||||
describe("replaceAndCalculateLocation", () => {
|
||||
it("should replace first occurrence and return correct line number", () => {
|
||||
const content = "line 1\nline 2 with text\nline 3 with text\nline 4";
|
||||
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
||||
const result = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "text", newText: "replaced" },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("line 1\nline 2 with replaced\nline 3 with text\nline 4");
|
||||
expect(result.newContent).toBe(
|
||||
"line 1\nline 2 with replaced\nline 3 with text\nline 4",
|
||||
);
|
||||
expect(result.lineNumbers).toEqual([1]); // Line 2 (0-based indexing)
|
||||
});
|
||||
|
||||
@@ -16,15 +20,20 @@ describe("replaceAndCalculateLocation", () => {
|
||||
{ oldText: "text", newText: "replaced", replaceAll: true },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("line 1\nline 2 with replaced\nline 3 with replaced\nline 4");
|
||||
expect(result.newContent).toBe(
|
||||
"line 1\nline 2 with replaced\nline 3 with replaced\nline 4",
|
||||
);
|
||||
expect(result.lineNumbers).toEqual([1, 2]); // Lines 2 and 3 (0-based)
|
||||
});
|
||||
|
||||
it("should return empty line numbers array when no match found", () => {
|
||||
const content = "line 1\nline 2\nline 3";
|
||||
expect(() => {
|
||||
replaceAndCalculateLocation(content, [{ oldText: "notfound", newText: "replaced" }]);
|
||||
}).toThrow('The provided `old_string` does not appear in the file: "notfound"');
|
||||
const result = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "notfound", newText: "replaced" },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe(content);
|
||||
expect(result.lineNumbers).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle Windows line endings (CRLF)", () => {
|
||||
@@ -45,7 +54,9 @@ describe("replaceAndCalculateLocation", () => {
|
||||
{ oldText: "text", newText: "replaced", replaceAll: true },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("line 1\rline 2 with replaced\rline 3 with replaced\rline 4");
|
||||
expect(result.newContent).toBe(
|
||||
"line 1\rline 2 with replaced\rline 3 with replaced\rline 4",
|
||||
);
|
||||
expect(result.lineNumbers).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
@@ -55,13 +66,17 @@ describe("replaceAndCalculateLocation", () => {
|
||||
{ oldText: "text", newText: "replaced", replaceAll: true },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("line 1\nline 2 with replaced\r\nline 3 with replaced\rline 4");
|
||||
expect(result.newContent).toBe(
|
||||
"line 1\nline 2 with replaced\r\nline 3 with replaced\rline 4",
|
||||
);
|
||||
expect(result.lineNumbers).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("should handle text at the beginning of file", () => {
|
||||
const content = "text at start\nline 2\nline 3";
|
||||
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
||||
const result = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "text", newText: "replaced" },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("replaced at start\nline 2\nline 3");
|
||||
expect(result.lineNumbers).toEqual([0]); // First line (0-based)
|
||||
@@ -69,7 +84,9 @@ describe("replaceAndCalculateLocation", () => {
|
||||
|
||||
it("should handle text at the end of file", () => {
|
||||
const content = "line 1\nline 2\nlast line with text";
|
||||
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
||||
const result = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "text", newText: "replaced" },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("line 1\nline 2\nlast line with replaced");
|
||||
expect(result.lineNumbers).toEqual([2]); // Last line (0-based)
|
||||
@@ -81,41 +98,53 @@ describe("replaceAndCalculateLocation", () => {
|
||||
{ oldText: "text", newText: "replaced", replaceAll: true },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("line 1\nreplaced replaced replaced\nline 3");
|
||||
expect(result.newContent).toBe(
|
||||
"line 1\nreplaced replaced replaced\nline 3",
|
||||
);
|
||||
expect(result.lineNumbers).toEqual([1]); // All on line 2 (0-based), deduplicated
|
||||
});
|
||||
|
||||
it("should handle empty file content", () => {
|
||||
expect(() => {
|
||||
replaceAndCalculateLocation("", [{ oldText: "text", newText: "replaced" }]);
|
||||
}).toThrowError(
|
||||
'The provided `old_string` does not appear in the file: "text".\n\nNo edits were applied.',
|
||||
);
|
||||
const content = "";
|
||||
const result = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "text", newText: "replaced" },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("");
|
||||
expect(result.lineNumbers).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle empty search text", () => {
|
||||
// Test with replaceAll false (default)
|
||||
const content1 = "line 1\nline 2\nline 3";
|
||||
expect(() => {
|
||||
replaceAndCalculateLocation(content1, [{ oldText: "", newText: "replaced" }]);
|
||||
}).toThrowError("The provided `old_string` is empty.\n\nNo edits were applied.");
|
||||
const result1 = replaceAndCalculateLocation(content1, [
|
||||
{ oldText: "", newText: "replaced" },
|
||||
]);
|
||||
expect(result1.newContent).toBe(content1);
|
||||
expect(result1.lineNumbers).toEqual([]);
|
||||
|
||||
// Test with replaceAll true on single line
|
||||
const content2 = "abc";
|
||||
expect(() => {
|
||||
replaceAndCalculateLocation(content2, [{ oldText: "", newText: "X", replaceAll: true }]);
|
||||
}).toThrowError("The provided `old_string` is empty.\n\nNo edits were applied.");
|
||||
const result2 = replaceAndCalculateLocation(content2, [
|
||||
{ oldText: "", newText: "X", replaceAll: true },
|
||||
]);
|
||||
expect(result2.newContent).toBe(content2);
|
||||
expect(result2.lineNumbers).toEqual([]);
|
||||
|
||||
// Test with replaceAll true on multiline content
|
||||
const content3 = "ab\ncd";
|
||||
expect(() => {
|
||||
replaceAndCalculateLocation(content3, [{ oldText: "", newText: "X", replaceAll: true }]);
|
||||
}).toThrowError("The provided `old_string` is empty.\n\nNo edits were applied.");
|
||||
const result3 = replaceAndCalculateLocation(content3, [
|
||||
{ oldText: "", newText: "X", replaceAll: true },
|
||||
]);
|
||||
expect(result3.newContent).toBe(content3);
|
||||
expect(result3.lineNumbers).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle single line content", () => {
|
||||
const content = "single line with text here";
|
||||
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
||||
const result = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "text", newText: "replaced" },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("single line with replaced here");
|
||||
expect(result.lineNumbers).toEqual([0]); // Line 1 (0-based)
|
||||
@@ -154,7 +183,9 @@ describe("replaceAndCalculateLocation", () => {
|
||||
it("should handle very long lines", () => {
|
||||
const longLine = "a".repeat(10000) + "text" + "b".repeat(10000);
|
||||
const content = `line 1\n${longLine}\nline 3`;
|
||||
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
||||
const result = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "text", newText: "replaced" },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe(
|
||||
`line 1\n${"a".repeat(10000)}replaced${"b".repeat(10000)}\nline 3`,
|
||||
@@ -167,7 +198,9 @@ describe("replaceAndCalculateLocation", () => {
|
||||
lines[500] = "line with text";
|
||||
const content = lines.join("\n");
|
||||
|
||||
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
||||
const result = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "text", newText: "replaced" },
|
||||
]);
|
||||
|
||||
expect(result.lineNumbers).toEqual([500]);
|
||||
expect(result.newContent).includes("line with replaced");
|
||||
@@ -191,7 +224,9 @@ describe("replaceAndCalculateLocation", () => {
|
||||
const result1 = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "short", newText: "very very long text" },
|
||||
]);
|
||||
expect(result1.newContent).toBe("very very long text\nmedium line\nlong line here");
|
||||
expect(result1.newContent).toBe(
|
||||
"very very long text\nmedium line\nlong line here",
|
||||
);
|
||||
expect(result1.lineNumbers).toEqual([0]);
|
||||
|
||||
// Replacing with shorter text
|
||||
@@ -225,7 +260,9 @@ describe("replaceAndCalculateLocation", () => {
|
||||
|
||||
it("should handle replacement at exact end of line", () => {
|
||||
const content = "line ends with text\nnext line";
|
||||
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
|
||||
const result = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "text", newText: "replaced" },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("line ends with replaced\nnext line");
|
||||
expect(result.lineNumbers).toEqual([0]);
|
||||
@@ -265,7 +302,9 @@ describe("replaceAndCalculateLocation", () => {
|
||||
// - All "foo" occurrences (lines 1, 3, 5 in original)
|
||||
// - First "bar" occurrence (line 1 after first edit)
|
||||
// - "line 3" (line 2 after previous edits)
|
||||
expect(result.newContent).toBe("line 1\nFOO BAR\nLINE THREE\nFOO baz\nline 5\nbar FOO\nline 7");
|
||||
expect(result.newContent).toBe(
|
||||
"line 1\nFOO BAR\nLINE THREE\nFOO baz\nline 5\nbar FOO\nline 7",
|
||||
);
|
||||
|
||||
// Line numbers reflect position in final content, deduplicated and sorted
|
||||
expect(result.lineNumbers).toEqual([
|
||||
@@ -287,7 +326,9 @@ describe("replaceAndCalculateLocation", () => {
|
||||
// First edit replaces first "hello" only
|
||||
// Second edit replaces all "world" in the modified content
|
||||
// Third edit replaces remaining "hello"s in the modified content
|
||||
expect(result.newContent).toBe("hi earth\ngreetings greetings\nearth greetings earth");
|
||||
expect(result.newContent).toBe(
|
||||
"hi earth\ngreetings greetings\nearth greetings earth",
|
||||
);
|
||||
|
||||
// Line numbers reflect position in final content, deduplicated and sorted
|
||||
expect(result.lineNumbers).toEqual([
|
||||
@@ -299,14 +340,17 @@ describe("replaceAndCalculateLocation", () => {
|
||||
|
||||
it("should handle complex scenarios with marker-like text", () => {
|
||||
// Test that our marker approach doesn't get confused by similar text
|
||||
const content = "line 1\n__MARKER__text\nline 3\n__REPLACE_MARKER_abc__\nline 5";
|
||||
const content =
|
||||
"line 1\n__MARKER__text\nline 3\n__REPLACE_MARKER_abc__\nline 5";
|
||||
const result = replaceAndCalculateLocation(content, [
|
||||
{ oldText: "__MARKER__", newText: "REPLACED", replaceAll: true },
|
||||
{ oldText: "text", newText: "content" },
|
||||
{ oldText: "__REPLACE_MARKER_abc__", newText: "DONE" },
|
||||
]);
|
||||
|
||||
expect(result.newContent).toBe("line 1\nREPLACEDcontent\nline 3\nDONE\nline 5");
|
||||
expect(result.newContent).toBe(
|
||||
"line 1\nREPLACEDcontent\nline 3\nDONE\nline 5",
|
||||
);
|
||||
|
||||
// Line numbers should be correct even with marker-like text
|
||||
expect(result.lineNumbers).toEqual([
|
||||
|
||||
335
src/tools.ts
335
src/tools.ts
@@ -1,10 +1,10 @@
|
||||
import { ContentBlock } from "@modelcontextprotocol/sdk/types.js";
|
||||
import {
|
||||
PlanEntry,
|
||||
ToolCallContent,
|
||||
ToolCallLocation,
|
||||
ToolKind,
|
||||
} from "@zed-industries/agent-client-protocol";
|
||||
import { replaceAndCalculateLocation, SYSTEM_REMINDER } from "./mcp-server.js";
|
||||
|
||||
interface ToolInfo {
|
||||
title: string;
|
||||
@@ -19,10 +19,7 @@ interface ToolUpdate {
|
||||
locations?: ToolCallLocation[];
|
||||
}
|
||||
|
||||
export function toolInfoFromToolUse(
|
||||
toolUse: any,
|
||||
cachedFileContent: { [key: string]: string },
|
||||
): ToolInfo {
|
||||
export function toolInfoFromToolUse(toolUse: any): ToolInfo {
|
||||
const name = toolUse.name;
|
||||
const input = toolUse.input;
|
||||
|
||||
@@ -44,15 +41,30 @@ export function toolInfoFromToolUse(
|
||||
|
||||
case "NotebookRead":
|
||||
return {
|
||||
title: input?.notebook_path ? `Read Notebook ${input.notebook_path}` : "Read Notebook",
|
||||
title: input?.notebook_path
|
||||
? `Read Notebook ${input.notebook_path}`
|
||||
: "Read Notebook",
|
||||
kind: "read",
|
||||
content: [],
|
||||
content:
|
||||
input && input.notebook_path
|
||||
? [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: input.notebook_path.toString(),
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
locations: input?.notebook_path ? [{ path: input.notebook_path }] : [],
|
||||
};
|
||||
|
||||
case "NotebookEdit":
|
||||
return {
|
||||
title: input?.notebook_path ? `Edit Notebook ${input.notebook_path}` : "Edit Notebook",
|
||||
title: input?.notebook_path
|
||||
? `Edit Notebook ${input.notebook_path}`
|
||||
: "Edit Notebook",
|
||||
kind: "edit",
|
||||
content:
|
||||
input && input.new_source
|
||||
@@ -67,12 +79,13 @@ export function toolInfoFromToolUse(
|
||||
};
|
||||
|
||||
case "Bash":
|
||||
case "mcp__acp__Bash":
|
||||
return {
|
||||
title: input?.command ? "`" + input.command.replaceAll("`", "\\`") + "`" : "Terminal",
|
||||
title: input?.command
|
||||
? "`" + input.command.replaceAll("`", "\\`") + "`"
|
||||
: "Terminal",
|
||||
kind: "execute",
|
||||
content:
|
||||
input && input.description
|
||||
input && input.command
|
||||
? [
|
||||
{
|
||||
type: "content",
|
||||
@@ -83,7 +96,6 @@ export function toolInfoFromToolUse(
|
||||
};
|
||||
|
||||
case "BashOutput":
|
||||
case "mcp__acp__BashOutput":
|
||||
return {
|
||||
title: "Tail Logs",
|
||||
kind: "execute",
|
||||
@@ -91,7 +103,6 @@ export function toolInfoFromToolUse(
|
||||
};
|
||||
|
||||
case "KillBash":
|
||||
case "mcp__acp__KillBash":
|
||||
return {
|
||||
title: "Kill Process",
|
||||
kind: "execute",
|
||||
@@ -102,7 +113,11 @@ export function toolInfoFromToolUse(
|
||||
let limit = "";
|
||||
if (input.limit) {
|
||||
limit =
|
||||
" (" + ((input.offset ?? 0) + 1) + " - " + ((input.offset ?? 0) + input.limit) + ")";
|
||||
" (" +
|
||||
((input.offset ?? 0) + 1) +
|
||||
" - " +
|
||||
((input.offset ?? 0) + input.limit) +
|
||||
")";
|
||||
} else if (input.offset) {
|
||||
limit = " (from line " + (input.offset + 1) + ")";
|
||||
}
|
||||
@@ -123,118 +138,65 @@ export function toolInfoFromToolUse(
|
||||
return {
|
||||
title: "Read File",
|
||||
kind: "read",
|
||||
content: [],
|
||||
locations: [{ path: input.file_path, line: input.offset ?? 0 }],
|
||||
content:
|
||||
input && input.abs_path
|
||||
? [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: input.abs_path.toString() },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
locations: [{ path: input.abs_path, line: input.offset ?? 0 }],
|
||||
};
|
||||
|
||||
case "LS":
|
||||
return {
|
||||
title: `List the ${input?.path ? "`" + input.path + "`" : "current"} directory's contents`,
|
||||
title: input?.path ? input.path : "Current Directory",
|
||||
kind: "search",
|
||||
content: [],
|
||||
locations: [],
|
||||
locations: input?.path ? [{ path: input.path }] : [],
|
||||
};
|
||||
|
||||
case "mcp__acp__edit":
|
||||
case "Edit": {
|
||||
const path = input?.abs_path ?? input?.file_path;
|
||||
let oldText = input.old_string ?? null;
|
||||
let newText = input.new_string ?? "";
|
||||
let affectedLines: number[] = [];
|
||||
|
||||
if (path && oldText) {
|
||||
try {
|
||||
const oldContent = cachedFileContent[path] || "";
|
||||
const newContent = replaceAndCalculateLocation(oldContent, [
|
||||
{
|
||||
oldText,
|
||||
newText,
|
||||
replaceAll: false,
|
||||
},
|
||||
]);
|
||||
oldText = oldContent;
|
||||
newText = newContent.newContent;
|
||||
affectedLines = newContent.lineNumbers;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
case "Edit":
|
||||
return {
|
||||
title: path ? `Edit \`${path}\`` : "Edit",
|
||||
title: input?.abs_path ? `Edit ${input.abs_path}` : "Edit",
|
||||
kind: "edit",
|
||||
content:
|
||||
input && path
|
||||
input && input.abs_path
|
||||
? [
|
||||
{
|
||||
type: "diff",
|
||||
path,
|
||||
oldText,
|
||||
newText,
|
||||
path: input.abs_path,
|
||||
oldText: input.old_text || null,
|
||||
newText: input.new_text,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
locations: path
|
||||
? affectedLines.length > 0
|
||||
? affectedLines.map((line) => ({ line, path }))
|
||||
: [{ path }]
|
||||
: [],
|
||||
locations: input?.abs_path ? [{ path: input.abs_path }] : [],
|
||||
};
|
||||
}
|
||||
|
||||
case "mcp__acp__multi-edit":
|
||||
case "MultiEdit": {
|
||||
const multiInput = input as {
|
||||
file_path: string;
|
||||
edits: {
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
replace_all?: boolean;
|
||||
}[];
|
||||
};
|
||||
let oldTextMulti = multiInput.edits.map((edit: any) => edit.old_string).join("\n");
|
||||
let newTextMulti = multiInput.edits.map((edit: any) => edit.new_string).join("\n");
|
||||
let affectedLines: number[] = [];
|
||||
try {
|
||||
if (multiInput.edits && multiInput.file_path) {
|
||||
const oldContent =
|
||||
cachedFileContent[multiInput.file_path] ||
|
||||
multiInput.edits.map((edit: any) => edit.oldText).join("\n");
|
||||
const newContent = replaceAndCalculateLocation(
|
||||
oldContent,
|
||||
multiInput.edits.map((edit) => ({
|
||||
oldText: edit.old_string,
|
||||
newText: edit.new_string,
|
||||
replaceAll: edit.replace_all,
|
||||
})),
|
||||
);
|
||||
oldTextMulti = oldContent;
|
||||
newTextMulti = newContent.newContent;
|
||||
affectedLines = newContent.lineNumbers;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
case "MultiEdit":
|
||||
// Display it as a normal edit, because end users don't care about
|
||||
// the distinction between edits and multi-edits.
|
||||
return {
|
||||
title: input?.file_path ? `Edit ${input.file_path}` : "Edit",
|
||||
kind: "edit",
|
||||
content: [
|
||||
{
|
||||
type: "diff" as const,
|
||||
path: input.file_path,
|
||||
oldText: oldTextMulti,
|
||||
newText: newTextMulti,
|
||||
},
|
||||
],
|
||||
locations: input?.file_path
|
||||
? affectedLines.length > 0
|
||||
? affectedLines.map((line) => ({ line, path: input.file_path }))
|
||||
: [{ path: input.file_path }]
|
||||
: [],
|
||||
content:
|
||||
input && input.edits && input.edits.length > 0
|
||||
? input.edits.map((edit: any) => ({
|
||||
type: "diff" as const,
|
||||
path: input.file_path,
|
||||
oldText: edit.old_string || null,
|
||||
newText: edit.new_string,
|
||||
}))
|
||||
: [],
|
||||
locations: input?.file_path ? [{ path: input.file_path }] : [],
|
||||
};
|
||||
}
|
||||
case "mcp__acp__write": {
|
||||
|
||||
case "mcp__acp__write":
|
||||
let content: ToolCallContent[] = [];
|
||||
if (input && input.abs_path) {
|
||||
content = [
|
||||
@@ -259,33 +221,32 @@ export function toolInfoFromToolUse(
|
||||
content,
|
||||
locations: input?.abs_path ? [{ path: input.abs_path }] : [],
|
||||
};
|
||||
}
|
||||
|
||||
case "Write":
|
||||
return {
|
||||
title: input?.file_path ? `Write ${input.file_path}` : "Write",
|
||||
title: input?.abs_path ? `Write ${input.abs_path}` : "Write",
|
||||
kind: "edit",
|
||||
content:
|
||||
input && input.file_path
|
||||
input && input.abs_path
|
||||
? [
|
||||
{
|
||||
type: "diff",
|
||||
path: input.file_path,
|
||||
path: input.abs_path,
|
||||
oldText: null,
|
||||
newText: input.content,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
locations: input?.file_path ? [{ path: input.file_path }] : [],
|
||||
locations: input?.abs_path ? [{ path: input.abs_path }] : [],
|
||||
};
|
||||
|
||||
case "Glob": {
|
||||
let label = "Find";
|
||||
if (input.path) {
|
||||
label += ` \`${input.path}\``;
|
||||
label += ` ${input.path}`;
|
||||
}
|
||||
if (input.pattern) {
|
||||
label += ` \`${input.pattern}\``;
|
||||
label += ` ${input.pattern}`;
|
||||
}
|
||||
return {
|
||||
title: label,
|
||||
@@ -324,7 +285,6 @@ export function toolInfoFromToolUse(
|
||||
label += " -c";
|
||||
break;
|
||||
case "Content":
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -434,67 +394,64 @@ export function toolInfoFromToolUse(
|
||||
}
|
||||
}
|
||||
|
||||
type ToolResult = {
|
||||
type: "tool_result";
|
||||
content: any;
|
||||
tool_use_id: string;
|
||||
is_error: boolean;
|
||||
};
|
||||
|
||||
export function toolUpdateFromToolResult(
|
||||
toolResult: ToolResult,
|
||||
toolResult: any,
|
||||
toolUse: any | undefined,
|
||||
): ToolUpdate {
|
||||
switch (toolUse?.name) {
|
||||
case "Read":
|
||||
case "mcp__acp__read":
|
||||
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
|
||||
return {
|
||||
content: toolResult.content.map((content: any) => ({
|
||||
type: "content",
|
||||
content:
|
||||
content.type === "text"
|
||||
? {
|
||||
type: "text",
|
||||
text: markdownEscape(content.text.replace(SYSTEM_REMINDER, "")),
|
||||
}
|
||||
: content,
|
||||
})),
|
||||
};
|
||||
} else if (typeof toolResult.content === "string" && toolResult.content.length > 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: markdownEscape(toolResult.content.replace(SYSTEM_REMINDER, "")),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
||||
case "mcp__acp__Bash":
|
||||
case "mcp__acp__edit":
|
||||
case "edit":
|
||||
case "Edit":
|
||||
case "mcp__acp__multi-edit":
|
||||
case "multi-edit":
|
||||
case "MultiEdit":
|
||||
case "mcp__acp__write":
|
||||
case "Write": {
|
||||
if (toolResult.is_error && toolResult.content?.length > 0) {
|
||||
// Only return errors
|
||||
return toAcpContentUpdate(toolResult.content);
|
||||
case "Edit": {
|
||||
// Parse the line numbers from the tool result
|
||||
if (toolResult.content && toolResult.content.length > 0) {
|
||||
try {
|
||||
const firstContent = toolResult.content[0];
|
||||
const data = JSON.parse(firstContent.text || firstContent);
|
||||
if (data.lineNumbers && Array.isArray(data.lineNumbers)) {
|
||||
const locations: ToolCallLocation[] = data.lineNumbers.map(
|
||||
(line: number) => ({
|
||||
path: toolUse?.input?.abs_path || toolUse?.input?.file_path,
|
||||
line: line,
|
||||
}),
|
||||
);
|
||||
return { locations };
|
||||
}
|
||||
} catch (e) {
|
||||
// If parsing fails, return empty object
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
case "mcp__acp__multi-edit":
|
||||
case "multi-edit":
|
||||
case "MultiEdit": {
|
||||
// Parse the line numbers from the tool result
|
||||
if (toolResult.content && toolResult.content.length > 0) {
|
||||
try {
|
||||
const firstContent = toolResult.content[0];
|
||||
const data = JSON.parse(firstContent.text || firstContent);
|
||||
if (data.lineNumbers && Array.isArray(data.lineNumbers)) {
|
||||
const locations: ToolCallLocation[] = data.lineNumbers.map(
|
||||
(line: number) => ({
|
||||
path: toolUse?.input?.file_path || toolUse?.input?.abs_path,
|
||||
line: line,
|
||||
}),
|
||||
);
|
||||
return { locations };
|
||||
}
|
||||
} catch (e) {
|
||||
// If parsing fails, return empty object
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
case "Task":
|
||||
case "NotebookEdit":
|
||||
case "NotebookRead":
|
||||
case "mcp__acp__write":
|
||||
case "Write":
|
||||
case "TodoWrite":
|
||||
case "exit_plan_mode":
|
||||
case "Bash":
|
||||
@@ -506,36 +463,42 @@ export function toolUpdateFromToolResult(
|
||||
case "WebFetch":
|
||||
case "WebSearch":
|
||||
case "Other":
|
||||
case "mcp__acp__read":
|
||||
if (Array.isArray(toolResult.content)) {
|
||||
return {
|
||||
content: toolResult.content.map((content: any) => ({
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: `\`\`\`${toolUse.input?.abs_path ?? ""}\n${content.text ?? ""}\n\`\`\``,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
return {};
|
||||
default: {
|
||||
return toAcpContentUpdate(toolResult.content);
|
||||
if (Array.isArray(toolResult.content)) {
|
||||
return {
|
||||
content: toolResult.content.map((content: any) => ({
|
||||
type: "content",
|
||||
content,
|
||||
})),
|
||||
};
|
||||
} else if (typeof toolResult.content === "string") {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: toolResult.content },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toAcpContentUpdate(content: any): { content?: ToolCallContent[] } {
|
||||
if (Array.isArray(content) && content.length > 0) {
|
||||
return {
|
||||
content: content.map((content: any) => ({
|
||||
type: "content",
|
||||
content,
|
||||
})),
|
||||
};
|
||||
} else if (typeof content === "string" && content.length > 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: content,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
type ClaudePlanEntry = {
|
||||
content: string;
|
||||
status: "pending" | "in_progress" | "completed";
|
||||
@@ -549,13 +512,3 @@ export function planEntries(input: { todos: ClaudePlanEntry[] }): PlanEntry[] {
|
||||
priority: "medium",
|
||||
}));
|
||||
}
|
||||
|
||||
export function markdownEscape(text: string): string {
|
||||
let escape = "```";
|
||||
for (const [m] of text.matchAll(/^```+/gm)) {
|
||||
while (m.length >= escape.length) {
|
||||
escape += "`";
|
||||
}
|
||||
}
|
||||
return escape + "\n" + text + (text.endsWith("\n") ? "" : "\n") + escape;
|
||||
}
|
||||
|
||||
19
src/utils.ts
19
src/utils.ts
@@ -45,23 +45,24 @@ export class Pushable<T> implements AsyncIterable<T> {
|
||||
}
|
||||
|
||||
// Helper to convert Node.js streams to Web Streams
|
||||
export function nodeToWebWritable(nodeStream: Writable): WritableStream<Uint8Array> {
|
||||
export function nodeToWebWritable(
|
||||
nodeStream: Writable,
|
||||
): WritableStream<Uint8Array> {
|
||||
return new WritableStream<Uint8Array>({
|
||||
write(chunk) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
nodeStream.write(Buffer.from(chunk), (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Array> {
|
||||
export function nodeToWebReadable(
|
||||
nodeStream: Readable,
|
||||
): ReadableStream<Uint8Array> {
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
nodeStream.on("data", (chunk: Buffer) => {
|
||||
@@ -76,7 +77,3 @@ export function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Arr
|
||||
export function unreachable(value: never): never {
|
||||
throw new Error(`Unexpected case: ${value}`);
|
||||
}
|
||||
|
||||
export function sleep(time: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, time));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user