acpreact 1.0.6 → 1.0.8
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 +78 -66
- package/core.js +97 -211
- package/package.json +1 -1
- package/parser.js +58 -0
package/README.md
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
# acpreact - ACP SDK
|
|
2
2
|
|
|
3
|
-
A lightweight SDK for
|
|
3
|
+
A lightweight SDK for registering tools and running them via kilo CLI or opencode. Allows kilo and opencode to call registered tools via a custom JSON-RPC 2.0 protocol injected into the prompt.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **ACPProtocol**: Core ACP protocol implementation with JSON-RPC 2.0 support
|
|
8
8
|
- **Tool Registration**: Register custom tools with descriptions and input schemas
|
|
9
9
|
- **Tool Whitelist**: Built-in security model for controlling tool access
|
|
10
|
-
- **Tool Execution**: Execute whitelisted tools with validation
|
|
10
|
+
- **Tool Execution**: Execute whitelisted tools with validation and logging
|
|
11
|
+
- **CLI Integration**: Works with kilo CLI and opencode via `process()` method
|
|
11
12
|
- **ES Module**: Pure ES modules, no build step required
|
|
12
13
|
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
Install kilo CLI and/or opencode before using `process()`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @kilocode/cli # for kilo
|
|
20
|
+
npm install -g opencode-ai # for opencode
|
|
21
|
+
```
|
|
22
|
+
|
|
13
23
|
## Installation
|
|
14
24
|
|
|
15
25
|
```bash
|
|
@@ -18,52 +28,46 @@ npm install acpreact
|
|
|
18
28
|
|
|
19
29
|
## Quick Start
|
|
20
30
|
|
|
21
|
-
###
|
|
31
|
+
### Register Tools and Process with kilo CLI
|
|
22
32
|
|
|
23
33
|
```javascript
|
|
24
34
|
import { ACPProtocol } from 'acpreact';
|
|
25
35
|
|
|
26
|
-
const acp = new ACPProtocol();
|
|
36
|
+
const acp = new ACPProtocol('You are a calculator assistant. Use the add tool when asked to add numbers.');
|
|
27
37
|
|
|
28
|
-
// Register a custom tool
|
|
29
38
|
acp.registerTool(
|
|
30
|
-
'
|
|
31
|
-
'
|
|
39
|
+
'add',
|
|
40
|
+
'Add two numbers together',
|
|
32
41
|
{
|
|
33
42
|
type: 'object',
|
|
34
43
|
properties: {
|
|
35
|
-
|
|
44
|
+
a: { type: 'number', description: 'First number' },
|
|
45
|
+
b: { type: 'number', description: 'Second number' }
|
|
36
46
|
},
|
|
37
|
-
required: ['
|
|
47
|
+
required: ['a', 'b']
|
|
38
48
|
},
|
|
39
|
-
async (params) => {
|
|
40
|
-
return {
|
|
41
|
-
location: params.location,
|
|
42
|
-
temperature: 72,
|
|
43
|
-
condition: 'sunny'
|
|
44
|
-
};
|
|
45
|
-
}
|
|
49
|
+
async (params) => ({ sum: params.a + params.b })
|
|
46
50
|
);
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const result = await acp.process('What is 15 + 27? Use the add tool.', { cli: 'kilo' });
|
|
53
|
+
console.log(result.text); // human-readable text response
|
|
54
|
+
console.log(result.toolCalls); // [{ tool: 'add', result: { sum: 42 } }]
|
|
55
|
+
console.log(result.logs); // tool call audit log
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Using opencode
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
const result = await acp.
|
|
53
|
-
console.log(result);
|
|
60
|
+
```javascript
|
|
61
|
+
const result = await acp.process('What is 15 + 27? Use the add tool.', { cli: 'opencode' });
|
|
54
62
|
```
|
|
55
63
|
|
|
56
64
|
### Using System Instructions
|
|
57
65
|
|
|
58
|
-
Pass a system instruction to the ACPProtocol constructor. The instruction will be included in the initialization response and communicated to opencode or kilo CLI:
|
|
59
|
-
|
|
60
66
|
```javascript
|
|
61
67
|
import { ACPProtocol } from 'acpreact';
|
|
62
68
|
|
|
63
|
-
const
|
|
64
|
-
const acp = new ACPProtocol(instruction);
|
|
69
|
+
const acp = new ACPProtocol('You are a helpful weather assistant. Always provide temperature in Fahrenheit.');
|
|
65
70
|
|
|
66
|
-
// Register tools as usual
|
|
67
71
|
acp.registerTool(
|
|
68
72
|
'weather',
|
|
69
73
|
'Get weather information for a location',
|
|
@@ -74,85 +78,96 @@ acp.registerTool(
|
|
|
74
78
|
},
|
|
75
79
|
required: ['location']
|
|
76
80
|
},
|
|
77
|
-
async (params) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
83
|
-
}
|
|
81
|
+
async (params) => ({
|
|
82
|
+
location: params.location,
|
|
83
|
+
temperature: 72,
|
|
84
|
+
condition: 'sunny'
|
|
85
|
+
})
|
|
84
86
|
);
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
console.log(
|
|
89
|
-
// Output: "You are a helpful weather assistant. Always provide temperature in Fahrenheit."
|
|
88
|
+
const result = await acp.process('What is the weather in San Francisco?', { cli: 'kilo' });
|
|
89
|
+
console.log(result.text); // text response (tool call JSON filtered out)
|
|
90
|
+
console.log(result.toolCalls); // [{ tool: 'weather', result: { location: 'San Francisco', ... } }]
|
|
90
91
|
```
|
|
91
92
|
|
|
92
93
|
## API
|
|
93
94
|
|
|
94
95
|
### ACPProtocol
|
|
95
96
|
|
|
96
|
-
Main class for
|
|
97
|
+
Main class for ACP protocol communication.
|
|
97
98
|
|
|
98
99
|
**Constructor:**
|
|
99
100
|
|
|
100
|
-
- `new ACPProtocol(instruction)`: Initialize the protocol
|
|
101
|
-
- `instruction` (optional): String - system instruction to
|
|
101
|
+
- `new ACPProtocol(instruction?)`: Initialize the protocol
|
|
102
|
+
- `instruction` (optional): String - system instruction prepended to every prompt sent to the CLI
|
|
102
103
|
|
|
103
104
|
**Methods:**
|
|
104
105
|
|
|
105
106
|
- `registerTool(name, description, inputSchema, handler)`: Register a custom tool
|
|
106
107
|
- `name`: String - tool identifier
|
|
107
|
-
- `description`: String - tool description
|
|
108
|
+
- `description`: String - tool description shown to the model
|
|
108
109
|
- `inputSchema`: Object - JSON Schema for tool inputs
|
|
109
110
|
- `handler`: Async function - receives params object, returns result
|
|
110
111
|
- Returns: Tool definition object
|
|
111
112
|
|
|
112
|
-
- `
|
|
113
|
+
- `async process(text, options?)`: Send a prompt to kilo or opencode and execute any tool calls
|
|
114
|
+
- `text`: String - the user prompt
|
|
115
|
+
- `options.cli`: `'kilo'` (default) or `'opencode'`
|
|
116
|
+
- `options.model`: String - model in `provider/model` format (uses CLI default if omitted)
|
|
117
|
+
- Returns: `{ text, rawOutput, toolCalls, logs }` or `{ text, rawOutput, error, logs }` on parse failure
|
|
118
|
+
|
|
119
|
+
- `createInitializeResponse()`: Generate ACP protocol initialization response with registered tools
|
|
120
|
+
|
|
121
|
+
- `createJsonRpcRequest(method, params)`: Create JSON-RPC 2.0 request object
|
|
113
122
|
|
|
114
|
-
- `
|
|
123
|
+
- `createJsonRpcResponse(id, result)`: Create JSON-RPC 2.0 response object
|
|
115
124
|
|
|
116
|
-
- `
|
|
125
|
+
- `createJsonRpcError(id, error)`: Create JSON-RPC 2.0 error object (accepts Error or string)
|
|
117
126
|
|
|
118
|
-
- `
|
|
127
|
+
- `validateToolCall(toolName)`: Check if tool is whitelisted, returns `{ allowed, error? }`
|
|
119
128
|
|
|
120
|
-
- `
|
|
129
|
+
- `async callTool(toolName, params)`: Execute a registered tool directly
|
|
121
130
|
|
|
122
|
-
- `
|
|
131
|
+
- `parseTextOutput(output)`: Parse human-readable text from CLI JSON output (filters tool call JSON)
|
|
132
|
+
|
|
133
|
+
- `parseToolCalls(output)`: Parse JSON-RPC tool calls from CLI output, deduplicated by id+method
|
|
123
134
|
|
|
124
135
|
**Properties:**
|
|
125
136
|
|
|
126
|
-
- `instruction`: String (optional) - system instruction
|
|
137
|
+
- `instruction`: String (optional) - system instruction prepended to prompts
|
|
127
138
|
- `toolWhitelist`: Set of registered tool names
|
|
128
|
-
- `toolCallLog`: Array of executed tool calls with timestamps
|
|
129
|
-
- `rejectedCallLog`: Array of rejected tool attempts
|
|
139
|
+
- `toolCallLog`: Array of executed tool calls with timestamps and results
|
|
140
|
+
- `rejectedCallLog`: Array of rejected tool attempts with reasons
|
|
141
|
+
|
|
142
|
+
## How It Works
|
|
143
|
+
|
|
144
|
+
`process()` injects the registered tool list and JSON-RPC call format into the prompt, invokes the CLI, and parses any JSON-RPC tool calls from the output. Matched tool calls are executed locally and their results returned.
|
|
145
|
+
|
|
146
|
+
The model outputs tool calls as JSON-RPC lines:
|
|
147
|
+
```
|
|
148
|
+
{"jsonrpc":"2.0","id":1,"method":"tools/add","params":{"a":15,"b":27}}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
These are parsed, executed, and returned in `result.toolCalls`. The `text` field contains only human-readable model output with tool call JSON filtered out.
|
|
130
152
|
|
|
131
153
|
## Example: Multiple Tools
|
|
132
154
|
|
|
133
155
|
```javascript
|
|
134
156
|
import { ACPProtocol } from 'acpreact';
|
|
135
157
|
|
|
136
|
-
const acp = new ACPProtocol();
|
|
158
|
+
const acp = new ACPProtocol('You are a data assistant.');
|
|
137
159
|
|
|
138
|
-
// Register database tool
|
|
139
160
|
acp.registerTool(
|
|
140
161
|
'query_database',
|
|
141
162
|
'Query the application database',
|
|
142
163
|
{
|
|
143
164
|
type: 'object',
|
|
144
|
-
properties: {
|
|
145
|
-
query: { type: 'string' }
|
|
146
|
-
},
|
|
165
|
+
properties: { query: { type: 'string' } },
|
|
147
166
|
required: ['query']
|
|
148
167
|
},
|
|
149
|
-
async (params) => {
|
|
150
|
-
// Your database logic here
|
|
151
|
-
return { data: [] };
|
|
152
|
-
}
|
|
168
|
+
async (params) => ({ data: [] })
|
|
153
169
|
);
|
|
154
170
|
|
|
155
|
-
// Register API tool
|
|
156
171
|
acp.registerTool(
|
|
157
172
|
'call_api',
|
|
158
173
|
'Call an external API',
|
|
@@ -164,15 +179,12 @@ acp.registerTool(
|
|
|
164
179
|
},
|
|
165
180
|
required: ['endpoint', 'method']
|
|
166
181
|
},
|
|
167
|
-
async (params) => {
|
|
168
|
-
// Your API logic here
|
|
169
|
-
return { response: {} };
|
|
170
|
-
}
|
|
182
|
+
async (params) => ({ response: {} })
|
|
171
183
|
);
|
|
172
184
|
|
|
173
|
-
// Initialize and use
|
|
174
185
|
const initResponse = acp.createInitializeResponse();
|
|
175
|
-
console.log(initResponse.result.
|
|
186
|
+
console.log(initResponse.result.tools.length); // 2
|
|
187
|
+
console.log(initResponse.result.agentCapabilities); // { toolCalling: true, streaming: false }
|
|
176
188
|
```
|
|
177
189
|
|
|
178
190
|
## License
|
package/core.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
|
+
import { parseTextOutput, parseToolCalls } from './parser.js';
|
|
3
4
|
|
|
4
5
|
class ACPProtocol extends EventEmitter {
|
|
5
6
|
constructor(instruction) {
|
|
@@ -12,25 +13,12 @@ class ACPProtocol extends EventEmitter {
|
|
|
12
13
|
this.toolCallLog = [];
|
|
13
14
|
this.rejectedCallLog = [];
|
|
14
15
|
this.tools = {};
|
|
15
|
-
this.cliProcess = null;
|
|
16
|
-
this.pendingRequests = new Map();
|
|
17
|
-
this.buffer = '';
|
|
18
|
-
this.initialized = false;
|
|
19
|
-
this.sessionId = null;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
generateRequestId() {
|
|
23
19
|
return ++this.messageId;
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
createJsonRpcResponse(id, result) {
|
|
27
|
-
return { jsonrpc: "2.0", id, result };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
createJsonRpcError(id, error) {
|
|
31
|
-
return { jsonrpc: "2.0", id, error: { code: -32603, message: error } };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
22
|
registerTool(name, description, inputSchema, handler) {
|
|
35
23
|
this.toolWhitelist.add(name);
|
|
36
24
|
this.tools[name] = handler;
|
|
@@ -40,252 +28,150 @@ class ACPProtocol extends EventEmitter {
|
|
|
40
28
|
}
|
|
41
29
|
|
|
42
30
|
getToolsList() {
|
|
43
|
-
return Array.from(this.toolWhitelist).map(
|
|
44
|
-
name
|
|
45
|
-
description: this.toolDescriptions[
|
|
46
|
-
inputSchema: this.toolSchemas[
|
|
31
|
+
return Array.from(this.toolWhitelist).map(name => ({
|
|
32
|
+
name,
|
|
33
|
+
description: this.toolDescriptions[name],
|
|
34
|
+
inputSchema: this.toolSchemas[name],
|
|
47
35
|
}));
|
|
48
36
|
}
|
|
49
37
|
|
|
50
38
|
getToolsPrompt() {
|
|
51
39
|
const tools = this.getToolsList();
|
|
52
40
|
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';
|
|
41
|
+
let prompt = '\n\nYou have access to the following tools. You MUST use these tools to interact:\n\n';
|
|
55
42
|
for (const tool of tools) {
|
|
56
43
|
prompt += `## Tool: ${tool.name}\n${tool.description}\n`;
|
|
57
44
|
prompt += `Parameters: ${JSON.stringify(tool.inputSchema, null, 2)}\n`;
|
|
58
|
-
prompt += `To call this tool, output a JSON-RPC request
|
|
59
|
-
prompt += `{"jsonrpc":"2.0","id":<
|
|
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`;
|
|
60
47
|
}
|
|
61
|
-
prompt += 'IMPORTANT:
|
|
48
|
+
prompt += 'IMPORTANT: When you need to use a tool, output ONLY the JSON-RPC request, nothing else.\n';
|
|
62
49
|
return prompt;
|
|
63
50
|
}
|
|
64
51
|
|
|
52
|
+
createInitializeResponse() {
|
|
53
|
+
return {
|
|
54
|
+
jsonrpc: '2.0',
|
|
55
|
+
id: 1,
|
|
56
|
+
result: {
|
|
57
|
+
tools: this.getToolsList(),
|
|
58
|
+
instruction: this.instruction,
|
|
59
|
+
agentCapabilities: { toolCalling: true, streaming: false },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
createJsonRpcRequest(method, params) {
|
|
65
|
+
return { jsonrpc: '2.0', id: this.generateRequestId(), method, params };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
createJsonRpcResponse(id, result) {
|
|
69
|
+
return { jsonrpc: '2.0', id, result };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
createJsonRpcError(id, error) {
|
|
73
|
+
return {
|
|
74
|
+
jsonrpc: '2.0',
|
|
75
|
+
id,
|
|
76
|
+
error: {
|
|
77
|
+
code: error?.code ?? -32000,
|
|
78
|
+
message: error instanceof Error ? error.message : String(error),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
validateToolCall(toolName) {
|
|
66
84
|
if (!this.toolWhitelist.has(toolName)) {
|
|
67
85
|
const availableTools = Array.from(this.toolWhitelist);
|
|
68
|
-
const error = `Tool not available. Available: ${availableTools.join(', ')}`;
|
|
69
86
|
this.rejectedCallLog.push({
|
|
70
87
|
timestamp: new Date().toISOString(),
|
|
71
88
|
attemptedTool: toolName,
|
|
72
89
|
reason: 'Not in whitelist',
|
|
73
90
|
availableTools,
|
|
74
91
|
});
|
|
75
|
-
return { allowed: false, error };
|
|
92
|
+
return { allowed: false, error: `Tool not available. Available: ${availableTools.join(', ')}` };
|
|
76
93
|
}
|
|
77
94
|
return { allowed: true };
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
async callTool(toolName, params) {
|
|
81
98
|
const validation = this.validateToolCall(toolName);
|
|
82
|
-
if (!validation.allowed)
|
|
83
|
-
throw new Error(validation.error);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
this.toolCallLog.push({
|
|
87
|
-
timestamp: new Date().toISOString(),
|
|
88
|
-
toolName,
|
|
89
|
-
params,
|
|
90
|
-
status: 'executing',
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
if (this.tools[toolName]) {
|
|
94
|
-
const result = await this.tools[toolName](params);
|
|
95
|
-
const lastLog = this.toolCallLog[this.toolCallLog.length - 1];
|
|
96
|
-
lastLog.status = 'completed';
|
|
97
|
-
lastLog.result = result;
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
100
|
-
throw new Error(`Unknown tool: ${toolName}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async start(cli = 'kilo') {
|
|
104
|
-
if (this.cliProcess) return this.sessionId;
|
|
105
|
-
|
|
106
|
-
return new Promise((resolve, reject) => {
|
|
107
|
-
this.cliProcess = spawn('script', ['-q', '-c', `${cli} acp`, '/dev/null'], {
|
|
108
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
109
|
-
cwd: process.cwd(),
|
|
110
|
-
env: { ...process.env, TERM: 'dumb' },
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
this.cliProcess.stdout.on('data', (data) => {
|
|
114
|
-
this.buffer += data.toString();
|
|
115
|
-
this.processBuffer();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
this.cliProcess.stderr.on('data', (data) => {
|
|
119
|
-
this.emit('stderr', data.toString());
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
this.cliProcess.on('error', (error) => {
|
|
123
|
-
this.emit('error', error);
|
|
124
|
-
reject(error);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
this.cliProcess.on('close', (code) => {
|
|
128
|
-
this.emit('close', code);
|
|
129
|
-
this.cliProcess = null;
|
|
130
|
-
this.initialized = false;
|
|
131
|
-
this.sessionId = null;
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const timeout = setTimeout(() => {
|
|
135
|
-
reject(new Error('ACP initialization timeout'));
|
|
136
|
-
}, 30000);
|
|
99
|
+
if (!validation.allowed) throw new Error(validation.error);
|
|
137
100
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
resolve(this.sessionId);
|
|
141
|
-
});
|
|
101
|
+
const entry = { timestamp: new Date().toISOString(), toolName, params, status: 'executing' };
|
|
102
|
+
this.toolCallLog.push(entry);
|
|
142
103
|
|
|
143
|
-
|
|
144
|
-
|
|
104
|
+
if (!this.tools[toolName]) throw new Error(`Unknown tool: ${toolName}`);
|
|
105
|
+
const result = await this.tools[toolName](params);
|
|
106
|
+
entry.status = 'completed';
|
|
107
|
+
entry.result = result;
|
|
108
|
+
return result;
|
|
145
109
|
}
|
|
146
110
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.buffer = lines.pop() || '';
|
|
150
|
-
|
|
151
|
-
for (const line of lines) {
|
|
152
|
-
const trimmed = line.trim();
|
|
153
|
-
if (!trimmed) continue;
|
|
154
|
-
this.handleMessage(trimmed);
|
|
155
|
-
}
|
|
111
|
+
parseTextOutput(output) {
|
|
112
|
+
return parseTextOutput(output);
|
|
156
113
|
}
|
|
157
114
|
|
|
158
|
-
|
|
159
|
-
|
|
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);
|
|
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);
|
|
190
|
-
}
|
|
115
|
+
parseToolCalls(output) {
|
|
116
|
+
return parseToolCalls(output);
|
|
191
117
|
}
|
|
192
118
|
|
|
193
|
-
async
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.
|
|
199
|
-
}
|
|
200
|
-
}
|
|
119
|
+
async process(text, options = {}) {
|
|
120
|
+
const cli = options.cli || 'kilo';
|
|
121
|
+
const model = options.model;
|
|
122
|
+
const fullPrompt = this.instruction
|
|
123
|
+
? `${this.instruction}${this.getToolsPrompt()}\n\n---\n\n${text}`
|
|
124
|
+
: `${this.getToolsPrompt()}\n\n---\n\n${text}`;
|
|
201
125
|
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
126
|
+
const args = ['run', '--format', 'json'];
|
|
127
|
+
if (cli === 'kilo') args.push('--auto');
|
|
128
|
+
if (model) args.push('--model', model);
|
|
129
|
+
args.push(fullPrompt);
|
|
207
130
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
}
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
let output = '';
|
|
133
|
+
let errorOutput = '';
|
|
220
134
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
jsonrpc: "2.0",
|
|
224
|
-
id: 1,
|
|
225
|
-
method: "session/new",
|
|
226
|
-
params: {
|
|
135
|
+
const child = spawn(cli, args, {
|
|
136
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
227
137
|
cwd: process.cwd(),
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
}
|
|
138
|
+
env: { ...process.env },
|
|
139
|
+
});
|
|
232
140
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
141
|
+
child.stdin.end();
|
|
142
|
+
child.stdout.on('data', (data) => { output += data.toString(); });
|
|
143
|
+
child.stderr.on('data', (data) => { errorOutput += data.toString(); });
|
|
237
144
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
145
|
+
child.on('close', async (code) => {
|
|
146
|
+
if (code !== 0 && code !== null && !output) {
|
|
147
|
+
reject(new Error(`CLI exited with code ${code}: ${errorOutput}`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const toolCalls = parseToolCalls(output);
|
|
152
|
+
const results = [];
|
|
153
|
+
for (const call of toolCalls) {
|
|
154
|
+
const toolName = call.method.replace('tools/', '');
|
|
155
|
+
if (this.toolWhitelist.has(toolName)) {
|
|
156
|
+
const result = await this.callTool(toolName, call.params);
|
|
157
|
+
results.push({ tool: toolName, result });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
resolve({ text: parseTextOutput(output), rawOutput: output, toolCalls: results, logs: this.toolCallLog });
|
|
161
|
+
} catch (e) {
|
|
162
|
+
resolve({ text: parseTextOutput(output), rawOutput: output, error: e.message, logs: this.toolCallLog });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
242
165
|
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
},
|
|
166
|
+
child.on('error', (error) => {
|
|
167
|
+
reject(new Error(`Failed to spawn ${cli}: ${error.message}`));
|
|
253
168
|
});
|
|
254
169
|
|
|
255
|
-
setTimeout(() => {
|
|
256
|
-
if (this.pendingRequests.has(reqId)) {
|
|
257
|
-
this.pendingRequests.delete(reqId);
|
|
258
|
-
resolve({ timeout: true });
|
|
259
|
-
}
|
|
260
|
-
}, 120000);
|
|
170
|
+
setTimeout(() => { child.kill(); reject(new Error('Timeout')); }, 120000);
|
|
261
171
|
});
|
|
262
172
|
}
|
|
263
173
|
|
|
264
|
-
|
|
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;
|
|
288
|
-
}
|
|
174
|
+
stop() {}
|
|
289
175
|
}
|
|
290
176
|
|
|
291
177
|
export { ACPProtocol };
|
package/package.json
CHANGED
package/parser.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function parseTextOutput(output) {
|
|
2
|
+
let text = '';
|
|
3
|
+
for (const line of output.split('\n')) {
|
|
4
|
+
const trimmed = line.trim();
|
|
5
|
+
if (!trimmed) continue;
|
|
6
|
+
try {
|
|
7
|
+
const json = JSON.parse(trimmed);
|
|
8
|
+
if (json.type === 'text' && json.part?.text) {
|
|
9
|
+
const partText = json.part.text;
|
|
10
|
+
try {
|
|
11
|
+
const inner = JSON.parse(partText.trim());
|
|
12
|
+
if (inner.jsonrpc === '2.0' && inner.method?.startsWith('tools/')) continue;
|
|
13
|
+
} catch {}
|
|
14
|
+
text += partText;
|
|
15
|
+
}
|
|
16
|
+
} catch {}
|
|
17
|
+
}
|
|
18
|
+
return text;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseToolCalls(output) {
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const calls = [];
|
|
24
|
+
|
|
25
|
+
const tryAdd = (candidate) => {
|
|
26
|
+
const trimmed = candidate.trim();
|
|
27
|
+
if (!trimmed) return;
|
|
28
|
+
try {
|
|
29
|
+
const json = JSON.parse(trimmed);
|
|
30
|
+
if (json.jsonrpc === '2.0' && json.method?.startsWith('tools/') && json.params) {
|
|
31
|
+
const key = `${json.id}:${json.method}`;
|
|
32
|
+
if (!seen.has(key)) {
|
|
33
|
+
seen.add(key);
|
|
34
|
+
calls.push({ id: json.id, method: json.method, params: json.params });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
for (const line of output.split('\n')) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (!trimmed) continue;
|
|
43
|
+
try {
|
|
44
|
+
const json = JSON.parse(trimmed);
|
|
45
|
+
if (json.type === 'text' && json.part?.text) {
|
|
46
|
+
for (const inner of json.part.text.split('\n')) tryAdd(inner);
|
|
47
|
+
} else {
|
|
48
|
+
tryAdd(trimmed);
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
tryAdd(trimmed);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return calls;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { parseTextOutput, parseToolCalls };
|