recess-cli 1.3.2 → 1.4.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 +16 -1
- package/dist/cli.js +219 -0
- package/package.json +1 -1
- package/skill/recess-cli/SKILL.md +29 -3
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ recess setup
|
|
|
13
13
|
|
|
14
14
|
`setup` installs the bundled skill for both Codex and Claude and then opens Recess SSO in your browser (skip the browser step with `--skill-only`; it is also skipped when a live session already exists). Restart your agent afterwards so it discovers the skill. `npx -y recess-cli setup` works too, but leaves no `recess` on your PATH — which is the command the installed skill tells the agent to run — so `setup` warns when it detects it is running from an npx cache.
|
|
15
15
|
|
|
16
|
-
Publishing rides the production deploy (`.github/workflows/admin-cli-publish.yml`): bump `version` in `apps/admin-cli/package.json` in a normal PR to `staging`, and it publishes when `staging` promotes to `production`. A production deploy that did not bump the version is a no-op — a `gate` job checks the version against npm first. The same workflow is still dispatchable by hand for out-of-band releases. pnpm packs the CLI so the workspace `catalog:` dependency becomes a real range; npm publishes that tarball through the
|
|
16
|
+
Publishing rides the production deploy (`.github/workflows/admin-cli-publish.yml`): bump `version` in `apps/admin-cli/package.json` in a normal PR to `staging`, and it publishes when `staging` promotes to `production`. A production deploy that did not bump the version is a no-op — a `gate` job checks the version against npm first. The same workflow is still dispatchable by hand for out-of-band releases. pnpm packs the CLI so the workspace `catalog:` dependency becomes a real range; npm then publishes that tarball through the workflow's OIDC trusted-publishing path. The registry-side publisher must be configured as described in `docs/codebase/admin-cli.md`.
|
|
17
17
|
|
|
18
18
|
## Install (from a checkout — CLI development)
|
|
19
19
|
|
|
@@ -92,6 +92,21 @@ human explicitly approves the exceptional `--allow-strand` override.
|
|
|
92
92
|
|
|
93
93
|
MAP uploads accept one PDF up to 15 MB. The preview includes the resolved path, byte count, and SHA-256 without contacting the API; the confirmed command sends the report to the existing tutor-dashboard extraction route.
|
|
94
94
|
|
|
95
|
+
## Store catalog and Village
|
|
96
|
+
|
|
97
|
+
Village store goods are main-Recess `StoreItem` rows, not Village-island database rows. List them
|
|
98
|
+
and preview an availability change with:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
recess --json store-items list --item-type VILLAGE_ITEM
|
|
102
|
+
recess --json store-items set-status <store-item-id> --status INACTIVE
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The status command resolves the exact `VILLAGE_ITEM` first and includes its name, catalog metadata,
|
|
106
|
+
current status, purchase count, and lack of feed-publication side effects in the approval preview;
|
|
107
|
+
it refuses non-Village item IDs. After approval, rerun the unchanged command with `--confirm`. The
|
|
108
|
+
separate `village models` commands edit the Village island's reusable models and placements.
|
|
109
|
+
|
|
95
110
|
## Authoring learning content
|
|
96
111
|
|
|
97
112
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -28,6 +28,11 @@ Usage:
|
|
|
28
28
|
[--slots N]
|
|
29
29
|
recess [--json] users tier set <kid-id> --tier social|academics|lite|complete|platform
|
|
30
30
|
[--slots N] --expected-updated-at <iso> [--allow-strand] [--confirm]
|
|
31
|
+
recess [--json] guides create --email <email> --first-name TEXT
|
|
32
|
+
[--last-name TEXT] [--no-invite] [--confirm]
|
|
33
|
+
recess [--json] guides invite --user <user-id> [--confirm]
|
|
34
|
+
recess [--json] guardians invite --family <family-id> --email <email>
|
|
35
|
+
--first-name TEXT [--last-name TEXT] [--no-invite] [--confirm]
|
|
31
36
|
recess [--json] students upload-map-scores --student <kid-id>
|
|
32
37
|
--file </path/to/map-report.pdf> [--confirm]
|
|
33
38
|
recess [--json] enrollments list --user <user-id>
|
|
@@ -113,6 +118,12 @@ Usage:
|
|
|
113
118
|
recess [--json] village models remove <placement-id> [--world village-1] [--confirm]
|
|
114
119
|
recess [--json] village render --min-x N --min-z N --max-x N --max-z N
|
|
115
120
|
[--world village-1]
|
|
121
|
+
recess [--json] store-items list [--search TEXT]
|
|
122
|
+
[--status ACTIVE|INACTIVE|COMING_SOON] [--item-type TYPE]
|
|
123
|
+
[--page 0] [--limit 20] [--sort-by name|price|createdAt|updatedAt|order]
|
|
124
|
+
[--sort-order asc|desc]
|
|
125
|
+
recess [--json] store-items set-status <village-store-item-id>
|
|
126
|
+
--status ACTIVE|INACTIVE|COMING_SOON [--confirm]
|
|
116
127
|
recess [--json] content-library search <query> [--limit 8]
|
|
117
128
|
recess [--json] skills list [--query TEXT] [--category TEXT]
|
|
118
129
|
recess [--json] skills get <skill-name> [--reference NAME | --all-references]
|
|
@@ -456,6 +467,27 @@ const SCHOOL_TIER_IDS = SCHOOL_TIER_OPTIONS.map((tier) => tier.id);
|
|
|
456
467
|
const MAX_MAP_PDF_BYTES = 15 * 1024 * 1024;
|
|
457
468
|
const GOAL_TEMPLATE_KINDS = ["SIMPLE", "BLUEPRINT"];
|
|
458
469
|
const GOAL_TEMPLATE_SETUP_AUDIENCES = ["KID_FRIENDLY", "PARENT_SETUP"];
|
|
470
|
+
const STORE_ITEM_STATUSES = ["ACTIVE", "INACTIVE", "COMING_SOON"];
|
|
471
|
+
const STORE_ITEM_TYPES = [
|
|
472
|
+
"PROFILE_RING",
|
|
473
|
+
"ANIMATED_COVER",
|
|
474
|
+
"PROFILE_STICKER",
|
|
475
|
+
"CLASS_PASS",
|
|
476
|
+
"GUIDE_MENTORSHIP",
|
|
477
|
+
"TOOL_CREDIT",
|
|
478
|
+
"MERCH_ITEM",
|
|
479
|
+
"STEAM_GAME",
|
|
480
|
+
"KID_PROFILE_PICTURE",
|
|
481
|
+
"WORLD_AVATAR_ITEM",
|
|
482
|
+
"VILLAGE_ITEM",
|
|
483
|
+
];
|
|
484
|
+
const STORE_ITEM_SORT_FIELDS = [
|
|
485
|
+
"name",
|
|
486
|
+
"price",
|
|
487
|
+
"createdAt",
|
|
488
|
+
"updatedAt",
|
|
489
|
+
"order",
|
|
490
|
+
];
|
|
459
491
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
460
492
|
/**
|
|
461
493
|
* Read and parse a JSON file supplied by an authoring agent. Large payloads —
|
|
@@ -1057,6 +1089,103 @@ export async function runCommand(argv) {
|
|
|
1057
1089
|
}
|
|
1058
1090
|
throw new CliError("invalid_arguments", "Use village models list|upload|publish|archive|place|move|remove.");
|
|
1059
1091
|
}
|
|
1092
|
+
if (noun === "store-items") {
|
|
1093
|
+
if (verb === "list") {
|
|
1094
|
+
const search = flagString(parsed, "search");
|
|
1095
|
+
const rawStatus = flagString(parsed, "status");
|
|
1096
|
+
const rawItemType = flagString(parsed, "item-type");
|
|
1097
|
+
const page = flagNumber(parsed, "page") ?? 0;
|
|
1098
|
+
const limit = flagNumber(parsed, "limit") ?? 20;
|
|
1099
|
+
const rawSortBy = flagString(parsed, "sort-by");
|
|
1100
|
+
const rawSortOrder = flagString(parsed, "sort-order");
|
|
1101
|
+
if (!Number.isInteger(page) || page < 0) {
|
|
1102
|
+
throw new CliError("invalid_arguments", "--page must be a non-negative integer.");
|
|
1103
|
+
}
|
|
1104
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
1105
|
+
throw new CliError("invalid_arguments", "--limit must be an integer from 1 through 100.");
|
|
1106
|
+
}
|
|
1107
|
+
const status = rawStatus
|
|
1108
|
+
? assertChoice(rawStatus, STORE_ITEM_STATUSES, "--status")
|
|
1109
|
+
: undefined;
|
|
1110
|
+
const itemType = rawItemType
|
|
1111
|
+
? assertChoice(rawItemType, STORE_ITEM_TYPES, "--item-type")
|
|
1112
|
+
: undefined;
|
|
1113
|
+
const sortBy = rawSortBy
|
|
1114
|
+
? assertChoice(rawSortBy, STORE_ITEM_SORT_FIELDS, "--sort-by")
|
|
1115
|
+
: "order";
|
|
1116
|
+
const sortOrder = rawSortOrder
|
|
1117
|
+
? assertChoice(rawSortOrder, ["asc", "desc"], "--sort-order")
|
|
1118
|
+
: "asc";
|
|
1119
|
+
return unwrap(await api.client.GET("/admin/store-items/", {
|
|
1120
|
+
params: {
|
|
1121
|
+
query: {
|
|
1122
|
+
...(search ? { search } : {}),
|
|
1123
|
+
...(status ? { status } : {}),
|
|
1124
|
+
...(itemType ? { itemType } : {}),
|
|
1125
|
+
page,
|
|
1126
|
+
limit,
|
|
1127
|
+
sortBy,
|
|
1128
|
+
sortOrder,
|
|
1129
|
+
},
|
|
1130
|
+
},
|
|
1131
|
+
}));
|
|
1132
|
+
}
|
|
1133
|
+
if (verb === "set-status") {
|
|
1134
|
+
const storeItemId = positional(parsed, 2, "store item ID");
|
|
1135
|
+
const status = assertChoice(flagString(parsed, "status", { required: true }), STORE_ITEM_STATUSES, "--status");
|
|
1136
|
+
let page = 0;
|
|
1137
|
+
let storeItem;
|
|
1138
|
+
// Resolve the exact live row before the confirmation gate. Filtering at
|
|
1139
|
+
// the API makes this command incapable of mutating non-Village catalog
|
|
1140
|
+
// items, while the preview can name the row and its current state.
|
|
1141
|
+
do {
|
|
1142
|
+
const result = unwrap(await api.client.GET("/admin/store-items/", {
|
|
1143
|
+
params: {
|
|
1144
|
+
query: {
|
|
1145
|
+
itemType: "VILLAGE_ITEM",
|
|
1146
|
+
page,
|
|
1147
|
+
limit: 100,
|
|
1148
|
+
sortBy: "order",
|
|
1149
|
+
sortOrder: "asc",
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
1152
|
+
}));
|
|
1153
|
+
storeItem = result.items.find((item) => item.id === storeItemId);
|
|
1154
|
+
if (storeItem || page + 1 >= result.totalPages)
|
|
1155
|
+
break;
|
|
1156
|
+
page += 1;
|
|
1157
|
+
} while (true);
|
|
1158
|
+
if (!storeItem) {
|
|
1159
|
+
throw new CliError("not_found", `No Village store item found for ${storeItemId}.`);
|
|
1160
|
+
}
|
|
1161
|
+
const metadata = storeItem.metadata &&
|
|
1162
|
+
typeof storeItem.metadata === "object" &&
|
|
1163
|
+
!Array.isArray(storeItem.metadata)
|
|
1164
|
+
? storeItem.metadata
|
|
1165
|
+
: {};
|
|
1166
|
+
const body = { status };
|
|
1167
|
+
return writeCommand(parsed, {
|
|
1168
|
+
action: `set Village store item status to ${status}`,
|
|
1169
|
+
target: {
|
|
1170
|
+
storeItemId,
|
|
1171
|
+
name: storeItem.name,
|
|
1172
|
+
itemType: storeItem.itemType,
|
|
1173
|
+
villageKind: metadata.villageKind,
|
|
1174
|
+
catalogId: metadata.catalogId,
|
|
1175
|
+
},
|
|
1176
|
+
request: body,
|
|
1177
|
+
details: {
|
|
1178
|
+
currentStatus: storeItem.status,
|
|
1179
|
+
purchaseCount: storeItem._count.purchases,
|
|
1180
|
+
publishesFeedActivity: false,
|
|
1181
|
+
},
|
|
1182
|
+
}, async () => unwrap(await api.client.PATCH("/admin/store-items/{id}", {
|
|
1183
|
+
params: { path: { id: storeItemId } },
|
|
1184
|
+
body,
|
|
1185
|
+
})));
|
|
1186
|
+
}
|
|
1187
|
+
throw new CliError("invalid_arguments", "Use store-items list or store-items set-status.");
|
|
1188
|
+
}
|
|
1060
1189
|
if (noun === "users" && verb === "search") {
|
|
1061
1190
|
const query = parsed.positionals.slice(2).join(" ").trim();
|
|
1062
1191
|
if (!query)
|
|
@@ -1150,6 +1279,96 @@ export async function runCommand(argv) {
|
|
|
1150
1279
|
params: { path: { userId } },
|
|
1151
1280
|
}));
|
|
1152
1281
|
}
|
|
1282
|
+
if (noun === "guides" && verb === "create") {
|
|
1283
|
+
const email = flagString(parsed, "email", { required: true })
|
|
1284
|
+
.trim()
|
|
1285
|
+
.toLowerCase();
|
|
1286
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
1287
|
+
throw new CliError("invalid_arguments", "--email does not look like a valid email address.");
|
|
1288
|
+
}
|
|
1289
|
+
const firstName = flagString(parsed, "first-name", { required: true });
|
|
1290
|
+
const lastName = flagString(parsed, "last-name");
|
|
1291
|
+
const sendInvite = !hasFlag(parsed, "no-invite");
|
|
1292
|
+
const body = {
|
|
1293
|
+
email,
|
|
1294
|
+
firstName,
|
|
1295
|
+
...(lastName !== undefined ? { lastName } : {}),
|
|
1296
|
+
sendInvite,
|
|
1297
|
+
};
|
|
1298
|
+
return writeCommand(parsed, {
|
|
1299
|
+
action: sendInvite
|
|
1300
|
+
? "provision a GUIDE staff account and email its sign-in invite (outward email)"
|
|
1301
|
+
: "provision a GUIDE staff account without sending the invite email",
|
|
1302
|
+
target: { email },
|
|
1303
|
+
request: body,
|
|
1304
|
+
}, async () => unwrap(await api.client.POST("/admin/guides/", { body })));
|
|
1305
|
+
}
|
|
1306
|
+
if (noun === "guardians" && verb === "invite") {
|
|
1307
|
+
const familyId = flagString(parsed, "family", { required: true });
|
|
1308
|
+
const email = flagString(parsed, "email", { required: true })
|
|
1309
|
+
.trim()
|
|
1310
|
+
.toLowerCase();
|
|
1311
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
1312
|
+
throw new CliError("invalid_arguments", "--email does not look like a valid email address.");
|
|
1313
|
+
}
|
|
1314
|
+
const firstName = flagString(parsed, "first-name", { required: true });
|
|
1315
|
+
const lastName = flagString(parsed, "last-name");
|
|
1316
|
+
const sendInvite = !hasFlag(parsed, "no-invite");
|
|
1317
|
+
// Read-only preflight: the approval preview must name the exact family
|
|
1318
|
+
// this guardian is being granted over — a wrong family id hands a
|
|
1319
|
+
// stranger full guardian access to someone's kids.
|
|
1320
|
+
const family = unwrap(await api.client.GET("/admin/onboarding/families/{familyId}/status", {
|
|
1321
|
+
params: { path: { familyId } },
|
|
1322
|
+
}));
|
|
1323
|
+
const body = {
|
|
1324
|
+
email,
|
|
1325
|
+
firstName,
|
|
1326
|
+
...(lastName !== undefined ? { lastName } : {}),
|
|
1327
|
+
sendInvite,
|
|
1328
|
+
};
|
|
1329
|
+
return writeCommand(parsed, {
|
|
1330
|
+
action: sendInvite
|
|
1331
|
+
? "invite a new GUARDIAN into an existing family and email the sign-in invite (outward email)"
|
|
1332
|
+
: "add a new GUARDIAN to an existing family without sending the invite email",
|
|
1333
|
+
target: { familyId, email },
|
|
1334
|
+
request: body,
|
|
1335
|
+
details: {
|
|
1336
|
+
familyName: family.familyName,
|
|
1337
|
+
kids: family.kids.map((kid) => kid.firstName),
|
|
1338
|
+
},
|
|
1339
|
+
}, async () => unwrap(await api.client.POST("/admin/users/family/{familyId}/guardians/", {
|
|
1340
|
+
params: { path: { familyId } },
|
|
1341
|
+
body,
|
|
1342
|
+
})));
|
|
1343
|
+
}
|
|
1344
|
+
if (noun === "guides" && verb === "invite") {
|
|
1345
|
+
const userId = flagString(parsed, "user", { required: true });
|
|
1346
|
+
// Read-only preflight: the approval preview must name the exact person
|
|
1347
|
+
// about to receive an outward email, and refuse a non-guide before the
|
|
1348
|
+
// gate rather than after approval (the backend enforces the same rule).
|
|
1349
|
+
const { user } = unwrap(await api.client.GET("/admin/users/{userId}", {
|
|
1350
|
+
params: { path: { userId } },
|
|
1351
|
+
}));
|
|
1352
|
+
if (user.role !== "GUIDE") {
|
|
1353
|
+
throw new CliError("invalid_arguments", `User ${userId} has role ${user.role}; only GUIDE accounts can be invited. Use guides create for a new guide.`);
|
|
1354
|
+
}
|
|
1355
|
+
if (!user.email) {
|
|
1356
|
+
throw new CliError("invalid_arguments", `Guide ${userId} has no email address on file, so no invite can be sent.`);
|
|
1357
|
+
}
|
|
1358
|
+
return writeCommand(parsed, {
|
|
1359
|
+
action: "send the guide sign-in invite email (outward email)",
|
|
1360
|
+
target: { userId },
|
|
1361
|
+
request: {},
|
|
1362
|
+
details: {
|
|
1363
|
+
to: user.email,
|
|
1364
|
+
firstName: user.firstName,
|
|
1365
|
+
lastName: user.lastName,
|
|
1366
|
+
role: user.role,
|
|
1367
|
+
},
|
|
1368
|
+
}, async () => unwrap(await api.client.POST("/admin/guides/{userId}/invite/", {
|
|
1369
|
+
params: { path: { userId } },
|
|
1370
|
+
})));
|
|
1371
|
+
}
|
|
1153
1372
|
if (noun === "students" && verb === "upload-map-scores") {
|
|
1154
1373
|
const studentId = flagString(parsed, "student", { required: true });
|
|
1155
1374
|
const pdf = await readMapScorePdf(flagString(parsed, "file", { required: true }));
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: recess-cli
|
|
3
|
-
description: Safely perform Recess staff administration through the recess CLI. Use when a Recess admin asks Codex to find a kid, parent, family, enrollment, subscription, invoice, or cohort; search the curated Content Library; inspect or change a school kid's tier and capability gates; upload a kid's MAP Growth report; pause or resume billing; refund or credit an invoice item; extend a trial; cancel or restore a subscription; register or unregister a cohort against an enrollment; switch or move kids from one cohort to another; manage guide payout invoices (biweekly pay-cycle line-item changes, invoice status moves, payout recipient lookups); run class operations (take attendance, cancel or reschedule a class session, add a one-off session, end a cohort, pause cohort billing, email a cohort's families, approve or deny pending registrations); process class-cancellation credits (the "Please credit these students accordingly" Slack message — credit every registered kid for a guide-canceled session); or author learning content — build, validate, publish, and loss-safely patch a deterministic GoalTemplate, apply a template to a kid or a roster, create a goal directly on a kid, write a Mesa draft or goal workspace, capture a template snapshot, and read Mesa workspace or snapshot files.
|
|
3
|
+
description: Safely perform Recess staff administration through the recess CLI. Use when a Recess admin asks Codex to find a kid, parent, family, enrollment, subscription, invoice, or cohort; provision a new GUIDE staff account and email its sign-in invite (or resend one); edit the Recess Village map or manage store-catalog availability; search the curated Content Library; inspect or change a school kid's tier and capability gates; upload a kid's MAP Growth report; pause or resume billing; refund or credit an invoice item; extend a trial; cancel or restore a subscription; register or unregister a cohort against an enrollment; switch or move kids from one cohort to another; manage guide payout invoices (biweekly pay-cycle line-item changes, invoice status moves, payout recipient lookups); run class operations (take attendance, cancel or reschedule a class session, add a one-off session, end a cohort, pause cohort billing, email a cohort's families, approve or deny pending registrations); process class-cancellation credits (the "Please credit these students accordingly" Slack message — credit every registered kid for a guide-canceled session); or author learning content — build, validate, publish, and loss-safely patch a deterministic GoalTemplate, apply a template to a kid or a roster, create a goal directly on a kid, write a Mesa draft or goal workspace, capture a template snapshot, and read Mesa workspace or snapshot files.
|
|
4
4
|
# Bundle version. Bump on every substantive edit; the CLI reports it and `doctor`
|
|
5
5
|
# compares it against the served copy to tell an operator a refresh is available.
|
|
6
|
-
version: 1.
|
|
6
|
+
version: 1.5.0
|
|
7
7
|
# The lowest `recess` version this bundle is safe to install onto. Raise it ONLY
|
|
8
8
|
# when the bundle documents a command, flag, or changed semantic that an older
|
|
9
9
|
# binary does not have — an older CLI keeps its bundled copy instead of taking
|
|
10
10
|
# this one. Prose, formatting, and Gotcha edits must NOT raise it; that is the
|
|
11
11
|
# whole point of serving the bundle.
|
|
12
|
-
minCliVersion: 1.
|
|
12
|
+
minCliVersion: 1.4.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
# Recess CLI (`recess`)
|
|
@@ -51,6 +51,9 @@ Never infer approval from the original task, prior approval, urgency, or a succe
|
|
|
51
51
|
| `invoices refund … --who-pays recess` | Recess absorbs the cost instead of the guide |
|
|
52
52
|
| `cohorts end --cancel-subscriptions` | sets EVERY active enrollment's Stripe subscription to cancel at period end |
|
|
53
53
|
| `cohorts email` | real outward email blast to families (include the recipient count from `cohorts parent-emails`) |
|
|
54
|
+
| `guides create` | mints a staff-privileged GUIDE account (the `/admin` gate admits GUIDE) and by default emails the sign-in invite; quote the exact email address — the address IS the credential handle for passwordless login |
|
|
55
|
+
| `guides invite` | real outward email to the previewed recipient telling them their account is live; quote `details.to` |
|
|
56
|
+
| `guardians invite` | grants a new email address FULL guardian access over an existing family's kids and by default emails the sign-in invite; quote the previewed `details.familyName` + kid list and the exact email address |
|
|
54
57
|
| `events cancel` | family-facing fan-out: chat messages, parent email blast, credit-owed notes, Slack |
|
|
55
58
|
| `payout invoices set-status` to `OPEN`/`PAID`/`CANCELED` | moves real account balances; `--send-email` additionally emails the guide |
|
|
56
59
|
| destructive `goal-templates patch-spec` | removes subjects, recipes, plans, queue items, source URLs, or missing-coverage records from a global template; quote the exact `details.safety.removed` inventory and hashes, then use the fresh token only after approval |
|
|
@@ -155,6 +158,14 @@ recess --json users tier set <kid-id> --tier <id> [--slots N] \
|
|
|
155
158
|
recess --json enrollments list --user <user-id>
|
|
156
159
|
recess --json enrollments get-for-subscription --subscription <id>
|
|
157
160
|
|
|
161
|
+
# Guide staff accounts (ADMIN-only; both send outward email by default)
|
|
162
|
+
recess --json guides create --email <email> --first-name TEXT [--last-name TEXT] [--no-invite] [--confirm]
|
|
163
|
+
recess --json guides invite --user <user-id> [--confirm]
|
|
164
|
+
|
|
165
|
+
# Invite a guardian into an EXISTING family (ADMIN-only; outward email by default;
|
|
166
|
+
# the preview names the family + kids — verify them before requesting approval)
|
|
167
|
+
recess --json guardians invite --family <family-id> --email <email> --first-name TEXT [--last-name TEXT] [--no-invite] [--confirm]
|
|
168
|
+
|
|
158
169
|
# Billing & subscriptions (reference/billing.md)
|
|
159
170
|
recess --json subscriptions list --family <family-id> [--kid <kid-id>]
|
|
160
171
|
recess --json invoices list --subscription <subscription-id>
|
|
@@ -209,6 +220,19 @@ recess --json onboarding attest <family-id> --condition app_downloaded|tutor_met
|
|
|
209
220
|
recess --json onboarding set-intake <family-id> --session <id> --data <json> [--expected-updated-at <iso>] [--confirm]
|
|
210
221
|
recess --json onboarding extract <family-id> --session <id> (--transcript-file <path> | --granola <ref>) [--confirm]
|
|
211
222
|
|
|
223
|
+
# Recess Village map administration
|
|
224
|
+
recess --json village render --min-x N --min-z N --max-x N --max-z N [--world village-1]
|
|
225
|
+
recess --json village models list [--world village-1] [--query TEXT] [--archived]
|
|
226
|
+
recess --json village models upload --file <model.glb> [--world village-1] [--name TEXT] [--confirm]
|
|
227
|
+
recess --json village models publish|archive <model-id> [--world village-1] [--confirm]
|
|
228
|
+
recess --json village models place <model-id> --x N --z N [--y N] [--rotation 0..3] [--world village-1] [--confirm]
|
|
229
|
+
recess --json village models move <placement-id> --x N --z N [--y N] [--rotation 0..3] [--world village-1] [--confirm]
|
|
230
|
+
recess --json village models remove <placement-id> [--world village-1] [--confirm]
|
|
231
|
+
|
|
232
|
+
# Main Recess store catalog (status writes are restricted to Village goods)
|
|
233
|
+
recess --json store-items list [--search TEXT] [--status ACTIVE|INACTIVE|COMING_SOON] [--item-type TYPE]
|
|
234
|
+
recess --json store-items set-status <village-store-item-id> --status ACTIVE|INACTIVE|COMING_SOON [--confirm]
|
|
235
|
+
|
|
212
236
|
# Tutor skills — the authoring know-how (served, never bundled)
|
|
213
237
|
recess --json skills list [--query TEXT] [--category TEXT]
|
|
214
238
|
recess --json skills get <skill-name> [--reference NAME | --all-references] [--refresh]
|
|
@@ -486,3 +510,5 @@ Dated, newest last. Add an entry every time reality surprises you.
|
|
|
486
510
|
- 2026-08-05 — **The goal-audit GET cannot investigate an already-soft-deleted goal.** `request get /admin/browser/students/goals/<goal-id>/audit/` returns 404 because the handler's `ensureGoalAccess` calls `canManageGoal`, which requires `Goal.deletedAt: null` before it loads `GoalAuditLog`. The 404 is not evidence that the audit row is absent. Use the `query-db` skill's guarded read-only production query for deletion forensics.
|
|
487
511
|
- 2026-08-05 — **`set-metadata --output-template-file` edits an AI_CHAT template's `outputTemplate`** (the goal-description payload DailyTodoGeneration consumes for description-only goals). Like `--agent-instructions-file`, it reads a local file and the server bumps the version + freezes a `GoalTemplateVersion` row. Editing a template never rewrites goals already created from it — the description was copied at goal-creation time; re-apply or edit live goals separately.
|
|
488
512
|
- 2026-08-05 — **Content Library search now has a content/Pipeline-read-only CLI command.** `content-library search <query> [--limit 8]` calls the same hybrid/vector + rerank `/agent/search` funnel as Rocky's `search_gem_library`, returns the full fit/gist/coverage payload, and attributes the funnel's standard `offered` telemetry to `admin-cli`. It cannot inspect, ingest, file requests, curate, or edit the Pipeline; the permanent library token stays on the web server.
|
|
513
|
+
- 2026-08-07 — **Village store goods do not live in the Village database.** They are ordinary main-Recess `StoreItem` rows with `itemType=VILLAGE_ITEM`; use `store-items list --item-type VILLAGE_ITEM` and the confirmation-gated `store-items set-status`, while `village models …` remains the separate Village-island map/model boundary. `set-status` refuses non-Village IDs and its live preflight names the item, current status, purchase count, and feed consequence before approval. A checked-in seed still owns fresh-database defaults, so an operational status edit does not replace updating that seed when the product default itself changes.
|
|
514
|
+
- 2026-08-11 — **An external bundle leaf does not infer its grade answer from `availableWhen`.** When an `EXTERNAL_SKILL_QUEUE_SETUP` recipe has grade-specific plans and the shared wizard step is not named `grade`, set `handler.config.gradeLevelStepKey` to that exact step key. The server preview can validate without this optional binding, but apply dry-run then returns `No plan matched subject="…"`. Compare the new leaf with a working grade-bound sibling and dry-run the exact answers before calling the template finished.
|