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.
- kash/actions/__init__.py +51 -6
- kash/actions/core/minify_html.py +2 -2
- kash/commands/base/general_commands.py +4 -2
- kash/commands/help/assistant_commands.py +4 -3
- kash/commands/help/welcome.py +1 -1
- kash/config/colors.py +7 -3
- kash/config/logger.py +4 -0
- kash/config/text_styles.py +1 -0
- kash/config/unified_live.py +249 -0
- kash/docs/markdown/assistant_instructions_template.md +3 -3
- kash/docs/markdown/topics/a1_what_is_kash.md +22 -20
- kash/docs/markdown/topics/a2_installation.md +10 -10
- kash/docs/markdown/topics/a3_getting_started.md +8 -8
- kash/docs/markdown/topics/a4_elements.md +3 -3
- kash/docs/markdown/topics/a5_tips_for_use_with_other_tools.md +12 -12
- kash/docs/markdown/topics/b0_philosophy_of_kash.md +17 -17
- kash/docs/markdown/topics/b1_kash_overview.md +7 -7
- kash/docs/markdown/topics/b2_workspace_and_file_formats.md +1 -1
- kash/docs/markdown/topics/b3_modern_shell_tool_recommendations.md +1 -1
- kash/docs/markdown/topics/b4_faq.md +7 -7
- kash/docs/markdown/welcome.md +1 -1
- kash/embeddings/embeddings.py +110 -39
- kash/embeddings/text_similarity.py +2 -2
- kash/exec/shell_callable_action.py +4 -3
- kash/help/help_embeddings.py +5 -2
- kash/mcp/mcp_server_sse.py +0 -5
- kash/model/graph_model.py +2 -0
- kash/model/items_model.py +4 -4
- kash/shell/output/shell_output.py +2 -2
- kash/shell/shell_main.py +64 -6
- kash/shell/version.py +18 -2
- kash/utils/file_utils/csv_utils.py +105 -0
- kash/utils/rich_custom/multitask_status.py +19 -5
- kash/web_gen/templates/base_styles.css.jinja +384 -31
- kash/web_gen/templates/base_webpage.html.jinja +43 -0
- kash/web_gen/templates/components/toc_styles.css.jinja +25 -4
- kash/web_gen/templates/components/tooltip_styles.css.jinja +2 -0
- kash/web_gen/templates/content_styles.css.jinja +23 -9
- kash/web_gen/templates/item_view.html.jinja +12 -4
- kash/web_gen/templates/simple_webpage.html.jinja +2 -2
- kash/xonsh_custom/custom_shell.py +6 -6
- {kash_shell-0.3.25.dist-info → kash_shell-0.3.27.dist-info}/METADATA +59 -56
- {kash_shell-0.3.25.dist-info → kash_shell-0.3.27.dist-info}/RECORD +46 -44
- {kash_shell-0.3.25.dist-info → kash_shell-0.3.27.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.25.dist-info → kash_shell-0.3.27.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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)
|
kash/actions/core/minify_html.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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
|
|
4
|
-
from kash.config.text_styles import PROMPT_ASSIST
|
|
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
|
|
47
|
+
with get_unified_live().status("Thinking…"):
|
|
47
48
|
shell_context_assistance(input, model=model, assistance_type=type)
|
|
48
49
|
|
|
49
50
|
|
kash/commands/help/welcome.py
CHANGED
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("
|
|
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("
|
|
142
|
-
bg_meta_solid=hsl_to_hex("
|
|
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)
|
kash/config/text_styles.py
CHANGED
|
@@ -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
|
|
8
|
+
Kash is written in Python, runs on a user’s 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
|
|
72
|
+
- If you’re unsure of what command might help, simply say "I’m 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
|
|
84
|
+
Don’t 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
|
|
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
|
|
15
|
+
knowledge tasks. It’s 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
|
|
19
|
+
It’s new and still has some rough edges, but it’s 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
|
|
52
|
-
(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
63
|
-
combining tools, so it
|
|
63
|
+
as pretty HTML, etc.). The goal is to reduce the “interstitial complexity” of
|
|
64
|
+
combining tools, so it’s 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
|
|
91
|
+
As I’ve been building kash over the past couple months, I found I’ve found it’s not only
|
|
91
92
|
faster to do complex things, but that it has also become replacement for my usual shell.
|
|
92
|
-
It
|
|
93
|
+
It’s 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
|
|
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
|
|
100
|
+
If you’re 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
|
|
109
|
-
|
|
109
|
+
I’ve 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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
It’s the result of a couple months of coding and experimentation, and it’s 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
|
|
7
|
+
If you’ve 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
|
|
16
|
+
But remember that’s 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
|
|
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
|
|
41
|
+
See [uv’s 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
|
|
50
|
+
In addition to Python, it’s 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
|
|
107
|
+
your home directory (recommended if you’ll 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
|
|
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
|
|
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
|
|
154
|
-
This way, you don
|
|
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
|
|
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
|
|