@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
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Sentiment analysis en el AI Node — las etiquetas, los puertos y el orden.
3
+ *
4
+ * ## Por qué vive aquí y no en cada repo
5
+ *
6
+ * Las tres etiquetas son a la vez TRES cosas: el `enum` del esquema que se le
7
+ * da al modelo, el nombre de los puertos por los que el nodo despacha, y las
8
+ * pastillas de la tarjeta del lienzo. Si el api, hw-nodes y el dashboard
9
+ * escriben su propia lista, la primera que alguien reordene o rebautice deja
10
+ * una arista apuntando a un puerto que ya no existe — y eso no lo dice ningún
11
+ * compilador. Es el mismo motivo por el que las operaciones de cada nodo
12
+ * viven en este paquete.
13
+ *
14
+ * ## Por qué `unclear` no es una etiqueta más
15
+ *
16
+ * El modelo elige entre las TRES de `SENTIMENT_LABELS`; `unclear` no se le
17
+ * ofrece nunca. Es lo que hace el NODO cuando la confianza queda por debajo
18
+ * del umbral (`sentimentThreshold`) o cuando la respuesta no se puede leer.
19
+ * Ofrecérsela al modelo sería darle una salida cómoda para no mojarse, que es
20
+ * justo lo contrario de lo que se busca al poner un umbral.
21
+ *
22
+ * A diferencia del `other` del clasificador, `unclear` SÍ tiene puerto: el
23
+ * caso de uso es mandar a revisión humana lo que el modelo no ve claro, y sin
24
+ * puerto eso obligaba a adivinar por qué un item no salió por ninguna rama.
25
+ */
26
+ /** Lo que el modelo puede contestar, en orden de peor a mejor. */
27
+ export const SENTIMENT_LABELS = ['negative', 'neutral', 'positive'];
28
+ /** La salida del nodo cuando no hay etiqueta fiable. No se le ofrece al modelo. */
29
+ export const SENTIMENT_UNCLEAR = 'unclear';
30
+ /** Los cuatro puertos del modo sentiment, en el orden en que se pintan. */
31
+ export const SENTIMENT_PORTS = [
32
+ ...SENTIMENT_LABELS,
33
+ SENTIMENT_UNCLEAR,
34
+ ];
35
+ /** El nombre que ve la persona, por puerto. */
36
+ export const SENTIMENT_PORT_LABEL = {
37
+ negative: 'Negative',
38
+ neutral: 'Neutral',
39
+ positive: 'Positive',
40
+ unclear: 'Unclear',
41
+ };
42
+ /** Qué significa cada puerto, para el modal de conexión y la doc. */
43
+ export const SENTIMENT_PORT_SUMMARY = {
44
+ negative: 'Complaints, frustration, criticism',
45
+ neutral: 'Factual, mixed, or no clear feeling',
46
+ positive: 'Praise, satisfaction, enthusiasm',
47
+ unclear: 'Below the confidence floor, or unreadable',
48
+ };
49
+ /** ¿Es una de las tres que el modelo puede contestar? */
50
+ export function esEtiquetaDeSentimiento(v) {
51
+ return (typeof v === 'string' &&
52
+ SENTIMENT_LABELS.includes(v));
53
+ }
@@ -34,7 +34,8 @@ export interface SheetsToolkitSpec {
34
34
  toolName: string;
35
35
  description: string;
36
36
  parameters: SheetsToolkitParameter[];
37
- /** Escribe en la hoja del usuario. Lo usa la confirmación previa del AI Node. */
37
+ /** Pisa o borra. Lo pone `marcarDestructivas` leyendo el verbo del
38
+ * nombre (ver `clase-de-herramienta.ts`), no se escribe a mano. */
38
39
  destructive?: boolean;
39
40
  }
40
41
  export declare const SHEETS_TOOLKIT_SPECS: SheetsToolkitSpec[];
@@ -19,6 +19,7 @@
19
19
  * pueda usarse sin adaptador. Los textos van en inglés porque los lee el modelo
20
20
  * y quien mire la lista de herramientas del servidor MCP.
21
21
  */
22
+ import { marcarDestructivas } from './clase-de-herramienta.js';
22
23
  const p = (name, description, required = true, type = 'string') => ({ name, type, description, required });
23
24
  /*
24
25
  * La hoja y la pestaña del nodo van como **defecto**, y el modelo puede
@@ -41,14 +42,13 @@ const p = (name, description, required = true, type = 'string') => ({ name, type
41
42
  const hoja = () => p('spreadsheetId', "Id of the Google Sheets document — the long chunk of its URL, between /d/ and /edit.");
42
43
  const pestana = () => p('sheetName', 'Name of the tab inside the document, exactly as it reads on its tab strip.');
43
44
  const fila = (que) => p('row', `JSON object holding ${que}. Keys are the sheet's **header names**, spelled exactly as they appear in its title row: {"Name": "Ariel", "Email": "a@b.c"}. A key that is not a header makes the call fail, so when you do not know the headers, call get_rows first.`);
44
- export const SHEETS_TOOLKIT_SPECS = [
45
+ export const SHEETS_TOOLKIT_SPECS = marcarDestructivas([
45
46
  {
46
47
  operation: 'appendRow',
47
48
  label: 'Append row',
48
49
  toolName: 'append_row',
49
50
  description: 'Add a new row at the bottom of the tab. It does not check whether the row already exists — when it might, use append_or_update_row instead so you do not duplicate it.',
50
51
  parameters: [fila('the values of the new row'), hoja(), pestana()],
51
- destructive: true,
52
52
  },
53
53
  {
54
54
  operation: 'updateRow',
@@ -62,7 +62,6 @@ export const SHEETS_TOOLKIT_SPECS = [
62
62
  hoja(),
63
63
  pestana(),
64
64
  ],
65
- destructive: true,
66
65
  },
67
66
  {
68
67
  operation: 'appendOrUpdateRow',
@@ -76,7 +75,6 @@ export const SHEETS_TOOLKIT_SPECS = [
76
75
  hoja(),
77
76
  pestana(),
78
77
  ],
79
- destructive: true,
80
78
  },
81
79
  {
82
80
  operation: 'readRange',
@@ -113,9 +111,8 @@ export const SHEETS_TOOLKIT_SPECS = [
113
111
  * en el nodo, y la api la aplica.
114
112
  */
115
113
  parameters: [p('title', 'Name of the new document.')],
116
- destructive: true,
117
114
  },
118
- ];
115
+ ]);
119
116
  /** Las herramientas por nombre, para despachar una llamada del modelo. */
120
117
  export const SHEETS_TOOLKIT_BY_TOOL_NAME = Object.fromEntries(SHEETS_TOOLKIT_SPECS.map((s) => [s.toolName, s]));
121
118
  /**
@@ -38,9 +38,8 @@ export interface SlackToolkitSpec {
38
38
  /** Descripción (más reglas de uso) que ve el LLM. */
39
39
  description: string;
40
40
  parameters: SlackToolkitParameter[];
41
- /** Operaciones irreversibles o que cambian privilegios. La capa MCP las
42
- * bloquea cuando el nodo de IA lleva `requireConfirmationForDestructive`,
43
- * salvo que la llamada traiga confirmación explícita. */
41
+ /** Pisa o borra. Lo pone `marcarDestructivas` leyendo el verbo del
42
+ * nombre (ver `clase-de-herramienta.ts`), no se escribe a mano. */
44
43
  destructive?: boolean;
45
44
  }
46
45
  export declare const SLACK_TOOLKIT_SPECS: SlackToolkitSpec[];
@@ -23,8 +23,9 @@
23
23
  * y quien mire la lista de herramientas del servidor MCP.
24
24
  */
25
25
  import { camposDeSlackNoDisponibles, operacionesDeSlackPara, } from './slack-operations.js';
26
+ import { marcarDestructivas } from './clase-de-herramienta.js';
26
27
  const p = (name, description, required = true, type = 'string') => ({ name, type, description, required });
27
- export const SLACK_TOOLKIT_SPECS = [
28
+ export const SLACK_TOOLKIT_SPECS = marcarDestructivas([
28
29
  // ── Messages ─────────────────────────────────────────────────────
29
30
  {
30
31
  operation: 'sendMessage',
@@ -63,7 +64,6 @@ export const SLACK_TOOLKIT_SPECS = [
63
64
  p('channel', 'Channel where the message lives.'),
64
65
  p('ts', 'Timestamp of the message to delete.'),
65
66
  ],
66
- destructive: true,
67
67
  },
68
68
  {
69
69
  operation: 'sendEphemeral',
@@ -142,7 +142,6 @@ export const SLACK_TOOLKIT_SPECS = [
142
142
  p('name', 'Channel name — lowercase, no spaces, max 80 chars.'),
143
143
  p('isPrivate', 'Create a private channel instead of public. Default false.', false, 'boolean'),
144
144
  ],
145
- destructive: true,
146
145
  },
147
146
  {
148
147
  operation: 'inviteToChannel',
@@ -212,7 +211,7 @@ export const SLACK_TOOLKIT_SPECS = [
212
211
  p('threadTs', 'Thread to attach the file to.', false),
213
212
  ],
214
213
  },
215
- ];
214
+ ]);
216
215
  export const SLACK_TOOLKIT_BY_TOOL_NAME = Object.fromEntries(SLACK_TOOLKIT_SPECS.map((s) => [s.toolName, s]));
217
216
  /**
218
217
  * Las herramientas que ofrecerle al modelo con ESTA credencial.
@@ -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
@@ -19,6 +19,7 @@
19
19
  * usarse sin adaptador. Los textos van en inglés porque los lee el modelo y
20
20
  * quien mire la lista de herramientas del servidor MCP.
21
21
  */
22
+ import { marcarDestructivas } from './clase-de-herramienta.js';
22
23
  const p = (name, description, required = true, type = 'string') => ({ name, type, description, required });
23
24
  /*
24
25
  * `sendAndWaitForResponse` NO está aquí, y es una decisión, no un olvido: la op
@@ -33,7 +34,7 @@ const p = (name, description, required = true, type = 'string') => ({ name, type
33
34
  * campo en ninguna de las dos capas, y añadirlo cambiaría cuándo el AI Node pide
34
35
  * confirmación. Eso es otra decisión, no parte de mudar el esquema de sitio.
35
36
  */
36
- export const TELEGRAM_TOOLKIT_SPECS = [
37
+ export const TELEGRAM_TOOLKIT_SPECS = marcarDestructivas([
37
38
  {
38
39
  operation: 'sendMessage',
39
40
  label: 'Send message',
@@ -178,6 +179,6 @@ export const TELEGRAM_TOOLKIT_SPECS = [
178
179
  p('showAlert', 'Optional. true = modal alert; false (default) = toast notification.', false, 'boolean'),
179
180
  ],
180
181
  },
181
- ];
182
+ ]);
182
183
  /** Las herramientas por nombre, para despachar una llamada del modelo. */
183
184
  export const TELEGRAM_TOOLKIT_BY_TOOL_NAME = Object.fromEntries(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,74 @@
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
+ import { GMAIL_ALL_TOOLKIT_SPECS, NATIVE_EMAIL_TOOLKIT_SPECS } from './gmail-operations.js';
24
+ import { GOOGLE_CALENDAR_TOOLKIT_SPECS } from './calendar-toolkit.js';
25
+ import { DRIVE_TOOLKIT_SPECS } from './drive-toolkit.js';
26
+ import { TELEGRAM_TOOLKIT_SPECS } from './telegram-toolkit.js';
27
+ import { DISCORD_TOOLKIT_SPECS } from './discord-toolkit.js';
28
+ import { SLACK_TOOLKIT_SPECS } from './slack-toolkit.js';
29
+ import { SHEETS_TOOLKIT_SPECS } from './sheets-toolkit.js';
30
+ import { DOCS_TOOLKIT_SPECS } from './docs-toolkit.js';
31
+ import { MONGO_TOOLKIT_SPECS } from './mongo-toolkit.js';
32
+ import { POSTGRES_TOOLKIT_SPECS } from './postgres-toolkit.js';
33
+ export const TOOLKITS_POR_TIPO = Object.freeze({
34
+ gmailAction: GMAIL_ALL_TOOLKIT_SPECS,
35
+ emailAction: NATIVE_EMAIL_TOOLKIT_SPECS,
36
+ calendarAction: GOOGLE_CALENDAR_TOOLKIT_SPECS,
37
+ driveAction: DRIVE_TOOLKIT_SPECS,
38
+ telegramAction: TELEGRAM_TOOLKIT_SPECS,
39
+ discordAction: DISCORD_TOOLKIT_SPECS,
40
+ slackAction: SLACK_TOOLKIT_SPECS,
41
+ sheetsAction: SHEETS_TOOLKIT_SPECS,
42
+ docsAction: DOCS_TOOLKIT_SPECS,
43
+ mongoAction: MONGO_TOOLKIT_SPECS,
44
+ postgresAction: POSTGRES_TOOLKIT_SPECS,
45
+ });
46
+ /**
47
+ * La operación de un toolkit, buscada por su nombre interno (`updateRow`) O
48
+ * por el nombre de su herramienta (`update_row`).
49
+ *
50
+ * Las dos formas porque las dos llegan: el AI Node guarda `operationOverride`
51
+ * con el nombre interno, y el servidor MCP recibe el nombre de la herramienta.
52
+ */
53
+ export function operacionDeToolkit(nodeType, operacionONombre) {
54
+ if (!nodeType || !operacionONombre)
55
+ return null;
56
+ const toolkit = TOOLKITS_POR_TIPO[nodeType];
57
+ if (!toolkit)
58
+ return null;
59
+ return (toolkit.find((s) => s.operation === operacionONombre) ??
60
+ toolkit.find((s) => s.toolName === operacionONombre) ??
61
+ null);
62
+ }
63
+ /**
64
+ * ¿Es destructiva esta operación de toolkit?
65
+ *
66
+ * `null` cuando no se sabe —el tipo no es un toolkit de este paquete, o la
67
+ * operación no existe en él—. Quien llama decide qué hace con el «no sé»; el
68
+ * gate del AI Node lo combina con los patrones de nombre, que siguen ahí para
69
+ * las herramientas que no son de toolkit (MCP, HTTP…).
70
+ */
71
+ export function esOperacionDestructiva(nodeType, operacionONombre) {
72
+ const op = operacionDeToolkit(nodeType, operacionONombre);
73
+ return op ? op.destructive === true : null;
74
+ }
@@ -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/esm/types.js CHANGED
@@ -6,3 +6,29 @@ export function singleMeta() {
6
6
  export function iterableMeta(field, count) {
7
7
  return { iterable: true, iterateField: field, count };
8
8
  }
9
+ /**
10
+ * El juego de salidas que aplica AHORA a esta entidad, o `undefined` si el
11
+ * nodo no ramifica en este momento.
12
+ *
13
+ * Es la única lectura de `special.outputGroups` que entiende las dos formas
14
+ * (uno o varios) y las dos puertas (con `gate` o sin ella). Todo el que
15
+ * pregunte tiene que pasar por aquí: el lienzo, el modal de conexión y el
16
+ * despachador. Escribirlo cada uno a su manera es como el router acabó con un
17
+ * camino propio que nadie arregló al arreglar el de los demás.
18
+ */
19
+ export function outputGroupsFor(config, entity) {
20
+ const declarado = config?.special?.outputGroups;
21
+ if (!declarado)
22
+ return undefined;
23
+ const lista = Array.isArray(declarado) ? declarado : [declarado];
24
+ return lista.find((c) => !c.gate || entity?.[c.gate.field] === c.gate.value);
25
+ }
26
+ /** Todos los juegos declarados, pasen o no su puerta. Para quien necesita el
27
+ * UNIVERSO de puertos que un tipo puede emitir (la auditoría de puertos),
28
+ * no el de ahora mismo. */
29
+ export function todosLosOutputGroups(config) {
30
+ const declarado = config?.special?.outputGroups;
31
+ if (!declarado)
32
+ return [];
33
+ return Array.isArray(declarado) ? declarado : [declarado];
34
+ }
package/dist/esm/ui.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * Dashboard UI config — handle positions, dot visibility, canvas prefixes.
3
3
  * Only imported by the Dashboard. Broker and API don't need this.
4
4
  */
5
+ import { SENTIMENT_PORTS, SENTIMENT_PORT_LABEL, SENTIMENT_PORT_SUMMARY, } from './sentiment.js';
5
6
  export const NODE_UI = {
6
7
  // ── Source nodes ──
7
8
  // Una fuente arranca el flujo: `fromNodes: false` significa que nada puede
@@ -45,7 +46,15 @@ export const NODE_UI = {
45
46
  // no handle and no route. In any other outputMode the node falls back to
46
47
  // the single right-out handle NodeShell renders when no dynamic outputs
47
48
  // materialize.
48
- 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' } } } },
49
+ // En modo `sentiment` (2026-09-22) el abanico son CUATRO puertos fijos
50
+ // —negative / neutral / positive / unclear— y no un contenedor del usuario:
51
+ // el modelo elige entre las tres primeras y el nodo manda a `unclear` lo que
52
+ // no llega al umbral de confianza. Por eso el AI Node declara DOS juegos de
53
+ // salidas, uno por modo; `outputGroupsFor` elige el que toca.
54
+ ai: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { loopBackAllowed: true, connectModal: 'output-group', outputGroups: [
55
+ { field: 'categories', nameField: 'name', summaryField: 'description', label: 'category', gate: { field: 'outputMode', value: 'classifier' } },
56
+ { label: 'sentiment', gate: { field: 'outputMode', value: 'sentiment' }, fixedGroups: SENTIMENT_PORTS.map((p) => ({ name: SENTIMENT_PORT_LABEL[p], port: p, summary: SENTIMENT_PORT_SUMMARY[p] })) },
57
+ ] } },
49
58
  markdown: { fromNodes: true, toNodes: true, special: { loopBackAllowed: true } },
50
59
  fileTransform: { fromNodes: true, toNodes: true },
51
60
  limit: { fromNodes: true, toNodes: true, special: { loopBackAllowed: true } },
@@ -151,6 +151,8 @@ export interface GmailToolkitSpec {
151
151
  toolName: string;
152
152
  description: string;
153
153
  parameters: GmailToolkitParameter[];
154
+ /** Pisa o borra. Lo pone `marcarDestructivas` leyendo el verbo del
155
+ * nombre (ver `clase-de-herramienta.ts`), no se escribe a mano. */
154
156
  destructive?: boolean;
155
157
  }
156
158
  export declare const GMAIL_TOOLKIT_SPECS: GmailToolkitSpec[];
@@ -1,8 +1,4 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GMAIL_SEND_LEGACY_FIELDS = exports.NATIVE_EMAIL_TOOLKIT_BY_TOOL_NAME = exports.GMAIL_TOOLKIT_BY_TOOL_NAME = exports.NATIVE_EMAIL_TOOLKIT_SPECS = exports.GMAIL_ALL_TOOLKIT_SPECS = exports.GMAIL_SEND_AND_WAIT_TOOL_SPEC = exports.GMAIL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_OPERATIONS = exports.GMAIL_DROPDOWN_OPERATIONS = exports.GMAIL_OPERATION_SPECS = exports.GMAIL_OPERATION_GROUPS = exports.GMAIL_OPERATIONS = void 0;
4
- exports.isGmailOperation = isGmailOperation;
5
- exports.resolveGmailSendFields = resolveGmailSendFields;
6
2
  /**
7
3
  * Gmail operation enum — single source of truth across the API, Dashboard,
8
4
  * and Message Broker. Used by:
@@ -17,6 +13,11 @@ exports.resolveGmailSendFields = resolveGmailSendFields;
17
13
  * dashboard owns the UI form schema, the api owns the dispatch logic),
18
14
  * but the canonical list of valid operation strings lives here.
19
15
  */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.GMAIL_SEND_LEGACY_FIELDS = exports.NATIVE_EMAIL_TOOLKIT_BY_TOOL_NAME = exports.GMAIL_TOOLKIT_BY_TOOL_NAME = exports.NATIVE_EMAIL_TOOLKIT_SPECS = exports.GMAIL_ALL_TOOLKIT_SPECS = exports.GMAIL_SEND_AND_WAIT_TOOL_SPEC = exports.GMAIL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_OPERATIONS = exports.GMAIL_DROPDOWN_OPERATIONS = exports.GMAIL_OPERATION_SPECS = exports.GMAIL_OPERATION_GROUPS = exports.GMAIL_OPERATIONS = void 0;
18
+ exports.isGmailOperation = isGmailOperation;
19
+ exports.resolveGmailSendFields = resolveGmailSendFields;
20
+ const clase_de_herramienta_js_1 = require("./clase-de-herramienta.js");
20
21
  exports.GMAIL_OPERATIONS = [
21
22
  'send',
22
23
  // Pause-the-pipeline op — sends an email with Approve / (optional)
@@ -301,7 +302,7 @@ const p = (name, description, required = true, type = 'string') => ({ name, type
301
302
  const ADJUNTOS = p('attachments', 'Optional. Files to attach, taken from the payload: pass the `_file` reference object(s) ' +
302
303
  'exactly as they appear (a single `_file`, or the `_files` / `files` array), as a JSON ' +
303
304
  'string. Never invent one. Up to 5 files, 25 MB in total.', false);
304
- exports.GMAIL_TOOLKIT_SPECS = [
305
+ exports.GMAIL_TOOLKIT_SPECS = (0, clase_de_herramienta_js_1.marcarDestructivas)([
305
306
  {
306
307
  operation: 'send',
307
308
  label: 'Send message',
@@ -510,7 +511,7 @@ exports.GMAIL_TOOLKIT_SPECS = [
510
511
  p('threadId', 'Gmail thread id.'),
511
512
  ],
512
513
  },
513
- ];
514
+ ]);
514
515
  /**
515
516
  * La op que espera, **fuera** del toolkit y aparte a propósito. Sigue haciendo
516
517
  * falta para dos cosas que no son el toolkit: reconocer la operación de una
@@ -540,12 +541,12 @@ exports.GMAIL_SEND_AND_WAIT_TOOL_SPEC = {
540
541
  ],
541
542
  };
542
543
  /** Todo lo que el nodo sabe ejecutar, se ofrezca o no como herramienta. */
543
- exports.GMAIL_ALL_TOOLKIT_SPECS = [
544
+ exports.GMAIL_ALL_TOOLKIT_SPECS = (0, clase_de_herramienta_js_1.marcarDestructivas)([
544
545
  ...exports.GMAIL_TOOLKIT_SPECS,
545
546
  exports.GMAIL_SEND_AND_WAIT_TOOL_SPEC,
546
- ];
547
+ ]);
547
548
  /** El toolkit del proveedor nativo (Resend): una sola herramienta. */
548
- exports.NATIVE_EMAIL_TOOLKIT_SPECS = [
549
+ exports.NATIVE_EMAIL_TOOLKIT_SPECS = (0, clase_de_herramienta_js_1.marcarDestructivas)([
549
550
  {
550
551
  operation: 'send',
551
552
  label: 'Send email',
@@ -564,7 +565,7 @@ exports.NATIVE_EMAIL_TOOLKIT_SPECS = [
564
565
  p('attachment_file_ids', 'Optional. Comma-separated list of file IDs from `_file.id` refs in the upstream payload (e.g., a File Transform output or a webhook multipart upload). Max 5 files, 30MB total. Do NOT invent IDs — only use values actually present in the current payload.', false),
565
566
  ],
566
567
  },
567
- ];
568
+ ]);
568
569
  exports.GMAIL_TOOLKIT_BY_TOOL_NAME = Object.fromEntries(exports.GMAIL_ALL_TOOLKIT_SPECS.map((s) => [s.toolName, s]));
569
570
  exports.NATIVE_EMAIL_TOOLKIT_BY_TOOL_NAME = Object.fromEntries(exports.NATIVE_EMAIL_TOOLKIT_SPECS.map((s) => [s.toolName, s]));
570
571
  /** Los tres campos que un día vivieron en la raíz. */