@tutorflow/cli 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +261 -659
  2. package/dist/client.d.ts +16 -2
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +76 -16
  5. package/dist/client.js.map +1 -1
  6. package/dist/commands/agent.d.ts.map +1 -1
  7. package/dist/commands/agent.js +38 -23
  8. package/dist/commands/agent.js.map +1 -1
  9. package/dist/commands/auth.d.ts.map +1 -1
  10. package/dist/commands/auth.js +20 -12
  11. package/dist/commands/auth.js.map +1 -1
  12. package/dist/commands/classroom.d.ts +3 -0
  13. package/dist/commands/classroom.d.ts.map +1 -0
  14. package/dist/commands/classroom.js +27 -0
  15. package/dist/commands/classroom.js.map +1 -0
  16. package/dist/commands/config.d.ts.map +1 -1
  17. package/dist/commands/config.js +4 -4
  18. package/dist/commands/config.js.map +1 -1
  19. package/dist/commands/course.d.ts.map +1 -1
  20. package/dist/commands/course.js +27 -96
  21. package/dist/commands/course.js.map +1 -1
  22. package/dist/commands/credits.d.ts +3 -0
  23. package/dist/commands/credits.d.ts.map +1 -0
  24. package/dist/commands/credits.js +39 -0
  25. package/dist/commands/credits.js.map +1 -0
  26. package/dist/commands/evaluate.d.ts +3 -0
  27. package/dist/commands/evaluate.d.ts.map +1 -1
  28. package/dist/commands/evaluate.js +98 -57
  29. package/dist/commands/evaluate.js.map +1 -1
  30. package/dist/commands/interactive.d.ts +13 -0
  31. package/dist/commands/interactive.d.ts.map +1 -0
  32. package/dist/commands/interactive.js +105 -0
  33. package/dist/commands/interactive.js.map +1 -0
  34. package/dist/commands/learner.d.ts +4 -0
  35. package/dist/commands/learner.d.ts.map +1 -0
  36. package/dist/commands/learner.js +76 -0
  37. package/dist/commands/learner.js.map +1 -0
  38. package/dist/commands/module.d.ts.map +1 -1
  39. package/dist/commands/module.js +20 -98
  40. package/dist/commands/module.js.map +1 -1
  41. package/dist/commands/platform.d.ts.map +1 -1
  42. package/dist/commands/platform.js +10 -2
  43. package/dist/commands/platform.js.map +1 -1
  44. package/dist/commands/run.d.ts +3 -0
  45. package/dist/commands/run.d.ts.map +1 -0
  46. package/dist/commands/run.js +69 -0
  47. package/dist/commands/run.js.map +1 -0
  48. package/dist/commands/slide.d.ts.map +1 -1
  49. package/dist/commands/slide.js +27 -97
  50. package/dist/commands/slide.js.map +1 -1
  51. package/dist/commands/test.d.ts.map +1 -1
  52. package/dist/commands/test.js +40 -106
  53. package/dist/commands/test.js.map +1 -1
  54. package/dist/commands/video.d.ts.map +1 -1
  55. package/dist/commands/video.js +143 -86
  56. package/dist/commands/video.js.map +1 -1
  57. package/dist/commands/webhook.d.ts +3 -0
  58. package/dist/commands/webhook.d.ts.map +1 -0
  59. package/dist/commands/webhook.js +75 -0
  60. package/dist/commands/webhook.js.map +1 -0
  61. package/dist/config.d.ts +3 -0
  62. package/dist/config.d.ts.map +1 -1
  63. package/dist/config.js +17 -16
  64. package/dist/config.js.map +1 -1
  65. package/dist/content/context.d.ts +26 -0
  66. package/dist/content/context.d.ts.map +1 -0
  67. package/dist/content/context.js +74 -0
  68. package/dist/content/context.js.map +1 -0
  69. package/dist/content/resource.d.ts +66 -0
  70. package/dist/content/resource.d.ts.map +1 -0
  71. package/dist/content/resource.js +181 -0
  72. package/dist/content/resource.js.map +1 -0
  73. package/dist/content/runs.d.ts +51 -0
  74. package/dist/content/runs.d.ts.map +1 -0
  75. package/dist/content/runs.js +171 -0
  76. package/dist/content/runs.js.map +1 -0
  77. package/dist/content/sharing.d.ts +24 -0
  78. package/dist/content/sharing.d.ts.map +1 -0
  79. package/dist/content/sharing.js +88 -0
  80. package/dist/content/sharing.js.map +1 -0
  81. package/dist/index.js +71 -21
  82. package/dist/index.js.map +1 -1
  83. package/dist/types.d.ts +15 -65
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/types.js +4 -4
  86. package/dist/utils/args.d.ts +15 -0
  87. package/dist/utils/args.d.ts.map +1 -0
  88. package/dist/utils/args.js +91 -0
  89. package/dist/utils/args.js.map +1 -0
  90. package/dist/utils/keys.d.ts +11 -0
  91. package/dist/utils/keys.d.ts.map +1 -0
  92. package/dist/utils/keys.js +29 -0
  93. package/dist/utils/keys.js.map +1 -0
  94. package/dist/utils/output.d.ts.map +1 -1
  95. package/dist/utils/output.js +21 -6
  96. package/dist/utils/output.js.map +1 -1
  97. package/dist/utils/runtime.d.ts +14 -4
  98. package/dist/utils/runtime.d.ts.map +1 -1
  99. package/dist/utils/runtime.js +42 -7
  100. package/dist/utils/runtime.js.map +1 -1
  101. package/package.json +4 -3
  102. package/dist/utils/spinner.d.ts +0 -8
  103. package/dist/utils/spinner.d.ts.map +0 -1
  104. package/dist/utils/spinner.js +0 -26
  105. package/dist/utils/spinner.js.map +0 -1
package/README.md CHANGED
@@ -1,766 +1,368 @@
1
1
  # @tutorflow/cli
2
2
 
3
- 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/*`).
3
+ 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/*`).
4
4
 
5
5
  - **Binary:** `tutorflow`
6
6
  - **Package:** [`@tutorflow/cli`](https://www.npmjs.com/package/@tutorflow/cli) (current version in [`package.json`](package.json))
7
7
  - **Source:** [tutorflow-io/cli](https://github.com/tutorflow-io/cli) (private)
8
- - **Node:** `>= 18` for runtime, `>= 20` for development (Vitest 4 requirement)
9
- - **Agent-friendly:** every command supports `--json` output, env-var auth (`TUTORFLOW_API_KEY`), and proper exit codes — see [Non-interactive / agent usage](#non-interactive--agent-usage)
8
+ - **Node:** `>= 18` at runtime, `>= 20` for development (Vitest 4)
9
+ - **Agent-friendly:** `--json` output, env-var auth, no prompts without a TTY, stable error codes and exit codes. See [Agent usage](#agent-usage).
10
10
 
11
- ## Installation
11
+ > **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](#migrating-from-03).
12
12
 
13
- ### A. End users (recommended)
14
-
15
- Install globally with any package manager:
16
-
17
- ```bash
18
- npm install -g @tutorflow/cli
19
- pnpm add -g @tutorflow/cli
20
- yarn global add @tutorflow/cli
21
- ```
22
-
23
- Then anywhere on the system:
24
-
25
- ```bash
26
- tutorflow --version
27
- tutorflow auth login
28
- tutorflow agent register --name "My Agent Workspace" --slug my-agent-workspace --email me@example.com
29
- tutorflow --json platform manifest
30
- tutorflow slide create "My first deck"
31
- ```
32
-
33
- **One-shot use without installing**:
34
-
35
- ```bash
36
- npx @tutorflow/cli slide create "My first deck"
37
- ```
38
-
39
- This is the recommended path for AI agents and CI environments — no install step, always pulls the latest published version.
40
-
41
- ### B. Local development (this repo)
42
-
43
- ```bash
44
- git clone https://github.com/tutorflow-io/cli.git
45
- cd cli
46
- pnpm install
47
- pnpm build
48
- pnpm link # exposes the `tutorflow` binary globally
49
- ```
50
-
51
- Verify:
13
+ ## Quick start
52
14
 
53
15
  ```bash
54
- tutorflow --version # matches package.json
55
- tutorflow --help
56
- ```
57
-
58
- To unlink later: `npm unlink -g @tutorflow/cli`.
59
-
60
- Run from source without building:
61
-
62
- ```bash
63
- pnpm dev -- <command> [options]
64
- # e.g. pnpm dev -- slide list --limit 5
65
- ```
66
-
67
- ## Releasing
68
-
69
- Releases are automated by GitHub Actions on pushes to `master` / `main`.
70
- Auth uses **npm OIDC Trusted Publishing** — no `NPM_TOKEN` secret in CI.
71
-
72
- ```bash
73
- git checkout master
74
- git pull
75
-
76
- npm version patch --no-git-tag-version # 0.3.0 → 0.3.1 (or minor / major)
77
- git push origin master # Release workflow publishes if that version is not on npm yet
78
- ```
79
-
80
- The [`Release` workflow](.github/workflows/release.yml):
81
-
82
- 1. Reads `package.json` name and version
83
- 2. Skips npm publish if that exact version already exists
84
- 3. Runs `pnpm typecheck` + `pnpm build` + `pnpm test`
85
- 4. Publishes new versions with `npm publish --access public` (auth via OIDC)
86
- 5. Creates a GitHub Release only for `v*.*.*` tag pushes
87
-
88
- For full setup (npm org, Trusted Publisher config) see [RELEASING.md](RELEASING.md).
89
-
90
- ### Pre-release / beta
91
-
92
- ```bash
93
- npm version prerelease --preid=beta --no-git-tag-version # 0.3.1 → 0.3.2-beta.0
94
- git push origin master
95
- ```
96
-
97
- Pre-release versions publish to a dist-tag derived from the prerelease id
98
- (`0.3.1-beta.0` publishes with `--tag beta`).
99
-
100
- ## ⚠️ Caveats & gotchas
101
-
102
- These tripped us up during initial setup. Read before changing release infrastructure.
103
-
104
- ### Versioning
105
-
106
- - **Don't hardcode the version in `src/index.ts`.** It reads `version` from [`package.json`](package.json) via `resolveJsonModule`. Hardcoding will silently lie to users via `--version` after every `npm version` bump.
107
- - The `prepublishOnly` script in `package.json` rebuilds `dist/` before publish. Don't skip it.
16
+ npm install -g @tutorflow/cli # or: npx @tutorflow/cli <command>
108
17
 
109
- ### npm Trusted Publishing (OIDC)
110
-
111
- - **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.
112
- - The Trusted Publisher binding at [npmjs.com/package/@tutorflow/cli/access](https://www.npmjs.com/package/@tutorflow/cli/access) hardcodes:
113
- - GitHub org: `tutorflow-io`
114
- - Repo: `cli`
115
- - Workflow filename: `release.yml`
116
- - Environment: (none)
117
-
118
- Renaming the repo, moving the workflow file, or adding an `environment:` to the publish job will break OIDC auth until the binding is updated.
119
- - 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.
120
-
121
- ### Provenance
122
-
123
- - `--provenance` is **disabled** in [release.yml](.github/workflows/release.yml). npm only accepts provenance attestations from **public** GitHub repos. Add `--provenance` back to the `npm publish` command when this repo goes public.
124
-
125
- ### Node + tooling
18
+ # 1. Create an agent account. The tf_content_ key is saved to ~/.tutorflow/config.json.
19
+ tutorflow agent register --name "My Agent" --slug my-agent --email me@example.com
126
20
 
127
- - **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`).
128
-
129
- ### Tarball hygiene
21
+ # 2. Generate content into the account's default classroom and wait for it.
22
+ tutorflow course generate "Beginner Python for analysts" --lessons 3 --wait
23
+ tutorflow slide generate "Photosynthesis" --count 8 --wait
24
+ tutorflow test generate "Algebra basics" --items 5 --wait
130
25
 
131
- - `tsconfig.json` excludes `**/*.test.ts` so test files don't end up in `dist/` (and therefore not in the published tarball).
132
- - `package.json#files` is an allowlist (`dist`, `README.md`, `LICENSE`). Anything not listed is excluded.
133
- - Run `npm pack --dry-run` to inspect the tarball before publish.
134
-
135
- ### Source maps and a private repo
136
-
137
- 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](tsconfig.json) before going public.
26
+ # 3. Grade an answer and read the learner model it builds.
27
+ tutorflow evaluate --type open-ended --question "Why is the sky blue?" \
28
+ --student-answer "Air scatters blue light more." --learner-id student-7 --concept-tags light,atmosphere
29
+ tutorflow evaluation mastery student-7
138
30
 
139
- ## Development
140
-
141
- Run with `tsx` (no build step):
142
-
143
- ```bash
144
- pnpm dev -- <command> [options]
145
- # e.g. pnpm dev -- slide list --limit 5
31
+ # 4. Check what it cost.
32
+ tutorflow credits balance
146
33
  ```
147
34
 
148
- Build artifacts land in `dist/` (entry: `dist/index.js`, declared in `package.json#bin`).
35
+ ## Keys and classrooms
149
36
 
150
- ## Non-interactive / agent usage
37
+ | Key prefix | Works with | Notes |
38
+ | --- | --- | --- |
39
+ | `tf_content_…` | every command | Returned by `agent register` as `contentApiKey`, or created in the Agent Platform dashboard. |
40
+ | `tf_content_test_…` | content commands | Reaches only the sandbox classroom, returns canned results and spends nothing. Learner routes refuse test keys. |
41
+ | `tf_platform_…` (deprecated) | `agent`, `platform` | Refused by content commands with `EPLATFORMKEY`. The Platform content routes it served are being removed. |
151
42
 
152
- Every command supports the contract below so AI agents and CI scripts can use the CLI without a TTY.
43
+ Every content command acts in one classroom, chosen in this order:
153
44
 
154
- ### Auth without prompts
155
-
156
- ```bash
157
- # Option 1: env var (preferred for one-shot calls)
158
- TUTORFLOW_API_KEY=tf_xxx tutorflow --json slide list
159
-
160
- # Option 2: persist to ~/.tutorflow/config.json once
161
- tutorflow auth login --api-key tf_xxx
162
- tutorflow --json slide create "Photosynthesis for 5th graders" -n 12
163
- ```
45
+ 1. `--classroom <id>` (a global flag; works before or after the subcommand)
46
+ 2. `TUTORFLOW_CLASSROOM_ID`
47
+ 3. `classroomId` in `~/.tutorflow/config.json` (`tutorflow config set classroomId <id>`)
48
+ 4. `GET /v1/platform/agent/account`: `sandboxClassroomId` for a test key, `defaultClassroomId` otherwise
49
+ 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
164
50
 
165
- `TUTORFLOW_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.
51
+ `tutorflow classroom list` shows the classrooms the key can reach.
166
52
 
167
- ### Machine-readable output
53
+ ## Commands
168
54
 
169
- `--json` (or `TUTORFLOW_JSON=1`) makes every command emit a single JSON line on stdout. Errors emit JSON to stderr.
55
+ All ids are UUIDs. Every command accepts `--json`.
170
56
 
171
- ```bash
172
- $ tutorflow --json auth whoami
173
- {"authenticated":true,"apiKeyPreview":"tf_xxxxxxx...","baseUrl":"https://api.tutorflow.io","source":"env"}
57
+ ### Account and discovery (`/v1/platform`)
174
58
 
175
- $ tutorflow --json auth whoami # without auth
176
- # stdout: (empty)
177
- # stderr: {"error":{"message":"Not authenticated","code":"EAUTH"}}
178
- # exit: 1
179
- ```
59
+ | Command | Route |
60
+ | --- | --- |
61
+ | `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. |
62
+ | `agent account` | `GET /v1/platform/agent/account`: balance, `defaultClassroomId`, `sandboxClassroomId`, Content API URLs |
63
+ | `agent billing status` | `GET /v1/platform/agent/billing/status` |
64
+ | `agent billing session` | `POST /v1/platform/agent/billing/session`: hosted checkout URL for a human (verified email) |
65
+ | `agent billing x402-deposit --amount-usd <n>` | `POST /v1/platform/agent/billing/x402/deposit`: USDC deposit address on Base (verified email) |
66
+ | `agent checkout info --token` / `agent checkout create --token --amount-usd` | `GET` / `POST /v1/platform/agent/checkout` (public) |
67
+ | `agent verify-email resend` | `POST /v1/platform/agent/verify-email/resend` |
68
+ | `platform manifest` / `platform pricing` | `GET /v1/platform/.well-known/agent.json` / `GET /v1/platform/pricing-catalog` (public) |
69
+ | `platform health` | `GET /v1/platform/health` |
70
+ | `platform share-redirect <shareToken>` | `GET /v1/platform/share-redirects/{token}` (public): where an old Platform share link moved |
180
71
 
181
- List commands return raw arrays:
72
+ ### Generated content (`/v1/content/classrooms/{classroomId}/…`)
182
73
 
183
- ```bash
184
- $ tutorflow --json slide list --limit 3
185
- [{"id":"...","title":"...","status":"ready","createdAt":"..."},...]
186
- ```
74
+ | Command | Route | Credits |
75
+ | --- | --- | --- |
76
+ | `course generate [prompt] [--lessons] [--language] [--level] [--title] [--description]` | `POST …/courses/generate` | 3 + 3 per lesson |
77
+ | `slide generate [topic] [--count 5-20] [--theme] [--instructions] [--text-length concise\|detailed]` | `POST …/slides/generate` | per page |
78
+ | `test generate [topic] [--items] [--item-types select,blank,…] [--level] [--total-score]` | `POST …/tests/generate` | 3 |
79
+ | `module generate [topic] [--type markdown\|ai-tutor\|flashcard\|…] [--reference]` | `POST …/modules/generate` | 3 |
187
80
 
188
- Pipe into `jq` for downstream automation:
81
+ `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`.
189
82
 
190
- ```bash
191
- tutorflow --json slide list --status ready | jq -r '.[].id' | xargs -I{} tutorflow --json slide get {}
192
- ```
83
+ Each of `course`, `slide`, `test`, `module`, `video`, `game` and `simulation` also has:
193
84
 
194
- ### Exit codes
85
+ | Subcommand | Route |
86
+ | --- | --- |
87
+ | `list [--limit] [--page] [--search]` | `GET …/{resource}` (JSON mode prints the raw `{ data, meta }` page) |
88
+ | `get <id>` | `GET …/{resource}/{id}` |
89
+ | `open <id> [--editor]` | prints `editorUrl` and `publicUrl`; opens one in a browser on a TTY |
90
+ | `delete <id> --yes` | `DELETE …/{resource}/{id}` (`--yes` required without a TTY) |
91
+ | `share <id> [--listed]` | `PATCH …/{resource}/{id}`: makes it open by link and prints `publicUrl` |
92
+ | `visibility <id> <value>` | `PATCH …/{resource}/{id}` with that visibility |
195
93
 
196
- | Code | Meaning |
197
- | ----- | ----------------------------------------------------------------- |
198
- | `0` | Success |
199
- | `1` | Generic failure (auth, network, API error, etc.) |
200
- | `2` | Bad input (invalid flag value, missing required input in non-TTY) |
201
- | `130` | User cancelled an interactive prompt (Ctrl+C) |
94
+ `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.
202
95
 
203
- The error JSON includes a stable `code`: `EAUTH`, `EINVAL`, `ENOINTERACTIVE`, `ENETWORK`, `EHTTP_<status>`, `ENOPREVIEW`, `ENOTFOUND`, `ECANCELLED`.
96
+ | Resource | `share` sets | `visibility` values |
97
+ | --- | --- | --- |
98
+ | `video`, `test` | `UNLISTED` (opens by link, kept out of lists and search); `--listed` sets `PUBLIC` | `PUBLIC`, `UNLISTED`, `PRIVATE` |
99
+ | `slide`, `game`, `simulation` | `PUBLIC` | `PUBLIC`, `PRIVATE` |
100
+ | `course` | `PUBLIC` | `PUBLIC`, `PRIVATE`, `ORGANIZATION`, `CLASSROOM`, `COURSE` |
101
+ | `module` | `isPublic: true` | `PUBLIC`, `PRIVATE` (written as `isPublic`) |
204
102
 
205
- ### Required-input flags (no prompts)
206
-
207
- Commands that have required input expose every prompt as a flag:
103
+ `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`.
208
104
 
209
105
  ```bash
210
- # self-register a platform workspace and save the returned API key
211
- tutorflow --json agent register \
212
- --name "Agent Workspace" \
213
- --slug agent-workspace \
214
- --email agent@example.com \
215
- --agent-identity "codex"
216
-
217
- # slide / video / course create — accept positional [prompt]
218
- tutorflow --json slide create "Photosynthesis" -n 12 -l en
219
- tutorflow --json video create "Newton's laws" -s 6 -r 16:9 -d 90
220
- tutorflow --json course create "Beginner Python" -l 10 -lang en
221
-
222
- # evaluate — every input is a flag
223
- tutorflow --json evaluate \
224
- --type exact \
225
- --question "What is 2+2?" \
226
- --student-answer "4" \
227
- --expected-answer "4"
228
-
229
- # logout (skip confirmation)
230
- tutorflow auth logout --force
106
+ tutorflow --json video share <videoId> | jq -r .publicUrl
231
107
  ```
232
108
 
233
- If 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.
109
+ `test results <testId>` lists learners' results (`GET …/tests/{id}/results`).
234
110
 
235
- ### Globals
111
+ ### Runs
236
112
 
237
- | Flag | Env var | Effect |
238
- | --------------------------------- | -------------------------------- | ----------------------------------------------- |
239
- | `--json` | `TUTORFLOW_JSON=1` | JSON output to stdout, JSON errors to stderr |
240
- | `--debug` | `DEBUG=1` or `TUTORFLOW_DEBUG=1` | Print stack traces on errors |
241
- | `--api-key <k>` (auth login only) | `TUTORFLOW_API_KEY` | API key |
242
- | (n/a) | `TUTORFLOW_BASE_URL` | Override API base URL (staging, self-hosted) |
113
+ | Command | Route |
114
+ | --- | --- |
115
+ | `run list [--kind course\|slide\|test\|module] [--status] [--limit] [--cursor]` | `GET …/runs` |
116
+ | `run get <runId>` | `GET …/runs/{runId}` |
117
+ | `run wait <runId> [--timeout]` | polls `GET …/runs/{runId}`; prints `{ run, resource }` |
118
+ | `run resume <runId>` | `POST …/runs/{runId}/resume` (course runs only) |
243
119
 
244
- ### MCP server
120
+ 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.
245
121
 
246
- 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.
122
+ ### Videos
247
123
 
248
- ## Authentication
124
+ The Content API builds videos from scenes you script; it does not write the script from a prompt.
249
125
 
250
- Generate an API key in your TutorFlow workspace, then:
126
+ | Command | Route |
127
+ | --- | --- |
128
+ | `video create --title [--description] [--visibility PUBLIC\|UNLISTED\|PRIVATE] [--scene "<script>"]…` | `POST …/videos`, then one scene per `--scene` in order |
129
+ | `video scene add <videoId> [--script] [--display-text] [--duration] [--after <sceneId>]` | `POST …/videos/{id}/scenes`, then `PATCH` the new scene |
130
+ | `video scene update <videoId> <sceneId> [--script] [--display-text] [--duration]` | `PATCH …/scenes/{sceneId}` |
131
+ | `video scene delete <videoId> <sceneId> --yes` | `DELETE …/scenes/{sceneId}` |
132
+ | `video narrate <videoId> [--scene <sceneId>]` | `POST …/narration` (1 credit per scene) |
133
+ | `video render <videoId> [--generate-missing-narration] [--wait]` | `POST …/videos/{id}/render` (5 credits; `--wait` polls until `COMPLETED` and prints the mp4 URL) |
134
+ | `video render-status <videoId>` / `video render-cancel <videoId>` | `GET …/render` / `POST …/render/cancel` |
251
135
 
252
136
  ```bash
253
- tutorflow auth login # interactive password prompt for API key
254
- tutorflow auth whoami # prints masked API key + base URL
255
- tutorflow auth logout # confirms then deletes ~/.tutorflow/config.json
137
+ tutorflow video create --title "Why the sky is blue" \
138
+ --scene "Sunlight is made of many colors." \
139
+ --scene "Air scatters blue light the most, so the sky looks blue."
140
+ tutorflow video render <videoId> --generate-missing-narration --wait
256
141
  ```
257
142
 
258
- Credentials are persisted to `~/.tutorflow/config.json`:
143
+ ### Games and simulations
259
144
 
260
- ```json
261
- {
262
- "apiKey": "tf_xxxxxxxxxxxxxxxxxxxx",
263
- "baseUrl": "https://api.tutorflow.io",
264
- "workspace": "optional-workspace-slug"
265
- }
266
- ```
145
+ `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.
267
146
 
268
- The API key is sent as the `Authorization: Bearer <apiKey>` header on authenticated requests.
147
+ | Command | Route | Credits |
148
+ | --- | --- | --- |
149
+ | `create --title [--description] [--visibility PRIVATE\|PUBLIC]` | `POST …/{games\|simulations}` | 0 |
150
+ | `brief <id> [--reference <markdown>] [--wait]` | `POST …/{id}/brief` | 1 |
151
+ | `build <id> [--wait]` | `POST …/{id}/build` | game 40, simulation 20 (2D) or 30 (3D) |
152
+ | `revise <id> --feedback <text> [--wait]` | `POST …/{id}/revise` | as build |
153
+ | `status <id>` | `GET …/{id}/run` | |
154
+
155
+ ### Evaluations and learners
156
+
157
+ | Command | Route |
158
+ | --- | --- |
159
+ | `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) |
160
+ | `evaluation list [--learner-id] [--status] [--type] [--limit] [--cursor]` / `evaluation get <id>` | `GET …/evaluations` |
161
+ | `evaluation concepts` / `evaluation mastery <learnerId>` | `GET …/concepts` / `GET …/mastery/{learner}` |
162
+ | `learner list` / `get <id>` / `progress <id>` | `GET …/learners…` (live keys only) |
163
+ | `learner invite --email [--name] [--locale] [--course <id>]… [--test <id>]…` | `POST …/learners/invitations` |
164
+ | `learner remove <id> --yes` | `DELETE …/learners/{id}` |
269
165
 
270
- ### Config schema
166
+ `--type` takes `exact`, `rubric_short_answer` or `open_ended` (aliases `rubric`, `open-ended`). `exact` needs `--expected-answer`; `rubric_short_answer` needs `--rubric-json`.
271
167
 
272
- ```ts
273
- interface IConfig {
274
- apiKey?: string; // required for all non-auth/config commands
275
- baseUrl?: string; // default: https://api.tutorflow.io
276
- workspace?: string; // optional workspace slug
277
- }
278
- ```
279
-
280
- ## Configuration commands
281
-
282
- ```bash
283
- tutorflow config get # print full config (apiKey is masked)
284
- tutorflow config get <key> # print one key as KEY=VALUE
285
- tutorflow config set <key> <value> # update one key
286
- ```
287
-
288
- | Key | Type | Default | Notes |
289
- | ----------- | -------- | -------------------------- | -------------------------------------- |
290
- | `apiKey` | `string` | none | Set via `auth login` or `config set` |
291
- | `baseUrl` | `string` | `https://api.tutorflow.io` | Override for staging / self-hosted |
292
- | `workspace` | `string` | none | Reserved for future scoping |
168
+ ### Webhooks and credits (`/v1/content`)
293
169
 
294
- `config set` rejects unknown keys.
295
-
296
- ## Commands
170
+ | Command | Route |
171
+ | --- | --- |
172
+ | `webhook list` / `get <id>` | `GET /v1/content/webhooks[/{id}]` |
173
+ | `webhook create --url --event <event>… [--mode live\|test]` | `POST /v1/content/webhooks` (the signing secret is shown once) |
174
+ | `webhook test <id>` / `deliveries <id>` / `rotate-secret <id>` / `delete <id> --yes` | the matching `/v1/content/webhooks/{id}/…` routes |
175
+ | `credits balance` | `GET /v1/content/credits` |
176
+ | `credits history [--limit] [--cursor] [--from] [--to] [--type]` | `GET /v1/content/credits/history` |
297
177
 
298
- All resource commands require authentication. Without an API key they print:
299
- `✗ Not authenticated. Run "tutorflow auth login" first.`
178
+ 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.
300
179
 
301
- ### `tutorflow agent`
180
+ ### Auth and config
302
181
 
303
- Manage the agent-facing TutorFlow Platform account flow.
182
+ | Command | Effect |
183
+ | --- | --- |
184
+ | `auth login --api-key <key>` | Save a key (prompts on a TTY when omitted) |
185
+ | `auth whoami` | Masked key, `keyKind` (`content_live`, `content_test`, `platform`, `unknown`), base URL, `source` (`env` or `config`). No network call. |
186
+ | `auth logout --force` | Delete `~/.tutorflow/config.json` |
187
+ | `config get [key]` / `config set <key> <value>` | Keys: `apiKey`, `baseUrl`, `classroomId` |
304
188
 
305
- #### `agent register`
189
+ The config file is written with mode `0600`.
306
190
 
307
- Public self-service registration. Creates a platform workspace through
308
- `POST /v1/platform/agent/register`, returns the one-time API key, and saves it
309
- to `~/.tutorflow/config.json` by default so the next CLI command can run
310
- without a separate `auth login`.
311
-
312
- | Option | Type | Default | Description |
313
- | ------------------------------ | -------- | ------- | -------------------------------------------- |
314
- | `--name <name>` | `string` | — | Workspace display name |
315
- | `--slug <slug>` | `string` | — | Unique lowercase slug |
316
- | `--email <email>` | `string` | none | Contact email for verification and billing |
317
- | `--agent-identity <identity>` | `string` | none | Optional agent identity string |
318
- | `--no-save` | boolean | false | Print the API key but do not persist it |
319
-
320
- ```bash
321
- tutorflow --json agent register \
322
- --name "Agent Workspace" \
323
- --slug agent-workspace \
324
- --email agent@example.com \
325
- --agent-identity "codex"
326
- ```
327
-
328
- #### `agent account`
329
-
330
- GET `/v1/platform/agent/account`. Shows workspace status, credit balance,
331
- email verification state, rate limits, and `restrictedActions`.
191
+ ## Agent usage
332
192
 
333
193
  ```bash
334
- tutorflow --json agent account
335
- ```
336
-
337
- #### `agent billing status`
194
+ export TUTORFLOW_API_KEY=tf_content_xxx
195
+ export TUTORFLOW_JSON=1
338
196
 
339
- GET `/v1/platform/agent/billing/status`. Shows payment-method state, credit
340
- balance, billing mode, email verification state, and `restrictedActions`.
341
-
342
- ```bash
343
- tutorflow --json agent billing status
197
+ run=$(tutorflow course generate "Fractions for 5th grade" --lessons 2)
198
+ tutorflow run wait "$(echo "$run" | jq -r .runId)" | jq -r .resource.publicUrl
344
199
  ```
345
200
 
346
- #### `agent billing session`
347
-
348
- POST `/v1/platform/agent/billing/session`. Returns a TutorFlow-hosted checkout
349
- URL that an agent should forward to a human operator.
350
-
351
- ```bash
352
- tutorflow --json agent billing session
353
- ```
201
+ To hand the work to a person, make it open by link and send them `publicUrl`: `tutorflow course share <courseId> | jq -r .publicUrl`.
354
202
 
355
- #### `agent verify-email resend`
203
+ - **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.
204
+ - **No prompts without a TTY:** a missing required input exits 2 with `ENOINTERACTIVE` and names the flag to pass. Destructive commands need `--yes`.
205
+ - **Retries:** create and generate commands accept `--idempotency-key`.
206
+ - **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`.
356
207
 
357
- POST `/v1/platform/agent/verify-email/resend`. Resends the workspace email
358
- verification message using the active API key.
208
+ | Exit | Meaning |
209
+ | --- | --- |
210
+ | `0` | Success |
211
+ | `1` | Failure: auth, network, API error, failed run, timeout |
212
+ | `2` | Bad input: invalid flag, missing input without a TTY, ambiguous classroom |
213
+ | `130` | Cancelled at a prompt |
214
+
215
+ | Code | When |
216
+ | --- | --- |
217
+ | `EAUTH` | No API key |
218
+ | `EPLATFORMKEY` | A `tf_platform_` key on a content command |
219
+ | `ECLASSROOM` | No classroom could be chosen; pass `--classroom` |
220
+ | `EINVAL` | Invalid flag value |
221
+ | `ENOINTERACTIVE` | Missing input or confirmation without a TTY |
222
+ | `EHTTP_<status>` | API error; see `apiCode` |
223
+ | `ENETWORK` | The API could not be reached |
224
+ | `ERUNFAILED` | A waited run, build or render failed |
225
+ | `ETIMEOUT` | `--wait` gave up; the work continues on the server |
226
+ | `ENOPREVIEW` | `open` found no URL |
227
+ | `ENOTFOUND` | `config get` of an unset key |
228
+ | `ECANCELLED` | Prompt cancelled |
229
+
230
+ | Environment variable | Effect |
231
+ | --- | --- |
232
+ | `TUTORFLOW_API_KEY` | API key (wins over the config file) |
233
+ | `TUTORFLOW_BASE_URL` | API origin, e.g. `https://dev-api.tutorflow.io` for staging |
234
+ | `TUTORFLOW_CLASSROOM_ID` | Classroom for content commands |
235
+ | `TUTORFLOW_JSON=1` | JSON mode |
236
+ | `TUTORFLOW_DEBUG=1` / `DEBUG=1` | Stack traces on errors |
237
+ | `TUTORFLOW_POLL_INTERVAL_MS` | `--wait` poll interval (default 3000) |
238
+
239
+ Empty environment variables are ignored.
240
+
241
+ ## Migrating from 0.3
242
+
243
+ | 0.3 | Now |
244
+ | --- | --- |
245
+ | `tf_platform_` key on every command | `tf_content_` key; `agent register` saves it. Platform keys still work on `agent` and `platform`. |
246
+ | `/v1/platform/{courses,slides,tests,modules,videos,evaluations}` | `/v1/content/classrooms/{classroomId}/…` |
247
+ | `course create` returned the finished course (`mode: sync`) | `course generate` (alias `create`) returns a run; add `--wait` |
248
+ | `course create -l 10 -lang ko` | `course generate --lessons 10 --language ko` |
249
+ | `slide create -t <theme>` | `slide generate --theme <themeId>` (5 to 20 pages) |
250
+ | `video create "<prompt>" -s 6 -r 16:9 -d 90` | `video create --title … --scene "<script>"…`, then `video render --generate-missing-narration --wait` |
251
+ | `module create --title --target --locale --private --no-quiz` | `module generate [--type] [--reference]`; edit the rest in the classroom editor |
252
+ | `test create --classroom-id --tier --mode --time-limit` | `test generate` with the global `--classroom`; generation is always asynchronous |
253
+ | `test edit-token`, `previewUrl` | `editorUrl` and `publicUrl` on every resource; `open <id>` |
254
+ | list `--offset` | `--page` |
255
+ | `evaluate` → `/v1/platform/evaluations` | `evaluate` → `…/evaluations`, plus `--learner-id` and `--concept-tags` |
256
+
257
+ 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`.
359
258
 
360
- ```bash
361
- tutorflow --json agent verify-email resend
362
- ```
363
-
364
- #### `agent checkout info`
365
-
366
- GET `/v1/platform/agent/checkout?token=<token>`. Reads workspace info for a
367
- TutorFlow checkout token returned through the hosted billing-session flow.
368
-
369
- ```bash
370
- tutorflow --json agent checkout info --token checkout_123
371
- ```
372
-
373
- #### `agent checkout create`
374
-
375
- POST `/v1/platform/agent/checkout`. Creates a Stripe Checkout URL from a
376
- TutorFlow checkout token and amount. This is normally called by the hosted
377
- checkout page; CLI support exists for complete automation and testing.
378
-
379
- ```bash
380
- tutorflow --json agent checkout create --token checkout_123 --amount-usd 25
381
- ```
382
-
383
- ### `tutorflow platform`
384
-
385
- Inspect public discovery metadata and validate the active Platform API key.
386
-
387
- | Subcommand | Endpoint | Auth |
388
- | ---------- | -------- | ---- |
389
- | `manifest` | `GET /v1/platform/.well-known/agent.json` | none |
390
- | `pricing` | `GET /v1/platform/pricing-catalog` | none |
391
- | `health` | `GET /v1/platform/health` | Platform API key |
392
-
393
- ```bash
394
- tutorflow --json platform manifest
395
- tutorflow --json platform pricing
396
- tutorflow --json platform health
397
- ```
398
-
399
- ### `tutorflow slide`
400
-
401
- Create and manage slide decks.
402
-
403
- #### `slide create [prompt]`
404
-
405
- Creates a slide deck synchronously (`mode: 'sync'`).
406
-
407
- | Argument / Option | Type | Default | Description |
408
- | ----------------------- | --------------------- | ------- | ------------------------------------ |
409
- | `[prompt]` | `string` (positional) | — | Prompted interactively if omitted |
410
- | `-n, --count <number>` | `string → int` | `10` | Slide count (parsed via `parseInt`) |
411
- | `-l, --language <lang>` | `string` | `en` | ISO 639-1 language code |
412
- | `-t, --theme <theme>` | `string` | none | Theme identifier |
413
-
414
- POST `/v1/platform/slides` body:
415
-
416
- ```json
417
- {
418
- "prompt": "...",
419
- "slideCount": 10,
420
- "language": "en",
421
- "theme": "...",
422
- "mode": "sync"
423
- }
424
- ```
425
-
426
- Output: `id`, `status`, `previewUrl` (if returned). After creation, prompts to open the preview URL in the browser.
259
+ ## Development
427
260
 
428
261
  ```bash
429
- tutorflow slide create "Photosynthesis for 5th graders" -n 12 -l en -t classic
430
- ```
431
-
432
- #### `slide list`
433
-
434
- | Option | Type | Default | Description |
435
- | ----------------------- | -------- | ------- | -------------------------- |
436
- | `-l, --limit <number>` | `string` | `10` | Max rows to fetch |
437
- | `-s, --status <status>` | `string` | none | Server-side status filter |
438
-
439
- GET `/v1/platform/slides?limit=<n>[&status=<s>]`. Renders an ASCII table with `ID` (8-char prefix), `Title`, `Status`, `Created`.
440
-
441
- #### `slide open <id>`
442
-
443
- Fetches `/v1/platform/slides/:id` and opens `previewUrl` in the default browser via [`open`](https://github.com/sindresorhus/open).
444
-
445
- ---
446
-
447
- ### `tutorflow video`
448
-
449
- Create and manage AI-generated videos.
450
-
451
- #### `video create [prompt]`
452
-
453
- Creates a video synchronously (`mode: 'sync'`).
454
-
455
- | Argument / Option | Type | Default | Description |
456
- | -------------------------- | --------------------- | ------- | ---------------------------------------- |
457
- | `[prompt]` | `string` (positional) | — | Prompted interactively if omitted |
458
- | `-s, --scenes <number>` | `string → int` | `5` | Scene count |
459
- | `-r, --ratio <ratio>` | `string` | `16:9` | Aspect ratio (`16:9`, `9:16`, `1:1`, …) |
460
- | `-d, --duration <seconds>` | `string → int` | `60` | Target duration in seconds |
461
- | `-l, --language <lang>` | `string` | `en` | ISO 639-1 language code |
462
-
463
- POST `/v1/platform/videos` body:
464
-
465
- ```json
466
- {
467
- "prompt": "...",
468
- "sceneCount": 5,
469
- "aspectRatio": "16:9",
470
- "targetDurationSeconds": 60,
471
- "language": "en",
472
- "mode": "sync"
473
- }
262
+ git clone https://github.com/tutorflow-io/cli.git && cd cli
263
+ pnpm install
264
+ pnpm build && pnpm test # the e2e suites spawn dist/index.js
265
+ pnpm dev -- course list # run from source
266
+ pnpm link # expose `tutorflow` globally
474
267
  ```
475
268
 
476
- Output: `id`, `status`, `sceneCount`, `previewUrl` (if returned). Prompts to open in browser.
269
+ Try it against staging with a throwaway account:
477
270
 
478
271
  ```bash
479
- tutorflow video create "Intro to Newton's laws" -s 6 -r 16:9 -d 90 -l en
272
+ export HOME=$(mktemp -d) TUTORFLOW_BASE_URL=https://dev-api.tutorflow.io
273
+ node dist/index.js agent register --name "CLI test" --slug cli-test-$(date +%s)
274
+ node dist/index.js course generate "Water cycle" --lessons 1 --wait
480
275
  ```
481
276
 
482
- #### `video list`
483
-
484
- | Option | Type | Default | Description |
485
- | ----------------------- | -------- | ------- | -------------------------- |
486
- | `-l, --limit <number>` | `string` | `10` | Max rows |
487
- | `-s, --status <status>` | `string` | none | Server-side status filter |
488
-
489
- GET `/v1/platform/videos?limit=<n>[&status=<s>]`. Table columns: `ID`, `Title`, `Status`, `Scenes`, `Created`.
490
-
491
- #### `video open <id>`
492
-
493
- Opens the video preview URL in the browser.
494
-
495
- ---
496
-
497
- ### `tutorflow course`
498
-
499
- Create and manage multi-lesson courses.
500
-
501
- #### `course create [prompt]`
502
-
503
- | Argument / Option | Type | Default | Description |
504
- | -------------------------- | --------------------- | ------- | --------------------------------- |
505
- | `[prompt]` | `string` (positional) | — | Prompted interactively if omitted |
506
- | `-l, --lessons <number>` | `string → int` | `8` | Lesson count |
507
- | `-lang, --language <lang>` | `string` | `en` | ISO 639-1 language code |
508
-
509
- > Note: `--language` uses the short flag `-lang` (not `-L`), because `-l` is already taken by `--lessons`.
277
+ ### Layout
510
278
 
511
- POST `/v1/platform/courses` body:
512
-
513
- ```json
514
- {
515
- "prompt": "...",
516
- "lessonCount": 8,
517
- "language": "en",
518
- "mode": "sync"
519
- }
520
279
  ```
521
-
522
- ```bash
523
- tutorflow course create "Beginner Python in Korean" -l 10 -lang ko
280
+ src/
281
+ ├── index.ts # commander entrypoint and global flags (--json, --debug, --classroom)
282
+ ├── client.ts # ApiClient: config resolution, HTTP, error envelope → CliError
283
+ ├── config.ts # ~/.tutorflow/config.json (mode 0600)
284
+ ├── content/
285
+ │ ├── context.ts # tf_content_ key check and classroom resolution
286
+ │ ├── resource.ts # shared list/get/open/delete/generate subcommands
287
+ │ ├── sharing.ts # shared share/visibility subcommands
288
+ │ └── runs.ts # waiting on runs, game/simulation builds and renders
289
+ ├── commands/ # one file per top-level command
290
+ ├── utils/ # runtime (JSON mode, CliError), output, args, keys
291
+ └── testing/harness.ts # test-only: fake API server and CLI spawner
524
292
  ```
525
293
 
526
- #### `course list`
527
-
528
- | Option | Type | Default | Description |
529
- | ---------------------- | -------- | ------- | ----------- |
530
- | `-l, --limit <number>` | `string` | `10` | Max rows |
531
-
532
- GET `/v1/platform/courses?limit=<n>`. Table columns: `ID`, `Title`, `Status`, `Created`.
533
-
534
- #### `course open <id>`
535
-
536
- Opens the course preview URL in the browser.
537
-
538
- ---
539
-
540
- ### `tutorflow module`
541
-
542
- Create and manage standalone lesson modules.
543
-
544
- #### `module create [prompt]`
545
-
546
- POST `/v1/platform/modules`. Generates one classroom-compatible standalone
547
- lesson. `--idempotency-key` is sent as the `Idempotency-Key` header so agents
548
- can safely retry a create call.
294
+ ## Releasing
549
295
 
550
- | Argument / Option | Type | Default | Description |
551
- | ------------------------------ | --------------------- | -------- | -------------------------------- |
552
- | `[prompt]` | `string` (positional) | — | Lesson/module prompt |
553
- | `--title <title>` | `string` | none | Override generated title |
554
- | `--description <description>` | `string` | none | Override generated description |
555
- | `--target <target>` | `string` | none | Target learner audience |
556
- | `--type <type>` | `string` | `lesson` | Module type |
557
- | `--locale <locale>` | `string` | `en` | Locale code |
558
- | `--language <language>` | `string` | `en` | Language code |
559
- | `--slug <slug>` | `string` | none | Optional slug |
560
- | `--private` | boolean | false | Set `isPublic: false` |
561
- | `--no-quiz` | boolean | false | Set `hasQuiz: false` |
562
- | `--idempotency-key <key>` | `string` | none | Safe retry key |
296
+ Releases are automated by GitHub Actions on pushes to `master` / `main`.
297
+ Auth uses **npm OIDC Trusted Publishing** — no `NPM_TOKEN` secret in CI.
563
298
 
564
299
  ```bash
565
- tutorflow --json module create "Fractions for 5th grade" \
566
- --title "Fractions" \
567
- --target "5th grade" \
568
- --no-quiz \
569
- --idempotency-key lesson-001
570
- ```
571
-
572
- #### `module list`
573
-
574
- GET `/v1/platform/modules?limit=<n>&offset=<n>[&search=<q>]`.
300
+ git checkout master
301
+ git pull
575
302
 
576
- ```bash
577
- tutorflow --json module list --limit 20 --offset 0 --search fractions
303
+ npm version patch --no-git-tag-version # 0.3.0 → 0.3.1 (or minor / major)
304
+ git push origin master # Release workflow publishes if that version is not on npm yet
578
305
  ```
579
306
 
580
- #### `module get <id>` / `module open <id>`
581
-
582
- Fetch a module by ID or open its `publicUrl`.
583
-
584
- ---
585
-
586
- ### `tutorflow test`
587
-
588
- Create and manage generated assessments.
589
-
590
- #### `test create [prompt]`
591
-
592
- POST `/v1/platform/tests`. Generates a test/assessment. `--idempotency-key` is
593
- sent as the `Idempotency-Key` header.
594
-
595
- | Argument / Option | Type | Default | Description |
596
- | ------------------------- | --------------------- | ---------- | ------------------------------ |
597
- | `[prompt]` | `string` (positional) | — | Test generation prompt |
598
- | `--title <title>` | `string` | none | Override generated title |
599
- | `--description <text>` | `string` | none | Override generated description |
600
- | `-l, --language <lang>` | `string` | `en` | Language code |
601
- | `--level <level>` | `string` | none | Difficulty level |
602
- | `--subject <subject>` | `string` | none | Subject area |
603
- | `-n, --items <number>` | `string -> int` | none | Target item count |
604
- | `--time-limit <minutes>` | `string -> int` | none | Time limit in minutes |
605
- | `--classroom-id <id>` | `string` | none | Classroom ID |
606
- | `--tier <tier>` | `string` | `standard` | Pricing tier |
607
- | `--mode <mode>` | `string` | `sync` | `sync` or `async` |
608
- | `--idempotency-key <key>` | `string` | none | Safe retry key |
307
+ The [`Release` workflow](.github/workflows/release.yml):
609
308
 
610
- ```bash
611
- tutorflow --json test create "Algebra quiz" \
612
- --title "Algebra Basics" \
613
- --items 8 \
614
- --time-limit 30 \
615
- --idempotency-key test-001
616
- ```
309
+ 1. Reads `package.json` name and version
310
+ 2. Skips npm publish if that exact version already exists
311
+ 3. Runs `pnpm typecheck` + `pnpm build` + `pnpm test`
312
+ 4. Publishes new versions with `npm publish --access public` (auth via OIDC)
313
+ 5. Creates a GitHub Release only for `v*.*.*` tag pushes
617
314
 
618
- #### `test list`
315
+ For full setup (npm org, Trusted Publisher config) see [RELEASING.md](RELEASING.md).
619
316
 
620
- GET `/v1/platform/tests?limit=<n>&offset=<n>[&status=<s>]`.
317
+ ### Pre-release / beta
621
318
 
622
319
  ```bash
623
- tutorflow --json test list --limit 20 --status READY
320
+ npm version prerelease --preid=beta --no-git-tag-version # 0.3.1 → 0.3.2-beta.0
321
+ git push origin master
624
322
  ```
625
323
 
626
- #### `test get <id>` / `test open <id>` / `test edit-token <id>`
627
-
628
- Fetch a test, open its preview URL, or generate a fresh edit token.
629
-
630
- ---
631
-
632
- ### `tutorflow evaluate`
633
-
634
- Evaluates a single learner response through the Platform Evaluations API.
635
-
636
- | Option | Type | Default | Allowed values / notes |
637
- | ------------------------- | -------- | ---------- | ---------------------- |
638
- | `-t, --type <type>` | `string` | (prompted) | `exact`, `rubric_short_answer`, `open_ended`. Aliases: `rubric`, `open-ended` |
639
- | `-q, --question <text>` | `string` | (prompted) | Question text |
640
- | `-a, --student-answer <text>` | `string` | (prompted) | Learner answer |
641
- | `-e, --expected-answer <text>` | `string` | none | Required for `exact`; sent as `referenceAnswer` |
642
- | `--rubric-json <json>` | `string` | none | Required for `rubric_short_answer` |
643
- | `--max-score <number>` | `number` | backend default | Maximum score |
644
- | `-l, --language <lang>` | `string` | `en` | Feedback language |
645
- | `--idempotency-key <key>` | `string` | none | Safe retry key |
646
-
647
- Interactive flow:
648
-
649
- 1. Type (if not supplied)
650
- 2. Question
651
- 3. Student answer
652
- 4. Expected answer — required only when type is `exact`
653
-
654
- POST `/v1/platform/evaluations` body:
655
-
656
- ```json
657
- {
658
- "evaluationType": "exact",
659
- "questionText": "What is 2+2?",
660
- "learnerAnswer": "4",
661
- "referenceAnswer": "4",
662
- "language": "en",
663
- "mode": "sync"
664
- }
665
- ```
324
+ Pre-release versions publish to a dist-tag derived from the prerelease id
325
+ (`0.3.1-beta.0` publishes with `--tag beta`).
666
326
 
667
- `referenceAnswer` is omitted when `evaluationType` is `open_ended`.
668
- `rubric_short_answer` requires `--rubric-json`, for example:
327
+ ## ⚠️ Caveats & gotchas
669
328
 
670
- ```bash
671
- tutorflow --json evaluate \
672
- --type rubric_short_answer \
673
- --question "Explain the water cycle" \
674
- --student-answer "Water evaporates and condenses." \
675
- --rubric-json '{"criteria":[{"name":"Accuracy","description":"Scientific correctness","maxScore":10}]}'
676
- ```
329
+ These tripped us up during initial setup. Read before changing release infrastructure.
677
330
 
678
- ---
331
+ ### Versioning
679
332
 
680
- ### `tutorflow auth`
333
+ - **Don't hardcode the version in `src/index.ts`.** It reads `version` from [`package.json`](package.json) via `resolveJsonModule`. Hardcoding will silently lie to users via `--version` after every `npm version` bump.
334
+ - The `prepublishOnly` script in `package.json` rebuilds `dist/` before publish. Don't skip it.
681
335
 
682
- | Subcommand | Description |
683
- | ---------- | ---------------------------------------------------- |
684
- | `login` | Interactive prompt for API key, writes to config |
685
- | `logout` | Prompts to confirm, then deletes config file |
686
- | `whoami` | Prints masked API key (first 10 chars) and `baseUrl` |
336
+ ### npm Trusted Publishing (OIDC)
687
337
 
688
- ## Exit behavior & errors
338
+ - **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.
339
+ - The Trusted Publisher binding at [npmjs.com/package/@tutorflow/cli/access](https://www.npmjs.com/package/@tutorflow/cli/access) hardcodes:
340
+ - GitHub org: `tutorflow-io`
341
+ - Repo: `cli`
342
+ - Workflow filename: `release.yml`
343
+ - Environment: (none)
689
344
 
690
- - 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 (typically `0`). Don't rely on exit codes for scripting yet.
691
- - `await prompt(...)` from `@clack/prompts` returns a `symbol` when the user cancels (Ctrl+C); commands check for this and print `Cancelled` instead of throwing.
692
- - HTTP errors from the API are surfaced as `Error: API Error: <status> <body-json>`.
345
+ Renaming the repo, moving the workflow file, or adding an `environment:` to the publish job will break OIDC auth until the binding is updated.
346
+ - 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.
693
347
 
694
- ## API client
348
+ ### Provenance
695
349
 
696
- Internal HTTP client lives in [src/client.ts](src/client.ts):
350
+ - `--provenance` is **disabled** in [release.yml](.github/workflows/release.yml). npm only accepts provenance attestations from **public** GitHub repos. Add `--provenance` back to the `npm publish` command when this repo goes public.
697
351
 
698
- ```ts
699
- class ApiClient {
700
- static async create(): Promise<ApiClient>; // loads from ~/.tutorflow/config.json
701
- request<T>(method: string, endpoint: string, body?: object): Promise<T>;
702
- setApiKey(apiKey: string): void;
703
- setBaseUrl(baseUrl: string): void;
704
- }
705
- ```
352
+ ### Node + tooling
706
353
 
707
- - Uses [`undici.fetch`](https://github.com/nodejs/undici).
708
- - Headers always include `Content-Type: application/json`. Authenticated requests also include `Authorization: Bearer <apiKey>`.
709
- - Throws `Error('API Error: <status> <body>')` on non-2xx.
354
+ - **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`).
710
355
 
711
- ## Project layout
356
+ ### Tarball hygiene
712
357
 
713
- ```
714
- .
715
- ├── package.json # bin: { tutorflow: ./dist/index.js }
716
- ├── tsconfig.json
717
- ├── vitest.config.ts
718
- ├── .github/workflows/
719
- │ ├── ci.yml # typecheck + build + test on PRs and master
720
- │ └── release.yml # OIDC publish on master/main pushes when version is new
721
- ├── RELEASING.md # full release procedure + Trusted Publisher setup
722
- └── src/
723
- ├── index.ts # commander entrypoint, registers all commands
724
- ├── client.ts # undici-based ApiClient
725
- ├── client.test.ts
726
- ├── config.ts # ~/.tutorflow/config.json read/write/update/delete
727
- ├── config.test.ts
728
- ├── types.ts # API response interfaces (SlideResource, etc.)
729
- ├── commands/
730
- │ ├── agent.ts # register / account / billing / email verification
731
- │ ├── auth.ts # login / logout / whoami
732
- │ ├── config.ts # get / set
733
- │ ├── course.ts # create / list / open
734
- │ ├── evaluate.ts # interactive evaluation
735
- │ ├── module.ts # standalone lesson modules
736
- │ ├── platform.ts # discovery / pricing / health
737
- │ ├── slide.ts # create / list / open
738
- │ ├── test.ts # generated assessments
739
- │ └── video.ts # create / list / open
740
- └── utils/
741
- ├── output.ts # formatTable, formatJson
742
- └── spinner.ts
743
- ```
358
+ - `tsconfig.json` excludes `**/*.test.ts` and `src/testing/` so test code doesn't end up in `dist/` (and therefore not in the published tarball).
359
+ - `package.json#files` is an allowlist (`dist`, `README.md`, `LICENSE`). Anything not listed is excluded.
360
+ - Run `npm pack --dry-run` to inspect the tarball before publish.
744
361
 
745
- ## npm scripts
362
+ ### Source maps and a private repo
746
363
 
747
- | Script | Command | Purpose |
748
- | ---------------- | ----------------------- | --------------------------------------- |
749
- | `dev` | `tsx src/index.ts` | Run from source |
750
- | `build` | `tsc --outDir dist` | Emit JS + `.d.ts` to `dist/` |
751
- | `link` | `npm link` | Symlink `tutorflow` globally |
752
- | `typecheck` | `tsc --noEmit` | Type-check without emitting |
753
- | `test` | `vitest run` | Run all tests once |
754
- | `test:watch` | `vitest` | Watch-mode tests |
755
- | `test:coverage` | `vitest run --coverage` | Generate coverage report |
756
- | `prepublishOnly` | `rm -rf dist && build` | Auto-runs before `npm publish` |
364
+ 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](tsconfig.json) before going public.
757
365
 
758
366
  ## Stack
759
367
 
760
- - **CLI framework:** [commander](https://github.com/tj/commander.js) `^11.0.0`
761
- - **Prompts:** [@clack/prompts](https://github.com/natemoo-re/clack) `^0.7.0`
762
- - **HTTP:** [undici](https://github.com/nodejs/undici) `^5.27.0`
763
- - **Color:** [chalk](https://github.com/chalk/chalk) `^5.3.0`
764
- - **Browser open:** [open](https://github.com/sindresorhus/open) `^9.0.0`
765
- - **Tests:** [Vitest](https://vitest.dev) `^4.1.5`
766
- - **Dev:** TypeScript `^5.0.0`, tsx `^3.0.0`, `@types/node` `^20.0.0`
368
+ [commander](https://github.com/tj/commander.js) 11, [@clack/prompts](https://github.com/natemoo-re/clack) 0.7, [undici](https://github.com/nodejs/undici) 5, [chalk](https://github.com/chalk/chalk) 5, [open](https://github.com/sindresorhus/open) 9; Vitest 4, TypeScript 5, tsx 3.