npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bookingcare/agent

v0.5.2

Published

General-purpose agent with transport abstraction, state management, and attachment support

Readme

@bookingcare/agent

Agent loop with tool dispatch, file operations, and on-demand skill loading.

Installation

pnpm add @bookingcare/agent

Quick Start

import { agentLoop, createToolDispatch } from "@bookingcare/agent";
import { getModel } from "@bookingcare/ai";

const model = getModel("gpt-5.4-nano")!;

// Simple query — no tools
const { messages, iterations } = await agentLoop("What is 2+2?", { model });

// With file tools (bash, read_file, write_file, edit_file)
const result = await agentLoop("Create a file called notes.txt with 'hello'", {
  model,
  workdir: "/path/to/workspace",
});

Agent Loop

The loop runs a conversation until the model stops calling tools or a max iteration limit is hit.

const { messages, iterations } = await agentLoop(query, {
  model,
  system: "You are a coding assistant.", // optional system prompt
  workdir: "/workspace", // for file tool sandboxing
  maxTokens: 8000, // per-completion token limit
  maxIterations: 50, // safety cap
  onStreamResult: (result, i) => {
    // called after each iteration
    console.log(`Iteration ${i}:`, result.text);
  },
});

Returns { messages, iterations } — the full message history and how many loop cycles ran.

MCP Support

The agent can connect to Model Context Protocol (MCP) servers and expose their tools alongside local tools.

Basic Usage

import { Agent } from "@bookingcare/agent";
import { getModel } from "@bookingcare/ai";

const agent = new Agent({
  initialState: {
    model: getModel("gpt-4o-mini")!,
  },
  mcpServers: [
    {
      name: "filesystem",
      transport: "sse",
      connection: {
        type: "sse",
        url: "http://localhost:3000/sse",
      },
    },
  ],
});

try {
  await agent.prompt("List all files in /tmp");
} finally {
  await agent.shutdown();
}

Configuration File (.mcp.json)

Array-style configuration:

{
  "servers": [
    {
      "name": "github",
      "transport": "stdio",
      "connection": {
        "type": "stdio",
        "command": "node",
        "args": ["./mcp-server.js"]
      }
    }
  ]
}

Remote MCP object-style configuration:

{
  "mcpServers": {
    "dentaltrip-kb": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": {
        "authorization": "Bearer ${MCP_AUTH_TOKEN}"
      }
    }
  }
}
import { Agent } from "@bookingcare/agent";
import { getModel } from "@bookingcare/ai";

const agent = new Agent({
  model: getModel("gpt-4o-mini")!,
  loadMcpConfig: true,
});

Supported Transports

  • http — Streamable HTTP for remote MCP servers
  • sse — Server-Sent Events
  • stdio — Standard input/output for local MCP servers

WebSocket transport is not supported.

Streamable HTTP servers can include per-server request headers for service-to-service auth:

const agent = new Agent({
  model,
  mcpServers: [
    {
      name: "dentaltrip-kb",
      transport: "http",
      connection: {
        type: "http",
        url: "https://mcp.example.com/mcp",
        headers: {
          authorization: `Bearer ${process.env.MCP_AUTH_TOKEN}`,
        },
      },
    },
  ],
});

Tool Naming

MCP tools are namespaced as server:tool to avoid collisions.

  • Server: filesystem
  • Tool: read_file
  • Agent sees: filesystem:read_file

Shutdown

Always call agent.shutdown() when you are done so MCP connections are closed cleanly.

const agent = new Agent({
  model,
  mcpServers,
});

try {
  await agent.prompt("...");
} finally {
  await agent.shutdown();
}

API Reference

AgentOptions.mcpServers

List of MCP server configs to connect when the agent starts.

mcpServers?: McpServerConfig[];

AgentOptions.loadMcpConfig

When true, the agent loads MCP servers from .mcp.json before starting.

loadMcpConfig?: boolean;

AgentOptions.mcpConfigPath

Optional path to the MCP config file or workspace directory used for loading MCP servers.

mcpConfigPath?: string;

McpServerConfig

Configuration for one MCP server connection.

interface McpServerConfig {
  name: string;
  transport: "http" | "sse" | "stdio";
  connection:
    | { type: "http"; url: string; headers?: Record<string, string> }
    | { type: "sse"; url: string }
    | { type: "stdio"; command: string; args?: string[] };
}

Tools

Four file tools are available when workdir is set (or passed to createToolDispatch):

| Tool | Description | | ------------ | ---------------------------------------- | | bash | Run a shell command | | read_file | Read file contents (optional line limit) | | write_file | Write content to a file (creates dirs) | | edit_file | Replace an exact, unique text segment | | todo | Update task list for multi-step tracking | | load_skill | Load a skill body on demand |

File paths are sandboxed to workdir — path traversal attempts throw.

Sandbox integration

Pass a sandbox from @bookingcare/infra to route bash and file tools through process-isolated execution. Local sandboxes do not inherit the parent environment, so include a safe PATH (and on Windows, SystemRoot/ComSpec when needed) if commands need external binaries:

import { createSandbox } from "@bookingcare/infra";

const sandbox = createSandbox({
  kind: "local",
  workdir: "/workspace",
  env: {
    PATH: "/usr/local/bin:/usr/bin:/bin",
  },
});
const { tools, dispatch } = createToolDispatch("/workspace", undefined, sandbox);

Custom dispatch

import { createToolDispatch } from "@bookingcare/agent";

const { tools, dispatch } = createToolDispatch("/workspace");

// Pass to agentLoop with custom overrides
await agentLoop("do something", {
  model,
  tools,
  dispatch: {
    ...dispatch,
    my_tool: (args) => `result: ${args.input}`,
  },
});

Skill Loading

On-demand knowledge injection. Skills live as SKILL.md files in a directory:

skills/
  code-review/
    SKILL.md
  greeter/
    SKILL.md

Each SKILL.md has YAML frontmatter:

---
name: code-review
description: Perform structured code reviews with a checklist
---

# Code Review Skill

When reviewing code, use this checklist:
...

Two-layer design:

  • Layer 1 — Skill names and descriptions injected into the system prompt (~100 tokens/skill)
  • Layer 2 — Full skill body returned via load_skill tool call when the model needs it (~2000 tokens/skill)
import { agentLoop } from "@bookingcare/agent";
import { SkillLoader } from "@bookingcare/agent";

// Automatic: pass skillsDir to agentLoop
await agentLoop("Review this code", {
  model,
  skillsDir: "./skills",
  workdir: "/workspace",
});

// Manual: use SkillLoader directly
const loader = new SkillLoader("./skills");
loader.getDescriptions(); // "  - code-review: Perform structured...\n  - greeter: ..."
loader.getContent("code-review"); // <skill name="code-review">...</skill>

When skillsDir is provided, the load_skill tool is added to the dispatch automatically and skill descriptions are appended to the system prompt.

Todo Tracking

The agent tracks its own progress via a todo tool. When the loop starts, a TodoManager is created automatically and the todo tool is added to the dispatch.

import { TodoManager } from "@bookingcare/agent";

const mgr = new TodoManager();
mgr.update([
  { id: "1", text: "Plan the feature", status: "completed" },
  { id: "2", text: "Write code", status: "in_progress" },
  { id: "3", text: "Write tests", status: "pending" },
]);

console.log(mgr.render());
// [x] #1: Plan the feature
// [>] #2: Write code
// [ ] #3: Write tests
//
// (1/3 completed)

Nag reminder

If the model doesn't update its todos for 3 consecutive rounds of tool calls, a <reminder>Update your todos.</reminder> message is injected to nudge it back on track. This keeps the agent's progress visible without scripting its route.

Constraints:

  • Maximum 20 items
  • Only one item can be in_progress at a time
  • Valid statuses: pending, in_progress, completed

Agent Class

Stateful wrapper around the streaming agent loop. Owns the transcript, emits lifecycle events, executes tools, and exposes queueing APIs.

import { Agent } from "@bookingcare/agent";
import { getModel } from "@bookingcare/ai";

const agent = new Agent({
  initialState: {
    model: getModel("gpt-5.4-nano")!,
    systemPrompt: "You are a helpful assistant.",
    tools: [],
    messages: [],
    thinkingLevel: "off",
  },
});

// Subscribe to specific channels
agent.eventBus.on("streaming", (event) => {
  if (event.type === "message_end") {
    console.log("Got message:", event.message);
  }
});

// Legacy API still works, but is deprecated
agent.subscribe((event) => {
  console.log(event.type);
});

// Prompt the agent
await agent.prompt("What is 2+2?");

// State is preserved between calls
console.log(agent.state.messages);

Events

| Channel | Event types | | ----------- | ----------------------------------------------------------------------------- | | lifecycle | context_trimmed, agent_end | | streaming | message_start, message_update, message_end | | tools | permission_needed, tool_execution_start, tool_execution_end, turn_end |

agent.eventBus.once(channel, listener) is available for one-shot subscriptions. agent.subscribe() remains available for backward compatibility and subscribes the handler to all channels.

PermissionManager

Agent can gate tool calls with a rule-based PermissionManager.

import { Agent, PermissionManager } from "@bookingcare/agent";
import { getModel } from "@bookingcare/ai";

const permissions = new PermissionManager();
permissions.grant({ tool: "read_file", action: "allow" });
permissions.grant({ tool: "bash", action: "ask" });

const agent = new Agent({
  initialState: {
    model: getModel("gpt-5.4-nano")!,
    systemPrompt: "You are a coding assistant.",
    tools: [],
    messages: [],
    thinkingLevel: "off",
  },
  permissionManager: permissions,
});

agent.subscribe((event) => {
  if (event.type === "permission_needed") {
    event.resolve("allow");
  }
});
  • allow and deny are checked before beforeToolCall
  • ask emits permission_needed and waits for resolve("allow" | "deny")
  • Default rules: read_file allow, bash/write_file/edit_file ask, wildcard deny

Steering and Follow-up Queues

  • steer(message) — inject after the current assistant turn
  • followUp(message) — run after the agent would otherwise stop
  • abort() — abort the current run
  • waitForIdle() — resolve when the current run finishes
  • reset() — clear transcript, runtime state, and queues

Queue Modes

steeringMode and followUpMode control how queued messages are drained:

  • "one-at-a-time" (default) — one message per poll
  • "all" — drain all queued messages at once

AgentTool

Extends Tool from @bookingcare/ai with execution capabilities:

import { AgentTool } from "@bookingcare/agent";
import { Type, Static } from "@bookingcare/ai";

const ReadFileParams = Type.Object({
  path: Type.String(),
  offset: Type.Optional(Type.Number()),
  limit: Type.Optional(Type.Number()),
});

type ReadFileArgs = Static<typeof ReadFileParams>;

const readFileTool: AgentTool<typeof ReadFileParams> = {
  name: "read_file",
  description: "Read file contents",
  parameters: ReadFileParams,
  label: "Read File",
  prepareArguments: (args) => {
    // Shim for raw LLM arguments before schema validation
    return args as ReadFileArgs;
  },
  execute: async (toolCallId, params, signal) => {
    const content = fs.readFileSync(params.path, "utf-8");
    return { content };
  },
  executionMode: "sequential", // per-tool override
};

AgentTool Fields

| Field | Description | | ------------------------------------------------- | --------------------------------------------------------------------------- | | name, description, parameters | Inherited from Tool | | label | Human-readable label for UI display | | prepareArguments? | Shim for raw tool-call arguments before validation | | execute(toolCallId, params, signal?, onUpdate?) | Execute the tool call. Throw on failure. Returns Promise<AgentToolResult> | | executionMode? | Per-tool override ("parallel" or "sequential") |

Persistence

Agent can persist its session state to an external Store (e.g. createStore({ type: "json" ... }) or createStore({ type: "mysql" ... }) from @bookingcare/infra). When a store is provided, the agent automatically saves the full transcript, todo state, and metadata to the store on every agent_end event.

import { Agent } from "@bookingcare/agent";
import { createStore } from "@bookingcare/infra";

const store = await createStore({ type: "json", baseDir: "./data" });

const agent = new Agent({
  initialState: { model, systemPrompt: "You are helpful.", tools: [] },
  store, // sessionId is auto-generated if not provided
  todoManager: new TodoManager(), // optional — persists todo state too
});

await agent.prompt("Plan this project");

// Later, resume the session
const resumed = await Agent.resume({
  sessionId: agent.sessionId!,
  store,
  model, // optional — will be resolved from saved model ID if omitted
  todoManager: new TodoManager(), // optional — restores todo state
});

await resumed.prompt("Continue from where we left off");

// Close the store when shutting down.
await store.close();

Notes on resumption:

  • Tools are not persisted (they contain function references). Re-register tools after resume().
  • sessionId must be provided or auto-generated to persist. Without a store, the agent is purely in-memory and sessionId may be undefined.
  • Streaming state (streamingMessage, pendingToolCalls) is intentionally not persisted. If the process crashes mid-run, the last completed agent_end state is what gets restored.

MCP Transport Support

The MCP client currently supports stdio, sse, and Streamable HTTP (http) transports. WebSocket transport is intentionally disabled until @modelcontextprotocol/sdk exports WebSocketClientTransport.

Architecture

src/
  types.ts          — all public types
  agent-loop.ts     — merged loop: agentLoop (simple) + runAgentLoop/runAgentLoopContinue (streaming)
  agent.ts          — Agent class: state, events, queues, abort
  tools.ts          — tool schemas, handlers, and createToolDispatch()
  skill-loader.ts   — SkillLoader: scan, describe, load skills
  todo-manager.ts   — TodoManager: structured state for task tracking
  index.ts          — barrel export