kash-shell 0.3.9__py3-none-any.whl → 0.3.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. kash/actions/__init__.py +4 -4
  2. kash/actions/core/markdownify.py +5 -2
  3. kash/actions/core/readability.py +5 -2
  4. kash/actions/core/render_as_html.py +18 -0
  5. kash/actions/core/webpage_config.py +12 -4
  6. kash/commands/__init__.py +8 -20
  7. kash/commands/base/basic_file_commands.py +15 -0
  8. kash/commands/base/debug_commands.py +13 -0
  9. kash/commands/base/general_commands.py +21 -16
  10. kash/commands/base/logs_commands.py +4 -2
  11. kash/commands/base/model_commands.py +8 -8
  12. kash/commands/base/search_command.py +3 -2
  13. kash/commands/base/show_command.py +5 -3
  14. kash/commands/extras/parse_uv_lock.py +186 -0
  15. kash/commands/help/doc_commands.py +2 -31
  16. kash/commands/help/welcome.py +33 -0
  17. kash/commands/workspace/selection_commands.py +11 -6
  18. kash/commands/workspace/workspace_commands.py +18 -15
  19. kash/config/colors.py +2 -0
  20. kash/config/env_settings.py +14 -1
  21. kash/config/init.py +2 -2
  22. kash/config/logger.py +59 -56
  23. kash/config/logger_basic.py +3 -3
  24. kash/config/settings.py +116 -57
  25. kash/config/setup.py +28 -12
  26. kash/config/text_styles.py +3 -13
  27. kash/docs/load_api_docs.py +2 -1
  28. kash/docs/markdown/topics/a3_getting_started.md +3 -2
  29. kash/{concepts → embeddings}/text_similarity.py +2 -2
  30. kash/exec/__init__.py +20 -3
  31. kash/exec/action_decorators.py +18 -4
  32. kash/exec/action_exec.py +41 -23
  33. kash/exec/action_registry.py +13 -48
  34. kash/exec/command_registry.py +2 -1
  35. kash/exec/fetch_url_metadata.py +4 -6
  36. kash/exec/importing.py +56 -0
  37. kash/exec/llm_transforms.py +6 -7
  38. kash/exec/precondition_registry.py +2 -1
  39. kash/exec/preconditions.py +16 -1
  40. kash/exec/shell_callable_action.py +33 -19
  41. kash/file_storage/file_store.py +23 -10
  42. kash/file_storage/item_file_format.py +5 -2
  43. kash/file_storage/metadata_dirs.py +11 -2
  44. kash/help/assistant.py +1 -1
  45. kash/help/assistant_instructions.py +2 -1
  46. kash/help/help_embeddings.py +2 -2
  47. kash/help/help_printing.py +7 -11
  48. kash/llm_utils/clean_headings.py +1 -1
  49. kash/llm_utils/llm_api_keys.py +4 -4
  50. kash/llm_utils/llm_features.py +68 -0
  51. kash/llm_utils/llm_messages.py +1 -2
  52. kash/llm_utils/llm_names.py +1 -1
  53. kash/llm_utils/llms.py +8 -3
  54. kash/local_server/__init__.py +5 -2
  55. kash/local_server/local_server.py +8 -5
  56. kash/local_server/local_server_commands.py +2 -2
  57. kash/local_server/local_url_formatters.py +1 -1
  58. kash/mcp/__init__.py +5 -2
  59. kash/mcp/mcp_cli.py +5 -5
  60. kash/mcp/mcp_server_commands.py +5 -5
  61. kash/mcp/mcp_server_routes.py +5 -5
  62. kash/mcp/mcp_server_sse.py +4 -2
  63. kash/media_base/media_cache.py +8 -8
  64. kash/media_base/media_services.py +1 -1
  65. kash/media_base/media_tools.py +6 -6
  66. kash/media_base/services/local_file_media.py +2 -2
  67. kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -110
  68. kash/media_base/transcription_format.py +73 -0
  69. kash/media_base/transcription_whisper.py +38 -0
  70. kash/model/__init__.py +73 -5
  71. kash/model/actions_model.py +38 -4
  72. kash/model/concept_model.py +30 -0
  73. kash/model/items_model.py +44 -7
  74. kash/model/params_model.py +24 -0
  75. kash/shell/completions/completion_scoring.py +37 -5
  76. kash/shell/output/kerm_codes.py +1 -2
  77. kash/shell/output/shell_formatting.py +14 -4
  78. kash/shell/shell_main.py +2 -2
  79. kash/shell/utils/exception_printing.py +6 -0
  80. kash/shell/utils/native_utils.py +26 -20
  81. kash/text_handling/custom_sliding_transforms.py +12 -4
  82. kash/text_handling/doc_normalization.py +6 -2
  83. kash/text_handling/markdown_render.py +117 -0
  84. kash/text_handling/markdown_utils.py +204 -0
  85. kash/utils/common/import_utils.py +12 -3
  86. kash/utils/common/type_utils.py +0 -29
  87. kash/utils/common/url.py +27 -3
  88. kash/utils/errors.py +6 -0
  89. kash/utils/file_utils/file_formats.py +2 -2
  90. kash/utils/file_utils/file_formats_model.py +3 -0
  91. kash/web_content/dir_store.py +1 -2
  92. kash/web_content/file_cache_utils.py +37 -10
  93. kash/web_content/file_processing.py +68 -0
  94. kash/web_content/local_file_cache.py +12 -9
  95. kash/web_content/web_extract.py +8 -3
  96. kash/web_content/web_fetch.py +12 -4
  97. kash/web_gen/tabbed_webpage.py +5 -2
  98. kash/web_gen/templates/base_styles.css.jinja +120 -14
  99. kash/web_gen/templates/base_webpage.html.jinja +60 -13
  100. kash/web_gen/templates/content_styles.css.jinja +4 -2
  101. kash/web_gen/templates/item_view.html.jinja +2 -2
  102. kash/web_gen/templates/tabbed_webpage.html.jinja +1 -2
  103. kash/workspaces/__init__.py +15 -2
  104. kash/workspaces/selections.py +18 -3
  105. kash/workspaces/source_items.py +0 -1
  106. kash/workspaces/workspaces.py +5 -11
  107. kash/xonsh_custom/command_nl_utils.py +40 -19
  108. kash/xonsh_custom/custom_shell.py +43 -11
  109. kash/xonsh_custom/customize_prompt.py +39 -21
  110. kash/xonsh_custom/load_into_xonsh.py +22 -25
  111. kash/xonsh_custom/shell_load_commands.py +2 -2
  112. kash/xonsh_custom/xonsh_completers.py +2 -249
  113. kash/xonsh_custom/xonsh_keybindings.py +282 -0
  114. kash/xonsh_custom/xonsh_modern_tools.py +3 -3
  115. kash/xontrib/kash_extension.py +5 -6
  116. {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/METADATA +8 -6
  117. {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/RECORD +122 -123
  118. kash/concepts/concept_formats.py +0 -23
  119. kash/shell/clideps/api_keys.py +0 -100
  120. kash/shell/clideps/dotenv_setup.py +0 -115
  121. kash/shell/clideps/dotenv_utils.py +0 -98
  122. kash/shell/clideps/pkg_deps.py +0 -257
  123. kash/shell/clideps/platforms.py +0 -11
  124. kash/shell/clideps/terminal_features.py +0 -56
  125. kash/shell/utils/osc_utils.py +0 -95
  126. kash/shell/utils/terminal_images.py +0 -133
  127. kash/text_handling/markdown_util.py +0 -167
  128. kash/utils/common/atomic_var.py +0 -171
  129. kash/utils/common/string_replace.py +0 -93
  130. kash/utils/common/string_template.py +0 -101
  131. /kash/{concepts → embeddings}/cosine.py +0 -0
  132. /kash/{concepts → embeddings}/embeddings.py +0 -0
  133. {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/WHEEL +0 -0
  134. {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/entry_points.txt +0 -0
  135. {kash_shell-0.3.9.dist-info → kash_shell-0.3.10.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,7 @@ from chopdiff.html import has_timestamp
7
7
 
8
8
  from kash.exec.precondition_registry import kash_precondition
9
9
  from kash.model.items_model import Item, ItemType
10
- from kash.text_handling.markdown_util import extract_bullet_points
10
+ from kash.text_handling.markdown_utils import extract_bullet_points
11
11
  from kash.utils.file_utils.file_formats_model import Format
12
12
 
13
13
 
@@ -16,6 +16,21 @@ def is_resource(item: Item) -> bool:
16
16
  return item.type == ItemType.resource
17
17
 
18
18
 
19
+ @kash_precondition
20
+ def is_doc_resource(item: Item) -> bool:
21
+ return bool(is_resource(item) and item.format and item.format.is_doc)
22
+
23
+
24
+ @kash_precondition
25
+ def is_html_resource(item: Item) -> bool:
26
+ return bool(is_resource(item) and item.format and item.format == Format.html)
27
+
28
+
29
+ @kash_precondition
30
+ def is_docx_resource(item: Item) -> bool:
31
+ return bool(is_resource(item) and item.format and item.format == Format.docx)
32
+
33
+
19
34
  @kash_precondition
20
35
  def is_concept(item: Item) -> bool:
21
36
  return item.type == ItemType.concept
@@ -1,5 +1,6 @@
1
1
  from funlog import log_tallies
2
2
 
3
+ from kash.config.env_settings import KashEnv
3
4
  from kash.config.logger import get_console, get_logger
4
5
  from kash.config.text_styles import COLOR_ERROR, SPINNER
5
6
  from kash.exec.action_exec import run_action_with_shell_context
@@ -31,34 +32,43 @@ class ShellCallableAction:
31
32
  def __call__(self, args: list[str]) -> ShellResult | None:
32
33
  from kash.commands.help import help_commands
33
34
 
34
- action_cls = self.action_cls
35
- PrintHooks.before_shell_action_run()
35
+ log.debug("ShellCallableAction: %s: %s", self.action_cls.name, args)
36
36
 
37
- shell_args = parse_shell_args(args)
38
-
39
- # We will instantiate the action later but we create an unconfigured
40
- # instance for help/info.
41
- action = action_cls.create(None)
42
- if shell_args.show_help:
43
- print_action_help(action, verbose=True)
44
- return ShellResult()
45
- elif shell_args.options.get("show_source", False):
46
- return help_commands.source_code(action_cls.name)
47
-
48
- # Handle --rerun option at action invocation time.
49
- rerun = bool(shell_args.options.get("rerun", False))
50
-
51
- log.info("Action shell args: %s", shell_args)
52
37
  try:
38
+ action_cls = self.action_cls
39
+ PrintHooks.before_shell_action_run()
40
+
41
+ shell_args = parse_shell_args(args)
42
+
43
+ # We will instantiate the action later but we create an unconfigured
44
+ # instance for help/info.
45
+ action = action_cls.create(None)
46
+ if shell_args.show_help:
47
+ print_action_help(action, verbose=True)
48
+ return ShellResult()
49
+ elif shell_args.options.get("show_source", False):
50
+ return help_commands.source_code(action_cls.name)
51
+
52
+ # Handle --rerun and --refetch options at action invocation time.
53
+ rerun = bool(shell_args.options.get("rerun", False))
54
+ refetch = bool(shell_args.options.get("refetch", False))
55
+ no_format = bool(shell_args.options.get("no_format", False))
56
+
57
+ log.info("Action shell args: %s", shell_args)
53
58
  explicit_values = RawParamValues(shell_args.options)
54
59
  if not action.interactive_input:
55
60
  with get_console().status(f"Running action {action.name}…", spinner=SPINNER):
56
61
  result = run_action_with_shell_context(
57
- action_cls, explicit_values, *shell_args.args, rerun=rerun
62
+ action_cls,
63
+ explicit_values,
64
+ *shell_args.args,
65
+ rerun=rerun,
66
+ refetch=refetch,
67
+ no_format=no_format,
58
68
  )
59
69
  else:
60
70
  result = run_action_with_shell_context(
61
- action_cls, explicit_values, *shell_args.args, rerun=rerun
71
+ action_cls, explicit_values, *shell_args.args, rerun=rerun, refetch=refetch
62
72
  )
63
73
  # We don't return the result to keep the xonsh shell output clean.
64
74
  except NONFATAL_EXCEPTIONS as e:
@@ -66,6 +76,10 @@ class ShellCallableAction:
66
76
  log.error(f"[{COLOR_ERROR}]Action error:[/{COLOR_ERROR}] %s", summarize_traceback(e))
67
77
  log.info("Action error details: %s", e, exc_info=True)
68
78
  return ShellResult(exception=e)
79
+ except Exception as e:
80
+ # Log here while we are in the true call stack (not inside the xonsh call stack).
81
+ log.error("Action error: %s", e, exc_info=KashEnv.KASH_SHOW_TRACEBACK.read_bool(True))
82
+ raise
69
83
  finally:
70
84
  log_tallies(level="warning", if_slower_than=10.0)
71
85
  # output_separator()
@@ -270,10 +270,10 @@ class FileStore(Workspace):
270
270
  return self._tmp_path_for(item), False, None
271
271
  elif item.store_path:
272
272
  return StorePath(item.store_path), True, None
273
- elif item_id in self.id_map:
273
+ elif item_id in self.id_map and self.exists(self.id_map[item_id]):
274
274
  # If this item has an identity and we've saved under that id before, use the same store path.
275
275
  store_path = self.id_map[item_id]
276
- log.info(
276
+ log.warning(
277
277
  "Found existing item with same id:\n%s",
278
278
  fmt_lines([fmt_loc(store_path), item_id]),
279
279
  )
@@ -309,10 +309,16 @@ class FileStore(Workspace):
309
309
  )
310
310
 
311
311
  @log_calls()
312
- def save(self, item: Item, as_tmp: bool = False, overwrite: bool = True) -> StorePath:
312
+ def save(
313
+ self, item: Item, *, overwrite: bool = True, as_tmp: bool = False, no_format: bool = False
314
+ ) -> StorePath:
313
315
  """
314
- Save the item. Uses the store_path if it's already set or generates a new one.
315
- Updates item.store_path.
316
+ Save the item. Uses the `store_path` if it's already set or generates a new one.
317
+ Updates `item.store_path`.
318
+
319
+ If `as_tmp` is true, will save the item to a temporary file.
320
+ If `overwrite` is false, will skip saving if the item already exists.
321
+ If `no_format` is true, will not normalize body text formatting (for Markdown).
316
322
  """
317
323
  # If external file already exists within the workspace, the file is already saved (without metadata).
318
324
  external_path = item.external_path and Path(item.external_path).resolve()
@@ -348,7 +354,7 @@ class FileStore(Workspace):
348
354
  if item.external_path:
349
355
  copyfile_atomic(item.external_path, full_path)
350
356
  else:
351
- write_item(item, full_path)
357
+ write_item(item, full_path, normalize=not no_format)
352
358
  except OSError as e:
353
359
  log.error("Error saving item: %s", e)
354
360
  try:
@@ -398,6 +404,7 @@ class FileStore(Workspace):
398
404
  def import_item(
399
405
  self,
400
406
  locator: Locator,
407
+ *,
401
408
  as_type: ItemType | None = None,
402
409
  reimport: bool = False,
403
410
  ) -> StorePath:
@@ -496,7 +503,9 @@ class FileStore(Workspace):
496
503
  as_type: ItemType | None = None,
497
504
  reimport: bool = False,
498
505
  ) -> list[StorePath]:
499
- return [self.import_item(locator, as_type, reimport) for locator in locators]
506
+ return [
507
+ self.import_item(locator, as_type=as_type, reimport=reimport) for locator in locators
508
+ ]
500
509
 
501
510
  def _filter_selection_paths(self):
502
511
  """
@@ -537,7 +546,7 @@ class FileStore(Workspace):
537
546
  # TODO: Update metadata of all relations that point to this path too.
538
547
 
539
548
  def archive(
540
- self, store_path: StorePath, missing_ok: bool = False, quiet: bool = False
549
+ self, store_path: StorePath, *, missing_ok: bool = False, quiet: bool = False
541
550
  ) -> StorePath:
542
551
  """
543
552
  Archive the item by moving it into the archive directory.
@@ -553,6 +562,9 @@ class FileStore(Workspace):
553
562
  if missing_ok and not orig_path.exists():
554
563
  log.message("Item to archive not found so moving on: %s", fmt_loc(orig_path))
555
564
  return store_path
565
+ if not orig_path.exists():
566
+ log.warning("Item to archive not found: %s", fmt_loc(orig_path))
567
+ return store_path
556
568
  move_file(orig_path, archive_path)
557
569
  self._remove_references([store_path])
558
570
 
@@ -572,7 +584,7 @@ class FileStore(Workspace):
572
584
  move_file(full_input_path, original_path)
573
585
  return StorePath(store_path)
574
586
 
575
- def log_workspace_info(self, once: bool = False):
587
+ def log_workspace_info(self, *, once: bool = False):
576
588
  """
577
589
  Log helpful information about the workspace.
578
590
  """
@@ -603,7 +615,7 @@ class FileStore(Workspace):
603
615
 
604
616
  if self.is_global_ws:
605
617
  PrintHooks.spacer()
606
- log.warning("Note you are currently using the default `global` workspace.")
618
+ log.warning("Note you are currently using the default global workspace.")
607
619
  cprint(
608
620
  "Create or switch to another workspace with the `workspace` command.",
609
621
  style=STYLE_HINT,
@@ -615,6 +627,7 @@ class FileStore(Workspace):
615
627
  def walk_items(
616
628
  self,
617
629
  store_path: StorePath | None = None,
630
+ *,
618
631
  use_ignore: bool = True,
619
632
  ) -> Generator[StorePath, None, None]:
620
633
  """
@@ -22,7 +22,7 @@ _item_cache = MtimeCache[Item](max_size=2000, name="Item")
22
22
 
23
23
 
24
24
  @tally_calls()
25
- def write_item(item: Item, path: Path):
25
+ def write_item(item: Item, path: Path, normalize: bool = True):
26
26
  """
27
27
  Write a text item to a file with standard frontmatter format YAML.
28
28
  Also normalizes formatting of the body text.
@@ -36,7 +36,10 @@ def write_item(item: Item, path: Path):
36
36
  # Clear cache before writing.
37
37
  _item_cache.delete(path)
38
38
 
39
- body = normalize_formatting_ansi(item.body_text(), item.format)
39
+ if normalize:
40
+ body = normalize_formatting_ansi(item.body_text(), item.format)
41
+ else:
42
+ body = item.body_text()
40
43
 
41
44
  # Special case for YAML files to avoid a possible duplicate `---` divider in the body.
42
45
  if body and item.format == Format.yaml:
@@ -7,7 +7,12 @@ from pathlib import Path
7
7
  from pydantic.dataclasses import dataclass
8
8
 
9
9
  from kash.config.logger import get_logger
10
- from kash.config.settings import CONTENT_CACHE_NAME, DOT_DIR, MEDIA_CACHE_NAME, get_system_cache_dir
10
+ from kash.config.settings import (
11
+ CONTENT_CACHE_NAME,
12
+ DOT_DIR,
13
+ MEDIA_CACHE_NAME,
14
+ global_settings,
15
+ )
11
16
  from kash.file_storage.persisted_yaml import PersistedYaml
12
17
  from kash.model.paths_model import StorePath
13
18
  from kash.utils.common.format_utils import fmt_loc
@@ -62,7 +67,11 @@ class MetadataDirs:
62
67
  # in which case it is in the global cache path.
63
68
  @property
64
69
  def cache_dir(self) -> Path:
65
- return get_system_cache_dir() if self.is_global_ws else StorePath(f"{DOT_DIR}/cache")
70
+ return (
71
+ global_settings().system_cache_dir
72
+ if self.is_global_ws
73
+ else StorePath(f"{DOT_DIR}/cache")
74
+ )
66
75
 
67
76
  @property
68
77
  def media_cache_dir(self) -> Path:
kash/help/assistant.py CHANGED
@@ -110,7 +110,7 @@ def assist_current_state() -> Message:
110
110
  ws_info = f"Based on the current directory, the current workspace is: {ws_base_dir.name} at {fmt_loc(ws_base_dir)}"
111
111
  else:
112
112
  if is_global_ws:
113
- about_ws = "You are currently using the `global` workspace."
113
+ about_ws = "You are currently using the default global workspace."
114
114
  else:
115
115
  about_ws = "The current directory is not a workspace."
116
116
  ws_info = (
@@ -1,10 +1,11 @@
1
1
  from functools import cache
2
2
  from textwrap import dedent
3
3
 
4
+ from strif import StringTemplate
5
+
4
6
  from kash.config.logger import get_logger
5
7
  from kash.docs.all_docs import all_docs
6
8
  from kash.docs.load_help_topics import load_help_src
7
- from kash.utils.common.string_template import StringTemplate
8
9
 
9
10
  log = get_logger(__name__)
10
11
 
@@ -3,9 +3,9 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass, field
4
4
  from pathlib import Path
5
5
 
6
- from kash.concepts.embeddings import Embeddings
7
- from kash.concepts.text_similarity import rank_by_relatedness
8
6
  from kash.config.logger import get_logger
7
+ from kash.embeddings.embeddings import Embeddings
8
+ from kash.embeddings.text_similarity import rank_by_relatedness
9
9
  from kash.help.help_types import HelpDoc, HelpDocType
10
10
  from kash.web_content.local_file_cache import Loadable
11
11
 
@@ -1,5 +1,8 @@
1
+ from typing import Any
2
+
1
3
  from kash.config.logger import get_logger
2
4
  from kash.config.text_styles import STYLE_HINT
5
+ from kash.docs.all_docs import DocSelection
3
6
  from kash.exec.action_registry import look_up_action_class
4
7
  from kash.exec.command_registry import CommandFunction, look_up_command
5
8
  from kash.help.assistant import assist_preamble, assistance_unstructured
@@ -10,7 +13,7 @@ from kash.help.tldr_help import tldr_help
10
13
  from kash.llm_utils import LLM
11
14
  from kash.llm_utils.llm_messages import Message
12
15
  from kash.model.actions_model import Action
13
- from kash.model.params_model import COMMON_SHELL_PARAMS, RUNTIME_ACTION_PARAMS, Param
16
+ from kash.model.params_model import COMMON_SHELL_PARAMS, Param
14
17
  from kash.model.preconditions_model import Precondition
15
18
  from kash.shell.output.shell_formatting import format_name_and_description, format_name_and_value
16
19
  from kash.shell.output.shell_output import (
@@ -32,7 +35,7 @@ GENERAL_HELP = (
32
35
  def _print_command_help(
33
36
  name: str,
34
37
  description: str | None = None,
35
- param_info: list[Param] | None = None,
38
+ param_info: list[Param[Any]] | None = None,
36
39
  precondition: Precondition | None = None,
37
40
  verbose: bool = True,
38
41
  is_action: bool = False, # pyright: ignore[reportUnusedParameter]
@@ -97,14 +100,7 @@ def print_action_help(
97
100
  include_options: bool = True,
98
101
  include_precondition: bool = True,
99
102
  ):
100
- if include_options:
101
- params = (
102
- list(action.params)
103
- + list(RUNTIME_ACTION_PARAMS.values())
104
- + list(COMMON_SHELL_PARAMS.values())
105
- )
106
- else:
107
- params = None
103
+ params = action.shell_params if include_options else None
108
104
 
109
105
  _print_command_help(
110
106
  action.name,
@@ -176,7 +172,7 @@ def print_explain_command(text: str, assistant_model: LLM | None = None):
176
172
  # Give the LLM full context on kash APIs.
177
173
  # But we do this here lazily to prevent circular dependencies.
178
174
  system_message = Message(
179
- assist_preamble(is_structured=False, skip_api_docs=False, base_actions_only=False)
175
+ assist_preamble(is_structured=False, doc_selection=DocSelection.full)
180
176
  )
181
177
  chat_history.extend(
182
178
  [
@@ -1,6 +1,6 @@
1
1
  from kash.llm_utils import Message, MessageTemplate, llm_template_completion
2
2
  from kash.llm_utils.llms import LLM
3
- from kash.text_handling.markdown_util import as_bullet_points
3
+ from kash.text_handling.markdown_utils import as_bullet_points
4
4
 
5
5
  # TODO: Enforce that the edits below doesn't contain anything extraneous.
6
6
 
@@ -1,15 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import litellm
4
+ from clideps.env_vars.dotenv_utils import env_var_is_set
5
+ from clideps.env_vars.env_names import EnvName
4
6
  from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
5
7
 
6
8
  from kash.llm_utils.llm_names import LLMName
7
9
  from kash.llm_utils.llms import LLM
8
- from kash.shell.clideps.api_keys import ApiEnvKey
9
- from kash.shell.clideps.dotenv_utils import env_var_is_set
10
10
 
11
11
 
12
- def api_for_model(model: LLMName) -> ApiEnvKey | None:
12
+ def api_for_model(model: LLMName) -> EnvName | None:
13
13
  """
14
14
  Get the API key name for a model or None if not found.
15
15
  """
@@ -18,7 +18,7 @@ def api_for_model(model: LLMName) -> ApiEnvKey | None:
18
18
  except litellm.exceptions.BadRequestError:
19
19
  return None
20
20
 
21
- return ApiEnvKey.for_provider(custom_llm_provider)
21
+ return EnvName.api_env_name(custom_llm_provider)
22
22
 
23
23
 
24
24
  def have_key_for_model(model: LLMName) -> bool:
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal, TypeAlias
5
+
6
+ from prettyfmt import custom_key_sort
7
+
8
+ from kash.llm_utils.llm_names import LLMName
9
+ from kash.llm_utils.llms import LLM
10
+
11
+ Speed: TypeAlias = Literal["fast", "medium", "slow"]
12
+
13
+ ContextSize: TypeAlias = Literal["small", "medium", "large"]
14
+
15
+ ModelSize: TypeAlias = Literal["small", "medium", "large"]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class LLMFeatures:
20
+ speed: Speed | None = None
21
+ context_size: ContextSize | None = None
22
+ model_size: ModelSize | None = None
23
+ structured_output: bool | None = None
24
+ thinking: bool = False
25
+
26
+ def satisfies(self, features: LLMFeatures) -> bool:
27
+ return all(
28
+ getattr(self, attr) == getattr(features, attr)
29
+ for attr in features.__dataclass_fields__
30
+ if getattr(self, attr) is not None
31
+ )
32
+
33
+
34
+ def pick_llm(desired_features: LLMFeatures) -> LLMName:
35
+ """
36
+ Pick the preferred model that satisfies the desired features.
37
+ """
38
+ satisfied_models: list[LLMName] = [
39
+ llm for llm, features in FEATURES.items() if features.satisfies(desired_features)
40
+ ]
41
+ satisfied_models.sort(key=custom_key_sort(preferred_llms))
42
+ if not satisfied_models:
43
+ raise ValueError(f"No model found for features: {desired_features}")
44
+ return satisfied_models[0]
45
+
46
+
47
+ FEATURES = {
48
+ LLM.o3_mini: LLMFeatures(
49
+ speed="fast",
50
+ context_size="small",
51
+ model_size="small",
52
+ structured_output=True,
53
+ thinking=True,
54
+ ),
55
+ # FIXME
56
+ }
57
+
58
+ preferred_llms: list[LLMName] = [
59
+ LLM.o3_mini,
60
+ LLM.o1_mini,
61
+ LLM.o1,
62
+ LLM.gpt_4o_mini,
63
+ LLM.gpt_4o,
64
+ LLM.gpt_4,
65
+ LLM.claude_3_7_sonnet,
66
+ LLM.claude_3_5_sonnet,
67
+ LLM.claude_3_5_haiku,
68
+ ]
@@ -10,8 +10,7 @@ from pydantic_core.core_schema import (
10
10
  str_schema,
11
11
  to_string_ser_schema,
12
12
  )
13
-
14
- from kash.utils.common.string_template import StringTemplate
13
+ from strif import StringTemplate
15
14
 
16
15
 
17
16
  class Message(str):
@@ -12,7 +12,7 @@ from pydantic_core.core_schema import (
12
12
  )
13
13
  from rich.text import Text
14
14
 
15
- from kash.config.text_styles import format_success_emoji
15
+ from kash.shell.output.shell_formatting import format_success_emoji
16
16
  from kash.utils.common.type_utils import not_none
17
17
 
18
18
 
kash/llm_utils/llms.py CHANGED
@@ -8,18 +8,23 @@ from kash.llm_utils.llm_names import LLMName
8
8
  class LLM(LLMName, Enum):
9
9
  """
10
10
  Convenience names for common LLMs. This isn't exhaustive, but just common
11
- ones for autocomplete, docs, etc. Values are all LiteLLM names.
11
+ ones for autocomplete, docs, etc. Values are all LiteLLM names. See:
12
+ https://github.com/BerriAI/litellm/blob/main/litellm/model_prices_and_context_window_backup.json
12
13
  """
13
14
 
14
15
  # https://platform.openai.com/docs/models
15
- o3_mini = LLMName("o3-mini")
16
16
  o1_mini = LLMName("o1-mini")
17
17
  o1 = LLMName("o1")
18
+ o3 = LLMName("o3")
19
+ o3_mini = LLMName("o3-mini")
20
+ o4_mini = LLMName("o4-mini")
18
21
  o1_preview = LLMName("o1-preview")
19
22
  gpt_4o_mini = LLMName("gpt-4o-mini")
20
23
  gpt_4o = LLMName("gpt-4o")
21
24
  gpt_4 = LLMName("gpt-4")
22
- gpt_3_5_turbo = LLMName("gpt-3.5-turbo")
25
+ gpt_4_1 = LLMName("gpt-4.1")
26
+ gpt_4_1_mini = LLMName("gpt-4.1-mini")
27
+ gpt_4_1_nano = LLMName("gpt-4.1-nano")
23
28
 
24
29
  # https://docs.anthropic.com/en/docs/about-claude/models/all-models
25
30
  claude_3_7_sonnet = LLMName("claude-3-7-sonnet-latest")
@@ -1,2 +1,5 @@
1
- # Ensure commands are registered.
2
- import kash.local_server.local_server_commands # noqa: F401
1
+ from pathlib import Path
2
+
3
+ from kash.exec.importing import import_and_register
4
+
5
+ import_and_register(__package__, Path(__file__).parent, ["."])
@@ -14,7 +14,10 @@ from prettyfmt import fmt_path
14
14
 
15
15
  from kash.config.logger import get_logger
16
16
  from kash.config.server_config import create_server_config
17
- from kash.config.settings import atomic_global_settings, global_settings, local_server_log_path
17
+ from kash.config.settings import (
18
+ atomic_global_settings,
19
+ global_settings,
20
+ )
18
21
  from kash.local_server import local_server_routes
19
22
  from kash.local_server.port_tools import find_available_local_port
20
23
  from kash.utils.errors import InvalidInput, InvalidState
@@ -32,14 +35,14 @@ def _app_setup() -> FastAPI:
32
35
 
33
36
  # Map common exceptions to HTTP codes.
34
37
  # FileNotFound first, since it might also be an InvalidInput.
35
- @app.exception_handler(FileNotFoundError)
38
+ @app.exception_handler(FileNotFoundError) # pyright: ignore[reportUntypedFunctionDecorator]
36
39
  async def file_not_found_exception_handler(_request: Request, exc: FileNotFoundError):
37
40
  return JSONResponse(
38
41
  status_code=404,
39
42
  content={"message": f"File not found: {exc}"},
40
43
  )
41
44
 
42
- @app.exception_handler(InvalidInput)
45
+ @app.exception_handler(InvalidInput) # pyright: ignore[reportUntypedFunctionDecorator]
43
46
  async def invalid_input_exception_handler(_request: Request, exc: InvalidInput):
44
47
  return JSONResponse(
45
48
  status_code=400,
@@ -47,7 +50,7 @@ def _app_setup() -> FastAPI:
47
50
  )
48
51
 
49
52
  # Global exception handler.
50
- @app.exception_handler(Exception)
53
+ @app.exception_handler(Exception) # pyright: ignore[reportUntypedFunctionDecorator]
51
54
  async def global_exception_handler(_request: Request, _exc: Exception):
52
55
  return JSONResponse(
53
56
  status_code=500,
@@ -175,7 +178,7 @@ class LocalServer:
175
178
 
176
179
  # Singleton instance for the UI server.
177
180
  # Note this is quick to set up (lazy imports).
178
- _ui_server = LocalServer(UI_SERVER_NAME, UI_SERVER_HOST, local_server_log_path())
181
+ _ui_server = LocalServer(UI_SERVER_NAME, UI_SERVER_HOST, global_settings().local_server_log_path)
179
182
 
180
183
 
181
184
  def start_ui_server():
@@ -1,5 +1,5 @@
1
1
  from kash.config.logger import get_logger
2
- from kash.config.settings import local_server_log_path
2
+ from kash.config.settings import global_settings
3
3
  from kash.exec import kash_command
4
4
  from kash.local_server.local_url_formatters import enable_local_urls
5
5
  from kash.shell.utils.native_utils import tail_file
@@ -50,7 +50,7 @@ def local_server_logs(follow: bool = False) -> None:
50
50
 
51
51
  :param follow: Follow the file as it grows.
52
52
  """
53
- log_path = local_server_log_path()
53
+ log_path = global_settings().local_server_log_path
54
54
  if not log_path.exists():
55
55
  raise InvalidState(
56
56
  f"Local ui server log not found (forgot to run `start_ui_server`?): {log_path}"
@@ -4,13 +4,13 @@ from pathlib import Path
4
4
 
5
5
  from rich.style import Style
6
6
  from rich.text import Text
7
+ from strif import AtomicVar
7
8
  from typing_extensions import override
8
9
 
9
10
  from kash.config.logger import get_logger
10
11
  from kash.config.text_styles import STYLE_HINT
11
12
  from kash.model.paths_model import StorePath
12
13
  from kash.shell.output.kerm_codes import KriLink, TextTooltip, UIAction, UIActionType
13
- from kash.utils.common.atomic_var import AtomicVar
14
14
  from kash.utils.common.format_utils import fmt_loc
15
15
  from kash.utils.errors import InvalidState
16
16
  from kash.workspaces import current_ws
kash/mcp/__init__.py CHANGED
@@ -1,2 +1,5 @@
1
- # Ensure commands are registered.
2
- import kash.mcp.mcp_server_commands # noqa: F401
1
+ from pathlib import Path
2
+
3
+ from kash.exec.importing import import_and_register
4
+
5
+ import_and_register(__package__, Path(__file__).parent, ["."])
kash/mcp/mcp_cli.py CHANGED
@@ -9,8 +9,8 @@ import logging
9
9
  import os
10
10
  from pathlib import Path
11
11
 
12
- from kash.config.settings import DEFAULT_MCP_SERVER_PORT, LogLevel, get_system_logs_dir
13
- from kash.config.setup import setup
12
+ from kash.config.settings import DEFAULT_MCP_SERVER_PORT, LogLevel, global_settings
13
+ from kash.config.setup import kash_setup
14
14
  from kash.shell.utils.argparse_utils import WrappedColorFormatter
15
15
  from kash.shell.version import get_version
16
16
 
@@ -18,7 +18,7 @@ __version__ = get_version()
18
18
 
19
19
  DEFAULT_PROXY_URL = f"http://localhost:{DEFAULT_MCP_SERVER_PORT}/sse"
20
20
 
21
- MCP_CLI_LOG_PATH = get_system_logs_dir() / "mcp_cli.log"
21
+ MCP_CLI_LOG_PATH = global_settings().system_logs_dir / "mcp_cli.log"
22
22
 
23
23
 
24
24
  log = logging.getLogger()
@@ -113,10 +113,10 @@ def main():
113
113
  args = build_parser().parse_args()
114
114
 
115
115
  if args.list_tools or args.tool_help:
116
- setup(rich_logging=True, level=LogLevel.warning)
116
+ kash_setup(rich_logging=True, level=LogLevel.warning)
117
117
  show_tool_info(args.tool_help)
118
118
  else:
119
- setup(rich_logging=False, level=LogLevel.info)
119
+ kash_setup(rich_logging=False, level=LogLevel.info)
120
120
  run_server(args)
121
121
 
122
122