bilinc 2.2.0__tar.gz → 2.3.1__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.1}/PKG-INFO +29 -22
- {bilinc-2.2.0 → bilinc-2.3.1}/README.md +28 -21
- {bilinc-2.2.0 → bilinc-2.3.1}/pyproject.toml +1 -1
- {bilinc-2.2.0 → bilinc-2.3.1}/src/bilinc/__init__.py +2 -2
- bilinc-2.3.1/src/bilinc/cli/login.py +241 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/src/bilinc/cli/main.py +111 -31
- {bilinc-2.2.0 → bilinc-2.3.1}/src/bilinc/client.py +17 -11
- {bilinc-2.2.0 → bilinc-2.3.1/src/bilinc.egg-info}/PKG-INFO +29 -22
- {bilinc-2.2.0 → bilinc-2.3.1}/src/bilinc.egg-info/SOURCES.txt +1 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/tests/test_cloud_only_package.py +58 -8
- {bilinc-2.2.0 → bilinc-2.3.1}/LICENSE +0 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/MANIFEST.in +0 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/setup.cfg +0 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/setup.py +0 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/src/bilinc/cli/__init__.py +0 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/src/bilinc/cli/__main__.py +0 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/src/bilinc/cloud_mcp.py +0 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/src/bilinc.egg-info/dependency_links.txt +0 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/src/bilinc.egg-info/entry_points.txt +0 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/src/bilinc.egg-info/requires.txt +0 -0
- {bilinc-2.2.0 → bilinc-2.3.1}/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.1
|
|
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.1 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.1
|
|
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
|
|
|
@@ -134,18 +131,28 @@ Bilinc exposes a standard Model Context Protocol server over **stdio**, so any
|
|
|
134
131
|
MCP-compatible client can connect — Claude Code, Codex, Cursor, Hermes-Agent,
|
|
135
132
|
opencode, and others.
|
|
136
133
|
|
|
134
|
+
After `bilinc login`, let the CLI write the setup for your client:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
bilinc mcp install --client claude-code # prints a ready `claude mcp add ...` command
|
|
138
|
+
bilinc mcp install --client claude-desktop # prints the config and where Claude Desktop keeps it
|
|
139
|
+
bilinc mcp install # plain mcpServers JSON for any other client
|
|
140
|
+
```
|
|
141
|
+
|
|
137
142
|
```json
|
|
138
143
|
{
|
|
139
144
|
"mcpServers": {
|
|
140
145
|
"bilinc": {
|
|
141
|
-
"command": "
|
|
142
|
-
"args": ["-m", "bilinc.cloud_mcp"]
|
|
143
|
-
"env": { "BILINC_API_KEY": "bil_live_..." }
|
|
146
|
+
"command": "/path/to/python3",
|
|
147
|
+
"args": ["-m", "bilinc.cloud_mcp"]
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
```
|
|
148
152
|
|
|
153
|
+
`command` is the interpreter that has Bilinc installed, and the adapter reads the key saved by `bilinc login`, so
|
|
154
|
+
the config carries no key. On a machine without a saved key, add `"env": { "BILINC_API_KEY": "bil_live_..." }`.
|
|
155
|
+
|
|
149
156
|
Eight tools — the core memory lifecycle, and nothing else:
|
|
150
157
|
|
|
151
158
|
| Tool | What it does |
|
|
@@ -227,10 +234,10 @@ bilinc rollback execute --snapshot snap_... --reason "undo bad agent run" \
|
|
|
227
234
|
Useful first-run commands:
|
|
228
235
|
|
|
229
236
|
```bash
|
|
230
|
-
bilinc
|
|
231
|
-
bilinc login --api-key bil_live_...
|
|
237
|
+
bilinc login # browser sign-in; --device without a browser
|
|
232
238
|
bilinc quicktest
|
|
233
239
|
bilinc mcp install
|
|
240
|
+
bilinc start # prints the first-run guide
|
|
234
241
|
```
|
|
235
242
|
|
|
236
243
|
## 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.1 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.1
|
|
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
|
|
|
@@ -92,18 +89,28 @@ Bilinc exposes a standard Model Context Protocol server over **stdio**, so any
|
|
|
92
89
|
MCP-compatible client can connect — Claude Code, Codex, Cursor, Hermes-Agent,
|
|
93
90
|
opencode, and others.
|
|
94
91
|
|
|
92
|
+
After `bilinc login`, let the CLI write the setup for your client:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
bilinc mcp install --client claude-code # prints a ready `claude mcp add ...` command
|
|
96
|
+
bilinc mcp install --client claude-desktop # prints the config and where Claude Desktop keeps it
|
|
97
|
+
bilinc mcp install # plain mcpServers JSON for any other client
|
|
98
|
+
```
|
|
99
|
+
|
|
95
100
|
```json
|
|
96
101
|
{
|
|
97
102
|
"mcpServers": {
|
|
98
103
|
"bilinc": {
|
|
99
|
-
"command": "
|
|
100
|
-
"args": ["-m", "bilinc.cloud_mcp"]
|
|
101
|
-
"env": { "BILINC_API_KEY": "bil_live_..." }
|
|
104
|
+
"command": "/path/to/python3",
|
|
105
|
+
"args": ["-m", "bilinc.cloud_mcp"]
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
```
|
|
106
110
|
|
|
111
|
+
`command` is the interpreter that has Bilinc installed, and the adapter reads the key saved by `bilinc login`, so
|
|
112
|
+
the config carries no key. On a machine without a saved key, add `"env": { "BILINC_API_KEY": "bil_live_..." }`.
|
|
113
|
+
|
|
107
114
|
Eight tools — the core memory lifecycle, and nothing else:
|
|
108
115
|
|
|
109
116
|
| Tool | What it does |
|
|
@@ -185,10 +192,10 @@ bilinc rollback execute --snapshot snap_... --reason "undo bad agent run" \
|
|
|
185
192
|
Useful first-run commands:
|
|
186
193
|
|
|
187
194
|
```bash
|
|
188
|
-
bilinc
|
|
189
|
-
bilinc login --api-key bil_live_...
|
|
195
|
+
bilinc login # browser sign-in; --device without a browser
|
|
190
196
|
bilinc quicktest
|
|
191
197
|
bilinc mcp install
|
|
198
|
+
bilinc start # prints the first-run guide
|
|
192
199
|
```
|
|
193
200
|
|
|
194
201
|
## Hosted Endpoints
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Bilinc 2.
|
|
1
|
+
"""Bilinc 2.3.1: 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.1"
|
|
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}")
|
|
@@ -6,6 +6,8 @@ from __future__ import annotations
|
|
|
6
6
|
import argparse
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
import shlex
|
|
10
|
+
import subprocess
|
|
9
11
|
import sys
|
|
10
12
|
import time
|
|
11
13
|
import webbrowser
|
|
@@ -23,6 +25,7 @@ from bilinc.client import (
|
|
|
23
25
|
load_config_api_key,
|
|
24
26
|
save_config_api_key,
|
|
25
27
|
)
|
|
28
|
+
from bilinc.cli.login import LoginError, device_login, is_headless, loopback_login
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
def _parse_value(value: str) -> Any:
|
|
@@ -47,14 +50,15 @@ def _has_key(args: argparse.Namespace) -> bool:
|
|
|
47
50
|
def _print_start_guide(*, opened: bool = False) -> None:
|
|
48
51
|
_print(
|
|
49
52
|
{
|
|
50
|
-
"next": "
|
|
51
|
-
"goal": "
|
|
53
|
+
"next": "Connect this computer to Bilinc Cloud",
|
|
54
|
+
"goal": "Sign in once from the terminal and finish with bilinc quicktest.",
|
|
52
55
|
"steps": [
|
|
53
|
-
"1.
|
|
54
|
-
"2.
|
|
55
|
-
"3. Run: bilinc login --api-key <key>",
|
|
56
|
-
"4. Run: bilinc quicktest",
|
|
56
|
+
"1. Run: bilinc login (opens your browser; sign in with Google, GitHub, or email)",
|
|
57
|
+
"2. Run: bilinc quicktest",
|
|
57
58
|
],
|
|
59
|
+
"no_browser_on_this_machine": "bilinc login --device",
|
|
60
|
+
"ci": "bilinc login --api-key <key>, or set BILINC_API_KEY",
|
|
61
|
+
"pricing": "Free, no card required",
|
|
58
62
|
"signup": ACTIVATION_SIGNUP_URL,
|
|
59
63
|
"install_guide": INSTALL_URL,
|
|
60
64
|
"opened_browser": opened,
|
|
@@ -62,16 +66,38 @@ def _print_start_guide(*, opened: bool = False) -> None:
|
|
|
62
66
|
)
|
|
63
67
|
|
|
64
68
|
|
|
69
|
+
MCP_KEY_PLACEHOLDER = "bil_live_..."
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _mcp_server_entry() -> dict[str, Any]:
|
|
73
|
+
# The absolute interpreter works where a bare `python` does not: macOS ships
|
|
74
|
+
# only python3, and Claude Desktop launches servers without the shell PATH.
|
|
75
|
+
entry: dict[str, Any] = {"command": sys.executable, "args": ["-m", "bilinc.cloud_mcp"]}
|
|
76
|
+
if not load_config_api_key():
|
|
77
|
+
# With a key saved by `bilinc login` the adapter reads it itself, so the
|
|
78
|
+
# client config carries no key at all. Without one, the client must pass it.
|
|
79
|
+
entry["env"] = {"BILINC_API_KEY": MCP_KEY_PLACEHOLDER}
|
|
80
|
+
return entry
|
|
81
|
+
|
|
82
|
+
|
|
65
83
|
def _mcp_config() -> dict[str, Any]:
|
|
66
|
-
return {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
return {"mcpServers": {"bilinc": _mcp_server_entry()}}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _claude_code_command() -> str:
|
|
88
|
+
argv = ["claude", "mcp", "add", "--scope", "user", "bilinc"]
|
|
89
|
+
if not load_config_api_key():
|
|
90
|
+
argv += ["-e", f"BILINC_API_KEY={MCP_KEY_PLACEHOLDER}"]
|
|
91
|
+
argv += ["--", sys.executable, "-m", "bilinc.cloud_mcp"]
|
|
92
|
+
return subprocess.list2cmdline(argv) if os.name == "nt" else shlex.join(argv)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _claude_desktop_config_path() -> str:
|
|
96
|
+
if sys.platform == "darwin":
|
|
97
|
+
return "~/Library/Application Support/Claude/claude_desktop_config.json"
|
|
98
|
+
if os.name == "nt":
|
|
99
|
+
return r"%APPDATA%\Claude\claude_desktop_config.json"
|
|
100
|
+
return "~/.config/Claude/claude_desktop_config.json"
|
|
75
101
|
|
|
76
102
|
|
|
77
103
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -89,9 +115,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
89
115
|
start = sub.add_parser("start", help="Open the simplest path from install to first memory")
|
|
90
116
|
start.add_argument("--open", action="store_true", help="Open the Bilinc signup page in a browser")
|
|
91
117
|
|
|
92
|
-
login = sub.add_parser(
|
|
93
|
-
|
|
94
|
-
|
|
118
|
+
login = sub.add_parser(
|
|
119
|
+
"login",
|
|
120
|
+
help="Sign in from your browser and save an API key for this computer",
|
|
121
|
+
)
|
|
122
|
+
login.add_argument("--api-key", help="Save this API key instead of signing in (for CI)")
|
|
123
|
+
login.add_argument(
|
|
124
|
+
"--device",
|
|
125
|
+
action="store_true",
|
|
126
|
+
help="Sign in with a code approved in any browser (for SSH sessions and servers)",
|
|
127
|
+
)
|
|
128
|
+
login.add_argument(
|
|
129
|
+
"--no-browser",
|
|
130
|
+
action="store_true",
|
|
131
|
+
help="Print the sign-in URL instead of opening a browser",
|
|
132
|
+
)
|
|
133
|
+
login.add_argument("--open", action="store_true", help=argparse.SUPPRESS)
|
|
95
134
|
|
|
96
135
|
commit = sub.add_parser("commit", help="Commit a memory entry to Bilinc Cloud")
|
|
97
136
|
commit.add_argument("--key", required=True)
|
|
@@ -159,9 +198,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
159
198
|
|
|
160
199
|
mcp = sub.add_parser("mcp", help="Print hosted Cloud MCP adapter configuration")
|
|
161
200
|
mcp_sub = mcp.add_subparsers(dest="mcp_command")
|
|
162
|
-
mcp_sub.add_parser("install", help="Print MCP config for agent runtimes")
|
|
201
|
+
mcp_install = mcp_sub.add_parser("install", help="Print MCP config for agent runtimes")
|
|
202
|
+
mcp_install.add_argument(
|
|
203
|
+
"--client",
|
|
204
|
+
choices=["json", "claude-code", "claude-desktop"],
|
|
205
|
+
default="json",
|
|
206
|
+
help="json: mcpServers config (default); claude-code: a `claude mcp add` command; "
|
|
207
|
+
"claude-desktop: the config plus where Claude Desktop keeps it",
|
|
208
|
+
)
|
|
163
209
|
|
|
164
|
-
sub.add_parser("signup", help="Print signup URL
|
|
210
|
+
sub.add_parser("signup", help="Print the signup URL (free, no card required)")
|
|
165
211
|
|
|
166
212
|
return parser
|
|
167
213
|
|
|
@@ -193,18 +239,37 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
193
239
|
return 0
|
|
194
240
|
|
|
195
241
|
if args.command == "login":
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
242
|
+
if args.api_key:
|
|
243
|
+
try:
|
|
244
|
+
path = save_config_api_key(args.api_key, base_url=args.base_url)
|
|
245
|
+
except BilincApiKeyRequired as exc:
|
|
246
|
+
print(f"bilinc: error: {exc}", file=sys.stderr)
|
|
247
|
+
return 1
|
|
248
|
+
_print({"saved": True, "config": str(path), "next": "Run bilinc quicktest"})
|
|
201
249
|
return 0
|
|
202
250
|
try:
|
|
203
|
-
|
|
204
|
-
|
|
251
|
+
if args.device or is_headless():
|
|
252
|
+
result = device_login(args.base_url, timeout=args.timeout)
|
|
253
|
+
else:
|
|
254
|
+
result = loopback_login(args.base_url, timeout=args.timeout, open_browser=not args.no_browser)
|
|
255
|
+
api_key = result.get("api_key")
|
|
256
|
+
if not isinstance(api_key, str) or not api_key:
|
|
257
|
+
raise LoginError("Bilinc Cloud did not return an API key. Run bilinc login again.")
|
|
258
|
+
path = save_config_api_key(api_key, base_url=args.base_url)
|
|
259
|
+
except (LoginError, BilincApiKeyRequired, BilincCloudError) as exc:
|
|
205
260
|
print(f"bilinc: error: {exc}", file=sys.stderr)
|
|
206
261
|
return 1
|
|
207
|
-
|
|
262
|
+
except KeyboardInterrupt:
|
|
263
|
+
print("bilinc: login cancelled", file=sys.stderr)
|
|
264
|
+
return 130
|
|
265
|
+
_print(
|
|
266
|
+
{
|
|
267
|
+
"saved": True,
|
|
268
|
+
"config": str(path),
|
|
269
|
+
"api_key_name": result.get("key_name"),
|
|
270
|
+
"next": "Run bilinc quicktest",
|
|
271
|
+
}
|
|
272
|
+
)
|
|
208
273
|
return 0
|
|
209
274
|
|
|
210
275
|
if args.command == "signup":
|
|
@@ -212,13 +277,28 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
212
277
|
{
|
|
213
278
|
"signup": ACTIVATION_SIGNUP_URL,
|
|
214
279
|
"base_signup": SIGNUP_URL,
|
|
215
|
-
"
|
|
216
|
-
"next": "
|
|
280
|
+
"pricing": "Free, no card required",
|
|
281
|
+
"next": "Run bilinc login to sign in from your browser, then bilinc quicktest",
|
|
217
282
|
}
|
|
218
283
|
)
|
|
219
284
|
return 0
|
|
220
285
|
if args.command == "mcp" and args.mcp_command == "install":
|
|
221
|
-
|
|
286
|
+
# stdout stays copy-paste clean; guidance goes to stderr.
|
|
287
|
+
if args.client == "claude-code":
|
|
288
|
+
print(_claude_code_command())
|
|
289
|
+
else:
|
|
290
|
+
_print(_mcp_config())
|
|
291
|
+
if args.client == "claude-desktop":
|
|
292
|
+
print(
|
|
293
|
+
f'Merge the "bilinc" entry into mcpServers in {_claude_desktop_config_path()}, '
|
|
294
|
+
"then restart Claude Desktop.",
|
|
295
|
+
file=sys.stderr,
|
|
296
|
+
)
|
|
297
|
+
if not load_config_api_key():
|
|
298
|
+
print(
|
|
299
|
+
f"No saved key yet: run `bilinc login` first, or replace {MCP_KEY_PLACEHOLDER} with your API key.",
|
|
300
|
+
file=sys.stderr,
|
|
301
|
+
)
|
|
222
302
|
return 0
|
|
223
303
|
if args.command is None:
|
|
224
304
|
parser.print_help()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Bilinc Cloud client.
|
|
2
2
|
|
|
3
|
-
Bilinc 2.
|
|
3
|
+
Bilinc 2.3.1 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.1"
|
|
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.1
|
|
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.1 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.1
|
|
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
|
|
|
@@ -134,18 +131,28 @@ Bilinc exposes a standard Model Context Protocol server over **stdio**, so any
|
|
|
134
131
|
MCP-compatible client can connect — Claude Code, Codex, Cursor, Hermes-Agent,
|
|
135
132
|
opencode, and others.
|
|
136
133
|
|
|
134
|
+
After `bilinc login`, let the CLI write the setup for your client:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
bilinc mcp install --client claude-code # prints a ready `claude mcp add ...` command
|
|
138
|
+
bilinc mcp install --client claude-desktop # prints the config and where Claude Desktop keeps it
|
|
139
|
+
bilinc mcp install # plain mcpServers JSON for any other client
|
|
140
|
+
```
|
|
141
|
+
|
|
137
142
|
```json
|
|
138
143
|
{
|
|
139
144
|
"mcpServers": {
|
|
140
145
|
"bilinc": {
|
|
141
|
-
"command": "
|
|
142
|
-
"args": ["-m", "bilinc.cloud_mcp"]
|
|
143
|
-
"env": { "BILINC_API_KEY": "bil_live_..." }
|
|
146
|
+
"command": "/path/to/python3",
|
|
147
|
+
"args": ["-m", "bilinc.cloud_mcp"]
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
```
|
|
148
152
|
|
|
153
|
+
`command` is the interpreter that has Bilinc installed, and the adapter reads the key saved by `bilinc login`, so
|
|
154
|
+
the config carries no key. On a machine without a saved key, add `"env": { "BILINC_API_KEY": "bil_live_..." }`.
|
|
155
|
+
|
|
149
156
|
Eight tools — the core memory lifecycle, and nothing else:
|
|
150
157
|
|
|
151
158
|
| Tool | What it does |
|
|
@@ -227,10 +234,10 @@ bilinc rollback execute --snapshot snap_... --reason "undo bad agent run" \
|
|
|
227
234
|
Useful first-run commands:
|
|
228
235
|
|
|
229
236
|
```bash
|
|
230
|
-
bilinc
|
|
231
|
-
bilinc login --api-key bil_live_...
|
|
237
|
+
bilinc login # browser sign-in; --device without a browser
|
|
232
238
|
bilinc quicktest
|
|
233
239
|
bilinc mcp install
|
|
240
|
+
bilinc start # prints the first-run guide
|
|
234
241
|
```
|
|
235
242
|
|
|
236
243
|
## 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.1"
|
|
16
|
+
assert bilinc.version == "2.3.1"
|
|
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.1",
|
|
114
114
|
"X-Bilinc-Client": "python-sdk",
|
|
115
|
-
"X-Bilinc-Client-Version": "2.
|
|
115
|
+
"X-Bilinc-Client-Version": "2.3.1",
|
|
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.1",
|
|
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()
|
|
@@ -327,6 +327,56 @@ def test_cli_start_login_doctor_quicktest_and_mcp_config(monkeypatch, capsys, tm
|
|
|
327
327
|
assert "bil_live_cli_test" not in mcp_out.out
|
|
328
328
|
|
|
329
329
|
|
|
330
|
+
def test_mcp_install_uses_the_login_key_and_a_real_interpreter(monkeypatch, capsys, tmp_path):
|
|
331
|
+
import sys
|
|
332
|
+
|
|
333
|
+
from bilinc.cli import main as cli_main
|
|
334
|
+
|
|
335
|
+
monkeypatch.delenv("BILINC_API_KEY", raising=False)
|
|
336
|
+
monkeypatch.setenv("BILINC_CONFIG_DIR", str(tmp_path))
|
|
337
|
+
assert cli_main.main(["login", "--api-key", "bil_live_saved_key"]) == 0
|
|
338
|
+
capsys.readouterr()
|
|
339
|
+
|
|
340
|
+
# Default JSON: absolute interpreter, and no env block — the adapter reads the saved key.
|
|
341
|
+
assert cli_main.main(["mcp", "install"]) == 0
|
|
342
|
+
out = capsys.readouterr()
|
|
343
|
+
server = json.loads(out.out)["mcpServers"]["bilinc"]
|
|
344
|
+
assert server == {"command": sys.executable, "args": ["-m", "bilinc.cloud_mcp"]}
|
|
345
|
+
assert "bil_live_saved_key" not in out.out + out.err
|
|
346
|
+
assert "${BILINC_API_KEY}" not in out.out
|
|
347
|
+
|
|
348
|
+
# Claude Code: one runnable command, user scope, no key on the command line.
|
|
349
|
+
assert cli_main.main(["mcp", "install", "--client", "claude-code"]) == 0
|
|
350
|
+
out = capsys.readouterr()
|
|
351
|
+
assert out.out.startswith("claude mcp add --scope user bilinc -- ")
|
|
352
|
+
assert out.out.rstrip().endswith("-m bilinc.cloud_mcp")
|
|
353
|
+
assert "BILINC_API_KEY" not in out.out
|
|
354
|
+
assert "bil_live_saved_key" not in out.out + out.err
|
|
355
|
+
|
|
356
|
+
# Claude Desktop: clean JSON on stdout, the config location on stderr.
|
|
357
|
+
assert cli_main.main(["mcp", "install", "--client", "claude-desktop"]) == 0
|
|
358
|
+
out = capsys.readouterr()
|
|
359
|
+
assert json.loads(out.out)["mcpServers"]["bilinc"]["command"] == sys.executable
|
|
360
|
+
assert "claude_desktop_config.json" in out.err
|
|
361
|
+
assert "bil_live_saved_key" not in out.out + out.err
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def test_mcp_install_without_a_saved_key_asks_for_one(monkeypatch, capsys, tmp_path):
|
|
365
|
+
from bilinc.cli import main as cli_main
|
|
366
|
+
|
|
367
|
+
monkeypatch.delenv("BILINC_API_KEY", raising=False)
|
|
368
|
+
monkeypatch.setenv("BILINC_CONFIG_DIR", str(tmp_path))
|
|
369
|
+
|
|
370
|
+
assert cli_main.main(["mcp", "install"]) == 0
|
|
371
|
+
out = capsys.readouterr()
|
|
372
|
+
assert json.loads(out.out)["mcpServers"]["bilinc"]["env"] == {"BILINC_API_KEY": "bil_live_..."}
|
|
373
|
+
assert "bilinc login" in out.err
|
|
374
|
+
|
|
375
|
+
assert cli_main.main(["mcp", "install", "--client", "claude-code"]) == 0
|
|
376
|
+
out = capsys.readouterr()
|
|
377
|
+
assert "-e BILINC_API_KEY=bil_live_..." in out.out
|
|
378
|
+
|
|
379
|
+
|
|
330
380
|
def _forbidden_package_prefixes(root: str = "") -> tuple[str, ...]:
|
|
331
381
|
return tuple(
|
|
332
382
|
f"{root}{prefix}"
|
|
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
|