kash-shell 0.3.8__py3-none-any.whl → 0.3.10__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 (154) hide show
  1. kash/actions/__init__.py +4 -4
  2. kash/actions/core/markdownify.py +5 -2
  3. kash/actions/core/readability.py +5 -2
  4. kash/actions/core/render_as_html.py +18 -0
  5. kash/actions/core/webpage_config.py +12 -4
  6. kash/commands/__init__.py +8 -20
  7. kash/commands/base/basic_file_commands.py +15 -0
  8. kash/commands/base/debug_commands.py +15 -2
  9. kash/commands/base/general_commands.py +27 -18
  10. kash/commands/base/logs_commands.py +1 -4
  11. kash/commands/base/model_commands.py +8 -8
  12. kash/commands/base/search_command.py +3 -2
  13. kash/commands/base/show_command.py +5 -3
  14. kash/commands/extras/parse_uv_lock.py +186 -0
  15. kash/commands/help/doc_commands.py +2 -31
  16. kash/commands/help/welcome.py +33 -0
  17. kash/commands/workspace/selection_commands.py +11 -6
  18. kash/commands/workspace/workspace_commands.py +19 -16
  19. kash/config/colors.py +2 -0
  20. kash/config/env_settings.py +72 -0
  21. kash/config/init.py +2 -2
  22. kash/config/logger.py +61 -59
  23. kash/config/logger_basic.py +12 -5
  24. kash/config/server_config.py +6 -6
  25. kash/config/settings.py +117 -67
  26. kash/config/setup.py +35 -9
  27. kash/config/suppress_warnings.py +30 -12
  28. kash/config/text_styles.py +3 -13
  29. kash/docs/load_api_docs.py +2 -1
  30. kash/docs/markdown/topics/a2_installation.md +7 -3
  31. kash/docs/markdown/topics/a3_getting_started.md +3 -2
  32. kash/docs/markdown/warning.md +3 -8
  33. kash/docs/markdown/welcome.md +4 -0
  34. kash/docs_base/load_recipe_snippets.py +1 -1
  35. kash/docs_base/recipes/{general_system_commands.ksh → general_system_commands.sh} +1 -1
  36. kash/{concepts → embeddings}/cosine.py +2 -1
  37. kash/embeddings/text_similarity.py +57 -0
  38. kash/exec/__init__.py +20 -3
  39. kash/exec/action_decorators.py +18 -4
  40. kash/exec/action_exec.py +41 -23
  41. kash/exec/action_registry.py +13 -48
  42. kash/exec/command_registry.py +2 -1
  43. kash/exec/fetch_url_metadata.py +4 -6
  44. kash/exec/importing.py +56 -0
  45. kash/exec/llm_transforms.py +6 -6
  46. kash/exec/precondition_registry.py +2 -1
  47. kash/exec/preconditions.py +16 -1
  48. kash/exec/shell_callable_action.py +33 -19
  49. kash/file_storage/file_store.py +23 -14
  50. kash/file_storage/item_file_format.py +13 -3
  51. kash/file_storage/metadata_dirs.py +11 -2
  52. kash/help/assistant.py +2 -2
  53. kash/help/assistant_instructions.py +2 -1
  54. kash/help/help_embeddings.py +2 -2
  55. kash/help/help_printing.py +14 -10
  56. kash/help/tldr_help.py +5 -3
  57. kash/llm_utils/clean_headings.py +1 -1
  58. kash/llm_utils/llm_api_keys.py +4 -4
  59. kash/llm_utils/llm_completion.py +2 -2
  60. kash/llm_utils/llm_features.py +68 -0
  61. kash/llm_utils/llm_messages.py +1 -2
  62. kash/llm_utils/llm_names.py +1 -1
  63. kash/llm_utils/llms.py +17 -12
  64. kash/local_server/__init__.py +5 -2
  65. kash/local_server/local_server.py +56 -46
  66. kash/local_server/local_server_commands.py +15 -15
  67. kash/local_server/local_server_routes.py +2 -2
  68. kash/local_server/local_url_formatters.py +1 -1
  69. kash/mcp/__init__.py +5 -2
  70. kash/mcp/mcp_cli.py +54 -17
  71. kash/mcp/mcp_server_commands.py +5 -6
  72. kash/mcp/mcp_server_routes.py +14 -11
  73. kash/mcp/mcp_server_sse.py +61 -34
  74. kash/mcp/mcp_server_stdio.py +0 -8
  75. kash/media_base/audio_processing.py +81 -7
  76. kash/media_base/media_cache.py +18 -18
  77. kash/media_base/media_services.py +1 -1
  78. kash/media_base/media_tools.py +6 -6
  79. kash/media_base/services/local_file_media.py +2 -2
  80. kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -109
  81. kash/media_base/transcription_format.py +73 -0
  82. kash/media_base/transcription_whisper.py +38 -0
  83. kash/model/__init__.py +73 -5
  84. kash/model/actions_model.py +38 -4
  85. kash/model/concept_model.py +30 -0
  86. kash/model/items_model.py +56 -13
  87. kash/model/params_model.py +24 -0
  88. kash/shell/completions/completion_scoring.py +37 -5
  89. kash/shell/output/kerm_codes.py +1 -2
  90. kash/shell/output/shell_formatting.py +14 -4
  91. kash/shell/shell_main.py +2 -2
  92. kash/shell/utils/exception_printing.py +6 -0
  93. kash/shell/utils/native_utils.py +26 -20
  94. kash/text_handling/custom_sliding_transforms.py +12 -4
  95. kash/text_handling/doc_normalization.py +6 -2
  96. kash/text_handling/markdown_render.py +117 -0
  97. kash/text_handling/markdown_utils.py +204 -0
  98. kash/utils/common/import_utils.py +12 -3
  99. kash/utils/common/type_utils.py +0 -29
  100. kash/utils/common/url.py +80 -28
  101. kash/utils/errors.py +6 -0
  102. kash/utils/file_utils/{dir_size.py → dir_info.py} +25 -4
  103. kash/utils/file_utils/file_ext.py +2 -3
  104. kash/utils/file_utils/file_formats.py +28 -2
  105. kash/utils/file_utils/file_formats_model.py +50 -19
  106. kash/utils/file_utils/filename_parsing.py +10 -4
  107. kash/web_content/dir_store.py +1 -2
  108. kash/web_content/file_cache_utils.py +37 -10
  109. kash/web_content/file_processing.py +68 -0
  110. kash/web_content/local_file_cache.py +12 -9
  111. kash/web_content/web_extract.py +8 -3
  112. kash/web_content/web_fetch.py +12 -4
  113. kash/web_gen/tabbed_webpage.py +5 -2
  114. kash/web_gen/templates/base_styles.css.jinja +120 -14
  115. kash/web_gen/templates/base_webpage.html.jinja +60 -13
  116. kash/web_gen/templates/content_styles.css.jinja +4 -2
  117. kash/web_gen/templates/item_view.html.jinja +2 -2
  118. kash/web_gen/templates/tabbed_webpage.html.jinja +1 -2
  119. kash/workspaces/__init__.py +15 -2
  120. kash/workspaces/selections.py +18 -3
  121. kash/workspaces/source_items.py +4 -2
  122. kash/workspaces/workspace_output.py +11 -4
  123. kash/workspaces/workspaces.py +5 -11
  124. kash/xonsh_custom/command_nl_utils.py +40 -19
  125. kash/xonsh_custom/custom_shell.py +44 -12
  126. kash/xonsh_custom/customize_prompt.py +39 -21
  127. kash/xonsh_custom/load_into_xonsh.py +26 -27
  128. kash/xonsh_custom/shell_load_commands.py +2 -2
  129. kash/xonsh_custom/xonsh_completers.py +2 -249
  130. kash/xonsh_custom/xonsh_keybindings.py +282 -0
  131. kash/xonsh_custom/xonsh_modern_tools.py +3 -3
  132. kash/xontrib/kash_extension.py +5 -6
  133. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/METADATA +26 -12
  134. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/RECORD +140 -140
  135. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/entry_points.txt +1 -1
  136. kash/concepts/concept_formats.py +0 -23
  137. kash/concepts/text_similarity.py +0 -112
  138. kash/shell/clideps/api_keys.py +0 -99
  139. kash/shell/clideps/dotenv_setup.py +0 -114
  140. kash/shell/clideps/dotenv_utils.py +0 -89
  141. kash/shell/clideps/pkg_deps.py +0 -232
  142. kash/shell/clideps/platforms.py +0 -11
  143. kash/shell/clideps/terminal_features.py +0 -56
  144. kash/shell/utils/osc_utils.py +0 -95
  145. kash/shell/utils/terminal_images.py +0 -133
  146. kash/text_handling/markdown_util.py +0 -167
  147. kash/utils/common/atomic_var.py +0 -158
  148. kash/utils/common/string_replace.py +0 -93
  149. kash/utils/common/string_template.py +0 -101
  150. /kash/docs_base/recipes/{python_dev_commands.ksh → python_dev_commands.sh} +0 -0
  151. /kash/docs_base/recipes/{tldr_standard_commands.ksh → tldr_standard_commands.sh} +0 -0
  152. /kash/{concepts → embeddings}/embeddings.py +0 -0
  153. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/WHEEL +0 -0
  154. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,17 @@
1
1
  import os
2
- import sys
2
+ import signal
3
3
  import threading
4
4
  import time
5
5
  from collections.abc import Callable
6
6
  from os.path import expanduser
7
- from typing import cast
7
+ from subprocess import CalledProcessError
8
+ from types import TracebackType
9
+ from typing import TypeAlias, cast
8
10
 
9
11
  import xonsh.tools as xt
10
12
  from prompt_toolkit.formatted_text import FormattedText
11
13
  from pygments.token import Token
12
- from strif import abbrev_str
14
+ from strif import abbrev_str, quote_if_needed
13
15
  from typing_extensions import override
14
16
  from xonsh.built_ins import XSH
15
17
  from xonsh.environ import xonshrc_context
@@ -27,7 +29,7 @@ from kash.config import colors
27
29
  from kash.config.lazy_imports import import_start_time # usort:skip
28
30
  from kash.config.logger import get_console, get_log_settings, get_logger
29
31
  from kash.config.settings import APP_NAME, find_rcfiles
30
- from kash.config.text_styles import SPINNER, STYLE_ASSISTANCE
32
+ from kash.config.text_styles import SPINNER, STYLE_ASSISTANCE, STYLE_HINT
31
33
  from kash.help.assistant import AssistanceType
32
34
  from kash.shell.output.shell_output import cprint
33
35
  from kash.shell.ui.shell_syntax import is_assist_request_str
@@ -108,12 +110,33 @@ class CustomPTKPromptFormatter(PTKPromptFormatter):
108
110
  return super().__call__(template=cast(str, template), **kwargs)
109
111
 
110
112
 
113
+ def exit_code_str(e: CalledProcessError) -> str:
114
+ """
115
+ Prettier version of `CalledProcessError.__str__()`.
116
+ """
117
+ if isinstance(e.cmd, list):
118
+ cmd = "`" + " ".join(quote_if_needed(c) for c in e.cmd) + "`"
119
+ else:
120
+ cmd = str(e.cmd)
121
+ if e.returncode and e.returncode < 0:
122
+ try:
123
+ signal_name = signal.Signals(-e.returncode).name
124
+ return f"Command died with {signal_name} ({e.returncode}): {cmd}"
125
+ except ValueError:
126
+ return f"Command died with unknown signal {e.returncode}: {cmd}"
127
+ else:
128
+ return f"Command returned non-zero exit status {e.returncode}: {cmd}"
129
+
130
+
111
131
  # Base shell can be ReadlineShell or PromptToolkitShell.
112
132
  # Completer can be RankingCompleter or the standard Completer.
113
133
  # from xonsh.completer import Completer
114
134
  # from xonsh.shells.readline_shell import ReadlineShell
115
135
  from xonsh.shells.ptk_shell import PromptToolkitShell
116
136
 
137
+ ExcInfo: TypeAlias = tuple[type[BaseException], BaseException, TracebackType]
138
+ OptExcInfo: TypeAlias = ExcInfo | tuple[None, None, None]
139
+
117
140
 
118
141
  class CustomAssistantShell(PromptToolkitShell):
119
142
  """
@@ -122,7 +145,6 @@ class CustomAssistantShell(PromptToolkitShell):
122
145
  We're trying to reuse code where possible but need to change some of xonsh's
123
146
  behavior. Note event hooks in xonsh do let you customize handling but don't
124
147
  let you disable xonsh's processing, so it seems like this is necessary.
125
- so it seems like this is necessary.
126
148
  """
127
149
 
128
150
  def __init__(self, **kwargs):
@@ -186,16 +208,24 @@ class CustomAssistantShell(PromptToolkitShell):
186
208
  exc_info = (None, None, None)
187
209
  try:
188
210
  log.info("Running shell code: %r", src)
189
- exc_info = run_compiled_code(code, self.ctx, None, "single")
190
- if exc_info != (None, None, None):
191
- raise exc_info[1] # pyright: ignore
211
+ exc_info: OptExcInfo = run_compiled_code(code, self.ctx, None, "single") # pyright: ignore
212
+ log.debug("Completed shell code: %r", src)
213
+ _type, exc, _traceback = exc_info
214
+ if exc:
215
+ log.info("Shell exception info: %s", exc)
216
+ raise exc
192
217
  ts1 = time.time()
193
218
  if hist is not None and hist.last_cmd_rtn is None:
194
219
  hist.last_cmd_rtn = 0 # returncode for success
195
- log.info("Shell code completed successfully: %s", src)
220
+ except CalledProcessError as e:
221
+ # No point in logging stack trace here as it is only the shell stack,
222
+ # not the original code.
223
+ log.warning("%s", exit_code_str(e))
224
+ cprint("See `logs` for more details.", style=STYLE_HINT)
225
+ # print(e.args[0], file=sys.stderr)
196
226
  except xt.XonshError as e:
197
227
  log.info("Shell exception details: %s", e, exc_info=True)
198
- print(e.args[0], file=sys.stderr)
228
+ # print(e.args[0], file=sys.stderr)
199
229
  if hist is not None and hist.last_cmd_rtn is None: # pyright: ignore
200
230
  hist.last_cmd_rtn = 1 # return code for failure
201
231
  except (SystemExit, KeyboardInterrupt) as err:
@@ -323,7 +353,9 @@ def customize_xonsh_settings(is_interactive: bool):
323
353
  # Disable suggest for command not found errors (we handle this ourselves).
324
354
  "SUGGEST_COMMANDS": False,
325
355
  # TODO: Consider enabling and adapting auto-suggestions.
326
- "AUTO_SUGGEST": False,
356
+ "AUTO_SUGGEST": True,
357
+ # Show auto-suggestions in the completion menu.
358
+ "AUTO_SUGGEST_IN_COMPLETIONS": False,
327
359
  # Completions can be "none", "single", "multi", or "readline".
328
360
  # "single" lets us have rich completions with descriptions alongside.
329
361
  # https://xon.sh/envvars.html#completions-display
@@ -445,7 +477,7 @@ def start_shell(single_command: str | None = None, ready_event: threading.Event
445
477
 
446
478
  # If we want to replicate all the xonsh settings including .xonshrc, we could call
447
479
  # start_services(). It may be problematic to support all xonsh enhancements, however,
448
- # so let's only load ~/.kashrc files.
480
+ # so let's only load ~/.config/kash/kashrc files.
449
481
  load_rcfiles(execer, ctx)
450
482
 
451
483
  # Imports are so slow we will need to improve this. Let's time it.
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from dataclasses import dataclass
2
3
  from enum import Enum
3
4
  from pathlib import Path
@@ -29,8 +30,10 @@ class PromptInfo:
29
30
  workspace_details: str
30
31
  is_global_ws: bool
31
32
  cwd_str: str
33
+ cwd_short_str: str
32
34
  cwd_details: str
33
35
  cwd_in_workspace: bool
36
+ cwd_in_home: bool
34
37
 
35
38
 
36
39
  def get_prompt_info() -> PromptInfo:
@@ -42,28 +45,39 @@ def get_prompt_info() -> PromptInfo:
42
45
  workspace_details = f"Workspace at {ws.base_dir}"
43
46
 
44
47
  cwd = Path(".").resolve()
45
- if cwd.is_relative_to(ws.base_dir):
46
- cwd_in_workspace = True
48
+ cwd_in_home = cwd.is_relative_to(Path.home())
49
+ cwd_in_workspace = cwd.is_relative_to(ws.base_dir)
50
+ if cwd_in_workspace:
47
51
  rel_cwd = cwd.relative_to(ws.base_dir)
48
52
  if rel_cwd != Path("."):
49
53
  cwd_str = str(rel_cwd)
50
54
  else:
51
55
  cwd_str = ""
52
- elif cwd.is_relative_to(Path.home()):
53
- cwd_in_workspace = False
56
+ cwd_short_str = cwd_str
57
+ elif cwd_in_home:
54
58
  rel_to_home = cwd.relative_to(Path.home())
55
- if rel_to_home != Path("."):
56
- cwd_str = "~/" + rel_to_home.as_posix()
59
+ if cwd == Path.home():
60
+ cwd_str = cwd_short_str = "~"
61
+ elif cwd.parent == Path.home():
62
+ cwd_str = cwd_short_str = os.path.join("~", cwd.name)
57
63
  else:
58
- cwd_str = "~"
64
+ cwd_str = "~/" + str(rel_to_home)
65
+ # Show only last two levels of dirs when inside home.
66
+ cwd_short_str = os.path.join(cwd.parent.name, cwd.name)
59
67
  else:
60
- cwd_in_workspace = False
61
- cwd_str = cwd.as_posix()
68
+ cwd_str = cwd_short_str = str(cwd)
62
69
 
63
70
  cwd_details = f"Current directory at {cwd}"
64
71
 
65
72
  return PromptInfo(
66
- ws_name, workspace_details, is_global_ws, cwd_str, cwd_details, cwd_in_workspace
73
+ ws_name,
74
+ workspace_details,
75
+ is_global_ws,
76
+ cwd_str,
77
+ cwd_short_str,
78
+ cwd_details,
79
+ cwd_in_workspace,
80
+ cwd_in_home,
67
81
  )
68
82
 
69
83
 
@@ -167,17 +181,21 @@ def kash_xonsh_prompt() -> FormattedText:
167
181
  ).as_ptk_tokens(style=workspace_color)
168
182
 
169
183
  cwd_style = settings.ptk_style_bright if info.cwd_in_workspace else settings.ptk_style_normal
170
- # Use two-line prompt if cwd is outside workspace, one-line prompt otherwise.
171
- if info.cwd_str and not info.cwd_in_workspace:
184
+ # Separator could be "\n" if you like a 2-line prompt.
185
+ # separator = "\n" + settings.prompt_prefix
186
+ separator = ""
187
+ # Using shorter cwd str.
188
+ cwd_str = info.cwd_short_str
189
+ # If outside the workspace, show both workspace and cwd.
190
+ if info.cwd_short_str and not info.cwd_in_workspace:
172
191
  cwd_tokens = (
173
- [(settings.ptk_style_dim, "\n" + settings.prompt_prefix)]
174
- + text_with_tooltip(info.cwd_str, hover_text=info.cwd_details).as_ptk_tokens(
175
- style=cwd_style
176
- )
192
+ [(settings.ptk_style_dim, separator)]
193
+ + text_with_tooltip(cwd_str, hover_text=info.cwd_details).as_ptk_tokens(style=cwd_style)
177
194
  + [(settings.ptk_style_dim, " ")]
178
195
  )
179
- elif info.cwd_str:
180
- cwd_tokens = text_with_tooltip(info.cwd_str, hover_text=info.cwd_details).as_ptk_tokens(
196
+ elif cwd_str:
197
+ # If inside the workspace, show just cwd.
198
+ cwd_tokens = text_with_tooltip(cwd_str, hover_text=info.cwd_details).as_ptk_tokens(
181
199
  style=cwd_style
182
200
  )
183
201
  else:
@@ -187,9 +205,9 @@ def kash_xonsh_prompt() -> FormattedText:
187
205
  ptk_tokens = (
188
206
  settings.hrule
189
207
  + [(settings.ptk_style_dim, settings.prompt_prefix)]
190
- + [
191
- (settings.ptk_style_dim, "workspace: "),
192
- ]
208
+ # + [
209
+ # (settings.ptk_style_dim, "ws: "),
210
+ # ]
193
211
  + workspace_tokens
194
212
  + [
195
213
  (settings.ptk_style_dim, " "),
@@ -1,51 +1,49 @@
1
- from kash.config.setup import setup
2
- from kash.config.text_styles import LOGO_NAME, STYLE_HINT
3
- from kash.xonsh_custom.shell_load_commands import (
4
- is_interactive,
5
- log_command_action_info,
6
- reload_shell_commands_and_actions,
7
- set_env,
8
- )
1
+ from kash.config.setup import kash_setup
9
2
 
10
- setup(rich_logging=True) # Set up logging first.
3
+ kash_setup(rich_logging=True) # Set up logging first.
11
4
 
12
5
  import time
13
6
 
7
+ from clideps.pkgs.pkg_check import pkg_check
14
8
  from xonsh.built_ins import XSH
15
9
  from xonsh.prompt.base import PromptFields
16
10
 
17
11
  from kash.commands.base.general_commands import self_check
18
- from kash.commands.help import doc_commands
12
+ from kash.commands.help.welcome import welcome
19
13
  from kash.config.logger import get_logger
20
- from kash.config.settings import check_kerm_code_support
21
- from kash.local_server.local_server import start_local_server
14
+ from kash.config.settings import RECOMMENDED_PKGS, check_kerm_code_support
15
+ from kash.config.text_styles import LOGO_NAME, STYLE_HINT
16
+ from kash.local_server.local_server import start_ui_server
22
17
  from kash.local_server.local_url_formatters import enable_local_urls
23
- from kash.shell.clideps.pkg_deps import pkg_check
18
+ from kash.mcp.mcp_server_commands import start_mcp_server
24
19
  from kash.shell.output.shell_output import PrintHooks, cprint
25
20
  from kash.shell.version import get_version_tag
26
21
  from kash.workspaces import current_ws
27
22
  from kash.xonsh_custom.customize_prompt import get_prompt_info, kash_xonsh_prompt
23
+ from kash.xonsh_custom.shell_load_commands import (
24
+ is_interactive,
25
+ log_command_action_info,
26
+ reload_shell_commands_and_actions,
27
+ set_env,
28
+ )
28
29
  from kash.xonsh_custom.xonsh_completers import load_completers
30
+ from kash.xonsh_custom.xonsh_keybindings import add_key_bindings
29
31
  from kash.xonsh_custom.xonsh_modern_tools import modernize_shell
30
32
 
31
33
  log = get_logger(__name__)
32
34
 
33
35
 
34
- def _kash_workspace_str() -> str:
35
- return get_prompt_info().workspace_name
36
-
37
-
38
36
  def _shell_interactive_setup():
39
- from kash.xonsh_custom.xonsh_completers import add_key_bindings
40
-
41
37
  # Set up a prompt field for the workspace string.
42
38
  fields = PromptFields(XSH)
43
- fields["workspace_str"] = _kash_workspace_str
39
+ prompt_info = get_prompt_info()
40
+ fields["workspace_str"] = prompt_info.workspace_name
41
+ fields["cwd_short_str"] = prompt_info.cwd_short_str
44
42
  set_env("PROMPT_FIELDS", fields)
45
43
 
46
44
  # Set up the prompt and title template.
47
45
  set_env("PROMPT", kash_xonsh_prompt)
48
- set_env("TITLE", LOGO_NAME + " - {workspace_str}")
46
+ set_env("TITLE", LOGO_NAME + " - {workspace_str} - {cwd_short_str}")
49
47
 
50
48
  add_key_bindings()
51
49
 
@@ -58,12 +56,12 @@ def load_into_xonsh():
58
56
  """
59
57
 
60
58
  if is_interactive():
59
+ # Do welcome first since init could take a few seconds.
60
+ welcome()
61
+
61
62
  # Do first so in case there is an error, the shell prompt etc works as expected.
62
63
  _shell_interactive_setup()
63
64
 
64
- # Then do welcome first since init could take a few seconds.
65
- doc_commands.welcome()
66
-
67
65
  def load():
68
66
  load_start_time = time.time()
69
67
 
@@ -91,20 +89,21 @@ def load_into_xonsh():
91
89
  # Currently only Kerm supports our advanced UI with Kerm codes.
92
90
  supports_kerm_codes = check_kerm_code_support()
93
91
  if supports_kerm_codes:
94
- start_local_server()
92
+ start_ui_server()
95
93
  enable_local_urls(True)
96
94
  else:
97
95
  cprint(
98
- "If your terminal supports it, you may use `start_local_server` to enable local links.",
96
+ "If your terminal supports it, you may use `start_ui_server` to enable local links.",
99
97
  style=STYLE_HINT,
100
98
  )
99
+ start_mcp_server()
101
100
 
102
101
  cprint()
103
102
  log_command_action_info()
104
103
 
105
104
  current_ws() # Validates and logs info for user.
106
105
 
107
- pkg_check().warn_if_missing()
106
+ pkg_check().warn_if_missing(*RECOMMENDED_PKGS)
108
107
 
109
108
  else:
110
109
  reload_shell_commands_and_actions()
@@ -1,8 +1,8 @@
1
1
  from kash.actions import get_loaded_kits
2
- from kash.config.setup import setup
2
+ from kash.config.setup import kash_setup
3
3
  from kash.config.text_styles import COLOR_VALUE, STYLE_HINT
4
4
 
5
- setup(rich_logging=True) # Set up logging first.
5
+ kash_setup(rich_logging=True) # Set up logging first.
6
6
 
7
7
  from collections.abc import Callable
8
8
  from typing import TYPE_CHECKING, TypeVar
@@ -1,14 +1,8 @@
1
- import re
2
1
  from dataclasses import dataclass
3
2
  from typing import Any, cast
4
3
 
5
4
  from funlog import log_calls
6
- from prompt_toolkit import search
7
- from prompt_toolkit.application import get_app
8
- from prompt_toolkit.filters import Condition
9
- from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings
10
- from prompt_toolkit.key_binding.key_processor import KeyPressEvent
11
- from prompt_toolkit.shortcuts import print_formatted_text
5
+ from strif import AtomicVar
12
6
  from xonsh.built_ins import XSH
13
7
  from xonsh.completers.completer import add_one_completer
14
8
  from xonsh.completers.tools import (
@@ -19,7 +13,6 @@ from xonsh.completers.tools import (
19
13
  )
20
14
  from xonsh.parsers.completion_context import CommandContext
21
15
 
22
- from kash.actions.core.assistant_chat import assistant_chat
23
16
  from kash.config.logger import get_logger
24
17
  from kash.exec.action_registry import get_all_actions_defaults
25
18
  from kash.exec.command_registry import get_all_commands
@@ -34,8 +27,6 @@ from kash.shell.completions.shell_completions import (
34
27
  get_std_command_completions,
35
28
  trace_completions,
36
29
  )
37
- from kash.shell.ui.shell_syntax import assist_request_str
38
- from kash.utils.common.atomic_var import AtomicVar
39
30
  from kash.utils.errors import ApiResultError, InvalidState
40
31
  from kash.xonsh_custom.command_nl_utils import as_nl_words, looks_like_nl
41
32
  from kash.xonsh_custom.shell_which import is_valid_command
@@ -351,7 +342,7 @@ def _params_for_command(command_name: str) -> list[Param[Any]] | None:
351
342
  if command:
352
343
  return annotate_param_info(command)
353
344
  elif action:
354
- return list(action.params)
345
+ return action.shell_params
355
346
  else:
356
347
  return None
357
348
 
@@ -482,244 +473,6 @@ def options_enum_completer(context: CompletionContext) -> CompleterResult:
482
473
  # - `source_code`
483
474
 
484
475
 
485
- @Condition
486
- def is_unquoted_assist_request():
487
- app = get_app()
488
- buf = app.current_buffer
489
- text = buf.text.strip()
490
- is_default_buffer = buf.name == "DEFAULT_BUFFER"
491
- has_prefix = text.startswith("?") and not (text.startswith('? "') or text.startswith("? '"))
492
- return is_default_buffer and has_prefix
493
-
494
-
495
- _command_regex = re.compile(r"^[a-zA-Z0-9_-]+$")
496
-
497
- _python_keyword_regex = re.compile(
498
- r"assert|async|await|break|class|continue|def|del|elif|else|except|finally|"
499
- r"for|from|global|if|import|lambda|nonlocal|pass|raise|return|try|while|with|yield"
500
- )
501
-
502
-
503
- def _extract_command_name(text: str) -> str | None:
504
- text = text.split()[0]
505
- if _python_keyword_regex.match(text):
506
- return None
507
- if _command_regex.match(text):
508
- return text
509
- return None
510
-
511
-
512
- @Condition
513
- def whitespace_only() -> bool:
514
- app = get_app()
515
- buf = app.current_buffer
516
- return not buf.text.strip()
517
-
518
-
519
- @Condition
520
- def is_typo_command() -> bool:
521
- """
522
- Is the command itself invalid? Should be conservative, so we can suppress
523
- executing it if it is definitely a typo.
524
- """
525
-
526
- app = get_app()
527
- buf = app.current_buffer
528
- text = buf.text.strip()
529
-
530
- is_default_buffer = buf.name == "DEFAULT_BUFFER"
531
- if not is_default_buffer:
532
- return False
533
-
534
- # Assistant NL requests always allowed.
535
- has_assistant_prefix = text.startswith("?") or text.rstrip().endswith("?")
536
- if has_assistant_prefix:
537
- return False
538
-
539
- # Anything more complex is probably Python.
540
- # TODO: Do a better syntax parse of this as Python, or use xonsh's algorithm.
541
- for s in ["\n", "(", ")"]:
542
- if s in text:
543
- return False
544
-
545
- # Empty command line allowed.
546
- if not text:
547
- return False
548
-
549
- # Now look at the command.
550
- command_name = _extract_command_name(text)
551
-
552
- # Python or missing command is fine.
553
- if not command_name:
554
- return False
555
-
556
- # Recognized command.
557
- if is_valid_command(command_name):
558
- return False
559
-
560
- # Okay it's almost certainly a command typo.
561
- return True
562
-
563
-
564
- @Condition
565
- def is_completion_menu_active() -> bool:
566
- app = get_app()
567
- return app.current_buffer.complete_state is not None
568
-
569
-
570
- @Condition
571
- def could_show_more_tab_completions() -> bool:
572
- return _MULTI_TAB_STATE.value.could_show_more()
573
-
574
-
575
- # Set up prompt_toolkit key bindings.
576
- def add_key_bindings() -> None:
577
- custom_bindings = KeyBindings()
578
-
579
- # Need to be careful only to bind with a filter if the state is suitable.
580
- # Only add more completions if we've seen some results but user hasn't pressed
581
- # tab a second time yet. Otherwise the behavior should fall back to usual ptk
582
- # tab behavior (selecting each completion one by one).
583
- @custom_bindings.add("tab", filter=could_show_more_tab_completions)
584
- def _(event: KeyPressEvent):
585
- """
586
- Add a second tab to show more completions.
587
- """
588
- with _MULTI_TAB_STATE.updates() as state:
589
- state.more_results_requested = True
590
-
591
- trace_completions("More completion results requested", state)
592
-
593
- # Restart completions.
594
- buf = event.app.current_buffer
595
- buf.complete_state = None
596
- buf.start_completion()
597
-
598
- @custom_bindings.add("s-tab")
599
- def _(event: KeyPressEvent):
600
- with _MULTI_TAB_STATE.updates() as state:
601
- state.more_results_requested = True
602
-
603
- trace_completions("More completion results requested", state)
604
-
605
- # Restart completions.
606
- buf = event.app.current_buffer
607
- buf.complete_state = None
608
- buf.start_completion()
609
-
610
- @custom_bindings.add(" ", filter=whitespace_only)
611
- def _(event: KeyPressEvent):
612
- """
613
- Map space at the start of the line to `? ` to invoke an assistant question.
614
- """
615
- buf = event.app.current_buffer
616
- if buf.text == " " or buf.text == "":
617
- buf.delete_before_cursor(len(buf.text))
618
- buf.insert_text("? ")
619
- else:
620
- buf.insert_text(" ")
621
-
622
- @custom_bindings.add(" ", filter=is_typo_command)
623
- def _(event: KeyPressEvent):
624
- """
625
- If the user types two words and the first word is likely an invalid
626
- command, jump back to prefix the whole line with `? ` to make it clear we're
627
- in natural language mode.
628
- """
629
-
630
- buf = event.app.current_buffer
631
- text = buf.text.strip()
632
-
633
- if (
634
- buf.cursor_position == len(buf.text)
635
- and len(text.split()) >= 2
636
- and not text.startswith("?")
637
- ):
638
- buf.transform_current_line(lambda line: "? " + line)
639
- buf.cursor_position += 2
640
-
641
- buf.insert_text(" ")
642
-
643
- @custom_bindings.add("enter", filter=whitespace_only)
644
- def _(_event: KeyPressEvent):
645
- """
646
- Suppress enter if the command line is empty, but add a newline above the prompt.
647
- """
648
- print_formatted_text("")
649
-
650
- @custom_bindings.add("enter", filter=is_unquoted_assist_request)
651
- def _(event: KeyPressEvent):
652
- """
653
- Automatically add quotes around assistant questions, so there are not
654
- syntax errors if the command line contains unclosed quotes etc.
655
- """
656
-
657
- buf = event.app.current_buffer
658
- text = buf.text.strip()
659
-
660
- question_text = text[1:].strip()
661
- if not question_text:
662
- # If the user enters an empty assistant request, treat it as a shortcut to go to the assistant chat.
663
- buf.delete_before_cursor(len(buf.text))
664
- buf.insert_text(assistant_chat.__name__)
665
- else:
666
- # Convert it to an assistant question starting with a `?`.
667
- buf.delete_before_cursor(len(buf.text))
668
- buf.insert_text(assist_request_str(question_text))
669
-
670
- buf.validate_and_handle()
671
-
672
- @custom_bindings.add("enter", filter=is_typo_command)
673
- def _(event: KeyPressEvent):
674
- """
675
- Suppress enter and if possible give completions if the command is just not a valid command.
676
- """
677
-
678
- buf = event.app.current_buffer
679
- buf.start_completion()
680
-
681
- # TODO: Also suppress enter if a command or action doesn't meet the required args,
682
- # selection, or preconditions.
683
- # Perhaps also have a way to get confirmation if its a rarely used or unexpected command
684
- # (based on history/suggestions).
685
- # TODO: Add suggested replacements, e.g. df -> duf, top -> btm, etc.
686
-
687
- @custom_bindings.add("@")
688
- def _(event: KeyPressEvent):
689
- """
690
- Auto-trigger item completions after `@` sign.
691
- """
692
- buf = event.app.current_buffer
693
- buf.insert_text("@")
694
- buf.start_completion()
695
-
696
- @custom_bindings.add("escape", eager=True, filter=is_completion_menu_active)
697
- def _(event: KeyPressEvent):
698
- """
699
- Close the completion menu when escape is pressed.
700
- """
701
- event.app.current_buffer.cancel_completion()
702
-
703
- @custom_bindings.add("c-c", eager=True)
704
- def _(event):
705
- """
706
- Control-C to reset the current buffer. Similar to usual behavior but doesn't
707
- leave ugly prompt chars.
708
- """
709
- print_formatted_text("")
710
- buf = event.app.current_buffer
711
- # Abort reverse search/filtering, clear any selection, and reset the buffer.
712
- search.stop_search()
713
- buf.exit_selection()
714
- buf.reset()
715
-
716
- existing_bindings = __xonsh__.shell.shell.prompter.app.key_bindings # noqa: F821 # pyright: ignore[reportUndefinedVariable]
717
- merged_bindings = merge_key_bindings([existing_bindings, custom_bindings])
718
- __xonsh__.shell.shell.prompter.app.key_bindings = merged_bindings # noqa: F821 # pyright: ignore[reportUndefinedVariable]
719
-
720
- log.info("Added custom %s key bindings.", len(merged_bindings.bindings))
721
-
722
-
723
476
  def load_completers():
724
477
  add_one_completer("command_completer", command_completer, "start")
725
478
  add_one_completer("recommended_shell_completer", recommended_shell_completer, "start")