bilinc 2.2.0__tar.gz → 2.3.0__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.
- {bilinc-2.2.0/src/bilinc.egg-info → bilinc-2.3.0}/PKG-INFO +16 -19
- {bilinc-2.2.0 → bilinc-2.3.0}/README.md +15 -18
- {bilinc-2.2.0 → bilinc-2.3.0}/pyproject.toml +1 -1
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc/__init__.py +2 -2
- bilinc-2.3.0/src/bilinc/cli/login.py +241 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc/cli/main.py +54 -20
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc/client.py +17 -11
- {bilinc-2.2.0 → bilinc-2.3.0/src/bilinc.egg-info}/PKG-INFO +16 -19
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc.egg-info/SOURCES.txt +1 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/tests/test_cloud_only_package.py +8 -8
- {bilinc-2.2.0 → bilinc-2.3.0}/LICENSE +0 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/MANIFEST.in +0 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/setup.cfg +0 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/setup.py +0 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc/cli/__init__.py +0 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc/cli/__main__.py +0 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc/cloud_mcp.py +0 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc.egg-info/dependency_links.txt +0 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc.egg-info/entry_points.txt +0 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc.egg-info/requires.txt +0 -0
- {bilinc-2.2.0 → bilinc-2.3.0}/src/bilinc.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bilinc
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Cloud-only memory SDK and MCP adapter for autonomous agents
|
|
5
5
|
Author: Bilinc Team
|
|
6
6
|
License-Expression: BUSL-1.1
|
|
@@ -56,7 +56,7 @@ Dynamic: license-file
|
|
|
56
56
|
|
|
57
57
|
Retrieval answers *"what is similar to this?"*. Long-running agents also need to answer *"who wrote this state, was it verified, did it contradict what we already knew, and can we undo it?"* — that is the layer Bilinc provides.
|
|
58
58
|
|
|
59
|
-
Bilinc 2.
|
|
59
|
+
Bilinc 2.3.0 on PyPI is the public cloud-only package: a thin Python SDK, CLI, and MCP adapter for Bilinc Cloud. It does not ship the local StatePlane, storage backends, eval, observability, integrations, or server runtime internals.
|
|
60
60
|
|
|
61
61
|
> **Frozen regression receipt** — LongMemEval-s cleaned retrieval fixture, 500 questions: **Hit@5 98.0%**, **NDCG@5 0.913**, no LLM reranker, no paid API. This is an isolated retrieval guardrail, not a current hosted SLA, end-to-end agent score, or competitor ranking — see [Benchmark receipt](#benchmark-receipt) for the full scope and qualification.
|
|
62
62
|
|
|
@@ -102,31 +102,28 @@ storage runtime.
|
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
104
|
pip install -U bilinc
|
|
105
|
-
bilinc
|
|
105
|
+
bilinc login
|
|
106
|
+
bilinc quicktest
|
|
106
107
|
```
|
|
107
108
|
|
|
108
|
-
`bilinc
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
2. Confirm email.
|
|
114
|
-
3. Create one hosted API key in the Cloud dashboard.
|
|
115
|
-
4. Connect the CLI:
|
|
109
|
+
`bilinc login` opens your browser. Sign in with Google, GitHub, or email (free,
|
|
110
|
+
no card required) and approve this computer; the CLI saves its own API key with
|
|
111
|
+
owner-only permissions. The key never appears in a URL or your terminal.
|
|
112
|
+
`bilinc quicktest` then performs one hosted commit, one hosted recall, and one
|
|
113
|
+
Cloud status check.
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
bilinc
|
|
120
|
-
```
|
|
115
|
+
- **No browser on this machine** (SSH, servers, containers): `bilinc login --device`
|
|
116
|
+
prints a short code to approve from any browser.
|
|
117
|
+
- **CI and scripts:** `bilinc login --api-key bil_live_...`, or set `BILINC_API_KEY`.
|
|
121
118
|
|
|
122
119
|
To reproduce this release exactly:
|
|
123
120
|
|
|
124
121
|
```bash
|
|
125
|
-
pip install -U bilinc==2.
|
|
122
|
+
pip install -U bilinc==2.3.0
|
|
126
123
|
```
|
|
127
124
|
|
|
128
125
|
If you prefer a browser guide, open https://bilinc.space/install and follow the
|
|
129
|
-
same
|
|
126
|
+
same three-step path.
|
|
130
127
|
|
|
131
128
|
## MCP Adapter
|
|
132
129
|
|
|
@@ -227,10 +224,10 @@ bilinc rollback execute --snapshot snap_... --reason "undo bad agent run" \
|
|
|
227
224
|
Useful first-run commands:
|
|
228
225
|
|
|
229
226
|
```bash
|
|
230
|
-
bilinc
|
|
231
|
-
bilinc login --api-key bil_live_...
|
|
227
|
+
bilinc login # browser sign-in; --device without a browser
|
|
232
228
|
bilinc quicktest
|
|
233
229
|
bilinc mcp install
|
|
230
|
+
bilinc start # prints the first-run guide
|
|
234
231
|
```
|
|
235
232
|
|
|
236
233
|
## Hosted Endpoints
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
Retrieval answers *"what is similar to this?"*. Long-running agents also need to answer *"who wrote this state, was it verified, did it contradict what we already knew, and can we undo it?"* — that is the layer Bilinc provides.
|
|
16
16
|
|
|
17
|
-
Bilinc 2.
|
|
17
|
+
Bilinc 2.3.0 on PyPI is the public cloud-only package: a thin Python SDK, CLI, and MCP adapter for Bilinc Cloud. It does not ship the local StatePlane, storage backends, eval, observability, integrations, or server runtime internals.
|
|
18
18
|
|
|
19
19
|
> **Frozen regression receipt** — LongMemEval-s cleaned retrieval fixture, 500 questions: **Hit@5 98.0%**, **NDCG@5 0.913**, no LLM reranker, no paid API. This is an isolated retrieval guardrail, not a current hosted SLA, end-to-end agent score, or competitor ranking — see [Benchmark receipt](#benchmark-receipt) for the full scope and qualification.
|
|
20
20
|
|
|
@@ -60,31 +60,28 @@ storage runtime.
|
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
pip install -U bilinc
|
|
63
|
-
bilinc
|
|
63
|
+
bilinc login
|
|
64
|
+
bilinc quicktest
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
`bilinc
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
2. Confirm email.
|
|
72
|
-
3. Create one hosted API key in the Cloud dashboard.
|
|
73
|
-
4. Connect the CLI:
|
|
67
|
+
`bilinc login` opens your browser. Sign in with Google, GitHub, or email (free,
|
|
68
|
+
no card required) and approve this computer; the CLI saves its own API key with
|
|
69
|
+
owner-only permissions. The key never appears in a URL or your terminal.
|
|
70
|
+
`bilinc quicktest` then performs one hosted commit, one hosted recall, and one
|
|
71
|
+
Cloud status check.
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
bilinc
|
|
78
|
-
```
|
|
73
|
+
- **No browser on this machine** (SSH, servers, containers): `bilinc login --device`
|
|
74
|
+
prints a short code to approve from any browser.
|
|
75
|
+
- **CI and scripts:** `bilinc login --api-key bil_live_...`, or set `BILINC_API_KEY`.
|
|
79
76
|
|
|
80
77
|
To reproduce this release exactly:
|
|
81
78
|
|
|
82
79
|
```bash
|
|
83
|
-
pip install -U bilinc==2.
|
|
80
|
+
pip install -U bilinc==2.3.0
|
|
84
81
|
```
|
|
85
82
|
|
|
86
83
|
If you prefer a browser guide, open https://bilinc.space/install and follow the
|
|
87
|
-
same
|
|
84
|
+
same three-step path.
|
|
88
85
|
|
|
89
86
|
## MCP Adapter
|
|
90
87
|
|
|
@@ -185,10 +182,10 @@ bilinc rollback execute --snapshot snap_... --reason "undo bad agent run" \
|
|
|
185
182
|
Useful first-run commands:
|
|
186
183
|
|
|
187
184
|
```bash
|
|
188
|
-
bilinc
|
|
189
|
-
bilinc login --api-key bil_live_...
|
|
185
|
+
bilinc login # browser sign-in; --device without a browser
|
|
190
186
|
bilinc quicktest
|
|
191
187
|
bilinc mcp install
|
|
188
|
+
bilinc start # prints the first-run guide
|
|
192
189
|
```
|
|
193
190
|
|
|
194
191
|
## Hosted Endpoints
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Bilinc 2.
|
|
1
|
+
"""Bilinc 2.3.0: cloud-only SDK for agent memory."""
|
|
2
2
|
|
|
3
3
|
from bilinc.client import (
|
|
4
4
|
ACTIVATION_SIGNUP_URL,
|
|
@@ -25,7 +25,7 @@ from bilinc.client import (
|
|
|
25
25
|
CloudClient,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
__version__ = "2.
|
|
28
|
+
__version__ = "2.3.0"
|
|
29
29
|
version = __version__
|
|
30
30
|
|
|
31
31
|
__all__ = [
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""One-click ``bilinc login``.
|
|
2
|
+
|
|
3
|
+
The default flow opens the browser and hands a one-time code back to a
|
|
4
|
+
listener on ``127.0.0.1`` (an OAuth-style loopback redirect with PKCE, RFC 8252
|
|
5
|
+
and RFC 7636). The CLI then exchanges that code, plus the PKCE verifier only it
|
|
6
|
+
knows, for an API key it saves locally. Machines without a browser use a device
|
|
7
|
+
code instead (RFC 8628): the CLI prints a short code, the user approves it in
|
|
8
|
+
any browser, and the CLI polls until it is approved.
|
|
9
|
+
|
|
10
|
+
The API key only ever travels in the body of an HTTPS response; it never
|
|
11
|
+
appears in a URL, the browser, or a log line.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import hashlib
|
|
18
|
+
import html
|
|
19
|
+
import http.server
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import secrets
|
|
24
|
+
import socket
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
import urllib.parse
|
|
28
|
+
import webbrowser
|
|
29
|
+
from typing import Any, Callable, TextIO
|
|
30
|
+
|
|
31
|
+
from bilinc.client import BilincCloudError, _default_transport
|
|
32
|
+
|
|
33
|
+
LOOPBACK_TIMEOUT_SECONDS = 600
|
|
34
|
+
CALLBACK_PATH = "/callback"
|
|
35
|
+
_HOSTNAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LoginError(Exception):
|
|
39
|
+
"""The sign-in did not complete; the message says what to do next."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def pkce_pair() -> tuple[str, str]:
|
|
43
|
+
"""Return a PKCE (verifier, S256 challenge) pair."""
|
|
44
|
+
|
|
45
|
+
verifier = secrets.token_urlsafe(64)[:96]
|
|
46
|
+
digest = hashlib.sha256(verifier.encode("ascii")).digest()
|
|
47
|
+
challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
|
|
48
|
+
return verifier, challenge
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def machine_name() -> str:
|
|
52
|
+
"""A display name for this computer, shown on the approval page and in the key name."""
|
|
53
|
+
|
|
54
|
+
name = socket.gethostname().split(".")[0].strip()
|
|
55
|
+
return name if _HOSTNAME_PATTERN.match(name) else "bilinc-cli"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_headless() -> bool:
|
|
59
|
+
"""True when no local browser can be opened (SSH sessions, servers, containers)."""
|
|
60
|
+
|
|
61
|
+
has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
|
|
62
|
+
if os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_TTY"):
|
|
63
|
+
return not has_display
|
|
64
|
+
if sys.platform.startswith("linux"):
|
|
65
|
+
return not has_display
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _post(
|
|
70
|
+
transport: Callable[..., dict[str, Any]],
|
|
71
|
+
base_url: str,
|
|
72
|
+
path: str,
|
|
73
|
+
payload: dict[str, Any],
|
|
74
|
+
timeout: float,
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
from bilinc import __version__
|
|
77
|
+
|
|
78
|
+
return transport(
|
|
79
|
+
"POST",
|
|
80
|
+
f"{base_url.rstrip('/')}{path}",
|
|
81
|
+
headers={
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
"Accept": "application/json",
|
|
84
|
+
"User-Agent": f"bilinc-cli/{__version__}",
|
|
85
|
+
},
|
|
86
|
+
body=json.dumps(payload).encode("utf-8"),
|
|
87
|
+
timeout=timeout,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_DONE_PAGE = """<!doctype html><html><head><meta charset="utf-8"><title>Bilinc CLI</title>
|
|
92
|
+
<style>body{{background:#02040f;color:#e0f0ff;font:16px/1.5 ui-monospace,Menlo,monospace;
|
|
93
|
+
display:grid;place-items:center;min-height:100vh;margin:0}}main{{max-width:34rem;padding:2rem}}
|
|
94
|
+
h1{{color:#00f0ff;font-size:1.25rem}}</style></head><body><main><h1>{title}</h1><p>{body}</p></main></body></html>"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _CallbackServer(http.server.HTTPServer):
|
|
98
|
+
result: dict[str, str] | None = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class _CallbackHandler(http.server.BaseHTTPRequestHandler):
|
|
102
|
+
server: _CallbackServer
|
|
103
|
+
|
|
104
|
+
def do_GET(self) -> None: # noqa: N802 - http.server API
|
|
105
|
+
parsed = urllib.parse.urlsplit(self.path)
|
|
106
|
+
if parsed.path != CALLBACK_PATH:
|
|
107
|
+
self.send_error(404)
|
|
108
|
+
return
|
|
109
|
+
params = {key: values[0] for key, values in urllib.parse.parse_qs(parsed.query).items() if values}
|
|
110
|
+
if self.server.result is None:
|
|
111
|
+
self.server.result = params
|
|
112
|
+
if params.get("code"):
|
|
113
|
+
title, body = "Bilinc CLI is signed in", "You can close this tab and return to your terminal."
|
|
114
|
+
else:
|
|
115
|
+
title, body = "Sign-in was not completed", "Return to your terminal for details."
|
|
116
|
+
page = _DONE_PAGE.format(title=html.escape(title), body=html.escape(body)).encode("utf-8")
|
|
117
|
+
self.send_response(200)
|
|
118
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
119
|
+
self.send_header("Content-Length", str(len(page)))
|
|
120
|
+
self.send_header("Cache-Control", "no-store")
|
|
121
|
+
self.send_header("Referrer-Policy", "no-referrer")
|
|
122
|
+
self.end_headers()
|
|
123
|
+
self.wfile.write(page)
|
|
124
|
+
|
|
125
|
+
def log_message(self, format: str, *args: Any) -> None: # noqa: A002 - http.server API
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def loopback_login(
|
|
130
|
+
base_url: str,
|
|
131
|
+
*,
|
|
132
|
+
timeout: float = 30.0,
|
|
133
|
+
open_browser: bool = True,
|
|
134
|
+
transport: Callable[..., dict[str, Any]] = _default_transport,
|
|
135
|
+
browser_open: Callable[[str], bool] = webbrowser.open,
|
|
136
|
+
stderr: TextIO = sys.stderr,
|
|
137
|
+
wait_seconds: float = LOOPBACK_TIMEOUT_SECONDS,
|
|
138
|
+
on_listening: Callable[[str], None] | None = None,
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
"""Sign in through the browser and return the token response (``api_key``, ``key_name``)."""
|
|
141
|
+
|
|
142
|
+
verifier, challenge = pkce_pair()
|
|
143
|
+
state = secrets.token_urlsafe(24)
|
|
144
|
+
server = _CallbackServer(("127.0.0.1", 0), _CallbackHandler)
|
|
145
|
+
server.timeout = 0.5
|
|
146
|
+
try:
|
|
147
|
+
port = server.server_address[1]
|
|
148
|
+
query = urllib.parse.urlencode(
|
|
149
|
+
{
|
|
150
|
+
"port": port,
|
|
151
|
+
"state": state,
|
|
152
|
+
"code_challenge": challenge,
|
|
153
|
+
"code_challenge_method": "S256",
|
|
154
|
+
"hostname": machine_name(),
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
url = f"{base_url.rstrip('/')}/cli/authorize?{query}"
|
|
158
|
+
opened = False
|
|
159
|
+
if open_browser:
|
|
160
|
+
print("Opening your browser to sign in to Bilinc Cloud...", file=stderr)
|
|
161
|
+
try:
|
|
162
|
+
opened = bool(browser_open(url))
|
|
163
|
+
except Exception: # noqa: BLE001 - any browser failure falls back to the printed URL
|
|
164
|
+
opened = False
|
|
165
|
+
if not opened:
|
|
166
|
+
print(f"Open this URL in a browser on this computer to continue:\n\n {url}\n", file=stderr)
|
|
167
|
+
if on_listening is not None:
|
|
168
|
+
on_listening(url)
|
|
169
|
+
|
|
170
|
+
deadline = time.monotonic() + wait_seconds
|
|
171
|
+
while server.result is None and time.monotonic() < deadline:
|
|
172
|
+
server.handle_request()
|
|
173
|
+
result = server.result
|
|
174
|
+
finally:
|
|
175
|
+
server.server_close()
|
|
176
|
+
|
|
177
|
+
if result is None:
|
|
178
|
+
raise LoginError("Timed out waiting for the browser. Run bilinc login again, or bilinc login --device.")
|
|
179
|
+
if not secrets.compare_digest(result.get("state", ""), state):
|
|
180
|
+
raise LoginError("The browser returned an unexpected sign-in response. Run bilinc login again.")
|
|
181
|
+
if result.get("error"):
|
|
182
|
+
raise LoginError("Sign-in was denied in the browser.")
|
|
183
|
+
code = result.get("code")
|
|
184
|
+
if not code:
|
|
185
|
+
raise LoginError("The browser did not return a sign-in code. Run bilinc login again.")
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
return _post(transport, base_url, "/api/cli/token", {"code": code, "code_verifier": verifier}, timeout)
|
|
189
|
+
except BilincCloudError as exc:
|
|
190
|
+
raise LoginError(_explain(exc)) from exc
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def device_login(
|
|
194
|
+
base_url: str,
|
|
195
|
+
*,
|
|
196
|
+
timeout: float = 30.0,
|
|
197
|
+
transport: Callable[..., dict[str, Any]] = _default_transport,
|
|
198
|
+
stderr: TextIO = sys.stderr,
|
|
199
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
200
|
+
clock: Callable[[], float] = time.monotonic,
|
|
201
|
+
) -> dict[str, Any]:
|
|
202
|
+
"""Sign in with a code approved in any browser and return the token response."""
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
start = _post(transport, base_url, "/api/cli/device/code", {"hostname": machine_name()}, timeout)
|
|
206
|
+
except BilincCloudError as exc:
|
|
207
|
+
raise LoginError(_explain(exc)) from exc
|
|
208
|
+
|
|
209
|
+
print(
|
|
210
|
+
"To sign in, open this page in any browser:\n\n"
|
|
211
|
+
f" {start['verification_uri_complete']}\n\n"
|
|
212
|
+
f"and confirm the code {start['user_code']}. Waiting for approval...",
|
|
213
|
+
file=stderr,
|
|
214
|
+
)
|
|
215
|
+
interval = max(1, int(start.get("interval", 5)))
|
|
216
|
+
deadline = clock() + int(start.get("expires_in", 600))
|
|
217
|
+
while clock() < deadline:
|
|
218
|
+
sleep(interval)
|
|
219
|
+
try:
|
|
220
|
+
return _post(transport, base_url, "/api/cli/device/token", {"device_code": start["device_code"]}, timeout)
|
|
221
|
+
except BilincCloudError as exc:
|
|
222
|
+
if exc.code == "authorization_pending":
|
|
223
|
+
continue
|
|
224
|
+
if exc.code == "slow_down":
|
|
225
|
+
interval += 5
|
|
226
|
+
continue
|
|
227
|
+
raise LoginError(_explain(exc)) from exc
|
|
228
|
+
raise LoginError("The code expired before it was approved. Run bilinc login --device again.")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _explain(exc: BilincCloudError) -> str:
|
|
232
|
+
return {
|
|
233
|
+
"access_denied": "Sign-in was denied in the browser.",
|
|
234
|
+
"expired_token": "The code expired before it was approved. Run bilinc login again.",
|
|
235
|
+
"invalid_grant": "The sign-in code was already used or has expired. Run bilinc login again.",
|
|
236
|
+
"api_key_limit_reached": (
|
|
237
|
+
"Your plan has no free API key slot. Revoke an unused key at https://bilinc.space/api-keys, "
|
|
238
|
+
"then run bilinc login again."
|
|
239
|
+
),
|
|
240
|
+
"rate_limited": "Too many sign-in attempts. Wait a few minutes and run bilinc login again.",
|
|
241
|
+
}.get(exc.code or "", f"Sign-in failed: {exc}")
|
|
@@ -23,6 +23,7 @@ from bilinc.client import (
|
|
|
23
23
|
load_config_api_key,
|
|
24
24
|
save_config_api_key,
|
|
25
25
|
)
|
|
26
|
+
from bilinc.cli.login import LoginError, device_login, is_headless, loopback_login
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def _parse_value(value: str) -> Any:
|
|
@@ -47,14 +48,15 @@ def _has_key(args: argparse.Namespace) -> bool:
|
|
|
47
48
|
def _print_start_guide(*, opened: bool = False) -> None:
|
|
48
49
|
_print(
|
|
49
50
|
{
|
|
50
|
-
"next": "
|
|
51
|
-
"goal": "
|
|
51
|
+
"next": "Connect this computer to Bilinc Cloud",
|
|
52
|
+
"goal": "Sign in once from the terminal and finish with bilinc quicktest.",
|
|
52
53
|
"steps": [
|
|
53
|
-
"1.
|
|
54
|
-
"2.
|
|
55
|
-
"3. Run: bilinc login --api-key <key>",
|
|
56
|
-
"4. Run: bilinc quicktest",
|
|
54
|
+
"1. Run: bilinc login (opens your browser; sign in with Google, GitHub, or email)",
|
|
55
|
+
"2. Run: bilinc quicktest",
|
|
57
56
|
],
|
|
57
|
+
"no_browser_on_this_machine": "bilinc login --device",
|
|
58
|
+
"ci": "bilinc login --api-key <key>, or set BILINC_API_KEY",
|
|
59
|
+
"pricing": "Free, no card required",
|
|
58
60
|
"signup": ACTIVATION_SIGNUP_URL,
|
|
59
61
|
"install_guide": INSTALL_URL,
|
|
60
62
|
"opened_browser": opened,
|
|
@@ -89,9 +91,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
89
91
|
start = sub.add_parser("start", help="Open the simplest path from install to first memory")
|
|
90
92
|
start.add_argument("--open", action="store_true", help="Open the Bilinc signup page in a browser")
|
|
91
93
|
|
|
92
|
-
login = sub.add_parser(
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
login = sub.add_parser(
|
|
95
|
+
"login",
|
|
96
|
+
help="Sign in from your browser and save an API key for this computer",
|
|
97
|
+
)
|
|
98
|
+
login.add_argument("--api-key", help="Save this API key instead of signing in (for CI)")
|
|
99
|
+
login.add_argument(
|
|
100
|
+
"--device",
|
|
101
|
+
action="store_true",
|
|
102
|
+
help="Sign in with a code approved in any browser (for SSH sessions and servers)",
|
|
103
|
+
)
|
|
104
|
+
login.add_argument(
|
|
105
|
+
"--no-browser",
|
|
106
|
+
action="store_true",
|
|
107
|
+
help="Print the sign-in URL instead of opening a browser",
|
|
108
|
+
)
|
|
109
|
+
login.add_argument("--open", action="store_true", help=argparse.SUPPRESS)
|
|
95
110
|
|
|
96
111
|
commit = sub.add_parser("commit", help="Commit a memory entry to Bilinc Cloud")
|
|
97
112
|
commit.add_argument("--key", required=True)
|
|
@@ -161,7 +176,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
161
176
|
mcp_sub = mcp.add_subparsers(dest="mcp_command")
|
|
162
177
|
mcp_sub.add_parser("install", help="Print MCP config for agent runtimes")
|
|
163
178
|
|
|
164
|
-
sub.add_parser("signup", help="Print signup URL
|
|
179
|
+
sub.add_parser("signup", help="Print the signup URL (free, no card required)")
|
|
165
180
|
|
|
166
181
|
return parser
|
|
167
182
|
|
|
@@ -193,18 +208,37 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
193
208
|
return 0
|
|
194
209
|
|
|
195
210
|
if args.command == "login":
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
if args.api_key:
|
|
212
|
+
try:
|
|
213
|
+
path = save_config_api_key(args.api_key, base_url=args.base_url)
|
|
214
|
+
except BilincApiKeyRequired as exc:
|
|
215
|
+
print(f"bilinc: error: {exc}", file=sys.stderr)
|
|
216
|
+
return 1
|
|
217
|
+
_print({"saved": True, "config": str(path), "next": "Run bilinc quicktest"})
|
|
201
218
|
return 0
|
|
202
219
|
try:
|
|
203
|
-
|
|
204
|
-
|
|
220
|
+
if args.device or is_headless():
|
|
221
|
+
result = device_login(args.base_url, timeout=args.timeout)
|
|
222
|
+
else:
|
|
223
|
+
result = loopback_login(args.base_url, timeout=args.timeout, open_browser=not args.no_browser)
|
|
224
|
+
api_key = result.get("api_key")
|
|
225
|
+
if not isinstance(api_key, str) or not api_key:
|
|
226
|
+
raise LoginError("Bilinc Cloud did not return an API key. Run bilinc login again.")
|
|
227
|
+
path = save_config_api_key(api_key, base_url=args.base_url)
|
|
228
|
+
except (LoginError, BilincApiKeyRequired, BilincCloudError) as exc:
|
|
205
229
|
print(f"bilinc: error: {exc}", file=sys.stderr)
|
|
206
230
|
return 1
|
|
207
|
-
|
|
231
|
+
except KeyboardInterrupt:
|
|
232
|
+
print("bilinc: login cancelled", file=sys.stderr)
|
|
233
|
+
return 130
|
|
234
|
+
_print(
|
|
235
|
+
{
|
|
236
|
+
"saved": True,
|
|
237
|
+
"config": str(path),
|
|
238
|
+
"api_key_name": result.get("key_name"),
|
|
239
|
+
"next": "Run bilinc quicktest",
|
|
240
|
+
}
|
|
241
|
+
)
|
|
208
242
|
return 0
|
|
209
243
|
|
|
210
244
|
if args.command == "signup":
|
|
@@ -212,8 +246,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
212
246
|
{
|
|
213
247
|
"signup": ACTIVATION_SIGNUP_URL,
|
|
214
248
|
"base_signup": SIGNUP_URL,
|
|
215
|
-
"
|
|
216
|
-
"next": "
|
|
249
|
+
"pricing": "Free, no card required",
|
|
250
|
+
"next": "Run bilinc login to sign in from your browser, then bilinc quicktest",
|
|
217
251
|
}
|
|
218
252
|
)
|
|
219
253
|
return 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Bilinc Cloud client.
|
|
2
2
|
|
|
3
|
-
Bilinc 2.
|
|
3
|
+
Bilinc 2.3.0 is cloud-only: the PyPI package is a thin SDK and MCP adapter for
|
|
4
4
|
https://bilinc.space. Local self-hosted StatePlane internals are no longer
|
|
5
5
|
shipped in the public package.
|
|
6
6
|
"""
|
|
@@ -18,10 +18,10 @@ from dataclasses import dataclass
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Any, Callable
|
|
20
20
|
|
|
21
|
-
__version__ = "2.
|
|
21
|
+
__version__ = "2.3.0"
|
|
22
22
|
DEFAULT_BASE_URL = "https://bilinc.space"
|
|
23
23
|
SIGNUP_URL = "https://bilinc.space/signup"
|
|
24
|
-
ACTIVATION_CAMPAIGN = "
|
|
24
|
+
ACTIVATION_CAMPAIGN = "activation_2_3_0"
|
|
25
25
|
ACTIVATION_SIGNUP_URL = (
|
|
26
26
|
f"{SIGNUP_URL}?utm_source=pypi&utm_medium=cli&utm_campaign={ACTIVATION_CAMPAIGN}"
|
|
27
27
|
)
|
|
@@ -381,15 +381,20 @@ def save_config_api_key(api_key: str, *, base_url: str = DEFAULT_BASE_URL) -> Pa
|
|
|
381
381
|
key = api_key.strip()
|
|
382
382
|
if not key:
|
|
383
383
|
raise BilincApiKeyRequired(
|
|
384
|
-
"
|
|
385
|
-
|
|
386
|
-
"
|
|
384
|
+
"No Bilinc Cloud API key is configured. "
|
|
385
|
+
"Run bilinc login to sign in from your browser (free, no card required; "
|
|
386
|
+
f"new here? {ACTIVATION_SIGNUP_URL}), "
|
|
387
|
+
"or set BILINC_API_KEY. Then run bilinc quicktest."
|
|
387
388
|
)
|
|
388
389
|
|
|
389
390
|
path = config_path()
|
|
390
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
391
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
391
392
|
payload = {"api_key": key, "base_url": base_url.rstrip("/") or DEFAULT_BASE_URL}
|
|
392
|
-
|
|
393
|
+
# Create the file owner-only from the start, so the key is never briefly
|
|
394
|
+
# readable by other users between the write and a later chmod.
|
|
395
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
396
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
397
|
+
handle.write(json.dumps(payload, indent=2))
|
|
393
398
|
try:
|
|
394
399
|
path.chmod(0o600)
|
|
395
400
|
except OSError:
|
|
@@ -452,9 +457,10 @@ class CloudClient:
|
|
|
452
457
|
self.api_key = os.environ.get("BILINC_API_KEY") or load_config_api_key()
|
|
453
458
|
if not self.api_key:
|
|
454
459
|
raise BilincApiKeyRequired(
|
|
455
|
-
"
|
|
456
|
-
|
|
457
|
-
"
|
|
460
|
+
"No Bilinc Cloud API key is configured. "
|
|
461
|
+
"Run bilinc login to sign in from your browser (free, no card required; "
|
|
462
|
+
f"new here? {ACTIVATION_SIGNUP_URL}), "
|
|
463
|
+
"or set BILINC_API_KEY. Then run bilinc quicktest."
|
|
458
464
|
)
|
|
459
465
|
if self.base_url == DEFAULT_BASE_URL:
|
|
460
466
|
self.base_url = os.environ.get("BILINC_BASE_URL", self.base_url)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bilinc
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Cloud-only memory SDK and MCP adapter for autonomous agents
|
|
5
5
|
Author: Bilinc Team
|
|
6
6
|
License-Expression: BUSL-1.1
|
|
@@ -56,7 +56,7 @@ Dynamic: license-file
|
|
|
56
56
|
|
|
57
57
|
Retrieval answers *"what is similar to this?"*. Long-running agents also need to answer *"who wrote this state, was it verified, did it contradict what we already knew, and can we undo it?"* — that is the layer Bilinc provides.
|
|
58
58
|
|
|
59
|
-
Bilinc 2.
|
|
59
|
+
Bilinc 2.3.0 on PyPI is the public cloud-only package: a thin Python SDK, CLI, and MCP adapter for Bilinc Cloud. It does not ship the local StatePlane, storage backends, eval, observability, integrations, or server runtime internals.
|
|
60
60
|
|
|
61
61
|
> **Frozen regression receipt** — LongMemEval-s cleaned retrieval fixture, 500 questions: **Hit@5 98.0%**, **NDCG@5 0.913**, no LLM reranker, no paid API. This is an isolated retrieval guardrail, not a current hosted SLA, end-to-end agent score, or competitor ranking — see [Benchmark receipt](#benchmark-receipt) for the full scope and qualification.
|
|
62
62
|
|
|
@@ -102,31 +102,28 @@ storage runtime.
|
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
104
|
pip install -U bilinc
|
|
105
|
-
bilinc
|
|
105
|
+
bilinc login
|
|
106
|
+
bilinc quicktest
|
|
106
107
|
```
|
|
107
108
|
|
|
108
|
-
`bilinc
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
2. Confirm email.
|
|
114
|
-
3. Create one hosted API key in the Cloud dashboard.
|
|
115
|
-
4. Connect the CLI:
|
|
109
|
+
`bilinc login` opens your browser. Sign in with Google, GitHub, or email (free,
|
|
110
|
+
no card required) and approve this computer; the CLI saves its own API key with
|
|
111
|
+
owner-only permissions. The key never appears in a URL or your terminal.
|
|
112
|
+
`bilinc quicktest` then performs one hosted commit, one hosted recall, and one
|
|
113
|
+
Cloud status check.
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
bilinc
|
|
120
|
-
```
|
|
115
|
+
- **No browser on this machine** (SSH, servers, containers): `bilinc login --device`
|
|
116
|
+
prints a short code to approve from any browser.
|
|
117
|
+
- **CI and scripts:** `bilinc login --api-key bil_live_...`, or set `BILINC_API_KEY`.
|
|
121
118
|
|
|
122
119
|
To reproduce this release exactly:
|
|
123
120
|
|
|
124
121
|
```bash
|
|
125
|
-
pip install -U bilinc==2.
|
|
122
|
+
pip install -U bilinc==2.3.0
|
|
126
123
|
```
|
|
127
124
|
|
|
128
125
|
If you prefer a browser guide, open https://bilinc.space/install and follow the
|
|
129
|
-
same
|
|
126
|
+
same three-step path.
|
|
130
127
|
|
|
131
128
|
## MCP Adapter
|
|
132
129
|
|
|
@@ -227,10 +224,10 @@ bilinc rollback execute --snapshot snap_... --reason "undo bad agent run" \
|
|
|
227
224
|
Useful first-run commands:
|
|
228
225
|
|
|
229
226
|
```bash
|
|
230
|
-
bilinc
|
|
231
|
-
bilinc login --api-key bil_live_...
|
|
227
|
+
bilinc login # browser sign-in; --device without a browser
|
|
232
228
|
bilinc quicktest
|
|
233
229
|
bilinc mcp install
|
|
230
|
+
bilinc start # prints the first-run guide
|
|
234
231
|
```
|
|
235
232
|
|
|
236
233
|
## Hosted Endpoints
|
|
@@ -12,9 +12,9 @@ import pytest
|
|
|
12
12
|
def test_public_api_is_cloud_only():
|
|
13
13
|
import bilinc
|
|
14
14
|
|
|
15
|
-
assert bilinc.__version__ == "2.
|
|
16
|
-
assert bilinc.version == "2.
|
|
17
|
-
assert "utm_campaign=
|
|
15
|
+
assert bilinc.__version__ == "2.3.0"
|
|
16
|
+
assert bilinc.version == "2.3.0"
|
|
17
|
+
assert "utm_campaign=activation_2_3_0" in bilinc.ACTIVATION_SIGNUP_URL
|
|
18
18
|
assert hasattr(bilinc, "Bilinc")
|
|
19
19
|
assert hasattr(bilinc, "CloudClient")
|
|
20
20
|
assert not hasattr(bilinc, "StatePlane")
|
|
@@ -110,9 +110,9 @@ def test_cloud_client_commit_posts_to_hosted_api():
|
|
|
110
110
|
"headers": {
|
|
111
111
|
"Authorization": "Bearer bil_live_test",
|
|
112
112
|
"Content-Type": "application/json",
|
|
113
|
-
"User-Agent": "bilinc-python/2.
|
|
113
|
+
"User-Agent": "bilinc-python/2.3.0",
|
|
114
114
|
"X-Bilinc-Client": "python-sdk",
|
|
115
|
-
"X-Bilinc-Client-Version": "2.
|
|
115
|
+
"X-Bilinc-Client-Version": "2.3.0",
|
|
116
116
|
},
|
|
117
117
|
"body": json.dumps(
|
|
118
118
|
{
|
|
@@ -188,7 +188,7 @@ def test_cloud_client_status_reads_the_authenticated_status_endpoint():
|
|
|
188
188
|
"url": "https://bilinc.space/api/cloud/status",
|
|
189
189
|
"headers": {
|
|
190
190
|
"Authorization": "Bearer bil_live_test",
|
|
191
|
-
"User-Agent": "bilinc-python/2.
|
|
191
|
+
"User-Agent": "bilinc-python/2.3.0",
|
|
192
192
|
},
|
|
193
193
|
"body": None,
|
|
194
194
|
"timeout": 30.0,
|
|
@@ -261,7 +261,7 @@ def test_cli_signup_and_missing_key_failure(monkeypatch, capsys):
|
|
|
261
261
|
assert main(["signup"]) == 0
|
|
262
262
|
signup_out = capsys.readouterr()
|
|
263
263
|
assert "https://bilinc.space/signup" in signup_out.out
|
|
264
|
-
assert "
|
|
264
|
+
assert "activation_2_3_0" in signup_out.out
|
|
265
265
|
assert "quicktest" in signup_out.out
|
|
266
266
|
|
|
267
267
|
assert main(["commit", "--key", "smoke_key", "--value", "hello"]) == 1
|
|
@@ -305,7 +305,7 @@ def test_cli_start_login_doctor_quicktest_and_mcp_config(monkeypatch, capsys, tm
|
|
|
305
305
|
assert "bilinc login --api-key <key>" in start_out.out
|
|
306
306
|
assert "bilinc quicktest" in start_out.out
|
|
307
307
|
assert "install_guide" in start_out.out
|
|
308
|
-
assert "
|
|
308
|
+
assert "activation_2_3_0" in start_out.out
|
|
309
309
|
|
|
310
310
|
assert cli_main.main(["login", "--api-key", "bil_live_cli_test"]) == 0
|
|
311
311
|
login_out = capsys.readouterr()
|
|
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
|