kash-shell 0.3.35__py3-none-any.whl → 0.3.37__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.
@@ -29,6 +29,7 @@ def windowed_llm_transform(
29
29
  windowing: WindowSettings | None,
30
30
  diff_filter: DiffFilter | None = None,
31
31
  check_no_results: bool = True,
32
+ enable_web_search: bool = False,
32
33
  ) -> TextDoc:
33
34
  def doc_transform(input_doc: TextDoc) -> TextDoc:
34
35
  return TextDoc.from_text(
@@ -41,6 +42,7 @@ def windowed_llm_transform(
41
42
  input=input_doc.reassemble(),
42
43
  body_template=template,
43
44
  check_no_results=check_no_results,
45
+ enable_web_search=enable_web_search,
44
46
  ).content
45
47
  )
46
48
  )
@@ -67,6 +69,7 @@ def llm_transform_str(options: LLMOptions, input_str: str, check_no_results: boo
67
69
  input_str,
68
70
  options.windowing,
69
71
  diff_filter=options.diff_filter,
72
+ enable_web_search=options.enable_web_search,
70
73
  ).reassemble()
71
74
  else:
72
75
  log.info(
@@ -81,6 +84,7 @@ def llm_transform_str(options: LLMOptions, input_str: str, check_no_results: boo
81
84
  body_template=options.body_template,
82
85
  input=input_str,
83
86
  check_no_results=check_no_results,
87
+ enable_web_search=options.enable_web_search,
84
88
  ).content
85
89
 
86
90
  return result_str
@@ -485,6 +485,9 @@ class FileStore(Workspace):
485
485
  If `with_sidematter` is true, will copy any sidematter files (metadata/assets) to
486
486
  the destination.
487
487
  """
488
+ # TODO: Make sure importing a text item that already has
489
+ # frontmatter doesn't accidentally duplicate the frontmatter
490
+
488
491
  from kash.file_storage.item_file_format import read_item
489
492
  from kash.web_content.canon_url import canonicalize_url
490
493
 
@@ -531,6 +534,7 @@ class FileStore(Workspace):
531
534
  # This will read the file with or without frontmatter.
532
535
  # We are importing so we want to drop the external path so we save the body.
533
536
  item = read_item(path, self.base_dir)
537
+ log.info("Imported text item: %s", item)
534
538
  item.external_path = None
535
539
 
536
540
  if item.type and as_type and item.type != as_type:
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import time
4
4
  from dataclasses import dataclass
5
- from typing import TYPE_CHECKING, cast
5
+ from typing import TYPE_CHECKING, Any, cast
6
6
 
7
7
  from flowmark import Wrap, fill_text
8
8
  from funlog import format_duration, log_calls
@@ -51,6 +51,7 @@ class LLMCompletionResult:
51
51
  message: LiteLLMMessage
52
52
  content: str
53
53
  citations: CitationList | None
54
+ tool_calls: list[dict[str, Any]] | None = None
54
55
 
55
56
  @property
56
57
  def content_with_citations(self) -> str:
@@ -59,40 +60,111 @@ class LLMCompletionResult:
59
60
  content = content + "\n\n" + self.citations.as_markdown_footnotes()
60
61
  return content
61
62
 
63
+ @property
64
+ def has_tool_calls(self) -> bool:
65
+ """Check if the response contains tool calls."""
66
+ return bool(self.tool_calls)
67
+
68
+ @property
69
+ def tool_call_names(self) -> list[str]:
70
+ """Get list of tool names that were called."""
71
+ if not self.tool_calls:
72
+ return []
73
+ names = []
74
+ for call in self.tool_calls:
75
+ # Handle both LiteLLM objects and dict representations
76
+ if hasattr(call, "function") and hasattr(getattr(call, "function", None), "name"):
77
+ # LiteLLM object format
78
+ names.append(f"{call.function.name}()") # pyright: ignore[reportAttributeAccessIssue]
79
+ elif isinstance(call, dict) and call.get("function", {}).get("name"):
80
+ # Dict format
81
+ names.append(f"{call['function']['name']}()")
82
+ else:
83
+ names.append(str(call))
84
+ return names
85
+
62
86
 
63
87
  @log_calls(level="info")
64
88
  def llm_completion(
65
89
  model: LLMName,
66
90
  messages: list[dict[str, str]],
67
91
  save_objects: bool = True,
68
- response_format: dict | type[BaseModel] | None = None,
92
+ response_format: dict[str, Any] | type[BaseModel] | None = None,
93
+ tools: list[dict[str, Any]] | None = None,
94
+ enable_web_search: bool = False,
69
95
  **kwargs,
70
96
  ) -> LLMCompletionResult:
71
97
  """
72
98
  Perform an LLM completion with LiteLLM.
99
+
100
+ Args:
101
+ model: The LLM model to use
102
+ messages: Chat messages
103
+ save_objects: Whether to save chat history
104
+ response_format: Response format specification
105
+ tools: List of tools available for function calling (e.g., web_search)
106
+ enable_web_search: If True, automatically add web search tools for the model
107
+ **kwargs: Additional LiteLLM parameters
73
108
  """
74
- import litellm
75
109
  from litellm.types.utils import Choices, ModelResponse
76
110
 
77
111
  init_litellm()
78
112
 
113
+ # Prepare completion parameters
114
+ completion_params = {
115
+ "model": model.litellm_name,
116
+ "messages": messages,
117
+ **kwargs,
118
+ }
119
+
120
+ # Auto-enable web search if requested
121
+ if enable_web_search:
122
+ import litellm
123
+
124
+ if litellm.supports_web_search(model=model.litellm_name):
125
+ log.message("Enabling web search for model %s", model.litellm_name)
126
+ completion_params["web_search_options"] = {"search_context_size": "medium"}
127
+ else:
128
+ log.warning("Web search requested but not supported by model %s", model.litellm_name)
129
+
79
130
  chat_history = ChatHistory.from_dicts(messages)
131
+
132
+ # Enhanced logging to detect tool use
133
+ tools_info = f", {len(tools)} tools" if tools else ", no tools"
80
134
  log.info(
81
- "Calling LLM completion from %s on %s, response_format=%s",
135
+ "Calling LLM completion from %s on %s, response_format=%s%s",
82
136
  model.litellm_name,
83
137
  chat_history.size_summary(),
84
138
  response_format,
139
+ tools_info,
85
140
  )
86
141
 
142
+ if tools:
143
+ tool_names = []
144
+ for tool in tools:
145
+ if tool.get("type") == "function":
146
+ tool_names.append(tool.get("function", {}).get("name", "unknown"))
147
+ elif tool.get("type") == "native_web_search":
148
+ tool_names.append("native_web_search")
149
+ else:
150
+ tool_names.append(tool.get("type", "unknown"))
151
+
152
+ log.message("Tools enabled: %s", tool_names)
153
+
87
154
  start_time = time.time()
155
+
156
+ if response_format:
157
+ completion_params["response_format"] = response_format
158
+
159
+ if tools:
160
+ completion_params["tools"] = tools
161
+ log.info("Enabling function calling with %d tools", len(tools))
162
+
163
+ import litellm
164
+
88
165
  llm_output = cast(
89
166
  ModelResponse,
90
- litellm.completion(
91
- model.litellm_name,
92
- messages=messages,
93
- response_format=response_format,
94
- **kwargs,
95
- ), # pyright: ignore
167
+ litellm.completion(**completion_params), # pyright: ignore
96
168
  )
97
169
  elapsed = time.time() - start_time
98
170
 
@@ -100,23 +172,47 @@ def llm_completion(
100
172
 
101
173
  message = choices.message
102
174
 
175
+ # Extract tool calls from the response
176
+ tool_calls = getattr(message, "tool_calls", None)
177
+ tool_calls_list = list(tool_calls) if tool_calls else None
178
+
103
179
  # Just sanity checking and logging.
104
180
  content = choices.message.content
105
181
  if not content or not isinstance(content, str):
106
182
  raise ApiResultError(f"LLM completion failed: {model.litellm_name}: {llm_output}")
107
183
 
184
+ # Create the result object with tool calls
185
+ citations = llm_output.get("citations", None)
186
+ result = LLMCompletionResult(
187
+ message=message,
188
+ content=content,
189
+ citations=CitationList(citations=citations) if citations else None,
190
+ tool_calls=tool_calls_list,
191
+ )
192
+
193
+ # Log tool calls if present
194
+ if result.has_tool_calls:
195
+ tool_count = len(result.tool_calls or [])
196
+ log.message("LLM executed %d function calls: %s", tool_count, result.tool_call_names)
197
+ log.message(
198
+ "⚠️ Function calls require implementation - LLM requested tools but no handlers are implemented"
199
+ )
200
+
201
+ # Performance logging
108
202
  total_input_len = sum(len(m["content"]) for m in messages)
109
203
  speed = len(content) / elapsed
204
+ tool_count = len(result.tool_calls or []) if result.has_tool_calls else 0
205
+ tool_info = f", {tool_count} tool calls" if result.has_tool_calls else ""
110
206
  log.info(
111
207
  f"{EMOJI_TIMING} LLM completion from {model.litellm_name} in {format_duration(elapsed)}: "
112
208
  f"input {total_input_len} chars in {len(messages)} messages, output {len(content)} chars "
113
- f"({speed:.0f} char/s)"
209
+ f"({speed:.0f} char/s){tool_info}"
114
210
  )
115
211
 
116
- citations = llm_output.get("citations", None)
117
-
118
212
  if save_objects:
119
213
  metadata = {"citations": citations} if citations else {}
214
+ if result.has_tool_calls:
215
+ metadata["tool_calls"] = len(result.tool_calls or [])
120
216
  chat_history.messages.append(
121
217
  ChatMessage(role=ChatRole.assistant, content=content, metadata=metadata)
122
218
  )
@@ -128,11 +224,7 @@ def llm_completion(
128
224
  file_ext="yml",
129
225
  )
130
226
 
131
- return LLMCompletionResult(
132
- message=message,
133
- content=content,
134
- citations=CitationList(citations=citations) if citations else None,
135
- )
227
+ return result
136
228
 
137
229
 
138
230
  def llm_template_completion(
@@ -143,7 +235,9 @@ def llm_template_completion(
143
235
  previous_messages: list[dict[str, str]] | None = None,
144
236
  save_objects: bool = True,
145
237
  check_no_results: bool = True,
146
- response_format: dict | type[BaseModel] | None = None,
238
+ response_format: dict[str, Any] | type[BaseModel] | None = None,
239
+ tools: list[dict[str, Any]] | None = None,
240
+ enable_web_search: bool = False,
147
241
  **kwargs,
148
242
  ) -> LLMCompletionResult:
149
243
  """
@@ -169,6 +263,8 @@ def llm_template_completion(
169
263
  ],
170
264
  save_objects=save_objects,
171
265
  response_format=response_format,
266
+ tools=tools,
267
+ enable_web_search=enable_web_search,
172
268
  **kwargs,
173
269
  )
174
270
 
kash/llm_utils/llms.py CHANGED
@@ -28,22 +28,23 @@ class LLM(LLMName, Enum):
28
28
  gpt_4_1 = LLMName("gpt-4.1")
29
29
  gpt_4o = LLMName("gpt-4o")
30
30
  gpt_4o_mini = LLMName("gpt-4o-mini")
31
+ gpt_4o_search_preview = LLMName("gpt-4o-search-preview")
31
32
  gpt_4 = LLMName("gpt-4")
32
33
  gpt_4_1_mini = LLMName("gpt-4.1-mini")
33
34
  gpt_4_1_nano = LLMName("gpt-4.1-nano")
34
35
 
35
- # https://docs.anthropic.com/en/docs/about-claude/models/all-models
36
+ # https://docs.claude.com/en/docs/about-claude/models
36
37
 
37
- claude_4_1_opus = LLMName("claude-opus-4-1")
38
- claude_4_opus = LLMName("claude-opus-4-20250514")
39
- claude_4_sonnet = LLMName("claude-sonnet-4-20250514")
40
- claude_3_7_sonnet = LLMName("claude-3-7-sonnet-latest")
41
- claude_3_5_haiku = LLMName("claude-3-5-haiku-latest")
38
+ claude_sonnet_4_5 = LLMName("claude-sonnet-4-5-20250929")
39
+ claude_haiku_4_5 = LLMName("claude-haiku-4-5-20251001")
40
+ claude_opus_4_1 = LLMName("claude-opus-4-1-20250805")
41
+ claude_sonnet_4 = LLMName("claude-sonnet-4-20250514")
42
+ claude_opus_4 = LLMName("claude-opus-4-20250514")
42
43
 
43
44
  # https://ai.google.dev/gemini-api/docs/models
44
45
  gemini_2_5_pro = LLMName("gemini/gemini-2.5-pro")
45
46
  gemini_2_5_flash = LLMName("gemini/gemini-2.5-flash")
46
- gemini_2_5_flash_lite = LLMName("gemini-2.5-flash-lite-preview-06-17")
47
+ gemini_2_5_flash_lite = LLMName("gemini/gemini-2.5-flash-lite")
47
48
 
48
49
  # https://docs.x.ai/docs/models
49
50
  xai_grok_3 = LLMName("xai/grok-3")
@@ -1,4 +1,4 @@
1
- from enum import Enum
1
+ from enum import StrEnum
2
2
  from pathlib import Path
3
3
  from urllib.parse import urlencode
4
4
 
@@ -55,7 +55,7 @@ def format_local_url(route_path: str, **params: str | None) -> str:
55
55
  return url
56
56
 
57
57
 
58
- class Route(str, Enum):
58
+ class Route(StrEnum):
59
59
  file_view = "/file/view"
60
60
  item_view = "/item/view"
61
61
  explain = "/explain"
@@ -5,34 +5,28 @@ from pathlib import Path
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from clideps.env_vars.dotenv_utils import load_dotenv_paths
8
- from httpx import Timeout
9
8
 
10
9
  from kash.config.logger import CustomLogger, get_logger
11
10
  from kash.config.settings import global_settings
12
11
  from kash.media_base.transcription_format import SpeakerSegment, format_speaker_segments
13
- from kash.utils.errors import ApiError, ContentError
12
+ from kash.utils.errors import ContentError
14
13
 
15
14
  if TYPE_CHECKING:
16
- from deepgram import PrerecordedResponse
15
+ from deepgram.types.listen_v1accepted_response import ListenV1AcceptedResponse
16
+ from deepgram.types.listen_v1response import ListenV1Response
17
17
 
18
18
  log: CustomLogger = get_logger(__name__)
19
19
 
20
20
 
21
21
  def deepgram_transcribe_raw(
22
22
  audio_file_path: Path, language: str | None = None
23
- ) -> PrerecordedResponse:
23
+ ) -> ListenV1Response | ListenV1AcceptedResponse:
24
24
  """
25
25
  Transcribe an audio file using Deepgram and return the raw response.
26
26
  """
27
27
  # Slow import, do lazily.
28
- from deepgram import (
29
- ClientOptionsFromEnv,
30
- DeepgramClient,
31
- FileSource,
32
- ListenRESTClient,
33
- PrerecordedOptions,
34
- PrerecordedResponse,
35
- )
28
+ from deepgram import DeepgramClient
29
+ from deepgram.core.request_options import RequestOptions
36
30
 
37
31
  size = getsize(audio_file_path)
38
32
  log.info(
@@ -40,21 +34,19 @@ def deepgram_transcribe_raw(
40
34
  )
41
35
 
42
36
  load_dotenv_paths(True, True, global_settings().system_config_dir)
43
- deepgram = DeepgramClient("", ClientOptionsFromEnv())
37
+ deepgram = DeepgramClient()
44
38
 
45
39
  with open(audio_file_path, "rb") as audio_file:
46
40
  buffer_data = audio_file.read()
47
41
 
48
- payload: FileSource = {
49
- "buffer": buffer_data,
50
- }
51
-
52
- options = PrerecordedOptions(model="nova-2", smart_format=True, diarize=True, language=language)
53
- client: ListenRESTClient = deepgram.listen.rest.v("1") # pyright: ignore
54
-
55
- response = client.transcribe_file(payload, options, timeout=Timeout(500))
56
- if not isinstance(response, PrerecordedResponse):
57
- raise ApiError("Deepgram returned an unexpected response type")
42
+ response = deepgram.listen.v1.media.transcribe_file(
43
+ request=buffer_data,
44
+ model="nova-2",
45
+ smart_format=True,
46
+ diarize=True,
47
+ language=language,
48
+ request_options=RequestOptions(timeout_in_seconds=500),
49
+ )
58
50
 
59
51
  return response
60
52
 
@@ -64,7 +56,9 @@ def deepgram_transcribe_audio(audio_file_path: Path, language: str | None = None
64
56
 
65
57
  log.save_object("Deepgram response", None, response)
66
58
 
67
- diarized_segments = _deepgram_diarized_segments(response)
59
+ # Convert Pydantic model to dict for processing.
60
+ response_dict = response.model_dump()
61
+ diarized_segments = _deepgram_diarized_segments(response_dict)
68
62
  log.debug("Diarized response: %s", diarized_segments)
69
63
 
70
64
  if not diarized_segments:
@@ -171,6 +171,7 @@ class LLMOptions:
171
171
  body_template: MessageTemplate = MessageTemplate("{body}")
172
172
  windowing: WindowSettings = WINDOW_NONE
173
173
  diff_filter: DiffFilter | None = None
174
+ enable_web_search: bool = False
174
175
 
175
176
  def updated_with(self, param_name: str, value: Any) -> LLMOptions:
176
177
  """Update option from an action parameter."""
@@ -543,8 +544,8 @@ class Action(ABC):
543
544
  def preassemble_result(self, context: ActionContext) -> ActionResult | None:
544
545
  """
545
546
  Actions can have a separate preliminary step to pre-assemble outputs. This allows
546
- us to determine the title and types for the output items and check if they were
547
- already generated before running slow or expensive actions.
547
+ us to determine thew expected shape of the expected output and check if it already
548
+ exists.
548
549
 
549
550
  For now, this only applies to actions with a single output, when `self.cacheable`
550
551
  is True.
@@ -568,7 +569,10 @@ class Action(ABC):
568
569
  self.name,
569
570
  )
570
571
  output_type = ItemType.doc
571
- primary_output = primary_input.derived_copy(context, 0, type=output_type)
572
+ output_format = context.action.output_format or primary_input.format
573
+ primary_output = primary_input.derived_copy(
574
+ context, 0, type=output_type, format=output_format
575
+ )
572
576
  log.info("Preassembled output: source %s, %s", primary_output.source, primary_output)
573
577
  return ActionResult([primary_output])
574
578
  else:
@@ -1,11 +1,11 @@
1
- from enum import Enum
1
+ from enum import StrEnum
2
2
 
3
3
  from pydantic import BaseModel
4
4
 
5
5
  from kash.exec_model.commands_model import CommentedCommand
6
6
 
7
7
 
8
- class Confidence(str, Enum):
8
+ class Confidence(StrEnum):
9
9
  """
10
10
  How confident the assistant is that the answer is correct.
11
11
  """
kash/model/items_model.py CHANGED
@@ -193,9 +193,16 @@ class ItemId:
193
193
  from kash.web_content.canon_url import canonicalize_url
194
194
 
195
195
  item_id = None
196
- if item.type == ItemType.resource and item.format == Format.url and item.url:
196
+ if (
197
+ item.type == ItemType.resource
198
+ and item.format == Format.url
199
+ and item.url
200
+ and not item.source
201
+ ):
202
+ # This is a plain URL resource, so its identity is its URL.
197
203
  item_id = ItemId(item.type, IdType.url, canonicalize_url(item.url))
198
204
  elif item.type == ItemType.concept and item.title:
205
+ # This is a concept, so its identity is its title.
199
206
  item_id = ItemId(item.type, IdType.concept, canonicalize_concept(item.title))
200
207
  elif item.source and item.source.cacheable and item.source.operation.has_known_inputs:
201
208
  # We know the source of this and if the action was cacheable, we can create
@@ -206,10 +206,10 @@ A list of parameter declarations, possibly with default values.
206
206
 
207
207
  # These are the default models for typical use cases.
208
208
  # The user may override them with parameters.
209
- DEFAULT_CAREFUL_LLM = LLM.gpt_5
210
- DEFAULT_STRUCTURED_LLM = LLM.gpt_5
211
- DEFAULT_STANDARD_LLM = LLM.gpt_5
212
- DEFAULT_FAST_LLM = LLM.gpt_5_mini
209
+ DEFAULT_CAREFUL_LLM = LLM.claude_sonnet_4_5
210
+ DEFAULT_STRUCTURED_LLM = LLM.claude_sonnet_4_5
211
+ DEFAULT_STANDARD_LLM = LLM.claude_sonnet_4_5
212
+ DEFAULT_FAST_LLM = LLM.claude_haiku_4_5
213
213
 
214
214
 
215
215
  # Parameters set globally such as in the workspace.
@@ -17,7 +17,7 @@
17
17
 
18
18
  import os
19
19
  from dataclasses import dataclass
20
- from enum import Enum
20
+ from enum import StrEnum
21
21
 
22
22
  NBSP = "\u00a0"
23
23
 
@@ -35,7 +35,7 @@ class Icon:
35
35
 
36
36
 
37
37
  # fmt: off
38
- class Icons(str, Enum):
38
+ class Icons(StrEnum):
39
39
  ARCHIVE = '\uf410' # 
40
40
  AUDIO = "\uf001" # 
41
41
  BINARY = "\ueae8" # 
@@ -67,7 +67,7 @@ https://www.ethanheilman.com/x/28/index.html
67
67
  # files, so that legacy shell commands automatically have tooltips with info
68
68
  # about files).
69
69
 
70
- from enum import Enum
70
+ from enum import StrEnum
71
71
  from html import escape
72
72
  from typing import Annotated, Literal, Self, TypeAlias
73
73
  from urllib.parse import parse_qs, quote, urlencode, urlparse
@@ -91,7 +91,7 @@ KUI_SCHEME = f"{KUI_PROTOCOL}//"
91
91
  """The Kerm code URI scheme for embedding UI elements into links."""
92
92
 
93
93
 
94
- class UIActionType(str, Enum):
94
+ class UIActionType(StrEnum):
95
95
  paste_text = "paste_text"
96
96
  """Default action for pasting text into the terminal. If value is omitted, paste the link text."""
97
97
 
@@ -120,7 +120,7 @@ class UIAction(BaseModel):
120
120
  return self.model_dump_json()
121
121
 
122
122
 
123
- class DisplayStyle(str, Enum):
123
+ class DisplayStyle(StrEnum):
124
124
  """
125
125
  Style for text.
126
126
  """
@@ -157,14 +157,14 @@ class DisplayHints(BaseModel):
157
157
  dimensions: Dimensions | None = Field(default=None, description="Dimensions.")
158
158
 
159
159
 
160
- class UIRole(str, Enum):
160
+ class UIRole(StrEnum):
161
161
  tooltip = "tooltip"
162
162
  popover = "popover"
163
163
  output = "output"
164
164
  input = "input"
165
165
 
166
166
 
167
- class UIElementType(str, Enum):
167
+ class UIElementType(StrEnum):
168
168
  text_tooltip = "text_tooltip"
169
169
  link_tooltip = "link_tooltip"
170
170
  iframe_tooltip = "iframe_tooltip"
@@ -32,13 +32,11 @@ class CachingSession(requests.Session):
32
32
  ):
33
33
  super().__init__()
34
34
  self._limiter: Limiter | None = None
35
+ self._max_wait_secs = max_wait_secs
35
36
  if limit and limit_interval_secs:
36
37
  rate = Rate(limit, Duration.SECOND * limit_interval_secs)
37
38
  bucket = InMemoryBucket([rate])
38
- # Explicitly set raise_when_fail=False and max_delay to enable blocking.
39
- self._limiter = Limiter(
40
- bucket, raise_when_fail=False, max_delay=Duration.SECOND * max_wait_secs
41
- )
39
+ self._limiter = Limiter(bucket)
42
40
  log.info(
43
41
  "CachingSession: rate limiting requests with limit=%d, interval=%d, max_wait=%d",
44
42
  limit,
@@ -56,9 +54,11 @@ class CachingSession(requests.Session):
56
54
 
57
55
  def save(path: Path):
58
56
  if self._limiter:
59
- acquired = self._limiter.try_acquire("caching_session_get")
57
+ acquired = self._limiter.try_acquire(
58
+ "caching_session_get", blocking=True, timeout=self._max_wait_secs
59
+ )
60
60
  if not acquired:
61
- # Generally shouldn't happen.
61
+ # Generally shouldn't happen with blocking=True and reasonable timeout.
62
62
  raise RuntimeError("Rate limiter failed to acquire after maximum delay")
63
63
 
64
64
  response = super(CachingSession, self).get(url, **kwargs)
@@ -94,7 +94,7 @@ content: |
94
94
  from __future__ import annotations
95
95
 
96
96
  from dataclasses import field
97
- from enum import Enum
97
+ from enum import Enum, StrEnum
98
98
  from io import StringIO
99
99
  from pathlib import Path
100
100
  from textwrap import dedent
@@ -106,7 +106,7 @@ from pydantic.dataclasses import dataclass
106
106
  from sidematter_format import to_json_string
107
107
 
108
108
 
109
- class ChatRole(str, Enum):
109
+ class ChatRole(StrEnum):
110
110
  """
111
111
  The role of a message in a chat. Represents the "role" in LLM APIs but note we slightly
112
112
  abuse this term to also represent other types of messages, such as commands issued
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import UTC, datetime
4
- from enum import Enum
4
+ from enum import StrEnum
5
5
  from pathlib import Path
6
6
  from typing import TYPE_CHECKING
7
7
 
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
20
20
  log = get_logger(__name__)
21
21
 
22
22
 
23
- class SortOption(str, Enum):
23
+ class SortOption(StrEnum):
24
24
  filename = "filename"
25
25
  size = "size"
26
26
  accessed = "accessed"
@@ -28,13 +28,13 @@ class SortOption(str, Enum):
28
28
  modified = "modified"
29
29
 
30
30
 
31
- class GroupByOption(str, Enum):
31
+ class GroupByOption(StrEnum):
32
32
  flat = "flat"
33
33
  parent = "parent"
34
34
  suffix = "suffix"
35
35
 
36
36
 
37
- class FileType(str, Enum):
37
+ class FileType(StrEnum):
38
38
  file = "file"
39
39
  dir = "dir"
40
40
 
@@ -1,8 +1,9 @@
1
1
  {% extends "simple_webpage.html.jinja" %}
2
2
 
3
3
  {#
4
- Extends the simple page with a right-side YouTube popover player.
5
- Usage: pass page_template="youtube_webpage.html.jinja" to simple_webpage_render.
4
+ Extends the simple page with YouTube popover player.
5
+ Safe to use instead of simple_webpage.html.jinja since it only adds
6
+ functionality for YouTube if applicable.
6
7
  #}
7
8
 
8
9
  {% block custom_styles %}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kash-shell
3
- Version: 0.3.35
3
+ Version: 0.3.37
4
4
  Summary: The knowledge agent shell (core)
5
5
  Project-URL: Repository, https://github.com/jlevy/kash-shell
6
6
  Author-email: Joshua Levy <joshua@cal.berkeley.edu>
@@ -97,7 +97,7 @@ kash/exec/command_registry.py,sha256=1s2ogU8b8nqK_AEtslbr1eYrXCGDkeT30UrB7L0BRoM
97
97
  kash/exec/fetch_url_items.py,sha256=Bb8i9YW_XfJHwIAh5yf2_zLXagyESkQF1LL1uNaQQx0,6345
98
98
  kash/exec/history.py,sha256=l2XwHGBR1UgTGSFPSBE9mltmxvjR_5qFFO6d-Z008nc,1208
99
99
  kash/exec/importing.py,sha256=xunmBapeUMNc6Zox7y6e_DZkidyWeouiFZpphajwSzc,1843
100
- kash/exec/llm_transforms.py,sha256=jQAnxHhcvF3oILjpSHx0YRw8Qw-ZnioukXaJuaqjNVk,4626
100
+ kash/exec/llm_transforms.py,sha256=2fxas6K2YcixVGpXKt0-nxlN2qc1SR3-O1KbE0sFKIg,4834
101
101
  kash/exec/precondition_checks.py,sha256=HymxL7qm4Yz8V76Um5pKdIRnQ2N-p9rpQQi1fI38bNA,2139
102
102
  kash/exec/precondition_registry.py,sha256=9O-01d2OrsYLuhqodVuWjouLR2ABJbUotAmfJNBCRzs,1439
103
103
  kash/exec/preconditions.py,sha256=bwNuuPEnkymj24ySPTl8K5DEgTtB2NPGAm9cWaomhAk,5262
@@ -110,7 +110,7 @@ kash/exec_model/commands_model.py,sha256=iM8QhzA0tAas5OwF5liUfHtm45XIH1LcvCviuh3
110
110
  kash/exec_model/script_model.py,sha256=1VG3LhkTmlKzHOYouZ92ZpOSKSCcsz3-tHNcFMQF788,5031
111
111
  kash/exec_model/shell_model.py,sha256=LUhQivbpXlerM-DUzNY7BtctNBbn08Wto8CSSxQDxRU,568
112
112
  kash/file_storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
113
- kash/file_storage/file_store.py,sha256=rWFJD_PVzNgBtFBUD8Ltj9GK-am5zFj9vrMdyIy9pjQ,31442
113
+ kash/file_storage/file_store.py,sha256=0re_Gp0NoN3KxsMPtU8RSrG3Jw_PelC2wqju07AGxTU,31634
114
114
  kash/file_storage/item_file_format.py,sha256=QNNpvMDDNyBLLrIwy3yhNgN0Ktsvjh-_Uzkf2XQpI98,7531
115
115
  kash/file_storage/item_id_index.py,sha256=aiDHomizFLO_Ui2i6n-YIF4RUosIsxRIQca35yCdIIk,4589
116
116
  kash/file_storage/metadata_dirs.py,sha256=9AqO3S3SSY1dtvP2iLX--E4ui0VIzXttG8R040otfyg,3820
@@ -135,14 +135,14 @@ kash/llm_utils/custom_sliding_transforms.py,sha256=z07WCdBoiywBIGk2EXK3xY4t_6g8I
135
135
  kash/llm_utils/fuzzy_parsing.py,sha256=bbG2Y7i5w6kxAVPAixyluv3MDS2hW_pkhnJpVOLHZQc,3278
136
136
  kash/llm_utils/init_litellm.py,sha256=5Fn9uW4P7lfEO8Rk1EJJUzDEGNjw-PDvxFgHlKDf-Ok,409
137
137
  kash/llm_utils/llm_api_keys.py,sha256=nTB9wSFfHTOXvqshSQCQGCPxUwOW1U7oslngya8nHkw,1168
138
- kash/llm_utils/llm_completion.py,sha256=SzeWGRrsjuN1TXdPwscYG6whLQkHclITtwTvQK19GE0,5436
138
+ kash/llm_utils/llm_completion.py,sha256=UNFNgJbgU8sAWXmSrMV9DGrQ_1x-0IeEbtvrDG_P4NE,9251
139
139
  kash/llm_utils/llm_messages.py,sha256=70QwIIvdwo-h4Jfn_6MbEHb3LTUjUmzg_v_dU_Ey__g,1174
140
140
  kash/llm_utils/llm_names.py,sha256=VZbdKnoeBx_luB5YQ-Rz37gMt3_FcueJdp40ZaQbpUA,3620
141
- kash/llm_utils/llms.py,sha256=S4hBDeOraKOp9e99M8Ga61u6xAkocxs02jXgJRdFNSA,3983
141
+ kash/llm_utils/llms.py,sha256=sGDrTnYAqjg_keKhXa7JR3HHkp5Klt-OhO3t9YjuhVQ,4036
142
142
  kash/local_server/__init__.py,sha256=AyNpvCOJlQF6A4DnlYKRMbbfRNzdizEA-ytp-F2SLZU,162
143
143
  kash/local_server/local_server.py,sha256=EugjL30VM0pWdZDsiQxU-o6EdEa082qlGd_7RHvI5tk,5863
144
144
  kash/local_server/local_server_commands.py,sha256=T5wN-Xty3IbwbyeJxTULPB2NqssWLJcuV8IoqMu9bus,1668
145
- kash/local_server/local_server_routes.py,sha256=JlIVsrbsU0yiwv7vAoD9BctqiBI0w6u8Ld3BYY4jmo8,10530
145
+ kash/local_server/local_server_routes.py,sha256=Qx9I2GLoACLYhTlT8-SxbnFe-SKqfhWB6wDMDSEQD2s,10531
146
146
  kash/local_server/local_url_formatters.py,sha256=SqHjGMEufvm43n34SCa_8Asdwm7utx91Wwymj15TuSY,5327
147
147
  kash/local_server/port_tools.py,sha256=oFfOvO6keqS5GowTpVg2FTu5KqkPHBq-dWAEomUIgGo,2008
148
148
  kash/local_server/rich_html_template.py,sha256=O9CnkMYkWuMvKJkqD0P8jaZqfUe6hMP4LXFvcLpwN8Q,196
@@ -159,23 +159,23 @@ kash/media_base/media_cache.py,sha256=wNguo9nNnCRmr7GERrzDOj83xCYife7EnVvC2tlNtr
159
159
  kash/media_base/media_services.py,sha256=kmAjLIrG62twI8GCowSeR1yfpVWf1KfAhPbuxPiaQAA,5475
160
160
  kash/media_base/media_tools.py,sha256=enzSebW41vDBGWUwkEn5udhRHmSBvH01MAt5rY1s388,1466
161
161
  kash/media_base/timestamp_citations.py,sha256=IHTvlZD3lIfe8T2uHFitgom82WahdHBeKqyc2OjUi9g,2368
162
- kash/media_base/transcription_deepgram.py,sha256=yuwqgTPY-d7XnLMTuNDX0PLBxazqOy8osDXEscNOrGo,5308
162
+ kash/media_base/transcription_deepgram.py,sha256=IXuGB8yRc4EMANCObxfedrAHktE1hVJGna-ZUkFCnf0,5191
163
163
  kash/media_base/transcription_format.py,sha256=rOVPTpwvW22c27BRwYF-Tc_xzqK_wOtUZpOPlvkHiDY,2344
164
164
  kash/media_base/transcription_whisper.py,sha256=GqvroW9kBAH4-gcbYkMgNCfs2MpMIgm1ip3NMWtJ0IE,1169
165
165
  kash/media_base/services/local_file_media.py,sha256=_NV-T90rShJ8ucUjQXMPCKKJ50GSFE9PyyVzhXp5z9w,5624
166
166
  kash/model/__init__.py,sha256=kFfBKb5N70NWYUfpRRxn_Sb9p_vXlB6BBaTCqWmSReo,2978
167
- kash/model/actions_model.py,sha256=Y_MyZ6ATxr7-0AAnBFyWOJ8_81DCeWviNc2U4ig59bM,23705
168
- kash/model/assistant_response_model.py,sha256=6eDfC27nyuBDFjv5nCYMa_Qb2mPbKwDzZy7uLOIyskI,2653
167
+ kash/model/actions_model.py,sha256=I87Gb2nU6xelEuM8WFuuHWDqczjnlbpBYkw6wWIYkbo,23825
168
+ kash/model/assistant_response_model.py,sha256=tS0PmVxhdRXpBM-qNUGB4opyXUf5R4Z_4750w6mYCbA,2654
169
169
  kash/model/compound_actions_model.py,sha256=oYEtVKtQv-mA1abZkK7PvaM9xazVBUuk1z0geKBulak,6965
170
170
  kash/model/concept_model.py,sha256=we2qOcy9Mv1q7XPfkDLp_CyO_-8DwAUfUYlpgy_jrFs,1011
171
171
  kash/model/exec_model.py,sha256=3Su3NEmEtDoSuQSxvg75FYY_EdClSM5pwQK1i7_S88A,3131
172
172
  kash/model/graph_model.py,sha256=T034y0E9OJtITd1g9zp9vll5pLscdatq6JoT08KvPZE,2724
173
- kash/model/items_model.py,sha256=1WXNNmiqDs32XR3d8v_dUY4Y6T-3ow3XKuTJOoF_how,39937
173
+ kash/model/items_model.py,sha256=e-KCVhC6poWjocsxZVBa0A8gmehtsZRcIqhLVIna5Ho,40152
174
174
  kash/model/language_list.py,sha256=I3RIbxTseVmPdhExQimimEv18Gmy2ImMbpXe0-_t1Qw,450
175
175
  kash/model/llm_actions_model.py,sha256=a29uXVNfS2CiqvM7HPdC6H9A23rSQQihAideuBLMH8g,2110
176
176
  kash/model/media_model.py,sha256=ZnlZ-FkswbAIGpUAuNqLce1WDZK-WbnwHn2ipg8x7-0,3511
177
177
  kash/model/operations_model.py,sha256=WmU-xeWGsqMLVN369dQEyVGU8T7G_KyLLsj6YFc5sVw,6517
178
- kash/model/params_model.py,sha256=NHW-74_sFCY58spf9bzU1EaVxLDpdbqmiw8SCSKASCw,15079
178
+ kash/model/params_model.py,sha256=PWyx2P9tt1zj8NpBnvFkn61_KCegFtTJ_FSPYSLnsiY,15121
179
179
  kash/model/paths_model.py,sha256=KDFm7wan7hjObHbnV2rR8-jsyLTVqbKcwFdKeLFRtdM,15889
180
180
  kash/model/preconditions_model.py,sha256=-IfsVR0NkQhq_3hUTXzK2bFYAd--3YjSwUiDKHVQQqk,2887
181
181
  kash/shell/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -185,7 +185,7 @@ kash/shell/completions/completion_scoring.py,sha256=-svesm2cR1AA86jYcxlynXCBZON2
185
185
  kash/shell/completions/completion_types.py,sha256=FocRXd6Df3Df0nL2Y1GevMx3FsljJwbQdVgWsIngpaQ,4793
186
186
  kash/shell/completions/shell_completions.py,sha256=A0WWrm2lEwguasDMnDuaI8xGny9MhwGfNvCY3rHNUnw,8844
187
187
  kash/shell/file_icons/color_for_format.py,sha256=bFuE1lwiyUkgWB3Gk3jrch-9EIQz9thILQiX_a5dGb8,2034
188
- kash/shell/file_icons/nerd_icons.py,sha256=qF1Awc5cWIwaOWo7k_nmG9_A6XXwdNNcaluUUlde3KM,36046
188
+ kash/shell/file_icons/nerd_icons.py,sha256=fxqICNUFXMy9miZxpr3dXAIEfQ-zUMweP3Ph3YjSR7Y,36047
189
189
  kash/shell/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
190
190
  kash/shell/input/input_prompts.py,sha256=LkF7Q7HygBOVT1XzvC0dY7gfCVV0pedfwI5nlX-I6YA,3820
191
191
  kash/shell/input/inquirer_settings.py,sha256=2zOtgE2DJNzl4vEiEb_Dt8SUeM1YWjeXhCRaAPOCScM,1645
@@ -193,7 +193,7 @@ kash/shell/input/param_inputs.py,sha256=FiGClMtMe0pv0y_45qJ9PKF61vn8SOPiNeh5yvH0
193
193
  kash/shell/input/shell_confirm.py,sha256=A17HTxx_DPnis2503Q_LOEW8P9yIWvaj1zHVz4LSz_Q,1070
194
194
  kash/shell/output/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
195
195
  kash/shell/output/kerm_code_utils.py,sha256=92A4AV-IFKKZMWLNZnd_zksNFMBgE_VNXySyn0Kn6Zk,1673
196
- kash/shell/output/kerm_codes.py,sha256=KLVdTM_dL_NeYmGbllzsQoW4IHXJjEsgqqIp1s7P1yI,18877
196
+ kash/shell/output/kerm_codes.py,sha256=1ofHqH74XlUOH91_eLmn2PLx-yyd8usnbSKuJDczWqQ,18872
197
197
  kash/shell/output/kmarkdown.py,sha256=RRB5b0Ip0KZ71vnJKFfvxerYkeDFTCVTlHqHfmMy80Y,3675
198
198
  kash/shell/output/shell_formatting.py,sha256=oxmAeJ2j0ANYSUsL15CUv--KcGlQ6Wa_rywXSDlsZM4,3331
199
199
  kash/shell/output/shell_output.py,sha256=yG-4YjQUkPQZASMRGcfVRUadOcRO6hm-eaAC74jms6c,11812
@@ -206,7 +206,7 @@ kash/shell/utils/shell_function_wrapper.py,sha256=fgUuVhocYMKLkGJJQJOER5nFMAvM0Z
206
206
  kash/utils/__init__.py,sha256=4Jl_AtgRADdGORimWhYZwbSfQSpQ6SiexNIZzmbcngI,111
207
207
  kash/utils/errors.py,sha256=2lPL0fxI8pPOiDvjl0j-rvwY8uhmWetsrYYIc2-x1WY,3906
208
208
  kash/utils/api_utils/api_retries.py,sha256=TtgxLxoMnXIzYMKbMUzsnVcPf-aKFm3cJ95zOcSeae8,21844
209
- kash/utils/api_utils/cache_requests_limited.py,sha256=TA5buZ9Dgbj4I1zHhwerTXre018i0TCACGsezsjX9Uc,3140
209
+ kash/utils/api_utils/cache_requests_limited.py,sha256=4beytcBbZYuCXbT1QK8LaD98GBLnT3mcvjhFkNN0YII,3127
210
210
  kash/utils/api_utils/gather_limited.py,sha256=6K0Z3u_NeX9wBfFFk21wUQeSimaDIm53AHlGYRLD6LQ,33018
211
211
  kash/utils/api_utils/http_utils.py,sha256=Ou6QNiba5w7n71cgNmV168OFTLmMDNxWW5MM-XkFEME,1461
212
212
  kash/utils/api_utils/multitask_gather.py,sha256=LAylwWZ2APbv-O_l0kLwBfP762D0qswMBV8ID4eCOA0,4446
@@ -228,14 +228,14 @@ kash/utils/common/type_utils.py,sha256=SJirXhPilQom_-OKkFToDLm_82ZwpjcNjRy8U1HaQ
228
228
  kash/utils/common/uniquifier.py,sha256=75OY4KIVF8u1eoO0FCPbEGTyVpPOtM-0ctoG_s_jahM,3082
229
229
  kash/utils/common/url.py,sha256=u4qT0sWYsuEYazFCr3IIpmrR6nXyATH-_6GHM2xF5E0,9689
230
230
  kash/utils/common/url_slice.py,sha256=QJb_qJp-hd5lFrxpw7P009yeMTJWDMfF11KRKEo7mIA,10963
231
- kash/utils/file_formats/chat_format.py,sha256=XQpyUyq3jBKqM1f1eRZmg5hPBWZIobHNusQPFctFbRQ,11901
231
+ kash/utils/file_formats/chat_format.py,sha256=MY2Gz09p8c_-MxS8l2WkGAOP8os9Sw8c_ZcKrpqEzN4,11908
232
232
  kash/utils/file_utils/__init__.py,sha256=loL_iW0oOZs0mJ5GelBPptBcqzYKSWdsGcHrpRyxitQ,43
233
233
  kash/utils/file_utils/csv_utils.py,sha256=ZqchXD4y0buWtgnEsR_UeHyWBHM1wEJl5u-IxQvFa3s,4064
234
234
  kash/utils/file_utils/dir_info.py,sha256=HamMr58k_DanTLifj7A2JDxTGWXEZZx2pQuE6Hjcm8g,1856
235
235
  kash/utils/file_utils/file_ext.py,sha256=U0fG3rowgtc56fNhhCK8P1UCYFbCNoa87pv7mGiEQgA,1876
236
236
  kash/utils/file_utils/file_formats.py,sha256=4bq0-ZomFidL2GJBFyszGPurNQeTMOdLaXlGgrXznhA,4973
237
237
  kash/utils/file_utils/file_formats_model.py,sha256=WqtAb10_vBuWeP2cUHzUHNjNmLx5GdqPjsnuqziB6Zg,15970
238
- kash/utils/file_utils/file_sort_filter.py,sha256=_k1chT3dJl5lSmKA2PW90KaoG4k4zftGdtwWoNEljP4,7136
238
+ kash/utils/file_utils/file_sort_filter.py,sha256=Y-0Yw9-CaG0LMIaVo9dSTI-eLlvIzmnU0MoeJwMJBs0,7133
239
239
  kash/utils/file_utils/file_walk.py,sha256=cpwVDPuaVm95_ZwFJiAdIuZAGhASI3gJ3ZUsCGP75b8,5527
240
240
  kash/utils/file_utils/filename_parsing.py,sha256=drHrH2B9W_5yAbXURNGJxNqj9GmTe8FayH6Gjw9e4-U,4194
241
241
  kash/utils/file_utils/ignore_files.py,sha256=QJ0SFeGdxSCaf4v45qQE_BMsMT5nOgomma0TuJRibp8,3546
@@ -277,7 +277,7 @@ kash/web_gen/templates/explain_view.html.jinja,sha256=DNw5Iw5SrhIUFRGB4qNvfcKXsB
277
277
  kash/web_gen/templates/item_view.html.jinja,sha256=_b51RuoBmYu7nxVq-S_zw_tv8mfQXHICGqfDWNIB9Xg,7304
278
278
  kash/web_gen/templates/simple_webpage.html.jinja,sha256=_RVu0AvrN5fK_Kz-tEi4AlPQ_wuu8-S9oFIYg_sdcec,2556
279
279
  kash/web_gen/templates/tabbed_webpage.html.jinja,sha256=umkipUXW-lDGnbV-tlDroCfCx_385PFnpbzsvwmityo,1843
280
- kash/web_gen/templates/youtube_webpage.html.jinja,sha256=uUdu3HWv0lwwgEh094QbzzesxAdYv9rCxCkiuuIr1MY,1380
280
+ kash/web_gen/templates/youtube_webpage.html.jinja,sha256=eEvWXQsqEIapH0-0nXfG1SGh4BAmbxKIMmvZMcteYQE,1397
281
281
  kash/web_gen/templates/components/toc_scripts.js.jinja,sha256=9AanLJaVormGi52h-k2tKJTRT4BiBGGNnw3Kmrnr40Q,10481
282
282
  kash/web_gen/templates/components/toc_styles.css.jinja,sha256=SM99C9bOu11qdZ1U6hN7lLe2w_Hk6u9qNZcx2knSkgg,7553
283
283
  kash/web_gen/templates/components/tooltip_scripts.js.jinja,sha256=63HUSXtT07Vh2ndp90ViuPHTdzslrkGO5it4s-5EheM,40181
@@ -305,8 +305,8 @@ kash/xonsh_custom/xonsh_modern_tools.py,sha256=mj_b34LZXfE8MJe9EpDmp5JZ0tDM1biYN
305
305
  kash/xonsh_custom/xonsh_ranking_completer.py,sha256=ZRGiAfoEgqgnlq2-ReUVEaX5oOgW1DQ9WxIv2OJLuTo,5620
306
306
  kash/xontrib/fnm.py,sha256=V2tsOdmIDgbFbZSfMLpsvDIwwJJqiYnOkOySD1cXNXw,3700
307
307
  kash/xontrib/kash_extension.py,sha256=FLIMlgR3C_6A1fwKE-Ul0nmmpJSszVPbAriinUyQ8Zg,1896
308
- kash_shell-0.3.35.dist-info/METADATA,sha256=tiHz-9gjkl9cyzEt8XxLl7kHDYbCb86dW9xpeU7qSGg,33581
309
- kash_shell-0.3.35.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
310
- kash_shell-0.3.35.dist-info/entry_points.txt,sha256=SQraWDAo8SqYpthLXThei0mf_hGGyhYBUO-Er_0HcwI,85
311
- kash_shell-0.3.35.dist-info/licenses/LICENSE,sha256=rCh2PsfYeiU6FK_0wb58kHGm_Fj5c43fdcHEexiVzIo,34562
312
- kash_shell-0.3.35.dist-info/RECORD,,
308
+ kash_shell-0.3.37.dist-info/METADATA,sha256=Nl6PtqxydXQXFJu9v7IKaioebse690jbQ68AB4myiXQ,33581
309
+ kash_shell-0.3.37.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
310
+ kash_shell-0.3.37.dist-info/entry_points.txt,sha256=SQraWDAo8SqYpthLXThei0mf_hGGyhYBUO-Er_0HcwI,85
311
+ kash_shell-0.3.37.dist-info/licenses/LICENSE,sha256=rCh2PsfYeiU6FK_0wb58kHGm_Fj5c43fdcHEexiVzIo,34562
312
+ kash_shell-0.3.37.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any