recess-cli 3.0.0 → 3.2.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.
@@ -2,6 +2,8 @@ import { CliError } from "./errors.js";
2
2
  export const AGENT_CONTEXT_SCHEMA_VERSION = "4";
3
3
  const BOOLEAN_FLAGS = new Set([
4
4
  "allow-possible-duplicate",
5
+ "allow-state-modification",
6
+ "allow-assigned-edits",
5
7
  "all-references",
6
8
  "analyzed",
7
9
  "apply",
@@ -27,6 +29,7 @@ const BOOLEAN_FLAGS = new Set([
27
29
  "mirrored",
28
30
  "no-collision",
29
31
  "no-invite",
32
+ "open",
30
33
  "refresh",
31
34
  "resend",
32
35
  "restore",
@@ -139,6 +142,15 @@ const AUTHENTICATED_COMMAND_PREFIXES = [
139
142
  "village render",
140
143
  ];
141
144
  const FAMILY_AI_COMMANDS = new Set([
145
+ "apps assign",
146
+ "apps list",
147
+ "apps preview",
148
+ "apps publish",
149
+ "apps pull",
150
+ "apps scaffold",
151
+ "apps standards",
152
+ "apps status",
153
+ "apps validate",
142
154
  "chat-logs by-todo",
143
155
  "chat-logs get",
144
156
  "chat-logs list",
@@ -161,6 +173,7 @@ const FAMILY_AI_COMMANDS = new Set([
161
173
  "goals files read",
162
174
  "goals files write",
163
175
  "goals list",
176
+ "goals move-module",
164
177
  "goals pdf upload",
165
178
  "goals queue get",
166
179
  "goals queue set",
@@ -179,6 +192,10 @@ const FAMILY_AI_COMMANDS = new Set([
179
192
  "students xp-history",
180
193
  "todos create",
181
194
  "todos edit",
195
+ "tutor-templates assign",
196
+ "tutor-templates get",
197
+ "tutor-templates list",
198
+ "tutor-templates unassign",
182
199
  ]);
183
200
  const STAFF_COMMANDS = new Set([
184
201
  "cohorts schedule get",
@@ -191,15 +208,6 @@ const STAFF_COMMANDS = new Set([
191
208
  "memories history",
192
209
  "memories revision",
193
210
  "memories restore",
194
- "apps assign",
195
- "apps list",
196
- "apps preview",
197
- "apps publish",
198
- "apps pull",
199
- "apps scaffold",
200
- "apps standards",
201
- "apps status",
202
- "apps validate",
203
211
  "cohorts email",
204
212
  "cohorts end",
205
213
  "cohorts get",
@@ -291,6 +299,9 @@ const STAFF_COMMANDS = new Set([
291
299
  "todos delete",
292
300
  "todos restore",
293
301
  "todos generate-applet",
302
+ "tutor-templates create",
303
+ "tutor-templates delete",
304
+ "tutor-templates update",
294
305
  "users get",
295
306
  "users tier list-tiers",
296
307
  "village events",
@@ -358,13 +358,22 @@ async function submit(ctx, dir, dryRun) {
358
358
  const manifest = await readManifest(dir);
359
359
  const link = await readAppLink(dir);
360
360
  const name = flagString(ctx.parsed, "name") ?? manifest.title ?? path.basename(dir);
361
+ // --open ships the folder's frame as written: Studio skips the locked-scaffold check and the
362
+ // guide's own shell, styles and screens are what the kid gets. Plain apps already own every file.
363
+ const open = hasFlag(ctx.parsed, "open");
364
+ if (open && !manifest.template) {
365
+ throw new CliError("invalid_arguments", "--open is for template apps (manifest.template); a plain app already ships every file as written.");
366
+ }
367
+ const template = manifest.template
368
+ ? { ...manifest.template, ...(open ? { scaffold: false } : {}) }
369
+ : undefined;
361
370
  const body = {
362
371
  ...(link?.projectId ? { projectId: link.projectId } : {}),
363
372
  name,
364
373
  ...(manifest.description ? { description: manifest.description } : {}),
365
374
  files,
366
375
  contract,
367
- ...(manifest.template ? { template: manifest.template } : {}),
376
+ ...(template ? { template } : {}),
368
377
  ...(manifest.primitives?.length ? { primitives: manifest.primitives } : {}),
369
378
  dryRun,
370
379
  };
@@ -0,0 +1,187 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { unwrap } from "../api.js";
5
+ import { flagString } from "../args.js";
6
+ import { CliError } from "../errors.js";
7
+ import { flagInteger, positional } from "./shared.js";
8
+ /** Selection files only supply explicit IDs; they never expand a filter into a mutation. */
9
+ export async function readAssignmentSelection(filePath) {
10
+ const absolutePath = path.resolve(filePath);
11
+ const stat = await fs.stat(absolutePath);
12
+ if (!stat.isFile() || stat.size > 1024 * 1024) {
13
+ throw new CliError("invalid_arguments", "The assignment selection must be a regular JSON file no larger than 1 MiB.");
14
+ }
15
+ const bytes = await fs.readFile(absolutePath);
16
+ let document;
17
+ try {
18
+ document = JSON.parse(bytes.toString("utf8"));
19
+ }
20
+ catch {
21
+ throw new CliError("invalid_arguments", "The assignment selection must contain valid JSON.");
22
+ }
23
+ if (!document ||
24
+ typeof document !== "object" ||
25
+ Array.isArray(document) ||
26
+ Object.keys(document).length !== 1 ||
27
+ !("todoIds" in document) ||
28
+ !Array.isArray(document.todoIds) ||
29
+ !document.todoIds.every((id) => typeof id === "string")) {
30
+ throw new CliError("invalid_arguments", 'The selection file must contain exactly {"todoIds":["assignment-uuid", ...]}.');
31
+ }
32
+ return {
33
+ todoIds: document.todoIds,
34
+ file: {
35
+ absolutePath,
36
+ sizeBytes: bytes.byteLength,
37
+ sha256: createHash("sha256").update(bytes).digest("hex"),
38
+ },
39
+ };
40
+ }
41
+ async function readResolutions(filePath) {
42
+ const absolutePath = path.resolve(filePath);
43
+ let bytes;
44
+ try {
45
+ const stat = await fs.stat(absolutePath);
46
+ if (!stat.isFile() || stat.size > 1024 * 1024) {
47
+ throw new CliError("invalid_arguments", "The resolution file must be a regular JSON file no larger than 1 MiB.");
48
+ }
49
+ bytes = await fs.readFile(absolutePath);
50
+ }
51
+ catch (error) {
52
+ if (error.code === "ENOENT") {
53
+ throw new CliError("invalid_arguments", `File not found: ${absolutePath}`);
54
+ }
55
+ throw error;
56
+ }
57
+ let document;
58
+ try {
59
+ document = JSON.parse(bytes.toString("utf8"));
60
+ }
61
+ catch {
62
+ throw new CliError("invalid_arguments", "The resolution file must contain valid JSON.");
63
+ }
64
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
65
+ throw new CliError("invalid_arguments", "The resolution file must contain a JSON object.");
66
+ }
67
+ // The backend validates document version, choices, conflict IDs, and whether
68
+ // each choice is supported by the current three-way comparison.
69
+ return {
70
+ document: document,
71
+ file: {
72
+ absolutePath,
73
+ sizeBytes: bytes.byteLength,
74
+ sha256: createHash("sha256").update(bytes).digest("hex"),
75
+ },
76
+ };
77
+ }
78
+ export async function runGoalCurriculumCommand({ parsed, api, resolveTemplateId, previewBoundWrite, recoverExecutedPreview, }) {
79
+ const verb = positional(parsed, 2, "curriculum operation");
80
+ const [id, session] = await Promise.all([
81
+ resolveTemplateId(positional(parsed, 3, "template ID or slug")),
82
+ api.client.GET("/auth/admin-cli/session/").then(unwrap),
83
+ ]);
84
+ if (session.user.role !== "ADMIN" || session.cliScope !== "full_admin") {
85
+ throw new CliError("forbidden", "Curriculum updates require a full-admin session.");
86
+ }
87
+ const environment = {
88
+ actorId: session.user.id,
89
+ apiOrigin: api.config.apiOrigin,
90
+ };
91
+ if (verb === "status" || verb === "retry") {
92
+ const jobId = flagString(parsed, "job", { required: true });
93
+ if (verb === "status") {
94
+ return unwrap(await api.client.GET("/ai/goal-templates/{id}/curriculum/jobs/{jobId}", {
95
+ params: { path: { id, jobId } },
96
+ }));
97
+ }
98
+ const requestPreview = {
99
+ action: "resume the previously approved curriculum job",
100
+ target: { templateId: id, jobId, ...environment },
101
+ request: {},
102
+ };
103
+ const receipt = await recoverExecutedPreview(requestPreview);
104
+ const preview = receipt ?? {
105
+ ...requestPreview,
106
+ details: {
107
+ job: unwrap(await api.client.GET("/ai/goal-templates/{id}/curriculum/jobs/{jobId}", {
108
+ params: { path: { id, jobId } },
109
+ })),
110
+ note: "Resumes this job's stored scope and resolutions. Completed goals are not replayed; stale goals require a new preview.",
111
+ },
112
+ };
113
+ return previewBoundWrite(parsed, preview, async () => unwrap(await api.client.POST("/ai/goal-templates/{id}/curriculum/jobs/{jobId}/retry", {
114
+ params: { path: { id, jobId } },
115
+ body: {},
116
+ })));
117
+ }
118
+ const goalIds = flagString(parsed, "goals", { required: true })
119
+ .split(",")
120
+ .map((value) => value.trim());
121
+ if (goalIds.some((value) => !value) ||
122
+ new Set(goalIds).size !== goalIds.length) {
123
+ throw new CliError("invalid_arguments", "--goals must list distinct, nonempty goal IDs.");
124
+ }
125
+ if (verb === "backlog") {
126
+ const dueBefore = flagString(parsed, "due-before");
127
+ return unwrap(await api.client.GET("/ai/goal-templates/{id}/curriculum/backlog", {
128
+ params: {
129
+ path: { id },
130
+ query: { goalIds: goalIds.join(","), dueBefore },
131
+ },
132
+ }));
133
+ }
134
+ const targetVersion = flagInteger(parsed, "version", {
135
+ required: true,
136
+ min: 1,
137
+ });
138
+ const filePath = flagString(parsed, "resolutions-file", {
139
+ required: verb === "validate-resolutions",
140
+ });
141
+ const resolutionFile = filePath ? await readResolutions(filePath) : undefined;
142
+ const body = {
143
+ goalIds,
144
+ targetVersion,
145
+ resolutions: resolutionFile?.document ?? { version: 1, resolutions: [] },
146
+ };
147
+ const requestPreview = {
148
+ action: verb === "reverse"
149
+ ? "merge a prior template curriculum version into the selected student goals"
150
+ : "merge the selected template curriculum version into the selected student goals",
151
+ target: { templateId: id, goalIds, ...environment },
152
+ request: { ...body, resolutionFile: resolutionFile?.file ?? null },
153
+ };
154
+ const receipt = verb === "apply" || verb === "reverse"
155
+ ? await recoverExecutedPreview(requestPreview)
156
+ : null;
157
+ const comparison = receipt
158
+ ? receipt.details?.comparison
159
+ : unwrap(await api.client.POST("/ai/goal-templates/{id}/curriculum/preview", {
160
+ params: { path: { id } },
161
+ body,
162
+ }));
163
+ if (verb === "preview" || verb === "validate-resolutions") {
164
+ return { ...comparison, resolutionFile: resolutionFile?.file ?? null };
165
+ }
166
+ if (verb !== "apply" && verb !== "reverse") {
167
+ throw new CliError("invalid_arguments", "Use curriculum backlog|preview|validate-resolutions|apply|status|retry|reverse.");
168
+ }
169
+ const preview = receipt ?? {
170
+ ...requestPreview,
171
+ details: {
172
+ comparison,
173
+ note: "Only the exact selected goals are included. Unresolved conflicts block their goal. Assignments and earned history remain protected; reverse updates preserve changes made since the earlier version.",
174
+ },
175
+ };
176
+ return previewBoundWrite(parsed, preview, async () => unwrap(await api.client.POST("/ai/goal-templates/{id}/curriculum/apply", {
177
+ params: { path: { id } },
178
+ body: {
179
+ ...body,
180
+ fingerprint: comparison.fingerprint,
181
+ operationKey: flagString(parsed, "operation-key", {
182
+ required: true,
183
+ }),
184
+ },
185
+ })));
186
+ }
187
+ //# sourceMappingURL=goal-curriculum.js.map
package/dist/help.js CHANGED
@@ -28,8 +28,8 @@ Usage:
28
28
  recess [--json] jobs prune [--older-than-days 30]
29
29
  recess [--json] feedback list [--limit 20]
30
30
  recess [--json] feedback submit <text> [--confirm]
31
- recess [--json] auth login [--client-id ID] [--callback-port 8765]
32
- recess [--json] auth request [--label TEXT]
31
+ recess [--json] auth login [--client-id ID] [--callback-port 8765] [--duration 12h|30d]
32
+ recess [--json] auth request [--label TEXT] [--duration 12h|30d]
33
33
  recess [--json] auth poll [--timeout 300]
34
34
  recess [--json] auth status|logout
35
35
  recess [--json] users search <name-or-id> [--limit 10]
@@ -78,8 +78,8 @@ Usage:
78
78
  recess [--json] apps standards <query>
79
79
  recess [--json] apps list
80
80
  recess [--json] apps pull <project-id> [dir]
81
- recess [--json] apps validate [dir] [--name TEXT] [--no-wait]
82
- recess [--json] apps publish [dir] [--name TEXT] [--assign <kid-id>] [--due YYYY-MM-DD] [--no-wait]
81
+ recess [--json] apps validate [dir] [--name TEXT] [--open] [--no-wait]
82
+ recess [--json] apps publish [dir] [--name TEXT] [--open] [--assign <kid-id>] [--due YYYY-MM-DD] [--no-wait]
83
83
  recess [--json] apps assign [dir] --student <kid-id> [--due YYYY-MM-DD]
84
84
  recess [--json] apps status <build-id>
85
85
  recess [--json] applications list [--status SUBMITTED|CLAIMED|ENROLLED|CLOSED]
@@ -339,6 +339,29 @@ Usage:
339
339
  [--kind SIMPLE|BLUEPRINT] [--starter-only] [--include-deleted]
340
340
  [--limit 20] [--cursor <id>]
341
341
  recess [--json] goal-templates get <template-id|slug> [--spec-only]
342
+ recess [--json] goal-templates policy <template-id|slug>
343
+ recess [--json] goal-templates set-policy <template-id|slug> --recipe KEY
344
+ [--target-modules N] [--required-modules N|all] --expected-version N
345
+ [--confirm --approval-token TOKEN]
346
+ recess [--json] goal-templates migrate-policy <template-id|slug> --students ID,ID
347
+ [--confirm --approval-token TOKEN]
348
+ recess [--json] goal-templates student-policy <template-id|slug> --goal ID
349
+ --file <path/edit.json> [--confirm --approval-token TOKEN]
350
+ recess [--json] goal-templates retire-assignments <template-id|slug> --goal ID
351
+ [--todos ID,ID | --todos-file <path/selection.json>]
352
+ --retirement-reason TEXT [--confirm --approval-token TOKEN]
353
+ recess [--json] goal-templates curriculum backlog <template-id|slug>
354
+ --goals ID,ID [--due-before YYYY-MM-DD]
355
+ recess [--json] goal-templates curriculum preview <template-id|slug>
356
+ --goals ID,ID --version N [--resolutions-file <path/resolutions.json>]
357
+ recess [--json] goal-templates curriculum validate-resolutions <template-id|slug>
358
+ --goals ID,ID --version N --resolutions-file <path/resolutions.json>
359
+ recess [--json] goal-templates curriculum apply|reverse <template-id|slug>
360
+ --goals ID,ID --version N [--resolutions-file <path/resolutions.json>]
361
+ [--confirm --approval-token TOKEN]
362
+ recess [--json] goal-templates curriculum status <template-id|slug> --job ID
363
+ recess [--json] goal-templates curriculum retry <template-id|slug> --job ID
364
+ [--confirm --approval-token TOKEN]
342
365
  recess [--json] goal-templates versions <template-id> [--version N]
343
366
  recess [--json] goal-templates validate-spec --file <path/template.json>
344
367
  recess [--json] goal-templates create --file <path/template.json>
@@ -382,6 +405,8 @@ Usage:
382
405
  [--confirm --approval-token TOKEN]
383
406
  recess [--json] goals unarchive <goal-id> --student <kid-id>
384
407
  [--confirm --approval-token TOKEN]
408
+ recess [--json] goals move-module <module-ref> --student <kid-id> --goal <goal-id>
409
+ (--to-end | --position <n>) [--confirm --approval-token TOKEN]
385
410
  recess [--json] goals queue get <goal-id> --student <kid-id>
386
411
  recess [--json] goals queue set <goal-id> --student <kid-id>
387
412
  --entries-file <path.json> --delta TEXT [--replace-description-pointer]
@@ -412,6 +437,19 @@ Usage:
412
437
  recess [--json] rocky get --student <kid-id>
413
438
  recess [--json] rocky set --student <kid-id> --patch-file <path/patch.json>
414
439
  --expected-updated-at ISO|none [--confirm]
440
+ recess [--json] tutor-templates list [--q TEXT] [--subject TEXT] [--grade N]
441
+ recess [--json] tutor-templates get <slug>
442
+ recess [--json] tutor-templates assign <slug> [--student <kid-id>] [--confirm]
443
+ recess [--json] tutor-templates unassign <slug> [--student <kid-id>] [--confirm]
444
+ recess [--json] tutor-templates create --slug SLUG --title TEXT --summary TEXT
445
+ --subject TEXT --cadence TEXT --body-file <path.md> [--lessons-dir <dir>]
446
+ [--emoji TEXT] [--grade-min N] [--grade-max N]
447
+ [--visibility PUBLIC|STAFF] [--confirm]
448
+ recess [--json] tutor-templates update <slug> [--title TEXT] [--summary TEXT]
449
+ [--subject TEXT] [--cadence TEXT] [--body-file <path.md>]
450
+ [--lessons-dir <dir>] [--emoji TEXT] [--grade-min N] [--grade-max N]
451
+ [--visibility PUBLIC|STAFF] [--confirm]
452
+ recess [--json] tutor-templates delete <slug> [--confirm]
415
453
  recess [--json] goals files list --student <goal-owner-id> --goal <goal-id>
416
454
  recess [--json] goals files read --student <goal-owner-id> --goal <goal-id> --path P
417
455
  recess [--json] goals files init --student <goal-owner-id> --draft <draft-slug>
@@ -420,11 +458,13 @@ Usage:
420
458
  (--goal <goal-id> | --draft <draft-slug>)
421
459
  --output-dir <local-dir>
422
460
  recess [--json] goals files push --source-dir <local-checkout>
423
- [--message TEXT] [--confirm --approval-token TOKEN]
461
+ [--message TEXT] [--allow-state-modification] [--allow-assigned-edits]
462
+ [--confirm --approval-token TOKEN]
424
463
  recess [--json] goals files write --student <goal-owner-id>
425
464
  (--goal <goal-id> | --draft <draft-slug>)
426
465
  (--source-dir <local-dir> | --source-file <local-file> --path P)
427
- [--message TEXT] [--confirm --approval-token TOKEN]
466
+ [--message TEXT] [--allow-state-modification] [--allow-assigned-edits]
467
+ [--confirm --approval-token TOKEN]
428
468
  recess [--json] goals pdf upload --student <goal-owner-id>
429
469
  (--goal <goal-id> | --draft <draft-slug>) --source-file <local.pdf>
430
470
  [--path uploads/name.pdf] [--message TEXT]
@@ -436,6 +476,14 @@ bundled into this npm package. Load os-v2-goal-template-builder and its
436
476
  references/deterministic-workflow-setup.md BEFORE authoring a template; that is
437
477
  the same guidance the recess.gg/ai agent follows, so there is exactly one
438
478
  standard. Responses cache under ~/.recess-cli/skills-cache/ (--refresh re-fetches).
479
+ Goal file writes/pushes warn and refuse curriculum edits with unfinished assignments
480
+ unless --allow-assigned-edits is supplied. This can change lessons while a student
481
+ is working; assignments are not retired/completed and student answers are not merged.
482
+ The flag requires a fresh preview and confirmation; revision checks still apply.
483
+ Goal file writes/pushes require --allow-state-modification to modify state or
484
+ student-work folders, or replace/delete an existing resources/artifacts file.
485
+ Review the protected paths in the warning; the flag does not preserve answers
486
+ automatically or bypass assignment/revision checks. Adding it needs a new preview.
439
487
  For goal workspace and PDF commands, --student names the goal owner. A
440
488
  full_admin session may use a KID or ADMIN user id, including its own ADMIN id;
441
489
  family_ai sessions remain limited to managed KID profiles.
@@ -528,7 +576,8 @@ cannot call /admin; KID sessions cannot call any non-Village API command. For a
528
576
  cloud agent that has no local browser to open, "auth request" prints an approval URL; whoever
529
577
  opens it and approves in a signed-in web session grants THEIR OWN scope, so a guardian approving
530
578
  mints a family-scoped session and only an admin can mint a full-admin one. "auth poll" then
531
- collects the 12h session. Kids cannot approve. When it lapses, run "auth request" again for a
579
+ collects the session. Sessions default to 12h; admins can request 30 days with --duration 30d
580
+ on either login or request. Only admins can approve 30-day requests. Kids cannot approve. When it lapses, run "auth request" again for a
532
581
  fresh link. Both paths yield the same session.
533
582
 
534
583
  Skill notes: this CLI's own agent skill ships inside the npm package AND is served
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recess-cli",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "Safe Recess administration and family AI tools from the command line.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -41,7 +41,8 @@ For machine-readable discovery filtered to the current session's locally decoded
41
41
  - Headless (no browser): `auth request`, then a human approves the printed URL while signed in to
42
42
  Recess, then `auth poll`. The session takes on the **approver's** scope — a guardian approving
43
43
  grants family-only access, not staff access. Kids cannot approve.
44
- - Sessions last 12 hours. `auth status` and `doctor` recheck the live user role and permissions;
44
+ - Sessions default to 12 hours. Admins can use `auth login --duration 30d` locally or
45
+ `auth request --duration 30d` remotely; only an admin can approve a 30-day request. `auth status` and `doctor` recheck the live user role and permissions;
45
46
  discovery stays local and may reflect the minted scope until the next login.
46
47
  - `auth logout` clears the stored session.
47
48
  - The default API is production. If `RECESS_CLI_API_ORIGIN` is set, state the non-default origin before acting.
@@ -90,7 +91,7 @@ bundles/authorized URLs. `/mcp/legacy` temporarily retains `recess_exec({command
90
91
 
91
92
  ## Building Studio apps (`recess apps`)
92
93
 
93
- Guides build Recess Studio apps in their own editor and publish them with the CLI; Studio never
94
+ Guides and guardians build Recess Studio apps in their own editor and publish them with the CLI; Studio never
94
95
  prompts a model for them.
95
96
 
96
97
  - `recess --json apps init <dir> --reason "..."` writes the locked scaffold plus `AGENTS.md` (the
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "3.0.0",
3
- "minCliVersion": "3.0.0"
2
+ "version": "3.2.0",
3
+ "minCliVersion": "3.2.0"
4
4
  }