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/snapshot.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Read-only snapshot publisher (r2-codex; pairs with S06 host/VM split).
|
|
2
|
+
|
|
3
|
+
The **authoritative writable index lives on the HOST** under %LOCALAPPDATA% /
|
|
4
|
+
Application Support (WAL, single-writer). The Cowork VM must NEVER write it.
|
|
5
|
+
Instead the host publishes a **read-only snapshot** that the VM mounts read-only
|
|
6
|
+
in ``./.brain/``.
|
|
7
|
+
|
|
8
|
+
Two correctness properties this module guarantees:
|
|
9
|
+
|
|
10
|
+
1. **Atomic publish.** The snapshot DB is written to a temp file then
|
|
11
|
+
``os.replace``d into place (atomic on POSIX & Windows for same-filesystem
|
|
12
|
+
moves). A reader never sees a half-written index. The manifest is written the
|
|
13
|
+
same way and last, so a present manifest always describes a complete DB.
|
|
14
|
+
|
|
15
|
+
2. **Generation id.** Each publish increments a monotonic ``generation`` and
|
|
16
|
+
records it (plus age inputs, counts, embed model/dim, sha256) in
|
|
17
|
+
``snapshot.manifest.json``. ``brain status`` reports the generation + age so a
|
|
18
|
+
VM session can tell whether its read-only view is fresh or stale.
|
|
19
|
+
|
|
20
|
+
WAL caveat (documented, S06): SQLCipher + WAL over VirtioFS with two concurrent
|
|
21
|
+
Cowork sessions on one mounted index risks lock contention / corruption. The
|
|
22
|
+
snapshot is therefore a *copy*, published by the single host writer — the VM
|
|
23
|
+
never opens the authoritative WAL DB. The index stays rebuildable-from-markdown,
|
|
24
|
+
so a corrupt snapshot is always recoverable by re-publishing.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
import sqlite3
|
|
32
|
+
import time
|
|
33
|
+
from dataclasses import dataclass, asdict
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from . import config
|
|
37
|
+
|
|
38
|
+
SNAPSHOT_DB = "index.snapshot.sqlite"
|
|
39
|
+
MANIFEST = "snapshot.manifest.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class SnapshotManifest:
|
|
44
|
+
generation: int
|
|
45
|
+
created_epoch: float
|
|
46
|
+
created_iso: str
|
|
47
|
+
source_db: str
|
|
48
|
+
snapshot_db: str
|
|
49
|
+
sha256: str
|
|
50
|
+
bytes: int
|
|
51
|
+
notes: int
|
|
52
|
+
chunks: int
|
|
53
|
+
embed_model: str | None
|
|
54
|
+
embed_dim: str | None
|
|
55
|
+
schema_version: str | None
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return asdict(self)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _sha256_file(path: Path) -> str:
|
|
62
|
+
import hashlib
|
|
63
|
+
|
|
64
|
+
h = hashlib.sha256()
|
|
65
|
+
with path.open("rb") as fh:
|
|
66
|
+
for blk in iter(lambda: fh.read(1 << 20), b""):
|
|
67
|
+
h.update(blk)
|
|
68
|
+
return h.hexdigest()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _finalize_readonly(db_path: Path) -> None:
|
|
72
|
+
"""Convert the copied snapshot to a self-contained rollback-journal DB.
|
|
73
|
+
|
|
74
|
+
The source index runs in WAL mode; a raw copy carries WAL in its header, so a
|
|
75
|
+
pure read-only consumer (the VM, opening ``mode=ro`` on a possibly read-only
|
|
76
|
+
mount) would need ``-wal``/``-shm`` sidecars it cannot create. Checkpointing
|
|
77
|
+
then switching to ``journal_mode=DELETE`` and closing leaves a single,
|
|
78
|
+
self-contained file the VM can open read-only with NO sidecars — the property
|
|
79
|
+
the host/VM split depends on.
|
|
80
|
+
"""
|
|
81
|
+
con = sqlite3.connect(str(db_path))
|
|
82
|
+
try:
|
|
83
|
+
con.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
84
|
+
con.execute("PRAGMA journal_mode=DELETE")
|
|
85
|
+
con.commit()
|
|
86
|
+
except sqlite3.OperationalError:
|
|
87
|
+
pass
|
|
88
|
+
finally:
|
|
89
|
+
con.close()
|
|
90
|
+
# Remove any residual sidecars so the published snapshot is one file.
|
|
91
|
+
for suffix in ("-wal", "-shm"):
|
|
92
|
+
side = Path(str(db_path) + suffix)
|
|
93
|
+
if side.exists():
|
|
94
|
+
try:
|
|
95
|
+
side.unlink()
|
|
96
|
+
except OSError:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _read_counts_and_meta(db_path: Path) -> dict:
|
|
101
|
+
out = {"notes": 0, "chunks": 0, "embed_model": None, "embed_dim": None,
|
|
102
|
+
"schema_version": None}
|
|
103
|
+
con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
104
|
+
try:
|
|
105
|
+
out["notes"] = int(con.execute("SELECT COUNT(*) FROM notes").fetchone()[0])
|
|
106
|
+
try:
|
|
107
|
+
out["chunks"] = int(con.execute("SELECT COUNT(*) FROM chunks").fetchone()[0])
|
|
108
|
+
except sqlite3.OperationalError:
|
|
109
|
+
out["chunks"] = 0
|
|
110
|
+
for k in ("embed_model", "embed_dim", "schema_version"):
|
|
111
|
+
r = con.execute("SELECT v FROM meta WHERE k=?", (k,)).fetchone()
|
|
112
|
+
out[k] = r[0] if r else None
|
|
113
|
+
finally:
|
|
114
|
+
con.close()
|
|
115
|
+
return out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def read_manifest(dest_dir: Path) -> SnapshotManifest | None:
|
|
119
|
+
p = Path(dest_dir) / MANIFEST
|
|
120
|
+
if not p.is_file():
|
|
121
|
+
return None
|
|
122
|
+
try:
|
|
123
|
+
d = json.loads(p.read_text(encoding="utf-8"))
|
|
124
|
+
return SnapshotManifest(**d)
|
|
125
|
+
except Exception:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def publish_snapshot(source_db: Path, dest_dir: Path) -> SnapshotManifest:
|
|
130
|
+
"""Atomically publish ``source_db`` as a read-only snapshot in ``dest_dir``.
|
|
131
|
+
|
|
132
|
+
Returns the new manifest. ``generation`` = previous generation + 1 (1 on the
|
|
133
|
+
first publish).
|
|
134
|
+
"""
|
|
135
|
+
source_db = Path(source_db)
|
|
136
|
+
dest_dir = Path(dest_dir)
|
|
137
|
+
if not source_db.is_file():
|
|
138
|
+
raise FileNotFoundError(f"source index not found: {source_db}")
|
|
139
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
|
|
141
|
+
prev = read_manifest(dest_dir)
|
|
142
|
+
generation = (prev.generation + 1) if prev else 1
|
|
143
|
+
|
|
144
|
+
# Checkpoint the WAL into the main DB before snapshotting so the copy is a
|
|
145
|
+
# self-contained, consistent point-in-time (no dependence on -wal/-shm).
|
|
146
|
+
con = sqlite3.connect(str(source_db))
|
|
147
|
+
try:
|
|
148
|
+
con.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
149
|
+
except sqlite3.OperationalError:
|
|
150
|
+
pass
|
|
151
|
+
finally:
|
|
152
|
+
con.close()
|
|
153
|
+
|
|
154
|
+
final_db = dest_dir / SNAPSHOT_DB
|
|
155
|
+
tmp_db = dest_dir / (SNAPSHOT_DB + f".tmp.{os.getpid()}.{generation}")
|
|
156
|
+
shutil.copy2(source_db, tmp_db)
|
|
157
|
+
try:
|
|
158
|
+
os.replace(tmp_db, final_db) # atomic swap
|
|
159
|
+
finally:
|
|
160
|
+
if tmp_db.exists():
|
|
161
|
+
tmp_db.unlink()
|
|
162
|
+
# Make the snapshot a self-contained rollback-journal DB BEFORE tightening
|
|
163
|
+
# its permissions, so a VM read-only open needs no -wal/-shm sidecars.
|
|
164
|
+
_finalize_readonly(final_db)
|
|
165
|
+
# Owner-only (0600), NOT the previous world-readable 0444 -- the snapshot
|
|
166
|
+
# can carry note bodies up to and including MNPI-tier content, and a
|
|
167
|
+
# shared/multi-user machine is exactly the case the classification gate
|
|
168
|
+
# cannot protect against (it is an egress *decision*, not containment; see
|
|
169
|
+
# docs/operations/egress-provider-posture.md §2). Real read-only
|
|
170
|
+
# enforcement is the ``mode=ro`` + ``PRAGMA query_only=ON`` SQLite connection
|
|
171
|
+
# (BrainIndex.conn, read_only=True), NOT the filesystem bit, so this change
|
|
172
|
+
# does not weaken the write-protection guarantee -- it only removes the
|
|
173
|
+
# world-readable exposure.
|
|
174
|
+
config.secure_file_permissions(final_db)
|
|
175
|
+
|
|
176
|
+
cm = _read_counts_and_meta(final_db)
|
|
177
|
+
now = time.time()
|
|
178
|
+
manifest = SnapshotManifest(
|
|
179
|
+
generation=generation,
|
|
180
|
+
created_epoch=now,
|
|
181
|
+
created_iso=time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(now)) + "Z",
|
|
182
|
+
source_db=str(source_db),
|
|
183
|
+
snapshot_db=str(final_db),
|
|
184
|
+
sha256=_sha256_file(final_db),
|
|
185
|
+
bytes=final_db.stat().st_size,
|
|
186
|
+
notes=cm["notes"],
|
|
187
|
+
chunks=cm["chunks"],
|
|
188
|
+
embed_model=cm["embed_model"],
|
|
189
|
+
embed_dim=cm["embed_dim"],
|
|
190
|
+
schema_version=cm["schema_version"],
|
|
191
|
+
)
|
|
192
|
+
# Write manifest LAST, atomically — a present manifest always implies a
|
|
193
|
+
# complete DB at the recorded generation.
|
|
194
|
+
tmp_man = dest_dir / (MANIFEST + f".tmp.{os.getpid()}")
|
|
195
|
+
tmp_man.write_text(json.dumps(manifest.to_dict(), indent=2) + "\n", encoding="utf-8")
|
|
196
|
+
os.replace(tmp_man, dest_dir / MANIFEST)
|
|
197
|
+
return manifest
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def snapshot_status(dest_dir: Path) -> dict:
|
|
201
|
+
"""Report snapshot generation + age for ``brain status`` (VM-side)."""
|
|
202
|
+
m = read_manifest(dest_dir)
|
|
203
|
+
if m is None:
|
|
204
|
+
return {"snapshot": "absent", "dest": str(dest_dir)}
|
|
205
|
+
age_s = max(0.0, time.time() - m.created_epoch)
|
|
206
|
+
return {
|
|
207
|
+
"snapshot": "present",
|
|
208
|
+
"generation": m.generation,
|
|
209
|
+
"created_iso": m.created_iso,
|
|
210
|
+
"age_seconds": round(age_s, 1),
|
|
211
|
+
"age_human": _human_age(age_s),
|
|
212
|
+
"notes": m.notes,
|
|
213
|
+
"chunks": m.chunks,
|
|
214
|
+
"embed_model": m.embed_model,
|
|
215
|
+
"embed_dim": m.embed_dim,
|
|
216
|
+
"sha256": m.sha256,
|
|
217
|
+
"dest": str(dest_dir),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _human_age(seconds: float) -> str:
|
|
222
|
+
s = int(seconds)
|
|
223
|
+
if s < 90:
|
|
224
|
+
return f"{s}s"
|
|
225
|
+
m = s // 60
|
|
226
|
+
if m < 90:
|
|
227
|
+
return f"{m}m"
|
|
228
|
+
h = m // 60
|
|
229
|
+
if h < 48:
|
|
230
|
+
return f"{h}h"
|
|
231
|
+
return f"{h // 24}d"
|