zrb 1.15.3__py3-none-any.whl → 2.0.0a4__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 zrb might be problematic. Click here for more details.

Files changed (204) hide show
  1. zrb/__init__.py +118 -133
  2. zrb/attr/type.py +10 -7
  3. zrb/builtin/__init__.py +55 -1
  4. zrb/builtin/git.py +12 -1
  5. zrb/builtin/group.py +31 -15
  6. zrb/builtin/llm/chat.py +147 -0
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
  9. zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
  10. zrb/builtin/searxng/config/settings.yml +5671 -0
  11. zrb/builtin/searxng/start.py +21 -0
  12. zrb/builtin/shell/autocomplete/bash.py +4 -3
  13. zrb/builtin/shell/autocomplete/zsh.py +4 -3
  14. zrb/callback/callback.py +8 -1
  15. zrb/cmd/cmd_result.py +2 -1
  16. zrb/config/config.py +555 -169
  17. zrb/config/helper.py +84 -0
  18. zrb/config/web_auth_config.py +50 -35
  19. zrb/context/any_shared_context.py +20 -3
  20. zrb/context/context.py +39 -5
  21. zrb/context/print_fn.py +13 -0
  22. zrb/context/shared_context.py +17 -8
  23. zrb/group/any_group.py +3 -3
  24. zrb/group/group.py +3 -3
  25. zrb/input/any_input.py +5 -1
  26. zrb/input/base_input.py +18 -6
  27. zrb/input/option_input.py +41 -1
  28. zrb/input/text_input.py +7 -24
  29. zrb/llm/agent/__init__.py +9 -0
  30. zrb/llm/agent/agent.py +215 -0
  31. zrb/llm/agent/summarizer.py +20 -0
  32. zrb/llm/app/__init__.py +10 -0
  33. zrb/llm/app/completion.py +281 -0
  34. zrb/llm/app/confirmation/allow_tool.py +66 -0
  35. zrb/llm/app/confirmation/handler.py +178 -0
  36. zrb/llm/app/confirmation/replace_confirmation.py +77 -0
  37. zrb/llm/app/keybinding.py +34 -0
  38. zrb/llm/app/layout.py +117 -0
  39. zrb/llm/app/lexer.py +155 -0
  40. zrb/llm/app/redirection.py +28 -0
  41. zrb/llm/app/style.py +16 -0
  42. zrb/llm/app/ui.py +733 -0
  43. zrb/llm/config/__init__.py +4 -0
  44. zrb/llm/config/config.py +122 -0
  45. zrb/llm/config/limiter.py +247 -0
  46. zrb/llm/history_manager/__init__.py +4 -0
  47. zrb/llm/history_manager/any_history_manager.py +23 -0
  48. zrb/llm/history_manager/file_history_manager.py +91 -0
  49. zrb/llm/history_processor/summarizer.py +108 -0
  50. zrb/llm/note/__init__.py +3 -0
  51. zrb/llm/note/manager.py +122 -0
  52. zrb/llm/prompt/__init__.py +29 -0
  53. zrb/llm/prompt/claude_compatibility.py +92 -0
  54. zrb/llm/prompt/compose.py +55 -0
  55. zrb/llm/prompt/default.py +51 -0
  56. zrb/llm/prompt/markdown/file_extractor.md +112 -0
  57. zrb/llm/prompt/markdown/mandate.md +23 -0
  58. zrb/llm/prompt/markdown/persona.md +3 -0
  59. zrb/llm/prompt/markdown/repo_extractor.md +112 -0
  60. zrb/llm/prompt/markdown/repo_summarizer.md +29 -0
  61. zrb/llm/prompt/markdown/summarizer.md +21 -0
  62. zrb/llm/prompt/note.py +41 -0
  63. zrb/llm/prompt/system_context.py +46 -0
  64. zrb/llm/prompt/zrb.py +41 -0
  65. zrb/llm/skill/__init__.py +3 -0
  66. zrb/llm/skill/manager.py +86 -0
  67. zrb/llm/task/__init__.py +4 -0
  68. zrb/llm/task/llm_chat_task.py +316 -0
  69. zrb/llm/task/llm_task.py +245 -0
  70. zrb/llm/tool/__init__.py +39 -0
  71. zrb/llm/tool/bash.py +75 -0
  72. zrb/llm/tool/code.py +266 -0
  73. zrb/llm/tool/file.py +419 -0
  74. zrb/llm/tool/note.py +70 -0
  75. zrb/{builtin/llm → llm}/tool/rag.py +33 -37
  76. zrb/llm/tool/search/brave.py +53 -0
  77. zrb/llm/tool/search/searxng.py +47 -0
  78. zrb/llm/tool/search/serpapi.py +47 -0
  79. zrb/llm/tool/skill.py +19 -0
  80. zrb/llm/tool/sub_agent.py +70 -0
  81. zrb/llm/tool/web.py +97 -0
  82. zrb/llm/tool/zrb_task.py +66 -0
  83. zrb/llm/util/attachment.py +101 -0
  84. zrb/llm/util/prompt.py +104 -0
  85. zrb/llm/util/stream_response.py +178 -0
  86. zrb/runner/cli.py +21 -20
  87. zrb/runner/common_util.py +24 -19
  88. zrb/runner/web_route/task_input_api_route.py +5 -5
  89. zrb/runner/web_util/user.py +7 -3
  90. zrb/session/any_session.py +12 -9
  91. zrb/session/session.py +38 -17
  92. zrb/task/any_task.py +24 -3
  93. zrb/task/base/context.py +42 -22
  94. zrb/task/base/execution.py +67 -55
  95. zrb/task/base/lifecycle.py +14 -7
  96. zrb/task/base/monitoring.py +12 -7
  97. zrb/task/base_task.py +113 -50
  98. zrb/task/base_trigger.py +16 -6
  99. zrb/task/cmd_task.py +6 -0
  100. zrb/task/http_check.py +11 -5
  101. zrb/task/make_task.py +5 -3
  102. zrb/task/rsync_task.py +30 -10
  103. zrb/task/scaffolder.py +7 -4
  104. zrb/task/scheduler.py +7 -4
  105. zrb/task/tcp_check.py +6 -4
  106. zrb/util/ascii_art/art/bee.txt +17 -0
  107. zrb/util/ascii_art/art/cat.txt +9 -0
  108. zrb/util/ascii_art/art/ghost.txt +16 -0
  109. zrb/util/ascii_art/art/panda.txt +17 -0
  110. zrb/util/ascii_art/art/rose.txt +14 -0
  111. zrb/util/ascii_art/art/unicorn.txt +15 -0
  112. zrb/util/ascii_art/banner.py +92 -0
  113. zrb/util/attr.py +54 -39
  114. zrb/util/cli/markdown.py +32 -0
  115. zrb/util/cli/text.py +30 -0
  116. zrb/util/cmd/command.py +33 -10
  117. zrb/util/file.py +61 -33
  118. zrb/util/git.py +2 -2
  119. zrb/util/{llm/prompt.py → markdown.py} +2 -3
  120. zrb/util/match.py +78 -0
  121. zrb/util/run.py +3 -3
  122. zrb/util/string/conversion.py +1 -1
  123. zrb/util/truncate.py +23 -0
  124. zrb/util/yaml.py +204 -0
  125. zrb/xcom/xcom.py +10 -0
  126. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/METADATA +41 -27
  127. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/RECORD +129 -131
  128. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +1 -1
  129. zrb/attr/__init__.py +0 -0
  130. zrb/builtin/llm/chat_session.py +0 -311
  131. zrb/builtin/llm/history.py +0 -71
  132. zrb/builtin/llm/input.py +0 -27
  133. zrb/builtin/llm/llm_ask.py +0 -187
  134. zrb/builtin/llm/previous-session.js +0 -21
  135. zrb/builtin/llm/tool/__init__.py +0 -0
  136. zrb/builtin/llm/tool/api.py +0 -71
  137. zrb/builtin/llm/tool/cli.py +0 -38
  138. zrb/builtin/llm/tool/code.py +0 -254
  139. zrb/builtin/llm/tool/file.py +0 -626
  140. zrb/builtin/llm/tool/sub_agent.py +0 -137
  141. zrb/builtin/llm/tool/web.py +0 -195
  142. zrb/builtin/project/__init__.py +0 -0
  143. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
  144. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
  145. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
  146. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
  147. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
  148. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
  149. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
  150. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
  151. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
  152. zrb/builtin/project/create/__init__.py +0 -0
  153. zrb/builtin/shell/__init__.py +0 -0
  154. zrb/builtin/shell/autocomplete/__init__.py +0 -0
  155. zrb/callback/__init__.py +0 -0
  156. zrb/cmd/__init__.py +0 -0
  157. zrb/config/default_prompt/file_extractor_system_prompt.md +0 -12
  158. zrb/config/default_prompt/interactive_system_prompt.md +0 -35
  159. zrb/config/default_prompt/persona.md +0 -1
  160. zrb/config/default_prompt/repo_extractor_system_prompt.md +0 -112
  161. zrb/config/default_prompt/repo_summarizer_system_prompt.md +0 -10
  162. zrb/config/default_prompt/summarization_prompt.md +0 -16
  163. zrb/config/default_prompt/system_prompt.md +0 -32
  164. zrb/config/llm_config.py +0 -243
  165. zrb/config/llm_context/config.py +0 -129
  166. zrb/config/llm_context/config_parser.py +0 -46
  167. zrb/config/llm_rate_limitter.py +0 -137
  168. zrb/content_transformer/__init__.py +0 -0
  169. zrb/context/__init__.py +0 -0
  170. zrb/dot_dict/__init__.py +0 -0
  171. zrb/env/__init__.py +0 -0
  172. zrb/group/__init__.py +0 -0
  173. zrb/input/__init__.py +0 -0
  174. zrb/runner/__init__.py +0 -0
  175. zrb/runner/web_route/__init__.py +0 -0
  176. zrb/runner/web_route/home_page/__init__.py +0 -0
  177. zrb/session/__init__.py +0 -0
  178. zrb/session_state_log/__init__.py +0 -0
  179. zrb/session_state_logger/__init__.py +0 -0
  180. zrb/task/__init__.py +0 -0
  181. zrb/task/base/__init__.py +0 -0
  182. zrb/task/llm/__init__.py +0 -0
  183. zrb/task/llm/agent.py +0 -243
  184. zrb/task/llm/config.py +0 -103
  185. zrb/task/llm/conversation_history.py +0 -128
  186. zrb/task/llm/conversation_history_model.py +0 -242
  187. zrb/task/llm/default_workflow/coding.md +0 -24
  188. zrb/task/llm/default_workflow/copywriting.md +0 -17
  189. zrb/task/llm/default_workflow/researching.md +0 -18
  190. zrb/task/llm/error.py +0 -95
  191. zrb/task/llm/history_summarization.py +0 -216
  192. zrb/task/llm/print_node.py +0 -101
  193. zrb/task/llm/prompt.py +0 -325
  194. zrb/task/llm/tool_wrapper.py +0 -220
  195. zrb/task/llm/typing.py +0 -3
  196. zrb/task/llm_task.py +0 -341
  197. zrb/task_status/__init__.py +0 -0
  198. zrb/util/__init__.py +0 -0
  199. zrb/util/cli/__init__.py +0 -0
  200. zrb/util/cmd/__init__.py +0 -0
  201. zrb/util/codemod/__init__.py +0 -0
  202. zrb/util/string/__init__.py +0 -0
  203. zrb/xcom/__init__.py +0 -0
  204. {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/entry_points.txt +0 -0
zrb/util/attr.py CHANGED
@@ -9,52 +9,59 @@ from zrb.attr.type import (
9
9
  StrDictAttr,
10
10
  StrListAttr,
11
11
  )
12
+ from zrb.context.any_context import AnyContext
12
13
  from zrb.context.any_shared_context import AnySharedContext
13
14
  from zrb.util.string.conversion import to_boolean
14
15
 
15
16
 
16
17
  def get_str_list_attr(
17
- shared_ctx: AnySharedContext, attr: StrListAttr | None, auto_render: bool = True
18
+ ctx: AnyContext | AnySharedContext,
19
+ attr: StrListAttr | None,
20
+ auto_render: bool = True,
18
21
  ) -> list[str]:
19
22
  """
20
23
  Retrieve a list of strings from shared context attributes.
21
24
 
22
25
  Args:
23
- shared_ctx (AnySharedContext): The shared context object.
26
+ ctx (AnyContext): The shared context object.
24
27
  attr (StrListAttr | None): The string list attribute to retrieve.
25
28
  auto_render (bool): Whether to auto-render the attribute values.
26
29
 
27
30
  Returns:
28
31
  list[str]: A list of string attributes.
29
32
  """
33
+ if attr is None:
34
+ return []
30
35
  if callable(attr):
31
- return attr(shared_ctx)
32
- return {get_str_attr(shared_ctx, val, "", auto_render) for val in attr}
36
+ return attr(ctx)
37
+ return [get_str_attr(ctx, val, "", auto_render) for val in attr]
33
38
 
34
39
 
35
40
  def get_str_dict_attr(
36
- shared_ctx: AnySharedContext, attr: StrDictAttr | None, auto_render: bool = True
41
+ ctx: AnyContext | AnySharedContext,
42
+ attr: StrDictAttr | None,
43
+ auto_render: bool = True,
37
44
  ) -> dict[str, Any]:
38
45
  """
39
46
  Retrieve a dictionary of strings from shared context attributes.
40
47
 
41
48
  Args:
42
- shared_ctx (AnySharedContext): The shared context object.
49
+ ctx (AnyContext): The shared context object.
43
50
  attr (StrDictAttr | None): The string dictionary attribute to retrieve.
44
51
  auto_render (bool): Whether to auto-render the attribute values.
45
52
 
46
53
  Returns:
47
54
  dict[str, Any]: A dictionary of string attributes.
48
55
  """
56
+ if attr is None:
57
+ return {}
49
58
  if callable(attr):
50
- return attr(shared_ctx)
51
- return {
52
- key: get_str_attr(shared_ctx, val, "", auto_render) for key, val in attr.items()
53
- }
59
+ return attr(ctx)
60
+ return {key: get_str_attr(ctx, val, "", auto_render) for key, val in attr.items()}
54
61
 
55
62
 
56
63
  def get_str_attr(
57
- shared_ctx: AnySharedContext,
64
+ ctx: AnyContext | AnySharedContext,
58
65
  attr: StrAttr | None,
59
66
  default: StrAttr = "",
60
67
  auto_render: bool = True,
@@ -63,7 +70,7 @@ def get_str_attr(
63
70
  Retrieve a string from shared context attributes.
64
71
 
65
72
  Args:
66
- shared_ctx (AnySharedContext): The shared context object.
73
+ ctx (AnyContext): The shared context object.
67
74
  attr (StrAttr | None): The string attribute to retrieve.
68
75
  default (StrAttr): The default value if the attribute is None.
69
76
  auto_render (bool): Whether to auto-render the attribute value.
@@ -71,14 +78,16 @@ def get_str_attr(
71
78
  Returns:
72
79
  str: The string attribute value.
73
80
  """
74
- val = get_attr(shared_ctx, attr, default, auto_render)
75
- if not isinstance(val, str):
76
- return str(val)
77
- return val
81
+ val = get_attr(ctx, attr, default, auto_render)
82
+ if isinstance(val, str):
83
+ return val
84
+ if val is None:
85
+ return ""
86
+ return str(val)
78
87
 
79
88
 
80
89
  def get_bool_attr(
81
- shared_ctx: AnySharedContext,
90
+ ctx: AnyContext | AnySharedContext,
82
91
  attr: BoolAttr | None,
83
92
  default: BoolAttr = False,
84
93
  auto_render: bool = True,
@@ -87,7 +96,7 @@ def get_bool_attr(
87
96
  Retrieve a boolean from shared context attributes.
88
97
 
89
98
  Args:
90
- shared_ctx (AnySharedContext): The shared context object.
99
+ ctx (AnyContext): The shared context object.
91
100
  attr (BoolAttr | None): The boolean attribute to retrieve.
92
101
  default (BoolAttr): The default value if the attribute is None.
93
102
  auto_render (bool): Whether to auto-render the attribute value if it's a string.
@@ -95,14 +104,16 @@ def get_bool_attr(
95
104
  Returns:
96
105
  bool: The boolean attribute value.
97
106
  """
98
- val = get_attr(shared_ctx, attr, default, auto_render)
99
- if isinstance(val, str):
100
- return to_boolean(val)
101
- return val
107
+ val = get_attr(ctx, attr, default, auto_render)
108
+ if isinstance(val, bool):
109
+ return val
110
+ if val is None:
111
+ return False
112
+ return to_boolean(val)
102
113
 
103
114
 
104
115
  def get_int_attr(
105
- shared_ctx: AnySharedContext,
116
+ ctx: AnyContext | AnySharedContext,
106
117
  attr: IntAttr | None,
107
118
  default: IntAttr = 0,
108
119
  auto_render: bool = True,
@@ -111,7 +122,7 @@ def get_int_attr(
111
122
  Retrieve an integer from shared context attributes.
112
123
 
113
124
  Args:
114
- shared_ctx (AnySharedContext): The shared context object.
125
+ ctx (AnyContext): The shared context object.
115
126
  attr (IntAttr | None): The integer attribute to retrieve.
116
127
  default (IntAttr): The default value if the attribute is None.
117
128
  auto_render (bool): Whether to auto-render the attribute value if it's a string.
@@ -119,14 +130,16 @@ def get_int_attr(
119
130
  Returns:
120
131
  int: The integer attribute value.
121
132
  """
122
- val = get_attr(shared_ctx, attr, default, auto_render)
123
- if isinstance(val, str):
124
- return int(val)
125
- return val
133
+ val = get_attr(ctx, attr, default, auto_render)
134
+ if isinstance(val, int):
135
+ return val
136
+ if val is None:
137
+ return 0
138
+ return int(val)
126
139
 
127
140
 
128
141
  def get_float_attr(
129
- shared_ctx: AnySharedContext,
142
+ ctx: AnyContext | AnySharedContext,
130
143
  attr: FloatAttr | None,
131
144
  default: FloatAttr = 0.0,
132
145
  auto_render: bool = True,
@@ -135,7 +148,7 @@ def get_float_attr(
135
148
  Retrieve a float from shared context attributes.
136
149
 
137
150
  Args:
138
- shared_ctx (AnySharedContext): The shared context object.
151
+ ctx (AnyContext): The shared context object.
139
152
  attr (FloatAttr | None): The float attribute to retrieve.
140
153
  default (FloatAttr): The default value if the attribute is None.
141
154
  auto_render (bool): Whether to auto-render the attribute value if it's a string.
@@ -143,14 +156,16 @@ def get_float_attr(
143
156
  Returns:
144
157
  float | None: The float attribute value.
145
158
  """
146
- val = get_attr(shared_ctx, attr, default, auto_render)
147
- if isinstance(val, str):
148
- return float(val)
149
- return val
159
+ val = get_attr(ctx, attr, default, auto_render)
160
+ if isinstance(val, (int, float)):
161
+ return val
162
+ if val is None:
163
+ return 0.0
164
+ return float(val)
150
165
 
151
166
 
152
167
  def get_attr(
153
- shared_ctx: AnySharedContext,
168
+ ctx: AnyContext | AnySharedContext,
154
169
  attr: AnyAttr,
155
170
  default: AnyAttr,
156
171
  auto_render: bool = True,
@@ -159,7 +174,7 @@ def get_attr(
159
174
  Retrieve an attribute value from shared context, handling callables and rendering.
160
175
 
161
176
  Args:
162
- shared_ctx (AnySharedContext): The shared context object.
177
+ ctx (AnyContext): The shared context object.
163
178
  attr (AnyAttr): The attribute to retrieve. Can be a value, a callable,
164
179
  or a string to render.
165
180
  default (AnyAttr): The default value if the attribute is None.
@@ -170,10 +185,10 @@ def get_attr(
170
185
  """
171
186
  if attr is None:
172
187
  if callable(default):
173
- return default(shared_ctx)
188
+ return default(ctx)
174
189
  return default
175
190
  if callable(attr):
176
- return attr(shared_ctx)
191
+ return attr(ctx)
177
192
  if isinstance(attr, str) and auto_render:
178
- return shared_ctx.render(attr)
193
+ return ctx.render(attr)
179
194
  return attr
@@ -0,0 +1,32 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from rich.theme import Theme
5
+
6
+
7
+ def render_markdown(
8
+ markdown_text: str, width: int | None = None, theme: "Theme | None" = None
9
+ ) -> str:
10
+ """
11
+ Renders Markdown to a string, ensuring link URLs are visible.
12
+ """
13
+ from rich.console import Console
14
+ from rich.markdown import Markdown
15
+ from rich.theme import Theme
16
+
17
+ if theme is None:
18
+ theme = Theme(
19
+ {
20
+ "markdown.link": "bold bright_cyan underline",
21
+ "markdown.link_url": "italic bright_cyan underline",
22
+ # Optional: You can customize headers or code blocks here too
23
+ "markdown.h1": "bold magenta",
24
+ "markdown.code": "bold white on #333333",
25
+ }
26
+ )
27
+
28
+ console = Console(width=width, theme=theme, force_terminal=True)
29
+ markdown = Markdown(markdown_text, hyperlinks=False)
30
+ with console.capture() as capture:
31
+ console.print(markdown)
32
+ return capture.get()
zrb/util/cli/text.py ADDED
@@ -0,0 +1,30 @@
1
+ import os
2
+ import subprocess
3
+ import tempfile
4
+
5
+ from zrb.util.file import read_file
6
+
7
+
8
+ def edit_text(
9
+ prompt_message: str,
10
+ value: str,
11
+ editor: str = "vi",
12
+ extension: str = ".txt",
13
+ ) -> str:
14
+ with tempfile.NamedTemporaryFile(delete=False, suffix=extension) as temp_file:
15
+ temp_file_name = temp_file.name
16
+ if prompt_message.strip() != "":
17
+ prompt_message_eol = f"{prompt_message}\n"
18
+ temp_file.write(prompt_message_eol.encode())
19
+ # Pre-fill with default content
20
+ if value:
21
+ temp_file.write(value.encode())
22
+ temp_file.flush()
23
+ subprocess.call([editor, temp_file_name])
24
+ # Read the edited content
25
+ edited_content = read_file(temp_file_name)
26
+ if prompt_message.strip() != "":
27
+ parts = [text.strip() for text in edited_content.split(prompt_message, 1)]
28
+ edited_content = "\n".join(parts).lstrip()
29
+ os.remove(temp_file_name)
30
+ return edited_content
zrb/util/cmd/command.py CHANGED
@@ -5,7 +5,7 @@ import signal
5
5
  import sys
6
6
  from collections import deque
7
7
  from collections.abc import Callable
8
- from typing import TextIO
8
+ from typing import Any, TextIO
9
9
 
10
10
  import psutil
11
11
 
@@ -62,6 +62,8 @@ async def run_command(
62
62
  register_pid_method: Callable[[int], None] | None = None,
63
63
  max_output_line: int = 1000,
64
64
  max_error_line: int = 1000,
65
+ max_display_line: int | None = None,
66
+ timeout: int = 3600,
65
67
  is_interactive: bool = False,
66
68
  ) -> tuple[CmdResult, int]:
67
69
  """
@@ -77,6 +79,8 @@ async def run_command(
77
79
  actual_print_method = print_method if print_method is not None else print
78
80
  if cwd is None:
79
81
  cwd = os.getcwd()
82
+ if max_display_line is None:
83
+ max_display_line = max(max_output_line, max_error_line)
80
84
  # While environment variables alone weren't the fix, they are still
81
85
  # good practice for encouraging simpler output from tools.
82
86
  child_env = (env_map or os.environ).copy()
@@ -95,17 +99,33 @@ async def run_command(
95
99
  if register_pid_method is not None:
96
100
  register_pid_method(cmd_process.pid)
97
101
  # Use the new, simple, and correct stream reader.
102
+ display_lines = deque(maxlen=max_display_line if max_display_line > 0 else 0)
98
103
  stdout_task = asyncio.create_task(
99
- __read_stream(cmd_process.stdout, actual_print_method, max_output_line)
104
+ __read_stream(
105
+ cmd_process.stdout, actual_print_method, max_output_line, display_lines
106
+ )
100
107
  )
101
108
  stderr_task = asyncio.create_task(
102
- __read_stream(cmd_process.stderr, actual_print_method, max_error_line)
109
+ __read_stream(
110
+ cmd_process.stderr, actual_print_method, max_error_line, display_lines
111
+ )
112
+ )
113
+ timeout_task = (
114
+ asyncio.create_task(asyncio.sleep(timeout)) if timeout and timeout > 0 else None
103
115
  )
104
116
  try:
105
- return_code = await cmd_process.wait()
117
+ wait_task = asyncio.create_task(cmd_process.wait())
118
+ done, pending = await asyncio.wait(
119
+ {wait_task, timeout_task} if timeout_task else {wait_task},
120
+ return_when=asyncio.FIRST_COMPLETED,
121
+ )
122
+ if timeout_task and timeout_task in done:
123
+ raise asyncio.TimeoutError()
124
+ return_code = wait_task.result()
106
125
  stdout, stderr = await asyncio.gather(stdout_task, stderr_task)
107
- return CmdResult(stdout, stderr), return_code
108
- except (KeyboardInterrupt, asyncio.CancelledError):
126
+ display = "\r\n".join(display_lines)
127
+ return CmdResult(stdout, stderr, display=display), return_code
128
+ except (KeyboardInterrupt, asyncio.CancelledError, asyncio.TimeoutError):
109
129
  try:
110
130
  os.killpg(cmd_process.pid, signal.SIGINT)
111
131
  await asyncio.wait_for(cmd_process.wait(), timeout=2.0)
@@ -133,13 +153,14 @@ def __get_cmd_stdin(is_interactive: bool) -> int | TextIO:
133
153
  async def __read_stream(
134
154
  stream: asyncio.StreamReader,
135
155
  print_method: Callable[..., None],
136
- max_lines: int,
156
+ max_line: int,
157
+ display_queue: deque[Any],
137
158
  ) -> str:
138
159
  """
139
160
  Reads from the stream using the robust `readline()` and correctly
140
161
  interprets carriage returns (`\r`) as distinct print events.
141
162
  """
142
- captured_lines = deque(maxlen=max_lines if max_lines > 0 else 0)
163
+ captured_lines = deque(maxlen=max_line if max_line > 0 else 0)
143
164
  while True:
144
165
  try:
145
166
  line_bytes = await stream.readline()
@@ -149,8 +170,9 @@ async def __read_stream(
149
170
  # Safety valve for the memory limit.
150
171
  error_msg = "[ERROR] A single line of output was too long to process."
151
172
  print_method(error_msg)
152
- if max_lines > 0:
173
+ if max_line > 0:
153
174
  captured_lines.append(error_msg)
175
+ display_queue.append(error_msg)
154
176
  break
155
177
  except (KeyboardInterrupt, asyncio.CancelledError):
156
178
  raise
@@ -165,8 +187,9 @@ async def __read_stream(
165
187
  print_method(clean_part, end="\r\n")
166
188
  except Exception:
167
189
  print_method(clean_part)
168
- if max_lines > 0:
190
+ if max_line > 0:
169
191
  captured_lines.append(clean_part)
192
+ display_queue.append(clean_part)
170
193
  return "\r\n".join(captured_lines)
171
194
 
172
195
 
zrb/util/file.py CHANGED
@@ -1,5 +1,7 @@
1
+ import fnmatch
1
2
  import os
2
3
  import re
4
+ from typing import Literal
3
5
 
4
6
 
5
7
  def read_file(file_path: str, replace_map: dict[str, str] = {}) -> str:
@@ -48,46 +50,22 @@ def _read_pdf_file_content(file_path: str) -> str:
48
50
  )
49
51
 
50
52
 
51
- def read_file_with_line_numbers(
52
- file_path: str, replace_map: dict[str, str] = {}
53
- ) -> str:
54
- """Reads a file and returns content with line numbers.
55
-
56
- Args:
57
- file_path: The path to the file.
58
- replace_map: A dictionary of strings to replace.
59
-
60
- Returns:
61
- The content of the file with line numbers and replacements applied.
62
- """
63
- content = read_file(file_path, replace_map)
64
- lines = content.splitlines()
65
- numbered_lines = [f"{i + 1} | {line}" for i, line in enumerate(lines)]
66
- return "\n".join(numbered_lines)
67
-
68
-
69
- def read_dir(dir_path: str) -> list[str]:
70
- """Reads a directory and returns a list of file names.
71
-
72
- Args:
73
- dir_path: The path to the directory.
74
-
75
- Returns:
76
- A list of file names in the directory.
77
- """
78
- return [f for f in os.listdir(os.path.abspath(os.path.expanduser(dir_path)))]
79
-
80
-
81
- def write_file(file_path: str, content: str | list[str]):
53
+ def write_file(
54
+ file_path: str,
55
+ content: str | list[str],
56
+ mode: Literal["w", "wt", "tw", "a", "at", "ta", "x", "xt", "tx"] = "w",
57
+ ):
82
58
  """Writes content to a file.
83
59
 
84
60
  Args:
85
61
  file_path: The path to the file.
86
62
  content: The content to write, either a string or a list of strings.
63
+ mode: Writing mode (by default "w")
87
64
  """
88
65
  if isinstance(content, list):
89
66
  content = "\n".join([line for line in content if line is not None])
90
- dir_path = os.path.dirname(file_path)
67
+ abs_file_path = os.path.abspath(os.path.expanduser(file_path))
68
+ dir_path = os.path.dirname(abs_file_path)
91
69
  os.makedirs(dir_path, exist_ok=True)
92
70
  should_add_eol = content.endswith("\n")
93
71
  # Remove trailing newlines, but keep one if the file originally ended up with newline
@@ -95,5 +73,55 @@ def write_file(file_path: str, content: str | list[str]):
95
73
  content = content.rstrip("\n")
96
74
  if should_add_eol:
97
75
  content += "\n"
98
- with open(os.path.abspath(os.path.expanduser(file_path)), "w") as f:
76
+ with open(abs_file_path, mode) as f:
99
77
  f.write(content)
78
+
79
+
80
+ def list_files(
81
+ path: str = ".",
82
+ include_hidden: bool = False,
83
+ depth: int = 3,
84
+ excluded_patterns: list[str] = [],
85
+ ) -> list[str]:
86
+ all_files: list[str] = []
87
+ abs_path = os.path.abspath(os.path.expanduser(path))
88
+ if not os.path.exists(abs_path):
89
+ raise FileNotFoundError(f"Path does not exist: {path}")
90
+
91
+ patterns_to_exclude = excluded_patterns
92
+ if depth <= 0:
93
+ depth = 1
94
+
95
+ initial_depth = abs_path.rstrip(os.sep).count(os.sep)
96
+ for root, dirs, files in os.walk(abs_path, topdown=True):
97
+ current_depth = root.rstrip(os.sep).count(os.sep) - initial_depth
98
+ if current_depth >= depth - 1:
99
+ del dirs[:]
100
+
101
+ dirs[:] = [
102
+ d
103
+ for d in dirs
104
+ if (include_hidden or not d.startswith("."))
105
+ and not _is_excluded(d, patterns_to_exclude)
106
+ ]
107
+
108
+ for filename in files:
109
+ if (include_hidden or not filename.startswith(".")) and not _is_excluded(
110
+ filename, patterns_to_exclude
111
+ ):
112
+ full_path = os.path.join(root, filename)
113
+ rel_full_path = os.path.relpath(full_path, abs_path)
114
+ if not _is_excluded(rel_full_path, patterns_to_exclude):
115
+ all_files.append(rel_full_path)
116
+ return sorted(all_files)
117
+
118
+
119
+ def _is_excluded(name: str, patterns: list[str]) -> bool:
120
+ for pattern in patterns:
121
+ if fnmatch.fnmatch(name, pattern):
122
+ return True
123
+ parts = name.split(os.path.sep)
124
+ for part in parts:
125
+ if fnmatch.fnmatch(part, pattern):
126
+ return True
127
+ return False
zrb/util/git.py CHANGED
@@ -131,7 +131,7 @@ async def get_branches(
131
131
  Exception: If the git command returns a non-zero exit code.
132
132
  """
133
133
  cmd_result, exit_code = await run_command(
134
- cmd=["git", "rev-parse", "--abbrev-ref", "HEAD"],
134
+ cmd=["git", "branch"],
135
135
  cwd=repo_dir,
136
136
  print_method=print_method,
137
137
  )
@@ -160,7 +160,7 @@ async def delete_branch(
160
160
  Exception: If the git command returns a non-zero exit code.
161
161
  """
162
162
  cmd_result, exit_code = await run_command(
163
- cmd=["git", "branch", "-D", branch_name],
163
+ cmd=["git", "branch", "-d", branch_name],
164
164
  cwd=repo_dir,
165
165
  print_method=print_method,
166
166
  )
@@ -8,7 +8,6 @@ def _adjust_markdown_headers(md: str, level_change: int) -> str:
8
8
  for line in lines:
9
9
  stripped_line = line.strip()
10
10
  fence_match = re.match(r"^([`~]{3,})", stripped_line)
11
-
12
11
  if fence_match:
13
12
  current_fence = fence_match.group(1)
14
13
  if (
@@ -31,7 +30,7 @@ def _adjust_markdown_headers(md: str, level_change: int) -> str:
31
30
  new_lines.append(new_header)
32
31
  else:
33
32
  new_lines.append(line)
34
- return "\n".join(new_lines)
33
+ return "\n".join(new_lines).rstrip()
35
34
 
36
35
 
37
36
  def demote_markdown_headers(md: str) -> str:
@@ -42,7 +41,7 @@ def promote_markdown_headers(md: str) -> str:
42
41
  return _adjust_markdown_headers(md, level_change=-1)
43
42
 
44
43
 
45
- def make_prompt_section(header: str, content: str, as_code: bool = False) -> str:
44
+ def make_markdown_section(header: str, content: str, as_code: bool = False) -> str:
46
45
  if content.strip() == "":
47
46
  return ""
48
47
  if as_code:
zrb/util/match.py ADDED
@@ -0,0 +1,78 @@
1
+ import os
2
+ import re
3
+
4
+
5
+ def fuzzy_match(text: str, pattern: str) -> tuple[bool, float]:
6
+ """
7
+ Match text against a pattern using a fuzzy search algorithm similar to VSCode's Ctrl+P.
8
+
9
+ The pattern is split into tokens by whitespace and path separators.
10
+ Each token must be found in the text (in order).
11
+
12
+ Args:
13
+ text: The string to search in.
14
+ pattern: The search pattern (e.g., "src main" or "util/io").
15
+
16
+ Returns:
17
+ A tuple (matched, score).
18
+ - matched: True if the pattern matches the text.
19
+ - score: A float representing the match quality (lower is better).
20
+ """
21
+ text_cmp = text.lower()
22
+ # Normalize pattern -> tokens split on path separators or whitespace
23
+ search_pattern = pattern.strip()
24
+ tokens = (
25
+ [t for t in re.split(rf"[{re.escape(os.path.sep)}\s]+", search_pattern) if t]
26
+ if search_pattern
27
+ else []
28
+ )
29
+ tokens = [t.lower() for t in tokens]
30
+ if not tokens:
31
+ return True, 0.0
32
+ last_pos = 0
33
+ score = 0.0
34
+ for token in tokens:
35
+ # try contiguous substring search first
36
+ idx = text_cmp.find(token, last_pos)
37
+ if idx != -1:
38
+ # good match: reward contiguous early matches
39
+ score += idx # smaller idx preferred
40
+ last_pos = idx + len(token)
41
+ else:
42
+ # fallback to subsequence matching
43
+ res = _find_subsequence_range(text_cmp, token, last_pos)
44
+ if res is None:
45
+ return False, 0.0
46
+
47
+ pos, end_pos = res
48
+
49
+ # subsequence match is less preferred than contiguous substring
50
+ score += pos + 0.5 * len(token)
51
+ last_pos = end_pos
52
+ # prefer shorter texts when score ties, so include length as tiebreaker
53
+ score += 0.01 * len(text)
54
+ return True, score
55
+
56
+
57
+ def _find_subsequence_range(
58
+ hay: str, needle: str, start: int = 0
59
+ ) -> tuple[int, int] | None:
60
+ """
61
+ Try to locate needle in hay as a subsequence starting at `start`.
62
+ Returns (start_index, end_index) where end_index is the index AFTER the last matched character.
63
+ """
64
+ if not needle:
65
+ return start, start
66
+ i = start
67
+ j = 0
68
+ first_pos = None
69
+ while i < len(hay) and j < len(needle):
70
+ if hay[i] == needle[j]:
71
+ if first_pos is None:
72
+ first_pos = i
73
+ j += 1
74
+ i += 1
75
+
76
+ if j == len(needle):
77
+ return first_pos, i
78
+ return None
zrb/util/run.py CHANGED
@@ -5,7 +5,7 @@ from typing import Any
5
5
 
6
6
  async def run_async(value: Any) -> Any:
7
7
  """
8
- Run a value asynchronously, awaiting if it's awaitable or running in a thread if not.
8
+ Run a value asynchronously, awaiting if it's awaitable or returning it directly.
9
9
 
10
10
  Args:
11
11
  value (Any): The value to run. Can be awaitable or not.
@@ -14,7 +14,7 @@ async def run_async(value: Any) -> Any:
14
14
  Any: The result of the awaited value or the value itself if not awaitable.
15
15
  """
16
16
  if isinstance(value, asyncio.Task):
17
- return value
17
+ return await value
18
18
  if inspect.isawaitable(value):
19
19
  return await value
20
- return await asyncio.to_thread(lambda: value)
20
+ return value
@@ -1,7 +1,7 @@
1
1
  import re
2
2
 
3
3
  NON_ALPHA_NUM = re.compile(r"[^a-zA-Z0-9]+")
4
- TRUE_STRS = ["true", "1", "yes", "y", "active", "on"]
4
+ TRUE_STRS = ["true", "1", "yes", "y", "active", "on", "okay", "ok"]
5
5
  FALSE_STRS = ["false", "0", "no", "n", "inactive", "off"]
6
6
 
7
7
 
zrb/util/truncate.py ADDED
@@ -0,0 +1,23 @@
1
+ from collections.abc import Mapping, Sequence
2
+ from typing import Any
3
+
4
+
5
+ def truncate_str(value: Any, limit: int):
6
+ # If value is a string, truncate
7
+ if isinstance(value, str):
8
+ if len(value) > limit:
9
+ if limit < 4:
10
+ return value[:limit]
11
+ return value[: limit - 4] + " ..."
12
+ # If value is a dict, process recursively
13
+ elif isinstance(value, Mapping):
14
+ return {k: truncate_str(v, limit) for k, v in value.items()}
15
+ # If value is a list or tuple, process recursively preserving type
16
+ elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
17
+ t = type(value)
18
+ return t(truncate_str(v, limit) for v in value)
19
+ # If value is a set, process recursively preserving type
20
+ elif isinstance(value, set):
21
+ return {truncate_str(v, limit) for v in value}
22
+ # Other types are returned unchanged
23
+ return value