bilinc 2.3.7__tar.gz → 2.3.8__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.3.7
3
+ Version: 2.3.8
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
@@ -21,7 +21,7 @@ Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: certifi>=2024.2.2
24
- Requires-Dist: mcp<2.0,>=1.0.0
24
+ Requires-Dist: mcp<2.0,>=1.10.0
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: build<2.0,>=1.2; extra == "dev"
27
27
  Requires-Dist: pytest<9.0,>=7.0; extra == "dev"
@@ -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.3.7 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.8 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
 
@@ -157,7 +157,7 @@ Cloud status check.
157
157
  To reproduce this release exactly:
158
158
 
159
159
  ```bash
160
- pip install -U bilinc==2.3.7
160
+ pip install -U bilinc==2.3.8
161
161
  ```
162
162
 
163
163
  If you prefer a browser guide, open https://bilinc.space/install and follow the
@@ -191,7 +191,7 @@ bilinc mcp install # plain mcpServers JSON for any othe
191
191
  `command` is the interpreter that has Bilinc installed, and the adapter reads the key saved by `bilinc login`, so
192
192
  the config carries no key. On a machine without a saved key, add `"env": { "BILINC_API_KEY": "bil_live_..." }`.
193
193
 
194
- Eight tools — the core memory lifecycle, and nothing else:
194
+ Eleven tools: the eight-step memory lifecycle plus three review tools.
195
195
 
196
196
  | Tool | What it does |
197
197
  | --- | --- |
@@ -203,8 +203,11 @@ Eight tools — the core memory lifecycle, and nothing else:
203
203
  | `snapshot` | Checkpoint a project before risky work, or list existing checkpoints. |
204
204
  | `diff` | Compare a checkpoint against another checkpoint or current state. Values are redacted by default. |
205
205
  | `rollback` | **Destructive in execute mode.** Restore a checkpoint through a free preview plus an explicitly confirmed execute. |
206
+ | `list_memories` | Browse what is stored, ordered by key, with prefix, type and updated-time filters. Read-only and free. |
207
+ | `history` | Show one memory's recorded changes, newest first, with the value before and after. Read-only and free. |
208
+ | `confirm` | Record that a memory is still accurate without resending its value. Costs one write. |
206
209
 
207
- Operator and debug tooling — health probes, benchmarks, export/import, workspace replay — stays
210
+ Operator and debug tooling — health probes, benchmarks, bulk import, workspace replay — stays
208
211
  local-only, as do the epistemic read tools for claims, contradictions, and graph queries. The hosted
209
212
  adapter does not bundle local runtime internals.
210
213
 
@@ -243,6 +246,24 @@ client.status() # what can this key do?
243
246
  client.health() # is the service reachable?
244
247
  ```
245
248
 
249
+ ### Review what Bilinc remembers
250
+
251
+ Listing, history and export are reads and are not billed. Confirming costs one write.
252
+
253
+ ```python
254
+ page = client.list_memories(prefix="agent.", limit=50) # one page, ordered by key
255
+ for entry in client.iter_memories(memory_type="semantic"): # every page
256
+ print(entry["key"], entry["updatedAt"])
257
+
258
+ client.history("agent.goal") # newest changes first (20 by default, up to 100); "truncated" marks older ones
259
+ client.confirm("agent.goal", expected_version=page["entries"][0]["entryVersion"]) # still true
260
+
261
+ backup = client.export() # every stored memory with its full value, as one dict
262
+ ```
263
+
264
+ Forgetting a memory removes its value from history as well: earlier values of a forgotten
265
+ memory are not returned.
266
+
246
267
  For server, CI, and hosted agent runtimes, store the key as `BILINC_API_KEY`.
247
268
 
248
269
  ## CLI
@@ -260,6 +281,16 @@ bilinc forget --key agent.goal --reason "superseded by the planner service"
260
281
  bilinc doctor
261
282
  ```
262
283
 
284
+ Review what is stored:
285
+
286
+ ```bash
287
+ bilinc list --prefix agent. # table of keys, types, update times, value previews
288
+ bilinc list --all --json # every page as JSON
289
+ bilinc history agent.goal # recorded changes, newest first
290
+ bilinc confirm agent.goal # still accurate; the value is unchanged
291
+ bilinc export -o bilinc-export.json # full values; the file is readable only by you
292
+ ```
293
+
263
294
  Rollback is two stages. Execute takes the token from the preview and never prompts interactively,
264
295
  so it stays safe inside automation:
265
296
 
@@ -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.3.7 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.8 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
 
@@ -115,7 +115,7 @@ Cloud status check.
115
115
  To reproduce this release exactly:
116
116
 
117
117
  ```bash
118
- pip install -U bilinc==2.3.7
118
+ pip install -U bilinc==2.3.8
119
119
  ```
120
120
 
121
121
  If you prefer a browser guide, open https://bilinc.space/install and follow the
@@ -149,7 +149,7 @@ bilinc mcp install # plain mcpServers JSON for any othe
149
149
  `command` is the interpreter that has Bilinc installed, and the adapter reads the key saved by `bilinc login`, so
150
150
  the config carries no key. On a machine without a saved key, add `"env": { "BILINC_API_KEY": "bil_live_..." }`.
151
151
 
152
- Eight tools — the core memory lifecycle, and nothing else:
152
+ Eleven tools: the eight-step memory lifecycle plus three review tools.
153
153
 
154
154
  | Tool | What it does |
155
155
  | --- | --- |
@@ -161,8 +161,11 @@ Eight tools — the core memory lifecycle, and nothing else:
161
161
  | `snapshot` | Checkpoint a project before risky work, or list existing checkpoints. |
162
162
  | `diff` | Compare a checkpoint against another checkpoint or current state. Values are redacted by default. |
163
163
  | `rollback` | **Destructive in execute mode.** Restore a checkpoint through a free preview plus an explicitly confirmed execute. |
164
+ | `list_memories` | Browse what is stored, ordered by key, with prefix, type and updated-time filters. Read-only and free. |
165
+ | `history` | Show one memory's recorded changes, newest first, with the value before and after. Read-only and free. |
166
+ | `confirm` | Record that a memory is still accurate without resending its value. Costs one write. |
164
167
 
165
- Operator and debug tooling — health probes, benchmarks, export/import, workspace replay — stays
168
+ Operator and debug tooling — health probes, benchmarks, bulk import, workspace replay — stays
166
169
  local-only, as do the epistemic read tools for claims, contradictions, and graph queries. The hosted
167
170
  adapter does not bundle local runtime internals.
168
171
 
@@ -201,6 +204,24 @@ client.status() # what can this key do?
201
204
  client.health() # is the service reachable?
202
205
  ```
203
206
 
207
+ ### Review what Bilinc remembers
208
+
209
+ Listing, history and export are reads and are not billed. Confirming costs one write.
210
+
211
+ ```python
212
+ page = client.list_memories(prefix="agent.", limit=50) # one page, ordered by key
213
+ for entry in client.iter_memories(memory_type="semantic"): # every page
214
+ print(entry["key"], entry["updatedAt"])
215
+
216
+ client.history("agent.goal") # newest changes first (20 by default, up to 100); "truncated" marks older ones
217
+ client.confirm("agent.goal", expected_version=page["entries"][0]["entryVersion"]) # still true
218
+
219
+ backup = client.export() # every stored memory with its full value, as one dict
220
+ ```
221
+
222
+ Forgetting a memory removes its value from history as well: earlier values of a forgotten
223
+ memory are not returned.
224
+
204
225
  For server, CI, and hosted agent runtimes, store the key as `BILINC_API_KEY`.
205
226
 
206
227
  ## CLI
@@ -218,6 +239,16 @@ bilinc forget --key agent.goal --reason "superseded by the planner service"
218
239
  bilinc doctor
219
240
  ```
220
241
 
242
+ Review what is stored:
243
+
244
+ ```bash
245
+ bilinc list --prefix agent. # table of keys, types, update times, value previews
246
+ bilinc list --all --json # every page as JSON
247
+ bilinc history agent.goal # recorded changes, newest first
248
+ bilinc confirm agent.goal # still accurate; the value is unchanged
249
+ bilinc export -o bilinc-export.json # full values; the file is readable only by you
250
+ ```
251
+
221
252
  Rollback is two stages. Execute takes the token from the preview and never prompts interactively,
222
253
  so it stays safe inside automation:
223
254
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bilinc"
7
- version = "2.3.7"
7
+ version = "2.3.8"
8
8
  description = "Cloud-only memory SDK and MCP adapter for autonomous agents"
9
9
  readme = "README.md"
10
10
  license = "BUSL-1.1"
@@ -33,7 +33,7 @@ classifiers = [
33
33
 
34
34
  dependencies = [
35
35
  "certifi>=2024.2.2",
36
- "mcp>=1.0.0,<2.0",
36
+ "mcp>=1.10.0,<2.0",
37
37
  ]
38
38
 
39
39
  [project.urls]
@@ -1,4 +1,4 @@
1
- """Bilinc 2.3.7: cloud-only SDK for agent memory."""
1
+ """Bilinc 2.3.8: 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.3.7"
28
+ __version__ = "2.3.8"
29
29
  version = __version__
30
30
 
31
31
  __all__ = [
@@ -4,13 +4,16 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import argparse
7
+ import contextlib
7
8
  import json
8
9
  import os
9
10
  import shlex
10
11
  import subprocess
11
12
  import sys
13
+ import tempfile
12
14
  import time
13
15
  import webbrowser
16
+ from datetime import datetime
14
17
  from typing import Any
15
18
 
16
19
  from bilinc import __version__
@@ -18,9 +21,12 @@ from bilinc.client import (
18
21
  ACTIVATION_SIGNUP_URL,
19
22
  BilincApiKeyRequired,
20
23
  BilincCloudError,
24
+ BilincValidationError,
21
25
  CloudClient,
22
26
  INSTALL_URL,
27
+ MEMORY_TYPES,
23
28
  SIGNUP_URL,
29
+ VALUE_MODES,
24
30
  config_path,
25
31
  load_config_api_key,
26
32
  save_config_api_key,
@@ -39,6 +45,125 @@ def _print(payload: dict[str, Any]) -> None:
39
45
  print(json.dumps(payload, indent=2, ensure_ascii=False))
40
46
 
41
47
 
48
+ def _short_time(value: Any) -> str:
49
+ """Render an ISO-8601 timestamp as `YYYY-MM-DD HH:MM:SS` UTC for tables."""
50
+ if not isinstance(value, str) or not value:
51
+ return "-"
52
+ try:
53
+ moment = datetime.fromisoformat(value.replace("Z", "+00:00"))
54
+ except ValueError:
55
+ return value
56
+ return moment.strftime("%Y-%m-%d %H:%M:%S")
57
+
58
+
59
+ def _short_value(value: Any, width: int = 60) -> str:
60
+ text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
61
+ text = " ".join(text.split())
62
+ return text if len(text) <= width else text[: width - 1] + "…"
63
+
64
+
65
+ def _print_history(payload: dict[str, Any]) -> None:
66
+ key = payload.get("key", "")
67
+ entries = payload.get("entries") or []
68
+ if not entries:
69
+ print(f"No recorded changes for {key}.")
70
+ return
71
+ forgotten = any(entry.get("op") == "forget" for entry in entries[:1])
72
+ state = "" if payload.get("exists", True) else (", forgotten" if forgotten else ", not currently stored")
73
+ print(f"{key} ({len(entries)} shown, newest first{state})")
74
+ for entry in entries:
75
+ op = str(entry.get("op", "?"))
76
+ line = f" {_short_time(entry.get('at'))} {op:<8}"
77
+ if entry.get("valuesRedacted"):
78
+ line += " (value not shown: the memory was forgotten)"
79
+ elif "before" in entry and "after" in entry and op != "confirm":
80
+ line += f" {_short_value(entry['before'], 30)} -> {_short_value(entry['after'], 30)}"
81
+ elif "after" in entry:
82
+ line += f" {_short_value(entry['after'])}"
83
+ if entry.get("reason"):
84
+ line += f" reason: {_short_value(entry['reason'], 40)}"
85
+ if entry.get("source"):
86
+ line += f" source: {entry['source']}"
87
+ print(line)
88
+ if payload.get("truncated"):
89
+ print(" … older changes not shown; raise --limit to see more.")
90
+
91
+
92
+ def _continuation_command(next_cursor: str, filters: dict[str, Any] | None) -> str:
93
+ """The `bilinc list` command for the next page, with the same filters.
94
+
95
+ A cursor only marks a position; the filters must be repeated or the next
96
+ page would cover a different set of memories.
97
+ """
98
+
99
+ parts = ["bilinc list"]
100
+ flags = {
101
+ "prefix": "--prefix",
102
+ "memory_type": "--type",
103
+ "updated_after": "--updated-after",
104
+ "updated_before": "--updated-before",
105
+ "limit": "--limit",
106
+ }
107
+ for name, flag in flags.items():
108
+ value = (filters or {}).get(name)
109
+ if value is not None and value != "":
110
+ parts.append(f"{flag} {shlex.quote(str(value))}")
111
+ values = (filters or {}).get("values")
112
+ if values and values != "preview":
113
+ parts.append(f"--values {shlex.quote(str(values))}")
114
+ parts.append(f"--cursor {shlex.quote(next_cursor)}")
115
+ return " ".join(parts)
116
+
117
+
118
+ def _print_memories(
119
+ entries: list[dict[str, Any]],
120
+ *,
121
+ total: Any,
122
+ next_cursor: Any,
123
+ filters: dict[str, Any] | None = None,
124
+ ) -> None:
125
+ if not entries:
126
+ print("No memories match.")
127
+ return
128
+ key_width = min(max(len(str(entry.get("key", ""))) for entry in entries), 40)
129
+ print(f"{'KEY':<{key_width}} {'TYPE':<10} {'UPDATED (UTC)':<19} VALUE")
130
+ for entry in entries:
131
+ key = _short_value(str(entry.get("key", "")), key_width)
132
+ value = _short_value(entry["value"]) if "value" in entry else ""
133
+ print(
134
+ f"{key:<{key_width}} {str(entry.get('memoryType', '-')):<10} "
135
+ f"{_short_time(entry.get('updatedAt')):<19} {value}"
136
+ )
137
+ summary = f"{len(entries)} shown"
138
+ if isinstance(total, int):
139
+ summary += f" of {total}"
140
+ print(summary)
141
+ if isinstance(next_cursor, str) and next_cursor:
142
+ print(f"More: {_continuation_command(next_cursor, filters)} (or --all)")
143
+
144
+
145
+ def _write_private_json(path: str, payload: dict[str, Any]) -> None:
146
+ """Write an export owner-only: it holds the full memory contents.
147
+
148
+ The content goes to a new temporary file (created 0600) beside the
149
+ target, which then atomically replaces it. An existing file at ``path``
150
+ keeps its old contents until the write has fully succeeded, and the
151
+ result never inherits a wider mode from the file it replaces. This avoids
152
+ ``os.fchmod``, which Windows lacks before Python 3.13.
153
+ """
154
+ directory = os.path.dirname(os.path.abspath(path))
155
+ fd, temporary = tempfile.mkstemp(prefix=".bilinc-export-", suffix=".json", dir=directory)
156
+ try:
157
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
158
+ handle.write(json.dumps(payload, indent=2, ensure_ascii=False))
159
+ handle.write("\n")
160
+ os.replace(temporary, path)
161
+ except BaseException:
162
+ with contextlib.suppress(FileNotFoundError):
163
+ os.unlink(temporary)
164
+ raise
165
+
166
+
42
167
  def _client(args: argparse.Namespace) -> CloudClient:
43
168
  return CloudClient(api_key=args.api_key, base_url=args.base_url, timeout=args.timeout)
44
169
 
@@ -165,6 +290,33 @@ def build_parser() -> argparse.ArgumentParser:
165
290
  forget.add_argument("--reason", required=True, help="Required audit reason for the deletion")
166
291
  forget.add_argument("--expected-version", help="Fail if the memory changed since this version")
167
292
 
293
+ confirm = sub.add_parser("confirm", help="Record that an existing memory is still accurate")
294
+ confirm.add_argument("key", help="Memory key")
295
+ confirm.add_argument("--expected-version", help="Fail if the memory changed since this version")
296
+ confirm.add_argument("--reason", help="Optional note recorded in the memory's history")
297
+
298
+ history = sub.add_parser("history", help="Show one memory's recorded changes, newest first")
299
+ history.add_argument("key", help="Memory key")
300
+ history.add_argument("--limit", type=int, default=20)
301
+ history.add_argument("--values", choices=VALUE_MODES, default="full", help="How much of each value to show")
302
+ history.add_argument("--json", action="store_true", help="Print the raw JSON response")
303
+
304
+ list_cmd = sub.add_parser("list", help="List stored memories, ordered by key")
305
+ list_cmd.add_argument("--prefix", help="Only keys starting with this text")
306
+ list_cmd.add_argument("--type", choices=MEMORY_TYPES, help="Only this memory type")
307
+ list_cmd.add_argument("--updated-after", help="Only memories updated after this ISO-8601 time")
308
+ list_cmd.add_argument("--updated-before", help="Only memories updated before this ISO-8601 time")
309
+ list_cmd.add_argument("--limit", type=int, default=50, help="Page size (1-100)")
310
+ list_cmd.add_argument("--cursor", help="Continue from a previous page")
311
+ list_cmd.add_argument("--all", action="store_true", help="Follow every page")
312
+ list_cmd.add_argument("--values", choices=VALUE_MODES, default="preview", help="How much of each value to show")
313
+ list_cmd.add_argument("--json", action="store_true", help="Print the raw JSON response")
314
+
315
+ export = sub.add_parser("export", help="Export every stored memory with its full value as JSON")
316
+ export.add_argument("-o", "--output", help="Write to this file (owner-only) instead of stdout")
317
+ export.add_argument("--prefix", help="Only keys starting with this text")
318
+ export.add_argument("--type", choices=MEMORY_TYPES, help="Only this memory type")
319
+
168
320
  snapshot = sub.add_parser("snapshot", help="Create or list project checkpoints")
169
321
  snapshot.add_argument("action", choices=["create", "list"], nargs="?", default="create")
170
322
  snapshot.add_argument("--label", help="Human-readable label for a new checkpoint")
@@ -340,6 +492,63 @@ def main(argv: list[str] | None = None) -> int:
340
492
  expected_version=args.expected_version,
341
493
  )
342
494
  )
495
+ elif args.command == "confirm":
496
+ _print(
497
+ client.confirm(
498
+ args.key,
499
+ expected_version=args.expected_version,
500
+ reason=args.reason,
501
+ )
502
+ )
503
+ elif args.command == "history":
504
+ try:
505
+ result = client.history(args.key, limit=args.limit, values=args.values)
506
+ except BilincValidationError as exc:
507
+ if (exc.details or {}).get("reason") != "response_too_large":
508
+ raise
509
+ raise BilincValidationError(
510
+ "This history is too large to return at once. "
511
+ "Try --limit 1, or --values preview.",
512
+ code=exc.code,
513
+ status=exc.status,
514
+ details=exc.details,
515
+ ) from exc
516
+ if args.json:
517
+ _print(result)
518
+ else:
519
+ _print_history(result)
520
+ elif args.command == "list":
521
+ filters = {
522
+ "prefix": args.prefix,
523
+ "memory_type": args.type,
524
+ "updated_after": args.updated_after,
525
+ "updated_before": args.updated_before,
526
+ "values": args.values,
527
+ }
528
+ if args.all:
529
+ entries = list(client.iter_memories(page_size=args.limit, cursor=args.cursor, **filters))
530
+ if args.json:
531
+ _print({"entries": entries, "count": len(entries)})
532
+ else:
533
+ _print_memories(entries, total=len(entries), next_cursor=None)
534
+ else:
535
+ result = client.list_memories(cursor=args.cursor, limit=args.limit, **filters)
536
+ if args.json:
537
+ _print(result)
538
+ else:
539
+ _print_memories(
540
+ result.get("entries") or [],
541
+ total=result.get("total"),
542
+ next_cursor=result.get("nextCursor"),
543
+ filters={**filters, "limit": args.limit},
544
+ )
545
+ elif args.command == "export":
546
+ exported = client.export(prefix=args.prefix, memory_type=args.type)
547
+ if args.output:
548
+ _write_private_json(args.output, exported)
549
+ print(f"Exported {exported['count']} memories to {args.output}", file=sys.stderr)
550
+ else:
551
+ _print(exported)
343
552
  elif args.command == "snapshot":
344
553
  if args.action == "list":
345
554
  _print(client.list_snapshots(limit=args.limit))
@@ -1,6 +1,6 @@
1
1
  """Bilinc Cloud client.
2
2
 
3
- Bilinc 2.3.7 is cloud-only: the PyPI package is a thin SDK and MCP adapter for
3
+ Bilinc 2.3.8 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
  """
@@ -15,10 +15,11 @@ import urllib.error
15
15
  import urllib.parse
16
16
  import urllib.request
17
17
  from dataclasses import dataclass
18
+ from datetime import datetime, timezone
18
19
  from pathlib import Path
19
- from typing import Any, Callable
20
+ from typing import Any, Callable, Iterator
20
21
 
21
- __version__ = "2.3.7"
22
+ __version__ = "2.3.8"
22
23
  DEFAULT_BASE_URL = "https://bilinc.space"
23
24
  SIGNUP_URL = "https://bilinc.space/signup"
24
25
  ACTIVATION_CAMPAIGN = "activation_2_3_4"
@@ -46,6 +47,7 @@ CANONICAL_ERROR_CODES: dict[str, int] = {
46
47
  "payment_required": 402,
47
48
  "invalid_request": 400,
48
49
  "memory_not_found": 404,
50
+ "memory_exists": 409,
49
51
  "snapshot_not_found": 404,
50
52
  "version_conflict": 409,
51
53
  "idempotency_conflict": 409,
@@ -53,9 +55,13 @@ CANONICAL_ERROR_CODES: dict[str, int] = {
53
55
  "rollback_confirmation_expired": 410,
54
56
  "rate_limited": 429,
55
57
  "cloud_runtime_unavailable": 503,
58
+ "capability_unavailable": 503,
56
59
  }
57
60
 
58
61
  RETRYABLE_ERROR_CODES = frozenset({"rate_limited", "cloud_runtime_unavailable", "connection_failed"})
62
+ #: Answered with 503, but retrying cannot help: the running service lacks the
63
+ #: feature until it is upgraded.
64
+ NON_RETRYABLE_ERROR_CODES = frozenset({"capability_unavailable"})
59
65
 
60
66
 
61
67
  class BilincError(RuntimeError):
@@ -153,7 +159,9 @@ def error_for_response(status: int, payload: Any) -> BilincCloudError:
153
159
  message = body.get("message") if isinstance(body.get("message"), str) else None
154
160
  request_id = body.get("requestId") if isinstance(body.get("requestId"), str) else None
155
161
  retryable = body.get("retryable")
156
- if not isinstance(retryable, bool):
162
+ if code in NON_RETRYABLE_ERROR_CODES:
163
+ retryable = False
164
+ elif not isinstance(retryable, bool):
157
165
  retryable = status in (429, 503) or status >= 500 or code in RETRYABLE_ERROR_CODES
158
166
 
159
167
  error_class = _STATUS_ERRORS.get(status, BilincCloudError)
@@ -181,6 +189,12 @@ MAX_REASON_LENGTH = 512
181
189
  MAX_RECALL_LIMIT = 100
182
190
  MAX_SNAPSHOT_LIST_LIMIT = 100
183
191
  MAX_DIFF_LIMIT = 500
192
+ MAX_HISTORY_LIMIT = 100
193
+ MAX_LIST_LIMIT = 100
194
+
195
+ #: How much of each stored value history and list return: the whole value, a
196
+ #: short single-string preview, or nothing (keys and metadata only).
197
+ VALUE_MODES = ("full", "preview", "none")
184
198
 
185
199
  #: Bounded retries for read-only calls. Mutations are never retried
186
200
  #: automatically: billing idempotency alone does not prove that a retried write
@@ -279,6 +293,15 @@ def _optional_text(value: Any, field: str) -> str | None:
279
293
  return value.strip()
280
294
 
281
295
 
296
+ def _optional_raw_text(value: Any, field: str) -> str | None:
297
+ """Like :func:`_optional_text`, but keeps the value exactly as given."""
298
+ if value is None:
299
+ return None
300
+ if not isinstance(value, str) or value == "":
301
+ raise _invalid(field, "must be a non-empty string when provided")
302
+ return value
303
+
304
+
282
305
  def _optional_query_timestamp(value: Any) -> str | None:
283
306
  """Validate the bounded public representation of a point-in-time query.
284
307
 
@@ -296,6 +319,24 @@ def _optional_query_timestamp(value: Any) -> str | None:
296
319
  return parsed
297
320
 
298
321
 
322
+ def _optional_timestamp(value: Any, field: str) -> str | None:
323
+ """Accept a datetime or an ISO-8601 string; send ISO-8601.
324
+
325
+ A naive datetime is read as UTC, so the same call means the same instant
326
+ on every machine.
327
+ """
328
+
329
+ if value is None:
330
+ return None
331
+ if isinstance(value, datetime):
332
+ moment = value if value.tzinfo else value.replace(tzinfo=timezone.utc)
333
+ return moment.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
334
+ parsed = _optional_text(value, field)
335
+ if parsed is not None and len(parsed) > MAX_QUERY_TIMESTAMP_LENGTH:
336
+ raise _invalid(field, f"must be at most {MAX_QUERY_TIMESTAMP_LENGTH} characters")
337
+ return parsed
338
+
339
+
299
340
  def _optional_bool(value: Any, field: str) -> bool | None:
300
341
  if value is None:
301
342
  return None
@@ -443,6 +484,11 @@ def _default_transport(
443
484
  ) from exc
444
485
 
445
486
 
487
+ def _is_response_too_large(error: BilincCloudError) -> bool:
488
+ details = getattr(error, "details", None)
489
+ return isinstance(details, dict) and details.get("reason") == "response_too_large"
490
+
491
+
446
492
  @dataclass(slots=True)
447
493
  class CloudClient:
448
494
  """Minimal Bilinc Cloud SDK client."""
@@ -598,6 +644,153 @@ class CloudClient:
598
644
  idempotency_key=_optional_text(idempotency_key, "idempotency_key"),
599
645
  )
600
646
 
647
+ def confirm(
648
+ self,
649
+ key: str,
650
+ *,
651
+ expected_version: str | None = None,
652
+ reason: str | None = None,
653
+ idempotency_key: str | None = None,
654
+ ) -> dict[str, Any]:
655
+ """Record that an existing memory is still accurate.
656
+
657
+ The value stays exactly as stored; only its last-updated time moves to
658
+ now, and history records the step as a confirmation rather than an
659
+ edit. Costs one write operation. Pass ``expected_version`` to get
660
+ ``version_conflict`` if the memory changed since you read it.
661
+ """
662
+
663
+ payload: dict[str, Any] = {"key": _require_key(key)}
664
+ _put_optional(payload, "expectedVersion", _optional_text(expected_version, "expected_version"))
665
+ _put_optional(payload, "reason", _optional_text(reason, "reason"))
666
+
667
+ return self._post(
668
+ "/api/cloud/memory/confirm",
669
+ payload,
670
+ idempotency_key=_optional_text(idempotency_key, "idempotency_key"),
671
+ )
672
+
673
+ def history(self, key: str, *, limit: int = 20, values: str = "full") -> dict[str, Any]:
674
+ """Return one memory's recorded changes, newest first.
675
+
676
+ Read-only and not billed. Each entry carries the operation, when it
677
+ happened, the recorded reason and source, and (unless ``values`` is
678
+ ``"none"``) the value before and after. Values recorded before the
679
+ memory was forgotten are never returned.
680
+ """
681
+
682
+ return self._read(
683
+ "/api/cloud/memory/history",
684
+ {
685
+ "key": _require_key(key),
686
+ "limit": _require_limit(limit, "limit", MAX_HISTORY_LIMIT),
687
+ "values": _require_choice(values, "values", VALUE_MODES),
688
+ },
689
+ )
690
+
691
+ def list_memories(
692
+ self,
693
+ *,
694
+ prefix: str | None = None,
695
+ memory_type: str | None = None,
696
+ updated_after: datetime | str | None = None,
697
+ updated_before: datetime | str | None = None,
698
+ cursor: str | None = None,
699
+ limit: int = 50,
700
+ values: str = "preview",
701
+ ) -> dict[str, Any]:
702
+ """Return one page of stored memories, ordered by key.
703
+
704
+ Read-only and not billed. ``total`` counts every memory matching the
705
+ filters; pass ``nextCursor`` back as ``cursor`` for the next page, or
706
+ use :meth:`iter_memories` to walk every page.
707
+ """
708
+
709
+ payload: dict[str, Any] = {
710
+ "limit": _require_limit(limit, "limit", MAX_LIST_LIMIT),
711
+ "values": _require_choice(values, "values", VALUE_MODES),
712
+ }
713
+ # Not stripped: a trailing space is part of the prefix ("note " is not "note").
714
+ _put_optional(payload, "prefix", _optional_raw_text(prefix, "prefix"))
715
+ if memory_type is not None:
716
+ payload["memoryType"] = _require_memory_type(memory_type)
717
+ _put_optional(payload, "updatedAfter", _optional_timestamp(updated_after, "updated_after"))
718
+ _put_optional(payload, "updatedBefore", _optional_timestamp(updated_before, "updated_before"))
719
+ _put_optional(payload, "cursor", _optional_text(cursor, "cursor"))
720
+
721
+ return self._read("/api/cloud/memory/list", payload)
722
+
723
+ def iter_memories(
724
+ self,
725
+ *,
726
+ prefix: str | None = None,
727
+ memory_type: str | None = None,
728
+ updated_after: datetime | str | None = None,
729
+ updated_before: datetime | str | None = None,
730
+ page_size: int = MAX_LIST_LIMIT,
731
+ values: str = "preview",
732
+ cursor: str | None = None,
733
+ ) -> Iterator[dict[str, Any]]:
734
+ """Yield every stored memory matching the filters, page by page.
735
+
736
+ Starts after ``cursor`` when one is given. With ``values="full"`` a
737
+ page too large for one response is fetched again in smaller pages,
738
+ and the walk steps back up one size after each page that fits; Bilinc
739
+ Cloud always returns a single entry whole, so it never fails on size.
740
+ """
741
+
742
+ sizes = sorted({page_size, min(page_size, 10), 1}, reverse=True)
743
+ size_index = 0
744
+ seen: set[str] = set()
745
+ while True:
746
+ try:
747
+ page = self.list_memories(
748
+ prefix=prefix,
749
+ memory_type=memory_type,
750
+ updated_after=updated_after,
751
+ updated_before=updated_before,
752
+ cursor=cursor,
753
+ limit=sizes[size_index],
754
+ values=values,
755
+ )
756
+ except BilincValidationError as exc:
757
+ if not _is_response_too_large(exc) or size_index == len(sizes) - 1:
758
+ raise
759
+ size_index += 1
760
+ continue
761
+ # Step back up after a page that fit, so one large value does not
762
+ # leave the rest of the walk at one entry per request.
763
+ size_index = max(0, size_index - 1)
764
+ entries = page.get("entries")
765
+ yield from entries if isinstance(entries, list) else []
766
+ next_cursor = page.get("nextCursor")
767
+ if not isinstance(next_cursor, str) or not next_cursor:
768
+ return
769
+ if next_cursor in seen:
770
+ raise BilincCloudError(
771
+ "Bilinc Cloud returned a repeated list cursor",
772
+ code="invalid_response",
773
+ )
774
+ seen.add(next_cursor)
775
+ cursor = next_cursor
776
+
777
+ def export(
778
+ self,
779
+ *,
780
+ prefix: str | None = None,
781
+ memory_type: str | None = None,
782
+ ) -> dict[str, Any]:
783
+ """Return every stored memory with its full value, as one JSON-ready dict.
784
+
785
+ Pages through :meth:`iter_memories`; reads are not billed. Every
786
+ exported memory carries its full value. History is not included; use
787
+ :meth:`history` per key for that.
788
+ """
789
+
790
+ exported_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
791
+ entries = list(self.iter_memories(prefix=prefix, memory_type=memory_type, values="full"))
792
+ return {"exported_at": exported_at, "count": len(entries), "entries": entries}
793
+
601
794
  def snapshot(
602
795
  self,
603
796
  action: str = "create",
@@ -834,6 +1027,8 @@ __all__ = [
834
1027
  "INSTALL_URL",
835
1028
  "load_config_api_key",
836
1029
  "MAX_DIFF_LIMIT",
1030
+ "MAX_HISTORY_LIMIT",
1031
+ "MAX_LIST_LIMIT",
837
1032
  "MAX_RECALL_LIMIT",
838
1033
  "MAX_SNAPSHOT_LIST_LIMIT",
839
1034
  "MEMORY_TYPES",
@@ -842,4 +1037,5 @@ __all__ = [
842
1037
  "REVISION_STRATEGIES",
843
1038
  "save_config_api_key",
844
1039
  "SIGNUP_URL",
1040
+ "VALUE_MODES",
845
1041
  ]
@@ -9,7 +9,9 @@ Requires BILINC_API_KEY at call time, not at import time.
9
9
  This adapter exposes the eight core memory-lifecycle capabilities of hosted
10
10
  Bilinc — write, recall, deliberately revise, deliberately forget, checkpoint,
11
11
  inspect a change, restore a known-good state, and inspect runtime status —
12
- without bundling local StatePlane or storage internals.
12
+ plus three review tools (list what is stored, show one memory's history, and
13
+ confirm a memory is still accurate), without bundling local StatePlane or
14
+ storage internals.
13
15
 
14
16
  Operator/debug tooling (health, benchmark, export/import, workspace replay) and
15
17
  the epistemic read tools (verify, claims, contradictions, graph queries) stay
@@ -33,6 +35,9 @@ CLOUD_MCP_TOOLS = (
33
35
  "snapshot",
34
36
  "diff",
35
37
  "rollback",
38
+ "list_memories",
39
+ "history",
40
+ "confirm",
36
41
  )
37
42
 
38
43
 
@@ -50,6 +55,8 @@ def build_server():
50
55
  except Exception as exc: # pragma: no cover - depends on optional MCP runtime import path
51
56
  raise RuntimeError("Bilinc Cloud MCP adapter requires mcp>=1.0.0") from exc
52
57
 
58
+ from mcp.types import ToolAnnotations
59
+
53
60
  mcp = FastMCP("bilinc")
54
61
 
55
62
  @mcp.tool()
@@ -265,6 +272,98 @@ def build_server():
265
272
  )
266
273
  raise ValueError("mode must be either preview or execute")
267
274
 
275
+ @mcp.tool(
276
+ title="List stored memories",
277
+ annotations=ToolAnnotations(
278
+ readOnlyHint=True,
279
+ destructiveHint=False,
280
+ idempotentHint=True,
281
+ openWorldHint=False,
282
+ ),
283
+ )
284
+ def list_memories(
285
+ prefix: str | None = None,
286
+ memory_type: str | None = None,
287
+ updated_after: str | None = None,
288
+ updated_before: str | None = None,
289
+ cursor: str | None = None,
290
+ limit: int = 50,
291
+ values: str = "preview",
292
+ ) -> dict[str, Any]:
293
+ """List the memories stored in this Bilinc Cloud project, ordered by key.
294
+
295
+ Read-only and free. Filters: key `prefix`, `memory_type`, and ISO-8601
296
+ `updated_after` / `updated_before`. Returns up to `limit` entries
297
+ (1-100) with key, type, importance, source, created and updated times,
298
+ entry version, and a value whose size follows `values` ("preview" by
299
+ default, "full", or "none"). `total` counts all matches; `nextCursor`
300
+ is the `cursor` for the next page and is null on the last page.
301
+ """
302
+
303
+ return create_client().list_memories(
304
+ prefix=prefix,
305
+ memory_type=memory_type,
306
+ updated_after=updated_after,
307
+ updated_before=updated_before,
308
+ cursor=cursor,
309
+ limit=limit,
310
+ values=values,
311
+ )
312
+
313
+ @mcp.tool(
314
+ title="Show a memory's history",
315
+ annotations=ToolAnnotations(
316
+ readOnlyHint=True,
317
+ destructiveHint=False,
318
+ idempotentHint=True,
319
+ openWorldHint=False,
320
+ ),
321
+ )
322
+ def history(key: str, limit: int = 20, values: str = "full") -> dict[str, Any]:
323
+ """Show the recorded changes to one memory key, newest first.
324
+
325
+ Read-only and free. Each entry has the operation (create, update,
326
+ confirm, forget or delete), its time, the recorded reason and source,
327
+ and the value before and after unless `values` is "none". A change
328
+ made by a rollback keeps its create, update or delete operation and
329
+ also carries `rollbackSnapshotId`.
330
+ Values recorded before a memory was forgotten are not returned.
331
+ `exists` is false when the key is not currently stored.
332
+ """
333
+
334
+ return create_client().history(key, limit=limit, values=values)
335
+
336
+ @mcp.tool(
337
+ title="Confirm a memory is still accurate",
338
+ annotations=ToolAnnotations(
339
+ readOnlyHint=False,
340
+ destructiveHint=False,
341
+ # Each call adds a history entry, so a repeat is not a no-op.
342
+ idempotentHint=False,
343
+ openWorldHint=False,
344
+ ),
345
+ )
346
+ def confirm(
347
+ key: str,
348
+ expected_version: str | None = None,
349
+ reason: str | None = None,
350
+ idempotency_key: str | None = None,
351
+ ) -> dict[str, Any]:
352
+ """Record that an existing memory is still accurate.
353
+
354
+ The stored value is not changed; the memory's updated time moves to now
355
+ and its history gains a confirm entry. Costs one write operation.
356
+ Fails with `memory_not_found` when the key does not exist, and with
357
+ `version_conflict` when `expected_version` no longer matches.
358
+ """
359
+
360
+ return create_client().confirm(
361
+ key,
362
+ expected_version=expected_version,
363
+ reason=reason,
364
+ idempotency_key=idempotency_key,
365
+ )
366
+
268
367
  return mcp
269
368
 
270
369
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bilinc
3
- Version: 2.3.7
3
+ Version: 2.3.8
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
@@ -21,7 +21,7 @@ Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: certifi>=2024.2.2
24
- Requires-Dist: mcp<2.0,>=1.0.0
24
+ Requires-Dist: mcp<2.0,>=1.10.0
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: build<2.0,>=1.2; extra == "dev"
27
27
  Requires-Dist: pytest<9.0,>=7.0; extra == "dev"
@@ -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.3.7 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.8 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
 
@@ -157,7 +157,7 @@ Cloud status check.
157
157
  To reproduce this release exactly:
158
158
 
159
159
  ```bash
160
- pip install -U bilinc==2.3.7
160
+ pip install -U bilinc==2.3.8
161
161
  ```
162
162
 
163
163
  If you prefer a browser guide, open https://bilinc.space/install and follow the
@@ -191,7 +191,7 @@ bilinc mcp install # plain mcpServers JSON for any othe
191
191
  `command` is the interpreter that has Bilinc installed, and the adapter reads the key saved by `bilinc login`, so
192
192
  the config carries no key. On a machine without a saved key, add `"env": { "BILINC_API_KEY": "bil_live_..." }`.
193
193
 
194
- Eight tools — the core memory lifecycle, and nothing else:
194
+ Eleven tools: the eight-step memory lifecycle plus three review tools.
195
195
 
196
196
  | Tool | What it does |
197
197
  | --- | --- |
@@ -203,8 +203,11 @@ Eight tools — the core memory lifecycle, and nothing else:
203
203
  | `snapshot` | Checkpoint a project before risky work, or list existing checkpoints. |
204
204
  | `diff` | Compare a checkpoint against another checkpoint or current state. Values are redacted by default. |
205
205
  | `rollback` | **Destructive in execute mode.** Restore a checkpoint through a free preview plus an explicitly confirmed execute. |
206
+ | `list_memories` | Browse what is stored, ordered by key, with prefix, type and updated-time filters. Read-only and free. |
207
+ | `history` | Show one memory's recorded changes, newest first, with the value before and after. Read-only and free. |
208
+ | `confirm` | Record that a memory is still accurate without resending its value. Costs one write. |
206
209
 
207
- Operator and debug tooling — health probes, benchmarks, export/import, workspace replay — stays
210
+ Operator and debug tooling — health probes, benchmarks, bulk import, workspace replay — stays
208
211
  local-only, as do the epistemic read tools for claims, contradictions, and graph queries. The hosted
209
212
  adapter does not bundle local runtime internals.
210
213
 
@@ -243,6 +246,24 @@ client.status() # what can this key do?
243
246
  client.health() # is the service reachable?
244
247
  ```
245
248
 
249
+ ### Review what Bilinc remembers
250
+
251
+ Listing, history and export are reads and are not billed. Confirming costs one write.
252
+
253
+ ```python
254
+ page = client.list_memories(prefix="agent.", limit=50) # one page, ordered by key
255
+ for entry in client.iter_memories(memory_type="semantic"): # every page
256
+ print(entry["key"], entry["updatedAt"])
257
+
258
+ client.history("agent.goal") # newest changes first (20 by default, up to 100); "truncated" marks older ones
259
+ client.confirm("agent.goal", expected_version=page["entries"][0]["entryVersion"]) # still true
260
+
261
+ backup = client.export() # every stored memory with its full value, as one dict
262
+ ```
263
+
264
+ Forgetting a memory removes its value from history as well: earlier values of a forgotten
265
+ memory are not returned.
266
+
246
267
  For server, CI, and hosted agent runtimes, store the key as `BILINC_API_KEY`.
247
268
 
248
269
  ## CLI
@@ -260,6 +281,16 @@ bilinc forget --key agent.goal --reason "superseded by the planner service"
260
281
  bilinc doctor
261
282
  ```
262
283
 
284
+ Review what is stored:
285
+
286
+ ```bash
287
+ bilinc list --prefix agent. # table of keys, types, update times, value previews
288
+ bilinc list --all --json # every page as JSON
289
+ bilinc history agent.goal # recorded changes, newest first
290
+ bilinc confirm agent.goal # still accurate; the value is unchanged
291
+ bilinc export -o bilinc-export.json # full values; the file is readable only by you
292
+ ```
293
+
263
294
  Rollback is two stages. Execute takes the token from the preview and never prompts interactively,
264
295
  so it stays safe inside automation:
265
296
 
@@ -1,5 +1,5 @@
1
1
  certifi>=2024.2.2
2
- mcp<2.0,>=1.0.0
2
+ mcp<2.0,>=1.10.0
3
3
 
4
4
  [dev]
5
5
  build<2.0,>=1.2
@@ -12,8 +12,8 @@ import pytest
12
12
  def test_public_api_is_cloud_only():
13
13
  import bilinc
14
14
 
15
- assert bilinc.__version__ == "2.3.7"
16
- assert bilinc.version == "2.3.7"
15
+ assert bilinc.__version__ == "2.3.8"
16
+ assert bilinc.version == "2.3.8"
17
17
  assert "utm_campaign=activation_2_3_4" in bilinc.ACTIVATION_SIGNUP_URL
18
18
  assert hasattr(bilinc, "Bilinc")
19
19
  assert hasattr(bilinc, "CloudClient")
@@ -32,7 +32,7 @@ def test_declared_wheel_packages_stay_cloud_only():
32
32
  pyproject = tomllib.load(handle)
33
33
 
34
34
  assert pyproject["tool"]["setuptools"]["packages"] == ["bilinc", "bilinc.cli"]
35
- assert set(pyproject["project"]["dependencies"]) == {"certifi>=2024.2.2", "mcp>=1.0.0,<2.0"}
35
+ assert set(pyproject["project"]["dependencies"]) == {"certifi>=2024.2.2", "mcp>=1.10.0,<2.0"}
36
36
 
37
37
 
38
38
  class RecordingTransport:
@@ -110,9 +110,9 @@ def test_cloud_client_commit_posts_to_hosted_api():
110
110
  "headers": {
111
111
  "Authorization": "Bearer bil_live_example",
112
112
  "Content-Type": "application/json",
113
- "User-Agent": "bilinc-python/2.3.7",
113
+ "User-Agent": "bilinc-python/2.3.8",
114
114
  "X-Bilinc-Client": "python-sdk",
115
- "X-Bilinc-Client-Version": "2.3.7",
115
+ "X-Bilinc-Client-Version": "2.3.8",
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_example",
191
- "User-Agent": "bilinc-python/2.3.7",
191
+ "User-Agent": "bilinc-python/2.3.8",
192
192
  },
193
193
  "body": None,
194
194
  "timeout": 30.0,
File without changes
File without changes
File without changes
File without changes
File without changes