@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.
- package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -262
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -247
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -589
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -1391
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -623
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -636
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -3642
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -863
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -799
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -42
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -581
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -503
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -562
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -557
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -609
- package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -146
- package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -127
- package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -91
- package/AGENTS.md +476 -456
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +830 -808
- package/bin/pipe.js +25 -4
- package/dist/apply/adopt.js +128 -0
- package/dist/apply/agentops.js +42 -0
- package/dist/apply/compile.js +722 -0
- package/dist/apply/execute.js +277 -0
- package/dist/apply/flowops.js +293 -0
- package/dist/apply/idmap.js +233 -0
- package/dist/apply/mutations.js +704 -0
- package/dist/apply/registry.js +348 -0
- package/dist/apply/types.js +18 -0
- package/dist/cli/args.js +79 -0
- package/dist/cli.js +196 -0
- package/dist/codec/flow.js +164 -0
- package/dist/codec/pack.js +88 -0
- package/dist/codec/roundtrip.js +86 -0
- package/dist/codec/unpack.js +181 -0
- package/dist/commands/agents.js +145 -0
- package/dist/commands/apply.js +982 -0
- package/dist/commands/context.js +79 -0
- package/dist/commands/create.js +63 -0
- package/dist/commands/diff.js +294 -0
- package/dist/commands/flows.js +348 -0
- package/dist/commands/misc.js +562 -0
- package/dist/commands/plan.js +301 -0
- package/dist/commands/pull.js +549 -0
- package/dist/commands/runs.js +74 -0
- package/dist/commands/skills.js +131 -0
- package/dist/commands/verify.js +205 -0
- package/dist/config.js +125 -0
- package/dist/diff/agents.js +80 -0
- package/dist/diff/diff.js +983 -0
- package/dist/diff/flow.js +258 -0
- package/{src/diff/html.ts → dist/diff/html.js} +274 -322
- package/dist/diff/render.js +90 -0
- package/dist/model/payload.js +84 -0
- package/dist/model/tree.js +7 -0
- package/dist/model/volatile.js +54 -0
- package/dist/pipefy/agents.js +131 -0
- package/dist/pipefy/automations.js +155 -0
- package/dist/pipefy/capability.js +84 -0
- package/dist/pipefy/client.js +204 -0
- package/dist/pipefy/discovery.js +148 -0
- package/dist/pipefy/internal.js +406 -0
- package/dist/pipefy/ipaas.js +269 -0
- package/dist/pipefy/reconstruct.js +676 -0
- package/dist/pipefy/reference.js +172 -0
- package/dist/pipefy/snapshot.js +185 -0
- package/dist/pipefy/toolkit.js +174 -0
- package/{src → dist}/pipefy/toolkit_bearer.py +60 -14
- package/dist/report/integrations.js +165 -0
- package/dist/report/run.js +356 -0
- package/dist/util/fsx.js +50 -0
- package/dist/util/git.js +25 -0
- package/dist/util/json.js +56 -0
- package/dist/util/log.js +69 -0
- package/dist/util/pool.js +42 -0
- package/dist/util/slug.js +23 -0
- package/dist/util/tui.js +287 -0
- package/dist/validate/index.js +86 -0
- package/dist/validate/integrity.js +370 -0
- package/dist/validate/reference.js +120 -0
- package/dist/validate/schema.js +278 -0
- package/dist/version.js +18 -0
- package/dist/workspace/agents.js +245 -0
- package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
- package/dist/workspace/flows.js +149 -0
- package/dist/workspace/layout.js +146 -0
- package/dist/workspace/lock.js +83 -0
- package/dist/workspace/read.js +168 -0
- package/dist/workspace/reference.js +24 -0
- package/dist/workspace/stamp.js +234 -0
- package/dist/workspace/write.js +194 -0
- package/package.json +38 -35
- package/src/apply/adopt.ts +0 -150
- package/src/apply/agentops.ts +0 -71
- package/src/apply/compile.ts +0 -875
- package/src/apply/execute.ts +0 -336
- package/src/apply/flowops.ts +0 -399
- package/src/apply/idmap.ts +0 -241
- package/src/apply/mutations.ts +0 -955
- package/src/apply/registry.ts +0 -430
- package/src/apply/types.ts +0 -134
- package/src/cli/args.ts +0 -88
- package/src/cli.ts +0 -211
- package/src/codec/flow.ts +0 -199
- package/src/codec/pack.ts +0 -103
- package/src/codec/roundtrip.ts +0 -94
- package/src/codec/unpack.ts +0 -198
- package/src/commands/agents.ts +0 -184
- package/src/commands/apply.ts +0 -1144
- package/src/commands/context.ts +0 -119
- package/src/commands/create.ts +0 -79
- package/src/commands/diff.ts +0 -314
- package/src/commands/flows.ts +0 -414
- package/src/commands/misc.ts +0 -644
- package/src/commands/plan.ts +0 -331
- package/src/commands/pull.ts +0 -567
- package/src/commands/runs.ts +0 -83
- package/src/commands/skills.ts +0 -137
- package/src/commands/verify.ts +0 -253
- package/src/config.ts +0 -168
- package/src/diff/agents.ts +0 -122
- package/src/diff/diff.ts +0 -1130
- package/src/diff/flow.ts +0 -318
- package/src/diff/render.ts +0 -101
- package/src/model/payload.ts +0 -154
- package/src/model/tree.ts +0 -99
- package/src/model/volatile.ts +0 -55
- package/src/pipefy/agents.ts +0 -165
- package/src/pipefy/automations.ts +0 -219
- package/src/pipefy/capability.ts +0 -119
- package/src/pipefy/client.ts +0 -267
- package/src/pipefy/discovery.ts +0 -209
- package/src/pipefy/internal.ts +0 -380
- package/src/pipefy/ipaas.ts +0 -365
- package/src/pipefy/reconstruct.ts +0 -775
- package/src/pipefy/reference.ts +0 -251
- package/src/pipefy/snapshot.ts +0 -245
- package/src/pipefy/toolkit.ts +0 -200
- package/src/report/integrations.ts +0 -231
- package/src/report/run.ts +0 -475
- package/src/util/fsx.ts +0 -45
- package/src/util/git.ts +0 -32
- package/src/util/json.ts +0 -55
- package/src/util/log.ts +0 -76
- package/src/util/pool.ts +0 -48
- package/src/util/slug.ts +0 -26
- package/src/util/tui.ts +0 -335
- package/src/validate/index.ts +0 -123
- package/src/validate/integrity.ts +0 -387
- package/src/validate/reference.ts +0 -136
- package/src/validate/schema.ts +0 -328
- package/src/workspace/agents.ts +0 -290
- package/src/workspace/flows.ts +0 -191
- package/src/workspace/layout.ts +0 -165
- package/src/workspace/lock.ts +0 -148
- package/src/workspace/read.ts +0 -165
- package/src/workspace/reference.ts +0 -24
- package/src/workspace/stamp.ts +0 -301
- 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);
|