recess-cli 3.1.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.
- package/README.md +7 -1
- package/dist/args.js +1 -0
- package/dist/auth.js +8 -3
- package/dist/cli.js +285 -3
- package/dist/command-schema.js +20 -9
- package/dist/commands/apps.js +10 -1
- package/dist/help.js +33 -7
- package/package.json +1 -1
- package/skill/recess-cli/SKILL.md +3 -2
- package/skill/recess-cli/agents/version.json +2 -2
package/README.md
CHANGED
|
@@ -77,13 +77,19 @@ Create an approved `OAuthClient` row in each Recess environment. This remains a
|
|
|
77
77
|
|
|
78
78
|
The production row ID (`c7e34138-18f9-45b1-a2fb-26a4e3a6d739`) is the CLI's built-in default, so production login needs no client-ID setup. `--client-id`, `RECESS_CLI_OAUTH_CLIENT_ID`, and a stored client ID remain overrides for local/staging clients. The web-server decodes the assertion audience, loads that exact `OAuthClient`, and requires both `approved` and `adminCliEnabled`; there is no separate server environment allowlist. Production redirect validation is exact, so a different callback port must also be explicitly registered.
|
|
79
79
|
|
|
80
|
-
The browser SSO assertion is exchanged once and discarded. The CLI stores a separate
|
|
80
|
+
The browser SSO assertion is exchanged once and discarded. The CLI stores a separate signed Recess session (12 hours by default) at `~/.recess-cli/config.json` with mode `0600`. A guardian must hold the live `access:ai` permission; removing it immediately invalidates session checks. Guardian sessions cannot call `/admin` routes and every target is independently restricted to their family. A KID session has `village_home` scope: application-level and shared-auth fences permit only session inspection and Village assertion minting, denying every other backend route.
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
83
|
recess --json auth login
|
|
84
84
|
recess --json doctor --reason "Verify CLI connectivity, identity, and scope"
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
Sessions default to 12 hours. Admins can request 30 days locally with
|
|
88
|
+
`recess --json auth login --duration 30d`, or remotely with
|
|
89
|
+
`recess --json auth request --duration 30d --label "remote agent"`, approve the link as an
|
|
90
|
+
admin, then run `recess --json auth poll`. The approval page shows the requested duration;
|
|
91
|
+
the link still expires after 10 minutes. Non-admins cannot authorize 30-day sessions.
|
|
92
|
+
|
|
87
93
|
## Testing against a local server
|
|
88
94
|
|
|
89
95
|
`auth login` opens production SSO, so local iteration uses the env-cookie hatch instead:
|
package/dist/args.js
CHANGED
package/dist/auth.js
CHANGED
|
@@ -77,7 +77,11 @@ export async function login(config, options) {
|
|
|
77
77
|
const assertion = await callback;
|
|
78
78
|
const response = await fetch(new URL("/auth/admin-cli/exchange/", config.apiOrigin), {
|
|
79
79
|
method: "POST",
|
|
80
|
-
headers: cliRequestHeaders({
|
|
80
|
+
headers: cliRequestHeaders({
|
|
81
|
+
authorization: `Bearer ${assertion}`,
|
|
82
|
+
"content-type": "application/json",
|
|
83
|
+
}),
|
|
84
|
+
body: JSON.stringify({ sessionDuration: options.sessionDuration }),
|
|
81
85
|
});
|
|
82
86
|
const body = (await response.json());
|
|
83
87
|
if (!response.ok)
|
|
@@ -109,7 +113,7 @@ export async function requestDeviceAuth(config, options) {
|
|
|
109
113
|
const response = await fetch(new URL("/auth/admin-cli/device/authorize/", config.apiOrigin), {
|
|
110
114
|
method: "POST",
|
|
111
115
|
headers: cliRequestHeaders({ "content-type": "application/json" }),
|
|
112
|
-
body: JSON.stringify(options
|
|
116
|
+
body: JSON.stringify(options),
|
|
113
117
|
});
|
|
114
118
|
const body = (await response.json());
|
|
115
119
|
if (!response.ok)
|
|
@@ -128,7 +132,8 @@ export async function requestDeviceAuth(config, options) {
|
|
|
128
132
|
userCode: authorize.userCode,
|
|
129
133
|
expiresAt: authorize.expiresAt,
|
|
130
134
|
interval: authorize.interval,
|
|
131
|
-
|
|
135
|
+
sessionDuration: options.sessionDuration ?? "12h",
|
|
136
|
+
instructions: `Open ${authorize.approvalUrl} in a browser and approve it while signed in to Recess ${options.sessionDuration === "30d" ? "as an admin to authorize 30 days of access" : "as the person this agent should act as"} — the session takes on THAT account's scope, so a guardian grants family-only access. Then run \`recess auth poll\`.`,
|
|
132
137
|
};
|
|
133
138
|
}
|
|
134
139
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
package/dist/cli.js
CHANGED
|
@@ -659,6 +659,44 @@ async function requirePreviewBoundConfirmation(parsed, preview, persistPreview =
|
|
|
659
659
|
});
|
|
660
660
|
return { operationKey: suppliedOperationKey, fingerprint: approvalToken };
|
|
661
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* A tutor template's `body` is the `## Subjects` block markdown (no `###`
|
|
664
|
+
* heading — the server renders that from `title` + `cadence`).
|
|
665
|
+
*/
|
|
666
|
+
async function readTutorTemplateBody(filePath) {
|
|
667
|
+
const absolutePath = path.resolve(filePath);
|
|
668
|
+
const contents = await fs.readFile(absolutePath, "utf8").catch(() => null);
|
|
669
|
+
if (contents === null) {
|
|
670
|
+
throw new CliError("invalid_arguments", `--body-file is not readable: ${absolutePath}`);
|
|
671
|
+
}
|
|
672
|
+
if (contents.trim().length === 0) {
|
|
673
|
+
throw new CliError("invalid_arguments", "--body-file is empty.");
|
|
674
|
+
}
|
|
675
|
+
return contents;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Every `*.md` in the directory becomes one `lessons[name]` entry, referenced
|
|
679
|
+
* from a module line as `lesson:<slug>/<name>`.
|
|
680
|
+
*/
|
|
681
|
+
async function readTutorTemplateLessons(directory) {
|
|
682
|
+
const absolutePath = path.resolve(directory);
|
|
683
|
+
const entries = await fs
|
|
684
|
+
.readdir(absolutePath, { withFileTypes: true })
|
|
685
|
+
.catch(() => null);
|
|
686
|
+
if (entries === null) {
|
|
687
|
+
throw new CliError("invalid_arguments", `--lessons-dir is not a readable directory: ${absolutePath}`);
|
|
688
|
+
}
|
|
689
|
+
const lessons = {};
|
|
690
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
691
|
+
if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md"))
|
|
692
|
+
continue;
|
|
693
|
+
lessons[entry.name] = await fs.readFile(path.join(absolutePath, entry.name), "utf8");
|
|
694
|
+
}
|
|
695
|
+
if (Object.keys(lessons).length === 0) {
|
|
696
|
+
throw new CliError("invalid_arguments", `--lessons-dir contains no .md files: ${absolutePath}`);
|
|
697
|
+
}
|
|
698
|
+
return lessons;
|
|
699
|
+
}
|
|
662
700
|
async function previewBoundWrite(parsed, preview, execute, persistPreview = false) {
|
|
663
701
|
const context = await requirePreviewBoundConfirmation(parsed, preview, persistPreview);
|
|
664
702
|
try {
|
|
@@ -848,6 +886,9 @@ async function readGoalWorkspaceWriteSource(parsed) {
|
|
|
848
886
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
849
887
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
850
888
|
for (const entry of entries) {
|
|
889
|
+
// Git metadata is local checkout state, including worktree .git files.
|
|
890
|
+
if (entry.name === ".git")
|
|
891
|
+
continue;
|
|
851
892
|
const relativePath = relativeRoot
|
|
852
893
|
? path.join(relativeRoot, entry.name)
|
|
853
894
|
: entry.name;
|
|
@@ -1916,6 +1957,10 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
1916
1957
|
};
|
|
1917
1958
|
}
|
|
1918
1959
|
if (noun === "auth") {
|
|
1960
|
+
const duration = flagString(parsed, "duration");
|
|
1961
|
+
if (duration !== undefined && duration !== "12h" && duration !== "30d") {
|
|
1962
|
+
throw new CliError("invalid_arguments", "--duration must be 12h or 30d (admins only).");
|
|
1963
|
+
}
|
|
1919
1964
|
if (verb === "login") {
|
|
1920
1965
|
const oauthClientId = flagString(parsed, "client-id") ?? config.oauthClientId;
|
|
1921
1966
|
const callbackPort = flagNumber(parsed, "callback-port") ?? 8765;
|
|
@@ -1924,11 +1969,15 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
1924
1969
|
callbackPort > 65535) {
|
|
1925
1970
|
throw new CliError("invalid_arguments", "--callback-port must be an integer from 1024 through 65535.");
|
|
1926
1971
|
}
|
|
1927
|
-
return login(config, {
|
|
1972
|
+
return login(config, {
|
|
1973
|
+
callbackPort,
|
|
1974
|
+
oauthClientId,
|
|
1975
|
+
sessionDuration: duration,
|
|
1976
|
+
});
|
|
1928
1977
|
}
|
|
1929
1978
|
if (verb === "request") {
|
|
1930
1979
|
const label = flagString(parsed, "label");
|
|
1931
|
-
return requestDeviceAuth(config,
|
|
1980
|
+
return requestDeviceAuth(config, { label, sessionDuration: duration });
|
|
1932
1981
|
}
|
|
1933
1982
|
if (verb === "poll") {
|
|
1934
1983
|
const timeoutSeconds = flagNumber(parsed, "timeout") ?? 300;
|
|
@@ -4307,6 +4356,65 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
4307
4356
|
.includes(query))),
|
|
4308
4357
|
};
|
|
4309
4358
|
}
|
|
4359
|
+
// Moves ONE kid's lesson, not the template's. The daily queue serves
|
|
4360
|
+
// modules by `ordinal ASC`, so pushing the one a kid is stuck on past the
|
|
4361
|
+
// rest is what hands them the next lesson tomorrow — no template version,
|
|
4362
|
+
// no retiring the work they have already done, nobody else's goal touched.
|
|
4363
|
+
if (verb === "move-module") {
|
|
4364
|
+
const moduleRef = positional(parsed, 2, "module ref (e.g. Q006)");
|
|
4365
|
+
const studentId = flagString(parsed, "student", { required: true });
|
|
4366
|
+
const goalId = flagString(parsed, "goal", { required: true });
|
|
4367
|
+
const toEnd = hasFlag(parsed, "to-end");
|
|
4368
|
+
const position = flagNumber(parsed, "position");
|
|
4369
|
+
if (toEnd === (position !== undefined)) {
|
|
4370
|
+
throw new CliError("invalid_arguments", "Pass exactly one of --to-end or --position <n>.");
|
|
4371
|
+
}
|
|
4372
|
+
const goal = unwrap(await api.client.GET("/tutor/browser/students/{userId}/goals/", {
|
|
4373
|
+
params: { path: { userId: studentId } },
|
|
4374
|
+
})).goals.find((candidate) => candidate.id === goalId);
|
|
4375
|
+
const modules = goal?.osV2?.modules ?? [];
|
|
4376
|
+
const target = modules.find((module) => module.moduleRef.toLocaleLowerCase() === moduleRef.toLocaleLowerCase());
|
|
4377
|
+
if (!target) {
|
|
4378
|
+
throw new CliError("not_found", `Module ${moduleRef} was not found on goal ${goalId}. Known refs: ${modules.map((m) => m.moduleRef).join(", ") || "none"}.`);
|
|
4379
|
+
}
|
|
4380
|
+
// Sparse past the current maximum, so no sibling has to be renumbered.
|
|
4381
|
+
const last = modules.reduce((max, m) => Math.max(max, m.ordinal), 0);
|
|
4382
|
+
const ordinal = toEnd ? last + 1 : position;
|
|
4383
|
+
if (ordinal === target.ordinal) {
|
|
4384
|
+
throw new CliError("already_positioned", `Module ${target.moduleRef} ("${target.title}") is already at position ${ordinal}.`);
|
|
4385
|
+
}
|
|
4386
|
+
const collision = modules.find((m) => m.id !== target.id && m.ordinal === ordinal);
|
|
4387
|
+
const preview = {
|
|
4388
|
+
action: toEnd
|
|
4389
|
+
? "move a lesson to the end of this kid's curriculum"
|
|
4390
|
+
: "move a lesson to a specific position in this kid's curriculum",
|
|
4391
|
+
target: {
|
|
4392
|
+
studentUserId: studentId,
|
|
4393
|
+
goalId,
|
|
4394
|
+
goalTitle: goal?.title ?? null,
|
|
4395
|
+
moduleRef: target.moduleRef,
|
|
4396
|
+
title: target.title,
|
|
4397
|
+
from: target.ordinal,
|
|
4398
|
+
to: ordinal,
|
|
4399
|
+
completed: target.completedAt !== null,
|
|
4400
|
+
},
|
|
4401
|
+
request: { moduleId: target.id, ordinal },
|
|
4402
|
+
details: {
|
|
4403
|
+
note: "Affects only this student's copy of the curriculum — the goal template and every other kid on it are untouched. Completion state is preserved: an unfinished lesson stays unfinished and is simply served later.",
|
|
4404
|
+
...(collision
|
|
4405
|
+
? {
|
|
4406
|
+
warning: `Position ${ordinal} is already held by ${collision.moduleRef} ("${collision.title}"). Both will sit at the same position and the queue will order them by creation date — pass --to-end instead if you just want this one out of the way.`,
|
|
4407
|
+
}
|
|
4408
|
+
: {}),
|
|
4409
|
+
},
|
|
4410
|
+
};
|
|
4411
|
+
return previewBoundWrite(parsed, preview, async () => unwrap(await api.client.PATCH("/tutor/students/{studentId}/goals/{goalId}/modules/{moduleId}", {
|
|
4412
|
+
params: {
|
|
4413
|
+
path: { studentId, goalId, moduleId: target.id },
|
|
4414
|
+
},
|
|
4415
|
+
body: { ordinal },
|
|
4416
|
+
})));
|
|
4417
|
+
}
|
|
4310
4418
|
if (verb === "create") {
|
|
4311
4419
|
const requestedStudentId = flagString(parsed, "student");
|
|
4312
4420
|
const sourceDirectory = flagString(parsed, "source-dir");
|
|
@@ -4654,7 +4762,7 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
4654
4762
|
}
|
|
4655
4763
|
throw new CliError("invalid_arguments", "Use goals queue get|set.");
|
|
4656
4764
|
}
|
|
4657
|
-
throw new CliError("invalid_arguments", "Use goals list|create|edit|delete|restore|archive|unarchive|complete|undo-completion|queue|files|pdf.");
|
|
4765
|
+
throw new CliError("invalid_arguments", "Use goals list|create|edit|delete|restore|archive|unarchive|complete|undo-completion|move-module|queue|files|pdf.");
|
|
4658
4766
|
}
|
|
4659
4767
|
if (noun === "chat-logs") {
|
|
4660
4768
|
return runChatLogsCommand({ parsed, api, writeCommand });
|
|
@@ -5081,6 +5189,156 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5081
5189
|
}
|
|
5082
5190
|
throw new CliError("invalid_arguments", "Use rocky get|set.");
|
|
5083
5191
|
}
|
|
5192
|
+
if (noun === "tutor-templates") {
|
|
5193
|
+
if (verb === "list") {
|
|
5194
|
+
const grade = flagNumber(parsed, "grade");
|
|
5195
|
+
return unwrap(await api.client.GET("/tutor-templates/", {
|
|
5196
|
+
params: {
|
|
5197
|
+
query: {
|
|
5198
|
+
q: flagString(parsed, "q"),
|
|
5199
|
+
subject: flagString(parsed, "subject"),
|
|
5200
|
+
...(grade === undefined ? {} : { grade }),
|
|
5201
|
+
},
|
|
5202
|
+
},
|
|
5203
|
+
}));
|
|
5204
|
+
}
|
|
5205
|
+
if (verb === "get") {
|
|
5206
|
+
const slug = positional(parsed, 2, "template slug");
|
|
5207
|
+
return unwrap(await api.client.GET("/tutor-templates/{slug}", {
|
|
5208
|
+
params: { path: { slug } },
|
|
5209
|
+
}));
|
|
5210
|
+
}
|
|
5211
|
+
if (verb === "assign" || verb === "unassign") {
|
|
5212
|
+
const slug = positional(parsed, 2, "template slug");
|
|
5213
|
+
const studentId = flagString(parsed, "student");
|
|
5214
|
+
const body = studentId ? { studentId } : {};
|
|
5215
|
+
const template = unwrap(await api.client.GET("/tutor-templates/{slug}", {
|
|
5216
|
+
params: { path: { slug } },
|
|
5217
|
+
}));
|
|
5218
|
+
return writeCommand(parsed, {
|
|
5219
|
+
action: verb === "assign"
|
|
5220
|
+
? "add a tutor template's subject block to a kid's plan"
|
|
5221
|
+
: "remove a tutor template's subject block from a kid's plan",
|
|
5222
|
+
target: { slug, studentUserId: studentId ?? "the signed-in kid" },
|
|
5223
|
+
request: body,
|
|
5224
|
+
details: {
|
|
5225
|
+
title: template.title,
|
|
5226
|
+
cadence: template.cadence,
|
|
5227
|
+
consequence: verb === "assign"
|
|
5228
|
+
? "Appends `### <title> — <cadence>` to rocky-v3/plan.md, rebuilds Today, and writes an ACTIVE assignment row."
|
|
5229
|
+
: "Deletes that subject block from rocky-v3/plan.md and marks the assignment REMOVED.",
|
|
5230
|
+
},
|
|
5231
|
+
}, async () => verb === "assign"
|
|
5232
|
+
? unwrap(await api.client.POST("/tutor-templates/{slug}/assign", {
|
|
5233
|
+
params: { path: { slug } },
|
|
5234
|
+
body,
|
|
5235
|
+
}))
|
|
5236
|
+
: unwrap(await api.client.POST("/tutor-templates/{slug}/unassign", {
|
|
5237
|
+
params: { path: { slug } },
|
|
5238
|
+
body,
|
|
5239
|
+
})));
|
|
5240
|
+
}
|
|
5241
|
+
if (verb === "create" || verb === "update") {
|
|
5242
|
+
const bodyFile = flagString(parsed, "body-file", {
|
|
5243
|
+
required: verb === "create",
|
|
5244
|
+
});
|
|
5245
|
+
const lessonsDir = flagString(parsed, "lessons-dir");
|
|
5246
|
+
const visibility = flagString(parsed, "visibility");
|
|
5247
|
+
const emoji = flagString(parsed, "emoji");
|
|
5248
|
+
const gradeMin = flagNumber(parsed, "grade-min");
|
|
5249
|
+
const gradeMax = flagNumber(parsed, "grade-max");
|
|
5250
|
+
const shared = {
|
|
5251
|
+
...(flagString(parsed, "title") === undefined
|
|
5252
|
+
? {}
|
|
5253
|
+
: { title: flagString(parsed, "title") }),
|
|
5254
|
+
...(flagString(parsed, "summary") === undefined
|
|
5255
|
+
? {}
|
|
5256
|
+
: { summary: flagString(parsed, "summary") }),
|
|
5257
|
+
...(flagString(parsed, "subject") === undefined
|
|
5258
|
+
? {}
|
|
5259
|
+
: { subject: flagString(parsed, "subject") }),
|
|
5260
|
+
...(flagString(parsed, "cadence") === undefined
|
|
5261
|
+
? {}
|
|
5262
|
+
: { cadence: flagString(parsed, "cadence") }),
|
|
5263
|
+
...(emoji === undefined ? {} : { emoji }),
|
|
5264
|
+
...(gradeMin === undefined ? {} : { gradeMin }),
|
|
5265
|
+
...(gradeMax === undefined ? {} : { gradeMax }),
|
|
5266
|
+
...(visibility === undefined
|
|
5267
|
+
? {}
|
|
5268
|
+
: {
|
|
5269
|
+
visibility: assertChoice(visibility, ["PUBLIC", "STAFF"], "--visibility"),
|
|
5270
|
+
}),
|
|
5271
|
+
...(bodyFile === undefined
|
|
5272
|
+
? {}
|
|
5273
|
+
: { body: await readTutorTemplateBody(bodyFile) }),
|
|
5274
|
+
...(lessonsDir === undefined
|
|
5275
|
+
? {}
|
|
5276
|
+
: { lessons: await readTutorTemplateLessons(lessonsDir) }),
|
|
5277
|
+
};
|
|
5278
|
+
if (verb === "create") {
|
|
5279
|
+
const missing = ["title", "summary", "subject", "cadence"].filter((name) => !(name in shared));
|
|
5280
|
+
if (missing.length > 0) {
|
|
5281
|
+
throw new CliError("invalid_arguments", `tutor-templates create requires ${missing
|
|
5282
|
+
.map((name) => `--${name}`)
|
|
5283
|
+
.join(", ")}.`);
|
|
5284
|
+
}
|
|
5285
|
+
const slug = flagString(parsed, "slug", { required: true });
|
|
5286
|
+
const body = { slug, ...shared };
|
|
5287
|
+
return writeCommand(parsed, {
|
|
5288
|
+
action: "create a tutor template in the subject catalog",
|
|
5289
|
+
target: { slug },
|
|
5290
|
+
request: {
|
|
5291
|
+
...body,
|
|
5292
|
+
bodyFile: bodyFile ? path.resolve(bodyFile) : null,
|
|
5293
|
+
lessonsDir: lessonsDir ? path.resolve(lessonsDir) : null,
|
|
5294
|
+
},
|
|
5295
|
+
details: {
|
|
5296
|
+
lessonNames: Object.keys(shared.lessons ?? {}),
|
|
5297
|
+
renamingNote: "`title` is the `### <heading>` join key in every kid's plan.md — renaming later orphans existing plans.",
|
|
5298
|
+
},
|
|
5299
|
+
}, async () => unwrap(await api.client.POST("/tutor-templates/", { body })));
|
|
5300
|
+
}
|
|
5301
|
+
const slug = positional(parsed, 2, "template slug");
|
|
5302
|
+
const body = shared;
|
|
5303
|
+
if (Object.keys(body).length === 0) {
|
|
5304
|
+
throw new CliError("invalid_arguments", "tutor-templates update needs at least one field to change.");
|
|
5305
|
+
}
|
|
5306
|
+
return writeCommand(parsed, {
|
|
5307
|
+
action: "update a tutor template in the subject catalog",
|
|
5308
|
+
target: { slug },
|
|
5309
|
+
request: {
|
|
5310
|
+
...body,
|
|
5311
|
+
bodyFile: bodyFile ? path.resolve(bodyFile) : null,
|
|
5312
|
+
lessonsDir: lessonsDir ? path.resolve(lessonsDir) : null,
|
|
5313
|
+
},
|
|
5314
|
+
details: {
|
|
5315
|
+
fields: Object.keys(body),
|
|
5316
|
+
renamingNote: "Changing `title` orphans the `### <heading>` in every plan.md already carrying this template.",
|
|
5317
|
+
},
|
|
5318
|
+
}, async () => unwrap(await api.client.PATCH("/tutor-templates/{slug}", {
|
|
5319
|
+
params: { path: { slug } },
|
|
5320
|
+
body,
|
|
5321
|
+
})));
|
|
5322
|
+
}
|
|
5323
|
+
if (verb === "delete") {
|
|
5324
|
+
const slug = positional(parsed, 2, "template slug");
|
|
5325
|
+
const template = unwrap(await api.client.GET("/tutor-templates/{slug}", {
|
|
5326
|
+
params: { path: { slug } },
|
|
5327
|
+
}));
|
|
5328
|
+
return writeCommand(parsed, {
|
|
5329
|
+
action: "soft-delete a tutor template from the subject catalog",
|
|
5330
|
+
target: { slug },
|
|
5331
|
+
request: {},
|
|
5332
|
+
details: {
|
|
5333
|
+
title: template.title,
|
|
5334
|
+
consequence: "The row is soft-deleted; subject blocks already written into kids' plan.md stay where they are.",
|
|
5335
|
+
},
|
|
5336
|
+
}, async () => unwrap(await api.client.DELETE("/tutor-templates/{slug}", {
|
|
5337
|
+
params: { path: { slug } },
|
|
5338
|
+
})));
|
|
5339
|
+
}
|
|
5340
|
+
throw new CliError("invalid_arguments", "Use tutor-templates list|get|create|update|delete|assign|unassign.");
|
|
5341
|
+
}
|
|
5084
5342
|
if (noun === "goals" && verb === "pdf") {
|
|
5085
5343
|
const action = positional(parsed, 2, "goals pdf action");
|
|
5086
5344
|
if (action !== "upload") {
|
|
@@ -5351,7 +5609,15 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5351
5609
|
content: change.content,
|
|
5352
5610
|
contentEncoding: change.contentEncoding,
|
|
5353
5611
|
});
|
|
5612
|
+
const assignedEdits = hasFlag(parsed, "allow-assigned-edits")
|
|
5613
|
+
? { allowAssignedEdits: true }
|
|
5614
|
+
: {};
|
|
5615
|
+
const stateModification = hasFlag(parsed, "allow-state-modification")
|
|
5616
|
+
? { allowStateModification: true }
|
|
5617
|
+
: {};
|
|
5354
5618
|
const body = {
|
|
5619
|
+
...stateModification,
|
|
5620
|
+
...assignedEdits,
|
|
5355
5621
|
studentUserId: checkout.metadata.studentId,
|
|
5356
5622
|
target: checkout.metadata.target,
|
|
5357
5623
|
message,
|
|
@@ -5367,6 +5633,8 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5367
5633
|
throw new CliError("unexpected_response", "Goal workspace push did not return a preview; nothing was written.");
|
|
5368
5634
|
}
|
|
5369
5635
|
const request = {
|
|
5636
|
+
...stateModification,
|
|
5637
|
+
...assignedEdits,
|
|
5370
5638
|
sourceDirectory: checkout.root,
|
|
5371
5639
|
mesaBaseChangeId,
|
|
5372
5640
|
localBaseCommit,
|
|
@@ -5392,6 +5660,8 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5392
5660
|
target: boundTarget ?? preflight.target,
|
|
5393
5661
|
request,
|
|
5394
5662
|
details: {
|
|
5663
|
+
stateModificationPaths: preflight.stateModificationPaths ?? [],
|
|
5664
|
+
warnings: preflight.warnings ?? [],
|
|
5395
5665
|
currentChangeId: preflight.currentChangeId,
|
|
5396
5666
|
resolvedTarget: preflight.target,
|
|
5397
5667
|
note: "The committed local diff is published as one atomic Mesa change. Deletes and renames follow Git's diff. A changed Mesa tip refuses the push.",
|
|
@@ -5436,7 +5706,15 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5436
5706
|
: { kind: "draft", draftSlug: draftSlug };
|
|
5437
5707
|
const message = flagString(parsed, "message") ??
|
|
5438
5708
|
`recess-cli: write ${input.files.length} workspace file${input.files.length === 1 ? "" : "s"}`;
|
|
5709
|
+
const assignedEdits = hasFlag(parsed, "allow-assigned-edits")
|
|
5710
|
+
? { allowAssignedEdits: true }
|
|
5711
|
+
: {};
|
|
5712
|
+
const stateModification = hasFlag(parsed, "allow-state-modification")
|
|
5713
|
+
? { allowStateModification: true }
|
|
5714
|
+
: {};
|
|
5439
5715
|
const body = {
|
|
5716
|
+
...stateModification,
|
|
5717
|
+
...assignedEdits,
|
|
5440
5718
|
studentUserId: studentId,
|
|
5441
5719
|
target,
|
|
5442
5720
|
message,
|
|
@@ -5446,6 +5724,8 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5446
5724
|
? unwrap(await api.client.GET("/auth/admin-cli/session/"))
|
|
5447
5725
|
: null;
|
|
5448
5726
|
const request = {
|
|
5727
|
+
...stateModification,
|
|
5728
|
+
...assignedEdits,
|
|
5449
5729
|
source: input.source,
|
|
5450
5730
|
sourceSha256: input.sha256,
|
|
5451
5731
|
fileCount: input.files.length,
|
|
@@ -5480,6 +5760,8 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5480
5760
|
target: boundTarget ?? preflight.target,
|
|
5481
5761
|
request,
|
|
5482
5762
|
details: {
|
|
5763
|
+
stateModificationPaths: preflight.stateModificationPaths ?? [],
|
|
5764
|
+
warnings: preflight.warnings ?? [],
|
|
5483
5765
|
currentChangeId: preflight.currentChangeId,
|
|
5484
5766
|
files: preflight.files,
|
|
5485
5767
|
resolvedTarget: preflight.target,
|
package/dist/command-schema.js
CHANGED
|
@@ -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",
|
package/dist/commands/apps.js
CHANGED
|
@@ -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
|
-
...(
|
|
376
|
+
...(template ? { template } : {}),
|
|
368
377
|
...(manifest.primitives?.length ? { primitives: manifest.primitives } : {}),
|
|
369
378
|
dryRun,
|
|
370
379
|
};
|
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]
|
|
@@ -405,6 +405,8 @@ Usage:
|
|
|
405
405
|
[--confirm --approval-token TOKEN]
|
|
406
406
|
recess [--json] goals unarchive <goal-id> --student <kid-id>
|
|
407
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]
|
|
408
410
|
recess [--json] goals queue get <goal-id> --student <kid-id>
|
|
409
411
|
recess [--json] goals queue set <goal-id> --student <kid-id>
|
|
410
412
|
--entries-file <path.json> --delta TEXT [--replace-description-pointer]
|
|
@@ -435,6 +437,19 @@ Usage:
|
|
|
435
437
|
recess [--json] rocky get --student <kid-id>
|
|
436
438
|
recess [--json] rocky set --student <kid-id> --patch-file <path/patch.json>
|
|
437
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]
|
|
438
453
|
recess [--json] goals files list --student <goal-owner-id> --goal <goal-id>
|
|
439
454
|
recess [--json] goals files read --student <goal-owner-id> --goal <goal-id> --path P
|
|
440
455
|
recess [--json] goals files init --student <goal-owner-id> --draft <draft-slug>
|
|
@@ -443,11 +458,13 @@ Usage:
|
|
|
443
458
|
(--goal <goal-id> | --draft <draft-slug>)
|
|
444
459
|
--output-dir <local-dir>
|
|
445
460
|
recess [--json] goals files push --source-dir <local-checkout>
|
|
446
|
-
[--message TEXT] [--
|
|
461
|
+
[--message TEXT] [--allow-state-modification] [--allow-assigned-edits]
|
|
462
|
+
[--confirm --approval-token TOKEN]
|
|
447
463
|
recess [--json] goals files write --student <goal-owner-id>
|
|
448
464
|
(--goal <goal-id> | --draft <draft-slug>)
|
|
449
465
|
(--source-dir <local-dir> | --source-file <local-file> --path P)
|
|
450
|
-
[--message TEXT] [--
|
|
466
|
+
[--message TEXT] [--allow-state-modification] [--allow-assigned-edits]
|
|
467
|
+
[--confirm --approval-token TOKEN]
|
|
451
468
|
recess [--json] goals pdf upload --student <goal-owner-id>
|
|
452
469
|
(--goal <goal-id> | --draft <draft-slug>) --source-file <local.pdf>
|
|
453
470
|
[--path uploads/name.pdf] [--message TEXT]
|
|
@@ -459,6 +476,14 @@ bundled into this npm package. Load os-v2-goal-template-builder and its
|
|
|
459
476
|
references/deterministic-workflow-setup.md BEFORE authoring a template; that is
|
|
460
477
|
the same guidance the recess.gg/ai agent follows, so there is exactly one
|
|
461
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.
|
|
462
487
|
For goal workspace and PDF commands, --student names the goal owner. A
|
|
463
488
|
full_admin session may use a KID or ADMIN user id, including its own ADMIN id;
|
|
464
489
|
family_ai sessions remain limited to managed KID profiles.
|
|
@@ -551,7 +576,8 @@ cannot call /admin; KID sessions cannot call any non-Village API command. For a
|
|
|
551
576
|
cloud agent that has no local browser to open, "auth request" prints an approval URL; whoever
|
|
552
577
|
opens it and approves in a signed-in web session grants THEIR OWN scope, so a guardian approving
|
|
553
578
|
mints a family-scoped session and only an admin can mint a full-admin one. "auth poll" then
|
|
554
|
-
collects the
|
|
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
|
|
555
581
|
fresh link. Both paths yield the same session.
|
|
556
582
|
|
|
557
583
|
Skill notes: this CLI's own agent skill ships inside the npm package AND is served
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|