recess-cli 3.1.0 → 3.3.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 +335 -3
- package/dist/command-schema.js +20 -9
- package/dist/commands/apps.js +10 -1
- package/dist/help.js +39 -8
- 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;
|
|
@@ -2973,6 +3022,9 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
2973
3022
|
...(userId ? { userId } : {}),
|
|
2974
3023
|
...(id ? { id } : {}),
|
|
2975
3024
|
limit,
|
|
3025
|
+
...(hasFlag(parsed, "include-archived")
|
|
3026
|
+
? { includeArchived: "true" }
|
|
3027
|
+
: {}),
|
|
2976
3028
|
...(flagString(parsed, "cursor")
|
|
2977
3029
|
? { cursor: flagString(parsed, "cursor") }
|
|
2978
3030
|
: {}),
|
|
@@ -2980,6 +3032,53 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
2980
3032
|
},
|
|
2981
3033
|
}));
|
|
2982
3034
|
}
|
|
3035
|
+
if ((area === "recipients" &&
|
|
3036
|
+
["archive", "unarchive", "remove-schedule", "add-schedule"].includes(action)) ||
|
|
3037
|
+
(area === "payruns" && action === "remove-account")) {
|
|
3038
|
+
const accountId = area === "payruns"
|
|
3039
|
+
? flagString(parsed, "recipient", { required: true })
|
|
3040
|
+
: positional(parsed, 3, "account ID");
|
|
3041
|
+
const payRunId = area === "payruns" ? positional(parsed, 3, "payrun ID") : undefined;
|
|
3042
|
+
const scheduleId = action.endsWith("schedule")
|
|
3043
|
+
? flagString(parsed, "schedule", { required: true })
|
|
3044
|
+
: undefined;
|
|
3045
|
+
const account = unwrap(await api.client.GET("/admin/payout/recipient/", {
|
|
3046
|
+
params: { query: { id: accountId, includeArchived: "true" } },
|
|
3047
|
+
})).recipients[0];
|
|
3048
|
+
const effects = action === "archive"
|
|
3049
|
+
? "Archive payout account, end all schedules and cancel all draft/review invoices. Stops new Recess accruals; preserves payment history and the guide login."
|
|
3050
|
+
: action === "unarchive"
|
|
3051
|
+
? "Unarchive payout account. Does not restore schedules or canceled invoices; add a schedule separately to resume payroll."
|
|
3052
|
+
: action === "remove-schedule"
|
|
3053
|
+
? "Remove account from recurring schedule and cancel its draft/review invoices for that schedule. Other schedules and payment history are preserved."
|
|
3054
|
+
: action === "add-schedule"
|
|
3055
|
+
? "Add active payout account to recurring schedule. Future payroll cycles will accrue compensation in Recess."
|
|
3056
|
+
: "Remove account from this payroll cycle and cancel its draft/review invoices. Future cycles are unchanged; regeneration will not re-add this account.";
|
|
3057
|
+
return writeCommand(parsed, {
|
|
3058
|
+
action: effects,
|
|
3059
|
+
target: {
|
|
3060
|
+
accountId,
|
|
3061
|
+
...(payRunId ? { payRunId } : {}),
|
|
3062
|
+
...(scheduleId ? { scheduleId } : {}),
|
|
3063
|
+
},
|
|
3064
|
+
request: { action },
|
|
3065
|
+
details: { accountName: account.name, user: account.user },
|
|
3066
|
+
}, async () => {
|
|
3067
|
+
if (action === "archive")
|
|
3068
|
+
return unwrap(await api.client.DELETE("/admin/payout/recipient/{accountId}", {
|
|
3069
|
+
params: { path: { accountId } },
|
|
3070
|
+
}));
|
|
3071
|
+
if (action === "unarchive")
|
|
3072
|
+
return unwrap(await api.client.POST("/admin/payout/recipient/{accountId}/unarchive", { params: { path: { accountId } } }));
|
|
3073
|
+
if (action === "add-schedule")
|
|
3074
|
+
return unwrap(await api.client.POST("/admin/payout/payrun/add-account/", {
|
|
3075
|
+
body: { accountId, scheduleId: scheduleId },
|
|
3076
|
+
}));
|
|
3077
|
+
if (action === "remove-schedule")
|
|
3078
|
+
return unwrap(await api.client.DELETE("/admin/payout/schedule/{targetId}/recipient/{accountId}", { params: { path: { accountId, targetId: scheduleId } } }));
|
|
3079
|
+
return unwrap(await api.client.DELETE("/admin/payout/cycle/{targetId}/recipient/{accountId}", { params: { path: { accountId, targetId: payRunId } } }));
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
2983
3082
|
if (area === "invoices" && action === "list") {
|
|
2984
3083
|
const payRunId = flagString(parsed, "payrun");
|
|
2985
3084
|
const accountId = flagString(parsed, "recipient");
|
|
@@ -4307,6 +4406,65 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
4307
4406
|
.includes(query))),
|
|
4308
4407
|
};
|
|
4309
4408
|
}
|
|
4409
|
+
// Moves ONE kid's lesson, not the template's. The daily queue serves
|
|
4410
|
+
// modules by `ordinal ASC`, so pushing the one a kid is stuck on past the
|
|
4411
|
+
// rest is what hands them the next lesson tomorrow — no template version,
|
|
4412
|
+
// no retiring the work they have already done, nobody else's goal touched.
|
|
4413
|
+
if (verb === "move-module") {
|
|
4414
|
+
const moduleRef = positional(parsed, 2, "module ref (e.g. Q006)");
|
|
4415
|
+
const studentId = flagString(parsed, "student", { required: true });
|
|
4416
|
+
const goalId = flagString(parsed, "goal", { required: true });
|
|
4417
|
+
const toEnd = hasFlag(parsed, "to-end");
|
|
4418
|
+
const position = flagNumber(parsed, "position");
|
|
4419
|
+
if (toEnd === (position !== undefined)) {
|
|
4420
|
+
throw new CliError("invalid_arguments", "Pass exactly one of --to-end or --position <n>.");
|
|
4421
|
+
}
|
|
4422
|
+
const goal = unwrap(await api.client.GET("/tutor/browser/students/{userId}/goals/", {
|
|
4423
|
+
params: { path: { userId: studentId } },
|
|
4424
|
+
})).goals.find((candidate) => candidate.id === goalId);
|
|
4425
|
+
const modules = goal?.osV2?.modules ?? [];
|
|
4426
|
+
const target = modules.find((module) => module.moduleRef.toLocaleLowerCase() === moduleRef.toLocaleLowerCase());
|
|
4427
|
+
if (!target) {
|
|
4428
|
+
throw new CliError("not_found", `Module ${moduleRef} was not found on goal ${goalId}. Known refs: ${modules.map((m) => m.moduleRef).join(", ") || "none"}.`);
|
|
4429
|
+
}
|
|
4430
|
+
// Sparse past the current maximum, so no sibling has to be renumbered.
|
|
4431
|
+
const last = modules.reduce((max, m) => Math.max(max, m.ordinal), 0);
|
|
4432
|
+
const ordinal = toEnd ? last + 1 : position;
|
|
4433
|
+
if (ordinal === target.ordinal) {
|
|
4434
|
+
throw new CliError("already_positioned", `Module ${target.moduleRef} ("${target.title}") is already at position ${ordinal}.`);
|
|
4435
|
+
}
|
|
4436
|
+
const collision = modules.find((m) => m.id !== target.id && m.ordinal === ordinal);
|
|
4437
|
+
const preview = {
|
|
4438
|
+
action: toEnd
|
|
4439
|
+
? "move a lesson to the end of this kid's curriculum"
|
|
4440
|
+
: "move a lesson to a specific position in this kid's curriculum",
|
|
4441
|
+
target: {
|
|
4442
|
+
studentUserId: studentId,
|
|
4443
|
+
goalId,
|
|
4444
|
+
goalTitle: goal?.title ?? null,
|
|
4445
|
+
moduleRef: target.moduleRef,
|
|
4446
|
+
title: target.title,
|
|
4447
|
+
from: target.ordinal,
|
|
4448
|
+
to: ordinal,
|
|
4449
|
+
completed: target.completedAt !== null,
|
|
4450
|
+
},
|
|
4451
|
+
request: { moduleId: target.id, ordinal },
|
|
4452
|
+
details: {
|
|
4453
|
+
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.",
|
|
4454
|
+
...(collision
|
|
4455
|
+
? {
|
|
4456
|
+
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.`,
|
|
4457
|
+
}
|
|
4458
|
+
: {}),
|
|
4459
|
+
},
|
|
4460
|
+
};
|
|
4461
|
+
return previewBoundWrite(parsed, preview, async () => unwrap(await api.client.PATCH("/tutor/students/{studentId}/goals/{goalId}/modules/{moduleId}", {
|
|
4462
|
+
params: {
|
|
4463
|
+
path: { studentId, goalId, moduleId: target.id },
|
|
4464
|
+
},
|
|
4465
|
+
body: { ordinal },
|
|
4466
|
+
})));
|
|
4467
|
+
}
|
|
4310
4468
|
if (verb === "create") {
|
|
4311
4469
|
const requestedStudentId = flagString(parsed, "student");
|
|
4312
4470
|
const sourceDirectory = flagString(parsed, "source-dir");
|
|
@@ -4654,7 +4812,7 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
4654
4812
|
}
|
|
4655
4813
|
throw new CliError("invalid_arguments", "Use goals queue get|set.");
|
|
4656
4814
|
}
|
|
4657
|
-
throw new CliError("invalid_arguments", "Use goals list|create|edit|delete|restore|archive|unarchive|complete|undo-completion|queue|files|pdf.");
|
|
4815
|
+
throw new CliError("invalid_arguments", "Use goals list|create|edit|delete|restore|archive|unarchive|complete|undo-completion|move-module|queue|files|pdf.");
|
|
4658
4816
|
}
|
|
4659
4817
|
if (noun === "chat-logs") {
|
|
4660
4818
|
return runChatLogsCommand({ parsed, api, writeCommand });
|
|
@@ -5081,6 +5239,156 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5081
5239
|
}
|
|
5082
5240
|
throw new CliError("invalid_arguments", "Use rocky get|set.");
|
|
5083
5241
|
}
|
|
5242
|
+
if (noun === "tutor-templates") {
|
|
5243
|
+
if (verb === "list") {
|
|
5244
|
+
const grade = flagNumber(parsed, "grade");
|
|
5245
|
+
return unwrap(await api.client.GET("/tutor-templates/", {
|
|
5246
|
+
params: {
|
|
5247
|
+
query: {
|
|
5248
|
+
q: flagString(parsed, "q"),
|
|
5249
|
+
subject: flagString(parsed, "subject"),
|
|
5250
|
+
...(grade === undefined ? {} : { grade }),
|
|
5251
|
+
},
|
|
5252
|
+
},
|
|
5253
|
+
}));
|
|
5254
|
+
}
|
|
5255
|
+
if (verb === "get") {
|
|
5256
|
+
const slug = positional(parsed, 2, "template slug");
|
|
5257
|
+
return unwrap(await api.client.GET("/tutor-templates/{slug}", {
|
|
5258
|
+
params: { path: { slug } },
|
|
5259
|
+
}));
|
|
5260
|
+
}
|
|
5261
|
+
if (verb === "assign" || verb === "unassign") {
|
|
5262
|
+
const slug = positional(parsed, 2, "template slug");
|
|
5263
|
+
const studentId = flagString(parsed, "student");
|
|
5264
|
+
const body = studentId ? { studentId } : {};
|
|
5265
|
+
const template = unwrap(await api.client.GET("/tutor-templates/{slug}", {
|
|
5266
|
+
params: { path: { slug } },
|
|
5267
|
+
}));
|
|
5268
|
+
return writeCommand(parsed, {
|
|
5269
|
+
action: verb === "assign"
|
|
5270
|
+
? "add a tutor template's subject block to a kid's plan"
|
|
5271
|
+
: "remove a tutor template's subject block from a kid's plan",
|
|
5272
|
+
target: { slug, studentUserId: studentId ?? "the signed-in kid" },
|
|
5273
|
+
request: body,
|
|
5274
|
+
details: {
|
|
5275
|
+
title: template.title,
|
|
5276
|
+
cadence: template.cadence,
|
|
5277
|
+
consequence: verb === "assign"
|
|
5278
|
+
? "Appends `### <title> — <cadence>` to rocky-v3/plan.md, rebuilds Today, and writes an ACTIVE assignment row."
|
|
5279
|
+
: "Deletes that subject block from rocky-v3/plan.md and marks the assignment REMOVED.",
|
|
5280
|
+
},
|
|
5281
|
+
}, async () => verb === "assign"
|
|
5282
|
+
? unwrap(await api.client.POST("/tutor-templates/{slug}/assign", {
|
|
5283
|
+
params: { path: { slug } },
|
|
5284
|
+
body,
|
|
5285
|
+
}))
|
|
5286
|
+
: unwrap(await api.client.POST("/tutor-templates/{slug}/unassign", {
|
|
5287
|
+
params: { path: { slug } },
|
|
5288
|
+
body,
|
|
5289
|
+
})));
|
|
5290
|
+
}
|
|
5291
|
+
if (verb === "create" || verb === "update") {
|
|
5292
|
+
const bodyFile = flagString(parsed, "body-file", {
|
|
5293
|
+
required: verb === "create",
|
|
5294
|
+
});
|
|
5295
|
+
const lessonsDir = flagString(parsed, "lessons-dir");
|
|
5296
|
+
const visibility = flagString(parsed, "visibility");
|
|
5297
|
+
const emoji = flagString(parsed, "emoji");
|
|
5298
|
+
const gradeMin = flagNumber(parsed, "grade-min");
|
|
5299
|
+
const gradeMax = flagNumber(parsed, "grade-max");
|
|
5300
|
+
const shared = {
|
|
5301
|
+
...(flagString(parsed, "title") === undefined
|
|
5302
|
+
? {}
|
|
5303
|
+
: { title: flagString(parsed, "title") }),
|
|
5304
|
+
...(flagString(parsed, "summary") === undefined
|
|
5305
|
+
? {}
|
|
5306
|
+
: { summary: flagString(parsed, "summary") }),
|
|
5307
|
+
...(flagString(parsed, "subject") === undefined
|
|
5308
|
+
? {}
|
|
5309
|
+
: { subject: flagString(parsed, "subject") }),
|
|
5310
|
+
...(flagString(parsed, "cadence") === undefined
|
|
5311
|
+
? {}
|
|
5312
|
+
: { cadence: flagString(parsed, "cadence") }),
|
|
5313
|
+
...(emoji === undefined ? {} : { emoji }),
|
|
5314
|
+
...(gradeMin === undefined ? {} : { gradeMin }),
|
|
5315
|
+
...(gradeMax === undefined ? {} : { gradeMax }),
|
|
5316
|
+
...(visibility === undefined
|
|
5317
|
+
? {}
|
|
5318
|
+
: {
|
|
5319
|
+
visibility: assertChoice(visibility, ["PUBLIC", "STAFF"], "--visibility"),
|
|
5320
|
+
}),
|
|
5321
|
+
...(bodyFile === undefined
|
|
5322
|
+
? {}
|
|
5323
|
+
: { body: await readTutorTemplateBody(bodyFile) }),
|
|
5324
|
+
...(lessonsDir === undefined
|
|
5325
|
+
? {}
|
|
5326
|
+
: { lessons: await readTutorTemplateLessons(lessonsDir) }),
|
|
5327
|
+
};
|
|
5328
|
+
if (verb === "create") {
|
|
5329
|
+
const missing = ["title", "summary", "subject", "cadence"].filter((name) => !(name in shared));
|
|
5330
|
+
if (missing.length > 0) {
|
|
5331
|
+
throw new CliError("invalid_arguments", `tutor-templates create requires ${missing
|
|
5332
|
+
.map((name) => `--${name}`)
|
|
5333
|
+
.join(", ")}.`);
|
|
5334
|
+
}
|
|
5335
|
+
const slug = flagString(parsed, "slug", { required: true });
|
|
5336
|
+
const body = { slug, ...shared };
|
|
5337
|
+
return writeCommand(parsed, {
|
|
5338
|
+
action: "create a tutor template in the subject catalog",
|
|
5339
|
+
target: { slug },
|
|
5340
|
+
request: {
|
|
5341
|
+
...body,
|
|
5342
|
+
bodyFile: bodyFile ? path.resolve(bodyFile) : null,
|
|
5343
|
+
lessonsDir: lessonsDir ? path.resolve(lessonsDir) : null,
|
|
5344
|
+
},
|
|
5345
|
+
details: {
|
|
5346
|
+
lessonNames: Object.keys(shared.lessons ?? {}),
|
|
5347
|
+
renamingNote: "`title` is the `### <heading>` join key in every kid's plan.md — renaming later orphans existing plans.",
|
|
5348
|
+
},
|
|
5349
|
+
}, async () => unwrap(await api.client.POST("/tutor-templates/", { body })));
|
|
5350
|
+
}
|
|
5351
|
+
const slug = positional(parsed, 2, "template slug");
|
|
5352
|
+
const body = shared;
|
|
5353
|
+
if (Object.keys(body).length === 0) {
|
|
5354
|
+
throw new CliError("invalid_arguments", "tutor-templates update needs at least one field to change.");
|
|
5355
|
+
}
|
|
5356
|
+
return writeCommand(parsed, {
|
|
5357
|
+
action: "update a tutor template in the subject catalog",
|
|
5358
|
+
target: { slug },
|
|
5359
|
+
request: {
|
|
5360
|
+
...body,
|
|
5361
|
+
bodyFile: bodyFile ? path.resolve(bodyFile) : null,
|
|
5362
|
+
lessonsDir: lessonsDir ? path.resolve(lessonsDir) : null,
|
|
5363
|
+
},
|
|
5364
|
+
details: {
|
|
5365
|
+
fields: Object.keys(body),
|
|
5366
|
+
renamingNote: "Changing `title` orphans the `### <heading>` in every plan.md already carrying this template.",
|
|
5367
|
+
},
|
|
5368
|
+
}, async () => unwrap(await api.client.PATCH("/tutor-templates/{slug}", {
|
|
5369
|
+
params: { path: { slug } },
|
|
5370
|
+
body,
|
|
5371
|
+
})));
|
|
5372
|
+
}
|
|
5373
|
+
if (verb === "delete") {
|
|
5374
|
+
const slug = positional(parsed, 2, "template slug");
|
|
5375
|
+
const template = unwrap(await api.client.GET("/tutor-templates/{slug}", {
|
|
5376
|
+
params: { path: { slug } },
|
|
5377
|
+
}));
|
|
5378
|
+
return writeCommand(parsed, {
|
|
5379
|
+
action: "soft-delete a tutor template from the subject catalog",
|
|
5380
|
+
target: { slug },
|
|
5381
|
+
request: {},
|
|
5382
|
+
details: {
|
|
5383
|
+
title: template.title,
|
|
5384
|
+
consequence: "The row is soft-deleted; subject blocks already written into kids' plan.md stay where they are.",
|
|
5385
|
+
},
|
|
5386
|
+
}, async () => unwrap(await api.client.DELETE("/tutor-templates/{slug}", {
|
|
5387
|
+
params: { path: { slug } },
|
|
5388
|
+
})));
|
|
5389
|
+
}
|
|
5390
|
+
throw new CliError("invalid_arguments", "Use tutor-templates list|get|create|update|delete|assign|unassign.");
|
|
5391
|
+
}
|
|
5084
5392
|
if (noun === "goals" && verb === "pdf") {
|
|
5085
5393
|
const action = positional(parsed, 2, "goals pdf action");
|
|
5086
5394
|
if (action !== "upload") {
|
|
@@ -5351,7 +5659,15 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5351
5659
|
content: change.content,
|
|
5352
5660
|
contentEncoding: change.contentEncoding,
|
|
5353
5661
|
});
|
|
5662
|
+
const assignedEdits = hasFlag(parsed, "allow-assigned-edits")
|
|
5663
|
+
? { allowAssignedEdits: true }
|
|
5664
|
+
: {};
|
|
5665
|
+
const stateModification = hasFlag(parsed, "allow-state-modification")
|
|
5666
|
+
? { allowStateModification: true }
|
|
5667
|
+
: {};
|
|
5354
5668
|
const body = {
|
|
5669
|
+
...stateModification,
|
|
5670
|
+
...assignedEdits,
|
|
5355
5671
|
studentUserId: checkout.metadata.studentId,
|
|
5356
5672
|
target: checkout.metadata.target,
|
|
5357
5673
|
message,
|
|
@@ -5367,6 +5683,8 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5367
5683
|
throw new CliError("unexpected_response", "Goal workspace push did not return a preview; nothing was written.");
|
|
5368
5684
|
}
|
|
5369
5685
|
const request = {
|
|
5686
|
+
...stateModification,
|
|
5687
|
+
...assignedEdits,
|
|
5370
5688
|
sourceDirectory: checkout.root,
|
|
5371
5689
|
mesaBaseChangeId,
|
|
5372
5690
|
localBaseCommit,
|
|
@@ -5392,6 +5710,8 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5392
5710
|
target: boundTarget ?? preflight.target,
|
|
5393
5711
|
request,
|
|
5394
5712
|
details: {
|
|
5713
|
+
stateModificationPaths: preflight.stateModificationPaths ?? [],
|
|
5714
|
+
warnings: preflight.warnings ?? [],
|
|
5395
5715
|
currentChangeId: preflight.currentChangeId,
|
|
5396
5716
|
resolvedTarget: preflight.target,
|
|
5397
5717
|
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 +5756,15 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5436
5756
|
: { kind: "draft", draftSlug: draftSlug };
|
|
5437
5757
|
const message = flagString(parsed, "message") ??
|
|
5438
5758
|
`recess-cli: write ${input.files.length} workspace file${input.files.length === 1 ? "" : "s"}`;
|
|
5759
|
+
const assignedEdits = hasFlag(parsed, "allow-assigned-edits")
|
|
5760
|
+
? { allowAssignedEdits: true }
|
|
5761
|
+
: {};
|
|
5762
|
+
const stateModification = hasFlag(parsed, "allow-state-modification")
|
|
5763
|
+
? { allowStateModification: true }
|
|
5764
|
+
: {};
|
|
5439
5765
|
const body = {
|
|
5766
|
+
...stateModification,
|
|
5767
|
+
...assignedEdits,
|
|
5440
5768
|
studentUserId: studentId,
|
|
5441
5769
|
target,
|
|
5442
5770
|
message,
|
|
@@ -5446,6 +5774,8 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5446
5774
|
? unwrap(await api.client.GET("/auth/admin-cli/session/"))
|
|
5447
5775
|
: null;
|
|
5448
5776
|
const request = {
|
|
5777
|
+
...stateModification,
|
|
5778
|
+
...assignedEdits,
|
|
5449
5779
|
source: input.source,
|
|
5450
5780
|
sourceSha256: input.sha256,
|
|
5451
5781
|
fileCount: input.files.length,
|
|
@@ -5480,6 +5810,8 @@ export async function executeRecessCommand(argv, options = {}) {
|
|
|
5480
5810
|
target: boundTarget ?? preflight.target,
|
|
5481
5811
|
request,
|
|
5482
5812
|
details: {
|
|
5813
|
+
stateModificationPaths: preflight.stateModificationPaths ?? [],
|
|
5814
|
+
warnings: preflight.warnings ?? [],
|
|
5483
5815
|
currentChangeId: preflight.currentChangeId,
|
|
5484
5816
|
files: preflight.files,
|
|
5485
5817
|
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]
|
|
@@ -130,10 +130,15 @@ Usage:
|
|
|
130
130
|
recess [--json] billing cancel-subscription --subscription <id>
|
|
131
131
|
[--immediate] [--reason TEXT] [--restore] [--confirm]
|
|
132
132
|
recess [--json] payout payruns list [--status A,B] [--schedule <id>]
|
|
133
|
-
recess [--json] payout recipients list [--search <name>] [--user <id>] [--id <id>]
|
|
133
|
+
recess [--json] payout recipients list [--search <name>] [--user <id>] [--id <id>] [--include-archived]
|
|
134
134
|
[--limit 20] [--cursor <id>]
|
|
135
135
|
recess [--json] payout invoices list [--payrun <id>] [--recipient <account-id>]
|
|
136
136
|
[--user <id>] [--status A,B] (at least one filter)
|
|
137
|
+
recess [--json] payout recipients archive <account-id> [--confirm]
|
|
138
|
+
recess [--json] payout recipients unarchive <account-id> [--confirm]
|
|
139
|
+
recess [--json] payout recipients remove-schedule <account-id> --schedule <id> [--confirm]
|
|
140
|
+
recess [--json] payout recipients add-schedule <account-id> --schedule <id> [--confirm]
|
|
141
|
+
recess [--json] payout payruns remove-account <payrun-id> --recipient <account-id> [--confirm]
|
|
137
142
|
recess [--json] payout invoices get <invoice-id>
|
|
138
143
|
recess [--json] payout invoices set-status <invoice-id>
|
|
139
144
|
--status IN_REVIEW|OPEN|PAID|CANCELED [--send-email] [--confirm]
|
|
@@ -405,6 +410,8 @@ Usage:
|
|
|
405
410
|
[--confirm --approval-token TOKEN]
|
|
406
411
|
recess [--json] goals unarchive <goal-id> --student <kid-id>
|
|
407
412
|
[--confirm --approval-token TOKEN]
|
|
413
|
+
recess [--json] goals move-module <module-ref> --student <kid-id> --goal <goal-id>
|
|
414
|
+
(--to-end | --position <n>) [--confirm --approval-token TOKEN]
|
|
408
415
|
recess [--json] goals queue get <goal-id> --student <kid-id>
|
|
409
416
|
recess [--json] goals queue set <goal-id> --student <kid-id>
|
|
410
417
|
--entries-file <path.json> --delta TEXT [--replace-description-pointer]
|
|
@@ -435,6 +442,19 @@ Usage:
|
|
|
435
442
|
recess [--json] rocky get --student <kid-id>
|
|
436
443
|
recess [--json] rocky set --student <kid-id> --patch-file <path/patch.json>
|
|
437
444
|
--expected-updated-at ISO|none [--confirm]
|
|
445
|
+
recess [--json] tutor-templates list [--q TEXT] [--subject TEXT] [--grade N]
|
|
446
|
+
recess [--json] tutor-templates get <slug>
|
|
447
|
+
recess [--json] tutor-templates assign <slug> [--student <kid-id>] [--confirm]
|
|
448
|
+
recess [--json] tutor-templates unassign <slug> [--student <kid-id>] [--confirm]
|
|
449
|
+
recess [--json] tutor-templates create --slug SLUG --title TEXT --summary TEXT
|
|
450
|
+
--subject TEXT --cadence TEXT --body-file <path.md> [--lessons-dir <dir>]
|
|
451
|
+
[--emoji TEXT] [--grade-min N] [--grade-max N]
|
|
452
|
+
[--visibility PUBLIC|STAFF] [--confirm]
|
|
453
|
+
recess [--json] tutor-templates update <slug> [--title TEXT] [--summary TEXT]
|
|
454
|
+
[--subject TEXT] [--cadence TEXT] [--body-file <path.md>]
|
|
455
|
+
[--lessons-dir <dir>] [--emoji TEXT] [--grade-min N] [--grade-max N]
|
|
456
|
+
[--visibility PUBLIC|STAFF] [--confirm]
|
|
457
|
+
recess [--json] tutor-templates delete <slug> [--confirm]
|
|
438
458
|
recess [--json] goals files list --student <goal-owner-id> --goal <goal-id>
|
|
439
459
|
recess [--json] goals files read --student <goal-owner-id> --goal <goal-id> --path P
|
|
440
460
|
recess [--json] goals files init --student <goal-owner-id> --draft <draft-slug>
|
|
@@ -443,11 +463,13 @@ Usage:
|
|
|
443
463
|
(--goal <goal-id> | --draft <draft-slug>)
|
|
444
464
|
--output-dir <local-dir>
|
|
445
465
|
recess [--json] goals files push --source-dir <local-checkout>
|
|
446
|
-
[--message TEXT] [--
|
|
466
|
+
[--message TEXT] [--allow-state-modification] [--allow-assigned-edits]
|
|
467
|
+
[--confirm --approval-token TOKEN]
|
|
447
468
|
recess [--json] goals files write --student <goal-owner-id>
|
|
448
469
|
(--goal <goal-id> | --draft <draft-slug>)
|
|
449
470
|
(--source-dir <local-dir> | --source-file <local-file> --path P)
|
|
450
|
-
[--message TEXT] [--
|
|
471
|
+
[--message TEXT] [--allow-state-modification] [--allow-assigned-edits]
|
|
472
|
+
[--confirm --approval-token TOKEN]
|
|
451
473
|
recess [--json] goals pdf upload --student <goal-owner-id>
|
|
452
474
|
(--goal <goal-id> | --draft <draft-slug>) --source-file <local.pdf>
|
|
453
475
|
[--path uploads/name.pdf] [--message TEXT]
|
|
@@ -459,6 +481,14 @@ bundled into this npm package. Load os-v2-goal-template-builder and its
|
|
|
459
481
|
references/deterministic-workflow-setup.md BEFORE authoring a template; that is
|
|
460
482
|
the same guidance the recess.gg/ai agent follows, so there is exactly one
|
|
461
483
|
standard. Responses cache under ~/.recess-cli/skills-cache/ (--refresh re-fetches).
|
|
484
|
+
Goal file writes/pushes warn and refuse curriculum edits with unfinished assignments
|
|
485
|
+
unless --allow-assigned-edits is supplied. This can change lessons while a student
|
|
486
|
+
is working; assignments are not retired/completed and student answers are not merged.
|
|
487
|
+
The flag requires a fresh preview and confirmation; revision checks still apply.
|
|
488
|
+
Goal file writes/pushes require --allow-state-modification to modify state or
|
|
489
|
+
student-work folders, or replace/delete an existing resources/artifacts file.
|
|
490
|
+
Review the protected paths in the warning; the flag does not preserve answers
|
|
491
|
+
automatically or bypass assignment/revision checks. Adding it needs a new preview.
|
|
462
492
|
For goal workspace and PDF commands, --student names the goal owner. A
|
|
463
493
|
full_admin session may use a KID or ADMIN user id, including its own ADMIN id;
|
|
464
494
|
family_ai sessions remain limited to managed KID profiles.
|
|
@@ -551,7 +581,8 @@ cannot call /admin; KID sessions cannot call any non-Village API command. For a
|
|
|
551
581
|
cloud agent that has no local browser to open, "auth request" prints an approval URL; whoever
|
|
552
582
|
opens it and approves in a signed-in web session grants THEIR OWN scope, so a guardian approving
|
|
553
583
|
mints a family-scoped session and only an admin can mint a full-admin one. "auth poll" then
|
|
554
|
-
collects the
|
|
584
|
+
collects the session. Sessions default to 12h; admins can request 30 days with --duration 30d
|
|
585
|
+
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
586
|
fresh link. Both paths yield the same session.
|
|
556
587
|
|
|
557
588
|
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
|