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

@flow-state-dev/claude-code

v0.1.3

Published

Claude Code integration for flow-state-dev: dispatch cloud coding tasks via the local claude CLI.

Readme

@flow-state-dev/claude-code

Claude Code integration for flow-state-dev, with two entry points. The /cli entry dispatches a cloud coding task by shelling out to your local claude CLI (no Anthropic SDK dependency). The /sdk entry runs a Claude Code agent in-process and streams its work through the flow's item stream, backed by the optional @anthropic-ai/claude-agent-sdk peer dependency.

The /sdk entry is a harness — a coding agent driven as a block, returning the neutral run handle declared in @flow-state-dev/core. @flow-state-dev/codex is the other one, and @flow-state-dev/harness-manager drives either from a task board. The /cli entry is not a harness: it is fire-and-forget, so there is no outcome, final message, usage or cost to report.

Installation

pnpm add @flow-state-dev/claude-code

The host machine needs the Claude Code CLI installed and signed in to claude.ai (cloud dispatch requires subscription auth, not an API key).

Quick start

A capability is a reusable bundle you attach to a block. Installing this one is the host's explicit opt-in to letting the process run claude:

import { generator } from "@flow-state-dev/core";
import { createClaudeCliCapability } from "@flow-state-dev/claude-code/cli";

const planner = generator({
  name: "planner",
  model: "openai/gpt-5.4-mini",
  uses: [createClaudeCliCapability()],
});

The generator can now call the dispatch tool. To dispatch deterministically instead, use the handler directly as a sequencer step:

import { claudeRemoteDispatch } from "@flow-state-dev/claude-code/cli";

const dispatch = claudeRemoteDispatch();
// seq.step(dispatch) with input { instructions: "..." }

How it works (CLI)

claude --remote "<instructions>" creates a cloud session on claude.ai that clones your repo's GitHub remote at the current branch (push first — the cloud VM clones from GitHub, not your working tree). The block parses the returned session URL, persists a handle, and returns it.

It is fire-and-forget: the CLI exposes no headless way to poll or stream cloud-task progress today, so the block does not wait or poll. Watch progress via /tasks in the CLI, claude.ai, or the mobile app.

Trust model

Nothing shells out unless the host opts in by installing the capability (or passing a resolver). resolveClaudeCli supplies the binary path, working directory, environment, and the exec function — so the binary location is host-controlled and the subprocess is mockable in tests.

Dispatching --remote needs a TTY

claude --remote refuses to run unless stdout is a TTY, so the default resolveClaudeCli (a bare spawn) cannot dispatch — it exits 1 with "--remote requires an interactive terminal". Pass resolvePtyClaudeCli, which runs claude under script(1) (a pseudo-terminal) and scrubs inherited CLAUDE_* / ANTHROPIC_API_KEY state so the dispatch authenticates as your logged-in user rather than tripping a "Detected a custom API key" prompt:

import { claudeRemoteDispatch, resolvePtyClaudeCli } from "@flow-state-dev/claude-code/cli";

const dispatch = claudeRemoteDispatch({ resolveClaudeCli: resolvePtyClaudeCli });

Requires script(1) (present on macOS and Linux).

Session state

Each dispatch appends a handle to claudeRemoteTasks in session state:

type ClaudeRemoteHandle = {
  source: "claude-code/cli-remote";
  status: "dispatched";
  sessionId: string | null;   // parsed from CLI output when present
  url: string | null;         // claude.ai session URL when present
  instructions: string;
  dispatchedAt: number;
  raw: string;                // verbatim CLI stdout
};

A later request reads ctx.session.state.claudeRemoteTasks to reference prior dispatches.

Running as background work (/sdk)

The SDK agent keeps its own session state — sdkSessionId (the run it resumes) and sdkAgentRuns (the handles it has returned). Pass detached: true to run it as background work instead, which switches that off:

claudeCodeAgent({ detached: true });

Nothing is declared, read, or written, and the SDK is handed no resume, so each run starts fresh. Use it when the agent runs as a task-board worker in a child session: rows may share that session, so the board refuses a hand-off whose block declares session state. The run's own history is the child session's item stream instead.

The returned handle still carries the SDK sessionId, and as a worker's output it is persisted with the task — the option governs session state and resume, not the result.

createClaudeCodeAgentCapability({ detached: true }) takes the same option, and you have to pass it: the board refuses a block whose capability declares the schema, and cannot see one a capability's preset adds at all.

Continuing a background run

To have the next job pick up the last one's conversation, pair two options:

claudeCodeAgent({
  detached: true,
  resume: (ctx) => lastSessionFor(ctx),   // `null` or `""` starts fresh
  onSession: (id, ctx) => recordSessionFor(ctx, id),
});

onSession fires during the run, as soon as the agent names its session — not from the returned handle, because a cancelled run returns none, and that is the run you most want to continue. What it reports is the session the agent confirmed it is in, which may not be the one you asked for: record what the hook gives you, and treat "never fired" as "nothing to continue" so the next attempt starts fresh instead of re-sending a session that is gone.

Both are background-path only. In session the block already resumes and records the id itself, so passing either without detached: true throws at construction.

Giving a run its own working directory (/sdk)

By default a run works in whatever directory the server process is running in. Pass cwd to point it somewhere else:

import { mkdir, mkdtemp } from "node:fs/promises";
import { join } from "node:path";

const CHECKOUT_ROOT = "/var/agent-checkouts";

claudeCodeAgent({
  // A fresh directory per run, created by the server. No caller input reaches
  // the path.
  cwd: async () => {
    // `mkdtemp` creates the leaf, not the parent — ENOENT if the root is
    // missing, which on a fresh machine it is.
    await mkdir(CHECKOUT_ROOT, { recursive: true });
    return mkdtemp(join(CHECKOUT_ROOT, "run-"));
  },
  // A fresh directory per run needs a fresh conversation to match.
  detached: true,
});

A function, not a string: one flow build serves many runs, so it resolves per invocation. It may return a promise, as here.

detached: true belongs in this example rather than beside it. By default the agent hands the SDK a resume handle from the previous run in the same session; combined with mkdtemp, the second invocation resumes a conversation started in a directory that has nothing to do with the empty tree it now runs in. Start fresh per run, as here, or keep a stable directory when you want resume — a per-run directory with resume left on is the surprising combination.

The run's file tools address relative paths inside that directory, and so does recordWork's record of what the run touched. It is a working directory, not a boundary — a run can still reach an absolute path outside it, and that operation is recorded at the path it reached.

Controlling what a run reads and what it runs in (/sdk)

Four more options travel alongside cwd:

// `checkoutForThisRun()` allocates a directory, so calling it in both resolvers
// would hand the run one directory and name a different one in its settings,
// with nothing throwing. Calling it once at build time is the other way to get
// it wrong: one flow build serves many runs, and they would all share the
// directory. So: once per invocation, keyed on the run's own context, which
// both resolvers are handed.
const checkouts = new WeakMap<object, Promise<string>>();
const checkoutFor = (ctx: object) => {
  const existing = checkouts.get(ctx);
  if (existing) return existing;
  const fresh = checkoutForThisRun();
  checkouts.set(ctx, fresh);
  return fresh;
};

claudeCodeAgent({
  cwd: (ctx) => checkoutFor(ctx),
  // Which filesystem settings the run loads. Omitted, it loads all of them,
  // exactly as the CLI does.
  settingSources: ["user"],
  // The run's environment. This REPLACES the process environment rather than
  // adding to it — spread `process.env` when you mean to add.
  env: { ...process.env, CI: "1" },
  // The SDK's sandbox settings (`SandboxSettings`, an open object — the Agent
  // SDK is an optional peer here, so its own type is not imported). A value or
  // a resolver: the settings that confine a run name the directory it works
  // in, and that is per run.
  sandbox: async (ctx) => ({
    enabled: true,
    filesystem: { allowWrite: [await checkoutFor(ctx)] },
  }),
  // Capabilities installed on the block, same slot any other block takes.
  uses: [myCapability],
  // Runs after the block threw, with the error. It does not swallow it — the
  // run still fails — so this is where anything the run was holding gets
  // released. A capability cannot contribute lifecycle hooks, so this option
  // is the only way to reach one.
  onErrored: async (error, ctx) => { await releaseWhateverThisRunHeld(ctx); },
});

settingSources is the one worth reading twice. "project" is what makes a run read CLAUDE.md and .claude/settings.json out of its working directory. If that directory is one your server assembled — from resources your application's users can write — then those files are user input, and the run reading configuration out of them means your users configure your agent. Pass [] to load none, or list only the sources you control.

allowWrite is not a fence. The SDK documents it as additional paths to allow writing, merged with the paths that Edit(...) permission rules already grant — so listing your workspace there widens what the run may write, it does not narrow it to that directory. What confines a run is enabled: true turning sandboxing on at all, plus denyWrite and the permission rules; allowWrite is how you punch your workspace through those. If you need a hard boundary, say so with denyWrite and verify it against a real run rather than assuming this option gives you one.

Nothing here changes by default: leave an option out and the run behaves exactly as it does today.

Reusing a directory across runs

A throwaway directory is the easy case. If you want runs that belong together to share a checkout — a retry picking up where the last attempt stopped, say — the path has to be derived from something stable, and that is where the sharp edges are:

import { createHash } from "node:crypto";
import { mkdir } from "node:fs/promises";
import { isAbsolute, join, relative } from "node:path";

function segment(value: string | undefined): string {
  // Absence is `0`; a present value is `1` plus a SHA-256 digest of its UTF-16
  // code units — a fixed 65 characters, so a long id cannot overflow the
  // 255-character filename limit.
  //
  // Digest the CODE UNITS, not the UTF-8 bytes: UTF-8 cannot represent a lone
  // surrogate, so hashing `value` directly maps "\ud800", "\ud801" and a
  // literal "�" onto one digest.
  return value === undefined
    ? "0"
    : `1${createHash("sha256").update(Buffer.from(value, "utf16le")).digest("hex")}`;
}

function checkoutFor(tenantId: string | undefined, key: string): string {
  const dir = join(CHECKOUT_ROOT, segment(tenantId), segment(key));
  // Belt and braces. Encoding already makes escape impossible; this costs a
  // line and fails loudly if the encoding is ever swapped for something weaker.
  const rel = relative(CHECKOUT_ROOT, dir);
  if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
    throw new Error(`refusing a checkout outside ${CHECKOUT_ROOT}`);
  }
  return dir;
}

checkoutFor derives a path and nothing more; provisioning is the resolver's job. Wire both halves of the identity in — the authenticated tenant is on ctx.session.identity.tenantId, and ctx.session.identity.id stays the bare session id two tenants can share — and create the directory before handing it over:

const agent = claudeCodeAgent({
  cwd: async (ctx) => {
    const dir = checkoutFor(
      ctx.session.identity.tenantId,
      ctx.session.identity.id,
    );
    // The SDK spawns into this directory, so it has to exist or the spawn
    // fails with ENOENT. `recursive` makes reuse idempotent.
    await mkdir(dir, { recursive: true });
    return dir;
  },
});

A reused checkout is shared mutable state. Actions run concurrently unless you say otherwise, and the resolver above creates the directory without claiming it, so two runs for one session get the same tree and race in it. Declare a concurrency policy on the action that runs the agent — concurrency: "queue" serializes on the session, which is the same value the checkout is derived from, so one run finishes before the next starts ("reject" if a second request should be dropped instead).

A policy arbitrates dispatches, which leaves two cases open. A generator's tool calls in one model step run concurrently, so a block exposed as a model-facing tool can be invoked twice inside a single dispatch, where the policy never sees it. And the arbiter is a map in the running process, so external workers are not arbitrated either. The recipe is safe for a single-instance host running the agent as a step, one invocation per run; give the agent a fresh directory per run otherwise.

Encoding rather than validating is the whole point, and it is worth being explicit about why. A validating grammar has to enumerate every way a string can misbehave as a path, and that list is longer than it looks: separators and .. are the obvious two, but Windows also strips trailing dots (so acme and acme. are one directory), reserves CON, PRN, AUX, NUL, COM1LPT9 as device names that cannot be directories at all, and folds case. Every one of those is a value two different tenants could hold.

A derived segment sidesteps the whole list: distinct ids give distinct directories, and the output alphabet contains nothing any filesystem treats specially.

A digest rather than a reversible encoding, because the output has to be bounded. Filenames stop at 255 characters, and anything that preserves its input grows with it — hex of UTF-16 code units runs to four characters each, so a 64-character session id produced a 257-character component and mkdir failed ENAMETOOLONG. A digest is a fixed 65 characters for any input. The honest trade is that distinctness now rests on SHA-256 rather than on arithmetic, and the path no longer tells you whose checkout it is. Do not truncate instead — trimming a reversible encoding to fit maps two long ids onto one segment, which is the collision the derivation exists to prevent.

Hash the code units, not the UTF-8 bytes. UTF-8 cannot represent a lone surrogate — a legal JS string that JSON will carry — so anything transcoding through it maps "\ud800", "\ud801" and a literal "�" onto one value.

Give each value its own segment; concatenating them into one string brings back the ambiguity the tenant is there to remove.

Encode whether a value is there, not just what it is. A missing tenant is tempting to fill in with a stand-in — tenantId ?? "default" — but a stand-in is a value some tenant may legitimately hold, and then an un-tenanted host and that tenant address one directory. The tag does the same job without the collision, and keeps every segment non-empty, which matters because join discards an empty one.

If your key is already something you control and know to be safe — a numeric job id, a UUID — the encoding is close to a no-op and you can skip it. Encode by default anyway: the moment the key starts coming from somewhere else, the rules you would have to remember are a list nobody finishes.

Prefer a key your own code assigned over one that arrived with the request.

Full behaviour, including what an empty or symlinked directory does, is on the cwd option's own docs in src/sdk/agent.ts and in the SDK agent guide.

Recording what a run did (/sdk)

recordWork: true records the run's file operations and its own to-do list as state you can read afterwards. Off by default; on, the agent declares three resource collections and writes into them as it goes:

| Accessor | One entry per | |----------|---------------| | observed-file-ops | path the run's file-writing/editing tools touched — lastKind, outcome (pending/applied/failed), lastTouchedAt, appliedCount. Paths, never contents | | observed-plan | to-do item the run kept — title, status, previousStatus, lastOutcome | | observed-gaps | mutation the recorder understood and could not record — kind (file/plan/run) says which record it stands in for, plus the reason and the raw path |

claudeCodeAgent({ detached: true, recordWork: true });

Entries are keyed as <requestId>/<invocation>, so a session reused across runs — and a request that runs the agent more than once — both answer per run. All three declare client state reads, so GET /sessions/:id/resources/observed-file-ops?topicPrefix=observed-file-ops/<requestId>/ returns them; each row's payload is on clientData. Follow nextCursor — the route pages.

The request id in that prefix is percent-escaped into one key segment, so a filter built from a raw id only matches when the id needs no escaping. Escape % first, then /, \ and control characters, and .. as %2E%2E; every other id — brackets, dots and ./... included — is used verbatim. An id with no such characters, which is the common case, needs nothing.

appliedCount counts only the operations on that path the harness confirmed applied, not the attempts: each one is recorded twice, once when the call is seen and once when its result arrives. outcome beside it describes only the last settlement, so the count is what says how many of a path's touches landed. 0 and null are different answers — 0 means the run touched the path and nothing applied, null means the row was written before the field existed.

The file record covers tool-driven operations only. A run that edits through the shell makes no file-tool call, so nothing is recorded for it. Recording never fails the run: what it cannot handle becomes a gap row.

createClaudeCodeAgentCapability({ recordWork: true }) takes the same option and needs it — the capability declares the collections itself, because a block in a capability's tools contributes no resource declarations to the flow.

A run whose files are resources (/sdk)

cwd hands a run a directory. It doesn't put anything in it, and it doesn't bring anything back. createWorkspaceAgentCapability does both:

import { createWorkspaceAgentCapability } from "@flow-state-dev/claude-code/sdk";

const workspace = createWorkspaceAgentCapability({
  root: async () => mkdtemp(join(CHECKOUT_ROOT, "run-")),
});

generator({
  name: "coder",
  model: "openai/gpt-5.4-mini",
  prompt: "Use the workspace agent to make the change.",
  uses: [workspace],
});

Every resource collection on the block's context is mounted at its pattern prefix, so a collection matching artifacts/** appears at <root>/artifacts/. Narrow it with collections or exclude if you want fewer.

After the run, what changed goes back to the collection it came from — unless something else changed the same file while the run held it. Then nothing is written and the path is recorded instead.

Reading what could not be saved

Three outcomes need a person: a conflict (two writers, one file), a contested path (another run was writing it at the same moment, so this one stood off), and an orphan (a file written outside every mounted collection). All three land in the workspace-outcomes collection, keyed by run:

const unsettled = await ctx.resources["workspace-outcomes"].list();

A conflict carries three hashes — what the projection last wrote, what the collection holds now, and what the run left — which is what lets you say who changed what. ours: null means the run deleted a file somebody else had edited. A contested path carries no hashes: nothing has been written yet, only a claim held elsewhere. Claims are per file, so two runs working on different files in one collection never contend.

A status item says how many there were, so a run that ends with unsaved work doesn't end quietly.

Containment

By default the run is confined to the workspace it was given. Two settings, answering different halves of the same question:

  • settingSources: [] — the run doesn't read its configuration out of the workspace. A projected directory holds whatever the mounted collections hold, and in a real application those are written by its users. A CLAUDE.md or a .claude/settings.json sitting among them is user input, and without this the run obeys it.
  • Sandbox settings — the run doesn't write outside the workspace. cwd is a working directory, not a fence: absolute paths still resolve, so the boundary has to be declared. The default names the root in filesystem.allowWrite and refuses allowUnsandboxedCommands, which is the escape a command can otherwise ask for by itself.
  • disallowedTools — the run doesn't leave the workspace. The SDK's worktree tools relocate a run mid-flight when the model asks, and a projection that hydrated one directory would then be reconciling a tree the run had already left.

Set settingSources or sandbox yourself and yours wins — containment is a default, not a lock. disallowedTools merges instead, so adding your own doesn't silently take the relocation ones away. contain: false turns all three off, which is what a trusted-workspace deployment wants and what nothing else should.

Limitations

  • No headless polling/streaming of cloud-task progress (CLI limitation).
  • Dispatches the current branch as pushed to GitHub; push local commits first.
  • The exact claude --remote stdout shape is undocumented; the parser is defensive and falls back to retaining raw output if it can't find a URL.

Quick start (SDK)

The /sdk entry runs a Claude Code agent in-process. Install the optional peer:

pnpm add @flow-state-dev/claude-code @anthropic-ai/claude-agent-sdk

Attach the capability so a generator can hand work to the agent, or use the block directly as a sequencer step:

import { generator } from "@flow-state-dev/core";
import { createClaudeCodeAgentCapability } from "@flow-state-dev/claude-code/sdk";

const orchestrator = generator({
  name: "orchestrator",
  model: "openai/gpt-5.4-mini",
  uses: [createClaudeCodeAgentCapability()],
});

The agent's messages, reasoning, tool calls, and sub-agents become flow-state-dev items as it runs, and its session persists across requests. See the Claude Code SDK agent guide for the full surface.

Choosing /cli or /sdk

| | /cli | /sdk | |--|--------|--------| | Execution | Fire-and-forget cloud session | In-process agent | | Dependency | None (shells out to claude) | Optional @anthropic-ai/claude-agent-sdk peer | | Auth | claude.ai subscription | Anthropic credentials | | Progress | Watch via /tasks, claude.ai, mobile | Streamed live as flow-state-dev items | | Session | Cloud session handle | Persistent, resumed across requests | | As background work | Already fire-and-forget | Task-board worker with detached: true; the child session's item stream is the run's record | | Reach for it when | Offloading long autonomous work | A real agent in the loop, observed step by step |

Running tests

pnpm --filter @flow-state-dev/claude-code test

Documentation

Coding agents · Claude Code SDK agent · Claude Code remote dispatch · Harness manager