kash-shell 0.3.17__py3-none-any.whl → 0.3.20__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 (49) hide show
  1. kash/actions/core/{markdownify.py → markdownify_html.py} +3 -6
  2. kash/actions/core/minify_html.py +41 -0
  3. kash/commands/base/show_command.py +11 -1
  4. kash/commands/workspace/workspace_commands.py +10 -88
  5. kash/config/colors.py +6 -2
  6. kash/docs/markdown/topics/a1_what_is_kash.md +52 -23
  7. kash/docs/markdown/topics/a2_installation.md +17 -30
  8. kash/docs/markdown/topics/a3_getting_started.md +5 -19
  9. kash/exec/__init__.py +3 -0
  10. kash/exec/action_exec.py +3 -3
  11. kash/exec/fetch_url_items.py +109 -0
  12. kash/exec/precondition_registry.py +3 -3
  13. kash/file_storage/file_store.py +24 -1
  14. kash/file_storage/store_filenames.py +4 -0
  15. kash/help/function_param_info.py +1 -1
  16. kash/help/help_pages.py +1 -1
  17. kash/help/help_printing.py +1 -1
  18. kash/llm_utils/llm_features.py +5 -1
  19. kash/llm_utils/llms.py +18 -8
  20. kash/media_base/media_cache.py +48 -24
  21. kash/media_base/media_services.py +63 -14
  22. kash/media_base/services/local_file_media.py +9 -1
  23. kash/model/items_model.py +22 -8
  24. kash/model/media_model.py +9 -1
  25. kash/model/params_model.py +9 -3
  26. kash/utils/common/function_inspect.py +97 -1
  27. kash/utils/common/parse_docstring.py +347 -0
  28. kash/utils/common/testing.py +58 -0
  29. kash/utils/common/url_slice.py +329 -0
  30. kash/utils/file_utils/file_formats.py +1 -1
  31. kash/utils/text_handling/markdown_utils.py +424 -16
  32. kash/web_content/web_extract.py +34 -15
  33. kash/web_content/web_page_model.py +10 -1
  34. kash/web_gen/templates/base_styles.css.jinja +137 -15
  35. kash/web_gen/templates/base_webpage.html.jinja +13 -17
  36. kash/web_gen/templates/components/toc_scripts.js.jinja +319 -0
  37. kash/web_gen/templates/components/toc_styles.css.jinja +284 -0
  38. kash/web_gen/templates/components/tooltip_scripts.js.jinja +730 -0
  39. kash/web_gen/templates/components/tooltip_styles.css.jinja +482 -0
  40. kash/web_gen/templates/content_styles.css.jinja +13 -8
  41. kash/web_gen/templates/simple_webpage.html.jinja +15 -481
  42. kash/workspaces/workspaces.py +10 -1
  43. {kash_shell-0.3.17.dist-info → kash_shell-0.3.20.dist-info}/METADATA +75 -72
  44. {kash_shell-0.3.17.dist-info → kash_shell-0.3.20.dist-info}/RECORD +47 -40
  45. kash/exec/fetch_url_metadata.py +0 -72
  46. kash/help/docstring_utils.py +0 -111
  47. {kash_shell-0.3.17.dist-info → kash_shell-0.3.20.dist-info}/WHEEL +0 -0
  48. {kash_shell-0.3.17.dist-info → kash_shell-0.3.20.dist-info}/entry_points.txt +0 -0
  49. {kash_shell-0.3.17.dist-info → kash_shell-0.3.20.dist-info}/licenses/LICENSE +0 -0
@@ -11,13 +11,10 @@ from kash.web_content.web_extract_readabilipy import extract_text_readabilipy
11
11
  log = get_logger(__name__)
12
12
 
13
13
 
14
- @kash_action(
15
- precondition=is_url_resource | has_html_body,
16
- mcp_tool=True,
17
- )
18
- def markdownify(item: Item) -> Item:
14
+ @kash_action(precondition=is_url_resource | has_html_body, mcp_tool=True)
15
+ def markdownify_html(item: Item) -> Item:
19
16
  """
20
- Converts a URL or raw HTML item to Markdown, fetching with the content
17
+ Converts raw HTML or the URL of an HTML page to Markdown, fetching with the content
21
18
  cache if needed. Also uses readability to clean up the HTML.
22
19
  """
23
20
 
@@ -0,0 +1,41 @@
1
+ from kash.exec import kash_action
2
+ from kash.exec.preconditions import has_fullpage_html_body
3
+ from kash.model import Format, Item, Param
4
+ from kash.utils.errors import InvalidInput
5
+ from kash.workspaces.workspaces import current_ws
6
+
7
+
8
+ @kash_action(
9
+ precondition=has_fullpage_html_body,
10
+ params=(
11
+ Param("no_js_min", "Disable JS minification", bool),
12
+ Param("no_css_min", "Disable CSS minification", bool),
13
+ ),
14
+ )
15
+ def minify_html(item: Item) -> Item:
16
+ """
17
+ Minify an HTML item's content using [html-minifier-terser](https://github.com/terser/html-minifier-terser).
18
+
19
+ Also supports Tailwind CSS v4 compilation and inlining, if any Tailwind
20
+ CSS v4 CDN script tags are found.
21
+
22
+ The terser minification seems a bit slower but more robust than
23
+ [minify-html](https://github.com/wilsonzlin/minify-html).
24
+ """
25
+ from minify_tw_html import minify_tw_html
26
+
27
+ if not item.store_path:
28
+ raise InvalidInput(f"Missing store path: {item}")
29
+
30
+ ws = current_ws()
31
+ input_path = ws.base_dir / item.store_path
32
+
33
+ output_item = item.derived_copy(format=Format.html, body=None)
34
+ output_path = ws.target_path_for(output_item)
35
+
36
+ minify_tw_html(input_path, output_path)
37
+
38
+ output_item.body = output_path.read_text()
39
+ output_item.external_path = str(output_path) # Indicate item is already saved.
40
+
41
+ return output_item
@@ -1,6 +1,7 @@
1
1
  from kash.config.logger import get_logger
2
2
  from kash.config.text_styles import STYLE_HINT
3
3
  from kash.exec import assemble_path_args, kash_command
4
+ from kash.exec_model.shell_model import ShellResult
4
5
  from kash.model.paths_model import StorePath
5
6
  from kash.shell.output.shell_output import cprint
6
7
  from kash.shell.utils.native_utils import ViewMode, terminal_show_image, view_file_native
@@ -19,7 +20,8 @@ def show(
19
20
  thumbnail: bool = False,
20
21
  browser: bool = False,
21
22
  plain: bool = False,
22
- ) -> None:
23
+ noselect: bool = False,
24
+ ) -> ShellResult:
23
25
  """
24
26
  Show the contents of a file if one is given, or the first file if multiple files
25
27
  are selected. Will try to use native apps or web browser to display the file if
@@ -33,6 +35,7 @@ def show(
33
35
  :param thumbnail: If there is a thumbnail image, show it too.
34
36
  :param browser: Force display with your default web browser.
35
37
  :param plain: Use plain view in the console (this is `bat`'s `plain` style).
38
+ :param noselect: Disable default behavior where `show` also will `select` the file.
36
39
  """
37
40
  view_mode = (
38
41
  ViewMode.console
@@ -63,9 +66,16 @@ def show(
63
66
  view_file_native(ws.base_dir / input_path, view_mode=view_mode, plain=plain)
64
67
  else:
65
68
  view_file_native(input_path, view_mode=view_mode, plain=plain)
69
+ if not noselect:
70
+ from kash.commands.workspace.selection_commands import select
71
+
72
+ select(input_path)
73
+ return ShellResult(show_selection=True)
66
74
  except (InvalidInput, InvalidState):
67
75
  if path:
68
76
  # If path is absolute or we couldbn't get a selection, just show the file.
69
77
  view_file_native(path, view_mode=view_mode)
70
78
  else:
71
79
  raise InvalidInput("No selection")
80
+
81
+ return ShellResult(show_selection=False)
@@ -23,14 +23,12 @@ from kash.exec import (
23
23
  resolve_locator_arg,
24
24
  )
25
25
  from kash.exec.action_registry import get_all_actions_defaults
26
- from kash.exec.fetch_url_metadata import fetch_url_metadata
26
+ from kash.exec.fetch_url_items import fetch_url_item
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_resource
30
29
  from kash.exec_model.shell_model import ShellResult
31
30
  from kash.local_server.local_url_formatters import local_url_formatter
32
31
  from kash.media_base import media_tools
33
- from kash.media_base.media_services import is_media_url
34
32
  from kash.model.items_model import Item, ItemType
35
33
  from kash.model.params_model import GLOBAL_PARAMS
36
34
  from kash.model.paths_model import StorePath, fmt_store_path
@@ -54,12 +52,11 @@ from kash.utils.common.format_utils import fmt_loc
54
52
  from kash.utils.common.obj_replace import remove_values
55
53
  from kash.utils.common.parse_key_vals import parse_key_value
56
54
  from kash.utils.common.type_utils import not_none
57
- from kash.utils.common.url import Url, is_url, parse_http_url
55
+ from kash.utils.common.url import Url
58
56
  from kash.utils.errors import InvalidInput
59
57
  from kash.utils.file_formats.chat_format import tail_chat_history
60
58
  from kash.utils.file_utils.dir_info import is_nonempty_dir
61
59
  from kash.utils.file_utils.file_formats_model import Format
62
- from kash.utils.text_handling.doc_normalization import can_normalize
63
60
  from kash.web_content.file_cache_utils import cache_file
64
61
  from kash.workspaces import (
65
62
  current_ws,
@@ -189,85 +186,6 @@ def cache_content(*urls_or_paths: str, refetch: bool = False) -> None:
189
186
  PrintHooks.spacer()
190
187
 
191
188
 
192
- @kash_command
193
- def download(*urls_or_paths: str, refetch: bool = False, no_format: bool = False) -> ShellResult:
194
- """
195
- Download a URL or resource. Uses cached content if available, unless `refetch` is true.
196
- Inputs can be URLs or paths to URL resources.
197
- Creates both resource and document versions for text content.
198
-
199
- :param no_format: If true, do not also normalize Markdown content.
200
- """
201
- ws = current_ws()
202
- saved_paths = []
203
-
204
- for url_or_path in urls_or_paths:
205
- locator = resolve_locator_arg(url_or_path)
206
- url: Url | None = None
207
-
208
- # Get the URL from the locator
209
- if not isinstance(locator, Path) and is_url(locator):
210
- url = Url(locator)
211
- elif isinstance(locator, StorePath):
212
- url_item = ws.load(locator)
213
- if is_url_resource(url_item):
214
- url = url_item.url
215
-
216
- if not url:
217
- raise InvalidInput(f"Not a URL or URL resource: {fmt_loc(locator)}")
218
-
219
- # Handle media URLs differently
220
- if is_media_url(url):
221
- log.message(
222
- "URL is a media URL, so adding as a resource and will cache media: %s", fmt_loc(url)
223
- )
224
- store_path = ws.import_item(url, as_type=ItemType.resource, reimport=refetch)
225
- saved_paths.append(store_path)
226
- media_tools.cache_media(url)
227
- else:
228
- # Cache the content first
229
- expiration_sec = 0 if refetch else None
230
- cache_result = cache_file(url, expiration_sec=expiration_sec)
231
- original_filename = Path(parse_http_url(url).path).name
232
- mime_type = cache_result.content.headers and cache_result.content.headers.mime_type
233
-
234
- # Create a resource item
235
- resource_item = Item.from_external_path(
236
- cache_result.content.path,
237
- ItemType.resource,
238
- url=url,
239
- mime_type=mime_type,
240
- original_filename=original_filename,
241
- )
242
- # For initial content, do not format or add frontmatter.
243
- store_path = ws.save(resource_item, no_frontmatter=True, no_format=True)
244
- saved_paths.append(store_path)
245
- select(store_path)
246
-
247
- # Also create a doc version for text content if we want to normalize formatting.
248
- if resource_item.format and can_normalize(resource_item.format) and not no_format:
249
- doc_item = Item.from_external_path(
250
- cache_result.content.path,
251
- ItemType.doc,
252
- url=url,
253
- mime_type=mime_type,
254
- original_filename=original_filename,
255
- )
256
- # Now use default formatting and frontmatter.
257
- doc_store_path = ws.save(doc_item)
258
- saved_paths.append(doc_store_path)
259
- select(doc_store_path)
260
-
261
- print_status(
262
- "Downloaded %s %s:\n%s",
263
- len(saved_paths),
264
- plural("item", len(saved_paths)),
265
- fmt_lines(saved_paths),
266
- )
267
-
268
- return ShellResult(show_selection=True)
269
-
270
-
271
189
  @kash_command
272
190
  def history(max: int = 30, raw: bool = False) -> None:
273
191
  """
@@ -536,10 +454,14 @@ def save_clipboard(
536
454
 
537
455
 
538
456
  @kash_command
539
- def fetch_metadata(*files_or_urls: str, refetch: bool = False) -> ShellResult:
457
+ def fetch_url(*files_or_urls: str, refetch: bool = False) -> ShellResult:
540
458
  """
541
- Fetch metadata for the given URLs or resources. Imports new URLs and saves back
542
- the fetched metadata for existing resources.
459
+ Fetch content and metadata for the given URLs or resources, saving to the
460
+ current workspace.
461
+
462
+ Imports new URLs and saves back the fetched metadata for existing resources.
463
+ Also saves a resource item with the content of the URL, either HTML, text, or
464
+ of any other type.
543
465
 
544
466
  Skips items that already have a title and description, unless `refetch` is true.
545
467
  Skips (with a warning) items that are not URL resources.
@@ -552,7 +474,7 @@ def fetch_metadata(*files_or_urls: str, refetch: bool = False) -> ShellResult:
552
474
  store_paths = []
553
475
  for locator in locators:
554
476
  try:
555
- fetched_item = fetch_url_metadata(locator, refetch=refetch)
477
+ fetched_item = fetch_url_item(locator, refetch=refetch)
556
478
  store_paths.append(fetched_item.store_path)
557
479
  except InvalidInput as e:
558
480
  log.warning(
kash/config/colors.py CHANGED
@@ -139,14 +139,16 @@ web_light_translucent = SimpleNamespace(
139
139
  bg_header=hsl_to_hex("hsla(188, 42%, 70%, 0.2)"),
140
140
  bg_alt=hsl_to_hex("hsla(39, 24%, 90%, 0.3)"),
141
141
  bg_alt_solid=hsl_to_hex("hsla(39, 24%, 97%, 1)"),
142
+ bg_selected=hsl_to_hex("hsla(188, 44%, 94%, 0.95)"),
142
143
  text=hsl_to_hex("hsl(188, 39%, 11%)"),
143
144
  code=hsl_to_hex("hsl(44, 38%, 23%)"),
144
145
  border=hsl_to_hex("hsl(188, 8%, 50%)"),
145
146
  border_hint=hsl_to_hex("hsla(188, 8%, 72%, 0.3)"),
146
147
  border_accent=hsl_to_hex("hsla(305, 18%, 65%, 0.85)"),
147
148
  hover=hsl_to_hex("hsl(188, 12%, 84%)"),
148
- hover_bg=hsl_to_hex("hsla(188, 12%, 94%, 0.8)"),
149
+ hover_bg=hsl_to_hex("hsla(188, 44%, 94%, 1)"),
149
150
  hint=hsl_to_hex("hsl(188, 11%, 65%)"),
151
+ hint_strong=hsl_to_hex("hsl(188, 11%, 46%)"),
150
152
  hint_gentle=hsl_to_hex("hsla(188, 11%, 65%, 0.2)"),
151
153
  tooltip_bg=hsl_to_hex("hsla(188, 6%, 37%, 0.7)"),
152
154
  popover_bg=hsl_to_hex("hsla(188, 6%, 37%, 0.7)"),
@@ -170,14 +172,16 @@ web_dark_translucent = SimpleNamespace(
170
172
  bg_header=hsl_to_hex("hsla(188, 42%, 20%, 0.3)"),
171
173
  bg_alt=hsl_to_hex("hsla(220, 14%, 12%, 0.5)"),
172
174
  bg_alt_solid=hsl_to_hex("hsl(220, 14%, 12%)"),
175
+ bg_selected=hsl_to_hex("hsla(188, 12%, 50%, 0.95)"),
173
176
  text=hsl_to_hex("hsl(188, 10%, 90%)"),
174
177
  code=hsl_to_hex("hsl(44, 38%, 72%)"),
175
178
  border=hsl_to_hex("hsl(188, 8%, 25%)"),
176
179
  border_hint=hsl_to_hex("hsla(188, 8%, 35%, 0.3)"),
177
180
  border_accent=hsl_to_hex("hsla(305, 30%, 55%, 0.85)"),
178
181
  hover=hsl_to_hex("hsl(188, 12%, 35%)"),
179
- hover_bg=hsl_to_hex("hsla(188, 20%, 25%, 0.4)"),
182
+ hover_bg=hsl_to_hex("hsla(188, 12%, 40%, 0.95)"),
180
183
  hint=hsl_to_hex("hsl(188, 11%, 55%)"),
184
+ hint_strong=hsl_to_hex("hsl(188, 11%, 72%)"),
181
185
  hint_gentle=hsl_to_hex("hsla(188, 11%, 55%, 0.2)"),
182
186
  tooltip_bg=hsl_to_hex("hsla(188, 6%, 20%, 0.9)"),
183
187
  popover_bg=hsl_to_hex("hsla(188, 6%, 20%, 0.9)"),
@@ -3,35 +3,64 @@
3
3
  > “*Simple should be simple.
4
4
  > Complex should be possible.*” —Alan Kay
5
5
 
6
- Kash (“Knowledge Agent SHell”) is an **interactive, AI-native command-line** shell for
7
- practical knowledge tasks.
6
+ Kash (“Knowledge Agent SHell”) is an experiment in making software tasks more modular,
7
+ exploratory, and flexible using Python and current AI tools.
8
+
9
+ The philosophy behind kash is similar to Unix shell tools: simple commands that can be
10
+ combined in flexible and powerful ways.
11
+ It operates on "items" such as URLs, files, or Markdown notes within a workspace
12
+ directory.
13
+
14
+ You can use Kash as an **interactive, AI-native command-line** shell for practical
15
+ knowledge tasks. It's also **a Python library** that lets you convert a simple Python
16
+ function into a command and an MCP tool, so it integrates with other tools like
17
+ Anthropic Desktop or Cursor.
18
+
19
+ It's new and still has some rough edges, but it's now working well enough it is feeling
20
+ quite powerful. It now serves as a replacement for my usual shell (previously bash or
21
+ zsh). I use it routinely to remix, combine, and interactively explore and then gradually
22
+ automate complex tasks by composing AI tools, APIs, and libraries.
23
+ And last but not least, the same framework lets me build other tools (like
24
+ [textpress](https://github.com/jlevy/textpress)).
8
25
 
9
- It's also **a Python library** that lets you convert a simple Python function into a
10
- command and an MCP tool, so it integrates with other tools like Anthropic Desktop or
11
- Cursor.
26
+ And of course, kash can read its own functionality and enhance itself by writing new
27
+ actions.
12
28
 
13
- You can think of it a kind of power-tool for technical users who want to use Python and
14
- APIs, a kind of hybrid between an AI assistant, a shell, and a developer tool like
15
- Cursor or Claude Code.
29
+ ### Kash Packages
16
30
 
17
- It's my attempt at finding a way to remix, combine, and interactively explore and then
18
- gradually automate complex tasks by composing AI tools, APIs, and libraries.
31
+ The [kash-shell](https://github.com/jlevy/kash) package is the base package and includes
32
+ the Python framework, a few core utilities, and the Kash command-line shell.
19
33
 
20
- And of course, kash can read its own functionality and enhance itself by writing new
21
- actions.
34
+ Additional actions for handling more complex tasks like converting documents and
35
+ transcribing, researching, or annotating videos, are in the
36
+ [kash-docs](https://github.com/jlevy/kash-docs) and
37
+ [kash-media](https://github.com/jlevy/kash-media) packages, all available on PyPI and
38
+ quick to install via uv.
22
39
 
23
40
  ### Key Concepts
24
41
 
25
- - **Actions:** The core of Kash are **Kash actions**. By decorating a Python function,
26
- you can turn it into an action, which makes it more flexible and powerful, able to
27
- work with file inputs stored and outputs in a given directory, also called a
28
- **workspace**.
42
+ - **Actions:** The core of Kash are **actions**. By decorating a Python function with
43
+ `@kash_action`, you can turn it into an action, which makes it more flexible and
44
+ powerful. It can then be used like a command line command as well as a Python function
45
+ or an MCP tool.
46
+
47
+ - **Workspaces:** A key element of Kash is that it does most nontrivial work in the
48
+ context of a **workspace**. A workspace is just a directory of files that have a few
49
+ conventions to make it easier to maintain context and perform actions.
50
+ A bit like how Git repos work, it has a `.kash/` directory that holds metadata and
51
+ cached content. The rest can be anything, but is typically directories of resources
52
+ (like .docx or .pdf or links to web pages) or content, typically Markdown files with
53
+ YAML frontmatter. All text files use
54
+ [frontmatter-format](https://github.com/jlevy/frontmatter-format) so have easy-to-read
55
+ YAML metadata that includes not just title or description, but also the names of the
56
+ actions that created it.
29
57
 
30
58
  - **Compositionality:** An action is composable with other actions simply as a Python
31
- function, so complex (like transcribing and annotating a video) actions can be built
32
- from simpler actions (like downloading and caching a YouTube video, identifying the
33
- speakers in a transcript, etc.). The goal is to reduce the "interstitial complexity"
34
- of combining tools, so it's easy for you (or an LLM!) to combine tools in flexible and
59
+ function, so complex operations (for example, transcribing and annotating a video and
60
+ publishing it on a website) actions can be built from simpler actions (say downloading
61
+ and caching a YouTube video, identifying the speakers in a transcript, formatting it
62
+ as pretty HTML, etc.). The goal is to reduce the "interstitial complexity" of
63
+ combining tools, so it's easy for you (or an LLM!) to combine tools in flexible and
35
64
  powerful ways.
36
65
 
37
66
  - **Command-line usage:** In addition to using the function in other libraries and
@@ -40,9 +69,6 @@ actions.
40
69
  video. In kash you have **smart tab completions**, **Python expressions**, and an **LLM
41
70
  assistant** built into the shell.
42
71
 
43
- - **MCP support:** Finally, an action is also an **MCP tool server** so you can use it
44
- in any MCP client, like Anthropic Desktop or Cursor.
45
-
46
72
  - **Support for any API:** Kash is tool agnostic and runs locally, on file inputs in
47
73
  simple formats, so you own and manage your data and workspaces however you like.
48
74
  You can use it with any models or APIs you like, and is already set up to use the APIs
@@ -51,6 +77,9 @@ actions.
51
77
  **Perplexity**, **Firecrawl**, **Exa**, and any Python libraries.
52
78
  There is also some experimental support for **LlamaIndex** and **ChromaDB**.
53
79
 
80
+ - **MCP support:** Finally, an action is also an **MCP tool server** so you can use it
81
+ in any MCP client, like Anthropic Desktop or Cursor.
82
+
54
83
  ### What Can Kash Do?
55
84
 
56
85
  You can use kash actions to do deep research, transcribe videos, summarize and organize
@@ -2,9 +2,9 @@
2
2
 
3
3
  ### Running the Kash Shell
4
4
 
5
- Kash offers a shell environment based on [xonsh](https://xon.sh/) augmented with an LLM
6
- assistant and a variety of other enhanced commands and customizations.
7
- If you've used a bash or Python shell before, xonsh is very intuitive.
5
+ Kash offers a shell environment based on [xonsh](https://xon.sh/) augmented with a bunch
6
+ of enhanced commands and customizations.
7
+ If you've used a bash or Python shell before, it should be very intuitive.
8
8
 
9
9
  Within the kash shell, you get a full environment with all actions and commands.
10
10
  You also get intelligent auto-complete, a built-in assistant to help you perform tasks,
@@ -38,30 +38,17 @@ These are for `kash-media` but you can use a `kash-shell` for a more basic setup
38
38
  Kash is easiest to use via [**uv**](https://docs.astral.sh/uv/), the new package
39
39
  manager for Python. `uv` replaces traditional use of `pyenv`, `pipx`, `poetry`, `pip`,
40
40
  etc. Installing `uv` also ensures you get a compatible version of Python.
41
-
42
- If you don't have `uv` installed, a quick way to install it is:
43
-
41
+ See [uv's docs](https://docs.astral.sh/uv/getting-started/installation/) for other
42
+ installation methods and platforms.
43
+ Usually you just want to run:
44
44
  ```shell
45
45
  curl -LsSf https://astral.sh/uv/install.sh | sh
46
46
  ```
47
47
 
48
- For macOS, you prefer [brew](https://brew.sh/) you can install or upgrade uv with:
49
-
50
- ```shell
51
- brew update
52
- brew install uv
53
- ```
54
- See [uv's docs](https://docs.astral.sh/uv/getting-started/installation/) for other
55
- installation methods and platforms.
56
-
57
48
  2. **Install additional command-line tools:**
58
49
 
59
50
  In addition to Python, it's highly recommended to install a few other dependencies to
60
- make more tools and commands work: `ripgrep` (for search), `bat` (for prettier file
61
- display), `eza` (a much improved version of `ls`), `hexyl` (a much improved hex
62
- viewer), `imagemagick` (for image display in modern terminals), `libmagic` (for file
63
- type detection), `ffmpeg` (for audio and video conversions)
64
-
51
+ make more tools and commands work.
65
52
  For macOS, you can again use brew:
66
53
 
67
54
  ```shell
@@ -82,22 +69,22 @@ These are for `kash-media` but you can use a `kash-shell` for a more basic setup
82
69
 
83
70
  For Windows or other platforms, see the uv instructions.
84
71
 
72
+ Kash auto-detects and uses `ripgrep` (for search), `bat` (for prettier file display),
73
+ `eza` (a much improved version of `ls`), `hexyl` (a much improved hex viewer),
74
+ `imagemagick` (for image display in modern terminals), `libmagic` (for file type
75
+ detection), `ffmpeg` (for audio and video conversions)
76
+
85
77
  3. **Install kash or a kash kit:**
86
78
 
79
+ For a more meaningful demo, use an enhanced version of kash that also has various
80
+ media tools (like yt-dlp and Deepgram support):
81
+
87
82
  ```shell
88
- uv tool install kash-media --python=3.13
83
+ uv tool install kash-media --upgrade --python=3.13
89
84
  ```
90
85
 
91
86
  Other versions of Python should work but 3.13 is recommended.
92
- For a setup without the media tools, use `kash-shell` instead.
93
-
94
- If you've installed an older version and want to be sure you have the latest shell,
95
- you may want to add `--upgrade --force` to be sure you get the latest version of the
96
- kit.
97
-
98
- ```shell
99
- uv tool install kash-media --python=3.13 --upgrade --force
100
- ```
87
+ For a setup without the media tools, just install `kash-shell` instead.
101
88
 
102
89
  4. **Set up API keys:**
103
90
 
@@ -15,25 +15,11 @@ Type `help` for the full documentation.
15
15
  The simplest way to illustrate how to use kash is by example.
16
16
  You can go through the commands below a few at a time, trying each one.
17
17
 
18
- This is a "real" example that uses a bunch of libraries.
18
+ This is a "real" example that uses ffmpeg and a few other libraries.
19
19
  So to get it to work you must install not just the main shell but the kash "media kit"
20
20
  with extra dependencies.
21
-
22
- You need the following tools:
23
-
24
- ```shell
25
- # On MacOS:
26
- brew install ripgrep bat eza hexyl imagemagick libmagic ffmpeg
27
- # On Linux:
28
- apt install ripgrep bat eza hexyl imagemagick libmagic ffmpeg
29
- ```
30
-
31
- Then install the `kash-media`, which includes kash-shell and many other libs like yt-dlp
32
- for YouTube handling:
33
-
34
- ```shell
35
- uv tool install kash-media
36
- ```
21
+ This is discussed in [the installation instructions](#installation-steps).
22
+ If you don't have these already installed, you can add these tools:
37
23
 
38
24
  Then run `kash` to start.
39
25
 
@@ -152,7 +138,7 @@ show_webpage
152
138
  show_webpage --help
153
139
 
154
140
  # And you can actually how this works by looking at its source:
155
- source_code show_webpage
141
+ show_webpage --show_source
156
142
 
157
143
  # What if something isn't working right?
158
144
  # Sometimes we may want to browse more detailed system logs:
@@ -171,7 +157,7 @@ transcribe_format https://www.youtube.com/watch?v=_8djNYprRDI
171
157
 
172
158
  # Getting a little fancier, this one adds little paragraph annotations and
173
159
  # a nicer summary at the top:
174
- transcribe_annotate_summarize https://www.youtube.com/watch?v=_8djNYprRDI
160
+ transcribe_annotate https://www.youtube.com/watch?v=_8djNYprRDI
175
161
 
176
162
  show_webpage
177
163
  ```
kash/exec/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from kash.exec.action_decorators import kash_action, kash_action_class
2
2
  from kash.exec.action_exec import SkipItem, prepare_action_input, run_action_with_shell_context
3
3
  from kash.exec.command_registry import kash_command
4
+ from kash.exec.fetch_url_items import fetch_url_item, fetch_url_item_content
4
5
  from kash.exec.importing import import_and_register
5
6
  from kash.exec.llm_transforms import llm_transform_item, llm_transform_str
6
7
  from kash.exec.precondition_registry import kash_precondition
@@ -21,6 +22,8 @@ __all__ = [
21
22
  "prepare_action_input",
22
23
  "run_action_with_shell_context",
23
24
  "kash_command",
25
+ "fetch_url_item",
26
+ "fetch_url_item_content",
24
27
  "kash_runtime",
25
28
  "current_runtime_settings",
26
29
  "import_and_register",
kash/exec/action_exec.py CHANGED
@@ -43,7 +43,7 @@ def prepare_action_input(*input_args: CommandArg, refetch: bool = False) -> Acti
43
43
  URL or file resources, either finding them in the workspace or importing them.
44
44
  Also fetches metadata for URLs if they don't already have title and description.
45
45
  """
46
- from kash.exec.fetch_url_metadata import fetch_url_item_metadata
46
+ from kash.exec.fetch_url_items import fetch_url_item_content
47
47
 
48
48
  ws = current_ws()
49
49
 
@@ -55,7 +55,7 @@ def prepare_action_input(*input_args: CommandArg, refetch: bool = False) -> Acti
55
55
  if input_items:
56
56
  log.message("Assembling metadata for input items:\n%s", fmt_lines(input_items))
57
57
  input_items = [
58
- fetch_url_item_metadata(item, refetch=refetch) if is_url_resource(item) else item
58
+ fetch_url_item_content(item, refetch=refetch) if is_url_resource(item) else item
59
59
  for item in input_items
60
60
  ]
61
61
 
@@ -102,7 +102,7 @@ def log_action(action: Action, action_input: ActionInput, operation: Operation):
102
102
  """
103
103
  PrintHooks.before_log_action_run()
104
104
  log.message("%s Action: `%s`", EMOJI_START, action.name)
105
- log.message("Running: `%s`", operation.command_line(with_options=True))
105
+ log.info("Running: `%s`", operation.command_line(with_options=True))
106
106
  if len(action.param_value_summary()) > 0:
107
107
  log.message("Parameters:\n%s", action.param_value_summary_str())
108
108
  log.info("Operation is: %s", operation)
@@ -0,0 +1,109 @@
1
+ from kash.config.logger import get_logger
2
+ from kash.exec.preconditions import is_url_resource
3
+ from kash.media_base.media_services import get_media_metadata
4
+ from kash.model.items_model import Item, ItemType
5
+ from kash.model.paths_model import StorePath
6
+ from kash.utils.common.format_utils import fmt_loc
7
+ from kash.utils.common.url import Url, is_url
8
+ from kash.utils.common.url_slice import add_slice_to_url, parse_url_slice
9
+ from kash.utils.errors import InvalidInput
10
+
11
+ log = get_logger(__name__)
12
+
13
+
14
+ def fetch_url_item(
15
+ locator: Url | StorePath, *, save_content: bool = True, refetch: bool = False
16
+ ) -> Item:
17
+ from kash.workspaces import current_ws
18
+
19
+ ws = current_ws()
20
+ if is_url(locator):
21
+ # Import or find URL as a resource in the current workspace.
22
+ store_path = ws.import_item(locator, as_type=ItemType.resource)
23
+ item = ws.load(store_path)
24
+ elif isinstance(locator, StorePath):
25
+ item = ws.load(locator)
26
+ if not is_url_resource(item):
27
+ raise InvalidInput(f"Not a URL resource: {fmt_loc(locator)}")
28
+ else:
29
+ raise InvalidInput(f"Not a URL or URL resource: {fmt_loc(locator)}")
30
+
31
+ return fetch_url_item_content(item, save_content=save_content, refetch=refetch)
32
+
33
+
34
+ def fetch_url_item_content(item: Item, *, save_content: bool = True, refetch: bool = False) -> Item:
35
+ """
36
+ Fetch content and metadata for a URL using a media service if we
37
+ recognize the URL as a known media service. Otherwise, fetch and extract the
38
+ metadata and content from the web page and save it to the URL item.
39
+
40
+ If `save_content` is true, a copy of the content is also saved as
41
+ a resource item.
42
+
43
+ The content item is returned if content was saved. Otherwise, the updated
44
+ URL item is returned.
45
+ """
46
+ from kash.web_content.canon_url import canonicalize_url
47
+ from kash.web_content.web_extract import fetch_page_content
48
+ from kash.workspaces import current_ws
49
+
50
+ ws = current_ws()
51
+ if not refetch and item.title and item.description:
52
+ log.message(
53
+ "Already have title and description, will not fetch metadata: %s", item.fmt_loc()
54
+ )
55
+ return item
56
+
57
+ if not item.url:
58
+ raise InvalidInput(f"No URL for item: {item.fmt_loc()}")
59
+
60
+ url = canonicalize_url(item.url)
61
+ log.message("No metadata for URL, will fetch: %s", url)
62
+
63
+ # Prefer fetching metadata from media using the media service if possible.
64
+ # Data is cleaner and YouTube for example often blocks regular scraping.
65
+ media_metadata = get_media_metadata(url)
66
+ url_item: Item | None = None
67
+ content_item: Item | None = None
68
+ if media_metadata:
69
+ url_item = Item.from_media_metadata(media_metadata)
70
+ # Preserve and canonicalize any slice suffix on the URL.
71
+ _base_url, slice = parse_url_slice(item.url)
72
+ if slice:
73
+ new_url = add_slice_to_url(media_metadata.url, slice)
74
+ if new_url != item.url:
75
+ log.message("Updated URL from metadata and added slice: %s", new_url)
76
+ url_item.url = new_url
77
+
78
+ url_item = item.merged_copy(url_item)
79
+ else:
80
+ page_data = fetch_page_content(url, refetch=refetch, cache=save_content)
81
+ url_item = item.new_copy_with(
82
+ title=page_data.title or item.title,
83
+ description=page_data.description or item.description,
84
+ thumbnail_url=page_data.thumbnail_url or item.thumbnail_url,
85
+ )
86
+ if save_content:
87
+ assert page_data.saved_content
88
+ assert page_data.format_info
89
+ content_item = url_item.new_copy_with(
90
+ external_path=str(page_data.saved_content),
91
+ # Use the original filename, not the local cache filename (which has a hash suffix).
92
+ original_filename=item.get_filename(),
93
+ format=page_data.format_info.format,
94
+ )
95
+ ws.save(content_item)
96
+
97
+ if not url_item.title:
98
+ log.warning("Failed to fetch page data: title is missing: %s", item.url)
99
+
100
+ # Now save the updated URL item and also the content item if we have one.
101
+ ws.save(url_item)
102
+ assert url_item.store_path
103
+ log.debug("Saved URL item: %s", url_item.fmt_loc())
104
+ if content_item:
105
+ ws.save(content_item)
106
+ assert content_item.store_path
107
+ log.debug("Saved content item: %s", content_item.fmt_loc())
108
+
109
+ return content_item or url_item