kash-shell 0.3.11__py3-none-any.whl → 0.3.13__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 (95) hide show
  1. kash/actions/core/markdownify.py +5 -4
  2. kash/actions/core/readability.py +4 -4
  3. kash/actions/core/render_as_html.py +8 -6
  4. kash/actions/core/show_webpage.py +2 -2
  5. kash/actions/core/strip_html.py +2 -2
  6. kash/commands/base/basic_file_commands.py +24 -3
  7. kash/commands/base/diff_commands.py +38 -3
  8. kash/commands/base/files_command.py +5 -4
  9. kash/commands/base/reformat_command.py +1 -1
  10. kash/commands/base/show_command.py +1 -1
  11. kash/commands/extras/parse_uv_lock.py +12 -3
  12. kash/commands/workspace/selection_commands.py +1 -1
  13. kash/commands/workspace/workspace_commands.py +62 -16
  14. kash/config/env_settings.py +2 -42
  15. kash/config/logger.py +30 -25
  16. kash/config/logger_basic.py +6 -6
  17. kash/config/settings.py +23 -7
  18. kash/config/setup.py +33 -5
  19. kash/config/text_styles.py +25 -22
  20. kash/docs/load_source_code.py +1 -1
  21. kash/embeddings/cosine.py +12 -4
  22. kash/embeddings/embeddings.py +16 -6
  23. kash/embeddings/text_similarity.py +10 -4
  24. kash/exec/__init__.py +3 -0
  25. kash/exec/action_decorators.py +4 -19
  26. kash/exec/action_exec.py +46 -27
  27. kash/exec/fetch_url_metadata.py +8 -5
  28. kash/exec/importing.py +4 -4
  29. kash/exec/llm_transforms.py +2 -2
  30. kash/exec/preconditions.py +11 -19
  31. kash/exec/runtime_settings.py +134 -0
  32. kash/exec/shell_callable_action.py +5 -3
  33. kash/file_storage/file_store.py +91 -53
  34. kash/file_storage/item_file_format.py +6 -3
  35. kash/file_storage/store_filenames.py +7 -3
  36. kash/help/help_embeddings.py +2 -2
  37. kash/llm_utils/clean_headings.py +1 -1
  38. kash/{text_handling → llm_utils}/custom_sliding_transforms.py +0 -3
  39. kash/llm_utils/init_litellm.py +16 -0
  40. kash/llm_utils/llm_api_keys.py +6 -2
  41. kash/llm_utils/llm_completion.py +12 -5
  42. kash/local_server/__init__.py +1 -1
  43. kash/local_server/local_server_commands.py +2 -1
  44. kash/mcp/__init__.py +1 -1
  45. kash/mcp/mcp_cli.py +3 -2
  46. kash/mcp/mcp_server_commands.py +8 -2
  47. kash/mcp/mcp_server_routes.py +11 -12
  48. kash/media_base/media_cache.py +10 -3
  49. kash/media_base/transcription_deepgram.py +15 -2
  50. kash/model/__init__.py +1 -1
  51. kash/model/actions_model.py +9 -54
  52. kash/model/exec_model.py +79 -0
  53. kash/model/items_model.py +131 -81
  54. kash/model/operations_model.py +38 -15
  55. kash/model/paths_model.py +2 -0
  56. kash/shell/output/shell_output.py +10 -8
  57. kash/shell/shell_main.py +2 -2
  58. kash/shell/ui/shell_results.py +2 -1
  59. kash/shell/utils/exception_printing.py +2 -2
  60. kash/utils/common/format_utils.py +0 -14
  61. kash/utils/common/import_utils.py +46 -18
  62. kash/utils/common/task_stack.py +4 -15
  63. kash/utils/errors.py +14 -9
  64. kash/utils/file_utils/file_formats_model.py +61 -26
  65. kash/utils/file_utils/file_sort_filter.py +10 -3
  66. kash/utils/file_utils/filename_parsing.py +41 -16
  67. kash/{text_handling → utils/text_handling}/doc_normalization.py +23 -13
  68. kash/utils/text_handling/escape_html_tags.py +156 -0
  69. kash/{text_handling → utils/text_handling}/markdown_utils.py +82 -4
  70. kash/utils/text_handling/markdownify_utils.py +87 -0
  71. kash/{text_handling → utils/text_handling}/unified_diffs.py +1 -44
  72. kash/web_content/file_cache_utils.py +42 -34
  73. kash/web_content/local_file_cache.py +29 -12
  74. kash/web_content/web_extract.py +1 -1
  75. kash/web_content/web_extract_readabilipy.py +4 -2
  76. kash/web_content/web_fetch.py +42 -7
  77. kash/web_content/web_page_model.py +2 -1
  78. kash/web_gen/simple_webpage.py +1 -1
  79. kash/web_gen/templates/base_styles.css.jinja +139 -16
  80. kash/web_gen/templates/simple_webpage.html.jinja +1 -1
  81. kash/workspaces/__init__.py +12 -3
  82. kash/workspaces/selections.py +2 -2
  83. kash/workspaces/workspace_dirs.py +58 -0
  84. kash/workspaces/workspace_importing.py +2 -2
  85. kash/workspaces/workspace_output.py +2 -2
  86. kash/workspaces/workspaces.py +26 -90
  87. kash/xonsh_custom/load_into_xonsh.py +4 -2
  88. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/METADATA +4 -4
  89. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/RECORD +93 -89
  90. kash/shell/utils/argparse_utils.py +0 -20
  91. kash/utils/lang_utils/inflection.py +0 -18
  92. /kash/{text_handling → utils/text_handling}/markdown_render.py +0 -0
  93. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/WHEEL +0 -0
  94. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/entry_points.txt +0 -0
  95. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@ import time
5
5
  from collections.abc import Callable, Generator
6
6
  from os.path import join, relpath
7
7
  from pathlib import Path
8
- from typing import Any, TypeVar
8
+ from typing import Concatenate, ParamSpec, TypeVar
9
9
 
10
10
  from funlog import format_duration, log_calls
11
11
  from prettyfmt import fmt_lines, fmt_path
@@ -13,13 +13,13 @@ from strif import copyfile_atomic, hash_file, move_file
13
13
  from typing_extensions import override
14
14
 
15
15
  from kash.config.logger import get_log_settings, get_logger
16
- from kash.config.text_styles import EMOJI_SAVED, STYLE_HINT
16
+ from kash.config.text_styles import EMOJI_SAVED
17
17
  from kash.file_storage.item_file_format import read_item, write_item
18
18
  from kash.file_storage.metadata_dirs import MetadataDirs
19
19
  from kash.file_storage.store_filenames import folder_for_type, join_suffix, parse_item_filename
20
20
  from kash.model.items_model import Item, ItemId, ItemType
21
21
  from kash.model.paths_model import StorePath
22
- from kash.shell.output.shell_output import PrintHooks, cprint
22
+ from kash.shell.output.shell_output import PrintHooks
23
23
  from kash.utils.common.format_utils import fmt_loc
24
24
  from kash.utils.common.uniquifier import Uniquifier
25
25
  from kash.utils.common.url import Locator, Url, is_url
@@ -34,16 +34,18 @@ from kash.workspaces.workspaces import Workspace
34
34
  log = get_logger(__name__)
35
35
 
36
36
 
37
+ SelfT = TypeVar("SelfT")
37
38
  T = TypeVar("T")
39
+ P = ParamSpec("P")
38
40
 
39
41
 
40
- def synchronized(method: Callable[..., T]) -> Callable[..., T]:
42
+ def synchronized(method: Callable[Concatenate[SelfT, P], T]) -> Callable[Concatenate[SelfT, P], T]:
41
43
  """
42
44
  Simple way to synchronize a few methods.
43
45
  """
44
46
 
45
47
  @functools.wraps(method)
46
- def synchronized_method(self, *args: Any, **kwargs: Any) -> T:
48
+ def synchronized_method(self, *args: P.args, **kwargs: P.kwargs) -> T:
47
49
  with self._lock:
48
50
  return method(self, *args, **kwargs)
49
51
 
@@ -180,12 +182,37 @@ class FileStore(Workspace):
180
182
  except (FileNotFoundError, InvalidFilename):
181
183
  pass
182
184
 
185
+ def resolve_path(self, path: Path | StorePath) -> StorePath | None:
186
+ """
187
+ Return a StorePath if the given path is within the store, otherwise None.
188
+ If it is already a StorePath, return it unchanged.
189
+ """
190
+ if isinstance(path, StorePath):
191
+ return path
192
+ resolved = path.resolve()
193
+ if resolved.is_relative_to(self.base_dir):
194
+ return StorePath(resolved.relative_to(self.base_dir))
195
+ else:
196
+ return None
197
+
198
+ def exists(self, store_path: StorePath) -> bool:
199
+ """
200
+ Check given store path refers to an existing file.
201
+ """
202
+ return (self.base_dir / store_path).exists()
203
+
183
204
  @synchronized
184
- def _new_filename_for(self, item: Item) -> tuple[str, str | None]:
205
+ def _pick_filename_for(self, item: Item, *, overwrite: bool = False) -> tuple[str, str | None]:
185
206
  """
186
- Get a suitable filename for this item that is close to the slugified title yet also unique.
187
- Also return the old filename if it's different.
207
+ Get a suitable filename for this item.
208
+ If `overwrite` is true, use the the slugified title.
209
+ If it is false, use the slugified title with a suffix to make it unique
210
+ and in this case returns the old filename for this item, if it is different.
188
211
  """
212
+ if overwrite:
213
+ log.info("Picked default filename: %s for item: %s", item.default_filename(), item)
214
+ return item.default_filename(), None
215
+
189
216
  slug = item.slug_name()
190
217
  full_suffix = item.get_full_suffix()
191
218
  # Get a unique name per item type.
@@ -196,29 +223,15 @@ class FileStore(Workspace):
196
223
 
197
224
  old_filename = join_suffix(old_slugs[0], full_suffix) if old_slugs else None
198
225
 
226
+ log.info("Picked new unique filename: %s for item: %s", new_unique_filename, item)
199
227
  return new_unique_filename, old_filename
200
228
 
201
- def _default_path_for(self, item: Item) -> StorePath:
202
- folder_path = folder_for_type(item.type)
203
- slug = item.slug_name()
204
- suffix = item.get_full_suffix()
205
- return StorePath(folder_path / join_suffix(slug, suffix))
206
-
207
- def exists(self, store_path: StorePath) -> bool:
208
- return (self.base_dir / store_path).exists()
209
-
210
- def resolve_path(self, path: Path | StorePath) -> StorePath | None:
229
+ def default_path_for(self, item: Item) -> StorePath:
211
230
  """
212
- Return a StorePath if the given path is within the store, otherwise None.
213
- If it is already a StorePath, return it unchanged.
231
+ Get the default store path for an item based on slugifying its title or other metadata.
214
232
  """
215
- if isinstance(path, StorePath):
216
- return path
217
- resolved = path.resolve()
218
- if resolved.is_relative_to(self.base_dir):
219
- return StorePath(resolved.relative_to(self.base_dir))
220
- else:
221
- return None
233
+ folder_path = folder_for_type(item.type)
234
+ return StorePath(folder_path / item.default_filename())
222
235
 
223
236
  @synchronized
224
237
  def find_by_id(self, item: Item) -> StorePath | None:
@@ -233,7 +246,7 @@ class FileStore(Workspace):
233
246
  store_path = self.id_map.get(item_id)
234
247
  if not store_path:
235
248
  # Just in case the id_map is not complete, check the default path too.
236
- default_path = self._default_path_for(item)
249
+ default_path = self.default_path_for(item)
237
250
  if self.exists(default_path):
238
251
  old_item = self.load(default_path)
239
252
  if old_item.item_id() == item_id:
@@ -254,7 +267,7 @@ class FileStore(Workspace):
254
267
 
255
268
  @synchronized
256
269
  def store_path_for(
257
- self, item: Item, as_tmp: bool = False
270
+ self, item: Item, *, as_tmp: bool = False, overwrite: bool = False
258
271
  ) -> tuple[StorePath, bool, StorePath | None]:
259
272
  """
260
273
  Return the store path for an item. If the item already has a `store_path`, we use that.
@@ -263,6 +276,10 @@ class FileStore(Workspace):
263
276
  Returns `store_path, found, old_store_path` where `found` indicates whether the path was
264
277
  already found (in the item or in the store by checking for identity) and `old_store_path`
265
278
  is the previous similarly named item with a different identity (or None there is none).
279
+
280
+ If `as_tmp` is true, will return a path from the temporary directory in the store.
281
+ Normally an item is always saved to a unique store path but if `overwrite` is true,
282
+ will always save to the same path
266
283
  """
267
284
  item_id = item.item_id()
268
285
  old_filename = None
@@ -279,9 +296,9 @@ class FileStore(Workspace):
279
296
  )
280
297
  return store_path, True, None
281
298
  else:
282
- # We need to generate a new filename.
299
+ # We need to pick the path and filename.
283
300
  folder_path = folder_for_type(item.type)
284
- filename, old_filename = self._new_filename_for(item)
301
+ filename, old_filename = self._pick_filename_for(item, overwrite=overwrite)
285
302
  store_path = folder_path / filename
286
303
 
287
304
  old_store_path = None
@@ -310,16 +327,33 @@ class FileStore(Workspace):
310
327
 
311
328
  @log_calls()
312
329
  def save(
313
- self, item: Item, *, overwrite: bool = True, as_tmp: bool = False, no_format: bool = False
330
+ self,
331
+ item: Item,
332
+ *,
333
+ overwrite: bool = False,
334
+ skip_dup_names: bool = False,
335
+ as_tmp: bool = False,
336
+ no_format: bool = False,
314
337
  ) -> StorePath:
315
338
  """
316
339
  Save the item. Uses the `store_path` if it's already set or generates a new one.
317
340
  Updates `item.store_path`.
318
341
 
342
+ Unless `no_format` is true, also normalizes body text formatting (for Markdown)
343
+ and updates the item's body to match.
344
+
345
+ If `overwrite` is true, will overwrite a file that has the same path.
346
+
319
347
  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).
348
+
349
+ If `skip_dup_names` is true, will skip saving if an item if an item with a
350
+ matching path (based on its title) already exists.
322
351
  """
352
+ if overwrite and skip_dup_names:
353
+ raise ValueError("Cannot both overwrite and skip duplicate names.")
354
+ if overwrite and as_tmp:
355
+ raise ValueError("Cannot both overwrite and save to a temporary file.")
356
+
323
357
  # If external file already exists within the workspace, the file is already saved (without metadata).
324
358
  external_path = item.external_path and Path(item.external_path).resolve()
325
359
  if external_path and self._is_in_store(external_path):
@@ -331,10 +365,15 @@ class FileStore(Workspace):
331
365
  return StorePath(rel_path)
332
366
  else:
333
367
  # Otherwise it's still in memory or in a file outside the workspace and we need to save it.
334
- store_path, found, old_store_path = self.store_path_for(item, as_tmp=as_tmp)
368
+ store_path, found, old_store_path = self.store_path_for(
369
+ item, as_tmp=as_tmp, overwrite=overwrite
370
+ )
335
371
 
336
- if not overwrite and found:
337
- log.message("Skipping save of item already saved: %s", fmt_loc(store_path))
372
+ if skip_dup_names and found:
373
+ log.message(
374
+ "Skipping save because an item of the same name already exists: %s",
375
+ fmt_loc(store_path),
376
+ )
338
377
  item.store_path = str(store_path)
339
378
  return store_path
340
379
 
@@ -352,8 +391,14 @@ class FileStore(Workspace):
352
391
  # Now save the new item.
353
392
  try:
354
393
  if item.external_path:
355
- copyfile_atomic(item.external_path, full_path)
394
+ copyfile_atomic(item.external_path, full_path, make_parents=True)
356
395
  else:
396
+ if overwrite and full_path.exists():
397
+ log.info(
398
+ "Overwrite is enabled and a previous file exists so will archive it: %s",
399
+ fmt_loc(store_path),
400
+ )
401
+ self.archive(store_path, quiet=True)
357
402
  write_item(item, full_path, normalize=not no_format)
358
403
  except OSError as e:
359
404
  log.error("Error saving item: %s", e)
@@ -475,7 +520,6 @@ class FileStore(Workspace):
475
520
  store_path = self.save(item)
476
521
  log.info("Imported text file: %s", item.as_str())
477
522
  else:
478
- log.message("Importing non-text file: %s", fmt_loc(path))
479
523
  # Binary or other files we just copy over as-is, preserving the name.
480
524
  # We know the extension is recognized.
481
525
  store_path, _found, old_store_path = self.store_path_for(item)
@@ -586,12 +630,13 @@ class FileStore(Workspace):
586
630
  move_file(full_input_path, original_path)
587
631
  return StorePath(store_path)
588
632
 
589
- def log_workspace_info(self, *, once: bool = False):
633
+ @synchronized
634
+ def log_workspace_info(self, *, once: bool = False) -> bool:
590
635
  """
591
636
  Log helpful information about the workspace.
592
637
  """
593
638
  if once and self.info_logged:
594
- return
639
+ return False
595
640
 
596
641
  self.info_logged = True
597
642
 
@@ -606,25 +651,18 @@ class FileStore(Workspace):
606
651
  fmt_path(get_log_settings().log_file_path.absolute(), rel_to_cwd=False),
607
652
  )
608
653
  log.message(
609
- "Media cache: %s", fmt_path(self.base_dir / self.dirs.media_cache_dir, rel_to_cwd=False)
610
- )
611
- log.message(
612
- "Content cache: %s",
654
+ "Caches: %s, %s",
655
+ fmt_path(self.base_dir / self.dirs.media_cache_dir, rel_to_cwd=False),
613
656
  fmt_path(self.base_dir / self.dirs.content_cache_dir, rel_to_cwd=False),
614
657
  )
658
+ log.message("Current working directory: %s", fmt_path(Path.cwd(), rel_to_cwd=False))
659
+
615
660
  for warning in self.warnings:
616
661
  log.warning("%s", warning)
617
662
 
618
- if self.is_global_ws:
619
- PrintHooks.spacer()
620
- log.warning("Note you are currently using the default global workspace.")
621
- cprint(
622
- "Create or switch to another workspace with the `workspace` command.",
623
- style=STYLE_HINT,
624
- )
625
-
626
663
  log.info("File store startup took %s.", format_duration(self.end_time - self.start_time))
627
664
  # TODO: Log more info like number of items by type.
665
+ return True
628
666
 
629
667
  def walk_items(
630
668
  self,
@@ -7,10 +7,10 @@ from prettyfmt import custom_key_sort, fmt_size_human
7
7
  from kash.config.logger import get_logger
8
8
  from kash.model.items_model import ITEM_FIELDS, Item
9
9
  from kash.model.operations_model import OPERATION_FIELDS
10
- from kash.text_handling.doc_normalization import normalize_formatting_ansi
11
10
  from kash.utils.common.format_utils import fmt_loc
12
11
  from kash.utils.file_utils.file_formats_model import Format
13
12
  from kash.utils.file_utils.mtime_cache import MtimeCache
13
+ from kash.utils.text_handling.doc_normalization import normalize_formatting
14
14
 
15
15
  log = get_logger(__name__)
16
16
 
@@ -25,7 +25,7 @@ _item_cache = MtimeCache[Item](max_size=2000, name="Item")
25
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
- Also normalizes formatting of the body text.
28
+ By default normalizes formatting of the body text and updates the item's body.
29
29
  """
30
30
  item.validate()
31
31
  if item.is_binary:
@@ -37,7 +37,7 @@ def write_item(item: Item, path: Path, normalize: bool = True):
37
37
  _item_cache.delete(path)
38
38
 
39
39
  if normalize:
40
- body = normalize_formatting_ansi(item.body_text(), item.format)
40
+ body = normalize_formatting(item.body_text(), item.format)
41
41
  else:
42
42
  body = item.body_text()
43
43
 
@@ -79,6 +79,9 @@ def write_item(item: Item, path: Path, normalize: bool = True):
79
79
  # Update cache.
80
80
  _item_cache.update(path, item)
81
81
 
82
+ # Update the item's body to reflect normalization.
83
+ item.body = body
84
+
82
85
 
83
86
  def read_item(path: Path, base_dir: Path | None) -> Item:
84
87
  """
@@ -1,15 +1,19 @@
1
+ from functools import cache
1
2
  from pathlib import Path
2
3
 
4
+ from prettyfmt import plural
5
+
3
6
  from kash.config.logger import get_logger
4
7
  from kash.model.items_model import ItemType
5
8
  from kash.utils.file_utils.file_formats_model import FileExt, Format
6
9
  from kash.utils.file_utils.filename_parsing import split_filename
7
- from kash.utils.lang_utils.inflection import plural
8
10
 
9
11
  log = get_logger(__name__)
10
12
 
11
13
 
12
- _type_to_folder = {name: plural(name) for name, _value in ItemType.__members__.items()}
14
+ @cache
15
+ def _get_type_to_folder() -> dict[str, str]:
16
+ return {name: plural(name) for name, _value in ItemType.__members__.items()}
13
17
 
14
18
 
15
19
  def folder_for_type(item_type: ItemType) -> Path:
@@ -22,7 +26,7 @@ def folder_for_type(item_type: ItemType) -> Path:
22
26
  export -> exports
23
27
  etc.
24
28
  """
25
- return Path(_type_to_folder[item_type.name])
29
+ return Path(_get_type_to_folder()[item_type.name])
26
30
 
27
31
 
28
32
  def join_suffix(base_slug: str, full_suffix: str) -> str:
@@ -62,9 +62,9 @@ class HelpIndex:
62
62
  embeddings.to_npz(target_path)
63
63
 
64
64
  key = f"kash_help_embeddings_v1_{len(self.docs)}.npz"
65
- path, _ = cache_file(
65
+ path = cache_file(
66
66
  Loadable(key, save=calculate_and_save_help_embeddings), global_cache=True
67
- )
67
+ ).content.path
68
68
  log.info("Loaded help doc embeddings from: %s", path)
69
69
  return Embeddings.read_from_npz(path)
70
70
 
@@ -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_utils import as_bullet_points
3
+ from kash.utils.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
 
@@ -31,9 +31,6 @@ log = get_logger(__name__)
31
31
  # more customized logging and task stack.
32
32
 
33
33
 
34
- log = get_logger(__name__)
35
-
36
-
37
34
  def filtered_transform(
38
35
  doc: TextDoc,
39
36
  transform_func: TextDocTransform,
@@ -0,0 +1,16 @@
1
+ from functools import cache
2
+
3
+
4
+ @cache
5
+ def init_litellm():
6
+ """
7
+ Configure litellm to suppress overly prominent exception messages.
8
+ Do this lazily since litellm is slow to import.
9
+ """
10
+ try:
11
+ import litellm
12
+ from litellm import _logging # noqa: F401
13
+
14
+ litellm.suppress_debug_info = True # Suppress overly prominent exception messages.
15
+ except ImportError:
16
+ pass
@@ -1,10 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- import litellm
4
3
  from clideps.env_vars.dotenv_utils import env_var_is_set
5
4
  from clideps.env_vars.env_names import EnvName
6
- from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
7
5
 
6
+ from kash.llm_utils.init_litellm import init_litellm
8
7
  from kash.llm_utils.llm_names import LLMName
9
8
  from kash.llm_utils.llms import LLM
10
9
 
@@ -13,6 +12,11 @@ def api_for_model(model: LLMName) -> EnvName | None:
13
12
  """
14
13
  Get the API key name for a model or None if not found.
15
14
  """
15
+ import litellm
16
+ from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
17
+
18
+ init_litellm()
19
+
16
20
  try:
17
21
  _model, custom_llm_provider, _dynamic_api_key, _api_base = get_llm_provider(model)
18
22
  except litellm.exceptions.BadRequestError:
@@ -1,24 +1,27 @@
1
+ from __future__ import annotations
2
+
1
3
  import time
2
- from typing import cast
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, cast
3
6
 
4
- import litellm
5
7
  from flowmark import Wrap, fill_text
6
8
  from funlog import format_duration, log_calls
7
- from litellm.types.utils import Choices, ModelResponse
8
- from litellm.types.utils import Message as LiteLLMMessage
9
9
  from prettyfmt import slugify_snake
10
10
  from pydantic import BaseModel
11
- from pydantic.dataclasses import dataclass
12
11
 
13
12
  from kash.config.logger import get_logger
14
13
  from kash.config.text_styles import EMOJI_TIMING
15
14
  from kash.llm_utils.fuzzy_parsing import is_no_results
15
+ from kash.llm_utils.init_litellm import init_litellm
16
16
  from kash.llm_utils.llm_messages import Message, MessageTemplate
17
17
  from kash.llm_utils.llm_names import LLMName
18
18
  from kash.utils.common.url import Url, is_url
19
19
  from kash.utils.errors import ApiResultError
20
20
  from kash.utils.file_formats.chat_format import ChatHistory, ChatMessage, ChatRole
21
21
 
22
+ if TYPE_CHECKING:
23
+ from litellm.types.utils import Message as LiteLLMMessage
24
+
22
25
  log = get_logger(__name__)
23
26
 
24
27
 
@@ -68,6 +71,10 @@ def llm_completion(
68
71
  """
69
72
  Perform an LLM completion with LiteLLM.
70
73
  """
74
+ import litellm
75
+ from litellm.types.utils import Choices, ModelResponse
76
+
77
+ init_litellm()
71
78
 
72
79
  chat_history = ChatHistory.from_dicts(messages)
73
80
  log.info(
@@ -2,4 +2,4 @@ from pathlib import Path
2
2
 
3
3
  from kash.exec.importing import import_and_register
4
4
 
5
- import_and_register(__package__, Path(__file__).parent, ["."])
5
+ import_and_register(__package__, Path(__file__).parent, ["local_server_commands"])
@@ -1,7 +1,6 @@
1
1
  from kash.config.logger import get_logger
2
2
  from kash.config.settings import global_settings
3
3
  from kash.exec import kash_command
4
- from kash.local_server.local_url_formatters import enable_local_urls
5
4
  from kash.shell.utils.native_utils import tail_file
6
5
  from kash.utils.errors import InvalidState
7
6
 
@@ -17,6 +16,7 @@ def start_ui_server() -> None:
17
16
  tooltips.
18
17
  """
19
18
  from kash.local_server.local_server import start_ui_server
19
+ from kash.local_server.local_url_formatters import enable_local_urls
20
20
 
21
21
  start_ui_server()
22
22
  enable_local_urls(True)
@@ -28,6 +28,7 @@ def stop_ui_server() -> None:
28
28
  Stop the kash local server.
29
29
  """
30
30
  from kash.local_server.local_server import stop_ui_server
31
+ from kash.local_server.local_url_formatters import enable_local_urls
31
32
 
32
33
  stop_ui_server()
33
34
  enable_local_urls(False)
kash/mcp/__init__.py CHANGED
@@ -2,4 +2,4 @@ from pathlib import Path
2
2
 
3
3
  from kash.exec.importing import import_and_register
4
4
 
5
- import_and_register(__package__, Path(__file__).parent, ["."])
5
+ import_and_register(__package__, Path(__file__).parent, ["mcp_server_commands"])
kash/mcp/mcp_cli.py CHANGED
@@ -9,9 +9,10 @@ import logging
9
9
  import os
10
10
  from pathlib import Path
11
11
 
12
+ from clideps.utils.readable_argparse import ReadableColorFormatter
13
+
12
14
  from kash.config.settings import DEFAULT_MCP_SERVER_PORT, LogLevel, global_settings
13
15
  from kash.config.setup import kash_setup
14
- from kash.shell.utils.argparse_utils import WrappedColorFormatter
15
16
  from kash.shell.version import get_version
16
17
 
17
18
  __version__ = get_version()
@@ -27,7 +28,7 @@ log = logging.getLogger()
27
28
  def build_parser():
28
29
  from kash.workspaces.workspaces import global_ws_dir
29
30
 
30
- parser = argparse.ArgumentParser(description=__doc__, formatter_class=WrappedColorFormatter)
31
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=ReadableColorFormatter)
31
32
  parser.add_argument(
32
33
  "--version",
33
34
  action="version",
@@ -6,8 +6,6 @@ from kash.config.settings import (
6
6
  global_settings,
7
7
  )
8
8
  from kash.exec import kash_command
9
- from kash.mcp import mcp_server_routes
10
- from kash.mcp.mcp_cli import MCP_CLI_LOG_PATH
11
9
  from kash.shell.output.shell_formatting import format_name_and_value
12
10
  from kash.shell.output.shell_output import cprint, print_h2
13
11
  from kash.shell.utils.native_utils import tail_file
@@ -21,6 +19,7 @@ def start_mcp_server() -> None:
21
19
  """
22
20
  Start the MCP server, using all currently known actions marked as MCP tools.
23
21
  """
22
+ from kash.mcp import mcp_server_routes
24
23
  from kash.mcp.mcp_server_sse import start_mcp_server_sse
25
24
 
26
25
  mcp_server_routes.publish_mcp_tools()
@@ -42,6 +41,7 @@ def restart_mcp_server() -> None:
42
41
  """
43
42
  Restart the MCP server, republishing all actions marked as MCP tools.
44
43
  """
44
+ from kash.mcp import mcp_server_routes
45
45
  from kash.mcp.mcp_server_sse import restart_mcp_server_sse
46
46
 
47
47
  mcp_server_routes.unpublish_mcp_tools(None)
@@ -57,6 +57,8 @@ def mcp_logs(follow: bool = False, all: bool = False) -> None:
57
57
  :param follow: Follow the file as it grows.
58
58
  :param all: Show all logs, not just the server logs, including Claude Desktop logs if found.
59
59
  """
60
+ from kash.mcp.mcp_cli import MCP_CLI_LOG_PATH
61
+
60
62
  settings = global_settings()
61
63
  if all:
62
64
  global_log_base = settings.system_logs_dir
@@ -96,6 +98,7 @@ def list_mcp_tools() -> None:
96
98
  """
97
99
  List published MCP tools.
98
100
  """
101
+ from kash.mcp import mcp_server_routes
99
102
 
100
103
  tools = mcp_server_routes.get_published_tools()
101
104
 
@@ -118,6 +121,8 @@ def publish_mcp_tool(*action_names: str) -> None:
118
121
  Publish one or more actions as local MCP tools. With no arguments, publish all
119
122
  actions marked as MCP tools.
120
123
  """
124
+ from kash.mcp import mcp_server_routes
125
+
121
126
  if not action_names:
122
127
  log.message("Publishing all actions marked as MCP tools.")
123
128
  mcp_server_routes.publish_mcp_tools()
@@ -131,6 +136,7 @@ def unpublish_mcp_tool(*action_names: str) -> None:
131
136
  Un-publish one or more actions as local MCP tools. With no arguments,
132
137
  un-publish all published actions.
133
138
  """
139
+ from kash.mcp import mcp_server_routes
134
140
 
135
141
  if not action_names:
136
142
  log.message("Un-publishing all actions marked as MCP tools.")
@@ -13,12 +13,13 @@ from strif import AtomicVar
13
13
  from kash.config.capture_output import CapturedOutput, captured_output
14
14
  from kash.config.logger import get_logger
15
15
  from kash.config.settings import global_settings
16
+ from kash.exec import kash_runtime
16
17
  from kash.exec.action_exec import prepare_action_input, run_action_with_caching
17
18
  from kash.exec.action_registry import get_all_actions_defaults, look_up_action_class
18
- from kash.model.actions_model import Action, ActionResult, ExecContext
19
+ from kash.model.actions_model import Action, ActionResult
20
+ from kash.model.exec_model import ExecContext
19
21
  from kash.model.params_model import TypedParamValues
20
22
  from kash.model.paths_model import StorePath
21
- from kash.workspaces.workspaces import current_ws, get_ws
22
23
 
23
24
  log = get_logger(__name__)
24
25
 
@@ -214,9 +215,14 @@ def run_mcp_tool(action_name: str, arguments: dict) -> list[TextContent]:
214
215
  # current workspace, which could be changed by the user by changing working
215
216
  # directories. Maybe confusing?
216
217
  explicit_mcp_ws = global_settings().mcp_ws_dir
217
- ws = get_ws(explicit_mcp_ws) if explicit_mcp_ws else current_ws()
218
218
 
219
- with ws:
219
+ with kash_runtime(
220
+ workspace_dir=explicit_mcp_ws,
221
+ rerun=True, # Enabling rerun always for now, seems good for tools.
222
+ refetch=False, # Using the file caches.
223
+ # Keeping all transient files for now, but maybe make transient?
224
+ override_state=None,
225
+ ) as exec_settings:
220
226
  action_cls = look_up_action_class(action_name)
221
227
 
222
228
  # Extract items array and remaining params from arguments.
@@ -228,14 +234,7 @@ def run_mcp_tool(action_name: str, arguments: dict) -> list[TextContent]:
228
234
  action = action_cls.create(param_values)
229
235
 
230
236
  # Create execution context and assemble action input.
231
- context = ExecContext(
232
- action=action,
233
- workspace_dir=ws.base_dir,
234
- rerun=True, # Enabling rerun always for now, seems good for tools.
235
- refetch=False, # Using the file caches.
236
- # Keeping all transient files for now, but maybe make transient?
237
- override_state=None,
238
- )
237
+ context = ExecContext(action=action, settings=exec_settings)
239
238
  action_input = prepare_action_input(*input_items)
240
239
 
241
240
  result, result_store_paths, _archived_store_paths = run_action_with_caching(