agentlink-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.
- agentlink_cli-0.1.0.dist-info/METADATA +136 -0
- agentlink_cli-0.1.0.dist-info/RECORD +55 -0
- agentlink_cli-0.1.0.dist-info/WHEEL +4 -0
- agentlink_cli-0.1.0.dist-info/entry_points.txt +3 -0
- connector/__init__.py +3 -0
- connector/acp/__init__.py +6 -0
- connector/acp/adapter.py +1221 -0
- connector/acp/config_options.py +175 -0
- connector/acp/discovery.py +385 -0
- connector/acp/manifest.py +110 -0
- connector/acp/manifests/__init__.py +1 -0
- connector/acp/manifests/codebuddy.json +37 -0
- connector/acp/manifests/cursor.json +39 -0
- connector/acp/manifests/gemini.json +33 -0
- connector/acp/manifests/grok_build.json +31 -0
- connector/acp/reducer.py +615 -0
- connector/acp/rpc.py +308 -0
- connector/adapter.py +39 -0
- connector/attachments.py +36 -0
- connector/capabilities.py +603 -0
- connector/claude/__init__.py +8 -0
- connector/claude/history_adapter.py +642 -0
- connector/claude/normalized.py +23 -0
- connector/claude/normalizers.py +97 -0
- connector/claude/path_utils.py +13 -0
- connector/claude/preferences.py +38 -0
- connector/claude/sdk_adapter.py +1376 -0
- connector/claude/timeline_identity.py +47 -0
- connector/claude/timeline_reducer.py +379 -0
- connector/claude/trust.py +69 -0
- connector/cli.py +280 -0
- connector/codex/__init__.py +3 -0
- connector/codex/adapter.py +1150 -0
- connector/codex/history.py +199 -0
- connector/codex/reducer.py +1309 -0
- connector/codex/rpc.py +261 -0
- connector/control.py +298 -0
- connector/json_rpc.py +143 -0
- connector/launch.py +310 -0
- connector/local/__init__.py +6 -0
- connector/local/common.py +118 -0
- connector/local/file_ops.py +144 -0
- connector/local/ops.py +92 -0
- connector/local/shell.py +225 -0
- connector/local/terminal.py +658 -0
- connector/local_ops.py +5 -0
- connector/local_runtime.py +139 -0
- connector/logging.py +50 -0
- connector/perf.py +89 -0
- connector/protocol.py +26 -0
- connector/registry.py +49 -0
- connector/runtime.py +1309 -0
- connector/sync_state.py +155 -0
- connector/time.py +7 -0
- connector/version.py +13 -0
connector/cli.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from connector.control import ConnectorController
|
|
14
|
+
from connector.json_rpc import JsonRpcStdioServer, open_stdio_server
|
|
15
|
+
from connector.local_runtime import (
|
|
16
|
+
assert_can_start,
|
|
17
|
+
clear_runtime,
|
|
18
|
+
runtime_path,
|
|
19
|
+
write_runtime,
|
|
20
|
+
)
|
|
21
|
+
from connector.logging import install_rpc_log_sink
|
|
22
|
+
from connector.runtime import BackendRpcClient, ConnectorConfig
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
SHELL_LIFETIME_WARNING = (
|
|
26
|
+
"Note: this connector runs in the current shell session. "
|
|
27
|
+
"If you do not run it with a tool such as systemd, tmux, screen, "
|
|
28
|
+
"or a background service, the connection will stop when this shell session ends."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv: list[str] | None = None) -> None:
|
|
33
|
+
parser = _build_parser()
|
|
34
|
+
args = parser.parse_args(argv)
|
|
35
|
+
try:
|
|
36
|
+
if args.command in {"pair", "login"}:
|
|
37
|
+
asyncio.run(_pair(args))
|
|
38
|
+
elif args.command == "configure":
|
|
39
|
+
_configure(args)
|
|
40
|
+
elif args.command == "start":
|
|
41
|
+
asyncio.run(_start(args))
|
|
42
|
+
elif args.command == "rpc":
|
|
43
|
+
asyncio.run(_rpc(args))
|
|
44
|
+
else:
|
|
45
|
+
parser.print_help()
|
|
46
|
+
except CliError as exc:
|
|
47
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
48
|
+
raise SystemExit(2) from None
|
|
49
|
+
except httpx.TimeoutException as exc:
|
|
50
|
+
print(f"error: request timed out: {exc.request.url if exc.request else exc}", file=sys.stderr)
|
|
51
|
+
raise SystemExit(2) from None
|
|
52
|
+
except httpx.HTTPStatusError as exc:
|
|
53
|
+
detail = _response_detail(exc.response)
|
|
54
|
+
print(f"error: server returned HTTP {exc.response.status_code}: {detail}", file=sys.stderr)
|
|
55
|
+
raise SystemExit(2) from None
|
|
56
|
+
except httpx.RequestError as exc:
|
|
57
|
+
print(f"error: cannot reach server: {exc}", file=sys.stderr)
|
|
58
|
+
raise SystemExit(2) from None
|
|
59
|
+
except (TimeoutError, RuntimeError, ValueError) as exc:
|
|
60
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
61
|
+
raise SystemExit(2) from None
|
|
62
|
+
except KeyboardInterrupt:
|
|
63
|
+
raise SystemExit(130) from None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CliError(RuntimeError):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
71
|
+
parser = argparse.ArgumentParser(prog="agentlink-cli", description="Agent Server Codex connector CLI")
|
|
72
|
+
subparsers = parser.add_subparsers(dest="command", metavar="{start,pair,configure,rpc}")
|
|
73
|
+
|
|
74
|
+
start = subparsers.add_parser("start", help="start the connector")
|
|
75
|
+
_add_config_args(start)
|
|
76
|
+
start.add_argument("--server-url", help="backend server URL")
|
|
77
|
+
start.add_argument("--connector-id", help="connector id")
|
|
78
|
+
start.add_argument("--connector-token", help="connector token")
|
|
79
|
+
|
|
80
|
+
pair = subparsers.add_parser(
|
|
81
|
+
"pair",
|
|
82
|
+
aliases=["login"],
|
|
83
|
+
help="pair with a backend, save credentials, and start the connector",
|
|
84
|
+
)
|
|
85
|
+
_add_pair_args(pair)
|
|
86
|
+
|
|
87
|
+
configure = subparsers.add_parser("configure", help="save connector credentials to local JSON")
|
|
88
|
+
_add_config_args(configure)
|
|
89
|
+
configure.add_argument("--server-url", required=True, help="backend server URL")
|
|
90
|
+
configure.add_argument("--connector-id", required=True, help="connector id")
|
|
91
|
+
configure.add_argument("--connector-token", required=True, help="connector token")
|
|
92
|
+
|
|
93
|
+
rpc = subparsers.add_parser("rpc", help="serve the desktop connector JSON-RPC API over stdio")
|
|
94
|
+
_add_config_args(rpc)
|
|
95
|
+
return parser
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _add_config_args(parser: argparse.ArgumentParser) -> None:
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--config",
|
|
101
|
+
default=str(ConnectorConfig.default_path()),
|
|
102
|
+
help="local connector config JSON path",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _add_pair_args(parser: argparse.ArgumentParser) -> None:
|
|
107
|
+
_add_config_args(parser)
|
|
108
|
+
parser.add_argument("server", nargs="?", help="backend server URL, for example anywhere.com or https://api.anywhere.com")
|
|
109
|
+
parser.add_argument("--server-url", help="backend server URL (deprecated; use positional server)")
|
|
110
|
+
parser.add_argument("--poll-interval", type=float, default=2, help="seconds between pairing polls")
|
|
111
|
+
parser.add_argument("--timeout", type=float, default=600, help="pairing timeout in seconds")
|
|
112
|
+
parser.add_argument("--no-start", action="store_true", help="save credentials without starting the connector")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def _start(args: argparse.Namespace) -> None:
|
|
116
|
+
config = _resolve_config(args)
|
|
117
|
+
await _run_cli_connector(config, config_path=args.config)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def _rpc(args: argparse.Namespace) -> None:
|
|
121
|
+
server: JsonRpcStdioServer | None = None
|
|
122
|
+
|
|
123
|
+
async def notify(method: str, params: object) -> None:
|
|
124
|
+
if server is not None:
|
|
125
|
+
await server.notify(method, params)
|
|
126
|
+
|
|
127
|
+
controller = ConnectorController(config_path=args.config, notifier=notify)
|
|
128
|
+
handlers = {
|
|
129
|
+
"connector.getState": controller.get_state,
|
|
130
|
+
"connector.getPaths": controller.get_paths,
|
|
131
|
+
"connector.getConfig": controller.get_config,
|
|
132
|
+
"connector.saveConfig": controller.save_config,
|
|
133
|
+
"connector.start": controller.start,
|
|
134
|
+
"connector.stop": controller.stop,
|
|
135
|
+
"connector.restart": controller.restart,
|
|
136
|
+
"connector.startPairing": controller.start_pairing,
|
|
137
|
+
"connector.cancelPairing": controller.cancel_pairing,
|
|
138
|
+
}
|
|
139
|
+
log_sink = install_rpc_log_sink(notify, remove_default_sink=True)
|
|
140
|
+
server = await open_stdio_server(handlers)
|
|
141
|
+
try:
|
|
142
|
+
await server.serve_forever()
|
|
143
|
+
finally:
|
|
144
|
+
await controller.shutdown()
|
|
145
|
+
await log_sink.close()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def _pair(args: argparse.Namespace) -> None:
|
|
149
|
+
server_url = await _resolve_server_url_for_pair(args.server or args.server_url, timeout=10)
|
|
150
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
151
|
+
start_response = await client.post(
|
|
152
|
+
f"{server_url}/pairing/start",
|
|
153
|
+
json={"serverUrl": server_url, "ttlSeconds": int(args.timeout)},
|
|
154
|
+
)
|
|
155
|
+
start_response.raise_for_status()
|
|
156
|
+
pairing = start_response.json()
|
|
157
|
+
pairing_id = pairing["pairingId"]
|
|
158
|
+
code = pairing["code"]
|
|
159
|
+
|
|
160
|
+
print(f"Pairing code: {code}")
|
|
161
|
+
print("Claim it from the web UI")
|
|
162
|
+
# print(
|
|
163
|
+
# "curl -s "
|
|
164
|
+
# f"{server_url}/pairing/claim "
|
|
165
|
+
# "-H 'content-type: application/json' "
|
|
166
|
+
# f"-d '{{\"code\":\"{code}\",\"name\":\"local-codex\",\"userId\":\"local\",\"serverUrl\":\"{server_url}\"}}'"
|
|
167
|
+
# )
|
|
168
|
+
print("Waiting for credentials...")
|
|
169
|
+
|
|
170
|
+
deadline = time.monotonic() + args.timeout
|
|
171
|
+
while time.monotonic() < deadline:
|
|
172
|
+
poll_response = await client.post(f"{server_url}/pairing/poll", json={"pairingId": pairing_id})
|
|
173
|
+
poll_response.raise_for_status()
|
|
174
|
+
payload = poll_response.json()
|
|
175
|
+
if payload["status"] == "claimed" and payload.get("config"):
|
|
176
|
+
config = ConnectorConfig.from_mapping(payload["config"])
|
|
177
|
+
path = config.save(args.config)
|
|
178
|
+
print(f"Saved connector config: {path}")
|
|
179
|
+
if args.no_start:
|
|
180
|
+
return
|
|
181
|
+
print("Starting connector...")
|
|
182
|
+
await _run_cli_connector(config, config_path=args.config)
|
|
183
|
+
return
|
|
184
|
+
if payload["status"] in {"expired", "consumed"}:
|
|
185
|
+
raise RuntimeError(f"pairing ended with status: {payload['status']}")
|
|
186
|
+
await asyncio.sleep(args.poll_interval)
|
|
187
|
+
|
|
188
|
+
raise TimeoutError("pairing timed out")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def _run_cli_connector(config: ConnectorConfig, *, config_path: str | Path | None) -> None:
|
|
192
|
+
runtime_file = runtime_path(config_path)
|
|
193
|
+
assert_can_start(runtime_file, config)
|
|
194
|
+
write_runtime(runtime_file, config, kind="cli")
|
|
195
|
+
print(SHELL_LIFETIME_WARNING)
|
|
196
|
+
try:
|
|
197
|
+
await BackendRpcClient(config).run_forever()
|
|
198
|
+
finally:
|
|
199
|
+
clear_runtime(runtime_file)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def _resolve_server_url_for_pair(value: str | None, *, timeout: float = 10) -> str:
|
|
203
|
+
if not value:
|
|
204
|
+
raise CliError("missing server address. Usage: agentlink-cli pair <server>")
|
|
205
|
+
normalized = value.strip().rstrip("/")
|
|
206
|
+
if not normalized:
|
|
207
|
+
raise CliError("missing server address. Usage: agentlink-cli pair <server>")
|
|
208
|
+
|
|
209
|
+
parsed = urlparse(normalized)
|
|
210
|
+
if parsed.scheme:
|
|
211
|
+
if parsed.scheme in {"http", "https"}:
|
|
212
|
+
return normalized
|
|
213
|
+
if "://" in normalized:
|
|
214
|
+
raise CliError("server URL must use http or https")
|
|
215
|
+
|
|
216
|
+
candidates = [f"https://{normalized}", f"http://{normalized}"]
|
|
217
|
+
errors: list[str] = []
|
|
218
|
+
for candidate in candidates:
|
|
219
|
+
try:
|
|
220
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
221
|
+
response = await client.get(f"{candidate}/health")
|
|
222
|
+
if response.status_code < 500:
|
|
223
|
+
return candidate
|
|
224
|
+
errors.append(f"{candidate}: HTTP {response.status_code}")
|
|
225
|
+
except httpx.RequestError as exc:
|
|
226
|
+
errors.append(f"{candidate}: {exc}")
|
|
227
|
+
joined = "; ".join(errors)
|
|
228
|
+
raise CliError(f"could not reach server over https or http ({joined})")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _configure(args: argparse.Namespace) -> None:
|
|
232
|
+
config = ConnectorConfig(
|
|
233
|
+
server_url=args.server_url.rstrip("/"),
|
|
234
|
+
connector_id=args.connector_id,
|
|
235
|
+
connector_token=args.connector_token,
|
|
236
|
+
)
|
|
237
|
+
path = config.save(args.config)
|
|
238
|
+
print(f"Saved connector config: {path}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _resolve_config(args: argparse.Namespace) -> ConnectorConfig:
|
|
242
|
+
server_url = args.server_url or os.environ.get("AGENT_SERVER_URL")
|
|
243
|
+
connector_id = args.connector_id or os.environ.get("AGENT_CONNECTOR_ID")
|
|
244
|
+
connector_token = args.connector_token or os.environ.get("AGENT_CONNECTOR_TOKEN")
|
|
245
|
+
if server_url and connector_id and connector_token:
|
|
246
|
+
return ConnectorConfig(
|
|
247
|
+
server_url=server_url.rstrip("/"),
|
|
248
|
+
connector_id=connector_id,
|
|
249
|
+
connector_token=connector_token,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
config_path = Path(args.config)
|
|
253
|
+
if config_path.exists():
|
|
254
|
+
return ConnectorConfig.load(config_path)
|
|
255
|
+
|
|
256
|
+
missing = []
|
|
257
|
+
if not server_url:
|
|
258
|
+
missing.append("--server-url")
|
|
259
|
+
if not connector_id:
|
|
260
|
+
missing.append("--connector-id")
|
|
261
|
+
if not connector_token:
|
|
262
|
+
missing.append("--connector-token")
|
|
263
|
+
missing.append(f"or config file {config_path}")
|
|
264
|
+
raise CliError("missing connector credentials: " + ", ".join(missing))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _response_detail(response: httpx.Response) -> str:
|
|
268
|
+
try:
|
|
269
|
+
payload = response.json()
|
|
270
|
+
except ValueError:
|
|
271
|
+
text = response.text.strip()
|
|
272
|
+
return text[:300] if text else response.reason_phrase
|
|
273
|
+
if isinstance(payload, dict):
|
|
274
|
+
detail = payload.get("detail") or payload.get("message") or payload
|
|
275
|
+
return str(detail)
|
|
276
|
+
return str(payload)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
main(sys.argv[1:])
|