mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-04-28 11:30:03 +00:00
163 lines
6 KiB
Python
163 lines
6 KiB
Python
"""CLI event parser for Claude Code CLI output.
|
|
|
|
This parser emits an ordered stream of low-level events suitable for building a
|
|
Claude Code-like transcript in messaging UIs.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from loguru import logger
|
|
|
|
|
|
def parse_cli_event(event: Any) -> list[dict]:
|
|
"""
|
|
Parse a CLI event and return a structured result.
|
|
|
|
Args:
|
|
event: Raw event dictionary from CLI
|
|
|
|
Returns:
|
|
List of parsed event dicts. Empty list if not recognized.
|
|
"""
|
|
if not isinstance(event, dict):
|
|
return []
|
|
|
|
etype = event.get("type")
|
|
results: list[dict[str, Any]] = []
|
|
|
|
# Some CLI/proxy layers emit "system" events that are not user-visible and
|
|
# carry no transcript content. Ignore them explicitly to avoid noisy logs.
|
|
if etype == "system":
|
|
return []
|
|
|
|
# 1. Handle full messages (assistant/user or result)
|
|
msg_obj = None
|
|
if etype == "assistant" or etype == "user":
|
|
msg_obj = event.get("message")
|
|
elif etype == "result":
|
|
res = event.get("result")
|
|
if isinstance(res, dict):
|
|
msg_obj = res.get("message")
|
|
# Some variants put content directly on the result.
|
|
if not msg_obj and isinstance(res.get("content"), list):
|
|
msg_obj = {"content": res.get("content")}
|
|
if not msg_obj:
|
|
msg_obj = event.get("message")
|
|
# Some variants put content directly on the event.
|
|
if not msg_obj and isinstance(event.get("content"), list):
|
|
msg_obj = {"content": event.get("content")}
|
|
|
|
if msg_obj and isinstance(msg_obj, dict):
|
|
content = msg_obj.get("content", [])
|
|
if isinstance(content, list):
|
|
# Preserve order exactly as content blocks appear.
|
|
for c in content:
|
|
if not isinstance(c, dict):
|
|
continue
|
|
ctype = c.get("type")
|
|
if ctype == "text":
|
|
results.append({"type": "text_chunk", "text": c.get("text", "")})
|
|
elif ctype == "thinking":
|
|
results.append(
|
|
{"type": "thinking_chunk", "text": c.get("thinking", "")}
|
|
)
|
|
elif ctype == "tool_use":
|
|
results.append(
|
|
{
|
|
"type": "tool_use",
|
|
"id": str(c.get("id", "") or "").strip(),
|
|
"name": c.get("name", ""),
|
|
"input": c.get("input"),
|
|
}
|
|
)
|
|
elif ctype == "tool_result":
|
|
results.append(
|
|
{
|
|
"type": "tool_result",
|
|
"tool_use_id": str(c.get("tool_use_id", "") or "").strip(),
|
|
"content": c.get("content"),
|
|
"is_error": bool(c.get("is_error", False)),
|
|
}
|
|
)
|
|
|
|
if results:
|
|
return results
|
|
|
|
# 2. Handle streaming deltas
|
|
if etype == "content_block_delta":
|
|
delta = event.get("delta", {})
|
|
if isinstance(delta, dict):
|
|
if delta.get("type") == "text_delta":
|
|
return [
|
|
{
|
|
"type": "text_delta",
|
|
"index": event.get("index", -1),
|
|
"text": delta.get("text", ""),
|
|
}
|
|
]
|
|
if delta.get("type") == "thinking_delta":
|
|
return [
|
|
{
|
|
"type": "thinking_delta",
|
|
"index": event.get("index", -1),
|
|
"text": delta.get("thinking", ""),
|
|
}
|
|
]
|
|
if delta.get("type") == "input_json_delta":
|
|
return [
|
|
{
|
|
"type": "tool_use_delta",
|
|
"index": event.get("index", -1),
|
|
"partial_json": delta.get("partial_json", ""),
|
|
}
|
|
]
|
|
|
|
# 3. Handle tool usage start
|
|
if etype == "content_block_start":
|
|
block = event.get("content_block", {})
|
|
if isinstance(block, dict):
|
|
btype = block.get("type")
|
|
if btype == "thinking":
|
|
return [{"type": "thinking_start", "index": event.get("index", -1)}]
|
|
if btype == "text":
|
|
return [{"type": "text_start", "index": event.get("index", -1)}]
|
|
if btype == "tool_use":
|
|
return [
|
|
{
|
|
"type": "tool_use_start",
|
|
"index": event.get("index", -1),
|
|
"id": str(block.get("id", "") or "").strip(),
|
|
"name": block.get("name", ""),
|
|
"input": block.get("input"),
|
|
}
|
|
]
|
|
|
|
# 3.5 Handle block stop (to close open streaming segments)
|
|
if etype == "content_block_stop":
|
|
return [{"type": "block_stop", "index": event.get("index", -1)}]
|
|
|
|
# 4. Handle errors and exit
|
|
if etype == "error":
|
|
err = event.get("error")
|
|
msg = err.get("message") if isinstance(err, dict) else str(err)
|
|
logger.info(f"CLI_PARSER: Parsed error event: {msg}")
|
|
return [{"type": "error", "message": msg}]
|
|
elif etype == "exit":
|
|
code = event.get("code", 0)
|
|
stderr = event.get("stderr")
|
|
if code == 0:
|
|
logger.debug(f"CLI_PARSER: Successful exit (code={code})")
|
|
return [{"type": "complete", "status": "success"}]
|
|
else:
|
|
# Non-zero exit is an error
|
|
error_msg = stderr if stderr else f"Process exited with code {code}"
|
|
logger.warning(f"CLI_PARSER: Error exit (code={code}): {error_msg}")
|
|
return [
|
|
{"type": "error", "message": error_msg},
|
|
{"type": "complete", "status": "failed"},
|
|
]
|
|
|
|
# Log unrecognized events for debugging
|
|
if etype:
|
|
logger.debug(f"CLI_PARSER: Unrecognized event type: {etype}")
|
|
return []
|