Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Sensor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ADR Sensor is a Python library that collects telemetry from AI coding agents to
| **Cline (Claude Dev)** | `cline` | JSON task files | macOS, Linux, Windows |
| **Claude Desktop** | `claude_desktop` | JSONL audit logs | macOS, Windows |
| **OpenAI Codex CLI** | `codex` | JSONL (`~/.codex/sessions/`) | macOS, Linux, Windows |
| **GitHub Copilot** | `copilot` | JSONL (`~/.copilot/session-state/`) | macOS, Linux, Windows |
| **Warp Terminal** | `warp` | SQLite (`warp.sqlite`) | macOS, Windows |
| **opencode** | `opencode` | SQLite (`opencode.db`) or JSON tree | macOS, Linux |

Expand Down Expand Up @@ -110,6 +111,7 @@ adr-sensor
adr-sensor --source claude
adr-sensor --source cursor
adr-sensor --source codex
adr-sensor --source copilot
adr-sensor --source claude_desktop
adr-sensor --source opencode

Expand Down Expand Up @@ -349,6 +351,7 @@ adr-sensor/
│ │ ├── cline_parser.py
│ │ ├── claude_desktop_parser.py
│ │ ├── codex_parser.py
│ │ ├── copilot_parser.py
│ │ ├── opencode_parser.py
│ │ └── warp_parser.py
│ ├── schemas/
Expand Down
537 changes: 529 additions & 8 deletions Sensor/adr_sensor/observer.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Sensor/adr_sensor/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .claude_desktop_parser import ClaudeDesktopParser
from .claude_parser import ClaudeParser
from .cline_parser import ClineParser
from .copilot_parser import CopilotParser
from .codex_parser import CodexParser
from .cursor_parser import CursorParser
from .opencode_parser import OpencodeParser
Expand All @@ -18,6 +19,7 @@
"ClaudeDesktopParser",
"ClaudeParser",
"ClineParser",
"CopilotParser",
"CodexParser",
"CursorParser",
"OpencodeParser",
Expand Down
32 changes: 31 additions & 1 deletion Sensor/adr_sensor/parsers/codex_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def parse_jsonl_file(self, file_path: Path) -> Optional[AgentEvent]:
session_data: Dict[str, Any] = {
"id": None,
"timestamp": None,
"first_event_timestamp": None,
"last_event_timestamp": None,
"event_count": 0,
"cwd": None,
"model": None,
"messages": [],
Expand All @@ -61,6 +64,7 @@ def parse_jsonl_file(self, file_path: Path) -> Optional[AgentEvent]:
continue
try:
event = json.loads(line)
session_data["event_count"] += 1
self._process_event(event, session_data)
except json.JSONDecodeError:
continue
Expand Down Expand Up @@ -94,13 +98,26 @@ def parse_jsonl_file(self, file_path: Path) -> Optional[AgentEvent]:
)

return AgentEvent(
timestamp=session_data["timestamp"] or datetime.now(timezone.utc),
# Keep the filename identity stable; resumed content is detected by the exporter.
timestamp=(
session_data["timestamp"]
or session_data["first_event_timestamp"]
or datetime.now(timezone.utc)
),
source="codex",
session_id=f"codex_{session_data['id']}",
project_path=session_data["cwd"],
model=session_data["model"],
chat_history=chat_history,
raw_log_path=str(file_path),
session_context=(
{
"last_event_at": session_data["last_event_timestamp"].isoformat(),
"event_count": session_data["event_count"],
}
if session_data["last_event_timestamp"]
else {"event_count": session_data["event_count"]}
),
)

except Exception as e:
Expand Down Expand Up @@ -174,6 +191,19 @@ def _process_event(self, event: Dict[str, Any], session_data: Dict[str, Any]):
evt_type = event.get("type")
payload = event.get("payload", {})

event_timestamp = event.get("timestamp")
if event_timestamp:
try:
normalized = normalize_timestamp(event_timestamp)
current = session_data.get("first_event_timestamp")
if current is None or normalized < current:
session_data["first_event_timestamp"] = normalized
current = session_data.get("last_event_timestamp")
if current is None or normalized > current:
session_data["last_event_timestamp"] = normalized
except Exception:
pass

if evt_type == "session_meta":
session_data["id"] = payload.get("id")
if payload.get("timestamp"):
Expand Down
Loading