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 CHANGED
@@ -1,16 +1,34 @@
1
1
  # ACP adapter for AmpCode
2
2
 
3
- Use [Amp](https://ampcode.com) from [ACP](https://agentclientprotocol.com/)-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
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
- "AMP_EXECUTABLE": "path of AMP bin",
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
- Replace `"path of AMP bin"` with your Amp CLI path (e.g., `/usr/local/bin/amp`).
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
- - Streams Amp's JSON over ACP
34
- - 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
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
- **Connection fails**: Ensure `amp login` was successful and the CLI is in your `AMP_EXECUTABLE`.
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.2.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 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/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,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 CLI via stdin; ignore for now
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
- proc.stdin.write(textInput);
128
- proc.stdin.end();
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 new Promise((resolve) => {
132
- proc.on('close', () => {
133
- try { rlOut.close(); } catch {}
134
- try { rlErr.close(); } catch {}
135
- resolve();
136
- });
137
- });
138
- return { stopReason: s.cancelled ? 'cancelled' : (hadOutput ? 'end_turn' : '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;
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.proc) {
158
+ if (s.active && s.controller) {
149
159
  s.cancelled = true;
150
- try { s.proc.kill('SIGINT'); } catch {}
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(_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
+ }
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
- }