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

@formfactory-dev/workflows

v0.8.2

Published

Runtime SDK for Form Factory workflow scripts (agent, sandbox, worktree).

Readme

@formfactory-dev/workflows

Runtime SDK for Form Factory workflow scripts. Agent providers, sandbox providers, worktree management, and the runner for claude invocations with idle timeouts and JSONL session capture.

Install

pnpm add -D @formfactory-dev/workflows

Requires Node.js 24+.

ff init adds this as a dev dependency automatically. Install directly only when authoring workflows outside an ff init repo.

Usage

A workflow is a TypeScript file at .ff/workflows/<name>.ts that imports the SDK and orchestrates one or more agent runs:

import {
  claudeCode,
  createWorktree,
  docker,
  noSandbox,
} from "@formfactory-dev/workflows"

const [ticket] = process.argv.slice(2)
const wt = await createWorktree({
  branch: ticket.toLowerCase(),
  baseBranch: "main",
})

try {
  await wt.run({
    agent: claudeCode({ model: "claude-opus-4-7" }),
    sandbox: process.env.FF_SANDBOX === "docker" ? docker() : noSandbox(),
    promptFile: ".ff/prompts/work.md",
    promptArgs: { TICKET: ticket },
    idleTimeoutSeconds: 600,
  })
} finally {
  await wt.cleanup()
}

Run it: pnpm run work PROJ-1 (ff init writes the script entry). For the scaffolding flow see @formfactory-dev/cli's README.

Everything is exported from the package root — import { ... } from "@formfactory-dev/workflows". GitHub helpers (listMyOpenPrs, fetchUnresolvedThreads, etc.) live in @formfactory-dev/toolkit.

API reference

run(options)

Render a prompt, spawn an agent, watch the idle timer, capture the JSONL session into ~/.claude/projects/<encoded-cwd>/<id>.jsonl so claude --resume <id> works.

type RunOptions = {
  agent: AgentProvider // claudeCode({ ... })
  sandbox?: SandboxProvider // docker() | noSandbox(); default noSandbox()
  cwd?: string // default process.cwd()
  repoRoot?: string // for sandbox env forwarding; default = cwd
  prompt?: string // inline (no substitution)
  promptFile?: string // file with {{KEY}} placeholders
  promptArgs?: Record<string, string | number | boolean>
  idleTimeoutSeconds?: number // kill after N seconds with no output
  signal?: AbortSignal
  dryRun?: boolean // render argv without spawning
  name?: string // labels stderr lines as `[name] ...` and is
  // forwarded as the third arg to onText/onToolCall
  onText?: (text: string, name?: string) => void
  onToolCall?: (toolName: string, argSummary: string, name?: string) => void
}

Returns either { kind: "dry-run", prompt, argv, stdin? } or { kind: "ran", exitCode, session?, stdout, commits, usage? }.

  • stdout: string — assembled prose output. The agent's final result event wins when seen; otherwise the runner concatenates streamed text events. Workflows that need to parse structured output (e.g. a planner emitting JSON wrapped in <plan>...</plan>) read this.
  • commits: { sha: string }[] — commits added to cwd's HEAD during the run, oldest first.
  • usage?: TokenUsage{ inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens } from the last assistant event in the session JSONL. Undefined when the session wasn't captured.

run() throws tagged errors on failure (instanceof-checkable):

  • IdleTimeoutError — no agent output for idleTimeoutSeconds. A warning: agent idle for N minute(s) line is written to stderr each minute before the kill (suppressed when onText/onToolCall callbacks are set).
  • AgentExitError — non-zero exit code; carries .exitCode.
  • PromptResolutionError — bad options (both/neither of prompt/promptFile, missing prompt file, non-positive idleTimeoutSeconds).
  • The AbortSignal's reason propagates verbatim on abort — run() does not wrap it.

interactive(options)

TUI takeover — hands stdin/stdout/stderr to the agent. Defaults to noSandbox() for the common "ask Claude something quick on this checkout" case.

createWorktree(options)

Provision a git worktree and get back a handle for chained runs:

const wt = await createWorktree({
  branch: "proj-1",
  baseBranch: "main",
  copyToWorktree: ["node_modules"],   // optional; staged before any agent run
})
try {
  await wt.run({ agent: claudeCode(...), sandbox: docker(), promptFile: "..." })
  await wt.run({ ... })
} finally {
  await wt.cleanup()
}

The handle exposes path, branch, reused, run(opts), interactive(opts), cleanup(). reused is true when an existing worktree was attached to instead of created.

claudeCode(options)

claudeCode({
  model?: string              // --model flag
  maxTurns?: number           // --max-turns
  binPath?: string            // default "claude" (PATH lookup)
})

The runner pipes the rendered prompt to the agent's stdin instead of passing it as a -p value — Claude's CLI (like all commander-style parsers) rejects values starting with -/--/---, which YAML-frontmatter prompts do.

detectRepo(options?)

detectRepo({
  cwd?: string                // default process.cwd()
  remote?: string             // default "origin"
})

owner/repo slug from the named git remote, falling back to the cwd basename when the remote is missing or unparseable. Handy for {{REPO}} in prompt templates.

docker(options) / noSandbox()

docker({
  image?: string              // when omitted, lazily build from .ff/sandbox/Dockerfile
  network?: string            // default = docker bridge
})

When options.image is omitted, docker() builds an image from <repoRoot>/.ff/sandbox/Dockerfile on first use and tags it ff-workflow-sandbox:<sha12> (sha of the full Docker build context — Dockerfile plus every sibling file it could COPY/ADD). Edits to any context file produce a new tag and a fresh build automatically.

The policy is locked down inside the SDK (non-root user, all caps dropped, no new privileges; bind-mount matches host path so claude --resume works on the host).

GitHub helpers

Live in @formfactory-dev/toolkit. All shell out to gh / gh api graphql, reusing the operator's existing auth.

import {
  listMyOpenPrs,
  fetchUnresolvedThreads,
  replyToThread,
  resolveThread,
  addReaction,
} from "@formfactory-dev/toolkit"

const prs = await listMyOpenPrs()
const threads = await fetchUnresolvedThreads(prs[0].number)
await replyToThread(threads[0].id, "Addressed in <sha>: <one-liner>")
await addReaction(threads[0].comments[0].id, "+1")
// Optional — `revise.md` defaults to leaving threads open as audit trail.
await resolveThread(threads[0].id)

These power the bundled revise.ts workflow. gh calls bound to a 30 s timeout; reviewThreads paginate; listMyOpenPrs sets --limit 1000 (gh defaults to 30).

Prompt template substitution

When promptFile is set, {{KEY}} placeholders are replaced with values from promptArgs. Inline prompt: is passed through verbatim — combining promptArgs with inline prompt is rejected.

Live progress

By default the runner mirrors Claude's text to stderr and prints → <ToolName> <preview> per tool call (per-tool display field: Bash → command, Read → file_path, etc.; truncated at 200 chars).

Set RunOptions.name to prefix every line [name] … for parallel runs sharing a terminal. The value is sanitised (ANSI + control characters stripped), so name is safe to derive from external strings like Jira ticket titles.

Pass onText / onToolCall to take over rendering. Either hook suppresses the default printer and receives name as a third argument. JSONL capture to ~/.claude/projects/... runs regardless.

Sandbox auth

Create <repoRoot>/.ff/.env (gitignored) with either:

CLAUDE_CODE_OAUTH_TOKEN=   # from `claude setup-token` — reuses your subscription
ANTHROPIC_API_KEY=         # pay-as-you-go alternative

Empty values fall through to process.env. Keys present in the file form the allowlist of vars forwarded into the sandbox.

Publishing

Published via pnpm publish from GitHub Actions using OIDC (the npm scope is configured as a "trusted publisher" tied to this repo and the release workflow — local publishes are rejected). pnpm publish rewrites catalog: and workspace: protocol references in package.json to concrete version ranges before upload, so consumers installing via npm, yarn, pnpm, or bun see normal version strings.

License

MIT — see LICENSE.