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/fsdev

v0.2.0

Published

The fsdev command-line interface for flow-state-dev.

Readme

@flow-state-dev/fsdev

The developer interface. Run flows, execute blocks, inspect definitions — all from the terminal.

Installation

pnpm add -g @flow-state-dev/fsdev
fsdev run my-agent chat -i '{"message": "Hello!"}'

That discovers your flow, executes the action, and streams NDJSON events to stdout as blocks run. Session state persists between invocations.

Commands

fsdev run — Execute a flow action

Discovers flows from conventional directories (src/flows/, flows/), validates the action, and executes with streaming output. The first argument is the flow instance's id: its kind for an ordinary flow, the member's own id for a flow declared cardinality: "collection" (whose bare kind is not found; the error lists the ids that are). A session stays with the instance that started it, so re-running it through another instance exits non-zero with the session unchanged. See the CLI reference.

# Inline JSON input
fsdev run knowledge-base-agent answerQuestion \
  -i '{"question": "What is RAG?", "topK": 5}'

# Input from file, reuse a session
fsdev run market-intel-agent runStrategy \
  -f ./test-inputs/strategy.json \
  --session sess_abc123

# Override the model for generators run in this process
fsdev run my-agent chat -i '{"message": "hi"}' --model gpt-5

# Seed session state before execution
fsdev run support-triage triageTicket \
  -i '{"ticketId": "T1"}' \
  --seed-session '{"openTicketCount": 3}'

Options:

| Flag | Description | |------|-------------| | -i, --input <json> | Inline JSON input | | -f, --input-file <path> | JSON input from file | | -m, --model <model> | Override model for generator blocks run in this process | | -s, --session <id> | Session ID for reuse across invocations | | --seed-session <json\|path> | Seed session-level state (JSON or file path) | | --flow-dir <path> | Override flow discovery root (repeatable) | | --dotenv <path> | Load a specific .env file before the cwd walk-up (repeatable, resolved from cwd) | | --quiet | Suppress [flow-state] * runtime logs on stderr | | --log-level <level> | Stderr log level: debug \| info \| warn \| error (default: info) | | --capture <path> | Write the full structured run output to a JSON file (additive with stdout) |

Stderr runtime logs

By default fsdev run emits [flow-state] * runtime events to stderr at info level — action lifecycle, block lifecycle, retries, errors. They are separate from the NDJSON stream on stdout, so piping stdout to jq works without filtering. Pass --quiet to suppress them entirely; pass --log-level debug to include nested-block events.

Capture mode

--capture <path> writes a single JSON file with the full run for later inspection:

{
  "command": { "flow": "...", "action": "...", "input": {...}, "model": null, "session": null, ... },
  "events":  [ /* every NDJSON event in order */ ],
  "result":  { "success": true, "flow": {...}, "output": {...}, "execution": {...}, "exitCode": 0 }
}

Stdout NDJSON streaming continues unchanged when --capture is set — you get both. Parent directories are created as needed.

NDJSON streaming

Events stream to stdout as blocks execute, one JSON object per line:

{"type":"item_added","item":{"id":"...","type":"message","role":"assistant"}}
{"type":"content_delta","itemId":"msg_1","delta":"Hello"}
{"type":"content_delta","itemId":"msg_1","delta":" there!"}
{"type":"state_change","scope":"session","resourcePath":"counter","changeType":"update"}
{"type":"flow_complete","output":{"reply":"Hello there!"},"durationMs":1234,"items":3}

Event types:

| Type | Description | |------|-------------| | item_added | New output item created | | item_updated | Patch applied to an existing item (e.g. a trace gaining modelUsage) | | item_done | Item reached its terminal state; carries the finalized item | | content_delta | Incremental content chunk for an item | | state_change | Scope state or resource was modified | | flow_complete | Action completed successfully | | error | Action failed |

Session reuse

Pass --session to persist state between invocations:

# First run — counter starts at 0
fsdev run stateful increment -i '{"increment": 1}' --session my-session
# → {"count": 1}

# Second run — counter continues from 1
fsdev run stateful increment -i '{"increment": 1}' --session my-session
# → {"count": 2}

Error messages

When a flow or action isn't found, the error lists available options:

Flow "chat" not found. Available flows: echo, stateful, knowledge-base-agent
Searched: src/flows/, flows/

fsdev dev — Start the DevTool dev server

Starts an HTTP server serving both the flow API and the DevTool UI. Discovers flows, registers them, and opens your browser.

# Default: port 4200
fsdev dev

# Custom port, model override
fsdev dev --port 3000 --model openai/gpt-5.4-mini

# Specific flow directory, no browser
fsdev dev --flow-dir ./my-flows --no-open

Options:

| Flag | Description | |------|-------------| | -p, --port <port> | Port to listen on (default: 4200) | | --flow-dir <path> | Override flow discovery root (repeatable) | | --dotenv <path> | Load a specific .env file before the cwd walk-up (repeatable, resolved from cwd) | | -m, --model <model> | Override model for generator blocks run in this process | | --no-open | Don't open the browser automatically |

Requires @flow-state-dev/devtool to be installed (provides the pre-built UI assets). The CLI lists it as an optional peer dependency.

fsdev serve — Start a production server

Starts an HTTP server for the flow API and MCP endpoints, with no DevTool UI. The production counterpart to fsdev dev. It requires a committed fsdev.config.* that default-exports a FlowState; without one it errors (no directory discovery, no --flow-dir, no --no-config).

# Bind $HOST (then 0.0.0.0) on $PORT (then 3000)
fsdev serve

# Override the port from the environment
PORT=8080 fsdev serve

# Explicit config file
fsdev serve --config ./fsdev.config.ts

Options:

| Flag | Description | |------|-------------| | -p, --port <port> | Port to listen on (default: $PORT, then 3000) | | --host <host> | Host to bind (default: $HOST, then 0.0.0.0) | | --config <path> | Load an explicit fsdev.config file instead of searching the cwd | | --dotenv <path> | Load a specific .env file before the cwd walk-up (repeatable, resolved from cwd) | | --allow-unauthenticated | Skip the loopback-bind guard on a network bind |

Before binding a non-loopback host, fsdev serve refuses to start when a served flow has no authentication configured (its authentication.resolvePrincipal is unset or the framework default). Fix it by configuring resolvePrincipal, binding a loopback host (--host 127.0.0.1), or passing --allow-unauthenticated. Unlike fsdev dev, this command does not require @flow-state-dev/devtool — it never mounts the UI. It shuts down gracefully on SIGTERM/SIGINT.

fsdev chat — Hold an interactive session over a flow

Opens a persistent terminal REPL over your flows. Type a message and it routes to the default target, streaming the reply back; a /-prefixed line runs a built-in command. Runtime resolution matches fsdev run (config wins over discovery).

# Bind a flow + action and start chatting
fsdev chat hello-chat chat

# Start unbound, then pick a target with /use
fsdev chat

# Resume a session under a specific identity
fsdev chat hello-chat chat --user devuser --session sess_abc

Options:

| Flag | Description | |------|-------------| | -s, --session <id> | Resume an engine session for the initially bound flow | | -m, --model <model> | Override model for generator blocks run in this process | | -u, --user <id> | Engine identity for sessions and turns (default: cli-user) | | --flow-dir <path> | Override flow discovery root (repeatable) | | --dotenv <path> | Load a specific .env file before the cwd walk-up (repeatable, resolved from cwd) | | --quiet / --log-level <level> | Stderr runtime-log discipline (default level warn) |

Built-in commands: /help, /targets, /use <flow> [action], /status, /session [new|<id>], /exit. A /name no built-in claims is sent to the flow as chat text (how skills are invoked). Messages are sent as { message: "<text>" }. Sessions are kept per flow kind and persist in the engine's stores.

fsdev block — Execute a single block in isolation

Runs a block outside of a flow using the testing harness. Useful for development and debugging.

# Execute a handler block
fsdev block ./src/flows/my-app/blocks/counter.ts \
  -i '{"increment": 1}'

# Override model for a generator block
fsdev block ./src/blocks/summarizer.ts \
  -i '{"text": "..."}' \
  -m gpt-5

Options:

| Flag | Description | |------|-------------| | -i, --input <json> | Inline JSON input | | -f, --input-file <path> | JSON input from file | | -m, --model <model> | Model override for generator blocks | | --format <format> | Output format (default: json) |

Output is a JSON object with execution results, schema validation status, and timing:

{
  "success": true,
  "block": { "kind": "handler", "name": "counter" },
  "output": { "count": 1 },
  "schemaValidation": {
    "input": { "passed": true },
    "output": { "passed": true }
  },
  "execution": { "durationMs": 12 }
}

fsdev gen — Register an app's kinds, blocks and resource modules from its files

Walks workforce/flows/workers/, workforce/flows/channels/ and workforce/blocks/, and writes workforce/workforce.gen.ts beside them. The generated module exports kinds, channelKinds and blocks: the maps hireWorkforce, channelInstances and a task board already take. Each discovered file registers under its basename.

It also walks every resources/ folder the workforce convention reads — the organisation's, each team's, and each worker's own — and exports a fourth map, resourceModules. A resources/ folder takes Markdown documents and TypeScript modules side by side: the .md files are documents, and a .ts file there is a capability or a resource. Each module registers under the same ref a document of that name in that folder would get, so a document and a module cannot share one — the command refuses the pair by name rather than letting one quietly win.

# Write the module from the tree
fsdev gen

# Fail instead of writing when the committed file is out of date
fsdev gen --check

Options:

| Flag | Description | |------|-------------| | --root <dir> | The workforce directory (default: workforce) | | --check | Compare against the committed file and exit non-zero on a difference |

Loads no app code — not your fsdev.config.ts, and not one file it walked. It reads the tree, so the names it can refuse are the ones a walker can see: an illegal basename, a directory inside one of the three folders, one basename claimed by both flow folders, one ref claimed by both a document and a module, and a folder that is present and unreadable. Every refusal is collected, so one run names all of them and nothing is written.

What a discovered file exports is checked by your own tsc against the generated maps, not by the command. A module exporting neither a capability nor a resource fails there, naming its file — as does a capability written inside a single worker's own resources/ folder, which is not allowed because every seat of a worker kind shares that kind's capabilities.

Commit the generated file and run fsdev gen in front of your build. Give --check its own CI step: inside a build script it would regenerate the file first and always pass.

fsdev benchmark — Compare coordination patterns

Loads a defineBenchmark(...) file, runs each pattern (plus a single-generator baseline) against the same task suite on the same model, and prints a comparative scorecard. One independent variable: the coordination shape. A blinded judge (a distinct model) scores every output against each task's locked rubric.

Real runs make real model calls and need provider credentials in the environment (OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY).

# Default suite, table scorecard
fsdev benchmark ./benchmark.ts

# Markdown table, capped spend
fsdev benchmark ./benchmark.ts --format markdown --max-cost 0.50

# A subset of patterns, one category, written to a file
fsdev benchmark ./benchmark.ts \
  --patterns supervisor,debate \
  --category reasoning \
  --output results.json --format json

# Cross-model: run the patterns on Haiku and compare them against pure Haiku
# AND pure Sonnet ("does a Haiku swarm beat raw Sonnet?")
fsdev benchmark ./benchmark.ts \
  --model anthropic/claude-haiku-4-5 \
  --baseline-model anthropic/claude-sonnet-4-6

--baseline-model adds a pure single-generator baseline on that model; the run model is always included as the same-model baseline (the delta reference). The other baselines appear as their own rows, so you read whether the patterns beat them from the absolute scores.

Options:

| Flag | Description | |------|-------------| | -m, --model <model> | Override the executor model for all subjects | | --judge-model <model> | Override the judge model | | --runs <n> | Repetitions per (subject, task) | | --concurrency <n> | Concurrent (subject, task, run) cells | | --category <name> | Only run tasks in this category | | --patterns <names> | Comma-separated subset of pattern names to run | | --baseline-model <model> | Add a pure-model baseline to compare against (repeatable; the run model is always included) | | --no-baseline | Skip the single-generator baseline subject | | --max-cost <usd> | Abort the sweep when the estimated cost exceeds this | | --output <path> | Write the scorecard to a file instead of stdout | | --format <format> | table | markdown | json (default: table) |

Cost is tracked best-effort. When --max-cost is exceeded the sweep stops, prints a partial scorecard, and the command exits 1 so CI notices.

A table scorecard puts subjects in rows, categories plus overall in columns, and mean±stddev of the judge score (0-1) in each cell:

subject           reasoning     multi-step-research  overall
supervisor        0.840±0.060   0.910±0.040          0.875±0.058
debate            0.870±0.090   0.800±0.080          0.835±0.091
single-generator  0.720±0.070   0.690±0.090          0.705±0.083

See the Benchmarks docs for the methodology and the walkthrough guide for a worked example.

Environment variables

fsdev run, fsdev dev, fsdev serve, fsdev chat, and fsdev benchmark load .env.local before importing your config, so generator providers see your gateway and API keys. fsdev serve also reads $HOST and $PORT for its bind defaults.

Resolution, highest precedence first:

  1. The real shell environment. Anything already exported always wins — a file never overwrites it.
  2. Files named with --dotenv <path>, in the order given.
  3. .env.local in the working directory, then each parent directory up to the filesystem root.

The auto walk-up only climbs, so running from the repo root will not find an app's .env.local one level down. Point at it explicitly:

# from the repo root, load the app's env:
fsdev run my-flow action -i '{}' --dotenv apps/my-app/.env.local

--dotenv is repeatable and resolved relative to cwd (absolute paths work too). A named file that doesn't exist is an error, unlike the silent walk-up. The flag is --dotenv rather than --env-file because Node and tsx reserve --env-file as a built-in flag and would intercept it before the CLI sees it.

Flow discovery

Flows are discovered from conventional directories relative to the working directory:

src/flows/<flow-name>/flow.ts   → default exports a FlowInstance
flows/<flow-name>/flow.ts       → default exports a FlowInstance
flows/<flow-name>.ts            → direct file export

In monorepo structures, the CLI also scans one level of subdirectories under packages/, examples/, apps/, and labs/:

packages/*/src/flows/<flow-name>/flow.ts
packages/*/flows/<flow-name>/flow.ts
examples/*/src/flows/<flow-name>/flow.ts
apps/*/src/flows/<flow-name>/flow.ts
labs/*/src/flows/<flow-name>/flow.ts

Use --flow-dir to override default discovery with explicit paths:

# Search only specific directories
fsdev run my-flow action -i '{}' --flow-dir ./packages/api/src/flows --flow-dir ./shared/flows

Each module must default-export a FlowInstance created by defineFlow(...)({ id: "..." }). When the same flow kind is found in multiple directories, the first discovery wins.

A module that throws during import doesn't abort discovery: the CLI prints a Warning: failed to import flow module: <path> diagnostic to stderr and lists the failure in the "not found" error, so a broken flow is distinguishable from a missing one.

Using fsdev.config.ts

Directory discovery covers a simple app whose providers are env-keyed. An app with intent-mapped models, a gateway, or a custom store adapter keeps that wiring in its createFlowState call. Put a fsdev.config.ts at your project root that default-exports that same FlowState handle, and fsdev run, fsdev dev, and fsdev chat use your registry, stores, and model resolver instead of CLI defaults.

The CLI searches the current directory for fsdev.config.{ts,mts,js,mjs} (TS first). Pass --config <path> to point at an explicit file, or --no-config to ignore any config and force directory discovery. With a config loaded, --model is routed through your resolver, and --flow-dir is rejected (use --no-config if you wanted directory discovery).

import { createFlowState, inMemoryStores } from "@flow-state-dev/engine";
import chatFlow from "./src/flows/chat/flow";

export default createFlowState({
  flows: { chat: chatFlow },
  models: { default: "openai/gpt-5.4-mini" },
  stores: { default: { primary: inMemoryStores() } },
});

A .ts config needs Node >= 22.18 (native type stripping) or tsx in a consumer repo; an .mjs/.js config works everywhere. See App Configuration for the full convention, runtime requirements, and caveats.

Programmatic API

The CLI exports its core utilities for use in scripts, CI, and tooling:

import {
  discoverFlows,
  resolveFlow,
  isFlowInstance,
  resolveBlock,
  isBlockDefinition,
  parseInputArg,
  formatOutput,
} from "@flow-state-dev/fsdev";

import type { FlowRunResult, FlowEvent, BlockExecResult } from "@flow-state-dev/fsdev";

discoverFlows accepts an onImportFailed callback in its options object, invoked with a FlowImportFailure (filePath, message, cause) for each module that throws during import. Discovery continues with remaining modules; without the callback, failures are skipped silently.

The next-steps block

The install-fsd skill prints this block after it wires a project. So does a scaffolder that embeds CANONICAL_NEXT_STEPS. The block names the servers, what each is for, the ports, and the caveats.

import {
  CANONICAL_NEXT_STEPS,
  renderNextSteps,
  assertCanonicalNextSteps,
} from "@flow-state-dev/fsdev";

renderNextSteps({
  topology: "mounted-route", // or "second-process"
  packageManager: "pnpm", // npm | pnpm | yarn
  devScript: "serve",
  devUrl: "http://localhost:4000",
  mountPath: "/api/flows",
});

CANONICAL_NEXT_STEPS is the source: one text with two conditional branches and six named placeholders. A tool embeds it verbatim in its own source, renders the branch its host shape needs, and calls assertCanonicalNextSteps on its embedded copy from its own tests — that is what keeps two tools from drifting apart on what they tell a developer. Embed both branches even if you only ever render one; the comparison reads the whole block.

renderNextSteps throws rather than printing an unfilled placeholder, and refuses a package manager it has no command forms for. The second-process branch needs neither a dev script nor a mount path, so a project without one is not an error. devScript is shell-quoted when it needs to be, so a script name carrying a space or a metacharacter still renders as one argument.

Dependencies

  • @flow-state-dev/core — block/flow type definitions
  • @flow-state-dev/engine — execution engine, stores, streaming
  • @flow-state-dev/testing — isolated block execution context
  • commander — CLI framework
  • @flow-state-dev/devtool (optional peer) — pre-built DevTool UI assets for fsdev dev only; fsdev serve does not need it

Scripts

pnpm --filter @flow-state-dev/fsdev build
pnpm --filter @flow-state-dev/fsdev typecheck
pnpm --filter @flow-state-dev/fsdev test

Architecture reference

  • Flows — defineFlow, actions, lifecycle
  • Blocks — The four block kinds
  • Streaming — Item/content model, event taxonomy