aru-code 0.13.1__tar.gz → 0.13.3__tar.gz

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 (57) hide show
  1. {aru_code-0.13.1/aru_code.egg-info → aru_code-0.13.3}/PKG-INFO +1 -1
  2. aru_code-0.13.3/aru/__init__.py +1 -0
  3. {aru_code-0.13.1 → aru_code-0.13.3}/aru/cli.py +18 -7
  4. {aru_code-0.13.1 → aru_code-0.13.3}/aru/context.py +106 -36
  5. {aru_code-0.13.1 → aru_code-0.13.3}/aru/runner.py +24 -5
  6. {aru_code-0.13.1 → aru_code-0.13.3}/aru/runtime.py +1 -0
  7. {aru_code-0.13.1 → aru_code-0.13.3}/aru/tools/codebase.py +3 -3
  8. {aru_code-0.13.1 → aru_code-0.13.3/aru_code.egg-info}/PKG-INFO +1 -1
  9. {aru_code-0.13.1 → aru_code-0.13.3}/pyproject.toml +1 -1
  10. aru_code-0.13.1/aru/__init__.py +0 -1
  11. {aru_code-0.13.1 → aru_code-0.13.3}/LICENSE +0 -0
  12. {aru_code-0.13.1 → aru_code-0.13.3}/README.md +0 -0
  13. {aru_code-0.13.1 → aru_code-0.13.3}/aru/agent_factory.py +0 -0
  14. {aru_code-0.13.1 → aru_code-0.13.3}/aru/agents/__init__.py +0 -0
  15. {aru_code-0.13.1 → aru_code-0.13.3}/aru/agents/base.py +0 -0
  16. {aru_code-0.13.1 → aru_code-0.13.3}/aru/agents/executor.py +0 -0
  17. {aru_code-0.13.1 → aru_code-0.13.3}/aru/agents/planner.py +0 -0
  18. {aru_code-0.13.1 → aru_code-0.13.3}/aru/commands.py +0 -0
  19. {aru_code-0.13.1 → aru_code-0.13.3}/aru/completers.py +0 -0
  20. {aru_code-0.13.1 → aru_code-0.13.3}/aru/config.py +0 -0
  21. {aru_code-0.13.1 → aru_code-0.13.3}/aru/display.py +0 -0
  22. {aru_code-0.13.1 → aru_code-0.13.3}/aru/permissions.py +0 -0
  23. {aru_code-0.13.1 → aru_code-0.13.3}/aru/providers.py +0 -0
  24. {aru_code-0.13.1 → aru_code-0.13.3}/aru/session.py +0 -0
  25. {aru_code-0.13.1 → aru_code-0.13.3}/aru/tools/__init__.py +0 -0
  26. {aru_code-0.13.1 → aru_code-0.13.3}/aru/tools/ast_tools.py +0 -0
  27. {aru_code-0.13.1 → aru_code-0.13.3}/aru/tools/gitignore.py +0 -0
  28. {aru_code-0.13.1 → aru_code-0.13.3}/aru/tools/mcp_client.py +0 -0
  29. {aru_code-0.13.1 → aru_code-0.13.3}/aru/tools/ranker.py +0 -0
  30. {aru_code-0.13.1 → aru_code-0.13.3}/aru/tools/tasklist.py +0 -0
  31. {aru_code-0.13.1 → aru_code-0.13.3}/aru_code.egg-info/SOURCES.txt +0 -0
  32. {aru_code-0.13.1 → aru_code-0.13.3}/aru_code.egg-info/dependency_links.txt +0 -0
  33. {aru_code-0.13.1 → aru_code-0.13.3}/aru_code.egg-info/entry_points.txt +0 -0
  34. {aru_code-0.13.1 → aru_code-0.13.3}/aru_code.egg-info/requires.txt +0 -0
  35. {aru_code-0.13.1 → aru_code-0.13.3}/aru_code.egg-info/top_level.txt +0 -0
  36. {aru_code-0.13.1 → aru_code-0.13.3}/setup.cfg +0 -0
  37. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_agents_base.py +0 -0
  38. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_ast_tools.py +0 -0
  39. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_cli.py +0 -0
  40. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_cli_advanced.py +0 -0
  41. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_cli_base.py +0 -0
  42. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_cli_completers.py +0 -0
  43. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_cli_new.py +0 -0
  44. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_cli_run_cli.py +0 -0
  45. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_cli_session.py +0 -0
  46. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_cli_shell.py +0 -0
  47. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_codebase.py +0 -0
  48. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_config.py +0 -0
  49. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_context.py +0 -0
  50. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_executor.py +0 -0
  51. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_gitignore.py +0 -0
  52. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_main.py +0 -0
  53. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_mcp_client.py +0 -0
  54. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_permissions.py +0 -0
  55. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_planner.py +0 -0
  56. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_providers.py +0 -0
  57. {aru_code-0.13.1 → aru_code-0.13.3}/tests/test_ranker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.13.1
3
+ Version: 0.13.3
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.13.3"
@@ -214,18 +214,29 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
214
214
  _render_input_separator()
215
215
  model_tb = session.model_display
216
216
  from prompt_toolkit.formatted_text import HTML
217
+
218
+ def _bottom_toolbar():
219
+ mcp_part = ""
220
+ if ctx.mcp_loaded_msg:
221
+ mcp_part = (
222
+ f' <style fg="ansigray">│</style>'
223
+ f' <style fg="ansigray">{ctx.mcp_loaded_msg}</style>'
224
+ )
225
+ return HTML(
226
+ f' <style fg="ansigray">{model_tb}</style>'
227
+ f' <style fg="ansigray">│</style>'
228
+ f' <style fg="ansigray">/help</style>'
229
+ f' <style fg="ansigray">│</style>'
230
+ f' <style fg="ansigray">Esc+Enter newline</style>'
231
+ f'{mcp_part}'
232
+ )
233
+
217
234
  user_text = (
218
235
  await asyncio.to_thread(
219
236
  prompt_session.prompt,
220
237
  HTML('<b><ansigreen>❯</ansigreen></b> '),
221
238
  multiline=False,
222
- bottom_toolbar=HTML(
223
- f' <style fg="ansigray">{model_tb}</style>'
224
- f' <style fg="ansigray">│</style>'
225
- f' <style fg="ansigray">/help</style>'
226
- f' <style fg="ansigray">│</style>'
227
- f' <style fg="ansigray">Esc+Enter newline</style>'
228
- ),
239
+ bottom_toolbar=_bottom_toolbar,
229
240
  )
230
241
  ).strip()
231
242
  _render_input_separator()
@@ -10,8 +10,6 @@ from __future__ import annotations
10
10
 
11
11
  # ── Constants ──────────────────────────────────────────────────────
12
12
 
13
- # Pruning: protect the most recent N chars of content from eviction
14
- PRUNE_PROTECT_CHARS = 50_000 # ~14K tokens
15
13
  # Pruning: minimum chars that must be freeable to justify a prune pass
16
14
  PRUNE_MINIMUM_CHARS = 20_000 # ~5.7K tokens
17
15
  # Placeholder that replaces evicted content
@@ -27,8 +25,10 @@ TRUNCATE_MAX_BYTES = 20 * 1024 # 20 KB
27
25
  TRUNCATE_KEEP_START = 350 # lines to keep from the start
28
26
  TRUNCATE_KEEP_END = 100 # lines to keep from the end
29
27
 
30
- # Compaction: trigger when cumulative input tokens exceed this fraction of model limit
28
+ # Compaction: trigger when per-run input tokens exceed this fraction of model limit
31
29
  COMPACTION_THRESHOLD_RATIO = 0.85
30
+ # Compaction: target post-compaction size as fraction of model context limit
31
+ COMPACTION_TARGET_RATIO = 0.15
32
32
  # Default model context limits (input tokens)
33
33
  MODEL_CONTEXT_LIMITS: dict[str, int] = {
34
34
  # Anthropic
@@ -83,12 +83,27 @@ Be concise but complete. This summary replaces the full conversation history."""
83
83
 
84
84
  # ── Layer 1: Pruning ──────────────────────────────────────────────
85
85
 
86
- def prune_history(history: list[dict[str, str]]) -> list[dict[str, str]]:
86
+ def _get_prune_protect_chars(model_id: str = "default") -> int:
87
+ """Scale protection window based on model context size.
88
+
89
+ Larger models get more protection; smaller models prune more aggressively
90
+ to delay compaction. Returns ~10% of the model's context in chars (~3.5 chars/token).
91
+ """
92
+ limit = MODEL_CONTEXT_LIMITS.get(model_id, MODEL_CONTEXT_LIMITS["default"])
93
+ # ~3.5 chars per token, protect ~10% of context
94
+ protect = int(limit * 0.10 * 3.5)
95
+ # Clamp between 20K (minimum usable) and 80K (diminishing returns)
96
+ return max(20_000, min(protect, 80_000))
97
+
98
+
99
+ def prune_history(
100
+ history: list[dict[str, str]], model_id: str = "default"
101
+ ) -> list[dict[str, str]]:
87
102
  """Replace old messages with a short placeholder to reduce tokens.
88
103
 
89
104
  Walks backward through history, protecting the most recent content
90
- (up to PRUNE_PROTECT_CHARS total across both roles). Older messages
91
- beyond that budget are pruned:
105
+ (scaled to the model's context size). Older messages beyond that
106
+ budget are pruned:
92
107
  - Assistant messages: replaced entirely with placeholder
93
108
  - User messages over PRUNE_USER_MSG_THRESHOLD: truncated to first N chars
94
109
 
@@ -97,11 +112,13 @@ def prune_history(history: list[dict[str, str]]) -> list[dict[str, str]]:
97
112
  if len(history) <= 2:
98
113
  return list(history)
99
114
 
115
+ protect_chars = _get_prune_protect_chars(model_id)
116
+
100
117
  # Calculate total prunable chars (both roles)
101
118
  total_chars = sum(len(msg["content"]) for msg in history)
102
119
 
103
120
  # Not enough to prune
104
- if total_chars < PRUNE_PROTECT_CHARS + PRUNE_MINIMUM_CHARS:
121
+ if total_chars < protect_chars + PRUNE_MINIMUM_CHARS:
105
122
  return list(history)
106
123
 
107
124
  # Walk backward, protecting recent content
@@ -112,7 +129,7 @@ def prune_history(history: list[dict[str, str]]) -> list[dict[str, str]]:
112
129
  msg = result[i]
113
130
  msg_len = len(msg["content"])
114
131
 
115
- if protected + msg_len <= PRUNE_PROTECT_CHARS:
132
+ if protected + msg_len <= protect_chars:
116
133
  # Still within protection window
117
134
  protected += msg_len
118
135
  else:
@@ -121,8 +138,6 @@ def prune_history(history: list[dict[str, str]]) -> list[dict[str, str]]:
121
138
  if msg["content"] != PRUNED_PLACEHOLDER:
122
139
  result[i] = {"role": "assistant", "content": PRUNED_PLACEHOLDER}
123
140
  elif msg["role"] == "user" and msg_len > PRUNE_USER_MSG_THRESHOLD:
124
- # Large user messages (e.g. @file mentions with file contents)
125
- # are truncated to keep a brief summary of the original request
126
141
  truncated = msg["content"][:PRUNE_USER_MSG_KEEP] + \
127
142
  f"\n\n[... {msg_len - PRUNE_USER_MSG_KEEP:,} chars pruned to save context ...]"
128
143
  result[i] = {"role": "user", "content": truncated}
@@ -181,21 +196,79 @@ def truncate_output(text: str) -> str:
181
196
 
182
197
  # ── Layer 3: Compaction ───────────────────────────────────────────
183
198
 
184
- def should_compact(total_input_tokens: int, model_id: str = "default") -> bool:
185
- """Check if the conversation should be compacted based on token usage."""
199
+ def estimate_history_tokens(history: list[dict[str, str]]) -> int:
200
+ """Estimate token count from conversation history chars (~3.5 chars/token)."""
201
+ total_chars = sum(len(msg["content"]) for msg in history)
202
+ return int(total_chars / 3.5)
203
+
204
+
205
+ def should_compact(
206
+ history_or_tokens: int | list[dict[str, str]],
207
+ model_id: str = "default",
208
+ ) -> bool:
209
+ """Check if the conversation should be compacted (reactive, post-run).
210
+
211
+ Accepts either an estimated token count (int) or the history list
212
+ (from which tokens are estimated via char count).
213
+ """
214
+ if isinstance(history_or_tokens, list):
215
+ tokens = estimate_history_tokens(history_or_tokens)
216
+ else:
217
+ tokens = history_or_tokens
186
218
  limit = MODEL_CONTEXT_LIMITS.get(model_id, MODEL_CONTEXT_LIMITS["default"])
187
219
  threshold = int(limit * COMPACTION_THRESHOLD_RATIO)
188
- return total_input_tokens >= threshold
220
+ return tokens >= threshold
221
+
222
+
223
+ def would_prune(history: list[dict[str, str]], model_id: str = "default") -> bool:
224
+ """Check if prune_history would discard content from this history.
225
+
226
+ Uses the exact same criteria as prune_history: total chars exceed
227
+ the protection window + minimum prunable threshold.
228
+ """
229
+ if len(history) <= 2:
230
+ return False
231
+ total_chars = sum(len(msg["content"]) for msg in history)
232
+ protect_chars = _get_prune_protect_chars(model_id)
233
+ return total_chars >= protect_chars + PRUNE_MINIMUM_CHARS
234
+
189
235
 
236
+ def _split_history(history: list[dict[str, str]], model_id: str = "default") -> tuple[list[dict[str, str]], list[dict[str, str]]]:
237
+ """Split history into old (to summarize) and recent (to keep intact).
238
+
239
+ Uses the same protection window as pruning.
240
+ """
241
+ protect_chars = _get_prune_protect_chars(model_id)
242
+ protected = 0
243
+ split_idx = len(history)
244
+ for i in range(len(history) - 1, -1, -1):
245
+ msg_len = len(history[i]["content"])
246
+ if protected + msg_len <= protect_chars:
247
+ protected += msg_len
248
+ split_idx = i
249
+ else:
250
+ break
251
+ return history[:split_idx], history[split_idx:]
252
+
253
+
254
+ def build_compaction_prompt(
255
+ history: list[dict[str, str]],
256
+ plan_task: str | None = None,
257
+ model_id: str = "default",
258
+ ) -> str:
259
+ """Build the prompt sent to the compaction agent.
260
+
261
+ Only includes OLD messages (outside the protection window) for
262
+ summarization. Recent messages are kept intact by apply_compaction.
263
+ """
264
+ old_msgs, _ = _split_history(history, model_id)
190
265
 
191
- def build_compaction_prompt(history: list[dict[str, str]], plan_task: str | None = None) -> str:
192
- """Build the prompt sent to the compaction agent to summarize the conversation."""
193
266
  parts = [COMPACTION_TEMPLATE, "\n\n---\n\n## Conversation to summarize:\n"]
194
267
 
195
268
  if plan_task:
196
269
  parts.append(f"**Active task:** {plan_task}\n\n")
197
270
 
198
- for msg in history:
271
+ for msg in old_msgs:
199
272
  role = msg["role"].upper()
200
273
  content = msg["content"]
201
274
  # Cap individual messages in the compaction input to avoid blowing up
@@ -206,26 +279,22 @@ def build_compaction_prompt(history: list[dict[str, str]], plan_task: str | None
206
279
  return "".join(parts)
207
280
 
208
281
 
209
- def apply_compaction(history: list[dict[str, str]], summary: str) -> list[dict[str, str]]:
210
- """Replace history with a compaction summary + the most recent exchange."""
282
+
283
+ def apply_compaction(
284
+ history: list[dict[str, str]], summary: str, model_id: str = "default"
285
+ ) -> list[dict[str, str]]:
286
+ """Replace OLD messages with a summary, keep RECENT messages intact.
287
+
288
+ Uses the same protection window as pruning: recent messages within
289
+ the window are preserved as-is, older messages are replaced by a
290
+ compaction summary. This preserves the natural conversation flow.
291
+ """
292
+ _, recent = _split_history(history, model_id)
293
+
211
294
  compacted = [
212
295
  {"role": "user", "content": f"[Conversation compacted]\n\n{summary}"}
213
296
  ]
214
- # Keep the last user message and last assistant message for continuity
215
- last_user = None
216
- last_assistant = None
217
- for msg in reversed(history):
218
- if msg["role"] == "user" and last_user is None:
219
- last_user = msg
220
- elif msg["role"] == "assistant" and last_assistant is None:
221
- last_assistant = msg
222
- if last_user and last_assistant:
223
- break
224
-
225
- if last_assistant:
226
- compacted.append(last_assistant)
227
- if last_user and last_user != compacted[0]:
228
- compacted.append(last_user)
297
+ compacted.extend(recent)
229
298
 
230
299
  return compacted
231
300
 
@@ -234,6 +303,7 @@ async def compact_conversation(
234
303
  history: list[dict[str, str]],
235
304
  model_ref: str,
236
305
  plan_task: str | None = None,
306
+ model_id: str = "default",
237
307
  ) -> list[dict[str, str]]:
238
308
  """Run the compaction agent to summarize and replace history.
239
309
 
@@ -243,7 +313,7 @@ async def compact_conversation(
243
313
  from aru.runtime import get_ctx
244
314
  from aru.providers import create_model
245
315
 
246
- prompt = build_compaction_prompt(history, plan_task)
316
+ prompt = build_compaction_prompt(history, plan_task, model_id=model_id)
247
317
 
248
318
  try:
249
319
  from agno.agent import Agent
@@ -263,12 +333,12 @@ async def compact_conversation(
263
333
  # Fallback: simple mechanical summary
264
334
  summary = _fallback_summary(history, plan_task)
265
335
 
266
- return apply_compaction(history, summary)
336
+ return apply_compaction(history, summary, model_id=model_id)
267
337
 
268
338
  except Exception:
269
339
  # Fallback if agent fails
270
340
  summary = _fallback_summary(history, plan_task)
271
- return apply_compaction(history, summary)
341
+ return apply_compaction(history, summary, model_id=model_id)
272
342
 
273
343
 
274
344
  def _fallback_summary(history: list[dict[str, str]], plan_task: str | None = None) -> str:
@@ -115,11 +115,25 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
115
115
  run_message = message
116
116
 
117
117
  # Build conversation history as real messages for the LLM
118
- from aru.context import prune_history
118
+ # Compact BEFORE pruning: if the history is large enough that pruning
119
+ # would discard content, compact first to preserve context via summary
120
+ # instead of losing it to placeholders.
121
+ from aru.context import prune_history, should_compact, compact_conversation, would_prune
122
+ if session and session.history and not lightweight:
123
+ if would_prune(session.history, model_id=session.model_id):
124
+ try:
125
+ session.history = await compact_conversation(
126
+ session.history, session.model_ref, session.plan_task,
127
+ model_id=session.model_id,
128
+ )
129
+ console.print("[dim]Context compacted to save tokens.[/dim]")
130
+ except Exception:
131
+ pass
132
+
119
133
  history_messages: list[Message] = []
120
134
  if session and session.history and not lightweight:
121
135
  prior_history = session.history[:-1]
122
- pruned = prune_history(prior_history)
136
+ pruned = prune_history(prior_history, model_id=session.model_id)
123
137
  for msg in pruned:
124
138
  history_messages.append(Message(role=msg["role"], content=msg["content"], from_history=True))
125
139
 
@@ -228,11 +242,16 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
228
242
  if run_output and session and hasattr(run_output, "metrics"):
229
243
  session.track_tokens(run_output.metrics)
230
244
 
231
- from aru.context import should_compact, compact_conversation
232
- if should_compact(session.total_input_tokens, session.model_id):
245
+ # Reactive compaction: use per-run input_tokens (sum of all API
246
+ # calls within this arun) as a conservative proxy for context pressure.
247
+ # session.history doesn't include tool results, so char-based estimates
248
+ # would miss the bulk of the context sent to the model.
249
+ run_input_tokens = getattr(run_output.metrics, "input_tokens", 0) or 0
250
+ if should_compact(run_input_tokens, session.model_id):
233
251
  try:
234
252
  session.history = await compact_conversation(
235
- session.history, session.model_ref, session.plan_task
253
+ session.history, session.model_ref, session.plan_task,
254
+ model_id=session.model_id,
236
255
  )
237
256
  console.print("[dim]Context compacted to save tokens.[/dim]")
238
257
  except Exception:
@@ -117,6 +117,7 @@ class RuntimeContext:
117
117
 
118
118
  # -- MCP --
119
119
  mcp_catalog_text: str = ""
120
+ mcp_loaded_msg: str = ""
120
121
 
121
122
 
122
123
  # ── ContextVar plumbing ──────────────────────────────────────────────
@@ -1232,7 +1232,7 @@ async def load_mcp_tools(eager: bool = False):
1232
1232
  if eager:
1233
1233
  # Legacy: each MCP tool = one Agno Function (expensive)
1234
1234
  mcp_tools = manager.get_eager_tools()
1235
- get_ctx().console.print(f"[dim]Loaded {tool_count} tools from MCP servers (eager mode).[/dim]")
1235
+ get_ctx().mcp_loaded_msg = f"Loaded {tool_count} tools from MCP servers (eager mode)."
1236
1236
  for t in mcp_tools:
1237
1237
  ALL_TOOLS.append(t)
1238
1238
  EXECUTOR_TOOLS.append(t)
@@ -1245,10 +1245,10 @@ async def load_mcp_tools(eager: bool = False):
1245
1245
  GENERAL_TOOLS.append(gateway)
1246
1246
  # Store catalog text for injection into system prompt
1247
1247
  get_ctx().mcp_catalog_text = manager.get_catalog_text()
1248
- get_ctx().console.print(f"[dim]Loaded {tool_count} tools from MCP servers.[/dim]")
1248
+ get_ctx().mcp_loaded_msg = f"Loaded {tool_count} tools from MCP servers."
1249
1249
 
1250
1250
  except Exception as e:
1251
- get_ctx().console.print(f"[dim]Failed to load MCP tools: {e}[/dim]")
1251
+ get_ctx().mcp_loaded_msg = f"Failed to load MCP tools: {e}"
1252
1252
 
1253
1253
 
1254
1254
  def _build_mcp_gateway(manager):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.13.1
3
+ Version: 0.13.3
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aru-code"
7
- version = "0.13.1"
7
+ version = "0.13.3"
8
8
  description = "A Claude Code clone built with Agno agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "0.13.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes