@pipefy/pipefy-process-coder 0.1.0 → 0.1.1

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 (159) hide show
  1. package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -262
  2. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -247
  3. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -589
  4. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -1391
  5. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -623
  6. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -636
  7. package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -3642
  8. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -863
  9. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -799
  10. package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -42
  11. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -581
  12. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -503
  13. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -562
  14. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -557
  15. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -609
  16. package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -146
  17. package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -127
  18. package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -91
  19. package/AGENTS.md +465 -456
  20. package/README.md +825 -807
  21. package/bin/pipe.js +25 -4
  22. package/dist/apply/adopt.js +128 -0
  23. package/dist/apply/agentops.js +42 -0
  24. package/dist/apply/compile.js +722 -0
  25. package/dist/apply/execute.js +277 -0
  26. package/dist/apply/flowops.js +293 -0
  27. package/dist/apply/idmap.js +233 -0
  28. package/dist/apply/mutations.js +704 -0
  29. package/dist/apply/registry.js +348 -0
  30. package/dist/apply/types.js +18 -0
  31. package/dist/cli/args.js +79 -0
  32. package/dist/cli.js +187 -0
  33. package/dist/codec/flow.js +164 -0
  34. package/dist/codec/pack.js +88 -0
  35. package/dist/codec/roundtrip.js +86 -0
  36. package/dist/codec/unpack.js +181 -0
  37. package/dist/commands/agents.js +145 -0
  38. package/dist/commands/apply.js +982 -0
  39. package/dist/commands/context.js +79 -0
  40. package/dist/commands/create.js +63 -0
  41. package/dist/commands/diff.js +294 -0
  42. package/dist/commands/flows.js +348 -0
  43. package/dist/commands/misc.js +528 -0
  44. package/dist/commands/plan.js +301 -0
  45. package/dist/commands/pull.js +511 -0
  46. package/dist/commands/runs.js +74 -0
  47. package/dist/commands/skills.js +131 -0
  48. package/dist/commands/verify.js +205 -0
  49. package/dist/config.js +125 -0
  50. package/dist/diff/agents.js +80 -0
  51. package/dist/diff/diff.js +983 -0
  52. package/dist/diff/flow.js +258 -0
  53. package/{src/diff/html.ts → dist/diff/html.js} +274 -322
  54. package/dist/diff/render.js +90 -0
  55. package/dist/model/payload.js +84 -0
  56. package/dist/model/tree.js +7 -0
  57. package/dist/model/volatile.js +54 -0
  58. package/dist/pipefy/agents.js +122 -0
  59. package/dist/pipefy/automations.js +155 -0
  60. package/dist/pipefy/capability.js +84 -0
  61. package/dist/pipefy/client.js +204 -0
  62. package/dist/pipefy/discovery.js +148 -0
  63. package/dist/pipefy/internal.js +315 -0
  64. package/dist/pipefy/ipaas.js +269 -0
  65. package/dist/pipefy/reconstruct.js +674 -0
  66. package/dist/pipefy/reference.js +172 -0
  67. package/dist/pipefy/snapshot.js +185 -0
  68. package/dist/pipefy/toolkit.js +158 -0
  69. package/dist/report/integrations.js +165 -0
  70. package/dist/report/run.js +355 -0
  71. package/dist/util/fsx.js +50 -0
  72. package/dist/util/git.js +25 -0
  73. package/dist/util/json.js +56 -0
  74. package/dist/util/log.js +69 -0
  75. package/dist/util/pool.js +42 -0
  76. package/dist/util/slug.js +23 -0
  77. package/dist/util/tui.js +287 -0
  78. package/dist/validate/index.js +86 -0
  79. package/dist/validate/integrity.js +370 -0
  80. package/dist/validate/reference.js +120 -0
  81. package/dist/validate/schema.js +278 -0
  82. package/dist/workspace/agents.js +245 -0
  83. package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
  84. package/dist/workspace/flows.js +149 -0
  85. package/dist/workspace/layout.js +146 -0
  86. package/dist/workspace/lock.js +83 -0
  87. package/dist/workspace/read.js +168 -0
  88. package/dist/workspace/reference.js +24 -0
  89. package/dist/workspace/stamp.js +234 -0
  90. package/dist/workspace/write.js +194 -0
  91. package/package.json +37 -35
  92. package/src/apply/adopt.ts +0 -150
  93. package/src/apply/agentops.ts +0 -71
  94. package/src/apply/compile.ts +0 -875
  95. package/src/apply/execute.ts +0 -336
  96. package/src/apply/flowops.ts +0 -399
  97. package/src/apply/idmap.ts +0 -241
  98. package/src/apply/mutations.ts +0 -955
  99. package/src/apply/registry.ts +0 -430
  100. package/src/apply/types.ts +0 -134
  101. package/src/cli/args.ts +0 -88
  102. package/src/cli.ts +0 -211
  103. package/src/codec/flow.ts +0 -199
  104. package/src/codec/pack.ts +0 -103
  105. package/src/codec/roundtrip.ts +0 -94
  106. package/src/codec/unpack.ts +0 -198
  107. package/src/commands/agents.ts +0 -184
  108. package/src/commands/apply.ts +0 -1144
  109. package/src/commands/context.ts +0 -119
  110. package/src/commands/create.ts +0 -79
  111. package/src/commands/diff.ts +0 -314
  112. package/src/commands/flows.ts +0 -414
  113. package/src/commands/misc.ts +0 -644
  114. package/src/commands/plan.ts +0 -331
  115. package/src/commands/pull.ts +0 -567
  116. package/src/commands/runs.ts +0 -83
  117. package/src/commands/skills.ts +0 -137
  118. package/src/commands/verify.ts +0 -253
  119. package/src/config.ts +0 -168
  120. package/src/diff/agents.ts +0 -122
  121. package/src/diff/diff.ts +0 -1130
  122. package/src/diff/flow.ts +0 -318
  123. package/src/diff/render.ts +0 -101
  124. package/src/model/payload.ts +0 -154
  125. package/src/model/tree.ts +0 -99
  126. package/src/model/volatile.ts +0 -55
  127. package/src/pipefy/agents.ts +0 -165
  128. package/src/pipefy/automations.ts +0 -219
  129. package/src/pipefy/capability.ts +0 -119
  130. package/src/pipefy/client.ts +0 -267
  131. package/src/pipefy/discovery.ts +0 -209
  132. package/src/pipefy/internal.ts +0 -380
  133. package/src/pipefy/ipaas.ts +0 -365
  134. package/src/pipefy/reconstruct.ts +0 -775
  135. package/src/pipefy/reference.ts +0 -251
  136. package/src/pipefy/snapshot.ts +0 -245
  137. package/src/pipefy/toolkit.ts +0 -200
  138. package/src/report/integrations.ts +0 -231
  139. package/src/report/run.ts +0 -475
  140. package/src/util/fsx.ts +0 -45
  141. package/src/util/git.ts +0 -32
  142. package/src/util/json.ts +0 -55
  143. package/src/util/log.ts +0 -76
  144. package/src/util/pool.ts +0 -48
  145. package/src/util/slug.ts +0 -26
  146. package/src/util/tui.ts +0 -335
  147. package/src/validate/index.ts +0 -123
  148. package/src/validate/integrity.ts +0 -387
  149. package/src/validate/reference.ts +0 -136
  150. package/src/validate/schema.ts +0 -328
  151. package/src/workspace/agents.ts +0 -290
  152. package/src/workspace/flows.ts +0 -191
  153. package/src/workspace/layout.ts +0 -165
  154. package/src/workspace/lock.ts +0 -148
  155. package/src/workspace/read.ts +0 -165
  156. package/src/workspace/reference.ts +0 -24
  157. package/src/workspace/stamp.ts +0 -301
  158. package/src/workspace/write.ts +0 -225
  159. /package/{src → dist}/pipefy/toolkit_bearer.py +0 -0
@@ -0,0 +1,704 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { debug, warn } from "../util/log.js";
3
+ /**
4
+ * Mutation builders.
5
+ *
6
+ * Every shape here is the one the Change Migrator recipes actually send —
7
+ * including the details that are easy to get wrong and that the recipes prove:
8
+ *
9
+ * - `deletePhaseField` needs `pipeUuid` alongside the field id (step 43/63)
10
+ * - `createPhaseField` takes `label`, `phase_id`, `type`, `options`,
11
+ * `description` (step 34)
12
+ * - `createFieldCondition` takes the read shape after stripping labels and
13
+ * renaming `phase → phaseId`, `phaseField → phaseFieldId` (steps 85/86),
14
+ * while `updateFieldCondition` wants `phase_id` (steps 109/110)
15
+ *
16
+ * Argument sets are then filtered against the mutation's real input type, so an
17
+ * argument this Pipefy version does not accept is dropped with a debug line
18
+ * instead of failing the whole call.
19
+ */
20
+ const dropUndefined = (o) => {
21
+ const out = {};
22
+ for (const [k, v] of Object.entries(o))
23
+ if (v !== undefined)
24
+ out[k] = v;
25
+ return out;
26
+ };
27
+ /**
28
+ * Build `mutation($input: <T>!) { <name>(input: $input) { <selection> } }` with
29
+ * the input filtered to the fields the schema accepts.
30
+ */
31
+ export const buildGraphql = async (client, mutation, input, selection) => {
32
+ /**
33
+ * Offline (no token): fall back to the conventional input type name and keep
34
+ * every argument. Classification and ordering are what `pipe plan` is for, and
35
+ * both are pure functions of the ChangeSet — so they must not need a network.
36
+ * The steps are marked unverified, and `pipe apply` re-compiles with a client.
37
+ */
38
+ const typeName = (client ? await client.mutationInputType(mutation) : null) ?? `${mutation[0]?.toUpperCase()}${mutation.slice(1)}Input`;
39
+ const accepted = client ? await client.inputFields(typeName) : null;
40
+ const required = client ? await client.requiredInputFields(typeName) : new Set();
41
+ const filtered = {};
42
+ const dropped = [];
43
+ for (const [k, v] of Object.entries(dropUndefined(input))) {
44
+ if (accepted && !accepted.has(k)) {
45
+ dropped.push(k);
46
+ continue;
47
+ }
48
+ filtered[k] = v;
49
+ }
50
+ if (dropped.length)
51
+ debug(`${mutation}: dropped args not in ${typeName} — ${dropped.join(', ')}`);
52
+ const missingRequired = [...required].filter((k) => filtered[k] === undefined || filtered[k] === null);
53
+ if (missingRequired.length) {
54
+ debug(`${mutation}: missing required ${typeName} fields — ${missingRequired.join(', ')}`);
55
+ }
56
+ return {
57
+ mutation,
58
+ ...(missingRequired.length ? { missingRequired } : {}),
59
+ query: `mutation($input: ${typeName}!) {\n ${mutation}(input: $input) {\n clientMutationId\n ${selection}\n }\n}`,
60
+ variables: { input: filtered },
61
+ };
62
+ };
63
+ // ── phases ───────────────────────────────────────────────────────────────────
64
+ export const createPhase = (client, pipeId, phase) => buildGraphql(client, 'createPhase', {
65
+ pipe_id: String(pipeId),
66
+ name: phase['name'],
67
+ done: phase['done'],
68
+ description: phase['description'],
69
+ lateness_time: phase['lateness_time'],
70
+ can_receive_card_directly_from_draft: phase['can_receive_card_directly_from_draft'],
71
+ only_admin_can_move_to_previous: phase['only_admin_can_move_to_previous'],
72
+ // createPhase takes index: Float — updatePhase does not (PLAN.md §2).
73
+ index: phase['index'],
74
+ uuid: phase['uuid'],
75
+ }, 'phase { id name index }');
76
+ export const updatePhase = (client, phase) => buildGraphql(client, 'updatePhase', {
77
+ id: String(phase['id']),
78
+ name: phase['name'],
79
+ done: phase['done'],
80
+ description: phase['description'],
81
+ lateness_time: phase['lateness_time'],
82
+ can_receive_card_directly_from_draft: phase['can_receive_card_directly_from_draft'],
83
+ only_admin_can_move_to_previous: phase['only_admin_can_move_to_previous'],
84
+ }, 'phase { id name }');
85
+ export const deletePhase = (client, phaseId) => buildGraphql(client, 'deletePhase', { id: String(phaseId) }, 'success');
86
+ /**
87
+ * `phaseSettings` takes `SettingsInput`, keyed on `id` — not `phaseId`, which is
88
+ * what an earlier version sent. It is also a superset of `updatePhase`: name,
89
+ * description, done, lateness, colour and the team all live here.
90
+ */
91
+ export const phaseSettings = (client, phaseId, settings) => buildGraphql(client, 'phaseSettings', {
92
+ id: String(phaseId),
93
+ identifyTask: settings['identify_task'],
94
+ latenessTime: settings['lateness_time'],
95
+ teamMemberIds: Array.isArray(settings['team_member_ids'])
96
+ ? settings['team_member_ids'].map(String)
97
+ : undefined,
98
+ }, 'clientMutationId');
99
+ // ── fields ───────────────────────────────────────────────────────────────────
100
+ /**
101
+ * `label` is NON_NULL on update, so the compiler always sends the full object
102
+ * rather than a sparse patch (PLAN.md §2).
103
+ */
104
+ const fieldArgs = (field) => ({
105
+ label: field['label'],
106
+ options: field['options'],
107
+ description: field['description'],
108
+ help: field['help'],
109
+ required: field['required'],
110
+ editable: field['editable'],
111
+ minimal_view: field['minimal_view'],
112
+ custom_validation: field['custom_validation'],
113
+ unique: field['unique'],
114
+ index: field['index'],
115
+ sync_with_card: field['card_synced'],
116
+ can_create_database_record: field['can_create_connected_cards'],
117
+ can_have_multiple_database_records: field['can_connect_multiple_cards'],
118
+ child_must_exist_to_finish_parent: field['child_must_exist_to_finish_parent'],
119
+ all_children_must_be_done_to_move_parent: field['all_children_must_be_done_to_move_parent'],
120
+ all_children_must_be_done_to_finish_parent: field['all_children_must_be_done_to_finish_parent'],
121
+ });
122
+ export const createPhaseField = (client, phaseId, field) => buildGraphql(client, 'createPhaseField', {
123
+ phase_id: phaseId,
124
+ type: field['type_id'],
125
+ uuid: field['uuid'],
126
+ connected_repo_id: field['connected_pipe_id'] ? String(field['connected_pipe_id']) : undefined,
127
+ ...fieldArgs(field),
128
+ }, 'phase_field { id internal_id uuid }');
129
+ export const updatePhaseField = (client, field) => buildGraphql(client, 'updatePhaseField', { id: String(field['id']), uuid: field['uuid'], ...fieldArgs(field) }, 'phase_field { id internal_id }');
130
+ // ── database (table) fields ──────────────────────────────────────────────────
131
+ /**
132
+ * A database's columns, which are `table_fields` on the write side even though a
133
+ * database's *payload* calls them `fields` like a pipe's.
134
+ *
135
+ * Every identifier below was measured, not inferred (VALIDATION.md §3g). The
136
+ * short version: **a table field is addressed by its slug** — for update, for
137
+ * delete and inside the order call. That is the exact opposite of
138
+ * `updatePhaseField`, whose `id` is the numeric internal id, and sending the
139
+ * internal id or the uuid here is refused with `Field not found with id: <the id
140
+ * that plainly exists>`.
141
+ *
142
+ * Two arguments this input does not have, both load-bearing:
143
+ *
144
+ * - **no `index`.** A create lands at the end and order is a separate
145
+ * `setTableFieldOrder` call.
146
+ * - **no `type`** on update. A type change would mean delete + create, which
147
+ * destroys that column in every record, so it is refused rather than done
148
+ * quietly — the same rule pipe fields already have.
149
+ */
150
+ const tableFieldArgs = (field) => ({
151
+ label: field['label'],
152
+ options: field['options'],
153
+ description: field['description'],
154
+ help: field['help'],
155
+ required: field['required'],
156
+ minimal_view: field['minimal_view'],
157
+ custom_validation: field['custom_validation'],
158
+ unique: field['unique'],
159
+ canConnectExisting: field['can_search_connected_cards'],
160
+ canConnectMultiples: field['can_connect_multiple_cards'],
161
+ canCreateNewConnected: field['can_create_connected_cards'],
162
+ childMustExistToFinishParent: field['child_must_exist_to_finish_parent'],
163
+ allChildrenMustBeDoneToFinishParent: field['all_children_must_be_done_to_finish_parent'],
164
+ });
165
+ /** `table_id` accepts either the api id or the numeric internal id — both measured. */
166
+ export const createTableField = (client, tableId, field) => buildGraphql(client, 'createTableField', {
167
+ table_id: String(tableId),
168
+ type: field['type_id'],
169
+ connectedRepoId: field['connected_pipe_id'] ? String(field['connected_pipe_id']) : undefined,
170
+ ...tableFieldArgs(field),
171
+ }, 'table_field { id internal_id uuid }');
172
+ /**
173
+ * `id` is the field's **slug**, and `table_id` is required alongside it.
174
+ *
175
+ * Measured: the numeric internal id and the uuid are both refused, and the slug
176
+ * both succeeds and actually changes the row — the "accepted but unchanged" case
177
+ * this project has been bitten by elsewhere does not happen here.
178
+ */
179
+ export const updateTableField = (client, tableId, field) => buildGraphql(client, 'updateTableField', { id: slugOf(field), table_id: String(tableId), ...tableFieldArgs(field) }, 'table_field { id internal_id label }');
180
+ /** Also the slug, also with `table_id`. Destroys that column in every record. */
181
+ export const deleteTableField = (client, tableId, field) => buildGraphql(client, 'deleteTableField', { id: slugOf(field), table_id: String(tableId) }, 'success');
182
+ /**
183
+ * Set-replacing per table, and it **returns the applied order**.
184
+ *
185
+ * That return is worth having: unlike `setFieldConditionOrder`, which answers a
186
+ * 200 and nothing else, this one lets the step assert its own effect instead of
187
+ * trusting the status. `field_ids` are slugs.
188
+ */
189
+ export const setTableFieldOrder = (client, tableId, fields) => buildGraphql(client, 'setTableFieldOrder', { table_id: String(tableId), field_ids: fields.map(slugOf) }, 'table_fields { id index }');
190
+ /**
191
+ * The field's slug, which the payload stores in `slug` and the table API returns
192
+ * as `id`. Falls back to the numeric id only so a request fails loudly rather
193
+ * than silently addressing nothing.
194
+ */
195
+ const slugOf = (field) => String(field['slug'] ?? field['id'] ?? '');
196
+ /** Needs the pipe uuid as well as the field id — recipe steps 42/43. */
197
+ /**
198
+ * `deletePhaseField.id` is the field's **slug**, not the numeric id the payload
199
+ * stores as `id`.
200
+ *
201
+ * A field has three identifiers and the mutations disagree about which one they
202
+ * take: `updatePhaseField.id` accepts the numeric internal id, `archiveField`
203
+ * wants the uuid, and this one wants the slug. Sent the numeric id it answers
204
+ * `Field not found with id: <that id>` — about a field that is plainly there,
205
+ * which reads like a stale workspace rather than a wrong argument.
206
+ *
207
+ * Measured on a throwaway field: delete by internal id was refused with "Phase
208
+ * field not found with id", delete by slug succeeded. The slug is in the payload
209
+ * already, so nothing needs looking up.
210
+ */
211
+ export const deletePhaseField = (client, field, pipeUuid) => {
212
+ const slug = field['slug'];
213
+ return buildGraphql(client, 'deletePhaseField', {
214
+ /**
215
+ * Falls back to the id only when the row has no slug, which happens for a
216
+ * field the workspace authored and never pulled. That request will fail,
217
+ * and failing loudly beats deleting something else.
218
+ */
219
+ id: String(slug ?? field['id'] ?? ''),
220
+ pipeUuid: pipeUuid ?? undefined,
221
+ }, 'success');
222
+ };
223
+ /**
224
+ * `ArchiveFieldInput` is `{ uuid: String! }` — the field's **uuid**, not its id.
225
+ * Sent as an id, the mutation is rejected; the required-field check now catches
226
+ * that at plan time, which is how the real shape came to light.
227
+ */
228
+ export const archiveField = (client, field, archived) => buildGraphql(client, archived ? 'archiveField' : 'unarchiveField', { uuid: field['uuid'] ? String(field['uuid']) : undefined }, 'clientMutationId');
229
+ // ── labels ───────────────────────────────────────────────────────────────────
230
+ /**
231
+ * `CreateLabelInput` has **both** `pipe_id` and `table_id`, and a database's
232
+ * labels go in the second.
233
+ *
234
+ * Worth spelling out because the obvious probe fails in a misleading way: sending
235
+ * a database id as `pipe_id` is refused with `Pipe not found with id: <the
236
+ * database id>`, which reads like "databases have no labels" and is not. With
237
+ * `table_id` it works, and so do `updateLabel` and `deleteLabel`, both of which
238
+ * take only the label's own id regardless of which kind of repo owns it. Measured
239
+ * as full CRUD on a throwaway database.
240
+ */
241
+ export const createLabel = (client, repoId, label, kind = 'pipe') => buildGraphql(client, 'createLabel', {
242
+ ...(kind === 'table' ? { table_id: String(repoId) } : { pipe_id: String(repoId) }),
243
+ name: label['name'],
244
+ color: label['color'],
245
+ uuid: label['uuid'],
246
+ }, 'label { id name }');
247
+ export const updateLabel = (client, label) => buildGraphql(client, 'updateLabel', { id: String(label['id']), name: label['name'], color: label['color'] }, 'label { id }');
248
+ export const deleteLabel = (client, id) => buildGraphql(client, 'deleteLabel', { id: String(id) }, 'success');
249
+ // ── webhooks ─────────────────────────────────────────────────────────────────
250
+ export const createWebhook = (client, pipeId, hook) => buildGraphql(client, 'createWebhook', {
251
+ pipe_id: String(pipeId),
252
+ name: hook['name'],
253
+ url: hook['url'],
254
+ email: hook['email'],
255
+ actions: hook['actions'],
256
+ headers: hook['headers'],
257
+ filters: hook['filters'],
258
+ }, 'webhook { id name url }');
259
+ export const updateWebhook = (client, hook) => buildGraphql(client, 'updateWebhook', { id: String(hook['id']), name: hook['name'], url: hook['url'], email: hook['email'], actions: hook['actions'], headers: hook['headers'] }, 'webhook { id }');
260
+ export const deleteWebhook = (client, id) => buildGraphql(client, 'deleteWebhook', { id: String(id) }, 'success');
261
+ // ── field conditions ─────────────────────────────────────────────────────────
262
+ /**
263
+ * Recipe step 85 transforms the read shape into the create input: strip
264
+ * `label`, `phase` → `phaseId`, `phaseField` → `phaseFieldId`, keep
265
+ * `whenEvaluator` and `actionId`, keep the condition with its expressions.
266
+ * Step 109 does the same for update but emits `phase_id`.
267
+ */
268
+ export const fieldConditionInput = (fc, style) => {
269
+ const cond = fc['condition'];
270
+ const base = {
271
+ name: fc['name'],
272
+ actions: (fc['actions'] ?? []).map((a) => ({
273
+ actionId: a['action_id'],
274
+ phaseFieldId: String(a['field_id'] ?? ''),
275
+ whenEvaluator: a['when_evaluator'],
276
+ })),
277
+ /**
278
+ * Always sent, even with nothing in it.
279
+ *
280
+ * An unconditional hide — one action, no expressions — is a real and useful
281
+ * shape: it is how a system field that only an automation writes is kept off
282
+ * a human form, and real pipes carry exactly that, stored as
283
+ * `expressions_structure: []`. But the mutation will not accept it unless the
284
+ * `condition` key is present:
285
+ *
286
+ * condition omitted -> "Something went wrong"
287
+ * condition: null -> "Something went wrong"
288
+ * condition: {expressions: []} -> works
289
+ *
290
+ * `condition` is not NON_NULL on the input type, so nothing in the schema
291
+ * says this, and the endpoint's answer names neither the field nor the
292
+ * reason. Measured by creating one condition per shape on a throwaway phase.
293
+ */
294
+ /**
295
+ * The same builder automations use.
296
+ *
297
+ * This path had its own copy of the expression mapping and passed
298
+ * `structure_id` straight through, so an authored expression that did not
299
+ * carry one was rejected with "Invalid input: Structure can't be blank" —
300
+ * `expressions_structure` names structure ids, and there was nothing to name.
301
+ * One builder means the derivation cannot be fixed in one place and missed in
302
+ * the other, which is exactly what happened.
303
+ */
304
+ condition: conditionInput(cond ?? {}),
305
+ };
306
+ if (style === 'create') {
307
+ base['phaseId'] = String(fc['phase_id'] ?? '');
308
+ }
309
+ else {
310
+ base['id'] = String(fc['id'] ?? '');
311
+ base['phase_id'] = String(fc['phase_id'] ?? '');
312
+ }
313
+ return base;
314
+ };
315
+ export const createFieldCondition = (client, fc) => buildGraphql(client, 'createFieldCondition', fieldConditionInput(fc, 'create'), 'fieldCondition { id }');
316
+ export const updateFieldCondition = (client, fc) => buildGraphql(client, 'updateFieldCondition', fieldConditionInput(fc, 'update'), 'fieldCondition { id }');
317
+ export const deleteFieldCondition = (client, id) => buildGraphql(client, 'deleteFieldCondition', { id: String(id) }, 'success');
318
+ export const setFieldConditionOrder = (client, phaseId, ids) => buildGraphql(client, 'setFieldConditionOrder', { phaseId: String(phaseId), fieldConditionIds: ids.map(String) }, 'clientMutationId');
319
+ // ── pipe relations ───────────────────────────────────────────────────────────
320
+ /**
321
+ * Every boolean on the relation inputs is NON_NULL, and `autoFillFieldEnabled`
322
+ * has no column in the snapshot payload at all — so the flags are defaulted
323
+ * here rather than passed through as the null the payload would hand over.
324
+ */
325
+ /**
326
+ * The mutation and the payload spell the same three flags differently.
327
+ *
328
+ * `createPipeRelation` takes `canCreateNewItems`, `canConnectExistingItems` and
329
+ * `canConnectMultipleItems`; the row the payload hands back calls them
330
+ * `can_create_connected_cards`, `can_search_connected_cards` and
331
+ * `can_connect_multiple_cards` — items in one, cards in the other. So a relation
332
+ * read from a pull carries none of the keys this builder was looking for, every
333
+ * one of them fell through to its `?? default`, and updating a relation's *name*
334
+ * silently switched off "can create new items" on it. Which is what happened to
335
+ * the FDE → CS handoff.
336
+ *
337
+ * Both spellings are read, mutation-side first, so a hand-authored row and a
338
+ * pulled row behave identically. A missing value still falls back, but now only
339
+ * when the value is genuinely absent.
340
+ */
341
+ const relationFlags = (r) => {
342
+ const flag = (...keys) => {
343
+ for (const k of keys)
344
+ if (r[k] !== undefined && r[k] !== null)
345
+ return Boolean(r[k]);
346
+ return undefined;
347
+ };
348
+ return {
349
+ canCreateNewItems: flag('can_create_new_items', 'can_create_connected_cards') ?? false,
350
+ canConnectExistingItems: flag('can_connect_existing_items', 'can_search_connected_cards') ?? true,
351
+ canConnectMultipleItems: flag('can_connect_multiple_items', 'can_connect_multiple_cards') ?? false,
352
+ childMustExistToMoveParent: flag('child_must_exist_to_move_parent') ?? false,
353
+ childMustExistToFinishParent: flag('child_must_exist_to_finish_parent') ?? false,
354
+ allChildrenMustBeDoneToMoveParent: flag('all_children_must_be_done_to_move_parent') ?? false,
355
+ allChildrenMustBeDoneToFinishParent: flag('all_children_must_be_done_to_finish_parent') ?? false,
356
+ autoFillFieldEnabled: flag('auto_fill_field_enabled') ?? false,
357
+ };
358
+ };
359
+ export const createPipeRelation = (client, r) => buildGraphql(client, 'createPipeRelation', { parentId: String(r['parent_id']), childId: String(r['child_id']), name: r['name'], ...relationFlags(r) }, 'pipeRelation { id }');
360
+ export const updatePipeRelation = (client, r) => buildGraphql(client, 'updatePipeRelation', { id: String(r['id']), name: r['name'], ...relationFlags(r) }, 'pipeRelation { id }');
361
+ export const deletePipeRelation = (client, id) => buildGraphql(client, 'deletePipeRelation', { id: String(id) }, 'success');
362
+ // ── repo root ────────────────────────────────────────────────────────────────
363
+ /**
364
+ * `updatePipe.title_field_id` takes the field's **uuid** or its slug — never the
365
+ * numeric id, which is exactly what the payload column holds. Probed against the
366
+ * live endpoint: the numeric id is rejected with "Field not found with id", while
367
+ * the uuid and `request_title` are both accepted. So the numeric id is
368
+ * translated, and the argument is dropped rather than guessed when it cannot be.
369
+ */
370
+ export const updatePipe = (client, repo, fieldUuidById = {}) => buildGraphql(client, 'updatePipe', {
371
+ id: String(repo['id']),
372
+ name: repo['name'],
373
+ noun: repo['noun'],
374
+ icon: repo['icon'],
375
+ color: repo['color'],
376
+ public: repo['public'],
377
+ title_field_id: repo['title_field_id'] ? fieldUuidById[String(repo['title_field_id'])] : undefined,
378
+ expiration_time_by_unit: repo['expiration_time'],
379
+ only_admin_can_remove_cards: repo['only_admin_can_remove_cards'],
380
+ only_assignees_can_edit_cards: repo['only_assignees_can_edit_cards'],
381
+ /**
382
+ * `anyone_can_create_card`, `create_card_label` and `description` used to
383
+ * be sent here. None of the three is in `UpdatePipeInput`, so all three were
384
+ * dropped by the introspection filter and the mutation reported success
385
+ * without applying them. They are refused at plan time now, by
386
+ * `repo.update.settableColumns`, which is the honest answer.
387
+ */
388
+ }, 'pipe { id name }');
389
+ /**
390
+ * The database row. A different mutation from `updatePipe`, with a different
391
+ * settable set — so a database's root edit must not be routed through the pipe
392
+ * builder, where every table-only column would be silently dropped.
393
+ *
394
+ * Two arguments disagree about identifiers, and both were measured
395
+ * (VALIDATION.md §3g):
396
+ *
397
+ * - **`title_field_id` takes the field's uuid or its slug**, never the numeric
398
+ * internal id, which is refused with `Field not found with id:`. The slug is
399
+ * used here because a database's fields are addressed by slug everywhere else.
400
+ * - **`summary_attributes` takes the numeric internal id** — or a repo attribute
401
+ * slug such as `created_at` — and refuses a *field* slug with `Something went
402
+ * wrong`. It is the one argument on this input that wants the internal form,
403
+ * which is why it is spelled out rather than sharing a helper.
404
+ *
405
+ * `labels` and `members` are absent from `UpdateTableInput` — they can only be
406
+ * set when the database is created — so neither is sent, and the registry says so.
407
+ */
408
+ export const updateTable = (client, repo, fieldSlugById = {}) => buildGraphql(client, 'updateTable', {
409
+ id: String(repo['id']),
410
+ name: repo['name'],
411
+ noun: repo['noun'],
412
+ icon: repo['icon'],
413
+ color: repo['color'],
414
+ public: repo['public'],
415
+ description: repo['description'],
416
+ authorization: repo['authorization'],
417
+ create_record_button_label: repo['create_card_label'],
418
+ title_field_id: repo['title_field_id'] ? fieldSlugById[String(repo['title_field_id'])] : undefined,
419
+ }, 'table { id internal_id name }');
420
+ /**
421
+ * `UpdateRepoPreferencesInput` is `{ repoUuid: ID!, hiddenStartFormAttributes,
422
+ * hiddenTopButtons }` and nothing else — keyed on the repo **uuid**, and covering
423
+ * only those two arrays. Everything an earlier version sent here (start form
424
+ * title, custom sorting, the AI toggles) simply has no input.
425
+ *
426
+ * The three preferences that *are* settable elsewhere go through
427
+ * `updatePipe.preferences`, which is `RepoPreferenceInput`: findable,
428
+ * inboxEmailEnabled, mainTabViews.
429
+ */
430
+ export const updateRepoPreferences = (client, repoUuid, prefs) => buildGraphql(client, 'updateRepoPreferences', {
431
+ repoUuid: repoUuid ?? undefined,
432
+ hiddenStartFormAttributes: prefs['hidden_start_form_attributes'],
433
+ hiddenTopButtons: prefs['hidden_top_buttons'],
434
+ }, 'clientMutationId');
435
+ /** The preference subset `updatePipe` accepts, as `RepoPreferenceInput`. */
436
+ export const updatePipePreferences = (client, repoId, prefs) => buildGraphql(client, 'updatePipe', {
437
+ id: String(repoId),
438
+ preferences: {
439
+ findable: prefs['findable'],
440
+ inboxEmailEnabled: prefs['inbox_email_enabled'],
441
+ mainTabViews: prefs['main_tab_views'],
442
+ },
443
+ }, 'pipe { id }');
444
+ /** The public form, as `updatePipe.publicFormSettings`. */
445
+ export const updatePublicForm = (client, repoId, form) => buildGraphql(client, 'updatePipe', {
446
+ id: String(repoId),
447
+ publicFormSettings: {
448
+ title: form['title'],
449
+ description: form['description'],
450
+ submitButtonText: form['submit_button_text'],
451
+ afterSubmitMessage: form['after_submit_message'],
452
+ brandColor: form['brand_color'],
453
+ backgroundColor: form['background_color'],
454
+ displayPipefyLogo: form['display_pipefy_logo'],
455
+ showSubmitAnotherResponseButton: form['show_submit_another_response_button'],
456
+ submitterEmailCollectionEnabled: form['submitter_email_collection_enabled'],
457
+ reuseLastSubmissionResponse: form['reuse_last_submission_response'],
458
+ },
459
+ }, 'pipe { id }');
460
+ // ── automations (internal endpoint) ──────────────────────────────────────────
461
+ /**
462
+ * Variables for the internal `createAutomation`, built from a workspace
463
+ * automation node. The recipes send exactly this variable set (step 121).
464
+ */
465
+ /**
466
+ * The payload's snake_case params to the names the *input* types accept.
467
+ *
468
+ * Introspected from `AutomationActionParamsInput` and `AutomationEventParamsInput`,
469
+ * because the casing is genuinely inconsistent — `card_id`, `to_phase_id`,
470
+ * `field_map` and `fields_map_order` are snake_case while `taskParams`,
471
+ * `aiParams`, `slaParams`, `httpMethod` and the `authentication*` keys are camel.
472
+ * No generic camelizer produces this, which is why it is a table.
473
+ *
474
+ * Getting it wrong is not cosmetic: sending `task_params` had the recreate half
475
+ * of an automation update rejected *after* the delete half had succeeded, which
476
+ * destroyed a live automation. Every `send_a_task` automation in a pipe goes
477
+ * through this path.
478
+ */
479
+ const ACTION_PARAM_INPUT_NAMES = {
480
+ task_params: 'taskParams',
481
+ ai_params: 'aiParams',
482
+ ai_behavior_params: 'aiBehaviorParams',
483
+ sla_params: 'slaParams',
484
+ http_method: 'httpMethod',
485
+ authentication_key: 'authenticationKey',
486
+ authentication_value: 'authenticationValue',
487
+ authentication_add_to: 'authenticationAddTo',
488
+ authentication_type: 'authenticationType',
489
+ oauth2_uuid: 'oauth2Uuid',
490
+ };
491
+ const EVENT_PARAM_INPUT_NAMES = {
492
+ from_phase_id: 'fromPhaseId',
493
+ in_phase_id: 'inPhaseId',
494
+ kind_of_sla: 'kindOfSla',
495
+ trigger_automation_id: 'triggerAutomationId',
496
+ trigger_field_ids: 'triggerFieldIds',
497
+ };
498
+ /** Nested inputs have their own names again: AutomationAiParamsInput is camel. */
499
+ const NESTED_PARAM_INPUT_NAMES = {
500
+ field_ids: 'fieldIds',
501
+ skills_ids: 'skillsIds',
502
+ mcp_server_config: 'mcpServerConfig',
503
+ start_hour: 'startHour',
504
+ end_hour: 'endHour',
505
+ // AiBehaviorParamsInput / AiBehaviorActionAttributesInput / AiBehaviorMetadataInput —
506
+ // same reasoning: the wire shape is camel two and three levels down from
507
+ // action_params too, confirmed against captured createAiAgent/updateAiAgent
508
+ // traffic (src/pipefy/agents.ts has the read side of the same shape).
509
+ data_source_ids: 'dataSourceIds',
510
+ referenced_field_ids: 'referencedFieldIds',
511
+ provider_id: 'providerId',
512
+ system_provider_id: 'systemProviderId',
513
+ capabilities_attributes: 'capabilitiesAttributes',
514
+ capability_type: 'capabilityType',
515
+ actions_attributes: 'actionsAttributes',
516
+ action_type: 'actionType',
517
+ reference_id: 'referenceId',
518
+ destination_phase_id: 'destinationPhaseId',
519
+ pipe_id: 'pipeId',
520
+ table_id: 'tableId',
521
+ email_template_id: 'emailTemplateId',
522
+ allow_template_modifications: 'allowTemplateModifications',
523
+ mcp_server_id: 'mcpServerId',
524
+ tool_name: 'toolName',
525
+ tool_inputs: 'toolInputs',
526
+ fields_attributes: 'fieldsAttributes',
527
+ field_id: 'fieldId',
528
+ input_mode: 'inputMode',
529
+ };
530
+ /** Keys the read path adds that no input accepts. */
531
+ const NOT_INPUT_KEYS = new Set(['has_authentication_value', 'hasAuthenticationValue', 'phase']);
532
+ const toInputNames = (value, top) => {
533
+ if (Array.isArray(value))
534
+ return value.map((v) => toInputNames(v, NESTED_PARAM_INPUT_NAMES));
535
+ if (value && typeof value === 'object') {
536
+ const out = {};
537
+ for (const [k, v] of Object.entries(value)) {
538
+ if (NOT_INPUT_KEYS.has(k))
539
+ continue;
540
+ if (v === undefined)
541
+ continue;
542
+ const name = top[k] ?? NESTED_PARAM_INPUT_NAMES[k] ?? k;
543
+ out[name] = toInputNames(v, NESTED_PARAM_INPUT_NAMES);
544
+ }
545
+ return out;
546
+ }
547
+ return value;
548
+ };
549
+ /**
550
+ * A condition, in the shape the API actually binds.
551
+ *
552
+ * `expressions_structure` is a nested list of **structure ids**, not indices: it
553
+ * is how the API links each expression into the and/or tree. An expression with
554
+ * no `structure_id` therefore has nothing for the structure to point at, and the
555
+ * whole condition is dropped — mutation successful, `expressions: []` stored, no
556
+ * error anywhere. Measured on create and on update, with four different
557
+ * operation and structure shapes; only the variant carrying `structure_id`
558
+ * survived.
559
+ *
560
+ * Pulled automations already carry server-assigned structure ids, which is why
561
+ * this never showed up on a round trip and only bit hand-authored conditions.
562
+ * So the id is derived from position when absent and left alone when present,
563
+ * and the structure's leaves are stringified because that is how the API stores
564
+ * and compares them.
565
+ */
566
+ const structureIds = (v) => Array.isArray(v) ? v.map(structureIds) : v === null || v === undefined ? v : String(v);
567
+ export const conditionInput = (cond) => {
568
+ const rows = cond['expressions'] ?? [];
569
+ const expressions = rows.map((e, i) => ({
570
+ field_address: String(e['field_address'] ?? e['field_id'] ?? ''),
571
+ operation: e['operation'],
572
+ value: e['value'],
573
+ structure_id: e['structure_id'] ?? String(i),
574
+ }));
575
+ const declared = cond['expressions_structure'];
576
+ return {
577
+ expressions,
578
+ /**
579
+ * With no structure declared, a single and-group over every expression is the
580
+ * only reading that keeps them all bound. An empty structure drops them.
581
+ */
582
+ expressions_structure: declared
583
+ ? structureIds(declared)
584
+ : expressions.length
585
+ ? [expressions.map((e) => String(e.structure_id))]
586
+ : [],
587
+ };
588
+ };
589
+ /**
590
+ * Automations, on the **public** endpoint.
591
+ *
592
+ * `createAutomation`, `updateAutomation` and `deleteAutomation` are all on the
593
+ * public API and documented. This tool used the internal endpoint for all three,
594
+ * inherited from the Change Migrator recipes and never re-checked — which meant
595
+ * automations were the one entity written through an unversioned endpoint with no
596
+ * introspection and no compatibility contract, for no reason.
597
+ *
598
+ * Probed to confirm before switching: a create and a delete both succeeded
599
+ * against the public endpoint.
600
+ */
601
+ export const createAutomation = (client, auto) => buildGraphql(client, 'createAutomation', automationVariables(auto), 'automation { id name event_id action_id }');
602
+ export const updateAutomation = (client, auto) => buildGraphql(client, 'updateAutomation', { id: String(auto['id'] ?? ''), ...automationVariables(auto) }, 'automation { id name }');
603
+ export const deleteAutomation = (client, id) => buildGraphql(client, 'deleteAutomation', { id: String(id) }, 'success');
604
+ export const automationVariables = (auto) => {
605
+ const actionParams = {
606
+ ...(toInputNames(auto['action_params'] ?? {}, ACTION_PARAM_INPUT_NAMES) ?? {}),
607
+ };
608
+ const fieldMaps = auto['field_maps'] ?? [];
609
+ if (Array.isArray(fieldMaps) && fieldMaps.length) {
610
+ actionParams['field_map'] = fieldMaps.map((m) => ({
611
+ fieldId: String(m['field_id'] ?? ''),
612
+ value: m['value'] ?? '',
613
+ inputMode: m['input_mode'] ?? undefined,
614
+ }));
615
+ /**
616
+ * `fields_map_order` is not derived from `field_map` by the API — it is a
617
+ * separate argument, and both the automation editor and the run engine read
618
+ * it (not `field_map`) to know which fields the automation writes. Omitting
619
+ * it here does not error and does not no-op quietly either: the automation
620
+ * is created, `field_map` holds exactly what was authored, and the pipe
621
+ * still writes nothing and shows no fields configured, because nothing named
622
+ * them in filling order. Every observed working automation stores the same
623
+ * ids here as `field_map`, in the same order, so that is the default unless
624
+ * the row states its own.
625
+ */
626
+ actionParams['fields_map_order'] = Array.isArray(auto['fields_map_order']) && auto['fields_map_order'].length
627
+ ? auto['fields_map_order']
628
+ : fieldMaps.map((m) => String(m['field_id'] ?? ''));
629
+ }
630
+ const cond = auto['condition'];
631
+ const condition = cond ? conditionInput(cond) : undefined;
632
+ return dropUndefined({
633
+ name: auto['name'],
634
+ /**
635
+ * `active` was absent from this builder, so disabling an automation in the
636
+ * workspace silently did nothing: the mutation succeeded and the automation
637
+ * stayed on. Measured by toggling it and reading the row back.
638
+ */
639
+ active: typeof auto['active'] === 'boolean' ? auto['active'] : undefined,
640
+ action_id: auto['action_id'],
641
+ event_id: auto['event_id'],
642
+ action_repo_id: auto['action_repo_id'] ? String(auto['action_repo_id']) : undefined,
643
+ event_repo_id: auto['event_repo_id'] ? String(auto['event_repo_id']) : undefined,
644
+ event_params: toInputNames(auto['event_params'] ?? {}, EVENT_PARAM_INPUT_NAMES),
645
+ action_params: Object.keys(actionParams).length ? actionParams : undefined,
646
+ condition,
647
+ scheduler_frequency: auto['scheduler_frequency'] ?? undefined,
648
+ searchFor: auto['search_for'] && Object.keys(auto['search_for']).length ? auto['search_for'] : undefined,
649
+ schedulerCron: auto['scheduler_cron'] ?? undefined,
650
+ responseSchema: auto['response_schema'] ?? undefined,
651
+ });
652
+ };
653
+ // ── AI agents ────────────────────────────────────────────────────────────────
654
+ /**
655
+ * `createAiAgent` / `updateAiAgent` — confirmed against real Pipefy web UI
656
+ * traffic (captured, not guessed) and reproduced directly against the API.
657
+ *
658
+ * Both take the **entire** agent in one call: `AiAgentInput` carries the
659
+ * agent's own fields plus its complete `behaviors` list, existing ones
660
+ * unchanged (by `id`) alongside whatever is new or edited — there is no
661
+ * per-behavior mutation. Omitting an existing behavior deletes it, which is
662
+ * why every caller here sends the workspace's current, complete list for
663
+ * that agent, never a partial one.
664
+ *
665
+ * The behavior shape nested inside is *not* `UpdateAutomationInput` — it is
666
+ * its own input type, confirmed camelCase at the top (`eventId`, `actionId`,
667
+ * `eventParams`, `active`) where the automation mutations are snake_case.
668
+ * Everything below that first level matches what `pipefy/agents.ts` already
669
+ * reads back, aliased the same way in reverse.
670
+ */
671
+ /** A fresh v4 UUID for an `actionsAttributes[]` entry the author added with none. */
672
+ const referenceIdFor = (a) => String(a['reference_id'] ?? a['referenceId'] ?? randomUUID());
673
+ /** One behavior, in the shape `AiAgentInput.behaviors[]` actually takes. */
674
+ export const behaviorInput = (b) => {
675
+ const params = b['action_params']?.['ai_behavior_params'];
676
+ const actionsAttributes = (params?.['actions_attributes'] ?? []).map((a) => ({
677
+ ...a,
678
+ reference_id: referenceIdFor(a),
679
+ }));
680
+ const aiBehaviorParams = toInputNames({ ...(params ?? {}), actions_attributes: actionsAttributes }, NESTED_PARAM_INPUT_NAMES);
681
+ const cond = b['condition'];
682
+ return dropUndefined({
683
+ id: b['id'] ? String(b['id']) : undefined,
684
+ name: b['name'],
685
+ active: typeof b['active'] === 'boolean' ? b['active'] : true,
686
+ actionId: b['action_id'] ?? 'ai_behavior',
687
+ eventId: b['event_id'],
688
+ eventParams: toInputNames(b['event_params'] ?? {}, EVENT_PARAM_INPUT_NAMES),
689
+ condition: cond ? conditionInput(cond) : null,
690
+ actionParams: { aiBehaviorParams },
691
+ });
692
+ };
693
+ /** The agent itself plus its complete behaviors list, as `AiAgentInput`. */
694
+ export const agentInput = (agent, behaviors) => dropUndefined({
695
+ repoUuid: agent['repoUuid'] ? String(agent['repoUuid']) : undefined,
696
+ name: agent['name'],
697
+ instruction: agent['instruction'] ?? '',
698
+ dataSourceIds: agent['dataSourceIds'] ?? [],
699
+ behaviors: behaviors.map((b) => behaviorInput(b)),
700
+ });
701
+ export const createAiAgent = (client, agent, behaviors) => buildGraphql(client, 'createAiAgent', { agent: agentInput(agent, behaviors) }, 'agent { uuid name }');
702
+ export const updateAiAgent = (client, uuid, agent, behaviors) => buildGraphql(client, 'updateAiAgent', { uuid, agent: agentInput(agent, behaviors) }, 'agent { uuid name behaviors { id } }');
703
+ export const deleteAiAgent = (client, uuid) => buildGraphql(client, 'deleteAiAgent', { uuid }, 'success');
704
+ export const warnUnverified = (what) => warn(`${what}: shape could not be verified against this schema — the step is marked unverified and will report its own failure`);