@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.
Files changed (92) hide show
  1. package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -0
  2. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -0
  3. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -0
  4. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -0
  5. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -0
  6. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -0
  7. package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -0
  8. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -0
  9. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -0
  10. package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -0
  11. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -0
  12. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -0
  13. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -0
  14. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -0
  15. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -0
  16. package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -0
  17. package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -0
  18. package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -0
  19. package/AGENTS.md +456 -0
  20. package/README.md +808 -0
  21. package/bin/pipe.js +13 -0
  22. package/package.json +35 -0
  23. package/src/apply/adopt.ts +150 -0
  24. package/src/apply/agentops.ts +71 -0
  25. package/src/apply/compile.ts +875 -0
  26. package/src/apply/execute.ts +336 -0
  27. package/src/apply/flowops.ts +399 -0
  28. package/src/apply/idmap.ts +241 -0
  29. package/src/apply/mutations.ts +955 -0
  30. package/src/apply/registry.ts +430 -0
  31. package/src/apply/types.ts +134 -0
  32. package/src/cli/args.ts +88 -0
  33. package/src/cli.ts +211 -0
  34. package/src/codec/flow.ts +199 -0
  35. package/src/codec/pack.ts +103 -0
  36. package/src/codec/roundtrip.ts +94 -0
  37. package/src/codec/unpack.ts +198 -0
  38. package/src/commands/agents.ts +184 -0
  39. package/src/commands/apply.ts +1144 -0
  40. package/src/commands/context.ts +119 -0
  41. package/src/commands/create.ts +79 -0
  42. package/src/commands/diff.ts +314 -0
  43. package/src/commands/flows.ts +414 -0
  44. package/src/commands/misc.ts +644 -0
  45. package/src/commands/plan.ts +331 -0
  46. package/src/commands/pull.ts +567 -0
  47. package/src/commands/runs.ts +83 -0
  48. package/src/commands/skills.ts +137 -0
  49. package/src/commands/verify.ts +253 -0
  50. package/src/config.ts +168 -0
  51. package/src/diff/agents.ts +122 -0
  52. package/src/diff/diff.ts +1130 -0
  53. package/src/diff/flow.ts +318 -0
  54. package/src/diff/html.ts +322 -0
  55. package/src/diff/render.ts +101 -0
  56. package/src/model/payload.ts +154 -0
  57. package/src/model/tree.ts +99 -0
  58. package/src/model/volatile.ts +55 -0
  59. package/src/pipefy/agents.ts +165 -0
  60. package/src/pipefy/automations.ts +219 -0
  61. package/src/pipefy/capability.ts +119 -0
  62. package/src/pipefy/client.ts +267 -0
  63. package/src/pipefy/discovery.ts +209 -0
  64. package/src/pipefy/internal.ts +380 -0
  65. package/src/pipefy/ipaas.ts +365 -0
  66. package/src/pipefy/reconstruct.ts +775 -0
  67. package/src/pipefy/reference.ts +251 -0
  68. package/src/pipefy/snapshot.ts +245 -0
  69. package/src/pipefy/toolkit.ts +200 -0
  70. package/src/pipefy/toolkit_bearer.py +137 -0
  71. package/src/report/integrations.ts +231 -0
  72. package/src/report/run.ts +475 -0
  73. package/src/util/fsx.ts +45 -0
  74. package/src/util/git.ts +32 -0
  75. package/src/util/json.ts +55 -0
  76. package/src/util/log.ts +76 -0
  77. package/src/util/pool.ts +48 -0
  78. package/src/util/slug.ts +26 -0
  79. package/src/util/tui.ts +335 -0
  80. package/src/validate/index.ts +123 -0
  81. package/src/validate/integrity.ts +387 -0
  82. package/src/validate/reference.ts +136 -0
  83. package/src/validate/schema.ts +328 -0
  84. package/src/workspace/agents.ts +290 -0
  85. package/src/workspace/docs.ts +407 -0
  86. package/src/workspace/flows.ts +191 -0
  87. package/src/workspace/layout.ts +165 -0
  88. package/src/workspace/lock.ts +148 -0
  89. package/src/workspace/read.ts +165 -0
  90. package/src/workspace/reference.ts +24 -0
  91. package/src/workspace/stamp.ts +301 -0
  92. package/src/workspace/write.ts +225 -0
@@ -0,0 +1,137 @@
1
+ """Print a Pipefy bearer token resolved by the official ai-toolkit.
2
+
3
+ Run inside the toolkit's own environment (see toolkit.ts for how it is located),
4
+ so the credential precedence, the keychain backend and the refresh logic are all
5
+ the toolkit's rather than reimplemented here:
6
+
7
+ static token (PIPEFY_TOKEN / config.toml)
8
+ -> service account (PIPEFY_SERVICE_ACCOUNT_*)
9
+ -> stored session from `pipefy auth login`, refreshed if stale
10
+
11
+ Emits one JSON object on stdout and nothing else. Never writes the token
12
+ anywhere; the caller holds it in memory for the life of one command.
13
+
14
+ {"ok": true, "source": "stored-session", "token": "...", "base_url": "..."}
15
+ {"ok": false, "reason": "no-credentials", "message": "..."}
16
+
17
+ Exit code is 0 whenever the JSON was produced, including for "no credentials" —
18
+ that is an answer, not a failure. A non-zero exit means the toolkit itself could
19
+ not be used, and the caller falls back to its own credential store.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import sys
26
+
27
+
28
+ def _fail(reason: str, message: str) -> int:
29
+ json.dump({"ok": False, "reason": reason, "message": message}, sys.stdout)
30
+ return 0
31
+
32
+
33
+ def main() -> int:
34
+ try:
35
+ from pipefy_auth import ( # noqa: PLC0415
36
+ AuthSettings,
37
+ OidcClient,
38
+ ServiceAccountAuth,
39
+ StaticTokenAuth,
40
+ StoredSessionAuth,
41
+ configure_keychain_backend,
42
+ ensure_fresh_session,
43
+ resolve_pipefy_auth,
44
+ )
45
+ from pipefy_auth.resolver import ServiceAccount # noqa: PLC0415
46
+ except Exception as exc: # pragma: no cover - environment problem
47
+ print(f"pipefy_auth is not importable: {exc}", file=sys.stderr)
48
+ return 3
49
+
50
+ settings = AuthSettings()
51
+
52
+ # Select the keyring backend before any session read. On Windows the default
53
+ # `auto` backend cannot hold a Keycloak session (CredWrite caps the blob at
54
+ # ~2.5 KB), so machines set `keychain_backend = "file"` — and a reader that
55
+ # skips this looks in the empty store and reports "not signed in" while
56
+ # `pipefy auth status` says otherwise.
57
+ configure_keychain_backend(settings.keychain_backend)
58
+
59
+ service_account = None
60
+ if settings.service_account_client_id and settings.service_account_client_secret:
61
+ service_account = ServiceAccount(
62
+ token_url=settings.service_account_url,
63
+ client_id=settings.service_account_client_id,
64
+ client_secret=settings.service_account_client_secret,
65
+ )
66
+
67
+ oidc = OidcClient(issuer_url=settings.auth_url, client_id=settings.auth_client_id)
68
+
69
+ resolved = resolve_pipefy_auth(
70
+ static_token=settings.static_token,
71
+ service_account=service_account,
72
+ oidc_client=oidc,
73
+ )
74
+
75
+ if resolved is None:
76
+ return _fail(
77
+ "no-credentials",
78
+ "no Pipefy credential found: run `pipefy auth login`, set PIPEFY_TOKEN, "
79
+ "or configure PIPEFY_SERVICE_ACCOUNT_*",
80
+ )
81
+
82
+ common = {"ok": True, "base_url": settings.base_url}
83
+
84
+ if isinstance(resolved, StaticTokenAuth):
85
+ json.dump({**common, "source": "static-token", "token": resolved.token}, sys.stdout)
86
+ return 0
87
+
88
+ if isinstance(resolved, StoredSessionAuth):
89
+ session = ensure_fresh_session(
90
+ issuer=resolved.oidc_client.issuer_url,
91
+ client_id=resolved.oidc_client.client_id,
92
+ )
93
+ if session is None:
94
+ return _fail(
95
+ "session-expired",
96
+ "the stored session could not be refreshed — run `pipefy auth login` again",
97
+ )
98
+ json.dump(
99
+ {
100
+ **common,
101
+ "source": "stored-session",
102
+ "token": session.token.access_token,
103
+ "issuer": session.issuer,
104
+ "expires_in": session.token.expires_in,
105
+ },
106
+ sys.stdout,
107
+ )
108
+ return 0
109
+
110
+ if isinstance(resolved, ServiceAccountAuth):
111
+ # The client-credentials exchange happens here so the secret never
112
+ # crosses the process boundary — only the short-lived bearer does.
113
+ import httpx # noqa: PLC0415
114
+
115
+ creds = resolved.credentials
116
+ try:
117
+ response = httpx.post(
118
+ creds.token_url,
119
+ data={
120
+ "grant_type": "client_credentials",
121
+ "client_id": creds.client_id,
122
+ "client_secret": creds.client_secret,
123
+ },
124
+ timeout=30.0,
125
+ )
126
+ response.raise_for_status()
127
+ token = response.json()["access_token"]
128
+ except Exception as exc:
129
+ return _fail("service-account-failed", f"client-credentials grant failed: {exc}")
130
+ json.dump({**common, "source": "service-account", "token": token}, sys.stdout)
131
+ return 0
132
+
133
+ return _fail("unknown-method", f"unrecognised auth method {type(resolved).__name__}")
134
+
135
+
136
+ if __name__ == "__main__":
137
+ sys.exit(main())
@@ -0,0 +1,231 @@
1
+ import type { PayloadEnvelope, Row } from '../model/payload.ts';
2
+
3
+ /**
4
+ * The integration surface of a pipe, pulled out of the payload as its own
5
+ * artifact.
6
+ *
7
+ * "Integration" here means every way this pipe touches something outside
8
+ * itself, because those are the parts of a change whose blast radius leaves the
9
+ * pipe: a webhook fires at a third party, a pipe relation writes cards into
10
+ * another process, a cross-repo automation does both. They are also the parts
11
+ * that carry credentials, which is why the rendered summary is redacted and the
12
+ * raw rows go in a file the workspace .gitignore covers.
13
+ *
14
+ * Recorded per run rather than derived on demand: a run is the only moment when
15
+ * "what did this pipe reach, before and after" is knowable, and by the next
16
+ * apply the answer has changed.
17
+ */
18
+
19
+ /** Automation actions that reach outside the pipe they are triggered in. */
20
+ const OUTBOUND_ACTIONS = new Set([
21
+ 'http_request',
22
+ 'send_email',
23
+ 'send_email_template',
24
+ 'create_card',
25
+ 'create_card_relation',
26
+ 'move_single_card',
27
+ 'create_table_record',
28
+ 'update_table_record',
29
+ ]);
30
+
31
+ export type IntegrationEdge = {
32
+ kind: 'webhook' | 'pipe_relation' | 'automation' | 'connected_field' | 'email_inbox';
33
+ /** What this pipe reaches, named the way a human would say it. */
34
+ target: string;
35
+ /** The row's id, so it can be found again. */
36
+ id: string | null;
37
+ detail: string;
38
+ /** True when the row carries a credential, so the report redacts it. */
39
+ bearsSecret?: boolean;
40
+ };
41
+
42
+ export type IntegrationsView = {
43
+ generatedAt: string;
44
+ repoId: number;
45
+ repoName: string;
46
+ /**
47
+ * Coverage for the groups this view reads. `unknown` here means the list
48
+ * below is not "no integrations" but "not read" — the same doctrine as
49
+ * everywhere else, and it matters most here, where the absent row might be a
50
+ * live webhook.
51
+ */
52
+ coverage: Record<string, string>;
53
+ counts: { webhooks: number; pipe_relations: number; outbound_automations: number; connected_fields: number };
54
+ edges: IntegrationEdge[];
55
+ /** Every repo id this pipe is wired to, however it is wired. */
56
+ connected_repo_ids: number[];
57
+ /** Raw rows, unredacted — the workspace .gitignore covers this file. */
58
+ webhooks: Row[];
59
+ pipe_relations: Row[];
60
+ outbound_automations: Row[];
61
+ email_inboxes: Row[];
62
+ public_form: { open: boolean; title: unknown } | null;
63
+ };
64
+
65
+ const s = (v: unknown): string | null => (v === null || v === undefined ? null : String(v));
66
+
67
+ const hostOf = (url: unknown): string => {
68
+ const raw = s(url);
69
+ if (!raw) return '(no url)';
70
+ try {
71
+ return new URL(raw).host;
72
+ } catch {
73
+ return raw.slice(0, 40);
74
+ }
75
+ };
76
+
77
+ /** Whether a row holds anything bearer-equivalent. */
78
+ const bearsSecret = (row: Row): boolean => {
79
+ const j = JSON.stringify(row).toLowerCase();
80
+ return (
81
+ j.includes('authentication_value') ||
82
+ j.includes('authentication_key') ||
83
+ j.includes('token') ||
84
+ j.includes('secret') ||
85
+ /[?&](key|token|sig|signature|auth)=/.test(j)
86
+ );
87
+ };
88
+
89
+ export const integrationsFrom = (env: PayloadEnvelope): IntegrationsView => {
90
+ const rel = env.payload.relations;
91
+ const repoId = Number(env.payload.id);
92
+ const webhooks = rel.webhooks ?? [];
93
+ const relations = rel.pipe_relations ?? [];
94
+ const automations = rel.automations ?? [];
95
+ const inboxes = rel.email_inboxes ?? [];
96
+ const fields = rel.fields ?? [];
97
+
98
+ const outbound = automations.filter((a) => {
99
+ const action = s(a['action_id']) ?? '';
100
+ const target = s(a['action_repo_id']);
101
+ const crossRepo = target !== null && target !== s(a['event_repo_id']);
102
+ return crossRepo || OUTBOUND_ACTIONS.has(action);
103
+ });
104
+
105
+ const connectedFields = fields.filter((f) => s(f['connected_repo_id']) ?? s(f['connected_pipe_id']));
106
+
107
+ const edges: IntegrationEdge[] = [
108
+ ...webhooks.map(
109
+ (w): IntegrationEdge => ({
110
+ kind: 'webhook',
111
+ target: hostOf(w['url']),
112
+ id: s(w['id']),
113
+ detail:
114
+ `${s(w['name']) ?? 'unnamed'} on ` +
115
+ `${Array.isArray(w['actions']) ? (w['actions'] as unknown[]).join(', ') : (s(w['actions']) ?? 'no actions')}`,
116
+ bearsSecret: bearsSecret(w),
117
+ }),
118
+ ),
119
+ ...relations.map(
120
+ (r): IntegrationEdge => ({
121
+ kind: 'pipe_relation',
122
+ target: `repo ${s(r['child_id']) ?? s(r['parent_id']) ?? '?'}`,
123
+ id: s(r['id']),
124
+ detail: `${s(r['name']) ?? 'unnamed'} — parent ${s(r['parent_id'])} to child ${s(r['child_id'])}`,
125
+ }),
126
+ ),
127
+ ...outbound.map(
128
+ (a): IntegrationEdge => ({
129
+ kind: 'automation',
130
+ target:
131
+ s(a['action_repo_id']) && s(a['action_repo_id']) !== String(repoId)
132
+ ? `repo ${s(a['action_repo_id'])}`
133
+ : (s(a['action_id']) ?? 'action'),
134
+ id: s(a['id']),
135
+ detail:
136
+ `${s(a['name']) ?? 'unnamed'} — ${s(a['event_id'])} to ${s(a['action_id'])}` +
137
+ `${a['active'] === false ? ' (inactive)' : ''}`,
138
+ bearsSecret: bearsSecret(a),
139
+ }),
140
+ ),
141
+ ...connectedFields.map(
142
+ (f): IntegrationEdge => ({
143
+ kind: 'connected_field',
144
+ target: `repo ${s(f['connected_repo_id']) ?? s(f['connected_pipe_id'])}`,
145
+ id: s(f['id']),
146
+ // Payload rows spell the type `type_id`; a reconstructed row may spell it
147
+ // `type`. Neither is guaranteed, so both are tried before giving up.
148
+ detail: `field "${s(f['label']) ?? s(f['name']) ?? '?'}" (${s(f['type_id']) ?? s(f['type']) ?? 'unknown type'})`,
149
+ }),
150
+ ),
151
+ ...inboxes.map(
152
+ (i): IntegrationEdge => ({
153
+ kind: 'email_inbox',
154
+ target: s(i['email']) ?? s(i['name']) ?? 'inbox',
155
+ id: s(i['id']),
156
+ detail: 'inbound email',
157
+ }),
158
+ ),
159
+ ];
160
+
161
+ const connected = new Set<number>();
162
+ const note = (v: unknown) => {
163
+ const n = Number(v);
164
+ if (Number.isFinite(n) && n !== repoId && n !== 0) connected.add(n);
165
+ };
166
+ for (const r of relations) {
167
+ note(r['parent_id']);
168
+ note(r['child_id']);
169
+ }
170
+ for (const a of outbound) note(a['action_repo_id']);
171
+ for (const f of connectedFields) note(f['connected_repo_id'] ?? f['connected_pipe_id']);
172
+
173
+ const form = (rel.public_forms ?? [])[0] ?? null;
174
+
175
+ return {
176
+ generatedAt: new Date().toISOString(),
177
+ repoId,
178
+ repoName: String(env.payload['name'] ?? repoId),
179
+ coverage: {
180
+ webhooks: env.coverage.webhooks,
181
+ pipe_relations: env.coverage.pipe_relations,
182
+ automations: env.coverage.automations,
183
+ email_inboxes: env.coverage.email_inboxes,
184
+ },
185
+ counts: {
186
+ webhooks: webhooks.length,
187
+ pipe_relations: relations.length,
188
+ outbound_automations: outbound.length,
189
+ connected_fields: connectedFields.length,
190
+ },
191
+ edges,
192
+ connected_repo_ids: [...connected].sort((a, b) => a - b),
193
+ webhooks,
194
+ pipe_relations: relations,
195
+ outbound_automations: outbound,
196
+ email_inboxes: inboxes,
197
+ public_form: form ? { open: Boolean(env.payload['public']), title: form['title'] ?? null } : null,
198
+ };
199
+ };
200
+
201
+ /** The integration surface as report prose. Hosts kept, credentials never. */
202
+ export const renderIntegrations = (v: IntegrationsView): string[] => {
203
+ const lines: string[] = [];
204
+ const gaps = Object.entries(v.coverage).filter(([, c]) => c !== 'complete');
205
+ if (gaps.length) {
206
+ lines.push(
207
+ `> Coverage: ${gaps.map(([k, c]) => `${k} is ${c}`).join(', ')}. ` +
208
+ 'An absent row below is not evidence that nothing is there.',
209
+ '',
210
+ );
211
+ }
212
+ if (!v.edges.length) {
213
+ lines.push('This pipe reaches nothing outside itself.');
214
+ return lines;
215
+ }
216
+ lines.push('| Kind | Target | Id | Detail |', '| --- | --- | --- | --- |');
217
+ for (const e of v.edges) {
218
+ lines.push(
219
+ `| ${e.kind.replace(/_/g, ' ')} | ${e.target}${e.bearsSecret ? ' (key)' : ''} | ${e.id ?? '-'} | ` +
220
+ `${e.detail.replace(/\|/g, '\\|')} |`,
221
+ );
222
+ }
223
+ if (v.edges.some((e) => e.bearsSecret)) {
224
+ lines.push(
225
+ '',
226
+ '(key) marks a row that carries a credential. The value is in `integrations.json`, ' +
227
+ 'which the workspace .gitignore covers.',
228
+ );
229
+ }
230
+ return lines;
231
+ };