brainpick 0.4.2__tar.gz → 0.4.4__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.
- {brainpick-0.4.2 → brainpick-0.4.4}/PKG-INFO +1 -1
- {brainpick-0.4.2 → brainpick-0.4.4}/pyproject.toml +1 -1
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/__init__.py +1 -1
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_skill/SKILL.md +22 -1
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/cli.py +22 -8
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/mcp_server.py +65 -16
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick.egg-info/PKG-INFO +1 -1
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_e2e_mcp.py +8 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_e2e_serve.py +1 -1
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_federation.py +5 -1
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_mcp_tools.py +103 -5
- {brainpick-0.4.2 → brainpick-0.4.4}/README.md +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/setup.cfg +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/__main__.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/assets/Editor-x4G_0KV0.js +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/assets/index-CTcBQZsl.js +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/assets/index-Dn6TTKMy.css +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/assets/virtual_pwa-register-Cb81Y-Xh.js +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/assets/workbox-window.prod.es5-BBnX5xw4.js +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/assets/worker-CVB4_RMu.js +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/icons/apple-touch-icon.png +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/icons/favicon.svg +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/icons/icon-192.png +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/icons/icon-512.png +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/icons/icon-maskable-512.png +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/index.html +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/manifest.webmanifest +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/sw.js +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/workbox-a3c94b52.js +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/auth.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/compile/__init__.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/compile/pipeline.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/compile/similarity_gaps.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/compile/t1.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/compile/t2.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/compile/t3.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/config.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/core/__init__.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/core/bundle.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/core/canonical.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/core/frontmatter.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/core/fs.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/core/links.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/deltas.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/detect.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/embed.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/federation.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/integrate.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/kg.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/kgadapt/__init__.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/kgadapt/algorithmic.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/kgadapt/protocol.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/llm.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/merge.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/query/__init__.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/query/keyword.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/query/present.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/query/router.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/query/vectors.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/scaffold.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/serve/__init__.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/serve/app.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/serve/live.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/serve/rest.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/serve/state.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/serve/watcher.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/timeline.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/vectorstore.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick.egg-info/SOURCES.txt +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick.egg-info/dependency_links.txt +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick.egg-info/entry_points.txt +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick.egg-info/requires.txt +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick.egg-info/top_level.txt +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_auth.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_bundle.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_cli.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_config.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_conformance.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_deltas.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_detect.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_frontmatter.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_integrate.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_keyword.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_kg.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_links.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_llm.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_merge.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_pipeline.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_present.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_report.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_router.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_scaffold.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_serve_state.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_similarity_gaps.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_similarity_gaps_stage.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_t1.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_t2_chunker.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_t2_embed.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_t2_stage.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_t2_store.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_t3_algorithmic.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_t3_backend.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_t3_export.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_t3_stage.py +0 -0
- {brainpick-0.4.2 → brainpick-0.4.4}/tests/test_timeline.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "brainpick"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.4"
|
|
8
8
|
description = "A turn-key brain stack for agents: compile an OKF markdown bundle into a queryable, servable, visualizable brain."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -17,7 +17,7 @@ the brain comes up short.
|
|
|
17
17
|
|
|
18
18
|
Start every session with one call to get oriented, then search.
|
|
19
19
|
|
|
20
|
-
## The
|
|
20
|
+
## The six MCP tools
|
|
21
21
|
|
|
22
22
|
Call these if a `brainpick` MCP server is connected (tools are named `brain_*`):
|
|
23
23
|
|
|
@@ -26,6 +26,7 @@ Call these if a `brainpick` MCP server is connected (tools are named `brain_*`):
|
|
|
26
26
|
- `brain_read({doc})` — open one doc. `doc` is forgiving: a path (`kuu.md`), a bare stem (`kuu`), or an approximate title. Pass `sections:["Heading"]` to read just parts.
|
|
27
27
|
- `brain_neighbors({doc})` — walk the links around a doc (`depth` 1–3). Find what connects to what.
|
|
28
28
|
- `brain_write({doc, content})` — add a doc, guarded by the repo's contract. See "Writing" below.
|
|
29
|
+
- `brain_show({nodes})` — spotlight a subgraph live in an open brainpick UI: highlight nodes, fly the camera, caption it. "Let me explain" becomes "let me show you." See "Showing the brain" below — **it needs a running `brainpick serve`, unlike the other five.**
|
|
29
30
|
|
|
30
31
|
Every result carries a `hint` naming a sensible next call. Follow it.
|
|
31
32
|
|
|
@@ -62,6 +63,26 @@ federated:
|
|
|
62
63
|
- `brain_write` with a bare target writes to the project you are in; qualify it to
|
|
63
64
|
write elsewhere. It never guesses.
|
|
64
65
|
|
|
66
|
+
## Showing the brain (brain_show)
|
|
67
|
+
|
|
68
|
+
Use `brain_show` when explaining beats showing — "let me show you" instead of a
|
|
69
|
+
wall of text. It highlights nodes, flies the camera, and drops a caption into
|
|
70
|
+
every **connected** brainpick UI. It never writes the brain.
|
|
71
|
+
|
|
72
|
+
**It needs a running `brainpick serve`.** The other five tools work over a plain
|
|
73
|
+
stdio MCP connection (`claude mcp add brainpick -- brainpick mcp --root <bundle>`,
|
|
74
|
+
the common setup) because they only read compiled artifacts. `brain_show`
|
|
75
|
+
broadcasts to a live browser — and a stdio-connected MCP server has none attached;
|
|
76
|
+
it builds its own private, unshared state. Call it over stdio today and you get
|
|
77
|
+
back `{"ok": true, "shown": N}` while nothing appears anywhere — a false positive,
|
|
78
|
+
not a "no UI open" signal.
|
|
79
|
+
|
|
80
|
+
To actually see it: start `brainpick serve --root <bundle>` first (a browser tab
|
|
81
|
+
open to it, or `POST /api/show` with the same body, both work), or connect your
|
|
82
|
+
MCP client over HTTP to that server's `/mcp` endpoint instead of stdio. If you
|
|
83
|
+
called `brain_show` and the human says they saw nothing, don't trust the `"ok"`
|
|
84
|
+
— ask what's actually running before repeating the call.
|
|
85
|
+
|
|
65
86
|
## Writing knowledge back (brain_write)
|
|
66
87
|
|
|
67
88
|
Only write when asked to record knowledge. Follow the wiki's conventions or the
|
|
@@ -229,10 +229,24 @@ def _cmd_integrate(args: argparse.Namespace) -> int:
|
|
|
229
229
|
return run_integrate(args.target, Path(args.root), dry_run=args.dry_run)
|
|
230
230
|
|
|
231
231
|
|
|
232
|
-
def
|
|
232
|
+
def connectable_host(host: str) -> str:
|
|
233
|
+
"""A server may bind 0.0.0.0/:: (everywhere); a client must connect via a
|
|
234
|
+
real address — shared by the CLI's `show` and brain_show's server-proxy
|
|
235
|
+
attempt (mcp_server.py) so both resolve a running `brainpick serve` the
|
|
236
|
+
same way."""
|
|
237
|
+
return "127.0.0.1" if host in ("0.0.0.0", "::") else host
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def post_show(base_url: str, body: dict,
|
|
241
|
+
token: str | None = None) -> tuple[dict | None, str | None, bool]:
|
|
233
242
|
"""POST a presentation body to a running server's /api/show (spec/95). Returns
|
|
234
|
-
(response, None) or (None, instruction) — the
|
|
235
|
-
resolving locally: the live server resolves and
|
|
243
|
+
(response, None, False) on success, or (None, instruction, unreachable) — the
|
|
244
|
+
caller is a client here, never resolving locally: the live server resolves and
|
|
245
|
+
broadcasts to open UIs. `unreachable` is True only when nothing answered at all
|
|
246
|
+
(connection refused/DNS/timeout) — a caller that wants to fall back to a local,
|
|
247
|
+
UI-less presentation must check it, so a server that's up but REJECTS the
|
|
248
|
+
request (bad auth, bad body) is never mistaken for "no server running" and
|
|
249
|
+
silently papered over."""
|
|
236
250
|
import json
|
|
237
251
|
import urllib.error
|
|
238
252
|
import urllib.request
|
|
@@ -244,17 +258,17 @@ def post_show(base_url: str, body: dict, token: str | None = None) -> tuple[dict
|
|
|
244
258
|
request = urllib.request.Request(f"{base_url}/api/show", data=data, headers=headers, method="POST")
|
|
245
259
|
try:
|
|
246
260
|
with urllib.request.urlopen(request, timeout=10) as response: # noqa: S310 (localhost control-plane)
|
|
247
|
-
return json.loads(response.read().decode("utf-8")), None
|
|
261
|
+
return json.loads(response.read().decode("utf-8")), None, False
|
|
248
262
|
except urllib.error.HTTPError as error:
|
|
249
263
|
detail = error.read().decode("utf-8", errors="replace")
|
|
250
264
|
try:
|
|
251
265
|
message = json.loads(detail).get("error", detail)
|
|
252
266
|
except (ValueError, AttributeError):
|
|
253
267
|
message = detail
|
|
254
|
-
return None, f"the server rejected the presentation ({error.code}): {message}"
|
|
268
|
+
return None, f"the server rejected the presentation ({error.code}): {message}", False
|
|
255
269
|
except urllib.error.URLError as error:
|
|
256
270
|
return None, (f"no brainpick server at {base_url} — start one with 'brainpick serve' "
|
|
257
|
-
f"({error.reason})")
|
|
271
|
+
f"({error.reason})"), True
|
|
258
272
|
|
|
259
273
|
|
|
260
274
|
def _cmd_show(args: argparse.Namespace) -> int:
|
|
@@ -265,7 +279,7 @@ def _cmd_show(args: argparse.Namespace) -> int:
|
|
|
265
279
|
config = load_config(root)
|
|
266
280
|
host = args.host or config.serve.host
|
|
267
281
|
port = args.port or config.serve.port
|
|
268
|
-
display_host =
|
|
282
|
+
display_host = connectable_host(host)
|
|
269
283
|
base_url = f"http://{display_host}:{port}"
|
|
270
284
|
token = args.token or (config.serve.token or None)
|
|
271
285
|
|
|
@@ -279,7 +293,7 @@ def _cmd_show(args: argparse.Namespace) -> int:
|
|
|
279
293
|
if args.clear:
|
|
280
294
|
body["clear"] = True
|
|
281
295
|
|
|
282
|
-
result, error = post_show(base_url, body, token)
|
|
296
|
+
result, error, _unreachable = post_show(base_url, body, token)
|
|
283
297
|
if error is not None:
|
|
284
298
|
if args.json:
|
|
285
299
|
print(to_json({"error": error, "hint": "start the server with: brainpick serve"}))
|
|
@@ -575,38 +575,87 @@ def _single_write(state: ServeState, doc: str, content: str, mode: str = "create
|
|
|
575
575
|
# -- brain_show ----------------------------------------------------------------------
|
|
576
576
|
|
|
577
577
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
578
|
+
NO_SERVER_CAVEAT = (" No brainpick serve is running for this bundle, so nothing is actually "
|
|
579
|
+
"visible yet — start one with `brainpick serve --root <bundle>`.")
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def show_hint(shown: int, dropped: list[str], cleared: bool, server_reachable: bool = True) -> str:
|
|
583
|
+
"""The 'what to do next' line for brain_show (spec/95 small-LLM ergonomics).
|
|
584
|
+
`server_reachable` is False only for the local-fallback path (no running
|
|
585
|
+
`brainpick serve` answered) — the one case where "live in every open UI" would
|
|
586
|
+
otherwise be a flat lie, so it earns an honest caveat."""
|
|
582
587
|
if cleared:
|
|
583
|
-
|
|
584
|
-
shown
|
|
585
|
-
|
|
586
|
-
return ("nothing resolved — no name matched a doc or entity; "
|
|
588
|
+
base = "cleared — every open UI dropped its spotlight and caption."
|
|
589
|
+
elif shown == 0 and dropped:
|
|
590
|
+
base = ("nothing resolved — no name matched a doc or entity; "
|
|
587
591
|
"check them with brain_search, then brain_show again.")
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
+
else:
|
|
593
|
+
base = (f"showing {shown} node(s) live in every open UI — "
|
|
594
|
+
"call brain_show again to change it, or with clear:true to dismiss.")
|
|
595
|
+
if dropped:
|
|
596
|
+
base += f" (dropped {len(dropped)}: {', '.join(dropped)})"
|
|
597
|
+
if not server_reachable:
|
|
598
|
+
base += NO_SERVER_CAVEAT
|
|
592
599
|
return base
|
|
593
600
|
|
|
594
601
|
|
|
602
|
+
def _proxy_show(state: ServeState, body: dict) -> dict | None:
|
|
603
|
+
"""Try to hand this presentation to an already-running `brainpick serve` for
|
|
604
|
+
the same bundle root, the same way `brainpick show` (the CLI) already does —
|
|
605
|
+
it broadcasts to a REAL UI, unlike the stdio MCP process's own private,
|
|
606
|
+
unattached ServeState. Returns the server's result (with our hint attached),
|
|
607
|
+
an error dict if a server answered but rejected the request, or None if no
|
|
608
|
+
server answered at all — the caller's cue to fall back to the local, UI-less
|
|
609
|
+
presentation (spec/95 follow-up: this proxy is Python-engine-only for now,
|
|
610
|
+
the Node twin still only does the local fallback — see NO_SERVER_CAVEAT)."""
|
|
611
|
+
from brainpick.cli import connectable_host, post_show
|
|
612
|
+
|
|
613
|
+
base_url = f"http://{connectable_host(state.config.serve.host)}:{state.config.serve.port}"
|
|
614
|
+
token = state.config.serve.token or None
|
|
615
|
+
result, error, unreachable = post_show(base_url, body, token)
|
|
616
|
+
if result is not None:
|
|
617
|
+
result["hint"] = show_hint(result["shown"], result["dropped"], cleared=bool(body.get("clear")))
|
|
618
|
+
return result
|
|
619
|
+
if unreachable:
|
|
620
|
+
return None
|
|
621
|
+
return {"ok": False, "dropped": [], "error": error,
|
|
622
|
+
"hint": "a brainpick serve is running but rejected the presentation — fix the error above, then try again."}
|
|
623
|
+
|
|
624
|
+
|
|
595
625
|
def _single_show(state: ServeState, nodes: list[str] | None = None, focus: str | None = None,
|
|
596
626
|
mode: str | None = None, annotation: str | None = None,
|
|
597
627
|
clear: bool = False) -> dict:
|
|
598
|
-
"""brain_show's MCP result (spec/95):
|
|
599
|
-
|
|
600
|
-
|
|
628
|
+
"""brain_show's MCP result (spec/95): proxy to a running `brainpick serve` when
|
|
629
|
+
one answers (it does the real resolving + broadcasting); otherwise fall back to
|
|
630
|
+
resolving + broadcasting on this process's own orphaned state, honestly caveated
|
|
631
|
+
since nothing is actually watching it. Never writes — not behind [serve] writes,
|
|
632
|
+
only the normal auth. Forgiving: unresolved nodes are dropped, listed."""
|
|
633
|
+
body: dict = {}
|
|
634
|
+
if nodes is not None:
|
|
635
|
+
body["nodes"] = nodes
|
|
636
|
+
if focus is not None:
|
|
637
|
+
body["focus"] = focus
|
|
638
|
+
if mode is not None:
|
|
639
|
+
body["mode"] = mode
|
|
640
|
+
if annotation is not None:
|
|
641
|
+
body["annotation"] = annotation
|
|
642
|
+
if clear:
|
|
643
|
+
body["clear"] = True
|
|
644
|
+
proxied = _proxy_show(state, body)
|
|
645
|
+
if proxied is not None:
|
|
646
|
+
return proxied
|
|
647
|
+
|
|
601
648
|
presentation, dropped = state.present(
|
|
602
649
|
nodes=nodes, focus=focus, mode=mode, annotation=annotation, clear=clear,
|
|
603
650
|
)
|
|
651
|
+
cleared = (not presentation["nodes"] and presentation["focus"] is None
|
|
652
|
+
and presentation["mode"] is None and presentation["annotation"] is None)
|
|
604
653
|
return {
|
|
605
654
|
"ok": True,
|
|
606
655
|
"shown": len(presentation["nodes"]),
|
|
607
656
|
"dropped": dropped,
|
|
608
657
|
"seq": presentation["seq"],
|
|
609
|
-
"hint": show_hint(presentation, dropped),
|
|
658
|
+
"hint": show_hint(len(presentation["nodes"]), dropped, cleared, server_reachable=False),
|
|
610
659
|
}
|
|
611
660
|
|
|
612
661
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
+
import socket
|
|
5
6
|
import sys
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
@@ -103,6 +104,13 @@ async def _scenario(root):
|
|
|
103
104
|
|
|
104
105
|
|
|
105
106
|
def test_mcp_stdio_roundtrip(kotiaurinko):
|
|
107
|
+
# brain_show now tries a real `brainpick serve` first (spec/95 follow-up) — pin
|
|
108
|
+
# it to a port nothing binds, so this doesn't depend on whatever else happens to
|
|
109
|
+
# be listening on the default :4747 on the machine running the suite.
|
|
110
|
+
with socket.socket() as probe:
|
|
111
|
+
probe.bind(("127.0.0.1", 0))
|
|
112
|
+
free_port = probe.getsockname()[1]
|
|
113
|
+
(kotiaurinko / "brainpick.toml").write_text(f"[serve]\nport = {free_port}\n", encoding="utf-8")
|
|
106
114
|
run_compile(kotiaurinko)
|
|
107
115
|
_run_scenario(_scenario(kotiaurinko))
|
|
108
116
|
text = (kotiaurinko / "uusi-kivi.md").read_text(encoding="utf-8")
|
|
@@ -107,7 +107,7 @@ def test_sse_frame_format():
|
|
|
107
107
|
def test_health(kotiaurinko):
|
|
108
108
|
with TestClient(make_app(kotiaurinko)) as client:
|
|
109
109
|
assert client.get("/api/health").json() == {
|
|
110
|
-
"impl": "python", "name": "brainpick", "spec_version": "0.1", "version": "0.4.
|
|
110
|
+
"impl": "python", "name": "brainpick", "spec_version": "0.1", "version": "0.4.4",
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
|
|
@@ -352,7 +352,11 @@ def test_write_targets_here_or_declines(tmp_path):
|
|
|
352
352
|
assert result["ok"] is True and result["path"] == "aurinko:uusi-kivi.md"
|
|
353
353
|
|
|
354
354
|
|
|
355
|
-
def test_show_targets_one_brain_and_drops_the_rest(tmp_path):
|
|
355
|
+
def test_show_targets_one_brain_and_drops_the_rest(tmp_path, monkeypatch):
|
|
356
|
+
# no running `brainpick serve` here — pin brain_show to its local fallback so
|
|
357
|
+
# this doesn't silently depend on whatever happens to be on :4747 (spec/95 follow-up)
|
|
358
|
+
monkeypatch.setattr("brainpick.cli.post_show",
|
|
359
|
+
lambda base_url, body, token=None: (None, "no server", True))
|
|
356
360
|
brain_set = make_set(tmp_path)
|
|
357
361
|
result = show_payload(brain_set, nodes=["kirja:kahvi.md", "aurinko:kuu.md"])
|
|
358
362
|
assert result["shown"] == 1 and result["dropped"] == ["aurinko:kuu.md"]
|
|
@@ -539,9 +539,21 @@ def test_write_henxels_contract_at_repo_root_governs_bundle(monkeypatch, tmp_pat
|
|
|
539
539
|
# -- brain_show (spec/95): the 6th tool — ephemeral presentations, not write-gated --
|
|
540
540
|
|
|
541
541
|
|
|
542
|
-
def
|
|
542
|
+
def _no_server(monkeypatch, reason="[Errno 111] Connection refused"):
|
|
543
|
+
"""Make brain_show's proxy attempt behave as if no `brainpick serve` is
|
|
544
|
+
listening — every existing local-fallback test needs this so it doesn't
|
|
545
|
+
silently depend on whatever happens to be bound to 127.0.0.1:4747 on the
|
|
546
|
+
machine running the suite (spec/95 follow-up: the stdio MCP process has no
|
|
547
|
+
server of its own to ask, so it must genuinely try one over HTTP first)."""
|
|
548
|
+
def fake_post_show(base_url, body, token=None):
|
|
549
|
+
return None, f"no brainpick server at {base_url} — start one with 'brainpick serve' ({reason})", True
|
|
550
|
+
monkeypatch.setattr("brainpick.cli.post_show", fake_post_show)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def test_show_payload_reports_shown_dropped_seq_and_hint(kotiaurinko, monkeypatch):
|
|
543
554
|
from brainpick.mcp_server import show_payload
|
|
544
555
|
|
|
556
|
+
_no_server(monkeypatch)
|
|
545
557
|
state = make_state(kotiaurinko)
|
|
546
558
|
queue = state.subscribe()
|
|
547
559
|
result = show_payload(state, nodes=["aurinko.md", "ei-ole"], annotation="hi")
|
|
@@ -549,27 +561,113 @@ def test_show_payload_reports_shown_dropped_seq_and_hint(kotiaurinko):
|
|
|
549
561
|
assert result["shown"] == 1
|
|
550
562
|
assert result["dropped"] == ["ei-ole"]
|
|
551
563
|
assert result["seq"] == 1
|
|
552
|
-
# the exact hint — pinned so the Node twin stays byte-identical (cross-engine parity)
|
|
564
|
+
# the exact hint — pinned so the Node twin stays byte-identical (cross-engine parity),
|
|
565
|
+
# aside from the trailing no-server caveat (Python-only for now, see mcp_server.py)
|
|
553
566
|
assert result["hint"] == (
|
|
554
567
|
"showing 1 node(s) live in every open UI — "
|
|
555
568
|
"call brain_show again to change it, or with clear:true to dismiss. (dropped 1: ei-ole)"
|
|
569
|
+
" No brainpick serve is running for this bundle, so nothing is actually visible yet"
|
|
570
|
+
" — start one with `brainpick serve --root <bundle>`."
|
|
556
571
|
)
|
|
557
|
-
#
|
|
572
|
+
# local fallback still updates its own (UI-less) state so the CLI/tests can introspect it
|
|
558
573
|
assert [name for name, _, _ in drain(queue)] == ["brain.show"]
|
|
559
574
|
assert state.seq == 1
|
|
560
575
|
|
|
561
576
|
|
|
562
|
-
def test_show_payload_clear_has_a_dedicated_hint(kotiaurinko):
|
|
577
|
+
def test_show_payload_clear_has_a_dedicated_hint(kotiaurinko, monkeypatch):
|
|
563
578
|
from brainpick.mcp_server import show_payload
|
|
564
579
|
|
|
580
|
+
_no_server(monkeypatch)
|
|
565
581
|
state = make_state(kotiaurinko)
|
|
566
582
|
result = show_payload(state, clear=True)
|
|
567
583
|
assert result == {
|
|
568
584
|
"ok": True, "shown": 0, "dropped": [], "seq": 1,
|
|
569
|
-
"hint": "cleared — every open UI dropped its spotlight and caption."
|
|
585
|
+
"hint": ("cleared — every open UI dropped its spotlight and caption."
|
|
586
|
+
" No brainpick serve is running for this bundle, so nothing is actually visible yet"
|
|
587
|
+
" — start one with `brainpick serve --root <bundle>`."),
|
|
570
588
|
}
|
|
571
589
|
|
|
572
590
|
|
|
591
|
+
def test_show_payload_proxies_to_a_running_server_and_never_touches_local_state(kotiaurinko, monkeypatch):
|
|
592
|
+
"""The actual bug fix: when a `brainpick serve` answers, brain_show must use
|
|
593
|
+
ITS response (a real broadcast to a real UI) and leave the stdio process's
|
|
594
|
+
own orphaned state untouched — no local seq bump, no local broadcast."""
|
|
595
|
+
from brainpick.mcp_server import show_payload
|
|
596
|
+
|
|
597
|
+
calls = []
|
|
598
|
+
|
|
599
|
+
def fake_post_show(base_url, body, token=None):
|
|
600
|
+
calls.append((base_url, body, token))
|
|
601
|
+
return {"ok": True, "shown": 2, "dropped": [], "seq": 7}, None, False
|
|
602
|
+
|
|
603
|
+
monkeypatch.setattr("brainpick.cli.post_show", fake_post_show)
|
|
604
|
+
state = make_state(kotiaurinko)
|
|
605
|
+
queue = state.subscribe()
|
|
606
|
+
|
|
607
|
+
result = show_payload(state, nodes=["aurinko.md", "kuu"], annotation="hi", mode="brain")
|
|
608
|
+
|
|
609
|
+
assert result == {
|
|
610
|
+
"ok": True, "shown": 2, "dropped": [], "seq": 7,
|
|
611
|
+
"hint": "showing 2 node(s) live in every open UI — "
|
|
612
|
+
"call brain_show again to change it, or with clear:true to dismiss.",
|
|
613
|
+
}
|
|
614
|
+
assert len(calls) == 1
|
|
615
|
+
base_url, body, token = calls[0]
|
|
616
|
+
assert base_url == "http://127.0.0.1:4747" # the bundle's default [serve] config
|
|
617
|
+
assert body == {"nodes": ["aurinko.md", "kuu"], "annotation": "hi", "mode": "brain"}
|
|
618
|
+
assert token is None # no token configured for this fixture bundle
|
|
619
|
+
# the orphaned local state was never touched — the real server did the broadcasting
|
|
620
|
+
assert drain(queue) == []
|
|
621
|
+
assert state.presentation_seq == 0
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def test_show_payload_normalizes_a_wildcard_bind_to_a_connectable_host(kotiaurinko, monkeypatch):
|
|
625
|
+
"""A server can bind 0.0.0.0 (everywhere); a client can't connect TO that
|
|
626
|
+
address — must resolve to 127.0.0.1 before POSTing, same as `brainpick show`."""
|
|
627
|
+
from brainpick.config import load_config
|
|
628
|
+
from brainpick.mcp_server import show_payload
|
|
629
|
+
from brainpick.serve.state import ServeState
|
|
630
|
+
|
|
631
|
+
calls = []
|
|
632
|
+
monkeypatch.setattr(
|
|
633
|
+
"brainpick.cli.post_show",
|
|
634
|
+
lambda base_url, body, token=None: (calls.append(base_url), ({"ok": True, "shown": 0, "dropped": [], "seq": 1}, None, False))[1],
|
|
635
|
+
)
|
|
636
|
+
config = load_config(kotiaurinko)
|
|
637
|
+
config.serve.host = "0.0.0.0"
|
|
638
|
+
state = ServeState(kotiaurinko, config)
|
|
639
|
+
state.load()
|
|
640
|
+
|
|
641
|
+
show_payload(state, clear=True)
|
|
642
|
+
|
|
643
|
+
assert calls == ["http://127.0.0.1:4747"]
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def test_show_payload_reports_a_server_side_rejection_without_touching_local_state(kotiaurinko, monkeypatch):
|
|
647
|
+
"""A running server that REJECTS the presentation (e.g. a bad bearer token
|
|
648
|
+
on a non-local bind) must surface as an error, never silently paper over it
|
|
649
|
+
by falling back to updating the orphaned local state — that would hide a
|
|
650
|
+
real misconfiguration behind a false 'ok'."""
|
|
651
|
+
from brainpick.mcp_server import show_payload
|
|
652
|
+
|
|
653
|
+
def fake_post_show(base_url, body, token=None):
|
|
654
|
+
return None, "the server rejected the presentation (401): auth required", False
|
|
655
|
+
|
|
656
|
+
monkeypatch.setattr("brainpick.cli.post_show", fake_post_show)
|
|
657
|
+
state = make_state(kotiaurinko)
|
|
658
|
+
queue = state.subscribe()
|
|
659
|
+
|
|
660
|
+
result = show_payload(state, nodes=["aurinko.md"])
|
|
661
|
+
|
|
662
|
+
assert result == {
|
|
663
|
+
"ok": False, "dropped": [],
|
|
664
|
+
"error": "the server rejected the presentation (401): auth required",
|
|
665
|
+
"hint": "a brainpick serve is running but rejected the presentation — fix the error above, then try again.",
|
|
666
|
+
}
|
|
667
|
+
assert drain(queue) == []
|
|
668
|
+
assert state.presentation_seq == 0
|
|
669
|
+
|
|
670
|
+
|
|
573
671
|
def test_brain_show_registered_as_sixth_tool_even_when_writes_refused(kotiaurinko):
|
|
574
672
|
import asyncio
|
|
575
673
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/assets/virtual_pwa-register-Cb81Y-Xh.js
RENAMED
|
File without changes
|
{brainpick-0.4.2 → brainpick-0.4.4}/src/brainpick/_static/assets/workbox-window.prod.es5-BBnX5xw4.js
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|