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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bilinc
3
- Version: 2.2.0
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.2.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.
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 start
105
+ bilinc login
106
+ bilinc quicktest
106
107
  ```
107
108
 
108
- `bilinc start` is the first-run guide. The activation target is simple: reach a
109
- passing `bilinc quicktest`, which performs one hosted commit, one hosted recall,
110
- and one Cloud status check.
111
-
112
- 1. Start the 7-day Bilinc Cloud trial at https://bilinc.space/signup.
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
- ```bash
118
- bilinc login --api-key bil_live_...
119
- bilinc quicktest
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.2.0
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 four-step path.
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 start
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.2.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.
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 start
63
+ bilinc login
64
+ bilinc quicktest
64
65
  ```
65
66
 
66
- `bilinc start` is the first-run guide. The activation target is simple: reach a
67
- passing `bilinc quicktest`, which performs one hosted commit, one hosted recall,
68
- and one Cloud status check.
69
-
70
- 1. Start the 7-day Bilinc Cloud trial at https://bilinc.space/signup.
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
- ```bash
76
- bilinc login --api-key bil_live_...
77
- bilinc quicktest
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.2.0
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 four-step path.
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 start
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bilinc"
7
- version = "2.2.0"
7
+ version = "2.3.0"
8
8
  description = "Cloud-only memory SDK and MCP adapter for autonomous agents"
9
9
  readme = "README.md"
10
10
  license = "BUSL-1.1"
@@ -1,4 +1,4 @@
1
- """Bilinc 2.2.0: cloud-only SDK for agent memory."""
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.2.0"
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": "Run the 60-second Bilinc Cloud activation path",
51
- "goal": "Connect one hosted API key and finish with bilinc quicktest.",
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. Start the 7-day Cloud trial and confirm email",
54
- "2. After confirmation, copy the first hosted API key from onboarding",
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("login", help="Save a Bilinc Cloud API key for local CLI use")
93
- login.add_argument("--api-key", help="Bilinc Cloud API key to save locally")
94
- login.add_argument("--open", action="store_true", help="Open the Bilinc signup page in a browser")
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 for a 7-day trial")
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
- opened = False
197
- if args.open:
198
- opened = webbrowser.open(ACTIVATION_SIGNUP_URL)
199
- if not args.api_key:
200
- _print_start_guide(opened=opened)
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
- path = save_config_api_key(args.api_key, base_url=args.base_url)
204
- except BilincApiKeyRequired as exc:
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
- _print({"saved": True, "config": str(path), "next": "Run bilinc quicktest"})
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
- "trial": "7 days",
216
- "next": "Confirm email, copy the first API key, run bilinc login, then bilinc quicktest",
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.2.0 is cloud-only: the PyPI package is a thin SDK and MCP adapter for
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.2.0"
21
+ __version__ = "2.3.0"
22
22
  DEFAULT_BASE_URL = "https://bilinc.space"
23
23
  SIGNUP_URL = "https://bilinc.space/signup"
24
- ACTIVATION_CAMPAIGN = "activation_2_2_0"
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
- "Bilinc requires a Bilinc Cloud API key. "
385
- f"Start a 7-day trial at {ACTIVATION_SIGNUP_URL}, "
386
- "then run bilinc login --api-key <key> and bilinc quicktest."
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
- path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
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
- "Bilinc requires a Bilinc Cloud API key. "
456
- f"Start a 7-day trial at {ACTIVATION_SIGNUP_URL}, "
457
- "then run bilinc login --api-key <key> and bilinc quicktest."
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.2.0
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.2.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.
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 start
105
+ bilinc login
106
+ bilinc quicktest
106
107
  ```
107
108
 
108
- `bilinc start` is the first-run guide. The activation target is simple: reach a
109
- passing `bilinc quicktest`, which performs one hosted commit, one hosted recall,
110
- and one Cloud status check.
111
-
112
- 1. Start the 7-day Bilinc Cloud trial at https://bilinc.space/signup.
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
- ```bash
118
- bilinc login --api-key bil_live_...
119
- bilinc quicktest
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.2.0
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 four-step path.
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 start
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,5 +14,6 @@ src/bilinc.egg-info/requires.txt
14
14
  src/bilinc.egg-info/top_level.txt
15
15
  src/bilinc/cli/__init__.py
16
16
  src/bilinc/cli/__main__.py
17
+ src/bilinc/cli/login.py
17
18
  src/bilinc/cli/main.py
18
19
  tests/test_cloud_only_package.py
@@ -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.2.0"
16
- assert bilinc.version == "2.2.0"
17
- assert "utm_campaign=activation_2_2_0" in bilinc.ACTIVATION_SIGNUP_URL
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.2.0",
113
+ "User-Agent": "bilinc-python/2.3.0",
114
114
  "X-Bilinc-Client": "python-sdk",
115
- "X-Bilinc-Client-Version": "2.2.0",
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.2.0",
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 "activation_2_2_0" in signup_out.out
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 "activation_2_2_0" in start_out.out
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