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