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
@@ -1,8 +1,9 @@
1
1
  from kash.config.logger import get_logger
2
2
  from kash.exec import kash_action
3
- from kash.exec.preconditions import has_html_body, is_url_item
3
+ from kash.exec.preconditions import has_html_body, is_url_resource
4
4
  from kash.model import Format, Item
5
5
  from kash.model.params_model import common_params
6
+ from kash.utils.text_handling.markdownify_utils import markdownify_custom
6
7
  from kash.web_content.file_cache_utils import get_url_html
7
8
  from kash.web_content.web_extract_readabilipy import extract_text_readabilipy
8
9
 
@@ -10,7 +11,7 @@ log = get_logger(__name__)
10
11
 
11
12
 
12
13
  @kash_action(
13
- precondition=is_url_item | has_html_body,
14
+ precondition=is_url_resource | has_html_body,
14
15
  params=common_params("refetch"),
15
16
  mcp_tool=True,
16
17
  )
@@ -19,12 +20,12 @@ def markdownify(item: Item, refetch: bool = False) -> Item:
19
20
  Converts a URL or raw HTML item to Markdown, fetching with the content
20
21
  cache if needed. Also uses readability to clean up the HTML.
21
22
  """
22
- from markdownify import markdownify as markdownify_convert
23
23
 
24
24
  expiration_sec = 0 if refetch else None
25
25
  url, html_content = get_url_html(item, expiration_sec=expiration_sec)
26
26
  page_data = extract_text_readabilipy(url, html_content)
27
- markdown_content = markdownify_convert(page_data.clean_html)
27
+ assert page_data.clean_html
28
+ markdown_content = markdownify_custom(page_data.clean_html)
28
29
 
29
30
  output_item = item.derived_copy(format=Format.markdown, body=markdown_content)
30
31
  return output_item
@@ -1,6 +1,6 @@
1
1
  from kash.config.logger import get_logger
2
2
  from kash.exec import kash_action
3
- from kash.exec.preconditions import has_html_body, is_url_item
3
+ from kash.exec.preconditions import has_html_body, is_url_resource
4
4
  from kash.model import Format, Item
5
5
  from kash.model.params_model import common_params
6
6
  from kash.web_content.file_cache_utils import get_url_html
@@ -10,7 +10,7 @@ log = get_logger(__name__)
10
10
 
11
11
 
12
12
  @kash_action(
13
- precondition=is_url_item | has_html_body,
13
+ precondition=is_url_resource | has_html_body,
14
14
  params=common_params("refetch"),
15
15
  mcp_tool=True,
16
16
  )
@@ -20,8 +20,8 @@ def readability(item: Item, refetch: bool = False) -> Item:
20
20
  See `markdownify` to also convert to Markdown.
21
21
  """
22
22
  expiration_sec = 0 if refetch else None
23
- url, html_content = get_url_html(item, expiration_sec=expiration_sec)
24
- page_data = extract_text_readabilipy(url, html_content)
23
+ locator, html_content = get_url_html(item, expiration_sec=expiration_sec)
24
+ page_data = extract_text_readabilipy(locator, html_content)
25
25
 
26
26
  output_item = item.derived_copy(format=Format.html, body=page_data.clean_html)
27
27
 
@@ -1,7 +1,7 @@
1
1
  from kash.actions.core.tabbed_webpage_config import tabbed_webpage_config
2
2
  from kash.actions.core.tabbed_webpage_generate import tabbed_webpage_generate
3
3
  from kash.exec import kash_action
4
- from kash.exec.preconditions import has_full_html_page_body, has_text_body, is_html
4
+ from kash.exec.preconditions import has_full_html_page_body, has_html_body, has_simple_text_body
5
5
  from kash.exec_model.args_model import ONE_OR_MORE_ARGS
6
6
  from kash.model import ActionInput, ActionResult, Param
7
7
  from kash.model.items_model import ItemType
@@ -11,10 +11,10 @@ from kash.web_gen.simple_webpage import simple_webpage_render
11
11
 
12
12
  @kash_action(
13
13
  expected_args=ONE_OR_MORE_ARGS,
14
- precondition=(is_html | has_text_body) & ~has_full_html_page_body,
15
- params=(Param("add_title", "Add a title to the page body.", type=bool),),
14
+ precondition=(has_html_body | has_simple_text_body) & ~has_full_html_page_body,
15
+ params=(Param("no_title", "Don't add a title to the page body.", type=bool),),
16
16
  )
17
- def render_as_html(input: ActionInput, add_title: bool = False) -> ActionResult:
17
+ def render_as_html(input: ActionInput, no_title: bool = False) -> ActionResult:
18
18
  """
19
19
  Convert text, Markdown, or HTML to pretty, formatted HTML using a clean
20
20
  and simple page template. Supports GFM-flavored Markdown tables and footnotes.
@@ -27,11 +27,13 @@ def render_as_html(input: ActionInput, add_title: bool = False) -> ActionResult:
27
27
  """
28
28
  if len(input.items) == 1:
29
29
  input_item = input.items[0]
30
- html_body = simple_webpage_render(input_item, add_title_h1=add_title)
30
+ html_body = simple_webpage_render(input_item, add_title_h1=not no_title)
31
31
  result_item = input_item.derived_copy(
32
32
  type=ItemType.export, format=Format.html, body=html_body
33
33
  )
34
34
  return ActionResult([result_item])
35
35
  else:
36
36
  config_result = tabbed_webpage_config(input)
37
- return tabbed_webpage_generate(ActionInput(items=config_result.items), add_title=add_title)
37
+ return tabbed_webpage_generate(
38
+ ActionInput(items=config_result.items), add_title=not no_title
39
+ )
@@ -1,7 +1,7 @@
1
1
  from kash.actions.core.render_as_html import render_as_html
2
2
  from kash.commands.base.show_command import show
3
3
  from kash.exec import kash_action
4
- from kash.exec.preconditions import has_full_html_page_body, has_text_body, is_html
4
+ from kash.exec.preconditions import has_full_html_page_body, has_html_body, has_simple_text_body
5
5
  from kash.exec_model.args_model import ONE_OR_MORE_ARGS
6
6
  from kash.exec_model.commands_model import Command
7
7
  from kash.exec_model.shell_model import ShellResult
@@ -10,7 +10,7 @@ from kash.model import ActionInput, ActionResult
10
10
 
11
11
  @kash_action(
12
12
  expected_args=ONE_OR_MORE_ARGS,
13
- precondition=(is_html | has_text_body) & ~has_full_html_page_body,
13
+ precondition=(has_html_body | has_simple_text_body) & ~has_full_html_page_body,
14
14
  )
15
15
  def show_webpage(input: ActionInput) -> ActionResult:
16
16
  """
@@ -1,6 +1,6 @@
1
1
  from kash.config.logger import get_logger
2
2
  from kash.exec import kash_action
3
- from kash.exec.preconditions import has_html_body, has_text_body
3
+ from kash.exec.preconditions import has_html_body, has_simple_text_body
4
4
  from kash.model import Format, Item
5
5
  from kash.utils.common.format_utils import html_to_plaintext
6
6
  from kash.utils.errors import InvalidInput
@@ -8,7 +8,7 @@ from kash.utils.errors import InvalidInput
8
8
  log = get_logger(__name__)
9
9
 
10
10
 
11
- @kash_action(precondition=has_html_body | has_text_body)
11
+ @kash_action(precondition=has_html_body | has_simple_text_body)
12
12
  def strip_html(item: Item) -> Item:
13
13
  """
14
14
  Strip HTML tags from HTML or Markdown. This is a simple filter, simply searching
@@ -2,7 +2,7 @@ import os
2
2
 
3
3
  from frontmatter_format import fmf_read_raw, fmf_strip_frontmatter
4
4
  from prettyfmt import fmt_lines
5
- from strif import copyfile_atomic
5
+ from strif import atomic_output_file, copyfile_atomic
6
6
 
7
7
  from kash.config.logger import get_logger
8
8
  from kash.config.text_styles import STYLE_EMPH
@@ -28,10 +28,10 @@ log = get_logger(__name__)
28
28
 
29
29
 
30
30
  @kash_command
31
- def cbcopy(path: str | None = None, raw: bool = False) -> None:
31
+ def clipboard_copy(path: str | None = None, raw: bool = False) -> None:
32
32
  """
33
33
  Copy the contents of a file (or the first file in the selection) to the OS-native
34
- clipboard.
34
+ clipboard. Similar to `pbcopy` on macOS.
35
35
 
36
36
  :param raw: Copy the full exact contents of the file. Otherwise frontmatter is omitted.
37
37
  """
@@ -39,6 +39,8 @@ def cbcopy(path: str | None = None, raw: bool = False) -> None:
39
39
  import pyperclip
40
40
 
41
41
  input_paths = assemble_path_args(path)
42
+ if not input_paths:
43
+ raise InvalidInput("No path provided")
42
44
  input_path = input_paths[0]
43
45
 
44
46
  format = detect_file_format(input_path)
@@ -69,6 +71,25 @@ def cbcopy(path: str | None = None, raw: bool = False) -> None:
69
71
  )
70
72
 
71
73
 
74
+ @kash_command
75
+ def clipboard_paste(path: str = "untitled_paste.txt") -> None:
76
+ """
77
+ Paste the contents of the OS-native clipboard into a new file.
78
+ """
79
+ # TODO: Get this to work for images!
80
+ # And can we convert rich text to Markdown?
81
+ import pyperclip
82
+
83
+ contents = pyperclip.paste()
84
+ if not contents.strip():
85
+ raise InvalidInput("Clipboard is empty")
86
+
87
+ with atomic_output_file(path, backup_suffix=".{timestamp}.bak") as f:
88
+ f.write_text(contents)
89
+
90
+ print_status("Pasted clipboard contents to:\n%s", fmt_lines([fmt_loc(path)]))
91
+
92
+
72
93
  @kash_command
73
94
  def edit(path: str | None = None, all: bool = False) -> None:
74
95
  """
@@ -5,16 +5,51 @@ from kash.commands.workspace.selection_commands import select
5
5
  from kash.config.logger import get_logger
6
6
  from kash.exec import import_locator_args, kash_command
7
7
  from kash.exec_model.shell_model import ShellResult
8
- from kash.model.items_model import Item, ItemType
8
+ from kash.model.items_model import Item, ItemRelations, ItemType
9
+ from kash.model.paths_model import StorePath
9
10
  from kash.shell.output.shell_output import Wrap, cprint
10
- from kash.text_handling.unified_diffs import unified_diff_files, unified_diff_items
11
- from kash.utils.errors import InvalidInput, InvalidOperation
11
+ from kash.utils.errors import ContentError, InvalidInput, InvalidOperation
12
12
  from kash.utils.file_utils.file_formats_model import Format
13
+ from kash.utils.text_handling.unified_diffs import unified_diff, unified_diff_files
13
14
  from kash.workspaces import current_ws
14
15
 
15
16
  log = get_logger(__name__)
16
17
 
17
18
 
19
+ def unified_diff_items(from_item: Item, to_item: Item, strict: bool = True) -> Item:
20
+ """
21
+ Generate a unified diff between two items. If `strict` is true, will raise
22
+ an error if the items are of different formats.
23
+ """
24
+ if not from_item.body and not to_item.body:
25
+ raise ContentError(f"No body to diff for {from_item} and {to_item}")
26
+ if not from_item.store_path or not to_item.store_path:
27
+ raise ContentError("No store path on items; save before diffing")
28
+ diff_items = [item for item in [from_item, to_item] if item.format == Format.diff]
29
+ if len(diff_items) == 1:
30
+ raise ContentError(
31
+ f"Cannot compare diffs to non-diffs: {from_item.format}, {to_item.format}"
32
+ )
33
+ if len(diff_items) > 0 or from_item.format != to_item.format:
34
+ msg = f"Diffing items of incompatible format: {from_item.format}, {to_item.format}"
35
+ if strict:
36
+ raise ContentError(msg)
37
+ else:
38
+ log.warning("%s", msg)
39
+
40
+ from_path, to_path = StorePath(from_item.store_path), StorePath(to_item.store_path)
41
+
42
+ diff = unified_diff(from_item.body, to_item.body, str(from_path), str(to_path))
43
+
44
+ return Item(
45
+ type=ItemType.doc,
46
+ title=f"Diff of {from_path} and {to_path}",
47
+ format=Format.diff,
48
+ relations=ItemRelations(diff_of=[from_path, to_path]),
49
+ body=diff.patch_text,
50
+ )
51
+
52
+
18
53
  @kash_command
19
54
  def diff_items(*paths: str, force: bool = False) -> ShellResult:
20
55
  """
@@ -75,7 +75,8 @@ def _print_listing_tallies(
75
75
  cprint("(use --no_max to remove cutoff)", style=STYLE_HINT)
76
76
 
77
77
 
78
- DEFAULT_MAX_PG = 100
78
+ DEFAULT_MAX_PER_GROUP = 50
79
+ """Default maximum number of files to display per group."""
79
80
 
80
81
 
81
82
  @kash_command
@@ -301,13 +302,13 @@ def files(
301
302
  # there are lots of groups and lots of files per group.
302
303
  # Default is max 100 per group but if we have 4 * 100 items, cut to 25.
303
304
  # If we have 2 * 100 items, cut to 50.
304
- final_max_pg = DEFAULT_MAX_PG if cap_per_group else max_per_group
305
+ final_max_pg = DEFAULT_MAX_PER_GROUP if cap_per_group else max_per_group
305
306
  max_pg_explicit = max_per_group > 0
306
307
  if not max_pg_explicit:
307
308
  group_lens = [len(group_df) for group_df in grouped]
308
309
  for ratio in [2, 4]:
309
- if sum(group_lens) > ratio * DEFAULT_MAX_PG:
310
- final_max_pg = int(DEFAULT_MAX_PG / ratio)
310
+ if sum(group_lens) > ratio * DEFAULT_MAX_PER_GROUP:
311
+ final_max_pg = int(DEFAULT_MAX_PER_GROUP / ratio)
311
312
 
312
313
  total_displayed = 0
313
314
  total_displayed_size = 0
@@ -8,9 +8,9 @@ from kash.config.logger import get_logger
8
8
  from kash.exec import assemble_path_args, kash_command, resolvable_paths
9
9
  from kash.exec_model.shell_model import ShellResult
10
10
  from kash.shell.output.shell_output import print_status
11
- from kash.text_handling.doc_normalization import normalize_text_file
12
11
  from kash.utils.common.format_utils import fmt_loc
13
12
  from kash.utils.file_utils.filename_parsing import join_filename, split_filename
13
+ from kash.utils.text_handling.doc_normalization import normalize_text_file
14
14
 
15
15
  log = get_logger(__name__)
16
16
 
@@ -54,7 +54,7 @@ def show(
54
54
  item = ws.load(input_path)
55
55
  if thumbnail and item.thumbnail_url:
56
56
  try:
57
- local_path, _was_cached = cache_file(item.thumbnail_url)
57
+ local_path = cache_file(item.thumbnail_url).content.path
58
58
  terminal_show_image(local_path)
59
59
  except Exception as e:
60
60
  log.info("Had trouble showing thumbnail image (will skip): %s", e)
@@ -1,8 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import subprocess
2
4
  import tomllib
3
5
  from pathlib import Path
6
+ from typing import TYPE_CHECKING
4
7
 
5
- import pandas as pd
6
8
  from packaging.tags import Tag, sys_tags
7
9
  from packaging.utils import parse_wheel_filename
8
10
  from prettyfmt import fmt_size_dual
@@ -13,6 +15,9 @@ from kash.config.text_styles import COLOR_STATUS
13
15
  from kash.exec import kash_command
14
16
  from kash.shell.output.shell_output import cprint
15
17
 
18
+ if TYPE_CHECKING:
19
+ from pandas import DataFrame
20
+
16
21
  log = get_logger(__name__)
17
22
 
18
23
 
@@ -49,13 +54,15 @@ def get_platform() -> str:
49
54
  return next(sys_tags()).platform
50
55
 
51
56
 
52
- def parse_uv_lock(lock_path: Path) -> pd.DataFrame:
57
+ def parse_uv_lock(lock_path: Path) -> DataFrame:
53
58
  """
54
59
  Return one row per package from a uv.lock file, selecting the best
55
60
  matching wheel for the current interpreter or falling back to the sdist.
56
61
 
57
62
  Columns: name, version, registry, file_type, url, hash, size, filename.
58
63
  """
64
+ from pandas import DataFrame
65
+
59
66
  with open(lock_path, "rb") as f:
60
67
  data = tomllib.load(f)
61
68
 
@@ -88,7 +95,7 @@ def parse_uv_lock(lock_path: Path) -> pd.DataFrame:
88
95
  }
89
96
  )
90
97
 
91
- return pd.DataFrame(rows)
98
+ return DataFrame(rows)
92
99
 
93
100
 
94
101
  def uv_runtime_packages(
@@ -141,6 +148,8 @@ def uv_dep_info(
141
148
  By default, filters to show only 'main' dependencies from pyproject.toml.
142
149
  Helpful for looking at sizes of dependencies.
143
150
  """
151
+ import pandas as pd
152
+
144
153
  uv_lock_path = Path(uv_lock)
145
154
  pyproject_path = Path(pyproject)
146
155
 
@@ -2,6 +2,7 @@ from os.path import basename
2
2
  from pathlib import Path
3
3
 
4
4
  from frontmatter_format import fmf_strip_frontmatter
5
+ from prettyfmt import plural
5
6
  from strif import copyfile_atomic
6
7
 
7
8
  from kash.config.logger import get_logger
@@ -11,7 +12,6 @@ from kash.model.paths_model import StorePath
11
12
  from kash.shell.ui.shell_results import shell_print_selection_history
12
13
  from kash.utils.common.format_utils import fmt_loc
13
14
  from kash.utils.errors import InvalidInput
14
- from kash.utils.lang_utils.inflection import plural
15
15
  from kash.workspaces import Selection, current_ws
16
16
 
17
17
  log = get_logger(__name__)
@@ -2,7 +2,7 @@ import os
2
2
  from pathlib import Path
3
3
 
4
4
  from frontmatter_format import to_yaml_string
5
- from prettyfmt import fmt_lines
5
+ from prettyfmt import fmt_lines, plural
6
6
  from rich.text import Text
7
7
 
8
8
  from kash.commands.base.basic_file_commands import trash
@@ -26,7 +26,7 @@ from kash.exec.action_registry import get_all_actions_defaults
26
26
  from kash.exec.fetch_url_metadata import fetch_url_metadata
27
27
  from kash.exec.precondition_checks import actions_matching_paths
28
28
  from kash.exec.precondition_registry import get_all_preconditions
29
- from kash.exec.preconditions import is_url_item
29
+ from kash.exec.preconditions import is_url_resource
30
30
  from kash.exec_model.shell_model import ShellResult
31
31
  from kash.local_server.local_url_formatters import local_url_formatter
32
32
  from kash.media_base import media_tools
@@ -54,11 +54,11 @@ from kash.utils.common.format_utils import fmt_loc
54
54
  from kash.utils.common.obj_replace import remove_values
55
55
  from kash.utils.common.parse_key_vals import parse_key_value
56
56
  from kash.utils.common.type_utils import not_none
57
- from kash.utils.common.url import Url, is_url
57
+ from kash.utils.common.url import Url, is_url, parse_http_url
58
58
  from kash.utils.errors import InvalidInput
59
59
  from kash.utils.file_formats.chat_format import tail_chat_history
60
60
  from kash.utils.file_utils.dir_info import is_nonempty_dir
61
- from kash.utils.lang_utils.inflection import plural
61
+ from kash.utils.file_utils.file_formats_model import Format
62
62
  from kash.web_content.file_cache_utils import cache_file
63
63
  from kash.workspaces import (
64
64
  current_ws,
@@ -181,15 +181,15 @@ def cache_content(*urls_or_paths: str, refetch: bool = False) -> None:
181
181
  PrintHooks.spacer()
182
182
  for url_or_path in urls_or_paths:
183
183
  locator = resolve_locator_arg(url_or_path)
184
- cache_path, was_cached = cache_file(locator, expiration_sec=expiration_sec)
185
- cache_str = " (already cached)" if was_cached else ""
184
+ cache_result = cache_file(locator, expiration_sec=expiration_sec)
185
+ cache_str = " (already cached)" if cache_result.was_cached else ""
186
186
  cprint(f"{fmt_loc(url_or_path)}{cache_str}:", style=STYLE_EMPH, text_wrap=Wrap.NONE)
187
- cprint(f"{cache_path}", text_wrap=Wrap.INDENT_ONLY)
187
+ cprint(f"{cache_result.content.path}", text_wrap=Wrap.INDENT_ONLY)
188
188
  PrintHooks.spacer()
189
189
 
190
190
 
191
191
  @kash_command
192
- def download(*urls_or_paths: str, refetch: bool = False) -> None:
192
+ def download(*urls_or_paths: str, refetch: bool = False) -> ShellResult:
193
193
  """
194
194
  Download a URL or resource. Uses cached content if available, unless `refetch` is true.
195
195
  Inputs can be URLs or paths to URL resources.
@@ -198,31 +198,49 @@ def download(*urls_or_paths: str, refetch: bool = False) -> None:
198
198
 
199
199
  # TODO: Add option to include frontmatter metadata for text files.
200
200
  ws = current_ws()
201
+ saved_paths = []
201
202
  for url_or_path in urls_or_paths:
202
203
  locator = resolve_locator_arg(url_or_path)
203
- url = None
204
+ url: Url | None = None
204
205
  if not isinstance(locator, Path) and is_url(locator):
205
206
  url = Url(locator)
206
207
  if isinstance(locator, StorePath):
207
208
  url_item = ws.load(locator)
208
- if is_url_item(url_item):
209
+ if is_url_resource(url_item):
209
210
  url = url_item.url
210
211
  if not url:
211
212
  raise InvalidInput(f"Not a URL or URL resource: {fmt_loc(locator)}")
212
213
 
213
214
  if is_media_url(url):
214
- store_path = ws.import_item(locator, as_type=ItemType.resource)
215
215
  log.message(
216
- "URL is a media URL, so added as a resource and will cache media: %s", fmt_loc(url)
216
+ "URL is a media URL, so adding as a resource and will cache media: %s", fmt_loc(url)
217
217
  )
218
+ store_path = ws.import_item(locator, as_type=ItemType.resource)
218
219
  media_tools.cache_media(url)
219
220
  else:
220
221
  log.message("Will cache file and save to workspace: %s", fmt_loc(url))
221
- cache_path, _was_cached = cache_file(url, expiration_sec=expiration_sec)
222
- item = Item.from_external_path(cache_path, item_type=ItemType.resource)
222
+ original_filename = Path(parse_http_url(url).path).name
223
+ cache_result = cache_file(url, expiration_sec=expiration_sec)
224
+ # If available, use the mime type to help set item file extension.
225
+ mime_type = cache_result.content.headers and cache_result.content.headers.mime_type
226
+ item = Item.from_external_path(
227
+ cache_result.content.path,
228
+ ItemType.resource,
229
+ mime_type=mime_type,
230
+ original_filename=original_filename,
231
+ )
223
232
  store_path = ws.save(item)
233
+ saved_paths.append(store_path)
234
+
235
+ print_status(
236
+ "Downloaded %s %s:\n%s",
237
+ len(urls_or_paths),
238
+ plural("item", len(urls_or_paths)),
239
+ fmt_lines(saved_paths),
240
+ )
241
+ select(*saved_paths)
224
242
 
225
- log.info("Saved item to workspace: %s", fmt_loc(store_path))
243
+ return ShellResult(show_selection=True)
226
244
 
227
245
 
228
246
  @kash_command
@@ -437,7 +455,7 @@ def import_item(
437
455
  *files_or_urls: str, type: ItemType | None = None, inplace: bool = False
438
456
  ) -> ShellResult:
439
457
  """
440
- Add a file or URL resource to the workspace as an item, with associated metadata.
458
+ Add a file or URL resource to the workspace as an item.
441
459
 
442
460
  :param inplace: If set and the item is already in the store, reimport the item,
443
461
  adding or rewriting metadata frontmatter.
@@ -464,6 +482,34 @@ def import_item(
464
482
  return ShellResult(show_selection=True)
465
483
 
466
484
 
485
+ @kash_command
486
+ def import_clipboard(
487
+ title: str | None = "pasted_text",
488
+ type: ItemType = ItemType.resource,
489
+ format: Format = Format.plaintext,
490
+ ) -> ShellResult:
491
+ """
492
+ Import the contents of the OS-native clipboard as a new item in the workspace.
493
+
494
+ :param title: The title of the new item (default: "pasted_text").
495
+ :param type: The type of the new item (default: resource).
496
+ :param format: The format of the new item (default: plaintext).
497
+ """
498
+ import pyperclip
499
+
500
+ contents = pyperclip.paste()
501
+ if not contents.strip():
502
+ raise InvalidInput("Clipboard is empty")
503
+
504
+ ws = current_ws()
505
+ store_path = ws.save(Item(type=type, format=format, title=title, body=contents))
506
+
507
+ print_status("Imported clipboard contents to:\n%s", fmt_lines([fmt_loc(store_path)]))
508
+ select(store_path)
509
+
510
+ return ShellResult(show_selection=True)
511
+
512
+
467
513
  @kash_command
468
514
  def fetch_metadata(*files_or_urls: str, refetch: bool = False) -> ShellResult:
469
515
  """
@@ -1,10 +1,7 @@
1
- import os
2
- from enum import Enum
3
- from pathlib import Path
4
- from typing import overload
1
+ from clideps.env_vars.env_enum import EnvEnum
5
2
 
6
3
 
7
- class KashEnv(str, Enum):
4
+ class KashEnv(EnvEnum):
8
5
  """
9
6
  Environment variable settings for kash. None are required, but these may be
10
7
  used to override default values.
@@ -33,40 +30,3 @@ class KashEnv(str, Enum):
33
30
 
34
31
  KASH_USER_AGENT = "KASH_USER_AGENT"
35
32
  """The user agent to use for HTTP requests."""
36
-
37
- @overload
38
- def read_str(self) -> str | None: ...
39
-
40
- @overload
41
- def read_str(self, default: str) -> str: ...
42
-
43
- def read_str(self, default: str | None = None) -> str | None:
44
- """
45
- Get the value of the environment variable from the environment (with
46
- optional default).
47
- """
48
- return os.environ.get(self.value, default)
49
-
50
- @overload
51
- def read_path(self) -> Path | None: ...
52
-
53
- @overload
54
- def read_path(self, default: Path) -> Path: ...
55
-
56
- def read_path(self, default: Path | None = None) -> Path | None:
57
- """
58
- Get the value of the environment variable as a resolved path (with
59
- optional default).
60
- """
61
- value = os.environ.get(self.value)
62
- if value:
63
- return Path(value).expanduser().resolve()
64
- else:
65
- return default.expanduser().resolve() if default else None
66
-
67
- def read_bool(self, default: bool = False) -> bool:
68
- """
69
- Get the value of the environment variable as a boolean.
70
- """
71
- value = str(os.environ.get(self.value, default) or "").lower()
72
- return bool(value and value != "0" and value != "false" and value != "no")