@maka/maka-cli 5.214.0 → 5.216.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,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.214.0",
3
+ "version": "5.216.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",
@@ -1,5 +1,6 @@
1
1
  import { Command } from './command.js';
2
2
  import { nameablesFor, whereAmI } from '../utilities/nameables.js';
3
+ import { playerVerbs, verbShelf, helpCategories } from '../utilities/verb-index.js';
3
4
  import { normalizeLoose } from '../utilities/fuzzy-match.js';
4
5
  /**
5
6
  * TAB COMPLETION, ANSWERED BY THE ENGINE (f79LLqTerep4nKWcA: "How do I
@@ -71,6 +72,40 @@ export class CompleteCommand extends Command {
71
72
  // long one out loud.
72
73
  return all.filter(n => normalizeLoose(n.name).includes(want));
73
74
  }
75
+ /**
76
+ * THE FIRST WORD IS A VERB, NOT A NAME (yaw2btNX8aNEEF9D3, Maka: "If
77
+ * someone presses 'tab' with nothing in the input box, the first list
78
+ * to reference is the 'command' list").
79
+ *
80
+ * Tab used to answer with NAMEABLES whatever was in the box, so an
81
+ * empty box dumped every icon and body in reach into the Mechanics
82
+ * panel -- the reporter's first sentence -- and a half-typed verb
83
+ * matched names by accident or nothing at all. But a command line has
84
+ * two halves and they have different vocabularies: before the first
85
+ * space you are choosing a VERB, after it you are naming a TARGET.
86
+ *
87
+ * The test is the space, not the emptiness: "ha" and "" are both the
88
+ * verb slot, "hack " and "hack ca" are both the target slot. That is
89
+ * also why `head` is what it is -- with no space there is no head, so
90
+ * a completed verb replaces the whole box.
91
+ */
92
+ verbSlot(raw) {
93
+ return !raw.includes(' ');
94
+ }
95
+ /** The verb list, shaped like nameables so one renderer prints both. */
96
+ verbs() {
97
+ return playerVerbs().map(v => {
98
+ const shelf = verbShelf(v);
99
+ const primary = shelf?.primary;
100
+ return {
101
+ glyph: '›',
102
+ name: v,
103
+ what: shelf
104
+ ? `${shelf.category}${primary && primary !== v ? ` -- same as "${primary}"` : ''}`
105
+ : 'a command',
106
+ };
107
+ });
108
+ }
74
109
  async execute(args = []) {
75
110
  const actor = this.actor;
76
111
  // The client sends the box VERBATIM, so the words rebuild it exactly;
@@ -80,7 +115,20 @@ export class CompleteCommand extends Command {
80
115
  // A VERB ALONE IS NOT A NAME. With nothing typed after the verb the
81
116
  // player wants the roster, which is the `fragment === ''` case below
82
117
  // and handled by matches() returning everything.
83
- const all = nameablesFor(this.scene, actor, this.rooms);
118
+ const onVerb = this.verbSlot(raw);
119
+ // AN EMPTY BOX IS NOT A QUERY, it is "what can I even do?", and the
120
+ // answer is not 180 verbs truncated at 12 -- that is the dump the
121
+ // report is about, wearing different clothes. The SHELVES are the
122
+ // command list at the altitude a person can read: a dozen lines,
123
+ // each one a place to look, and `help <shelf>` opens it.
124
+ if (onVerb && fragment.length === 0) {
125
+ const shelves = helpCategories();
126
+ return [
127
+ `Type a verb, or a letter or two and Tab again. The shelves:`,
128
+ ...shelves.map(c => ` › ${c.title} -- ${c.entries.length} commands ("help ${c.title}")`),
129
+ ].join('\n');
130
+ }
131
+ const all = onVerb ? this.verbs() : nameablesFor(this.scene, actor, this.rooms);
84
132
  if (all.length === 0) {
85
133
  return `Nothing to name from ${whereAmI(actor)}.`;
86
134
  }
@@ -90,6 +138,17 @@ export class CompleteCommand extends Command {
90
138
  return `${COMPLETE_MARKER}${head}${only.name}`;
91
139
  }
92
140
  if (hits.length === 0) {
141
+ // The two slots miss for different reasons and the line has to say
142
+ // which: a verb that does not exist is a different problem from a
143
+ // name that is not HERE, and telling somebody "nothing at the
144
+ // Night Market answers to 'inevntory'" sends them looking round
145
+ // the room for their own pack.
146
+ if (onVerb) {
147
+ return [
148
+ `No command starts with "${fragment}". Try a shelf:`,
149
+ ...helpCategories().map(c => ` › ${c.title} ("help ${c.title}")`),
150
+ ].join('\n');
151
+ }
93
152
  return [
94
153
  `Nothing at ${whereAmI(actor)} answers to "${fragment}". You could name:`,
95
154
  ...all.slice(0, 12).map(n => ` ${n.glyph} ${n.name} -- ${n.what}`),
@@ -1086,5 +1086,57 @@
1086
1086
  // stay guarded...", "Suspicious system-note injection detected" --
1087
1087
  // are all dated 2026-08-24 and predate isOutOfCharacter, which
1088
1088
  // already catches them. Checked, not assumed.)
1089
- export const ENGINE_VERSION = '1.74.0';
1089
+ // 1.74.1 (2026-09-16): A PERSON MAY SAY WHAT THEY KNOW. 1.74.0's
1090
+ // INTERIOR rule leaked onto the SPEECH channel and gagged ordinary
1091
+ // dialogue -- an NPC saying "I know exactly what you mean" out loud
1092
+ // was refused as mind-reading. Caught before anyone played it, by
1093
+ // reading judgeNarration's other caller rather than by a report.
1094
+ // THE TRAP IS WORTH RECORDING. Dialogue has run through this judge
1095
+ // since KwiMKNMN2e8KBqTY7, and it was safe from the agency rules for
1096
+ // an INVISIBLE reason: that caller passes an empty cast, so there is
1097
+ // no subject to resolve and CONTACT/COMPELLED_MOVEMENT can never fire.
1098
+ // A real guarantee, nowhere stated. INTERIOR needs no subject, so it
1099
+ // walked straight through. judgeNarration now takes an explicit
1100
+ // `channel` ('beat' by default, 'speech' from the dialogue call) and
1101
+ // the beat-only rules are gated on it, so the next rule added here has
1102
+ // to decide which channel it belongs to instead of inheriting an
1103
+ // accident. The distinction is the rule itself: a character may SAY
1104
+ // what they know; they may not have it NARRATED at a player who cannot
1105
+ // hear it.
1106
+ // 1.75.0 (2026-09-16): TAB COMPLETES A VERB BEFORE THE SPACE AND A
1107
+ // TARGET AFTER IT (yaw2btNX8aNEEF9D3, Maka: "the 'tab' key shouldn't
1108
+ // SHOW all the things in the room in the Mechanics panel ... If
1109
+ // someone presses 'tab' with nothing in the input box, the first list
1110
+ // to reference is the 'command' list. Then, tab can cycle through
1111
+ // entities. Be careful though, if I can't percieve the thing, it
1112
+ // shouldn't be on the tab list."). Four asks, four changes.
1113
+ // - THE SPACE IS THE BOUNDARY. `complete` answered with NAMEABLES
1114
+ // whatever was in the box, so an empty box dumped every icon and
1115
+ // body in reach into the panel and a half-typed verb matched entity
1116
+ // names by accident. A command line has two halves with two
1117
+ // vocabularies; before the first space you are choosing a verb.
1118
+ // - AN EMPTY BOX GETS THE SHELVES, not 180 verbs truncated at 12 --
1119
+ // that is the same dump wearing different clothes. A dozen lines,
1120
+ // each a place to look, each with its "help <shelf>".
1121
+ // - THE VERB TABLE MOVED OUT OF game.ts to utilities/verb-index.ts, so
1122
+ // Tab and the help screen read ONE list. A second hand-maintained
1123
+ // copy of ~180 verbs is a copy that drifts. Aliases complete too:
1124
+ // somebody typing "inventor" is reaching for `inventory`, and it is
1125
+ // an alias of `inv`. Role-aware, so Tab never offers a play-tester
1126
+ // verb to an account the server will 403.
1127
+ // - TAB WAS A FREE SEARCH. nameablesFor read the room's WHOLE
1128
+ // inventory, so it offered loot nobody had found -- while `look` and
1129
+ // `take` filter the same set and would then refuse to show or hand
1130
+ // over the very name Tab had just typed for you. It now uses take.ts's
1131
+ // gate exactly: cased the room, or the thing arrived in the open in
1132
+ // front of you. Off-plane items go too, for take.ts's reason.
1133
+ // - AND TAB AGAIN WALKS THE LIST (utilities/tab-cycle.ts, CLI only for
1134
+ // now -- cycling is a property of an input box and only the CLI has
1135
+ // one). Pure model, so a test can press Tab four times without a
1136
+ // terminal. Continuation is an EXACT match on what the last press
1137
+ // wrote: one typed letter ends the walk and asks the engine again,
1138
+ // because that is what narrowing means. One candidate is not a
1139
+ // cycle -- a key that rewrites the box to what it already holds
1140
+ // looks broken.
1141
+ export const ENGINE_VERSION = '1.75.0';
1090
1142
  //# sourceMappingURL=engine-version.js.map
@@ -60,7 +60,8 @@ import { NoteCommand } from './commands/note.js';
60
60
  import { BacklogCommand } from './commands/backlog.js';
61
61
  // Whether this account may actually USE the backlog -- gates what the
62
62
  // help index offers, never what the verb accepts (item D).
63
- import { isPlaytester, probePlaytesterStanding } from './utilities/playtester.js';
63
+ import { probePlaytesterStanding } from './utilities/playtester.js';
64
+ import { HELP_CATEGORIES, HIDDEN_VERBS, PLAYTESTER_VERBS, helpCategories } from './utilities/verb-index.js';
64
65
  import { RolesCommand } from './commands/roles.js';
65
66
  import { ReviewCommand } from './commands/review.js';
66
67
  import { SinCommand } from './commands/sin.js';
@@ -6747,46 +6748,13 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6747
6748
  * the renderer is ENTITLED to make -- the table was what lied.
6748
6749
  * help-taxonomy.test.ts now holds that line for the whole table.
6749
6750
  */
6750
- static HELP_CATEGORIES = [
6751
- { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach'], ['run'], ['sprint'], ['climb', 'mantle', 'scale', 'clamber'], ['descend'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow'], ['unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
6752
- { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['loot'], ['drop'], ['give'], ['store'], ['open'], ['close'], ['put'], ['equip'], ['unequip'], ['fit'], ['unfit'], ['brandish', 'draw'], ['holster'], ['reload'], ['use'], ['read']] },
6753
- { title: 'combat', entries: [['attack'], ['kill'], ['subdue', 'stun', 'knock'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['aim'], ['defend'], ['delay'], ['end-turn', 'endturn', 'done', 'pass'], ['initiative', 'tracker', 'turn'], ['nudge'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
6754
- // The party layer: everyone who walks (or flies, or manifests) at
6755
- // your side answers to these.
6756
- // "players" folded back under crew after playtesting ("crew
6757
- // players" / "crew street"); the bare verb still answers quietly.
6758
- { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
6759
- { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
6760
- { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['force', 'brute', 'smash'], ['mark', 'marks'], ['unmark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
6761
- // The Emerged get their own shelf (player request: "there MUST be
6762
- // resonance" -- matrix is the place, Resonance is the talent).
6763
- // `sustain` shelves here and not under magic: the command is
6764
- // technomancer-gated (a sprite task, SR5 p.256) and refuses anyone
6765
- // else, so the Emerged shelf is where its audience looks.
6766
- { title: 'resonance', entries: [['compile'], ['decompile'], ['thread'], ['unravel'], ['register'], ['standby'], ['sustain'], ['sprites']] },
6767
- { title: 'social', entries: [['say'], ['shout'], ['emote', 'me'], ['tell', 'talk-to'], ['call'], ['message'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
6768
- { title: 'street', entries: [['abandon'], ['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['garage'], ['pay']] },
6769
- { title: 'barriers', entries: [['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
6770
- { title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['sin', 'sins', 'papers'], ['initiate', 'submerge'], ['install', 'chrome']] },
6771
- { title: 'session', entries: [['help'], ['hints'], ['time', 'clock'], ['note'], ['backlog'], ['review'], ['roles'], ['clear'], ['quit', 'exit']] },
6772
- ];
6773
- /** Registered but deliberately UNLISTED: reachable as subcommands or
6774
- * muscle-memory ("players" folded back under crew after playtesting
6775
- * -- "crew players" is the advertised path; the bare verb still
6776
- * answers). Without this, the uncategorized sweep resurrects them
6777
- * in an "other" bucket. */
6778
- // 'first' and 'apply' exist ONLY to catch the two-word phrasings
6779
- // ("first aid", "apply first aid") that used to die at the parser. As
6780
- // bare words they mean nothing, so they are claimed but not advertised
6781
- // -- `heal` is the verb the help screen teaches.
6782
- // `complete` is what Tab sends (commands/complete.ts) -- a question the
6783
- // input box asks, not a verb anyone types, so it stays out of help.
6784
- static HIDDEN_VERBS = ['players', 'first', 'apply', 'complete'];
6785
- /** Session verbs that only a play-tester (or admin) can actually use
6786
- * -- the server role-gates both and 403s everyone else. Listed in
6787
- * help ONLY for a confirmed play-tester; see helpCategories. `roles`
6788
- * rides along because it is admin-only, a strictly narrower group. */
6789
- static PLAYTESTER_VERBS = new Set(['backlog', 'review', 'roles']);
6751
+ // THE VERB TABLES MOVED TO utilities/verb-index.ts. Tab needs the
6752
+ // same list the help screen prints (yaw2btNX8aNEEF9D3), and two
6753
+ // hand-maintained copies of ~180 verbs is a copy that drifts. These
6754
+ // aliases keep every call site below reading as it did.
6755
+ static HELP_CATEGORIES = HELP_CATEGORIES;
6756
+ static HIDDEN_VERBS = HIDDEN_VERBS;
6757
+ static PLAYTESTER_VERBS = PLAYTESTER_VERBS;
6790
6758
  /** Descriptions for the verbs handled inline in handleInput() rather than registered. */
6791
6759
  static BUILTIN_DESCRIPTIONS = {
6792
6760
  help: 'This overview. "help <category>" for one group with full descriptions; "help <command>" for one command.',
@@ -6810,12 +6778,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6810
6778
  * and why the verb itself stays registered and usable regardless.
6811
6779
  */
6812
6780
  helpCategories() {
6813
- if (isPlaytester())
6814
- return Game.HELP_CATEGORIES;
6815
- return Game.HELP_CATEGORIES.map(c => c.title !== 'session' ? c : {
6816
- ...c,
6817
- entries: c.entries.filter(e => !Game.PLAYTESTER_VERBS.has(e[0])),
6818
- });
6781
+ return helpCategories();
6819
6782
  }
6820
6783
  getHelpMessage(args = []) {
6821
6784
  const registered = Object.keys(CommandFactory.getAllRegisteredCommands());
@@ -2515,6 +2515,13 @@ Your objective is to drive the story line.`}
2515
2515
  const placeVerdict = judgeNarration(said, this.name, [], {
2516
2516
  here: this.currentLocation?.name,
2517
2517
  worldRooms: Object.values(this._scene?.getRooms?.() ?? {}).map(r => r.name),
2518
+ // SAYS WHICH CHANNEL THIS IS, rather than relying on the empty
2519
+ // `others` above to keep the beat-only rules from firing. That
2520
+ // guarantee held for the agency rules, which need a subject to
2521
+ // resolve -- and broke the moment a rule arrived that does not
2522
+ // (INTERIOR), refusing an ordinary spoken "I know exactly what
2523
+ // you mean" as mind-reading. A person may SAY what they know.
2524
+ channel: 'speech',
2518
2525
  });
2519
2526
  if (placeVerdict.refused) {
2520
2527
  this.logger.write(`${this.name} speech refused (claimed to be elsewhere): ${said.slice(0, 160)}`);
@@ -8,6 +8,7 @@ import { getArchetype, outfitArchetype } from './archetypes.js';
8
8
  import { attachLineEditor, attachArrowMovement } from './utilities/line-editor.js';
9
9
  import { attachInputHistory } from './utilities/input-history.js';
10
10
  import { COMPLETE_MARKER } from './commands/complete.js';
11
+ import { startCycle, advance, continues } from './utilities/tab-cycle.js';
11
12
  import { TERMINAL_RESET_SEQUENCE } from './utilities/terminal-reset.js';
12
13
  import { readSave } from './utilities/persistence.js';
13
14
  import { requireCatalog, CatalogUnavailableError } from './utilities/catalog.js';
@@ -328,6 +329,10 @@ export async function main(playerName, sceneSeed, options) {
328
329
  // asked for in its place is a way to know which icon you are about
329
330
  // to hit. Bound on the input box because that's where focus lives
330
331
  // during play.
332
+ // The walk the previous Tab set up, if any. Lives out here so it
333
+ // survives between presses and nowhere else, which is the whole of
334
+ // its scope: no cycle outlives the input box it belongs to.
335
+ let tabCycle;
331
336
  game.inputBox.key(['tab'], () => {
332
337
  // Blessed's textarea reader appends the literal '\t' to the box
333
338
  // value on this same keypress (listener order isn't guaranteed
@@ -355,9 +360,26 @@ export async function main(playerName, sceneSeed, options) {
355
360
  // So: send the box, let the engine answer, on both run shapes.
356
361
  void (async () => {
357
362
  const box = game.inputBox.getValue().replace(/\t/g, '');
363
+ // TAB AGAIN TAKES THE NEXT ONE (yaw2btNX8aNEEF9D3, Maka: "Then,
364
+ // tab can cycle through entities"). No round trip and no second
365
+ // copy of the list in the panel: the names from the previous
366
+ // press are still good, and walking them is a property of this
367
+ // input box rather than a question for the engine. The model is
368
+ // in utilities/tab-cycle.ts, pure, so a test can press Tab four
369
+ // times without a terminal. `continues` is an exact match on
370
+ // what the last press wrote, so one typed letter ends the walk
371
+ // and asks the engine again -- which is what narrowing means.
372
+ if (continues(tabCycle, box)) {
373
+ const { box: filled, next } = advance(tabCycle);
374
+ tabCycle = next;
375
+ game.inputBox.setValue(filled);
376
+ screen.render();
377
+ return;
378
+ }
358
379
  const res = await game.handleInput(`complete ${box}`);
359
380
  if (!res)
360
381
  return;
382
+ tabCycle = startCycle(box, res);
361
383
  const lines = res.split('\n');
362
384
  // THE MARKER LINE refills the box (see commands/complete.ts).
363
385
  // Everything else is the candidate list, and goes to the ticker
@@ -365,12 +387,19 @@ export async function main(playerName, sceneSeed, options) {
365
387
  if (lines[0]?.startsWith(COMPLETE_MARKER)) {
366
388
  const filled = lines[0].slice(COMPLETE_MARKER.length);
367
389
  game.inputBox.setValue(filled);
390
+ // The engine may have grown the box to the common head; the
391
+ // cycle has to compare against what is ACTUALLY there or the
392
+ // next press would look like a player keystroke.
393
+ if (tabCycle)
394
+ tabCycle = { ...tabCycle, box: filled };
368
395
  lines.shift();
369
396
  }
370
397
  for (const l of lines) {
371
398
  if (l.trim().length > 0)
372
399
  gameLog.meta(l, 'white');
373
400
  }
401
+ if (tabCycle)
402
+ gameLog.meta(` (Tab again walks these.)`, 'white');
374
403
  screen.render();
375
404
  })();
376
405
  });
@@ -65,7 +65,26 @@ function meatNameables(actor) {
65
65
  for (const device of room.openableDevices()) {
66
66
  push(out, seen, { glyph: '▤', name: device.name, what: 'a device' });
67
67
  }
68
+ // WHAT YOU HAVE ACTUALLY SEEN (yaw2btNX8aNEEF9D3, Maka: "if I can't
69
+ // percieve the thing, it shouldn't be on the tab list").
70
+ //
71
+ // This read the room's whole inventory, so Tab offered loot nobody had
72
+ // found yet -- and `look` (look.ts) and `take` (take.ts) both filter
73
+ // the same set and would have refused to show or hand over the very
74
+ // name Tab had just typed for you. Tab was a free `search`.
75
+ //
76
+ // The gate is take.ts's, verbatim in meaning: having cased the room
77
+ // reveals everything in it, and until then you know only what arrived
78
+ // in the open in front of you (Room.plainSightItems -- a dead NPC's
79
+ // scattered kit, a witnessed drop). Off-plane items are dropped for
80
+ // the same reason take.ts drops them: a matrix file on the floor is
81
+ // not a thing a body can pick up.
82
+ const cased = room.searchedBy.has(actor.name);
68
83
  for (const item of room.getItemsForActor(actor)) {
84
+ if (item.plane !== actor.plane)
85
+ continue;
86
+ if (!cased && !room.plainSightItems.has(item.name.toLowerCase()))
87
+ continue;
69
88
  push(out, seen, { glyph: '◇', name: item.name, what: 'on the ground' });
70
89
  }
71
90
  // WHAT YOU ARE CARRYING is nameable too -- "equip", "drop", "reload
@@ -309,6 +309,21 @@ function aimedAtAPerson(text, matches, subject) {
309
309
  * @param opts.holding who this NPC already has a grapple on, if anyone --
310
310
  * narration about a hold the dice already settled is a
311
311
  * description, not a claim.
312
+ * @param opts.channel which half of the reply this is. 'beat' (the
313
+ * default) is exposition and gets every rule. 'speech'
314
+ * is a line of DIALOGUE and gets the place rule only.
315
+ *
316
+ * WHY THE CHANNEL IS A PARAMETER AND NOT AN ACCIDENT. Dialogue has been
317
+ * passing through here since KwiMKNMN2e8KBqTY7 ("Whisper ... says he
318
+ * is"), and it survived the agency rules only because its caller passes
319
+ * an EMPTY `others` -- with nobody to resolve as the subject, CONTACT
320
+ * and COMPELLED_MOVEMENT can never fire. That is a real guarantee but an
321
+ * invisible one, and the INTERIOR rule broke it the moment it was
322
+ * written: interiority needs no subject, so a perfectly ordinary spoken
323
+ * "I know exactly what you mean" was refused as mind-reading. A person
324
+ * is allowed to say what they know. They are not allowed to have it
325
+ * NARRATED at a player who cannot hear it -- which is the whole
326
+ * distinction, and it now has a name in the signature.
312
327
  */
313
328
  export function judgeNarration(line, speaker, others, opts = {}) {
314
329
  const t = line.replace(/^[\s│|>*-]+/, '').trim();
@@ -327,8 +342,9 @@ export function judgeNarration(line, speaker, others, opts = {}) {
327
342
  }
328
343
  }
329
344
  // ALSO BEFORE the agency rules, and for the same reason: this is about
330
- // the SPEAKER's own head, so there is no subject to resolve.
331
- if (INTERIOR.test(t)) {
345
+ // the SPEAKER's own head, so there is no subject to resolve -- which
346
+ // is exactly why it must be told to stay off the speech channel.
347
+ if (opts.channel !== 'speech' && INTERIOR.test(t)) {
332
348
  return {
333
349
  refused: true,
334
350
  subject: speaker,
@@ -0,0 +1,100 @@
1
+ /**
2
+ * TAB AGAIN TAKES THE NEXT ONE (yaw2btNX8aNEEF9D3, Maka: "Then, tab can
3
+ * cycle through entities").
4
+ *
5
+ * The engine answers a Tab with the candidates and, when they share a
6
+ * head, fills the box as far as they agree (commands/complete.ts). That
7
+ * is the shell behaviour everyone has in their fingers for the FIRST
8
+ * press. The second press is where a shell offers the list and a good
9
+ * one walks it, and the report asks for the walk.
10
+ *
11
+ * WHY A PURE MODEL AND NOT A FEW LINES IN ui.ts. The whole feature is
12
+ * "what counts as the same Tab?" -- and that is a state question with
13
+ * exactly the shape that rots in an event handler: a player who types a
14
+ * letter, or recalls a command from history, or clears the box, must
15
+ * start a NEW cycle, and a player who just presses Tab again must not.
16
+ * Wiring belongs in ui.ts; the decision belongs here, where a test can
17
+ * press Tab four times in a row without a terminal.
18
+ *
19
+ * DELIBERATELY CLIENT-SIDE, AND CLI-ONLY FOR NOW. The engine stays
20
+ * stateless -- it answers the same question the same way however many
21
+ * times it is asked, which is what lets three clients share it. Cycling
22
+ * is a property of an input box, and only the CLI has one of these.
23
+ */
24
+ /** A name the engine offered, as printed in a candidate line. */
25
+ const CANDIDATE_LINE = /^\s{2}\S+\s+(.+?)\s+--\s/;
26
+ /**
27
+ * The names out of a `complete` reply.
28
+ *
29
+ * Reads the LIST lines, not the marker line: the marker is what the box
30
+ * already holds. A reply with no list (one match, or a refusal) yields
31
+ * nothing and ends any cycle, which is correct -- there is nothing to
32
+ * walk.
33
+ */
34
+ export function candidatesIn(reply) {
35
+ const out = [];
36
+ for (const line of reply.split('\n')) {
37
+ const m = CANDIDATE_LINE.exec(line);
38
+ if (m?.[1])
39
+ out.push(m[1]);
40
+ }
41
+ return out;
42
+ }
43
+ /**
44
+ * Split a box into the command so far and the fragment being typed --
45
+ * the SAME rule commands/complete.ts uses, deliberately duplicated
46
+ * rather than imported so the two can be compared by eye. A trailing
47
+ * space means an empty fragment.
48
+ */
49
+ export function splitBox(raw) {
50
+ if (raw.length === 0 || raw.endsWith(' '))
51
+ return { head: raw, fragment: '' };
52
+ const cut = raw.lastIndexOf(' ');
53
+ return cut < 0 ? { head: '', fragment: raw } : { head: raw.slice(0, cut + 1), fragment: raw.slice(cut + 1) };
54
+ }
55
+ /**
56
+ * Begin a cycle from a reply, or return undefined when there is nothing
57
+ * to walk.
58
+ *
59
+ * ONE CANDIDATE IS NOT A CYCLE: the engine already filled the box with
60
+ * it, and a "cycle" of length one that rewrites the box on every press
61
+ * is just a key that appears to do nothing.
62
+ */
63
+ export function startCycle(boxWhenPressed, reply) {
64
+ const names = candidatesIn(reply);
65
+ if (names.length < 2)
66
+ return undefined;
67
+ const { head } = splitBox(boxWhenPressed);
68
+ // `box` SEEDS TO THE BOX AS PRESSED, not to ''. The engine grows the
69
+ // box to the candidates' common head only when there IS one -- "hack
70
+ // s" with two s-names, but not "hack " with three unrelated ones. When
71
+ // it does not, nothing rewrites the box, so a cycle remembering ''
72
+ // would fail its own sameness test on the very next press and go back
73
+ // to the engine instead of walking. The caller overwrites this when a
74
+ // marker line did change the box.
75
+ return { head, names, index: -1, box: boxWhenPressed };
76
+ }
77
+ /**
78
+ * The next box contents, and the cycle to remember.
79
+ *
80
+ * Wraps at the end on purpose -- a walk you cannot get back to the start
81
+ * of is a walk you have to abandon by retyping.
82
+ */
83
+ export function advance(cycle) {
84
+ const index = (cycle.index + 1) % cycle.names.length;
85
+ const box = `${cycle.head}${cycle.names[index]}`;
86
+ return { box, next: { ...cycle, index, box } };
87
+ }
88
+ /**
89
+ * Is this press a continuation of `cycle`?
90
+ *
91
+ * The box must be EXACTLY what the last press wrote. Not "starts with",
92
+ * not case-insensitive: a player who typed one more letter is narrowing
93
+ * their search and wants a fresh answer from the engine, and a cycle
94
+ * that swallowed that keystroke would be the most annoying possible
95
+ * version of this feature.
96
+ */
97
+ export function continues(cycle, box) {
98
+ return cycle !== undefined && cycle.box === box && cycle.names.length > 1;
99
+ }
100
+ //# sourceMappingURL=tab-cycle.js.map
@@ -0,0 +1,99 @@
1
+ import { isPlaytester } from './playtester.js';
2
+ /**
3
+ * THE VERBS A PLAYER MAY TYPE, in one place.
4
+ *
5
+ * This table WAS a private static on Game, read only by the help screen.
6
+ * Tab needed the same list (yaw2btNX8aNEEF9D3, Maka: "If someone presses
7
+ * 'tab' with nothing in the input box, the first list to reference is
8
+ * the 'command' list"), and a second hand-maintained copy of ~180 verbs
9
+ * is a copy that drifts -- so it moved here and `help` reads it from
10
+ * here too. There is exactly one list.
11
+ *
12
+ * ALIASES ARE FIRST-CLASS. Each entry is [primary, ...aliases], and help
13
+ * prints them as "primary (alias, alias)". Tab must match all of them:
14
+ * somebody typing "inventor<Tab>" is reaching for `inventory`, which is
15
+ * an alias of `inv`, and a completer that only knew primaries would
16
+ * leave them stuck on a word that works.
17
+ */
18
+ export const HELP_CATEGORIES = [
19
+ { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach'], ['run'], ['sprint'], ['climb', 'mantle', 'scale', 'clamber'], ['descend'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow'], ['unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
20
+ { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['loot'], ['drop'], ['give'], ['store'], ['open'], ['close'], ['put'], ['equip'], ['unequip'], ['fit'], ['unfit'], ['brandish', 'draw'], ['holster'], ['reload'], ['use'], ['read']] },
21
+ { title: 'combat', entries: [['attack'], ['kill'], ['subdue', 'stun', 'knock'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['aim'], ['defend'], ['delay'], ['end-turn', 'endturn', 'done', 'pass'], ['initiative', 'tracker', 'turn'], ['nudge'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
22
+ // The party layer: everyone who walks (or flies, or manifests) at
23
+ // your side answers to these.
24
+ // "players" folded back under crew after playtesting ("crew
25
+ // players" / "crew street"); the bare verb still answers quietly.
26
+ { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
27
+ { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
28
+ { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['force', 'brute', 'smash'], ['mark', 'marks'], ['unmark'], ['disable'], ['brick'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['hide'], ['edit'], ['erase', 'wipe'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
29
+ // The Emerged get their own shelf (player request: "there MUST be
30
+ // resonance" -- matrix is the place, Resonance is the talent).
31
+ // `sustain` shelves here and not under magic: the command is
32
+ // technomancer-gated (a sprite task, SR5 p.256) and refuses anyone
33
+ // else, so the Emerged shelf is where its audience looks.
34
+ { title: 'resonance', entries: [['compile'], ['decompile'], ['thread'], ['unravel'], ['register'], ['standby'], ['sustain'], ['sprites']] },
35
+ { title: 'social', entries: [['say'], ['shout'], ['emote', 'me'], ['tell', 'talk-to'], ['call'], ['message'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
36
+ { title: 'street', entries: [['abandon'], ['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['garage'], ['pay']] },
37
+ { title: 'barriers', entries: [['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
38
+ { title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['sin', 'sins', 'papers'], ['initiate', 'submerge'], ['install', 'chrome']] },
39
+ { title: 'session', entries: [['help'], ['hints'], ['time', 'clock'], ['note'], ['backlog'], ['review'], ['roles'], ['clear'], ['quit', 'exit']] },
40
+ ];
41
+ /** Registered but deliberately UNLISTED: reachable as subcommands or
42
+ * muscle-memory ("players" folded back under crew after playtesting
43
+ * -- "crew players" is the advertised path; the bare verb still
44
+ * answers). Without this, the uncategorized sweep resurrects them
45
+ * in an "other" bucket. */
46
+ // 'first' and 'apply' exist ONLY to catch the two-word phrasings
47
+ // ("first aid", "apply first aid") that used to die at the parser. As
48
+ // bare words they mean nothing, so they are claimed but not advertised
49
+ // -- `heal` is the verb the help screen teaches.
50
+ // `complete` is what Tab sends (commands/complete.ts) -- a question the
51
+ // input box asks, not a verb anyone types, so it stays out of help.
52
+ export const HIDDEN_VERBS = ['players', 'first', 'apply', 'complete'];
53
+ /** Session verbs that only a play-tester (or admin) can actually use
54
+ * -- the server role-gates both and 403s everyone else. Listed in
55
+ * help ONLY for a confirmed play-tester; see helpCategories. `roles`
56
+ * rides along because it is admin-only, a strictly narrower group. */
57
+ export const PLAYTESTER_VERBS = new Set(['backlog', 'review', 'roles']);
58
+ /**
59
+ * The help index as THIS account should see it (backlog wave 2, item
60
+ * D: "let's remove Seattle Backlog for non playtesters"). The backlog
61
+ * is role-gated on the server and 403s everyone else, so listing it
62
+ * for every player only advertises a refusal. Hidden unless the
63
+ * account is a CONFIRMED play-tester -- see utilities/playtester.ts
64
+ * for why "unknown" (offline, logged out) hides rather than shows,
65
+ * and why the verb itself stays registered and usable regardless.
66
+ */
67
+ export function helpCategories() {
68
+ if (isPlaytester())
69
+ return HELP_CATEGORIES;
70
+ return HELP_CATEGORIES.map(c => c.title !== 'session' ? c : {
71
+ ...c,
72
+ entries: c.entries.filter(e => !PLAYTESTER_VERBS.has(e[0])),
73
+ });
74
+ }
75
+ /**
76
+ * EVERY WORD A PLAYER MAY TYPE AS A FIRST WORD, aliases included, in the
77
+ * order the help screen shelves them -- so an empty Tab reads like the
78
+ * help index rather than like an alphabetised dump.
79
+ *
80
+ * Role-aware through helpCategories(), for the reason given there: a
81
+ * completer that offers `backlog` to an account the server will 403 is
82
+ * advertising a refusal, and Tab is the worst place to learn about one.
83
+ */
84
+ export function playerVerbs() {
85
+ return helpCategories().flatMap(c => c.entries.flat());
86
+ }
87
+ /** Which shelf a verb sits on, for the one-line hint Tab prints beside
88
+ * it. Falls back to the primary spelling for an alias, because "walk"
89
+ * is more useful annotated "moving -- see move" than not at all. */
90
+ export function verbShelf(verb) {
91
+ for (const c of HELP_CATEGORIES) {
92
+ for (const entry of c.entries) {
93
+ if (entry.includes(verb))
94
+ return { category: c.title, primary: entry[0] };
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+ //# sourceMappingURL=verb-index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.214.0",
3
+ "version": "5.216.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",