stdout-chat 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to `stdout-chat` (the CLI). Dates are release dates.
4
4
 
5
+ ## [0.3.0] — 2026-09-20
6
+
7
+ ### What's New
8
+
9
+ - `/dm <nick|sid>` at the prompt invites someone from #void to a private chat. Pass a nick, or the id of one of their lines (the dim column on the left) to target that author. The invite lives 10 minutes; accepting happens in the app, and the private chat itself opens on your phone — the CLI prints the server's answer (`invite sent · nova has 10 min · you'll get a push when they accept`) or its refusal verbatim (`not_found`, `busy`, `rate_limited` …).
10
+
11
+ ### Technical
12
+
13
+ - `lib/api.js`: `dm({ nick | sid }, key)` → `POST /void/dm`, JSON body `{sid}` or `{nick}`, `Accept: application/json`, returns `{ id, to, expires_at }`. Errors go through the existing `ApiError` mapping (server `message` verbatim, `retry_after` honoured).
14
+ - `lib/session.js`: `cmdDm` — no key → the usual hint, no argument → `usage: /dm <nick|sid>`, argument equal to a `sid` seen this session → `{sid}`, otherwise `{nick}`; the success line is built from the returned `to`.
15
+ - Tests: 76 → 82 (`/dm` sid/nick routing, usage, no-key, verbatim errors; `api.dm` request shape).
16
+
5
17
  ## [0.2.0] — 2026-09-20
6
18
 
7
19
  ### What's New
package/README.md CHANGED
@@ -47,6 +47,7 @@ The key is checked against the server, then saved to `~/.config/stdout-chat/conf
47
47
  | `/r <id> text` | reply to a line — ids are the dim column on the left |
48
48
  | `/top` | this week's top authors |
49
49
  | `/who` | how many are in the room |
50
+ | `/dm <nick|sid>` | invite them to a private chat — a line's id targets its author; the chat itself opens on your phone |
50
51
  | `/key sc_…` | save a key · `/key` shows who you are · `/key off` forgets it |
51
52
  | `/notify` | desktop banners: `/notify` shows the level · `/notify mentions` (default) · `all` · `off` |
52
53
  | `/clear` | clear the screen |
@@ -97,6 +98,7 @@ void() { # void · void -f · void -r a1b4 text · void some words
97
98
  - `GET /void` for history, `GET /void/stream` (Server-Sent Events) for the live feed, `POST /void` to speak, `GET /void/me` to check a key. All JSON.
98
99
  - Reconnects with backoff (1 → 30 s) and `Last-Event-ID`, so nothing is missed across the server's 15-minute stream rotation.
99
100
  - Plain scrolling output with a `readline` prompt: no alternate screen, no curses — works in tmux splits and over ssh.
101
+ - `POST /void/dm` sends a private-chat invite (`/dm`). The invite lives 10 minutes; when they accept you get a push and the private chat opens in the app on your phone — the terminal only sends the invite and prints what the server says.
100
102
 
101
103
  ## Notifications
102
104
 
@@ -25,7 +25,7 @@ usage: npx stdout-chat [options]
25
25
  -v, --version print the version
26
26
 
27
27
  at the prompt:
28
- /help /r <id> text /top /who /key sc_… /key off /notify /clear /quit
28
+ /help /r <id> text /dm <nick|sid> /top /who /key sc_… /key off /notify /clear /quit
29
29
  anything else is posted to #void
30
30
 
31
31
  desktop banners (macOS / Linux) when someone replies to you or writes @you —
package/lib/api.js CHANGED
@@ -90,6 +90,20 @@ export function createApi({ base = DEFAULT_API, version = '0.0.0', fetchImpl = n
90
90
  });
91
91
  },
92
92
 
93
+ /**
94
+ * POST /void/dm {sid} | {nick} → 201 { id, to, expires_at }. Same shape as
95
+ * post(): JSON in, JSON out (Accept: application/json); the session builds
96
+ * the human line from `to`. Errors carry the server's message verbatim.
97
+ */
98
+ dm({ nick, sid } = {}, key) {
99
+ const body = sid != null && sid !== '' ? { sid: String(sid) } : { nick: String(nick == null ? '' : nick) };
100
+ return json(`${root}/void/dm`, {
101
+ method: 'POST',
102
+ headers: headers({ 'Content-Type': 'application/json' }, key),
103
+ body: JSON.stringify(body),
104
+ });
105
+ },
106
+
93
107
  /** GET /void/stream → Response (200, body is a ReadableStream). Throws ApiError on non-2xx. */
94
108
  async openStream({ lastEventId = null, signal = null } = {}) {
95
109
  const extra = { 'Cache-Control': 'no-cache' };
package/lib/session.js CHANGED
@@ -15,6 +15,7 @@ export const HELP_TEXT = [
15
15
  '/r <id> text reply to a line (ids are the dim column on the left)',
16
16
  '/top this week\'s top authors',
17
17
  '/who how many are in the room',
18
+ '/dm <nick|sid> invite them to a private chat (accept happens in the app)',
18
19
  '/key sc_… save your key (get it in the app: /key)',
19
20
  '/key off forget the key on this machine (revoke it in the app)',
20
21
  '/notify desktop banners: mentions (default) · all · off',
@@ -309,6 +310,9 @@ export class Session {
309
310
  case '/who':
310
311
  await this.cmdWho();
311
312
  return;
313
+ case '/dm':
314
+ await this.cmdDm(rest);
315
+ return;
312
316
  case '/key':
313
317
  await this.cmdKey(rest);
314
318
  return;
@@ -352,6 +356,28 @@ export class Session {
352
356
  this.info(this.count === 1 ? '1 in room' : `${this.count} in room`);
353
357
  }
354
358
 
359
+ /** `/dm <nick|sid>`: a sid seen this session targets that line's author, anything else is a nick. */
360
+ async cmdDm(arg) {
361
+ if (!this.key) { this.info(HINT_NO_KEY); return; }
362
+ if (!arg) { this.info('usage: /dm <nick|sid>'); return; }
363
+ const target = this.isKnownSid(arg) ? { sid: arg } : { nick: arg };
364
+ let res;
365
+ try {
366
+ res = await this.api.dm(target, this.key);
367
+ } catch (err) {
368
+ this.error(errorMessage(err));
369
+ return;
370
+ }
371
+ const to = res && res.to != null && res.to !== '' ? String(res.to) : arg;
372
+ this.info(`invite sent · ${to} has 10 min · you'll get a push when they accept`);
373
+ }
374
+
375
+ /** True when `s` is the short id (the dim left column) of a line seen this session. */
376
+ isKnownSid(s) {
377
+ for (const m of this.lines.values()) if (m && m.sid != null && String(m.sid) === s) return true;
378
+ return false;
379
+ }
380
+
355
381
  async cmdKey(arg) {
356
382
  if (!arg) {
357
383
  if (this.me) this.info(greeting(this.me));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stdout-chat",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "#void from your terminal — read, tail and post to stdout.chat's public room. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {