brainplug 0.4.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {brainplug-0.4.0 → brainplug-0.4.1}/PKG-INFO +2 -4
  2. {brainplug-0.4.0 → brainplug-0.4.1}/README.md +1 -3
  3. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/__init__.py +1 -1
  4. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/__main__.py +5 -0
  5. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/connect.py +22 -7
  6. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug.egg-info/PKG-INFO +2 -4
  7. {brainplug-0.4.0 → brainplug-0.4.1}/pyproject.toml +1 -1
  8. {brainplug-0.4.0 → brainplug-0.4.1}/tests/test_connect.py +16 -0
  9. {brainplug-0.4.0 → brainplug-0.4.1}/tests/test_memory.py +10 -0
  10. {brainplug-0.4.0 → brainplug-0.4.1}/LICENSE +0 -0
  11. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/agent.py +0 -0
  12. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/analysis.py +0 -0
  13. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/app.py +0 -0
  14. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/brain.py +0 -0
  15. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/demo.py +0 -0
  16. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/export.py +0 -0
  17. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/memory.py +0 -0
  18. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/notion_api.py +0 -0
  19. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/pipeline.py +0 -0
  20. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/render.py +0 -0
  21. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/sources.py +0 -0
  22. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/stopwords.py +0 -0
  23. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/sync.py +0 -0
  24. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/templates/app.html +0 -0
  25. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/templates/viewer.html +0 -0
  26. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/vendor/INTER_LICENSE +0 -0
  27. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/vendor/THREE_LICENSE +0 -0
  28. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/vendor/inter-latin.woff2 +0 -0
  29. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug/vendor/three.module.min.js +0 -0
  30. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug.egg-info/SOURCES.txt +0 -0
  31. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug.egg-info/dependency_links.txt +0 -0
  32. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug.egg-info/entry_points.txt +0 -0
  33. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug.egg-info/requires.txt +0 -0
  34. {brainplug-0.4.0 → brainplug-0.4.1}/brainplug.egg-info/top_level.txt +0 -0
  35. {brainplug-0.4.0 → brainplug-0.4.1}/setup.cfg +0 -0
  36. {brainplug-0.4.0 → brainplug-0.4.1}/tests/test_brainplug.py +0 -0
  37. {brainplug-0.4.0 → brainplug-0.4.1}/tests/test_extension_package.py +0 -0
  38. {brainplug-0.4.0 → brainplug-0.4.1}/tests/test_new_sources.py +0 -0
  39. {brainplug-0.4.0 → brainplug-0.4.1}/tests/test_tools.py +0 -0
  40. {brainplug-0.4.0 → brainplug-0.4.1}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainplug
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: A second brain for you and your AI assistants: a 3D map of everything you write and a memory they plug into. 100% local.
5
5
  Author: Juan Rodríguez
6
6
  License-Expression: MIT
@@ -65,9 +65,7 @@ brainplug doctor ~/Documents/MyVault # checks that it answers, end to e
65
65
  ```
66
66
 
67
67
  `brainplug brain ~/Documents/MyVault --draft-about` writes a first draft of your "About me"
68
- note from what you have written, while it is still empty. Until the first release reaches PyPI,
69
- install it from GitHub:
70
- `uv tool install "brainplug @ git+https://github.com/juanmariarodriguez2-png/brainplug"`.
68
+ note from what you have written, while it is still empty.
71
69
  Add `--sessions` to `connect` to include what you typed to Claude Code and Codex, and put
72
70
  export files after the vault to include what you wrote before (`… MyVault chatgpt-export.zip`).
73
71
 
@@ -29,9 +29,7 @@ brainplug doctor ~/Documents/MyVault # checks that it answers, end to e
29
29
  ```
30
30
 
31
31
  `brainplug brain ~/Documents/MyVault --draft-about` writes a first draft of your "About me"
32
- note from what you have written, while it is still empty. Until the first release reaches PyPI,
33
- install it from GitHub:
34
- `uv tool install "brainplug @ git+https://github.com/juanmariarodriguez2-png/brainplug"`.
32
+ note from what you have written, while it is still empty.
35
33
  Add `--sessions` to `connect` to include what you typed to Claude Code and Codex, and put
36
34
  export files after the vault to include what you wrote before (`… MyVault chatgpt-export.zip`).
37
35
 
@@ -1,3 +1,3 @@
1
1
  """Brainplug: turn your AI chat history into a 3D map of your mind."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.1"
@@ -174,6 +174,11 @@ def main(argv: list[str] | None = None) -> int:
174
174
  vault = getattr(args, "brain", None) or getattr(args, "vault", None)
175
175
  if args.cmd == "mcp" and not args.inputs and not args.notion and not vault and not args.sessions:
176
176
  ap.error("mcp needs your vault or your files, e.g.: brainplug mcp --brain ~/Vault export.zip")
177
+ if vault and not Path(vault).expanduser().is_dir():
178
+ if args.cmd == "mcp" or args.remember or args.review or args.context or args.draft_about:
179
+ ap.error(f'{vault} does not exist. Check the path; to start a new, empty brain there, '
180
+ f'run: brainplug brain "{vault}"')
181
+ print(f"Creating a new folder for your brain: {Path(vault).expanduser().resolve()}")
177
182
  if args.cmd == "brain" and args.remember:
178
183
  path = memory.remember(vault, args.remember, kind=args.kind, pin=args.pin, source="me")
179
184
  print(f"Saved {path.resolve()}")
@@ -64,11 +64,24 @@ class Client:
64
64
  return bool((self.cli and shutil.which(self.cli)) or (self.marker and self.marker.exists()))
65
65
 
66
66
 
67
+ def _claude_desktop() -> list[Client]:
68
+ """Claude Desktop reads %APPDATA%\\Claude (or the macOS/Linux equivalent). Installed as a Windows
69
+ app package (MSIX), it reads a virtualized copy instead, under
70
+ %LOCALAPPDATA%\\Packages\\Claude_<id>\\LocalCache\\Roaming\\Claude, and its own "Edit Config"
71
+ button may open the copy it does not read. Both get the entry when both exist."""
72
+ found = [Client("claude-desktop", "Claude Desktop", _app_config("Claude") / "claude_desktop_config.json",
73
+ marker=_app_config("Claude"))]
74
+ local = os.environ.get("LOCALAPPDATA")
75
+ for package in sorted(Path(local).glob("Packages/Claude_*")) if local else []:
76
+ folder = package / "LocalCache" / "Roaming" / "Claude"
77
+ found.append(Client("claude-desktop", "Claude Desktop", folder / "claude_desktop_config.json", marker=package))
78
+ return found
79
+
80
+
67
81
  def clients() -> list[Client]:
68
82
  home = _home()
69
83
  return [
70
- Client("claude-desktop", "Claude Desktop", _app_config("Claude") / "claude_desktop_config.json",
71
- marker=_app_config("Claude")),
84
+ *_claude_desktop(),
72
85
  Client("claude-code", "Claude Code", cli="claude", marker=home / ".claude"),
73
86
  Client("cursor", "Cursor", home / ".cursor" / "mcp.json", marker=home / ".cursor"),
74
87
  Client("codex", "Codex", _codex_home() / "config.toml", cli="codex", marker=_codex_home()),
@@ -247,19 +260,21 @@ def _handshake(command: str, args: list[str], timeout: float = 90.0) -> tuple[bo
247
260
  async with ClientSession(read, write) as session:
248
261
  await session.initialize()
249
262
  tools = [t.name for t in (await session.list_tools()).tools]
250
- briefing = (await session.call_tool("context", {})).content[0].text
251
- return time.monotonic() - began, tools, briefing
263
+ await session.call_tool("context", {}) # answers at once, before the files are read
264
+ answered = time.monotonic() - began
265
+ summary = (await session.call_tool("overview", {})).content[0].text # waits for the index
266
+ return answered, tools, summary
252
267
 
253
268
  try:
254
- took, tools, briefing = asyncio.run(asyncio.wait_for(talk(), timeout))
269
+ took, tools, summary = asyncio.run(asyncio.wait_for(talk(), timeout))
255
270
  except Exception as err: # noqa: BLE001 (shown to the person, who can act on it)
256
271
  log.seek(0)
257
272
  said = " | ".join(line.strip() for line in log.read().splitlines()[-3:] if line.strip())
258
273
  return False, f"the server did not answer: {type(err).__name__}: {err}" + (f" (it said: {said})" if said else "")
259
274
  finally:
260
275
  log.close()
261
- last = [line for line in briefing.splitlines() if line.strip()][-1:]
262
- return True, f"answered in {took:.1f} s with {len(tools)} tools; it says: {last[0] if last else '(empty)'}"
276
+ first = next((line for line in summary.splitlines() if line.strip()), "(empty)")
277
+ return True, f"answered in {took:.1f} s with {len(tools)} tools; your brain: {first}"
263
278
 
264
279
 
265
280
  def _registered(client: Client) -> tuple[str, list[str]] | None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainplug
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: A second brain for you and your AI assistants: a 3D map of everything you write and a memory they plug into. 100% local.
5
5
  Author: Juan Rodríguez
6
6
  License-Expression: MIT
@@ -65,9 +65,7 @@ brainplug doctor ~/Documents/MyVault # checks that it answers, end to e
65
65
  ```
66
66
 
67
67
  `brainplug brain ~/Documents/MyVault --draft-about` writes a first draft of your "About me"
68
- note from what you have written, while it is still empty. Until the first release reaches PyPI,
69
- install it from GitHub:
70
- `uv tool install "brainplug @ git+https://github.com/juanmariarodriguez2-png/brainplug"`.
68
+ note from what you have written, while it is still empty.
71
69
  Add `--sessions` to `connect` to include what you typed to Claude Code and Codex, and put
72
70
  export files after the vault to include what you wrote before (`… MyVault chatgpt-export.zip`).
73
71
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "brainplug"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "A second brain for you and your AI assistants: a 3D map of everything you write and a memory they plug into. 100% local."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -16,6 +16,7 @@ def home(tmp_path, monkeypatch):
16
16
  h = tmp_path / "home"
17
17
  h.mkdir()
18
18
  for name, value in (("HOME", h), ("USERPROFILE", h), ("APPDATA", h / "AppData" / "Roaming"),
19
+ ("LOCALAPPDATA", h / "AppData" / "Local"), # never a real Windows app package
19
20
  ("XDG_CONFIG_HOME", h / ".config"), ("BRAINPLUG_CACHE", tmp_path / "cache")):
20
21
  monkeypatch.setenv(name, str(value))
21
22
  monkeypatch.delenv("CODEX_HOME", raising=False)
@@ -105,6 +106,7 @@ def test_doctor_checks_everything_end_to_end(home, tmp_path, monkeypatch, capsys
105
106
  assert main(["doctor", str(vault)]) == 0
106
107
  out = capsys.readouterr().out
107
108
  assert "the server starts and answers" in out and "All good." in out and "✗" not in out
109
+ assert "your brain: 21 items" in out # it waits for the files to be read, then says how much is there
108
110
  (home / ".cursor" / "mcp.json").write_text('{"mcpServers": {"brainplug": {"command": "/no/such/brainplug", "args": []}}}')
109
111
  assert main(["doctor", str(vault)]) == 1
110
112
  assert "brainplug connect" in capsys.readouterr().out
@@ -122,3 +124,17 @@ def test_a_hanging_assistant_cli_is_reported_not_fatal(monkeypatch):
122
124
  raise bc.subprocess.TimeoutExpired("claude", 60)
123
125
  monkeypatch.setattr(bc.subprocess, "run", hang)
124
126
  assert bc._run(["claude", "mcp", "add"]).returncode == 1
127
+
128
+
129
+ def test_connect_finds_claude_desktop_installed_as_a_windows_package(home, tmp_path, monkeypatch, capsys):
130
+ # The packaged (MSIX) app reads a virtualized copy of %APPDATA%\\Claude under its package folder.
131
+ monkeypatch.setattr(bc.shutil, "which", fake_which({"brainplug": "/opt/tools/brainplug"}))
132
+ package = home / "AppData" / "Local" / "Packages" / "Claude_pzs8sxrjxfjjc"
133
+ package.mkdir(parents=True)
134
+ vault = tmp_path / "Vault"
135
+ vault.mkdir()
136
+ assert main(["connect", str(vault)]) == 0
137
+ config = package / "LocalCache" / "Roaming" / "Claude" / "claude_desktop_config.json"
138
+ assert json.loads(config.read_text())["mcpServers"]["brainplug"]["args"] == ["mcp", "--brain", str(vault.resolve())]
139
+ assert not bc._app_config("Claude").exists() # the copy this app does not read is not created
140
+ assert "Claude Desktop" in capsys.readouterr().out
@@ -315,3 +315,13 @@ def test_a_vault_inside_another_input_is_read_once(tmp_path):
315
315
  (tmp_path / "Docs" / "elsewhere.md").write_text("a loose note outside the vault about tide pools", encoding="utf-8")
316
316
  items = sources.load([vault, tmp_path / "Docs"])
317
317
  assert len(items) == 11 and len({i.text for i in items}) == 11
318
+
319
+
320
+ def test_brain_does_not_create_a_mistyped_folder(tmp_path, capsys):
321
+ import pytest
322
+ for flag in ("--draft-about", "--context", "--review"):
323
+ with pytest.raises(SystemExit):
324
+ main(["brain", str(tmp_path / "C" / "ruta" / "de" / "su" / "bóveda"), flag])
325
+ assert not (tmp_path / "C").exists() and "does not exist" in capsys.readouterr().err
326
+ assert main(["brain", str(tmp_path / "New brain")]) == 0 # setting up a new brain may create it, and says so
327
+ assert "Creating a new folder for your brain" in capsys.readouterr().out
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