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,571 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import uuid
5
+ from typing import Dict, Mapping
6
+
7
+ from guzli.commands.campaign_launch import add_campaign_launch_parser
8
+ from guzli.commands.common import parse_bool_flag as _parse_bool_flag
9
+ from guzli.errors import ConfigError, ValidationError
10
+ from guzli.runtime import RuntimeContext
11
+ from guzli.validators import parse_json, parse_json_or_file, require_non_empty
12
+
13
+
14
+ def handle_campaign_list(context: RuntimeContext, args: argparse.Namespace) -> None:
15
+ params: Dict[str, object] = {}
16
+ if args.status:
17
+ params["status"] = args.status
18
+ if args.search:
19
+ params["search"] = args.search
20
+ if args.limit:
21
+ params["limit"] = args.limit
22
+ payload = context.campaigns.list_campaigns(params)
23
+ context.renderer.print_json(payload)
24
+
25
+
26
+ def handle_campaign_get(context: RuntimeContext, args: argparse.Namespace) -> None:
27
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
28
+ payload = context.campaigns.get_campaign(campaign_id)
29
+ context.renderer.print_json(payload)
30
+
31
+
32
+ def handle_campaign_preview(context: RuntimeContext, args: argparse.Namespace) -> None:
33
+ body = parse_json_or_file(args.config_json, args.config_file, "campaign preview")
34
+ if not isinstance(body, Mapping):
35
+ raise ValidationError("campaign preview requires a JSON object")
36
+ context.renderer.print_json(context.campaigns.preview_campaign(dict(body)))
37
+
38
+
39
+ def handle_campaign_create(context: RuntimeContext, args: argparse.Namespace) -> None:
40
+ body = parse_json_or_file(args.config_json, args.config_file, "campaign config")
41
+ if not isinstance(body, Mapping):
42
+ raise ValidationError("campaign config must be a JSON object")
43
+ body = dict(body)
44
+ if args.name:
45
+ body["name"] = args.name
46
+ if "name" not in body or not str(body.get("name", "")).strip():
47
+ raise ValidationError('name is required (pass --name or include "name" in the config)')
48
+ if not str(body.get("organization_id", "")).strip():
49
+ if not context.organization_id:
50
+ raise ConfigError(
51
+ "organization_id is required for campaign create. "
52
+ "Set it via `guzli auth login --organization-id …` or include it in the config."
53
+ )
54
+ body["organization_id"] = context.organization_id
55
+ if not str(body.get("agent_id", "")).strip() and context.agent_id:
56
+ body["agent_id"] = context.agent_id
57
+ payload = context.campaigns.create_campaign(body)
58
+ context.renderer.print_json(payload)
59
+
60
+
61
+ def handle_campaign_update(context: RuntimeContext, args: argparse.Namespace) -> None:
62
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
63
+ body = parse_json_or_file(args.config_json, args.config_file, "campaign update")
64
+ if not isinstance(body, Mapping):
65
+ raise ValidationError("campaign update must be a JSON object (matches CampaignUpdate)")
66
+ payload = context.campaigns.update_campaign(campaign_id, dict(body))
67
+ context.renderer.print_json(payload)
68
+
69
+
70
+ def handle_campaign_readiness(context: RuntimeContext, args: argparse.Namespace) -> None:
71
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
72
+ payload = context.campaigns.get_readiness(campaign_id)
73
+ context.renderer.print_json(payload)
74
+
75
+
76
+ def handle_campaign_activate(context: RuntimeContext, args: argparse.Namespace) -> None:
77
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
78
+ payload = context.campaigns.activate_campaign(campaign_id)
79
+ context.renderer.print_json(payload)
80
+
81
+
82
+ def handle_campaign_pause(context: RuntimeContext, args: argparse.Namespace) -> None:
83
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
84
+ payload = context.campaigns.pause_campaign(campaign_id)
85
+ context.renderer.print_json(payload)
86
+
87
+
88
+ def handle_campaign_archive(context: RuntimeContext, args: argparse.Namespace) -> None:
89
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
90
+ context.renderer.print_json(context.campaigns.archive_campaign(campaign_id))
91
+
92
+
93
+ def handle_campaign_run(context: RuntimeContext, args: argparse.Namespace) -> None:
94
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
95
+ body: Dict[str, object] = {}
96
+ metadata = parse_json(args.metadata, "metadata")
97
+ if metadata is not None:
98
+ if not isinstance(metadata, Mapping):
99
+ raise ValidationError("metadata must be a JSON object")
100
+ body["metadata"] = dict(metadata)
101
+ audience = parse_json(args.audience_override, "audience_override")
102
+ if audience is not None:
103
+ if not isinstance(audience, Mapping):
104
+ raise ValidationError("audience_override must be a JSON object")
105
+ body["audience_override"] = dict(audience)
106
+ payload = context.campaigns.trigger_manual_run(campaign_id, body)
107
+ context.renderer.print_json(payload)
108
+
109
+
110
+ def handle_campaign_audience_import(context: RuntimeContext, args: argparse.Namespace) -> None:
111
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
112
+ body = parse_json_or_file(args.config_json, args.config_file, "audience import")
113
+ if body is None:
114
+ raise ValidationError("audience import requires --config-json or --config-file")
115
+ # Accept either a bare list of items or a full import request object.
116
+ if isinstance(body, list):
117
+ request: Dict[str, object] = {"items": list(body)}
118
+ elif isinstance(body, Mapping):
119
+ request = dict(body)
120
+ else:
121
+ raise ValidationError(
122
+ 'audience import body must be a JSON array of items, or an object with an "items" array'
123
+ )
124
+ items = request.get("items")
125
+ if not isinstance(items, list) or not items:
126
+ raise ValidationError('audience import requires a non-empty "items" array')
127
+ if args.operation_id:
128
+ request["operation_id"] = args.operation_id
129
+ if not str(request.get("operation_id", "") or "").strip():
130
+ request["operation_id"] = f"cli-import-{uuid.uuid4().hex[:16]}"
131
+ if args.no_update_existing:
132
+ request["update_existing"] = False
133
+ elif "update_existing" not in request:
134
+ request["update_existing"] = True
135
+ payload = context.campaigns.import_audience_members(campaign_id, request)
136
+ context.renderer.print_json(payload)
137
+
138
+
139
+ def handle_campaign_audience_list(context: RuntimeContext, args: argparse.Namespace) -> None:
140
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
141
+ params: Dict[str, object] = {}
142
+ if args.lifecycle_status:
143
+ params["lifecycle_status"] = args.lifecycle_status
144
+ if args.phone:
145
+ params["target_phone_e164"] = args.phone
146
+ if args.cursor:
147
+ params["cursor"] = args.cursor
148
+ if args.limit:
149
+ params["limit"] = args.limit
150
+ payload = context.campaigns.list_audience_members(campaign_id, params)
151
+ context.renderer.print_json(payload)
152
+
153
+
154
+ def handle_campaign_audience_queue_eligible(
155
+ context: RuntimeContext, args: argparse.Namespace
156
+ ) -> None:
157
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
158
+ body: Dict[str, object] = {"operation_id": args.operation_id or str(uuid.uuid4())}
159
+ if args.limit:
160
+ body["limit"] = args.limit
161
+ if args.include_line_type_lookup:
162
+ body["include_line_type_lookup"] = True
163
+ payload = context.campaigns.queue_eligible_audience_members(campaign_id, body)
164
+ context.renderer.print_json(payload)
165
+
166
+
167
+ def handle_compliance_get(context: RuntimeContext, args: argparse.Namespace) -> None:
168
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
169
+ payload = context.campaigns.get_compliance(campaign_id)
170
+ context.renderer.print_json(payload)
171
+
172
+
173
+ _COMPLIANCE_DEFAULTS: Dict[str, object] = {
174
+ "policy_region": "US",
175
+ "compliance_mode": "enforced",
176
+ "call_purpose": "support",
177
+ "recipient_type": "consumer",
178
+ }
179
+
180
+
181
+ def _apply_compliance_flags(body: Dict[str, object], args: argparse.Namespace) -> Dict[str, object]:
182
+ if getattr(args, "compliance_mode", None):
183
+ body["compliance_mode"] = args.compliance_mode
184
+ if getattr(args, "ai_notice", None) is not None:
185
+ body["automated_voice_notice_required"] = args.ai_notice
186
+ if getattr(args, "opt_out", None) is not None:
187
+ body["spoken_opt_out_enabled"] = args.opt_out
188
+ if getattr(args, "recording_notice", None) is not None:
189
+ body["recording_notice_required"] = args.recording_notice
190
+ return body
191
+
192
+
193
+ def handle_compliance_set(context: RuntimeContext, args: argparse.Namespace) -> None:
194
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
195
+ base = parse_json_or_file(args.config_json, args.config_file, "compliance settings")
196
+ if base is None:
197
+ body: Dict[str, object] = dict(_COMPLIANCE_DEFAULTS)
198
+ elif isinstance(base, Mapping):
199
+ body = dict(base)
200
+ else:
201
+ raise ValidationError(
202
+ "compliance settings must be a JSON object (CampaignComplianceSettings)"
203
+ )
204
+ _apply_compliance_flags(body, args)
205
+ payload = context.campaigns.save_compliance(campaign_id, body)
206
+ context.renderer.print_json(payload)
207
+
208
+
209
+ def handle_compliance_approve(context: RuntimeContext, args: argparse.Namespace) -> None:
210
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
211
+ payload = context.campaigns.approve_compliance(campaign_id)
212
+ context.renderer.print_json(payload)
213
+
214
+
215
+ def handle_compliance_disable(context: RuntimeContext, args: argparse.Namespace) -> None:
216
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
217
+ payload = context.campaigns.disable_compliance(campaign_id)
218
+ context.renderer.print_json(payload)
219
+
220
+
221
+ def handle_campaign_runs_list(context: RuntimeContext, args: argparse.Namespace) -> None:
222
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
223
+ params: Dict[str, object] = {}
224
+ if args.status:
225
+ params["status"] = args.status
226
+ if args.cursor:
227
+ params["cursor"] = args.cursor
228
+ if args.limit:
229
+ params["limit"] = args.limit
230
+ payload = context.campaigns.list_runs(campaign_id, params)
231
+ context.renderer.print_json(payload)
232
+
233
+
234
+ def handle_campaign_run_get(context: RuntimeContext, args: argparse.Namespace) -> None:
235
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
236
+ run_id = require_non_empty(args.run_id, "run_id")
237
+ context.renderer.print_json(context.campaigns.get_run(campaign_id, run_id))
238
+
239
+
240
+ def handle_campaign_run_cancel(context: RuntimeContext, args: argparse.Namespace) -> None:
241
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
242
+ run_id = require_non_empty(args.run_id, "run_id")
243
+ context.renderer.print_json(context.campaigns.cancel_run(campaign_id, run_id))
244
+
245
+
246
+ def handle_campaign_results_list(context: RuntimeContext, args: argparse.Namespace) -> None:
247
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
248
+ params: Dict[str, object] = {}
249
+ for name in ("status", "campaign_run_id", "schema_name", "limit"):
250
+ value = getattr(args, name, None)
251
+ if value is not None:
252
+ params[name] = value
253
+ context.renderer.print_json(context.campaigns.list_results(campaign_id, params))
254
+
255
+
256
+ def handle_campaign_result_get(context: RuntimeContext, args: argparse.Namespace) -> None:
257
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
258
+ result_id = require_non_empty(args.result_id, "result_id")
259
+ context.renderer.print_json(context.campaigns.get_result(campaign_id, result_id))
260
+
261
+
262
+ def _compliance_check_body(args: argparse.Namespace, label: str) -> Dict[str, object]:
263
+ body = parse_json_or_file(args.config_json, args.config_file, label)
264
+ if body is None:
265
+ return {}
266
+ if not isinstance(body, Mapping):
267
+ raise ValidationError(f"{label} must be a JSON object")
268
+ return dict(body)
269
+
270
+
271
+ def handle_compliance_preview(context: RuntimeContext, args: argparse.Namespace) -> None:
272
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
273
+ body = _compliance_check_body(args, "compliance preview")
274
+ context.renderer.print_json(context.campaigns.preview_compliance(campaign_id, body))
275
+
276
+
277
+ def handle_compliance_preflight(context: RuntimeContext, args: argparse.Namespace) -> None:
278
+ campaign_id = require_non_empty(args.campaign_id, "campaign_id")
279
+ body = _compliance_check_body(args, "compliance preflight")
280
+ context.renderer.print_json(context.campaigns.preflight_compliance(campaign_id, body))
281
+
282
+
283
+ def _add_campaign_list(sub: argparse._SubParsersAction) -> None:
284
+ parser = sub.add_parser("list", help="List campaigns")
285
+ parser.add_argument(
286
+ "--status", help="Filter by status (DRAFT, ACTIVE, PAUSED, ARCHIVED, COMPLETED)"
287
+ )
288
+ parser.add_argument("--search", help="Substring search on campaign name")
289
+ parser.add_argument("--limit", type=int)
290
+ parser.set_defaults(handler=handle_campaign_list)
291
+
292
+
293
+ def _add_campaign_get(sub: argparse._SubParsersAction) -> None:
294
+ parser = sub.add_parser("get", help="Fetch a campaign definition")
295
+ parser.add_argument("campaign_id")
296
+ parser.set_defaults(handler=handle_campaign_get)
297
+
298
+
299
+ def _add_campaign_preview(sub: argparse._SubParsersAction) -> None:
300
+ parser = sub.add_parser("preview", help="Validate and preview a campaign definition")
301
+ parser.add_argument("--config-json", dest="config_json", help="Inline JSON body")
302
+ parser.add_argument("--config-file", dest="config_file", help="Path to JSON body")
303
+ parser.set_defaults(handler=handle_campaign_preview)
304
+
305
+
306
+ def _add_campaign_create(sub: argparse._SubParsersAction) -> None:
307
+ parser = sub.add_parser(
308
+ "create",
309
+ help="Create a campaign from JSON (matches CampaignCreate schema)",
310
+ )
311
+ parser.add_argument("--name", help="Convenience override for the campaign name")
312
+ parser.add_argument("--config-json", dest="config_json", help="Inline JSON body")
313
+ parser.add_argument("--config-file", dest="config_file", help="Path to JSON file with body")
314
+ parser.set_defaults(handler=handle_campaign_create)
315
+
316
+
317
+ def _add_campaign_update(sub: argparse._SubParsersAction) -> None:
318
+ parser = sub.add_parser("update", help="Patch a campaign (partial CampaignUpdate body)")
319
+ parser.add_argument("campaign_id")
320
+ parser.add_argument("--config-json", dest="config_json", help="Inline JSON partial body")
321
+ parser.add_argument(
322
+ "--config-file", dest="config_file", help="Path to JSON file with partial body"
323
+ )
324
+ parser.set_defaults(handler=handle_campaign_update)
325
+
326
+
327
+ def _add_campaign_readiness(sub: argparse._SubParsersAction) -> None:
328
+ parser = sub.add_parser("readiness", help="Check whether a campaign is ready to activate")
329
+ parser.add_argument("campaign_id")
330
+ parser.set_defaults(handler=handle_campaign_readiness)
331
+
332
+
333
+ def _add_campaign_activate(sub: argparse._SubParsersAction) -> None:
334
+ parser = sub.add_parser("activate", help="Transition a campaign to ACTIVE")
335
+ parser.add_argument("campaign_id")
336
+ parser.set_defaults(handler=handle_campaign_activate)
337
+
338
+
339
+ def _add_campaign_pause(sub: argparse._SubParsersAction) -> None:
340
+ parser = sub.add_parser("pause", help="Pause a running campaign")
341
+ parser.add_argument("campaign_id")
342
+ parser.set_defaults(handler=handle_campaign_pause)
343
+
344
+
345
+ def _add_campaign_archive(sub: argparse._SubParsersAction) -> None:
346
+ parser = sub.add_parser("archive", help="Archive a campaign")
347
+ parser.add_argument("campaign_id")
348
+ parser.set_defaults(handler=handle_campaign_archive)
349
+
350
+
351
+ def _add_campaign_run(sub: argparse._SubParsersAction) -> None:
352
+ parser = sub.add_parser("run", help="Trigger a manual outbound run for a campaign")
353
+ parser.add_argument("campaign_id")
354
+ parser.add_argument("--metadata", help="Inline JSON object passed as run metadata")
355
+ parser.add_argument(
356
+ "--audience-override",
357
+ dest="audience_override",
358
+ help="Inline JSON CampaignAudienceDefinition to override the campaign audience for this run",
359
+ )
360
+ parser.set_defaults(handler=handle_campaign_run)
361
+
362
+
363
+ def _add_campaign_audience_import(sub: argparse._SubParsersAction) -> None:
364
+ parser = sub.add_parser(
365
+ "import",
366
+ help="Import audience members (supports per-target campaign_metadata)",
367
+ )
368
+ parser.add_argument("campaign_id")
369
+ parser.add_argument(
370
+ "--config-json",
371
+ dest="config_json",
372
+ help="Inline JSON: an array of import items, or {operation_id, update_existing, items[]}",
373
+ )
374
+ parser.add_argument(
375
+ "--config-file",
376
+ dest="config_file",
377
+ help="Path to a JSON file with the same shape as --config-json",
378
+ )
379
+ parser.add_argument(
380
+ "--operation-id",
381
+ dest="operation_id",
382
+ help="Idempotency batch id (re-using one reprocesses that batch); generated if omitted",
383
+ )
384
+ parser.add_argument(
385
+ "--no-update-existing",
386
+ dest="no_update_existing",
387
+ action="store_true",
388
+ help="Preserve existing members instead of updating pending ones (default: update)",
389
+ )
390
+ parser.set_defaults(handler=handle_campaign_audience_import)
391
+
392
+
393
+ def _add_campaign_audience_list(sub: argparse._SubParsersAction) -> None:
394
+ parser = sub.add_parser("list", help="List a campaign's audience members")
395
+ parser.add_argument("campaign_id")
396
+ parser.add_argument("--lifecycle-status", dest="lifecycle_status")
397
+ parser.add_argument("--phone", help="Filter by target phone (E.164)")
398
+ parser.add_argument("--cursor")
399
+ parser.add_argument("--limit", type=int)
400
+ parser.set_defaults(handler=handle_campaign_audience_list)
401
+
402
+
403
+ def _add_campaign_audience_queue_eligible(sub: argparse._SubParsersAction) -> None:
404
+ parser = sub.add_parser(
405
+ "queue-eligible",
406
+ help="Queue eligible imported members for dialing — this is what actually dials members WITH their campaign_metadata (campaign must be active)",
407
+ )
408
+ parser.add_argument("campaign_id")
409
+ parser.add_argument(
410
+ "--limit", type=int, help="Max members to queue this call (1-200, default 50)"
411
+ )
412
+ parser.add_argument(
413
+ "--operation-id", dest="operation_id", help="Idempotency id (generated if omitted)"
414
+ )
415
+ parser.add_argument(
416
+ "--include-line-type-lookup",
417
+ dest="include_line_type_lookup",
418
+ action="store_true",
419
+ help="Run a line-type lookup on each number before queueing",
420
+ )
421
+ parser.set_defaults(handler=handle_campaign_audience_queue_eligible)
422
+
423
+
424
+ def _add_compliance_get(sub: argparse._SubParsersAction) -> None:
425
+ parser = sub.add_parser("get", help="Fetch campaign compliance settings")
426
+ parser.add_argument("campaign_id")
427
+ parser.set_defaults(handler=handle_compliance_get)
428
+
429
+
430
+ def _add_compliance_set(sub: argparse._SubParsersAction) -> None:
431
+ parser = sub.add_parser(
432
+ "set",
433
+ help="Save compliance settings. Without --config-* a sensible default body is used; shorthand flags override.",
434
+ )
435
+ parser.add_argument("campaign_id")
436
+ parser.add_argument("--config-json", dest="config_json", help="Inline JSON body")
437
+ parser.add_argument("--config-file", dest="config_file", help="Path to JSON file with body")
438
+ parser.add_argument(
439
+ "--compliance-mode",
440
+ dest="compliance_mode",
441
+ choices=["enforced", "advisory"],
442
+ help="Set compliance_mode",
443
+ )
444
+ parser.add_argument(
445
+ "--ai-notice",
446
+ dest="ai_notice",
447
+ type=_parse_bool_flag,
448
+ metavar="on|off",
449
+ help="Toggle automated_voice_notice_required",
450
+ )
451
+ parser.add_argument(
452
+ "--opt-out",
453
+ dest="opt_out",
454
+ type=_parse_bool_flag,
455
+ metavar="on|off",
456
+ help="Toggle spoken_opt_out_enabled",
457
+ )
458
+ parser.add_argument(
459
+ "--recording-notice",
460
+ dest="recording_notice",
461
+ type=_parse_bool_flag,
462
+ metavar="on|off",
463
+ help="Toggle recording_notice_required",
464
+ )
465
+ parser.set_defaults(handler=handle_compliance_set)
466
+
467
+
468
+ def _add_compliance_approve(sub: argparse._SubParsersAction) -> None:
469
+ parser = sub.add_parser("approve", help="Approve compliance settings (required before dialing)")
470
+ parser.add_argument("campaign_id")
471
+ parser.set_defaults(handler=handle_compliance_approve)
472
+
473
+
474
+ def _add_compliance_disable(sub: argparse._SubParsersAction) -> None:
475
+ parser = sub.add_parser("disable", help="Disable compliance enforcement for a campaign")
476
+ parser.add_argument("campaign_id")
477
+ parser.set_defaults(handler=handle_compliance_disable)
478
+
479
+
480
+ def _add_compliance_preview(sub: argparse._SubParsersAction) -> None:
481
+ parser = sub.add_parser("preview", help="Preview compliance decisions without dialing")
482
+ parser.add_argument("campaign_id")
483
+ parser.add_argument("--config-json", dest="config_json", help="Inline preview request")
484
+ parser.add_argument("--config-file", dest="config_file", help="Path to preview request")
485
+ parser.set_defaults(handler=handle_compliance_preview)
486
+
487
+
488
+ def _add_compliance_preflight(sub: argparse._SubParsersAction) -> None:
489
+ parser = sub.add_parser("preflight", help="Run the campaign compliance preflight")
490
+ parser.add_argument("campaign_id")
491
+ parser.add_argument("--config-json", dest="config_json", help="Inline preflight request")
492
+ parser.add_argument("--config-file", dest="config_file", help="Path to preflight request")
493
+ parser.set_defaults(handler=handle_compliance_preflight)
494
+
495
+
496
+ def _add_campaign_runs_list(sub: argparse._SubParsersAction) -> None:
497
+ parser = sub.add_parser("runs", help="List runs for a campaign")
498
+ parser.add_argument("campaign_id")
499
+ parser.add_argument("--status")
500
+ parser.add_argument("--cursor")
501
+ parser.add_argument("--limit", type=int)
502
+ parser.set_defaults(handler=handle_campaign_runs_list)
503
+
504
+
505
+ def _add_campaign_run_get(sub: argparse._SubParsersAction) -> None:
506
+ parser = sub.add_parser("run-get", help="Fetch a campaign run")
507
+ parser.add_argument("campaign_id")
508
+ parser.add_argument("run_id")
509
+ parser.set_defaults(handler=handle_campaign_run_get)
510
+
511
+
512
+ def _add_campaign_run_cancel(sub: argparse._SubParsersAction) -> None:
513
+ parser = sub.add_parser("run-cancel", help="Cancel a campaign run")
514
+ parser.add_argument("campaign_id")
515
+ parser.add_argument("run_id")
516
+ parser.set_defaults(handler=handle_campaign_run_cancel)
517
+
518
+
519
+ def _add_campaign_results(sub: argparse._SubParsersAction) -> None:
520
+ parser = sub.add_parser("results", help="Structured post-call extraction results")
521
+ results_sub = parser.add_subparsers(dest="results_command", required=True)
522
+ list_parser = results_sub.add_parser("list", help="List campaign results")
523
+ list_parser.add_argument("campaign_id")
524
+ list_parser.add_argument("--status")
525
+ list_parser.add_argument("--campaign-run-id", dest="campaign_run_id")
526
+ list_parser.add_argument("--schema-name", dest="schema_name")
527
+ list_parser.add_argument("--limit", type=int)
528
+ list_parser.set_defaults(handler=handle_campaign_results_list)
529
+ get_parser = results_sub.add_parser("get", help="Fetch one campaign result")
530
+ get_parser.add_argument("campaign_id")
531
+ get_parser.add_argument("result_id")
532
+ get_parser.set_defaults(handler=handle_campaign_result_get)
533
+
534
+
535
+ def register(subparsers: argparse._SubParsersAction) -> None:
536
+ campaigns = subparsers.add_parser("campaigns", help="Voice campaign operations")
537
+ camp_sub = campaigns.add_subparsers(dest="campaigns_command", required=True)
538
+ _add_campaign_list(camp_sub)
539
+ _add_campaign_get(camp_sub)
540
+ _add_campaign_preview(camp_sub)
541
+ _add_campaign_create(camp_sub)
542
+ _add_campaign_update(camp_sub)
543
+ _add_campaign_readiness(camp_sub)
544
+ _add_campaign_activate(camp_sub)
545
+ _add_campaign_pause(camp_sub)
546
+ _add_campaign_archive(camp_sub)
547
+ _add_campaign_run(camp_sub)
548
+ _add_campaign_runs_list(camp_sub)
549
+ _add_campaign_run_get(camp_sub)
550
+ _add_campaign_run_cancel(camp_sub)
551
+ _add_campaign_results(camp_sub)
552
+ add_campaign_launch_parser(camp_sub)
553
+
554
+ add_campaign_launch_parser(subparsers, command_name="call")
555
+
556
+ compliance = camp_sub.add_parser("compliance", help="Campaign compliance settings")
557
+ comp_sub = compliance.add_subparsers(dest="compliance_command", required=True)
558
+ _add_compliance_get(comp_sub)
559
+ _add_compliance_set(comp_sub)
560
+ _add_compliance_approve(comp_sub)
561
+ _add_compliance_disable(comp_sub)
562
+ _add_compliance_preview(comp_sub)
563
+ _add_compliance_preflight(comp_sub)
564
+
565
+ audience = camp_sub.add_parser(
566
+ "audience", help="Campaign audience members (import with per-target metadata)"
567
+ )
568
+ aud_sub = audience.add_subparsers(dest="audience_command", required=True)
569
+ _add_campaign_audience_import(aud_sub)
570
+ _add_campaign_audience_list(aud_sub)
571
+ _add_campaign_audience_queue_eligible(aud_sub)
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from guzli.errors import ValidationError
7
+ from guzli.runtime import RuntimeContext
8
+ from guzli.validators import parse_bool
9
+
10
+
11
+ def add_json_flag(parser: argparse.ArgumentParser) -> None:
12
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
13
+
14
+
15
+ def parse_bool_flag(value: str) -> bool:
16
+ parsed = parse_bool(value, "boolean flag")
17
+ if parsed is None:
18
+ raise ValidationError("Boolean flag is required")
19
+ return parsed
20
+
21
+
22
+ def require_org_id(context: RuntimeContext, args: argparse.Namespace) -> str:
23
+ organization_id = getattr(args, "organization_id_arg", None) or context.organization_id
24
+ if not organization_id:
25
+ raise ValidationError(
26
+ "organization-id required: pass --org / --organization-id, set "
27
+ "GUZLI_ORGANIZATION_ID, or store it in your profile (guzli auth use ...)."
28
+ )
29
+ return organization_id
30
+
31
+
32
+ def add_org_arg(parser: argparse.ArgumentParser) -> None:
33
+ parser.add_argument(
34
+ "--org",
35
+ "--organization-id",
36
+ dest="organization_id_arg",
37
+ help="Organization UUID (falls back to GUZLI_ORGANIZATION_ID / active profile)",
38
+ )
39
+
40
+
41
+ def print_error(message: str) -> None:
42
+ print(message, file=sys.stderr)