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 +12 -0
- package/README.md +2 -0
- package/bin/stdout-chat.js +1 -1
- package/lib/api.js +14 -0
- package/lib/session.js +26 -0
- package/package.json +1 -1
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
|
|
package/bin/stdout-chat.js
CHANGED
|
@@ -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));
|