@hostwebhook/node-types 1.85.0 → 1.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/calendar-toolkit.d.ts +3 -0
  2. package/dist/calendar-toolkit.js +3 -2
  3. package/dist/clase-de-herramienta.d.ts +61 -0
  4. package/dist/clase-de-herramienta.js +153 -0
  5. package/dist/discord-toolkit.d.ts +2 -3
  6. package/dist/discord-toolkit.js +3 -6
  7. package/dist/docs-toolkit.d.ts +2 -1
  8. package/dist/docs-toolkit.js +3 -6
  9. package/dist/drive-toolkit.d.ts +9 -4
  10. package/dist/drive-toolkit.js +3 -4
  11. package/dist/esm/calendar-toolkit.d.ts +3 -0
  12. package/dist/esm/calendar-toolkit.js +3 -2
  13. package/dist/esm/clase-de-herramienta.d.ts +61 -0
  14. package/dist/esm/clase-de-herramienta.js +148 -0
  15. package/dist/esm/discord-toolkit.d.ts +2 -3
  16. package/dist/esm/discord-toolkit.js +3 -6
  17. package/dist/esm/docs-toolkit.d.ts +2 -1
  18. package/dist/esm/docs-toolkit.js +3 -6
  19. package/dist/esm/drive-toolkit.d.ts +9 -4
  20. package/dist/esm/drive-toolkit.js +3 -4
  21. package/dist/esm/gmail-operations.d.ts +2 -0
  22. package/dist/esm/gmail-operations.js +7 -6
  23. package/dist/esm/index.d.ts +12 -0
  24. package/dist/esm/index.js +7 -0
  25. package/dist/esm/mongo-toolkit.d.ts +47 -0
  26. package/dist/esm/mongo-toolkit.js +98 -0
  27. package/dist/esm/postgres-toolkit.d.ts +52 -0
  28. package/dist/esm/postgres-toolkit.js +85 -0
  29. package/dist/esm/registry.js +10 -0
  30. package/dist/esm/sentiment.d.ts +39 -0
  31. package/dist/esm/sentiment.js +53 -0
  32. package/dist/esm/sheets-toolkit.d.ts +2 -1
  33. package/dist/esm/sheets-toolkit.js +3 -6
  34. package/dist/esm/slack-toolkit.d.ts +2 -3
  35. package/dist/esm/slack-toolkit.js +3 -4
  36. package/dist/esm/telegram-toolkit.d.ts +3 -0
  37. package/dist/esm/telegram-toolkit.js +3 -2
  38. package/dist/esm/toolkits.d.ts +47 -0
  39. package/dist/esm/toolkits.js +74 -0
  40. package/dist/esm/types.d.ts +111 -76
  41. package/dist/esm/types.js +26 -0
  42. package/dist/esm/ui.js +10 -1
  43. package/dist/gmail-operations.d.ts +2 -0
  44. package/dist/gmail-operations.js +11 -10
  45. package/dist/index.d.ts +12 -0
  46. package/dist/index.js +29 -4
  47. package/dist/mongo-toolkit.d.ts +47 -0
  48. package/dist/mongo-toolkit.js +101 -0
  49. package/dist/postgres-toolkit.d.ts +52 -0
  50. package/dist/postgres-toolkit.js +88 -0
  51. package/dist/registry.js +10 -0
  52. package/dist/sentiment.d.ts +39 -0
  53. package/dist/sentiment.js +57 -0
  54. package/dist/sheets-toolkit.d.ts +2 -1
  55. package/dist/sheets-toolkit.js +3 -6
  56. package/dist/slack-toolkit.d.ts +2 -3
  57. package/dist/slack-toolkit.js +3 -4
  58. package/dist/telegram-toolkit.d.ts +3 -0
  59. package/dist/telegram-toolkit.js +3 -2
  60. package/dist/toolkits.d.ts +47 -0
  61. package/dist/toolkits.js +79 -0
  62. package/dist/types.d.ts +111 -76
  63. package/dist/types.js +28 -0
  64. package/dist/ui.js +10 -1
  65. package/package.json +1 -1
@@ -39,6 +39,9 @@ export interface TelegramToolkitSpec {
39
39
  /** Descripción (más reglas de uso) que ve el LLM. */
40
40
  description: string;
41
41
  parameters: TelegramToolkitParameter[];
42
+ /** Pisa o borra. Lo pone `marcarDestructivas` leyendo el verbo del
43
+ * nombre (ver `clase-de-herramienta.ts`), no se escribe a mano. */
44
+ destructive?: boolean;
42
45
  /**
43
46
  * Encabezado de grupo para la lista de la página de detalle. Las filas
44
47
  * consecutivas que comparten grupo se pintan bajo un único título, así que el
@@ -22,6 +22,7 @@
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
24
  exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = void 0;
25
+ const clase_de_herramienta_js_1 = require("./clase-de-herramienta.js");
25
26
  const p = (name, description, required = true, type = 'string') => ({ name, type, description, required });
26
27
  /*
27
28
  * `sendAndWaitForResponse` NO está aquí, y es una decisión, no un olvido: la op
@@ -36,7 +37,7 @@ const p = (name, description, required = true, type = 'string') => ({ name, type
36
37
  * campo en ninguna de las dos capas, y añadirlo cambiaría cuándo el AI Node pide
37
38
  * confirmación. Eso es otra decisión, no parte de mudar el esquema de sitio.
38
39
  */
39
- exports.TELEGRAM_TOOLKIT_SPECS = [
40
+ exports.TELEGRAM_TOOLKIT_SPECS = (0, clase_de_herramienta_js_1.marcarDestructivas)([
40
41
  {
41
42
  operation: 'sendMessage',
42
43
  label: 'Send message',
@@ -181,6 +182,6 @@ exports.TELEGRAM_TOOLKIT_SPECS = [
181
182
  p('showAlert', 'Optional. true = modal alert; false (default) = toast notification.', false, 'boolean'),
182
183
  ],
183
184
  },
184
- ];
185
+ ]);
185
186
  /** Las herramientas por nombre, para despachar una llamada del modelo. */
186
187
  exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = Object.fromEntries(exports.TELEGRAM_TOOLKIT_SPECS.map((s) => [s.toolName, s]));
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Los toolkits por tipo de nodo: qué herramientas expone cada nodo en modo
3
+ * AI toolkit, en un solo sitio.
4
+ *
5
+ * ## Para qué (2026-09-24)
6
+ *
7
+ * Para que el gate «Block destructive tool calls» del AI Node pueda preguntar
8
+ * por la OPERACIÓN y no sólo por el nombre. Hasta hoy sólo miraba el nombre
9
+ * contra unos patrones (`delete_*`, `drop_*`…), así que `update_row` de Sheets
10
+ * —que pisa una fila— pasaba con el gate encendido. Con esto, la herramienta
11
+ * que el AI Node guardó al conectar (`nodeRefType` + `operationOverride`) se
12
+ * resuelve aquí a su operación, y su `destructive` —que sale del verbo, ver
13
+ * `clase-de-herramienta.ts`— decide.
14
+ *
15
+ * ## Lo que falta aquí, a propósito
16
+ *
17
+ * WhatsApp y Contacts siguen con sus specs escritos a mano en la api y en el
18
+ * dashboard, no en este paquete. Hasta que se muden, quien consulte este
19
+ * registro tiene que componerlos por su lado — {@link esOperacionDestructiva}
20
+ * devuelve `null` para ellos, que significa «no lo sé», no «no es
21
+ * destructiva».
22
+ */
23
+ /** Lo mínimo de una herramienta de toolkit que hace falta para decidir. */
24
+ export interface OperacionDeToolkit {
25
+ operation: string;
26
+ toolName: string;
27
+ label: string;
28
+ destructive?: boolean;
29
+ }
30
+ export declare const TOOLKITS_POR_TIPO: Readonly<Record<string, readonly OperacionDeToolkit[]>>;
31
+ /**
32
+ * La operación de un toolkit, buscada por su nombre interno (`updateRow`) O
33
+ * por el nombre de su herramienta (`update_row`).
34
+ *
35
+ * Las dos formas porque las dos llegan: el AI Node guarda `operationOverride`
36
+ * con el nombre interno, y el servidor MCP recibe el nombre de la herramienta.
37
+ */
38
+ export declare function operacionDeToolkit(nodeType: string | undefined | null, operacionONombre: string | undefined | null): OperacionDeToolkit | null;
39
+ /**
40
+ * ¿Es destructiva esta operación de toolkit?
41
+ *
42
+ * `null` cuando no se sabe —el tipo no es un toolkit de este paquete, o la
43
+ * operación no existe en él—. Quien llama decide qué hace con el «no sé»; el
44
+ * gate del AI Node lo combina con los patrones de nombre, que siguen ahí para
45
+ * las herramientas que no son de toolkit (MCP, HTTP…).
46
+ */
47
+ export declare function esOperacionDestructiva(nodeType: string | undefined | null, operacionONombre: string | undefined | null): boolean | null;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Los toolkits por tipo de nodo: qué herramientas expone cada nodo en modo
4
+ * AI toolkit, en un solo sitio.
5
+ *
6
+ * ## Para qué (2026-09-24)
7
+ *
8
+ * Para que el gate «Block destructive tool calls» del AI Node pueda preguntar
9
+ * por la OPERACIÓN y no sólo por el nombre. Hasta hoy sólo miraba el nombre
10
+ * contra unos patrones (`delete_*`, `drop_*`…), así que `update_row` de Sheets
11
+ * —que pisa una fila— pasaba con el gate encendido. Con esto, la herramienta
12
+ * que el AI Node guardó al conectar (`nodeRefType` + `operationOverride`) se
13
+ * resuelve aquí a su operación, y su `destructive` —que sale del verbo, ver
14
+ * `clase-de-herramienta.ts`— decide.
15
+ *
16
+ * ## Lo que falta aquí, a propósito
17
+ *
18
+ * WhatsApp y Contacts siguen con sus specs escritos a mano en la api y en el
19
+ * dashboard, no en este paquete. Hasta que se muden, quien consulte este
20
+ * registro tiene que componerlos por su lado — {@link esOperacionDestructiva}
21
+ * devuelve `null` para ellos, que significa «no lo sé», no «no es
22
+ * destructiva».
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.TOOLKITS_POR_TIPO = void 0;
26
+ exports.operacionDeToolkit = operacionDeToolkit;
27
+ exports.esOperacionDestructiva = esOperacionDestructiva;
28
+ const gmail_operations_js_1 = require("./gmail-operations.js");
29
+ const calendar_toolkit_js_1 = require("./calendar-toolkit.js");
30
+ const drive_toolkit_js_1 = require("./drive-toolkit.js");
31
+ const telegram_toolkit_js_1 = require("./telegram-toolkit.js");
32
+ const discord_toolkit_js_1 = require("./discord-toolkit.js");
33
+ const slack_toolkit_js_1 = require("./slack-toolkit.js");
34
+ const sheets_toolkit_js_1 = require("./sheets-toolkit.js");
35
+ const docs_toolkit_js_1 = require("./docs-toolkit.js");
36
+ const mongo_toolkit_js_1 = require("./mongo-toolkit.js");
37
+ const postgres_toolkit_js_1 = require("./postgres-toolkit.js");
38
+ exports.TOOLKITS_POR_TIPO = Object.freeze({
39
+ gmailAction: gmail_operations_js_1.GMAIL_ALL_TOOLKIT_SPECS,
40
+ emailAction: gmail_operations_js_1.NATIVE_EMAIL_TOOLKIT_SPECS,
41
+ calendarAction: calendar_toolkit_js_1.GOOGLE_CALENDAR_TOOLKIT_SPECS,
42
+ driveAction: drive_toolkit_js_1.DRIVE_TOOLKIT_SPECS,
43
+ telegramAction: telegram_toolkit_js_1.TELEGRAM_TOOLKIT_SPECS,
44
+ discordAction: discord_toolkit_js_1.DISCORD_TOOLKIT_SPECS,
45
+ slackAction: slack_toolkit_js_1.SLACK_TOOLKIT_SPECS,
46
+ sheetsAction: sheets_toolkit_js_1.SHEETS_TOOLKIT_SPECS,
47
+ docsAction: docs_toolkit_js_1.DOCS_TOOLKIT_SPECS,
48
+ mongoAction: mongo_toolkit_js_1.MONGO_TOOLKIT_SPECS,
49
+ postgresAction: postgres_toolkit_js_1.POSTGRES_TOOLKIT_SPECS,
50
+ });
51
+ /**
52
+ * La operación de un toolkit, buscada por su nombre interno (`updateRow`) O
53
+ * por el nombre de su herramienta (`update_row`).
54
+ *
55
+ * Las dos formas porque las dos llegan: el AI Node guarda `operationOverride`
56
+ * con el nombre interno, y el servidor MCP recibe el nombre de la herramienta.
57
+ */
58
+ function operacionDeToolkit(nodeType, operacionONombre) {
59
+ if (!nodeType || !operacionONombre)
60
+ return null;
61
+ const toolkit = exports.TOOLKITS_POR_TIPO[nodeType];
62
+ if (!toolkit)
63
+ return null;
64
+ return (toolkit.find((s) => s.operation === operacionONombre) ??
65
+ toolkit.find((s) => s.toolName === operacionONombre) ??
66
+ null);
67
+ }
68
+ /**
69
+ * ¿Es destructiva esta operación de toolkit?
70
+ *
71
+ * `null` cuando no se sabe —el tipo no es un toolkit de este paquete, o la
72
+ * operación no existe en él—. Quien llama decide qué hace con el «no sé»; el
73
+ * gate del AI Node lo combina con los patrones de nombre, que siguen ahí para
74
+ * las herramientas que no son de toolkit (MCP, HTTP…).
75
+ */
76
+ function esOperacionDestructiva(nodeType, operacionONombre) {
77
+ const op = operacionDeToolkit(nodeType, operacionONombre);
78
+ return op ? op.destructive === true : null;
79
+ }
package/dist/types.d.ts CHANGED
@@ -54,83 +54,15 @@ export interface NodeUIConfig {
54
54
  connectModal?: 'branch' | 'output-group';
55
55
  routerTargetLabel?: string;
56
56
  /**
57
- * Generic named output groups — used by connectModal: 'output-group'.
58
- * The modal reads entity[outputGroupsField] as an array of groups,
59
- * each with a name (outputGroupNameField), a summary (outputGroupSummaryField),
60
- * and its own outputNodes[].
61
- * Works for Split (outputs[]), Conditional (branches[]), or any future branching node.
57
+ * Los juegos de salidas de un nodo que ramifica: UNO, o VARIOS con su
58
+ * `gate` cada uno [2026-09-22]. Con varios, quien pregunta se queda con
59
+ * el PRIMERO cuya puerta pasa — ver `outputGroupsFor`. El AI Node los
60
+ * necesita porque ramifica de dos maneras según el modo: por las
61
+ * categorías que escribe el usuario (`classifier`) o por los cuatro
62
+ * puertos fijos del análisis de sentimiento (`sentiment`). La forma
63
+ * vieja (un objeto suelto) sigue valiendo y significa «sin alternativas».
62
64
  */
63
- outputGroups?: {
64
- /** Entity field containing the array of groups (e.g. 'outputs', 'branches') */
65
- field?: string;
66
- /** Field within each group for display name (e.g. 'name') */
67
- nameField?: string;
68
- /** Field within each group for summary/description (e.g. 'field', 'filterMode') */
69
- summaryField?: string;
70
- /**
71
- * Varios campos del grupo, unidos por espacios, cuando el resumen que
72
- * de verdad identifica al grupo no cabe en uno solo.
73
- *
74
- * Existe por las reglas del router: su condición son TRES campos
75
- * (`field`, `operator`, `value`) y el modal a medida que se retiró las
76
- * pintaba juntas — «status eq active». Con un único `summaryField` una
77
- * regla sin `label` quedaba en «rule 1» y «status», que no dice a qué
78
- * se está conectando uno.
79
- *
80
- * Los campos vacíos se saltan, para que una regla con operador
81
- * `exists` no salga con un espacio colgando al final.
82
- */
83
- summaryFields?: string[];
84
- /** Label shown in the modal (e.g. 'output', 'branch') */
85
- label: string;
86
- /** Entity field for the fallback/else path (e.g. 'elseOutputNodes') */
87
- elseField?: string;
88
- /**
89
- * Fixed output groups — for nodes with a static set of outputs (e.g. approval: approve/reject).
90
- * Each entry maps to a top-level entity field that holds outputNodes[].
91
- * When set, `field`/`nameField` are ignored — modal shows these fixed options.
92
- */
93
- fixedGroups?: Array<{
94
- name: string;
95
- field: string;
96
- }>;
97
- /**
98
- * Optional content-aware gate — the output-group modal only opens
99
- * when `entity[gate.field] === gate.value`. Lets a node opt into
100
- * multi-branch routing only in some modes (e.g. AI Node in
101
- * classifier mode) without trapping every connection in the
102
- * picker the rest of the time. When the gate fails, the
103
- * FlowCanvas falls through to the universal
104
- * `entity.outputNodes` connect path.
105
- */
106
- gate?: {
107
- field: string;
108
- value: unknown;
109
- };
110
- /**
111
- * Qué es el puerto `main` de este nodo MIENTRAS su contenedor está
112
- * vacío. Sin esto, conectar a un nodo que ramifica y todavía no tiene
113
- * ninguna salida escribía una arista en `main` **en silencio**, y el
114
- * usuario no tenía forma de saber qué había pasado con ella.
115
- *
116
- * No hay una respuesta única, y por eso es un dato y no una regla.
117
- * Medido en `api/src/common/pipeline-run.service.ts`:
118
- *
119
- * 'passthrough' — router sin reglas: `rules.length === 0
120
- * ? refsForPort(entity, MAIN_PORT) : []`. La
121
- * arista SÍ dispara... hasta que se cree la
122
- * primera regla, y entonces deja de hacerlo
123
- * sin avisar.
124
- * 'never-dispatched' — split: no tiene salida `main`, y el propio
125
- * despachador registra un warning por cada
126
- * arista que se quedó en ese puerto.
127
- *
128
- * Se declara sólo donde se ha medido. Sin declarar, el lienzo se
129
- * comporta como siempre: conecta por el camino universal y no dice
130
- * nada, que es lo que hacía antes de existir este campo.
131
- */
132
- emptyContainerMain?: 'passthrough' | 'never-dispatched';
133
- };
65
+ outputGroups?: OutputGroupsConfig | OutputGroupsConfig[];
134
66
  /**
135
67
  * Maps output fields to alternate payload fields on the source entity.
136
68
  * When a downstream node is connected via a specific output field,
@@ -224,3 +156,106 @@ export interface PrefixMapping {
224
156
  /** Canvas node type used in React Flow nodeTypes map (e.g. "cache-node", "email-action") */
225
157
  canvasType: string;
226
158
  }
159
+ /**
160
+ * Un juego de salidas de un nodo que ramifica.
161
+ *
162
+ * Vivía dentro de `NodeUIConfig.special` como un objeto anónimo; sale aquí
163
+ * [2026-09-22] para poder declarar varios por nodo y para que quien los lea
164
+ * nombre el tipo en vez de repetir su forma.
165
+ */
166
+ export interface OutputGroupsConfig {
167
+ /** Entity field containing the array of groups (e.g. 'outputs', 'branches') */
168
+ field?: string;
169
+ /** Field within each group for display name (e.g. 'name') */
170
+ nameField?: string;
171
+ /** Field within each group for summary/description (e.g. 'field', 'filterMode') */
172
+ summaryField?: string;
173
+ /**
174
+ * Varios campos del grupo, unidos por espacios, cuando el resumen que
175
+ * de verdad identifica al grupo no cabe en uno solo.
176
+ *
177
+ * Existe por las reglas del router: su condición son TRES campos
178
+ * (`field`, `operator`, `value`) y el modal a medida que se retiró las
179
+ * pintaba juntas — «status eq active». Con un único `summaryField` una
180
+ * regla sin `label` quedaba en «rule 1» y «status», que no dice a qué
181
+ * se está conectando uno.
182
+ *
183
+ * Los campos vacíos se saltan, para que una regla con operador
184
+ * `exists` no salga con un espacio colgando al final.
185
+ */
186
+ summaryFields?: string[];
187
+ /** Label shown in the modal (e.g. 'output', 'branch') */
188
+ label: string;
189
+ /** Entity field for the fallback/else path (e.g. 'elseOutputNodes') */
190
+ elseField?: string;
191
+ /**
192
+ * Fixed output groups — for nodes with a static set of outputs (e.g. approval: approve/reject).
193
+ * When set, `field`/`nameField` are ignored — modal shows these fixed options.
194
+ *
195
+ * Cada entrada nombra su puerto de una de dos formas, y sólo una:
196
+ * · `field` — un campo de primer nivel de los de ANTES de que los puertos
197
+ * existieran (`rejectionOutputNodes` → puerto `rejected`); se traduce
198
+ * con el mapa de campos heredados.
199
+ * · `port` — el puerto, tal cual. Es lo que usan las salidas nacidas ya
200
+ * con puertos (las cuatro de sentiment): inventarles un campo heredado
201
+ * que nunca existió sería mentir en el registro.
202
+ */
203
+ fixedGroups?: Array<{
204
+ name: string;
205
+ field?: string;
206
+ port?: string;
207
+ /** Una línea de qué entrega ese puerto, para el modal de conexión. */
208
+ summary?: string;
209
+ }>;
210
+ /**
211
+ * Optional content-aware gate — the output-group modal only opens
212
+ * when `entity[gate.field] === gate.value`. Lets a node opt into
213
+ * multi-branch routing only in some modes (e.g. AI Node in
214
+ * classifier mode) without trapping every connection in the
215
+ * picker the rest of the time. When the gate fails, the
216
+ * FlowCanvas falls through to the universal
217
+ * `entity.outputNodes` connect path.
218
+ */
219
+ gate?: {
220
+ field: string;
221
+ value: unknown;
222
+ };
223
+ /**
224
+ * Qué es el puerto `main` de este nodo MIENTRAS su contenedor está
225
+ * vacío. Sin esto, conectar a un nodo que ramifica y todavía no tiene
226
+ * ninguna salida escribía una arista en `main` **en silencio**, y el
227
+ * usuario no tenía forma de saber qué había pasado con ella.
228
+ *
229
+ * No hay una respuesta única, y por eso es un dato y no una regla.
230
+ * Medido en `api/src/common/pipeline-run.service.ts`:
231
+ *
232
+ * 'passthrough' — router sin reglas: `rules.length === 0
233
+ * ? refsForPort(entity, MAIN_PORT) : []`. La
234
+ * arista SÍ dispara... hasta que se cree la
235
+ * primera regla, y entonces deja de hacerlo
236
+ * sin avisar.
237
+ * 'never-dispatched' — split: no tiene salida `main`, y el propio
238
+ * despachador registra un warning por cada
239
+ * arista que se quedó en ese puerto.
240
+ *
241
+ * Se declara sólo donde se ha medido. Sin declarar, el lienzo se
242
+ * comporta como siempre: conecta por el camino universal y no dice
243
+ * nada, que es lo que hacía antes de existir este campo.
244
+ */
245
+ emptyContainerMain?: 'passthrough' | 'never-dispatched';
246
+ }
247
+ /**
248
+ * El juego de salidas que aplica AHORA a esta entidad, o `undefined` si el
249
+ * nodo no ramifica en este momento.
250
+ *
251
+ * Es la única lectura de `special.outputGroups` que entiende las dos formas
252
+ * (uno o varios) y las dos puertas (con `gate` o sin ella). Todo el que
253
+ * pregunte tiene que pasar por aquí: el lienzo, el modal de conexión y el
254
+ * despachador. Escribirlo cada uno a su manera es como el router acabó con un
255
+ * camino propio que nadie arregló al arreglar el de los demás.
256
+ */
257
+ export declare function outputGroupsFor(config: NodeUIConfig | undefined | null, entity: Record<string, unknown> | undefined | null): OutputGroupsConfig | undefined;
258
+ /** Todos los juegos declarados, pasen o no su puerta. Para quien necesita el
259
+ * UNIVERSO de puertos que un tipo puede emitir (la auditoría de puertos),
260
+ * no el de ahora mismo. */
261
+ export declare function todosLosOutputGroups(config: NodeUIConfig | undefined | null): OutputGroupsConfig[];
package/dist/types.js CHANGED
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.singleMeta = singleMeta;
4
4
  exports.iterableMeta = iterableMeta;
5
+ exports.outputGroupsFor = outputGroupsFor;
6
+ exports.todosLosOutputGroups = todosLosOutputGroups;
5
7
  /** Helper to create _meta for single-object outputs */
6
8
  function singleMeta() {
7
9
  return { iterable: false, count: 1 };
@@ -10,3 +12,29 @@ function singleMeta() {
10
12
  function iterableMeta(field, count) {
11
13
  return { iterable: true, iterateField: field, count };
12
14
  }
15
+ /**
16
+ * El juego de salidas que aplica AHORA a esta entidad, o `undefined` si el
17
+ * nodo no ramifica en este momento.
18
+ *
19
+ * Es la única lectura de `special.outputGroups` que entiende las dos formas
20
+ * (uno o varios) y las dos puertas (con `gate` o sin ella). Todo el que
21
+ * pregunte tiene que pasar por aquí: el lienzo, el modal de conexión y el
22
+ * despachador. Escribirlo cada uno a su manera es como el router acabó con un
23
+ * camino propio que nadie arregló al arreglar el de los demás.
24
+ */
25
+ function outputGroupsFor(config, entity) {
26
+ const declarado = config?.special?.outputGroups;
27
+ if (!declarado)
28
+ return undefined;
29
+ const lista = Array.isArray(declarado) ? declarado : [declarado];
30
+ return lista.find((c) => !c.gate || entity?.[c.gate.field] === c.gate.value);
31
+ }
32
+ /** Todos los juegos declarados, pasen o no su puerta. Para quien necesita el
33
+ * UNIVERSO de puertos que un tipo puede emitir (la auditoría de puertos),
34
+ * no el de ahora mismo. */
35
+ function todosLosOutputGroups(config) {
36
+ const declarado = config?.special?.outputGroups;
37
+ if (!declarado)
38
+ return [];
39
+ return Array.isArray(declarado) ? declarado : [declarado];
40
+ }
package/dist/ui.js CHANGED
@@ -6,6 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.PREFIX_TO_TYPE = exports.NODE_UI = void 0;
8
8
  exports.resolveNodeId = resolveNodeId;
9
+ const sentiment_js_1 = require("./sentiment.js");
9
10
  exports.NODE_UI = {
10
11
  // ── Source nodes ──
11
12
  // Una fuente arranca el flujo: `fromNodes: false` significa que nada puede
@@ -49,7 +50,15 @@ exports.NODE_UI = {
49
50
  // no handle and no route. In any other outputMode the node falls back to
50
51
  // the single right-out handle NodeShell renders when no dynamic outputs
51
52
  // materialize.
52
- ai: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { loopBackAllowed: true, connectModal: 'output-group', outputGroups: { field: 'categories', nameField: 'name', summaryField: 'description', label: 'category', gate: { field: 'outputMode', value: 'classifier' } } } },
53
+ // En modo `sentiment` (2026-09-22) el abanico son CUATRO puertos fijos
54
+ // —negative / neutral / positive / unclear— y no un contenedor del usuario:
55
+ // el modelo elige entre las tres primeras y el nodo manda a `unclear` lo que
56
+ // no llega al umbral de confianza. Por eso el AI Node declara DOS juegos de
57
+ // salidas, uno por modo; `outputGroupsFor` elige el que toca.
58
+ ai: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { loopBackAllowed: true, connectModal: 'output-group', outputGroups: [
59
+ { field: 'categories', nameField: 'name', summaryField: 'description', label: 'category', gate: { field: 'outputMode', value: 'classifier' } },
60
+ { label: 'sentiment', gate: { field: 'outputMode', value: 'sentiment' }, fixedGroups: sentiment_js_1.SENTIMENT_PORTS.map((p) => ({ name: sentiment_js_1.SENTIMENT_PORT_LABEL[p], port: p, summary: sentiment_js_1.SENTIMENT_PORT_SUMMARY[p] })) },
61
+ ] } },
53
62
  markdown: { fromNodes: true, toNodes: true, special: { loopBackAllowed: true } },
54
63
  fileTransform: { fromNodes: true, toNodes: true },
55
64
  limit: { fromNodes: true, toNodes: true, special: { loopBackAllowed: true } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/node-types",
3
- "version": "1.85.0",
3
+ "version": "1.87.0",
4
4
  "description": "Shared node type definitions, connection rules, and dispatch config for HostWebhook",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",