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.
Files changed (269) hide show
  1. kash/__init__.py +2 -0
  2. kash/__main__.py +4 -0
  3. kash/actions/__init__.py +55 -0
  4. kash/actions/core/assistant_chat.py +45 -0
  5. kash/actions/core/chat.py +90 -0
  6. kash/actions/core/format_markdown_template.py +92 -0
  7. kash/actions/core/markdownify.py +29 -0
  8. kash/actions/core/readability.py +27 -0
  9. kash/actions/core/show_webpage.py +28 -0
  10. kash/actions/core/strip_html.py +28 -0
  11. kash/actions/core/summarize_as_bullets.py +53 -0
  12. kash/actions/core/webpage_config.py +21 -0
  13. kash/actions/core/webpage_generate.py +29 -0
  14. kash/actions/meta/write_instructions.py +39 -0
  15. kash/actions/meta/write_new_action.py +157 -0
  16. kash/commands/__init__.py +21 -0
  17. kash/commands/base/basic_file_commands.py +183 -0
  18. kash/commands/base/browser_commands.py +62 -0
  19. kash/commands/base/debug_commands.py +214 -0
  20. kash/commands/base/diff_commands.py +90 -0
  21. kash/commands/base/files_command.py +408 -0
  22. kash/commands/base/general_commands.py +104 -0
  23. kash/commands/base/global_state_commands.py +41 -0
  24. kash/commands/base/logs_commands.py +92 -0
  25. kash/commands/base/reformat_command.py +54 -0
  26. kash/commands/base/search_command.py +65 -0
  27. kash/commands/base/show_command.py +69 -0
  28. kash/commands/extras/utils_commands.py +27 -0
  29. kash/commands/help/assistant_commands.py +97 -0
  30. kash/commands/help/doc_commands.py +226 -0
  31. kash/commands/help/help_commands.py +133 -0
  32. kash/commands/workspace/selection_commands.py +200 -0
  33. kash/commands/workspace/workspace_commands.py +640 -0
  34. kash/concepts/concept_formats.py +23 -0
  35. kash/concepts/embeddings.py +130 -0
  36. kash/concepts/text_similarity.py +108 -0
  37. kash/config/__init__.py +4 -0
  38. kash/config/api_keys.py +84 -0
  39. kash/config/capture_output.py +77 -0
  40. kash/config/colors.py +279 -0
  41. kash/config/init.py +18 -0
  42. kash/config/lazy_imports.py +22 -0
  43. kash/config/logger.py +355 -0
  44. kash/config/logger_basic.py +35 -0
  45. kash/config/logo.txt +4 -0
  46. kash/config/logo_fancy.txt +4 -0
  47. kash/config/server_config.py +51 -0
  48. kash/config/settings.py +196 -0
  49. kash/config/setup.py +51 -0
  50. kash/config/suppress_warnings.py +27 -0
  51. kash/config/text_styles.py +426 -0
  52. kash/docs/__init__.py +0 -0
  53. kash/docs/all_docs.py +58 -0
  54. kash/docs/load_actions_info.py +28 -0
  55. kash/docs/load_api_docs.py +13 -0
  56. kash/docs/load_help_topics.py +47 -0
  57. kash/docs/load_source_code.py +125 -0
  58. kash/docs/markdown/api_docs_template.md +42 -0
  59. kash/docs/markdown/assistant_instructions_template.md +114 -0
  60. kash/docs/markdown/readme_template.md +26 -0
  61. kash/docs/markdown/topics/a1_what_is_kash.md +76 -0
  62. kash/docs/markdown/topics/a2_progress.md +96 -0
  63. kash/docs/markdown/topics/a3_installation.md +119 -0
  64. kash/docs/markdown/topics/a4_getting_started.md +300 -0
  65. kash/docs/markdown/topics/a5_tips_for_use_with_other_tools.md +83 -0
  66. kash/docs/markdown/topics/b0_philosophy_of_kash.md +177 -0
  67. kash/docs/markdown/topics/b1_kash_overview.md +124 -0
  68. kash/docs/markdown/topics/b2_workspace_and_file_formats.md +61 -0
  69. kash/docs/markdown/topics/b3_modern_shell_tool_recommendations.md +83 -0
  70. kash/docs/markdown/topics/b4_faq.md +166 -0
  71. kash/docs/markdown/warning.md +7 -0
  72. kash/docs/markdown/welcome.md +9 -0
  73. kash/docs_base/docs_base.py +85 -0
  74. kash/docs_base/load_custom_command_info.py +27 -0
  75. kash/docs_base/load_faqs.py +48 -0
  76. kash/docs_base/load_recipe_snippets.py +48 -0
  77. kash/docs_base/recipes/general_system_commands.ksh +10 -0
  78. kash/docs_base/recipes/python_dev_commands.ksh +7 -0
  79. kash/docs_base/recipes/tldr_standard_commands.ksh +2144 -0
  80. kash/errors.py +176 -0
  81. kash/exec/__init__.py +16 -0
  82. kash/exec/action_decorators.py +412 -0
  83. kash/exec/action_exec.py +457 -0
  84. kash/exec/action_registry.py +123 -0
  85. kash/exec/combiners.py +127 -0
  86. kash/exec/command_exec.py +34 -0
  87. kash/exec/command_registry.py +72 -0
  88. kash/exec/fetch_url_metadata.py +71 -0
  89. kash/exec/history.py +44 -0
  90. kash/exec/llm_transforms.py +121 -0
  91. kash/exec/precondition_checks.py +71 -0
  92. kash/exec/precondition_registry.py +43 -0
  93. kash/exec/preconditions.py +152 -0
  94. kash/exec/resolve_args.py +123 -0
  95. kash/exec/shell_callable_action.py +90 -0
  96. kash/exec_model/__init__.py +0 -0
  97. kash/exec_model/args_model.py +93 -0
  98. kash/exec_model/commands_model.py +163 -0
  99. kash/exec_model/script_model.py +161 -0
  100. kash/exec_model/shell_model.py +21 -0
  101. kash/file_storage/__init__.py +0 -0
  102. kash/file_storage/file_store.py +642 -0
  103. kash/file_storage/item_file_format.py +152 -0
  104. kash/file_storage/metadata_dirs.py +108 -0
  105. kash/file_storage/mtime_cache.py +108 -0
  106. kash/file_storage/persisted_yaml.py +37 -0
  107. kash/file_storage/store_cache_warmer.py +37 -0
  108. kash/file_storage/store_filenames.py +53 -0
  109. kash/form_input/__init__.py +0 -0
  110. kash/form_input/prompt_input.py +44 -0
  111. kash/help/__init__.py +0 -0
  112. kash/help/assistant.py +324 -0
  113. kash/help/assistant_instructions.py +68 -0
  114. kash/help/assistant_output.py +43 -0
  115. kash/help/docstring_utils.py +111 -0
  116. kash/help/function_param_info.py +44 -0
  117. kash/help/help_embeddings.py +85 -0
  118. kash/help/help_lookups.py +60 -0
  119. kash/help/help_pages.py +122 -0
  120. kash/help/help_printing.py +169 -0
  121. kash/help/help_types.py +247 -0
  122. kash/help/recommended_commands.py +143 -0
  123. kash/help/tldr_help.py +296 -0
  124. kash/llm_utils/__init__.py +0 -0
  125. kash/llm_utils/chat_format.py +413 -0
  126. kash/llm_utils/clean_headings.py +65 -0
  127. kash/llm_utils/fuzzy_parsing.py +119 -0
  128. kash/llm_utils/language_models.py +178 -0
  129. kash/llm_utils/llm_completion.py +172 -0
  130. kash/llm_utils/llm_messages.py +36 -0
  131. kash/local_server/__init__.py +2 -0
  132. kash/local_server/local_server.py +183 -0
  133. kash/local_server/local_server_commands.py +55 -0
  134. kash/local_server/local_server_routes.py +306 -0
  135. kash/local_server/local_url_formatters.py +169 -0
  136. kash/local_server/port_tools.py +67 -0
  137. kash/local_server/rich_html_template.py +12 -0
  138. kash/mcp/__init__.py +2 -0
  139. kash/mcp/mcp_main.py +67 -0
  140. kash/mcp/mcp_server_commands.py +57 -0
  141. kash/mcp/mcp_server_routes.py +256 -0
  142. kash/mcp/mcp_server_sse.py +143 -0
  143. kash/mcp/mcp_server_stdio.py +45 -0
  144. kash/media_base/__init__.py +0 -0
  145. kash/media_base/audio_processing.py +27 -0
  146. kash/media_base/media_cache.py +178 -0
  147. kash/media_base/media_services.py +112 -0
  148. kash/media_base/media_tools.py +48 -0
  149. kash/media_base/services/local_file_media.py +165 -0
  150. kash/media_base/speech_transcription.py +224 -0
  151. kash/media_base/timestamp_citations.py +80 -0
  152. kash/model/__init__.py +73 -0
  153. kash/model/actions_model.py +633 -0
  154. kash/model/assistant_response_model.py +87 -0
  155. kash/model/compound_actions_model.py +188 -0
  156. kash/model/graph_model.py +92 -0
  157. kash/model/items_model.py +821 -0
  158. kash/model/language_list.py +39 -0
  159. kash/model/llm_actions_model.py +63 -0
  160. kash/model/media_model.py +124 -0
  161. kash/model/operations_model.py +176 -0
  162. kash/model/params_model.py +435 -0
  163. kash/model/paths_model.py +458 -0
  164. kash/model/preconditions_model.py +98 -0
  165. kash/shell/__init__.py +0 -0
  166. kash/shell/completions/completion_scoring.py +280 -0
  167. kash/shell/completions/completion_types.py +154 -0
  168. kash/shell/completions/shell_completions.py +277 -0
  169. kash/shell/file_icons/color_for_format.py +70 -0
  170. kash/shell/file_icons/nerd_icons.py +946 -0
  171. kash/shell/output/__init__.py +0 -0
  172. kash/shell/output/kerm_code_utils.py +59 -0
  173. kash/shell/output/kerm_codes.py +588 -0
  174. kash/shell/output/kmarkdown.py +117 -0
  175. kash/shell/output/shell_output.py +477 -0
  176. kash/shell/ui/__init__.py +0 -0
  177. kash/shell/ui/shell_results.py +118 -0
  178. kash/shell/ui/shell_syntax.py +26 -0
  179. kash/shell/utils/exception_printing.py +50 -0
  180. kash/shell/utils/native_utils.py +240 -0
  181. kash/shell/utils/osc_utils.py +95 -0
  182. kash/shell/utils/shell_function_wrapper.py +204 -0
  183. kash/shell/utils/sys_tool_deps.py +289 -0
  184. kash/shell/utils/terminal_images.py +133 -0
  185. kash/shell_main.py +67 -0
  186. kash/text_handling/custom_sliding_transforms.py +266 -0
  187. kash/text_handling/doc_normalization.py +64 -0
  188. kash/text_handling/markdown_util.py +167 -0
  189. kash/text_handling/unified_diffs.py +138 -0
  190. kash/utils/__init__.py +4 -0
  191. kash/utils/common/__init__.py +4 -0
  192. kash/utils/common/atomic_var.py +147 -0
  193. kash/utils/common/format_utils.py +81 -0
  194. kash/utils/common/function_inspect.py +178 -0
  195. kash/utils/common/import_utils.py +89 -0
  196. kash/utils/common/lazyobject.py +144 -0
  197. kash/utils/common/obj_replace.py +78 -0
  198. kash/utils/common/parse_key_vals.py +85 -0
  199. kash/utils/common/parse_shell_args.py +348 -0
  200. kash/utils/common/stack_traces.py +49 -0
  201. kash/utils/common/string_replace.py +93 -0
  202. kash/utils/common/string_template.py +101 -0
  203. kash/utils/common/task_stack.py +162 -0
  204. kash/utils/common/type_utils.py +137 -0
  205. kash/utils/common/uniquifier.py +95 -0
  206. kash/utils/common/url.py +155 -0
  207. kash/utils/file_utils/__init__.py +3 -0
  208. kash/utils/file_utils/dir_size.py +48 -0
  209. kash/utils/file_utils/file_ext.py +86 -0
  210. kash/utils/file_utils/file_formats.py +134 -0
  211. kash/utils/file_utils/file_formats_model.py +408 -0
  212. kash/utils/file_utils/file_sort_filter.py +235 -0
  213. kash/utils/file_utils/file_walk.py +163 -0
  214. kash/utils/file_utils/filename_parsing.py +99 -0
  215. kash/utils/file_utils/git_tools.py +19 -0
  216. kash/utils/file_utils/ignore_files.py +166 -0
  217. kash/utils/file_utils/path_utils.py +36 -0
  218. kash/utils/lang_utils/__init__.py +0 -0
  219. kash/utils/lang_utils/capitalization.py +128 -0
  220. kash/utils/lang_utils/inflection.py +18 -0
  221. kash/utils/rich_custom/__init__.py +3 -0
  222. kash/utils/rich_custom/ansi_cell_len.py +72 -0
  223. kash/utils/rich_custom/rich_char_transform.py +89 -0
  224. kash/utils/rich_custom/rich_indent.py +69 -0
  225. kash/utils/rich_custom/rich_markdown_fork.py +771 -0
  226. kash/version.py +31 -0
  227. kash/web_content/canon_url.py +24 -0
  228. kash/web_content/dir_store.py +103 -0
  229. kash/web_content/file_cache_utils.py +117 -0
  230. kash/web_content/local_file_cache.py +247 -0
  231. kash/web_content/web_extract.py +55 -0
  232. kash/web_content/web_extract_justext.py +86 -0
  233. kash/web_content/web_extract_readabilipy.py +23 -0
  234. kash/web_content/web_fetch.py +101 -0
  235. kash/web_content/web_page_model.py +28 -0
  236. kash/web_gen/__init__.py +4 -0
  237. kash/web_gen/tabbed_webpage.py +149 -0
  238. kash/web_gen/template_render.py +29 -0
  239. kash/web_gen/templates/base_styles.css.jinja +192 -0
  240. kash/web_gen/templates/base_webpage.html.jinja +124 -0
  241. kash/web_gen/templates/content_styles.css.jinja +194 -0
  242. kash/web_gen/templates/explain_view.html.jinja +49 -0
  243. kash/web_gen/templates/item_view.html.jinja +294 -0
  244. kash/web_gen/templates/tabbed_webpage.html.jinja +49 -0
  245. kash/workspaces/__init__.py +13 -0
  246. kash/workspaces/param_state.py +24 -0
  247. kash/workspaces/selections.py +333 -0
  248. kash/workspaces/source_items.py +88 -0
  249. kash/workspaces/workspace_importing.py +56 -0
  250. kash/workspaces/workspace_names.py +33 -0
  251. kash/workspaces/workspace_output.py +154 -0
  252. kash/workspaces/workspace_registry.py +78 -0
  253. kash/workspaces/workspaces.py +197 -0
  254. kash/xonsh_custom/custom_shell.py +366 -0
  255. kash/xonsh_custom/customize_prompt.py +197 -0
  256. kash/xonsh_custom/customize_xonsh.py +112 -0
  257. kash/xonsh_custom/shell_load_commands.py +152 -0
  258. kash/xonsh_custom/shell_which.py +64 -0
  259. kash/xonsh_custom/xonsh_completers.py +715 -0
  260. kash/xonsh_custom/xonsh_env.py +28 -0
  261. kash/xonsh_custom/xonsh_modern_tools.py +56 -0
  262. kash/xonsh_custom/xonsh_ranking_completer.py +152 -0
  263. kash/xontrib/fnm.py +120 -0
  264. kash/xontrib/kash_extension.py +61 -0
  265. kash_shell-0.3.0.dist-info/METADATA +757 -0
  266. kash_shell-0.3.0.dist-info/RECORD +269 -0
  267. kash_shell-0.3.0.dist-info/WHEEL +4 -0
  268. kash_shell-0.3.0.dist-info/entry_points.txt +3 -0
  269. 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
@@ -0,0 +1,4 @@
1
+ """
2
+ App specific setup, settings, and configs. Should have no dependencies outside
3
+ this module except for a very few things in the util module.
4
+ """
@@ -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