kash-shell 0.3.10__py3-none-any.whl → 0.3.11__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/format_markdown_template.py +2 -5
- kash/actions/core/markdownify.py +2 -4
- kash/actions/core/readability.py +2 -4
- kash/actions/core/render_as_html.py +30 -11
- kash/actions/core/show_webpage.py +6 -11
- kash/actions/core/strip_html.py +2 -6
- kash/actions/core/{webpage_config.py → tabbed_webpage_config.py} +5 -3
- kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
- kash/commands/base/files_command.py +28 -10
- kash/commands/workspace/workspace_commands.py +1 -2
- kash/config/colors.py +2 -2
- kash/exec/action_decorators.py +6 -6
- kash/exec/llm_transforms.py +6 -3
- kash/exec/preconditions.py +6 -0
- kash/exec/resolve_args.py +4 -0
- kash/file_storage/file_store.py +20 -18
- kash/help/function_param_info.py +1 -1
- kash/local_server/local_server_routes.py +1 -7
- kash/model/items_model.py +74 -28
- kash/shell/utils/shell_function_wrapper.py +15 -15
- kash/text_handling/doc_normalization.py +1 -1
- kash/text_handling/markdown_render.py +1 -0
- kash/text_handling/markdown_utils.py +22 -0
- kash/utils/common/function_inspect.py +360 -110
- kash/utils/file_utils/file_ext.py +4 -0
- kash/utils/file_utils/file_formats_model.py +17 -1
- kash/web_gen/__init__.py +0 -4
- kash/web_gen/simple_webpage.py +52 -0
- kash/web_gen/tabbed_webpage.py +23 -16
- kash/web_gen/template_render.py +37 -2
- kash/web_gen/templates/base_styles.css.jinja +76 -56
- kash/web_gen/templates/base_webpage.html.jinja +85 -67
- kash/web_gen/templates/item_view.html.jinja +47 -37
- kash/web_gen/templates/simple_webpage.html.jinja +24 -0
- kash/web_gen/templates/tabbed_webpage.html.jinja +42 -32
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.11.dist-info}/METADATA +5 -5
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.11.dist-info}/RECORD +40 -38
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.11.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.11.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.10.dist-info → kash_shell-0.3.11.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from kash.config.logger import get_logger
|
|
5
5
|
from kash.exec import kash_action
|
|
6
6
|
from kash.exec.preconditions import is_markdown
|
|
7
|
-
from kash.model import ONE_OR_MORE_ARGS, ActionInput, ActionResult,
|
|
7
|
+
from kash.model import ONE_OR_MORE_ARGS, ActionInput, ActionResult, Param
|
|
8
8
|
from kash.utils.common.type_utils import not_none
|
|
9
9
|
from kash.utils.errors import InvalidInput
|
|
10
10
|
|
|
@@ -84,9 +84,6 @@ def format_markdown_template(
|
|
|
84
84
|
# Format the body using the mapped items.
|
|
85
85
|
body = template.format(**item_map)
|
|
86
86
|
|
|
87
|
-
result_item = items[0].derived_copy(
|
|
88
|
-
type=ItemType.doc,
|
|
89
|
-
body=body,
|
|
90
|
-
)
|
|
87
|
+
result_item = items[0].derived_copy(body=body)
|
|
91
88
|
|
|
92
89
|
return ActionResult([result_item])
|
kash/actions/core/markdownify.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from kash.config.logger import get_logger
|
|
2
2
|
from kash.exec import kash_action
|
|
3
3
|
from kash.exec.preconditions import has_html_body, is_url_item
|
|
4
|
-
from kash.model import Format, Item
|
|
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
|
|
7
7
|
from kash.web_content.web_extract_readabilipy import extract_text_readabilipy
|
|
@@ -26,7 +26,5 @@ def markdownify(item: Item, refetch: bool = False) -> Item:
|
|
|
26
26
|
page_data = extract_text_readabilipy(url, html_content)
|
|
27
27
|
markdown_content = markdownify_convert(page_data.clean_html)
|
|
28
28
|
|
|
29
|
-
output_item = item.derived_copy(
|
|
30
|
-
type=ItemType.doc, format=Format.markdown, body=markdown_content
|
|
31
|
-
)
|
|
29
|
+
output_item = item.derived_copy(format=Format.markdown, body=markdown_content)
|
|
32
30
|
return output_item
|
kash/actions/core/readability.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from kash.config.logger import get_logger
|
|
2
2
|
from kash.exec import kash_action
|
|
3
3
|
from kash.exec.preconditions import has_html_body, is_url_item
|
|
4
|
-
from kash.model import Format, Item
|
|
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
|
|
7
7
|
from kash.web_content.web_extract_readabilipy import extract_text_readabilipy
|
|
@@ -23,8 +23,6 @@ def readability(item: Item, refetch: bool = False) -> Item:
|
|
|
23
23
|
url, html_content = get_url_html(item, expiration_sec=expiration_sec)
|
|
24
24
|
page_data = extract_text_readabilipy(url, html_content)
|
|
25
25
|
|
|
26
|
-
output_item = item.derived_copy(
|
|
27
|
-
type=ItemType.doc, format=Format.html, body=page_data.clean_html
|
|
28
|
-
)
|
|
26
|
+
output_item = item.derived_copy(format=Format.html, body=page_data.clean_html)
|
|
29
27
|
|
|
30
28
|
return output_item
|
|
@@ -1,18 +1,37 @@
|
|
|
1
|
-
from kash.actions.core.
|
|
2
|
-
from kash.actions.core.
|
|
1
|
+
from kash.actions.core.tabbed_webpage_config import tabbed_webpage_config
|
|
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_text_body, is_html
|
|
5
|
-
from kash.
|
|
4
|
+
from kash.exec.preconditions import has_full_html_page_body, has_text_body, is_html
|
|
5
|
+
from kash.exec_model.args_model import ONE_OR_MORE_ARGS
|
|
6
|
+
from kash.model import ActionInput, ActionResult, Param
|
|
7
|
+
from kash.model.items_model import ItemType
|
|
8
|
+
from kash.utils.file_utils.file_formats_model import Format
|
|
9
|
+
from kash.web_gen.simple_webpage import simple_webpage_render
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
@kash_action(
|
|
9
|
-
|
|
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),),
|
|
10
16
|
)
|
|
11
|
-
def render_as_html(input: ActionInput) -> ActionResult:
|
|
17
|
+
def render_as_html(input: ActionInput, add_title: bool = False) -> ActionResult:
|
|
12
18
|
"""
|
|
13
|
-
Convert text, Markdown, or HTML to pretty, formatted HTML using
|
|
14
|
-
page template.
|
|
19
|
+
Convert text, Markdown, or HTML to pretty, formatted HTML using a clean
|
|
20
|
+
and simple page template. Supports GFM-flavored Markdown tables and footnotes.
|
|
21
|
+
|
|
22
|
+
If it's a single input, the output is a simple HTML page.
|
|
23
|
+
If it's multiple inputs, the output is a tabbed HTML page.
|
|
24
|
+
|
|
25
|
+
This adds a header, footer, etc. so should be used on a plain document or HTML basic
|
|
26
|
+
page, not a full HTML page with header and body already present.
|
|
15
27
|
"""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
if len(input.items) == 1:
|
|
29
|
+
input_item = input.items[0]
|
|
30
|
+
html_body = simple_webpage_render(input_item, add_title_h1=add_title)
|
|
31
|
+
result_item = input_item.derived_copy(
|
|
32
|
+
type=ItemType.export, format=Format.html, body=html_body
|
|
33
|
+
)
|
|
34
|
+
return ActionResult([result_item])
|
|
35
|
+
else:
|
|
36
|
+
config_result = tabbed_webpage_config(input)
|
|
37
|
+
return tabbed_webpage_generate(ActionInput(items=config_result.items), add_title=add_title)
|
|
@@ -1,27 +1,22 @@
|
|
|
1
|
-
from kash.actions.core.
|
|
2
|
-
from kash.actions.core.webpage_generate import webpage_generate
|
|
1
|
+
from kash.actions.core.render_as_html import render_as_html
|
|
3
2
|
from kash.commands.base.show_command import show
|
|
4
|
-
from kash.config.logger import get_logger
|
|
5
3
|
from kash.exec import kash_action
|
|
6
|
-
from kash.exec.preconditions import has_text_body, is_html
|
|
4
|
+
from kash.exec.preconditions import has_full_html_page_body, has_text_body, is_html
|
|
5
|
+
from kash.exec_model.args_model import ONE_OR_MORE_ARGS
|
|
7
6
|
from kash.exec_model.commands_model import Command
|
|
8
7
|
from kash.exec_model.shell_model import ShellResult
|
|
9
8
|
from kash.model import ActionInput, ActionResult
|
|
10
9
|
|
|
11
|
-
log = get_logger(__name__)
|
|
12
|
-
|
|
13
10
|
|
|
14
11
|
@kash_action(
|
|
15
|
-
|
|
12
|
+
expected_args=ONE_OR_MORE_ARGS,
|
|
13
|
+
precondition=(is_html | has_text_body) & ~has_full_html_page_body,
|
|
16
14
|
)
|
|
17
15
|
def show_webpage(input: ActionInput) -> ActionResult:
|
|
18
16
|
"""
|
|
19
17
|
Show text, Markdown, or HTML as a nicely formatted webpage.
|
|
20
18
|
"""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
log.message("Configured web page: %s", config_result)
|
|
24
|
-
result = webpage_generate(ActionInput(items=config_result.items))
|
|
19
|
+
result = render_as_html(input)
|
|
25
20
|
|
|
26
21
|
# Automatically show the result.
|
|
27
22
|
result.shell_result = ShellResult(display_command=Command.assemble(show))
|
kash/actions/core/strip_html.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from kash.config.logger import get_logger
|
|
2
2
|
from kash.exec import kash_action
|
|
3
3
|
from kash.exec.preconditions import has_html_body, has_text_body
|
|
4
|
-
from kash.model import Format, Item
|
|
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
|
|
7
7
|
|
|
@@ -19,10 +19,6 @@ def strip_html(item: Item) -> Item:
|
|
|
19
19
|
raise InvalidInput("Item must have a body")
|
|
20
20
|
|
|
21
21
|
clean_body = html_to_plaintext(item.body)
|
|
22
|
-
output_item = item.derived_copy(
|
|
23
|
-
type=ItemType.doc,
|
|
24
|
-
format=Format.markdown,
|
|
25
|
-
body=clean_body,
|
|
26
|
-
)
|
|
22
|
+
output_item = item.derived_copy(format=Format.markdown, body=clean_body)
|
|
27
23
|
|
|
28
24
|
return output_item
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from kash.config.logger import get_logger
|
|
2
2
|
from kash.exec import kash_action
|
|
3
|
+
from kash.exec_model.args_model import ONE_OR_MORE_ARGS
|
|
3
4
|
from kash.model import ActionInput, ActionResult, Param
|
|
4
5
|
from kash.utils.errors import InvalidInput
|
|
5
6
|
from kash.web_gen import tabbed_webpage
|
|
@@ -8,15 +9,16 @@ log = get_logger(__name__)
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@kash_action(
|
|
12
|
+
expected_args=ONE_OR_MORE_ARGS,
|
|
11
13
|
params=(
|
|
12
14
|
Param(
|
|
13
15
|
name="clean_headings",
|
|
14
16
|
type=bool,
|
|
15
17
|
description="Use an LLM to clean up headings.",
|
|
16
18
|
),
|
|
17
|
-
)
|
|
19
|
+
),
|
|
18
20
|
)
|
|
19
|
-
def
|
|
21
|
+
def tabbed_webpage_config(input: ActionInput, clean_headings: bool = False) -> ActionResult:
|
|
20
22
|
"""
|
|
21
23
|
Set up a web page config with optional tabs for each page of content. Uses first item as the page title.
|
|
22
24
|
"""
|
|
@@ -24,6 +26,6 @@ def webpage_config(input: ActionInput, clean_headings: bool = False) -> ActionRe
|
|
|
24
26
|
if not item.body:
|
|
25
27
|
raise InvalidInput(f"Item must have a body: {item}")
|
|
26
28
|
|
|
27
|
-
config_item = tabbed_webpage.
|
|
29
|
+
config_item = tabbed_webpage.tabbed_webpage_config(input.items, clean_headings)
|
|
28
30
|
|
|
29
31
|
return ActionResult([config_item])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from kash.config.logger import get_logger
|
|
2
2
|
from kash.exec import kash_action
|
|
3
3
|
from kash.exec.preconditions import is_config
|
|
4
|
-
from kash.model import ONE_ARG, ActionInput, ActionResult, FileExt, Format, Item, ItemType
|
|
4
|
+
from kash.model import ONE_ARG, ActionInput, ActionResult, FileExt, Format, Item, ItemType, Param
|
|
5
5
|
from kash.web_gen import tabbed_webpage
|
|
6
6
|
|
|
7
7
|
log = get_logger(__name__)
|
|
@@ -10,13 +10,14 @@ log = get_logger(__name__)
|
|
|
10
10
|
@kash_action(
|
|
11
11
|
expected_args=ONE_ARG,
|
|
12
12
|
precondition=is_config,
|
|
13
|
+
params=(Param("add_title", "Add a title to the page body.", type=bool),),
|
|
13
14
|
)
|
|
14
|
-
def
|
|
15
|
+
def tabbed_webpage_generate(input: ActionInput, add_title: bool = False) -> ActionResult:
|
|
15
16
|
"""
|
|
16
|
-
Generate a web page from a
|
|
17
|
+
Generate a tabbed web page from a config item for the tabbed template.
|
|
17
18
|
"""
|
|
18
19
|
config_item = input.items[0]
|
|
19
|
-
html = tabbed_webpage.
|
|
20
|
+
html = tabbed_webpage.tabbed_webpage_generate(config_item, add_title_h1=add_title)
|
|
20
21
|
|
|
21
22
|
webpage_item = Item(
|
|
22
23
|
title=config_item.title,
|
|
@@ -75,6 +75,9 @@ 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
|
|
79
|
+
|
|
80
|
+
|
|
78
81
|
@kash_command
|
|
79
82
|
def files(
|
|
80
83
|
*paths: str,
|
|
@@ -108,9 +111,11 @@ def files(
|
|
|
108
111
|
and grouping.
|
|
109
112
|
|
|
110
113
|
:param overview: Recurse a couple levels and show files, but not too many.
|
|
111
|
-
Same as `--groupby=parent --depth=2 --max_per_group=
|
|
114
|
+
Same as `--groupby=parent --depth=2 --max_per_group=100 --omit_dirs`
|
|
115
|
+
except also scales down `max_per_group` to 25 or 50 if there are many files.
|
|
112
116
|
:param recent: Only shows the most recently modified files in each directory.
|
|
113
|
-
Same as `--sort=modified --reverse --groupby=parent --max_per_group=
|
|
117
|
+
Same as `--sort=modified --reverse --groupby=parent --max_per_group=100`
|
|
118
|
+
except also scales down `max_per_group` to 25 or 50 if there are many files.
|
|
114
119
|
:param recursive: List all files recursively. Same as `--depth=-1`.
|
|
115
120
|
:param flat: Show files in a flat list, rather than grouped by parent directory.
|
|
116
121
|
Same as `--groupby=flat`.
|
|
@@ -179,16 +184,17 @@ def files(
|
|
|
179
184
|
# Within workspaces, we show more files by default since they are always in
|
|
180
185
|
# subdirectories.
|
|
181
186
|
overview = True # Handled next.
|
|
187
|
+
cap_per_group = False
|
|
182
188
|
if overview:
|
|
183
|
-
max_per_group = 10 if max_per_group <= 0 else max_per_group
|
|
184
189
|
groupby = GroupByOption.parent if groupby is None else groupby
|
|
185
190
|
depth = 2 if depth is None else depth
|
|
191
|
+
cap_per_group = True
|
|
186
192
|
omit_dirs = True
|
|
187
193
|
if recent:
|
|
188
|
-
max_per_group = 10 if max_per_group <= 0 else max_per_group
|
|
189
194
|
groupby = GroupByOption.parent if groupby is None else groupby
|
|
190
195
|
depth = 2 if depth is None else depth
|
|
191
196
|
sort = SortOption.modified if sort is None else sort
|
|
197
|
+
cap_per_group = True
|
|
192
198
|
reverse = True
|
|
193
199
|
if flat:
|
|
194
200
|
groupby = GroupByOption.flat
|
|
@@ -291,6 +297,18 @@ def files(
|
|
|
291
297
|
|
|
292
298
|
return ShellResult(show_selection=True)
|
|
293
299
|
|
|
300
|
+
# Unless max_per_group is explicit, use heuristics to limit per group if
|
|
301
|
+
# there are lots of groups and lots of files per group.
|
|
302
|
+
# Default is max 100 per group but if we have 4 * 100 items, cut to 25.
|
|
303
|
+
# 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
|
+
max_pg_explicit = max_per_group > 0
|
|
306
|
+
if not max_pg_explicit:
|
|
307
|
+
group_lens = [len(group_df) for group_df in grouped]
|
|
308
|
+
for ratio in [2, 4]:
|
|
309
|
+
if sum(group_lens) > ratio * DEFAULT_MAX_PG:
|
|
310
|
+
final_max_pg = int(DEFAULT_MAX_PG / ratio)
|
|
311
|
+
|
|
294
312
|
total_displayed = 0
|
|
295
313
|
total_displayed_size = 0
|
|
296
314
|
now = datetime.now(UTC)
|
|
@@ -312,8 +330,8 @@ def files(
|
|
|
312
330
|
text_wrap=Wrap.NONE,
|
|
313
331
|
)
|
|
314
332
|
|
|
315
|
-
if
|
|
316
|
-
display_df = group_df.head(
|
|
333
|
+
if final_max_pg > 0:
|
|
334
|
+
display_df = group_df.head(final_max_pg)
|
|
317
335
|
else:
|
|
318
336
|
display_df = group_df
|
|
319
337
|
|
|
@@ -378,9 +396,9 @@ def files(
|
|
|
378
396
|
total_displayed_size += row.size
|
|
379
397
|
|
|
380
398
|
# Indicate if items are omitted.
|
|
381
|
-
if groupby and
|
|
399
|
+
if groupby and final_max_pg > 0 and len(group_df) > final_max_pg:
|
|
382
400
|
cprint(
|
|
383
|
-
f"{indent}… and {len(group_df) -
|
|
401
|
+
f"{indent}… and {len(group_df) - final_max_pg} more files",
|
|
384
402
|
style=COLOR_EXTRA,
|
|
385
403
|
text_wrap=Wrap.NONE,
|
|
386
404
|
)
|
|
@@ -388,9 +406,9 @@ def files(
|
|
|
388
406
|
if group_name:
|
|
389
407
|
PrintHooks.spacer()
|
|
390
408
|
|
|
391
|
-
if not groupby and
|
|
409
|
+
if not groupby and final_max_pg > 0 and items_matching > final_max_pg:
|
|
392
410
|
cprint(
|
|
393
|
-
f"{indent}… and {items_matching -
|
|
411
|
+
f"{indent}… and {items_matching - final_max_pg} more files",
|
|
394
412
|
style=COLOR_EXTRA,
|
|
395
413
|
text_wrap=Wrap.NONE,
|
|
396
414
|
)
|
|
@@ -285,7 +285,7 @@ def init_workspace(path: str | None = None) -> None:
|
|
|
285
285
|
@kash_command
|
|
286
286
|
def workspace(workspace_name: str | None = None) -> None:
|
|
287
287
|
"""
|
|
288
|
-
If no args are given,
|
|
288
|
+
If no args are given, show current workspace info.
|
|
289
289
|
If a workspace name is given, change to that workspace, creating it if it doesn't exist.
|
|
290
290
|
"""
|
|
291
291
|
if workspace_name:
|
|
@@ -302,7 +302,6 @@ def workspace(workspace_name: str | None = None) -> None:
|
|
|
302
302
|
ws.log_workspace_info()
|
|
303
303
|
else:
|
|
304
304
|
ws = current_ws(silent=True)
|
|
305
|
-
os.chdir(ws.base_dir)
|
|
306
305
|
ws.log_workspace_info()
|
|
307
306
|
|
|
308
307
|
|
kash/config/colors.py
CHANGED
|
@@ -136,8 +136,8 @@ web_light_translucent = SimpleNamespace(
|
|
|
136
136
|
bg=hsl_to_hex("hsla(44, 6%, 100%, 0.75)"),
|
|
137
137
|
bg_solid=hsl_to_hex("hsla(44, 6%, 100%, 1)"),
|
|
138
138
|
bg_header=hsl_to_hex("hsla(188, 42%, 70%, 0.2)"),
|
|
139
|
-
bg_alt=hsl_to_hex("hsla(
|
|
140
|
-
bg_alt_solid=hsl_to_hex("hsla(
|
|
139
|
+
bg_alt=hsl_to_hex("hsla(39, 24%, 90%, 0.3)"),
|
|
140
|
+
bg_alt_solid=hsl_to_hex("hsla(39, 24%, 97%, 1)"),
|
|
141
141
|
text=hsl_to_hex("hsl(188, 39%, 11%)"),
|
|
142
142
|
border=hsl_to_hex("hsl(188, 8%, 50%)"),
|
|
143
143
|
border_hint=hsl_to_hex("hsla(188, 8%, 72%, 0.7)"),
|
kash/exec/action_decorators.py
CHANGED
|
@@ -174,7 +174,7 @@ def _merge_param_declarations(
|
|
|
174
174
|
merged_params[fp.name] = Param(
|
|
175
175
|
name=fp.name,
|
|
176
176
|
description=None,
|
|
177
|
-
type=fp.
|
|
177
|
+
type=fp.effective_type or str,
|
|
178
178
|
default_value=fp.default if fp.has_default else None,
|
|
179
179
|
is_explicit=not fp.has_default,
|
|
180
180
|
)
|
|
@@ -248,13 +248,13 @@ def kash_action(
|
|
|
248
248
|
|
|
249
249
|
# Inspect and sanity check the formal params.
|
|
250
250
|
func_params = inspect_function_params(orig_func)
|
|
251
|
-
if len(func_params) == 0 or func_params[0].
|
|
251
|
+
if len(func_params) == 0 or func_params[0].effective_type not in (ActionInput, Item):
|
|
252
252
|
raise InvalidDefinition(
|
|
253
253
|
f"Decorator `@kash_action` requires exactly one positional parameter, "
|
|
254
254
|
f"`input` of type `ActionInput` or `Item` on function `{orig_func.__name__}` but "
|
|
255
255
|
f"got params: {func_params}"
|
|
256
256
|
)
|
|
257
|
-
if any(fp.
|
|
257
|
+
if any(fp.is_pure_positional for fp in func_params[1:]):
|
|
258
258
|
raise InvalidDefinition(
|
|
259
259
|
"Decorator `@kash_action` requires all parameters after the first positional "
|
|
260
260
|
f"parameter to be keyword parameters on function `{orig_func.__name__}` but "
|
|
@@ -265,7 +265,7 @@ def kash_action(
|
|
|
265
265
|
context_param = next((fp for fp in func_params if fp.name == "context"), None)
|
|
266
266
|
if context_param:
|
|
267
267
|
func_params.remove(context_param)
|
|
268
|
-
if context_param and context_param.
|
|
268
|
+
if context_param and context_param.is_pure_positional:
|
|
269
269
|
raise InvalidDefinition(
|
|
270
270
|
"Decorator `@kash_action` requires the `context` parameter to be a keyword "
|
|
271
271
|
"parameter, not positional, on function `{func.__name__}`"
|
|
@@ -273,7 +273,7 @@ def kash_action(
|
|
|
273
273
|
|
|
274
274
|
# If the original function is a simple action function (processes a single item),
|
|
275
275
|
# wrap it to convert to an ActionFunction.
|
|
276
|
-
is_simple_func = func_params[0].
|
|
276
|
+
is_simple_func = func_params[0].effective_type == Item
|
|
277
277
|
action_func: ActionFunction
|
|
278
278
|
if is_simple_func:
|
|
279
279
|
simple_func = cast(SimpleActionFunction, orig_func)
|
|
@@ -333,7 +333,7 @@ def kash_action(
|
|
|
333
333
|
if context_param:
|
|
334
334
|
kw_args["context"] = context
|
|
335
335
|
for fp in func_params[1:]:
|
|
336
|
-
if fp.
|
|
336
|
+
if fp.is_pure_positional:
|
|
337
337
|
pos_args.append(self.get_param(fp.name))
|
|
338
338
|
else:
|
|
339
339
|
kw_args[fp.name] = self.get_param(fp.name)
|
kash/exec/llm_transforms.py
CHANGED
|
@@ -12,7 +12,8 @@ from kash.llm_utils.fuzzy_parsing import strip_markdown_fence
|
|
|
12
12
|
from kash.llm_utils.llm_completion import llm_template_completion
|
|
13
13
|
from kash.llm_utils.llm_messages import Message, MessageTemplate
|
|
14
14
|
from kash.model.actions_model import LLMOptions
|
|
15
|
-
from kash.model.items_model import Item
|
|
15
|
+
from kash.model.items_model import Item
|
|
16
|
+
from kash.text_handling.doc_normalization import normalize_formatting_ansi
|
|
16
17
|
from kash.utils.errors import InvalidInput
|
|
17
18
|
from kash.utils.file_utils.file_formats_model import Format
|
|
18
19
|
|
|
@@ -90,6 +91,7 @@ def llm_transform_item(
|
|
|
90
91
|
normalize: bool = True,
|
|
91
92
|
strip_fence: bool = True,
|
|
92
93
|
check_no_results: bool = True,
|
|
94
|
+
format: Format | None = None,
|
|
93
95
|
) -> Item:
|
|
94
96
|
"""
|
|
95
97
|
Main function for running an LLM action on an item.
|
|
@@ -110,12 +112,13 @@ def llm_transform_item(
|
|
|
110
112
|
log.message("LLM transform from action `%s` on item: %s", action.name, item)
|
|
111
113
|
log.message("LLM options: %s", action.llm_options)
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
format = format or item.format or Format.markdown
|
|
116
|
+
result_item = item.derived_copy(body=None, format=format)
|
|
114
117
|
result_str = llm_transform_str(llm_options, item.body, check_no_results=check_no_results)
|
|
115
118
|
if strip_fence:
|
|
116
119
|
result_str = strip_markdown_fence(result_str)
|
|
117
120
|
if normalize:
|
|
118
|
-
result_str =
|
|
121
|
+
result_str = normalize_formatting_ansi(result_str, format=format)
|
|
119
122
|
|
|
120
123
|
result_item.body = result_str
|
|
121
124
|
return result_item
|
kash/exec/preconditions.py
CHANGED
|
@@ -8,6 +8,7 @@ from chopdiff.html import has_timestamp
|
|
|
8
8
|
from kash.exec.precondition_registry import kash_precondition
|
|
9
9
|
from kash.model.items_model import Item, ItemType
|
|
10
10
|
from kash.text_handling.markdown_utils import extract_bullet_points
|
|
11
|
+
from kash.utils.file_utils.file_formats import is_full_html_page
|
|
11
12
|
from kash.utils.file_utils.file_formats_model import Format
|
|
12
13
|
|
|
13
14
|
|
|
@@ -93,6 +94,11 @@ def has_html_body(item: Item) -> bool:
|
|
|
93
94
|
return has_body(item) and item.format in (Format.html, Format.md_html)
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
@kash_precondition
|
|
98
|
+
def has_full_html_page_body(item: Item) -> bool:
|
|
99
|
+
return bool(has_html_body(item) and item.body and is_full_html_page(item.body))
|
|
100
|
+
|
|
101
|
+
|
|
96
102
|
@kash_precondition
|
|
97
103
|
def is_plaintext(item: Item) -> bool:
|
|
98
104
|
return has_body(item) and item.format == Format.plaintext
|
kash/exec/resolve_args.py
CHANGED
|
@@ -105,6 +105,10 @@ def assemble_action_args(
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
def resolvable_paths(paths: Sequence[StorePath | Path]) -> list[StorePath]:
|
|
108
|
+
"""
|
|
109
|
+
Return which of the given StorePaths are resolvable (exist) in the
|
|
110
|
+
current workspace.
|
|
111
|
+
"""
|
|
108
112
|
ws = current_ws()
|
|
109
113
|
resolvable = list(filter(None, (ws.resolve_path(p) for p in paths)))
|
|
110
114
|
return resolvable
|
kash/file_storage/file_store.py
CHANGED
|
@@ -450,19 +450,11 @@ class FileStore(Workspace):
|
|
|
450
450
|
if not path.exists():
|
|
451
451
|
raise FileNotFound(f"File not found: {fmt_loc(path)}")
|
|
452
452
|
|
|
453
|
-
#
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
#
|
|
457
|
-
|
|
458
|
-
if not item_type and filename_item_type:
|
|
459
|
-
item_type = filename_item_type
|
|
460
|
-
if not item_type and format:
|
|
461
|
-
item_type = ItemType.for_format(format)
|
|
462
|
-
if not item_type:
|
|
463
|
-
item_type = ItemType.resource
|
|
464
|
-
|
|
465
|
-
if format and format.supports_frontmatter:
|
|
453
|
+
# First treat it as an external file to analyze file type and format.
|
|
454
|
+
item = Item.from_external_path(path)
|
|
455
|
+
|
|
456
|
+
# If it's a text/frontmatter-friendly, read it fully.
|
|
457
|
+
if item.format and item.format.supports_frontmatter:
|
|
466
458
|
log.message("Importing text file: %s", fmt_loc(path))
|
|
467
459
|
# This will read the file with or without frontmatter.
|
|
468
460
|
# We are importing so we want to drop the external path so we save the body.
|
|
@@ -486,15 +478,25 @@ class FileStore(Workspace):
|
|
|
486
478
|
log.message("Importing non-text file: %s", fmt_loc(path))
|
|
487
479
|
# Binary or other files we just copy over as-is, preserving the name.
|
|
488
480
|
# We know the extension is recognized.
|
|
489
|
-
|
|
490
|
-
store_path, _found, _prev = self.store_path_for(item)
|
|
481
|
+
store_path, _found, old_store_path = self.store_path_for(item)
|
|
491
482
|
if self.exists(store_path):
|
|
492
483
|
raise FileExists(f"Resource already in store: {fmt_loc(store_path)}")
|
|
493
484
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
log.message("Importing resource: %s -> %s", fmt_loc(path), fmt_loc(store_path))
|
|
485
|
+
log.message("Importing resource: %s", fmt_loc(path))
|
|
497
486
|
copyfile_atomic(path, self.base_dir / store_path, make_parents=True)
|
|
487
|
+
|
|
488
|
+
# Optimization: Don't import an identical file twice.
|
|
489
|
+
if old_store_path:
|
|
490
|
+
old_hash = self.hash(old_store_path)
|
|
491
|
+
new_hash = self.hash(store_path)
|
|
492
|
+
if old_hash == new_hash:
|
|
493
|
+
log.message(
|
|
494
|
+
"Imported resource is identical to the previous import: %s",
|
|
495
|
+
fmt_loc(old_store_path),
|
|
496
|
+
)
|
|
497
|
+
os.unlink(self.base_dir / store_path)
|
|
498
|
+
store_path = old_store_path
|
|
499
|
+
log.message("Imported resource: %s", fmt_loc(store_path))
|
|
498
500
|
return store_path
|
|
499
501
|
|
|
500
502
|
def import_items(
|
kash/help/function_param_info.py
CHANGED
|
@@ -12,7 +12,7 @@ def _look_up_param_docs(func: Callable[..., Any], kw_params: list[FuncParam]) ->
|
|
|
12
12
|
name = func_param.name
|
|
13
13
|
param = ALL_COMMON_PARAMS.get(name)
|
|
14
14
|
if not param:
|
|
15
|
-
param = Param(name, description=None, type=func_param.
|
|
15
|
+
param = Param(name, description=None, type=func_param.effective_type or str)
|
|
16
16
|
|
|
17
17
|
# Also check the docstring for a description of this parameter.
|
|
18
18
|
docstring = parse_docstring(func.__doc__ or "")
|
|
@@ -18,7 +18,6 @@ from kash.shell.file_icons.nerd_icons import icon_for_file
|
|
|
18
18
|
from kash.shell.output.shell_output import Wrap
|
|
19
19
|
from kash.utils.common.type_utils import not_none
|
|
20
20
|
from kash.utils.errors import FileNotFound, InvalidFilename
|
|
21
|
-
from kash.web_gen import base_templates_dir
|
|
22
21
|
from kash.web_gen.template_render import render_web_template
|
|
23
22
|
from kash.workspaces.workspace_output import print_file_info
|
|
24
23
|
|
|
@@ -140,14 +139,11 @@ def explain(text: str):
|
|
|
140
139
|
|
|
141
140
|
return HTMLResponse(
|
|
142
141
|
render_web_template(
|
|
143
|
-
base_templates_dir,
|
|
144
142
|
"base_webpage.html.jinja",
|
|
145
143
|
{
|
|
146
144
|
"title": f"Help: {text}",
|
|
147
145
|
"content": render_web_template(
|
|
148
|
-
|
|
149
|
-
"explain_view.html.jinja",
|
|
150
|
-
{"help_html": help_html, "page_url": page_url},
|
|
146
|
+
"explain_view.html.jinja", {"help_html": help_html, "page_url": page_url}
|
|
151
147
|
),
|
|
152
148
|
},
|
|
153
149
|
)
|
|
@@ -270,12 +266,10 @@ def _serve_item(
|
|
|
270
266
|
|
|
271
267
|
return HTMLResponse(
|
|
272
268
|
render_web_template(
|
|
273
|
-
base_templates_dir,
|
|
274
269
|
"base_webpage.html.jinja",
|
|
275
270
|
{
|
|
276
271
|
"title": display_title,
|
|
277
272
|
"content": render_web_template(
|
|
278
|
-
base_templates_dir,
|
|
279
273
|
"item_view.html.jinja",
|
|
280
274
|
{
|
|
281
275
|
"item": item,
|