
Research
/Security News
Chrome and Firefox Extensions Posing as Free VPNs Add Clipboard Stealers via Malicious Updates
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.
A TypeScript SDK for building AI agents with streaming support, tool calling, and middleware pattern.
preToolUse vs permissionRequestpreToolUse is now only for mutating tool calls (modifiedToolCall).
Permission decisions (allow / deny) must be handled in permissionRequest.
Before (old pattern):
hooks: {
preToolUse: async () => ({ allow: true }),
}
After (current pattern):
hooks: {
permissionRequest: async () => ({ allow: true }),
}
If you need both, use:
preToolUse to rewrite tool arguments/tool callpermissionRequest to allow or block executionpnpm add goatchain
# or
npm install goatchain
# or
bun add goatchain
GoatChain SDK is built around three core components:
import process from 'node:process'
import { Agent, createModel, createOpenAIAdapter } from 'goatchain'
// 1. Create model client
const model = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
}),
})
// 2. Create agent
const agent = new Agent({
name: 'Simple Assistant',
systemPrompt: 'You are a helpful assistant.',
model,
})
// 3. Create session and interact
const session = await agent.createSession()
session.send('Hello!')
// 4. Stream responses
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
} else if (event.type === 'done') {
console.log('\nDone:', event.stopReason)
}
}
Session is the core component for managing conversations. It handles message history, state persistence, and event streaming.
// Create a new session
const session = await agent.createSession()
// Create session with custom ID
const session = await agent.createSession({ sessionId: 'my-session-id' })
// Create session with configuration overrides
const session = await agent.createSession({
maxIterations: 10,
requestParams: {
temperature: 0.7,
maxTokens: 2000,
},
})
interface CreateSessionOptions {
sessionId?: string // Custom session ID
model?: ModelRef // Optional model override
variants?: ModelRef[] // Optional fallback model refs
maxIterations?: number // Max agent loop iterations (default: 1000)
cwd?: string // Working directory for file operations
messageQueueConfig?: {
autoProcessQueue?: boolean // Auto-process queued messages (default: true)
maxQueueSize?: number // Optional queue length limit
}
requestParams?: {
temperature?: number // Model temperature
maxTokens?: number // Max output tokens
stop?: string[] // Optional stop sequences
[key: string]: unknown // Provider-specific request params
}
}
You can set a working directory for the session, which will be automatically applied to tools that support setCwd() (for example Read, Write, Edit, Glob, Grep, and Bash):
// Set CWD when creating a session
const session = await agent.createSession({
cwd: '/path/to/project',
})
// Get the current working directory
const cwd = session.getCwd()
console.log('Current directory:', cwd)
// Change the working directory at runtime
session.setCwd('/path/to/another/project')
// All file operations now use the new directory
session.send('Read the README.md file')
Benefits:
session.setCwd()// Simple text message
session.send('What is the weather today?')
// Multiple messages in sequence
session.send('First question')
session.send('Follow-up question')
You can pass options to send() to control priority, tool execution, approval, and more:
// Higher priority messages are processed first (smaller number = higher priority)
session.send('Low priority', { priority: 10 })
session.send('High priority', { priority: 1 })
send() returns a message ID that can be used for queue management:
const messageId = session.send('Can be cancelled later')
session.cancelQueuedMessage(messageId)
Session now supports queue-based messaging by default. You can enqueue multiple messages safely even while a previous receive() is running.
const session = await agent.createSession()
session.send('First message')
session.send('Second message')
// Batch enqueue with priority
session.sendBatch([
{ input: 'Task A', priority: 2 },
{ input: 'Task B', priority: 1 },
])
// Inspect queue state
const queue = session.getQueueStatus()
console.log(queue.length, queue.isProcessing)
// Remove queued messages
session.clearQueue()
Manual queue mode:
const session = await agent.createSession({
messageQueueConfig: { autoProcessQueue: false },
})
session.send('Message 1')
session.send('Message 2')
for await (const event of session.receive()) {
// only Message 1
}
for await (const event of session.receive()) {
// Message 2
}
Tool context and approval options still work as before:
// Auto-approve all tools for this request
session.send('Create files and search the web', {
toolContext: {
approval: {
autoApprove: true, // Skip approval for all tools
},
},
})
// Require approval only for high-risk tools
session.send('Analyze the codebase', {
toolContext: {
approval: {
strategy: 'high_risk', // Only prompt for high/critical risk tools
},
},
})
// Combine with session-level CWD
session.setCwd('/path/to/project')
session.send('Read and analyze all TypeScript files', {
toolContext: {
approval: { autoApprove: true },
},
})
// Tools will use /path/to/project as working directory
// and execute without requiring approval
The receive() method returns an async generator that streams events:
for await (const event of session.receive()) {
switch (event.type) {
case 'text_delta':
// Partial text from LLM
process.stdout.write(event.delta)
break
case 'tool_call_start':
console.log(`\nCalling tool: ${event.toolName ?? event.callId}`)
break
case 'tool_result':
console.log(`Tool result: ${event.result}`)
break
case 'iteration_end':
console.log(`\nIteration ${event.iteration} complete`)
console.log(`Tokens used: ${event.usage?.totalTokens}`)
break
case 'done':
console.log(`\nConversation done: ${event.stopReason}`)
console.log(`Total tokens: ${event.usage?.totalTokens}`)
break
}
}
| Event Type | Description | Key Fields |
|---|---|---|
session_created | New session stream starts | sessionId |
text_start / text_delta / text_end | Assistant text stream | delta, content |
thinking_start / thinking_delta / thinking_end | Model reasoning stream | delta, content |
tool_call_start / tool_call_delta / tool_call_end | Tool call lifecycle | callId, toolName, toolCall |
tool_output_start / tool_output_delta | Live tool stdout/stderr stream | tool_call_id, delta, isStderr |
tool_result | Tool execution result | tool_call_id, result, isError |
tool_approval_requested | High-risk tool needs approval | tool_call_id, toolName, riskLevel, args |
requires_action | Execution paused for approval or AskUser answers | kind, checkpoint, checkpointRef, questions |
tool_skipped | Tool execution skipped | tool_call_id, toolName, reason |
iteration_start / iteration_end | Agent loop iteration lifecycle | iteration, usage |
subagent_event | Forwarded subagent status | subagentId, subagentType, phase |
compression_start / compression_end | Context compression lifecycle | tokensBefore, tokensAfter, strategy |
hook_evaluation | Prompt-hook evaluation lifecycle | hookName, phase, status |
done | Stream finished | stopReason, modelStopReason, error, usage |
// Access message history
console.log(session.messages) // Message[]
// Check session status
console.log(session.status) // 'active' | 'paused' | 'completed' | 'error' | 'archived'
// Get token usage
console.log(session.usage)
// {
// promptTokens: 150,
// completionTokens: 80,
// totalTokens: 230
// }
// Session metadata
console.log(session.id) // Session ID
console.log(session.createdAt) // Creation timestamp
console.log(session.updatedAt) // Last update timestamp
Sessions can be saved and restored. All session state including message history, configuration, and working directory (cwd) are preserved:
// Save session to snapshot
const snapshot = session.toSnapshot()
// Store snapshot to database, file, etc.
// Restore session from snapshot
session.restoreFromSnapshot(snapshot)
// Or create a new session from snapshot
const restored = await agent.createSession()
restored.restoreFromSnapshot(snapshot)
// The restored session maintains all state including:
// - Message history
// - Configuration overrides
// - Working directory (cwd)
// - Usage statistics
const session = await agent.createSession()
// Turn 1
session.send('What is 2 + 2?')
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
}
}
// Turn 2 (maintains context)
session.send('What about multiplying that by 3?')
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
}
}
// Session now contains the full conversation history plus the system prompt
console.log(session.messages.length) // 5 (system + 2 user + 2 assistant)
Resume interrupted sessions using checkpoints:
import { FileStateStore } from 'goatchain'
// Create agent with state store
const stateStore = new FileStateStore({
dir: './checkpoints',
deleteOnComplete: true,
})
const agent = new Agent({
name: 'MyAgent',
systemPrompt: 'You are helpful.',
model,
stateStore,
})
// Start session (automatically checkpointed)
const session = await agent.createSession({ sessionId: 'session-123' })
session.send('Start a long task')
for await (const event of session.receive()) {
// Process events...
}
// Later, resume from checkpoint
const resumed = await agent.resumeSession('session-123')
for await (const event of resumed.receive()) {
// Continue from where it left off
}
// Add message manually
session.addMessage({
role: 'user',
content: 'Custom message',
})
// Save session state manually
await session.save()
// Advanced: direct message mutation is allowed, but prefer snapshots/checkpoints
// when you need reproducible restore flows.
session.messages.push({ role: 'assistant', content: 'Synthetic entry' })
import { Agent, createModel, createOpenAIAdapter } from 'goatchain'
const agent = new Agent({
name: 'MyAgent',
systemPrompt: 'You are a helpful assistant.',
model,
tools, // Optional: ToolRegistry instance
stateStore, // Optional: for persistence
middleware: [], // Optional: middlewares to register on startup
mcpServers: [], // Optional: MCP server configs
enableLogging: false, // Optional: internal logs
})
interface AgentOptions {
id?: string // Optional custom agent ID
name: string // Agent name
systemPrompt: string // System instructions
model: ModelClient // LLM client
tools?: ToolRegistry // Tool registry (not an array)
stateStore?: StateStore // Persistence layer
middleware?: Middleware[] // Optional middleware list
mcpServers?: MCPServerConfig[] // Optional MCP server configs
enableLogging?: boolean // Internal debug logs
}
// Pin to specific model
agent.setModel({ provider: 'openai', modelId: 'gpt-4o-mini' })
// Switch to another concrete model client instance
const otherModelClient = createModel({
adapter: createOpenAIAdapter({ defaultModelId: 'gpt-4o' }),
})
agent.setModel(otherModelClient)
Manage multiple sessions:
const sessionManager = agent.sessionManager
// List all sessions
const sessions = await sessionManager.list()
// Get specific session
const session = await sessionManager.get('session-id')
// Destroy session
await sessionManager.destroy('session-id')
GoatChain SDK exports the following built-in tools:
| Tool Class | Runtime Name | Category | Purpose |
|---|---|---|---|
ReadTool | Read | File | Read file content (text, binary metadata, and selected converted formats) |
WriteTool | Write | File | Create or overwrite files |
EditTool | Edit | File | In-place text replacement edits |
GlobTool | Glob | File/Search | Find files by glob pattern |
GrepTool | Grep | File/Search | Search file contents by pattern |
BashTool | Bash | Command | Execute shell commands |
WebSearchTool | WebSearch | Web | Search the web (e.g. via Serper API) |
WebFetchTool | WebFetch | Web | Fetch and extract content from a specific URL |
TaskTool | Task | Subagent | Run a registered subagent task (for example Explore) |
TodoWriteTool | TodoWrite | Planning | Manage structured todo lists |
TodoPlanTool | TodoPlan | Planning | Create/update planning todos for plan flows |
AskUserTool | AskUserQuestion | Interaction | Ask the user structured follow-up questions |
EnterPlanModeTool | EnterPlanMode | Mode | Enter plan mode |
ExitPlanModeTool | ExitPlanMode | Mode | Exit plan mode |
import {
Agent,
ToolRegistry,
ReadTool,
WriteTool,
EditTool,
BashTool,
GrepTool,
GlobTool,
WebSearchTool,
WebFetchTool,
} from 'goatchain'
// Create a tool registry and register tools
const tools = new ToolRegistry()
tools.register(new ReadTool())
tools.register(new WriteTool())
tools.register(new EditTool())
tools.register(new BashTool())
tools.register(new GrepTool())
tools.register(new GlobTool())
tools.register(new WebSearchTool({ apiKey: process.env.SERPER_API_KEY }))
tools.register(new WebFetchTool())
const agent = new Agent({
name: 'MyAgent',
systemPrompt: 'You are helpful.',
model,
tools,
})
GoatChain can connect MCP servers and register their remote tools automatically:
const agent = new Agent({
name: 'MCP Assistant',
systemPrompt: 'You are helpful.',
model,
mcpServers: [
{
id: 'weather',
name: 'Weather API',
transport: 'http',
url: 'https://example.com/mcp',
auth: { type: 'bearer', token: process.env.WEATHER_API_KEY! },
},
{
id: 'local-tools',
name: 'Local Tools',
transport: 'stdio',
command: 'node',
args: ['./mcp-servers/tools.js'],
},
],
})
See docs/mcp.md for details.
File-related tools (ReadTool, WriteTool, EditTool, GlobTool, GrepTool, BashTool) support configuring the working directory:
import path from 'node:path'
// Define output directory
const OUTPUT_DIR = path.resolve(import.meta.dirname, 'output')
// Option 1: Set working directory only
const tools = new ToolRegistry()
tools.register(new ReadTool({ cwd: OUTPUT_DIR }))
tools.register(new WriteTool({ cwd: OUTPUT_DIR }))
tools.register(new EditTool({ cwd: OUTPUT_DIR }))
tools.register(new GlobTool({ cwd: OUTPUT_DIR }))
tools.register(new GrepTool({ cwd: OUTPUT_DIR }))
tools.register(new BashTool({ cwd: OUTPUT_DIR }))
// Option 2: Restrict to specific directory (security sandbox)
// This prevents access to files outside the allowed directory
const tools = new ToolRegistry()
tools.register(
new ReadTool({
cwd: OUTPUT_DIR,
allowedDirectory: OUTPUT_DIR, // Only allow reads within OUTPUT_DIR
}),
)
tools.register(
new WriteTool({
cwd: OUTPUT_DIR,
allowedDirectory: OUTPUT_DIR, // Only allow writes within OUTPUT_DIR
}),
)
How each file tool uses cwd:
| Tool | What it does | How cwd is applied | Per-call override | Extra sandbox options |
|---|---|---|---|---|
ReadTool | Reads files (and some converted formats) | Relative file_path resolves from cwd | file_path can be absolute | allowedDirectory, fileBlacklist, disableBlacklist |
WriteTool | Writes/overwrites files | Relative file_path resolves from cwd | file_path can be absolute | allowedDirectory, fileBlacklist, disableBlacklist |
EditTool | Replaces old_string with new_string in a file | Relative file_path resolves from cwd | file_path can be absolute | fileBlacklist, disableBlacklist |
GlobTool | Finds files by pattern | Search root defaults to cwd | path argument can change search root | fileBlacklist, disableBlacklist |
GrepTool | Searches text content in files | Search runs under cwd | path argument narrows search scope | fileBlacklist, disableBlacklist |
BashTool | Runs shell commands | Commands execute in cwd | workdir argument overrides per call | None |
Directory & Protection Options:
| Option | Description | Example |
|---|---|---|
cwd | Working directory for resolving relative paths | { cwd: '/app/output' } |
allowedDirectory | Restrict file access to this directory only (blocks path traversal) | { allowedDirectory: '/app/output' } |
When to use allowedDirectory:
import { BaseTool, ToolRegistry } from 'goatchain'
class MyCustomTool extends BaseTool {
name = 'my_tool'
description = 'Does something useful'
parameters = {
type: 'object',
properties: {
input: {
type: 'string',
description: 'Input parameter',
},
},
required: ['input'],
}
async execute(args: { input: string }) {
// Your tool logic here
return `Processed: ${args.input}`
}
}
// Use it
const tools = new ToolRegistry()
tools.register(new MyCustomTool())
const agent = new Agent({
name: 'MyAgent',
systemPrompt: 'You are helpful.',
model,
tools,
})
Dynamically manage tools:
const registry = agent.tools
// Register new tool
registry.register(new MyCustomTool())
// Unregister tool
registry.unregister('my_tool')
// Get tool
const tool = registry.get('my_tool')
// List all tools
const allTools = registry.list()
// Convert to OpenAI format
const openaiTools = registry.toOpenAIFormat()
Sessions support lifecycle hooks that let you intercept user input, tool calls, and session/subagent lifecycle events.
GoatChain has three relevant mechanisms for tool execution control:
preToolUse Hook - For tool-call mutation before permission/approval
modifiedToolCallpermissionRequestpermissionRequest Hook - For programmatic auto-approval/blocking
preToolUse, so it sees the modified tool callallow: true → skips approval flow (execution still goes through normal middleware/disabled checks)allow: false → tool is blockedApproval System (via toolContext.approval) - For interactive user approval
requires_action eventThese are independent: If permissionRequest returns allow: true, the approval system is bypassed.
interface AgentHooks {
// Session lifecycle
sessionStart?: (ctx: SessionStartContext) => Promise<void>
sessionEnd?: (ctx: SessionEndContext) => Promise<void>
stop?: (ctx: StopContext) => Promise<void>
userPromptSubmit?:
| ((ctx: UserPromptSubmitContext) => Promise<UserPromptSubmitResult>)
| PromptHookEntry
| Array<
| ((ctx: UserPromptSubmitContext) => Promise<UserPromptSubmitResult>)
| PromptHookEntry
>
// Tool lifecycle
// - Can modify tool call with modifiedToolCall
preToolUse?:
| ((ctx: ToolHookContext) => Promise<PreToolUseResult | void>)
| PromptHookEntry
| Array<
| ((ctx: ToolHookContext) => Promise<PreToolUseResult | void>)
| PromptHookEntry
>
permissionRequest?:
| ((ctx: ToolHookContext) => Promise<PermissionRequestResult>)
| PromptHookEntry
| Array<
| ((ctx: ToolHookContext) => Promise<PermissionRequestResult>)
| PromptHookEntry
>
postToolUse?:
| ((ctx: ToolHookContext, result: unknown) => Promise<void>)
| PromptHookEntry
| Array<
| ((ctx: ToolHookContext, result: unknown) => Promise<void>)
| PromptHookEntry
>
postToolUseFailure?:
| ((ctx: ToolHookContext, error: Error) => Promise<void>)
| PromptHookEntry
| Array<
| ((ctx: ToolHookContext, error: Error) => Promise<void>)
| PromptHookEntry
>
// Subagent lifecycle (used by parallel task/subagent middleware)
subagentStart?: (ctx: SubagentStartContext) => Promise<void>
subagentStop?:
| ((ctx: SubagentStopContext) => Promise<void>)
| PromptHookEntry
| Array<((ctx: SubagentStopContext) => Promise<void>) | PromptHookEntry>
}
// Backward-compatible alias
type ToolHooks = AgentHooks
interface PromptHookEntry {
type: 'prompt'
prompt: string // use $ARGUMENTS to inject serialized input
model?: { provider: string; modelId: string }
timeoutMs?: number // default: 30000
}
interface BaseHookContext {
sessionId: string
}
interface ToolHookContext extends BaseHookContext {
toolCall: {
id: string
type: 'function'
function: {
name: string
arguments: string
}
}
toolContext: ToolExecutionContext
}
interface PermissionRequestResult {
// If true, passes permission checks and skips approval flow
// (tool still goes through normal middleware/disabled checks)
// If false, blocks tool execution immediately
allow: boolean
// Optional: Modify the tool call before execution
modifiedToolCall?: ToolCall
}
interface PreToolUseResult {
// Optional: Modify the tool call before permission/approval/execution
modifiedToolCall?: ToolCall
}
interface UserPromptSubmitResult {
allow: boolean
modifiedInput?: MessageContent
}
interface SessionStartContext extends BaseHookContext {
startReason: 'new' | 'resume'
messages: Message[]
}
interface StopContext extends BaseHookContext {
stopReason:
| 'max_iterations'
| 'final_response'
| 'error'
| 'cancelled'
| 'approval_required'
| 'max_follow_ups'
// Stop reason reported by the latest model response, when available
// (e.g. 'tool_call' | 'final' | 'length' | 'error' | 'cancelled')
modelStopReason?: 'tool_call' | 'final' | 'length' | 'error' | 'cancelled'
finalResponse?: string
usage: Usage
error?: { code?: string; message: string }
messages: Message[]
}
interface SessionEndContext extends BaseHookContext {
stopReason: StopContext['stopReason']
finalResponse?: string
usage: Usage
durationMs: number
error?: { code?: string; message: string }
messages: Message[]
}
interface UserPromptSubmitContext extends BaseHookContext {
input: MessageContent
}
interface SubagentStartContext extends BaseHookContext {
subagentId: string
subagentType: string
taskDescription?: string
prompt: string
}
interface SubagentStopContext extends BaseHookContext {
subagentId: string
subagentType: string
result?: unknown
error?: Error
durationMs: number
usage?: Usage
messages: Message[]
}
Prompt hooks are evaluation-only in current SDK behavior:
userPromptSubmit, preToolUse, permissionRequest, postToolUse, postToolUseFailure, subagentStoppermissionRequest prompt evaluation only runs when the approval path is enteredsession.metadata._hookEvaluationsPrompt evaluation emits hook_evaluation stream events with phase:
startstream (text delta)end (status/result/error)hook_evaluation event shape:
interface HookEvaluationEvent extends BaseEvent {
type: 'hook_evaluation'
evaluationId: string
hookName:
| 'permissionRequest'
| 'preToolUse'
| 'postToolUse'
| 'postToolUseFailure'
| 'subagentStop'
| 'userPromptSubmit'
phase: 'start' | 'stream' | 'end'
prompt?: string
input?: unknown
delta?: string
rawResponse?: string
result?: unknown
usage?: Usage
durationMs?: number
status?: 'success' | 'error' | 'timeout'
error?: { code?: string; message: string }
toolCallId?: string
}
Metadata persistence shape:
session.metadata._hookEvaluations = {
preToolUse: [
{
evaluationId: '...',
timestamp: 1730000000000,
hookName: 'preToolUse',
prompt: '...',
input: { ... },
status: 'success',
durationMs: 120,
rawResponse: '{"ok":true}',
result: { ok: true },
usage: { promptTokens: 100, completionTokens: 20, totalTokens: 120 },
toolCallId: 'call_123',
},
],
}
Complete prompt hook example (function hook + prompt hook + event handling):
import { Agent } from 'goatchain'
import type { HookEvaluationEvent } from 'goatchain'
const session = await agent.createSession({
hooks: {
preToolUse: [
async (ctx) => {
// normal behavior hook
return undefined
},
{
type: 'prompt',
prompt: 'Analyze this tool call: $ARGUMENTS',
},
],
permissionRequest: {
type: 'prompt',
prompt: 'Review approval context: $ARGUMENTS',
},
},
})
session.send('Do the task', {
toolContext: {
approval: { strategy: 'high_risk' },
},
})
for await (const event of session.receive()) {
if (event.type === 'hook_evaluation') {
const ev = event as HookEvaluationEvent
if (ev.phase === 'end') {
console.log('hook evaluation done:', ev.hookName, ev.status, ev.result)
}
}
}
console.log(session.metadata?._hookEvaluations)
Typical order in one run:
sessionStart (once, on the first receive() for this session)userPromptSubmit (before a user message enters the loop; can block or rewrite input)preToolUse (runs first for each tool call; can rewrite tool call)permissionRequest (runs after preToolUse; allow/block decision before approval check)toolContext.approval) if still requiredpostToolUse or postToolUseFailurestop (after each LLM response completes; for no-LLM termination points, it runs before done)sessionEnd (when a run fully finishes; not emitted at approval_required pause)import { Agent, ToolRegistry, ReadTool, WriteTool } from 'goatchain'
const agent = new Agent({
name: 'MyAgent',
systemPrompt: 'You are helpful.',
model,
tools: (() => {
const tools = new ToolRegistry()
tools.register(new ReadTool())
tools.register(new WriteTool())
return tools
})(),
})
// Create session with hooks
const session = await agent.createSession({
hooks: {
preToolUse: async (ctx) => {
// Pre-mutate tool call before permission/approval
const toolName = ctx.toolCall.function.name
console.log(`PreToolUse: ${toolName}`)
},
permissionRequest: async (ctx) => {
const toolName = ctx.toolCall.function.name
console.log(`Tool requested: ${toolName}`)
// Returning { allow: true } skips approval flow
// (tool still goes through normal middleware/disabled checks before execution)
// Returning { allow: false } blocks tool execution
return { allow: true }
},
postToolUse: async (ctx, result) => {
console.log(`Tool ${ctx.toolCall.function.name} completed successfully`)
},
postToolUseFailure: async (ctx, error) => {
console.error(`Tool ${ctx.toolCall.function.name} failed:`, error)
},
},
})
The permissionRequest hook is powerful because allow: true bypasses the approval flow entirely, even for high-risk tools. This is useful for automated scenarios:
import type { RiskLevel } from 'goatchain'
// Define auto-approval policy
function shouldAutoApprove(toolName: string, riskLevel: RiskLevel): boolean {
// Auto-approve safe and low risk tools
if (riskLevel === 'safe' || riskLevel === 'low') {
return true
}
// Block critical tools
if (riskLevel === 'critical') {
return false
}
// For medium/high risk, you can implement custom logic
// e.g., check environment, user permissions, etc.
return process.env.AUTO_APPROVE_ALL === 'true'
}
const session = await agent.createSession({
hooks: {
permissionRequest: async (ctx) => {
const tool = agent.tools.get(ctx.toolCall.function.name)
const riskLevel = tool?.riskLevel ?? 'safe'
const allow = shouldAutoApprove(ctx.toolCall.function.name, riskLevel)
if (allow) {
console.log(`✓ Auto-approved: ${ctx.toolCall.function.name}`)
} else {
console.log(`✗ Blocked: ${ctx.toolCall.function.name}`)
}
return { allow }
},
},
})
The simplest way to bypass approval for a specific request is using toolContext.approval.autoApprove:
// Auto-approve all tools for this specific send() call
session.send('Create a file and search the web', {
toolContext: {
approval: {
autoApprove: true, // Bypasses approval for ALL tools in this request
},
},
})
for await (const event of session.receive()) {
// All tools execute without requiring approval
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
}
}
Use cases:
Approval strategies:
session.send('Your task', {
toolContext: {
approval: {
autoApprove: true, // Approve all tools automatically
// OR
strategy: 'high_risk', // Only require approval for high/critical risk tools
// OR
strategy: 'all', // Require approval for every tool call
},
},
})
Important: The permissionRequest hook with allow: true skips approval. For interactive approval flows where you want to pause and ask the user, use toolContext.approval instead:
import type { AgentLoopCheckpoint } from 'goatchain'
// Step 1: Start session WITHOUT permissionRequest hook (to use approval system)
const session = await agent.createSession()
let checkpoint: AgentLoopCheckpoint | undefined
session.send('Create a file and delete it', {
toolContext: {
approval: { strategy: 'high_risk' }, // Pause on high-risk tools for user approval
},
})
// Collect checkpoint when requires_action event is emitted
for await (const event of session.receive()) {
if (event.type === 'requires_action') {
// Save checkpoint
checkpoint =
event.checkpoint ||
(await agent.stateStore?.loadCheckpoint(event.checkpointRef?.sessionId))
break // Pause here
}
}
// Step 2: Resume with approval decisions
if (checkpoint) {
// Build approval decisions for pending tools
const decisions = Object.fromEntries(
checkpoint.pendingToolCalls.map((pending) => {
const toolName = pending.toolCall.function.name
const approved = confirm(`Approve ${toolName}?`)
return [
pending.toolCall.id,
{ approved, reason: approved ? undefined : 'User denied' },
]
}),
)
// Resume session with decisions
for await (const event of session.receive({
toolContext: {
approval: { decisions },
},
})) {
// Process events...
}
}
See examples/tool-approval-session.ts for a full example demonstrating:
# Run the example
bun run examples/tool-approval-session.ts
Key features shown:
requires_action eventsThe preToolUse hook can also modify tool calls before execution:
const session = await agent.createSession({
hooks: {
preToolUse: async (ctx) => {
const toolName = ctx.toolCall.function.name
// Example: Add safety constraints to file writes
if (toolName === 'Write') {
const args = JSON.parse(ctx.toolCall.function.arguments)
// Modify the tool call to add restrictions
const modifiedToolCall = {
...ctx.toolCall,
function: {
...ctx.toolCall.function,
arguments: JSON.stringify({
...args,
// Force files to be written in a safe directory
file_path: `/safe-dir/${args.file_path}`,
}),
},
}
return {
modifiedToolCall,
}
}
},
},
})
The toolContext parameter in send() and receive() is used for approval state and AskUser resume data:
session.send('Do something risky', {
toolContext: {
approval: {
strategy: 'high_risk', // Pause on high-risk tools
// or
decisions: {
tool_call_id_123: { approved: true },
tool_call_id_456: { approved: false, reason: 'Too dangerous' },
},
},
askUser: {
answers: {
tool_call_id_789: {
framework: 'React',
styling: 'Tailwind CSS',
},
},
},
},
})
GoatChain uses a Koa-style onion model for middleware. Each middleware wraps around the core execution:
outer:before → inner:before → exec (model.stream) → inner:after → outer:after
// Add named middleware (recommended)
await agent.use(async (state, next) => {
const start = Date.now()
console.log(`[${state.iteration}] Before model call`)
const nextState = await next(state) // Execute next middleware/model
console.log(`[${state.iteration}] After model call (${Date.now() - start}ms)`)
return nextState
}, 'logging')
// Remove by name
agent.removeMiddleware('logging')
// View all middleware names
console.log(agent.middlewareNames) // ['logging', 'compression', ...]
// Use unsubscribe function
const unsubscribe = await agent.use(middleware, 'temp')
unsubscribe() // Remove middleware
Adds planning phase before execution:
import { createPlanModeMiddleware } from 'goatchain'
// Automatically named 'plan_mode'
await agent.use(createPlanModeMiddleware())
// With custom configuration
await agent.use(
createPlanModeMiddleware({
name: 'my-plan', // Custom name
planPrompt: 'Create a detailed plan...', // Custom prompt
}),
)
Automatically compresses context from the full raw transcript when the prompt approaches the model context window. The middleware now:
tool messages before touching the current roundcontextLength or fails locally before the model callimport { createContextCompressionMiddleware } from 'goatchain'
// Automatically named 'context_compression'
await agent.use(
createContextCompressionMiddleware({
contextLength: 128000,
}),
)
The large E2E example writes round-N/stage1.json through stage4.json under examples/output/context-compression-large-e2e/ by default, so you can inspect each compression step directly.
See src/spec/middleware.md for the full middleware and compression spec.
await agent.use(async (state, next) => {
console.log(`Iteration ${state.iteration}:`, {
messages: state.messages.length,
pendingTools: state.pendingToolCalls.length,
})
const result = await next(state)
console.log(`Completed iteration ${state.iteration}:`, {
shouldContinue: result.shouldContinue,
usage: result.usage,
})
return result
}, 'logger')
await agent.use(async (state, next) => {
try {
return await next(state)
} catch (error) {
console.error('Agent error:', error)
state.shouldContinue = false
state.stopReason = 'error'
state.error = error
return state
}
}, 'error-handler')
import { RateLimiter } from 'some-rate-limiter'
const limiter = new RateLimiter({ requestsPerMinute: 60 })
await agent.use(async (state, next) => {
await limiter.acquire()
return next(state)
}, 'rate-limiter')
await agent.use(async (state, next) => {
let retries = 3
while (retries > 0) {
try {
return await next(state)
} catch (error) {
retries--
if (retries === 0) throw error
console.log(`Retrying... (${retries} attempts left)`)
await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
return state
}, 'retry')
The AgentLoopState object passed to middleware:
interface AgentLoopState {
sessionId: string // Current session ID
messages: Message[] // Conversation history
iteration: number // Current iteration number
pendingToolCalls: ToolCallWithResult[] // Pending tool executions
currentResponse: string // Current LLM response
currentThinking?: string // Current reasoning content, if the model emits it
shouldContinue: boolean // Whether to continue loop
stopReason?: string // Reason for stopping
usage: Usage // Cumulative token usage
error?: Error // Error if any
metadata: Record<string, unknown> // Middleware/hook shared runtime data
}
import { createModel, createOpenAIAdapter } from 'goatchain'
const model = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
baseUrl: 'https://api.openai.com/v1', // Optional
organization: 'org-xxx', // Optional
}),
})
interface OpenAIAdapterOptions {
defaultModelId?: string // Default model ID
apiKey?: string // OpenAI API key
baseUrl?: string // Custom API endpoint (note: lowercase 'u')
organization?: string // OpenAI organization ID
defaultHeaders?: Record<string, string> // Custom headers
timeout?: number // Request timeout (ms)
maxRetries?: number // Max retry attempts (default: 2)
}
// OpenAI
const openai = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
}),
})
// OpenAI-compatible endpoints
const deepseek = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'deepseek-chat',
apiKey: process.env.DEEPSEEK_API_KEY!,
baseUrl: 'https://api.deepseek.com/v1',
}),
})
Implement custom model adapters:
interface ModelClient {
modelId: string
// Streaming API (required)
stream(request: ModelRequest): AsyncIterable<ModelStreamEvent>
// Non-streaming API (optional)
run?(request: ModelRequest): Promise<ModelRunResult>
}
interface ModelRequest {
messages: Message[]
tools?: OpenAITool[]
temperature?: number
maxTokens?: number
topP?: number
stopSequences?: string[]
}
Persist agent state to filesystem:
import { FileStateStore } from 'goatchain'
const stateStore = new FileStateStore({
dir: './checkpoints', // Storage directory
deleteOnComplete: true, // Auto-delete successful completions
})
const agent = new Agent({
name: 'MyAgent',
systemPrompt: 'You are helpful.',
model,
stateStore,
})
For testing or temporary state:
import { InMemoryStateStore } from 'goatchain'
const stateStore = new InMemoryStateStore({
deleteOnComplete: false,
})
Implement custom state stores:
interface StateStore {
deleteOnComplete: boolean
save<T>(sessionId: string, key: string, data: T): Promise<void>
load<T>(sessionId: string, key: string): Promise<T | undefined>
delete(sessionId: string, key: string): Promise<void>
listKeys(sessionId: string): Promise<string[]>
listSessions(): Promise<string[]>
// Checkpoint helpers
saveCheckpoint(checkpoint: AgentLoopCheckpoint): Promise<void>
loadCheckpoint(sessionId: string): Promise<AgentLoopCheckpoint | undefined>
deleteCheckpoint(sessionId: string): Promise<void>
listCheckpoints(): Promise<AgentLoopCheckpoint[]>
}
interface AgentLoopCheckpoint {
sessionId: string
messages: Message[]
iteration: number
usage: Usage
createdAt: number
updatedAt: number
}
// Prefer SDK-managed checkpoints during session.receive().
// You can still inspect or clean them up manually:
const checkpoint = await stateStore.loadCheckpoint('session-id')
// List all checkpoints
const checkpoints = await stateStore.listCheckpoints()
// Delete checkpoint
await stateStore.deleteCheckpoint('session-id')
import process from 'node:process'
import { Agent, createModel, createOpenAIAdapter } from 'goatchain'
const model = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
}),
})
const agent = new Agent({
name: 'Q&A Bot',
systemPrompt: 'You are a helpful assistant that answers questions concisely.',
model,
})
const session = await agent.createSession()
session.send('What is the capital of France?')
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
}
}
import {
Agent,
createModel,
createOpenAIAdapter,
ToolRegistry,
ReadTool,
WriteTool,
BashTool,
} from 'goatchain'
const model = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
}),
})
const tools = new ToolRegistry()
tools.register(new ReadTool())
tools.register(new WriteTool())
tools.register(new BashTool())
const agent = new Agent({
name: 'File Assistant',
systemPrompt: 'You help users manage their files.',
model,
tools,
})
const session = await agent.createSession()
session.send('Read the package.json file and tell me the version')
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
} else if (event.type === 'tool_call_start') {
console.log(`\nCalling: ${event.toolName}`)
} else if (event.type === 'tool_result') {
console.log(`Result: ${JSON.stringify(event.result).slice(0, 100)}...`)
}
}
import {
Agent,
createModel,
createOpenAIAdapter,
FileStateStore,
} from 'goatchain'
const model = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
}),
})
const stateStore = new FileStateStore({
dir: './agent-state',
deleteOnComplete: false, // Keep history
})
const agent = new Agent({
name: 'Persistent Agent',
systemPrompt: 'You are a helpful assistant.',
model,
stateStore,
})
// Create or resume session
let session
const sessionId = 'my-conversation'
try {
session = await agent.resumeSession(sessionId)
console.log('Resumed existing session')
} catch {
session = await agent.createSession({ sessionId })
console.log('Created new session')
}
session.send('Remember this: My favorite color is blue')
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
}
}
import {
Agent,
createModel,
createOpenAIAdapter,
createPlanModeMiddleware,
} from 'goatchain'
const model = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
}),
})
const agent = new Agent({
name: 'Planning Agent',
systemPrompt: 'You are a helpful assistant.',
model,
})
// Add logging middleware
await agent.use(async (state, next) => {
console.log(`\n=== Iteration ${state.iteration} ===`)
const result = await next(state)
console.log(`Tokens used: ${result.usage?.totalTokens || 0}`)
return result
}, 'logger')
// Add plan mode
await agent.use(createPlanModeMiddleware())
const session = await agent.createSession()
session.send('Create a todo list app with React and TypeScript')
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
}
}
import { Agent, createModel, createOpenAIAdapter } from 'goatchain'
const model = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
}),
})
const agent = new Agent({
name: 'Conversational Agent',
systemPrompt: 'You are a helpful assistant.',
model,
})
const session = await agent.createSession()
async function chat(message: string) {
console.log(`\nUser: ${message}`)
console.log('Assistant: ')
session.send(message)
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
}
}
console.log('\n')
}
// Multi-turn conversation
await chat('My name is Alice')
await chat('What is 2 + 2?')
await chat('What is my name?') // Should remember "Alice"
await chat('Multiply the previous result by 3') // Should remember "4"
console.log(`Total messages: ${session.messages.length}`)
console.log(`Total tokens: ${session.usage.totalTokens}`)
Combine session-level working directory with auto-approval for automated file operations:
import {
Agent,
createModel,
createOpenAIAdapter,
ToolRegistry,
ReadTool,
WriteTool,
EditTool,
GlobTool,
GrepTool,
} from 'goatchain'
const model = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
}),
})
const tools = new ToolRegistry()
tools.register(new ReadTool())
tools.register(new WriteTool())
tools.register(new EditTool())
tools.register(new GlobTool())
tools.register(new GrepTool())
const agent = new Agent({
name: 'File Agent',
systemPrompt: 'You are a file management assistant.',
model,
tools,
})
// Set working directory at session creation
const session = await agent.createSession({
cwd: '/path/to/project',
})
// Auto-approve all file operations for automated workflow
session.send('List all TypeScript files, then create a summary.md file', {
toolContext: {
approval: {
autoApprove: true, // No approval prompts - fully automated
},
},
})
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
} else if (event.type === 'tool_call_start') {
console.log(`\nExecuting: ${event.toolName}`)
}
}
// Change directory and continue with auto-approval
session.setCwd('/path/to/another/project')
session.send('Analyze the project structure and create a report', {
toolContext: {
approval: { autoApprove: true },
},
})
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
}
}
Perfect for:
For more control, you can configure individual tools with specific directories and restrictions:
import path from 'node:path'
import {
Agent,
createModel,
createOpenAIAdapter,
ToolRegistry,
ReadTool,
WriteTool,
GlobTool,
GrepTool,
} from 'goatchain'
const model = createModel({
adapter: createOpenAIAdapter({
defaultModelId: 'gpt-4o',
apiKey: process.env.OPENAI_API_KEY!,
}),
})
// Define output directory for agent's file operations
const OUTPUT_DIR = path.resolve(process.cwd(), 'output')
// Configure tools with working directory and restrictions
const tools = new ToolRegistry()
tools.register(
new ReadTool({
cwd: OUTPUT_DIR,
allowedDirectory: OUTPUT_DIR, // Sandbox: only allow reads in OUTPUT_DIR
}),
)
tools.register(
new WriteTool({
cwd: OUTPUT_DIR,
allowedDirectory: OUTPUT_DIR, // Sandbox: only allow writes in OUTPUT_DIR
}),
)
tools.register(new GlobTool({ cwd: OUTPUT_DIR }))
tools.register(new GrepTool({ cwd: OUTPUT_DIR }))
const agent = new Agent({
name: 'Sandboxed File Agent',
systemPrompt: `You are a file management assistant.
All your file operations are restricted to the output directory.
You can create, read, and modify files within this sandbox.`,
model,
tools,
})
const session = await agent.createSession()
session.send("Create a report.md file with a summary of today's tasks")
for await (const event of session.receive()) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta)
} else if (event.type === 'tool_call_start') {
console.log(`\n[Tool] ${event.toolName}`)
}
}
new Agent(options: AgentOptions)
Options:
name: string - Agent name (required)systemPrompt: string - System instructions (required)model: ModelClient - LLM client (required)tools?: ToolRegistry - Tool registry (not an array)stateStore?: StateStore - Persistence layermiddleware?: Middleware[] - Middleware list to register at startupmcpServers?: MCPServerConfig[] - MCP server configuration listenableLogging?: boolean - Enable internal logscreateSession(options?): Promise<Session>
Create a new session.
const session = await agent.createSession({
sessionId: 'custom-id', // Optional custom session ID
maxIterations: 10, // Optional max iterations
requestParams: {
// Optional request parameters
temperature: 0.7,
maxTokens: 2000,
},
})
resumeSession(sessionId, options?): Promise<Session>
Resume an existing session from checkpoint.
const session = await agent.resumeSession('session-123')
use(middleware, name?): Promise<() => void>
Add middleware. Returns unsubscribe function.
const unsubscribe = await agent.use(myMiddleware, 'my_middleware')
unsubscribe() // Remove middleware
removeMiddleware(nameOrFn): boolean
Remove middleware by name or function reference.
agent.removeMiddleware('my_middleware')
setModel(modelOrRef): void
Switch or pin model at runtime.
agent.setModel({ provider: 'openai', modelId: 'gpt-4o-mini' })
id: string - Agent IDname: string - Agent namesystemPrompt: string - System promptmodel: ModelClient - Current model clienttools?: ToolRegistry - Tool registrystateStore?: StateStore - State storesessionManager?: BaseSessionManager - Session managermiddlewareNames: string[] - List of middleware namessend(input, options?): string
Enqueue a message and return its queue message ID.
const id = session.send('Hello!')
sendBatch(messages): string[]
Batch enqueue messages and return queue message IDs.
const ids = session.sendBatch([
{ input: 'task-1', priority: 1 },
{ input: 'task-2', priority: 2 },
])
cancelQueuedMessage(messageId): boolean
Cancel a queued message by ID.
session.cancelQueuedMessage(id)
getQueueStatus(): MessageQueueStatus
Query queue length, preview list, processing status, and config.
console.log(session.getQueueStatus())
receive(options?): AsyncGenerator<AgentEvent>
Stream agent events.
for await (const event of session.receive()) {
console.log(event)
}
addMessage(message): void
Manually add a message.
session.addMessage({
role: 'user',
content: 'Hello',
})
save(): Promise<void>
Manually save session state.
await session.save()
toSnapshot(): SessionSnapshot
Export session to snapshot.
const snapshot = session.toSnapshot()
restoreFromSnapshot(snapshot): void
Restore session from snapshot.
getCwd(): string | undefined
Get the current working directory for this session.
const cwd = session.getCwd()
console.log('Working directory:', cwd)
setCwd(cwd: string): void
Set the current working directory for this session. This automatically syncs the new directory to all tools that support it.
session.setCwd('/path/to/project')
// All file operation tools now use this directory
id: string - Session IDstatus: SessionStatus - Session status ('active' | 'paused' | 'completed' | 'error' | 'archived')messages: Message[] - Message historyusage: Usage - Token usage statisticscreatedAt: number - Creation timestampupdatedAt: number - Last update timestampinterface Message {
role: 'system' | 'user' | 'assistant' | 'tool'
content: MessageContent
reasoning_content?: string // Assistant messages
tool_calls?: ToolCall[] // Assistant messages
tool_call_id?: string // Tool messages
name?: string
isError?: boolean // Tool messages
}
interface Usage {
promptTokens: number
completionTokens: number
totalTokens: number
}
type AgentEvent =
| TextStartEvent
| TextDeltaEvent
| TextEndEvent
| ToolCallStartEvent
| ToolCallDeltaEvent
| ToolCallEndEvent
| ToolOutputStartEvent
| ToolOutputDeltaEvent
| ToolApprovalRequestedEvent
| RequiresActionEvent
| ToolSkippedEvent
| ToolResultEvent
| ThinkingStartEvent
| ThinkingDeltaEvent
| ThinkingEndEvent
| IterationStartEvent
| IterationEndEvent
| DoneEvent
interface TextDeltaEvent {
type: 'text_delta'
delta: string
}
interface ToolCallStartEvent {
type: 'tool_call_start'
callId: string
toolName?: string
}
interface RequiresActionEvent {
type: 'requires_action'
kind: 'tool_approval' | 'ask_user'
checkpoint?: AgentLoopCheckpoint
checkpointRef?: { sessionId: string }
}
interface ToolResultEvent {
type: 'tool_result'
tool_call_id: string
result: unknown
isError?: boolean
}
interface DoneEvent {
type: 'done'
finalResponse?: string
stopReason:
| 'max_iterations'
| 'final_response'
| 'error'
| 'cancelled'
| 'approval_required'
| 'max_follow_ups'
modelStopReason?: 'tool_call' | 'final' | 'length' | 'error' | 'cancelled'
error?: {
code?: string
message: string
status?: number
retryable?: boolean
}
usage?: Usage
}
classDiagram
direction TB
class Agent {
+id: string
+name: string
+systemPrompt: string
+model: ModelClient
+tools: ToolRegistry?
+stateStore: StateStore?
+sessionManager: BaseSessionManager?
+use(middleware): Promise~function~
+createSession(): Promise~Session~
+resumeSession(id): Promise~Session~
}
class ModelClient {
<<interface>>
+modelId: string
+stream(request): AsyncIterable~ModelStreamEvent~
}
class StateStore {
<<interface>>
+saveCheckpoint(): Promise~void~
+loadCheckpoint(): Promise~Checkpoint~
+deleteCheckpoint(): Promise~void~
+listCheckpoints(): Promise~Checkpoint[]~
}
class BaseTool {
<<abstract>>
+name: string
+description: string
+parameters: JSONSchema
+execute(args): Promise~unknown~
}
class ToolRegistry {
+register(tool): void
+unregister(name): boolean
+get(name): BaseTool
+list(): BaseTool[]
}
class BaseSession {
<<abstract>>
+id: string
+status: SessionStatus
+messages: Message[]
+usage: Usage
+send(input, options?): string
+receive(): AsyncGenerator~AgentEvent~
+save(): Promise~void~
}
class Middleware {
<<function>>
(state, next) => Promise~AgentLoopState~
}
Agent --> ModelClient : uses
Agent --> ToolRegistry : uses
Agent --> StateStore : uses
Agent --> BaseSession : creates
Agent ..> Middleware : applies
ToolRegistry --> BaseTool : contains
DimCode includes a terminal UI (TUI) for interactive agent sessions:
# Install globally
npm install -g dimcode@latest
# Run
dim
Run the local API server + Web GUI:
dim server --open
Features:
See docs/cli.md and docs/server.md for details.
Expose DimCode as an Agent Client Protocol server for editor integrations:
dim acp
For source checkouts, use a cwd-independent command:
node /absolute/path/to/GoatChain/scripts/acpx-agent.mjs
Configuration for Zed (settings.json):
{
"agent_servers": {
"dimcode": {
"command": "/absolute/path/to/dim",
"args": ["acp"]
}
}
}
For OpenClaw acpx, use either /absolute/path/to/dim acp or node /absolute/path/to/GoatChain/scripts/acpx-agent.mjs.
Do not use bun run acp-server there; it depends on the launcher cwd being the GoatChain repo root.
See docs/acp-server.md for details.
dim server and use the browser UIFAQs
Unknown package
The npm package goatchain receives a total of 51 weekly downloads. As such, goatchain popularity was classified as not popular.
We found that goatchain demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers 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.

Research
/Security News
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.

Research
/Security News
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.

Security News
Rolldown paused Rust React Compiler integration after a 5MB binary size increase raised concerns about shipping React-specific code to all Vite users.