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 CHANGED
@@ -1,45 +1,62 @@
1
- # ACP adapter for Amp
1
+ # ACP adapter for AmpCode
2
2
 
3
- Use [Amp](https://ampcode.com) from ACP-compatible clients such as [Zed](https://zed.dev).
3
+ ![Screenshot](img/screenshot.png)
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
- - [Amp CLI](https://ampcode.com) installed and authenticated (`amp login`)
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
- ```bash
13
- npm install -g amp-acp
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
- Or run locally:
26
+ That's it! The SDK handles authentication and Amp integration automatically.
17
27
 
18
- ```bash
19
- npm install
20
- npm start
21
- ```
28
+ ## First Use
22
29
 
23
- ## Usage
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
- 1. Start the adapter:
26
- ```bash
27
- amp-acp
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
- 2. In Zed:
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
- - Streams Amp's Claude Code–compatible JSON over ACP
38
- - Renders Amp messages and interactions in Zed
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
- **Connection fails**: Ensure `amp login` was successful and the CLI is in your PATH.
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
- **Adapter not found**: Check that `amp-acp` is running before connecting from Zed.
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.1.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
7
- "amp-acp": "./src/index.js"
7
+ "amp-acp": "src/index.js"
8
8
  },
9
- "description": "ACP adapter that bridges Amp CLI to Agent Client Protocol (Zed external agent)",
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 { spawn } from 'node:child_process';
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
- proc: null,
28
- rl: null,
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: 'Always Ask', description: 'Prompts for permission on first use of each tool' },
41
- { id: 'acceptEdits', name: 'Accept Edits', description: 'Automatically accepts file edit permissions for the session' },
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
- // Start a fresh Amp process per turn (amp -x is single-turn). Avoid reusing prior proc to prevent races.
60
- const proc = spawn('amp', ['-x', '--stream-json', '--stream-json-input'], {
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
- content.push({ type: 'text', text: chunk.text });
80
+ textInput += chunk.text;
87
81
  break;
88
82
  case 'resource_link':
89
- content.push({ type: 'text', text: chunk.uri });
83
+ textInput += `\n${chunk.uri}\n`;
90
84
  break;
91
85
  case 'resource':
92
86
  if ('text' in chunk.resource) {
93
- content.push({ type: 'text', text: chunk.resource.uri });
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
- if (chunk.data) {
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 userMsg = {
110
- type: 'user',
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.proc.stdin.write(JSON.stringify(userMsg) + '\n');
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
- while (true) {
120
- const msg = await s.queue.next();
121
- if (msg == null) {
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
- switch (msg.type) {
125
- case 'system':
126
- // ignore init/compact/etc
127
- break;
128
- case 'assistant': {
129
- const notifs = toAcpNotifications(msg, params.sessionId);
130
- for (const n of notifs) await this.client.sessionUpdate(n);
131
- break;
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
- default:
143
- break;
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
- throw new Error('Session did not end in result');
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.proc) {
158
+ if (s.active && s.controller) {
157
159
  s.cancelled = true;
158
- try { s.proc.kill('SIGINT'); } catch {}
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(_params) { return {}; }
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
- }