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.
- zrb/__init__.py +118 -133
- zrb/attr/type.py +10 -7
- zrb/builtin/__init__.py +55 -1
- zrb/builtin/git.py +12 -1
- zrb/builtin/group.py +31 -15
- zrb/builtin/llm/chat.py +147 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
- zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
- zrb/builtin/searxng/config/settings.yml +5671 -0
- zrb/builtin/searxng/start.py +21 -0
- zrb/builtin/shell/autocomplete/bash.py +4 -3
- zrb/builtin/shell/autocomplete/zsh.py +4 -3
- zrb/callback/callback.py +8 -1
- zrb/cmd/cmd_result.py +2 -1
- zrb/config/config.py +555 -169
- zrb/config/helper.py +84 -0
- zrb/config/web_auth_config.py +50 -35
- zrb/context/any_shared_context.py +20 -3
- zrb/context/context.py +39 -5
- zrb/context/print_fn.py +13 -0
- zrb/context/shared_context.py +17 -8
- zrb/group/any_group.py +3 -3
- zrb/group/group.py +3 -3
- zrb/input/any_input.py +5 -1
- zrb/input/base_input.py +18 -6
- zrb/input/option_input.py +41 -1
- zrb/input/text_input.py +7 -24
- zrb/llm/agent/__init__.py +9 -0
- zrb/llm/agent/agent.py +215 -0
- zrb/llm/agent/summarizer.py +20 -0
- zrb/llm/app/__init__.py +10 -0
- zrb/llm/app/completion.py +281 -0
- zrb/llm/app/confirmation/allow_tool.py +66 -0
- zrb/llm/app/confirmation/handler.py +178 -0
- zrb/llm/app/confirmation/replace_confirmation.py +77 -0
- zrb/llm/app/keybinding.py +34 -0
- zrb/llm/app/layout.py +117 -0
- zrb/llm/app/lexer.py +155 -0
- zrb/llm/app/redirection.py +28 -0
- zrb/llm/app/style.py +16 -0
- zrb/llm/app/ui.py +733 -0
- zrb/llm/config/__init__.py +4 -0
- zrb/llm/config/config.py +122 -0
- zrb/llm/config/limiter.py +247 -0
- zrb/llm/history_manager/__init__.py +4 -0
- zrb/llm/history_manager/any_history_manager.py +23 -0
- zrb/llm/history_manager/file_history_manager.py +91 -0
- zrb/llm/history_processor/summarizer.py +108 -0
- zrb/llm/note/__init__.py +3 -0
- zrb/llm/note/manager.py +122 -0
- zrb/llm/prompt/__init__.py +29 -0
- zrb/llm/prompt/claude_compatibility.py +92 -0
- zrb/llm/prompt/compose.py +55 -0
- zrb/llm/prompt/default.py +51 -0
- zrb/llm/prompt/markdown/file_extractor.md +112 -0
- zrb/llm/prompt/markdown/mandate.md +23 -0
- zrb/llm/prompt/markdown/persona.md +3 -0
- zrb/llm/prompt/markdown/repo_extractor.md +112 -0
- zrb/llm/prompt/markdown/repo_summarizer.md +29 -0
- zrb/llm/prompt/markdown/summarizer.md +21 -0
- zrb/llm/prompt/note.py +41 -0
- zrb/llm/prompt/system_context.py +46 -0
- zrb/llm/prompt/zrb.py +41 -0
- zrb/llm/skill/__init__.py +3 -0
- zrb/llm/skill/manager.py +86 -0
- zrb/llm/task/__init__.py +4 -0
- zrb/llm/task/llm_chat_task.py +316 -0
- zrb/llm/task/llm_task.py +245 -0
- zrb/llm/tool/__init__.py +39 -0
- zrb/llm/tool/bash.py +75 -0
- zrb/llm/tool/code.py +266 -0
- zrb/llm/tool/file.py +419 -0
- zrb/llm/tool/note.py +70 -0
- zrb/{builtin/llm → llm}/tool/rag.py +33 -37
- zrb/llm/tool/search/brave.py +53 -0
- zrb/llm/tool/search/searxng.py +47 -0
- zrb/llm/tool/search/serpapi.py +47 -0
- zrb/llm/tool/skill.py +19 -0
- zrb/llm/tool/sub_agent.py +70 -0
- zrb/llm/tool/web.py +97 -0
- zrb/llm/tool/zrb_task.py +66 -0
- zrb/llm/util/attachment.py +101 -0
- zrb/llm/util/prompt.py +104 -0
- zrb/llm/util/stream_response.py +178 -0
- zrb/runner/cli.py +21 -20
- zrb/runner/common_util.py +24 -19
- zrb/runner/web_route/task_input_api_route.py +5 -5
- zrb/runner/web_util/user.py +7 -3
- zrb/session/any_session.py +12 -9
- zrb/session/session.py +38 -17
- zrb/task/any_task.py +24 -3
- zrb/task/base/context.py +42 -22
- zrb/task/base/execution.py +67 -55
- zrb/task/base/lifecycle.py +14 -7
- zrb/task/base/monitoring.py +12 -7
- zrb/task/base_task.py +113 -50
- zrb/task/base_trigger.py +16 -6
- zrb/task/cmd_task.py +6 -0
- zrb/task/http_check.py +11 -5
- zrb/task/make_task.py +5 -3
- zrb/task/rsync_task.py +30 -10
- zrb/task/scaffolder.py +7 -4
- zrb/task/scheduler.py +7 -4
- zrb/task/tcp_check.py +6 -4
- zrb/util/ascii_art/art/bee.txt +17 -0
- zrb/util/ascii_art/art/cat.txt +9 -0
- zrb/util/ascii_art/art/ghost.txt +16 -0
- zrb/util/ascii_art/art/panda.txt +17 -0
- zrb/util/ascii_art/art/rose.txt +14 -0
- zrb/util/ascii_art/art/unicorn.txt +15 -0
- zrb/util/ascii_art/banner.py +92 -0
- zrb/util/attr.py +54 -39
- zrb/util/cli/markdown.py +32 -0
- zrb/util/cli/text.py +30 -0
- zrb/util/cmd/command.py +33 -10
- zrb/util/file.py +61 -33
- zrb/util/git.py +2 -2
- zrb/util/{llm/prompt.py → markdown.py} +2 -3
- zrb/util/match.py +78 -0
- zrb/util/run.py +3 -3
- zrb/util/string/conversion.py +1 -1
- zrb/util/truncate.py +23 -0
- zrb/util/yaml.py +204 -0
- zrb/xcom/xcom.py +10 -0
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/METADATA +41 -27
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/RECORD +129 -131
- {zrb-1.15.3.dist-info → zrb-2.0.0a4.dist-info}/WHEEL +1 -1
- zrb/attr/__init__.py +0 -0
- zrb/builtin/llm/chat_session.py +0 -311
- zrb/builtin/llm/history.py +0 -71
- zrb/builtin/llm/input.py +0 -27
- zrb/builtin/llm/llm_ask.py +0 -187
- zrb/builtin/llm/previous-session.js +0 -21
- zrb/builtin/llm/tool/__init__.py +0 -0
- zrb/builtin/llm/tool/api.py +0 -71
- zrb/builtin/llm/tool/cli.py +0 -38
- zrb/builtin/llm/tool/code.py +0 -254
- zrb/builtin/llm/tool/file.py +0 -626
- zrb/builtin/llm/tool/sub_agent.py +0 -137
- zrb/builtin/llm/tool/web.py +0 -195
- zrb/builtin/project/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/service/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/__init__.py +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/__init__.py +0 -0
- zrb/builtin/project/create/__init__.py +0 -0
- zrb/builtin/shell/__init__.py +0 -0
- zrb/builtin/shell/autocomplete/__init__.py +0 -0
- zrb/callback/__init__.py +0 -0
- zrb/cmd/__init__.py +0 -0
- zrb/config/default_prompt/file_extractor_system_prompt.md +0 -12
- zrb/config/default_prompt/interactive_system_prompt.md +0 -35
- zrb/config/default_prompt/persona.md +0 -1
- zrb/config/default_prompt/repo_extractor_system_prompt.md +0 -112
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +0 -10
- zrb/config/default_prompt/summarization_prompt.md +0 -16
- zrb/config/default_prompt/system_prompt.md +0 -32
- zrb/config/llm_config.py +0 -243
- zrb/config/llm_context/config.py +0 -129
- zrb/config/llm_context/config_parser.py +0 -46
- zrb/config/llm_rate_limitter.py +0 -137
- zrb/content_transformer/__init__.py +0 -0
- zrb/context/__init__.py +0 -0
- zrb/dot_dict/__init__.py +0 -0
- zrb/env/__init__.py +0 -0
- zrb/group/__init__.py +0 -0
- zrb/input/__init__.py +0 -0
- zrb/runner/__init__.py +0 -0
- zrb/runner/web_route/__init__.py +0 -0
- zrb/runner/web_route/home_page/__init__.py +0 -0
- zrb/session/__init__.py +0 -0
- zrb/session_state_log/__init__.py +0 -0
- zrb/session_state_logger/__init__.py +0 -0
- zrb/task/__init__.py +0 -0
- zrb/task/base/__init__.py +0 -0
- zrb/task/llm/__init__.py +0 -0
- zrb/task/llm/agent.py +0 -243
- zrb/task/llm/config.py +0 -103
- zrb/task/llm/conversation_history.py +0 -128
- zrb/task/llm/conversation_history_model.py +0 -242
- zrb/task/llm/default_workflow/coding.md +0 -24
- zrb/task/llm/default_workflow/copywriting.md +0 -17
- zrb/task/llm/default_workflow/researching.md +0 -18
- zrb/task/llm/error.py +0 -95
- zrb/task/llm/history_summarization.py +0 -216
- zrb/task/llm/print_node.py +0 -101
- zrb/task/llm/prompt.py +0 -325
- zrb/task/llm/tool_wrapper.py +0 -220
- zrb/task/llm/typing.py +0 -3
- zrb/task/llm_task.py +0 -341
- zrb/task_status/__init__.py +0 -0
- zrb/util/__init__.py +0 -0
- zrb/util/cli/__init__.py +0 -0
- zrb/util/cmd/__init__.py +0 -0
- zrb/util/codemod/__init__.py +0 -0
- zrb/util/string/__init__.py +0 -0
- zrb/xcom/__init__.py +0 -0
- {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
|
-
|
|
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
|
-
|
|
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(
|
|
32
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
75
|
-
if
|
|
76
|
-
return
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
99
|
-
if isinstance(val,
|
|
100
|
-
return
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
123
|
-
if isinstance(val,
|
|
124
|
-
return
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
147
|
-
if isinstance(val,
|
|
148
|
-
return
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
188
|
+
return default(ctx)
|
|
174
189
|
return default
|
|
175
190
|
if callable(attr):
|
|
176
|
-
return attr(
|
|
191
|
+
return attr(ctx)
|
|
177
192
|
if isinstance(attr, str) and auto_render:
|
|
178
|
-
return
|
|
193
|
+
return ctx.render(attr)
|
|
179
194
|
return attr
|
zrb/util/cli/markdown.py
ADDED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
52
|
-
file_path: str,
|
|
53
|
-
|
|
54
|
-
"""
|
|
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
|
-
|
|
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(
|
|
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", "
|
|
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", "-
|
|
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
|
|
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
|
|
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
|
|
20
|
+
return value
|
zrb/util/string/conversion.py
CHANGED
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
|