@maka/maka-cli 5.17.16 → 5.17.18

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 (55) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +1 -1
  3. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/call.d.ts +2 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +35 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/command.d.ts +16 -0
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/command.js +10 -0
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/command.js.map +1 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/compile.d.ts +27 -0
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +8 -1
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.d.ts +12 -2
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +69 -79
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js.map +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js +4 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js.map +1 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/players.d.ts +4 -6
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/players.js +6 -9
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/players.js.map +1 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/register.d.ts +16 -0
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/register.js +74 -0
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/register.js.map +1 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +18 -9
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.d.ts +17 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +165 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js.map +1 -0
  29. package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +19 -0
  30. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +22 -0
  31. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -0
  32. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +19 -4
  33. package/bundle/typescript/src/commands/game/sideQuest/game.js +149 -49
  34. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +10 -1
  36. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +20 -4
  37. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +15 -0
  39. package/bundle/typescript/src/commands/game/sideQuest/ui.d.ts +6 -0
  40. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  41. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +4 -0
  42. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +13 -0
  43. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/utilities/session-telemetry.js +9 -0
  45. package/bundle/typescript/src/commands/game/sideQuest/utilities/session-telemetry.js.map +1 -1
  46. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.d.ts +12 -7
  47. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +29 -17
  48. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js.map +1 -1
  49. package/bundle/typescript/src/commands/game/sideQuest/utilities/social.d.ts +1 -30
  50. package/bundle/typescript/src/commands/game/sideQuest/utilities/social.js +4 -89
  51. package/bundle/typescript/src/commands/game/sideQuest/utilities/social.js.map +1 -1
  52. package/package.json +1 -1
  53. package/bundle/typescript/src/commands/game/sideQuest/utilities/crew-ledger.d.ts +0 -21
  54. package/bundle/typescript/src/commands/game/sideQuest/utilities/crew-ledger.js +0 -25
  55. package/bundle/typescript/src/commands/game/sideQuest/utilities/crew-ledger.js.map +0 -1
@@ -6,8 +6,7 @@ import { Room } from './models/_index.js';
6
6
  import { Door } from './models/door.js';
7
7
  import { SAVE_VERSION } from './types/save-file.js';
8
8
  import { serializePlayer, restorePlayer, captureHubOverlay, applyHubOverlay, writeSaveAtomic, deleteSave, restoreItem, readSave, serializeItem, saveSlug, } from './utilities/persistence.js';
9
- import { appendCrewLedger } from './utilities/crew-ledger.js';
10
- import { heartbeatPresence, heartbeatPresenceNow, ghostHireable, syncCrewLedger, claimGhostEarnings } from './utilities/social.js';
9
+ import { heartbeatPresence, heartbeatPresenceNow } from './utilities/social.js';
11
10
  import { pushSave, burnCloudSaveFor } from './utilities/cloud-saves.js';
12
11
  import { DOCWAGON_TIERS, RESUSC_FEE, DEATH_COMP } from './utilities/docwagon.js';
13
12
  import { isFence, LIFESTYLE_TIERS } from './utilities/commerce.js';
@@ -77,7 +76,9 @@ import { ContactsCommand } from './commands/contacts.js';
77
76
  import { JumpCommand } from './commands/jump.js';
78
77
  import { InitiateCommand } from './commands/initiate.js';
79
78
  import { SummonCommand } from './commands/summon.js';
80
- import { CompileCommand } from './commands/compile.js';
79
+ import { CompileCommand, SPRITE_TYPES } from './commands/compile.js';
80
+ import { ThreadCommand } from './commands/thread.js';
81
+ import { RegisterCommand } from './commands/register.js';
81
82
  import { OrderCommand } from './commands/order.js';
82
83
  import { DeployCommand, AgentCommand, RecallCommand } from './commands/companions.js';
83
84
  import { DeckCommand, LoadProgramCommand, UnloadProgramCommand } from './commands/deck.js';
@@ -133,6 +134,11 @@ export default class Game {
133
134
  _inputBox;
134
135
  _useAi = false;
135
136
  _aiRefusalAnnounced = false;
137
+ // ACCOUNT BINDING (ruling 2026-08-23): who this runner belongs to,
138
+ // and whether they're explicitly account-less. Anonymous = the whole
139
+ // social layer stays dark (presence, cloud mirror, street, drafts).
140
+ _owner;
141
+ _anonymous = false;
136
142
  _logFilePath = '';
137
143
  _tier = 1;
138
144
  // Guards against two overlapping continueToNextScene() calls -- found via
@@ -222,9 +228,9 @@ export default class Game {
222
228
  // talent pool churns; whoever you HIRED is the persistent part.
223
229
  crewForHire = [];
224
230
  // PRESENCE (social layer, Phase B): a hub heartbeat announces this
225
- // runner to the street's player list on maka-cli.com -- and carries
226
- // the "hireable while away" ghost opt-in. Only beats while the scene
227
- // IS the hub (the run is nobody's business); silent no-op logged out.
231
+ // runner to the street's player list on maka-cli.com. Only beats
232
+ // while the scene IS the hub (the run is nobody's business); silent
233
+ // no-op logged out.
228
234
  _presenceTimer;
229
235
  static PRESENCE_TICK_MS = 60_000;
230
236
  presencePayload() {
@@ -232,7 +238,6 @@ export default class Game {
232
238
  playerName: this.player.name,
233
239
  slug: saveSlug(this.player.name),
234
240
  hubName: this._hubName,
235
- ghostHireable: ghostHireable(),
236
241
  // ONLINE means the LINK is live (player ruling): commlink equipped
237
242
  // and broadcasting. Pull the link or run silent -> off the street.
238
243
  linkUp: this.player.equipment.head?.commlink != null && !this.player.runningSilent,
@@ -276,32 +281,19 @@ export default class Game {
276
281
  * push one right now and say so.
277
282
  */
278
283
  async pushPresenceNow() {
284
+ if (this._anonymous)
285
+ return 'offline';
279
286
  return heartbeatPresenceNow(this.presencePayload());
280
287
  }
281
288
  startPresenceHeartbeat() {
289
+ if (this._anonymous)
290
+ return; // account-less: never on the street
282
291
  if (this._presenceTimer)
283
292
  return;
284
293
  this.beatPresence();
285
294
  this._presenceTimer = setInterval(() => this.beatPresence(), Game.PRESENCE_TICK_MS);
286
295
  this._presenceTimer.unref();
287
296
  }
288
- /** Ghost earnings home delivery: what YOUR runner made riding in
289
- * other players' crews since last time. Fire-and-forget from hub
290
- * arrival beats; pays onto the job credstick when anything landed. */
291
- async collectGhostEarnings() {
292
- try {
293
- const claimed = await claimGhostEarnings(saveSlug(this.player.name));
294
- if (!claimed || this.scene !== this._hubScene)
295
- return;
296
- const paid = this.scene.payRunner(claimed.total, 'ghost earnings');
297
- if (!paid)
298
- return;
299
- const jobs = claimed.entries.map(e => `"${e.job}" with ${e.hiredBy} (${e.amount}¥)`).join(', ');
300
- Logger.getInstance().logWithColor(`While you were gone, your runner rode other crews: ${jobs}. ${paid.net}¥ certified lands on your stick.`, 'green');
301
- this.updateStatus();
302
- }
303
- catch { /* offline -- next homecoming */ }
304
- }
305
297
  static HIRE_NAMES = [
306
298
  'Jax', 'Sable', 'Vex', 'Rook', 'Nyx', 'Cinder', 'Halo', 'Grifter',
307
299
  'Static', 'Mercy', 'Tinder', 'Chrome Marie', 'Deacon', 'Fetch', 'Loa',
@@ -451,9 +443,6 @@ export default class Game {
451
443
  const cut = Math.floor(gross * (member.cutPct * scale) / 100);
452
444
  taken += cut;
453
445
  parts.push(`${member.name} ${member.cutPct}% (${cut}¥)`);
454
- if (member.ghost) {
455
- appendCrewLedger({ slug: member.ghost.slug, owner: member.ghost.owner, amount: cut, job: this.currentJobName ?? 'a job', when: new Date().toISOString() });
456
- }
457
446
  }
458
447
  return { share: gross - taken, line: `Crew cut: ${parts.join(', ')}.` };
459
448
  }
@@ -723,6 +712,9 @@ export default class Game {
723
712
  // death both remove the entry; persistent state (the frame, the deck)
724
713
  // lives on Items in the player's possession, never here.
725
714
  companions = [];
715
+ /** A registered sprite folded for travel, waiting to re-shell at the
716
+ * destination (the drone _pendingRedeploys school). */
717
+ _pendingSpriteRedeploy;
726
718
  // Drones deployed at travel time re-shell on arrival ("the rotors spin
727
719
  // back up at the curb") -- tracked as their items across the swap.
728
720
  _pendingRedeploys = [];
@@ -924,7 +916,15 @@ export default class Game {
924
916
  lines.push(`Your bound spirit thins into the rain -- the summons doesn't outlast the ride.`);
925
917
  }
926
918
  else if (entry.kind === 'sprite') {
927
- lines.push(`Your sprite decompiles into clean static -- the thread doesn't outlast the ride.`);
919
+ // A REGISTERED sprite persists (register.ts): it folds into
920
+ // your PAN for the ride and re-shells at the curb.
921
+ if (entry.registered && entry.spriteType) {
922
+ this._pendingSpriteRedeploy = { spriteType: entry.spriteType, level: entry.force, tasks: entry.services ?? 0 };
923
+ lines.push(`${entry.npc.name} folds into your PAN for the ride -- registered code travels with you.`);
924
+ }
925
+ else {
926
+ lines.push(`Your sprite decompiles into clean static -- the thread doesn't outlast the ride.`);
927
+ }
928
928
  }
929
929
  else if (entry.kind === 'drone') {
930
930
  lines.push(`The ${entry.boundDevice?.name ?? 'drone'} autopilots home to your hand for the ride.`);
@@ -951,8 +951,42 @@ export default class Game {
951
951
  if (shell)
952
952
  lines.push(`The ${item.name}'s rotors spin back up at the curb.`);
953
953
  }
954
+ // A REGISTERED sprite re-shells from the PAN (register.ts).
955
+ if (this._pendingSpriteRedeploy) {
956
+ const spec = this._pendingSpriteRedeploy;
957
+ this._pendingSpriteRedeploy = undefined;
958
+ const line = this.respawnRegisteredSprite(spec);
959
+ if (line)
960
+ lines.push(line);
961
+ }
954
962
  return lines;
955
963
  }
964
+ /** Re-shell a registered sprite (travel arrival, or a resumed save).
965
+ * Returns the arrival line, or undefined when the shell can't land
966
+ * (name collision -- vanishingly rare). */
967
+ respawnRegisteredSprite(spec) {
968
+ const type = SPRITE_TYPES[spec.spriteType];
969
+ if (!type)
970
+ return undefined;
971
+ const shell = this.spawnCompanionShell({
972
+ name: `${type.name} Sprite (Level ${spec.level})`,
973
+ description: `A registered ${type.name.toLowerCase()} sprite riding ${this.player.name}'s PAN -- ${type.blurb}. It serves until its ${spec.tasks} task${spec.tasks === 1 ? '' : 's'} are spent.`,
974
+ kind: 'sprite',
975
+ combat: type.combat(spec.level),
976
+ room: this.player.currentLocation,
977
+ plane: 'matrix',
978
+ force: spec.level,
979
+ services: spec.tasks,
980
+ });
981
+ if (!shell)
982
+ return undefined;
983
+ const entry = this.companions.find(c => c.npc === shell);
984
+ if (entry) {
985
+ entry.registered = true;
986
+ entry.spriteType = spec.spriteType;
987
+ }
988
+ return `${shell.name} unfolds from your PAN, luminous and patient -- ${spec.tasks} task${spec.tasks === 1 ? '' : 's'} standing.`;
989
+ }
956
990
  /**
957
991
  * Records a real transaction with a hub NPC: first contact opens the
958
992
  * relationship at Loyalty 1 (a safeguard -- the hub cast is pre-seeded
@@ -1210,6 +1244,8 @@ export default class Game {
1210
1244
  /** The transport, hooks-first, CLI default second; undefined =
1211
1245
  * offline/logged out/classic mode (callers degrade quietly). */
1212
1246
  async streetTransport() {
1247
+ if (this._anonymous)
1248
+ return undefined; // account-less: no street
1213
1249
  if (this.listStreetPlayers && this.sendPage) {
1214
1250
  return { list: this.listStreetPlayers, send: this.sendPage };
1215
1251
  }
@@ -1242,7 +1278,11 @@ export default class Game {
1242
1278
  if (!channel)
1243
1279
  return undefined;
1244
1280
  player.crewChannel = undefined;
1245
- void this.streetTransport().then(t => t?.send({ from: player.name, handle: channel.handle, text: END_CALL_SENTINEL })).catch(() => undefined);
1281
+ // The PARTY LINE is a conference: dropping off is local, nobody
1282
+ // else's line dies. Only a 1:1 call signals the far side.
1283
+ if (!channel.party && channel.handle) {
1284
+ void this.streetTransport().then(t => t?.send({ from: player.name, handle: channel.handle, text: END_CALL_SENTINEL })).catch(() => undefined);
1285
+ }
1246
1286
  return channel.playerName;
1247
1287
  }
1248
1288
  /** Trash the offer wholesale ("decline", and any future fixer snub). */
@@ -1567,6 +1607,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1567
1607
  CommandFactory.registerCommand('conjure', SummonCommand);
1568
1608
  CommandFactory.registerCommand('compile', CompileCommand);
1569
1609
  CommandFactory.registerCommand('decompile', CompileCommand);
1610
+ CommandFactory.registerCommand('thread', ThreadCommand);
1611
+ CommandFactory.registerCommand('register', RegisterCommand);
1570
1612
  // "At" spots (utilities/spots.ts): walk/move/approach are one
1571
1613
  // crossing; run and sprint are the fast modes (combat semantics in
1572
1614
  // the spot-move phase).
@@ -1676,6 +1718,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1676
1718
  this._useAi = options.useAi;
1677
1719
  this._logFilePath = options.logFilePath;
1678
1720
  this._saveFilePath = options.saveFilePath;
1721
+ this._owner = options.owner;
1722
+ this._anonymous = options.anonymousRunner === true;
1679
1723
  // ONE loud notice the moment the AI provider refuses on ACCOUNT
1680
1724
  // grounds (empty credits, bad key). Without it a dead key mutes
1681
1725
  // every NPC and makes every job "fall through" invisibly -- a real
@@ -1751,16 +1795,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1751
1795
  // First deal of the for-hire board (rotates each homecoming).
1752
1796
  this.rollHireCandidates();
1753
1797
  // The street learns you're home (social layer): presence beats,
1754
- // unsynced ghost earnings go up, and anything YOUR runner earned
1755
- // riding other crews comes down. All fire-and-forget. The DDP hub
1756
- // link (run invites, shared sessions -- Phase C) warms up behind
1757
- // them.
1798
+ // and the DDP hub link (run invites, shared sessions -- Phase C)
1799
+ // warms up behind them. All fire-and-forget.
1758
1800
  this.startPresenceHeartbeat();
1759
1801
  void import('./utilities/shared-run.js')
1760
1802
  .then(m => m.ensureHubLink(this))
1761
1803
  .catch(() => undefined);
1762
- void syncCrewLedger();
1763
- void this.collectGhostEarnings();
1764
1804
  // The next run starts cooking the moment you're home (player
1765
1805
  // request): generation takes 20-40s, and waiting for the player
1766
1806
  // to CALL first meant "give me a bit" every time. Draft now --
@@ -1808,6 +1848,15 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1808
1848
  if (this._hubScene && this.scene === this._hubScene) {
1809
1849
  this.requestSave('hub-arrival');
1810
1850
  }
1851
+ // A REGISTERED sprite from the save re-shells at the cot (the
1852
+ // technomancer's persistent companion -- register.ts).
1853
+ if (this._pendingSpriteRedeploy && this.scene === this._hubScene) {
1854
+ const spec = this._pendingSpriteRedeploy;
1855
+ this._pendingSpriteRedeploy = undefined;
1856
+ const line = this.respawnRegisteredSprite(spec);
1857
+ if (line)
1858
+ Logger.getInstance().logWithColor(line, 'cyan');
1859
+ }
1811
1860
  if (this._resume) {
1812
1861
  const pendingNote = this._pendingRunSeed
1813
1862
  ? hint(`\n\n${FIXER_NAME}'s job is still on offer: "${this._pendingRunSeed.name}". "call taxi" when you're ready, or "call ${FIXER_NAME}" to hear it again.`)
@@ -1872,7 +1921,16 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1872
1921
  this.docwagon = save.game.docwagon ? { ...save.game.docwagon } : undefined;
1873
1922
  // THE CREW rides the save; the deprecated one-slot enlistedAlly
1874
1923
  // migrates in once (old saves) and is never written again.
1875
- this.crew = (save.game.crew ?? []).map(m => ({ ...m, gear: [...m.gear] }));
1924
+ // GHOST MIGRATION (mechanic removed 2026-08-23): hired AI copies of
1925
+ // other players' runners dissolve on load -- live seats (liveOnly)
1926
+ // and ordinary hires ride on unchanged.
1927
+ this.crew = (save.game.crew ?? [])
1928
+ .filter(m => !(m.ghost && !m.ghost.liveOnly))
1929
+ .map(m => ({ ...m, gear: [...m.gear] }));
1930
+ // The registered sprite stages for its re-shell at hub arrival.
1931
+ if (save.game.registeredSprite) {
1932
+ this._pendingSpriteRedeploy = { ...save.game.registeredSprite };
1933
+ }
1876
1934
  if (this.crew.length === 0 && save.game.enlistedAlly) {
1877
1935
  const legacy = save.game.enlistedAlly;
1878
1936
  this.crew.push({
@@ -1942,7 +2000,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1942
2000
  // Local-first: the beat is DONE the moment the file lands. The
1943
2001
  // cloud mirror rides behind, fire-and-forget (see cloud-saves.ts
1944
2002
  // -- logged-out/offline/expired all degrade to a silent no-op).
1945
- void pushSave(this._saveFilePath, save);
2003
+ // An ANONYMOUS runner's life never leaves this machine.
2004
+ if (!this._anonymous)
2005
+ void pushSave(this._saveFilePath, save);
1946
2006
  }
1947
2007
  catch (err) {
1948
2008
  logger.error(`requestSave(${reason}) failed: ${err}`);
@@ -1953,6 +2013,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1953
2013
  version: SAVE_VERSION,
1954
2014
  savedAt: new Date().toISOString(),
1955
2015
  reason,
2016
+ owner: this._owner,
2017
+ anonymous: this._anonymous ? true : undefined,
1956
2018
  playerName: this.player.name,
1957
2019
  summary: {
1958
2020
  archetypeName: this.player.archetypeName,
@@ -1970,6 +2032,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1970
2032
  contacts: [...this.contacts.entries()],
1971
2033
  procurements: [...this.pendingProcurements],
1972
2034
  docwagon: this.docwagon ? { ...this.docwagon } : undefined,
2035
+ registeredSprite: (() => {
2036
+ // A live registered sprite rides the save; a travel-folded one
2037
+ // is in the redeploy stash. Either way it survives the session.
2038
+ const live = this.companions.find(c => c.kind === 'sprite' && c.registered && c.spriteType);
2039
+ if (live)
2040
+ return { spriteType: live.spriteType, level: live.force, tasks: live.services ?? 0 };
2041
+ return this._pendingSpriteRedeploy ? { ...this._pendingSpriteRedeploy } : undefined;
2042
+ })(),
1973
2043
  crew: this.crew.map(m => ({ ...m, gear: [...m.gear] })),
1974
2044
  commandRefusals: { ...this.commandRefusals },
1975
2045
  fencedCategories: [...this.fencedCategories],
@@ -2170,11 +2240,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2170
2240
  // homecoming lines (obituaries, write-back) rode in above. Fresh
2171
2241
  // faces on the for-hire board each homecoming.
2172
2242
  this.rollHireCandidates();
2173
- // Social settlement: presence resumes, ghost cuts sync up, earnings
2174
- // come home.
2243
+ // Social settlement: the street hears you're home.
2175
2244
  this.beatPresence();
2176
- void syncCrewLedger();
2177
- void this.collectGhostEarnings();
2178
2245
  const allyLine = crewLines.length > 0 ? `\n${crewLines.join('\n')}` : '';
2179
2246
  // DOCWAGON's year ticks in homecomings (12 = a street year, player
2180
2247
  // ruling): the contract lapses quietly unless renewed at Doc's.
@@ -2235,7 +2302,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2235
2302
  // offer flow (his call, accept/decline, the taxi) works unchanged.
2236
2303
  // Offline owners fall back to the Phase B ghost riders below.
2237
2304
  let sharedDeclineNote = '';
2238
- if (this.hasHub) {
2305
+ if (this.hasHub && !this._anonymous) {
2239
2306
  const srMod = await import('./utilities/shared-run.js').catch(() => undefined);
2240
2307
  const shared = srMod ? await srMod.tryCreateSharedSession(this).catch(() => undefined) : undefined;
2241
2308
  if (shared && 'declined' in shared) {
@@ -2247,7 +2314,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2247
2314
  // means update THIS machine.
2248
2315
  const skew = /runs engine ([\d.]+)/.exec(String(shared.declined));
2249
2316
  sharedDeclineNote = skew
2250
- ? ` "Crew channel's scrambled tonight -- this one's quoted without them."\n{yellow-fg}(Out of character: version mismatch -- this maka is ${srMod?.engineVersion() ?? '?'}, the street runs ${skew[1]}. Update THIS machine: npm i -g @maka/maka-cli@${skew[1]}, then relaunch.){/yellow-fg}`
2317
+ ? ` "Crew channel's scrambled tonight -- this one's quoted without them."\n{yellow-fg}(Out of character: engine mismatch -- this machine runs ${srMod?.engineVersion() ?? '?'}, the street runs ${skew[1]}. If yours is older: npm i -g @maka/maka-cli@latest, then relaunch. If newer, the street is still rolling out.){/yellow-fg}`
2251
2318
  : ` "Couldn't raise the whole crew -- ${shared.declined} This one's quoted without them."`;
2252
2319
  }
2253
2320
  else if (shared) {
@@ -2913,7 +2980,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2913
2980
  if (player.crewChannel) {
2914
2981
  // Reads exactly like an NPC call (player ruling: "keep it simple
2915
2982
  // -- On a call with Steve, just like any call to a contact").
2916
- lines.push(`{${CALL_COLOR}-fg}${CALL_ICON} On a call with ${player.crewChannel.playerName}{/${CALL_COLOR}-fg}`);
2983
+ lines.push(`{${CALL_COLOR}-fg}${CALL_ICON} ${player.crewChannel.party ? 'On the party line' : `On a call with ${player.crewChannel.playerName}`}{/${CALL_COLOR}-fg}`);
2917
2984
  }
2918
2985
  // The party: one line per live companion shell, with the tracks that
2919
2986
  // matter for its kind. A spirit shows its BODY too (it's a full
@@ -3168,7 +3235,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3168
3235
  // players" / "crew street"); the bare verb still answers quietly.
3169
3236
  { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall']] },
3170
3237
  { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
3171
- { title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['pan'], ['silent'], ['reboot'], ['agent'], ['compile', 'decompile'], ['drone'], ['jump', 'rig']] },
3238
+ { title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['pan'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
3239
+ // The Emerged get their own shelf (player request: "there MUST be
3240
+ // resonance" -- matrix is the place, Resonance is the talent).
3241
+ { title: 'resonance', entries: [['compile', 'decompile'], ['thread'], ['register']] },
3172
3242
  { title: 'social', entries: [['say'], ['talk-to'], ['call'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
3173
3243
  { title: 'street', entries: [['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['pay']] },
3174
3244
  { title: 'barriers', entries: [['solve'], ['pick'], ['bluff'], ['unlock'], ['lock']] },
@@ -3211,14 +3281,44 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3211
3281
  }
3212
3282
  return lines.join('\n');
3213
3283
  }
3214
- // "help <command>": one description, aliases resolved to their primary.
3215
- const allVerbs = [...covered, ...uncategorized];
3216
- const pickedVerb = fuzzyPickName(query, allVerbs);
3284
+ // "help <command>" (and "help <command> <sub>"): the command's
3285
+ // one-liner plus a TIDY subcommand table when it declares one
3286
+ // (player ruling: structured rows, not a paragraph; a second word
3287
+ // drills into one row).
3288
+ const words = query.split(/\s+/);
3289
+ const pickedVerb = fuzzyPickName(words[0], [...covered, ...uncategorized])
3290
+ ?? fuzzyPickName(query, [...covered, ...uncategorized]);
3217
3291
  if (pickedVerb) {
3218
3292
  const entry = Game.HELP_CATEGORIES.flatMap(c => c.entries).find(e => e.includes(pickedVerb));
3219
3293
  const primary = entry?.[0] ?? pickedVerb;
3220
3294
  const aliases = entry?.slice(1) ?? [];
3221
- return `${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`;
3295
+ const CommandClass = CommandFactory.getAllRegisteredCommands()[primary];
3296
+ const subs = CommandClass?.getSubcommands?.();
3297
+ const title = `${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}`;
3298
+ if (subs && subs.length > 0) {
3299
+ // Drill-down: "help crew hire" -> just the hire row, long form.
3300
+ const subQuery = words.slice(1).join(' ');
3301
+ if (subQuery) {
3302
+ // A row's key: the word after the primary verb when usage
3303
+ // starts with it, else its first word.
3304
+ const subKey = (s) => {
3305
+ const t = s.usage.split(/\\s+/);
3306
+ return (t[0] === primary ? t[1] ?? t[0] : t[0]).replace(/[<>|]/g, '');
3307
+ };
3308
+ const pickedSub = fuzzyPickName(subQuery, subs.map(subKey));
3309
+ const sub = pickedSub ? subs.find(s => subKey(s) === pickedSub) : undefined;
3310
+ if (sub)
3311
+ return `${sub.usage}\n ${sub.long ?? sub.short}`;
3312
+ return `${title}: no subcommand matching "${subQuery}".\n${subs.map(s => ` ${s.usage.padEnd(24)} ${s.short}`).join('\n')}`;
3313
+ }
3314
+ const lines = [`${title}: ${this.describeVerb(primary).split('. ')[0]}.`];
3315
+ for (const s of subs)
3316
+ lines.push(` ${s.usage.padEnd(24)} ${s.short}`);
3317
+ lines.push('');
3318
+ lines.push(`"help ${primary} <sub>" drills into one.`);
3319
+ return lines.join('\n');
3320
+ }
3321
+ return `${title}: ${this.describeVerb(primary)}`;
3222
3322
  }
3223
3323
  return `No command or category called "${query}". Try plain "help".`;
3224
3324
  }