flock-core 0.5.0b27__py3-none-any.whl → 0.5.0b50__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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (357) hide show
  1. flock/__init__.py +12 -217
  2. flock/agent.py +678 -0
  3. flock/api/themes.py +71 -0
  4. flock/artifacts.py +79 -0
  5. flock/cli.py +75 -0
  6. flock/components.py +173 -0
  7. flock/dashboard/__init__.py +28 -0
  8. flock/dashboard/collector.py +283 -0
  9. flock/dashboard/events.py +182 -0
  10. flock/dashboard/launcher.py +230 -0
  11. flock/dashboard/service.py +537 -0
  12. flock/dashboard/websocket.py +235 -0
  13. flock/engines/__init__.py +6 -0
  14. flock/engines/dspy_engine.py +856 -0
  15. flock/examples.py +128 -0
  16. flock/{core/util → helper}/cli_helper.py +4 -3
  17. flock/{core/logging → logging}/__init__.py +2 -3
  18. flock/{core/logging → logging}/formatters/enum_builder.py +3 -4
  19. flock/{core/logging → logging}/formatters/theme_builder.py +19 -44
  20. flock/{core/logging → logging}/formatters/themed_formatter.py +69 -115
  21. flock/{core/logging → logging}/logging.py +77 -61
  22. flock/{core/logging → logging}/telemetry.py +20 -26
  23. flock/{core/logging → logging}/telemetry_exporter/base_exporter.py +2 -2
  24. flock/{core/logging → logging}/telemetry_exporter/file_exporter.py +6 -9
  25. flock/{core/logging → logging}/telemetry_exporter/sqlite_exporter.py +2 -3
  26. flock/{core/logging → logging}/trace_and_logged.py +20 -24
  27. flock/mcp/__init__.py +91 -0
  28. flock/{core/mcp/mcp_client.py → mcp/client.py} +103 -154
  29. flock/{core/mcp/mcp_config.py → mcp/config.py} +62 -117
  30. flock/mcp/manager.py +255 -0
  31. flock/mcp/servers/sse/__init__.py +1 -1
  32. flock/mcp/servers/sse/flock_sse_server.py +11 -53
  33. flock/mcp/servers/stdio/__init__.py +1 -1
  34. flock/mcp/servers/stdio/flock_stdio_server.py +8 -48
  35. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +17 -62
  36. flock/mcp/servers/websockets/flock_websocket_server.py +7 -40
  37. flock/{core/mcp/flock_mcp_tool.py → mcp/tool.py} +16 -26
  38. flock/mcp/types/__init__.py +42 -0
  39. flock/{core/mcp → mcp}/types/callbacks.py +9 -15
  40. flock/{core/mcp → mcp}/types/factories.py +7 -6
  41. flock/{core/mcp → mcp}/types/handlers.py +13 -18
  42. flock/{core/mcp → mcp}/types/types.py +70 -74
  43. flock/{core/mcp → mcp}/util/helpers.py +1 -1
  44. flock/orchestrator.py +645 -0
  45. flock/registry.py +148 -0
  46. flock/runtime.py +262 -0
  47. flock/service.py +140 -0
  48. flock/store.py +69 -0
  49. flock/subscription.py +111 -0
  50. flock/themes/andromeda.toml +1 -1
  51. flock/themes/apple-system-colors.toml +1 -1
  52. flock/themes/arcoiris.toml +1 -1
  53. flock/themes/atomonelight.toml +1 -1
  54. flock/themes/ayu copy.toml +1 -1
  55. flock/themes/ayu-light.toml +1 -1
  56. flock/themes/belafonte-day.toml +1 -1
  57. flock/themes/belafonte-night.toml +1 -1
  58. flock/themes/blulocodark.toml +1 -1
  59. flock/themes/breeze.toml +1 -1
  60. flock/themes/broadcast.toml +1 -1
  61. flock/themes/brogrammer.toml +1 -1
  62. flock/themes/builtin-dark.toml +1 -1
  63. flock/themes/builtin-pastel-dark.toml +1 -1
  64. flock/themes/catppuccin-latte.toml +1 -1
  65. flock/themes/catppuccin-macchiato.toml +1 -1
  66. flock/themes/catppuccin-mocha.toml +1 -1
  67. flock/themes/cga.toml +1 -1
  68. flock/themes/chalk.toml +1 -1
  69. flock/themes/ciapre.toml +1 -1
  70. flock/themes/coffee-theme.toml +1 -1
  71. flock/themes/cyberpunkscarletprotocol.toml +1 -1
  72. flock/themes/dark+.toml +1 -1
  73. flock/themes/darkermatrix.toml +1 -1
  74. flock/themes/darkside.toml +1 -1
  75. flock/themes/desert.toml +1 -1
  76. flock/themes/django.toml +1 -1
  77. flock/themes/djangosmooth.toml +1 -1
  78. flock/themes/doomone.toml +1 -1
  79. flock/themes/dotgov.toml +1 -1
  80. flock/themes/dracula+.toml +1 -1
  81. flock/themes/duckbones.toml +1 -1
  82. flock/themes/encom.toml +1 -1
  83. flock/themes/espresso.toml +1 -1
  84. flock/themes/everblush.toml +1 -1
  85. flock/themes/fairyfloss.toml +1 -1
  86. flock/themes/fideloper.toml +1 -1
  87. flock/themes/fishtank.toml +1 -1
  88. flock/themes/flexoki-light.toml +1 -1
  89. flock/themes/floraverse.toml +1 -1
  90. flock/themes/framer.toml +1 -1
  91. flock/themes/galizur.toml +1 -1
  92. flock/themes/github.toml +1 -1
  93. flock/themes/grass.toml +1 -1
  94. flock/themes/grey-green.toml +1 -1
  95. flock/themes/gruvboxlight.toml +1 -1
  96. flock/themes/guezwhoz.toml +1 -1
  97. flock/themes/harper.toml +1 -1
  98. flock/themes/hax0r-blue.toml +1 -1
  99. flock/themes/hopscotch.256.toml +1 -1
  100. flock/themes/ic-green-ppl.toml +1 -1
  101. flock/themes/iceberg-dark.toml +1 -1
  102. flock/themes/japanesque.toml +1 -1
  103. flock/themes/jubi.toml +1 -1
  104. flock/themes/kibble.toml +1 -1
  105. flock/themes/kolorit.toml +1 -1
  106. flock/themes/kurokula.toml +1 -1
  107. flock/themes/materialdesigncolors.toml +1 -1
  108. flock/themes/matrix.toml +1 -1
  109. flock/themes/mellifluous.toml +1 -1
  110. flock/themes/midnight-in-mojave.toml +1 -1
  111. flock/themes/monokai-remastered.toml +1 -1
  112. flock/themes/monokai-soda.toml +1 -1
  113. flock/themes/neon.toml +1 -1
  114. flock/themes/neopolitan.toml +1 -1
  115. flock/themes/nord-light.toml +1 -1
  116. flock/themes/ocean.toml +1 -1
  117. flock/themes/onehalfdark.toml +1 -1
  118. flock/themes/onehalflight.toml +1 -1
  119. flock/themes/palenighthc.toml +1 -1
  120. flock/themes/paulmillr.toml +1 -1
  121. flock/themes/pencildark.toml +1 -1
  122. flock/themes/pnevma.toml +1 -1
  123. flock/themes/purple-rain.toml +1 -1
  124. flock/themes/purplepeter.toml +1 -1
  125. flock/themes/raycast-dark.toml +1 -1
  126. flock/themes/red-sands.toml +1 -1
  127. flock/themes/relaxed.toml +1 -1
  128. flock/themes/retro.toml +1 -1
  129. flock/themes/rose-pine.toml +1 -1
  130. flock/themes/royal.toml +1 -1
  131. flock/themes/ryuuko.toml +1 -1
  132. flock/themes/sakura.toml +1 -1
  133. flock/themes/scarlet-protocol.toml +1 -1
  134. flock/themes/seoulbones-dark.toml +1 -1
  135. flock/themes/shades-of-purple.toml +1 -1
  136. flock/themes/smyck.toml +1 -1
  137. flock/themes/softserver.toml +1 -1
  138. flock/themes/solarized-darcula.toml +1 -1
  139. flock/themes/square.toml +1 -1
  140. flock/themes/sugarplum.toml +1 -1
  141. flock/themes/thayer-bright.toml +1 -1
  142. flock/themes/tokyonight.toml +1 -1
  143. flock/themes/tomorrow.toml +1 -1
  144. flock/themes/ubuntu.toml +1 -1
  145. flock/themes/ultradark.toml +1 -1
  146. flock/themes/ultraviolent.toml +1 -1
  147. flock/themes/unikitty.toml +1 -1
  148. flock/themes/urple.toml +1 -1
  149. flock/themes/vesper.toml +1 -1
  150. flock/themes/vimbones.toml +1 -1
  151. flock/themes/wildcherry.toml +1 -1
  152. flock/themes/wilmersdorf.toml +1 -1
  153. flock/themes/wryan.toml +1 -1
  154. flock/themes/xcodedarkhc.toml +1 -1
  155. flock/themes/xcodelight.toml +1 -1
  156. flock/themes/zenbones-light.toml +1 -1
  157. flock/themes/zenwritten-dark.toml +1 -1
  158. flock/utilities.py +301 -0
  159. flock/{components/utility → utility}/output_utility_component.py +68 -53
  160. flock/visibility.py +107 -0
  161. flock_core-0.5.0b50.dist-info/METADATA +747 -0
  162. flock_core-0.5.0b50.dist-info/RECORD +398 -0
  163. flock_core-0.5.0b50.dist-info/entry_points.txt +2 -0
  164. {flock_core-0.5.0b27.dist-info → flock_core-0.5.0b50.dist-info}/licenses/LICENSE +1 -1
  165. flock/adapter/__init__.py +0 -14
  166. flock/adapter/azure_adapter.py +0 -68
  167. flock/adapter/chroma_adapter.py +0 -73
  168. flock/adapter/faiss_adapter.py +0 -97
  169. flock/adapter/pinecone_adapter.py +0 -51
  170. flock/adapter/vector_base.py +0 -47
  171. flock/cli/assets/release_notes.md +0 -140
  172. flock/cli/config.py +0 -8
  173. flock/cli/constants.py +0 -36
  174. flock/cli/create_agent.py +0 -1
  175. flock/cli/create_flock.py +0 -280
  176. flock/cli/execute_flock.py +0 -620
  177. flock/cli/load_agent.py +0 -1
  178. flock/cli/load_examples.py +0 -1
  179. flock/cli/load_flock.py +0 -192
  180. flock/cli/load_release_notes.py +0 -20
  181. flock/cli/loaded_flock_cli.py +0 -254
  182. flock/cli/manage_agents.py +0 -459
  183. flock/cli/registry_management.py +0 -889
  184. flock/cli/runner.py +0 -41
  185. flock/cli/settings.py +0 -857
  186. flock/cli/utils.py +0 -135
  187. flock/cli/view_results.py +0 -29
  188. flock/cli/yaml_editor.py +0 -396
  189. flock/components/__init__.py +0 -30
  190. flock/components/evaluation/__init__.py +0 -9
  191. flock/components/evaluation/declarative_evaluation_component.py +0 -606
  192. flock/components/routing/__init__.py +0 -15
  193. flock/components/routing/conditional_routing_component.py +0 -494
  194. flock/components/routing/default_routing_component.py +0 -103
  195. flock/components/routing/llm_routing_component.py +0 -206
  196. flock/components/utility/__init__.py +0 -15
  197. flock/components/utility/memory_utility_component.py +0 -550
  198. flock/components/utility/metrics_utility_component.py +0 -700
  199. flock/config.py +0 -61
  200. flock/core/__init__.py +0 -110
  201. flock/core/agent/__init__.py +0 -16
  202. flock/core/agent/default_agent.py +0 -180
  203. flock/core/agent/flock_agent_components.py +0 -104
  204. flock/core/agent/flock_agent_execution.py +0 -101
  205. flock/core/agent/flock_agent_integration.py +0 -260
  206. flock/core/agent/flock_agent_lifecycle.py +0 -186
  207. flock/core/agent/flock_agent_serialization.py +0 -381
  208. flock/core/api/__init__.py +0 -10
  209. flock/core/api/custom_endpoint.py +0 -45
  210. flock/core/api/endpoints.py +0 -254
  211. flock/core/api/main.py +0 -162
  212. flock/core/api/models.py +0 -97
  213. flock/core/api/run_store.py +0 -224
  214. flock/core/api/runner.py +0 -44
  215. flock/core/api/service.py +0 -214
  216. flock/core/component/__init__.py +0 -15
  217. flock/core/component/agent_component_base.py +0 -309
  218. flock/core/component/evaluation_component.py +0 -62
  219. flock/core/component/routing_component.py +0 -74
  220. flock/core/component/utility_component.py +0 -69
  221. flock/core/config/flock_agent_config.py +0 -58
  222. flock/core/config/scheduled_agent_config.py +0 -40
  223. flock/core/context/context.py +0 -213
  224. flock/core/context/context_manager.py +0 -37
  225. flock/core/context/context_vars.py +0 -10
  226. flock/core/evaluation/utils.py +0 -396
  227. flock/core/execution/batch_executor.py +0 -369
  228. flock/core/execution/evaluation_executor.py +0 -438
  229. flock/core/execution/local_executor.py +0 -31
  230. flock/core/execution/opik_executor.py +0 -103
  231. flock/core/execution/temporal_executor.py +0 -164
  232. flock/core/flock.py +0 -634
  233. flock/core/flock_agent.py +0 -336
  234. flock/core/flock_factory.py +0 -551
  235. flock/core/flock_scheduler.py +0 -166
  236. flock/core/flock_server_manager.py +0 -136
  237. flock/core/interpreter/python_interpreter.py +0 -689
  238. flock/core/mcp/__init__.py +0 -1
  239. flock/core/mcp/flock_mcp_server.py +0 -680
  240. flock/core/mcp/mcp_client_manager.py +0 -201
  241. flock/core/mcp/types/__init__.py +0 -1
  242. flock/core/mixin/dspy_integration.py +0 -403
  243. flock/core/mixin/prompt_parser.py +0 -125
  244. flock/core/orchestration/__init__.py +0 -15
  245. flock/core/orchestration/flock_batch_processor.py +0 -94
  246. flock/core/orchestration/flock_evaluator.py +0 -113
  247. flock/core/orchestration/flock_execution.py +0 -295
  248. flock/core/orchestration/flock_initialization.py +0 -149
  249. flock/core/orchestration/flock_server_manager.py +0 -67
  250. flock/core/orchestration/flock_web_server.py +0 -117
  251. flock/core/registry/__init__.py +0 -45
  252. flock/core/registry/agent_registry.py +0 -69
  253. flock/core/registry/callable_registry.py +0 -139
  254. flock/core/registry/component_discovery.py +0 -142
  255. flock/core/registry/component_registry.py +0 -64
  256. flock/core/registry/config_mapping.py +0 -64
  257. flock/core/registry/decorators.py +0 -137
  258. flock/core/registry/registry_hub.py +0 -205
  259. flock/core/registry/server_registry.py +0 -57
  260. flock/core/registry/type_registry.py +0 -86
  261. flock/core/serialization/__init__.py +0 -13
  262. flock/core/serialization/callable_registry.py +0 -52
  263. flock/core/serialization/flock_serializer.py +0 -832
  264. flock/core/serialization/json_encoder.py +0 -41
  265. flock/core/serialization/secure_serializer.py +0 -175
  266. flock/core/serialization/serializable.py +0 -342
  267. flock/core/serialization/serialization_utils.py +0 -412
  268. flock/core/util/file_path_utils.py +0 -223
  269. flock/core/util/hydrator.py +0 -309
  270. flock/core/util/input_resolver.py +0 -164
  271. flock/core/util/loader.py +0 -59
  272. flock/core/util/splitter.py +0 -219
  273. flock/di.py +0 -27
  274. flock/platform/docker_tools.py +0 -49
  275. flock/platform/jaeger_install.py +0 -86
  276. flock/webapp/__init__.py +0 -1
  277. flock/webapp/app/__init__.py +0 -0
  278. flock/webapp/app/api/__init__.py +0 -0
  279. flock/webapp/app/api/agent_management.py +0 -241
  280. flock/webapp/app/api/execution.py +0 -709
  281. flock/webapp/app/api/flock_management.py +0 -129
  282. flock/webapp/app/api/registry_viewer.py +0 -30
  283. flock/webapp/app/chat.py +0 -665
  284. flock/webapp/app/config.py +0 -104
  285. flock/webapp/app/dependencies.py +0 -117
  286. flock/webapp/app/main.py +0 -1070
  287. flock/webapp/app/middleware.py +0 -113
  288. flock/webapp/app/models_ui.py +0 -7
  289. flock/webapp/app/services/__init__.py +0 -0
  290. flock/webapp/app/services/feedback_file_service.py +0 -363
  291. flock/webapp/app/services/flock_service.py +0 -337
  292. flock/webapp/app/services/sharing_models.py +0 -81
  293. flock/webapp/app/services/sharing_store.py +0 -598
  294. flock/webapp/app/templates/theme_mapper.html +0 -326
  295. flock/webapp/app/theme_mapper.py +0 -812
  296. flock/webapp/app/utils.py +0 -85
  297. flock/webapp/run.py +0 -215
  298. flock/webapp/static/css/chat.css +0 -301
  299. flock/webapp/static/css/components.css +0 -167
  300. flock/webapp/static/css/header.css +0 -39
  301. flock/webapp/static/css/layout.css +0 -46
  302. flock/webapp/static/css/sidebar.css +0 -127
  303. flock/webapp/static/css/two-pane.css +0 -48
  304. flock/webapp/templates/base.html +0 -200
  305. flock/webapp/templates/chat.html +0 -152
  306. flock/webapp/templates/chat_settings.html +0 -19
  307. flock/webapp/templates/flock_editor.html +0 -16
  308. flock/webapp/templates/index.html +0 -12
  309. flock/webapp/templates/partials/_agent_detail_form.html +0 -93
  310. flock/webapp/templates/partials/_agent_list.html +0 -18
  311. flock/webapp/templates/partials/_agent_manager_view.html +0 -51
  312. flock/webapp/templates/partials/_agent_tools_checklist.html +0 -14
  313. flock/webapp/templates/partials/_chat_container.html +0 -15
  314. flock/webapp/templates/partials/_chat_messages.html +0 -57
  315. flock/webapp/templates/partials/_chat_settings_form.html +0 -85
  316. flock/webapp/templates/partials/_create_flock_form.html +0 -50
  317. flock/webapp/templates/partials/_dashboard_flock_detail.html +0 -17
  318. flock/webapp/templates/partials/_dashboard_flock_file_list.html +0 -16
  319. flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +0 -28
  320. flock/webapp/templates/partials/_dashboard_upload_flock_form.html +0 -16
  321. flock/webapp/templates/partials/_dynamic_input_form_content.html +0 -22
  322. flock/webapp/templates/partials/_env_vars_table.html +0 -23
  323. flock/webapp/templates/partials/_execution_form.html +0 -118
  324. flock/webapp/templates/partials/_execution_view_container.html +0 -28
  325. flock/webapp/templates/partials/_flock_file_list.html +0 -23
  326. flock/webapp/templates/partials/_flock_properties_form.html +0 -52
  327. flock/webapp/templates/partials/_flock_upload_form.html +0 -16
  328. flock/webapp/templates/partials/_header_flock_status.html +0 -5
  329. flock/webapp/templates/partials/_load_manager_view.html +0 -49
  330. flock/webapp/templates/partials/_registry_table.html +0 -25
  331. flock/webapp/templates/partials/_registry_viewer_content.html +0 -70
  332. flock/webapp/templates/partials/_results_display.html +0 -78
  333. flock/webapp/templates/partials/_settings_env_content.html +0 -9
  334. flock/webapp/templates/partials/_settings_theme_content.html +0 -14
  335. flock/webapp/templates/partials/_settings_view.html +0 -36
  336. flock/webapp/templates/partials/_share_chat_link_snippet.html +0 -11
  337. flock/webapp/templates/partials/_share_link_snippet.html +0 -35
  338. flock/webapp/templates/partials/_sidebar.html +0 -74
  339. flock/webapp/templates/partials/_streaming_results_container.html +0 -195
  340. flock/webapp/templates/partials/_structured_data_view.html +0 -40
  341. flock/webapp/templates/partials/_theme_preview.html +0 -36
  342. flock/webapp/templates/registry_viewer.html +0 -84
  343. flock/webapp/templates/shared_run_page.html +0 -140
  344. flock/workflow/__init__.py +0 -0
  345. flock/workflow/activities.py +0 -196
  346. flock/workflow/agent_activities.py +0 -24
  347. flock/workflow/agent_execution_activity.py +0 -202
  348. flock/workflow/flock_workflow.py +0 -214
  349. flock/workflow/temporal_config.py +0 -96
  350. flock/workflow/temporal_setup.py +0 -68
  351. flock_core-0.5.0b27.dist-info/METADATA +0 -274
  352. flock_core-0.5.0b27.dist-info/RECORD +0 -559
  353. flock_core-0.5.0b27.dist-info/entry_points.txt +0 -2
  354. /flock/{core/logging → logging}/formatters/themes.py +0 -0
  355. /flock/{core/logging → logging}/span_middleware/baggage_span_processor.py +0 -0
  356. /flock/{core/mcp → mcp}/util/__init__.py +0 -0
  357. {flock_core-0.5.0b27.dist-info → flock_core-0.5.0b50.dist-info}/WHEEL +0 -0
flock/cli/settings.py DELETED
@@ -1,857 +0,0 @@
1
- """Settings editor for the Flock CLI.
2
-
3
- This module provides functionality to view, edit, add, and delete
4
- environment variables in the .env file.
5
- """
6
-
7
- import os
8
- import shutil
9
- from pathlib import Path
10
- from typing import Dict, List, Optional, Tuple
11
- import math
12
-
13
- import questionary
14
- from rich.console import Console
15
- from rich.panel import Panel
16
- from rich.table import Table
17
- from rich.text import Text
18
-
19
- from flock.core.util.cli_helper import init_console
20
-
21
- # Constants
22
- ENV_FILE = ".env"
23
- ENV_TEMPLATE_FILE = ".env_template"
24
- ENV_PROFILE_PREFIX = ".env_"
25
- DEFAULT_PROFILE_COMMENT = "# Profile: {profile_name}"
26
- SHOW_SECRETS_KEY = "SHOW_SECRETS"
27
- VARS_PER_PAGE_KEY = "VARS_PER_PAGE"
28
- DEFAULT_VARS_PER_PAGE = 20
29
-
30
- console = Console()
31
-
32
-
33
- def settings_editor():
34
- """Main entry point for the settings editor."""
35
- while True:
36
- init_console()
37
- console.print(Panel("[bold green]Environment Settings Editor[/]"), justify="center")
38
-
39
- # Get current profile name
40
- current_profile = get_current_profile()
41
- if current_profile:
42
- console.print(f"Current Profile: [bold cyan]{current_profile}[/]")
43
- else:
44
- console.print("No profile detected")
45
-
46
- console.line()
47
-
48
- choice = questionary.select(
49
- "What would you like to do?",
50
- choices=[
51
- questionary.Separator(line=" "),
52
- "View all environment variables",
53
- "Edit an environment variable",
54
- "Add a new environment variable",
55
- "Delete an environment variable",
56
- questionary.Separator(),
57
- "Manage environment profiles",
58
- questionary.Separator(),
59
- "Toggle show secrets",
60
- "Change variables per page",
61
- questionary.Separator(),
62
- "Back to main menu",
63
- ],
64
- ).ask()
65
-
66
- if choice == "View all environment variables":
67
- view_env_variables()
68
- elif choice == "Edit an environment variable":
69
- edit_env_variable()
70
- elif choice == "Add a new environment variable":
71
- add_env_variable()
72
- elif choice == "Delete an environment variable":
73
- delete_env_variable()
74
- elif choice == "Manage environment profiles":
75
- manage_profiles()
76
- elif choice == "Toggle show secrets":
77
- toggle_show_secrets()
78
- elif choice == "Change variables per page":
79
- change_vars_per_page()
80
- elif choice == "Back to main menu":
81
- break
82
-
83
- if choice != "Back to main menu":
84
- input("\nPress Enter to continue...")
85
-
86
-
87
- def view_env_variables(page: int = 1, page_size: Optional[int] = None):
88
- """View all environment variables with pagination.
89
-
90
- Args:
91
- page: Page number to display
92
- page_size: Number of variables per page (if None, use the setting in .env)
93
- """
94
- env_vars = load_env_file()
95
-
96
- # If page_size is not specified, get it from settings
97
- if page_size is None:
98
- page_size = get_vars_per_page_setting(env_vars)
99
-
100
- # Calculate pagination
101
- total_vars = len(env_vars)
102
- total_pages = math.ceil(total_vars / page_size) if total_vars > 0 else 1
103
-
104
- # Validate page number
105
- page = min(max(1, page), total_pages)
106
-
107
- start_idx = (page - 1) * page_size
108
- end_idx = min(start_idx + page_size, total_vars)
109
-
110
- # Get current page variables
111
- current_page_vars = list(env_vars.items())[start_idx:end_idx]
112
-
113
- # Check if secrets should be shown
114
- show_secrets = get_show_secrets_setting(env_vars)
115
-
116
- # Create table
117
- table = Table(title=f"Environment Variables (Page {page}/{total_pages}, {page_size} per page)")
118
- table.add_column("Name", style="cyan")
119
- table.add_column("Value", style="green")
120
-
121
- # Show secrets status
122
- secrets_status = "[green]ON[/]" if show_secrets else "[red]OFF[/]"
123
- init_console()
124
- console.print(f"Show Secrets: {secrets_status}")
125
-
126
- for key, value in current_page_vars:
127
- # Skip lines that are comments or empty
128
- if key.startswith('#') or not key:
129
- continue
130
-
131
- # Mask sensitive values if show_secrets is False
132
- if is_sensitive(key) and not show_secrets:
133
- masked_value = mask_sensitive_value(value)
134
- table.add_row(key, masked_value)
135
- else:
136
- table.add_row(key, value)
137
-
138
- console.print(table)
139
-
140
- # Pagination controls with more intuitive shortcuts
141
- console.print("\nNavigation: ", end="")
142
- if page > 1:
143
- console.print("[bold]Previous (p)[/] | ", end="")
144
- if page < total_pages:
145
- console.print("[bold]Next (n)[/] | ", end="")
146
- if show_secrets:
147
- console.print("[bold]Hide secrets (h)[/] | ", end="")
148
- else:
149
- console.print("[bold]Show secrets (s)[/] | ", end="")
150
- console.print("[bold]Change variables per page (v)[/] | ", end="")
151
- console.print("[bold]Back (b)[/]")
152
-
153
- # Handle navigation
154
- while True:
155
- key = input("Enter option: ").lower()
156
- if key == 'p' and page > 1:
157
- view_env_variables(page - 1, page_size)
158
- break
159
- elif key == 'n' and page < total_pages:
160
- view_env_variables(page + 1, page_size)
161
- break
162
- elif key == 's' and not show_secrets:
163
- # Confirm showing secrets
164
- confirm = questionary.confirm("Are you sure you want to show sensitive values?").ask()
165
- if confirm:
166
- set_show_secrets_setting(True)
167
- view_env_variables(page, page_size)
168
- break
169
- elif key == 'h' and show_secrets:
170
- set_show_secrets_setting(False)
171
- view_env_variables(page, page_size)
172
- break
173
- elif key == 'v':
174
- new_page_size = change_vars_per_page()
175
- if new_page_size:
176
- view_env_variables(1, new_page_size) # Reset to first page with new page size
177
- break
178
- elif key == 'b':
179
- break
180
-
181
-
182
- def change_vars_per_page():
183
- """Change the number of variables displayed per page.
184
-
185
- Returns:
186
- The new page size or None if cancelled
187
- """
188
- env_vars = load_env_file()
189
- current_setting = get_vars_per_page_setting(env_vars)
190
-
191
- console.print(f"Current variables per page: [cyan]{current_setting}[/]")
192
-
193
- # Predefined options plus custom option
194
- page_size_options = ["10", "20", "30", "50", "Custom", "Cancel"]
195
-
196
- choice = questionary.select(
197
- "Select number of variables per page:",
198
- choices=page_size_options,
199
- ).ask()
200
-
201
- if choice == "Cancel":
202
- return None
203
-
204
- if choice == "Custom":
205
- while True:
206
- try:
207
- custom_size = questionary.text(
208
- "Enter custom page size (5-100):",
209
- default=str(current_setting)
210
- ).ask()
211
-
212
- if not custom_size:
213
- return None
214
-
215
- new_size = int(custom_size)
216
- if 5 <= new_size <= 100:
217
- break
218
- else:
219
- console.print("[yellow]Page size must be between 5 and 100.[/]")
220
- except ValueError:
221
- console.print("[yellow]Please enter a valid number.[/]")
222
- else:
223
- new_size = int(choice)
224
-
225
- # Save the setting
226
- set_vars_per_page_setting(new_size)
227
- console.print(f"[green]Variables per page set to {new_size}.[/]")
228
-
229
- return new_size
230
-
231
-
232
- def get_vars_per_page_setting(env_vars: Dict[str, str] = None) -> int:
233
- """Get the current variables per page setting.
234
-
235
- Args:
236
- env_vars: Optional dictionary of environment variables
237
-
238
- Returns:
239
- Number of variables per page
240
- """
241
- if env_vars is None:
242
- env_vars = load_env_file()
243
-
244
- if VARS_PER_PAGE_KEY in env_vars:
245
- try:
246
- page_size = int(env_vars[VARS_PER_PAGE_KEY])
247
- # Ensure the value is within reasonable bounds
248
- if 5 <= page_size <= 100:
249
- return page_size
250
- except ValueError:
251
- pass
252
-
253
- return DEFAULT_VARS_PER_PAGE
254
-
255
-
256
- def set_vars_per_page_setting(page_size: int):
257
- """Set the variables per page setting.
258
-
259
- Args:
260
- page_size: Number of variables to display per page
261
- """
262
- env_vars = load_env_file()
263
- env_vars[VARS_PER_PAGE_KEY] = str(page_size)
264
- save_env_file(env_vars)
265
-
266
-
267
- def toggle_show_secrets():
268
- """Toggle the show secrets setting."""
269
- env_vars = load_env_file()
270
- current_setting = get_show_secrets_setting(env_vars)
271
-
272
- if current_setting:
273
- console.print("Currently showing sensitive values. Do you want to hide them?")
274
- confirm = questionary.confirm("Hide sensitive values?").ask()
275
- if confirm:
276
- set_show_secrets_setting(False)
277
- console.print("[green]Sensitive values will now be masked.[/]")
278
- else:
279
- console.print("[yellow]Warning:[/] Showing sensitive values can expose sensitive information.")
280
- confirm = questionary.confirm("Are you sure you want to show sensitive values?").ask()
281
- if confirm:
282
- set_show_secrets_setting(True)
283
- console.print("[green]Sensitive values will now be shown.[/]")
284
-
285
-
286
- def get_show_secrets_setting(env_vars: Dict[str, str] = None) -> bool:
287
- """Get the current show secrets setting.
288
-
289
- Args:
290
- env_vars: Optional dictionary of environment variables
291
-
292
- Returns:
293
- True if secrets should be shown, False otherwise
294
- """
295
- if env_vars is None:
296
- env_vars = load_env_file()
297
-
298
- if SHOW_SECRETS_KEY in env_vars:
299
- return env_vars[SHOW_SECRETS_KEY].lower() == 'true'
300
-
301
- return False
302
-
303
-
304
- def set_show_secrets_setting(show_secrets: bool):
305
- """Set the show secrets setting.
306
-
307
- Args:
308
- show_secrets: Whether to show secrets
309
- """
310
- env_vars = load_env_file()
311
- env_vars[SHOW_SECRETS_KEY] = str(show_secrets)
312
- save_env_file(env_vars)
313
-
314
-
315
- def edit_env_variable():
316
- """Edit an environment variable."""
317
- # Get list of variables
318
- env_vars = load_env_file()
319
-
320
- if not env_vars:
321
- console.print("[yellow]No environment variables found to edit.[/]")
322
- return
323
-
324
- # Filter out comments
325
- variables = [k for k in env_vars.keys() if not k.startswith('#') and k]
326
-
327
- # Display variables with selection
328
- init_console()
329
- console.print("Select a variable to edit:")
330
-
331
- # Let user select a variable to edit
332
- var_name = questionary.select(
333
- "Select a variable to edit:",
334
- choices=variables + ["Cancel"],
335
- ).ask()
336
-
337
- if var_name == "Cancel":
338
- return
339
-
340
- current_value = env_vars[var_name]
341
- is_sensitive_var = is_sensitive(var_name)
342
-
343
- if is_sensitive_var:
344
- console.print(f"[yellow]Warning:[/] You are editing a sensitive value: {var_name}")
345
- confirm = questionary.confirm("Are you sure you want to continue?").ask()
346
- if not confirm:
347
- return
348
-
349
- # Show current value (masked if sensitive and show_secrets is False)
350
- show_secrets = get_show_secrets_setting(env_vars)
351
- if is_sensitive_var and not show_secrets:
352
- console.print(f"Current value: {mask_sensitive_value(current_value)}")
353
- else:
354
- console.print(f"Current value: {current_value}")
355
-
356
- # Get new value with hint
357
- console.print("[italic]Enter new value (or leave empty to cancel)[/]")
358
- new_value = questionary.text("Enter new value:", default=current_value).ask()
359
-
360
- if new_value is None:
361
- console.print("[yellow]Edit cancelled.[/]")
362
- return
363
-
364
- if new_value == "":
365
- # Confirm if user wants to set an empty value or cancel
366
- confirm = questionary.confirm("Do you want to set an empty value? Select No to cancel.", default=False).ask()
367
- if not confirm:
368
- console.print("[yellow]Edit cancelled.[/]")
369
- return
370
-
371
- if new_value == current_value:
372
- console.print("[yellow]No changes made.[/]")
373
- return
374
-
375
- # Update the value
376
- env_vars[var_name] = new_value
377
- save_env_file(env_vars)
378
- console.print(f"[green]Updated {var_name} successfully.[/]")
379
-
380
-
381
- def add_env_variable():
382
- """Add a new environment variable."""
383
- env_vars = load_env_file()
384
-
385
- console.print("[italic]Enter variable name (or leave empty to go back)[/]")
386
-
387
- # Get variable name
388
- while True:
389
- var_name = questionary.text("Enter variable name:").ask()
390
-
391
- if not var_name:
392
- # Ask if user wants to go back
393
- go_back = questionary.confirm("Do you want to go back to the settings menu?", default=True).ask()
394
- if go_back:
395
- return
396
- else:
397
- console.print("[italic]Please enter a variable name (or leave empty to go back)[/]")
398
- continue
399
-
400
- if var_name in env_vars and not var_name.startswith('#'):
401
- console.print(f"[yellow]Variable {var_name} already exists. Please use edit instead.[/]")
402
- continue
403
-
404
- break
405
-
406
- # Get variable value
407
- var_value = questionary.text("Enter variable value:").ask()
408
-
409
- # Add to env_vars
410
- env_vars[var_name] = var_value
411
- save_env_file(env_vars)
412
- console.print(f"[green]Added {var_name} successfully.[/]")
413
-
414
-
415
- def delete_env_variable():
416
- """Delete an environment variable."""
417
- # Get list of variables
418
- env_vars = load_env_file()
419
-
420
- if not env_vars:
421
- console.print("[yellow]No environment variables found to delete.[/]")
422
- return
423
-
424
- # Filter out comments
425
- variables = [k for k in env_vars.keys() if not k.startswith('#') and k]
426
-
427
- # Display variables with selection
428
- init_console()
429
- console.print("Select a variable to delete:")
430
-
431
- # Let user select a variable to delete with hint
432
- var_name = questionary.select(
433
- "Select a variable to delete:",
434
- choices=variables + ["Cancel"],
435
- ).ask()
436
-
437
- if var_name == "Cancel":
438
- return
439
-
440
- # Confirm deletion
441
- confirm = questionary.confirm(f"Are you sure you want to delete {var_name}?").ask()
442
- if not confirm:
443
- console.print("[yellow]Deletion cancelled.[/]")
444
- return
445
-
446
- # Delete the variable
447
- del env_vars[var_name]
448
- save_env_file(env_vars)
449
- console.print(f"[green]Deleted {var_name} successfully.[/]")
450
-
451
-
452
- def manage_profiles():
453
- """Manage environment profiles."""
454
- init_console()
455
- console.print(Panel("[bold green]Environment Profile Management[/]"), justify="center")
456
-
457
- # Get current profile and available profiles
458
- current_profile = get_current_profile()
459
- available_profiles = get_available_profiles()
460
-
461
- if current_profile:
462
- console.print(f"Current Profile: [bold cyan]{current_profile}[/]")
463
-
464
- if not available_profiles:
465
- console.print("[yellow]No profiles found.[/]")
466
- else:
467
- console.print("Available Profiles:")
468
- for profile in available_profiles:
469
- if profile == current_profile:
470
- console.print(f" [bold cyan]{profile} (active)[/]")
471
- else:
472
- console.print(f" {profile}")
473
-
474
- console.line()
475
-
476
- # Profile management options
477
- choice = questionary.select(
478
- "What would you like to do?",
479
- choices=[
480
- questionary.Separator(line=" "),
481
- "Switch to a different profile",
482
- "Create a new profile",
483
- "Rename a profile",
484
- "Delete a profile",
485
- "Back to settings menu",
486
- ],
487
- ).ask()
488
-
489
- if choice == "Switch to a different profile":
490
- switch_profile()
491
- elif choice == "Create a new profile":
492
- create_profile()
493
- elif choice == "Rename a profile":
494
- rename_profile()
495
- elif choice == "Delete a profile":
496
- delete_profile()
497
-
498
-
499
- def switch_profile():
500
- """Switch to a different environment profile."""
501
- available_profiles = get_available_profiles()
502
- current_profile = get_current_profile()
503
-
504
- if not available_profiles:
505
- console.print("[yellow]No profiles available to switch to.[/]")
506
- return
507
-
508
- # Remove current profile from the list to avoid switching to the same profile
509
- selectable_profiles = [p for p in available_profiles if p != current_profile]
510
-
511
- if not selectable_profiles:
512
- console.print("[yellow]No other profiles available to switch to.[/]")
513
- return
514
-
515
- target_profile = questionary.select(
516
- "Select a profile to switch to:",
517
- choices=selectable_profiles + ["Cancel"],
518
- ).ask()
519
-
520
- if target_profile == "Cancel":
521
- return
522
-
523
- # Confirm switch
524
- confirm = questionary.confirm(f"Are you sure you want to switch to the {target_profile} profile?").ask()
525
- if not confirm:
526
- return
527
-
528
- # Backup current .env file
529
- backup_env_file()
530
-
531
- # Copy selected profile to .env
532
- source_file = f"{ENV_PROFILE_PREFIX}{target_profile}"
533
- if os.path.exists(source_file):
534
- shutil.copy2(source_file, ENV_FILE)
535
- console.print(f"[green]Switched to {target_profile} profile successfully.[/]")
536
- else:
537
- console.print(f"[red]Error: Could not find profile file {source_file}.[/]")
538
-
539
-
540
- def create_profile():
541
- """Create a new environment profile."""
542
- profile_name = questionary.text("Enter new profile name:").ask()
543
-
544
- if not profile_name:
545
- console.print("[yellow]Profile name cannot be empty.[/]")
546
- return
547
-
548
- # Check if profile already exists
549
- target_file = f"{ENV_PROFILE_PREFIX}{profile_name}"
550
- if os.path.exists(target_file):
551
- console.print(f"[yellow]Profile {profile_name} already exists.[/]")
552
- return
553
-
554
- # Determine source file - use current .env or template
555
- source_choices = ["Current environment (.env)", ".env_template"]
556
- if os.path.exists(ENV_TEMPLATE_FILE):
557
- source_choices.append(ENV_TEMPLATE_FILE)
558
-
559
- source_choice = questionary.select(
560
- "Create profile based on:",
561
- choices=source_choices + ["Cancel"],
562
- ).ask()
563
-
564
- if source_choice == "Cancel":
565
- return
566
-
567
- source_file = ENV_FILE if source_choice == "Current environment (.env)" else ENV_TEMPLATE_FILE
568
-
569
- if not os.path.exists(source_file):
570
- console.print(f"[red]Error: Source file {source_file} not found.[/]")
571
- return
572
-
573
- # Create new profile file
574
- try:
575
- # Copy source file
576
- shutil.copy2(source_file, target_file)
577
-
578
- # Add profile header if it doesn't exist
579
- with open(target_file, 'r') as file:
580
- content = file.read()
581
-
582
- if not content.startswith("# Profile:"):
583
- with open(target_file, 'w') as file:
584
- profile_header = DEFAULT_PROFILE_COMMENT.format(profile_name=profile_name)
585
- file.write(f"{profile_header}\n{content}")
586
-
587
- console.print(f"[green]Created {profile_name} profile successfully.[/]")
588
- except Exception as e:
589
- console.print(f"[red]Error creating profile: {str(e)}[/]")
590
-
591
-
592
- def rename_profile():
593
- """Rename an existing profile."""
594
- available_profiles = get_available_profiles()
595
- current_profile = get_current_profile()
596
-
597
- if not available_profiles:
598
- console.print("[yellow]No profiles available to rename.[/]")
599
- return
600
-
601
- # Let user select a profile to rename
602
- profile_to_rename = questionary.select(
603
- "Select a profile to rename:",
604
- choices=available_profiles + ["Cancel"],
605
- ).ask()
606
-
607
- if profile_to_rename == "Cancel":
608
- return
609
-
610
- # Get new name
611
- new_name = questionary.text("Enter new profile name:").ask()
612
-
613
- if not new_name:
614
- console.print("[yellow]New profile name cannot be empty.[/]")
615
- return
616
-
617
- if new_name in available_profiles:
618
- console.print(f"[yellow]Profile {new_name} already exists.[/]")
619
- return
620
-
621
- # Rename profile file
622
- source_file = f"{ENV_PROFILE_PREFIX}{profile_to_rename}"
623
- target_file = f"{ENV_PROFILE_PREFIX}{new_name}"
624
-
625
- try:
626
- # Read content of the source file
627
- with open(source_file, 'r') as file:
628
- content = file.readlines()
629
-
630
- # Update profile header if it exists
631
- if content and content[0].startswith("# Profile:"):
632
- content[0] = DEFAULT_PROFILE_COMMENT.format(profile_name=new_name) + "\n"
633
-
634
- # Write to new file
635
- with open(target_file, 'w') as file:
636
- file.writelines(content)
637
-
638
- # Remove old file
639
- os.remove(source_file)
640
-
641
- # If this was the current profile, update .env as well
642
- if profile_to_rename == current_profile:
643
- with open(ENV_FILE, 'r') as file:
644
- content = file.readlines()
645
-
646
- if content and content[0].startswith("# Profile:"):
647
- content[0] = DEFAULT_PROFILE_COMMENT.format(profile_name=new_name) + "\n"
648
-
649
- with open(ENV_FILE, 'w') as file:
650
- file.writelines(content)
651
-
652
- console.print(f"[green]Renamed {profile_to_rename} to {new_name} successfully.[/]")
653
- except Exception as e:
654
- console.print(f"[red]Error renaming profile: {str(e)}[/]")
655
-
656
-
657
- def delete_profile():
658
- """Delete an existing profile."""
659
- available_profiles = get_available_profiles()
660
- current_profile = get_current_profile()
661
-
662
- if not available_profiles:
663
- console.print("[yellow]No profiles available to delete.[/]")
664
- return
665
-
666
- # Let user select a profile to delete
667
- profile_to_delete = questionary.select(
668
- "Select a profile to delete:",
669
- choices=available_profiles + ["Cancel"],
670
- ).ask()
671
-
672
- if profile_to_delete == "Cancel":
673
- return
674
-
675
- # Confirm deletion
676
- confirm = questionary.confirm(
677
- f"Are you sure you want to delete the {profile_to_delete} profile? This cannot be undone."
678
- ).ask()
679
-
680
- if not confirm:
681
- return
682
-
683
- # Delete profile file
684
- profile_file = f"{ENV_PROFILE_PREFIX}{profile_to_delete}"
685
-
686
- try:
687
- os.remove(profile_file)
688
-
689
- # Warn if deleting the current profile
690
- if profile_to_delete == current_profile:
691
- console.print(
692
- f"[yellow]Warning: You deleted the currently active profile. "
693
- f"The .env file still contains those settings but is no longer marked as a profile.[/]"
694
- )
695
-
696
- # Remove profile header from .env
697
- with open(ENV_FILE, 'r') as file:
698
- content = file.readlines()
699
-
700
- if content and content[0].startswith("# Profile:"):
701
- content = content[1:]
702
- with open(ENV_FILE, 'w') as file:
703
- file.writelines(content)
704
-
705
- console.print(f"[green]Deleted {profile_to_delete} profile successfully.[/]")
706
- except Exception as e:
707
- console.print(f"[red]Error deleting profile: {str(e)}[/]")
708
-
709
-
710
- def is_sensitive(key: str) -> bool:
711
- """Check if a variable is considered sensitive.
712
-
713
- Args:
714
- key: The variable name
715
-
716
- Returns:
717
- True if sensitive, False otherwise
718
- """
719
- sensitive_patterns = ['key', 'token', 'secret', 'password', 'api', 'pat']
720
- key_lower = key.lower()
721
- return any(pattern in key_lower for pattern in sensitive_patterns)
722
-
723
-
724
- def mask_sensitive_value(value: str) -> str:
725
- """Mask a sensitive value.
726
-
727
- Args:
728
- value: The sensitive value
729
-
730
- Returns:
731
- Masked value
732
- """
733
- if not value:
734
- return value
735
-
736
- if len(value) <= 4:
737
- return "••••"
738
-
739
- # Show first 2 and last 2 characters
740
- return value[:2] + "•" * (len(value) - 4) + value[-2:]
741
-
742
-
743
- def get_current_profile() -> Optional[str]:
744
- """Get the name of the current active profile.
745
-
746
- Returns:
747
- Profile name or None if no profile is active
748
- """
749
- if not os.path.exists(ENV_FILE):
750
- return None
751
-
752
- try:
753
- with open(ENV_FILE, 'r') as file:
754
- first_line = file.readline().strip()
755
-
756
- if first_line.startswith("# Profile:"):
757
- return first_line.replace("# Profile:", "").strip()
758
- except Exception:
759
- pass
760
-
761
- return None
762
-
763
-
764
- def get_available_profiles() -> List[str]:
765
- """Get a list of available profiles.
766
-
767
- Returns:
768
- List of profile names
769
- """
770
- profiles = []
771
-
772
- for file in os.listdir():
773
- if file.startswith(ENV_PROFILE_PREFIX):
774
- profile_name = file[len(ENV_PROFILE_PREFIX):]
775
- profiles.append(profile_name)
776
-
777
- return profiles
778
-
779
-
780
- def backup_env_file():
781
- """Create a backup of the current .env file."""
782
- if not os.path.exists(ENV_FILE):
783
- return
784
-
785
- backup_file = f"{ENV_FILE}.bak"
786
- shutil.copy2(ENV_FILE, backup_file)
787
-
788
-
789
- def load_env_file() -> Dict[str, str]:
790
- """Load the .env file into a dictionary.
791
-
792
- Returns:
793
- Dictionary of environment variables
794
- """
795
- env_vars = {}
796
-
797
- if not os.path.exists(ENV_FILE):
798
- console.print(f"[yellow]Warning: {ENV_FILE} file not found.[/]")
799
- return env_vars
800
-
801
- try:
802
- with open(ENV_FILE, 'r') as file:
803
- lines = file.readlines()
804
-
805
- # Process each line
806
- for line in lines:
807
- line = line.strip()
808
-
809
- # Skip empty lines
810
- if not line:
811
- env_vars[""] = ""
812
- continue
813
-
814
- # Handle comments
815
- if line.startswith('#'):
816
- env_vars[line] = ""
817
- continue
818
-
819
- # Handle regular variables
820
- if '=' in line:
821
- key, value = line.split('=', 1)
822
- env_vars[key] = value
823
- else:
824
- # Handle lines without equals sign
825
- env_vars[line] = ""
826
-
827
- except Exception as e:
828
- console.print(f"[red]Error loading .env file: {str(e)}[/]")
829
-
830
- return env_vars
831
-
832
-
833
- def save_env_file(env_vars: Dict[str, str]):
834
- """Save environment variables back to the .env file.
835
-
836
- Args:
837
- env_vars: Dictionary of environment variables
838
- """
839
- # Create backup
840
- backup_env_file()
841
-
842
- try:
843
- with open(ENV_FILE, 'w') as file:
844
- for key, value in env_vars.items():
845
- if key.startswith('#'):
846
- # Write comments as is
847
- file.write(f"{key}\n")
848
- elif not key:
849
- # Write empty lines
850
- file.write("\n")
851
- else:
852
- # Write regular variables
853
- file.write(f"{key}={value}\n")
854
-
855
- console.print("[green]Settings saved successfully.[/]")
856
- except Exception as e:
857
- console.print(f"[red]Error saving .env file: {str(e)}[/]")