henchman-ai 0.1.13__py3-none-any.whl → 0.1.14__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.
- henchman/cli/input.py +2 -2
- henchman/cli/repl.py +44 -8
- henchman/core/session.py +18 -4
- henchman/tools/builtins/__init__.py +2 -0
- henchman/tools/builtins/shell.py +4 -0
- henchman/tools/builtins/web_search.py +129 -0
- henchman_ai-0.1.14.dist-info/METADATA +317 -0
- {henchman_ai-0.1.13.dist-info → henchman_ai-0.1.14.dist-info}/RECORD +11 -10
- henchman_ai-0.1.13.dist-info/METADATA +0 -144
- {henchman_ai-0.1.13.dist-info → henchman_ai-0.1.14.dist-info}/WHEEL +0 -0
- {henchman_ai-0.1.13.dist-info → henchman_ai-0.1.14.dist-info}/entry_points.txt +0 -0
- {henchman_ai-0.1.13.dist-info → henchman_ai-0.1.14.dist-info}/licenses/LICENSE +0 -0
henchman/cli/input.py
CHANGED
|
@@ -210,14 +210,14 @@ class KeyMonitor:
|
|
|
210
210
|
self._in_raw_mode = True
|
|
211
211
|
with self.input.attach(lambda: None):
|
|
212
212
|
while not self._stop_event.is_set() and not self._exit_event.is_set() and self._suspended.is_set():
|
|
213
|
-
# Check for keys every
|
|
213
|
+
# Check for keys every 10ms
|
|
214
214
|
keys = self.input.read_keys()
|
|
215
215
|
for key in keys:
|
|
216
216
|
if key.key == Keys.Escape:
|
|
217
217
|
self._stop_event.set()
|
|
218
218
|
elif key.key == Keys.ControlC:
|
|
219
219
|
self._exit_event.set()
|
|
220
|
-
await asyncio.sleep(0.
|
|
220
|
+
await asyncio.sleep(0.01)
|
|
221
221
|
finally:
|
|
222
222
|
self._in_raw_mode = False
|
|
223
223
|
# If we were suspended, wait a bit before potentially re-entering raw mode
|
henchman/cli/repl.py
CHANGED
|
@@ -198,6 +198,30 @@ class Repl:
|
|
|
198
198
|
|
|
199
199
|
return status
|
|
200
200
|
|
|
201
|
+
def _get_rich_status_message(self) -> str:
|
|
202
|
+
"""Get rich status message for persistent display."""
|
|
203
|
+
from henchman.utils.tokens import TokenCounter
|
|
204
|
+
|
|
205
|
+
parts = []
|
|
206
|
+
|
|
207
|
+
# Plan Mode
|
|
208
|
+
plan_mode = self.session.plan_mode if self.session else False
|
|
209
|
+
parts.append("[yellow]PLAN[/]" if plan_mode else "[blue]CHAT[/]")
|
|
210
|
+
|
|
211
|
+
# Tokens
|
|
212
|
+
try:
|
|
213
|
+
msgs = self.agent.get_messages_for_api()
|
|
214
|
+
tokens = TokenCounter.count_messages(msgs)
|
|
215
|
+
parts.append(f"Tokens: ~[cyan]{tokens}[/]")
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
# RAG Status
|
|
220
|
+
if self.rag_system and getattr(self.rag_system, "is_indexing", False):
|
|
221
|
+
parts.append("[cyan]RAG: Indexing...[/]")
|
|
222
|
+
|
|
223
|
+
return " | ".join(parts)
|
|
224
|
+
|
|
201
225
|
def _register_builtin_tools(self) -> None:
|
|
202
226
|
"""Register built-in tools with the registry."""
|
|
203
227
|
from henchman.tools.builtins import (
|
|
@@ -209,6 +233,7 @@ class Repl:
|
|
|
209
233
|
ReadFileTool,
|
|
210
234
|
ShellTool,
|
|
211
235
|
WebFetchTool,
|
|
236
|
+
DuckDuckGoSearchTool,
|
|
212
237
|
WriteFileTool,
|
|
213
238
|
)
|
|
214
239
|
|
|
@@ -222,6 +247,7 @@ class Repl:
|
|
|
222
247
|
GrepTool(),
|
|
223
248
|
ShellTool(),
|
|
224
249
|
WebFetchTool(),
|
|
250
|
+
DuckDuckGoSearchTool(),
|
|
225
251
|
]
|
|
226
252
|
for tool in tools:
|
|
227
253
|
self.tool_registry.register(tool)
|
|
@@ -322,6 +348,8 @@ class Repl:
|
|
|
322
348
|
KeyboardInterrupt: If user presses Ctrl+C.
|
|
323
349
|
EOFError: If user presses Ctrl+D.
|
|
324
350
|
"""
|
|
351
|
+
# Ensure a fresh line for the prompt
|
|
352
|
+
self.console.print()
|
|
325
353
|
return await self.prompt_session.prompt_async(self.config.prompt)
|
|
326
354
|
|
|
327
355
|
async def process_input(self, user_input: str) -> bool:
|
|
@@ -417,13 +445,16 @@ class Repl:
|
|
|
417
445
|
self.current_monitor = monitor
|
|
418
446
|
monitor_task = asyncio.create_task(monitor.monitor())
|
|
419
447
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
448
|
+
from rich.status import Status
|
|
449
|
+
with self.console.status(self._get_rich_status_message(), spinner="dots") as status_obj:
|
|
450
|
+
# Run the agent stream processing as a separate task so we can cancel it
|
|
451
|
+
agent_task = asyncio.create_task(
|
|
452
|
+
self._process_agent_stream(
|
|
453
|
+
self.agent.run(user_input),
|
|
454
|
+
assistant_content,
|
|
455
|
+
status_obj
|
|
456
|
+
)
|
|
425
457
|
)
|
|
426
|
-
)
|
|
427
458
|
|
|
428
459
|
try:
|
|
429
460
|
while not agent_task.done():
|
|
@@ -437,7 +468,7 @@ class Repl:
|
|
|
437
468
|
agent_task.cancel()
|
|
438
469
|
break
|
|
439
470
|
# Small sleep to keep the loop responsive
|
|
440
|
-
await asyncio.sleep(0
|
|
471
|
+
await asyncio.sleep(0)
|
|
441
472
|
|
|
442
473
|
if not agent_task.done():
|
|
443
474
|
try:
|
|
@@ -458,7 +489,8 @@ class Repl:
|
|
|
458
489
|
async def _process_agent_stream(
|
|
459
490
|
self,
|
|
460
491
|
event_stream: AsyncIterator[AgentEvent],
|
|
461
|
-
content_collector: list[str] | None = None
|
|
492
|
+
content_collector: list[str] | None = None,
|
|
493
|
+
status_obj: Status | None = None, # New parameter
|
|
462
494
|
) -> None:
|
|
463
495
|
"""Process an agent event stream, handling tool calls properly.
|
|
464
496
|
|
|
@@ -499,6 +531,10 @@ class Repl:
|
|
|
499
531
|
accumulated_content: list[str] = []
|
|
500
532
|
|
|
501
533
|
async for event in event_stream:
|
|
534
|
+
# Update status continuously
|
|
535
|
+
if status_obj:
|
|
536
|
+
status_obj.update(self._get_rich_status_message())
|
|
537
|
+
|
|
502
538
|
if event.type == EventType.CONTENT:
|
|
503
539
|
# Stream content to console
|
|
504
540
|
self.console.print(event.data, end="")
|
henchman/core/session.py
CHANGED
|
@@ -353,18 +353,32 @@ class SessionManager:
|
|
|
353
353
|
"""Load a session from disk.
|
|
354
354
|
|
|
355
355
|
Args:
|
|
356
|
-
session_id: ID of session to load.
|
|
356
|
+
session_id: ID or ID prefix of session to load.
|
|
357
357
|
|
|
358
358
|
Returns:
|
|
359
359
|
Loaded Session instance.
|
|
360
360
|
|
|
361
361
|
Raises:
|
|
362
|
-
FileNotFoundError: If session doesn't exist.
|
|
362
|
+
FileNotFoundError: If session doesn't exist or is ambiguous.
|
|
363
363
|
"""
|
|
364
364
|
path = self._get_session_path(session_id)
|
|
365
|
-
if
|
|
365
|
+
if path.exists():
|
|
366
|
+
return Session.from_json(path.read_text())
|
|
367
|
+
|
|
368
|
+
# If not found by exact ID, try as prefix
|
|
369
|
+
if not self.data_dir.exists():
|
|
370
|
+
raise FileNotFoundError(f"Session not found: {session_id}")
|
|
371
|
+
|
|
372
|
+
matches = list(self.data_dir.glob(f"{session_id}*.json"))
|
|
373
|
+
if not matches:
|
|
366
374
|
raise FileNotFoundError(f"Session not found: {session_id}")
|
|
367
|
-
|
|
375
|
+
|
|
376
|
+
if len(matches) > 1:
|
|
377
|
+
# Prefer exact match if somehow multiple match prefix but one is exact
|
|
378
|
+
# (though glob already failed exact match if we are here)
|
|
379
|
+
raise ValueError(f"Ambiguous session ID prefix: {session_id}")
|
|
380
|
+
|
|
381
|
+
return Session.from_json(matches[0].read_text())
|
|
368
382
|
|
|
369
383
|
def load_by_tag(
|
|
370
384
|
self,
|
|
@@ -10,6 +10,7 @@ from henchman.tools.builtins.ls import LsTool
|
|
|
10
10
|
from henchman.tools.builtins.rag_search import RagSearchTool
|
|
11
11
|
from henchman.tools.builtins.shell import ShellTool
|
|
12
12
|
from henchman.tools.builtins.web_fetch import WebFetchTool
|
|
13
|
+
from henchman.tools.builtins.web_search import DuckDuckGoSearchTool
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
"AskUserTool",
|
|
@@ -21,5 +22,6 @@ __all__ = [
|
|
|
21
22
|
"ReadFileTool",
|
|
22
23
|
"ShellTool",
|
|
23
24
|
"WebFetchTool",
|
|
25
|
+
"DuckDuckGoSearchTool",
|
|
24
26
|
"WriteFileTool",
|
|
25
27
|
]
|
henchman/tools/builtins/shell.py
CHANGED
|
@@ -101,6 +101,10 @@ class ShellTool(Tool):
|
|
|
101
101
|
success=False,
|
|
102
102
|
error=f"Timeout after {timeout} seconds",
|
|
103
103
|
)
|
|
104
|
+
except asyncio.CancelledError:
|
|
105
|
+
process.kill()
|
|
106
|
+
await process.wait()
|
|
107
|
+
raise
|
|
104
108
|
|
|
105
109
|
# Decode output
|
|
106
110
|
stdout_text = stdout.decode("utf-8", errors="replace")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Web search tool implementation using DuckDuckGo."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from henchman.tools.base import Tool, ToolKind, ToolResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DuckDuckGoSearchTool(Tool):
|
|
10
|
+
"""Search the web using DuckDuckGo.
|
|
11
|
+
|
|
12
|
+
This tool searches DuckDuckGo and returns a summary of the results.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def name(self) -> str:
|
|
17
|
+
"""Tool name."""
|
|
18
|
+
return "web_search"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def description(self) -> str:
|
|
22
|
+
"""Tool description."""
|
|
23
|
+
return "Search the web for information."
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def parameters(self) -> dict[str, object]:
|
|
27
|
+
"""JSON Schema for parameters."""
|
|
28
|
+
return {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"query": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "The search query",
|
|
34
|
+
},
|
|
35
|
+
"max_results": {
|
|
36
|
+
"type": "integer",
|
|
37
|
+
"description": "Maximum number of results to return",
|
|
38
|
+
"default": 5,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
"required": ["query"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def kind(self) -> ToolKind:
|
|
46
|
+
"""Tool kind - NETWORK requires confirmation."""
|
|
47
|
+
return ToolKind.NETWORK
|
|
48
|
+
|
|
49
|
+
async def execute( # type: ignore[override]
|
|
50
|
+
|
|
51
|
+
self,
|
|
52
|
+
query: str = "",
|
|
53
|
+
max_results: int = 5,
|
|
54
|
+
**kwargs: object, # noqa: ARG002
|
|
55
|
+
) -> ToolResult:
|
|
56
|
+
"""Execute web search.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
query: Search query.
|
|
60
|
+
max_results: Maximum results to return.
|
|
61
|
+
**kwargs: Additional arguments (ignored).
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
ToolResult with search results.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
import aiohttp
|
|
68
|
+
from bs4 import BeautifulSoup
|
|
69
|
+
except ImportError:
|
|
70
|
+
return ToolResult(
|
|
71
|
+
content="Error: aiohttp and beautifulsoup4 are required for web_search",
|
|
72
|
+
success=False,
|
|
73
|
+
error="Dependencies not installed. Please run: pip install aiohttp beautifulsoup4",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# DuckDuckGo HTML search URL
|
|
78
|
+
url = f"https://html.duckduckgo.com/html/?q={query}"
|
|
79
|
+
headers = {
|
|
80
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async with (
|
|
84
|
+
aiohttp.ClientSession(headers=headers) as session,
|
|
85
|
+
session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response,
|
|
86
|
+
):
|
|
87
|
+
if response.status >= 400:
|
|
88
|
+
return ToolResult(
|
|
89
|
+
content=f"Search failed with HTTP {response.status}",
|
|
90
|
+
success=False,
|
|
91
|
+
error=f"HTTP {response.status}",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
html = await response.text()
|
|
95
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
96
|
+
|
|
97
|
+
results = []
|
|
98
|
+
# DuckDuckGo HTML results are usually in 'result' class divs
|
|
99
|
+
for i, result in enumerate(soup.find_all("div", class_="result")):
|
|
100
|
+
if i >= max_results:
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
title_elem = result.find("a", class_="result__a")
|
|
104
|
+
snippet_elem = result.find("a", class_="result__snippet")
|
|
105
|
+
|
|
106
|
+
if title_elem:
|
|
107
|
+
title = title_elem.get_text(strip=True)
|
|
108
|
+
link = title_elem.get("href", "")
|
|
109
|
+
snippet = snippet_elem.get_text(strip=True) if snippet_elem else ""
|
|
110
|
+
|
|
111
|
+
results.append(f"### {title}\nURL: {link}\n{snippet}\n")
|
|
112
|
+
|
|
113
|
+
if not results:
|
|
114
|
+
return ToolResult(
|
|
115
|
+
content="No results found.",
|
|
116
|
+
success=True,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return ToolResult(
|
|
120
|
+
content="\n".join(results),
|
|
121
|
+
success=True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return ToolResult(
|
|
126
|
+
content=f"Error performing search: {e}",
|
|
127
|
+
success=False,
|
|
128
|
+
error=str(e),
|
|
129
|
+
)
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: henchman-ai
|
|
3
|
+
Version: 0.1.14
|
|
4
|
+
Summary: A model-agnostic AI agent CLI - your AI henchman for the terminal
|
|
5
|
+
Project-URL: Homepage, https://github.com/MGPowerlytics/henchman-ai
|
|
6
|
+
Project-URL: Repository, https://github.com/MGPowerlytics/henchman-ai
|
|
7
|
+
Project-URL: Documentation, https://github.com/MGPowerlytics/henchman-ai/tree/main/docs
|
|
8
|
+
Project-URL: Changelog, https://github.com/MGPowerlytics/henchman-ai/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/MGPowerlytics/henchman-ai/issues
|
|
10
|
+
Project-URL: Discussions, https://github.com/MGPowerlytics/henchman-ai/discussions
|
|
11
|
+
Author-email: Matthew <matthew@example.com>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: agent,ai,anthropic,assistant,cli,deepseek,henchman,llm,openai,terminal
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Environment :: Console
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
28
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
30
|
+
Classifier: Topic :: Utilities
|
|
31
|
+
Classifier: Typing :: Typed
|
|
32
|
+
Requires-Python: >=3.10
|
|
33
|
+
Requires-Dist: aiohttp>=3.9
|
|
34
|
+
Requires-Dist: anthropic>=0.40
|
|
35
|
+
Requires-Dist: anyio>=4.0
|
|
36
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
37
|
+
Requires-Dist: chromadb>=0.4
|
|
38
|
+
Requires-Dist: click>=8.0
|
|
39
|
+
Requires-Dist: fastembed>=0.3
|
|
40
|
+
Requires-Dist: httpx>=0.27
|
|
41
|
+
Requires-Dist: mcp>=1.0
|
|
42
|
+
Requires-Dist: openai>=1.40
|
|
43
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
44
|
+
Requires-Dist: pydantic>=2.0
|
|
45
|
+
Requires-Dist: pyyaml>=6.0
|
|
46
|
+
Requires-Dist: rich>=13.0
|
|
47
|
+
Requires-Dist: tiktoken>=0.5
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest-mock>=3.0; extra == 'dev'
|
|
53
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
54
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
55
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
56
|
+
Description-Content-Type: text/markdown
|
|
57
|
+
|
|
58
|
+
# Henchman-AI
|
|
59
|
+
|
|
60
|
+
> Your AI Henchman for the Terminal - A Model-Agnostic AI Agent CLI
|
|
61
|
+
|
|
62
|
+
[](https://pypi.org/project/henchman-ai/)
|
|
63
|
+
[](https://pypi.org/project/henchman-ai/)
|
|
64
|
+
[](https://opensource.org/licenses/MIT)
|
|
65
|
+
|
|
66
|
+
Henchman-AI is a powerful, terminal-based AI agent that supports multiple LLM providers (DeepSeek, OpenAI, Anthropic, Ollama, and more) through a unified interface. Inspired by gemini-cli, built for extensibility and production use.
|
|
67
|
+
|
|
68
|
+
## ✨ Features
|
|
69
|
+
|
|
70
|
+
- 🔄 **Model-Agnostic**: Support any LLM provider through a unified abstraction layer
|
|
71
|
+
- 🐍 **Pythonic**: Leverages Python's async ecosystem and rich libraries for optimal performance
|
|
72
|
+
- 🔌 **Extensible**: Plugin system for tools, providers, and custom commands
|
|
73
|
+
- 🚀 **Production-Ready**: Proper error handling, comprehensive testing, and semantic versioning
|
|
74
|
+
- 🛠️ **Tool Integration**: Built-in support for file operations, web search, code execution, and more
|
|
75
|
+
- ⚡ **Fast & Efficient**: Async-first design with intelligent caching and rate limiting
|
|
76
|
+
- 🔒 **Secure**: Environment-based configuration and safe execution sandboxing
|
|
77
|
+
|
|
78
|
+
## 📦 Installation
|
|
79
|
+
|
|
80
|
+
### From PyPI (Recommended)
|
|
81
|
+
```bash
|
|
82
|
+
pip install henchman-ai
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### From Source
|
|
86
|
+
```bash
|
|
87
|
+
git clone https://github.com/MGPowerlytics/henchman-ai.git
|
|
88
|
+
cd henchman-ai
|
|
89
|
+
pip install -e ".[dev]"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### With uv (Fastest)
|
|
93
|
+
```bash
|
|
94
|
+
uv pip install henchman-ai
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 🚀 Quick Start
|
|
98
|
+
|
|
99
|
+
1. **Set your API key** (choose your preferred provider):
|
|
100
|
+
```bash
|
|
101
|
+
export DEEPSEEK_API_KEY="your-api-key-here"
|
|
102
|
+
# or
|
|
103
|
+
export OPENAI_API_KEY="your-api-key-here"
|
|
104
|
+
# or
|
|
105
|
+
export ANTHROPIC_API_KEY="your-api-key-here"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
2. **Start the CLI**:
|
|
109
|
+
```bash
|
|
110
|
+
henchman
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
3. **Or run with a prompt directly**:
|
|
114
|
+
```bash
|
|
115
|
+
henchman --prompt "Explain this Python code" < example.py
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 📖 Usage Examples
|
|
119
|
+
|
|
120
|
+
### Basic Commands
|
|
121
|
+
```bash
|
|
122
|
+
# Show version
|
|
123
|
+
henchman --version
|
|
124
|
+
|
|
125
|
+
# Show help
|
|
126
|
+
henchman --help
|
|
127
|
+
|
|
128
|
+
# Interactive mode (default)
|
|
129
|
+
henchman
|
|
130
|
+
|
|
131
|
+
# Headless mode with prompt
|
|
132
|
+
henchman -p "Summarize the key points from README.md"
|
|
133
|
+
|
|
134
|
+
# Specify a provider
|
|
135
|
+
henchman --provider openai -p "Write a Python function to calculate fibonacci"
|
|
136
|
+
|
|
137
|
+
# Use a specific model
|
|
138
|
+
henchman --model gpt-4-turbo -p "Analyze this code for security issues"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### File Operations
|
|
142
|
+
```bash
|
|
143
|
+
# Read and analyze a file
|
|
144
|
+
henchman -p "Review this code for bugs" < script.py
|
|
145
|
+
|
|
146
|
+
# Process multiple files
|
|
147
|
+
cat *.py | henchman -p "Find common patterns in these files"
|
|
148
|
+
|
|
149
|
+
# Generate documentation
|
|
150
|
+
henchman -p "Create API documentation for this module" < module.py > docs.md
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## ⚙️ Configuration
|
|
154
|
+
|
|
155
|
+
Henchman-AI uses hierarchical configuration (later settings override earlier ones):
|
|
156
|
+
|
|
157
|
+
1. **Default settings** (built-in sensible defaults)
|
|
158
|
+
2. **User settings**: `~/.henchman/settings.yaml`
|
|
159
|
+
3. **Workspace settings**: `.henchman/settings.yaml` (project-specific)
|
|
160
|
+
4. **Environment variables** (highest priority)
|
|
161
|
+
|
|
162
|
+
### Example `settings.yaml`
|
|
163
|
+
```yaml
|
|
164
|
+
# Provider configuration
|
|
165
|
+
providers:
|
|
166
|
+
default: deepseek # or openai, anthropic, ollama
|
|
167
|
+
deepseek:
|
|
168
|
+
model: deepseek-chat
|
|
169
|
+
base_url: "https://api.deepseek.com"
|
|
170
|
+
temperature: 0.7
|
|
171
|
+
openai:
|
|
172
|
+
model: gpt-4-turbo-preview
|
|
173
|
+
organization: "org-xxx"
|
|
174
|
+
|
|
175
|
+
# Tool settings
|
|
176
|
+
tools:
|
|
177
|
+
auto_accept_read: true
|
|
178
|
+
shell_timeout: 60
|
|
179
|
+
web_search_max_results: 5
|
|
180
|
+
|
|
181
|
+
# UI settings
|
|
182
|
+
ui:
|
|
183
|
+
theme: "monokai"
|
|
184
|
+
show_tokens: true
|
|
185
|
+
streaming: true
|
|
186
|
+
|
|
187
|
+
# System settings
|
|
188
|
+
system:
|
|
189
|
+
cache_enabled: true
|
|
190
|
+
cache_ttl: 3600
|
|
191
|
+
max_tokens: 4096
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Environment Variables
|
|
195
|
+
```bash
|
|
196
|
+
# Provider API keys
|
|
197
|
+
export DEEPSEEK_API_KEY="sk-xxx"
|
|
198
|
+
export OPENAI_API_KEY="sk-xxx"
|
|
199
|
+
export ANTHROPIC_API_KEY="sk-xxx"
|
|
200
|
+
|
|
201
|
+
# Configuration overrides
|
|
202
|
+
export HENCHMAN_DEFAULT_PROVIDER="openai"
|
|
203
|
+
export HENCHMAN_DEFAULT_MODEL="gpt-4"
|
|
204
|
+
export HENCHMAN_TEMPERATURE="0.5"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## 🔌 Supported Providers
|
|
208
|
+
|
|
209
|
+
| Provider | Models | Features |
|
|
210
|
+
|----------|--------|----------|
|
|
211
|
+
| **DeepSeek** | deepseek-chat, deepseek-coder | Free tier, Code completion |
|
|
212
|
+
| **OpenAI** | gpt-4, gpt-3.5-turbo, etc. | Function calling, JSON mode |
|
|
213
|
+
| **Anthropic** | claude-3-opus, claude-3-sonnet | Long context, Constitutional AI |
|
|
214
|
+
| **Ollama** | llama2, mistral, codellama | Local models, Custom models |
|
|
215
|
+
| **Custom** | Any OpenAI-compatible API | Self-hosted, Local inference |
|
|
216
|
+
|
|
217
|
+
## 🛠️ Development
|
|
218
|
+
|
|
219
|
+
### Setup Development Environment
|
|
220
|
+
```bash
|
|
221
|
+
# Clone and install
|
|
222
|
+
git clone https://github.com/MGPowerlytics/henchman-ai.git
|
|
223
|
+
cd henchman-ai
|
|
224
|
+
python -m venv venv
|
|
225
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
226
|
+
pip install -e ".[dev]"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Running Tests
|
|
230
|
+
```bash
|
|
231
|
+
# Run all tests
|
|
232
|
+
pytest
|
|
233
|
+
|
|
234
|
+
# Run with coverage
|
|
235
|
+
pytest --cov=henchman --cov-report=html
|
|
236
|
+
|
|
237
|
+
# Run specific test categories
|
|
238
|
+
pytest tests/unit/ -v
|
|
239
|
+
pytest tests/integration/ -v
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Code Quality
|
|
243
|
+
```bash
|
|
244
|
+
# Linting
|
|
245
|
+
ruff check src/ tests/
|
|
246
|
+
ruff format src/ tests/
|
|
247
|
+
|
|
248
|
+
# Type checking
|
|
249
|
+
mypy src/
|
|
250
|
+
|
|
251
|
+
# Security scanning
|
|
252
|
+
bandit -r src/
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Building and Publishing
|
|
256
|
+
```bash
|
|
257
|
+
# Build package
|
|
258
|
+
hatch build
|
|
259
|
+
|
|
260
|
+
# Test build
|
|
261
|
+
hatch run test
|
|
262
|
+
|
|
263
|
+
# Publish to PyPI (requires credentials)
|
|
264
|
+
hatch publish
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## 📚 Documentation
|
|
268
|
+
|
|
269
|
+
### Online Documentation
|
|
270
|
+
For detailed documentation, see the [docs directory](docs/) in this repository:
|
|
271
|
+
|
|
272
|
+
- [Getting Started](docs/getting-started.md)
|
|
273
|
+
- [Configuration Guide](docs/configuration.md)
|
|
274
|
+
- [API Reference](docs/api.md)
|
|
275
|
+
- [Tool Development](docs/tools.md)
|
|
276
|
+
- [Provider Integration](docs/providers.md)
|
|
277
|
+
- [MCP Integration](docs/mcp.md)
|
|
278
|
+
- [Extensions](docs/extensions.md)
|
|
279
|
+
|
|
280
|
+
### Building Documentation Locally
|
|
281
|
+
You can build and view the documentation locally:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# Install documentation dependencies
|
|
285
|
+
pip install mkdocs mkdocs-material mkdocstrings[python]
|
|
286
|
+
|
|
287
|
+
# Build static HTML documentation
|
|
288
|
+
python scripts/build_docs.py
|
|
289
|
+
|
|
290
|
+
# Or serve documentation locally (live preview)
|
|
291
|
+
mkdocs serve
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The documentation will be available at `http://localhost:8000` when served locally.
|
|
295
|
+
|
|
296
|
+
## 🤝 Contributing
|
|
297
|
+
|
|
298
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
299
|
+
|
|
300
|
+
## 🐛 Reporting Issues
|
|
301
|
+
|
|
302
|
+
Found a bug or have a feature request? Please [open an issue](https://github.com/MGPowerlytics/henchman-ai/issues) on GitHub.
|
|
303
|
+
|
|
304
|
+
## 📄 License
|
|
305
|
+
|
|
306
|
+
Henchman-AI is released under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
307
|
+
|
|
308
|
+
## 🙏 Acknowledgments
|
|
309
|
+
|
|
310
|
+
- Inspired by [gemini-cli](https://github.com/google/gemini-cli)
|
|
311
|
+
- Built with [Rich](https://github.com/Textualize/rich) for beautiful terminal output
|
|
312
|
+
- Uses [Pydantic](https://docs.pydantic.dev/) for data validation
|
|
313
|
+
- Powered by the Python async ecosystem
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
**Happy coding with your AI Henchman!** 🦸♂️🤖
|
|
@@ -4,10 +4,10 @@ henchman/version.py,sha256=g1Eesg-ffAqlRayLSNQqW5rBPWm91ti9pjNfGtTq5W4,161
|
|
|
4
4
|
henchman/cli/__init__.py,sha256=Gv86a_heuBLqUd-y46JZUyzUaDl5H-9RtcWGr3rMwBw,673
|
|
5
5
|
henchman/cli/app.py,sha256=ausKDDrRJ5KgiespK2P9vhX1yn-DxdJhYyBJ6tB5sb4,11507
|
|
6
6
|
henchman/cli/console.py,sha256=S4Jvq0UTmu9KtOkLNsIsvG_8X9eg1Guc6NAh8T_JeNI,8017
|
|
7
|
-
henchman/cli/input.py,sha256=
|
|
7
|
+
henchman/cli/input.py,sha256=1-dz9mg3IvCtKMJbXzstxNbJYpwU1pGr-a0wJ0gqUik,7129
|
|
8
8
|
henchman/cli/json_output.py,sha256=9kP9S5q0xBgP4HQGTT4P6DDT76F9VVTdEY_KiEpoZnI,2669
|
|
9
9
|
henchman/cli/prompts.py,sha256=m3Velzi2tXBIHinN9jIpU9kDMYL80ngYQsv2EYo7IZU,6647
|
|
10
|
-
henchman/cli/repl.py,sha256=
|
|
10
|
+
henchman/cli/repl.py,sha256=YLV1oVfGASYWLux4G5_I88RCaGk7y97um3B6uRxAacg,27844
|
|
11
11
|
henchman/cli/repl.py.backup,sha256=3iagruUgsvtcfpDv1mTAYg4I14X4CaNSEeMQjj91src,15638
|
|
12
12
|
henchman/cli/repl.py.backup2,sha256=-zgSUrnobd_sHq3jG-8NbwPTVlPc3FaqSkv32gAFdPo,11328
|
|
13
13
|
henchman/cli/commands/__init__.py,sha256=8s6NBCPlc4jKTCdvnKJCmdLwRCQ4QLCARjQbr7ICipw,3828
|
|
@@ -27,7 +27,7 @@ henchman/core/__init__.py,sha256=BtOHfsJB6eW-QhtK4sbqOzzO6GLqWeVLkewOA-wuqL0,521
|
|
|
27
27
|
henchman/core/agent.py,sha256=l9BJO8Zw4bMdUyTDjcZKG84WdZ1Kndm3Y09oUAZFYp0,13475
|
|
28
28
|
henchman/core/agent.py.backup,sha256=Tq0IhWAPMRQTxjETeH7WTosEmzuUVz7um0YbCnuNbLQ,7417
|
|
29
29
|
henchman/core/events.py,sha256=Uijv3NGNV8yJnQfY48u0pBBvEauAEczAbkGARJy8mfI,1423
|
|
30
|
-
henchman/core/session.py,sha256=
|
|
30
|
+
henchman/core/session.py,sha256=1KNcH2xkDrDhbnzdRd_IZjeii5annY8Deqz-om7NJbY,13093
|
|
31
31
|
henchman/core/turn.py,sha256=iRaTr_8hGSGQUt0vqmUE8w5D-drvkLeZlNEHmNUfX0M,8617
|
|
32
32
|
henchman/extensions/__init__.py,sha256=C7LrK50uiHwmLlOGkQyngbFvuUYdCcZEb6ucOklY_ws,310
|
|
33
33
|
henchman/extensions/base.py,sha256=cHUzWu4OGFju9Wr1xAiGHZOOW7eQbcC1-dqEd2Oe3QM,2290
|
|
@@ -61,7 +61,7 @@ henchman/skills/store.py,sha256=z_qHnVdyHAm-jtGNy5b33d9jQ-3pN-mkjrBAzc7CA5U,5331
|
|
|
61
61
|
henchman/tools/__init__.py,sha256=NLQuYRtPECju8TZHg-ZgVUrtdlHdMTu7P8C3D4Nhdqg,440
|
|
62
62
|
henchman/tools/base.py,sha256=PPeKS6jOCv3jFSgjd5j3kzgjTzw7VpBAY3ikmeDlIcI,4820
|
|
63
63
|
henchman/tools/registry.py,sha256=YrDz29mwMGPGdJB5RD0QlakHiJ-24p3d_jnIaMY05hI,10228
|
|
64
|
-
henchman/tools/builtins/__init__.py,sha256=
|
|
64
|
+
henchman/tools/builtins/__init__.py,sha256=g6znk4vJ9rNAUzu9G4pf407sjs2lphL4fYyes7se-KI,893
|
|
65
65
|
henchman/tools/builtins/ask_user.py,sha256=xPu74cB0rYahZHajVdjKgdmKU121SWyAgZSkU_wBYjQ,3007
|
|
66
66
|
henchman/tools/builtins/file_edit.py,sha256=VjfpYVZulpIBufRSIsTx9eD5gYGnSybksyo5vGCL4wo,3709
|
|
67
67
|
henchman/tools/builtins/file_read.py,sha256=RJCsK9Y-M2bd4IB8hnGaMjdzl62WSq7wOS9apcA3thA,4173
|
|
@@ -70,15 +70,16 @@ henchman/tools/builtins/glob_tool.py,sha256=7NAlan5A6v-RWAIUj8ID78aYRSvXe9Jtt2I6
|
|
|
70
70
|
henchman/tools/builtins/grep.py,sha256=PV8X2ydnAutrWCS5VR9lABFpfSv0Olzsqa1Ktb5X4z0,5321
|
|
71
71
|
henchman/tools/builtins/ls.py,sha256=5iSqHilrEiZ8ziOG4nKwC90fuLEx01V_0BzfS2PNAro,4167
|
|
72
72
|
henchman/tools/builtins/rag_search.py,sha256=yk0z0mIVRH-yl47uteNXTy76aXP8PLxBq512juAfIuU,4568
|
|
73
|
-
henchman/tools/builtins/shell.py,sha256=
|
|
73
|
+
henchman/tools/builtins/shell.py,sha256=MJEZzWcIm71ofq7739QBik6ihJBypHZ9TvWXfjFOwus,4267
|
|
74
74
|
henchman/tools/builtins/web_fetch.py,sha256=uwgZm0ye3yDuS2U2DPV4D-8bjviYDTKN-cNi7mCMRpw,3370
|
|
75
|
+
henchman/tools/builtins/web_search.py,sha256=dapmhN5Yf_WYJT5bnwkkhyDe1n2aDmT-mU9ZY1BC6Sw,4265
|
|
75
76
|
henchman/utils/__init__.py,sha256=ayu2XRNx3Fw0z8vbIne63A3gBjxu779QE8sUQsjNnm4,240
|
|
76
77
|
henchman/utils/compaction.py,sha256=ARS0jUDI2adsoCTfJjygRom31N16QtWbRzNXDKzX6cA,22871
|
|
77
78
|
henchman/utils/retry.py,sha256=sobZk9LLGxglSJw_jeNaBYCrvH14YNFrBVyp_OwLWcw,4993
|
|
78
79
|
henchman/utils/tokens.py,sha256=D9H4ciFNH7l1b05IGbw0U0tmy2yF5aItFZyDufGF53k,5665
|
|
79
80
|
henchman/utils/validation.py,sha256=moj4LQXVXt2J-3_pWVH_0-EabyRYApOU2Oh5JSTIua8,4146
|
|
80
|
-
henchman_ai-0.1.
|
|
81
|
-
henchman_ai-0.1.
|
|
82
|
-
henchman_ai-0.1.
|
|
83
|
-
henchman_ai-0.1.
|
|
84
|
-
henchman_ai-0.1.
|
|
81
|
+
henchman_ai-0.1.14.dist-info/METADATA,sha256=B3V44ITO-lpcEVr-cJTdAunvIgMii_OTJZkkXLRzaxk,9186
|
|
82
|
+
henchman_ai-0.1.14.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
83
|
+
henchman_ai-0.1.14.dist-info/entry_points.txt,sha256=dtPyd6BzK3A8lmrj1KXTFlHBplIWcWMdryjtR0jw5iU,51
|
|
84
|
+
henchman_ai-0.1.14.dist-info/licenses/LICENSE,sha256=TMoSCCG1I1vCMK-Bjtvxe80E8kIdSdrtuQXYHc_ahqg,1064
|
|
85
|
+
henchman_ai-0.1.14.dist-info/RECORD,,
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: henchman-ai
|
|
3
|
-
Version: 0.1.13
|
|
4
|
-
Summary: A model-agnostic AI agent CLI - your AI henchman for the terminal
|
|
5
|
-
Project-URL: Homepage, https://github.com/MGPowerlytics/henchman-ai
|
|
6
|
-
Project-URL: Repository, https://github.com/MGPowerlytics/henchman-ai
|
|
7
|
-
Project-URL: Documentation, https://github.com/MGPowerlytics/henchman-ai#readme
|
|
8
|
-
Author-email: Matthew <matthew@example.com>
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: agent,ai,anthropic,assistant,cli,deepseek,henchman,llm,openai,terminal
|
|
12
|
-
Classifier: Development Status :: 4 - Beta
|
|
13
|
-
Classifier: Environment :: Console
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
-
Classifier: Operating System :: OS Independent
|
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
-
Classifier: Topic :: Utilities
|
|
23
|
-
Classifier: Typing :: Typed
|
|
24
|
-
Requires-Python: >=3.10
|
|
25
|
-
Requires-Dist: aiohttp>=3.9
|
|
26
|
-
Requires-Dist: anthropic>=0.40
|
|
27
|
-
Requires-Dist: anyio>=4.0
|
|
28
|
-
Requires-Dist: chromadb>=0.4
|
|
29
|
-
Requires-Dist: click>=8.0
|
|
30
|
-
Requires-Dist: fastembed>=0.3
|
|
31
|
-
Requires-Dist: httpx>=0.27
|
|
32
|
-
Requires-Dist: mcp>=1.0
|
|
33
|
-
Requires-Dist: openai>=1.40
|
|
34
|
-
Requires-Dist: prompt-toolkit>=3.0
|
|
35
|
-
Requires-Dist: pydantic>=2.0
|
|
36
|
-
Requires-Dist: pyyaml>=6.0
|
|
37
|
-
Requires-Dist: rich>=13.0
|
|
38
|
-
Requires-Dist: tiktoken>=0.5
|
|
39
|
-
Provides-Extra: dev
|
|
40
|
-
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
41
|
-
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
42
|
-
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
43
|
-
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
44
|
-
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
45
|
-
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
46
|
-
Description-Content-Type: text/markdown
|
|
47
|
-
|
|
48
|
-
# Henchman-AI
|
|
49
|
-
|
|
50
|
-
> A Model-Agnostic AI Agent CLI in Python
|
|
51
|
-
|
|
52
|
-
Henchman-AI is a terminal-based AI agent that supports multiple LLM providers (DeepSeek, OpenAI, Anthropic, Ollama) through a unified interface. Inspired by gemini-cli, built for extensibility.
|
|
53
|
-
|
|
54
|
-
## Features
|
|
55
|
-
|
|
56
|
-
- 🔄 **Model-Agnostic**: Support any LLM provider through a unified abstraction
|
|
57
|
-
- 🐍 **Pythonic**: Leverages Python's async ecosystem and rich libraries
|
|
58
|
-
- 🔌 **Extensible**: Plugin system for tools, providers, and commands
|
|
59
|
-
- 🚀 **Production-Ready**: Proper error handling, testing, and packaging
|
|
60
|
-
|
|
61
|
-
## Installation
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
pip install henchman-ai
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Or install from source:
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
git clone https://github.com/matthew/henchman-ai.git
|
|
71
|
-
cd henchman-ai
|
|
72
|
-
pip install -e ".[dev]"
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Quick Start
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
# Set your API key
|
|
79
|
-
export DEEPSEEK_API_KEY="your-api-key"
|
|
80
|
-
|
|
81
|
-
# Start the CLI
|
|
82
|
-
henchman
|
|
83
|
-
|
|
84
|
-
# Or run with a prompt directly
|
|
85
|
-
henchman --prompt "Explain this code" < file.py
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Usage
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
# Show version
|
|
92
|
-
henchman --version
|
|
93
|
-
|
|
94
|
-
# Show help
|
|
95
|
-
henchman --help
|
|
96
|
-
|
|
97
|
-
# Interactive mode (default)
|
|
98
|
-
henchman
|
|
99
|
-
|
|
100
|
-
# Headless mode with prompt
|
|
101
|
-
henchman -p "Summarize README.md"
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Configuration
|
|
105
|
-
|
|
106
|
-
Henchman-AI uses hierarchical configuration:
|
|
107
|
-
|
|
108
|
-
1. Default settings
|
|
109
|
-
2. User settings: `~/.henchman/settings.yaml`
|
|
110
|
-
3. Workspace settings: `.henchman/settings.yaml`
|
|
111
|
-
4. Environment variables
|
|
112
|
-
|
|
113
|
-
Example `settings.yaml`:
|
|
114
|
-
|
|
115
|
-
```yaml
|
|
116
|
-
providers:
|
|
117
|
-
default: deepseek
|
|
118
|
-
deepseek:
|
|
119
|
-
model: deepseek-chat
|
|
120
|
-
|
|
121
|
-
tools:
|
|
122
|
-
auto_accept_read: true
|
|
123
|
-
shell_timeout: 60
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## Development
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
# Install dev dependencies
|
|
130
|
-
pip install -e ".[dev]"
|
|
131
|
-
|
|
132
|
-
# Run tests
|
|
133
|
-
pytest
|
|
134
|
-
|
|
135
|
-
# Linting
|
|
136
|
-
ruff check src/ tests/
|
|
137
|
-
|
|
138
|
-
# Type checking
|
|
139
|
-
mypy src/
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## License
|
|
143
|
-
|
|
144
|
-
MIT License - see [LICENSE](LICENSE) for details.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|