kash-shell 0.3.8__py3-none-any.whl → 0.3.10__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 (154) hide show
  1. kash/actions/__init__.py +4 -4
  2. kash/actions/core/markdownify.py +5 -2
  3. kash/actions/core/readability.py +5 -2
  4. kash/actions/core/render_as_html.py +18 -0
  5. kash/actions/core/webpage_config.py +12 -4
  6. kash/commands/__init__.py +8 -20
  7. kash/commands/base/basic_file_commands.py +15 -0
  8. kash/commands/base/debug_commands.py +15 -2
  9. kash/commands/base/general_commands.py +27 -18
  10. kash/commands/base/logs_commands.py +1 -4
  11. kash/commands/base/model_commands.py +8 -8
  12. kash/commands/base/search_command.py +3 -2
  13. kash/commands/base/show_command.py +5 -3
  14. kash/commands/extras/parse_uv_lock.py +186 -0
  15. kash/commands/help/doc_commands.py +2 -31
  16. kash/commands/help/welcome.py +33 -0
  17. kash/commands/workspace/selection_commands.py +11 -6
  18. kash/commands/workspace/workspace_commands.py +19 -16
  19. kash/config/colors.py +2 -0
  20. kash/config/env_settings.py +72 -0
  21. kash/config/init.py +2 -2
  22. kash/config/logger.py +61 -59
  23. kash/config/logger_basic.py +12 -5
  24. kash/config/server_config.py +6 -6
  25. kash/config/settings.py +117 -67
  26. kash/config/setup.py +35 -9
  27. kash/config/suppress_warnings.py +30 -12
  28. kash/config/text_styles.py +3 -13
  29. kash/docs/load_api_docs.py +2 -1
  30. kash/docs/markdown/topics/a2_installation.md +7 -3
  31. kash/docs/markdown/topics/a3_getting_started.md +3 -2
  32. kash/docs/markdown/warning.md +3 -8
  33. kash/docs/markdown/welcome.md +4 -0
  34. kash/docs_base/load_recipe_snippets.py +1 -1
  35. kash/docs_base/recipes/{general_system_commands.ksh → general_system_commands.sh} +1 -1
  36. kash/{concepts → embeddings}/cosine.py +2 -1
  37. kash/embeddings/text_similarity.py +57 -0
  38. kash/exec/__init__.py +20 -3
  39. kash/exec/action_decorators.py +18 -4
  40. kash/exec/action_exec.py +41 -23
  41. kash/exec/action_registry.py +13 -48
  42. kash/exec/command_registry.py +2 -1
  43. kash/exec/fetch_url_metadata.py +4 -6
  44. kash/exec/importing.py +56 -0
  45. kash/exec/llm_transforms.py +6 -6
  46. kash/exec/precondition_registry.py +2 -1
  47. kash/exec/preconditions.py +16 -1
  48. kash/exec/shell_callable_action.py +33 -19
  49. kash/file_storage/file_store.py +23 -14
  50. kash/file_storage/item_file_format.py +13 -3
  51. kash/file_storage/metadata_dirs.py +11 -2
  52. kash/help/assistant.py +2 -2
  53. kash/help/assistant_instructions.py +2 -1
  54. kash/help/help_embeddings.py +2 -2
  55. kash/help/help_printing.py +14 -10
  56. kash/help/tldr_help.py +5 -3
  57. kash/llm_utils/clean_headings.py +1 -1
  58. kash/llm_utils/llm_api_keys.py +4 -4
  59. kash/llm_utils/llm_completion.py +2 -2
  60. kash/llm_utils/llm_features.py +68 -0
  61. kash/llm_utils/llm_messages.py +1 -2
  62. kash/llm_utils/llm_names.py +1 -1
  63. kash/llm_utils/llms.py +17 -12
  64. kash/local_server/__init__.py +5 -2
  65. kash/local_server/local_server.py +56 -46
  66. kash/local_server/local_server_commands.py +15 -15
  67. kash/local_server/local_server_routes.py +2 -2
  68. kash/local_server/local_url_formatters.py +1 -1
  69. kash/mcp/__init__.py +5 -2
  70. kash/mcp/mcp_cli.py +54 -17
  71. kash/mcp/mcp_server_commands.py +5 -6
  72. kash/mcp/mcp_server_routes.py +14 -11
  73. kash/mcp/mcp_server_sse.py +61 -34
  74. kash/mcp/mcp_server_stdio.py +0 -8
  75. kash/media_base/audio_processing.py +81 -7
  76. kash/media_base/media_cache.py +18 -18
  77. kash/media_base/media_services.py +1 -1
  78. kash/media_base/media_tools.py +6 -6
  79. kash/media_base/services/local_file_media.py +2 -2
  80. kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -109
  81. kash/media_base/transcription_format.py +73 -0
  82. kash/media_base/transcription_whisper.py +38 -0
  83. kash/model/__init__.py +73 -5
  84. kash/model/actions_model.py +38 -4
  85. kash/model/concept_model.py +30 -0
  86. kash/model/items_model.py +56 -13
  87. kash/model/params_model.py +24 -0
  88. kash/shell/completions/completion_scoring.py +37 -5
  89. kash/shell/output/kerm_codes.py +1 -2
  90. kash/shell/output/shell_formatting.py +14 -4
  91. kash/shell/shell_main.py +2 -2
  92. kash/shell/utils/exception_printing.py +6 -0
  93. kash/shell/utils/native_utils.py +26 -20
  94. kash/text_handling/custom_sliding_transforms.py +12 -4
  95. kash/text_handling/doc_normalization.py +6 -2
  96. kash/text_handling/markdown_render.py +117 -0
  97. kash/text_handling/markdown_utils.py +204 -0
  98. kash/utils/common/import_utils.py +12 -3
  99. kash/utils/common/type_utils.py +0 -29
  100. kash/utils/common/url.py +80 -28
  101. kash/utils/errors.py +6 -0
  102. kash/utils/file_utils/{dir_size.py → dir_info.py} +25 -4
  103. kash/utils/file_utils/file_ext.py +2 -3
  104. kash/utils/file_utils/file_formats.py +28 -2
  105. kash/utils/file_utils/file_formats_model.py +50 -19
  106. kash/utils/file_utils/filename_parsing.py +10 -4
  107. kash/web_content/dir_store.py +1 -2
  108. kash/web_content/file_cache_utils.py +37 -10
  109. kash/web_content/file_processing.py +68 -0
  110. kash/web_content/local_file_cache.py +12 -9
  111. kash/web_content/web_extract.py +8 -3
  112. kash/web_content/web_fetch.py +12 -4
  113. kash/web_gen/tabbed_webpage.py +5 -2
  114. kash/web_gen/templates/base_styles.css.jinja +120 -14
  115. kash/web_gen/templates/base_webpage.html.jinja +60 -13
  116. kash/web_gen/templates/content_styles.css.jinja +4 -2
  117. kash/web_gen/templates/item_view.html.jinja +2 -2
  118. kash/web_gen/templates/tabbed_webpage.html.jinja +1 -2
  119. kash/workspaces/__init__.py +15 -2
  120. kash/workspaces/selections.py +18 -3
  121. kash/workspaces/source_items.py +4 -2
  122. kash/workspaces/workspace_output.py +11 -4
  123. kash/workspaces/workspaces.py +5 -11
  124. kash/xonsh_custom/command_nl_utils.py +40 -19
  125. kash/xonsh_custom/custom_shell.py +44 -12
  126. kash/xonsh_custom/customize_prompt.py +39 -21
  127. kash/xonsh_custom/load_into_xonsh.py +26 -27
  128. kash/xonsh_custom/shell_load_commands.py +2 -2
  129. kash/xonsh_custom/xonsh_completers.py +2 -249
  130. kash/xonsh_custom/xonsh_keybindings.py +282 -0
  131. kash/xonsh_custom/xonsh_modern_tools.py +3 -3
  132. kash/xontrib/kash_extension.py +5 -6
  133. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/METADATA +26 -12
  134. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/RECORD +140 -140
  135. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/entry_points.txt +1 -1
  136. kash/concepts/concept_formats.py +0 -23
  137. kash/concepts/text_similarity.py +0 -112
  138. kash/shell/clideps/api_keys.py +0 -99
  139. kash/shell/clideps/dotenv_setup.py +0 -114
  140. kash/shell/clideps/dotenv_utils.py +0 -89
  141. kash/shell/clideps/pkg_deps.py +0 -232
  142. kash/shell/clideps/platforms.py +0 -11
  143. kash/shell/clideps/terminal_features.py +0 -56
  144. kash/shell/utils/osc_utils.py +0 -95
  145. kash/shell/utils/terminal_images.py +0 -133
  146. kash/text_handling/markdown_util.py +0 -167
  147. kash/utils/common/atomic_var.py +0 -158
  148. kash/utils/common/string_replace.py +0 -93
  149. kash/utils/common/string_template.py +0 -101
  150. /kash/docs_base/recipes/{python_dev_commands.ksh → python_dev_commands.sh} +0 -0
  151. /kash/docs_base/recipes/{tldr_standard_commands.ksh → tldr_standard_commands.sh} +0 -0
  152. /kash/{concepts → embeddings}/embeddings.py +0 -0
  153. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/WHEEL +0 -0
  154. {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,9 @@
1
1
  :root {
2
2
  font-size: 16px;
3
- {# Adding Hack Nerd Font to all fonts for icon support, if it is installed. #}
4
- --font-sans: "Varta", sans-serif, "Hack Nerd Font";
3
+ /* Adding Hack Nerd Font to all fonts for icon support, if it is installed. */
4
+ --font-sans: "Source Sans 3 Variable", sans-serif, "Hack Nerd Font";
5
5
  --font-serif: "PT Serif", serif, "Hack Nerd Font";
6
+ --font-weight-sans-bold: 625; /* Source Sans 3 Variable better at this weight. */
6
7
  --font-mono: "Hack Nerd Font", "Menlo", "DejaVu Sans Mono", Consolas, "Lucida Console", monospace;
7
8
 
8
9
  --font-size-large: 1.2rem;
@@ -63,16 +64,32 @@
63
64
  body {
64
65
  font-family: var(--font-serif);
65
66
  color: var(--color-text);
66
- line-height: 1.3;
67
+ line-height: 1.4;
67
68
  padding: 0; /* No padding so we can have full width elements. */
68
69
  margin: auto;
69
70
  background-color: var(--color-bg);
71
+ overflow-wrap: break-word; /* Don't let long words/URLs break layout. */
70
72
  }
71
73
 
72
74
  p {
73
75
  margin-bottom: 1rem;
74
76
  }
75
77
 
78
+ b, strong {
79
+ font-weight: var(--font-weight-sans-bold);
80
+ }
81
+
82
+ a {
83
+ color: var(--color-primary);
84
+ text-decoration: none;
85
+ }
86
+
87
+ a:hover {
88
+ color: var(--color-primary-light);
89
+ text-decoration: underline;
90
+ transition: all 0.15s ease-in-out;
91
+ }
92
+
76
93
  h1,
77
94
  h2,
78
95
  h3,
@@ -88,19 +105,19 @@ h1 {
88
105
 
89
106
  h2 {
90
107
  font-size: 1.4rem;
91
- margin-top: 2rem;
108
+ margin-top: 2.5rem;
92
109
  margin-bottom: 1rem;
93
110
  }
94
111
 
95
112
  h3 {
96
- font-size: 1.15rem;
97
- margin-top: 1.5rem;
98
- margin-bottom: 1rem;
113
+ font-size: 1.09rem;
114
+ margin-top: 1.7rem;
115
+ margin-bottom: 0.7rem;
99
116
  }
100
117
 
101
118
  h4 {
102
119
  margin-top: 1rem;
103
- margin-bottom: 1rem;
120
+ margin-bottom: 0.7rem;
104
121
  }
105
122
 
106
123
  /* Long text stylings, for nicely formatting blog post length or longer texts. */
@@ -118,8 +135,9 @@ h4 {
118
135
 
119
136
  .long-text h3 {
120
137
  font-family: var(--font-sans);
121
- font-weight: 700;
138
+ font-weight: var(--font-weight-sans-bold);
122
139
  text-transform: uppercase;
140
+ letter-spacing: 0.02em;
123
141
  }
124
142
 
125
143
  .long-text h4 {
@@ -141,22 +159,31 @@ ul {
141
159
  }
142
160
 
143
161
  li {
144
- margin-top: 0.5rem;
162
+ margin-top: 0.7rem;
145
163
  margin-bottom: 0;
146
164
  position: relative;
147
165
  }
148
166
 
149
- ul>li::before {
167
+ li > p {
168
+ margin-bottom: 0;
169
+ }
170
+
171
+ ul > li::before {
150
172
  content: "▪︎";
151
173
  position: absolute;
152
- left: -1rem;
153
- font-size: 0.9rem;
174
+ left: -.85rem;
175
+ top: .25rem;
176
+ font-size: 0.62rem;
154
177
  }
155
178
 
156
179
  ol {
180
+ margin-bottom: 0.7rem;
157
181
  list-style-type: decimal;
158
182
  margin-left: 2rem;
159
- margin-bottom: 1rem;
183
+ }
184
+
185
+ ol > li {
186
+ padding-left: 0.25rem;
160
187
  }
161
188
 
162
189
  blockquote {
@@ -181,6 +208,37 @@ pre {
181
208
  {# overflow-x: auto; #}
182
209
  }
183
210
 
211
+ table {
212
+ font-family: var(--font-sans);
213
+ font-size: var(--font-size-small);
214
+ width: auto;
215
+ margin-left: auto;
216
+ margin-right: auto;
217
+ border-collapse: collapse;
218
+ word-break: break-word; /* long words/URLs wrap instead of inflating the column */
219
+ border: 1px solid var(--color-border-hint);
220
+ }
221
+
222
+ th {
223
+ text-transform: uppercase;
224
+ letter-spacing: 0.02em;
225
+ border-bottom: 1px solid var(--color-border-hint);
226
+ }
227
+
228
+ th, td {
229
+ padding: 0.3rem 0.6rem;
230
+ max-width: 40rem;
231
+ min-width: 6rem;
232
+ }
233
+
234
+ th {
235
+ background-color: var(--color-bg-alt-solid);
236
+ }
237
+
238
+ tbody tr:nth-child(even) {
239
+ background-color: var(--color-bg-alt-solid);
240
+ }
241
+
184
242
  nav {
185
243
  display: flex;
186
244
  flex-wrap: wrap;
@@ -190,3 +248,51 @@ nav {
190
248
  gap: 1rem;
191
249
  /* Add some space between the buttons */
192
250
  }
251
+
252
+ /* Container for wide tables to allow tables to break out of parent width. */
253
+ .table-container {
254
+ {# max-width: calc(100vw - 6rem); #}
255
+ position: relative;
256
+ left: 50%;
257
+ transform: translateX(-50%);
258
+ box-sizing: border-box;
259
+ margin-bottom: 1rem;
260
+ background-color: var(--color-bg-solid);
261
+
262
+ }
263
+
264
+ /* Bleed wide on larger screens. */
265
+ /* TODO: Don't make so wide if table itself isn't large? */
266
+ @media (min-width: 768px) {
267
+ table {
268
+ width: calc(100vw - 6rem);
269
+ }
270
+ .table-container {
271
+ width: calc(100vw - 6rem);
272
+ }
273
+ }
274
+
275
+ @media (max-width: 768px) {
276
+ table {
277
+ font-size: var(--font-size-smaller);
278
+ }
279
+ }
280
+
281
+ /* Footnotes. */
282
+ sup {
283
+ font-size: 80%;
284
+ }
285
+
286
+ .footnote-ref a, .footnote {
287
+ text-decoration: none;
288
+ padding: 0 0.15rem;
289
+ border-radius: 4px;
290
+ transition: all 0.15s ease-in-out;
291
+ }
292
+
293
+ .footnote-ref a:hover, .footnote:hover {
294
+ background-color: var(--color-hover-bg);
295
+ color: var(--color-primary-light);
296
+ text-decoration: none;
297
+ }
298
+
@@ -20,12 +20,19 @@
20
20
  <link rel="preload" as="font" type="font/woff2" crossorigin
21
21
  href="https://cdn.jsdelivr.net/fontsource/fonts/pt-serif@latest/latin-700-italic.woff2" />
22
22
  <link rel="preload" as="font" type="font/woff2" crossorigin
23
- href="https://cdn.jsdelivr.net/fontsource/fonts/varta:vf@latest/latin-wght-normal.woff2" />
23
+ href="https://cdn.jsdelivr.net/fontsource/fonts/source-sans-3:vf@latest/latin-wght-normal.woff2" />
24
+ <link rel="preload" as="font" type="font/woff2" crossorigin
25
+ href="https://cdn.jsdelivr.net/fontsource/fonts/source-sans-3:vf@latest/latin-wght-italic.woff2" />
24
26
 
25
27
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet" />
26
28
  <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js" defer></script>
27
29
 
30
+ {% if extra_head %}
31
+ {{ extra_head|safe }}
32
+ {% endif %}
33
+
28
34
  <style>
35
+ /* https://fontsource.org/fonts/pt-serif/cdn */
29
36
  /* pt-serif-latin-400-normal */
30
37
  @font-face {
31
38
  font-family: 'PT Serif';
@@ -62,14 +69,27 @@
62
69
  src: url(https://cdn.jsdelivr.net/fontsource/fonts/pt-serif@latest/latin-700-italic.woff2) format('woff2');
63
70
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
64
71
  }
72
+ /* https://fontsource.org/fonts/source-sans-3/cdn */
73
+ /* source-sans-3-latin-wght-normal */
65
74
  @font-face {
66
- font-family: 'Varta';
75
+ font-family: 'Source Sans 3 Variable';
67
76
  font-style: normal;
68
77
  font-display: block;
69
- font-weight: 300 700;
70
- src: url(https://cdn.jsdelivr.net/fontsource/fonts/varta:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
71
- unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
78
+ font-weight: 200 900;
79
+ src: url(https://cdn.jsdelivr.net/fontsource/fonts/source-sans-3:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
80
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
72
81
  }
82
+ /* source-sans-3-latin-wght-normal */
83
+ @font-face {
84
+ font-family: 'Source Sans 3 Variable';
85
+ font-style: italic;
86
+ font-display: block;
87
+ font-weight: 200 900;
88
+ src: url(https://cdn.jsdelivr.net/fontsource/fonts/source-sans-3:vf@latest/latin-wght-italic.woff2) format('woff2-variations');
89
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
90
+ }
91
+
92
+ {# Other decent sans serif options: Work Sans Variable, Nunito Sans Variable #}
73
93
 
74
94
  {% include "base_styles.css.jinja" %}
75
95
  {% include "content_styles.css.jinja" %}
@@ -82,24 +102,45 @@
82
102
  </head>
83
103
 
84
104
  <body>
85
- {{ content|safe }}
86
- </body>
105
+ {{ content|safe }}
87
106
 
88
107
  <script>
89
- // Some messages are sent to the parent window, in case we are in a viewport like a
90
- // tooltip that supports it.
91
-
92
- // Request a resize of the parent viewport (e.g. tooltip).
93
108
  document.addEventListener('DOMContentLoaded', () => {
109
+ // Send messages to the parent window, in case we are in a viewport where that matters
110
+ // (e.g. an iframe tooltip).
111
+ // Request a resize of the parent viewport. This iframe size message format isn't
112
+ // standardized by ResizeObserver, but is common. It is supported by Kerm.
94
113
  const content = document.body;
95
114
  console.log("Suggesting resize to parent:", content.offsetWidth, content.offsetHeight);
96
- // This iframe size message format isn't standardized by ResizeObserver, but is common.
97
- // It is supported by Kerm.
115
+
98
116
  window.parent.postMessage({
99
117
  type: 'resize',
100
118
  width: Math.max(content.offsetWidth, 600),
101
119
  height: Math.max(content.offsetHeight, 100)
102
120
  }, '*');
121
+
122
+ // Wrap tables within the main content area for horizontal scrolling.
123
+ const containers = [];
124
+ document.querySelectorAll('.long-text').forEach(el => {
125
+ const pane = el.querySelector('.tab-pane');
126
+ containers.push(pane || el);
127
+ });
128
+ containers.forEach(container => {
129
+ // Grab all tables, then only wrap the ones whose parent is this container.
130
+ container.querySelectorAll('table').forEach(table => {
131
+ if (table.parentElement !== container) {
132
+ return; // Only direct children.
133
+ }
134
+ if (table.parentNode.classList.contains('table-container')) {
135
+ return; // Already wrapped.
136
+ }
137
+ const wrapper = document.createElement('div');
138
+ wrapper.className = 'table-container';
139
+ container.insertBefore(wrapper, table);
140
+ wrapper.appendChild(table);
141
+ });
142
+ });
143
+
103
144
  });
104
145
 
105
146
  // Double-click to expand (e.g. expand tooltip to popover).
@@ -119,6 +160,12 @@
119
160
  }, '*');
120
161
  }
121
162
  });
163
+
164
+ {% if extra_footer_js %}
165
+ {{ extra_footer_js|safe }}
166
+ {% endif %}
122
167
  </script>
123
168
 
169
+ </body>
170
+
124
171
  </html>
@@ -15,7 +15,7 @@
15
15
  padding: 0 0.4rem;
16
16
  }
17
17
 
18
- /* More novel bracket ideas: [❲⟦⟪⟬〔〘〚〖 ]❳⟧⟫⟭ 〕〙〛〗 */
18
+ {# More novel bracket ideas: [❲⟦⟪⟬〔〘〚〖 ]❳⟧⟫⟭ 〕〙〛〗 #}
19
19
  .citation::before {
20
20
  content: "[";
21
21
  }
@@ -57,6 +57,7 @@
57
57
  font-weight: 500;
58
58
  font-size: 1.2rem;
59
59
  text-transform: uppercase;
60
+ letter-spacing: 0.02em;
60
61
  margin-bottom: 0.5rem;
61
62
  }
62
63
 
@@ -81,6 +82,7 @@
81
82
  font-weight: 500;
82
83
  font-size: 1.2rem;
83
84
  text-transform: uppercase;
85
+ letter-spacing: 0.02em;
84
86
  margin-bottom: 0.5rem;
85
87
 
86
88
  /* Hack to center the header above the columns */
@@ -107,7 +109,7 @@
107
109
  font-size: var(--font-size-small);
108
110
  font-weight: 600;
109
111
  text-transform: uppercase;
110
- letter-spacing: 0.05em;
112
+ letter-spacing: 0.02em;
111
113
  line-height: 1.2;
112
114
  padding: 0 0.5rem;
113
115
  border-bottom-width: 2px;
@@ -19,9 +19,9 @@
19
19
  .item-type {
20
20
  font-family: var(--font-sans);
21
21
  text-transform: uppercase;
22
+ letter-spacing: 0.02em;
22
23
  font-size: var(--font-size-smaller);
23
- font-weight: 800;
24
- letter-spacing: 0.05em;
24
+ font-weight: var(--font-weight-sans-bold);
25
25
  padding-right: 1rem;
26
26
  }
27
27
 
@@ -1,4 +1,4 @@
1
- <div class="long-text container max-w-3xl mx-auto bg-white py-8 px-16 shadow-lg">
1
+ <div class="long-text container max-w-3xl mx-auto bg-white py-8 px-6 md:px-16 md:shadow-lg">
2
2
  <h1 class="text-center text-4xl mt-6 mb-6">{{ title }}</h1>
3
3
  <div>
4
4
  <!-- Navigation Tabs -->
@@ -30,7 +30,6 @@
30
30
  {% endfor %}
31
31
  </div>
32
32
  </div>
33
- <!-- TODO: Footer info (match with pdf export) -->
34
33
  </div>
35
34
 
36
35
  <script>
@@ -1,5 +1,3 @@
1
- # flake8: noqa: F401
2
-
3
1
  from kash.workspaces.selections import Selection, SelectionHistory
4
2
  from kash.workspaces.workspaces import (
5
3
  Workspace,
@@ -9,5 +7,20 @@ from kash.workspaces.workspaces import (
9
7
  get_ws,
10
8
  global_ws_dir,
11
9
  resolve_ws,
10
+ switch_to_ws,
12
11
  ws_param_value,
13
12
  )
13
+
14
+ __all__ = [
15
+ "Selection",
16
+ "SelectionHistory",
17
+ "Workspace",
18
+ "current_ignore",
19
+ "current_ws",
20
+ "get_global_ws",
21
+ "get_ws",
22
+ "global_ws_dir",
23
+ "resolve_ws",
24
+ "ws_param_value",
25
+ "switch_to_ws",
26
+ ]
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections.abc import Callable, Sequence
2
4
  from functools import wraps
3
5
  from pathlib import Path
@@ -78,6 +80,12 @@ class Selection(BaseModel):
78
80
  if current_path == old_path:
79
81
  self.paths[idx] = new_path
80
82
 
83
+ def refresh(self, base_dir: Path) -> None:
84
+ """
85
+ Refresh the selection paths, dropping any that no longer exist.
86
+ """
87
+ self.paths[:] = [p for p in self.paths if (base_dir / p).exists()]
88
+
81
89
  def as_str(self, max_lines: int = SELECTION_DISPLAY_MAX) -> str:
82
90
  lines = [
83
91
  f"{fmt_count_items(len(self.paths), 'item')}:",
@@ -113,7 +121,7 @@ class SelectionHistory(BaseModel):
113
121
  }
114
122
 
115
123
  @classmethod
116
- def init(cls, save_path: Path, max_history: int = SELECTION_HISTORY_MAX) -> "SelectionHistory":
124
+ def init(cls, save_path: Path, max_history: int = SELECTION_HISTORY_MAX) -> SelectionHistory:
117
125
  """
118
126
  Initialize selection history, loading from save_path if it exists.
119
127
  """
@@ -144,9 +152,9 @@ class SelectionHistory(BaseModel):
144
152
  yaml_util.write_yaml_file(data, str(self._save_path))
145
153
 
146
154
  @persist_after(_save)
147
- def clear(self) -> None:
155
+ def clear_all(self) -> None:
148
156
  """
149
- Clear the history.
157
+ Clear the entire selection history.
150
158
  """
151
159
  self.history.clear()
152
160
  self.current_index = 0
@@ -306,6 +314,13 @@ class SelectionHistory(BaseModel):
306
314
  for selection in self.history:
307
315
  selection.replace_values(replacements)
308
316
 
317
+ @persist_after(_save)
318
+ def refresh_current(self, base_dir: Path) -> None:
319
+ """
320
+ Refresh the current selection to drop any paths that no longer exist.
321
+ """
322
+ self.current.refresh(base_dir)
323
+
309
324
  def previous_n(self, n: int, expected_size: int | None = None) -> list[Selection]:
310
325
  """
311
326
  Get the `n` previous selections (backwards and including the current position),
@@ -7,7 +7,6 @@ from kash.model.items_model import Item
7
7
  from kash.model.paths_model import StorePath
8
8
  from kash.model.preconditions_model import Precondition
9
9
  from kash.utils.common.format_utils import fmt_loc
10
- from kash.utils.common.type_utils import not_none
11
10
  from kash.utils.errors import NoMatch
12
11
  from kash.workspaces import current_ws
13
12
 
@@ -59,7 +58,10 @@ def find_upstream_item(
59
58
 
60
59
  log.message("Looking for upstream item that matches precondition: %s", precondition)
61
60
  for source_item in source_items:
62
- source_path = not_none(source_item.store_path)
61
+ source_path = source_item.store_path
62
+ if not source_path:
63
+ log.error("Source item has no store path: %s", source_item)
64
+ continue
63
65
  if precondition(source_item):
64
66
  log.message(
65
67
  "Found source item that matches requirements: %s",
@@ -17,7 +17,7 @@ from kash.shell.output.shell_formatting import format_name_and_value
17
17
  from kash.shell.output.shell_output import PrintHooks, cprint
18
18
  from kash.utils.common.format_utils import fmt_count_items, fmt_loc
19
19
  from kash.utils.file_formats.chat_format import ChatHistory
20
- from kash.utils.file_utils.dir_size import get_dir_size
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
22
22
  from kash.workspaces import Selection, current_ws
23
23
 
@@ -67,14 +67,21 @@ def post_shell_result(res: ShellResult) -> None:
67
67
  suggest_actions()
68
68
 
69
69
 
70
- def print_dir_info(path: Path, text_wrap: Wrap = Wrap.NONE):
71
- dir_info = get_dir_size(path)
70
+ def print_dir_info(path: Path, tally_formats: bool = False, text_wrap: Wrap = Wrap.NONE):
71
+ dir_info = get_dir_info(path, tally_formats)
72
72
 
73
73
  cprint(format_name_and_value("total files", f"{dir_info.file_count}"), text_wrap=text_wrap)
74
74
  cprint(
75
75
  format_name_and_value("total size", fmt_size_dual(dir_info.total_size)), text_wrap=text_wrap
76
76
  )
77
77
 
78
+ if tally_formats and dir_info.format_tallies:
79
+ for format, count in dir_info.format_tallies.items():
80
+ cprint(
81
+ format_name_and_value(f"format: {format}", f"{count}"),
82
+ text_wrap=text_wrap,
83
+ )
84
+
78
85
 
79
86
  def print_file_info(
80
87
  input_path: Path,
@@ -83,7 +90,7 @@ def print_file_info(
83
90
  text_wrap: Wrap = Wrap.NONE,
84
91
  ):
85
92
  if input_path.is_dir():
86
- print_dir_info(input_path, text_wrap)
93
+ print_dir_info(input_path, tally_formats=True, text_wrap=text_wrap)
87
94
  return
88
95
 
89
96
  # Format info.
@@ -7,18 +7,14 @@ from typing import TYPE_CHECKING, TypeVar
7
7
 
8
8
  from prettyfmt import fmt_path
9
9
 
10
- from kash.config.logger import get_logger, reset_log_root
10
+ from kash.config.logger import get_logger, reset_rich_logging
11
11
  from kash.config.settings import (
12
12
  GLOBAL_WS_NAME,
13
- RECOMMENDED_API_KEYS,
14
- get_global_ws_dir,
15
- get_ws_root_dir,
16
13
  global_settings,
17
14
  resolve_and_create_dirs,
18
15
  )
19
16
  from kash.file_storage.metadata_dirs import MetadataDirs
20
17
  from kash.model.params_model import GLOBAL_PARAMS, RawParamValues
21
- from kash.shell.clideps.api_keys import print_api_key_setup
22
18
  from kash.utils.errors import FileNotFound, InvalidInput, InvalidState
23
19
  from kash.utils.file_utils.ignore_files import IgnoreFilter, is_ignored_default
24
20
  from kash.workspaces.workspace_registry import WorkspaceInfo, get_ws_registry
@@ -128,7 +124,7 @@ def resolve_ws(name: str | Path) -> WorkspaceInfo:
128
124
  resolved = name
129
125
  parent_dir = resolved.parent
130
126
  else:
131
- parent_dir = get_ws_root_dir()
127
+ parent_dir = global_settings().ws_root_dir
132
128
  resolved = parent_dir / name
133
129
  elif name_str.startswith(".") or name_str.startswith("/"):
134
130
  # Explicit paths respected otherwise use workspace root.
@@ -136,7 +132,7 @@ def resolve_ws(name: str | Path) -> WorkspaceInfo:
136
132
  parent_dir = resolved.parent
137
133
  name = resolved.name
138
134
  else:
139
- parent_dir = get_ws_root_dir()
135
+ parent_dir = global_settings().ws_root_dir
140
136
  resolved = parent_dir / Path(name_str)
141
137
 
142
138
  ws_name = check_strict_workspace_name(resolved.name)
@@ -161,7 +157,7 @@ def get_ws(name_or_path: str | Path, auto_init: bool = True) -> "FileStore":
161
157
 
162
158
  @cache
163
159
  def global_ws_dir() -> Path:
164
- kb_path = resolve_and_create_dirs(get_global_ws_dir(), is_dir=True)
160
+ kb_path = resolve_and_create_dirs(global_settings().global_ws_dir, is_dir=True)
165
161
  log.debug("Global workspace path: %s", kb_path)
166
162
  return kb_path
167
163
 
@@ -190,7 +186,7 @@ def switch_to_ws(base_dir: Path) -> "FileStore":
190
186
  ws_dirs = MetadataDirs(base_dir=info.base_dir, is_global_ws=info.is_global_ws)
191
187
 
192
188
  # Use the global log root for the global_ws, and the workspace log root otherwise.
193
- reset_log_root(None, info.name if not info.is_global_ws else None)
189
+ reset_rich_logging(None, info.name if not info.is_global_ws else None)
194
190
 
195
191
  if info.is_global_ws:
196
192
  # If not in a workspace, use the global cache locations.
@@ -237,8 +233,6 @@ def current_ws(silent: bool = False) -> "FileStore":
237
233
  ws = switch_to_ws(base_dir)
238
234
 
239
235
  if not silent:
240
- # Delayed, once-only logging of any setup warnings.
241
- print_api_key_setup(RECOMMENDED_API_KEYS, once=True)
242
236
  ws.log_workspace_info(once=True)
243
237
 
244
238
  return ws
@@ -3,20 +3,16 @@ import re
3
3
  from prettyfmt import fmt_words
4
4
 
5
5
  INNER_PUNCT_CHARS = r"-'’–—"
6
- OUTER_PUNCT_CHARS = r".,'\"“”‘’:!?()"
6
+ OUTER_PUNCT_CHARS = r".,'\"" "''':;!?()"
7
7
 
8
- WORD_PAT = (
9
- rf"[{OUTER_PUNCT_CHARS}]{{0,2}}[\w]+(?:[{INNER_PUNCT_CHARS}\w]+)*[{OUTER_PUNCT_CHARS}]{{0,2}}"
10
- )
11
- """
12
- Pattern to match a word in natural language text (i.e. words and natural
13
- language-only punctuation).
14
- """
8
+ ESCAPED_INNER_PUNCT_CHARS = re.escape(INNER_PUNCT_CHARS)
9
+ ESCAPED_OUTER_PUNCT_CHARS = re.escape(OUTER_PUNCT_CHARS)
15
10
 
16
- NL_PAT = rf"^{WORD_PAT}(?:\s+{WORD_PAT})*$"
17
- """
18
- Pattern to match natural language text in a command line.
19
- """
11
+ PUNCT_SEQ_RE = re.compile(rf"[{ESCAPED_INNER_PUNCT_CHARS}{ESCAPED_OUTER_PUNCT_CHARS}]+")
12
+
13
+ ONLY_WORDS_RE = re.compile(rf"^[\w\s{ESCAPED_INNER_PUNCT_CHARS}]*$")
14
+
15
+ PLAIN_WORD_RE = re.compile(r"^\w.*\w$")
20
16
 
21
17
 
22
18
  def as_nl_words(text: str) -> str:
@@ -30,18 +26,25 @@ def as_nl_words(text: str) -> str:
30
26
 
31
27
  def looks_like_nl(text: str) -> bool:
32
28
  """
33
- Check if a text looks like plain natural language text, i.e. word chars,
34
- possibly with ? or hyphens/apostrophes when inside words but not other
35
- code or punctuation.
29
+ Check if a text looks like plain natural language text. Just very simple
30
+ based on words and only basic punctuation.
36
31
  """
37
- return bool(re.match(NL_PAT, text.strip()))
32
+ is_only_word_chars = bool(ONLY_WORDS_RE.fullmatch(text))
33
+ without_punct = PUNCT_SEQ_RE.sub("", text)
34
+ is_only_words_punct = bool(ONLY_WORDS_RE.fullmatch(without_punct))
35
+ words = without_punct.strip().split()
36
+ one_longer_word = any(len(word) > 3 for word in words)
37
+
38
+ return one_longer_word and (
39
+ (is_only_words_punct and len(words) >= 3) or (is_only_word_chars and len(words) >= 2)
40
+ )
38
41
 
39
42
 
40
43
  ## Tests
41
44
 
42
45
 
43
46
  def test_as_nl_words():
44
- assert as_nl_words("x=3+9; foo('bar')") == "x=3+9; foo('bar"
47
+ assert as_nl_words("x=3+9; foo('bar')") == "x=3+9 foo('bar"
45
48
  assert as_nl_words("cd ..") == "cd .."
46
49
  assert as_nl_words("transcribe some-file_23.mp3") == "transcribe some-file_23.mp3"
47
50
  assert as_nl_words("hello world ") == "hello world"
@@ -57,14 +60,32 @@ def test_looks_like_nl():
57
60
  assert looks_like_nl("hello world")
58
61
  assert looks_like_nl(" hello world ")
59
62
  assert looks_like_nl("what's up")
60
- assert looks_like_nl("hello-world")
61
63
  assert looks_like_nl("is this a question?")
62
64
  assert looks_like_nl("'quoted text'")
63
65
  assert looks_like_nl("git push origin main")
66
+ assert looks_like_nl("this is natural language")
67
+ assert looks_like_nl(" what's up, doc? ")
68
+ assert looks_like_nl("multiple spaces here")
69
+ assert looks_like_nl("go to the store (buy milk)")
70
+ assert looks_like_nl("'quoted text' has three words")
71
+ assert looks_like_nl("git push origin main")
72
+ assert looks_like_nl("what's up")
64
73
 
74
+ assert not looks_like_nl("hello-world")
75
+ assert not looks_like_nl("cd ..")
76
+ assert not looks_like_nl("file_name.txt")
77
+ assert not looks_like_nl("ls -la")
78
+ assert not looks_like_nl("https://example.com")
79
+ assert not looks_like_nl("cmd | grep pattern")
80
+ assert not looks_like_nl("use a+b")
81
+ assert not looks_like_nl("x=3")
82
+ assert not looks_like_nl("a > b")
83
+ assert not looks_like_nl("file_name.txt")
84
+ assert not looks_like_nl("foo;")
85
+ assert not looks_like_nl("hello-world")
65
86
  assert not looks_like_nl("ls -la")
66
87
  assert not looks_like_nl("cd ..")
67
88
  assert not looks_like_nl("echo $HOME")
68
89
  assert not looks_like_nl("https://example.com")
69
- assert not looks_like_nl("file.txt")
90
+ assert not looks_like_nl(text="file.txt")
70
91
  assert not looks_like_nl("cmd | grep pattern")