recess-cli 2.6.0 → 2.7.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 +9 -1
- package/dist/cli.js +117 -9
- package/dist/command-schema.js +1 -0
- package/dist/commands/onboarding.js +25 -0
- package/dist/commands/village-events.js +26 -3
- package/dist/help.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -274,7 +274,10 @@ recess --json goals list --student <kid-id> --reason "Review the student's goals
|
|
|
274
274
|
recess --json todos create --student <kid-id> --title "Read chapter 4" --reason "Add the assigned reading"
|
|
275
275
|
recess --json todos complete <todo-id> --xp 35 --reason "Complete the todo with a 35 XP total reward"
|
|
276
276
|
recess --json goals delete <goal-id> --student <kid-id> --reason "Remove this obsolete goal"
|
|
277
|
+
recess --json goals restore <goal-id> --student <kid-id> --reason "Restore this goal"
|
|
277
278
|
recess --json todos delete <todo-id> --reason "Remove this disposable todo"
|
|
279
|
+
recess --json todos restore <todo-id> --reason "Restore this todo"
|
|
280
|
+
recess --json todos generate-learning-analysis <todo-id> --reason "Backfill this todo's missing learning analysis"
|
|
278
281
|
recess --json todos generate-applet <todo-id> --student <kid-id> --reason "Generate this todo's applet"
|
|
279
282
|
recess --json memories context --student <kid-id> --reason "Review durable tutor context"
|
|
280
283
|
recess --json memories log --student <kid-id> --date 2026-08-12 --reason "Review the learning log for this date"
|
|
@@ -286,7 +289,12 @@ into the confirmed request. `todos complete` is staff-only; `--xp` is the target
|
|
|
286
289
|
todo, so its preview reports prior credit and the new delta before the normal completion and reward
|
|
287
290
|
side effects run. Applet generation is also staff-only: it resolves the todo's latest learning
|
|
288
291
|
analysis, defaults the generated todo to tomorrow in the student's timezone, and lets the server
|
|
289
|
-
select v1 or v2 for that student.
|
|
292
|
+
select v1 or v2 for that student. Learning-analysis generation is ADMIN-only and queues the
|
|
293
|
+
idempotent forensic pipeline from the latest completed Gemini analysis without rerunning completion
|
|
294
|
+
or rewards. Goal and todo deletion is soft-only. Restoring a goal retains its Mesa workspace,
|
|
295
|
+
rechecks open-goal capacity, and restores only linked todos sharing that deletion's tombstone;
|
|
296
|
+
standalone todo restore uses the same preview-bound tombstone check. A guardian cannot target
|
|
297
|
+
another family, inspect frozen/deleted
|
|
290
298
|
template history, choose todo rewards/completion/internal fields, or use the ADMIN-only
|
|
291
299
|
device-authorization flow. `memories context` and `memories log` read only the tutor repository's
|
|
292
300
|
spine, rules, reminders, and session logs; private guide remarks are never returned. The CLI does
|
package/dist/cli.js
CHANGED
|
@@ -36,9 +36,6 @@ function requiredRequestReason(parsed) {
|
|
|
36
36
|
* worse than no preview at all.
|
|
37
37
|
*/
|
|
38
38
|
async function resolveSessionRole(api) {
|
|
39
|
-
if (api.config.authSource === "config" && api.config.user?.role) {
|
|
40
|
-
return api.config.user.role;
|
|
41
|
-
}
|
|
42
39
|
const session = await api.client.GET("/auth/admin-cli/session/");
|
|
43
40
|
return session.data?.user.role;
|
|
44
41
|
}
|
|
@@ -4086,7 +4083,7 @@ export async function runCommand(argv) {
|
|
|
4086
4083
|
// answers 401. A preview is a promise about what --confirm will do.
|
|
4087
4084
|
const sessionRole = await resolveSessionRole(api);
|
|
4088
4085
|
if (sessionRole && sessionRole !== "ADMIN") {
|
|
4089
|
-
throw new CliError("forbidden", "Deleting a goal is
|
|
4086
|
+
throw new CliError("forbidden", "Deleting a goal is admin-only. To free an open-goal slot for a finished course, use `recess goals archive <goal-id> --student <kid-id>` — it sets the terminal status, keeps the goal and its history, and needs no staff involvement.", 1, { sessionRole, requiredRole: "ADMIN", alternative: "goals archive" });
|
|
4090
4087
|
}
|
|
4091
4088
|
const current = unwrap(await api.client.GET("/tutor/browser/students/{userId}/goals/", {
|
|
4092
4089
|
params: { path: { userId: studentId } },
|
|
@@ -4104,7 +4101,7 @@ export async function runCommand(argv) {
|
|
|
4104
4101
|
},
|
|
4105
4102
|
request: { expectedUpdatedAt: current.updatedAt },
|
|
4106
4103
|
details: {
|
|
4107
|
-
note: "Soft-deletes the goal and its current/future dated todos. Historical todos
|
|
4104
|
+
note: "Soft-deletes the goal and its current/future dated todos. Historical todos, the deletion audit, and any Mesa workspace remain available; `goals restore` reverses this tombstone.",
|
|
4108
4105
|
},
|
|
4109
4106
|
};
|
|
4110
4107
|
return previewBoundWrite(parsed, preview, async () => {
|
|
@@ -4114,6 +4111,39 @@ export async function runCommand(argv) {
|
|
|
4114
4111
|
return { deleted: true, goalId, studentUserId: studentId };
|
|
4115
4112
|
});
|
|
4116
4113
|
}
|
|
4114
|
+
if (verb === "restore") {
|
|
4115
|
+
const goalId = positional(parsed, 2, "goal ID");
|
|
4116
|
+
const studentId = flagString(parsed, "student", { required: true });
|
|
4117
|
+
const sessionRole = await resolveSessionRole(api);
|
|
4118
|
+
if (sessionRole && sessionRole !== "ADMIN") {
|
|
4119
|
+
throw new CliError("forbidden", "Restoring a deleted goal is admin-only.", 1, { sessionRole, requiredRole: "ADMIN" });
|
|
4120
|
+
}
|
|
4121
|
+
const state = unwrap(await api.client.GET("/admin/browser/students/goals/{goalId}/deletion-state/", { params: { path: { goalId } } }));
|
|
4122
|
+
if (state.userId !== studentId) {
|
|
4123
|
+
throw new CliError("not_found", `Goal ${goalId} was not found for student ${studentId}.`);
|
|
4124
|
+
}
|
|
4125
|
+
if (!state.deletedAt) {
|
|
4126
|
+
throw new CliError("not_deleted", `Goal ${goalId} is not deleted.`);
|
|
4127
|
+
}
|
|
4128
|
+
if (!state.restorable) {
|
|
4129
|
+
throw new CliError("restore_unavailable", state.restoreBlockedReason ?? "This goal cannot be restored safely.");
|
|
4130
|
+
}
|
|
4131
|
+
const body = { expectedDeletedAt: state.deletedAt };
|
|
4132
|
+
const preview = {
|
|
4133
|
+
action: "restore a soft-deleted goal for a managed student",
|
|
4134
|
+
target: {
|
|
4135
|
+
goalId,
|
|
4136
|
+
studentUserId: studentId,
|
|
4137
|
+
title: state.title,
|
|
4138
|
+
status: state.status,
|
|
4139
|
+
},
|
|
4140
|
+
request: body,
|
|
4141
|
+
details: {
|
|
4142
|
+
note: "Clears the goal tombstone and restores only linked todos that were tombstoned by the same goal deletion. Restoring an ACTIVE or PAUSED goal rechecks the student's open-goal capacity.",
|
|
4143
|
+
},
|
|
4144
|
+
};
|
|
4145
|
+
return previewBoundWrite(parsed, preview, async () => unwrap(await api.client.POST("/admin/browser/students/goals/{goalId}/restore/", { params: { path: { goalId } }, body })));
|
|
4146
|
+
}
|
|
4117
4147
|
if (verb === "queue") {
|
|
4118
4148
|
const subverb = positional(parsed, 2, "queue action (get|set)");
|
|
4119
4149
|
const goalId = positional(parsed, 3, "goal ID");
|
|
@@ -4205,7 +4235,7 @@ export async function runCommand(argv) {
|
|
|
4205
4235
|
}
|
|
4206
4236
|
throw new CliError("invalid_arguments", "Use goals queue get|set.");
|
|
4207
4237
|
}
|
|
4208
|
-
throw new CliError("invalid_arguments", "Use goals list|create|edit|delete|archive|unarchive|complete|undo-completion|queue|files|pdf.");
|
|
4238
|
+
throw new CliError("invalid_arguments", "Use goals list|create|edit|delete|restore|archive|unarchive|complete|undo-completion|queue|files|pdf.");
|
|
4209
4239
|
}
|
|
4210
4240
|
if (noun === "students") {
|
|
4211
4241
|
if (verb === "list") {
|
|
@@ -4397,7 +4427,7 @@ export async function runCommand(argv) {
|
|
|
4397
4427
|
params: { path: { id: todoId } },
|
|
4398
4428
|
}));
|
|
4399
4429
|
const preview = {
|
|
4400
|
-
action: "
|
|
4430
|
+
action: "soft-delete a todo for a managed student",
|
|
4401
4431
|
target: {
|
|
4402
4432
|
todoId,
|
|
4403
4433
|
studentUserId: current.userId,
|
|
@@ -4411,13 +4441,91 @@ export async function runCommand(argv) {
|
|
|
4411
4441
|
creationSource: current.creationSource,
|
|
4412
4442
|
creationSourceId: current.creationSourceId,
|
|
4413
4443
|
url: "url" in current ? current.url : null,
|
|
4414
|
-
note: "
|
|
4444
|
+
note: "Sets deletedAt without awarding XP or running todo-completion side effects. The todo, its work evidence, and related records remain available; `todos restore` reverses the tombstone.",
|
|
4415
4445
|
},
|
|
4416
4446
|
};
|
|
4417
4447
|
return previewBoundWrite(parsed, preview, async () => unwrap(await api.client.DELETE("/admin/todos/{id}/", {
|
|
4418
4448
|
params: { path: { id: todoId } },
|
|
4419
4449
|
})));
|
|
4420
4450
|
}
|
|
4451
|
+
if (verb === "restore") {
|
|
4452
|
+
const todoId = positional(parsed, 2, "todo ID");
|
|
4453
|
+
const state = unwrap(await api.client.GET("/admin/todos/{id}/deletion-state", {
|
|
4454
|
+
params: { path: { id: todoId } },
|
|
4455
|
+
}));
|
|
4456
|
+
if (!state.deletedAt) {
|
|
4457
|
+
throw new CliError("not_deleted", `Todo ${todoId} is not deleted.`);
|
|
4458
|
+
}
|
|
4459
|
+
if (state.goal?.deletedAt) {
|
|
4460
|
+
throw new CliError("restore_parent_goal", `Todo ${todoId} belongs to deleted goal ${state.goal.id}. Restore the goal instead so its deletion set is reversed together.`);
|
|
4461
|
+
}
|
|
4462
|
+
const body = { expectedDeletedAt: state.deletedAt };
|
|
4463
|
+
const preview = {
|
|
4464
|
+
action: "restore a soft-deleted todo for a managed student",
|
|
4465
|
+
target: {
|
|
4466
|
+
todoId,
|
|
4467
|
+
studentUserId: state.userId,
|
|
4468
|
+
title: state.title,
|
|
4469
|
+
status: state.status,
|
|
4470
|
+
goalId: state.goal?.id ?? null,
|
|
4471
|
+
},
|
|
4472
|
+
request: body,
|
|
4473
|
+
details: {
|
|
4474
|
+
dueDate: state.dueDate,
|
|
4475
|
+
note: "Clears deletedAt only when the tombstone still matches this preview. All preserved work evidence and related records become visible with the todo again.",
|
|
4476
|
+
},
|
|
4477
|
+
};
|
|
4478
|
+
return previewBoundWrite(parsed, preview, async () => unwrap(await api.client.POST("/admin/todos/{id}/restore", {
|
|
4479
|
+
params: { path: { id: todoId } },
|
|
4480
|
+
body,
|
|
4481
|
+
})));
|
|
4482
|
+
}
|
|
4483
|
+
if (verb === "generate-learning-analysis") {
|
|
4484
|
+
const todoId = positional(parsed, 2, "todo ID");
|
|
4485
|
+
const sessionRole = await resolveSessionRole(api);
|
|
4486
|
+
if (sessionRole !== "ADMIN") {
|
|
4487
|
+
throw new CliError("forbidden", "Generating a learning analysis is admin-only.", 1, { sessionRole: sessionRole ?? null, requiredRole: "ADMIN" });
|
|
4488
|
+
}
|
|
4489
|
+
const [todoResponse, detailsResponse, geminiResponse] = await Promise.all([
|
|
4490
|
+
api.client.GET("/tutor/browser/todos/{id}/", {
|
|
4491
|
+
params: { path: { id: todoId } },
|
|
4492
|
+
}),
|
|
4493
|
+
api.client.GET("/admin/todos/{id}/details", {
|
|
4494
|
+
params: { path: { id: todoId } },
|
|
4495
|
+
}),
|
|
4496
|
+
api.client.GET("/admin/todos/{id}/gemini-analysis", {
|
|
4497
|
+
params: { path: { id: todoId } },
|
|
4498
|
+
}),
|
|
4499
|
+
]);
|
|
4500
|
+
const currentTodo = unwrap(todoResponse);
|
|
4501
|
+
const details = unwrap(detailsResponse);
|
|
4502
|
+
const gemini = unwrap(geminiResponse);
|
|
4503
|
+
const latestCompletedGeminiAnalysis = gemini.newPipelineAnalyses.find((analysis) => analysis.status === "completed");
|
|
4504
|
+
if (!latestCompletedGeminiAnalysis) {
|
|
4505
|
+
throw new CliError("not_found", "This todo does not have a completed Gemini analysis to use.");
|
|
4506
|
+
}
|
|
4507
|
+
const body = {
|
|
4508
|
+
geminiAnalysisId: latestCompletedGeminiAnalysis.id,
|
|
4509
|
+
};
|
|
4510
|
+
const preview = {
|
|
4511
|
+
action: "queue a learning analysis from a todo's latest completed Gemini analysis",
|
|
4512
|
+
target: {
|
|
4513
|
+
todoId,
|
|
4514
|
+
studentUserId: currentTodo.userId,
|
|
4515
|
+
title: currentTodo.title,
|
|
4516
|
+
status: currentTodo.status,
|
|
4517
|
+
},
|
|
4518
|
+
request: body,
|
|
4519
|
+
details: {
|
|
4520
|
+
existingLearningAnalysisId: details.learningAnalysis?.id ?? null,
|
|
4521
|
+
note: "The forensic learning pipeline is idempotent for this Gemini analysis. The command does not rerun completion or change rewards.",
|
|
4522
|
+
},
|
|
4523
|
+
};
|
|
4524
|
+
return writeCommand(parsed, preview, async () => unwrap(await api.client.POST("/admin/learning-pipeline/{todoId}/start/", {
|
|
4525
|
+
params: { path: { todoId } },
|
|
4526
|
+
body,
|
|
4527
|
+
})));
|
|
4528
|
+
}
|
|
4421
4529
|
if (verb === "generate-applet") {
|
|
4422
4530
|
const todoId = positional(parsed, 2, "todo ID");
|
|
4423
4531
|
const studentId = flagString(parsed, "student", { required: true });
|
|
@@ -4459,7 +4567,7 @@ export async function runCommand(argv) {
|
|
|
4459
4567
|
body: targetDueDateISO ? { targetDueDateISO } : {},
|
|
4460
4568
|
})));
|
|
4461
4569
|
}
|
|
4462
|
-
throw new CliError("invalid_arguments", "Use todos create|edit|complete|delete|generate-applet.");
|
|
4570
|
+
throw new CliError("invalid_arguments", "Use todos create|edit|complete|delete|restore|generate-learning-analysis|generate-applet.");
|
|
4463
4571
|
}
|
|
4464
4572
|
if (noun === "memories") {
|
|
4465
4573
|
const studentId = flagString(parsed, "student", { required: true });
|
package/dist/command-schema.js
CHANGED
|
@@ -152,6 +152,31 @@ export async function runOnboardingCommand({ parsed, api, writeCommand, }) {
|
|
|
152
152
|
},
|
|
153
153
|
}));
|
|
154
154
|
}
|
|
155
|
+
if (verb === "rearm-kid-first-run") {
|
|
156
|
+
const kidUserId = positional(parsed, 2, "kid ID");
|
|
157
|
+
const preview = unwrap(await api.client.GET("/admin/onboarding/kids/{kidUserId}/first-run/rearm", { params: { path: { kidUserId } } }));
|
|
158
|
+
const body = {
|
|
159
|
+
expectedCompletedAt: preview.kidFirstRunCompletedAt,
|
|
160
|
+
};
|
|
161
|
+
return writeCommand(parsed, {
|
|
162
|
+
action: preview.alreadyEligible
|
|
163
|
+
? "leave the kid's native first-run onboarding rearmed (it is already eligible)"
|
|
164
|
+
: "rearm the kid's Rocky-guided native first-run onboarding",
|
|
165
|
+
target: {
|
|
166
|
+
kidUserId: preview.kid.id,
|
|
167
|
+
familyId: preview.kid.familyId,
|
|
168
|
+
name: personName(preview.kid.firstName, preview.kid.lastName, preview.kid.id),
|
|
169
|
+
email: preview.kid.email,
|
|
170
|
+
},
|
|
171
|
+
request: body,
|
|
172
|
+
details: {
|
|
173
|
+
currentKidFirstRunCompletedAt: preview.kidFirstRunCompletedAt,
|
|
174
|
+
changes: ["User.kidFirstRunCompletedAt → null"],
|
|
175
|
+
retained: "Profile, todos, goals, XP, family/enrollment state, and Village progress are unchanged. A partial prior first run resumes from its existing durable progress.",
|
|
176
|
+
prerequisite: "The kid-onboarding feature flag must evaluate true for this kid separately.",
|
|
177
|
+
},
|
|
178
|
+
}, async () => unwrap(await api.client.POST("/admin/onboarding/kids/{kidUserId}/first-run/rearm", { params: { path: { kidUserId } }, body })));
|
|
179
|
+
}
|
|
155
180
|
if (verb === "timeline") {
|
|
156
181
|
const familyId = positional(parsed, 2, "family ID");
|
|
157
182
|
return unwrap(await api.client.GET("/admin/onboarding/families/{familyId}/timeline", {
|
|
@@ -2,6 +2,7 @@ import { unwrap } from "../api.js";
|
|
|
2
2
|
import { flagString, hasFlag } from "../args.js";
|
|
3
3
|
import { apiError, CliError } from "../errors.js";
|
|
4
4
|
const NONE = "—";
|
|
5
|
+
const WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
|
|
5
6
|
async function fetchTemplatesAndRooms(ctx) {
|
|
6
7
|
const [templatesResponse, rooms] = await Promise.all([
|
|
7
8
|
ctx.api.client.GET("/recess/event-templates-editable/"),
|
|
@@ -17,6 +18,17 @@ function schedule(template) {
|
|
|
17
18
|
return template.singleEventDate.slice(0, 10);
|
|
18
19
|
return template.rrule || NONE;
|
|
19
20
|
}
|
|
21
|
+
function templateDays(template) {
|
|
22
|
+
if (template.singleEventDate)
|
|
23
|
+
return null;
|
|
24
|
+
const match = /BYDAY=([A-Z,]+)/.exec(template.rrule);
|
|
25
|
+
if (!match)
|
|
26
|
+
return null;
|
|
27
|
+
const days = match[1]
|
|
28
|
+
.split(",")
|
|
29
|
+
.filter((day) => WEEKDAYS.includes(day));
|
|
30
|
+
return days.length > 0 ? days : null;
|
|
31
|
+
}
|
|
20
32
|
function renderColumns(rows) {
|
|
21
33
|
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
|
|
22
34
|
return rows
|
|
@@ -45,7 +57,7 @@ async function listEvents(ctx) {
|
|
|
45
57
|
};
|
|
46
58
|
});
|
|
47
59
|
const unlinkedRooms = rooms
|
|
48
|
-
.filter((room) =>
|
|
60
|
+
.filter((room) => room.bookings.length === 0)
|
|
49
61
|
.map((room) => ({ id: room.id, name: room.name }));
|
|
50
62
|
if (hasFlag(ctx.parsed, "json")) {
|
|
51
63
|
return { templates: templateRows, unlinkedRooms };
|
|
@@ -97,14 +109,22 @@ async function linkEvent(ctx) {
|
|
|
97
109
|
throw new CliError("not_found", `No editable event template found for ${templateId}. Run \`recess village events\` to list them.`);
|
|
98
110
|
}
|
|
99
111
|
let room;
|
|
112
|
+
let informationalBookings = [];
|
|
100
113
|
if (roomId) {
|
|
101
114
|
room = rooms.find((row) => row.id === roomId);
|
|
102
115
|
if (!room) {
|
|
103
116
|
throw new CliError("not_found", `No Village Town Center zone found for ${roomId}. Run \`recess village events\` to list linkable zones.`);
|
|
104
117
|
}
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
const otherBookings = room.bookings.filter((booking) => booking.templateId !== templateId);
|
|
119
|
+
const days = templateDays(template);
|
|
120
|
+
const conflict = days
|
|
121
|
+
? otherBookings.find((booking) => booking.days.some((day) => days.includes(day)))
|
|
122
|
+
: undefined;
|
|
123
|
+
if (conflict) {
|
|
124
|
+
throw new CliError("invalid_arguments", `Zone ${room.id} (${room.name}) is already linked to template ${conflict.templateId} (${conflict.name}). Unlink that template first.`);
|
|
107
125
|
}
|
|
126
|
+
if (!days)
|
|
127
|
+
informationalBookings = otherBookings;
|
|
108
128
|
}
|
|
109
129
|
const previousRoom = template.villageRoomId
|
|
110
130
|
? rooms.find((row) => row.id === template.villageRoomId)
|
|
@@ -123,6 +143,9 @@ async function linkEvent(ctx) {
|
|
|
123
143
|
currentVillageRoomId: template.villageRoomId,
|
|
124
144
|
},
|
|
125
145
|
request: body,
|
|
146
|
+
...(informationalBookings.length > 0 && {
|
|
147
|
+
details: { bookings: informationalBookings },
|
|
148
|
+
}),
|
|
126
149
|
}, async () => {
|
|
127
150
|
const result = await api.client.PATCH("/recess/event-templates/{id}/", {
|
|
128
151
|
params: { path: { id: template.id } },
|
package/dist/help.js
CHANGED
|
@@ -148,6 +148,7 @@ Usage:
|
|
|
148
148
|
recess [--json] onboarding queue [--school <institution-slug>]
|
|
149
149
|
recess [--json] onboarding kids [--time-period-days N] [--cohort <id>]
|
|
150
150
|
[--limit N] [--stage-filter all|scheduled|oriented|course|converted|lost]
|
|
151
|
+
recess [--json] onboarding rearm-kid-first-run <kid-id> [--confirm]
|
|
151
152
|
recess [--json] onboarding timeline <family-id>
|
|
152
153
|
recess [--json] onboarding readiness <family-id>
|
|
153
154
|
recess [--json] onboarding family <family-id>
|
|
@@ -352,6 +353,8 @@ Usage:
|
|
|
352
353
|
--delta TEXT [--confirm --approval-token TOKEN]
|
|
353
354
|
recess [--json] goals delete <goal-id> --student <kid-id>
|
|
354
355
|
[--confirm --approval-token TOKEN]
|
|
356
|
+
recess [--json] goals restore <goal-id> --student <kid-id>
|
|
357
|
+
[--confirm --approval-token TOKEN]
|
|
355
358
|
recess [--json] goals complete <goal-id> [--confirm]
|
|
356
359
|
recess [--json] goals undo-completion <goal-id> [--confirm]
|
|
357
360
|
recess [--json] goals archive <goal-id> --student <kid-id>
|
|
@@ -370,6 +373,9 @@ Usage:
|
|
|
370
373
|
[--confirm --approval-token TOKEN]
|
|
371
374
|
recess [--json] todos delete <todo-id>
|
|
372
375
|
[--confirm --approval-token TOKEN]
|
|
376
|
+
recess [--json] todos restore <todo-id>
|
|
377
|
+
[--confirm --approval-token TOKEN]
|
|
378
|
+
recess [--json] todos generate-learning-analysis <todo-id> [--confirm]
|
|
373
379
|
recess [--json] todos generate-applet <todo-id> --student <kid-id>
|
|
374
380
|
[--due-date YYYY-MM-DD] [--confirm --approval-token TOKEN]
|
|
375
381
|
recess [--json] memories context --student <kid-id>
|