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.
- kash/actions/core/markdownify.py +5 -4
- kash/actions/core/readability.py +4 -4
- kash/actions/core/render_as_html.py +8 -6
- kash/actions/core/show_webpage.py +2 -2
- kash/actions/core/strip_html.py +2 -2
- kash/commands/base/basic_file_commands.py +24 -3
- kash/commands/base/diff_commands.py +38 -3
- kash/commands/base/files_command.py +5 -4
- kash/commands/base/reformat_command.py +1 -1
- kash/commands/base/show_command.py +1 -1
- kash/commands/extras/parse_uv_lock.py +12 -3
- kash/commands/workspace/selection_commands.py +1 -1
- kash/commands/workspace/workspace_commands.py +62 -16
- kash/config/env_settings.py +2 -42
- kash/config/logger.py +30 -25
- kash/config/logger_basic.py +6 -6
- kash/config/settings.py +23 -7
- kash/config/setup.py +33 -5
- kash/config/text_styles.py +25 -22
- kash/docs/load_source_code.py +1 -1
- kash/embeddings/cosine.py +12 -4
- kash/embeddings/embeddings.py +16 -6
- kash/embeddings/text_similarity.py +10 -4
- kash/exec/__init__.py +3 -0
- kash/exec/action_decorators.py +4 -19
- kash/exec/action_exec.py +46 -27
- kash/exec/fetch_url_metadata.py +8 -5
- kash/exec/importing.py +4 -4
- kash/exec/llm_transforms.py +2 -2
- kash/exec/preconditions.py +11 -19
- kash/exec/runtime_settings.py +134 -0
- kash/exec/shell_callable_action.py +5 -3
- kash/file_storage/file_store.py +91 -53
- kash/file_storage/item_file_format.py +6 -3
- kash/file_storage/store_filenames.py +7 -3
- kash/help/help_embeddings.py +2 -2
- kash/llm_utils/clean_headings.py +1 -1
- kash/{text_handling → llm_utils}/custom_sliding_transforms.py +0 -3
- kash/llm_utils/init_litellm.py +16 -0
- kash/llm_utils/llm_api_keys.py +6 -2
- kash/llm_utils/llm_completion.py +12 -5
- kash/local_server/__init__.py +1 -1
- kash/local_server/local_server_commands.py +2 -1
- kash/mcp/__init__.py +1 -1
- kash/mcp/mcp_cli.py +3 -2
- kash/mcp/mcp_server_commands.py +8 -2
- kash/mcp/mcp_server_routes.py +11 -12
- kash/media_base/media_cache.py +10 -3
- kash/media_base/transcription_deepgram.py +15 -2
- kash/model/__init__.py +1 -1
- kash/model/actions_model.py +9 -54
- kash/model/exec_model.py +79 -0
- kash/model/items_model.py +131 -81
- kash/model/operations_model.py +38 -15
- kash/model/paths_model.py +2 -0
- kash/shell/output/shell_output.py +10 -8
- kash/shell/shell_main.py +2 -2
- kash/shell/ui/shell_results.py +2 -1
- kash/shell/utils/exception_printing.py +2 -2
- kash/utils/common/format_utils.py +0 -14
- kash/utils/common/import_utils.py +46 -18
- kash/utils/common/task_stack.py +4 -15
- kash/utils/errors.py +14 -9
- kash/utils/file_utils/file_formats_model.py +61 -26
- kash/utils/file_utils/file_sort_filter.py +10 -3
- kash/utils/file_utils/filename_parsing.py +41 -16
- kash/{text_handling → utils/text_handling}/doc_normalization.py +23 -13
- kash/utils/text_handling/escape_html_tags.py +156 -0
- kash/{text_handling → utils/text_handling}/markdown_utils.py +82 -4
- kash/utils/text_handling/markdownify_utils.py +87 -0
- kash/{text_handling → utils/text_handling}/unified_diffs.py +1 -44
- kash/web_content/file_cache_utils.py +42 -34
- kash/web_content/local_file_cache.py +29 -12
- kash/web_content/web_extract.py +1 -1
- kash/web_content/web_extract_readabilipy.py +4 -2
- kash/web_content/web_fetch.py +42 -7
- kash/web_content/web_page_model.py +2 -1
- kash/web_gen/simple_webpage.py +1 -1
- kash/web_gen/templates/base_styles.css.jinja +139 -16
- kash/web_gen/templates/simple_webpage.html.jinja +1 -1
- kash/workspaces/__init__.py +12 -3
- kash/workspaces/selections.py +2 -2
- kash/workspaces/workspace_dirs.py +58 -0
- kash/workspaces/workspace_importing.py +2 -2
- kash/workspaces/workspace_output.py +2 -2
- kash/workspaces/workspaces.py +26 -90
- kash/xonsh_custom/load_into_xonsh.py +4 -2
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/METADATA +4 -4
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/RECORD +93 -89
- kash/shell/utils/argparse_utils.py +0 -20
- kash/utils/lang_utils/inflection.py +0 -18
- /kash/{text_handling → utils/text_handling}/markdown_render.py +0 -0
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/licenses/LICENSE +0 -0
kash/file_storage/file_store.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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[
|
|
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:
|
|
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
|
|
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
|
|
187
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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.
|
|
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
|
|
299
|
+
# We need to pick the path and filename.
|
|
283
300
|
folder_path = folder_for_type(item.type)
|
|
284
|
-
filename, old_filename = self.
|
|
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,
|
|
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
|
-
|
|
321
|
-
If `
|
|
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(
|
|
368
|
+
store_path, found, old_store_path = self.store_path_for(
|
|
369
|
+
item, as_tmp=as_tmp, overwrite=overwrite
|
|
370
|
+
)
|
|
335
371
|
|
|
336
|
-
if
|
|
337
|
-
log.message(
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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:
|
kash/help/help_embeddings.py
CHANGED
|
@@ -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
|
|
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
|
|
kash/llm_utils/clean_headings.py
CHANGED
|
@@ -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
|
|
|
@@ -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
|
kash/llm_utils/llm_api_keys.py
CHANGED
|
@@ -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:
|
kash/llm_utils/llm_completion.py
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import time
|
|
2
|
-
from
|
|
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(
|
kash/local_server/__init__.py
CHANGED
|
@@ -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
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=
|
|
31
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=ReadableColorFormatter)
|
|
31
32
|
parser.add_argument(
|
|
32
33
|
"--version",
|
|
33
34
|
action="version",
|
kash/mcp/mcp_server_commands.py
CHANGED
|
@@ -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.")
|
kash/mcp/mcp_server_routes.py
CHANGED
|
@@ -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
|
|
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
|
|
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(
|