@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.
- package/README.md +119 -25
- package/dist/main.js +39 -6
- 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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
13
|
+
No install needed:
|
|
16
14
|
|
|
17
15
|
```bash
|
|
18
|
-
|
|
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
|
|
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
|
-
#
|
|
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 ./
|
|
33
|
+
npx @novedu/cli validate ./my-quiz.yaml --kind quiz --json
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
`--kind` accepts `tutor` (default), `fragment`, `quiz`, `writing`, or
|
|
37
|
-
is caller-declared, not auto-detected.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
83
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
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": {
|