secondbrain-py 0.2.1__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.
Files changed (273) hide show
  1. brain/__init__.py +0 -0
  2. brain/__main__.py +18 -0
  3. brain/_capture_command.py +445 -0
  4. brain/_compose.py +52 -0
  5. brain/activity.py +206 -0
  6. brain/ask.py +631 -0
  7. brain/audio.py +591 -0
  8. brain/backfill/__init__.py +12 -0
  9. brain/backfill/search_extras.py +141 -0
  10. brain/backfill/source_rows.py +101 -0
  11. brain/bin/__init__.py +1 -0
  12. brain/bin/_launcher.py +107 -0
  13. brain/bin/down.py +8 -0
  14. brain/bin/launchd.py +268 -0
  15. brain/bin/monitor.py +570 -0
  16. brain/bin/rebuild.py +8 -0
  17. brain/bin/status.py +8 -0
  18. brain/bin/up.py +8 -0
  19. brain/brief.py +272 -0
  20. brain/capture.py +49 -0
  21. brain/chat.py +293 -0
  22. brain/cli.py +9760 -0
  23. brain/cli_claude.py +81 -0
  24. brain/cli_connect.py +285 -0
  25. brain/cli_demo.py +266 -0
  26. brain/config.py +1949 -0
  27. brain/connect.py +925 -0
  28. brain/db.py +540 -0
  29. brain/demo/__init__.py +452 -0
  30. brain/demo/corpus/manifest.json +403 -0
  31. brain/demo/embedder.py +74 -0
  32. brain/durations.py +84 -0
  33. brain/edit_session.py +156 -0
  34. brain/editor.py +67 -0
  35. brain/elicit/__init__.py +16 -0
  36. brain/elicit/detectors.py +250 -0
  37. brain/elicit/drafter.py +70 -0
  38. brain/elicit/queue.py +220 -0
  39. brain/elicit/schema.py +48 -0
  40. brain/elicit/session.py +445 -0
  41. brain/embedding_targets.py +54 -0
  42. brain/embeddings.py +424 -0
  43. brain/enrichment.py +808 -0
  44. brain/errors.py +357 -0
  45. brain/eval/__init__.py +129 -0
  46. brain/eval/answer_eval.py +281 -0
  47. brain/eval/baseline.py +265 -0
  48. brain/eval/concept_extraction.py +378 -0
  49. brain/eval/corpus.py +152 -0
  50. brain/eval/errors.py +19 -0
  51. brain/eval/graph_baseline.py +226 -0
  52. brain/eval/graph_retrieval.py +202 -0
  53. brain/eval/graph_runner.py +319 -0
  54. brain/eval/metrics.py +101 -0
  55. brain/eval/runner.py +223 -0
  56. brain/format.py +783 -0
  57. brain/gaps.py +390 -0
  58. brain/graph_rag/__init__.py +94 -0
  59. brain/graph_rag/_retrieval_common.py +113 -0
  60. brain/graph_rag/aggregates.py +303 -0
  61. brain/graph_rag/aliases/__init__.py +583 -0
  62. brain/graph_rag/backends/__init__.py +10 -0
  63. brain/graph_rag/backends/_age_helpers.py +473 -0
  64. brain/graph_rag/backends/age.py +782 -0
  65. brain/graph_rag/backends/base.py +272 -0
  66. brain/graph_rag/build.py +344 -0
  67. brain/graph_rag/communities.py +644 -0
  68. brain/graph_rag/communities_summary.py +437 -0
  69. brain/graph_rag/concepts.py +202 -0
  70. brain/graph_rag/cooccur.py +193 -0
  71. brain/graph_rag/cross_type.py +312 -0
  72. brain/graph_rag/extract.py +885 -0
  73. brain/graph_rag/fuse.py +371 -0
  74. brain/graph_rag/global_.py +412 -0
  75. brain/graph_rag/grouping.py +372 -0
  76. brain/graph_rag/person_resolver.py +167 -0
  77. brain/graph_rag/reconcile.py +792 -0
  78. brain/graph_rag/relational.py +353 -0
  79. brain/graph_rag/retrieve.py +526 -0
  80. brain/graph_rag/router.py +288 -0
  81. brain/graph_rag/schema.py +320 -0
  82. brain/graph_rag/sync.py +237 -0
  83. brain/graph_rag/tenancy.py +43 -0
  84. brain/graph_rag/themes.py +501 -0
  85. brain/graph_rag/weighting.py +202 -0
  86. brain/ingest/__init__.py +1926 -0
  87. brain/ingest/chunker.py +249 -0
  88. brain/ingest/docx.py +40 -0
  89. brain/ingest/gmail.py +621 -0
  90. brain/ingest/markdown.py +37 -0
  91. brain/ingest/pdf.py +61 -0
  92. brain/ingest/stdin.py +22 -0
  93. brain/ingest/sub_tokens.py +91 -0
  94. brain/ingest/text.py +16 -0
  95. brain/interactions.py +205 -0
  96. brain/maintenance.py +355 -0
  97. brain/mcp_server.py +3405 -0
  98. brain/migrations/001_init.sql +43 -0
  99. brain/migrations/002_qwen3_embedding.sql +17 -0
  100. brain/migrations/003_vault_model.sql +41 -0
  101. brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
  102. brain/migrations/005_derived_links.sql +67 -0
  103. brain/migrations/006_dedup_file_by_source_path.sql +25 -0
  104. brain/migrations/007_email_thread_and_draft.sql +15 -0
  105. brain/migrations/008_gmail_thread_unique.sql +11 -0
  106. brain/migrations/009_chunks_weighted_tsv.sql +28 -0
  107. brain/migrations/010_interactions.sql +30 -0
  108. brain/migrations/011_documents_summary.sql +23 -0
  109. brain/migrations/012_graphrag.sql +171 -0
  110. brain/migrations/013_graphrag_communities.sql +125 -0
  111. brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
  112. brain/migrations/015_interactions_graph_targets.sql +89 -0
  113. brain/migrations/016_index_hygiene.sql +61 -0
  114. brain/migrations/017_elicit.sql +30 -0
  115. brain/migrations/018_review_gap_signal_kinds.sql +40 -0
  116. brain/migrations/019_search_queries.sql +35 -0
  117. brain/migrations/020_link_suggestions.sql +40 -0
  118. brain/migrations/021_timeline_doc_date.sql +34 -0
  119. brain/migrations/022_link_suggestions_undirected.sql +84 -0
  120. brain/migrations/023_search_queries_fts_count.sql +28 -0
  121. brain/quartz_overrides/__init__.py +8 -0
  122. brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
  123. brain/quartz_overrides/quartz/build.ts +568 -0
  124. brain/quartz_overrides/quartz/cli/args.js +152 -0
  125. brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
  126. brain/quartz_overrides/quartz/cli/handlers.js +636 -0
  127. brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
  128. brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
  129. brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
  130. brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
  131. brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
  132. brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
  133. brain/quartz_overrides/quartz/components/Search.tsx +161 -0
  134. brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
  135. brain/quartz_overrides/quartz/components/index.ts +92 -0
  136. brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
  137. brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
  138. brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
  139. brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
  140. brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
  141. brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
  142. brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
  143. brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
  144. brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
  145. brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
  146. brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
  147. brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
  148. brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
  149. brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
  150. brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
  151. brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
  152. brain/quartz_overrides/quartz/processors/parse.ts +371 -0
  153. brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
  154. brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
  155. brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
  156. brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
  157. brain/quartz_overrides/quartz/static/emailThread.js +334 -0
  158. brain/quartz_overrides/quartz/static/favicon.ico +0 -0
  159. brain/quartz_overrides/quartz/static/icon.png +0 -0
  160. brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
  161. brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
  162. brain/quartz_overrides/quartz/static/reload.js +168 -0
  163. brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
  164. brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
  165. brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
  166. brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
  167. brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
  168. brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
  169. brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
  170. brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
  171. brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
  172. brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
  173. brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
  174. brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
  175. brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
  176. brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
  177. brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
  178. brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
  179. brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
  180. brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
  181. brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
  182. brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
  183. brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
  184. brain/quartz_overrides/quartz/styles/custom.scss +89 -0
  185. brain/quartz_overrides/quartz/styles/graph.scss +505 -0
  186. brain/quartz_overrides/quartz/util/ctx.ts +92 -0
  187. brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
  188. brain/quartz_overrides/quartz/util/path.ts +358 -0
  189. brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
  190. brain/quartz_overrides/quartz.config.ts +270 -0
  191. brain/quartz_overrides/quartz.layout.ts +314 -0
  192. brain/queries.py +1188 -0
  193. brain/rank_fusion.py +8 -0
  194. brain/resurface.py +210 -0
  195. brain/review/__init__.py +26 -0
  196. brain/review/emit.py +27 -0
  197. brain/review/queries.py +436 -0
  198. brain/review/render.py +196 -0
  199. brain/review/scans.py +355 -0
  200. brain/review/weekly.py +413 -0
  201. brain/search.py +704 -0
  202. brain/set_similarity.py +15 -0
  203. brain/setup.py +1205 -0
  204. brain/tags.py +56 -0
  205. brain/templates/Caddyfile.j2 +9 -0
  206. brain/templates/__init__.py +1 -0
  207. brain/templates/bin/__init__.py +1 -0
  208. brain/templates/bin/_brain-brief-fg.sh +25 -0
  209. brain/templates/bin/_brain-build-fg.sh +53 -0
  210. brain/templates/bin/_brain-watcher-fg.sh +65 -0
  211. brain/templates/bin/brain-down.sh +89 -0
  212. brain/templates/bin/brain-status.sh +83 -0
  213. brain/templates/bin/brain-up.sh +221 -0
  214. brain/templates/docker/age/Dockerfile +79 -0
  215. brain/templates/docker-compose.stock.yml.j2 +26 -0
  216. brain/templates/docker-compose.yml.j2 +34 -0
  217. brain/templates/env.example +190 -0
  218. brain/templates/launchd/__init__.py +1 -0
  219. brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
  220. brain/templates/launchd/com.brain.build.plist.j2 +46 -0
  221. brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
  222. brain/templates/skill/SKILL.md +63 -0
  223. brain/templates/skill/__init__.py +1 -0
  224. brain/timeline.py +834 -0
  225. brain/todo.py +124 -0
  226. brain/uninstall.py +185 -0
  227. brain/vault/__init__.py +115 -0
  228. brain/vault/_atomic.py +25 -0
  229. brain/vault/daily_index.py +228 -0
  230. brain/vault/derived_links/__init__.py +50 -0
  231. brain/vault/derived_links/directory.py +683 -0
  232. brain/vault/derived_links/fence.py +408 -0
  233. brain/vault/derived_links/gws.py +64 -0
  234. brain/vault/derived_links/participants.py +143 -0
  235. brain/vault/derived_links/pass_runner.py +362 -0
  236. brain/vault/derived_links/rules.py +137 -0
  237. brain/vault/export.py +683 -0
  238. brain/vault/frontmatter.py +165 -0
  239. brain/vault/graph.py +620 -0
  240. brain/vault/graph_format.py +388 -0
  241. brain/vault/link_rewrite.py +235 -0
  242. brain/vault/links.py +260 -0
  243. brain/vault/note_builder.py +211 -0
  244. brain/vault/paths.py +55 -0
  245. brain/vault/quartz_overlay.py +236 -0
  246. brain/vault/rename.py +591 -0
  247. brain/vault/resolver.py +304 -0
  248. brain/vault/slug.py +127 -0
  249. brain/vault/sync.py +1513 -0
  250. brain/vault/sync_summaries.py +264 -0
  251. brain/vault/templates.py +145 -0
  252. brain/vault/watch.py +1052 -0
  253. brain/wiki/__init__.py +6 -0
  254. brain/wiki/_github_slugger.py +76 -0
  255. brain/wiki/_person_name.py +314 -0
  256. brain/wiki/build_homepage.py +541 -0
  257. brain/wiki/build_partial.py +273 -0
  258. brain/wiki/build_people.py +934 -0
  259. brain/wiki/build_related.py +758 -0
  260. brain/wiki/build_swap.py +585 -0
  261. brain/wiki/build_watcher.py +975 -0
  262. brain/wiki/edit_classifier.py +215 -0
  263. brain/wiki/errors.py +10 -0
  264. brain/wiki/fastpath_manifest.py +475 -0
  265. brain/wiki/fastpath_state.py +174 -0
  266. brain/wiki/install.py +296 -0
  267. brain/wiki/slug.py +111 -0
  268. secondbrain_py-0.2.1.dist-info/METADATA +195 -0
  269. secondbrain_py-0.2.1.dist-info/RECORD +273 -0
  270. secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
  271. secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
  272. secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
  273. secondbrain_py-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,334 @@
1
+ // Brain wiki — email-thread reading mode runtime (P4.4 of the Wiki UX
2
+ // Overhaul).
3
+ //
4
+ // This file is a TEMPLATE. It is installed at
5
+ // `<vault>/.quartz/quartz/static/emailThread.js` by `brain vault render
6
+ // --overlay`, and is copied verbatim into every build's `static/` dir
7
+ // by Quartz's stock `Plugin.Static()` emitter (no extra wiring needed —
8
+ // Quartz already mirrors `quartz/static/` 1:1 into the build output).
9
+ // The script is referenced from page `<head>` by the brain
10
+ // `Plugin.EmailThreadReader` transformer's `externalResources()` hook
11
+ // (see `emailThread.ts` for the inject side).
12
+ //
13
+ // Tested against Quartz v4.5.x (April 2026). Plain vanilla JS — no
14
+ // transpile, no bundler — so it survives any future Quartz refactor of
15
+ // its plugin pipeline. The only contracts are (a) Quartz keeps copying
16
+ // `quartz/static/*` into `<build>/static/`, and (b) `brain.ingest.gmail
17
+ // .to_extracted_thread` keeps emitting `## YYYY-MM-DD HH:MM — <from>`
18
+ // for the latest message and `<details><summary>YYYY-MM-DD HH:MM —
19
+ // <from></summary>...` for older messages. If the markdown assembly
20
+ // shape changes, update `parseFromAddress` and the section walker
21
+ // together.
22
+ //
23
+ // What this script does, and why: an email_thread page (markdown body
24
+ // produced by `to_extracted_thread`) renders as a leading `<h2>` (the
25
+ // most recent message — always visible) followed by zero or more
26
+ // `<details>` elements (older messages, collapsed by default). The
27
+ // script enhances that shape with two affordances:
28
+ //
29
+ // 1. Annotates every message section with `data-brain-thread-from`
30
+ // (the From address parsed from the heading) and
31
+ // `data-brain-is-mine` ("true" iff the From contains the user's
32
+ // email — read from `window.BRAIN_USER_EMAIL`, baked in at build
33
+ // time by the EmailThreadReader transformer).
34
+ // 2. Renders a "Show only my replies" toggle button at the top of
35
+ // `<article>`. State persists in localStorage under
36
+ // `brain.email.repliesOnly` as a JSON boolean. When ON, the body
37
+ // gets a `brain-replies-only` class which `_email_thread.scss`
38
+ // uses to hide every `[data-brain-is-mine="false"]` section.
39
+ //
40
+ // Detection: only runs on email-thread pages. The detection heuristic
41
+ // is `/_ingested/gmail/` in the pathname AND the article contains
42
+ // either a `<details>` element or an `<h2>` whose text matches the
43
+ // "YYYY-MM-DD HH:MM — <from>" shape. Two-condition gate so a non-thread
44
+ // page that happens to live under `_ingested/gmail/` (unlikely, but
45
+ // defensive) doesn't get a stray button. Symmetric with how the
46
+ // explorer toggle (P4.2) gates itself.
47
+ //
48
+ // Idempotent: the wrapping logic and button render check for an
49
+ // existing wiring marker on `<article>` and skip if already done. Re-
50
+ // runs on Quartz SPA `nav` events so navigation between thread pages
51
+ // re-wires the new article.
52
+ //
53
+ // Browser support: targets evergreen Chrome / Firefox / Safari. Uses
54
+ // `document.querySelectorAll`, `dataset`, `classList`, and
55
+ // `addEventListener` — all baseline since 2017.
56
+ ;(function () {
57
+ // brain: localStorage key for the "Show only my replies" filter
58
+ // state. Mirrors the `brain.<feature>.<setting>` namespace from
59
+ // search.inline.ts (`brain.search.activeSources`) and explorer
60
+ // .inline.ts (`brain.explorer.showIngested`) so brain prefs cluster
61
+ // cleanly under one prefix in devtools.
62
+ var REPLIES_ONLY_KEY = "brain.email.repliesOnly"
63
+
64
+ // brain: data attributes the SCSS reads. Centralised here so a
65
+ // future rename is a one-line change in JS + a search-replace in
66
+ // _email_thread.scss.
67
+ var FROM_ATTR = "data-brain-thread-from"
68
+ var IS_MINE_ATTR = "data-brain-is-mine"
69
+ var WIRED_ATTR = "data-brain-thread-wired"
70
+
71
+ // brain: body class flipped by the toggle. SCSS keys off this to
72
+ // hide non-user messages.
73
+ var REPLIES_ONLY_CLASS = "brain-replies-only"
74
+
75
+ // brain: section class added to wrapped latest-message + each
76
+ // <details>. Gives the SCSS a single hook regardless of whether the
77
+ // section is the leading H2 (wrapped at runtime) or a markdown
78
+ // <details>.
79
+ var SECTION_CLASS = "brain-thread-message"
80
+ var LATEST_CLASS = "brain-thread-latest"
81
+
82
+ // brain: button class. Matches the spec verbatim — the static test
83
+ // pins this literal so a future rename trips it.
84
+ var TOGGLE_CLASS = "brain-email-replies-only-toggle"
85
+
86
+ // brain: button labels. Factored out so the static test can pin
87
+ // them and a future i18n pass has a single seam to retarget.
88
+ var LABELS = {
89
+ show: "Show only my replies",
90
+ showingMine: "Show all messages",
91
+ }
92
+
93
+ // brain: heading separator between the date and the From address,
94
+ // emitted by `to_extracted_thread`'s `_format_thread_section`. Pinned
95
+ // here as a constant rather than an inline literal so a future
96
+ // markdown-shape change has a single seam.
97
+ var FROM_SEPARATOR = " — "
98
+
99
+ // brain: extract the `From` address from a heading text like
100
+ // `"2026-04-28 14:00 — Alice <alice@example.com>"`. Returns the
101
+ // substring after the first ` — ` (em-dash padded with single
102
+ // spaces — the exact form `to_extracted_thread` writes), or empty
103
+ // string when the separator is missing. We don't try to crack the
104
+ // address into name/local/domain — substring containment against
105
+ // `BRAIN_USER_EMAIL` is enough to decide ownership and tolerates
106
+ // both `Name <addr>` and bare-address forms.
107
+ function parseFromAddress(text) {
108
+ if (typeof text !== "string") return ""
109
+ var idx = text.indexOf(FROM_SEPARATOR)
110
+ if (idx < 0) return ""
111
+ return text.slice(idx + FROM_SEPARATOR.length).trim()
112
+ }
113
+
114
+ // brain: heuristic match for "this heading text looks like a
115
+ // per-message thread heading". Used for the email-thread page
116
+ // gate so a stray H2 (`## Conclusion`) on an unrelated page can't
117
+ // be mistaken for a thread message. The H2 must contain ` — ` AND
118
+ // start with a YYYY-MM-DD date pattern.
119
+ var THREAD_HEADING_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}\s+—\s+/
120
+
121
+ // brain: case-insensitive substring match between the parsed `from`
122
+ // and the user's email. We match on the email (not display name)
123
+ // because Gmail headers are stable on the address and noisy on the
124
+ // name (people change display names per client). Empty user email
125
+ // → no match (button still renders; toggling hides everything,
126
+ // which is the right "you forgot to set BRAIN_USER_EMAIL" cue).
127
+ function fromMatchesUser(from, userEmail) {
128
+ if (!from || !userEmail) return false
129
+ return from.toLowerCase().indexOf(userEmail.toLowerCase()) !== -1
130
+ }
131
+
132
+ // brain: read the persisted toggle state. Default (no key, missing
133
+ // localStorage, parse failure, non-boolean) is `false` so a fresh
134
+ // visit shows every message — discoverable affordance over hidden-
135
+ // by-default. Try/catch covers Safari private mode which can throw
136
+ // on `localStorage.getItem`.
137
+ function loadRepliesOnly() {
138
+ if (typeof localStorage === "undefined") return false
139
+ try {
140
+ var raw = localStorage.getItem(REPLIES_ONLY_KEY)
141
+ if (raw === null) return false
142
+ var parsed = JSON.parse(raw)
143
+ return typeof parsed === "boolean" ? parsed : false
144
+ } catch (err) {
145
+ return false
146
+ }
147
+ }
148
+
149
+ // brain: persist the toggle state. Errors are swallowed —
150
+ // localStorage may throw `QuotaExceededError` in private mode, and
151
+ // a failed write should not tear down the article. Worst case the
152
+ // user re-flips the toggle next session.
153
+ function persistRepliesOnly(value) {
154
+ if (typeof localStorage === "undefined") return
155
+ try {
156
+ localStorage.setItem(REPLIES_ONLY_KEY, JSON.stringify(value))
157
+ } catch (err) {
158
+ // intentional: see docstring
159
+ }
160
+ }
161
+
162
+ // brain: get the user's email from the `<script>` global the build-
163
+ // time transformer injected. Tolerates a missing global (script
164
+ // injection failed, dev-only build skipped the transformer, etc.) —
165
+ // returns "" rather than throwing.
166
+ function getUserEmail() {
167
+ var raw =
168
+ typeof window !== "undefined" && typeof window.BRAIN_USER_EMAIL === "string"
169
+ ? window.BRAIN_USER_EMAIL
170
+ : ""
171
+ return raw.trim()
172
+ }
173
+
174
+ // brain: detect an email-thread page. Two-part gate:
175
+ // 1. URL contains `/_ingested/gmail/` (the brain mirror tier
176
+ // where thread pages live).
177
+ // 2. The article DOM has either a `<details>` element or an
178
+ // `<h2>` whose text matches the per-message heading shape.
179
+ // Both checks must pass — keeps the toggle off non-thread pages
180
+ // even if a future feature mounts a stray gmail-mirror page.
181
+ function isThreadPage(article) {
182
+ if (!article) return false
183
+ if (window.location.pathname.indexOf("/_ingested/gmail/") === -1) {
184
+ return false
185
+ }
186
+ if (article.querySelector("details")) return true
187
+ var h2s = article.querySelectorAll("h2")
188
+ for (var i = 0; i < h2s.length; i++) {
189
+ if (THREAD_HEADING_RE.test(h2s[i].textContent || "")) return true
190
+ }
191
+ return false
192
+ }
193
+
194
+ // brain: wrap the leading H2-message (and its trailing siblings up
195
+ // to the first `<details>` / end of article) in a synthetic
196
+ // `<section>` so the SCSS / filter has the same shape regardless of
197
+ // whether the section is markdown-emitted `<details>` or the latest
198
+ // plain H2. The wrapper carries the same data attributes so the
199
+ // toggle can hide it via the `[data-brain-is-mine]` selector.
200
+ //
201
+ // Rationale: `to_extracted_thread` renders the latest message as
202
+ // `## H2 + body` (test-pinned in `tests/test_gmail_thread.py`). We
203
+ // can't change that without churning the test contract; wrapping
204
+ // client-side gets us a uniform DOM shape with zero ingest-side
205
+ // change. Idempotent — checks `WIRED_ATTR` on `<article>` first.
206
+ function wrapLatestMessage(article, userEmail) {
207
+ var children = Array.prototype.slice.call(article.children)
208
+ var startIdx = -1
209
+ for (var i = 0; i < children.length; i++) {
210
+ var child = children[i]
211
+ if (
212
+ child.tagName === "H2" &&
213
+ THREAD_HEADING_RE.test(child.textContent || "")
214
+ ) {
215
+ startIdx = i
216
+ break
217
+ }
218
+ }
219
+ if (startIdx === -1) return // nothing to wrap
220
+
221
+ var endIdx = children.length
222
+ for (var j = startIdx + 1; j < children.length; j++) {
223
+ var node = children[j]
224
+ if (
225
+ node.tagName === "DETAILS" ||
226
+ // P4.4 — the runtime button itself is also injected at the top
227
+ // of <article>; don't sweep it into the latest-message section.
228
+ (node.classList && node.classList.contains(TOGGLE_CLASS))
229
+ ) {
230
+ endIdx = j
231
+ break
232
+ }
233
+ }
234
+
235
+ var section = document.createElement("section")
236
+ section.className = SECTION_CLASS + " " + LATEST_CLASS
237
+ var fromText = parseFromAddress(children[startIdx].textContent || "")
238
+ section.setAttribute(FROM_ATTR, fromText)
239
+ section.setAttribute(
240
+ IS_MINE_ATTR,
241
+ String(fromMatchesUser(fromText, userEmail)),
242
+ )
243
+ children[startIdx].parentNode.insertBefore(section, children[startIdx])
244
+ for (var k = startIdx; k < endIdx; k++) {
245
+ section.appendChild(children[k])
246
+ }
247
+ }
248
+
249
+ // brain: stamp `data-brain-thread-from` + `data-brain-is-mine` on
250
+ // each `<details>` inside the article. The summary's text carries
251
+ // the same `YYYY-MM-DD HH:MM — <from>` heading we parse from H2s.
252
+ // Adds the section class for SCSS parity with the wrapped latest.
253
+ function annotateDetails(article, userEmail) {
254
+ var detailsList = article.querySelectorAll("details")
255
+ for (var i = 0; i < detailsList.length; i++) {
256
+ var d = detailsList[i]
257
+ if (d.hasAttribute(FROM_ATTR)) continue // idempotent
258
+ d.classList.add(SECTION_CLASS)
259
+ var summary = d.querySelector("summary")
260
+ var fromText = summary ? parseFromAddress(summary.textContent || "") : ""
261
+ d.setAttribute(FROM_ATTR, fromText)
262
+ d.setAttribute(
263
+ IS_MINE_ATTR,
264
+ String(fromMatchesUser(fromText, userEmail)),
265
+ )
266
+ }
267
+ }
268
+
269
+ // brain: build + insert the toggle button at the top of <article>.
270
+ // Idempotent — bails if a button is already present (covers SPA
271
+ // re-init on the same article element). The button's
272
+ // `aria-pressed` mirrors the body class state for assistive tech.
273
+ function renderToggle(article) {
274
+ if (article.querySelector("." + TOGGLE_CLASS)) return
275
+ var pressed = loadRepliesOnly()
276
+
277
+ var button = document.createElement("button")
278
+ button.type = "button"
279
+ button.className = TOGGLE_CLASS
280
+ button.setAttribute("aria-pressed", String(pressed))
281
+ button.textContent = pressed ? LABELS.showingMine : LABELS.show
282
+
283
+ document.body.classList.toggle(REPLIES_ONLY_CLASS, pressed)
284
+
285
+ button.addEventListener("click", function () {
286
+ var next = !document.body.classList.contains(REPLIES_ONLY_CLASS)
287
+ document.body.classList.toggle(REPLIES_ONLY_CLASS, next)
288
+ persistRepliesOnly(next)
289
+ button.setAttribute("aria-pressed", String(next))
290
+ button.textContent = next ? LABELS.showingMine : LABELS.show
291
+ })
292
+
293
+ article.insertBefore(button, article.firstChild)
294
+ }
295
+
296
+ // brain: top-level entry. Locates `<article>`, gates on
297
+ // `isThreadPage`, then runs annotate + wrap + render. Idempotent at
298
+ // article granularity (`WIRED_ATTR`).
299
+ function init() {
300
+ var article = document.querySelector("article")
301
+ if (!article) return
302
+ if (!isThreadPage(article)) {
303
+ // Not a thread page — but still flip the body class so a stale
304
+ // `brain-replies-only` from a prior thread page doesn't bleed
305
+ // into a non-thread navigation.
306
+ document.body.classList.remove(REPLIES_ONLY_CLASS)
307
+ return
308
+ }
309
+ if (article.getAttribute(WIRED_ATTR) === "true") {
310
+ // Re-runs of init() on the same article (SPA back/forward) just
311
+ // re-sync the body class with persisted state — no DOM rewrite.
312
+ document.body.classList.toggle(REPLIES_ONLY_CLASS, loadRepliesOnly())
313
+ return
314
+ }
315
+
316
+ var userEmail = getUserEmail()
317
+ annotateDetails(article, userEmail)
318
+ wrapLatestMessage(article, userEmail)
319
+ renderToggle(article)
320
+ article.setAttribute(WIRED_ATTR, "true")
321
+ }
322
+
323
+ if (document.readyState === "loading") {
324
+ document.addEventListener("DOMContentLoaded", init)
325
+ } else {
326
+ init()
327
+ }
328
+
329
+ // brain: re-run on Quartz SPA navigation. `enableSPA: true` in
330
+ // `quartz.config.ts` makes Quartz dispatch a `nav` event on
331
+ // `document` after each in-page route change — same hook the
332
+ // linkSourceTag.js script uses.
333
+ document.addEventListener("nav", init)
334
+ })()
@@ -0,0 +1,104 @@
1
+ // Brain wiki redesign — link source-tagger client script (Lane B).
2
+ //
3
+ // This file is a TEMPLATE. It is installed at
4
+ // `<vault>/.quartz/quartz/static/linkSourceTag.js` by `brain vault
5
+ // render --overlay`, and is copied verbatim into every build's
6
+ // `static/` dir by Quartz's stock `Plugin.Static()` emitter (no extra
7
+ // wiring needed — Quartz already mirrors `quartz/static/` 1:1 into
8
+ // the build output). The script is referenced from page `<head>` by
9
+ // the brain `Plugin.LinkSourceTag` transformer's `externalResources()`
10
+ // hook (see `linkSourceTag.ts` for the inject side).
11
+ //
12
+ // Tested against Quartz v4.5.x (April 2026). Plain vanilla JS — no
13
+ // transpile, no bundler — so it survives any future Quartz refactor
14
+ // of its plugin pipeline. The only contracts are (a) Quartz keeps
15
+ // copying `quartz/static/*` into `<build>/static/`, and (b) the
16
+ // brain redesign keeps the `data-brain-link-kind="ingested"` CSS
17
+ // attribute selector pattern.
18
+ //
19
+ // What this script does, and why: the `linkKindMark.ts` transformer
20
+ // stamps `data-brain-link-kind="ingested"` on every `<a>` whose URL
21
+ // starts with `_ingested/`, but it can't determine WHICH source
22
+ // (krisp/slack/gmail/manual) the target came from without a
23
+ // document-level lookup that's expensive at build time. The source
24
+ // is encoded in the URL path itself (`_ingested/<source>/<slug>`),
25
+ // so this 25-line client script extracts it once on `DOMContentLoaded`
26
+ // and stamps `data-brain-source="<source>"` on each ingested link.
27
+ // `_links.scss` reads that attribute via `&[data-brain-source="krisp"]`
28
+ // rules to pick the source-tinted left-rail color from the
29
+ // `--brain-source-*` token palette in `_tokens.scss`.
30
+ //
31
+ // Idempotent: if `data-brain-source` is already set (e.g. SPA
32
+ // navigation re-runs us, or a future build-time emitter pre-stamps
33
+ // it), we skip — first writer wins. Quartz's enableSPA mode
34
+ // dispatches `nav` events on each route change; the listener at the
35
+ // bottom re-runs the tagger so links rendered into the new page get
36
+ // tagged too.
37
+ //
38
+ // Browser support: targets evergreen Chrome / Firefox / Safari. Uses
39
+ // `document.querySelectorAll`, `forEach`, and `addEventListener` —
40
+ // all baseline since 2017.
41
+ ;(function () {
42
+ // brain: the four sources `brain ingest --source <name>` accepts.
43
+ // Anything else falls back to "manual" via the `--brain-source-manual`
44
+ // CSS variable default in `_links.scss`. Keeping the allowlist
45
+ // explicit means we don't accidentally stamp `data-brain-source="."`
46
+ // on a malformed URL.
47
+ var KNOWN_SOURCES = ["krisp", "slack", "gmail", "manual"]
48
+
49
+ // brain: the data-attribute the SCSS reads. Mirrors the constant in
50
+ // `_links.scss`. If you rename one, rename both.
51
+ var SOURCE_ATTR = "data-brain-source"
52
+
53
+ // brain: extract the source segment from a `_ingested/<source>/...`
54
+ // URL. Handles three forms the build may emit:
55
+ // * `_ingested/krisp/...` (relative)
56
+ // * `/_ingested/krisp/...` (absolute)
57
+ // * `./_ingested/krisp/...` (explicit relative)
58
+ // Returns the source string (lowercased, validated against the
59
+ // allowlist) or `null` if extraction fails.
60
+ function sourceFromHref(href) {
61
+ if (typeof href !== "string") return null
62
+ // Strip query strings + fragments before splitting on `/`.
63
+ var clean = href.split("?")[0].split("#")[0]
64
+ var match = clean.match(/(?:^|\/|\.\/)_ingested\/([^/]+)/)
65
+ if (match === null) return null
66
+ var source = match[1].toLowerCase()
67
+ if (KNOWN_SOURCES.indexOf(source) === -1) return null
68
+ return source
69
+ }
70
+
71
+ // brain: tag every ingested-link `<a>` in the document. Selector
72
+ // covers both the relative and absolute href forms; the prefix
73
+ // check below also covers the explicit `./` form. Idempotent —
74
+ // skips elements that already carry `data-brain-source`.
75
+ function tagAll() {
76
+ var nodes = document.querySelectorAll(
77
+ 'a[href^="_ingested/"], a[href*="/_ingested/"], a[href^="./_ingested/"]',
78
+ )
79
+ nodes.forEach(function (a) {
80
+ if (a.hasAttribute(SOURCE_ATTR)) return
81
+ var source = sourceFromHref(a.getAttribute("href"))
82
+ if (source === null) return
83
+ a.setAttribute(SOURCE_ATTR, source)
84
+ })
85
+ }
86
+
87
+ // brain: run on first DOM ready. `DOMContentLoaded` fires before
88
+ // first paint of below-fold content, so any flash-of-unstyled-link
89
+ // is bounded to above-fold links — acceptable per the redesign
90
+ // plan's risk note.
91
+ if (document.readyState === "loading") {
92
+ document.addEventListener("DOMContentLoaded", tagAll)
93
+ } else {
94
+ tagAll()
95
+ }
96
+
97
+ // brain: re-run on Quartz SPA navigation. `enableSPA: true` in
98
+ // `quartz.config.ts` makes Quartz dispatch a `nav` event on
99
+ // `document` after each in-page route change (see
100
+ // `quartz/components/scripts/spa.inline.ts` upstream). Listening for
101
+ // it keeps source tagging consistent across the whole session
102
+ // without a full page reload.
103
+ document.addEventListener("nav", tagAll)
104
+ })()
@@ -0,0 +1,142 @@
1
+ // Brain wiki — live relative-date client script (home-page Recent rail).
2
+ //
3
+ // This file is a TEMPLATE. It is installed at
4
+ // `<vault>/.quartz/quartz/static/relativeDate.js` by `brain vault
5
+ // render --overlay`, and is copied verbatim into every build's
6
+ // `static/` dir by Quartz's stock `Plugin.Static()` emitter (no extra
7
+ // wiring needed — Quartz already mirrors `quartz/static/` 1:1 into
8
+ // the build output). The script is referenced from page `<head>` by
9
+ // the brain `Plugin.RelativeDate` transformer's `externalResources()`
10
+ // hook (see `relativeDate.ts` for the inject side).
11
+ //
12
+ // Tested against Quartz v4.5.x (April 2026). Plain vanilla JS — no
13
+ // transpile, no bundler — so it survives any future Quartz refactor
14
+ // of its plugin pipeline. The only contracts are (a) Quartz keeps
15
+ // copying `quartz/static/*` into `<build>/static/`, and (b) the
16
+ // recent-rail markup keeps emitting `<span class="brain-rel-date"
17
+ // data-date="YYYY-MM-DD">…</span>` (see
18
+ // `brain.wiki.build_homepage._render_bullets`). The `data-date` is a plain
19
+ // CALENDAR date (`YYYY-MM-DD`, no time, no offset) — NOT a full ISO 8601
20
+ // timestamp. It is parsed here as a LOCAL naive date (no UTC parse), so a
21
+ // Krisp meeting stored at UTC-midnight renders on its real calendar day in
22
+ // every timezone instead of shifting back a day.
23
+ //
24
+ // What this script does, and why: the recent rail in `index.md` used to
25
+ // bake a relative string ("today", "3d ago") at BUILD time. That string
26
+ // decays — the home note isn't re-rendered daily, so a doc ingested 3
27
+ // days before a build still reads "3d ago" weeks later. The server now
28
+ // emits the ABSOLUTE date as `data-date` (the machine-readable source of
29
+ // truth) and a non-decaying "Jun 10"-style fallback as the span's text.
30
+ // This script recomputes the relative bucket client-side on every page
31
+ // load + SPA navigation, so the rail is always honest relative to the
32
+ // reader's current local date.
33
+ //
34
+ // BUCKET PARITY: this MUST mirror
35
+ // `brain.wiki.build_homepage._format_relative_date` exactly — same
36
+ // calendar-day delta (LOCAL time; compare local-midnight dates, not 24h
37
+ // windows) and the same four buckets:
38
+ // * delta <= 0 (today or future) → "today"
39
+ // * 1..6 → "Nd ago"
40
+ // * 7..34 → "Nw ago" (delta // 7)
41
+ // * >= 35 → "Mon D" (e.g. "Jun 10")
42
+ // If you change a bucket here, change it there too (and the parity test
43
+ // in `tests/test_brain_recent_homepage.py`).
44
+ //
45
+ // Idempotent + safe: a missing or unparseable `data-date` leaves the
46
+ // element's existing text (the absolute fallback) untouched, so the
47
+ // reader never sees a blank or "Invalid Date" cell. Re-runs on the
48
+ // Quartz `nav` SPA event so spans rendered into a new route get the live
49
+ // text too.
50
+ //
51
+ // Browser support: targets evergreen Chrome / Firefox / Safari. Uses
52
+ // `document.querySelectorAll`, `forEach`, `addEventListener`, and the
53
+ // `Date` API — all baseline since 2017.
54
+ ;(function () {
55
+ // brain: English month abbreviations, index 0 = January. Hard-coded
56
+ // (NOT `toLocaleString`) so the rendered "Mon D" fallback matches
57
+ // Python's `strftime("%b")` English output regardless of the browser
58
+ // locale — avoids env drift between the server-baked fallback and the
59
+ // client-recomputed >= 35-day bucket.
60
+ var MONTHS = [
61
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
62
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
63
+ ]
64
+
65
+ // brain: project a Date onto local midnight so the delta is a calendar-
66
+ // day count (matching Python's local-midnight calendar-day delta; the
67
+ // UTC-vs-local date decision is made server-side in
68
+ // `_recent_calendar_date`, then emitted as the `YYYY-MM-DD` `data-date`
69
+ // this script parses as a local naive date), not a raw elapsed-hours
70
+ // count. Without this, a doc ingested at 23:59 yesterday vs one at 00:01
71
+ // today would mis-bucket near the boundary.
72
+ function localMidnight(d) {
73
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate())
74
+ }
75
+
76
+ // brain: compute the relative-date text for a `YYYY-MM-DD` `data-date`.
77
+ // Parses STRICTLY as a calendar date — split the parts and build a LOCAL
78
+ // midnight `Date` (`new Date(y, m-1, d)`), NEVER `new Date(iso)` (which
79
+ // would parse a bare `YYYY-MM-DD` as UTC and shift the day back in
80
+ // negative-offset zones — the off-by-one bug). Returns `null` when the
81
+ // value is missing or doesn't match the calendar-date shape (empty,
82
+ // garbage, or an old full-ISO timestamp) so the caller leaves the absolute
83
+ // fallback in place. Mirrors the bucket order in `_format_relative_date`.
84
+ function relativeText(iso) {
85
+ if (typeof iso !== "string" || iso.length === 0) return null
86
+ var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso)
87
+ if (m === null) return null
88
+ var when = new Date(
89
+ parseInt(m[1], 10),
90
+ parseInt(m[2], 10) - 1,
91
+ parseInt(m[3], 10),
92
+ )
93
+ if (isNaN(when.getTime())) return null
94
+
95
+ var nowMid = localMidnight(new Date())
96
+ var whenMid = localMidnight(when)
97
+ // Whole-day delta. `getTime()` is ms since epoch; dividing the local-
98
+ // midnight difference by a day yields an integer calendar-day count.
99
+ // Round to absorb any DST-induced sub-millisecond drift.
100
+ var deltaDays = Math.round(
101
+ (nowMid.getTime() - whenMid.getTime()) / 86400000,
102
+ )
103
+
104
+ if (deltaDays <= 0) return "today"
105
+ if (deltaDays < 7) return deltaDays + "d ago"
106
+ if (deltaDays < 35) return Math.floor(deltaDays / 7) + "w ago"
107
+ // >= 35 days → absolute "Mon D" (no leading zero on the day), matching
108
+ // the server's `_format_absolute_date`.
109
+ return MONTHS[whenMid.getMonth()] + " " + whenMid.getDate()
110
+ }
111
+
112
+ // brain: rewrite every recent-rail span's text from its `data-date`.
113
+ // Idempotent — recomputing from `data-date` (never from the existing
114
+ // text) means repeated runs converge on the same value, and a bad
115
+ // `data-date` is a no-op (the absolute fallback text stays put).
116
+ function updateAll() {
117
+ var nodes = document.querySelectorAll(".brain-rel-date[data-date]")
118
+ nodes.forEach(function (el) {
119
+ var text = relativeText(el.getAttribute("data-date"))
120
+ if (text === null) return
121
+ el.textContent = text
122
+ })
123
+ }
124
+
125
+ // brain: run on first DOM ready. `DOMContentLoaded` fires before first
126
+ // paint of below-fold content, so any flash of the absolute fallback is
127
+ // bounded to above-fold spans — acceptable, and the fallback is a real
128
+ // date either way.
129
+ if (document.readyState === "loading") {
130
+ document.addEventListener("DOMContentLoaded", updateAll)
131
+ } else {
132
+ updateAll()
133
+ }
134
+
135
+ // brain: re-run on Quartz SPA navigation. `enableSPA: true` in
136
+ // `quartz.config.ts` makes Quartz dispatch a `nav` event on `document`
137
+ // after each in-page route change (see
138
+ // `quartz/components/scripts/spa.inline.ts` upstream). Listening for it
139
+ // keeps the relative dates live across the whole session without a full
140
+ // page reload.
141
+ document.addEventListener("nav", updateAll)
142
+ })()