@brian/claude-chat-client (0.1.4)

Published 2026-07-21 19:48:35 +00:00 by brian in brian/claudechatwindow

Installation

@brian:registry=
npm install @brian/claude-chat-client@0.1.4
"@brian/claude-chat-client": "0.1.4"

About this package

@brian/claude-chat-client

Shared Node/TypeScript Anthropic client for homeclaude and kittycharactersheet's backends (both already use @anthropic-ai/sdk directly). Owns the streaming request + tool-use round-trip loop; each app still supplies its own API key, model, and tool implementations, so per-app usage tracking is unchanged.

Usage

import { createClaudeClient } from "@brian/claude-chat-client";

const claude = createClaudeClient({
  apiKey: config.anthropicApiKey, // this app's own key, unchanged
  model: config.claudeModel,      // this app's own model, unchanged
});

const result = await claude.chat(
  {
    system: "You are ...",
    messages: conversationSoFar,
    tools: MY_APP_TOOLS,
  },
  {
    onTextDelta: (delta) => res.write(delta), // pipe into your existing SSE/NDJSON response
    onToolUse: (call) => logger.info("tool call", call),
    executeTool: async (call) => {
      switch (call.name) {
        case "create_calendar_event":
          return await createCalendarEvent(call.input);
        default:
          throw new Error(`unknown tool ${call.name}`);
      }
    },
  }
);

// result.messages -> persist as the new conversation history
// result.finalText -> full assistant text across all tool-loop rounds

Tool execution stays app-specific on purpose (calendar writes, dose logs, character-sheet edits, ...) — this package only owns the loop that calls the model, detects tool_use blocks, awaits your executeTool, and feeds tool_result blocks back in until the model stops asking for tools or maxToolRounds is hit (default 8). Multiple tool_use blocks in one round run sequentially, not concurrently — matches how every hand-rolled loop this package replaces already worked, since tool calls can have ordering dependencies or share mutable state.

Three more callbacks cover what a richer app (homeclaude) needed beyond the single-call view onToolUse/executeTool give you:

  • onUsage({ inputTokens, outputTokens, cacheReadInputTokens, cacheCreationInputTokens }) — fired once per API call (every round is separately billed, not just once for the whole chat() call).
  • onToolUseBatch(calls, assistantContent) — fired once per round with every tool_use block in that round (for a single combined status line like "fetching weather · searching web" instead of one per call) plus the full assistant content array, which may include text blocks alongside the tool_use ones (a "Let me check that" preamble) — useful if you persist the exact turn Claude generated rather than just the tool calls.
  • onToolResultBatch(results) — fired once per round after all of that round's executeTool calls resolve, with each { toolUseId, content, isError }.

System prompt caching

system accepts a plain string, or an array of blocks when part of it should get its own cache_control breakpoint — e.g. a large, stable rules/docs reference that would otherwise get re-sent (and re-billed) as fresh input tokens on every call in a multi-turn or multi-round-tool-loop conversation. Build the cached block with cachedTextBlock(); put volatile, per-request text in blocks after it so it doesn't bust the cache:

import { cachedTextBlock } from "@brian/claude-chat-client";

await claude.chat({
  system: [
    cachedTextBlock({ text: bigRulesReferenceText, ttl: "1h" }),
    { type: "text", text: `Current state:\n${summarizeState(state)}` },
  ],
  messages: conversationSoFar,
});

Image attachments

imageContentBlock() builds a Claude image content block from base64 + media type (mirrors homeclaude's own buildAttachmentBlock()), for including in a user message's content array alongside a text block:

import { imageContentBlock, isSupportedImageType } from "@brian/claude-chat-client";

const content: Anthropic.MessageParam["content"] = [
  { type: "text", text: userText },
  ...images.map((img) => imageContentBlock({ mediaType: img.mediaType, base64: img.base64 })),
];

Only image/jpeg, image/png, image/gif, and image/webp are supported (SUPPORTED_IMAGE_MEDIA_TYPES) — check with isSupportedImageType() before calling imageContentBlock() on user-supplied media types.

pdfContentBlock({ base64, title }) and textContentBlock({ text, title }) cover homeclaude's other two attachment kinds (PDF, and its fallback for everything else — CSV, markdown, JSON, ... read as UTF-8 text):

import { pdfContentBlock, textContentBlock } from "@brian/claude-chat-client";

pdfContentBlock({ base64: pdfBytes.toString("base64"), title: "invoice.pdf" });
textContentBlock({ text: fileBytes.toString("utf8"), title: "notes.md" });

Dependencies

Dependencies

ID Version
@anthropic-ai/sdk ^0.100.1

Development dependencies

ID Version
typescript ^5.5.0
Details
npm
2026-07-21 19:48:35 +00:00
3
latest
6.3 KiB
Assets (1)
Versions (5) View all
0.1.4 2026-07-21
0.1.3 2026-07-21
0.1.2 2026-07-21
0.1.1 2026-07-20
0.1.0 2026-07-20