amp-acp 0.1.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 +41 -24
- package/img/screenshot.png +0 -0
- package/package.json +7 -5
- package/src/index.js +0 -0
- package/src/server.js +83 -122
package/README.md
CHANGED
|
@@ -1,45 +1,62 @@
|
|
|
1
|
-
# ACP adapter for
|
|
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
|
-
|
|
13
|
+
Add to your Zed `settings.json` (open with `cmd+,` or `ctrl+,`):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"agent_servers": {
|
|
18
|
+
"Amp": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["-y", "amp-acp"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
14
24
|
```
|
|
15
25
|
|
|
16
|
-
|
|
26
|
+
That's it! The SDK handles authentication and Amp integration automatically.
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
npm install
|
|
20
|
-
npm start
|
|
21
|
-
```
|
|
28
|
+
## First Use
|
|
22
29
|
|
|
23
|
-
|
|
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
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"agent_servers": {
|
|
35
|
+
"Amp": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "amp-acp"],
|
|
38
|
+
"env": {
|
|
39
|
+
"AMP_API_KEY": "your-api-key-here"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
29
45
|
|
|
30
|
-
|
|
31
|
-
- Open Settings → External Agents (ACP)
|
|
32
|
-
- Connect to the adapter
|
|
33
|
-
- Start a new Amp thread
|
|
46
|
+
**If you [have Amp CLI installed](https://ampcode.com/manual#getting-started-command-line-interface)**: Run `amp login` first to authenticate.
|
|
34
47
|
|
|
35
48
|
## How it Works
|
|
36
49
|
|
|
37
|
-
-
|
|
38
|
-
-
|
|
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
|
|
39
53
|
- Tool permissions are handled by Amp (no additional configuration needed)
|
|
54
|
+
- Supports conversation continuity across multiple prompts
|
|
40
55
|
|
|
41
56
|
## Troubleshooting
|
|
42
57
|
|
|
43
|
-
**
|
|
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.
|
|
44
61
|
|
|
45
|
-
**
|
|
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
|
-
"amp-acp": "
|
|
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/index.js
CHANGED
|
File without changes
|
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,160 +72,105 @@ export class AmpAcpAgent {
|
|
|
56
72
|
s.cancelled = false;
|
|
57
73
|
s.active = true;
|
|
58
74
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
cwd: params.cwd || process.cwd(),
|
|
62
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
63
|
-
env: process.env,
|
|
64
|
-
});
|
|
65
|
-
const rl = readline.createInterface({ input: proc.stdout });
|
|
66
|
-
const queue = createJsonQueue(rl);
|
|
67
|
-
// Capture exit to clean state
|
|
68
|
-
proc.on('close', () => {
|
|
69
|
-
// Do not null out queue while a turn may still await next(); just signal end
|
|
70
|
-
try { queue?.end?.(); } catch {}
|
|
71
|
-
});
|
|
72
|
-
// Optionally log stderr (redirected to our stderr by default)
|
|
73
|
-
proc.stderr?.on('data', (d) => {
|
|
74
|
-
console.error(`[amp] ${d.toString()}`.trim());
|
|
75
|
-
});
|
|
76
|
-
s.proc = proc;
|
|
77
|
-
s.rl = rl;
|
|
78
|
-
s.queue = queue;
|
|
79
|
-
// Don't wait for init; amp will emit it before assistant/user events
|
|
80
|
-
|
|
81
|
-
// Build Amp user message JSON line from ACP prompt chunks
|
|
82
|
-
const content = [];
|
|
75
|
+
// Build plain-text input for Amp from ACP prompt chunks
|
|
76
|
+
let textInput = '';
|
|
83
77
|
for (const chunk of params.prompt) {
|
|
84
78
|
switch (chunk.type) {
|
|
85
79
|
case 'text':
|
|
86
|
-
|
|
80
|
+
textInput += chunk.text;
|
|
87
81
|
break;
|
|
88
82
|
case 'resource_link':
|
|
89
|
-
|
|
83
|
+
textInput += `\n${chunk.uri}\n`;
|
|
90
84
|
break;
|
|
91
85
|
case 'resource':
|
|
92
86
|
if ('text' in chunk.resource) {
|
|
93
|
-
|
|
94
|
-
content.push({ type: 'text', text: `\n<context ref="${chunk.resource.uri}">\n${chunk.resource.text}\n</context>` });
|
|
87
|
+
textInput += `\n<context ref="${chunk.resource.uri}">\n${chunk.resource.text}\n</context>\n`;
|
|
95
88
|
}
|
|
96
89
|
break;
|
|
97
90
|
case 'image':
|
|
98
|
-
|
|
99
|
-
content.push({ type: 'image', source: { type: 'base64', data: chunk.data, media_type: chunk.mimeType } });
|
|
100
|
-
} else if (chunk.uri && chunk.uri.startsWith('http')) {
|
|
101
|
-
content.push({ type: 'image', source: { type: 'url', url: chunk.uri } });
|
|
102
|
-
}
|
|
91
|
+
// Images not supported by Amp SDK yet via simple prompt string; ignore for now
|
|
103
92
|
break;
|
|
104
93
|
default:
|
|
105
94
|
break;
|
|
106
95
|
}
|
|
107
96
|
}
|
|
108
97
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
message: { role: 'user', content },
|
|
112
|
-
parent_tool_use_id: null,
|
|
113
|
-
session_id: params.sessionId,
|
|
98
|
+
const options = {
|
|
99
|
+
cwd: params.cwd || process.cwd(),
|
|
114
100
|
};
|
|
115
101
|
|
|
116
|
-
s.
|
|
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;
|
|
117
116
|
|
|
118
117
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return { stopReason: s.cancelled ? 'cancelled' : 'refusal' };
|
|
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;
|
|
123
121
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
case 'user': {
|
|
134
|
-
// Skip echoing user messages to avoid duplicates in the client UI
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
case 'result': {
|
|
138
|
-
if (msg.subtype === 'success') return { stopReason: 'end_turn' };
|
|
139
|
-
if (msg.subtype === 'error_max_turns') return { stopReason: 'max_turn_requests' };
|
|
140
|
-
return { stopReason: 'refusal' };
|
|
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
|
+
}
|
|
141
131
|
}
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
});
|
|
144
138
|
}
|
|
145
139
|
}
|
|
146
|
-
|
|
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;
|
|
147
148
|
} finally {
|
|
148
149
|
s.active = false;
|
|
149
150
|
s.cancelled = false;
|
|
151
|
+
s.controller = null;
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
async cancel(params) {
|
|
154
156
|
const s = this.sessions.get(params.sessionId);
|
|
155
157
|
if (!s) return {};
|
|
156
|
-
if (s.active && s.
|
|
158
|
+
if (s.active && s.controller) {
|
|
157
159
|
s.cancelled = true;
|
|
158
|
-
|
|
159
|
-
// ensure readers unblock
|
|
160
|
-
try { s.queue?.end?.(); } catch {}
|
|
160
|
+
s.controller.abort();
|
|
161
161
|
}
|
|
162
162
|
return {};
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
async setSessionModel(_params) { return {}; }
|
|
166
166
|
|
|
167
|
-
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
|
+
}
|
|
168
173
|
|
|
169
174
|
async readTextFile(params) { return this.client.readTextFile(params); }
|
|
170
175
|
async writeTextFile(params) { return this.client.writeTextFile(params); }
|
|
171
176
|
}
|
|
172
|
-
|
|
173
|
-
function createJsonQueue(rl) {
|
|
174
|
-
const buf = [];
|
|
175
|
-
const waiters = [];
|
|
176
|
-
rl.on('line', (line) => {
|
|
177
|
-
let obj;
|
|
178
|
-
try {
|
|
179
|
-
obj = JSON.parse(line);
|
|
180
|
-
} catch {
|
|
181
|
-
return; // ignore non-JSON
|
|
182
|
-
}
|
|
183
|
-
if (waiters.length) {
|
|
184
|
-
const resolve = waiters.shift();
|
|
185
|
-
resolve(obj);
|
|
186
|
-
} else {
|
|
187
|
-
buf.push(obj);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
let ended = false;
|
|
191
|
-
function end() {
|
|
192
|
-
if (ended) return;
|
|
193
|
-
ended = true;
|
|
194
|
-
while (waiters.length) {
|
|
195
|
-
const resolve = waiters.shift();
|
|
196
|
-
resolve(null);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
rl.on('close', end);
|
|
200
|
-
rl.on('SIGINT', end);
|
|
201
|
-
return {
|
|
202
|
-
next() {
|
|
203
|
-
return new Promise((resolve) => {
|
|
204
|
-
if (buf.length) {
|
|
205
|
-
resolve(buf.shift());
|
|
206
|
-
} else if (ended) {
|
|
207
|
-
resolve(null);
|
|
208
|
-
} else {
|
|
209
|
-
waiters.push(resolve);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
},
|
|
213
|
-
end,
|
|
214
|
-
};
|
|
215
|
-
}
|