alegra-data-agent 0.7.1 → 0.7.2
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/README.md +8 -432
- package/dist/autoupdate.js +64 -17
- package/dist/codex-trust.js +128 -0
- package/dist/commands/install.js +35 -0
- package/dist/commands/status.js +11 -0
- package/dist/commands/update.js +9 -1
- package/dist/installer.js +75 -31
- package/dist/lib.js +6 -0
- package/dist/main.js +1 -0
- package/dist/targets.js +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,445 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# alegra-data-agent
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> gateway y en el login. El repositorio es `Alegra-Team/data-agent-selfservice` y el paquete npm
|
|
5
|
-
> es `alegra-data-agent`: son tres registros independientes y no tienen por qué coincidir.
|
|
6
|
-
>
|
|
7
|
-
> El paquete **no se renombra**: su nombre está incrustado en el hook de arranque de cada máquina
|
|
8
|
-
> ya instalada, así que cambiarlo obligaría a migrar a todo el mundo a mano. Lo mismo vale para
|
|
9
|
-
> el `ISSUER`/`AUDIENCE` de los JWT (`gateway/lib/jwt.ts`) — tocarlos invalidaría los tokens ya
|
|
10
|
-
> emitidos.
|
|
11
|
-
|
|
12
|
-
Agente de Data interno de Alegra. Una línea instala las skills + MCPs del equipo BI Data en tu herramienta de IA (Claude Code, Cursor, OpenCode, Antigravity, Codex, Hermes), gated por OAuth `@alegra.com`.
|
|
13
|
-
|
|
14
|
-
> Uso interno de Alegra. No publicar, no promocionar, no contribuciones externas.
|
|
15
|
-
|
|
16
|
-
## Estado
|
|
17
|
-
|
|
18
|
-
- **CLI**: [`alegra-data-agent`](https://www.npmjs.com/package/alegra-data-agent) en npmjs.com.
|
|
19
|
-
- **Gateway en producción**: [https://alegra-data-agent.vercel.app](https://alegra-data-agent.vercel.app).
|
|
20
|
-
- **Healthcheck**: `https://alegra-data-agent.vercel.app/api/health`.
|
|
21
|
-
- **Guía de uso y documentación**: [wiki del agente](https://wiki.alegra.com/doc/alegra-data-agent-DKiPc9cvFS) — empezar, cómo preguntar, qué responde, problemas comunes, [arquitectura y seguridad](https://wiki.alegra.com/doc/arquitectura-y-seguridad-vWfNc90Myy) e [historial de cambios](https://wiki.alegra.com/doc/historial-de-cambios-J8uMHq1sfd). La página de entrada del gateway enlaza a todas.
|
|
22
|
-
|
|
23
|
-
## Compatibilidad
|
|
24
|
-
|
|
25
|
-
| Sistema operativo | ✓ |
|
|
26
|
-
|---|---|
|
|
27
|
-
| macOS (Intel + Apple Silicon) | ✅ |
|
|
28
|
-
| Ubuntu / Debian / Linux | ✅ |
|
|
29
|
-
| WSL (Windows Subsystem for Linux) | ✅ |
|
|
30
|
-
| Windows 10/11 nativo | ✅ |
|
|
31
|
-
|
|
32
|
-
| Cliente AI | ✓ |
|
|
33
|
-
|---|---|
|
|
34
|
-
| [Claude Code](https://claude.com/claude-code) | ✅ |
|
|
35
|
-
| [Cursor](https://cursor.com) | ✅ |
|
|
36
|
-
| [OpenCode](https://opencode.ai) | ✅ |
|
|
37
|
-
| [Antigravity](https://antigravity.dev) | ✅ |
|
|
38
|
-
| [Codex](https://developers.openai.com/codex) | ✅ |
|
|
39
|
-
| [Hermes Agent](https://hermes-agent.nousresearch.com) | ✅ |
|
|
40
|
-
|
|
41
|
-
Si no tenés ningún cliente instalado, el bootstrap te instala Claude Code automáticamente.
|
|
42
|
-
|
|
43
|
-
### Si no usás terminal
|
|
44
|
-
|
|
45
|
-
El comando de arriba es para clientes CLI/IDE. En **claude.ai, Claude Desktop, Cowork y móvil**
|
|
46
|
-
el agente llega sin instalar nada:
|
|
47
|
-
|
|
48
|
-
| | Cómo |
|
|
49
|
-
|---|---|
|
|
50
|
-
| MCPs | Configuración → **Conectores**. En Team/Enterprise el owner los agrega en *Organization settings* y le quedan disponibles a toda Alegra. |
|
|
51
|
-
| Skills | Configuración → **Habilidades** → subir el `.zip`. El owner puede provisionarlas para toda la organización y **aparecen solas**, sin que nadie suba nada. |
|
|
52
|
-
|
|
53
|
-
Los ZIP se generan con `pnpm run pack:web` (uno por skill, en `dist/skills-web/`).
|
|
54
|
-
|
|
55
|
-
Dos cosas que conviene saber:
|
|
56
|
-
|
|
57
|
-
- Un conector de la cuenta se sirve a **todas** las superficies, **Claude Code incluido** — ahí
|
|
58
|
-
aparecen en `/mcp` bajo `claude.ai`. Si ya tenés el MCP como server local, el conector queda
|
|
59
|
-
oculto por URL duplicada; el propio `/mcp` te dice cómo cambiar de uno al otro.
|
|
60
|
-
- Los ZIP subidos **no se auto-actualizan**, y no hay API para subirlos. El CLI sí (manifest
|
|
61
|
-
firmado). Después de un release, `pnpm run pack:web` dice cuáles cambiaron: solo esos se
|
|
62
|
-
vuelven a subir (paso 5 del «Flujo de release»).
|
|
63
|
-
|
|
64
|
-
Detalle y el porqué en [ADR-0010](./docs/adr/0010-superficies-no-code.md).
|
|
3
|
+
Agente de Data interno de Alegra. Instala skills + MCPs en tu cliente de IA con un comando.
|
|
65
4
|
|
|
66
5
|
## Instalación
|
|
67
6
|
|
|
68
|
-
**El comando único es:**
|
|
69
|
-
|
|
70
7
|
```sh
|
|
71
8
|
pnpm dlx alegra-data-agent
|
|
72
9
|
```
|
|
73
10
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
### Si no tenés Node o pnpm
|
|
77
|
-
|
|
78
|
-
Si al correr `pnpm dlx alegra-data-agent` te dice `pnpm: command not found`, primero corré el bootstrap que te instala Node 22+ y pnpm 11+ automáticamente:
|
|
79
|
-
|
|
80
|
-
**macOS / Linux / WSL** — en cualquier terminal:
|
|
81
|
-
|
|
82
|
-
```sh
|
|
83
|
-
curl -fsSL https://alegra-data-agent.vercel.app/install.sh | bash
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
**Windows nativo** — en PowerShell (sin admin):
|
|
87
|
-
|
|
88
|
-
```powershell
|
|
89
|
-
irm https://alegra-data-agent.vercel.app/install.ps1 | iex
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
El bootstrap deja Node + pnpm + git + Claude Code listos, y al final corre `pnpm dlx alegra-data-agent` por vos. Después de eso, el comando único es siempre `pnpm dlx alegra-data-agent`.
|
|
93
|
-
|
|
94
|
-
### Después del install
|
|
95
|
-
|
|
96
|
-
1. **Reiniciá tu cliente AI** (Claude Code / Cursor / OpenCode / Antigravity / Codex / Hermes).
|
|
97
|
-
2. **Hacé tu primera pregunta** en español. Ejemplo: *"cuántos nuevos clientes en mayo en Colombia"*.
|
|
98
|
-
3. **La primera vez** que el agente use un servicio (Metabase, Amplitude, Wiki) te va a pedir 1 click con tu cuenta `@alegra.com`. Después no te pide nada más.
|
|
99
|
-
|
|
100
|
-
> El agente nunca te va a pedir API keys, tokens, ni variables de entorno. Todo es OAuth `@alegra.com` automático.
|
|
101
|
-
|
|
102
|
-
> **HubSpot es la excepción: no lo instala el CLI, se habilita como conector de la cuenta.** El
|
|
103
|
-
> servidor de HubSpot no soporta el registro automático que usan los clientes AI, así que una entry
|
|
104
|
-
> local nunca autentica. El conector oficial sí funciona: Configuración → Conectores → HubSpot →
|
|
105
|
-
> Conectar, y queda disponible en Claude Code, Claude Desktop y claude.ai. En Cursor, OpenCode y
|
|
106
|
-
> Codex hoy no hay ruta. Detalle en [`agent/mcps/hubspot/README.md`](./agent/mcps/hubspot/README.md).
|
|
11
|
+
Solo accesible para cuentas `@alegra.com` (OAuth Google). El CLI te abre el navegador para iniciar sesión.
|
|
107
12
|
|
|
108
13
|
## Requisitos
|
|
109
14
|
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
- Un cliente AI compatible — el bootstrap instala Claude Code si no detecta ninguno.
|
|
114
|
-
|
|
115
|
-
## Qué se instala
|
|
116
|
-
|
|
117
|
-
| Herramienta | Skills | Configuración MCP |
|
|
118
|
-
|---|---|---|
|
|
119
|
-
| Claude Code | `~/.claude/skills/<n>/` (carpeta) | `~/.claude.json` → `mcpServers` |
|
|
120
|
-
| Cursor | `~/.cursor/skills/<n>/` (carpeta) | `~/.cursor/mcp.json` → `mcpServers` |
|
|
121
|
-
| OpenCode | `~/.config/opencode/skills/<n>/` (carpeta) | `~/.config/opencode/opencode.json` → `mcp` |
|
|
122
|
-
| Antigravity | `~/.gemini/skills/<n>/` (carpeta) | `~/.gemini/config/mcp_config.json` → `mcpServers` |
|
|
123
|
-
| Codex | `~/.agents/skills/<n>/` (carpeta) | `~/.codex/config.toml` → `[mcp_servers.<n>]` |
|
|
124
|
-
|
|
125
|
-
### Acotar los MCPs a una carpeta (opcional)
|
|
126
|
-
|
|
127
|
-
Por defecto los MCPs se instalan a nivel usuario: quedan disponibles en toda sesión, de
|
|
128
|
-
cualquier carpeta. Si en la misma máquina separás contextos —trabajo de Alegra, proyectos
|
|
129
|
-
propios, trabajo de un cliente— podés acotarlos a una sola carpeta:
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
mkdir -p ~/.alegra
|
|
133
|
-
echo 'MCP_DIR=~/trabajo/alegra' >> ~/.alegra/agent.conf
|
|
134
|
-
pnpm dlx alegra-data-agent install --yes
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Los MCPs pasan a `~/trabajo/alegra/.mcp.json`, que Claude Code resuelve subiendo desde el
|
|
138
|
-
directorio actual: **cascadea a todo lo que esté dentro de esa carpeta y es invisible fuera
|
|
139
|
-
de ella**. Se pre-aprueban solos, así que no hay que aceptarlos repo por repo.
|
|
140
|
-
|
|
141
|
-
También funciona como env var (`ALEGRA_DATA_AGENT_MCP_DIR`, le gana al archivo), pero el
|
|
142
|
-
archivo es lo recomendado: el hook de auto-update corre en un shell no interactivo y no ve
|
|
143
|
-
lo que exportes en tu `.bashrc`, así que reinstalaría a nivel usuario.
|
|
144
|
-
|
|
145
|
-
**Qué clientes lo respetan.** Claude Code y Cursor pueden acotar un MCP a una carpeta, así
|
|
146
|
-
que con `MCP_DIR` los MCPs van solo a esos dos. OpenCode, Hermes, Codex y Antigravity no
|
|
147
|
-
pueden: OpenCode busca su config de proyecto solo hasta la raíz del repo git, y Hermes guarda
|
|
148
|
-
los MCPs por perfil. Ahí los MCPs quedarían globales, así que no se instalan y se retiran los
|
|
149
|
-
que ya tuvieran. Las skills sí se instalan en todos. Para consultar datos de Alegra, usá
|
|
150
|
-
Claude Code o Cursor dentro de la carpeta.
|
|
151
|
-
|
|
152
|
-
**Se corrige solo.** Cada arranque compara dónde debería estar cada MCP con dónde está y
|
|
153
|
-
retira lo que sobra, siempre que sea nuestro: una copia en `~/.claude.json` de antes de
|
|
154
|
-
configurar `MCP_DIR`, la de una carpeta vieja si lo cambiaste o lo sacaste, o la de un cliente
|
|
155
|
-
que no corresponde. Un server tuyo con el mismo nombre y otra configuración no se toca.
|
|
156
|
-
`alegra-data-agent status` te dice en qué archivo quedaron y avisa si ve algo fuera de lugar.
|
|
157
|
-
|
|
158
|
-
Detalle completo en [ADR-0012](docs/adr/0012-mcp-scope-por-workspace.md).
|
|
159
|
-
|
|
160
|
-
### Dónde quedan los archivos que genera el agente
|
|
161
|
-
|
|
162
|
-
Cuando pedís un CSV, un Excel o un gráfico, el agente lo guarda en `alegra-data/` dentro de
|
|
163
|
-
la carpeta donde abriste la sesión, y te dice la ruta al terminar (en WSL, también la de
|
|
164
|
-
Windows). Nunca en los temporales del cliente. Para que vaya siempre a la misma carpeta:
|
|
165
|
-
|
|
166
|
-
```bash
|
|
167
|
-
mkdir -p ~/.alegra
|
|
168
|
-
echo "OUTPUT_DIR=$HOME/Documentos/alegra-data" >> ~/.alegra/agent.conf
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Lo lee el agente, no el CLI, así que aplica desde la siguiente pregunta, sin reinstalar. En
|
|
172
|
-
claude.ai, Desktop y Cowork el archivo aparece para descargar en el chat.
|
|
173
|
-
|
|
174
|
-
El CLI:
|
|
175
|
-
|
|
176
|
-
1. Verifica auth contra el gateway (OAuth Google `hd=alegra.com`).
|
|
177
|
-
2. Descarga el manifest firmado Ed25519 y verifica firma antes de tocar disco.
|
|
178
|
-
3. Verifica SHA-256 por archivo antes de escribirlo.
|
|
179
|
-
4. Escribe atómicamente (tmp + rename) los configs JSON y respalda lo que ya existía (`*.bak-<ts>`).
|
|
180
|
-
5. Registra en `~/.config/alegra-data-agent/lock.json` qué quedó instalado por target.
|
|
181
|
-
6. Configura **auto-update en cada cliente instalado** (los 5, no solo Claude Code): Claude Code y Codex vía hook `SessionStart`, Cursor vía `workspaceOpen`, Antigravity vía `SessionStart` (Gemini), OpenCode vía plugin. Al abrir el cliente, el contenido se actualiza solo.
|
|
182
|
-
|
|
183
|
-
## Comandos
|
|
184
|
-
|
|
185
|
-
```sh
|
|
186
|
-
pnpm dlx alegra-data-agent # instala / actualiza todo (default)
|
|
187
|
-
pnpm dlx alegra-data-agent --target=claude-code,cursor
|
|
188
|
-
pnpm dlx alegra-data-agent --only=alegra-sales,alegra-growth
|
|
189
|
-
pnpm dlx alegra-data-agent --dry-run
|
|
190
|
-
|
|
191
|
-
pnpm dlx alegra-data-agent update # re-descarga lo que cambió
|
|
192
|
-
pnpm dlx alegra-data-agent status # estado actual + drift
|
|
193
|
-
pnpm dlx alegra-data-agent list # catálogo del gateway
|
|
194
|
-
pnpm dlx alegra-data-agent uninstall
|
|
195
|
-
|
|
196
|
-
pnpm dlx alegra-data-agent login
|
|
197
|
-
pnpm dlx alegra-data-agent logout
|
|
198
|
-
pnpm dlx alegra-data-agent whoami
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
Ayuda completa: `pnpm dlx alegra-data-agent help`.
|
|
202
|
-
|
|
203
|
-
> El CLI siempre termina ejecutando la última versión publicada: si pnpm sirve una anterior (cooldown de 24h de pnpm 11), el comando detecta la nueva en el registry y se re-ejecuta solo con ella (self-bounce).
|
|
204
|
-
|
|
205
|
-
## Skills
|
|
206
|
-
|
|
207
|
-
16 skills. Cada equipo dueño mantiene la suya; BI Data mantiene la infraestructura y las skills horizontales.
|
|
208
|
-
|
|
209
|
-
| Skill | Dueño | Tipo |
|
|
210
|
-
|---|---|---|
|
|
211
|
-
| `alegra-bi` (router) | BI Data | Router |
|
|
212
|
-
| `alegra-strategic` | Strategic | Dominio |
|
|
213
|
-
| `alegra-product` | Producto | Dominio |
|
|
214
|
-
| `alegra-sales` | Sales | Dominio |
|
|
215
|
-
| `alegra-accountant` | Programa Contadores | Dominio |
|
|
216
|
-
| `alegra-cx` | Customer Experience | Dominio |
|
|
217
|
-
| `alegra-growth` | Growth | Dominio |
|
|
218
|
-
| `alegra-alanube` | Alanube | Dominio |
|
|
219
|
-
| `alegra-collection` | Collection | Dominio |
|
|
220
|
-
| `alegra-sr` | Strategic Relationships | Dominio |
|
|
221
|
-
| `alegra-board` | BI Data | Dominio |
|
|
222
|
-
| `dashboard-auditor` | BI Data | Horizontal (review) |
|
|
223
|
-
| `dataviz-reviewer` | BI Data | Horizontal (review) |
|
|
224
|
-
| `presentation-coach` | BI Data | Horizontal (coach) |
|
|
225
|
-
| `launch-measurement` | BI Data | Horizontal (proceso) |
|
|
226
|
-
| `agent-onboarding` | BI Data | Soporte |
|
|
227
|
-
|
|
228
|
-
Arquitectura: [`docs/skills-architecture.md`](./docs/skills-architecture.md). Contrato de ejecución (MBQL por defecto, SQL nativo de fallback, acceso MCP-first sin tokens): [`agent/skills/SKILL_STANDARD.md`](./agent/skills/SKILL_STANDARD.md) §3.1.
|
|
229
|
-
|
|
230
|
-
### Cómo responde el agente
|
|
231
|
-
|
|
232
|
-
Tres reglas transversales que heredan todas las skills de dominio (detalle en [`agent/skills/SKILL_STANDARD.md`](./agent/skills/SKILL_STANDARD.md)):
|
|
233
|
-
|
|
234
|
-
- **Pregunta la población, asume los defaults.** Si falta el período, el país o el segmento, elige el más útil y lo avisa en una línea. Si no está claro **qué se cuenta** — "nuevos usuarios" puede ser signups (se registraron) o new logos (empezaron a pagar), que difieren en un orden de magnitud — pregunta antes de consultar. El vocabulario de poblaciones vive en el router (`alegra-bi`, sección «Vocabulario»).
|
|
235
|
-
- **La respuesta dice de dónde sale el número.** `New logos (empezaron a pagar · fact_customers_mrr) | 71`, no `Nuevos | 71`. Una cifra sin procedencia no se puede auditar, y las mismas palabras nombran poblaciones distintas según la tabla.
|
|
236
|
-
- **Una pregunta = una consulta.** Dos si hace falta una semilla o un delta. MBQL por defecto; cuando MBQL no lo expresa (CTEs, subqueries, window functions) va SQL nativo por `execute_sql` en una llamada, en vez de partirlo en varias.
|
|
237
|
-
|
|
238
|
-
**Fuentes de verdad, cuando dos tablas dan números distintos:** `dm_strategic.fact_customers_mrr` manda para todo lo que sea quién paga, quién empezó y quién dejó de pagar. `dm_sales.fact_closed_deals` es su vista comercial (quién cerró, contra qué meta), conectada por `id_company`. Para eventos de uso de producto, `amplitude_events_enriched` en Metabase — no el MCP de Amplitude, no las columnas `*_m` de retención.
|
|
239
|
-
|
|
240
|
-
## MCPs
|
|
241
|
-
|
|
242
|
-
Los remotos van vía OAuth — el cliente abre el flow en el primer uso, **cero tokens manuales, cero env vars con secretos**. `salesforce` es la excepción: MCP stdio local del vendor, auth por la Salesforce CLI (`sf org login web --set-default`, requiere Node ≥ 22), solo lectura, para casos puntuales fuera del mirror de Metabase. No se autentica desde `/mcp` — soporte en el Caso 11 de `agent-onboarding`.
|
|
243
|
-
|
|
244
|
-
| MCP | Backend | Transporte | Auth |
|
|
245
|
-
|---|---|---|---|
|
|
246
|
-
| `amplitude` | Amplitude SaaS | remoto (http) | OAuth |
|
|
247
|
-
| `hubspot` | HubSpot SaaS | conector de cuenta | OAuth — **no se instala**: se habilita en Configuración → Conectores ([por qué](./agent/mcps/hubspot/README.md)) |
|
|
248
|
-
| `metabase` | Metabase self-hosted | remoto (http) | OAuth corporativo |
|
|
249
|
-
| `salesforce` | Salesforce DX (solo lectura) | stdio local | Salesforce CLI (`sf org login web`) — no por `/mcp` |
|
|
250
|
-
| `wiki-alegra` | Wiki self-hosted | remoto (http) | OAuth corporativo |
|
|
251
|
-
|
|
252
|
-
Llave universal para joins: `id_company`. Detalles en [`docs/joining-data-sources.md`](./docs/joining-data-sources.md).
|
|
253
|
-
|
|
254
|
-
## Para mantenedores
|
|
255
|
-
|
|
256
|
-
Skills viven en `agent/skills/<name>/SKILL.md` (+ anexos). MCPs en `agent/mcps/<name>/config.example.json`. El manifest firmado se regenera cuando esos paths cambian — **hoy en local**, porque Actions está apagado (ver «Release sin Actions» más abajo).
|
|
257
|
-
|
|
258
|
-
### Flujo de release
|
|
259
|
-
|
|
260
|
-
> Los pasos 1 y 2 describen el flujo **con CI**. Mientras Actions siga apagado, la validación y la firma van en local: ver «Release sin Actions» justo abajo.
|
|
261
|
-
|
|
262
|
-
1. PR `<rama>-changes` o `feature/* → testing` modificando `agent/skills/**` o `agent/mcps/**` (CI valida standards, scan y frescura del manifest).
|
|
263
|
-
2. PR `testing → main`: el workflow `sign-manifest.yml` se dispara en el PR (toca esos paths), regenera el manifest, lo escanea, lo firma con `ALEGRA_SIGNING_KEY` y commitea el manifest firmado de vuelta al branch del PR vía la REST API de GitHub (queda Verified, server-side). Al mergear, `main` ya tiene el manifest correcto.
|
|
264
|
-
3. Gateway en Vercel se redespliega manualmente con `pnpm run deploy` desde `gateway/` (corre `prepare:skills`, que verifica firma y sha256, y después `vercel --prod`; nunca `vercel --prod` a secas). La integración Vercel ↔ GitHub no está habilitada: el proyecto de Vercel vive en su propio team y no depende de la organización de GitHub.
|
|
265
|
-
4. Próximo `update` de cualquier alegrito trae los cambios — sin republicar el CLI a npm.
|
|
266
|
-
5. **Superficies no-code (claude.ai, Desktop, Cowork).** Los pasos anteriores no llegan ahí: esos
|
|
267
|
-
ZIP se suben a mano y no hay API para automatizarlo (ADR-0010). `pnpm run pack:web` lista
|
|
268
|
-
exactamente cuáles cambiaron respecto de lo que ya está en la organización. Se suben esos, y
|
|
269
|
-
después `pnpm run pack:web -- --mark-published <skills>` registra lo subido en
|
|
270
|
-
`agent/web-published.json`, que se commitea. Sin este paso, quien usa Desktop sigue con las
|
|
271
|
-
definiciones viejas sin saberlo.
|
|
272
|
-
|
|
273
|
-
### Release sin Actions
|
|
274
|
-
|
|
275
|
-
Con los workflows apagados, los dos pasos que hacía el CI se hacen en local. Requieren la clave
|
|
276
|
-
privada de firma (gestor del equipo) y el token de npm.
|
|
277
|
-
|
|
278
|
-
```sh
|
|
279
|
-
# 1. Validar, escanear, regenerar y FIRMAR el manifest
|
|
280
|
-
ALEGRA_SIGNING_KEY="$(cat /ruta/a/la/clave)" pnpm run manifest
|
|
281
|
-
|
|
282
|
-
# 2. Verificar que el manifest quedó firmado y válido antes de commitear
|
|
283
|
-
node --experimental-strip-types -e "import('./src/signature.ts').then(async m => {
|
|
284
|
-
const man = JSON.parse((await import('node:fs')).readFileSync('agent/skills/index.json','utf-8'));
|
|
285
|
-
const r = m.verifyManifestSignature(man);
|
|
286
|
-
console.log(r.ok ? 'firma válida — ' + man.signature.keyId : 'INVÁLIDA: ' + r.reason);
|
|
287
|
-
})"
|
|
288
|
-
|
|
289
|
-
# 3. Publicar a npm: DESPUÉS del merge a main, del deploy del gateway verificado en producción
|
|
290
|
-
# con una sesión real (manifest nuevo, firma válida, update instala) y del tag. Lección de la 0.5.0.
|
|
291
|
-
pnpm run prepublishOnly && cp .github/README-npm.md README.md && pnpm publish --access public --no-git-checks
|
|
292
|
-
git checkout README.md # devolver el README completo al repo
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
> `--no-git-checks` es obligatorio acá, no opcional: el swap del paso anterior deja el working tree
|
|
296
|
-
> "sucio" a propósito (es exactamente lo que hace `publish.yml` en `Swap to minimal README for npm
|
|
297
|
-
> tarball` + `--no-git-checks` en su paso `Publish`). Sin el flag, `pnpm publish` rechaza con
|
|
298
|
-
> `ERR_PNPM_GIT_UNCLEAN` — no es una falla real, es el propio swap documentado arriba.
|
|
299
|
-
|
|
300
|
-
El manifest firmado se commitea en el PR, en vez de que el bot lo commitee al branch. Todo lo
|
|
301
|
-
demás del flujo no cambia.
|
|
302
|
-
|
|
303
|
-
**Después de publicar, la wiki.** Copiar la entrada nueva del `CHANGELOG.md` **arriba** de la
|
|
304
|
-
página hija [Historial de cambios](https://wiki.alegra.com/doc/historial-de-cambios-J8uMHq1sfd) y
|
|
305
|
-
actualizar la línea **Estado** del home de la wiki (versión, skills, fecha). El home no lleva
|
|
306
|
-
historial: solo el enlace a esa página, para que no crezca con cada release. El CHANGELOG del repo
|
|
307
|
-
es la fuente; la wiki lo copia, nunca al revés.
|
|
308
|
-
|
|
309
|
-
### Publicación de nueva versión del CLI
|
|
310
|
-
|
|
311
|
-
1. Bump `version` en `package.json` y `gateway/package.json` + entrada en `CHANGELOG.md` + regenerar el manifest con **`pnpm run manifest`** (el pipeline completo: `validate` → `scan` → `generate` → `sign`; el `productVersion` sale de package.json). Todo dentro del mismo PR del release.
|
|
312
|
-
2. Tras el merge a main, tag sobre el merge commit: `git tag -s vX.Y.Z && git push origin vX.Y.Z`.
|
|
313
|
-
3. **Con Actions apagado este paso no ocurre:** el tag no publica nada y el publish se hace a mano (ver «Release sin Actions»). Cuando Actions vuelva, el workflow `publish.yml` valida que `package.json` coincida con el tag, corre los gates (validate → scan → tests) y publica: build → swap a `.github/README-npm.md` (README mínimo del tarball) → `pnpm publish --access public`. Requiere el secret `NPM_TOKEN` configurado.
|
|
314
|
-
|
|
315
|
-
> ⚠️ **Nunca corras `manifest:generate` solo.** Borra el bloque `signature` de
|
|
316
|
-
> `agent/skills/index.json`, y `verifyManifestSignature` es hard-fail: un manifest sin firma hace
|
|
317
|
-
> que el CLI rechace **toda** instalación y update, no solo la skill que tocaste. Siempre
|
|
318
|
-
> `pnpm run manifest`, que firma al final. `tests/security/manifest-committed.test.mjs` lo caza en
|
|
319
|
-
> local si se te pasa.
|
|
320
|
-
|
|
321
|
-
**Si el release sale mal:** mover el dist-tag `latest` hacia atrás **no revierte a nadie** — el
|
|
322
|
-
hook resuelve por rango semver y los tags no participan. El procedimiento real está en
|
|
323
|
-
[`docs/rollback.md`](./docs/rollback.md).
|
|
324
|
-
|
|
325
|
-
### Local
|
|
326
|
-
|
|
327
|
-
```sh
|
|
328
|
-
pnpm run manifest:scan # buscar prompt-injection / credenciales
|
|
329
|
-
pnpm run manifest:generate # regenera agent/skills/index.json
|
|
330
|
-
pnpm run manifest:sign # firma (requiere ALEGRA_SIGNING_KEY)
|
|
331
|
-
pnpm run manifest # los cuatro en orden (validate → scan → generate → sign)
|
|
332
|
-
|
|
333
|
-
pnpm install
|
|
334
|
-
pnpm typecheck
|
|
335
|
-
pnpm build # → dist/
|
|
336
|
-
|
|
337
|
-
pnpm test # Fase 1: SOLO valida el schema de los evals — no
|
|
338
|
-
# ejecuta ningún agente
|
|
339
|
-
pnpm test:security # la suite real: firma, paths, clientes, drift de MCPs
|
|
340
|
-
pnpm test:all # ambos
|
|
341
|
-
|
|
342
|
-
pnpm run eval:run -- --local # Fase 2: ejecuta los evals de verdad contra las skills
|
|
343
|
-
# del repo. Cuesta tokens y tarda horas (116 casos); para
|
|
344
|
-
# un caso puntual: --only <nombre-del-caso>
|
|
345
|
-
|
|
346
|
-
pnpm run verify:clients # E2E: instala en un HOME temporal con 5 de los 6 clientes
|
|
347
|
-
# simulados (Hermes aún no está cubierto) y verifica rutas, shapes de MCP, hooks
|
|
348
|
-
# e idempotencia (requiere sesión y red)
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
> **`test:all` en verde no dice que las skills respondan bien.** La Fase 1 solo lee los YAML;
|
|
352
|
-
> si tardó un segundo, no ejecutó nada. Medir routing, tablas y números es `eval:run`: corre cada
|
|
353
|
-
> caso aislado (sin el repo ni la memoria del dev a la vista) y consulta Metabase de verdad.
|
|
354
|
-
> Detalle en [`docs/evals.md`](./docs/evals.md).
|
|
355
|
-
|
|
356
|
-
El grueso de la verificación corre en local, no en CI: la organización está en plan Free y el
|
|
357
|
-
pool de minutos de Actions es compartido. `pre-commit install && pre-commit install --hook-type
|
|
358
|
-
pre-push` deja los mismos gates enganchados a los commits.
|
|
359
|
-
|
|
360
|
-
### Keypair de firma
|
|
361
|
-
|
|
362
|
-
```sh
|
|
363
|
-
pnpm run signing-key:generate # escribe la privada a un archivo 600; NO la imprime
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
La pública va en `TRUSTED_KEYS` (`src/signature.ts`); la privada, **en dos lugares**: el gestor
|
|
367
|
-
de contraseñas del equipo y el secret `ALEGRA_SIGNING_KEY` del repo. Un secret de GitHub no se
|
|
368
|
-
puede leer: si es la única copia, se pierde en cuanto haga falta usarla en otro lado — pasó con
|
|
369
|
-
la clave v1 y obligó a rotar.
|
|
370
|
-
|
|
371
|
-
El CLI acepta **varias claves a la vez**, que es lo que permite rotar sin ventana de rotura: con
|
|
372
|
-
una sola, el CLI nuevo rechaza el manifest viejo *y* el viejo rechaza el nuevo. Procedimiento
|
|
373
|
-
completo en [SECURITY.md](SECURITY.md#custodia-y-rotación-de-la-clave-de-firma).
|
|
374
|
-
|
|
375
|
-
## Estructura del repo
|
|
376
|
-
|
|
377
|
-
```
|
|
378
|
-
data-agent-selfservice/
|
|
379
|
-
├── src/ # CLI TypeScript (publicado a npm como dist/)
|
|
380
|
-
├── scripts/ # generate-manifest, sign-manifest, scan-skills, generate-signing-key, verify-clients
|
|
381
|
-
├── agent/
|
|
382
|
-
│ ├── skills/ # SKILL.md + anexos, agrupados por dueño
|
|
383
|
-
│ └── mcps/ # config.example.json por MCP
|
|
384
|
-
├── gateway/ # Next.js en Vercel — OAuth + manifest firmado + skills
|
|
385
|
-
├── docs/ # ADRs + guías
|
|
386
|
-
├── tests/ # evals + security tests
|
|
387
|
-
├── install.sh # bootstrap para macOS/Linux/WSL (servido por gateway)
|
|
388
|
-
└── install.ps1 # bootstrap para Windows (servido por gateway)
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
## Troubleshooting
|
|
392
|
-
|
|
393
|
-
| Síntoma | Probable causa | Fix |
|
|
394
|
-
|---|---|---|
|
|
395
|
-
| `pnpm: command not found` | pnpm no instalado | `corepack enable && corepack prepare pnpm@11.1.2 --activate` |
|
|
396
|
-
| `Node.js >= 22.6.0` requerido | Node viejo | `nvm install 22` o reinstala desde nodejs.org |
|
|
397
|
-
| `Solo cuentas @alegra.com` | OAuth con cuenta personal | Login con tu cuenta corporativa |
|
|
398
|
-
| El cliente no ve los MCPs | Cambio de config no recargado | Reinicia el cliente |
|
|
399
|
-
| `keyId no reconocido` | El CLI no conoce la clave con la que se firmó el manifest (rotación en curso y CLI viejo) | `pnpm dlx alegra-data-agent@latest`. Durante una rotación el CLI acepta la clave nueva y la anterior, así que esto solo aparece en versiones muy atrasadas |
|
|
400
|
-
| Una versión recién publicada no se instala / no actualiza | Cooldown de pnpm 11 (`minimumReleaseAge`, ~24h) sobre versiones de <24h | Desde 0.2.2 el CLI hace self-bounce: aunque pnpm sirva una versión anterior, el comando detecta la última en el registry y se re-ejecuta con ella solo. El self-update (hook) y el bootstrap además corren con `--config.minimumReleaseAge=0`. Para forzarlo a mano: `pnpm dlx alegra-data-agent@<versión>` (pin exacto) |
|
|
401
|
-
|
|
402
|
-
Si nada funciona: la skill `agent-onboarding` (instalada por defecto) tiene una guía paso a paso de troubleshooting que el modelo puede correr contigo.
|
|
403
|
-
|
|
404
|
-
## Branching
|
|
405
|
-
|
|
406
|
-
`<nombre>-changes → testing → main`. Cada quien trabaja en su rama personal (`jhon-changes`,
|
|
407
|
-
`luis-changes`…) y llega por PR: primero a `testing`, después a `main`. Nada entra directo a
|
|
408
|
-
`main`, y **el PR a `main` lo aprueba y mergea otra persona** — nadie mergea el suyo. Detalle en
|
|
409
|
-
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
410
|
-
|
|
411
|
-
Cambios a `agent/skills/**` o `agent/mcps/**` disparan el workflow de firma en el PR hacia `main`
|
|
412
|
-
(el manifest firmado se commitea al branch del PR antes del merge). Cambios a `scripts/**.mjs`
|
|
413
|
-
requieren review humano: no autoejecutan la firma, para que un PR a los scripts no pueda
|
|
414
|
-
exfiltrar la clave.
|
|
415
|
-
|
|
416
|
-
> En plan Free los repos privados no tienen rulesets, así que esto es **un acuerdo del equipo,
|
|
417
|
-
> no un candado técnico**. Lo mismo vale para los commits firmados. Ver
|
|
418
|
-
> [Cómo trabajamos en GitHub (Alegra-Team)](https://wiki.alegra.com/doc/como-trabajamos-en-github-alegra-team-2WuRXpYEmg).
|
|
419
|
-
|
|
420
|
-
## CI / Workflows
|
|
421
|
-
|
|
422
|
-
> ⚠️ **Hoy Actions está deshabilitado en este repo.** La organización agotó su presupuesto
|
|
423
|
-
> (`the job was not started because recent account payments have failed or your spending limit
|
|
424
|
-
> needs to be increased`), así que ningún workflow puede correr y dejarlos activos solo llenaría
|
|
425
|
-
> los PRs de checks rojos que no dicen nada del código. **Mientras dure, todo es local**: la
|
|
426
|
-
> validación con pre-commit/pre-push, y la firma del manifest y la publicación a npm a mano —
|
|
427
|
-
> ver «Release sin Actions» abajo. Para reactivarlos cuando haya presupuesto:
|
|
428
|
-
> `gh api -X PUT /repos/Alegra-Team/data-agent-selfservice/actions/permissions -F enabled=true`.
|
|
429
|
-
|
|
430
|
-
Cuatro workflows, y solo uno corre por PR. El pool de minutos de Actions es compartido entre
|
|
431
|
-
todos los repos de la organización (plan Free), así que en GitHub queda únicamente lo que no se
|
|
432
|
-
puede garantizar en la máquina del autor; el resto vive en `.pre-commit-config.yaml`.
|
|
433
|
-
|
|
434
|
-
| Workflow | Cuándo dispara | Qué hace |
|
|
435
|
-
|---|---|---|
|
|
436
|
-
| `pr.yml` | PRs a `testing` y a `main` (`paths-ignore` docs-only) | **Un solo job**: gates pnpm-only, standards de skills/MCPs (strict), scan de secretos y prompt injection, frescura del manifest, typecheck y tests. El typecheck del gateway solo si el PR toca `gateway/` |
|
|
437
|
-
| `sign-manifest.yml` | PRs a `main` que tocan `agent/skills/**` o `agent/mcps/**` | Regenera + escanea + firma el manifest y lo commitea al branch del PR vía REST API de GitHub, no con `git push`: así queda firmado server-side y Verified. Necesita el secret `ALEGRA_SIGNING_KEY` |
|
|
438
|
-
| `security.yml` | Schedule semanal + `workflow_dispatch` | Gitleaks + OSV + Trivy + pnpm audit. CodeQL solo bajo dispatch explícito (es el job más caro). No corre por push ni por PR |
|
|
439
|
-
| `publish.yml` | Tag `v*.*.*` | Valida versión == tag + gates + publica el CLI a npmjs.com. Necesita el secret `NPM_TOKEN` |
|
|
15
|
+
- Node ≥ 22.6.0
|
|
16
|
+
- pnpm ≥ 11.1.2
|
|
17
|
+
- Uno de: Claude Code, Cursor, OpenCode, Antigravity, Codex, Hermes Agent
|
|
440
18
|
|
|
441
|
-
|
|
442
|
-
partir la validación en cuatro costaba cuatro minutos aunque dos de ellos fueran un `grep` de
|
|
443
|
-
tres segundos.
|
|
19
|
+
## Soporte
|
|
444
20
|
|
|
445
|
-
|
|
21
|
+
Guía de uso, documentación y soporte para el equipo de Alegra: entra a https://alegra-data-agent.vercel.app con tu cuenta `@alegra.com`.
|
package/dist/autoupdate.js
CHANGED
|
@@ -4,7 +4,7 @@ import { logWarn } from "./colors.js";
|
|
|
4
4
|
import { hookCommandFor, runHookInstall, runHookUninstall, } from "./commands/hook.js";
|
|
5
5
|
import { HERMES_PLUGIN_NAME, hermesPluginManifest, hermesPluginModule } from "./hermes-plugin.js";
|
|
6
6
|
import { runHermesCliAt } from "./installer.js";
|
|
7
|
-
import { COMMAND_NAME, getClaudeSettingsPath, getCodexHooksPath, getCursorHooksPath, getGeminiSettingsPath, getHermesHome, getHermesPluginsDir, getOpenCodePluginPath, readLockFile, } from "./lib.js";
|
|
7
|
+
import { COMMAND_NAME, getAntigravityHooksPath, getClaudeSettingsPath, getCodexHooksPath, getCursorHooksPath, getGeminiSettingsPath, getHermesHome, getHermesPluginsDir, getOpenCodePluginPath, readLockFile, } from "./lib.js";
|
|
8
8
|
function readJsonSafe(path) {
|
|
9
9
|
if (!existsSync(path))
|
|
10
10
|
return {};
|
|
@@ -60,6 +60,7 @@ export function guiResilientCommand(base, opts) {
|
|
|
60
60
|
`else mkdir -p "$(dirname ${logFile})" && ` +
|
|
61
61
|
`echo "$(date '+%Y-%m-%d %H:%M:%S') [${opts.client}] pnpm no esta en el PATH del cliente; auto-update omitido" >> ${logFile}; fi`);
|
|
62
62
|
}
|
|
63
|
+
const CURSOR_HOOK_EVENTS = ["workspaceOpen", "sessionStart"];
|
|
63
64
|
function installCursor(command) {
|
|
64
65
|
const path = getCursorHooksPath();
|
|
65
66
|
const cfg = readJsonSafe(path);
|
|
@@ -69,9 +70,11 @@ function installCursor(command) {
|
|
|
69
70
|
}
|
|
70
71
|
if (typeof cfg.version !== "number")
|
|
71
72
|
cfg.version = 1;
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
for (const event of CURSOR_HOOK_EVENTS) {
|
|
74
|
+
const list = freshHookList(cfg, event);
|
|
75
|
+
list.push({ command, timeout: 120 });
|
|
76
|
+
cfg.hooks[event] = list;
|
|
77
|
+
}
|
|
75
78
|
writeJson(path, cfg);
|
|
76
79
|
}
|
|
77
80
|
function installCodex(command) {
|
|
@@ -81,34 +84,74 @@ function installCodex(command) {
|
|
|
81
84
|
logWarn(`${path} no es JSON válido; salteo el auto-update de Codex.`);
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
87
|
+
const before = JSON.stringify(cfg);
|
|
84
88
|
const list = freshHookList(cfg, "SessionStart");
|
|
85
89
|
list.push({ matcher: "startup|resume", hooks: [{ type: "command", command }] });
|
|
86
90
|
cfg.hooks.SessionStart = list;
|
|
91
|
+
if (JSON.stringify(cfg) === before)
|
|
92
|
+
return;
|
|
87
93
|
writeJson(path, cfg);
|
|
88
94
|
}
|
|
95
|
+
const ANTIGRAVITY_HOOK_NAME = "alegra-data-agent-autoupdate";
|
|
96
|
+
export function antigravityHookCommand(resilient, platform = process.platform) {
|
|
97
|
+
if (platform === "win32")
|
|
98
|
+
return resilient;
|
|
99
|
+
return `(${resilient}) </dev/null >/dev/null 2>&1 & echo '{}'`;
|
|
100
|
+
}
|
|
89
101
|
function installAntigravity(command) {
|
|
90
|
-
const path =
|
|
102
|
+
const path = getAntigravityHooksPath();
|
|
91
103
|
const cfg = readJsonSafe(path);
|
|
92
104
|
if (cfg === null) {
|
|
93
105
|
logWarn(`${path} no es JSON válido; salteo el auto-update de Antigravity.`);
|
|
94
106
|
return;
|
|
95
107
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
cfg[ANTIGRAVITY_HOOK_NAME] = {
|
|
109
|
+
SessionStart: [{ type: "command", command, timeout: 120 }],
|
|
110
|
+
};
|
|
99
111
|
writeJson(path, cfg);
|
|
112
|
+
removeJsonHook(getGeminiSettingsPath(), "SessionStart");
|
|
113
|
+
}
|
|
114
|
+
function removeAntigravityHook() {
|
|
115
|
+
const path = getAntigravityHooksPath();
|
|
116
|
+
const cfg = readJsonSafe(path);
|
|
117
|
+
if (cfg && ANTIGRAVITY_HOOK_NAME in cfg) {
|
|
118
|
+
delete cfg[ANTIGRAVITY_HOOK_NAME];
|
|
119
|
+
writeJson(path, cfg);
|
|
120
|
+
}
|
|
121
|
+
removeJsonHook(getGeminiSettingsPath(), "SessionStart");
|
|
122
|
+
}
|
|
123
|
+
function antigravityHookIsOurs() {
|
|
124
|
+
const cfg = readJsonSafe(getAntigravityHooksPath());
|
|
125
|
+
return Boolean(cfg && ANTIGRAVITY_HOOK_NAME in cfg);
|
|
100
126
|
}
|
|
101
127
|
function installOpenCode(command) {
|
|
102
128
|
const path = getOpenCodePluginPath();
|
|
103
129
|
mkdirSync(dirname(path), { recursive: true });
|
|
104
130
|
const plugin = `// alegra-data-agent: auto-update al iniciar OpenCode. Generado por el instalador.
|
|
105
131
|
// Borrar este archivo desactiva el auto-update. No editar a mano.
|
|
106
|
-
|
|
107
|
-
|
|
132
|
+
import { spawn } from "node:child_process"
|
|
133
|
+
|
|
134
|
+
const COMMAND = ${JSON.stringify(command)}
|
|
135
|
+
|
|
136
|
+
function runUpdate() {
|
|
108
137
|
try {
|
|
109
|
-
|
|
138
|
+
const child = spawn(COMMAND, { shell: true, detached: true, stdio: "ignore", windowsHide: true })
|
|
139
|
+
child.on("error", () => {})
|
|
140
|
+
child.unref()
|
|
110
141
|
} catch (_e) {}
|
|
111
|
-
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default {
|
|
145
|
+
id: "alegra-data-agent-autoupdate",
|
|
146
|
+
// OpenCode 1.x
|
|
147
|
+
server: async () => {
|
|
148
|
+
runUpdate()
|
|
149
|
+
return {}
|
|
150
|
+
},
|
|
151
|
+
// OpenCode 2.x
|
|
152
|
+
setup: async () => {
|
|
153
|
+
runUpdate()
|
|
154
|
+
},
|
|
112
155
|
}
|
|
113
156
|
`;
|
|
114
157
|
writeFileSync(path, plugin, "utf-8");
|
|
@@ -187,7 +230,10 @@ export function installAutoUpdate(target, mode = "auto") {
|
|
|
187
230
|
installCodex(hookCommandFor(effective));
|
|
188
231
|
break;
|
|
189
232
|
case "antigravity":
|
|
190
|
-
installAntigravity(guiResilientCommand(hookCommandFor(effective), {
|
|
233
|
+
installAntigravity(antigravityHookCommand(guiResilientCommand(hookCommandFor(effective), {
|
|
234
|
+
client: "antigravity",
|
|
235
|
+
silence: true,
|
|
236
|
+
})));
|
|
191
237
|
break;
|
|
192
238
|
case "opencode":
|
|
193
239
|
installOpenCode(hookCommandFor(effective));
|
|
@@ -208,13 +254,14 @@ export function removeAutoUpdate(target) {
|
|
|
208
254
|
runHookUninstall({ quiet: true });
|
|
209
255
|
break;
|
|
210
256
|
case "cursor":
|
|
211
|
-
|
|
257
|
+
for (const event of CURSOR_HOOK_EVENTS)
|
|
258
|
+
removeJsonHook(getCursorHooksPath(), event);
|
|
212
259
|
break;
|
|
213
260
|
case "codex":
|
|
214
261
|
removeJsonHook(getCodexHooksPath(), "SessionStart");
|
|
215
262
|
break;
|
|
216
263
|
case "antigravity":
|
|
217
|
-
|
|
264
|
+
removeAntigravityHook();
|
|
218
265
|
break;
|
|
219
266
|
case "opencode": {
|
|
220
267
|
const p = getOpenCodePluginPath();
|
|
@@ -243,11 +290,11 @@ export function hasAutoUpdate(target) {
|
|
|
243
290
|
case "claude-code":
|
|
244
291
|
return jsonHookHasOurs(getClaudeSettingsPath(), "SessionStart");
|
|
245
292
|
case "cursor":
|
|
246
|
-
return jsonHookHasOurs(getCursorHooksPath(),
|
|
293
|
+
return CURSOR_HOOK_EVENTS.some((event) => jsonHookHasOurs(getCursorHooksPath(), event));
|
|
247
294
|
case "codex":
|
|
248
295
|
return jsonHookHasOurs(getCodexHooksPath(), "SessionStart");
|
|
249
296
|
case "antigravity":
|
|
250
|
-
return jsonHookHasOurs(getGeminiSettingsPath(), "SessionStart");
|
|
297
|
+
return antigravityHookIsOurs() || jsonHookHasOurs(getGeminiSettingsPath(), "SessionStart");
|
|
251
298
|
case "opencode":
|
|
252
299
|
return existsSync(getOpenCodePluginPath());
|
|
253
300
|
case "hermes": {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { COMMAND_NAME, getConfigDir } from "./lib.js";
|
|
7
|
+
function consentPath() {
|
|
8
|
+
return join(getConfigDir(), "codex-hook-trust.json");
|
|
9
|
+
}
|
|
10
|
+
export function hasCodexTrustConsent() {
|
|
11
|
+
return existsSync(consentPath());
|
|
12
|
+
}
|
|
13
|
+
export function saveCodexTrustConsent() {
|
|
14
|
+
const path = consentPath();
|
|
15
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
16
|
+
writeFileSync(path, `${JSON.stringify({ consented: true, at: new Date().toISOString() })}\n`);
|
|
17
|
+
}
|
|
18
|
+
function codexBin() {
|
|
19
|
+
const bin = process.env.CODEX_BIN;
|
|
20
|
+
return bin && bin.length > 0 ? bin : "codex";
|
|
21
|
+
}
|
|
22
|
+
async function withAppServer(fn, timeoutMs) {
|
|
23
|
+
const child = spawn(codexBin(), ["app-server"], {
|
|
24
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
25
|
+
windowsHide: true,
|
|
26
|
+
shell: process.platform === "win32",
|
|
27
|
+
});
|
|
28
|
+
const pending = new Map();
|
|
29
|
+
let nextId = 0;
|
|
30
|
+
const fail = (err) => {
|
|
31
|
+
for (const p of pending.values())
|
|
32
|
+
p.reject(err);
|
|
33
|
+
pending.clear();
|
|
34
|
+
};
|
|
35
|
+
child.on("error", (err) => fail(err));
|
|
36
|
+
child.on("exit", () => fail(new Error("codex app-server terminó antes de responder")));
|
|
37
|
+
child.stdin.on("error", (err) => fail(err));
|
|
38
|
+
const rl = createInterface({ input: child.stdout });
|
|
39
|
+
rl.on("line", (line) => {
|
|
40
|
+
let msg;
|
|
41
|
+
try {
|
|
42
|
+
msg = JSON.parse(line);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (typeof msg.id !== "number")
|
|
48
|
+
return;
|
|
49
|
+
const p = pending.get(msg.id);
|
|
50
|
+
if (!p)
|
|
51
|
+
return;
|
|
52
|
+
pending.delete(msg.id);
|
|
53
|
+
if (msg.error)
|
|
54
|
+
p.reject(new Error(msg.error.message ?? "error de codex app-server"));
|
|
55
|
+
else
|
|
56
|
+
p.resolve(msg.result);
|
|
57
|
+
});
|
|
58
|
+
const req = (method, params) => new Promise((resolve, reject) => {
|
|
59
|
+
const id = ++nextId;
|
|
60
|
+
pending.set(id, { resolve, reject });
|
|
61
|
+
child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
62
|
+
});
|
|
63
|
+
const timer = setTimeout(() => fail(new Error("codex app-server no respondió a tiempo")), timeoutMs);
|
|
64
|
+
try {
|
|
65
|
+
await req("initialize", {
|
|
66
|
+
clientInfo: { name: COMMAND_NAME, version: "1" },
|
|
67
|
+
capabilities: { experimentalApi: true },
|
|
68
|
+
});
|
|
69
|
+
child.stdin.write(`${JSON.stringify({ method: "initialized" })}\n`);
|
|
70
|
+
return await fn(req);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
rl.close();
|
|
75
|
+
child.stdout.destroy();
|
|
76
|
+
child.kill();
|
|
77
|
+
const force = setTimeout(() => child.kill("SIGKILL"), 2_000);
|
|
78
|
+
force.unref();
|
|
79
|
+
child.unref();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function isOurCommand(command) {
|
|
83
|
+
return new RegExp(`${COMMAND_NAME}@\\S+ update\\b`).test(command);
|
|
84
|
+
}
|
|
85
|
+
async function ourHooks(req) {
|
|
86
|
+
const res = (await req("hooks/list", { cwds: [homedir()] }));
|
|
87
|
+
return (res.data ?? [])
|
|
88
|
+
.flatMap((d) => d.hooks ?? [])
|
|
89
|
+
.filter((h) => h.source === "user" && isOurCommand(h.command ?? ""));
|
|
90
|
+
}
|
|
91
|
+
export async function approveCodexHook(timeoutMs = 20_000) {
|
|
92
|
+
try {
|
|
93
|
+
return await withAppServer(async (req) => {
|
|
94
|
+
const needs = (await ourHooks(req)).filter((h) => h.trustStatus !== "trusted" && h.trustStatus !== "managed" && h.currentHash);
|
|
95
|
+
if (needs.length > 0) {
|
|
96
|
+
await req("config/batchWrite", {
|
|
97
|
+
edits: [
|
|
98
|
+
{
|
|
99
|
+
keyPath: "hooks.state",
|
|
100
|
+
value: Object.fromEntries(needs.map((h) => [h.key, { trusted_hash: h.currentHash }])),
|
|
101
|
+
mergeStrategy: "upsert",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
reloadUserConfig: true,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const after = await ourHooks(req);
|
|
108
|
+
const pending = after.filter((h) => h.trustStatus !== "trusted" && h.trustStatus !== "managed").length;
|
|
109
|
+
return { ok: pending === 0 && after.length > 0, approved: needs.length, pending };
|
|
110
|
+
}, timeoutMs);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
approved: 0,
|
|
116
|
+
pending: 0,
|
|
117
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export async function codexHookTrustStatus(timeoutMs = 20_000) {
|
|
122
|
+
try {
|
|
123
|
+
return await withAppServer(async (req) => (await ourHooks(req)).map((h) => h.trustStatus ?? "unknown"), timeoutMs);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
package/dist/commands/install.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { AuthRequiredError } from "../api.js";
|
|
2
2
|
import { hasValidAuth, readAuth } from "../auth.js";
|
|
3
3
|
import { installAutoUpdate } from "../autoupdate.js";
|
|
4
|
+
import { approveCodexHook, hasCodexTrustConsent, saveCodexTrustConsent } from "../codex-trust.js";
|
|
4
5
|
import { bold, cyan, dim, gray, green, log, logError, red, yellow } from "../colors.js";
|
|
5
6
|
import { installAll } from "../installer.js";
|
|
6
7
|
import { COMMAND_NAME, formatBytes, getPackageVersion, getTargetSkillsDir } from "../lib.js";
|
|
8
|
+
import { confirm } from "../ui.js";
|
|
7
9
|
import { runLogin } from "./login.js";
|
|
8
10
|
const BANNER_WIDTH = 58;
|
|
9
11
|
const SHORT_DESCRIPTIONS = {
|
|
@@ -147,6 +149,37 @@ function printNextSteps(noHook) {
|
|
|
147
149
|
function isClaudeCode() {
|
|
148
150
|
return process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE === "1";
|
|
149
151
|
}
|
|
152
|
+
async function setUpCodex(manifest, results, opts) {
|
|
153
|
+
if (!opts.noHook) {
|
|
154
|
+
let consent = hasCodexTrustConsent();
|
|
155
|
+
if (!consent && !opts.yes && process.stdin.isTTY && process.stdout.isTTY) {
|
|
156
|
+
log(` ${bold("Codex")} pide aprobar cada hook antes de ejecutarlo. El nuestro solo corre ${cyan(`${COMMAND_NAME} update`)} al abrir una sesión.`);
|
|
157
|
+
consent = await confirm(" ¿Lo apruebo para que el auto-update funcione en Codex?", true);
|
|
158
|
+
if (consent)
|
|
159
|
+
saveCodexTrustConsent();
|
|
160
|
+
}
|
|
161
|
+
const res = consent ? await approveCodexHook() : null;
|
|
162
|
+
if (res?.ok) {
|
|
163
|
+
log(` ${green("✓")} Codex: auto-update aprobado.`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
if (res?.detail)
|
|
167
|
+
log(` ${gray(`(no pude aprobarlo solo: ${res.detail})`)}`);
|
|
168
|
+
log(` ${bold(yellow("Codex:"))} al abrirlo elegí ${bold("«Trust all and continue»")} ${gray("(o /hooks)")} para activar el auto-update.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const installedMcps = new Set(results.filter((r) => r.kind === "mcp" && r.status !== "failed").map((r) => r.name));
|
|
172
|
+
const remotes = Object.entries(manifest.mcps ?? {})
|
|
173
|
+
.filter(([name, m]) => m.url && installedMcps.has(name))
|
|
174
|
+
.map(([name]) => name)
|
|
175
|
+
.sort();
|
|
176
|
+
if (remotes.length > 0) {
|
|
177
|
+
log(` ${gray("Conectá cada MCP de Codex una vez:")}`);
|
|
178
|
+
for (const name of remotes)
|
|
179
|
+
log(` ${cyan(`codex mcp login ${name}`)}`);
|
|
180
|
+
}
|
|
181
|
+
log("");
|
|
182
|
+
}
|
|
150
183
|
export async function runInstall(opts = {}) {
|
|
151
184
|
const compact = isClaudeCode();
|
|
152
185
|
if (!compact)
|
|
@@ -243,6 +276,8 @@ export async function runInstall(opts = {}) {
|
|
|
243
276
|
if (!compact)
|
|
244
277
|
log("");
|
|
245
278
|
}
|
|
279
|
+
if (result.targets.includes("codex"))
|
|
280
|
+
await setUpCodex(result.manifest, result.results, opts);
|
|
246
281
|
if (hasFails)
|
|
247
282
|
return 1;
|
|
248
283
|
if (!compact) {
|
package/dist/commands/status.js
CHANGED
|
@@ -3,6 +3,8 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { AuthRequiredError, fetchManifest } from "../api.js";
|
|
5
5
|
import { readAuth } from "../auth.js";
|
|
6
|
+
import { hasAutoUpdate } from "../autoupdate.js";
|
|
7
|
+
import { codexHookTrustStatus } from "../codex-trust.js";
|
|
6
8
|
import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
|
|
7
9
|
import { detectDrift, findMisplacedMcps, inspectMcpInTarget } from "../installer.js";
|
|
8
10
|
import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeJsonPath, getClaudeSkillsDir, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.js";
|
|
@@ -27,6 +29,13 @@ function reportAutoUpdateLog() {
|
|
|
27
29
|
catch {
|
|
28
30
|
}
|
|
29
31
|
}
|
|
32
|
+
async function reportCodexHookTrust() {
|
|
33
|
+
const states = await codexHookTrustStatus();
|
|
34
|
+
if (!states || states.every((s) => s === "trusted" || s === "managed"))
|
|
35
|
+
return;
|
|
36
|
+
logWarn(`El auto-update de Codex está sin aprobar (${states.join(", ")}): Codex no lo ejecuta. Abrí Codex y elegí «Trust all and continue» (o /hooks), o corré ${bold(`${COMMAND_NAME} install --target=codex`)} y aceptá la aprobación.`);
|
|
37
|
+
log("");
|
|
38
|
+
}
|
|
30
39
|
function reportSkillBackups() {
|
|
31
40
|
const backupsLog = join(dirname(getAutoUpdateLogPath()), "backups", "backups.log");
|
|
32
41
|
if (!existsSync(backupsLog))
|
|
@@ -309,6 +318,8 @@ export async function runStatus() {
|
|
|
309
318
|
log("");
|
|
310
319
|
reportAutoUpdateLog();
|
|
311
320
|
reportSkillBackups();
|
|
321
|
+
if (installedTargets.includes("codex") && hasAutoUpdate("codex"))
|
|
322
|
+
await reportCodexHookTrust();
|
|
312
323
|
if (!auth) {
|
|
313
324
|
logWarn(`Inicia sesión con: ${bold(`${COMMAND_NAME} login`)}`);
|
|
314
325
|
log("");
|
package/dist/commands/update.js
CHANGED
|
@@ -2,7 +2,8 @@ import { appendFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { AuthRequiredError, ensureFreshAuth, fetchManifest } from "../api.js";
|
|
4
4
|
import { hasValidAuth } from "../auth.js";
|
|
5
|
-
import { refreshAutoUpdate } from "../autoupdate.js";
|
|
5
|
+
import { hasAutoUpdate, refreshAutoUpdate } from "../autoupdate.js";
|
|
6
|
+
import { approveCodexHook, hasCodexTrustConsent } from "../codex-trust.js";
|
|
6
7
|
import { cyan, dim, gray, green, log, logError, yellow } from "../colors.js";
|
|
7
8
|
import { detectDrift, installAll, summarize } from "../installer.js";
|
|
8
9
|
import { COMMAND_NAME, getAutoUpdateLogPath, getLastCheckPath, getPackageVersion, } from "../lib.js";
|
|
@@ -123,6 +124,11 @@ export async function runUpdate(opts = {}) {
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
}
|
|
127
|
+
async function reapproveCodexHook() {
|
|
128
|
+
if (!hasCodexTrustConsent() || !hasAutoUpdate("codex"))
|
|
129
|
+
return;
|
|
130
|
+
await approveCodexHook();
|
|
131
|
+
}
|
|
126
132
|
async function runUpdateOnce(opts) {
|
|
127
133
|
const manifest = await fetchManifest();
|
|
128
134
|
if (!manifest) {
|
|
@@ -142,6 +148,7 @@ async function runUpdateOnce(opts) {
|
|
|
142
148
|
markChecked();
|
|
143
149
|
if (!opts.dryRun) {
|
|
144
150
|
refreshAutoUpdate();
|
|
151
|
+
await reapproveCodexHook();
|
|
145
152
|
clearAutoUpdateLog();
|
|
146
153
|
}
|
|
147
154
|
return 0;
|
|
@@ -173,6 +180,7 @@ async function runUpdateOnce(opts) {
|
|
|
173
180
|
}
|
|
174
181
|
summarize(result.results);
|
|
175
182
|
refreshAutoUpdate();
|
|
183
|
+
await reapproveCodexHook();
|
|
176
184
|
const failed = result.results.some((r) => r.status === "failed");
|
|
177
185
|
if (!failed) {
|
|
178
186
|
clearAutoUpdateLog();
|
package/dist/installer.js
CHANGED
|
@@ -5,7 +5,7 @@ import { basename, dirname, join, resolve, sep } from "node:path";
|
|
|
5
5
|
import { AuthRequiredError, fetchManifest, fetchSkillFile, recordManifestFloor } from "./api.js";
|
|
6
6
|
import { dim, gray, green, logError, logWarn, red, yellow } from "./colors.js";
|
|
7
7
|
import { ensureLazyProxy, lazyMcpDir, lazyProxyPath, lazyToolsPath, stdioLaunch, } from "./lazy-mcp.js";
|
|
8
|
-
import { computeBundleHash, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getConfigDir, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getOpenCodeLegacyPromptsDir, getPackageVersion, getUserScopeMcpConfigPath, isMcpScopeEnvOverride, readLockFile, sha256File, walkSkillFiles, writeLockFile, } from "./lib.js";
|
|
8
|
+
import { computeBundleHash, ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityGeminiSkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getConfigDir, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getOpenCodeLegacyPromptsDir, getPackageVersion, getUserScopeMcpConfigPath, isMcpScopeEnvOverride, readLockFile, sha256File, walkSkillFiles, writeLockFile, } from "./lib.js";
|
|
9
9
|
import { renderHermesMcpOps, renderMcpForTarget } from "./render.js";
|
|
10
10
|
import { TARGETS, detectInstalledTargets } from "./targets.js";
|
|
11
11
|
const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -686,15 +686,65 @@ function tomlMarkers(name) {
|
|
|
686
686
|
end: `# <<< alegra-data-agent:${name} <<<`,
|
|
687
687
|
};
|
|
688
688
|
}
|
|
689
|
+
function splitTomlBlock(inner, name) {
|
|
690
|
+
const lines = inner.split("\n");
|
|
691
|
+
const isOurs = (h) => h === `[mcp_servers.${name}]` || h === `[mcp_servers.${name}.env]`;
|
|
692
|
+
const at = lines.findIndex((l) => {
|
|
693
|
+
const t = l.trim();
|
|
694
|
+
return t.startsWith("[") && !isOurs(tomlHeader(t));
|
|
695
|
+
});
|
|
696
|
+
if (at === -1)
|
|
697
|
+
return { ours: inner, foreign: "" };
|
|
698
|
+
return {
|
|
699
|
+
ours: lines.slice(0, at).join("\n").replace(/\n+$/, ""),
|
|
700
|
+
foreign: lines.slice(at).join("\n").replace(/\n+$/, ""),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function tomlHeader(line) {
|
|
704
|
+
return line.replace(/\]\s*#.*$/, "]");
|
|
705
|
+
}
|
|
706
|
+
function dropServerSubtables(content, name) {
|
|
707
|
+
const prefix = `[mcp_servers.${name}.`;
|
|
708
|
+
const out = [];
|
|
709
|
+
let skipping = false;
|
|
710
|
+
let dropped = false;
|
|
711
|
+
for (const line of content.split("\n")) {
|
|
712
|
+
const t = line.trim();
|
|
713
|
+
const isMarker = t.startsWith("# >>> alegra-data-agent:") || t.startsWith("# <<< alegra-data-agent:");
|
|
714
|
+
if (t.startsWith("[") || isMarker)
|
|
715
|
+
skipping = !isMarker && tomlHeader(t).startsWith(prefix);
|
|
716
|
+
if (skipping)
|
|
717
|
+
dropped = true;
|
|
718
|
+
else
|
|
719
|
+
out.push(line);
|
|
720
|
+
}
|
|
721
|
+
return dropped ? out.join("\n").replace(/\n{3,}/g, "\n\n") : content;
|
|
722
|
+
}
|
|
723
|
+
function findTomlBlock(content, name) {
|
|
724
|
+
const { begin, end } = tomlMarkers(name);
|
|
725
|
+
const bi = content.indexOf(begin);
|
|
726
|
+
if (bi === -1)
|
|
727
|
+
return null;
|
|
728
|
+
const ei = content.indexOf(end, bi);
|
|
729
|
+
if (ei === -1)
|
|
730
|
+
return null;
|
|
731
|
+
return {
|
|
732
|
+
start: bi,
|
|
733
|
+
stop: ei + end.length,
|
|
734
|
+
inner: content
|
|
735
|
+
.slice(bi + begin.length, ei)
|
|
736
|
+
.replace(/^\n/, "")
|
|
737
|
+
.replace(/\n$/, ""),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
689
740
|
export function upsertTomlBlock(content, name, block) {
|
|
690
741
|
const { begin, end } = tomlMarkers(name);
|
|
691
742
|
const fenced = `${begin}\n${block}\n${end}`;
|
|
692
|
-
const
|
|
693
|
-
if (
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
743
|
+
const found = findTomlBlock(content, name);
|
|
744
|
+
if (found) {
|
|
745
|
+
const { foreign } = splitTomlBlock(found.inner, name);
|
|
746
|
+
const moved = foreign ? `\n\n${foreign}` : "";
|
|
747
|
+
return content.slice(0, found.start) + fenced + moved + content.slice(found.stop);
|
|
698
748
|
}
|
|
699
749
|
if (content.length === 0)
|
|
700
750
|
return `${fenced}\n`;
|
|
@@ -702,33 +752,24 @@ export function upsertTomlBlock(content, name, block) {
|
|
|
702
752
|
return `${content}${sep}${fenced}\n`;
|
|
703
753
|
}
|
|
704
754
|
export function extractTomlBlock(content, name) {
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
if (bi === -1)
|
|
708
|
-
return null;
|
|
709
|
-
const ei = content.indexOf(end, bi);
|
|
710
|
-
if (ei === -1)
|
|
711
|
-
return null;
|
|
712
|
-
return content
|
|
713
|
-
.slice(bi + begin.length, ei)
|
|
714
|
-
.replace(/^\n/, "")
|
|
715
|
-
.replace(/\n$/, "");
|
|
755
|
+
const found = findTomlBlock(content, name);
|
|
756
|
+
return found ? splitTomlBlock(found.inner, name).ours : null;
|
|
716
757
|
}
|
|
717
758
|
export function removeTomlBlock(content, name) {
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
if (bi === -1)
|
|
759
|
+
const found = findTomlBlock(content, name);
|
|
760
|
+
if (!found)
|
|
721
761
|
return content;
|
|
722
|
-
const
|
|
723
|
-
if (
|
|
724
|
-
return content;
|
|
725
|
-
|
|
726
|
-
let
|
|
762
|
+
const { foreign } = splitTomlBlock(found.inner, name);
|
|
763
|
+
if (foreign) {
|
|
764
|
+
return dropServerSubtables(content.slice(0, found.start) + foreign + content.slice(found.stop), name);
|
|
765
|
+
}
|
|
766
|
+
let start = found.start;
|
|
767
|
+
let stop = found.stop;
|
|
727
768
|
if (content[stop] === "\n")
|
|
728
769
|
stop++;
|
|
729
770
|
if (start > 0 && content[start - 1] === "\n")
|
|
730
771
|
start--;
|
|
731
|
-
return content.slice(0, start) + content.slice(stop);
|
|
772
|
+
return dropServerSubtables(content.slice(0, start) + content.slice(stop), name);
|
|
732
773
|
}
|
|
733
774
|
function registerMcpInTomlBlock(mcp, spec) {
|
|
734
775
|
const existing = existsSync(spec.mcpConfigPath) ? readFileSync(spec.mcpConfigPath, "utf-8") : "";
|
|
@@ -891,6 +932,7 @@ function cleanupLegacySkillArtifacts(spec) {
|
|
|
891
932
|
const legacySkillDirs = [
|
|
892
933
|
getAntigravityLegacySkillsDir(),
|
|
893
934
|
getAntigravityGeminiLegacySkillsDir(),
|
|
935
|
+
getAntigravityGeminiSkillsDir(),
|
|
894
936
|
];
|
|
895
937
|
for (const base of legacySkillDirs) {
|
|
896
938
|
for (const name of [...Object.keys(lock.skills), ...LEGACY_RENAMED_SKILLS]) {
|
|
@@ -1309,10 +1351,6 @@ export function uninstallMcp(name, opts = {}) {
|
|
|
1309
1351
|
export function uninstallAll(opts = {}) {
|
|
1310
1352
|
const lock = readLockFile();
|
|
1311
1353
|
const results = [];
|
|
1312
|
-
for (const name of Object.keys(lock.skills))
|
|
1313
|
-
results.push(uninstallSkill(name, opts));
|
|
1314
|
-
for (const name of Object.keys(lock.mcps ?? {}))
|
|
1315
|
-
results.push(uninstallMcp(name, opts));
|
|
1316
1354
|
if (!opts.dryRun) {
|
|
1317
1355
|
for (const spec of Object.values(TARGETS)) {
|
|
1318
1356
|
try {
|
|
@@ -1321,6 +1359,12 @@ export function uninstallAll(opts = {}) {
|
|
|
1321
1359
|
catch {
|
|
1322
1360
|
}
|
|
1323
1361
|
}
|
|
1362
|
+
}
|
|
1363
|
+
for (const name of Object.keys(lock.skills))
|
|
1364
|
+
results.push(uninstallSkill(name, opts));
|
|
1365
|
+
for (const name of Object.keys(lock.mcps ?? {}))
|
|
1366
|
+
results.push(uninstallMcp(name, opts));
|
|
1367
|
+
if (!opts.dryRun) {
|
|
1324
1368
|
if (Object.keys(readLockFile().mcps ?? {}).length === 0) {
|
|
1325
1369
|
try {
|
|
1326
1370
|
rmSync(lazyMcpDir(), { recursive: true, force: true });
|
package/dist/lib.js
CHANGED
|
@@ -174,6 +174,9 @@ export function getOpenCodeConfigPath() {
|
|
|
174
174
|
export function getAntigravitySkillsDir() {
|
|
175
175
|
return join(homedir(), ".gemini", "antigravity-cli", "skills");
|
|
176
176
|
}
|
|
177
|
+
export function getAntigravityConfigSkillsDir() {
|
|
178
|
+
return join(homedir(), ".gemini", "config", "skills");
|
|
179
|
+
}
|
|
177
180
|
export function getAntigravityGeminiSkillsDir() {
|
|
178
181
|
return join(homedir(), ".gemini", "skills");
|
|
179
182
|
}
|
|
@@ -183,6 +186,9 @@ export function getAntigravityMcpConfigPath() {
|
|
|
183
186
|
export function getGeminiSettingsPath() {
|
|
184
187
|
return join(homedir(), ".gemini", "settings.json");
|
|
185
188
|
}
|
|
189
|
+
export function getAntigravityHooksPath() {
|
|
190
|
+
return join(homedir(), ".gemini", "config", "hooks.json");
|
|
191
|
+
}
|
|
186
192
|
export function getAntigravityLegacySkillsDir() {
|
|
187
193
|
return join(homedir(), ".antigravity", "skills");
|
|
188
194
|
}
|
package/dist/main.js
CHANGED
package/dist/targets.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { getAntigravityConfigSkillsDir, getAntigravityMcpConfigPath, getAntigravitySkillsDir, getClaudeMcpConfigPath, getClaudeSkillsDir, getCodexConfigPath, getCodexSkillsDir, getCursorMcpPath, getCursorSkillsDir, getHermesConfigPath, getHermesHome, getHermesSkillsDir, getOpenCodeConfigPath, getOpenCodeSkillsDir, } from "./lib.js";
|
|
5
5
|
export const TARGETS = {
|
|
6
6
|
"claude-code": {
|
|
7
7
|
id: "claude-code",
|
|
@@ -37,7 +37,7 @@ export const TARGETS = {
|
|
|
37
37
|
id: "antigravity",
|
|
38
38
|
label: "Antigravity",
|
|
39
39
|
skillsDir: getAntigravitySkillsDir(),
|
|
40
|
-
mirrorSkillsDirs: [
|
|
40
|
+
mirrorSkillsDirs: [getAntigravityConfigSkillsDir()],
|
|
41
41
|
skillFormat: "folder",
|
|
42
42
|
mcpConfigPath: getAntigravityMcpConfigPath(),
|
|
43
43
|
mcpRootKey: "mcpServers",
|
|
@@ -76,7 +76,11 @@ export function detectInstalledTargets() {
|
|
|
76
76
|
"claude-code": [join(home, ".claude")],
|
|
77
77
|
cursor: [join(home, ".cursor")],
|
|
78
78
|
opencode: [join(xdgConfig, "opencode")],
|
|
79
|
-
antigravity: [
|
|
79
|
+
antigravity: [
|
|
80
|
+
join(home, ".gemini", "antigravity-cli"),
|
|
81
|
+
join(home, ".gemini", "antigravity"),
|
|
82
|
+
join(home, ".gemini", "antigravity-ide"),
|
|
83
|
+
],
|
|
80
84
|
codex: [codexHome],
|
|
81
85
|
hermes: [getHermesHome()],
|
|
82
86
|
};
|