pairling 0.0.1 → 0.1.0
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.
- package/package.json +5 -1
- package/payload/mac/SOURCE_BRANCH +1 -0
- package/payload/mac/SOURCE_DIRTY +1 -0
- package/payload/mac/SOURCE_REVISION +1 -0
- package/payload/mac/VERSION +1 -0
- package/payload/mac/companiond/integrations/__init__.py +1 -0
- package/payload/mac/companiond/integrations/aperture_cli/__init__.py +23 -0
- package/payload/mac/companiond/integrations/aperture_cli/launch.py +456 -0
- package/payload/mac/companiond/integrations/aperture_cli/status.py +393 -0
- package/payload/mac/companiond/live_activity_publisher.py +380 -0
- package/payload/mac/companiond/llm_route.py +108 -0
- package/payload/mac/companiond/local_mcp_bridge.py +156 -0
- package/payload/mac/companiond/model_status_contract.py +101 -0
- package/payload/mac/companiond/pairdrop_store.py +920 -0
- package/payload/mac/companiond/pairling_connectd_status.py +149 -0
- package/payload/mac/companiond/pairling_devices.py +459 -0
- package/payload/mac/companiond/pairling_pairing.py +404 -0
- package/payload/mac/companiond/pairling_relay_claims.py +232 -0
- package/payload/mac/companiond/pairling_tools.py +706 -0
- package/payload/mac/companiond/pairlingd.py +18438 -0
- package/payload/mac/companiond/providers/__init__.py +1 -0
- package/payload/mac/companiond/providers/base.py +255 -0
- package/payload/mac/companiond/providers/claude.py +127 -0
- package/payload/mac/companiond/providers/codex.py +124 -0
- package/payload/mac/companiond/providers/external.py +46 -0
- package/payload/mac/companiond/providers/registry.py +70 -0
- package/payload/mac/companiond/pty_broker.py +887 -0
- package/payload/mac/companiond/push_dispatcher.py +1990 -0
- package/payload/mac/companiond/push_event_catalog.py +566 -0
- package/payload/mac/companiond/request_proof.py +142 -0
- package/payload/mac/companiond/runtime_contract.py +47 -0
- package/payload/mac/companiond/runtime_manifest.py +197 -0
- package/payload/mac/companiond/runtime_paths.py +87 -0
- package/payload/mac/companiond/safety_monitor.py +542 -0
- package/payload/mac/companiond/sentinel_notifications.py +491 -0
- package/payload/mac/companiond/standard_push_publisher.py +516 -0
- package/payload/mac/companiond/substrate_status_contract.py +139 -0
- package/payload/mac/companiond/terminal_screen_backend.py +332 -0
- package/payload/mac/companiond/terminal_text_sanitizer.py +54 -0
- package/payload/mac/companiond/workstate_feed_contract.py +108 -0
- package/payload/mac/connectd/cmd/pairling-connectd/auth_open_test.go +116 -0
- package/payload/mac/connectd/cmd/pairling-connectd/main.go +345 -0
- package/payload/mac/connectd/cmd/pairling-connectd/upstream_health_test.go +33 -0
- package/payload/mac/connectd/go.mod +51 -0
- package/payload/mac/connectd/go.sum +229 -0
- package/payload/mac/connectd/internal/gateway/proxy.go +597 -0
- package/payload/mac/connectd/internal/gateway/proxy_test.go +531 -0
- package/payload/mac/connectd/internal/runtime/config.go +99 -0
- package/payload/mac/connectd/internal/runtime/config_test.go +29 -0
- package/payload/mac/connectd/internal/status/status.go +300 -0
- package/payload/mac/connectd/internal/status/status_test.go +263 -0
- package/payload/mac/guardian/companion-power-guardian.py +613 -0
- package/payload/mac/guardian/guardian_contract.py +67 -0
- package/payload/mac/install/bootstrap-first-run.sh +206 -0
- package/payload/mac/install/doctor.sh +660 -0
- package/payload/mac/install/install-runtime.sh +1241 -0
- package/payload/mac/install/render-launchd.py +119 -0
- package/payload/mac/install/uninstall-runtime.sh +136 -0
- package/payload/mac/mcp/phone_tools.py +210 -0
- package/payload/mac/packaging/bin/pairling +63 -0
- package/payload-manifest.json +255 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
APERTURE_CLI_KNOWN_VERSION = "v0.0.8"
|
|
15
|
+
APERTURE_CLI_STATUS_PATH = "/aperture-cli/status"
|
|
16
|
+
APERTURE_CLI_PROVIDER_PATH = "/aperture-cli/providers"
|
|
17
|
+
DEFAULT_ENDPOINT = "http://ai"
|
|
18
|
+
PROVIDER_TIMEOUT_SECONDS = 10
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _redact_home(path: Path, home: Path) -> str:
|
|
22
|
+
try:
|
|
23
|
+
return "~/" + str(path.resolve().relative_to(home.resolve()))
|
|
24
|
+
except Exception:
|
|
25
|
+
return str(path)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _run_text(args: list[str], timeout: int = 3) -> tuple[bool, str]:
|
|
29
|
+
try:
|
|
30
|
+
proc = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
return False, f"{type(exc).__name__}: {str(exc)[:160]}"
|
|
33
|
+
text = (proc.stdout or proc.stderr or "").strip()
|
|
34
|
+
return proc.returncode == 0, text[:400]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _dedupe_paths(paths: list[Path]) -> list[Path]:
|
|
38
|
+
seen: set[str] = set()
|
|
39
|
+
result: list[Path] = []
|
|
40
|
+
for path in paths:
|
|
41
|
+
key = str(path.expanduser())
|
|
42
|
+
if key in seen:
|
|
43
|
+
continue
|
|
44
|
+
seen.add(key)
|
|
45
|
+
result.append(path.expanduser())
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _safe_str(value: Any, limit: int = 240) -> str | None:
|
|
50
|
+
if not isinstance(value, str):
|
|
51
|
+
return None
|
|
52
|
+
value = value.strip()
|
|
53
|
+
if not value:
|
|
54
|
+
return None
|
|
55
|
+
return value[:limit]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _safe_bool(value: Any) -> bool:
|
|
59
|
+
return bool(value) if isinstance(value, bool) else False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _canonical_endpoint_url(raw: str) -> tuple[str, str | None, str | None, bool]:
|
|
63
|
+
value = raw.strip().rstrip("/")
|
|
64
|
+
if not value:
|
|
65
|
+
return "", None, None, False
|
|
66
|
+
normalized = value if "://" in value else f"http://{value}"
|
|
67
|
+
parsed = urllib.parse.urlsplit(normalized)
|
|
68
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
69
|
+
return value, None, None, normalized != value
|
|
70
|
+
path = parsed.path.rstrip("/")
|
|
71
|
+
canonical = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, path, "", "")).rstrip("/")
|
|
72
|
+
return canonical, parsed.netloc, parsed.hostname, canonical != value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _normalize_endpoint(obj: Any) -> dict[str, Any] | None:
|
|
76
|
+
if not isinstance(obj, dict):
|
|
77
|
+
return None
|
|
78
|
+
url = _safe_str(obj.get("url"))
|
|
79
|
+
if not url:
|
|
80
|
+
return None
|
|
81
|
+
bridge_id = _safe_str(obj.get("bridgeId"), 96)
|
|
82
|
+
normalized_url, display_host, host, normalized = _canonical_endpoint_url(url)
|
|
83
|
+
return {
|
|
84
|
+
"url": normalized_url,
|
|
85
|
+
"mode": "bridge" if bridge_id else "direct",
|
|
86
|
+
"bridge_id": bridge_id,
|
|
87
|
+
"runtime_url": normalized_url,
|
|
88
|
+
"display_host": display_host or url.rstrip("/"),
|
|
89
|
+
"normalized_url": normalized_url,
|
|
90
|
+
"host": host,
|
|
91
|
+
"normalized": normalized,
|
|
92
|
+
"stale": False,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _dedupe_endpoints(endpoints: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
97
|
+
seen: set[tuple[str, str, str | None]] = set()
|
|
98
|
+
result: list[dict[str, Any]] = []
|
|
99
|
+
for endpoint in endpoints:
|
|
100
|
+
key = (
|
|
101
|
+
str(endpoint.get("normalized_url") or endpoint.get("url") or ""),
|
|
102
|
+
str(endpoint.get("mode") or "direct"),
|
|
103
|
+
endpoint.get("bridge_id"),
|
|
104
|
+
)
|
|
105
|
+
if key in seen:
|
|
106
|
+
continue
|
|
107
|
+
seen.add(key)
|
|
108
|
+
result.append(endpoint)
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _normalize_bridge(obj: Any) -> dict[str, Any] | None:
|
|
113
|
+
if not isinstance(obj, dict):
|
|
114
|
+
return None
|
|
115
|
+
bridge_id = _safe_str(obj.get("id"), 96)
|
|
116
|
+
name = _safe_str(obj.get("name"), 120)
|
|
117
|
+
if not bridge_id:
|
|
118
|
+
return None
|
|
119
|
+
return {"id": bridge_id, "name": name or bridge_id}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _parse_provider_items(raw: Any) -> list[dict[str, Any]]:
|
|
123
|
+
if not isinstance(raw, list):
|
|
124
|
+
return []
|
|
125
|
+
items: list[dict[str, Any]] = []
|
|
126
|
+
for obj in raw:
|
|
127
|
+
if not isinstance(obj, dict):
|
|
128
|
+
continue
|
|
129
|
+
provider_id = _safe_str(obj.get("id"), 120)
|
|
130
|
+
if not provider_id:
|
|
131
|
+
continue
|
|
132
|
+
compatibility = obj.get("compatibility")
|
|
133
|
+
if not isinstance(compatibility, dict):
|
|
134
|
+
compatibility = {}
|
|
135
|
+
models = obj.get("models")
|
|
136
|
+
if not isinstance(models, list):
|
|
137
|
+
models = []
|
|
138
|
+
items.append({
|
|
139
|
+
"id": provider_id,
|
|
140
|
+
"name": _safe_str(obj.get("name"), 160) or provider_id,
|
|
141
|
+
"description": _safe_str(obj.get("description"), 500) or "",
|
|
142
|
+
"models": [m[:240] for m in models if isinstance(m, str)],
|
|
143
|
+
"compatibility": {str(k): bool(v) for k, v in compatibility.items()},
|
|
144
|
+
})
|
|
145
|
+
return items
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ApertureCLIProbe:
|
|
149
|
+
"""Read-only probe for Aperture CLI's local launcher state."""
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
home: Path | None = None,
|
|
154
|
+
env: dict[str, str] | None = None,
|
|
155
|
+
now: float | None = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
self.home = (home or Path.home()).expanduser()
|
|
158
|
+
self.env = env if env is not None else os.environ
|
|
159
|
+
self.now = now
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def config_root(self) -> Path:
|
|
163
|
+
return self.home / "Library" / "Application Support" / "aperture"
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def settings_path(self) -> Path:
|
|
167
|
+
return self.config_root / "settings.json"
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def launcher_path(self) -> Path:
|
|
171
|
+
return self.config_root / "launcher.json"
|
|
172
|
+
|
|
173
|
+
def resolve_binary(self) -> tuple[Path | None, str | None]:
|
|
174
|
+
candidates: list[tuple[Path, str]] = [
|
|
175
|
+
(self.home / "go" / "bin" / "aperture", "known_gopath"),
|
|
176
|
+
(self.home / ".local" / "bin" / "aperture", "known_local"),
|
|
177
|
+
(Path("/opt/homebrew/bin/aperture"), "known_homebrew"),
|
|
178
|
+
(Path("/usr/local/bin/aperture"), "known_usr_local"),
|
|
179
|
+
]
|
|
180
|
+
for prefix in self.env.get("PATH", "").split(":"):
|
|
181
|
+
if prefix:
|
|
182
|
+
candidates.append((Path(prefix) / "aperture", "path"))
|
|
183
|
+
seen: set[str] = set()
|
|
184
|
+
for path, source in candidates:
|
|
185
|
+
path = path.expanduser()
|
|
186
|
+
key = str(path)
|
|
187
|
+
if key in seen:
|
|
188
|
+
continue
|
|
189
|
+
seen.add(key)
|
|
190
|
+
if path.exists() and os.access(path, os.X_OK):
|
|
191
|
+
return path, source
|
|
192
|
+
return None, None
|
|
193
|
+
|
|
194
|
+
def _read_json(self, path: Path) -> tuple[bool, dict[str, Any], str | None]:
|
|
195
|
+
if not path.is_file():
|
|
196
|
+
return False, {}, None
|
|
197
|
+
try:
|
|
198
|
+
obj = json.loads(path.read_text(errors="replace"))
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
return True, {}, f"{type(exc).__name__}: {str(exc)[:160]}"
|
|
201
|
+
if not isinstance(obj, dict):
|
|
202
|
+
return True, {}, "JSON root is not an object"
|
|
203
|
+
return True, obj, None
|
|
204
|
+
|
|
205
|
+
def _settings_payload(self) -> dict[str, Any]:
|
|
206
|
+
found, obj, error = self._read_json(self.settings_path)
|
|
207
|
+
endpoints = []
|
|
208
|
+
for item in obj.get("endpoints", []) if isinstance(obj.get("endpoints"), list) else []:
|
|
209
|
+
endpoint = _normalize_endpoint(item)
|
|
210
|
+
if endpoint:
|
|
211
|
+
endpoints.append(endpoint)
|
|
212
|
+
raw_endpoint_count = len(endpoints)
|
|
213
|
+
endpoints = _dedupe_endpoints(endpoints)
|
|
214
|
+
if not endpoints:
|
|
215
|
+
endpoints = [_normalize_endpoint({"url": DEFAULT_ENDPOINT}) or {
|
|
216
|
+
"url": DEFAULT_ENDPOINT,
|
|
217
|
+
"mode": "direct",
|
|
218
|
+
"bridge_id": None,
|
|
219
|
+
"runtime_url": DEFAULT_ENDPOINT,
|
|
220
|
+
"display_host": "ai",
|
|
221
|
+
"normalized_url": DEFAULT_ENDPOINT,
|
|
222
|
+
"host": "ai",
|
|
223
|
+
"normalized": False,
|
|
224
|
+
"stale": False,
|
|
225
|
+
}]
|
|
226
|
+
raw_endpoint_count = 0
|
|
227
|
+
bridges = []
|
|
228
|
+
for item in obj.get("bridges", []) if isinstance(obj.get("bridges"), list) else []:
|
|
229
|
+
bridge = _normalize_bridge(item)
|
|
230
|
+
if bridge:
|
|
231
|
+
bridges.append(bridge)
|
|
232
|
+
return {
|
|
233
|
+
"found": found,
|
|
234
|
+
"path_redacted": _redact_home(self.settings_path, self.home),
|
|
235
|
+
"parse_error": error,
|
|
236
|
+
"active_endpoint": endpoints[0],
|
|
237
|
+
"endpoints": endpoints,
|
|
238
|
+
"bridges": bridges,
|
|
239
|
+
"endpoint_count": len(endpoints),
|
|
240
|
+
"raw_endpoint_count": raw_endpoint_count,
|
|
241
|
+
"bridge_count": len(bridges),
|
|
242
|
+
"yolo_mode": _safe_bool(obj.get("yoloMode")),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
def _last_launch_payload(self) -> dict[str, Any]:
|
|
246
|
+
found, obj, error = self._read_json(self.launcher_path)
|
|
247
|
+
return {
|
|
248
|
+
"found": found,
|
|
249
|
+
"path_redacted": _redact_home(self.launcher_path, self.home),
|
|
250
|
+
"parse_error": error,
|
|
251
|
+
"client_name": _safe_str(obj.get("lastClientName") or obj.get("lastProfileName"), 120),
|
|
252
|
+
"backend_type": _safe_str(obj.get("lastBackendType"), 120),
|
|
253
|
+
"provider_id": _safe_str(obj.get("lastProviderId"), 120),
|
|
254
|
+
"model": _safe_str(obj.get("lastModel"), 240),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
def fetch_providers(self, endpoint: dict[str, Any]) -> dict[str, Any]:
|
|
258
|
+
base_url = str(endpoint.get("runtime_url") or endpoint.get("normalized_url") or endpoint.get("url") or DEFAULT_ENDPOINT).rstrip("/")
|
|
259
|
+
url = base_url + "/api/providers"
|
|
260
|
+
mode = endpoint.get("mode") or "direct"
|
|
261
|
+
if mode == "bridge":
|
|
262
|
+
return {
|
|
263
|
+
"reachable": False,
|
|
264
|
+
"items": [],
|
|
265
|
+
"count": 0,
|
|
266
|
+
"compatibility_keys": [],
|
|
267
|
+
"source_endpoint": endpoint,
|
|
268
|
+
"last_error": "Bridge endpoints require an active Aperture CLI tsnet proxy; Pairling native bridge probing is not enabled yet.",
|
|
269
|
+
}
|
|
270
|
+
try:
|
|
271
|
+
with urllib.request.urlopen(url, timeout=PROVIDER_TIMEOUT_SECONDS) as response:
|
|
272
|
+
status = getattr(response, "status", 200)
|
|
273
|
+
body = response.read(1024 * 1024)
|
|
274
|
+
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
275
|
+
source_endpoint = {**endpoint, "stale": True}
|
|
276
|
+
return {
|
|
277
|
+
"reachable": False,
|
|
278
|
+
"items": [],
|
|
279
|
+
"count": 0,
|
|
280
|
+
"compatibility_keys": [],
|
|
281
|
+
"source_endpoint": source_endpoint,
|
|
282
|
+
"last_error": f"{type(exc).__name__}: {str(exc)[:220]}",
|
|
283
|
+
}
|
|
284
|
+
if status < 200 or status >= 300:
|
|
285
|
+
source_endpoint = {**endpoint, "stale": True}
|
|
286
|
+
return {
|
|
287
|
+
"reachable": False,
|
|
288
|
+
"items": [],
|
|
289
|
+
"count": 0,
|
|
290
|
+
"compatibility_keys": [],
|
|
291
|
+
"source_endpoint": source_endpoint,
|
|
292
|
+
"last_error": f"unexpected status {status} from {url}",
|
|
293
|
+
}
|
|
294
|
+
try:
|
|
295
|
+
raw = json.loads(body.decode("utf-8", errors="replace"))
|
|
296
|
+
except Exception as exc:
|
|
297
|
+
source_endpoint = {**endpoint, "stale": True}
|
|
298
|
+
return {
|
|
299
|
+
"reachable": False,
|
|
300
|
+
"items": [],
|
|
301
|
+
"count": 0,
|
|
302
|
+
"compatibility_keys": [],
|
|
303
|
+
"source_endpoint": source_endpoint,
|
|
304
|
+
"last_error": f"provider JSON parse failed: {type(exc).__name__}: {str(exc)[:160]}",
|
|
305
|
+
}
|
|
306
|
+
items = _parse_provider_items(raw)
|
|
307
|
+
compatibility_keys = sorted({
|
|
308
|
+
key
|
|
309
|
+
for item in items
|
|
310
|
+
for key, enabled in item.get("compatibility", {}).items()
|
|
311
|
+
if enabled
|
|
312
|
+
})
|
|
313
|
+
return {
|
|
314
|
+
"reachable": True,
|
|
315
|
+
"items": items,
|
|
316
|
+
"count": len(items),
|
|
317
|
+
"ids": [item["id"] for item in items],
|
|
318
|
+
"compatibility_keys": compatibility_keys,
|
|
319
|
+
"source_endpoint": endpoint,
|
|
320
|
+
"last_error": None,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
def status(self) -> dict[str, Any]:
|
|
324
|
+
observed_at = self.now if self.now is not None else time.time()
|
|
325
|
+
binary, source = self.resolve_binary()
|
|
326
|
+
version = None
|
|
327
|
+
help_text = ""
|
|
328
|
+
help_flags: list[str] = []
|
|
329
|
+
if binary:
|
|
330
|
+
ok, text = _run_text([str(binary), "-version"], timeout=3)
|
|
331
|
+
if ok:
|
|
332
|
+
version = text
|
|
333
|
+
ok, help_text = _run_text([str(binary), "--help"], timeout=3)
|
|
334
|
+
if ok:
|
|
335
|
+
for flag in ("-debug", "-version"):
|
|
336
|
+
if flag in help_text:
|
|
337
|
+
help_flags.append(flag)
|
|
338
|
+
settings = self._settings_payload()
|
|
339
|
+
last_launch = self._last_launch_payload()
|
|
340
|
+
providers = self.fetch_providers(settings["active_endpoint"])
|
|
341
|
+
warnings: list[str] = [
|
|
342
|
+
"Aperture CLI is experimental; Pairling is using a version-pinned v0.0.8 contract."
|
|
343
|
+
]
|
|
344
|
+
if binary is None:
|
|
345
|
+
warnings.append("Aperture CLI binary was not found.")
|
|
346
|
+
elif version and version != APERTURE_CLI_KNOWN_VERSION:
|
|
347
|
+
warnings.append(f"Aperture CLI version {version} differs from tested {APERTURE_CLI_KNOWN_VERSION}.")
|
|
348
|
+
if settings.get("parse_error"):
|
|
349
|
+
warnings.append("Aperture CLI settings could not be parsed; Pairling is using the default endpoint.")
|
|
350
|
+
return {
|
|
351
|
+
"ok": True,
|
|
352
|
+
"schema_version": 1,
|
|
353
|
+
"installed": binary is not None,
|
|
354
|
+
"version": version,
|
|
355
|
+
"known_version": APERTURE_CLI_KNOWN_VERSION,
|
|
356
|
+
"binary_path": str(binary) if binary else None,
|
|
357
|
+
"binary_path_source": source,
|
|
358
|
+
"help_flags": help_flags,
|
|
359
|
+
"settings": settings,
|
|
360
|
+
"last_launch": last_launch,
|
|
361
|
+
"providers": {k: v for k, v in providers.items() if k != "items"},
|
|
362
|
+
"capabilities": {
|
|
363
|
+
"native_pairling_launch_supported": False,
|
|
364
|
+
"raw_aperture_tui_supported": binary is not None,
|
|
365
|
+
"bridge_probe_supported": False,
|
|
366
|
+
},
|
|
367
|
+
"warnings": warnings,
|
|
368
|
+
"observed_at": observed_at,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
def providers_payload(self) -> dict[str, Any]:
|
|
372
|
+
settings = self._settings_payload()
|
|
373
|
+
providers = self.fetch_providers(settings["active_endpoint"])
|
|
374
|
+
return {
|
|
375
|
+
"ok": True,
|
|
376
|
+
"schema_version": 1,
|
|
377
|
+
"source_endpoint": providers["source_endpoint"],
|
|
378
|
+
"reachable": providers["reachable"],
|
|
379
|
+
"items": providers["items"],
|
|
380
|
+
"count": providers["count"],
|
|
381
|
+
"ids": providers.get("ids", []),
|
|
382
|
+
"compatibility_keys": providers["compatibility_keys"],
|
|
383
|
+
"last_error": providers["last_error"],
|
|
384
|
+
"observed_at": self.now if self.now is not None else time.time(),
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def status_payload(home: Path | None = None, env: dict[str, str] | None = None) -> dict[str, Any]:
|
|
389
|
+
return ApertureCLIProbe(home=home, env=env).status()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def provider_payload(home: Path | None = None, env: dict[str, str] | None = None) -> dict[str, Any]:
|
|
393
|
+
return ApertureCLIProbe(home=home, env=env).providers_payload()
|