recess-cli 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/cli.js +130 -4
- package/dist/command-schema.js +222 -8
- package/dist/help.js +2 -0
- package/package.json +1 -1
- package/skill/recess-cli/SKILL.md +4 -3
- package/skill/recess-cli/agents/version.json +2 -2
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ Error or write preview:
|
|
|
111
111
|
|
|
112
112
|
Exit code `0` means success, `1` means an input/auth/API failure, and `2` means a write is awaiting explicit human confirmation.
|
|
113
113
|
|
|
114
|
-
`recess --json agent-context` returns the canonical command/flag/positional schema. `recess --json help payout recipients` returns scoped help
|
|
114
|
+
`recess --json agent-context` returns the canonical command/flag/positional schema filtered to the scope claim already stored in the current CLI session. Bare help, scoped help, and `agent-context` make no API request; real commands still go through server authorization, while `auth status` and `doctor` perform live session checks. `recess --json help payout recipients` returns scoped help, and human help marks exact-admin commands with `◆`. Unknown flags, duplicate non-repeatable flags, missing values, and extra positionals are errors instead of being silently ignored.
|
|
115
115
|
|
|
116
116
|
Every command-driven request to the Recess API except `auth` requires `--reason "..."`: a
|
|
117
117
|
non-empty, human-readable purpose of at most 1024 characters. The CLI sends it as
|
|
@@ -249,6 +249,7 @@ recess --json students schedule --student <kid-id> --days 30 --reason "Review th
|
|
|
249
249
|
recess --json students xp-history --student <kid-id> --range month --reason "Review recent XP history"
|
|
250
250
|
recess --json goals list --student <kid-id> --reason "Review the student's goals"
|
|
251
251
|
recess --json todos create --student <kid-id> --title "Read chapter 4" --reason "Add the assigned reading"
|
|
252
|
+
recess --json todos complete <todo-id> --xp 35 --reason "Complete the todo with a 35 XP total reward"
|
|
252
253
|
recess --json goals delete <goal-id> --student <kid-id> --reason "Remove this obsolete goal"
|
|
253
254
|
recess --json todos delete <todo-id> --reason "Remove this disposable todo"
|
|
254
255
|
recess --json todos generate-applet <todo-id> --student <kid-id> --reason "Generate this todo's applet"
|
|
@@ -258,7 +259,9 @@ recess --json rocky get --student <kid-id> --reason "Inspect the student's Rocky
|
|
|
258
259
|
```
|
|
259
260
|
|
|
260
261
|
All family writes still preview first. Goal/todo/Rocky edits also carry the current server version
|
|
261
|
-
into the confirmed request.
|
|
262
|
+
into the confirmed request. `todos complete` is staff-only; `--xp` is the target total XP for the
|
|
263
|
+
todo, so its preview reports prior credit and the new delta before the normal completion and reward
|
|
264
|
+
side effects run. Applet generation is also staff-only: it resolves the todo's latest learning
|
|
262
265
|
analysis, defaults the generated todo to tomorrow in the student's timezone, and lets the server
|
|
263
266
|
select v1 or v2 for that student. A guardian cannot target another family, inspect frozen/deleted
|
|
264
267
|
template history, choose todo rewards/completion/internal fields, or use the ADMIN-only
|
package/dist/cli.js
CHANGED
|
@@ -91,6 +91,69 @@ async function sessionStatus(config) {
|
|
|
91
91
|
}
|
|
92
92
|
return { ...result.data, authSource: config.authSource };
|
|
93
93
|
}
|
|
94
|
+
const DISCOVERY_ROLE_BY_SCOPE = {
|
|
95
|
+
full_admin: "ADMIN",
|
|
96
|
+
family_ai: "GUARDIAN",
|
|
97
|
+
guide_students: "GUIDE",
|
|
98
|
+
village_home: "KID",
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Command discovery is intentionally local: the signed session cookie already
|
|
102
|
+
* carries the role, CLI scope, and expiry minted at login. Decoding those
|
|
103
|
+
* unverified claims is safe here because they only hide or reveal help text;
|
|
104
|
+
* every real command still sends the cookie to the server for authorization.
|
|
105
|
+
*/
|
|
106
|
+
function resolveCommandDiscovery(config) {
|
|
107
|
+
const signedOut = {
|
|
108
|
+
scope: null,
|
|
109
|
+
role: null,
|
|
110
|
+
source: "signed_out",
|
|
111
|
+
};
|
|
112
|
+
const unavailable = {
|
|
113
|
+
scope: null,
|
|
114
|
+
role: null,
|
|
115
|
+
source: "unavailable",
|
|
116
|
+
};
|
|
117
|
+
if (!config.sessionCookie)
|
|
118
|
+
return signedOut;
|
|
119
|
+
try {
|
|
120
|
+
const separator = config.sessionCookie.indexOf("=");
|
|
121
|
+
if (separator < 1)
|
|
122
|
+
return unavailable;
|
|
123
|
+
const cookieValue = decodeURIComponent(config.sessionCookie.slice(separator + 1).split(";", 1)[0]);
|
|
124
|
+
const payloadSegment = cookieValue.split(".")[1];
|
|
125
|
+
if (!payloadSegment)
|
|
126
|
+
return unavailable;
|
|
127
|
+
const value = JSON.parse(Buffer.from(payloadSegment, "base64url").toString("utf8"));
|
|
128
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
129
|
+
return unavailable;
|
|
130
|
+
}
|
|
131
|
+
const claim = value;
|
|
132
|
+
if (typeof claim.exp !== "number" || !Number.isFinite(claim.exp)) {
|
|
133
|
+
return unavailable;
|
|
134
|
+
}
|
|
135
|
+
if (claim.exp * 1000 <= Date.now()) {
|
|
136
|
+
return signedOut;
|
|
137
|
+
}
|
|
138
|
+
const scope = typeof claim.cliScope === "string" &&
|
|
139
|
+
Object.hasOwn(DISCOVERY_ROLE_BY_SCOPE, claim.cliScope)
|
|
140
|
+
? claim.cliScope
|
|
141
|
+
: claim.role === "ADMIN" && claim.adminCli === true
|
|
142
|
+
? "full_admin"
|
|
143
|
+
: null;
|
|
144
|
+
if (!scope || claim.role !== DISCOVERY_ROLE_BY_SCOPE[scope]) {
|
|
145
|
+
return unavailable;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
scope,
|
|
149
|
+
role: DISCOVERY_ROLE_BY_SCOPE[scope],
|
|
150
|
+
source: "session_claim",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return unavailable;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
94
157
|
async function doctor(config, reason) {
|
|
95
158
|
const checks = {
|
|
96
159
|
config: {
|
|
@@ -1510,19 +1573,28 @@ export async function runCommand(argv) {
|
|
|
1510
1573
|
.map((name) => `--${name}`)
|
|
1511
1574
|
.join(", ")}.`, 1, { validFlags: ["--deliver", "--help", "--json", "--profile"] });
|
|
1512
1575
|
}
|
|
1513
|
-
|
|
1576
|
+
const config = await resolveConfig(flagString(parsed, "profile"));
|
|
1577
|
+
const discovery = resolveCommandDiscovery(config);
|
|
1578
|
+
return { help: scopedHelp(HELP, commands, [], discovery) };
|
|
1514
1579
|
}
|
|
1515
1580
|
if (noun === "help" || hasFlag(parsed, "help")) {
|
|
1516
1581
|
const scope = noun === "help" ? parsed.positionals.slice(1) : parsed.positionals;
|
|
1517
|
-
|
|
1582
|
+
const config = await resolveConfig(flagString(parsed, "profile"));
|
|
1583
|
+
const discovery = resolveCommandDiscovery(config);
|
|
1584
|
+
return { help: scopedHelp(HELP, commands, scope, discovery) };
|
|
1518
1585
|
}
|
|
1519
1586
|
validateInvocation(parsed, commands);
|
|
1520
1587
|
if (noun === "agent-context") {
|
|
1521
|
-
const profiles = await
|
|
1588
|
+
const [profiles, config] = await Promise.all([
|
|
1589
|
+
listProfiles(),
|
|
1590
|
+
resolveConfig(flagString(parsed, "profile")),
|
|
1591
|
+
]);
|
|
1592
|
+
const discovery = resolveCommandDiscovery(config);
|
|
1522
1593
|
return agentContext(commands, {
|
|
1523
1594
|
cliVersion: await readCliVersion(),
|
|
1524
1595
|
availableProfiles: profiles.profiles.map((profile) => profile.name),
|
|
1525
1596
|
feedbackUpstreamConfigured: Boolean(process.env.RECESS_CLI_FEEDBACK_ENDPOINT),
|
|
1597
|
+
discovery,
|
|
1526
1598
|
});
|
|
1527
1599
|
}
|
|
1528
1600
|
if (noun === "profile") {
|
|
@@ -4088,6 +4160,60 @@ export async function runCommand(argv) {
|
|
|
4088
4160
|
body,
|
|
4089
4161
|
})));
|
|
4090
4162
|
}
|
|
4163
|
+
if (verb === "complete") {
|
|
4164
|
+
const todoId = positional(parsed, 2, "todo ID");
|
|
4165
|
+
const targetXp = flagNumber(parsed, "xp");
|
|
4166
|
+
if (targetXp === undefined ||
|
|
4167
|
+
!Number.isInteger(targetXp) ||
|
|
4168
|
+
targetXp <= 0) {
|
|
4169
|
+
throw new CliError("invalid_arguments", "--xp must be a positive integer. It is the target total XP credited for this todo; prior awards count toward that total.");
|
|
4170
|
+
}
|
|
4171
|
+
const [currentResponse, detailsResponse] = await Promise.all([
|
|
4172
|
+
api.client.GET("/tutor/browser/todos/{id}/", {
|
|
4173
|
+
params: { path: { id: todoId } },
|
|
4174
|
+
}),
|
|
4175
|
+
api.client.GET("/admin/todos/{id}/details", {
|
|
4176
|
+
params: { path: { id: todoId } },
|
|
4177
|
+
}),
|
|
4178
|
+
]);
|
|
4179
|
+
const current = unwrap(currentResponse);
|
|
4180
|
+
const completionXpAwarded = unwrap(detailsResponse).totalXpAwarded ?? 0;
|
|
4181
|
+
if (current.status === "COMPLETED") {
|
|
4182
|
+
throw new CliError("already_completed", `Todo ${todoId} ("${current.title}") is already completed with ${completionXpAwarded} completion XP credited.`);
|
|
4183
|
+
}
|
|
4184
|
+
const body = {
|
|
4185
|
+
status: "COMPLETED",
|
|
4186
|
+
xpReward: targetXp,
|
|
4187
|
+
expectedUpdatedAt: current.updatedAt,
|
|
4188
|
+
};
|
|
4189
|
+
const newXp = Math.max(0, targetXp - completionXpAwarded);
|
|
4190
|
+
const preview = {
|
|
4191
|
+
action: "complete a todo through the staff reward path and set its total credited XP target",
|
|
4192
|
+
target: {
|
|
4193
|
+
todoId,
|
|
4194
|
+
studentUserId: current.userId,
|
|
4195
|
+
studentName: [current.user.firstName, current.user.lastName]
|
|
4196
|
+
.filter(Boolean)
|
|
4197
|
+
.join(" ") || null,
|
|
4198
|
+
title: current.title,
|
|
4199
|
+
status: current.status,
|
|
4200
|
+
goal: current.goal,
|
|
4201
|
+
},
|
|
4202
|
+
request: body,
|
|
4203
|
+
details: {
|
|
4204
|
+
currentXpReward: current.xpReward,
|
|
4205
|
+
alreadyAwardedXp: completionXpAwarded,
|
|
4206
|
+
targetTotalXp: targetXp,
|
|
4207
|
+
newlyAwardedXp: newXp,
|
|
4208
|
+
configuredCoinReward: current.reward,
|
|
4209
|
+
note: `The XP ledger is delta-guarded: this writes at most ${newXp} new XP so the todo reaches ${targetXp} total. Completion also claims any unawarded portion of the todo's configured ${current.reward}-coin reward, replaces its active completion analysis with a manual-completion record, records completion activity, may complete a linked goal module and its configured rewards, and may notify the parent through the normal completion pipeline.`,
|
|
4210
|
+
},
|
|
4211
|
+
};
|
|
4212
|
+
return previewBoundWrite(parsed, preview, async () => unwrap(await api.client.PATCH("/admin/todos/{id}", {
|
|
4213
|
+
params: { path: { id: todoId } },
|
|
4214
|
+
body,
|
|
4215
|
+
})));
|
|
4216
|
+
}
|
|
4091
4217
|
if (verb === "delete") {
|
|
4092
4218
|
const todoId = positional(parsed, 2, "todo ID");
|
|
4093
4219
|
const current = unwrap(await api.client.GET("/tutor/browser/todos/{id}/", {
|
|
@@ -4156,7 +4282,7 @@ export async function runCommand(argv) {
|
|
|
4156
4282
|
body: targetDueDateISO ? { targetDueDateISO } : {},
|
|
4157
4283
|
})));
|
|
4158
4284
|
}
|
|
4159
|
-
throw new CliError("invalid_arguments", "Use todos create|edit|delete|generate-applet.");
|
|
4285
|
+
throw new CliError("invalid_arguments", "Use todos create|edit|complete|delete|generate-applet.");
|
|
4160
4286
|
}
|
|
4161
4287
|
if (noun === "memories") {
|
|
4162
4288
|
const studentId = flagString(parsed, "student", { required: true });
|
package/dist/command-schema.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
|
-
export const AGENT_CONTEXT_SCHEMA_VERSION = "
|
|
2
|
+
export const AGENT_CONTEXT_SCHEMA_VERSION = "4";
|
|
3
3
|
const BOOLEAN_FLAGS = new Set([
|
|
4
4
|
"all-references",
|
|
5
5
|
"apply",
|
|
@@ -50,6 +50,185 @@ const GLOBAL_FLAG_TYPES = {
|
|
|
50
50
|
profile: "string",
|
|
51
51
|
reason: "string",
|
|
52
52
|
};
|
|
53
|
+
const LOCAL_COMMAND_NOUNS = new Set([
|
|
54
|
+
"--version",
|
|
55
|
+
"agent-context",
|
|
56
|
+
"auth",
|
|
57
|
+
"doctor",
|
|
58
|
+
"feedback",
|
|
59
|
+
"jobs",
|
|
60
|
+
"profile",
|
|
61
|
+
"setup",
|
|
62
|
+
]);
|
|
63
|
+
const AUTHENTICATED_COMMAND_PREFIXES = [
|
|
64
|
+
"village build",
|
|
65
|
+
"village library",
|
|
66
|
+
"village objects",
|
|
67
|
+
"village render",
|
|
68
|
+
];
|
|
69
|
+
const FAMILY_AI_COMMANDS = new Set([
|
|
70
|
+
"content-library search",
|
|
71
|
+
"goal-templates apply",
|
|
72
|
+
"goal-templates apply-starter",
|
|
73
|
+
"goal-templates capture-snapshot",
|
|
74
|
+
"goal-templates create",
|
|
75
|
+
"goal-templates get",
|
|
76
|
+
"goal-templates list",
|
|
77
|
+
"goal-templates snapshot-files",
|
|
78
|
+
"goal-templates validate-spec",
|
|
79
|
+
"goals archive",
|
|
80
|
+
"goals create",
|
|
81
|
+
"goals edit",
|
|
82
|
+
"goals files checkout",
|
|
83
|
+
"goals files init",
|
|
84
|
+
"goals files list",
|
|
85
|
+
"goals files push",
|
|
86
|
+
"goals files read",
|
|
87
|
+
"goals files write",
|
|
88
|
+
"goals list",
|
|
89
|
+
"goals pdf upload",
|
|
90
|
+
"goals queue get",
|
|
91
|
+
"goals queue set",
|
|
92
|
+
"goals unarchive",
|
|
93
|
+
"memories context",
|
|
94
|
+
"memories log",
|
|
95
|
+
"onboarding pairing-code",
|
|
96
|
+
"request get",
|
|
97
|
+
"rocky get",
|
|
98
|
+
"rocky set",
|
|
99
|
+
"skills guardian get",
|
|
100
|
+
"skills guardian list",
|
|
101
|
+
"students list",
|
|
102
|
+
"students schedule",
|
|
103
|
+
"students today",
|
|
104
|
+
"students xp-history",
|
|
105
|
+
"todos create",
|
|
106
|
+
"todos edit",
|
|
107
|
+
]);
|
|
108
|
+
const STAFF_COMMANDS = new Set([
|
|
109
|
+
"cohorts email",
|
|
110
|
+
"cohorts end",
|
|
111
|
+
"cohorts get",
|
|
112
|
+
"cohorts parent-emails",
|
|
113
|
+
"cohorts pause-billing",
|
|
114
|
+
"cohorts resume-billing",
|
|
115
|
+
"cohorts search",
|
|
116
|
+
"enrollments create",
|
|
117
|
+
"enrollments register-cohort",
|
|
118
|
+
"enrollments unregister-cohort",
|
|
119
|
+
"events add",
|
|
120
|
+
"events cancel",
|
|
121
|
+
"events get",
|
|
122
|
+
"events reschedule",
|
|
123
|
+
"events set-status",
|
|
124
|
+
"events take-attendance",
|
|
125
|
+
"goal-templates delete",
|
|
126
|
+
"goal-templates set-metadata",
|
|
127
|
+
"goal-templates versions",
|
|
128
|
+
"goals complete",
|
|
129
|
+
"goals undo-completion",
|
|
130
|
+
"onboarding active-tutors",
|
|
131
|
+
"onboarding attest",
|
|
132
|
+
"onboarding clear-for-cohort",
|
|
133
|
+
"onboarding cohort-options",
|
|
134
|
+
"onboarding comms",
|
|
135
|
+
"onboarding contracts",
|
|
136
|
+
"onboarding doctor",
|
|
137
|
+
"onboarding extract",
|
|
138
|
+
"onboarding family",
|
|
139
|
+
"onboarding generate-summaries",
|
|
140
|
+
"onboarding intake-session",
|
|
141
|
+
"onboarding intake-session-create",
|
|
142
|
+
"onboarding ixl-preview",
|
|
143
|
+
"onboarding kids",
|
|
144
|
+
"onboarding lifecycle-prompts",
|
|
145
|
+
"onboarding mark-reviewed",
|
|
146
|
+
"onboarding next",
|
|
147
|
+
"onboarding orientation-attendance",
|
|
148
|
+
"onboarding orientation-sessions",
|
|
149
|
+
"onboarding provision-ixl",
|
|
150
|
+
"onboarding provision-math-academy",
|
|
151
|
+
"onboarding queue",
|
|
152
|
+
"onboarding readiness",
|
|
153
|
+
"onboarding register-cohort",
|
|
154
|
+
"onboarding remove-ixl",
|
|
155
|
+
"onboarding review",
|
|
156
|
+
"onboarding reviews",
|
|
157
|
+
"onboarding seed-feed",
|
|
158
|
+
"onboarding send-comms",
|
|
159
|
+
"onboarding send-contract",
|
|
160
|
+
"onboarding set-intake",
|
|
161
|
+
"onboarding set-kid-grade",
|
|
162
|
+
"onboarding set-primary-tutor",
|
|
163
|
+
"onboarding starter-coverage",
|
|
164
|
+
"onboarding status",
|
|
165
|
+
"onboarding timeline",
|
|
166
|
+
"payout invoices get",
|
|
167
|
+
"payout invoices list",
|
|
168
|
+
"payout invoices set-status",
|
|
169
|
+
"payout items add",
|
|
170
|
+
"payout items delete",
|
|
171
|
+
"payout items edit",
|
|
172
|
+
"payout payruns list",
|
|
173
|
+
"payout recipients list",
|
|
174
|
+
"registrations approve",
|
|
175
|
+
"registrations deny",
|
|
176
|
+
"school codes get",
|
|
177
|
+
"school codes list",
|
|
178
|
+
"school create",
|
|
179
|
+
"school credit-transactions",
|
|
180
|
+
"school families",
|
|
181
|
+
"school family-search",
|
|
182
|
+
"school kid-slots",
|
|
183
|
+
"school list",
|
|
184
|
+
"school logo-upload",
|
|
185
|
+
"school partner-family",
|
|
186
|
+
"school representatives add",
|
|
187
|
+
"school representatives list",
|
|
188
|
+
"school representatives remove",
|
|
189
|
+
"school representatives search",
|
|
190
|
+
"school update",
|
|
191
|
+
"store-items list",
|
|
192
|
+
"students upload-map-scores",
|
|
193
|
+
"todos complete",
|
|
194
|
+
"todos delete",
|
|
195
|
+
"todos generate-applet",
|
|
196
|
+
"users get",
|
|
197
|
+
"users tier list-tiers",
|
|
198
|
+
]);
|
|
199
|
+
/**
|
|
200
|
+
* Classify the command itself, not the target passed to it. Target-level
|
|
201
|
+
* family, assignment, ownership, and feature-flag checks remain server-owned.
|
|
202
|
+
* A newly added command fails closed to admin-only discovery until its CLI
|
|
203
|
+
* audience is classified here.
|
|
204
|
+
*/
|
|
205
|
+
function commandAccess(path) {
|
|
206
|
+
const key = path.join(" ");
|
|
207
|
+
if (LOCAL_COMMAND_NOUNS.has(path[0]))
|
|
208
|
+
return "local";
|
|
209
|
+
if (AUTHENTICATED_COMMAND_PREFIXES.some((prefix) => key === prefix || key.startsWith(`${prefix} `))) {
|
|
210
|
+
return "authenticated";
|
|
211
|
+
}
|
|
212
|
+
if (FAMILY_AI_COMMANDS.has(key))
|
|
213
|
+
return "family_ai";
|
|
214
|
+
if (STAFF_COMMANDS.has(key))
|
|
215
|
+
return "staff";
|
|
216
|
+
return "admin";
|
|
217
|
+
}
|
|
218
|
+
export function commandIsAvailable(command, scope) {
|
|
219
|
+
if (command.access === "local")
|
|
220
|
+
return true;
|
|
221
|
+
if (!scope)
|
|
222
|
+
return false;
|
|
223
|
+
if (command.access === "authenticated")
|
|
224
|
+
return true;
|
|
225
|
+
if (command.access === "family_ai")
|
|
226
|
+
return scope !== "village_home";
|
|
227
|
+
if (command.access === "staff") {
|
|
228
|
+
return scope === "guide_students" || scope === "full_admin";
|
|
229
|
+
}
|
|
230
|
+
return scope === "full_admin";
|
|
231
|
+
}
|
|
53
232
|
function usageBlocks(help) {
|
|
54
233
|
const lines = help.split("\n");
|
|
55
234
|
const blocks = [];
|
|
@@ -161,6 +340,7 @@ export function buildCommandSchema(help) {
|
|
|
161
340
|
usage,
|
|
162
341
|
flags,
|
|
163
342
|
positionals,
|
|
343
|
+
access: commandAccess(path),
|
|
164
344
|
}));
|
|
165
345
|
});
|
|
166
346
|
}
|
|
@@ -172,20 +352,50 @@ export function findCommandSchema(commands, positionals) {
|
|
|
172
352
|
.filter((command) => pathStartsWith(positionals, command.path))
|
|
173
353
|
.sort((left, right) => right.path.length - left.path.length)[0];
|
|
174
354
|
}
|
|
175
|
-
export function scopedHelp(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const matches = commands.filter((command) => pathStartsWith(command.path, scope));
|
|
355
|
+
export function scopedHelp(_help, commands, scope, context) {
|
|
356
|
+
const matches = commands.filter((command) => pathStartsWith(command.path, scope) &&
|
|
357
|
+
commandIsAvailable(command, context.scope));
|
|
179
358
|
if (matches.length === 0) {
|
|
180
359
|
throw new CliError("unknown_command", `Unknown command scope: ${scope.join(" ")}. Run \`recess --help\` for available commands.`);
|
|
181
360
|
}
|
|
361
|
+
const uniqueUsages = Array.from(new Map(matches.map((command) => [command.usage, command])).values());
|
|
362
|
+
const accessLabel = context.scope
|
|
363
|
+
? context.role
|
|
364
|
+
: context.source === "unavailable"
|
|
365
|
+
? "session unavailable (local and authentication commands only)"
|
|
366
|
+
: "signed out (local and authentication commands only)";
|
|
182
367
|
return [
|
|
183
|
-
|
|
368
|
+
scope.length > 0
|
|
369
|
+
? `recess ${scope.join(" ")} — command help`
|
|
370
|
+
: "recess — safe Recess administration and family AI tools",
|
|
371
|
+
"",
|
|
372
|
+
`Access: ${accessLabel}`,
|
|
373
|
+
"Key: ◇ shared command ◆ admin-only command",
|
|
184
374
|
"",
|
|
185
375
|
"Usage:",
|
|
186
|
-
...
|
|
376
|
+
...uniqueUsages.flatMap((command) => wrapUsage(command.usage, command.access === "admin" ? "◆" : "◇")),
|
|
377
|
+
"",
|
|
378
|
+
"Run `recess help <noun> [verb]` for a focused list.",
|
|
187
379
|
].join("\n");
|
|
188
380
|
}
|
|
381
|
+
function wrapUsage(usage, icon, width = 100) {
|
|
382
|
+
const firstPrefix = ` ${icon} `;
|
|
383
|
+
const continuationPrefix = " ";
|
|
384
|
+
const output = [];
|
|
385
|
+
let current = firstPrefix;
|
|
386
|
+
for (const word of usage.split(/\s+/)) {
|
|
387
|
+
const separator = current.trim().length === 1 ? "" : " ";
|
|
388
|
+
if (current.length > firstPrefix.length &&
|
|
389
|
+
current.length + separator.length + word.length > width) {
|
|
390
|
+
output.push(current);
|
|
391
|
+
current = `${continuationPrefix}${word}`;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
current += `${separator}${word}`;
|
|
395
|
+
}
|
|
396
|
+
output.push(current);
|
|
397
|
+
return output;
|
|
398
|
+
}
|
|
189
399
|
export function validateInvocation(parsed, commands) {
|
|
190
400
|
const command = findCommandSchema(commands, parsed.positionals);
|
|
191
401
|
if (!command) {
|
|
@@ -232,14 +442,18 @@ export function agentContext(commands, options) {
|
|
|
232
442
|
return {
|
|
233
443
|
schema_version: AGENT_CONTEXT_SCHEMA_VERSION,
|
|
234
444
|
cli_version: options.cliVersion,
|
|
445
|
+
session: options.discovery,
|
|
235
446
|
commands: Object.fromEntries(commands
|
|
236
|
-
.filter((command) => command.path[0] !== "--version"
|
|
447
|
+
.filter((command) => command.path[0] !== "--version" &&
|
|
448
|
+
commandIsAvailable(command, options.discovery.scope))
|
|
237
449
|
.map((command) => [
|
|
238
450
|
command.path.join(" "),
|
|
239
451
|
{
|
|
240
452
|
usage: command.usage,
|
|
241
453
|
flags: command.flags,
|
|
242
454
|
positionals: command.positionals,
|
|
455
|
+
access: command.access,
|
|
456
|
+
admin_only: command.access === "admin",
|
|
243
457
|
},
|
|
244
458
|
])),
|
|
245
459
|
global_flags: {
|
package/dist/help.js
CHANGED
|
@@ -344,6 +344,8 @@ Usage:
|
|
|
344
344
|
[--due-date YYYY-MM-DD] [--estimated-minutes N] [--url URL] [--confirm]
|
|
345
345
|
recess [--json] todos edit <todo-id> --patch-file <path/patch.json>
|
|
346
346
|
[--confirm --approval-token TOKEN]
|
|
347
|
+
recess [--json] todos complete <todo-id> --xp N
|
|
348
|
+
[--confirm --approval-token TOKEN]
|
|
347
349
|
recess [--json] todos delete <todo-id>
|
|
348
350
|
[--confirm --approval-token TOKEN]
|
|
349
351
|
recess [--json] todos generate-applet <todo-id> --student <kid-id>
|
package/package.json
CHANGED
|
@@ -33,7 +33,7 @@ recess --json skills admin get recess-admin --all-references --reason "Load staf
|
|
|
33
33
|
|
|
34
34
|
Use `skills guardian list` or `skills admin list` when the needed skill is not obvious. Never substitute one audience for the other. A guardian session cannot fetch the admin catalog; an admin may fetch either catalog.
|
|
35
35
|
|
|
36
|
-
For machine-readable discovery, use `recess --json agent-context`; for a smaller syntax surface, use `recess --json help <noun> [verb]`. Unknown commands, flags, duplicate non-repeatable flags, missing flag values, and extra positional arguments fail explicitly.
|
|
36
|
+
For machine-readable discovery filtered to the current session's locally decoded CLI scope, use `recess --json agent-context`; for a smaller syntax surface, use `recess --json help <noun> [verb]`. These discovery commands make no API request; `auth status` and `doctor` are the live session checks. Human help uses `◇` for shared commands and `◆` for exact-admin commands. Unknown commands, flags, duplicate non-repeatable flags, missing flag values, and extra positional arguments fail explicitly.
|
|
37
37
|
|
|
38
38
|
## Authentication
|
|
39
39
|
|
|
@@ -41,8 +41,9 @@ For machine-readable discovery, use `recess --json agent-context`; for a smaller
|
|
|
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 last 12 hours and
|
|
45
|
-
|
|
44
|
+
- Sessions last 12 hours. `auth status` and `doctor` recheck the live user role and permissions;
|
|
45
|
+
discovery stays local and may reflect the minted scope until the next login.
|
|
46
|
+
- `auth logout` clears the stored session.
|
|
46
47
|
- The default API is production. If `RECESS_CLI_API_ORIGIN` is set, state the non-default origin before acting.
|
|
47
48
|
|
|
48
49
|
Never print or paste session cookies, device codes, or config-file contents.
|