
Security News
The Hidden Blast Radius of the Axios Compromise
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.
A lightweight SDK for registering tools and running them via kilo CLI, opencode, or gemini. Supports multi-service fallback with automatic rate-limit detection, per-provider cooldowns, and transparent failover across a service stack.
process() methodInstall kilo CLI and/or opencode before using process():
npm install -g @kilocode/cli # for kilo
npm install -g opencode-ai # for opencode
npm install acpreact
import { ACPProtocol } from 'acpreact';
const acp = new ACPProtocol('You are a calculator assistant. Use the add tool when asked to add numbers.');
acp.registerTool(
'add',
'Add two numbers together',
{
type: 'object',
properties: {
a: { type: 'number', description: 'First number' },
b: { type: 'number', description: 'Second number' }
},
required: ['a', 'b']
},
async (params) => ({ sum: params.a + params.b })
);
const result = await acp.process('What is 15 + 27? Use the add tool.', { cli: 'kilo' });
console.log(result.text); // human-readable text response
console.log(result.toolCalls); // [{ tool: 'add', result: { sum: 42 } }]
console.log(result.logs); // tool call audit log
const result = await acp.process('What is 15 + 27? Use the add tool.', { cli: 'opencode' });
Pass a services array to the constructor to enable automatic fallback across providers. On rate limits, the engine marks the current service unavailable and continues to the next:
import { ACPProtocol } from 'acpreact';
const acp = new ACPProtocol('You are a calculator.', [
{ cli: 'kilo' },
{ cli: 'opencode' },
{ cli: 'gemini' },
]);
acp.registerTool('add', 'Add two numbers', { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } }, required: ['a', 'b'] }, async ({ a, b }) => ({ sum: a + b }));
const result = await acp.process('What is 15 + 27? Use the add tool.');
Override services per call:
const result = await acp.process('prompt', {
services: [{ cli: 'opencode' }, { cli: 'kilo' }],
});
Multiple profiles per provider (useful for separate API key accounts):
const acp = new ACPProtocol('instruction', [
{ cli: 'kilo', profile: 'account-a' },
{ cli: 'kilo', profile: 'account-b' },
{ cli: 'opencode' },
]);
Build a typed service stack for use with FallbackEngine directly:
import { createServiceStack, FallbackEngine } from 'acpreact';
const stack = createServiceStack([
{ cli: 'kilo', profile: 'work' },
{ cli: 'opencode' },
{ cli: 'gemini' },
]);
const engine = new FallbackEngine(stack);
ACPProtocol (and FallbackEngine) emit events during fallback:
acp.on('rate-limited', ({ name, profileId, cooldownMs }) => {
console.log(`${name}(${profileId}) rate-limited for ${cooldownMs}ms`);
});
acp.on('fallback', ({ from, to }) => {
console.log(`Falling back from ${from.name} to ${to.name}`);
});
acp.on('success', ({ name, profileId, attempted }) => {
console.log(`Succeeded on ${name}(${profileId}) after ${attempted} attempt(s)`);
});
import { ACPProtocol } from 'acpreact';
const acp = new ACPProtocol('You are a helpful weather assistant. Always provide temperature in Fahrenheit.');
acp.registerTool(
'weather',
'Get weather information for a location',
{
type: 'object',
properties: {
location: { type: 'string', description: 'City name' }
},
required: ['location']
},
async (params) => ({
location: params.location,
temperature: 72,
condition: 'sunny'
})
);
const result = await acp.process('What is the weather in San Francisco?', { cli: 'kilo' });
console.log(result.text); // text response (tool call JSON filtered out)
console.log(result.toolCalls); // [{ tool: 'weather', result: { location: 'San Francisco', ... } }]
Main class for ACP protocol communication.
Constructor:
new ACPProtocol(instruction?, services?): Initialize the protocol
instruction (optional): String - system instruction prepended to every prompt sent to the CLIservices (optional): Array of { cli, profile?, model? } - instance-level default service stack for multi-service fallbackMethods:
registerTool(name, description, inputSchema, handler): Register a custom tool
name: String - tool identifierdescription: String - tool description shown to the modelinputSchema: Object - JSON Schema for tool inputshandler: Async function - receives params object, returns resultasync process(text, options?): Send a prompt to a CLI and execute any tool calls
text: String - the user promptoptions.cli: 'kilo' (default), 'opencode', or 'gemini' — used when options.services not setoptions.services: Array of { cli, profile?, model? } — enables multi-service fallback for this call; takes precedence over options.clioptions.model: String - model in provider/model format (uses CLI default if omitted)options.timeout: Number - per-attempt timeout in ms (default 120000){ text, rawOutput, toolCalls, logs } or { text, rawOutput, error, logs } on parse failureAggregateError when all services in the stack are exhaustedcreateInitializeResponse(): Generate ACP protocol initialization response with registered tools
createJsonRpcRequest(method, params): Create JSON-RPC 2.0 request object
createJsonRpcResponse(id, result): Create JSON-RPC 2.0 response object
createJsonRpcError(id, error): Create JSON-RPC 2.0 error object (accepts Error or string)
validateToolCall(toolName): Check if tool is whitelisted, returns { allowed, error? }
async callTool(toolName, params): Execute a registered tool directly
parseTextOutput(output): Parse human-readable text from CLI JSON output (filters tool call JSON)
parseToolCalls(output): Parse JSON-RPC tool calls from CLI output, deduplicated by id+method
Properties:
instruction: String (optional) - system instruction prepended to promptstoolWhitelist: Set of registered tool namestoolCallLog: Array of executed tool calls with timestamps and resultsrejectedCallLog: Array of rejected tool attempts with reasonsprocess() 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.
The model outputs tool calls as JSON-RPC lines:
{"jsonrpc":"2.0","id":1,"method":"tools/add","params":{"a":15,"b":27}}
These are parsed, executed, and returned in result.toolCalls. The text field contains only human-readable model output with tool call JSON filtered out.
import { ACPProtocol } from 'acpreact';
const acp = new ACPProtocol('You are a data assistant.');
acp.registerTool(
'query_database',
'Query the application database',
{
type: 'object',
properties: { query: { type: 'string' } },
required: ['query']
},
async (params) => ({ data: [] })
);
acp.registerTool(
'call_api',
'Call an external API',
{
type: 'object',
properties: {
endpoint: { type: 'string' },
method: { type: 'string', enum: ['GET', 'POST'] }
},
required: ['endpoint', 'method']
},
async (params) => ({ response: {} })
);
const initResponse = acp.createInitializeResponse();
console.log(initResponse.result.tools.length); // 2
console.log(initResponse.result.agentCapabilities); // { toolCalling: true, streaming: false }
ISC
FAQs
ACP SDK for monitoring and reacting to chat conversations
We found that acpreact demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.