Skip to content

Your first conversation

For a second turn, streaming, or permission handling, open a Command and keep it.

import { Claude, Command, AssistantEvent } from 'activecli';
const command = Command.open(Claude, { workingDir: '/proj' });
command.subscribe((event) => {
if (event instanceof AssistantEvent) process.stdout.write(event.text);
});
command.onPermission((request) => request.approve());
await command.setMessage('Find the bug').send();
await command.setMessage('Now fix it').send();
command.close();

Four things happen there that are worth naming.

Call What it does
Command.open(Claude, …) Mints a session id and describes the intent. No process starts.
subscribe / onPermission Registered before anything runs, so the first event cannot be missed.
setMessage(…) Accumulates. Returns this, so it chains.
send() Spawns if needed, then writes the turn. The only point at which anything reaches the CLI.

The second send() reuses the running process. A new one is spawned only when something changed that a running CLI cannot be talked out of — a different provider, or a permission mode, which is a spawn-time flag.

import { Claude, Command, AssistantEvent, ResultEvent } from 'activecli';
const command = Command.open(Claude, {
workingDir: process.cwd(),
permissions: 'ask-before-edit',
model: 'opus',
});
command.subscribe((event) => {
if (event instanceof AssistantEvent && event.hasText()) {
process.stdout.write(event.text);
}
if (event instanceof ResultEvent && event.raw['subtype'] !== 'success') {
console.error('turn did not finish cleanly:', command.session?.diagnostics);
}
});
command.onPermission((request) =>
request.subtype === 'can_use_tool' ? request.approve() : request.deny('not allowed here'),
);
await command.setMessage('Summarise this repository').send();
await command.setAttachment('src/index.ts').setMessage('Now explain this file').send();
const sessionId = command.sessionId; // resume later with Command.resume(sessionId, Claude)
command.close();