amp-acp 0.1.0 → 0.2.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,6 +1,6 @@
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
+ Use [Amp](https://ampcode.com) from [ACP](https://agentclientprotocol.com/)-compatible clients such as [Zed](https://zed.dev).
4
4
 
5
5
  ## Prerequisites
6
6
 
@@ -9,37 +9,31 @@ Use [Amp](https://ampcode.com) from ACP-compatible clients such as [Zed](https:/
9
9
 
10
10
  ## Installation
11
11
 
12
- ```bash
13
- npm install -g amp-acp
12
+ Add to your Zed `settings.json` (open with `cmd+,` or `ctrl+,`):
13
+
14
+ ```json
15
+ {
16
+ "agent_servers": {
17
+ "Amp": {
18
+ "command": "npx",
19
+ "args": ["-y", "amp-acp"],
20
+ "env": {
21
+ "AMP_EXECUTABLE": "path of AMP bin",
22
+ "AMP_PREFER_SYSTEM_PATH": "1"
23
+ }
24
+ }
25
+ }
26
+ }
14
27
  ```
15
28
 
16
- Or run locally:
17
-
18
- ```bash
19
- npm install
20
- npm start
21
- ```
22
-
23
- ## Usage
24
-
25
- 1. Start the adapter:
26
- ```bash
27
- amp-acp
28
- ```
29
-
30
- 2. In Zed:
31
- - Open Settings → External Agents (ACP)
32
- - Connect to the adapter
33
- - Start a new Amp thread
29
+ Replace `"path of AMP bin"` with your Amp CLI path (e.g., `/usr/local/bin/amp`).
34
30
 
35
31
  ## How it Works
36
32
 
37
- - Streams Amp's Claude Code–compatible JSON over ACP
33
+ - Streams Amp's JSON over ACP
38
34
  - Renders Amp messages and interactions in Zed
39
35
  - Tool permissions are handled by Amp (no additional configuration needed)
40
36
 
41
37
  ## Troubleshooting
42
38
 
43
- **Connection fails**: Ensure `amp login` was successful and the CLI is in your PATH.
44
-
45
- **Adapter not found**: Check that `amp-acp` is running before connecting from Zed.
39
+ **Connection fails**: Ensure `amp login` was successful and the CLI is in your `AMP_EXECUTABLE`.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "amp-acp",
3
- "version": "0.1.0",
3
+ "version": "0.2.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
9
  "description": "ACP adapter that bridges Amp CLI to Agent Client Protocol (Zed external agent)",
10
10
  "license": "Apache-2.0",
@@ -16,4 +16,4 @@
16
16
  "dependencies": {
17
17
  "@agentclientprotocol/sdk": "0.4.8"
18
18
  }
19
- }
19
+ }
package/src/index.js CHANGED
File without changes
package/src/server.js CHANGED
@@ -56,94 +56,86 @@ export class AmpAcpAgent {
56
56
  s.cancelled = false;
57
57
  s.active = true;
58
58
 
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'], {
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'], {
61
69
  cwd: params.cwd || process.cwd(),
62
70
  stdio: ['pipe', 'pipe', 'pipe'],
63
- env: process.env,
71
+ env: spawnEnv,
64
72
  });
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 {}
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
+ }
71
97
  });
72
- // Optionally log stderr (redirected to our stderr by default)
73
- proc.stderr?.on('data', (d) => {
74
- console.error(`[amp] ${d.toString()}`.trim());
98
+
99
+ rlErr.on('line', (line) => {
100
+ console.error(`[amp] ${line}`);
75
101
  });
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
102
 
81
- // Build Amp user message JSON line from ACP prompt chunks
82
- const content = [];
103
+ // Build plain-text input for Amp from ACP prompt chunks
104
+ let textInput = '';
83
105
  for (const chunk of params.prompt) {
84
106
  switch (chunk.type) {
85
107
  case 'text':
86
- content.push({ type: 'text', text: chunk.text });
108
+ textInput += chunk.text;
87
109
  break;
88
110
  case 'resource_link':
89
- content.push({ type: 'text', text: chunk.uri });
111
+ textInput += `\n${chunk.uri}\n`;
90
112
  break;
91
113
  case 'resource':
92
114
  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>` });
115
+ textInput += `\n<context ref="${chunk.resource.uri}">\n${chunk.resource.text}\n</context>\n`;
95
116
  }
96
117
  break;
97
118
  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
- }
119
+ // Images not supported by Amp CLI via stdin; ignore for now
103
120
  break;
104
121
  default:
105
122
  break;
106
123
  }
107
124
  }
125
+ if (!textInput.endsWith('\n')) textInput += '\n';
108
126
 
109
- const userMsg = {
110
- type: 'user',
111
- message: { role: 'user', content },
112
- parent_tool_use_id: null,
113
- session_id: params.sessionId,
114
- };
115
-
116
- s.proc.stdin.write(JSON.stringify(userMsg) + '\n');
127
+ proc.stdin.write(textInput);
128
+ proc.stdin.end();
117
129
 
118
130
  try {
119
- while (true) {
120
- const msg = await s.queue.next();
121
- if (msg == null) {
122
- return { stopReason: s.cancelled ? 'cancelled' : 'refusal' };
123
- }
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' };
141
- }
142
- default:
143
- break;
144
- }
145
- }
146
- throw new Error('Session did not end in result');
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') };
147
139
  } finally {
148
140
  s.active = false;
149
141
  s.cancelled = false;