@novedu/cli 0.24.0 → 0.25.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.
- package/README.md +2 -1
- package/dist/main.js +62 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -331,7 +331,8 @@ images list [--search <q>] [--all]
|
|
|
331
331
|
shareable `url`. `--start`/`--end` must be ISO 8601 **with an explicit
|
|
332
332
|
offset or `Z`** (e.g. `2026-07-07T08:00:00Z`); the
|
|
333
333
|
`--llm-provider`/`--llm-model` override pair is both-or-nothing, and
|
|
334
|
-
`--llm-reasoning <level>` (`minimal`, `low`, `medium
|
|
334
|
+
`--llm-reasoning <level>` (`none`, `minimal`, `low`, `medium`, `high` or
|
|
335
|
+
`xhigh`) rides on top of
|
|
335
336
|
the pair — it is rejected without it. The override replaces the activity's whole
|
|
336
337
|
`llm:` block, so leaving the level out also drops the file's.
|
|
337
338
|
- `codes sync <registry-file>` mints codes for a whole **course** at once — see
|
package/dist/main.js
CHANGED
|
@@ -2,15 +2,35 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import { spawn } from "node:child_process";
|
|
6
7
|
import { globSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { createServer } from "node:http";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
10
|
import { CryptoProvider, PublicClientApplication } from "@azure/msal-node";
|
|
10
11
|
import { parse, stringify } from "yaml";
|
|
11
|
-
import { z } from "zod";
|
|
12
12
|
import Handlebars from "handlebars";
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
|
+
//#region ../lib/llm/provider.ts
|
|
15
|
+
const LLM_PROVIDERS = ["SCCH", "Azure Foundry"];
|
|
16
|
+
const DEFAULT_PROVIDER = "SCCH";
|
|
17
|
+
const providerSchema = z.enum(LLM_PROVIDERS).default(DEFAULT_PROVIDER).meta({ description: "The LLM provider serving the model. For Azure Foundry, model is the deployment name." });
|
|
18
|
+
const REASONING_LEVELS = [
|
|
19
|
+
"none",
|
|
20
|
+
"minimal",
|
|
21
|
+
"low",
|
|
22
|
+
"medium",
|
|
23
|
+
"high",
|
|
24
|
+
"xhigh"
|
|
25
|
+
];
|
|
26
|
+
const reasoningLevelSchema = z.enum(REASONING_LEVELS).optional().meta({ description: "Optional reasoning effort for reasoning models. Not every model accepts every level. Omit to let the model decide (the parameter is then not sent); \"none\" instead turns a reasoning model off." });
|
|
27
|
+
function parseLenientProvider(value) {
|
|
28
|
+
return value === "SCCH" || value === "Azure Foundry" ? value : void 0;
|
|
29
|
+
}
|
|
30
|
+
function parseLenientReasoningLevel(value) {
|
|
31
|
+
return typeof value === "string" && REASONING_LEVELS.includes(value) ? value : void 0;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
14
34
|
//#region src/auth.ts
|
|
15
35
|
const DEFAULT_TENANT_ID = "91fc072c-edef-4f97-bdc5-cfb67718ae3a";
|
|
16
36
|
const DEFAULT_CLIENT_ID = "4d44fc4b-0434-4981-9765-62e2074ceecb";
|
|
@@ -85,17 +105,46 @@ async function acquireSilent(pca) {
|
|
|
85
105
|
return null;
|
|
86
106
|
}
|
|
87
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* How the system browser is launched per platform. Exported for tests.
|
|
110
|
+
*
|
|
111
|
+
* Windows goes through `cmd /c start`, and cmd RE-PARSES the command line it
|
|
112
|
+
* receives: an unquoted URL is cut at the first `&`, so Entra only ever saw
|
|
113
|
+
* `authorize?client_id=…` and answered AADSTS900144 ("the request body must
|
|
114
|
+
* contain the following parameter: 'scope'"). Node quotes an argument only
|
|
115
|
+
* when it contains whitespace, so the URL is quoted here explicitly and the
|
|
116
|
+
* command line handed over verbatim. The empty `""` is `start`'s window title
|
|
117
|
+
* — without it, `start` would take the quoted URL as the title.
|
|
118
|
+
*/
|
|
119
|
+
function browserCommand(url, platform = process.platform) {
|
|
120
|
+
if (platform === "darwin") return {
|
|
121
|
+
command: "open",
|
|
122
|
+
args: [url],
|
|
123
|
+
verbatim: false
|
|
124
|
+
};
|
|
125
|
+
if (platform === "win32") return {
|
|
126
|
+
command: "cmd",
|
|
127
|
+
args: [
|
|
128
|
+
"/c",
|
|
129
|
+
"start",
|
|
130
|
+
"\"\"",
|
|
131
|
+
`"${url}"`
|
|
132
|
+
],
|
|
133
|
+
verbatim: true
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
command: "xdg-open",
|
|
137
|
+
args: [url],
|
|
138
|
+
verbatim: false
|
|
139
|
+
};
|
|
140
|
+
}
|
|
88
141
|
function defaultOpenBrowser(url) {
|
|
89
|
-
const
|
|
90
|
-
"/c",
|
|
91
|
-
"start",
|
|
92
|
-
"",
|
|
93
|
-
url
|
|
94
|
-
]] : ["xdg-open", [url]];
|
|
142
|
+
const { command, args, verbatim } = browserCommand(url);
|
|
95
143
|
try {
|
|
96
144
|
spawn(command, args, {
|
|
97
145
|
stdio: "ignore",
|
|
98
|
-
detached: true
|
|
146
|
+
detached: true,
|
|
147
|
+
windowsVerbatimArguments: verbatim
|
|
99
148
|
}).unref();
|
|
100
149
|
} catch {}
|
|
101
150
|
}
|
|
@@ -280,24 +329,6 @@ async function runApiRequest(options) {
|
|
|
280
329
|
if (result.ok) printJson(result.payload ?? null);
|
|
281
330
|
}
|
|
282
331
|
//#endregion
|
|
283
|
-
//#region ../lib/llm/provider.ts
|
|
284
|
-
const LLM_PROVIDERS = ["SCCH", "Azure Foundry"];
|
|
285
|
-
const DEFAULT_PROVIDER = "SCCH";
|
|
286
|
-
const providerSchema = z.enum(LLM_PROVIDERS).default(DEFAULT_PROVIDER).meta({ description: "The LLM provider serving the model. For Azure Foundry, model is the deployment name." });
|
|
287
|
-
const REASONING_LEVELS = [
|
|
288
|
-
"minimal",
|
|
289
|
-
"low",
|
|
290
|
-
"medium",
|
|
291
|
-
"high"
|
|
292
|
-
];
|
|
293
|
-
const reasoningLevelSchema = z.enum(REASONING_LEVELS).optional().meta({ description: "Optional reasoning effort for reasoning models. Omit to let the model decide (the parameter is then not sent)." });
|
|
294
|
-
function parseLenientProvider(value) {
|
|
295
|
-
return value === "SCCH" || value === "Azure Foundry" ? value : void 0;
|
|
296
|
-
}
|
|
297
|
-
function parseLenientReasoningLevel(value) {
|
|
298
|
-
return typeof value === "string" && REASONING_LEVELS.includes(value) ? value : void 0;
|
|
299
|
-
}
|
|
300
|
-
//#endregion
|
|
301
332
|
//#region ../lib/registry-schema.ts
|
|
302
333
|
/** The fixed group names and the code module each one mints for. */
|
|
303
334
|
const GROUP_MODULES = {
|
|
@@ -867,7 +898,7 @@ async function readLock(lockPath) {
|
|
|
867
898
|
}
|
|
868
899
|
function registerCodes(program) {
|
|
869
900
|
const codes = program.command("codes").description("Manage activity codes on the Novedu server");
|
|
870
|
-
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("--llm-reasoning <level>",
|
|
901
|
+
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("--llm-reasoning <level>", `LLM override reasoning effort (${REASONING_LEVELS.join(", ")}; needs the provider/model pair)`).option(...SERVER_OPTION$3).action(async (options) => {
|
|
871
902
|
const llmGiven = options.llmProvider !== void 0 || options.llmModel !== void 0 || options.llmReasoning !== void 0;
|
|
872
903
|
await runApiRequest({
|
|
873
904
|
server: options.server,
|
|
@@ -1139,7 +1170,7 @@ function parseCoding(content) {
|
|
|
1139
1170
|
const reasoning = llm?.reasoning === void 0 ? void 0 : parseLenientReasoningLevel(llm.reasoning);
|
|
1140
1171
|
if (llm?.reasoning !== void 0 && !reasoning) return {
|
|
1141
1172
|
ok: false,
|
|
1142
|
-
message:
|
|
1173
|
+
message: `This coding activity uses an unsupported llm.reasoning (one of ${REASONING_LEVELS.join(", ")}).`
|
|
1143
1174
|
};
|
|
1144
1175
|
const instructions = asString$2(root.instructions);
|
|
1145
1176
|
if (!instructions) return {
|
|
@@ -2448,7 +2479,7 @@ function parseQuiz(content) {
|
|
|
2448
2479
|
const reasoning = llm?.reasoning === void 0 ? void 0 : parseLenientReasoningLevel(llm.reasoning);
|
|
2449
2480
|
if (llm?.reasoning !== void 0 && !reasoning) return {
|
|
2450
2481
|
ok: false,
|
|
2451
|
-
message:
|
|
2482
|
+
message: `This quiz uses an unsupported llm.reasoning (one of ${REASONING_LEVELS.join(", ")}).`
|
|
2452
2483
|
};
|
|
2453
2484
|
const quizFiles = Array.isArray(root.quiz_files) ? root.quiz_files : [];
|
|
2454
2485
|
const rawQuestions = Array.isArray(root.questions) ? root.questions : [];
|
|
@@ -2829,7 +2860,7 @@ function parseWriting(content) {
|
|
|
2829
2860
|
const reasoning = llm?.reasoning === void 0 ? void 0 : parseLenientReasoningLevel(llm.reasoning);
|
|
2830
2861
|
if (llm?.reasoning !== void 0 && !reasoning) return {
|
|
2831
2862
|
ok: false,
|
|
2832
|
-
message:
|
|
2863
|
+
message: `This writing activity uses an unsupported llm.reasoning (one of ${REASONING_LEVELS.join(", ")}).`
|
|
2833
2864
|
};
|
|
2834
2865
|
const instructions = asString(root.instructions);
|
|
2835
2866
|
if (!instructions) return {
|
|
@@ -5781,7 +5812,7 @@ async function runEvalCommand(pathsOrUrls, options, seams = {}) {
|
|
|
5781
5812
|
process.exitCode = batchPassed(batch) ? 0 : 1;
|
|
5782
5813
|
}
|
|
5783
5814
|
function registerEval(program) {
|
|
5784
|
-
program.command("eval").description("Run an eval file (quiz golden answers, or tutor conversations) against the real activity path and report the result").argument("<evalPathOrUrl...>", "one or more eval YAML files (paths, http(s)/file URLs, or a quoted glob pattern)").option("--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)").option("--concurrency <n>", "cases in flight per file", String(CONCURRENCY_DEFAULT)).option("--repeats <n>", "run every case N times (quiz: take the majority verdict)", "1").option("--llm-provider <provider>", "run with this provider instead of the activity's (\"SCCH\" or \"Azure Foundry\"; needs --llm-model)").option("--llm-model <model>", "run with this model instead of the activity's (needs --llm-provider)").option("--llm-reasoning <level>",
|
|
5815
|
+
program.command("eval").description("Run an eval file (quiz golden answers, or tutor conversations) against the real activity path and report the result").argument("<evalPathOrUrl...>", "one or more eval YAML files (paths, http(s)/file URLs, or a quoted glob pattern)").option("--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)").option("--concurrency <n>", "cases in flight per file", String(CONCURRENCY_DEFAULT)).option("--repeats <n>", "run every case N times (quiz: take the majority verdict)", "1").option("--llm-provider <provider>", "run with this provider instead of the activity's (\"SCCH\" or \"Azure Foundry\"; needs --llm-model)").option("--llm-model <model>", "run with this model instead of the activity's (needs --llm-provider)").option("--llm-reasoning <level>", `run at this reasoning effort (${REASONING_LEVELS.join(", ")}); on its own it keeps the activity's model`).option("--no-judge-feedback", "skip the LLM audit of what the model wrote (halves the LLM calls)").option("--judge-llm-provider <provider>", "judge with this provider (\"SCCH\" or \"Azure Foundry\"; needs --judge-llm-model)").option("--judge-llm-model <model>", "judge with this model instead of the one under test (needs --judge-llm-provider)").option("--judge-llm-reasoning <level>", `judge at this reasoning effort (${REASONING_LEVELS.join(", ")}); on its own it keeps the judge's model`).option("--json", "print the machine-readable batch report on stdout").option("--out <file>", "additionally write the machine-readable batch report to a file").option("--report <file>", "additionally write a readable Markdown report to a file").addHelpText("after", `
|
|
5785
5816
|
Examples:
|
|
5786
5817
|
# Evaluate one quiz's golden answers
|
|
5787
5818
|
$ novedu-cli eval ./0010-welcome-quiz.eval.yaml
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novedu/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Command-line companion for the Novedu chat app. Validates tutor, fragment, quiz, writing, coding and eval YAML definitions, dumps the exact LLM prompts an activity produces, evaluates a quiz's grading rubric against golden answers and replays scripted conversations against a tutor; signs in with Entra ID and manages codes, app-hosted files and images over the app's API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|