acpreact 1.0.5 → 1.0.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.
Files changed (2) hide show
  1. package/core.js +128 -189
  2. package/package.json +1 -1
package/core.js CHANGED
@@ -12,29 +12,12 @@ class ACPProtocol extends EventEmitter {
12
12
  this.toolCallLog = [];
13
13
  this.rejectedCallLog = [];
14
14
  this.tools = {};
15
- this.cliProcess = null;
16
- this.pendingRequests = new Map();
17
- this.buffer = '';
18
- this.initialized = false;
19
- this.sessionId = null;
20
15
  }
21
16
 
22
17
  generateRequestId() {
23
18
  return ++this.messageId;
24
19
  }
25
20
 
26
- createJsonRpcRequest(method, params) {
27
- return { jsonrpc: "2.0", id: this.generateRequestId(), method, params };
28
- }
29
-
30
- createJsonRpcResponse(id, result) {
31
- return { jsonrpc: "2.0", id, result };
32
- }
33
-
34
- createJsonRpcError(id, error) {
35
- return { jsonrpc: "2.0", id, error: { code: -32603, message: error } };
36
- }
37
-
38
21
  registerTool(name, description, inputSchema, handler) {
39
22
  this.toolWhitelist.add(name);
40
23
  this.tools[name] = handler;
@@ -51,6 +34,21 @@ class ACPProtocol extends EventEmitter {
51
34
  }));
52
35
  }
53
36
 
37
+ getToolsPrompt() {
38
+ const tools = this.getToolsList();
39
+ if (tools.length === 0) return '';
40
+
41
+ let prompt = '\n\nYou have access to the following tools. You MUST use these tools to interact:\n\n';
42
+ for (const tool of tools) {
43
+ prompt += `## Tool: ${tool.name}\n${tool.description}\n`;
44
+ prompt += `Parameters: ${JSON.stringify(tool.inputSchema, null, 2)}\n`;
45
+ prompt += `To call this tool, output a JSON-RPC request on a single line:\n`;
46
+ prompt += `{"jsonrpc":"2.0","id":<number>,"method":"tools/${tool.name}","params":{<parameters>}}\n\n`;
47
+ }
48
+ prompt += 'IMPORTANT: When you need to use a tool, output ONLY the JSON-RPC request, nothing else.\n';
49
+ return prompt;
50
+ }
51
+
54
52
  validateToolCall(toolName) {
55
53
  if (!this.toolWhitelist.has(toolName)) {
56
54
  const availableTools = Array.from(this.toolWhitelist);
@@ -89,202 +87,143 @@ class ACPProtocol extends EventEmitter {
89
87
  throw new Error(`Unknown tool: ${toolName}`);
90
88
  }
91
89
 
92
- async start(cli = 'kilo') {
93
- if (this.cliProcess) return this.sessionId;
94
-
95
- return new Promise((resolve, reject) => {
96
- this.cliProcess = spawn('script', ['-q', '-c', `${cli} acp`, '/dev/null'], {
97
- stdio: ['pipe', 'pipe', 'pipe'],
98
- cwd: process.cwd(),
99
- env: { ...process.env, TERM: 'dumb' },
100
- });
101
-
102
- this.cliProcess.stdout.on('data', (data) => {
103
- this.buffer += data.toString();
104
- this.processBuffer();
105
- });
106
-
107
- this.cliProcess.stderr.on('data', (data) => {
108
- this.emit('stderr', data.toString());
109
- });
110
-
111
- this.cliProcess.on('error', (error) => {
112
- this.emit('error', error);
113
- reject(error);
114
- });
115
-
116
- this.cliProcess.on('close', (code) => {
117
- this.emit('close', code);
118
- this.cliProcess = null;
119
- this.initialized = false;
120
- this.sessionId = null;
121
- });
122
-
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);
130
- });
131
-
132
- setTimeout(() => this.createSession(), 500);
133
- });
134
- }
135
-
136
- processBuffer() {
137
- const lines = this.buffer.split('\n');
138
- this.buffer = lines.pop() || '';
139
-
90
+ parseTextOutput(output) {
91
+ let text = '';
92
+ const lines = output.split('\n');
93
+
140
94
  for (const line of lines) {
141
95
  const trimmed = line.trim();
142
96
  if (!trimmed) continue;
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 });
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);
97
+
98
+ try {
99
+ const json = JSON.parse(trimmed);
100
+ if (json.type === 'text' && json.part?.text) {
101
+ text += json.part.text;
102
+ }
103
+ } catch {}
179
104
  }
105
+
106
+ return text;
180
107
  }
181
108
 
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));
109
+ parseToolCalls(output) {
110
+ const calls = [];
111
+
112
+ const textContent = this.parseTextOutput(output);
113
+
114
+ for (const line of textContent.split('\n')) {
115
+ const trimmed = line.trim();
116
+ if (!trimmed) continue;
117
+
118
+ try {
119
+ const json = JSON.parse(trimmed);
120
+ if (json.jsonrpc === '2.0' && json.method?.startsWith('tools/') && json.params) {
121
+ calls.push({
122
+ id: json.id,
123
+ method: json.method,
124
+ params: json.params
125
+ });
126
+ }
127
+ } catch {}
188
128
  }
189
- }
190
-
191
- send(msg) {
192
- if (this.cliProcess && this.cliProcess.stdin.writable) {
193
- this.cliProcess.stdin.write(JSON.stringify(msg) + '\n');
129
+
130
+ const lines = output.split('\n');
131
+ for (const line of lines) {
132
+ const trimmed = line.trim();
133
+ if (!trimmed) continue;
134
+
135
+ try {
136
+ const json = JSON.parse(trimmed);
137
+ if (json.jsonrpc === '2.0' && json.method?.startsWith('tools/') && json.params) {
138
+ calls.push({
139
+ id: json.id,
140
+ method: json.method,
141
+ params: json.params
142
+ });
143
+ }
144
+ } catch {}
194
145
  }
146
+
147
+ return calls;
195
148
  }
196
149
 
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;
206
- }
150
+ async process(text, options = {}) {
151
+ const cli = options.cli || 'kilo';
152
+ const model = options.model || 'kilo/z-ai/glm-5:free';
153
+
154
+ const fullPrompt = this.instruction
155
+ ? `${this.instruction}${this.getToolsPrompt()}\n\n---\n\n${text}`
156
+ : `${this.getToolsPrompt()}\n\n---\n\n${text}`;
207
157
 
208
- return { jsonrpc: "2.0", id, result };
209
- }
158
+ const escapedPrompt = fullPrompt.replace(/"/g, '\\"').replace(/\n/g, ' ');
159
+
160
+ return new Promise((resolve, reject) => {
161
+ let output = '';
162
+ let errorOutput = '';
210
163
 
211
- createSession() {
212
- this.send({
213
- jsonrpc: "2.0",
214
- id: 1,
215
- method: "session/new",
216
- params: {
164
+ const child = spawn('script', ['-q', '-c',
165
+ `${cli} run --auto --model ${model} --format json "${escapedPrompt}"`,
166
+ '/dev/null'
167
+ ], {
168
+ stdio: ['pipe', 'pipe', 'pipe'],
217
169
  cwd: process.cwd(),
218
- mcpServers: [],
219
- },
220
- });
221
- }
170
+ env: { ...process.env, TERM: 'dumb' },
171
+ });
222
172
 
223
- getToolsDescription() {
224
- const tools = this.getToolsList();
225
- if (tools.length === 0) return '';
226
-
227
- return '\n\nAvailable tools:\n' + tools.map(t =>
228
- `- ${t.name}: ${t.description}\n Parameters: ${JSON.stringify(t.inputSchema)}`
229
- ).join('\n');
230
- }
173
+ child.stdout.on('data', (data) => {
174
+ output += data.toString();
175
+ });
231
176
 
232
- async sendPrompt(content) {
233
- if (!this.sessionId) {
234
- throw new Error('No session. Call start() first.');
235
- }
177
+ child.stderr.on('data', (data) => {
178
+ errorOutput += data.toString();
179
+ });
180
+
181
+ child.on('close', async (code) => {
182
+ if (code !== 0 && code !== null && !output) {
183
+ reject(new Error(`CLI exited with code ${code}: ${errorOutput}`));
184
+ return;
185
+ }
236
186
 
237
- const reqId = this.generateRequestId();
238
- const promptWithTools = this.instruction
239
- ? `${this.instruction}${this.getToolsDescription()}\n\nUser message: ${content}`
240
- : content;
187
+ try {
188
+ const toolCalls = this.parseToolCalls(output);
189
+ const results = [];
190
+
191
+ for (const call of toolCalls) {
192
+ const toolName = call.method.replace('tools/', '');
193
+ if (this.toolWhitelist.has(toolName)) {
194
+ const result = await this.callTool(toolName, call.params);
195
+ results.push({ tool: toolName, result });
196
+ }
197
+ }
198
+
199
+ resolve({
200
+ text: this.parseTextOutput(output),
201
+ rawOutput: output,
202
+ toolCalls: results,
203
+ logs: this.toolCallLog
204
+ });
205
+ } catch (e) {
206
+ resolve({
207
+ text: this.parseTextOutput(output),
208
+ rawOutput: output,
209
+ error: e.message,
210
+ logs: this.toolCallLog
211
+ });
212
+ }
213
+ });
241
214
 
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
- },
215
+ child.on('error', (error) => {
216
+ reject(new Error(`Failed to spawn ${cli}: ${error.message}`));
252
217
  });
253
218
 
254
219
  setTimeout(() => {
255
- if (this.pendingRequests.has(reqId)) {
256
- this.pendingRequests.delete(reqId);
257
- resolve({ timeout: true });
258
- }
220
+ child.kill();
221
+ reject(new Error('Timeout'));
259
222
  }, 120000);
260
223
  });
261
224
  }
262
225
 
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;
287
- }
226
+ stop() {}
288
227
  }
289
228
 
290
229
  export { ACPProtocol };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acpreact",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "description": "ACP SDK for monitoring and reacting to chat conversations",