recess-cli 2.9.0 → 3.0.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 CHANGED
@@ -9,6 +9,13 @@ authenticated Village home building. It uses the web-server OpenAPI document, au
9
9
  through Recess SSO, emits stable JSON, and refuses live writes until the exact command is rerun with
10
10
  `--confirm` plus the preview's operation key after human approval.
11
11
 
12
+ ## Version 3 changes
13
+
14
+ `goals list` now shows ACTIVE and PAUSED goals by default. Add `--include-archived` to
15
+ include COMPLETED goals (both archived and rewarded completions), and `--query` to search.
16
+ Staff gain `cohorts schedule` and versioned `memories` file commands. Deploy their backend
17
+ and database support before publishing this CLI; writes require a server preview.
18
+
12
19
  ## Install (no checkout needed)
13
20
 
14
21
  Published to npm as [`recess-cli`](https://www.npmjs.com/package/recess-cli). On any machine with Node 20+:
package/dist/args.js CHANGED
@@ -19,6 +19,7 @@ const BOOLEAN_FLAGS = new Set([
19
19
  "help",
20
20
  "immediate",
21
21
  "include-deleted",
22
+ "include-archived",
22
23
  "mirrored",
23
24
  "no-invite",
24
25
  "no-collision",
package/dist/cli.js CHANGED
@@ -9,8 +9,10 @@ import { clearStoredSession, deleteProfile, listProfiles, resolveConfig, savePro
9
9
  import { agentContext, buildCommandSchema, findCommandSchema, remoteCommands, scopedHelp, validateInvocation, } from "./command-schema.js";
10
10
  import { runApplicationsCommand } from "./commands/applications.js";
11
11
  import { runAppsCommand } from "./commands/apps.js";
12
+ import { runChatLogsCommand } from "./commands/chat-logs.js";
12
13
  import { runMasteryCommand } from "./commands/mastery.js";
13
14
  import { runOnboardingCommand } from "./commands/onboarding.js";
15
+ import { runScheduleCommand, runMemoryFilesCommand, } from "./commands/safe-staff-operations.js";
14
16
  import { runSchoolCommand } from "./commands/school.js";
15
17
  import { runVillageEventsCommand } from "./commands/village-events.js";
16
18
  import { assertChoice, flagIdList, positional, readJsonFile, readJsonValue, } from "./commands/shared.js";
@@ -251,7 +253,10 @@ async function localWriteCommand(parsed, preview, execute) {
251
253
  action: preview.action,
252
254
  fingerprint,
253
255
  target: preview.target,
254
- ...(preview.action.startsWith("mastery.") ? { preview } : {}),
256
+ ...(preview.action.startsWith("mastery.") ||
257
+ preview.request.attendanceTakenAt
258
+ ? { preview }
259
+ : {}),
255
260
  });
256
261
  requireConfirmation(false, boundPreview);
257
262
  }
@@ -656,7 +661,12 @@ async function previewBoundWrite(parsed, preview, execute) {
656
661
  const result = await withIdempotencyContext(context, execute);
657
662
  await appendJobEvent({
658
663
  jobId: context.operationKey,
659
- status: "completed",
664
+ status: typeof result === "object" &&
665
+ result !== null &&
666
+ "outcome" in result &&
667
+ result.outcome === "outcome_unknown"
668
+ ? "unknown"
669
+ : "completed",
660
670
  timestamp: new Date().toISOString(),
661
671
  action: preview.action,
662
672
  fingerprint: context.fingerprint,
@@ -2676,6 +2686,14 @@ export async function executeRecessCommand(argv, options = {}) {
2676
2686
  if (noun === "school") {
2677
2687
  return runSchoolCommand({ parsed, api, writeCommand });
2678
2688
  }
2689
+ if (noun === "cohorts" && verb === "schedule")
2690
+ return runScheduleCommand({ parsed, api, writeCommand: previewBoundWrite });
2691
+ if (noun === "memories" && verb !== "context" && verb !== "log")
2692
+ return runMemoryFilesCommand({
2693
+ parsed,
2694
+ api,
2695
+ writeCommand: previewBoundWrite,
2696
+ });
2679
2697
  if (noun === "cohorts" && verb === "search") {
2680
2698
  const search = parsed.positionals.slice(2).join(" ").trim();
2681
2699
  if (!search)
@@ -3158,7 +3176,16 @@ export async function executeRecessCommand(argv, options = {}) {
3158
3176
  excused: true,
3159
3177
  })),
3160
3178
  ];
3161
- const body = { attendanceTakenAt: new Date().toISOString(), attendance };
3179
+ const operationKey = flagString(parsed, "operation-key");
3180
+ const stored = operationKey ? await getJob(operationKey) : undefined;
3181
+ const preparedAt = stored?.history.find((event) => event.preview?.request.attendanceTakenAt)?.preview?.request.attendanceTakenAt;
3182
+ if (hasFlag(parsed, "confirm") && typeof preparedAt !== "string") {
3183
+ throw new CliError("confirmation_required", "The saved attendance preview is unavailable. Request a new preview before confirming.");
3184
+ }
3185
+ const body = {
3186
+ attendanceTakenAt: typeof preparedAt === "string" ? preparedAt : new Date().toISOString(),
3187
+ attendance,
3188
+ };
3162
3189
  return writeCommand(parsed, {
3163
3190
  action: "take attendance (non-excused absentees may trigger automatic 'we missed you' emails to parents)",
3164
3191
  target: { eventId },
@@ -4047,9 +4074,21 @@ export async function executeRecessCommand(argv, options = {}) {
4047
4074
  }
4048
4075
  if (verb === "list") {
4049
4076
  const userId = flagString(parsed, "student", { required: true });
4050
- return unwrap(await api.client.GET("/tutor/browser/students/{userId}/goals/", {
4077
+ const result = unwrap(await api.client.GET("/tutor/browser/students/{userId}/goals/", {
4051
4078
  params: { path: { userId } },
4052
4079
  }));
4080
+ const query = flagString(parsed, "query")?.toLocaleLowerCase();
4081
+ return {
4082
+ ...result,
4083
+ goals: result.goals.filter((goal) => (goal.status === "ACTIVE" ||
4084
+ goal.status === "PAUSED" ||
4085
+ (hasFlag(parsed, "include-archived") &&
4086
+ goal.status === "COMPLETED")) &&
4087
+ (!query ||
4088
+ `${goal.title}\n${goal.description ?? ""}`
4089
+ .toLocaleLowerCase()
4090
+ .includes(query))),
4091
+ };
4053
4092
  }
4054
4093
  if (verb === "create") {
4055
4094
  const requestedStudentId = flagString(parsed, "student");
@@ -4400,6 +4439,9 @@ export async function executeRecessCommand(argv, options = {}) {
4400
4439
  }
4401
4440
  throw new CliError("invalid_arguments", "Use goals list|create|edit|delete|restore|archive|unarchive|complete|undo-completion|queue|files|pdf.");
4402
4441
  }
4442
+ if (noun === "chat-logs") {
4443
+ return runChatLogsCommand({ parsed, api, writeCommand });
4444
+ }
4403
4445
  if (noun === "students") {
4404
4446
  if (verb === "list") {
4405
4447
  const requestedScope = flagString(parsed, "scope");
@@ -22,6 +22,7 @@ const BOOLEAN_FLAGS = new Set([
22
22
  "immediate",
23
23
  "keep-tokens",
24
24
  "include-deleted",
25
+ "include-archived",
25
26
  "json",
26
27
  "mirrored",
27
28
  "no-collision",
@@ -138,6 +139,9 @@ const AUTHENTICATED_COMMAND_PREFIXES = [
138
139
  "village render",
139
140
  ];
140
141
  const FAMILY_AI_COMMANDS = new Set([
142
+ "chat-logs by-todo",
143
+ "chat-logs get",
144
+ "chat-logs list",
141
145
  "content-library search",
142
146
  "goal-templates apply",
143
147
  "goal-templates apply-starter",
@@ -177,6 +181,16 @@ const FAMILY_AI_COMMANDS = new Set([
177
181
  "todos edit",
178
182
  ]);
179
183
  const STAFF_COMMANDS = new Set([
184
+ "cohorts schedule get",
185
+ "cohorts schedule set",
186
+ "cohorts schedule changes",
187
+ "cohorts schedule change",
188
+ "memories files",
189
+ "memories read",
190
+ "memories edit",
191
+ "memories history",
192
+ "memories revision",
193
+ "memories restore",
180
194
  "apps assign",
181
195
  "apps list",
182
196
  "apps preview",
@@ -0,0 +1,43 @@
1
+ import { unwrap } from "../api.js";
2
+ import { flagNumber, flagString } from "../args.js";
3
+ import { CliError } from "../errors.js";
4
+ import { positional } from "./shared.js";
5
+ export async function runChatLogsCommand({ parsed, api, }) {
6
+ const verb = parsed.positionals[1];
7
+ if (verb === "list") {
8
+ const limit = flagNumber(parsed, "limit");
9
+ if (limit !== undefined &&
10
+ (!Number.isInteger(limit) || limit < 1 || limit > 500)) {
11
+ throw new CliError("invalid_arguments", "--limit must be an integer from 1 to 500.");
12
+ }
13
+ return unwrap(await api.client.GET("/ai/chat-logs", {
14
+ params: {
15
+ query: {
16
+ studentId: flagString(parsed, "student", { required: true }),
17
+ limit,
18
+ cursor: flagString(parsed, "cursor"),
19
+ },
20
+ },
21
+ }));
22
+ }
23
+ const query = verb === "get"
24
+ ? { conversationId: positional(parsed, 2, "conversation ID") }
25
+ : { todoId: positional(parsed, 2, "todo ID") };
26
+ const messages = [];
27
+ let cursor;
28
+ const seenCursors = new Set();
29
+ do {
30
+ const page = unwrap(await api.client.GET("/ai/chat-logs/messages", {
31
+ params: { query: { ...query, limit: 500, cursor } },
32
+ }));
33
+ messages.push(...page.messages);
34
+ if (!page.nextCursor)
35
+ return { ...page, messages, complete: true };
36
+ if (seenCursors.has(page.nextCursor)) {
37
+ throw new CliError("pagination_error", "Chat-log pagination did not advance; no complete download was produced.");
38
+ }
39
+ seenCursors.add(page.nextCursor);
40
+ cursor = page.nextCursor;
41
+ } while (cursor);
42
+ }
43
+ //# sourceMappingURL=chat-logs.js.map
@@ -0,0 +1,183 @@
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, hasFlag } from "../args.js";
6
+ import { CliError } from "../errors.js";
7
+ import { appendJobEvent } from "../jobs.js";
8
+ import { assertChoice, positional } from "./shared.js";
9
+ const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
10
+ function recoveryKey(ctx) {
11
+ if (!hasFlag(ctx.parsed, "confirm"))
12
+ return undefined;
13
+ const key = flagString(ctx.parsed, "operation-key");
14
+ const token = flagString(ctx.parsed, "approval-token");
15
+ if (key && (!token || !key.endsWith(`_${token}`)))
16
+ throw new CliError("approval_mismatch", "Recovery requires the original operation key and approval token.");
17
+ return key;
18
+ }
19
+ async function recovered(ctx, row, expectedInputHash) {
20
+ if (row.inputHash !== expectedInputHash)
21
+ throw new CliError("approval_mismatch", "This operation key belongs to different input. No operation was retried.");
22
+ await appendJobEvent({
23
+ jobId: row.operationKey,
24
+ status: row.outcome === "outcome_unknown" ? "unknown" : "completed",
25
+ timestamp: new Date().toISOString(),
26
+ action: "recover staff operation",
27
+ fingerprint: flagString(ctx.parsed, "approval-token"),
28
+ target: { apiOrigin: ctx.api.config.apiOrigin },
29
+ result: row,
30
+ });
31
+ return row;
32
+ }
33
+ export async function runScheduleCommand(ctx) {
34
+ const { api, parsed, writeCommand } = ctx;
35
+ const action = positional(parsed, 2, "schedule action");
36
+ const id = positional(parsed, 3, "cohort or change ID");
37
+ if (action === "change")
38
+ return unwrap(await api.client.GET("/admin/cohorts/schedule-changes/{changeId}", {
39
+ params: { path: { changeId: id } },
40
+ }));
41
+ const params = { path: { id } };
42
+ if (action === "get")
43
+ return unwrap(await api.client.GET("/admin/cohorts/{id}/schedule/", { params }));
44
+ if (action === "changes")
45
+ return unwrap(await api.client.GET("/admin/cohorts/{id}/schedule/changes", {
46
+ params: { ...params, query: { cursor: flagString(parsed, "cursor") } },
47
+ }));
48
+ if (action !== "set")
49
+ throw new CliError("invalid_arguments", "Use cohorts schedule get|set|changes|change.");
50
+ const input = {
51
+ days: flagString(parsed, "days", { required: true })
52
+ .split(",")
53
+ .map((d) => assertChoice(d.trim().toUpperCase(), ["MO", "TU", "WE", "TH", "FR", "SA", "SU"], "weekday")),
54
+ time: flagString(parsed, "time", { required: true }),
55
+ timezone: flagString(parsed, "timezone"),
56
+ effectiveDate: flagString(parsed, "effective-date", { required: true }),
57
+ notify: assertChoice(flagString(parsed, "notify", { required: true }), ["none", "parents"], "notify"),
58
+ reason: flagString(parsed, "reason", { required: true }).trim(),
59
+ };
60
+ const key = recoveryKey(ctx);
61
+ if (key) {
62
+ const history = unwrap(await api.client.GET("/admin/cohorts/{id}/schedule/changes", {
63
+ params: { ...params, query: { operationKey: key } },
64
+ }));
65
+ if (history.changes[0])
66
+ return recovered(ctx, history.changes[0], hash({ id, input }));
67
+ }
68
+ const capability = unwrap(await api.client.GET("/admin/cohorts/{id}/schedule/", { params }));
69
+ if (capability.capabilityVersion !== 1 || !capability.supported)
70
+ throw new CliError("unsupported_backend", capability.unsupportedReason ??
71
+ "Backend does not support safe schedule changes.");
72
+ const preview = unwrap(await api.client.POST("/admin/cohorts/{id}/schedule/preview", {
73
+ params,
74
+ body: input,
75
+ }));
76
+ return writeCommand(parsed, {
77
+ action: "change recurring cohort schedule",
78
+ target: { cohortId: id, apiOrigin: api.config.apiOrigin },
79
+ request: input,
80
+ details: preview,
81
+ }, async () => {
82
+ const result = unwrap(await api.client.POST("/admin/cohorts/{id}/schedule/apply", {
83
+ params,
84
+ body: {
85
+ ...input,
86
+ operationKey: flagString(parsed, "operation-key", {
87
+ required: true,
88
+ }),
89
+ fingerprint: preview.fingerprint,
90
+ },
91
+ }));
92
+ return unwrap(await api.client.GET("/admin/cohorts/schedule-changes/{changeId}", {
93
+ params: { path: { changeId: result.id } },
94
+ }));
95
+ });
96
+ }
97
+ export async function runMemoryFilesCommand(ctx) {
98
+ const { parsed, api, writeCommand } = ctx;
99
+ const action = positional(parsed, 1, "memory action");
100
+ const studentId = flagString(parsed, "student", { required: true });
101
+ const params = { path: { studentId } };
102
+ if (action === "files")
103
+ return unwrap(await api.client.GET("/tutor/students/{studentId}/memory-files", {
104
+ params,
105
+ }));
106
+ const memoryPath = flagString(parsed, "path", { required: true });
107
+ if (action === "read")
108
+ return unwrap(await api.client.GET("/tutor/students/{studentId}/memory-files/read", {
109
+ params: { ...params, query: { path: memoryPath } },
110
+ }));
111
+ if (action === "history")
112
+ return unwrap(await api.client.GET("/tutor/students/{studentId}/memory-files/history", {
113
+ params: {
114
+ ...params,
115
+ query: { path: memoryPath, cursor: flagString(parsed, "cursor") },
116
+ },
117
+ }));
118
+ if (action === "revision")
119
+ return unwrap(await api.client.GET("/tutor/students/{studentId}/memory-files/revision", {
120
+ params: {
121
+ ...params,
122
+ query: {
123
+ path: memoryPath,
124
+ revision: flagString(parsed, "revision", { required: true }),
125
+ },
126
+ },
127
+ }));
128
+ if (action !== "edit" && action !== "restore")
129
+ throw new CliError("invalid_arguments", "Use memories files|read|edit|history|revision|restore.");
130
+ let content;
131
+ let contentFile;
132
+ if (action === "edit") {
133
+ contentFile = path.resolve(flagString(parsed, "content-file", { required: true }));
134
+ const info = await fs.lstat(contentFile);
135
+ if (!info.isFile() ||
136
+ info.isSymbolicLink() ||
137
+ (await fs.realpath(contentFile)) !== contentFile)
138
+ throw new CliError("invalid_file", "Memory input must be a regular file without symlink components.");
139
+ content = new TextDecoder("utf-8", { fatal: true }).decode(await fs.readFile(contentFile));
140
+ }
141
+ const input = {
142
+ path: memoryPath,
143
+ content,
144
+ revision: action === "restore"
145
+ ? flagString(parsed, "revision", { required: true })
146
+ : undefined,
147
+ action,
148
+ reason: flagString(parsed, "reason", { required: true }).trim(),
149
+ };
150
+ const key = recoveryKey(ctx);
151
+ if (key) {
152
+ const result = unwrap(await api.client.GET("/tutor/students/{studentId}/memory-files/operation", { params: { ...params, query: { operationKey: key } } }));
153
+ if (result.operation)
154
+ return recovered(ctx, result.operation, hash({ studentId, input }));
155
+ }
156
+ const preview = unwrap(await api.client.POST("/tutor/students/{studentId}/memory-files/preview", {
157
+ params,
158
+ body: input,
159
+ }));
160
+ if (preview.capabilityVersion !== 1 || preview.writeSupported !== true)
161
+ throw new CliError("unsupported_backend", preview.writeUnsupportedReason ??
162
+ "Backend does not support safe staff memory edits and restores.");
163
+ return writeCommand(parsed, {
164
+ action: `${action} student memory file`,
165
+ target: { studentId, path: memoryPath, apiOrigin: api.config.apiOrigin },
166
+ request: { ...input, contentFile },
167
+ details: {
168
+ ...preview,
169
+ before: { ...preview.before, revision: undefined },
170
+ },
171
+ }, async () => unwrap(await api.client.POST("/tutor/students/{studentId}/memory-files/apply", {
172
+ params,
173
+ body: {
174
+ ...input,
175
+ expectedHash: preview.before.contentSha,
176
+ fingerprint: preview.fingerprint,
177
+ operationKey: flagString(parsed, "operation-key", {
178
+ required: true,
179
+ }),
180
+ },
181
+ })));
182
+ }
183
+ //# sourceMappingURL=safe-staff-operations.js.map
package/dist/help.js CHANGED
@@ -51,6 +51,9 @@ Usage:
51
51
  --first-name TEXT [--last-name TEXT] [--no-invite] [--confirm]
52
52
  recess [--json] students upload-map-scores --student <kid-id>
53
53
  --file </path/to/map-report.pdf> [--confirm]
54
+ recess [--json] chat-logs list --student <user-id> [--limit 100] [--cursor <cursor>]
55
+ recess [--json] chat-logs get <conversation-id>
56
+ recess [--json] chat-logs by-todo <todo-id>
54
57
  recess [--json] students list [--scope mine|family]
55
58
  recess [--json] students today --student <kid-id> [--date YYYY-MM-DD]
56
59
  recess [--json] students schedule --student <kid-id> [--days 14]
@@ -140,6 +143,10 @@ Usage:
140
143
  [--description TEXT] [--date YYYY-MM-DD] [--confirm]
141
144
  recess [--json] payout items delete <item-id> [--confirm]
142
145
  recess [--json] cohorts get <cohort-id> [--events-tab ACTIVE|ENDED|CANCELED|ARCHIVED]
146
+ recess [--json] cohorts schedule get <cohort-id>
147
+ recess [--json] cohorts schedule set <cohort-id> --days <MO,WE> --time <09:30> [--timezone <zone>] --effective-date <YYYY-MM-DD> --notify none|parents [--confirm] [--approval-token <token>]
148
+ recess [--json] cohorts schedule changes <cohort-id> [--cursor <cursor>]
149
+ recess [--json] cohorts schedule change <change-id>
143
150
  recess [--json] cohorts parent-emails <cohort-id>
144
151
  recess [--json] cohorts end <cohort-id> [--cancel-subscriptions] [--confirm]
145
152
  recess [--json] cohorts pause-billing <cohort-id> --weeks 1..6 [--confirm]
@@ -357,7 +364,7 @@ Usage:
357
364
  [--dry-run] [--confirm --approval-token TOKEN]
358
365
  recess [--json] goal-templates apply-starter <template-id> --student <kid-id>
359
366
  [--answers-file <path>] [--confirm]
360
- recess [--json] goals list --student <kid-id>
367
+ recess [--json] goals list --student <kid-id> [--query <text>] [--include-archived]
361
368
  recess [--json] goals create [--student <kid-id>] --title TEXT
362
369
  (--description TEXT | --description-file <path>) [--target-date <iso>]
363
370
  [--schedule TEXT] [(--draft <draft-slug> | --source-dir <local-checkout>)
@@ -392,6 +399,14 @@ Usage:
392
399
  recess [--json] todos generate-learning-analysis <todo-id> [--confirm]
393
400
  recess [--json] todos generate-applet <todo-id> --student <kid-id>
394
401
  [--due-date YYYY-MM-DD] [--confirm --approval-token TOKEN]
402
+ recess [--json] memories files --student <kid-id>
403
+ recess [--json] memories read --student <kid-id> --path <path>
404
+ recess [--json] memories edit --student <kid-id> --path <path> --content-file <file> [--confirm] [--approval-token <token>]
405
+ recess [--json] memories history --student <kid-id> --path <path> [--cursor <cursor>]
406
+ recess [--json] memories revision --student <kid-id> --path <path> --revision <revision>
407
+ recess [--json] memories restore --student <kid-id> --path <path> --revision <revision> [--confirm] [--approval-token <token>]
408
+ Staff memory edit/restore publication is disabled pending recoverable immutable history.
409
+ Reads, history, and recovery of existing operations remain available.
395
410
  recess [--json] memories context --student <kid-id>
396
411
  recess [--json] memories log --student <kid-id> --date YYYY-MM-DD
397
412
  recess [--json] rocky get --student <kid-id>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recess-cli",
3
- "version": "2.9.0",
3
+ "version": "3.0.0",
4
4
  "description": "Safe Recess administration and family AI tools from the command line.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -72,18 +72,21 @@ Any changed target, payload, local file, server revision, amount, recipient, or
72
72
 
73
73
  ### Hosted MCP transport
74
74
 
75
- When this skill is reached through the hosted Recess connector, use its only tool,
76
- `recess_exec({argv,input?})`. `argv` starts with the command noun, omits both `recess` and `--json`,
77
- and still carries `--reason`. The CLI envelope is returned in MCP `structuredContent`; a
78
- `confirmation_required` envelope is expected control flow, while other failures are tool errors.
79
- Start with `argv:["agent-context"]` or `argv:["help","apps"]` for the remote-only catalog. OAuth
80
- already supplies the live user and scopes: never ask for cookies, profiles, local paths, or
81
- `--deliver`.
82
-
83
- Hosted writes stage the exact argv and structured payload on the server. Preview without
84
- `--confirm`, show the human `error.details.preview`, then confirm with the same command path plus
85
- `--confirm --operation-key <key>` and **omit `input`**. The server executes the stored bytes; changed
86
- source, flags, project/version, or operation keys require a fresh preview.
75
+ Use the connector's individually named tools and their discovered argument schemas, for example
76
+ `recess_students_todos({student:"…",reason:"…"})`. Discovery replaces CLI help. OAuth supplies the
77
+ live user and consented scopes; never ask for cookies, local paths or profiles.
78
+
79
+ Preview writes with named arguments and `reason`, show `error.details.preview`, then confirm
80
+ through the same tool with **only** `{confirm:true,operationKey:"…"}`. Stored arguments and source
81
+ bytes are restored server-side. Changed input or consequences require a new preview. Retain the
82
+ key on interruption and inspect `recess_jobs_get`; never start a replacement for an uncertain
83
+ write. `confirmation_required` is normal control flow in the returned `structuredContent`.
84
+
85
+ Supply JSON/text directly, workspaces as file bundles with `baseRevision`, and binaries as
86
+ `{name,content,encoding}` or scoped `{uploadHandle}` references created by `recess_upload_create`.
87
+ Upload bytes to its signed URL(s); size/hash verification happens before use. Download returned
88
+ bundles/authorized URLs. `/mcp/legacy` temporarily retains `recess_exec({command,input?})` for
89
+ 30 days after rollout. Refresh connector metadata and consent for expanded capabilities.
87
90
 
88
91
  ## Building Studio apps (`recess apps`)
89
92
 
@@ -106,20 +109,16 @@ prompts a model for them.
106
109
  - `recess --json apps standards "<words or notation>"` finds the CCSS standard for `manifest.json`
107
110
  (`"grade 4 adding fractions"` → `4.NF.B.3a`…); guides rarely know the codes, look them up.
108
111
 
109
- Hosted authoring keeps those same contracts but carries no folder:
110
-
111
- - `apps scaffold [--for-todo <id>]` returns authoring instructions, the five editable starter
112
- files, fork guidance, and an optional learner brief. It performs no write.
113
- - Generate `input.appBundle` with exactly `skill.js`, `model.js`, `model.css`,
114
- `params.schema.json`, and `params.json`, plus `contract` and `manifest`. Preview and confirm
115
- `apps validate`; keep the returned project/version/build IDs and poll `apps status <build-id>`.
116
- - Fix findings in the bundle. For an existing project, first `apps pull <project-id>` and preserve
117
- its `baseVersion`; a stale version is a conflict, never an overwrite.
118
- - `apps preview <project-id> --version <n>` returns the short-lived capability URL.
119
- Preview/confirm `apps publish <project-id> --version <n>`, then poll its build. Publication sends
120
- no source and reviews exactly that validated version.
121
- - After publication, preview/confirm `apps assign <project-id> --student <id> [--due YYYY-MM-DD]`.
122
- Assignment is intentionally separate from hosted publish.
112
+ Hosted authoring keeps those same contracts with named tools:
113
+
114
+ - `recess_apps_scaffold` returns instructions, editable files, and an optional `forTodo` brief.
115
+ - Supply `appBundle` (files, contract, manifest) to `recess_apps_validate`; preview and confirm,
116
+ then keep its project/version/build IDs and poll `recess_apps_status`.
117
+ - Before editing an existing project, call `recess_apps_pull` and preserve its `baseVersion`.
118
+ - `recess_apps_preview` previews creation of a temporary capability URL. Preview and confirm
119
+ `recess_apps_publish` for the exact project/version; it reviews that validated version.
120
+ - Use `assign` and optional `due` on publication for recoverable publish-and-assign, or use
121
+ `recess_apps_assign` separately. A pending build retains the same operation key for retry.
123
122
 
124
123
  Two ways in. If the guide names a kid or a todo, pull the analysis first and build against it;
125
124
  otherwise build from their description:
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "2.6.0",
3
- "minCliVersion": "2.6.1"
2
+ "version": "3.0.0",
3
+ "minCliVersion": "3.0.0"
4
4
  }