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

@palon7/cc-client

v0.1.0

Published

Node.js client for the Claude Code CLI — typed EventEmitter sessions over stream-json

Downloads

35

Readme

cc-client

A Node.js client for the Claude Code CLI, modeled after the Anthropic SDK's MessageStream API.

Spawns claude --output-format stream-json as a subprocess, parses the NDJSON output, and exposes a typed EventEmitter-based session interface.

Prerequisites

  • Node.js >= 22
  • Claude Code CLI installed and authenticated (claude command available in PATH)

Installation

npm install @palon7/cc-client

Quick Start

import { ClaudeCodeClient } from '@palon7/cc-client';

const client = new ClaudeCodeClient({ workspacePath: '/path/to/workspace' });

const session = client.start('What is 2 + 2?');
session.on('text', (chunk) => process.stdout.write(chunk));

const result = await session.done();
console.log(`\nCost: $${result?.totalCostUsd.toFixed(6)}`);

API Reference

ClaudeCodeClient

class ClaudeCodeClient {
  constructor(options: ClaudeCodeClientOptions);

  /** Start a new session with the given prompt. */
  start(prompt: string): ClaudeCodeSession;

  /** Resume a previous session by ID. */
  resume(sessionId: string, message?: string): ClaudeCodeSession;
}

start() spawns the process immediately and returns the session synchronously. Register event listeners right after calling it — no events fire before the next tick.

ClaudeCodeClientOptions

| Option | Type | Default | Description | | -------------------- | --------------------------------------------------- | --------------------- | ----------------------------------------------- | | workspacePath | string | — | Working directory for the Claude CLI process | | model | string | CLI default | Model to use (e.g. "claude-sonnet-4-6") | | appendSystemPrompt | string | — | Text appended to the system prompt | | mcpConfig | Record<string, unknown> | — | MCP server config passed as --mcp-config | | allowedTools | string[] | — | Tools Claude is allowed to use | | disallowedTools | string[] | ['AskUserQuestion'] | Tools Claude is not allowed to use | | permissionMode | 'acceptEdits' \| 'default' \| 'bypassPermissions' | 'acceptEdits' | Permission mode | | bypassPermissions | boolean | false | Pass --dangerously-skip-permissions | | env | Record<string, string> | — | Extra environment variables for the CLI process | | extraArgs | string[] | — | Additional CLI arguments (appended last) |

ClaudeCodeSession

class ClaudeCodeSession extends EventEmitter<ClaudeCodeSessionEvents> {
  readonly sessionId: string; // Set after system/init received
  readonly exitReason: ExitReason | null; // Set after end event

  /** Resolves with the ResultEntry on completion, or undefined if interrupted/aborted. */
  done(): Promise<ResultEntry | undefined>;

  /** Send a follow-up message. Queued if called before connect fires. */
  send(text: string): void;

  /** Send SIGINT to interrupt generation (enables resume). */
  interrupt(): void;

  /** Send SIGTERM to fully stop the process. */
  abort(): void;
}

type ExitReason = 'complete' | 'interrupted' | 'aborted' | 'error';

ClaudeCodeSessionEvents

| Event | Payload | Description | | --------- | ------------- | --------------------------------------------------------------------------------------------- | | connect | — | Process started; stdin is ready | | entry | ParsedEntry | A fully parsed entry (see Entry Types below) | | text | string | Text chunk shortcut — equivalent to entry filtered to kind: 'text' | | raw | unknown | Raw JSON line from stdout (plus stdin messages for logging). stream_event lines are skipped | | stderr | string | A line from stderr | | error | Error | Process error or non-zero exit | | end | — | Process exited (fires after error if applicable) |

Entry Types (ParsedEntry)

All entries share { id: string, timestamp: string, kind: EntryKind }.

| kind | Additional fields | | -------------- | ------------------------------------------------------------------------------------ | | system | sessionId, model, tools, mcpServers, betas? | | text | text: string, isStreaming: boolean | | thinking | text: string, isStreaming: boolean | | tool_call | toolName, toolUseId, input: Record<string, unknown> | | tool_result | toolUseId, toolName, content: string, isError: boolean | | user_message | text: string | | result | subtype, totalCostUsd, numTurns, durationMs, isError, result?, errors? | | notification | text: string |

Streaming entries (isStreaming: true) arrive as incremental deltas. The same id is reused across deltas for the same block.

MessageParser

Parses raw StreamJsonMessage objects into ParsedEntry[]. Each instance maintains its own state — create a fresh instance per parsing context.

class MessageParser {
  parse(message: StreamJsonMessage): ParsedEntry[];
}

Useful for replaying saved .jsonl log files without spawning a process.

Examples

Stream text output

import { ClaudeCodeClient } from '@palon7/cc-client';

const client = new ClaudeCodeClient({ workspacePath: process.cwd() });
const session = client.start('Write a haiku about the ocean.');

session.on('text', (chunk) => process.stdout.write(chunk));

await session.done();
console.log(); // newline

Handle all entry types

import { ClaudeCodeClient } from '@palon7/cc-client';
import type { ParsedEntry } from '@palon7/cc-client';

const client = new ClaudeCodeClient({ workspacePath: process.cwd() });
const session = client.start('List the files in the current directory.');

session.on('entry', (entry: ParsedEntry) => {
  switch (entry.kind) {
    case 'system':
      console.log(`[session] ${entry.sessionId} model=${entry.model}`);
      break;
    case 'text':
      if (!entry.isStreaming) process.stdout.write(entry.text);
      break;
    case 'tool_call':
      console.log(`\n[tool] ${entry.toolName}`, entry.input);
      break;
    case 'tool_result':
      console.log(`[result] ${entry.content.slice(0, 80)}...`);
      break;
    case 'result':
      console.log(`\n[done] ${entry.numTurns} turns — $${entry.totalCostUsd.toFixed(6)}`);
      break;
  }
});

await session.done();

Interrupt and resume

import { ClaudeCodeClient } from '@palon7/cc-client';

const client = new ClaudeCodeClient({ workspacePath: process.cwd() });
const session = client.start('Count from 1 to 100, one number per line.');

// Interrupt after 2 seconds
const timer = setTimeout(() => session.interrupt(), 2000);

await session.done();
clearTimeout(timer);

if (session.exitReason === 'interrupted') {
  console.log('\nInterrupted — resuming...');
  const resumed = client.resume(session.sessionId, 'Continue from where you left off.');
  resumed.on('text', (chunk) => process.stdout.write(chunk));
  await resumed.done();
}

Multi-turn conversation

import { ClaudeCodeClient } from '@palon7/cc-client';

const client = new ClaudeCodeClient({ workspacePath: process.cwd() });

// First turn
let session = client.start('Remember the number 42.');
await session.done();
const sessionId = session.sessionId;

// Second turn — resume the same session
session = client.resume(sessionId, 'What number did I ask you to remember?');
session.on('text', (chunk) => process.stdout.write(chunk));
await session.done();
console.log();

Replay a saved log file

import fs from 'fs';
import readline from 'readline';
import { MessageParser } from '@palon7/cc-client';
import type { StreamJsonMessage } from '@palon7/cc-client';

const parser = new MessageParser();
const rl = readline.createInterface({
  input: fs.createReadStream('raw.jsonl', 'utf-8'),
  crlfDelay: Infinity,
});

for await (const line of rl) {
  if (!line.trim()) continue;
  const msg = JSON.parse(line) as StreamJsonMessage;
  const entries = parser.parse(msg);
  for (const entry of entries) {
    if (entry.kind === 'text' && !entry.isStreaming) {
      console.log(entry.text);
    }
  }
}

With MCP servers

import { ClaudeCodeClient } from '@palon7/cc-client';

const client = new ClaudeCodeClient({
  workspacePath: '/path/to/project',
  mcpConfig: {
    filesystem: {
      command: 'npx',
      args: ['-y', '@modelcontextprotocol/server-filesystem', '/path/to/project'],
    },
  },
  allowedTools: ['filesystem__read_file', 'filesystem__list_directory'],
});

const session = client.start('Summarize the README.md file.');
session.on('text', (chunk) => process.stdout.write(chunk));
await session.done();

Session History API

Read and analyze past Claude Code session files stored on disk (~/.claude/projects/<project>/<uuid>.jsonl).

getSessionDir

function getSessionDir(projectPath: string, configDir?: string): string;

Resolve the session directory for a given workspace path. Uses Claude Code's path encoding algorithm (/[^a-zA-Z0-9]/g-).

import { getSessionDir } from '@palon7/cc-client';

const dir = getSessionDir('/Users/me/work/my-project');
// → ~/.claude/projects/-Users-me-work-my-project

listSessions

function listSessions(sessionDir: string): Promise<SessionSummary[]>;

List all sessions in the given directory, sorted by lastModified descending.

interface SessionSummary {
  sessionId: string;
  model?: string;
  title?: string;        // custom-title > agent-name > slug
  numTurns: number;
  durationMs: number;
  startedAt: string;
  lastModified: string;
  lastMessage?: string;  // First 120 chars of last assistant text
}

loadSession

function loadSession(sessionDir: string, sessionId: string): Promise<SessionDetail>;

Load a complete session from disk, parsing all messages into ParsedEntry[].

interface SessionDetail {
  entries: ParsedEntry[];
  sessionId: string;
  model?: string;
  title?: string;
  numTurns: number;
  durationMs: number;
  entryCount: number;
  inputTokens: number;
  outputTokens: number;
}

Examples — Session History

List recent sessions

import { getSessionDir, listSessions } from '@palon7/cc-client';

const dir = getSessionDir('/path/to/workspace');
const sessions = await listSessions(dir);

for (const s of sessions.slice(0, 5)) {
  console.log(`${s.sessionId} [${s.model ?? 'unknown'}] ${s.title ?? '(untitled)'}`);
  console.log(`  ${s.numTurns} turns, ${(s.durationMs / 1000).toFixed(1)}s`);
  if (s.lastMessage) console.log(`  → ${s.lastMessage}`);
}

Load and replay a session

import { getSessionDir, loadSession } from '@palon7/cc-client';

const dir = getSessionDir('/path/to/workspace');
const detail = await loadSession(dir, 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');

console.log(`Model: ${detail.model}`);
console.log(`Tokens: ${detail.inputTokens} in / ${detail.outputTokens} out`);
console.log(`Entries: ${detail.entryCount}`);
console.log();

for (const entry of detail.entries) {
  switch (entry.kind) {
    case 'user_message':
      console.log(`[User] ${entry.text}`);
      break;
    case 'text':
      console.log(`[Assistant] ${entry.text}`);
      break;
    case 'tool_call':
      console.log(`[Tool] ${entry.toolName}(${JSON.stringify(entry.input).slice(0, 80)})`);
      break;
    case 'tool_result':
      console.log(`[Result] ${entry.content.slice(0, 100)}`);
      break;
  }
}

Find sessions by model

import { getSessionDir, listSessions } from '@palon7/cc-client';

const dir = getSessionDir('/path/to/workspace');
const sessions = await listSessions(dir);

const opusSessions = sessions.filter((s) => s.model?.includes('opus'));
console.log(`Found ${opusSessions.length} Opus sessions`);

Analyze token usage across sessions

import { getSessionDir, listSessions, loadSession } from '@palon7/cc-client';

const dir = getSessionDir('/path/to/workspace');
const sessions = await listSessions(dir);

let totalInput = 0;
let totalOutput = 0;

for (const s of sessions) {
  const detail = await loadSession(dir, s.sessionId);
  totalInput += detail.inputTokens;
  totalOutput += detail.outputTokens;
}

console.log(`Total tokens: ${totalInput} input, ${totalOutput} output`);

Notes

  • The claude binary must be in PATH and authenticated before use.
  • Streaming entries (isStreaming: true) are deltas — the same id carries incremental text. An assistant message received from stdout resets the streaming state and delivers complete final entries.
  • interrupt() sends SIGINT (or SIGTERM on Windows). After done() resolves with exitReason === 'interrupted', call client.resume(session.sessionId) to continue.
  • done() resolves on end and rejects on error. Interrupted or aborted sessions resolve with undefined.