amp-acp 0.2.0 → 0.3.7

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/AGENTS.md ADDED
@@ -0,0 +1,22 @@
1
+ # AGENTS.md
2
+
3
+ ## Commands
4
+ - `npm start` or `node src/index.js` — Run the ACP adapter
5
+ - `npm run lint` — Lint (currently a no-op)
6
+ - `npm test` — Test (currently a no-op)
7
+
8
+ ## Architecture
9
+ This is an ACP (Agent Client Protocol) adapter that bridges Amp Code to ACP-compatible clients like Zed.
10
+
11
+ - `src/index.js` — Entry point, redirects console to stderr (stdout reserved for ACP stream)
12
+ - `src/run-acp.js` — Sets up ACP connection using stdin/stdout JSON streams
13
+ - `src/server.js` — `AmpAcpAgent` class: handles sessions, prompts, MCP config, and calls `@sourcegraph/amp-sdk`
14
+ - `src/to-acp.js` — Converts Amp stream events to ACP `sessionUpdate` notifications
15
+ - `src/utils.js` — Node-to-Web stream converters
16
+
17
+ ## Code Style
18
+ - ES modules (`"type": "module"` in package.json), use `.js` extension in imports
19
+ - No TypeScript; plain JavaScript with JSDoc if needed
20
+ - Use `console.error` for logging (stdout is for ACP protocol only)
21
+ - Error handling: throw `RequestError` from `@agentclientprotocol/sdk` for protocol errors
22
+ - Naming: camelCase for variables/functions, PascalCase for classes
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,19 +1,25 @@
1
1
  {
2
2
  "name": "amp-acp",
3
- "version": "0.2.0",
3
+ "version": "0.3.7",
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
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/tao12345666333/amp-acp"
14
+ },
11
15
  "scripts": {
12
16
  "start": "node src/index.js",
13
17
  "lint": "echo 'no lint'",
14
18
  "test": "node -e 'console.log(\"OK\")'"
15
19
  },
16
20
  "dependencies": {
17
- "@agentclientprotocol/sdk": "0.4.8"
21
+ "@agentclientprotocol/sdk": "0.4.8",
22
+ "@sourcegraph/amp": "^0.0.1765685598-g5365e6",
23
+ "@sourcegraph/amp-sdk": "^0.1.0-20251214200908-g3251f72"
18
24
  }
19
- }
25
+ }
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,27 +23,64 @@ 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
- return {
53
+ const result = {
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
  };
63
+
64
+ setImmediate(async () => {
65
+ try {
66
+ await this.client.sessionUpdate({
67
+ sessionId,
68
+ update: {
69
+ sessionUpdate: 'available_commands_update',
70
+ availableCommands: [
71
+ {
72
+ name: 'init',
73
+ description: 'Generate an AGENTS.md file for the project',
74
+ },
75
+ ],
76
+ },
77
+ });
78
+ } catch (e) {
79
+ console.error('[acp] failed to send available_commands_update', e);
80
+ }
81
+ });
82
+
83
+ return result;
47
84
  }
48
85
 
49
86
  async authenticate(_params) {
@@ -56,56 +93,23 @@ export class AmpAcpAgent {
56
93
  s.cancelled = false;
57
94
  s.active = true;
58
95
 
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
96
  // Build plain-text input for Amp from ACP prompt chunks
104
97
  let textInput = '';
105
98
  for (const chunk of params.prompt) {
106
99
  switch (chunk.type) {
107
100
  case 'text':
108
- textInput += chunk.text;
101
+ if (chunk.text.trim() === '/init') {
102
+ textInput += `Please analyze this codebase and create an AGENTS.md file containing:
103
+ 1. Build/lint/test commands - especially for running a single test
104
+ 2. Architecture and codebase structure information, including important subprojects, internal APIs, databases, etc.
105
+ 3. Code style guidelines, including imports, conventions, formatting, types, naming conventions, error handling, etc.
106
+
107
+ The file you create will be given to agentic coding tools (such as yourself) that operate in this repository. Make it about 20 lines long.
108
+
109
+ If there are Cursor rules (in .cursor/rules/ or .cursorrules), Claude rules (CLAUDE.md), Windsurf rules (.windsurfrules), Cline rules (.clinerules), Goose rules (.goosehints), or Copilot rules (in .github/copilot-instructions.md), make sure to include them. Also, first check if there is an existing AGENTS.md or AGENT.md file, and if so, update it instead of overwriting it.`;
110
+ } else {
111
+ textInput += chunk.text;
112
+ }
109
113
  break;
110
114
  case 'resource_link':
111
115
  textInput += `\n${chunk.uri}\n`;
@@ -116,92 +120,89 @@ export class AmpAcpAgent {
116
120
  }
117
121
  break;
118
122
  case 'image':
119
- // Images not supported by Amp CLI via stdin; ignore for now
123
+ // Images not supported by Amp SDK yet via simple prompt string; ignore for now
120
124
  break;
121
125
  default:
122
126
  break;
123
127
  }
124
128
  }
125
- if (!textInput.endsWith('\n')) textInput += '\n';
126
129
 
127
- proc.stdin.write(textInput);
128
- proc.stdin.end();
130
+ const options = {
131
+ cwd: params.cwd || process.cwd(),
132
+ };
133
+
134
+ if (s.mode === 'bypass') {
135
+ options.dangerouslyAllowAll = true;
136
+ }
137
+
138
+ if (Object.keys(s.mcpConfig).length > 0) {
139
+ options.mcpConfig = s.mcpConfig;
140
+ }
141
+
142
+ if (s.threadId) {
143
+ options.continue = s.threadId;
144
+ }
145
+
146
+ const controller = new AbortController();
147
+ s.controller = controller;
129
148
 
130
149
  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') };
150
+ for await (const message of execute({ prompt: textInput, options, signal: controller.signal })) {
151
+ if (!s.threadId && message.session_id) {
152
+ s.threadId = message.session_id;
153
+ }
154
+
155
+ if (message.type === 'assistant') {
156
+ for (const n of toAcpNotifications(message, params.sessionId)) {
157
+ try {
158
+ await this.client.sessionUpdate(n);
159
+ } catch (e) {
160
+ console.error('[acp] sessionUpdate failed', e);
161
+ }
162
+ }
163
+ }
164
+
165
+ if (message.type === 'result' && message.is_error) {
166
+ await this.client.sessionUpdate({
167
+ sessionId: params.sessionId,
168
+ update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: `Error: ${message.error}` } },
169
+ });
170
+ }
171
+ }
172
+
173
+ return { stopReason: s.cancelled ? 'cancelled' : 'end_turn' };
174
+ } catch (err) {
175
+ if (s.cancelled || (err.name === 'AbortError') || err.message.includes('aborted')) {
176
+ return { stopReason: 'cancelled' };
177
+ }
178
+ console.error('[amp] Execution error:', err);
179
+ throw err;
139
180
  } finally {
140
181
  s.active = false;
141
182
  s.cancelled = false;
183
+ s.controller = null;
142
184
  }
143
185
  }
144
186
 
145
187
  async cancel(params) {
146
188
  const s = this.sessions.get(params.sessionId);
147
189
  if (!s) return {};
148
- if (s.active && s.proc) {
190
+ if (s.active && s.controller) {
149
191
  s.cancelled = true;
150
- try { s.proc.kill('SIGINT'); } catch {}
151
- // ensure readers unblock
152
- try { s.queue?.end?.(); } catch {}
192
+ s.controller.abort();
153
193
  }
154
194
  return {};
155
195
  }
156
196
 
157
197
  async setSessionModel(_params) { return {}; }
158
198
 
159
- async setSessionMode(_params) { return {}; }
199
+ async setSessionMode(params) {
200
+ const s = this.sessions.get(params.sessionId);
201
+ if (!s) throw new Error('Session not found');
202
+ s.mode = params.modeId;
203
+ return {};
204
+ }
160
205
 
161
206
  async readTextFile(params) { return this.client.readTextFile(params); }
162
207
  async writeTextFile(params) { return this.client.writeTextFile(params); }
163
208
  }
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
- }