kash-shell 0.3.11__py3-none-any.whl → 0.3.13__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 (95) hide show
  1. kash/actions/core/markdownify.py +5 -4
  2. kash/actions/core/readability.py +4 -4
  3. kash/actions/core/render_as_html.py +8 -6
  4. kash/actions/core/show_webpage.py +2 -2
  5. kash/actions/core/strip_html.py +2 -2
  6. kash/commands/base/basic_file_commands.py +24 -3
  7. kash/commands/base/diff_commands.py +38 -3
  8. kash/commands/base/files_command.py +5 -4
  9. kash/commands/base/reformat_command.py +1 -1
  10. kash/commands/base/show_command.py +1 -1
  11. kash/commands/extras/parse_uv_lock.py +12 -3
  12. kash/commands/workspace/selection_commands.py +1 -1
  13. kash/commands/workspace/workspace_commands.py +62 -16
  14. kash/config/env_settings.py +2 -42
  15. kash/config/logger.py +30 -25
  16. kash/config/logger_basic.py +6 -6
  17. kash/config/settings.py +23 -7
  18. kash/config/setup.py +33 -5
  19. kash/config/text_styles.py +25 -22
  20. kash/docs/load_source_code.py +1 -1
  21. kash/embeddings/cosine.py +12 -4
  22. kash/embeddings/embeddings.py +16 -6
  23. kash/embeddings/text_similarity.py +10 -4
  24. kash/exec/__init__.py +3 -0
  25. kash/exec/action_decorators.py +4 -19
  26. kash/exec/action_exec.py +46 -27
  27. kash/exec/fetch_url_metadata.py +8 -5
  28. kash/exec/importing.py +4 -4
  29. kash/exec/llm_transforms.py +2 -2
  30. kash/exec/preconditions.py +11 -19
  31. kash/exec/runtime_settings.py +134 -0
  32. kash/exec/shell_callable_action.py +5 -3
  33. kash/file_storage/file_store.py +91 -53
  34. kash/file_storage/item_file_format.py +6 -3
  35. kash/file_storage/store_filenames.py +7 -3
  36. kash/help/help_embeddings.py +2 -2
  37. kash/llm_utils/clean_headings.py +1 -1
  38. kash/{text_handling → llm_utils}/custom_sliding_transforms.py +0 -3
  39. kash/llm_utils/init_litellm.py +16 -0
  40. kash/llm_utils/llm_api_keys.py +6 -2
  41. kash/llm_utils/llm_completion.py +12 -5
  42. kash/local_server/__init__.py +1 -1
  43. kash/local_server/local_server_commands.py +2 -1
  44. kash/mcp/__init__.py +1 -1
  45. kash/mcp/mcp_cli.py +3 -2
  46. kash/mcp/mcp_server_commands.py +8 -2
  47. kash/mcp/mcp_server_routes.py +11 -12
  48. kash/media_base/media_cache.py +10 -3
  49. kash/media_base/transcription_deepgram.py +15 -2
  50. kash/model/__init__.py +1 -1
  51. kash/model/actions_model.py +9 -54
  52. kash/model/exec_model.py +79 -0
  53. kash/model/items_model.py +131 -81
  54. kash/model/operations_model.py +38 -15
  55. kash/model/paths_model.py +2 -0
  56. kash/shell/output/shell_output.py +10 -8
  57. kash/shell/shell_main.py +2 -2
  58. kash/shell/ui/shell_results.py +2 -1
  59. kash/shell/utils/exception_printing.py +2 -2
  60. kash/utils/common/format_utils.py +0 -14
  61. kash/utils/common/import_utils.py +46 -18
  62. kash/utils/common/task_stack.py +4 -15
  63. kash/utils/errors.py +14 -9
  64. kash/utils/file_utils/file_formats_model.py +61 -26
  65. kash/utils/file_utils/file_sort_filter.py +10 -3
  66. kash/utils/file_utils/filename_parsing.py +41 -16
  67. kash/{text_handling → utils/text_handling}/doc_normalization.py +23 -13
  68. kash/utils/text_handling/escape_html_tags.py +156 -0
  69. kash/{text_handling → utils/text_handling}/markdown_utils.py +82 -4
  70. kash/utils/text_handling/markdownify_utils.py +87 -0
  71. kash/{text_handling → utils/text_handling}/unified_diffs.py +1 -44
  72. kash/web_content/file_cache_utils.py +42 -34
  73. kash/web_content/local_file_cache.py +29 -12
  74. kash/web_content/web_extract.py +1 -1
  75. kash/web_content/web_extract_readabilipy.py +4 -2
  76. kash/web_content/web_fetch.py +42 -7
  77. kash/web_content/web_page_model.py +2 -1
  78. kash/web_gen/simple_webpage.py +1 -1
  79. kash/web_gen/templates/base_styles.css.jinja +139 -16
  80. kash/web_gen/templates/simple_webpage.html.jinja +1 -1
  81. kash/workspaces/__init__.py +12 -3
  82. kash/workspaces/selections.py +2 -2
  83. kash/workspaces/workspace_dirs.py +58 -0
  84. kash/workspaces/workspace_importing.py +2 -2
  85. kash/workspaces/workspace_output.py +2 -2
  86. kash/workspaces/workspaces.py +26 -90
  87. kash/xonsh_custom/load_into_xonsh.py +4 -2
  88. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/METADATA +4 -4
  89. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/RECORD +93 -89
  90. kash/shell/utils/argparse_utils.py +0 -20
  91. kash/utils/lang_utils/inflection.py +0 -18
  92. /kash/{text_handling → utils/text_handling}/markdown_render.py +0 -0
  93. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/WHEEL +0 -0
  94. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/entry_points.txt +0 -0
  95. {kash_shell-0.3.11.dist-info → kash_shell-0.3.13.dist-info}/licenses/LICENSE +0 -0
@@ -4,14 +4,15 @@
4
4
  /* Adding Hack Nerd Font to all fonts for icon support, if it is installed. */
5
5
  --font-sans: "Source Sans 3 Variable", sans-serif, "Hack Nerd Font";
6
6
  --font-serif: "PT Serif", serif, "Hack Nerd Font";
7
- --font-weight-sans-bold: 625; /* Source Sans 3 Variable better at this weight. */
7
+ /* Source Sans 3 Variable better at these weights. */
8
+ --font-weight-sans-bold: 620;
8
9
  --font-mono: "Hack Nerd Font", "Menlo", "DejaVu Sans Mono", Consolas, "Lucida Console", monospace;
9
10
 
10
11
  --font-size-large: 1.2rem;
11
12
  --font-size-normal: 1rem;
12
13
  --font-size-small: 0.95rem;
13
14
  --font-size-smaller: 0.85rem;
14
- --font-size-mono: 0.8rem;
15
+ --font-size-mono: 0.82rem;
15
16
  --font-size-mono-small: 0.75rem;
16
17
  --font-size-mono-tiny: 0.7rem;
17
18
 
@@ -71,7 +72,7 @@
71
72
  body {
72
73
  font-family: var(--font-serif);
73
74
  color: var(--color-text);
74
- line-height: 1.4;
75
+ line-height: 1.5;
75
76
  padding: 0; /* No padding so we can have full width elements. */
76
77
  margin: auto;
77
78
  background-color: var(--color-bg);
@@ -81,7 +82,13 @@ body {
81
82
 
82
83
  {% block typography %}
83
84
  p {
84
- margin-bottom: 1rem;
85
+ margin-top: 0.75rem;
86
+ margin-bottom: 0.75rem;
87
+ }
88
+
89
+ pre {
90
+ margin-top: 0.75rem;
91
+ margin-bottom: 0.75rem;
85
92
  }
86
93
 
87
94
  b, strong {
@@ -114,14 +121,14 @@ h1 {
114
121
 
115
122
  h2 {
116
123
  font-size: 1.4rem;
117
- margin-top: 2.5rem;
124
+ margin-top: 1.5rem;
118
125
  margin-bottom: 1rem;
119
126
  }
120
127
 
121
128
  h3 {
122
- font-size: 1.09rem;
123
- margin-top: 1.7rem;
124
- margin-bottom: 0.7rem;
129
+ font-size: 1.03rem;
130
+ margin-top: 1.5rem;
131
+ margin-bottom: 0.5rem;
125
132
  }
126
133
 
127
134
  h4 {
@@ -131,7 +138,7 @@ h4 {
131
138
 
132
139
  ul {
133
140
  list-style-type: none;
134
- margin-left: 2rem;
141
+ margin-left: 1.8rem;
135
142
  margin-bottom: 1rem;
136
143
  padding-left: 0;
137
144
  }
@@ -155,9 +162,9 @@ ul > li::before {
155
162
  }
156
163
 
157
164
  ol {
158
- margin-bottom: 0.7rem;
159
165
  list-style-type: decimal;
160
- margin-left: 2rem;
166
+ margin-left: 1.8rem;
167
+ margin-bottom: 0.7rem;
161
168
  }
162
169
 
163
170
  ol > li {
@@ -172,24 +179,60 @@ blockquote {
172
179
  color: var(--color-secondary);
173
180
  }
174
181
 
182
+ /* Inline code styling */
175
183
  code {
176
184
  font-family: var(--font-mono);
177
185
  font-size: var(--font-size-mono);
178
186
  letter-spacing: -0.025em;
179
- padding: 0.2rem 0.4rem;
180
187
  }
181
188
 
189
+ /* Code blocks (pre + code) */
182
190
  pre {
183
191
  font-family: var(--font-mono);
184
192
  font-size: var(--font-size-mono);
185
193
  letter-spacing: -0.025em;
186
- {# overflow-x: auto; #}
194
+ background-color: var(--color-bg-alt);
195
+ border-radius: 4px;
196
+ border: 1px dotted var(--color-border-hint);
197
+ padding: 0.2rem 0.2rem 0.1rem 0.2rem;
198
+ overflow-x: auto; /* Enable horizontal scrolling */
199
+ position: relative; /* Create new stacking context */
200
+ }
201
+
202
+ /* Reset code styling when inside pre blocks */
203
+ pre > code {
204
+ display: block; /* Make code block take full width */
205
+ line-height: 1.5; /* Improve readability */
187
206
  }
207
+
208
+ hr {
209
+ border: none;
210
+ height: 1.5rem;
211
+ position: relative;
212
+ text-align: center;
213
+ margin: 0.5rem auto;
214
+ overflow: visible;
215
+ }
216
+
217
+ hr:before {
218
+ content: "";
219
+ display: block;
220
+ position: absolute;
221
+ top: 50%;
222
+ width: 4rem;
223
+ left: calc(50% - 2rem);
224
+ border-top: 1px solid var(--black-light);
225
+ }
226
+
188
227
  {% endblock typography %}
189
228
 
190
229
  {% block long_text_styles %}
191
230
  /* Long text stylings, for nicely formatting blog post length or longer texts. */
192
231
 
232
+ .long-text {
233
+ box-shadow: none;
234
+ }
235
+
193
236
  .long-text h1 {
194
237
  font-family: var(--font-serif);
195
238
  font-weight: 400;
@@ -204,8 +247,9 @@ pre {
204
247
  .long-text h3 {
205
248
  font-family: var(--font-sans);
206
249
  font-weight: var(--font-weight-sans-bold);
250
+ font-size: 1.05rem;
207
251
  text-transform: uppercase;
208
- letter-spacing: 0.02em;
252
+ letter-spacing: 0.025em;
209
253
  }
210
254
 
211
255
  .long-text h4 {
@@ -218,6 +262,41 @@ pre {
218
262
  font-style: italic;
219
263
  font-size: 1rem;
220
264
  }
265
+
266
+ .long-text .sans-text {
267
+ font-family: var(--font-sans);
268
+ }
269
+
270
+ .long-text .sans-text p {
271
+ margin-top: 0.8rem;
272
+ margin-bottom: 0.8rem;
273
+ }
274
+
275
+ .long-text .sans-text h1 {
276
+ font-family: var(--font-sans);
277
+ font-size: 1.75rem;
278
+ font-weight: 380;
279
+ margin-top: 1rem;
280
+ margin-bottom: 1.2rem;
281
+ }
282
+
283
+ .long-text .sans-text h2 {
284
+ font-family: var(--font-sans);
285
+ font-size: 1.25rem;
286
+ font-weight: 440;
287
+ margin-top: 1rem;
288
+ margin-bottom: 0.8rem;
289
+ }
290
+
291
+ .long-text .sans-text h3 {
292
+ font-family: var(--font-sans);
293
+ font-size: 1.03rem;
294
+ font-weight: var(--font-weight-sans-bold);
295
+ text-transform: uppercase;
296
+ letter-spacing: 0.03em;
297
+ margin-top: 1.1rem;
298
+ margin-bottom: 0.8rem;
299
+ }
221
300
  {% endblock long_text_styles %}
222
301
 
223
302
  {% block table_styles %}
@@ -230,12 +309,14 @@ table {
230
309
  border-collapse: collapse;
231
310
  word-break: break-word; /* long words/URLs wrap instead of inflating the column */
232
311
  border: 1px solid var(--color-border-hint);
312
+ line-height: 1.3; /* Tables tigher but not as tight as headers */
233
313
  }
234
314
 
235
315
  th {
236
316
  text-transform: uppercase;
237
- letter-spacing: 0.02em;
317
+ letter-spacing: 0.03em;
238
318
  border-bottom: 1px solid var(--color-border-hint);
319
+ line-height: 1.2;
239
320
  }
240
321
 
241
322
  th, td {
@@ -281,14 +362,17 @@ nav {
281
362
  {% block footnote_styles %}
282
363
  /* Footnotes. */
283
364
  sup {
284
- font-size: 80%;
365
+ font-size: 85%;
285
366
  }
286
367
 
287
368
  .footnote-ref a, .footnote {
369
+ font-family: var(--font-sans);
288
370
  text-decoration: none;
289
371
  padding: 0 0.15rem;
290
372
  border-radius: 4px;
291
373
  transition: all 0.15s ease-in-out;
374
+ font-style: normal;
375
+ font-weight: 500;
292
376
  }
293
377
 
294
378
  .footnote-ref a:hover, .footnote:hover {
@@ -308,11 +392,50 @@ sup {
308
392
  .table-container {
309
393
  width: calc(100vw - 6rem);
310
394
  }
395
+
396
+ /* Apply shadow to long-text containers on larger screens */
397
+ .long-text {
398
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 -2px 6px -1px rgba(0, 0, 0, 0.07);
399
+ }
400
+ /* But remove shadow when wrapped in no-shadow class */
401
+ .no-shadow .long-text {
402
+ box-shadow: none !important;
403
+ }
311
404
  }
312
405
 
406
+ /* Make narrower screens more usable for lists and tables. */
313
407
  @media (max-width: 768px) {
408
+ /* Prevent horizontal scrolling on the body */
409
+ body {
410
+ overflow-x: hidden;
411
+ }
412
+
413
+ /* Constrain the long-text container */
414
+ .long-text {
415
+ max-width: 100%;
416
+ overflow-x: hidden;
417
+ }
418
+
419
+ /* Make table containers scrollable without affecting page layout */
420
+ .table-container {
421
+ max-width: 100%;
422
+ overflow-x: auto;
423
+ transform: none;
424
+ left: 0;
425
+ position: relative;
426
+ margin-left: auto;
427
+ margin-right: auto;
428
+ }
429
+
314
430
  table {
315
431
  font-size: var(--font-size-smaller);
432
+ /* Tables can be wider than container */
433
+ width: auto;
434
+ min-width: 100%;
435
+ }
436
+
437
+ ul, ol {
438
+ margin-left: 1rem;
316
439
  }
317
440
  }
318
441
  {% endblock responsive_styles %}
@@ -2,7 +2,7 @@
2
2
 
3
3
  <!-- simple_webpage begin main_content block -->
4
4
  {% block main_content %}
5
- <div class="long-text container max-w-3xl mx-auto bg-white py-8 px-6 md:px-16 md:shadow-lg">
5
+ <div class="long-text container max-w-3xl mx-auto bg-white py-4 px-6 md:px-16">
6
6
  {% block page_title %}
7
7
  {% if title and add_title_h1 %}
8
8
  <h1 class="text-center text-4xl mt-6 mb-6">{{ title }}</h1>
@@ -1,19 +1,28 @@
1
1
  from kash.workspaces.selections import Selection, SelectionHistory
2
+ from kash.workspaces.workspace_dirs import (
3
+ enclosing_ws_dir,
4
+ global_ws_dir,
5
+ is_global_ws_dir,
6
+ is_ws_dir,
7
+ )
2
8
  from kash.workspaces.workspaces import (
3
9
  Workspace,
10
+ _switch_ws_settings,
4
11
  current_ignore,
5
12
  current_ws,
6
13
  get_global_ws,
7
14
  get_ws,
8
- global_ws_dir,
9
15
  resolve_ws,
10
- switch_to_ws,
11
16
  ws_param_value,
12
17
  )
13
18
 
14
19
  __all__ = [
15
20
  "Selection",
16
21
  "SelectionHistory",
22
+ "enclosing_ws_dir",
23
+ "global_ws_dir",
24
+ "is_global_ws_dir",
25
+ "is_ws_dir",
17
26
  "Workspace",
18
27
  "current_ignore",
19
28
  "current_ws",
@@ -21,6 +30,6 @@ __all__ = [
21
30
  "get_ws",
22
31
  "global_ws_dir",
23
32
  "resolve_ws",
33
+ "_switch_ws_settings",
24
34
  "ws_param_value",
25
- "switch_to_ws",
26
35
  ]
@@ -6,13 +6,13 @@ from pathlib import Path
6
6
  from typing import TypeVar
7
7
 
8
8
  from frontmatter_format import new_yaml, yaml_util
9
- from prettyfmt import fmt_lines
9
+ from prettyfmt import fmt_count_items, fmt_lines
10
10
  from pydantic import BaseModel, Field, PrivateAttr, field_serializer, field_validator
11
11
 
12
12
  from kash.config.logger import get_logger
13
13
  from kash.model.paths_model import StorePath
14
14
  from kash.shell.utils.native_utils import native_trash
15
- from kash.utils.common.format_utils import fmt_count_items, fmt_loc
15
+ from kash.utils.common.format_utils import fmt_loc
16
16
  from kash.utils.errors import InvalidInput, InvalidOperation
17
17
 
18
18
  log = get_logger(__name__)
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from functools import cache
5
+ from pathlib import Path
6
+
7
+ from kash.config.logger import get_logger
8
+ from kash.config.settings import global_settings, resolve_and_create_dirs
9
+ from kash.file_storage.metadata_dirs import MetadataDirs
10
+ from kash.utils.errors import InvalidInput
11
+
12
+ log = get_logger(__name__)
13
+
14
+
15
+ @cache
16
+ def global_ws_dir() -> Path:
17
+ kb_path = resolve_and_create_dirs(global_settings().global_ws_dir, is_dir=True)
18
+ log.debug("Global workspace path: %s", kb_path)
19
+ return kb_path
20
+
21
+
22
+ def is_global_ws_dir(path: Path) -> bool:
23
+ return path.resolve() == global_settings().global_ws_dir
24
+
25
+
26
+ def is_ws_dir(path: Path) -> bool:
27
+ dirs = MetadataDirs(path, False)
28
+ return dirs.is_initialized()
29
+
30
+
31
+ def enclosing_ws_dir(path: Path | None = None) -> Path | None:
32
+ """
33
+ Get the workspace directory enclosing the given path, or of the current
34
+ working directory if no path is given.
35
+ """
36
+ if not path:
37
+ path = Path(".")
38
+
39
+ path = path.absolute()
40
+ while path != Path("/"):
41
+ if is_ws_dir(path):
42
+ return path
43
+ path = path.parent
44
+
45
+ return None
46
+
47
+
48
+ def normalize_workspace_name(ws_name: str) -> str:
49
+ return str(ws_name).strip().rstrip("/")
50
+
51
+
52
+ def check_strict_workspace_name(ws_name: str) -> str:
53
+ ws_name = normalize_workspace_name(ws_name)
54
+ if not re.match(r"^[\w.-]+$", ws_name):
55
+ raise InvalidInput(
56
+ f"Use an alphanumeric name (- and . also allowed) for the workspace name: `{ws_name}`"
57
+ )
58
+ return ws_name
@@ -24,7 +24,7 @@ def import_url(ws: FileStore, url: Url) -> Item:
24
24
  )
25
25
  item = Item(ItemType.resource, url=canon_url, format=Format.url)
26
26
  # No need to overwrite any resource we already have for the identical URL.
27
- store_path = ws.save(item, overwrite=False)
27
+ store_path = ws.save(item, skip_dup_names=True)
28
28
  # Load to fill in any metadata we may already have.
29
29
  item = ws.load(store_path)
30
30
  return item
@@ -45,7 +45,7 @@ def import_and_load(ws: FileStore, locator: Locator | str) -> Item:
45
45
  # It's already a StorePath.
46
46
  item = ws.load(locator)
47
47
  else:
48
- log.message("Importing locator as local path: %r", locator)
48
+ log.info("Importing locator as local path: %r", locator)
49
49
  path = Path(locator)
50
50
  if not path.exists():
51
51
  raise InvalidInput(f"File not found: {path}")
@@ -3,7 +3,7 @@ from pathlib import Path
3
3
  from chopdiff.divs.parse_divs import parse_divs
4
4
  from flowmark import Wrap
5
5
  from frontmatter_format import fmf_read, fmf_read_frontmatter_raw
6
- from prettyfmt import fmt_size_dual
6
+ from prettyfmt import fmt_count_items, fmt_size_dual
7
7
  from rich.box import SQUARE
8
8
  from rich.panel import Panel
9
9
  from rich.text import Text
@@ -15,7 +15,7 @@ from kash.model.items_model import ItemType
15
15
  from kash.shell.output.kerm_code_utils import click_to_paste
16
16
  from kash.shell.output.shell_formatting import format_name_and_value
17
17
  from kash.shell.output.shell_output import PrintHooks, cprint
18
- from kash.utils.common.format_utils import fmt_count_items, fmt_loc
18
+ from kash.utils.common.format_utils import fmt_loc
19
19
  from kash.utils.file_formats.chat_format import ChatHistory
20
20
  from kash.utils.file_utils.dir_info import get_dir_info
21
21
  from kash.utils.file_utils.file_formats_model import file_format_info
@@ -1,5 +1,5 @@
1
- import contextvars
2
- import re
1
+ from __future__ import annotations
2
+
3
3
  from abc import ABC, abstractmethod
4
4
  from functools import cache
5
5
  from pathlib import Path
@@ -13,10 +13,13 @@ from kash.config.settings import (
13
13
  global_settings,
14
14
  resolve_and_create_dirs,
15
15
  )
16
+ from kash.config.text_styles import STYLE_HINT
16
17
  from kash.file_storage.metadata_dirs import MetadataDirs
17
18
  from kash.model.params_model import GLOBAL_PARAMS, RawParamValues
19
+ from kash.shell.output.shell_output import PrintHooks, cprint
18
20
  from kash.utils.errors import FileNotFound, InvalidInput, InvalidState
19
21
  from kash.utils.file_utils.ignore_files import IgnoreFilter, is_ignored_default
22
+ from kash.workspaces.workspace_dirs import check_strict_workspace_name, is_global_ws_dir, is_ws_dir
20
23
  from kash.workspaces.workspace_registry import WorkspaceInfo, get_ws_registry
21
24
 
22
25
  if TYPE_CHECKING:
@@ -25,19 +28,6 @@ if TYPE_CHECKING:
25
28
  log = get_logger(__name__)
26
29
 
27
30
 
28
- def normalize_workspace_name(ws_name: str) -> str:
29
- return str(ws_name).strip().rstrip("/")
30
-
31
-
32
- def check_strict_workspace_name(ws_name: str) -> str:
33
- ws_name = normalize_workspace_name(ws_name)
34
- if not re.match(r"^[\w.-]+$", ws_name):
35
- raise InvalidInput(
36
- f"Use an alphanumeric name (- and . also allowed) for the workspace name: `{ws_name}`"
37
- )
38
- return ws_name
39
-
40
-
41
31
  class Workspace(ABC):
42
32
  """
43
33
  A workspace is the context for actions and is tied to a folder on disk.
@@ -59,50 +49,6 @@ class Workspace(ABC):
59
49
  def base_dir(self) -> Path:
60
50
  """The base directory for this workspace."""
61
51
 
62
- def __enter__(self):
63
- """
64
- Context manager to set this workspace as the current workspace.
65
- """
66
- from kash.workspaces.workspaces import current_ws_context
67
-
68
- self._token = current_ws_context.set(self.base_dir)
69
- return self
70
-
71
- def __exit__(self, exc_type, exc_val, exc_tb):
72
- """
73
- Restore the previous workspace context.
74
- """
75
- from kash.workspaces.workspaces import current_ws_context
76
-
77
- current_ws_context.reset(self._token)
78
-
79
-
80
- current_ws_context: contextvars.ContextVar[Path | None] = contextvars.ContextVar(
81
- "current_ws_context", default=None
82
- )
83
- """
84
- Context variable that tracks the current workspace. Only used if it is
85
- explicitly set with a `with ws.as_current()` block.
86
- """
87
-
88
-
89
- def is_ws_dir(path: Path) -> bool:
90
- dirs = MetadataDirs(path, False)
91
- return dirs.is_initialized()
92
-
93
-
94
- def enclosing_ws_dir(path: Path) -> Path | None:
95
- """
96
- Get the workspace directory enclosing the given path (itself or a parent or None).
97
- """
98
- path = path.absolute()
99
- while path != Path("/"):
100
- if is_ws_dir(path):
101
- return path
102
- path = path.parent
103
-
104
- return None
105
-
106
52
 
107
53
  def resolve_ws(name: str | Path) -> WorkspaceInfo:
108
54
  """
@@ -137,10 +83,10 @@ def resolve_ws(name: str | Path) -> WorkspaceInfo:
137
83
 
138
84
  ws_name = check_strict_workspace_name(resolved.name)
139
85
 
140
- return WorkspaceInfo(ws_name, resolved, is_global_ws_path(resolved))
86
+ return WorkspaceInfo(ws_name, resolved, is_global_ws_dir(resolved))
141
87
 
142
88
 
143
- def get_ws(name_or_path: str | Path, auto_init: bool = True) -> "FileStore":
89
+ def get_ws(name_or_path: str | Path, auto_init: bool = True) -> FileStore:
144
90
  """
145
91
  Get a workspace by name or path. Adds to the in-memory registry so we reuse it.
146
92
  With `auto_init` true, will initialize the workspace if it is not already initialized.
@@ -162,18 +108,14 @@ def global_ws_dir() -> Path:
162
108
  return kb_path
163
109
 
164
110
 
165
- def is_global_ws_path(path: Path) -> bool:
166
- return path.name.lower() == GLOBAL_WS_NAME.lower()
167
-
168
-
169
- def get_global_ws() -> "FileStore":
111
+ def get_global_ws() -> FileStore:
170
112
  """
171
113
  Get the global_ws workspace.
172
114
  """
173
115
  return get_ws_registry().load(GLOBAL_WS_NAME, global_ws_dir(), True)
174
116
 
175
117
 
176
- def switch_to_ws(base_dir: Path) -> "FileStore":
118
+ def _switch_ws_settings(base_dir: Path) -> FileStore:
177
119
  """
178
120
  Switch the current workspace to the given directory.
179
121
  Updates logging and cache directories to be within that workspace.
@@ -199,41 +141,35 @@ def switch_to_ws(base_dir: Path) -> "FileStore":
199
141
  return get_ws_registry().load(info.name, info.base_dir, info.is_global_ws)
200
142
 
201
143
 
202
- def _current_ws_info() -> tuple[Path | None, bool]:
203
- """
204
- Infer the current workspace from context or the current working directory.
205
- Does not load the workspace.
206
- """
207
- # First check if we have an explicit workspace context.
208
- override_dir = current_ws_context.get()
209
- if override_dir:
210
- return override_dir, is_global_ws_path(override_dir)
211
-
212
- # Fall back to detecting from the current working directory.
213
- dir = enclosing_ws_dir(Path("."))
214
- is_global_ws = is_global_ws_path(dir) if dir else False
215
- if not dir or is_global_ws:
216
- dir = global_ws_dir()
217
- return dir, is_global_ws
218
-
219
-
220
- def current_ws(silent: bool = False) -> "FileStore":
144
+ def current_ws(silent: bool = False) -> FileStore:
221
145
  """
222
146
  Get the current workspace based on the current working directory.
223
147
  Loads and registers the workspace if it is not already loaded.
224
- Also updates logging and cache directories if this has changed.
148
+
149
+ As a convenience, this call also auto-updates logging and cache directories
150
+ if this has changed.
225
151
  """
226
- base_dir, _is_global_ws = _current_ws_info()
152
+ from kash.exec.runtime_settings import current_ws_context
153
+
154
+ ws_context = current_ws_context()
155
+ base_dir = ws_context.current_ws_dir
227
156
  if not base_dir:
228
157
  raise InvalidState(
229
158
  f"No workspace found in: {fmt_path(Path('.').absolute(), resolve=False)}\n"
230
159
  "Create one with the `workspace` command."
231
160
  )
232
161
 
233
- ws = switch_to_ws(base_dir)
162
+ ws = _switch_ws_settings(base_dir)
234
163
 
235
164
  if not silent:
236
- ws.log_workspace_info(once=True)
165
+ did_log = ws.log_workspace_info(once=True)
166
+ if did_log and ws.is_global_ws and not ws_context.override_dir:
167
+ PrintHooks.spacer()
168
+ log.warning("Note you are currently using the default global workspace.")
169
+ cprint(
170
+ "Create or switch to another workspace with the `workspace` command.",
171
+ style=STYLE_HINT,
172
+ )
237
173
 
238
174
  return ws
239
175
 
@@ -13,8 +13,6 @@ from kash.commands.help.welcome import welcome
13
13
  from kash.config.logger import get_logger
14
14
  from kash.config.settings import RECOMMENDED_PKGS, check_kerm_code_support
15
15
  from kash.config.text_styles import LOGO_NAME, STYLE_HINT
16
- from kash.local_server.local_server import start_ui_server
17
- from kash.local_server.local_url_formatters import enable_local_urls
18
16
  from kash.mcp.mcp_server_commands import start_mcp_server
19
17
  from kash.shell.output.shell_output import PrintHooks, cprint
20
18
  from kash.shell.version import get_version_tag
@@ -89,6 +87,10 @@ def load_into_xonsh():
89
87
  # Currently only Kerm supports our advanced UI with Kerm codes.
90
88
  supports_kerm_codes = check_kerm_code_support()
91
89
  if supports_kerm_codes:
90
+ # Don't pay for import until needed.
91
+ from kash.local_server.local_server import start_ui_server
92
+ from kash.local_server.local_url_formatters import enable_local_urls
93
+
92
94
  start_ui_server()
93
95
  enable_local_urls(True)
94
96
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kash-shell
3
- Version: 0.3.11
3
+ Version: 0.3.13
4
4
  Summary: The knowledge agent shell (core)
5
5
  Project-URL: Repository, https://github.com/jlevy/kash-shell
6
6
  Author-email: Joshua Levy <joshua@cal.berkeley.edu>
@@ -20,17 +20,16 @@ Requires-Dist: anyio>=4.8.0
20
20
  Requires-Dist: audioop-lts>=0.2.1; python_version >= '3.13'
21
21
  Requires-Dist: cachetools>=5.5.2
22
22
  Requires-Dist: chopdiff>=0.2.1
23
- Requires-Dist: clideps>=0.1.1
23
+ Requires-Dist: clideps>=0.1.4
24
24
  Requires-Dist: colour>=0.1.5
25
25
  Requires-Dist: cssselect>=1.2.0
26
26
  Requires-Dist: deepgram-sdk>=3.10.1
27
27
  Requires-Dist: dunamai>=1.23.0
28
28
  Requires-Dist: fastapi>=0.115.11
29
- Requires-Dist: flowmark>=0.4.5
29
+ Requires-Dist: flowmark>=0.4.6
30
30
  Requires-Dist: frontmatter-format>=0.2.1
31
31
  Requires-Dist: funlog>=0.2.0
32
32
  Requires-Dist: humanfriendly>=10.0
33
- Requires-Dist: inflect>=7.5.0
34
33
  Requires-Dist: inquirerpy>=0.3.4
35
34
  Requires-Dist: jinja2>=3.1.6
36
35
  Requires-Dist: justext>=3.0.2
@@ -43,6 +42,7 @@ Requires-Dist: openai>=1.66.3
43
42
  Requires-Dist: pandas>=2.2.3
44
43
  Requires-Dist: patch-ng>=1.18.1
45
44
  Requires-Dist: pathspec>=0.12.1
45
+ Requires-Dist: pluralizer>=1.2.0
46
46
  Requires-Dist: prettyfmt>=0.3.1
47
47
  Requires-Dist: prompt-toolkit>=3.0.50
48
48
  Requires-Dist: pydantic>=2.10.6