@ar-ai/brink-cli 0.0.0-stage → 1.0.6
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 +58 -2
- package/package.json +18 -4
- package/support/activity.js +102 -0
- package/support/brand.js +110 -0
- package/support/bridge-stream.js +109 -0
- package/support/brink.js +275 -0
- package/support/chat-bridge.js +1093 -0
- package/support/client-setup.js +620 -0
- package/support/messages-bridge.js +317 -0
- package/support/messages-request.js +442 -0
- package/support/model-catalog.js +200 -0
- package/support/monitor.js +443 -0
- package/support/proxy.js +480 -0
- package/support/setup-ui.js +333 -0
- package/support/terminal-ui.js +630 -0
- package/support/transport.js +485 -0
- package/support/usage.js +144 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const readline = require("readline");
|
|
4
|
+
const { stripVTControlCharacters } = require("util");
|
|
5
|
+
const { credit, mascotLines } = require("./brand");
|
|
6
|
+
|
|
7
|
+
const ESC = "\x1b[";
|
|
8
|
+
const COLOR = {
|
|
9
|
+
base: `${ESC}48;2;19;21;24m${ESC}38;2;220;223;228m`,
|
|
10
|
+
text: `${ESC}38;2;220;223;228m`,
|
|
11
|
+
muted: `${ESC}38;2;148;155;166m`,
|
|
12
|
+
amber: `${ESC}38;2;225;177;99m`,
|
|
13
|
+
green: `${ESC}38;2;126;197;154m`,
|
|
14
|
+
red: `${ESC}38;2;238;137;137m`,
|
|
15
|
+
section: `${ESC}38;2;225;177;99m`,
|
|
16
|
+
border: `${ESC}38;2;66;72;82m`,
|
|
17
|
+
bold: `${ESC}1m`,
|
|
18
|
+
normal: `${ESC}22m`,
|
|
19
|
+
};
|
|
20
|
+
const LEVELS = ["all", "info", "warn", "error"];
|
|
21
|
+
const SEGMENTS = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
22
|
+
const COMBINING = /^[\p{Mark}\u200d\ufe0e\ufe0f]+$/u;
|
|
23
|
+
const EMOJI =
|
|
24
|
+
/\p{Emoji_Presentation}|\p{Extended_Pictographic}.*\ufe0f|[\u{1f1e6}-\u{1f1ff}]|\u20e3/u;
|
|
25
|
+
|
|
26
|
+
function clean(value, limit = 2000) {
|
|
27
|
+
return stripVTControlCharacters(String(value ?? ""))
|
|
28
|
+
.replace(/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, " ")
|
|
29
|
+
.slice(0, limit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function widthOf(segment) {
|
|
33
|
+
if (COMBINING.test(segment)) return 0;
|
|
34
|
+
if (EMOJI.test(segment)) return 2;
|
|
35
|
+
const code = segment.codePointAt(0);
|
|
36
|
+
return code >= 0x1100 &&
|
|
37
|
+
(code <= 0x115f ||
|
|
38
|
+
code === 0x2329 ||
|
|
39
|
+
code === 0x232a ||
|
|
40
|
+
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
|
|
41
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
42
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
43
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
44
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
45
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
46
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
47
|
+
(code >= 0x20000 && code <= 0x3fffd))
|
|
48
|
+
? 2
|
|
49
|
+
: 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fit(value, width) {
|
|
53
|
+
const text = clean(value);
|
|
54
|
+
const room = Math.max(0, width);
|
|
55
|
+
let result = "";
|
|
56
|
+
let used = 0;
|
|
57
|
+
for (const { segment } of SEGMENTS.segment(text)) {
|
|
58
|
+
const size = widthOf(segment);
|
|
59
|
+
if (used + size > room) break;
|
|
60
|
+
result += segment;
|
|
61
|
+
used += size;
|
|
62
|
+
}
|
|
63
|
+
return result + " ".repeat(room - used);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function wrap(value, width, maxLines = 8) {
|
|
67
|
+
const room = Math.max(1, width);
|
|
68
|
+
const lines = [];
|
|
69
|
+
let line = "";
|
|
70
|
+
let used = 0;
|
|
71
|
+
for (const { segment } of SEGMENTS.segment(clean(value))) {
|
|
72
|
+
const size = widthOf(segment);
|
|
73
|
+
if (used + size > room && line) {
|
|
74
|
+
lines.push(line);
|
|
75
|
+
if (lines.length >= maxLines) {
|
|
76
|
+
lines[lines.length - 1] =
|
|
77
|
+
fit(lines[lines.length - 1], Math.max(0, room - 3)).trimEnd() + "...";
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
80
|
+
line = "";
|
|
81
|
+
used = 0;
|
|
82
|
+
}
|
|
83
|
+
if (size > room) continue;
|
|
84
|
+
line += segment;
|
|
85
|
+
used += size;
|
|
86
|
+
}
|
|
87
|
+
if (line || !lines.length) lines.push(line);
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function duration(value) {
|
|
92
|
+
const ms = Math.max(0, Number(value) || 0);
|
|
93
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
94
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
95
|
+
if (ms < 3600000)
|
|
96
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor(ms / 1000) % 60}s`;
|
|
97
|
+
return `${Math.floor(ms / 3600000)}h ${Math.floor(ms / 60000) % 60}m`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function bytes(value) {
|
|
101
|
+
const size = Math.max(0, Number(value) || 0);
|
|
102
|
+
if (size < 1024) return `${size} B`;
|
|
103
|
+
if (size < 1048576) return `${(size / 1024).toFixed(1)} KiB`;
|
|
104
|
+
return `${(size / 1048576).toFixed(1)} MiB`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function tokens(value) {
|
|
108
|
+
return value == null ? "--" : Number(value).toLocaleString("en-US");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function tokenLabel(item) {
|
|
112
|
+
return `${tokens(item.inputTokens)} in / ${tokens(item.outputTokens)} out`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function clock(value) {
|
|
116
|
+
const date = new Date(value);
|
|
117
|
+
if (Number.isNaN(date.getTime())) return "--:--:--";
|
|
118
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()]
|
|
119
|
+
.map((n) => String(n).padStart(2, "0"))
|
|
120
|
+
.join(":");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function tone(state) {
|
|
124
|
+
if (state === "failed" || state === "error") return "red";
|
|
125
|
+
if (state === "streaming" || state === "completed") return "green";
|
|
126
|
+
if (state === "waiting" || state === "warn") return "amber";
|
|
127
|
+
return "muted";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
function previewLines(value, label, width, maxLines) {
|
|
132
|
+
const text = clean(value, 1600);
|
|
133
|
+
const segments = [...SEGMENTS.segment(text)];
|
|
134
|
+
const room = Math.max(1, width * maxLines - label.length - 5);
|
|
135
|
+
let start = segments.length;
|
|
136
|
+
let used = 0;
|
|
137
|
+
while (start > 0) {
|
|
138
|
+
const size = widthOf(segments[start - 1].segment);
|
|
139
|
+
if (used + size > room) break;
|
|
140
|
+
used += size;
|
|
141
|
+
start--;
|
|
142
|
+
}
|
|
143
|
+
const tail = start
|
|
144
|
+
? `...${text.slice(segments[start]?.index ?? text.length)}`
|
|
145
|
+
: text;
|
|
146
|
+
return wrap(`${label}: ${tail}`, width, maxLines);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function startTerminalUI(monitor, { clients = [] } = {}) {
|
|
150
|
+
const input = process.stdin;
|
|
151
|
+
const output = process.stdout;
|
|
152
|
+
if (!output.isTTY || !input.isTTY) return { close() {} };
|
|
153
|
+
|
|
154
|
+
let state = monitor.snapshot();
|
|
155
|
+
let closed = false;
|
|
156
|
+
let pending = null;
|
|
157
|
+
let lastRender = 0;
|
|
158
|
+
let previousLines = [];
|
|
159
|
+
let previousColumns = 0;
|
|
160
|
+
let previousRows = 0;
|
|
161
|
+
let pausedLogs = null;
|
|
162
|
+
let clearedIds = new Set();
|
|
163
|
+
let logOffset = 0;
|
|
164
|
+
let trafficOffset = 0;
|
|
165
|
+
let level = "all";
|
|
166
|
+
let query = "";
|
|
167
|
+
let searchDraft = null;
|
|
168
|
+
let searching = false;
|
|
169
|
+
let follow = true;
|
|
170
|
+
let showPreviews = true;
|
|
171
|
+
let lastLogLines = 0;
|
|
172
|
+
let lastTrafficLines = 0;
|
|
173
|
+
let bodyHeight = 1;
|
|
174
|
+
let logBodyHeight = 1;
|
|
175
|
+
let logCache = null;
|
|
176
|
+
let unsubscribe = () => {};
|
|
177
|
+
const wasRaw = Boolean(input.isRaw);
|
|
178
|
+
const wasPaused = input.isPaused();
|
|
179
|
+
const clientCommands = clients.filter((client) => client === "claude" || client === "codex");
|
|
180
|
+
const launchHint = clientCommands.length ? clientCommands.join(" or ") : "claude or codex";
|
|
181
|
+
|
|
182
|
+
function add(lines, value, color = "text", bold = false, width, maxLines) {
|
|
183
|
+
for (const text of wrap(value, width, maxLines))
|
|
184
|
+
lines.push({ text, color, bold });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function trafficLines(width) {
|
|
188
|
+
const lines = [];
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
const active = state.active || [];
|
|
191
|
+
const recent = state.recent || [];
|
|
192
|
+
const models = [...(state.models || [])].sort((a, b) => Number(Boolean(b.active)) - Number(Boolean(a.active)));
|
|
193
|
+
const section = (title) => {
|
|
194
|
+
if (lines.length) lines.push({ text: "" });
|
|
195
|
+
lines.push({ text: ` ${title} `, color: "section", bold: true });
|
|
196
|
+
};
|
|
197
|
+
section("MODELS & USAGE");
|
|
198
|
+
if (!models.length) add(lines, "No model usage yet.", "muted", false, width, 1);
|
|
199
|
+
for (const model of models.slice(0, 64)) {
|
|
200
|
+
add(lines, model.model || "(model not supplied)", model.active ? "amber" : "text", true, width, 2);
|
|
201
|
+
add(lines, `${model.requests} req / ${model.active} active / ${model.failed} failed`, "muted", false, width, 2);
|
|
202
|
+
add(lines, model.usageRequests ? `${tokenLabel(model)} tokens` : "Tokens -- / not reported", "muted", false, width, 2);
|
|
203
|
+
if (model.cachedTokens || model.reasoningTokens)
|
|
204
|
+
add(lines, `Cached ${tokens(model.cachedTokens)} / reasoning ${tokens(model.reasoningTokens)} (included)`, "muted", false, width, 2);
|
|
205
|
+
}
|
|
206
|
+
if (models.length > 64) add(lines, `${models.length - 64} more observed models`, "muted", false, width, 1);
|
|
207
|
+
|
|
208
|
+
section(`ACTIVE REQUESTS / ${active.length}`);
|
|
209
|
+
if (!active.length) {
|
|
210
|
+
add(lines, "Keep this terminal open. In another:", "muted", false, width, 2);
|
|
211
|
+
add(lines, launchHint, "amber", true, width, 1);
|
|
212
|
+
}
|
|
213
|
+
for (const request of active.slice(0, 100)) {
|
|
214
|
+
const activity = request.activity || {};
|
|
215
|
+
const phase = { waiting: "Waiting", reasoning: "Reasoning", speaking: "Receiving", summary: "Provider summary", tool: "Tool request" }[activity.phase] || (request.firstByteMs == null ? "Waiting" : "Receiving");
|
|
216
|
+
add(lines, `#${request.id} ${request.model || "(model not supplied)"}`, "text", true, width, 2);
|
|
217
|
+
add(lines, `${phase} / ${duration(now - request.startedAt)} / ${bytes(request.bytes)}`, "green", false, width, 2);
|
|
218
|
+
add(lines, `${tokenLabel(request)} / first byte ${request.firstByteMs == null ? "--" : duration(request.firstByteMs)}`, "muted", false, width, 2);
|
|
219
|
+
add(lines, request.route === "native-messages" ? "Messages -> AgentRouter" : request.route === "messages-bridge" ? "Messages -> Responses -> AgentRouter" : request.route === "claude-bridge" ? "Claude bridge -> AgentRouter" : "Responses -> AgentRouter", "muted", false, width, 1);
|
|
220
|
+
if (showPreviews) {
|
|
221
|
+
if (activity.assistant) for (const text of previewLines(activity.assistant, "Says", width, 2)) lines.push({ text, color: "text" });
|
|
222
|
+
if (activity.summary) for (const text of previewLines(activity.summary, "Provider summary", width, 2)) lines.push({ text, color: "muted" });
|
|
223
|
+
const tools = Array.isArray(activity.tools) ? activity.tools : [];
|
|
224
|
+
for (const tool of tools.slice(-2)) add(lines, `Tool ${tool.status === "preparing" ? "preparing" : "requested"}: ${tool.name || "(pending)"}`, "amber", false, width, 2);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (active.length > 100) add(lines, `${active.length - 100} more active requests`, "muted", false, width, 1);
|
|
228
|
+
|
|
229
|
+
section("RECENT REQUESTS");
|
|
230
|
+
if (!recent.length) add(lines, "No completed requests this session.", "muted", false, width, 2);
|
|
231
|
+
for (const request of recent.slice(0, 12)) {
|
|
232
|
+
add(lines, `${clock(request.endedAt)} #${request.id} ${String(request.state).toUpperCase()}`, tone(request.state), false, width, 2);
|
|
233
|
+
add(lines, request.model || "(model not supplied)", "text", false, width, 2);
|
|
234
|
+
add(lines, `${duration(request.durationMs)} / ${bytes(request.bytes)} / ${tokenLabel(request)}`, "muted", false, width, 2);
|
|
235
|
+
if (request.clientDisconnected) add(lines, request.completionObserved ? "Completed; client closed before EOF" : "Client disconnected before completion", "muted", false, width, 2);
|
|
236
|
+
if (request.error) add(lines, request.error, "red", false, width, 3);
|
|
237
|
+
}
|
|
238
|
+
if (state.insights?.length) {
|
|
239
|
+
section("SESSION SIGNALS");
|
|
240
|
+
for (const insight of state.insights.slice(0, 6)) add(lines, insight.message, tone(insight.level), false, width, 3);
|
|
241
|
+
}
|
|
242
|
+
return lines;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function logLines(width) {
|
|
246
|
+
const source = pausedLogs || state.logs || [];
|
|
247
|
+
const needle = (searching ? searchDraft : query).toLowerCase();
|
|
248
|
+
const sameEntries =
|
|
249
|
+
logCache &&
|
|
250
|
+
logCache.source.length === source.length &&
|
|
251
|
+
logCache.source[0] === source[0] &&
|
|
252
|
+
logCache.source.at(-1) === source.at(-1);
|
|
253
|
+
if (
|
|
254
|
+
sameEntries &&
|
|
255
|
+
logCache.width === width &&
|
|
256
|
+
logCache.level === level &&
|
|
257
|
+
logCache.needle === needle &&
|
|
258
|
+
logCache.clearedIds === clearedIds
|
|
259
|
+
)
|
|
260
|
+
return logCache;
|
|
261
|
+
const lines = [];
|
|
262
|
+
const rowCounts = new Map();
|
|
263
|
+
let count = 0;
|
|
264
|
+
for (const entry of source.slice(-400)) {
|
|
265
|
+
if (
|
|
266
|
+
clearedIds.has(entry.id) ||
|
|
267
|
+
(level !== "all" && entry.level !== level)
|
|
268
|
+
)
|
|
269
|
+
continue;
|
|
270
|
+
if (
|
|
271
|
+
needle &&
|
|
272
|
+
!clean(`${entry.message} ${entry.model || ""} ${entry.requestId ?? ""}`)
|
|
273
|
+
.toLowerCase()
|
|
274
|
+
.includes(needle)
|
|
275
|
+
)
|
|
276
|
+
continue;
|
|
277
|
+
count++;
|
|
278
|
+
const knownLevel = LEVELS.includes(entry.level) ? entry.level : "info";
|
|
279
|
+
const rowStart = lines.length;
|
|
280
|
+
const prefix = `${clock(entry.time)} ${knownLevel.toUpperCase().padEnd(5)}${entry.requestId != null ? ` #${entry.requestId}` : ""}`;
|
|
281
|
+
add(
|
|
282
|
+
lines,
|
|
283
|
+
`${prefix} ${entry.message}`,
|
|
284
|
+
tone(knownLevel) === "muted" ? "text" : tone(knownLevel),
|
|
285
|
+
false,
|
|
286
|
+
width,
|
|
287
|
+
8,
|
|
288
|
+
);
|
|
289
|
+
if (entry.model) add(lines, ` ${entry.model}`, "muted", false, width, 4);
|
|
290
|
+
rowCounts.set(entry.id, lines.length - rowStart);
|
|
291
|
+
}
|
|
292
|
+
if (!lines.length) {
|
|
293
|
+
lines.push({ text: "" });
|
|
294
|
+
add(
|
|
295
|
+
lines,
|
|
296
|
+
needle || level !== "all"
|
|
297
|
+
? "No logs match this filter."
|
|
298
|
+
: clearedIds.size
|
|
299
|
+
? "View cleared. New events will appear here."
|
|
300
|
+
: "Waiting for proxy events.",
|
|
301
|
+
"text",
|
|
302
|
+
true,
|
|
303
|
+
width,
|
|
304
|
+
);
|
|
305
|
+
add(
|
|
306
|
+
lines,
|
|
307
|
+
needle || level !== "all"
|
|
308
|
+
? "Press / to edit search; f changes the level."
|
|
309
|
+
: "Request bodies and credentials are not logged.",
|
|
310
|
+
"muted",
|
|
311
|
+
false,
|
|
312
|
+
width,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
logCache = {
|
|
316
|
+
source,
|
|
317
|
+
width,
|
|
318
|
+
level,
|
|
319
|
+
needle,
|
|
320
|
+
clearedIds,
|
|
321
|
+
lines,
|
|
322
|
+
count,
|
|
323
|
+
rowCounts,
|
|
324
|
+
};
|
|
325
|
+
return logCache;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function paint(text, width, color = "text", bold = false) {
|
|
329
|
+
return `${COLOR[color] || COLOR.text}${bold ? COLOR.bold : COLOR.normal}${fit(text, width)}${COLOR.normal}${COLOR.base}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function masthead(columns, rows) {
|
|
333
|
+
const totals = state.totals || {};
|
|
334
|
+
const information = [
|
|
335
|
+
{ text: "BRINK / CONNECTION", color: "amber", bold: true },
|
|
336
|
+
{ text: `RUNNING 127.0.0.1:${state.port}`, color: "green" },
|
|
337
|
+
{ text: `${totals.requests || 0} req / ${(state.active || []).length} active / ${totals.failed || 0} failed`, color: "muted" },
|
|
338
|
+
{ text: `Tokens ${totals.usageRequests ? tokenLabel(totals) : "pending"}`, color: "muted" },
|
|
339
|
+
{ text: "In another terminal:", color: "muted" },
|
|
340
|
+
{ text: launchHint, color: "amber", bold: true },
|
|
341
|
+
];
|
|
342
|
+
const sideBySide = columns >= 64;
|
|
343
|
+
const artBudget = rows >= 56 ? 23 : rows >= 34 ? 11 : rows >= 22 ? 6 : rows >= 16 ? 3 : 0;
|
|
344
|
+
const art = mascotLines(sideBySide ? columns - 36 : columns - 2, sideBySide ? artBudget : Math.min(3, artBudget));
|
|
345
|
+
const lines = [];
|
|
346
|
+
if (sideBySide && art.length) {
|
|
347
|
+
const artWidth = art[0].length;
|
|
348
|
+
const textWidth = columns - artWidth - 4;
|
|
349
|
+
const height = Math.max(art.length, rows >= 22 ? 6 : 3);
|
|
350
|
+
const textStart = Math.floor((height - Math.min(height, information.length)) / 2);
|
|
351
|
+
for (let i = 0; i < height; i++) {
|
|
352
|
+
const info = information[i - textStart] || { text: "" };
|
|
353
|
+
lines.push(
|
|
354
|
+
paint(" ", 1) + paint(art[i] || "", artWidth, "amber") +
|
|
355
|
+
paint(" │ ", 3, "border") + paint(info.text, textWidth, info.color, info.bold),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
for (const row of art) lines.push(paint(` ${row}`, columns, "amber"));
|
|
360
|
+
for (const info of information.slice(0, rows >= 12 ? 3 : 2))
|
|
361
|
+
lines.push(paint(` ${info.text}`, columns, info.color, info.bold));
|
|
362
|
+
}
|
|
363
|
+
return lines;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function render() {
|
|
367
|
+
pending = null;
|
|
368
|
+
if (closed) return;
|
|
369
|
+
lastRender = Date.now();
|
|
370
|
+
const columns = Math.max(1, output.columns || 80);
|
|
371
|
+
const rows = Math.max(1, output.rows || 24);
|
|
372
|
+
const resized = columns !== previousColumns || rows !== previousRows;
|
|
373
|
+
const frame = masthead(columns, rows);
|
|
374
|
+
if (columns < 48 || rows < 12) {
|
|
375
|
+
const contentWidth = Math.max(1, columns - 2);
|
|
376
|
+
const traffic = trafficLines(contentWidth);
|
|
377
|
+
const logs = logLines(contentWidth);
|
|
378
|
+
lastTrafficLines = traffic.length;
|
|
379
|
+
lastLogLines = logs.lines.length;
|
|
380
|
+
const footerRows = rows >= 12 ? 3 : 1;
|
|
381
|
+
const availableRows = Math.max(0, rows - frame.length - footerRows);
|
|
382
|
+
bodyHeight = Math.max(0, Math.floor((availableRows - 2) / 2));
|
|
383
|
+
logBodyHeight = Math.max(0, availableRows - bodyHeight - 2);
|
|
384
|
+
trafficOffset = Math.min(trafficOffset, Math.max(0, traffic.length - bodyHeight));
|
|
385
|
+
logOffset = follow ? Math.max(0, logs.lines.length - logBodyHeight) : Math.min(logOffset, Math.max(0, logs.lines.length - logBodyHeight));
|
|
386
|
+
if (availableRows >= 2) {
|
|
387
|
+
frame.push(paint(" OVERVIEW / j/k scroll", columns, "section", true));
|
|
388
|
+
for (let i = 0; i < bodyHeight; i++) {
|
|
389
|
+
const line = traffic[trafficOffset + i] || { text: "" };
|
|
390
|
+
frame.push(paint(` ${line.text}`, columns, line.color, line.bold));
|
|
391
|
+
}
|
|
392
|
+
frame.push(paint(` ACTIVITY LOG / ${pausedLogs ? "PAUSED" : "LIVE"} / ${level}`, columns, "section", true));
|
|
393
|
+
for (let i = 0; i < logBodyHeight; i++) {
|
|
394
|
+
const line = logs.lines[logOffset + i] || { text: "" };
|
|
395
|
+
frame.push(paint(` ${line.text}`, columns, line.color, line.bold));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
while (frame.length < rows - footerRows) frame.push(paint("", columns));
|
|
399
|
+
frame.length = Math.min(frame.length, Math.max(0, rows - footerRows));
|
|
400
|
+
frame.push(paint(searching ? ` Search: ${searchDraft}_` : " q Quit / Search p Pause c Clear f Level", columns, "text"));
|
|
401
|
+
if (footerRows === 3) {
|
|
402
|
+
frame.push(paint(" Arrows Logs | j/k Overview | v Preview", columns, "muted"));
|
|
403
|
+
frame.push(paint(` ${credit}`, columns, "muted"));
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
const leftWidth = Math.floor((columns - 1) / 2);
|
|
407
|
+
const rightWidth = columns - leftWidth - 1;
|
|
408
|
+
const leftContent = Math.max(1, leftWidth - 2);
|
|
409
|
+
const rightContent = Math.max(1, rightWidth - 2);
|
|
410
|
+
frame.push(paint("─".repeat(columns), columns, "border"));
|
|
411
|
+
const leftHeader = trafficOffset
|
|
412
|
+
? ` OVERVIEW / line ${trafficOffset + 1}`
|
|
413
|
+
: ` OVERVIEW / ${launchHint}`;
|
|
414
|
+
const rightHeader = ` ACTIVITY LOG / ${pausedLogs ? "PAUSED" : "LIVE"} / ${level}`;
|
|
415
|
+
frame.push(
|
|
416
|
+
paint(leftHeader, leftWidth, "section", true) +
|
|
417
|
+
paint("│", 1, "border") +
|
|
418
|
+
paint(rightHeader, rightWidth, "section", true),
|
|
419
|
+
);
|
|
420
|
+
bodyHeight = Math.max(1, rows - frame.length - 4);
|
|
421
|
+
logBodyHeight = bodyHeight;
|
|
422
|
+
const traffic = trafficLines(leftContent);
|
|
423
|
+
const logs = logLines(rightContent);
|
|
424
|
+
lastTrafficLines = traffic.length;
|
|
425
|
+
lastLogLines = logs.lines.length;
|
|
426
|
+
trafficOffset = Math.min(
|
|
427
|
+
trafficOffset,
|
|
428
|
+
Math.max(0, traffic.length - bodyHeight),
|
|
429
|
+
);
|
|
430
|
+
logOffset = follow
|
|
431
|
+
? Math.max(0, logs.lines.length - logBodyHeight)
|
|
432
|
+
: Math.min(logOffset, Math.max(0, logs.lines.length - logBodyHeight));
|
|
433
|
+
for (let i = 0; i < bodyHeight; i++) {
|
|
434
|
+
const left = traffic[trafficOffset + i] || { text: "" };
|
|
435
|
+
const right = logs.lines[logOffset + i] || { text: "" };
|
|
436
|
+
frame.push(
|
|
437
|
+
paint(" ", 1) +
|
|
438
|
+
paint(left.text, leftContent, left.color, left.bold) +
|
|
439
|
+
paint(" ", 1) +
|
|
440
|
+
paint("│", 1, "border") +
|
|
441
|
+
paint(" ", 1) +
|
|
442
|
+
paint(right.text, rightContent, right.color, right.bold) +
|
|
443
|
+
paint(" ", 1),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
frame.push(
|
|
447
|
+
paint(
|
|
448
|
+
` Traffic ${Math.min(trafficOffset + bodyHeight, traffic.length)}/${traffic.length} lines`,
|
|
449
|
+
leftWidth,
|
|
450
|
+
"muted",
|
|
451
|
+
) +
|
|
452
|
+
paint("│", 1, "border") +
|
|
453
|
+
paint(
|
|
454
|
+
` ${logs.count} events / ${pausedLogs ? "view frozen" : "memory only"}`,
|
|
455
|
+
rightWidth,
|
|
456
|
+
"muted",
|
|
457
|
+
),
|
|
458
|
+
);
|
|
459
|
+
const filter = searching
|
|
460
|
+
? ` Search: ${searchDraft}_ [Enter apply / Esc cancel]`
|
|
461
|
+
: ` / Search v Previews ${showPreviews ? "on" : "off"} ${query ? `Filter: ${query}` : "brink setup to configure"}`;
|
|
462
|
+
frame.push(paint(filter, columns, "section"));
|
|
463
|
+
const help =
|
|
464
|
+
columns >= 110
|
|
465
|
+
? " q Quit | p Pause | c Clear | f Level | / Search | Up/Down/PgUp/PgDn Logs | End Follow | j/k Traffic | v Preview"
|
|
466
|
+
: columns >= 76
|
|
467
|
+
? " q Quit p Pause c Clear f Level / Search | Up/Down Logs | j/k Traffic v Preview"
|
|
468
|
+
: " q Quit p Pause c Clear f Level / Search v Preview";
|
|
469
|
+
frame.push(paint(help, columns, "text"));
|
|
470
|
+
frame.push(paint(` ${credit}`, columns, "muted"));
|
|
471
|
+
}
|
|
472
|
+
let buffer = resized ? `${ESC}2J` : "";
|
|
473
|
+
for (let i = 0; i < frame.length; i++) {
|
|
474
|
+
if (resized || frame[i] !== previousLines[i])
|
|
475
|
+
buffer += `${ESC}${i + 1};1H${COLOR.base}${frame[i]}`;
|
|
476
|
+
}
|
|
477
|
+
if (buffer) output.write(buffer);
|
|
478
|
+
previousLines = frame;
|
|
479
|
+
previousColumns = columns;
|
|
480
|
+
previousRows = rows;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function schedule() {
|
|
484
|
+
if (closed || pending) return;
|
|
485
|
+
pending = setTimeout(render, Math.max(0, 250 - (Date.now() - lastRender)));
|
|
486
|
+
pending.unref();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function onState(next) {
|
|
490
|
+
if (closed) return;
|
|
491
|
+
const previous = pausedLogs || state.logs || [];
|
|
492
|
+
const nextLogs = pausedLogs || next.logs || [];
|
|
493
|
+
if (
|
|
494
|
+
!follow &&
|
|
495
|
+
!pausedLogs &&
|
|
496
|
+
previous.length &&
|
|
497
|
+
nextLogs.length &&
|
|
498
|
+
previous[0].id !== nextLogs[0].id
|
|
499
|
+
) {
|
|
500
|
+
// Keep the same visible entry as the bounded monitor buffer rolls over.
|
|
501
|
+
const anchor = previous.findIndex((entry) => entry.id === nextLogs[0].id);
|
|
502
|
+
if (anchor < 0) logOffset = 0;
|
|
503
|
+
else if (logCache) {
|
|
504
|
+
for (let i = 0; i < anchor; i++)
|
|
505
|
+
logOffset -= logCache.rowCounts.get(previous[i].id) || 0;
|
|
506
|
+
logOffset = Math.max(0, logOffset);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
state = next;
|
|
510
|
+
if (clearedIds.size) {
|
|
511
|
+
const retained = new Set(
|
|
512
|
+
[...(state.logs || []), ...(pausedLogs || [])].map((entry) => entry.id),
|
|
513
|
+
);
|
|
514
|
+
const kept = [...clearedIds].filter((id) => retained.has(id));
|
|
515
|
+
if (kept.length !== clearedIds.size) clearedIds = new Set(kept);
|
|
516
|
+
}
|
|
517
|
+
schedule();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function onKey(text, key = {}) {
|
|
521
|
+
if (closed) return;
|
|
522
|
+
if (key.ctrl && key.name === "c") {
|
|
523
|
+
close();
|
|
524
|
+
process.kill(process.pid, "SIGINT");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (searching) {
|
|
528
|
+
if (key.name === "escape") {
|
|
529
|
+
searching = false;
|
|
530
|
+
searchDraft = null;
|
|
531
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
532
|
+
query = searchDraft;
|
|
533
|
+
searching = false;
|
|
534
|
+
searchDraft = null;
|
|
535
|
+
} else if (key.name === "backspace") {
|
|
536
|
+
const segments = [...SEGMENTS.segment(searchDraft)];
|
|
537
|
+
searchDraft = segments
|
|
538
|
+
.slice(0, -1)
|
|
539
|
+
.map((part) => part.segment)
|
|
540
|
+
.join("");
|
|
541
|
+
} else if (key.ctrl && key.name === "u") {
|
|
542
|
+
searchDraft = "";
|
|
543
|
+
} else if (!key.ctrl && !key.meta && text && !text.includes("\x1b")) {
|
|
544
|
+
searchDraft = (searchDraft + clean(text)).slice(0, 160);
|
|
545
|
+
}
|
|
546
|
+
logOffset = 0;
|
|
547
|
+
follow = false;
|
|
548
|
+
schedule();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (text === "q") {
|
|
552
|
+
close();
|
|
553
|
+
process.kill(process.pid, "SIGINT");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (text === "p") {
|
|
557
|
+
pausedLogs = pausedLogs ? null : (state.logs || []).slice(-400);
|
|
558
|
+
logCache = null;
|
|
559
|
+
} else if (text === "c") {
|
|
560
|
+
clearedIds = new Set(
|
|
561
|
+
[...(state.logs || []), ...(pausedLogs || [])].map((entry) => entry.id),
|
|
562
|
+
);
|
|
563
|
+
logOffset = 0;
|
|
564
|
+
follow = true;
|
|
565
|
+
} else if (text === "f") {
|
|
566
|
+
level = LEVELS[(LEVELS.indexOf(level) + 1) % LEVELS.length];
|
|
567
|
+
logOffset = 0;
|
|
568
|
+
follow = true;
|
|
569
|
+
} else if (text === "/") {
|
|
570
|
+
searching = true;
|
|
571
|
+
searchDraft = query;
|
|
572
|
+
} else if (text === "v") {
|
|
573
|
+
showPreviews = !showPreviews;
|
|
574
|
+
} else if (text === "j") {
|
|
575
|
+
trafficOffset = Math.min(
|
|
576
|
+
Math.max(0, lastTrafficLines - bodyHeight),
|
|
577
|
+
trafficOffset + 1,
|
|
578
|
+
);
|
|
579
|
+
} else if (text === "k") {
|
|
580
|
+
trafficOffset = Math.max(0, trafficOffset - 1);
|
|
581
|
+
} else if (key.name === "up" || key.name === "pageup") {
|
|
582
|
+
follow = false;
|
|
583
|
+
logOffset = Math.max(
|
|
584
|
+
0,
|
|
585
|
+
logOffset - (key.name === "pageup" ? Math.max(1, logBodyHeight) : 1),
|
|
586
|
+
);
|
|
587
|
+
} else if (key.name === "down" || key.name === "pagedown") {
|
|
588
|
+
logOffset = Math.min(
|
|
589
|
+
Math.max(0, lastLogLines - logBodyHeight),
|
|
590
|
+
logOffset + (key.name === "pagedown" ? Math.max(1, logBodyHeight) : 1),
|
|
591
|
+
);
|
|
592
|
+
follow = logOffset >= lastLogLines - logBodyHeight;
|
|
593
|
+
} else if (key.name === "home") {
|
|
594
|
+
follow = false;
|
|
595
|
+
logOffset = 0;
|
|
596
|
+
} else if (key.name === "end") {
|
|
597
|
+
follow = true;
|
|
598
|
+
} else {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
schedule();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function close() {
|
|
605
|
+
if (closed) return;
|
|
606
|
+
closed = true;
|
|
607
|
+
clearTimeout(pending);
|
|
608
|
+
unsubscribe();
|
|
609
|
+
input.removeListener("keypress", onKey);
|
|
610
|
+
process.removeListener("SIGWINCH", schedule);
|
|
611
|
+
process.removeListener("exit", close);
|
|
612
|
+
input.setRawMode(wasRaw);
|
|
613
|
+
if (wasPaused) input.pause();
|
|
614
|
+
output.write(`${ESC}0m${ESC}?7h${ESC}?25h${ESC}?1049l`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
output.write(`${ESC}?1049h${ESC}?25l${ESC}?7l${ESC}2J`);
|
|
618
|
+
input.setRawMode(true);
|
|
619
|
+
readline.emitKeypressEvents(input);
|
|
620
|
+
input.on("keypress", onKey);
|
|
621
|
+
input.resume();
|
|
622
|
+
process.on("SIGWINCH", schedule);
|
|
623
|
+
process.on("exit", close);
|
|
624
|
+
unsubscribe = monitor.subscribe(onState);
|
|
625
|
+
clearTimeout(pending);
|
|
626
|
+
render();
|
|
627
|
+
return { close };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
module.exports = { startTerminalUI, clean, fit, wrap };
|