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.
- kash/actions/core/{markdownify.py → markdownify_html.py} +3 -6
- kash/actions/core/minify_html.py +41 -0
- kash/commands/base/show_command.py +11 -1
- kash/commands/workspace/workspace_commands.py +10 -88
- kash/config/colors.py +6 -2
- kash/docs/markdown/topics/a1_what_is_kash.md +52 -23
- kash/docs/markdown/topics/a2_installation.md +17 -30
- kash/docs/markdown/topics/a3_getting_started.md +5 -19
- kash/exec/__init__.py +3 -0
- kash/exec/action_exec.py +3 -3
- kash/exec/fetch_url_items.py +109 -0
- kash/exec/precondition_registry.py +3 -3
- kash/file_storage/file_store.py +24 -1
- kash/file_storage/store_filenames.py +4 -0
- kash/help/function_param_info.py +1 -1
- kash/help/help_pages.py +1 -1
- kash/help/help_printing.py +1 -1
- kash/llm_utils/llm_features.py +5 -1
- kash/llm_utils/llms.py +18 -8
- kash/media_base/media_cache.py +48 -24
- kash/media_base/media_services.py +63 -14
- kash/media_base/services/local_file_media.py +9 -1
- kash/model/items_model.py +22 -8
- kash/model/media_model.py +9 -1
- kash/model/params_model.py +9 -3
- kash/utils/common/function_inspect.py +97 -1
- kash/utils/common/parse_docstring.py +347 -0
- kash/utils/common/testing.py +58 -0
- kash/utils/common/url_slice.py +329 -0
- kash/utils/file_utils/file_formats.py +1 -1
- kash/utils/text_handling/markdown_utils.py +424 -16
- kash/web_content/web_extract.py +34 -15
- kash/web_content/web_page_model.py +10 -1
- kash/web_gen/templates/base_styles.css.jinja +137 -15
- kash/web_gen/templates/base_webpage.html.jinja +13 -17
- kash/web_gen/templates/components/toc_scripts.js.jinja +319 -0
- kash/web_gen/templates/components/toc_styles.css.jinja +284 -0
- kash/web_gen/templates/components/tooltip_scripts.js.jinja +730 -0
- kash/web_gen/templates/components/tooltip_styles.css.jinja +482 -0
- kash/web_gen/templates/content_styles.css.jinja +13 -8
- kash/web_gen/templates/simple_webpage.html.jinja +15 -481
- kash/workspaces/workspaces.py +10 -1
- {kash_shell-0.3.17.dist-info → kash_shell-0.3.20.dist-info}/METADATA +75 -72
- {kash_shell-0.3.17.dist-info → kash_shell-0.3.20.dist-info}/RECORD +47 -40
- kash/exec/fetch_url_metadata.py +0 -72
- kash/help/docstring_utils.py +0 -111
- {kash_shell-0.3.17.dist-info → kash_shell-0.3.20.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.17.dist-info → kash_shell-0.3.20.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
457
|
+
def fetch_url(*files_or_urls: str, refetch: bool = False) -> ShellResult:
|
|
540
458
|
"""
|
|
541
|
-
Fetch metadata for the given URLs or resources
|
|
542
|
-
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
Cursor.
|
|
26
|
+
And of course, kash can read its own functionality and enhance itself by writing new
|
|
27
|
+
actions.
|
|
12
28
|
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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 **
|
|
26
|
-
you can turn it into an action, which makes it more flexible and
|
|
27
|
-
|
|
28
|
-
|
|
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 (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
If you've used a bash or Python shell before,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|