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
kash/model/actions_model.py
CHANGED
|
@@ -9,12 +9,12 @@ from textwrap import dedent
|
|
|
9
9
|
from typing import Any, TypeVar, cast
|
|
10
10
|
|
|
11
11
|
from chopdiff.docs import DiffFilter
|
|
12
|
-
from chopdiff.docs.token_diffs import DIFF_FILTER_NONE
|
|
13
12
|
from chopdiff.transforms import WINDOW_NONE, WindowSettings
|
|
14
13
|
from flowmark import fill_text
|
|
15
14
|
from prettyfmt import abbrev_obj, fmt_lines
|
|
16
15
|
from pydantic.dataclasses import dataclass, rebuild_dataclass
|
|
17
16
|
from pydantic.json_schema import JsonSchemaValue
|
|
17
|
+
from strif import StringTemplate
|
|
18
18
|
from typing_extensions import override
|
|
19
19
|
|
|
20
20
|
from kash.config.logger import get_logger
|
|
@@ -27,13 +27,15 @@ from kash.model.items_model import UNTITLED, Item, ItemType, State
|
|
|
27
27
|
from kash.model.operations_model import Operation, Source
|
|
28
28
|
from kash.model.params_model import (
|
|
29
29
|
ALL_COMMON_PARAMS,
|
|
30
|
+
COMMON_SHELL_PARAMS,
|
|
31
|
+
RUNTIME_ACTION_PARAMS,
|
|
32
|
+
Param,
|
|
30
33
|
ParamDeclarations,
|
|
31
34
|
TypedParamValues,
|
|
32
35
|
)
|
|
33
36
|
from kash.model.paths_model import StorePath
|
|
34
37
|
from kash.model.preconditions_model import Precondition
|
|
35
38
|
from kash.utils.common.parse_key_vals import format_key_value
|
|
36
|
-
from kash.utils.common.string_template import StringTemplate
|
|
37
39
|
from kash.utils.common.type_utils import not_none
|
|
38
40
|
from kash.utils.errors import InvalidDefinition, InvalidInput
|
|
39
41
|
from kash.workspaces.workspaces import get_ws
|
|
@@ -65,7 +67,8 @@ class ActionInput:
|
|
|
65
67
|
@dataclass(frozen=True)
|
|
66
68
|
class ExecContext:
|
|
67
69
|
"""
|
|
68
|
-
An action and its context for execution.
|
|
70
|
+
An action and its context for execution. This is a good place for settings
|
|
71
|
+
that apply to any action and are bothersome to pass as parameters.
|
|
69
72
|
"""
|
|
70
73
|
|
|
71
74
|
action: Action
|
|
@@ -77,13 +80,33 @@ class ExecContext:
|
|
|
77
80
|
rerun: bool = False
|
|
78
81
|
"""If True, always run actions, even cacheable ones that have results."""
|
|
79
82
|
|
|
83
|
+
refetch: bool = False
|
|
84
|
+
"""If True, will refetch items even if they are already in the content caches."""
|
|
85
|
+
|
|
80
86
|
override_state: State | None = None
|
|
81
87
|
"""If specified, override the state of result items. Useful to mark items as transient."""
|
|
82
88
|
|
|
89
|
+
tmp_output: bool = False
|
|
90
|
+
"""If True, will save output items to a temporary file."""
|
|
91
|
+
|
|
92
|
+
no_format: bool = False
|
|
93
|
+
"""If True, will not normalize the output item's body text formatting (for Markdown)."""
|
|
94
|
+
|
|
83
95
|
@property
|
|
84
96
|
def workspace(self) -> FileStore:
|
|
85
97
|
return get_ws(self.workspace_dir)
|
|
86
98
|
|
|
99
|
+
@property
|
|
100
|
+
def runtime_options(self) -> dict[str, str]:
|
|
101
|
+
"""Return non-default runtime options."""
|
|
102
|
+
opts: dict[str, str] = {}
|
|
103
|
+
# Only these two settings directly affect the output:
|
|
104
|
+
if self.no_format:
|
|
105
|
+
opts["no_format"] = "true"
|
|
106
|
+
if self.override_state:
|
|
107
|
+
opts["override_state"] = self.override_state.name
|
|
108
|
+
return opts
|
|
109
|
+
|
|
87
110
|
def __repr__(self):
|
|
88
111
|
return abbrev_obj(self, field_max_len=80)
|
|
89
112
|
|
|
@@ -175,7 +198,7 @@ class LLMOptions:
|
|
|
175
198
|
system_message: Message = Message("")
|
|
176
199
|
body_template: MessageTemplate = MessageTemplate("{body}")
|
|
177
200
|
windowing: WindowSettings = WINDOW_NONE
|
|
178
|
-
diff_filter: DiffFilter =
|
|
201
|
+
diff_filter: DiffFilter | None = None
|
|
179
202
|
|
|
180
203
|
def updated_with(self, param_name: str, value: Any) -> LLMOptions:
|
|
181
204
|
"""Update option from an action parameter."""
|
|
@@ -409,6 +432,17 @@ class Action(ABC):
|
|
|
409
432
|
# Update corresponding LLM option if appropriate.
|
|
410
433
|
self.llm_options = self.llm_options.updated_with(param_name, value)
|
|
411
434
|
|
|
435
|
+
@property
|
|
436
|
+
def shell_params(self) -> list[Param]:
|
|
437
|
+
"""
|
|
438
|
+
List of parameters that are relevant to shell usage.
|
|
439
|
+
"""
|
|
440
|
+
return (
|
|
441
|
+
list(self.params)
|
|
442
|
+
+ list(RUNTIME_ACTION_PARAMS.values())
|
|
443
|
+
+ list(COMMON_SHELL_PARAMS.values())
|
|
444
|
+
)
|
|
445
|
+
|
|
412
446
|
def param_value_summary(self) -> dict[str, str]:
|
|
413
447
|
"""
|
|
414
448
|
Readable, serializable summary of the action's non-default parameters, to include in
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import NewType
|
|
2
|
+
|
|
3
|
+
from kash.utils.lang_utils.capitalization import capitalize_cms
|
|
4
|
+
|
|
5
|
+
Concept = NewType("Concept", str)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def canonicalize_concept(concept: str, capitalize: bool = True) -> Concept:
|
|
9
|
+
"""
|
|
10
|
+
Convert a concept string (general name, person, etc.) to a canonical form.
|
|
11
|
+
Drop any extraneous Markdown bullets. Drop any quoted phrases (e.g. book titles etc)
|
|
12
|
+
for consistency.
|
|
13
|
+
"""
|
|
14
|
+
concept = concept.strip("-* ")
|
|
15
|
+
for quote in ['"', "'"]:
|
|
16
|
+
if concept.startswith(quote) and concept.endswith(quote):
|
|
17
|
+
concept = concept[1:-1]
|
|
18
|
+
if capitalize:
|
|
19
|
+
return Concept(capitalize_cms(concept))
|
|
20
|
+
else:
|
|
21
|
+
return Concept(concept)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalize_concepts(
|
|
25
|
+
concepts: list[str], sort_dedup: bool = True, capitalize: bool = True
|
|
26
|
+
) -> list[Concept]:
|
|
27
|
+
if sort_dedup:
|
|
28
|
+
return sorted(set(canonicalize_concept(concept, capitalize) for concept in concepts))
|
|
29
|
+
else:
|
|
30
|
+
return [canonicalize_concept(concept, capitalize) for concept in concepts]
|
kash/model/items_model.py
CHANGED
|
@@ -19,12 +19,12 @@ from prettyfmt import (
|
|
|
19
19
|
from pydantic.dataclasses import dataclass
|
|
20
20
|
from strif import abbrev_str, format_iso_timestamp
|
|
21
21
|
|
|
22
|
-
from kash.concepts.concept_formats import canonicalize_concept
|
|
23
22
|
from kash.config.logger import get_logger
|
|
23
|
+
from kash.model.concept_model import canonicalize_concept
|
|
24
24
|
from kash.model.media_model import MediaMetadata
|
|
25
25
|
from kash.model.operations_model import OperationSummary, Source
|
|
26
26
|
from kash.model.paths_model import StorePath, fmt_store_path
|
|
27
|
-
from kash.text_handling.
|
|
27
|
+
from kash.text_handling.markdown_render import markdown_to_html
|
|
28
28
|
from kash.utils.common.format_utils import fmt_loc, html_to_plaintext, plaintext_to_html
|
|
29
29
|
from kash.utils.common.url import Locator, Url
|
|
30
30
|
from kash.utils.errors import FileFormatError
|
|
@@ -33,6 +33,7 @@ from kash.utils.file_utils.file_formats_model import FileExt, Format
|
|
|
33
33
|
|
|
34
34
|
if TYPE_CHECKING:
|
|
35
35
|
from kash.model.actions_model import ExecContext
|
|
36
|
+
from kash.workspaces import Workspace
|
|
36
37
|
|
|
37
38
|
log = get_logger(__name__)
|
|
38
39
|
|
|
@@ -310,7 +311,11 @@ class Item:
|
|
|
310
311
|
key: value for key, value in item_dict.items() if key not in all_fields
|
|
311
312
|
}
|
|
312
313
|
if unexpected_metadata:
|
|
313
|
-
log.info(
|
|
314
|
+
log.info(
|
|
315
|
+
"Skipping unexpected metadata on item: %s%s",
|
|
316
|
+
info_prefix,
|
|
317
|
+
unexpected_metadata,
|
|
318
|
+
)
|
|
314
319
|
|
|
315
320
|
result = cls(
|
|
316
321
|
type=type_,
|
|
@@ -328,7 +333,10 @@ class Item:
|
|
|
328
333
|
|
|
329
334
|
@classmethod
|
|
330
335
|
def from_external_path(
|
|
331
|
-
cls,
|
|
336
|
+
cls,
|
|
337
|
+
path: Path | str,
|
|
338
|
+
item_type: ItemType | None = None,
|
|
339
|
+
title: str | None = None,
|
|
332
340
|
) -> Item:
|
|
333
341
|
"""
|
|
334
342
|
Create a resource Item for a file with a format inferred from the file extension
|
|
@@ -400,6 +408,19 @@ class Item:
|
|
|
400
408
|
if self.type.expects_body and self.format.has_body and not self.body:
|
|
401
409
|
raise ValueError(f"Item type `{self.type.value}` is text but has no body: {self}")
|
|
402
410
|
|
|
411
|
+
def absolute_path(self, ws: "Workspace | None" = None) -> Path: # noqa: UP037
|
|
412
|
+
"""
|
|
413
|
+
Get the absolute path to the item. Throws `ValueError` if the item has no
|
|
414
|
+
store path. If no workspace is provided, uses the current workspace.
|
|
415
|
+
"""
|
|
416
|
+
from kash.workspaces import current_ws
|
|
417
|
+
|
|
418
|
+
if not self.store_path:
|
|
419
|
+
raise ValueError("Item has no store path")
|
|
420
|
+
if not ws:
|
|
421
|
+
ws = current_ws()
|
|
422
|
+
return ws.base_dir / self.store_path
|
|
423
|
+
|
|
403
424
|
@property
|
|
404
425
|
def is_binary(self) -> bool:
|
|
405
426
|
return bool(self.format and self.format.is_binary)
|
|
@@ -541,6 +562,13 @@ class Item:
|
|
|
541
562
|
|
|
542
563
|
return body_text[:max_len]
|
|
543
564
|
|
|
565
|
+
@property
|
|
566
|
+
def has_body(self) -> bool:
|
|
567
|
+
"""
|
|
568
|
+
True if the item has a non-empty body.
|
|
569
|
+
"""
|
|
570
|
+
return bool(self.body and self.body.strip())
|
|
571
|
+
|
|
544
572
|
def slug_name(self, max_len: int = SLUG_MAX_LEN) -> str:
|
|
545
573
|
"""
|
|
546
574
|
Get a readable slugified version of the title or filename or content
|
|
@@ -681,6 +709,10 @@ class Item:
|
|
|
681
709
|
updates = other_updates.copy()
|
|
682
710
|
updates["type"] = type
|
|
683
711
|
|
|
712
|
+
# If format was specified and user didn't specify file_ext, then infer it.
|
|
713
|
+
if "file_ext" not in other_updates and "format" in other_updates:
|
|
714
|
+
updates["file_ext"] = other_updates["format"].file_ext
|
|
715
|
+
|
|
684
716
|
# External resource paths only make sense for resources, so clear them out if new item
|
|
685
717
|
# is not a resource.
|
|
686
718
|
new_type = updates.get("type") or self.type
|
|
@@ -691,15 +723,20 @@ class Item:
|
|
|
691
723
|
if derived_from:
|
|
692
724
|
new_item.update_relations(derived_from=derived_from)
|
|
693
725
|
|
|
694
|
-
# Fall back to action title template if we have it and
|
|
726
|
+
# Fall back to action title template if we have it and title wasn't explicitly set.
|
|
695
727
|
if "title" not in other_updates:
|
|
728
|
+
prev_title = self.title or (Path(self.store_path).stem if self.store_path else UNTITLED)
|
|
696
729
|
if self.context:
|
|
697
730
|
action = self.context.action
|
|
698
731
|
new_item.title = action.title_template.format(
|
|
699
|
-
title=
|
|
732
|
+
title=prev_title, action_name=action.name
|
|
700
733
|
)
|
|
701
734
|
else:
|
|
702
|
-
log.warning(
|
|
735
|
+
log.warning(
|
|
736
|
+
"Deriving an item without action context so keeping previous title: %s",
|
|
737
|
+
self,
|
|
738
|
+
)
|
|
739
|
+
new_item.title = f"{prev_title} (derived copy)"
|
|
703
740
|
|
|
704
741
|
return new_item
|
|
705
742
|
|
kash/model/params_model.py
CHANGED
|
@@ -294,6 +294,18 @@ COMMON_ACTION_PARAMS: dict[str, Param] = {
|
|
|
294
294
|
type=DocSelection,
|
|
295
295
|
default_value=DocSelection.full,
|
|
296
296
|
),
|
|
297
|
+
"s3_bucket": Param(
|
|
298
|
+
"s3_bucket",
|
|
299
|
+
"The S3 bucket to upload to.",
|
|
300
|
+
type=str,
|
|
301
|
+
default_value=None,
|
|
302
|
+
),
|
|
303
|
+
"s3_prefix": Param(
|
|
304
|
+
"s3_prefix",
|
|
305
|
+
"The S3 prefix to upload to (with or without a trailing slash).",
|
|
306
|
+
type=str,
|
|
307
|
+
default_value=None,
|
|
308
|
+
),
|
|
297
309
|
}
|
|
298
310
|
|
|
299
311
|
# Extra parameters that are available when an action is invoked from the shell.
|
|
@@ -305,6 +317,18 @@ RUNTIME_ACTION_PARAMS: dict[str, Param] = {
|
|
|
305
317
|
"it produces an output item that already exists.",
|
|
306
318
|
type=bool,
|
|
307
319
|
),
|
|
320
|
+
"refetch": Param(
|
|
321
|
+
"refetch",
|
|
322
|
+
"Forcing re-fetching of any content, not using media or content caches.",
|
|
323
|
+
type=bool,
|
|
324
|
+
default_value=False,
|
|
325
|
+
),
|
|
326
|
+
"no_format": Param(
|
|
327
|
+
"no_format",
|
|
328
|
+
"Do not auto-format (normalize) Markdown outputs.",
|
|
329
|
+
type=bool,
|
|
330
|
+
default_value=False,
|
|
331
|
+
),
|
|
308
332
|
}
|
|
309
333
|
|
|
310
334
|
|
|
@@ -7,6 +7,7 @@ from datetime import UTC, datetime
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import TypeVar
|
|
9
9
|
|
|
10
|
+
import rich
|
|
10
11
|
from strif import abbrev_str
|
|
11
12
|
from thefuzz import fuzz
|
|
12
13
|
|
|
@@ -22,7 +23,7 @@ log = get_logger(__name__)
|
|
|
22
23
|
T = TypeVar("T")
|
|
23
24
|
|
|
24
25
|
# Scores less than this can be dropped early.
|
|
25
|
-
MIN_CUTOFF = Score(
|
|
26
|
+
MIN_CUTOFF = Score(70)
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def linear_boost(score: Score, min_score: Score) -> Score:
|
|
@@ -171,10 +172,29 @@ def score_phrase(prefix: str, text: str) -> Score:
|
|
|
171
172
|
Could experiment with this more but it's a rough attempt to balance
|
|
172
173
|
full matches and prefix matches.
|
|
173
174
|
"""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
if len(prefix) > 5:
|
|
176
|
+
return Score(
|
|
177
|
+
0.4 * fuzz.ratio(prefix, text)
|
|
178
|
+
+ 0.3 * fuzz.token_set_ratio(prefix, text)
|
|
179
|
+
+ 0.3 * fuzz.partial_ratio(prefix, text),
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
return Score(0.6 * fuzz.ratio(prefix, text) + 0.4 * fuzz.token_set_ratio(prefix, text))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def print_all_scores(prefix: str, text: str):
|
|
186
|
+
rich.inspect(
|
|
187
|
+
{
|
|
188
|
+
"ratio": fuzz.ratio(prefix, text),
|
|
189
|
+
"partial_ratio": fuzz.partial_ratio(prefix, text),
|
|
190
|
+
"token_sort_ratio": fuzz.token_sort_ratio(prefix, text),
|
|
191
|
+
"token_set_ratio": fuzz.token_set_ratio(prefix, text),
|
|
192
|
+
"partial_token_set_ratio": fuzz.partial_token_set_ratio(prefix, text),
|
|
193
|
+
"final": score_phrase(prefix, text),
|
|
194
|
+
},
|
|
195
|
+
methods=False,
|
|
196
|
+
docs=False,
|
|
197
|
+
sort=False,
|
|
178
198
|
)
|
|
179
199
|
|
|
180
200
|
|
|
@@ -281,3 +301,15 @@ def score_snippet(query: str, snippet: CommentedCommand) -> Score:
|
|
|
281
301
|
)
|
|
282
302
|
# Bias a little toward command matches.
|
|
283
303
|
return Score(max(command_score, 0.7 * comment_score))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
## Tests
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_score_phrase():
|
|
310
|
+
assert score_phrase("hello world", "hello world") == Score(100)
|
|
311
|
+
assert Score(90) >= score_phrase("hello world", "hello there world") >= Score(80)
|
|
312
|
+
assert Score(70) >= score_phrase("hello world", "hello there there") >= Score(60)
|
|
313
|
+
assert Score(85) >= score_phrase("duf", "df") >= Score(75)
|
|
314
|
+
assert Score(70) >= score_phrase("wik", "awk") >= Score(60)
|
|
315
|
+
assert Score(60) >= score_phrase("wiki", "awk") >= Score(50)
|
kash/shell/output/kerm_codes.py
CHANGED
|
@@ -72,13 +72,12 @@ from html import escape
|
|
|
72
72
|
from typing import Annotated, Literal, Self, TypeAlias
|
|
73
73
|
from urllib.parse import parse_qs, quote, urlencode, urlparse
|
|
74
74
|
|
|
75
|
+
from clideps.terminal.osc_utils import OscStr, osc8_link, osc8_link_codes, osc8_link_rich, osc_code
|
|
75
76
|
from prompt_toolkit.formatted_text import FormattedText
|
|
76
77
|
from pydantic import BaseModel, Field, TypeAdapter, model_validator
|
|
77
78
|
from rich.style import Style
|
|
78
79
|
from rich.text import Text
|
|
79
80
|
|
|
80
|
-
from kash.shell.utils.osc_utils import OscStr, osc8_link, osc8_link_codes, osc8_link_rich, osc_code
|
|
81
|
-
|
|
82
81
|
KC_VERSION = 0
|
|
83
82
|
"""Version of the Kerm codes format. Update when we make breaking changes."""
|
|
84
83
|
|
|
@@ -8,10 +8,7 @@ from flowmark import Wrap, fill_text
|
|
|
8
8
|
from rich.console import Group
|
|
9
9
|
from rich.text import Text
|
|
10
10
|
|
|
11
|
-
from kash.config.text_styles import
|
|
12
|
-
STYLE_HINT,
|
|
13
|
-
format_success_emoji,
|
|
14
|
-
)
|
|
11
|
+
from kash.config.text_styles import COLOR_FAILURE, COLOR_SUCCESS, STYLE_HINT
|
|
15
12
|
from kash.shell.output.kmarkdown import KMarkdown
|
|
16
13
|
|
|
17
14
|
|
|
@@ -84,6 +81,19 @@ def format_paragraphs(*paragraphs: str | Text | Group) -> Group:
|
|
|
84
81
|
return Group(*text)
|
|
85
82
|
|
|
86
83
|
|
|
84
|
+
EMOJI_TRUE = "✔︎"
|
|
85
|
+
|
|
86
|
+
EMOJI_FALSE = "✘"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def success_emoji(value: bool, success_only: bool = False) -> str:
|
|
90
|
+
return EMOJI_TRUE if value else " " if success_only else EMOJI_FALSE
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def format_success_emoji(value: bool, success_only: bool = False) -> Text:
|
|
94
|
+
return Text(success_emoji(value, success_only), style=COLOR_SUCCESS if value else COLOR_FAILURE)
|
|
95
|
+
|
|
96
|
+
|
|
87
97
|
def format_success(message: str | Text) -> Text:
|
|
88
98
|
return Text.assemble(format_success_emoji(True), message)
|
|
89
99
|
|
kash/shell/shell_main.py
CHANGED
|
@@ -16,12 +16,12 @@ import threading
|
|
|
16
16
|
import xonsh.main
|
|
17
17
|
from strif import quote_if_needed
|
|
18
18
|
|
|
19
|
-
from kash.config.setup import
|
|
19
|
+
from kash.config.setup import kash_setup
|
|
20
20
|
from kash.shell.utils.argparse_utils import WrappedColorFormatter
|
|
21
21
|
from kash.shell.version import get_full_version_name, get_version
|
|
22
22
|
from kash.xonsh_custom.custom_shell import install_to_xonshrc, start_shell
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
kash_setup(rich_logging=True) # Set up logging first.
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
__version__ = get_version()
|
|
@@ -46,5 +46,11 @@ def wrap_with_exception_printing(func: Callable[..., R]) -> Callable[[list[str]]
|
|
|
46
46
|
log.error(f"[{COLOR_ERROR}]Command error:[/{COLOR_ERROR}] %s", summarize_traceback(e))
|
|
47
47
|
log.info("Command error details: %s", e, exc_info=True)
|
|
48
48
|
return None
|
|
49
|
+
except Exception as e:
|
|
50
|
+
# Note xonsh can log exceptions but it will be in the xonsh call stack, which is
|
|
51
|
+
# useless for the user. Better to log all unexpected exception call stack
|
|
52
|
+
# here to our logs.
|
|
53
|
+
log.info("Command error details: %s", e, exc_info=True)
|
|
54
|
+
raise
|
|
49
55
|
|
|
50
56
|
return command
|
kash/shell/utils/native_utils.py
CHANGED
|
@@ -11,15 +11,15 @@ import webbrowser
|
|
|
11
11
|
from enum import Enum
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
+
from clideps.pkgs.pkg_check import pkg_check
|
|
15
|
+
from clideps.pkgs.platform_checks import Platform, get_platform
|
|
16
|
+
from clideps.terminal.terminal_images import terminal_show_image
|
|
14
17
|
from flowmark import Wrap
|
|
15
18
|
from funlog import log_calls
|
|
16
19
|
|
|
17
20
|
from kash.config.logger import get_logger
|
|
18
|
-
from kash.config.text_styles import BAT_STYLE, BAT_THEME, COLOR_ERROR
|
|
19
|
-
from kash.shell.clideps.pkg_deps import Pkg, pkg_check
|
|
20
|
-
from kash.shell.clideps.platforms import PLATFORM, Platform
|
|
21
|
+
from kash.config.text_styles import BAT_STYLE, BAT_STYLE_PLAIN, BAT_THEME, COLOR_ERROR
|
|
21
22
|
from kash.shell.output.shell_output import cprint
|
|
22
|
-
from kash.shell.utils.terminal_images import terminal_show_image
|
|
23
23
|
from kash.utils.common.format_utils import fmt_loc
|
|
24
24
|
from kash.utils.common.url import as_file_url, is_file_url, is_url
|
|
25
25
|
from kash.utils.errors import FileNotFound, SetupError
|
|
@@ -49,11 +49,11 @@ def file_size_check(
|
|
|
49
49
|
def native_open(filename: str | Path):
|
|
50
50
|
filename = str(filename)
|
|
51
51
|
log.message("Opening file: %s", filename)
|
|
52
|
-
if
|
|
52
|
+
if get_platform() == Platform.Darwin:
|
|
53
53
|
subprocess.run(["open", filename])
|
|
54
|
-
elif
|
|
54
|
+
elif get_platform() == Platform.Linux:
|
|
55
55
|
subprocess.run(["xdg-open", filename])
|
|
56
|
-
elif
|
|
56
|
+
elif get_platform() == Platform.Windows:
|
|
57
57
|
subprocess.run(["start", shlex.quote(filename)], shell=True)
|
|
58
58
|
else:
|
|
59
59
|
raise NotImplementedError("Unsupported platform")
|
|
@@ -110,12 +110,14 @@ def _detect_view_mode(file_or_url: str) -> ViewMode:
|
|
|
110
110
|
def view_file_native(
|
|
111
111
|
file_or_url: str | Path,
|
|
112
112
|
view_mode: ViewMode = ViewMode.auto,
|
|
113
|
+
plain: bool = False,
|
|
113
114
|
):
|
|
114
115
|
"""
|
|
115
116
|
Open a file or URL in the console or a native app. If `view_mode` is auto,
|
|
116
117
|
automatically determine whether to use console, web browser, or the user's
|
|
117
118
|
preferred native application. For images, also tries terminal-based image
|
|
118
|
-
display.
|
|
119
|
+
display. The `--plain` flag will disable line numbers, grid, etc. in `bat`
|
|
120
|
+
and force `ViewMode.console`.
|
|
119
121
|
"""
|
|
120
122
|
file_or_url = str(file_or_url)
|
|
121
123
|
path = None
|
|
@@ -124,6 +126,9 @@ def view_file_native(
|
|
|
124
126
|
if not path.exists():
|
|
125
127
|
raise FileNotFound(fmt_loc(path))
|
|
126
128
|
|
|
129
|
+
if plain:
|
|
130
|
+
view_mode = ViewMode.console
|
|
131
|
+
|
|
127
132
|
if view_mode == ViewMode.auto:
|
|
128
133
|
view_mode = _detect_view_mode(file_or_url)
|
|
129
134
|
|
|
@@ -133,7 +138,7 @@ def view_file_native(
|
|
|
133
138
|
webbrowser.open(url)
|
|
134
139
|
elif view_mode == ViewMode.console and path:
|
|
135
140
|
file_size, min_lines = file_size_check(path)
|
|
136
|
-
view_file_console(path, use_pager=min_lines > 40 or file_size > 20 * 1024)
|
|
141
|
+
view_file_console(path, use_pager=min_lines > 40 or file_size > 20 * 1024, plain=plain)
|
|
137
142
|
elif view_mode == ViewMode.terminal_image and path:
|
|
138
143
|
try:
|
|
139
144
|
terminal_show_image(path)
|
|
@@ -187,11 +192,11 @@ def tail_file(
|
|
|
187
192
|
if follow:
|
|
188
193
|
max_lines = follow_max_lines
|
|
189
194
|
|
|
190
|
-
pkg_check().require(
|
|
191
|
-
pkg_check().warn_if_missing(
|
|
195
|
+
pkg_check().require("tail")
|
|
196
|
+
pkg_check().warn_if_missing("bat")
|
|
192
197
|
|
|
193
198
|
if follow:
|
|
194
|
-
if pkg_check().
|
|
199
|
+
if pkg_check().is_found("bat"):
|
|
195
200
|
# Follow the file in real-time.
|
|
196
201
|
command = (
|
|
197
202
|
f"tail -{max_lines} -f {all_paths_str} | "
|
|
@@ -202,8 +207,8 @@ def tail_file(
|
|
|
202
207
|
command = f"tail -f {all_paths_str}"
|
|
203
208
|
cprint("Following file: `%s`", command, text_wrap=Wrap.NONE)
|
|
204
209
|
else:
|
|
205
|
-
pkg_check().require(
|
|
206
|
-
if pkg_check().
|
|
210
|
+
pkg_check().require("less")
|
|
211
|
+
if pkg_check().is_found("bat"):
|
|
207
212
|
command = (
|
|
208
213
|
f"tail -{max_lines} {all_paths_str} | "
|
|
209
214
|
f"bat --paging=never --color=always --style=plain --theme={BAT_THEME} -l log | "
|
|
@@ -216,7 +221,7 @@ def tail_file(
|
|
|
216
221
|
subprocess.run(command, shell=True, check=True)
|
|
217
222
|
|
|
218
223
|
|
|
219
|
-
def view_file_console(filename: str | Path, use_pager: bool = True):
|
|
224
|
+
def view_file_console(filename: str | Path, use_pager: bool = True, plain: bool = False):
|
|
220
225
|
"""
|
|
221
226
|
Displays a file in the console with pagination and syntax highlighting.
|
|
222
227
|
"""
|
|
@@ -226,18 +231,19 @@ def view_file_console(filename: str | Path, use_pager: bool = True):
|
|
|
226
231
|
# TODO: Visualize YAML frontmatter with different syntax/style than Markdown content.
|
|
227
232
|
|
|
228
233
|
is_text = file_format_info(filename).is_text
|
|
234
|
+
bat_style = BAT_STYLE_PLAIN if plain else BAT_STYLE
|
|
229
235
|
if is_text:
|
|
230
|
-
pkg_check().require(
|
|
231
|
-
if pkg_check().
|
|
236
|
+
pkg_check().require("less")
|
|
237
|
+
if pkg_check().is_found("bat"):
|
|
232
238
|
pager_str = "--pager=always --pager=less " if use_pager else ""
|
|
233
|
-
command = f"bat {pager_str}--color=always --style={
|
|
239
|
+
command = f"bat {pager_str}--color=always --style={bat_style} --theme={BAT_THEME} {quoted_filename}"
|
|
234
240
|
else:
|
|
235
|
-
pkg_check().require(
|
|
241
|
+
pkg_check().require("pygmentize")
|
|
236
242
|
command = f"pygmentize -g {quoted_filename}"
|
|
237
243
|
if use_pager:
|
|
238
244
|
command = f"{command} | less -R"
|
|
239
245
|
else:
|
|
240
|
-
pkg_check().require(
|
|
246
|
+
pkg_check().require("hexyl")
|
|
241
247
|
command = f"hexyl {quoted_filename}"
|
|
242
248
|
if use_pager:
|
|
243
249
|
command = f"{command} | less -R"
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from math import ceil
|
|
3
3
|
|
|
4
|
-
from chopdiff.docs import
|
|
4
|
+
from chopdiff.docs import (
|
|
5
|
+
DIFF_FILTER_NONE,
|
|
6
|
+
DiffFilter,
|
|
7
|
+
Paragraph,
|
|
8
|
+
TextDoc,
|
|
9
|
+
TextUnit,
|
|
10
|
+
diff_docs,
|
|
11
|
+
join_wordtoks,
|
|
12
|
+
)
|
|
5
13
|
from chopdiff.transforms import (
|
|
6
14
|
WindowSettings,
|
|
7
|
-
accept_all,
|
|
8
15
|
remove_window_br,
|
|
9
16
|
sliding_para_window,
|
|
10
17
|
sliding_window_transform,
|
|
@@ -31,7 +38,7 @@ def filtered_transform(
|
|
|
31
38
|
doc: TextDoc,
|
|
32
39
|
transform_func: TextDocTransform,
|
|
33
40
|
windowing: WindowSettings | None,
|
|
34
|
-
diff_filter: DiffFilter =
|
|
41
|
+
diff_filter: DiffFilter | None = None,
|
|
35
42
|
) -> TextDoc:
|
|
36
43
|
"""
|
|
37
44
|
Apply a transform with sliding window across the input doc, enforcing the changes it's
|
|
@@ -39,7 +46,7 @@ def filtered_transform(
|
|
|
39
46
|
|
|
40
47
|
If windowing is None, apply the transform to the entire document at once.
|
|
41
48
|
"""
|
|
42
|
-
has_filter = diff_filter !=
|
|
49
|
+
has_filter = bool(diff_filter and diff_filter != DIFF_FILTER_NONE)
|
|
43
50
|
|
|
44
51
|
if not windowing or not windowing.size:
|
|
45
52
|
transformed_doc = transform_func(doc)
|
|
@@ -52,6 +59,7 @@ def filtered_transform(
|
|
|
52
59
|
transformed_doc = transform_func(input_doc)
|
|
53
60
|
|
|
54
61
|
if has_filter:
|
|
62
|
+
assert diff_filter
|
|
55
63
|
# Check the transform did what it should have.
|
|
56
64
|
diff = diff_docs(input_doc, transformed_doc)
|
|
57
65
|
accepted_diff, rejected_diff = diff.filter(diff_filter)
|
|
@@ -21,7 +21,11 @@ def normalize_formatting_ansi(text: str, format: Format | None, width=DEFAULT_WR
|
|
|
21
21
|
text, width=width, word_splitter=simple_word_splitter, len_fn=ansi_cell_len
|
|
22
22
|
)
|
|
23
23
|
elif format == Format.markdown or format == Format.md_html:
|
|
24
|
-
return fill_markdown(
|
|
24
|
+
return fill_markdown(
|
|
25
|
+
text,
|
|
26
|
+
line_wrapper=line_wrap_by_sentence(len_fn=ansi_cell_len),
|
|
27
|
+
cleanups=True, # Safe cleanups like unbolding section headers.
|
|
28
|
+
)
|
|
25
29
|
elif format == Format.html:
|
|
26
30
|
# We don't currently auto-format HTML as we sometimes use HTML with specifically chosen line breaks.
|
|
27
31
|
return text
|
|
@@ -52,7 +56,7 @@ def normalize_text_file(
|
|
|
52
56
|
|
|
53
57
|
|
|
54
58
|
def test_osc8_link():
|
|
55
|
-
from
|
|
59
|
+
from clideps.terminal.osc_utils import osc8_link
|
|
56
60
|
|
|
57
61
|
link = osc8_link("https://example.com/" + "x" * 50, "Example")
|
|
58
62
|
assert ansi_cell_len(link) == 7
|