wxcli 1.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.
- wxcli/__init__.py +4 -0
- wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
- wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
- wxcli/_playbook/.claude/rules/cleanup.md +29 -0
- wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
- wxcli/_playbook/.claude/rules/org-health.md +14 -0
- wxcli/_playbook/.claude/settings.json +8 -0
- wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
- wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
- wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
- wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
- wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
- wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
- wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
- wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
- wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
- wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
- wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
- wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
- wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
- wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
- wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
- wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
- wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
- wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
- wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
- wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
- wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
- wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
- wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
- wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
- wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
- wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
- wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
- wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
- wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
- wxcli/_playbook/CLAUDE.md +373 -0
- wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
- wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
- wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
- wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
- wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
- wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
- wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
- wxcli/_playbook/docs/reference/admin-partner.md +357 -0
- wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
- wxcli/_playbook/docs/reference/authentication.md +965 -0
- wxcli/_playbook/docs/reference/call-control.md +1205 -0
- wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
- wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
- wxcli/_playbook/docs/reference/call-routing.md +2304 -0
- wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
- wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
- wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
- wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
- wxcli/_playbook/docs/reference/devices-core.md +1869 -0
- wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
- wxcli/_playbook/docs/reference/devices-platform.md +467 -0
- wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
- wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
- wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
- wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
- wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
- wxcli/_playbook/docs/reference/meetings-content.md +473 -0
- wxcli/_playbook/docs/reference/meetings-core.md +903 -0
- wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
- wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
- wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
- wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
- wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
- wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
- wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
- wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
- wxcli/_playbook/docs/reference/provisioning.md +1366 -0
- wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
- wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
- wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
- wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
- wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
- wxcli/_version.py +24 -0
- wxcli/auth.py +137 -0
- wxcli/commands/__init__.py +0 -0
- wxcli/commands/_registry.py +181 -0
- wxcli/commands/activation_email.py +103 -0
- wxcli/commands/admin_recordings.py +585 -0
- wxcli/commands/analytics.py +105 -0
- wxcli/commands/announcement_playlists.py +219 -0
- wxcli/commands/announcements.py +519 -0
- wxcli/commands/archive_users.py +36 -0
- wxcli/commands/attachment_actions.py +60 -0
- wxcli/commands/audit_events.py +83 -0
- wxcli/commands/authorizations.py +113 -0
- wxcli/commands/auto_attendant.py +642 -0
- wxcli/commands/broadworks_billing_reports.py +126 -0
- wxcli/commands/broadworks_enterprises.py +160 -0
- wxcli/commands/broadworks_subscribers.py +319 -0
- wxcli/commands/broadworks_workspaces.py +108 -0
- wxcli/commands/call_controls.py +1304 -0
- wxcli/commands/call_park.py +494 -0
- wxcli/commands/call_pickup.py +229 -0
- wxcli/commands/call_queue.py +1878 -0
- wxcli/commands/call_recording.py +707 -0
- wxcli/commands/call_routing.py +1610 -0
- wxcli/commands/call_settings_for_me_phase_5.py +212 -0
- wxcli/commands/caller_reputation.py +169 -0
- wxcli/commands/calling_service.py +305 -0
- wxcli/commands/cc_address_book.py +770 -0
- wxcli/commands/cc_agent_greetings.py +242 -0
- wxcli/commands/cc_agent_summaries.py +39 -0
- wxcli/commands/cc_agent_wellbeing.py +216 -0
- wxcli/commands/cc_agents.py +425 -0
- wxcli/commands/cc_ai_assistant.py +55 -0
- wxcli/commands/cc_ai_feature.py +294 -0
- wxcli/commands/cc_audio_files.py +149 -0
- wxcli/commands/cc_auto_csat.py +311 -0
- wxcli/commands/cc_aux_code.py +458 -0
- wxcli/commands/cc_business_hour.py +353 -0
- wxcli/commands/cc_call_monitoring.py +228 -0
- wxcli/commands/cc_callbacks.py +194 -0
- wxcli/commands/cc_campaign.py +212 -0
- wxcli/commands/cc_captures.py +39 -0
- wxcli/commands/cc_contact_list.py +169 -0
- wxcli/commands/cc_contact_number.py +295 -0
- wxcli/commands/cc_data_sources.py +235 -0
- wxcli/commands/cc_desktop_layout.py +380 -0
- wxcli/commands/cc_desktop_profile.py +322 -0
- wxcli/commands/cc_dial_number.py +511 -0
- wxcli/commands/cc_dial_plan.py +347 -0
- wxcli/commands/cc_dnc.py +101 -0
- wxcli/commands/cc_entry_point.py +545 -0
- wxcli/commands/cc_ewt.py +51 -0
- wxcli/commands/cc_flow.py +477 -0
- wxcli/commands/cc_global_vars.py +430 -0
- wxcli/commands/cc_holiday_list.py +335 -0
- wxcli/commands/cc_journey.py +1261 -0
- wxcli/commands/cc_legacy_flows.py +49 -0
- wxcli/commands/cc_multimedia_profile.py +436 -0
- wxcli/commands/cc_notification.py +51 -0
- wxcli/commands/cc_outdial_ani.py +604 -0
- wxcli/commands/cc_overrides.py +341 -0
- wxcli/commands/cc_queue.py +1083 -0
- wxcli/commands/cc_queue_stats.py +55 -0
- wxcli/commands/cc_realtime.py +51 -0
- wxcli/commands/cc_resource_collection.py +312 -0
- wxcli/commands/cc_search.py +46 -0
- wxcli/commands/cc_site.py +400 -0
- wxcli/commands/cc_skill.py +350 -0
- wxcli/commands/cc_skill_profile.py +289 -0
- wxcli/commands/cc_subscriptions.py +426 -0
- wxcli/commands/cc_summaries.py +130 -0
- wxcli/commands/cc_tasks.py +844 -0
- wxcli/commands/cc_team.py +325 -0
- wxcli/commands/cc_user_profiles.py +349 -0
- wxcli/commands/cc_users.py +701 -0
- wxcli/commands/cc_work_types.py +363 -0
- wxcli/commands/cdr.py +89 -0
- wxcli/commands/classifications.py +38 -0
- wxcli/commands/cleanup.py +1427 -0
- wxcli/commands/client_settings.py +72 -0
- wxcli/commands/conference.py +328 -0
- wxcli/commands/configure.py +72 -0
- wxcli/commands/converged_recordings.py +347 -0
- wxcli/commands/converged_recordings_export.py +310 -0
- wxcli/commands/cq_playlists.py +42 -0
- wxcli/commands/cucm.py +2794 -0
- wxcli/commands/cucm_config.py +129 -0
- wxcli/commands/customer_assist.py +373 -0
- wxcli/commands/data_sources.py +227 -0
- wxcli/commands/dect_devices.py +787 -0
- wxcli/commands/device_configurations.py +77 -0
- wxcli/commands/device_dynamic_settings.py +320 -0
- wxcli/commands/device_settings.py +1489 -0
- wxcli/commands/devices.py +280 -0
- wxcli/commands/domains.py +142 -0
- wxcli/commands/ecm.py +178 -0
- wxcli/commands/emergency_services.py +827 -0
- wxcli/commands/events.py +83 -0
- wxcli/commands/external_voicemail.py +52 -0
- wxcli/commands/groups.py +206 -0
- wxcli/commands/guest_management.py +76 -0
- wxcli/commands/hds.py +340 -0
- wxcli/commands/hot_desk.py +67 -0
- wxcli/commands/hot_desking_members.py +118 -0
- wxcli/commands/hot_desking_portal.py +124 -0
- wxcli/commands/hunt_group.py +577 -0
- wxcli/commands/hybrid_clusters.py +71 -0
- wxcli/commands/hybrid_connectors.py +67 -0
- wxcli/commands/identity_org.py +159 -0
- wxcli/commands/init_playbook.py +155 -0
- wxcli/commands/licenses.py +106 -0
- wxcli/commands/live_monitoring.py +40 -0
- wxcli/commands/location_call_handling.py +577 -0
- wxcli/commands/location_schedules.py +341 -0
- wxcli/commands/location_settings.py +1297 -0
- wxcli/commands/location_voicemail.py +494 -0
- wxcli/commands/locations.py +336 -0
- wxcli/commands/meeting_captions.py +110 -0
- wxcli/commands/meeting_chats.py +63 -0
- wxcli/commands/meeting_invitees.py +234 -0
- wxcli/commands/meeting_messages.py +28 -0
- wxcli/commands/meeting_participants.py +278 -0
- wxcli/commands/meeting_polls.py +111 -0
- wxcli/commands/meeting_preferences.py +515 -0
- wxcli/commands/meeting_qa.py +78 -0
- wxcli/commands/meeting_qualities.py +43 -0
- wxcli/commands/meeting_reports.py +101 -0
- wxcli/commands/meeting_session_types.py +105 -0
- wxcli/commands/meeting_site.py +57 -0
- wxcli/commands/meeting_slido.py +44 -0
- wxcli/commands/meeting_summaries.py +92 -0
- wxcli/commands/meeting_tracking_codes.py +233 -0
- wxcli/commands/meeting_transcripts.py +230 -0
- wxcli/commands/meetings.py +1997 -0
- wxcli/commands/memberships.py +164 -0
- wxcli/commands/messages.py +213 -0
- wxcli/commands/mode_management.py +244 -0
- wxcli/commands/my_call_settings.py +3577 -0
- wxcli/commands/numbers.py +405 -0
- wxcli/commands/operating_modes.py +410 -0
- wxcli/commands/org_contacts.py +284 -0
- wxcli/commands/org_health_cli.py +55 -0
- wxcli/commands/org_settings.py +71 -0
- wxcli/commands/organizations.py +83 -0
- wxcli/commands/paging_group.py +257 -0
- wxcli/commands/partner_admins.py +141 -0
- wxcli/commands/partner_reports.py +188 -0
- wxcli/commands/partner_tags.py +216 -0
- wxcli/commands/people.py +279 -0
- wxcli/commands/person_call_settings.py +71 -0
- wxcli/commands/pstn.py +280 -0
- wxcli/commands/recording_report.py +154 -0
- wxcli/commands/report_templates.py +38 -0
- wxcli/commands/reports.py +144 -0
- wxcli/commands/resource_group_memberships.py +160 -0
- wxcli/commands/resource_groups.py +67 -0
- wxcli/commands/roles.py +63 -0
- wxcli/commands/room_tabs.py +160 -0
- wxcli/commands/rooms.py +225 -0
- wxcli/commands/scim_bulk.py +46 -0
- wxcli/commands/scim_groups.py +246 -0
- wxcli/commands/scim_schemas.py +82 -0
- wxcli/commands/scim_users.py +283 -0
- wxcli/commands/security_audit.py +54 -0
- wxcli/commands/service_apps.py +51 -0
- wxcli/commands/single_number_reach.py +209 -0
- wxcli/commands/team_memberships.py +155 -0
- wxcli/commands/teams.py +153 -0
- wxcli/commands/ucm_profile.py +42 -0
- wxcli/commands/update.py +219 -0
- wxcli/commands/user_settings.py +5116 -0
- wxcli/commands/video_mesh.py +1090 -0
- wxcli/commands/virtual_extensions.py +502 -0
- wxcli/commands/virtual_line_settings.py +2072 -0
- wxcli/commands/webhooks.py +176 -0
- wxcli/commands/wholesale_billing_reports.py +147 -0
- wxcli/commands/wholesale_provisioning.py +523 -0
- wxcli/commands/workspace_locations.py +339 -0
- wxcli/commands/workspace_metrics.py +103 -0
- wxcli/commands/workspace_personalization.py +70 -0
- wxcli/commands/workspace_settings.py +3347 -0
- wxcli/commands/workspaces.py +279 -0
- wxcli/commands/xapi.py +114 -0
- wxcli/config.py +139 -0
- wxcli/errors.py +77 -0
- wxcli/main.py +210 -0
- wxcli/migration/CLAUDE.md +106 -0
- wxcli/migration/__init__.py +1 -0
- wxcli/migration/advisory/CLAUDE.md +196 -0
- wxcli/migration/advisory/__init__.py +46 -0
- wxcli/migration/advisory/advisor.py +107 -0
- wxcli/migration/advisory/advisory_patterns.py +2552 -0
- wxcli/migration/advisory/recommendation_rules.py +835 -0
- wxcli/migration/cucm/CLAUDE.md +74 -0
- wxcli/migration/cucm/__init__.py +5 -0
- wxcli/migration/cucm/connection.py +265 -0
- wxcli/migration/cucm/discovery.py +225 -0
- wxcli/migration/cucm/extractors/__init__.py +1 -0
- wxcli/migration/cucm/extractors/announcements.py +92 -0
- wxcli/migration/cucm/extractors/base.py +90 -0
- wxcli/migration/cucm/extractors/device_profiles.py +98 -0
- wxcli/migration/cucm/extractors/devices.py +193 -0
- wxcli/migration/cucm/extractors/e911.py +103 -0
- wxcli/migration/cucm/extractors/features.py +309 -0
- wxcli/migration/cucm/extractors/helpers.py +73 -0
- wxcli/migration/cucm/extractors/informational.py +295 -0
- wxcli/migration/cucm/extractors/locations.py +221 -0
- wxcli/migration/cucm/extractors/moh.py +100 -0
- wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
- wxcli/migration/cucm/extractors/routing.py +493 -0
- wxcli/migration/cucm/extractors/shared_lines.py +146 -0
- wxcli/migration/cucm/extractors/templates.py +229 -0
- wxcli/migration/cucm/extractors/tier4.py +173 -0
- wxcli/migration/cucm/extractors/users.py +240 -0
- wxcli/migration/cucm/extractors/voicemail.py +243 -0
- wxcli/migration/cucm/extractors/workspaces.py +88 -0
- wxcli/migration/cucm/unity_connection.py +304 -0
- wxcli/migration/execute/CLAUDE.md +372 -0
- wxcli/migration/execute/__init__.py +303 -0
- wxcli/migration/execute/batch.py +277 -0
- wxcli/migration/execute/dependency.py +483 -0
- wxcli/migration/execute/engine.py +964 -0
- wxcli/migration/execute/handlers.py +2232 -0
- wxcli/migration/execute/planner.py +1939 -0
- wxcli/migration/execute/runtime.py +571 -0
- wxcli/migration/export/CLAUDE.md +49 -0
- wxcli/migration/export/__init__.py +8 -0
- wxcli/migration/export/csv_export.py +126 -0
- wxcli/migration/export/deployment_plan.py +479 -0
- wxcli/migration/export/json_export.py +60 -0
- wxcli/migration/models.py +941 -0
- wxcli/migration/phone_models.py +50 -0
- wxcli/migration/preflight/CLAUDE.md +52 -0
- wxcli/migration/preflight/__init__.py +89 -0
- wxcli/migration/preflight/checks.py +911 -0
- wxcli/migration/preflight/runner.py +293 -0
- wxcli/migration/rate_limiter.py +144 -0
- wxcli/migration/report/CLAUDE.md +245 -0
- wxcli/migration/report/__init__.py +7 -0
- wxcli/migration/report/appendix.py +2437 -0
- wxcli/migration/report/assembler.py +183 -0
- wxcli/migration/report/charts.py +263 -0
- wxcli/migration/report/executive.py +776 -0
- wxcli/migration/report/explainer.py +704 -0
- wxcli/migration/report/helpers.py +74 -0
- wxcli/migration/report/ingest.py +152 -0
- wxcli/migration/report/notice_templates.py +192 -0
- wxcli/migration/report/score.py +375 -0
- wxcli/migration/report/styles.py +1052 -0
- wxcli/migration/report/user_diff.py +1052 -0
- wxcli/migration/report/user_notice.py +425 -0
- wxcli/migration/state.py +139 -0
- wxcli/migration/store.py +860 -0
- wxcli/migration/transform/CLAUDE.md +107 -0
- wxcli/migration/transform/__init__.py +12 -0
- wxcli/migration/transform/analysis_pipeline.py +453 -0
- wxcli/migration/transform/analyzers/__init__.py +142 -0
- wxcli/migration/transform/analyzers/css_permission.py +189 -0
- wxcli/migration/transform/analyzers/css_routing.py +335 -0
- wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
- wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
- wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
- wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
- wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
- wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
- wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
- wxcli/migration/transform/analyzers/missing_data.py +300 -0
- wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
- wxcli/migration/transform/analyzers/shared_line.py +170 -0
- wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
- wxcli/migration/transform/analyzers/workspace_license.py +212 -0
- wxcli/migration/transform/cross_reference.py +1136 -0
- wxcli/migration/transform/cucm_pattern.py +246 -0
- wxcli/migration/transform/decisions.py +301 -0
- wxcli/migration/transform/e164.py +145 -0
- wxcli/migration/transform/engine.py +256 -0
- wxcli/migration/transform/mappers/CLAUDE.md +123 -0
- wxcli/migration/transform/mappers/__init__.py +76 -0
- wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
- wxcli/migration/transform/mappers/base.py +190 -0
- wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
- wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
- wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
- wxcli/migration/transform/mappers/css_mapper.py +868 -0
- wxcli/migration/transform/mappers/dect_mapper.py +367 -0
- wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
- wxcli/migration/transform/mappers/device_mapper.py +227 -0
- wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
- wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
- wxcli/migration/transform/mappers/e911_mapper.py +129 -0
- wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
- wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
- wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
- wxcli/migration/transform/mappers/line_mapper.py +310 -0
- wxcli/migration/transform/mappers/location_mapper.py +235 -0
- wxcli/migration/transform/mappers/moh_mapper.py +114 -0
- wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
- wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
- wxcli/migration/transform/mappers/routing_mapper.py +821 -0
- wxcli/migration/transform/mappers/snr_mapper.py +201 -0
- wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
- wxcli/migration/transform/mappers/user_mapper.py +264 -0
- wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
- wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
- wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
- wxcli/migration/transform/normalizers.py +2008 -0
- wxcli/migration/transform/pattern_converter.py +106 -0
- wxcli/migration/transform/pipeline.py +271 -0
- wxcli/migration/transform/rules.py +255 -0
- wxcli/org_health/CLAUDE.md +63 -0
- wxcli/org_health/__init__.py +7 -0
- wxcli/org_health/analyze.py +93 -0
- wxcli/org_health/checks.py +458 -0
- wxcli/org_health/collector.py +62 -0
- wxcli/org_health/models.py +78 -0
- wxcli/org_health/report.py +300 -0
- wxcli/output.py +101 -0
- wxcli-1.2.0.dist-info/METADATA +505 -0
- wxcli-1.2.0.dist-info/RECORD +413 -0
- wxcli-1.2.0.dist-info/WHEEL +5 -0
- wxcli-1.2.0.dist-info/entry_points.txt +2 -0
- wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
- wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
- wxcli-1.2.0.dist-info/scm_version.json +8 -0
- wxcli-1.2.0.dist-info/top_level.txt +1 -0
wxcli/main.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
from wxcli import __version__
|
|
5
|
+
from wxcli.auth import get_api, resolve_token, WebexApi, WebexSession
|
|
6
|
+
from wxcli.config import get_expires_at, get_org_id, get_org_name, save_org
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
name="wxcli",
|
|
10
|
+
help="Webex Calling CLI — provision and manage Webex Calling from the terminal.",
|
|
11
|
+
no_args_is_help=True,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def version_callback(value: bool):
|
|
16
|
+
if value:
|
|
17
|
+
typer.echo(f"wxcli {__version__}")
|
|
18
|
+
raise typer.Exit()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.callback()
|
|
22
|
+
def main(
|
|
23
|
+
version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True),
|
|
24
|
+
):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def whoami(
|
|
30
|
+
debug: bool = typer.Option(False, "--debug", help="Show debug output"),
|
|
31
|
+
):
|
|
32
|
+
"""Show current authenticated user and org."""
|
|
33
|
+
api = get_api(debug=debug)
|
|
34
|
+
me = api.session.rest_get("https://webexapis.com/v1/people/me")
|
|
35
|
+
|
|
36
|
+
display_name = me.get("displayName", "")
|
|
37
|
+
email = (me.get("emails") or ["unknown"])[0]
|
|
38
|
+
org_id = me.get("orgId", "")
|
|
39
|
+
typer.echo(f"User: {display_name} ({email})")
|
|
40
|
+
typer.echo(f"Org: {org_id}")
|
|
41
|
+
|
|
42
|
+
target_org_id = get_org_id()
|
|
43
|
+
target_org_name = get_org_name()
|
|
44
|
+
if target_org_id:
|
|
45
|
+
typer.echo(f"Target: {target_org_id} ({target_org_name})")
|
|
46
|
+
|
|
47
|
+
roles = me.get("roles")
|
|
48
|
+
if roles:
|
|
49
|
+
typer.echo(f"Roles: {', '.join(roles)}")
|
|
50
|
+
|
|
51
|
+
expires = get_expires_at()
|
|
52
|
+
if expires:
|
|
53
|
+
try:
|
|
54
|
+
exp_dt = datetime.fromisoformat(expires)
|
|
55
|
+
now = datetime.now(timezone.utc)
|
|
56
|
+
remaining = exp_dt - now
|
|
57
|
+
if remaining.total_seconds() > 0:
|
|
58
|
+
hours = int(remaining.total_seconds() // 3600)
|
|
59
|
+
minutes = int((remaining.total_seconds() % 3600) // 60)
|
|
60
|
+
if hours < 2:
|
|
61
|
+
typer.echo(f"Token: expires in {hours}h {minutes}m — consider refreshing soon")
|
|
62
|
+
else:
|
|
63
|
+
typer.echo(f"Token: expires in {hours}h {minutes}m")
|
|
64
|
+
except ValueError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command("switch-org")
|
|
69
|
+
def switch_org(
|
|
70
|
+
org_id: str = typer.Argument(None, help="orgId to switch to (skip interactive prompt)"),
|
|
71
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
72
|
+
):
|
|
73
|
+
"""Switch target organization for partner multi-org tokens."""
|
|
74
|
+
token = resolve_token()
|
|
75
|
+
if not token:
|
|
76
|
+
typer.echo("Error: No token found. Run 'wxcli configure' first.", err=True)
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
|
|
79
|
+
api = WebexApi(WebexSession(token))
|
|
80
|
+
|
|
81
|
+
if org_id:
|
|
82
|
+
# Direct switch — resolve org name
|
|
83
|
+
try:
|
|
84
|
+
org = api.session.rest_get(f"https://webexapis.com/v1/organizations/{org_id}")
|
|
85
|
+
org_name = org.get("displayName", "Unknown")
|
|
86
|
+
except Exception:
|
|
87
|
+
org_name = "Unknown"
|
|
88
|
+
save_org(org_id, org_name)
|
|
89
|
+
typer.echo(f"Target org set: {org_name} ({org_id})")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Interactive — list orgs
|
|
93
|
+
try:
|
|
94
|
+
result = api.session.rest_get("https://webexapis.com/v1/organizations")
|
|
95
|
+
items = result.get("items", []) if isinstance(result, dict) else []
|
|
96
|
+
except Exception as e:
|
|
97
|
+
typer.echo(f"Error listing organizations: {e}", err=True)
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
if len(items) <= 1:
|
|
101
|
+
typer.echo("Single-org token — no org switching needed.")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
typer.echo(f"\nAvailable organizations:\n")
|
|
105
|
+
for i, org in enumerate(items, 1):
|
|
106
|
+
name = org.get("displayName", "Unknown")
|
|
107
|
+
oid = org.get("id", "")
|
|
108
|
+
typer.echo(f" {i}. {name:<30s} ({oid})")
|
|
109
|
+
|
|
110
|
+
typer.echo()
|
|
111
|
+
choice = typer.prompt(f"Select target org [1-{len(items)}]", type=int)
|
|
112
|
+
if choice < 1 or choice > len(items):
|
|
113
|
+
typer.echo("Invalid selection.", err=True)
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
selected = items[choice - 1]
|
|
117
|
+
save_org(selected.get("id"), selected.get("displayName"))
|
|
118
|
+
typer.echo(f"\nTarget org set: {selected.get('displayName')} ({selected.get('id')})")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@app.command("clear-org")
|
|
122
|
+
def clear_org():
|
|
123
|
+
"""Clear target organization — commands will target your own org."""
|
|
124
|
+
save_org(None, None)
|
|
125
|
+
typer.echo("Cleared target org. Commands will now target your own organization.")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.command("set-cc-region")
|
|
129
|
+
def set_cc_region(
|
|
130
|
+
region: str = typer.Argument(
|
|
131
|
+
help="Contact Center region: us1, eu1, eu2, anz1, ca1, jp1, sg1"
|
|
132
|
+
),
|
|
133
|
+
):
|
|
134
|
+
"""Set the Contact Center API region for cc-* commands."""
|
|
135
|
+
from wxcli.config import CC_REGIONS, save_cc_region
|
|
136
|
+
if region not in CC_REGIONS:
|
|
137
|
+
typer.echo(f"Error: Unknown region '{region}'. Valid: {', '.join(sorted(CC_REGIONS))}", err=True)
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
save_cc_region(region)
|
|
140
|
+
typer.echo(f"CC region set: {region} ({CC_REGIONS[region]})")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Hand-coded modules. locations.py/numbers.py are no longer registered here —
|
|
144
|
+
# they turned out to be generator-owned on disk (regenerated byte-identical at
|
|
145
|
+
# the 2026-07-01 spec sync). licenses.py was the last hand-written legacy
|
|
146
|
+
# module, retired 2026-07-02 (S3.1) after a live-verified parity check with
|
|
147
|
+
# the generated twin — `licenses` is now generated and manifest-registered.
|
|
148
|
+
from wxcli.commands.update import app as update_app
|
|
149
|
+
from wxcli.commands.configure import app as configure_app
|
|
150
|
+
from wxcli.commands.cucm import app as cucm_app
|
|
151
|
+
|
|
152
|
+
app.add_typer(update_app, name="update")
|
|
153
|
+
app.add_typer(configure_app, name="configure")
|
|
154
|
+
app.add_typer(cucm_app, name="cucm")
|
|
155
|
+
|
|
156
|
+
from wxcli.commands.cleanup import app as cleanup_app
|
|
157
|
+
app.add_typer(cleanup_app, name="cleanup")
|
|
158
|
+
|
|
159
|
+
from wxcli.commands.org_health_cli import app as org_health_app
|
|
160
|
+
app.add_typer(org_health_app, name="org-health")
|
|
161
|
+
|
|
162
|
+
from wxcli.commands.init_playbook import init as init_command
|
|
163
|
+
app.command(name="init")(init_command)
|
|
164
|
+
|
|
165
|
+
# Auto-generated groups — registration manifest emitted by tools/generate_commands.py.
|
|
166
|
+
# Explicit registrations above/below this loop are reserved for hand-written
|
|
167
|
+
# seams, aliases, and the guarded dev-only fs_* block.
|
|
168
|
+
from importlib import import_module
|
|
169
|
+
from wxcli.commands._registry import GENERATED_GROUPS
|
|
170
|
+
|
|
171
|
+
_generated_apps = {}
|
|
172
|
+
for _module, _group in GENERATED_GROUPS:
|
|
173
|
+
_generated_apps[_group] = import_module(f"wxcli.commands.{_module}").app
|
|
174
|
+
app.add_typer(_generated_apps[_group], name=_group)
|
|
175
|
+
|
|
176
|
+
# Hand-written download/export mounted onto the generated converged-recordings group
|
|
177
|
+
from wxcli.commands import converged_recordings_export
|
|
178
|
+
converged_recordings_export.register(_generated_apps["converged-recordings"])
|
|
179
|
+
|
|
180
|
+
# Aliases
|
|
181
|
+
app.add_typer(_generated_apps["customer-assist"], name="cx-essentials") # alias: cx-essentials → customer-assist
|
|
182
|
+
app.add_typer(_generated_apps["people"], name="users") # alias: users → people (replaced hand-coded users.py)
|
|
183
|
+
app.add_typer(_generated_apps["licenses"], name="licenses-api") # alias for one release: licenses-api → licenses (S3.1, 2026-07-02)
|
|
184
|
+
|
|
185
|
+
# Dev-only: Flow Store CLI (gitignored, generated from specs/webex-flow-store.json)
|
|
186
|
+
try:
|
|
187
|
+
from wxcli.commands.fs_flows import app as fs_flows_app
|
|
188
|
+
app.add_typer(fs_flows_app, name="fs-flows")
|
|
189
|
+
from wxcli.commands.fs_resources import app as fs_resources_app
|
|
190
|
+
app.add_typer(fs_resources_app, name="fs-resources")
|
|
191
|
+
from wxcli.commands.fs_templates import app as fs_templates_app
|
|
192
|
+
app.add_typer(fs_templates_app, name="fs-templates")
|
|
193
|
+
from wxcli.commands.fs_tracing import app as fs_tracing_app
|
|
194
|
+
app.add_typer(fs_tracing_app, name="fs-tracing")
|
|
195
|
+
from wxcli.commands.fs_flows_v2 import app as fs_flows_v2_app
|
|
196
|
+
app.add_typer(fs_flows_v2_app, name="fs-flows-v2")
|
|
197
|
+
from wxcli.commands.fs_user_prefs import app as fs_user_prefs_app
|
|
198
|
+
app.add_typer(fs_user_prefs_app, name="fs-user-prefs")
|
|
199
|
+
from wxcli.commands.fs_connectors import app as fs_connectors_app
|
|
200
|
+
app.add_typer(fs_connectors_app, name="fs-connectors")
|
|
201
|
+
from wxcli.commands.fs_expression_test import app as fs_expression_test_app
|
|
202
|
+
app.add_typer(fs_expression_test_app, name="fs-expression-test")
|
|
203
|
+
from wxcli.commands.fs_flow_props import app as fs_flow_props_app
|
|
204
|
+
app.add_typer(fs_flow_props_app, name="fs-flow-props")
|
|
205
|
+
from wxcli.commands.fs_flow_versions import app as fs_flow_versions_app
|
|
206
|
+
app.add_typer(fs_flow_versions_app, name="fs-flow-versions")
|
|
207
|
+
from wxcli.commands.fs_projects import app as fs_projects_app
|
|
208
|
+
app.add_typer(fs_projects_app, name="fs-projects")
|
|
209
|
+
except ImportError:
|
|
210
|
+
pass
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# migration/ — CUCM→Webex Migration Tool
|
|
2
|
+
|
|
3
|
+
All 11 phases complete. **2535 tests passing.** Wired into the CLI as `wxcli cucm <command>`. Does NOT use the auto-generator.
|
|
4
|
+
|
|
5
|
+
## File Map
|
|
6
|
+
|
|
7
|
+
| Path | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| `docs/plans/cucm-migration-roadmap.md` | **Master roadmap** — what's done, what's ready, what's next. Start here. |
|
|
10
|
+
| `docs/plans/cucm-pipeline-architecture.md` | Pipeline architecture summary — SQLite store, two-pass ELT, linter-pattern analyzers, NetworkX DAG |
|
|
11
|
+
| `docs/plans/cucm-pipeline/01-07 + 03b` | 8 detailed architecture docs |
|
|
12
|
+
| `src/wxcli/commands/cucm.py` | Phases 08+10 — CLI: 16 commands (init, discover, normalize, map, analyze, plan, preflight, decisions, decide, export, inventory, status, config, user-diff, user-notice, report) |
|
|
13
|
+
| `src/wxcli/commands/cucm_config.py` | Phase 08 — Config management helpers |
|
|
14
|
+
| `models.py` | Canonical data models — 38 types, DecisionType (27 values), Decision, MapperResult, TransformResult |
|
|
15
|
+
| `store.py` | SQLite-backed store — objects, cross_refs, decisions, journal, merge_log, merge_decisions() |
|
|
16
|
+
| `cucm/` | Phase 03 — AXL connection, 14 extractors, discovery pipeline |
|
|
17
|
+
| `transform/normalizers.py` | Phase 04 — 42 Pass 1 normalizers |
|
|
18
|
+
| `transform/cross_reference.py` | Phase 04 — CrossReferenceBuilder (31 relationships + 3 enrichments) |
|
|
19
|
+
| `transform/pipeline.py` | Phase 04 — `normalize_discovery()` entry point |
|
|
20
|
+
| `transform/mappers/` | Phase 05 — 26 mappers + base.py (announcement, button_template, call_forwarding, call_settings, css, dect, device, device_layout, device_profile, e911, ecbn, executive_assistant, feature, line, location, moh, monitoring, receptionist, routing, snr, softkey, user, voicemail, voicemail_group, workspace, device_settings) |
|
|
21
|
+
| `transform/analyzers/` | Phase 06 — 13 analyzers (css_permission, css_routing, device_compatibility, dn_ambiguity, duplicate_user, extension_conflict, feature_approximation, layout_overflow, location_ambiguity, missing_data, shared_line, voicemail_compatibility, workspace_license) |
|
|
22
|
+
| `transform/analysis_pipeline.py` | Phase 06 — Orchestrator: run analyzers → merge → auto-rules + resolve_and_cascade() |
|
|
23
|
+
| `execute/` | Phase 07 — planner.py, dependency.py (NetworkX DAG), batch.py |
|
|
24
|
+
| `export/` | Phase 09 — deployment_plan.py, json/csv exports (command_builder.py removed in Phase 12b) |
|
|
25
|
+
| `preflight/` | Phase 10 — checks.py (9 preflight checks), runner.py (orchestrator), CLI `wxcli cucm preflight` |
|
|
26
|
+
| `advisory/` | Phase 13 — Advisory system: per-decision recommendations (19 rules) + cross-cutting advisor (30 patterns) |
|
|
27
|
+
| `report/` | Assessment report generator — complexity score, SVG charts, executive summary + appendix → HTML/PDF. See its CLAUDE.md. |
|
|
28
|
+
| `.claude/skills/cucm-migrate/SKILL.md` | Phase 11 — 6-step execution skill: preflight → plan summary → batch execute → delegate → report |
|
|
29
|
+
|
|
30
|
+
**Where the design spec and pipeline architecture docs conflict, the pipeline architecture docs are authoritative.**
|
|
31
|
+
|
|
32
|
+
Each subdirectory has its own CLAUDE.md (local context) and TODO.md (outstanding work).
|
|
33
|
+
See `docs/plans/cucm-migration-roadmap.md` for the master project status.
|
|
34
|
+
|
|
35
|
+
## Known Issues
|
|
36
|
+
|
|
37
|
+
1. **CUCM CallPickupGroup creation with members fails on CUCM 15.0.** The AXL `addCallPickupGroup` operation with `<members>` containing `<directoryNumber>` fails with a null priority foreign key constraint (`pickupgroupmember.priority`). Workaround: create the pickup group empty, then use `updateLine` with `callPickupGroupName` to assign members at the line level. Affects both wxcadm and raw AXL calls. <!-- Verified on CUCM 15.0.1.13901(2), 2026-03-24 -->
|
|
38
|
+
|
|
39
|
+
## Pipeline Commands
|
|
40
|
+
|
|
41
|
+
## Feature Forwarding / Holiday / Night Service Migration
|
|
42
|
+
|
|
43
|
+
The `FeatureMapper` extracts CUCM hunt pilot forwarding destinations
|
|
44
|
+
(`forwardHuntNoAnswer`, `forwardHuntBusy`) and queue overflow targets
|
|
45
|
+
(`queueCalls.queueFullDestination`, `queueCalls.maxWaitTimeDestination`,
|
|
46
|
+
`queueCalls.noAgentDestination`) onto the canonical hunt group / call queue
|
|
47
|
+
objects, and CTI Route Point `callForwardAll` onto the canonical auto attendant.
|
|
48
|
+
The planner emits 6 tier-5 `configure_*` ops (`hunt_group:configure_forwarding`,
|
|
49
|
+
`call_queue:configure_forwarding`, `call_queue:configure_holiday_service`,
|
|
50
|
+
`call_queue:configure_night_service`, `call_queue:configure_stranded_calls`,
|
|
51
|
+
`auto_attendant:configure_forwarding`) that PUT the new settings via per-feature
|
|
52
|
+
endpoints once the feature has been created. Empty / None data short-circuits
|
|
53
|
+
the planner so unaffected features produce no extra ops. Schedules referenced
|
|
54
|
+
by holiday or night service are resolved by name + level (LOCATION /
|
|
55
|
+
ORGANIZATION) — operating modes and location schedules are already created at
|
|
56
|
+
tier 1, so no additional dependency edges are needed. Spec at
|
|
57
|
+
`docs/superpowers/specs/2026-04-10-feature-forwarding-night-service.md`.
|
|
58
|
+
|
|
59
|
+
## Hoteling / Hot Desking Migration
|
|
60
|
+
|
|
61
|
+
The `DeviceProfileMapper` produces execution-ready hoteling data alongside the
|
|
62
|
+
existing `FEATURE_APPROXIMATION` decisions. For each CUCM Extension Mobility
|
|
63
|
+
device profile it sets `hoteling_guest_enabled=True` (when the user is resolved),
|
|
64
|
+
populates `location_canonical_id` (from device_pool → location cross-ref), and
|
|
65
|
+
creates a `hoteling_location:{loc_cid}` MigrationObject for each unique location
|
|
66
|
+
with EM phones. Three new execution handlers (`enable_hoteling_guest`,
|
|
67
|
+
`enable_hoteling_host`, `enable_hotdesking`) configure Webex hoteling guest,
|
|
68
|
+
device host, and location-level voice portal sign-in respectively. Advisory
|
|
69
|
+
pattern 20 (`detect_extension_mobility_usage`) escalates severity to MEDIUM when
|
|
70
|
+
profiles contain multi-line or BLF features that Webex hot desking cannot
|
|
71
|
+
replicate, and `recommend_feature_approximation` always recommends "accept" for
|
|
72
|
+
EM decisions (no alternative architecture exists). Spec at
|
|
73
|
+
`docs/superpowers/specs/2026-04-10-hoteling-migration.md`.
|
|
74
|
+
|
|
75
|
+
## Voicemail Groups Migration
|
|
76
|
+
|
|
77
|
+
Unity Connection shared/group mailboxes (department voicemail — sales@, support@,
|
|
78
|
+
billing@ — typically reachable via a hunt pilot) are extracted via a new
|
|
79
|
+
`UnityConnectionClient.extract_shared_mailboxes()` method and normalized into
|
|
80
|
+
`voicemail_group:{name}` MigrationObjects. `VoicemailGroupMapper` (tier 4,
|
|
81
|
+
depends on `location_mapper` + `feature_mapper`) resolves each mailbox's
|
|
82
|
+
location (user extension match first, then single-location fallback;
|
|
83
|
+
multi-location without match → `LOCATION_AMBIGUOUS`),
|
|
84
|
+
generates a placeholder passcode (`MISSING_DATA`), flags custom greetings
|
|
85
|
+
(`AUDIO_ASSET_MANUAL`), and detects extension conflicts (`EXTENSION_CONFLICT`).
|
|
86
|
+
The `handle_voicemail_group_create` handler POSTs to
|
|
87
|
+
`/telephony/config/locations/{id}/voicemailGroups`. Overflow linkage (Phase C):
|
|
88
|
+
the `FeatureMapper` writes `feature_forwards_to_voicemail_group` cross-refs
|
|
89
|
+
when a hunt group/call queue forwarding destination matches a VM group extension,
|
|
90
|
+
and `dependency.py` enforces `voicemail_group:create` before
|
|
91
|
+
`hunt_group:configure_forwarding` / `call_queue:configure_forwarding` /
|
|
92
|
+
`call_queue:configure_stranded_calls`. The forwarding handlers already PUT the
|
|
93
|
+
destination extension — the dependency edge ensures the VM group exists first.
|
|
94
|
+
Spec at `docs/superpowers/specs/2026-04-10-voicemail-groups.md`.
|
|
95
|
+
|
|
96
|
+
## DECT Migration
|
|
97
|
+
|
|
98
|
+
CUCM DECT handsets (6823, 6825, 6825ip) are classified as `DeviceCompatibilityTier.DECT` by `DeviceMapper` — they are neither incompatible nor convertible, just require DECT network provisioning. `normalize_dect_group()` (Pass 1) groups DECT handsets by CUCM device pool into `dect_network:` MigrationObjects, each carrying a `handset_assignments` list. `DECTMapper` (Phase 05, depends on `location_mapper`, `device_mapper`, `user_mapper`) resolves each network's location via `device_pool_to_location` cross-refs, auto-selects the Webex model (`DBS-110` for ≤30 handsets, `DBS-210` for >30), enriches handset assignments with owner canonical IDs, and generates `DECT_NETWORK_DESIGN` (missing location, no base station inventory, or multi-zone ambiguity) and `DECT_HANDSET_ASSIGNMENT` (unowned handsets) decisions. Base station MAC addresses are supplied via `--dect-inventory <csv>` on `wxcli cucm discover` (columns: `coverage_zone`, `base_station_mac`, `base_station_model`); without them, `DECT_NETWORK_DESIGN` is raised at MEDIUM severity. The planner expands each `dect_network` into 3 ops: `create` → `create_base_stations` → `assign_handsets`, each with proper dependency edges. Three handlers in `execute/handlers.py` implement these ops: `handle_dect_network_create` (POST `/dectNetworks`), `handle_dect_base_station_create` (POST `/dectNetworks/{id}/baseStations`), `handle_dect_handset_assign` (POST `/dectNetworks/{id}/handsets/bulk`). DECT is not supported for FedRAMP tenants. Spec at `docs/superpowers/specs/2026-04-10-dect-migration.md`.
|
|
99
|
+
|
|
100
|
+
**To run a migration:** `wxcli cucm init` → `discover` → `normalize` → `map` → `analyze` → `decisions` → `plan` → `preflight` → `export` → then invoke `/cucm-migrate`.
|
|
101
|
+
|
|
102
|
+
**To generate an assessment report:** `wxcli cucm init` → `discover` (or `discover --from-file`) → `normalize` → `map` → `analyze` → `report --brand "..." --prepared-by "..."`. Does not require plan/preflight/export — the report reads directly from the post-analyze store.
|
|
103
|
+
|
|
104
|
+
**To generate a per-user diff:** `wxcli cucm init` → `discover` → `normalize` → `map` → `analyze` → `user-diff`. Does not require plan/preflight/export.
|
|
105
|
+
|
|
106
|
+
**To generate a user communication notice:** `wxcli cucm init` → `discover` → `normalize` → `map` → `analyze` → `user-notice --brand "..." --migration-date "..." --helpdesk "..."`. Does not require plan/preflight/export. Generates an email-ready notice covering detected scenarios.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CUCM-to-Webex Calling migration tool."""
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Migration Advisory System
|
|
2
|
+
|
|
3
|
+
Practitioner-level architectural advisory for CUCM-to-Webex Calling migrations. Two layers that add judgment to the pipeline's mechanical decision-making.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
analysis_pipeline.py run()
|
|
9
|
+
│
|
|
10
|
+
├── Phase 1: 12 existing analyzers → merge decisions into store
|
|
11
|
+
│
|
|
12
|
+
├── Phase 2: ArchitectureAdvisor → reads merged decisions + full inventory
|
|
13
|
+
│ → produces ARCHITECTURE_ADVISORY decisions → merge (separate stage)
|
|
14
|
+
│
|
|
15
|
+
└── Phase 3: populate_recommendations() → enriches ALL decisions with recommendations
|
|
16
|
+
│
|
|
17
|
+
▼
|
|
18
|
+
advisory/
|
|
19
|
+
├── recommendation_rules.py (Layer 1: per-decision)
|
|
20
|
+
├── advisory_patterns.py (Layer 2: cross-cutting)
|
|
21
|
+
└── advisor.py (ArchitectureAdvisor class)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Layer 1 — Per-Decision Recommendations.** Every decision the pipeline produces (from mappers and analyzers) gets an optional `recommendation` field: which option the system advises, and `recommendation_reasoning`: why. Populated by `populate_recommendations()` which calls into `recommendation_rules.py`. One function per DecisionType (16 total). Returns `(option_id, reasoning)` or `None` for genuinely ambiguous cases.
|
|
25
|
+
|
|
26
|
+
**Layer 2 — Cross-Cutting Advisor.** The `ArchitectureAdvisor` runs after all 13 analyzers have merged their decisions. It reads the full canonical model plus all prior decisions and produces `ARCHITECTURE_ADVISORY` decisions for patterns spanning multiple objects — things like "6 of your 14 CSSes are restriction-only and should be calling permissions, not dial plans" or "your trunk topology indicates Local Gateway, not Cloud Connected PSTN." 33 pattern detector functions total.
|
|
27
|
+
|
|
28
|
+
## Why Two Phases
|
|
29
|
+
|
|
30
|
+
The ArchitectureAdvisor needs to read decisions from the first 13 analyzers (e.g., Pattern 4 groups DEVICE_INCOMPATIBLE decisions by model for bulk upgrade planning). In a single-phase design, analyzer decisions aren't in the store until after ALL analyzers run — so the ArchitectureAdvisor would see nothing. The two-phase approach merges Phase 1 decisions first, then runs Phase 2 against the populated store.
|
|
31
|
+
|
|
32
|
+
Advisory decisions are merged separately using `decision_types=[ARCHITECTURE_ADVISORY]` and `stage="advisory"` so they don't stale-mark the Phase 1 decisions.
|
|
33
|
+
|
|
34
|
+
## Files
|
|
35
|
+
|
|
36
|
+
| File | Purpose |
|
|
37
|
+
|------|---------|
|
|
38
|
+
| `__init__.py` | Exports `populate_recommendations()` and `ArchitectureAdvisor` |
|
|
39
|
+
| `recommendation_rules.py` | 21 recommendation functions (one per DecisionType) + `RECOMMENDATION_DISPATCH` dict |
|
|
40
|
+
| `advisory_patterns.py` | 34 cross-cutting pattern detectors + `AdvisoryFinding` dataclass + `ALL_ADVISORY_PATTERNS` list |
|
|
41
|
+
| `advisor.py` | `ArchitectureAdvisor` class (extends Analyzer ABC) |
|
|
42
|
+
|
|
43
|
+
## Decision Model Fields
|
|
44
|
+
|
|
45
|
+
Two fields added to `Decision` in `models.py`:
|
|
46
|
+
- `recommendation: str | None` — the `id` of the recommended option (e.g., `"call_queue"`, `"convert"`)
|
|
47
|
+
- `recommendation_reasoning: str | None` — 1-3 sentences explaining why
|
|
48
|
+
|
|
49
|
+
Both are display-only — NOT part of the fingerprint. They can change across runs without invalidating resolved decisions. Stored in the `decisions` table as nullable TEXT columns.
|
|
50
|
+
|
|
51
|
+
One enum value added: `DecisionType.ARCHITECTURE_ADVISORY`
|
|
52
|
+
|
|
53
|
+
## Per-Decision Recommendations (Layer 1)
|
|
54
|
+
|
|
55
|
+
`recommendation_rules.py` has one function per DecisionType:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
def recommend_{type_name}(context: dict, options: list[dict]) -> tuple[str, str] | None:
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The `RECOMMENDATION_DISPATCH` dict maps type strings to functions. `populate_recommendations()` iterates all decisions in the store, calls the dispatch function, and writes results via `store.update_recommendation()`.
|
|
62
|
+
|
|
63
|
+
**Key rules by type:**
|
|
64
|
+
- `FEATURE_APPROXIMATION`: Queue features enabled → Call Queue. >8 agents → Call Queue. ≤4 agents top-down → Hunt Group. 5-8 ambiguous → None.
|
|
65
|
+
- `DEVICE_INCOMPATIBLE`: Model-to-replacement lookup (7811→9841, 7832→room device, ATA 190→ATA 192). Includes firmware type (MPP vs RoomOS) in reasoning.
|
|
66
|
+
- `CSS_ROUTING_MISMATCH`: Partition ordering dependency → "manual" with explanation. Scope difference → "use_union" unless it creates conflicts.
|
|
67
|
+
- `MISSING_DATA`: Almost always "skip" with remediation guidance. Trunk password → "generate".
|
|
68
|
+
|
|
69
|
+
Ambiguous cases return `None`. Honest uncertainty is a feature.
|
|
70
|
+
|
|
71
|
+
## Cross-Cutting Advisory Patterns (Layer 2)
|
|
72
|
+
|
|
73
|
+
`advisory_patterns.py` has 33 pattern detector functions. Each takes a `MigrationStore` and returns `list[AdvisoryFinding]`.
|
|
74
|
+
|
|
75
|
+
**Critical patterns (highest migration impact):**
|
|
76
|
+
1. **Partition Ordering Loss** — CSSes that depend on partition ordering to resolve overlapping patterns. Webex uses longest-match routing — no ordering equivalent. Calls may route differently after migration.
|
|
77
|
+
2. **CPN Transformation Chain** — Route patterns and trunks with calling/called party transformations. CUCM chains transformations across 5 levels. Webex has flat caller ID per user/location.
|
|
78
|
+
3. **PSTN Connection Type** — Classifies trunk topology into Local Gateway, Cloud Connected PSTN, or Premises-based PSTN. Drives the entire Webex routing architecture.
|
|
79
|
+
|
|
80
|
+
**Eliminate patterns (CUCM workarounds to remove):**
|
|
81
|
+
4. Restriction CSS Consolidation — CSSes with only blocking patterns → calling permission policies
|
|
82
|
+
5. Translation Pattern Elimination — Digit normalization handled natively by Webex location settings
|
|
83
|
+
6. Partition-Based Time Routing — Time schedule→partition→CSS chain → AA business hours schedule
|
|
84
|
+
7. Over-Engineered Dial Plan — Patterns matching Webex's built-in extension routing
|
|
85
|
+
8. Voicemail Pilot Simplification — Multiple pilots to same VM system → Webex location-level voicemail
|
|
86
|
+
|
|
87
|
+
**Rebuild patterns (use Webex-native approach):**
|
|
88
|
+
9. Hunt Pilot Behavioral Reclassification — HGs with queue-like behavior → Call Queues
|
|
89
|
+
10. Location Consolidation — Multiple device pools same tz+region → fewer Webex locations
|
|
90
|
+
11. Shared Line Simplification — Monitoring-only appearances → virtual extensions
|
|
91
|
+
12. Trunk Destination Consolidation — Multiple trunks same destination → single Webex trunk
|
|
92
|
+
|
|
93
|
+
**Informational / out-of-scope:**
|
|
94
|
+
13. Device Bulk Upgrade Planning — Groups incompatible devices by model with replacement options
|
|
95
|
+
14. Globalized vs. Localized Dial Plan — Detects dial plan style to guide migration approach
|
|
96
|
+
15. Media Resource Scope Removal — MRGLs, conference bridges, transcoders → cloud-managed, skip
|
|
97
|
+
16. E911/CER Migration Flag — Emergency services require separate workstream
|
|
98
|
+
|
|
99
|
+
**Tier 4 feature gap patterns:**
|
|
100
|
+
17. Recording Enabled Users — phones with recordingFlag enabled → configure Webex recording
|
|
101
|
+
18. SNR Configured Users — remote destination profiles → manual Webex SNR setup
|
|
102
|
+
19. Transformation Patterns — calling/called party transformations → manual caller ID review
|
|
103
|
+
20. Extension Mobility Usage — device profiles → Webex hot desking configuration
|
|
104
|
+
21. Call Intercept Candidates — heuristically detected blocked partitions + CFA-to-voicemail → manual Webex intercept setup
|
|
105
|
+
22. Selective Call Handling Opportunities — CSS/partition patterns suggesting per-caller routing → Webex Selective Forward / Accept / Reject (admin) or Priority Alert (user-only)
|
|
106
|
+
|
|
107
|
+
**Gap patterns (silent failures / platform limits):**
|
|
108
|
+
23. Mixed CSS — CSSes mixing routing and restriction partitions (silent gap in Pattern 1)
|
|
109
|
+
24. Cumulative Virtual Line Consumption — count VLs recommended across all decisions, warn if approaching limits
|
|
110
|
+
25. User OAuth Required — features that require per-user OAuth (admin tokens can't set them)
|
|
111
|
+
26. Trunk Type Selection — trunk type (REGISTERING vs CERTIFICATE_BASED) is immutable post-creation
|
|
112
|
+
27. Intercluster Trunks — CUCM ICT disposition decision (no Webex equivalent)
|
|
113
|
+
28. Legacy Gateway Protocols — MGCP and H.323 gateways need SIP conversion before migration
|
|
114
|
+
|
|
115
|
+
**User communication:**
|
|
116
|
+
29. Voicemail Greeting Re-Recording — counts users with custom greetings, produces user communication template
|
|
117
|
+
|
|
118
|
+
**Audio migration:**
|
|
119
|
+
30. Custom Audio Assets — custom MoH sources + announcements requiring manual download/upload
|
|
120
|
+
|
|
121
|
+
**Feature migration gaps:**
|
|
122
|
+
31. DECT Deployment — DECT networks present without base station inventory → operator must supply MAC CSV
|
|
123
|
+
32. Feature Forwarding Gaps — hunt groups / call queues / AAs with CUCM forwarding destinations that aren't mapped to Webex forwarding fields (MEDIUM/rebuild)
|
|
124
|
+
33. Workspace Call Settings Gaps — workspaces with empty call_settings but CUCM device had custom caller ID, forwarding, or DND config (LOW/migrate_as_is)
|
|
125
|
+
|
|
126
|
+
## AdvisoryFinding Dataclass
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
@dataclass
|
|
130
|
+
class AdvisoryFinding:
|
|
131
|
+
pattern_name: str # "restriction_css_consolidation"
|
|
132
|
+
severity: str # "CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"
|
|
133
|
+
summary: str # One-liner for decision summary
|
|
134
|
+
detail: str # 2-5 sentences of reasoning
|
|
135
|
+
affected_objects: list[str] # canonical_ids
|
|
136
|
+
recommendation: str # Always "accept" for advisories
|
|
137
|
+
recommendation_reasoning: str
|
|
138
|
+
category: str # "migrate_as_is" | "rebuild" | "eliminate" | "out_of_scope"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The `category` field classifies advisories into the migration decision framework: migrate as-is, rebuild using Webex-native patterns, eliminate (CUCM workaround), or out-of-scope (separate workstream).
|
|
142
|
+
|
|
143
|
+
## Implementation Notes
|
|
144
|
+
|
|
145
|
+
**Pattern 1 uses `blockEnable`, not `classify_block_pattern()`.** The spec says to use `classify_block_pattern()` from `cucm_pattern.py`, but that function requires a `category_rules` list from the migration config. Using `pre_migration_state.blockEnable` is more direct and robust — it detects blocking patterns without config dependency. Code review approved this deviation.
|
|
146
|
+
|
|
147
|
+
**Pattern 9 groups by voicemail system before firing.** Uses the first 3 digits of pilot number + CSS name as a grouping key. Only fires when all pilots share the same key (i.e., point to the same VM system). Prevents false positives on deployments with genuinely independent voicemail systems.
|
|
148
|
+
|
|
149
|
+
**Pattern 11 `affected_objects` includes CSS + partition + route pattern IDs.** Not just CSS IDs — the partitions and route patterns involved in ordering conflicts are also tracked for traceability.
|
|
150
|
+
|
|
151
|
+
**Pattern 16 (E911) always fires**, even on empty stores. When no E911 signals are detected, it produces a warning that CER data may not be visible via AXL. This is by design per the spec's "if detection data is sparse" guidance. Pattern 16 now includes quantified ECBN readiness counts (DIRECT_LINE / LOCATION_ECBN / ambiguous / mismatch).
|
|
152
|
+
|
|
153
|
+
**Test count:** 87 tests (35 pattern + 7 advisor + 45 new-pattern/rule tests). The prompt estimated ~50; the actual count is lower because some patterns share positive/negative cases and the simpler patterns need fewer test scenarios. Pattern 27 (voicemail greeting re-recording) added 6 tests.
|
|
154
|
+
|
|
155
|
+
## Pipeline Integration (Phase 13d — COMPLETE)
|
|
156
|
+
|
|
157
|
+
In `analysis_pipeline.py`, the `run()` method has steps 5-6 after the original 4:
|
|
158
|
+
1. Run 12 existing analyzers (unchanged)
|
|
159
|
+
2. Convert to store dicts and merge (unchanged)
|
|
160
|
+
3. Apply auto-rules (unchanged)
|
|
161
|
+
4. Apply auto-resolution rules (unchanged)
|
|
162
|
+
5. Run ArchitectureAdvisor → merge advisory decisions separately (`decision_types=[ARCHITECTURE_ADVISORY]`, `stage="advisory"`)
|
|
163
|
+
6. `populate_recommendations()` → enrich all decisions
|
|
164
|
+
|
|
165
|
+
Advisory decisions do NOT trigger cascades. Resolving an advisory does not re-run any analyzer. Stats dict includes `architecture_advisor` key.
|
|
166
|
+
|
|
167
|
+
## CLI Integration (Phase 13d — COMPLETE)
|
|
168
|
+
|
|
169
|
+
- `wxcli cucm decisions` shows `[REC: option]` tag on decisions with recommendations (in a Recommendations section below the table)
|
|
170
|
+
- `wxcli cucm decisions --type advisory` filters to ARCHITECTURE_ADVISORY only (shorthand mapping)
|
|
171
|
+
- JSON export includes `recommendation` and `recommendation_reasoning` fields
|
|
172
|
+
|
|
173
|
+
## Skill Integration (Phase 13d — COMPLETE)
|
|
174
|
+
|
|
175
|
+
The `cucm-migrate` skill's decision review has two phases:
|
|
176
|
+
- **Phase A:** Present advisories grouped by category (eliminate, rebuild, out_of_scope, migrate_as_is) with bulk accept
|
|
177
|
+
- **Phase B:** Present per-decision items as auto_apply, recommended (with bulk accept and reasoning), or needs_input
|
|
178
|
+
|
|
179
|
+
## Adding New Patterns
|
|
180
|
+
|
|
181
|
+
To add a new advisory pattern:
|
|
182
|
+
1. Write a function `detect_{name}(store: MigrationStore) -> list[AdvisoryFinding]` in `advisory_patterns.py`
|
|
183
|
+
2. Register it in `ALL_ADVISORY_PATTERNS`
|
|
184
|
+
3. Add tests in `tests/migration/advisory/test_advisory_patterns.py`
|
|
185
|
+
|
|
186
|
+
To add a new recommendation rule:
|
|
187
|
+
1. Add logic to the existing function for that DecisionType in `recommendation_rules.py`
|
|
188
|
+
2. Add tests in `tests/migration/advisory/test_recommendation_rules.py`
|
|
189
|
+
|
|
190
|
+
No framework changes needed for either. The knowledge base grows by adding functions.
|
|
191
|
+
|
|
192
|
+
## Design Spec
|
|
193
|
+
|
|
194
|
+
Full design: `docs/superpowers/specs/2026-03-24-migration-advisory-design.md`
|
|
195
|
+
|
|
196
|
+
Build prompts: `docs/prompts/phase-13a` through `phase-13d`
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Migration advisory system — practitioner-level recommendations.
|
|
2
|
+
|
|
3
|
+
Layer 1 (recommendation_rules): per-decision recommendations
|
|
4
|
+
Layer 2 (advisory_patterns + advisor): cross-cutting observations
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from wxcli.migration.advisory.advisor import ArchitectureAdvisor
|
|
12
|
+
from wxcli.migration.advisory.recommendation_rules import RECOMMENDATION_DISPATCH
|
|
13
|
+
from wxcli.migration.store import MigrationStore
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def populate_recommendations(store: MigrationStore) -> int:
|
|
19
|
+
"""Enrich all decisions in the store with recommendations.
|
|
20
|
+
|
|
21
|
+
Iterates all decisions, calls the recommendation function for each type,
|
|
22
|
+
and writes results via store.update_recommendation().
|
|
23
|
+
|
|
24
|
+
Returns count of recommendations added.
|
|
25
|
+
"""
|
|
26
|
+
all_decisions = store.get_all_decisions()
|
|
27
|
+
count = 0
|
|
28
|
+
for dec in all_decisions:
|
|
29
|
+
if dec.get("chosen_option") == "__stale__":
|
|
30
|
+
continue
|
|
31
|
+
dec_type = dec.get("type", "")
|
|
32
|
+
fn = RECOMMENDATION_DISPATCH.get(dec_type)
|
|
33
|
+
if fn is None:
|
|
34
|
+
continue
|
|
35
|
+
context = dec.get("context", {})
|
|
36
|
+
options = dec.get("options", [])
|
|
37
|
+
try:
|
|
38
|
+
result = fn(context, options)
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
logger.warning("Recommendation %s failed: %s", fn.__name__, exc)
|
|
41
|
+
continue
|
|
42
|
+
if result is not None:
|
|
43
|
+
option_id, reasoning = result
|
|
44
|
+
store.update_recommendation(dec["decision_id"], option_id, reasoning)
|
|
45
|
+
count += 1
|
|
46
|
+
return count
|