recess-cli 2.3.0 → 2.5.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.
@@ -1,8 +1,10 @@
1
1
  import { CliError } from "./errors.js";
2
- export const AGENT_CONTEXT_SCHEMA_VERSION = "3";
2
+ export const AGENT_CONTEXT_SCHEMA_VERSION = "4";
3
3
  const BOOLEAN_FLAGS = new Set([
4
4
  "all-references",
5
+ "analyzed",
5
6
  "apply",
7
+ "allow-billing",
6
8
  "allow-strand",
7
9
  "archived",
8
10
  "cancel-subscriptions",
@@ -11,6 +13,8 @@ const BOOLEAN_FLAGS = new Set([
11
13
  "confirm-destructive-changes",
12
14
  "disable-applet-follow-ups",
13
15
  "dry-run",
16
+ "force",
17
+ "no-wait",
14
18
  "enable-applet-follow-ups",
15
19
  "full",
16
20
  "help",
@@ -30,7 +34,9 @@ const BOOLEAN_FLAGS = new Set([
30
34
  "spec-only",
31
35
  "starter-only",
32
36
  "supersede",
37
+ "unlink",
33
38
  "visual-only",
39
+ "whole-family",
34
40
  "wait",
35
41
  ]);
36
42
  const REPEATABLE_FLAGS = new Set(["kid", "unassign"]);
@@ -50,6 +56,188 @@ const GLOBAL_FLAG_TYPES = {
50
56
  profile: "string",
51
57
  reason: "string",
52
58
  };
59
+ const LOCAL_COMMAND_NOUNS = new Set([
60
+ "--version",
61
+ "agent-context",
62
+ "auth",
63
+ "doctor",
64
+ "feedback",
65
+ "jobs",
66
+ "profile",
67
+ "setup",
68
+ ]);
69
+ const AUTHENTICATED_COMMAND_PREFIXES = [
70
+ "village build",
71
+ "village library",
72
+ "village objects",
73
+ "village render",
74
+ ];
75
+ const FAMILY_AI_COMMANDS = new Set([
76
+ "content-library search",
77
+ "goal-templates apply",
78
+ "goal-templates apply-starter",
79
+ "goal-templates capture-snapshot",
80
+ "goal-templates create",
81
+ "goal-templates get",
82
+ "goal-templates list",
83
+ "goal-templates snapshot-files",
84
+ "goal-templates validate-spec",
85
+ "goals archive",
86
+ "goals create",
87
+ "goals edit",
88
+ "goals files checkout",
89
+ "goals files init",
90
+ "goals files list",
91
+ "goals files push",
92
+ "goals files read",
93
+ "goals files write",
94
+ "goals list",
95
+ "goals pdf upload",
96
+ "goals queue get",
97
+ "goals queue set",
98
+ "goals unarchive",
99
+ "memories context",
100
+ "memories log",
101
+ "onboarding pairing-code",
102
+ "request get",
103
+ "rocky get",
104
+ "rocky set",
105
+ "skills guardian get",
106
+ "skills guardian list",
107
+ "students list",
108
+ "students schedule",
109
+ "students today",
110
+ "students xp-history",
111
+ "todos create",
112
+ "todos edit",
113
+ ]);
114
+ const STAFF_COMMANDS = new Set([
115
+ "cohorts email",
116
+ "cohorts end",
117
+ "cohorts get",
118
+ "cohorts parent-emails",
119
+ "cohorts pause-billing",
120
+ "cohorts resume-billing",
121
+ "cohorts search",
122
+ "enrollments create",
123
+ "enrollments register-cohort",
124
+ "enrollments unregister-cohort",
125
+ "events add",
126
+ "events cancel",
127
+ "events get",
128
+ "events reschedule",
129
+ "events set-status",
130
+ "events take-attendance",
131
+ "goal-templates delete",
132
+ "goal-templates set-metadata",
133
+ "goal-templates versions",
134
+ "goals complete",
135
+ "goals undo-completion",
136
+ "onboarding active-tutors",
137
+ "onboarding attest",
138
+ "onboarding clear-for-cohort",
139
+ "onboarding cohort-options",
140
+ "onboarding comms",
141
+ "onboarding contracts",
142
+ "onboarding doctor",
143
+ "onboarding extract",
144
+ "onboarding family",
145
+ "onboarding generate-summaries",
146
+ "onboarding intake-session",
147
+ "onboarding intake-session-create",
148
+ "onboarding ixl-preview",
149
+ "onboarding kids",
150
+ "onboarding lifecycle-prompts",
151
+ "onboarding link-math-academy",
152
+ "onboarding mark-reviewed",
153
+ "onboarding next",
154
+ "onboarding orientation-attendance",
155
+ "onboarding orientation-sessions",
156
+ "onboarding provision-ixl",
157
+ "onboarding provision-math-academy",
158
+ "onboarding queue",
159
+ "onboarding readiness",
160
+ "onboarding register-cohort",
161
+ "onboarding remove-ixl",
162
+ "onboarding review",
163
+ "onboarding reviews",
164
+ "onboarding seed-feed",
165
+ "onboarding send-comms",
166
+ "onboarding send-contract",
167
+ "onboarding set-intake",
168
+ "onboarding set-kid-grade",
169
+ "onboarding set-primary-tutor",
170
+ "onboarding starter-coverage",
171
+ "onboarding status",
172
+ "onboarding timeline",
173
+ "payout invoices get",
174
+ "payout invoices list",
175
+ "payout invoices set-status",
176
+ "payout items add",
177
+ "payout items delete",
178
+ "payout items edit",
179
+ "payout payruns list",
180
+ "payout recipients list",
181
+ "registrations approve",
182
+ "registrations deny",
183
+ "school codes get",
184
+ "school codes list",
185
+ "school create",
186
+ "school credit-transactions",
187
+ "school families",
188
+ "school family-search",
189
+ "school kid-slots",
190
+ "school list",
191
+ "school logo-upload",
192
+ "school partner-family",
193
+ "school representatives add",
194
+ "school representatives list",
195
+ "school representatives remove",
196
+ "school representatives search",
197
+ "school update",
198
+ "store-items list",
199
+ "students upload-map-scores",
200
+ "todos complete",
201
+ "todos delete",
202
+ "todos generate-applet",
203
+ "users get",
204
+ "users tier list-tiers",
205
+ "village events",
206
+ "village link-event",
207
+ ]);
208
+ /**
209
+ * Classify the command itself, not the target passed to it. Target-level
210
+ * family, assignment, ownership, and feature-flag checks remain server-owned.
211
+ * A newly added command fails closed to admin-only discovery until its CLI
212
+ * audience is classified here.
213
+ */
214
+ function commandAccess(path) {
215
+ const key = path.join(" ");
216
+ if (LOCAL_COMMAND_NOUNS.has(path[0]))
217
+ return "local";
218
+ if (AUTHENTICATED_COMMAND_PREFIXES.some((prefix) => key === prefix || key.startsWith(`${prefix} `))) {
219
+ return "authenticated";
220
+ }
221
+ if (FAMILY_AI_COMMANDS.has(key))
222
+ return "family_ai";
223
+ if (STAFF_COMMANDS.has(key))
224
+ return "staff";
225
+ return "admin";
226
+ }
227
+ export function commandIsAvailable(command, scope) {
228
+ if (command.access === "local")
229
+ return true;
230
+ if (!scope)
231
+ return false;
232
+ if (command.access === "authenticated")
233
+ return true;
234
+ if (command.access === "family_ai")
235
+ return scope !== "village_home";
236
+ if (command.access === "staff") {
237
+ return scope === "guide_students" || scope === "full_admin";
238
+ }
239
+ return scope === "full_admin";
240
+ }
53
241
  function usageBlocks(help) {
54
242
  const lines = help.split("\n");
55
243
  const blocks = [];
@@ -128,8 +316,13 @@ function positionalBounds(usage) {
128
316
  const rest = usage.replace(/^recess\s+(?:\[--json\]\s+)?/, "");
129
317
  const tokens = rest.split(/\s+/);
130
318
  let count = 0;
319
+ let optional = 0;
131
320
  let variadic = false;
132
321
  for (const token of tokens) {
322
+ if (/^\[[a-z][a-z0-9-]*\]$/.test(token)) {
323
+ optional += 1;
324
+ continue;
325
+ }
133
326
  if (token.startsWith("--") ||
134
327
  token.startsWith("[") ||
135
328
  token.startsWith("(")) {
@@ -139,7 +332,7 @@ function positionalBounds(usage) {
139
332
  if (/^<[^,>]+\.\.\.>$/.test(token))
140
333
  variadic = true;
141
334
  }
142
- return { min: count, max: variadic ? null : count };
335
+ return { min: count, max: variadic ? null : count + optional };
143
336
  }
144
337
  export function buildCommandSchema(help) {
145
338
  return usageBlocks(help).flatMap((usage) => {
@@ -161,6 +354,7 @@ export function buildCommandSchema(help) {
161
354
  usage,
162
355
  flags,
163
356
  positionals,
357
+ access: commandAccess(path),
164
358
  }));
165
359
  });
166
360
  }
@@ -172,20 +366,50 @@ export function findCommandSchema(commands, positionals) {
172
366
  .filter((command) => pathStartsWith(positionals, command.path))
173
367
  .sort((left, right) => right.path.length - left.path.length)[0];
174
368
  }
175
- export function scopedHelp(help, commands, scope) {
176
- if (scope.length === 0)
177
- return help;
178
- const matches = commands.filter((command) => pathStartsWith(command.path, scope));
369
+ export function scopedHelp(_help, commands, scope, context) {
370
+ const matches = commands.filter((command) => pathStartsWith(command.path, scope) &&
371
+ commandIsAvailable(command, context.scope));
179
372
  if (matches.length === 0) {
180
373
  throw new CliError("unknown_command", `Unknown command scope: ${scope.join(" ")}. Run \`recess --help\` for available commands.`);
181
374
  }
375
+ const uniqueUsages = Array.from(new Map(matches.map((command) => [command.usage, command])).values());
376
+ const accessLabel = context.scope
377
+ ? context.role
378
+ : context.source === "unavailable"
379
+ ? "session unavailable (local and authentication commands only)"
380
+ : "signed out (local and authentication commands only)";
182
381
  return [
183
- `recess ${scope.join(" ")} — command help`,
382
+ scope.length > 0
383
+ ? `recess ${scope.join(" ")} — command help`
384
+ : "recess — safe Recess administration and family AI tools",
385
+ "",
386
+ `Access: ${accessLabel}`,
387
+ "Key: ◇ shared command ◆ admin-only command",
184
388
  "",
185
389
  "Usage:",
186
- ...Array.from(new Set(matches.map((command) => ` ${command.usage}`))),
390
+ ...uniqueUsages.flatMap((command) => wrapUsage(command.usage, command.access === "admin" ? "◆" : "◇")),
391
+ "",
392
+ "Run `recess help <noun> [verb]` for a focused list.",
187
393
  ].join("\n");
188
394
  }
395
+ function wrapUsage(usage, icon, width = 100) {
396
+ const firstPrefix = ` ${icon} `;
397
+ const continuationPrefix = " ";
398
+ const output = [];
399
+ let current = firstPrefix;
400
+ for (const word of usage.split(/\s+/)) {
401
+ const separator = current.trim().length === 1 ? "" : " ";
402
+ if (current.length > firstPrefix.length &&
403
+ current.length + separator.length + word.length > width) {
404
+ output.push(current);
405
+ current = `${continuationPrefix}${word}`;
406
+ continue;
407
+ }
408
+ current += `${separator}${word}`;
409
+ }
410
+ output.push(current);
411
+ return output;
412
+ }
189
413
  export function validateInvocation(parsed, commands) {
190
414
  const command = findCommandSchema(commands, parsed.positionals);
191
415
  if (!command) {
@@ -232,14 +456,18 @@ export function agentContext(commands, options) {
232
456
  return {
233
457
  schema_version: AGENT_CONTEXT_SCHEMA_VERSION,
234
458
  cli_version: options.cliVersion,
459
+ session: options.discovery,
235
460
  commands: Object.fromEntries(commands
236
- .filter((command) => command.path[0] !== "--version")
461
+ .filter((command) => command.path[0] !== "--version" &&
462
+ commandIsAvailable(command, options.discovery.scope))
237
463
  .map((command) => [
238
464
  command.path.join(" "),
239
465
  {
240
466
  usage: command.usage,
241
467
  flags: command.flags,
242
468
  positionals: command.positionals,
469
+ access: command.access,
470
+ admin_only: command.access === "admin",
243
471
  },
244
472
  ])),
245
473
  global_flags: {