kash-shell 0.3.0__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/__init__.py +2 -0
- kash/__main__.py +4 -0
- kash/actions/__init__.py +55 -0
- kash/actions/core/assistant_chat.py +45 -0
- kash/actions/core/chat.py +90 -0
- kash/actions/core/format_markdown_template.py +92 -0
- kash/actions/core/markdownify.py +29 -0
- kash/actions/core/readability.py +27 -0
- kash/actions/core/show_webpage.py +28 -0
- kash/actions/core/strip_html.py +28 -0
- kash/actions/core/summarize_as_bullets.py +53 -0
- kash/actions/core/webpage_config.py +21 -0
- kash/actions/core/webpage_generate.py +29 -0
- kash/actions/meta/write_instructions.py +39 -0
- kash/actions/meta/write_new_action.py +157 -0
- kash/commands/__init__.py +21 -0
- kash/commands/base/basic_file_commands.py +183 -0
- kash/commands/base/browser_commands.py +62 -0
- kash/commands/base/debug_commands.py +214 -0
- kash/commands/base/diff_commands.py +90 -0
- kash/commands/base/files_command.py +408 -0
- kash/commands/base/general_commands.py +104 -0
- kash/commands/base/global_state_commands.py +41 -0
- kash/commands/base/logs_commands.py +92 -0
- kash/commands/base/reformat_command.py +54 -0
- kash/commands/base/search_command.py +65 -0
- kash/commands/base/show_command.py +69 -0
- kash/commands/extras/utils_commands.py +27 -0
- kash/commands/help/assistant_commands.py +97 -0
- kash/commands/help/doc_commands.py +226 -0
- kash/commands/help/help_commands.py +133 -0
- kash/commands/workspace/selection_commands.py +200 -0
- kash/commands/workspace/workspace_commands.py +640 -0
- kash/concepts/concept_formats.py +23 -0
- kash/concepts/embeddings.py +130 -0
- kash/concepts/text_similarity.py +108 -0
- kash/config/__init__.py +4 -0
- kash/config/api_keys.py +84 -0
- kash/config/capture_output.py +77 -0
- kash/config/colors.py +279 -0
- kash/config/init.py +18 -0
- kash/config/lazy_imports.py +22 -0
- kash/config/logger.py +355 -0
- kash/config/logger_basic.py +35 -0
- kash/config/logo.txt +4 -0
- kash/config/logo_fancy.txt +4 -0
- kash/config/server_config.py +51 -0
- kash/config/settings.py +196 -0
- kash/config/setup.py +51 -0
- kash/config/suppress_warnings.py +27 -0
- kash/config/text_styles.py +426 -0
- kash/docs/__init__.py +0 -0
- kash/docs/all_docs.py +58 -0
- kash/docs/load_actions_info.py +28 -0
- kash/docs/load_api_docs.py +13 -0
- kash/docs/load_help_topics.py +47 -0
- kash/docs/load_source_code.py +125 -0
- kash/docs/markdown/api_docs_template.md +42 -0
- kash/docs/markdown/assistant_instructions_template.md +114 -0
- kash/docs/markdown/readme_template.md +26 -0
- kash/docs/markdown/topics/a1_what_is_kash.md +76 -0
- kash/docs/markdown/topics/a2_progress.md +96 -0
- kash/docs/markdown/topics/a3_installation.md +119 -0
- kash/docs/markdown/topics/a4_getting_started.md +300 -0
- kash/docs/markdown/topics/a5_tips_for_use_with_other_tools.md +83 -0
- kash/docs/markdown/topics/b0_philosophy_of_kash.md +177 -0
- kash/docs/markdown/topics/b1_kash_overview.md +124 -0
- kash/docs/markdown/topics/b2_workspace_and_file_formats.md +61 -0
- kash/docs/markdown/topics/b3_modern_shell_tool_recommendations.md +83 -0
- kash/docs/markdown/topics/b4_faq.md +166 -0
- kash/docs/markdown/warning.md +7 -0
- kash/docs/markdown/welcome.md +9 -0
- kash/docs_base/docs_base.py +85 -0
- kash/docs_base/load_custom_command_info.py +27 -0
- kash/docs_base/load_faqs.py +48 -0
- kash/docs_base/load_recipe_snippets.py +48 -0
- kash/docs_base/recipes/general_system_commands.ksh +10 -0
- kash/docs_base/recipes/python_dev_commands.ksh +7 -0
- kash/docs_base/recipes/tldr_standard_commands.ksh +2144 -0
- kash/errors.py +176 -0
- kash/exec/__init__.py +16 -0
- kash/exec/action_decorators.py +412 -0
- kash/exec/action_exec.py +457 -0
- kash/exec/action_registry.py +123 -0
- kash/exec/combiners.py +127 -0
- kash/exec/command_exec.py +34 -0
- kash/exec/command_registry.py +72 -0
- kash/exec/fetch_url_metadata.py +71 -0
- kash/exec/history.py +44 -0
- kash/exec/llm_transforms.py +121 -0
- kash/exec/precondition_checks.py +71 -0
- kash/exec/precondition_registry.py +43 -0
- kash/exec/preconditions.py +152 -0
- kash/exec/resolve_args.py +123 -0
- kash/exec/shell_callable_action.py +90 -0
- kash/exec_model/__init__.py +0 -0
- kash/exec_model/args_model.py +93 -0
- kash/exec_model/commands_model.py +163 -0
- kash/exec_model/script_model.py +161 -0
- kash/exec_model/shell_model.py +21 -0
- kash/file_storage/__init__.py +0 -0
- kash/file_storage/file_store.py +642 -0
- kash/file_storage/item_file_format.py +152 -0
- kash/file_storage/metadata_dirs.py +108 -0
- kash/file_storage/mtime_cache.py +108 -0
- kash/file_storage/persisted_yaml.py +37 -0
- kash/file_storage/store_cache_warmer.py +37 -0
- kash/file_storage/store_filenames.py +53 -0
- kash/form_input/__init__.py +0 -0
- kash/form_input/prompt_input.py +44 -0
- kash/help/__init__.py +0 -0
- kash/help/assistant.py +324 -0
- kash/help/assistant_instructions.py +68 -0
- kash/help/assistant_output.py +43 -0
- kash/help/docstring_utils.py +111 -0
- kash/help/function_param_info.py +44 -0
- kash/help/help_embeddings.py +85 -0
- kash/help/help_lookups.py +60 -0
- kash/help/help_pages.py +122 -0
- kash/help/help_printing.py +169 -0
- kash/help/help_types.py +247 -0
- kash/help/recommended_commands.py +143 -0
- kash/help/tldr_help.py +296 -0
- kash/llm_utils/__init__.py +0 -0
- kash/llm_utils/chat_format.py +413 -0
- kash/llm_utils/clean_headings.py +65 -0
- kash/llm_utils/fuzzy_parsing.py +119 -0
- kash/llm_utils/language_models.py +178 -0
- kash/llm_utils/llm_completion.py +172 -0
- kash/llm_utils/llm_messages.py +36 -0
- kash/local_server/__init__.py +2 -0
- kash/local_server/local_server.py +183 -0
- kash/local_server/local_server_commands.py +55 -0
- kash/local_server/local_server_routes.py +306 -0
- kash/local_server/local_url_formatters.py +169 -0
- kash/local_server/port_tools.py +67 -0
- kash/local_server/rich_html_template.py +12 -0
- kash/mcp/__init__.py +2 -0
- kash/mcp/mcp_main.py +67 -0
- kash/mcp/mcp_server_commands.py +57 -0
- kash/mcp/mcp_server_routes.py +256 -0
- kash/mcp/mcp_server_sse.py +143 -0
- kash/mcp/mcp_server_stdio.py +45 -0
- kash/media_base/__init__.py +0 -0
- kash/media_base/audio_processing.py +27 -0
- kash/media_base/media_cache.py +178 -0
- kash/media_base/media_services.py +112 -0
- kash/media_base/media_tools.py +48 -0
- kash/media_base/services/local_file_media.py +165 -0
- kash/media_base/speech_transcription.py +224 -0
- kash/media_base/timestamp_citations.py +80 -0
- kash/model/__init__.py +73 -0
- kash/model/actions_model.py +633 -0
- kash/model/assistant_response_model.py +87 -0
- kash/model/compound_actions_model.py +188 -0
- kash/model/graph_model.py +92 -0
- kash/model/items_model.py +821 -0
- kash/model/language_list.py +39 -0
- kash/model/llm_actions_model.py +63 -0
- kash/model/media_model.py +124 -0
- kash/model/operations_model.py +176 -0
- kash/model/params_model.py +435 -0
- kash/model/paths_model.py +458 -0
- kash/model/preconditions_model.py +98 -0
- kash/shell/__init__.py +0 -0
- kash/shell/completions/completion_scoring.py +280 -0
- kash/shell/completions/completion_types.py +154 -0
- kash/shell/completions/shell_completions.py +277 -0
- kash/shell/file_icons/color_for_format.py +70 -0
- kash/shell/file_icons/nerd_icons.py +946 -0
- kash/shell/output/__init__.py +0 -0
- kash/shell/output/kerm_code_utils.py +59 -0
- kash/shell/output/kerm_codes.py +588 -0
- kash/shell/output/kmarkdown.py +117 -0
- kash/shell/output/shell_output.py +477 -0
- kash/shell/ui/__init__.py +0 -0
- kash/shell/ui/shell_results.py +118 -0
- kash/shell/ui/shell_syntax.py +26 -0
- kash/shell/utils/exception_printing.py +50 -0
- kash/shell/utils/native_utils.py +240 -0
- kash/shell/utils/osc_utils.py +95 -0
- kash/shell/utils/shell_function_wrapper.py +204 -0
- kash/shell/utils/sys_tool_deps.py +289 -0
- kash/shell/utils/terminal_images.py +133 -0
- kash/shell_main.py +67 -0
- kash/text_handling/custom_sliding_transforms.py +266 -0
- kash/text_handling/doc_normalization.py +64 -0
- kash/text_handling/markdown_util.py +167 -0
- kash/text_handling/unified_diffs.py +138 -0
- kash/utils/__init__.py +4 -0
- kash/utils/common/__init__.py +4 -0
- kash/utils/common/atomic_var.py +147 -0
- kash/utils/common/format_utils.py +81 -0
- kash/utils/common/function_inspect.py +178 -0
- kash/utils/common/import_utils.py +89 -0
- kash/utils/common/lazyobject.py +144 -0
- kash/utils/common/obj_replace.py +78 -0
- kash/utils/common/parse_key_vals.py +85 -0
- kash/utils/common/parse_shell_args.py +348 -0
- kash/utils/common/stack_traces.py +49 -0
- kash/utils/common/string_replace.py +93 -0
- kash/utils/common/string_template.py +101 -0
- kash/utils/common/task_stack.py +162 -0
- kash/utils/common/type_utils.py +137 -0
- kash/utils/common/uniquifier.py +95 -0
- kash/utils/common/url.py +155 -0
- kash/utils/file_utils/__init__.py +3 -0
- kash/utils/file_utils/dir_size.py +48 -0
- kash/utils/file_utils/file_ext.py +86 -0
- kash/utils/file_utils/file_formats.py +134 -0
- kash/utils/file_utils/file_formats_model.py +408 -0
- kash/utils/file_utils/file_sort_filter.py +235 -0
- kash/utils/file_utils/file_walk.py +163 -0
- kash/utils/file_utils/filename_parsing.py +99 -0
- kash/utils/file_utils/git_tools.py +19 -0
- kash/utils/file_utils/ignore_files.py +166 -0
- kash/utils/file_utils/path_utils.py +36 -0
- kash/utils/lang_utils/__init__.py +0 -0
- kash/utils/lang_utils/capitalization.py +128 -0
- kash/utils/lang_utils/inflection.py +18 -0
- kash/utils/rich_custom/__init__.py +3 -0
- kash/utils/rich_custom/ansi_cell_len.py +72 -0
- kash/utils/rich_custom/rich_char_transform.py +89 -0
- kash/utils/rich_custom/rich_indent.py +69 -0
- kash/utils/rich_custom/rich_markdown_fork.py +771 -0
- kash/version.py +31 -0
- kash/web_content/canon_url.py +24 -0
- kash/web_content/dir_store.py +103 -0
- kash/web_content/file_cache_utils.py +117 -0
- kash/web_content/local_file_cache.py +247 -0
- kash/web_content/web_extract.py +55 -0
- kash/web_content/web_extract_justext.py +86 -0
- kash/web_content/web_extract_readabilipy.py +23 -0
- kash/web_content/web_fetch.py +101 -0
- kash/web_content/web_page_model.py +28 -0
- kash/web_gen/__init__.py +4 -0
- kash/web_gen/tabbed_webpage.py +149 -0
- kash/web_gen/template_render.py +29 -0
- kash/web_gen/templates/base_styles.css.jinja +192 -0
- kash/web_gen/templates/base_webpage.html.jinja +124 -0
- kash/web_gen/templates/content_styles.css.jinja +194 -0
- kash/web_gen/templates/explain_view.html.jinja +49 -0
- kash/web_gen/templates/item_view.html.jinja +294 -0
- kash/web_gen/templates/tabbed_webpage.html.jinja +49 -0
- kash/workspaces/__init__.py +13 -0
- kash/workspaces/param_state.py +24 -0
- kash/workspaces/selections.py +333 -0
- kash/workspaces/source_items.py +88 -0
- kash/workspaces/workspace_importing.py +56 -0
- kash/workspaces/workspace_names.py +33 -0
- kash/workspaces/workspace_output.py +154 -0
- kash/workspaces/workspace_registry.py +78 -0
- kash/workspaces/workspaces.py +197 -0
- kash/xonsh_custom/custom_shell.py +366 -0
- kash/xonsh_custom/customize_prompt.py +197 -0
- kash/xonsh_custom/customize_xonsh.py +112 -0
- kash/xonsh_custom/shell_load_commands.py +152 -0
- kash/xonsh_custom/shell_which.py +64 -0
- kash/xonsh_custom/xonsh_completers.py +715 -0
- kash/xonsh_custom/xonsh_env.py +28 -0
- kash/xonsh_custom/xonsh_modern_tools.py +56 -0
- kash/xonsh_custom/xonsh_ranking_completer.py +152 -0
- kash/xontrib/fnm.py +120 -0
- kash/xontrib/kash_extension.py +61 -0
- kash_shell-0.3.0.dist-info/METADATA +757 -0
- kash_shell-0.3.0.dist-info/RECORD +269 -0
- kash_shell-0.3.0.dist-info/WHEEL +4 -0
- kash_shell-0.3.0.dist-info/entry_points.txt +3 -0
- kash_shell-0.3.0.dist-info/licenses/LICENSE +664 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from litellm import embedding
|
|
10
|
+
from pydantic.dataclasses import dataclass
|
|
11
|
+
from strif import abbrev_list
|
|
12
|
+
|
|
13
|
+
from kash.config.logger import get_logger
|
|
14
|
+
from kash.llm_utils.language_models import DEFAULT_EMBEDDING_MODEL
|
|
15
|
+
|
|
16
|
+
log = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
BATCH_SIZE = 1024
|
|
20
|
+
|
|
21
|
+
Key: TypeAlias = str
|
|
22
|
+
|
|
23
|
+
KeyVal: TypeAlias = tuple[Key, str]
|
|
24
|
+
"""
|
|
25
|
+
A key-value pair where the key is a unique identifier (such as the path)
|
|
26
|
+
and the value is the text to embed.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Embeddings:
|
|
32
|
+
"""
|
|
33
|
+
Embedded string values. Each string value has a unique key (e.g. its id or title or for
|
|
34
|
+
small texts, the text itself).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
data: dict[Key, tuple[str, list[float]]]
|
|
38
|
+
"""Mapping of key to text and embedding."""
|
|
39
|
+
|
|
40
|
+
def as_iterable(self) -> Iterable[tuple[Key, str, list[float]]]:
|
|
41
|
+
return ((key, text, emb) for key, (text, emb) in self.data.items())
|
|
42
|
+
|
|
43
|
+
def as_df(self) -> pd.DataFrame:
|
|
44
|
+
keys, texts, embeddings = zip(
|
|
45
|
+
*[(key, text, emb) for key, (text, emb) in self.data.items()], strict=False
|
|
46
|
+
)
|
|
47
|
+
return pd.DataFrame(
|
|
48
|
+
{
|
|
49
|
+
"key": keys,
|
|
50
|
+
"text": texts,
|
|
51
|
+
"embedding": embeddings,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, key: Key) -> tuple[str, list[float]]:
|
|
56
|
+
if key in self.data:
|
|
57
|
+
return self.data[key]
|
|
58
|
+
else:
|
|
59
|
+
raise KeyError(f"Key '{key}' not found in embeddings")
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def embed(cls, keyvals: list[KeyVal], model=DEFAULT_EMBEDDING_MODEL) -> Embeddings:
|
|
63
|
+
data = {}
|
|
64
|
+
log.message(
|
|
65
|
+
"Embedding %d texts (model %s, batch size %s)…",
|
|
66
|
+
len(keyvals),
|
|
67
|
+
model.litellm_name,
|
|
68
|
+
BATCH_SIZE,
|
|
69
|
+
)
|
|
70
|
+
for batch_start in range(0, len(keyvals), BATCH_SIZE):
|
|
71
|
+
batch_end = batch_start + BATCH_SIZE
|
|
72
|
+
batch = keyvals[batch_start:batch_end]
|
|
73
|
+
keys = [kv[0] for kv in batch]
|
|
74
|
+
texts = [kv[1] for kv in batch]
|
|
75
|
+
|
|
76
|
+
response = embedding(model=model.litellm_name, input=texts)
|
|
77
|
+
if not response.data:
|
|
78
|
+
raise ValueError("No embedding response data")
|
|
79
|
+
|
|
80
|
+
batch_embeddings = [e["embedding"] for e in response.data]
|
|
81
|
+
data.update(
|
|
82
|
+
{
|
|
83
|
+
key: (text, emb)
|
|
84
|
+
for key, text, emb in zip(keys, texts, batch_embeddings, strict=False)
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
log.info(
|
|
89
|
+
"Embedded batch %d-%d: %s",
|
|
90
|
+
batch_start,
|
|
91
|
+
batch_end,
|
|
92
|
+
abbrev_list(texts),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return cls(data=data)
|
|
96
|
+
|
|
97
|
+
def to_csv(self, path: Path) -> None:
|
|
98
|
+
self.as_df().to_csv(path, index=False)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def read_from_csv(cls, path: Path) -> Embeddings:
|
|
102
|
+
df = pd.read_csv(path)
|
|
103
|
+
df["embedding"] = df["embedding"].apply(ast.literal_eval)
|
|
104
|
+
data = {row["key"]: (row["text"], row["embedding"]) for _, row in df.iterrows()}
|
|
105
|
+
return cls(data=data) # pyright: ignore
|
|
106
|
+
|
|
107
|
+
def to_npz(self, path: Path) -> None:
|
|
108
|
+
"""Save embeddings in numpy's compressed format."""
|
|
109
|
+
import numpy as np
|
|
110
|
+
|
|
111
|
+
keys: list[Key] = list(self.data.keys())
|
|
112
|
+
texts: list[str] = [self.data[k][0] for k in keys]
|
|
113
|
+
embeddings = np.array([self.data[k][1] for k in keys])
|
|
114
|
+
np.savez_compressed(path, keys=keys, texts=texts, embeddings=embeddings)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def read_from_npz(cls, path: Path) -> Embeddings:
|
|
118
|
+
"""Load embeddings from numpy's compressed format."""
|
|
119
|
+
import numpy as np
|
|
120
|
+
|
|
121
|
+
with np.load(path) as data:
|
|
122
|
+
loaded_data = {
|
|
123
|
+
k: (t, e.tolist())
|
|
124
|
+
for k, t, e in zip(data["keys"], data["texts"], data["embeddings"], strict=False)
|
|
125
|
+
}
|
|
126
|
+
return cls(data=loaded_data)
|
|
127
|
+
|
|
128
|
+
def __str__(self) -> str:
|
|
129
|
+
dims = -1 if len(self.data) == 0 else len(next(iter(self.data))[1])
|
|
130
|
+
return f"Embeddings({len(self.data)} items, {dims} dimensions)"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import litellm
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from funlog import log_calls, tally_calls
|
|
4
|
+
from litellm import embedding
|
|
5
|
+
from litellm.types.utils import EmbeddingResponse
|
|
6
|
+
from scipy import spatial
|
|
7
|
+
|
|
8
|
+
from kash.concepts.embeddings import Embeddings
|
|
9
|
+
from kash.config.logger import get_logger
|
|
10
|
+
from kash.errors import ApiResultError
|
|
11
|
+
from kash.llm_utils.language_models import DEFAULT_EMBEDDING_MODEL, EmbeddingModel
|
|
12
|
+
|
|
13
|
+
log = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def sort_by_length(values: list[str]) -> list[str]:
|
|
17
|
+
return sorted(values, key=lambda x: (len(x), x))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def cosine_relatedness(x, y):
|
|
21
|
+
return 1 - spatial.distance.cosine(x, y)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@log_calls(level="info", show_return_value=False)
|
|
25
|
+
def embed_query(model: EmbeddingModel, query: str) -> EmbeddingResponse:
|
|
26
|
+
try:
|
|
27
|
+
response = embedding(model=model.litellm_name, input=[query])
|
|
28
|
+
except litellm.exceptions.APIError as e:
|
|
29
|
+
log.info("API error embedding query: %s", e)
|
|
30
|
+
raise ApiResultError(str(e))
|
|
31
|
+
if not response.data:
|
|
32
|
+
log.info("API error embedding query, got: %s", response)
|
|
33
|
+
raise ApiResultError("No embedding response data")
|
|
34
|
+
return response
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@log_calls(level="info", show_return_value=False)
|
|
38
|
+
def rank_by_relatedness(
|
|
39
|
+
query: str,
|
|
40
|
+
embeddings: Embeddings,
|
|
41
|
+
relatedness_fn=cosine_relatedness,
|
|
42
|
+
model=DEFAULT_EMBEDDING_MODEL,
|
|
43
|
+
top_n: int = -1,
|
|
44
|
+
) -> list[tuple[str, str, float]]:
|
|
45
|
+
"""
|
|
46
|
+
Returns a list of strings and relatednesses, sorted from most related to least.
|
|
47
|
+
"""
|
|
48
|
+
response = embed_query(model, query)
|
|
49
|
+
|
|
50
|
+
query_embedding = response.data[0]["embedding"]
|
|
51
|
+
|
|
52
|
+
scored_strings = [
|
|
53
|
+
(key, text, relatedness_fn(query_embedding, emb))
|
|
54
|
+
for key, text, emb in embeddings.as_iterable()
|
|
55
|
+
]
|
|
56
|
+
scored_strings.sort(key=lambda x: x[2], reverse=True)
|
|
57
|
+
|
|
58
|
+
return scored_strings[:top_n]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@tally_calls(level="warning", min_total_runtime=5, if_slower_than=10)
|
|
62
|
+
def relate_texts_by_embedding(
|
|
63
|
+
embeddings: Embeddings, relatedness_fn=cosine_relatedness
|
|
64
|
+
) -> pd.DataFrame:
|
|
65
|
+
log.message("Computing relatedness matrix of %d text embeddings…", len(embeddings.data))
|
|
66
|
+
|
|
67
|
+
keys = [key for key, _, _ in embeddings.as_iterable()]
|
|
68
|
+
relatedness_matrix = pd.DataFrame(index=keys, columns=keys) # pyright: ignore
|
|
69
|
+
|
|
70
|
+
for i, (key1, _, emb1) in enumerate(embeddings.as_iterable()):
|
|
71
|
+
for j, (key2, _, emb2) in enumerate(embeddings.as_iterable()):
|
|
72
|
+
if i <= j:
|
|
73
|
+
score = relatedness_fn(emb1, emb2)
|
|
74
|
+
relatedness_matrix.at[key1, key2] = score
|
|
75
|
+
relatedness_matrix.at[key2, key1] = score
|
|
76
|
+
|
|
77
|
+
# Fill diagonal (self-relatedness).
|
|
78
|
+
for key in keys:
|
|
79
|
+
relatedness_matrix.at[key, key] = 1.0
|
|
80
|
+
|
|
81
|
+
return relatedness_matrix
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def find_related_pairs(
|
|
85
|
+
relatedness_matrix: pd.DataFrame, threshold: float = 0.9
|
|
86
|
+
) -> list[tuple[str, str, float]]:
|
|
87
|
+
log.message(
|
|
88
|
+
"Finding near duplicates among %s items (threshold %s)",
|
|
89
|
+
relatedness_matrix.shape[0],
|
|
90
|
+
threshold,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
pairs: list[tuple[str, str, float]] = []
|
|
94
|
+
keys = relatedness_matrix.index.tolist()
|
|
95
|
+
|
|
96
|
+
for i, key1 in enumerate(keys):
|
|
97
|
+
for j, key2 in enumerate(keys):
|
|
98
|
+
if i < j:
|
|
99
|
+
relatedness = relatedness_matrix.at[key1, key2]
|
|
100
|
+
if relatedness >= threshold:
|
|
101
|
+
# Put shortest one first.
|
|
102
|
+
[short_key, long_key] = sort_by_length([key1, key2])
|
|
103
|
+
pairs.append((short_key, long_key, relatedness))
|
|
104
|
+
|
|
105
|
+
# Sort with highest relatedness first.
|
|
106
|
+
pairs.sort(key=lambda x: x[2], reverse=True)
|
|
107
|
+
|
|
108
|
+
return pairs
|
kash/config/__init__.py
ADDED
kash/config/api_keys.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from dotenv import find_dotenv, load_dotenv
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
from kash.shell.output.shell_output import cprint, format_success_or_failure
|
|
8
|
+
from kash.utils.common.atomic_var import AtomicVar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Api(str, Enum):
|
|
12
|
+
openai = "OPENAI_API_KEY"
|
|
13
|
+
anthropic = "ANTHROPIC_API_KEY"
|
|
14
|
+
gemini = "GEMINI_API_KEY"
|
|
15
|
+
xai = "XAI_API_KEY"
|
|
16
|
+
deepseek = "DEEPSEEK_API_KEY"
|
|
17
|
+
mistral = "MISTRAL_API_KEY"
|
|
18
|
+
perplexityai = "PERPLEXITYAI_API_KEY"
|
|
19
|
+
deepgram = "DEEPGRAM_API_KEY"
|
|
20
|
+
groq = "GROQ_API_KEY"
|
|
21
|
+
firecrawl = "FIRECRAWL_API_KEY"
|
|
22
|
+
exa = "EXA_API_KEY"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
RECOMMENDED_APIS = [
|
|
26
|
+
Api.openai,
|
|
27
|
+
Api.anthropic,
|
|
28
|
+
Api.deepgram,
|
|
29
|
+
Api.groq,
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def api_setup() -> str:
|
|
34
|
+
dotenv_path = find_dotenv(usecwd=True)
|
|
35
|
+
if dotenv_path:
|
|
36
|
+
load_dotenv(dotenv_path)
|
|
37
|
+
return dotenv_path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_log_api_setup_done = AtomicVar(False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def warn_if_missing_api_keys(keys: list[Api] = RECOMMENDED_APIS) -> list[Api]:
|
|
44
|
+
from kash.config.logger import get_logger
|
|
45
|
+
|
|
46
|
+
log = get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
missing_keys = [api for api in keys if api.value not in os.environ]
|
|
49
|
+
if missing_keys:
|
|
50
|
+
log.warning(
|
|
51
|
+
"Missing recommended API keys (check .env file or set them?): %s",
|
|
52
|
+
", ".join(missing_keys),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return missing_keys
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def print_api_key_setup(once: bool = False) -> None:
|
|
59
|
+
if once and _log_api_setup_done:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
dotenv_path = api_setup()
|
|
63
|
+
|
|
64
|
+
cprint(
|
|
65
|
+
Text.assemble(
|
|
66
|
+
format_success_or_failure(
|
|
67
|
+
value=bool(dotenv_path),
|
|
68
|
+
true_str=f"Found .env file: {dotenv_path}",
|
|
69
|
+
false_str="No .env file found. Set up your API keys in a .env file.",
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def is_set(key: str) -> bool:
|
|
75
|
+
value = os.environ.get(key, None)
|
|
76
|
+
return bool(value and value.strip() and "changeme" not in value)
|
|
77
|
+
|
|
78
|
+
texts = [format_success_or_failure(is_set(api.value), api.name) for api in Api]
|
|
79
|
+
|
|
80
|
+
cprint(Text.assemble("API keys found: ", Text(" ").join(texts)))
|
|
81
|
+
|
|
82
|
+
warn_if_missing_api_keys()
|
|
83
|
+
|
|
84
|
+
_log_api_setup_done.set(True)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from contextlib import redirect_stdout
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from kash.config.logger import get_logger, record_console
|
|
8
|
+
|
|
9
|
+
log = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class CapturedOutput:
|
|
14
|
+
"""
|
|
15
|
+
Holds captured console and stdout output.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
console_text: str
|
|
19
|
+
stdout_text: str
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def logs(self) -> str:
|
|
23
|
+
logs = ""
|
|
24
|
+
if self.console_text:
|
|
25
|
+
logs += f"Console:\n{self.console_text}\n\n"
|
|
26
|
+
if self.stdout_text:
|
|
27
|
+
logs += f"Stdout:\n{self.stdout_text}\n\n"
|
|
28
|
+
return logs
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class captured_output:
|
|
32
|
+
"""
|
|
33
|
+
Context manager for capturing console and stdout output.
|
|
34
|
+
Returns a CapturedOutput instance when exiting the context.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self._output = None
|
|
39
|
+
self.stdout_buffer = None
|
|
40
|
+
self.console = None
|
|
41
|
+
self.stdout_redirector = None
|
|
42
|
+
|
|
43
|
+
def __enter__(self) -> captured_output:
|
|
44
|
+
self.stdout_buffer = io.StringIO()
|
|
45
|
+
self.console = record_console().__enter__()
|
|
46
|
+
self.stdout_redirector = redirect_stdout(self.stdout_buffer)
|
|
47
|
+
self.stdout_redirector.__enter__()
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
51
|
+
console_text = self.console.export_text() if self.console else ""
|
|
52
|
+
stdout_text = self.stdout_buffer.getvalue() if self.stdout_buffer else ""
|
|
53
|
+
|
|
54
|
+
# Create output object before cleanup.
|
|
55
|
+
self._output = CapturedOutput(console_text=console_text, stdout_text=stdout_text)
|
|
56
|
+
|
|
57
|
+
if console_text:
|
|
58
|
+
log.info("Captured console:\n%s", console_text)
|
|
59
|
+
if stdout_text:
|
|
60
|
+
log.info("Captured stdout:\n%s", stdout_text)
|
|
61
|
+
|
|
62
|
+
# Clean up the context managers using stored instances.
|
|
63
|
+
stdout_suppress = True
|
|
64
|
+
if self.stdout_redirector:
|
|
65
|
+
stdout_suppress = self.stdout_redirector.__exit__(exc_type, exc_val, exc_tb)
|
|
66
|
+
console_suppress = True
|
|
67
|
+
if self.console:
|
|
68
|
+
console_suppress = self.console.__exit__(exc_type, exc_val, exc_tb)
|
|
69
|
+
|
|
70
|
+
# Propagate exceptions unless suppressed by both handlers.
|
|
71
|
+
return stdout_suppress and console_suppress
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def output(self) -> CapturedOutput:
|
|
75
|
+
if self._output is None:
|
|
76
|
+
raise RuntimeError("Cannot access output before context manager exits")
|
|
77
|
+
return self._output
|
kash/config/colors.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
from colour import Color
|
|
4
|
+
from rich.terminal_theme import TerminalTheme
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def hsl_to_hex(hsl_string: str) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Convert an HSL/HSLA string to an RGB hex string or RGBA value.
|
|
10
|
+
"hsl(134, 43%, 60%)" -> "#6dbd6d"
|
|
11
|
+
"hsla(220, 14%, 96%, 0.86)" -> "rgba(244, 245, 247, 0.86)"
|
|
12
|
+
"""
|
|
13
|
+
is_hsla = hsl_string.startswith("hsla")
|
|
14
|
+
hsl_values = (
|
|
15
|
+
hsl_string.replace("hsla(", "")
|
|
16
|
+
.replace("hsl(", "")
|
|
17
|
+
.replace(")", "")
|
|
18
|
+
.replace("%", "")
|
|
19
|
+
.split(",")
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if is_hsla:
|
|
23
|
+
hue, saturation, lightness, alpha = (float(value.strip()) for value in hsl_values)
|
|
24
|
+
else:
|
|
25
|
+
hue, saturation, lightness = (float(value.strip()) for value in hsl_values)
|
|
26
|
+
alpha = 1.0
|
|
27
|
+
|
|
28
|
+
saturation /= 100
|
|
29
|
+
lightness /= 100
|
|
30
|
+
|
|
31
|
+
color = Color(hsl=(hue / 360, saturation, lightness))
|
|
32
|
+
|
|
33
|
+
if alpha < 1:
|
|
34
|
+
rgb = color.rgb
|
|
35
|
+
return f"rgba({int(rgb[0] * 255)}, {int(rgb[1] * 255)}, {int(rgb[2] * 255)}, {alpha})"
|
|
36
|
+
return color.hex_l
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def hex_to_int(hex_string: str) -> tuple[int, int, int]:
|
|
40
|
+
"""
|
|
41
|
+
Convert a hex color string to RGB integers.
|
|
42
|
+
Supports both 6-digit and 3-digit hex codes:
|
|
43
|
+
"#6dbd6d" -> (109, 189, 109)
|
|
44
|
+
"#333" -> (51, 51, 51) # equivalent to #333333
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
hex_string = hex_string.lstrip("#")
|
|
48
|
+
if len(hex_string) == 3:
|
|
49
|
+
hex_string = "".join(c + c for c in hex_string)
|
|
50
|
+
if len(hex_string) != 6:
|
|
51
|
+
raise ValueError(f"Invalid hex color length: {hex_string}")
|
|
52
|
+
|
|
53
|
+
r = int(hex_string[0:2], 16)
|
|
54
|
+
g = int(hex_string[2:4], 16)
|
|
55
|
+
b = int(hex_string[4:6], 16)
|
|
56
|
+
|
|
57
|
+
return (r, g, b)
|
|
58
|
+
except Exception:
|
|
59
|
+
raise ValueError(f"Could not parse hex color: `{hex_string}`")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Main colors.
|
|
63
|
+
terminal_colors = SimpleNamespace(
|
|
64
|
+
# Based on:
|
|
65
|
+
# https://rootloops.sh?sugar=8&colors=7&sogginess=5&flavor=2&fruit=9&milk=1
|
|
66
|
+
# Some tools only like hex colors so convert them at once.
|
|
67
|
+
# black
|
|
68
|
+
black_darkest=hsl_to_hex("hsl(0, 0%, 7%)"),
|
|
69
|
+
black_darker=hsl_to_hex("hsl(0, 0%, 13%)"),
|
|
70
|
+
black_dark=hsl_to_hex("hsl(0, 0%, 30%)"),
|
|
71
|
+
black_light=hsl_to_hex("hsl(0, 0%, 73%)"),
|
|
72
|
+
black_lighter=hsl_to_hex("hsl(0, 0%, 90%)"),
|
|
73
|
+
# red
|
|
74
|
+
red_darkest=hsl_to_hex("hsl(7, 50%, 35%)"),
|
|
75
|
+
red_darker=hsl_to_hex("hsl(7, 43%, 48%)"),
|
|
76
|
+
red_dark=hsl_to_hex("hsl(7, 73%, 72%)"),
|
|
77
|
+
red_light=hsl_to_hex("hsl(7, 87%, 85%)"),
|
|
78
|
+
red_lighter=hsl_to_hex("hsl(7, 95%, 94%)"),
|
|
79
|
+
# green
|
|
80
|
+
green_darkest=hsl_to_hex("hsl(134, 35%, 34%)"),
|
|
81
|
+
green_darker=hsl_to_hex("hsl(134, 37%, 46%)"),
|
|
82
|
+
green_dark=hsl_to_hex("hsl(134, 43%, 60%)"),
|
|
83
|
+
green_light=hsl_to_hex("hsl(134, 53%, 73%)"),
|
|
84
|
+
green_lighter=hsl_to_hex("hsl(134, 70%, 90%)"),
|
|
85
|
+
# yellow
|
|
86
|
+
yellow_darkest=hsl_to_hex("hsl(44, 47%, 34%)"),
|
|
87
|
+
yellow_darker=hsl_to_hex("hsl(44, 47%, 44%)"),
|
|
88
|
+
yellow_dark=hsl_to_hex("hsl(44, 54%, 55%)"),
|
|
89
|
+
yellow_light=hsl_to_hex("hsl(44, 74%, 76%)"),
|
|
90
|
+
yellow_lighter=hsl_to_hex("hsl(44, 80%, 90%)"),
|
|
91
|
+
# blue
|
|
92
|
+
blue_darkest=hsl_to_hex("hsl(225, 35%, 34%)"),
|
|
93
|
+
blue_darker=hsl_to_hex("hsl(225, 46%, 52%)"),
|
|
94
|
+
blue_dark=hsl_to_hex("hsl(225, 71%, 76%)"),
|
|
95
|
+
blue_light=hsl_to_hex("hsl(225, 86%, 88%)"),
|
|
96
|
+
blue_lighter=hsl_to_hex("hsl(225, 90%, 94%)"),
|
|
97
|
+
# magenta
|
|
98
|
+
magenta_darkest=hsl_to_hex("hsl(305, 35%, 34%)"),
|
|
99
|
+
magenta_darker=hsl_to_hex("hsl(305, 38%, 55%)"),
|
|
100
|
+
magenta_dark=hsl_to_hex("hsl(305, 54%, 71%)"),
|
|
101
|
+
magenta_light=hsl_to_hex("hsl(305, 68%, 85%)"),
|
|
102
|
+
magenta_lighter=hsl_to_hex("hsl(305, 96%, 95%)"),
|
|
103
|
+
# cyan
|
|
104
|
+
cyan_darkest=hsl_to_hex("hsl(188, 60%, 32%)"),
|
|
105
|
+
cyan_darker=hsl_to_hex("hsl(188, 60%, 41%)"),
|
|
106
|
+
cyan_dark=hsl_to_hex("hsl(188, 58%, 57%)"),
|
|
107
|
+
cyan_light=hsl_to_hex("hsl(188, 52%, 76%)"),
|
|
108
|
+
cyan_lighter=hsl_to_hex("hsl(188, 52%, 92%)"),
|
|
109
|
+
# white
|
|
110
|
+
white_darkest=hsl_to_hex("hsl(240, 6%, 60%)"),
|
|
111
|
+
white_darker=hsl_to_hex("hsl(240, 6%, 72%)"),
|
|
112
|
+
white_dark=hsl_to_hex("hsl(240, 6%, 87%)"),
|
|
113
|
+
white_light=hsl_to_hex("hsl(240, 6%, 94%)"),
|
|
114
|
+
white_lighter=hsl_to_hex("hsl(240, 6%, 98%)"),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Only support dark terminal colors for now.
|
|
119
|
+
terminal_dark = SimpleNamespace(
|
|
120
|
+
foreground="#fff",
|
|
121
|
+
background="#000",
|
|
122
|
+
border=hsl_to_hex("hsl(188, 8%, 33%)"),
|
|
123
|
+
cursor=hsl_to_hex("hsl(305, 84%, 68%)"),
|
|
124
|
+
input=hsl_to_hex("hsl(305, 92%, 95%)"),
|
|
125
|
+
input_form=hsl_to_hex("hsl(188, 52%, 76%)"),
|
|
126
|
+
**terminal_colors.__dict__,
|
|
127
|
+
)
|
|
128
|
+
terminal = terminal_dark
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Web light colors.
|
|
132
|
+
web_light_translucent = SimpleNamespace(
|
|
133
|
+
primary=hsl_to_hex("hsl(188, 31%, 41%)"),
|
|
134
|
+
primary_light=hsl_to_hex("hsl(188, 40%, 62%)"),
|
|
135
|
+
secondary=hsl_to_hex("hsl(188, 12%, 28%)"),
|
|
136
|
+
bg=hsl_to_hex("hsla(44, 6%, 100%, 0.75)"),
|
|
137
|
+
bg_header=hsl_to_hex("hsla(188, 42%, 70%, 0.2)"),
|
|
138
|
+
bg_alt=hsl_to_hex("hsla(44, 28%, 90%, 0.3)"),
|
|
139
|
+
text=hsl_to_hex("hsl(188, 39%, 11%)"),
|
|
140
|
+
border=hsl_to_hex("hsl(188, 8%, 50%)"),
|
|
141
|
+
border_hint=hsl_to_hex("hsla(188, 8%, 72%, 0.7)"),
|
|
142
|
+
border_accent=hsl_to_hex("hsla(305, 18%, 65%, 0.85)"),
|
|
143
|
+
hover=hsl_to_hex("hsl(188, 12%, 84%)"),
|
|
144
|
+
hover_bg=hsl_to_hex("hsla(188, 7%, 94%, 0.8)"),
|
|
145
|
+
hint=hsl_to_hex("hsl(188, 11%, 65%)"),
|
|
146
|
+
tooltip_bg=hsl_to_hex("hsla(188, 6%, 37%, 0.7)"),
|
|
147
|
+
popover_bg=hsl_to_hex("hsla(188, 6%, 37%, 0.7)"),
|
|
148
|
+
bright=hsl_to_hex("hsl(134, 43%, 60%)"),
|
|
149
|
+
selection="hsla(225, 61%, 82%, 0.80)",
|
|
150
|
+
scrollbar=hsl_to_hex("hsla(189, 12%, 55%, 0.9)"),
|
|
151
|
+
scrollbar_hover=hsl_to_hex("hsla(190, 12%, 38%, 0.9)"),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
rich_terminal_dark = TerminalTheme(
|
|
156
|
+
hex_to_int(terminal_dark.background),
|
|
157
|
+
hex_to_int(terminal_dark.foreground),
|
|
158
|
+
# normal colors [black, red, green, yellow, blue, magenta, cyan, white]
|
|
159
|
+
[
|
|
160
|
+
hex_to_int(terminal_dark.black_dark),
|
|
161
|
+
hex_to_int(terminal_dark.red_dark),
|
|
162
|
+
hex_to_int(terminal_dark.green_dark),
|
|
163
|
+
hex_to_int(terminal_dark.yellow_dark),
|
|
164
|
+
hex_to_int(terminal_dark.blue_dark),
|
|
165
|
+
hex_to_int(terminal_dark.magenta_dark),
|
|
166
|
+
hex_to_int(terminal_dark.cyan_dark),
|
|
167
|
+
hex_to_int(terminal_dark.white_dark),
|
|
168
|
+
],
|
|
169
|
+
# bright colors [black, red, green, yellow, blue, magenta, cyan, white]
|
|
170
|
+
[
|
|
171
|
+
hex_to_int(terminal_dark.black_lighter),
|
|
172
|
+
hex_to_int(terminal_dark.red_lighter),
|
|
173
|
+
hex_to_int(terminal_dark.green_lighter),
|
|
174
|
+
hex_to_int(terminal_dark.yellow_lighter),
|
|
175
|
+
hex_to_int(terminal_dark.blue_lighter),
|
|
176
|
+
hex_to_int(terminal_dark.magenta_lighter),
|
|
177
|
+
hex_to_int(terminal_dark.cyan_lighter),
|
|
178
|
+
hex_to_int(terminal_dark.white_lighter),
|
|
179
|
+
],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
rich_terminal_light = TerminalTheme(
|
|
183
|
+
hex_to_int(terminal_dark.background),
|
|
184
|
+
hex_to_int(terminal_dark.foreground),
|
|
185
|
+
# normal colors [black, red, green, yellow, blue, magenta, cyan, white]
|
|
186
|
+
[
|
|
187
|
+
hex_to_int(terminal_dark.black_dark),
|
|
188
|
+
hex_to_int(terminal_dark.red_dark),
|
|
189
|
+
hex_to_int(terminal_dark.green_dark),
|
|
190
|
+
hex_to_int(terminal_dark.yellow_dark),
|
|
191
|
+
hex_to_int(terminal_dark.blue_dark),
|
|
192
|
+
hex_to_int(terminal_dark.magenta_dark),
|
|
193
|
+
hex_to_int(terminal_dark.cyan_dark),
|
|
194
|
+
hex_to_int(terminal_dark.white_dark),
|
|
195
|
+
],
|
|
196
|
+
# bright colors [black, red, green, yellow, blue, magenta, cyan, white]
|
|
197
|
+
[
|
|
198
|
+
hex_to_int(terminal_dark.black_darker),
|
|
199
|
+
hex_to_int(terminal_dark.red_darker),
|
|
200
|
+
hex_to_int(terminal_dark.green_darker),
|
|
201
|
+
hex_to_int(terminal_dark.yellow_darker),
|
|
202
|
+
hex_to_int(terminal_dark.blue_darker),
|
|
203
|
+
hex_to_int(terminal_dark.magenta_darker),
|
|
204
|
+
hex_to_int(terminal_dark.cyan_darker),
|
|
205
|
+
hex_to_int(terminal_dark.white_darker),
|
|
206
|
+
],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# We default to light colors for Rich content in HTML.
|
|
210
|
+
rich_terminal = rich_terminal_light
|
|
211
|
+
|
|
212
|
+
# Only support light web colors for now.
|
|
213
|
+
web = web_light_translucent
|
|
214
|
+
|
|
215
|
+
# Logical colors
|
|
216
|
+
logical = SimpleNamespace(
|
|
217
|
+
concept_dark=terminal.green_dark,
|
|
218
|
+
concept_light=terminal.green_light,
|
|
219
|
+
concept_lighter=terminal.green_lighter,
|
|
220
|
+
doc_dark=terminal.blue_dark,
|
|
221
|
+
doc_light=terminal.blue_light,
|
|
222
|
+
doc_lighter=terminal.blue_lighter,
|
|
223
|
+
resource_dark=terminal.cyan_dark,
|
|
224
|
+
resource_light=terminal.cyan_light,
|
|
225
|
+
resource_lighter=terminal.cyan_lighter,
|
|
226
|
+
link_dark=terminal.yellow_dark,
|
|
227
|
+
link_light=terminal.yellow_light,
|
|
228
|
+
link_lighter=terminal.yellow_lighter,
|
|
229
|
+
other=terminal.white_dark,
|
|
230
|
+
other_light=terminal.white_light,
|
|
231
|
+
other_lighter=terminal.white_lighter,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def consolidate_color_vars(overrides: dict[str, str] | None = None) -> dict[str, str]:
|
|
236
|
+
"""
|
|
237
|
+
Consolidate all color variables into a single dictionary with appropriate prefixes.
|
|
238
|
+
Terminal variables have no prefix, while web and logical variables have "color-" prefix.
|
|
239
|
+
"""
|
|
240
|
+
if overrides is None:
|
|
241
|
+
overrides = {}
|
|
242
|
+
return {
|
|
243
|
+
# Terminal variables (no prefix)
|
|
244
|
+
**terminal.__dict__,
|
|
245
|
+
# Web and logical variables with "color-" prefix
|
|
246
|
+
**{f"color-{k}": v for k, v in web.__dict__.items()},
|
|
247
|
+
**{f"color-{k}": v for k, v in logical.__dict__.items()},
|
|
248
|
+
# Overrides take precedence (assume they already have correct prefixes)
|
|
249
|
+
**overrides,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def normalize_var_names(variables: dict[str, str]) -> dict[str, str]:
|
|
254
|
+
"""
|
|
255
|
+
Normalize variable names from Python style to CSS style.
|
|
256
|
+
Example: color_bg -> color-bg
|
|
257
|
+
"""
|
|
258
|
+
return {k.replace("_", "-"): v for k, v in variables.items()}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def generate_css_vars(overrides: dict[str, str] | None = None) -> str:
|
|
262
|
+
"""
|
|
263
|
+
Generate CSS variables for the terminal and web colors.
|
|
264
|
+
"""
|
|
265
|
+
if overrides is None:
|
|
266
|
+
overrides = {}
|
|
267
|
+
normalized_vars = normalize_var_names(consolidate_color_vars(overrides))
|
|
268
|
+
|
|
269
|
+
# Generate the CSS.
|
|
270
|
+
css_variables = ":root {\n"
|
|
271
|
+
for name, value in normalized_vars.items():
|
|
272
|
+
css_variables += f" --{name}: {value};\n"
|
|
273
|
+
css_variables += "}"
|
|
274
|
+
|
|
275
|
+
return css_variables
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
print(generate_css_vars())
|
kash/config/init.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from kash.model.actions_model import Action
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def kash_reload_all() -> tuple[dict[str, Callable], dict[str, type["Action"]]]:
|
|
9
|
+
"""
|
|
10
|
+
Import all kash modules that define actions and commands.
|
|
11
|
+
"""
|
|
12
|
+
from kash.exec.action_registry import reload_all_action_classes
|
|
13
|
+
from kash.exec.command_registry import get_all_commands
|
|
14
|
+
|
|
15
|
+
commands = get_all_commands()
|
|
16
|
+
actions = reload_all_action_classes()
|
|
17
|
+
|
|
18
|
+
return commands, actions
|