recess-cli 2.0.0 → 2.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 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 receive the full staff surface; GUARDIAN accounts with `access:ai` receive family-scoped class schedules, progress, goals, todos, memories, Rocky configuration, learning research, GoalTemplate, and goal-content commands; GUIDE accounts receive that same student surface for the students they hold an ACTIVE tutor assignment to — not their wider class roster. It uses the web-server OpenAPI document, authenticates through Recess SSO, emits stable JSON, and refuses live writes until the exact command is rerun with `--confirm` plus the preview's operation key after human approval.
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) {