agent-shell-py 0.1.0__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.
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-shell-py
3
+ Version: 0.1.0
4
+ Summary: A lightweight abstraction for executing CLI coding agents headlessly
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+
8
+ # Agent Shell
9
+ Agent Shell is a light weight abstraction for executing a cli coding agent headlessly
10
+ and returning the output that can be used programatically as a unified contract
11
+
12
+ ## Examples
13
+
14
+ ### Execute
15
+
16
+ ```python
17
+ from agent_shell.shell import AgentShell
18
+ from agent_shell.models.agent import AgentType
19
+
20
+ shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
21
+
22
+ response = await shell.execute(
23
+ cwd="/path/to/project",
24
+ prompt="Can you tell me about this project?",
25
+ allowed_tools=["Read", "Glob", "Grep"],
26
+ model="sonnet",
27
+ )
28
+
29
+ print(response.response)
30
+ print(f"Cost: ${response.cost:.4f}")
31
+ ```
32
+
33
+ ### Stream
34
+
35
+ ```python
36
+ from agent_shell.shell import AgentShell
37
+ from agent_shell.models.agent import AgentType
38
+
39
+ shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
40
+
41
+ async for event in shell.stream(
42
+ cwd="/path/to/project",
43
+ prompt="Refactor the auth module",
44
+ allowed_tools=["Read", "Edit", "Bash"],
45
+ model="sonnet",
46
+ effort="high",
47
+ include_thinking=True,
48
+ ):
49
+ print(f"[{event.type}] {event.content}")
50
+ ```
51
+
52
+ ## Supported CLI Agents:
53
+
54
+ - [x] Claude Code
55
+ - [ ] OpenCode
56
+ - [ ] Gemini CLI
57
+ - [ ] Copilot CLI
58
+ - [ ] Codex
59
+
60
+
61
+
62
+
@@ -0,0 +1,55 @@
1
+ # Agent Shell
2
+ Agent Shell is a light weight abstraction for executing a cli coding agent headlessly
3
+ and returning the output that can be used programatically as a unified contract
4
+
5
+ ## Examples
6
+
7
+ ### Execute
8
+
9
+ ```python
10
+ from agent_shell.shell import AgentShell
11
+ from agent_shell.models.agent import AgentType
12
+
13
+ shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
14
+
15
+ response = await shell.execute(
16
+ cwd="/path/to/project",
17
+ prompt="Can you tell me about this project?",
18
+ allowed_tools=["Read", "Glob", "Grep"],
19
+ model="sonnet",
20
+ )
21
+
22
+ print(response.response)
23
+ print(f"Cost: ${response.cost:.4f}")
24
+ ```
25
+
26
+ ### Stream
27
+
28
+ ```python
29
+ from agent_shell.shell import AgentShell
30
+ from agent_shell.models.agent import AgentType
31
+
32
+ shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
33
+
34
+ async for event in shell.stream(
35
+ cwd="/path/to/project",
36
+ prompt="Refactor the auth module",
37
+ allowed_tools=["Read", "Edit", "Bash"],
38
+ model="sonnet",
39
+ effort="high",
40
+ include_thinking=True,
41
+ ):
42
+ print(f"[{event.type}] {event.content}")
43
+ ```
44
+
45
+ ## Supported CLI Agents:
46
+
47
+ - [x] Claude Code
48
+ - [ ] OpenCode
49
+ - [ ] Gemini CLI
50
+ - [ ] Copilot CLI
51
+ - [ ] Codex
52
+
53
+
54
+
55
+
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "agent-shell-py"
3
+ version = "0.1.0"
4
+ description = "A lightweight abstraction for executing CLI coding agents headlessly"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = []
8
+ [tool.setuptools.packages.find]
9
+ where = ["src"]
10
+
11
+ [tool.pytest.ini_options]
12
+ asyncio_mode = "auto"
13
+ markers = [
14
+ "e2e: end-to-end tests that call real CLI agents (requires credentials, costs money)",
15
+ ]
16
+
17
+ [dependency-groups]
18
+ dev = [
19
+ "pytest>=9.0.2",
20
+ "pytest-asyncio>=1.3.0",
21
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,28 @@
1
+ from typing import Protocol, AsyncIterator
2
+ from agent_shell.models.agent import AgentResponse, StreamEvent
3
+
4
+ class AgentAdapter(Protocol):
5
+ async def execute(
6
+ self,
7
+ cwd: str,
8
+ prompt: str,
9
+ allowed_tools: list[str] | None = None,
10
+ model: str | None = None,
11
+ effort: str | None = None,
12
+ include_thinking: bool = False,
13
+ ) -> AgentResponse:
14
+ ...
15
+
16
+ def stream(
17
+ self,
18
+ cwd: str,
19
+ prompt: str,
20
+ allowed_tools: list[str] | None = None,
21
+ model: str | None = None,
22
+ effort: str | None = None,
23
+ include_thinking: bool = False,
24
+ ) -> AsyncIterator[StreamEvent]:
25
+ ...
26
+
27
+ async def cancel(self) -> None:
28
+ ...
@@ -0,0 +1,140 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ from typing import AsyncIterator
5
+
6
+ from agent_shell.models.agent import AgentResponse, StreamEvent
7
+
8
+ class ClaudeCodeAdapter():
9
+ def __init__(self):
10
+ self._active_processes = []
11
+
12
+ async def execute(
13
+ self,
14
+ cwd: str,
15
+ prompt: str,
16
+ allowed_tools: list[str] | None = None,
17
+ model: str | None = None,
18
+ effort: str | None = None,
19
+ include_thinking: bool = False,
20
+ ) -> AgentResponse:
21
+ chunks: list[StreamEvent] = []
22
+ async for event in self.stream(
23
+ cwd=cwd,
24
+ prompt=prompt,
25
+ allowed_tools=allowed_tools,
26
+ model=model,
27
+ effort=effort,
28
+ include_thinking=include_thinking,
29
+ ):
30
+ chunks.append(event)
31
+
32
+ text = "\n".join(e.content for e in chunks if e.type == "text")
33
+ cost = next((e.cost for e in reversed(chunks) if e.type == "result"), 0.0)
34
+ return AgentResponse(response=text, cost=cost)
35
+
36
+ async def stream(
37
+ self,
38
+ cwd: str,
39
+ prompt: str,
40
+ allowed_tools: list[str] | None = None,
41
+ model: str | None = None,
42
+ effort: str | None = None,
43
+ include_thinking: bool = False,
44
+ ) -> AsyncIterator[StreamEvent]:
45
+ cmd = [
46
+ "claude", "-p", prompt,
47
+ "--output-format", "stream-json",
48
+ "--verbose"
49
+ ]
50
+
51
+ if allowed_tools:
52
+ cmd.extend(["--allowed-tools", ",".join(allowed_tools)])
53
+
54
+ if model:
55
+ cmd.extend(["--model", model])
56
+
57
+ if effort:
58
+ cmd.extend(["--effort", effort])
59
+
60
+ process = await asyncio.create_subprocess_exec(
61
+ *cmd,
62
+ stdin=asyncio.subprocess.DEVNULL,
63
+ stdout=asyncio.subprocess.PIPE,
64
+ stderr=asyncio.subprocess.PIPE,
65
+ cwd=os.path.abspath(cwd),
66
+ preexec_fn=os.setsid,
67
+ )
68
+
69
+ self._active_processes.append(process)
70
+
71
+ buffer = ""
72
+ while True:
73
+ chunk = await process.stdout.read(65536)
74
+ if not chunk:
75
+ if buffer.strip():
76
+ try:
77
+ for event in self._parse_event(
78
+ event=json.loads(buffer),
79
+ include_thinking=include_thinking
80
+ ):
81
+ yield event
82
+ except json.JSONDecodeError:
83
+ pass
84
+ break
85
+
86
+ buffer += chunk.decode("utf-8")
87
+ while "\n" in buffer:
88
+ line, buffer = buffer.split("\n", 1)
89
+ if line.strip():
90
+ try:
91
+ for event in self._parse_event(
92
+ event=json.loads(line),
93
+ include_thinking=include_thinking):
94
+ yield event
95
+ except json.JSONDecodeError:
96
+ pass
97
+
98
+ await process.wait()
99
+ if process in self._active_processes:
100
+ self._active_processes.remove(process)
101
+
102
+ stderr = await process.stderr.read()
103
+ if stderr and process.returncode != 0:
104
+ yield StreamEvent(type="error", content=stderr.decode("utf-8")[-500:])
105
+
106
+ def _parse_event(self, event: dict, include_thinking: bool) -> list[StreamEvent]:
107
+ t = event.get("type", "")
108
+ events = []
109
+
110
+ if t == "assistant":
111
+ for item in event.get("message", {}).get("content", []):
112
+ if item.get("type") == "text":
113
+ events.append(StreamEvent(type="text", content=item["text"]))
114
+ elif item.get("type") == "tool_use":
115
+ events.append(StreamEvent(type="tool_use", content=item.get("name", "")))
116
+ elif item.get("type") == "thinking" and include_thinking:
117
+ events.append(StreamEvent(type="thinking", content=item.get("thinking", "")))
118
+
119
+ elif t == "result":
120
+ cost = event.get("total_cost_usd", 0) or 0
121
+ duration = (event.get("duration_ms", 0) or 0) / 1000
122
+ is_error = event.get("is_error", False)
123
+ status = "error" if is_error else "ok"
124
+ events.append(StreamEvent(type="result", content=status, cost=cost, duration=duration))
125
+
126
+ return events
127
+
128
+ async def cancel(self) -> None:
129
+ for process in self._active_processes:
130
+ try:
131
+ os.killpg(os.getpgid(process.pid), 9)
132
+ except ProcessLookupError:
133
+ pass
134
+ self._active_processes.clear()
135
+
136
+
137
+
138
+
139
+
140
+
@@ -0,0 +1,22 @@
1
+ from enum import StrEnum
2
+ from dataclasses import dataclass
3
+
4
+ class AgentType(StrEnum):
5
+ CLAUDE_CODE = "claude_code"
6
+ OPENCODE = "opencode"
7
+ GEMINI_CLI = "gemini_cli"
8
+ COPILOT_CLI = "copilot_cli"
9
+ CODEX = "codex"
10
+
11
+ @dataclass
12
+ class AgentResponse:
13
+ response: str
14
+ cost: float
15
+
16
+ @dataclass
17
+ class StreamEvent:
18
+ type: str
19
+ content: str
20
+ cost: float = 0.0
21
+ duration: float = 0.0
22
+
@@ -0,0 +1,77 @@
1
+ from pathlib import Path
2
+ from typing import AsyncIterator
3
+
4
+ from agent_shell.models.agent import AgentType, AgentResponse, StreamEvent
5
+ from agent_shell.adapters.agent_adapter_protocol import AgentAdapter
6
+ from agent_shell.adapters.claude_code_adapter import ClaudeCodeAdapter
7
+
8
+
9
+ class AgentShell():
10
+ def __init__(self, agent_type: AgentType):
11
+ self._adapter = self._resolve_adapter(agent_type=agent_type)
12
+
13
+ def _resolve_adapter(self, agent_type: AgentType) -> AgentAdapter:
14
+ adapters = {
15
+ AgentType.CLAUDE_CODE: ClaudeCodeAdapter
16
+ }
17
+
18
+ adapter_cls = adapters.get(agent_type)
19
+
20
+ if not adapter_cls:
21
+ raise ValueError(f"Unsupported agent: {agent_type}")
22
+
23
+ return adapter_cls()
24
+
25
+ async def execute(
26
+ self,
27
+ cwd: str,
28
+ prompt: str,
29
+ allowed_tools: list[str] | None = None,
30
+ model: str | None = None,
31
+ effort: str | None = None,
32
+ include_thinking: bool = False,
33
+ ) -> AgentResponse:
34
+
35
+ if not Path(cwd).is_dir():
36
+ raise ValueError(f"Directory does not exist: {cwd}")
37
+
38
+ try:
39
+ return await self._adapter.execute(
40
+ cwd=cwd,
41
+ prompt=prompt,
42
+ allowed_tools=allowed_tools,
43
+ model=model,
44
+ effort=effort,
45
+ include_thinking=include_thinking,
46
+ )
47
+ except KeyboardInterrupt:
48
+ await self._adapter.cancel()
49
+ raise
50
+
51
+ async def stream(
52
+ self,
53
+ cwd: str,
54
+ prompt: str,
55
+ allowed_tools: list[str] | None = None,
56
+ model: str | None = None,
57
+ effort: str | None = None,
58
+ include_thinking: bool = False,
59
+ ) -> AsyncIterator[StreamEvent]:
60
+
61
+ if not Path(cwd).is_dir():
62
+ raise ValueError(f"Directory does not exist: {cwd}")
63
+
64
+ try:
65
+ async for chunk in self._adapter.stream(
66
+ cwd=cwd,
67
+ prompt=prompt,
68
+ allowed_tools=allowed_tools,
69
+ model=model,
70
+ effort=effort,
71
+ include_thinking=include_thinking,
72
+ ):
73
+ yield chunk
74
+ except KeyboardInterrupt:
75
+ await self._adapter.cancel()
76
+ raise
77
+
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-shell-py
3
+ Version: 0.1.0
4
+ Summary: A lightweight abstraction for executing CLI coding agents headlessly
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+
8
+ # Agent Shell
9
+ Agent Shell is a light weight abstraction for executing a cli coding agent headlessly
10
+ and returning the output that can be used programatically as a unified contract
11
+
12
+ ## Examples
13
+
14
+ ### Execute
15
+
16
+ ```python
17
+ from agent_shell.shell import AgentShell
18
+ from agent_shell.models.agent import AgentType
19
+
20
+ shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
21
+
22
+ response = await shell.execute(
23
+ cwd="/path/to/project",
24
+ prompt="Can you tell me about this project?",
25
+ allowed_tools=["Read", "Glob", "Grep"],
26
+ model="sonnet",
27
+ )
28
+
29
+ print(response.response)
30
+ print(f"Cost: ${response.cost:.4f}")
31
+ ```
32
+
33
+ ### Stream
34
+
35
+ ```python
36
+ from agent_shell.shell import AgentShell
37
+ from agent_shell.models.agent import AgentType
38
+
39
+ shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
40
+
41
+ async for event in shell.stream(
42
+ cwd="/path/to/project",
43
+ prompt="Refactor the auth module",
44
+ allowed_tools=["Read", "Edit", "Bash"],
45
+ model="sonnet",
46
+ effort="high",
47
+ include_thinking=True,
48
+ ):
49
+ print(f"[{event.type}] {event.content}")
50
+ ```
51
+
52
+ ## Supported CLI Agents:
53
+
54
+ - [x] Claude Code
55
+ - [ ] OpenCode
56
+ - [ ] Gemini CLI
57
+ - [ ] Copilot CLI
58
+ - [ ] Codex
59
+
60
+
61
+
62
+
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/agent_shell/__init__.py
4
+ src/agent_shell/shell.py
5
+ src/agent_shell/adapters/__init__.py
6
+ src/agent_shell/adapters/agent_adapter_protocol.py
7
+ src/agent_shell/adapters/claude_code_adapter.py
8
+ src/agent_shell/models/__init__.py
9
+ src/agent_shell/models/agent.py
10
+ src/agent_shell_py.egg-info/PKG-INFO
11
+ src/agent_shell_py.egg-info/SOURCES.txt
12
+ src/agent_shell_py.egg-info/dependency_links.txt
13
+ src/agent_shell_py.egg-info/top_level.txt