acpreact 1.0.4 → 1.0.5

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.
Files changed (2) hide show
  1. package/core.js +193 -147
  2. package/package.json +1 -1
package/core.js CHANGED
@@ -1,18 +1,22 @@
1
1
  import { spawn } from 'child_process';
2
- import { fileURLToPath } from 'url';
3
- import { dirname, join } from 'path';
2
+ import { EventEmitter } from 'events';
4
3
 
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = dirname(__filename);
7
-
8
- class ACPProtocol {
4
+ class ACPProtocol extends EventEmitter {
9
5
  constructor(instruction) {
6
+ super();
10
7
  this.messageId = 0;
11
8
  this.instruction = instruction;
12
9
  this.toolWhitelist = new Set();
10
+ this.toolSchemas = {};
11
+ this.toolDescriptions = {};
13
12
  this.toolCallLog = [];
14
13
  this.rejectedCallLog = [];
15
14
  this.tools = {};
15
+ this.cliProcess = null;
16
+ this.pendingRequests = new Map();
17
+ this.buffer = '';
18
+ this.initialized = false;
19
+ this.sessionId = null;
16
20
  }
17
21
 
18
22
  generateRequestId() {
@@ -20,84 +24,42 @@ class ACPProtocol {
20
24
  }
21
25
 
22
26
  createJsonRpcRequest(method, params) {
23
- return {
24
- jsonrpc: "2.0",
25
- id: this.generateRequestId(),
26
- method,
27
- params,
28
- };
27
+ return { jsonrpc: "2.0", id: this.generateRequestId(), method, params };
29
28
  }
30
29
 
31
30
  createJsonRpcResponse(id, result) {
32
- return {
33
- jsonrpc: "2.0",
34
- id,
35
- result,
36
- };
31
+ return { jsonrpc: "2.0", id, result };
37
32
  }
38
33
 
39
34
  createJsonRpcError(id, error) {
40
- return {
41
- jsonrpc: "2.0",
42
- id,
43
- error: {
44
- code: -32603,
45
- message: error,
46
- },
47
- };
35
+ return { jsonrpc: "2.0", id, error: { code: -32603, message: error } };
48
36
  }
49
37
 
50
38
  registerTool(name, description, inputSchema, handler) {
51
39
  this.toolWhitelist.add(name);
52
40
  this.tools[name] = handler;
53
- return {
54
- name,
55
- description,
56
- inputSchema,
57
- };
41
+ this.toolSchemas[name] = inputSchema;
42
+ this.toolDescriptions[name] = description;
43
+ return { name, description, inputSchema };
58
44
  }
59
45
 
60
- createInitializeResponse() {
61
- const agentCapabilities = Array.from(this.toolWhitelist).map(toolName => ({
62
- type: "tool",
46
+ getToolsList() {
47
+ return Array.from(this.toolWhitelist).map(toolName => ({
63
48
  name: toolName,
64
- whitelisted: true,
49
+ description: this.toolDescriptions[toolName],
50
+ inputSchema: this.toolSchemas[toolName],
65
51
  }));
66
-
67
- const result = {
68
- protocolVersion: "1.0",
69
- serverInfo: {
70
- name: "acpreact ACP Server",
71
- version: "1.0.0",
72
- },
73
- securityConfiguration: {
74
- toolWhitelistEnabled: true,
75
- allowedTools: Array.from(this.toolWhitelist),
76
- rejectionBehavior: "strict",
77
- },
78
- agentCapabilities,
79
- };
80
-
81
- if (this.instruction) {
82
- result.instruction = this.instruction;
83
- }
84
-
85
- return {
86
- jsonrpc: "2.0",
87
- id: 0,
88
- result,
89
- };
90
52
  }
91
53
 
92
54
  validateToolCall(toolName) {
93
55
  if (!this.toolWhitelist.has(toolName)) {
94
56
  const availableTools = Array.from(this.toolWhitelist);
95
- const error = `Tool not available. Only these tools are available: ${availableTools.join(', ')}`;
57
+ const error = `Tool not available. Available: ${availableTools.join(', ')}`;
96
58
  this.rejectedCallLog.push({
97
59
  timestamp: new Date().toISOString(),
98
60
  attemptedTool: toolName,
99
61
  reason: 'Not in whitelist',
100
- availableTools: availableTools,
62
+ availableTools,
101
63
  });
102
64
  return { allowed: false, error };
103
65
  }
@@ -127,117 +89,201 @@ class ACPProtocol {
127
89
  throw new Error(`Unknown tool: ${toolName}`);
128
90
  }
129
91
 
130
- async process(text, options = {}) {
131
- const cli = options.cli || 'opencode';
132
- const instruction = options.instruction || this.instruction || '';
133
-
134
- const toolsDesc = Array.from(this.toolWhitelist).map(name => {
135
- const tool = this.tools[name];
136
- return `- ${name}: Tool available`;
137
- }).join('\n');
138
-
139
- const prompt = `${instruction}
140
-
141
- Available tools:
142
- ${toolsDesc}
143
-
144
- Text to analyze:
145
- ${text}
146
-
147
- Analyze the text and call appropriate tools using the ACP protocol. Respond with JSON-RPC tool calls.`;
92
+ async start(cli = 'kilo') {
93
+ if (this.cliProcess) return this.sessionId;
148
94
 
149
95
  return new Promise((resolve, reject) => {
150
- let output = '';
151
- let errorOutput = '';
152
-
153
- // Different CLIs have different invocation styles
154
- let args;
155
- let useStdin = false;
156
-
157
- if (cli === 'kilo') {
158
- // kilo uses: kilo run <message>
159
- args = ['run', prompt];
160
- } else {
161
- // opencode uses: opencode --stdin (with prompt via stdin)
162
- args = ['--stdin'];
163
- useStdin = true;
164
- }
165
-
166
- const child = spawn(cli, args, {
96
+ this.cliProcess = spawn('script', ['-q', '-c', `${cli} acp`, '/dev/null'], {
167
97
  stdio: ['pipe', 'pipe', 'pipe'],
168
98
  cwd: process.cwd(),
169
- timeout: 30000 // 30 second timeout
99
+ env: { ...process.env, TERM: 'dumb' },
170
100
  });
171
101
 
172
- child.stdout.on('data', (data) => {
173
- output += data.toString();
102
+ this.cliProcess.stdout.on('data', (data) => {
103
+ this.buffer += data.toString();
104
+ this.processBuffer();
174
105
  });
175
106
 
176
- child.stderr.on('data', (data) => {
177
- errorOutput += data.toString();
107
+ this.cliProcess.stderr.on('data', (data) => {
108
+ this.emit('stderr', data.toString());
178
109
  });
179
110
 
180
- child.on('close', async (code) => {
181
- if (code !== 0 && code !== null) {
182
- reject(new Error(`CLI exited with code ${code}: ${errorOutput}`));
183
- return;
184
- }
111
+ this.cliProcess.on('error', (error) => {
112
+ this.emit('error', error);
113
+ reject(error);
114
+ });
185
115
 
186
- try {
187
- const toolCalls = this.parseToolCalls(output);
188
- const results = [];
189
-
190
- for (const call of toolCalls) {
191
- if (this.toolWhitelist.has(call.method)) {
192
- const result = await this.callTool(call.method, call.params);
193
- results.push({ tool: call.method, result });
194
- }
195
- }
196
-
197
- resolve({
198
- text: output,
199
- toolCalls: results,
200
- logs: this.toolCallLog
201
- });
202
- } catch (e) {
203
- resolve({
204
- text: output,
205
- error: e.message,
206
- logs: this.toolCallLog
207
- });
208
- }
116
+ this.cliProcess.on('close', (code) => {
117
+ this.emit('close', code);
118
+ this.cliProcess = null;
119
+ this.initialized = false;
120
+ this.sessionId = null;
209
121
  });
210
122
 
211
- child.on('error', (error) => {
212
- reject(new Error(`Failed to spawn ${cli}: ${error.message}`));
123
+ const timeout = setTimeout(() => {
124
+ reject(new Error('ACP initialization timeout'));
125
+ }, 30000);
126
+
127
+ this.once('ready', () => {
128
+ clearTimeout(timeout);
129
+ resolve(this.sessionId);
213
130
  });
214
131
 
215
- if (useStdin) {
216
- child.stdin.write(prompt);
217
- child.stdin.end();
218
- }
132
+ setTimeout(() => this.createSession(), 500);
219
133
  });
220
134
  }
221
135
 
222
- parseToolCalls(output) {
223
- const calls = [];
224
- const lines = output.split('\n');
225
-
136
+ processBuffer() {
137
+ const lines = this.buffer.split('\n');
138
+ this.buffer = lines.pop() || '';
139
+
226
140
  for (const line of lines) {
227
141
  const trimmed = line.trim();
228
142
  if (!trimmed) continue;
229
-
230
- try {
231
- const json = JSON.parse(trimmed);
232
- if (json.jsonrpc === '2.0' && json.method && json.params) {
233
- calls.push({ method: json.method, params: json.params });
234
- }
235
- } catch (e) {
236
- // Not JSON, skip
143
+ this.handleMessage(trimmed);
144
+ }
145
+ }
146
+
147
+ handleMessage(line) {
148
+ let msg;
149
+ try {
150
+ msg = JSON.parse(line);
151
+ } catch {
152
+ return;
153
+ }
154
+
155
+ if (msg.method === 'initialize') {
156
+ this.send(this.createInitializeResponse(msg.id));
157
+ this.initialized = true;
158
+ } else if (msg.id !== undefined && msg.result !== undefined) {
159
+ const resolver = this.pendingRequests.get(msg.id);
160
+ if (resolver) {
161
+ this.pendingRequests.delete(msg.id);
162
+ resolver(msg.result);
163
+ }
164
+ if (msg.id === 1 && msg.result?.sessionId) {
165
+ this.sessionId = msg.result.sessionId;
166
+ this.emit('ready');
167
+ }
168
+ } else if (msg.id !== undefined && msg.error !== undefined) {
169
+ const resolver = this.pendingRequests.get(msg.id);
170
+ if (resolver) {
171
+ this.pendingRequests.delete(msg.id);
172
+ resolver({ error: msg.error });
237
173
  }
174
+ } else if (msg.method?.startsWith('tools/')) {
175
+ const toolName = msg.method.replace('tools/', '');
176
+ this.handleToolCall(msg.id, toolName, msg.params);
177
+ } else if (msg.method === 'session/update') {
178
+ this.emit('update', msg.params);
179
+ }
180
+ }
181
+
182
+ async handleToolCall(id, toolName, params) {
183
+ try {
184
+ const result = await this.callTool(toolName, params);
185
+ this.send(this.createJsonRpcResponse(id, result));
186
+ } catch (e) {
187
+ this.send(this.createJsonRpcError(id, e.message));
188
+ }
189
+ }
190
+
191
+ send(msg) {
192
+ if (this.cliProcess && this.cliProcess.stdin.writable) {
193
+ this.cliProcess.stdin.write(JSON.stringify(msg) + '\n');
194
+ }
195
+ }
196
+
197
+ createInitializeResponse(id) {
198
+ const result = {
199
+ protocolVersion: 1,
200
+ capabilities: { tools: this.getToolsList() },
201
+ serverInfo: { name: 'acpreact', version: '1.0.0' },
202
+ };
203
+
204
+ if (this.instruction) {
205
+ result.instruction = this.instruction;
238
206
  }
207
+
208
+ return { jsonrpc: "2.0", id, result };
209
+ }
210
+
211
+ createSession() {
212
+ this.send({
213
+ jsonrpc: "2.0",
214
+ id: 1,
215
+ method: "session/new",
216
+ params: {
217
+ cwd: process.cwd(),
218
+ mcpServers: [],
219
+ },
220
+ });
221
+ }
222
+
223
+ getToolsDescription() {
224
+ const tools = this.getToolsList();
225
+ if (tools.length === 0) return '';
239
226
 
240
- return calls;
227
+ return '\n\nAvailable tools:\n' + tools.map(t =>
228
+ `- ${t.name}: ${t.description}\n Parameters: ${JSON.stringify(t.inputSchema)}`
229
+ ).join('\n');
230
+ }
231
+
232
+ async sendPrompt(content) {
233
+ if (!this.sessionId) {
234
+ throw new Error('No session. Call start() first.');
235
+ }
236
+
237
+ const reqId = this.generateRequestId();
238
+ const promptWithTools = this.instruction
239
+ ? `${this.instruction}${this.getToolsDescription()}\n\nUser message: ${content}`
240
+ : content;
241
+
242
+ return new Promise((resolve) => {
243
+ this.pendingRequests.set(reqId, resolve);
244
+ this.send({
245
+ jsonrpc: "2.0",
246
+ id: reqId,
247
+ method: "session/prompt",
248
+ params: {
249
+ sessionId: this.sessionId,
250
+ prompt: [{ type: "text", text: promptWithTools }],
251
+ },
252
+ });
253
+
254
+ setTimeout(() => {
255
+ if (this.pendingRequests.has(reqId)) {
256
+ this.pendingRequests.delete(reqId);
257
+ resolve({ timeout: true });
258
+ }
259
+ }, 120000);
260
+ });
261
+ }
262
+
263
+ async process(text, options = {}) {
264
+ const cli = options.cli || 'kilo';
265
+
266
+ if (!this.cliProcess) {
267
+ await this.start(cli);
268
+ }
269
+
270
+ const result = await this.sendPrompt(text);
271
+
272
+ return {
273
+ text,
274
+ result,
275
+ toolCalls: this.toolCallLog.slice(-10),
276
+ logs: this.toolCallLog,
277
+ };
278
+ }
279
+
280
+ stop() {
281
+ if (this.cliProcess) {
282
+ this.cliProcess.kill();
283
+ this.cliProcess = null;
284
+ }
285
+ this.initialized = false;
286
+ this.sessionId = null;
241
287
  }
242
288
  }
243
289
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acpreact",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "description": "ACP SDK for monitoring and reacting to chat conversations",