@enerlence/suntropy-cli 0.11.9 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enerlence/suntropy-cli",
3
- "version": "0.11.9",
3
+ "version": "0.13.0",
4
4
  "description": "Agent-first CLI for Suntropy solar platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,279 @@
1
+ Referencia de la API pública HTTP de Satvolt (`/api/v1`), la misma que usa `suntropy satvolt`. Úsala para integrar Satvolt desde código o `curl`: crear y configurar campañas, seguirlas, ampliarlas, leer y exportar leads, gestionar plantillas y consultar el consumo de créditos. Si tienes la CLI a mano, es más cómodo `satvolt-cli`.
2
+
3
+ ## Base, autenticación y formato
4
+
5
+ | Entorno | Base URL |
6
+ |---|---|
7
+ | Producción | `https://api.enerlence.com/satvolt/api/v1` |
8
+ | Local | `http://localhost:8099/api/v1` |
9
+
10
+ - **Autenticación:** `Authorization: Bearer <JWT>`, el mismo token de Suntropy. Se verifican la firma y la caducidad, y todo queda acotado al `clientUID` del token.
11
+ - `401 MISSING_TOKEN`: falta la cabecera.
12
+ - `401 INVALID_TOKEN`: el token no es válido.
13
+ - `401 TOKEN_EXPIRED`: el token ha caducado.
14
+ - `401 MISSING_CLIENT`: el token no trae `clientUID`.
15
+ - **Respuestas:** con el código HTTP real y un sobre:
16
+ - éxito: `{ "code": 200, "data": ... }`
17
+ - error: `{ "code": 422, "error": { "code": "VALIDATION_ERROR", "message": "...", "details": [...] } }`
18
+ - **Paginación:** `?limit=` (25 por defecto, 200 como máximo; 500 en `export-tables/:id/data`) y `?offset=`. La respuesta trae `{ items, total, limit, offset, hasMore }`.
19
+ - **Campañas de otra empresa:** devuelven `404 CAMPAIGN_NOT_FOUND`, igual que un id inexistente.
20
+
21
+ ```bash
22
+ API=https://api.enerlence.com/satvolt/api/v1
23
+ curl -s -H "Authorization: Bearer $TOKEN" "$API/campaigns?state=completed&limit=10"
24
+ ```
25
+
26
+ ## Catálogo
27
+
28
+ | Método | Ruta | Devuelve |
29
+ |---|---|---|
30
+ | GET | `/catalog/actions` | Acciones LEAD visibles: `action`, `name`, `description`, `multiple`, `isAsync`, `creditCost` (fijo por lead; las ejecuciones fallidas o saltadas no cobran), `finalLeadState`, `dependencies`, `resultsPropertyKeys` (solo claves de primer nivel), `configSchema` (JSON Schema de la configuración de entrada). Las rutas de los datos que escribe cada paso salen de `/campaigns/:id/fields` |
31
+ | GET | `/catalog/ai-agents` | Agentes válidos para `AI_AGENT.config.agentId` |
32
+ | GET | `/catalog/business-groups` | Grupos para `businessGroups` |
33
+ | GET | `/catalog/states` | Estados de campaña y de lead |
34
+
35
+ ## Campañas
36
+
37
+ | Método | Ruta | Parámetros / body |
38
+ |---|---|---|
39
+ | GET | `/campaigns` | `?limit&offset&search&state=a,b&source=maps\|excel\|campaign` |
40
+ | POST | `/campaigns` | Crear (ver abajo) |
41
+ | GET | `/campaigns/:id` | Detalle con `leadStates`, `sectorSearch` y `configuration` |
42
+ | DELETE | `/campaigns/:id` | Borra la campaña con sectores, leads, ejecuciones, configuración y jobs |
43
+ | POST | `/campaigns/:id/start` | Arranca una campaña `queued` (`409 INVALID_CAMPAIGN_STATE` si no lo está) |
44
+ | POST | `/campaigns/:id/reset` | `?start=true` para relanzarla. Borra leads y resultados |
45
+ | POST | `/campaigns/:id/extend` | `{ "maxLeads": 500 }` o `{ "maxLeads": null }` para quitar el límite |
46
+ | POST | `/campaigns/:id/resume` | `{ "step": { "action": "AI_AGENT", "config": {...} } }`: lo añade al final y lo ejecuta sobre los leads |
47
+ | GET | `/campaigns/:id/usage` | `?include=leads`: créditos totales, por paso y, opcionalmente, por lead |
48
+ | GET | `/campaigns/:id/logs` | `?limit&sinceTs&level=debug\|log\|warn\|error` → `{ entries, lastTs }` |
49
+ | GET | `/campaigns/:id/funnel` | Por paso LEAD: `reached`, `success`, `failure`, `skipped`, `processing` y `pending`, más `leadStates`. `criteria` (o `null`) añade `met`/`unmet` según el `successIf` del paso |
50
+
51
+ ### Criterio de éxito y reintentos por paso
52
+
53
+ Ejecutar un paso y que traiga el dato son cosas distintas: un agente puede terminar en
54
+ `success` respondiendo que no encontró nada. Dos claves de `config`, admitidas por
55
+ cualquier acción, lo separan:
56
+
57
+ ```jsonc
58
+ { "successIf": ["response.linkedinUrl"], "maxRetries": 2 }
59
+ ```
60
+
61
+ - `successIf`: rutas que deben traer valor. Relativas a lo que el paso escribe (en un
62
+ agente, a su `response`) o absolutas con `fullData.`/`lead.`. Vacío = `null`, `""`,
63
+ `[]` o `{}`; `false` y `0` sí son datos.
64
+ - `maxRetries` (0-5): repite el paso mientras no se cumpla el criterio. Los créditos se
65
+ cobran una sola vez por paso, no por intento. Agotados los intentos, el lead sigue al
66
+ paso siguiente y la ejecución queda con `metadata.successCriteria.met = false`.
67
+
68
+ El criterio se evalúa sobre los datos actuales del lead, así que `funnel` lo recalcula al
69
+ vuelo: cámbialo con `PATCH /campaigns/:id/configuration` y vuelve a medir sin re-ejecutar.
70
+
71
+ ```bash
72
+ curl -s -X PATCH -H "$H" "$API/campaigns/$ID/configuration" \
73
+ -d '{"steps":[{"uid":"7f3edaf055d60627","config":{"successIf":["response.linkedinUrl"],"maxRetries":2}}]}'
74
+ curl -s -H "$H" "$API/campaigns/$ID/funnel" | jq '.data.steps[] | {name, reached, criteria}'
75
+ ```
76
+
77
+ ### Crear campaña
78
+
79
+ ```json
80
+ POST /campaigns
81
+ {
82
+ "name": "Sonda Huévar",
83
+ "area": { "type": "circle", "center": { "lat": 37.3509, "lng": -6.2757 }, "radiusMeters": 5000 },
84
+ "templateId": "Greenvolt industria",
85
+ "maxLeads": 50,
86
+ "start": false
87
+ }
88
+ ```
89
+
90
+ - **`area`** admite tres formas:
91
+ - `{type:"circle", center, radiusMeters}`, de 100 m a 50 km;
92
+ - `{type:"bounds", northWest, southEast}`;
93
+ - `{type:"polygon", coordinates:[[lat,lng],...]}`, que se busca en su rectángulo envolvente (con aviso).
94
+
95
+ Los puntos pueden ir como `{lat,lng}` o `[lat,lng]`.
96
+ - **Base opcional:** `templateId` (id o nombre) o `fromCampaignId` (una campaña de Maps). Aporta `steps`, `businessGroups`, `description`, `searchQuery` y `maxLeads`; lo que se envía explícitamente tiene prioridad. No se pueden usar las dos a la vez.
97
+ - **Resto de campos:**
98
+ - `steps` son solo los pasos LEAD `[{action, config?, uid?, disable?}]`. Se validan contra `configSchema`, se rellenan los `default` y se añaden las dependencias que falten, con aviso.
99
+ - `businessGroups` es `["businesses"]` por defecto si no hay base.
100
+ - Opcionales: `searchQuery`, `description`, `inputAddress` y `region`.
101
+ - **Respuesta:** `{ campaign, area, configuration, estimatedCreditsPerLead, basedOn, started, warnings }`.
102
+ - **Errores:** `422 VALIDATION_ERROR` trae en `details[]` el `index`, `uid`, `action`, `field` y `message` de cada problema. Otros: `400 INVALID_AREA`, `404 TEMPLATE_NOT_FOUND`, `400 UNSUPPORTED_SOURCE`.
103
+
104
+ ### Ampliar (`extend`)
105
+
106
+ - **Qué hace:** guarda el nuevo límite y vuelve a buscar solo en los sectores cuya búsqueda no se agotó. Solo los leads nuevos pasan por el pipeline.
107
+ - **Respuesta:** `{ campaignId, previousMaxLeads, maxLeads, currentLeads, sectors: {total, queued, exhausted}, started, campaign }`.
108
+ - **Si no queda nada que buscar:** `sectors.queued = 0`. El límite se guarda, pero no se busca nada.
109
+ - **Errores:**
110
+ - `409 CAMPAIGN_RUNNING`: la campaña está en marcha o tiene búsquedas en cola.
111
+ - `409 CAMPAIGN_NOT_STARTED`: todavía está `queued`.
112
+ - `400 UNSUPPORTED_SOURCE`: no es de Maps.
113
+ - `400 VALIDATION_ERROR`: el límite no es mayor que los leads actuales.
114
+ - **Cuándo tiene sentido:** en `GET /campaigns/:id`, `sectorSearch` da `{ total, exhausted, incomplete, queued, unknown }`. Con `incomplete + unknown > 0` todavía puede aparecer algo.
115
+
116
+ ## Pipeline
117
+
118
+ | Método | Ruta | Body |
119
+ |---|---|---|
120
+ | GET | `/campaigns/:id/configuration` | → `{ campaignId, source, businessGroups, description, updatedAtMs, steps[{uid, action, target, disable, structural, config}] }` |
121
+ | PUT | `/campaigns/:id/configuration` | `{ steps: [...lista completa de pasos LEAD...], businessGroups?, description? }`. Los estructurales se ignoran, así que se puede reenviar lo que devuelve el GET |
122
+ | PATCH | `/campaigns/:id/configuration` | `{ steps: [parche, ...], businessGroups?, description? }` |
123
+ | GET | `/campaigns/:id/steps` | Pasos con `name`, `creditCost`, `isAsync`, `executedLeads`, `neverExecuted`, `runnable` y `reason` |
124
+ | POST | `/campaigns/:id/steps/:uid/run` | Ejecuta sobre los leads existentes un paso que ningún lead ha ejecutado |
125
+
126
+ **Formas de parche** (en `PATCH configuration` y `PATCH campaign-templates`):
127
+
128
+ | Parche | Efecto |
129
+ |---|---|
130
+ | `{ "uid": "…", "config": { "k": "v", "otra": null } }` | Fusiona (JSON Merge Patch); `null` devuelve la clave a su default |
131
+ | `{ "uid": "…", "replaceConfig": true, "config": {...} }` | Sustituye la config entera |
132
+ | `{ "uid": "…", "disable": true }` | Desactiva el paso |
133
+ | `{ "uid": "…", "before": "<uid>" }` / `"after"` | Mueve el paso |
134
+ | `{ "uid": "…", "remove": true }` | Lo quita |
135
+ | `{ "action": "QUALIFY", "config": {...} }` (sin uid) | Lo añade antes de COMPLETE, o donde indiquen `before`/`after` |
136
+
137
+ La `action` de un paso existente no se puede cambiar. Si la campaña está en marcha, cambiar el orden o quitar pasos devuelve un aviso.
138
+
139
+ ## Leads
140
+
141
+ | Método | Ruta | Parámetros |
142
+ |---|---|---|
143
+ | GET | `/campaigns/:id/leads` | `?limit&offset&name&search&state=a,b&step=<paso>&stepStatus=reached\|success\|failure\|skipped\|processing\|pending&include=steps` |
144
+ | GET | `/campaigns/:id/leads/:leadId` | `?fullData=true` (todo) o `?fullData=clave1,clave2` |
145
+ | POST | `/campaigns/:id/leads/:leadId/steps/:step/run` | `{ "mode": "only" \| "continue", "force": false }` → 202 |
146
+ | GET | `/campaigns/:id/fields` | `?sample=25` (máx. 100) `&step=<paso>`: campos para columnas de exportación, agrupados por paso (ver *Tablas de exportación*) |
147
+
148
+ **`<paso>`** (en `step=` de `/leads` y `/fields`, y en la ruta de `run`): uid, acción si aparece una sola vez, nombre completo del paso (`customName`, sin distinguir mayúsculas ni tildes; no vale un trozo) o clave de fullData donde deja sus datos (`cif`, `qualification_<uid>`). Varias coincidencias: `400 AMBIGUOUS_STEP` con los uids en `details`; ninguna: `404 STEP_NOT_FOUND` con la lista de pasos.
149
+
150
+ **Cada lead de la lista** trae `idLead`, `commercialName`, `state`, `stateError`, `lastAction`, `lastStepUid`, `address`, `phone`, `url`, `googlePlacesType`, `coordinates`, `qualifications` y, con `include=steps`, `steps`.
151
+
152
+ **Ejecutar un paso en un lead:**
153
+ - **`step`:** el uid, o la acción si aparece una vez (`400 AMBIGUOUS_STEP` lista los uids).
154
+ - **`mode: "only"`:** solo ese paso. No encola los siguientes, y un lead `completed` o `unQualified` conserva su estado salvo que cambie el resultado del filtro.
155
+ - **`mode: "continue"`:** sigue el pipeline desde ese paso y vuelve a ejecutar los posteriores.
156
+ - **Respuesta:** `{ campaignId, leadId, stepUid, action, mode, jobId, isAsync, name, creditCost }`.
157
+ - **Errores:**
158
+ - `409 DEPENDENCY_NOT_MET` (con `details.missing`): faltan dependencias; `force: true` las ignora.
159
+ - `409 STEP_IN_PROGRESS`: un paso asíncrono sigue esperando su webhook.
160
+ - `400 STEP_NOT_RUNNABLE`: COMPLETE, paso desactivado o que no es LEAD.
161
+ - `404 LEAD_NOT_FOUND`.
162
+ - `409 CAMPAIGN_NOT_STARTED`.
163
+
164
+ ## Tablas de exportación
165
+
166
+ | Método | Ruta | Body / parámetros |
167
+ |---|---|---|
168
+ | GET | `/campaigns/:id/export-tables` | Tablas de la campaña |
169
+ | POST | `/campaigns/:id/export-tables` | `{ name, description?, columns: [{ id?, label, path, type? }] }` → 201 con la tabla en `data` (incluye `warnings`) |
170
+ | GET | `/export-tables/:tableId` | Definición |
171
+ | PUT | `/export-tables/:tableId` | `{ name, description?, columns }` (lista completa; conserva los `id`) |
172
+ | PATCH | `/export-tables/:tableId` | `{ name?, description?, columns? }` (`columns` sustituye la lista entera) |
173
+ | DELETE | `/export-tables/:tableId` | Borra la tabla |
174
+ | POST | `/export-tables/:tableId/duplicate` | `{ targetCampaignId, name? }` |
175
+ | GET | `/export-tables/:tableId/data` | `?limit(≤500)&offset&search` → `{ columns, items, total, ... }` |
176
+ | GET | `/export-tables/:tableId/export` | `?format=xlsx\|csv` → fichero con todas las filas (hasta 100.000, sin paginar); cabeceras `Content-Disposition` y `X-Row-Count`. CSV: UTF-8 con BOM, separado por comas, textos con comas, comillas o saltos de línea entre comillas dobles, booleanos `true`/`false` |
177
+
178
+ **Tabla** (respuesta de GET, POST, PUT y PATCH; estas tres últimas añaden `warnings`):
179
+
180
+ ```json
181
+ { "id": "6650f0c2a1b2c3d4e5f60718", "campaignId": 62, "name": "CRM", "description": null,
182
+ "columns": [{ "id": "c_1a2b3c4d", "label": "Empresa", "path": "lead.commercialName", "type": "string" }],
183
+ "createdAtMs": 1789500035718, "updatedAtMs": 1789500035718, "warnings": [] }
184
+ ```
185
+
186
+ **Columnas una a una** (sin reenviar la lista entera):
187
+
188
+ | Método | Ruta | Body |
189
+ |---|---|---|
190
+ | GET | `/export-tables/:tableId/columns` | Columnas en orden |
191
+ | POST | `/export-tables/:tableId/columns` | `{ label, path, type?, id?, position? }` → 201 `{ table, column, warnings }` |
192
+ | PATCH | `/export-tables/:tableId/columns/:columnId` | `{ label?, path?, type?, position? }` → `{ table, column, warnings }` |
193
+ | DELETE | `/export-tables/:tableId/columns/:columnId` | → `{ table, column, warnings }` |
194
+ | PUT | `/export-tables/:tableId/columns/order` | `{ columnIds: [...] }` con todas las columnas en el orden nuevo → `{ table, warnings }` |
195
+
196
+ - **`position`:** índice desde 0 (0 = primera columna). Sin `position`, POST añade al final y PATCH deja la columna donde está.
197
+ - **`type`:** opcional al crear. Por defecto se usa el de la columna del lead (`lead.url` → `url`) o el que declara el paso que escribe la ruta (`fullData.consumptionEstimate.annualKwh` → `number`); si no se conoce, `string`. Al cambiar `path` en un PATCH el tipo se mantiene salvo que se mande `type`.
198
+ - **`id`:** letras, dígitos, `_` o `-`, hasta 32 caracteres; se genera si no se manda.
199
+ - **Concurrencia:** cada operación se aplica sobre el estado actual de la tabla, así que dos cambios simultáneos no se pisan. `409 CONCURRENT_UPDATE` si no se pudo aplicar tras varios intentos: repite la llamada.
200
+ - **Errores:** `404 COLUMN_NOT_FOUND`, `400 DUPLICATE_COLUMN_ID`, `400 INVALID_POSITION`, `400 INVALID_ORDER` (faltan o sobran ids en `columnIds`).
201
+
202
+ **Descubrir campos: `GET /campaigns/:id/fields`.** Devuelve los campos agrupados por paso del pipeline. Cada paso trae los campos que declara, así que funciona antes de lanzar la campaña, y la muestra de leads añade cobertura, ejemplos y los campos que dependen de la configuración:
203
+
204
+ ```json
205
+ {
206
+ "campaignId": 62, "sampledLeads": 25,
207
+ "lead": [{ "path": "lead.commercialName", "label": "Business name", "type": "string" }],
208
+ "synthetic": [{ "path": "synthetic.googleMapsUrl", "label": "Google Maps URL", "type": "url" }],
209
+ "steps": [{
210
+ "uid": "95161b8b080180e4", "action": "AI_AGENT", "name": "Buscador de CIF y Facturacion",
211
+ "keys": ["cif"], "coverage": 0.32,
212
+ "fields": [
213
+ { "path": "fullData.cif.response.extras.cnae", "label": "Extras · Cnae", "type": "string",
214
+ "source": "observed", "coverage": 0.32, "example": "2512" }
215
+ ],
216
+ "dynamic": [{ "path": "fullData.cif.response", "description": "Agent response: ...",
217
+ "inferredFrom": { "campaignId": 59, "sampledLeads": 25 } }]
218
+ }],
219
+ "other": []
220
+ }
221
+ ```
222
+
223
+ - **`source`:** `catalog` (lo declara el paso), `observed` (aparece en los leads de la muestra) u `otherCampaign` (deducido de otra campaña tuya con el mismo agente, porque esta aún no tiene respuestas).
224
+ - **`keys`:** claves de `fullData` del paso. Un AI_AGENT o CUSTOM_WEBHOOK con `outputKey` usa el alias; QUALIFY usa `resultKey` o `qualification_<uid>`.
225
+ - **Campos fijos de los pasos más usados:**
226
+ - QUALIFY: `fullData.<clave>.qualifies` (boolean) y `.explanation`.
227
+ - AI_AGENT: `fullData.<clave>.response.<campo>` (depende del agente), más `threadId`, `state` y `completedAt`.
228
+ - ESTIMATE_CONSUMPTION: `fullData.consumptionEstimate.annualKwh`, `.confidence.label`, `.inputsUsed.cnae_2`…
229
+ - FIND_ROOFTOP: `fullData.catastralParcel.catastralReference` y `.area`.
230
+ - `synthetic.googleMapsUrl`: enlace a Google Maps construido con las coordenadas.
231
+ - **`dynamic`:** partes cuya forma depende de la configuración del paso (respuesta de un agente o de un webhook). Sus campos aparecen en `fields` cuando hay leads con respuesta o se deducen de otra campaña; si no hay ninguno, la ruta se completa a mano (`fullData.<clave>.response.<campo>`).
232
+ - **`coverage`:** parte de los leads muestreados con ese dato (null si la campaña no tiene leads). Una cobertura baja en un campo de un paso posterior a un filtro (QUALIFY) es normal.
233
+ - **`other`:** datos de los leads que no escribe ningún paso actual (pasos borrados o alias cambiados).
234
+
235
+ - **Rutas de columna:** `lead.<columna>`, `synthetic.<clave>` y `fullData.<ruta.anidada>` (arrays con índice: `fullData.consumptionEstimate.monthlyKwh[0]`).
236
+ - **Tipos:** `string`, `number`, `boolean`, `date` y `url`.
237
+ - **Avisos:** crear o editar columnas devuelve `warnings` con `UNKNOWN_FULLDATA_KEY` si ningún paso de la campaña escribe la clave de `fullData` de la ruta (errata o alias cambiado). No bloquea: la columna se guarda.
238
+ - **Errores:** `404 EXPORT_TABLE_NOT_FOUND` si el id no existe (o no es un ObjectId) y `400 VALIDATION_ERROR` si alguna ruta o id es inválido.
239
+
240
+ ## Plantillas de campaña
241
+
242
+ | Método | Ruta | Body |
243
+ |---|---|---|
244
+ | GET | `/campaign-templates` | `?search=` |
245
+ | GET | `/campaign-templates/:idOrName` | Detalle |
246
+ | POST | `/campaign-templates` | `{ name, description?, steps, businessGroups?, configurationDescription?, searchQuery?, maxLeads? }` o `{ fromCampaignId, name, description? }` |
247
+ | PUT | `/campaign-templates/:idOrName` | Plantilla completa (`steps` obligatorio; lo que no se envía se borra) |
248
+ | PATCH | `/campaign-templates/:idOrName` | Campos sueltos; `steps` son parches por uid; `maxLeads: null` o `searchQuery: null` quitan el valor |
249
+ | DELETE | `/campaign-templates/:idOrName` | Borra la plantilla |
250
+
251
+ - **Vista:** `{ id, name, description, source, steps[{uid, action, target, disable, config}], businessGroups, configurationDescription, searchQuery, maxLeads, sourceCampaignId, createdAtMs, updatedAtMs }`.
252
+ - **Errores:** `409 TEMPLATE_NAME_TAKEN` y `404 TEMPLATE_NOT_FOUND`.
253
+ - **uids compartidos:** las campañas creadas desde la misma plantilla comparten uids de pasos.
254
+
255
+ ## Ejemplo de principio a fin con curl
256
+
257
+ ```bash
258
+ H="Authorization: Bearer $TOKEN"; J="Content-Type: application/json"
259
+ # 1. Plantilla desde una campaña validada
260
+ curl -s -X POST "$API/campaign-templates" -H "$H" -H "$J" -d '{"fromCampaignId":62,"name":"Greenvolt industria"}'
261
+ # 2. Sonda de 50 leads y arranque
262
+ ID=$(curl -s -X POST "$API/campaigns" -H "$H" -H "$J" -d '{"name":"Sonda Elche","templateId":"Greenvolt industria","maxLeads":50,
263
+ "area":{"type":"circle","center":{"lat":38.293,"lng":-0.617},"radiusMeters":3000},"start":true}' | jq -r .data.campaign.idCampaign)
264
+ # 3. Seguimiento
265
+ curl -s -H "$H" "$API/campaigns/$ID/funnel" | jq '.data.steps[] | {name, success, failure, pending}'
266
+ # 4. Ampliar cuando termine y quede área por buscar
267
+ curl -s -X POST "$API/campaigns/$ID/extend" -H "$H" -H "$J" -d '{"maxLeads":null}'
268
+ # 5. Créditos consumidos
269
+ curl -s -H "$H" "$API/campaigns/$ID/usage" | jq '.data | {totalCredits, avgCreditsPerLead, byStep}'
270
+ # 6. Tabla de exportación: rutas del paso QUALIFY y del agente de CIF, tabla, columna en 2ª posición y CSV
271
+ curl -s -H "$H" "$API/campaigns/$ID/fields?step=QUALIFY" | jq '.data.steps[0].fields[] | {path, type}'
272
+ curl -s -H "$H" "$API/campaigns/$ID/fields?step=cif" | jq '.data.steps[0].fields[] | select(.path | test("cnae")) | .path'
273
+ TABLE=$(curl -s -X POST "$API/campaigns/$ID/export-tables" -H "$H" -H "$J" -d '{"name":"Cualificación","columns":[
274
+ {"label":"Empresa","path":"lead.commercialName"},
275
+ {"label":"Cualifica","path":"fullData.qualification_<uid>.qualifies"},
276
+ {"label":"Motivo","path":"fullData.qualification_<uid>.explanation"}]}' | jq -r .data.id)
277
+ curl -s -X POST "$API/export-tables/$TABLE/columns" -H "$H" -H "$J" -d '{"label":"CNAE","path":"fullData.cif.response.extras.cnae","position":1}'
278
+ curl -s -H "$H" "$API/export-tables/$TABLE/export?format=csv" -o cualificacion.csv
279
+ ```
@@ -0,0 +1,228 @@
1
+ Crea, configura y explota una campaña de Satvolt (captación de leads B2B desde Google Maps con un pipeline de enriquecimiento) usando `suntropy satvolt`. Todo lo que aquí se hace es lo mismo que permite la web de Satvolt; la CLI habla con la API pública `/satvolt/api/v1` con el mismo token de `suntropy auth`.
2
+
3
+ Cada acción del pipeline gasta créditos por lead (1 crédito = 0,005 €). Antes de arrancar o reanudar una campaña, enseña al usuario el coste estimado y pide confirmación.
4
+
5
+ ## Parámetros de entrada
6
+
7
+ | Parámetro | Obligatorio | Default |
8
+ |-----------|-------------|---------|
9
+ | Nombre de la campaña | Sí | - |
10
+ | Área: centro + radio, rectángulo o polígono | Sí | - |
11
+ | Consulta de texto (en vez de búsqueda por cercanía) | No | - |
12
+ | Máximo de leads | No | sin límite |
13
+ | Grupos de negocio | No | `businesses` |
14
+ | Plantilla o campaña base (pasos, grupos, límite) | No | - |
15
+ | Pasos del pipeline (acciones y su config) | No | los de la base; sin base, ninguno (solo descubre leads) |
16
+ | Arrancar al crear | No | no (queda en cola) |
17
+
18
+ ## Ejecución
19
+
20
+ ### Paso 0: Catálogo
21
+
22
+ ```bash
23
+ suntropy satvolt catalog actions # acciones, créditos por lead, dependencias y JSON Schema de config
24
+ suntropy satvolt catalog business-groups
25
+ suntropy satvolt catalog ai-agents # ids válidos para AI_AGENT config.agentId
26
+ ```
27
+
28
+ Reglas del catálogo:
29
+ - `configSchema.required` son obligatorios en pasos activos; los `default` se rellenan solos.
30
+ - Solo las acciones con `multiple: true` (QUALIFY, AI_AGENT, CUSTOM_WEBHOOK) pueden repetirse.
31
+ - Si falta una dependencia (p. ej. ESTIMATE_CONSUMPTION necesita FIND_ROOFTOP), el backend la añade y lo avisa en `warnings`.
32
+
33
+ ### Paso 1: Pasos del pipeline
34
+
35
+ Si ya hay una plantilla o una campaña con el pipeline que se quiere, úsala como base y sáltate este paso (`templates list`, `--template` o `--from-campaign` en el paso 2). Si el pipeline es nuevo y se va a repetir, guárdalo como plantilla: `templates create --name ... --steps @steps.json`.
36
+
37
+ Escribe los pasos LEAD en orden en un fichero. SECTORIZE, FIND_LEADS y COMPLETE los pone el backend:
38
+
39
+ ```bash
40
+ cat > steps.json <<'EOF'
41
+ [
42
+ { "action": "FIND_ROOFTOP" },
43
+ { "action": "QUALIFY", "config": { "qualificationDefinition": "Nave industrial con cubierta > 1000 m2 y actividad con consumo diurno" } },
44
+ { "action": "AI_AGENT", "config": { "customName": "Buscador de CIF y Facturacion", "agentId": "<id>", "outputKey": "cif" } },
45
+ { "action": "ESTIMATE_CONSUMPTION", "config": { "tariffTemplate": "3.0TD", "cnaeTemplate": "{{fullData.cif.response.extras.cnae}}" } }
46
+ ]
47
+ EOF
48
+ ```
49
+
50
+ ESTIMATE_CONSUMPTION usa la superficie construida del Catastro, el código postal y, si se le pasa, el CNAE. Sin CNAE la confianza no pasa de "media" y la estimación anual de los fabricantes sale muy por debajo. Colócalo después del paso que obtiene el CNAE y referencia su salida en `cnaeTemplate`.
51
+
52
+ ### Paso 2: Crear la campaña
53
+
54
+ ```bash
55
+ # Círculo (100 m – 50 km)
56
+ suntropy satvolt campaigns create --name "<nombre>" \
57
+ --circle <lat>,<lng> --radius <metros> \
58
+ --max-leads 300 --steps @steps.json
59
+
60
+ # Rectángulo
61
+ suntropy satvolt campaigns create --name "<nombre>" --bounds <nwLat>,<nwLng>,<seLat>,<seLng> --steps @steps.json
62
+
63
+ # Polígono ([[lat,lng],...] o GeoJSON). Se busca en su rectángulo envolvente: avisa al usuario.
64
+ suntropy satvolt campaigns create --name "<nombre>" --polygon @area.geojson --steps @steps.json
65
+
66
+ # Desde una plantilla o copiando otra campaña (los flags explícitos tienen prioridad)
67
+ suntropy satvolt campaigns create --name "<nombre>" --template "<plantilla>" --circle <lat>,<lng> --radius <m>
68
+ suntropy satvolt campaigns create --name "<nombre>" --from-campaign <id> --bounds <nwLat>,<nwLng>,<seLat>,<seLng> --max-leads 100
69
+ ```
70
+
71
+ La respuesta trae `campaign.idCampaign`, `estimatedCreditsPerLead` y `warnings`. La campaña queda en `queued`.
72
+
73
+ Coste estimado, antes de arrancar:
74
+ - **Máximo:** `estimatedCreditsPerLead × maxLeads` (sin contar FIND_LEADS), como si todos los leads pasaran todos los pasos.
75
+ - **Realista:** si hay una campaña anterior con la misma configuración, `campaigns usage <id>` → `avgCreditsPerLead × maxLeads`. Los filtros (QUALIFY) hacen que la mayoría de leads no pague los pasos caros.
76
+
77
+ Enséñale al usuario el rango y pide confirmación. Las ejecuciones fallidas o saltadas no cobran.
78
+
79
+ ### Paso 3: Arrancar y seguir
80
+
81
+ ```bash
82
+ suntropy satvolt campaigns start <campaignId>
83
+ suntropy satvolt campaigns logs <campaignId> --follow --format human # termina solo al acabar la campaña
84
+ suntropy satvolt campaigns funnel <campaignId> --format human # alcanzados/success/failure/processing por paso
85
+ suntropy satvolt campaigns funnel <campaignId> --mode success # en cuántos leads el paso trajo el dato
86
+ ```
87
+
88
+ **Ejecutar ≠ acertar.** Un agente puede terminar sin error respondiendo que no encontró
89
+ nada: el paso cuenta como `success` y la columna se queda vacía. Para medirlo, cada paso
90
+ admite dos claves de config comunes:
91
+
92
+ | Clave | Qué hace |
93
+ |---|---|
94
+ | `successIf` | Rutas que el paso debe rellenar para contar como útil. Relativas a lo que escribe el paso (en un agente, a su respuesta: `response.linkedinUrl`) o absolutas con `fullData.`/`lead.`. Al ser relativas, sobreviven a copiar la campaña o guardarla como plantilla. |
95
+ | `maxRetries` | 0-5. Repite el paso mientras no se cumpla `successIf`. Para pasos no deterministas (agentes, identificación de paneles). Los créditos se cobran una vez por paso, no por intento; agotados los intentos el lead continúa al paso siguiente. |
96
+
97
+ ```bash
98
+ suntropy satvolt steps set <campaignId> <stepUid> \
99
+ --config '{"successIf":["response.linkedinUrl"],"maxRetries":2}'
100
+ ```
101
+
102
+ El criterio se evalúa sobre los datos actuales del lead, así que se puede cambiar y volver
103
+ a medir una campaña ya terminada sin re-ejecutar nada.
104
+
105
+ ### Paso 4: Revisar leads
106
+
107
+ ```bash
108
+ suntropy satvolt leads list <campaignId> --limit 50 --format human
109
+ suntropy satvolt leads list <campaignId> --name "logística"
110
+ suntropy satvolt leads list <campaignId> --step QUALIFY --step-status failure # uid del paso si la acción se repite
111
+ suntropy satvolt leads list <campaignId> --step <uid> --step-status pending
112
+ suntropy satvolt leads get <campaignId> <leadId> --full-data consumptionEstimate,qualification_<uid>
113
+ suntropy satvolt leads full-data <campaignId> <leadId> --path cif.response.extras.cnae
114
+ ```
115
+
116
+ `--step-status`: `reached` (por defecto), `success`, `failure`, `skipped`, `processing` o `pending`.
117
+
118
+ ### Paso 5: Tabla de exportación
119
+
120
+ Descubre qué campos hay y en qué ruta de `fullData` los deja cada paso. Funciona antes de lanzar la campaña: cada paso lista los campos que declara, y los de un agente se deducen de otra campaña con el mismo agente si esta aún no tiene respuestas.
121
+
122
+ ```bash
123
+ suntropy satvolt export-tables fields <campaignId> --format human # todos, agrupados por paso
124
+ suntropy satvolt export-tables fields <campaignId> --step <uid|ACCIÓN> --format human
125
+ suntropy satvolt export-tables fields <campaignId> --search cnae --format human # dónde está un dato
126
+ ```
127
+
128
+ Columnas que salen: `group` (paso), `path` (ruta para la columna), `label`, `type`, `source` (`catalog`, `observed`, `otherCampaign`, `dynamic`), `coverage` (leads de la muestra con el dato) y `example`.
129
+
130
+ ```bash
131
+ suntropy satvolt export-tables create <campaignId> --name "CRM" \
132
+ --columns "Empresa=lead.commercialName;Teléfono=lead.phone;Web=lead.url;Consumo anual kWh=fullData.consumptionEstimate.annualKwh;Maps=synthetic.googleMapsUrl"
133
+
134
+ # Ajustar columnas sueltas (por id o etiqueta; posiciones desde 0)
135
+ suntropy satvolt export-tables columns add <tableId> --label CIF --path fullData.cif.response.cif --after Empresa
136
+ suntropy satvolt export-tables columns set <tableId> "Consumo anual kWh" --label "Consumo (kWh/año)"
137
+ suntropy satvolt export-tables columns move <tableId> Maps --position 0
138
+ suntropy satvolt export-tables columns remove <tableId> Teléfono
139
+ suntropy satvolt export-tables columns list <tableId> --format human
140
+
141
+ suntropy satvolt export-tables data <tableId> --limit 20 --format human
142
+ suntropy satvolt export-tables export <tableId> --file-format xlsx --out campaña.xlsx
143
+ suntropy satvolt export-tables export <tableId> --file-format csv --out campaña.csv
144
+ ```
145
+
146
+ - **Tipos de columna:** `string`, `number`, `boolean`, `date`, `url`. Sin tipo, se usa el que declara el paso para esa ruta.
147
+ - **`warnings` con `UNKNOWN_FULLDATA_KEY`:** ningún paso de la campaña escribe esa clave; revisa la ruta con `export-tables fields`.
148
+
149
+ ### Paso 6: Cambiar el pipeline
150
+
151
+ ```bash
152
+ suntropy satvolt steps list <campaignId> --format human
153
+ suntropy satvolt steps set <campaignId> <uid> --config '{"enableWebSearch": true}' # fusiona; null devuelve la clave a su valor por defecto
154
+ suntropy satvolt steps add <campaignId> --action AI_AGENT --config @agent.json
155
+ suntropy satvolt steps remove <campaignId> <uid>
156
+
157
+ # JSON completo
158
+ suntropy satvolt config get <campaignId> --save pipeline.json
159
+ suntropy satvolt config update <campaignId> --data @pipeline.json
160
+ ```
161
+
162
+ ### Paso 7: Reanudar con un paso nuevo
163
+
164
+ Para añadir una acción a una campaña terminada sin reprocesarla, usa `resume`. Añade el paso al final y lo lanza sobre los leads que llegaron al paso anterior:
165
+
166
+ ```bash
167
+ suntropy satvolt catalog ai-agents --format human # id, nombre y descripción de cada agente
168
+ suntropy satvolt campaigns funnel <campaignId> # leads que llegaron al último paso: los que pagarán el nuevo
169
+ suntropy satvolt campaigns resume <campaignId> --action AI_AGENT \
170
+ --config '{"customName":"Web corporativa","agentId":"<id>","outputKey":"web"}'
171
+ ```
172
+
173
+ `--config` es solo el objeto de configuración del paso (el `configSchema` de `catalog actions`), no `{ action, config }`. Coste ≈ leads que llegaron al último paso × `creditCost` de la acción.
174
+
175
+ Si el `agentId` sale de una variable de shell, no lo metas entre comillas simples (no se expande); usa un fichero:
176
+
177
+ ```bash
178
+ cat > agent.json <<EOF
179
+ { "customName": "Web corporativa", "agentId": "$AGENT_ID", "outputKey": "web" }
180
+ EOF
181
+ suntropy satvolt campaigns resume <campaignId> --action AI_AGENT --config @agent.json
182
+ ```
183
+
184
+ Para guardar el pipeline con el paso nuevo en una plantilla, haz `templates create --from-campaign` después del `resume`: la plantilla copia la configuración que tenga la campaña en ese momento.
185
+
186
+ Si se añadió antes un paso con `steps add` y nunca se ejecutó, `steps list` lo marca con `runnable: true`. Se lanza con:
187
+
188
+ ```bash
189
+ suntropy satvolt steps run <campaignId> <uid>
190
+ ```
191
+
192
+ Para repetir un paso en leads concretos (p. ej. tras un fallo) usa `leads run-step <campaignId> <leadId> <uid|ACCIÓN>`: solo ese paso, o `--continue` para seguir el pipeline. La skill `satvolt-lead-troubleshooting` detalla cuándo usar cada uno.
193
+
194
+ ### Ampliar una campaña con más leads
195
+
196
+ Para sacar más leads de una campaña de Maps ya terminada (típico tras una sonda) usa `extend`, no `reset`. `reset` borra los leads y vuelve a pagarlos todos. `extend` sube el límite, o lo quita con `--no-limit`, y vuelve a buscar solo en los sectores que se quedaron a medias. Los leads que ya existen no se reprocesan; solo los nuevos pasan por el pipeline.
197
+
198
+ ```bash
199
+ suntropy satvolt campaigns get <campaignId> # sectorSearch: incomplete + unknown > 0 → quedan leads por buscar
200
+ suntropy satvolt campaigns extend <campaignId> --max-leads 500
201
+ suntropy satvolt campaigns extend <campaignId> --no-limit # barre entero cada sector pendiente
202
+ ```
203
+
204
+ - El nuevo límite tiene que ser mayor que los leads actuales. La campaña no puede estar en ejecución (409 `CAMPAIGN_RUNNING`) ni sin arrancar (409 `CAMPAIGN_NOT_STARTED`).
205
+ - Coste ≈ leads nuevos × créditos por lead de la campaña (míralo con `campaigns usage`). Enséñaselo al usuario y pide confirmación antes de ampliar.
206
+ - En las campañas creadas antes de esta función, los sectores salen como `unknown` y cuentan como pendientes: se repiten sus primeras peticiones a Places, pero los duplicados no se crean.
207
+
208
+ ### Consumo, reinicio y borrado
209
+
210
+ ```bash
211
+ suntropy satvolt campaigns usage <campaignId> --format human # créditos totales, por lead y por paso
212
+ suntropy satvolt campaigns reset <campaignId> --yes [--start] # BORRA leads y resultados; pide confirmación explícita al usuario
213
+ suntropy satvolt campaigns delete <campaignId> --yes # borra la campaña entera; pide confirmación explícita al usuario
214
+ ```
215
+
216
+ Para sacar más leads de una campaña ya terminada, usa `extend` (sección anterior), nunca `reset`.
217
+
218
+ ## Errores
219
+
220
+ Los errores salen por stderr como `{ error, status, message, details }`:
221
+
222
+ | status / código | Significado |
223
+ |---|---|
224
+ | 422 `VALIDATION_ERROR` | pasos o config inválidos. `details` lista `index`, `uid`, `action`, `field` y `message` de cada problema. |
225
+ | 400 `INVALID_AREA` | área mal formada o fuera de límites. |
226
+ | 409 `INVALID_CAMPAIGN_STATE` | `start` sobre una campaña que no está en cola: hay que hacer `reset` antes. |
227
+ | 409 `PENDING_STEP`, `STEP_NOT_RUNNABLE` | no se puede reanudar; `message` explica por qué. |
228
+ | 401 `TOKEN_EXPIRED` | renueva el token con `suntropy auth refresh`. |