acpreact 1.0.3 → 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 +194 -132
  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,101 +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
- const child = spawn(cli, ['--stdin'], {
96
+ this.cliProcess = spawn('script', ['-q', '-c', `${cli} acp`, '/dev/null'], {
154
97
  stdio: ['pipe', 'pipe', 'pipe'],
155
- cwd: process.cwd()
98
+ cwd: process.cwd(),
99
+ env: { ...process.env, TERM: 'dumb' },
156
100
  });
157
101
 
158
- child.stdout.on('data', (data) => {
159
- output += data.toString();
102
+ this.cliProcess.stdout.on('data', (data) => {
103
+ this.buffer += data.toString();
104
+ this.processBuffer();
160
105
  });
161
106
 
162
- child.stderr.on('data', (data) => {
163
- errorOutput += data.toString();
107
+ this.cliProcess.stderr.on('data', (data) => {
108
+ this.emit('stderr', data.toString());
164
109
  });
165
110
 
166
- child.on('close', async (code) => {
167
- if (code !== 0 && code !== null) {
168
- reject(new Error(`CLI exited with code ${code}: ${errorOutput}`));
169
- return;
170
- }
111
+ this.cliProcess.on('error', (error) => {
112
+ this.emit('error', error);
113
+ reject(error);
114
+ });
171
115
 
172
- try {
173
- const toolCalls = this.parseToolCalls(output);
174
- const results = [];
175
-
176
- for (const call of toolCalls) {
177
- if (this.toolWhitelist.has(call.method)) {
178
- const result = await this.callTool(call.method, call.params);
179
- results.push({ tool: call.method, result });
180
- }
181
- }
182
-
183
- resolve({
184
- text: output,
185
- toolCalls: results,
186
- logs: this.toolCallLog
187
- });
188
- } catch (e) {
189
- resolve({
190
- text: output,
191
- error: e.message,
192
- logs: this.toolCallLog
193
- });
194
- }
116
+ this.cliProcess.on('close', (code) => {
117
+ this.emit('close', code);
118
+ this.cliProcess = null;
119
+ this.initialized = false;
120
+ this.sessionId = null;
195
121
  });
196
122
 
197
- child.on('error', (error) => {
198
- 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);
199
130
  });
200
131
 
201
- child.stdin.write(prompt);
202
- child.stdin.end();
132
+ setTimeout(() => this.createSession(), 500);
203
133
  });
204
134
  }
205
135
 
206
- parseToolCalls(output) {
207
- const calls = [];
208
- const lines = output.split('\n');
209
-
136
+ processBuffer() {
137
+ const lines = this.buffer.split('\n');
138
+ this.buffer = lines.pop() || '';
139
+
210
140
  for (const line of lines) {
211
141
  const trimmed = line.trim();
212
142
  if (!trimmed) continue;
213
-
214
- try {
215
- const json = JSON.parse(trimmed);
216
- if (json.jsonrpc === '2.0' && json.method && json.params) {
217
- calls.push({ method: json.method, params: json.params });
218
- }
219
- } catch (e) {
220
- // 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 });
221
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;
222
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 '';
223
226
 
224
- 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;
225
287
  }
226
288
  }
227
289
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acpreact",
3
- "version": "1.0.3",
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",