@danieltmn/openbridge 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ Todos los cambios relevantes de OpenBridge. Formato basado en
4
4
  [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y
5
5
  [Versionado Semantico](https://semver.org/lang/es/).
6
6
 
7
+ ## [0.5.0] - 2026-09-14
8
+
9
+ ### Agregado
10
+
11
+ - Comando `openbridge update` (compara con npm; `--yes` actualiza la instalacion
12
+ global).
13
+ - Comando `openbridge join <url>` para sumar una PC como puente de un hub:
14
+ persiste `apiUrl`/`apiToken`/`bridgeId` y arranca el puente (`--no-start` solo
15
+ guarda).
16
+ - `npm run lint` (`scripts/lint.mjs`): `node --check` sobre todo el JS; corre en
17
+ CI.
18
+ - Test end-to-end del puente con `opencode` mockeado (`test/bridge.test.js`) y
19
+ tests del CLI (`test/cli.test.js`).
20
+ - **MCP**: vista en la web y comando de chat `/mcp` que muestran los servidores
21
+ MCP de opencode y su estado (`opencode mcp list`); comando de puente
22
+ `mcp_list`.
23
+ - `CONTRIBUTING.md`.
24
+
25
+ ### Cambiado
26
+
27
+ - Claves internas `ocx_*` renombradas a `ob_*` con migracion automatica
28
+ (localStorage y cookie de tema). Se mantienen como compatibilidad el env
29
+ `OPENCONEX_HOME` y el bin `openconex` (deprecados).
30
+ - `config.command` puede apuntar a un script de Node (`.js`/`.mjs`/`.cjs`): se
31
+ ejecuta con el `node` actual (wrappers propios y pruebas con CLI simulado).
32
+
7
33
  ## [0.4.0] - 2026-09-14
8
34
 
9
35
  ### Agregado
package/README.md CHANGED
@@ -90,10 +90,12 @@ iniciar sesión** una vez; los chats, carpetas, túnel y push se conservan.
90
90
  | `openbridge tunnel` | Muestra o cambia el proveedor de túnel y su dominio fijo (`--domain`) |
91
91
  | `openbridge logs` | Logs (`--follow`, `--server`, `--bridge`) |
92
92
  | `openbridge bridge` | Corre **solo** el puente (`--api --token --id --name`) |
93
+ | `openbridge join` | Vincula esta PC como puente de un hub (`<url> --token --id --name`) |
93
94
  | `openbridge import` | Trae `data/` de OpenConex (`<data-dir> [--force]`) |
94
95
  | `openbridge reset` | Borra chats/datos (`--session <id>`, `--yes`) |
95
96
  | `openbridge autostart` | Arranque automático (`install`/`remove`) |
96
97
  | `openbridge doctor` | Verifica Node, opencode, configuración y puerto |
98
+ | `openbridge update` | Busca una versión nueva en npm (`--yes` para actualizar) |
97
99
 
98
100
  ## Casa portable
99
101
 
@@ -130,10 +132,11 @@ Ejemplo concreto:
130
132
  2. Copiá el **token del puente** del hub: `.openbridge/app.json` → `bridgeToken`.
131
133
  3. En la **PC 2** (remota):
132
134
  ```bash
133
- openbridge init --tunnel none # no necesita túnel propio
134
- openbridge bridge --api https://tu-url-publica/api.php \
135
- --token <bridgeToken> --id pc2 --name "PC 2"
135
+ openbridge join https://tu-url-publica --token <bridgeToken> --id pc2 --name "PC 2"
136
+ # guarda la config y arranca el puente; con --no-start solo guarda
136
137
  ```
138
+ (equivale a `openbridge bridge --api … --token … --id … --name …`, pero
139
+ persiste la config para no repetir flags)
137
140
  4. Abrí la URL del hub desde el celular: en el sidebar aparecen **PC 1** y
138
141
  **PC 2** para alternar.
139
142
 
@@ -164,6 +167,12 @@ suscripción no persiste entre reinicios (el push está atado al origen); la app
164
167
  vuelve a registrar cuando la abrís en la URL nueva. Para que sea estable, usá una
165
168
  URL fija (ngrok con `--domain`).
166
169
 
170
+ ## MCP
171
+
172
+ La web tiene una vista **mcp** y el chat acepta `/mcp`: muestran los servidores
173
+ MCP de opencode y su estado (`opencode mcp list`). El comando corre en la PC del
174
+ puente, así que refleja la configuración de opencode de esa computadora.
175
+
167
176
  ## Seguridad
168
177
 
169
178
  - La app escucha **solo en `127.0.0.1`**; el túnel la expone a internet.
@@ -19,7 +19,7 @@ el estado); `--stream` lo deja en primer plano.
19
19
  | Ruta | Responsabilidad |
20
20
  |---|---|
21
21
  | `bin/openbridge.js` | Entrada CLI + guard de Node. |
22
- | `src/cli.js` | Comandos (`init`, `passwd`, `users`, `server`, `stop`, `status`, `qr`, `tunnel`, `logs`, `bridge`, `import`, `reset`, `autostart`, `doctor`). |
22
+ | `src/cli.js` | Comandos (`init`, `passwd`, `users`, `server`, `stop`, `status`, `qr`, `tunnel`, `logs`, `bridge`, `join`, `import`, `reset`, `autostart`, `doctor`, `update`). |
23
23
  | `src/paths.js` | Casa portable: ubicacion de `config.json`, `app.json`, `data/`, `logs/`. |
24
24
  | `src/config.js` | Lectura/escritura de config, hash scrypt, **usuarios/roles**, VAPID. |
25
25
  | `src/auth.js` | Cookie de sesion firmada, CSRF, remember-me, **roles**, rate limit, token del puente. |
@@ -65,9 +65,13 @@ OpenBridge. La CLI pasa la **base** (`--dir`/cwd) al puente via
65
65
 
66
66
  ## Multi-PC
67
67
 
68
- El hub corre la app; cada PC corre `openbridge bridge` apuntando al hub
69
- (`apiUrl` + `apiToken` + `bridgeId`). El catalogo se guarda por PC
70
- (`data/catalog-<id>.json`) y el sidebar permite elegir que PC usar.
68
+ El hub corre la app; cada PC corre `openbridge bridge` (o `openbridge join <url>`,
69
+ que persiste `apiUrl` + `apiToken` + `bridgeId`) apuntando al hub. El catalogo se
70
+ guarda por PC (`data/catalog-<id>.json`) y el sidebar permite elegir que PC usar.
71
+
72
+ `config.command` es el CLI a ejecutar (default `opencode`). Si apunta a un script
73
+ de Node (`.js`/`.mjs`/`.cjs`) el puente lo corre con el `node` actual, lo que
74
+ permite wrappers propios y las pruebas con un opencode simulado.
71
75
 
72
76
  ## Usuarios y roles
73
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieltmn/openbridge",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Tu opencode en el celular, sin hosting: corre la app, el puente y un tunel publico desde tu PC.",
5
5
  "type": "commonjs",
6
6
  "author": "tamnora",
@@ -43,6 +43,7 @@
43
43
  "scripts": {
44
44
  "start": "node bin/openbridge.js server",
45
45
  "test": "node --test",
46
+ "lint": "node scripts/lint.mjs",
46
47
  "release": "node scripts/release.mjs",
47
48
  "release:dry": "node scripts/release.mjs --dry-run",
48
49
  "prepublishOnly": "npm test"
@@ -412,6 +412,21 @@ function parseShimExe(shimPath, shimDir) {
412
412
  return exe;
413
413
  }
414
414
 
415
+ // Si `command` apunta a un script de Node (.js/.mjs/.cjs) lo ejecutamos con el
416
+ // node actual. Permite wrappers propios (p. ej. un runner que filtra logs) y
417
+ // que las pruebas usen un opencode simulado sin depender del PATH.
418
+ function isNodeScriptCommand(cmd) {
419
+ return /\.(c|m)?js$/i.test(String(cmd || '').trim());
420
+ }
421
+
422
+ function commandSpec(args) {
423
+ const cmd = config.command;
424
+ if (isNodeScriptCommand(cmd)) {
425
+ return { cmd: process.execPath, argv: [path.resolve(cmd)].concat(args) };
426
+ }
427
+ return { cmd: resolveCommand(), argv: args };
428
+ }
429
+
415
430
  // ---------------------------------------------------------------------------
416
431
  // Ejecutar comandos auxiliares de opencode
417
432
  // ---------------------------------------------------------------------------
@@ -457,9 +472,9 @@ function runCmd(bin, args, opts) {
457
472
 
458
473
  function runCli(args, opts) {
459
474
  return new Promise((resolve) => {
460
- const cmd = resolveCommand();
475
+ const spec = commandSpec(args);
461
476
  const timeoutMs = Math.max((opts && opts.timeout) || 0, 0);
462
- const child = spawn(cmd, args, {
477
+ const child = spawn(spec.cmd, spec.argv, {
463
478
  cwd: (opts && opts.cwd) || undefined,
464
479
  stdio: ['pipe', 'pipe', 'pipe'],
465
480
  env: process.env,
@@ -511,9 +526,9 @@ function runCli(args, opts) {
511
526
  // Resolución: { ok, code, text, reasoning, killed, canceled, sessionID, errorText }.
512
527
  function streamCli(args, opts, onPartial) {
513
528
  return new Promise((resolve) => {
514
- const cmd = resolveCommand();
529
+ const spec = commandSpec(args);
515
530
  const timeoutMs = Math.max((opts && opts.timeout) || 0, 0);
516
- const child = spawn(cmd, args, {
531
+ const child = spawn(spec.cmd, spec.argv, {
517
532
  cwd: (opts && opts.cwd) || undefined,
518
533
  stdio: ['pipe', 'pipe', 'pipe'],
519
534
  env: process.env,
@@ -834,7 +849,7 @@ async function runOpencode(msg, onPartial) {
834
849
  try {
835
850
  const mime = msg.img.slice(5, msg.img.indexOf(';'));
836
851
  const ext = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/webp': '.webp', 'image/gif': '.gif' }[mime] || '.png';
837
- imgPath = path.join(os.tmpdir(), 'ocx-img-' + msg.session_id + '-' + msg.id + ext);
852
+ imgPath = path.join(os.tmpdir(), 'ob-img-' + msg.session_id + '-' + msg.id + ext);
838
853
  fs.writeFileSync(imgPath, Buffer.from(msg.img.slice(msg.img.indexOf(',') + 1), 'base64'));
839
854
  log('imagen adjunta: ' + Math.round(fs.statSync(imgPath).size / 1024) + ' KB para el mensaje #' + msg.id);
840
855
  } catch (e) {
@@ -975,6 +990,16 @@ async function runMessage(msg) {
975
990
  return { text: '', opencodeSession: null, done: true };
976
991
  }
977
992
 
993
+ if (name === 'mcp') {
994
+ log('comando /mcp');
995
+ const r = await runCli(['mcp', 'list'], { timeout: 30000 });
996
+ const text = r.ok
997
+ ? (r.text || 'No hay servidores MCP configurados.')
998
+ : 'No se pudieron listar los MCP: ' + (r.text || ('exit ' + r.code));
999
+ await respondSimple(msg, text, false);
1000
+ return { text: '', opencodeSession: null, done: true };
1001
+ }
1002
+
978
1003
  if (name === 'workspace') {
979
1004
  log('comando /workspace');
980
1005
  const ws = config.workspace ? path.resolve(config.workspace) : '';
@@ -1014,6 +1039,7 @@ async function runMessage(msg) {
1014
1039
  + '• /compact, /resumen, /summarize — liberar contexto\n'
1015
1040
  + '• /models [filtro] — proveedores disponibles o búsqueda de modelos\n'
1016
1041
  + '• /agents — listar agentes disponibles\n'
1042
+ + '• /mcp — servidores MCP de opencode y su estado\n'
1017
1043
  + '• /folders, /carpetas, /dirs — listar carpetas del workspace\n'
1018
1044
  + '• /workspace — mostrar el espacio de trabajo\n'
1019
1045
  + '• /status, /estado — estado del puente\n'
@@ -2115,6 +2141,19 @@ async function handleCommand(cmd) {
2115
2141
  await handleFsCommand(cmd);
2116
2142
  return;
2117
2143
  }
2144
+ // Servidores MCP de opencode: se devuelve JSON con la salida cruda para que
2145
+ // la web la muestre en el panel.
2146
+ if (cmd.name === 'mcp_list') {
2147
+ const r = await runCli(['mcp', 'list'], { timeout: 30000 });
2148
+ await api('command_done', {
2149
+ id: cmd.id,
2150
+ ok: r.ok,
2151
+ text: JSON.stringify({ output: r.text || '', ok: r.ok }),
2152
+ error: r.ok ? '' : ('exit ' + r.code),
2153
+ }, cmd._t);
2154
+ log('comando #' + cmd.id + ' mcp_list (ok=' + r.ok + ')');
2155
+ return;
2156
+ }
2118
2157
  // Cambios git del proyecto (status/diff/revertir).
2119
2158
  if (cmd.name === 'git_status' || cmd.name === 'git_diff' || cmd.name === 'git_checkout') {
2120
2159
  await handleGitCommand(cmd);
package/src/cli.js CHANGED
@@ -600,8 +600,8 @@ async function cmdLogs(argv) {
600
600
  // ---------------------------------------------------------------------------
601
601
  async function cmdBridge(argv) {
602
602
  const { flags } = parseArgs(argv);
603
- if (!paths.exists()) {
604
- console.error('No hay configuracion en ' + paths.home() + '. Corre primero: openbridge init');
603
+ if (!fs.existsSync(paths.configPath())) {
604
+ console.error('No hay configuracion de puente en ' + paths.home() + '. Corre primero: openbridge join <url> o openbridge init');
605
605
  return 1;
606
606
  }
607
607
  // Permite apuntar a otro hub sin editar config.json a mano.
@@ -620,6 +620,41 @@ async function cmdBridge(argv) {
620
620
  return new Promise((resolve) => child.on('exit', (code) => resolve(code || 0)));
621
621
  }
622
622
 
623
+ // ---------------------------------------------------------------------------
624
+ // join: esta PC se suma como puente de un hub (otra PC con `openbridge server`).
625
+ // Guarda la URL/token/identidad en config.json y arranca el puente.
626
+ // ---------------------------------------------------------------------------
627
+ async function cmdJoin(argv) {
628
+ const { flags, _ } = parseArgs(argv);
629
+ const url = String(_[0] || flags.api || '').trim();
630
+ if (!url || !/^https?:\/\//i.test(url)) {
631
+ console.error('Uso: openbridge join <url-del-hub> [--token <t>] [--id <pc>] [--name "<nombre>"] [--no-start]');
632
+ console.error('Ej.: openbridge join https://mi-pc.trycloudflare.com --token <t> --id pc2 --name "PC oficina"');
633
+ return 1;
634
+ }
635
+ paths.ensureDirs();
636
+ const cfg = config.readBridge();
637
+ cfg.apiUrl = url.replace(/\/+$/, '');
638
+ if (typeof flags.token === 'string') cfg.apiToken = flags.token;
639
+ if (typeof flags.id === 'string') cfg.bridgeId = sanitizeId(flags.id);
640
+ else if (!cfg.bridgeId) cfg.bridgeId = sanitizeId(os.hostname());
641
+ if (typeof flags.name === 'string' && flags.name.trim()) cfg.bridgeName = flags.name.trim().slice(0, 40);
642
+ else if (!cfg.bridgeName) cfg.bridgeName = cfg.bridgeId;
643
+ config.writeBridge(cfg);
644
+
645
+ console.log('Puente vinculado al hub: ' + cfg.apiUrl);
646
+ console.log(' id : ' + cfg.bridgeId);
647
+ console.log(' nombre : ' + cfg.bridgeName);
648
+ if (!cfg.apiToken) console.log('aviso: sin token (--token). Solo sirve si el hub no exige token del puente.');
649
+ if (flags['no-start']) {
650
+ console.log('Arrancalo cuando quieras con: openbridge bridge');
651
+ return 0;
652
+ }
653
+ console.log('');
654
+ console.log('Arrancando el puente (Ctrl+C para salir)...');
655
+ return cmdBridge([]);
656
+ }
657
+
623
658
  // ---------------------------------------------------------------------------
624
659
  // import / reset (datos)
625
660
  // ---------------------------------------------------------------------------
@@ -915,6 +950,53 @@ async function cmdDoctor() {
915
950
  return ok ? 0 : 1;
916
951
  }
917
952
 
953
+ // ---------------------------------------------------------------------------
954
+ // update: compara con npm y, con --yes, actualiza la instalacion global.
955
+ // ---------------------------------------------------------------------------
956
+ function npmRun(args) {
957
+ return spawnSync('npm', args, { encoding: 'utf8', shell: process.platform === 'win32', windowsHide: true });
958
+ }
959
+
960
+ async function cmdUpdate(argv) {
961
+ const { flags } = parseArgs(argv);
962
+ const tag = (typeof flags.tag === 'string' && flags.tag) ? flags.tag : 'latest';
963
+ const pkg = '@danieltmn/openbridge';
964
+ const spec = pkg + '@' + tag;
965
+ console.log('Version actual: ' + VERSION);
966
+
967
+ const view = npmRun(['view', spec, 'version', '--json']);
968
+ let latest = '';
969
+ try { latest = String(JSON.parse(view.stdout)).trim(); } catch (e) { latest = String(view.stdout || '').trim().replace(/"/g, ''); }
970
+ if (view.status !== 0 || !/^\d+\.\d+\.\d+/.test(latest)) {
971
+ console.error('No se pudo consultar npm (' + tag + '): ' + ((view.stderr || '').trim() || 'revisa tu conexion'));
972
+ return 1;
973
+ }
974
+ console.log('Disponible (' + tag + '): ' + latest);
975
+ if (latest === VERSION) {
976
+ console.log('Ya estas en la ultima version.');
977
+ return 0;
978
+ }
979
+ if (flags.check) {
980
+ console.log('Hay una version nueva. Corre: openbridge update --yes');
981
+ return 0;
982
+ }
983
+ if (!flags.yes && !flags.y) {
984
+ console.log('');
985
+ console.log('Para actualizar ahora:');
986
+ console.log(' openbridge update --yes (o) npm i -g ' + spec);
987
+ return 0;
988
+ }
989
+
990
+ console.log('Actualizando a ' + spec + '...');
991
+ const r = spawnSync('npm', ['i', '-g', spec], { stdio: 'inherit', shell: process.platform === 'win32', windowsHide: true });
992
+ if (r.status !== 0) {
993
+ console.error('La actualizacion fallo. Proba a mano: npm i -g ' + spec);
994
+ return 1;
995
+ }
996
+ console.log('Listo. Reinicia el server para usar la version nueva: openbridge stop && openbridge server');
997
+ return 0;
998
+ }
999
+
918
1000
  // ---------------------------------------------------------------------------
919
1001
  function usage() {
920
1002
  console.log('OpenBridge ' + VERSION + ' — tu opencode en el celular, sin hosting');
@@ -931,10 +1013,12 @@ function usage() {
931
1013
  console.log(' tunnel Muestra o cambia el proveedor de tunel (tunnelmole|ngrok|cloudflare|none) [--domain]');
932
1014
  console.log(' logs Ultimas lineas de los logs (--follow --server --bridge)');
933
1015
  console.log(' bridge Corre solo el puente (--api --token --id --name)');
1016
+ console.log(' join Vincula esta PC como puente de un hub (<url> --token --id --name)');
934
1017
  console.log(' import Trae data/ de OpenConex (<data-dir> [--force])');
935
1018
  console.log(' reset Borra todos los chats/datos (--session <id> --yes)');
936
1019
  console.log(' autostart Instala/quita el arranque automatico (install|remove)');
937
1020
  console.log(' doctor Verifica Node, opencode, configuracion y puerto');
1021
+ console.log(' update Busca una version nueva en npm (--yes para actualizar)');
938
1022
  console.log('');
939
1023
  console.log('Opciones comunes: --dir <ruta> (casa portable; default: directorio actual)');
940
1024
  console.log('init: --workspace --name --id --port --password --tunnel --domain --yes --force');
@@ -964,10 +1048,12 @@ async function main(argv) {
964
1048
  case 'tunnel': return cmdTunnel(args.slice(1));
965
1049
  case 'logs': return cmdLogs(args.slice(1));
966
1050
  case 'bridge': return cmdBridge(args.slice(1));
1051
+ case 'join': return cmdJoin(args.slice(1));
967
1052
  case 'import': return cmdImport(args.slice(1));
968
1053
  case 'reset': return cmdReset(args.slice(1));
969
1054
  case 'autostart': return cmdAutostart(args.slice(1));
970
1055
  case 'doctor': return cmdDoctor();
1056
+ case 'update': return cmdUpdate(args.slice(1));
971
1057
  case 'version': case '-v': case '--version': console.log(VERSION); return 0;
972
1058
  case 'help': case '-h': case '--help': usage(); return 0;
973
1059
  default:
@@ -1,5 +1,21 @@
1
1
  "use strict";
2
2
 
3
+ // Migracion de claves viejas `ocx_*` a `ob_*` (una sola vez, antes de leer nada).
4
+ (function () {
5
+ try {
6
+ var keys = [];
7
+ for (var i = 0; i < localStorage.length; i++) {
8
+ var k = localStorage.key(i);
9
+ if (k && k.indexOf('ocx_') === 0) keys.push(k);
10
+ }
11
+ for (var j = 0; j < keys.length; j++) {
12
+ var nk = 'ob_' + keys[j].slice(4);
13
+ if (localStorage.getItem(nk) === null) localStorage.setItem(nk, localStorage.getItem(keys[j]));
14
+ localStorage.removeItem(keys[j]);
15
+ }
16
+ } catch (e) { /* almacenamiento no disponible */ }
17
+ })();
18
+
3
19
  var CFG = window.APP_CONFIG || {};
4
20
  var CSRF = CFG.csrf || '';
5
21
  var INITIAL_SESSION = typeof CFG.initialSession === 'number' ? CFG.initialSession : null;
@@ -50,6 +66,7 @@ var els = {
50
66
  skinPanel: document.getElementById('skinPanel'),
51
67
  viewProcs: document.getElementById('viewProcs'),
52
68
  viewChanges: document.getElementById('viewChanges'),
69
+ viewMcp: document.getElementById('viewMcp'),
53
70
  themeColor: document.getElementById('themeColor'),
54
71
  themeLink: document.getElementById('themeStylesheet'),
55
72
  statusbar: document.getElementById('statusbar'),
@@ -104,6 +121,7 @@ var CMDS = [
104
121
  { cmd: '/compact', desc: 'Liberar contexto' },
105
122
  { cmd: '/models', desc: 'Proveedores y búsqueda de modelos' },
106
123
  { cmd: '/agents', desc: 'Listar agentes disponibles' },
124
+ { cmd: '/mcp', desc: 'Servidores MCP de opencode' },
107
125
  { cmd: '/folders', desc: 'Carpetas del workspace' },
108
126
  { cmd: '/workspace', desc: 'Mostrar el espacio de trabajo' },
109
127
  { cmd: '/status', desc: 'Estado del puente' },
@@ -167,7 +185,7 @@ var state = {
167
185
 
168
186
  // Puente activo y lista conocida se restauran antes de pintar (caché local).
169
187
  state.activeBridge = loadStoredBridge();
170
- var _cachedBridges = lsGet('ocx_bridges');
188
+ var _cachedBridges = lsGet('ob_bridges');
171
189
  if (Array.isArray(_cachedBridges) && _cachedBridges.length) state.bridges = _cachedBridges;
172
190
 
173
191
  // Estado del panel "procesos" de la sesión activa (dev servers en la PC).
@@ -271,8 +289,8 @@ function applyTheme(slug, opts) {
271
289
  } catch (e) {}
272
290
  if (!opts || opts.persist !== false) {
273
291
  try {
274
- document.cookie = 'ocx_theme=' + encodeURIComponent(s) + ';path=/;max-age=' + (60 * 60 * 24 * 365) + ';SameSite=Lax';
275
- localStorage.setItem('ocx_theme', s);
292
+ document.cookie = 'ob_theme=' + encodeURIComponent(s) + ';path=/;max-age=' + (60 * 60 * 24 * 365) + ';SameSite=Lax';
293
+ localStorage.setItem('ob_theme', s);
276
294
  } catch (e) {}
277
295
  }
278
296
  renderSkinPanel();
@@ -329,16 +347,16 @@ function activeBridgeInfo() { return bridgeInfo(activeBridgeId()); }
329
347
  // El catálogo/estado se guardan por puente activo (cada PC tiene el suyo).
330
348
  function catalogKey() {
331
349
  var id = activeBridgeId();
332
- return id ? 'ocx_catalog_' + id : 'ocx_catalog';
350
+ return id ? 'ob_catalog_' + id : 'ob_catalog';
333
351
  }
334
352
  function catalogVerKey() {
335
353
  return catalogKey() + '_ver';
336
354
  }
337
355
  function loadStoredBridge() {
338
- try { return localStorage.getItem('ocx_bridge') || ''; } catch (e) { return ''; }
356
+ try { return localStorage.getItem('ob_bridge') || ''; } catch (e) { return ''; }
339
357
  }
340
358
  function storeBridge(id) {
341
- try { localStorage.setItem('ocx_bridge', id || ''); } catch (e) {}
359
+ try { localStorage.setItem('ob_bridge', id || ''); } catch (e) {}
342
360
  }
343
361
 
344
362
  // Sesiones visibles: solo las del puente activo cuando hay varios puentes.
@@ -385,7 +403,7 @@ function renderBridgeBar() {
385
403
  // applyBootPayload / switchActiveBridge para no mezclar catálogos).
386
404
  function applyBridges(list) {
387
405
  state.bridges = Array.isArray(list) ? list : [];
388
- try { localStorage.setItem('ocx_bridges', JSON.stringify(state.bridges)); } catch (e) {}
406
+ try { localStorage.setItem('ob_bridges', JSON.stringify(state.bridges)); } catch (e) {}
389
407
  renderBridgeBar();
390
408
  }
391
409
 
@@ -446,7 +464,7 @@ function applyBootPayload(data) {
446
464
  state.catalog.last_online_ts = data.online_ts;
447
465
  }
448
466
  var sessions = data.sessions || [];
449
- lsSet('ocx_sessions', sessions);
467
+ lsSet('ob_sessions', sessions);
450
468
  renderHome(sessions);
451
469
  applyOnlineUI();
452
470
  }
@@ -718,13 +736,13 @@ function projectLabel(folderPath) {
718
736
  function loadOpenProjects() {
719
737
  if (state.openProjects) return state.openProjects;
720
738
  var stored = null;
721
- try { stored = JSON.parse(localStorage.getItem('ocx_openProjects') || 'null'); } catch (e) {}
739
+ try { stored = JSON.parse(localStorage.getItem('ob_openProjects') || 'null'); } catch (e) {}
722
740
  state.openProjects = (stored && typeof stored === 'object') ? stored : null;
723
741
  return state.openProjects;
724
742
  }
725
743
 
726
744
  function saveOpenProjects() {
727
- try { localStorage.setItem('ocx_openProjects', JSON.stringify(state.openProjects || {})); } catch (e) {}
745
+ try { localStorage.setItem('ob_openProjects', JSON.stringify(state.openProjects || {})); } catch (e) {}
728
746
  }
729
747
 
730
748
  function isProjectOpen(folderPath, sessions) {
@@ -992,6 +1010,7 @@ function showView(name) {
992
1010
  els.viewPreview.style.display = 'none';
993
1011
  els.viewProcs.style.display = 'none';
994
1012
  els.viewChanges.style.display = 'none';
1013
+ els.viewMcp.style.display = 'none';
995
1014
 
996
1015
  if (name !== 'procs') stopProcView();
997
1016
 
@@ -1064,6 +1083,12 @@ function showView(name) {
1064
1083
  els.sendForm.style.display = 'none';
1065
1084
  els.btnCmds.style.display = 'none';
1066
1085
  renderChangesView();
1086
+ } else if (name === 'mcp') {
1087
+ els.viewMcp.style.display = '';
1088
+ els.btnBack.style.display = 'none';
1089
+ els.sendForm.style.display = 'none';
1090
+ els.btnCmds.style.display = 'none';
1091
+ renderMcpView();
1067
1092
  }
1068
1093
 
1069
1094
  if (els.fabNew) els.fabNew.classList.toggle('show', name === 'home');
@@ -1131,21 +1156,21 @@ if (composeCloseEl) composeCloseEl.addEventListener('click', composeCloseSheet);
1131
1156
  // Sidebar: logo → inicio, ajuste de ancho y ocultar (desktop) / cerrar drawer (móvil)
1132
1157
  // ---------------------------------------------------------------------------
1133
1158
  function sbHidden() {
1134
- try { return localStorage.getItem('ocx_sbHidden') === '1'; } catch (e) { return false; }
1159
+ try { return localStorage.getItem('ob_sbHidden') === '1'; } catch (e) { return false; }
1135
1160
  }
1136
1161
  function setSbHidden(hidden) {
1137
- try { localStorage.setItem('ocx_sbHidden', hidden ? '1' : '0'); } catch (e) {}
1162
+ try { localStorage.setItem('ob_sbHidden', hidden ? '1' : '0'); } catch (e) {}
1138
1163
  document.body.classList.toggle('sb-hide', hidden);
1139
1164
  }
1140
1165
  function sbWidth() {
1141
1166
  try {
1142
- var w = parseInt(localStorage.getItem('ocx_sbW'), 10);
1167
+ var w = parseInt(localStorage.getItem('ob_sbW'), 10);
1143
1168
  if (w >= 200 && w <= 480) return w;
1144
1169
  } catch (e) {}
1145
1170
  return 264;
1146
1171
  }
1147
1172
  function setSbWidth(w) {
1148
- try { localStorage.setItem('ocx_sbW', String(w)); } catch (e) {}
1173
+ try { localStorage.setItem('ob_sbW', String(w)); } catch (e) {}
1149
1174
  document.documentElement.style.setProperty('--sb-w', w + 'px');
1150
1175
  }
1151
1176
 
@@ -1321,7 +1346,7 @@ function renderHome(sessions) {
1321
1346
  async function loadSessions() {
1322
1347
  var data = await api('api.php?action=sessions');
1323
1348
  if (data.ok) {
1324
- lsSet('ocx_sessions', data.sessions || []);
1349
+ lsSet('ob_sessions', data.sessions || []);
1325
1350
  renderHome(data.sessions || []);
1326
1351
  } else if (!state.sessions.length) {
1327
1352
  els.home.innerHTML = '<div class="empty">Sin conexión</div>';
@@ -3007,6 +3032,36 @@ function renderChangesView() {
3007
3032
  loadChanges(folder);
3008
3033
  }
3009
3034
 
3035
+ // ---------------------------------------------------------------------------
3036
+ // Vista MCP: estado de los servidores MCP de opencode (opencode mcp list).
3037
+ // ---------------------------------------------------------------------------
3038
+ function renderMcpView() {
3039
+ els.hTitle.textContent = 'mcp';
3040
+ els.hSub.textContent = 'servidores MCP de opencode';
3041
+ if (!state.online) {
3042
+ els.viewMcp.innerHTML = '<div class="placeholder">El puente está apagado. Iniciá “OpenBridge” en tu PC.</div>';
3043
+ return;
3044
+ }
3045
+ els.viewMcp.innerHTML = '<div class="view-head"><h2>servidores MCP</h2>'
3046
+ + '<div class="view-sub">opencode mcp list</div></div>'
3047
+ + '<div class="placeholder">consultando opencode…</div>';
3048
+ loadMcp();
3049
+ }
3050
+
3051
+ async function loadMcp() {
3052
+ var res = await ocCommand('mcp_list', [], 15, 700);
3053
+ if (!els.viewMcp) return;
3054
+ var head = '<div class="view-head"><h2>servidores MCP</h2>'
3055
+ + '<div class="view-sub">opencode mcp list</div></div>';
3056
+ if (!res.ok) {
3057
+ els.viewMcp.innerHTML = head + '<div class="placeholder">' + esc(res.error || 'no se pudo consultar') + '</div>';
3058
+ return;
3059
+ }
3060
+ var output = (res.data && res.data.output) || '';
3061
+ els.viewMcp.innerHTML = head + '<pre class="codeblock">'
3062
+ + esc(output || 'No hay servidores MCP configurados.') + '</pre>';
3063
+ }
3064
+
3010
3065
  function chgStatusClass(st) {
3011
3066
  if (st === '??' || st === 'A') return 'add';
3012
3067
  if (st.indexOf('D') >= 0) return 'del';
@@ -3402,7 +3457,7 @@ function validPort(p) {
3402
3457
 
3403
3458
  function renderPreviewView() {
3404
3459
  if (!state.previewPort) {
3405
- try { state.previewPort = localStorage.getItem('ocx_lastPort') || ''; } catch (e) {}
3460
+ try { state.previewPort = localStorage.getItem('ob_lastPort') || ''; } catch (e) {}
3406
3461
  }
3407
3462
  els.viewPreview.innerHTML = '<div class="view-head"><h2>vista previa</h2>'
3408
3463
  + '<div class="view-sub">túneles del proyecto (TunnelMole, corren en tu PC)</div></div>'
@@ -3474,7 +3529,7 @@ function renderTunnels(busyMsg) {
3474
3529
  async function startTunnel() {
3475
3530
  var port = validPort(state.previewPort);
3476
3531
  if (!port) { toast('escribí un puerto válido (1–65535)', 'error'); return; }
3477
- try { localStorage.setItem('ocx_lastPort', String(port)); } catch (e) {}
3532
+ try { localStorage.setItem('ob_lastPort', String(port)); } catch (e) {}
3478
3533
  var already = (state.tunnels || []).some(function (t) { return parseInt(t.port, 10) === port && (t.https || t.http); });
3479
3534
  if (already) { toast('ya hay un túnel para el puerto ' + port, 'ok'); return; }
3480
3535
  renderTunnels('levantando túnel para el puerto ' + port + '… (la primera vez puede tardar si descarga tunnelmole)');
@@ -3965,7 +4020,7 @@ async function panelStartTunnel() {
3965
4020
  var inp = document.getElementById('ppTunPort');
3966
4021
  var port = validPort(inp ? inp.value : '');
3967
4022
  if (!port) { toast('escribí un puerto válido (1–65535)', 'error'); return; }
3968
- try { localStorage.setItem('ocx_lastPort', String(port)); } catch (e) {}
4023
+ try { localStorage.setItem('ob_lastPort', String(port)); } catch (e) {}
3969
4024
  var already = (state.tunnels || []).some(function (t) { return parseInt(t.port, 10) === port; });
3970
4025
  if (already) { toast('ya hay un túnel para el puerto ' + port, 'ok'); return; }
3971
4026
  renderProcTunnels('levantando túnel para el puerto ' + port + '… (la primera vez descarga tunnelmole)');
@@ -4139,7 +4194,7 @@ function refreshActiveView(reason) {
4139
4194
  async function boot() {
4140
4195
  var cachedCat = lsGet(catalogKey());
4141
4196
  var cachedVer = lsGet(catalogVerKey());
4142
- var cachedSessions = lsGet('ocx_sessions');
4197
+ var cachedSessions = lsGet('ob_sessions');
4143
4198
  var painted = false;
4144
4199
  if (cachedCat && typeof cachedCat === 'object') {
4145
4200
  state.catalog = cachedCat;
package/src/web/routes.js CHANGED
@@ -37,7 +37,7 @@ async function renderTpl(name, vars) {
37
37
  function pickTheme(req, query, known) {
38
38
  const cookies = auth.parseCookies(req.headers.cookie);
39
39
  const fromUrl = String(query.get('theme') || '').replace(/[^a-z-]/g, '').toLowerCase();
40
- const fromCookie = String(cookies['ocx_theme'] || '').replace(/[^a-z-]/g, '').toLowerCase();
40
+ const fromCookie = String(cookies['ob_theme'] || cookies['ocx_theme'] || '').replace(/[^a-z-]/g, '').toLowerCase();
41
41
  if (fromUrl && known.includes(fromUrl)) return fromUrl;
42
42
  if (fromCookie && known.includes(fromCookie)) return fromCookie;
43
43
  return 'terminal';
@@ -196,6 +196,7 @@ const OC_ALLOWED = {
196
196
  session_list: [],
197
197
  session_info: ['arg'],
198
198
  opencode_version: [],
199
+ mcp_list: [],
199
200
  fs_list: ['path'],
200
201
  fs_read: ['path'],
201
202
  git_status: ['path'],
@@ -1203,6 +1203,7 @@
1203
1203
  <button data-view="preview" type="button" title="Vista previa del proyecto (túnel)">preview</button>
1204
1204
  <button data-view="procs" type="button" title="Procesos de la sesión">procs</button>
1205
1205
  <button data-view="changes" type="button" title="Cambios del proyecto (git)">cambios</button>
1206
+ <button data-view="mcp" type="button" title="Servidores MCP de opencode">mcp</button>
1206
1207
  </div>
1207
1208
  <div class="footer">
1208
1209
  <div class="row" id="bridgeStatusRow">
@@ -1285,6 +1286,7 @@
1285
1286
  <div id="viewPreview" class="view" style="display:none"></div>
1286
1287
  <div id="viewProcs" class="view" style="display:none"></div>
1287
1288
  <div id="viewChanges" class="view" style="display:none"></div>
1289
+ <div id="viewMcp" class="view" style="display:none"></div>
1288
1290
 
1289
1291
  <footer id="statusbar">
1290
1292
  <span class="dot" id="sbDot"></span>
@@ -89,12 +89,12 @@
89
89
  (function () {
90
90
  var knownThemes = {{THEMES_JSON}};
91
91
  var params = new URLSearchParams(location.search);
92
- var cookie = document.cookie.match(/(?:^|;\s*)ocx_theme=([^;]+)/);
92
+ var cookie = document.cookie.match(/(?:^|;\s*)ob_theme=([^;]+)/) || document.cookie.match(/(?:^|;\s*)ocx_theme=([^;]+)/);
93
93
  var saved = (params.get('theme') || (cookie && decodeURIComponent(cookie[1])) || document.body.getAttribute('data-theme') || 'terminal');
94
94
  if (knownThemes.indexOf(saved) === -1) saved = 'terminal';
95
95
  document.body.setAttribute('data-theme', saved);
96
96
  document.getElementById('themeStylesheet').setAttribute('href', 'themes/' + saved + '.css');
97
- document.cookie = 'ocx_theme=' + encodeURIComponent(saved) + ';path=/;max-age=' + (60 * 60 * 24 * 365) + ';SameSite=Lax';
97
+ document.cookie = 'ob_theme=' + encodeURIComponent(saved) + ';path=/;max-age=' + (60 * 60 * 24 * 365) + ';SameSite=Lax';
98
98
  window.addEventListener('load', function () {
99
99
  var bg = getComputedStyle(document.documentElement).getPropertyValue('--bg').trim();
100
100
  if (bg) document.getElementById('themeColor').setAttribute('content', bg);