anywhere-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.
@@ -0,0 +1,199 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import hashlib
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from loguru import logger
10
+
11
+
12
+ CODEX_HISTORY_ITEM_TYPES = {
13
+ "message",
14
+ "reasoning",
15
+ "function_call",
16
+ "function_call_output",
17
+ "custom_tool_call",
18
+ "custom_tool_call_output",
19
+ }
20
+
21
+
22
+ @dataclass(slots=True)
23
+ class CodexHistoryItem:
24
+ turn_id: str | None
25
+ item: dict[str, Any]
26
+
27
+
28
+ def read_timeline_history(
29
+ thread_id: str,
30
+ *,
31
+ rollout_path: str | Path | None = None,
32
+ sessions_root: Path | None = None,
33
+ ) -> list[CodexHistoryItem]:
34
+ path = _usable_rollout_path(rollout_path)
35
+ if path is None:
36
+ path = find_rollout_path(thread_id, sessions_root=sessions_root)
37
+ if path is None:
38
+ return []
39
+
40
+ items: list[CodexHistoryItem] = []
41
+ current_turn_id: str | None = None
42
+ try:
43
+ with path.open("r", encoding="utf-8", errors="ignore") as file:
44
+ for line_no, line in enumerate(file, start=1):
45
+ try:
46
+ record = json.loads(line)
47
+ except json.JSONDecodeError:
48
+ logger.trace("codex history line is not json path={} line={}", path, line_no)
49
+ continue
50
+
51
+ record_type = record.get("type")
52
+ payload = record.get("payload")
53
+ if isinstance(payload, dict):
54
+ turn_id = _turn_id_from_record(record_type, payload)
55
+ if turn_id:
56
+ current_turn_id = turn_id
57
+
58
+ if record_type != "response_item" or not isinstance(payload, dict):
59
+ event_item = _event_item(record_type, payload)
60
+ if event_item is not None:
61
+ event_item["_historyLine"] = line_no
62
+ items.append(CodexHistoryItem(turn_id=current_turn_id, item=event_item))
63
+ continue
64
+ item = _response_item(payload)
65
+ if item is None:
66
+ continue
67
+ item = dict(item)
68
+ item["_historyLine"] = line_no
69
+ items.append(CodexHistoryItem(turn_id=current_turn_id, item=item))
70
+ except OSError as exc:
71
+ logger.warning("failed to read codex history path={} error={}", path, exc)
72
+ return []
73
+ return items
74
+
75
+
76
+ def read_tool_history(
77
+ thread_id: str,
78
+ *,
79
+ rollout_path: str | Path | None = None,
80
+ sessions_root: Path | None = None,
81
+ ) -> list[CodexHistoryItem]:
82
+ return [
83
+ entry
84
+ for entry in read_timeline_history(thread_id, rollout_path=rollout_path, sessions_root=sessions_root)
85
+ if entry.item.get("type") in {"function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output"}
86
+ ]
87
+
88
+
89
+ def _usable_rollout_path(value: str | Path | None) -> Path | None:
90
+ if value is None:
91
+ return None
92
+ path = Path(value).expanduser()
93
+ if path.is_file():
94
+ return path
95
+ logger.trace("codex rollout path is not readable path={}", path)
96
+ return None
97
+
98
+
99
+ def find_rollout_path(thread_id: str, *, sessions_root: Path | None = None) -> Path | None:
100
+ root = sessions_root or Path.home() / ".codex" / "sessions"
101
+ if not root.exists():
102
+ return None
103
+
104
+ matches = sorted(root.rglob(f"rollout-*{thread_id}.jsonl"), key=lambda path: path.stat().st_mtime, reverse=True)
105
+ if matches:
106
+ return matches[0]
107
+
108
+ # Older or hand-written fixtures may not include the thread id in the filename.
109
+ for path in sorted(root.rglob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True):
110
+ if _jsonl_has_session_id(path, thread_id):
111
+ return path
112
+ return None
113
+
114
+
115
+ def _jsonl_has_session_id(path: Path, thread_id: str) -> bool:
116
+ try:
117
+ with path.open("r", encoding="utf-8", errors="ignore") as file:
118
+ for line in file:
119
+ try:
120
+ record = json.loads(line)
121
+ except json.JSONDecodeError:
122
+ continue
123
+ if record.get("type") != "session_meta":
124
+ continue
125
+ payload = record.get("payload")
126
+ return isinstance(payload, dict) and payload.get("id") == thread_id
127
+ except OSError:
128
+ return False
129
+ return False
130
+
131
+
132
+ def _turn_id_from_record(record_type: Any, payload: dict[str, Any]) -> str | None:
133
+ if record_type == "turn_context" and isinstance(payload.get("turn_id"), str):
134
+ return payload["turn_id"]
135
+ if record_type == "event_msg" and payload.get("type") == "task_started" and isinstance(payload.get("turn_id"), str):
136
+ return payload["turn_id"]
137
+ return None
138
+
139
+
140
+ def _response_item(payload: dict[str, Any]) -> dict[str, Any] | None:
141
+ item_type = payload.get("type")
142
+ if item_type not in CODEX_HISTORY_ITEM_TYPES:
143
+ return None
144
+ if item_type == "message":
145
+ role = payload.get("role")
146
+ if role == "user":
147
+ return {**payload, "type": "userMessage"}
148
+ if role == "assistant":
149
+ return {**payload, "type": "agentMessage"}
150
+ return None
151
+ return dict(payload)
152
+
153
+
154
+ def _event_item(record_type: Any, payload: dict[str, Any]) -> dict[str, Any] | None:
155
+ if record_type != "event_msg":
156
+ return None
157
+ event_type = payload.get("type")
158
+ if event_type == "task_started":
159
+ return {
160
+ "type": "turnStart",
161
+ "id": f"turn-start-{payload.get('turn_id')}",
162
+ "status": "running",
163
+ "_derivedKey": "turn-start",
164
+ }
165
+ if event_type in {"task_complete", "turn_aborted"}:
166
+ result = "interrupted" if event_type == "turn_aborted" else "completed"
167
+ return {
168
+ "type": "turnEnd",
169
+ "id": f"turn-end-{payload.get('turn_id')}",
170
+ "status": result,
171
+ "result": result,
172
+ "error": {"message": payload.get("reason")} if event_type == "turn_aborted" else None,
173
+ "_derivedKey": "turn-end",
174
+ }
175
+ if event_type == "patch_apply_end":
176
+ changes = []
177
+ raw_changes = payload.get("changes")
178
+ if isinstance(raw_changes, dict):
179
+ changes = [
180
+ {"path": str(path), "action": str(change.get("type") or "change") if isinstance(change, dict) else "change"}
181
+ for path, change in raw_changes.items()
182
+ ]
183
+ return {
184
+ "type": "fileChange",
185
+ "id": _string_value(payload.get("call_id")) or f"patch-{_short_event_key(payload)}",
186
+ "changes": changes,
187
+ "status": "completed" if payload.get("success") is True else "failed",
188
+ "_derivedKey": f"patch-{payload.get('call_id') or _short_event_key(payload)}",
189
+ }
190
+ return None
191
+
192
+
193
+ def _short_event_key(payload: dict[str, Any]) -> str:
194
+ encoded = json.dumps(payload, sort_keys=True, default=str).encode("utf-8")
195
+ return hashlib.sha256(encoded).hexdigest()[:16]
196
+
197
+
198
+ def _string_value(value: Any) -> str | None:
199
+ return value if isinstance(value, str) else None