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.
Files changed (48) hide show
  1. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/PKG-INFO +1 -1
  2. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/codex/adapter.py +12 -6
  3. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/codex/rpc.py +39 -37
  4. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/pyproject.toml +1 -1
  5. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_codex_adapter.py +82 -0
  6. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/uv.lock +1 -1
  7. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/.gitignore +0 -0
  8. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/README.md +0 -0
  9. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/__init__.py +0 -0
  10. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/adapter.py +0 -0
  11. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/attachments.py +0 -0
  12. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/capabilities.py +0 -0
  13. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/__init__.py +0 -0
  14. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/history_adapter.py +0 -0
  15. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/normalized.py +0 -0
  16. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/normalizers.py +0 -0
  17. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/path_utils.py +0 -0
  18. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/preferences.py +0 -0
  19. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/sdk_adapter.py +0 -0
  20. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/timeline_identity.py +0 -0
  21. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/timeline_reducer.py +0 -0
  22. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/claude/trust.py +0 -0
  23. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/cli.py +0 -0
  24. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/codex/__init__.py +0 -0
  25. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/codex/history.py +0 -0
  26. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/codex/reducer.py +0 -0
  27. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/launch.py +0 -0
  28. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/__init__.py +0 -0
  29. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/common.py +0 -0
  30. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/file_ops.py +0 -0
  31. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/ops.py +0 -0
  32. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/shell.py +0 -0
  33. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local/terminal.py +0 -0
  34. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/local_ops.py +0 -0
  35. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/protocol.py +0 -0
  36. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/runtime.py +0 -0
  37. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/sync_state.py +0 -0
  38. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/connector/time.py +0 -0
  39. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/run.sh +0 -0
  40. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_claude_history_adapter.py +0 -0
  41. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_claude_preferences.py +0 -0
  42. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_claude_sdk_adapter.py +0 -0
  43. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_claude_timeline_parity.py +0 -0
  44. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_claude_trust.py +0 -0
  45. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_connector_capabilities.py +0 -0
  46. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_connector_cli.py +0 -0
  47. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_connector_runtime.py +0 -0
  48. {anywhere_cli-0.1.2 → anywhere_cli-0.1.3}/tests/test_terminal_backend.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anywhere-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Local runtime connector for Agents Anywhere
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: claude-agent-sdk
@@ -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
- assert self.rpc is not None
121
- await self.rpc.start(self.handle_notification)
122
- if self._started:
123
- return
124
- await self._best_effort_bootstrap_reads()
125
- self._started = True
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
- if self.process and self._initialized:
36
- self._notification_handler = handler
37
- return
36
+ async with self._start_lock:
37
+ if self.process and self._initialized:
38
+ self._notification_handler = handler
39
+ return
38
40
 
39
- self._notification_handler = handler
40
- if self.process is None:
41
- logger.info("starting codex app-server command={}", self.command)
42
- self.process = await asyncio.create_subprocess_exec(
43
- *self.command,
44
- stdin=asyncio.subprocess.PIPE,
45
- stdout=asyncio.subprocess.PIPE,
46
- stderr=asyncio.subprocess.PIPE,
47
- limit=APP_SERVER_STREAM_LIMIT,
48
- )
49
- self._track_reader(asyncio.create_task(self._read_stdout()), "stdout")
50
- self._track_reader(asyncio.create_task(self._read_stderr()), "stderr")
51
-
52
- await self.request(
53
- "initialize",
54
- {
55
- "clientInfo": {
56
- "name": "agent-server-connector",
57
- "title": "Agent Server Connector",
58
- "version": "0.1.0",
59
- },
60
- "capabilities": {
61
- "experimentalApi": True,
62
- "requestAttestation": False,
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
- await self.notify("initialized")
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 self.process and self.process.stdout
126
- while line := await self.process.stdout.readline():
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 self.process and self.process.stderr
147
- while line := await self.process.stderr.readline():
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:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anywhere-cli"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Local runtime connector for Agents Anywhere"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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]
@@ -26,7 +26,7 @@ wheels = [
26
26
 
27
27
  [[package]]
28
28
  name = "anywhere-cli"
29
- version = "0.1.2"
29
+ version = "0.1.3"
30
30
  source = { editable = "." }
31
31
  dependencies = [
32
32
  { name = "claude-agent-sdk" },
File without changes
File without changes
File without changes