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
@@ -1,7 +1,7 @@
1
1
  from kash.actions.core.tabbed_webpage_config import tabbed_webpage_config
2
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_full_html_page_body, has_text_body, is_html
4
+ from kash.exec.preconditions import has_full_html_page_body, has_html_body, has_simple_text_body
5
5
  from kash.exec_model.args_model import ONE_OR_MORE_ARGS
6
6
  from kash.model import ActionInput, ActionResult, Param
7
7
  from kash.model.items_model import ItemType
@@ -11,7 +11,7 @@ from kash.web_gen.simple_webpage import simple_webpage_render
11
11
 
12
12
  @kash_action(
13
13
  expected_args=ONE_OR_MORE_ARGS,
14
- precondition=(is_html | has_text_body) & ~has_full_html_page_body,
14
+ precondition=(has_html_body | has_simple_text_body) & ~has_full_html_page_body,
15
15
  params=(Param("add_title", "Add a title to the page body.", type=bool),),
16
16
  )
17
17
  def render_as_html(input: ActionInput, add_title: bool = False) -> ActionResult:
@@ -1,7 +1,7 @@
1
1
  from kash.actions.core.render_as_html import render_as_html
2
2
  from kash.commands.base.show_command import show
3
3
  from kash.exec import kash_action
4
- from kash.exec.preconditions import has_full_html_page_body, has_text_body, is_html
4
+ from kash.exec.preconditions import has_full_html_page_body, has_html_body, has_simple_text_body
5
5
  from kash.exec_model.args_model import ONE_OR_MORE_ARGS
6
6
  from kash.exec_model.commands_model import Command
7
7
  from kash.exec_model.shell_model import ShellResult
@@ -10,7 +10,7 @@ from kash.model import ActionInput, ActionResult
10
10
 
11
11
  @kash_action(
12
12
  expected_args=ONE_OR_MORE_ARGS,
13
- precondition=(is_html | has_text_body) & ~has_full_html_page_body,
13
+ precondition=(has_html_body | has_simple_text_body) & ~has_full_html_page_body,
14
14
  )
15
15
  def show_webpage(input: ActionInput) -> ActionResult:
16
16
  """
@@ -1,6 +1,6 @@
1
1
  from kash.config.logger import get_logger
2
2
  from kash.exec import kash_action
3
- from kash.exec.preconditions import has_html_body, has_text_body
3
+ from kash.exec.preconditions import has_html_body, has_simple_text_body
4
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
@@ -8,7 +8,7 @@ from kash.utils.errors import InvalidInput
8
8
  log = get_logger(__name__)
9
9
 
10
10
 
11
- @kash_action(precondition=has_html_body | has_text_body)
11
+ @kash_action(precondition=has_html_body | has_simple_text_body)
12
12
  def strip_html(item: Item) -> Item:
13
13
  """
14
14
  Strip HTML tags from HTML or Markdown. This is a simple filter, simply searching
@@ -2,7 +2,7 @@ import os
2
2
 
3
3
  from frontmatter_format import fmf_read_raw, fmf_strip_frontmatter
4
4
  from prettyfmt import fmt_lines
5
- from strif import copyfile_atomic
5
+ from strif import atomic_output_file, copyfile_atomic
6
6
 
7
7
  from kash.config.logger import get_logger
8
8
  from kash.config.text_styles import STYLE_EMPH
@@ -28,10 +28,10 @@ log = get_logger(__name__)
28
28
 
29
29
 
30
30
  @kash_command
31
- def cbcopy(path: str | None = None, raw: bool = False) -> None:
31
+ def clipboard_copy(path: str | None = None, raw: bool = False) -> None:
32
32
  """
33
33
  Copy the contents of a file (or the first file in the selection) to the OS-native
34
- clipboard.
34
+ clipboard. Similar to `pbcopy` on macOS.
35
35
 
36
36
  :param raw: Copy the full exact contents of the file. Otherwise frontmatter is omitted.
37
37
  """
@@ -39,6 +39,8 @@ def cbcopy(path: str | None = None, raw: bool = False) -> None:
39
39
  import pyperclip
40
40
 
41
41
  input_paths = assemble_path_args(path)
42
+ if not input_paths:
43
+ raise InvalidInput("No path provided")
42
44
  input_path = input_paths[0]
43
45
 
44
46
  format = detect_file_format(input_path)
@@ -69,6 +71,22 @@ def cbcopy(path: str | None = None, raw: bool = False) -> None:
69
71
  )
70
72
 
71
73
 
74
+ @kash_command
75
+ def clipboard_paste(path: str = "untitled_paste.txt") -> None:
76
+ """
77
+ Paste the contents of the OS-native clipboard into a new file.
78
+ """
79
+ # TODO: Get this to work for images!
80
+ # And can we convert rich text to Markdown?
81
+ import pyperclip
82
+
83
+ contents = pyperclip.paste()
84
+ with atomic_output_file(path, backup_suffix=".{timestamp}.bak") as f:
85
+ f.write_text(contents)
86
+
87
+ print_status("Pasted clipboard contents to:\n%s", fmt_lines([fmt_loc(path)]))
88
+
89
+
72
90
  @kash_command
73
91
  def edit(path: str | None = None, all: bool = False) -> None:
74
92
  """
@@ -75,7 +75,8 @@ 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
78
+ DEFAULT_MAX_PER_GROUP = 50
79
+ """Default maximum number of files to display per group."""
79
80
 
80
81
 
81
82
  @kash_command
@@ -301,13 +302,13 @@ def files(
301
302
  # there are lots of groups and lots of files per group.
302
303
  # Default is max 100 per group but if we have 4 * 100 items, cut to 25.
303
304
  # 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
+ final_max_pg = DEFAULT_MAX_PER_GROUP if cap_per_group else max_per_group
305
306
  max_pg_explicit = max_per_group > 0
306
307
  if not max_pg_explicit:
307
308
  group_lens = [len(group_df) for group_df in grouped]
308
309
  for ratio in [2, 4]:
309
- if sum(group_lens) > ratio * DEFAULT_MAX_PG:
310
- final_max_pg = int(DEFAULT_MAX_PG / ratio)
310
+ if sum(group_lens) > ratio * DEFAULT_MAX_PER_GROUP:
311
+ final_max_pg = int(DEFAULT_MAX_PER_GROUP / ratio)
311
312
 
312
313
  total_displayed = 0
313
314
  total_displayed_size = 0
@@ -1,8 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import subprocess
2
4
  import tomllib
3
5
  from pathlib import Path
6
+ from typing import TYPE_CHECKING
4
7
 
5
- import pandas as pd
6
8
  from packaging.tags import Tag, sys_tags
7
9
  from packaging.utils import parse_wheel_filename
8
10
  from prettyfmt import fmt_size_dual
@@ -13,6 +15,9 @@ from kash.config.text_styles import COLOR_STATUS
13
15
  from kash.exec import kash_command
14
16
  from kash.shell.output.shell_output import cprint
15
17
 
18
+ if TYPE_CHECKING:
19
+ from pandas import DataFrame
20
+
16
21
  log = get_logger(__name__)
17
22
 
18
23
 
@@ -49,13 +54,15 @@ def get_platform() -> str:
49
54
  return next(sys_tags()).platform
50
55
 
51
56
 
52
- def parse_uv_lock(lock_path: Path) -> pd.DataFrame:
57
+ def parse_uv_lock(lock_path: Path) -> DataFrame:
53
58
  """
54
59
  Return one row per package from a uv.lock file, selecting the best
55
60
  matching wheel for the current interpreter or falling back to the sdist.
56
61
 
57
62
  Columns: name, version, registry, file_type, url, hash, size, filename.
58
63
  """
64
+ from pandas import DataFrame
65
+
59
66
  with open(lock_path, "rb") as f:
60
67
  data = tomllib.load(f)
61
68
 
@@ -88,7 +95,7 @@ def parse_uv_lock(lock_path: Path) -> pd.DataFrame:
88
95
  }
89
96
  )
90
97
 
91
- return pd.DataFrame(rows)
98
+ return DataFrame(rows)
92
99
 
93
100
 
94
101
  def uv_runtime_packages(
@@ -141,6 +148,8 @@ def uv_dep_info(
141
148
  By default, filters to show only 'main' dependencies from pyproject.toml.
142
149
  Helpful for looking at sizes of dependencies.
143
150
  """
151
+ import pandas as pd
152
+
144
153
  uv_lock_path = Path(uv_lock)
145
154
  pyproject_path = Path(pyproject)
146
155
 
@@ -10,8 +10,8 @@ from kash.exec_model.shell_model import ShellResult
10
10
  from kash.model.paths_model import StorePath
11
11
  from kash.shell.ui.shell_results import shell_print_selection_history
12
12
  from kash.utils.common.format_utils import fmt_loc
13
+ from kash.utils.common.inflection import plural
13
14
  from kash.utils.errors import InvalidInput
14
- from kash.utils.lang_utils.inflection import plural
15
15
  from kash.workspaces import Selection, current_ws
16
16
 
17
17
  log = get_logger(__name__)
@@ -51,6 +51,7 @@ from kash.shell.output.shell_output import (
51
51
  )
52
52
  from kash.shell.utils.native_utils import tail_file
53
53
  from kash.utils.common.format_utils import fmt_loc
54
+ from kash.utils.common.inflection import plural
54
55
  from kash.utils.common.obj_replace import remove_values
55
56
  from kash.utils.common.parse_key_vals import parse_key_value
56
57
  from kash.utils.common.type_utils import not_none
@@ -58,7 +59,6 @@ from kash.utils.common.url import Url, is_url
58
59
  from kash.utils.errors import InvalidInput
59
60
  from kash.utils.file_formats.chat_format import tail_chat_history
60
61
  from kash.utils.file_utils.dir_info import is_nonempty_dir
61
- from kash.utils.lang_utils.inflection import plural
62
62
  from kash.web_content.file_cache_utils import cache_file
63
63
  from kash.workspaces import (
64
64
  current_ws,
@@ -1,10 +1,7 @@
1
- import os
2
- from enum import Enum
3
- from pathlib import Path
4
- from typing import overload
1
+ from clideps.env_vars.env_enum import EnvEnum
5
2
 
6
3
 
7
- class KashEnv(str, Enum):
4
+ class KashEnv(EnvEnum):
8
5
  """
9
6
  Environment variable settings for kash. None are required, but these may be
10
7
  used to override default values.
@@ -33,40 +30,3 @@ class KashEnv(str, Enum):
33
30
 
34
31
  KASH_USER_AGENT = "KASH_USER_AGENT"
35
32
  """The user agent to use for HTTP requests."""
36
-
37
- @overload
38
- def read_str(self) -> str | None: ...
39
-
40
- @overload
41
- def read_str(self, default: str) -> str: ...
42
-
43
- def read_str(self, default: str | None = None) -> str | None:
44
- """
45
- Get the value of the environment variable from the environment (with
46
- optional default).
47
- """
48
- return os.environ.get(self.value, default)
49
-
50
- @overload
51
- def read_path(self) -> Path | None: ...
52
-
53
- @overload
54
- def read_path(self, default: Path) -> Path: ...
55
-
56
- def read_path(self, default: Path | None = None) -> Path | None:
57
- """
58
- Get the value of the environment variable as a resolved path (with
59
- optional default).
60
- """
61
- value = os.environ.get(self.value)
62
- if value:
63
- return Path(value).expanduser().resolve()
64
- else:
65
- return default.expanduser().resolve() if default else None
66
-
67
- def read_bool(self, default: bool = False) -> bool:
68
- """
69
- Get the value of the environment variable as a boolean.
70
- """
71
- value = str(os.environ.get(self.value, default) or "").lower()
72
- return bool(value and value != "0" and value != "false" and value != "no")
kash/config/logger.py CHANGED
@@ -55,6 +55,10 @@ class LogSettings:
55
55
  log_objects_dir: Path
56
56
  log_file_path: Path
57
57
 
58
+ @property
59
+ def is_quiet(self) -> bool:
60
+ return self.log_console_level >= LogLevel.error
61
+
58
62
 
59
63
  LOG_NAME_GLOBAL = "workspace"
60
64
 
@@ -83,6 +87,13 @@ def get_log_settings() -> LogSettings:
83
87
  return _log_settings.copy()
84
88
 
85
89
 
90
+ def is_console_quiet() -> bool:
91
+ """
92
+ Whether to suppress non-logging console output.
93
+ """
94
+ return global_settings().console_quiet
95
+
96
+
86
97
  def make_valid_log_name(name: str) -> str:
87
98
  name = str(name).strip().rstrip("/").removesuffix(".log")
88
99
  name = re.sub(r"[^\w-]", "_", name)
@@ -244,14 +255,6 @@ def _do_logging_setup(log_settings: LogSettings):
244
255
 
245
256
  # Manually adjust logging for a few packages, removing previous verbose default handlers.
246
257
 
247
- try:
248
- import litellm
249
- from litellm import _logging # noqa: F401
250
-
251
- litellm.suppress_debug_info = True # Suppress overly prominent exception messages.
252
- except ImportError:
253
- pass
254
-
255
258
  log_levels = {
256
259
  None: INFO,
257
260
  "LiteLLM": INFO,
@@ -276,10 +279,12 @@ def prefix(line: str, emoji: str = "", warn_emoji: str = "") -> str:
276
279
  return " ".join(filter(None, [prefix, emojis, line]))
277
280
 
278
281
 
279
- def prefix_args(args: tuple[Any], emoji: str = "", warn_emoji: str = "") -> tuple[Any]:
280
- if len(args) > 0:
281
- args = (prefix(str(args[0]), emoji, warn_emoji),) + args[1:]
282
- return args
282
+ def prefix_args(
283
+ msg: object, *other_args: object, emoji: str = "", warn_emoji: str = ""
284
+ ) -> tuple[str, *tuple[object, ...]]:
285
+ """Prefixes the string representation of msg and returns it with other_args."""
286
+ prefixed_msg = prefix(str(msg), emoji, warn_emoji)
287
+ return (prefixed_msg,) + other_args
283
288
 
284
289
 
285
290
  class CustomLogger(logging.Logger):
@@ -290,29 +295,29 @@ class CustomLogger(logging.Logger):
290
295
  """
291
296
 
292
297
  @override
293
- def debug(self, *args, **kwargs):
294
- super().debug(*prefix_args(args), **kwargs)
298
+ def debug(self, msg: object, *args: object, **kwargs: Any) -> None:
299
+ super().debug(*prefix_args(msg, *args), **kwargs)
295
300
 
296
301
  @override
297
- def info(self, *args, **kwargs):
298
- super().info(*prefix_args(args), **kwargs)
302
+ def info(self, msg: object, *args: object, **kwargs: Any) -> None:
303
+ super().info(*prefix_args(msg, *args), **kwargs)
299
304
 
300
305
  @override
301
- def warning(self, *args, **kwargs):
302
- super().warning(*prefix_args(args, warn_emoji=EMOJI_WARN), **kwargs)
306
+ def warning(self, msg: object, *args: object, **kwargs: Any) -> None:
307
+ super().warning(*prefix_args(msg, *args, warn_emoji=EMOJI_WARN), **kwargs)
303
308
 
304
309
  @override
305
- def error(self, *args, **kwargs):
306
- super().error(*prefix_args(args, warn_emoji=EMOJI_ERROR), **kwargs)
310
+ def error(self, msg: object, *args: object, **kwargs: Any) -> None:
311
+ super().error(*prefix_args(msg, *args, warn_emoji=EMOJI_ERROR), **kwargs)
307
312
 
308
- def log_at(self, level: LogLevel, *args, **kwargs):
313
+ def log_at(self, level: LogLevel, *args: object, **kwargs: Any) -> None:
309
314
  getattr(self, level.name)(*args, **kwargs)
310
315
 
311
- def message(self, *args, **kwargs):
316
+ def message(self, msg: object, *args: object, **kwargs: Any) -> None:
312
317
  """
313
318
  An informative message that should appear even if log level is set to warning.
314
319
  """
315
- super().warning(*prefix_args(args), **kwargs)
320
+ super().warning(*prefix_args(msg, *args), **kwargs)
316
321
 
317
322
  def save_object(
318
323
  self,
@@ -321,7 +326,7 @@ class CustomLogger(logging.Logger):
321
326
  obj: Any,
322
327
  level: LogLevel = LogLevel.info,
323
328
  file_ext: str = "txt",
324
- ):
329
+ ) -> None:
325
330
  """
326
331
  Save an object to a file in the log directory. Useful for details too large to
327
332
  log normally but useful for debugging.
@@ -342,7 +347,7 @@ class CustomLogger(logging.Logger):
342
347
 
343
348
  self.log_at(level, "%s %s saved: %s", EMOJI_SAVED, description, path)
344
349
 
345
- def dump_stack(self, all_threads: bool = True, level: LogLevel = LogLevel.info):
350
+ def dump_stack(self, all_threads: bool = True, level: LogLevel = LogLevel.info) -> None:
346
351
  self.log_at(level, "Stack trace dump:\n%s", current_stack_traces(all_threads))
347
352
 
348
353
  def __repr__(self):
@@ -3,7 +3,7 @@ import sys
3
3
  from logging import FileHandler, Formatter, LogRecord
4
4
  from pathlib import Path
5
5
 
6
- from kash.config.settings import LogLevel
6
+ from kash.config.settings import LogLevel, LogLevelStr
7
7
  from kash.config.suppress_warnings import demote_warnings
8
8
 
9
9
  # Basic logging setup for non-interactive logging, like on a server.
@@ -16,21 +16,21 @@ class SuppressedWarningsStreamHandler(logging.StreamHandler):
16
16
  super().emit(record)
17
17
 
18
18
 
19
- def basic_file_handler(path: Path, level: LogLevel) -> logging.FileHandler:
19
+ def basic_file_handler(path: Path, level: LogLevel | LogLevelStr) -> logging.FileHandler:
20
20
  handler = logging.FileHandler(path)
21
- handler.setLevel(level.value)
21
+ handler.setLevel(LogLevel.parse(level).value)
22
22
  handler.setFormatter(Formatter("%(asctime)s %(levelname).1s %(name)s - %(message)s"))
23
23
  return handler
24
24
 
25
25
 
26
- def basic_stderr_handler(level: LogLevel) -> logging.StreamHandler:
26
+ def basic_stderr_handler(level: LogLevel | LogLevelStr) -> logging.StreamHandler:
27
27
  handler = SuppressedWarningsStreamHandler(stream=sys.stderr)
28
- handler.setLevel(level.value)
28
+ handler.setLevel(LogLevel.parse(level).value)
29
29
  handler.setFormatter(Formatter("%(asctime)s %(levelname).1s %(name)s - %(message)s"))
30
30
  return handler
31
31
 
32
32
 
33
- def basic_logging_setup(log_path: Path | None, level: LogLevel):
33
+ def basic_logging_setup(log_path: Path | None, level: LogLevel | LogLevelStr):
34
34
  """
35
35
  Set up basic logging to a file and to stderr.
36
36
  """
kash/config/settings.py CHANGED
@@ -1,10 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
- from enum import Enum
4
+ from dataclasses import dataclass
5
+ from enum import IntEnum
3
6
  from functools import cache
4
7
  from logging import DEBUG, ERROR, INFO, WARNING
5
8
  from pathlib import Path
9
+ from typing import Literal
6
10
 
7
- from pydantic.dataclasses import dataclass
8
11
  from strif import AtomicVar
9
12
 
10
13
  from kash.config.env_settings import KashEnv
@@ -64,7 +67,14 @@ LOCAL_SERVER_PORTS_MAX = 30
64
67
  LOCAL_SERVER_LOG_NAME = "local_server"
65
68
 
66
69
 
67
- class LogLevel(Enum):
70
+ LogLevelStr = Literal["debug", "info", "message", "warning", "error"]
71
+
72
+
73
+ class LogLevel(IntEnum):
74
+ """
75
+ Convenience enum for log levels with parsing and ordering.
76
+ """
77
+
68
78
  debug = DEBUG
69
79
  info = INFO
70
80
  warning = WARNING
@@ -72,7 +82,9 @@ class LogLevel(Enum):
72
82
  error = ERROR
73
83
 
74
84
  @classmethod
75
- def parse(cls, level_str: str):
85
+ def parse(cls, level_str: str | LogLevelStr | LogLevel) -> LogLevel:
86
+ if isinstance(level_str, LogLevel):
87
+ return level_str
76
88
  canon_name = level_str.strip().lower()
77
89
  if canon_name == "warn":
78
90
  canon_name = "warning"
@@ -87,7 +99,7 @@ class LogLevel(Enum):
87
99
  return self.name
88
100
 
89
101
 
90
- DEFAULT_LOG_LEVEL = LogLevel.parse(KashEnv.KASH_LOG_LEVEL.read_str("warning"))
102
+ DEFAULT_LOG_LEVEL = LogLevel.parse(KashEnv.KASH_LOG_LEVEL.read_str(default="warning"))
91
103
 
92
104
 
93
105
  def resolve_and_create_dirs(path: Path | str, is_dir: bool = False) -> Path:
@@ -174,6 +186,9 @@ class Settings:
174
186
  console_log_level: LogLevel
175
187
  """The log level for console-based logging."""
176
188
 
189
+ console_quiet: bool
190
+ """If true, suppress non-logging console output."""
191
+
177
192
  file_log_level: LogLevel
178
193
  """The log level for file-based logging."""
179
194
 
@@ -205,7 +220,7 @@ def _get_ws_root_dir() -> Path:
205
220
 
206
221
 
207
222
  def _get_global_ws_dir() -> Path:
208
- kash_ws_dir = KashEnv.KASH_GLOBAL_WS.read_path()
223
+ kash_ws_dir = KashEnv.KASH_GLOBAL_WS.read_path(default=None)
209
224
  if kash_ws_dir:
210
225
  return kash_ws_dir
211
226
  else:
@@ -225,7 +240,7 @@ def _get_system_cache_dir() -> Path:
225
240
 
226
241
 
227
242
  def _get_mcp_ws_dir() -> Path | None:
228
- mcp_dir = KashEnv.KASH_MCP_WS.read_str()
243
+ mcp_dir = KashEnv.KASH_MCP_WS.read_str(default=None)
229
244
  if mcp_dir:
230
245
  return Path(mcp_dir).expanduser().resolve()
231
246
  else:
@@ -254,6 +269,7 @@ def _read_settings():
254
269
  default_editor="nano",
255
270
  file_log_level=LogLevel.info,
256
271
  console_log_level=DEFAULT_LOG_LEVEL,
272
+ console_quiet=False,
257
273
  local_server_ports_start=LOCAL_SERVER_PORT_START,
258
274
  local_server_ports_max=LOCAL_SERVER_PORTS_MAX,
259
275
  local_server_port=0,
kash/config/setup.py CHANGED
@@ -7,7 +7,13 @@ from clideps.env_vars.dotenv_utils import load_dotenv_paths
7
7
 
8
8
  from kash.config.logger import reset_rich_logging
9
9
  from kash.config.logger_basic import basic_logging_setup
10
- from kash.config.settings import LogLevel, configure_ws_and_settings, global_settings
10
+ from kash.config.settings import (
11
+ LogLevel,
12
+ LogLevelStr,
13
+ atomic_global_settings,
14
+ configure_ws_and_settings,
15
+ global_settings,
16
+ )
11
17
 
12
18
 
13
19
  @cache
@@ -16,7 +22,9 @@ def kash_setup(
16
22
  rich_logging: bool,
17
23
  kash_ws_root: Path | None = None,
18
24
  log_path: Path | None = None,
19
- level: LogLevel = LogLevel.info,
25
+ log_level: LogLevel | LogLevelStr | None = None,
26
+ console_log_level: LogLevel | LogLevelStr | None = None,
27
+ console_quiet: bool | None = None,
20
28
  ):
21
29
  """
22
30
  One-time top-level setup of essential logging, keys, directories, and configs.
@@ -25,8 +33,14 @@ def kash_setup(
25
33
  Can call this if embedding kash in another app.
26
34
  Can be used to set the global default workspace and logs directory
27
35
  and/or the default log file.
28
- If `rich_logging` is True, then rich logging with warnings only for console use.
29
- If `rich_logging` is False, then use basic logging to a file and stderr.
36
+
37
+ Basic logging is to the specified log file.
38
+ If enabled, rich logging is to the console as well.
39
+
40
+ By default console is "warning" level but can be controlled with
41
+ the `console_log_level` parameter.
42
+ All console/shell output can be suppressed with `console_quiet`. By default
43
+ console is quiet if `console_log_level` is "error" or higher.
30
44
  """
31
45
  from kash.utils.common.stack_traces import add_stacktrace_handler
32
46
 
@@ -40,10 +54,24 @@ def kash_setup(
40
54
  configure_ws_and_settings(kash_ws_root)
41
55
 
42
56
  # Now set up logging, as it might depend on workspace root.
57
+ log_level = LogLevel.parse(log_level) if log_level else LogLevel.info
58
+
43
59
  if rich_logging:
60
+ # These settings are only used for rich logging.
61
+ console_log_level = (
62
+ LogLevel.parse(console_log_level) if console_log_level else LogLevel.warning
63
+ )
64
+ console_quiet = (
65
+ console_quiet if console_quiet is not None else console_log_level >= LogLevel.error
66
+ )
67
+
68
+ with atomic_global_settings().updates() as settings:
69
+ settings.console_log_level = console_log_level
70
+ settings.file_log_level = log_level
71
+ settings.console_quiet = console_quiet
44
72
  reset_rich_logging(log_path=log_path)
45
73
  else:
46
- basic_logging_setup(log_path=log_path, level=level)
74
+ basic_logging_setup(log_path=log_path, level=log_level)
47
75
 
48
76
  _lib_setup()
49
77