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.
- kash/actions/__init__.py +4 -4
- kash/actions/core/markdownify.py +5 -2
- kash/actions/core/readability.py +5 -2
- kash/actions/core/render_as_html.py +18 -0
- kash/actions/core/webpage_config.py +12 -4
- kash/commands/__init__.py +8 -20
- kash/commands/base/basic_file_commands.py +15 -0
- kash/commands/base/debug_commands.py +13 -0
- kash/commands/base/general_commands.py +21 -16
- kash/commands/base/logs_commands.py +4 -2
- kash/commands/base/model_commands.py +8 -8
- kash/commands/base/search_command.py +3 -2
- kash/commands/base/show_command.py +5 -3
- kash/commands/extras/parse_uv_lock.py +186 -0
- kash/commands/help/doc_commands.py +2 -31
- kash/commands/help/welcome.py +33 -0
- kash/commands/workspace/selection_commands.py +11 -6
- kash/commands/workspace/workspace_commands.py +18 -15
- kash/config/colors.py +2 -0
- kash/config/env_settings.py +14 -1
- kash/config/init.py +2 -2
- kash/config/logger.py +59 -56
- kash/config/logger_basic.py +3 -3
- kash/config/settings.py +116 -57
- kash/config/setup.py +28 -12
- kash/config/text_styles.py +3 -13
- kash/docs/load_api_docs.py +2 -1
- kash/docs/markdown/topics/a3_getting_started.md +3 -2
- kash/{concepts → embeddings}/text_similarity.py +2 -2
- kash/exec/__init__.py +20 -3
- kash/exec/action_decorators.py +18 -4
- kash/exec/action_exec.py +41 -23
- kash/exec/action_registry.py +13 -48
- kash/exec/command_registry.py +2 -1
- kash/exec/fetch_url_metadata.py +4 -6
- kash/exec/importing.py +56 -0
- kash/exec/llm_transforms.py +6 -7
- kash/exec/precondition_registry.py +2 -1
- kash/exec/preconditions.py +16 -1
- kash/exec/shell_callable_action.py +33 -19
- kash/file_storage/file_store.py +23 -10
- kash/file_storage/item_file_format.py +5 -2
- kash/file_storage/metadata_dirs.py +11 -2
- kash/help/assistant.py +1 -1
- kash/help/assistant_instructions.py +2 -1
- kash/help/help_embeddings.py +2 -2
- kash/help/help_printing.py +7 -11
- kash/llm_utils/clean_headings.py +1 -1
- kash/llm_utils/llm_api_keys.py +4 -4
- kash/llm_utils/llm_features.py +68 -0
- kash/llm_utils/llm_messages.py +1 -2
- kash/llm_utils/llm_names.py +1 -1
- kash/llm_utils/llms.py +8 -3
- kash/local_server/__init__.py +5 -2
- kash/local_server/local_server.py +8 -5
- kash/local_server/local_server_commands.py +2 -2
- kash/local_server/local_url_formatters.py +1 -1
- kash/mcp/__init__.py +5 -2
- kash/mcp/mcp_cli.py +5 -5
- kash/mcp/mcp_server_commands.py +5 -5
- kash/mcp/mcp_server_routes.py +5 -5
- kash/mcp/mcp_server_sse.py +4 -2
- kash/media_base/media_cache.py +8 -8
- kash/media_base/media_services.py +1 -1
- kash/media_base/media_tools.py +6 -6
- kash/media_base/services/local_file_media.py +2 -2
- kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -110
- kash/media_base/transcription_format.py +73 -0
- kash/media_base/transcription_whisper.py +38 -0
- kash/model/__init__.py +73 -5
- kash/model/actions_model.py +38 -4
- kash/model/concept_model.py +30 -0
- kash/model/items_model.py +44 -7
- kash/model/params_model.py +24 -0
- kash/shell/completions/completion_scoring.py +37 -5
- kash/shell/output/kerm_codes.py +1 -2
- kash/shell/output/shell_formatting.py +14 -4
- kash/shell/shell_main.py +2 -2
- kash/shell/utils/exception_printing.py +6 -0
- kash/shell/utils/native_utils.py +26 -20
- kash/text_handling/custom_sliding_transforms.py +12 -4
- kash/text_handling/doc_normalization.py +6 -2
- kash/text_handling/markdown_render.py +117 -0
- kash/text_handling/markdown_utils.py +204 -0
- kash/utils/common/import_utils.py +12 -3
- kash/utils/common/type_utils.py +0 -29
- kash/utils/common/url.py +27 -3
- kash/utils/errors.py +6 -0
- kash/utils/file_utils/file_formats.py +2 -2
- kash/utils/file_utils/file_formats_model.py +3 -0
- kash/web_content/dir_store.py +1 -2
- kash/web_content/file_cache_utils.py +37 -10
- kash/web_content/file_processing.py +68 -0
- kash/web_content/local_file_cache.py +12 -9
- kash/web_content/web_extract.py +8 -3
- kash/web_content/web_fetch.py +12 -4
- kash/web_gen/tabbed_webpage.py +5 -2
- kash/web_gen/templates/base_styles.css.jinja +120 -14
- kash/web_gen/templates/base_webpage.html.jinja +60 -13
- kash/web_gen/templates/content_styles.css.jinja +4 -2
- kash/web_gen/templates/item_view.html.jinja +2 -2
- kash/web_gen/templates/tabbed_webpage.html.jinja +1 -2
- kash/workspaces/__init__.py +15 -2
- kash/workspaces/selections.py +18 -3
- kash/workspaces/source_items.py +0 -1
- kash/workspaces/workspaces.py +5 -11
- kash/xonsh_custom/command_nl_utils.py +40 -19
- kash/xonsh_custom/custom_shell.py +43 -11
- kash/xonsh_custom/customize_prompt.py +39 -21
- kash/xonsh_custom/load_into_xonsh.py +22 -25
- kash/xonsh_custom/shell_load_commands.py +2 -2
- kash/xonsh_custom/xonsh_completers.py +2 -249
- kash/xonsh_custom/xonsh_keybindings.py +282 -0
- kash/xonsh_custom/xonsh_modern_tools.py +3 -3
- kash/xontrib/kash_extension.py +5 -6
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/METADATA +8 -6
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/RECORD +122 -123
- kash/concepts/concept_formats.py +0 -23
- kash/shell/clideps/api_keys.py +0 -100
- kash/shell/clideps/dotenv_setup.py +0 -115
- kash/shell/clideps/dotenv_utils.py +0 -98
- kash/shell/clideps/pkg_deps.py +0 -257
- kash/shell/clideps/platforms.py +0 -11
- kash/shell/clideps/terminal_features.py +0 -56
- kash/shell/utils/osc_utils.py +0 -95
- kash/shell/utils/terminal_images.py +0 -133
- kash/text_handling/markdown_util.py +0 -167
- kash/utils/common/atomic_var.py +0 -171
- kash/utils/common/string_replace.py +0 -93
- kash/utils/common/string_template.py +0 -101
- /kash/{concepts → embeddings}/cosine.py +0 -0
- /kash/{concepts → embeddings}/embeddings.py +0 -0
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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.
|
|
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.
|
|
38
|
+
if installed_tools.is_found("eza"):
|
|
39
39
|
if global_settings().use_nerd_icons:
|
|
40
40
|
icons = ["--icons"]
|
|
41
41
|
else:
|
kash/xontrib/kash_extension.py
CHANGED
|
@@ -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 =
|
|
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.
|
|
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
|
|
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.
|
|
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>=
|
|
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
|
|
593
|
-
create a workspace, which is a directory to hold
|
|
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.
|