@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.
@@ -95,41 +95,56 @@ export const inspectToolkit = async () => {
95
95
  cachedInterpreter ??= await findInterpreter();
96
96
  if (!cachedInterpreter)
97
97
  return { installed: false };
98
- const credential = await readBearer();
99
- if (!credential)
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
- * Ask the toolkit for a bearer. `null` means the toolkit could not be used at
111
- * all — distinct from a `{ ok: false }` result, which is the toolkit answering
112
- * that it holds no credential.
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
- export const readBearer = async () => {
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 null;
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
- debug(`toolkit bearer helper failed: ${e.message}`);
130
- return null;
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
- 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
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(settings.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 settings.service_account_client_id and settings.service_account_client_secret:
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
@@ -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: { name: 'pipefy-process-coder', version: '0.1.0' },
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
- /** Single-line progress that collapses to nothing on a non-tty. */
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
- process.stderr.write(CLEAR_LINE + dim(' ' + s));
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
- const out = cursorUp(this.linesDrawn) + body.map((l) => CLEAR_LINE + l).join('\n') + '\n';
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
  }
@@ -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 };
@@ -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
 
@@ -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.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": "UNLICENSED",
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"