recess-cli 2.7.0 → 2.9.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,6 +1,7 @@
1
1
  import { CliError } from "./errors.js";
2
2
  export const AGENT_CONTEXT_SCHEMA_VERSION = "4";
3
3
  const BOOLEAN_FLAGS = new Set([
4
+ "allow-possible-duplicate",
4
5
  "all-references",
5
6
  "analyzed",
6
7
  "apply",
@@ -56,6 +57,70 @@ const GLOBAL_FLAG_TYPES = {
56
57
  profile: "string",
57
58
  reason: "string",
58
59
  };
60
+ export const RECESS_MCP_SCOPES = {
61
+ templatesRead: "recess:goal-templates:read",
62
+ templatesWrite: "recess:goal-templates:write",
63
+ appsRead: "recess:apps:read",
64
+ appsWrite: "recess:apps:write",
65
+ studentContextRead: "recess:students:read",
66
+ todoAssign: "recess:todos:assign",
67
+ };
68
+ const REMOTE_COMMAND_SCOPES = new Map([
69
+ ["goal-templates list", [RECESS_MCP_SCOPES.templatesRead]],
70
+ ["goal-templates get", [RECESS_MCP_SCOPES.templatesRead]],
71
+ ["goal-templates validate-spec", [RECESS_MCP_SCOPES.templatesWrite]],
72
+ ["goal-templates create", [RECESS_MCP_SCOPES.templatesWrite]],
73
+ ["skills guardian get", [RECESS_MCP_SCOPES.templatesRead]],
74
+ ["doctor", []],
75
+ ["help", []],
76
+ ["agent-context", []],
77
+ [
78
+ "apps scaffold",
79
+ [RECESS_MCP_SCOPES.appsRead, RECESS_MCP_SCOPES.studentContextRead],
80
+ ],
81
+ ["apps standards", [RECESS_MCP_SCOPES.appsRead]],
82
+ ["apps list", [RECESS_MCP_SCOPES.appsRead]],
83
+ [
84
+ "apps pull",
85
+ [RECESS_MCP_SCOPES.appsRead, RECESS_MCP_SCOPES.studentContextRead],
86
+ ],
87
+ ["apps preview", [RECESS_MCP_SCOPES.appsRead]],
88
+ ["apps status", [RECESS_MCP_SCOPES.appsRead]],
89
+ ["apps validate", [RECESS_MCP_SCOPES.appsWrite]],
90
+ ["apps publish", [RECESS_MCP_SCOPES.appsWrite]],
91
+ ["apps assign", [RECESS_MCP_SCOPES.appsRead, RECESS_MCP_SCOPES.todoAssign]],
92
+ ["students list", [RECESS_MCP_SCOPES.studentContextRead]],
93
+ ["students todos", [RECESS_MCP_SCOPES.studentContextRead]],
94
+ ["students analysis", [RECESS_MCP_SCOPES.studentContextRead]],
95
+ ]);
96
+ const REMOTE_USAGE = new Map([
97
+ ["goal-templates validate-spec", "recess goal-templates validate-spec"],
98
+ [
99
+ "goal-templates create",
100
+ "recess goal-templates create [--coin-amount N] [--confirm --operation-key <key>]",
101
+ ],
102
+ ["apps scaffold", "recess apps scaffold [--for-todo <todo-id>]"],
103
+ [
104
+ "apps validate",
105
+ "recess apps validate (input.appBundle) [--confirm --operation-key <key>]",
106
+ ],
107
+ ["apps pull", "recess apps pull <project-id>"],
108
+ ["apps preview", "recess apps preview <project-id> --version <number>"],
109
+ [
110
+ "apps publish",
111
+ "recess apps publish <project-id> --version <number> [--confirm --operation-key <key>]",
112
+ ],
113
+ [
114
+ "apps assign",
115
+ "recess apps assign <project-id> --student <kid-id> [--due YYYY-MM-DD] [--confirm --operation-key <key>]",
116
+ ],
117
+ ]);
118
+ const REMOTE_CONFIRMATION_COMMANDS = new Set([
119
+ "goal-templates create",
120
+ "apps validate",
121
+ "apps publish",
122
+ "apps assign",
123
+ ]);
59
124
  const LOCAL_COMMAND_NOUNS = new Set([
60
125
  "--version",
61
126
  "agent-context",
@@ -112,6 +177,15 @@ const FAMILY_AI_COMMANDS = new Set([
112
177
  "todos edit",
113
178
  ]);
114
179
  const STAFF_COMMANDS = new Set([
180
+ "apps assign",
181
+ "apps list",
182
+ "apps preview",
183
+ "apps publish",
184
+ "apps pull",
185
+ "apps scaffold",
186
+ "apps standards",
187
+ "apps status",
188
+ "apps validate",
115
189
  "cohorts email",
116
190
  "cohorts end",
117
191
  "cohorts get",
@@ -197,6 +271,8 @@ const STAFF_COMMANDS = new Set([
197
271
  "school update",
198
272
  "store-items list",
199
273
  "students upload-map-scores",
274
+ "students todos",
275
+ "students analysis",
200
276
  "todos complete",
201
277
  "todos delete",
202
278
  "todos restore",
@@ -324,6 +400,11 @@ function positionalBounds(usage) {
324
400
  optional += 1;
325
401
  continue;
326
402
  }
403
+ // `[<url>...]`: zero or more positionals (commands that also accept --file).
404
+ if (/^\[<[^,>]+>\.\.\.\]$/.test(token)) {
405
+ variadic = true;
406
+ continue;
407
+ }
327
408
  if (token.startsWith("--") ||
328
409
  token.startsWith("[") ||
329
410
  token.startsWith("(")) {
@@ -350,13 +431,26 @@ export function buildCommandSchema(help) {
350
431
  },
351
432
  ]));
352
433
  const positionals = positionalBounds(usage);
353
- return commandPath(usage).map((path) => ({
354
- path,
355
- usage,
356
- flags,
357
- positionals,
358
- access: commandAccess(path),
359
- }));
434
+ return commandPath(usage).map((path) => {
435
+ const pathKey = path.join(" ");
436
+ const remoteFlags = {};
437
+ if (pathKey === "apps preview" || pathKey === "apps publish") {
438
+ remoteFlags.version = {
439
+ type: "string",
440
+ repeatable: false,
441
+ required: false,
442
+ };
443
+ }
444
+ return {
445
+ path,
446
+ usage,
447
+ flags: { ...flags, ...remoteFlags },
448
+ positionals,
449
+ access: commandAccess(path),
450
+ remoteCapable: REMOTE_COMMAND_SCOPES.has(pathKey),
451
+ oauthScopes: REMOTE_COMMAND_SCOPES.get(pathKey) ?? [],
452
+ };
453
+ });
360
454
  });
361
455
  }
362
456
  function pathStartsWith(path, prefix) {
@@ -367,11 +461,32 @@ export function findCommandSchema(commands, positionals) {
367
461
  .filter((command) => pathStartsWith(positionals, command.path))
368
462
  .sort((left, right) => right.path.length - left.path.length)[0];
369
463
  }
370
- export function scopedHelp(_help, commands, scope, context) {
464
+ function remoteUsage(command) {
465
+ const path = command.path.join(" ");
466
+ const usage = (REMOTE_USAGE.get(path) ?? command.usage)
467
+ .replace(/^recess\s+(?:\[--json\]\s+)?/, "")
468
+ .replace(" (input.appBundle)", "");
469
+ if (path === "help" || path === "agent-context")
470
+ return usage;
471
+ return `${usage} --reason TEXT`;
472
+ }
473
+ const REMOTE_HELP_NOTES = [
474
+ "Send one command string; omit recess and --json. Quote values containing spaces.",
475
+ "Every command except help and agent-context requires --reason TEXT (1–1024 characters).",
476
+ "For writes, provide the reason in the preview; confirm using only the command, --confirm, and --operation-key.",
477
+ "Supply app source as input.appBundle when calling apps validate.",
478
+ "Guides and admins can create reusable goal templates. First load skills guardian get recess-goal-authoring --all-references; use input.template (the complete template document) for goal-templates validate-spec and create, without --file. Preview the full template, then confirm with only goal-templates create --confirm --operation-key <key>.",
479
+ 'Example: students todos --student "Student Name" --date today --reason "Show today\'s todos at the user\'s request"',
480
+ "Student names must match exactly (ignoring case and extra spaces) within your roster; use an ID to disambiguate.",
481
+ "--date today uses the student's timezone; an explicit YYYY-MM-DD selects that calendar date. Omit --date for recent todos.",
482
+ "Results include the resolved date/timezone and pagination. If hasMore, repeat with --cursor nextCursor and the same filters; use the resolved date when paging today.",
483
+ "Report recorded todo statuses only. These results do not establish account creation, complete activity history, or whether work happened outside Recess.",
484
+ ];
485
+ export function scopedHelp(_help, commands, scope, context, options = {}) {
371
486
  const matches = commands.filter((command) => pathStartsWith(command.path, scope) &&
372
487
  commandIsAvailable(command, context.scope));
373
488
  if (matches.length === 0) {
374
- throw new CliError("unknown_command", `Unknown command scope: ${scope.join(" ")}. Run \`recess --help\` for available commands.`);
489
+ throw new CliError("unknown_command", `Unknown command scope: ${scope.join(" ")}. Run \`${options.remoteOnly ? "help" : "recess --help"}\` for available commands.`);
375
490
  }
376
491
  const uniqueUsages = Array.from(new Map(matches.map((command) => [command.usage, command])).values());
377
492
  const accessLabel = context.scope
@@ -388,11 +503,17 @@ export function scopedHelp(_help, commands, scope, context) {
388
503
  "Key: ◇ shared command ◆ admin-only command",
389
504
  "",
390
505
  "Usage:",
391
- ...uniqueUsages.flatMap((command) => wrapUsage(command.usage, command.access === "admin" ? "◆" : "◇")),
506
+ ...uniqueUsages.flatMap((command) => wrapUsage(options.remoteOnly ? remoteUsage(command) : command.usage, command.access === "admin" ? "◆" : "◇")),
392
507
  "",
393
- "Run `recess help <noun> [verb]` for a focused list.",
508
+ options.remoteOnly
509
+ ? "Run `help <noun> [verb]` for a focused list."
510
+ : "Run `recess help <noun> [verb]` for a focused list.",
511
+ ...(options.remoteOnly ? ["", ...REMOTE_HELP_NOTES] : []),
394
512
  ].join("\n");
395
513
  }
514
+ export function remoteCommands(commands) {
515
+ return commands.filter((command) => command.remoteCapable);
516
+ }
396
517
  function wrapUsage(usage, icon, width = 100) {
397
518
  const firstPrefix = ` ${icon} `;
398
519
  const continuationPrefix = " ";
@@ -411,7 +532,7 @@ function wrapUsage(usage, icon, width = 100) {
411
532
  output.push(current);
412
533
  return output;
413
534
  }
414
- export function validateInvocation(parsed, commands) {
535
+ export function validateInvocation(parsed, commands, options = {}) {
415
536
  const command = findCommandSchema(commands, parsed.positionals);
416
537
  if (!command) {
417
538
  const scope = parsed.positionals.join(" ");
@@ -422,7 +543,14 @@ export function validateInvocation(parsed, commands) {
422
543
  .map((candidate) => candidate.path.join(" "));
423
544
  throw new CliError("unknown_command", `Unknown command: ${scope || "(none)"}.`, 1, suggestions.length > 0 ? { suggestions } : undefined);
424
545
  }
425
- const allowed = new Set([...Object.keys(command.flags), ...GLOBAL_FLAGS]);
546
+ const usage = options.remote ? remoteUsage(command) : command.usage;
547
+ const remoteConfirmation = options.remote && REMOTE_CONFIRMATION_COMMANDS.has(command.path.join(" "));
548
+ const allowed = new Set([
549
+ ...Object.keys(command.flags),
550
+ ...GLOBAL_FLAGS,
551
+ ...(remoteConfirmation ? ["confirm"] : []),
552
+ ].filter((flag) => !options.remote ||
553
+ !["json", "profile", "deliver", "file"].includes(flag)));
426
554
  const unknown = Array.from(parsed.flags.keys()).filter((name) => name !== "version" && !allowed.has(name));
427
555
  if (unknown.length > 0) {
428
556
  const validFlags = Array.from(allowed)
@@ -430,25 +558,27 @@ export function validateInvocation(parsed, commands) {
430
558
  .map((name) => `--${name}`);
431
559
  throw new CliError("unknown_flag", `Unknown flag${unknown.length === 1 ? "" : "s"}: ${unknown
432
560
  .map((name) => `--${name}`)
433
- .join(", ")}.`, 1, { usage: command.usage, validFlags });
561
+ .join(", ")}.`, 1, { usage, validFlags });
434
562
  }
435
563
  const duplicate = Array.from(parsed.occurrences).find(([name, count]) => count > 1 && !command.flags[name]?.repeatable);
436
564
  if (duplicate) {
437
- throw new CliError("duplicate_flag", `--${duplicate[0]} may only be passed once.`, 1, { usage: command.usage });
565
+ throw new CliError("duplicate_flag", `--${duplicate[0]} may only be passed once.`, 1, { usage });
438
566
  }
439
567
  const positionalCount = parsed.positionals.length;
440
568
  if (positionalCount < command.positionals.min ||
441
569
  (command.positionals.max !== null &&
442
570
  positionalCount > command.positionals.max)) {
443
- throw new CliError("invalid_arguments", `Wrong number of positional arguments for ${command.path.join(" ")}.`, 1, { usage: command.usage });
571
+ throw new CliError("invalid_arguments", `Wrong number of positional arguments for ${command.path.join(" ")}.`, 1, { usage });
444
572
  }
445
573
  for (const [name, value] of parsed.flags) {
446
- const type = command.flags[name]?.type ?? GLOBAL_FLAG_TYPES[name];
574
+ const type = command.flags[name]?.type ??
575
+ GLOBAL_FLAG_TYPES[name] ??
576
+ (remoteConfirmation && name === "confirm" ? "boolean" : undefined);
447
577
  if (type === "boolean" && value !== true) {
448
- throw new CliError("invalid_arguments", `--${name} is a boolean flag and does not take a value.`, 1, { usage: command.usage });
578
+ throw new CliError("invalid_arguments", `--${name} is a boolean flag and does not take a value.`, 1, { usage });
449
579
  }
450
580
  if (type === "string" && value === true) {
451
- throw new CliError("invalid_arguments", `--${name} requires a value.`, 1, { usage: command.usage });
581
+ throw new CliError("invalid_arguments", `--${name} requires a value.`, 1, { usage });
452
582
  }
453
583
  }
454
584
  return command;
@@ -459,33 +589,65 @@ export function agentContext(commands, options) {
459
589
  cli_version: options.cliVersion,
460
590
  session: options.discovery,
461
591
  commands: Object.fromEntries(commands
592
+ .filter((command) => !options.remoteOnly || command.remoteCapable)
462
593
  .filter((command) => command.path[0] !== "--version" &&
463
594
  commandIsAvailable(command, options.discovery.scope))
464
595
  .map((command) => [
465
596
  command.path.join(" "),
466
597
  {
467
- usage: command.usage,
468
- flags: command.flags,
598
+ usage: options.remoteOnly ? remoteUsage(command) : command.usage,
599
+ flags: options.remoteOnly &&
600
+ REMOTE_CONFIRMATION_COMMANDS.has(command.path.join(" "))
601
+ ? {
602
+ ...Object.fromEntries(Object.entries(command.flags).filter(([name]) => !["json", "file"].includes(name))),
603
+ confirm: {
604
+ type: "boolean",
605
+ repeatable: false,
606
+ required: false,
607
+ },
608
+ }
609
+ : options.remoteOnly
610
+ ? Object.fromEntries(Object.entries(command.flags).filter(([name]) => !["json", "file"].includes(name)))
611
+ : command.flags,
469
612
  positionals: command.positionals,
470
613
  access: command.access,
471
614
  admin_only: command.access === "admin",
615
+ remote_capable: command.remoteCapable,
616
+ oauth_scopes: command.oauthScopes,
472
617
  },
473
618
  ])),
474
- global_flags: {
475
- "--json": { type: "boolean" },
476
- "--profile": { type: "string" },
477
- "--operation-key": { type: "string" },
478
- "--reason": {
479
- type: "string",
480
- required_for: "every Recess API request except authentication",
481
- max_length: 1024,
619
+ global_flags: options.remoteOnly
620
+ ? {
621
+ "--operation-key": { type: "string" },
622
+ "--reason": {
623
+ type: "string",
624
+ required_for: "every Recess API request",
625
+ max_length: 1024,
626
+ },
627
+ "--help": { type: "boolean" },
628
+ }
629
+ : {
630
+ "--json": { type: "boolean" },
631
+ "--profile": { type: "string" },
632
+ "--operation-key": { type: "string" },
633
+ "--reason": {
634
+ type: "string",
635
+ required_for: "every Recess API request except authentication",
636
+ max_length: 1024,
637
+ },
638
+ "--deliver": { type: "enum", values: ["stdout", "file:<path>"] },
639
+ "--help": { type: "boolean" },
482
640
  },
483
- "--deliver": { type: "enum", values: ["stdout", "file:<path>"] },
484
- "--help": { type: "boolean" },
485
- },
641
+ ...(options.remoteOnly ? { instructions: REMOTE_HELP_NOTES } : {}),
486
642
  available_profiles: options.availableProfiles,
487
- jobs: { commands: ["jobs list", "jobs get", "jobs prune"] },
488
- feedback: { upstream_configured: options.feedbackUpstreamConfigured },
643
+ ...(options.remoteOnly
644
+ ? {}
645
+ : {
646
+ jobs: { commands: ["jobs list", "jobs get", "jobs prune"] },
647
+ feedback: {
648
+ upstream_configured: options.feedbackUpstreamConfigured,
649
+ },
650
+ }),
489
651
  };
490
652
  }
491
653
  //# sourceMappingURL=command-schema.js.map