guzli-cli 0.2.0__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 (53) hide show
  1. guzli/__init__.py +3 -0
  2. guzli/__main__.py +4 -0
  3. guzli/auth.py +182 -0
  4. guzli/cli.py +84 -0
  5. guzli/commands/__init__.py +1 -0
  6. guzli/commands/admin.py +421 -0
  7. guzli/commands/auth.py +502 -0
  8. guzli/commands/campaign_launch.py +188 -0
  9. guzli/commands/campaigns.py +571 -0
  10. guzli/commands/common.py +42 -0
  11. guzli/commands/conversations.py +404 -0
  12. guzli/commands/integrations.py +48 -0
  13. guzli/commands/telephony.py +274 -0
  14. guzli/commands/voice_calls.py +270 -0
  15. guzli/config.py +137 -0
  16. guzli/config_store.py +218 -0
  17. guzli/errors.py +47 -0
  18. guzli/http_client.py +213 -0
  19. guzli/mcp/__init__.py +1 -0
  20. guzli/mcp/server.py +320 -0
  21. guzli/models.py +173 -0
  22. guzli/oauth_callback.py +126 -0
  23. guzli/output.py +79 -0
  24. guzli/runtime.py +159 -0
  25. guzli/services/__init__.py +29 -0
  26. guzli/services/api_keys.py +105 -0
  27. guzli/services/calls.py +48 -0
  28. guzli/services/campaigns.py +193 -0
  29. guzli/services/cli_auth.py +103 -0
  30. guzli/services/conversations.py +112 -0
  31. guzli/services/integrations.py +18 -0
  32. guzli/services/replies.py +21 -0
  33. guzli/services/telephony.py +80 -0
  34. guzli/services/voice_profiles.py +56 -0
  35. guzli/services/webhooks.py +169 -0
  36. guzli/types.py +15 -0
  37. guzli/validators.py +83 -0
  38. guzli/workflows/__init__.py +13 -0
  39. guzli/workflows/campaign_launch.py +501 -0
  40. guzli_cli-0.2.0.data/data/share/guzli/CHANGELOG.md +16 -0
  41. guzli_cli-0.2.0.data/data/share/guzli/PROVENANCE.md +23 -0
  42. guzli_cli-0.2.0.data/data/share/guzli/SECURITY.md +18 -0
  43. guzli_cli-0.2.0.data/data/share/guzli/SECURITY_AUDIT.md +47 -0
  44. guzli_cli-0.2.0.data/data/share/guzli/SKILL.md +101 -0
  45. guzli_cli-0.2.0.data/data/share/guzli/examples/compliance-advisory-us.json +15 -0
  46. guzli_cli-0.2.0.data/data/share/guzli/examples/pizza-order-extraction.json +65 -0
  47. guzli_cli-0.2.0.data/data/share/guzli/references/api.md +245 -0
  48. guzli_cli-0.2.0.dist-info/METADATA +341 -0
  49. guzli_cli-0.2.0.dist-info/RECORD +53 -0
  50. guzli_cli-0.2.0.dist-info/WHEEL +5 -0
  51. guzli_cli-0.2.0.dist-info/entry_points.txt +3 -0
  52. guzli_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
  53. guzli_cli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,404 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Dict, List, Mapping
5
+
6
+ from guzli.commands.common import add_json_flag as _add_json_flag
7
+ from guzli.commands.common import parse_bool_flag as _parse_bool_flag
8
+ from guzli.errors import ApiError, ValidationError
9
+ from guzli.models import AgentReplyResult, ConversationDetailView, ConversationSummaryView
10
+ from guzli.runtime import RuntimeContext
11
+ from guzli.types import JsonValue
12
+ from guzli.validators import normalize_list, parse_iso_datetime, parse_json, require_non_empty
13
+
14
+
15
+ def handle_conversations_list(context: RuntimeContext, args: argparse.Namespace) -> None:
16
+ params = build_conversation_list_params(args)
17
+ payload = context.conversations.list_conversations(params)
18
+ if args.json:
19
+ context.renderer.print_json(payload)
20
+ return
21
+ items = _extract_items(payload)
22
+ summaries = [ConversationSummaryView.from_json(item) for item in items]
23
+ context.renderer.print_conversation_list(summaries)
24
+ _print_cursor(payload)
25
+
26
+
27
+ def handle_conversation_get(context: RuntimeContext, args: argparse.Namespace) -> None:
28
+ conversation_id = require_non_empty(args.conversation_id, "conversation_id")
29
+ payload = context.conversations.get_conversation(conversation_id, not args.no_transcript)
30
+ if args.json:
31
+ context.renderer.print_json(payload)
32
+ return
33
+ if not isinstance(payload, Mapping):
34
+ raise ApiError("Unexpected response payload")
35
+ detail = ConversationDetailView.from_json(payload)
36
+ context.renderer.print_conversation_detail(detail, include_transcript=not args.no_transcript)
37
+
38
+
39
+ def handle_conversation_history(context: RuntimeContext, args: argparse.Namespace) -> None:
40
+ fingerprint_id = require_non_empty(args.fingerprint_id, "fingerprint_id")
41
+ payload = context.conversations.get_history(fingerprint_id)
42
+ context.renderer.print_json(payload)
43
+
44
+
45
+ def handle_conversation_filters(context: RuntimeContext, args: argparse.Namespace) -> None:
46
+ payload = context.conversations.get_filters()
47
+ context.renderer.print_json(payload)
48
+
49
+
50
+ def handle_conversation_interactions(context: RuntimeContext, args: argparse.Namespace) -> None:
51
+ conversation_id = require_non_empty(args.conversation_id, "conversation_id")
52
+ payload = context.conversations.list_automation_interactions(conversation_id)
53
+ context.renderer.print_json(payload)
54
+
55
+
56
+ def handle_conversation_assign(context: RuntimeContext, args: argparse.Namespace) -> None:
57
+ conversation_id = require_non_empty(args.conversation_id, "conversation_id")
58
+ assignment_type = require_non_empty(args.assignment_type, "assignment_type")
59
+ assigned_to_id = args.assigned_to_id
60
+ if assignment_type in {"agent", "team"} and not assigned_to_id:
61
+ raise ValidationError("assigned_to_id is required for agent/team assignments")
62
+ payload = {
63
+ "assignment_type": assignment_type,
64
+ "assigned_to_id": assigned_to_id,
65
+ "queue_label": args.queue_label,
66
+ "reason": args.reason,
67
+ }
68
+ response = context.conversations.update_assignment(conversation_id, payload)
69
+ context.renderer.print_json(response)
70
+
71
+
72
+ def handle_conversation_mark(context: RuntimeContext, args: argparse.Namespace) -> None:
73
+ conversation_id = require_non_empty(args.conversation_id, "conversation_id")
74
+ response = context.conversations.mark_conversation(conversation_id)
75
+ context.renderer.print_json(response)
76
+
77
+
78
+ def handle_conversation_unmark(context: RuntimeContext, args: argparse.Namespace) -> None:
79
+ conversation_id = require_non_empty(args.conversation_id, "conversation_id")
80
+ response = context.conversations.unmark_conversation(conversation_id)
81
+ context.renderer.print_json(response)
82
+
83
+
84
+ def handle_conversation_mute(context: RuntimeContext, args: argparse.Namespace) -> None:
85
+ conversation_id = require_non_empty(args.conversation_id, "conversation_id")
86
+ mute_expires_at = parse_iso_datetime(args.mute_expires_at, "mute_expires_at")
87
+ payload = {"mute_expires_at": mute_expires_at} if mute_expires_at else None
88
+ response = context.conversations.mute_conversation(conversation_id, payload)
89
+ context.renderer.print_json(response)
90
+
91
+
92
+ def handle_conversation_unmute(context: RuntimeContext, args: argparse.Namespace) -> None:
93
+ conversation_id = require_non_empty(args.conversation_id, "conversation_id")
94
+ response = context.conversations.unmute_conversation(conversation_id)
95
+ context.renderer.print_json(response)
96
+
97
+
98
+ def handle_conversation_close(context: RuntimeContext, args: argparse.Namespace) -> None:
99
+ conversation_id = require_non_empty(args.conversation_id, "conversation_id")
100
+ summary = _resolve_summary(args.summary, args.summary_json)
101
+ metadata_updates = parse_json(args.metadata_updates, "metadata_updates")
102
+ payload = {
103
+ "summary": summary,
104
+ "duration_seconds": args.duration_seconds,
105
+ "metadata_updates": metadata_updates or {},
106
+ }
107
+ response = context.conversations.close_conversation(conversation_id, payload)
108
+ context.renderer.print_json(response)
109
+
110
+
111
+ def handle_conversation_interaction_state(
112
+ context: RuntimeContext, args: argparse.Namespace
113
+ ) -> None:
114
+ conversation_id = require_non_empty(args.conversation_id, "conversation_id")
115
+ state = require_non_empty(args.state, "state")
116
+ _validate_interaction_state(state)
117
+ payload = {
118
+ "state": state,
119
+ "interaction_id": args.interaction_id,
120
+ "reason": args.reason,
121
+ }
122
+ response = context.conversations.update_interaction_state(conversation_id, payload)
123
+ context.renderer.print_json(response)
124
+
125
+
126
+ def handle_conversation_metrics(context: RuntimeContext, args: argparse.Namespace) -> None:
127
+ metrics = normalize_list(args.metric)
128
+ params: Dict[str, object] = {"hours": args.hours}
129
+ if metrics:
130
+ params["metric"] = metrics
131
+ if args.metric_agent_id:
132
+ params["agent_id"] = args.metric_agent_id
133
+ if args.metric_channel:
134
+ params["channel"] = args.metric_channel
135
+ payload = context.conversations.get_metrics(params)
136
+ context.renderer.print_json(payload)
137
+
138
+
139
+ def handle_agent_reply(context: RuntimeContext, args: argparse.Namespace) -> None:
140
+ conversation_id = require_non_empty(args.conversation_id, "conversation_id")
141
+ message = require_non_empty(args.message, "message")
142
+ if len(message) > 4000:
143
+ raise ValidationError("message exceeds 4000 characters")
144
+ metadata = parse_json(args.metadata, "metadata") or {}
145
+ payload = {"message": message, "metadata": metadata}
146
+ response = context.replies.send_agent_reply(conversation_id, payload)
147
+ if args.json:
148
+ context.renderer.print_json(response)
149
+ return
150
+ if not isinstance(response, Mapping):
151
+ raise ApiError("Unexpected response payload")
152
+ result = AgentReplyResult.from_json(response)
153
+ context.renderer.print_reply_result(result)
154
+
155
+
156
+ def build_conversation_list_params(args: argparse.Namespace) -> Dict[str, object]:
157
+ params: Dict[str, object] = {}
158
+ status = normalize_list(args.status)
159
+ channel = normalize_list(args.channel)
160
+ tags = normalize_list(args.tags)
161
+ platform = normalize_list(args.platform)
162
+ priority = normalize_list(args.priority)
163
+ assignment_type = normalize_list(args.assignment_type)
164
+ sentiment = normalize_list(args.sentiment)
165
+ interaction_state = normalize_list(args.interaction_state)
166
+ if status:
167
+ params["status"] = status
168
+ if channel:
169
+ params["channel"] = channel
170
+ if tags:
171
+ params["tags"] = tags
172
+ if platform:
173
+ params["platform"] = platform
174
+ if args.since:
175
+ params["since"] = parse_iso_datetime(args.since, "since")
176
+ if args.limit:
177
+ params["limit"] = args.limit
178
+ if args.cursor:
179
+ params["cursor"] = args.cursor
180
+ if args.sort_by:
181
+ params["sort_by"] = args.sort_by
182
+ if args.sort_order:
183
+ params["sort_order"] = args.sort_order
184
+ if assignment_type:
185
+ params["assignment_type"] = assignment_type
186
+ if args.assigned_to_id:
187
+ params["assigned_to_id"] = args.assigned_to_id
188
+ if args.assigned_to_type:
189
+ params["assigned_to_type"] = args.assigned_to_type
190
+ if priority:
191
+ params["priority"] = priority
192
+ if args.is_marked is not None:
193
+ params["is_marked"] = args.is_marked
194
+ if args.is_muted is not None:
195
+ params["is_muted"] = args.is_muted
196
+ if sentiment:
197
+ params["sentiment"] = sentiment
198
+ if interaction_state:
199
+ params["interaction_state"] = interaction_state
200
+ if args.has_unread is not None:
201
+ params["has_unread"] = args.has_unread
202
+ if args.unread_participant_type:
203
+ params["unread_participant_type"] = args.unread_participant_type
204
+ if args.fingerprint_id:
205
+ params["fingerprint_id"] = args.fingerprint_id
206
+ if args.pane_assignee_id:
207
+ params["pane_assignee_id"] = args.pane_assignee_id
208
+ if args.view_id:
209
+ params["view_id"] = args.view_id
210
+ if args.queue_label:
211
+ params["queue_label"] = args.queue_label
212
+ return params
213
+
214
+
215
+ def _resolve_summary(summary_text: str | None, summary_json: str | None) -> JsonValue | None:
216
+ if summary_text and summary_json:
217
+ raise ValidationError("Provide either summary or summary_json, not both")
218
+ if summary_json:
219
+ summary = parse_json(summary_json, "summary_json")
220
+ if not isinstance(summary, Mapping):
221
+ raise ValidationError("summary_json must be an object")
222
+ return summary
223
+ if summary_text:
224
+ return summary_text.strip() or None
225
+ return None
226
+
227
+
228
+ def _validate_interaction_state(value: str) -> None:
229
+ allowed = {
230
+ "none",
231
+ "awaiting_visitor",
232
+ "awaiting_agent",
233
+ "awaiting_tool",
234
+ "completed",
235
+ "cancelled",
236
+ "timed_out",
237
+ }
238
+ if value.strip().lower() not in allowed:
239
+ raise ValidationError(f"Invalid interaction state: {value}")
240
+
241
+
242
+ def _extract_items(payload: JsonValue) -> List[Mapping[str, JsonValue]]:
243
+ if not isinstance(payload, Mapping):
244
+ raise ApiError("Unexpected response payload")
245
+ items = payload.get("items")
246
+ if not isinstance(items, list):
247
+ return []
248
+ return [item for item in items if isinstance(item, Mapping)]
249
+
250
+
251
+ def _print_cursor(payload: JsonValue) -> None:
252
+ if not isinstance(payload, Mapping):
253
+ return
254
+ next_cursor = payload.get("nextCursor") or payload.get("next_cursor")
255
+ if isinstance(next_cursor, str) and next_cursor:
256
+ print(f"Next cursor: {next_cursor}")
257
+
258
+
259
+ def _add_conversations_list(conv_sub: argparse._SubParsersAction) -> None:
260
+ parser = conv_sub.add_parser("list", help="List conversations")
261
+ parser.add_argument("--status", action="append")
262
+ parser.add_argument("--channel", action="append")
263
+ parser.add_argument("--tags", action="append")
264
+ parser.add_argument("--platform", action="append")
265
+ parser.add_argument("--since")
266
+ parser.add_argument("--limit", type=int)
267
+ parser.add_argument("--cursor")
268
+ parser.add_argument("--sort-by", dest="sort_by")
269
+ parser.add_argument("--sort-order", dest="sort_order")
270
+ parser.add_argument("--assignment-type", dest="assignment_type", action="append")
271
+ parser.add_argument("--assigned-to-id", dest="assigned_to_id")
272
+ parser.add_argument("--assigned-to-type", dest="assigned_to_type")
273
+ parser.add_argument("--priority", action="append")
274
+ parser.add_argument("--sentiment", action="append")
275
+ parser.add_argument("--is-marked", dest="is_marked", type=_parse_bool_flag)
276
+ parser.add_argument("--is-muted", dest="is_muted", type=_parse_bool_flag)
277
+ parser.add_argument("--interaction-state", dest="interaction_state", action="append")
278
+ parser.add_argument("--has-unread", dest="has_unread", type=_parse_bool_flag)
279
+ parser.add_argument("--unread-participant-type", dest="unread_participant_type")
280
+ parser.add_argument("--fingerprint-id", dest="fingerprint_id")
281
+ parser.add_argument("--pane-assignee-id", dest="pane_assignee_id")
282
+ parser.add_argument("--view-id", dest="view_id")
283
+ parser.add_argument("--queue-label", dest="queue_label")
284
+ _add_json_flag(parser)
285
+ parser.set_defaults(handler=handle_conversations_list)
286
+
287
+
288
+ def _add_conversation_get(conv_sub: argparse._SubParsersAction) -> None:
289
+ parser = conv_sub.add_parser("get", help="Get conversation details")
290
+ parser.add_argument("conversation_id")
291
+ parser.add_argument("--no-transcript", action="store_true")
292
+ _add_json_flag(parser)
293
+ parser.set_defaults(handler=handle_conversation_get)
294
+
295
+
296
+ def _add_conversation_history(conv_sub: argparse._SubParsersAction) -> None:
297
+ parser = conv_sub.add_parser("history", help="Get conversation history by fingerprint")
298
+ parser.add_argument("fingerprint_id")
299
+ parser.set_defaults(handler=handle_conversation_history)
300
+
301
+
302
+ def _add_conversation_filters(conv_sub: argparse._SubParsersAction) -> None:
303
+ parser = conv_sub.add_parser("filters", help="List available conversation filters")
304
+ parser.set_defaults(handler=handle_conversation_filters)
305
+
306
+
307
+ def _add_conversation_interactions(conv_sub: argparse._SubParsersAction) -> None:
308
+ parser = conv_sub.add_parser("automation-interactions", help="List automation interactions")
309
+ parser.add_argument("conversation_id")
310
+ parser.set_defaults(handler=handle_conversation_interactions)
311
+
312
+
313
+ def _add_conversation_assign(conv_sub: argparse._SubParsersAction) -> None:
314
+ parser = conv_sub.add_parser("assign", help="Assign conversation")
315
+ parser.add_argument("conversation_id")
316
+ parser.add_argument("--assignment-type", dest="assignment_type", required=True)
317
+ parser.add_argument("--assigned-to-id", dest="assigned_to_id")
318
+ parser.add_argument("--queue-label", dest="queue_label")
319
+ parser.add_argument("--reason")
320
+ parser.set_defaults(handler=handle_conversation_assign)
321
+
322
+
323
+ def _add_conversation_mark(conv_sub: argparse._SubParsersAction) -> None:
324
+ parser = conv_sub.add_parser("mark", help="Mark conversation")
325
+ parser.add_argument("conversation_id")
326
+ parser.set_defaults(handler=handle_conversation_mark)
327
+
328
+
329
+ def _add_conversation_unmark(conv_sub: argparse._SubParsersAction) -> None:
330
+ parser = conv_sub.add_parser("unmark", help="Unmark conversation")
331
+ parser.add_argument("conversation_id")
332
+ parser.set_defaults(handler=handle_conversation_unmark)
333
+
334
+
335
+ def _add_conversation_mute(conv_sub: argparse._SubParsersAction) -> None:
336
+ parser = conv_sub.add_parser("mute", help="Mute conversation")
337
+ parser.add_argument("conversation_id")
338
+ parser.add_argument("--mute-expires-at", dest="mute_expires_at")
339
+ parser.set_defaults(handler=handle_conversation_mute)
340
+
341
+
342
+ def _add_conversation_unmute(conv_sub: argparse._SubParsersAction) -> None:
343
+ parser = conv_sub.add_parser("unmute", help="Unmute conversation")
344
+ parser.add_argument("conversation_id")
345
+ parser.set_defaults(handler=handle_conversation_unmute)
346
+
347
+
348
+ def _add_conversation_close(conv_sub: argparse._SubParsersAction) -> None:
349
+ parser = conv_sub.add_parser("close", help="Close conversation")
350
+ parser.add_argument("conversation_id")
351
+ parser.add_argument("--summary")
352
+ parser.add_argument("--summary-json", dest="summary_json")
353
+ parser.add_argument("--duration-seconds", dest="duration_seconds", type=int)
354
+ parser.add_argument("--metadata-updates", dest="metadata_updates")
355
+ parser.set_defaults(handler=handle_conversation_close)
356
+
357
+
358
+ def _add_conversation_interaction_state(conv_sub: argparse._SubParsersAction) -> None:
359
+ parser = conv_sub.add_parser("interaction-state", help="Update interaction state")
360
+ parser.add_argument("conversation_id")
361
+ parser.add_argument("--state", required=True)
362
+ parser.add_argument("--interaction-id", dest="interaction_id")
363
+ parser.add_argument("--reason")
364
+ parser.set_defaults(handler=handle_conversation_interaction_state)
365
+
366
+
367
+ def _add_conversation_metrics(conv_sub: argparse._SubParsersAction) -> None:
368
+ parser = conv_sub.add_parser("metrics", help="Fetch conversation metrics")
369
+ parser.add_argument("--hours", type=int, default=24)
370
+ parser.add_argument("--metric", action="append")
371
+ parser.add_argument("--agent-id", dest="metric_agent_id")
372
+ parser.add_argument("--channel", dest="metric_channel")
373
+ parser.set_defaults(handler=handle_conversation_metrics)
374
+
375
+
376
+ def _add_agent_reply(replies_sub: argparse._SubParsersAction) -> None:
377
+ parser = replies_sub.add_parser("send", help="Send agent reply")
378
+ parser.add_argument("conversation_id")
379
+ parser.add_argument("message")
380
+ parser.add_argument("--metadata")
381
+ _add_json_flag(parser)
382
+ parser.set_defaults(handler=handle_agent_reply)
383
+
384
+
385
+ def register(subparsers: argparse._SubParsersAction) -> None:
386
+ conversations = subparsers.add_parser("conversations", help="Conversation operations")
387
+ conv_sub = conversations.add_subparsers(dest="conversations_command", required=True)
388
+ _add_conversations_list(conv_sub)
389
+ _add_conversation_get(conv_sub)
390
+ _add_conversation_history(conv_sub)
391
+ _add_conversation_filters(conv_sub)
392
+ _add_conversation_interactions(conv_sub)
393
+ _add_conversation_assign(conv_sub)
394
+ _add_conversation_mark(conv_sub)
395
+ _add_conversation_unmark(conv_sub)
396
+ _add_conversation_mute(conv_sub)
397
+ _add_conversation_unmute(conv_sub)
398
+ _add_conversation_close(conv_sub)
399
+ _add_conversation_interaction_state(conv_sub)
400
+ _add_conversation_metrics(conv_sub)
401
+
402
+ replies = subparsers.add_parser("agent-replies", help="Send agent replies")
403
+ replies_sub = replies.add_subparsers(dest="replies_command", required=True)
404
+ _add_agent_reply(replies_sub)
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Dict
5
+
6
+ from guzli.errors import ConfigError
7
+ from guzli.runtime import RuntimeContext
8
+ from guzli.validators import require_non_empty
9
+
10
+
11
+ def handle_integrations_connect_twilio(context: RuntimeContext, args: argparse.Namespace) -> None:
12
+ organization_id = (
13
+ getattr(args, "organization_id", None) or context.organization_id or ""
14
+ ).strip()
15
+ if not organization_id:
16
+ raise ConfigError(
17
+ "organization_id is required — set it via `guzli auth login --organization-id …`."
18
+ )
19
+ body: Dict[str, object] = {
20
+ "account_sid": require_non_empty(args.account_sid, "account_sid"),
21
+ "auth_token": require_non_empty(args.auth_token, "auth_token"),
22
+ }
23
+ if args.label:
24
+ body["label"] = args.label
25
+ if args.agent_id:
26
+ body["agent_id"] = args.agent_id
27
+ payload = context.integrations.connect_twilio(organization_id, body)
28
+ context.renderer.print_json(payload)
29
+
30
+
31
+ def _add_integrations_connect_twilio(sub: argparse._SubParsersAction) -> None:
32
+ parser = sub.add_parser(
33
+ "connect-twilio",
34
+ help="Connect a Twilio integration (creates the telephony account Campaign Launch needs)",
35
+ )
36
+ parser.add_argument(
37
+ "--account-sid", dest="account_sid", required=True, help="Twilio Account SID (AC…)"
38
+ )
39
+ parser.add_argument("--auth-token", dest="auth_token", required=True, help="Twilio auth token")
40
+ parser.add_argument("--label", help="Optional display label for the account")
41
+ parser.add_argument("--agent-id", dest="agent_id", help="Optional agent binding")
42
+ parser.set_defaults(handler=handle_integrations_connect_twilio)
43
+
44
+
45
+ def register(subparsers: argparse._SubParsersAction) -> None:
46
+ integrations = subparsers.add_parser("integrations", help="Connect external integrations")
47
+ integ_sub = integrations.add_subparsers(dest="integrations_command", required=True)
48
+ _add_integrations_connect_twilio(integ_sub)