souleyez 2.43.26__py3-none-any.whl → 2.43.34__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 (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23434 -10286
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.26.dist-info/RECORD +0 -379
  356. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -17,6 +17,7 @@ from souleyez.ui.design_system import DesignSystem
17
17
  # Key codes - using readchar constants when available
18
18
  try:
19
19
  import readchar
20
+
20
21
  KEY_UP = readchar.key.UP
21
22
  KEY_DOWN = readchar.key.DOWN
22
23
  KEY_LEFT = readchar.key.LEFT
@@ -28,15 +29,15 @@ try:
28
29
  KEY_SPACE = readchar.key.SPACE
29
30
  except (ImportError, AttributeError):
30
31
  # Fallback to raw codes
31
- KEY_UP = '\x1b[A'
32
- KEY_DOWN = '\x1b[B'
33
- KEY_LEFT = '\x1b[D'
34
- KEY_RIGHT = '\x1b[C'
35
- KEY_PAGE_UP = '\x1b[5~'
36
- KEY_PAGE_DOWN = '\x1b[6~'
37
- KEY_ESCAPE = '\x1b'
38
- KEY_ENTER = '\r'
39
- KEY_SPACE = ' '
32
+ KEY_UP = "\x1b[A"
33
+ KEY_DOWN = "\x1b[B"
34
+ KEY_LEFT = "\x1b[D"
35
+ KEY_RIGHT = "\x1b[C"
36
+ KEY_PAGE_UP = "\x1b[5~"
37
+ KEY_PAGE_DOWN = "\x1b[6~"
38
+ KEY_ESCAPE = "\x1b"
39
+ KEY_ENTER = "\r"
40
+ KEY_SPACE = " "
40
41
 
41
42
 
42
43
  def _get_key() -> str:
@@ -48,6 +49,7 @@ def _get_key() -> str:
48
49
  """
49
50
  try:
50
51
  import readchar
52
+
51
53
  key = readchar.readkey()
52
54
  return key
53
55
  except ImportError:
@@ -58,44 +60,44 @@ def _get_key() -> str:
58
60
  ch = click.getchar()
59
61
 
60
62
  # Handle escape sequences (arrow keys send \x1b[A, \x1b[B, etc.)
61
- if ch == '\x1b' or (len(ch) > 1 and ch.startswith('\x1b')):
63
+ if ch == "\x1b" or (len(ch) > 1 and ch.startswith("\x1b")):
62
64
  # click.getchar() may return the full sequence or just escape
63
65
  if len(ch) >= 3:
64
- if ch == '\x1b[A':
66
+ if ch == "\x1b[A":
65
67
  return KEY_UP
66
- elif ch == '\x1b[B':
68
+ elif ch == "\x1b[B":
67
69
  return KEY_DOWN
68
- elif ch == '\x1b[C':
70
+ elif ch == "\x1b[C":
69
71
  return KEY_RIGHT
70
- elif ch == '\x1b[D':
72
+ elif ch == "\x1b[D":
71
73
  return KEY_LEFT
72
- elif ch in ('\x1b[5~', '\x1b[5'):
74
+ elif ch in ("\x1b[5~", "\x1b[5"):
73
75
  return KEY_PAGE_UP
74
- elif ch in ('\x1b[6~', '\x1b[6'):
76
+ elif ch in ("\x1b[6~", "\x1b[6"):
75
77
  return KEY_PAGE_DOWN
76
- elif ch == '\x1b':
78
+ elif ch == "\x1b":
77
79
  # Got just escape - try to read more (arrow key sequence)
78
80
  try:
79
81
  ch2 = click.getchar()
80
- if ch2 == '[':
82
+ if ch2 == "[":
81
83
  ch3 = click.getchar()
82
- if ch3 == 'A':
84
+ if ch3 == "A":
83
85
  return KEY_UP
84
- elif ch3 == 'B':
86
+ elif ch3 == "B":
85
87
  return KEY_DOWN
86
- elif ch3 == 'C':
88
+ elif ch3 == "C":
87
89
  return KEY_RIGHT
88
- elif ch3 == 'D':
90
+ elif ch3 == "D":
89
91
  return KEY_LEFT
90
- elif ch3 in ('5', '6'):
92
+ elif ch3 in ("5", "6"):
91
93
  click.getchar() # consume ~
92
- return KEY_PAGE_UP if ch3 == '5' else KEY_PAGE_DOWN
94
+ return KEY_PAGE_UP if ch3 == "5" else KEY_PAGE_DOWN
93
95
  # Unknown sequence, ignore it
94
- return '' # Return empty to ignore
96
+ return "" # Return empty to ignore
95
97
  except Exception:
96
98
  return KEY_ESCAPE
97
99
  # Unknown escape sequence, ignore
98
- return ''
100
+ return ""
99
101
  return ch
100
102
  except Exception:
101
103
  pass
@@ -113,29 +115,29 @@ def _get_key() -> str:
113
115
  ch = sys.stdin.read(1)
114
116
 
115
117
  # Handle escape sequences (arrow keys, etc.)
116
- if ch == '\x1b':
118
+ if ch == "\x1b":
117
119
  # Check if more characters are available (escape sequence)
118
120
  if select.select([sys.stdin], [], [], 0.05)[0]:
119
121
  ch2 = sys.stdin.read(1)
120
- if ch2 == '[':
122
+ if ch2 == "[":
121
123
  # CSI sequence - read the final byte
122
124
  if select.select([sys.stdin], [], [], 0.05)[0]:
123
125
  ch3 = sys.stdin.read(1)
124
126
  # Map to our key constants
125
- if ch3 == 'A':
127
+ if ch3 == "A":
126
128
  return KEY_UP
127
- elif ch3 == 'B':
129
+ elif ch3 == "B":
128
130
  return KEY_DOWN
129
- elif ch3 == 'C':
131
+ elif ch3 == "C":
130
132
  return KEY_RIGHT
131
- elif ch3 == 'D':
133
+ elif ch3 == "D":
132
134
  return KEY_LEFT
133
- elif ch3 == '5':
135
+ elif ch3 == "5":
134
136
  # Page Up - consume the ~
135
137
  if select.select([sys.stdin], [], [], 0.05)[0]:
136
138
  sys.stdin.read(1)
137
139
  return KEY_PAGE_UP
138
- elif ch3 == '6':
140
+ elif ch3 == "6":
139
141
  # Page Down - consume the ~
140
142
  if select.select([sys.stdin], [], [], 0.05)[0]:
141
143
  sys.stdin.read(1)
@@ -181,12 +183,12 @@ class InteractiveSelector:
181
183
  """
182
184
 
183
185
  # Checkbox characters (using larger circles for visibility)
184
- CHECKBOX_EMPTY = ''
185
- CHECKBOX_CHECKED = ''
186
+ CHECKBOX_EMPTY = ""
187
+ CHECKBOX_CHECKED = ""
186
188
 
187
189
  # Cursor indicator
188
- CURSOR = ''
189
- NO_CURSOR = ' '
190
+ CURSOR = ""
191
+ NO_CURSOR = " "
190
192
 
191
193
  def __init__(
192
194
  self,
@@ -194,11 +196,11 @@ class InteractiveSelector:
194
196
  columns: List[Dict[str, Any]],
195
197
  selected_ids: Set[Any],
196
198
  get_id: Callable[[Dict], Any],
197
- title: str = 'SELECT ITEMS',
199
+ title: str = "SELECT ITEMS",
198
200
  page_size: int = 20,
199
201
  format_cell: Optional[Callable[[Dict, str], str]] = None,
200
202
  show_header_info: Optional[Callable[[], str]] = None,
201
- extra_actions: Optional[Dict[str, str]] = None
203
+ extra_actions: Optional[Dict[str, str]] = None,
202
204
  ):
203
205
  """
204
206
  Initialize the interactive selector.
@@ -244,7 +246,7 @@ class InteractiveSelector:
244
246
  The modified selected_ids set
245
247
  """
246
248
  if not self.items:
247
- click.echo(click.style(" No items to select.", fg='yellow'))
249
+ click.echo(click.style(" No items to select.", fg="yellow"))
248
250
  click.pause()
249
251
  return self.selected_ids
250
252
 
@@ -266,7 +268,11 @@ class InteractiveSelector:
266
268
  # Title
267
269
  click.echo()
268
270
  click.echo("┌" + "─" * (width - 2) + "┐")
269
- click.echo("│" + click.style(f" {self.title} ".center(width - 2), bold=True, fg='cyan') + "│")
271
+ click.echo(
272
+ "│"
273
+ + click.style(f" {self.title} ".center(width - 2), bold=True, fg="cyan")
274
+ + "│"
275
+ )
270
276
  click.echo("└" + "─" * (width - 2) + "┘")
271
277
  click.echo()
272
278
 
@@ -280,13 +286,15 @@ class InteractiveSelector:
280
286
  # Stats
281
287
  total = len(self.items)
282
288
  selected_count = len(self.selected_ids)
283
- click.echo(f" {click.style('Total:', bold=True)} {total} items | "
284
- f"{click.style('Selected:', bold=True, fg='cyan')} {selected_count}")
289
+ click.echo(
290
+ f" {click.style('Total:', bold=True)} {total} items | "
291
+ f"{click.style('Selected:', bold=True, fg='cyan')} {selected_count}"
292
+ )
285
293
  click.echo()
286
294
 
287
295
  # Calculate visible items
288
296
  page_end = min(self.page_start + self.page_size, len(self.items))
289
- visible_items = self.items[self.page_start:page_end]
297
+ visible_items = self.items[self.page_start : page_end]
290
298
 
291
299
  # Create table
292
300
  table = Table(
@@ -294,7 +302,7 @@ class InteractiveSelector:
294
302
  header_style="bold cyan",
295
303
  box=DesignSystem.TABLE_BOX,
296
304
  padding=(0, 1),
297
- expand=True
305
+ expand=True,
298
306
  )
299
307
 
300
308
  # Add cursor column
@@ -305,11 +313,11 @@ class InteractiveSelector:
305
313
  # Add user-defined columns
306
314
  for col in self.columns:
307
315
  table.add_column(
308
- col['name'],
309
- width=col.get('width'),
310
- justify=col.get('justify', 'left'),
311
- no_wrap=col.get('no_wrap', False),
312
- style=col.get('style')
316
+ col["name"],
317
+ width=col.get("width"),
318
+ justify=col.get("justify", "left"),
319
+ no_wrap=col.get("no_wrap", False),
320
+ style=col.get("style"),
313
321
  )
314
322
 
315
323
  # Add rows
@@ -318,7 +326,7 @@ class InteractiveSelector:
318
326
  item_id = self.get_id(item)
319
327
 
320
328
  # Cursor indicator
321
- is_cursor = (absolute_idx == self.cursor_pos)
329
+ is_cursor = absolute_idx == self.cursor_pos
322
330
  cursor = self.CURSOR if is_cursor else self.NO_CURSOR
323
331
 
324
332
  # Checkbox
@@ -329,13 +337,13 @@ class InteractiveSelector:
329
337
  row_values = [cursor, checkbox]
330
338
 
331
339
  for col in self.columns:
332
- key = col['key']
340
+ key = col["key"]
333
341
  if self.format_cell:
334
342
  value = self.format_cell(item, key)
335
- elif 'format' in col:
336
- value = col['format'](item.get(key, ''))
343
+ elif "format" in col:
344
+ value = col["format"](item.get(key, ""))
337
345
  else:
338
- value = str(item.get(key, '-') or '-')
346
+ value = str(item.get(key, "-") or "-")
339
347
  row_values.append(value)
340
348
 
341
349
  # Apply highlight style for cursor row
@@ -350,8 +358,7 @@ class InteractiveSelector:
350
358
  if len(self.items) > self.page_size:
351
359
  page_num = (self.page_start // self.page_size) + 1
352
360
  total_pages = (len(self.items) + self.page_size - 1) // self.page_size
353
- click.echo(f"\n Page {page_num}/{total_pages} | "
354
- f"p/n: Prev/Next page")
361
+ click.echo(f"\n Page {page_num}/{total_pages} | " f"p/n: Prev/Next page")
355
362
 
356
363
  # Help bar
357
364
  click.echo()
@@ -375,7 +382,7 @@ class InteractiveSelector:
375
382
  def _handle_key(self, key: str):
376
383
  """Handle a keypress."""
377
384
  # Navigation - Up
378
- if key in (KEY_UP, 'k'):
385
+ if key in (KEY_UP, "k"):
379
386
  if self.cursor_pos > 0:
380
387
  self.cursor_pos -= 1
381
388
  # Scroll up if needed
@@ -383,7 +390,7 @@ class InteractiveSelector:
383
390
  self.page_start = max(0, self.page_start - self.page_size)
384
391
 
385
392
  # Navigation - Down
386
- elif key in (KEY_DOWN, 'j'):
393
+ elif key in (KEY_DOWN, "j"):
387
394
  if self.cursor_pos < len(self.items) - 1:
388
395
  self.cursor_pos += 1
389
396
  # Scroll down if needed
@@ -391,14 +398,14 @@ class InteractiveSelector:
391
398
  self.page_start += self.page_size
392
399
 
393
400
  # Page Up / Previous page
394
- elif key in (KEY_PAGE_UP, 'p', '[', '<'):
401
+ elif key in (KEY_PAGE_UP, "p", "[", "<"):
395
402
  current_page = self.page_start // self.page_size
396
403
  if current_page > 0:
397
404
  self.page_start = (current_page - 1) * self.page_size
398
405
  self.cursor_pos = self.page_start
399
406
 
400
407
  # Page Down / Next page
401
- elif key in (KEY_PAGE_DOWN, 'n', ']', '>'):
408
+ elif key in (KEY_PAGE_DOWN, "n", "]", ">"):
402
409
  total_pages = (len(self.items) + self.page_size - 1) // self.page_size
403
410
  current_page = self.page_start // self.page_size
404
411
  if current_page < total_pages - 1:
@@ -420,18 +427,18 @@ class InteractiveSelector:
420
427
  self.selected_ids.add(item_id)
421
428
 
422
429
  # Select all
423
- elif key == 'a':
430
+ elif key == "a":
424
431
  for item in self.items:
425
432
  self.selected_ids.add(self.get_id(item))
426
433
 
427
434
  # Select none / Unselect all
428
- elif key == 'u':
435
+ elif key == "u":
429
436
  # Only clear items that are in current list
430
437
  current_ids = {self.get_id(item) for item in self.items}
431
438
  self.selected_ids -= current_ids
432
439
 
433
440
  # Exit - q, Enter, or Escape
434
- elif key in (KEY_ESCAPE, KEY_ENTER, 'q', '\x03', '\r', '\n'): # \x03 is Ctrl+C
441
+ elif key in (KEY_ESCAPE, KEY_ENTER, "q", "\x03", "\r", "\n"): # \x03 is Ctrl+C
435
442
  self.exit_key = key
436
443
  self.running = False
437
444
 
@@ -446,8 +453,8 @@ def interactive_select(
446
453
  columns: List[Dict[str, Any]],
447
454
  selected_ids: Set[Any],
448
455
  get_id: Callable[[Dict], Any],
449
- title: str = 'SELECT ITEMS',
450
- **kwargs
456
+ title: str = "SELECT ITEMS",
457
+ **kwargs,
451
458
  ) -> Set[Any]:
452
459
  """
453
460
  Convenience function to run an interactive selector.
@@ -469,6 +476,6 @@ def interactive_select(
469
476
  selected_ids=selected_ids,
470
477
  get_id=get_id,
471
478
  title=title,
472
- **kwargs
479
+ **kwargs,
473
480
  )
474
481
  return selector.run()
@@ -11,72 +11,80 @@ from typing import List, Optional
11
11
  def format_json_log_line(line: str) -> Optional[str]:
12
12
  """
13
13
  Format a JSON log line into human-readable format.
14
-
14
+
15
15
  Args:
16
16
  line: Raw JSON log line
17
-
17
+
18
18
  Returns:
19
19
  Formatted string or None if not JSON
20
20
  """
21
21
  try:
22
22
  data = json.loads(line.strip())
23
-
23
+
24
24
  # JSON can parse primitives (numbers, strings, booleans) - we only want objects
25
25
  if not isinstance(data, dict):
26
26
  return None
27
-
27
+
28
28
  # Extract common fields
29
- timestamp = data.get('timestamp', '')
30
- level = data.get('levelname', 'INFO')
31
- name = data.get('name', '')
32
- message = data.get('message', '')
33
-
29
+ timestamp = data.get("timestamp", "")
30
+ level = data.get("levelname", "INFO")
31
+ name = data.get("name", "")
32
+ message = data.get("message", "")
33
+
34
34
  # Parse timestamp
35
35
  if timestamp:
36
36
  try:
37
- dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
38
- time_str = dt.strftime('%H:%M:%S')
37
+ dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
38
+ time_str = dt.strftime("%H:%M:%S")
39
39
  except:
40
40
  time_str = timestamp[:8] if len(timestamp) > 8 else timestamp
41
41
  else:
42
- time_str = '??:??:??'
43
-
42
+ time_str = "??:??:??"
43
+
44
44
  # Color codes for log levels
45
45
  level_colors = {
46
- 'DEBUG': '\033[36m', # Cyan
47
- 'INFO': '\033[32m', # Green
48
- 'WARNING': '\033[33m', # Yellow
49
- 'ERROR': '\033[31m', # Red
50
- 'CRITICAL': '\033[35m' # Magenta
46
+ "DEBUG": "\033[36m", # Cyan
47
+ "INFO": "\033[32m", # Green
48
+ "WARNING": "\033[33m", # Yellow
49
+ "ERROR": "\033[31m", # Red
50
+ "CRITICAL": "\033[35m", # Magenta
51
51
  }
52
- reset = '\033[0m'
53
-
54
- level_color = level_colors.get(level, '')
52
+ reset = "\033[0m"
53
+
54
+ level_color = level_colors.get(level, "")
55
55
  level_short = level[0] # D, I, W, E, C
56
-
56
+
57
57
  # Build formatted line
58
58
  formatted = f"{time_str} {level_color}[{level_short}]{reset} "
59
-
59
+
60
60
  # Add module name if not too long
61
61
  if name and len(name) < 30:
62
- module = name.split('.')[-1] # Last part only
62
+ module = name.split(".")[-1] # Last part only
63
63
  formatted += f"{module}: "
64
-
64
+
65
65
  formatted += message
66
-
66
+
67
67
  # Add interesting extra fields
68
68
  extras = []
69
- skip_fields = {'timestamp', 'levelname', 'name', 'message', 'log_file', 'log_level', 'log_format'}
69
+ skip_fields = {
70
+ "timestamp",
71
+ "levelname",
72
+ "name",
73
+ "message",
74
+ "log_file",
75
+ "log_level",
76
+ "log_format",
77
+ }
70
78
  for key, value in data.items():
71
79
  if key not in skip_fields and value:
72
80
  if isinstance(value, (str, int, float, bool)):
73
81
  extras.append(f"{key}={value}")
74
-
82
+
75
83
  if extras:
76
84
  formatted += f" ({', '.join(extras[:3])})" # Limit to 3 extras
77
-
85
+
78
86
  return formatted
79
-
87
+
80
88
  except (json.JSONDecodeError, KeyError):
81
89
  # Not JSON or malformed
82
90
  return None
@@ -85,20 +93,20 @@ def format_json_log_line(line: str) -> Optional[str]:
85
93
  def format_log_stream(lines: List[str], max_lines: int = 50) -> List[str]:
86
94
  """
87
95
  Format a stream of log lines (mix of JSON and plain text).
88
-
96
+
89
97
  Args:
90
98
  lines: List of raw log lines
91
99
  max_lines: Maximum lines to return
92
-
100
+
93
101
  Returns:
94
102
  List of formatted lines
95
103
  """
96
104
  formatted = []
97
-
105
+
98
106
  for line in lines[-max_lines:]:
99
107
  if not line.strip():
100
108
  continue
101
-
109
+
102
110
  # Try JSON formatting first
103
111
  json_formatted = format_json_log_line(line)
104
112
  if json_formatted:
@@ -106,25 +114,25 @@ def format_log_stream(lines: List[str], max_lines: int = 50) -> List[str]:
106
114
  else:
107
115
  # Keep plain text as-is (tool output, etc.)
108
116
  formatted.append(line.rstrip())
109
-
117
+
110
118
  return formatted
111
119
 
112
120
 
113
121
  def tail_and_format_log(log_path: str, num_lines: int = 50) -> List[str]:
114
122
  """
115
123
  Read last N lines from log file and format them.
116
-
124
+
117
125
  Args:
118
126
  log_path: Path to log file
119
127
  num_lines: Number of lines to read
120
-
128
+
121
129
  Returns:
122
130
  List of formatted lines
123
131
  """
124
132
  try:
125
- with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
133
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
126
134
  lines = f.readlines()
127
-
135
+
128
136
  return format_log_stream(lines, max_lines=num_lines)
129
137
  except Exception as e:
130
138
  return [f"Error reading log: {e}"]
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Reusable menu UI components for consistent layouts.
3
3
  """
4
+
4
5
  import click
5
6
  from typing import List, Dict
6
7
  from souleyez.ui.design_system import DesignSystem
@@ -10,7 +11,13 @@ class StandardMenu:
10
11
  """Standard menu renderer with consistent formatting."""
11
12
 
12
13
  @staticmethod
13
- def render(options: List[Dict[str, str]], show_back: bool = True, shortcuts: Dict[str, int] = None, show_shortcuts: bool = True, tip: str = None):
14
+ def render(
15
+ options: List[Dict[str, str]],
16
+ show_back: bool = True,
17
+ shortcuts: Dict[str, int] = None,
18
+ show_shortcuts: bool = True,
19
+ tip: str = None,
20
+ ):
14
21
  """
15
22
  Render a standardized menu with single OPTIONS section.
16
23
 
@@ -29,14 +36,14 @@ class StandardMenu:
29
36
  width = DesignSystem.get_terminal_width()
30
37
 
31
38
  click.echo()
32
- click.echo(click.style("⚙️ OPTIONS", bold=True, fg='cyan'))
39
+ click.echo(click.style("⚙️ OPTIONS", bold=True, fg="cyan"))
33
40
  click.echo("─" * width)
34
41
  click.echo()
35
42
 
36
43
  for opt in options:
37
- number = opt['number']
38
- label = opt['label']
39
- desc = opt.get('description', '')
44
+ number = opt["number"]
45
+ label = opt["label"]
46
+ desc = opt.get("description", "")
40
47
 
41
48
  if desc:
42
49
  click.echo(f" [{number}] {label} - {desc}")
@@ -56,20 +63,20 @@ class StandardMenu:
56
63
  shortcut_hints = []
57
64
  for key, option_num in shortcuts.items():
58
65
  # Map common shortcuts to their actions
59
- if key == 'n':
66
+ if key == "n":
60
67
  shortcut_hints.append(f"'{key}' = Next Page")
61
- elif key == 'p':
68
+ elif key == "p":
62
69
  shortcut_hints.append(f"'{key}' = Previous Page")
63
- elif key == '>':
70
+ elif key == ">":
64
71
  shortcut_hints.append("'>' = Next Page")
65
- elif key == '<':
72
+ elif key == "<":
66
73
  shortcut_hints.append("'<' = Previous Page")
67
- elif key == '?':
74
+ elif key == "?":
68
75
  shortcut_hints.append("'?' = Help")
69
76
  else:
70
77
  # For other shortcuts, try to find the label
71
78
  for opt in options:
72
- if opt['number'] == option_num:
79
+ if opt["number"] == option_num:
73
80
  shortcut_hints.append(f"'{key}' = {opt['label']}")
74
81
  break
75
82
  if shortcut_hints:
@@ -78,12 +85,14 @@ class StandardMenu:
78
85
  # Show tip if provided
79
86
  if tip:
80
87
  click.echo()
81
- click.echo(" " + click.style("💡 TIP:", fg='cyan', bold=True) + " " + tip)
88
+ click.echo(" " + click.style("💡 TIP:", fg="cyan", bold=True) + " " + tip)
82
89
 
83
90
  click.echo()
84
91
 
85
92
  try:
86
- choice_input = click.prompt("Select option", type=str, default="0", show_default=False)
93
+ choice_input = click.prompt(
94
+ "Select option", type=str, default="0", show_default=False
95
+ )
87
96
 
88
97
  # Handle keyboard shortcuts
89
98
  if shortcuts and choice_input.lower() in shortcuts: