amp-acp 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -9
- package/img/screenshot.png +0 -0
- package/package.json +5 -3
- package/src/server.js +84 -115
package/README.md
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
# ACP adapter for AmpCode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Use [Amp](https://ampcode.com) from [ACP](https://agentclientprotocol.com/)-compatible clients such as [Zed](https://zed.dev) or [Toad](https://github.com/batrachianai/toad).
|
|
4
6
|
|
|
5
7
|
## Prerequisites
|
|
6
8
|
|
|
7
|
-
-
|
|
8
|
-
- Node.js (for running the adapter)
|
|
9
|
+
- Node.js 18+ (the adapter will be installed automatically via `npx`)
|
|
9
10
|
|
|
10
11
|
## Installation
|
|
11
12
|
|
|
12
13
|
Add to your Zed `settings.json` (open with `cmd+,` or `ctrl+,`):
|
|
13
14
|
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"agent_servers": {
|
|
18
|
+
"Amp": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["-y", "amp-acp"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's it! The SDK handles authentication and Amp integration automatically.
|
|
27
|
+
|
|
28
|
+
## First Use
|
|
29
|
+
|
|
30
|
+
**If you don't have Amp CLI installed**: Add the `AMP_API_KEY` environment variable to your Zed config. You can get your API key from https://ampcode.com/settings
|
|
31
|
+
|
|
14
32
|
```json
|
|
15
33
|
{
|
|
16
34
|
"agent_servers": {
|
|
@@ -18,22 +36,27 @@ Add to your Zed `settings.json` (open with `cmd+,` or `ctrl+,`):
|
|
|
18
36
|
"command": "npx",
|
|
19
37
|
"args": ["-y", "amp-acp"],
|
|
20
38
|
"env": {
|
|
21
|
-
"
|
|
22
|
-
"AMP_PREFER_SYSTEM_PATH": "1"
|
|
39
|
+
"AMP_API_KEY": "your-api-key-here"
|
|
23
40
|
}
|
|
24
41
|
}
|
|
25
42
|
}
|
|
26
43
|
}
|
|
27
44
|
```
|
|
28
45
|
|
|
29
|
-
|
|
46
|
+
**If you [have Amp CLI installed](https://ampcode.com/manual#getting-started-command-line-interface)**: Run `amp login` first to authenticate.
|
|
30
47
|
|
|
31
48
|
## How it Works
|
|
32
49
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
50
|
+
- Uses the official Amp SDK to communicate with AmpCode
|
|
51
|
+
- Streams Amp's responses over the Agent Client Protocol (ACP)
|
|
52
|
+
- Renders Amp messages and interactions natively in Zed
|
|
35
53
|
- Tool permissions are handled by Amp (no additional configuration needed)
|
|
54
|
+
- Supports conversation continuity across multiple prompts
|
|
36
55
|
|
|
37
56
|
## Troubleshooting
|
|
38
57
|
|
|
39
|
-
**
|
|
58
|
+
**Adapter doesn't start**: Make sure you have Node.js 18 or later installed. Run `node --version` to check.
|
|
59
|
+
|
|
60
|
+
**Connection issues**: Restart Zed and try again. The adapter creates a fresh connection each time.
|
|
61
|
+
|
|
62
|
+
**Tool execution problems**: Check Zed's output panel for detailed error messages from the Amp SDK.
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amp-acp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"amp-acp": "src/index.js"
|
|
8
8
|
},
|
|
9
|
-
"description": "ACP adapter that bridges Amp
|
|
9
|
+
"description": "ACP adapter that bridges Amp Code to Agent Client Protocol (Zed external agent)",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"start": "node src/index.js",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"test": "node -e 'console.log(\"OK\")'"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@agentclientprotocol/sdk": "0.4.8"
|
|
17
|
+
"@agentclientprotocol/sdk": "0.4.8",
|
|
18
|
+
"@sourcegraph/amp": "^0.0.1765685598-g5365e6",
|
|
19
|
+
"@sourcegraph/amp-sdk": "^0.1.0-20251214200908-g3251f72"
|
|
18
20
|
}
|
|
19
21
|
}
|
package/src/server.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { RequestError } from '@agentclientprotocol/sdk';
|
|
2
|
-
import {
|
|
3
|
-
import readline from 'node:readline';
|
|
2
|
+
import { execute } from '@sourcegraph/amp-sdk';
|
|
4
3
|
import { toAcpNotifications } from './to-acp.js';
|
|
5
4
|
|
|
6
5
|
export class AmpAcpAgent {
|
|
@@ -15,6 +14,7 @@ export class AmpAcpAgent {
|
|
|
15
14
|
protocolVersion: 1,
|
|
16
15
|
agentCapabilities: {
|
|
17
16
|
promptCapabilities: { image: true, embeddedContext: true },
|
|
17
|
+
mcpCapabilities: { http: true, sse: true },
|
|
18
18
|
},
|
|
19
19
|
authMethods: [],
|
|
20
20
|
};
|
|
@@ -23,24 +23,40 @@ export class AmpAcpAgent {
|
|
|
23
23
|
async newSession(params) {
|
|
24
24
|
const sessionId = `S-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
25
25
|
|
|
26
|
+
// Convert ACP mcpServers to Amp SDK mcpConfig format
|
|
27
|
+
const mcpConfig = {};
|
|
28
|
+
if (Array.isArray(params.mcpServers)) {
|
|
29
|
+
for (const server of params.mcpServers) {
|
|
30
|
+
if ('type' in server && (server.type === 'http' || server.type === 'sse')) {
|
|
31
|
+
// HTTP/SSE type - Amp SDK may not support this directly
|
|
32
|
+
// Skip for now or handle if SDK supports it
|
|
33
|
+
} else {
|
|
34
|
+
// stdio type
|
|
35
|
+
mcpConfig[server.name] = {
|
|
36
|
+
command: server.command,
|
|
37
|
+
args: server.args,
|
|
38
|
+
env: server.env ? Object.fromEntries(server.env.map((e) => [e.name, e.value])) : undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
this.sessions.set(sessionId, {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
queue: null,
|
|
45
|
+
threadId: null,
|
|
46
|
+
controller: null,
|
|
30
47
|
cancelled: false,
|
|
31
48
|
active: false,
|
|
49
|
+
mode: 'default',
|
|
50
|
+
mcpConfig,
|
|
32
51
|
});
|
|
33
52
|
|
|
34
53
|
return {
|
|
35
54
|
sessionId,
|
|
36
|
-
models: { currentModelId: 'default', availableModels: [{ modelId: 'default', name: 'Default', description: 'Amp default' }] },
|
|
37
55
|
modes: {
|
|
38
56
|
currentModeId: 'default',
|
|
39
57
|
availableModes: [
|
|
40
|
-
{ id: 'default', name: '
|
|
41
|
-
{ id: '
|
|
42
|
-
{ id: 'bypassPermissions', name: 'Bypass Permissions', description: 'Skips all permission prompts' },
|
|
43
|
-
{ id: 'plan', name: 'Plan Mode', description: 'Analyze but not modify files or execute commands' },
|
|
58
|
+
{ id: 'default', name: 'Default', description: 'Prompts for permission on first use of each tool' },
|
|
59
|
+
{ id: 'bypass', name: 'Bypass', description: 'Skips all permission prompts' },
|
|
44
60
|
],
|
|
45
61
|
},
|
|
46
62
|
};
|
|
@@ -56,50 +72,6 @@ export class AmpAcpAgent {
|
|
|
56
72
|
s.cancelled = false;
|
|
57
73
|
s.active = true;
|
|
58
74
|
|
|
59
|
-
// Start a fresh Amp process per turn. Amp does not expose the Claude Code JSON stream flags;
|
|
60
|
-
// we pipe plain text and stream stdout lines back to ACP.
|
|
61
|
-
const ampCmd = process.env.AMP_EXECUTABLE || 'amp';
|
|
62
|
-
const spawnEnv = { ...process.env };
|
|
63
|
-
if (process.env.AMP_PREFER_SYSTEM_PATH === '1' && spawnEnv.PATH) {
|
|
64
|
-
// Drop npx/npm-local node_modules/.bin segments so we pick the system 'amp'
|
|
65
|
-
const parts = spawnEnv.PATH.split(':').filter((p) => !/\bnode_modules\/\.bin\b|\/_npx\//.test(p));
|
|
66
|
-
spawnEnv.PATH = parts.join(':');
|
|
67
|
-
}
|
|
68
|
-
const proc = spawn(ampCmd, ['--no-notifications'], {
|
|
69
|
-
cwd: params.cwd || process.cwd(),
|
|
70
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
71
|
-
env: spawnEnv,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const rlOut = readline.createInterface({ input: proc.stdout });
|
|
75
|
-
const rlErr = readline.createInterface({ input: proc.stderr });
|
|
76
|
-
|
|
77
|
-
s.proc = proc;
|
|
78
|
-
s.rl = rlOut;
|
|
79
|
-
s.queue = null;
|
|
80
|
-
|
|
81
|
-
let hadOutput = false;
|
|
82
|
-
|
|
83
|
-
rlOut.on('line', async (line) => {
|
|
84
|
-
hadOutput = true;
|
|
85
|
-
if (!line) return;
|
|
86
|
-
try {
|
|
87
|
-
await this.client.sessionUpdate({
|
|
88
|
-
sessionId: params.sessionId,
|
|
89
|
-
update: {
|
|
90
|
-
sessionUpdate: 'agent_message_chunk',
|
|
91
|
-
content: { type: 'text', text: line },
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
} catch (e) {
|
|
95
|
-
console.error('[acp] sessionUpdate failed', e);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
rlErr.on('line', (line) => {
|
|
100
|
-
console.error(`[amp] ${line}`);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
75
|
// Build plain-text input for Amp from ACP prompt chunks
|
|
104
76
|
let textInput = '';
|
|
105
77
|
for (const chunk of params.prompt) {
|
|
@@ -116,92 +88,89 @@ export class AmpAcpAgent {
|
|
|
116
88
|
}
|
|
117
89
|
break;
|
|
118
90
|
case 'image':
|
|
119
|
-
// Images not supported by Amp
|
|
91
|
+
// Images not supported by Amp SDK yet via simple prompt string; ignore for now
|
|
120
92
|
break;
|
|
121
93
|
default:
|
|
122
94
|
break;
|
|
123
95
|
}
|
|
124
96
|
}
|
|
125
|
-
if (!textInput.endsWith('\n')) textInput += '\n';
|
|
126
97
|
|
|
127
|
-
|
|
128
|
-
|
|
98
|
+
const options = {
|
|
99
|
+
cwd: params.cwd || process.cwd(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (s.mode === 'bypass') {
|
|
103
|
+
options.dangerouslyAllowAll = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (Object.keys(s.mcpConfig).length > 0) {
|
|
107
|
+
options.mcpConfig = s.mcpConfig;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (s.threadId) {
|
|
111
|
+
options.continue = s.threadId;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
s.controller = controller;
|
|
129
116
|
|
|
130
117
|
try {
|
|
131
|
-
await
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
118
|
+
for await (const message of execute({ prompt: textInput, options, signal: controller.signal })) {
|
|
119
|
+
if (!s.threadId && message.session_id) {
|
|
120
|
+
s.threadId = message.session_id;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (message.type === 'assistant') {
|
|
124
|
+
for (const n of toAcpNotifications(message, params.sessionId)) {
|
|
125
|
+
try {
|
|
126
|
+
await this.client.sessionUpdate(n);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error('[acp] sessionUpdate failed', e);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (message.type === 'result' && message.is_error) {
|
|
134
|
+
await this.client.sessionUpdate({
|
|
135
|
+
sessionId: params.sessionId,
|
|
136
|
+
update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: `Error: ${message.error}` } },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { stopReason: s.cancelled ? 'cancelled' : 'end_turn' };
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (s.cancelled || (err.name === 'AbortError') || err.message.includes('aborted')) {
|
|
144
|
+
return { stopReason: 'cancelled' };
|
|
145
|
+
}
|
|
146
|
+
console.error('[amp] Execution error:', err);
|
|
147
|
+
throw err;
|
|
139
148
|
} finally {
|
|
140
149
|
s.active = false;
|
|
141
150
|
s.cancelled = false;
|
|
151
|
+
s.controller = null;
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
async cancel(params) {
|
|
146
156
|
const s = this.sessions.get(params.sessionId);
|
|
147
157
|
if (!s) return {};
|
|
148
|
-
if (s.active && s.
|
|
158
|
+
if (s.active && s.controller) {
|
|
149
159
|
s.cancelled = true;
|
|
150
|
-
|
|
151
|
-
// ensure readers unblock
|
|
152
|
-
try { s.queue?.end?.(); } catch {}
|
|
160
|
+
s.controller.abort();
|
|
153
161
|
}
|
|
154
162
|
return {};
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
async setSessionModel(_params) { return {}; }
|
|
158
166
|
|
|
159
|
-
async setSessionMode(
|
|
167
|
+
async setSessionMode(params) {
|
|
168
|
+
const s = this.sessions.get(params.sessionId);
|
|
169
|
+
if (!s) throw new Error('Session not found');
|
|
170
|
+
s.mode = params.modeId;
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
160
173
|
|
|
161
174
|
async readTextFile(params) { return this.client.readTextFile(params); }
|
|
162
175
|
async writeTextFile(params) { return this.client.writeTextFile(params); }
|
|
163
176
|
}
|
|
164
|
-
|
|
165
|
-
function createJsonQueue(rl) {
|
|
166
|
-
const buf = [];
|
|
167
|
-
const waiters = [];
|
|
168
|
-
rl.on('line', (line) => {
|
|
169
|
-
let obj;
|
|
170
|
-
try {
|
|
171
|
-
obj = JSON.parse(line);
|
|
172
|
-
} catch {
|
|
173
|
-
return; // ignore non-JSON
|
|
174
|
-
}
|
|
175
|
-
if (waiters.length) {
|
|
176
|
-
const resolve = waiters.shift();
|
|
177
|
-
resolve(obj);
|
|
178
|
-
} else {
|
|
179
|
-
buf.push(obj);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
let ended = false;
|
|
183
|
-
function end() {
|
|
184
|
-
if (ended) return;
|
|
185
|
-
ended = true;
|
|
186
|
-
while (waiters.length) {
|
|
187
|
-
const resolve = waiters.shift();
|
|
188
|
-
resolve(null);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
rl.on('close', end);
|
|
192
|
-
rl.on('SIGINT', end);
|
|
193
|
-
return {
|
|
194
|
-
next() {
|
|
195
|
-
return new Promise((resolve) => {
|
|
196
|
-
if (buf.length) {
|
|
197
|
-
resolve(buf.shift());
|
|
198
|
-
} else if (ended) {
|
|
199
|
-
resolve(null);
|
|
200
|
-
} else {
|
|
201
|
-
waiters.push(resolve);
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
},
|
|
205
|
-
end,
|
|
206
|
-
};
|
|
207
|
-
}
|