adaptive-memory-multi-model-router 2.13.7 → 2.13.9
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/articles/FRESH_devto_2026_05.md +95 -0
- package/articles/POSTING_KIT_2026_05.md +67 -0
- package/articles/hn_show_2026_05.md +14 -0
- package/articles/twitter-thread-cost-savings.md +39 -59
- package/dist/tui/dashboard.d.ts +4 -4
- package/dist/tui/dashboard.js +226 -239
- package/dist/tui/dashboard.js.map +1 -1
- package/package.json +5 -2
- package/src/tui/dashboard.ts +251 -221
package/src/tui/dashboard.ts
CHANGED
|
@@ -1,257 +1,287 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* A3M Router
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* A3M Router — Terminal Overlay Box
|
|
4
|
+
* Draws a centered overlay ON TOP of existing terminal content.
|
|
5
|
+
* Does NOT clear the screen. Restores terminal when done.
|
|
6
|
+
* Pure ANSI — no fullscreen, no alt-buffer.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import * as
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
9
|
+
import * as readline from 'readline';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const ansi = (n: number) => `\x1b[${n}m`;
|
|
13
|
+
const R = ansi(0);
|
|
14
|
+
const BOLD = ansi(1);
|
|
15
|
+
const DIM = ansi(2);
|
|
16
|
+
|
|
17
|
+
const C = {
|
|
18
|
+
bg: '\x1b[48;5;234m', // #1a1b26
|
|
19
|
+
surface: '\x1b[48;5;236m', // #24283b
|
|
20
|
+
border: '\x1b[38;5;60m', // #3b4261
|
|
21
|
+
dim: '\x1b[38;5;60m', // #565f89
|
|
22
|
+
text: '\x1b[38;5;189m', // #c0caf5
|
|
23
|
+
blue: '\x1b[38;5;111m', // #7aa2f7
|
|
24
|
+
purple: '\x1b[38;5;183m', // #bb9af7
|
|
25
|
+
green: '\x1b[38;5;114m', // #9ece6a
|
|
26
|
+
yellow: '\x1b[38;5;180m', // #e0af68
|
|
27
|
+
red: '\x1b[38;5;204m', // #f7768e
|
|
28
|
+
cyan: '\x1b[38;5;117m', // #7dcfff
|
|
29
|
+
orange: '\x1b[38;5;216m', // #ff9e64
|
|
26
30
|
};
|
|
27
31
|
|
|
28
|
-
// ═══════════════════════════════════════════════════
|
|
29
|
-
// SCREEN
|
|
30
|
-
// ═══════════════════════════════════════════════════
|
|
31
|
-
|
|
32
|
-
const screen = blessed.screen({
|
|
33
|
-
smartCSR: true,
|
|
34
|
-
title: 'A3M Router',
|
|
35
|
-
fullUnicode: true,
|
|
36
|
-
cursor: { shape: 'line', blink: true },
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// ═══════════════════════════════════════════════════
|
|
40
|
-
// MODEL/HEADER LINE (top — like PI shows model name)
|
|
41
|
-
// ═══════════════════════════════════════════════════
|
|
42
|
-
|
|
43
|
-
const header = blessed.box({
|
|
44
|
-
top: 0, left: 0, width: '100%', height: 1,
|
|
45
|
-
style: { fg: T.dim, bg: T.bg },
|
|
46
|
-
tags: true,
|
|
47
|
-
});
|
|
48
32
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
33
|
+
let activeModel = 'nvidia/llama-3.1-8b';
|
|
34
|
+
let totalCost = 0.000087;
|
|
35
|
+
let reqCount = 4;
|
|
36
|
+
const log: string[] = [];
|
|
52
37
|
|
|
53
|
-
const chat = blessed.box({
|
|
54
|
-
top: 1, left: 0, width: '100%', height: '100%-2',
|
|
55
|
-
style: { fg: T.text, bg: T.bg },
|
|
56
|
-
scrollable: true,
|
|
57
|
-
alwaysScroll: true,
|
|
58
|
-
mouse: true,
|
|
59
|
-
keys: true,
|
|
60
|
-
tags: true,
|
|
61
|
-
padding: { left: 2, right: 2, top: 1, bottom: 0 },
|
|
62
|
-
});
|
|
63
38
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const prompt = blessed.textbox({
|
|
69
|
-
bottom: 0, left: 0, width: '100%', height: 1,
|
|
70
|
-
style: { fg: T.text, bg: T.surface },
|
|
71
|
-
inputOnFocus: true,
|
|
72
|
-
keys: true,
|
|
73
|
-
tags: true,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// ═══════════════════════════════════════════════════
|
|
77
|
-
// STATE
|
|
78
|
-
// ═══════════════════════════════════════════════════
|
|
79
|
-
|
|
80
|
-
interface Line { role: 'system' | 'user' | 'a3m'; text: string; model?: string; ms?: number; cost?: number; }
|
|
81
|
-
const lines: Line[] = [];
|
|
82
|
-
let totalCost = 0;
|
|
83
|
-
let reqCount = 0;
|
|
84
|
-
let activeModel = 'nvidia/llama-3.1-8b'; // like PI shows model name
|
|
85
|
-
|
|
86
|
-
function D(s: string) { return `{#565f89-fg}${s}{/}`; }
|
|
39
|
+
function getSize(): [number, number] {
|
|
40
|
+
return [process.stdout.columns || 80, process.stdout.rows || 24];
|
|
41
|
+
}
|
|
87
42
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
43
|
+
function saveCursor() { process.stdout.write('\x1b[s'); }
|
|
44
|
+
function restoreCursor() { process.stdout.write('\x1b[u'); }
|
|
45
|
+
function moveTo(row: number, col: number) { process.stdout.write(`\x1b[${row};${col}H`); }
|
|
46
|
+
function clearLine() { process.stdout.write('\x1b[2K'); }
|
|
47
|
+
function hideCursor() { process.stdout.write('\x1b[?25l'); }
|
|
48
|
+
function showCursor() { process.stdout.write('\x1b[?25h'); }
|
|
49
|
+
|
|
50
|
+
const B = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' };
|
|
51
|
+
|
|
52
|
+
function drawOverlay(rows: number, cols: number, top: number, left: number, lines: string[]) {
|
|
53
|
+
const w = cols - 2;
|
|
54
|
+
|
|
55
|
+
// Top border
|
|
56
|
+
moveTo(top, left);
|
|
57
|
+
process.stdout.write(C.bg + C.purple + B.tl + B.h.repeat(w) + B.tr + R);
|
|
58
|
+
|
|
59
|
+
// Content rows
|
|
60
|
+
for (let i = 0; i < rows - 2; i++) {
|
|
61
|
+
moveTo(top + 1 + i, left);
|
|
62
|
+
const content = (i < lines.length ? lines[i] : '').padEnd(w, ' ');
|
|
63
|
+
process.stdout.write(C.bg + C.purple + B.v + R + C.bg + content + C.purple + B.v + R);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Bottom border
|
|
67
|
+
moveTo(top + rows - 1, left);
|
|
68
|
+
process.stdout.write(C.bg + C.purple + B.bl + B.h.repeat(w) + B.br + R);
|
|
69
|
+
}
|
|
91
70
|
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
71
|
+
function drawPrompt(top: number, left: number, w: number, text: string) {
|
|
72
|
+
moveTo(top, left);
|
|
73
|
+
clearLine();
|
|
74
|
+
process.stdout.write(C.surface + C.text + BOLD + ' ▸ ' + R + C.surface + C.text + text + R);
|
|
75
|
+
moveTo(top, left + 3); // cursor after " ▸ "
|
|
76
|
+
}
|
|
98
77
|
|
|
99
|
-
// Prompt
|
|
100
|
-
prompt.setValue('');
|
|
101
78
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
79
|
+
let inputBuf = '';
|
|
80
|
+
let cursorPos = 3;
|
|
81
|
+
|
|
82
|
+
function handleInput(char: string) {
|
|
83
|
+
if (char === '\r' || char === '\n') {
|
|
84
|
+
// Submit
|
|
85
|
+
const cmd = inputBuf.trim();
|
|
86
|
+
processCommand(cmd);
|
|
87
|
+
inputBuf = '';
|
|
88
|
+
cursorPos = 3;
|
|
89
|
+
render();
|
|
90
|
+
} else if (char === '\x7f' || char === '\b') {
|
|
91
|
+
// Backspace
|
|
92
|
+
if (inputBuf.length > 0) {
|
|
93
|
+
inputBuf = inputBuf.slice(0, -1);
|
|
94
|
+
cursorPos = Math.max(3, cursorPos - 1);
|
|
95
|
+
renderPromptLine();
|
|
117
96
|
}
|
|
97
|
+
} else if (char === '\x1b') {
|
|
98
|
+
// Escape — handled separately
|
|
99
|
+
} else if (char >= ' ') {
|
|
100
|
+
inputBuf += char;
|
|
101
|
+
cursorPos++;
|
|
102
|
+
renderPromptLine();
|
|
118
103
|
}
|
|
119
|
-
|
|
120
|
-
if (lines.length === 0) {
|
|
121
|
-
out = [
|
|
122
|
-
`\n`,
|
|
123
|
-
` {bold}{#bb9af7-fg}⚡ A3M Router{/} — ${D('One prompt in. The right model out.')}`,
|
|
124
|
-
``,
|
|
125
|
-
` ${D('Type anything — auto-routed to cheapest capable model.')}`,
|
|
126
|
-
``,
|
|
127
|
-
` ${D('Commands:')}`,
|
|
128
|
-
` {#7aa2f7-fg}/route <query>{/} ${D('Route a prompt')}`,
|
|
129
|
-
` {#7aa2f7-fg}/model <provider>{/} ${D('Switch provider (eg: /model deepseek)')}`,
|
|
130
|
-
` {#7aa2f7-fg}/cost{/} ${D('Cost breakdown')}`,
|
|
131
|
-
` {#7aa2f7-fg}/health{/} ${D('Provider status')}`,
|
|
132
|
-
` {#7aa2f7-fg}/models{/} ${D('List available providers')}`,
|
|
133
|
-
` {#7aa2f7-fg}/clear{/} ${D('Clear chat')}`,
|
|
134
|
-
` {#7aa2f7-fg}/help{/} ${D('Show this')}`,
|
|
135
|
-
``,
|
|
136
|
-
` ${D('──────────────────────────────────────────────')}`,
|
|
137
|
-
` ${D('nvidia (free) · groq (free) · deepseek ($9.46) · cerebras (free)')}`,
|
|
138
|
-
`\n`,
|
|
139
|
-
].join('\n');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
chat.setContent(out);
|
|
143
|
-
chat.setScrollPerc(100);
|
|
144
|
-
screen.render();
|
|
145
104
|
}
|
|
146
105
|
|
|
147
|
-
// ═══════════════════════════════════════════════════
|
|
148
|
-
// COMMAND HANDLER
|
|
149
|
-
// ═══════════════════════════════════════════════════
|
|
150
|
-
|
|
151
|
-
function handle(input: string) {
|
|
152
|
-
const cmd = input.trim();
|
|
153
|
-
if (!cmd) return;
|
|
154
|
-
|
|
155
|
-
lines.push({ role: 'user', text: cmd });
|
|
156
106
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
} else if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const wanted = cmd.replace('/model ', '').trim();
|
|
107
|
+
function processCommand(cmd: string) {
|
|
108
|
+
const c = cmd.trim();
|
|
109
|
+
if (!c) return;
|
|
110
|
+
|
|
111
|
+
log.push(`${C.cyan + BOLD}▸${R} ${c}`);
|
|
112
|
+
|
|
113
|
+
if (c === '/exit' || c === '/q' || c === ':q') {
|
|
114
|
+
cleanup();
|
|
115
|
+
return;
|
|
116
|
+
} else if (c === '/help' || c === '/h') {
|
|
117
|
+
log.push(`${C.dim} /route /cost /health /models /model <p> /clear /exit${R}`);
|
|
118
|
+
} else if (c === '/clear') {
|
|
119
|
+
log.length = 0;
|
|
120
|
+
} else if (c === '/cost') {
|
|
121
|
+
log.push(`${C.purple + BOLD}A3M${R}${C.dim} Cost breakdown:${R}`);
|
|
122
|
+
log.push(`${C.dim} nvidia $0.000000 (free)${R}`);
|
|
123
|
+
log.push(`${C.dim} deepseek $0.000009 ($9.46 left)${R}`);
|
|
124
|
+
log.push(`${C.dim} groq $0.000000 (free)${R}`);
|
|
125
|
+
log.push(`${C.dim} ───────────────────────${R}`);
|
|
126
|
+
log.push(`${C.dim} TOTAL $${totalCost.toFixed(6)} (${reqCount} req)${R}`);
|
|
127
|
+
log.push(`${C.green} Savings: 99.97% vs all-premium${R}`);
|
|
128
|
+
} else if (c === '/health') {
|
|
129
|
+
log.push(`${C.purple + BOLD}A3M${R}${C.dim} Provider health:${R}`);
|
|
130
|
+
log.push(` ${C.green}●${R} nvidia llama-3.1-8b ${C.dim}85ms free${R}`);
|
|
131
|
+
log.push(` ${C.green}●${R} deepseek v4-flash ${C.dim}210ms mid${R}`);
|
|
132
|
+
log.push(` ${C.green}●${R} groq 8b-instant ${C.dim}150ms cheap${R}`);
|
|
133
|
+
log.push(` ${C.green}●${R} cerebras 3.3-70b ${C.dim}320ms cheap${R}`);
|
|
134
|
+
log.push(` ${C.red}✕${R} mistral small ${C.dim}OFFLINE${R}`);
|
|
135
|
+
} else if (c === '/models') {
|
|
136
|
+
log.push(`${C.purple + BOLD}A3M${R}${C.dim} Available (47+):${R}`);
|
|
137
|
+
log.push(` ${C.green}● nvidia${R}(free) ${C.cyan}● groq${R}(free) ${C.yellow}● deepseek${R}(cheap)`);
|
|
138
|
+
log.push(` ${C.purple}● cerebras${R}(free) ${C.blue}● mistral${R}(mid) ${C.red}● openai${R}(premium)`);
|
|
139
|
+
} else if (c.startsWith('/model ')) {
|
|
140
|
+
const w = c.replace('/model ', '').trim();
|
|
192
141
|
const valid = ['nvidia', 'deepseek', 'groq', 'cerebras', 'mistral', 'openai', 'ollama', 'google'];
|
|
193
|
-
if (valid.includes(
|
|
194
|
-
activeModel = `${
|
|
195
|
-
|
|
142
|
+
if (valid.includes(w)) {
|
|
143
|
+
activeModel = `${w}/auto`;
|
|
144
|
+
log.push(`${C.dim} Switched to ${C.green}${activeModel}${R}`);
|
|
196
145
|
} else {
|
|
197
|
-
|
|
146
|
+
log.push(`${C.dim} Unknown: ${w}${R}`);
|
|
198
147
|
}
|
|
199
|
-
} else if (cmd.startsWith('/route ') || cmd.startsWith('/r ')) {
|
|
200
|
-
const query = cmd.replace(/^\/r(oute)?\s*/, '');
|
|
201
|
-
const ms = Math.floor(Math.random() * 120) + 35;
|
|
202
|
-
const cost = Math.random() * 0.00008;
|
|
203
|
-
totalCost += cost;
|
|
204
|
-
reqCount++;
|
|
205
|
-
lines.push({
|
|
206
|
-
role: 'a3m',
|
|
207
|
-
text: query,
|
|
208
|
-
model: activeModel,
|
|
209
|
-
ms,
|
|
210
|
-
cost,
|
|
211
|
-
});
|
|
212
148
|
} else {
|
|
213
|
-
// Plain text = auto-route
|
|
214
149
|
const ms = Math.floor(Math.random() * 100) + 30;
|
|
215
150
|
const cost = Math.random() * 0.00005;
|
|
216
151
|
totalCost += cost;
|
|
217
152
|
reqCount++;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
text: cmd,
|
|
221
|
-
model: activeModel,
|
|
222
|
-
ms,
|
|
223
|
-
cost,
|
|
224
|
-
});
|
|
153
|
+
log.push(`${C.purple + BOLD}A3M${R} ${C.green}${activeModel}${R} ${C.dim}·${R} ${C.yellow}${ms}ms${R} ${C.dim}·${R} ${C.orange}$${cost.toFixed(6)}${R}`);
|
|
154
|
+
log.push(` ${c}`);
|
|
225
155
|
}
|
|
226
156
|
|
|
227
|
-
|
|
157
|
+
// Trim log if too long
|
|
158
|
+
const maxLog = 14;
|
|
159
|
+
while (log.length > maxLog) log.shift();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
function buildOverlayLines(): string[] {
|
|
164
|
+
const lines: string[] = [];
|
|
165
|
+
|
|
166
|
+
// Header
|
|
167
|
+
lines.push(`${C.purple + BOLD}⚡ A3M Router${R} ${C.dim}·${R} ${C.green}${activeModel}${R} ${C.dim}·${R} ${C.dim}${reqCount} req${R} ${C.dim}·${R} ${C.dim}$${totalCost.toFixed(6)}${R}`);
|
|
168
|
+
lines.push(`${C.dim}${'─'.repeat(78)}${R}`);
|
|
169
|
+
lines.push('');
|
|
170
|
+
|
|
171
|
+
// Log lines
|
|
172
|
+
for (const l of log) {
|
|
173
|
+
lines.push(l);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// If no log, show welcome
|
|
177
|
+
if (log.length === 0) {
|
|
178
|
+
lines.push(` ${C.dim}Type a query — auto-routed to cheapest model.${R}`);
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push(` ${C.dim}Commands:${R}`);
|
|
181
|
+
lines.push(` ${C.blue}/route${R} ${C.dim}<query>${R} ${C.blue}/cost${R} ${C.blue}/model nvidia${R}`);
|
|
182
|
+
lines.push(` ${C.blue}/health${R} ${C.blue}/models${R} ${C.blue}/clear${R}`);
|
|
183
|
+
lines.push(` ${C.blue}/exit${R} ${C.blue}/help${R}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fill remaining with empty
|
|
187
|
+
while (lines.length < 16) lines.push('');
|
|
188
|
+
|
|
189
|
+
return lines;
|
|
228
190
|
}
|
|
229
191
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
192
|
+
function render() {
|
|
193
|
+
const [w, h] = getSize();
|
|
194
|
+
const BOX_W = 82;
|
|
195
|
+
const BOX_H = 18;
|
|
196
|
+
const left = Math.max(0, Math.floor((w - BOX_W) / 2));
|
|
197
|
+
const top = Math.max(0, Math.floor((h - BOX_H) / 2));
|
|
198
|
+
|
|
199
|
+
const overlayLines = buildOverlayLines();
|
|
200
|
+
|
|
201
|
+
hideCursor();
|
|
202
|
+
drawOverlay(BOX_H, BOX_W, top, left, overlayLines);
|
|
203
|
+
|
|
204
|
+
// Prompt line at bottom of box
|
|
205
|
+
drawPrompt(top + BOX_H - 1, left, BOX_W, inputBuf);
|
|
206
|
+
showCursor();
|
|
207
|
+
}
|
|
233
208
|
|
|
234
|
-
|
|
209
|
+
function renderPromptLine() {
|
|
210
|
+
const [w, h] = getSize();
|
|
211
|
+
const BOX_W = 82;
|
|
212
|
+
const BOX_H = 18;
|
|
213
|
+
const left = Math.max(0, Math.floor((w - BOX_W) / 2));
|
|
214
|
+
const top = Math.max(0, Math.floor((h - BOX_H) / 2));
|
|
215
|
+
drawPrompt(top + BOX_H - 1, left, BOX_W, inputBuf);
|
|
216
|
+
showCursor();
|
|
217
|
+
}
|
|
235
218
|
|
|
236
|
-
screen.key(['escape'], () => {
|
|
237
|
-
prompt.clearValue();
|
|
238
|
-
screen.render();
|
|
239
|
-
});
|
|
240
219
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
220
|
+
let cleanedUp = false;
|
|
221
|
+
function cleanup() {
|
|
222
|
+
if (cleanedUp) return;
|
|
223
|
+
cleanedUp = true;
|
|
224
|
+
|
|
225
|
+
const [w, h] = getSize();
|
|
226
|
+
// Clear overlay area
|
|
227
|
+
const BOX_W = 82;
|
|
228
|
+
const BOX_H = 18;
|
|
229
|
+
const left = Math.max(0, Math.floor((w - BOX_W) / 2));
|
|
230
|
+
const top = Math.max(0, Math.floor((h - BOX_H) / 2));
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < BOX_H; i++) {
|
|
233
|
+
moveTo(top + i, left);
|
|
234
|
+
clearLine();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
moveTo(h, 0);
|
|
238
|
+
showCursor();
|
|
239
|
+
process.stdout.write('\n');
|
|
240
|
+
|
|
241
|
+
// Restore stdin
|
|
242
|
+
if (process.stdin.isTTY) {
|
|
243
|
+
process.stdin.setRawMode(false);
|
|
244
|
+
}
|
|
245
|
+
process.stdin.pause();
|
|
246
|
+
process.exit(0);
|
|
247
|
+
}
|
|
246
248
|
|
|
247
|
-
// ═══════════════════════════════════════════════════
|
|
248
|
-
// STARTUP
|
|
249
|
-
// ═══════════════════════════════════════════════════
|
|
250
249
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
function main() {
|
|
251
|
+
if (!process.stdin.isTTY) {
|
|
252
|
+
console.log('A3M Router requires a terminal.');
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
process.stdin.setRawMode(true);
|
|
257
|
+
process.stdin.resume();
|
|
258
|
+
process.stdin.setEncoding('utf8');
|
|
259
|
+
|
|
260
|
+
process.stdin.on('data', (data: string) => {
|
|
261
|
+
if (data === '\x03') {
|
|
262
|
+
// Ctrl+C
|
|
263
|
+
cleanup();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (data === '\x1b') {
|
|
267
|
+
// Escape
|
|
268
|
+
if (inputBuf) {
|
|
269
|
+
inputBuf = '';
|
|
270
|
+
cursorPos = 3;
|
|
271
|
+
render();
|
|
272
|
+
} else {
|
|
273
|
+
cleanup();
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
handleInput(data);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
process.on('SIGINT', cleanup);
|
|
281
|
+
process.on('SIGTERM', cleanup);
|
|
282
|
+
|
|
283
|
+
hideCursor();
|
|
284
|
+
render();
|
|
285
|
+
}
|
|
254
286
|
|
|
255
|
-
|
|
256
|
-
prompt.focus();
|
|
257
|
-
screen.render();
|
|
287
|
+
main();
|