bilinc 2.6.2__tar.gz → 2.7.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.
Files changed (31) hide show
  1. {bilinc-2.6.2/src/bilinc.egg-info → bilinc-2.7.0}/PKG-INFO +7 -5
  2. {bilinc-2.6.2 → bilinc-2.7.0}/README.md +6 -4
  3. {bilinc-2.6.2 → bilinc-2.7.0}/pyproject.toml +2 -2
  4. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc/__init__.py +2 -2
  5. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc/cli/main.py +243 -0
  6. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc/client.py +223 -10
  7. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc/cloud_mcp.py +156 -3
  8. bilinc-2.7.0/src/bilinc/wiki/__init__.py +6 -0
  9. bilinc-2.7.0/src/bilinc/wiki/export.py +226 -0
  10. bilinc-2.7.0/src/bilinc/wiki/fetch.py +240 -0
  11. bilinc-2.7.0/src/bilinc/wiki/links.py +53 -0
  12. bilinc-2.7.0/src/bilinc/wiki/location.py +285 -0
  13. bilinc-2.7.0/src/bilinc/wiki/manifest.py +172 -0
  14. bilinc-2.7.0/src/bilinc/wiki/paths.py +200 -0
  15. bilinc-2.7.0/src/bilinc/wiki/render.py +431 -0
  16. bilinc-2.7.0/src/bilinc/wiki/reuse.py +157 -0
  17. bilinc-2.7.0/src/bilinc/wiki/writer.py +344 -0
  18. {bilinc-2.6.2 → bilinc-2.7.0/src/bilinc.egg-info}/PKG-INFO +7 -5
  19. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc.egg-info/SOURCES.txt +10 -0
  20. {bilinc-2.6.2 → bilinc-2.7.0}/tests/test_cloud_only_package.py +13 -6
  21. {bilinc-2.6.2 → bilinc-2.7.0}/LICENSE +0 -0
  22. {bilinc-2.6.2 → bilinc-2.7.0}/MANIFEST.in +0 -0
  23. {bilinc-2.6.2 → bilinc-2.7.0}/setup.cfg +0 -0
  24. {bilinc-2.6.2 → bilinc-2.7.0}/setup.py +0 -0
  25. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc/cli/__init__.py +0 -0
  26. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc/cli/__main__.py +0 -0
  27. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc/cli/login.py +0 -0
  28. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc.egg-info/dependency_links.txt +0 -0
  29. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc.egg-info/entry_points.txt +0 -0
  30. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc.egg-info/requires.txt +0 -0
  31. {bilinc-2.6.2 → bilinc-2.7.0}/src/bilinc.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bilinc
3
- Version: 2.6.2
3
+ Version: 2.7.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
@@ -185,6 +185,9 @@ The config carries no key; the adapter reads the one `bilinc login` saved. On a
185
185
  | `list_memories` | Browse what is stored. Free. |
186
186
  | `history` | One memory's changes, newest first. Free. |
187
187
  | `confirm` | Record that a memory is still accurate. One write. |
188
+ | `dream_start` | Start a dream pass: the entries changed since the last one, for your agent to review. Stdio adapter; coming to the hosted connector. |
189
+ | `dream_apply` | Apply the dream's merges, retirements, confirmations and flags. **Destructive**; undo with `dream_undo`. Stdio adapter; coming to the hosted connector. |
190
+ | `dream_undo` | Undo a dream in one step, for 7 days. Stdio adapter; coming to the hosted connector. |
188
191
 
189
192
  The remote connector at `https://mcp.bilinc.space/mcp` has ten tools; a read-only connection lists only the four read tools. Client guides: [Claude Code](https://bilinc.space/docs/claude-code) · [Codex](https://bilinc.space/docs/codex) · [Cursor](https://bilinc.space/docs/cursor) · [any MCP client](https://bilinc.space/for/mcp).
190
193
 
@@ -211,11 +214,10 @@ All endpoints live under `https://bilinc.space`. Memory operations need an API k
211
214
 
212
215
  Send an `Idempotency-Key` header on any write you might retry: the same key with the same payload replays the original result and is billed once; a different payload is refused with `409 idempotency_conflict`. Saves that contain a credential, such as an API key, token or private key, are refused with `secret_detected` and the value is never echoed back.
213
216
 
214
- ## What's new in 2.6.2
217
+ ## What's new in 2.7.0
215
218
 
216
- - **"What changed today?"**: recall answers time questions in English and Turkish ("since Monday", "dünkü kararlar", "son 3 günde") with the memories written in that window, newest first, in the project's time zone.
217
- - **Less noise in time questions**: sources such as raw chat transcripts can be left out of time answers unless you ask for chats.
218
- - **Nothing else moves**: queries without a time phrase are answered exactly as before.
219
+ - **Wiki export**: `bilinc wiki <dir>` writes your memory as Obsidian-compatible Markdown, one page per key, with recent changes, links between keys and an index. It is one-way, never touches files it did not write, and reruns reuse unchanged pages. `--wikilinks` writes `[[...]]` links; the stdio MCP adapter has the same export as `export_wiki`.
220
+ - **Dream pass**: your agent tidies its own memory (merges duplicates, retires stale entries, flags what a person should decide) and you can undo the pass's own changes in one step for 7 days. Bilinc runs no model for this. Available in the SDK, the CLI (`bilinc dream`) and the stdio MCP adapter; coming to the hosted connector.
219
221
 
220
222
  Every release: [changelog](https://bilinc.space/changelog).
221
223
 
@@ -137,6 +137,9 @@ The config carries no key; the adapter reads the one `bilinc login` saved. On a
137
137
  | `list_memories` | Browse what is stored. Free. |
138
138
  | `history` | One memory's changes, newest first. Free. |
139
139
  | `confirm` | Record that a memory is still accurate. One write. |
140
+ | `dream_start` | Start a dream pass: the entries changed since the last one, for your agent to review. Stdio adapter; coming to the hosted connector. |
141
+ | `dream_apply` | Apply the dream's merges, retirements, confirmations and flags. **Destructive**; undo with `dream_undo`. Stdio adapter; coming to the hosted connector. |
142
+ | `dream_undo` | Undo a dream in one step, for 7 days. Stdio adapter; coming to the hosted connector. |
140
143
 
141
144
  The remote connector at `https://mcp.bilinc.space/mcp` has ten tools; a read-only connection lists only the four read tools. Client guides: [Claude Code](https://bilinc.space/docs/claude-code) · [Codex](https://bilinc.space/docs/codex) · [Cursor](https://bilinc.space/docs/cursor) · [any MCP client](https://bilinc.space/for/mcp).
142
145
 
@@ -163,11 +166,10 @@ All endpoints live under `https://bilinc.space`. Memory operations need an API k
163
166
 
164
167
  Send an `Idempotency-Key` header on any write you might retry: the same key with the same payload replays the original result and is billed once; a different payload is refused with `409 idempotency_conflict`. Saves that contain a credential, such as an API key, token or private key, are refused with `secret_detected` and the value is never echoed back.
165
168
 
166
- ## What's new in 2.6.2
169
+ ## What's new in 2.7.0
167
170
 
168
- - **"What changed today?"**: recall answers time questions in English and Turkish ("since Monday", "dünkü kararlar", "son 3 günde") with the memories written in that window, newest first, in the project's time zone.
169
- - **Less noise in time questions**: sources such as raw chat transcripts can be left out of time answers unless you ask for chats.
170
- - **Nothing else moves**: queries without a time phrase are answered exactly as before.
171
+ - **Wiki export**: `bilinc wiki <dir>` writes your memory as Obsidian-compatible Markdown, one page per key, with recent changes, links between keys and an index. It is one-way, never touches files it did not write, and reruns reuse unchanged pages. `--wikilinks` writes `[[...]]` links; the stdio MCP adapter has the same export as `export_wiki`.
172
+ - **Dream pass**: your agent tidies its own memory (merges duplicates, retires stale entries, flags what a person should decide) and you can undo the pass's own changes in one step for 7 days. Bilinc runs no model for this. Available in the SDK, the CLI (`bilinc dream`) and the stdio MCP adapter; coming to the hosted connector.
171
173
 
172
174
  Every release: [changelog](https://bilinc.space/changelog).
173
175
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bilinc"
7
- version = "2.6.2"
7
+ version = "2.7.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"
@@ -75,7 +75,7 @@ bilinc = "bilinc.cli.main:main"
75
75
 
76
76
  [tool.setuptools]
77
77
  package-dir = {"" = "src"}
78
- packages = ["bilinc", "bilinc.cli"]
78
+ packages = ["bilinc", "bilinc.cli", "bilinc.wiki"]
79
79
 
80
80
  [tool.pytest.ini_options]
81
81
  testpaths = ["tests"]
@@ -1,4 +1,4 @@
1
- """Bilinc 2.6.2: cloud-only SDK for agent memory."""
1
+ """Bilinc 2.7.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.6.2"
28
+ __version__ = "2.7.0"
29
29
  version = __version__
30
30
 
31
31
  __all__ = [
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
 
6
6
  import argparse
7
7
  import contextlib
8
+ import http.client
8
9
  import json
9
10
  import os
10
11
  import shlex
@@ -24,6 +25,7 @@ from bilinc.client import (
24
25
  BilincValidationError,
25
26
  CloudClient,
26
27
  INSTALL_URL,
28
+ MAX_HISTORY_LIMIT,
27
29
  MEMORY_TYPES,
28
30
  SIGNUP_URL,
29
31
  VALUE_MODES,
@@ -32,6 +34,7 @@ from bilinc.client import (
32
34
  save_config_api_key,
33
35
  )
34
36
  from bilinc.cli.login import LoginError, device_login, is_headless, loopback_login
37
+ from bilinc.wiki import WikiExportError, export_wiki, summary_line
35
38
 
36
39
 
37
40
  def _parse_value(value: str) -> Any:
@@ -169,6 +172,194 @@ def _write_private_json(path: str, payload: dict[str, Any]) -> None:
169
172
  raise
170
173
 
171
174
 
175
+ def _history_count(text: str) -> int:
176
+ try:
177
+ value = int(text)
178
+ except ValueError:
179
+ raise argparse.ArgumentTypeError(f"must be a whole number from 0 to {MAX_HISTORY_LIMIT}") from None
180
+ if not 0 <= value <= MAX_HISTORY_LIMIT:
181
+ raise argparse.ArgumentTypeError(f"must be a whole number from 0 to {MAX_HISTORY_LIMIT}")
182
+ return value
183
+
184
+
185
+ def _name_some(keys: list[str], limit: int = 20) -> str:
186
+ shown = ", ".join(keys[:limit])
187
+ return shown + (f", … ({len(keys) - limit} more)" if len(keys) > limit else "")
188
+
189
+
190
+ def _read_dream_actions(
191
+ parser: argparse.ArgumentParser, source: str
192
+ ) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
193
+ """Load `{"actions": [...], "report": {...}}` or a bare action list; bad input is a usage error."""
194
+ try:
195
+ if source == "-":
196
+ text = sys.stdin.read()
197
+ else:
198
+ with open(source, encoding="utf-8") as handle:
199
+ text = handle.read()
200
+ except OSError as exc:
201
+ parser.error(f"dream apply: cannot read {source}: {exc.strerror or exc}")
202
+ try:
203
+ payload = json.loads(text)
204
+ except json.JSONDecodeError as exc:
205
+ parser.error(f"dream apply: {source} is not valid JSON ({exc.msg}, line {exc.lineno})")
206
+ report = None
207
+ if isinstance(payload, dict):
208
+ actions = payload.get("actions")
209
+ report = payload.get("report")
210
+ if report is not None and not isinstance(report, dict):
211
+ parser.error("dream apply: report must be a JSON object")
212
+ else:
213
+ actions = payload
214
+ if not isinstance(actions, list):
215
+ parser.error('dream apply: expected {"actions": [...]} or a JSON list of actions')
216
+ return actions, report
217
+
218
+
219
+ def _dream_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
220
+ commands = next(action for action in parser._actions if isinstance(action, argparse._SubParsersAction))
221
+ return commands.choices["dream"]
222
+
223
+
224
+ def _print_dream_start(result: dict[str, Any]) -> None:
225
+ if not result.get("dream_id"):
226
+ print(f"Nothing changed since {result.get('since') or 'the last dream'}; no dream started.")
227
+ return
228
+ candidates = result.get("candidates") or []
229
+ count = result.get("candidate_count", len(candidates))
230
+ window = result.get("undo_window_days", 7)
231
+ print(
232
+ f"dream {result['dream_id']}: {count} candidates, forget cap {result.get('forget_cap', 0)}, "
233
+ f"undo window {window} days"
234
+ )
235
+ for item in candidates:
236
+ print(
237
+ f"{item.get('key')} [{item.get('memory_type') or '-'}] "
238
+ f"{_short_time(item.get('updated_at'))} {item.get('entry_version') or '-'}"
239
+ )
240
+ if result.get("truncated"):
241
+ print("(more candidates than shown; run again after this dream, or narrow with --prefix)")
242
+
243
+
244
+ def _print_dream_results(results: list[dict[str, Any]]) -> None:
245
+ totals = {"applied": 0, "conflict": 0, "error": 0, "skipped": 0}
246
+ for row in results:
247
+ outcome = str(row.get("result") or "-")
248
+ if outcome in totals:
249
+ totals[outcome] += 1
250
+ line = f"{row.get('n')} {row.get('op')} {row.get('key')} {outcome}"
251
+ if row.get("error"):
252
+ line += f" {row['error']}"
253
+ print(line)
254
+ print(
255
+ f"applied {totals['applied']}, conflicts {totals['conflict']}, "
256
+ f"errors {totals['error']}, skipped {totals['skipped']}"
257
+ )
258
+
259
+
260
+ def _print_dream_undo(result: dict[str, Any]) -> None:
261
+ print(f"dream {result.get('dream_id')}: {result.get('status')}")
262
+ restored = result.get("restored") or []
263
+ conflicts = result.get("conflicts") or []
264
+ kept = result.get("kept") or []
265
+ print(f"restored ({len(restored)})")
266
+ for row in restored:
267
+ line = f" {row.get('n')} {row.get('op')} {row.get('key')}"
268
+ if row.get("metadata_restored") is False:
269
+ line += " (metadata not restored)"
270
+ print(line)
271
+ print(f"conflicts ({len(conflicts)})")
272
+ for row in conflicts:
273
+ print(f" {row.get('n')} {row.get('key')} {row.get('reason') or ''}".rstrip())
274
+ print(f"kept ({len(kept)})")
275
+ for row in kept:
276
+ print(f" {row.get('n')} {row.get('key')}")
277
+
278
+
279
+ def _run_dream(client: CloudClient, args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
280
+ if args.dream_command == "start":
281
+ result = client.dream_start(prefix=args.prefix, since=args.since, limit=args.limit)
282
+ if args.json:
283
+ _print(result)
284
+ else:
285
+ _print_dream_start(result)
286
+ elif args.dream_command == "apply":
287
+ actions, report = _read_dream_actions(parser, args.file)
288
+ try:
289
+ result = client.dream_apply(args.dream_id, actions, report=report)
290
+ except BilincCloudError as exc:
291
+ partial = getattr(exc, "partial", None)
292
+ if isinstance(partial, dict) and partial.get("results"):
293
+ _print_dream_results(partial["results"])
294
+ raise
295
+ _print_dream_results(result.get("results") or [])
296
+ elif args.dream_command == "undo":
297
+ result = client.dream_undo(args.dream_id)
298
+ if args.json:
299
+ _print(result)
300
+ else:
301
+ _print_dream_undo(result)
302
+ elif args.dream_command == "show":
303
+ _print(client.dream_show(args.dream_id))
304
+ return 0
305
+
306
+
307
+ def _run_wiki(client: CloudClient, args: argparse.Namespace) -> int:
308
+ """Export to a folder; one summary line on stdout, everything else on stderr."""
309
+ counted = []
310
+
311
+ def progress(done: int, total: int) -> None:
312
+ counted.append(done)
313
+ print(f"\rhistory {done}/{total}", end="", file=sys.stderr, flush=True)
314
+
315
+ try:
316
+ result = export_wiki(
317
+ client,
318
+ args.dir,
319
+ history=args.history,
320
+ prefix=args.prefix,
321
+ wikilinks=args.wikilinks,
322
+ refresh_history=args.refresh_history,
323
+ progress=progress if sys.stderr.isatty() else None,
324
+ )
325
+ except KeyboardInterrupt:
326
+ if counted:
327
+ print(file=sys.stderr)
328
+ print("bilinc: wiki export cancelled", file=sys.stderr)
329
+ return 130
330
+ except WikiExportError as exc:
331
+ if counted:
332
+ print(file=sys.stderr)
333
+ print(f"bilinc: error: {exc}", file=sys.stderr)
334
+ return 1
335
+ except (OSError, http.client.HTTPException) as exc:
336
+ # The SDK's transport lets a timeout or dropped connection while reading
337
+ # a response through unwrapped; nothing was written at that point.
338
+ if counted:
339
+ print(file=sys.stderr)
340
+ print(f"bilinc: error: Bilinc Cloud request failed: {exc}", file=sys.stderr)
341
+ return 1
342
+ if counted:
343
+ print(file=sys.stderr)
344
+ for line in result.warnings:
345
+ print(line, file=sys.stderr)
346
+ if result.history_unavailable:
347
+ count = len(result.history_unavailable)
348
+ print(
349
+ f"warning: history unavailable for {count} key{'s' if count != 1 else ''}: "
350
+ f"{_name_some(result.history_unavailable)}",
351
+ file=sys.stderr,
352
+ )
353
+ if result.skipped:
354
+ print(
355
+ "warning: left these previously exported files alone because a folder on the way "
356
+ f"is no longer a real folder: {_name_some(result.skipped)}",
357
+ file=sys.stderr,
358
+ )
359
+ print(summary_line(result, args.dir))
360
+ return 0
361
+
362
+
172
363
  def _client(args: argparse.Namespace) -> CloudClient:
173
364
  return CloudClient(api_key=args.api_key, base_url=args.base_url, timeout=args.timeout)
174
365
 
@@ -335,6 +526,51 @@ def build_parser() -> argparse.ArgumentParser:
335
526
  export.add_argument("--prefix", help="Only keys starting with this text")
336
527
  export.add_argument("--type", choices=MEMORY_TYPES, help="Only this memory type")
337
528
 
529
+ wiki_help = "Write every stored memory as Obsidian-compatible Markdown pages in a folder"
530
+ wiki = sub.add_parser("wiki", help=wiki_help, description=wiki_help)
531
+ wiki.add_argument("dir", help="Folder to write: new, empty, or a previous bilinc wiki export")
532
+ wiki.add_argument(
533
+ "--history",
534
+ type=_history_count,
535
+ default=5,
536
+ metavar="N",
537
+ help=f"Recorded changes per page, newest first (0-{MAX_HISTORY_LIMIT}, default 5; 0 skips history)",
538
+ )
539
+ wiki.add_argument("--prefix", help="Only keys starting with this text")
540
+ wiki.add_argument(
541
+ "--wikilinks",
542
+ action="store_true",
543
+ help="Write [[...]] links (Obsidian style) instead of standard Markdown links",
544
+ )
545
+ wiki.add_argument(
546
+ "--refresh-history",
547
+ action="store_true",
548
+ help="Fetch every page's history again; by default history is reused only when "
549
+ "nothing changed since the last export",
550
+ )
551
+
552
+ dream_help = "Tidy your agent's memory in a dream pass, and undo it in one step"
553
+ dream = sub.add_parser("dream", help=dream_help, description=dream_help)
554
+ dream_sub = dream.add_subparsers(dest="dream_command")
555
+ dream_start = dream_sub.add_parser("start", help="Start a dream and list the candidate entries")
556
+ dream_start.add_argument("--prefix", help="Only keys starting with this text")
557
+ dream_start.add_argument("--since", help="Only entries changed after this ISO-8601 time")
558
+ dream_start.add_argument("--limit", type=int, default=200, help="Most candidates to return (1-200)")
559
+ dream_start.add_argument("--json", action="store_true", help="Print the raw JSON response")
560
+ dream_apply = dream_sub.add_parser("apply", help="Apply a dream's actions from a JSON file or stdin")
561
+ dream_apply.add_argument("dream_id", help="The id printed by bilinc dream start")
562
+ dream_apply.add_argument(
563
+ "--file",
564
+ default="-",
565
+ help='JSON with {"actions": [...], "report": {...}} or a bare action list; - reads stdin (default)',
566
+ )
567
+ dream_undo = dream_sub.add_parser("undo", help="Undo a dream's own changes")
568
+ dream_undo.add_argument("dream_id")
569
+ dream_undo.add_argument("--json", action="store_true", help="Print the raw JSON response")
570
+ dream_show = dream_sub.add_parser("show", help="Show a dream's report")
571
+ dream_show.add_argument("dream_id")
572
+ dream_show.add_argument("--json", action="store_true", help="Print the raw JSON response (the default)")
573
+
338
574
  snapshot = sub.add_parser("snapshot", help="Create or list project checkpoints")
339
575
  snapshot.add_argument("action", choices=["create", "list"], nargs="?", default="create")
340
576
  snapshot.add_argument("--label", help="Human-readable label for a new checkpoint")
@@ -474,6 +710,9 @@ def main(argv: list[str] | None = None) -> int:
474
710
  if args.command is None:
475
711
  parser.print_help()
476
712
  return 0
713
+ if args.command == "dream" and args.dream_command is None:
714
+ _dream_parser(parser).print_help(sys.stderr)
715
+ return 2
477
716
 
478
717
  try:
479
718
  client = _client(args)
@@ -571,6 +810,10 @@ def main(argv: list[str] | None = None) -> int:
571
810
  print(f"Exported {exported['count']} memories to {args.output}", file=sys.stderr)
572
811
  else:
573
812
  _print(exported)
813
+ elif args.command == "wiki":
814
+ return _run_wiki(client, args)
815
+ elif args.command == "dream":
816
+ return _run_dream(client, args, parser)
574
817
  elif args.command == "snapshot":
575
818
  if args.action == "list":
576
819
  _print(client.list_snapshots(limit=args.limit))