kash-shell 0.3.28__py3-none-any.whl → 0.3.30__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 (61) hide show
  1. kash/actions/core/markdownify_html.py +1 -4
  2. kash/actions/core/minify_html.py +4 -5
  3. kash/actions/core/render_as_html.py +9 -7
  4. kash/actions/core/save_sidematter_meta.py +47 -0
  5. kash/actions/core/zip_sidematter.py +47 -0
  6. kash/commands/base/basic_file_commands.py +7 -4
  7. kash/commands/base/diff_commands.py +6 -4
  8. kash/commands/base/files_command.py +31 -30
  9. kash/commands/base/general_commands.py +3 -2
  10. kash/commands/base/logs_commands.py +6 -4
  11. kash/commands/base/reformat_command.py +3 -2
  12. kash/commands/base/search_command.py +4 -3
  13. kash/commands/base/show_command.py +9 -7
  14. kash/commands/help/assistant_commands.py +6 -4
  15. kash/commands/help/help_commands.py +7 -4
  16. kash/commands/workspace/selection_commands.py +18 -16
  17. kash/commands/workspace/workspace_commands.py +39 -26
  18. kash/config/setup.py +2 -27
  19. kash/docs/markdown/topics/a1_what_is_kash.md +26 -18
  20. kash/exec/action_decorators.py +2 -2
  21. kash/exec/action_exec.py +56 -50
  22. kash/exec/fetch_url_items.py +36 -9
  23. kash/exec/preconditions.py +2 -2
  24. kash/exec/resolve_args.py +4 -1
  25. kash/exec/runtime_settings.py +1 -0
  26. kash/file_storage/file_store.py +59 -23
  27. kash/file_storage/item_file_format.py +91 -26
  28. kash/help/help_types.py +1 -1
  29. kash/llm_utils/llms.py +6 -1
  30. kash/local_server/local_server_commands.py +2 -1
  31. kash/mcp/mcp_server_commands.py +3 -2
  32. kash/mcp/mcp_server_routes.py +1 -1
  33. kash/model/actions_model.py +31 -30
  34. kash/model/compound_actions_model.py +4 -3
  35. kash/model/exec_model.py +30 -3
  36. kash/model/items_model.py +114 -57
  37. kash/model/params_model.py +4 -4
  38. kash/shell/output/shell_output.py +1 -2
  39. kash/utils/file_formats/chat_format.py +7 -4
  40. kash/utils/file_utils/file_ext.py +1 -0
  41. kash/utils/file_utils/file_formats.py +4 -2
  42. kash/utils/file_utils/file_formats_model.py +12 -0
  43. kash/utils/text_handling/doc_normalization.py +1 -1
  44. kash/utils/text_handling/markdown_footnotes.py +224 -0
  45. kash/utils/text_handling/markdown_utils.py +532 -41
  46. kash/utils/text_handling/markdownify_utils.py +2 -1
  47. kash/web_gen/templates/components/tooltip_scripts.js.jinja +186 -1
  48. kash/web_gen/templates/components/youtube_popover_scripts.js.jinja +223 -0
  49. kash/web_gen/templates/components/youtube_popover_styles.css.jinja +150 -0
  50. kash/web_gen/templates/content_styles.css.jinja +53 -1
  51. kash/web_gen/templates/youtube_webpage.html.jinja +47 -0
  52. kash/web_gen/webpage_render.py +103 -0
  53. kash/workspaces/workspaces.py +0 -5
  54. kash/xonsh_custom/custom_shell.py +4 -3
  55. {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/METADATA +33 -24
  56. {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/RECORD +59 -54
  57. kash/llm_utils/llm_features.py +0 -72
  58. kash/web_gen/simple_webpage.py +0 -55
  59. {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/WHEEL +0 -0
  60. {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/entry_points.txt +0 -0
  61. {kash_shell-0.3.28.dist-info → kash_shell-0.3.30.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,16 @@
1
1
  from pathlib import Path
2
2
 
3
- from frontmatter_format import FmStyle, fmf_has_frontmatter, fmf_read, fmf_write
3
+ from frontmatter_format import (
4
+ FmStyle,
5
+ fmf_has_frontmatter,
6
+ fmf_read,
7
+ fmf_read_frontmatter,
8
+ fmf_write,
9
+ )
4
10
  from funlog import tally_calls
5
11
  from prettyfmt import custom_key_sort, fmt_size_human
12
+ from sidematter_format import Sidematter
13
+ from strif import atomic_output_file, single_line
6
14
 
7
15
  from kash.config.logger import get_logger
8
16
  from kash.model.items_model import ITEM_FIELDS, Item
@@ -22,19 +30,28 @@ _item_cache = MtimeCache[Item](max_size=2000, name="Item")
22
30
 
23
31
 
24
32
  @tally_calls()
25
- def write_item(item: Item, path: Path, normalize: bool = True):
33
+ def write_item(item: Item, path: Path, *, normalize: bool = True, use_frontmatter: bool = True):
26
34
  """
27
- Write a text item to a file with standard frontmatter format YAML.
35
+ Write a text item to a file with standard frontmatter format YAML or sidematter format.
28
36
  By default normalizes formatting of the body text and updates the item's body.
37
+
38
+ If `use_frontmatter` is True, uses frontmatter on the file for metadata, and omits
39
+ the sidematter metadata file.
40
+
41
+ This function does not explicitly write sidematter assets; these can be written
42
+ separately.
29
43
  """
30
44
  item.validate()
31
- if item.format and not item.format.supports_frontmatter:
45
+ if use_frontmatter and item.format and not item.format.supports_frontmatter:
32
46
  raise ValueError(f"Item format `{item.format.value}` does not support frontmatter: {item}")
33
47
 
34
48
  # Clear cache before writing.
35
49
  _item_cache.delete(path)
36
50
 
51
+ title = item.title
37
52
  if normalize:
53
+ if item.title:
54
+ title = single_line(item.title)
38
55
  body = normalize_formatting(item.body_text(), item.format)
39
56
  else:
40
57
  body = item.body_text()
@@ -65,29 +82,44 @@ def write_item(item: Item, path: Path, normalize: bool = True):
65
82
 
66
83
  log.debug("Writing item to %s: body length %s, metadata %s", path, len(body), item.metadata())
67
84
 
68
- fmf_write(
69
- path,
70
- body,
71
- item.metadata(),
72
- style=fm_style,
73
- key_sort=ITEM_FIELD_SORT,
74
- make_parents=True,
75
- )
85
+ # Use sidematter format
86
+ spath = Sidematter(path)
87
+ if use_frontmatter:
88
+ # Use frontmatter format
89
+ fmf_write(
90
+ path,
91
+ body,
92
+ item.metadata(),
93
+ style=fm_style,
94
+ key_sort=ITEM_FIELD_SORT,
95
+ make_parents=True,
96
+ )
97
+ else:
98
+ # Write the main file with just the body (no frontmatter)
99
+ with atomic_output_file(path, make_parents=True) as f:
100
+ f.write_text(body, encoding="utf-8")
101
+
102
+ # Sidematter metadata
103
+ spath.write_meta(item.metadata(), key_sort=ITEM_FIELD_SORT)
76
104
 
77
105
  # Update cache.
78
106
  _item_cache.update(path, item)
79
107
 
80
- # Update the item's body to reflect normalization.
108
+ # Update the item.
109
+ item.title = title
81
110
  item.body = body
82
111
 
83
112
 
84
- def read_item(path: Path, base_dir: Path | None) -> Item:
113
+ def read_item(path: Path, base_dir: Path | None, preserve_filename: bool = True) -> Item:
85
114
  """
86
- Read an item from a file. Uses `base_dir` to resolve paths, so the item's
115
+ Read a text item from a file. Uses `base_dir` to resolve paths, so the item's
87
116
  `store_path` will be set and be relative to `base_dir`.
88
117
 
89
- If frontmatter format YAML is present, it is parsed. If not, the item will
90
- be a resource with a format inferred from the file extension or the content.
118
+ Automatically detects and reads sidematter format (metadata in .meta.yml/.meta.json
119
+ sidecar files), which takes precedence over frontmatter when present. Falls back to
120
+ frontmatter format YAML if no sidematter is found. If neither is present, the item
121
+ will be a resource with a format inferred from the file extension or the content.
122
+
91
123
  The `store_path` will be the path relative to the `base_dir`, if the file
92
124
  is within `base_dir`, or otherwise the `external_path` will be set to the path
93
125
  it was read from.
@@ -98,20 +130,48 @@ def read_item(path: Path, base_dir: Path | None) -> Item:
98
130
  log.debug("Cache hit for item: %s", path)
99
131
  return cached_item
100
132
 
101
- return _read_item_uncached(path, base_dir)
133
+ return _read_item_uncached(path, base_dir, preserve_filename=preserve_filename)
102
134
 
103
135
 
104
136
  @tally_calls()
105
- def _read_item_uncached(path: Path, base_dir: Path | None) -> Item:
137
+ def _read_item_uncached(
138
+ path: Path,
139
+ base_dir: Path | None,
140
+ *,
141
+ preserve_filename: bool = True,
142
+ prefer_frontmatter: bool = True,
143
+ ) -> Item:
144
+ # First, try to resolve sidematter
145
+ sidematter = Sidematter(path).resolve(use_frontmatter=False)
146
+
147
+ # Use sidematter metadata unless we find and prefer frontmatter for metadata.
106
148
  has_frontmatter = fmf_has_frontmatter(path)
107
- body = metadata = None
108
- if has_frontmatter:
149
+ frontmatter_meta = prefer_frontmatter and has_frontmatter and fmf_read_frontmatter(path)
150
+ if sidematter.meta and not frontmatter_meta:
151
+ metadata = sidematter.meta
152
+ body, _frontmatter_metadata = fmf_read(path)
153
+ log.debug(
154
+ "Read item from sidematter %s: body length %s, metadata %s",
155
+ sidematter.meta_path,
156
+ len(body),
157
+ metadata,
158
+ )
159
+ elif has_frontmatter:
109
160
  body, metadata = fmf_read(path)
110
- log.debug("Read item from %s: body length %s, metadata %s", path, len(body), metadata)
161
+ log.debug(
162
+ "Read item from %s: body length %s, metadata %s",
163
+ path,
164
+ len(body),
165
+ metadata,
166
+ )
167
+ else:
168
+ # Not readable, binary or otherwise.
169
+ metadata = None
170
+ body = None
111
171
 
112
- path = path.resolve()
113
- if base_dir:
114
- base_dir = base_dir.resolve()
172
+ path = path.resolve()
173
+ if base_dir:
174
+ base_dir = base_dir.resolve()
115
175
 
116
176
  # Ensure store_path is used if it's within the base_dir, and
117
177
  # external_path otherwise.
@@ -128,7 +188,8 @@ def _read_item_uncached(path: Path, base_dir: Path | None) -> Item:
128
188
  metadata, body=body, store_path=store_path, external_path=external_path
129
189
  )
130
190
  else:
131
- # This is a file without frontmatter. Infer format from the file and content,
191
+ # This is a file without frontmatter or sidematter.
192
+ # Infer format from the file and content,
132
193
  # and use store_path or external_path as appropriate.
133
194
  item = Item.from_external_path(path)
134
195
  if item.format:
@@ -154,6 +215,10 @@ def _read_item_uncached(path: Path, base_dir: Path | None) -> Item:
154
215
  item.body = f.read()
155
216
  item.external_path = None
156
217
 
218
+ # Preserve the original filename.
219
+ if preserve_filename:
220
+ item.original_filename = path.name
221
+
157
222
  # Update modified time.
158
223
  item.set_modified(path.stat().st_mtime)
159
224
 
kash/help/help_types.py CHANGED
@@ -5,7 +5,7 @@ from dataclasses import dataclass
5
5
  from enum import Enum
6
6
  from typing import TYPE_CHECKING, ClassVar
7
7
 
8
- from flowmark.sentence_split_regex import split_sentences_regex
8
+ from flowmark import split_sentences_regex
9
9
  from rich.console import Group
10
10
  from rich.text import Text
11
11
  from strif import abbrev_str, single_line
kash/llm_utils/llms.py CHANGED
@@ -13,6 +13,10 @@ class LLM(LLMName, Enum):
13
13
  """
14
14
 
15
15
  # https://platform.openai.com/docs/models
16
+ gpt_5 = LLMName("gpt-5")
17
+ gpt_5_mini = LLMName("gpt-5-mini")
18
+ gpt_5_nano = LLMName("gpt-5-nano")
19
+ gpt_5_chat = LLMName("gpt-5-chat")
16
20
  o4_mini = LLMName("o4-mini")
17
21
  o3 = LLMName("o3")
18
22
  o3_pro = LLMName("o3-pro")
@@ -25,11 +29,12 @@ class LLM(LLMName, Enum):
25
29
  gpt_4o = LLMName("gpt-4o")
26
30
  gpt_4o_mini = LLMName("gpt-4o-mini")
27
31
  gpt_4 = LLMName("gpt-4")
28
-
29
32
  gpt_4_1_mini = LLMName("gpt-4.1-mini")
30
33
  gpt_4_1_nano = LLMName("gpt-4.1-nano")
31
34
 
32
35
  # https://docs.anthropic.com/en/docs/about-claude/models/all-models
36
+
37
+ claude_4_1_opus = LLMName("claude-opus-4-1")
33
38
  claude_4_opus = LLMName("claude-opus-4-20250514")
34
39
  claude_4_sonnet = LLMName("claude-sonnet-4-20250514")
35
40
  claude_3_7_sonnet = LLMName("claude-3-7-sonnet-latest")
@@ -49,7 +49,8 @@ def local_server_logs(follow: bool = False) -> None:
49
49
  """
50
50
  Show the logs from the kash local (UI and MCP) servers.
51
51
 
52
- :param follow: Follow the file as it grows.
52
+ Args:
53
+ follow: Follow the file as it grows.
53
54
  """
54
55
  log_path = global_settings().local_server_log_path
55
56
  if not log_path.exists():
@@ -54,8 +54,9 @@ def mcp_logs(follow: bool = False, all: bool = False) -> None:
54
54
  """
55
55
  Show the logs from the MCP server and CLI proxy process.
56
56
 
57
- :param follow: Follow the file as it grows.
58
- :param all: Show all logs, not just the server logs, including Claude Desktop logs if found.
57
+ Args:
58
+ follow: Follow the file as it grows.
59
+ all: Show all logs, not just the server logs, including Claude Desktop logs if found.
59
60
  """
60
61
  from kash.mcp.mcp_cli import MCP_CLI_LOG_PATH
61
62
 
@@ -238,7 +238,7 @@ def run_mcp_tool(action_name: str, arguments: dict) -> list[TextContent]:
238
238
  action_input = prepare_action_input(*input_items)
239
239
 
240
240
  result, result_store_paths, _archived_store_paths = run_action_with_caching(
241
- context=context, action_input=action_input
241
+ context, action_input
242
242
  )
243
243
 
244
244
  # Return final result, formatted for the LLM to understand.
@@ -21,9 +21,8 @@ from kash.exec_model.args_model import NO_ARGS, ONE_ARG, ArgCount, ArgType, Sign
21
21
  from kash.exec_model.shell_model import ShellResult
22
22
  from kash.llm_utils import LLM, LLMName
23
23
  from kash.llm_utils.llm_messages import Message, MessageTemplate
24
- from kash.model.exec_model import ExecContext
24
+ from kash.model.exec_model import ActionContext, ExecContext
25
25
  from kash.model.items_model import UNTITLED, Format, Item, ItemType
26
- from kash.model.operations_model import Operation, Source
27
26
  from kash.model.params_model import (
28
27
  ALL_COMMON_PARAMS,
29
28
  COMMON_SHELL_PARAMS,
@@ -61,6 +60,17 @@ class ActionInput:
61
60
  """An empty input, for when an action processes no items."""
62
61
  return ActionInput(items=[])
63
62
 
63
+ # XXX For convenience, we have the ability to include the context on each item
64
+ # (this helps soper-item functions don't have to take context args everywhere).
65
+ # TODO: Probably better to move this to a context var.
66
+ def set_context(self, context: ActionContext) -> None:
67
+ for item in self.items:
68
+ item.context = context
69
+
70
+ def clear_context(self) -> None:
71
+ for item in self.items:
72
+ item.context = None
73
+
64
74
 
65
75
  class PathOpType(Enum):
66
76
  archive = "archive"
@@ -111,6 +121,10 @@ class ActionResult:
111
121
  self.replaces_input or self.skip_duplicates or self.path_ops or self.shell_result
112
122
  )
113
123
 
124
+ def set_context(self, context: ActionContext) -> None:
125
+ for item in self.items:
126
+ item.context = context
127
+
114
128
  def clear_context(self):
115
129
  for item in self.items:
116
130
  item.context = None
@@ -505,33 +519,17 @@ class Action(ABC):
505
519
  fmt_lines(overrides),
506
520
  )
507
521
 
508
- def _preassemble_one(
509
- self,
510
- operation: Operation,
511
- input: ActionInput,
512
- output_num: int,
513
- type: ItemType,
514
- **kwargs,
515
- ) -> Item:
522
+ def format_title(self, prev_title: str | None) -> str:
516
523
  """
517
- Preassemble a single empty output item from the given input items. Include the title,
518
- type, and last Operation so we can do an identity check if the output already exists.
524
+ Format the title for an output item of this action.
519
525
  """
520
- primary_input = input.items[output_num]
521
- item = primary_input.derived_copy(type=type, body=None, **kwargs)
522
-
526
+ prev_title = prev_title or UNTITLED
523
527
  if self.title_template:
524
- item.title = self.title_template.format(
525
- title=primary_input.title or UNTITLED, action_name=self.name
526
- )
527
-
528
- item.update_history(
529
- Source(operation=operation, output_num=output_num, cacheable=self.cacheable)
530
- )
531
-
532
- return item
528
+ return self.title_template.format(title=prev_title, action_name=self.name)
529
+ else:
530
+ return prev_title
533
531
 
534
- def preassemble(self, operation: Operation, input: ActionInput) -> ActionResult | None:
532
+ def preassemble_result(self, context: ActionContext) -> ActionResult | None:
535
533
  """
536
534
  Actions can have a separate preliminary step to pre-assemble outputs. This allows
537
535
  us to determine the title and types for the output items and check if they were
@@ -549,9 +547,11 @@ class Action(ABC):
549
547
  self.cacheable,
550
548
  )
551
549
  if can_preassemble:
552
- return ActionResult(
553
- [self._preassemble_one(operation, input, output_num=0, type=self.output_type)]
554
- )
550
+ # Using first input to determine the output title.
551
+ primary_input = context.action_input.items[0]
552
+ # In this case we only expect one output.
553
+ item = primary_input.derived_copy(context, 0)
554
+ return ActionResult([item])
555
555
  else:
556
556
  # Caching disabled.
557
557
  return None
@@ -600,7 +600,7 @@ class Action(ABC):
600
600
  return input_schema
601
601
 
602
602
  @abstractmethod
603
- def run(self, input: ActionInput, context: ExecContext) -> ActionResult:
603
+ def run(self, input: ActionInput, context: ActionContext) -> ActionResult:
604
604
  pass
605
605
 
606
606
  def __repr__(self):
@@ -626,7 +626,7 @@ class PerItemAction(Action, ABC):
626
626
  super().__post_init__()
627
627
 
628
628
  @override
629
- def run(self, input: ActionInput, context: ExecContext) -> ActionResult:
629
+ def run(self, input: ActionInput, context: ActionContext) -> ActionResult:
630
630
  log.info("Running action `%s` per-item.", self.name)
631
631
  for item in input.items:
632
632
  item.context = context
@@ -642,3 +642,4 @@ class PerItemAction(Action, ABC):
642
642
  # Handle circular dependency in Python dataclasses.
643
643
  rebuild_dataclass(Item) # pyright: ignore
644
644
  rebuild_dataclass(ExecContext) # pyright: ignore
645
+ rebuild_dataclass(ActionContext) # pyright: ignore
@@ -5,7 +5,8 @@ from pydantic.dataclasses import dataclass
5
5
 
6
6
  from kash.config.logger import get_logger
7
7
  from kash.exec.combiners import Combiner
8
- from kash.model.actions_model import Action, ActionInput, ActionResult, ExecContext
8
+ from kash.model.actions_model import Action, ActionInput, ActionResult
9
+ from kash.model.exec_model import ActionContext
9
10
  from kash.model.items_model import Item, State
10
11
  from kash.model.params_model import RawParamValues
11
12
  from kash.model.paths_model import StorePath
@@ -46,7 +47,7 @@ class SequenceAction(Action):
46
47
  )
47
48
  self.description = seq_description
48
49
 
49
- def run(self, input: ActionInput, context: ExecContext) -> ActionResult:
50
+ def run(self, input: ActionInput, context: ActionContext) -> ActionResult:
50
51
  from kash.exec.action_exec import run_action_with_shell_context
51
52
  from kash.workspaces import current_ws
52
53
 
@@ -140,7 +141,7 @@ class ComboAction(Action):
140
141
 
141
142
  self.description = combo_description
142
143
 
143
- def run(self, input: ActionInput, context: ExecContext) -> ActionResult:
144
+ def run(self, input: ActionInput, context: ActionContext) -> ActionResult:
144
145
  from kash.exec.action_exec import run_action_with_shell_context
145
146
  from kash.exec.combiners import combine_as_paragraphs
146
147
 
kash/model/exec_model.py CHANGED
@@ -8,10 +8,11 @@ from pydantic.dataclasses import dataclass
8
8
 
9
9
  from kash.config.logger import get_logger
10
10
  from kash.model.items_model import State
11
+ from kash.model.operations_model import Operation
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from kash.file_storage.file_store import FileStore
14
- from kash.model.actions_model import Action
15
+ from kash.model.actions_model import Action, ActionInput
15
16
 
16
17
 
17
18
  log = get_logger(__name__)
@@ -68,8 +69,8 @@ class RuntimeSettings:
68
69
  @dataclass(frozen=True)
69
70
  class ExecContext:
70
71
  """
71
- An action and its context for execution. This is a good place for settings
72
- that apply to any action and are bothersome to pass as parameters.
72
+ An action and its general context for execution. This is a good place for general
73
+ settings that apply to any action and are bothersome to pass as parameters.
73
74
  """
74
75
 
75
76
  action: Action
@@ -77,3 +78,29 @@ class ExecContext:
77
78
 
78
79
  settings: RuntimeSettings
79
80
  """The workspace and other run-time settings for the action."""
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class ActionContext:
85
+ """
86
+ All context for the currently executing action, with all inputs and options.
87
+ """
88
+
89
+ exec_context: ExecContext
90
+ """The context of the current execution."""
91
+
92
+ action_input: ActionInput
93
+ """The assembled input to the current action."""
94
+
95
+ operation: Operation
96
+ """The operation in full detail, including inputs and options."""
97
+
98
+ @property
99
+ def action(self) -> Action:
100
+ """The action being executed."""
101
+ return self.exec_context.action
102
+
103
+ @property
104
+ def settings(self) -> RuntimeSettings:
105
+ """The workspace and other run-time settings for the action."""
106
+ return self.exec_context.settings