kash-shell 0.3.9__py3-none-any.whl → 0.3.11__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/format_markdown_template.py +2 -5
- kash/actions/core/markdownify.py +7 -6
- kash/actions/core/readability.py +7 -6
- kash/actions/core/render_as_html.py +37 -0
- kash/actions/core/show_webpage.py +6 -11
- kash/actions/core/strip_html.py +2 -6
- kash/actions/core/tabbed_webpage_config.py +31 -0
- kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -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/files_command.py +28 -10
- 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 +19 -17
- kash/config/colors.py +3 -1
- 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 +24 -10
- 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 +12 -10
- kash/exec/precondition_registry.py +2 -1
- kash/exec/preconditions.py +22 -1
- kash/exec/resolve_args.py +4 -0
- kash/exec/shell_callable_action.py +33 -19
- kash/file_storage/file_store.py +42 -27
- 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/function_param_info.py +1 -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_server_routes.py +1 -7
- 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 +115 -32
- 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/shell/utils/shell_function_wrapper.py +15 -15
- kash/text_handling/custom_sliding_transforms.py +12 -4
- kash/text_handling/doc_normalization.py +6 -2
- kash/text_handling/markdown_render.py +118 -0
- kash/text_handling/markdown_utils.py +226 -0
- kash/utils/common/function_inspect.py +360 -110
- 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_ext.py +4 -0
- kash/utils/file_utils/file_formats.py +2 -2
- kash/utils/file_utils/file_formats_model.py +20 -1
- 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/__init__.py +0 -4
- kash/web_gen/simple_webpage.py +52 -0
- kash/web_gen/tabbed_webpage.py +24 -14
- kash/web_gen/template_render.py +37 -2
- kash/web_gen/templates/base_styles.css.jinja +169 -43
- kash/web_gen/templates/base_webpage.html.jinja +110 -45
- kash/web_gen/templates/content_styles.css.jinja +4 -2
- kash/web_gen/templates/item_view.html.jinja +49 -39
- kash/web_gen/templates/simple_webpage.html.jinja +24 -0
- kash/web_gen/templates/tabbed_webpage.html.jinja +42 -33
- 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.11.dist-info}/METADATA +10 -8
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/RECORD +137 -136
- kash/actions/core/webpage_config.py +0 -21
- 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.11.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.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
|
@@ -6,7 +6,7 @@ from dataclasses import asdict, field, is_dataclass
|
|
|
6
6
|
from datetime import UTC, datetime
|
|
7
7
|
from enum import Enum
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import TYPE_CHECKING, Any, TypeVar
|
|
9
|
+
from typing import TYPE_CHECKING, Any, NotRequired, TypedDict, TypeVar, Unpack
|
|
10
10
|
|
|
11
11
|
from frontmatter_format import from_yaml_string, new_yaml
|
|
12
12
|
from prettyfmt import (
|
|
@@ -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
|
|
|
@@ -119,6 +120,28 @@ class IdType(Enum):
|
|
|
119
120
|
source = "source"
|
|
120
121
|
|
|
121
122
|
|
|
123
|
+
class ItemUpdateOptions(TypedDict, total=False):
|
|
124
|
+
"""
|
|
125
|
+
Keyword arguments that can be passed to update an Item.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
type: NotRequired[ItemType]
|
|
129
|
+
state: NotRequired[State]
|
|
130
|
+
title: NotRequired[str | None]
|
|
131
|
+
url: NotRequired[Url | None]
|
|
132
|
+
description: NotRequired[str | None]
|
|
133
|
+
format: NotRequired[Format | None]
|
|
134
|
+
file_ext: NotRequired[FileExt | None]
|
|
135
|
+
body: NotRequired[str | None]
|
|
136
|
+
external_path: NotRequired[str | None]
|
|
137
|
+
original_filename: NotRequired[str | None]
|
|
138
|
+
relations: NotRequired[ItemRelations]
|
|
139
|
+
source: NotRequired[Source | None]
|
|
140
|
+
history: NotRequired[list[OperationSummary] | None]
|
|
141
|
+
thumbnail_url: NotRequired[Url | None]
|
|
142
|
+
extra: NotRequired[dict | None]
|
|
143
|
+
|
|
144
|
+
|
|
122
145
|
@dataclass(frozen=True)
|
|
123
146
|
class ItemId:
|
|
124
147
|
"""
|
|
@@ -155,7 +178,9 @@ class ItemId:
|
|
|
155
178
|
if item.type == ItemType.resource and item.format == Format.url and item.url:
|
|
156
179
|
item_id = ItemId(item.type, IdType.url, canonicalize_url(item.url))
|
|
157
180
|
elif item.type == ItemType.concept and item.title:
|
|
158
|
-
item_id = ItemId(
|
|
181
|
+
item_id = ItemId(
|
|
182
|
+
item.type, IdType.concept, canonicalize_concept(item.title)
|
|
183
|
+
)
|
|
159
184
|
elif item.source and item.source.cacheable:
|
|
160
185
|
# We know the source of this and if the action was cacheable, we can create
|
|
161
186
|
# an identity based on the source.
|
|
@@ -257,7 +282,9 @@ class Item:
|
|
|
257
282
|
item_dict = {**item_dict, **kwargs}
|
|
258
283
|
|
|
259
284
|
info_prefix = (
|
|
260
|
-
f"{fmt_store_path(item_dict['store_path'])}: "
|
|
285
|
+
f"{fmt_store_path(item_dict['store_path'])}: "
|
|
286
|
+
if "store_path" in item_dict
|
|
287
|
+
else ""
|
|
261
288
|
)
|
|
262
289
|
|
|
263
290
|
# Metadata formats might change over time so it's important to gracefully handle issues.
|
|
@@ -287,7 +314,9 @@ class Item:
|
|
|
287
314
|
body = item_dict.get("body")
|
|
288
315
|
history = [OperationSummary(**op) for op in item_dict.get("history", [])]
|
|
289
316
|
relations = (
|
|
290
|
-
ItemRelations(**item_dict["relations"])
|
|
317
|
+
ItemRelations(**item_dict["relations"])
|
|
318
|
+
if "relations" in item_dict
|
|
319
|
+
else ItemRelations()
|
|
291
320
|
)
|
|
292
321
|
store_path = item_dict.get("store_path")
|
|
293
322
|
|
|
@@ -305,12 +334,18 @@ class Item:
|
|
|
305
334
|
]
|
|
306
335
|
all_fields = [f.name for f in cls.__dataclass_fields__.values()]
|
|
307
336
|
allowed_fields = [f for f in all_fields if f not in excluded_fields]
|
|
308
|
-
other_metadata = {
|
|
337
|
+
other_metadata = {
|
|
338
|
+
key: value for key, value in item_dict.items() if key in allowed_fields
|
|
339
|
+
}
|
|
309
340
|
unexpected_metadata = {
|
|
310
341
|
key: value for key, value in item_dict.items() if key not in all_fields
|
|
311
342
|
}
|
|
312
343
|
if unexpected_metadata:
|
|
313
|
-
log.info(
|
|
344
|
+
log.info(
|
|
345
|
+
"Skipping unexpected metadata on item: %s%s",
|
|
346
|
+
info_prefix,
|
|
347
|
+
unexpected_metadata,
|
|
348
|
+
)
|
|
314
349
|
|
|
315
350
|
result = cls(
|
|
316
351
|
type=type_,
|
|
@@ -328,7 +363,10 @@ class Item:
|
|
|
328
363
|
|
|
329
364
|
@classmethod
|
|
330
365
|
def from_external_path(
|
|
331
|
-
cls,
|
|
366
|
+
cls,
|
|
367
|
+
path: Path | str,
|
|
368
|
+
item_type: ItemType | None = None,
|
|
369
|
+
title: str | None = None,
|
|
332
370
|
) -> Item:
|
|
333
371
|
"""
|
|
334
372
|
Create a resource Item for a file with a format inferred from the file extension
|
|
@@ -347,7 +385,9 @@ class Item:
|
|
|
347
385
|
if not item_type:
|
|
348
386
|
# Default to doc for general text files and resource for everything else.
|
|
349
387
|
item_type = (
|
|
350
|
-
ItemType.doc
|
|
388
|
+
ItemType.doc
|
|
389
|
+
if format and format.supports_frontmatter
|
|
390
|
+
else ItemType.resource
|
|
351
391
|
)
|
|
352
392
|
item = cls(
|
|
353
393
|
type=item_type,
|
|
@@ -398,7 +438,22 @@ class Item:
|
|
|
398
438
|
if not self.format:
|
|
399
439
|
raise ValueError(f"Item has no format: {self}")
|
|
400
440
|
if self.type.expects_body and self.format.has_body and not self.body:
|
|
401
|
-
raise ValueError(
|
|
441
|
+
raise ValueError(
|
|
442
|
+
f"Item type `{self.type.value}` is text but has no body: {self}"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def absolute_path(self, ws: "Workspace | None" = None) -> Path: # noqa: UP037
|
|
446
|
+
"""
|
|
447
|
+
Get the absolute path to the item. Throws `ValueError` if the item has no
|
|
448
|
+
store path. If no workspace is provided, uses the current workspace.
|
|
449
|
+
"""
|
|
450
|
+
from kash.workspaces import current_ws
|
|
451
|
+
|
|
452
|
+
if not self.store_path:
|
|
453
|
+
raise ValueError("Item has no store path")
|
|
454
|
+
if not ws:
|
|
455
|
+
ws = current_ws()
|
|
456
|
+
return ws.base_dir / self.store_path
|
|
402
457
|
|
|
403
458
|
@property
|
|
404
459
|
def is_binary(self) -> bool:
|
|
@@ -438,7 +493,9 @@ class Item:
|
|
|
438
493
|
return {k: serialize(v) for k, v in v.items()}
|
|
439
494
|
elif isinstance(v, Enum):
|
|
440
495
|
return v.value
|
|
441
|
-
elif hasattr(
|
|
496
|
+
elif hasattr(
|
|
497
|
+
v, "as_dict"
|
|
498
|
+
): # Handle Operation or any object with as_dict method.
|
|
442
499
|
return v.as_dict()
|
|
443
500
|
elif is_dataclass(v) and not isinstance(v, type):
|
|
444
501
|
# Handle Python and Pydantic dataclasses.
|
|
@@ -486,17 +543,16 @@ class Item:
|
|
|
486
543
|
return abbrev_str(self.url, max_len)
|
|
487
544
|
|
|
488
545
|
# Special case for filenames with no title.
|
|
489
|
-
|
|
490
|
-
(self.store_path and Path(self.store_path).
|
|
491
|
-
or (self.external_path and Path(self.external_path).
|
|
492
|
-
or (self.original_filename and Path(self.original_filename).
|
|
546
|
+
path_name = (
|
|
547
|
+
(self.store_path and Path(self.store_path).name)
|
|
548
|
+
or (self.external_path and Path(self.external_path).name)
|
|
549
|
+
or (self.original_filename and Path(self.original_filename).name)
|
|
493
550
|
)
|
|
494
|
-
if not self.title and path_stem:
|
|
495
|
-
return abbrev_str(path_stem, max_len)
|
|
496
551
|
|
|
497
|
-
#
|
|
552
|
+
# Use the title or the path if possible, falling back to description or even body text.
|
|
498
553
|
title_raw_text = (
|
|
499
554
|
self.title
|
|
555
|
+
or path_name
|
|
500
556
|
or self.description
|
|
501
557
|
or (not self.is_binary and self.abbrev_body(max_len))
|
|
502
558
|
or UNTITLED
|
|
@@ -541,6 +597,13 @@ class Item:
|
|
|
541
597
|
|
|
542
598
|
return body_text[:max_len]
|
|
543
599
|
|
|
600
|
+
@property
|
|
601
|
+
def has_body(self) -> bool:
|
|
602
|
+
"""
|
|
603
|
+
True if the item has a non-empty body.
|
|
604
|
+
"""
|
|
605
|
+
return bool(self.body and self.body.strip())
|
|
606
|
+
|
|
544
607
|
def slug_name(self, max_len: int = SLUG_MAX_LEN) -> str:
|
|
545
608
|
"""
|
|
546
609
|
Get a readable slugified version of the title or filename or content
|
|
@@ -554,7 +617,9 @@ class Item:
|
|
|
554
617
|
"""
|
|
555
618
|
Get or infer description.
|
|
556
619
|
"""
|
|
557
|
-
return abbrev_on_words(
|
|
620
|
+
return abbrev_on_words(
|
|
621
|
+
html_to_plaintext(self.description or self.body or ""), max_len
|
|
622
|
+
)
|
|
558
623
|
|
|
559
624
|
def read_as_config(self) -> Any:
|
|
560
625
|
"""
|
|
@@ -585,7 +650,6 @@ class Item:
|
|
|
585
650
|
"""
|
|
586
651
|
Get the full file extension suffix (e.g. "note.md") for this item.
|
|
587
652
|
"""
|
|
588
|
-
|
|
589
653
|
if self.type == ItemType.extension:
|
|
590
654
|
# Python files cannot have more than one . in them.
|
|
591
655
|
return f"{FileExt.py.value}"
|
|
@@ -619,7 +683,10 @@ class Item:
|
|
|
619
683
|
raise ValueError(f"Cannot convert item of type {self.format} to HTML: {self}")
|
|
620
684
|
|
|
621
685
|
def _copy_and_update(
|
|
622
|
-
self,
|
|
686
|
+
self,
|
|
687
|
+
other: Item | None = None,
|
|
688
|
+
update_timestamp: bool = False,
|
|
689
|
+
**other_updates: Unpack[ItemUpdateOptions],
|
|
623
690
|
) -> dict[str, Any]:
|
|
624
691
|
overrides: dict[str, Any] = {"store_path": None, "modified_at": None}
|
|
625
692
|
if update_timestamp:
|
|
@@ -637,12 +704,16 @@ class Item:
|
|
|
637
704
|
|
|
638
705
|
return fields
|
|
639
706
|
|
|
640
|
-
def new_copy_with(
|
|
707
|
+
def new_copy_with(
|
|
708
|
+
self, update_timestamp: bool = True, **other_updates: Unpack[ItemUpdateOptions]
|
|
709
|
+
) -> Item:
|
|
641
710
|
"""
|
|
642
711
|
Copy item with the given field updates. Resets store_path to None. Updates
|
|
643
712
|
created time if requested.
|
|
644
713
|
"""
|
|
645
|
-
new_fields = self._copy_and_update(
|
|
714
|
+
new_fields = self._copy_and_update(
|
|
715
|
+
update_timestamp=update_timestamp, **other_updates
|
|
716
|
+
)
|
|
646
717
|
return Item(**new_fields)
|
|
647
718
|
|
|
648
719
|
def merged_copy(self, other: Item) -> Item:
|
|
@@ -653,7 +724,7 @@ class Item:
|
|
|
653
724
|
merged_fields = self._copy_and_update(other, update_timestamp=False)
|
|
654
725
|
return Item(**merged_fields)
|
|
655
726
|
|
|
656
|
-
def derived_copy(self,
|
|
727
|
+
def derived_copy(self, **updates: Unpack[ItemUpdateOptions]) -> Item:
|
|
657
728
|
"""
|
|
658
729
|
Same as `new_copy_with()`, but also makes any other updates and updates the
|
|
659
730
|
`derived_from` relation. If we also have an action context, then use the
|
|
@@ -678,8 +749,12 @@ class Item:
|
|
|
678
749
|
else:
|
|
679
750
|
derived_from = [StorePath(self.store_path)]
|
|
680
751
|
|
|
681
|
-
updates =
|
|
682
|
-
|
|
752
|
+
updates = updates.copy()
|
|
753
|
+
|
|
754
|
+
# If format was specified and user didn't specify file_ext, then infer it.
|
|
755
|
+
if "file_ext" not in updates and "format" in updates:
|
|
756
|
+
assert updates["format"] is not None
|
|
757
|
+
updates["file_ext"] = updates["format"].file_ext
|
|
683
758
|
|
|
684
759
|
# External resource paths only make sense for resources, so clear them out if new item
|
|
685
760
|
# is not a resource.
|
|
@@ -691,15 +766,22 @@ class Item:
|
|
|
691
766
|
if derived_from:
|
|
692
767
|
new_item.update_relations(derived_from=derived_from)
|
|
693
768
|
|
|
694
|
-
# Fall back to action title template if we have it and
|
|
695
|
-
if "title" not in
|
|
769
|
+
# Fall back to action title template if we have it and title wasn't explicitly set.
|
|
770
|
+
if "title" not in updates:
|
|
771
|
+
prev_title = self.title or (
|
|
772
|
+
Path(self.store_path).stem if self.store_path else UNTITLED
|
|
773
|
+
)
|
|
696
774
|
if self.context:
|
|
697
775
|
action = self.context.action
|
|
698
776
|
new_item.title = action.title_template.format(
|
|
699
|
-
title=
|
|
777
|
+
title=prev_title, action_name=action.name
|
|
700
778
|
)
|
|
701
779
|
else:
|
|
702
|
-
log.warning(
|
|
780
|
+
log.warning(
|
|
781
|
+
"Deriving an item without action context so keeping previous title: %s",
|
|
782
|
+
self,
|
|
783
|
+
)
|
|
784
|
+
new_item.title = f"{prev_title} (derived copy)"
|
|
703
785
|
|
|
704
786
|
return new_item
|
|
705
787
|
|
|
@@ -727,7 +809,8 @@ class Item:
|
|
|
727
809
|
|
|
728
810
|
def content_equals(self, other: Item) -> bool:
|
|
729
811
|
"""
|
|
730
|
-
Check if two items have identical content, ignoring timestamps
|
|
812
|
+
Check if two items have identical content, ignoring timestamps, store path,
|
|
813
|
+
and any trailing newlines or whitespace.
|
|
731
814
|
"""
|
|
732
815
|
# Check relevant metadata fields.
|
|
733
816
|
self_fields = self.__dict__.copy()
|
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
|