@pipefy/pipefy-process-coder 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) 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 +476 -456
  20. package/LICENSE +202 -0
  21. package/NOTICE +11 -0
  22. package/README.md +830 -808
  23. package/bin/pipe.js +25 -4
  24. package/dist/apply/adopt.js +128 -0
  25. package/dist/apply/agentops.js +42 -0
  26. package/dist/apply/compile.js +722 -0
  27. package/dist/apply/execute.js +277 -0
  28. package/dist/apply/flowops.js +293 -0
  29. package/dist/apply/idmap.js +233 -0
  30. package/dist/apply/mutations.js +704 -0
  31. package/dist/apply/registry.js +348 -0
  32. package/dist/apply/types.js +18 -0
  33. package/dist/cli/args.js +79 -0
  34. package/dist/cli.js +196 -0
  35. package/dist/codec/flow.js +164 -0
  36. package/dist/codec/pack.js +88 -0
  37. package/dist/codec/roundtrip.js +86 -0
  38. package/dist/codec/unpack.js +181 -0
  39. package/dist/commands/agents.js +145 -0
  40. package/dist/commands/apply.js +982 -0
  41. package/dist/commands/context.js +79 -0
  42. package/dist/commands/create.js +63 -0
  43. package/dist/commands/diff.js +294 -0
  44. package/dist/commands/flows.js +348 -0
  45. package/dist/commands/misc.js +562 -0
  46. package/dist/commands/plan.js +301 -0
  47. package/dist/commands/pull.js +549 -0
  48. package/dist/commands/runs.js +74 -0
  49. package/dist/commands/skills.js +131 -0
  50. package/dist/commands/verify.js +205 -0
  51. package/dist/config.js +125 -0
  52. package/dist/diff/agents.js +80 -0
  53. package/dist/diff/diff.js +983 -0
  54. package/dist/diff/flow.js +258 -0
  55. package/{src/diff/html.ts → dist/diff/html.js} +274 -322
  56. package/dist/diff/render.js +90 -0
  57. package/dist/model/payload.js +84 -0
  58. package/dist/model/tree.js +7 -0
  59. package/dist/model/volatile.js +54 -0
  60. package/dist/pipefy/agents.js +131 -0
  61. package/dist/pipefy/automations.js +155 -0
  62. package/dist/pipefy/capability.js +84 -0
  63. package/dist/pipefy/client.js +204 -0
  64. package/dist/pipefy/discovery.js +148 -0
  65. package/dist/pipefy/internal.js +406 -0
  66. package/dist/pipefy/ipaas.js +269 -0
  67. package/dist/pipefy/reconstruct.js +676 -0
  68. package/dist/pipefy/reference.js +172 -0
  69. package/dist/pipefy/snapshot.js +185 -0
  70. package/dist/pipefy/toolkit.js +174 -0
  71. package/{src → dist}/pipefy/toolkit_bearer.py +60 -14
  72. package/dist/report/integrations.js +165 -0
  73. package/dist/report/run.js +356 -0
  74. package/dist/util/fsx.js +50 -0
  75. package/dist/util/git.js +25 -0
  76. package/dist/util/json.js +56 -0
  77. package/dist/util/log.js +69 -0
  78. package/dist/util/pool.js +42 -0
  79. package/dist/util/slug.js +23 -0
  80. package/dist/util/tui.js +287 -0
  81. package/dist/validate/index.js +86 -0
  82. package/dist/validate/integrity.js +370 -0
  83. package/dist/validate/reference.js +120 -0
  84. package/dist/validate/schema.js +278 -0
  85. package/dist/version.js +18 -0
  86. package/dist/workspace/agents.js +245 -0
  87. package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
  88. package/dist/workspace/flows.js +149 -0
  89. package/dist/workspace/layout.js +146 -0
  90. package/dist/workspace/lock.js +83 -0
  91. package/dist/workspace/read.js +168 -0
  92. package/dist/workspace/reference.js +24 -0
  93. package/dist/workspace/stamp.js +234 -0
  94. package/dist/workspace/write.js +194 -0
  95. package/package.json +38 -35
  96. package/src/apply/adopt.ts +0 -150
  97. package/src/apply/agentops.ts +0 -71
  98. package/src/apply/compile.ts +0 -875
  99. package/src/apply/execute.ts +0 -336
  100. package/src/apply/flowops.ts +0 -399
  101. package/src/apply/idmap.ts +0 -241
  102. package/src/apply/mutations.ts +0 -955
  103. package/src/apply/registry.ts +0 -430
  104. package/src/apply/types.ts +0 -134
  105. package/src/cli/args.ts +0 -88
  106. package/src/cli.ts +0 -211
  107. package/src/codec/flow.ts +0 -199
  108. package/src/codec/pack.ts +0 -103
  109. package/src/codec/roundtrip.ts +0 -94
  110. package/src/codec/unpack.ts +0 -198
  111. package/src/commands/agents.ts +0 -184
  112. package/src/commands/apply.ts +0 -1144
  113. package/src/commands/context.ts +0 -119
  114. package/src/commands/create.ts +0 -79
  115. package/src/commands/diff.ts +0 -314
  116. package/src/commands/flows.ts +0 -414
  117. package/src/commands/misc.ts +0 -644
  118. package/src/commands/plan.ts +0 -331
  119. package/src/commands/pull.ts +0 -567
  120. package/src/commands/runs.ts +0 -83
  121. package/src/commands/skills.ts +0 -137
  122. package/src/commands/verify.ts +0 -253
  123. package/src/config.ts +0 -168
  124. package/src/diff/agents.ts +0 -122
  125. package/src/diff/diff.ts +0 -1130
  126. package/src/diff/flow.ts +0 -318
  127. package/src/diff/render.ts +0 -101
  128. package/src/model/payload.ts +0 -154
  129. package/src/model/tree.ts +0 -99
  130. package/src/model/volatile.ts +0 -55
  131. package/src/pipefy/agents.ts +0 -165
  132. package/src/pipefy/automations.ts +0 -219
  133. package/src/pipefy/capability.ts +0 -119
  134. package/src/pipefy/client.ts +0 -267
  135. package/src/pipefy/discovery.ts +0 -209
  136. package/src/pipefy/internal.ts +0 -380
  137. package/src/pipefy/ipaas.ts +0 -365
  138. package/src/pipefy/reconstruct.ts +0 -775
  139. package/src/pipefy/reference.ts +0 -251
  140. package/src/pipefy/snapshot.ts +0 -245
  141. package/src/pipefy/toolkit.ts +0 -200
  142. package/src/report/integrations.ts +0 -231
  143. package/src/report/run.ts +0 -475
  144. package/src/util/fsx.ts +0 -45
  145. package/src/util/git.ts +0 -32
  146. package/src/util/json.ts +0 -55
  147. package/src/util/log.ts +0 -76
  148. package/src/util/pool.ts +0 -48
  149. package/src/util/slug.ts +0 -26
  150. package/src/util/tui.ts +0 -335
  151. package/src/validate/index.ts +0 -123
  152. package/src/validate/integrity.ts +0 -387
  153. package/src/validate/reference.ts +0 -136
  154. package/src/validate/schema.ts +0 -328
  155. package/src/workspace/agents.ts +0 -290
  156. package/src/workspace/flows.ts +0 -191
  157. package/src/workspace/layout.ts +0 -165
  158. package/src/workspace/lock.ts +0 -148
  159. package/src/workspace/read.ts +0 -165
  160. package/src/workspace/reference.ts +0 -24
  161. package/src/workspace/stamp.ts +0 -301
  162. package/src/workspace/write.ts +0 -225
@@ -0,0 +1,90 @@
1
+ import { bold, dim, green, red, yellow, blue, magenta } from "../util/log.js";
2
+ /** Text rendering of a ChangeSet, for the CLI. */
3
+ const OP_MARK = {
4
+ create: '+',
5
+ delete: '-',
6
+ update: '~',
7
+ reorder: '>',
8
+ archive: 'a',
9
+ unarchive: 'u',
10
+ };
11
+ const colorFor = (op) => op === 'create' ? green : op === 'delete' ? red : op === 'reorder' ? magenta : yellow;
12
+ const short = (v, max = 90) => {
13
+ if (v === null || v === undefined)
14
+ return 'null';
15
+ if (typeof v === 'string')
16
+ return v.length > max ? JSON.stringify(v.slice(0, max) + '…') : JSON.stringify(v);
17
+ const s = JSON.stringify(v);
18
+ return s.length > max ? s.slice(0, max) + '…' : s;
19
+ };
20
+ const renderDelta = (d, indent) => `${indent}${dim(d.key)}: ${red(short(d.before))} ${dim('->')} ${green(short(d.after))}`;
21
+ export const renderChange = (c, opts = {}) => {
22
+ const paint = colorFor(c.op);
23
+ const lines = [];
24
+ const flags = c.flags.length ? ' ' + dim(`[${c.flags.join(' ')}]`) : '';
25
+ lines.push(` ${paint(OP_MARK[c.op])} ${bold(c.entity)} ${c.label}${flags}`);
26
+ const deltas = opts.verbose ? c.deltas : c.deltas.slice(0, 6);
27
+ for (const d of deltas)
28
+ lines.push(renderDelta(d, ' '));
29
+ if (c.deltas.length > deltas.length) {
30
+ lines.push(` ${dim(`…and ${c.deltas.length - deltas.length} more field${c.deltas.length - deltas.length === 1 ? '' : 's'}`)}`);
31
+ }
32
+ if (c.op === 'create' && c.after && !c.deltas.length) {
33
+ const keys = Object.keys(c.after).filter((k) => !k.startsWith('_')).slice(0, 8);
34
+ lines.push(` ${dim(keys.map((k) => `${k}=${short(c.after?.[k], 30)}`).join(' '))}`);
35
+ }
36
+ return lines;
37
+ };
38
+ export const renderChangeSet = (cs, opts = {}) => {
39
+ const lines = [];
40
+ const header = cs.mode === 'cross-repo'
41
+ ? `${cs.from.name} (${cs.from.repoId}) -> ${cs.to.name} (${cs.to.repoId})`
42
+ : `${cs.to.name} (${cs.to.repoId})`;
43
+ lines.push(bold(`${cs.mode} diff — ${header}`));
44
+ lines.push(dim(`baseline read from ${cs.from.source}; compared tree read from ${cs.to.source}`));
45
+ lines.push('');
46
+ if (!cs.changes.length) {
47
+ lines.push(green(' no changes'));
48
+ }
49
+ else {
50
+ const byEntity = new Map();
51
+ for (const c of cs.changes) {
52
+ const arr = byEntity.get(c.entity) ?? [];
53
+ arr.push(c);
54
+ byEntity.set(c.entity, arr);
55
+ }
56
+ for (const [entity, group] of byEntity) {
57
+ lines.push(blue(`${entity} (${group.length})`));
58
+ for (const c of group)
59
+ lines.push(...renderChange(c, opts));
60
+ lines.push('');
61
+ }
62
+ }
63
+ if (cs.skipped.length) {
64
+ lines.push(yellow('not diffed — no read path on one side:'));
65
+ for (const s of cs.skipped)
66
+ lines.push(` ${s.entity}: ${dim(s.reason)}`);
67
+ lines.push('');
68
+ }
69
+ const counts = cs.changes.reduce((acc, c) => {
70
+ acc[c.op] = (acc[c.op] ?? 0) + 1;
71
+ return acc;
72
+ }, {});
73
+ const summary = Object.entries(counts)
74
+ .map(([op, n]) => `${n} ${op}`)
75
+ .join(', ');
76
+ lines.push(bold(summary || 'nothing to do'));
77
+ return lines.join('\n');
78
+ };
79
+ /** Machine-readable form, for `--json` and for the HTML view. */
80
+ export const toJson = (cs) => ({
81
+ mode: cs.mode,
82
+ from: cs.from,
83
+ to: cs.to,
84
+ summary: cs.changes.reduce((acc, c) => {
85
+ acc[c.op] = (acc[c.op] ?? 0) + 1;
86
+ return acc;
87
+ }, {}),
88
+ skipped: cs.skipped,
89
+ changes: cs.changes,
90
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The snapshot payload: the `repos` row (snake_case DB columns) plus
3
+ * `relations` with twenty flat entity arrays. This shape is the canonical
4
+ * intermediate for BOTH read paths — snapshot download and GraphQL
5
+ * reconstruction — so everything downstream (codec, diff, plan) is written
6
+ * once. PLAN.md §1.3.
7
+ */
8
+ export const ENTITY_NAMES = [
9
+ 'phases',
10
+ 'phase_team_memberships',
11
+ 'phase_jumps',
12
+ 'fields',
13
+ 'field_conditions',
14
+ 'field_condition_actions',
15
+ 'automations',
16
+ 'conditions',
17
+ 'condition_expressions',
18
+ 'field_maps',
19
+ 'distribute_assignments',
20
+ 'response_schemas',
21
+ 'repo_preferences',
22
+ 'labels',
23
+ 'pipe_relations',
24
+ 'email_inboxes',
25
+ 'webhooks',
26
+ 'public_forms',
27
+ 'email_templates',
28
+ 'visibilities',
29
+ ];
30
+ export const emptyRelations = () => Object.fromEntries(ENTITY_NAMES.map((n) => [n, []]));
31
+ export const fullCoverage = (c) => Object.fromEntries(ENTITY_NAMES.map((n) => [n, c]));
32
+ export const ENTITIES = {
33
+ phases: { name: 'phases', identity: 'uuid', naturalKey: ['name'] },
34
+ fields: { name: 'fields', identity: 'uuid', naturalKey: ['phase_id', 'label'], foldedInto: 'phases' },
35
+ automations: { name: 'automations', identity: 'uuid', naturalKey: ['name'] },
36
+ labels: { name: 'labels', identity: 'uuid', naturalKey: ['name'] },
37
+ field_conditions: { name: 'field_conditions', identity: 'id', naturalKey: ['phase_id', 'name'] },
38
+ webhooks: { name: 'webhooks', identity: 'id', naturalKey: ['name', 'url'] },
39
+ pipe_relations: { name: 'pipe_relations', identity: 'id', naturalKey: ['parent_id', 'child_id', 'name'] },
40
+ conditions: { name: 'conditions', identity: 'owned', ownerKeys: ['source_type', 'source_id'] },
41
+ condition_expressions: { name: 'condition_expressions', identity: 'owned', ownerKeys: ['condition_id'] },
42
+ field_maps: { name: 'field_maps', identity: 'owned', ownerKeys: ['owner_type', 'owner_id'] },
43
+ field_condition_actions: {
44
+ name: 'field_condition_actions',
45
+ identity: 'owned',
46
+ ownerKeys: ['field_condition_id'],
47
+ foldedInto: 'field_conditions',
48
+ },
49
+ phase_jumps: { name: 'phase_jumps', identity: 'owned', ownerKeys: ['source_phase_id'], internal: true },
50
+ phase_team_memberships: {
51
+ name: 'phase_team_memberships',
52
+ identity: 'owned',
53
+ ownerKeys: ['phase_id'],
54
+ foldedInto: 'phases',
55
+ },
56
+ distribute_assignments: {
57
+ name: 'distribute_assignments',
58
+ identity: 'owned',
59
+ ownerKeys: ['automation_id'],
60
+ foldedInto: 'automations',
61
+ },
62
+ response_schemas: {
63
+ name: 'response_schemas',
64
+ identity: 'owned',
65
+ ownerKeys: ['automation_id'],
66
+ foldedInto: 'automations',
67
+ },
68
+ public_forms: { name: 'public_forms', identity: 'owned', ownerKeys: ['repo_id'] },
69
+ repo_preferences: { name: 'repo_preferences', identity: 'owned', ownerKeys: ['repo_id'] },
70
+ visibilities: { name: 'visibilities', identity: 'owned', ownerKeys: ['resource_type', 'resource_id'], readonly: true },
71
+ email_templates: { name: 'email_templates', identity: 'id', readonly: true },
72
+ email_inboxes: { name: 'email_inboxes', identity: 'id', readonly: true },
73
+ };
74
+ export const READONLY_ENTITIES = ENTITY_NAMES.filter((n) => ENTITIES[n].readonly);
75
+ export const rel = (p, n) => p.relations?.[n] ?? [];
76
+ export const numId = (r) => {
77
+ const v = r['id'];
78
+ if (typeof v === 'number')
79
+ return v;
80
+ if (typeof v === 'string' && /^\d+$/.test(v))
81
+ return Number(v);
82
+ return null;
83
+ };
84
+ export const strId = (r) => String(r['id'] ?? '');
@@ -0,0 +1,7 @@
1
+ export const repoId = (t) => Number(t.repo['id']);
2
+ export const repoName = (t) => String(t.repo['name'] ?? `repo ${repoId(t)}`);
3
+ /** The start form is a phase (id/index 0). PLAN.md §4 — deleting it is catastrophic. */
4
+ export const startFormPhase = (t) => t.phases.find((p) => Number(p['index']) === 0) ?? t.phases[0];
5
+ export const allFields = (t) => t.phases.flatMap((p) => p.fields);
6
+ export const fieldById = (t, id) => allFields(t).find((f) => String(f['id']) === String(id));
7
+ export const phaseById = (t, id) => t.phases.find((p) => String(p['id']) === String(id));
@@ -0,0 +1,54 @@
1
+ /**
2
+ * PLAN.md 9: "Volatile fields will make every diff pure noise." These change
3
+ * with no structural edit, so the codec strips them on unpack and the
4
+ * comparator ignores them. Without this, `pipe diff` is unusable on day one.
5
+ */
6
+ export const VOLATILE_KEYS = new Set([
7
+ 'created_at',
8
+ 'updated_at',
9
+ 'deleted_at',
10
+ 'created_by_id',
11
+ 'updated_by_id',
12
+ 'run_count',
13
+ 'cards_count',
14
+ 'users_count',
15
+ 'opened_cards_count',
16
+ 'last_updated_by_card',
17
+ 'cloning',
18
+ 'sequential_id',
19
+ 'clone_from_id',
20
+ 'cloned_from_id',
21
+ 'cloned_from_phase_uuid',
22
+ 'cloned_from_field_uuid',
23
+ 'migrated_from_repo_uuid',
24
+ ]);
25
+ /**
26
+ * Server-owned identity and ownership columns. Kept in the files (Claude needs
27
+ * to read them for a field reference to mean anything) but never diffed as
28
+ * content, because they are consequences of structure rather than structure.
29
+ */
30
+ export const SERVER_OWNED_KEYS = new Set([
31
+ 'id',
32
+ 'repo_id',
33
+ 'organization_id',
34
+ 'organization_uuid',
35
+ 'suid',
36
+ 'slug',
37
+ ]);
38
+ export const stripVolatile = (row) => {
39
+ const out = {};
40
+ for (const [k, v] of Object.entries(row)) {
41
+ if (VOLATILE_KEYS.has(k))
42
+ continue;
43
+ out[k] = v;
44
+ }
45
+ return out;
46
+ };
47
+ /** The volatile values we drop, kept aside so the UI can still show them. */
48
+ export const takeVolatile = (row) => {
49
+ const out = {};
50
+ for (const [k, v] of Object.entries(row))
51
+ if (VOLATILE_KEYS.has(k))
52
+ out[k] = v;
53
+ return out;
54
+ };
@@ -0,0 +1,131 @@
1
+ import { debug } from "../util/log.js";
2
+ /**
3
+ * The selection under `aiBehaviorParams`, aliased to the snake_case the files
4
+ * use. Shared with `internal.ts`'s `loadAutomation`, because `AiBehaviorParams`
5
+ * is the same object type on both endpoints — and the internal query once
6
+ * selected it bare, as though it were a scalar. GraphQL rejects that before
7
+ * resolving anything, so every automation in the pipe failed to load, not only
8
+ * the AI ones. One selection, two call sites, so the two cannot drift apart.
9
+ */
10
+ export const AI_BEHAVIOR_PARAMS_SELECTION = `{
11
+ data_source_ids: dataSourceIds
12
+ instruction
13
+ referenced_field_ids: referencedFieldIds
14
+ provider_id: providerId
15
+ system_provider_id: systemProviderId
16
+ actions_attributes: actionsAttributes {
17
+ id
18
+ name
19
+ action_type: actionType
20
+ reference_id: referenceId
21
+ metadata {
22
+ destination_phase_id: destinationPhaseId
23
+ pipe_id: pipeId
24
+ table_id: tableId
25
+ email_template_id: emailTemplateId
26
+ allow_template_modifications: allowTemplateModifications
27
+ mcp_server_id: mcpServerId
28
+ tool_name: toolName
29
+ title
30
+ emails
31
+ tool_inputs: toolInputs { name source value field_id: fieldId }
32
+ fields_attributes: fieldsAttributes { field_id: fieldId input_mode: inputMode value }
33
+ }
34
+ }
35
+ capabilities_attributes: capabilitiesAttributes { capability_type: capabilityType enabled }
36
+ }`;
37
+ const QUERY = `
38
+ query($repoUuid: ID!, $after: String) {
39
+ aiAgents(repoUuid: $repoUuid, first: 50, after: $after) {
40
+ edges {
41
+ node {
42
+ uuid
43
+ name
44
+ instruction
45
+ repoUuid
46
+ disabledAt
47
+ needReview
48
+ dataSourceIds
49
+ createdAt
50
+ updatedAt
51
+ behaviors {
52
+ id
53
+ uuid
54
+ name
55
+ active
56
+ event_id
57
+ action_id
58
+ event_params {
59
+ to_phase_id
60
+ from_phase_id: fromPhaseId
61
+ in_phase_id: inPhaseId
62
+ trigger_field_ids: triggerFieldIds
63
+ kind_of_sla: kindOfSla
64
+ trigger_automation_id: triggerAutomationId
65
+ }
66
+ condition {
67
+ id
68
+ expressions_structure
69
+ expressions { id field_address operation value structure_id }
70
+ }
71
+ action_params {
72
+ ai_behavior_params: aiBehaviorParams ${AI_BEHAVIOR_PARAMS_SELECTION}
73
+ }
74
+ }
75
+ }
76
+ }
77
+ pageInfo { hasNextPage endCursor }
78
+ }
79
+ }
80
+ `;
81
+ /**
82
+ * `event_params` always comes back with every key this query asked for, most
83
+ * of them null for any given event — `card_created` genuinely accepts none of
84
+ * them. An ordinary automation's `event_params` only ever has the keys its
85
+ * own event actually uses, and `pipe validate` checks a present key against
86
+ * the event's catalogue regardless of its value, so a null one reads as one
87
+ * more thing this event does not accept. Dropped to match.
88
+ */
89
+ const pruneNulls = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== null && v !== undefined));
90
+ /** A behavior as the query returns it, given the shape `tree.automations` (AutomationNode) needs. */
91
+ const normaliseBehavior = (b) => ({
92
+ ...b,
93
+ event_params: pruneNulls(b['event_params'] ?? {}),
94
+ condition: b['condition'] ?? null,
95
+ field_maps: [],
96
+ distribute_assignments: [],
97
+ response_schemas: [],
98
+ });
99
+ export const fetchAgents = async (client, repoUuid) => {
100
+ const queryFields = await client.typeFields('Query');
101
+ if (!queryFields?.has('aiAgents')) {
102
+ debug('no aiAgents query on this schema');
103
+ return { agents: [], status: 'unknown', note: 'agents: no aiAgents query on this endpoint — AI agents cannot be read or checked' };
104
+ }
105
+ const agents = [];
106
+ let after = null;
107
+ for (let page = 0; page < 50; page++) {
108
+ const data = await client.tryRaw(QUERY, { repoUuid, after }, 'ai agents');
109
+ const result = data?.aiAgents;
110
+ if (!result) {
111
+ return {
112
+ agents: [],
113
+ status: 'unknown',
114
+ note: 'agents: aiAgents exists but did not answer — AI agent configuration is unverified',
115
+ };
116
+ }
117
+ for (const { node } of result.edges) {
118
+ const { behaviors, ...rest } = node;
119
+ agents.push({
120
+ ...rest,
121
+ uuid: String(rest['uuid']),
122
+ name: String(rest['name'] ?? ''),
123
+ behaviors: (behaviors ?? []).map(normaliseBehavior),
124
+ });
125
+ }
126
+ if (!result.pageInfo.hasNextPage)
127
+ break;
128
+ after = result.pageInfo.endCursor;
129
+ }
130
+ return { agents, status: 'complete' };
131
+ };
@@ -0,0 +1,155 @@
1
+ import { debug } from "../util/log.js";
2
+ const EVENTS_QUERY = `query AutomationEvents {
3
+ automationEvents { id acceptedParameters actionsBlacklist }
4
+ }`;
5
+ const ACTIONS_QUERY = `query AutomationActions($repoId: ID!) {
6
+ automationActions(repoId: $repoId) { id acceptedParameters eventsBlacklist }
7
+ }`;
8
+ /**
9
+ * Read the catalogue for one pipe. Actions are per-repo (a pipe without AI
10
+ * enabled offers fewer), events are global.
11
+ */
12
+ export const readAutomationCatalogue = async (client, repoId) => {
13
+ const [ev, ac] = await Promise.all([
14
+ client.raw(EVENTS_QUERY),
15
+ client.raw(ACTIONS_QUERY, { repoId: String(repoId) }),
16
+ ]);
17
+ const events = new Map();
18
+ for (const e of ev?.automationEvents ?? []) {
19
+ events.set(e.id, {
20
+ id: e.id,
21
+ acceptedParameters: e.acceptedParameters ?? [],
22
+ actionsBlacklist: e.actionsBlacklist ?? [],
23
+ });
24
+ }
25
+ const actions = new Map();
26
+ for (const a of ac?.automationActions ?? []) {
27
+ actions.set(a.id, {
28
+ id: a.id,
29
+ acceptedParameters: a.acceptedParameters ?? [],
30
+ eventsBlacklist: a.eventsBlacklist ?? [],
31
+ });
32
+ }
33
+ debug(`automation catalogue: ${events.size} events, ${actions.size} actions for repo ${repoId}`);
34
+ return { events, actions };
35
+ };
36
+ /**
37
+ * Parameter names the payload spells differently from the API.
38
+ *
39
+ * The stored row and the mutation input disagree in both directions, so a check
40
+ * against `acceptedParameters` has to normalise first or it reports correct
41
+ * automations as broken.
42
+ */
43
+ const PARAM_ALIASES = {
44
+ // event params
45
+ inPhaseId: 'in_phase_id',
46
+ fromPhaseId: 'from_phase_id',
47
+ toPhaseId: 'to_phase_id',
48
+ triggerFieldIds: 'trigger_field_ids',
49
+ triggerAutomationId: 'trigger_automation_id',
50
+ kindOfSla: 'kind_of_sla',
51
+ // action params
52
+ fieldsMapOrder: 'fields_map_order',
53
+ cardId: 'card_id',
54
+ toPhaseIdAction: 'to_phase_id',
55
+ taskParams: 'task_params',
56
+ aiParams: 'ai_params',
57
+ aiBehaviorParams: 'ai_behavior_params',
58
+ slaParams: 'sla_params',
59
+ httpMethod: 'http_method',
60
+ emailTemplateId: 'email_template_id',
61
+ authenticationKey: 'authentication_key',
62
+ authenticationValue: 'authentication_value',
63
+ authenticationAddTo: 'authentication_add_to',
64
+ authenticationType: 'authentication_type',
65
+ oauth2Uuid: 'oauth2_uuid',
66
+ };
67
+ const canonical = (name) => PARAM_ALIASES[name] ?? name;
68
+ /**
69
+ * Params the API accepts on every action regardless of the catalogue.
70
+ *
71
+ * `field_map` carries the values an `update_card_field` or `create_card` writes
72
+ * and is not listed in `acceptedParameters` — the catalogue lists
73
+ * `fields_map_order` beside it and stops. Flagging `field_map` would refuse every
74
+ * automation that writes a field, which is most of them.
75
+ */
76
+ const ALWAYS_ALLOWED_ACTION_PARAMS = new Set(['field_map', 'schema']);
77
+ /**
78
+ * Check one automation against the catalogue, before anything is sent.
79
+ *
80
+ * Returns findings rather than throwing, so the compiler can refuse the step and
81
+ * name the parameter — which is the whole point. An unknown event or action is an
82
+ * error; a catalogue this pipe does not offer is silence, not a guess.
83
+ */
84
+ export const checkAutomationParams = (auto, catalogue) => {
85
+ const findings = [];
86
+ const eventId = String(auto['event_id'] ?? '');
87
+ const actionId = String(auto['action_id'] ?? '');
88
+ const event = catalogue.events.get(eventId);
89
+ const action = catalogue.actions.get(actionId);
90
+ if (eventId && !event) {
91
+ findings.push({
92
+ severity: 'error',
93
+ where: 'event',
94
+ message: `event_id ${JSON.stringify(eventId)} is not an event this endpoint offers`,
95
+ fix: `available: ${[...catalogue.events.keys()].join(', ')}`,
96
+ });
97
+ }
98
+ if (actionId && !action) {
99
+ findings.push({
100
+ severity: 'error',
101
+ where: 'action',
102
+ message: `action_id ${JSON.stringify(actionId)} is not an action this pipe offers`,
103
+ fix: `available: ${[...catalogue.actions.keys()].join(', ')}`,
104
+ });
105
+ }
106
+ if (event && action) {
107
+ if (event.actionsBlacklist.includes(actionId)) {
108
+ findings.push({
109
+ severity: 'error',
110
+ where: 'pairing',
111
+ message: `event ${eventId} cannot be paired with action ${actionId}`,
112
+ });
113
+ }
114
+ else if (action.eventsBlacklist.includes(eventId)) {
115
+ findings.push({
116
+ severity: 'error',
117
+ where: 'pairing',
118
+ message: `action ${actionId} cannot be triggered by event ${eventId}`,
119
+ });
120
+ }
121
+ }
122
+ if (event) {
123
+ const accepted = new Set(event.acceptedParameters.map(canonical));
124
+ for (const key of Object.keys((auto['event_params'] ?? {}))) {
125
+ const name = canonical(key);
126
+ if (accepted.has(name))
127
+ continue;
128
+ findings.push({
129
+ severity: 'error',
130
+ where: 'event',
131
+ message: `event ${eventId} does not accept event_params.${key}`,
132
+ fix: accepted.size
133
+ ? `it accepts: ${[...accepted].join(', ')}`
134
+ : `it accepts no parameters — remove event_params.${key}`,
135
+ });
136
+ }
137
+ }
138
+ if (action) {
139
+ const accepted = new Set(action.acceptedParameters.map(canonical));
140
+ for (const key of Object.keys((auto['action_params'] ?? {}))) {
141
+ const name = canonical(key);
142
+ if (accepted.has(name) || ALWAYS_ALLOWED_ACTION_PARAMS.has(name))
143
+ continue;
144
+ findings.push({
145
+ severity: 'error',
146
+ where: 'action',
147
+ message: `action ${actionId} does not accept action_params.${key}`,
148
+ fix: accepted.size
149
+ ? `it accepts: ${[...accepted].join(', ')}`
150
+ : `it accepts no parameters — remove action_params.${key}`,
151
+ });
152
+ }
153
+ }
154
+ return findings;
155
+ };
@@ -0,0 +1,84 @@
1
+ import { debug } from "../util/log.js";
2
+ /** Mutations the plan compiler emits, checked as a set so gaps are visible up front. */
3
+ export const REQUIRED_MUTATIONS = [
4
+ 'updatePipe',
5
+ 'createPhase',
6
+ 'updatePhase',
7
+ 'deletePhase',
8
+ 'createPhaseField',
9
+ 'updatePhaseField',
10
+ 'deletePhaseField',
11
+ 'createLabel',
12
+ 'updateLabel',
13
+ 'deleteLabel',
14
+ 'createWebhook',
15
+ 'updateWebhook',
16
+ 'deleteWebhook',
17
+ 'createFieldCondition',
18
+ 'updateFieldCondition',
19
+ 'deleteFieldCondition',
20
+ 'setFieldConditionOrder',
21
+ 'createAutomation',
22
+ 'updateAutomation',
23
+ 'deleteAutomation',
24
+ 'createPipeRelation',
25
+ 'updatePipeRelation',
26
+ 'deletePipeRelation',
27
+ 'phaseSettings',
28
+ 'updateRepoPreferences',
29
+ 'archiveField',
30
+ 'unarchiveField',
31
+ 'moveCardToPhase',
32
+ ];
33
+ export const detect = async (client, internal) => {
34
+ const mutationFields = await client.typeFields('Mutation');
35
+ const pipeFields = await client.typeFields('Pipe');
36
+ const has = (n) => Boolean(mutationFields?.has(n));
37
+ const mutations = {};
38
+ for (const m of REQUIRED_MUTATIONS)
39
+ mutations[m] = has(m);
40
+ const caps = {
41
+ snapshots: has('createRepoSnapshot') && Boolean(pipeFields?.has('snapshots') || pipeFields?.has('snapshot')),
42
+ /**
43
+ * Both spellings, because this build was looking for the wrong one.
44
+ *
45
+ * `restoreRepoSnapshot` does not exist on this endpoint and never did.
46
+ * **`restoreRepoToSnapshot`** does — "Restores a Pipe structure to its latest
47
+ * uploaded snapshot" — so every apply has printed "no one-command rollback"
48
+ * beside a snapshot that could in fact be restored, and `pipe rollback` has
49
+ * refused to try. That is the same failure as automations sitting on the
50
+ * internal endpoint: a name assumed once and never re-checked.
51
+ *
52
+ * The capability is still narrower than it sounds, and `restoreLatestOnly`
53
+ * below is what says so.
54
+ */
55
+ restoreRepoSnapshot: has('restoreRepoSnapshot') || has('restoreRepoToSnapshot'),
56
+ restoreMutation: has('restoreRepoToSnapshot')
57
+ ? 'restoreRepoToSnapshot'
58
+ : has('restoreRepoSnapshot')
59
+ ? 'restoreRepoSnapshot'
60
+ : null,
61
+ /**
62
+ * A restore only accepts the **latest** uploaded snapshot. Measured: an
63
+ * older versionId is refused with "Version_id must reference the latest
64
+ * uploaded snapshot of the Pipe".
65
+ *
66
+ * That is the whole story for rollback on this endpoint, and it is why the
67
+ * pre-apply snapshot is usually not restorable: `pipe apply` reads the repo
68
+ * back to verify, and that read takes a snapshot of its own, which becomes
69
+ * the latest. Recovery stays forward-first.
70
+ */
71
+ restoreLatestOnly: has('restoreRepoToSnapshot'),
72
+ createRepoDraft: has('createRepoDraft'),
73
+ importRepoSnapshot: has('importRepoSnapshot') || has('createRepoSnapshotFromPayload'),
74
+ mutations,
75
+ internalApi: internal?.status ?? 'unknown',
76
+ checkedAt: new Date().toISOString(),
77
+ };
78
+ debug(`capabilities: snapshots=${caps.snapshots} restore=${caps.restoreRepoSnapshot} ` +
79
+ `draft=${caps.createRepoDraft} import=${caps.importRepoSnapshot}`);
80
+ return caps;
81
+ };
82
+ export const missingMutations = (c) => Object.entries(c.mutations)
83
+ .filter(([, present]) => !present)
84
+ .map(([name]) => name);