kash-shell 0.3.25__py3-none-any.whl → 0.3.27__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 (46) hide show
  1. kash/actions/__init__.py +51 -6
  2. kash/actions/core/minify_html.py +2 -2
  3. kash/commands/base/general_commands.py +4 -2
  4. kash/commands/help/assistant_commands.py +4 -3
  5. kash/commands/help/welcome.py +1 -1
  6. kash/config/colors.py +7 -3
  7. kash/config/logger.py +4 -0
  8. kash/config/text_styles.py +1 -0
  9. kash/config/unified_live.py +249 -0
  10. kash/docs/markdown/assistant_instructions_template.md +3 -3
  11. kash/docs/markdown/topics/a1_what_is_kash.md +22 -20
  12. kash/docs/markdown/topics/a2_installation.md +10 -10
  13. kash/docs/markdown/topics/a3_getting_started.md +8 -8
  14. kash/docs/markdown/topics/a4_elements.md +3 -3
  15. kash/docs/markdown/topics/a5_tips_for_use_with_other_tools.md +12 -12
  16. kash/docs/markdown/topics/b0_philosophy_of_kash.md +17 -17
  17. kash/docs/markdown/topics/b1_kash_overview.md +7 -7
  18. kash/docs/markdown/topics/b2_workspace_and_file_formats.md +1 -1
  19. kash/docs/markdown/topics/b3_modern_shell_tool_recommendations.md +1 -1
  20. kash/docs/markdown/topics/b4_faq.md +7 -7
  21. kash/docs/markdown/welcome.md +1 -1
  22. kash/embeddings/embeddings.py +110 -39
  23. kash/embeddings/text_similarity.py +2 -2
  24. kash/exec/shell_callable_action.py +4 -3
  25. kash/help/help_embeddings.py +5 -2
  26. kash/mcp/mcp_server_sse.py +0 -5
  27. kash/model/graph_model.py +2 -0
  28. kash/model/items_model.py +4 -4
  29. kash/shell/output/shell_output.py +2 -2
  30. kash/shell/shell_main.py +64 -6
  31. kash/shell/version.py +18 -2
  32. kash/utils/file_utils/csv_utils.py +105 -0
  33. kash/utils/rich_custom/multitask_status.py +19 -5
  34. kash/web_gen/templates/base_styles.css.jinja +384 -31
  35. kash/web_gen/templates/base_webpage.html.jinja +43 -0
  36. kash/web_gen/templates/components/toc_styles.css.jinja +25 -4
  37. kash/web_gen/templates/components/tooltip_styles.css.jinja +2 -0
  38. kash/web_gen/templates/content_styles.css.jinja +23 -9
  39. kash/web_gen/templates/item_view.html.jinja +12 -4
  40. kash/web_gen/templates/simple_webpage.html.jinja +2 -2
  41. kash/xonsh_custom/custom_shell.py +6 -6
  42. {kash_shell-0.3.25.dist-info → kash_shell-0.3.27.dist-info}/METADATA +59 -56
  43. {kash_shell-0.3.25.dist-info → kash_shell-0.3.27.dist-info}/RECORD +46 -44
  44. {kash_shell-0.3.25.dist-info → kash_shell-0.3.27.dist-info}/WHEEL +0 -0
  45. {kash_shell-0.3.25.dist-info → kash_shell-0.3.27.dist-info}/entry_points.txt +0 -0
  46. {kash_shell-0.3.25.dist-info → kash_shell-0.3.27.dist-info}/licenses/LICENSE +0 -0
kash/actions/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from dataclasses import dataclass
2
+ from importlib import metadata
2
3
  from pathlib import Path
3
4
 
4
5
  from strif import AtomicVar
@@ -15,9 +16,11 @@ import_and_register(__package__, Path(__file__).parent, ["core", "meta"])
15
16
 
16
17
  @dataclass(frozen=True)
17
18
  class Kit:
18
- name: str
19
+ module_name: str
20
+ distribution_name: str
19
21
  full_module_name: str
20
22
  path: Path | None
23
+ version: str | None = None
21
24
 
22
25
 
23
26
  _kits: AtomicVar[dict[str, Kit]] = AtomicVar(initial_value={})
@@ -30,6 +33,44 @@ def get_loaded_kits() -> dict[str, Kit]:
30
33
  return _kits.copy()
31
34
 
32
35
 
36
+ def get_kit_distribution_name(module_name: str) -> str | None:
37
+ """
38
+ Guess the distribution name for a kit module using naming conventions.
39
+ Assumes kits follow patterns like:
40
+ - kash.kits.example_kit -> kash-example-kit
41
+ """
42
+ if not module_name.startswith("kash.kits."):
43
+ return None
44
+
45
+ kit_name = module_name.removeprefix("kash.kits.")
46
+
47
+ # Try common naming patterns
48
+ candidates = [
49
+ f"kash-{kit_name}",
50
+ f"kash-{kit_name.replace('_', '-')}",
51
+ ]
52
+
53
+ for candidate in candidates:
54
+ try:
55
+ metadata.version(candidate)
56
+ return candidate
57
+ except metadata.PackageNotFoundError:
58
+ continue
59
+
60
+ return None
61
+
62
+
63
+ def get_distribution_version(module_name: str) -> str | None:
64
+ """
65
+ Get the version of a module that can be used with `metadata.version()`.
66
+ """
67
+ try:
68
+ dist_name = get_kit_distribution_name(module_name)
69
+ return metadata.version(dist_name) if dist_name else None
70
+ except Exception:
71
+ return None
72
+
73
+
33
74
  def load_kits() -> dict[str, Kit]:
34
75
  """
35
76
  Import all kits (modules within `kash.kits`) by inspecting the namespace.
@@ -38,11 +79,15 @@ def load_kits() -> dict[str, Kit]:
38
79
  new_kits = {}
39
80
  try:
40
81
  imported = import_namespace_modules(kits_namespace)
41
- for name, module in imported.items():
42
- new_kits[name] = Kit(
43
- name,
44
- module.__name__,
45
- Path(module.__file__) if module.__file__ else None,
82
+ for module_name, module in imported.items():
83
+ dist_name = get_kit_distribution_name(module.__name__) or module.__name__
84
+
85
+ new_kits[module_name] = Kit(
86
+ module_name=module_name,
87
+ distribution_name=dist_name,
88
+ full_module_name=module.__name__,
89
+ path=Path(module.__file__) if module.__file__ else None,
90
+ version=metadata.version(dist_name),
46
91
  )
47
92
  except ImportError:
48
93
  log.info("No kits found in namespace `%s`", kits_namespace)
@@ -22,7 +22,7 @@ def minify_html(item: Item) -> Item:
22
22
  The terser minification seems a bit slower but more robust than
23
23
  [minify-html](https://github.com/wilsonzlin/minify-html).
24
24
  """
25
- from minify_tw_html import minify_tw_html
25
+ from tminify.main import tminify
26
26
 
27
27
  if not item.store_path:
28
28
  raise InvalidInput(f"Missing store path: {item}")
@@ -33,7 +33,7 @@ def minify_html(item: Item) -> Item:
33
33
  output_item = item.derived_copy(format=Format.html, body=None)
34
34
  output_path = ws.target_path_for(output_item)
35
35
 
36
- minify_tw_html(input_path, output_path)
36
+ tminify(input_path, output_path)
37
37
 
38
38
  output_item.body = output_path.read_text()
39
39
  output_item.external_path = str(output_path) # Indicate item is already saved.
@@ -39,7 +39,7 @@ def version() -> None:
39
39
  """
40
40
  Show the version of kash.
41
41
  """
42
- cprint(get_full_version_name())
42
+ cprint(get_full_version_name(with_kits=True))
43
43
 
44
44
 
45
45
  @kash_command
@@ -205,7 +205,9 @@ def kits() -> None:
205
205
  cprint("Currently imported kits:")
206
206
  for kit in get_loaded_kits().values():
207
207
  cprint(
208
- format_name_and_value(f"{kit.name} kit", str(kit.path or ""), text_wrap=Wrap.NONE)
208
+ format_name_and_value(
209
+ f"{kit.distribution_name} kit", str(kit.path or ""), text_wrap=Wrap.NONE
210
+ )
209
211
  )
210
212
 
211
213
 
@@ -1,7 +1,8 @@
1
1
  from kash.commands.base.basic_file_commands import trash
2
2
  from kash.commands.workspace.selection_commands import select
3
- from kash.config.logger import get_console, get_logger
4
- from kash.config.text_styles import PROMPT_ASSIST, SPINNER
3
+ from kash.config.logger import get_logger
4
+ from kash.config.text_styles import PROMPT_ASSIST
5
+ from kash.config.unified_live import get_unified_live
5
6
  from kash.docs.all_docs import DocSelection
6
7
  from kash.exec import kash_command
7
8
  from kash.exec_model.shell_model import ShellResult
@@ -43,7 +44,7 @@ def assist(
43
44
  help()
44
45
  return
45
46
 
46
- with get_console().status("Thinking…", spinner=SPINNER): # noqa: F821
47
+ with get_unified_live().status("Thinking…"):
47
48
  shell_context_assistance(input, model=model, assistance_type=type)
48
49
 
49
50
 
@@ -31,4 +31,4 @@ def welcome() -> None:
31
31
  )
32
32
  )
33
33
  cprint(Panel(Markdown(help_topics.warning), box=SQUARE, border_style=COLOR_HINT))
34
- cprint("%s", get_full_version_name())
34
+ cprint("%s", get_full_version_name(with_kits=True))
kash/config/colors.py CHANGED
@@ -135,15 +135,17 @@ web_light_translucent = SimpleNamespace(
135
135
  secondary=hsl_to_hex("hsl(188, 12%, 28%)"),
136
136
  tertiary=hsl_to_hex("hsl(188, 7%, 64%)"),
137
137
  bg=hsl_to_hex("hsla(44, 6%, 100%, 0.75)"),
138
- bg_solid=hsl_to_hex("hsla(44, 6%, 100%, 1)"),
138
+ bg_solid=hsl_to_hex("hsl(44, 6%, 100%)"),
139
139
  bg_header=hsl_to_hex("hsla(188, 42%, 70%, 0.2)"),
140
140
  bg_alt=hsl_to_hex("hsla(39, 24%, 90%, 0.3)"),
141
- bg_alt_solid=hsl_to_hex("hsla(39, 24%, 97%, 1)"),
142
- bg_meta_solid=hsl_to_hex("hsla(39, 24%, 94%, 1)"),
141
+ bg_alt_solid=hsl_to_hex("hsl(39, 24%, 97%)"),
142
+ bg_meta_solid=hsl_to_hex("hsl(39, 24%, 94%)"),
143
+ bg_strong_solid=hsl_to_hex("hsl(39, 8%, 90%)"),
143
144
  bg_selected=hsl_to_hex("hsla(188, 21%, 94%, 0.9)"),
144
145
  text=hsl_to_hex("hsl(188, 39%, 11%)"),
145
146
  code=hsl_to_hex("hsl(44, 38%, 23%)"),
146
147
  border=hsl_to_hex("hsl(188, 8%, 50%)"),
148
+ border_hairline=hsl_to_hex("hsl(188, 2%, 34%)"),
147
149
  border_hint=hsl_to_hex("hsla(188, 8%, 72%, 0.3)"),
148
150
  border_accent=hsl_to_hex("hsla(305, 18%, 65%, 0.85)"),
149
151
  hover=hsl_to_hex("hsl(188, 12%, 84%)"),
@@ -174,10 +176,12 @@ web_dark_translucent = SimpleNamespace(
174
176
  bg_alt=hsl_to_hex("hsla(220, 14%, 12%, 0.5)"),
175
177
  bg_alt_solid=hsl_to_hex("hsl(220, 15%, 16%)"),
176
178
  bg_meta_solid=hsl_to_hex("hsl(220, 14%, 25%)"),
179
+ bg_strong_solid=hsl_to_hex("hsl(220, 14%, 35%)"),
177
180
  bg_selected=hsl_to_hex("hsla(188, 13%, 33%, 0.95)"),
178
181
  text=hsl_to_hex("hsl(188, 10%, 90%)"),
179
182
  code=hsl_to_hex("hsl(44, 38%, 72%)"),
180
183
  border=hsl_to_hex("hsl(188, 8%, 25%)"),
184
+ border_hairline=hsl_to_hex("hsl(188, 2%, 80%)"),
181
185
  border_hint=hsl_to_hex("hsla(188, 8%, 35%, 0.3)"),
182
186
  border_accent=hsl_to_hex("hsla(305, 30%, 55%, 0.85)"),
183
187
  hover=hsl_to_hex("hsl(188, 12%, 35%)"),
kash/config/logger.py CHANGED
@@ -170,6 +170,7 @@ def reset_rich_logging(
170
170
  the `.log` extension. If `log_path` is provided, it will be used to infer
171
171
  the log root and name.
172
172
  """
173
+ _init_rich_logging()
173
174
  if log_path:
174
175
  if not log_path.parent.exists():
175
176
  log_path.parent.mkdir(parents=True, exist_ok=True)
@@ -206,6 +207,9 @@ def reload_rich_logging_setup():
206
207
 
207
208
  @cache
208
209
  def _init_rich_logging():
210
+ """
211
+ One-time idempotent setup of rich logging.
212
+ """
209
213
  rich.reconfigure(theme=get_theme(), highlighter=get_highlighter())
210
214
 
211
215
  logging.setLoggerClass(CustomLogger)
@@ -57,6 +57,7 @@ COLOR_PLAIN = "default"
57
57
  COLOR_LINK = "cyan"
58
58
  COLOR_SUCCESS = "green"
59
59
  COLOR_FAILURE = "bright_red"
60
+ COLOR_SPINNER = "bright_cyan"
60
61
  COLOR_WARN = "bright_red"
61
62
  COLOR_ERROR = "bright_red"
62
63
  COLOR_EXTRA = "bright_blue"
@@ -0,0 +1,249 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import threading
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
7
+ from dataclasses import dataclass, field
8
+
9
+ from rich.console import Console, Group, RenderableType
10
+ from rich.live import Live
11
+ from rich.spinner import Spinner
12
+ from rich.text import Text
13
+ from strif import AtomicVar
14
+
15
+ from kash.config.logger import get_console
16
+ from kash.config.text_styles import COLOR_SPINNER, SPINNER
17
+
18
+
19
+ @dataclass
20
+ class LiveContent:
21
+ """
22
+ Container for different types of live-updating status content.
23
+ """
24
+
25
+ current_status: str | None = None # Single current status message
26
+ multitask_display: RenderableType | None = None
27
+ custom_content: list[RenderableType] = field(default_factory=list)
28
+
29
+
30
+ class UnifiedLive:
31
+ """
32
+ Unified live display manager that handles all Rich live content in a single Live container.
33
+
34
+ This eliminates Rich's one-live-display-at-a-time limitation by
35
+ providing a single Live that all other components render into.
36
+
37
+ Layout structure:
38
+ - Status message at the top (single spinner and message)
39
+ - MultiTask progress displays in the middle
40
+ - Custom live content at the bottom
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ *,
46
+ console: Console | None = None,
47
+ transient: bool = True,
48
+ refresh_per_second: float = 10,
49
+ ):
50
+ self.console = console or get_console()
51
+ self._content = LiveContent()
52
+ self._live = Live(
53
+ console=self.console,
54
+ transient=transient,
55
+ refresh_per_second=refresh_per_second,
56
+ )
57
+ self._is_active = False
58
+ self._lock = threading.RLock() # Thread safety for mutable state
59
+ self._usage_count = 0 # Track how many things are using this live display
60
+
61
+ def start(self) -> None:
62
+ """Start the unified live display."""
63
+ with self._lock:
64
+ if self._is_active:
65
+ return
66
+
67
+ try:
68
+ self._live.__enter__()
69
+ self._is_active = True
70
+ self._update_display()
71
+ except Exception:
72
+ # If starting fails, ensure we're in a clean state
73
+ self._is_active = False
74
+ raise
75
+
76
+ def stop(self) -> None:
77
+ """Stop the unified live display and restore terminal state."""
78
+ with self._lock:
79
+ if not self._is_active:
80
+ return
81
+
82
+ self._is_active = False
83
+ try:
84
+ self._live.__exit__(None, None, None)
85
+ except Exception:
86
+ # Always try to restore terminal state even if Live cleanup fails
87
+ pass
88
+ finally:
89
+ # Force terminal state restoration
90
+ try:
91
+ # Ensure cursor is visible and terminal is in normal state
92
+ self.console.show_cursor()
93
+ if hasattr(self.console, "_buffer"):
94
+ self.console._buffer.clear()
95
+ except Exception:
96
+ pass
97
+
98
+ def _increment_usage(self) -> None:
99
+ """Increment usage counter (called when entering a context)."""
100
+ with self._lock:
101
+ self._usage_count += 1
102
+
103
+ def _decrement_usage(self) -> None:
104
+ """Decrement usage counter and stop if unused (called when exiting a context)."""
105
+ with self._lock:
106
+ self._usage_count = max(0, self._usage_count - 1)
107
+ # Auto-stop if nothing is using it and it has content that would be cleared anyway
108
+ if self._usage_count == 0 and self._content.current_status is None:
109
+ self.stop()
110
+
111
+ def set_status(self, message: str | None) -> None:
112
+ """Set the current status message (or None to clear it)."""
113
+ with self._lock:
114
+ self._content.current_status = message
115
+ self._update_display()
116
+
117
+ @contextmanager
118
+ def status(self, message: str, *, spinner: str = SPINNER) -> Generator[None, None, None]: # pyright: ignore[reportUnusedParameter]
119
+ """
120
+ Context manager for showing a status message in this unified live display.
121
+
122
+ Args:
123
+ message: Status message to display
124
+ spinner: Spinner type (for future animation support)
125
+ """
126
+ self._increment_usage()
127
+ self.set_status(message)
128
+ try:
129
+ yield
130
+ finally:
131
+ self.set_status(None)
132
+ self._decrement_usage()
133
+
134
+ def set_multitask_display(self, display: RenderableType | None) -> None:
135
+ """Set the multitask progress display content."""
136
+ with self._lock:
137
+ self._content.multitask_display = display
138
+ self._update_display()
139
+
140
+ def add_custom_content(self, content: RenderableType) -> int:
141
+ """Add custom live content. Returns an ID for later removal."""
142
+ with self._lock:
143
+ self._content.custom_content.append(content)
144
+ self._update_display()
145
+ return len(self._content.custom_content) - 1
146
+
147
+ def remove_custom_content(self, content_id: int) -> None:
148
+ """Remove custom content by ID."""
149
+ with self._lock:
150
+ if 0 <= content_id < len(self._content.custom_content):
151
+ del self._content.custom_content[content_id]
152
+ self._update_display()
153
+
154
+ def _update_display(self) -> None:
155
+ """Update the live display with current content. Must be called with lock held."""
156
+ if not self._is_active:
157
+ return
158
+
159
+ renderables: list[RenderableType] = []
160
+
161
+ # Add multitask display at the top
162
+ if self._content.multitask_display is not None:
163
+ renderables.append(self._content.multitask_display)
164
+
165
+ # Add custom content in the middle
166
+ renderables.extend(self._content.custom_content)
167
+
168
+ # Add current status message with animated spinner at the bottom
169
+ if self._content.current_status is not None:
170
+ from rich.columns import Columns
171
+
172
+ spinner = Spinner(SPINNER, style=COLOR_SPINNER)
173
+ status_text = Text(self._content.current_status)
174
+
175
+ # Use Columns to display spinner and message side by side
176
+ status_line = Columns([spinner, status_text], padding=(0, 1))
177
+ renderables.append(status_line)
178
+
179
+ # Update the live display - use Group to stack vertically
180
+ if renderables:
181
+ self._live.update(Group(*renderables))
182
+ else:
183
+ # Show empty space if no content
184
+ self._live.update("")
185
+
186
+ @property
187
+ def is_active(self) -> bool:
188
+ """Check if this unified live is currently active."""
189
+ with self._lock:
190
+ return self._is_active
191
+
192
+
193
+ # Global unified live instance, auto-initialized on first access (thread-safe)
194
+ _global_unified_live = AtomicVar[UnifiedLive | None](None)
195
+
196
+
197
+ def _cleanup_unified_live() -> None:
198
+ """Clean up the global unified live display on process exit."""
199
+ current = _global_unified_live.value
200
+ if current is not None:
201
+ current.stop()
202
+
203
+
204
+ # Register cleanup handler for normal exit only
205
+ atexit.register(_cleanup_unified_live)
206
+
207
+
208
+ def get_unified_live() -> UnifiedLive:
209
+ """
210
+ Get the global unified live display, auto-initializing if needed.
211
+
212
+ Always returns a valid UnifiedLive instance. Creates and starts one
213
+ automatically if none exists yet. Thread-safe using AtomicVar.
214
+ """
215
+ with _global_unified_live.lock:
216
+ if not _global_unified_live:
217
+ live = UnifiedLive()
218
+ live.start()
219
+ _global_unified_live.set(live)
220
+
221
+ result = _global_unified_live.value
222
+ assert result
223
+ return result
224
+
225
+
226
+ def has_unified_live() -> bool:
227
+ """Check if there's currently an active unified live display."""
228
+ current = _global_unified_live.value
229
+ return current is not None and current.is_active
230
+
231
+
232
+ @contextmanager
233
+ def unified_live_context(
234
+ console: Console | None = None,
235
+ ) -> Generator[UnifiedLive, None, None]:
236
+ """
237
+ Context manager for working with the unified live display.
238
+
239
+ Returns the global unified live instance, creating and starting it if needed.
240
+ The live display continues running after this context exits.
241
+ """
242
+ # Always return the global instance, creating if needed
243
+ live = get_unified_live()
244
+
245
+ # Update settings if this is a new instance
246
+ if console is not None:
247
+ live.console = console
248
+
249
+ yield live
@@ -5,7 +5,7 @@ organizing knowledge.
5
5
  Kash can be used as a shell, with access to common commands like `ps` and `cd`, but has
6
6
  far more capabilities and can generate and manipulate text documents, videos, and more.
7
7
 
8
- Kash is written in Python, runs on a user's own computer.
8
+ Kash is written in Python, runs on a users own computer.
9
9
  It can connect to the web to download or read content or use LLM-based tools and APIs
10
10
  such as ones from OpenAI or Anthropic.
11
11
  It saves all content and state to files.
@@ -69,7 +69,7 @@ necessary.
69
69
 
70
70
  Always follow these guidelines:
71
71
 
72
- - If you're unsure of what command might help, simply say "I'm not sure how to help with
72
+ - If youre unsure of what command might help, simply say "Im not sure how to help with
73
73
  that. Run `help` for more about kash.`" Suggest the user run `help` to get more
74
74
  information themselves.
75
75
 
@@ -81,7 +81,7 @@ Always follow these guidelines:
81
81
 
82
82
  - If there is more than one command that might be relevant, mention all the commands
83
83
  that might be of interest.
84
- Don't repeatedly mention the same command.
84
+ Dont repeatedly mention the same command.
85
85
  Be brief!
86
86
 
87
87
  - If they ask for a task that is not covered by the current set of actions, you may
@@ -8,15 +8,15 @@ exploratory, and flexible using Python and current AI tools.
8
8
 
9
9
  The philosophy behind kash is similar to Unix shell tools: simple commands that can be
10
10
  combined in flexible and powerful ways.
11
- It operates on "items" such as URLs, files, or Markdown notes within a workspace
11
+ It operates on items such as URLs, files, or Markdown notes within a workspace
12
12
  directory.
13
13
 
14
14
  You can use Kash as an **interactive, AI-native command-line** shell for practical
15
- knowledge tasks. It's also **a Python library** that lets you convert a simple Python
15
+ knowledge tasks. Its also **a Python library** that lets you convert a simple Python
16
16
  function into a command and an MCP tool, so it integrates with other tools like
17
17
  Anthropic Desktop or Cursor.
18
18
 
19
- It's new and still has some rough edges, but it's now working well enough it is feeling
19
+ Its new and still has some rough edges, but its now working well enough it is feeling
20
20
  quite powerful. It now serves as a replacement for my usual shell (previously bash or
21
21
  zsh). I use it routinely to remix, combine, and interactively explore and then gradually
22
22
  automate complex tasks by composing AI tools, APIs, and libraries.
@@ -48,19 +48,20 @@ quick to install via uv.
48
48
  context of a **workspace**. A workspace is just a directory of files that have a few
49
49
  conventions to make it easier to maintain context and perform actions.
50
50
  A bit like how Git repos work, it has a `.kash/` directory that holds metadata and
51
- cached content. The rest can be anything, but is typically directories of resources
52
- (like .docx or .pdf or links to web pages) or content, typically Markdown files with
53
- YAML frontmatter. All text files use
54
- [frontmatter-format](https://github.com/jlevy/frontmatter-format) so have easy-to-read
55
- YAML metadata that includes not just title or description, but also the names of the
56
- actions that created it.
51
+ cached content. The rest can be anything, but is typically directories of content and
52
+ resources (often Markdown or HTML but also .docx or .pdf or links to web pages).
53
+ All text files use [frontmatter-format](https://github.com/jlevy/frontmatter-format)
54
+ so have YAML metadata that includes not just title or description, but also how it was
55
+ created. All Markdown files are auto-formatted with
56
+ [flowmark](https://github.com/jlevy/flowmark), which makes documents much easier to
57
+ diff and version control (and prettier to read and edit).
57
58
 
58
59
  - **Compositionality:** An action is composable with other actions simply as a Python
59
60
  function, so complex operations (for example, transcribing and annotating a video and
60
61
  publishing it on a website) actions can be built from simpler actions (say downloading
61
62
  and caching a YouTube video, identifying the speakers in a transcript, formatting it
62
- as pretty HTML, etc.). The goal is to reduce the "interstitial complexity" of
63
- combining tools, so it's easy for you (or an LLM!) to combine tools in flexible and
63
+ as pretty HTML, etc.). The goal is to reduce the interstitial complexity of
64
+ combining tools, so its easy for you (or an LLM!) to combine tools in flexible and
64
65
  powerful ways.
65
66
 
66
67
  - **Command-line usage:** In addition to using the function in other libraries and
@@ -87,16 +88,16 @@ transcripts and notes, write blog posts, extract or visualize concepts, check ci
87
88
  convert notes to PDFs or beautifully formatted HTML, or perform numerous other
88
89
  content-related tasks possible by orchestrating AI tools in the right ways.
89
90
 
90
- As I've been building kash over the past couple months, I found I've found it's not only
91
+ As Ive been building kash over the past couple months, I found Ive found its not only
91
92
  faster to do complex things, but that it has also become replacement for my usual shell.
92
- It's the power-tool I want to use alongside Cursor and ChatGPT/Claude.
93
+ Its the power-tool I want to use alongside Cursor and ChatGPT/Claude.
93
94
  We all know and trust shells like bash, zsh, and fish, but now I find this is much more
94
95
  powerful for everyday usage.
95
96
  It has little niceties, like you can just type `files` for a better listing of files or
96
97
  `show` and it will show you a file the right way, no matter what kind of file it is.
97
- You can also type something like "? find md files" and press tab and it will list you I
98
+ You can also type something like “? find md files and press tab and it will list you I
98
99
  find it is much more powerful for local usage than than bash/zsh/fish.
99
- If you're a command-line nerd, you might like it a lot.
100
+ If youre a command-line nerd, you might like it a lot.
100
101
 
101
102
  But my hope is that with these enhancements, the shell is also far more friendly and
102
103
  usable by anyone reasonably technical, and does not feel so esoteric as a typical Unix
@@ -105,16 +106,17 @@ shell.
105
106
  Finally, one more thing: Kash is also my way of experimenting with something else new: a
106
107
  **terminal GUI support** that adds GUI features terminal like clickable text, buttons,
107
108
  tooltips, and popovers in the terminal.
108
- I've separately built a new desktop terminal app, Kerm, which adds support for a simple
109
- "Kerm codes" protocol for such visual components, encoded as OSC codes then rendered in
109
+ Ive separately built a new desktop terminal app, Kerm, which adds support for a simple
110
+ Kerm codes protocol for such visual components, encoded as OSC codes then rendered in
110
111
  the terminal. Because Kash supports these codes, as this develops you will get the
111
112
  visuals of a web app layered on the flexibility of a text-based terminal.
112
113
 
113
114
  ### Is Kash Mature?
114
115
 
115
- No. :) It's the result of a couple months of coding and experimentation, and it's very
116
- much in progress. Please help me make it better by sharing your ideas and feedback!
117
- It's easiest to DM me at [twitter.com/ojoshe](https://x.com/ojoshe).
116
+ Its the result of a couple months of coding and experimentation, and its still in
117
+ progress and has rough edges.
118
+ Please help me make it better by sharing your ideas and feedback!
119
+ It’s easiest to DM me at [twitter.com/ojoshe](https://x.com/ojoshe).
118
120
  My contact info is at [github.com/jlevy](https://github.com/jlevy).
119
121
 
120
122
  [**Please follow or DM me**](https://x.com/ojoshe) for future updates or if you have
@@ -4,7 +4,7 @@
4
4
 
5
5
  Kash offers a shell environment based on [xonsh](https://xon.sh/) augmented with a bunch
6
6
  of enhanced commands and customizations.
7
- If you've used a bash or Python shell before, it should be very intuitive.
7
+ If youve used a bash or Python shell before, it should be very intuitive.
8
8
 
9
9
  Within the kash shell, you get a full environment with all actions and commands.
10
10
  You also get intelligent auto-complete, a built-in assistant to help you perform tasks,
@@ -13,7 +13,7 @@ and enhanced tab completion.
13
13
  The shell is an easy way to use Kash actions, simply calling them like other shell
14
14
  commands from the command line.
15
15
 
16
- But remember that's just one way to use actions; you can also use them directly in
16
+ But remember thats just one way to use actions; you can also use them directly in
17
17
  Python or from an MCP client.
18
18
 
19
19
  ### Current Kash Packages
@@ -23,7 +23,7 @@ However, some use cases require additional libraries, like video downloading too
23
23
  handling, etc.
24
24
 
25
25
  To keep kash dependencies more manageable, these additional utilities and actions are
26
- packaged additional "kits".
26
+ packaged additional kits”.
27
27
 
28
28
  The examples below use video transcription from YouTube as an example.
29
29
  To start with more full examples, I suggest starting with the `kash-media` kit.
@@ -38,7 +38,7 @@ These are for `kash-media` but you can use a `kash-shell` for a more basic setup
38
38
  Kash is easiest to use via [**uv**](https://docs.astral.sh/uv/), the new package
39
39
  manager for Python. `uv` replaces traditional use of `pyenv`, `pipx`, `poetry`, `pip`,
40
40
  etc. Installing `uv` also ensures you get a compatible version of Python.
41
- See [uv's docs](https://docs.astral.sh/uv/getting-started/installation/) for other
41
+ See [uvs docs](https://docs.astral.sh/uv/getting-started/installation/) for other
42
42
  installation methods and platforms.
43
43
  Usually you just want to run:
44
44
  ```shell
@@ -47,7 +47,7 @@ These are for `kash-media` but you can use a `kash-shell` for a more basic setup
47
47
 
48
48
  2. **Install additional command-line tools:**
49
49
 
50
- In addition to Python, it's highly recommended to install a few other dependencies to
50
+ In addition to Python, its highly recommended to install a few other dependencies to
51
51
  make more tools and commands work.
52
52
  For macOS, you can again use brew:
53
53
 
@@ -104,7 +104,7 @@ These are for `kash-media` but you can use a `kash-shell` for a more basic setup
104
104
  ```
105
105
 
106
106
  These keys should go in the `.env` file in your current work directory or a parent or
107
- your home directory (recommended if you'll be working in several directories, as with
107
+ your home directory (recommended if youll be working in several directories, as with
108
108
  a typical shell).
109
109
 
110
110
  5. **Run kash:**
@@ -144,14 +144,14 @@ If you add the `--proxy` arg, it will run an MCP stdio server but connect to the
144
144
  server you are running in the kash shell, by default at `localhost:4440`.
145
145
 
146
146
  Then if you run `start_mcp_server` from the shell, your client will connect to your
147
- shell, and you can actually use any "published" kash action as an MCP tool.
147
+ shell, and you can actually use any published kash action as an MCP tool.
148
148
 
149
- Then you can for example ask your MCP client "can you transcribe this video?"
149
+ Then you can for example ask your MCP client can you transcribe this video?”
150
150
  and give it a URL, and it will be able to call the `transcribe` action as a tool.
151
151
 
152
152
  What is even better is that all the inputs and outputs are saved in the current kash
153
- workspace, just as if you'd been running these commands yourself in the shell.
154
- This way, you don't lose context or any work, and can seamlessly switch between an MCP
153
+ workspace, just as if youd been running these commands yourself in the shell.
154
+ This way, you dont lose context or any work, and can seamlessly switch between an MCP
155
155
  client like Cursor, the shell, and any other tools to edit the inputs or outputs of
156
156
  actions in your workspace directory.
157
157