@tutorflow/cli 0.3.0 → 0.4.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 (101) hide show
  1. package/README.md +246 -650
  2. package/dist/client.d.ts +18 -3
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +85 -18
  5. package/dist/client.js.map +1 -1
  6. package/dist/commands/agent.d.ts.map +1 -1
  7. package/dist/commands/agent.js +41 -26
  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 +25 -91
  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 +135 -44
  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 +100 -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 +18 -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 +12 -4
  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 +25 -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 +38 -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 +138 -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/index.js +71 -21
  78. package/dist/index.js.map +1 -1
  79. package/dist/types.d.ts +17 -64
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/types.js +4 -4
  82. package/dist/utils/args.d.ts +15 -0
  83. package/dist/utils/args.d.ts.map +1 -0
  84. package/dist/utils/args.js +91 -0
  85. package/dist/utils/args.js.map +1 -0
  86. package/dist/utils/keys.d.ts +11 -0
  87. package/dist/utils/keys.d.ts.map +1 -0
  88. package/dist/utils/keys.js +29 -0
  89. package/dist/utils/keys.js.map +1 -0
  90. package/dist/utils/output.d.ts.map +1 -1
  91. package/dist/utils/output.js +21 -6
  92. package/dist/utils/output.js.map +1 -1
  93. package/dist/utils/runtime.d.ts +14 -4
  94. package/dist/utils/runtime.d.ts.map +1 -1
  95. package/dist/utils/runtime.js +42 -7
  96. package/dist/utils/runtime.js.map +1 -1
  97. package/package.json +4 -3
  98. package/dist/utils/spinner.d.ts +0 -8
  99. package/dist/utils/spinner.d.ts.map +0 -1
  100. package/dist/utils/spinner.js +0 -26
  101. package/dist/utils/spinner.js.map +0 -1
package/README.md CHANGED
@@ -1,67 +1,274 @@
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:
13
+ ## Quick start
16
14
 
17
15
  ```bash
18
- npm install -g @tutorflow/cli
19
- pnpm add -g @tutorflow/cli
20
- yarn global add @tutorflow/cli
21
- ```
16
+ npm install -g @tutorflow/cli # or: npx @tutorflow/cli <command>
22
17
 
23
- Then anywhere on the system:
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
24
20
 
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
- ```
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
32
25
 
33
- **One-shot use without installing**:
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
34
30
 
35
- ```bash
36
- npx @tutorflow/cli slide create "My first deck"
31
+ # 4. Check what it cost.
32
+ tutorflow credits balance
37
33
  ```
38
34
 
39
- This is the recommended path for AI agents and CI environments — no install step, always pulls the latest published version.
35
+ ## Keys and classrooms
36
+
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. |
42
+
43
+ Every content command acts in one classroom, chosen in this order:
44
+
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
50
+
51
+ `tutorflow classroom list` shows the classrooms the key can reach.
52
+
53
+ ## Commands
54
+
55
+ All ids are UUIDs. Every command accepts `--json`.
56
+
57
+ ### Account and discovery (`/v1/platform`)
58
+
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 |
71
+
72
+ ### Generated content (`/v1/content/classrooms/{classroomId}/…`)
73
+
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 |
40
80
 
41
- ### B. Local development (this repo)
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`.
82
+
83
+ Each of `course`, `slide`, `test`, `module`, `video`, `game` and `simulation` also has:
84
+
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
+
92
+ `test results <testId>` lists learners' results (`GET …/tests/{id}/results`).
93
+
94
+ ### Runs
95
+
96
+ | Command | Route |
97
+ | --- | --- |
98
+ | `run list [--kind course\|slide\|test\|module] [--status] [--limit] [--cursor]` | `GET …/runs` |
99
+ | `run get <runId>` | `GET …/runs/{runId}` |
100
+ | `run wait <runId> [--timeout]` | polls `GET …/runs/{runId}`; prints `{ run, resource }` |
101
+ | `run resume <runId>` | `POST …/runs/{runId}/resume` (course runs only) |
102
+
103
+ 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.
104
+
105
+ ### Videos
106
+
107
+ The Content API builds videos from scenes you script; it does not write the script from a prompt.
108
+
109
+ | Command | Route |
110
+ | --- | --- |
111
+ | `video create --title [--description] [--visibility] [--scene "<script>"]…` | `POST …/videos`, then one scene per `--scene` in order |
112
+ | `video scene add <videoId> [--script] [--display-text] [--duration] [--after <sceneId>]` | `POST …/videos/{id}/scenes`, then `PATCH` the new scene |
113
+ | `video scene update <videoId> <sceneId> [--script] [--display-text] [--duration]` | `PATCH …/scenes/{sceneId}` |
114
+ | `video scene delete <videoId> <sceneId> --yes` | `DELETE …/scenes/{sceneId}` |
115
+ | `video narrate <videoId> [--scene <sceneId>]` | `POST …/narration` (1 credit per scene) |
116
+ | `video render <videoId> [--generate-missing-narration] [--wait]` | `POST …/videos/{id}/render` (5 credits; `--wait` polls until `COMPLETED` and prints the mp4 URL) |
117
+ | `video render-status <videoId>` / `video render-cancel <videoId>` | `GET …/render` / `POST …/render/cancel` |
118
+
119
+ ```bash
120
+ tutorflow video create --title "Why the sky is blue" \
121
+ --scene "Sunlight is made of many colors." \
122
+ --scene "Air scatters blue light the most, so the sky looks blue."
123
+ tutorflow video render <videoId> --generate-missing-narration --wait
124
+ ```
125
+
126
+ ### Games and simulations
127
+
128
+ `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.
129
+
130
+ | Command | Route | Credits |
131
+ | --- | --- | --- |
132
+ | `create --title [--description] [--visibility PRIVATE\|PUBLIC]` | `POST …/{games\|simulations}` | 0 |
133
+ | `brief <id> [--reference <markdown>] [--wait]` | `POST …/{id}/brief` | 1 |
134
+ | `build <id> [--wait]` | `POST …/{id}/build` | game 40, simulation 20 (2D) or 30 (3D) |
135
+ | `revise <id> --feedback <text> [--wait]` | `POST …/{id}/revise` | as build |
136
+ | `status <id>` | `GET …/{id}/run` | |
137
+
138
+ ### Evaluations and learners
139
+
140
+ | Command | Route |
141
+ | --- | --- |
142
+ | `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) |
143
+ | `evaluation list [--learner-id] [--status] [--type] [--limit] [--cursor]` / `evaluation get <id>` | `GET …/evaluations` |
144
+ | `evaluation concepts` / `evaluation mastery <learnerId>` | `GET …/concepts` / `GET …/mastery/{learner}` |
145
+ | `learner list` / `get <id>` / `progress <id>` | `GET …/learners…` (live keys only) |
146
+ | `learner invite --email [--name] [--locale] [--course <id>]… [--test <id>]…` | `POST …/learners/invitations` |
147
+ | `learner remove <id> --yes` | `DELETE …/learners/{id}` |
148
+
149
+ `--type` takes `exact`, `rubric_short_answer` or `open_ended` (aliases `rubric`, `open-ended`). `exact` needs `--expected-answer`; `rubric_short_answer` needs `--rubric-json`.
150
+
151
+ ### Webhooks and credits (`/v1/content`)
152
+
153
+ | Command | Route |
154
+ | --- | --- |
155
+ | `webhook list` / `get <id>` | `GET /v1/content/webhooks[/{id}]` |
156
+ | `webhook create --url --event <event>… [--mode live\|test]` | `POST /v1/content/webhooks` (the signing secret is shown once) |
157
+ | `webhook test <id>` / `deliveries <id>` / `rotate-secret <id>` / `delete <id> --yes` | the matching `/v1/content/webhooks/{id}/…` routes |
158
+ | `credits balance` | `GET /v1/content/credits` |
159
+ | `credits history [--limit] [--cursor] [--from] [--to] [--type]` | `GET /v1/content/credits/history` |
160
+
161
+ 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.
162
+
163
+ ### Auth and config
164
+
165
+ | Command | Effect |
166
+ | --- | --- |
167
+ | `auth login --api-key <key>` | Save a key (prompts on a TTY when omitted) |
168
+ | `auth whoami` | Masked key, `keyKind` (`content_live`, `content_test`, `platform`, `unknown`), base URL, `source` (`env` or `config`). No network call. |
169
+ | `auth logout --force` | Delete `~/.tutorflow/config.json` |
170
+ | `config get [key]` / `config set <key> <value>` | Keys: `apiKey`, `baseUrl`, `classroomId` |
171
+
172
+ The config file is written with mode `0600`.
173
+
174
+ ## Agent usage
175
+
176
+ ```bash
177
+ export TUTORFLOW_API_KEY=tf_content_xxx
178
+ export TUTORFLOW_JSON=1
179
+
180
+ run=$(tutorflow course generate "Fractions for 5th grade" --lessons 2)
181
+ tutorflow run wait "$(echo "$run" | jq -r .runId)" | jq -r .resource.publicUrl
182
+ ```
183
+
184
+ - **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.
185
+ - **No prompts without a TTY:** a missing required input exits 2 with `ENOINTERACTIVE` and names the flag to pass. Destructive commands need `--yes`.
186
+ - **Retries:** create and generate commands accept `--idempotency-key`.
187
+ - **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`.
188
+
189
+ | Exit | Meaning |
190
+ | --- | --- |
191
+ | `0` | Success |
192
+ | `1` | Failure: auth, network, API error, failed run, timeout |
193
+ | `2` | Bad input: invalid flag, missing input without a TTY, ambiguous classroom |
194
+ | `130` | Cancelled at a prompt |
195
+
196
+ | Code | When |
197
+ | --- | --- |
198
+ | `EAUTH` | No API key |
199
+ | `EPLATFORMKEY` | A `tf_platform_` key on a content command |
200
+ | `ECLASSROOM` | No classroom could be chosen; pass `--classroom` |
201
+ | `EINVAL` | Invalid flag value |
202
+ | `ENOINTERACTIVE` | Missing input or confirmation without a TTY |
203
+ | `EHTTP_<status>` | API error; see `apiCode` |
204
+ | `ENETWORK` | The API could not be reached |
205
+ | `ERUNFAILED` | A waited run, build or render failed |
206
+ | `ETIMEOUT` | `--wait` gave up; the work continues on the server |
207
+ | `ENOPREVIEW` | `open` found no URL |
208
+ | `ENOTFOUND` | `config get` of an unset key |
209
+ | `ECANCELLED` | Prompt cancelled |
210
+
211
+ | Environment variable | Effect |
212
+ | --- | --- |
213
+ | `TUTORFLOW_API_KEY` | API key (wins over the config file) |
214
+ | `TUTORFLOW_BASE_URL` | API origin, e.g. `https://dev-api.tutorflow.io` for staging |
215
+ | `TUTORFLOW_CLASSROOM_ID` | Classroom for content commands |
216
+ | `TUTORFLOW_JSON=1` | JSON mode |
217
+ | `TUTORFLOW_DEBUG=1` / `DEBUG=1` | Stack traces on errors |
218
+ | `TUTORFLOW_POLL_INTERVAL_MS` | `--wait` poll interval (default 3000) |
219
+
220
+ Empty environment variables are ignored.
221
+
222
+ ## Migrating from 0.3
223
+
224
+ | 0.3 | Now |
225
+ | --- | --- |
226
+ | `tf_platform_` key on every command | `tf_content_` key; `agent register` saves it. Platform keys still work on `agent` and `platform`. |
227
+ | `/v1/platform/{courses,slides,tests,modules,videos,evaluations}` | `/v1/content/classrooms/{classroomId}/…` |
228
+ | `course create` returned the finished course (`mode: sync`) | `course generate` (alias `create`) returns a run; add `--wait` |
229
+ | `course create -l 10 -lang ko` | `course generate --lessons 10 --language ko` |
230
+ | `slide create -t <theme>` | `slide generate --theme <themeId>` (5 to 20 pages) |
231
+ | `video create "<prompt>" -s 6 -r 16:9 -d 90` | `video create --title … --scene "<script>"…`, then `video render --generate-missing-narration --wait` |
232
+ | `module create --title --target --locale --private --no-quiz` | `module generate [--type] [--reference]`; edit the rest in the classroom editor |
233
+ | `test create --classroom-id --tier --mode --time-limit` | `test generate` with the global `--classroom`; generation is always asynchronous |
234
+ | `test edit-token`, `previewUrl` | `editorUrl` and `publicUrl` on every resource; `open <id>` |
235
+ | list `--offset` | `--page` |
236
+ | `evaluate` → `/v1/platform/evaluations` | `evaluate` → `…/evaluations`, plus `--learner-id` and `--concept-tags` |
237
+
238
+ 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`.
239
+
240
+ ## Development
42
241
 
43
242
  ```bash
44
- git clone https://github.com/tutorflow-io/cli.git
45
- cd cli
243
+ git clone https://github.com/tutorflow-io/cli.git && cd cli
46
244
  pnpm install
47
- pnpm build
48
- pnpm link # exposes the `tutorflow` binary globally
245
+ pnpm build && pnpm test # the e2e suites spawn dist/index.js
246
+ pnpm dev -- course list # run from source
247
+ pnpm link # expose `tutorflow` globally
49
248
  ```
50
249
 
51
- Verify:
250
+ Try it against staging with a throwaway account:
52
251
 
53
252
  ```bash
54
- tutorflow --version # matches package.json
55
- tutorflow --help
253
+ export HOME=$(mktemp -d) TUTORFLOW_BASE_URL=https://dev-api.tutorflow.io
254
+ node dist/index.js agent register --name "CLI test" --slug cli-test-$(date +%s)
255
+ node dist/index.js course generate "Water cycle" --lessons 1 --wait
56
256
  ```
57
257
 
58
- To unlink later: `npm unlink -g @tutorflow/cli`.
258
+ ### Layout
59
259
 
60
- Run from source without building:
61
-
62
- ```bash
63
- pnpm dev -- <command> [options]
64
- # e.g. pnpm dev -- slide list --limit 5
260
+ ```
261
+ src/
262
+ ├── index.ts # commander entrypoint and global flags (--json, --debug, --classroom)
263
+ ├── client.ts # ApiClient: config resolution, HTTP, error envelope → CliError
264
+ ├── config.ts # ~/.tutorflow/config.json (mode 0600)
265
+ ├── content/
266
+ │ ├── context.ts # tf_content_ key check and classroom resolution
267
+ │ ├── resource.ts # shared list/get/open/delete/generate subcommands
268
+ │ └── runs.ts # waiting on runs, game/simulation builds and renders
269
+ ├── commands/ # one file per top-level command
270
+ ├── utils/ # runtime (JSON mode, CliError), output, args, keys
271
+ └── testing/harness.ts # test-only: fake API server and CLI spawner
65
272
  ```
66
273
 
67
274
  ## Releasing
@@ -128,7 +335,7 @@ These tripped us up during initial setup. Read before changing release infrastru
128
335
 
129
336
  ### Tarball hygiene
130
337
 
131
- - `tsconfig.json` excludes `**/*.test.ts` so test files don't end up in `dist/` (and therefore not in the published tarball).
338
+ - `tsconfig.json` excludes `**/*.test.ts` and `src/testing/` so test code doesn't end up in `dist/` (and therefore not in the published tarball).
132
339
  - `package.json#files` is an allowlist (`dist`, `README.md`, `LICENSE`). Anything not listed is excluded.
133
340
  - Run `npm pack --dry-run` to inspect the tarball before publish.
134
341
 
@@ -136,617 +343,6 @@ These tripped us up during initial setup. Read before changing release infrastru
136
343
 
137
344
  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.
138
345
 
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
146
- ```
147
-
148
- Build artifacts land in `dist/` (entry: `dist/index.js`, declared in `package.json#bin`).
149
-
150
- ## Non-interactive / agent usage
151
-
152
- Every command supports the contract below so AI agents and CI scripts can use the CLI without a TTY.
153
-
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
- ```
164
-
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.
166
-
167
- ### Machine-readable output
168
-
169
- `--json` (or `TUTORFLOW_JSON=1`) makes every command emit a single JSON line on stdout. Errors emit JSON to stderr.
170
-
171
- ```bash
172
- $ tutorflow --json auth whoami
173
- {"authenticated":true,"apiKeyPreview":"tf_xxxxxxx...","baseUrl":"https://api.tutorflow.io","source":"env"}
174
-
175
- $ tutorflow --json auth whoami # without auth
176
- # stdout: (empty)
177
- # stderr: {"error":{"message":"Not authenticated","code":"EAUTH"}}
178
- # exit: 1
179
- ```
180
-
181
- List commands return raw arrays:
182
-
183
- ```bash
184
- $ tutorflow --json slide list --limit 3
185
- [{"id":"...","title":"...","status":"ready","createdAt":"..."},...]
186
- ```
187
-
188
- Pipe into `jq` for downstream automation:
189
-
190
- ```bash
191
- tutorflow --json slide list --status ready | jq -r '.[].id' | xargs -I{} tutorflow --json slide get {}
192
- ```
193
-
194
- ### Exit codes
195
-
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) |
202
-
203
- The error JSON includes a stable `code`: `EAUTH`, `EINVAL`, `ENOINTERACTIVE`, `ENETWORK`, `EHTTP_<status>`, `ENOPREVIEW`, `ENOTFOUND`, `ECANCELLED`.
204
-
205
- ### Required-input flags (no prompts)
206
-
207
- Commands that have required input expose every prompt as a flag:
208
-
209
- ```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 rubric \
225
- --question "What is 2+2?" \
226
- --student-answer "4" \
227
- --expected-answer "4"
228
-
229
- # logout (skip confirmation)
230
- tutorflow auth logout --force
231
- ```
232
-
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.
234
-
235
- ### Globals
236
-
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) |
243
-
244
- ### MCP server
245
-
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.
247
-
248
- ## Authentication
249
-
250
- Generate an API key in your TutorFlow workspace, then:
251
-
252
- ```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
256
- ```
257
-
258
- Credentials are persisted to `~/.tutorflow/config.json`:
259
-
260
- ```json
261
- {
262
- "apiKey": "tf_xxxxxxxxxxxxxxxxxxxx",
263
- "baseUrl": "https://api.tutorflow.io",
264
- "workspace": "optional-workspace-slug"
265
- }
266
- ```
267
-
268
- The API key is sent as the `X-API-Key` header on every request.
269
-
270
- ### Config schema
271
-
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 |
293
-
294
- `config set` rejects unknown keys.
295
-
296
- ## Commands
297
-
298
- All resource commands require authentication. Without an API key they print:
299
- `✗ Not authenticated. Run "tutorflow auth login" first.`
300
-
301
- ### `tutorflow agent`
302
-
303
- Manage the agent-facing TutorFlow Platform account flow.
304
-
305
- #### `agent register`
306
-
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`.
332
-
333
- ```bash
334
- tutorflow --json agent account
335
- ```
336
-
337
- #### `agent billing status`
338
-
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
344
- ```
345
-
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
- ```
354
-
355
- #### `agent verify-email resend`
356
-
357
- POST `/v1/platform/agent/verify-email/resend`. Resends the workspace email
358
- verification message using the active API key.
359
-
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.
427
-
428
- ```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
- }
474
- ```
475
-
476
- Output: `id`, `status`, `sceneCount`, `previewUrl` (if returned). Prompts to open in browser.
477
-
478
- ```bash
479
- tutorflow video create "Intro to Newton's laws" -s 6 -r 16:9 -d 90 -l en
480
- ```
481
-
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`.
510
-
511
- POST `/v1/platform/courses` body:
512
-
513
- ```json
514
- {
515
- "prompt": "...",
516
- "lessonCount": 8,
517
- "language": "en",
518
- "mode": "sync"
519
- }
520
- ```
521
-
522
- ```bash
523
- tutorflow course create "Beginner Python in Korean" -l 10 -lang ko
524
- ```
525
-
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.
549
-
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 |
563
-
564
- ```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>]`.
575
-
576
- ```bash
577
- tutorflow --json module list --limit 20 --offset 0 --search fractions
578
- ```
579
-
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 |
609
-
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
- ```
617
-
618
- #### `test list`
619
-
620
- GET `/v1/platform/tests?limit=<n>&offset=<n>[&status=<s>]`.
621
-
622
- ```bash
623
- tutorflow --json test list --limit 20 --status READY
624
- ```
625
-
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 student response. Fully interactive; only the type can be passed as a flag.
635
-
636
- | Option | Type | Default | Allowed values |
637
- | ------------------- | -------- | ---------- | ------------------------------- |
638
- | `-t, --type <type>` | `string` | (prompted) | `exact`, `rubric`, `open-ended` |
639
-
640
- Interactive flow:
641
-
642
- 1. Type (if not supplied)
643
- 2. Question
644
- 3. Student answer
645
- 4. Expected answer — **skipped** when type is `open-ended`
646
-
647
- POST `/v1/platform/evaluate` body:
648
-
649
- ```json
650
- {
651
- "evaluationType": "rubric",
652
- "question": "...",
653
- "studentAnswer": "...",
654
- "expectedAnswer": "..."
655
- }
656
- ```
657
-
658
- `expectedAnswer` is omitted when `evaluationType` is `open-ended`. Response prints `score` and `feedback`.
659
-
660
- ```bash
661
- tutorflow evaluate -t open-ended
662
- ```
663
-
664
- ---
665
-
666
- ### `tutorflow auth`
667
-
668
- | Subcommand | Description |
669
- | ---------- | ---------------------------------------------------- |
670
- | `login` | Interactive prompt for API key, writes to config |
671
- | `logout` | Prompts to confirm, then deletes config file |
672
- | `whoami` | Prints masked API key (first 10 chars) and `baseUrl` |
673
-
674
- ## Exit behavior & errors
675
-
676
- - 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.
677
- - `await prompt(...)` from `@clack/prompts` returns a `symbol` when the user cancels (Ctrl+C); commands check for this and print `Cancelled` instead of throwing.
678
- - HTTP errors from the API are surfaced as `Error: API Error: <status> <body-json>`.
679
-
680
- ## API client
681
-
682
- Internal HTTP client lives in [src/client.ts](src/client.ts):
683
-
684
- ```ts
685
- class ApiClient {
686
- static async create(): Promise<ApiClient>; // loads from ~/.tutorflow/config.json
687
- request<T>(method: string, endpoint: string, body?: object): Promise<T>;
688
- setApiKey(apiKey: string): void;
689
- setBaseUrl(baseUrl: string): void;
690
- }
691
- ```
692
-
693
- - Uses [`undici.fetch`](https://github.com/nodejs/undici).
694
- - Headers always include `Content-Type: application/json` and `X-API-Key: <apiKey>`.
695
- - Throws `Error('API Error: <status> <body>')` on non-2xx.
696
-
697
- ## Project layout
698
-
699
- ```
700
- .
701
- ├── package.json # bin: { tutorflow: ./dist/index.js }
702
- ├── tsconfig.json
703
- ├── vitest.config.ts
704
- ├── .github/workflows/
705
- │ ├── ci.yml # typecheck + build + test on PRs and master
706
- │ └── release.yml # OIDC publish on master/main pushes when version is new
707
- ├── RELEASING.md # full release procedure + Trusted Publisher setup
708
- └── src/
709
- ├── index.ts # commander entrypoint, registers all commands
710
- ├── client.ts # undici-based ApiClient
711
- ├── client.test.ts
712
- ├── config.ts # ~/.tutorflow/config.json read/write/update/delete
713
- ├── config.test.ts
714
- ├── types.ts # API response interfaces (SlideResource, etc.)
715
- ├── commands/
716
- │ ├── agent.ts # register / account / billing / email verification
717
- │ ├── auth.ts # login / logout / whoami
718
- │ ├── config.ts # get / set
719
- │ ├── course.ts # create / list / open
720
- │ ├── evaluate.ts # interactive evaluation
721
- │ ├── module.ts # standalone lesson modules
722
- │ ├── platform.ts # discovery / pricing / health
723
- │ ├── slide.ts # create / list / open
724
- │ ├── test.ts # generated assessments
725
- │ └── video.ts # create / list / open
726
- └── utils/
727
- ├── output.ts # formatTable, formatJson
728
- └── spinner.ts
729
- ```
730
-
731
- ## npm scripts
732
-
733
- | Script | Command | Purpose |
734
- | ---------------- | ----------------------- | --------------------------------------- |
735
- | `dev` | `tsx src/index.ts` | Run from source |
736
- | `build` | `tsc --outDir dist` | Emit JS + `.d.ts` to `dist/` |
737
- | `link` | `npm link` | Symlink `tutorflow` globally |
738
- | `typecheck` | `tsc --noEmit` | Type-check without emitting |
739
- | `test` | `vitest run` | Run all tests once |
740
- | `test:watch` | `vitest` | Watch-mode tests |
741
- | `test:coverage` | `vitest run --coverage` | Generate coverage report |
742
- | `prepublishOnly` | `rm -rf dist && build` | Auto-runs before `npm publish` |
743
-
744
346
  ## Stack
745
347
 
746
- - **CLI framework:** [commander](https://github.com/tj/commander.js) `^11.0.0`
747
- - **Prompts:** [@clack/prompts](https://github.com/natemoo-re/clack) `^0.7.0`
748
- - **HTTP:** [undici](https://github.com/nodejs/undici) `^5.27.0`
749
- - **Color:** [chalk](https://github.com/chalk/chalk) `^5.3.0`
750
- - **Browser open:** [open](https://github.com/sindresorhus/open) `^9.0.0`
751
- - **Tests:** [Vitest](https://vitest.dev) `^4.1.5`
752
- - **Dev:** TypeScript `^5.0.0`, tsx `^3.0.0`, `@types/node` `^20.0.0`
348
+ [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.