kash-shell 0.3.11__py3-none-any.whl → 0.3.12__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 (62) hide show
  1. kash/actions/core/render_as_html.py +2 -2
  2. kash/actions/core/show_webpage.py +2 -2
  3. kash/actions/core/strip_html.py +2 -2
  4. kash/commands/base/basic_file_commands.py +21 -3
  5. kash/commands/base/files_command.py +5 -4
  6. kash/commands/extras/parse_uv_lock.py +12 -3
  7. kash/commands/workspace/selection_commands.py +1 -1
  8. kash/commands/workspace/workspace_commands.py +1 -1
  9. kash/config/env_settings.py +2 -42
  10. kash/config/logger.py +30 -25
  11. kash/config/logger_basic.py +6 -6
  12. kash/config/settings.py +23 -7
  13. kash/config/setup.py +33 -5
  14. kash/config/text_styles.py +25 -22
  15. kash/embeddings/cosine.py +12 -4
  16. kash/embeddings/embeddings.py +16 -6
  17. kash/embeddings/text_similarity.py +10 -4
  18. kash/exec/__init__.py +3 -0
  19. kash/exec/action_decorators.py +4 -19
  20. kash/exec/action_exec.py +43 -23
  21. kash/exec/llm_transforms.py +2 -2
  22. kash/exec/preconditions.py +4 -12
  23. kash/exec/runtime_settings.py +134 -0
  24. kash/exec/shell_callable_action.py +5 -3
  25. kash/file_storage/file_store.py +18 -21
  26. kash/file_storage/item_file_format.py +6 -3
  27. kash/file_storage/store_filenames.py +6 -3
  28. kash/llm_utils/init_litellm.py +16 -0
  29. kash/llm_utils/llm_api_keys.py +6 -2
  30. kash/llm_utils/llm_completion.py +11 -4
  31. kash/mcp/mcp_cli.py +3 -2
  32. kash/mcp/mcp_server_routes.py +11 -12
  33. kash/media_base/transcription_deepgram.py +15 -2
  34. kash/model/__init__.py +1 -1
  35. kash/model/actions_model.py +6 -54
  36. kash/model/exec_model.py +79 -0
  37. kash/model/items_model.py +71 -50
  38. kash/model/operations_model.py +38 -15
  39. kash/model/paths_model.py +2 -0
  40. kash/shell/output/shell_output.py +10 -8
  41. kash/shell/shell_main.py +2 -2
  42. kash/shell/utils/exception_printing.py +2 -2
  43. kash/text_handling/doc_normalization.py +16 -8
  44. kash/text_handling/markdown_utils.py +83 -2
  45. kash/utils/common/format_utils.py +2 -8
  46. kash/utils/common/inflection.py +22 -0
  47. kash/utils/common/task_stack.py +4 -15
  48. kash/utils/errors.py +14 -9
  49. kash/utils/file_utils/file_formats_model.py +15 -0
  50. kash/utils/file_utils/file_sort_filter.py +10 -3
  51. kash/web_gen/templates/base_styles.css.jinja +8 -3
  52. kash/workspaces/__init__.py +12 -3
  53. kash/workspaces/workspace_dirs.py +58 -0
  54. kash/workspaces/workspace_importing.py +1 -1
  55. kash/workspaces/workspaces.py +26 -90
  56. {kash_shell-0.3.11.dist-info → kash_shell-0.3.12.dist-info}/METADATA +4 -4
  57. {kash_shell-0.3.11.dist-info → kash_shell-0.3.12.dist-info}/RECORD +60 -57
  58. kash/shell/utils/argparse_utils.py +0 -20
  59. kash/utils/lang_utils/inflection.py +0 -18
  60. {kash_shell-0.3.11.dist-info → kash_shell-0.3.12.dist-info}/WHEEL +0 -0
  61. {kash_shell-0.3.11.dist-info → kash_shell-0.3.12.dist-info}/entry_points.txt +0 -0
  62. {kash_shell-0.3.11.dist-info → kash_shell-0.3.12.dist-info}/licenses/LICENSE +0 -0
@@ -73,8 +73,8 @@ COLOR_VALUE = "cyan"
73
73
  COLOR_PATH = "cyan"
74
74
  COLOR_HINT = "bright_black"
75
75
  COLOR_HINT_DIM = "dim bright_black"
76
- COLOR_SKIP = "green"
77
- COLOR_TASK = "magenta"
76
+ COLOR_SKIP = "bright_blue"
77
+ COLOR_ACTION = "magenta"
78
78
  COLOR_SAVED = "blue"
79
79
  COLOR_TIMING = "bright_black"
80
80
  COLOR_CALL = "bright_yellow"
@@ -133,7 +133,7 @@ RICH_STYLES = {
133
133
  "hint": COLOR_HINT,
134
134
  "hint_dim": COLOR_HINT_DIM,
135
135
  "skip": COLOR_SKIP,
136
- "task": COLOR_TASK,
136
+ "action": COLOR_ACTION,
137
137
  "saved": COLOR_SAVED,
138
138
  "timing": COLOR_TIMING,
139
139
  "call": COLOR_CALL,
@@ -218,12 +218,14 @@ RICH_STYLES = {
218
218
  "kash.size_k": STYLE_SIZE2,
219
219
  "kash.size_m": STYLE_SIZE3,
220
220
  "kash.size_gtp": STYLE_SIZE4,
221
- "kash.filename": Style(color=COLOR_VALUE),
222
- "kash.task_stack_header": Style(color=COLOR_TASK),
223
- "kash.task_stack": Style(color=COLOR_TASK),
221
+ "kash.filename": Style(color=COLOR_HINT),
222
+ "kash.start_action": Style(color=COLOR_ACTION, bold=True),
223
+ "kash.task_stack_header": Style(color=COLOR_HINT),
224
+ "kash.task_stack": Style(color=COLOR_HINT),
224
225
  "kash.task_stack_prefix": Style(color=COLOR_HINT),
225
226
  # Emoji colors:
226
- "kash.task": Style(color=COLOR_TASK),
227
+ "kash.action": Style(color=COLOR_ACTION),
228
+ "kash.start": Style(color=COLOR_ACTION, bold=True),
227
229
  "kash.success": Style(color=COLOR_SUCCESS, bold=True),
228
230
  "kash.skip": Style(color=COLOR_SKIP, bold=True),
229
231
  "kash.failure": Style(color=COLOR_ERROR, bold=True),
@@ -259,17 +261,24 @@ PROMPT_ASSIST = "(assistant) ❯"
259
261
 
260
262
  EMOJI_HINT = "👉"
261
263
 
262
- # More ideas: ⦿⧁⧀⦿⦾⟐⦊⟡
264
+ EMOJI_MSG_INDENT = "⋮"
263
265
 
266
+ EMOJI_START = "[➤]"
264
267
 
265
- EMOJI_COMMAND = ""
268
+ EMOJI_SUCCESS = "[✔︎]"
266
269
 
267
- EMOJI_ACTION = ""
270
+ EMOJI_SKIP = "[-]"
271
+
272
+ EMOJI_FAILURE = "[✘]"
268
273
 
269
274
  EMOJI_SNIPPET = "❯"
270
275
 
271
276
  EMOJI_HELP = "?"
272
277
 
278
+ EMOJI_ACTION = "⛭"
279
+
280
+ EMOJI_COMMAND = "⧁" # More ideas: ⦿⧁⧀⦿⦾⟐⦊⟡
281
+
273
282
  EMOJI_SHELL = "⦊"
274
283
 
275
284
  EMOJI_RECOMMENDED = "•"
@@ -282,26 +291,18 @@ EMOJI_SAVED = "⩣"
282
291
 
283
292
  EMOJI_TIMING = "⏱"
284
293
 
285
- EMOJI_SUCCESS = "[✔︎]"
286
-
287
- EMOJI_SKIP = "[∕]"
288
-
289
- EMOJI_FAILURE = "[✘]"
290
-
291
294
  EMOJI_CALL_BEGIN = "≫"
292
295
 
293
296
  EMOJI_CALL_END = "≪"
294
297
 
295
298
  EMOJI_ASSISTANT = "🤖"
296
299
 
297
- EMOJI_MSG_INDENT = "⋮"
298
-
299
300
  EMOJI_BREADCRUMB_SEP = "›"
300
301
 
301
302
 
302
303
  ## Special headings
303
304
 
304
- TASK_STACK_HEADER = "Task stack:"
305
+ TASK_STACK_HEADER = "Task stack"
305
306
 
306
307
 
307
308
  ## Rich setup
@@ -320,15 +321,17 @@ class KashHighlighter(RegexHighlighter):
320
321
  base_style = "kash."
321
322
  highlights = [
322
323
  _combine_regex(
324
+ # Important patterns that color the whole line:
325
+ f"(?P<start_action>{re.escape(EMOJI_START + ' Action')}.*)",
326
+ f"(?P<timing>{re.escape(EMOJI_TIMING)}.*)",
323
327
  # Task stack in logs:
324
328
  f"(?P<task_stack_header>{re.escape(TASK_STACK_HEADER)})",
325
329
  f"(?P<task_stack>{re.escape(EMOJI_BREADCRUMB_SEP)}.*)",
326
330
  f"(?P<task_stack_prefix>{re.escape(EMOJI_MSG_INDENT)})",
327
- # Emojis that color the whole line:
328
- f"(?P<timing>{re.escape(EMOJI_TIMING)}.*)",
329
331
  # Color emojis by themselves:
330
332
  f"(?P<saved>{re.escape(EMOJI_SAVED)})",
331
- f"(?P<task>{re.escape(EMOJI_ACTION)})",
333
+ f"(?P<action>{re.escape(EMOJI_ACTION)})",
334
+ f"(?P<start>{re.escape(EMOJI_START)})",
332
335
  f"(?P<success>{re.escape(EMOJI_SUCCESS)})",
333
336
  f"(?P<skip>{re.escape(EMOJI_SKIP)})",
334
337
  f"(?P<failure>{re.escape(EMOJI_FAILURE)})",
kash/embeddings/cosine.py CHANGED
@@ -1,10 +1,16 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections.abc import Sequence
2
- from typing import Any, TypeAlias
4
+ from typing import TYPE_CHECKING, Any, TypeAlias
3
5
 
4
- import numpy as np
6
+ if TYPE_CHECKING:
7
+ from numpy import ndarray
5
8
 
6
- # Type aliases for clarity
7
- ArrayLike: TypeAlias = Sequence[float] | np.ndarray[Any, Any]
9
+ # Type aliases for clarity
10
+ ArrayLike: TypeAlias = Sequence[float] | ndarray[Any, Any]
11
+ else:
12
+ # Keep numpy import lazy.
13
+ ArrayLike = Any
8
14
 
9
15
 
10
16
  def cosine(u: ArrayLike, v: ArrayLike) -> float:
@@ -12,6 +18,8 @@ def cosine(u: ArrayLike, v: ArrayLike) -> float:
12
18
  Compute the cosine distance between two 1-D arrays.
13
19
  Could use scipy.spatial.distance.cosine, but avoiding the dependency.
14
20
  """
21
+ import numpy as np
22
+
15
23
  # Convert to numpy arrays
16
24
  u_array = np.asarray(u, dtype=np.float64)
17
25
  v_array = np.asarray(v, dtype=np.float64)
@@ -3,17 +3,18 @@ from __future__ import annotations
3
3
  import ast
4
4
  from collections.abc import Iterable
5
5
  from pathlib import Path
6
- from typing import TypeAlias, cast
6
+ from typing import TYPE_CHECKING, TypeAlias, cast
7
7
 
8
- import pandas as pd
9
- from litellm import embedding
10
- from litellm.types.utils import EmbeddingResponse
11
8
  from pydantic.dataclasses import dataclass
12
9
  from strif import abbrev_list
13
10
 
14
11
  from kash.config.logger import get_logger
12
+ from kash.llm_utils.init_litellm import init_litellm
15
13
  from kash.llm_utils.llms import DEFAULT_EMBEDDING_MODEL
16
14
 
15
+ if TYPE_CHECKING:
16
+ from pandas import DataFrame
17
+
17
18
  log = get_logger(__name__)
18
19
 
19
20
 
@@ -41,11 +42,13 @@ class Embeddings:
41
42
  def as_iterable(self) -> Iterable[tuple[Key, str, list[float]]]:
42
43
  return ((key, text, emb) for key, (text, emb) in self.data.items())
43
44
 
44
- def as_df(self) -> pd.DataFrame:
45
+ def as_df(self) -> DataFrame:
46
+ from pandas import DataFrame
47
+
45
48
  keys, texts, embeddings = zip(
46
49
  *[(key, text, emb) for key, (text, emb) in self.data.items()], strict=False
47
50
  )
48
- return pd.DataFrame(
51
+ return DataFrame(
49
52
  {
50
53
  "key": keys,
51
54
  "text": texts,
@@ -61,6 +64,11 @@ class Embeddings:
61
64
 
62
65
  @classmethod
63
66
  def embed(cls, keyvals: list[KeyVal], model=DEFAULT_EMBEDDING_MODEL) -> Embeddings:
67
+ from litellm import embedding
68
+ from litellm.types.utils import EmbeddingResponse
69
+
70
+ init_litellm()
71
+
64
72
  data = {}
65
73
  log.message(
66
74
  "Embedding %d texts (model %s, batch size %s)…",
@@ -102,6 +110,8 @@ class Embeddings:
102
110
 
103
111
  @classmethod
104
112
  def read_from_csv(cls, path: Path) -> Embeddings:
113
+ import pandas as pd
114
+
105
115
  df = pd.read_csv(path)
106
116
  df["embedding"] = df["embedding"].apply(ast.literal_eval)
107
117
  data = {row["key"]: (row["text"], row["embedding"]) for _, row in df.iterrows()}
@@ -1,9 +1,8 @@
1
- from typing import cast
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, cast
2
4
 
3
- import litellm
4
5
  from funlog import log_calls
5
- from litellm import embedding
6
- from litellm.types.utils import EmbeddingResponse
7
6
 
8
7
  from kash.config.logger import get_logger
9
8
  from kash.embeddings.cosine import ArrayLike, cosine
@@ -11,6 +10,9 @@ from kash.embeddings.embeddings import Embeddings
11
10
  from kash.llm_utils.llms import DEFAULT_EMBEDDING_MODEL, EmbeddingModel
12
11
  from kash.utils.errors import ApiResultError
13
12
 
13
+ if TYPE_CHECKING:
14
+ from litellm.types.utils import EmbeddingResponse
15
+
14
16
  log = get_logger(__name__)
15
17
 
16
18
 
@@ -20,6 +22,10 @@ def cosine_relatedness(x: ArrayLike, y: ArrayLike) -> float:
20
22
 
21
23
  @log_calls(level="info", show_return_value=False)
22
24
  def embed_query(model: EmbeddingModel, query: str) -> EmbeddingResponse:
25
+ import litellm
26
+ from litellm import embedding
27
+ from litellm.types.utils import EmbeddingResponse
28
+
23
29
  try:
24
30
  response: EmbeddingResponse = cast(
25
31
  EmbeddingResponse, embedding(model=model.litellm_name, input=[query])
kash/exec/__init__.py CHANGED
@@ -12,6 +12,7 @@ from kash.exec.resolve_args import (
12
12
  resolve_locator_arg,
13
13
  resolve_path_arg,
14
14
  )
15
+ from kash.exec.runtime_settings import current_runtime_settings, kash_runtime
15
16
 
16
17
  __all__ = [
17
18
  "kash_action",
@@ -20,6 +21,8 @@ __all__ = [
20
21
  "prepare_action_input",
21
22
  "run_action_with_shell_context",
22
23
  "kash_command",
24
+ "kash_runtime",
25
+ "current_runtime_settings",
23
26
  "import_and_register",
24
27
  "llm_transform_item",
25
28
  "llm_transform_str",
@@ -21,22 +21,22 @@ from typing_extensions import override
21
21
  from kash.config.logger import get_logger
22
22
  from kash.exec.action_exec import run_action_with_caching
23
23
  from kash.exec.action_registry import register_action_class
24
+ from kash.exec.runtime_settings import current_runtime_settings
24
25
  from kash.exec_model.args_model import ONE_ARG, ArgCount, ArgType
25
26
  from kash.model.actions_model import (
26
27
  Action,
27
28
  ActionInput,
28
29
  ActionResult,
29
- ExecContext,
30
30
  LLMOptions,
31
31
  ParamSource,
32
32
  TitleTemplate,
33
33
  )
34
- from kash.model.items_model import Item, ItemType, State
34
+ from kash.model.exec_model import ExecContext
35
+ from kash.model.items_model import Item, ItemType
35
36
  from kash.model.params_model import Param, ParamDeclarations, TypedParamValues
36
37
  from kash.model.preconditions_model import Precondition
37
38
  from kash.utils.common.function_inspect import FuncParam, inspect_function_params
38
39
  from kash.utils.errors import InvalidDefinition
39
- from kash.workspaces.workspaces import current_ws
40
40
 
41
41
  log = get_logger(__name__)
42
42
 
@@ -209,13 +209,6 @@ def kash_action(
209
209
  mcp_tool: bool = False,
210
210
  title_template: TitleTemplate = TitleTemplate("{title}"),
211
211
  llm_options: LLMOptions = LLMOptions(),
212
- override_state: State | None = None,
213
- # Including these for completeness but usually don't want to set them globally
214
- # in the decorator:
215
- rerun: bool = False,
216
- refetch: bool = False,
217
- tmp_output: bool = False,
218
- no_format: bool = False,
219
212
  ) -> Callable[[AF], AF]:
220
213
  """
221
214
  A function decorator to create and register an action. The annotated function must
@@ -386,15 +379,7 @@ def kash_action(
386
379
  if provided_context:
387
380
  context = provided_context
388
381
  else:
389
- context = ExecContext(
390
- action,
391
- current_ws().base_dir,
392
- rerun=rerun,
393
- refetch=refetch,
394
- override_state=override_state,
395
- tmp_output=tmp_output,
396
- no_format=no_format,
397
- )
382
+ context = ExecContext(action, current_runtime_settings())
398
383
 
399
384
  # Run the action.
400
385
  result, _, _ = run_action_with_caching(context, action_input)
kash/exec/action_exec.py CHANGED
@@ -4,7 +4,12 @@ from dataclasses import replace
4
4
  from prettyfmt import fmt_lines
5
5
 
6
6
  from kash.config.logger import get_logger
7
- from kash.config.text_styles import EMOJI_SKIP, EMOJI_SUCCESS, EMOJI_TIMING
7
+ from kash.config.text_styles import (
8
+ EMOJI_SKIP,
9
+ EMOJI_START,
10
+ EMOJI_SUCCESS,
11
+ EMOJI_TIMING,
12
+ )
8
13
  from kash.exec.preconditions import is_url_item
9
14
  from kash.exec.resolve_args import assemble_action_args
10
15
  from kash.exec_model.args_model import CommandArg
@@ -17,15 +22,16 @@ from kash.model.actions_model import (
17
22
  ExecContext,
18
23
  PathOpType,
19
24
  )
25
+ from kash.model.exec_model import RuntimeSettings
20
26
  from kash.model.items_model import Item, State
21
27
  from kash.model.operations_model import Input, Operation, Source
22
28
  from kash.model.params_model import ALL_COMMON_PARAMS, GLOBAL_PARAMS, RawParamValues
23
29
  from kash.model.paths_model import StorePath
24
- from kash.shell.output.shell_output import PrintHooks, print_h3
30
+ from kash.shell.output.shell_output import PrintHooks
31
+ from kash.utils.common.inflection import plural
25
32
  from kash.utils.common.task_stack import task_stack
26
33
  from kash.utils.common.type_utils import not_none
27
- from kash.utils.errors import NONFATAL_EXCEPTIONS, ContentError, InvalidOutput
28
- from kash.utils.lang_utils.inflection import plural
34
+ from kash.utils.errors import ContentError, InvalidOutput, get_nonfatal_exceptions
29
35
  from kash.workspaces import Selection, current_ws
30
36
  from kash.workspaces.workspace_importing import import_and_load
31
37
 
@@ -63,6 +69,7 @@ def validate_action_input(
63
69
  """
64
70
  Validate an action input, ensuring the right number of args, all explicit params are filled,
65
71
  and the precondition holds and return an `Operation` that describes what will happen.
72
+ For flexibility, we don't require the items to be saved (have a store path).
66
73
  """
67
74
  input_items = action_input.items
68
75
  # Validations:
@@ -75,10 +82,16 @@ def validate_action_input(
75
82
 
76
83
  # Now make a note of the the operation we will perform.
77
84
  # If the inputs are paths, record the input paths, including hashes.
78
- store_paths = [StorePath(not_none(item.store_path)) for item in input_items if item.store_path]
79
- inputs = [Input(store_path, ws.hash(store_path)) for store_path in store_paths]
85
+ def input_for(item: Item) -> Input:
86
+ if item.store_path:
87
+ return Input(StorePath(item.store_path), ws.hash(StorePath(item.store_path)))
88
+ else:
89
+ return Input(path=None, source_info="unsaved")
90
+
91
+ inputs = [input_for(item) for item in input_items]
92
+
80
93
  # Add any non-default runtime options into the options summary.
81
- options = {**action.param_value_summary(), **context.runtime_options}
94
+ options = {**action.param_value_summary(), **context.settings.non_default_options}
82
95
  operation = Operation(action.name, inputs, options)
83
96
 
84
97
  return operation
@@ -89,7 +102,7 @@ def log_action(action: Action, action_input: ActionInput, operation: Operation):
89
102
  Log the action and the operation we are about to run.
90
103
  """
91
104
  PrintHooks.before_log_action_run()
92
- print_h3(f"Action `{action.name}`")
105
+ log.message("%s Action: `%s`", EMOJI_START, action.name)
93
106
  log.message("Running: `%s`", operation.command_line(with_options=True))
94
107
  if len(action.param_value_summary()) > 0:
95
108
  log.message("Parameters:\n%s", action.param_value_summary_str())
@@ -106,8 +119,9 @@ def check_for_existing_result(
106
119
  already exist.
107
120
  """
108
121
  action = context.action
109
- ws = context.workspace
110
- rerun = context.rerun
122
+ settings = context.settings
123
+ ws = settings.workspace
124
+ rerun = settings.rerun
111
125
 
112
126
  existing_result = None
113
127
 
@@ -154,6 +168,7 @@ def run_action_operation(
154
168
 
155
169
  # Run the action.
156
170
  action = context.action
171
+ settings = context.settings
157
172
  if action.run_per_item:
158
173
  result = _run_for_each_item(context, action_input)
159
174
  else:
@@ -172,9 +187,9 @@ def run_action_operation(
172
187
  item.update_history(Source(operation=this_op, output_num=i, cacheable=action.cacheable))
173
188
 
174
189
  # Override the state if appropriate (this handles marking items as transient).
175
- if context.override_state:
190
+ if settings.override_state:
176
191
  for item in result.items:
177
- item.state = context.override_state
192
+ item.state = settings.override_state
178
193
 
179
194
  log.info("Action `%s` result: %s", action.name, result)
180
195
 
@@ -233,7 +248,7 @@ def _run_for_each_item(context: ExecContext, input: ActionInput) -> ActionResult
233
248
  log.info("Caught SkipItem exception, skipping run on this item")
234
249
  result_items.append(item)
235
250
  continue
236
- except NONFATAL_EXCEPTIONS as e:
251
+ except get_nonfatal_exceptions() as e:
237
252
  errors.append(e)
238
253
  had_error = True
239
254
 
@@ -293,14 +308,18 @@ def save_action_result(
293
308
  fmt_lines(skipped_paths),
294
309
  )
295
310
 
296
- input_store_paths = [StorePath(not_none(item.store_path)) for item in input_items]
311
+ unsaved_items = [item for item in input_items if not item.store_path]
312
+ input_store_paths = [StorePath(item.store_path) for item in input_items if item.store_path]
297
313
  result_store_paths = [StorePath(item.store_path) for item in result.items if item.store_path]
298
314
  old_inputs = sorted(set(input_store_paths) - set(result_store_paths))
315
+ if unsaved_items:
316
+ log.info("unsaved_items:\n%s", fmt_lines(unsaved_items))
299
317
  log.info("result_store_paths:\n%s", fmt_lines(result_store_paths))
300
- log.info("old_inputs:\n%s", fmt_lines(old_inputs))
318
+ if old_inputs:
319
+ log.info("old_inputs:\n%s", fmt_lines(old_inputs))
301
320
 
302
321
  # If there is a hint that the action replaces the input, archive any inputs that are not in the result.
303
- archived_store_paths = []
322
+ archived_store_paths: list[StorePath] = []
304
323
  if result.replaces_input and input_items:
305
324
  for input_store_path in old_inputs:
306
325
  # Note some outputs may be missing if replace_input was used.
@@ -325,7 +344,8 @@ def run_action_with_caching(
325
344
  Note: Mutates the input but only to add `context` to each item.
326
345
  """
327
346
  action = context.action
328
- ws = context.workspace
347
+ settings = context.settings
348
+ ws = settings.workspace
329
349
 
330
350
  # For convenience, we include the context to each item too (this helps so per-item
331
351
  # functions don't have to take context args everywhere).
@@ -341,7 +361,7 @@ def run_action_with_caching(
341
361
  # Check if a previous run already produced the result.
342
362
  existing_result = check_for_existing_result(context, action_input, operation)
343
363
 
344
- if existing_result and not context.rerun:
364
+ if existing_result and not settings.rerun:
345
365
  # Use the cached result.
346
366
  result = existing_result
347
367
  result_store_paths = [StorePath(not_none(item.store_path)) for item in result.items]
@@ -349,7 +369,7 @@ def run_action_with_caching(
349
369
 
350
370
  PrintHooks.before_done_message()
351
371
  log.message(
352
- "%s Action skipped: `%s` completed with %s %s",
372
+ "%s Skipped: `%s` completed with %s %s",
353
373
  EMOJI_SKIP,
354
374
  action.name,
355
375
  len(result.items),
@@ -359,12 +379,12 @@ def run_action_with_caching(
359
379
  # Run it!
360
380
  result = run_action_operation(context, action_input, operation)
361
381
  result_store_paths, archived_store_paths = save_action_result(
362
- ws, result, action_input, as_tmp=context.tmp_output, no_format=context.no_format
382
+ ws, result, action_input, as_tmp=settings.tmp_output, no_format=settings.no_format
363
383
  )
364
384
 
365
385
  PrintHooks.before_done_message()
366
386
  log.message(
367
- "%s Action done: `%s` completed with %s %s",
387
+ "%s Done: `%s` completed with %s %s",
368
388
  EMOJI_SUCCESS,
369
389
  action.name,
370
390
  len(result.items),
@@ -415,8 +435,7 @@ def run_action_with_shell_context(
415
435
  action_name = action.name
416
436
 
417
437
  # Execution context. This is fixed for the duration of the action.
418
- context = ExecContext(
419
- action=action,
438
+ settings = RuntimeSettings(
420
439
  workspace_dir=ws.base_dir,
421
440
  rerun=rerun,
422
441
  refetch=refetch,
@@ -424,6 +443,7 @@ def run_action_with_shell_context(
424
443
  tmp_output=tmp_output,
425
444
  no_format=no_format,
426
445
  )
446
+ context = ExecContext(action, settings)
427
447
 
428
448
  # Collect args from the provided args or otherwise the current selection.
429
449
  args, from_selection = assemble_action_args(*provided_args, use_selection=action.uses_selection)
@@ -13,7 +13,7 @@ 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
15
  from kash.model.items_model import Item
16
- from kash.text_handling.doc_normalization import normalize_formatting_ansi
16
+ from kash.text_handling.doc_normalization import normalize_formatting
17
17
  from kash.utils.errors import InvalidInput
18
18
  from kash.utils.file_utils.file_formats_model import Format
19
19
 
@@ -118,7 +118,7 @@ def llm_transform_item(
118
118
  if strip_fence:
119
119
  result_str = strip_markdown_fence(result_str)
120
120
  if normalize:
121
- result_str = normalize_formatting_ansi(result_str, format=format)
121
+ result_str = normalize_formatting(result_str, format=format)
122
122
 
123
123
  result_item.body = result_str
124
124
  return result_item
@@ -85,13 +85,13 @@ def contains_curly_vars(item: Item) -> bool:
85
85
 
86
86
 
87
87
  @kash_precondition
88
- def has_text_body(item: Item) -> bool:
89
- return has_body(item) and item.format in (Format.plaintext, Format.markdown, Format.md_html)
88
+ def has_simple_text_body(item: Item) -> bool:
89
+ return bool(has_body(item) and item.format and item.format.is_simple_text)
90
90
 
91
91
 
92
92
  @kash_precondition
93
93
  def has_html_body(item: Item) -> bool:
94
- return has_body(item) and item.format in (Format.html, Format.md_html)
94
+ return bool(has_body(item) and item.format and item.format.is_html)
95
95
 
96
96
 
97
97
  @kash_precondition
@@ -106,7 +106,7 @@ def is_plaintext(item: Item) -> bool:
106
106
 
107
107
  @kash_precondition
108
108
  def is_markdown(item: Item) -> bool:
109
- return has_body(item) and item.format in (Format.markdown, Format.md_html)
109
+ return bool(has_body(item) and item.format and item.format.is_markdown)
110
110
 
111
111
 
112
112
  @kash_precondition
@@ -119,14 +119,6 @@ def is_html(item: Item) -> bool:
119
119
  return has_body(item) and item.format == Format.html
120
120
 
121
121
 
122
- @kash_precondition
123
- def is_text_doc(item: Item) -> bool:
124
- """
125
- A document that can be processed by LLMs and other plaintext tools.
126
- """
127
- return (is_plaintext(item) or is_markdown(item)) and has_body(item)
128
-
129
-
130
122
  @kash_precondition
131
123
  def is_markdown_list(item: Item) -> bool:
132
124
  try: