recess-cli 2.0.0 → 2.1.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 +13 -2
- package/dist/api.js +80 -0
- package/dist/cli.js +175 -659
- package/dist/command-schema.js +4 -0
- package/dist/commands/applications.js +336 -0
- package/dist/commands/onboarding.js +623 -0
- package/dist/commands/shared.js +100 -0
- package/dist/help.js +376 -0
- package/package.json +3 -3
- package/skill/recess-cli/SKILL.md +2 -0
- package/skill/recess-cli/agents/version.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Recess CLI
|
|
2
2
|
|
|
3
|
-
`recess-cli` is the typed, agent-friendly command layer for Recess operations. ADMIN accounts
|
|
3
|
+
`recess-cli` is the typed, agent-friendly command layer for Recess operations. ADMIN accounts
|
|
4
|
+
receive the full staff surface; GUARDIAN accounts with `access:ai` receive family-scoped class
|
|
5
|
+
schedules, progress, goals, todos, memories, Rocky configuration, learning research, GoalTemplate,
|
|
6
|
+
and goal-content commands; GUIDE accounts receive that same student surface for the students they
|
|
7
|
+
hold an ACTIVE tutor assignment to—not their wider class roster. KID accounts receive only
|
|
8
|
+
authenticated Village home building. It uses the web-server OpenAPI document, authenticates
|
|
9
|
+
through Recess SSO, emits stable JSON, and refuses live writes until the exact command is rerun with
|
|
10
|
+
`--confirm` plus the preview's operation key after human approval.
|
|
4
11
|
|
|
5
12
|
## Install (no checkout needed)
|
|
6
13
|
|
|
@@ -40,7 +47,7 @@ Create an approved `OAuthClient` row in each Recess environment. This remains a
|
|
|
40
47
|
|
|
41
48
|
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.
|
|
42
49
|
|
|
43
|
-
The browser SSO assertion is exchanged once and discarded. The CLI stores a separate 12-hour signed Recess session 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.
|
|
50
|
+
The browser SSO assertion is exchanged once and discarded. The CLI stores a separate 12-hour signed Recess session 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.
|
|
44
51
|
|
|
45
52
|
```bash
|
|
46
53
|
recess --json auth login
|
|
@@ -156,6 +163,10 @@ current status, purchase count, and lack of feed-publication side effects in the
|
|
|
156
163
|
it refuses non-Village item IDs. After approval, rerun the unchanged command with `--confirm` and
|
|
157
164
|
the preview's `--operation-key`. The
|
|
158
165
|
separate `village models` commands edit the Village island's reusable models and placements.
|
|
166
|
+
`village build cmd` sends one confirmed command through Village's ordinary authenticated command
|
|
167
|
+
dispatcher. `village library search|get`, `village objects list|get`, and `village render` use the
|
|
168
|
+
same scoped Village identity and are own-home-only outside ADMIN. `village worlds
|
|
169
|
+
export|import|promote` moves or promotes data-built worlds through the fixed admin bridge.
|
|
159
170
|
|
|
160
171
|
## Authoring learning content
|
|
161
172
|
|
package/dist/api.js
CHANGED
|
@@ -18,6 +18,7 @@ export class RecessAdminApi {
|
|
|
18
18
|
reason;
|
|
19
19
|
clientTag;
|
|
20
20
|
client;
|
|
21
|
+
villageSessionPromise;
|
|
21
22
|
constructor(config, reason, clientTag = RECESS_CLIENT_CLI) {
|
|
22
23
|
this.config = config;
|
|
23
24
|
this.reason = reason;
|
|
@@ -91,6 +92,28 @@ export class RecessAdminApi {
|
|
|
91
92
|
throw apiError(response.status, body);
|
|
92
93
|
return body;
|
|
93
94
|
}
|
|
95
|
+
async villageCommand(worldId, command) {
|
|
96
|
+
const result = await this.villageScopedRequest("/api/cli/command", {
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: { worldId, command },
|
|
99
|
+
});
|
|
100
|
+
if (result &&
|
|
101
|
+
typeof result === "object" &&
|
|
102
|
+
"ok" in result &&
|
|
103
|
+
result.ok === false) {
|
|
104
|
+
const reason = "reason" in result && typeof result.reason === "string"
|
|
105
|
+
? result.reason
|
|
106
|
+
: "Village rejected the command.";
|
|
107
|
+
throw new CliError("village_command_rejected", reason, 1, result);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
async villageRead(path) {
|
|
112
|
+
if (!path.startsWith("/api/cli/") || path.startsWith("//")) {
|
|
113
|
+
throw new CliError("invalid_arguments", "Village read paths must stay under /api/cli/.");
|
|
114
|
+
}
|
|
115
|
+
return this.villageScopedRequest(path);
|
|
116
|
+
}
|
|
94
117
|
async uploadVillageModel(worldId, file, fileName, metadata) {
|
|
95
118
|
this.requireAuth();
|
|
96
119
|
const form = new FormData();
|
|
@@ -142,6 +165,63 @@ export class RecessAdminApi {
|
|
|
142
165
|
throw apiError(response.status, body);
|
|
143
166
|
return body;
|
|
144
167
|
}
|
|
168
|
+
villageSession() {
|
|
169
|
+
this.requireAuth();
|
|
170
|
+
this.villageSessionPromise ??= (async () => {
|
|
171
|
+
const assertionResult = await this.client.POST("/auth/admin-cli/village-assertion/");
|
|
172
|
+
const signed = unwrap(assertionResult);
|
|
173
|
+
const exchangeResponse = await fetch(new URL("/api/auth/cli/exchange", signed.villageOrigin), {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: { "content-type": "application/json" },
|
|
176
|
+
body: JSON.stringify({ assertion: signed.assertion }),
|
|
177
|
+
});
|
|
178
|
+
const exchangeBody = await readResponseBody(exchangeResponse);
|
|
179
|
+
if (!exchangeResponse.ok) {
|
|
180
|
+
throw apiError(exchangeResponse.status, exchangeBody);
|
|
181
|
+
}
|
|
182
|
+
const token = exchangeBody &&
|
|
183
|
+
typeof exchangeBody === "object" &&
|
|
184
|
+
"token" in exchangeBody &&
|
|
185
|
+
typeof exchangeBody.token === "string"
|
|
186
|
+
? exchangeBody.token
|
|
187
|
+
: null;
|
|
188
|
+
if (!token) {
|
|
189
|
+
throw new CliError("invalid_response", "Village assertion exchange returned no scoped token.");
|
|
190
|
+
}
|
|
191
|
+
return { villageOrigin: signed.villageOrigin, token };
|
|
192
|
+
})();
|
|
193
|
+
return this.villageSessionPromise;
|
|
194
|
+
}
|
|
195
|
+
async villageScopedRequest(path, options = {}) {
|
|
196
|
+
const { villageOrigin, token } = await this.villageSession();
|
|
197
|
+
const response = await fetch(new URL(path, villageOrigin), {
|
|
198
|
+
method: options.method ?? "GET",
|
|
199
|
+
headers: {
|
|
200
|
+
authorization: `Bearer ${token}`,
|
|
201
|
+
...(options.body === undefined
|
|
202
|
+
? {}
|
|
203
|
+
: { "content-type": "application/json" }),
|
|
204
|
+
},
|
|
205
|
+
...(options.body === undefined
|
|
206
|
+
? {}
|
|
207
|
+
: { body: JSON.stringify(options.body) }),
|
|
208
|
+
});
|
|
209
|
+
const body = await readResponseBody(response);
|
|
210
|
+
if (!response.ok)
|
|
211
|
+
throw apiError(response.status, body);
|
|
212
|
+
return body;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function readResponseBody(response) {
|
|
216
|
+
const text = await response.text();
|
|
217
|
+
if (!text)
|
|
218
|
+
return null;
|
|
219
|
+
try {
|
|
220
|
+
return JSON.parse(text);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return text;
|
|
224
|
+
}
|
|
145
225
|
}
|
|
146
226
|
export function unwrap(result) {
|
|
147
227
|
if (!result.response.ok || result.data === undefined) {
|