mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 08:03:46 +00:00
docs: add mcp and skill session config (#106)
This commit is contained in:
parent
d236edf35c
commit
4c8d93e077
95 changed files with 10014 additions and 1342 deletions
5
examples/shared/Dockerfile
Normal file
5
examples/shared/Dockerfile
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
FROM node:22-bookworm-slim
|
||||
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates > /dev/null 2>&1 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
npm install -g --silent @sandbox-agent/cli@latest && \
|
||||
sandbox-agent install-agent claude
|
||||
58
examples/shared/Dockerfile.dev
Normal file
58
examples/shared/Dockerfile.dev
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
FROM node:22-bookworm-slim AS frontend
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
WORKDIR /build
|
||||
|
||||
# Copy workspace root config
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
|
||||
# Copy packages needed for the inspector build chain:
|
||||
# inspector -> sandbox-agent SDK -> cli-shared
|
||||
COPY sdks/typescript/ sdks/typescript/
|
||||
COPY sdks/cli-shared/ sdks/cli-shared/
|
||||
COPY frontend/packages/inspector/ frontend/packages/inspector/
|
||||
COPY docs/openapi.json docs/
|
||||
|
||||
# Create stub package.json for workspace packages referenced in pnpm-workspace.yaml
|
||||
# but not needed for the inspector build (avoids install errors).
|
||||
RUN set -e; for dir in \
|
||||
sdks/cli sdks/gigacode \
|
||||
resources/agent-schemas resources/vercel-ai-sdk-schemas \
|
||||
scripts/release scripts/sandbox-testing \
|
||||
examples/shared examples/docker examples/e2b examples/vercel \
|
||||
examples/daytona examples/cloudflare examples/file-system \
|
||||
examples/mcp examples/mcp-custom-tool \
|
||||
examples/skills examples/skills-custom-tool \
|
||||
frontend/packages/website; do \
|
||||
mkdir -p "$dir"; \
|
||||
printf '{"name":"@stub/%s","private":true,"version":"0.0.0"}\n' "$(basename "$dir")" > "$dir/package.json"; \
|
||||
done; \
|
||||
for parent in sdks/cli/platforms sdks/gigacode/platforms; do \
|
||||
for plat in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-x64; do \
|
||||
mkdir -p "$parent/$plat"; \
|
||||
printf '{"name":"@stub/%s-%s","private":true,"version":"0.0.0"}\n' "$(basename "$parent")" "$plat" > "$parent/$plat/package.json"; \
|
||||
done; \
|
||||
done
|
||||
|
||||
RUN pnpm install --no-frozen-lockfile
|
||||
ENV SKIP_OPENAPI_GEN=1
|
||||
RUN pnpm --filter sandbox-agent build && \
|
||||
pnpm --filter @sandbox-agent/inspector build
|
||||
|
||||
FROM rust:1.88.0-bookworm AS builder
|
||||
WORKDIR /build
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY server/ ./server/
|
||||
COPY gigacode/ ./gigacode/
|
||||
COPY resources/agent-schemas/artifacts/ ./resources/agent-schemas/artifacts/
|
||||
COPY --from=frontend /build/frontend/packages/inspector/dist/ ./frontend/packages/inspector/dist/
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/build/target \
|
||||
cargo build -p sandbox-agent --release && \
|
||||
cp target/release/sandbox-agent /sandbox-agent
|
||||
|
||||
FROM node:22-bookworm-slim
|
||||
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates > /dev/null 2>&1 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /sandbox-agent /usr/local/bin/sandbox-agent
|
||||
RUN sandbox-agent install-agent claude
|
||||
|
|
@ -3,15 +3,18 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/sandbox-agent-client.ts"
|
||||
".": "./src/sandbox-agent-client.ts",
|
||||
"./docker": "./src/docker.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"dockerode": "latest",
|
||||
"sandbox-agent": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dockerode": "latest",
|
||||
"@types/node": "latest",
|
||||
"typescript": "latest"
|
||||
}
|
||||
|
|
|
|||
301
examples/shared/src/docker.ts
Normal file
301
examples/shared/src/docker.ts
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
import Docker from "dockerode";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { waitForHealth } from "./sandbox-agent-client.ts";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const EXAMPLE_IMAGE = "sandbox-agent-examples:latest";
|
||||
const EXAMPLE_IMAGE_DEV = "sandbox-agent-examples-dev:latest";
|
||||
const DOCKERFILE_DIR = path.resolve(__dirname, "..");
|
||||
const REPO_ROOT = path.resolve(DOCKERFILE_DIR, "../..");
|
||||
|
||||
export interface DockerSandboxOptions {
|
||||
/** Container port used by sandbox-agent inside Docker. */
|
||||
port: number;
|
||||
/** Optional fixed host port mapping. If omitted, Docker assigns a free host port automatically. */
|
||||
hostPort?: number;
|
||||
/** Additional shell commands to run before starting sandbox-agent. */
|
||||
setupCommands?: string[];
|
||||
/** Docker image to use. Defaults to the pre-built sandbox-agent-examples image. */
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface DockerSandbox {
|
||||
baseUrl: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}
|
||||
|
||||
const DIRECT_CREDENTIAL_KEYS = [
|
||||
"ANTHROPIC_API_KEY",
|
||||
"CLAUDE_API_KEY",
|
||||
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"OPENAI_API_KEY",
|
||||
"CODEX_API_KEY",
|
||||
"CEREBRAS_API_KEY",
|
||||
"OPENCODE_API_KEY",
|
||||
] as const;
|
||||
|
||||
function stripShellQuotes(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
||||
return trimmed.slice(1, -1);
|
||||
}
|
||||
if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
||||
return trimmed.slice(1, -1);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function parseExtractedCredentials(output: string): Record<string, string> {
|
||||
const parsed: Record<string, string> = {};
|
||||
for (const rawLine of output.split("\n")) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
const cleanLine = line.startsWith("export ") ? line.slice(7) : line;
|
||||
const match = cleanLine.match(/^([A-Z0-9_]+)=(.*)$/);
|
||||
if (!match) continue;
|
||||
const [, key, rawValue] = match;
|
||||
const value = stripShellQuotes(rawValue);
|
||||
if (!value) continue;
|
||||
parsed[key] = value;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
interface ClaudeCredentialFile {
|
||||
hostPath: string;
|
||||
containerPath: string;
|
||||
base64Content: string;
|
||||
}
|
||||
|
||||
function readClaudeCredentialFiles(): ClaudeCredentialFile[] {
|
||||
const homeDir = process.env.HOME || "";
|
||||
if (!homeDir) return [];
|
||||
|
||||
const candidates: Array<{ hostPath: string; containerPath: string }> = [
|
||||
{
|
||||
hostPath: path.join(homeDir, ".claude", ".credentials.json"),
|
||||
containerPath: "/root/.claude/.credentials.json",
|
||||
},
|
||||
{
|
||||
hostPath: path.join(homeDir, ".claude-oauth-credentials.json"),
|
||||
containerPath: "/root/.claude-oauth-credentials.json",
|
||||
},
|
||||
];
|
||||
|
||||
const files: ClaudeCredentialFile[] = [];
|
||||
for (const candidate of candidates) {
|
||||
if (!fs.existsSync(candidate.hostPath)) continue;
|
||||
try {
|
||||
const raw = fs.readFileSync(candidate.hostPath, "utf8");
|
||||
files.push({
|
||||
hostPath: candidate.hostPath,
|
||||
containerPath: candidate.containerPath,
|
||||
base64Content: Buffer.from(raw, "utf8").toString("base64"),
|
||||
});
|
||||
} catch {
|
||||
// Ignore unreadable credential file candidates.
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function collectCredentialEnv(): Record<string, string> {
|
||||
const merged: Record<string, string> = {};
|
||||
let extracted: Record<string, string> = {};
|
||||
try {
|
||||
const output = execFileSync(
|
||||
"sandbox-agent",
|
||||
["credentials", "extract-env"],
|
||||
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
extracted = parseExtractedCredentials(output);
|
||||
} catch {
|
||||
// Fall back to direct env vars if extraction is unavailable.
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(extracted)) {
|
||||
if (value) merged[key] = value;
|
||||
}
|
||||
for (const key of DIRECT_CREDENTIAL_KEYS) {
|
||||
const direct = process.env[key];
|
||||
if (direct) merged[key] = direct;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function shellSingleQuotedLiteral(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
||||
}
|
||||
|
||||
function stripAnsi(value: string): string {
|
||||
return value.replace(
|
||||
/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureExampleImage(_docker: Docker): Promise<string> {
|
||||
const dev = !!process.env.SANDBOX_AGENT_DEV;
|
||||
const imageName = dev ? EXAMPLE_IMAGE_DEV : EXAMPLE_IMAGE;
|
||||
|
||||
if (dev) {
|
||||
console.log(" Building sandbox image from source (may take a while, only runs once)...");
|
||||
try {
|
||||
execFileSync("docker", [
|
||||
"build", "-t", imageName,
|
||||
"-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"),
|
||||
REPO_ROOT,
|
||||
], {
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : "";
|
||||
throw new Error(`Failed to build sandbox image: ${stderr}`);
|
||||
}
|
||||
} else {
|
||||
console.log(" Building sandbox image (may take a while, only runs once)...");
|
||||
try {
|
||||
execFileSync("docker", ["build", "-t", imageName, DOCKERFILE_DIR], {
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : "";
|
||||
throw new Error(`Failed to build sandbox image: ${stderr}`);
|
||||
}
|
||||
}
|
||||
|
||||
return imageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a Docker container running sandbox-agent and wait for it to be healthy.
|
||||
* Registers SIGINT/SIGTERM handlers for cleanup.
|
||||
*/
|
||||
export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<DockerSandbox> {
|
||||
const { port, hostPort } = opts;
|
||||
const useCustomImage = !!opts.image;
|
||||
let image = opts.image ?? EXAMPLE_IMAGE;
|
||||
// TODO: Replace setupCommands shell bootstrapping with native sandbox-agent exec API once available.
|
||||
const setupCommands = [...(opts.setupCommands ?? [])];
|
||||
const credentialEnv = collectCredentialEnv();
|
||||
const claudeCredentialFiles = readClaudeCredentialFiles();
|
||||
const bootstrapEnv: Record<string, string> = {};
|
||||
|
||||
if (claudeCredentialFiles.length > 0) {
|
||||
delete credentialEnv.ANTHROPIC_API_KEY;
|
||||
delete credentialEnv.CLAUDE_API_KEY;
|
||||
delete credentialEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
delete credentialEnv.ANTHROPIC_AUTH_TOKEN;
|
||||
|
||||
const credentialBootstrapCommands = claudeCredentialFiles.flatMap((file, index) => {
|
||||
const envKey = `SANDBOX_AGENT_CLAUDE_CREDENTIAL_${index}_B64`;
|
||||
bootstrapEnv[envKey] = file.base64Content;
|
||||
return [
|
||||
`mkdir -p ${shellSingleQuotedLiteral(path.posix.dirname(file.containerPath))}`,
|
||||
`printf %s "$${envKey}" | base64 -d > ${shellSingleQuotedLiteral(file.containerPath)}`,
|
||||
];
|
||||
});
|
||||
setupCommands.unshift(...credentialBootstrapCommands);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(credentialEnv)) {
|
||||
if (!process.env[key]) process.env[key] = value;
|
||||
}
|
||||
|
||||
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
||||
|
||||
if (useCustomImage) {
|
||||
try {
|
||||
await docker.getImage(image).inspect();
|
||||
} catch {
|
||||
console.log(` Pulling ${image}...`);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => {
|
||||
if (err) return reject(err);
|
||||
docker.modem.followProgress(stream, (err: Error | null) => (err ? reject(err) : resolve()));
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
image = await ensureExampleImage(docker);
|
||||
}
|
||||
|
||||
const bootCommands = [
|
||||
...setupCommands,
|
||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`,
|
||||
];
|
||||
|
||||
const container = await docker.createContainer({
|
||||
Image: image,
|
||||
WorkingDir: "/root",
|
||||
Cmd: ["sh", "-c", bootCommands.join(" && ")],
|
||||
Env: [
|
||||
...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`),
|
||||
...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`),
|
||||
],
|
||||
ExposedPorts: { [`${port}/tcp`]: {} },
|
||||
HostConfig: {
|
||||
AutoRemove: true,
|
||||
PortBindings: { [`${port}/tcp`]: [{ HostPort: hostPort ? `${hostPort}` : "0" }] },
|
||||
},
|
||||
});
|
||||
await container.start();
|
||||
|
||||
const logChunks: string[] = [];
|
||||
const startupLogs = await container.logs({
|
||||
follow: true,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
since: 0,
|
||||
}) as NodeJS.ReadableStream;
|
||||
const stdoutStream = new PassThrough();
|
||||
const stderrStream = new PassThrough();
|
||||
stdoutStream.on("data", (chunk) => {
|
||||
logChunks.push(stripAnsi(String(chunk)));
|
||||
});
|
||||
stderrStream.on("data", (chunk) => {
|
||||
logChunks.push(stripAnsi(String(chunk)));
|
||||
});
|
||||
docker.modem.demuxStream(startupLogs, stdoutStream, stderrStream);
|
||||
const stopStartupLogs = () => {
|
||||
const stream = startupLogs as NodeJS.ReadableStream & { destroy?: () => void };
|
||||
try { stream.destroy?.(); } catch {}
|
||||
};
|
||||
|
||||
const inspect = await container.inspect();
|
||||
const mappedPorts = inspect.NetworkSettings?.Ports?.[`${port}/tcp`];
|
||||
const mappedHostPort = mappedPorts?.[0]?.HostPort;
|
||||
if (!mappedHostPort) {
|
||||
throw new Error(`Failed to resolve mapped host port for container port ${port}`);
|
||||
}
|
||||
const baseUrl = `http://127.0.0.1:${mappedHostPort}`;
|
||||
|
||||
try {
|
||||
await waitForHealth({ baseUrl });
|
||||
} catch (err) {
|
||||
stopStartupLogs();
|
||||
console.error(" Container logs:");
|
||||
for (const chunk of logChunks) {
|
||||
process.stderr.write(` ${chunk}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
stopStartupLogs();
|
||||
console.log(` Ready (${baseUrl})`);
|
||||
|
||||
const cleanup = async () => {
|
||||
stopStartupLogs();
|
||||
try { await container.stop({ t: 5 }); } catch {}
|
||||
try { await container.remove({ force: true }); } catch {}
|
||||
process.exit(0);
|
||||
};
|
||||
process.once("SIGINT", cleanup);
|
||||
process.once("SIGTERM", cleanup);
|
||||
|
||||
return { baseUrl, cleanup };
|
||||
}
|
||||
|
|
@ -3,11 +3,7 @@
|
|||
* Provides minimal helpers for connecting to and interacting with sandbox-agent servers.
|
||||
*/
|
||||
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import type { PermissionEventData, QuestionEventData } from "sandbox-agent";
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.replace(/\/+$/, "");
|
||||
|
|
@ -27,10 +23,12 @@ export function buildInspectorUrl({
|
|||
baseUrl,
|
||||
token,
|
||||
headers,
|
||||
sessionId,
|
||||
}: {
|
||||
baseUrl: string;
|
||||
token?: string;
|
||||
headers?: Record<string, string>;
|
||||
sessionId?: string;
|
||||
}): string {
|
||||
const normalized = normalizeBaseUrl(ensureUrl(baseUrl));
|
||||
const params = new URLSearchParams();
|
||||
|
|
@ -41,7 +39,8 @@ export function buildInspectorUrl({
|
|||
params.set("headers", JSON.stringify(headers));
|
||||
}
|
||||
const queryString = params.toString();
|
||||
return `${normalized}/ui/${queryString ? `?${queryString}` : ""}`;
|
||||
const sessionPath = sessionId ? `sessions/${sessionId}` : "";
|
||||
return `${normalized}/ui/${sessionPath}${queryString ? `?${queryString}` : ""}`;
|
||||
}
|
||||
|
||||
export function logInspectorUrl({
|
||||
|
|
@ -110,125 +109,39 @@ export async function waitForHealth({
|
|||
throw (lastError ?? new Error("Timed out waiting for /v1/health")) as Error;
|
||||
}
|
||||
|
||||
function detectAgent(): string {
|
||||
export function generateSessionId(): string {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let id = "session-";
|
||||
for (let i = 0; i < 8; i++) {
|
||||
id += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function detectAgent(): string {
|
||||
if (process.env.SANDBOX_AGENT) return process.env.SANDBOX_AGENT;
|
||||
if (process.env.ANTHROPIC_API_KEY) return "claude";
|
||||
if (process.env.OPENAI_API_KEY) return "codex";
|
||||
const hasClaude = Boolean(
|
||||
process.env.ANTHROPIC_API_KEY ||
|
||||
process.env.CLAUDE_API_KEY ||
|
||||
process.env.CLAUDE_CODE_OAUTH_TOKEN ||
|
||||
process.env.ANTHROPIC_AUTH_TOKEN,
|
||||
);
|
||||
const openAiLikeKey = process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || "";
|
||||
const hasCodexApiKey = openAiLikeKey.startsWith("sk-");
|
||||
if (hasCodexApiKey && hasClaude) {
|
||||
console.log("Both Claude and Codex API keys detected; defaulting to codex. Set SANDBOX_AGENT to override.");
|
||||
return "codex";
|
||||
}
|
||||
if (!hasCodexApiKey && openAiLikeKey) {
|
||||
console.log("OpenAI/Codex credential is not an API key (expected sk-...), skipping codex auto-select.");
|
||||
}
|
||||
if (hasCodexApiKey) return "codex";
|
||||
if (hasClaude) {
|
||||
if (openAiLikeKey && !hasCodexApiKey) {
|
||||
console.log("Using claude by default.");
|
||||
}
|
||||
return "claude";
|
||||
}
|
||||
return "claude";
|
||||
}
|
||||
|
||||
export async function runPrompt(baseUrl: string): Promise<void> {
|
||||
console.log(`UI: ${buildInspectorUrl({ baseUrl })}`);
|
||||
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
|
||||
const agent = detectAgent();
|
||||
console.log(`Using agent: ${agent}`);
|
||||
const sessionId = randomUUID();
|
||||
await client.createSession(sessionId, { agent });
|
||||
console.log(`Session ${sessionId}. Press Ctrl+C to quit.`);
|
||||
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
let isThinking = false;
|
||||
let hasStartedOutput = false;
|
||||
let turnResolve: (() => void) | null = null;
|
||||
let sessionEnded = false;
|
||||
|
||||
const processEvents = async () => {
|
||||
for await (const event of client.streamEvents(sessionId)) {
|
||||
if (event.type === "item.started") {
|
||||
const item = (event.data as any)?.item;
|
||||
if (item?.role === "assistant") {
|
||||
isThinking = true;
|
||||
hasStartedOutput = false;
|
||||
process.stdout.write("Thinking...");
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "item.delta" && isThinking) {
|
||||
const delta = (event.data as any)?.delta;
|
||||
if (delta) {
|
||||
if (!hasStartedOutput) {
|
||||
process.stdout.write("\r\x1b[K");
|
||||
hasStartedOutput = true;
|
||||
}
|
||||
const text = typeof delta === "string" ? delta : delta.type === "text" ? delta.text || "" : "";
|
||||
if (text) process.stdout.write(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "item.completed") {
|
||||
const item = (event.data as any)?.item;
|
||||
if (item?.role === "assistant") {
|
||||
isThinking = false;
|
||||
process.stdout.write("\n");
|
||||
turnResolve?.();
|
||||
turnResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "permission.requested") {
|
||||
const data = event.data as PermissionEventData;
|
||||
if (isThinking && !hasStartedOutput) {
|
||||
process.stdout.write("\r\x1b[K");
|
||||
}
|
||||
console.log(`[Auto-approved] ${data.action}`);
|
||||
await client.replyPermission(sessionId, data.permission_id, { reply: "once" });
|
||||
}
|
||||
|
||||
if (event.type === "question.requested") {
|
||||
const data = event.data as QuestionEventData;
|
||||
if (isThinking && !hasStartedOutput) {
|
||||
process.stdout.write("\r\x1b[K");
|
||||
}
|
||||
console.log(`[Question rejected] ${data.prompt}`);
|
||||
await client.rejectQuestion(sessionId, data.question_id);
|
||||
}
|
||||
|
||||
if (event.type === "error") {
|
||||
const data = event.data as any;
|
||||
console.error(`\nError: ${data?.message || JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
if (event.type === "session.ended") {
|
||||
const data = event.data as any;
|
||||
const reason = data?.reason || "unknown";
|
||||
if (reason === "error") {
|
||||
console.error(`\nAgent exited with error: ${data?.message || ""}`);
|
||||
if (data?.exit_code !== undefined) {
|
||||
console.error(` Exit code: ${data.exit_code}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`Agent session ${reason}`);
|
||||
}
|
||||
sessionEnded = true;
|
||||
turnResolve?.();
|
||||
turnResolve = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
processEvents().catch((err) => {
|
||||
if (!sessionEnded) {
|
||||
console.error("Event stream error:", err instanceof Error ? err.message : err);
|
||||
}
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const line = await rl.question("> ");
|
||||
if (!line.trim()) continue;
|
||||
|
||||
const turnComplete = new Promise<void>((resolve) => {
|
||||
turnResolve = resolve;
|
||||
});
|
||||
|
||||
try {
|
||||
await client.postMessage(sessionId, { message: line.trim() });
|
||||
await turnComplete;
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
turnResolve = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue