@cspeach/cli 1.0.0 → 1.1.1

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.
Files changed (100) hide show
  1. package/dist/agent/loop.js +22 -9
  2. package/dist/approvals/op-labels.js +124 -0
  3. package/dist/approvals/render.js +42 -36
  4. package/dist/cli.js +15 -0
  5. package/dist/commands/compact.js +28 -2
  6. package/dist/commands/config-set.js +189 -0
  7. package/dist/commands/config-show.js +20 -0
  8. package/dist/commands/export-audit.js +43 -0
  9. package/dist/commands/help.js +5 -0
  10. package/dist/commands/plan-audit-evidence.js +266 -0
  11. package/dist/commands/plan-audit.js +692 -0
  12. package/dist/commands/plan-chain.js +671 -0
  13. package/dist/commands/plan-continue.js +179 -0
  14. package/dist/commands/plan-gate.js +154 -0
  15. package/dist/commands/plan-resume.js +588 -33
  16. package/dist/config/loader.js +128 -4
  17. package/dist/config/model-defaults.js +14 -0
  18. package/dist/cost/pricing.js +27 -1
  19. package/dist/doctor/checks/system-roles.js +41 -0
  20. package/dist/doctor/run.js +2 -0
  21. package/dist/models/resolve.js +61 -0
  22. package/dist/models/server-config.js +155 -0
  23. package/dist/one-shot.js +25 -3
  24. package/dist/projects/extract-cca.js +3 -1
  25. package/dist/projects/extract-modernize.js +3 -1
  26. package/dist/projects/extract-plan.js +60 -6
  27. package/dist/projects/extract-test-coverage.js +3 -1
  28. package/dist/projects/extract-upgrade.js +3 -1
  29. package/dist/projects/handover-md.js +195 -0
  30. package/dist/projects/index.js +1 -1
  31. package/dist/projects/plan-run.js +137 -13
  32. package/dist/projects/plan-schema.js +73 -0
  33. package/dist/projects/run-lease.js +157 -0
  34. package/dist/projects/save-command.js +26 -15
  35. package/dist/renderer/status-footer.js +22 -12
  36. package/dist/renderer/thinking-heartbeat.js +64 -8
  37. package/dist/renderer/todo-block.js +51 -0
  38. package/dist/renderer/tool-widget.js +37 -0
  39. package/dist/repl/bracketed-paste.js +28 -19
  40. package/dist/repl/builtin-commands.js +5 -0
  41. package/dist/repl/current-transport.js +10 -0
  42. package/dist/repl/history.js +86 -0
  43. package/dist/repl/ink-stdin-guard.js +64 -0
  44. package/dist/repl/mode-ceiling.js +16 -0
  45. package/dist/repl/mode-cycle.js +104 -0
  46. package/dist/repl/post-turn-status.js +24 -4
  47. package/dist/repl/slash-completer.js +5 -0
  48. package/dist/repl.js +954 -83
  49. package/dist/rewind/candidates.js +194 -0
  50. package/dist/rewind/cli.js +137 -0
  51. package/dist/rewind/format.js +27 -0
  52. package/dist/rewind/restore.js +245 -0
  53. package/dist/session/audit-export.js +459 -0
  54. package/dist/session/context-report.js +163 -0
  55. package/dist/session/recap.js +160 -0
  56. package/dist/skill-catalog.js +9 -3
  57. package/dist/skills/bundled-skills.js +71 -78
  58. package/dist/tools/approval.js +115 -7
  59. package/dist/tools/ask-question.js +304 -3
  60. package/dist/tools/extend-model/anchored-insert.js +604 -0
  61. package/dist/tools/extend-model/tool.js +162 -10
  62. package/dist/tools/fiori/fe-extend.js +76 -0
  63. package/dist/tools/fiori/fe-scaffold.js +29 -3
  64. package/dist/tools/fiori/floorplan-map.js +19 -0
  65. package/dist/tools/fiori/samples/data/index.json +13602 -0
  66. package/dist/tools/fiori/samples/data/sources.generated.js +808 -0
  67. package/dist/tools/fiori/samples/loader.js +248 -0
  68. package/dist/tools/fiori/samples/search.js +63 -0
  69. package/dist/tools/fiori/samples/types.js +2 -0
  70. package/dist/tools/fiori/smoke/assertions.js +74 -0
  71. package/dist/tools/fiori/smoke/browser.js +52 -0
  72. package/dist/tools/fiori/smoke/driver.js +89 -0
  73. package/dist/tools/fiori/smoke/freestyle-spec.js +317 -0
  74. package/dist/tools/fiori/smoke/run-smoke.js +149 -0
  75. package/dist/tools/fiori/tools.js +328 -3
  76. package/dist/tools/local-build.js +11 -1
  77. package/dist/tools/sap-read.js +79 -11
  78. package/dist/tools/sap-write.js +24 -4
  79. package/dist/tools/snapshot.js +27 -1
  80. package/dist/tools/subagent/agent_run.js +27 -3
  81. package/dist/tools/todo.js +144 -0
  82. package/dist/ui/app.js +372 -19
  83. package/dist/ui/approval-modal.js +49 -16
  84. package/dist/ui/ask-question-emitter.js +14 -0
  85. package/dist/ui/context-grid.js +108 -0
  86. package/dist/ui/footer.js +109 -30
  87. package/dist/ui/header.js +7 -0
  88. package/dist/ui/line-resolution.js +18 -2
  89. package/dist/ui/rewind-emitter.js +10 -0
  90. package/dist/ui/rewind-panel.js +81 -0
  91. package/dist/ui/sap-state-store.js +1 -0
  92. package/dist/ui/status-line.js +43 -0
  93. package/dist/ui/text-input.js +72 -8
  94. package/dist/ui/todo-emitter.js +25 -0
  95. package/dist/ui/todo-panel.js +64 -0
  96. package/dist/ui/turn-status-emitter.js +50 -4
  97. package/dist/ui/turn-status.js +18 -3
  98. package/dist/ui/widgets/ask-form.js +242 -0
  99. package/dist/ui/widgets/ask-question-modal.js +17 -7
  100. package/package.json +4 -1
@@ -14,16 +14,26 @@
14
14
  *
15
15
  * Module-scope singleton per CLI process. Tests must call clearCurrentTransport()
16
16
  * in beforeEach to avoid leakage across Vitest test files.
17
+ *
18
+ * Task 9 (ux-wave1) — this setter is ALSO the single statusline wire for the
19
+ * `tr <TRKORR>` segment: every path that changes the session transport
20
+ * (sap_transport create success, owning-transport adoption in the create
21
+ * guard, /transport slash command) funnels through set/clear here, so the
22
+ * globalStore push lives HERE rather than scattered across tool handlers.
23
+ * Store writes are harmless in classic mode (nothing subscribes there).
17
24
  */
25
+ import { globalStore } from '../ui/sap-state-store.js';
18
26
  let currentTransport = null;
19
27
  export function setCurrentTransport(trkorr) {
20
28
  currentTransport = trkorr;
29
+ globalStore.update({ transport: trkorr });
21
30
  }
22
31
  export function getCurrentTransport() {
23
32
  return currentTransport;
24
33
  }
25
34
  export function clearCurrentTransport() {
26
35
  currentTransport = null;
36
+ globalStore.update({ transport: null });
27
37
  }
28
38
  /**
29
39
  * Handles the `/transport [<trkorr>|clear]` slash command. Returns user-facing
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Persistent REPL history — the ONE writer shared by every append site:
3
+ * - classic readline path (repl.tsx)
4
+ * - Ink handleSubmit (repl.tsx)
5
+ * - Ink Footer (ui/footer.tsx)
6
+ *
7
+ * File shape: `~/.cspeach/history`, one entry per line, chronological
8
+ * (oldest at top, newest at bottom), appended on every submit; capped at
9
+ * READ time (HISTORY_SIZE in repl.tsx), not write time.
10
+ *
11
+ * Task 8 (ux-wave1) — consecutive-duplicate dedup lives HERE so all
12
+ * append sites share a single guard (CC parity: bash
13
+ * HISTCONTROL=ignoredups). `x, y, x` stores three entries — only
14
+ * back-to-back repeats collapse, never across gaps. Comparison is on the
15
+ * flattened + trimmed entry, against the file tail — so it also holds
16
+ * across session restarts, AND it collapses the Ink path's historical
17
+ * double append (Footer wrote the entry, then repl.tsx's handleSubmit
18
+ * wrote the same submission again — unflattened).
19
+ */
20
+ import * as fs from 'node:fs';
21
+ import * as os from 'node:os';
22
+ import * as path from 'node:path';
23
+ // Resolved lazily (not at module load) so it honours the HOME/USERPROFILE
24
+ // active at call time — lets tests point it at a temp dir.
25
+ export function historyFile() {
26
+ return path.join(os.homedir(), '.cspeach', 'history');
27
+ }
28
+ /** All persisted entries, oldest → newest, trimmed, blank lines dropped. */
29
+ export function readHistoryEntries() {
30
+ try {
31
+ return fs
32
+ .readFileSync(historyFile(), 'utf-8')
33
+ .split('\n')
34
+ .map((l) => l.trim())
35
+ .filter((l) => l.length > 0);
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ }
41
+ /**
42
+ * Flatten a submission into a single history-file entry: `\r\n` becomes
43
+ * ONE space, then every remaining C0 control + DEL becomes a space.
44
+ *
45
+ * Wave1 fast-follow (tab-flatten): newlines were already flattened, but
46
+ * tabs (e.g. from a pasted tab-indented ABAP snippet) survived into the
47
+ * history file. A tab-bearing entry later rendered in the Ctrl+R fuzzy
48
+ * list dodges Ink's truncate-end — widestLine/string-width measure `\t`
49
+ * as 0 cells while the terminal renders up to 8 — re-opening the exact
50
+ * wrapped-row erase-accounting leak Bug 1 fixed (see text-input.tsx
51
+ * maxDisplayWidth doc). Flattening at WRITE time keeps new entries clean;
52
+ * the fuzzy-row render flattens too, covering pre-fix history files.
53
+ */
54
+ export function flattenHistoryEntry(line) {
55
+ // eslint-disable-next-line no-control-regex
56
+ return line.replace(/\r\n/g, ' ').replace(/[\x00-\x1f\x7f]/g, ' ').trim();
57
+ }
58
+ /**
59
+ * Append one entry to the history file.
60
+ * - Submissions are flattened (newlines, tabs, and all other C0
61
+ * controls → ' ') — the file is one-entry-per-line (Task 7 contract)
62
+ * and entries must never carry width-invisible controls (tab-flatten
63
+ * fast-follow; see flattenHistoryEntry).
64
+ * - Skips when the flattened + trimmed entry equals the file's LAST
65
+ * entry (consecutive dedup — trimmed comparison).
66
+ * - Never throws: history persistence failure must not crash the REPL.
67
+ *
68
+ * @returns true when the entry was written, false when skipped.
69
+ */
70
+ export function appendHistoryEntry(line) {
71
+ const entry = flattenHistoryEntry(line);
72
+ if (!entry)
73
+ return false;
74
+ try {
75
+ const entries = readHistoryEntries();
76
+ if (entries.length > 0 && entries[entries.length - 1] === entry)
77
+ return false;
78
+ const file = historyFile();
79
+ fs.mkdirSync(path.dirname(file), { recursive: true });
80
+ fs.appendFileSync(file, entry + '\n', 'utf-8');
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * InkStdinPasteGuard — bracketed-paste interception for the INK path
3
+ * (wave1 live-fix, Bug 2: multiline paste submitted line-by-line on real
4
+ * Windows Terminal).
5
+ *
6
+ * What the live test proved (and Task 7's assumption it disproved):
7
+ * Windows Terminal does NOT deliver a paste as one big chunk with
8
+ * embedded newlines — pasted newlines arrive as `\r`, and a `\r` can
9
+ * land as its OWN stdin chunk. Ink's parseKeypress flags a chunk that is
10
+ * exactly '\r' as key.return (ink/build/parse-keypress.js:145-148), so
11
+ * every pasted line submitted immediately — the garbled multi-dispatch
12
+ * the user saw.
13
+ *
14
+ * Why the interception must happen BEFORE Ink parses:
15
+ * - With DEC 2004 enabled the paste arrives as `\x1b[200~ … \x1b[201~`,
16
+ * but Ink's parser mangles the markers: parse-keypress' fnKeyRe
17
+ * matches `\x1b[200~` with code '[200~' → name undefined, and
18
+ * use-input.js:73-75 strips the leading ESC from the sequence, so the
19
+ * marker would land in the input as literal garbage text ("[200~…").
20
+ * There is no reliable way to reassemble a marker split across chunks
21
+ * from inside useInput.
22
+ * - The reliable seam is the stdin stream itself: Ink accepts a custom
23
+ * `stdin` in its render options and touches exactly this surface on
24
+ * it (ink/build/components/App.js:104-139): `isTTY`, `setEncoding`,
25
+ * `ref`/`unref`, `setRawMode`, `addListener('readable')` + `read()`.
26
+ * A Transform (Duplex) provides the readable side natively; the four
27
+ * TTY-ish members are shimmed here, forwarding to process.stdin.
28
+ *
29
+ * Wiring (repl.tsx Ink branch): enableBracketedPaste() →
30
+ * `process.stdin.pipe(guard)` → `render(…, { stdin: guard })`. The base
31
+ * class (BracketedPasteDecoder with emitVerbatim) strips the markers,
32
+ * accumulates the whole span — including any `\r`-only chunks inside it —
33
+ * and re-emits the paste as ONE chunk with newlines preserved. Ink then
34
+ * hands that single chunk unsplit to useInput, where text-input's Task-7
35
+ * paste branch buffers complete lines via onPasteLines and keeps the
36
+ * tail fragment. NOTHING can submit mid-paste because no `\r`-only chunk
37
+ * ever reaches parseKeypress between the markers.
38
+ *
39
+ * Terminals without bracketed-paste support simply never send markers:
40
+ * the guard is pass-through and behaviour is exactly the pre-fix Task 7
41
+ * path (no regression, same as the classic-mode caveat).
42
+ */
43
+ import { BracketedPasteDecoder } from './bracketed-paste.js';
44
+ export class InkStdinPasteGuard extends BracketedPasteDecoder {
45
+ /**
46
+ * Mirror the REAL stdin's TTY-ness (the base class hardcodes true for
47
+ * readline's sake). Ink gates isRawModeSupported on this — lying about
48
+ * a non-TTY stdin would make Ink call setRawMode on a pipe.
49
+ */
50
+ // eslint-disable-next-line @typescript-eslint/naming-convention
51
+ isTTY = process.stdin.isTTY ?? false;
52
+ constructor() {
53
+ super({ emitVerbatim: true });
54
+ }
55
+ /** Ink calls stdin.ref()/unref() around raw-mode enable/disable. */
56
+ ref() {
57
+ process.stdin.ref?.();
58
+ return this;
59
+ }
60
+ unref() {
61
+ process.stdin.unref?.();
62
+ return this;
63
+ }
64
+ }
@@ -0,0 +1,16 @@
1
+ /** Ordered from most to least restricted. */
2
+ export const WRITE_MODE_ORDER = ['advisory-only', 'approval-gated', 'auto'];
3
+ const ROLE_CEILING = {
4
+ dev: 'auto',
5
+ qas: 'approval-gated',
6
+ prd: 'advisory-only',
7
+ };
8
+ /** The most permissive write mode a system role allows. */
9
+ export function modeCeiling(role) {
10
+ return role === undefined ? 'auto' : ROLE_CEILING[role];
11
+ }
12
+ /** Clamp a mode to the role's ceiling. */
13
+ export function clampToCeiling(mode, role) {
14
+ const ceiling = modeCeiling(role);
15
+ return WRITE_MODE_ORDER.indexOf(mode) <= WRITE_MODE_ORDER.indexOf(ceiling) ? mode : ceiling;
16
+ }
@@ -0,0 +1,104 @@
1
+ import { clampToCeiling, modeCeiling, WRITE_MODE_ORDER, } from './mode-ceiling.js';
2
+ /**
3
+ * Session-scoped effective write mode + cycling core (UX Wave 1, Task 2).
4
+ *
5
+ * The config's `write_mode` is the durable setting; this module layers a
6
+ * SESSION override on top (Shift+Tab-style cycling, lost on exit) and clamps
7
+ * the result to the active system role's ceiling (mode-ceiling.ts):
8
+ *
9
+ * effective = clamp(sessionOverride ?? configMode, roleCeiling)
10
+ *
11
+ * Cycling walks WRITE_MODE_ORDER restricted to the modes the role allows:
12
+ * dev / unset → advisory → gated → auto → advisory …
13
+ * qas → advisory → gated → advisory … (auto not in the cycle — a
14
+ * gated→advisory step is a
15
+ * WRAP, not a cap)
16
+ * prd → pinned at advisory-only; a cycle attempt reports
17
+ * `capped: true` so the caller can render the refusal line.
18
+ *
19
+ * Pure module — no I/O, no config access; callers pass config mode + role in.
20
+ * Module-level state is test-resettable via `setSessionWriteMode(null)`.
21
+ */
22
+ let sessionWriteMode = null;
23
+ /** Session override; null = follow config. Module-level, test-resettable. */
24
+ export function setSessionWriteMode(mode) {
25
+ sessionWriteMode = mode;
26
+ }
27
+ export function getSessionWriteMode() {
28
+ return sessionWriteMode;
29
+ }
30
+ /** cfgMode overlaid by session override, then clamped to the alias role's ceiling. */
31
+ export function getEffectiveWriteMode(cfgMode, role) {
32
+ return clampToCeiling(sessionWriteMode ?? cfgMode, role);
33
+ }
34
+ /**
35
+ * Cycle to the next mode the role allows (wraps). Sets the session override
36
+ * so the new mode takes effect immediately. Returns the new mode AND whether
37
+ * the requested step was capped (for the refusal line).
38
+ */
39
+ export function cycleWriteMode(cfgMode, role) {
40
+ const ceilingIdx = WRITE_MODE_ORDER.indexOf(modeCeiling(role));
41
+ // Modes at or below the ceiling, in cycling order (most→least restricted).
42
+ const allowed = WRITE_MODE_ORDER.slice(0, ceilingIdx + 1);
43
+ if (allowed.length === 1) {
44
+ // prd: pinned — there is nowhere to step. Report capped for the refusal line.
45
+ setSessionWriteMode(allowed[0]);
46
+ return { mode: allowed[0], capped: true };
47
+ }
48
+ const current = getEffectiveWriteMode(cfgMode, role);
49
+ const next = allowed[(allowed.indexOf(current) + 1) % allowed.length];
50
+ setSessionWriteMode(next);
51
+ return { mode: next, capped: false };
52
+ }
53
+ /** Short /mode arguments → WriteMode. */
54
+ const MODE_ARGS = {
55
+ advisory: 'advisory-only',
56
+ gated: 'approval-gated',
57
+ auto: 'auto',
58
+ };
59
+ function announceLine(prev, next) {
60
+ return `mode: ${prev} → ${next} (session only — config unchanged)`;
61
+ }
62
+ function refusalLine(clamped, role, alias) {
63
+ return `mode capped at ${clamped} — sap.${alias ?? '?'}.role = ${role} (policy)`;
64
+ }
65
+ /**
66
+ * /mode command core (UX Wave 1, Task 3). Pure apart from the session
67
+ * override it shares with cycleWriteMode — the REPL builtin and the
68
+ * Shift+Tab / Alt+M keybindings are thin shells over this.
69
+ *
70
+ * - bare / empty arg → cycle (wraps within the role's allowed modes)
71
+ * - 'advisory'|'gated'|'auto' → jump directly (still clamped to the ceiling;
72
+ * a jump above it returns the refusal line and
73
+ * the clamped mode)
74
+ * - anything else → usage line, mode untouched
75
+ *
76
+ * Returns the line to print (caller dims it) and the resulting effective
77
+ * mode (caller pushes it into the SapStateStore so the statusline is live).
78
+ */
79
+ export function runModeCommand(arg, cfgMode, role, alias) {
80
+ const prev = getEffectiveWriteMode(cfgMode, role);
81
+ const normalized = arg?.trim().toLowerCase() ?? '';
82
+ if (normalized === '') {
83
+ const { mode, capped } = cycleWriteMode(cfgMode, role);
84
+ // capped is only ever true with a prd role, so `role!` is safe here.
85
+ if (capped)
86
+ return { line: refusalLine(mode, role, alias), mode };
87
+ return { line: announceLine(prev, mode), mode };
88
+ }
89
+ const target = MODE_ARGS[normalized];
90
+ if (target === undefined) {
91
+ return {
92
+ line: `usage: /mode [advisory|gated|auto] — bare /mode cycles (current: ${prev})`,
93
+ mode: prev,
94
+ };
95
+ }
96
+ const clamped = clampToCeiling(target, role);
97
+ setSessionWriteMode(clamped);
98
+ if (clamped !== target) {
99
+ // Jump above the ceiling — clamp landed us lower than requested. `role!`
100
+ // is safe: without a role the ceiling is 'auto' and nothing clamps.
101
+ return { line: refusalLine(clamped, role, alias), mode: clamped };
102
+ }
103
+ return { line: announceLine(prev, clamped), mode: clamped };
104
+ }
@@ -9,11 +9,16 @@
9
9
  * Ink mode → chunkEmitter.emit('chunk', formatted) — Body's
10
10
  * <Static> commits the line to scrollback
11
11
  *
12
- * The sapStateStore push that used to drive a Header / Sidebar is
13
- * gone (no consumer). All status visibility now lives in the printed
14
- * status line that accompanies each turn — matches CC's pattern.
12
+ * The sapStateStore push that used to drive a Header / Sidebar was
13
+ * removed when the Header died (no consumer). UX Wave 1 Task 9 partially
14
+ * revives it: the Task 4 <StatusLine /> reads dollarSpendToday from the
15
+ * store, and this helper is the one chokepoint that already computes the
16
+ * session cost total after EVERY turn (both classic and Ink paths call
17
+ * it), so the spend wire lives here. The printed status line remains the
18
+ * primary per-turn status surface.
15
19
  */
16
20
  import { computeCost } from '../cost/pricing.js';
21
+ import { globalStore } from '../ui/sap-state-store.js';
17
22
  import { printStatusFooter, formatStatusFooter } from '../renderer/status-footer.js';
18
23
  import { shouldUseInk } from '../renderer/tty.js';
19
24
  export function snapshotTurnStart(session) {
@@ -44,6 +49,14 @@ export function pushPostTurnStatus(params) {
44
49
  cacheRead: session.usage.cache_read_input_tokens,
45
50
  cacheCreate: session.usage.cache_creation_input_tokens ?? 0,
46
51
  });
52
+ // Task 9 (ux-wave1) — statusline spend wire. Honest semantics note: the
53
+ // store field is named `dollarSpendToday` (legacy sidebar naming, kept to
54
+ // avoid rename churn) but the value written is SESSION-scoped — the same
55
+ // sessionCost total shown in the printed status footer, computed from
56
+ // lifetime session.usage at session.model rates (mixed-model turns are an
57
+ // approximation, same caveat as the footer's session line). There is no
58
+ // day-spanning ledger; the statusline shows session spend.
59
+ globalStore.update({ dollarSpendToday: sessionCost });
47
60
  const statusPayload = {
48
61
  sessionId: session.id,
49
62
  sapAlias: selectedAlias,
@@ -60,7 +73,14 @@ export function pushPostTurnStatus(params) {
60
73
  // Push through chunkEmitter so the line lands in Body's <Static>
61
74
  // scrollback alongside the conversation, instead of corrupting
62
75
  // the live Ink frame.
63
- chunkEmitter.emit('chunk', formatStatusFooter(statusPayload));
76
+ //
77
+ // Wave1 fast-follow — variant:'turn' slims the Ink receipt to
78
+ // turn-only facts (skill │ turns │ tok │ $ │ duration): the
79
+ // persistent <StatusLine /> already shows the alias and the session
80
+ // $ total, so the full receipt double-reported both after every
81
+ // turn. Classic mode (and the no-emitter fallback) keeps the full
82
+ // receipt — there is no statusline there.
83
+ chunkEmitter.emit('chunk', formatStatusFooter({ ...statusPayload, variant: 'turn' }));
64
84
  }
65
85
  else {
66
86
  printStatusFooter(statusPayload);
@@ -22,12 +22,17 @@ const BUILTIN_COMMANDS = [
22
22
  '/cancel',
23
23
  '/compact',
24
24
  '/cost',
25
+ '/context',
26
+ '/export',
25
27
  '/exit',
26
28
  '/quit',
27
29
  '/help',
28
30
  '/skills',
31
+ '/mode',
29
32
  '/new',
33
+ '/recap',
30
34
  '/reset',
35
+ '/rewind',
31
36
  '/ui',
32
37
  '/reroute',
33
38
  '/transport',