@danieltmn/openbridge 0.4.0 → 0.5.1
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 +33 -0
- package/README.md +13 -3
- package/docs/ARCHITECTURE.md +8 -4
- package/package.json +2 -1
- package/src/bridge/bridge.js +44 -5
- package/src/cli.js +99 -2
- package/src/web/assets/app.js +74 -19
- package/src/web/routes.js +2 -1
- package/src/web/templates/chat.html +2 -0
- package/src/web/templates/login.html +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ 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.1] - 2026-09-14
|
|
8
|
+
|
|
9
|
+
### Corregido
|
|
10
|
+
|
|
11
|
+
- `openbridge join` normaliza la URL del hub a `/api.php` (antes, pasar la base
|
|
12
|
+
o `/chat.php` apuntaba a `/` y el puente se quedaba sin trabajo).
|
|
13
|
+
|
|
14
|
+
## [0.5.0] - 2026-09-14
|
|
15
|
+
|
|
16
|
+
### Agregado
|
|
17
|
+
|
|
18
|
+
- Comando `openbridge update` (compara con npm; `--yes` actualiza la instalacion
|
|
19
|
+
global).
|
|
20
|
+
- Comando `openbridge join <url>` para sumar una PC como puente de un hub:
|
|
21
|
+
persiste `apiUrl`/`apiToken`/`bridgeId` y arranca el puente (`--no-start` solo
|
|
22
|
+
guarda).
|
|
23
|
+
- `npm run lint` (`scripts/lint.mjs`): `node --check` sobre todo el JS; corre en
|
|
24
|
+
CI.
|
|
25
|
+
- Test end-to-end del puente con `opencode` mockeado (`test/bridge.test.js`) y
|
|
26
|
+
tests del CLI (`test/cli.test.js`).
|
|
27
|
+
- **MCP**: vista en la web y comando de chat `/mcp` que muestran los servidores
|
|
28
|
+
MCP de opencode y su estado (`opencode mcp list`); comando de puente
|
|
29
|
+
`mcp_list`.
|
|
30
|
+
- `CONTRIBUTING.md`.
|
|
31
|
+
|
|
32
|
+
### Cambiado
|
|
33
|
+
|
|
34
|
+
- Claves internas `ocx_*` renombradas a `ob_*` con migracion automatica
|
|
35
|
+
(localStorage y cookie de tema). Se mantienen como compatibilidad el env
|
|
36
|
+
`OPENCONEX_HOME` y el bin `openconex` (deprecados).
|
|
37
|
+
- `config.command` puede apuntar a un script de Node (`.js`/`.mjs`/`.cjs`): se
|
|
38
|
+
ejecuta con el `node` actual (wrappers propios y pruebas con CLI simulado).
|
|
39
|
+
|
|
7
40
|
## [0.4.0] - 2026-09-14
|
|
8
41
|
|
|
9
42
|
### 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,12 @@ 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
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
openbridge join https://tu-url-publica --token <bridgeToken> --id pc2 --name "PC 2"
|
|
136
|
+
# acepta la base, /chat.php o /api.php; se normaliza a /api.php
|
|
137
|
+
# con --no-start solo guarda la config
|
|
136
138
|
```
|
|
139
|
+
(equivale a `openbridge bridge --api … --token … --id … --name …`, pero
|
|
140
|
+
persiste la config para no repetir flags)
|
|
137
141
|
4. Abrí la URL del hub desde el celular: en el sidebar aparecen **PC 1** y
|
|
138
142
|
**PC 2** para alternar.
|
|
139
143
|
|
|
@@ -164,6 +168,12 @@ suscripción no persiste entre reinicios (el push está atado al origen); la app
|
|
|
164
168
|
vuelve a registrar cuando la abrís en la URL nueva. Para que sea estable, usá una
|
|
165
169
|
URL fija (ngrok con `--domain`).
|
|
166
170
|
|
|
171
|
+
## MCP
|
|
172
|
+
|
|
173
|
+
La web tiene una vista **mcp** y el chat acepta `/mcp`: muestran los servidores
|
|
174
|
+
MCP de opencode y su estado (`opencode mcp list`). El comando corre en la PC del
|
|
175
|
+
puente, así que refleja la configuración de opencode de esa computadora.
|
|
176
|
+
|
|
167
177
|
## Seguridad
|
|
168
178
|
|
|
169
179
|
- La app escucha **solo en `127.0.0.1`**; el túnel la expone a internet.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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`
|
|
69
|
-
|
|
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.
|
|
3
|
+
"version": "0.5.1",
|
|
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"
|
package/src/bridge/bridge.js
CHANGED
|
@@ -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
|
|
475
|
+
const spec = commandSpec(args);
|
|
461
476
|
const timeoutMs = Math.max((opts && opts.timeout) || 0, 0);
|
|
462
|
-
const child = spawn(cmd,
|
|
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
|
|
529
|
+
const spec = commandSpec(args);
|
|
515
530
|
const timeoutMs = Math.max((opts && opts.timeout) || 0, 0);
|
|
516
|
-
const child = spawn(cmd,
|
|
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(), '
|
|
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.
|
|
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,52 @@ 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
|
+
// Normaliza la URL del hub a su endpoint de API: acepta la base, /chat.php o
|
|
628
|
+
// /api.php y devuelve siempre algo terminado en /api.php (lo que espera el
|
|
629
|
+
// puente). Sin esto, un `join https://host` apuntaba a / y no al API.
|
|
630
|
+
function hubApiUrl(raw) {
|
|
631
|
+
let u = String(raw || '').trim().replace(/\/+$/, '');
|
|
632
|
+
u = u.replace(/\/chat\.php$/i, '');
|
|
633
|
+
if (!/\/api\.php$/i.test(u)) u += '/api.php';
|
|
634
|
+
return u;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function cmdJoin(argv) {
|
|
638
|
+
const { flags, _ } = parseArgs(argv);
|
|
639
|
+
const url = String(_[0] || flags.api || '').trim();
|
|
640
|
+
if (!url || !/^https?:\/\//i.test(url)) {
|
|
641
|
+
console.error('Uso: openbridge join <url-del-hub> [--token <t>] [--id <pc>] [--name "<nombre>"] [--no-start]');
|
|
642
|
+
console.error('Ej.: openbridge join https://mi-pc.trycloudflare.com --token <t> --id pc2 --name "PC oficina"');
|
|
643
|
+
console.error('(acepta la base, /chat.php o /api.php; se normaliza a /api.php)');
|
|
644
|
+
return 1;
|
|
645
|
+
}
|
|
646
|
+
paths.ensureDirs();
|
|
647
|
+
const cfg = config.readBridge();
|
|
648
|
+
cfg.apiUrl = hubApiUrl(url);
|
|
649
|
+
if (typeof flags.token === 'string') cfg.apiToken = flags.token;
|
|
650
|
+
if (typeof flags.id === 'string') cfg.bridgeId = sanitizeId(flags.id);
|
|
651
|
+
else if (!cfg.bridgeId) cfg.bridgeId = sanitizeId(os.hostname());
|
|
652
|
+
if (typeof flags.name === 'string' && flags.name.trim()) cfg.bridgeName = flags.name.trim().slice(0, 40);
|
|
653
|
+
else if (!cfg.bridgeName) cfg.bridgeName = cfg.bridgeId;
|
|
654
|
+
config.writeBridge(cfg);
|
|
655
|
+
|
|
656
|
+
console.log('Puente vinculado al hub: ' + cfg.apiUrl);
|
|
657
|
+
console.log(' id : ' + cfg.bridgeId);
|
|
658
|
+
console.log(' nombre : ' + cfg.bridgeName);
|
|
659
|
+
if (!cfg.apiToken) console.log('aviso: sin token (--token). Solo sirve si el hub no exige token del puente.');
|
|
660
|
+
if (flags['no-start']) {
|
|
661
|
+
console.log('Arrancalo cuando quieras con: openbridge bridge');
|
|
662
|
+
return 0;
|
|
663
|
+
}
|
|
664
|
+
console.log('');
|
|
665
|
+
console.log('Arrancando el puente (Ctrl+C para salir)...');
|
|
666
|
+
return cmdBridge([]);
|
|
667
|
+
}
|
|
668
|
+
|
|
623
669
|
// ---------------------------------------------------------------------------
|
|
624
670
|
// import / reset (datos)
|
|
625
671
|
// ---------------------------------------------------------------------------
|
|
@@ -915,6 +961,53 @@ async function cmdDoctor() {
|
|
|
915
961
|
return ok ? 0 : 1;
|
|
916
962
|
}
|
|
917
963
|
|
|
964
|
+
// ---------------------------------------------------------------------------
|
|
965
|
+
// update: compara con npm y, con --yes, actualiza la instalacion global.
|
|
966
|
+
// ---------------------------------------------------------------------------
|
|
967
|
+
function npmRun(args) {
|
|
968
|
+
return spawnSync('npm', args, { encoding: 'utf8', shell: process.platform === 'win32', windowsHide: true });
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async function cmdUpdate(argv) {
|
|
972
|
+
const { flags } = parseArgs(argv);
|
|
973
|
+
const tag = (typeof flags.tag === 'string' && flags.tag) ? flags.tag : 'latest';
|
|
974
|
+
const pkg = '@danieltmn/openbridge';
|
|
975
|
+
const spec = pkg + '@' + tag;
|
|
976
|
+
console.log('Version actual: ' + VERSION);
|
|
977
|
+
|
|
978
|
+
const view = npmRun(['view', spec, 'version', '--json']);
|
|
979
|
+
let latest = '';
|
|
980
|
+
try { latest = String(JSON.parse(view.stdout)).trim(); } catch (e) { latest = String(view.stdout || '').trim().replace(/"/g, ''); }
|
|
981
|
+
if (view.status !== 0 || !/^\d+\.\d+\.\d+/.test(latest)) {
|
|
982
|
+
console.error('No se pudo consultar npm (' + tag + '): ' + ((view.stderr || '').trim() || 'revisa tu conexion'));
|
|
983
|
+
return 1;
|
|
984
|
+
}
|
|
985
|
+
console.log('Disponible (' + tag + '): ' + latest);
|
|
986
|
+
if (latest === VERSION) {
|
|
987
|
+
console.log('Ya estas en la ultima version.');
|
|
988
|
+
return 0;
|
|
989
|
+
}
|
|
990
|
+
if (flags.check) {
|
|
991
|
+
console.log('Hay una version nueva. Corre: openbridge update --yes');
|
|
992
|
+
return 0;
|
|
993
|
+
}
|
|
994
|
+
if (!flags.yes && !flags.y) {
|
|
995
|
+
console.log('');
|
|
996
|
+
console.log('Para actualizar ahora:');
|
|
997
|
+
console.log(' openbridge update --yes (o) npm i -g ' + spec);
|
|
998
|
+
return 0;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
console.log('Actualizando a ' + spec + '...');
|
|
1002
|
+
const r = spawnSync('npm', ['i', '-g', spec], { stdio: 'inherit', shell: process.platform === 'win32', windowsHide: true });
|
|
1003
|
+
if (r.status !== 0) {
|
|
1004
|
+
console.error('La actualizacion fallo. Proba a mano: npm i -g ' + spec);
|
|
1005
|
+
return 1;
|
|
1006
|
+
}
|
|
1007
|
+
console.log('Listo. Reinicia el server para usar la version nueva: openbridge stop && openbridge server');
|
|
1008
|
+
return 0;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
918
1011
|
// ---------------------------------------------------------------------------
|
|
919
1012
|
function usage() {
|
|
920
1013
|
console.log('OpenBridge ' + VERSION + ' — tu opencode en el celular, sin hosting');
|
|
@@ -931,10 +1024,12 @@ function usage() {
|
|
|
931
1024
|
console.log(' tunnel Muestra o cambia el proveedor de tunel (tunnelmole|ngrok|cloudflare|none) [--domain]');
|
|
932
1025
|
console.log(' logs Ultimas lineas de los logs (--follow --server --bridge)');
|
|
933
1026
|
console.log(' bridge Corre solo el puente (--api --token --id --name)');
|
|
1027
|
+
console.log(' join Vincula esta PC como puente de un hub (<url> --token --id --name)');
|
|
934
1028
|
console.log(' import Trae data/ de OpenConex (<data-dir> [--force])');
|
|
935
1029
|
console.log(' reset Borra todos los chats/datos (--session <id> --yes)');
|
|
936
1030
|
console.log(' autostart Instala/quita el arranque automatico (install|remove)');
|
|
937
1031
|
console.log(' doctor Verifica Node, opencode, configuracion y puerto');
|
|
1032
|
+
console.log(' update Busca una version nueva en npm (--yes para actualizar)');
|
|
938
1033
|
console.log('');
|
|
939
1034
|
console.log('Opciones comunes: --dir <ruta> (casa portable; default: directorio actual)');
|
|
940
1035
|
console.log('init: --workspace --name --id --port --password --tunnel --domain --yes --force');
|
|
@@ -964,10 +1059,12 @@ async function main(argv) {
|
|
|
964
1059
|
case 'tunnel': return cmdTunnel(args.slice(1));
|
|
965
1060
|
case 'logs': return cmdLogs(args.slice(1));
|
|
966
1061
|
case 'bridge': return cmdBridge(args.slice(1));
|
|
1062
|
+
case 'join': return cmdJoin(args.slice(1));
|
|
967
1063
|
case 'import': return cmdImport(args.slice(1));
|
|
968
1064
|
case 'reset': return cmdReset(args.slice(1));
|
|
969
1065
|
case 'autostart': return cmdAutostart(args.slice(1));
|
|
970
1066
|
case 'doctor': return cmdDoctor();
|
|
1067
|
+
case 'update': return cmdUpdate(args.slice(1));
|
|
971
1068
|
case 'version': case '-v': case '--version': console.log(VERSION); return 0;
|
|
972
1069
|
case 'help': case '-h': case '--help': usage(); return 0;
|
|
973
1070
|
default:
|
package/src/web/assets/app.js
CHANGED
|
@@ -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('
|
|
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 = '
|
|
275
|
-
localStorage.setItem('
|
|
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 ? '
|
|
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('
|
|
356
|
+
try { return localStorage.getItem('ob_bridge') || ''; } catch (e) { return ''; }
|
|
339
357
|
}
|
|
340
358
|
function storeBridge(id) {
|
|
341
|
-
try { localStorage.setItem('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
1159
|
+
try { return localStorage.getItem('ob_sbHidden') === '1'; } catch (e) { return false; }
|
|
1135
1160
|
}
|
|
1136
1161
|
function setSbHidden(hidden) {
|
|
1137
|
-
try { localStorage.setItem('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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 = '
|
|
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);
|