baserun-cli 0.1.0__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.
- baserun_cli/__init__.py +0 -0
- baserun_cli/_vendored/__init__.py +28 -0
- baserun_cli/_vendored/base.py +218 -0
- baserun_cli/_vendored/cli.py +332 -0
- baserun_cli/_vendored/parsers/__init__.py +35 -0
- baserun_cli/_vendored/parsers/base.py +47 -0
- baserun_cli/_vendored/parsers/bash_agent.py +83 -0
- baserun_cli/_vendored/parsers/claude.py +97 -0
- baserun_cli/_vendored/parsers/codex.py +202 -0
- baserun_cli/channel.py +350 -0
- baserun_cli/main.py +98 -0
- baserun_cli/runner.py +319 -0
- baserun_cli-0.1.0.dist-info/METADATA +47 -0
- baserun_cli-0.1.0.dist-info/RECORD +17 -0
- baserun_cli-0.1.0.dist-info/WHEEL +5 -0
- baserun_cli-0.1.0.dist-info/entry_points.txt +2 -0
- baserun_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Bash Agent stream-json parser.
|
|
2
|
+
|
|
3
|
+
Format: `ccagent --output stream-json` (bash-agent C implementation).
|
|
4
|
+
All text/thinking is token-level delta only; no complete message blocks
|
|
5
|
+
and no result event with final text. The delta accumulator in cli.py
|
|
6
|
+
handles flushing complete events.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ..base import ConnectorEvent, ConnectorEventType
|
|
11
|
+
from .base import BaseParser
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BashAgentParser(BaseParser):
|
|
15
|
+
schema = "bash_agent"
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def extract_session_id(obj: dict) -> str | None:
|
|
19
|
+
if obj.get("type") == "session_start":
|
|
20
|
+
return obj.get("session_id")
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def parse(obj: dict) -> list[ConnectorEvent]:
|
|
25
|
+
out: list[ConnectorEvent] = []
|
|
26
|
+
t = obj.get("type", "")
|
|
27
|
+
|
|
28
|
+
if t in ("session_start", "user_input"):
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
if t == "thinking":
|
|
32
|
+
out.append(ConnectorEvent(
|
|
33
|
+
ConnectorEventType.THINKING,
|
|
34
|
+
{"delta": obj.get("content", ""), "is_delta": True},
|
|
35
|
+
))
|
|
36
|
+
elif t == "text":
|
|
37
|
+
out.append(ConnectorEvent(
|
|
38
|
+
ConnectorEventType.MESSAGE,
|
|
39
|
+
{"delta": obj.get("content", ""), "is_delta": True},
|
|
40
|
+
))
|
|
41
|
+
elif t == "tool_call":
|
|
42
|
+
out.append(ConnectorEvent(
|
|
43
|
+
ConnectorEventType.TOOL_CALL,
|
|
44
|
+
{"tool": obj.get("name", ""), "args": obj.get("input", {}), "call_id": obj.get("id", "")},
|
|
45
|
+
))
|
|
46
|
+
elif t == "tool_result":
|
|
47
|
+
out.append(ConnectorEvent(
|
|
48
|
+
ConnectorEventType.TOOL_RESULT,
|
|
49
|
+
{"call_id": obj.get("tool_use_id", ""), "result": obj.get("content", "")},
|
|
50
|
+
))
|
|
51
|
+
elif t == "usage":
|
|
52
|
+
_in = obj.get("input_tokens", 0)
|
|
53
|
+
_out = obj.get("output_tokens", 0)
|
|
54
|
+
_cache_read = obj.get("cache_read_input_tokens", 0) or 0
|
|
55
|
+
_cache_creation = obj.get("cache_creation_input_tokens", 0) or 0
|
|
56
|
+
raw = {k: v for k, v in obj.items() if k != "type"}
|
|
57
|
+
out.append(ConnectorEvent(
|
|
58
|
+
ConnectorEventType.USAGE,
|
|
59
|
+
{
|
|
60
|
+
"input": _in,
|
|
61
|
+
"output": _out,
|
|
62
|
+
"cache_read_input_tokens": _cache_read,
|
|
63
|
+
"cache_creation_input_tokens": _cache_creation,
|
|
64
|
+
"total": _in + _out + _cache_read + _cache_creation,
|
|
65
|
+
"raw": raw,
|
|
66
|
+
},
|
|
67
|
+
))
|
|
68
|
+
elif t == "error":
|
|
69
|
+
out.append(ConnectorEvent(
|
|
70
|
+
ConnectorEventType.ERROR,
|
|
71
|
+
{"message": obj.get("message", str(obj))},
|
|
72
|
+
))
|
|
73
|
+
elif t == "stop":
|
|
74
|
+
if obj.get("reason") == "end_turn":
|
|
75
|
+
out.append(ConnectorEvent(
|
|
76
|
+
ConnectorEventType.FINAL,
|
|
77
|
+
{"text": "", "session_id": None},
|
|
78
|
+
))
|
|
79
|
+
# tool_use → non-terminal, skip
|
|
80
|
+
else:
|
|
81
|
+
out.append(BaseParser.catch_all(f"bash_agent:unhandled:{t}", obj))
|
|
82
|
+
|
|
83
|
+
return out
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Claude Code stream-json parser.
|
|
2
|
+
|
|
3
|
+
Format: `claude -p --output-format stream-json --verbose --include-partial-messages`
|
|
4
|
+
--verbose outputs stream_event (token-level delta) + complete assistant messages.
|
|
5
|
+
text/thinking arrive as stream_event deltas (per-token); tool_use arrives in
|
|
6
|
+
assistant blocks (with full args).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ..base import ConnectorEvent, ConnectorEventType
|
|
11
|
+
from .base import BaseParser
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ClaudeParser(BaseParser):
|
|
15
|
+
schema = "claude"
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def extract_session_id(obj: dict) -> str | None:
|
|
19
|
+
if obj.get("type") == "system" and obj.get("subtype") == "init":
|
|
20
|
+
return obj.get("session_id")
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def parse(obj: dict) -> list[ConnectorEvent]:
|
|
25
|
+
out: list[ConnectorEvent] = []
|
|
26
|
+
t = obj.get("type")
|
|
27
|
+
|
|
28
|
+
if t == "stream_event":
|
|
29
|
+
event = obj.get("event", {})
|
|
30
|
+
et = event.get("type")
|
|
31
|
+
if et == "content_block_delta":
|
|
32
|
+
delta = event.get("delta", {})
|
|
33
|
+
dt = delta.get("type")
|
|
34
|
+
if dt == "text_delta":
|
|
35
|
+
out.append(ConnectorEvent(
|
|
36
|
+
ConnectorEventType.MESSAGE,
|
|
37
|
+
{"delta": delta.get("text", ""), "is_delta": True},
|
|
38
|
+
))
|
|
39
|
+
elif dt == "thinking_delta":
|
|
40
|
+
out.append(ConnectorEvent(
|
|
41
|
+
ConnectorEventType.THINKING,
|
|
42
|
+
{"delta": delta.get("thinking", ""), "is_delta": True},
|
|
43
|
+
))
|
|
44
|
+
# input_json_delta (tool args delta) — partial JSON, skip
|
|
45
|
+
elif t == "assistant":
|
|
46
|
+
for block in obj.get("message", {}).get("content", []):
|
|
47
|
+
bt = block.get("type")
|
|
48
|
+
if bt == "thinking":
|
|
49
|
+
out.append(ConnectorEvent(
|
|
50
|
+
ConnectorEventType.THINKING,
|
|
51
|
+
{"delta": block.get("thinking", ""), "is_delta": False},
|
|
52
|
+
))
|
|
53
|
+
elif bt == "text":
|
|
54
|
+
out.append(ConnectorEvent(
|
|
55
|
+
ConnectorEventType.MESSAGE,
|
|
56
|
+
{"delta": block.get("text", ""), "is_delta": False},
|
|
57
|
+
))
|
|
58
|
+
elif bt == "tool_use":
|
|
59
|
+
out.append(ConnectorEvent(
|
|
60
|
+
ConnectorEventType.TOOL_CALL,
|
|
61
|
+
{"tool": block.get("name"), "args": block.get("input"), "call_id": block.get("id")},
|
|
62
|
+
))
|
|
63
|
+
elif t == "user":
|
|
64
|
+
for block in obj.get("message", {}).get("content", []):
|
|
65
|
+
if block.get("type") == "tool_result":
|
|
66
|
+
out.append(ConnectorEvent(
|
|
67
|
+
ConnectorEventType.TOOL_RESULT,
|
|
68
|
+
{"call_id": block.get("tool_use_id"), "result": block.get("content")},
|
|
69
|
+
))
|
|
70
|
+
elif t == "result":
|
|
71
|
+
usage = obj.get("usage", {}) or {}
|
|
72
|
+
if usage:
|
|
73
|
+
_in = usage.get("input_tokens", 0)
|
|
74
|
+
_out = usage.get("output_tokens", 0)
|
|
75
|
+
_cache_read = usage.get("cache_read_input_tokens", 0) or 0
|
|
76
|
+
_cache_creation = usage.get("cache_creation_input_tokens", 0) or 0
|
|
77
|
+
out.append(ConnectorEvent(
|
|
78
|
+
ConnectorEventType.USAGE,
|
|
79
|
+
{
|
|
80
|
+
"input": _in,
|
|
81
|
+
"output": _out,
|
|
82
|
+
"cache_read_input_tokens": _cache_read,
|
|
83
|
+
"cache_creation_input_tokens": _cache_creation,
|
|
84
|
+
"total": _in + _out + _cache_read + _cache_creation,
|
|
85
|
+
"raw": dict(usage),
|
|
86
|
+
},
|
|
87
|
+
))
|
|
88
|
+
out.append(ConnectorEvent(
|
|
89
|
+
ConnectorEventType.FINAL,
|
|
90
|
+
{"text": obj.get("result", ""), "session_id": None},
|
|
91
|
+
))
|
|
92
|
+
elif t == "system":
|
|
93
|
+
pass # system/init handled by extract_session_id
|
|
94
|
+
else:
|
|
95
|
+
out.append(BaseParser.catch_all(f"claude:unhandled:{t}", obj))
|
|
96
|
+
|
|
97
|
+
return out
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Codex CLI JSONL parser.
|
|
2
|
+
|
|
3
|
+
Format: `codex exec --json --full-auto`
|
|
4
|
+
Validated against real CLI output + binary analysis of codex-cli 0.125.0.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from ..base import ConnectorEvent, ConnectorEventType
|
|
9
|
+
from .base import BaseParser
|
|
10
|
+
|
|
11
|
+
# Item types that are pure lifecycle noise — skipped entirely.
|
|
12
|
+
_SKIP_ITEMS = frozenset({
|
|
13
|
+
"context_compaction",
|
|
14
|
+
"hook_prompt",
|
|
15
|
+
"user_message",
|
|
16
|
+
"entered_review_mode",
|
|
17
|
+
"exited_review_mode",
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _extract_plan_text(item: dict) -> str:
|
|
22
|
+
fragments = item.get("fragments") or []
|
|
23
|
+
parts = []
|
|
24
|
+
for frag in fragments:
|
|
25
|
+
if isinstance(frag, dict):
|
|
26
|
+
md = frag.get("plan_markdown") or frag.get("markdown") or ""
|
|
27
|
+
if md:
|
|
28
|
+
parts.append(md)
|
|
29
|
+
if not parts:
|
|
30
|
+
md = item.get("plan_markdown", "")
|
|
31
|
+
if md:
|
|
32
|
+
return md
|
|
33
|
+
import json as _json
|
|
34
|
+
return f"[plan] {_json.dumps(item, ensure_ascii=False)[:500]}"
|
|
35
|
+
return "\n".join(parts)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _extract_reasoning_text(item: dict) -> str:
|
|
39
|
+
text = item.get("summary_text") or item.get("raw_content") or ""
|
|
40
|
+
if text:
|
|
41
|
+
return text
|
|
42
|
+
for key in ("summary", "text", "content"):
|
|
43
|
+
val = item.get(key)
|
|
44
|
+
if isinstance(val, str) and val:
|
|
45
|
+
return val
|
|
46
|
+
import json as _json
|
|
47
|
+
return f"[reasoning] {_json.dumps(item, ensure_ascii=False)[:500]}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CodexParser(BaseParser):
|
|
51
|
+
schema = "codex"
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def extract_session_id(obj: dict) -> str | None:
|
|
55
|
+
if obj.get("type") == "thread.started":
|
|
56
|
+
return obj.get("thread_id")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def parse(obj: dict) -> list[ConnectorEvent]:
|
|
61
|
+
out: list[ConnectorEvent] = []
|
|
62
|
+
t = obj.get("type", "")
|
|
63
|
+
|
|
64
|
+
if t in ("thread.started", "turn.started"):
|
|
65
|
+
return out
|
|
66
|
+
|
|
67
|
+
if t == "item.started":
|
|
68
|
+
return CodexParser._item_started(obj.get("item", {}))
|
|
69
|
+
|
|
70
|
+
if t == "item.completed":
|
|
71
|
+
return CodexParser._item_completed(obj.get("item", {}))
|
|
72
|
+
|
|
73
|
+
if t == "turn.completed":
|
|
74
|
+
usage = obj.get("usage", {}) or {}
|
|
75
|
+
if usage:
|
|
76
|
+
_in = usage.get("input_tokens", 0)
|
|
77
|
+
_cached = usage.get("cached_input_tokens", 0)
|
|
78
|
+
_out = usage.get("output_tokens", 0)
|
|
79
|
+
fresh_in = max(0, _in - _cached)
|
|
80
|
+
_total = _in + _out
|
|
81
|
+
out.append(ConnectorEvent(
|
|
82
|
+
ConnectorEventType.USAGE,
|
|
83
|
+
{
|
|
84
|
+
"input": fresh_in,
|
|
85
|
+
"output": _out,
|
|
86
|
+
"cached_input_tokens": _cached,
|
|
87
|
+
"total": _total,
|
|
88
|
+
"raw": dict(usage),
|
|
89
|
+
},
|
|
90
|
+
))
|
|
91
|
+
out.append(ConnectorEvent(
|
|
92
|
+
ConnectorEventType.FINAL,
|
|
93
|
+
{"text": "", "session_id": None},
|
|
94
|
+
))
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
if t in ("error", "turn.failed"):
|
|
98
|
+
if t == "error":
|
|
99
|
+
msg = obj.get("message", "")
|
|
100
|
+
else:
|
|
101
|
+
err = obj.get("error", {})
|
|
102
|
+
msg = err.get("message", "") if isinstance(err, dict) else str(err)
|
|
103
|
+
out.append(ConnectorEvent(
|
|
104
|
+
ConnectorEventType.ERROR,
|
|
105
|
+
{"message": msg},
|
|
106
|
+
))
|
|
107
|
+
if t == "turn.failed":
|
|
108
|
+
out.append(ConnectorEvent(
|
|
109
|
+
ConnectorEventType.FINAL,
|
|
110
|
+
{"text": "", "session_id": None},
|
|
111
|
+
))
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
out.append(BaseParser.catch_all(f"codex:unhandled:{t}", obj))
|
|
115
|
+
return out
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _item_started(item: dict) -> list[ConnectorEvent]:
|
|
119
|
+
item_type = item.get("type")
|
|
120
|
+
item_id = item.get("id")
|
|
121
|
+
|
|
122
|
+
if item_type in _SKIP_ITEMS:
|
|
123
|
+
return []
|
|
124
|
+
|
|
125
|
+
tool_map = {
|
|
126
|
+
"command_execution": ("shell", {"command": item.get("command", "")}),
|
|
127
|
+
"file_change": ("file_change", {"changes": item.get("changes", [])}),
|
|
128
|
+
"web_search": ("web_search", {"queries": item.get("queries", [])}),
|
|
129
|
+
"image_generation": ("image_generation", item.get("input", {})),
|
|
130
|
+
"image_view": ("image_view", {"path": item.get("path", "")}),
|
|
131
|
+
"collab_agent_tool_call": ("collab_agent", {"prompt": item.get("prompt", "")}),
|
|
132
|
+
}
|
|
133
|
+
if item_type in tool_map:
|
|
134
|
+
name, args = tool_map[item_type]
|
|
135
|
+
return [ConnectorEvent(
|
|
136
|
+
ConnectorEventType.TOOL_CALL,
|
|
137
|
+
{"tool": name, "args": args, "call_id": item_id},
|
|
138
|
+
)]
|
|
139
|
+
if item_type in ("mcp_tool_call", "dynamic_tool_call"):
|
|
140
|
+
prefix = "mcp" if item_type == "mcp_tool_call" else "dynamic"
|
|
141
|
+
return [ConnectorEvent(
|
|
142
|
+
ConnectorEventType.TOOL_CALL,
|
|
143
|
+
{"tool": f"{prefix}:{item.get('name', 'unknown')}", "args": item.get("arguments"), "call_id": item_id},
|
|
144
|
+
)]
|
|
145
|
+
if item_type == "plan":
|
|
146
|
+
return [ConnectorEvent(
|
|
147
|
+
ConnectorEventType.THINKING,
|
|
148
|
+
{"delta": _extract_plan_text(item), "is_delta": False},
|
|
149
|
+
)]
|
|
150
|
+
if item_type == "reasoning":
|
|
151
|
+
return [ConnectorEvent(
|
|
152
|
+
ConnectorEventType.THINKING,
|
|
153
|
+
{"delta": _extract_reasoning_text(item), "is_delta": False},
|
|
154
|
+
)]
|
|
155
|
+
|
|
156
|
+
return [BaseParser.catch_all(f"codex:item.started:{item_type}", item)]
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _item_completed(item: dict) -> list[ConnectorEvent]:
|
|
160
|
+
item_type = item.get("type")
|
|
161
|
+
item_id = item.get("id")
|
|
162
|
+
|
|
163
|
+
if item_type in _SKIP_ITEMS:
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
if item_type == "agent_message":
|
|
167
|
+
return [ConnectorEvent(
|
|
168
|
+
ConnectorEventType.MESSAGE,
|
|
169
|
+
{"delta": item.get("text", ""), "is_delta": False},
|
|
170
|
+
)]
|
|
171
|
+
|
|
172
|
+
# all tool-like completions → TOOL_RESULT
|
|
173
|
+
tool_result_types = {
|
|
174
|
+
"command_execution", "file_change", "web_search",
|
|
175
|
+
"mcp_tool_call", "dynamic_tool_call",
|
|
176
|
+
"collab_agent_tool_call", "image_generation", "image_view",
|
|
177
|
+
}
|
|
178
|
+
if item_type in tool_result_types:
|
|
179
|
+
result_val = (
|
|
180
|
+
item.get("aggregated_output")
|
|
181
|
+
or item.get("output")
|
|
182
|
+
or item.get("result")
|
|
183
|
+
or item.get("changes")
|
|
184
|
+
or item
|
|
185
|
+
)
|
|
186
|
+
return [ConnectorEvent(
|
|
187
|
+
ConnectorEventType.TOOL_RESULT,
|
|
188
|
+
{"call_id": item_id, "result": result_val},
|
|
189
|
+
)]
|
|
190
|
+
|
|
191
|
+
if item_type == "plan":
|
|
192
|
+
return [ConnectorEvent(
|
|
193
|
+
ConnectorEventType.THINKING,
|
|
194
|
+
{"delta": _extract_plan_text(item), "is_delta": False},
|
|
195
|
+
)]
|
|
196
|
+
if item_type == "reasoning":
|
|
197
|
+
return [ConnectorEvent(
|
|
198
|
+
ConnectorEventType.THINKING,
|
|
199
|
+
{"delta": _extract_reasoning_text(item), "is_delta": False},
|
|
200
|
+
)]
|
|
201
|
+
|
|
202
|
+
return [BaseParser.catch_all(f"codex:item.completed:{item_type}", item)]
|