@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,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,122 @@
1
+ import { debug } from "../util/log.js";
2
+ const QUERY = `
3
+ query($repoUuid: ID!, $after: String) {
4
+ aiAgents(repoUuid: $repoUuid, first: 50, after: $after) {
5
+ edges {
6
+ node {
7
+ uuid
8
+ name
9
+ instruction
10
+ repoUuid
11
+ disabledAt
12
+ needReview
13
+ dataSourceIds
14
+ createdAt
15
+ updatedAt
16
+ behaviors {
17
+ id
18
+ uuid
19
+ name
20
+ active
21
+ event_id
22
+ action_id
23
+ event_params {
24
+ to_phase_id
25
+ from_phase_id: fromPhaseId
26
+ in_phase_id: inPhaseId
27
+ trigger_field_ids: triggerFieldIds
28
+ kind_of_sla: kindOfSla
29
+ trigger_automation_id: triggerAutomationId
30
+ }
31
+ condition {
32
+ id
33
+ expressions_structure
34
+ expressions { id field_address operation value structure_id }
35
+ }
36
+ action_params {
37
+ ai_behavior_params: aiBehaviorParams {
38
+ data_source_ids: dataSourceIds
39
+ instruction
40
+ referenced_field_ids: referencedFieldIds
41
+ provider_id: providerId
42
+ system_provider_id: systemProviderId
43
+ actions_attributes: actionsAttributes {
44
+ id
45
+ name
46
+ action_type: actionType
47
+ reference_id: referenceId
48
+ metadata {
49
+ destination_phase_id: destinationPhaseId
50
+ pipe_id: pipeId
51
+ table_id: tableId
52
+ email_template_id: emailTemplateId
53
+ allow_template_modifications: allowTemplateModifications
54
+ mcp_server_id: mcpServerId
55
+ tool_name: toolName
56
+ title
57
+ emails
58
+ tool_inputs: toolInputs { name source value field_id: fieldId }
59
+ fields_attributes: fieldsAttributes { field_id: fieldId input_mode: inputMode value }
60
+ }
61
+ }
62
+ capabilities_attributes: capabilitiesAttributes { capability_type: capabilityType enabled }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ pageInfo { hasNextPage endCursor }
69
+ }
70
+ }
71
+ `;
72
+ /**
73
+ * `event_params` always comes back with every key this query asked for, most
74
+ * of them null for any given event — `card_created` genuinely accepts none of
75
+ * them. An ordinary automation's `event_params` only ever has the keys its
76
+ * own event actually uses, and `pipe validate` checks a present key against
77
+ * the event's catalogue regardless of its value, so a null one reads as one
78
+ * more thing this event does not accept. Dropped to match.
79
+ */
80
+ const pruneNulls = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== null && v !== undefined));
81
+ /** A behavior as the query returns it, given the shape `tree.automations` (AutomationNode) needs. */
82
+ const normaliseBehavior = (b) => ({
83
+ ...b,
84
+ event_params: pruneNulls(b['event_params'] ?? {}),
85
+ condition: b['condition'] ?? null,
86
+ field_maps: [],
87
+ distribute_assignments: [],
88
+ response_schemas: [],
89
+ });
90
+ export const fetchAgents = async (client, repoUuid) => {
91
+ const queryFields = await client.typeFields('Query');
92
+ if (!queryFields?.has('aiAgents')) {
93
+ debug('no aiAgents query on this schema');
94
+ return { agents: [], status: 'unknown', note: 'agents: no aiAgents query on this endpoint — AI agents cannot be read or checked' };
95
+ }
96
+ const agents = [];
97
+ let after = null;
98
+ for (let page = 0; page < 50; page++) {
99
+ const data = await client.tryRaw(QUERY, { repoUuid, after }, 'ai agents');
100
+ const result = data?.aiAgents;
101
+ if (!result) {
102
+ return {
103
+ agents: [],
104
+ status: 'unknown',
105
+ note: 'agents: aiAgents exists but did not answer — AI agent configuration is unverified',
106
+ };
107
+ }
108
+ for (const { node } of result.edges) {
109
+ const { behaviors, ...rest } = node;
110
+ agents.push({
111
+ ...rest,
112
+ uuid: String(rest['uuid']),
113
+ name: String(rest['name'] ?? ''),
114
+ behaviors: (behaviors ?? []).map(normaliseBehavior),
115
+ });
116
+ }
117
+ if (!result.pageInfo.hasNextPage)
118
+ break;
119
+ after = result.pageInfo.endCursor;
120
+ }
121
+ return { agents, status: 'complete' };
122
+ };
@@ -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);