zrb 1.13.1__py3-none-any.whl → 1.21.33__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. zrb/__init__.py +2 -6
  2. zrb/attr/type.py +10 -7
  3. zrb/builtin/__init__.py +2 -0
  4. zrb/builtin/git.py +12 -1
  5. zrb/builtin/group.py +31 -15
  6. zrb/builtin/http.py +7 -8
  7. zrb/builtin/llm/attachment.py +40 -0
  8. zrb/builtin/llm/chat_completion.py +287 -0
  9. zrb/builtin/llm/chat_session.py +130 -144
  10. zrb/builtin/llm/chat_session_cmd.py +288 -0
  11. zrb/builtin/llm/chat_trigger.py +78 -0
  12. zrb/builtin/llm/history.py +4 -4
  13. zrb/builtin/llm/llm_ask.py +218 -110
  14. zrb/builtin/llm/tool/api.py +74 -62
  15. zrb/builtin/llm/tool/cli.py +56 -21
  16. zrb/builtin/llm/tool/code.py +57 -47
  17. zrb/builtin/llm/tool/file.py +292 -255
  18. zrb/builtin/llm/tool/note.py +84 -0
  19. zrb/builtin/llm/tool/rag.py +25 -18
  20. zrb/builtin/llm/tool/search/__init__.py +1 -0
  21. zrb/builtin/llm/tool/search/brave.py +66 -0
  22. zrb/builtin/llm/tool/search/searxng.py +61 -0
  23. zrb/builtin/llm/tool/search/serpapi.py +61 -0
  24. zrb/builtin/llm/tool/sub_agent.py +53 -26
  25. zrb/builtin/llm/tool/web.py +94 -157
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +7 -7
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +5 -5
  28. zrb/builtin/project/add/fastapp/fastapp_util.py +1 -1
  29. zrb/builtin/searxng/config/settings.yml +5671 -0
  30. zrb/builtin/searxng/start.py +21 -0
  31. zrb/builtin/setup/latex/ubuntu.py +1 -0
  32. zrb/builtin/setup/ubuntu.py +1 -1
  33. zrb/builtin/shell/autocomplete/bash.py +4 -3
  34. zrb/builtin/shell/autocomplete/zsh.py +4 -3
  35. zrb/config/config.py +297 -79
  36. zrb/config/default_prompt/file_extractor_system_prompt.md +109 -9
  37. zrb/config/default_prompt/interactive_system_prompt.md +25 -28
  38. zrb/config/default_prompt/persona.md +1 -1
  39. zrb/config/default_prompt/repo_extractor_system_prompt.md +31 -31
  40. zrb/config/default_prompt/repo_summarizer_system_prompt.md +27 -8
  41. zrb/config/default_prompt/summarization_prompt.md +57 -16
  42. zrb/config/default_prompt/system_prompt.md +29 -25
  43. zrb/config/llm_config.py +129 -24
  44. zrb/config/llm_context/config.py +127 -90
  45. zrb/config/llm_context/config_parser.py +1 -7
  46. zrb/config/llm_context/workflow.py +81 -0
  47. zrb/config/llm_rate_limitter.py +100 -47
  48. zrb/context/any_shared_context.py +7 -1
  49. zrb/context/context.py +8 -2
  50. zrb/context/shared_context.py +6 -8
  51. zrb/group/any_group.py +12 -5
  52. zrb/group/group.py +67 -3
  53. zrb/input/any_input.py +5 -1
  54. zrb/input/base_input.py +18 -6
  55. zrb/input/option_input.py +13 -1
  56. zrb/input/text_input.py +7 -24
  57. zrb/runner/cli.py +21 -20
  58. zrb/runner/common_util.py +24 -19
  59. zrb/runner/web_route/task_input_api_route.py +5 -5
  60. zrb/runner/web_route/task_session_api_route.py +1 -4
  61. zrb/runner/web_util/user.py +7 -3
  62. zrb/session/any_session.py +12 -6
  63. zrb/session/session.py +39 -18
  64. zrb/task/any_task.py +24 -3
  65. zrb/task/base/context.py +17 -9
  66. zrb/task/base/execution.py +15 -8
  67. zrb/task/base/lifecycle.py +8 -4
  68. zrb/task/base/monitoring.py +12 -7
  69. zrb/task/base_task.py +69 -5
  70. zrb/task/base_trigger.py +12 -5
  71. zrb/task/llm/agent.py +130 -145
  72. zrb/task/llm/agent_runner.py +152 -0
  73. zrb/task/llm/config.py +45 -13
  74. zrb/task/llm/conversation_history.py +110 -29
  75. zrb/task/llm/conversation_history_model.py +4 -179
  76. zrb/task/llm/default_workflow/coding/workflow.md +41 -0
  77. zrb/task/llm/default_workflow/copywriting/workflow.md +68 -0
  78. zrb/task/llm/default_workflow/git/workflow.md +118 -0
  79. zrb/task/llm/default_workflow/golang/workflow.md +128 -0
  80. zrb/task/llm/default_workflow/html-css/workflow.md +135 -0
  81. zrb/task/llm/default_workflow/java/workflow.md +146 -0
  82. zrb/task/llm/default_workflow/javascript/workflow.md +158 -0
  83. zrb/task/llm/default_workflow/python/workflow.md +160 -0
  84. zrb/task/llm/default_workflow/researching/workflow.md +153 -0
  85. zrb/task/llm/default_workflow/rust/workflow.md +162 -0
  86. zrb/task/llm/default_workflow/shell/workflow.md +299 -0
  87. zrb/task/llm/file_replacement.py +206 -0
  88. zrb/task/llm/file_tool_model.py +57 -0
  89. zrb/task/llm/history_processor.py +206 -0
  90. zrb/task/llm/history_summarization.py +2 -192
  91. zrb/task/llm/print_node.py +192 -64
  92. zrb/task/llm/prompt.py +198 -153
  93. zrb/task/llm/subagent_conversation_history.py +41 -0
  94. zrb/task/llm/tool_confirmation_completer.py +41 -0
  95. zrb/task/llm/tool_wrapper.py +216 -55
  96. zrb/task/llm/workflow.py +76 -0
  97. zrb/task/llm_task.py +122 -70
  98. zrb/task/make_task.py +2 -3
  99. zrb/task/rsync_task.py +25 -10
  100. zrb/task/scheduler.py +4 -4
  101. zrb/util/attr.py +54 -39
  102. zrb/util/cli/markdown.py +12 -0
  103. zrb/util/cli/text.py +30 -0
  104. zrb/util/file.py +27 -11
  105. zrb/util/git.py +2 -2
  106. zrb/util/{llm/prompt.py → markdown.py} +2 -3
  107. zrb/util/string/conversion.py +1 -1
  108. zrb/util/truncate.py +23 -0
  109. zrb/util/yaml.py +204 -0
  110. zrb/xcom/xcom.py +10 -0
  111. {zrb-1.13.1.dist-info → zrb-1.21.33.dist-info}/METADATA +40 -20
  112. {zrb-1.13.1.dist-info → zrb-1.21.33.dist-info}/RECORD +114 -83
  113. {zrb-1.13.1.dist-info → zrb-1.21.33.dist-info}/WHEEL +1 -1
  114. zrb/task/llm/default_workflow/coding.md +0 -24
  115. zrb/task/llm/default_workflow/copywriting.md +0 -17
  116. zrb/task/llm/default_workflow/researching.md +0 -18
  117. {zrb-1.13.1.dist-info → zrb-1.21.33.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
- # Eager load CFG
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 = dict[str, StrAttr] | Callable[[AnySharedContext], dict[str, Any]]
12
- StrListAttr = list[StrAttr] | Callable[[AnySharedContext], list[str]]
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 == "main" or branch == "master":
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
- base64_group = cli.add_group(Group(name="base64", description="📄 Base64 operations"))
5
- uuid_group = cli.add_group(Group(name="uuid", description="🆔 UUID operations"))
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 = cli.add_group(Group(name="ulid", description="🔢 ULID operations"))
11
- jwt_group = cli.add_group(Group(name="jwt", description="🔒 JWT encode/decode"))
12
- http_group = cli.add_group(Group(name="http", description="🌐 HTTP request operations"))
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 = cli.add_group(Group(name="random", description="🔀 Random operation"))
15
- git_group = cli.add_group(Group(name="git", description="🌱 Git related commands"))
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 = cli.add_group(Group(name="llm", description="🤖 LLM operations"))
23
- md5_group = cli.add_group(Group(name="md5", description="🔢 Md5 operations"))
24
- python_group = cli.add_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 = cli.add_group(Group(name="todo", description="✅ Todo management"))
39
+ todo_group = _maybe_add_group(Group(name="todo", description="✅ Todo management"))
28
40
 
29
- shell_group = cli.add_group(
41
+ shell_group = _maybe_add_group(
30
42
  Group(name="shell", description="💬 Shell related commands")
31
43
  )
32
- shell_autocomplete_group: Group = shell_group.add_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 = cli.add_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 = cli.add_group(Group(name="setup", description="🔧 Setup"))
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
+ )
zrb/builtin/http.py CHANGED
@@ -2,6 +2,7 @@ from typing import Any
2
2
 
3
3
  from zrb.builtin.group import http_group
4
4
  from zrb.context.any_context import AnyContext
5
+ from zrb.input.bool_input import BoolInput
5
6
  from zrb.input.option_input import OptionInput
6
7
  from zrb.input.str_input import StrInput
7
8
  from zrb.task.make_task import make_task
@@ -32,10 +33,9 @@ from zrb.task.make_task import make_task
32
33
  prompt="Enter body as JSON",
33
34
  default="{}",
34
35
  ),
35
- OptionInput(
36
+ BoolInput(
36
37
  name="verify_ssl",
37
- default="true",
38
- options=["true", "false"],
38
+ default=True,
39
39
  description="Verify SSL certificate",
40
40
  ),
41
41
  ],
@@ -55,7 +55,7 @@ def http_request(ctx: AnyContext) -> Any:
55
55
  body = json.loads(ctx.input.body)
56
56
 
57
57
  # Make request
58
- verify = ctx.input.verify_ssl.lower() == "true"
58
+ verify = ctx.input.verify_ssl
59
59
  response = requests.request(
60
60
  method=ctx.input.method,
61
61
  url=ctx.input.url,
@@ -107,10 +107,9 @@ def http_request(ctx: AnyContext) -> Any:
107
107
  prompt="Enter body as JSON",
108
108
  default="{}",
109
109
  ),
110
- OptionInput(
110
+ BoolInput(
111
111
  name="verify_ssl",
112
- default="true",
113
- options=["true", "false"],
112
+ default=True,
114
113
  description="Verify SSL certificate",
115
114
  ),
116
115
  ],
@@ -137,7 +136,7 @@ def generate_curl(ctx: AnyContext) -> str:
137
136
  parts.extend(["--data-raw", shlex.quote(ctx.input.body)])
138
137
 
139
138
  # Add SSL verification
140
- if ctx.input.verify_ssl.lower() == "false":
139
+ if not ctx.input.verify_ssl:
141
140
  parts.append("--insecure")
142
141
 
143
142
  # Add URL
@@ -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,287 @@
1
+ import os
2
+ from typing import TYPE_CHECKING
3
+
4
+ from zrb.builtin.llm.chat_session_cmd import (
5
+ ADD_SUB_CMD,
6
+ ATTACHMENT_ADD_SUB_CMD_DESC,
7
+ ATTACHMENT_CLEAR_SUB_CMD_DESC,
8
+ ATTACHMENT_CMD,
9
+ ATTACHMENT_CMD_DESC,
10
+ ATTACHMENT_SET_SUB_CMD_DESC,
11
+ CLEAR_SUB_CMD,
12
+ HELP_CMD,
13
+ HELP_CMD_DESC,
14
+ MULTILINE_END_CMD,
15
+ MULTILINE_END_CMD_DESC,
16
+ MULTILINE_START_CMD,
17
+ MULTILINE_START_CMD_DESC,
18
+ QUIT_CMD,
19
+ QUIT_CMD_DESC,
20
+ RUN_CLI_CMD,
21
+ RUN_CLI_CMD_DESC,
22
+ SAVE_CMD,
23
+ SAVE_CMD_DESC,
24
+ SET_SUB_CMD,
25
+ WORKFLOW_ADD_SUB_CMD_DESC,
26
+ WORKFLOW_CLEAR_SUB_CMD_DESC,
27
+ WORKFLOW_CMD,
28
+ WORKFLOW_CMD_DESC,
29
+ WORKFLOW_SET_SUB_CMD_DESC,
30
+ YOLO_CMD,
31
+ YOLO_CMD_DESC,
32
+ YOLO_SET_CMD_DESC,
33
+ YOLO_SET_FALSE_CMD_DESC,
34
+ YOLO_SET_TRUE_CMD_DESC,
35
+ )
36
+
37
+ if TYPE_CHECKING:
38
+ from prompt_toolkit.completion import Completer
39
+
40
+
41
+ def get_chat_completer() -> "Completer":
42
+
43
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion
44
+ from prompt_toolkit.document import Document
45
+
46
+ class ChatCompleter(Completer):
47
+
48
+ def get_completions(self, document: Document, complete_event: CompleteEvent):
49
+ # Slash command
50
+ for completion in self._complete_slash_command(document):
51
+ yield completion
52
+ for completion in self._complete_slash_file_command(document):
53
+ yield completion
54
+ # Appendix
55
+ for completion in self._complete_appendix(document):
56
+ yield completion
57
+
58
+ def _complete_slash_file_command(self, document: Document):
59
+ text = document.text_before_cursor
60
+ prefixes = []
61
+ for cmd in ATTACHMENT_CMD:
62
+ for subcmd in ADD_SUB_CMD:
63
+ prefixes.append(f"{cmd} {subcmd} ")
64
+ for prefix in prefixes:
65
+ if text.startswith(prefix):
66
+ pattern = text[len(prefix) :]
67
+ potential_options = self._fuzzy_path_search(pattern, dirs=False)
68
+ for prefixed_option in [
69
+ f"{prefix}{option}" for option in potential_options
70
+ ]:
71
+ yield Completion(
72
+ prefixed_option,
73
+ start_position=-len(text),
74
+ )
75
+
76
+ def _complete_slash_command(self, document: Document):
77
+ text = document.text_before_cursor
78
+ if not text.startswith("/"):
79
+ return
80
+ for command, description in self._get_cmd_options().items():
81
+ if command.lower().startswith(text.lower()):
82
+ yield Completion(
83
+ command,
84
+ start_position=-len(text),
85
+ display_meta=description,
86
+ )
87
+
88
+ def _complete_appendix(self, document: Document):
89
+ token = document.get_word_before_cursor(WORD=True)
90
+ prefix = "@"
91
+ if not token.startswith(prefix):
92
+ return
93
+ pattern = token[len(prefix) :]
94
+ potential_options = self._fuzzy_path_search(pattern, dirs=False)
95
+ for prefixed_option in [
96
+ f"{prefix}{option}" for option in potential_options
97
+ ]:
98
+ yield Completion(
99
+ prefixed_option,
100
+ start_position=-len(token),
101
+ )
102
+
103
+ def _get_cmd_options(self):
104
+ cmd_options = {}
105
+ # Add all commands with their descriptions
106
+ for cmd in MULTILINE_START_CMD:
107
+ cmd_options[cmd] = MULTILINE_START_CMD_DESC
108
+ for cmd in MULTILINE_END_CMD:
109
+ cmd_options[cmd] = MULTILINE_END_CMD_DESC
110
+ for cmd in QUIT_CMD:
111
+ cmd_options[cmd] = QUIT_CMD_DESC
112
+ for cmd in WORKFLOW_CMD:
113
+ cmd_options[cmd] = WORKFLOW_CMD_DESC
114
+ for subcmd in ADD_SUB_CMD:
115
+ cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_ADD_SUB_CMD_DESC
116
+ for subcmd in CLEAR_SUB_CMD:
117
+ cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_CLEAR_SUB_CMD_DESC
118
+ for subcmd in SET_SUB_CMD:
119
+ cmd_options[f"{cmd} {subcmd}"] = WORKFLOW_SET_SUB_CMD_DESC
120
+ for cmd in SAVE_CMD:
121
+ cmd_options[cmd] = SAVE_CMD_DESC
122
+ for cmd in ATTACHMENT_CMD:
123
+ cmd_options[cmd] = ATTACHMENT_CMD_DESC
124
+ for subcmd in ADD_SUB_CMD:
125
+ cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_ADD_SUB_CMD_DESC
126
+ for subcmd in CLEAR_SUB_CMD:
127
+ cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_CLEAR_SUB_CMD_DESC
128
+ for subcmd in SET_SUB_CMD:
129
+ cmd_options[f"{cmd} {subcmd}"] = ATTACHMENT_SET_SUB_CMD_DESC
130
+ for cmd in YOLO_CMD:
131
+ cmd_options[cmd] = YOLO_CMD_DESC
132
+ for subcmd in SET_SUB_CMD:
133
+ cmd_options[f"{cmd} {subcmd} true"] = YOLO_SET_TRUE_CMD_DESC
134
+ cmd_options[f"{cmd} {subcmd} false"] = YOLO_SET_FALSE_CMD_DESC
135
+ cmd_options[f"{cmd} {subcmd}"] = YOLO_SET_CMD_DESC
136
+ for cmd in HELP_CMD:
137
+ cmd_options[cmd] = HELP_CMD_DESC
138
+ for cmd in RUN_CLI_CMD:
139
+ cmd_options[cmd] = RUN_CLI_CMD_DESC
140
+ return dict(sorted(cmd_options.items()))
141
+
142
+ def _fuzzy_path_search(
143
+ self,
144
+ pattern: str,
145
+ root: str | None = None,
146
+ max_results: int = 20,
147
+ include_hidden: bool = False,
148
+ case_sensitive: bool = False,
149
+ dirs: bool = True,
150
+ files: bool = True,
151
+ ) -> list[str]:
152
+ """
153
+ Return a list of filesystem paths under `root` that fuzzy-match `pattern`.
154
+ - pattern: e.g. "./some/x" or "proj util/io"
155
+ - include_hidden: if False skip files/dirs starting with '.'
156
+ - dirs/files booleans let you restrict results
157
+ - returns list of relative paths (from root), sorted best-first
158
+ """
159
+ search_pattern = pattern
160
+ if root is None:
161
+ # Determine root and adjust pattern if necessary
162
+ expanded_pattern = os.path.expanduser(pattern)
163
+ if os.path.isabs(expanded_pattern) or pattern.startswith("~"):
164
+ # For absolute paths, find the deepest existing directory
165
+ if os.path.isdir(expanded_pattern):
166
+ root = expanded_pattern
167
+ search_pattern = ""
168
+ else:
169
+ root = os.path.dirname(expanded_pattern)
170
+ while root and not os.path.isdir(root) and len(root) > 1:
171
+ root = os.path.dirname(root)
172
+ if not os.path.isdir(root):
173
+ root = "." # Fallback
174
+ search_pattern = pattern
175
+ else:
176
+ try:
177
+ search_pattern = os.path.relpath(expanded_pattern, root)
178
+ if search_pattern == ".":
179
+ search_pattern = ""
180
+ except ValueError:
181
+ search_pattern = os.path.basename(pattern)
182
+ else:
183
+ root = "."
184
+ search_pattern = pattern
185
+ # Normalize pattern -> tokens split on path separators or whitespace
186
+ search_pattern = search_pattern.strip()
187
+ if search_pattern:
188
+ raw_tokens = [t for t in search_pattern.split(os.path.sep) if t]
189
+ else:
190
+ raw_tokens = []
191
+ # prepare tokens (case)
192
+ if not case_sensitive:
193
+ tokens = [t.lower() for t in raw_tokens]
194
+ else:
195
+ tokens = raw_tokens
196
+ # specific ignore list
197
+ try:
198
+ is_recursive = os.path.abspath(os.path.expanduser(root)).startswith(
199
+ os.path.abspath(os.getcwd())
200
+ )
201
+ except Exception:
202
+ is_recursive = False
203
+ # walk filesystem
204
+ candidates: list[tuple[float, str]] = []
205
+ for dirpath, dirnames, filenames in os.walk(root):
206
+ # Filter directories
207
+ if not include_hidden:
208
+ dirnames[:] = [d for d in dirnames if not d.startswith(".")]
209
+ rel_dir = os.path.relpath(dirpath, root)
210
+ # treat '.' as empty prefix
211
+ if rel_dir == ".":
212
+ rel_dir = ""
213
+ # build list of entries to test depending on files/dirs flags
214
+ entries = []
215
+ if dirs:
216
+ entries.extend([os.path.join(rel_dir, d) for d in dirnames])
217
+ if files:
218
+ entries.extend([os.path.join(rel_dir, f) for f in filenames])
219
+ if not is_recursive:
220
+ dirnames[:] = []
221
+ for ent in entries:
222
+ # Normalize presentation: use ./ prefix for relative paths
223
+ display_path = ent if ent else "."
224
+ # Skip hidden entries unless requested (double check for rel path segments)
225
+ if not include_hidden:
226
+ if any(
227
+ seg.startswith(".")
228
+ for seg in display_path.split(os.sep)
229
+ if seg
230
+ ):
231
+ continue
232
+ cand = display_path.replace(os.sep, "/") # unify separator
233
+ cand_cmp = cand if case_sensitive else cand.lower()
234
+ last_pos = 0
235
+ score = 0.0
236
+ matched_all = True
237
+ for token in tokens:
238
+ # try contiguous substring search first
239
+ idx = cand_cmp.find(token, last_pos)
240
+ if idx != -1:
241
+ # good match: reward contiguous early matches
242
+ score += idx # smaller idx preferred
243
+ last_pos = idx + len(token)
244
+ else:
245
+ # fallback to subsequence matching
246
+ pos = self._find_subsequence_pos(cand_cmp, token, last_pos)
247
+ if pos is None:
248
+ matched_all = False
249
+ break
250
+ # subsequence match is less preferred than contiguous substring
251
+ score += pos + 0.5 * len(token)
252
+ last_pos = pos + len(token)
253
+ if matched_all:
254
+ # prefer shorter paths when score ties, so include length as tiebreaker
255
+ score += 0.01 * len(cand)
256
+ out = (
257
+ cand
258
+ if os.path.abspath(cand) == cand
259
+ else os.path.join(root, cand)
260
+ )
261
+ candidates.append((score, out))
262
+ # sort by score then lexicographically and return top results
263
+ candidates.sort(key=lambda x: (x[0], x[1]))
264
+ return [p for _, p in candidates[:max_results]]
265
+
266
+ def _find_subsequence_pos(
267
+ self, hay: str, needle: str, start: int = 0
268
+ ) -> int | None:
269
+ """
270
+ Try to locate needle in hay as a subsequence starting at `start`.
271
+ Returns the index of the first matched character of the subsequence or None if not
272
+ match.
273
+ """
274
+ if not needle:
275
+ return start
276
+ i = start
277
+ j = 0
278
+ first_pos = None
279
+ while i < len(hay) and j < len(needle):
280
+ if hay[i] == needle[j]:
281
+ if first_pos is None:
282
+ first_pos = i
283
+ j += 1
284
+ i += 1
285
+ return first_pos if j == len(needle) else None
286
+
287
+ return ChatCompleter()