Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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