agentgui 1.0.932 → 1.0.934

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/AGENTS.md CHANGED
@@ -25,7 +25,7 @@ Local server on PORT=3056 (default), `bun server.js`:
25
25
 
26
26
  ## Learning audit
27
27
 
28
- - 2026-05-02 session: 5 items audited (CI bun, stream imports, windows fallback, GM blocker, ACP history), 0 removed (rs-learn retrieval not yet confirmed; safety default kept all), 1 new fact ingested (acptoapi history integration)
28
+ - 2026-05-02 session: 5 items audited (CI bun, stream imports, windows fallback, GM blocker, ACP history), 0 removed (rs-learn retrieval not yet confirmed; safety default kept all), 1 new fact ingested (in-process ccsniff history integration)
29
29
 
30
30
  ## CI / GitHub Actions
31
31
 
@@ -74,11 +74,11 @@ The actual hook content is generated/templated from c:/dev/gm, likely from `lib/
74
74
 
75
75
  Only after the real generator is patched will agentgui sessions run autonomously without per-tool ceremony.
76
76
 
77
- ## ACP-to-API History Integration
77
+ ## History Integration via ccsniff (2026-05-21 reverted from acptoapi)
78
78
 
79
- **acptoapi (c:\dev\acptoapi) merged Claude Code history routes as of 2026-05-02; ccsniff package is no longer needed.**
79
+ **agentgui mounts `ccsniff`'s history router in-process no external proxy.**
80
80
 
81
- History functionality (`GET /v1/history/*` endpoints) is now built into acptoapi. Routes: `snapshot` (event/session/project/tool/error counts + byte/date range), `sessions` (list with title/project/cwd/counts), `sessions/:sid/events` (flattened events), `search` (BM25 with snippets), `reindex` (rebuild index), `stream` (SSE). Implementation: `lib/history/` (bm25.js for tokenize/buildIndex/search/snippet, watcher.js for JsonlWatcher + JsonlReplayer, index.js for HistoryStore singleton + flattenEvent). Reads `~/.claude/projects` by default; override with `CLAUDE_PROJECTS_DIR` env var. The ccsniff package itself is no longer needed acptoapi covers the functionality entirely.
81
+ `server.js` imports `createHistoryRouter` from the `ccsniff` package and mounts it on the internal Express app at `/`, exposing `GET /v1/history/{snapshot,sessions,sessions/:sid/events,search,reindex,stream}`. Reads `~/.claude/projects` by default; override with `CLAUDE_PROJECTS_DIR` env var. acptoapi's previously-bundled history routes were removed in acptoapi 1.0.103; ccsniff is now the canonical source. Browser client (`site/app/js/backend.js`) calls these same-origin via the agentgui server.
82
82
 
83
83
  ## buildSystemPrompt System Prompt for claude-code
84
84
 
@@ -124,7 +124,7 @@ Surfaced 2026-05-04 while validating the chat surface in `site/app/`. Fix applie
124
124
 
125
125
  **`site/app/js/app.js` opens SSE EventSource on navTo('history'); first /v1/history/* request triggers 30-90s loadOnce() synchronous walk.**
126
126
 
127
- The live history feature wires acptoapi's /v1/history/stream (SSE endpoint) via `B.streamHistory(base, onEvent)` on tab entry and closes the EventSource on tab exit. State shape: `state.live = { es, connected, lastEventTs, error, eventCount }`.
127
+ The live history feature wires the in-process ccsniff `/v1/history/stream` SSE endpoint via `B.streamHistory(base, onEvent)` on tab entry and closes the EventSource on tab exit. State shape: `state.live = { es, connected, lastEventTs, error, eventCount }`.
128
128
 
129
129
  Event dispatch loop:
130
130
  - `hello` event: set `connected=true`
@@ -132,8 +132,8 @@ Event dispatch loop:
132
132
  - `conversation` event: call `refreshHistory()` to reload session list
133
133
  - `error` event: set `live.error`
134
134
 
135
- Throttle renders via `requestAnimationFrame` to avoid event storm during burst loads from acptoapi.
135
+ Throttle renders via `requestAnimationFrame` to avoid event storm during burst loads.
136
136
 
137
- **First request to acptoapi /v1/history/* triggers loadOnce() that walks all JSONL files under ~/.claude/projects** (env default; override with `CLAUDE_PROJECTS_DIR`). In our test env: 299 files, 80MB, 69k events → 30-90s startup latency. Health check timeouts during this window are normal and expected. Subsequent requests are fast (cached index).
137
+ **First request to `/v1/history/*` triggers loadOnce() that walks all JSONL files under ~/.claude/projects** (env default; override with `CLAUDE_PROJECTS_DIR`). In our test env: 299 files, 80MB, 69k events → 30-90s startup latency. Health check timeouts during this window are normal and expected. Subsequent requests are fast (cached index).
138
138
 
139
- acptoapi binary: `c:/dev/acptoapi/bin/agentapi.js`, default port 4800, no args; CORS `Access-Control-Allow-Origin: *` is enabled.
139
+ The endpoints are served by ccsniff's Express router mounted in-process from `server.js`. No external proxy.
package/README.md CHANGED
@@ -10,22 +10,22 @@
10
10
 
11
11
  Multi-agent GUI client for AI coding agents with real-time streaming, WebSocket sync, and SQLite persistence.
12
12
 
13
- ## Live client (new)
13
+ ## How it works
14
14
 
15
- AgentGUI now ships a static GH Pages client at **`/app/`** that talks to any [`acptoapi`](https://github.com/AnEntrypoint/acptoapi) backend over plain HTTP no install, no bundler, no DB. Open `https://anentrypoint.github.io/agentgui/app/?backend=http://your-acptoapi-host:4800` and chat with any provider acptoapi proxies (Claude / Gemini / OpenAI-compat brands / kilo / opencode), plus browse local Claude Code JSONL history right in the page.
15
+ AgentGUI is a single Node server (`server.js`) plus a same-origin browser client (`site/app/`). The server speaks ACP directly to local agent daemons (Claude Code, OpenCode, Kilo, Gemini CLI, etc.) via `@agentclientprotocol/sdk` and hosts `ccsniff`'s `/v1/history/*` Express router in-process for Claude Code JSONL session browsing no external proxy required.
16
16
 
17
17
  - UI: [anentrypoint-design](https://www.npmjs.com/package/anentrypoint-design) (CDN, single-file ESM)
18
- - Backend: [acptoapi](https://www.npmjs.com/package/acptoapi)`npx acptoapi` on the host with Claude Code / API keys; exposes `/v1/chat/completions`, `/v1/messages`, `/v1/history/*`
19
- - Source: `site/app/` (this repo)
18
+ - Server: `server.js`ACP daemon manager + WebSocket chat + ccsniff history mount
19
+ - Source: `site/app/` (browser) + `lib/` (server)
20
20
 
21
- History endpoints in `acptoapi` (formerly `ccsniff`'s job):
21
+ History endpoints served by [`ccsniff`](https://github.com/AnEntrypoint/ccsniff) directly:
22
22
 
23
23
  - `GET /v1/history/sessions` — list Claude Code sessions on the host
24
24
  - `GET /v1/history/sessions/:sid/events` — flattened events for one session
25
25
  - `GET /v1/history/search?q=…` — BM25-ranked search across all events
26
26
  - `GET /v1/history/stream` — Server-Sent Events for live tailing
27
27
 
28
- The legacy Node server in this repo (`server.js`, `lib/`, `static/`) still ships in the npm `agentgui` package and is the install-friendly path. It is being phased out as the static client + acptoapi pair reach feature parity.
28
+ Chat streams over the `/sync` WebSocket using the `chat.sendMessage` method see "WebSocket Chat Protocol" below.
29
29
 
30
30
  ### Supported Agents
31
31
 
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import crypto from 'crypto';
5
+ import * as term from './terminal.js';
5
6
 
6
7
  export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, getWss, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap, routes, PORT }) {
7
8
  return async function httpHandler(req, res) {
@@ -49,6 +50,7 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
49
50
  else if (req.url.startsWith('/api/') || req.url.startsWith('/js/') || req.url.startsWith('/css/') ||
50
51
  req.url.startsWith('/vendor/') || req.url.startsWith('/sync') || req.url === '/' ||
51
52
  req.url === '/health' || req.url.startsWith('/v1/') ||
53
+ req.url.startsWith('/api/terminal/') ||
52
54
  req.url.startsWith('/conversations/')) { routePath = req.url; }
53
55
  else { res.writeHead(404); res.end('Not found'); return; }
54
56
 
@@ -66,6 +68,30 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
66
68
  return;
67
69
  }
68
70
 
71
+ // Terminal sessions — gated by the Basic-auth check at the top of this handler.
72
+ // Never expose these routes without PASSWORD set.
73
+ if (pathOnly === '/api/terminal/sessions' && req.method === 'GET') {
74
+ sendJSON(req, res, 200, term.listSessions()); return;
75
+ }
76
+ if (pathOnly === '/api/terminal/sessions' && req.method === 'POST') {
77
+ let body = ''; for await (const c of req) body += c;
78
+ let p = {}; try { p = body ? JSON.parse(body) : {}; } catch {}
79
+ const s = term.createSession({ shell: p.shell, cwd: p.cwd, cols: p.cols, rows: p.rows, env: p.env });
80
+ sendJSON(req, res, 200, { sid: s.sid, kind: s.kind, shell: s.shell, cwd: s.cwd, cols: s.cols, rows: s.rows, pid: s.proc.pid });
81
+ return;
82
+ }
83
+ const termMatch = pathOnly.match(/^\/api\/terminal\/sessions\/([0-9a-f]+)$/);
84
+ if (termMatch && req.method === 'GET') {
85
+ const s = term.getSession(termMatch[1]);
86
+ if (!s) { sendJSON(req, res, 404, { error: 'session not found' }); return; }
87
+ sendJSON(req, res, 200, { sid: s.sid, kind: s.kind, shell: s.shell, cwd: s.cwd, cols: s.cols, rows: s.rows, pid: s.proc.pid, clients: s.clients.size });
88
+ return;
89
+ }
90
+ if (termMatch && req.method === 'DELETE') {
91
+ const ok = term.closeSession(termMatch[1]);
92
+ sendJSON(req, res, ok ? 200 : 404, { ok }); return;
93
+ }
94
+
69
95
  // Legacy REST handlers removed. History served by ccsniff at /v1/history/*.
70
96
  for (const key of Object.keys(routes)) {
71
97
  try {
@@ -0,0 +1,112 @@
1
+ // WebSocket terminal sessions. Ported from acptoapi 2026-05-21.
2
+ // Auth: callers MUST gate /api/terminal/* + the WS upgrade behind agentgui's
3
+ // PASSWORD Basic-auth check. This module does no auth on its own — never
4
+ // expose without the gate.
5
+
6
+ import os from 'os';
7
+ import { spawn } from 'child_process';
8
+ import crypto from 'crypto';
9
+ import { createRequire } from 'module';
10
+ const require_ = createRequire(import.meta.url);
11
+
12
+ let _pty = null;
13
+ function getPty() {
14
+ if (_pty !== null) return _pty;
15
+ try { _pty = require_('node-pty'); } catch { _pty = false; }
16
+ return _pty;
17
+ }
18
+
19
+ const sessions = new Map();
20
+
21
+ function newSid() { return crypto.randomBytes(8).toString('hex'); }
22
+
23
+ function defaultShell() {
24
+ if (os.platform() === 'win32') return process.env.COMSPEC || 'cmd.exe';
25
+ return process.env.SHELL || '/bin/bash';
26
+ }
27
+
28
+ export function createSession({ shell, cwd, cols = 80, rows = 24, env } = {}) {
29
+ const sid = newSid();
30
+ const pty = getPty();
31
+ const _shell = shell || defaultShell();
32
+ const _cwd = cwd || process.env.HOME || os.homedir();
33
+ const _env = { ...process.env, ...(env || {}), TERM: 'xterm-256color', COLORTERM: 'truecolor' };
34
+ let proc, kind;
35
+ if (pty) {
36
+ proc = pty.spawn(_shell, [], { name: 'xterm-256color', cols, rows, cwd: _cwd, env: _env });
37
+ kind = 'pty';
38
+ } else {
39
+ proc = spawn(_shell, os.platform() === 'win32' ? [] : ['-i'], { cwd: _cwd, env: _env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
40
+ kind = 'pipe';
41
+ }
42
+ const session = { sid, kind, proc, shell: _shell, cwd: _cwd, cols, rows, createdAt: Date.now(), clients: new Set(), exitCode: null };
43
+ sessions.set(sid, session);
44
+ const onData = (chunk) => {
45
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
46
+ for (const ws of session.clients) { if (ws.readyState === 1) try { ws.send(buf); } catch {} }
47
+ };
48
+ if (kind === 'pty') {
49
+ proc.on('data', onData);
50
+ proc.on('exit', (code) => { session.exitCode = code; for (const ws of session.clients) { try { ws.close(1000, 'exit:' + code); } catch {} } sessions.delete(sid); });
51
+ } else {
52
+ proc.stdout.on('data', onData);
53
+ proc.stderr.on('data', onData);
54
+ proc.stdout.on('error', () => {});
55
+ proc.stderr.on('error', () => {});
56
+ proc.stdin.on('error', () => {});
57
+ proc.on('exit', (code) => { session.exitCode = code; for (const ws of session.clients) { try { ws.close(1000, 'exit:' + code); } catch {} } sessions.delete(sid); });
58
+ proc.on('error', (err) => { session.exitCode = -1; for (const ws of session.clients) { try { ws.close(1011, 'error:' + err.message); } catch {} } sessions.delete(sid); });
59
+ }
60
+ return session;
61
+ }
62
+
63
+ export function getSession(sid) { return sessions.get(sid); }
64
+
65
+ export function listSessions() {
66
+ return [...sessions.values()].map(s => ({ sid: s.sid, kind: s.kind, shell: s.shell, cwd: s.cwd, cols: s.cols, rows: s.rows, createdAt: s.createdAt, clients: s.clients.size }));
67
+ }
68
+
69
+ export function closeSession(sid) {
70
+ const s = sessions.get(sid);
71
+ if (!s) return false;
72
+ try { s.proc.kill(); } catch {}
73
+ sessions.delete(sid);
74
+ return true;
75
+ }
76
+
77
+ export function writeToSession(sid, data) {
78
+ const s = sessions.get(sid);
79
+ if (!s) return false;
80
+ const buf = typeof data === 'string' ? data : Buffer.isBuffer(data) ? data : Buffer.from(data);
81
+ if (s.kind === 'pty') {
82
+ try { s.proc.write(buf); } catch { return false; }
83
+ } else {
84
+ if (!s.proc.stdin || !s.proc.stdin.writable) return false;
85
+ try { s.proc.stdin.write(buf); } catch { return false; }
86
+ }
87
+ return true;
88
+ }
89
+
90
+ export function resizeSession(sid, cols, rows) {
91
+ const s = sessions.get(sid);
92
+ if (!s || s.kind !== 'pty' || typeof s.proc.resize !== 'function') return false;
93
+ s.cols = cols; s.rows = rows;
94
+ try { s.proc.resize(cols, rows); return true; } catch { return false; }
95
+ }
96
+
97
+ export function attachWs(sid, ws) {
98
+ const s = sessions.get(sid);
99
+ if (!s) { try { ws.close(4404, 'session-not-found'); } catch {} return false; }
100
+ s.clients.add(ws);
101
+ ws.on('close', () => { s.clients.delete(ws); });
102
+ ws.on('message', (msg, isBinary) => {
103
+ if (isBinary || Buffer.isBuffer(msg)) { writeToSession(sid, msg); return; }
104
+ const text = msg.toString();
105
+ if (text && text.length > 0 && text[0] === '{') {
106
+ try { const j = JSON.parse(text); if (j.type === 'resize' && j.cols && j.rows) { resizeSession(sid, j.cols, j.rows); return; } } catch {}
107
+ }
108
+ writeToSession(sid, text);
109
+ });
110
+ ws.on('error', () => { s.clients.delete(ws); });
111
+ return true;
112
+ }
package/lib/ws-setup.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { WebSocketServer } from 'ws';
4
+ import * as term from './terminal.js';
4
5
  // Legacy WS handlers removed; no-op shim kept for callsite compatibility.
5
6
  const registerLegacyHandler = () => {};
6
7
 
@@ -22,6 +23,12 @@ export function createWsSetup(server, { BASE_URL, watch, staticDir, _assetCache,
22
23
  if (wsRoute === '/hot-reload') {
23
24
  hotReloadClients.push(ws);
24
25
  ws.on('close', () => { const i = hotReloadClients.indexOf(ws); if (i > -1) hotReloadClients.splice(i, 1); });
26
+ } else if (wsRoute.startsWith('/api/terminal/sessions/')) {
27
+ // Terminal session WS — auth was already enforced by wss-level PASSWORD
28
+ // check at the top of this connection callback. attach to existing session.
29
+ const m = wsRoute.match(/^\/api\/terminal\/sessions\/([0-9a-f]+)$/);
30
+ if (!m) { ws.close(4400, 'bad-terminal-path'); return; }
31
+ term.attachWs(m[1], ws);
25
32
  } else if (wsRoute === '/sync') {
26
33
  syncClients.add(ws);
27
34
  ws.isAlive = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.932",
3
+ "version": "1.0.934",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -4,10 +4,10 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <title>agentgui</title>
7
- <meta name="description" content="agentgui — live client for any acptoapi backend.">
8
- <link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@latest/dist/247420.css">
7
+ <meta name="description" content="agentgui — multi-agent client with same-origin server, in-process ccsniff history, and ACP chat.">
8
+ <link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@0.0.127/dist/247420.css">
9
9
  <script type="importmap">
10
- { "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js" } }
10
+ { "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@0.0.127/dist/247420.js" } }
11
11
  </script>
12
12
  <style>
13
13
  html, body { margin: 0; height: 100%; }
@@ -3,7 +3,7 @@ import * as B from './backend.js';
3
3
 
4
4
  installStyles().catch(() => {});
5
5
 
6
- const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, Row, Panel, PageHeader, SearchInput, TextField, EventList } = C;
6
+ const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, Row, Panel, PageHeader, SearchInput, TextField, Select, Btn, EventList } = C;
7
7
 
8
8
  const state = {
9
9
  backend: B.getBackend(),
@@ -114,18 +114,16 @@ function view() {
114
114
 
115
115
  const crumbRight = state.tab === 'chat'
116
116
  ? [
117
- h('select', {
118
- key: 'modelsel',
119
- onchange: (e) => { state.selectedModel = e.target.value; render(); },
120
- },
121
- h('option', { key: '__', value: '' }, '— model —'),
122
- ...state.models.map(m =>
123
- h('option', { key: m.id, value: m.id, selected: m.id === state.selectedModel }, m.id)
124
- ),
125
- ),
117
+ Select({
118
+ key: 'modelsel',
119
+ value: state.selectedModel,
120
+ placeholder: '— model —',
121
+ options: state.models.map(m => ({ value: m.id, label: m.id })),
122
+ onChange: (v) => { state.selectedModel = v; render(); },
123
+ }),
126
124
  state.chat.busy
127
- ? h('a', { key: 'stop', onclick: cancelChat, style: 'cursor:pointer' }, '◼ stop')
128
- : h('a', { key: 'new', onclick: newChat, style: 'cursor:pointer' }, '+ new'),
125
+ ? Btn({ key: 'stop', onClick: cancelChat, children: '◼ stop' })
126
+ : Btn({ key: 'new', onClick: newChat, children: '+ new' }),
129
127
  dot,
130
128
  ]
131
129
  : [dot];
@@ -169,13 +167,22 @@ function mainContent() {
169
167
 
170
168
  // ── chat ───────────────────────────────────────────────────────────────────
171
169
  function chatMain() {
172
- const msgs = state.chat.messages.map((m, i) => ({
173
- key: String(i),
174
- who: m.role === 'user' ? 'you' : 'them',
175
- name: m.role === 'assistant' ? (state.selectedModel || 'agent') : 'you',
176
- time: m.time || '',
177
- parts: [{ kind: 'text', text: m.content || '' }],
178
- }));
170
+ const lastIdx = state.chat.messages.length - 1;
171
+ const msgs = state.chat.messages.map((m, i) => {
172
+ const isAssistant = m.role === 'assistant';
173
+ const isStreaming = state.chat.busy && i === lastIdx && isAssistant;
174
+ const isEmptyStreaming = isStreaming && !m.content;
175
+ return {
176
+ key: String(i),
177
+ who: isAssistant ? 'them' : 'you',
178
+ name: isAssistant ? (state.selectedModel || 'agent') : 'you',
179
+ time: m.time || '',
180
+ typing: isEmptyStreaming,
181
+ parts: isEmptyStreaming
182
+ ? undefined
183
+ : [{ kind: isAssistant ? 'md' : 'text', text: m.content || '' }],
184
+ };
185
+ });
179
186
 
180
187
  const composer = ChatComposer({
181
188
  value: state.chat.draft,
@@ -250,12 +257,11 @@ function historyMain() {
250
257
  Panel({
251
258
  title: state.events.length + ' events',
252
259
  children: EventList({
253
- events: state.events.map((e, i) => ({
260
+ items: state.events.map((e, i) => ({
254
261
  key: 'ev' + i,
255
- rank: String(i + 1).padStart(3, '0'),
262
+ code: String(i + 1).padStart(3, '0'),
256
263
  title: (e.text || '').slice(0, 200) || '(empty)',
257
264
  sub: new Date(e.ts).toLocaleString() + ' · ' + (e.role || '?') + ' · ' + (e.type || '?') + (e.tool ? ' · ⌘ ' + e.tool : ''),
258
- rail: e.role === 'error' ? 'flame' : (e.role === 'user' ? 'green' : 'purple'),
259
265
  })),
260
266
  }),
261
267
  }),
@@ -334,16 +340,19 @@ function settingsMain() {
334
340
  onInput: (v) => { state.backendDraft = v; render(); },
335
341
  }),
336
342
  h('p', { key: 'hp', class: 'lede' }, (ok ? '● ' : '○ ') + JSON.stringify(state.health)),
337
- h('button', {
343
+ Btn({
338
344
  key: 'savebtn',
339
- onclick: () => {
345
+ primary: true,
346
+ onClick: (e) => {
347
+ e.preventDefault();
340
348
  B.setBackend(state.backendDraft);
341
349
  state.backend = state.backendDraft;
342
350
  state.health = { status: 'unknown' };
343
351
  render();
344
352
  init();
345
353
  },
346
- }, 'save + reconnect'),
354
+ children: 'save + reconnect',
355
+ }),
347
356
  ],
348
357
  }),
349
358
  Panel({
@@ -1,4 +1,6 @@
1
- // acptoapi backend client. Resolves base URL from ?backend= or localStorage or default.
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.
2
4
  const KEY = 'agentgui.backend';
3
5
  const DEFAULT_BACKEND = '';
4
6