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,358 @@
1
+ // Sample Quartz v4 path utilities for brain vaults.
2
+ //
3
+ // This file is a TEMPLATE. It is installed at
4
+ // `<vault>/.quartz/quartz/util/path.ts` by `brain vault render --overlay`,
5
+ // replacing the stock upstream module verbatim.
6
+ //
7
+ // Tested against Quartz v4.5.x (April 2026). If a future Quartz version
8
+ // changes the public API of this module — `transformInternalLink`,
9
+ // `transformLink`, `slugifyFilePath`, etc. — pull the latest from
10
+ // https://github.com/jackyzha0/quartz/blob/v4/quartz/util/path.ts and
11
+ // re-apply the brain tweaks flagged below with `// brain:` comments.
12
+ //
13
+ // What this brain tweak does, and why: a Quartz build will crash with
14
+ // `URIError: URI malformed` if any inline-HTML `href`/`src` (or any
15
+ // markdown link target) contains a raw `%` not followed by valid
16
+ // hex — typical for ERB / Liquid / Jinja template fragments embedded
17
+ // in pasted email/HTML content (`<%= loginurl %>`, `{{ url }}`, etc.).
18
+ // Stock Quartz calls `decodeURI(link)` directly, which throws on
19
+ // malformed input. The brain patch wraps that single call in a
20
+ // `safeDecodeURI` helper that catches `URIError` and returns the raw
21
+ // string unchanged, so the build keeps going and the doc still
22
+ // renders (the malformed `href` becomes an opaque link, but no crash).
23
+ // Everything else in this file is upstream verbatim — diff against
24
+ // upstream to see the brain delta isolated to two lines.
25
+ import { slug as slugAnchor } from "github-slugger"
26
+ import type { Element as HastElement } from "hast"
27
+ import { clone } from "./clone"
28
+
29
+ // this file must be isomorphic so it can't use node libs (e.g. path)
30
+
31
+ export const QUARTZ = "quartz"
32
+
33
+ /// Utility type to simulate nominal types in TypeScript
34
+ type SlugLike<T> = string & { __brand: T }
35
+
36
+ /** Cannot be relative and must have a file extension. */
37
+ export type FilePath = SlugLike<"filepath">
38
+ export function isFilePath(s: string): s is FilePath {
39
+ const validStart = !s.startsWith(".")
40
+ return validStart && _hasFileExtension(s)
41
+ }
42
+
43
+ /** Cannot be relative and may not have leading or trailing slashes. It can have `index` as it's last segment. Use this wherever possible is it's the most 'general' interpretation of a slug. */
44
+ export type FullSlug = SlugLike<"full">
45
+ export function isFullSlug(s: string): s is FullSlug {
46
+ const validStart = !(s.startsWith(".") || s.startsWith("/"))
47
+ const validEnding = !s.endsWith("/")
48
+ return validStart && validEnding && !containsForbiddenCharacters(s)
49
+ }
50
+
51
+ /** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */
52
+ export type SimpleSlug = SlugLike<"simple">
53
+ export function isSimpleSlug(s: string): s is SimpleSlug {
54
+ const validStart = !(s.startsWith(".") || (s.length > 1 && s.startsWith("/")))
55
+ const validEnding = !endsWith(s, "index")
56
+ return validStart && !containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
57
+ }
58
+
59
+ /** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */
60
+ export type RelativeURL = SlugLike<"relative">
61
+ export function isRelativeURL(s: string): s is RelativeURL {
62
+ const validStart = /^\.{1,2}/.test(s)
63
+ const validEnding = !endsWith(s, "index")
64
+ return validStart && validEnding && ![".md", ".html"].includes(getFileExtension(s) ?? "")
65
+ }
66
+
67
+ export function isAbsoluteURL(s: string): boolean {
68
+ try {
69
+ new URL(s)
70
+ } catch {
71
+ return false
72
+ }
73
+ return true
74
+ }
75
+
76
+ export function getFullSlug(window: Window): FullSlug {
77
+ const res = window.document.body.dataset.slug! as FullSlug
78
+ return res
79
+ }
80
+
81
+ function sluggify(s: string): string {
82
+ return s
83
+ .split("/")
84
+ .map((segment) =>
85
+ segment
86
+ .replace(/\s/g, "-")
87
+ .replace(/&/g, "-and-")
88
+ .replace(/%/g, "-percent")
89
+ .replace(/\?/g, "")
90
+ .replace(/#/g, ""),
91
+ )
92
+ .join("/") // always use / as sep
93
+ .replace(/\/$/, "")
94
+ }
95
+
96
+ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
97
+ fp = stripSlashes(fp) as FilePath
98
+ let ext = getFileExtension(fp)
99
+ const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
100
+ if (excludeExt || [".md", ".html", undefined].includes(ext)) {
101
+ ext = ""
102
+ }
103
+
104
+ let slug = sluggify(withoutFileExt)
105
+
106
+ // treat _index as index
107
+ if (endsWith(slug, "_index")) {
108
+ slug = slug.replace(/_index$/, "index")
109
+ }
110
+
111
+ return (slug + ext) as FullSlug
112
+ }
113
+
114
+ export function simplifySlug(fp: FullSlug): SimpleSlug {
115
+ const res = stripSlashes(trimSuffix(fp, "index"), true)
116
+ return (res.length === 0 ? "/" : res) as SimpleSlug
117
+ }
118
+
119
+ // brain: tolerant wrapper around `decodeURI`. ERB/Liquid/Jinja template
120
+ // fragments pasted into ingested docs (`<%= loginurl %>`, `{{ url }}`,
121
+ // etc.) hit Quartz's link transformer as inline-HTML `href` values and
122
+ // crash the entire build with `URIError: URI malformed` on the lone `%`.
123
+ // Falling back to the raw string lets the build complete; the malformed
124
+ // link is an opaque slug that won't resolve to a vault page, but the
125
+ // surrounding doc still renders and the rest of the corpus is unaffected.
126
+ function safeDecodeURI(link: string): string {
127
+ try {
128
+ return decodeURI(link)
129
+ } catch {
130
+ return link
131
+ }
132
+ }
133
+
134
+ export function transformInternalLink(link: string): RelativeURL {
135
+ // brain: was `decodeURI(link)` upstream — see safeDecodeURI above.
136
+ let [fplike, anchor] = splitAnchor(safeDecodeURI(link))
137
+
138
+ const folderPath = isFolderPath(fplike)
139
+ let segments = fplike.split("/").filter((x) => x.length > 0)
140
+ let prefix = segments.filter(isRelativeSegment).join("/")
141
+ let fp = segments.filter((seg) => !isRelativeSegment(seg) && seg !== "").join("/")
142
+
143
+ // manually add ext here as we want to not strip 'index' if it has an extension
144
+ const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath))
145
+ const joined = joinSegments(stripSlashes(prefix), stripSlashes(simpleSlug))
146
+ const trail = folderPath ? "/" : ""
147
+ const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL
148
+ return res
149
+ }
150
+
151
+ // from micromorph/src/utils.ts
152
+ // https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
153
+ const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) => {
154
+ const rebased = new URL(el.getAttribute(attr)!, newBase)
155
+ el.setAttribute(attr, rebased.pathname + rebased.hash)
156
+ }
157
+ export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) {
158
+ el.querySelectorAll('[href=""], [href^="./"], [href^="../"]').forEach((item) =>
159
+ _rebaseHtmlElement(item, "href", destination),
160
+ )
161
+ el.querySelectorAll('[src=""], [src^="./"], [src^="../"]').forEach((item) =>
162
+ _rebaseHtmlElement(item, "src", destination),
163
+ )
164
+ }
165
+
166
+ const _rebaseHastElement = (
167
+ el: HastElement,
168
+ attr: string,
169
+ curBase: FullSlug,
170
+ newBase: FullSlug,
171
+ ) => {
172
+ if (el.properties?.[attr]) {
173
+ if (!isRelativeURL(String(el.properties[attr]))) {
174
+ return
175
+ }
176
+
177
+ const rel = joinSegments(resolveRelative(curBase, newBase), "..", el.properties[attr] as string)
178
+ el.properties[attr] = rel
179
+ }
180
+ }
181
+
182
+ export function normalizeHastElement(rawEl: HastElement, curBase: FullSlug, newBase: FullSlug) {
183
+ const el = clone(rawEl) // clone so we dont modify the original page
184
+ _rebaseHastElement(el, "src", curBase, newBase)
185
+ _rebaseHastElement(el, "href", curBase, newBase)
186
+ if (el.children) {
187
+ el.children = el.children.map((child) =>
188
+ normalizeHastElement(child as HastElement, curBase, newBase),
189
+ )
190
+ }
191
+
192
+ return el
193
+ }
194
+
195
+ // resolve /a/b/c to ../..
196
+ export function pathToRoot(slug: FullSlug): RelativeURL {
197
+ let rootPath = slug
198
+ .split("/")
199
+ .filter((x) => x !== "")
200
+ .slice(0, -1)
201
+ .map((_) => "..")
202
+ .join("/")
203
+
204
+ if (rootPath.length === 0) {
205
+ rootPath = "."
206
+ }
207
+
208
+ return rootPath as RelativeURL
209
+ }
210
+
211
+ export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug): RelativeURL {
212
+ const res = joinSegments(pathToRoot(current), simplifySlug(target as FullSlug)) as RelativeURL
213
+ return res
214
+ }
215
+
216
+ export function splitAnchor(link: string): [string, string] {
217
+ let [fp, anchor] = link.split("#", 2)
218
+ if (fp.endsWith(".pdf")) {
219
+ return [fp, anchor === undefined ? "" : `#${anchor}`]
220
+ }
221
+ anchor = anchor === undefined ? "" : "#" + slugAnchor(anchor)
222
+ return [fp, anchor]
223
+ }
224
+
225
+ export function slugTag(tag: string) {
226
+ return tag
227
+ .split("/")
228
+ .map((tagSegment) => sluggify(tagSegment))
229
+ .join("/")
230
+ }
231
+
232
+ export function joinSegments(...args: string[]): string {
233
+ if (args.length === 0) {
234
+ return ""
235
+ }
236
+
237
+ let joined = args
238
+ .filter((segment) => segment !== "" && segment !== "/")
239
+ .map((segment) => stripSlashes(segment))
240
+ .join("/")
241
+
242
+ // if the first segment starts with a slash, add it back
243
+ if (args[0].startsWith("/")) {
244
+ joined = "/" + joined
245
+ }
246
+
247
+ // if the last segment is a folder, add a trailing slash
248
+ if (args[args.length - 1].endsWith("/")) {
249
+ joined = joined + "/"
250
+ }
251
+
252
+ return joined
253
+ }
254
+
255
+ export function getAllSegmentPrefixes(tags: string): string[] {
256
+ const segments = tags.split("/")
257
+ const results: string[] = []
258
+ for (let i = 0; i < segments.length; i++) {
259
+ results.push(segments.slice(0, i + 1).join("/"))
260
+ }
261
+ return results
262
+ }
263
+
264
+ export interface TransformOptions {
265
+ strategy: "absolute" | "relative" | "shortest"
266
+ allSlugs: FullSlug[]
267
+ }
268
+
269
+ export function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL {
270
+ let targetSlug = transformInternalLink(target)
271
+
272
+ if (opts.strategy === "relative") {
273
+ return targetSlug as RelativeURL
274
+ } else {
275
+ const folderTail = isFolderPath(targetSlug) ? "/" : ""
276
+ const canonicalSlug = stripSlashes(targetSlug.slice(".".length))
277
+ let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug)
278
+
279
+ if (opts.strategy === "shortest") {
280
+ // if the file name is unique, then it's just the filename
281
+ const matchingFileNames = opts.allSlugs.filter((slug) => {
282
+ const parts = slug.split("/")
283
+ const fileName = parts.at(-1)
284
+ return targetCanonical === fileName
285
+ })
286
+
287
+ // only match, just use it
288
+ if (matchingFileNames.length === 1) {
289
+ const targetSlug = matchingFileNames[0]
290
+ return (resolveRelative(src, targetSlug) + targetAnchor) as RelativeURL
291
+ }
292
+ }
293
+
294
+ // if it's not unique, then it's the absolute path from the vault root
295
+ return (joinSegments(pathToRoot(src), canonicalSlug) + folderTail) as RelativeURL
296
+ }
297
+ }
298
+
299
+ // path helpers
300
+ export function isFolderPath(fplike: string): boolean {
301
+ return (
302
+ fplike.endsWith("/") ||
303
+ endsWith(fplike, "index") ||
304
+ endsWith(fplike, "index.md") ||
305
+ endsWith(fplike, "index.html")
306
+ )
307
+ }
308
+
309
+ export function endsWith(s: string, suffix: string): boolean {
310
+ return s === suffix || s.endsWith("/" + suffix)
311
+ }
312
+
313
+ export function trimSuffix(s: string, suffix: string): string {
314
+ if (endsWith(s, suffix)) {
315
+ s = s.slice(0, -suffix.length)
316
+ }
317
+ return s
318
+ }
319
+
320
+ function containsForbiddenCharacters(s: string): boolean {
321
+ return s.includes(" ") || s.includes("#") || s.includes("?") || s.includes("&")
322
+ }
323
+
324
+ function _hasFileExtension(s: string): boolean {
325
+ return getFileExtension(s) !== undefined
326
+ }
327
+
328
+ export function getFileExtension(s: string): string | undefined {
329
+ return s.match(/\.[A-Za-z0-9]+$/)?.[0]
330
+ }
331
+
332
+ function isRelativeSegment(s: string): boolean {
333
+ return /^\.{0,2}$/.test(s)
334
+ }
335
+
336
+ export function stripSlashes(s: string, onlyStripPrefix?: boolean): string {
337
+ if (s.startsWith("/")) {
338
+ s = s.substring(1)
339
+ }
340
+
341
+ if (!onlyStripPrefix && s.endsWith("/")) {
342
+ s = s.slice(0, -1)
343
+ }
344
+
345
+ return s
346
+ }
347
+
348
+ function _addRelativeToStart(s: string): string {
349
+ if (s === "") {
350
+ s = "."
351
+ }
352
+
353
+ if (!s.startsWith(".")) {
354
+ s = joinSegments(".", s)
355
+ }
356
+
357
+ return s
358
+ }
@@ -0,0 +1,55 @@
1
+ // Brain shared source-icon vocabulary — Phase 3.3 of the Wiki UX Overhaul.
2
+ //
3
+ // This file is a TEMPLATE. It is installed at
4
+ // `<vault>/.quartz/quartz/util/sourceIcons.ts` by `brain vault render
5
+ // --overlay`. It does NOT compile or run from the brain repo itself;
6
+ // imports are resolved by the cloned Quartz workspace.
7
+ //
8
+ // Why this exists: P3.2 hard-coded the source-icon table at module
9
+ // scope inside `Search.tsx` (and a fallback copy inside
10
+ // `search.inline.ts`). P3.3 needs the same vocabulary on the
11
+ // server-rendered tag-content rows. Rather than copy a third time, we
12
+ // factor the table into this small util module so a future ingest
13
+ // source (e.g. notion) is a single-line change. Search, TagContent,
14
+ // RelatedDocs, and CommandPalette all import this canonical table.
15
+
16
+ // brain: source-icon mapping. Adding a key here is necessary but not
17
+ // sufficient — source chip styles and any source-specific palettes must
18
+ // also gain the new entry. Static tests keep the consumers in lockstep.
19
+ export const SOURCE_ICONS: Record<string, string> = {
20
+ gmail: "📧",
21
+ krisp: "🎙️",
22
+ slack: "💬",
23
+ manual: "✍️",
24
+ vault: "🌱",
25
+ }
26
+
27
+ // brain: ordered chip vocabulary — render order for any UI surface
28
+ // that lists every source. Pinned (vs derived from the index) so the
29
+ // rail renders identically when the live corpus is missing a source.
30
+ export const SOURCE_CHIP_ORDER: ReadonlyArray<keyof typeof SOURCE_ICONS> = [
31
+ "krisp",
32
+ "slack",
33
+ "gmail",
34
+ "manual",
35
+ "vault",
36
+ ] as const
37
+
38
+ // brain: fall back to a slug-based source classification when the
39
+ // emitter's frontmatter lift didn't graft a `source` field (e.g.
40
+ // legacy entries written before P1.5). Mirrors the heuristic the
41
+ // contentIndex post-processor uses (`slug.startsWith("_ingested/")`
42
+ // ⇒ ingested-tier; the next path segment is the source).
43
+ export function inferSource(slug: string, source: string | undefined): string {
44
+ if (typeof source === "string" && source.length > 0) return source
45
+ const match = slug.match(/^_ingested\/([^/]+)\//)
46
+ if (match) return match[1]
47
+ return "vault"
48
+ }
49
+
50
+ // brain: glyph lookup with a "vault" default fallback. Centralised so
51
+ // every consumer (Search row, tag-content row, future notebook chip,
52
+ // …) renders the same character even when the source is unrecognised.
53
+ export function sourceIconFor(source: string): string {
54
+ return SOURCE_ICONS[source] ?? SOURCE_ICONS["vault"]
55
+ }
@@ -0,0 +1,270 @@
1
+ // Sample Quartz v4 configuration for brain vaults.
2
+ //
3
+ // This file is a TEMPLATE. Copy it into your Quartz workspace as
4
+ // `<vault>/.quartz/quartz.config.ts` after cloning Quartz with
5
+ // `git clone https://github.com/jackyzha0/quartz.git <vault>/.quartz`.
6
+ // It does NOT compile or run from the brain repo itself; the imports
7
+ // below resolve against the dependencies Quartz pulls into the cloned
8
+ // workspace via `npm install`, not against any package brain ships.
9
+ //
10
+ // Tested against Quartz v4.5.x (April 2026). If a future Quartz version
11
+ // renames a plugin or its option shape, pull the latest config from
12
+ // https://quartz.jzhao.xyz/ and re-apply the brain-specific tweaks
13
+ // flagged below with `// brain:` comments.
14
+ //
15
+ // brain: this template wires up two brain-specific transformers:
16
+ //
17
+ // * `Plugin.DerivedFenceMark` (defined in
18
+ // `<vault>/.quartz/quartz/plugins/transformers/derivedFenceMark.ts`,
19
+ // installed by `brain vault render --overlay`). It must run after
20
+ // `Plugin.ObsidianFlavoredMarkdown()` so `[[wiki-link]]` syntax
21
+ // has been converted into mdast `link` nodes by the time the
22
+ // fence walker stamps `data-brain-derived` attributes on them.
23
+ // See the transformer's top-of-file comment for the full contract.
24
+ //
25
+ // * `Plugin.ReloadSignal` (defined in
26
+ // `<vault>/.quartz/quartz/plugins/transformers/reloadSignal.ts`,
27
+ // also installed by the overlay). Injects an ETag-aware polling reload
28
+ // `<script>` into every page when `BRAIN_WIKI_RELOAD=1` is set in
29
+ // the build env — replaces Quartz's `--serve` WebSocket reload
30
+ // path, which is dead in the brain blue-green serve flow because
31
+ // Caddy (not Quartz) serves the static output. See the
32
+ // transformer's top-of-file comment for the full contract.
33
+
34
+ import { QuartzConfig } from "./quartz/cfg"
35
+ import * as Plugin from "./quartz/plugins"
36
+
37
+ const config: QuartzConfig = {
38
+ configuration: {
39
+ // brain: friendlier title than the default "Quartz 4".
40
+ pageTitle: "Second Brain",
41
+ pageTitleSuffix: "",
42
+ enableSPA: true,
43
+ enablePopovers: true,
44
+ // brain: analytics off — the wiki is private and rendered locally.
45
+ // If you deploy the static output somewhere public, set this to your
46
+ // provider of choice (plausible, google, umami, etc.).
47
+ analytics: null,
48
+ locale: "en-US",
49
+ // brain: localhost for dev; flip to your domain before deploying.
50
+ baseUrl: "localhost:8080",
51
+ // brain: skip tooling-managed folders. `_templates` / `_attachments`
52
+ // are brain conventions; `.quartz` is the workspace itself; `dist` /
53
+ // `public` are build outputs that must NOT be re-walked as content.
54
+ ignorePatterns: [
55
+ "private",
56
+ "_templates",
57
+ "_attachments",
58
+ // brain: P4.7 — server-rendered partials (e.g. `_partials/recent.md`,
59
+ // the source-of-truth for the home-page "Recently captured" rail) live
60
+ // outside the public surface. The home note renders them inline via
61
+ // a fence; the partial itself must never appear as its own page.
62
+ "_partials",
63
+ "dist",
64
+ "public",
65
+ ".obsidian",
66
+ ".git",
67
+ ".quartz",
68
+ ],
69
+ defaultDateType: "modified",
70
+ theme: {
71
+ fontOrigin: "googleFonts",
72
+ cdnCaching: true,
73
+ // brain: Linear-style typography for the tech-forward variant of
74
+ // the 2026 redesign. Geist (Vercel's grotesk) for both display and
75
+ // body — clean, neutral, modern; Geist Mono for code and metadata.
76
+ // Both are on Google Fonts; Quartz's Google Fonts loader uses the
77
+ // family name verbatim.
78
+ typography: {
79
+ header: "Geist",
80
+ body: "Geist",
81
+ code: "Geist Mono",
82
+ },
83
+ // brain: Linear-style charcoal + indigo-violet palette. Dark mode
84
+ // is the headline aesthetic — Linear lives in dark — but light mode
85
+ // is a clean pure-neutral zinc scale for daytime reading.
86
+ colors: {
87
+ lightMode: {
88
+ light: "#ffffff",
89
+ lightgray: "#e4e4e7",
90
+ // brain: zinc-500 (#71717a) clears WCAG AA against pure white
91
+ // bg for muted text, metadata, dingbats.
92
+ gray: "#71717a",
93
+ darkgray: "#3f3f46",
94
+ dark: "#09090b",
95
+ secondary: "#5e6ad2",
96
+ tertiary: "#8b5cf6",
97
+ highlight: "rgba(94, 106, 210, 0.12)",
98
+ textHighlight: "#fde047b3",
99
+ },
100
+ darkMode: {
101
+ light: "#0a0a0a",
102
+ lightgray: "#27272a",
103
+ // brain: zinc-400 (#a1a1aa) clears WCAG AA against #0a0a0a.
104
+ gray: "#a1a1aa",
105
+ darkgray: "#d4d4d8",
106
+ dark: "#fafafa",
107
+ secondary: "#7170ff",
108
+ tertiary: "#a78bfa",
109
+ highlight: "rgba(113, 112, 255, 0.18)",
110
+ textHighlight: "#fde04766",
111
+ },
112
+ },
113
+ },
114
+ },
115
+ plugins: {
116
+ transformers: [
117
+ Plugin.FrontMatter(),
118
+ Plugin.CreatedModifiedDate({
119
+ priority: ["frontmatter", "git", "filesystem"],
120
+ }),
121
+ Plugin.SyntaxHighlighting({
122
+ theme: { light: "github-light", dark: "github-dark" },
123
+ keepBackground: false,
124
+ }),
125
+ Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
126
+ // brain: tag derived-edge `<a>` tags inside Phase D fences with
127
+ // `data-brain-derived` / `data-brain-rule` / `data-brain-weight`
128
+ // so the custom contentIndex emitter can classify them as
129
+ // `kind: "derived"` in `static/contentIndex.json`. Must run after
130
+ // ObsidianFlavoredMarkdown so wiki-link syntax has already been
131
+ // converted to mdast link nodes.
132
+ Plugin.DerivedFenceMark(),
133
+ // brain: Lane B redesign — classify every mdast link node into
134
+ // one of five kinds (wiki / external / tag / ingested / derived)
135
+ // and stamp `data-brain-link-kind` on `node.data.hProperties`.
136
+ // The Lane B `_links.scss` consumes that attribute to give each
137
+ // kind a distinct visual treatment (terracotta underline / dotted
138
+ // + arrow / pill chip / source-tinted left rail / italic dashed).
139
+ // Must run AFTER `ObsidianFlavoredMarkdown` (so wiki-link mdast
140
+ // nodes exist) AND AFTER `DerivedFenceMark` (so its
141
+ // `data-brain-derived` stamp is already in place — the
142
+ // classifier reads it to short-circuit the kind to `derived`).
143
+ // See `quartz/plugins/transformers/linkKindMark.ts` for the
144
+ // full classification contract.
145
+ Plugin.LinkKindMark(),
146
+ // brain: Lane B redesign — inject the runtime source-tagger
147
+ // (`/static/linkSourceTag.js`) into every page. The tagger
148
+ // extracts the source segment from `_ingested/<source>/...`
149
+ // hrefs at `DOMContentLoaded` (and on Quartz SPA `nav` events)
150
+ // and stamps `data-brain-source="krisp"` etc. on each ingested
151
+ // link, so `_links.scss`'s source-tinted left-rail rules
152
+ // (`&[data-brain-source="krisp"]`) can pick the right
153
+ // `--brain-source-*` color from `_tokens.scss`. Always emits
154
+ // (no env-var gate, unlike `ReloadSignal`) — the script is part
155
+ // of the production redesign. No markdown ordering requirement;
156
+ // grouped here next to `LinkKindMark` so the Lane B plugins
157
+ // stay co-located. See `quartz/plugins/transformers/linkSourceTag.ts`
158
+ // for the inject contract and the static script for the
159
+ // tagging logic.
160
+ Plugin.LinkSourceTag(),
161
+ // brain: Recent rail — inject the live relative-date recomputer
162
+ // (`/static/relativeDate.js`) into every page. The home-page Recent
163
+ // rail emits `<span class="brain-rel-date" data-date="<ISO>">{abs}</span>`
164
+ // (see `brain.wiki.build_homepage`); this script reads each
165
+ // `data-date` and recomputes the relative text ("3d ago") live on
166
+ // `DOMContentLoaded` + Quartz SPA `nav`, so the rail never decays
167
+ // between daily builds. Bucket logic mirrors
168
+ // `build_homepage._format_relative_date`. Always emits (no env-var
169
+ // gate, like `LinkSourceTag`); no markdown ordering requirement;
170
+ // grouped here next to `LinkSourceTag` so the brain script-only
171
+ // transformers stay co-located. See
172
+ // `quartz/plugins/transformers/relativeDate.ts` for the inject
173
+ // contract and `quartz/static/relativeDate.js` for the recomputer.
174
+ Plugin.RelativeDate(),
175
+ // brain: Lane C redesign — inject the runtime code-copy injector
176
+ // (`/static/codeCopy.js`) into every page. The injector walks
177
+ // every `<pre>` in the article body, lifts the `data-language`
178
+ // attribute from the inner `<code>` to the outer `<pre>` (so
179
+ // the CSS-only language label in `_code.scss` can read it via
180
+ // `attr()`), and appends a brain-themed `.brain-code-copy`
181
+ // button. Stock Quartz's `.clipboard-button` is hidden via
182
+ // `_code.scss` so the two don't render side-by-side. Always
183
+ // emits (no env-var gate, like `LinkSourceTag`) — the script is
184
+ // part of the production redesign. No markdown ordering
185
+ // requirement; grouped here next to `LinkSourceTag` so the
186
+ // brain script-only transformers stay co-located. See
187
+ // `quartz/plugins/transformers/codeCopy.ts` for the inject
188
+ // contract and `quartz/static/codeCopy.js` for the injector.
189
+ Plugin.CodeCopy(),
190
+ // brain: P4.4 — inject the email-thread reading-mode runtime
191
+ // (`/static/emailThread.js`) plus an inline `window
192
+ // .BRAIN_USER_EMAIL` global (read from
193
+ // `process.env.BRAIN_USER_EMAIL` at build time). The runtime
194
+ // (a) annotates each per-message section in an `email_thread`
195
+ // body with `data-brain-thread-from` + `data-brain-is-mine`,
196
+ // and (b) renders a "Show only my replies" filter button at
197
+ // the top of `<article>` that toggles the
198
+ // `body.brain-replies-only` class — the SCSS partial
199
+ // `_email_thread.scss` then hides every
200
+ // `[data-brain-is-mine="false"]` section. Always emits (no
201
+ // env-var gate, like `LinkSourceTag` and `CodeCopy`) — the
202
+ // runtime is part of the production redesign and self-gates on
203
+ // an `_ingested/gmail/` URL pathname. No markdown ordering
204
+ // requirement; grouped here next to `CodeCopy` so the brain
205
+ // script-only transformers stay co-located. See
206
+ // `quartz/plugins/transformers/emailThread.ts` for the inject
207
+ // contract and `quartz/static/emailThread.js` for the runtime
208
+ // logic.
209
+ Plugin.EmailThreadReader(),
210
+ // brain: inject the ETag-aware reload watcher (`/static/reload.js`)
211
+ // into every page when `BRAIN_WIKI_RELOAD=1` at build time.
212
+ // Replaces Quartz's `--serve` WebSocket reload, which is dead in
213
+ // our blue-green serve flow because Caddy (not Quartz) is the
214
+ // static file server. `bin/brain-up` sets the env var; `brain
215
+ // vault render` (prod path) leaves it unset so prod pages ship
216
+ // without a polling client. The transformer has no markdown
217
+ // ordering requirement — it only contributes a `<script>` tag
218
+ // via `externalResources()` — but is grouped here next to
219
+ // `DerivedFenceMark` so the two brain-extension transformers
220
+ // stay co-located.
221
+ Plugin.ReloadSignal(),
222
+ Plugin.GitHubFlavoredMarkdown(),
223
+ Plugin.TableOfContents(),
224
+ // brain: Lane B redesign — `externalLinkIcon: false` disables
225
+ // Quartz's stock inline-SVG `↗` arrow on external links. The Lane B
226
+ // `_links.scss` paints its own `↗` via `::after` on
227
+ // `a[data-brain-link-kind="external"]` so the color/opacity track
228
+ // the redesign tokens; leaving stock's icon enabled would render
229
+ // every external link with two arrows side-by-side.
230
+ Plugin.CrawlLinks({ markdownLinkResolution: "shortest", externalLinkIcon: false }),
231
+ // brain: P4.1 — strip home-page `<li>`s whose link resolves to an
232
+ // empty folder (today: `daily/` when the user hasn't created any
233
+ // daily notes yet). Generic — extend the `folders` option to
234
+ // protect future top-level subdirs (e.g. a hypothetical
235
+ // `workbench/`) from the same 404. Must run AFTER `CrawlLinks` so
236
+ // the rehype-stage `<a href>`s have already been resolved to the
237
+ // shape the matcher expects (`daily/`, `daily/index`, …); without
238
+ // that ordering the matcher's normalizer sees raw markdown URLs
239
+ // and mis-classifies. The transformer reads `ctx.argv.directory`
240
+ // once per build to compute which folders are empty.
241
+ Plugin.EmptyDoorFilter(),
242
+ Plugin.Description(),
243
+ // strict: "ignore" suppresses KaTeX strict-mode build warnings for
244
+ // typographic chars (en/em-dashes, », ¢) in math contexts — cosmetic
245
+ // only, rendering unchanged.
246
+ Plugin.Latex({ renderEngine: "katex", katexOptions: { strict: "ignore" } }),
247
+ ],
248
+ filters: [Plugin.RemoveDrafts()],
249
+ emitters: [
250
+ Plugin.AliasRedirects(),
251
+ Plugin.ComponentResources(),
252
+ Plugin.ContentPage(),
253
+ Plugin.FolderPage(),
254
+ Plugin.TagPage(),
255
+ Plugin.ContentIndex({
256
+ enableSiteMap: true,
257
+ enableRSS: true,
258
+ }),
259
+ Plugin.Assets(),
260
+ Plugin.Static(),
261
+ Plugin.NotFoundPage(),
262
+ // brain: the graph view is the killer feature for an Obsidian-style
263
+ // vault. The actual graph rendering happens via `Component.Graph(...)`
264
+ // in `quartz.layout.ts`; there is no `Plugin.Graph` emitter in stock
265
+ // Quartz v4.5.x — only the component is needed here.
266
+ ],
267
+ },
268
+ }
269
+
270
+ export default config