@tutorflow/cli
v0.3.1
Published
TutorFlow CLI - agent-friendly tool for creating AI-powered educational content (slides, videos, courses) and evaluating student responses. Supports --json output, env-var auth, and non-interactive use.
Maintainers
Readme
@tutorflow/cli
TutorFlow CLI for creating AI-powered educational content (slides, videos, courses) and evaluating student responses from the terminal or from AI agents. Wraps the TutorFlow Platform API (/v1/platform/*).
- Binary:
tutorflow - Package:
@tutorflow/cli(current version inpackage.json) - Source: tutorflow-io/cli (private)
- Node:
>= 18for runtime,>= 20for development (Vitest 4 requirement) - Agent-friendly: every command supports
--jsonoutput, env-var auth (TUTORFLOW_API_KEY), and proper exit codes — see Non-interactive / agent usage
Installation
A. End users (recommended)
Install globally with any package manager:
npm install -g @tutorflow/cli
pnpm add -g @tutorflow/cli
yarn global add @tutorflow/cliThen anywhere on the system:
tutorflow --version
tutorflow auth login
tutorflow agent register --name "My Agent Workspace" --slug my-agent-workspace --email [email protected]
tutorflow --json platform manifest
tutorflow slide create "My first deck"One-shot use without installing:
npx @tutorflow/cli slide create "My first deck"This is the recommended path for AI agents and CI environments — no install step, always pulls the latest published version.
B. Local development (this repo)
git clone https://github.com/tutorflow-io/cli.git
cd cli
pnpm install
pnpm build
pnpm link # exposes the `tutorflow` binary globallyVerify:
tutorflow --version # matches package.json
tutorflow --helpTo unlink later: npm unlink -g @tutorflow/cli.
Run from source without building:
pnpm dev -- <command> [options]
# e.g. pnpm dev -- slide list --limit 5Releasing
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 yetThe Release workflow:
- Reads
package.jsonname and version - Skips npm publish if that exact version already exists
- Runs
pnpm typecheck+pnpm build+pnpm test - Publishes new versions with
npm publish --access public(auth via OIDC) - 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 masterPre-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 readsversionfrompackage.jsonviaresolveJsonModule. Hardcoding will silently lie to users via--versionafter everynpm versionbump. - The
prepublishOnlyscript inpackage.jsonrebuildsdist/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.- GitHub org:
Local manual publishes still need a token (set
NPM_TOKENin.envor runnpm login). Use a Granular Access Token with "Bypass two-factor authentication (2FA)" checked, otherwise CI/scripts getEOTPerrors.
Provenance
--provenanceis disabled in release.yml. npm only accepts provenance attestations from public GitHub repos. Add--provenanceback to thenpm publishcommand 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 inpackage.json#engines).
Tarball hygiene
tsconfig.jsonexcludes**/*.test.tsso test files don't end up indist/(and therefore not in the published tarball).package.json#filesis an allowlist (dist,README.md,LICENSE). Anything not listed is excluded.- Run
npm pack --dry-runto 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.
Development
Run with tsx (no build step):
pnpm dev -- <command> [options]
# e.g. pnpm dev -- slide list --limit 5Build artifacts land in dist/ (entry: dist/index.js, declared in package.json#bin).
Non-interactive / agent usage
Every command supports the contract below so AI agents and CI scripts can use the CLI without a TTY.
Auth without prompts
# Option 1: env var (preferred for one-shot calls)
TUTORFLOW_API_KEY=tf_xxx tutorflow --json slide list
# Option 2: persist to ~/.tutorflow/config.json once
tutorflow auth login --api-key tf_xxx
tutorflow --json slide create "Photosynthesis for 5th graders" -n 12TUTORFLOW_API_KEY always wins over the config file. tutorflow auth whoami --json reports source: "env" | "config" so an agent can verify which credential is active.
Machine-readable output
--json (or TUTORFLOW_JSON=1) makes every command emit a single JSON line on stdout. Errors emit JSON to stderr.
$ tutorflow --json auth whoami
{"authenticated":true,"apiKeyPreview":"tf_xxxxxxx...","baseUrl":"https://api.tutorflow.io","source":"env"}
$ tutorflow --json auth whoami # without auth
# stdout: (empty)
# stderr: {"error":{"message":"Not authenticated","code":"EAUTH"}}
# exit: 1List commands return raw arrays:
$ tutorflow --json slide list --limit 3
[{"id":"...","title":"...","status":"ready","createdAt":"..."},...]Pipe into jq for downstream automation:
tutorflow --json slide list --status ready | jq -r '.[].id' | xargs -I{} tutorflow --json slide get {}Exit codes
| Code | Meaning |
| ----- | ----------------------------------------------------------------- |
| 0 | Success |
| 1 | Generic failure (auth, network, API error, etc.) |
| 2 | Bad input (invalid flag value, missing required input in non-TTY) |
| 130 | User cancelled an interactive prompt (Ctrl+C) |
The error JSON includes a stable code: EAUTH, EINVAL, ENOINTERACTIVE, ENETWORK, EHTTP_<status>, ENOPREVIEW, ENOTFOUND, ECANCELLED.
Required-input flags (no prompts)
Commands that have required input expose every prompt as a flag:
# self-register a platform workspace and save the returned API key
tutorflow --json agent register \
--name "Agent Workspace" \
--slug agent-workspace \
--email [email protected] \
--agent-identity "codex"
# slide / video / course create — accept positional [prompt]
tutorflow --json slide create "Photosynthesis" -n 12 -l en
tutorflow --json video create "Newton's laws" -s 6 -r 16:9 -d 90
tutorflow --json course create "Beginner Python" -l 10 -lang en
# evaluate — every input is a flag
tutorflow --json evaluate \
--type exact \
--question "What is 2+2?" \
--student-answer "4" \
--expected-answer "4"
# logout (skip confirmation)
tutorflow auth logout --forceIf a required input is missing and stdin is not a TTY, the command exits 2 with ENOINTERACTIVE and an error message naming the flag(s) to use. It never hangs waiting for input.
Globals
| Flag | Env var | Effect |
| --------------------------------- | -------------------------------- | ----------------------------------------------- |
| --json | TUTORFLOW_JSON=1 | JSON output to stdout, JSON errors to stderr |
| --debug | DEBUG=1 or TUTORFLOW_DEBUG=1 | Print stack traces on errors |
| --api-key <k> (auth login only) | TUTORFLOW_API_KEY | API key |
| (n/a) | TUTORFLOW_BASE_URL | Override API base URL (staging, self-hosted) |
MCP server
A native MCP server (so agents can call tools directly without shelling out) is on the roadmap. For now, the --json shell contract is the recommended integration path.
Authentication
Generate an API key in your TutorFlow workspace, then:
tutorflow auth login # interactive password prompt for API key
tutorflow auth whoami # prints masked API key + base URL
tutorflow auth logout # confirms then deletes ~/.tutorflow/config.jsonCredentials are persisted to ~/.tutorflow/config.json:
{
"apiKey": "tf_xxxxxxxxxxxxxxxxxxxx",
"baseUrl": "https://api.tutorflow.io",
"workspace": "optional-workspace-slug"
}The API key is sent as the Authorization: Bearer <apiKey> header on authenticated requests.
Config schema
interface IConfig {
apiKey?: string; // required for all non-auth/config commands
baseUrl?: string; // default: https://api.tutorflow.io
workspace?: string; // optional workspace slug
}Configuration commands
tutorflow config get # print full config (apiKey is masked)
tutorflow config get <key> # print one key as KEY=VALUE
tutorflow config set <key> <value> # update one key| Key | Type | Default | Notes |
| ----------- | -------- | -------------------------- | -------------------------------------- |
| apiKey | string | none | Set via auth login or config set |
| baseUrl | string | https://api.tutorflow.io | Override for staging / self-hosted |
| workspace | string | none | Reserved for future scoping |
config set rejects unknown keys.
Commands
All resource commands require authentication. Without an API key they print:
✗ Not authenticated. Run "tutorflow auth login" first.
tutorflow agent
Manage the agent-facing TutorFlow Platform account flow.
agent register
Public self-service registration. Creates a platform workspace through
POST /v1/platform/agent/register, returns the one-time API key, and saves it
to ~/.tutorflow/config.json by default so the next CLI command can run
without a separate auth login.
| Option | Type | Default | Description |
| ------------------------------ | -------- | ------- | -------------------------------------------- |
| --name <name> | string | — | Workspace display name |
| --slug <slug> | string | — | Unique lowercase slug |
| --email <email> | string | none | Contact email for verification and billing |
| --agent-identity <identity> | string | none | Optional agent identity string |
| --no-save | boolean | false | Print the API key but do not persist it |
tutorflow --json agent register \
--name "Agent Workspace" \
--slug agent-workspace \
--email [email protected] \
--agent-identity "codex"agent account
GET /v1/platform/agent/account. Shows workspace status, credit balance,
email verification state, rate limits, and restrictedActions.
tutorflow --json agent accountagent billing status
GET /v1/platform/agent/billing/status. Shows payment-method state, credit
balance, billing mode, email verification state, and restrictedActions.
tutorflow --json agent billing statusagent billing session
POST /v1/platform/agent/billing/session. Returns a TutorFlow-hosted checkout
URL that an agent should forward to a human operator.
tutorflow --json agent billing sessionagent verify-email resend
POST /v1/platform/agent/verify-email/resend. Resends the workspace email
verification message using the active API key.
tutorflow --json agent verify-email resendagent checkout info
GET /v1/platform/agent/checkout?token=<token>. Reads workspace info for a
TutorFlow checkout token returned through the hosted billing-session flow.
tutorflow --json agent checkout info --token checkout_123agent checkout create
POST /v1/platform/agent/checkout. Creates a Stripe Checkout URL from a
TutorFlow checkout token and amount. This is normally called by the hosted
checkout page; CLI support exists for complete automation and testing.
tutorflow --json agent checkout create --token checkout_123 --amount-usd 25tutorflow platform
Inspect public discovery metadata and validate the active Platform API key.
| Subcommand | Endpoint | Auth |
| ---------- | -------- | ---- |
| manifest | GET /v1/platform/.well-known/agent.json | none |
| pricing | GET /v1/platform/pricing-catalog | none |
| health | GET /v1/platform/health | Platform API key |
tutorflow --json platform manifest
tutorflow --json platform pricing
tutorflow --json platform healthtutorflow slide
Create and manage slide decks.
slide create [prompt]
Creates a slide deck synchronously (mode: 'sync').
| Argument / Option | Type | Default | Description |
| ----------------------- | --------------------- | ------- | ------------------------------------ |
| [prompt] | string (positional) | — | Prompted interactively if omitted |
| -n, --count <number> | string → int | 10 | Slide count (parsed via parseInt) |
| -l, --language <lang> | string | en | ISO 639-1 language code |
| -t, --theme <theme> | string | none | Theme identifier |
POST /v1/platform/slides body:
{
"prompt": "...",
"slideCount": 10,
"language": "en",
"theme": "...",
"mode": "sync"
}Output: id, status, previewUrl (if returned). After creation, prompts to open the preview URL in the browser.
tutorflow slide create "Photosynthesis for 5th graders" -n 12 -l en -t classicslide list
| Option | Type | Default | Description |
| ----------------------- | -------- | ------- | -------------------------- |
| -l, --limit <number> | string | 10 | Max rows to fetch |
| -s, --status <status> | string | none | Server-side status filter |
GET /v1/platform/slides?limit=<n>[&status=<s>]. Renders an ASCII table with ID (8-char prefix), Title, Status, Created.
slide open <id>
Fetches /v1/platform/slides/:id and opens previewUrl in the default browser via open.
tutorflow video
Create and manage AI-generated videos.
video create [prompt]
Creates a video synchronously (mode: 'sync').
| Argument / Option | Type | Default | Description |
| -------------------------- | --------------------- | ------- | ---------------------------------------- |
| [prompt] | string (positional) | — | Prompted interactively if omitted |
| -s, --scenes <number> | string → int | 5 | Scene count |
| -r, --ratio <ratio> | string | 16:9 | Aspect ratio (16:9, 9:16, 1:1, …) |
| -d, --duration <seconds> | string → int | 60 | Target duration in seconds |
| -l, --language <lang> | string | en | ISO 639-1 language code |
POST /v1/platform/videos body:
{
"prompt": "...",
"sceneCount": 5,
"aspectRatio": "16:9",
"targetDurationSeconds": 60,
"language": "en",
"mode": "sync"
}Output: id, status, sceneCount, previewUrl (if returned). Prompts to open in browser.
tutorflow video create "Intro to Newton's laws" -s 6 -r 16:9 -d 90 -l envideo list
| Option | Type | Default | Description |
| ----------------------- | -------- | ------- | -------------------------- |
| -l, --limit <number> | string | 10 | Max rows |
| -s, --status <status> | string | none | Server-side status filter |
GET /v1/platform/videos?limit=<n>[&status=<s>]. Table columns: ID, Title, Status, Scenes, Created.
video open <id>
Opens the video preview URL in the browser.
tutorflow course
Create and manage multi-lesson courses.
course create [prompt]
| Argument / Option | Type | Default | Description |
| -------------------------- | --------------------- | ------- | --------------------------------- |
| [prompt] | string (positional) | — | Prompted interactively if omitted |
| -l, --lessons <number> | string → int | 8 | Lesson count |
| -lang, --language <lang> | string | en | ISO 639-1 language code |
Note:
--languageuses the short flag-lang(not-L), because-lis already taken by--lessons.
POST /v1/platform/courses body:
{
"prompt": "...",
"lessonCount": 8,
"language": "en",
"mode": "sync"
}tutorflow course create "Beginner Python in Korean" -l 10 -lang kocourse list
| Option | Type | Default | Description |
| ---------------------- | -------- | ------- | ----------- |
| -l, --limit <number> | string | 10 | Max rows |
GET /v1/platform/courses?limit=<n>. Table columns: ID, Title, Status, Created.
course open <id>
Opens the course preview URL in the browser.
tutorflow module
Create and manage standalone lesson modules.
module create [prompt]
POST /v1/platform/modules. Generates one classroom-compatible standalone
lesson. --idempotency-key is sent as the Idempotency-Key header so agents
can safely retry a create call.
| Argument / Option | Type | Default | Description |
| ------------------------------ | --------------------- | -------- | -------------------------------- |
| [prompt] | string (positional) | — | Lesson/module prompt |
| --title <title> | string | none | Override generated title |
| --description <description> | string | none | Override generated description |
| --target <target> | string | none | Target learner audience |
| --type <type> | string | lesson | Module type |
| --locale <locale> | string | en | Locale code |
| --language <language> | string | en | Language code |
| --slug <slug> | string | none | Optional slug |
| --private | boolean | false | Set isPublic: false |
| --no-quiz | boolean | false | Set hasQuiz: false |
| --idempotency-key <key> | string | none | Safe retry key |
tutorflow --json module create "Fractions for 5th grade" \
--title "Fractions" \
--target "5th grade" \
--no-quiz \
--idempotency-key lesson-001module list
GET /v1/platform/modules?limit=<n>&offset=<n>[&search=<q>].
tutorflow --json module list --limit 20 --offset 0 --search fractionsmodule get <id> / module open <id>
Fetch a module by ID or open its publicUrl.
tutorflow test
Create and manage generated assessments.
test create [prompt]
POST /v1/platform/tests. Generates a test/assessment. --idempotency-key is
sent as the Idempotency-Key header.
| Argument / Option | Type | Default | Description |
| ------------------------- | --------------------- | ---------- | ------------------------------ |
| [prompt] | string (positional) | — | Test generation prompt |
| --title <title> | string | none | Override generated title |
| --description <text> | string | none | Override generated description |
| -l, --language <lang> | string | en | Language code |
| --level <level> | string | none | Difficulty level |
| --subject <subject> | string | none | Subject area |
| -n, --items <number> | string -> int | none | Target item count |
| --time-limit <minutes> | string -> int | none | Time limit in minutes |
| --classroom-id <id> | string | none | Classroom ID |
| --tier <tier> | string | standard | Pricing tier |
| --mode <mode> | string | sync | sync or async |
| --idempotency-key <key> | string | none | Safe retry key |
tutorflow --json test create "Algebra quiz" \
--title "Algebra Basics" \
--items 8 \
--time-limit 30 \
--idempotency-key test-001test list
GET /v1/platform/tests?limit=<n>&offset=<n>[&status=<s>].
tutorflow --json test list --limit 20 --status READYtest get <id> / test open <id> / test edit-token <id>
Fetch a test, open its preview URL, or generate a fresh edit token.
tutorflow evaluate
Evaluates a single learner response through the Platform Evaluations API.
| Option | Type | Default | Allowed values / notes |
| ------------------------- | -------- | ---------- | ---------------------- |
| -t, --type <type> | string | (prompted) | exact, rubric_short_answer, open_ended. Aliases: rubric, open-ended |
| -q, --question <text> | string | (prompted) | Question text |
| -a, --student-answer <text> | string | (prompted) | Learner answer |
| -e, --expected-answer <text> | string | none | Required for exact; sent as referenceAnswer |
| --rubric-json <json> | string | none | Required for rubric_short_answer |
| --max-score <number> | number | backend default | Maximum score |
| -l, --language <lang> | string | en | Feedback language |
| --idempotency-key <key> | string | none | Safe retry key |
Interactive flow:
- Type (if not supplied)
- Question
- Student answer
- Expected answer — required only when type is
exact
POST /v1/platform/evaluations body:
{
"evaluationType": "exact",
"questionText": "What is 2+2?",
"learnerAnswer": "4",
"referenceAnswer": "4",
"language": "en",
"mode": "sync"
}referenceAnswer is omitted when evaluationType is open_ended.
rubric_short_answer requires --rubric-json, for example:
tutorflow --json evaluate \
--type rubric_short_answer \
--question "Explain the water cycle" \
--student-answer "Water evaporates and condenses." \
--rubric-json '{"criteria":[{"name":"Accuracy","description":"Scientific correctness","maxScore":10}]}'tutorflow auth
| Subcommand | Description |
| ---------- | ---------------------------------------------------- |
| login | Interactive prompt for API key, writes to config |
| logout | Prompts to confirm, then deletes config file |
| whoami | Prints masked API key (first 10 chars) and baseUrl |
Exit behavior & errors
- All commands wrap their action in
try/catch. Failures print✗ <message>in red and the raw error, but the process exit code is not set explicitly — it's whatever Node returns (typically0). Don't rely on exit codes for scripting yet. await prompt(...)from@clack/promptsreturns asymbolwhen the user cancels (Ctrl+C); commands check for this and printCancelledinstead of throwing.- HTTP errors from the API are surfaced as
Error: API Error: <status> <body-json>.
API client
Internal HTTP client lives in src/client.ts:
class ApiClient {
static async create(): Promise<ApiClient>; // loads from ~/.tutorflow/config.json
request<T>(method: string, endpoint: string, body?: object): Promise<T>;
setApiKey(apiKey: string): void;
setBaseUrl(baseUrl: string): void;
}- Uses
undici.fetch. - Headers always include
Content-Type: application/json. Authenticated requests also includeAuthorization: Bearer <apiKey>. - Throws
Error('API Error: <status> <body>')on non-2xx.
Project layout
.
├── package.json # bin: { tutorflow: ./dist/index.js }
├── tsconfig.json
├── vitest.config.ts
├── .github/workflows/
│ ├── ci.yml # typecheck + build + test on PRs and master
│ └── release.yml # OIDC publish on master/main pushes when version is new
├── RELEASING.md # full release procedure + Trusted Publisher setup
└── src/
├── index.ts # commander entrypoint, registers all commands
├── client.ts # undici-based ApiClient
├── client.test.ts
├── config.ts # ~/.tutorflow/config.json read/write/update/delete
├── config.test.ts
├── types.ts # API response interfaces (SlideResource, etc.)
├── commands/
│ ├── agent.ts # register / account / billing / email verification
│ ├── auth.ts # login / logout / whoami
│ ├── config.ts # get / set
│ ├── course.ts # create / list / open
│ ├── evaluate.ts # interactive evaluation
│ ├── module.ts # standalone lesson modules
│ ├── platform.ts # discovery / pricing / health
│ ├── slide.ts # create / list / open
│ ├── test.ts # generated assessments
│ └── video.ts # create / list / open
└── utils/
├── output.ts # formatTable, formatJson
└── spinner.tsnpm scripts
| Script | Command | Purpose |
| ---------------- | ----------------------- | --------------------------------------- |
| dev | tsx src/index.ts | Run from source |
| build | tsc --outDir dist | Emit JS + .d.ts to dist/ |
| link | npm link | Symlink tutorflow globally |
| typecheck | tsc --noEmit | Type-check without emitting |
| test | vitest run | Run all tests once |
| test:watch | vitest | Watch-mode tests |
| test:coverage | vitest run --coverage | Generate coverage report |
| prepublishOnly | rm -rf dist && build | Auto-runs before npm publish |
