@novedu/cli 0.11.0 → 0.12.1

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 (3) hide show
  1. package/README.md +119 -25
  2. package/dist/main.js +39 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,43 +1,46 @@
1
1
  # @novedu/cli
2
2
 
3
3
  Command-line companion for the Novedu chat app (installed command: `novedu-cli`;
4
- requires Node >= 20). It validates every activity YAML the app accepts — **tutors**,
5
- **fragment libraries**, **quizzes**, **writing activities**, and **coding
6
- activities** — and signs in to Microsoft Entra ID (`login` / `logout` / `whoami`)
7
- to call the app's protected APIs; more commands will follow. Validating a tutor
8
- also fully validates every fragment library it references; pass `--kind` to
9
- validate any other kind on its own.
4
+ requires Node >= 20). It covers two jobs:
10
5
 
11
- It reuses the app's exact validation pipeline (`lib/tutors`, `lib/quiz-validate`,
12
- `lib/writing-validate`, `lib/coding-validate`), so an activity that passes here is
13
- the same one the app would accept — no separate, drifting rules.
6
+ - **Validate activity YAML** tutors, fragment libraries, quizzes, writing
7
+ activities, and coding activities with the app's exact validation pipeline,
8
+ offline and without signing in.
9
+ - **Manage the app as a teacher** — sign in with Microsoft Entra ID, then mint
10
+ activity codes, upload app-hosted YAML files, and triage student reports,
11
+ straight from the terminal (or from a coding agent, see below).
14
12
 
15
- ## Usage
13
+ No install needed:
16
14
 
17
15
  ```bash
18
- # Validate a local file (relative fragment_files resolve against the file's location)
16
+ npx @novedu/cli --help
17
+ ```
18
+
19
+ ## Validating activities: `validate`
20
+
21
+ ```bash
22
+ # Validate a local tutor (relative fragment_files resolve against the file's location)
19
23
  npx @novedu/cli validate ./activities/examples/sorting-algorithms/sorting-tutor.yaml
20
24
 
21
- # Validate a published tutor by URL
25
+ # Validate a published activity by URL
22
26
  npx @novedu/cli validate https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/examples/sorting-algorithms/sorting-tutor.yaml
23
27
 
24
- # Validate a fragment library on its own
28
+ # Other kinds: fragment library, quiz, writing activity, coding activity
25
29
  npx @novedu/cli validate ./activities/examples/shared/general-fragments.yaml --kind fragment
26
-
27
- # Validate a quiz, a writing activity, or a coding activity
28
30
  npx @novedu/cli validate ./activities/examples/sorting-algorithms/sorting-quiz.yaml --kind quiz
29
- npx @novedu/cli validate ./activities/examples/review-writing/restaurant-review-letter.yaml --kind writing
30
- npx @novedu/cli validate ./activities/examples/sorting-algorithms/sorting-visualizer.yaml --kind coding
31
31
 
32
32
  # Machine-readable output (the raw validation result)
33
- npx @novedu/cli validate ./activities/examples/sorting-algorithms/sorting-tutor.yaml --json
33
+ npx @novedu/cli validate ./my-quiz.yaml --kind quiz --json
34
34
  ```
35
35
 
36
- `--kind` accepts `tutor` (default), `fragment`, `quiz`, `writing`, or `coding`; it
37
- is caller-declared, not auto-detected.
38
-
39
- Exit code is `0` when the activity is valid and `1` when it has errors, so it works
40
- as a pre-commit / CI gate.
36
+ - `--kind` accepts `tutor` (default), `fragment`, `quiz`, `writing`, or
37
+ `coding`; it is caller-declared, not auto-detected.
38
+ - The CLI reuses the app's exact validation pipeline (`lib/prompt-fragments`,
39
+ `lib/tutors`, `lib/quiz-validate`, `lib/writing-validate`,
40
+ `lib/coding-validate`), so an activity that passes here is the same one the
41
+ app accepts — no separate, drifting rules. Validating a tutor also fully
42
+ validates every fragment library it references.
43
+ - Exit code `0` = valid, `1` = errors found — usable as a pre-commit / CI gate.
41
44
 
42
45
  ## Authentication
43
46
 
@@ -61,6 +64,9 @@ npx @novedu/cli logout # remove the cached credentials from this machine
61
64
  file `0600`). The cache holds a refresh token, so after the one sign-in every
62
65
  command runs non-interactively; treat the file like a credential. `logout`
63
66
  is purely local — issued tokens expire on their own (~1 h).
67
+ - `whoami` proves the full round-trip and shows your display name, user id, and
68
+ whether the account is a teacher (`Teacher: yes/no`) — the management
69
+ commands below need a teacher account.
64
70
  - The server defaults to the production app; override per command with
65
71
  `--server <url>` or the `NOVEDU_SERVER` env var (e.g.
66
72
  `http://localhost:3000` for development). Other deployments of the app can
@@ -69,6 +75,94 @@ npx @novedu/cli logout # remove the cached credentials from this machine
69
75
  - Not signed in (or the cached token expired for good)? Commands exit 1 with
70
76
  `Not signed in — run "novedu-cli login".`
71
77
 
78
+ ## Managing codes & files (teacher account required)
79
+
80
+ The `codes` and `files` groups call the app's API as the signed-in teacher. The
81
+ server runs the identical validation pipeline as the web forms and is
82
+ authoritative — the CLI sends your input as-is and relays the server's answer.
83
+
84
+ ```
85
+ codes create --module <tutor|quiz|writing|coding> --file <url>
86
+ [--start <iso>] [--end <iso>] [--note <text>]
87
+ [--llm-provider <p> --llm-model <m>]
88
+ codes list [--search <q>] [--module <m>] [--all]
89
+ files upload <name> [--kind <tutor|fragment|quiz|writing|coding>]
90
+ (--file <path> | reads stdin)
91
+ files list [--search <q>] [--all]
92
+ ```
93
+
94
+ - **Output is JSON only.** Success: the API's objects verbatim on stdout, exit
95
+ 0 (pipe into `jq`). Failure: JSON on stderr — `{ message }` or
96
+ `{ errors: [...] }` with the full structured validation detail — and exit 1.
97
+ - `codes create` mints a shareable code for an activity YAML at a public URL
98
+ (or an app-hosted `…/api/files/<name>` URL); the YAML is validated
99
+ server-side before the code is stored, and the response includes the
100
+ shareable `url`. `--start`/`--end` must be ISO 8601 **with an explicit
101
+ offset or `Z`** (e.g. `2026-07-07T08:00:00Z`); the
102
+ `--llm-provider`/`--llm-model` override pair is both-or-nothing.
103
+ - `files upload <name>` is an **upsert**: creating a new file requires
104
+ `--kind`; an existing file's kind is frozen at create time (a contradicting
105
+ `--kind` fails with 409). The YAML comes from `--file <path>` or stdin.
106
+ Every hosted file is public at the `url` the list returns — no download
107
+ command needed.
108
+ - Both `list` commands default to **only your own** codes/files (like the web
109
+ lists); `--all` widens to every teacher's, `--search` is a contains-filter.
110
+
111
+ Example — host a quiz and share it:
112
+
113
+ ```bash
114
+ npx @novedu/cli files upload sorting-quiz --kind quiz --file ./sorting-quiz.yaml
115
+ # { "name": "sorting-quiz", "kind": "quiz", "url": "https://…/api/files/sorting-quiz", "action": "created" }
116
+
117
+ npx @novedu/cli codes create --module quiz \
118
+ --file https://…/api/files/sorting-quiz \
119
+ --start 2026-07-07T08:00:00Z --note "3A Monday"
120
+ # { "code": "…", "url": "https://…/<code>", … } — hand the url to students
121
+ ```
122
+
123
+ ## Triaging student reports (teacher account required)
124
+
125
+ Students can flag an AI interaction — a chat or a graded quiz answer — with a
126
+ reaction and an optional note. The `reports` group reads and resolves those
127
+ flags; same JSON stdout/stderr contract as above.
128
+
129
+ ```
130
+ reports list [--status <open|resolved|all>] [--reaction <good|omg|bad|holysh>]
131
+ [--search <q>] [--all]
132
+ reports show <id>
133
+ reports resolve <id...>
134
+ ```
135
+
136
+ - `reports list` defaults to **open reports on your own codes** (like the web
137
+ inbox), most urgent first; `--all` widens to every teacher's codes.
138
+ - `reports show <id>` prints one report in full. A **chat** report embeds the
139
+ conversation transcript as a `messages` array; a **quiz-answer** report
140
+ already carries its question / answer / feedback snapshot inline.
141
+ - `reports resolve <id...>` resolves one or more reports in a single request;
142
+ unknown or already-resolved ids are silently ignored.
143
+ - The CLI deliberately cannot file, reopen, or delete a report — those stay in
144
+ the web `/reports` inbox.
145
+
146
+ The typical loop for turning a report into a better activity:
147
+
148
+ ```bash
149
+ npx @novedu/cli reports list --reaction holysh # find the urgent flags
150
+ npx @novedu/cli reports show 3f2c… # read the report + transcript
151
+ # fix the activity YAML, then check it offline:
152
+ npx @novedu/cli validate ./sorting-quiz.yaml --kind quiz
153
+ npx @novedu/cli files upload sorting-quiz --file ./sorting-quiz.yaml
154
+ npx @novedu/cli reports resolve 3f2c… # existing codes already serve the fix
155
+ ```
156
+
157
+ ## Using the CLI from a coding agent
158
+
159
+ The app repo ships a Claude Code skill that teaches coding agents the full CLI
160
+ workflow — validation, the sign-in hand-off, code/file management, and the
161
+ report-triage loop:
162
+ [`.claude/skills/novedu-tutor-cli/SKILL.md`](https://github.com/Teaching-HTL-Leonding/novedu-chat-mvp/blob/main/.claude/skills/novedu-tutor-cli/SKILL.md)
163
+ (mirrored at `.agents/skills/novedu-tutor-cli/`). Agents working inside that
164
+ repo pick it up automatically.
165
+
72
166
  ## Development
73
167
 
74
168
  The CLI lives in the app repo as an npm workspace.
@@ -79,5 +173,5 @@ npm run cli:build # bundle to cli/dist via ts
79
173
  npm run test:cli # build + integration tests (local & live URLs)
80
174
  ```
81
175
 
82
- The fast in-process unit test (`cli/src/commands/validate.unit.test.ts`) runs in
83
- CI; the integration tests hit the network and are local-only.
176
+ The fast in-process unit tests (`cli/src/commands/*.unit.test.ts`) run in CI;
177
+ the integration tests hit the network and are local-only.
package/dist/main.js CHANGED
@@ -249,10 +249,10 @@ async function runApiRequest(options) {
249
249
  }
250
250
  //#endregion
251
251
  //#region src/commands/codes.ts
252
- const SERVER_OPTION$1 = ["--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)"];
252
+ const SERVER_OPTION$2 = ["--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)"];
253
253
  function registerCodes(program) {
254
254
  const codes = program.command("codes").description("Manage activity codes on the Novedu server");
255
- codes.command("create").description("Create a code for an activity YAML (validated server-side before storing)").requiredOption("--module <module>", "activity module: tutor, quiz, writing or coding").requiredOption("--file <url>", "public http(s) URL of the activity YAML").option("--start <iso>", "window start, ISO 8601 with explicit offset (e.g. 2026-07-07T08:00:00Z)").option("--end <iso>", "window end, ISO 8601 with explicit offset").option("--note <text>", "note shown in the codes list").option("--llm-provider <provider>", "LLM override provider (\"SCCH\" or \"Azure Foundry\"; needs --llm-model)").option("--llm-model <model>", "LLM override model id (needs --llm-provider)").option(...SERVER_OPTION$1).action(async (options) => {
255
+ codes.command("create").description("Create a code for an activity YAML (validated server-side before storing)").requiredOption("--module <module>", "activity module: tutor, quiz, writing or coding").requiredOption("--file <url>", "public http(s) URL of the activity YAML").option("--start <iso>", "window start, ISO 8601 with explicit offset (e.g. 2026-07-07T08:00:00Z)").option("--end <iso>", "window end, ISO 8601 with explicit offset").option("--note <text>", "note shown in the codes list").option("--llm-provider <provider>", "LLM override provider (\"SCCH\" or \"Azure Foundry\"; needs --llm-model)").option("--llm-model <model>", "LLM override model id (needs --llm-provider)").option(...SERVER_OPTION$2).action(async (options) => {
256
256
  await runApiRequest({
257
257
  server: options.server,
258
258
  path: "/api/codes",
@@ -270,7 +270,7 @@ function registerCodes(program) {
270
270
  }
271
271
  });
272
272
  });
273
- codes.command("list").description("List codes (defaults to only your own, like the web list)").option("--search <q>", "contains-filter over note/code").option("--module <module>", "only codes for one activity module").option("--all", "include codes created by other teachers").option(...SERVER_OPTION$1).action(async (options) => {
273
+ codes.command("list").description("List codes (defaults to only your own, like the web list)").option("--search <q>", "contains-filter over note/code").option("--module <module>", "only codes for one activity module").option("--all", "include codes created by other teachers").option(...SERVER_OPTION$2).action(async (options) => {
274
274
  const params = new URLSearchParams();
275
275
  if (options.search) params.set("q", options.search);
276
276
  if (options.module) params.set("module", options.module);
@@ -284,7 +284,7 @@ function registerCodes(program) {
284
284
  }
285
285
  //#endregion
286
286
  //#region src/commands/files.ts
287
- const SERVER_OPTION = ["--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)"];
287
+ const SERVER_OPTION$1 = ["--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)"];
288
288
  async function readStdin() {
289
289
  const chunks = [];
290
290
  for await (const chunk of process.stdin) chunks.push(chunk);
@@ -292,7 +292,7 @@ async function readStdin() {
292
292
  }
293
293
  function registerFiles(program) {
294
294
  const files = program.command("files").description("Manage app-hosted YAML files on the Novedu server");
295
- files.command("upload <name>").description("Create or update an app-hosted YAML file from --file or stdin (validated server-side)").option("--kind <kind>", "file kind (tutor, fragment, quiz, writing, coding) — required when creating").option("--file <path>", "read the YAML from this path instead of stdin").option(...SERVER_OPTION).action(async (name, options) => {
295
+ files.command("upload <name>").description("Create or update an app-hosted YAML file from --file or stdin (validated server-side)").option("--kind <kind>", "file kind (tutor, fragment, quiz, writing, coding) — required when creating").option("--file <path>", "read the YAML from this path instead of stdin").option(...SERVER_OPTION$1).action(async (name, options) => {
296
296
  let content;
297
297
  try {
298
298
  content = options.file === void 0 ? await readStdin() : await readFile(options.file, "utf8");
@@ -310,7 +310,7 @@ function registerFiles(program) {
310
310
  }
311
311
  });
312
312
  });
313
- files.command("list").description("List app-hosted YAML files (defaults to only your own, like the web list)").option("--search <q>", "contains-filter over name/title/description").option("--all", "include files last written by other teachers").option(...SERVER_OPTION).action(async (options) => {
313
+ files.command("list").description("List app-hosted YAML files (defaults to only your own, like the web list)").option("--search <q>", "contains-filter over name/title/description").option("--all", "include files last written by other teachers").option(...SERVER_OPTION$1).action(async (options) => {
314
314
  const params = new URLSearchParams();
315
315
  if (options.search) params.set("q", options.search);
316
316
  if (options.all) params.set("mine", "0");
@@ -358,6 +358,38 @@ Purely local — already-issued access tokens stay valid until they expire
358
358
  });
359
359
  }
360
360
  //#endregion
361
+ //#region src/commands/reports.ts
362
+ const SERVER_OPTION = ["--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)"];
363
+ function registerReports(program) {
364
+ const reports = program.command("reports").description("Triage student reports on the Novedu server");
365
+ reports.command("list").description("List reports (defaults to open reports on your own codes, like the web inbox)").option("--status <status>", "open (default), resolved or all").option("--reaction <reaction>", "filter by reaction: good, omg, bad or holysh").option("--search <q>", "contains-filter over description, reporter, code and note").option("--all", "include reports on codes created by other teachers").option(...SERVER_OPTION).action(async (options) => {
366
+ const params = new URLSearchParams();
367
+ if (options.status) params.set("status", options.status);
368
+ if (options.reaction) params.set("reaction", options.reaction);
369
+ if (options.search) params.set("q", options.search);
370
+ if (options.all) params.set("mine", "0");
371
+ const query = params.toString();
372
+ await runApiRequest({
373
+ server: options.server,
374
+ path: `/api/reports${query ? `?${query}` : ""}`
375
+ });
376
+ });
377
+ reports.command("show <id>").description("Show one report; a chat report embeds its conversation transcript").option(...SERVER_OPTION).action(async (id, options) => {
378
+ await runApiRequest({
379
+ server: options.server,
380
+ path: `/api/reports/${encodeURIComponent(id)}`
381
+ });
382
+ });
383
+ reports.command("resolve <id...>").description("Resolve one or more reports by id (bulk, in a single request)").option(...SERVER_OPTION).action(async (ids, options) => {
384
+ await runApiRequest({
385
+ server: options.server,
386
+ path: "/api/reports/resolve",
387
+ method: "POST",
388
+ body: { ids }
389
+ });
390
+ });
391
+ }
392
+ //#endregion
361
393
  //#region ../lib/prompt-fragments/assemble.ts
362
394
  const COMPILE_OPTIONS = {
363
395
  strict: true,
@@ -1705,6 +1737,7 @@ registerLogout(program);
1705
1737
  registerWhoami(program);
1706
1738
  registerCodes(program);
1707
1739
  registerFiles(program);
1740
+ registerReports(program);
1708
1741
  program.parseAsync().catch((err) => {
1709
1742
  console.error(err instanceof Error ? err.message : err);
1710
1743
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novedu/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "Command-line companion for the Novedu chat app. Validates tutor, fragment, quiz, writing and coding YAML definitions; signs in with Entra ID and manages codes and app-hosted files over the app's API.",
5
5
  "type": "module",
6
6
  "repository": {