specrails-desktop 2.5.0 → 2.7.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.
Files changed (76) hide show
  1. package/client/dist/assets/{ActivityFeedPage-BTYWMRwB.js → ActivityFeedPage-LKqd18-G.js} +1 -1
  2. package/client/dist/assets/{AgentsPage-BfOCeHHt.js → AgentsPage-Cb-b-6Ot.js} +1 -1
  3. package/client/dist/assets/AnalyticsPage-HVxQQ1wy.js +1 -0
  4. package/client/dist/assets/{BarChart-DlshJN3Z.js → BarChart-BOyHB0dw.js} +1 -1
  5. package/client/dist/assets/{CodePage-DJCjDG4I.js → CodePage-DnOnwKGB.js} +1 -1
  6. package/client/dist/assets/{DesktopAnalyticsPage-CTqZ9mbB.js → DesktopAnalyticsPage-D2auU39x.js} +1 -1
  7. package/client/dist/assets/{DocsDialog-KiJOSRvX.js → DocsDialog-CTuDX3GK.js} +1 -1
  8. package/client/dist/assets/{DocsPage-B17CR54A.js → DocsPage-DRyMmu0Z.js} +1 -1
  9. package/client/dist/assets/{ExportDropdown-BAu6z3b6.js → ExportDropdown-DO-GGiMh.js} +1 -1
  10. package/client/dist/assets/{IntegrationsPage-CCG64Q-6.js → IntegrationsPage-BhbO4jFT.js} +1 -1
  11. package/client/dist/assets/{JobDetailPage-BnGJSMiS.js → JobDetailPage-DJooEg1s.js} +1 -1
  12. package/client/dist/assets/{JobsPage-B-tn4CIf.js → JobsPage-BbaC-YOg.js} +1 -1
  13. package/client/dist/assets/{addspec-DeDOztDr.js → addspec-B-BKlvDj.js} +1 -1
  14. package/client/dist/assets/{addspec-v8j6A7CD.js → addspec-BErjOdNK.js} +1 -1
  15. package/client/dist/assets/{addspec-B1FTtI2a.js → addspec-CIGb34PS.js} +1 -1
  16. package/client/dist/assets/{addspec-GWm4ffKl.js → addspec-C_3NBarY.js} +1 -1
  17. package/client/dist/assets/{addspec-Dw-0Dg-4.js → addspec-DDvvnE6N.js} +1 -1
  18. package/client/dist/assets/{addspec-DpRgmfmx.js → addspec-RuL8Zd7w.js} +1 -1
  19. package/client/dist/assets/{addspec-rp496P_F.js → addspec-rmhOaH7N.js} +1 -1
  20. package/client/dist/assets/{addspec-BCT9vm_c.js → addspec-xjDbYZWL.js} +1 -1
  21. package/client/dist/assets/{dist-js-B16c3VyT.js → dist-js-CiIVMsx3.js} +1 -1
  22. package/client/dist/assets/{dist-js-P2FkJ6fA.js → dist-js-Xc2lRKp2.js} +1 -1
  23. package/client/dist/assets/{index-AfVF6BgE.js → index-DK214dak.js} +45 -45
  24. package/client/dist/assets/index-DgKfQFcf.css +2 -0
  25. package/client/dist/assets/{lib-rNNmltMb.js → lib-Bo5s6xpe.js} +1 -1
  26. package/client/dist/assets/{settings-D3LurcR5.js → settings-BI_cVCqN.js} +1 -1
  27. package/client/dist/assets/{settings-5tzo0Rn3.js → settings-BRaLLSVi.js} +1 -1
  28. package/client/dist/assets/{settings-BEWv3VEu.js → settings-BcqH0oea.js} +1 -1
  29. package/client/dist/assets/settings-C0-7Fpxg.js +1 -0
  30. package/client/dist/assets/{settings-BORg56um.js → settings-D6QMBlGQ.js} +1 -1
  31. package/client/dist/assets/{settings-DcqWIEM6.js → settings-GOBKOTGl.js} +1 -1
  32. package/client/dist/assets/{settings-BDAW3trC.js → settings-pT3MzfRu.js} +1 -1
  33. package/client/dist/assets/{settings-Dfz8QbZS.js → settings-u-16ISHt.js} +1 -1
  34. package/client/dist/assets/{setup-D3rNZA9A.js → setup-BIIkb-_K.js} +1 -1
  35. package/client/dist/assets/{setup-C1IA-9YS.js → setup-BeQxu9kD.js} +1 -1
  36. package/client/dist/assets/{setup-pjgmYHx6.js → setup-CPa6GnlI.js} +1 -1
  37. package/client/dist/assets/{setup-gzLG8T6F.js → setup-CZl4OEJx.js} +1 -1
  38. package/client/dist/assets/{setup-C0dzw8j4.js → setup-ChpodNfn.js} +1 -1
  39. package/client/dist/assets/{setup-WP6WOYQh.js → setup-D_fjJH6u.js} +1 -1
  40. package/client/dist/assets/{setup-UD2aanGs.js → setup-YzD8DX4O.js} +1 -1
  41. package/client/dist/assets/{setup-CpfjaNut.js → setup-fRpDozmq.js} +1 -1
  42. package/client/dist/assets/{useProjectCache-Cid_GxRM.js → useProjectCache-DVNypkmR.js} +1 -1
  43. package/client/dist/index.html +5 -5
  44. package/docs/adding-a-provider.md +107 -0
  45. package/docs/agy-cli-provider-study.md +179 -0
  46. package/docs/gemini-cli-provider-study.md +301 -0
  47. package/docs/gemini-core-support-evaluation.md +160 -0
  48. package/docs/gemini.md +106 -0
  49. package/docs/internals/api-reference.md +4 -7
  50. package/package.json +2 -2
  51. package/server/dist/chat-manager.js +1 -1
  52. package/server/dist/core-package.js +6 -1
  53. package/server/dist/desktop-router.js +27 -8
  54. package/server/dist/explore-cwd-manager.js +1 -1
  55. package/server/dist/mobile/index.js +5 -5
  56. package/server/dist/mobile/mobile-admin-router.js +28 -35
  57. package/server/dist/mobile/mobile-datachannel.js +228 -0
  58. package/server/dist/mobile/mobile-gateway.js +72 -98
  59. package/server/dist/mobile/mobile-router.js +4 -35
  60. package/server/dist/mobile/mobile-signal-reconnect.js +84 -0
  61. package/server/dist/mobile/mobile-types.js +5 -5
  62. package/server/dist/mobile/mobile-webrtc-peer.js +129 -0
  63. package/server/dist/mobile/mobile-webrtc.js +117 -0
  64. package/server/dist/pricing.js +13 -0
  65. package/server/dist/project-router-tickets.js +63 -18
  66. package/server/dist/providers/gemini-adapter.js +234 -0
  67. package/server/dist/providers/index.js +4 -1
  68. package/server/dist/setup-manager.js +13 -7
  69. package/server/dist/setup-prerequisites.js +4 -0
  70. package/server/dist/spec-models.js +17 -3
  71. package/server/dist/util/cli-prompt.js +17 -1
  72. package/client/dist/assets/AnalyticsPage-AbVXKh9v.js +0 -1
  73. package/client/dist/assets/index-NlH5BbXJ.css +0 -2
  74. package/client/dist/assets/settings-yMubjqYw.js +0 -1
  75. package/server/dist/mobile/mobile-mdns.js +0 -81
  76. package/server/dist/mobile/mobile-pairing.js +0 -179
@@ -0,0 +1,160 @@
1
+ > Documento de planificación. Generado 2026-06-17 mediante auditoría multi-agente del repo real `/Users/javi/repos/specrails-core` + verificación de fuentes primarias de gemini-cli. Complementa `docs/gemini-cli-provider-study.md` (desktop) — esta evalúa el trabajo en **specrails-core** para habilitar rails/implement en Gemini.
2
+
3
+ ---
4
+
5
+ # Evaluación: ¿Hay que tocar specrails-core para Gemini CLI?
6
+
7
+ > **Veredicto en una línea:** Sí, **obligatoriamente hay que tocar core** para que `implement`/`batch-implement` (rails) funcionen en Gemini. El desktop por sí solo (PR-A/PR-B + `gemini-adapter.ts`) cubre **solo** spec/explore/quick. El pipeline architect→developer→reviewer **es expresable sin rediseño de fases** en el modelo plano de Gemini, pero **sin un target gemini en core no hay agentes ni comandos ni skills que ejecutar**, así que los rails en gemini hoy son inejecutables.
8
+
9
+ ---
10
+
11
+ ## 1. Respuesta directa: ¿hay que tocar core?
12
+
13
+ **Sí, para los rails. No, para spec/explore/quick.** El corte es exacto y limpio:
14
+
15
+ ### Ya funciona HOY sin tocar core (solo desktop adapter)
16
+ El desktop tiene `server/providers/gemini-adapter.ts` registrado (`server/providers/index.ts:12,16`), con `projectDirName: '.gemini'`, `instructionsFilename: 'GEMINI.md'`, `nativeCostUsd: false` (gemini-adapter.ts:225-235). Esto cubre las superficies que **NO dependen de artefactos instalados en el repo**:
17
+ - **Explore Spec** — spawnea gemini desde `explore-cwd/` con un `CLAUDE.md`/system-prompt embebido por el desktop; no lee `.gemini/*`.
18
+ - **Quick spec** (`POST /tickets/generate-spec`) — turno suelto, system prompt inyectado por el desktop.
19
+ - **Sidebar chat** — idem.
20
+
21
+ Estas tres superficies inyectan su propio prompt y no resuelven slash-commands ni subagentes del proyecto. Por eso el adapter desktop basta.
22
+
23
+ ### EXIGE core (sin esto, los rails en gemini no arrancan)
24
+ `implement` y `batch-implement` **no son prompts** que el desktop inyecte: son **artefactos instalados en el repo** que el orquestador resuelve nativamente. El desktop solo pasa el comando resuelto al binario (`queue-manager.ts` `buildArgs('rail-job', …)`), y el binario espera encontrar en disco:
25
+ 1. **Comandos** `implement` / `batch-implement` (en formato gemini: `.gemini/commands/*.toml`).
26
+ 2. **Agentes** `sr-architect`/`sr-developer`/`sr-reviewer` (en `.gemini/agents/*.md` con frontmatter).
27
+ 3. **Skills/comandos OpenSpec** (`opsx:*`) instalados por `openspec init --tools gemini`.
28
+ 4. **`GEMINI.md`** (instrucciones de proyecto) + settings.
29
+
30
+ **Core no emite NADA de esto para gemini** — verificado: `grep -rni gemini` sobre `src/`, `templates/`, `integration-contract.json` devuelve **0 matches**. El tipo `Provider` es un set cerrado `'claude' | 'codex'` que **rechaza** cualquier otro valor en `install-config.ts:96` (`unsupported provider '...'`). Por tanto, hoy un proyecto gemini no puede ni siquiera instalarse: el handshake desktop→core (desktop escribe `provider:` en `install-config.yaml`, `setup-manager.ts:906`; core valida en `install-config.ts:96` e `init.ts:86`) **falla en duro** con "unsupported provider 'gemini'".
31
+
32
+ **Conclusión inequívoca:** spec/explore/quick = solo desktop, ya hecho. Rails (implement/batch-implement + agentes + skills + OpenSpec) = **bloqueado en core**, requiere un target gemini.
33
+
34
+ ---
35
+
36
+ ## 2. Cómo emite core hoy por proveedor
37
+
38
+ El flujo real de instalación: `init.ts` → `provider-detect.derivedPaths(provider)` → `scaffold.scaffoldInstallation` → `installOpenSpecProject`.
39
+
40
+ ### Detección/selección de proveedor (todo cerrado a 2 literales)
41
+ - **Tipo:** `Provider = 'claude' | 'codex'` declarado en DOS sitios que deben coincidir: `provider-detect.ts:15` e `install-config.ts:13`.
42
+ - **Detección:** `detectAvailability()` solo sondea claude/codex en PATH — verificado verbatim:
43
+ ```ts
44
+ const [claude, codex] = await Promise.all([commandExists('claude'), commandExists('codex')])
45
+ return { claude, codex }
46
+ ```
47
+ (`provider-detect.ts:33-36`).
48
+ - **Validación dura:** `install-config.ts:96` rechaza ≠ claude/codex; `init.ts:86` rechaza el flag `--provider`; `bin/tui-installer.mjs:114/118`.
49
+
50
+ ### Dónde divergen claude vs codex
51
+ Toda la divergencia es **imperativa, `if (input.provider === 'codex')`**, no hay registry/adapter (contraste con el `ProviderAdapter` del desktop). Sitios clave en `scaffold.ts`:
52
+ - **Convención de paths** centralizada en `derivedPaths` — verificado verbatim: codex → `{ '.codex', 'AGENTS.md' }`, else → `{ '.claude', 'CLAUDE.md' }` (`provider-detect.ts:87-92`).
53
+ - Esqueleto de directorios (`scaffold.ts:174-184`): claude `.claude/commands/specrails` + `.claude/skills`; codex `.codex/skills/{enrich,doctor,rails}`.
54
+ - Colocación de comandos (`copyBundledCommands`, `scaffold.ts:278-311`): claude copia `.md` verbatim; codex porta cada comando a `.codex/skills/<name>/SKILL.md` vía `writeCodexSkillFromCommand` (rewrite `.claude/`→`.codex/`, `/specrails:x`→`$x`) salvo que exista override en `templates/codex-skills/<name>/`.
55
+ - Agentes (`scaffold.ts:493-648`): claude coloca `.claude/agents/sr-*.md` + memory dirs; codex **salta agentes** y usa rail-skills en `.codex/skills/rails/`.
56
+ - Skills (`placeSkills`, `scaffold.ts:762-830`): claude **genera** `sr-*` skills desde el cuerpo del comando (`SKILL_FROM_COMMAND`, `scaffold.ts:46-82` + `writeClaudeSkillFromCommand`); codex copia `templates/codex-skills/rails/`.
57
+ - Settings (`scaffold.ts:244-250`): codex-only `applyCodexSettings` escribe `.codex/config.toml` + `AGENTS.md` con bloque sentinel-managed (`renderInitialAgentsMd`, `scaffold.ts:666-747`). Claude **no** tiene settings-writer aquí (su `CLAUDE.md` no lo escribe el instalador, solo lo chequea `doctor.ts:91`).
58
+
59
+ ### Cómo se instala OpenSpec (esto SÍ es ya per-provider)
60
+ `installOpenSpecProject(repoRoot, provider)` (llamado solo en `init.ts:128`, **no** en update) shellea — verificado verbatim:
61
+ ```ts
62
+ args: ['--yes', '-p', `@fission-ai/openspec@${pinnedVersion}`, '--',
63
+ 'openspec', 'init', '--tools', provider, repoRoot]
64
+ ```
65
+ (`init.ts:204-218`, pin `1.3.1` desde `pinned-versions.json:3`). **Pasa `provider` DIRECTO a `--tools`.** Esto significa que la capa OpenSpec **no es el bloqueante**: `openspec init --tools gemini` es nativamente soportado por openspec 1.3.1 (genera `.gemini/commands/opsx` + `.gemini/skills/openspec-*` + el dir agnóstico `openspec/{specs,changes}`). En cuanto core propague el literal `gemini` a esta llamada (pasos 1+3 abajo), OpenSpec hace lo correcto **sin cambios de código**.
66
+
67
+ ---
68
+
69
+ ## 3. El target gemini en core: qué emitir, transform vs autoría nueva, additividad
70
+
71
+ ### Qué debe emitir
72
+ | Artefacto | Formato gemini | Origen |
73
+ |---|---|---|
74
+ | `implement`, `batch-implement` (+ resto comandos) | `.gemini/commands/specrails/*.toml` — **solo `prompt` + `description`**, sin `tools`/`model` | **Transform** de `templates/commands/specrails/*.md` (reusar el cuerpo, re-envolver en TOML) |
75
+ | `sr-architect/developer/reviewer` (+ opcionales) | `.gemini/agents/sr-*.md` con **YAML frontmatter** que SÍ lleva `model` + `tools` | **Transform** de `templates/agents/sr-*.md` (markdown ya compatible; ajustar frontmatter + `MEMORY_PATH`→`.gemini/agent-memory/`) |
76
+ | `GEMINI.md` + settings | `GEMINI.md` con bloque sentinel + `.gemini/settings.json` (con `experimental.enableAgents`) | **Autoría nueva** — clon de `applyCodexSettings`/`renderInitialAgentsMd` (`scaffold.ts:666-747`) |
77
+ | OpenSpec (`opsx:*`) | `.gemini/commands/opsx/*` + `.gemini/skills/openspec-*` | **Sin cambios** — lo emite `openspec init --tools gemini` |
78
+
79
+ ### Transform vs autoría — la asimetría es **decisiva** y la dicta el formato gemini
80
+ El veredicto verificado fija la regla: **commands TOML solo aceptan `prompt`+`description`** (sin `tools`/`model` por comando), pero **subagents `.md` SÍ aceptan `tools`+`model` en frontmatter**. Consecuencia de arquitectura para el scaffold:
81
+
82
+ > El **model-routing y el tool-gating** que hoy viven embebidos en `implement.md` (bloque ORCHESTRATOR_MODEL ~líneas 160-171 + overrides por agente) **deben migrar al frontmatter de los `.gemini/agents/sr-*.md`**, NO al comando TOML.
83
+
84
+ Mapeo correcto: orquestador → `.gemini/commands/specrails/{implement,batch-implement}.toml` (solo prompt+description); roles → `.gemini/agents/sr-*.md` (con `model:`, `tools:`). Esto **espeja el split de codex** (comando-vs-skill) pero con el primitivo de subagente nativo de gemini en vez de `spawn_agent`.
85
+
86
+ **Neto:** es una **mezcla** — cuerpos de comandos y agentes son **transforms mecánicos** de los templates claude existentes; el **emisor TOML** (`writeGeminiCommandFromCommand`, análogo a `writeCodexSkillFromCommand`), el **escritor `GEMINI.md`/settings**, y el **plumbing de tipo/detección** son **autoría nueva**. Lo más limpio: añadir `templates/gemini-skills/` (o `templates/gemini-*`) como dir de override para lo que el transform mecánico produzca mal, igual que `templates/codex-skills/`.
87
+
88
+ ### Cómo se añade de forma aditiva sin romper claude/codex
89
+ Es aditivo en **semántica** (cada branch es `=== 'codex'` / else-claude; añadir un brazo `=== 'gemini'` deja los paths existentes byte-idénticos; el allow-list de validación solo se ensancha, nunca rechaza configs previas válidas), pero **NO aditivo en código** (no hay abstracción → hay que editar cada sitio). Cambios concretos (~7 archivos + nuevo árbol de templates):
90
+ 1. Ensanchar `Provider` en `provider-detect.ts:15` **y** `install-config.ts:13` (deben quedar idénticos); relajar validadores `init.ts:86`, `install-config.ts:96`, `bin/tui-installer.mjs:114/118`.
91
+ 2. `detectAvailability` (`provider-detect.ts:33-36`): añadir `commandExists('gemini')`; extender `resolveProvider` (61-80) + el mensaje de error que hoy solo nombra Claude+Codex.
92
+ 3. `derivedPaths` (`provider-detect.ts:87-92`): brazo gemini → `{ '.gemini', 'GEMINI.md' }`.
93
+ 4. `scaffold.ts`: brazos gemini en cada `=== 'codex'` (dir skeleton 174-184, colocación comandos 278-325, quick-tier 493-648, `placeSkills` 762-830, prune 454-462) + `applyGeminiSettings` clon de `applyCodexSettings`.
94
+ 5. `prereqs.ts:91-104`: gatear el bloque claude-only o dar a gemini un path mínimo (sin auth-assert, como codex).
95
+ 6. `doctor.ts:96/160/167` + `update.ts:196-210` (`resolveExistingProvider` probe `.gemini`).
96
+ 7. `integration-contract.json:3-26`: bloque `providers.gemini` (la superficie casi-declarativa que lee el desktop; la de menor fricción).
97
+ 8. OpenSpec: **cero cambios de código** más allá de propagar `gemini` (pasos 1+3 → ya llega a `init.ts:215`).
98
+
99
+ ---
100
+
101
+ ## 4. El bloqueante de orquestación: ¿plano de gemini lo soporta?
102
+
103
+ **Veredicto honesto (verificado contra fuentes Google primarias): el pipeline ES expresable en el modelo plano de gemini SIN rediseño de fases.** No es un blocker de capacidad; es trabajo imperativo en core.
104
+
105
+ Razonamiento (todo confirmado):
106
+ - El pipeline es **recursivo pero SHALLOW**: el orquestador spawnea architect→developer→reviewer a **profundidad 1**; las hojas **nunca re-spawnean** (un developer no llama a un reviewer; lo único "hacia abajo" es un `Skill("opsx:ff/apply/archive")` **in-context**, que shellea al CLI `openspec`, **no es un subagente**). Contrato verbatim: `implement.md:5` "delegate to the agents"; `implement.md:547` spawn de sr-architect; codex `implement/SKILL.md:24` "Each phase MUST be a real spawn_agent call".
107
+ - El modelo de gemini es exactamente eso: orquestador delega depth-1, subagentes **FLAT** (`docs/core/subagents.md` verbatim: "subagents cannot call other subagents"). La restricción plana (las hojas no spawnean) **nunca se viola** porque todo el spawning requerido ocurre en el orquestador a depth-1.
108
+ - **El único rediseño** es `batch-implement`, que conceptualmente anida (batch→implement→roles = depth 2), ilegal en plano. **Pero el proyecto YA lo resolvió para codex**: `codex-skills/batch-implement/SKILL.md:5` "Drives architect/developer/reviewer spawns at the ROOT agent level — does NOT spawn a nested $implement sub-agent per ticket". El batch-implement gemini **reutiliza esa misma disciplina de aplanamiento ya probada**.
109
+
110
+ **Por tanto: NO hay rediseño del pipeline de fases. La viabilidad de rails en gemini está gateada por el trabajo imperativo en core, no por un gap de capacidad de gemini.**
111
+
112
+ Matiz honesto (no inventado): subagents en gemini son **experimentales** (namespace `experimental.enableAgents`, PR #14371, públicos desde v0.38.1 — ahora habilitados por defecto pero aún bajo `experimental.*`). Y queda un **unknown externo** que estos repos no pueden resolver: si el orquestador gemini puede **iniciar** spawns en headless/non-interactivo o si la delegación `@name` está garantizada sin TTY (ver riesgos §6).
113
+
114
+ ---
115
+
116
+ ## 5. Esfuerzo + secuenciación + versión + reutilización agy/Antigravity
117
+
118
+ **Tamaño del trabajo en core:** moderado, no trivial. ~7 archivos editados (`provider-detect`, `install-config`, `scaffold`, `prereqs`, `doctor`, `init`, `update`, `bin/tui-installer.mjs`) + `integration-contract.json` + **un árbol nuevo `templates/gemini-skills/`** (o `gemini-*`) + tests (incluido el contrato `reserved-paths.test.ts`, que debe seguir verde para init+update gemini). La falta de abstracción installer-side significa **N ediciones inline, no un descriptor**. La mayor parte del esfuerzo de autoría es: el emisor TOML, el escritor `GEMINI.md`/settings, y **validar empíricamente** que la orquestación plana de gemini ejecuta las fases en headless.
119
+
120
+ **Secuenciación con el beta desktop:**
121
+ 1. **PR-A #396** (generaliza tipos) y **PR-B #397** (adapter beta-gated) habilitan spec/explore/quick en gemini — **independientes de core**, se mergean primero.
122
+ 2. El target gemini en core es **secuencialmente posterior** y desbloquea rails. Mientras tanto, el desktop debería **ocultar rails/implement para gemini** (capability-intersection: igual que codex fuerza `profile=null` y oculta Agents/Integrations) hasta que core publique el target.
123
+
124
+ **Versión de core:** feature aditiva → conventional-commit `feat:` → release-please **MINOR**. Desde 4.7.1 actual → **4.8.0**. El profile-gate `>= 4.1.0` (`queue-manager.ts:77`, `profiles-router.ts:233`) **no se toca**: proyectos gemini en 4.8.0 ya lo satisfacen. Que gemini **participe** en profiles es una decisión de política del desktop (probablemente como codex: `profile=null`), no un cambio de gate.
125
+
126
+ **¿Puramente aditivo?** En contrato/runtime: **sí** (no rompe claude/codex). En código: **no** (cada branch imperativo gana un brazo gemini). Reserved-paths sin cambios.
127
+
128
+ **Reutilización agy/Antigravity (no es trabajo tirado):** Antigravity comparte el harness `~/.gemini` (mismas convenciones `.gemini/commands/*.toml`, `.gemini/agents/*.md`, `GEMINI.md`). Un target gemini en core que emita en formato `.gemini/*` **sirve de base directa para agy** más adelante — el scaffold gemini es reutilizable casi tal cual. Esto sube el ROI del trabajo de core.
129
+
130
+ ---
131
+
132
+ ## 6. Recomendación
133
+
134
+ ### Decisión: **HACER, pero DESPUÉS del beta desktop de spec/explore/quick (no en paralelo, no bloqueante del beta).**
135
+
136
+ Justificación: el beta desktop (PR-A/PR-B) entrega valor real de gemini (spec/explore/quick) **sin tocar core** y sin riesgo. Los rails en gemini son un trabajo de core mayor, con un unknown empírico (headless), y **no deben retrasar el beta**. El desktop oculta rails para gemini mientras tanto.
137
+
138
+ ### Ownership
139
+ - **Desktop owna:** el adapter gemini (ya hecho), capability-intersection para ocultar rails/Agents/Integrations en gemini hasta que core publique, command-syntax translation en `queue-manager` (hoy claude=verbatim, codex=`/x`→`$x`; gemini necesita su forma), y `profile=null` para rails gemini.
140
+ - **Core owna:** el target de instalación gemini completo (tipos/detección, `.gemini/commands/*.toml`, `.gemini/agents/sr-*.md`, `GEMINI.md`/settings, `templates/gemini-skills/`, `integration-contract.json` gemini block, propagar `gemini` a `openspec init --tools`), y los tests.
141
+
142
+ ### Primer paso concreto en core
143
+ Un **spike de validación de orquestación**, ANTES de escribir el scaffold completo: instalar manualmente a mano un `.gemini/commands/specrails/implement.toml` (prompt mínimo) + `.gemini/agents/sr-{architect,developer,reviewer}.md` (frontmatter con `model`/`tools`) + `enableAgents`, correr `openspec init --tools gemini`, y ejecutar el binario `gemini` en modo headless/exec (como lo spawnearía el desktop) para **confirmar empíricamente** que el orquestador inicia los spawns depth-1 y completa architect→developer→reviewer sin TTY. Si pasa, se procede al scaffold; si no, el riesgo se materializa antes de invertir en los 7 archivos.
144
+
145
+ ### Riesgos honestos
146
+ 1. **TOML sin `tools`/`model` por comando** — confirmado: los `.gemini/commands/*.toml` no pueden restringir tools ni fijar modelo. El routing/gating **debe** vivir en el frontmatter de los `.gemini/agents/*.md`. Si algún comando dependía de gating a nivel comando, hay que re-arquitecturarlo a nivel agente.
147
+ 2. **Subagentes experimentales** — `experimental.enableAgents`; comportamiento puede cambiar entre versiones de gemini-cli. Pinear/probar contra una versión concreta.
148
+ 3. **Shell-injection con confirm interactivo en headless** — los agentes shellean al CLI `openspec` vía Skill in-context; si gemini exige confirmación interactiva de tool-calls de shell y el desktop lo spawnea sin TTY, las fases podrían colgarse. **Riesgo no verificado, alto impacto** — es lo que el spike del primer paso debe descartar.
149
+ 4. **`@`-routing headless no garantizado** — la delegación forzada `@sr-architect` puede no funcionar sin interactividad; quizás haya que apoyarse en auto-delegación, menos determinista para un pipeline que exige las tres fases.
150
+ 5. **OpenSpec install adaptado a gemini** — `openspec init --tools gemini` está confirmado nativo en 1.3.1, pero **no verifiqué empíricamente en esta sesión** que los `.gemini/skills/openspec-*` que genera sean **invocables** por el orquestador gemini en el flujo real (solo que el `--tools` lo acepta). Validar en el spike.
151
+
152
+ ### Lo que NO está cierto (declarado explícitamente)
153
+ - Si el orquestador gemini puede **iniciar** spawns en headless/exec, o si solo el harness puede pre-definir un set fijo de agentes. Si fuera lo segundo, el orquestador `implement` habría que re-expresarlo como agentes paralelos definidos por harness en vez de spawns iniciados por el orquestador — **lift mayor**. Esto **decide la viabilidad real** y no es deducible de estos repos.
154
+ - El comportamiento exacto de confirm de shell-calls de gemini en headless (riesgo #3).
155
+
156
+ **Archivos clave (absolutos):**
157
+ - Core gap: `/Users/javi/repos/specrails-core/src/installer/phases/provider-detect.ts` (15, 33-36, 87-92), `/Users/javi/repos/specrails-core/src/installer/phases/install-config.ts` (13, 96), `/Users/javi/repos/specrails-core/src/installer/phases/scaffold.ts` (174-184, 244-250, 278-311, 666-747, 762-830), `/Users/javi/repos/specrails-core/src/installer/commands/init.ts` (86, 128, 204-218), `/Users/javi/repos/specrails-core/integration-contract.json` (3-26), `/Users/javi/repos/specrails-core/pinned-versions.json` (3).
158
+ - Templates a transformar: `/Users/javi/repos/specrails-core/templates/commands/specrails/implement.md`, `/Users/javi/repos/specrails-core/templates/commands/specrails/batch-implement.md`, `/Users/javi/repos/specrails-core/templates/agents/sr-{architect,developer,reviewer}.md`.
159
+ - Precedente de aplanamiento codex: `/Users/javi/repos/specrails-core/templates/codex-skills/batch-implement/SKILL.md` (5, 19-32), `/Users/javi/repos/specrails-core/templates/codex-skills/implement/SKILL.md` (24).
160
+ - Desktop (ya hecho): `/Users/javi/repos/specrails-desktop/server/providers/gemini-adapter.ts` (225-235), `/Users/javi/repos/specrails-desktop/server/providers/index.ts` (12, 16); a tocar: `/Users/javi/repos/specrails-desktop/server/queue-manager.ts` (command translation, `profile=null`), `/Users/javi/repos/specrails-desktop/client/src/lib/provider-capabilities.ts` (ocultar rails gemini).
package/docs/gemini.md ADDED
@@ -0,0 +1,106 @@
1
+ # Using Specrails with the Gemini CLI
2
+
3
+ Specrails supports Google's [Gemini CLI](https://github.com/google-gemini/gemini-cli)
4
+ as a third AI provider alongside Claude Code and the Codex CLI.
5
+
6
+ > **Beta — opt-in.** Gemini is gated behind `SPECRAILS_GEMINI_BETA` and is **off by
7
+ > default**. Until you set it, Gemini is invisible everywhere (it won't show in Add
8
+ > Project and can't be selected). See [Enabling the beta](#enabling-the-beta).
9
+ >
10
+ > **Scope today.** With the beta on, Gemini powers the *non-pipeline* surfaces —
11
+ > Explore Spec, Quick spec, AI Edit, the terminal launcher, and cost analytics.
12
+ > The full **rails pipeline** (`/specrails:implement`, `batch-implement`) needs a
13
+ > Gemini artifact target in `specrails-core` (`.gemini/commands/*.toml` +
14
+ > `.gemini/agents/sr-*.md`), which ships separately. Rails are hidden for Gemini
15
+ > projects until then.
16
+
17
+ ## Prerequisites
18
+
19
+ - **Gemini CLI ≥ 0.11.0** on your `PATH` (`npm i -g @google/gemini-cli`; `gemini --version`).
20
+ Older versions lack `--output-format stream-json` and headless `--resume`.
21
+ - **A Gemini API key.** Specrails spawns Gemini headlessly, so it needs
22
+ non-interactive auth: set `GEMINI_API_KEY` (a paid Gemini Developer API key from
23
+ [Google AI Studio](https://aistudio.google.com/apikey)). The free OAuth
24
+ "Login with Google" tier exists but is being wound down for the CLI and is not a
25
+ reliable unattended path — use an API key.
26
+
27
+ ## Enabling the beta
28
+
29
+ Set the env var where the Specrails server runs, then restart the app:
30
+
31
+ ```bash
32
+ SPECRAILS_GEMINI_BETA=1 # or 'true'
33
+ ```
34
+
35
+ With it set, `GET /api/available-providers` reports Gemini's real install status,
36
+ Add Project shows a **Gemini** checkbox, and projects can select it.
37
+
38
+ ## Adding a Gemini project
39
+
40
+ Add Project → tick **Gemini** (visible once the beta is on and `gemini` is on PATH).
41
+ A project can install Gemini alongside Claude/Codex; the first provider selected is
42
+ the primary/default, and per-invocation engine pickers let you choose per spec/rail.
43
+
44
+ ## What's different vs Claude
45
+
46
+ - **Cost is estimated, not native.** Gemini does not report a USD cost in its
47
+ stream, so Specrails estimates it from a rate card (`server/pricing.ts`,
48
+ keys `gemini:<model>`). Token counts (incl. cached) come straight from the run.
49
+ - **No per-agent profiles / SMASH / Contract Refine.** These are Claude-only; on a
50
+ mixed project the capability intersection hides them (same as Codex).
51
+ - **System prompt is folded.** Gemini has no `--system-prompt` flag, so for
52
+ non-Explore actions the system prompt is folded into the user prompt. Explore
53
+ turns stay user-only and trust the app-managed `GEMINI.md` in the explore cwd.
54
+ - **OTEL is native.** Unlike Codex, Gemini emits OTLP via `GEMINI_TELEMETRY_*`, so
55
+ pipeline telemetry needs no synthetic bridge.
56
+
57
+ ## Models
58
+
59
+ Curated catalog (`server/providers/gemini-adapter.ts`): `gemini-2.5-pro` (default),
60
+ `gemini-2.5-flash`, `gemini-2.5-flash-lite`, `gemini-3.1-pro-preview`. Concrete GA
61
+ ids are pinned (preview ids rotate). Free-tier API keys serve Flash; Pro/preview
62
+ need billing.
63
+
64
+ ## Trusted folders (headless)
65
+
66
+ Gemini's "trusted folders" gate silently overrides `--yolo` back to `default` —
67
+ blocking *every* tool call — in a directory it doesn't trust. Specrails injects
68
+ `GEMINI_CLI_TRUST_WORKSPACE=true` into every Gemini spawn (`server/util/cli-prompt.ts`
69
+ `spawnGemini`) so headless rails/explore can actually run tools. No action needed
70
+ from you.
71
+
72
+ ## Troubleshooting
73
+
74
+ - **"Gemini" never appears in Add Project** → the beta is off (`SPECRAILS_GEMINI_BETA`
75
+ unset), or `gemini` isn't on PATH. Check `GET /api/available-providers`.
76
+ - **Tools never run / job does nothing** → almost always auth or trust. Confirm
77
+ `GEMINI_API_KEY` is set in the server env; the trust var is handled automatically.
78
+ - **`limit: 0` / quota errors on Pro** → free-tier API keys don't serve Pro models;
79
+ use a Flash model or enable billing.
80
+ - **Cost shows `—`** → no `gemini:<model>` pricing row for the model used; add one to
81
+ `server/pricing.ts`.
82
+
83
+ ## Emergency rollback
84
+
85
+ Unset `SPECRAILS_GEMINI_BETA` (or set it to `0`). Gemini disappears from the UI and
86
+ becomes unselectable immediately; existing non-Gemini projects are unaffected. The
87
+ adapter stays registered but dormant.
88
+
89
+ ## Architecture pointers (for specrails-desktop developers)
90
+
91
+ - Adapter: `server/providers/gemini-adapter.ts` (`nativeCostUsd:false`,
92
+ `nativeOtelEnv:true`, `systemPromptArg:false`, `instructionsFilename:'GEMINI.md'`,
93
+ `mcpRegistration:'project-json'`). Registered in `server/providers/index.ts`.
94
+ - Stream schema (validated against gemini 0.46): `init{session_id,model}` /
95
+ `message{role,content,delta}` / `tool_use{tool_name,tool_id,parameters}` /
96
+ `tool_result` / `result{stats}`. `stats.input_tokens` includes `stats.cached`.
97
+ - Beta gate + provider list: `server/desktop-router.ts` (`isGeminiBetaEnabled`,
98
+ `/available-providers`, `POST /projects`).
99
+ - Trust-folder env: `server/util/cli-prompt.ts` `spawnGemini`.
100
+
101
+ ## See also
102
+
103
+ - [`docs/codex.md`](./codex.md) — the Codex provider (the other non-native provider).
104
+ - [`docs/adding-a-provider.md`](./adding-a-provider.md) — how to add a provider.
105
+ - [`docs/gemini-cli-provider-study.md`](./gemini-cli-provider-study.md) — the design study.
106
+ - [`docs/gemini-core-support-evaluation.md`](./gemini-core-support-evaluation.md) — the rails/core work.
@@ -94,18 +94,15 @@ A non-localhost `Origin` header is rejected by the CORS middleware with `403 For
94
94
 
95
95
  ### Mobile Gateway admin (`/api/mobile/*`)
96
96
 
97
- Loopback-only admin surface for the Mobile Gateway (the gateway itself is a separate HTTPS+WSS listener the phone talks to). The wire contract the phone consumes intentionally keeps the legacy `hub.*` names — see the frozen mobile wire-compat note in `CLAUDE.md`.
97
+ Loopback-only admin surface for the Mobile Gateway (the gateway itself is a separate HTTPS listener that tunnels the web companion's RPC over WebRTC). The wire contract the phone consumes intentionally keeps the legacy `hub.*` names — see the frozen mobile wire-compat note in `CLAUDE.md`.
98
98
 
99
99
  | Method | Path | Notes |
100
100
  |--------|------|-------|
101
- | `GET` | `/status` | Gateway status (`{ enabled, running, port, certFingerprint, lanAddresses, mdnsEnabled, desktopName }`) |
101
+ | `GET` | `/status` | Gateway status (`{ enabled, running, port, certFingerprint, desktopName }`) |
102
102
  | `POST` | `/enable` | Start the gateway (generates the TLS identity on first run) |
103
103
  | `POST` | `/disable` | Stop the gateway |
104
- | `POST` | `/pairing-session` | Open a pairing session; returns the QR payload |
105
- | `GET` | `/pairing-session` | Poll the current pairing session (pending claim, if any) |
106
- | `POST` | `/pairing-session/approve` | Approve the pending claim (pairs the device) |
107
- | `POST` | `/pairing-session/deny` | Deny the pending claim |
108
- | `DELETE` | `/pairing-session` | Cancel the pairing session |
104
+ | `POST` | `/webrtc/offer` | Create a serverless pairing offer (offer SDP + single-use secret + desktop identity) for the first QR |
105
+ | `POST` | `/webrtc/answer` | Apply the companion's scanned answer SDP to the open offer |
109
106
  | `GET` | `/devices` | List paired devices |
110
107
  | `DELETE` | `/devices/:id` | Revoke a paired device |
111
108
  | `POST` | `/cert/rotate` | Rotate the gateway TLS identity (unpairs every device) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,7 +42,6 @@
42
42
  "ci": "npm run typecheck && npm run check-core-compat && npm run test:coverage && cd client && npm run test:coverage"
43
43
  },
44
44
  "dependencies": {
45
- "@homebridge/ciao": "^1.3.9",
46
45
  "@tailwindcss/typography": "^0.5.19",
47
46
  "ajv": "^8.17.1",
48
47
  "better-sqlite3": "^12.8.0",
@@ -57,6 +56,7 @@
57
56
  "read-excel-file": "^9.0.6",
58
57
  "selfsigned": "^5.5.0",
59
58
  "tree-kill": "^1.2.2",
59
+ "werift": "^0.23.0",
60
60
  "ws": "^8.16.0"
61
61
  },
62
62
  "devDependencies": {
@@ -278,7 +278,7 @@ class ChatManager {
278
278
  slug: this._projectSlug,
279
279
  projectPath: this._cwd,
280
280
  projectName: this._projectName,
281
- provider: (providerId ?? this._adapter.id),
281
+ provider: providerId ?? this._adapter.id,
282
282
  });
283
283
  console.log(`[chat-manager] explore spawn cwd=${cwd} (mcp=off)`);
284
284
  return cwd;
@@ -8,7 +8,12 @@ exports.CORE_PACKAGE_SPEC = void 0;
8
8
  * minute it is published — adopting a new major is a deliberate one-line
9
9
  * bump here, shipped through the app's own release pipeline.
10
10
  *
11
+ * Floor bumped to 4.8.0 — the version that ships the Gemini provider target
12
+ * (`.gemini/` commands + agents). It also changes the `npx` package-spec string,
13
+ * which invalidates any stale `_npx` exec cache that npx would otherwise reuse
14
+ * for the old `^4.6.0` range (so users actually resolve the newer core).
15
+ *
11
16
  * `SPECRAILS_CORE_BIN` remains the escape hatch for local/linked builds
12
17
  * (see getCoreCommand in setup-manager.ts).
13
18
  */
14
- exports.CORE_PACKAGE_SPEC = 'specrails-core@^4.6.0';
19
+ exports.CORE_PACKAGE_SPEC = 'specrails-core@^4.8.0';
@@ -29,6 +29,14 @@ function isCodexBetaDisabled() {
29
29
  const v = process.env.SPECRAILS_CODEX_BETA ?? process.env.SPECRAILS_HUB_CODEX_BETA;
30
30
  return v === '0';
31
31
  }
32
+ // Gemini is opt-IN (default off): unlike codex, its stream-json schema has not
33
+ // yet been validated against a live binary, so it stays hidden + unselectable
34
+ // until SPECRAILS_GEMINI_BETA=1 (or 'true'). The adapter is always registered
35
+ // (pricing/getAdapter work); only project selection is gated.
36
+ function isGeminiBetaEnabled() {
37
+ const v = (process.env.SPECRAILS_GEMINI_BETA ?? '').toLowerCase();
38
+ return v === '1' || v === 'true';
39
+ }
32
40
  // Theme allow-list. Mirror of THEME_IDS in `client/src/lib/themes.ts` —
33
41
  // kept duplicated to avoid pulling client code into the server bundle.
34
42
  const THEME_ID_ALLOWLIST = new Set(['dracula', 'aurora-light', 'obsidian-dark', 'matrix', 'specrails']);
@@ -144,14 +152,19 @@ function createDesktopRouter(registry, broadcast) {
144
152
  const providers = (0, core_compat_1.detectAvailableCLIs)();
145
153
  // tiers: quick install is always available (app-driven config); full requires an AI CLI
146
154
  const tiers = ['quick'];
147
- if (providers.claude || providers.codex)
155
+ if (Object.values(providers).some(Boolean))
148
156
  tiers.push('full');
149
- const codexBetaOff = isCodexBetaDisabled();
150
- res.json({
151
- claude: providers.claude,
152
- codex: codexBetaOff ? false : providers.codex,
153
- tiers,
154
- });
157
+ // Return the full detected map (registry-driven) so a newly-registered
158
+ // provider surfaces here with no edit. Apply per-provider beta gates: codex
159
+ // is forced unavailable when SPECRAILS_CODEX_BETA=0 (emergency rollback).
160
+ const gated = { ...providers };
161
+ if (isCodexBetaDisabled())
162
+ gated.codex = false;
163
+ // Gemini is opt-in: omit it entirely (not just `false`) when the beta flag is
164
+ // off, so it stays fully invisible in the UI until SPECRAILS_GEMINI_BETA=1.
165
+ if (!isGeminiBetaEnabled())
166
+ delete gated.gemini;
167
+ res.json({ ...gated, tiers });
155
168
  });
156
169
  router.get('/setup-prerequisites', (req, res) => {
157
170
  const status = (0, setup_prerequisites_1.getSetupPrerequisitesStatus)();
@@ -218,6 +231,12 @@ function createDesktopRouter(registry, broadcast) {
218
231
  });
219
232
  return;
220
233
  }
234
+ if (providers.includes('gemini') && !isGeminiBetaEnabled()) {
235
+ res.status(400).json({
236
+ error: 'Gemini provider is in beta and disabled by default. Set SPECRAILS_GEMINI_BETA=1 to enable.',
237
+ });
238
+ return;
239
+ }
221
240
  const resolvedPath = path_1.default.resolve(projectPath);
222
241
  // Validate path exists
223
242
  if (!fs_1.default.existsSync(resolvedPath)) {
@@ -243,7 +262,7 @@ function createDesktopRouter(registry, broadcast) {
243
262
  name: derivedName,
244
263
  path: canonicalPath,
245
264
  provider: providers[0],
246
- providers: providers,
265
+ providers,
247
266
  });
248
267
  broadcast({
249
268
  type: 'desktop.project_added',
@@ -127,7 +127,7 @@ function ensureExploreCwd(input, baseDir) {
127
127
  // — provider is immutable post-creation but the lifecycle code MUST handle
128
128
  // the edge per spec), remove any stale instructions file authored by a
129
129
  // different provider so the explore-cwd doesn't carry both.
130
- const STALE_INSTRUCTION_FILES = ['CLAUDE.md', 'AGENTS.md'];
130
+ const STALE_INSTRUCTION_FILES = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'];
131
131
  for (const stale of STALE_INSTRUCTION_FILES) {
132
132
  if (stale === adapter.instructionsFilename)
133
133
  continue;
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
- // Mobile Gateway — public surface. The gateway is a second HTTPS+WSS listener in
3
- // the same Node process (default :4202, OFF by default) that pairs phones by QR +
4
- // desktop approval and exposes a redacted, deny-by-default allow-list of the
5
- // desktop API over per-device tokens. The main server at 127.0.0.1:4200 is never
6
- // exposed.
2
+ // Mobile Gateway — public surface. The gateway is a second HTTPS listener in the
3
+ // same Node process (default :4202, OFF by default) that pairs the web companion
4
+ // serverlessly over WebRTC and exposes a redacted, deny-by-default allow-list of
5
+ // the desktop API over per-device tokens. The main server at 127.0.0.1:4200 is
6
+ // never exposed.
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.MOBILE_ALLOWLIST = exports.getMobileEventBus = exports.createMobileAdminRouter = exports.MobileGateway = void 0;
9
9
  var mobile_gateway_1 = require("./mobile-gateway");
@@ -24,41 +24,6 @@ function createMobileAdminRouter(deps) {
24
24
  await gateway.setEnabled(false);
25
25
  res.json(gateway.status());
26
26
  });
27
- // —— Pairing session ——
28
- router.post('/pairing-session', (_req, res) => {
29
- if (!gateway.running || !gateway.pairing) {
30
- res.status(409).json({ error: 'Enable mobile access first' });
31
- return;
32
- }
33
- res.json({ qr: gateway.pairing.createSession() });
34
- });
35
- router.get('/pairing-session', (_req, res) => {
36
- if (!gateway.pairing) {
37
- res.json({ status: 'none' });
38
- return;
39
- }
40
- res.json(gateway.pairing.getDesktopState() ?? { status: 'none' });
41
- });
42
- router.post('/pairing-session/approve', (_req, res) => {
43
- if (!gateway.pairing) {
44
- res.status(409).json({ error: 'No pairing session' });
45
- return;
46
- }
47
- const result = gateway.pairing.approve();
48
- if (!result.ok) {
49
- res.status(409).json({ error: result.reason });
50
- return;
51
- }
52
- res.json({ ok: true });
53
- });
54
- router.post('/pairing-session/deny', (_req, res) => {
55
- gateway.pairing?.deny();
56
- res.json({ ok: true });
57
- });
58
- router.delete('/pairing-session', (_req, res) => {
59
- gateway.pairing?.cancel();
60
- res.json({ ok: true });
61
- });
62
27
  // —— Devices ——
63
28
  router.get('/devices', (_req, res) => {
64
29
  res.json({ devices: (0, mobile_devices_1.listDevices)(desktopDb) });
@@ -80,5 +45,33 @@ function createMobileAdminRouter(deps) {
80
45
  const status = await gateway.rotateCert();
81
46
  res.json(status);
82
47
  });
48
+ // —— Serverless WebRTC pairing signaling (webview ↔ local peer) ——
49
+ // The webview asks for an offer (→ first QR), then posts the companion's
50
+ // scanned answer SDP. No SDP ever leaves the desktop except as a QR.
51
+ router.post('/webrtc/offer', async (_req, res) => {
52
+ if (!gateway.running || !gateway.webrtc) {
53
+ res.status(409).json({ error: 'Enable mobile access first' });
54
+ return;
55
+ }
56
+ try {
57
+ const offer = await gateway.webrtcOffer();
58
+ if (!offer) {
59
+ res.status(409).json({ error: 'WebRTC unavailable' });
60
+ return;
61
+ }
62
+ res.json(offer);
63
+ }
64
+ catch {
65
+ res.status(500).json({ error: 'Failed to create offer' });
66
+ }
67
+ });
68
+ router.post('/webrtc/answer', async (req, res) => {
69
+ const sdp = typeof req.body?.sdp === 'string' ? req.body.sdp : '';
70
+ if (!sdp) {
71
+ res.status(400).json({ error: 'sdp required' });
72
+ return;
73
+ }
74
+ res.json({ ok: await gateway.webrtcAnswer(sdp) });
75
+ });
83
76
  return router;
84
77
  }