@pipefy/pipefy-process-coder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -0
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -0
- package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -0
- package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -0
- package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -0
- package/AGENTS.md +456 -0
- package/README.md +808 -0
- package/bin/pipe.js +13 -0
- package/package.json +35 -0
- package/src/apply/adopt.ts +150 -0
- package/src/apply/agentops.ts +71 -0
- package/src/apply/compile.ts +875 -0
- package/src/apply/execute.ts +336 -0
- package/src/apply/flowops.ts +399 -0
- package/src/apply/idmap.ts +241 -0
- package/src/apply/mutations.ts +955 -0
- package/src/apply/registry.ts +430 -0
- package/src/apply/types.ts +134 -0
- package/src/cli/args.ts +88 -0
- package/src/cli.ts +211 -0
- package/src/codec/flow.ts +199 -0
- package/src/codec/pack.ts +103 -0
- package/src/codec/roundtrip.ts +94 -0
- package/src/codec/unpack.ts +198 -0
- package/src/commands/agents.ts +184 -0
- package/src/commands/apply.ts +1144 -0
- package/src/commands/context.ts +119 -0
- package/src/commands/create.ts +79 -0
- package/src/commands/diff.ts +314 -0
- package/src/commands/flows.ts +414 -0
- package/src/commands/misc.ts +644 -0
- package/src/commands/plan.ts +331 -0
- package/src/commands/pull.ts +567 -0
- package/src/commands/runs.ts +83 -0
- package/src/commands/skills.ts +137 -0
- package/src/commands/verify.ts +253 -0
- package/src/config.ts +168 -0
- package/src/diff/agents.ts +122 -0
- package/src/diff/diff.ts +1130 -0
- package/src/diff/flow.ts +318 -0
- package/src/diff/html.ts +322 -0
- package/src/diff/render.ts +101 -0
- package/src/model/payload.ts +154 -0
- package/src/model/tree.ts +99 -0
- package/src/model/volatile.ts +55 -0
- package/src/pipefy/agents.ts +165 -0
- package/src/pipefy/automations.ts +219 -0
- package/src/pipefy/capability.ts +119 -0
- package/src/pipefy/client.ts +267 -0
- package/src/pipefy/discovery.ts +209 -0
- package/src/pipefy/internal.ts +380 -0
- package/src/pipefy/ipaas.ts +365 -0
- package/src/pipefy/reconstruct.ts +775 -0
- package/src/pipefy/reference.ts +251 -0
- package/src/pipefy/snapshot.ts +245 -0
- package/src/pipefy/toolkit.ts +200 -0
- package/src/pipefy/toolkit_bearer.py +137 -0
- package/src/report/integrations.ts +231 -0
- package/src/report/run.ts +475 -0
- package/src/util/fsx.ts +45 -0
- package/src/util/git.ts +32 -0
- package/src/util/json.ts +55 -0
- package/src/util/log.ts +76 -0
- package/src/util/pool.ts +48 -0
- package/src/util/slug.ts +26 -0
- package/src/util/tui.ts +335 -0
- package/src/validate/index.ts +123 -0
- package/src/validate/integrity.ts +387 -0
- package/src/validate/reference.ts +136 -0
- package/src/validate/schema.ts +328 -0
- package/src/workspace/agents.ts +290 -0
- package/src/workspace/docs.ts +407 -0
- package/src/workspace/flows.ts +191 -0
- package/src/workspace/layout.ts +165 -0
- package/src/workspace/lock.ts +148 -0
- package/src/workspace/read.ts +165 -0
- package/src/workspace/reference.ts +24 -0
- package/src/workspace/stamp.ts +301 -0
- package/src/workspace/write.ts +225 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { bold, dim, green, red, yellow, blue, magenta } from '../util/log.ts';
|
|
2
|
+
import type { Change, ChangeSet, FieldDelta } from './diff.ts';
|
|
3
|
+
|
|
4
|
+
/** Text rendering of a ChangeSet, for the CLI. */
|
|
5
|
+
|
|
6
|
+
const OP_MARK: Record<Change['op'], string> = {
|
|
7
|
+
create: '+',
|
|
8
|
+
delete: '-',
|
|
9
|
+
update: '~',
|
|
10
|
+
reorder: '>',
|
|
11
|
+
archive: 'a',
|
|
12
|
+
unarchive: 'u',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const colorFor = (op: Change['op']) =>
|
|
16
|
+
op === 'create' ? green : op === 'delete' ? red : op === 'reorder' ? magenta : yellow;
|
|
17
|
+
|
|
18
|
+
const short = (v: unknown, max = 90): string => {
|
|
19
|
+
if (v === null || v === undefined) return 'null';
|
|
20
|
+
if (typeof v === 'string') return v.length > max ? JSON.stringify(v.slice(0, max) + '…') : JSON.stringify(v);
|
|
21
|
+
const s = JSON.stringify(v);
|
|
22
|
+
return s.length > max ? s.slice(0, max) + '…' : s;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const renderDelta = (d: FieldDelta, indent: string): string =>
|
|
26
|
+
`${indent}${dim(d.key)}: ${red(short(d.before))} ${dim('->')} ${green(short(d.after))}`;
|
|
27
|
+
|
|
28
|
+
export const renderChange = (c: Change, opts: { verbose?: boolean } = {}): string[] => {
|
|
29
|
+
const paint = colorFor(c.op);
|
|
30
|
+
const lines: string[] = [];
|
|
31
|
+
const flags = c.flags.length ? ' ' + dim(`[${c.flags.join(' ')}]`) : '';
|
|
32
|
+
lines.push(` ${paint(OP_MARK[c.op])} ${bold(c.entity)} ${c.label}${flags}`);
|
|
33
|
+
|
|
34
|
+
const deltas = opts.verbose ? c.deltas : c.deltas.slice(0, 6);
|
|
35
|
+
for (const d of deltas) lines.push(renderDelta(d, ' '));
|
|
36
|
+
if (c.deltas.length > deltas.length) {
|
|
37
|
+
lines.push(` ${dim(`…and ${c.deltas.length - deltas.length} more field${c.deltas.length - deltas.length === 1 ? '' : 's'}`)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (c.op === 'create' && c.after && !c.deltas.length) {
|
|
41
|
+
const keys = Object.keys(c.after).filter((k) => !k.startsWith('_')).slice(0, 8);
|
|
42
|
+
lines.push(` ${dim(keys.map((k) => `${k}=${short(c.after?.[k], 30)}`).join(' '))}`);
|
|
43
|
+
}
|
|
44
|
+
return lines;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const renderChangeSet = (cs: ChangeSet, opts: { verbose?: boolean } = {}): string => {
|
|
48
|
+
const lines: string[] = [];
|
|
49
|
+
const header =
|
|
50
|
+
cs.mode === 'cross-repo'
|
|
51
|
+
? `${cs.from.name} (${cs.from.repoId}) -> ${cs.to.name} (${cs.to.repoId})`
|
|
52
|
+
: `${cs.to.name} (${cs.to.repoId})`;
|
|
53
|
+
lines.push(bold(`${cs.mode} diff — ${header}`));
|
|
54
|
+
lines.push(dim(`baseline read from ${cs.from.source}; compared tree read from ${cs.to.source}`));
|
|
55
|
+
lines.push('');
|
|
56
|
+
|
|
57
|
+
if (!cs.changes.length) {
|
|
58
|
+
lines.push(green(' no changes'));
|
|
59
|
+
} else {
|
|
60
|
+
const byEntity = new Map<string, Change[]>();
|
|
61
|
+
for (const c of cs.changes) {
|
|
62
|
+
const arr = byEntity.get(c.entity) ?? [];
|
|
63
|
+
arr.push(c);
|
|
64
|
+
byEntity.set(c.entity, arr);
|
|
65
|
+
}
|
|
66
|
+
for (const [entity, group] of byEntity) {
|
|
67
|
+
lines.push(blue(`${entity} (${group.length})`));
|
|
68
|
+
for (const c of group) lines.push(...renderChange(c, opts));
|
|
69
|
+
lines.push('');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (cs.skipped.length) {
|
|
74
|
+
lines.push(yellow('not diffed — no read path on one side:'));
|
|
75
|
+
for (const s of cs.skipped) lines.push(` ${s.entity}: ${dim(s.reason)}`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const counts = cs.changes.reduce<Record<string, number>>((acc, c) => {
|
|
80
|
+
acc[c.op] = (acc[c.op] ?? 0) + 1;
|
|
81
|
+
return acc;
|
|
82
|
+
}, {});
|
|
83
|
+
const summary = Object.entries(counts)
|
|
84
|
+
.map(([op, n]) => `${n} ${op}`)
|
|
85
|
+
.join(', ');
|
|
86
|
+
lines.push(bold(summary || 'nothing to do'));
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Machine-readable form, for `--json` and for the HTML view. */
|
|
91
|
+
export const toJson = (cs: ChangeSet) => ({
|
|
92
|
+
mode: cs.mode,
|
|
93
|
+
from: cs.from,
|
|
94
|
+
to: cs.to,
|
|
95
|
+
summary: cs.changes.reduce<Record<string, number>>((acc, c) => {
|
|
96
|
+
acc[c.op] = (acc[c.op] ?? 0) + 1;
|
|
97
|
+
return acc;
|
|
98
|
+
}, {}),
|
|
99
|
+
skipped: cs.skipped,
|
|
100
|
+
changes: cs.changes,
|
|
101
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
|
|
9
|
+
export type Row = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export const ENTITY_NAMES = [
|
|
12
|
+
'phases',
|
|
13
|
+
'phase_team_memberships',
|
|
14
|
+
'phase_jumps',
|
|
15
|
+
'fields',
|
|
16
|
+
'field_conditions',
|
|
17
|
+
'field_condition_actions',
|
|
18
|
+
'automations',
|
|
19
|
+
'conditions',
|
|
20
|
+
'condition_expressions',
|
|
21
|
+
'field_maps',
|
|
22
|
+
'distribute_assignments',
|
|
23
|
+
'response_schemas',
|
|
24
|
+
'repo_preferences',
|
|
25
|
+
'labels',
|
|
26
|
+
'pipe_relations',
|
|
27
|
+
'email_inboxes',
|
|
28
|
+
'webhooks',
|
|
29
|
+
'public_forms',
|
|
30
|
+
'email_templates',
|
|
31
|
+
'visibilities',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export type EntityName = (typeof ENTITY_NAMES)[number];
|
|
35
|
+
|
|
36
|
+
export type Relations = Record<EntityName, Row[]>;
|
|
37
|
+
|
|
38
|
+
export type RepoPayload = Row & { id: number; relations: Relations };
|
|
39
|
+
|
|
40
|
+
export const emptyRelations = (): Relations =>
|
|
41
|
+
Object.fromEntries(ENTITY_NAMES.map((n) => [n, [] as Row[]])) as Relations;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* How much of an entity array a given read actually knows about.
|
|
45
|
+
*
|
|
46
|
+
* This is the safety mechanism a non-snapshot read requires and that neither
|
|
47
|
+
* PLAN.md nor PLAN-ROUTE-B.md has: a reconstructed payload cannot distinguish
|
|
48
|
+
* "this pipe has no webhooks" from "I have no way to read webhooks". Recording
|
|
49
|
+
* `unknown` keeps pack and diff from ever reading absence as deletion
|
|
50
|
+
* (PLAN-ROUTE-B.md §7 — "a dropped entity means the importer deletes it").
|
|
51
|
+
*/
|
|
52
|
+
export type Coverage = 'complete' | 'partial' | 'unknown';
|
|
53
|
+
|
|
54
|
+
export type PayloadEnvelope = {
|
|
55
|
+
/** Where the structure came from. */
|
|
56
|
+
source: 'snapshot' | 'reconstructed';
|
|
57
|
+
/** Snapshot version id, when source === 'snapshot'. */
|
|
58
|
+
versionId?: string | null;
|
|
59
|
+
/** ISO time of the read. */
|
|
60
|
+
readAt: string;
|
|
61
|
+
repoKind: 'pipe' | 'table';
|
|
62
|
+
coverage: Record<EntityName, Coverage>;
|
|
63
|
+
/** Whether the repo row's own columns were all readable. */
|
|
64
|
+
repo_columns?: Coverage;
|
|
65
|
+
/** What the read path could not see, for the docs and the diff header. */
|
|
66
|
+
notes: string[];
|
|
67
|
+
payload: RepoPayload;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const fullCoverage = (c: Coverage): Record<EntityName, Coverage> =>
|
|
71
|
+
Object.fromEntries(ENTITY_NAMES.map((n) => [n, c])) as Record<EntityName, Coverage>;
|
|
72
|
+
|
|
73
|
+
export type Identity = 'uuid' | 'id' | 'owned';
|
|
74
|
+
|
|
75
|
+
export type EntitySpec = {
|
|
76
|
+
name: EntityName;
|
|
77
|
+
/**
|
|
78
|
+
* How an entity is recognised across two reads.
|
|
79
|
+
* `uuid` is present on only 4 of 20 arrays (PLAN.md §1.3); `owned` entities
|
|
80
|
+
* have no identity of their own and are diffed as a replace-set under their
|
|
81
|
+
* owner, exactly as ROUTE-B-V2.md §4 proposes the importer treat them.
|
|
82
|
+
*/
|
|
83
|
+
identity: Identity;
|
|
84
|
+
/** For `owned`: the columns naming the owner, in precedence order. */
|
|
85
|
+
ownerKeys?: string[];
|
|
86
|
+
/** Fallback natural key, used when matching across two different repos. */
|
|
87
|
+
naturalKey?: string[];
|
|
88
|
+
/** No public write path exists — PLAN.md §2. */
|
|
89
|
+
readonly?: boolean;
|
|
90
|
+
/** Written through the internal API rather than public GraphQL. */
|
|
91
|
+
internal?: boolean;
|
|
92
|
+
/** Folded into the owner's file by the codec rather than getting its own. */
|
|
93
|
+
foldedInto?: EntityName;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const ENTITIES: Record<EntityName, EntitySpec> = {
|
|
97
|
+
phases: { name: 'phases', identity: 'uuid', naturalKey: ['name'] },
|
|
98
|
+
fields: { name: 'fields', identity: 'uuid', naturalKey: ['phase_id', 'label'], foldedInto: 'phases' },
|
|
99
|
+
automations: { name: 'automations', identity: 'uuid', naturalKey: ['name'] },
|
|
100
|
+
labels: { name: 'labels', identity: 'uuid', naturalKey: ['name'] },
|
|
101
|
+
|
|
102
|
+
field_conditions: { name: 'field_conditions', identity: 'id', naturalKey: ['phase_id', 'name'] },
|
|
103
|
+
webhooks: { name: 'webhooks', identity: 'id', naturalKey: ['name', 'url'] },
|
|
104
|
+
pipe_relations: { name: 'pipe_relations', identity: 'id', naturalKey: ['parent_id', 'child_id', 'name'] },
|
|
105
|
+
|
|
106
|
+
conditions: { name: 'conditions', identity: 'owned', ownerKeys: ['source_type', 'source_id'] },
|
|
107
|
+
condition_expressions: { name: 'condition_expressions', identity: 'owned', ownerKeys: ['condition_id'] },
|
|
108
|
+
field_maps: { name: 'field_maps', identity: 'owned', ownerKeys: ['owner_type', 'owner_id'] },
|
|
109
|
+
field_condition_actions: {
|
|
110
|
+
name: 'field_condition_actions',
|
|
111
|
+
identity: 'owned',
|
|
112
|
+
ownerKeys: ['field_condition_id'],
|
|
113
|
+
foldedInto: 'field_conditions',
|
|
114
|
+
},
|
|
115
|
+
phase_jumps: { name: 'phase_jumps', identity: 'owned', ownerKeys: ['source_phase_id'], internal: true },
|
|
116
|
+
phase_team_memberships: {
|
|
117
|
+
name: 'phase_team_memberships',
|
|
118
|
+
identity: 'owned',
|
|
119
|
+
ownerKeys: ['phase_id'],
|
|
120
|
+
foldedInto: 'phases',
|
|
121
|
+
},
|
|
122
|
+
distribute_assignments: {
|
|
123
|
+
name: 'distribute_assignments',
|
|
124
|
+
identity: 'owned',
|
|
125
|
+
ownerKeys: ['automation_id'],
|
|
126
|
+
foldedInto: 'automations',
|
|
127
|
+
},
|
|
128
|
+
response_schemas: {
|
|
129
|
+
name: 'response_schemas',
|
|
130
|
+
identity: 'owned',
|
|
131
|
+
ownerKeys: ['automation_id'],
|
|
132
|
+
foldedInto: 'automations',
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
public_forms: { name: 'public_forms', identity: 'owned', ownerKeys: ['repo_id'] },
|
|
136
|
+
repo_preferences: { name: 'repo_preferences', identity: 'owned', ownerKeys: ['repo_id'] },
|
|
137
|
+
|
|
138
|
+
visibilities: { name: 'visibilities', identity: 'owned', ownerKeys: ['resource_type', 'resource_id'], readonly: true },
|
|
139
|
+
email_templates: { name: 'email_templates', identity: 'id', readonly: true },
|
|
140
|
+
email_inboxes: { name: 'email_inboxes', identity: 'id', readonly: true },
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const READONLY_ENTITIES = ENTITY_NAMES.filter((n) => ENTITIES[n].readonly);
|
|
144
|
+
|
|
145
|
+
export const rel = (p: RepoPayload, n: EntityName): Row[] => p.relations?.[n] ?? [];
|
|
146
|
+
|
|
147
|
+
export const numId = (r: Row): number | null => {
|
|
148
|
+
const v = r['id'];
|
|
149
|
+
if (typeof v === 'number') return v;
|
|
150
|
+
if (typeof v === 'string' && /^\d+$/.test(v)) return Number(v);
|
|
151
|
+
return null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const strId = (r: Row): string => String(r['id'] ?? '');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Coverage, EntityName, Row } from './payload.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The workspace tree — what Claude and the FDE actually edit. PLAN.md §4.
|
|
5
|
+
*
|
|
6
|
+
* Folding is *movement only*: a folded row is the payload row, unchanged, moved
|
|
7
|
+
* under its owner. Nothing is renamed or reshaped, which is what makes
|
|
8
|
+
* `pack(unpack(P))` equal to `P` by construction rather than by care.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type ConditionNode = Row & {
|
|
12
|
+
/** condition_expressions rows belonging to this condition. */
|
|
13
|
+
expressions: Row[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type PhaseNode = Row & {
|
|
17
|
+
/** fields rows whose phase_id is this phase, ordered by index. */
|
|
18
|
+
fields: Row[];
|
|
19
|
+
/**
|
|
20
|
+
* phase_jumps targets for this phase, as a set. The internal settings
|
|
21
|
+
* endpoint is set-replacing (PLAN.md §2.1), so a set is the honest shape and
|
|
22
|
+
* there is no add/remove diffing to get wrong.
|
|
23
|
+
*/
|
|
24
|
+
jump_target_ids: Array<number | string>;
|
|
25
|
+
/** phase_team_memberships, flattened to the user ids phaseSettings takes. */
|
|
26
|
+
team_member_ids: Array<number | string>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AutomationNode = Row & {
|
|
30
|
+
condition?: ConditionNode | null;
|
|
31
|
+
field_maps: Row[];
|
|
32
|
+
distribute_assignments: Row[];
|
|
33
|
+
response_schemas: Row[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type FieldConditionNode = Row & {
|
|
37
|
+
actions: Row[];
|
|
38
|
+
condition?: ConditionNode | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type TreeMeta = {
|
|
42
|
+
source: 'snapshot' | 'reconstructed';
|
|
43
|
+
versionId?: string | null;
|
|
44
|
+
readAt: string;
|
|
45
|
+
repoKind: 'pipe' | 'table';
|
|
46
|
+
coverage: Record<EntityName, Coverage>;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the repo row itself was read in full. The public API has no
|
|
49
|
+
* `color_uuid`, `authorization` or `draft`, so a reconstructed root is
|
|
50
|
+
* partial — and the diff must not read those absences as "set to null".
|
|
51
|
+
*/
|
|
52
|
+
repo_columns: Coverage;
|
|
53
|
+
notes: string[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type Tree = {
|
|
57
|
+
meta: TreeMeta;
|
|
58
|
+
/** The `repos` row, volatile columns stripped. */
|
|
59
|
+
repo: Row;
|
|
60
|
+
phases: PhaseNode[];
|
|
61
|
+
automations: AutomationNode[];
|
|
62
|
+
field_conditions: FieldConditionNode[];
|
|
63
|
+
labels: Row[];
|
|
64
|
+
webhooks: Row[];
|
|
65
|
+
pipe_relations: Row[];
|
|
66
|
+
public_form: Row | null;
|
|
67
|
+
preferences: Row | null;
|
|
68
|
+
/** PLAN.md §4: first-class, so the planner can reject edits by path alone. */
|
|
69
|
+
readonly: {
|
|
70
|
+
email_templates: Row[];
|
|
71
|
+
email_inboxes: Row[];
|
|
72
|
+
visibilities: Row[];
|
|
73
|
+
/**
|
|
74
|
+
* `field_maps` owned by a **field**, not an automation — a connector
|
|
75
|
+
* field's own "copy this value from the connected card" configuration.
|
|
76
|
+
* `unpack`/`pack` only ever modelled the automation-owned half of this
|
|
77
|
+
* entity (PLAN.md, ROUTE-B-V2.md both describe it that way), so these rows
|
|
78
|
+
* were silently dropped on every pull until the round-trip check caught it.
|
|
79
|
+
* `owner_type`/`owner_id` are kept, unlike the automation-nested form,
|
|
80
|
+
* because this is a flat bucket spanning however many fields own one.
|
|
81
|
+
*/
|
|
82
|
+
field_maps: Row[];
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const repoId = (t: Tree): number => Number(t.repo['id']);
|
|
87
|
+
export const repoName = (t: Tree): string => String(t.repo['name'] ?? `repo ${repoId(t)}`);
|
|
88
|
+
|
|
89
|
+
/** The start form is a phase (id/index 0). PLAN.md §4 — deleting it is catastrophic. */
|
|
90
|
+
export const startFormPhase = (t: Tree): PhaseNode | undefined =>
|
|
91
|
+
t.phases.find((p) => Number(p['index']) === 0) ?? t.phases[0];
|
|
92
|
+
|
|
93
|
+
export const allFields = (t: Tree): Row[] => t.phases.flatMap((p) => p.fields);
|
|
94
|
+
|
|
95
|
+
export const fieldById = (t: Tree, id: string | number): Row | undefined =>
|
|
96
|
+
allFields(t).find((f) => String(f['id']) === String(id));
|
|
97
|
+
|
|
98
|
+
export const phaseById = (t: Tree, id: string | number): PhaseNode | undefined =>
|
|
99
|
+
t.phases.find((p) => String(p['id']) === String(id));
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
|
|
7
|
+
export const VOLATILE_KEYS = new Set([
|
|
8
|
+
'created_at',
|
|
9
|
+
'updated_at',
|
|
10
|
+
'deleted_at',
|
|
11
|
+
'created_by_id',
|
|
12
|
+
'updated_by_id',
|
|
13
|
+
'run_count',
|
|
14
|
+
'cards_count',
|
|
15
|
+
'users_count',
|
|
16
|
+
'opened_cards_count',
|
|
17
|
+
'last_updated_by_card',
|
|
18
|
+
'cloning',
|
|
19
|
+
'sequential_id',
|
|
20
|
+
'clone_from_id',
|
|
21
|
+
'cloned_from_id',
|
|
22
|
+
'cloned_from_phase_uuid',
|
|
23
|
+
'cloned_from_field_uuid',
|
|
24
|
+
'migrated_from_repo_uuid',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Server-owned identity and ownership columns. Kept in the files (Claude needs
|
|
29
|
+
* to read them for a field reference to mean anything) but never diffed as
|
|
30
|
+
* content, because they are consequences of structure rather than structure.
|
|
31
|
+
*/
|
|
32
|
+
export const SERVER_OWNED_KEYS = new Set([
|
|
33
|
+
'id',
|
|
34
|
+
'repo_id',
|
|
35
|
+
'organization_id',
|
|
36
|
+
'organization_uuid',
|
|
37
|
+
'suid',
|
|
38
|
+
'slug',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export const stripVolatile = <T extends Record<string, unknown>>(row: T): T => {
|
|
42
|
+
const out: Record<string, unknown> = {};
|
|
43
|
+
for (const [k, v] of Object.entries(row)) {
|
|
44
|
+
if (VOLATILE_KEYS.has(k)) continue;
|
|
45
|
+
out[k] = v;
|
|
46
|
+
}
|
|
47
|
+
return out as T;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The volatile values we drop, kept aside so the UI can still show them. */
|
|
51
|
+
export const takeVolatile = (row: Record<string, unknown>): Record<string, unknown> => {
|
|
52
|
+
const out: Record<string, unknown> = {};
|
|
53
|
+
for (const [k, v] of Object.entries(row)) if (VOLATILE_KEYS.has(k)) out[k] = v;
|
|
54
|
+
return out;
|
|
55
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { debug } from '../util/log.ts';
|
|
2
|
+
import type { Row } from '../model/payload.ts';
|
|
3
|
+
import type { PipefyClient } from './client.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AI agents — not in the snapshot payload at all (ROUTE-B-V2.md ask 5), so read
|
|
7
|
+
* separately over the public GraphQL API, keyed by the pipe's **uuid**, not its
|
|
8
|
+
* numeric id.
|
|
9
|
+
*
|
|
10
|
+
* `AiAgent.behaviors` answers with full `Automation` rows — a behavior is not a
|
|
11
|
+
* new kind of thing, it is an automation with `action_id: "ai_behavior"`. But
|
|
12
|
+
* the *snapshot* payload's row for one is a stub: `behavior_uuid` plus a couple
|
|
13
|
+
* of field-id lists, no `instruction`, no `actionsAttributes` — the actual
|
|
14
|
+
* configuration only exists behind this query. So this is the one source for a
|
|
15
|
+
* behavior's real content; the snapshot's copy of it is never used.
|
|
16
|
+
*
|
|
17
|
+
* The query aliases every field to the snake_case name the rest of this tool
|
|
18
|
+
* already uses on disk (matching `automations/*.json`), so nothing downstream
|
|
19
|
+
* has to know these rows came from a different endpoint.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type AgentRow = Row & {
|
|
23
|
+
uuid: string;
|
|
24
|
+
name: string;
|
|
25
|
+
/** Full automation-shaped rows — same content `workspace/agents.ts` files under `behaviors/`. */
|
|
26
|
+
behaviors: Row[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AgentsRead = {
|
|
30
|
+
agents: AgentRow[];
|
|
31
|
+
/** `complete` means this pull read them; `unknown` means it could not. */
|
|
32
|
+
status: 'complete' | 'unknown';
|
|
33
|
+
note?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type Edge = { node: Row & { behaviors?: Row[] | null } };
|
|
37
|
+
type Page = { edges: Edge[]; pageInfo: { hasNextPage: boolean; endCursor: string | null } };
|
|
38
|
+
|
|
39
|
+
const QUERY = `
|
|
40
|
+
query($repoUuid: ID!, $after: String) {
|
|
41
|
+
aiAgents(repoUuid: $repoUuid, first: 50, after: $after) {
|
|
42
|
+
edges {
|
|
43
|
+
node {
|
|
44
|
+
uuid
|
|
45
|
+
name
|
|
46
|
+
instruction
|
|
47
|
+
repoUuid
|
|
48
|
+
disabledAt
|
|
49
|
+
needReview
|
|
50
|
+
dataSourceIds
|
|
51
|
+
createdAt
|
|
52
|
+
updatedAt
|
|
53
|
+
behaviors {
|
|
54
|
+
id
|
|
55
|
+
uuid
|
|
56
|
+
name
|
|
57
|
+
active
|
|
58
|
+
event_id
|
|
59
|
+
action_id
|
|
60
|
+
event_params {
|
|
61
|
+
to_phase_id
|
|
62
|
+
from_phase_id: fromPhaseId
|
|
63
|
+
in_phase_id: inPhaseId
|
|
64
|
+
trigger_field_ids: triggerFieldIds
|
|
65
|
+
kind_of_sla: kindOfSla
|
|
66
|
+
trigger_automation_id: triggerAutomationId
|
|
67
|
+
}
|
|
68
|
+
condition {
|
|
69
|
+
id
|
|
70
|
+
expressions_structure
|
|
71
|
+
expressions { id field_address operation value structure_id }
|
|
72
|
+
}
|
|
73
|
+
action_params {
|
|
74
|
+
ai_behavior_params: aiBehaviorParams {
|
|
75
|
+
data_source_ids: dataSourceIds
|
|
76
|
+
instruction
|
|
77
|
+
referenced_field_ids: referencedFieldIds
|
|
78
|
+
provider_id: providerId
|
|
79
|
+
system_provider_id: systemProviderId
|
|
80
|
+
actions_attributes: actionsAttributes {
|
|
81
|
+
id
|
|
82
|
+
name
|
|
83
|
+
action_type: actionType
|
|
84
|
+
reference_id: referenceId
|
|
85
|
+
metadata {
|
|
86
|
+
destination_phase_id: destinationPhaseId
|
|
87
|
+
pipe_id: pipeId
|
|
88
|
+
table_id: tableId
|
|
89
|
+
email_template_id: emailTemplateId
|
|
90
|
+
allow_template_modifications: allowTemplateModifications
|
|
91
|
+
mcp_server_id: mcpServerId
|
|
92
|
+
tool_name: toolName
|
|
93
|
+
title
|
|
94
|
+
emails
|
|
95
|
+
tool_inputs: toolInputs { name source value field_id: fieldId }
|
|
96
|
+
fields_attributes: fieldsAttributes { field_id: fieldId input_mode: inputMode value }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
capabilities_attributes: capabilitiesAttributes { capability_type: capabilityType enabled }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
pageInfo { hasNextPage endCursor }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* `event_params` always comes back with every key this query asked for, most
|
|
112
|
+
* of them null for any given event — `card_created` genuinely accepts none of
|
|
113
|
+
* them. An ordinary automation's `event_params` only ever has the keys its
|
|
114
|
+
* own event actually uses, and `pipe validate` checks a present key against
|
|
115
|
+
* the event's catalogue regardless of its value, so a null one reads as one
|
|
116
|
+
* more thing this event does not accept. Dropped to match.
|
|
117
|
+
*/
|
|
118
|
+
const pruneNulls = (o: Row): Row => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== null && v !== undefined));
|
|
119
|
+
|
|
120
|
+
/** A behavior as the query returns it, given the shape `tree.automations` (AutomationNode) needs. */
|
|
121
|
+
const normaliseBehavior = (b: Row): Row => ({
|
|
122
|
+
...b,
|
|
123
|
+
event_params: pruneNulls((b['event_params'] as Row) ?? {}),
|
|
124
|
+
condition: b['condition'] ?? null,
|
|
125
|
+
field_maps: [],
|
|
126
|
+
distribute_assignments: [],
|
|
127
|
+
response_schemas: [],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export const fetchAgents = async (client: PipefyClient, repoUuid: string): Promise<AgentsRead> => {
|
|
131
|
+
const queryFields = await client.typeFields('Query');
|
|
132
|
+
if (!queryFields?.has('aiAgents')) {
|
|
133
|
+
debug('no aiAgents query on this schema');
|
|
134
|
+
return { agents: [], status: 'unknown', note: 'agents: no aiAgents query on this endpoint — AI agents cannot be read or checked' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const agents: AgentRow[] = [];
|
|
138
|
+
let after: string | null = null;
|
|
139
|
+
for (let page = 0; page < 50; page++) {
|
|
140
|
+
const data: { aiAgents: Page | null } | null = await client.tryRaw(QUERY, { repoUuid, after }, 'ai agents');
|
|
141
|
+
const result: Page | null | undefined = data?.aiAgents;
|
|
142
|
+
if (!result) {
|
|
143
|
+
return {
|
|
144
|
+
agents: [],
|
|
145
|
+
status: 'unknown',
|
|
146
|
+
note: 'agents: aiAgents exists but did not answer — AI agent configuration is unverified',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const { node } of result.edges) {
|
|
151
|
+
const { behaviors, ...rest } = node;
|
|
152
|
+
agents.push({
|
|
153
|
+
...rest,
|
|
154
|
+
uuid: String(rest['uuid']),
|
|
155
|
+
name: String(rest['name'] ?? ''),
|
|
156
|
+
behaviors: (behaviors ?? []).map(normaliseBehavior),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!result.pageInfo.hasNextPage) break;
|
|
161
|
+
after = result.pageInfo.endCursor;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { agents, status: 'complete' };
|
|
165
|
+
};
|