@askalf/dario 3.38.6 → 4.0.1

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.
@@ -777,7 +777,7 @@ export function _resetInstalledVersionProbeForTest() {
777
777
  */
778
778
  export const SUPPORTED_CC_RANGE = {
779
779
  min: '1.0.0',
780
- maxTested: '2.1.142',
780
+ maxTested: '2.1.143',
781
781
  };
782
782
  /**
783
783
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
package/dist/proxy.js CHANGED
@@ -567,7 +567,12 @@ export async function startProxy(opts = {}) {
567
567
  // eventual `7d_opus`) doesn't slip past unnoticed. Pure observability —
568
568
  // routing already handles unknown families generically.
569
569
  const seenPerModelBuckets = new Set();
570
- const analytics = pool ? new Analytics() : null;
570
+ // v4 promotion: analytics is always-on so the TUI's Analytics + Hits
571
+ // tabs work in both pool and single-account mode. Pre-v4 this was
572
+ // `pool ? new Analytics() : null` — that gated the /analytics
573
+ // endpoint, but burn-rate / per-request visibility is useful for
574
+ // single-account users too.
575
+ const analytics = new Analytics();
571
576
  let status;
572
577
  if (pool) {
573
578
  for (const acc of accountsList) {
@@ -912,17 +917,70 @@ export async function startProxy(opts = {}) {
912
917
  }));
913
918
  return;
914
919
  }
915
- // Analytics endpoint — request history + burn-rate summary (pool mode only).
920
+ // Analytics endpoint — rolling-window summary + burn-rate snapshot.
921
+ // Always-on as of v4 (pre-v4 this was gated to pool mode).
916
922
  if (urlPath === '/analytics' && req.method === 'GET') {
917
- if (!analytics) {
918
- res.writeHead(200, JSON_HEADERS);
919
- res.end(JSON.stringify({ mode: 'single-account', note: 'Analytics are only collected in pool mode.' }));
920
- return;
921
- }
922
923
  res.writeHead(200, JSON_HEADERS);
923
924
  res.end(JSON.stringify(analytics.summary()));
924
925
  return;
925
926
  }
927
+ // Analytics live stream — SSE of new RequestRecord JSON, one event
928
+ // per record as it lands. Drives the v4 TUI's Hits tab. Sends a
929
+ // backlog of the most-recent 50 records on connect so a freshly-
930
+ // attached subscriber sees state immediately, then live-tails.
931
+ //
932
+ // Auth: same as /analytics — no auth in single-account default mode;
933
+ // the proxy listens on loopback by default. DARIO_API_KEY users
934
+ // get rejected by the earlier auth gate up the handler chain.
935
+ //
936
+ // Disconnect handling: the 'close' event on `req` removes our
937
+ // listener from the Analytics EventEmitter so we don't leak.
938
+ if (urlPath === '/analytics/stream' && req.method === 'GET') {
939
+ // SECURITY_HEADERS sets Cache-Control: no-store; SSE wants
940
+ // no-cache, no-transform. Spread SECURITY_HEADERS first then
941
+ // override the cache directive — order matters since spread
942
+ // overlap is last-wins in JS.
943
+ const sseHeaders = {
944
+ ...SECURITY_HEADERS,
945
+ 'Content-Type': 'text/event-stream',
946
+ 'Cache-Control': 'no-cache, no-transform',
947
+ 'Connection': 'keep-alive',
948
+ 'X-Accel-Buffering': 'no', // disable any proxy buffering
949
+ 'Access-Control-Allow-Origin': corsOrigin,
950
+ };
951
+ res.writeHead(200, sseHeaders);
952
+ // Backlog: replay recent records so a TUI attaching mid-session
953
+ // sees something. 50 is a soft default; lots of room to send more
954
+ // since this is one-time on connect.
955
+ for (const past of analytics.recent(50)) {
956
+ res.write(`data: ${JSON.stringify(past)}\n\n`);
957
+ }
958
+ // Live tail
959
+ const onRecord = (r) => {
960
+ // Use try/catch so a broken socket (peer hung up between events)
961
+ // doesn't crash the request hot-path — Analytics already wraps
962
+ // its emit in try/catch but the .write itself can also throw.
963
+ try {
964
+ res.write(`data: ${JSON.stringify(r)}\n\n`);
965
+ }
966
+ catch { /* ignored */ }
967
+ };
968
+ analytics.on('record', onRecord);
969
+ // Heartbeat every 25s — SSE comments are ignored by clients but
970
+ // keep middle-boxes (CDNs, dev-proxies) from closing the pipe.
971
+ const heartbeat = setInterval(() => {
972
+ try {
973
+ res.write(`: heartbeat ${Date.now()}\n\n`);
974
+ }
975
+ catch { /* ignored */ }
976
+ }, 25_000);
977
+ heartbeat.unref?.();
978
+ req.on('close', () => {
979
+ analytics.off('record', onRecord);
980
+ clearInterval(heartbeat);
981
+ });
982
+ return;
983
+ }
926
984
  if (urlPath === '/v1/models' && req.method === 'GET') {
927
985
  requestCount++;
928
986
  res.writeHead(200, { ...JSON_HEADERS, 'Access-Control-Allow-Origin': corsOrigin });
@@ -1585,12 +1643,19 @@ export async function startProxy(opts = {}) {
1585
1643
  }
1586
1644
  }
1587
1645
  requestCount++;
1588
- if (analytics && poolAccount) {
1646
+ // v4: analytics is always-on. Pool mode supplies the rate-limit
1647
+ // snapshot from `poolAccount.rateLimit` (already authoritative);
1648
+ // single-account mode parses it from the upstream response
1649
+ // headers on the spot so the TUI's Hits feed shows the same
1650
+ // bucket / utilization fields in both modes.
1651
+ {
1652
+ const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
1589
1653
  analytics.record({
1590
- timestamp: Date.now(), account: poolAccount.alias, model: requestModel,
1654
+ timestamp: Date.now(),
1655
+ account: poolAccount?.alias ?? ACCOUNT_KEY_SINGLE,
1656
+ model: requestModel,
1591
1657
  inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
1592
- claim: poolAccount.rateLimit.claim, util5h: poolAccount.rateLimit.util5h,
1593
- util7d: poolAccount.rateLimit.util7d, overageUtil: poolAccount.rateLimit.overageUtil,
1658
+ claim: rl.claim, util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
1594
1659
  latencyMs: Date.now() - startTime, status: 429, isStream: false, isOpenAI,
1595
1660
  });
1596
1661
  }
@@ -1669,12 +1734,14 @@ export async function startProxy(opts = {}) {
1669
1734
  }
1670
1735
  }
1671
1736
  requestCount++;
1672
- if (analytics && poolAccount) {
1737
+ {
1738
+ const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
1673
1739
  analytics.record({
1674
- timestamp: Date.now(), account: poolAccount.alias, model: requestModel,
1740
+ timestamp: Date.now(),
1741
+ account: poolAccount?.alias ?? ACCOUNT_KEY_SINGLE,
1742
+ model: requestModel,
1675
1743
  inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
1676
- claim: poolAccount.rateLimit.claim, util5h: poolAccount.rateLimit.util5h,
1677
- util7d: poolAccount.rateLimit.util7d, overageUtil: poolAccount.rateLimit.overageUtil,
1744
+ claim: rl.claim, util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
1678
1745
  latencyMs: Date.now() - startTime, status: 429, isStream: false, isOpenAI,
1679
1746
  });
1680
1747
  }
@@ -1880,14 +1947,16 @@ export async function startProxy(opts = {}) {
1880
1947
  lastResponseTime = Date.now();
1881
1948
  lastResponseTokens = streamOutputTokens;
1882
1949
  }
1883
- if (analytics && poolAccount) {
1950
+ {
1951
+ const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
1884
1952
  analytics.record({
1885
- timestamp: Date.now(), account: poolAccount.alias, model: requestModel,
1953
+ timestamp: Date.now(),
1954
+ account: poolAccount?.alias ?? ACCOUNT_KEY_SINGLE,
1955
+ model: requestModel,
1886
1956
  inputTokens: streamInputTokens, outputTokens: streamOutputTokens,
1887
1957
  cacheReadTokens: streamCacheReadTokens, cacheCreateTokens: streamCacheCreateTokens,
1888
1958
  thinkingTokens: 0,
1889
- claim: poolAccount.rateLimit.claim, util5h: poolAccount.rateLimit.util5h,
1890
- util7d: poolAccount.rateLimit.util7d, overageUtil: poolAccount.rateLimit.overageUtil,
1959
+ claim: rl.claim, util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
1891
1960
  latencyMs: Date.now() - startTime, status: upstream.status, isStream: true, isOpenAI,
1892
1961
  });
1893
1962
  }
@@ -1938,16 +2007,17 @@ export async function startProxy(opts = {}) {
1938
2007
  lastResponseTime = Date.now();
1939
2008
  lastResponseTokens = bufferedUsage?.outputTokens ?? 0;
1940
2009
  }
1941
- if (analytics && poolAccount && bufferedUsage) {
2010
+ if (bufferedUsage) {
1942
2011
  try {
2012
+ const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
1943
2013
  analytics.record({
1944
- timestamp: Date.now(), account: poolAccount.alias,
2014
+ timestamp: Date.now(),
2015
+ account: poolAccount?.alias ?? ACCOUNT_KEY_SINGLE,
1945
2016
  model: bufferedUsage.model || requestModel,
1946
2017
  inputTokens: bufferedUsage.inputTokens, outputTokens: bufferedUsage.outputTokens,
1947
2018
  cacheReadTokens: bufferedUsage.cacheReadTokens, cacheCreateTokens: bufferedUsage.cacheCreateTokens,
1948
2019
  thinkingTokens: bufferedUsage.thinkingTokens,
1949
- claim: poolAccount.rateLimit.claim, util5h: poolAccount.rateLimit.util5h,
1950
- util7d: poolAccount.rateLimit.util7d, overageUtil: poolAccount.rateLimit.overageUtil,
2020
+ claim: rl.claim, util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
1951
2021
  latencyMs: Date.now() - startTime, status: upstream.status, isStream: false, isOpenAI,
1952
2022
  });
1953
2023
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The TUI App — main render loop + lifecycle + key dispatch.
3
+ *
4
+ * The shape is deliberately simple: one render function (caller-
5
+ * supplied) that takes `state + dimensions` and returns the full frame
6
+ * as a string. The App drives the loop, manages stdin raw mode, handles
7
+ * resize events, and flushes one write per frame. Tabs (M4) are
8
+ * decoupled state machines that the caller's render() composes.
9
+ *
10
+ * Lifecycle invariants:
11
+ *
12
+ * - Enters alt-screen on start, leaves on stop. The shell that
13
+ * spawned dario sees its prior content restored when the TUI quits.
14
+ * - Hides the cursor on start, restores it on stop. ALWAYS, even on
15
+ * uncaught exceptions or SIGINT — we install signal + exit hooks.
16
+ * - Sets stdin raw mode on start, restores it on stop.
17
+ *
18
+ * The hook chain is registered on process events; quit calls them all
19
+ * synchronously so no terminal state leaks out.
20
+ */
21
+ import { type Key } from './input.js';
22
+ export interface AppOptions<S> {
23
+ /**
24
+ * Initial state. The App holds a reference and re-renders when
25
+ * `setState` is called.
26
+ */
27
+ initialState: S;
28
+ /**
29
+ * Pure function: state + dimensions → full screen content.
30
+ * The App calls this on every redraw + flushes the returned string
31
+ * to stdout in a single write.
32
+ */
33
+ render: (state: S, dim: {
34
+ cols: number;
35
+ rows: number;
36
+ }) => string;
37
+ /**
38
+ * Key dispatch. Receives every parsed key from stdin (after the
39
+ * App has already handled global keys like Ctrl-C). Return a new
40
+ * state (or undefined for no change) — same shape as React's
41
+ * setState reducer.
42
+ */
43
+ onKey: (state: S, key: Key) => S | undefined;
44
+ /**
45
+ * Optional: called on every redraw after the new frame has been
46
+ * written. Used by tabs that have async data (e.g. SSE-fed Hits
47
+ * tab) to schedule periodic refreshes.
48
+ */
49
+ afterFrame?: (state: S) => void;
50
+ /**
51
+ * stdin / stdout — overridable for tests. Defaults to process.stdin
52
+ * and process.stdout.
53
+ */
54
+ stdin?: NodeJS.ReadStream;
55
+ stdout?: NodeJS.WriteStream;
56
+ }
57
+ export declare class App<S> {
58
+ private state;
59
+ private renderFn;
60
+ private keyFn;
61
+ private afterFrameFn?;
62
+ private stdin;
63
+ private stdout;
64
+ private cleanupFns;
65
+ private running;
66
+ private redrawScheduled;
67
+ constructor(opts: AppOptions<S>);
68
+ /**
69
+ * Replace state. If the new state differs from the old (shallow
70
+ * equality), schedule a redraw. Tabs use this both for synchronous
71
+ * key-driven updates and for async data arrivals (SSE 'record').
72
+ *
73
+ * The "differs" check is intentionally shallow — deep equality on
74
+ * potentially-large analytics records would be expensive and the
75
+ * caller almost always passes new object identity when it mutates.
76
+ * If you mutate state in place and don't change identity, the
77
+ * redraw won't fire (this is documented; sometimes desired).
78
+ */
79
+ setState(updater: Partial<S> | ((s: S) => S)): void;
80
+ /** Read-only state accessor — for callers that need to compute next state from current. */
81
+ getState(): S;
82
+ /**
83
+ * Start the TUI: enter alt-screen, hide cursor, raw stdin, attach
84
+ * resize listener, render once, then idle until stop() or process
85
+ * exit.
86
+ *
87
+ * Returns a Promise that resolves when stop() is called. Wires
88
+ * process exit / signal hooks so a Ctrl-C / kill leaves the
89
+ * terminal sane.
90
+ */
91
+ start(): Promise<void>;
92
+ /** Stop the TUI — restore terminal state and resolve the start() promise. */
93
+ stop(): void;
94
+ private scheduleRedraw;
95
+ private redraw;
96
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The TUI App — main render loop + lifecycle + key dispatch.
3
+ *
4
+ * The shape is deliberately simple: one render function (caller-
5
+ * supplied) that takes `state + dimensions` and returns the full frame
6
+ * as a string. The App drives the loop, manages stdin raw mode, handles
7
+ * resize events, and flushes one write per frame. Tabs (M4) are
8
+ * decoupled state machines that the caller's render() composes.
9
+ *
10
+ * Lifecycle invariants:
11
+ *
12
+ * - Enters alt-screen on start, leaves on stop. The shell that
13
+ * spawned dario sees its prior content restored when the TUI quits.
14
+ * - Hides the cursor on start, restores it on stop. ALWAYS, even on
15
+ * uncaught exceptions or SIGINT — we install signal + exit hooks.
16
+ * - Sets stdin raw mode on start, restores it on stop.
17
+ *
18
+ * The hook chain is registered on process events; quit calls them all
19
+ * synchronously so no terminal state leaks out.
20
+ */
21
+ import { attachKeyHandler } from './input.js';
22
+ import { clearScreen, enterAltScreen, leaveAltScreen, hideCursor, showCursor, } from './render.js';
23
+ export class App {
24
+ state;
25
+ renderFn;
26
+ keyFn;
27
+ afterFrameFn;
28
+ stdin;
29
+ stdout;
30
+ cleanupFns = [];
31
+ running = false;
32
+ // Coalesce setState calls that arrive in the same tick — only one
33
+ // redraw per tick regardless of how many setStates fire.
34
+ redrawScheduled = false;
35
+ constructor(opts) {
36
+ this.state = opts.initialState;
37
+ this.renderFn = opts.render;
38
+ this.keyFn = opts.onKey;
39
+ this.afterFrameFn = opts.afterFrame;
40
+ this.stdin = opts.stdin ?? process.stdin;
41
+ this.stdout = opts.stdout ?? process.stdout;
42
+ }
43
+ /**
44
+ * Replace state. If the new state differs from the old (shallow
45
+ * equality), schedule a redraw. Tabs use this both for synchronous
46
+ * key-driven updates and for async data arrivals (SSE 'record').
47
+ *
48
+ * The "differs" check is intentionally shallow — deep equality on
49
+ * potentially-large analytics records would be expensive and the
50
+ * caller almost always passes new object identity when it mutates.
51
+ * If you mutate state in place and don't change identity, the
52
+ * redraw won't fire (this is documented; sometimes desired).
53
+ */
54
+ setState(updater) {
55
+ const next = typeof updater === 'function'
56
+ ? updater(this.state)
57
+ : { ...this.state, ...updater };
58
+ if (next !== this.state) {
59
+ this.state = next;
60
+ this.scheduleRedraw();
61
+ }
62
+ }
63
+ /** Read-only state accessor — for callers that need to compute next state from current. */
64
+ getState() {
65
+ return this.state;
66
+ }
67
+ /**
68
+ * Start the TUI: enter alt-screen, hide cursor, raw stdin, attach
69
+ * resize listener, render once, then idle until stop() or process
70
+ * exit.
71
+ *
72
+ * Returns a Promise that resolves when stop() is called. Wires
73
+ * process exit / signal hooks so a Ctrl-C / kill leaves the
74
+ * terminal sane.
75
+ */
76
+ start() {
77
+ if (this.running)
78
+ throw new Error('TUI already running');
79
+ this.running = true;
80
+ // Enter alt-screen + hide cursor in one write so the terminal
81
+ // doesn't briefly show a normal screen with a hidden cursor.
82
+ this.stdout.write(enterAltScreen + hideCursor + clearScreen);
83
+ // Raw-mode key handler
84
+ try {
85
+ const detachKeys = attachKeyHandler(this.stdin, (key) => {
86
+ // Global keys handled by the App itself; everything else
87
+ // falls through to the user's onKey reducer.
88
+ if (key.name === 'printable' && key.ctrl && key.ch === 'c') {
89
+ // Ctrl-C → quit
90
+ this.stop();
91
+ return;
92
+ }
93
+ if (key.name === 'printable' && key.ctrl && key.ch === 'l') {
94
+ // Ctrl-L → forced redraw (no state change, but force a
95
+ // re-render which also re-clears the screen — clears any
96
+ // garbage left by misbehaving processes that wrote past the
97
+ // alt-screen boundary)
98
+ this.scheduleRedraw(true);
99
+ return;
100
+ }
101
+ const next = this.keyFn(this.state, key);
102
+ if (next !== undefined && next !== this.state) {
103
+ this.state = next;
104
+ this.scheduleRedraw();
105
+ }
106
+ });
107
+ this.cleanupFns.push(detachKeys);
108
+ }
109
+ catch (err) {
110
+ // Couldn't attach to stdin (not a TTY). Restore screen state
111
+ // before propagating so we don't leave the terminal in
112
+ // alt-screen mode.
113
+ this.stdout.write(leaveAltScreen + showCursor);
114
+ this.running = false;
115
+ throw err;
116
+ }
117
+ // Window resize → redraw with new dimensions
118
+ const onResize = () => this.scheduleRedraw(true);
119
+ this.stdout.on('resize', onResize);
120
+ this.cleanupFns.push(() => this.stdout.off('resize', onResize));
121
+ // Process-level safety net — any abnormal exit should leave the
122
+ // terminal in a usable state.
123
+ const finalCleanup = () => {
124
+ if (this.running)
125
+ this.stop();
126
+ };
127
+ process.once('SIGINT', finalCleanup);
128
+ process.once('SIGTERM', finalCleanup);
129
+ process.once('exit', finalCleanup);
130
+ this.cleanupFns.push(() => {
131
+ process.off('SIGINT', finalCleanup);
132
+ process.off('SIGTERM', finalCleanup);
133
+ process.off('exit', finalCleanup);
134
+ });
135
+ // First frame
136
+ this.redraw();
137
+ return new Promise((resolve) => {
138
+ this.cleanupFns.push(() => resolve());
139
+ });
140
+ }
141
+ /** Stop the TUI — restore terminal state and resolve the start() promise. */
142
+ stop() {
143
+ if (!this.running)
144
+ return;
145
+ this.running = false;
146
+ // Run cleanup fns in reverse order so most-recent goes first
147
+ // (matches typical resource-stack semantics).
148
+ while (this.cleanupFns.length > 0) {
149
+ const fn = this.cleanupFns.pop();
150
+ try {
151
+ fn();
152
+ }
153
+ catch { /* keep unwinding */ }
154
+ }
155
+ // Final state restore
156
+ this.stdout.write(leaveAltScreen + showCursor);
157
+ }
158
+ scheduleRedraw(force = false) {
159
+ if (this.redrawScheduled && !force)
160
+ return;
161
+ this.redrawScheduled = true;
162
+ queueMicrotask(() => {
163
+ this.redrawScheduled = false;
164
+ if (!this.running)
165
+ return;
166
+ this.redraw();
167
+ });
168
+ }
169
+ redraw() {
170
+ const cols = this.stdout.columns ?? 80;
171
+ const rows = this.stdout.rows ?? 24;
172
+ const frame = this.renderFn(this.state, { cols, rows });
173
+ // Single write — minimizes flicker
174
+ this.stdout.write(clearScreen + frame);
175
+ if (this.afterFrameFn)
176
+ this.afterFrameFn(this.state);
177
+ }
178
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * TUI input handling — stdin raw-mode key parser.
3
+ *
4
+ * Why not Node's `readline.emitKeypressEvents`: it works, but the Key
5
+ * shape (`{ name, ctrl, meta, sequence }`) is loosely typed, the
6
+ * legacy event flag is awkward to disable cleanly, and its escape-
7
+ * sequence parser has historically lagged on edge cases (Windows
8
+ * Terminal modifyOtherKeys, Kitty progressive enhancement, etc).
9
+ *
10
+ * Writing ~150 lines that handle the keys we ACTUALLY use is more
11
+ * predictable. The keys we care about:
12
+ *
13
+ * - Printable ASCII (0x20-0x7e)
14
+ * - Enter, Tab, Backspace, Escape
15
+ * - Arrow up/down/left/right
16
+ * - Home, End, PgUp, PgDn
17
+ * - Ctrl+C, Ctrl+D (exit), Ctrl+L (redraw)
18
+ *
19
+ * Standalone Esc vs Esc-led sequence (e.g. arrow): we use the same
20
+ * heuristic xterm uses — if ESC arrives in the same buffer chunk as
21
+ * subsequent bytes, treat as a CSI sequence. If ESC arrives alone in
22
+ * a chunk, treat as a standalone Escape keypress. This is reliable
23
+ * on real terminals and avoids the alternative (a wait-timer + lookahead)
24
+ * which adds complexity and a delay every Esc keypress.
25
+ */
26
+ export interface Key {
27
+ /** A short name for the key. 'printable' for normal characters. */
28
+ name: 'printable' | 'enter' | 'tab' | 'backspace' | 'escape' | 'up' | 'down' | 'left' | 'right' | 'home' | 'end' | 'pageup' | 'pagedown' | 'delete' | 'unknown';
29
+ /** The printable character (or the raw sequence for `unknown`). */
30
+ ch: string;
31
+ /** True if a Ctrl modifier was held. */
32
+ ctrl: boolean;
33
+ /** True if a Shift modifier was held (only detectable on some keys). */
34
+ shift: boolean;
35
+ /** True if an Alt/Meta modifier was held. */
36
+ meta: boolean;
37
+ }
38
+ /**
39
+ * Parse one stdin chunk into zero or more Key events. Pure function;
40
+ * the caller drives stdin and accumulates the result. Most chunks
41
+ * yield exactly one key (interactive typing); paste / IME burst can
42
+ * yield several.
43
+ */
44
+ export declare function parseKeys(chunk: Buffer): Key[];
45
+ /**
46
+ * Put stdin into raw mode and start emitting Key events to `handler`.
47
+ * Returns a cleanup function that restores stdin's pre-raw mode and
48
+ * unbinds the listener. ALWAYS call the cleanup on exit (including
49
+ * abnormal exits — wire a process exit / signal hook).
50
+ *
51
+ * Defaults are picked so the caller doesn't have to think about them:
52
+ * UTF-8 encoding (we don't see Buffer chunks split mid-character),
53
+ * resume() so paused stdin doesn't drop key events.
54
+ *
55
+ * Throws if stdin isn't a TTY — the TUI doesn't make sense in a pipe.
56
+ */
57
+ export declare function attachKeyHandler(stdin: NodeJS.ReadStream, handler: (key: Key) => void): () => void;