@novedu/cli 0.7.0 → 0.9.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/dist/main.js +390 -132
- package/package.json +4 -4
package/dist/main.js
CHANGED
|
@@ -6,11 +6,11 @@ import { createServer } from "node:http";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { dirname, join, resolve } from "node:path";
|
|
8
8
|
import { CryptoProvider, PublicClientApplication } from "@azure/msal-node";
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
9
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
11
|
import Handlebars from "handlebars";
|
|
11
12
|
import { parse } from "yaml";
|
|
12
13
|
import { z } from "zod";
|
|
13
|
-
import { readFile } from "node:fs/promises";
|
|
14
14
|
//#region src/auth.ts
|
|
15
15
|
const DEFAULT_TENANT_ID = "91fc072c-edef-4f97-bdc5-cfb67718ae3a";
|
|
16
16
|
const DEFAULT_CLIENT_ID = "4d44fc4b-0434-4981-9765-62e2074ceecb";
|
|
@@ -185,6 +185,143 @@ function displayName(result) {
|
|
|
185
185
|
return result.account?.name ?? result.account?.username ?? "(unknown account)";
|
|
186
186
|
}
|
|
187
187
|
//#endregion
|
|
188
|
+
//#region src/server-url.ts
|
|
189
|
+
const DEFAULT_SERVER = "https://novedu-chat-mvp-at.azurewebsites.net";
|
|
190
|
+
function resolveServerUrl(cliOption) {
|
|
191
|
+
return cliOption || process.env.NOVEDU_SERVER || DEFAULT_SERVER;
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/api.ts
|
|
195
|
+
/** Pretty-prints a success payload to stdout. */
|
|
196
|
+
function printJson(value) {
|
|
197
|
+
console.log(JSON.stringify(value, null, 2));
|
|
198
|
+
}
|
|
199
|
+
/** Prints a failure payload as JSON to stderr and marks the process failed. */
|
|
200
|
+
function failJson(value) {
|
|
201
|
+
console.error(JSON.stringify(value, null, 2));
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Performs one authenticated API request and prints the outcome per the JSON
|
|
206
|
+
* contract. Server error bodies (`{ message }` — incl. the generic 401/403 —
|
|
207
|
+
* or `{ errors }`) are passed through VERBATIM to stderr — the server's
|
|
208
|
+
* structured validation detail is the CLI's error message. No client-side
|
|
209
|
+
* pre-validation: the server runs the identical pipeline; offline checking is
|
|
210
|
+
* the `validate` command's job.
|
|
211
|
+
*/
|
|
212
|
+
async function runApiRequest(options) {
|
|
213
|
+
let token;
|
|
214
|
+
try {
|
|
215
|
+
token = await getAccessToken();
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error instanceof NotSignedInError) {
|
|
218
|
+
failJson({ message: error.message });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
const server = resolveServerUrl(options.server);
|
|
224
|
+
let response;
|
|
225
|
+
try {
|
|
226
|
+
response = await fetch(new URL(options.path, server), {
|
|
227
|
+
method: options.method ?? "GET",
|
|
228
|
+
headers: {
|
|
229
|
+
authorization: `Bearer ${token}`,
|
|
230
|
+
...options.body === void 0 ? {} : { "content-type": "application/json" }
|
|
231
|
+
},
|
|
232
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
233
|
+
});
|
|
234
|
+
} catch (error) {
|
|
235
|
+
failJson({ message: `Could not reach ${server}: ${error instanceof Error ? error.message : error}` });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
let payload;
|
|
239
|
+
try {
|
|
240
|
+
payload = await response.json();
|
|
241
|
+
} catch {
|
|
242
|
+
payload = void 0;
|
|
243
|
+
}
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
failJson(payload ?? { message: `${server} rejected the request: HTTP ${response.status}` });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
printJson(payload ?? null);
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
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)"];
|
|
253
|
+
function registerCodes(program) {
|
|
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) => {
|
|
256
|
+
await runApiRequest({
|
|
257
|
+
server: options.server,
|
|
258
|
+
path: "/api/codes",
|
|
259
|
+
method: "POST",
|
|
260
|
+
body: {
|
|
261
|
+
module: options.module,
|
|
262
|
+
fileUrl: options.file,
|
|
263
|
+
...options.start === void 0 ? {} : { validFrom: options.start },
|
|
264
|
+
...options.end === void 0 ? {} : { validUntil: options.end },
|
|
265
|
+
...options.note === void 0 ? {} : { note: options.note },
|
|
266
|
+
...options.llmProvider === void 0 && options.llmModel === void 0 ? {} : { llm: {
|
|
267
|
+
provider: options.llmProvider ?? "",
|
|
268
|
+
model: options.llmModel ?? ""
|
|
269
|
+
} }
|
|
270
|
+
}
|
|
271
|
+
});
|
|
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) => {
|
|
274
|
+
const params = new URLSearchParams();
|
|
275
|
+
if (options.search) params.set("q", options.search);
|
|
276
|
+
if (options.module) params.set("module", options.module);
|
|
277
|
+
if (options.all) params.set("mine", "0");
|
|
278
|
+
const query = params.toString();
|
|
279
|
+
await runApiRequest({
|
|
280
|
+
server: options.server,
|
|
281
|
+
path: `/api/codes${query ? `?${query}` : ""}`
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
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)"];
|
|
288
|
+
async function readStdin() {
|
|
289
|
+
const chunks = [];
|
|
290
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
291
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
292
|
+
}
|
|
293
|
+
function registerFiles(program) {
|
|
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) => {
|
|
296
|
+
let content;
|
|
297
|
+
try {
|
|
298
|
+
content = options.file === void 0 ? await readStdin() : await readFile(options.file, "utf8");
|
|
299
|
+
} catch (error) {
|
|
300
|
+
failJson({ message: `Could not read ${options.file}: ${error instanceof Error ? error.message : error}` });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await runApiRequest({
|
|
304
|
+
server: options.server,
|
|
305
|
+
path: `/api/files/${encodeURIComponent(name)}`,
|
|
306
|
+
method: "PUT",
|
|
307
|
+
body: {
|
|
308
|
+
...options.kind === void 0 ? {} : { kind: options.kind },
|
|
309
|
+
content
|
|
310
|
+
}
|
|
311
|
+
});
|
|
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) => {
|
|
314
|
+
const params = new URLSearchParams();
|
|
315
|
+
if (options.search) params.set("q", options.search);
|
|
316
|
+
if (options.all) params.set("mine", "0");
|
|
317
|
+
const query = params.toString();
|
|
318
|
+
await runApiRequest({
|
|
319
|
+
server: options.server,
|
|
320
|
+
path: `/api/files${query ? `?${query}` : ""}`
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
188
325
|
//#region src/commands/login.ts
|
|
189
326
|
function registerLogin(program) {
|
|
190
327
|
program.command("login").description("Sign in to Microsoft Entra ID (opens your browser)").option("--device-code", "sign in with the device code flow instead (for machines without a browser; the tenant must allow it)").addHelpText("after", `
|
|
@@ -221,25 +358,34 @@ Purely local — already-issued access tokens stay valid until they expire
|
|
|
221
358
|
});
|
|
222
359
|
}
|
|
223
360
|
//#endregion
|
|
224
|
-
//#region ../lib/
|
|
361
|
+
//#region ../lib/prompt-fragments/assemble.ts
|
|
225
362
|
const COMPILE_OPTIONS = {
|
|
226
363
|
strict: true,
|
|
227
364
|
noEscape: true
|
|
228
365
|
};
|
|
229
366
|
/**
|
|
230
|
-
* Render each fragment in priority order and append the
|
|
231
|
-
* instructions last (they carry no priority, so "after everything" is the
|
|
232
|
-
* deterministic position
|
|
367
|
+
* Render each fragment in priority order and, when provided, append the caller's
|
|
368
|
+
* trailing instructions last (they carry no priority, so "after everything" is the
|
|
369
|
+
* only deterministic position — the exact role `tutor_instructions` plays for a
|
|
370
|
+
* tutor, and the activity frame / `instructions` play for quiz / writing / coding).
|
|
371
|
+
* May throw if a template references a missing variable.
|
|
372
|
+
*
|
|
373
|
+
* `trailingInstructions` is optional so a consumer can assemble a fragment-only
|
|
374
|
+
* PREAMBLE (quiz / writing / coding) and concatenate its own frame afterwards. An
|
|
375
|
+
* empty plan with no trailing text renders to the empty string (so an activity that
|
|
376
|
+
* declares no fragments gets no stray whitespace); every non-empty result ends in a
|
|
377
|
+
* single trailing newline, byte-identical to the historic tutor output.
|
|
233
378
|
*/
|
|
234
|
-
function assembleSystemPrompt(plan,
|
|
379
|
+
function assembleSystemPrompt(plan, trailingInstructions) {
|
|
235
380
|
const parts = plan.map((fragment) => {
|
|
236
381
|
return Handlebars.compile(fragment.content, COMPILE_OPTIONS)(fragment.variables).trimEnd();
|
|
237
382
|
});
|
|
238
|
-
parts.push(
|
|
383
|
+
if (trailingInstructions !== void 0) parts.push(trailingInstructions.trimEnd());
|
|
384
|
+
if (parts.length === 0) return "";
|
|
239
385
|
return `${parts.join("\n\n")}\n`;
|
|
240
386
|
}
|
|
241
387
|
//#endregion
|
|
242
|
-
//#region ../lib/
|
|
388
|
+
//#region ../lib/prompt-fragments/errors.ts
|
|
243
389
|
/** Small helper to build an error object tersely at call sites. */
|
|
244
390
|
function error(code, message, extra = {}) {
|
|
245
391
|
return {
|
|
@@ -276,7 +422,7 @@ function formatZodIssues(zodIssues) {
|
|
|
276
422
|
return out;
|
|
277
423
|
}
|
|
278
424
|
//#endregion
|
|
279
|
-
//#region ../lib/
|
|
425
|
+
//#region ../lib/prompt-fragments/consistency.ts
|
|
280
426
|
/** Compare a supplied value against its declared property type. Returns null when it matches. */
|
|
281
427
|
function typeMismatch(prop, value) {
|
|
282
428
|
const actual = Array.isArray(value) ? "array" : typeof value;
|
|
@@ -295,11 +441,11 @@ function typeMismatch(prop, value) {
|
|
|
295
441
|
};
|
|
296
442
|
}
|
|
297
443
|
}
|
|
298
|
-
function checkConsistency(
|
|
444
|
+
function checkConsistency(block, fragmentFilesByAlias) {
|
|
299
445
|
const errors = [];
|
|
300
446
|
const warnings = [];
|
|
301
447
|
const aliasCounts = /* @__PURE__ */ new Map();
|
|
302
|
-
for (const ref of
|
|
448
|
+
for (const ref of block.fragment_files) aliasCounts.set(ref.id, (aliasCounts.get(ref.id) ?? 0) + 1);
|
|
303
449
|
for (const [alias, count] of aliasCounts) if (count > 1) errors.push(error("DUPLICATE_FRAGMENT_FILE_ALIAS", `Fragment-file alias "${alias}" is declared ${count} times`, { fileAlias: alias }));
|
|
304
450
|
const fragmentIndex = /* @__PURE__ */ new Map();
|
|
305
451
|
for (const [alias, file] of fragmentFilesByAlias) {
|
|
@@ -318,7 +464,7 @@ function checkConsistency(tutor, fragmentFilesByAlias) {
|
|
|
318
464
|
}
|
|
319
465
|
const resolved = [];
|
|
320
466
|
const seenRefs = /* @__PURE__ */ new Set();
|
|
321
|
-
for (const ref of
|
|
467
|
+
for (const ref of block.fragments) {
|
|
322
468
|
const refKey = `${ref.file}::${ref.id}`;
|
|
323
469
|
if (seenRefs.has(refKey)) warnings.push(warning("DUPLICATE_FRAGMENT_REFERENCE", `Fragment "${ref.id}" from "${ref.file}" is referenced more than once`, {
|
|
324
470
|
fileAlias: ref.file,
|
|
@@ -410,7 +556,7 @@ function checkConsistency(tutor, fragmentFilesByAlias) {
|
|
|
410
556
|
};
|
|
411
557
|
}
|
|
412
558
|
//#endregion
|
|
413
|
-
//#region ../lib/
|
|
559
|
+
//#region ../lib/prompt-fragments/fetcher.ts
|
|
414
560
|
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
415
561
|
/** Production fetcher: global `fetch` with an abort-based timeout so a slow host can't hang the request. */
|
|
416
562
|
const defaultFetcher = async (url) => {
|
|
@@ -426,7 +572,7 @@ const defaultFetcher = async (url) => {
|
|
|
426
572
|
}
|
|
427
573
|
};
|
|
428
574
|
//#endregion
|
|
429
|
-
//#region ../lib/
|
|
575
|
+
//#region ../lib/prompt-fragments/parse.ts
|
|
430
576
|
/** Parse a YAML document, mapping syntax errors to a structured `YAML_PARSE_ERROR`. */
|
|
431
577
|
function parseYaml(text, url) {
|
|
432
578
|
try {
|
|
@@ -456,12 +602,11 @@ function validate(value, schema, code, url) {
|
|
|
456
602
|
})
|
|
457
603
|
};
|
|
458
604
|
}
|
|
459
|
-
const providerSchema = z.enum(["SCCH", "Azure Foundry"]).default("SCCH");
|
|
460
605
|
//#endregion
|
|
461
|
-
//#region ../lib/
|
|
606
|
+
//#region ../lib/prompt-fragments/schemas.ts
|
|
462
607
|
/**
|
|
463
608
|
* A fragment-file reference: either an absolute http(s) URL or a relative path that
|
|
464
|
-
* `load.ts` resolves against the
|
|
609
|
+
* `load.ts` resolves against the activity YAML's own URL. We reject any *other* absolute
|
|
465
610
|
* scheme (`ftp:`, `mailto:`, …) so a typo can't smuggle in a non-http(s) target — the
|
|
466
611
|
* refine reads as "if it carries a URI scheme at all, that scheme must be http(s)".
|
|
467
612
|
* Strings without a scheme (relative paths) pass through and are resolved at load time.
|
|
@@ -470,7 +615,7 @@ const FragmentUrlRef = z.string().min(1).refine((u) => !/^[a-z][a-z0-9+.-]*:/i.t
|
|
|
470
615
|
/**
|
|
471
616
|
* A declared property is a string, a boolean, or an array of strings. Each may carry an
|
|
472
617
|
* optional `default`, typed to match its `type` (a string default on a boolean property
|
|
473
|
-
* is a schema error). When the
|
|
618
|
+
* is a schema error). When the activity omits the variable, the default is used; supplying
|
|
474
619
|
* a value overrides it. See `consistency.ts` for where defaults are injected.
|
|
475
620
|
*/
|
|
476
621
|
const PropertySchema = z.discriminatedUnion("type", [
|
|
@@ -526,35 +671,8 @@ const FragmentRefSchema = z.strictObject({
|
|
|
526
671
|
bind: z.record(z.string(), z.string()).optional(),
|
|
527
672
|
required: z.boolean().optional()
|
|
528
673
|
});
|
|
529
|
-
/**
|
|
530
|
-
* An example question offered to students on the welcome screen: the `title` is
|
|
531
|
-
* the clickable label, the `question` is the full text placed into the chat
|
|
532
|
-
* input on click. Tutors may define any number; the UI samples at most 5.
|
|
533
|
-
*/
|
|
534
|
-
const ExampleQuestionSchema = z.strictObject({
|
|
535
|
-
title: z.string().min(1),
|
|
536
|
-
question: z.string().min(1)
|
|
537
|
-
});
|
|
538
|
-
const TutorSchema = z.strictObject({
|
|
539
|
-
id: z.string(),
|
|
540
|
-
name: z.string(),
|
|
541
|
-
title: z.string().optional(),
|
|
542
|
-
description: z.string(),
|
|
543
|
-
exampleQuestions: z.array(ExampleQuestionSchema).optional(),
|
|
544
|
-
anonymous: z.boolean().optional(),
|
|
545
|
-
llm: z.strictObject({
|
|
546
|
-
model: z.string(),
|
|
547
|
-
provider: providerSchema,
|
|
548
|
-
imageInput: z.boolean().optional()
|
|
549
|
-
}),
|
|
550
|
-
prompt: z.strictObject({
|
|
551
|
-
fragment_files: z.array(FragmentFileRefSchema).default([]),
|
|
552
|
-
fragments: z.array(FragmentRefSchema).default([]),
|
|
553
|
-
tutor_instructions: z.string()
|
|
554
|
-
})
|
|
555
|
-
});
|
|
556
674
|
//#endregion
|
|
557
|
-
//#region ../lib/
|
|
675
|
+
//#region ../lib/prompt-fragments/fragment.ts
|
|
558
676
|
/**
|
|
559
677
|
* A placeholder value for a declared input, shaped to its type so the template
|
|
560
678
|
* actually exercises it: a string renders, a boolean drives `{{#if}}`, an array
|
|
@@ -657,17 +775,17 @@ function resolveRelativeUrl(ref, baseUrl) {
|
|
|
657
775
|
return new URL(ref, baseUrl).href;
|
|
658
776
|
}
|
|
659
777
|
//#endregion
|
|
660
|
-
//#region ../lib/
|
|
778
|
+
//#region ../lib/prompt-fragments/load.ts
|
|
661
779
|
/**
|
|
662
780
|
* Resolve a fragment-file reference to an absolute URL. An absolute http(s) ref is used
|
|
663
|
-
* as-is; anything else is treated as relative to the
|
|
664
|
-
* drops the
|
|
781
|
+
* as-is; anything else is treated as relative to the activity URL — standard URL resolution
|
|
782
|
+
* drops the activity's filename and appends the relative path (so `my-fragments.yaml`
|
|
665
783
|
* next to `.../tutors/my-tutor.yaml` becomes `.../tutors/my-fragments.yaml`,
|
|
666
784
|
* and `./` / `../` segments work too). Throws if a relative ref is unparseable; the schema
|
|
667
785
|
* already guarantees the only inputs here are http(s) URLs or relative paths.
|
|
668
786
|
*/
|
|
669
|
-
function resolveFragmentUrl(ref,
|
|
670
|
-
return resolveRelativeUrl(ref,
|
|
787
|
+
function resolveFragmentUrl(ref, baseUrl) {
|
|
788
|
+
return resolveRelativeUrl(ref, baseUrl);
|
|
671
789
|
}
|
|
672
790
|
async function fetchText(url, fetchImpl) {
|
|
673
791
|
try {
|
|
@@ -692,23 +810,33 @@ async function fetchText(url, fetchImpl) {
|
|
|
692
810
|
}
|
|
693
811
|
const DEFAULT_ALLOWED_SCHEMES = ["http:", "https:"];
|
|
694
812
|
/**
|
|
695
|
-
* The
|
|
696
|
-
* fetch
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
* schemes identically.
|
|
813
|
+
* The SSRF scheme gate shared by the top-level activity load (`loadYaml`) and every
|
|
814
|
+
* fragment-file fetch (`assembleFragmentPrompt`): a URL is allowed only if its scheme
|
|
815
|
+
* is in `allowedSchemes` (default http(s); the CLI adds `file:` for on-disk validation).
|
|
816
|
+
* Returns the structured error to surface, or `null` when the scheme is allowed.
|
|
700
817
|
*/
|
|
701
|
-
|
|
702
|
-
const allowedSchemes = opts.allowedSchemes ?? DEFAULT_ALLOWED_SCHEMES;
|
|
818
|
+
function schemeGate(url, allowedSchemes) {
|
|
703
819
|
let scheme;
|
|
704
820
|
try {
|
|
705
821
|
scheme = new URL(url).protocol;
|
|
706
822
|
} catch {
|
|
707
823
|
scheme = "";
|
|
708
824
|
}
|
|
709
|
-
if (
|
|
825
|
+
if (allowedSchemes.includes(scheme)) return null;
|
|
826
|
+
return error("INVALID_URL", `Provide a valid ${allowedSchemes.map((s) => s.replace(/:$/, "")).join("/")} URL`, { url });
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* The shared front of every load: enforce the URL scheme allow-list (SSRF guard),
|
|
830
|
+
* fetch the document, and parse it as YAML — returning the parsed-but-not-yet-schema-
|
|
831
|
+
* validated value or the first structured error. Reused by the activity builders, the
|
|
832
|
+
* standalone fragment checker, and the quiz/writing/coding validators so they all gate
|
|
833
|
+
* schemes identically.
|
|
834
|
+
*/
|
|
835
|
+
async function loadYaml(url, fetchImpl, opts = {}) {
|
|
836
|
+
const schemeError = schemeGate(url, opts.allowedSchemes ?? DEFAULT_ALLOWED_SCHEMES);
|
|
837
|
+
if (schemeError) return {
|
|
710
838
|
ok: false,
|
|
711
|
-
error:
|
|
839
|
+
error: schemeError
|
|
712
840
|
};
|
|
713
841
|
const fetched = await fetchText(url, fetchImpl);
|
|
714
842
|
if (!fetched.ok) return {
|
|
@@ -725,25 +853,30 @@ async function loadYaml(url, fetchImpl, opts = {}) {
|
|
|
725
853
|
value: parsed.value
|
|
726
854
|
};
|
|
727
855
|
}
|
|
728
|
-
|
|
856
|
+
/**
|
|
857
|
+
* Resolve a document-level fragment block to a finished prompt string: fetch every
|
|
858
|
+
* declared fragment file in parallel (relative refs resolved against `baseUrl`),
|
|
859
|
+
* schema-validate each, (optionally) run the thorough whole-library check, check
|
|
860
|
+
* consistency, and assemble the priority-ordered plan followed by the optional
|
|
861
|
+
* `trailingInstructions`.
|
|
862
|
+
*
|
|
863
|
+
* The single seam every activity kind shares — the sole owner of the fetch → validate
|
|
864
|
+
* → consistency → assemble pipeline. Consumers concatenate their own frame only when
|
|
865
|
+
* they pass no `trailingInstructions` (a fragment-only preamble); tutors pass their
|
|
866
|
+
* `tutor_instructions` and get a complete prompt.
|
|
867
|
+
*/
|
|
868
|
+
async function assembleFragmentPrompt(block, baseUrl, fetchImpl, opts = {}, trailingInstructions) {
|
|
729
869
|
const warnings = [];
|
|
730
|
-
const
|
|
731
|
-
if (
|
|
732
|
-
ok:
|
|
733
|
-
|
|
734
|
-
warnings
|
|
735
|
-
};
|
|
736
|
-
const tutorValid = validate(tutorYaml.value, TutorSchema, "TUTOR_SCHEMA_ERROR", url);
|
|
737
|
-
if (!tutorValid.ok) return {
|
|
738
|
-
ok: false,
|
|
739
|
-
errors: [tutorValid.error],
|
|
870
|
+
const allowedSchemes = opts.allowedSchemes ?? DEFAULT_ALLOWED_SCHEMES;
|
|
871
|
+
if (block.fragment_files.length === 0 && block.fragments.length === 0) return {
|
|
872
|
+
ok: true,
|
|
873
|
+
prompt: assembleSystemPrompt([], trailingInstructions),
|
|
740
874
|
warnings
|
|
741
875
|
};
|
|
742
|
-
const
|
|
743
|
-
const settled = await Promise.all(tutor.prompt.fragment_files.map(async (ref) => {
|
|
876
|
+
const settled = await Promise.all(block.fragment_files.map(async (ref) => {
|
|
744
877
|
let fragmentUrl;
|
|
745
878
|
try {
|
|
746
|
-
fragmentUrl = resolveFragmentUrl(ref.url,
|
|
879
|
+
fragmentUrl = resolveFragmentUrl(ref.url, baseUrl);
|
|
747
880
|
} catch {
|
|
748
881
|
return {
|
|
749
882
|
alias: ref.id,
|
|
@@ -753,6 +886,14 @@ async function loadAndBuildTutorPrompt(url, fetchImpl, opts = {}) {
|
|
|
753
886
|
})
|
|
754
887
|
};
|
|
755
888
|
}
|
|
889
|
+
const schemeError = schemeGate(fragmentUrl, allowedSchemes);
|
|
890
|
+
if (schemeError) return {
|
|
891
|
+
alias: ref.id,
|
|
892
|
+
error: {
|
|
893
|
+
...schemeError,
|
|
894
|
+
fileAlias: ref.id
|
|
895
|
+
}
|
|
896
|
+
};
|
|
756
897
|
const fetched = await fetchText(fragmentUrl, fetchImpl);
|
|
757
898
|
if (!fetched.ok) return {
|
|
758
899
|
alias: ref.id,
|
|
@@ -799,7 +940,7 @@ async function loadAndBuildTutorPrompt(url, fetchImpl, opts = {}) {
|
|
|
799
940
|
libraryErrors.push(...checked.errors);
|
|
800
941
|
warnings.push(...checked.warnings);
|
|
801
942
|
}
|
|
802
|
-
const consistency = checkConsistency(
|
|
943
|
+
const consistency = checkConsistency(block, fragmentFilesByAlias);
|
|
803
944
|
warnings.push(...consistency.warnings);
|
|
804
945
|
const preAssemblyErrors = [...libraryErrors, ...consistency.errors];
|
|
805
946
|
if (preAssemblyErrors.length > 0) return {
|
|
@@ -810,14 +951,7 @@ async function loadAndBuildTutorPrompt(url, fetchImpl, opts = {}) {
|
|
|
810
951
|
try {
|
|
811
952
|
return {
|
|
812
953
|
ok: true,
|
|
813
|
-
prompt: assembleSystemPrompt(consistency.plan,
|
|
814
|
-
model: tutor.llm.model,
|
|
815
|
-
provider: tutor.llm.provider,
|
|
816
|
-
imageInput: tutor.llm.imageInput ?? true,
|
|
817
|
-
anonymous: tutor.anonymous ?? true,
|
|
818
|
-
title: tutor.title,
|
|
819
|
-
description: tutor.description,
|
|
820
|
-
exampleQuestions: tutor.exampleQuestions ?? [],
|
|
954
|
+
prompt: assembleSystemPrompt(consistency.plan, trailingInstructions),
|
|
821
955
|
warnings
|
|
822
956
|
};
|
|
823
957
|
} catch (e) {
|
|
@@ -831,9 +965,9 @@ async function loadAndBuildTutorPrompt(url, fetchImpl, opts = {}) {
|
|
|
831
965
|
/**
|
|
832
966
|
* Validate a fragment FILE on its own (the `--kind fragment` / "Fragment library"
|
|
833
967
|
* path): scheme-gate + fetch + parse, then the pure `checkFragmentFileValue`. A
|
|
834
|
-
* fragment library is self-contained, so — unlike
|
|
968
|
+
* fragment library is self-contained, so — unlike an activity — there are no further
|
|
835
969
|
* files to fetch. The caller already knows it asked for a fragment, so this returns
|
|
836
|
-
* a `FragmentCheckResult` directly (no
|
|
970
|
+
* a `FragmentCheckResult` directly (no activity `BuildResult`, no kind discriminator).
|
|
837
971
|
*/
|
|
838
972
|
async function loadAndCheckFragmentFile(url, fetchImpl, opts = {}) {
|
|
839
973
|
const yaml = await loadYaml(url, fetchImpl, opts);
|
|
@@ -844,6 +978,7 @@ async function loadAndCheckFragmentFile(url, fetchImpl, opts = {}) {
|
|
|
844
978
|
};
|
|
845
979
|
return checkFragmentFileValue(yaml.value, url);
|
|
846
980
|
}
|
|
981
|
+
const providerSchema = z.enum(["SCCH", "Azure Foundry"]).default("SCCH");
|
|
847
982
|
//#endregion
|
|
848
983
|
//#region ../lib/coding-schema.ts
|
|
849
984
|
const CodingYamlSchema = z.strictObject({
|
|
@@ -854,23 +989,18 @@ const CodingYamlSchema = z.strictObject({
|
|
|
854
989
|
model: z.string().min(1),
|
|
855
990
|
provider: providerSchema
|
|
856
991
|
}),
|
|
992
|
+
fragment_files: z.array(FragmentFileRefSchema).default([]),
|
|
993
|
+
fragments: z.array(FragmentRefSchema).default([]),
|
|
857
994
|
instructions: z.string().min(1)
|
|
858
995
|
});
|
|
859
996
|
//#endregion
|
|
860
997
|
//#region ../lib/coding-validate.ts
|
|
861
998
|
/**
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
999
|
+
* Extract metadata from an already-schema-validated coding value. Split from
|
|
1000
|
+
* `checkCodingValue` so `loadAndCheckCoding` can reuse the single `validate` it already
|
|
1001
|
+
* ran (no second parse of the same document against the same schema).
|
|
865
1002
|
*/
|
|
866
|
-
function
|
|
867
|
-
const valid = validate(parsed, CodingYamlSchema, "CODING_SCHEMA_ERROR", url);
|
|
868
|
-
if (!valid.ok) return {
|
|
869
|
-
ok: false,
|
|
870
|
-
errors: [valid.error],
|
|
871
|
-
warnings: []
|
|
872
|
-
};
|
|
873
|
-
const coding = valid.data;
|
|
1003
|
+
function checkCodingParsed(coding) {
|
|
874
1004
|
return {
|
|
875
1005
|
ok: true,
|
|
876
1006
|
codingId: coding.id,
|
|
@@ -892,7 +1022,31 @@ async function loadAndCheckCoding(url, fetchImpl, opts = {}) {
|
|
|
892
1022
|
errors: [yaml.error],
|
|
893
1023
|
warnings: []
|
|
894
1024
|
};
|
|
895
|
-
|
|
1025
|
+
const valid = validate(yaml.value, CodingYamlSchema, "CODING_SCHEMA_ERROR", url);
|
|
1026
|
+
if (!valid.ok) return {
|
|
1027
|
+
ok: false,
|
|
1028
|
+
errors: [valid.error],
|
|
1029
|
+
warnings: []
|
|
1030
|
+
};
|
|
1031
|
+
const checked = checkCodingParsed(valid.data);
|
|
1032
|
+
if (!checked.ok) return checked;
|
|
1033
|
+
const assembled = await assembleFragmentPrompt({
|
|
1034
|
+
fragment_files: valid.data.fragment_files,
|
|
1035
|
+
fragments: valid.data.fragments
|
|
1036
|
+
}, url, fetchImpl, {
|
|
1037
|
+
allowedSchemes: opts.allowedSchemes,
|
|
1038
|
+
validateLibraries: opts.validateLibraries ?? true
|
|
1039
|
+
});
|
|
1040
|
+
const warnings = [...checked.warnings, ...assembled.warnings];
|
|
1041
|
+
if (!assembled.ok) return {
|
|
1042
|
+
ok: false,
|
|
1043
|
+
errors: assembled.errors,
|
|
1044
|
+
warnings
|
|
1045
|
+
};
|
|
1046
|
+
return {
|
|
1047
|
+
...checked,
|
|
1048
|
+
warnings
|
|
1049
|
+
};
|
|
896
1050
|
}
|
|
897
1051
|
//#endregion
|
|
898
1052
|
//#region ../lib/quiz-schema.ts
|
|
@@ -913,7 +1067,8 @@ const QuizQuestionSchema = z.strictObject({
|
|
|
913
1067
|
title: z.string().optional(),
|
|
914
1068
|
question: z.string().min(1),
|
|
915
1069
|
evaluation: z.string().min(1),
|
|
916
|
-
image: ImageRefSchema.optional()
|
|
1070
|
+
image: ImageRefSchema.optional(),
|
|
1071
|
+
imageInput: z.boolean().optional()
|
|
917
1072
|
});
|
|
918
1073
|
const QuizYamlSchema = z.strictObject({
|
|
919
1074
|
id: z.string().min(1),
|
|
@@ -924,9 +1079,12 @@ const QuizYamlSchema = z.strictObject({
|
|
|
924
1079
|
shuffle: z.boolean().optional(),
|
|
925
1080
|
llm: z.strictObject({
|
|
926
1081
|
model: z.string().min(1),
|
|
927
|
-
provider: providerSchema
|
|
1082
|
+
provider: providerSchema,
|
|
1083
|
+
imageInput: z.boolean().optional()
|
|
928
1084
|
}),
|
|
929
1085
|
discussion: z.strictObject({ instructions: z.string().min(1) }).optional(),
|
|
1086
|
+
fragment_files: z.array(FragmentFileRefSchema).default([]),
|
|
1087
|
+
fragments: z.array(FragmentRefSchema).default([]),
|
|
930
1088
|
questions: z.array(QuizQuestionSchema).min(1)
|
|
931
1089
|
});
|
|
932
1090
|
//#endregion
|
|
@@ -947,18 +1105,11 @@ function findDuplicateQuestionIds(quiz) {
|
|
|
947
1105
|
return errors;
|
|
948
1106
|
}
|
|
949
1107
|
/**
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
1108
|
+
* Check an already-schema-validated quiz: unique question ids → metadata. Split from
|
|
1109
|
+
* `checkQuizValue` so `loadAndCheckQuiz` can reuse the single `validate` it already ran
|
|
1110
|
+
* (no second parse of the same document against the same schema).
|
|
953
1111
|
*/
|
|
954
|
-
function
|
|
955
|
-
const valid = validate(parsed, QuizYamlSchema, "QUIZ_SCHEMA_ERROR", url);
|
|
956
|
-
if (!valid.ok) return {
|
|
957
|
-
ok: false,
|
|
958
|
-
errors: [valid.error],
|
|
959
|
-
warnings: []
|
|
960
|
-
};
|
|
961
|
-
const quiz = valid.data;
|
|
1112
|
+
function checkQuizParsed(quiz) {
|
|
962
1113
|
const errors = findDuplicateQuestionIds(quiz);
|
|
963
1114
|
if (errors.length > 0) return {
|
|
964
1115
|
ok: false,
|
|
@@ -977,9 +1128,11 @@ function checkQuizValue(parsed, url) {
|
|
|
977
1128
|
};
|
|
978
1129
|
}
|
|
979
1130
|
/**
|
|
980
|
-
* Validate a quiz FILE: scheme-gate + fetch + parse (shared `loadYaml`),
|
|
981
|
-
*
|
|
982
|
-
*
|
|
1131
|
+
* Validate a quiz FILE: scheme-gate + fetch + parse (shared `loadYaml`), the pure
|
|
1132
|
+
* `checkQuizValue`, then the document-level fragment block's authoring gate — fetch
|
|
1133
|
+
* every referenced library, run the THOROUGH whole-library check, consistency, and an
|
|
1134
|
+
* assembly dry-run (the strict-Handlebars backstop). The web app passes the default
|
|
1135
|
+
* http(s)-only schemes; the CLI adds `file:` so a local quiz YAML on disk validates too.
|
|
983
1136
|
*/
|
|
984
1137
|
async function loadAndCheckQuiz(url, fetchImpl, opts = {}) {
|
|
985
1138
|
const yaml = await loadYaml(url, fetchImpl, opts);
|
|
@@ -988,7 +1141,97 @@ async function loadAndCheckQuiz(url, fetchImpl, opts = {}) {
|
|
|
988
1141
|
errors: [yaml.error],
|
|
989
1142
|
warnings: []
|
|
990
1143
|
};
|
|
991
|
-
|
|
1144
|
+
const valid = validate(yaml.value, QuizYamlSchema, "QUIZ_SCHEMA_ERROR", url);
|
|
1145
|
+
if (!valid.ok) return {
|
|
1146
|
+
ok: false,
|
|
1147
|
+
errors: [valid.error],
|
|
1148
|
+
warnings: []
|
|
1149
|
+
};
|
|
1150
|
+
const checked = checkQuizParsed(valid.data);
|
|
1151
|
+
if (!checked.ok) return checked;
|
|
1152
|
+
const assembled = await assembleFragmentPrompt({
|
|
1153
|
+
fragment_files: valid.data.fragment_files,
|
|
1154
|
+
fragments: valid.data.fragments
|
|
1155
|
+
}, url, fetchImpl, {
|
|
1156
|
+
allowedSchemes: opts.allowedSchemes,
|
|
1157
|
+
validateLibraries: opts.validateLibraries ?? true
|
|
1158
|
+
});
|
|
1159
|
+
const warnings = [...checked.warnings, ...assembled.warnings];
|
|
1160
|
+
if (!assembled.ok) return {
|
|
1161
|
+
ok: false,
|
|
1162
|
+
errors: assembled.errors,
|
|
1163
|
+
warnings
|
|
1164
|
+
};
|
|
1165
|
+
return {
|
|
1166
|
+
...checked,
|
|
1167
|
+
warnings
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
//#endregion
|
|
1171
|
+
//#region ../lib/tutors/schemas.ts
|
|
1172
|
+
/**
|
|
1173
|
+
* An example question offered to students on the welcome screen: the `title` is
|
|
1174
|
+
* the clickable label, the `question` is the full text placed into the chat
|
|
1175
|
+
* input on click. Tutors may define any number; the UI samples at most 5.
|
|
1176
|
+
*/
|
|
1177
|
+
const ExampleQuestionSchema = z.strictObject({
|
|
1178
|
+
title: z.string().min(1),
|
|
1179
|
+
question: z.string().min(1)
|
|
1180
|
+
});
|
|
1181
|
+
const TutorSchema = z.strictObject({
|
|
1182
|
+
id: z.string(),
|
|
1183
|
+
name: z.string(),
|
|
1184
|
+
title: z.string().optional(),
|
|
1185
|
+
description: z.string(),
|
|
1186
|
+
exampleQuestions: z.array(ExampleQuestionSchema).optional(),
|
|
1187
|
+
anonymous: z.boolean().optional(),
|
|
1188
|
+
llm: z.strictObject({
|
|
1189
|
+
model: z.string(),
|
|
1190
|
+
provider: providerSchema,
|
|
1191
|
+
imageInput: z.boolean().optional()
|
|
1192
|
+
}),
|
|
1193
|
+
prompt: z.strictObject({
|
|
1194
|
+
fragment_files: z.array(FragmentFileRefSchema).default([]),
|
|
1195
|
+
fragments: z.array(FragmentRefSchema).default([]),
|
|
1196
|
+
tutor_instructions: z.string()
|
|
1197
|
+
})
|
|
1198
|
+
});
|
|
1199
|
+
//#endregion
|
|
1200
|
+
//#region ../lib/tutors/load.ts
|
|
1201
|
+
async function loadAndBuildTutorPrompt(url, fetchImpl, opts = {}) {
|
|
1202
|
+
const warnings = [];
|
|
1203
|
+
const tutorYaml = await loadYaml(url, fetchImpl, opts);
|
|
1204
|
+
if (!tutorYaml.ok) return {
|
|
1205
|
+
ok: false,
|
|
1206
|
+
errors: [tutorYaml.error],
|
|
1207
|
+
warnings
|
|
1208
|
+
};
|
|
1209
|
+
const tutorValid = validate(tutorYaml.value, TutorSchema, "TUTOR_SCHEMA_ERROR", url);
|
|
1210
|
+
if (!tutorValid.ok) return {
|
|
1211
|
+
ok: false,
|
|
1212
|
+
errors: [tutorValid.error],
|
|
1213
|
+
warnings
|
|
1214
|
+
};
|
|
1215
|
+
const tutor = tutorValid.data;
|
|
1216
|
+
const assembled = await assembleFragmentPrompt(tutor.prompt, url, fetchImpl, opts, tutor.prompt.tutor_instructions);
|
|
1217
|
+
warnings.push(...assembled.warnings);
|
|
1218
|
+
if (!assembled.ok) return {
|
|
1219
|
+
ok: false,
|
|
1220
|
+
errors: assembled.errors,
|
|
1221
|
+
warnings
|
|
1222
|
+
};
|
|
1223
|
+
return {
|
|
1224
|
+
ok: true,
|
|
1225
|
+
prompt: assembled.prompt,
|
|
1226
|
+
model: tutor.llm.model,
|
|
1227
|
+
provider: tutor.llm.provider,
|
|
1228
|
+
imageInput: tutor.llm.imageInput ?? true,
|
|
1229
|
+
anonymous: tutor.anonymous ?? true,
|
|
1230
|
+
title: tutor.title,
|
|
1231
|
+
description: tutor.description,
|
|
1232
|
+
exampleQuestions: tutor.exampleQuestions ?? [],
|
|
1233
|
+
warnings
|
|
1234
|
+
};
|
|
992
1235
|
}
|
|
993
1236
|
//#endregion
|
|
994
1237
|
//#region ../lib/writing-schema.ts
|
|
@@ -1002,6 +1245,8 @@ const WritingYamlSchema = z.strictObject({
|
|
|
1002
1245
|
model: z.string().min(1),
|
|
1003
1246
|
provider: providerSchema
|
|
1004
1247
|
}),
|
|
1248
|
+
fragment_files: z.array(FragmentFileRefSchema).default([]),
|
|
1249
|
+
fragments: z.array(FragmentRefSchema).default([]),
|
|
1005
1250
|
instructions: z.string().min(1),
|
|
1006
1251
|
placeholder: z.string().optional()
|
|
1007
1252
|
});
|
|
@@ -1010,18 +1255,11 @@ const WritingYamlSchema = z.strictObject({
|
|
|
1010
1255
|
/** Writing DIVERGES from tutor/quiz: it defaults to attributed (`anonymous: false`). */
|
|
1011
1256
|
const DEFAULT_ANONYMOUS = false;
|
|
1012
1257
|
/**
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1258
|
+
* Extract metadata from an already-schema-validated writing value. Split from
|
|
1259
|
+
* `checkWritingValue` so `loadAndCheckWriting` can reuse the single `validate` it already
|
|
1260
|
+
* ran (no second parse of the same document against the same schema).
|
|
1016
1261
|
*/
|
|
1017
|
-
function
|
|
1018
|
-
const valid = validate(parsed, WritingYamlSchema, "WRITING_SCHEMA_ERROR", url);
|
|
1019
|
-
if (!valid.ok) return {
|
|
1020
|
-
ok: false,
|
|
1021
|
-
errors: [valid.error],
|
|
1022
|
-
warnings: []
|
|
1023
|
-
};
|
|
1024
|
-
const writing = valid.data;
|
|
1262
|
+
function checkWritingParsed(writing) {
|
|
1025
1263
|
return {
|
|
1026
1264
|
ok: true,
|
|
1027
1265
|
writingId: writing.id,
|
|
@@ -1044,7 +1282,31 @@ async function loadAndCheckWriting(url, fetchImpl, opts = {}) {
|
|
|
1044
1282
|
errors: [yaml.error],
|
|
1045
1283
|
warnings: []
|
|
1046
1284
|
};
|
|
1047
|
-
|
|
1285
|
+
const valid = validate(yaml.value, WritingYamlSchema, "WRITING_SCHEMA_ERROR", url);
|
|
1286
|
+
if (!valid.ok) return {
|
|
1287
|
+
ok: false,
|
|
1288
|
+
errors: [valid.error],
|
|
1289
|
+
warnings: []
|
|
1290
|
+
};
|
|
1291
|
+
const checked = checkWritingParsed(valid.data);
|
|
1292
|
+
if (!checked.ok) return checked;
|
|
1293
|
+
const assembled = await assembleFragmentPrompt({
|
|
1294
|
+
fragment_files: valid.data.fragment_files,
|
|
1295
|
+
fragments: valid.data.fragments
|
|
1296
|
+
}, url, fetchImpl, {
|
|
1297
|
+
allowedSchemes: opts.allowedSchemes,
|
|
1298
|
+
validateLibraries: opts.validateLibraries ?? true
|
|
1299
|
+
});
|
|
1300
|
+
const warnings = [...checked.warnings, ...assembled.warnings];
|
|
1301
|
+
if (!assembled.ok) return {
|
|
1302
|
+
ok: false,
|
|
1303
|
+
errors: assembled.errors,
|
|
1304
|
+
warnings
|
|
1305
|
+
};
|
|
1306
|
+
return {
|
|
1307
|
+
...checked,
|
|
1308
|
+
warnings
|
|
1309
|
+
};
|
|
1048
1310
|
}
|
|
1049
1311
|
//#endregion
|
|
1050
1312
|
//#region src/file-fetcher.ts
|
|
@@ -1310,12 +1572,6 @@ function formatOutcome(outcome, source) {
|
|
|
1310
1572
|
}
|
|
1311
1573
|
}
|
|
1312
1574
|
//#endregion
|
|
1313
|
-
//#region src/server-url.ts
|
|
1314
|
-
const DEFAULT_SERVER = "https://novedu-chat-mvp-at.azurewebsites.net";
|
|
1315
|
-
function resolveServerUrl(cliOption) {
|
|
1316
|
-
return cliOption || process.env.NOVEDU_SERVER || DEFAULT_SERVER;
|
|
1317
|
-
}
|
|
1318
|
-
//#endregion
|
|
1319
1575
|
//#region src/commands/whoami.ts
|
|
1320
1576
|
function registerWhoami(program) {
|
|
1321
1577
|
program.command("whoami").description("Show who is signed in by calling the Novedu server's /api/me").option("--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)").action(async (options) => {
|
|
@@ -1359,6 +1615,8 @@ registerValidate(program);
|
|
|
1359
1615
|
registerLogin(program);
|
|
1360
1616
|
registerLogout(program);
|
|
1361
1617
|
registerWhoami(program);
|
|
1618
|
+
registerCodes(program);
|
|
1619
|
+
registerFiles(program);
|
|
1362
1620
|
program.parseAsync().catch((err) => {
|
|
1363
1621
|
console.error(err instanceof Error ? err.message : err);
|
|
1364
1622
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novedu/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Command-line companion for the Novedu chat app. Validates tutor, fragment, quiz, writing and coding YAML definitions
|
|
3
|
+
"version": "0.9.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"prepublishOnly": "npm run build"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@azure/msal-node": "^5.
|
|
28
|
+
"@azure/msal-node": "^5.4.0",
|
|
29
29
|
"commander": "^15.0.0",
|
|
30
30
|
"handlebars": "^4.7.9",
|
|
31
31
|
"yaml": "^2.9.0",
|
|
32
32
|
"zod": "^4.4.3"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"tsdown": "^0.22.
|
|
35
|
+
"tsdown": "^0.22.4"
|
|
36
36
|
}
|
|
37
37
|
}
|