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

@tutorflow/cli

v0.5.0

Published

TutorFlow CLI - agent-friendly command line for the TutorFlow Content API: generate courses, slides, tests, modules, videos, games and simulations, grade learner answers, and manage learners and webhooks. Supports --json output, env-var auth, --wait polli

Readme

@tutorflow/cli

The TutorFlow command line for developers, CI jobs and AI agents. It creates courses, slide decks, tests, interactive modules, videos, games and simulations, grades learner answers, and manages learners and webhooks through the TutorFlow Content API (/v1/content/*). Account, billing and discovery commands use the Agent Platform account routes (/v1/platform/agent/*).

  • Binary: tutorflow
  • Package: @tutorflow/cli (current version in package.json)
  • Source: tutorflow-io/cli (private)
  • Node: >= 18 at runtime, >= 20 for development (Vitest 4)
  • Agent-friendly: --json output, env-var auth, no prompts without a TTY, stable error codes and exit codes. See Agent usage.

Upgrading from 0.3.x? Content commands now need a tf_content_ key and call the Content API; tf_platform_ keys are refused there with EPLATFORMKEY. Generation is asynchronous: add --wait to get the finished resource. See Migrating from 0.3.

Quick start

npm install -g @tutorflow/cli        # or: npx @tutorflow/cli <command>

# 1. Create an agent account. The tf_content_ key is saved to ~/.tutorflow/config.json.
tutorflow agent register --name "My Agent" --slug my-agent --email [email protected]

# 2. Generate content into the account's default classroom and wait for it.
tutorflow course generate "Beginner Python for analysts" --lessons 3 --wait
tutorflow slide generate "Photosynthesis" --count 8 --wait
tutorflow test generate "Algebra basics" --items 5 --wait

# 3. Grade an answer and read the learner model it builds.
tutorflow evaluate --type open-ended --question "Why is the sky blue?" \
  --student-answer "Air scatters blue light more." --learner-id student-7 --concept-tags light,atmosphere
tutorflow evaluation mastery student-7

# 4. Check what it cost.
tutorflow credits balance

Keys and classrooms

| Key prefix | Works with | Notes | | --- | --- | --- | | tf_content_… | every command | Returned by agent register as contentApiKey, or created in the Agent Platform dashboard. | | tf_content_test_… | content commands | Reaches only the sandbox classroom, returns canned results and spends nothing. Learner routes refuse test keys. | | tf_platform_… (deprecated) | agent, platform | Refused by content commands with EPLATFORMKEY. The Platform content routes it served are being removed. |

Every content command acts in one classroom, chosen in this order:

  1. --classroom <id> (a global flag; works before or after the subcommand)
  2. TUTORFLOW_CLASSROOM_ID
  3. classroomId in ~/.tutorflow/config.json (tutorflow config set classroomId <id>)
  4. GET /v1/platform/agent/account: sandboxClassroomId for a test key, defaultClassroomId otherwise
  5. GET /v1/content/classrooms when the account route refuses the key (a classroom Settings key): the only classroom, or the sandbox one for a test key; with several, the command exits 2 (ECLASSROOM) and lists them

tutorflow classroom list shows the classrooms the key can reach.

Commands

All ids are UUIDs. Every command accepts --json.

Account and discovery (/v1/platform)

| Command | Route | | --- | --- | | agent register --name --slug [--email] [--agent-identity] [--no-save] | POST /v1/platform/agent/register (public). Saves contentApiKey. Pass --email: verification is required before buying credits. | | agent account | GET /v1/platform/agent/account: balance, defaultClassroomId, sandboxClassroomId, Content API URLs | | agent billing status | GET /v1/platform/agent/billing/status | | agent billing session | POST /v1/platform/agent/billing/session: hosted checkout URL for a human (verified email) | | agent billing x402-deposit --amount-usd <n> | POST /v1/platform/agent/billing/x402/deposit: USDC deposit address on Base (verified email) | | agent checkout info --token / agent checkout create --token --amount-usd | GET / POST /v1/platform/agent/checkout (public) | | agent verify-email resend | POST /v1/platform/agent/verify-email/resend | | platform manifest / platform pricing | GET /v1/platform/.well-known/agent.json / GET /v1/platform/pricing-catalog (public) | | platform health | GET /v1/platform/health | | platform share-redirect <shareToken> | GET /v1/platform/share-redirects/{token} (public): where an old Platform share link moved |

Generated content (/v1/content/classrooms/{classroomId}/…)

| Command | Route | Credits | | --- | --- | --- | | course generate [prompt] [--lessons] [--language] [--level] [--title] [--description] | POST …/courses/generate | 3 + 3 per lesson | | slide generate [topic] [--count 5-20] [--theme] [--instructions] [--text-length concise\|detailed] | POST …/slides/generate | per page | | test generate [topic] [--items] [--item-types select,blank,…] [--level] [--total-score] | POST …/tests/generate | 3 | | module generate [topic] [--type markdown\|ai-tutor\|flashcard\|…] [--reference] | POST …/modules/generate | 3 |

generate (alias create) answers 202 with a run. Add --wait [--timeout <seconds>] to poll the run and print { run, resource } with the resource's editorUrl and publicUrl. Every generate accepts --idempotency-key.

Each of course, slide, test, module, video, game and simulation also has:

| Subcommand | Route | | --- | --- | | list [--limit] [--page] [--search] | GET …/{resource} (JSON mode prints the raw { data, meta } page) | | get <id> | GET …/{resource}/{id} | | open <id> [--editor] | prints editorUrl and publicUrl; opens one in a browser on a TTY | | delete <id> --yes | DELETE …/{resource}/{id} (--yes required without a TTY) | | share <id> [--listed] | PATCH …/{resource}/{id}: makes it open by link and prints publicUrl | | visibility <id> <value> | PATCH …/{resource}/{id} with that visibility |

share sets the least open value that still opens by link, and visibility takes the values below (case does not matter). The PATCH answer carries the new publicUrl; JSON mode prints the whole answer.

| Resource | share sets | visibility values | | --- | --- | --- | | video, test | UNLISTED (opens by link, kept out of lists and search); --listed sets PUBLIC | PUBLIC, UNLISTED, PRIVATE | | slide, game, simulation | PUBLIC | PUBLIC, PRIVATE | | course | PUBLIC | PUBLIC, PRIVATE, ORGANIZATION, CLASSROOM, COURSE | | module | isPublic: true | PUBLIC, PRIVATE (written as isPublic) |

publicUrl can stay null after share: a video's watch page opens once it is rendered, a game or simulation's play page once it is built, a course page once the course is published, and the sandbox classroom of tf_content_test_ keys has no public pages. A test's public page shows its cover; answering needs a learner sign-in. New tests start PUBLIC.

tutorflow --json video share <videoId> | jq -r .publicUrl

test results <testId> lists learners' results (GET …/tests/{id}/results).

Runs

| Command | Route | | --- | --- | | run list [--kind course\|slide\|test\|module] [--status] [--limit] [--cursor] | GET …/runs | | run get <runId> | GET …/runs/{runId} | | run wait <runId> [--timeout] | polls GET …/runs/{runId}; prints { run, resource } | | run resume <runId> | POST …/runs/{runId}/resume (course runs only) |

A failed run exits 1 with ERUNFAILED; a timeout exits 1 with ETIMEOUT and names the run wait command to continue. Polling rides out up to five transient 5xx, 429 or network failures in a row.

Videos

The Content API builds videos from scenes you script; it does not write the script from a prompt.

| Command | Route | | --- | --- | | video create --title [--description] [--visibility PUBLIC\|UNLISTED\|PRIVATE] [--scene "<script>"]… | POST …/videos, then one scene per --scene in order | | video scene add <videoId> [--script] [--display-text] [--duration] [--after <sceneId>] | POST …/videos/{id}/scenes, then PATCH the new scene | | video scene update <videoId> <sceneId> [--script] [--display-text] [--duration] | PATCH …/scenes/{sceneId} | | video scene delete <videoId> <sceneId> --yes | DELETE …/scenes/{sceneId} | | video narrate <videoId> [--scene <sceneId>] | POST …/narration (1 credit per scene) | | video render <videoId> [--generate-missing-narration] [--wait] | POST …/videos/{id}/render (5 credits; --wait polls until COMPLETED and prints the mp4 URL) | | video render-status <videoId> / video render-cancel <videoId> | GET …/render / POST …/render/cancel |

tutorflow video create --title "Why the sky is blue" \
  --scene "Sunlight is made of many colors." \
  --scene "Air scatters blue light the most, so the sky looks blue."
tutorflow video render <videoId> --generate-missing-narration --wait

Games and simulations

game and simulation share one lifecycle. The CLI always asks for the asynchronous answer (Prefer: respond-async), so --wait polls the resource's run route.

| Command | Route | Credits | | --- | --- | --- | | create --title [--description] [--visibility PRIVATE\|PUBLIC] | POST …/{games\|simulations} | 0 | | brief <id> [--reference <markdown>] [--wait] | POST …/{id}/brief | 1 | | build <id> [--wait] | POST …/{id}/build | game 40, simulation 20 (2D) or 30 (3D) | | revise <id> --feedback <text> [--wait] | POST …/{id}/revise | as build | | status <id> | GET …/{id}/run | |

Evaluations and learners

| Command | Route | | --- | --- | | evaluate --type --question --student-answer [--expected-answer] [--rubric-json] [--max-score] [--learner-id] [--concept-tags a,b] [--subject] [--idempotency-key] | POST …/evaluations (1 credit, synchronous) | | evaluation list [--learner-id] [--status] [--type] [--limit] [--cursor] / evaluation get <id> | GET …/evaluations | | evaluation concepts / evaluation mastery <learnerId> | GET …/concepts / GET …/mastery/{learner} | | learner list / get <id> / progress <id> | GET …/learners… (live keys only) | | learner invite --email [--name] [--locale] [--course <id>]… [--test <id>]… | POST …/learners/invitations | | learner remove <id> --yes | DELETE …/learners/{id} |

--type takes exact, rubric_short_answer or open_ended (aliases rubric, open-ended). exact needs --expected-answer; rubric_short_answer needs --rubric-json.

Webhooks and credits (/v1/content)

| Command | Route | | --- | --- | | webhook list / get <id> | GET /v1/content/webhooks[/{id}] | | webhook create --url --event <event>… [--mode live\|test] | POST /v1/content/webhooks (the signing secret is shown once) | | webhook test <id> / deliveries <id> / rotate-secret <id> / delete <id> --yes | the matching /v1/content/webhooks/{id}/… routes | | credits balance | GET /v1/content/credits | | credits history [--limit] [--cursor] [--from] [--to] [--type] | GET /v1/content/credits/history |

Webhook management needs the webhooks:manage scope, which the key from agent register has. Events include course.generation.completed, slide.generation.completed, test.generation.completed, module.generation.completed, video.render.completed, game.build.completed, simulation.build.completed and their .failed twins.

Auth and config

| Command | Effect | | --- | --- | | auth login --api-key <key> | Save a key (prompts on a TTY when omitted) | | auth whoami | Masked key, keyKind (content_live, content_test, platform, unknown), base URL, source (env or config). No network call. | | auth logout --force | Delete ~/.tutorflow/config.json | | config get [key] / config set <key> <value> | Keys: apiKey, baseUrl, classroomId |

The config file is written with mode 0600.

Agent usage

export TUTORFLOW_API_KEY=tf_content_xxx
export TUTORFLOW_JSON=1

run=$(tutorflow course generate "Fractions for 5th grade" --lessons 2)
tutorflow run wait "$(echo "$run" | jq -r .runId)" | jq -r .resource.publicUrl

To hand the work to a person, make it open by link and send them publicUrl: tutorflow course share <courseId> | jq -r .publicUrl.

  • Output: with --json or TUTORFLOW_JSON=1, success is one JSON document on stdout and errors are JSON on stderr. Progress lines (text mode only) go to stderr.
  • No prompts without a TTY: a missing required input exits 2 with ENOINTERACTIVE and names the flag to pass. Destructive commands need --yes.
  • Retries: create and generate commands accept --idempotency-key.
  • Errors: {"error":{"message","code",…}}. API errors add status, apiCode (the Content API's code, e.g. content_payment_required), requestId, and when useful hint, retryAfterSeconds, requiredScope, validation, billingSessionUrl, x402.

| Exit | Meaning | | --- | --- | | 0 | Success | | 1 | Failure: auth, network, API error, failed run, timeout | | 2 | Bad input: invalid flag, missing input without a TTY, ambiguous classroom | | 130 | Cancelled at a prompt |

| Code | When | | --- | --- | | EAUTH | No API key | | EPLATFORMKEY | A tf_platform_ key on a content command | | ECLASSROOM | No classroom could be chosen; pass --classroom | | EINVAL | Invalid flag value | | ENOINTERACTIVE | Missing input or confirmation without a TTY | | EHTTP_<status> | API error; see apiCode | | ENETWORK | The API could not be reached | | ERUNFAILED | A waited run, build or render failed | | ETIMEOUT | --wait gave up; the work continues on the server | | ENOPREVIEW | open found no URL | | ENOTFOUND | config get of an unset key | | ECANCELLED | Prompt cancelled |

| Environment variable | Effect | | --- | --- | | TUTORFLOW_API_KEY | API key (wins over the config file) | | TUTORFLOW_BASE_URL | API origin, e.g. https://dev-api.tutorflow.io for staging | | TUTORFLOW_CLASSROOM_ID | Classroom for content commands | | TUTORFLOW_JSON=1 | JSON mode | | TUTORFLOW_DEBUG=1 / DEBUG=1 | Stack traces on errors | | TUTORFLOW_POLL_INTERVAL_MS | --wait poll interval (default 3000) |

Empty environment variables are ignored.

Migrating from 0.3

| 0.3 | Now | | --- | --- | | tf_platform_ key on every command | tf_content_ key; agent register saves it. Platform keys still work on agent and platform. | | /v1/platform/{courses,slides,tests,modules,videos,evaluations} | /v1/content/classrooms/{classroomId}/… | | course create returned the finished course (mode: sync) | course generate (alias create) returns a run; add --wait | | course create -l 10 -lang ko | course generate --lessons 10 --language ko | | slide create -t <theme> | slide generate --theme <themeId> (5 to 20 pages) | | video create "<prompt>" -s 6 -r 16:9 -d 90 | video create --title … --scene "<script>"…, then video render --generate-missing-narration --wait | | module create --title --target --locale --private --no-quiz | module generate [--type] [--reference]; edit the rest in the classroom editor | | test create --classroom-id --tier --mode --time-limit | test generate with the global --classroom; generation is always asynchronous | | test edit-token, previewUrl | editorUrl and publicUrl on every resource; open <id> | | list --offset | --page | | evaluate → /v1/platform/evaluations | evaluate → …/evaluations, plus --learner-id and --concept-tags |

New in this version: classroom, run, game, simulation, evaluation, learner, webhook, credits, video scene|narrate|render, agent billing x402-deposit, platform share-redirect, delete on every resource, and the global --classroom.

Development

git clone https://github.com/tutorflow-io/cli.git && cd cli
pnpm install
pnpm build && pnpm test       # the e2e suites spawn dist/index.js
pnpm dev -- course list       # run from source
pnpm link                     # expose `tutorflow` globally

Try it against staging with a throwaway account:

export HOME=$(mktemp -d) TUTORFLOW_BASE_URL=https://dev-api.tutorflow.io
node dist/index.js agent register --name "CLI test" --slug cli-test-$(date +%s)
node dist/index.js course generate "Water cycle" --lessons 1 --wait

Layout

src/
├── index.ts               # commander entrypoint and global flags (--json, --debug, --classroom)
├── client.ts              # ApiClient: config resolution, HTTP, error envelope → CliError
├── config.ts              # ~/.tutorflow/config.json (mode 0600)
├── content/
│   ├── context.ts         # tf_content_ key check and classroom resolution
│   ├── resource.ts        # shared list/get/open/delete/generate subcommands
│   ├── sharing.ts         # shared share/visibility subcommands
│   └── runs.ts            # waiting on runs, game/simulation builds and renders
├── commands/              # one file per top-level command
├── utils/                 # runtime (JSON mode, CliError), output, args, keys
└── testing/harness.ts     # test-only: fake API server and CLI spawner

Releasing

Releases are automated by GitHub Actions on pushes to master / main. Auth uses npm OIDC Trusted Publishing — no NPM_TOKEN secret in CI.

git checkout master
git pull

npm version patch --no-git-tag-version  # 0.3.0 → 0.3.1 (or minor / major)
git push origin master  # Release workflow publishes if that version is not on npm yet

The Release workflow:

  1. Reads package.json name and version
  2. Skips npm publish if that exact version already exists
  3. Runs pnpm typecheck + pnpm build + pnpm test
  4. Publishes new versions with npm publish --access public (auth via OIDC)
  5. Creates a GitHub Release only for v*.*.* tag pushes

For full setup (npm org, Trusted Publisher config) see RELEASING.md.

Pre-release / beta

npm version prerelease --preid=beta --no-git-tag-version  # 0.3.1 → 0.3.2-beta.0
git push origin master

Pre-release versions publish to a dist-tag derived from the prerelease id (0.3.1-beta.0 publishes with --tag beta).

⚠️ Caveats & gotchas

These tripped us up during initial setup. Read before changing release infrastructure.

Versioning

  • Don't hardcode the version in src/index.ts. It reads version from package.json via resolveJsonModule. Hardcoding will silently lie to users via --version after every npm version bump.
  • The prepublishOnly script in package.json rebuilds dist/ before publish. Don't skip it.

npm Trusted Publishing (OIDC)

  • npm CLI must be 11.5.1+ for OIDC auth. npm also requires a modern Node 22 runtime for trusted publishing, so the workflow uses Node 22.x and invokes pnpm dlx npm@latest ... for registry checks and publish. Do not replace that with the runner's bundled npm unless you verify its version supports OIDC trusted publishing.

  • The Trusted Publisher binding at npmjs.com/package/@tutorflow/cli/access hardcodes:

    • GitHub org: tutorflow-io
    • Repo: cli
    • Workflow filename: release.yml
    • Environment: (none)

    Renaming the repo, moving the workflow file, or adding an environment: to the publish job will break OIDC auth until the binding is updated.

  • Local manual publishes still need a token (set NPM_TOKEN in .env or run npm login). Use a Granular Access Token with "Bypass two-factor authentication (2FA)" checked, otherwise CI/scripts get EOTP errors.

Provenance

  • --provenance is disabled in release.yml. npm only accepts provenance attestations from public GitHub repos. Add --provenance back to the npm publish command when this repo goes public.

Node + tooling

  • Vitest 4.x requires Node 20+ (uses node:util.styleText). CI tests Node 20 and Node 22, while release publishing runs on Node 22 for npm OIDC. The runtime CLI itself still supports Node 18 (declared in package.json#engines).

Tarball hygiene

  • tsconfig.json excludes **/*.test.ts and src/testing/ so test code doesn't end up in dist/ (and therefore not in the published tarball).
  • package.json#files is an allowlist (dist, README.md, LICENSE). Anything not listed is excluded.
  • Run npm pack --dry-run to inspect the tarball before publish.

Source maps and a private repo

The published tarball currently includes .js.map and .d.ts.map files which embed paths from the build environment. While the source itself isn't shipped, this leaks file structure. If that matters, set sourceMap: false and declarationMap: false in tsconfig.json before going public.

Stack

commander 11, @clack/prompts 0.7, undici 5, chalk 5, open 9; Vitest 4, TypeScript 5, tsx 3.