anywhere-cli 0.1.0__tar.gz → 0.1.2__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.0 → anywhere_cli-0.1.2}/PKG-INFO +2 -1
  2. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/cli.py +83 -12
  3. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/pyproject.toml +2 -1
  4. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/tests/test_claude_sdk_adapter.py +0 -1
  5. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/tests/test_connector_cli.py +74 -8
  6. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/uv.lock +12 -1
  7. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/.gitignore +0 -0
  8. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/README.md +0 -0
  9. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/__init__.py +0 -0
  10. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/adapter.py +0 -0
  11. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/attachments.py +0 -0
  12. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/capabilities.py +0 -0
  13. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/claude/__init__.py +0 -0
  14. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/claude/history_adapter.py +0 -0
  15. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/claude/normalized.py +0 -0
  16. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/claude/normalizers.py +0 -0
  17. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/claude/path_utils.py +0 -0
  18. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/claude/preferences.py +0 -0
  19. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/claude/sdk_adapter.py +0 -0
  20. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/claude/timeline_identity.py +0 -0
  21. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/claude/timeline_reducer.py +0 -0
  22. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/claude/trust.py +0 -0
  23. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/codex/__init__.py +0 -0
  24. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/codex/adapter.py +0 -0
  25. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/codex/history.py +0 -0
  26. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/codex/reducer.py +0 -0
  27. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/codex/rpc.py +0 -0
  28. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/launch.py +0 -0
  29. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/local/__init__.py +0 -0
  30. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/local/common.py +0 -0
  31. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/local/file_ops.py +0 -0
  32. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/local/ops.py +0 -0
  33. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/local/shell.py +0 -0
  34. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/local/terminal.py +0 -0
  35. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/local_ops.py +0 -0
  36. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/protocol.py +0 -0
  37. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/runtime.py +0 -0
  38. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/sync_state.py +0 -0
  39. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/connector/time.py +0 -0
  40. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/run.sh +0 -0
  41. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/tests/test_claude_history_adapter.py +0 -0
  42. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/tests/test_claude_preferences.py +0 -0
  43. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/tests/test_claude_timeline_parity.py +0 -0
  44. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/tests/test_claude_trust.py +0 -0
  45. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/tests/test_codex_adapter.py +0 -0
  46. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/tests/test_connector_capabilities.py +0 -0
  47. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/tests/test_connector_runtime.py +0 -0
  48. {anywhere_cli-0.1.0 → anywhere_cli-0.1.2}/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.0
3
+ Version: 0.1.2
4
4
  Summary: Local runtime connector for Agents Anywhere
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: claude-agent-sdk
@@ -10,6 +10,7 @@ Requires-Dist: pexpect>=4.9.0; sys_platform != 'win32'
10
10
  Requires-Dist: ptyprocess>=0.7.0; sys_platform != 'win32'
11
11
  Requires-Dist: pydantic>=2.0.0
12
12
  Requires-Dist: pyte>=0.8.2
13
+ Requires-Dist: python-socks>=2.8.1
13
14
  Requires-Dist: pywinpty>=2.0.13; sys_platform == 'win32'
14
15
  Requires-Dist: websockets>=16.0
15
16
  Description-Content-Type: text/markdown
@@ -6,6 +6,7 @@ import os
6
6
  import sys
7
7
  import time
8
8
  from pathlib import Path
9
+ from urllib.parse import urlparse
9
10
 
10
11
  import httpx
11
12
 
@@ -16,21 +17,41 @@ def main(argv: list[str] | None = None) -> None:
16
17
  parser = _build_parser()
17
18
  args = parser.parse_args(argv)
18
19
  try:
19
- if args.command == "login":
20
- asyncio.run(_login(args))
20
+ if args.command in {"pair", "login"}:
21
+ asyncio.run(_pair(args))
21
22
  elif args.command == "configure":
22
23
  _configure(args)
23
24
  elif args.command == "start":
24
25
  asyncio.run(_start(args))
25
26
  else:
26
27
  parser.print_help()
28
+ except CliError as exc:
29
+ print(f"error: {exc}", file=sys.stderr)
30
+ raise SystemExit(2) from None
31
+ except httpx.TimeoutException as exc:
32
+ print(f"error: request timed out: {exc.request.url if exc.request else exc}", file=sys.stderr)
33
+ raise SystemExit(2) from None
34
+ except httpx.HTTPStatusError as exc:
35
+ detail = _response_detail(exc.response)
36
+ print(f"error: server returned HTTP {exc.response.status_code}: {detail}", file=sys.stderr)
37
+ raise SystemExit(2) from None
38
+ except httpx.RequestError as exc:
39
+ print(f"error: cannot reach server: {exc}", file=sys.stderr)
40
+ raise SystemExit(2) from None
41
+ except (TimeoutError, RuntimeError, ValueError) as exc:
42
+ print(f"error: {exc}", file=sys.stderr)
43
+ raise SystemExit(2) from None
27
44
  except KeyboardInterrupt:
28
45
  raise SystemExit(130) from None
29
46
 
30
47
 
48
+ class CliError(RuntimeError):
49
+ pass
50
+
51
+
31
52
  def _build_parser() -> argparse.ArgumentParser:
32
53
  parser = argparse.ArgumentParser(prog="anywhere-cli", description="Agent Server Codex connector CLI")
33
- subparsers = parser.add_subparsers(dest="command")
54
+ subparsers = parser.add_subparsers(dest="command", metavar="{start,pair,configure}")
34
55
 
35
56
  start = subparsers.add_parser("start", help="start the connector")
36
57
  _add_config_args(start)
@@ -38,12 +59,12 @@ def _build_parser() -> argparse.ArgumentParser:
38
59
  start.add_argument("--connector-id", help="connector id")
39
60
  start.add_argument("--connector-token", help="connector token")
40
61
 
41
- login = subparsers.add_parser("login", help="pair with a backend, save credentials, and start the connector")
42
- _add_config_args(login)
43
- login.add_argument("--server-url", required=True, help="backend server URL")
44
- login.add_argument("--poll-interval", type=float, default=2, help="seconds between pairing polls")
45
- login.add_argument("--timeout", type=float, default=600, help="pairing timeout in seconds")
46
- login.add_argument("--no-start", action="store_true", help="save credentials without starting the connector")
62
+ pair = subparsers.add_parser(
63
+ "pair",
64
+ aliases=["login"],
65
+ help="pair with a backend, save credentials, and start the connector",
66
+ )
67
+ _add_pair_args(pair)
47
68
 
48
69
  configure = subparsers.add_parser("configure", help="save connector credentials to local JSON")
49
70
  _add_config_args(configure)
@@ -61,13 +82,22 @@ def _add_config_args(parser: argparse.ArgumentParser) -> None:
61
82
  )
62
83
 
63
84
 
85
+ def _add_pair_args(parser: argparse.ArgumentParser) -> None:
86
+ _add_config_args(parser)
87
+ parser.add_argument("server", nargs="?", help="backend server URL, for example anywhere.com or https://api.anywhere.com")
88
+ parser.add_argument("--server-url", help="backend server URL (deprecated; use positional server)")
89
+ parser.add_argument("--poll-interval", type=float, default=2, help="seconds between pairing polls")
90
+ parser.add_argument("--timeout", type=float, default=600, help="pairing timeout in seconds")
91
+ parser.add_argument("--no-start", action="store_true", help="save credentials without starting the connector")
92
+
93
+
64
94
  async def _start(args: argparse.Namespace) -> None:
65
95
  config = _resolve_config(args)
66
96
  await BackendRpcClient(config).run_forever()
67
97
 
68
98
 
69
- async def _login(args: argparse.Namespace) -> None:
70
- server_url = args.server_url.rstrip("/")
99
+ async def _pair(args: argparse.Namespace) -> None:
100
+ server_url = await _resolve_server_url_for_pair(args.server or args.server_url, timeout=10)
71
101
  async with httpx.AsyncClient(timeout=30) as client:
72
102
  start_response = await client.post(
73
103
  f"{server_url}/pairing/start",
@@ -109,6 +139,35 @@ async def _login(args: argparse.Namespace) -> None:
109
139
  raise TimeoutError("pairing timed out")
110
140
 
111
141
 
142
+ async def _resolve_server_url_for_pair(value: str | None, *, timeout: float = 10) -> str:
143
+ if not value:
144
+ raise CliError("missing server address. Usage: anywhere-cli pair <server>")
145
+ normalized = value.strip().rstrip("/")
146
+ if not normalized:
147
+ raise CliError("missing server address. Usage: anywhere-cli pair <server>")
148
+
149
+ parsed = urlparse(normalized)
150
+ if parsed.scheme:
151
+ if parsed.scheme in {"http", "https"}:
152
+ return normalized
153
+ if "://" in normalized:
154
+ raise CliError("server URL must use http or https")
155
+
156
+ candidates = [f"https://{normalized}", f"http://{normalized}"]
157
+ errors: list[str] = []
158
+ for candidate in candidates:
159
+ try:
160
+ async with httpx.AsyncClient(timeout=timeout) as client:
161
+ response = await client.get(f"{candidate}/health")
162
+ if response.status_code < 500:
163
+ return candidate
164
+ errors.append(f"{candidate}: HTTP {response.status_code}")
165
+ except httpx.RequestError as exc:
166
+ errors.append(f"{candidate}: {exc}")
167
+ joined = "; ".join(errors)
168
+ raise CliError(f"could not reach server over https or http ({joined})")
169
+
170
+
112
171
  def _configure(args: argparse.Namespace) -> None:
113
172
  config = ConnectorConfig(
114
173
  server_url=args.server_url.rstrip("/"),
@@ -142,7 +201,19 @@ def _resolve_config(args: argparse.Namespace) -> ConnectorConfig:
142
201
  if not connector_token:
143
202
  missing.append("--connector-token")
144
203
  missing.append(f"or config file {config_path}")
145
- raise SystemExit("missing connector credentials: " + ", ".join(missing))
204
+ raise CliError("missing connector credentials: " + ", ".join(missing))
205
+
206
+
207
+ def _response_detail(response: httpx.Response) -> str:
208
+ try:
209
+ payload = response.json()
210
+ except ValueError:
211
+ text = response.text.strip()
212
+ return text[:300] if text else response.reason_phrase
213
+ if isinstance(payload, dict):
214
+ detail = payload.get("detail") or payload.get("message") or payload
215
+ return str(detail)
216
+ return str(payload)
146
217
 
147
218
 
148
219
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anywhere-cli"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "Local runtime connector for Agents Anywhere"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -12,6 +12,7 @@ dependencies = [
12
12
  "ptyprocess>=0.7.0; sys_platform != 'win32'",
13
13
  "pydantic>=2.0.0",
14
14
  "pyte>=0.8.2",
15
+ "python-socks>=2.8.1",
15
16
  "pywinpty>=2.0.13; sys_platform == 'win32'",
16
17
  "websockets>=16.0",
17
18
  ]
@@ -414,7 +414,6 @@ async def test_claude_sdk_adapter_streams_timeline_and_updates_external_session(
414
414
  "mediaType": "text/plain",
415
415
  "size": 12,
416
416
  "sha256": "abc",
417
- "downloadUrl": "/sessions/sess_1/attachments/file_1",
418
417
  }
419
418
  ]
420
419
  assert timeline[2]["id"].startswith("claude_msg_")
@@ -46,6 +46,30 @@ class FakeHttpClient:
46
46
  raise AssertionError(f"unexpected url: {url}")
47
47
 
48
48
 
49
+ class FakeProbeResponse:
50
+ def __init__(self, status_code: int) -> None:
51
+ self.status_code = status_code
52
+
53
+
54
+ class FakeFallbackHttpClient:
55
+ calls: list[str] = []
56
+
57
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
58
+ return None
59
+
60
+ async def __aenter__(self) -> FakeFallbackHttpClient:
61
+ return self
62
+
63
+ async def __aexit__(self, *args: Any) -> None:
64
+ return None
65
+
66
+ async def get(self, url: str) -> FakeProbeResponse:
67
+ self.calls.append(url)
68
+ if url.startswith("https://"):
69
+ raise cli_module.httpx.ConnectError("tls failed")
70
+ return FakeProbeResponse(200)
71
+
72
+
49
73
  class FakeBackendRpcClient:
50
74
  started_configs: list[ConnectorConfig] = []
51
75
 
@@ -56,7 +80,7 @@ class FakeBackendRpcClient:
56
80
  self.started_configs.append(self.config)
57
81
 
58
82
 
59
- def test_login_starts_connector_after_saving_credentials(monkeypatch, tmp_path) -> None:
83
+ def test_pair_starts_connector_after_saving_credentials(monkeypatch, tmp_path) -> None:
60
84
  config_path = tmp_path / "connector.json"
61
85
  FakeBackendRpcClient.started_configs = []
62
86
  monkeypatch.setattr(cli_module.httpx, "AsyncClient", FakeHttpClient)
@@ -64,8 +88,7 @@ def test_login_starts_connector_after_saving_credentials(monkeypatch, tmp_path)
64
88
 
65
89
  args = cli_module._build_parser().parse_args(
66
90
  [
67
- "login",
68
- "--server-url",
91
+ "pair",
69
92
  "http://127.0.0.1:8000",
70
93
  "--config",
71
94
  str(config_path),
@@ -73,14 +96,14 @@ def test_login_starts_connector_after_saving_credentials(monkeypatch, tmp_path)
73
96
  "0",
74
97
  ]
75
98
  )
76
- asyncio.run(cli_module._login(args))
99
+ asyncio.run(cli_module._pair(args))
77
100
 
78
101
  loaded = ConnectorConfig.load(config_path)
79
102
  assert loaded.connector_id == "conn_1"
80
103
  assert [config.connector_id for config in FakeBackendRpcClient.started_configs] == ["conn_1"]
81
104
 
82
105
 
83
- def test_login_no_start_only_saves_credentials(monkeypatch, tmp_path) -> None:
106
+ def test_pair_no_start_only_saves_credentials(monkeypatch, tmp_path) -> None:
84
107
  config_path = tmp_path / "connector.json"
85
108
  FakeBackendRpcClient.started_configs = []
86
109
  monkeypatch.setattr(cli_module.httpx, "AsyncClient", FakeHttpClient)
@@ -88,8 +111,7 @@ def test_login_no_start_only_saves_credentials(monkeypatch, tmp_path) -> None:
88
111
 
89
112
  args = cli_module._build_parser().parse_args(
90
113
  [
91
- "login",
92
- "--server-url",
114
+ "pair",
93
115
  "http://127.0.0.1:8000",
94
116
  "--config",
95
117
  str(config_path),
@@ -98,7 +120,51 @@ def test_login_no_start_only_saves_credentials(monkeypatch, tmp_path) -> None:
98
120
  "--no-start",
99
121
  ]
100
122
  )
101
- asyncio.run(cli_module._login(args))
123
+ asyncio.run(cli_module._pair(args))
102
124
 
103
125
  assert ConnectorConfig.load(config_path).connector_token == "cxt_secret"
104
126
  assert FakeBackendRpcClient.started_configs == []
127
+
128
+
129
+ def test_pair_accepts_legacy_server_url_flag(monkeypatch, tmp_path) -> None:
130
+ config_path = tmp_path / "connector.json"
131
+ FakeBackendRpcClient.started_configs = []
132
+ monkeypatch.setattr(cli_module.httpx, "AsyncClient", FakeHttpClient)
133
+ monkeypatch.setattr(cli_module, "BackendRpcClient", FakeBackendRpcClient)
134
+
135
+ args = cli_module._build_parser().parse_args(
136
+ [
137
+ "pair",
138
+ "--server-url",
139
+ "http://127.0.0.1:8000",
140
+ "--config",
141
+ str(config_path),
142
+ "--poll-interval",
143
+ "0",
144
+ "--no-start",
145
+ ]
146
+ )
147
+ asyncio.run(cli_module._pair(args))
148
+
149
+ assert ConnectorConfig.load(config_path).connector_id == "conn_1"
150
+
151
+
152
+ def test_login_alias_is_still_accepted() -> None:
153
+ args = cli_module._build_parser().parse_args(["login", "http://127.0.0.1:8000", "--no-start"])
154
+
155
+ assert args.command == "login"
156
+ assert args.server == "http://127.0.0.1:8000"
157
+ assert args.no_start is True
158
+
159
+
160
+ def test_pair_server_without_scheme_falls_back_to_http(monkeypatch) -> None:
161
+ FakeFallbackHttpClient.calls = []
162
+ monkeypatch.setattr(cli_module.httpx, "AsyncClient", FakeFallbackHttpClient)
163
+
164
+ resolved = asyncio.run(cli_module._resolve_server_url_for_pair("anywhere.test:6664", timeout=0.1))
165
+
166
+ assert resolved == "http://anywhere.test:6664"
167
+ assert FakeFallbackHttpClient.calls == [
168
+ "https://anywhere.test:6664/health",
169
+ "http://anywhere.test:6664/health",
170
+ ]
@@ -26,7 +26,7 @@ wheels = [
26
26
 
27
27
  [[package]]
28
28
  name = "anywhere-cli"
29
- version = "0.1.0"
29
+ version = "0.1.2"
30
30
  source = { editable = "." }
31
31
  dependencies = [
32
32
  { name = "claude-agent-sdk" },
@@ -36,6 +36,7 @@ dependencies = [
36
36
  { name = "ptyprocess", marker = "sys_platform != 'win32'" },
37
37
  { name = "pydantic" },
38
38
  { name = "pyte" },
39
+ { name = "python-socks" },
39
40
  { name = "pywinpty", marker = "sys_platform == 'win32'" },
40
41
  { name = "websockets" },
41
42
  ]
@@ -55,6 +56,7 @@ requires-dist = [
55
56
  { name = "ptyprocess", marker = "sys_platform != 'win32'", specifier = ">=0.7.0" },
56
57
  { name = "pydantic", specifier = ">=2.0.0" },
57
58
  { name = "pyte", specifier = ">=0.8.2" },
59
+ { name = "python-socks", specifier = ">=2.8.1" },
58
60
  { name = "pywinpty", marker = "sys_platform == 'win32'", specifier = ">=2.0.13" },
59
61
  { name = "websockets", specifier = ">=16.0" },
60
62
  ]
@@ -582,6 +584,15 @@ wheels = [
582
584
  { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
583
585
  ]
584
586
 
587
+ [[package]]
588
+ name = "python-socks"
589
+ version = "2.8.1"
590
+ source = { registry = "https://pypi.org/simple" }
591
+ sdist = { url = "https://files.pythonhosted.org/packages/36/0b/cd77011c1bc01b76404f7aba07fca18aca02a19c7626e329b40201217624/python_socks-2.8.1.tar.gz", hash = "sha256:698daa9616d46dddaffe65b87db222f2902177a2d2b2c0b9a9361df607ab3687", size = 38909, upload-time = "2026-02-16T05:24:00.745Z" }
592
+ wheels = [
593
+ { url = "https://files.pythonhosted.org/packages/15/fe/9a58cb6eec633ff6afae150ca53c16f8cc8b65862ccb3d088051efdfceb7/python_socks-2.8.1-py3-none-any.whl", hash = "sha256:28232739c4988064e725cdbcd15be194743dd23f1c910f784163365b9d7be035", size = 55087, upload-time = "2026-02-16T05:23:59.147Z" },
594
+ ]
595
+
585
596
  [[package]]
586
597
  name = "pywin32"
587
598
  version = "312"
File without changes
File without changes
File without changes