kash-shell 0.3.8__py3-none-any.whl → 0.3.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +15 -2
- kash/commands/base/general_commands.py +27 -18
- kash/commands/base/logs_commands.py +1 -4
- 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 -16
- kash/config/colors.py +2 -0
- kash/config/env_settings.py +72 -0
- kash/config/init.py +2 -2
- kash/config/logger.py +61 -59
- kash/config/logger_basic.py +12 -5
- kash/config/server_config.py +6 -6
- kash/config/settings.py +117 -67
- kash/config/setup.py +35 -9
- kash/config/suppress_warnings.py +30 -12
- kash/config/text_styles.py +3 -13
- kash/docs/load_api_docs.py +2 -1
- kash/docs/markdown/topics/a2_installation.md +7 -3
- kash/docs/markdown/topics/a3_getting_started.md +3 -2
- kash/docs/markdown/warning.md +3 -8
- kash/docs/markdown/welcome.md +4 -0
- kash/docs_base/load_recipe_snippets.py +1 -1
- kash/docs_base/recipes/{general_system_commands.ksh → general_system_commands.sh} +1 -1
- kash/{concepts → embeddings}/cosine.py +2 -1
- kash/embeddings/text_similarity.py +57 -0
- 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 -6
- 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 -14
- kash/file_storage/item_file_format.py +13 -3
- kash/file_storage/metadata_dirs.py +11 -2
- kash/help/assistant.py +2 -2
- kash/help/assistant_instructions.py +2 -1
- kash/help/help_embeddings.py +2 -2
- kash/help/help_printing.py +14 -10
- kash/help/tldr_help.py +5 -3
- kash/llm_utils/clean_headings.py +1 -1
- kash/llm_utils/llm_api_keys.py +4 -4
- kash/llm_utils/llm_completion.py +2 -2
- 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 +17 -12
- kash/local_server/__init__.py +5 -2
- kash/local_server/local_server.py +56 -46
- kash/local_server/local_server_commands.py +15 -15
- kash/local_server/local_server_routes.py +2 -2
- kash/local_server/local_url_formatters.py +1 -1
- kash/mcp/__init__.py +5 -2
- kash/mcp/mcp_cli.py +54 -17
- kash/mcp/mcp_server_commands.py +5 -6
- kash/mcp/mcp_server_routes.py +14 -11
- kash/mcp/mcp_server_sse.py +61 -34
- kash/mcp/mcp_server_stdio.py +0 -8
- kash/media_base/audio_processing.py +81 -7
- kash/media_base/media_cache.py +18 -18
- 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 -109
- 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 +56 -13
- 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 +80 -28
- kash/utils/errors.py +6 -0
- kash/utils/file_utils/{dir_size.py → dir_info.py} +25 -4
- kash/utils/file_utils/file_ext.py +2 -3
- kash/utils/file_utils/file_formats.py +28 -2
- kash/utils/file_utils/file_formats_model.py +50 -19
- kash/utils/file_utils/filename_parsing.py +10 -4
- 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 +4 -2
- kash/workspaces/workspace_output.py +11 -4
- kash/workspaces/workspaces.py +5 -11
- kash/xonsh_custom/command_nl_utils.py +40 -19
- kash/xonsh_custom/custom_shell.py +44 -12
- kash/xonsh_custom/customize_prompt.py +39 -21
- kash/xonsh_custom/load_into_xonsh.py +26 -27
- 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.8.dist-info → kash_shell-0.3.10.dist-info}/METADATA +26 -12
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/RECORD +140 -140
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/entry_points.txt +1 -1
- kash/concepts/concept_formats.py +0 -23
- kash/concepts/text_similarity.py +0 -112
- kash/shell/clideps/api_keys.py +0 -99
- kash/shell/clideps/dotenv_setup.py +0 -114
- kash/shell/clideps/dotenv_utils.py +0 -89
- kash/shell/clideps/pkg_deps.py +0 -232
- 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 -158
- kash/utils/common/string_replace.py +0 -93
- kash/utils/common/string_template.py +0 -101
- /kash/docs_base/recipes/{python_dev_commands.ksh → python_dev_commands.sh} +0 -0
- /kash/docs_base/recipes/{tldr_standard_commands.ksh → tldr_standard_commands.sh} +0 -0
- /kash/{concepts → embeddings}/embeddings.py +0 -0
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from rich.box import SQUARE
|
|
2
|
+
from rich.console import Group
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
|
|
5
|
+
from kash.commands.help.logo import branded_box
|
|
6
|
+
from kash.config.text_styles import (
|
|
7
|
+
COLOR_HINT,
|
|
8
|
+
)
|
|
9
|
+
from kash.docs.all_docs import all_docs
|
|
10
|
+
from kash.exec import kash_command
|
|
11
|
+
from kash.shell.output.shell_output import PrintHooks, cprint
|
|
12
|
+
from kash.shell.version import get_full_version_name
|
|
13
|
+
from kash.utils.rich_custom.rich_markdown_fork import Markdown
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@kash_command
|
|
17
|
+
def welcome() -> None:
|
|
18
|
+
"""
|
|
19
|
+
Print a welcome message.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
help_topics = all_docs.help_topics
|
|
23
|
+
version = get_full_version_name()
|
|
24
|
+
# Create header with logo and right-justified version
|
|
25
|
+
|
|
26
|
+
PrintHooks.before_welcome()
|
|
27
|
+
cprint(
|
|
28
|
+
branded_box(
|
|
29
|
+
Group(Markdown(help_topics.welcome)),
|
|
30
|
+
version,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
cprint(Panel(Markdown(help_topics.warning), box=SQUARE, border_style=COLOR_HINT))
|
|
@@ -27,8 +27,9 @@ def select(
|
|
|
27
27
|
previous: bool = False,
|
|
28
28
|
next: bool = False,
|
|
29
29
|
pop: bool = False,
|
|
30
|
-
|
|
30
|
+
clear_all: bool = False,
|
|
31
31
|
clear_future: bool = False,
|
|
32
|
+
refresh: bool = False,
|
|
32
33
|
) -> ShellResult:
|
|
33
34
|
"""
|
|
34
35
|
Set or show the current selection.
|
|
@@ -47,12 +48,13 @@ def select(
|
|
|
47
48
|
:param previous: Move back in the selection history to the previous selection.
|
|
48
49
|
:param next: Move forward in the selection history to the next selection.
|
|
49
50
|
:param pop: Pop the current selection from the history.
|
|
50
|
-
:param
|
|
51
|
+
:param clear_all: Clear the full selection history.
|
|
51
52
|
:param clear_future: Clear all selections from history after the current one.
|
|
53
|
+
:param refresh: Refresh the current selection to drop any paths that no longer exist.
|
|
52
54
|
"""
|
|
53
55
|
ws = current_ws()
|
|
54
56
|
|
|
55
|
-
#
|
|
57
|
+
# FIXME: It would be nice to be able to read stdin from a pipe but this isn't working rn.
|
|
56
58
|
# You could then run `... | select --stdin` to select the piped input.
|
|
57
59
|
# Globally we have THREAD_SUBPROCS=False to avoid hard-to-interrupt subprocesses.
|
|
58
60
|
# But xonsh seems to hang with stdin unless we modify the spec to be threadable?
|
|
@@ -61,7 +63,7 @@ def select(
|
|
|
61
63
|
# if stdin:
|
|
62
64
|
# paths = tuple(sys.stdin.read().splitlines())
|
|
63
65
|
|
|
64
|
-
exclusive_flags = [history, last, back, forward, previous, next, pop,
|
|
66
|
+
exclusive_flags = [history, last, back, forward, previous, next, pop, clear_all, clear_future]
|
|
65
67
|
if sum(bool(f) for f in exclusive_flags) > 1:
|
|
66
68
|
raise InvalidInput("Cannot combine multiple flags")
|
|
67
69
|
if paths and any(exclusive_flags):
|
|
@@ -96,12 +98,15 @@ def select(
|
|
|
96
98
|
elif pop:
|
|
97
99
|
ws.selections.pop()
|
|
98
100
|
return ShellResult(show_selection=True)
|
|
99
|
-
elif
|
|
100
|
-
ws.selections.
|
|
101
|
+
elif clear_all:
|
|
102
|
+
ws.selections.clear_all()
|
|
101
103
|
return ShellResult(show_selection=True)
|
|
102
104
|
elif clear_future:
|
|
103
105
|
ws.selections.clear_future()
|
|
104
106
|
return ShellResult(show_selection=True)
|
|
107
|
+
elif refresh:
|
|
108
|
+
ws.selections.refresh_current(ws.base_dir)
|
|
109
|
+
return ShellResult(show_selection=True)
|
|
105
110
|
else:
|
|
106
111
|
return ShellResult(show_selection=True)
|
|
107
112
|
|
|
@@ -15,7 +15,6 @@ from kash.config.text_styles import (
|
|
|
15
15
|
EMOJI_WARN,
|
|
16
16
|
STYLE_EMPH,
|
|
17
17
|
STYLE_HINT,
|
|
18
|
-
format_success_emoji,
|
|
19
18
|
)
|
|
20
19
|
from kash.exec import (
|
|
21
20
|
assemble_path_args,
|
|
@@ -36,7 +35,11 @@ from kash.model.items_model import Item, ItemType
|
|
|
36
35
|
from kash.model.params_model import GLOBAL_PARAMS
|
|
37
36
|
from kash.model.paths_model import StorePath, fmt_store_path
|
|
38
37
|
from kash.shell.input.param_inputs import input_param_name, input_param_value
|
|
39
|
-
from kash.shell.output.shell_formatting import
|
|
38
|
+
from kash.shell.output.shell_formatting import (
|
|
39
|
+
format_name_and_description,
|
|
40
|
+
format_name_and_value,
|
|
41
|
+
format_success_emoji,
|
|
42
|
+
)
|
|
40
43
|
from kash.shell.output.shell_output import (
|
|
41
44
|
PrintHooks,
|
|
42
45
|
Wrap,
|
|
@@ -54,7 +57,7 @@ from kash.utils.common.type_utils import not_none
|
|
|
54
57
|
from kash.utils.common.url import Url, is_url
|
|
55
58
|
from kash.utils.errors import InvalidInput
|
|
56
59
|
from kash.utils.file_formats.chat_format import tail_chat_history
|
|
57
|
-
from kash.utils.file_utils.
|
|
60
|
+
from kash.utils.file_utils.dir_info import is_nonempty_dir
|
|
58
61
|
from kash.utils.lang_utils.inflection import plural
|
|
59
62
|
from kash.web_content.file_cache_utils import cache_file
|
|
60
63
|
from kash.workspaces import (
|
|
@@ -170,14 +173,15 @@ def cache_media(*urls: str) -> None:
|
|
|
170
173
|
|
|
171
174
|
|
|
172
175
|
@kash_command
|
|
173
|
-
def cache_content(*urls_or_paths: str) -> None:
|
|
176
|
+
def cache_content(*urls_or_paths: str, refetch: bool = False) -> None:
|
|
174
177
|
"""
|
|
175
178
|
Cache the given file in the content cache. Downloads any URL or copies a local file.
|
|
176
179
|
"""
|
|
180
|
+
expiration_sec = 0 if refetch else None
|
|
177
181
|
PrintHooks.spacer()
|
|
178
182
|
for url_or_path in urls_or_paths:
|
|
179
183
|
locator = resolve_locator_arg(url_or_path)
|
|
180
|
-
cache_path, was_cached = cache_file(locator)
|
|
184
|
+
cache_path, was_cached = cache_file(locator, expiration_sec=expiration_sec)
|
|
181
185
|
cache_str = " (already cached)" if was_cached else ""
|
|
182
186
|
cprint(f"{fmt_loc(url_or_path)}{cache_str}:", style=STYLE_EMPH, text_wrap=Wrap.NONE)
|
|
183
187
|
cprint(f"{cache_path}", text_wrap=Wrap.INDENT_ONLY)
|
|
@@ -185,10 +189,13 @@ def cache_content(*urls_or_paths: str) -> None:
|
|
|
185
189
|
|
|
186
190
|
|
|
187
191
|
@kash_command
|
|
188
|
-
def download(*urls_or_paths: str) -> None:
|
|
192
|
+
def download(*urls_or_paths: str, refetch: bool = False) -> None:
|
|
189
193
|
"""
|
|
190
|
-
Download a URL or resource.
|
|
194
|
+
Download a URL or resource. Uses cached content if available, unless `refetch` is true.
|
|
195
|
+
Inputs can be URLs or paths to URL resources.
|
|
191
196
|
"""
|
|
197
|
+
expiration_sec = 0 if refetch else None
|
|
198
|
+
|
|
192
199
|
# TODO: Add option to include frontmatter metadata for text files.
|
|
193
200
|
ws = current_ws()
|
|
194
201
|
for url_or_path in urls_or_paths:
|
|
@@ -211,7 +218,7 @@ def download(*urls_or_paths: str) -> None:
|
|
|
211
218
|
media_tools.cache_media(url)
|
|
212
219
|
else:
|
|
213
220
|
log.message("Will cache file and save to workspace: %s", fmt_loc(url))
|
|
214
|
-
cache_path, _was_cached = cache_file(url)
|
|
221
|
+
cache_path, _was_cached = cache_file(url, expiration_sec=expiration_sec)
|
|
215
222
|
item = Item.from_external_path(cache_path, item_type=ItemType.resource)
|
|
216
223
|
store_path = ws.save(item)
|
|
217
224
|
|
|
@@ -402,12 +409,10 @@ def set_params(*key_vals: str) -> None:
|
|
|
402
409
|
|
|
403
410
|
|
|
404
411
|
@kash_command
|
|
405
|
-
def
|
|
412
|
+
def params(full: bool = False) -> None:
|
|
406
413
|
"""
|
|
407
|
-
|
|
414
|
+
List currently set workspace parameters, which are settings that may be used
|
|
408
415
|
by commands and actions or to override default parameters.
|
|
409
|
-
|
|
410
|
-
Run with no args to interactively set parameters.
|
|
411
416
|
"""
|
|
412
417
|
ws: Workspace = current_ws()
|
|
413
418
|
settable_params = GLOBAL_PARAMS
|
|
@@ -461,9 +466,7 @@ def import_item(
|
|
|
461
466
|
|
|
462
467
|
|
|
463
468
|
@kash_command
|
|
464
|
-
def fetch_metadata(
|
|
465
|
-
*files_or_urls: str, no_cache: bool = False, refetch: bool = False
|
|
466
|
-
) -> ShellResult:
|
|
469
|
+
def fetch_metadata(*files_or_urls: str, refetch: bool = False) -> ShellResult:
|
|
467
470
|
"""
|
|
468
471
|
Fetch metadata for the given URLs or resources. Imports new URLs and saves back
|
|
469
472
|
the fetched metadata for existing resources.
|
|
@@ -483,7 +486,7 @@ def fetch_metadata(
|
|
|
483
486
|
try:
|
|
484
487
|
if isinstance(locator, Path):
|
|
485
488
|
raise InvalidInput()
|
|
486
|
-
fetched_item = fetch_url_metadata(locator,
|
|
489
|
+
fetched_item = fetch_url_metadata(locator, refetch=refetch)
|
|
487
490
|
store_paths.append(fetched_item.store_path)
|
|
488
491
|
except InvalidInput:
|
|
489
492
|
log.warning("Not a URL or URL resource, will not fetch metadata: %s", fmt_loc(locator))
|
kash/config/colors.py
CHANGED
|
@@ -134,8 +134,10 @@ web_light_translucent = SimpleNamespace(
|
|
|
134
134
|
primary_light=hsl_to_hex("hsl(188, 40%, 62%)"),
|
|
135
135
|
secondary=hsl_to_hex("hsl(188, 12%, 28%)"),
|
|
136
136
|
bg=hsl_to_hex("hsla(44, 6%, 100%, 0.75)"),
|
|
137
|
+
bg_solid=hsl_to_hex("hsla(44, 6%, 100%, 1)"),
|
|
137
138
|
bg_header=hsl_to_hex("hsla(188, 42%, 70%, 0.2)"),
|
|
138
139
|
bg_alt=hsl_to_hex("hsla(44, 28%, 90%, 0.3)"),
|
|
140
|
+
bg_alt_solid=hsl_to_hex("hsla(44, 28%, 97%, 1)"),
|
|
139
141
|
text=hsl_to_hex("hsl(188, 39%, 11%)"),
|
|
140
142
|
border=hsl_to_hex("hsl(188, 8%, 50%)"),
|
|
141
143
|
border_hint=hsl_to_hex("hsla(188, 8%, 72%, 0.7)"),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import overload
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class KashEnv(str, Enum):
|
|
8
|
+
"""
|
|
9
|
+
Environment variable settings for kash. None are required, but these may be
|
|
10
|
+
used to override default values.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
KASH_LOG_LEVEL = "KASH_LOG_LEVEL"
|
|
14
|
+
"""The log level for console-based logging."""
|
|
15
|
+
|
|
16
|
+
KASH_WS_ROOT = "KASH_WS_ROOT"
|
|
17
|
+
"""The root directory for kash workspaces."""
|
|
18
|
+
|
|
19
|
+
KASH_GLOBAL_WS = "KASH_GLOBAL_WS"
|
|
20
|
+
"""The global workspace directory."""
|
|
21
|
+
|
|
22
|
+
KASH_SYSTEM_LOGS_DIR = "KASH_SYSTEM_LOGS_DIR"
|
|
23
|
+
"""The directory for system logs."""
|
|
24
|
+
|
|
25
|
+
KASH_SYSTEM_CACHE_DIR = "KASH_SYSTEM_CACHE_DIR"
|
|
26
|
+
"""The directory for system cache (caches separate from workspace caches)."""
|
|
27
|
+
|
|
28
|
+
KASH_MCP_WS = "KASH_MCP_WS"
|
|
29
|
+
"""The directory for the workspace for MCP servers."""
|
|
30
|
+
|
|
31
|
+
KASH_SHOW_TRACEBACK = "KASH_SHOW_TRACEBACK"
|
|
32
|
+
"""Whether to show tracebacks on actions and commands in the shell."""
|
|
33
|
+
|
|
34
|
+
KASH_USER_AGENT = "KASH_USER_AGENT"
|
|
35
|
+
"""The user agent to use for HTTP requests."""
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def read_str(self) -> str | None: ...
|
|
39
|
+
|
|
40
|
+
@overload
|
|
41
|
+
def read_str(self, default: str) -> str: ...
|
|
42
|
+
|
|
43
|
+
def read_str(self, default: str | None = None) -> str | None:
|
|
44
|
+
"""
|
|
45
|
+
Get the value of the environment variable from the environment (with
|
|
46
|
+
optional default).
|
|
47
|
+
"""
|
|
48
|
+
return os.environ.get(self.value, default)
|
|
49
|
+
|
|
50
|
+
@overload
|
|
51
|
+
def read_path(self) -> Path | None: ...
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def read_path(self, default: Path) -> Path: ...
|
|
55
|
+
|
|
56
|
+
def read_path(self, default: Path | None = None) -> Path | None:
|
|
57
|
+
"""
|
|
58
|
+
Get the value of the environment variable as a resolved path (with
|
|
59
|
+
optional default).
|
|
60
|
+
"""
|
|
61
|
+
value = os.environ.get(self.value)
|
|
62
|
+
if value:
|
|
63
|
+
return Path(value).expanduser().resolve()
|
|
64
|
+
else:
|
|
65
|
+
return default.expanduser().resolve() if default else None
|
|
66
|
+
|
|
67
|
+
def read_bool(self, default: bool = False) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Get the value of the environment variable as a boolean.
|
|
70
|
+
"""
|
|
71
|
+
value = str(os.environ.get(self.value, default) or "").lower()
|
|
72
|
+
return bool(value and value != "0" and value != "false" and value != "no")
|
kash/config/init.py
CHANGED
|
@@ -9,10 +9,10 @@ def kash_reload_all() -> tuple[dict[str, Callable], dict[str, type["Action"]]]:
|
|
|
9
9
|
"""
|
|
10
10
|
Import all kash modules that define actions and commands.
|
|
11
11
|
"""
|
|
12
|
-
from kash.exec.action_registry import
|
|
12
|
+
from kash.exec.action_registry import refresh_action_classes
|
|
13
13
|
from kash.exec.command_registry import get_all_commands
|
|
14
14
|
|
|
15
15
|
commands = get_all_commands()
|
|
16
|
-
actions =
|
|
16
|
+
actions = refresh_action_classes()
|
|
17
17
|
|
|
18
18
|
return commands, actions
|
kash/config/logger.py
CHANGED
|
@@ -2,7 +2,6 @@ import contextvars
|
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
|
-
import threading
|
|
6
5
|
from collections.abc import Generator
|
|
7
6
|
from contextlib import contextmanager
|
|
8
7
|
from dataclasses import dataclass
|
|
@@ -12,12 +11,12 @@ from pathlib import Path
|
|
|
12
11
|
from typing import IO, Any, cast
|
|
13
12
|
|
|
14
13
|
import rich
|
|
14
|
+
from prettyfmt import slugify_snake
|
|
15
15
|
from rich._null_file import NULL_FILE
|
|
16
16
|
from rich.console import Console
|
|
17
17
|
from rich.logging import RichHandler
|
|
18
18
|
from rich.theme import Theme
|
|
19
|
-
from
|
|
20
|
-
from strif import atomic_output_file, new_timestamped_uid
|
|
19
|
+
from strif import AtomicVar, atomic_output_file, new_timestamped_uid
|
|
21
20
|
from typing_extensions import override
|
|
22
21
|
|
|
23
22
|
import kash.config.suppress_warnings # noqa: F401
|
|
@@ -38,55 +37,41 @@ from kash.utils.common.stack_traces import current_stack_traces
|
|
|
38
37
|
from kash.utils.common.task_stack import task_stack_prefix_str
|
|
39
38
|
|
|
40
39
|
|
|
41
|
-
@dataclass
|
|
40
|
+
@dataclass
|
|
42
41
|
class LogSettings:
|
|
43
42
|
log_console_level: LogLevel
|
|
44
43
|
log_file_level: LogLevel
|
|
45
|
-
|
|
44
|
+
|
|
46
45
|
global_log_dir: Path
|
|
46
|
+
"""Global directory for log files."""
|
|
47
47
|
|
|
48
48
|
# These directories can change based on the current workspace:
|
|
49
49
|
log_dir: Path
|
|
50
|
-
|
|
51
|
-
log_file_path: Path
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
_log_dir = get_system_logs_dir()
|
|
55
|
-
"""
|
|
56
|
-
Parent of the "logs" directory. Initially the global kash workspace.
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
LOG_NAME_GLOBAL = "global"
|
|
50
|
+
"""Parent of the "logs" directory. Initially the global kash workspace."""
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
"""
|
|
64
|
-
Name of the log file. By default the workspace name or "global" if
|
|
65
|
-
for the global workspace.
|
|
66
|
-
"""
|
|
52
|
+
log_name: str
|
|
53
|
+
"""Name of the log file. Typically the workspace name or "workspace" if for the global workspace."""
|
|
67
54
|
|
|
68
|
-
|
|
55
|
+
log_objects_dir: Path
|
|
56
|
+
log_file_path: Path
|
|
69
57
|
|
|
70
58
|
|
|
71
|
-
|
|
72
|
-
name = str(name).strip().rstrip("/").removesuffix(".log")
|
|
73
|
-
name = re.sub(r"[^\w-]", "_", name)
|
|
74
|
-
return name
|
|
59
|
+
LOG_NAME_GLOBAL = "workspace"
|
|
75
60
|
|
|
76
61
|
|
|
77
62
|
def _read_log_settings() -> LogSettings:
|
|
78
|
-
global _log_dir, _log_name
|
|
79
63
|
return LogSettings(
|
|
80
64
|
log_console_level=global_settings().console_log_level,
|
|
81
65
|
log_file_level=global_settings().file_log_level,
|
|
82
66
|
global_log_dir=get_system_logs_dir(),
|
|
83
|
-
log_dir=
|
|
84
|
-
|
|
85
|
-
|
|
67
|
+
log_dir=get_system_logs_dir(),
|
|
68
|
+
log_name=LOG_NAME_GLOBAL,
|
|
69
|
+
log_objects_dir=get_system_logs_dir() / "objects" / LOG_NAME_GLOBAL,
|
|
70
|
+
log_file_path=get_system_logs_dir() / f"{LOG_NAME_GLOBAL}.log",
|
|
86
71
|
)
|
|
87
72
|
|
|
88
73
|
|
|
89
|
-
_log_settings: LogSettings = _read_log_settings()
|
|
74
|
+
_log_settings: AtomicVar[LogSettings] = AtomicVar(_read_log_settings())
|
|
90
75
|
|
|
91
76
|
_setup_done = False
|
|
92
77
|
|
|
@@ -95,19 +80,13 @@ def get_log_settings() -> LogSettings:
|
|
|
95
80
|
"""
|
|
96
81
|
Currently active log settings.
|
|
97
82
|
"""
|
|
98
|
-
return _log_settings
|
|
83
|
+
return _log_settings.copy()
|
|
99
84
|
|
|
100
85
|
|
|
101
|
-
def
|
|
102
|
-
"""
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"""
|
|
106
|
-
global _log_lock, _log_base, _log_name
|
|
107
|
-
with _log_lock:
|
|
108
|
-
_log_base = log_root or get_system_logs_dir()
|
|
109
|
-
_log_name = make_valid_log_name(log_name or LOG_NAME_GLOBAL)
|
|
110
|
-
reload_rich_logging_setup()
|
|
86
|
+
def make_valid_log_name(name: str) -> str:
|
|
87
|
+
name = str(name).strip().rstrip("/").removesuffix(".log")
|
|
88
|
+
name = re.sub(r"[^\w-]", "_", name)
|
|
89
|
+
return name
|
|
111
90
|
|
|
112
91
|
|
|
113
92
|
console_context_var: contextvars.ContextVar[Console | None] = contextvars.ContextVar(
|
|
@@ -169,6 +148,30 @@ _file_handler: logging.FileHandler
|
|
|
169
148
|
_console_handler: logging.Handler
|
|
170
149
|
|
|
171
150
|
|
|
151
|
+
def reset_rich_logging(
|
|
152
|
+
log_root: Path | None = None,
|
|
153
|
+
log_name: str | None = None,
|
|
154
|
+
log_path: Path | None = None,
|
|
155
|
+
):
|
|
156
|
+
"""
|
|
157
|
+
Set or reset the logging root or log name, if it has changed. None means no change
|
|
158
|
+
and global default values. `log_name` is the name of the log, excluding
|
|
159
|
+
the `.log` extension. If `log_path` is provided, it will be used to infer
|
|
160
|
+
the log root and name.
|
|
161
|
+
"""
|
|
162
|
+
if log_path:
|
|
163
|
+
if not log_path.parent.exists():
|
|
164
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
log_root = log_path.parent
|
|
166
|
+
log_name = log_path.name
|
|
167
|
+
|
|
168
|
+
global _log_settings
|
|
169
|
+
with _log_settings.updates() as settings:
|
|
170
|
+
settings.log_dir = log_root or get_system_logs_dir()
|
|
171
|
+
settings.log_name = make_valid_log_name(log_name or LOG_NAME_GLOBAL)
|
|
172
|
+
reload_rich_logging_setup()
|
|
173
|
+
|
|
174
|
+
|
|
172
175
|
def reload_rich_logging_setup():
|
|
173
176
|
"""
|
|
174
177
|
Set up or reset logging setup. This is for rich/formatted console logging and
|
|
@@ -176,12 +179,12 @@ def reload_rich_logging_setup():
|
|
|
176
179
|
Call at initial run and again if log directory changes. Replaces all previous
|
|
177
180
|
loggers and handlers. Can be called to reset with different settings.
|
|
178
181
|
"""
|
|
179
|
-
global
|
|
180
|
-
with
|
|
182
|
+
global _setup_done, _log_settings
|
|
183
|
+
with _log_settings.lock:
|
|
181
184
|
new_log_settings = _read_log_settings()
|
|
182
|
-
if not _setup_done or new_log_settings != _log_settings:
|
|
185
|
+
if not _setup_done or new_log_settings != _log_settings.value:
|
|
183
186
|
_do_logging_setup(new_log_settings)
|
|
184
|
-
_log_settings
|
|
187
|
+
_log_settings.set(new_log_settings)
|
|
185
188
|
_setup_done = True
|
|
186
189
|
|
|
187
190
|
# get_console().print(
|
|
@@ -190,6 +193,15 @@ def reload_rich_logging_setup():
|
|
|
190
193
|
# )
|
|
191
194
|
|
|
192
195
|
|
|
196
|
+
@cache
|
|
197
|
+
def _init_rich_logging():
|
|
198
|
+
rich.reconfigure(theme=get_theme(), highlighter=get_highlighter())
|
|
199
|
+
|
|
200
|
+
logging.setLoggerClass(CustomLogger)
|
|
201
|
+
|
|
202
|
+
reload_rich_logging_setup()
|
|
203
|
+
|
|
204
|
+
|
|
193
205
|
def _do_logging_setup(log_settings: LogSettings):
|
|
194
206
|
from kash.config.suppress_warnings import demote_warnings, filter_warnings
|
|
195
207
|
|
|
@@ -317,10 +329,9 @@ class CustomLogger(logging.Logger):
|
|
|
317
329
|
global _log_settings
|
|
318
330
|
prefix = prefix_slug + "." if prefix_slug else ""
|
|
319
331
|
filename = (
|
|
320
|
-
f"{prefix}{
|
|
321
|
-
f"{new_timestamped_uid()}.{file_ext.lstrip('.')}"
|
|
332
|
+
f"{prefix}{slugify_snake(description)}.{new_timestamped_uid()}.{file_ext.lstrip('.')}"
|
|
322
333
|
)
|
|
323
|
-
path = _log_settings.log_objects_dir / filename
|
|
334
|
+
path = _log_settings.copy().log_objects_dir / filename
|
|
324
335
|
with atomic_output_file(path, make_parents=True) as tmp_filename:
|
|
325
336
|
if isinstance(obj, bytes):
|
|
326
337
|
with open(tmp_filename, "wb") as f:
|
|
@@ -347,7 +358,7 @@ def get_logger(name: str) -> CustomLogger:
|
|
|
347
358
|
Get a logger that's compatible with system logging but has our additional custom
|
|
348
359
|
methods.
|
|
349
360
|
"""
|
|
350
|
-
|
|
361
|
+
_init_rich_logging()
|
|
351
362
|
logger = logging.getLogger(name)
|
|
352
363
|
# print("Logger is", logger)
|
|
353
364
|
return cast(CustomLogger, logger)
|
|
@@ -355,12 +366,3 @@ def get_logger(name: str) -> CustomLogger:
|
|
|
355
366
|
|
|
356
367
|
def get_log_file_stream():
|
|
357
368
|
return _file_handler.stream
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
@cache
|
|
361
|
-
def init_rich_logging():
|
|
362
|
-
rich.reconfigure(theme=get_theme(), highlighter=get_highlighter())
|
|
363
|
-
|
|
364
|
-
logging.setLoggerClass(CustomLogger)
|
|
365
|
-
|
|
366
|
-
reload_rich_logging_setup()
|
kash/config/logger_basic.py
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import sys
|
|
3
|
-
from logging import FileHandler, Formatter
|
|
3
|
+
from logging import FileHandler, Formatter, LogRecord
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
from kash.config.settings import LogLevel
|
|
7
|
+
from kash.config.suppress_warnings import demote_warnings
|
|
7
8
|
|
|
8
9
|
# Basic logging setup for non-interactive logging, like on a server.
|
|
9
10
|
# For richer logging, see logger.py.
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
class SuppressedWarningsStreamHandler(logging.StreamHandler):
|
|
14
|
+
def emit(self, record: LogRecord):
|
|
15
|
+
demote_warnings(record, level=logging.DEBUG)
|
|
16
|
+
super().emit(record)
|
|
17
|
+
|
|
18
|
+
|
|
12
19
|
def basic_file_handler(path: Path, level: LogLevel) -> logging.FileHandler:
|
|
13
20
|
handler = logging.FileHandler(path)
|
|
14
21
|
handler.setLevel(level.value)
|
|
@@ -17,13 +24,13 @@ def basic_file_handler(path: Path, level: LogLevel) -> logging.FileHandler:
|
|
|
17
24
|
|
|
18
25
|
|
|
19
26
|
def basic_stderr_handler(level: LogLevel) -> logging.StreamHandler:
|
|
20
|
-
handler =
|
|
27
|
+
handler = SuppressedWarningsStreamHandler(stream=sys.stderr)
|
|
21
28
|
handler.setLevel(level.value)
|
|
22
29
|
handler.setFormatter(Formatter("%(asctime)s %(levelname).1s %(name)s - %(message)s"))
|
|
23
30
|
return handler
|
|
24
31
|
|
|
25
32
|
|
|
26
|
-
def basic_logging_setup(
|
|
33
|
+
def basic_logging_setup(log_path: Path | None, level: LogLevel):
|
|
27
34
|
"""
|
|
28
35
|
Set up basic logging to a file and to stderr.
|
|
29
36
|
"""
|
|
@@ -31,8 +38,8 @@ def basic_logging_setup(file_log_path: Path | None, level: LogLevel):
|
|
|
31
38
|
for h in root_logger.handlers[:]:
|
|
32
39
|
root_logger.removeHandler(h)
|
|
33
40
|
|
|
34
|
-
if
|
|
35
|
-
file_handler: FileHandler = basic_file_handler(
|
|
41
|
+
if log_path:
|
|
42
|
+
file_handler: FileHandler = basic_file_handler(log_path, level)
|
|
36
43
|
root_logger.addHandler(file_handler)
|
|
37
44
|
|
|
38
45
|
stderr_handler = basic_stderr_handler(level)
|
kash/config/server_config.py
CHANGED
|
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def create_server_config(
|
|
10
|
-
app: Callable[..., Any], host: str, port: int,
|
|
10
|
+
app: Callable[..., Any], host: str, port: int, _server_name: str, log_path: Path
|
|
11
11
|
) -> "uvicorn.Config":
|
|
12
12
|
"""
|
|
13
13
|
Create a common server configuration for both local and MCP servers.
|
|
@@ -43,11 +43,11 @@ def create_server_config(
|
|
|
43
43
|
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
|
44
44
|
"uvicorn.error": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
|
45
45
|
"uvicorn.access": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
|
46
|
-
f"kash.{server_name}": {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
46
|
+
# f"kash.{server_name}": {
|
|
47
|
+
# "handlers": ["default"],
|
|
48
|
+
# "level": "INFO",
|
|
49
|
+
# "propagate": False,
|
|
50
|
+
# },
|
|
51
51
|
},
|
|
52
52
|
},
|
|
53
53
|
)
|