zrb 1.15.3__py3-none-any.whl → 1.21.29__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 +2 -6
- zrb/attr/type.py +10 -7
- zrb/builtin/__init__.py +2 -0
- zrb/builtin/git.py +12 -1
- zrb/builtin/group.py +31 -15
- zrb/builtin/llm/attachment.py +40 -0
- zrb/builtin/llm/chat_completion.py +274 -0
- zrb/builtin/llm/chat_session.py +126 -167
- zrb/builtin/llm/chat_session_cmd.py +288 -0
- zrb/builtin/llm/chat_trigger.py +79 -0
- zrb/builtin/llm/history.py +4 -4
- zrb/builtin/llm/llm_ask.py +217 -135
- zrb/builtin/llm/tool/api.py +74 -70
- zrb/builtin/llm/tool/cli.py +35 -21
- zrb/builtin/llm/tool/code.py +55 -73
- zrb/builtin/llm/tool/file.py +278 -344
- zrb/builtin/llm/tool/note.py +84 -0
- zrb/builtin/llm/tool/rag.py +27 -34
- zrb/builtin/llm/tool/sub_agent.py +54 -41
- zrb/builtin/llm/tool/web.py +74 -98
- 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/config/config.py +202 -27
- zrb/config/default_prompt/file_extractor_system_prompt.md +109 -9
- zrb/config/default_prompt/interactive_system_prompt.md +24 -30
- zrb/config/default_prompt/persona.md +1 -1
- zrb/config/default_prompt/repo_extractor_system_prompt.md +31 -31
- zrb/config/default_prompt/repo_summarizer_system_prompt.md +27 -8
- zrb/config/default_prompt/summarization_prompt.md +57 -16
- zrb/config/default_prompt/system_prompt.md +36 -30
- zrb/config/llm_config.py +119 -23
- zrb/config/llm_context/config.py +127 -90
- zrb/config/llm_context/config_parser.py +1 -7
- zrb/config/llm_context/workflow.py +81 -0
- zrb/config/llm_rate_limitter.py +100 -47
- zrb/context/any_shared_context.py +7 -1
- zrb/context/context.py +8 -2
- zrb/context/shared_context.py +3 -7
- 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 +13 -1
- zrb/input/text_input.py +7 -24
- 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 -6
- zrb/session/session.py +39 -18
- zrb/task/any_task.py +24 -3
- zrb/task/base/context.py +17 -9
- zrb/task/base/execution.py +15 -8
- zrb/task/base/lifecycle.py +8 -4
- zrb/task/base/monitoring.py +12 -7
- zrb/task/base_task.py +69 -5
- zrb/task/base_trigger.py +12 -5
- zrb/task/llm/agent.py +128 -167
- zrb/task/llm/agent_runner.py +152 -0
- zrb/task/llm/config.py +39 -20
- zrb/task/llm/conversation_history.py +110 -29
- zrb/task/llm/conversation_history_model.py +4 -179
- zrb/task/llm/default_workflow/coding/workflow.md +41 -0
- zrb/task/llm/default_workflow/copywriting/workflow.md +68 -0
- zrb/task/llm/default_workflow/git/workflow.md +118 -0
- zrb/task/llm/default_workflow/golang/workflow.md +128 -0
- zrb/task/llm/default_workflow/html-css/workflow.md +135 -0
- zrb/task/llm/default_workflow/java/workflow.md +146 -0
- zrb/task/llm/default_workflow/javascript/workflow.md +158 -0
- zrb/task/llm/default_workflow/python/workflow.md +160 -0
- zrb/task/llm/default_workflow/researching/workflow.md +153 -0
- zrb/task/llm/default_workflow/rust/workflow.md +162 -0
- zrb/task/llm/default_workflow/shell/workflow.md +299 -0
- zrb/task/llm/file_replacement.py +206 -0
- zrb/task/llm/file_tool_model.py +57 -0
- zrb/task/llm/history_processor.py +206 -0
- zrb/task/llm/history_summarization.py +2 -193
- zrb/task/llm/print_node.py +184 -64
- zrb/task/llm/prompt.py +175 -179
- zrb/task/llm/subagent_conversation_history.py +41 -0
- zrb/task/llm/tool_wrapper.py +226 -85
- zrb/task/llm/workflow.py +76 -0
- zrb/task/llm_task.py +109 -71
- zrb/task/make_task.py +2 -3
- zrb/task/rsync_task.py +25 -10
- zrb/task/scheduler.py +4 -4
- zrb/util/attr.py +54 -39
- zrb/util/cli/markdown.py +12 -0
- zrb/util/cli/text.py +30 -0
- zrb/util/file.py +12 -3
- zrb/util/git.py +2 -2
- zrb/util/{llm/prompt.py → markdown.py} +2 -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-1.21.29.dist-info}/METADATA +38 -18
- {zrb-1.15.3.dist-info → zrb-1.21.29.dist-info}/RECORD +105 -79
- {zrb-1.15.3.dist-info → zrb-1.21.29.dist-info}/WHEEL +1 -1
- 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-1.15.3.dist-info → zrb-1.21.29.dist-info}/entry_points.txt +0 -0
zrb/__init__.py
CHANGED
|
@@ -61,6 +61,7 @@ _LAZY_LOAD = {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
if TYPE_CHECKING:
|
|
64
|
+
from zrb import builtin
|
|
64
65
|
from zrb.attr.type import (
|
|
65
66
|
AnyAttr,
|
|
66
67
|
BoolAttr,
|
|
@@ -126,9 +127,4 @@ def __getattr__(name: str) -> Any:
|
|
|
126
127
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
127
128
|
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
CFG = __getattr__("CFG")
|
|
131
|
-
if CFG.LOAD_BUILTIN:
|
|
132
|
-
from zrb import builtin
|
|
133
|
-
|
|
134
|
-
assert builtin
|
|
130
|
+
from zrb import builtin
|
zrb/attr/type.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from typing import Any, Callable
|
|
2
2
|
|
|
3
|
+
from zrb.context.any_context import AnyContext
|
|
3
4
|
from zrb.context.any_shared_context import AnySharedContext
|
|
4
5
|
|
|
5
6
|
fstring = str
|
|
6
|
-
AnyAttr = Any | fstring | Callable[[AnySharedContext], Any]
|
|
7
|
-
StrAttr = str | fstring | Callable[[AnySharedContext], str]
|
|
8
|
-
BoolAttr = bool | fstring | Callable[[AnySharedContext], bool]
|
|
9
|
-
IntAttr = int | fstring | Callable[[AnySharedContext], int]
|
|
10
|
-
FloatAttr = float | fstring | Callable[[AnySharedContext], float]
|
|
11
|
-
StrDictAttr =
|
|
12
|
-
|
|
7
|
+
AnyAttr = Any | fstring | Callable[[AnyContext | AnySharedContext], Any]
|
|
8
|
+
StrAttr = str | fstring | Callable[[AnyContext | AnySharedContext], str | None]
|
|
9
|
+
BoolAttr = bool | fstring | Callable[[AnyContext | AnySharedContext], bool | None]
|
|
10
|
+
IntAttr = int | fstring | Callable[[AnyContext | AnySharedContext], int | None]
|
|
11
|
+
FloatAttr = float | fstring | Callable[[AnyContext | AnySharedContext], float | None]
|
|
12
|
+
StrDictAttr = (
|
|
13
|
+
dict[str, StrAttr] | Callable[[AnyContext | AnySharedContext], dict[str, Any]]
|
|
14
|
+
)
|
|
15
|
+
StrListAttr = list[StrAttr] | Callable[[AnyContext | AnySharedContext], list[str]]
|
zrb/builtin/__init__.py
CHANGED
|
@@ -9,12 +9,14 @@ from zrb.builtin.git import (
|
|
|
9
9
|
from zrb.builtin.git_subtree import git_add_subtree, git_pull_subtree, git_push_subtree
|
|
10
10
|
from zrb.builtin.http import generate_curl, http_request
|
|
11
11
|
from zrb.builtin.jwt import decode_jwt, encode_jwt, validate_jwt
|
|
12
|
+
from zrb.builtin.llm.chat_trigger import llm_chat_trigger
|
|
12
13
|
from zrb.builtin.llm.llm_ask import llm_ask
|
|
13
14
|
from zrb.builtin.md5 import hash_md5, sum_md5, validate_md5
|
|
14
15
|
from zrb.builtin.project.add.fastapp.fastapp_task import add_fastapp_to_project
|
|
15
16
|
from zrb.builtin.project.create.project_task import create_project
|
|
16
17
|
from zrb.builtin.python import format_python_code
|
|
17
18
|
from zrb.builtin.random import shuffle_values, throw_dice
|
|
19
|
+
from zrb.builtin.searxng.start import start_searxng
|
|
18
20
|
from zrb.builtin.setup.asdf.asdf import setup_asdf
|
|
19
21
|
from zrb.builtin.setup.latex.ubuntu import setup_latex_on_ubuntu
|
|
20
22
|
from zrb.builtin.setup.tmux.tmux import setup_tmux
|
zrb/builtin/git.py
CHANGED
|
@@ -82,6 +82,12 @@ async def get_git_diff(ctx: AnyContext):
|
|
|
82
82
|
|
|
83
83
|
@make_task(
|
|
84
84
|
name="prune-local-git-branches",
|
|
85
|
+
input=StrInput(
|
|
86
|
+
name="preserved-branch",
|
|
87
|
+
description="Branches to be preserved",
|
|
88
|
+
prompt="Branches to be preserved, comma separated",
|
|
89
|
+
default="master,main,dev,develop",
|
|
90
|
+
),
|
|
85
91
|
description="🧹 Prune local branches",
|
|
86
92
|
group=git_branch_group,
|
|
87
93
|
alias="prune",
|
|
@@ -93,8 +99,13 @@ async def prune_local_branches(ctx: AnyContext):
|
|
|
93
99
|
branches = await get_branches(repo_dir, print_method=ctx.print)
|
|
94
100
|
ctx.print(stylize_faint("Get current branch"))
|
|
95
101
|
current_branch = await get_current_branch(repo_dir, print_method=ctx.print)
|
|
102
|
+
preserved_branches = [
|
|
103
|
+
branch.strip()
|
|
104
|
+
for branch in ctx.input.preserved_branch.split(",")
|
|
105
|
+
if branch.strip() != ""
|
|
106
|
+
]
|
|
96
107
|
for branch in branches:
|
|
97
|
-
if branch == current_branch or branch
|
|
108
|
+
if branch == current_branch or branch in preserved_branches:
|
|
98
109
|
continue
|
|
99
110
|
ctx.print(stylize_faint(f"Removing local branch: {branch}"))
|
|
100
111
|
try:
|
zrb/builtin/group.py
CHANGED
|
@@ -1,39 +1,51 @@
|
|
|
1
|
+
from zrb.config.config import CFG
|
|
1
2
|
from zrb.group.group import Group
|
|
2
3
|
from zrb.runner.cli import cli
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
def _maybe_add_group(group: Group):
|
|
7
|
+
if CFG.LOAD_BUILTIN:
|
|
8
|
+
cli.add_group(group)
|
|
9
|
+
return group
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
base64_group = _maybe_add_group(
|
|
13
|
+
Group(name="base64", description="📄 Base64 operations")
|
|
14
|
+
)
|
|
15
|
+
uuid_group = _maybe_add_group(Group(name="uuid", description="🆔 UUID operations"))
|
|
6
16
|
uuid_v1_group = uuid_group.add_group(Group(name="v1", description="UUID V1 operations"))
|
|
7
17
|
uuid_v3_group = uuid_group.add_group(Group(name="v3", description="UUID V3 operations"))
|
|
8
18
|
uuid_v4_group = uuid_group.add_group(Group(name="v4", description="UUID V4 operations"))
|
|
9
19
|
uuid_v5_group = uuid_group.add_group(Group(name="v5", description="UUID V5 operations"))
|
|
10
|
-
ulid_group =
|
|
11
|
-
jwt_group =
|
|
12
|
-
http_group =
|
|
20
|
+
ulid_group = _maybe_add_group(Group(name="ulid", description="🔢 ULID operations"))
|
|
21
|
+
jwt_group = _maybe_add_group(Group(name="jwt", description="🔒 JWT encode/decode"))
|
|
22
|
+
http_group = _maybe_add_group(
|
|
23
|
+
Group(name="http", description="🌐 HTTP request operations")
|
|
24
|
+
)
|
|
13
25
|
|
|
14
|
-
random_group =
|
|
15
|
-
git_group =
|
|
26
|
+
random_group = _maybe_add_group(Group(name="random", description="🔀 Random operation"))
|
|
27
|
+
git_group = _maybe_add_group(Group(name="git", description="🌱 Git related commands"))
|
|
16
28
|
git_branch_group = git_group.add_group(
|
|
17
29
|
Group(name="branch", description="🌿 Git branch related commands")
|
|
18
30
|
)
|
|
19
31
|
git_subtree_group = git_group.add_group(
|
|
20
32
|
Group(name="subtree", description="🌳 Git subtree related commands")
|
|
21
33
|
)
|
|
22
|
-
llm_group =
|
|
23
|
-
md5_group =
|
|
24
|
-
python_group =
|
|
34
|
+
llm_group = _maybe_add_group(Group(name="llm", description="🤖 LLM operations"))
|
|
35
|
+
md5_group = _maybe_add_group(Group(name="md5", description="🔢 Md5 operations"))
|
|
36
|
+
python_group = _maybe_add_group(
|
|
25
37
|
Group(name="python", description="🐍 Python related commands")
|
|
26
38
|
)
|
|
27
|
-
todo_group =
|
|
39
|
+
todo_group = _maybe_add_group(Group(name="todo", description="✅ Todo management"))
|
|
28
40
|
|
|
29
|
-
shell_group =
|
|
41
|
+
shell_group = _maybe_add_group(
|
|
30
42
|
Group(name="shell", description="💬 Shell related commands")
|
|
31
43
|
)
|
|
32
|
-
shell_autocomplete_group
|
|
44
|
+
shell_autocomplete_group = shell_group.add_group(
|
|
33
45
|
Group(name="autocomplete", description="⌨️ Shell autocomplete related commands")
|
|
34
46
|
)
|
|
35
47
|
|
|
36
|
-
project_group =
|
|
48
|
+
project_group = _maybe_add_group(
|
|
37
49
|
Group(name="project", description="📁 Project related commands")
|
|
38
50
|
)
|
|
39
51
|
add_to_project_group = project_group.add_group(
|
|
@@ -43,7 +55,11 @@ add_fastapp_to_project_group = add_to_project_group.add_group(
|
|
|
43
55
|
Group(name="fastapp", description="🚀 Add Fastapp resources")
|
|
44
56
|
)
|
|
45
57
|
|
|
46
|
-
setup_group =
|
|
58
|
+
setup_group = _maybe_add_group(Group(name="setup", description="🔧 Setup"))
|
|
47
59
|
setup_latex_group = setup_group.add_group(
|
|
48
60
|
Group(name="latex", description="✍️ Setup LaTeX")
|
|
49
61
|
)
|
|
62
|
+
|
|
63
|
+
searxng_group = _maybe_add_group(
|
|
64
|
+
Group(name="searxng", description="🔎 Searxng related command")
|
|
65
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
def get_media_type(filename: str) -> str | None:
|
|
2
|
+
"""Guess media type string based on file extension."""
|
|
3
|
+
ext = filename.lower().rsplit(".", 1)[-1] if "." in filename else ""
|
|
4
|
+
mapping: dict[str, str] = {
|
|
5
|
+
# Audio
|
|
6
|
+
"wav": "audio/wav",
|
|
7
|
+
"mp3": "audio/mpeg",
|
|
8
|
+
"ogg": "audio/ogg",
|
|
9
|
+
"flac": "audio/flac",
|
|
10
|
+
"aiff": "audio/aiff",
|
|
11
|
+
"aac": "audio/aac",
|
|
12
|
+
# Image
|
|
13
|
+
"jpg": "image/jpeg",
|
|
14
|
+
"jpeg": "image/jpeg",
|
|
15
|
+
"png": "image/png",
|
|
16
|
+
"gif": "image/gif",
|
|
17
|
+
"webp": "image/webp",
|
|
18
|
+
# Document
|
|
19
|
+
"pdf": "application/pdf",
|
|
20
|
+
"txt": "text/plain",
|
|
21
|
+
"csv": "text/csv",
|
|
22
|
+
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
23
|
+
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
24
|
+
"html": "text/html",
|
|
25
|
+
"htm": "text/html",
|
|
26
|
+
"md": "text/markdown",
|
|
27
|
+
"doc": "application/msword",
|
|
28
|
+
"xls": "application/vnd.ms-excel",
|
|
29
|
+
# Video
|
|
30
|
+
"mkv": "video/x-matroska",
|
|
31
|
+
"mov": "video/quicktime",
|
|
32
|
+
"mp4": "video/mp4",
|
|
33
|
+
"webm": "video/webm",
|
|
34
|
+
"flv": "video/x-flv",
|
|
35
|
+
"mpeg": "video/mpeg",
|
|
36
|
+
"mpg": "video/mpeg",
|
|
37
|
+
"wmv": "video/x-ms-wmv",
|
|
38
|
+
"3gp": "video/3gpp",
|
|
39
|
+
}
|
|
40
|
+
return mapping.get(ext)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
4
|
+
from prompt_toolkit.document import Document
|
|
5
|
+
|
|
6
|
+
from zrb.builtin.llm.chat_session_cmd import (
|
|
7
|
+
ADD_SUB_CMD,
|
|
8
|
+
ATTACHMENT_ADD_SUB_CMD_DESC,
|
|
9
|
+
ATTACHMENT_CLEAR_SUB_CMD_DESC,
|
|
10
|
+
ATTACHMENT_CMD,
|
|
11
|
+
ATTACHMENT_CMD_DESC,
|
|
12
|
+
ATTACHMENT_SET_SUB_CMD_DESC,
|
|
13
|
+
CLEAR_SUB_CMD,
|
|
14
|
+
HELP_CMD,
|
|
15
|
+
HELP_CMD_DESC,
|
|
16
|
+
MULTILINE_END_CMD,
|
|
17
|
+
MULTILINE_END_CMD_DESC,
|
|
18
|
+
MULTILINE_START_CMD,
|
|
19
|
+
MULTILINE_START_CMD_DESC,
|
|
20
|
+
QUIT_CMD,
|
|
21
|
+
QUIT_CMD_DESC,
|
|
22
|
+
RUN_CLI_CMD,
|
|
23
|
+
RUN_CLI_CMD_DESC,
|
|
24
|
+
SAVE_CMD,
|
|
25
|
+
SAVE_CMD_DESC,
|
|
26
|
+
SET_SUB_CMD,
|
|
27
|
+
WORKFLOW_ADD_SUB_CMD_DESC,
|
|
28
|
+
WORKFLOW_CLEAR_SUB_CMD_DESC,
|
|
29
|
+
WORKFLOW_CMD,
|
|
30
|
+
WORKFLOW_CMD_DESC,
|
|
31
|
+
WORKFLOW_SET_SUB_CMD_DESC,
|
|
32
|
+
YOLO_CMD,
|
|
33
|
+
YOLO_CMD_DESC,
|
|
34
|
+
YOLO_SET_CMD_DESC,
|
|
35
|
+
YOLO_SET_FALSE_CMD_DESC,
|
|
36
|
+
YOLO_SET_TRUE_CMD_DESC,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ChatCompleter(Completer):
|
|
41
|
+
|
|
42
|
+
def get_completions(self, document: Document, complete_event: CompleteEvent):
|
|
43
|
+
# Slash command
|
|
44
|
+
for completion in self._complete_slash_command(document):
|
|
45
|
+
yield completion
|
|
46
|
+
for completion in self._complete_slash_file_command(document):
|
|
47
|
+
yield completion
|
|
48
|
+
# Appendix
|
|
49
|
+
for completion in self._complete_appendix(document):
|
|
50
|
+
yield completion
|
|
51
|
+
|
|
52
|
+
def _complete_slash_file_command(self, document: Document):
|
|
53
|
+
text = document.text_before_cursor
|
|
54
|
+
prefixes = []
|
|
55
|
+
for cmd in ATTACHMENT_CMD:
|
|
56
|
+
for subcmd in ADD_SUB_CMD:
|
|
57
|
+
prefixes.append(f"{cmd} {subcmd} ")
|
|
58
|
+
for prefix in prefixes:
|
|
59
|
+
if text.startswith(prefix):
|
|
60
|
+
pattern = text[len(prefix) :]
|
|
61
|
+
potential_options = self._fuzzy_path_search(pattern, dirs=False)
|
|
62
|
+
for prefixed_option in [
|
|
63
|
+
f"{prefix}{option}" for option in potential_options
|
|
64
|
+
]:
|
|
65
|
+
yield Completion(
|
|
66
|
+
prefixed_option,
|
|
67
|
+
start_position=-len(text),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _complete_slash_command(self, document: Document):
|
|
71
|
+
text = document.text_before_cursor
|
|
72
|
+
if not text.startswith("/"):
|
|
73
|
+
return
|
|
74
|
+
for command, description in self._get_cmd_options().items():
|
|
75
|
+
if command.lower().startswith(text.lower()):
|
|
76
|
+
yield Completion(
|
|
77
|
+
command,
|
|
78
|
+
start_position=-len(text),
|
|
79
|
+
display_meta=description,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _complete_appendix(self, document: Document):
|
|
83
|
+
token = document.get_word_before_cursor(WORD=True)
|
|
84
|
+
prefix = "@"
|
|
85
|
+
if not token.startswith(prefix):
|
|
86
|
+
return
|
|
87
|
+
pattern = token[len(prefix) :]
|
|
88
|
+
potential_options = self._fuzzy_path_search(pattern, dirs=False)
|
|
89
|
+
for prefixed_option in [f"{prefix}{option}" for option in potential_options]:
|
|
90
|
+
yield Completion(
|
|
91
|
+
prefixed_option,
|
|
92
|
+
start_position=-len(token),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _get_cmd_options(self):
|
|
96
|
+
cmd_options = {}
|
|
97
|
+
# Add all commands with their descriptions
|
|
98
|
+
for cmd in MULTILINE_START_CMD:
|
|
99
|
+
cmd_options[cmd] = MULTILINE_START_CMD_DESC
|
|
100
|
+
for cmd in MULTILINE_END_CMD:
|
|
101
|
+
cmd_options[cmd] = MULTILINE_END_CMD_DESC
|
|
102
|
+
for cmd in QUIT_CMD:
|
|
103
|
+
cmd_options[cmd] = QUIT_CMD_DESC
|
|
104
|
+
for cmd in WORKFLOW_CMD:
|
|
105
|
+
cmd_options[cmd] = WORKFLOW_CMD_DESC
|
|
106
|
+
for subcmd in ADD_SUB_CMD:
|
|
107
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_ADD_SUB_CMD_DESC
|
|
108
|
+
for subcmd in CLEAR_SUB_CMD:
|
|
109
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_CLEAR_SUB_CMD_DESC
|
|
110
|
+
for subcmd in SET_SUB_CMD:
|
|
111
|
+
cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_SET_SUB_CMD_DESC
|
|
112
|
+
for cmd in SAVE_CMD:
|
|
113
|
+
cmd_options[cmd] = SAVE_CMD_DESC
|
|
114
|
+
for cmd in ATTACHMENT_CMD:
|
|
115
|
+
cmd_options[cmd] = ATTACHMENT_CMD_DESC
|
|
116
|
+
for subcmd in ADD_SUB_CMD:
|
|
117
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_ADD_SUB_CMD_DESC
|
|
118
|
+
for subcmd in CLEAR_SUB_CMD:
|
|
119
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_CLEAR_SUB_CMD_DESC
|
|
120
|
+
for subcmd in SET_SUB_CMD:
|
|
121
|
+
cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_SET_SUB_CMD_DESC
|
|
122
|
+
for cmd in YOLO_CMD:
|
|
123
|
+
cmd_options[cmd] = YOLO_CMD_DESC
|
|
124
|
+
for subcmd in SET_SUB_CMD:
|
|
125
|
+
cmd_options[f"{cmd} {subcmd} true"] = YOLO_SET_TRUE_CMD_DESC
|
|
126
|
+
cmd_options[f"{cmd} {subcmd} false"] = YOLO_SET_FALSE_CMD_DESC
|
|
127
|
+
cmd_options[f"{cmd} {subcmd}"] = YOLO_SET_CMD_DESC
|
|
128
|
+
for cmd in HELP_CMD:
|
|
129
|
+
cmd_options[cmd] = HELP_CMD_DESC
|
|
130
|
+
for cmd in RUN_CLI_CMD:
|
|
131
|
+
cmd_options[cmd] = RUN_CLI_CMD_DESC
|
|
132
|
+
return dict(sorted(cmd_options.items()))
|
|
133
|
+
|
|
134
|
+
def _fuzzy_path_search(
|
|
135
|
+
self,
|
|
136
|
+
pattern: str,
|
|
137
|
+
root: str | None = None,
|
|
138
|
+
max_results: int = 20,
|
|
139
|
+
include_hidden: bool = False,
|
|
140
|
+
case_sensitive: bool = False,
|
|
141
|
+
dirs: bool = True,
|
|
142
|
+
files: bool = True,
|
|
143
|
+
) -> list[str]:
|
|
144
|
+
"""
|
|
145
|
+
Return a list of filesystem paths under `root` that fuzzy-match `pattern`.
|
|
146
|
+
- pattern: e.g. "./some/x" or "proj util/io"
|
|
147
|
+
- include_hidden: if False skip files/dirs starting with '.'
|
|
148
|
+
- dirs/files booleans let you restrict results
|
|
149
|
+
- returns list of relative paths (from root), sorted best-first
|
|
150
|
+
"""
|
|
151
|
+
search_pattern = pattern
|
|
152
|
+
if root is None:
|
|
153
|
+
# Determine root and adjust pattern if necessary
|
|
154
|
+
expanded_pattern = os.path.expanduser(pattern)
|
|
155
|
+
if os.path.isabs(expanded_pattern) or pattern.startswith("~"):
|
|
156
|
+
# For absolute paths, find the deepest existing directory
|
|
157
|
+
if os.path.isdir(expanded_pattern):
|
|
158
|
+
root = expanded_pattern
|
|
159
|
+
search_pattern = ""
|
|
160
|
+
else:
|
|
161
|
+
root = os.path.dirname(expanded_pattern)
|
|
162
|
+
while root and not os.path.isdir(root) and len(root) > 1:
|
|
163
|
+
root = os.path.dirname(root)
|
|
164
|
+
if not os.path.isdir(root):
|
|
165
|
+
root = "." # Fallback
|
|
166
|
+
search_pattern = pattern
|
|
167
|
+
else:
|
|
168
|
+
try:
|
|
169
|
+
search_pattern = os.path.relpath(expanded_pattern, root)
|
|
170
|
+
if search_pattern == ".":
|
|
171
|
+
search_pattern = ""
|
|
172
|
+
except ValueError:
|
|
173
|
+
search_pattern = os.path.basename(pattern)
|
|
174
|
+
else:
|
|
175
|
+
root = "."
|
|
176
|
+
search_pattern = pattern
|
|
177
|
+
# Normalize pattern -> tokens split on path separators or whitespace
|
|
178
|
+
search_pattern = search_pattern.strip()
|
|
179
|
+
if search_pattern:
|
|
180
|
+
raw_tokens = [t for t in search_pattern.split(os.path.sep) if t]
|
|
181
|
+
else:
|
|
182
|
+
raw_tokens = []
|
|
183
|
+
# prepare tokens (case)
|
|
184
|
+
if not case_sensitive:
|
|
185
|
+
tokens = [t.lower() for t in raw_tokens]
|
|
186
|
+
else:
|
|
187
|
+
tokens = raw_tokens
|
|
188
|
+
# specific ignore list
|
|
189
|
+
try:
|
|
190
|
+
is_recursive = os.path.abspath(os.path.expanduser(root)).startswith(
|
|
191
|
+
os.path.abspath(os.getcwd())
|
|
192
|
+
)
|
|
193
|
+
except Exception:
|
|
194
|
+
is_recursive = False
|
|
195
|
+
# walk filesystem
|
|
196
|
+
candidates: list[tuple[float, str]] = []
|
|
197
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
198
|
+
# Filter directories
|
|
199
|
+
if not include_hidden:
|
|
200
|
+
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
|
201
|
+
rel_dir = os.path.relpath(dirpath, root)
|
|
202
|
+
# treat '.' as empty prefix
|
|
203
|
+
if rel_dir == ".":
|
|
204
|
+
rel_dir = ""
|
|
205
|
+
# build list of entries to test depending on files/dirs flags
|
|
206
|
+
entries = []
|
|
207
|
+
if dirs:
|
|
208
|
+
entries.extend([os.path.join(rel_dir, d) for d in dirnames])
|
|
209
|
+
if files:
|
|
210
|
+
entries.extend([os.path.join(rel_dir, f) for f in filenames])
|
|
211
|
+
if not is_recursive:
|
|
212
|
+
dirnames[:] = []
|
|
213
|
+
for ent in entries:
|
|
214
|
+
# Normalize presentation: use ./ prefix for relative paths
|
|
215
|
+
display_path = ent if ent else "."
|
|
216
|
+
# Skip hidden entries unless requested (double check for rel path segments)
|
|
217
|
+
if not include_hidden:
|
|
218
|
+
if any(
|
|
219
|
+
seg.startswith(".") for seg in display_path.split(os.sep) if seg
|
|
220
|
+
):
|
|
221
|
+
continue
|
|
222
|
+
cand = display_path.replace(os.sep, "/") # unify separator
|
|
223
|
+
cand_cmp = cand if case_sensitive else cand.lower()
|
|
224
|
+
last_pos = 0
|
|
225
|
+
score = 0.0
|
|
226
|
+
matched_all = True
|
|
227
|
+
for token in tokens:
|
|
228
|
+
# try contiguous substring search first
|
|
229
|
+
idx = cand_cmp.find(token, last_pos)
|
|
230
|
+
if idx != -1:
|
|
231
|
+
# good match: reward contiguous early matches
|
|
232
|
+
score += idx # smaller idx preferred
|
|
233
|
+
last_pos = idx + len(token)
|
|
234
|
+
else:
|
|
235
|
+
# fallback to subsequence matching
|
|
236
|
+
pos = self._find_subsequence_pos(cand_cmp, token, last_pos)
|
|
237
|
+
if pos is None:
|
|
238
|
+
matched_all = False
|
|
239
|
+
break
|
|
240
|
+
# subsequence match is less preferred than contiguous substring
|
|
241
|
+
score += pos + 0.5 * len(token)
|
|
242
|
+
last_pos = pos + len(token)
|
|
243
|
+
if matched_all:
|
|
244
|
+
# prefer shorter paths when score ties, so include length as tiebreaker
|
|
245
|
+
score += 0.01 * len(cand)
|
|
246
|
+
out = (
|
|
247
|
+
cand
|
|
248
|
+
if os.path.abspath(cand) == cand
|
|
249
|
+
else os.path.join(root, cand)
|
|
250
|
+
)
|
|
251
|
+
candidates.append((score, out))
|
|
252
|
+
# sort by score then lexicographically and return top results
|
|
253
|
+
candidates.sort(key=lambda x: (x[0], x[1]))
|
|
254
|
+
return [p for _, p in candidates[:max_results]]
|
|
255
|
+
|
|
256
|
+
def _find_subsequence_pos(
|
|
257
|
+
self, hay: str, needle: str, start: int = 0
|
|
258
|
+
) -> int | None:
|
|
259
|
+
"""
|
|
260
|
+
Try to locate needle in hay as a subsequence starting at `start`.
|
|
261
|
+
Returns the index of the first matched character of the subsequence or None if not match.
|
|
262
|
+
"""
|
|
263
|
+
if not needle:
|
|
264
|
+
return start
|
|
265
|
+
i = start
|
|
266
|
+
j = 0
|
|
267
|
+
first_pos = None
|
|
268
|
+
while i < len(hay) and j < len(needle):
|
|
269
|
+
if hay[i] == needle[j]:
|
|
270
|
+
if first_pos is None:
|
|
271
|
+
first_pos = i
|
|
272
|
+
j += 1
|
|
273
|
+
i += 1
|
|
274
|
+
return first_pos if j == len(needle) else None
|