kash-shell 0.3.9__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 (135) 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 +13 -0
  9. kash/commands/base/general_commands.py +21 -16
  10. kash/commands/base/logs_commands.py +4 -2
  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 +18 -15
  19. kash/config/colors.py +2 -0
  20. kash/config/env_settings.py +14 -1
  21. kash/config/init.py +2 -2
  22. kash/config/logger.py +59 -56
  23. kash/config/logger_basic.py +3 -3
  24. kash/config/settings.py +116 -57
  25. kash/config/setup.py +28 -12
  26. kash/config/text_styles.py +3 -13
  27. kash/docs/load_api_docs.py +2 -1
  28. kash/docs/markdown/topics/a3_getting_started.md +3 -2
  29. kash/{concepts → embeddings}/text_similarity.py +2 -2
  30. kash/exec/__init__.py +20 -3
  31. kash/exec/action_decorators.py +18 -4
  32. kash/exec/action_exec.py +41 -23
  33. kash/exec/action_registry.py +13 -48
  34. kash/exec/command_registry.py +2 -1
  35. kash/exec/fetch_url_metadata.py +4 -6
  36. kash/exec/importing.py +56 -0
  37. kash/exec/llm_transforms.py +6 -7
  38. kash/exec/precondition_registry.py +2 -1
  39. kash/exec/preconditions.py +16 -1
  40. kash/exec/shell_callable_action.py +33 -19
  41. kash/file_storage/file_store.py +23 -10
  42. kash/file_storage/item_file_format.py +5 -2
  43. kash/file_storage/metadata_dirs.py +11 -2
  44. kash/help/assistant.py +1 -1
  45. kash/help/assistant_instructions.py +2 -1
  46. kash/help/help_embeddings.py +2 -2
  47. kash/help/help_printing.py +7 -11
  48. kash/llm_utils/clean_headings.py +1 -1
  49. kash/llm_utils/llm_api_keys.py +4 -4
  50. kash/llm_utils/llm_features.py +68 -0
  51. kash/llm_utils/llm_messages.py +1 -2
  52. kash/llm_utils/llm_names.py +1 -1
  53. kash/llm_utils/llms.py +8 -3
  54. kash/local_server/__init__.py +5 -2
  55. kash/local_server/local_server.py +8 -5
  56. kash/local_server/local_server_commands.py +2 -2
  57. kash/local_server/local_url_formatters.py +1 -1
  58. kash/mcp/__init__.py +5 -2
  59. kash/mcp/mcp_cli.py +5 -5
  60. kash/mcp/mcp_server_commands.py +5 -5
  61. kash/mcp/mcp_server_routes.py +5 -5
  62. kash/mcp/mcp_server_sse.py +4 -2
  63. kash/media_base/media_cache.py +8 -8
  64. kash/media_base/media_services.py +1 -1
  65. kash/media_base/media_tools.py +6 -6
  66. kash/media_base/services/local_file_media.py +2 -2
  67. kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -110
  68. kash/media_base/transcription_format.py +73 -0
  69. kash/media_base/transcription_whisper.py +38 -0
  70. kash/model/__init__.py +73 -5
  71. kash/model/actions_model.py +38 -4
  72. kash/model/concept_model.py +30 -0
  73. kash/model/items_model.py +44 -7
  74. kash/model/params_model.py +24 -0
  75. kash/shell/completions/completion_scoring.py +37 -5
  76. kash/shell/output/kerm_codes.py +1 -2
  77. kash/shell/output/shell_formatting.py +14 -4
  78. kash/shell/shell_main.py +2 -2
  79. kash/shell/utils/exception_printing.py +6 -0
  80. kash/shell/utils/native_utils.py +26 -20
  81. kash/text_handling/custom_sliding_transforms.py +12 -4
  82. kash/text_handling/doc_normalization.py +6 -2
  83. kash/text_handling/markdown_render.py +117 -0
  84. kash/text_handling/markdown_utils.py +204 -0
  85. kash/utils/common/import_utils.py +12 -3
  86. kash/utils/common/type_utils.py +0 -29
  87. kash/utils/common/url.py +27 -3
  88. kash/utils/errors.py +6 -0
  89. kash/utils/file_utils/file_formats.py +2 -2
  90. kash/utils/file_utils/file_formats_model.py +3 -0
  91. kash/web_content/dir_store.py +1 -2
  92. kash/web_content/file_cache_utils.py +37 -10
  93. kash/web_content/file_processing.py +68 -0
  94. kash/web_content/local_file_cache.py +12 -9
  95. kash/web_content/web_extract.py +8 -3
  96. kash/web_content/web_fetch.py +12 -4
  97. kash/web_gen/tabbed_webpage.py +5 -2
  98. kash/web_gen/templates/base_styles.css.jinja +120 -14
  99. kash/web_gen/templates/base_webpage.html.jinja +60 -13
  100. kash/web_gen/templates/content_styles.css.jinja +4 -2
  101. kash/web_gen/templates/item_view.html.jinja +2 -2
  102. kash/web_gen/templates/tabbed_webpage.html.jinja +1 -2
  103. kash/workspaces/__init__.py +15 -2
  104. kash/workspaces/selections.py +18 -3
  105. kash/workspaces/source_items.py +0 -1
  106. kash/workspaces/workspaces.py +5 -11
  107. kash/xonsh_custom/command_nl_utils.py +40 -19
  108. kash/xonsh_custom/custom_shell.py +43 -11
  109. kash/xonsh_custom/customize_prompt.py +39 -21
  110. kash/xonsh_custom/load_into_xonsh.py +22 -25
  111. kash/xonsh_custom/shell_load_commands.py +2 -2
  112. kash/xonsh_custom/xonsh_completers.py +2 -249
  113. kash/xonsh_custom/xonsh_keybindings.py +282 -0
  114. kash/xonsh_custom/xonsh_modern_tools.py +3 -3
  115. kash/xontrib/kash_extension.py +5 -6
  116. {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/METADATA +8 -6
  117. {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/RECORD +122 -123
  118. kash/concepts/concept_formats.py +0 -23
  119. kash/shell/clideps/api_keys.py +0 -100
  120. kash/shell/clideps/dotenv_setup.py +0 -115
  121. kash/shell/clideps/dotenv_utils.py +0 -98
  122. kash/shell/clideps/pkg_deps.py +0 -257
  123. kash/shell/clideps/platforms.py +0 -11
  124. kash/shell/clideps/terminal_features.py +0 -56
  125. kash/shell/utils/osc_utils.py +0 -95
  126. kash/shell/utils/terminal_images.py +0 -133
  127. kash/text_handling/markdown_util.py +0 -167
  128. kash/utils/common/atomic_var.py +0 -171
  129. kash/utils/common/string_replace.py +0 -93
  130. kash/utils/common/string_template.py +0 -101
  131. /kash/{concepts → embeddings}/cosine.py +0 -0
  132. /kash/{concepts → embeddings}/embeddings.py +0 -0
  133. {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/WHEEL +0 -0
  134. {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/entry_points.txt +0 -0
  135. {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/licenses/LICENSE +0 -0
@@ -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")
@@ -0,0 +1,282 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+
4
+ from prompt_toolkit import search
5
+ from prompt_toolkit.application import get_app
6
+ from prompt_toolkit.filters import Condition
7
+ from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings
8
+ from prompt_toolkit.key_binding.key_processor import KeyPressEvent
9
+ from prompt_toolkit.shortcuts import print_formatted_text
10
+ from strif import AtomicVar
11
+ from xonsh.parsers.completion_context import CommandContext
12
+
13
+ from kash.actions.core.assistant_chat import assistant_chat
14
+ from kash.config.logger import get_logger
15
+ from kash.shell.completions.completion_types import ScoredCompletion
16
+ from kash.shell.completions.shell_completions import (
17
+ trace_completions,
18
+ )
19
+ from kash.shell.ui.shell_syntax import assist_request_str
20
+ from kash.xonsh_custom.shell_which import is_valid_command
21
+
22
+ log = get_logger(__name__)
23
+
24
+
25
+ @dataclass
26
+ class MultiTabState:
27
+ last_context: CommandContext | None = None
28
+ first_results_shown: bool = False
29
+ more_results_requested: bool = False
30
+ more_completions: set[ScoredCompletion] | None = None
31
+
32
+ def reset_first_results(self, context: CommandContext):
33
+ self.last_context = context
34
+ self.first_results_shown = True
35
+ self.more_results_requested = False
36
+ self.more_completions = None
37
+
38
+ def could_show_more(self) -> bool:
39
+ return self.first_results_shown and not self.more_results_requested
40
+
41
+
42
+ # Maintain state for help completions for single and double tab.
43
+ # TODO: Move this into the CompletionContext.
44
+ _MULTI_TAB_STATE = AtomicVar(MultiTabState())
45
+
46
+
47
+ @Condition
48
+ def is_unquoted_assist_request():
49
+ app = get_app()
50
+ buf = app.current_buffer
51
+ text = buf.text.strip()
52
+ is_default_buffer = buf.name == "DEFAULT_BUFFER"
53
+ has_prefix = text.startswith("?") and not (text.startswith('? "') or text.startswith("? '"))
54
+ return is_default_buffer and has_prefix
55
+
56
+
57
+ _command_regex = re.compile(r"^[a-zA-Z0-9_-]+$")
58
+
59
+ _python_keyword_regex = re.compile(
60
+ r"assert|async|await|break|class|continue|def|del|elif|else|except|finally|"
61
+ r"for|from|global|if|import|lambda|nonlocal|pass|raise|return|try|while|with|yield"
62
+ )
63
+
64
+
65
+ def _extract_command_name(text: str) -> str | None:
66
+ text = text.split()[0]
67
+ if _python_keyword_regex.match(text):
68
+ return None
69
+ if _command_regex.match(text):
70
+ return text
71
+ return None
72
+
73
+
74
+ @Condition
75
+ def whitespace_only() -> bool:
76
+ app = get_app()
77
+ buf = app.current_buffer
78
+ return not buf.text.strip()
79
+
80
+
81
+ @Condition
82
+ def is_typo_command() -> bool:
83
+ """
84
+ Is the command itself invalid? Should be conservative, so we can suppress
85
+ executing it if it is definitely a typo.
86
+ """
87
+
88
+ app = get_app()
89
+ buf = app.current_buffer
90
+ text = buf.text.strip()
91
+
92
+ is_default_buffer = buf.name == "DEFAULT_BUFFER"
93
+ if not is_default_buffer:
94
+ return False
95
+
96
+ # Assistant NL requests always allowed.
97
+ has_assistant_prefix = text.startswith("?") or text.rstrip().endswith("?")
98
+ if has_assistant_prefix:
99
+ return False
100
+
101
+ # Anything more complex is probably Python.
102
+ # TODO: Do a better syntax parse of this as Python, or use xonsh's algorithm.
103
+ for s in ["\n", "(", ")"]:
104
+ if s in text:
105
+ return False
106
+
107
+ # Empty command line allowed.
108
+ if not text:
109
+ return False
110
+
111
+ # Now look at the command.
112
+ command_name = _extract_command_name(text)
113
+
114
+ # Python or missing command is fine.
115
+ if not command_name:
116
+ return False
117
+
118
+ # Recognized command.
119
+ if is_valid_command(command_name):
120
+ return False
121
+
122
+ # Okay it's almost certainly a command typo.
123
+ return True
124
+
125
+
126
+ @Condition
127
+ def is_completion_menu_active() -> bool:
128
+ app = get_app()
129
+ return app.current_buffer.complete_state is not None
130
+
131
+
132
+ @Condition
133
+ def could_show_more_tab_completions() -> bool:
134
+ return _MULTI_TAB_STATE.value.could_show_more()
135
+
136
+
137
+ # Set up prompt_toolkit key bindings.
138
+ def add_key_bindings() -> None:
139
+ custom_bindings = KeyBindings()
140
+
141
+ # Need to be careful only to bind with a filter if the state is suitable.
142
+ # Only add more completions if we've seen some results but user hasn't pressed
143
+ # tab a second time yet. Otherwise the behavior should fall back to usual ptk
144
+ # tab behavior (selecting each completion one by one).
145
+ @custom_bindings.add("tab", filter=could_show_more_tab_completions)
146
+ def _(event: KeyPressEvent):
147
+ """
148
+ Add a second tab to show more completions.
149
+ """
150
+ with _MULTI_TAB_STATE.updates() as state:
151
+ state.more_results_requested = True
152
+
153
+ trace_completions("More completion results requested", state)
154
+
155
+ # Restart completions.
156
+ buf = event.app.current_buffer
157
+ buf.complete_state = None
158
+ buf.start_completion()
159
+
160
+ @custom_bindings.add("s-tab")
161
+ def _(event: KeyPressEvent):
162
+ with _MULTI_TAB_STATE.updates() as state:
163
+ state.more_results_requested = True
164
+
165
+ trace_completions("More completion results requested", state)
166
+
167
+ # Restart completions.
168
+ buf = event.app.current_buffer
169
+ buf.complete_state = None
170
+ buf.start_completion()
171
+
172
+ @custom_bindings.add(" ", filter=whitespace_only)
173
+ def _(event: KeyPressEvent):
174
+ """
175
+ Map space at the start of the line to `? ` to invoke an assistant question.
176
+ """
177
+ buf = event.app.current_buffer
178
+ if buf.text == " " or buf.text == "":
179
+ buf.delete_before_cursor(len(buf.text))
180
+ buf.insert_text("? ")
181
+ else:
182
+ buf.insert_text(" ")
183
+
184
+ @custom_bindings.add(" ", filter=is_typo_command)
185
+ def _(event: KeyPressEvent):
186
+ """
187
+ If the user types two words and the first word is likely an invalid
188
+ command, jump back to prefix the whole line with `? ` to make it clear we're
189
+ in natural language mode.
190
+ """
191
+
192
+ buf = event.app.current_buffer
193
+ text = buf.text.strip()
194
+
195
+ if (
196
+ buf.cursor_position == len(buf.text)
197
+ and len(text.split()) >= 2
198
+ and not text.startswith("?")
199
+ ):
200
+ buf.transform_current_line(lambda line: "? " + line)
201
+ buf.cursor_position += 2
202
+
203
+ buf.insert_text(" ")
204
+
205
+ @custom_bindings.add("enter", filter=whitespace_only)
206
+ def _(_event: KeyPressEvent):
207
+ """
208
+ Suppress enter if the command line is empty, but add a newline above the prompt.
209
+ """
210
+ print_formatted_text("")
211
+
212
+ @custom_bindings.add("enter", filter=is_unquoted_assist_request)
213
+ def _(event: KeyPressEvent):
214
+ """
215
+ Automatically add quotes around assistant questions, so there are not
216
+ syntax errors if the command line contains unclosed quotes etc.
217
+ """
218
+
219
+ buf = event.app.current_buffer
220
+ text = buf.text.strip()
221
+
222
+ question_text = text[1:].strip()
223
+ if not question_text:
224
+ # If the user enters an empty assistant request, treat it as a shortcut to go to the assistant chat.
225
+ buf.delete_before_cursor(len(buf.text))
226
+ buf.insert_text(assistant_chat.__name__)
227
+ else:
228
+ # Convert it to an assistant question starting with a `?`.
229
+ buf.delete_before_cursor(len(buf.text))
230
+ buf.insert_text(assist_request_str(question_text))
231
+
232
+ buf.validate_and_handle()
233
+
234
+ @custom_bindings.add("enter", filter=is_typo_command)
235
+ def _(event: KeyPressEvent):
236
+ """
237
+ Suppress enter and if possible give completions if the command is just not a valid command.
238
+ """
239
+
240
+ buf = event.app.current_buffer
241
+ buf.start_completion()
242
+
243
+ # TODO: Also suppress enter if a command or action doesn't meet the required args,
244
+ # selection, or preconditions.
245
+ # Perhaps also have a way to get confirmation if its a rarely used or unexpected command
246
+ # (based on history/suggestions).
247
+ # TODO: Add suggested replacements, e.g. df -> duf, top -> btm, etc.
248
+
249
+ @custom_bindings.add("@")
250
+ def _(event: KeyPressEvent):
251
+ """
252
+ Auto-trigger item completions after `@` sign.
253
+ """
254
+ buf = event.app.current_buffer
255
+ buf.insert_text("@")
256
+ buf.start_completion()
257
+
258
+ @custom_bindings.add("escape", eager=True, filter=is_completion_menu_active)
259
+ def _(event: KeyPressEvent):
260
+ """
261
+ Close the completion menu when escape is pressed.
262
+ """
263
+ event.app.current_buffer.cancel_completion()
264
+
265
+ @custom_bindings.add("c-c", eager=True)
266
+ def _(event):
267
+ """
268
+ Control-C to reset the current buffer. Similar to usual behavior but doesn't
269
+ leave ugly prompt chars.
270
+ """
271
+ print_formatted_text("")
272
+ buf = event.app.current_buffer
273
+ # Abort reverse search/filtering, clear any selection, and reset the buffer.
274
+ search.stop_search()
275
+ buf.exit_selection()
276
+ buf.reset()
277
+
278
+ existing_bindings = __xonsh__.shell.shell.prompter.app.key_bindings # noqa: F821 # pyright: ignore[reportUndefinedVariable]
279
+ merged_bindings = merge_key_bindings([existing_bindings, custom_bindings])
280
+ __xonsh__.shell.shell.prompter.app.key_bindings = merged_bindings # noqa: F821 # pyright: ignore[reportUndefinedVariable]
281
+
282
+ log.info("Added custom %s key bindings.", len(merged_bindings.bindings))
@@ -1,10 +1,10 @@
1
1
  import subprocess
2
2
 
3
+ from clideps.pkgs.pkg_check import pkg_check
3
4
  from xonsh.built_ins import XSH
4
5
  from xonsh.xontribs import xontribs_load
5
6
 
6
7
  from kash.config.settings import global_settings
7
- from kash.shell.clideps.pkg_deps import Pkg, pkg_check
8
8
 
9
9
 
10
10
  def modernize_shell() -> None:
@@ -25,7 +25,7 @@ def add_fnm() -> None:
25
25
  def enable_zoxide() -> None:
26
26
  installed_tools = pkg_check()
27
27
 
28
- if installed_tools.has(Pkg.zoxide):
28
+ if installed_tools.is_found("zoxide"):
29
29
  assert XSH.builtins
30
30
  zoxide_init = subprocess.check_output(["zoxide", "init", "xonsh"]).decode()
31
31
  XSH.builtins.execx(zoxide_init, "exec", XSH.ctx, filename="zoxide")
@@ -35,7 +35,7 @@ def add_aliases() -> None:
35
35
  installed_tools = pkg_check()
36
36
 
37
37
  assert XSH.aliases
38
- if installed_tools.has(Pkg.eza):
38
+ if installed_tools.is_found("eza"):
39
39
  if global_settings().use_nerd_icons:
40
40
  icons = ["--icons"]
41
41
  else:
@@ -9,15 +9,10 @@ Can run from the custom kash shell (main.py) or from a regular xonsh shell.
9
9
  """
10
10
 
11
11
  # Using absolute imports to avoid polluting the user's shell namespace.
12
- import kash.actions
13
12
  import kash.exec.command_registry
14
- import kash.shell.output.shell_output
15
- import kash.utils.common.format_utils
16
13
  import kash.xonsh_custom.load_into_xonsh
17
- import kash.xonsh_custom.shell_load_commands
18
14
  import kash.xonsh_custom.xonsh_env
19
15
  from kash.config.logger import get_logger
20
- from kash.exec.action_registry import reload_all_action_classes
21
16
 
22
17
 
23
18
  # We add action loading here directly in the xontrib so we expose `load` and
@@ -34,6 +29,10 @@ def load(*paths: str) -> None:
34
29
 
35
30
  from prettyfmt import fmt_path
36
31
 
32
+ import kash.shell.output.shell_output
33
+ import kash.xonsh_custom.shell_load_commands
34
+ from kash.exec.action_registry import refresh_action_classes
35
+
37
36
  for path in paths:
38
37
  if os.path.isfile(path) and path.endswith(".py"):
39
38
  runpy.run_path(path, run_name="__main__")
@@ -41,7 +40,7 @@ def load(*paths: str) -> None:
41
40
  importlib.import_module(path)
42
41
 
43
42
  # Now reload all actions into the environment so the new action is visible.
44
- actions = reload_all_action_classes()
43
+ actions = refresh_action_classes()
45
44
  kash.xonsh_custom.shell_load_commands._register_actions_in_shell(actions)
46
45
 
47
46
  kash.shell.output.shell_output.cprint(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kash-shell
3
- Version: 0.3.9
3
+ Version: 0.3.10
4
4
  Summary: The knowledge agent shell (core)
5
5
  Project-URL: Repository, https://github.com/jlevy/kash-shell
6
6
  Author-email: Joshua Levy <joshua@cal.berkeley.edu>
@@ -19,13 +19,14 @@ Requires-Python: <4.0,>=3.11
19
19
  Requires-Dist: anyio>=4.8.0
20
20
  Requires-Dist: audioop-lts>=0.2.1; python_version >= '3.13'
21
21
  Requires-Dist: cachetools>=5.5.2
22
- Requires-Dist: chopdiff>=0.1.3
22
+ Requires-Dist: chopdiff>=0.2.1
23
+ Requires-Dist: clideps>=0.1.1
23
24
  Requires-Dist: colour>=0.1.5
24
25
  Requires-Dist: cssselect>=1.2.0
25
26
  Requires-Dist: deepgram-sdk>=3.10.1
26
27
  Requires-Dist: dunamai>=1.23.0
27
28
  Requires-Dist: fastapi>=0.115.11
28
- Requires-Dist: flowmark>=0.3.1
29
+ Requires-Dist: flowmark>=0.4.1
29
30
  Requires-Dist: frontmatter-format>=0.2.1
30
31
  Requires-Dist: funlog>=0.2.0
31
32
  Requires-Dist: humanfriendly>=10.0
@@ -58,7 +59,7 @@ Requires-Dist: rich>=14.0.0
58
59
  Requires-Dist: ripgrepy>=2.1.0
59
60
  Requires-Dist: send2trash>=1.8.3
60
61
  Requires-Dist: setproctitle>=1.3.5
61
- Requires-Dist: strif>=2.1.0
62
+ Requires-Dist: strif>=3.0.0-rc.1
62
63
  Requires-Dist: tenacity>=9.0.0
63
64
  Requires-Dist: thefuzz>=0.22.1
64
65
  Requires-Dist: tiktoken>=0.9.0
@@ -589,8 +590,9 @@ A few of the most important commands for managing files and work are these:
589
590
  browser to view it.
590
591
 
591
592
  - `workspace` shows or selects or creates a new workspace.
592
- Initially you work in the `global` workspace but for more real work you'll want to
593
- create a workspace, which is a directory to hold the files you are working with.
593
+ Initially you work in the default global workspace (typically at `~/Kash/workspace`)
594
+ but for more real work you'll want to create a workspace, which is a directory to hold
595
+ the files you are working with.
594
596
 
595
597
  - `select` shows or sets selections, which are the set of files the next command will
596
598
  run on, within the current workspace.