pipefy-cli 0.3.0a1__py3-none-any.whl

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 (40) hide show
  1. pipefy_cli/__init__.py +7 -0
  2. pipefy_cli/_docs.py +3 -0
  3. pipefy_cli/auth.py +225 -0
  4. pipefy_cli/commands/__init__.py +5 -0
  5. pipefy_cli/commands/_common.py +359 -0
  6. pipefy_cli/commands/agent.py +358 -0
  7. pipefy_cli/commands/ai_automation.py +318 -0
  8. pipefy_cli/commands/attachment.py +122 -0
  9. pipefy_cli/commands/audit.py +63 -0
  10. pipefy_cli/commands/auth.py +519 -0
  11. pipefy_cli/commands/automation.py +529 -0
  12. pipefy_cli/commands/card.py +439 -0
  13. pipefy_cli/commands/email.py +169 -0
  14. pipefy_cli/commands/export.py +61 -0
  15. pipefy_cli/commands/field.py +113 -0
  16. pipefy_cli/commands/field_condition.py +201 -0
  17. pipefy_cli/commands/graphql.py +110 -0
  18. pipefy_cli/commands/introspect.py +110 -0
  19. pipefy_cli/commands/label.py +109 -0
  20. pipefy_cli/commands/member.py +126 -0
  21. pipefy_cli/commands/org.py +60 -0
  22. pipefy_cli/commands/phase.py +264 -0
  23. pipefy_cli/commands/pipe.py +167 -0
  24. pipefy_cli/commands/portal.py +871 -0
  25. pipefy_cli/commands/record.py +219 -0
  26. pipefy_cli/commands/relation.py +173 -0
  27. pipefy_cli/commands/report_org.py +241 -0
  28. pipefy_cli/commands/report_pipe.py +290 -0
  29. pipefy_cli/commands/table.py +151 -0
  30. pipefy_cli/commands/usage.py +100 -0
  31. pipefy_cli/commands/webhook.py +141 -0
  32. pipefy_cli/main.py +132 -0
  33. pipefy_cli/output/__init__.py +8 -0
  34. pipefy_cli/output/json_renderer.py +24 -0
  35. pipefy_cli/output/rich_renderer.py +81 -0
  36. pipefy_cli/settings.py +66 -0
  37. pipefy_cli-0.3.0a1.dist-info/METADATA +104 -0
  38. pipefy_cli-0.3.0a1.dist-info/RECORD +40 -0
  39. pipefy_cli-0.3.0a1.dist-info/WHEEL +4 -0
  40. pipefy_cli-0.3.0a1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,358 @@
1
+ """AI Agent operations (list, CRUD, logs, behavior validation)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+ from pipefy_sdk import (
9
+ CreateAiAgentInput,
10
+ PipefyClient,
11
+ UpdateAiAgentInput,
12
+ )
13
+ from pipefy_sdk.ai_preflight import validate_ai_agent_behaviors_sdk
14
+ from pipefy_sdk.behavior_placeholders import (
15
+ expand_behaviors_placeholders,
16
+ normalize_pipefy_ai_instruction_tokens,
17
+ )
18
+ from pydantic import ValidationError
19
+
20
+ from pipefy_cli.commands._common import (
21
+ ID_POSITIONAL_CONTEXT_SETTINGS,
22
+ confirm_destructive,
23
+ parse_json_value,
24
+ resource_id_argument,
25
+ run_cli_command,
26
+ )
27
+
28
+ agent_app = typer.Typer(help="AI Agents (repo-scoped).", no_args_is_help=True)
29
+ logs_app = typer.Typer(help="AI Agent execution logs.", no_args_is_help=True)
30
+ agent_app.add_typer(logs_app, name="logs")
31
+
32
+
33
+ def _parse_behaviors_json(raw: str) -> list[dict[str, Any]]:
34
+ parsed = parse_json_value(raw, "--behaviors")
35
+ if not isinstance(parsed, list):
36
+ raise typer.BadParameter("--behaviors must be a JSON array")
37
+ out = [b for b in parsed if isinstance(b, dict)]
38
+ if not out:
39
+ raise typer.BadParameter("--behaviors must contain at least one object")
40
+ return out
41
+
42
+
43
+ def _raise_if_preflight_blocks(payload: dict[str, Any]) -> None:
44
+ if not payload.get("success"):
45
+ msg = payload.get("error") or payload.get("message") or "Validation failed"
46
+ raise typer.BadParameter(str(msg))
47
+ if not payload.get("valid"):
48
+ problems = payload.get("problems") or []
49
+ text = "\n".join(f" - {p}" for p in problems) if problems else "(no details)"
50
+ raise typer.BadParameter(f"validate-behaviors failed:\n{text}")
51
+
52
+
53
+ @agent_app.command("list")
54
+ def agent_list(
55
+ ctx: typer.Context,
56
+ repo: str = typer.Option(..., "--repo", help="Pipe UUID (repoUuid)."),
57
+ json_out: bool = typer.Option(
58
+ False, "--json", "-j", help="Print machine-readable JSON to stdout."
59
+ ),
60
+ ) -> None:
61
+ """List AI agents for a pipe (``get_ai_agents``)."""
62
+
63
+ async def factory(client: PipefyClient):
64
+ agents = await client.get_ai_agents(repo)
65
+ return {"success": True, "agents": agents}
66
+
67
+ run_cli_command(ctx, json_out, factory)
68
+
69
+
70
+ @agent_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
71
+ def agent_get(
72
+ ctx: typer.Context,
73
+ uuid: str = resource_id_argument(help="Agent UUID."),
74
+ json_out: bool = typer.Option(
75
+ False, "--json", "-j", help="Print machine-readable JSON to stdout."
76
+ ),
77
+ ) -> None:
78
+ """Fetch one AI agent (``get_ai_agent``)."""
79
+
80
+ async def factory(client: PipefyClient):
81
+ agent = await client.get_ai_agent(uuid)
82
+ return {"success": True, "agent": agent}
83
+
84
+ run_cli_command(ctx, json_out, factory)
85
+
86
+
87
+ @agent_app.command("create")
88
+ def agent_create(
89
+ ctx: typer.Context,
90
+ repo_uuid: str = typer.Option(..., "--repo-uuid", help="Pipe UUID."),
91
+ name: str = typer.Option(..., "--name", "-n", help="Agent display name."),
92
+ instruction: str = typer.Option(
93
+ ...,
94
+ "--instruction",
95
+ help="Agent-level instruction (token-normalized).",
96
+ ),
97
+ behaviors: str = typer.Option(
98
+ ...,
99
+ "--behaviors",
100
+ help="JSON array of behavior objects.",
101
+ ),
102
+ pipe: str = typer.Option(
103
+ ...,
104
+ "--pipe",
105
+ help="Numeric pipe id for validate-behaviors pre-flight.",
106
+ ),
107
+ data_sources: str | None = typer.Option(
108
+ None,
109
+ "--data-sources",
110
+ help="Optional JSON array of knowledge-source id strings.",
111
+ ),
112
+ strict_unknown: bool = typer.Option(
113
+ True,
114
+ "--strict/--no-strict",
115
+ help=(
116
+ "Pre-flight strictness: when --strict (default), unknown actionType values "
117
+ "block; with --no-strict they become warnings only."
118
+ ),
119
+ ),
120
+ json_out: bool = typer.Option(
121
+ False, "--json", "-j", help="Print machine-readable JSON to stdout."
122
+ ),
123
+ ) -> None:
124
+ """Create and configure an AI agent (``create_ai_agent`` + ``update_ai_agent`` chain)."""
125
+ behavior_list = _parse_behaviors_json(behaviors)
126
+ ds_raw = parse_json_value(data_sources, "--data-sources") if data_sources else None
127
+ data_source_ids: list[str] = []
128
+ if ds_raw is not None:
129
+ if not isinstance(ds_raw, list) or not all(isinstance(x, str) for x in ds_raw):
130
+ raise typer.BadParameter("--data-sources must be a JSON array of strings")
131
+ data_source_ids = list(ds_raw)
132
+
133
+ inst = normalize_pipefy_ai_instruction_tokens(instruction.strip())
134
+ try:
135
+ expanded = expand_behaviors_placeholders(behavior_list)
136
+ validated = CreateAiAgentInput(
137
+ name=name.strip(),
138
+ repo_uuid=repo_uuid.strip(),
139
+ instruction=inst,
140
+ behaviors=expanded,
141
+ data_source_ids=data_source_ids,
142
+ )
143
+ except ValidationError as exc:
144
+ raise typer.BadParameter(str(exc)) from exc
145
+
146
+ async def factory(client: PipefyClient):
147
+ pre = await validate_ai_agent_behaviors_sdk(
148
+ client,
149
+ pipe.strip(),
150
+ [b.model_dump(by_alias=True) for b in validated.behaviors],
151
+ strict_unknown_action_types=strict_unknown,
152
+ )
153
+ _raise_if_preflight_blocks(pre)
154
+ create_result = await client.create_ai_agent(validated)
155
+ agent_uuid = create_result["agent_uuid"]
156
+ update_input = UpdateAiAgentInput(
157
+ uuid=agent_uuid,
158
+ name=validated.name,
159
+ repo_uuid=validated.repo_uuid,
160
+ instruction=validated.instruction,
161
+ behaviors=validated.behaviors,
162
+ data_source_ids=validated.data_source_ids,
163
+ )
164
+ await client.update_ai_agent(update_input)
165
+ out: dict[str, Any] = {
166
+ "success": True,
167
+ "agent_uuid": agent_uuid,
168
+ "message": f"Created agent {agent_uuid}",
169
+ }
170
+ if pre.get("warnings"):
171
+ out["preflight"] = pre
172
+ return out
173
+
174
+ run_cli_command(ctx, json_out, factory)
175
+
176
+
177
+ @agent_app.command("update")
178
+ def agent_update(
179
+ ctx: typer.Context,
180
+ uuid: str = typer.Option(..., "--uuid", help="Agent UUID."),
181
+ name: str = typer.Option(..., "--name", "-n", help="Agent display name."),
182
+ repo_uuid: str = typer.Option(..., "--repo-uuid", help="Pipe UUID."),
183
+ instruction: str = typer.Option(
184
+ ..., "--instruction", help="Agent-level instruction."
185
+ ),
186
+ behaviors: str = typer.Option(
187
+ ..., "--behaviors", help="JSON array of behavior objects."
188
+ ),
189
+ pipe: str = typer.Option(
190
+ ...,
191
+ "--pipe",
192
+ help="Numeric pipe id for validate-behaviors pre-flight.",
193
+ ),
194
+ data_sources: str | None = typer.Option(
195
+ None,
196
+ "--data-sources",
197
+ help="Optional JSON array of knowledge-source id strings.",
198
+ ),
199
+ strict_unknown: bool = typer.Option(
200
+ True,
201
+ "--strict/--no-strict",
202
+ help=(
203
+ "Pre-flight strictness: --strict (default) blocks on unknown actionType "
204
+ "values; --no-strict converts them to warnings."
205
+ ),
206
+ ),
207
+ json_out: bool = typer.Option(False, "--json", "-j"),
208
+ ) -> None:
209
+ """Replace AI agent configuration (``update_ai_agent``)."""
210
+ behavior_list = _parse_behaviors_json(behaviors)
211
+ ds_raw = parse_json_value(data_sources, "--data-sources") if data_sources else None
212
+ data_source_ids: list[str] = []
213
+ if ds_raw is not None:
214
+ if not isinstance(ds_raw, list) or not all(isinstance(x, str) for x in ds_raw):
215
+ raise typer.BadParameter("--data-sources must be a JSON array of strings")
216
+ data_source_ids = list(ds_raw)
217
+
218
+ inst = normalize_pipefy_ai_instruction_tokens(instruction.strip())
219
+ try:
220
+ expanded = expand_behaviors_placeholders(behavior_list)
221
+ validated = UpdateAiAgentInput(
222
+ uuid=uuid.strip(),
223
+ name=name.strip(),
224
+ repo_uuid=repo_uuid.strip(),
225
+ instruction=inst,
226
+ behaviors=expanded,
227
+ data_source_ids=data_source_ids,
228
+ )
229
+ except ValidationError as exc:
230
+ raise typer.BadParameter(str(exc)) from exc
231
+
232
+ async def factory(client: PipefyClient):
233
+ pre = await validate_ai_agent_behaviors_sdk(
234
+ client,
235
+ pipe.strip(),
236
+ [b.model_dump(by_alias=True) for b in validated.behaviors],
237
+ strict_unknown_action_types=strict_unknown,
238
+ )
239
+ _raise_if_preflight_blocks(pre)
240
+ result = await client.update_ai_agent(validated)
241
+ if pre.get("warnings"):
242
+ return {**result, "preflight": pre}
243
+ return result
244
+
245
+ run_cli_command(ctx, json_out, factory)
246
+
247
+
248
+ @agent_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
249
+ def agent_delete(
250
+ ctx: typer.Context,
251
+ uuid: str = resource_id_argument(help="Agent UUID."),
252
+ yes: bool = typer.Option(
253
+ False, "--yes", "-y", help="Skip interactive confirmation."
254
+ ),
255
+ json_out: bool = typer.Option(False, "--json", "-j"),
256
+ ) -> None:
257
+ """Delete an AI agent (``delete_ai_agent``)."""
258
+ confirm_destructive(yes=yes, description=f"AI agent (UUID: {uuid})", verb="delete")
259
+
260
+ async def factory(client: PipefyClient):
261
+ return await client.delete_ai_agent(uuid)
262
+
263
+ run_cli_command(ctx, json_out, factory)
264
+
265
+
266
+ @agent_app.command("toggle", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
267
+ def agent_toggle(
268
+ ctx: typer.Context,
269
+ uuid: str = resource_id_argument(help="Agent UUID."),
270
+ active: bool = typer.Option(
271
+ True,
272
+ "--active/--inactive",
273
+ help="Enable or disable the agent.",
274
+ ),
275
+ yes: bool = typer.Option(
276
+ False, "--yes", "-y", help="Skip confirmation when disabling."
277
+ ),
278
+ json_out: bool = typer.Option(False, "--json", "-j"),
279
+ ) -> None:
280
+ """Enable or disable an AI agent (``toggle_ai_agent_status``)."""
281
+ if not active:
282
+ confirm_destructive(
283
+ yes=yes,
284
+ description=f"AI agent (UUID: {uuid})",
285
+ verb="disable",
286
+ )
287
+
288
+ async def factory(client: PipefyClient):
289
+ return await client.toggle_ai_agent_status(uuid, active=active)
290
+
291
+ run_cli_command(ctx, json_out, factory)
292
+
293
+
294
+ @logs_app.command("list")
295
+ def agent_logs_list(
296
+ ctx: typer.Context,
297
+ repo: str = typer.Option(..., "--repo", help="Pipe UUID."),
298
+ first: int = typer.Option(30, "--first", help="Page size."),
299
+ after: str | None = typer.Option(None, "--after", help="Pagination cursor."),
300
+ status: str | None = typer.Option(
301
+ None, "--status", help="processing|failed|success"
302
+ ),
303
+ search: str | None = typer.Option(None, "--search", help="Free-text search."),
304
+ json_out: bool = typer.Option(False, "--json", "-j"),
305
+ ) -> None:
306
+ """List AI agent execution logs (``get_ai_agent_logs``)."""
307
+
308
+ async def factory(client: PipefyClient):
309
+ return await client.get_ai_agent_logs(
310
+ repo, first=first, after=after, status=status, search_term=search
311
+ )
312
+
313
+ run_cli_command(ctx, json_out, factory)
314
+
315
+
316
+ @logs_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
317
+ def agent_logs_get(
318
+ ctx: typer.Context,
319
+ log_id: str = resource_id_argument(help="Log UUID."),
320
+ json_out: bool = typer.Option(False, "--json", "-j"),
321
+ ) -> None:
322
+ """Fetch one AI agent log entry (``get_ai_agent_log_details``)."""
323
+
324
+ async def factory(client: PipefyClient):
325
+ return await client.get_ai_agent_log_details(log_id)
326
+
327
+ run_cli_command(ctx, json_out, factory)
328
+
329
+
330
+ @agent_app.command("validate-behaviors")
331
+ def agent_validate_behaviors(
332
+ ctx: typer.Context,
333
+ pipe: str = typer.Option(..., "--pipe", help="Numeric pipe id."),
334
+ behaviors: str = typer.Option(
335
+ ..., "--behaviors", help="JSON array of behavior objects."
336
+ ),
337
+ strict_unknown: bool = typer.Option(
338
+ True,
339
+ "--strict/--no-strict",
340
+ help=(
341
+ "Pre-flight strictness: --strict (default) reports unknown actionType "
342
+ "values as problems; --no-strict reports them as warnings only."
343
+ ),
344
+ ),
345
+ json_out: bool = typer.Option(False, "--json", "-j"),
346
+ ) -> None:
347
+ """Dry-run behavior validation (``validate_ai_agent_behaviors``)."""
348
+ behavior_list = _parse_behaviors_json(behaviors)
349
+
350
+ async def factory(client: PipefyClient):
351
+ return await validate_ai_agent_behaviors_sdk(
352
+ client,
353
+ pipe.strip(),
354
+ behavior_list,
355
+ strict_unknown_action_types=strict_unknown,
356
+ )
357
+
358
+ run_cli_command(ctx, json_out, factory)
@@ -0,0 +1,318 @@
1
+ """AI Automations (generate_with_ai) via Internal API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+ from pipefy_sdk import (
9
+ CreateAiAutomationInput,
10
+ PipefyClient,
11
+ UpdateAiAutomationInput,
12
+ )
13
+ from pipefy_sdk.ai_preflight import (
14
+ filter_ai_automation_summaries,
15
+ validate_ai_automation_prompt_sdk,
16
+ )
17
+ from pydantic import ValidationError
18
+
19
+ from pipefy_cli.commands._common import (
20
+ ID_POSITIONAL_CONTEXT_SETTINGS,
21
+ confirm_destructive,
22
+ parse_json_object,
23
+ parse_json_value,
24
+ resource_id_argument,
25
+ run_cli_command,
26
+ )
27
+
28
+ ai_automation_app = typer.Typer(
29
+ help="AI Automations (generate_with_ai).",
30
+ no_args_is_help=True,
31
+ )
32
+
33
+
34
+ def _raise_if_prompt_preflight_blocks(payload: dict[str, Any]) -> None:
35
+ if not payload.get("success"):
36
+ raise typer.BadParameter(str(payload.get("error") or "validate-prompt failed"))
37
+ if not payload.get("valid"):
38
+ problems = payload.get("problems") or []
39
+ text = "\n".join(f" - {p}" for p in problems) if problems else "(no details)"
40
+ raise typer.BadParameter(f"validate-prompt failed:\n{text}")
41
+
42
+
43
+ def _parse_field_ids(raw: str) -> list[str]:
44
+ parsed = parse_json_value(raw, "--field-ids")
45
+ if not isinstance(parsed, list) or not all(isinstance(x, str) for x in parsed):
46
+ raise typer.BadParameter("--field-ids must be a JSON array of strings")
47
+ return list(parsed)
48
+
49
+
50
+ def _extract_existing_prompt_and_field_ids(
51
+ row: dict[str, Any],
52
+ ) -> tuple[str | None, list[str] | None]:
53
+ """Return ``(prompt, field_ids)`` from a ``generate_with_ai`` automation row.
54
+
55
+ Returns ``(None, None)`` for non-AI rows or rows missing the ``aiParams`` block;
56
+ callers must surface that as a clear user error.
57
+ """
58
+ action_params = row.get("action_params") or row.get("actionParams") or {}
59
+ ai_params = action_params.get("aiParams") or action_params.get("ai_params") or {}
60
+ prompt = ai_params.get("value")
61
+ field_ids = ai_params.get("fieldIds") or ai_params.get("field_ids")
62
+ if not isinstance(prompt, str):
63
+ prompt = None
64
+ if isinstance(field_ids, list) and all(isinstance(x, str) for x in field_ids):
65
+ return prompt, list(field_ids)
66
+ return prompt, None
67
+
68
+
69
+ @ai_automation_app.command("list")
70
+ def ai_automation_list(
71
+ ctx: typer.Context,
72
+ pipe: str = typer.Option(..., "--pipe", help="Pipe id."),
73
+ organization: str | None = typer.Option(
74
+ None, "--organization", "--org", help="Optional organization id."
75
+ ),
76
+ json_out: bool = typer.Option(False, "--json", "-j"),
77
+ ) -> None:
78
+ """List AI automations for a pipe (``get_ai_automations`` / filtered ``get_automations``)."""
79
+
80
+ async def factory(client: PipefyClient):
81
+ rows = await client.get_automations(
82
+ organization_id=organization,
83
+ pipe_id=pipe,
84
+ )
85
+ filtered = filter_ai_automation_summaries(rows or [])
86
+ return {"success": True, "data": filtered, "message": "AI automations listed."}
87
+
88
+ run_cli_command(ctx, json_out, factory)
89
+
90
+
91
+ @ai_automation_app.command("get", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
92
+ def ai_automation_get(
93
+ ctx: typer.Context,
94
+ automation_id: str = resource_id_argument(help="Automation rule id."),
95
+ json_out: bool = typer.Option(False, "--json", "-j"),
96
+ ) -> None:
97
+ """Load one automation row (``get_ai_automation`` / ``get_automation``)."""
98
+
99
+ async def factory(client: PipefyClient):
100
+ row = await client.get_automation(automation_id)
101
+ if row is None:
102
+ return {
103
+ "success": False,
104
+ "message": "No automation found for the given ID.",
105
+ }
106
+ return {"success": True, "message": "AI automation retrieved.", "data": row}
107
+
108
+ run_cli_command(ctx, json_out, factory)
109
+
110
+
111
+ @ai_automation_app.command("validate-prompt")
112
+ def ai_automation_validate_prompt(
113
+ ctx: typer.Context,
114
+ pipe: str = typer.Option(..., "--pipe", help="Pipe id."),
115
+ prompt: str = typer.Option(
116
+ ..., "--prompt", help="Prompt with %{internal_id} tokens."
117
+ ),
118
+ field_ids: str = typer.Option(
119
+ ..., "--field-ids", help="JSON array of output field internal ids."
120
+ ),
121
+ event_id: str | None = typer.Option(
122
+ None,
123
+ "--event-id",
124
+ help="Optional trigger id to validate against the pipe catalog.",
125
+ ),
126
+ json_out: bool = typer.Option(False, "--json", "-j"),
127
+ ) -> None:
128
+ """Pre-flight prompt validation (``validate_ai_automation_prompt``)."""
129
+ fids = _parse_field_ids(field_ids)
130
+
131
+ async def factory(client: PipefyClient):
132
+ return await validate_ai_automation_prompt_sdk(
133
+ client, pipe.strip(), prompt, fids, event_id
134
+ )
135
+
136
+ run_cli_command(ctx, json_out, factory)
137
+
138
+
139
+ @ai_automation_app.command("create")
140
+ def ai_automation_create(
141
+ ctx: typer.Context,
142
+ pipe: str = typer.Option(..., "--pipe", help="Pipe id."),
143
+ name: str = typer.Option(..., "--name", "-n", help="Automation name."),
144
+ event_id: str = typer.Option(..., "--event-id", help="Trigger event id."),
145
+ prompt: str = typer.Option(
146
+ ..., "--prompt", help="AI prompt with %{internal_id} refs."
147
+ ),
148
+ field_ids: str = typer.Option(
149
+ ..., "--field-ids", help="JSON array of output field ids."
150
+ ),
151
+ action_repo: str | None = typer.Option(
152
+ None, "--action-repo", help="Optional action repo id (defaults to --pipe)."
153
+ ),
154
+ skills_ids: str | None = typer.Option(
155
+ None, "--skills-ids", help="Optional JSON array of skill id strings."
156
+ ),
157
+ event_params: str | None = typer.Option(
158
+ None, "--event-params", help="Optional JSON object."
159
+ ),
160
+ condition: str | None = typer.Option(
161
+ None, "--condition", help="Optional JSON object (omit for default placeholder)."
162
+ ),
163
+ json_out: bool = typer.Option(False, "--json", "-j"),
164
+ ) -> None:
165
+ """Create an AI automation (``create_ai_automation``)."""
166
+ fids = _parse_field_ids(field_ids)
167
+ ep = parse_json_object(event_params, "--event-params")
168
+ cond = parse_json_object(condition, "--condition")
169
+ skills_raw = parse_json_value(skills_ids, "--skills-ids") if skills_ids else None
170
+ skills: list[str] = []
171
+ if skills_raw is not None:
172
+ if not isinstance(skills_raw, list) or not all(
173
+ isinstance(x, str) for x in skills_raw
174
+ ):
175
+ raise typer.BadParameter("--skills-ids must be a JSON array of strings")
176
+ skills = list(skills_raw)
177
+
178
+ async def factory(client: PipefyClient):
179
+ pre = await validate_ai_automation_prompt_sdk(
180
+ client, pipe.strip(), prompt, fids, event_id
181
+ )
182
+ _raise_if_prompt_preflight_blocks(pre)
183
+ try:
184
+ kwargs: dict[str, Any] = {
185
+ "name": name.strip(),
186
+ "event_id": event_id.strip(),
187
+ "pipe_id": pipe.strip(),
188
+ "action_repo_id": action_repo.strip() if action_repo else None,
189
+ "prompt": prompt,
190
+ "field_ids": fids,
191
+ "skills_ids": skills,
192
+ }
193
+ if ep is not None:
194
+ kwargs["event_params"] = ep
195
+ if cond is not None:
196
+ kwargs["condition"] = cond
197
+ inp = CreateAiAutomationInput(**kwargs)
198
+ except ValidationError as exc:
199
+ raise typer.BadParameter(str(exc)) from exc
200
+ return await client.create_ai_automation(inp)
201
+
202
+ run_cli_command(ctx, json_out, factory)
203
+
204
+
205
+ @ai_automation_app.command("update", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
206
+ def ai_automation_update(
207
+ ctx: typer.Context,
208
+ automation_id: str = resource_id_argument(help="Automation rule id."),
209
+ pipe: str = typer.Option(
210
+ ...,
211
+ "--pipe",
212
+ help="Pipe id for validate-prompt pre-flight.",
213
+ ),
214
+ prompt: str | None = typer.Option(
215
+ None,
216
+ "--prompt",
217
+ help="New prompt; omit to keep the current value (re-used in pre-flight).",
218
+ ),
219
+ field_ids: str | None = typer.Option(
220
+ None,
221
+ "--field-ids",
222
+ help=(
223
+ "JSON array of output field ids; omit to keep the current values "
224
+ "(re-used in pre-flight)."
225
+ ),
226
+ ),
227
+ name: str | None = typer.Option(None, "--name", "-n"),
228
+ patch_active: bool | None = typer.Option(
229
+ None,
230
+ "--patch-active/--no-patch-active",
231
+ help="Toggle the enabled flag; omit both flags to leave unchanged.",
232
+ ),
233
+ skills_ids: str | None = typer.Option(None, "--skills-ids"),
234
+ event_params: str | None = typer.Option(None, "--event-params"),
235
+ condition: str | None = typer.Option(None, "--condition"),
236
+ json_out: bool = typer.Option(False, "--json", "-j"),
237
+ ) -> None:
238
+ """Update an AI automation (``update_ai_automation``).
239
+
240
+ ``--prompt`` and ``--field-ids`` are optional: when omitted the CLI reads
241
+ the current values from the existing automation and re-uses them for the
242
+ ``validate_ai_automation_prompt`` pre-flight. Only fields you pass
243
+ explicitly are sent in the patch.
244
+ """
245
+ fids: list[str] | None = _parse_field_ids(field_ids) if field_ids else None
246
+ ep = parse_json_object(event_params, "--event-params")
247
+ cond = parse_json_object(condition, "--condition")
248
+ skills_raw = parse_json_value(skills_ids, "--skills-ids") if skills_ids else None
249
+ skills: list[str] | None = None
250
+ if skills_raw is not None:
251
+ if not isinstance(skills_raw, list) or not all(
252
+ isinstance(x, str) for x in skills_raw
253
+ ):
254
+ raise typer.BadParameter("--skills-ids must be a JSON array of strings")
255
+ skills = list(skills_raw)
256
+
257
+ async def factory(client: PipefyClient):
258
+ row = await client.get_automation(automation_id)
259
+ if row is None:
260
+ return {"success": False, "message": "Automation not found."}
261
+ existing_prompt, existing_fids = _extract_existing_prompt_and_field_ids(row)
262
+ effective_prompt = prompt if prompt is not None else existing_prompt
263
+ effective_fids = fids if fids is not None else existing_fids
264
+ if not effective_prompt or not effective_fids:
265
+ raise typer.BadParameter(
266
+ "Could not infer current prompt / field_ids from the existing automation. "
267
+ "Pass --prompt and --field-ids explicitly."
268
+ )
269
+ ev = str(row.get("event_id") or row.get("eventId") or "")
270
+ pre = await validate_ai_automation_prompt_sdk(
271
+ client,
272
+ pipe.strip(),
273
+ effective_prompt,
274
+ effective_fids,
275
+ ev or None,
276
+ )
277
+ _raise_if_prompt_preflight_blocks(pre)
278
+ try:
279
+ kwargs_u: dict[str, Any] = {
280
+ "automation_id": str(automation_id).strip(),
281
+ "name": name.strip() if name else None,
282
+ "active": patch_active,
283
+ "skills_ids": skills,
284
+ }
285
+ if prompt is not None:
286
+ kwargs_u["prompt"] = prompt
287
+ if fids is not None:
288
+ kwargs_u["field_ids"] = fids
289
+ if ep is not None:
290
+ kwargs_u["event_params"] = ep
291
+ if cond is not None:
292
+ kwargs_u["condition"] = cond
293
+ inp = UpdateAiAutomationInput(**kwargs_u)
294
+ except ValidationError as exc:
295
+ raise typer.BadParameter(str(exc)) from exc
296
+ return await client.update_ai_automation(inp)
297
+
298
+ run_cli_command(ctx, json_out, factory)
299
+
300
+
301
+ @ai_automation_app.command("delete", context_settings=ID_POSITIONAL_CONTEXT_SETTINGS)
302
+ def ai_automation_delete(
303
+ ctx: typer.Context,
304
+ automation_id: str = resource_id_argument(help="Automation rule id."),
305
+ yes: bool = typer.Option(
306
+ False, "--yes", "-y", help="Skip interactive confirmation."
307
+ ),
308
+ json_out: bool = typer.Option(False, "--json", "-j"),
309
+ ) -> None:
310
+ """Delete an AI automation (``delete_ai_automation`` / ``delete_automation``)."""
311
+ confirm_destructive(
312
+ yes=yes, description=f"AI automation {automation_id}", verb="delete"
313
+ )
314
+
315
+ async def factory(client: PipefyClient):
316
+ return await client.delete_automation(automation_id)
317
+
318
+ run_cli_command(ctx, json_out, factory)