anywhere-cli 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/PKG-INFO +1 -1
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/codex/adapter.py +12 -6
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/codex/rpc.py +39 -37
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/pyproject.toml +1 -1
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_codex_adapter.py +82 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/uv.lock +1 -1
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/.gitignore +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/README.md +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/__init__.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/adapter.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/attachments.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/capabilities.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/__init__.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/history_adapter.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/normalized.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/normalizers.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/path_utils.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/preferences.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/sdk_adapter.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/timeline_identity.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/timeline_reducer.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/trust.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/cli.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/codex/__init__.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/codex/history.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/codex/reducer.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/launch.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/__init__.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/common.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/file_ops.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/ops.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/shell.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/terminal.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local_ops.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/protocol.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/runtime.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/sync_state.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/time.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/run.sh +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_claude_history_adapter.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_claude_preferences.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_claude_sdk_adapter.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_claude_timeline_parity.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_claude_trust.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_connector_capabilities.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_connector_cli.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_connector_runtime.py +0 -0
- {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_terminal_backend.py +0 -0
|
@@ -92,6 +92,7 @@ class CodexAdapter:
|
|
|
92
92
|
_history_sync_tasks: dict[str, asyncio.Task[None]] = field(default_factory=dict)
|
|
93
93
|
_existing_thread_sync_markers: dict[str, str] = field(default_factory=dict)
|
|
94
94
|
_existing_thread_names: dict[str, str | None] = field(default_factory=dict)
|
|
95
|
+
_start_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False, repr=False)
|
|
95
96
|
|
|
96
97
|
def __post_init__(self) -> None:
|
|
97
98
|
if self.rpc is None:
|
|
@@ -117,12 +118,13 @@ class CodexAdapter:
|
|
|
117
118
|
self.sync_state_store.delete_runtime("codex", connector_id)
|
|
118
119
|
|
|
119
120
|
async def start(self) -> None:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
async with self._start_lock:
|
|
122
|
+
assert self.rpc is not None
|
|
123
|
+
await self.rpc.start(self.handle_notification)
|
|
124
|
+
if self._started:
|
|
125
|
+
return
|
|
126
|
+
await self._best_effort_bootstrap_reads()
|
|
127
|
+
self._started = True
|
|
126
128
|
|
|
127
129
|
async def create_session(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
128
130
|
await self.start()
|
|
@@ -929,6 +931,10 @@ def _unresumable_thread_failure_reason(error_text: str) -> str | None:
|
|
|
929
931
|
return "archived"
|
|
930
932
|
if "cannot resume" in normalized or "not resumable" in normalized or "unresumable" in normalized:
|
|
931
933
|
return "unresumable"
|
|
934
|
+
if "failed to load configuration" in normalized and "model provider" in normalized:
|
|
935
|
+
return "missing_model_provider"
|
|
936
|
+
if "model provider" in normalized and "not found" in normalized:
|
|
937
|
+
return "missing_model_provider"
|
|
932
938
|
return None
|
|
933
939
|
|
|
934
940
|
|
|
@@ -25,6 +25,7 @@ class JsonRpcStdioClient:
|
|
|
25
25
|
def __init__(self, command: list[str] | None = None) -> None:
|
|
26
26
|
self.command = command or _resolve_codex_command()
|
|
27
27
|
self.process: asyncio.subprocess.Process | None = None
|
|
28
|
+
self._start_lock = asyncio.Lock()
|
|
28
29
|
self._next_id = 1
|
|
29
30
|
self._pending: dict[int | str, asyncio.Future[dict[str, Any]]] = {}
|
|
30
31
|
self._server_request_ids: set[int | str] = set()
|
|
@@ -32,39 +33,40 @@ class JsonRpcStdioClient:
|
|
|
32
33
|
self._initialized = False
|
|
33
34
|
|
|
34
35
|
async def start(self, handler: NotificationHandler) -> None:
|
|
35
|
-
|
|
36
|
-
self.
|
|
37
|
-
|
|
36
|
+
async with self._start_lock:
|
|
37
|
+
if self.process and self._initialized:
|
|
38
|
+
self._notification_handler = handler
|
|
39
|
+
return
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
41
|
+
self._notification_handler = handler
|
|
42
|
+
if self.process is None:
|
|
43
|
+
logger.info("starting codex app-server command={}", self.command)
|
|
44
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
45
|
+
*self.command,
|
|
46
|
+
stdin=asyncio.subprocess.PIPE,
|
|
47
|
+
stdout=asyncio.subprocess.PIPE,
|
|
48
|
+
stderr=asyncio.subprocess.PIPE,
|
|
49
|
+
limit=APP_SERVER_STREAM_LIMIT,
|
|
50
|
+
)
|
|
51
|
+
self._track_reader(asyncio.create_task(self._read_stdout(self.process)), "stdout")
|
|
52
|
+
self._track_reader(asyncio.create_task(self._read_stderr(self.process)), "stderr")
|
|
53
|
+
|
|
54
|
+
await self.request(
|
|
55
|
+
"initialize",
|
|
56
|
+
{
|
|
57
|
+
"clientInfo": {
|
|
58
|
+
"name": "agent-server-connector",
|
|
59
|
+
"title": "Agent Server Connector",
|
|
60
|
+
"version": "0.1.0",
|
|
61
|
+
},
|
|
62
|
+
"capabilities": {
|
|
63
|
+
"experimentalApi": True,
|
|
64
|
+
"requestAttestation": False,
|
|
65
|
+
},
|
|
63
66
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
self._initialized = True
|
|
67
|
+
)
|
|
68
|
+
await self.notify("initialized")
|
|
69
|
+
self._initialized = True
|
|
68
70
|
|
|
69
71
|
async def request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
70
72
|
if not self.process or not self.process.stdin:
|
|
@@ -121,9 +123,9 @@ class JsonRpcStdioClient:
|
|
|
121
123
|
self.process = None
|
|
122
124
|
self._initialized = False
|
|
123
125
|
|
|
124
|
-
async def _read_stdout(self) -> None:
|
|
125
|
-
assert
|
|
126
|
-
while line := await
|
|
126
|
+
async def _read_stdout(self, process: asyncio.subprocess.Process) -> None:
|
|
127
|
+
assert process.stdout
|
|
128
|
+
while line := await process.stdout.readline():
|
|
127
129
|
try:
|
|
128
130
|
payload = json.loads(line)
|
|
129
131
|
except json.JSONDecodeError:
|
|
@@ -142,9 +144,9 @@ class JsonRpcStdioClient:
|
|
|
142
144
|
if self._notification_handler is not None:
|
|
143
145
|
await self._notification_handler(payload)
|
|
144
146
|
|
|
145
|
-
async def _read_stderr(self) -> None:
|
|
146
|
-
assert
|
|
147
|
-
while line := await
|
|
147
|
+
async def _read_stderr(self, process: asyncio.subprocess.Process) -> None:
|
|
148
|
+
assert process.stderr
|
|
149
|
+
while line := await process.stderr.readline():
|
|
148
150
|
logger.trace("codex app-server stderr: {}", line.decode(errors="replace").rstrip())
|
|
149
151
|
|
|
150
152
|
def _track_reader(self, task: asyncio.Task[None], name: str) -> None:
|
|
@@ -130,6 +130,21 @@ class MissingRolloutResumeRpc(FakeCodexRpc):
|
|
|
130
130
|
return {}
|
|
131
131
|
|
|
132
132
|
|
|
133
|
+
class MissingModelProviderResumeRpc(FakeCodexRpc):
|
|
134
|
+
async def request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
135
|
+
self.requests.append((method, params))
|
|
136
|
+
if method == "thread/resume":
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
json.dumps(
|
|
139
|
+
{
|
|
140
|
+
"code": -32600,
|
|
141
|
+
"message": "failed to load configuration: Model provider `codex` not found",
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
return await super().request(method, params)
|
|
146
|
+
|
|
147
|
+
|
|
133
148
|
def test_stdio_client_stream_limit_is_large_enough_for_codex_jsonl() -> None:
|
|
134
149
|
assert APP_SERVER_STREAM_LIMIT >= 64 * 1024 * 1024
|
|
135
150
|
|
|
@@ -179,6 +194,54 @@ def test_stdio_client_includes_empty_params_for_no_arg_messages() -> None:
|
|
|
179
194
|
asyncio.run(_exercise_stdio_client_includes_empty_params())
|
|
180
195
|
|
|
181
196
|
|
|
197
|
+
class EmptyAsyncReader:
|
|
198
|
+
async def readline(self) -> bytes:
|
|
199
|
+
return b""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class StartableFakeProcess:
|
|
203
|
+
def __init__(self) -> None:
|
|
204
|
+
self.stdin = FakeStdin()
|
|
205
|
+
self.stdout = EmptyAsyncReader()
|
|
206
|
+
self.stderr = EmptyAsyncReader()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def _exercise_stdio_client_start_is_serialized(monkeypatch) -> None:
|
|
210
|
+
created: list[StartableFakeProcess] = []
|
|
211
|
+
initialize_seen = asyncio.Event()
|
|
212
|
+
|
|
213
|
+
async def fake_create_subprocess_exec(*_args: str, **_kwargs: Any) -> StartableFakeProcess:
|
|
214
|
+
await asyncio.sleep(0)
|
|
215
|
+
process = StartableFakeProcess()
|
|
216
|
+
created.append(process)
|
|
217
|
+
return process
|
|
218
|
+
|
|
219
|
+
async def complete_initialize_when_written() -> None:
|
|
220
|
+
client = task_client
|
|
221
|
+
while 1 not in client._pending: # noqa: SLF001
|
|
222
|
+
await asyncio.sleep(0)
|
|
223
|
+
client._pending[1].set_result({}) # noqa: SLF001
|
|
224
|
+
initialize_seen.set()
|
|
225
|
+
|
|
226
|
+
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
|
|
227
|
+
task_client = JsonRpcStdioClient(command=["codex"])
|
|
228
|
+
|
|
229
|
+
async def handler(_payload: dict[str, Any]) -> None:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
helper = asyncio.create_task(complete_initialize_when_written())
|
|
233
|
+
await asyncio.gather(task_client.start(handler), task_client.start(handler))
|
|
234
|
+
await helper
|
|
235
|
+
|
|
236
|
+
assert initialize_seen.is_set()
|
|
237
|
+
assert len(created) == 1
|
|
238
|
+
assert task_client._initialized is True # noqa: SLF001
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_stdio_client_start_is_serialized(monkeypatch) -> None:
|
|
242
|
+
asyncio.run(_exercise_stdio_client_start_is_serialized(monkeypatch))
|
|
243
|
+
|
|
244
|
+
|
|
182
245
|
def test_stdio_client_ignores_response_for_cancelled_request() -> None:
|
|
183
246
|
client = JsonRpcStdioClient(command=["codex"])
|
|
184
247
|
loop = asyncio.new_event_loop()
|
|
@@ -992,6 +1055,10 @@ def test_adapter_treats_archived_resume_failure_as_skipped() -> None:
|
|
|
992
1055
|
asyncio.run(_exercise_archived_resume_failure_sync())
|
|
993
1056
|
|
|
994
1057
|
|
|
1058
|
+
def test_adapter_treats_missing_model_provider_as_skipped_once() -> None:
|
|
1059
|
+
asyncio.run(_exercise_missing_model_provider_thread_sync())
|
|
1060
|
+
|
|
1061
|
+
|
|
995
1062
|
async def _exercise_existing_thread_sync() -> None:
|
|
996
1063
|
rpc = FakeCodexRpc()
|
|
997
1064
|
adapter = CodexAdapter(rpc=rpc) # type: ignore[arg-type]
|
|
@@ -1071,6 +1138,21 @@ async def _exercise_archived_resume_failure_sync() -> None:
|
|
|
1071
1138
|
assert result["backendNotifications"] == []
|
|
1072
1139
|
|
|
1073
1140
|
|
|
1141
|
+
async def _exercise_missing_model_provider_thread_sync() -> None:
|
|
1142
|
+
rpc = MissingModelProviderResumeRpc()
|
|
1143
|
+
adapter = CodexAdapter(rpc=rpc) # type: ignore[arg-type]
|
|
1144
|
+
|
|
1145
|
+
result = await adapter.sync_existing_sessions("conn_1")
|
|
1146
|
+
replay = await adapter.sync_existing_sessions("conn_1")
|
|
1147
|
+
|
|
1148
|
+
assert result["threads"] == []
|
|
1149
|
+
assert result["skippedThreads"] == ["thr_existing"]
|
|
1150
|
+
assert result["backendNotifications"] == []
|
|
1151
|
+
assert replay["threads"] == []
|
|
1152
|
+
assert replay["skippedThreads"] == ["thr_existing"]
|
|
1153
|
+
assert rpc.requests.count(("thread/resume", {"threadId": "thr_existing"})) == 1
|
|
1154
|
+
|
|
1155
|
+
|
|
1074
1156
|
async def _exercise_existing_thread_sync_timeouts(monkeypatch) -> None:
|
|
1075
1157
|
rpc = FakeCodexRpc()
|
|
1076
1158
|
adapter = CodexAdapter(rpc=rpc) # type: ignore[arg-type]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|