kash-shell 0.3.9__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.
Files changed (151) hide show
  1. kash/actions/__init__.py +4 -4
  2. kash/actions/core/format_markdown_template.py +2 -5
  3. kash/actions/core/markdownify.py +7 -6
  4. kash/actions/core/readability.py +7 -6
  5. kash/actions/core/render_as_html.py +37 -0
  6. kash/actions/core/show_webpage.py +6 -11
  7. kash/actions/core/strip_html.py +2 -6
  8. kash/actions/core/tabbed_webpage_config.py +31 -0
  9. kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
  10. kash/commands/__init__.py +8 -20
  11. kash/commands/base/basic_file_commands.py +15 -0
  12. kash/commands/base/debug_commands.py +13 -0
  13. kash/commands/base/files_command.py +28 -10
  14. kash/commands/base/general_commands.py +21 -16
  15. kash/commands/base/logs_commands.py +4 -2
  16. kash/commands/base/model_commands.py +8 -8
  17. kash/commands/base/search_command.py +3 -2
  18. kash/commands/base/show_command.py +5 -3
  19. kash/commands/extras/parse_uv_lock.py +186 -0
  20. kash/commands/help/doc_commands.py +2 -31
  21. kash/commands/help/welcome.py +33 -0
  22. kash/commands/workspace/selection_commands.py +11 -6
  23. kash/commands/workspace/workspace_commands.py +19 -17
  24. kash/config/colors.py +3 -1
  25. kash/config/env_settings.py +14 -1
  26. kash/config/init.py +2 -2
  27. kash/config/logger.py +59 -56
  28. kash/config/logger_basic.py +3 -3
  29. kash/config/settings.py +116 -57
  30. kash/config/setup.py +28 -12
  31. kash/config/text_styles.py +3 -13
  32. kash/docs/load_api_docs.py +2 -1
  33. kash/docs/markdown/topics/a3_getting_started.md +3 -2
  34. kash/{concepts → embeddings}/text_similarity.py +2 -2
  35. kash/exec/__init__.py +20 -3
  36. kash/exec/action_decorators.py +24 -10
  37. kash/exec/action_exec.py +41 -23
  38. kash/exec/action_registry.py +13 -48
  39. kash/exec/command_registry.py +2 -1
  40. kash/exec/fetch_url_metadata.py +4 -6
  41. kash/exec/importing.py +56 -0
  42. kash/exec/llm_transforms.py +12 -10
  43. kash/exec/precondition_registry.py +2 -1
  44. kash/exec/preconditions.py +22 -1
  45. kash/exec/resolve_args.py +4 -0
  46. kash/exec/shell_callable_action.py +33 -19
  47. kash/file_storage/file_store.py +42 -27
  48. kash/file_storage/item_file_format.py +5 -2
  49. kash/file_storage/metadata_dirs.py +11 -2
  50. kash/help/assistant.py +1 -1
  51. kash/help/assistant_instructions.py +2 -1
  52. kash/help/function_param_info.py +1 -1
  53. kash/help/help_embeddings.py +2 -2
  54. kash/help/help_printing.py +7 -11
  55. kash/llm_utils/clean_headings.py +1 -1
  56. kash/llm_utils/llm_api_keys.py +4 -4
  57. kash/llm_utils/llm_features.py +68 -0
  58. kash/llm_utils/llm_messages.py +1 -2
  59. kash/llm_utils/llm_names.py +1 -1
  60. kash/llm_utils/llms.py +8 -3
  61. kash/local_server/__init__.py +5 -2
  62. kash/local_server/local_server.py +8 -5
  63. kash/local_server/local_server_commands.py +2 -2
  64. kash/local_server/local_server_routes.py +1 -7
  65. kash/local_server/local_url_formatters.py +1 -1
  66. kash/mcp/__init__.py +5 -2
  67. kash/mcp/mcp_cli.py +5 -5
  68. kash/mcp/mcp_server_commands.py +5 -5
  69. kash/mcp/mcp_server_routes.py +5 -5
  70. kash/mcp/mcp_server_sse.py +4 -2
  71. kash/media_base/media_cache.py +8 -8
  72. kash/media_base/media_services.py +1 -1
  73. kash/media_base/media_tools.py +6 -6
  74. kash/media_base/services/local_file_media.py +2 -2
  75. kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -110
  76. kash/media_base/transcription_format.py +73 -0
  77. kash/media_base/transcription_whisper.py +38 -0
  78. kash/model/__init__.py +73 -5
  79. kash/model/actions_model.py +38 -4
  80. kash/model/concept_model.py +30 -0
  81. kash/model/items_model.py +115 -32
  82. kash/model/params_model.py +24 -0
  83. kash/shell/completions/completion_scoring.py +37 -5
  84. kash/shell/output/kerm_codes.py +1 -2
  85. kash/shell/output/shell_formatting.py +14 -4
  86. kash/shell/shell_main.py +2 -2
  87. kash/shell/utils/exception_printing.py +6 -0
  88. kash/shell/utils/native_utils.py +26 -20
  89. kash/shell/utils/shell_function_wrapper.py +15 -15
  90. kash/text_handling/custom_sliding_transforms.py +12 -4
  91. kash/text_handling/doc_normalization.py +6 -2
  92. kash/text_handling/markdown_render.py +118 -0
  93. kash/text_handling/markdown_utils.py +226 -0
  94. kash/utils/common/function_inspect.py +360 -110
  95. kash/utils/common/import_utils.py +12 -3
  96. kash/utils/common/type_utils.py +0 -29
  97. kash/utils/common/url.py +27 -3
  98. kash/utils/errors.py +6 -0
  99. kash/utils/file_utils/file_ext.py +4 -0
  100. kash/utils/file_utils/file_formats.py +2 -2
  101. kash/utils/file_utils/file_formats_model.py +20 -1
  102. kash/web_content/dir_store.py +1 -2
  103. kash/web_content/file_cache_utils.py +37 -10
  104. kash/web_content/file_processing.py +68 -0
  105. kash/web_content/local_file_cache.py +12 -9
  106. kash/web_content/web_extract.py +8 -3
  107. kash/web_content/web_fetch.py +12 -4
  108. kash/web_gen/__init__.py +0 -4
  109. kash/web_gen/simple_webpage.py +52 -0
  110. kash/web_gen/tabbed_webpage.py +24 -14
  111. kash/web_gen/template_render.py +37 -2
  112. kash/web_gen/templates/base_styles.css.jinja +169 -43
  113. kash/web_gen/templates/base_webpage.html.jinja +110 -45
  114. kash/web_gen/templates/content_styles.css.jinja +4 -2
  115. kash/web_gen/templates/item_view.html.jinja +49 -39
  116. kash/web_gen/templates/simple_webpage.html.jinja +24 -0
  117. kash/web_gen/templates/tabbed_webpage.html.jinja +42 -33
  118. kash/workspaces/__init__.py +15 -2
  119. kash/workspaces/selections.py +18 -3
  120. kash/workspaces/source_items.py +0 -1
  121. kash/workspaces/workspaces.py +5 -11
  122. kash/xonsh_custom/command_nl_utils.py +40 -19
  123. kash/xonsh_custom/custom_shell.py +43 -11
  124. kash/xonsh_custom/customize_prompt.py +39 -21
  125. kash/xonsh_custom/load_into_xonsh.py +22 -25
  126. kash/xonsh_custom/shell_load_commands.py +2 -2
  127. kash/xonsh_custom/xonsh_completers.py +2 -249
  128. kash/xonsh_custom/xonsh_keybindings.py +282 -0
  129. kash/xonsh_custom/xonsh_modern_tools.py +3 -3
  130. kash/xontrib/kash_extension.py +5 -6
  131. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/METADATA +10 -8
  132. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/RECORD +137 -136
  133. kash/actions/core/webpage_config.py +0 -21
  134. kash/concepts/concept_formats.py +0 -23
  135. kash/shell/clideps/api_keys.py +0 -100
  136. kash/shell/clideps/dotenv_setup.py +0 -115
  137. kash/shell/clideps/dotenv_utils.py +0 -98
  138. kash/shell/clideps/pkg_deps.py +0 -257
  139. kash/shell/clideps/platforms.py +0 -11
  140. kash/shell/clideps/terminal_features.py +0 -56
  141. kash/shell/utils/osc_utils.py +0 -95
  142. kash/shell/utils/terminal_images.py +0 -133
  143. kash/text_handling/markdown_util.py +0 -167
  144. kash/utils/common/atomic_var.py +0 -171
  145. kash/utils/common/string_replace.py +0 -93
  146. kash/utils/common/string_template.py +0 -101
  147. /kash/{concepts → embeddings}/cosine.py +0 -0
  148. /kash/{concepts → embeddings}/embeddings.py +0 -0
  149. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/WHEEL +0 -0
  150. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/entry_points.txt +0 -0
  151. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -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.type or str,
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
  )
@@ -210,7 +210,12 @@ def kash_action(
210
210
  title_template: TitleTemplate = TitleTemplate("{title}"),
211
211
  llm_options: LLMOptions = LLMOptions(),
212
212
  override_state: State | None = None,
213
+ # Including these for completeness but usually don't want to set them globally
214
+ # in the decorator:
213
215
  rerun: bool = False,
216
+ refetch: bool = False,
217
+ tmp_output: bool = False,
218
+ no_format: bool = False,
214
219
  ) -> Callable[[AF], AF]:
215
220
  """
216
221
  A function decorator to create and register an action. The annotated function must
@@ -243,13 +248,13 @@ def kash_action(
243
248
 
244
249
  # Inspect and sanity check the formal params.
245
250
  func_params = inspect_function_params(orig_func)
246
- if len(func_params) == 0 or func_params[0].type not in (ActionInput, Item):
251
+ if len(func_params) == 0 or func_params[0].effective_type not in (ActionInput, Item):
247
252
  raise InvalidDefinition(
248
253
  f"Decorator `@kash_action` requires exactly one positional parameter, "
249
254
  f"`input` of type `ActionInput` or `Item` on function `{orig_func.__name__}` but "
250
255
  f"got params: {func_params}"
251
256
  )
252
- if any(fp.is_positional for fp in func_params[1:]):
257
+ if any(fp.is_pure_positional for fp in func_params[1:]):
253
258
  raise InvalidDefinition(
254
259
  "Decorator `@kash_action` requires all parameters after the first positional "
255
260
  f"parameter to be keyword parameters on function `{orig_func.__name__}` but "
@@ -260,7 +265,7 @@ def kash_action(
260
265
  context_param = next((fp for fp in func_params if fp.name == "context"), None)
261
266
  if context_param:
262
267
  func_params.remove(context_param)
263
- if context_param and context_param.is_positional:
268
+ if context_param and context_param.is_pure_positional:
264
269
  raise InvalidDefinition(
265
270
  "Decorator `@kash_action` requires the `context` parameter to be a keyword "
266
271
  "parameter, not positional, on function `{func.__name__}`"
@@ -268,7 +273,7 @@ def kash_action(
268
273
 
269
274
  # If the original function is a simple action function (processes a single item),
270
275
  # wrap it to convert to an ActionFunction.
271
- is_simple_func = func_params[0].type == Item
276
+ is_simple_func = func_params[0].effective_type == Item
272
277
  action_func: ActionFunction
273
278
  if is_simple_func:
274
279
  simple_func = cast(SimpleActionFunction, orig_func)
@@ -328,13 +333,16 @@ def kash_action(
328
333
  if context_param:
329
334
  kw_args["context"] = context
330
335
  for fp in func_params[1:]:
331
- if fp.is_positional:
336
+ if fp.is_pure_positional:
332
337
  pos_args.append(self.get_param(fp.name))
333
338
  else:
334
339
  kw_args[fp.name] = self.get_param(fp.name)
335
340
 
336
- log.info("Action function param declarations:\n%s", fmt_lines(self.params))
337
- log.info("Action function param values:\n%s", self.param_value_summary_str())
341
+ if self.params:
342
+ log.info("Action function param declarations:\n%s", fmt_lines(self.params))
343
+ log.info("Action function param values:\n%s", self.param_value_summary_str())
344
+ else:
345
+ log.info("Action function has no declared params")
338
346
 
339
347
  log.message(
340
348
  "Action function call:\n%s",
@@ -379,11 +387,17 @@ def kash_action(
379
387
  context = provided_context
380
388
  else:
381
389
  context = ExecContext(
382
- action, current_ws().base_dir, rerun=rerun, override_state=override_state
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,
383
397
  )
384
398
 
385
399
  # Run the action.
386
- result, _, _ = run_action_with_caching(context, action_input, rerun=rerun)
400
+ result, _, _ = run_action_with_caching(context, action_input)
387
401
 
388
402
  return result
389
403
 
kash/exec/action_exec.py CHANGED
@@ -32,7 +32,7 @@ from kash.workspaces.workspace_importing import import_and_load
32
32
  log = get_logger(__name__)
33
33
 
34
34
 
35
- def prepare_action_input(*input_args: CommandArg) -> ActionInput:
35
+ def prepare_action_input(*input_args: CommandArg, refetch: bool = False) -> ActionInput:
36
36
  """
37
37
  Prepare input args, which may be URLs or paths, into items that correspond to
38
38
  URL or file resources, either finding them in the workspace or importing them.
@@ -50,13 +50,16 @@ def prepare_action_input(*input_args: CommandArg) -> ActionInput:
50
50
  if input_items:
51
51
  log.message("Assembling metadata for input items:\n%s", fmt_lines(input_items))
52
52
  input_items = [
53
- fetch_url_item_metadata(item) if is_url_item(item) else item for item in input_items
53
+ fetch_url_item_metadata(item, refetch=refetch) if is_url_item(item) else item
54
+ for item in input_items
54
55
  ]
55
56
 
56
57
  return ActionInput(input_items)
57
58
 
58
59
 
59
- def validate_action_input(ws: FileStore, action: Action, action_input: ActionInput) -> Operation:
60
+ def validate_action_input(
61
+ context: ExecContext, ws: FileStore, action: Action, action_input: ActionInput
62
+ ) -> Operation:
60
63
  """
61
64
  Validate an action input, ensuring the right number of args, all explicit params are filled,
62
65
  and the precondition holds and return an `Operation` that describes what will happen.
@@ -74,7 +77,9 @@ def validate_action_input(ws: FileStore, action: Action, action_input: ActionInp
74
77
  # If the inputs are paths, record the input paths, including hashes.
75
78
  store_paths = [StorePath(not_none(item.store_path)) for item in input_items if item.store_path]
76
79
  inputs = [Input(store_path, ws.hash(store_path)) for store_path in store_paths]
77
- operation = Operation(action.name, inputs, action.param_value_summary())
80
+ # Add any non-default runtime options into the options summary.
81
+ options = {**action.param_value_summary(), **context.runtime_options}
82
+ operation = Operation(action.name, inputs, options)
78
83
 
79
84
  return operation
80
85
 
@@ -93,10 +98,7 @@ def log_action(action: Action, action_input: ActionInput, operation: Operation):
93
98
 
94
99
 
95
100
  def check_for_existing_result(
96
- context: ExecContext,
97
- action_input: ActionInput,
98
- operation: Operation,
99
- rerun: bool = False,
101
+ context: ExecContext, action_input: ActionInput, operation: Operation
100
102
  ) -> ActionResult | None:
101
103
  """
102
104
  Check if we already have the results for this operation (same action and inputs)
@@ -105,7 +107,7 @@ def check_for_existing_result(
105
107
  """
106
108
  action = context.action
107
109
  ws = context.workspace
108
- rerun = context.rerun or rerun
110
+ rerun = context.rerun
109
111
 
110
112
  existing_result = None
111
113
 
@@ -261,7 +263,12 @@ def _run_for_each_item(context: ExecContext, input: ActionInput) -> ActionResult
261
263
 
262
264
 
263
265
  def save_action_result(
264
- ws: FileStore, result: ActionResult, action_input: ActionInput
266
+ ws: FileStore,
267
+ result: ActionResult,
268
+ action_input: ActionInput,
269
+ *,
270
+ as_tmp: bool = False,
271
+ no_format: bool = False,
265
272
  ) -> tuple[list[StorePath], list[StorePath]]:
266
273
  """
267
274
  Save the result of an action to the workspace. Handles skipping duplicates and
@@ -277,7 +284,7 @@ def save_action_result(
277
284
  skipped_paths.append(store_path)
278
285
  continue
279
286
 
280
- ws.save(item)
287
+ ws.save(item, as_tmp=as_tmp, no_format=no_format)
281
288
 
282
289
  if skipped_paths:
283
290
  log.message(
@@ -308,9 +315,7 @@ def save_action_result(
308
315
 
309
316
 
310
317
  def run_action_with_caching(
311
- context: ExecContext,
312
- action_input: ActionInput,
313
- rerun: bool = False,
318
+ context: ExecContext, action_input: ActionInput
314
319
  ) -> tuple[ActionResult, list[StorePath], list[StorePath]]:
315
320
  """
316
321
  Run an action, including validation, only rerunning if `rerun` requested or
@@ -328,15 +333,15 @@ def run_action_with_caching(
328
333
  item.context = context
329
334
 
330
335
  # Assemble the operation and validate the action input.
331
- operation = validate_action_input(ws, action, action_input)
336
+ operation = validate_action_input(context, ws, action, action_input)
332
337
 
333
338
  # Log what we're about to run.
334
339
  log_action(action, action_input, operation)
335
340
 
336
341
  # Check if a previous run already produced the result.
337
- existing_result = check_for_existing_result(context, action_input, operation, rerun=rerun)
342
+ existing_result = check_for_existing_result(context, action_input, operation)
338
343
 
339
- if existing_result and not rerun:
344
+ if existing_result and not context.rerun:
340
345
  # Use the cached result.
341
346
  result = existing_result
342
347
  result_store_paths = [StorePath(not_none(item.store_path)) for item in result.items]
@@ -353,7 +358,9 @@ def run_action_with_caching(
353
358
  else:
354
359
  # Run it!
355
360
  result = run_action_operation(context, action_input, operation)
356
- result_store_paths, archived_store_paths = save_action_result(ws, result, action_input)
361
+ result_store_paths, archived_store_paths = save_action_result(
362
+ ws, result, action_input, as_tmp=context.tmp_output, no_format=context.no_format
363
+ )
357
364
 
358
365
  PrintHooks.before_done_message()
359
366
  log.message(
@@ -371,9 +378,12 @@ def run_action_with_shell_context(
371
378
  action_spec: str | type[Action],
372
379
  explicit_param_values: RawParamValues,
373
380
  *provided_args: str,
374
- rerun=False,
381
+ rerun: bool = False,
382
+ refetch: bool = False,
375
383
  override_state: State | None = None,
376
- internal_call=False,
384
+ tmp_output: bool = False,
385
+ no_format: bool = False,
386
+ internal_call: bool = False,
377
387
  ) -> ActionResult:
378
388
  """
379
389
  Main function to run an action from the shell. Wraps `run_action_if_needed` to
@@ -404,8 +414,16 @@ def run_action_with_shell_context(
404
414
  action = action_cls.create(explicit_parsed, ws_parsed)
405
415
  action_name = action.name
406
416
 
407
- # Execution context.
408
- context = ExecContext(action, ws.base_dir, rerun, override_state)
417
+ # Execution context. This is fixed for the duration of the action.
418
+ context = ExecContext(
419
+ action=action,
420
+ workspace_dir=ws.base_dir,
421
+ rerun=rerun,
422
+ refetch=refetch,
423
+ override_state=override_state,
424
+ tmp_output=tmp_output,
425
+ no_format=no_format,
426
+ )
409
427
 
410
428
  # Collect args from the provided args or otherwise the current selection.
411
429
  args, from_selection = assemble_action_args(*provided_args, use_selection=action.uses_selection)
@@ -422,7 +440,7 @@ def run_action_with_shell_context(
422
440
  )
423
441
 
424
442
  # Get items for each input arg.
425
- input = prepare_action_input(*args)
443
+ input = prepare_action_input(*args, refetch=refetch)
426
444
 
427
445
  # Finally, run the action.
428
446
  result, result_store_paths, archived_store_paths = run_action_with_caching(context, input)
@@ -1,18 +1,14 @@
1
- from pathlib import Path
2
-
3
1
  from cachetools import Cache, cached
4
- from prettyfmt import fmt_lines, fmt_path
2
+ from strif import AtomicVar
5
3
 
6
4
  from kash.config.logger import get_logger
7
5
  from kash.model.actions_model import Action
8
- from kash.utils.common.atomic_var import AtomicVar
9
- from kash.utils.common.import_utils import Tallies, import_subdirs
10
6
  from kash.utils.errors import InvalidInput
11
7
 
12
8
  log = get_logger(__name__)
13
9
 
14
10
  # Global registry of action classes.
15
- _action_classes: AtomicVar[dict[str, type[Action]]] = AtomicVar({})
11
+ action_classes: AtomicVar[dict[str, type[Action]]] = AtomicVar({})
16
12
 
17
13
 
18
14
  # Want it fast to get the full list of actions (important for tab completions
@@ -30,64 +26,29 @@ def register_action_class(cls: type[Action]):
30
26
  """
31
27
  Register an action class.
32
28
  """
33
- with _action_classes.updates() as action_classes:
34
- if cls.name in action_classes:
29
+ with action_classes.updates() as ac:
30
+ if cls.name in ac:
35
31
  log.warning(
36
32
  "Duplicate action name (defined twice by accident?): %s (%s)",
37
33
  cls.name,
38
34
  cls,
39
35
  )
40
- action_classes[cls.name] = cls
36
+ ac[cls.name] = cls
41
37
 
42
38
  clear_action_cache()
43
39
 
44
40
 
45
- def import_action_subdirs(
46
- subdirs: list[str],
47
- package_name: str | None,
48
- parent_dir: Path,
49
- tallies: Tallies | None = None,
50
- ):
51
- """
52
- Hook to call from `__init__.py` in a directory containing actions,
53
- so that they are auto-registered on import.
54
-
55
- Usage:
56
- ```
57
- import_action_subdirs(["subdir_name"], __package__, Path(__file__).parent)
58
- ```
59
- """
60
- if tallies is None:
61
- tallies = {}
62
- with _action_classes.updates() as action_classes:
63
- prev_count = len(action_classes)
64
-
65
- if not package_name:
66
- raise ValueError(f"Package name missing importing actions: {fmt_path(parent_dir)}")
67
-
68
- import_subdirs(package_name, parent_dir, subdirs, tallies)
69
- reload_all_action_classes()
70
-
71
- log.info(
72
- "Loaded actions: %s new actions in %s directories below %s:\n%s",
73
- len(action_classes) - prev_count,
74
- len(tallies),
75
- fmt_path(parent_dir),
76
- fmt_lines(f"{k}: {v} files" for k, v in tallies.items()),
77
- )
78
-
79
-
80
41
  @cached(_action_classes_cache)
81
42
  def get_all_action_classes() -> dict[str, type[Action]]:
82
43
  # Be sure actions are imported.
83
44
  import kash.actions # noqa: F401
84
45
 
85
46
  # Returns a copy for safety.
86
- action_classes = _action_classes.copy()
87
- if len(action_classes) == 0:
47
+ ac = action_classes.copy()
48
+ if len(ac) == 0:
88
49
  log.error("No actions found! Was there an import error?")
89
50
 
90
- return dict(action_classes)
51
+ return dict(ac)
91
52
 
92
53
 
93
54
  def look_up_action_class(action_name: str) -> type[Action]:
@@ -97,7 +58,11 @@ def look_up_action_class(action_name: str) -> type[Action]:
97
58
  return actions[action_name]
98
59
 
99
60
 
100
- def reload_all_action_classes() -> dict[str, type[Action]]:
61
+ def refresh_action_classes() -> dict[str, type[Action]]:
62
+ """
63
+ Reload all action classes, refreshing the cache. Call after registering
64
+ new action classes.
65
+ """
101
66
  clear_action_cache()
102
67
  return get_all_action_classes()
103
68
 
@@ -1,9 +1,10 @@
1
1
  from collections.abc import Callable
2
2
  from typing import overload
3
3
 
4
+ from strif import AtomicVar
5
+
4
6
  from kash.config.logger import get_logger
5
7
  from kash.exec_model.shell_model import ShellResult
6
- from kash.utils.common.atomic_var import AtomicVar
7
8
  from kash.utils.errors import InvalidInput
8
9
 
9
10
  log = get_logger(__name__)
@@ -13,9 +13,7 @@ from kash.workspaces import current_ws
13
13
  log = get_logger(__name__)
14
14
 
15
15
 
16
- def fetch_url_metadata(
17
- locator: Url | StorePath, use_cache: bool = True, refetch: bool = False
18
- ) -> Item:
16
+ def fetch_url_metadata(locator: Url | StorePath, refetch: bool = False) -> Item:
19
17
  ws = current_ws()
20
18
  if is_url(locator):
21
19
  # Import or find URL as a resource in the current workspace.
@@ -28,10 +26,10 @@ def fetch_url_metadata(
28
26
  else:
29
27
  raise InvalidInput(f"Not a URL or URL resource: {fmt_loc(locator)}")
30
28
 
31
- return fetch_url_item_metadata(item, use_cache=use_cache, refetch=refetch)
29
+ return fetch_url_item_metadata(item, refetch=refetch)
32
30
 
33
31
 
34
- def fetch_url_item_metadata(item: Item, use_cache: bool = True, refetch: bool = False) -> Item:
32
+ def fetch_url_item_metadata(item: Item, refetch: bool = False) -> Item:
35
33
  """
36
34
  Fetch metadata for a URL using a media service if we recognize the URL,
37
35
  and otherwise fetching and extracting it from the web page HTML.
@@ -56,7 +54,7 @@ def fetch_url_item_metadata(item: Item, use_cache: bool = True, refetch: bool =
56
54
  fetched_item = Item.from_media_metadata(media_metadata)
57
55
  fetched_item = item.merged_copy(fetched_item)
58
56
  else:
59
- page_data = fetch_extract(url, use_cache=use_cache)
57
+ page_data = fetch_extract(url, refetch=refetch)
60
58
  fetched_item = item.new_copy_with(
61
59
  title=page_data.title or item.title,
62
60
  description=page_data.description or item.description,
kash/exec/importing.py ADDED
@@ -0,0 +1,56 @@
1
+ from pathlib import Path
2
+
3
+ from prettyfmt import fmt_lines, fmt_path
4
+
5
+ from kash.config.logger import get_logger
6
+ from kash.exec.action_registry import action_classes, refresh_action_classes
7
+ from kash.exec.command_registry import get_all_commands
8
+ from kash.utils.common.import_utils import Tallies, import_subdirs
9
+
10
+ log = get_logger(__name__)
11
+
12
+
13
+ def import_and_register(
14
+ package_name: str | None,
15
+ parent_dir: Path,
16
+ subdir_names: list[str] | None = None,
17
+ tallies: Tallies | None = None,
18
+ ):
19
+ """
20
+ This hook can be used for auto-registering commands and actions from any
21
+ subdirectory of a given package.
22
+
23
+ Useful to call from `__init__.py` files to import a directory of code,
24
+ auto-registering annotated commands and actions and also handles refreshing the
25
+ action cache if new actions are registered.
26
+
27
+ Usage:
28
+ ```
29
+ import_and_register(["subdir1", "subdir2"], __package__, Path(__file__).parent)
30
+ ```
31
+ """
32
+ if not package_name:
33
+ raise ValueError(f"Package name missing importing actions: {fmt_path(parent_dir)}")
34
+ if tallies is None:
35
+ tallies = {}
36
+
37
+ with action_classes.updates() as ac:
38
+ prev_command_count = len(get_all_commands())
39
+ prev_action_count = len(ac)
40
+
41
+ import_subdirs(package_name, parent_dir, subdir_names, tallies)
42
+
43
+ new_command_count = len(get_all_commands()) - prev_command_count
44
+ new_action_count = len(ac) - prev_action_count
45
+
46
+ if new_action_count > 0:
47
+ refresh_action_classes()
48
+
49
+ log.info(
50
+ "Loaded %s new commands and %s new actions in %s directories below %s:\n%s",
51
+ new_command_count,
52
+ new_action_count,
53
+ len(tallies),
54
+ fmt_path(parent_dir),
55
+ fmt_lines(f"{k}: {v} files" for k, v in tallies.items()),
56
+ )
@@ -1,18 +1,19 @@
1
1
  from dataclasses import replace
2
2
 
3
3
  from chopdiff.docs import DiffFilter, TextDoc
4
- from chopdiff.transforms import WindowSettings, accept_all, filtered_transform
4
+ from chopdiff.transforms import WindowSettings, filtered_transform
5
+ from clideps.env_vars.dotenv_utils import load_dotenv_paths
5
6
  from flowmark import fill_markdown
6
7
 
7
8
  from kash.config.logger import get_logger
8
- from kash.config.settings import get_system_config_dir
9
+ from kash.config.settings import global_settings
9
10
  from kash.llm_utils import LLMName
10
11
  from kash.llm_utils.fuzzy_parsing import strip_markdown_fence
11
12
  from kash.llm_utils.llm_completion import llm_template_completion
12
13
  from kash.llm_utils.llm_messages import Message, MessageTemplate
13
14
  from kash.model.actions_model import LLMOptions
14
- from kash.model.items_model import Item, ItemType
15
- from kash.shell.clideps.dotenv_utils import load_dotenv_paths
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
 
@@ -25,7 +26,7 @@ def windowed_llm_transform(
25
26
  template: MessageTemplate,
26
27
  input: str,
27
28
  windowing: WindowSettings | None,
28
- diff_filter: DiffFilter,
29
+ diff_filter: DiffFilter | None = None,
29
30
  check_no_results: bool = True,
30
31
  ) -> TextDoc:
31
32
  def doc_transform(input_doc: TextDoc) -> TextDoc:
@@ -48,7 +49,7 @@ def windowed_llm_transform(
48
49
 
49
50
 
50
51
  def llm_transform_str(options: LLMOptions, input_str: str, check_no_results: bool = True) -> str:
51
- load_dotenv_paths(True, True, get_system_config_dir())
52
+ load_dotenv_paths(True, True, global_settings().system_config_dir)
52
53
 
53
54
  if options.windowing and options.windowing.size:
54
55
  log.message(
@@ -57,7 +58,6 @@ def llm_transform_str(options: LLMOptions, input_str: str, check_no_results: boo
57
58
  options.op_name,
58
59
  options.windowing,
59
60
  )
60
- diff_filter = options.diff_filter or accept_all
61
61
 
62
62
  result_str = windowed_llm_transform(
63
63
  options.model,
@@ -65,7 +65,7 @@ def llm_transform_str(options: LLMOptions, input_str: str, check_no_results: boo
65
65
  options.body_template,
66
66
  input_str,
67
67
  options.windowing,
68
- diff_filter,
68
+ diff_filter=options.diff_filter,
69
69
  ).reassemble()
70
70
  else:
71
71
  log.message(
@@ -91,6 +91,7 @@ def llm_transform_item(
91
91
  normalize: bool = True,
92
92
  strip_fence: bool = True,
93
93
  check_no_results: bool = True,
94
+ format: Format | None = None,
94
95
  ) -> Item:
95
96
  """
96
97
  Main function for running an LLM action on an item.
@@ -111,12 +112,13 @@ def llm_transform_item(
111
112
  log.message("LLM transform from action `%s` on item: %s", action.name, item)
112
113
  log.message("LLM options: %s", action.llm_options)
113
114
 
114
- result_item = item.derived_copy(type=ItemType.doc, body=None, format=Format.markdown)
115
+ format = format or item.format or Format.markdown
116
+ result_item = item.derived_copy(body=None, format=format)
115
117
  result_str = llm_transform_str(llm_options, item.body, check_no_results=check_no_results)
116
118
  if strip_fence:
117
119
  result_str = strip_markdown_fence(result_str)
118
120
  if normalize:
119
- result_str = fill_markdown(result_str)
121
+ result_str = normalize_formatting_ansi(result_str, format=format)
120
122
 
121
123
  result_item.body = result_str
122
124
  return result_item
@@ -1,9 +1,10 @@
1
1
  from collections.abc import Callable
2
2
 
3
+ from strif import AtomicVar
4
+
3
5
  from kash.config.logger import get_logger
4
6
  from kash.model.items_model import Item
5
7
  from kash.model.preconditions_model import Precondition
6
- from kash.utils.common.atomic_var import AtomicVar
7
8
 
8
9
  log = get_logger(__name__)
9
10
 
@@ -7,7 +7,8 @@ from chopdiff.html import has_timestamp
7
7
 
8
8
  from kash.exec.precondition_registry import kash_precondition
9
9
  from kash.model.items_model import Item, ItemType
10
- from kash.text_handling.markdown_util import extract_bullet_points
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
 
@@ -16,6 +17,21 @@ def is_resource(item: Item) -> bool:
16
17
  return item.type == ItemType.resource
17
18
 
18
19
 
20
+ @kash_precondition
21
+ def is_doc_resource(item: Item) -> bool:
22
+ return bool(is_resource(item) and item.format and item.format.is_doc)
23
+
24
+
25
+ @kash_precondition
26
+ def is_html_resource(item: Item) -> bool:
27
+ return bool(is_resource(item) and item.format and item.format == Format.html)
28
+
29
+
30
+ @kash_precondition
31
+ def is_docx_resource(item: Item) -> bool:
32
+ return bool(is_resource(item) and item.format and item.format == Format.docx)
33
+
34
+
19
35
  @kash_precondition
20
36
  def is_concept(item: Item) -> bool:
21
37
  return item.type == ItemType.concept
@@ -78,6 +94,11 @@ def has_html_body(item: Item) -> bool:
78
94
  return has_body(item) and item.format in (Format.html, Format.md_html)
79
95
 
80
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
+
81
102
  @kash_precondition
82
103
  def is_plaintext(item: Item) -> bool:
83
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