recess-cli 2.6.1 → 2.8.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,8 +271,11 @@ 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",
278
+ "todos restore",
202
279
  "todos generate-applet",
203
280
  "users get",
204
281
  "users tier list-tiers",
@@ -323,6 +400,11 @@ function positionalBounds(usage) {
323
400
  optional += 1;
324
401
  continue;
325
402
  }
403
+ // `[<url>...]`: zero or more positionals (commands that also accept --file).
404
+ if (/^\[<[^,>]+>\.\.\.\]$/.test(token)) {
405
+ variadic = true;
406
+ continue;
407
+ }
326
408
  if (token.startsWith("--") ||
327
409
  token.startsWith("[") ||
328
410
  token.startsWith("(")) {
@@ -349,13 +431,26 @@ export function buildCommandSchema(help) {
349
431
  },
350
432
  ]));
351
433
  const positionals = positionalBounds(usage);
352
- return commandPath(usage).map((path) => ({
353
- path,
354
- usage,
355
- flags,
356
- positionals,
357
- access: commandAccess(path),
358
- }));
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
+ });
359
454
  });
360
455
  }
361
456
  function pathStartsWith(path, prefix) {
@@ -366,11 +461,32 @@ export function findCommandSchema(commands, positionals) {
366
461
  .filter((command) => pathStartsWith(positionals, command.path))
367
462
  .sort((left, right) => right.path.length - left.path.length)[0];
368
463
  }
369
- 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 = {}) {
370
486
  const matches = commands.filter((command) => pathStartsWith(command.path, scope) &&
371
487
  commandIsAvailable(command, context.scope));
372
488
  if (matches.length === 0) {
373
- 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.`);
374
490
  }
375
491
  const uniqueUsages = Array.from(new Map(matches.map((command) => [command.usage, command])).values());
376
492
  const accessLabel = context.scope
@@ -387,11 +503,17 @@ export function scopedHelp(_help, commands, scope, context) {
387
503
  "Key: ◇ shared command ◆ admin-only command",
388
504
  "",
389
505
  "Usage:",
390
- ...uniqueUsages.flatMap((command) => wrapUsage(command.usage, command.access === "admin" ? "◆" : "◇")),
506
+ ...uniqueUsages.flatMap((command) => wrapUsage(options.remoteOnly ? remoteUsage(command) : command.usage, command.access === "admin" ? "◆" : "◇")),
391
507
  "",
392
- "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] : []),
393
512
  ].join("\n");
394
513
  }
514
+ export function remoteCommands(commands) {
515
+ return commands.filter((command) => command.remoteCapable);
516
+ }
395
517
  function wrapUsage(usage, icon, width = 100) {
396
518
  const firstPrefix = ` ${icon} `;
397
519
  const continuationPrefix = " ";
@@ -410,7 +532,7 @@ function wrapUsage(usage, icon, width = 100) {
410
532
  output.push(current);
411
533
  return output;
412
534
  }
413
- export function validateInvocation(parsed, commands) {
535
+ export function validateInvocation(parsed, commands, options = {}) {
414
536
  const command = findCommandSchema(commands, parsed.positionals);
415
537
  if (!command) {
416
538
  const scope = parsed.positionals.join(" ");
@@ -421,7 +543,14 @@ export function validateInvocation(parsed, commands) {
421
543
  .map((candidate) => candidate.path.join(" "));
422
544
  throw new CliError("unknown_command", `Unknown command: ${scope || "(none)"}.`, 1, suggestions.length > 0 ? { suggestions } : undefined);
423
545
  }
424
- 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)));
425
554
  const unknown = Array.from(parsed.flags.keys()).filter((name) => name !== "version" && !allowed.has(name));
426
555
  if (unknown.length > 0) {
427
556
  const validFlags = Array.from(allowed)
@@ -429,25 +558,27 @@ export function validateInvocation(parsed, commands) {
429
558
  .map((name) => `--${name}`);
430
559
  throw new CliError("unknown_flag", `Unknown flag${unknown.length === 1 ? "" : "s"}: ${unknown
431
560
  .map((name) => `--${name}`)
432
- .join(", ")}.`, 1, { usage: command.usage, validFlags });
561
+ .join(", ")}.`, 1, { usage, validFlags });
433
562
  }
434
563
  const duplicate = Array.from(parsed.occurrences).find(([name, count]) => count > 1 && !command.flags[name]?.repeatable);
435
564
  if (duplicate) {
436
- 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 });
437
566
  }
438
567
  const positionalCount = parsed.positionals.length;
439
568
  if (positionalCount < command.positionals.min ||
440
569
  (command.positionals.max !== null &&
441
570
  positionalCount > command.positionals.max)) {
442
- 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 });
443
572
  }
444
573
  for (const [name, value] of parsed.flags) {
445
- 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);
446
577
  if (type === "boolean" && value !== true) {
447
- 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 });
448
579
  }
449
580
  if (type === "string" && value === true) {
450
- 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 });
451
582
  }
452
583
  }
453
584
  return command;
@@ -458,33 +589,65 @@ export function agentContext(commands, options) {
458
589
  cli_version: options.cliVersion,
459
590
  session: options.discovery,
460
591
  commands: Object.fromEntries(commands
592
+ .filter((command) => !options.remoteOnly || command.remoteCapable)
461
593
  .filter((command) => command.path[0] !== "--version" &&
462
594
  commandIsAvailable(command, options.discovery.scope))
463
595
  .map((command) => [
464
596
  command.path.join(" "),
465
597
  {
466
- usage: command.usage,
467
- 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,
468
612
  positionals: command.positionals,
469
613
  access: command.access,
470
614
  admin_only: command.access === "admin",
615
+ remote_capable: command.remoteCapable,
616
+ oauth_scopes: command.oauthScopes,
471
617
  },
472
618
  ])),
473
- global_flags: {
474
- "--json": { type: "boolean" },
475
- "--profile": { type: "string" },
476
- "--operation-key": { type: "string" },
477
- "--reason": {
478
- type: "string",
479
- required_for: "every Recess API request except authentication",
480
- 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" },
481
640
  },
482
- "--deliver": { type: "enum", values: ["stdout", "file:<path>"] },
483
- "--help": { type: "boolean" },
484
- },
641
+ ...(options.remoteOnly ? { instructions: REMOTE_HELP_NOTES } : {}),
485
642
  available_profiles: options.availableProfiles,
486
- jobs: { commands: ["jobs list", "jobs get", "jobs prune"] },
487
- 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
+ }),
488
651
  };
489
652
  }
490
653
  //# sourceMappingURL=command-schema.js.map