@pipefy/pipefy-process-coder 0.1.1 → 0.1.3
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.md +33 -2
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +6 -2
- package/dist/apply/adopt.js +52 -0
- package/dist/cli.js +14 -5
- package/dist/codec/pack.js +2 -0
- package/dist/codec/unpack.js +1 -0
- package/dist/commands/apply.js +25 -6
- package/dist/commands/context.js +40 -2
- package/dist/commands/diff.js +22 -20
- package/dist/commands/misc.js +55 -34
- package/dist/commands/plan.js +3 -1
- package/dist/commands/pull.js +54 -6
- package/dist/commands/verify.js +11 -32
- package/dist/diff/diff.js +28 -4
- package/dist/pipefy/agents.js +36 -27
- package/dist/pipefy/internal.js +201 -92
- package/dist/pipefy/ipaas.js +4 -3
- package/dist/pipefy/reconstruct.js +147 -39
- package/dist/pipefy/toolkit.js +27 -11
- package/dist/pipefy/toolkit_bearer.py +60 -14
- package/dist/report/run.js +2 -1
- package/dist/util/log.js +14 -4
- package/dist/util/tui.js +43 -2
- package/dist/version.js +18 -0
- package/dist/workspace/docs.js +1 -1
- package/dist/workspace/read.js +1 -0
- package/package.json +4 -3
package/dist/pipefy/toolkit.js
CHANGED
|
@@ -95,41 +95,56 @@ export const inspectToolkit = async () => {
|
|
|
95
95
|
cachedInterpreter ??= await findInterpreter();
|
|
96
96
|
if (!cachedInterpreter)
|
|
97
97
|
return { installed: false };
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
98
|
+
const run = await runHelper();
|
|
99
|
+
if (!run)
|
|
100
100
|
return { installed: false };
|
|
101
|
+
if ('error' in run) {
|
|
102
|
+
return { installed: true, usable: false, cliPath: toolkitBinary('pipefy'), interpreter: cachedInterpreter.command, error: run.error };
|
|
103
|
+
}
|
|
101
104
|
return {
|
|
102
105
|
installed: true,
|
|
106
|
+
usable: true,
|
|
103
107
|
cliPath: toolkitBinary('pipefy'),
|
|
104
108
|
mcpServerPath: toolkitBinary('pipefy-mcp-server'),
|
|
105
109
|
interpreter: cachedInterpreter.command,
|
|
106
|
-
credential,
|
|
110
|
+
credential: run.result,
|
|
107
111
|
};
|
|
108
112
|
};
|
|
109
113
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
114
|
+
* Run the helper. `null` means there is nothing to run it with; `{ error }`
|
|
115
|
+
* means it ran and could not use the toolkit, carrying its own explanation
|
|
116
|
+
* (the helper prints which `pipefy_auth` version it found and what it lacks).
|
|
113
117
|
*/
|
|
114
|
-
|
|
118
|
+
const runHelper = async () => {
|
|
115
119
|
cachedInterpreter ??= await findInterpreter();
|
|
116
120
|
if (!cachedInterpreter)
|
|
117
121
|
return null;
|
|
118
122
|
if (!existsSync(HELPER)) {
|
|
119
123
|
debug(`toolkit helper missing at ${HELPER}`);
|
|
120
|
-
return
|
|
124
|
+
return { error: `this install of pipe is missing its toolkit helper (${HELPER})` };
|
|
121
125
|
}
|
|
122
126
|
try {
|
|
123
127
|
const { stdout } = await exec(cachedInterpreter.command, [...cachedInterpreter.args, HELPER], { timeout: 60_000, maxBuffer: 1024 * 1024 });
|
|
124
128
|
const parsed = JSON.parse(stdout.trim());
|
|
125
129
|
debug(`toolkit credential: ${parsed.ok ? parsed.source : parsed.reason}`);
|
|
126
|
-
return parsed;
|
|
130
|
+
return { result: parsed };
|
|
127
131
|
}
|
|
128
132
|
catch (e) {
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
const err = e;
|
|
134
|
+
debug(`toolkit bearer helper failed: ${err.message}`);
|
|
135
|
+
const said = err.stderr?.trim().split(/\r?\n/).filter(Boolean).pop();
|
|
136
|
+
return { error: said || err.message.split(/\r?\n/)[0] || 'the toolkit helper failed' };
|
|
131
137
|
}
|
|
132
138
|
};
|
|
139
|
+
/**
|
|
140
|
+
* Ask the toolkit for a bearer. `null` means the toolkit could not be used at
|
|
141
|
+
* all — distinct from a `{ ok: false }` result, which is the toolkit answering
|
|
142
|
+
* that it holds no credential.
|
|
143
|
+
*/
|
|
144
|
+
export const readBearer = async () => {
|
|
145
|
+
const run = await runHelper();
|
|
146
|
+
return run && 'result' in run ? run.result : null;
|
|
147
|
+
};
|
|
133
148
|
/** Run `pipefy auth login`, inheriting the terminal so the browser flow works. */
|
|
134
149
|
export const runToolkitLogin = async (extraArgs = []) => {
|
|
135
150
|
const cli = toolkitBinary('pipefy');
|
|
@@ -155,4 +170,5 @@ export const toolkitAuthStatus = async () => {
|
|
|
155
170
|
return null;
|
|
156
171
|
}
|
|
157
172
|
};
|
|
173
|
+
export const UPGRADE_HINT = 'reinstall the toolkit: `uv tool install --force --reinstall pipefy-cli` (any pipefy-auth from 0.3.0a1 to 0.5.4b1 on PyPI works)';
|
|
158
174
|
export const INSTALL_HINT = 'install the Pipefy AI toolkit (github.com/pipefy/ai-toolkit): `uv tool install pipefy-cli`, then `pipefy auth login`';
|
|
@@ -31,22 +31,63 @@ def _fail(reason: str, message: str) -> int:
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def main() -> int:
|
|
34
|
+
# Import the module, then look the names up one by one, so an incompatible
|
|
35
|
+
# install says which names it lacks and which version it is — rather than
|
|
36
|
+
# one ImportError that left `pipe doctor` reporting the toolkit as "not
|
|
37
|
+
# installed" on a machine where it plainly was. Every pipefy-auth release on
|
|
38
|
+
# PyPI (0.3.0a1 through 0.5.4b1) exports all of these; a field report of
|
|
39
|
+
# one that lacked ServiceAccountAuth came from an install none of them
|
|
40
|
+
# explain, so this names what it found instead of guessing why.
|
|
34
41
|
try:
|
|
35
|
-
|
|
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
|
|
42
|
+
import pipefy_auth # noqa: PLC0415
|
|
46
43
|
except Exception as exc: # pragma: no cover - environment problem
|
|
47
44
|
print(f"pipefy_auth is not importable: {exc}", file=sys.stderr)
|
|
48
45
|
return 3
|
|
49
46
|
|
|
47
|
+
version = getattr(pipefy_auth, "__version__", None)
|
|
48
|
+
if version is None:
|
|
49
|
+
try:
|
|
50
|
+
from importlib.metadata import version as dist_version # noqa: PLC0415
|
|
51
|
+
|
|
52
|
+
version = dist_version("pipefy-auth")
|
|
53
|
+
except Exception:
|
|
54
|
+
version = "unknown"
|
|
55
|
+
where = getattr(pipefy_auth, "__file__", "?")
|
|
56
|
+
|
|
57
|
+
# Required for every credential this helper can return.
|
|
58
|
+
required = (
|
|
59
|
+
"AuthSettings",
|
|
60
|
+
"OidcClient",
|
|
61
|
+
"StaticTokenAuth",
|
|
62
|
+
"StoredSessionAuth",
|
|
63
|
+
"ensure_fresh_session",
|
|
64
|
+
"resolve_pipefy_auth",
|
|
65
|
+
)
|
|
66
|
+
missing = [n for n in required if not hasattr(pipefy_auth, n)]
|
|
67
|
+
if missing:
|
|
68
|
+
print(
|
|
69
|
+
f"pipefy_auth {version} at {where} is incompatible: it lacks {', '.join(missing)}",
|
|
70
|
+
file=sys.stderr,
|
|
71
|
+
)
|
|
72
|
+
return 3
|
|
73
|
+
|
|
74
|
+
AuthSettings = pipefy_auth.AuthSettings
|
|
75
|
+
OidcClient = pipefy_auth.OidcClient
|
|
76
|
+
StaticTokenAuth = pipefy_auth.StaticTokenAuth
|
|
77
|
+
StoredSessionAuth = pipefy_auth.StoredSessionAuth
|
|
78
|
+
ensure_fresh_session = pipefy_auth.ensure_fresh_session
|
|
79
|
+
resolve_pipefy_auth = pipefy_auth.resolve_pipefy_auth
|
|
80
|
+
# Only a service-account credential needs these, so their absence costs
|
|
81
|
+
# that one method, not the static token or the browser session.
|
|
82
|
+
ServiceAccountAuth = getattr(pipefy_auth, "ServiceAccountAuth", None)
|
|
83
|
+
ServiceAccount = getattr(pipefy_auth, "ServiceAccount", None)
|
|
84
|
+
if ServiceAccount is None:
|
|
85
|
+
try:
|
|
86
|
+
from pipefy_auth.resolver import ServiceAccount # noqa: PLC0415
|
|
87
|
+
except Exception:
|
|
88
|
+
ServiceAccount = None
|
|
89
|
+
configure_keychain_backend = getattr(pipefy_auth, "configure_keychain_backend", None)
|
|
90
|
+
|
|
50
91
|
settings = AuthSettings()
|
|
51
92
|
|
|
52
93
|
# Select the keyring backend before any session read. On Windows the default
|
|
@@ -54,10 +95,15 @@ def main() -> int:
|
|
|
54
95
|
# ~2.5 KB), so machines set `keychain_backend = "file"` — and a reader that
|
|
55
96
|
# skips this looks in the empty store and reports "not signed in" while
|
|
56
97
|
# `pipefy auth status` says otherwise.
|
|
57
|
-
configure_keychain_backend
|
|
98
|
+
if configure_keychain_backend is not None:
|
|
99
|
+
configure_keychain_backend(settings.keychain_backend)
|
|
58
100
|
|
|
59
101
|
service_account = None
|
|
60
|
-
if
|
|
102
|
+
if (
|
|
103
|
+
ServiceAccount is not None
|
|
104
|
+
and settings.service_account_client_id
|
|
105
|
+
and settings.service_account_client_secret
|
|
106
|
+
):
|
|
61
107
|
service_account = ServiceAccount(
|
|
62
108
|
token_url=settings.service_account_url,
|
|
63
109
|
client_id=settings.service_account_client_id,
|
|
@@ -107,7 +153,7 @@ def main() -> int:
|
|
|
107
153
|
)
|
|
108
154
|
return 0
|
|
109
155
|
|
|
110
|
-
if isinstance(resolved, ServiceAccountAuth):
|
|
156
|
+
if ServiceAccountAuth is not None and isinstance(resolved, ServiceAccountAuth):
|
|
111
157
|
# The client-credentials exchange happens here so the secret never
|
|
112
158
|
# crosses the process boundary — only the short-lived bearer does.
|
|
113
159
|
import httpx # noqa: PLC0415
|
package/dist/report/run.js
CHANGED
|
@@ -3,6 +3,7 @@ import { promises as fs } from 'node:fs';
|
|
|
3
3
|
import { mkdirp, writeFile, writeJson, exists } from "../util/fsx.js";
|
|
4
4
|
import { paths } from "../workspace/layout.js";
|
|
5
5
|
import { integrationsFrom, renderIntegrations } from "./integrations.js";
|
|
6
|
+
import { TOOL } from "../version.js";
|
|
6
7
|
const stamp = (d = new Date()) => d.toISOString().replace(/[:.]/g, '-').replace(/-\d{3}Z$/, 'Z');
|
|
7
8
|
/**
|
|
8
9
|
* The command as typed, for reproduction — with any token elided. `--token` is
|
|
@@ -105,7 +106,7 @@ export class RunRecorder {
|
|
|
105
106
|
id: this.id,
|
|
106
107
|
kind: this.kind,
|
|
107
108
|
command: this.ctx.command,
|
|
108
|
-
tool: {
|
|
109
|
+
tool: { ...TOOL },
|
|
109
110
|
startedAt: this.startedAt.toISOString(),
|
|
110
111
|
finishedAt: finished.toISOString(),
|
|
111
112
|
durationMs: finished.getTime() - this.startedAt.getTime(),
|
package/dist/util/log.js
CHANGED
|
@@ -51,13 +51,23 @@ export const step = (s) => {
|
|
|
51
51
|
process.stderr.write(blue('> ') + s + '\n');
|
|
52
52
|
};
|
|
53
53
|
const CLEAR_LINE = `\r${ESC}2K`;
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Single-line progress that collapses to nothing on a non-tty.
|
|
56
|
+
*
|
|
57
|
+
* Dropped while a display holds the screen, like `step`: it wrote beneath the
|
|
58
|
+
* live frame without the frame knowing, and a long automation name wrapped to
|
|
59
|
+
* a second row, so the next repaint landed a row short. Clipped to the
|
|
60
|
+
* terminal otherwise, because a `\r` rewrite only reaches back one row too.
|
|
61
|
+
*/
|
|
55
62
|
export const progress = (s) => {
|
|
56
|
-
if (!process.stderr.isTTY)
|
|
63
|
+
if (!process.stderr.isTTY || sink)
|
|
57
64
|
return;
|
|
58
|
-
|
|
65
|
+
const line = ' ' + s;
|
|
66
|
+
const width = process.stderr.columns;
|
|
67
|
+
const cut = width && [...line].length > width - 1 ? [...line].slice(0, width - 2).join('') + '…' : line;
|
|
68
|
+
process.stderr.write(CLEAR_LINE + dim(cut));
|
|
59
69
|
};
|
|
60
|
-
export const progressDone = () => { if (process.stderr.isTTY)
|
|
70
|
+
export const progressDone = () => { if (process.stderr.isTTY && !sink)
|
|
61
71
|
process.stderr.write(CLEAR_LINE); };
|
|
62
72
|
export class UserError extends Error {
|
|
63
73
|
hint;
|
package/dist/util/tui.js
CHANGED
|
@@ -17,6 +17,42 @@ const HIDE_CURSOR = `${ESC}?25l`;
|
|
|
17
17
|
const SHOW_CURSOR = `${ESC}?25h`;
|
|
18
18
|
const CLEAR_LINE = `${ESC}2K`;
|
|
19
19
|
const cursorUp = (n) => (n > 0 ? `${ESC}${n}A` : '');
|
|
20
|
+
/**
|
|
21
|
+
* Cut a line to `max` visible columns, keeping its colour codes intact.
|
|
22
|
+
*
|
|
23
|
+
* The repaint moves up as many rows as it drew last time, counting one row per
|
|
24
|
+
* line. A line wider than the terminal wraps onto two, the cursor then lands
|
|
25
|
+
* one row short, and every frame leaves its top line behind — on a narrow
|
|
26
|
+
* PowerShell window a pull scrolled hundreds of copies of its header and cut
|
|
27
|
+
* the repo line off mid-word. Clipping is what keeps the count true. Every
|
|
28
|
+
* code point is taken as one column, which holds for everything this tool
|
|
29
|
+
* prints (the spinner, the dashes, Portuguese pipe names).
|
|
30
|
+
*/
|
|
31
|
+
const ANSI = /\x1b\[[0-9;?]*[A-Za-z]/y;
|
|
32
|
+
export const clipToWidth = (line, max) => {
|
|
33
|
+
if (max <= 0)
|
|
34
|
+
return '';
|
|
35
|
+
if ([...line.replace(new RegExp(ANSI.source, 'g'), '')].length <= max)
|
|
36
|
+
return line;
|
|
37
|
+
let out = '';
|
|
38
|
+
let visible = 0;
|
|
39
|
+
for (let i = 0; i < line.length;) {
|
|
40
|
+
ANSI.lastIndex = i;
|
|
41
|
+
const m = ANSI.exec(line);
|
|
42
|
+
if (m) {
|
|
43
|
+
out += m[0];
|
|
44
|
+
i += m[0].length;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (visible === max - 1)
|
|
48
|
+
break;
|
|
49
|
+
const ch = String.fromCodePoint(line.codePointAt(i));
|
|
50
|
+
out += ch;
|
|
51
|
+
visible++;
|
|
52
|
+
i += ch.length;
|
|
53
|
+
}
|
|
54
|
+
return `${out}…${ESC}0m`;
|
|
55
|
+
};
|
|
20
56
|
/** Braille spinner: 8 frames, reads as motion even at 80ms. */
|
|
21
57
|
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'];
|
|
22
58
|
const MARK = {
|
|
@@ -256,8 +292,13 @@ export class Progress {
|
|
|
256
292
|
...(hiddenBelow > 0 ? [dim(` … ${hiddenBelow} more`)] : []),
|
|
257
293
|
...this.notes.map((n) => ` ${n}`),
|
|
258
294
|
];
|
|
259
|
-
// Repaint in place: up N lines, then clear-and-write each one.
|
|
260
|
-
|
|
295
|
+
// Repaint in place: up N lines, then clear-and-write each one. Each line is
|
|
296
|
+
// clipped to the terminal so it occupies exactly one row (see clipToWidth);
|
|
297
|
+
// the final frame is not, since nothing repaints over it and a warning
|
|
298
|
+
// should be readable in full.
|
|
299
|
+
const width = this.stream.columns;
|
|
300
|
+
const fit = (l) => (final || !width ? l : clipToWidth(l, width - 1));
|
|
301
|
+
const out = cursorUp(this.linesDrawn) + body.map((l) => CLEAR_LINE + fit(l)).join('\n') + '\n';
|
|
261
302
|
this.stream.write(out);
|
|
262
303
|
this.linesDrawn = final ? 0 : body.length;
|
|
263
304
|
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
/**
|
|
5
|
+
* The tool's identity as recorded in workspace.json and every run.json.
|
|
6
|
+
*
|
|
7
|
+
* The version is read from package.json, the same file `pipe --version` reads,
|
|
8
|
+
* because it used to be a literal `'0.1.0'` in three places that a release did
|
|
9
|
+
* not touch — so 0.1.1 stamped 0.1.0 on everything it wrote, and the one field
|
|
10
|
+
* meant to say which build produced a workspace said the wrong one.
|
|
11
|
+
*
|
|
12
|
+
* `dist/` mirrors `src/` one-for-one, so `../package.json` from here is the
|
|
13
|
+
* package root from either tree.
|
|
14
|
+
*/
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
|
|
16
|
+
export const PACKAGE_NAME = pkg.name;
|
|
17
|
+
export const TOOL_VERSION = pkg.version;
|
|
18
|
+
export const TOOL = { name: 'pipefy-process-coder', version: TOOL_VERSION };
|
package/dist/workspace/docs.js
CHANGED
|
@@ -280,7 +280,7 @@ const CAPABILITIES_MD = (caps) => {
|
|
|
280
280
|
| \`importRepoSnapshot\` | ${caps.importRepoSnapshot ? 'yes' : 'no'} | Route B is unavailable; writes go through the mutation compiler |
|
|
281
281
|
| internal API | ${caps.internalApi} | automation detail and phase-jump editing depend on it |
|
|
282
282
|
|
|
283
|
-
${caps.internalApi === 'unavailable' ? '> The internal endpoint did not answer. Automations and phase jumps are reported as **unknown** rather than empty, and any change to them is refused rather than silently skipped.\n' : ''}`
|
|
283
|
+
${caps.internalApi === 'unavailable' ? '> The internal endpoint did not answer. Automations and phase jumps are reported as **unknown** rather than empty, and any change to them is refused rather than silently skipped.\n' : ''}${caps.internalApi === 'incompatible' ? '> The internal endpoint refused one of this tool\'s own queries as invalid — a bug in `pipe`, not an outage. Automations are reported as **unknown** or **partial** rather than empty, and any change to them is refused. `pipe doctor` names the query.\n' : ''}`
|
|
284
284
|
: '';
|
|
285
285
|
return `# Capabilities
|
|
286
286
|
|
package/dist/workspace/read.js
CHANGED
|
@@ -146,6 +146,7 @@ export const readTree = async (dir) => {
|
|
|
146
146
|
readAt: meta.readAt ?? new Date().toISOString(),
|
|
147
147
|
repoKind: meta.repoKind ?? 'pipe',
|
|
148
148
|
coverage: meta.coverage ?? fullCoverage('unknown'),
|
|
149
|
+
...(meta.columns ? { columns: meta.columns } : {}),
|
|
149
150
|
repo_columns: meta.repo_columns ?? 'complete',
|
|
150
151
|
notes: meta.notes ?? [],
|
|
151
152
|
},
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipefy/pipefy-process-coder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Pull a Pipefy pipe to disk as readable JSON, edit it with an agent, diff it, push it back.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/Guilherme-CA-Dias/pipefy-process-coder.git"
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"bin",
|
|
16
16
|
"dist",
|
|
17
17
|
".agents/skills",
|
|
18
|
-
"AGENTS.md"
|
|
18
|
+
"AGENTS.md",
|
|
19
|
+
"NOTICE"
|
|
19
20
|
],
|
|
20
21
|
"engines": {
|
|
21
22
|
"node": ">=22.18"
|