viberoom 0.5.8 → 0.6.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/dist/viewer.js CHANGED
@@ -1,9 +1,76 @@
1
1
  // viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
2
- import { extname } from "node:path";
2
+ import { extname, basename } from "node:path";
3
3
  export const VIEWER_MAX_BYTES = 2 * 1024 * 1024;
4
+ export const WINDOW_MAX_LINES = 500;
5
+ export const STREAM_MAX_BYTES = 64 * 1024 * 1024;
4
6
  const KINDS = { ".md": "markdown", ".markdown": "markdown", ".csv": "csv", ".tsv": "csv" };
5
7
  export function viewerKind(path) {
6
- return KINDS[extname(path).toLowerCase()] ?? null;
8
+ return KINDS[extname(path).toLowerCase()] ?? "text";
9
+ }
10
+ const LANGUAGES = {
11
+ ".ts": "typescript", ".mts": "typescript", ".cts": "typescript", ".tsx": "tsx",
12
+ ".js": "javascript", ".mjs": "javascript", ".cjs": "javascript", ".jsx": "jsx",
13
+ ".json": "json", ".jsonc": "json", ".json5": "json",
14
+ ".py": "python", ".rb": "ruby", ".php": "php", ".pl": "perl", ".lua": "lua", ".r": "r",
15
+ ".rs": "rust", ".go": "go", ".java": "java", ".kt": "kotlin", ".kts": "kotlin", ".swift": "swift",
16
+ ".c": "c", ".h": "c", ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", ".hpp": "cpp", ".hh": "cpp",
17
+ ".cs": "csharp", ".fs": "fsharp", ".scala": "scala", ".dart": "dart", ".ex": "elixir", ".exs": "elixir",
18
+ ".sh": "bash", ".bash": "bash", ".zsh": "bash", ".ps1": "powershell", ".psm1": "powershell", ".bat": "batch", ".cmd": "batch",
19
+ ".sql": "sql", ".graphql": "graphql", ".gql": "graphql", ".proto": "protobuf",
20
+ ".yaml": "yaml", ".yml": "yaml", ".toml": "toml", ".ini": "ini", ".cfg": "ini", ".conf": "ini", ".env": "bash",
21
+ ".xml": "xml", ".html": "html", ".htm": "html", ".svg": "xml", ".vue": "html",
22
+ ".css": "css", ".scss": "scss", ".sass": "scss", ".less": "less",
23
+ ".diff": "diff", ".patch": "diff",
24
+ };
25
+ const NAMED_FILES = {
26
+ dockerfile: "docker",
27
+ makefile: "makefile",
28
+ ".gitignore": "bash",
29
+ ".npmrc": "ini",
30
+ ".editorconfig": "ini",
31
+ };
32
+ export function languageOf(path) {
33
+ const name = basename(path).toLowerCase();
34
+ if (NAMED_FILES[name])
35
+ return NAMED_FILES[name];
36
+ return LANGUAGES[extname(path).toLowerCase()] || null;
37
+ }
38
+ const IMAGE_MEDIA = {
39
+ ".png": "image/png",
40
+ ".jpg": "image/jpeg",
41
+ ".jpeg": "image/jpeg",
42
+ ".gif": "image/gif",
43
+ ".webp": "image/webp",
44
+ ".bmp": "image/bmp",
45
+ ".avif": "image/avif",
46
+ ".ico": "image/x-icon",
47
+ ".svg": "image/svg+xml",
48
+ };
49
+ export const IMAGE_VIEW_MAX_BYTES = 20 * 1024 * 1024;
50
+ export function imageMediaType(path) {
51
+ return IMAGE_MEDIA[extname(path).toLowerCase()] ?? null;
52
+ }
53
+ export function looksBinary(head) {
54
+ if (!head.length)
55
+ return false;
56
+ let control = 0;
57
+ for (const byte of head) {
58
+ if (byte === 0)
59
+ return true;
60
+ if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13 && byte !== 12)
61
+ control++;
62
+ }
63
+ return control / head.length > 0.1;
64
+ }
65
+ export function sliceLines(text, from, to) {
66
+ const all = text.split(/\r?\n/);
67
+ if (all.length && all[all.length - 1] === "")
68
+ all.pop();
69
+ const lines = all.length;
70
+ const start = Math.max(1, Math.min(Math.floor(from ?? 1) || 1, Math.max(1, lines)));
71
+ const wanted = Math.floor(to ?? start + WINDOW_MAX_LINES - 1) || start;
72
+ const end = Math.max(start, Math.min(wanted, lines, start + WINDOW_MAX_LINES - 1));
73
+ return { text: all.slice(start - 1, end).join("\n"), from: start, to: end, lines };
7
74
  }
8
75
  export function detectDelimiter(text) {
9
76
  const first = text.split(/\r?\n/, 1)[0] ?? "";
package/dist/ws.js ADDED
@@ -0,0 +1,175 @@
1
+ // viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
2
+ import { createHash } from "node:crypto";
3
+ const HANDSHAKE_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
4
+ export function acceptKey(key) {
5
+ return createHash("sha1").update(key + HANDSHAKE_GUID).digest("base64");
6
+ }
7
+ export const OPCODE = { continuation: 0, text: 1, binary: 2, close: 8, ping: 9, pong: 10 };
8
+ export function encodeFrame(opcode, payload = Buffer.alloc(0)) {
9
+ const length = payload.length;
10
+ let header;
11
+ if (length < 126) {
12
+ header = Buffer.alloc(2);
13
+ header[1] = length;
14
+ }
15
+ else if (length < 65536) {
16
+ header = Buffer.alloc(4);
17
+ header[1] = 126;
18
+ header.writeUInt16BE(length, 2);
19
+ }
20
+ else {
21
+ header = Buffer.alloc(10);
22
+ header[1] = 127;
23
+ header.writeBigUInt64BE(BigInt(length), 2);
24
+ }
25
+ header[0] = 0x80 | (opcode & 0x0f);
26
+ return Buffer.concat([header, payload]);
27
+ }
28
+ export function encodeText(text) {
29
+ return encodeFrame(OPCODE.text, Buffer.from(text, "utf8"));
30
+ }
31
+ export function encodeClose(code = 1000, reason = "") {
32
+ const text = Buffer.from(reason, "utf8");
33
+ const payload = Buffer.alloc(2 + text.length);
34
+ payload.writeUInt16BE(code, 0);
35
+ text.copy(payload, 2);
36
+ return encodeFrame(OPCODE.close, payload);
37
+ }
38
+ export const MAX_FRAME_BYTES = 1 << 20;
39
+ export function decodeFrames(buffer) {
40
+ const frames = [];
41
+ let offset = 0;
42
+ while (buffer.length - offset >= 2) {
43
+ const first = buffer[offset];
44
+ const second = buffer[offset + 1];
45
+ const fin = (first & 0x80) !== 0;
46
+ const opcode = first & 0x0f;
47
+ const masked = (second & 0x80) !== 0;
48
+ let length = second & 0x7f;
49
+ let position = offset + 2;
50
+ if (length === 126) {
51
+ if (buffer.length < position + 2)
52
+ break;
53
+ length = buffer.readUInt16BE(position);
54
+ position += 2;
55
+ }
56
+ else if (length === 127) {
57
+ if (buffer.length < position + 8)
58
+ break;
59
+ const big = buffer.readBigUInt64BE(position);
60
+ if (big > BigInt(MAX_FRAME_BYTES))
61
+ throw new Error(`frame of ${big} bytes refused`);
62
+ length = Number(big);
63
+ position += 8;
64
+ }
65
+ if (length > MAX_FRAME_BYTES)
66
+ throw new Error(`frame of ${length} bytes refused`);
67
+ let mask = null;
68
+ if (masked) {
69
+ if (buffer.length < position + 4)
70
+ break;
71
+ mask = buffer.subarray(position, position + 4);
72
+ position += 4;
73
+ }
74
+ if (buffer.length < position + length)
75
+ break;
76
+ const payload = Buffer.from(buffer.subarray(position, position + length));
77
+ if (mask)
78
+ for (let i = 0; i < payload.length; i++)
79
+ payload[i] ^= mask[i & 3];
80
+ frames.push({ fin, opcode, payload });
81
+ offset = position + length;
82
+ }
83
+ return { frames, rest: buffer.subarray(offset) };
84
+ }
85
+ export class WebSocketPeer {
86
+ socket;
87
+ buffer;
88
+ closed = false;
89
+ closeListeners = [];
90
+ alive = true;
91
+ constructor(socket, head = Buffer.alloc(0)) {
92
+ this.socket = socket;
93
+ this.buffer = Buffer.alloc(0);
94
+ socket.on("data", (chunk) => this.onData(chunk));
95
+ socket.on("close", () => this.finish());
96
+ socket.on("end", () => this.finish());
97
+ socket.on("error", () => this.finish());
98
+ if (head.length)
99
+ this.onData(head);
100
+ }
101
+ onClose(listener) {
102
+ this.closeListeners.push(listener);
103
+ }
104
+ send(text) {
105
+ if (this.closed)
106
+ return;
107
+ this.socket.write(encodeText(text));
108
+ }
109
+ ping() {
110
+ if (this.closed)
111
+ return;
112
+ this.alive = false;
113
+ this.socket.write(encodeFrame(OPCODE.ping));
114
+ }
115
+ close(code = 1000, reason = "") {
116
+ if (this.closed)
117
+ return;
118
+ try {
119
+ this.socket.write(encodeClose(code, reason));
120
+ }
121
+ catch {
122
+ }
123
+ this.socket.end();
124
+ this.finish();
125
+ }
126
+ onData(chunk) {
127
+ if (this.closed)
128
+ return;
129
+ this.buffer = Buffer.concat([this.buffer, chunk]);
130
+ let decoded;
131
+ try {
132
+ decoded = decodeFrames(this.buffer);
133
+ }
134
+ catch {
135
+ this.close(1009, "frame too large");
136
+ return;
137
+ }
138
+ this.buffer = Buffer.from(decoded.rest);
139
+ for (const frame of decoded.frames) {
140
+ if (frame.opcode === OPCODE.ping)
141
+ this.socket.write(encodeFrame(OPCODE.pong, frame.payload));
142
+ else if (frame.opcode === OPCODE.pong)
143
+ this.alive = true;
144
+ else if (frame.opcode === OPCODE.close) {
145
+ try {
146
+ this.socket.write(encodeFrame(OPCODE.close, frame.payload.subarray(0, 2)));
147
+ }
148
+ catch {
149
+ }
150
+ this.socket.end();
151
+ this.finish();
152
+ return;
153
+ }
154
+ }
155
+ }
156
+ finish() {
157
+ if (this.closed)
158
+ return;
159
+ this.closed = true;
160
+ for (const listener of this.closeListeners)
161
+ listener();
162
+ }
163
+ }
164
+ export function acceptUpgrade(req, socket, head = Buffer.alloc(0)) {
165
+ const key = req.headers["sec-websocket-key"];
166
+ const upgrade = String(req.headers.upgrade ?? "").toLowerCase();
167
+ const version = req.headers["sec-websocket-version"];
168
+ if (upgrade !== "websocket" || typeof key !== "string" || version !== "13") {
169
+ socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
170
+ socket.destroy();
171
+ return null;
172
+ }
173
+ socket.write(["HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${acceptKey(key)}`, "", ""].join("\r\n"));
174
+ return new WebSocketPeer(socket, head);
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberoom",
3
- "version": "0.5.8",
3
+ "version": "0.6.0",
4
4
  "description": "viberoom: group chat rooms for a human and several coding agents over the Agent Client Protocol",
5
5
  "type": "module",
6
6
  "engines": {
@@ -33,6 +33,7 @@
33
33
  "@agentclientprotocol/sdk": "1.4.0",
34
34
  "marked": "^16.4.2",
35
35
  "mermaid": "^11.17.2",
36
+ "prismjs": "^1.30.0",
36
37
  "zod": "^4.0.0"
37
38
  },
38
39
  "devDependencies": {