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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. kash/actions/__init__.py +4 -4
  2. kash/actions/core/format_markdown_template.py +2 -5
  3. kash/actions/core/markdownify.py +7 -6
  4. kash/actions/core/readability.py +7 -6
  5. kash/actions/core/render_as_html.py +37 -0
  6. kash/actions/core/show_webpage.py +6 -11
  7. kash/actions/core/strip_html.py +2 -6
  8. kash/actions/core/tabbed_webpage_config.py +31 -0
  9. kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
  10. kash/commands/__init__.py +8 -20
  11. kash/commands/base/basic_file_commands.py +15 -0
  12. kash/commands/base/debug_commands.py +13 -0
  13. kash/commands/base/files_command.py +28 -10
  14. kash/commands/base/general_commands.py +21 -16
  15. kash/commands/base/logs_commands.py +4 -2
  16. kash/commands/base/model_commands.py +8 -8
  17. kash/commands/base/search_command.py +3 -2
  18. kash/commands/base/show_command.py +5 -3
  19. kash/commands/extras/parse_uv_lock.py +186 -0
  20. kash/commands/help/doc_commands.py +2 -31
  21. kash/commands/help/welcome.py +33 -0
  22. kash/commands/workspace/selection_commands.py +11 -6
  23. kash/commands/workspace/workspace_commands.py +19 -17
  24. kash/config/colors.py +3 -1
  25. kash/config/env_settings.py +14 -1
  26. kash/config/init.py +2 -2
  27. kash/config/logger.py +59 -56
  28. kash/config/logger_basic.py +3 -3
  29. kash/config/settings.py +116 -57
  30. kash/config/setup.py +28 -12
  31. kash/config/text_styles.py +3 -13
  32. kash/docs/load_api_docs.py +2 -1
  33. kash/docs/markdown/topics/a3_getting_started.md +3 -2
  34. kash/{concepts → embeddings}/text_similarity.py +2 -2
  35. kash/exec/__init__.py +20 -3
  36. kash/exec/action_decorators.py +24 -10
  37. kash/exec/action_exec.py +41 -23
  38. kash/exec/action_registry.py +13 -48
  39. kash/exec/command_registry.py +2 -1
  40. kash/exec/fetch_url_metadata.py +4 -6
  41. kash/exec/importing.py +56 -0
  42. kash/exec/llm_transforms.py +12 -10
  43. kash/exec/precondition_registry.py +2 -1
  44. kash/exec/preconditions.py +22 -1
  45. kash/exec/resolve_args.py +4 -0
  46. kash/exec/shell_callable_action.py +33 -19
  47. kash/file_storage/file_store.py +42 -27
  48. kash/file_storage/item_file_format.py +5 -2
  49. kash/file_storage/metadata_dirs.py +11 -2
  50. kash/help/assistant.py +1 -1
  51. kash/help/assistant_instructions.py +2 -1
  52. kash/help/function_param_info.py +1 -1
  53. kash/help/help_embeddings.py +2 -2
  54. kash/help/help_printing.py +7 -11
  55. kash/llm_utils/clean_headings.py +1 -1
  56. kash/llm_utils/llm_api_keys.py +4 -4
  57. kash/llm_utils/llm_features.py +68 -0
  58. kash/llm_utils/llm_messages.py +1 -2
  59. kash/llm_utils/llm_names.py +1 -1
  60. kash/llm_utils/llms.py +8 -3
  61. kash/local_server/__init__.py +5 -2
  62. kash/local_server/local_server.py +8 -5
  63. kash/local_server/local_server_commands.py +2 -2
  64. kash/local_server/local_server_routes.py +1 -7
  65. kash/local_server/local_url_formatters.py +1 -1
  66. kash/mcp/__init__.py +5 -2
  67. kash/mcp/mcp_cli.py +5 -5
  68. kash/mcp/mcp_server_commands.py +5 -5
  69. kash/mcp/mcp_server_routes.py +5 -5
  70. kash/mcp/mcp_server_sse.py +4 -2
  71. kash/media_base/media_cache.py +8 -8
  72. kash/media_base/media_services.py +1 -1
  73. kash/media_base/media_tools.py +6 -6
  74. kash/media_base/services/local_file_media.py +2 -2
  75. kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -110
  76. kash/media_base/transcription_format.py +73 -0
  77. kash/media_base/transcription_whisper.py +38 -0
  78. kash/model/__init__.py +73 -5
  79. kash/model/actions_model.py +38 -4
  80. kash/model/concept_model.py +30 -0
  81. kash/model/items_model.py +115 -32
  82. kash/model/params_model.py +24 -0
  83. kash/shell/completions/completion_scoring.py +37 -5
  84. kash/shell/output/kerm_codes.py +1 -2
  85. kash/shell/output/shell_formatting.py +14 -4
  86. kash/shell/shell_main.py +2 -2
  87. kash/shell/utils/exception_printing.py +6 -0
  88. kash/shell/utils/native_utils.py +26 -20
  89. kash/shell/utils/shell_function_wrapper.py +15 -15
  90. kash/text_handling/custom_sliding_transforms.py +12 -4
  91. kash/text_handling/doc_normalization.py +6 -2
  92. kash/text_handling/markdown_render.py +118 -0
  93. kash/text_handling/markdown_utils.py +226 -0
  94. kash/utils/common/function_inspect.py +360 -110
  95. kash/utils/common/import_utils.py +12 -3
  96. kash/utils/common/type_utils.py +0 -29
  97. kash/utils/common/url.py +27 -3
  98. kash/utils/errors.py +6 -0
  99. kash/utils/file_utils/file_ext.py +4 -0
  100. kash/utils/file_utils/file_formats.py +2 -2
  101. kash/utils/file_utils/file_formats_model.py +20 -1
  102. kash/web_content/dir_store.py +1 -2
  103. kash/web_content/file_cache_utils.py +37 -10
  104. kash/web_content/file_processing.py +68 -0
  105. kash/web_content/local_file_cache.py +12 -9
  106. kash/web_content/web_extract.py +8 -3
  107. kash/web_content/web_fetch.py +12 -4
  108. kash/web_gen/__init__.py +0 -4
  109. kash/web_gen/simple_webpage.py +52 -0
  110. kash/web_gen/tabbed_webpage.py +24 -14
  111. kash/web_gen/template_render.py +37 -2
  112. kash/web_gen/templates/base_styles.css.jinja +169 -43
  113. kash/web_gen/templates/base_webpage.html.jinja +110 -45
  114. kash/web_gen/templates/content_styles.css.jinja +4 -2
  115. kash/web_gen/templates/item_view.html.jinja +49 -39
  116. kash/web_gen/templates/simple_webpage.html.jinja +24 -0
  117. kash/web_gen/templates/tabbed_webpage.html.jinja +42 -33
  118. kash/workspaces/__init__.py +15 -2
  119. kash/workspaces/selections.py +18 -3
  120. kash/workspaces/source_items.py +0 -1
  121. kash/workspaces/workspaces.py +5 -11
  122. kash/xonsh_custom/command_nl_utils.py +40 -19
  123. kash/xonsh_custom/custom_shell.py +43 -11
  124. kash/xonsh_custom/customize_prompt.py +39 -21
  125. kash/xonsh_custom/load_into_xonsh.py +22 -25
  126. kash/xonsh_custom/shell_load_commands.py +2 -2
  127. kash/xonsh_custom/xonsh_completers.py +2 -249
  128. kash/xonsh_custom/xonsh_keybindings.py +282 -0
  129. kash/xonsh_custom/xonsh_modern_tools.py +3 -3
  130. kash/xontrib/kash_extension.py +5 -6
  131. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/METADATA +10 -8
  132. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/RECORD +137 -136
  133. kash/actions/core/webpage_config.py +0 -21
  134. kash/concepts/concept_formats.py +0 -23
  135. kash/shell/clideps/api_keys.py +0 -100
  136. kash/shell/clideps/dotenv_setup.py +0 -115
  137. kash/shell/clideps/dotenv_utils.py +0 -98
  138. kash/shell/clideps/pkg_deps.py +0 -257
  139. kash/shell/clideps/platforms.py +0 -11
  140. kash/shell/clideps/terminal_features.py +0 -56
  141. kash/shell/utils/osc_utils.py +0 -95
  142. kash/shell/utils/terminal_images.py +0 -133
  143. kash/text_handling/markdown_util.py +0 -167
  144. kash/utils/common/atomic_var.py +0 -171
  145. kash/utils/common/string_replace.py +0 -93
  146. kash/utils/common/string_template.py +0 -101
  147. /kash/{concepts → embeddings}/cosine.py +0 -0
  148. /kash/{concepts → embeddings}/embeddings.py +0 -0
  149. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/WHEEL +0 -0
  150. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/entry_points.txt +0 -0
  151. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -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:
@@ -443,19 +450,11 @@ class FileStore(Workspace):
443
450
  if not path.exists():
444
451
  raise FileNotFound(f"File not found: {fmt_loc(path)}")
445
452
 
446
- # It's a path outside the store, so copy it in.
447
- _name, filename_item_type, format, _file_ext = parse_item_filename(path)
453
+ # First treat it as an external file to analyze file type and format.
454
+ item = Item.from_external_path(path)
448
455
 
449
- # Best guesses on item types if not specified.
450
- item_type = as_type
451
- if not item_type and filename_item_type:
452
- item_type = filename_item_type
453
- if not item_type and format:
454
- item_type = ItemType.for_format(format)
455
- if not item_type:
456
- item_type = ItemType.resource
457
-
458
- if format and format.supports_frontmatter:
456
+ # If it's a text/frontmatter-friendly, read it fully.
457
+ if item.format and item.format.supports_frontmatter:
459
458
  log.message("Importing text file: %s", fmt_loc(path))
460
459
  # This will read the file with or without frontmatter.
461
460
  # We are importing so we want to drop the external path so we save the body.
@@ -479,15 +478,25 @@ class FileStore(Workspace):
479
478
  log.message("Importing non-text file: %s", fmt_loc(path))
480
479
  # Binary or other files we just copy over as-is, preserving the name.
481
480
  # We know the extension is recognized.
482
- item = Item.from_external_path(path)
483
- store_path, _found, _prev = self.store_path_for(item)
481
+ store_path, _found, old_store_path = self.store_path_for(item)
484
482
  if self.exists(store_path):
485
483
  raise FileExists(f"Resource already in store: {fmt_loc(store_path)}")
486
484
 
487
- item.type = item_type
488
-
489
- log.message("Importing resource: %s -> %s", fmt_loc(path), fmt_loc(store_path))
485
+ log.message("Importing resource: %s", fmt_loc(path))
490
486
  copyfile_atomic(path, self.base_dir / store_path, make_parents=True)
487
+
488
+ # Optimization: Don't import an identical file twice.
489
+ if old_store_path:
490
+ old_hash = self.hash(old_store_path)
491
+ new_hash = self.hash(store_path)
492
+ if old_hash == new_hash:
493
+ log.message(
494
+ "Imported resource is identical to the previous import: %s",
495
+ fmt_loc(old_store_path),
496
+ )
497
+ os.unlink(self.base_dir / store_path)
498
+ store_path = old_store_path
499
+ log.message("Imported resource: %s", fmt_loc(store_path))
491
500
  return store_path
492
501
 
493
502
  def import_items(
@@ -496,7 +505,9 @@ class FileStore(Workspace):
496
505
  as_type: ItemType | None = None,
497
506
  reimport: bool = False,
498
507
  ) -> list[StorePath]:
499
- return [self.import_item(locator, as_type, reimport) for locator in locators]
508
+ return [
509
+ self.import_item(locator, as_type=as_type, reimport=reimport) for locator in locators
510
+ ]
500
511
 
501
512
  def _filter_selection_paths(self):
502
513
  """
@@ -537,7 +548,7 @@ class FileStore(Workspace):
537
548
  # TODO: Update metadata of all relations that point to this path too.
538
549
 
539
550
  def archive(
540
- self, store_path: StorePath, missing_ok: bool = False, quiet: bool = False
551
+ self, store_path: StorePath, *, missing_ok: bool = False, quiet: bool = False
541
552
  ) -> StorePath:
542
553
  """
543
554
  Archive the item by moving it into the archive directory.
@@ -553,6 +564,9 @@ class FileStore(Workspace):
553
564
  if missing_ok and not orig_path.exists():
554
565
  log.message("Item to archive not found so moving on: %s", fmt_loc(orig_path))
555
566
  return store_path
567
+ if not orig_path.exists():
568
+ log.warning("Item to archive not found: %s", fmt_loc(orig_path))
569
+ return store_path
556
570
  move_file(orig_path, archive_path)
557
571
  self._remove_references([store_path])
558
572
 
@@ -572,7 +586,7 @@ class FileStore(Workspace):
572
586
  move_file(full_input_path, original_path)
573
587
  return StorePath(store_path)
574
588
 
575
- def log_workspace_info(self, once: bool = False):
589
+ def log_workspace_info(self, *, once: bool = False):
576
590
  """
577
591
  Log helpful information about the workspace.
578
592
  """
@@ -603,7 +617,7 @@ class FileStore(Workspace):
603
617
 
604
618
  if self.is_global_ws:
605
619
  PrintHooks.spacer()
606
- log.warning("Note you are currently using the default `global` workspace.")
620
+ log.warning("Note you are currently using the default global workspace.")
607
621
  cprint(
608
622
  "Create or switch to another workspace with the `workspace` command.",
609
623
  style=STYLE_HINT,
@@ -615,6 +629,7 @@ class FileStore(Workspace):
615
629
  def walk_items(
616
630
  self,
617
631
  store_path: StorePath | None = None,
632
+ *,
618
633
  use_ignore: bool = True,
619
634
  ) -> Generator[StorePath, None, None]:
620
635
  """
@@ -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
 
@@ -12,7 +12,7 @@ def _look_up_param_docs(func: Callable[..., Any], kw_params: list[FuncParam]) ->
12
12
  name = func_param.name
13
13
  param = ALL_COMMON_PARAMS.get(name)
14
14
  if not param:
15
- param = Param(name, description=None, type=func_param.type or str)
15
+ param = Param(name, description=None, type=func_param.effective_type or str)
16
16
 
17
17
  # Also check the docstring for a description of this parameter.
18
18
  docstring = parse_docstring(func.__doc__ or "")
@@ -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}"
@@ -18,7 +18,6 @@ from kash.shell.file_icons.nerd_icons import icon_for_file
18
18
  from kash.shell.output.shell_output import Wrap
19
19
  from kash.utils.common.type_utils import not_none
20
20
  from kash.utils.errors import FileNotFound, InvalidFilename
21
- from kash.web_gen import base_templates_dir
22
21
  from kash.web_gen.template_render import render_web_template
23
22
  from kash.workspaces.workspace_output import print_file_info
24
23
 
@@ -140,14 +139,11 @@ def explain(text: str):
140
139
 
141
140
  return HTMLResponse(
142
141
  render_web_template(
143
- base_templates_dir,
144
142
  "base_webpage.html.jinja",
145
143
  {
146
144
  "title": f"Help: {text}",
147
145
  "content": render_web_template(
148
- base_templates_dir,
149
- "explain_view.html.jinja",
150
- {"help_html": help_html, "page_url": page_url},
146
+ "explain_view.html.jinja", {"help_html": help_html, "page_url": page_url}
151
147
  ),
152
148
  },
153
149
  )
@@ -270,12 +266,10 @@ def _serve_item(
270
266
 
271
267
  return HTMLResponse(
272
268
  render_web_template(
273
- base_templates_dir,
274
269
  "base_webpage.html.jinja",
275
270
  {
276
271
  "title": display_title,
277
272
  "content": render_web_template(
278
- base_templates_dir,
279
273
  "item_view.html.jinja",
280
274
  {
281
275
  "item": item,
@@ -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