166 lines
6.2 KiB
Python
166 lines
6.2 KiB
Python
"""Claude Code statusLine wrapper that captures context-window data.
|
|
|
|
Claude Code only exposes ``context_window`` (the model's real window
|
|
size and live usage) to the ``statusLine`` command via stdin. This
|
|
wrapper reads that stdin, writes the relevant fields atomically to
|
|
``bridge_dir/context.json`` for the forwarder to consume, then exec's
|
|
the user's pre-existing statusLine command (if any) with the same
|
|
stdin so its terminal output still renders.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
_CONTEXT_FILE = "context.json"
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
"""
|
|
Capture Claude Code's statusLine stdin and chain to the user's command.
|
|
|
|
:param argv: Optional argv override (excluding program name).
|
|
:returns: ``0`` on success, ``0`` after a soft failure (the
|
|
statusLine must never crash Claude Code's TUI loop).
|
|
"""
|
|
args = _parse_args(sys.argv[1:] if argv is None else argv)
|
|
raw = sys.stdin.read()
|
|
payload: dict[str, object] | None = None
|
|
try:
|
|
parsed = json.loads(raw) if raw.strip() else None
|
|
except json.JSONDecodeError:
|
|
parsed = None
|
|
if isinstance(parsed, dict):
|
|
payload = parsed
|
|
if payload is not None:
|
|
_write_context_atomic(Path(args.bridge_dir), payload)
|
|
if args.chain:
|
|
_chain(args.chain, raw)
|
|
return 0
|
|
|
|
|
|
def _parse_args(argv: list[str]) -> argparse.Namespace:
|
|
"""
|
|
Parse wrapper arguments.
|
|
|
|
:param argv: CLI argv excluding program name.
|
|
:returns: Parsed namespace with ``bridge_dir`` and optional ``chain``.
|
|
"""
|
|
parser = argparse.ArgumentParser(prog="python -m omnigent.claude_native_status")
|
|
parser.add_argument("--bridge-dir", required=True)
|
|
# The user's original statusLine command (as a shell string). When
|
|
# set we exec it with the same stdin so claude-hud / their custom
|
|
# status bar still renders for omnigent-launched sessions.
|
|
parser.add_argument("--chain", default=None)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def _write_context_atomic(bridge_dir: Path, payload: dict[str, object]) -> None:
|
|
"""
|
|
Persist the statusLine payload's context fields to ``context.json``.
|
|
|
|
Atomic write so the forwarder never observes a half-written file.
|
|
Soft-fails (writes nothing) when ``context_window`` is missing or
|
|
malformed — there's nothing useful to record.
|
|
|
|
:param bridge_dir: Bridge directory shared with the forwarder.
|
|
:param payload: Decoded statusLine stdin JSON.
|
|
"""
|
|
context = payload.get("context_window")
|
|
if not isinstance(context, dict):
|
|
return
|
|
size = context.get("context_window_size")
|
|
usage = context.get("current_usage")
|
|
if not isinstance(size, int) or size <= 0:
|
|
return
|
|
record: dict[str, object] = {"context_window_size": size}
|
|
if isinstance(usage, dict):
|
|
record["current_usage"] = usage
|
|
used_pct = context.get("used_percentage")
|
|
if isinstance(used_pct, (int, float)):
|
|
record["used_percentage"] = used_pct
|
|
# Claude Code's statusLine stdin carries a top-level ``cost`` block with
|
|
# its own cumulative session billing. Capture ``total_cost_usd`` so the
|
|
# forwarder can report it (claude-native produces no ``response.completed``
|
|
# event, so the Omnigent relay's cost accumulation never runs for it).
|
|
cost = payload.get("cost")
|
|
if isinstance(cost, dict):
|
|
total_cost = cost.get("total_cost_usd")
|
|
if (
|
|
isinstance(total_cost, (int, float))
|
|
and not isinstance(total_cost, bool)
|
|
and total_cost >= 0
|
|
):
|
|
record["total_cost_usd"] = float(total_cost)
|
|
# Claude Code's statusLine stdin carries the active model as a ``model``
|
|
# block (``{"id": "claude-opus-4-8", "display_name": "Opus"}``), rewritten
|
|
# on every render — including right after an in-pane ``/model`` switch.
|
|
# Capture the concrete id so the forwarder can mirror the switch to
|
|
# ``model_override`` on the next poll, before the user's next message,
|
|
# rather than waiting for the next turn's transcript to reveal the model
|
|
# (which lagged model-gated policies by one turn). Defensive about the
|
|
# shape: accept a ``{id|display_name}`` dict or a bare string.
|
|
model = payload.get("model")
|
|
model_id: str | None = None
|
|
if isinstance(model, dict):
|
|
raw_model = model.get("id") or model.get("display_name")
|
|
if isinstance(raw_model, str) and raw_model.strip():
|
|
model_id = raw_model.strip()
|
|
elif isinstance(model, str) and model.strip():
|
|
model_id = model.strip()
|
|
if model_id is not None:
|
|
record["model"] = model_id
|
|
try:
|
|
bridge_dir.mkdir(parents=True, exist_ok=True)
|
|
fd, tmp_path = tempfile.mkstemp(prefix=".context-", dir=str(bridge_dir))
|
|
try:
|
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
json.dump(record, handle, separators=(",", ":"))
|
|
os.replace(tmp_path, str(bridge_dir / _CONTEXT_FILE))
|
|
except OSError:
|
|
with contextlib.suppress(OSError):
|
|
os.unlink(tmp_path)
|
|
raise
|
|
except OSError as exc:
|
|
print(f"omnigent claude status: write failed: {exc}", file=sys.stderr)
|
|
|
|
|
|
def _chain(command: str, stdin_payload: str) -> None:
|
|
"""
|
|
Exec the user's pre-existing statusLine command, piping our stdin.
|
|
|
|
:param command: Shell command string from the user's
|
|
``~/.claude/settings.json`` ``statusLine.command``.
|
|
:param stdin_payload: The original stdin we received from Claude
|
|
Code. Forwarded verbatim so the chained command sees exactly
|
|
what Claude Code sent.
|
|
"""
|
|
try:
|
|
proc = subprocess.run(
|
|
command,
|
|
input=stdin_payload,
|
|
shell=True,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
timeout=5.0,
|
|
)
|
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
print(f"omnigent claude status: chain failed: {exc}", file=sys.stderr)
|
|
return
|
|
if proc.stdout:
|
|
sys.stdout.write(proc.stdout)
|
|
if proc.stderr:
|
|
sys.stderr.write(proc.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|