brainiac-cli 0.16.0__py3-none-any.whl
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.
- brain/__init__.py +39 -0
- brain/__main__.py +14 -0
- brain/_assets/AGENTS.md +564 -0
- brain/_assets/overlay/template/brand/brand-guide.md +24 -0
- brain/_assets/overlay/template/keywords/glossary.md +15 -0
- brain/_assets/overlay/template/people/roster.md +14 -0
- brain/_assets/overlay/template/voice/voice-profile.md +34 -0
- brain/_assets/routines/manifest.json +257 -0
- brain/_assets/scripts/brain-brief-mac.plist +59 -0
- brain/_assets/scripts/brain-brief.sh +74 -0
- brain/_assets/scripts/brain-synthesis-mac.plist +48 -0
- brain/_assets/scripts/brain-synthesis.sh +214 -0
- brain/_assets/scripts/install-brief-mac.sh +152 -0
- brain/_assets/scripts/install-brief-windows.ps1 +97 -0
- brain/_assets/scripts/register_tasks.py +386 -0
- brain/_assets/templates/company.md +21 -0
- brain/_assets/templates/concept.md +27 -0
- brain/_assets/templates/daily.md +20 -0
- brain/_assets/templates/decision.md +42 -0
- brain/_assets/templates/meeting.md +33 -0
- brain/_assets/templates/person.md +20 -0
- brain/_assets/templates/project.md +23 -0
- brain/_assets/templates/state-moc.md +40 -0
- brain/_version.py +12 -0
- brain/anchor.py +121 -0
- brain/audit.py +422 -0
- brain/backup.py +210 -0
- brain/brief.py +417 -0
- brain/capture.py +117 -0
- brain/chunk.py +249 -0
- brain/classification.py +134 -0
- brain/cli.py +1906 -0
- brain/config.py +368 -0
- brain/connect.py +362 -0
- brain/context.py +108 -0
- brain/core.py +3018 -0
- brain/doctor.py +1161 -0
- brain/egress.py +148 -0
- brain/embed.py +857 -0
- brain/encryption.py +217 -0
- brain/frontmatter.py +102 -0
- brain/golden_probe.py +678 -0
- brain/graph.py +369 -0
- brain/graphify.py +352 -0
- brain/index.py +1576 -0
- brain/ingest/__init__.py +19 -0
- brain/ingest/handlers/__init__.py +43 -0
- brain/ingest/handlers/base.py +95 -0
- brain/ingest/handlers/docx.py +78 -0
- brain/ingest/handlers/email.py +228 -0
- brain/ingest/handlers/html.py +142 -0
- brain/ingest/handlers/image.py +91 -0
- brain/ingest/handlers/pdf.py +99 -0
- brain/ingest/handlers/pptx.py +69 -0
- brain/ingest/handlers/tables.py +41 -0
- brain/ingest/handlers/text.py +43 -0
- brain/ingest/handlers/xlsx.py +100 -0
- brain/ingest/handlers/zip.py +163 -0
- brain/ingest/pipeline.py +839 -0
- brain/ingest/transcript.py +158 -0
- brain/init.py +870 -0
- brain/maintenance.py +2266 -0
- brain/mcp_adapter.py +217 -0
- brain/multihop.py +232 -0
- brain/notes.py +195 -0
- brain/overlay.py +183 -0
- brain/projection.py +79 -0
- brain/rerank.py +425 -0
- brain/snapshot.py +231 -0
- brain/update.py +743 -0
- brain/vectors.py +225 -0
- brainiac_cli-0.16.0.dist-info/METADATA +306 -0
- brainiac_cli-0.16.0.dist-info/RECORD +77 -0
- brainiac_cli-0.16.0.dist-info/WHEEL +5 -0
- brainiac_cli-0.16.0.dist-info/entry_points.txt +4 -0
- brainiac_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
- brainiac_cli-0.16.0.dist-info/top_level.txt +1 -0
brain/connect.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""``brain connect`` (SUI-02) — universal per-client wirer.
|
|
2
|
+
|
|
3
|
+
``brain connect --client claude-code|claude-desktop|codex|gemini`` writes
|
|
4
|
+
that client's wiring itself instead of the human hand-copying four different
|
|
5
|
+
JSON/Markdown snippets from docs. HOST-ONLY (refused on ``role=vm`` at the
|
|
6
|
+
same CLI trust gate as ``supersede``/``ingest`` — see ``cli.py``
|
|
7
|
+
``VM_ALLOWED``): this module never touches ``BrainCore`` at all, so there is
|
|
8
|
+
nothing role-gated inside it beyond that refusal.
|
|
9
|
+
|
|
10
|
+
Every client transform is a pure ``plan_*`` function (old text -> new text ->
|
|
11
|
+
unified diff -> ``already_connected`` flag) so it is unit-testable against
|
|
12
|
+
fixture files with zero writes to the real home directory. ``apply_*``
|
|
13
|
+
performs the actual write + first-mutation backup. The interactive
|
|
14
|
+
diff-then-confirm loop and ``--yes``/non-interactive exit-nonzero contract
|
|
15
|
+
live in ``cli.py`` (same shape as the existing ``init --import-from`` dry-run
|
|
16
|
+
gate), not here.
|
|
17
|
+
|
|
18
|
+
Two writers for the Desktop MCP stanza would be a bug (the hardening addendum
|
|
19
|
+
for this session is explicit: verified there is currently NO Desktop-config
|
|
20
|
+
writer anywhere in this codebase — ``mcp-config`` only prints). This module
|
|
21
|
+
is that ONE writer; ``cli.py``'s ``mcp-config`` stays print-only and unchanged.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import difflib
|
|
26
|
+
import json
|
|
27
|
+
import platform
|
|
28
|
+
import re
|
|
29
|
+
import shutil
|
|
30
|
+
import subprocess
|
|
31
|
+
import time
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Callable, Optional
|
|
35
|
+
|
|
36
|
+
CLIENTS = ("claude-code", "claude-desktop", "codex", "gemini")
|
|
37
|
+
|
|
38
|
+
MARKETPLACE_NAME = "brainiac"
|
|
39
|
+
KERNEL_PLUGIN = "brainiac-kernel"
|
|
40
|
+
DEFAULT_MARKETPLACE_SOURCE = "Autopsias/brainiac" # README's documented add-source
|
|
41
|
+
|
|
42
|
+
# Marked block used for the two Markdown-append clients (claude-code's
|
|
43
|
+
# CLAUDE.md, codex's AGENTS.md) so a re-run detects "already connected" and
|
|
44
|
+
# --remove can strip exactly this block without touching anything else a
|
|
45
|
+
# human wrote in the file.
|
|
46
|
+
BLOCK_START = "<!-- brainiac:connect:begin -->"
|
|
47
|
+
BLOCK_END = "<!-- brainiac:connect:end -->"
|
|
48
|
+
|
|
49
|
+
BRAIN_USAGE_PARAGRAPH = (
|
|
50
|
+
"## Brain usage\n\n"
|
|
51
|
+
"Retrieval, capture, and indexing in this project are owned by the "
|
|
52
|
+
"**`brain` CLI** — call it from your native shell, never via MCP. Read "
|
|
53
|
+
"tools: `brain search \"<q>\" --json`, `brain get <id> --json`, "
|
|
54
|
+
"`brain recent --json`; capture with `brain draft-capture`. Every read "
|
|
55
|
+
"applies a classification egress filter before stdout. Run "
|
|
56
|
+
"`brain --help` for the always-current contract."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _marked_block(body: str) -> str:
|
|
61
|
+
return f"{BLOCK_START}\n{body}\n{BLOCK_END}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_BLOCK_RE = re.compile(
|
|
65
|
+
re.escape(BLOCK_START) + r"\n.*?\n" + re.escape(BLOCK_END) + r"\n?",
|
|
66
|
+
re.DOTALL,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --------------------------------------------------------------------------
|
|
71
|
+
# Plan result — the diff-first contract every client transform returns
|
|
72
|
+
# --------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ConnectPlan:
|
|
76
|
+
client: str
|
|
77
|
+
target_path: Path
|
|
78
|
+
action: str # "noop" | "create" | "append" | "merge" | "remove"
|
|
79
|
+
old_text: Optional[str]
|
|
80
|
+
new_text: Optional[str]
|
|
81
|
+
already_connected: bool
|
|
82
|
+
diff: str
|
|
83
|
+
kind: str = "file" # "file" | "subprocess" (claude-code plugin install)
|
|
84
|
+
extra: dict = field(default_factory=dict)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def has_changes(self) -> bool:
|
|
88
|
+
return self.action not in ("noop",)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _diff(old: str, new: str, path: Path) -> str:
|
|
92
|
+
return "".join(difflib.unified_diff(
|
|
93
|
+
old.splitlines(keepends=True), new.splitlines(keepends=True),
|
|
94
|
+
fromfile=f"{path} (before)", tofile=f"{path} (after)"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _backup_path(path: Path) -> Path:
|
|
98
|
+
stamp = time.strftime("%Y%m%d", time.gmtime())
|
|
99
|
+
return path.with_name(path.name + f".bak-{stamp}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def backup_original(path: Path) -> Optional[Path]:
|
|
103
|
+
"""Back up ``path`` alongside itself on FIRST mutation only — a backup
|
|
104
|
+
that already exists (today's stamp) is left untouched so the original
|
|
105
|
+
pre-connect content is never clobbered by a second run's backup."""
|
|
106
|
+
if not path.exists():
|
|
107
|
+
return None
|
|
108
|
+
bak = _backup_path(path)
|
|
109
|
+
if not bak.exists():
|
|
110
|
+
shutil.copy2(path, bak)
|
|
111
|
+
return bak
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def write_text(path: Path, text: str) -> None:
|
|
115
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
path.write_text(text, encoding="utf-8")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --------------------------------------------------------------------------
|
|
120
|
+
# Markdown marked-block clients: claude-code's project CLAUDE.md, codex's
|
|
121
|
+
# project AGENTS.md
|
|
122
|
+
# --------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def plan_marked_block(target_path: Path, body: str = BRAIN_USAGE_PARAGRAPH) -> ConnectPlan:
|
|
125
|
+
old_text = target_path.read_text(encoding="utf-8") if target_path.exists() else None
|
|
126
|
+
block = _marked_block(body)
|
|
127
|
+
if old_text is not None and BLOCK_START in old_text:
|
|
128
|
+
return ConnectPlan(
|
|
129
|
+
client="", target_path=target_path, action="noop",
|
|
130
|
+
old_text=old_text, new_text=old_text, already_connected=True, diff="")
|
|
131
|
+
base = old_text or ""
|
|
132
|
+
sep = "" if not base or base.endswith("\n\n") else ("\n" if base.endswith("\n") else "\n\n")
|
|
133
|
+
new_text = base + sep + block + "\n"
|
|
134
|
+
return ConnectPlan(
|
|
135
|
+
client="", target_path=target_path,
|
|
136
|
+
action="append" if old_text is not None else "create",
|
|
137
|
+
old_text=old_text, new_text=new_text, already_connected=False,
|
|
138
|
+
diff=_diff(old_text or "", new_text, target_path))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def apply_marked_block(plan: ConnectPlan) -> None:
|
|
142
|
+
if plan.old_text is not None:
|
|
143
|
+
backup_original(plan.target_path)
|
|
144
|
+
write_text(plan.target_path, plan.new_text or "")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def plan_remove_marked_block(target_path: Path) -> ConnectPlan:
|
|
148
|
+
if not target_path.exists():
|
|
149
|
+
return ConnectPlan(client="", target_path=target_path, action="noop",
|
|
150
|
+
old_text=None, new_text=None, already_connected=False, diff="")
|
|
151
|
+
old_text = target_path.read_text(encoding="utf-8")
|
|
152
|
+
new_text = _BLOCK_RE.sub("", old_text)
|
|
153
|
+
# collapse a leftover blank-line gap left by the removed separator
|
|
154
|
+
new_text = re.sub(r"\n{3,}\Z", "\n", new_text)
|
|
155
|
+
if new_text == old_text:
|
|
156
|
+
return ConnectPlan(client="", target_path=target_path, action="noop",
|
|
157
|
+
old_text=old_text, new_text=old_text,
|
|
158
|
+
already_connected=False, diff="")
|
|
159
|
+
return ConnectPlan(client="", target_path=target_path, action="remove",
|
|
160
|
+
old_text=old_text, new_text=new_text,
|
|
161
|
+
already_connected=False,
|
|
162
|
+
diff=_diff(old_text, new_text, target_path))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def apply_remove_marked_block(plan: ConnectPlan) -> None:
|
|
166
|
+
if plan.action != "remove":
|
|
167
|
+
return
|
|
168
|
+
backup_original(plan.target_path)
|
|
169
|
+
write_text(plan.target_path, plan.new_text or "")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# --------------------------------------------------------------------------
|
|
173
|
+
# JSON merge clients: claude-desktop's claude_desktop_config.json,
|
|
174
|
+
# gemini's .gemini/settings.json. Merge, never replace — preserve unknown
|
|
175
|
+
# top-level keys AND unrelated mcpServers entries.
|
|
176
|
+
# --------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def _load_json(path: Path) -> dict:
|
|
179
|
+
if not path.exists():
|
|
180
|
+
return {}
|
|
181
|
+
text = path.read_text(encoding="utf-8").strip()
|
|
182
|
+
return json.loads(text) if text else {}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _dump_json(data: dict) -> str:
|
|
186
|
+
return json.dumps(data, indent=2) + "\n"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def mcp_server_entry(vault: str, name: str = "brainiac", max_tier: str = "MNPI") -> dict:
|
|
190
|
+
"""The SAME entry shape ``brain mcp-config`` prints (cli.py) — the one
|
|
191
|
+
place this shape is built; both the print-only command and this writer
|
|
192
|
+
call it so they can never drift apart."""
|
|
193
|
+
import sys as _sys
|
|
194
|
+
|
|
195
|
+
brain_mcp = shutil.which("brain-mcp") or str(Path(_sys.executable).parent / "brain-mcp")
|
|
196
|
+
env = {"BRAIN_VAULT": str(vault), "BRAIN_MAX_EGRESS_TIER": max_tier}
|
|
197
|
+
model_dir = Path(vault) / ".brain" / "model"
|
|
198
|
+
if model_dir.is_dir():
|
|
199
|
+
env["BRAIN_MODEL_CACHE"] = str(model_dir)
|
|
200
|
+
return {name: {"command": brain_mcp, "env": env}}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def claude_desktop_config_path() -> Path:
|
|
204
|
+
system = platform.system()
|
|
205
|
+
if system == "Darwin":
|
|
206
|
+
return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
207
|
+
if system == "Windows":
|
|
208
|
+
import os
|
|
209
|
+
|
|
210
|
+
appdata = Path(os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming")))
|
|
211
|
+
return appdata / "Claude" / "claude_desktop_config.json"
|
|
212
|
+
return Path.home() / ".config" / "Claude" / "claude_desktop_config.json" # linux fallback
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def plan_claude_desktop(config_path: Path, vault: str, name: str = "brainiac",
|
|
216
|
+
max_tier: str = "MNPI") -> ConnectPlan:
|
|
217
|
+
old_data = _load_json(config_path)
|
|
218
|
+
old_text = json.dumps(old_data, indent=2, sort_keys=True) if config_path.exists() else None
|
|
219
|
+
entry = mcp_server_entry(vault, name, max_tier)
|
|
220
|
+
new_data = dict(old_data)
|
|
221
|
+
servers = dict(new_data.get("mcpServers") or {})
|
|
222
|
+
already = servers.get(name) == entry[name]
|
|
223
|
+
servers[name] = entry[name]
|
|
224
|
+
new_data["mcpServers"] = servers
|
|
225
|
+
new_text = _dump_json(new_data)
|
|
226
|
+
old_rendered = _dump_json(old_data) if config_path.exists() else ""
|
|
227
|
+
if already:
|
|
228
|
+
return ConnectPlan(client="claude-desktop", target_path=config_path,
|
|
229
|
+
action="noop", old_text=old_rendered, new_text=old_rendered,
|
|
230
|
+
already_connected=True, diff="")
|
|
231
|
+
return ConnectPlan(
|
|
232
|
+
client="claude-desktop", target_path=config_path,
|
|
233
|
+
action="merge" if config_path.exists() else "create",
|
|
234
|
+
old_text=old_rendered or None, new_text=new_text, already_connected=False,
|
|
235
|
+
diff=_diff(old_rendered, new_text, config_path))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def apply_json_merge(plan: ConnectPlan) -> None:
|
|
239
|
+
if plan.old_text:
|
|
240
|
+
backup_original(plan.target_path)
|
|
241
|
+
write_text(plan.target_path, plan.new_text or "")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def plan_gemini(settings_path: Path) -> ConnectPlan:
|
|
245
|
+
old_data = _load_json(settings_path)
|
|
246
|
+
old_rendered = _dump_json(old_data) if settings_path.exists() else ""
|
|
247
|
+
if old_data.get("contextFileName") == "AGENTS.md":
|
|
248
|
+
return ConnectPlan(client="gemini", target_path=settings_path, action="noop",
|
|
249
|
+
old_text=old_rendered, new_text=old_rendered,
|
|
250
|
+
already_connected=True, diff="")
|
|
251
|
+
new_data = dict(old_data)
|
|
252
|
+
new_data["contextFileName"] = "AGENTS.md"
|
|
253
|
+
new_text = _dump_json(new_data)
|
|
254
|
+
return ConnectPlan(
|
|
255
|
+
client="gemini", target_path=settings_path,
|
|
256
|
+
action="merge" if settings_path.exists() else "create",
|
|
257
|
+
old_text=old_rendered or None, new_text=new_text, already_connected=False,
|
|
258
|
+
diff=_diff(old_rendered, new_text, settings_path))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def plan_restore_from_backup(target_path: Path) -> dict:
|
|
262
|
+
"""--remove for the two JSON-merge clients: restore the FIRST backup
|
|
263
|
+
(file.bak-<oldest date found>) rather than the marked-block strip used
|
|
264
|
+
for the Markdown clients — there's no marker inside JSON to strip."""
|
|
265
|
+
candidates = sorted(target_path.parent.glob(target_path.name + ".bak-*"))
|
|
266
|
+
if not candidates:
|
|
267
|
+
return {"ok": False, "reason": "no backup found; nothing to unwire",
|
|
268
|
+
"target": str(target_path)}
|
|
269
|
+
bak = candidates[0] # earliest stamp = closest to pre-connect original
|
|
270
|
+
return {"ok": True, "backup": str(bak), "target": str(target_path)}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def apply_restore_from_backup(target_path: Path, backup: Path) -> None:
|
|
274
|
+
shutil.copy2(backup, target_path)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# --------------------------------------------------------------------------
|
|
278
|
+
# claude-code: real plugin-CLI mutation (verified non-interactive in this
|
|
279
|
+
# session's recon — `claude plugin marketplace add` / `claude plugin install`
|
|
280
|
+
# both exist), THEN the CLAUDE.md marked-block append. If the `claude`
|
|
281
|
+
# binary/subcommand surface is missing, callers fall back to printing the
|
|
282
|
+
# two commands (see cli.py) instead of claiming a mutation that didn't happen.
|
|
283
|
+
# --------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
Runner = Callable[..., "subprocess.CompletedProcess[str]"]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _default_runner(cmd: list[str], **kwargs: Any) -> "subprocess.CompletedProcess[str]":
|
|
289
|
+
kwargs.setdefault("capture_output", True)
|
|
290
|
+
kwargs.setdefault("text", True)
|
|
291
|
+
kwargs.setdefault("timeout", 120)
|
|
292
|
+
return subprocess.run(cmd, **kwargs)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def claude_code_plugin_commands(marketplace_source: str = DEFAULT_MARKETPLACE_SOURCE,
|
|
296
|
+
plugin: str = KERNEL_PLUGIN) -> list[list[str]]:
|
|
297
|
+
claude_bin = shutil.which("claude") or "claude"
|
|
298
|
+
return [
|
|
299
|
+
[claude_bin, "plugin", "marketplace", "add", marketplace_source],
|
|
300
|
+
[claude_bin, "plugin", "install", f"{plugin}@{MARKETPLACE_NAME}"],
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def claude_plugin_cli_available(run: Runner = _default_runner) -> bool:
|
|
305
|
+
if shutil.which("claude") is None:
|
|
306
|
+
return False
|
|
307
|
+
try:
|
|
308
|
+
out = run([shutil.which("claude"), "plugin", "--help"])
|
|
309
|
+
except Exception: # noqa: BLE001 — any probe failure means "not available"
|
|
310
|
+
return False
|
|
311
|
+
text = ((out.stdout or "") + (out.stderr or "")).lower()
|
|
312
|
+
return "marketplace" in text and "install" in text
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def is_plugin_installed(claude_home: Path, plugin: str = KERNEL_PLUGIN) -> bool:
|
|
316
|
+
installed_json = claude_home / "plugins" / "installed_plugins.json"
|
|
317
|
+
if not installed_json.exists():
|
|
318
|
+
return False
|
|
319
|
+
try:
|
|
320
|
+
data = json.loads(installed_json.read_text(encoding="utf-8"))
|
|
321
|
+
except (json.JSONDecodeError, OSError):
|
|
322
|
+
return False
|
|
323
|
+
return bool((data.get("plugins") or {}).get(f"{plugin}@{MARKETPLACE_NAME}"))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def run_claude_code_plugin_install(
|
|
327
|
+
marketplace_source: str = DEFAULT_MARKETPLACE_SOURCE, plugin: str = KERNEL_PLUGIN,
|
|
328
|
+
run: Runner = _default_runner, claude_home: Optional[Path] = None,
|
|
329
|
+
) -> dict:
|
|
330
|
+
claude_home = claude_home or (Path.home() / ".claude")
|
|
331
|
+
cmds = claude_code_plugin_commands(marketplace_source, plugin)
|
|
332
|
+
results = []
|
|
333
|
+
for cmd in cmds:
|
|
334
|
+
try:
|
|
335
|
+
out = run(cmd)
|
|
336
|
+
results.append({"cmd": cmd, "returncode": out.returncode,
|
|
337
|
+
"stdout": out.stdout, "stderr": out.stderr})
|
|
338
|
+
if out.returncode != 0:
|
|
339
|
+
return {"ok": False, "steps": results,
|
|
340
|
+
"reason": f"`{' '.join(cmd)}` exited {out.returncode}"}
|
|
341
|
+
except Exception as exc: # noqa: BLE001
|
|
342
|
+
results.append({"cmd": cmd, "error": f"{type(exc).__name__}: {exc}"})
|
|
343
|
+
return {"ok": False, "steps": results, "reason": str(exc)}
|
|
344
|
+
verified = is_plugin_installed(claude_home, plugin)
|
|
345
|
+
return {"ok": verified, "steps": results, "verified_installed": verified}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def run_claude_code_plugin_uninstall(
|
|
349
|
+
plugin: str = KERNEL_PLUGIN, run: Runner = _default_runner,
|
|
350
|
+
claude_home: Optional[Path] = None,
|
|
351
|
+
) -> dict:
|
|
352
|
+
claude_home = claude_home or (Path.home() / ".claude")
|
|
353
|
+
claude_bin = shutil.which("claude") or "claude"
|
|
354
|
+
cmd = [claude_bin, "plugin", "uninstall", f"{plugin}@{MARKETPLACE_NAME}"]
|
|
355
|
+
try:
|
|
356
|
+
out = run(cmd)
|
|
357
|
+
except Exception as exc: # noqa: BLE001
|
|
358
|
+
return {"ok": False, "cmd": cmd, "error": f"{type(exc).__name__}: {exc}"}
|
|
359
|
+
still_installed = is_plugin_installed(claude_home, plugin)
|
|
360
|
+
return {"ok": out.returncode == 0, "cmd": cmd, "returncode": out.returncode,
|
|
361
|
+
"stdout": out.stdout, "stderr": out.stderr,
|
|
362
|
+
"verified_removed": not still_installed}
|
brain/context.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""UPG-04 — Contextual Retrieval (Anthropic-style, index-time).
|
|
2
|
+
|
|
3
|
+
For each note, generate a ≤1-sentence document-level context that situates the
|
|
4
|
+
note within the corpus, and prepend it to every chunk before embedding (and to
|
|
5
|
+
the BM25 text). This is the technique Anthropic published (Sep 2024) as
|
|
6
|
+
"Contextual Retrieval": they reported a −49% retrieval-failure-rate reduction
|
|
7
|
+
for contextual embeddings alone, and −67% stacked with a cross-encoder reranker.
|
|
8
|
+
|
|
9
|
+
The context is generated ONCE per note at INDEX TIME by a small local CPU LLM
|
|
10
|
+
(Qwen2.5-0.5B, GGUF, via llama-cpp-python — no PyTorch, no GPU, no network).
|
|
11
|
+
Zero added query latency. Negligible index-size growth (one sentence per note).
|
|
12
|
+
|
|
13
|
+
This targets brain's documented failure modes (S10): atomic markdown notes and
|
|
14
|
+
cross-lingual entity-only chunks that are reachable by EN queries but lose the
|
|
15
|
+
note-level context that disambiguates them. The template-based prefix in
|
|
16
|
+
``brain.chunk`` already does a lighter version of this (title + zone + heading);
|
|
17
|
+
UPG-04 adds the LLM's semantic summary of what the note is ABOUT.
|
|
18
|
+
|
|
19
|
+
GATE (revert if not met): retrieval-failure-rate drops on the 135-q blind set
|
|
20
|
+
with NO regression on the identifier/temporal strata (those queries are already
|
|
21
|
+
lexical-exact and should not be perturbed by added context). Gate-off if it
|
|
22
|
+
regresses.
|
|
23
|
+
|
|
24
|
+
OFFLINE / NO-LLM FALLBACK: if ``llama-cpp-python`` is not importable or no model
|
|
25
|
+
is bundled, ``doc_context`` returns "" and the index degrades cleanly to the
|
|
26
|
+
template-prefix-only path (the S10 behaviour). Contextual Retrieval is an
|
|
27
|
+
OPT-IN upgrade via ``$BRAIN_CONTEXTUAL_LLM`` (path to a GGUF); without it, the
|
|
28
|
+
feature is inert.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
from functools import lru_cache
|
|
34
|
+
|
|
35
|
+
# The prompt asks the LLM for a single-sentence summary of what the note is about,
|
|
36
|
+
# in the note's own language. The summary is prepended to every chunk at embed time.
|
|
37
|
+
# Kept deliberately short (Anthropic found 50-100 tokens optimal; we cap tighter).
|
|
38
|
+
_PROMPT_TMPL = (
|
|
39
|
+
"<|im_start|>system\nYou write a single concise sentence (max 30 words) "
|
|
40
|
+
"summarising what this document is about, for retrieval. Reply in the "
|
|
41
|
+
"document's language. Output ONLY the summary sentence, nothing else."
|
|
42
|
+
"<|im_end|>\n<|im_start|>user\nDocument title: {title}\nDocument zone: {zone}\n\n"
|
|
43
|
+
"Document content:\n{body}<|im_end|>\n<|im_start|>assistant\n"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@lru_cache(maxsize=1)
|
|
48
|
+
def _llm():
|
|
49
|
+
"""Lazily load the local CPU LLM. Returns None if unavailable (clean fallback).
|
|
50
|
+
|
|
51
|
+
Resolves the model path from ``$BRAIN_CONTEXTUAL_LLM`` (a GGUF file). Without
|
|
52
|
+
it, contextual retrieval is inert (returns "") and the index degrades to the
|
|
53
|
+
template-prefix-only S10 path — never an error.
|
|
54
|
+
"""
|
|
55
|
+
model_path = os.environ.get("BRAIN_CONTEXTUAL_LLM")
|
|
56
|
+
if not model_path:
|
|
57
|
+
return None
|
|
58
|
+
try:
|
|
59
|
+
from llama_cpp import Llama
|
|
60
|
+
except ImportError:
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
return Llama(
|
|
64
|
+
model_path=model_path,
|
|
65
|
+
n_ctx=2048, # notes are chunked; 2K ctx is enough for the body slice
|
|
66
|
+
n_gpu_layers=0, # CPU-only by design
|
|
67
|
+
verbose=False,
|
|
68
|
+
n_threads=max(1, (os.cpu_count() or 4) // 2), # don't saturate all cores
|
|
69
|
+
)
|
|
70
|
+
except Exception:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def doc_context(title: str, zone: str, body: str, *, max_body_chars: int = 1600) -> str:
|
|
75
|
+
"""Generate a ≤1-sentence document-level context for ``Contextual Retrieval``.
|
|
76
|
+
|
|
77
|
+
Returns "" when the LLM is unavailable (the clean no-op fallback). The body
|
|
78
|
+
is truncated to ``max_body_chars`` to bound the LLM prompt — the first ~1.6K
|
|
79
|
+
chars (title + abstract + first sections) carry enough signal for a one-line
|
|
80
|
+
summary, and this keeps the per-note generation cost to a few seconds on CPU.
|
|
81
|
+
"""
|
|
82
|
+
llm = _llm()
|
|
83
|
+
if llm is None:
|
|
84
|
+
return ""
|
|
85
|
+
body_slice = (body or "")[:max_body_chars]
|
|
86
|
+
prompt = _PROMPT_TMPL.format(
|
|
87
|
+
title=(title or "(untitled)")[:120],
|
|
88
|
+
zone=(zone or "brain"),
|
|
89
|
+
body=body_slice,
|
|
90
|
+
)
|
|
91
|
+
try:
|
|
92
|
+
out = llm(
|
|
93
|
+
prompt,
|
|
94
|
+
max_tokens=60, # ~one sentence
|
|
95
|
+
temperature=0.0, # deterministic summary
|
|
96
|
+
stop=["<|im_end|>", "\n\n", "."], # stop at first sentence end
|
|
97
|
+
)
|
|
98
|
+
text = out["choices"][0]["text"].strip().strip('"').strip()
|
|
99
|
+
# collapse to a single line and cap length
|
|
100
|
+
text = " ".join(text.split())[:200]
|
|
101
|
+
return text + "." if text and not text.endswith(".") else text
|
|
102
|
+
except Exception:
|
|
103
|
+
return ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def contextual_available() -> bool:
|
|
107
|
+
"""True iff the contextual LLM is configured and loadable."""
|
|
108
|
+
return _llm() is not None
|