agentgui 1.0.934 → 1.0.935

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/lib/codec.js CHANGED
@@ -1,4 +1,13 @@
1
- import { pack, unpack } from 'msgpackr';
2
-
3
- export function encode(obj) { return pack(obj); }
4
- export function decode(buf) { return unpack(buf instanceof Uint8Array ? buf : new Uint8Array(buf)); }
1
+ // agentgui WS wire codec plain JSON (UTF-8 text frames).
2
+ // Browser-compatible (no msgpackr). encode() returns a string the ws library
3
+ // sends as a text frame; decode() handles both Buffer/Uint8Array (Node) and
4
+ // string (browser) inputs.
5
+
6
+ export function encode(obj) { return JSON.stringify(obj); }
7
+
8
+ export function decode(buf) {
9
+ if (typeof buf === 'string') return JSON.parse(buf);
10
+ if (buf instanceof Uint8Array) return JSON.parse(new TextDecoder().decode(buf));
11
+ if (buf && typeof buf.toString === 'function') return JSON.parse(buf.toString('utf8'));
12
+ return JSON.parse(String(buf));
13
+ }
@@ -1,7 +1,10 @@
1
1
  import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
+ import crypto from 'crypto';
4
5
  import { execSync, spawnSync } from 'child_process';
6
+ import { runClaudeWithStreaming } from './claude-runner-run.js';
7
+ import { registry } from './claude-runner-agents.js';
5
8
 
6
9
  function err(code, message) { const e = new Error(message); e.code = code; throw e; }
7
10
 
@@ -13,7 +16,106 @@ const SUB_AGENT_MAP = {
13
16
  };
14
17
 
15
18
  export function register(router, deps) {
16
- const { queries, wsOptimizer, broadcastSync, getProviderConfigs, saveProviderConfig, STARTUP_CWD, discoveredAgents } = deps;
19
+ const { queries, wsOptimizer, broadcastSync, getProviderConfigs, saveProviderConfig, STARTUP_CWD, discoveredAgents, subscriptionIndex, activeChats } = deps;
20
+
21
+ // --- agents.list: enumerate registered ACP agents + claude-code ---
22
+ router.handle('agents.list', () => {
23
+ const agents = registry.list().map(a => ({
24
+ id: a.id,
25
+ name: a.name,
26
+ protocol: a.protocol,
27
+ supportsStdin: !!a.supportsStdin,
28
+ features: a.supportedFeatures || [],
29
+ }));
30
+ return { agents };
31
+ });
32
+
33
+ // --- conversation.subscribe: register this ws for sessionId broadcasts ---
34
+ router.handle('conversation.subscribe', (p, ws) => {
35
+ const sid = p?.sessionId;
36
+ if (!sid || typeof sid !== 'string') err(400, 'sessionId required');
37
+ if (!subscriptionIndex.has(sid)) subscriptionIndex.set(sid, new Set());
38
+ subscriptionIndex.get(sid).add(ws);
39
+ ws.subscriptions = ws.subscriptions || new Set();
40
+ ws.subscriptions.add(sid);
41
+ return { subscribed: true, sessionId: sid };
42
+ });
43
+
44
+ // --- chat.sendMessage: start a one-shot streaming chat with an agent.
45
+ // Bypasses the gutted db-queries layer entirely; calls runClaudeWithStreaming
46
+ // directly and broadcasts streaming_* events scoped to an ephemeral sessionId.
47
+ router.handle('chat.sendMessage', async (p, ws) => {
48
+ const content = (p?.content || '').toString();
49
+ if (!content) err(400, 'content required');
50
+ const agentId = p?.agentId || 'claude-code';
51
+ const model = p?.model || undefined;
52
+ const subAgent = p?.subAgent || undefined;
53
+ const cwd = p?.cwd || STARTUP_CWD;
54
+ if (!registry.has(agentId)) err(404, `Unknown agentId: ${agentId}`);
55
+
56
+ const sessionId = 'chat-' + crypto.randomBytes(8).toString('hex');
57
+ // Auto-subscribe the originating ws so it receives its own broadcasts.
58
+ if (!subscriptionIndex.has(sessionId)) subscriptionIndex.set(sessionId, new Set());
59
+ subscriptionIndex.get(sessionId).add(ws);
60
+ ws.subscriptions = ws.subscriptions || new Set();
61
+ ws.subscriptions.add(sessionId);
62
+
63
+ const ctrl = { aborted: false, proc: null };
64
+ activeChats.set(sessionId, ctrl);
65
+
66
+ // Fire-and-forget. Errors broadcast as streaming_error.
67
+ (async () => {
68
+ let eventCount = 0;
69
+ broadcastSync({ type: 'streaming_start', sessionId, agentId, timestamp: Date.now() });
70
+ const onEvent = (parsed) => {
71
+ eventCount++;
72
+ if (parsed?.type === 'assistant' && parsed.message?.content) {
73
+ for (const block of parsed.message.content) {
74
+ broadcastSync({ type: 'streaming_progress', sessionId, block, blockRole: 'assistant', seq: eventCount, timestamp: Date.now() });
75
+ }
76
+ } else if (parsed?.type === 'user' && parsed.message?.content) {
77
+ const blocks = Array.isArray(parsed.message.content) ? parsed.message.content : [];
78
+ for (const block of blocks) {
79
+ if (block?.type === 'tool_result') {
80
+ broadcastSync({ type: 'streaming_progress', sessionId, block, blockRole: 'tool_result', seq: eventCount, timestamp: Date.now() });
81
+ }
82
+ }
83
+ } else if (parsed?.type === 'result') {
84
+ const block = { type: 'result', result: parsed.result, subtype: parsed.subtype, duration_ms: parsed.duration_ms, total_cost_usd: parsed.total_cost_usd, is_error: !!parsed.is_error };
85
+ broadcastSync({ type: 'streaming_progress', sessionId, block, blockRole: 'result', seq: eventCount, isResult: true, timestamp: Date.now() });
86
+ }
87
+ };
88
+ try {
89
+ const config = {
90
+ verbose: true, outputFormat: 'stream-json', timeout: 1800000, print: true,
91
+ model, subAgent, onEvent,
92
+ onPid: () => {}, onProcess: (proc) => { ctrl.proc = proc; },
93
+ };
94
+ await runClaudeWithStreaming(content, cwd, agentId, config);
95
+ broadcastSync({ type: 'streaming_complete', sessionId, agentId, eventCount, timestamp: Date.now() });
96
+ } catch (e) {
97
+ broadcastSync({ type: 'streaming_error', sessionId, agentId, error: e.message || String(e), recoverable: false, timestamp: Date.now() });
98
+ } finally {
99
+ activeChats.delete(sessionId);
100
+ }
101
+ })();
102
+
103
+ return { sessionId, started: true };
104
+ });
105
+
106
+ // --- chat.cancel: abort an in-flight chat ---
107
+ router.handle('chat.cancel', (p) => {
108
+ const sid = p?.sessionId;
109
+ if (!sid) err(400, 'sessionId required');
110
+ const ctrl = activeChats.get(sid);
111
+ if (!ctrl) return { cancelled: false, reason: 'not-found' };
112
+ ctrl.aborted = true;
113
+ try { ctrl.proc?.kill?.(); } catch {}
114
+ activeChats.delete(sid);
115
+ return { cancelled: true };
116
+ });
117
+
118
+
17
119
 
18
120
  router.handle('home', () => ({ home: os.homedir(), cwd: STARTUP_CWD }));
19
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.934",
3
+ "version": "1.0.935",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -37,7 +37,6 @@
37
37
  "form-data": "^4.0.5",
38
38
  "fsbrowse": "latest",
39
39
  "lru-cache": "^11.2.7",
40
- "msgpackr": "^1.11.8",
41
40
  "opencode-ai": "^1.2.15",
42
41
  "p-retry": "^7.1.1",
43
42
  "puppeteer-core": "^24.37.5",
@@ -0,0 +1,62 @@
1
+ // Smoke-test the WS chat protocol added in this session.
2
+ // Boots no server — assumes one is already running at ws://localhost:$PORT/sync.
3
+ //
4
+ // Usage: PORT=3990 node scripts/smoke-ws-chat.mjs
5
+ //
6
+ // Checks:
7
+ // 1. WS connects, receives sync_connected.
8
+ // 2. agents.list returns a non-empty array.
9
+ // 3. (optional) conversation.subscribe returns subscribed:true.
10
+
11
+ import WebSocket from 'ws';
12
+
13
+ const PORT = process.env.PORT || 3000;
14
+ const URL = `ws://localhost:${PORT}/sync`;
15
+
16
+ const ws = new WebSocket(URL);
17
+ let reqId = 0;
18
+ const pending = new Map();
19
+
20
+ const call = (method, params) => new Promise((resolve, reject) => {
21
+ const r = ++reqId;
22
+ pending.set(r, { resolve, reject });
23
+ ws.send(JSON.stringify({ m: method, r, p: params || {} }));
24
+ setTimeout(() => { if (pending.has(r)) { pending.delete(r); reject(new Error('timeout: ' + method)); } }, 5000);
25
+ });
26
+
27
+ ws.on('open', async () => {
28
+ console.log('WS open:', URL);
29
+ });
30
+
31
+ ws.on('message', async (data) => {
32
+ let msg;
33
+ try { msg = JSON.parse(data.toString('utf8')); } catch { console.log('decode fail:', data); return; }
34
+ const items = Array.isArray(msg) ? msg : [msg];
35
+ for (const m of items) {
36
+ if (m && m.r !== undefined && (m.d !== undefined || m.e !== undefined)) {
37
+ const p = pending.get(m.r);
38
+ if (!p) continue;
39
+ pending.delete(m.r);
40
+ if (m.e) p.reject(new Error(m.e.m));
41
+ else p.resolve(m.d);
42
+ } else if (m?.type === 'sync_connected') {
43
+ console.log('sync_connected, clientId =', m.clientId);
44
+ try {
45
+ const r1 = await call('agents.list');
46
+ console.log('agents.list OK, count =', r1.agents.length, '— first =', r1.agents[0]?.id);
47
+ const r2 = await call('conversation.subscribe', { sessionId: 'smoke-test-sid' });
48
+ console.log('conversation.subscribe OK =', r2);
49
+ console.log('PASS');
50
+ process.exit(0);
51
+ } catch (e) {
52
+ console.error('FAIL:', e.message);
53
+ process.exit(1);
54
+ }
55
+ }
56
+ }
57
+ });
58
+
59
+ ws.on('error', (e) => { console.error('ws error:', e.message); process.exit(1); });
60
+ ws.on('close', (code, reason) => { console.log('ws close:', code, reason?.toString()); });
61
+
62
+ setTimeout(() => { console.error('overall timeout'); process.exit(1); }, 15000);
package/server.js CHANGED
@@ -11,6 +11,7 @@ import { runClaudeWithStreaming } from './lib/claude-runner-run.js';
11
11
  import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
12
12
  import { discoverExternalACPServers, initializeAgentDiscovery } from './lib/agent-discovery.js';
13
13
  import { createRegistry } from './lib/routes-registry.js';
14
+ import { register as registerWsHandlers } from './lib/ws-handlers-util.js';
14
15
  import { BROADCAST_TYPES } from './lib/broadcast.js';
15
16
  import { WSOptimizer } from './lib/ws-optimizer.js';
16
17
  import { WsRouter } from './lib/ws-protocol.js';
@@ -123,8 +124,10 @@ const { processMessageWithStreaming } = createProcessMessage({
123
124
  scheduleRetry, drainMessageQueue, createEventHandler
124
125
  });
125
126
 
127
+ const activeChats = new Map();
126
128
  const wsRouter = new WsRouter();
127
129
  createRegistry(wsRouter, { queries, sendJSON, parseBody, broadcastSync, debugLog, PORT, BASE_URL, rootDir, STARTUP_CWD, PKG_VERSION, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, activeScripts, messageQueues, rateLimitState, cleanupExecution, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, logError, syncClients, wsOptimizer, errLogPath, getJsonlWatcher: () => getJsonlWatcher(), routes: _routes });
130
+ registerWsHandlers(wsRouter, { queries, wsOptimizer, broadcastSync, getProviderConfigs, saveProviderConfig, STARTUP_CWD, discoveredAgents, subscriptionIndex, activeChats });
128
131
 
129
132
 
130
133
  const { wss, hotReloadClients } = createWsSetup(server, {
@@ -1,6 +1,11 @@
1
- // agentgui backend client. Resolves base URL from ?backend= or localStorage or default
2
- // (same-origin). Talks to the local agentgui server: ccsniff history routes, /health,
3
- // and the /sync WebSocket for chat streaming.
1
+ // agentgui backend client. Same-origin by default. Talks to:
2
+ // - HTTP: /health, /v1/history/* (served by ccsniff)
3
+ // - WS : /sync (JSON envelope: requests {m, r, p}; replies {r, d|e};
4
+ // broadcasts {type, sessionId, ...})
5
+ // No external acptoapi dependency. Chat + agent listing flow over the WS.
6
+
7
+ import { encode, decode } from './codec.js';
8
+
4
9
  const KEY = 'agentgui.backend';
5
10
  const DEFAULT_BACKEND = '';
6
11
 
@@ -11,27 +16,19 @@ export function getBackend() {
11
16
  return localStorage.getItem(KEY) || DEFAULT_BACKEND;
12
17
  }
13
18
 
14
- export function setBackend(url) {
15
- localStorage.setItem(KEY, url);
16
- }
19
+ export function setBackend(url) { localStorage.setItem(KEY, url); }
17
20
 
18
21
  export async function probeBackend(base) {
19
22
  try {
20
23
  const r = await fetch(base + '/health', { method: 'GET' });
21
24
  if (!r.ok) return { ok: false, status: r.status };
22
- const j = await r.json();
23
- return { ok: true, info: j };
25
+ return { ok: true, info: await r.json() };
24
26
  } catch (e) {
25
27
  return { ok: false, error: e.message };
26
28
  }
27
29
  }
28
30
 
29
- export async function listModels(base) {
30
- const r = await fetch(base + '/v1/models');
31
- if (!r.ok) throw new Error('models: ' + r.status);
32
- const j = await r.json();
33
- return j.data || [];
34
- }
31
+ // ---------- History (HTTP, served by ccsniff) ----------
35
32
 
36
33
  export async function listSessions(base) {
37
34
  const r = await fetch(base + '/v1/history/sessions');
@@ -63,39 +60,184 @@ export function streamHistory(base, onEvent) {
63
60
  return es;
64
61
  }
65
62
 
66
- // Streaming chat completions using OpenAI-style SSE.
67
- export async function* streamChat(base, { model, messages, signal }) {
68
- const r = await fetch(base + '/v1/chat/completions', {
69
- method: 'POST',
70
- headers: { 'Content-Type': 'application/json' },
71
- body: JSON.stringify({ model, messages, stream: true }),
72
- signal,
73
- });
74
- if (!r.ok) {
75
- const t = await r.text();
76
- throw new Error('chat: ' + r.status + ' ' + t.slice(0, 300));
63
+ // ---------- WebSocket client (/sync) ----------
64
+
65
+ const SYNC_PATH = '/sync';
66
+ let _ws = null;
67
+ let _wsReady = null; // Promise that resolves when ws is OPEN
68
+ let _nextReqId = 1;
69
+ const _pending = new Map(); // requestId → { resolve, reject }
70
+ const _sessionListeners = new Map(); // sessionId → Set<(event)=>void>
71
+
72
+ function wsUrl(base) {
73
+ if (base) {
74
+ // Absolute base like http://host:port → ws(s)://host:port/sync
75
+ try {
76
+ const u = new URL(base);
77
+ const proto = u.protocol === 'https:' ? 'wss:' : 'ws:';
78
+ return proto + '//' + u.host + SYNC_PATH;
79
+ } catch {}
77
80
  }
78
- const reader = r.body.getReader();
79
- const dec = new TextDecoder();
80
- let buf = '';
81
- while (true) {
82
- const { done, value } = await reader.read();
83
- if (done) break;
84
- buf += dec.decode(value, { stream: true });
85
- let idx;
86
- while ((idx = buf.indexOf('\n\n')) !== -1) {
87
- const block = buf.slice(0, idx);
88
- buf = buf.slice(idx + 2);
89
- const line = block.split('\n').find(l => l.startsWith('data:'));
90
- if (!line) continue;
91
- const payload = line.slice(5).trim();
92
- if (payload === '[DONE]') return;
81
+ // Same-origin
82
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
83
+ return proto + '//' + location.host + SYNC_PATH;
84
+ }
85
+
86
+ function ensureWs(base) {
87
+ if (_ws && _ws.readyState === 1) return _wsReady;
88
+ if (_ws && _ws.readyState === 0) return _wsReady;
89
+ _ws = new WebSocket(wsUrl(base));
90
+ _wsReady = new Promise((resolve, reject) => {
91
+ _ws.addEventListener('open', () => resolve(_ws));
92
+ _ws.addEventListener('error', (e) => reject(e));
93
+ _ws.addEventListener('close', () => {
94
+ // Reject all pending requests on close so callers can recover.
95
+ for (const [, p] of _pending) p.reject(new Error('ws closed'));
96
+ _pending.clear();
97
+ _ws = null;
98
+ _wsReady = null;
99
+ });
100
+ _ws.addEventListener('message', (ev) => {
101
+ let msg;
93
102
  try {
94
- const j = JSON.parse(payload);
95
- const delta = j.choices?.[0]?.delta?.content;
96
- if (delta) yield { type: 'text', text: delta };
97
- if (j.error) yield { type: 'error', error: j.error };
98
- } catch (_) {}
103
+ // Server sends text frames (JSON via codec). ev.data is string.
104
+ msg = typeof ev.data === 'string' ? JSON.parse(ev.data) : decode(ev.data);
105
+ } catch { return; }
106
+ // Reply to a prior request?
107
+ if (msg && msg.r !== undefined && (msg.d !== undefined || msg.e !== undefined)) {
108
+ const p = _pending.get(msg.r);
109
+ if (!p) return;
110
+ _pending.delete(msg.r);
111
+ if (msg.e) p.reject(new Error(msg.e.m || ('ws error ' + msg.e.c)));
112
+ else p.resolve(msg.d);
113
+ return;
114
+ }
115
+ // Unsolicited broadcast — route by sessionId to subscribers.
116
+ // Server may send a single event or a batch (array) per ws-optimizer.
117
+ const items = Array.isArray(msg) ? msg : [msg];
118
+ for (const item of items) {
119
+ const sid = item?.sessionId;
120
+ if (!sid) continue;
121
+ const subs = _sessionListeners.get(sid);
122
+ if (!subs) continue;
123
+ for (const fn of subs) { try { fn(item); } catch {} }
124
+ }
125
+ });
126
+ });
127
+ return _wsReady;
128
+ }
129
+
130
+ function wsCall(base, method, params) {
131
+ return ensureWs(base).then(() => new Promise((resolve, reject) => {
132
+ const r = _nextReqId++;
133
+ _pending.set(r, { resolve, reject });
134
+ _ws.send(encode({ m: method, r, p: params || {} }));
135
+ }));
136
+ }
137
+
138
+ function addSessionListener(sessionId, fn) {
139
+ if (!_sessionListeners.has(sessionId)) _sessionListeners.set(sessionId, new Set());
140
+ _sessionListeners.get(sessionId).add(fn);
141
+ return () => {
142
+ const s = _sessionListeners.get(sessionId);
143
+ if (s) { s.delete(fn); if (s.size === 0) _sessionListeners.delete(sessionId); }
144
+ };
145
+ }
146
+
147
+ // ---------- Agents / models (WS) ----------
148
+
149
+ export async function listModels(base) {
150
+ const { agents } = await wsCall(base, 'agents.list', {});
151
+ // Compatibility shape: app.js expects an array of {id, name?, ...}
152
+ return agents || [];
153
+ }
154
+
155
+ // ---------- Streaming chat (WS) ----------
156
+ //
157
+ // Yields events of shape:
158
+ // { type: 'text', text: '...' } — assistant text deltas
159
+ // { type: 'tool', block: {...} } — tool_use blocks
160
+ // { type: 'result', block: {...} } — terminal result block
161
+ // { type: 'error', error: '...' }
162
+ //
163
+ // Caller signature kept compatible with the previous HTTP/SSE impl.
164
+ export async function* streamChat(base, { model, messages, signal, agentId }) {
165
+ // The last user message is the prompt; agentgui's claude-runner doesn't
166
+ // accept a full message list — it spawns the agent for a single prompt.
167
+ // For multi-turn, the agent's own session/resume handles continuity.
168
+ const last = messages[messages.length - 1];
169
+ const content = last?.content || '';
170
+ if (!content) return;
171
+
172
+ // app.js treats the "model" picker as the agent picker (selects from
173
+ // agents.list ids). If no explicit agentId is given, model IS the agent.
174
+ // If `model` looks like a real model id (has a slash), keep it as model
175
+ // and default agent to claude-code.
176
+ let resolvedAgentId = agentId;
177
+ let resolvedModel = model;
178
+ if (!resolvedAgentId) {
179
+ if (!model || /^[a-z][a-z0-9-]*$/.test(model)) {
180
+ // Bare slug — treat as agentId.
181
+ resolvedAgentId = model || 'claude-code';
182
+ resolvedModel = undefined;
183
+ } else {
184
+ resolvedAgentId = 'claude-code';
185
+ }
186
+ }
187
+
188
+ // Queue events here; the async iterator pulls from it.
189
+ const queue = [];
190
+ let resolveWait = null;
191
+ let done = false;
192
+ let errored = null;
193
+ const push = (ev) => { queue.push(ev); if (resolveWait) { resolveWait(); resolveWait = null; } };
194
+
195
+ // Kick off the chat on the server.
196
+ let started;
197
+ try {
198
+ started = await wsCall(base, 'chat.sendMessage', { content, agentId: resolvedAgentId, model: resolvedModel });
199
+ } catch (e) {
200
+ yield { type: 'error', error: e.message };
201
+ return;
202
+ }
203
+ const sessionId = started?.sessionId;
204
+ if (!sessionId) { yield { type: 'error', error: 'no sessionId from server' }; return; }
205
+
206
+ const unsub = addSessionListener(sessionId, (ev) => {
207
+ if (ev.type === 'streaming_progress') {
208
+ const block = ev.block;
209
+ if (block?.type === 'text' && block.text) push({ type: 'text', text: block.text });
210
+ else if (block?.type === 'tool_use') push({ type: 'tool', block });
211
+ else if (block?.type === 'tool_result') push({ type: 'tool', block });
212
+ else if (block?.type === 'result') push({ type: 'result', block });
213
+ } else if (ev.type === 'streaming_complete') {
214
+ done = true;
215
+ if (resolveWait) { resolveWait(); resolveWait = null; }
216
+ } else if (ev.type === 'streaming_error') {
217
+ errored = ev.error || 'streaming error';
218
+ done = true;
219
+ if (resolveWait) { resolveWait(); resolveWait = null; }
220
+ }
221
+ });
222
+
223
+ // Wire AbortSignal to chat.cancel.
224
+ const onAbort = () => { wsCall(base, 'chat.cancel', { sessionId }).catch(() => {}); };
225
+ if (signal) {
226
+ if (signal.aborted) onAbort();
227
+ else signal.addEventListener('abort', onAbort, { once: true });
228
+ }
229
+
230
+ try {
231
+ while (!done || queue.length > 0) {
232
+ if (queue.length === 0) {
233
+ await new Promise(r => { resolveWait = r; });
234
+ continue;
235
+ }
236
+ yield queue.shift();
99
237
  }
238
+ if (errored) yield { type: 'error', error: errored };
239
+ } finally {
240
+ unsub();
241
+ if (signal) signal.removeEventListener?.('abort', onAbort);
100
242
  }
101
243
  }
@@ -0,0 +1,8 @@
1
+ // Browser mirror of lib/codec.js. JSON over WS text frames.
2
+ export function encode(obj) { return JSON.stringify(obj); }
3
+ export function decode(buf) {
4
+ if (typeof buf === 'string') return JSON.parse(buf);
5
+ if (buf instanceof ArrayBuffer) return JSON.parse(new TextDecoder().decode(new Uint8Array(buf)));
6
+ if (buf instanceof Uint8Array) return JSON.parse(new TextDecoder().decode(buf));
7
+ return JSON.parse(String(buf));
8
+ }