aurival 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,8 +75,8 @@ bot.command('say', async (ctx) => {
75
75
  ctx.command; // "say"
76
76
  ctx.arguments; // the raw rest of the line, unparsed, possibly ""
77
77
  ctx.chat; // { id, type, name, member_count }
78
- ctx.sender; // { id, handle, name } — always set
79
- ctx.message.text; // "/say hi" — the invoking message, verbatim
78
+ ctx.sender; // { id, handle, name }, always set
79
+ ctx.message.text; // "/say hi", the invoking message, verbatim
80
80
  await ctx.reply('…');
81
81
  });
82
82
  ```
@@ -184,7 +184,8 @@ bot.command('fix', async (ctx) => {
184
184
  });
185
185
 
186
186
  bot.command('oops', async (ctx) => {
187
- await ctx.delete(ctx.message);
187
+ const sent = await ctx.reply('ignore that');
188
+ await ctx.delete(sent);
188
189
  });
189
190
 
190
191
  bot.command('upvote', async (ctx) => {
@@ -224,6 +225,10 @@ straight back to `edit()`, `delete()` and `react()` — no id juggling. The `sen
224
225
  those, when it is set at all, is known by id alone — `handle` and `name` are empty strings,
225
226
  because the REST entity names the sender with a bare `usr_…`.
226
227
 
228
+ `edit()` and `delete()` take only the bot's own messages; anyone else's answers
229
+ `message_not_yours`, which is why `oops` above deletes the reply it just sent rather than the
230
+ message that invoked it. `react()` and `unreact()` work on any message in a chat the bot is in.
231
+
227
232
  The typing indicator is automatic: a command handler still running 300 ms after it started
228
233
  shows the chat "is thinking", and the indicator clears when the handler returns, including on a
229
234
  throw. A handler that replies inside those 300 ms sends nothing, so a fast bot never flickers.
@@ -245,6 +250,11 @@ bot.on('member.joined', async (ctx) => {
245
250
  });
246
251
  ```
247
252
 
253
+ Every entry in `mentions` needs its `@handle` token actually present in `text`, outside any
254
+ code block or inline code, or the server rejects the request — write the handle into the
255
+ message yourself, in plain text; the SDK will not edit `text` for you, and a mention inside
256
+ code, or with no token at all, renders as nothing.
257
+
248
258
  ## Embeds and buttons
249
259
 
250
260
  `Embed` is a builder — every `setAuthor`/`setThumbnail`/`addField`/`setFooter` call returns the
@@ -258,20 +268,20 @@ already uses:
258
268
  ```ts
259
269
  import { Embed, Button } from 'aurival';
260
270
 
261
- const embed = new Embed({ title: 'Trivia round 4', description: 'Which ocean is the deepest?', color: '#3E6E8E' })
262
- .setAuthor('Quizbot', 'https://cdn.aurival.com/q.png')
263
- .setThumbnail('https://cdn.aurival.com/t.png')
264
- .addField('Players', '6', true)
265
- .addField('Round', '4 of 10', true)
266
- .setFooter('Answer within 30s');
271
+ const embed = new Embed({ title: 'Title', description: 'Description. Plain text or markdown, under the title.' })
272
+ .setAuthor('Author line')
273
+ .addField('Field name', 'Field value', true)
274
+ .addField('Second field', 'Second value', true)
275
+ .setImage('https://bots.aurival.com/docs-assets/aurival-welcome-cover.jpg')
276
+ .setFooter('Footer text', 'https://bots.aurival.com/docs-assets/aurival-mascot-hero.png');
267
277
 
268
278
  const buttons = [
269
- new Button({ label: 'Pacific', id: 'pacific', style: 'primary' }),
270
- new Button({ label: 'Atlantic', id: 'atlantic', style: 'secondary' }),
271
- new Button({ label: 'Indian', id: 'indian', style: 'secondary' }),
279
+ new Button({ label: 'Primary action', id: 'primary', style: 'primary' }),
280
+ new Button({ label: 'Secondary action', id: 'secondary', style: 'secondary' }),
281
+ new Button({ label: 'Danger action', id: 'danger', style: 'danger' }),
272
282
  ];
273
283
 
274
- await ctx.reply('Ready when you are.', { embeds: [embed], buttons });
284
+ await ctx.reply({ embeds: [embed], buttons });
275
285
  ```
276
286
 
277
287
  A card earns its place when there is something to put on it: buttons, fields, an image. A reply
@@ -288,19 +298,20 @@ url throws, because a link button with nowhere to go is a dead pill:
288
298
  import { Button } from 'aurival';
289
299
 
290
300
  const buttons = [
291
- Button.link({ label: 'Full lineup', url: 'https://aurival.com/spaces/deepcuts/lineup' }),
292
- Button.link({ label: 'Set notes', url: 'https://example.com/notes' }),
293
- new Button({ label: 'Remind me', id: 'remind', style: 'primary' }),
301
+ Button.link({ label: 'Opens aurival.com', url: 'https://aurival.com' }),
302
+ Button.link({ label: 'Opens the docs', url: 'https://bots.aurival.com' }),
303
+ new Button({ label: 'Primary action', id: 'primary', style: 'primary' }),
294
304
  ];
295
305
 
296
- await ctx.reply('Doors at 21:00.', { buttons });
306
+ await ctx.reply('Two link buttons and a regular one, sharing one row.', { buttons });
297
307
  ```
298
308
 
299
309
  A link button never comes back to you: no `button.pressed` event, ever, and it never flips the
300
310
  row to used. It also stays tappable after a sibling is pressed, and while a sibling is still
301
311
  waiting on your `ack()` — the row greys out around it, the link does not. `id` still has to be
302
312
  there and still has to be unique in the message, and the SDK still slugs it from the label when
303
- you leave it out, so that `Set notes` pill gets `id: 'set-notes'` like any other button. Link
313
+ you leave it out, so that `Opens the docs` pill gets `id: 'opens-the-docs'` like any other
314
+ button. Link
304
315
  buttons count toward the five-button cap, and a message made only of link buttons is fine.
305
316
 
306
317
  A url is `https://` and at most 2048 characters, everywhere a url is a link target. A non-link
@@ -315,11 +326,11 @@ you cannot see is worse than an error you can.
315
326
  import { Button } from 'aurival';
316
327
 
317
328
  const buttons = [
318
- new Button({ label: 'Maybe', id: 'maybe', style: 'secondary', emoji: '🤔' }),
319
- new Button({ label: 'Remind me', id: 'remind', style: 'primary', emoji: '⏰' }),
329
+ new Button({ label: 'Secondary action', id: 'secondary', style: 'secondary', emoji: '🤔' }),
330
+ new Button({ label: 'Primary action', id: 'primary', style: 'primary', emoji: '⏰' }),
320
331
  ];
321
332
 
322
- await ctx.reply('Set starts in an hour.', { buttons });
333
+ await ctx.reply('Two buttons, each with one emoji in front of the label.', { buttons });
323
334
  ```
324
335
 
325
336
  Exactly one unicode emoji, no custom emoji, no `:shortcode:`, no image url. The emoji does not
@@ -337,16 +348,15 @@ title tappable, and the third argument to `setAuthor` does the same for the auth
337
348
  import { Embed } from 'aurival';
338
349
 
339
350
  const embed = new Embed({
340
- title: "Tonight's set",
341
- description: 'Deep cuts only, and nothing after midnight.',
342
- color: '#3E6E8E',
343
- url: 'https://aurival.com/spaces/deepcuts',
351
+ title: 'A linked title',
352
+ description: 'The title and the author line below both open a url when tapped.',
353
+ url: 'https://aurival.com',
344
354
  })
345
- .setAuthor('Deep Cuts', 'https://cdn.aurival.com/dc.png', 'https://aurival.com/u/deepcuts')
346
- .setThumbnail('https://cdn.aurival.com/cover.jpg')
347
- .setFooter('set by deepcuts', 'https://cdn.aurival.com/dc-small.png');
355
+ .setAuthor('Author line', 'https://bots.aurival.com/docs-assets/aurival-mascot-hero.png', 'https://aurival.com')
356
+ .setThumbnail('https://bots.aurival.com/docs-assets/aurival-mascot-hero.png')
357
+ .setFooter('Footer text', 'https://bots.aurival.com/docs-assets/aurival-mascot-hero.png');
348
358
 
349
- await ctx.reply("Tonight's lineup.", { embeds: [embed] });
359
+ await ctx.reply({ embeds: [embed] });
350
360
  ```
351
361
 
352
362
  Each of the three needs the thing it attaches to: a footer icon needs footer text, an embed url
@@ -364,13 +374,13 @@ as four asterisks and two words:
364
374
  import { Embed } from 'aurival';
365
375
 
366
376
  const embed = new Embed({
367
- title: 'House rules',
377
+ title: 'Markdown in the description',
368
378
  description:
369
- '**Deep cuts** only. No *requests* after the ~~third~~ second hour.\n' +
370
- 'Type `!queue` to see what is next.',
371
- }).addField('Set length', '3 hours', true);
379
+ '**Bold**, *italic*, ~~strikethrough~~ and `inline code` all render.\n' +
380
+ 'Headings render as bold body text, not larger type.',
381
+ }).addField('Field name', 'Field value', true);
372
382
 
373
- await ctx.reply('Welcome in.', { embeds: [embed] });
383
+ await ctx.reply({ embeds: [embed] });
374
384
  ```
375
385
 
376
386
  Supported: bold, italic, bold-italic, strikethrough and inline code. Headings render as bold
@@ -422,8 +432,8 @@ A press comes back as a `button.pressed` event, handled the same way any other e
422
432
  ```ts
423
433
  bot.on('button.pressed', async (ctx) => {
424
434
  await ctx.ack();
425
- if (ctx.button === 'pacific') {
426
- await ctx.reply('Correct! The Pacific is deepest.');
435
+ if (ctx.button === 'primary') {
436
+ await ctx.reply('You pressed the primary button.');
427
437
  }
428
438
  });
429
439
  ```
@@ -432,9 +442,64 @@ The `ctx` there is a `ButtonContext`: `.chat`, `.user`, `.message`, `.button` (t
432
442
  when building it) and `.interaction`. Call `await ctx.ack()` exactly once per press — a button
433
443
  is single-use, and acking one a second time doesn't throw locally, the server 409s the request.
434
444
 
435
- The full set of showcase examples (trivia, giveaways, DJ bots, moderation reports…) is in the
445
+ The showcase bot runs every one of these cards live. The full set is in the
436
446
  [cookbook](https://bots.aurival.com/docs/cookbook).
437
447
 
448
+ ## Buttons only the caller can press
449
+
450
+ By default nobody is locked: any member in the chat can press a card's buttons, exactly as
451
+ before this existed. Pass `forUser` to `reply`, `send`, `edit` or `ack` to lock the press to one
452
+ member.
453
+
454
+ ```ts
455
+ await ctx.reply('Locked to the caller', { buttons, forUser: ctx.sender });
456
+ ```
457
+
458
+ `forUser` takes a `User` (`ctx.sender` is one) or a bare `usr_…` id string. Either one
459
+ serializes to the same id on the wire.
460
+
461
+ Everyone still sees the card and its text. Only the press is gated: a non-caller's buttons
462
+ render at `.56` opacity with no checkmark and are not tappable. Nothing is drawn under the
463
+ row: the card never says who it is for, so write that into the embed footer yourself if you
464
+ want it on the plate. A link button on a locked card stays pressable by anyone. It never
465
+ round-trips to the server, so there is nothing for the lock to gate.
466
+
467
+ A non-caller who presses anyway is refused by the server with a `403`, before anything is
468
+ spent. `used` stays unset and the card is unchanged. That refusal never reaches your bot: a bot
469
+ never presses a button, so there is no SDK exception for it.
470
+
471
+ Omitting `forUser` on a `reply`/`edit`/`ack` that replaces a card **keeps the existing lock**.
472
+ Passing `forUser: null` **clears** it, and the card opens to everyone. Passing a new id **moves**
473
+ the lock to that member. This matters most on `ack`, where a quiz redraws its own card between
474
+ questions and must not silently unlock itself by leaving `forUser` out.
475
+
476
+ A caller-only quiz, start to finish:
477
+
478
+ ```ts
479
+ import { Button, Embed } from 'aurival';
480
+
481
+ bot.command('locked', async (ctx) => {
482
+ const embed = new Embed({
483
+ title: 'Locked to the caller',
484
+ description: 'Everyone sees this card. Only the person who ran the command can press.',
485
+ });
486
+ await ctx.reply({
487
+ embeds: [embed],
488
+ buttons: [new Button({ label: 'Only the caller presses', id: 'only', style: 'primary' })],
489
+ forUser: ctx.sender,
490
+ });
491
+ });
492
+
493
+ bot.on('button.pressed', async (ctx) => {
494
+ await ctx.ack({ text: 'Pressed.', buttons: [] });
495
+ });
496
+ ```
497
+
498
+ `ButtonContext` carries no `forUser` field. The presser is always the locked user by
499
+ construction: the server refuses everyone else before your handler ever runs, so there is
500
+ nothing on the press for it to expose. If you need the lock on a card you are acking, you
501
+ already have it: you set it on the send.
502
+
438
503
  ## Cooldowns
439
504
 
440
505
  A cooldown paces a command or a button. You attach it, the SDK keeps the bucket, and the wire
@@ -503,7 +568,7 @@ Override it where it belongs, and precedence is button, then card, then the bot
503
568
  ```ts
504
569
  const bot = new Bot({ buttonCooldown: { rate: 1, per: 5 } }); // this bot's buttons
505
570
  await ctx.reply({ buttons, buttonCooldown: null }); // this card's buttons, off
506
- new Button({ label: 'Paint', cooldown: null }); // this one button, off
571
+ new Button({ label: 'Primary action', cooldown: null }); // this one button, off
507
572
  ```
508
573
 
509
574
  A per-card or per-button cooldown gets its own buckets, keyed on the message, the button id and
@@ -517,6 +582,23 @@ legitimate once an hour.
517
582
  Link buttons are outside all of this — a link opens on the device and never round-trips, so it
518
583
  cannot carry a cooldown and `Button.link` has no `cooldown` field.
519
584
 
585
+ ### Keep one Cooldown per button
586
+
587
+ A `Cooldown` owns its own bucket of remaining presses. Build a fresh one inside the ack handler —
588
+ or anywhere a press can re-run it — and it forgets every prior press, so the cooldown never
589
+ actually triggers. Define the `Cooldown` and the `Button` once, at module scope, and reuse those
590
+ same objects on the send and on every ack that follows it:
591
+
592
+ ```ts
593
+ // Once, at module scope — not inside the handler.
594
+ const cooldown = new Cooldown(1, 10);
595
+ const button = new Button({ label: '+1', id: 'inc', style: 'primary', cooldown });
596
+
597
+ bot.on('button.pressed', async (ctx) => {
598
+ await ctx.ack({ buttons: [button] }); // the same button, never rebuilt here
599
+ });
600
+ ```
601
+
520
602
  ### The caveat, said plainly
521
603
 
522
604
  **Buckets are process memory.** They reset on restart, and they are not shared between
@@ -607,31 +689,30 @@ Three states, one rule, on both doors:
607
689
  with all three left out throws `an edit needs text, embeds or buttons` before anything is sent,
608
690
  and the server answers the same sentence as `NothingToEdit`.
609
691
 
610
- A trivia round is the whole feature in one pair of handlers. Both cards earn their plate: the
611
- question card carries buttons, the result card carries a field.
692
+ Pressing a button is the whole feature in one pair of handlers. Both cards earn their plate: the
693
+ first card carries buttons, the replacement carries the result.
612
694
 
613
695
  ```ts
614
696
  import { Button, Embed } from 'aurival';
615
697
 
616
- bot.command('trivia', async (ctx) => {
617
- await ctx.reply({
618
- embeds: [new Embed({ title: 'Which planet is largest?' })],
619
- buttons: [
620
- new Button({ label: 'Mars', style: 'secondary' }),
621
- new Button({ label: 'Jupiter', style: 'secondary' }),
622
- ],
623
- });
698
+ bot.command('update', async (ctx) => {
699
+ const embed = new Embed({ title: 'Press a button', description: 'The card is replaced in place when you press one.' });
700
+ const buttons = [
701
+ new Button({ label: 'Press me, card updates', id: 'u1', style: 'primary' }),
702
+ new Button({ label: 'Or press me', id: 'u2', style: 'secondary' }),
703
+ ];
704
+ await ctx.reply({ embeds: [embed], buttons });
624
705
  });
625
706
 
626
707
  bot.on('button.pressed', async (ctx) => {
627
- const correct = ctx.button === 'jupiter';
708
+ const which = ctx.button === 'u1' ? 'first' : 'second';
628
709
  const result = new Embed({
629
- title: correct ? 'Jupiter' : 'Not quite',
630
- description: 'Jupiter is about eleven Earths across.',
631
- }).addField('Answered by', ctx.user.handle, true);
710
+ title: `You pressed the ${which} button`,
711
+ description: 'ack() replaced the card in place. Nothing else in the chat moved.',
712
+ });
632
713
  await ctx.ack({
633
714
  embeds: [result],
634
- buttons: [new Button({ id: 'again', label: 'Play again' })],
715
+ buttons: [new Button({ label: 'Press again', id: 'again', style: 'primary' })],
635
716
  });
636
717
  });
637
718
  ```
@@ -646,7 +727,7 @@ on the id.
646
727
  outside a press:
647
728
 
648
729
  ```ts
649
- await ctx.edit(sent, { embeds: [new Embed({ title: 'Starting in 3…' })] });
730
+ await ctx.edit(sent, { embeds: [new Embed({ title: 'Title' })] });
650
731
  await ctx.edit(sent, { buttons: [] }); // the row is gone, the plate stays
651
732
  ```
652
733
 
@@ -671,6 +752,44 @@ Two new error classes ship with it, `CooldownWithBody` and `CooldownRetryAfterIn
671
752
  `InvalidRequestError`. You will not normally see either: the SDK builds the cooldown ack itself
672
753
  and refuses an out-of-range button cooldown where you write it.
673
754
 
755
+ ### Upgrading from 0.8.x
756
+
757
+ 0.9.0 adds new per-field and per-embed caps, and **one of them can break a card that worked on
758
+ 0.8.x**: none of these fields were length-limited before, so a card built past one of the new
759
+ limits used to send successfully and now throws. `Embed.addField`, `setAuthor` and `setFooter`
760
+ check length locally as of this version, before any round trip:
761
+
762
+ - an embed field name is at most 256 characters
763
+ - an embed field value is at most 1024 characters
764
+ - an embed author name is at most 256 characters
765
+ - an embed footer's text is at most 2048 characters
766
+ - `setImage` and `setThumbnail` urls are at most 2048 characters (author and footer icons
767
+ stay uncapped)
768
+ - the embeds on one message carry at most 6000 characters in total, summed across every
769
+ title, description, field name and value, author name and footer text — checked when the
770
+ message is sent, since it spans every embed, not one
771
+
772
+ A card that never approaches these sizes is unaffected. One built past a limit now throws
773
+ locally, with the same sentence the server used to send back as a 400 — the SDK catches it
774
+ before the round trip instead of after. The server enforces the identical caps independently
775
+ of SDK version, so a bot on an older SDK sees the 400 from the wire the moment this ships, not
776
+ only on upgrade. Six matching error classes ship for the wire-error path, all
777
+ `InvalidRequestError`: `EmbedFieldNameTooLong`, `EmbedFieldValueTooLong`,
778
+ `EmbedAuthorNameTooLong`, `EmbedFooterTextTooLong`, `ImageURLTooLong` and `EmbedsTooLong`.
779
+
780
+ `mentions` got stricter too, and this is also a server-side change that applies regardless of
781
+ SDK version: a `@handle` token that appears only inside a code block or inline code no longer
782
+ satisfies the mention, and the server answers `mention_token_missing` the same way it always
783
+ has for a missing token — write the handle into `text` yourself, in plain text, outside any
784
+ fence or backtick.
785
+
786
+ Two fixes land alongside the caps: `ctx.edit(sent, { forUser: null })` with no other part
787
+ named is now a real edit that reaches the wire as `{ "for_user": null }`, rather than being
788
+ refused locally before the round trip — naming only the lock is a real change to the card.
789
+ And a successful button-cooldown ack now logs one `info` line naming the message, the button,
790
+ the presser and the window, so a cooldown firing and a press silently vanishing no longer look
791
+ identical from the outside.
792
+
674
793
  ## Shadowed commands
675
794
 
676
795
  A bot can declare up to 50 commands, the SDK refuses to connect past that.
@@ -686,6 +805,42 @@ aurival warn: /ping is shadowed by another bot in chat_01j…, so it will not re
686
805
  Rename the command, or get the other bot out of that chat. Nothing else in the SDK reacts
687
806
  to it — a shadowed command in one chat is still live in every other.
688
807
 
808
+ ## Command aliases
809
+
810
+ `aliases` on `bot.command(...)` gives one command several spellings, all handled by the same
811
+ function. `/roll` and `/r` fire the same handler, share the same cooldown bucket, and are one
812
+ command everywhere the server or the app talks about it.
813
+
814
+ ```ts
815
+ bot.command('roll', { description: 'Roll dice', aliases: ['r'] }, async (ctx) => {
816
+ await ctx.reply(`/${ctx.invokedAs} ran ${ctx.command}, dice rolled`);
817
+ });
818
+ ```
819
+
820
+ `ctx.command` is always the canonical name, `'roll'`, whichever spelling fired the handler.
821
+ `ctx.invokedAs` is the token the human actually typed, lowercased: `'roll'` or `'r'`. They are
822
+ equal on a canonical call. Code that checks `ctx.command === 'roll'` keeps working no matter
823
+ which spelling reached it.
824
+
825
+ The options object also takes `cooldown`, same as any other command. It is one bucket, keyed on
826
+ the canonical name, shared across every spelling:
827
+
828
+ ```ts
829
+ bot.command('roll', { description: 'Roll dice', aliases: ['r'], cooldown: { rate: 1, per: 5 } }, handler);
830
+ ```
831
+
832
+ At most three aliases per command. A fourth throws locally, before anything is synced. It is the
833
+ same validation path as a bad command name: lowercase letters, digits, hyphens and underscores
834
+ only, and none of the reserved names (`help`, `report`, `block`, `mute`, `kick`, `ban`, `admin`,
835
+ `staff`, `support`, `aurival`).
836
+
837
+ The cooldown notice echoes the typed token: hit the limit through `/r` and you get `Slow down.
838
+ Try /r again in 3 s.`, never `/roll`.
839
+
840
+ In the app, the command picker lists a command's aliases as a muted secondary line under its
841
+ row: `also /r`. Picking a row that matched by alias fills that alias, not the canonical
842
+ spelling: typing `/r` and picking the row inserts `/r `, not `/roll `.
843
+
689
844
  ## Environment
690
845
 
691
846
  | variable | what it does |
package/dist/bot.d.ts CHANGED
@@ -78,6 +78,19 @@ export interface CommandOptions {
78
78
  description?: string | undefined;
79
79
  cooldown?: CooldownLike | undefined;
80
80
  onCooldown?: OnCooldownHook | undefined;
81
+ /**
82
+ * AMENDMENT-09 §2.1: alternate spellings that fire this same handler —
83
+ * `/r` and `/dice` for a command registered as `roll`, say. At most
84
+ * {@link MAX_ALIASES_PER_COMMAND}, checked locally at registration; every
85
+ * other rule an alias obeys (the name pattern, the reserved list,
86
+ * uniqueness) is the server's alone (§2.1) — this SDK reuses
87
+ * `invalid_command_name` for those exactly as the server does, by simply
88
+ * not re-validating them here and letting the sync refusal surface.
89
+ * Dispatch, the cooldown bucket and `onCooldown` all stay keyed on the
90
+ * canonical name; an alias only ever changes which typed token reaches
91
+ * that same handler (`ctx.invokedAs`, `Context`).
92
+ */
93
+ aliases?: readonly string[] | undefined;
81
94
  }
82
95
  /**
83
96
  * How long a command handler runs before the chat is told the bot is thinking
@@ -97,6 +110,17 @@ export declare function ensurePaired(http: HttpClient, keyFile: KeyFile, host: s
97
110
  machine: Machine;
98
111
  paired: boolean;
99
112
  }>;
113
+ /**
114
+ * One `PUT /v1/bots/{bot}/commands` row. `aliases` is optional and, when
115
+ * present, always non-empty — the caller omits the key entirely for a
116
+ * command with no aliases (AMENDMENT-09 §2.1: absent and `[]` mean the same
117
+ * thing, so there is no reason to send the empty spelling).
118
+ */
119
+ export interface SyncPayloadEntry {
120
+ name: string;
121
+ description: string;
122
+ aliases?: string[];
123
+ }
100
124
  /**
101
125
  * The one door to command sync, so the lane that syncs is always the lane that
102
126
  * reports. Both `Bot` call sites go through here.
@@ -104,10 +128,7 @@ export declare function ensurePaired(http: HttpClient, keyFile: KeyFile, host: s
104
128
  * Exported for the tests, not from `index.ts` — the package's export list
105
129
  * mirrors Python's `__all__` (SDK-7) and this is not on it.
106
130
  */
107
- export declare function syncCommandsAndReport(http: HttpClient, bot: string, payload: Array<{
108
- name: string;
109
- description: string;
110
- }>, log: Logger): Promise<void>;
131
+ export declare function syncCommandsAndReport(http: HttpClient, bot: string, payload: SyncPayloadEntry[], log: Logger): Promise<void>;
111
132
  /**
112
133
  * One warning line per shadowed chat (S11). Silently dropping `conflicts` is
113
134
  * the silence the field exists to end: the developer whose command never fires.
package/dist/bot.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"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"}
package/dist/bot.js CHANGED
@@ -3,7 +3,7 @@ import { Auth, KeyFile, machineLabel, pair, resolveHost } from './auth.js';
3
3
  import { AurivalError, BotSuspended, ButtonAlreadyUsed, NotFound, RateLimitError, SessionSuperseded } from './errors.js';
4
4
  import { Context, Event, contextFor } from './events.js';
5
5
  import { DEFAULT_HOST, HttpClient, defaultLogger } from './http.js';
6
- import { commandCooldownNotice } from './caps.js';
6
+ import { CAP_TOO_MANY_ALIASES, MAX_ALIASES_PER_COMMAND, commandCooldownNotice } from './caps.js';
7
7
  import { Cooldown, buttonBucketKey, normalizeCooldownOption, resolveButtonCooldown, retryAfterMs, roundSeconds, subjectKey, } from './cooldown.js';
8
8
  import { EVENT_BACKLOG_OVERFLOWED, EVENT_COMMAND_INVOKED, Socket } from './socket.js';
9
9
  import * as status from './status.js';
@@ -139,6 +139,7 @@ export class Bot {
139
139
  let handler;
140
140
  let cooldownLike;
141
141
  let onCooldownHook;
142
+ let aliasesLike;
142
143
  if (typeof second === 'string') {
143
144
  description = second;
144
145
  handler = third;
@@ -150,14 +151,21 @@ export class Bot {
150
151
  description = second.description ?? '';
151
152
  cooldownLike = second.cooldown;
152
153
  onCooldownHook = second.onCooldown;
154
+ aliasesLike = second.aliases;
153
155
  handler = third;
154
156
  }
155
157
  if (handler === undefined)
156
158
  throw new AurivalError(`command(${name}) needs a handler`);
159
+ // AMENDMENT-09 §2.1: `[]` in every state but never `undefined` — a
160
+ // `Command` this SDK constructs always carries the list, even empty.
161
+ const aliases = aliasesLike !== undefined ? [...aliasesLike] : [];
162
+ if (aliases.length > MAX_ALIASES_PER_COMMAND) {
163
+ throw new AurivalError(CAP_TOO_MANY_ALIASES);
164
+ }
157
165
  const key = lookupKey(name);
158
166
  if (this.#registered.has(key))
159
167
  status.duplicateCommand(name, this.#quiet);
160
- const registered = { command: { name, description }, handler };
168
+ const registered = { command: { name, description, aliases }, handler };
161
169
  if (cooldownLike !== undefined)
162
170
  registered.cooldown = Cooldown.from(cooldownLike);
163
171
  if (onCooldownHook !== undefined)
@@ -285,10 +293,16 @@ export class Bot {
285
293
  * and land the sync in the background.
286
294
  */
287
295
  async #syncCommands(http, bot, signal) {
288
- const payload = [...this.#registered.values()].map((r) => ({
289
- name: r.command.name,
290
- description: r.command.description,
291
- }));
296
+ const payload = [...this.#registered.values()].map((r) => {
297
+ const entry = { name: r.command.name, description: r.command.description };
298
+ // AMENDMENT-09 §2.1: the key is present only when the list is
299
+ // non-empty — absent and `[]` mean the same thing on this wire, so
300
+ // there is nothing to gain from sending the empty spelling.
301
+ if (r.command.aliases !== undefined && r.command.aliases.length > 0) {
302
+ entry.aliases = [...r.command.aliases];
303
+ }
304
+ return entry;
305
+ });
292
306
  try {
293
307
  await syncCommandsAndReport(http, bot, payload, this.#log);
294
308
  return null;
@@ -424,7 +438,11 @@ export class Bot {
424
438
  }
425
439
  const n = roundSeconds(refusedAfterSeconds);
426
440
  try {
427
- await ctx.reply(commandCooldownNotice(ctx.command, n));
441
+ // AMENDMENT-09 §13.1: names the token the human actually typed, not
442
+ // the canonical spelling — the bucket is still the canonical
443
+ // command's (one bucket, all spellings); only the rendered sentence
444
+ // changes.
445
+ await ctx.reply(commandCooldownNotice(ctx.invokedAs, n));
428
446
  }
429
447
  catch (exc) {
430
448
  this.#log.error(`cooldown notice for ${JSON.stringify(ctx.command)} failed: ${exc instanceof Error ? (exc.stack ?? exc.message) : String(exc)}`);
@@ -468,6 +486,14 @@ export class Bot {
468
486
  return;
469
487
  try {
470
488
  await this.#http.ackCooldown(ctx.interaction, ms);
489
+ // The ack landed. Without this line a button cooldown is invisible to
490
+ // the bot author: the SDK answers the press itself, the handler never
491
+ // runs, and nothing is written anywhere — so a cooldown firing and a
492
+ // press vanishing look identical from the outside. `info`, not
493
+ // `debug`, because the swallowed-refusal lines below are the ones a
494
+ // reader can ignore; this one is the cooldown working. Byte-mirrors
495
+ // `sdk/python/aurival/bot.py`'s sentence (SDK-7).
496
+ this.#log.info(`cooldown: acked press on ${ctx.message.id}/${ctx.button} for ${ctx.user.id}, retry_after_ms=${ms}`);
471
497
  }
472
498
  catch (exc) {
473
499
  if (exc instanceof ButtonAlreadyUsed || exc instanceof NotFound) {