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.
- brain/__init__.py +0 -0
- brain/__main__.py +18 -0
- brain/_capture_command.py +445 -0
- brain/_compose.py +52 -0
- brain/activity.py +206 -0
- brain/ask.py +631 -0
- brain/audio.py +591 -0
- brain/backfill/__init__.py +12 -0
- brain/backfill/search_extras.py +141 -0
- brain/backfill/source_rows.py +101 -0
- brain/bin/__init__.py +1 -0
- brain/bin/_launcher.py +107 -0
- brain/bin/down.py +8 -0
- brain/bin/launchd.py +268 -0
- brain/bin/monitor.py +570 -0
- brain/bin/rebuild.py +8 -0
- brain/bin/status.py +8 -0
- brain/bin/up.py +8 -0
- brain/brief.py +272 -0
- brain/capture.py +49 -0
- brain/chat.py +293 -0
- brain/cli.py +9760 -0
- brain/cli_claude.py +81 -0
- brain/cli_connect.py +285 -0
- brain/cli_demo.py +266 -0
- brain/config.py +1949 -0
- brain/connect.py +925 -0
- brain/db.py +540 -0
- brain/demo/__init__.py +452 -0
- brain/demo/corpus/manifest.json +403 -0
- brain/demo/embedder.py +74 -0
- brain/durations.py +84 -0
- brain/edit_session.py +156 -0
- brain/editor.py +67 -0
- brain/elicit/__init__.py +16 -0
- brain/elicit/detectors.py +250 -0
- brain/elicit/drafter.py +70 -0
- brain/elicit/queue.py +220 -0
- brain/elicit/schema.py +48 -0
- brain/elicit/session.py +445 -0
- brain/embedding_targets.py +54 -0
- brain/embeddings.py +424 -0
- brain/enrichment.py +808 -0
- brain/errors.py +357 -0
- brain/eval/__init__.py +129 -0
- brain/eval/answer_eval.py +281 -0
- brain/eval/baseline.py +265 -0
- brain/eval/concept_extraction.py +378 -0
- brain/eval/corpus.py +152 -0
- brain/eval/errors.py +19 -0
- brain/eval/graph_baseline.py +226 -0
- brain/eval/graph_retrieval.py +202 -0
- brain/eval/graph_runner.py +319 -0
- brain/eval/metrics.py +101 -0
- brain/eval/runner.py +223 -0
- brain/format.py +783 -0
- brain/gaps.py +390 -0
- brain/graph_rag/__init__.py +94 -0
- brain/graph_rag/_retrieval_common.py +113 -0
- brain/graph_rag/aggregates.py +303 -0
- brain/graph_rag/aliases/__init__.py +583 -0
- brain/graph_rag/backends/__init__.py +10 -0
- brain/graph_rag/backends/_age_helpers.py +473 -0
- brain/graph_rag/backends/age.py +782 -0
- brain/graph_rag/backends/base.py +272 -0
- brain/graph_rag/build.py +344 -0
- brain/graph_rag/communities.py +644 -0
- brain/graph_rag/communities_summary.py +437 -0
- brain/graph_rag/concepts.py +202 -0
- brain/graph_rag/cooccur.py +193 -0
- brain/graph_rag/cross_type.py +312 -0
- brain/graph_rag/extract.py +885 -0
- brain/graph_rag/fuse.py +371 -0
- brain/graph_rag/global_.py +412 -0
- brain/graph_rag/grouping.py +372 -0
- brain/graph_rag/person_resolver.py +167 -0
- brain/graph_rag/reconcile.py +792 -0
- brain/graph_rag/relational.py +353 -0
- brain/graph_rag/retrieve.py +526 -0
- brain/graph_rag/router.py +288 -0
- brain/graph_rag/schema.py +320 -0
- brain/graph_rag/sync.py +237 -0
- brain/graph_rag/tenancy.py +43 -0
- brain/graph_rag/themes.py +501 -0
- brain/graph_rag/weighting.py +202 -0
- brain/ingest/__init__.py +1926 -0
- brain/ingest/chunker.py +249 -0
- brain/ingest/docx.py +40 -0
- brain/ingest/gmail.py +621 -0
- brain/ingest/markdown.py +37 -0
- brain/ingest/pdf.py +61 -0
- brain/ingest/stdin.py +22 -0
- brain/ingest/sub_tokens.py +91 -0
- brain/ingest/text.py +16 -0
- brain/interactions.py +205 -0
- brain/maintenance.py +355 -0
- brain/mcp_server.py +3405 -0
- brain/migrations/001_init.sql +43 -0
- brain/migrations/002_qwen3_embedding.sql +17 -0
- brain/migrations/003_vault_model.sql +41 -0
- brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
- brain/migrations/005_derived_links.sql +67 -0
- brain/migrations/006_dedup_file_by_source_path.sql +25 -0
- brain/migrations/007_email_thread_and_draft.sql +15 -0
- brain/migrations/008_gmail_thread_unique.sql +11 -0
- brain/migrations/009_chunks_weighted_tsv.sql +28 -0
- brain/migrations/010_interactions.sql +30 -0
- brain/migrations/011_documents_summary.sql +23 -0
- brain/migrations/012_graphrag.sql +171 -0
- brain/migrations/013_graphrag_communities.sql +125 -0
- brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
- brain/migrations/015_interactions_graph_targets.sql +89 -0
- brain/migrations/016_index_hygiene.sql +61 -0
- brain/migrations/017_elicit.sql +30 -0
- brain/migrations/018_review_gap_signal_kinds.sql +40 -0
- brain/migrations/019_search_queries.sql +35 -0
- brain/migrations/020_link_suggestions.sql +40 -0
- brain/migrations/021_timeline_doc_date.sql +34 -0
- brain/migrations/022_link_suggestions_undirected.sql +84 -0
- brain/migrations/023_search_queries_fts_count.sql +28 -0
- brain/quartz_overrides/__init__.py +8 -0
- brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
- brain/quartz_overrides/quartz/build.ts +568 -0
- brain/quartz_overrides/quartz/cli/args.js +152 -0
- brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
- brain/quartz_overrides/quartz/cli/handlers.js +636 -0
- brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
- brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
- brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
- brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
- brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
- brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
- brain/quartz_overrides/quartz/components/Search.tsx +161 -0
- brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
- brain/quartz_overrides/quartz/components/index.ts +92 -0
- brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
- brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
- brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
- brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
- brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
- brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
- brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
- brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
- brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
- brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
- brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
- brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
- brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
- brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
- brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
- brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
- brain/quartz_overrides/quartz/processors/parse.ts +371 -0
- brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
- brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
- brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
- brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
- brain/quartz_overrides/quartz/static/emailThread.js +334 -0
- brain/quartz_overrides/quartz/static/favicon.ico +0 -0
- brain/quartz_overrides/quartz/static/icon.png +0 -0
- brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
- brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
- brain/quartz_overrides/quartz/static/reload.js +168 -0
- brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
- brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
- brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
- brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
- brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
- brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
- brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
- brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
- brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
- brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
- brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
- brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
- brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
- brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
- brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
- brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
- brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
- brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
- brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
- brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
- brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
- brain/quartz_overrides/quartz/styles/custom.scss +89 -0
- brain/quartz_overrides/quartz/styles/graph.scss +505 -0
- brain/quartz_overrides/quartz/util/ctx.ts +92 -0
- brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
- brain/quartz_overrides/quartz/util/path.ts +358 -0
- brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
- brain/quartz_overrides/quartz.config.ts +270 -0
- brain/quartz_overrides/quartz.layout.ts +314 -0
- brain/queries.py +1188 -0
- brain/rank_fusion.py +8 -0
- brain/resurface.py +210 -0
- brain/review/__init__.py +26 -0
- brain/review/emit.py +27 -0
- brain/review/queries.py +436 -0
- brain/review/render.py +196 -0
- brain/review/scans.py +355 -0
- brain/review/weekly.py +413 -0
- brain/search.py +704 -0
- brain/set_similarity.py +15 -0
- brain/setup.py +1205 -0
- brain/tags.py +56 -0
- brain/templates/Caddyfile.j2 +9 -0
- brain/templates/__init__.py +1 -0
- brain/templates/bin/__init__.py +1 -0
- brain/templates/bin/_brain-brief-fg.sh +25 -0
- brain/templates/bin/_brain-build-fg.sh +53 -0
- brain/templates/bin/_brain-watcher-fg.sh +65 -0
- brain/templates/bin/brain-down.sh +89 -0
- brain/templates/bin/brain-status.sh +83 -0
- brain/templates/bin/brain-up.sh +221 -0
- brain/templates/docker/age/Dockerfile +79 -0
- brain/templates/docker-compose.stock.yml.j2 +26 -0
- brain/templates/docker-compose.yml.j2 +34 -0
- brain/templates/env.example +190 -0
- brain/templates/launchd/__init__.py +1 -0
- brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
- brain/templates/launchd/com.brain.build.plist.j2 +46 -0
- brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
- brain/templates/skill/SKILL.md +63 -0
- brain/templates/skill/__init__.py +1 -0
- brain/timeline.py +834 -0
- brain/todo.py +124 -0
- brain/uninstall.py +185 -0
- brain/vault/__init__.py +115 -0
- brain/vault/_atomic.py +25 -0
- brain/vault/daily_index.py +228 -0
- brain/vault/derived_links/__init__.py +50 -0
- brain/vault/derived_links/directory.py +683 -0
- brain/vault/derived_links/fence.py +408 -0
- brain/vault/derived_links/gws.py +64 -0
- brain/vault/derived_links/participants.py +143 -0
- brain/vault/derived_links/pass_runner.py +362 -0
- brain/vault/derived_links/rules.py +137 -0
- brain/vault/export.py +683 -0
- brain/vault/frontmatter.py +165 -0
- brain/vault/graph.py +620 -0
- brain/vault/graph_format.py +388 -0
- brain/vault/link_rewrite.py +235 -0
- brain/vault/links.py +260 -0
- brain/vault/note_builder.py +211 -0
- brain/vault/paths.py +55 -0
- brain/vault/quartz_overlay.py +236 -0
- brain/vault/rename.py +591 -0
- brain/vault/resolver.py +304 -0
- brain/vault/slug.py +127 -0
- brain/vault/sync.py +1513 -0
- brain/vault/sync_summaries.py +264 -0
- brain/vault/templates.py +145 -0
- brain/vault/watch.py +1052 -0
- brain/wiki/__init__.py +6 -0
- brain/wiki/_github_slugger.py +76 -0
- brain/wiki/_person_name.py +314 -0
- brain/wiki/build_homepage.py +541 -0
- brain/wiki/build_partial.py +273 -0
- brain/wiki/build_people.py +934 -0
- brain/wiki/build_related.py +758 -0
- brain/wiki/build_swap.py +585 -0
- brain/wiki/build_watcher.py +975 -0
- brain/wiki/edit_classifier.py +215 -0
- brain/wiki/errors.py +10 -0
- brain/wiki/fastpath_manifest.py +475 -0
- brain/wiki/fastpath_state.py +174 -0
- brain/wiki/install.py +296 -0
- brain/wiki/slug.py +111 -0
- secondbrain_py-0.2.1.dist-info/METADATA +195 -0
- secondbrain_py-0.2.1.dist-info/RECORD +273 -0
- secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
- secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
- secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
- secondbrain_py-0.2.1.dist-info/top_level.txt +1 -0
brain/tags.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Tag normalization rules used across brain (DB writes, file writes, ingest).
|
|
2
|
+
|
|
3
|
+
Single source of truth for the canonical tag form: lowercase
|
|
4
|
+
(``str.casefold`` for proper Unicode handling), hyphen-separated, deduped,
|
|
5
|
+
empty inputs dropped. Every tag-write boundary in the codebase
|
|
6
|
+
(``ingest.apply_tags``, ``vault.frontmatter.rewrite_tags``,
|
|
7
|
+
``vault.sync._coerce_tag_list``, the ``brain backfill normalize-tags``
|
|
8
|
+
subcommand) feeds inputs through :func:`normalize_tags` before persisting,
|
|
9
|
+
so future writes can never re-introduce uppercase / separator / duplicate
|
|
10
|
+
drift.
|
|
11
|
+
"""
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
|
|
15
|
+
_SEPARATOR_RE = re.compile(r"[\s_]+")
|
|
16
|
+
_HYPHEN_RUN_RE = re.compile(r"-+")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_tag(tag: str) -> str:
|
|
20
|
+
"""Canonicalize a single tag string.
|
|
21
|
+
|
|
22
|
+
Steps, in order:
|
|
23
|
+
|
|
24
|
+
1. ``str.strip`` to drop leading/trailing whitespace.
|
|
25
|
+
2. ``str.casefold`` so Unicode edge cases collapse correctly
|
|
26
|
+
(German ``ß`` → ``ss``, Greek final sigma → ``σ``, etc.).
|
|
27
|
+
3. Replace runs of whitespace + underscores with a single hyphen so
|
|
28
|
+
``Interview Prep`` and ``interview_prep`` both canonicalize to
|
|
29
|
+
``interview-prep``.
|
|
30
|
+
4. Collapse runs of hyphens to a single hyphen, then strip leading /
|
|
31
|
+
trailing hyphens — ``" --foo-- "`` → ``"foo"``.
|
|
32
|
+
|
|
33
|
+
An all-whitespace input (or an input that becomes empty after the
|
|
34
|
+
cleanup, e.g. ``"---"``) returns the empty string. Callers that work
|
|
35
|
+
with collections should use :func:`normalize_tags` which drops empties
|
|
36
|
+
automatically.
|
|
37
|
+
"""
|
|
38
|
+
folded = tag.strip().casefold()
|
|
39
|
+
hyphenated = _SEPARATOR_RE.sub("-", folded)
|
|
40
|
+
return _HYPHEN_RUN_RE.sub("-", hyphenated).strip("-")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_tags(tags: Iterable[str]) -> list[str]:
|
|
44
|
+
"""Canonicalize, dedupe, and drop empties from an iterable of tags.
|
|
45
|
+
|
|
46
|
+
Order is the first-seen order of the canonicalized form: the first
|
|
47
|
+
occurrence of each canonical tag wins, later duplicates (regardless of
|
|
48
|
+
casing or separator variant) are silently dropped. Empty / whitespace-
|
|
49
|
+
only entries — and entries that canonicalize to the empty string — are
|
|
50
|
+
dropped.
|
|
51
|
+
|
|
52
|
+
The return value is always a fresh ``list[str]``; callers can mutate it
|
|
53
|
+
safely.
|
|
54
|
+
"""
|
|
55
|
+
cleaned = (normalize_tag(t) for t in tags)
|
|
56
|
+
return [t for t in dict.fromkeys(cleaned) if t]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Jinja variables: wiki_port, vault_path
|
|
2
|
+
:{{ wiki_port }} {
|
|
3
|
+
root * {{ vault_path }}/.quartz/current
|
|
4
|
+
file_server
|
|
5
|
+
encode gzip
|
|
6
|
+
# ETag-driven reload (P6.1) — keep cache short on the build-id file
|
|
7
|
+
@buildid path /.build-id
|
|
8
|
+
header @buildid Cache-Control "max-age=2, must-revalidate"
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Marker so importlib.resources can resolve this subdirectory in pipx-installed wheels."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Marker so importlib.resources can resolve this subdirectory in pipx-installed wheels."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# One-shot wrapper for the daily brief, intended to run under launchd's
|
|
3
|
+
# StartCalendarInterval (07:00 local). Unlike _brain-watcher-fg / _brain-build-fg
|
|
4
|
+
# this does NOT supervise a long-running daemon and writes no pid file — it runs
|
|
5
|
+
# `brain brief --wiki` once and exits. launchd's StandardOut/ErrorPath capture
|
|
6
|
+
# the digest + any warnings.
|
|
7
|
+
#
|
|
8
|
+
# Env contract:
|
|
9
|
+
# BRAIN_VAULT_PATH (default: $HOME/brain-vault) — passed through to brief.
|
|
10
|
+
# BRAIN_PY pins the Python interpreter that has `brain` importable
|
|
11
|
+
# (exported by the launchd plist); falls back to python3.
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
# Pick the Python interpreter once. BRAIN_PY wins; otherwise prefer python3.
|
|
16
|
+
if [[ -n "${BRAIN_PY:-}" ]]; then
|
|
17
|
+
PY="$BRAIN_PY"
|
|
18
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
19
|
+
PY="$(command -v python3)"
|
|
20
|
+
else
|
|
21
|
+
echo "no python interpreter found (set BRAIN_PY=… to override)" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
exec "$PY" -m brain brief --wiki
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Foreground wrapper for the Quartz build watcher, intended to run under
|
|
3
|
+
# launchd. Mirrors `_brain-watcher-fg`'s contract — see that file's header
|
|
4
|
+
# for the rationale + pid-file semantics.
|
|
5
|
+
#
|
|
6
|
+
# This wrapper specifically supervises `python -m brain.wiki.build_watcher`,
|
|
7
|
+
# which re-renders the Quartz site into a fresh `<vault>/.quartz/builds/<id>/`
|
|
8
|
+
# and atomically retargets `current/` on every vault change. Pre-launchd it
|
|
9
|
+
# was started via `nohup ... &` from `bin/brain-up`, with the same drift
|
|
10
|
+
# problem (silent death + no supervisor).
|
|
11
|
+
#
|
|
12
|
+
# Env contract:
|
|
13
|
+
# BRAIN_VAULT_PATH (default: $HOME/brain-vault) — passed to `--vault`.
|
|
14
|
+
# BRAIN_WIKI_KEEP_BUILDS (default: 3) — old build dirs retained per swap.
|
|
15
|
+
# The wrapper sets BRAIN_WIKI_RELOAD=1 itself, matching the bin/brain-up
|
|
16
|
+
# invocation, so the LaunchAgent plist doesn't have to know about it.
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
VAULT="${BRAIN_VAULT_PATH:-$HOME/brain-vault}"
|
|
21
|
+
KEEP="${BRAIN_WIKI_KEEP_BUILDS:-3}"
|
|
22
|
+
# Resolve $BRAIN_HOME from this script's location ($BRAIN_HOME/.shims/ under a
|
|
23
|
+
# pipx install, $repo_root/bin/ in a dev checkout) so the pid file lands in a
|
|
24
|
+
# stable, non-reaped dir. Honor an explicit BRAIN_HOME override if set.
|
|
25
|
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
26
|
+
BRAIN_HOME_RESOLVED="${BRAIN_HOME:-$( cd "$SCRIPT_DIR/.." && pwd )}"
|
|
27
|
+
# Pid path defaults to $BRAIN_HOME/run/ — NOT /tmp. macOS's tmp_cleaner reaps
|
|
28
|
+
# files left untouched for 3 days; this daemon writes the pid once at launch
|
|
29
|
+
# then runs for weeks, so a /tmp pid file silently vanishes and brain-status
|
|
30
|
+
# misreports the live daemon as stopped. Overridable (unset = unchanged) so
|
|
31
|
+
# tests run this wrapper hermetically against a tmp pid dir.
|
|
32
|
+
BUILD_PID="${BRAIN_BUILD_PID:-$BRAIN_HOME_RESOLVED/run/brain-build.pid}"
|
|
33
|
+
|
|
34
|
+
# Pick the Python interpreter once. BRAIN_PY (env var) wins; otherwise
|
|
35
|
+
# prefer python3 on PATH. The installed-shim flow (in $BRAIN_HOME/bin/)
|
|
36
|
+
# always has BRAIN_PY exported by the Python launcher in
|
|
37
|
+
# src/brain/bin/_launcher.py — but we keep the python3 fallback for
|
|
38
|
+
# users who invoke the shims directly outside the launcher (e.g. via
|
|
39
|
+
# launchd plists that themselves set PATH but not BRAIN_PY).
|
|
40
|
+
if [[ -n "${BRAIN_PY:-}" ]]; then
|
|
41
|
+
PY="$BRAIN_PY"
|
|
42
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
43
|
+
PY="$(command -v python3)"
|
|
44
|
+
else
|
|
45
|
+
echo "no python interpreter found (set BRAIN_PY=… to override)" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
export BRAIN_WIKI_RELOAD=1
|
|
50
|
+
|
|
51
|
+
mkdir -p "$(dirname "$BUILD_PID")"
|
|
52
|
+
echo "$$" >"$BUILD_PID"
|
|
53
|
+
exec "$PY" -m brain.wiki.build_watcher --vault "$VAULT" --keep "$KEEP"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Foreground wrapper for the vault sync watcher, intended to run under launchd.
|
|
3
|
+
#
|
|
4
|
+
# Why a wrapper:
|
|
5
|
+
# The historical `bin/brain-up` flow uses `nohup ... &` to background-spawn
|
|
6
|
+
# the watcher, then writes the spawned pid into the pid file for
|
|
7
|
+
# `brain-status` to pick up. That works in a shell session but doesn't
|
|
8
|
+
# survive the next "what kills my daemons silently?" round (terminal close,
|
|
9
|
+
# sleep/wake, OOM, …) because nothing supervises the daemon.
|
|
10
|
+
#
|
|
11
|
+
# This script flips the pattern: it stays in the foreground, writes the
|
|
12
|
+
# pid file, then `exec`s the watcher so the watcher inherits the same pid.
|
|
13
|
+
# A LaunchAgent (com.brain.watcher) wraps THIS script with `KeepAlive=true`
|
|
14
|
+
# so launchd respawns us within `ThrottleInterval` seconds whenever the
|
|
15
|
+
# underlying watcher dies — no manual `brain-up` after every crash.
|
|
16
|
+
#
|
|
17
|
+
# PID-file contract:
|
|
18
|
+
# We write `$$` into the pid file ($BRAIN_HOME/run/brain-watch.pid) BEFORE
|
|
19
|
+
# the exec. `exec` replaces
|
|
20
|
+
# the current process image but keeps the pid, so the file points at the
|
|
21
|
+
# still-running watcher. `brain-status`'s `kill -0 $(cat …)` check then
|
|
22
|
+
# reports "running" exactly as it does for the legacy nohup path. When
|
|
23
|
+
# launchd kills us out-of-band the file becomes stale; that's fine, the
|
|
24
|
+
# `kill -0` check fails and the row flips to "stopped".
|
|
25
|
+
#
|
|
26
|
+
# Env contract:
|
|
27
|
+
# BRAIN_VAULT_PATH (default: $HOME/brain-vault) — passed to `--vault`.
|
|
28
|
+
# PATH must include the pipx bin dir (or wherever `brain` is installed)
|
|
29
|
+
# so `brain` resolves; the LaunchAgent plist's EnvironmentVariables block
|
|
30
|
+
# sets this explicitly.
|
|
31
|
+
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
|
|
34
|
+
VAULT="${BRAIN_VAULT_PATH:-$HOME/brain-vault}"
|
|
35
|
+
|
|
36
|
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
37
|
+
PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
|
|
38
|
+
VENV_BIN="$PROJECT_ROOT/.venv/bin"
|
|
39
|
+
# Pid path defaults to $BRAIN_HOME/run/ — NOT /tmp. macOS's tmp_cleaner reaps
|
|
40
|
+
# files left untouched for 3 days; this daemon writes the pid once at launch
|
|
41
|
+
# then runs for weeks, so a /tmp pid file silently vanishes and brain-status
|
|
42
|
+
# misreports the live daemon as stopped. $BRAIN_HOME resolves to an explicit
|
|
43
|
+
# BRAIN_HOME override if set, else PROJECT_ROOT (== the script's parent: the
|
|
44
|
+
# script lives in $BRAIN_HOME/.shims/ or $repo_root/bin/); kept consistent with
|
|
45
|
+
# the build fg wrapper and brain-{status,down,up}. Overridable (unset =
|
|
46
|
+
# unchanged) so tests run this wrapper hermetically against a tmp pid dir.
|
|
47
|
+
WATCH_PID="${BRAIN_WATCH_PID:-${BRAIN_HOME:-$PROJECT_ROOT}/run/brain-watch.pid}"
|
|
48
|
+
# brain: tests set BRAIN_SKIP_VENV_AUTOLOAD=1 to keep the wrapper from
|
|
49
|
+
# silently prepending the developer's real `.venv/bin` (and thus the real
|
|
50
|
+
# `brain`) ahead of the test stub on PATH. Same shape as the BRAIN_PY
|
|
51
|
+
# override in `bin/brain-up`. In production the variable is unset and the
|
|
52
|
+
# venv prepend just works (for dev-checkout users with a local .venv;
|
|
53
|
+
# pipx-installed users have brain on PATH via the launchd plist already).
|
|
54
|
+
if [[ "${BRAIN_SKIP_VENV_AUTOLOAD:-0}" != "1" && -d "$VENV_BIN" ]]; then
|
|
55
|
+
export PATH="$VENV_BIN:$PATH"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if ! command -v brain >/dev/null 2>&1; then
|
|
59
|
+
echo "brain CLI not on PATH (expected via launchd PATH or local .venv)" >&2
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
mkdir -p "$(dirname "$WATCH_PID")"
|
|
64
|
+
echo "$$" >"$WATCH_PID"
|
|
65
|
+
exec brain vault sync --watch --vault "$VAULT"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop the second-brain wiki build watcher + sync watcher (blue/green serve edition).
|
|
3
|
+
# Idempotent — safe to re-run.
|
|
4
|
+
#
|
|
5
|
+
# Caddy is intentionally left running: it keeps serving the last-good build
|
|
6
|
+
# from <vault>/.quartz/current so brain.test stays healthy across restarts.
|
|
7
|
+
#
|
|
8
|
+
# Env overrides (default to the production values when unset — production
|
|
9
|
+
# behavior is unchanged unless you explicitly set these; mirrors the
|
|
10
|
+
# BRAIN_LAUNCHCTL / BRAIN_LAUNCHD_DIR pattern below). They exist so the test
|
|
11
|
+
# suite can run this shim HERMETICALLY — pointing the pid files at a tmp dir
|
|
12
|
+
# and the orphan-killer at a stub — so the real `brain-up` watchers on a dev
|
|
13
|
+
# machine are never touched:
|
|
14
|
+
# BRAIN_WATCH_PID (default: $BRAIN_HOME/run/brain-watch.pid)
|
|
15
|
+
# BRAIN_BUILD_PID (default: $BRAIN_HOME/run/brain-build.pid)
|
|
16
|
+
# BRAIN_WIKI_PID (default: $BRAIN_HOME/run/brain-wiki.pid)
|
|
17
|
+
# BRAIN_PKILL (default: pkill) — binary used for the orphan sweep
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
# Resolve $BRAIN_HOME from this script's location ($BRAIN_HOME/.shims/ under a
|
|
22
|
+
# pipx install, $repo_root/bin/ in a dev checkout) so the pid paths match where
|
|
23
|
+
# the fg wrappers write them. Honor an explicit BRAIN_HOME override if set.
|
|
24
|
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
25
|
+
BRAIN_HOME_RESOLVED="${BRAIN_HOME:-$( cd "$SCRIPT_DIR/.." && pwd )}"
|
|
26
|
+
|
|
27
|
+
# Pid paths default to $BRAIN_HOME/run/ — NOT /tmp. macOS's tmp_cleaner reaps
|
|
28
|
+
# files left untouched for 3 days, which silently vanishes a long-lived
|
|
29
|
+
# daemon's pid file; $BRAIN_HOME/run/ is not reaped. Must match the fg
|
|
30
|
+
# wrappers' defaults. Overridable (unset = unchanged) for hermetic tests.
|
|
31
|
+
WATCH_PID="${BRAIN_WATCH_PID:-$BRAIN_HOME_RESOLVED/run/brain-watch.pid}" # `brain vault sync --watch`
|
|
32
|
+
BUILD_PID="${BRAIN_BUILD_PID:-$BRAIN_HOME_RESOLVED/run/brain-build.pid}" # `python -m brain.wiki.build_watcher`
|
|
33
|
+
WIKI_PID="${BRAIN_WIKI_PID:-$BRAIN_HOME_RESOLVED/run/brain-wiki.pid}" # legacy: pre-blue/green `npx quartz --serve`
|
|
34
|
+
PKILL="${BRAIN_PKILL:-pkill}" # orphan-sweep binary (tests stub it)
|
|
35
|
+
|
|
36
|
+
LAUNCHD_DIR="${BRAIN_LAUNCHD_DIR:-$HOME/Library/LaunchAgents}"
|
|
37
|
+
LAUNCHCTL="${BRAIN_LAUNCHCTL:-launchctl}"
|
|
38
|
+
|
|
39
|
+
# brain: if the user installed launchd supervision via
|
|
40
|
+
# `bin/brain-install-launchd`, plain `kill $pid` is ineffective — launchd
|
|
41
|
+
# respawns each daemon within ThrottleInterval seconds. Bootout the
|
|
42
|
+
# LaunchAgents first so the kills below actually stick. We don't remove
|
|
43
|
+
# the .plist files here (that's `bin/brain-uninstall-launchd`'s job);
|
|
44
|
+
# bootout-only means a subsequent `bin/brain-up` won't re-supervise, but
|
|
45
|
+
# `launchctl bootstrap gui/$UID <plist>` (or rerunning the install
|
|
46
|
+
# script) will restore supervision without rewriting the plists.
|
|
47
|
+
bootout_one() {
|
|
48
|
+
local label="$1"
|
|
49
|
+
local domain="gui/$UID/${label}"
|
|
50
|
+
if "$LAUNCHCTL" print "$domain" >/dev/null 2>&1; then
|
|
51
|
+
"$LAUNCHCTL" bootout "$domain" 2>/dev/null || true
|
|
52
|
+
echo "unloaded launchd: $domain"
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
bootout_one com.brain.watcher
|
|
57
|
+
bootout_one com.brain.build
|
|
58
|
+
|
|
59
|
+
stop_one() {
|
|
60
|
+
local pid_file="$1"
|
|
61
|
+
local label="$2"
|
|
62
|
+
if [[ ! -f "$pid_file" ]]; then
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
local pid
|
|
66
|
+
pid=$(cat "$pid_file")
|
|
67
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
68
|
+
kill "$pid" 2>/dev/null || true
|
|
69
|
+
sleep 1
|
|
70
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
71
|
+
kill -9 "$pid" 2>/dev/null || true
|
|
72
|
+
fi
|
|
73
|
+
echo "stopped $label (pid $pid)"
|
|
74
|
+
fi
|
|
75
|
+
rm -f "$pid_file"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stop_one "$WATCH_PID" 'watcher (vault sync)'
|
|
79
|
+
stop_one "$BUILD_PID" 'build watcher'
|
|
80
|
+
# Belt-and-suspenders: only stop a legacy wiki pid file if a stale install
|
|
81
|
+
# left one behind. Brand-new clones won't have this file.
|
|
82
|
+
stop_one "$WIKI_PID" 'wiki (legacy quartz --serve)'
|
|
83
|
+
|
|
84
|
+
# Belt + suspenders: any orphaned processes from prior crashed runs.
|
|
85
|
+
"$PKILL" -f 'brain.wiki.build_watcher' 2>/dev/null || true
|
|
86
|
+
"$PKILL" -f 'brain vault sync --watch' 2>/dev/null || true
|
|
87
|
+
|
|
88
|
+
echo '🧠 brain is down'
|
|
89
|
+
echo ' (caddy left running — brain.test continues to serve last good build)'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Report on what the brain is currently running (blue/green serve edition).
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
VAULT="${BRAIN_VAULT_PATH:-$HOME/brain-vault}"
|
|
7
|
+
QUARTZ_DIR="$VAULT/.quartz"
|
|
8
|
+
PORT="${BRAIN_WIKI_PORT:-8080}"
|
|
9
|
+
URL="http://localhost:$PORT"
|
|
10
|
+
|
|
11
|
+
# Shim is installed at $BRAIN_HOME/.shims/brain-status, so $BRAIN_HOME is
|
|
12
|
+
# two parent-traversals up (.shims/ → $BRAIN_HOME). Honor an explicit
|
|
13
|
+
# BRAIN_HOME override if the launcher set one.
|
|
14
|
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
15
|
+
BRAIN_HOME_RESOLVED="${BRAIN_HOME:-$( cd "$SCRIPT_DIR/.." && pwd )}"
|
|
16
|
+
|
|
17
|
+
# Pid paths default to $BRAIN_HOME/run/ — NOT /tmp. macOS's tmp_cleaner reaps
|
|
18
|
+
# files left untouched for 3 days; the build/watch daemons write their pid file
|
|
19
|
+
# once at launch and then run for weeks, so a /tmp pid file silently vanishes
|
|
20
|
+
# and this very script misreports a live daemon as "stopped". $BRAIN_HOME/run/
|
|
21
|
+
# is not reaped. Overridable (unset = unchanged) so the test suite can run this
|
|
22
|
+
# shim hermetically against a tmp pid dir, never colliding with a live
|
|
23
|
+
# brain-up install on a dev machine.
|
|
24
|
+
WATCH_PID="${BRAIN_WATCH_PID:-$BRAIN_HOME_RESOLVED/run/brain-watch.pid}"
|
|
25
|
+
BUILD_PID="${BRAIN_BUILD_PID:-$BRAIN_HOME_RESOLVED/run/brain-build.pid}"
|
|
26
|
+
WIKI_PID="${BRAIN_WIKI_PID:-$BRAIN_HOME_RESOLVED/run/brain-wiki.pid}" # legacy quartz --serve install
|
|
27
|
+
# T1.7 plist templates redirect StandardOut to $BRAIN_HOME/logs/com.brain.*.out.log,
|
|
28
|
+
# not the legacy /tmp paths the pre-launchd nohup era used.
|
|
29
|
+
WATCH_LOG="$BRAIN_HOME_RESOLVED/logs/com.brain.watcher.out.log"
|
|
30
|
+
BUILD_LOG="$BRAIN_HOME_RESOLVED/logs/com.brain.build.out.log"
|
|
31
|
+
|
|
32
|
+
check_one() {
|
|
33
|
+
local pid_file="$1"
|
|
34
|
+
local label="$2"
|
|
35
|
+
local extra="$3"
|
|
36
|
+
if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
|
|
37
|
+
printf ' %-9s ✅ running pid=%-7s %s\n' "$label:" "$(cat "$pid_file")" "$extra"
|
|
38
|
+
else
|
|
39
|
+
printf ' %-9s ⛔ stopped %s\n' "$label:" "$extra"
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
echo '🧠 brain status'
|
|
44
|
+
check_one "$WATCH_PID" 'watcher' "(logs: $WATCH_LOG)"
|
|
45
|
+
check_one "$BUILD_PID" 'build' "(logs: $BUILD_LOG)"
|
|
46
|
+
|
|
47
|
+
# Legacy row — only show if a stale pid file from the pre-blue/green
|
|
48
|
+
# install is still on disk. Helps diagnose half-migrated machines.
|
|
49
|
+
if [[ -f "$WIKI_PID" ]]; then
|
|
50
|
+
if kill -0 "$(cat "$WIKI_PID")" 2>/dev/null; then
|
|
51
|
+
printf ' %-9s ⚠️ legacy quartz --serve still running pid=%s — kill with brain-down\n' \
|
|
52
|
+
'wiki:' "$(cat "$WIKI_PID")"
|
|
53
|
+
else
|
|
54
|
+
printf ' %-9s ⛔ stopped (legacy quartz --serve was here — pid file %s)\n' \
|
|
55
|
+
'wiki:' "$WIKI_PID"
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Current build pointer — what Caddy is serving right now.
|
|
60
|
+
CURRENT="$QUARTZ_DIR/current"
|
|
61
|
+
if [[ -L "$CURRENT" ]]; then
|
|
62
|
+
target="$(readlink "$CURRENT")"
|
|
63
|
+
if resolved="$(cd "$CURRENT" 2>/dev/null && pwd -P)"; then
|
|
64
|
+
if [[ -f "$resolved/.build-id" ]]; then
|
|
65
|
+
build_id="$(cat "$resolved/.build-id")"
|
|
66
|
+
else
|
|
67
|
+
build_id='(no .build-id)'
|
|
68
|
+
fi
|
|
69
|
+
printf ' current → %s\n' "$target"
|
|
70
|
+
printf ' build-id %s\n' "$build_id"
|
|
71
|
+
else
|
|
72
|
+
printf ' current ⚠️ symlink dangles → %s\n' "$target"
|
|
73
|
+
fi
|
|
74
|
+
else
|
|
75
|
+
printf ' current ⛔ no symlink at %s (run brain-up to bootstrap)\n' "$CURRENT"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Quick health check on the wiki socket — Caddy now answers, not Quartz.
|
|
79
|
+
if curl -fsS -o /dev/null --max-time 2 "$URL/" 2>/dev/null; then
|
|
80
|
+
echo ' wiki url reachable ✅'
|
|
81
|
+
else
|
|
82
|
+
echo ' wiki url unreachable ⚠️ '
|
|
83
|
+
fi
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Start the second-brain wiki + sync watcher (blue/green serve edition).
|
|
3
|
+
# Idempotent — safe to re-run.
|
|
4
|
+
#
|
|
5
|
+
# Architecture (post blue/green migration + 2026-05-08 launchd handoff):
|
|
6
|
+
# - Caddy serves <vault>/.quartz/current/ statically on :8080 + brain.test.
|
|
7
|
+
# This script does NOT spawn Quartz's --serve dev server anymore.
|
|
8
|
+
# - Watcher A: `brain vault sync --watch` — DB <-> vault mirror.
|
|
9
|
+
# - Cold start: if <vault>/.quartz/current is missing or unhealthy, run a
|
|
10
|
+
# synchronous one-shot `python -m brain.wiki.build_swap` so the wiki has
|
|
11
|
+
# something to serve before Watcher B comes up.
|
|
12
|
+
# - Watcher B: `python -m brain.wiki.build_watcher` — re-renders the site
|
|
13
|
+
# into a fresh build dir and atomically retargets `current` on every
|
|
14
|
+
# vault change.
|
|
15
|
+
#
|
|
16
|
+
# Daemon supervision: BOTH watchers run under launchd via the LaunchAgents
|
|
17
|
+
# installed by `bin/brain-install-launchd` (KeepAlive=true → respawn within
|
|
18
|
+
# ~10s of any death). The pre-launchd nohup paths were removed because
|
|
19
|
+
# they silently lost daemons to terminal close / sleep-wake / unexplained
|
|
20
|
+
# crashes — `brain-status` would just go quiet and the user had to notice.
|
|
21
|
+
# This script delegates to install-launchd unconditionally; install-launchd
|
|
22
|
+
# is idempotent (rewrites + reboots the plists every run, no-ops if already
|
|
23
|
+
# loaded), so re-running brain-up is safe and self-healing if launchd has
|
|
24
|
+
# booted out the agents for any reason.
|
|
25
|
+
#
|
|
26
|
+
# Applies the brain Quartz overlay (`quartz_overrides/`) into the workspace
|
|
27
|
+
# BEFORE the cold-start build so the reload-signal transformer is wired in.
|
|
28
|
+
# Set BRAIN_NO_OVERLAY=1 to skip the overlay (use whatever's already in the
|
|
29
|
+
# workspace).
|
|
30
|
+
#
|
|
31
|
+
# Env overrides:
|
|
32
|
+
# BRAIN_VAULT_PATH (default: ~/brain-vault)
|
|
33
|
+
# BRAIN_WIKI_PORT (default: 8080 — for the URL we curl/open)
|
|
34
|
+
# BRAIN_OPEN_BROWSER (default: 1; set 0 to skip)
|
|
35
|
+
# BRAIN_NO_OVERLAY (default: 0; set 1 to skip the overlay step)
|
|
36
|
+
# BRAIN_WIKI_KEEP_BUILDS (default: 3 — old build dirs retained per swap)
|
|
37
|
+
# BRAIN_NO_BUILD_WATCHER (default: 0; set 1 to skip launchd supervision —
|
|
38
|
+
# used by the bin-script tests so they don't
|
|
39
|
+
# install LaunchAgents on the developer's
|
|
40
|
+
# machine. Name kept for backward compat with
|
|
41
|
+
# the pre-launchd nohup era.)
|
|
42
|
+
# BRAIN_LAUNCHCTL (default: launchctl on PATH) — passed through
|
|
43
|
+
# to install-launchd; tests stub it.
|
|
44
|
+
# BRAIN_LAUNCHD_DIR (default: ~/Library/LaunchAgents) — same.
|
|
45
|
+
|
|
46
|
+
set -euo pipefail
|
|
47
|
+
|
|
48
|
+
VAULT="${BRAIN_VAULT_PATH:-$HOME/brain-vault}"
|
|
49
|
+
QUARTZ_DIR="$VAULT/.quartz"
|
|
50
|
+
PORT="${BRAIN_WIKI_PORT:-8080}"
|
|
51
|
+
URL="http://localhost:$PORT"
|
|
52
|
+
KEEP="${BRAIN_WIKI_KEEP_BUILDS:-3}"
|
|
53
|
+
|
|
54
|
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
55
|
+
# Resolve $BRAIN_HOME from this script's location ($BRAIN_HOME/.shims/ under a
|
|
56
|
+
# pipx install, $repo_root/bin/ in a dev checkout). Honor an explicit
|
|
57
|
+
# BRAIN_HOME override if the launcher set one.
|
|
58
|
+
BRAIN_HOME_RESOLVED="${BRAIN_HOME:-$( cd "$SCRIPT_DIR/.." && pwd )}"
|
|
59
|
+
|
|
60
|
+
# Pid paths default to $BRAIN_HOME/run/ — NOT /tmp. macOS's tmp_cleaner reaps
|
|
61
|
+
# files left untouched for 3 days, silently vanishing a long-lived daemon's
|
|
62
|
+
# pid file so brain-status misreports it as stopped; $BRAIN_HOME/run/ is not
|
|
63
|
+
# reaped, and must match the fg wrappers' defaults. Log paths keep their legacy
|
|
64
|
+
# /tmp fallback (the launchd plists redirect real output to $BRAIN_HOME/logs/).
|
|
65
|
+
# Overridable (unset = unchanged) so the test suite can run this shim
|
|
66
|
+
# hermetically against a tmp dir, never colliding with a live brain-up install.
|
|
67
|
+
WATCH_PID="${BRAIN_WATCH_PID:-$BRAIN_HOME_RESOLVED/run/brain-watch.pid}"
|
|
68
|
+
WATCH_LOG="${BRAIN_WATCH_LOG:-/tmp/brain-watch.log}"
|
|
69
|
+
BUILD_PID="${BRAIN_BUILD_PID:-$BRAIN_HOME_RESOLVED/run/brain-build.pid}"
|
|
70
|
+
BUILD_LOG="${BRAIN_BUILD_LOG:-/tmp/brain-build.log}"
|
|
71
|
+
|
|
72
|
+
# Pick the Python interpreter once. BRAIN_PY (env var) wins; otherwise
|
|
73
|
+
# prefer python3 on PATH. The installed-shim flow (in $BRAIN_HOME/bin/)
|
|
74
|
+
# always has BRAIN_PY exported by the Python launcher in
|
|
75
|
+
# src/brain/bin/_launcher.py — but we keep the python3 fallback for
|
|
76
|
+
# users who invoke the shims directly outside the launcher (e.g. via
|
|
77
|
+
# launchd plists that themselves set PATH but not BRAIN_PY).
|
|
78
|
+
if [[ -n "${BRAIN_PY:-}" ]]; then
|
|
79
|
+
PY="$BRAIN_PY"
|
|
80
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
81
|
+
PY="$(command -v python3)"
|
|
82
|
+
else
|
|
83
|
+
echo "no python interpreter found (set BRAIN_PY=… to override)" >&2
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
apply_overlay() {
|
|
88
|
+
if [[ "${BRAIN_NO_OVERLAY:-0}" == '1' ]]; then
|
|
89
|
+
echo 'overlay: skipped (BRAIN_NO_OVERLAY=1)'
|
|
90
|
+
return 0
|
|
91
|
+
fi
|
|
92
|
+
if ! command -v brain >/dev/null 2>&1; then
|
|
93
|
+
echo 'overlay: skipped (brain CLI not on PATH)'
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
# `--no-build` applies the overlay to the workspace without running
|
|
97
|
+
# `npx quartz build` — the build watcher does its own builds on every
|
|
98
|
+
# vault change, and we run a cold-start build below if needed.
|
|
99
|
+
if ! brain vault render --overlay --no-build --vault "$VAULT" 2>&1; then
|
|
100
|
+
echo 'overlay: failed — wiki will start with whatever is in the workspace' >&2
|
|
101
|
+
return 0
|
|
102
|
+
fi
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
current_healthy() {
|
|
106
|
+
# Healthy = `current` is a symlink, resolves to a directory, and that
|
|
107
|
+
# directory contains both `index.html` AND `.build-id`. Anything else
|
|
108
|
+
# (no symlink, dangling target, empty target, missing `.build-id`)
|
|
109
|
+
# means we need a cold-start build before the watcher comes up.
|
|
110
|
+
local cur="$QUARTZ_DIR/current"
|
|
111
|
+
if [[ ! -L "$cur" ]]; then
|
|
112
|
+
return 1
|
|
113
|
+
fi
|
|
114
|
+
local resolved
|
|
115
|
+
resolved="$(cd "$cur" 2>/dev/null && pwd -P)" || return 1
|
|
116
|
+
if [[ -z "$resolved" || ! -d "$resolved" ]]; then
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
if [[ ! -f "$resolved/index.html" || ! -f "$resolved/.build-id" ]]; then
|
|
120
|
+
return 1
|
|
121
|
+
fi
|
|
122
|
+
return 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
cold_start_build() {
|
|
126
|
+
if current_healthy; then
|
|
127
|
+
echo 'cold-start build: skipping cold-start build (current/ is healthy)'
|
|
128
|
+
return 0
|
|
129
|
+
fi
|
|
130
|
+
if [[ ! -d "$QUARTZ_DIR" ]]; then
|
|
131
|
+
echo "quartz workspace not found at $QUARTZ_DIR" >&2
|
|
132
|
+
echo 'set up once with:' >&2
|
|
133
|
+
echo " git clone https://github.com/jackyzha0/quartz.git \"$QUARTZ_DIR\"" >&2
|
|
134
|
+
echo " cd \"$QUARTZ_DIR\" && npm install" >&2
|
|
135
|
+
echo " cp \"$SCRIPT_DIR/../quartz.config.ts\" ./quartz.config.ts # optional brain-tuned config" >&2
|
|
136
|
+
return 1
|
|
137
|
+
fi
|
|
138
|
+
echo 'cold-start build: running first-time build, this takes ~40s...'
|
|
139
|
+
local started
|
|
140
|
+
started="$(date +%s)"
|
|
141
|
+
if ! BRAIN_WIKI_RELOAD=1 "$PY" -m brain.wiki.build_swap \
|
|
142
|
+
--vault "$VAULT" --keep "$KEEP"; then
|
|
143
|
+
echo 'cold-start build: failed — aborting brain-up' >&2
|
|
144
|
+
return 1
|
|
145
|
+
fi
|
|
146
|
+
local elapsed=$(( $(date +%s) - started ))
|
|
147
|
+
echo "cold-start build: first build done in ${elapsed}s"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
ensure_supervised() {
|
|
151
|
+
# brain (2026-05-08): single source of truth for daemon lifecycle.
|
|
152
|
+
# `bin/brain-install-launchd` writes both LaunchAgent plists, boots out
|
|
153
|
+
# any prior incarnation (so the script self-heals if launchd has the
|
|
154
|
+
# labels in a half-loaded state), and bootstraps them into the
|
|
155
|
+
# gui/$UID domain. From there on launchd's KeepAlive=true keeps both
|
|
156
|
+
# daemons alive across crashes, terminal-close, sleep/wake — the very
|
|
157
|
+
# failure modes the pre-launchd nohup paths used to lose.
|
|
158
|
+
#
|
|
159
|
+
# `BRAIN_NO_BUILD_WATCHER=1` keeps its name (back-compat with tests
|
|
160
|
+
# that use the pre-launchd flag) but its meaning is now "don't install
|
|
161
|
+
# LaunchAgents during this brain-up run." Tests rely on this so they
|
|
162
|
+
# don't accidentally drop plists into the developer's
|
|
163
|
+
# ~/Library/LaunchAgents.
|
|
164
|
+
if [[ "${BRAIN_NO_BUILD_WATCHER:-0}" == '1' ]]; then
|
|
165
|
+
echo 'launchd supervision: skipped (BRAIN_NO_BUILD_WATCHER=1)'
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
168
|
+
if ! "${BRAIN_INSTALL_LAUNCHD:-brain-install-launchd}"; then
|
|
169
|
+
echo 'launchd supervision: brain-install-launchd failed — daemons NOT supervised' >&2
|
|
170
|
+
return 1
|
|
171
|
+
fi
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
wait_for_url() {
|
|
175
|
+
local tries=30
|
|
176
|
+
while ((tries-- > 0)); do
|
|
177
|
+
if curl -fsS -o /dev/null "$URL/"; then
|
|
178
|
+
return 0
|
|
179
|
+
fi
|
|
180
|
+
sleep 1
|
|
181
|
+
done
|
|
182
|
+
echo "warning: $URL did not respond within 30s — is Caddy running and serving $QUARTZ_DIR/current?" >&2
|
|
183
|
+
return 1
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
apply_overlay
|
|
187
|
+
cold_start_build
|
|
188
|
+
ensure_supervised
|
|
189
|
+
|
|
190
|
+
if [[ "${BRAIN_OPEN_BROWSER:-1}" == '1' ]]; then
|
|
191
|
+
if wait_for_url; then
|
|
192
|
+
if command -v open >/dev/null 2>&1; then
|
|
193
|
+
open "$URL"
|
|
194
|
+
elif command -v xdg-open >/dev/null 2>&1; then
|
|
195
|
+
xdg-open "$URL" >/dev/null 2>&1 &
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
WATCH_PID_VALUE='?'
|
|
201
|
+
BUILD_PID_VALUE='?'
|
|
202
|
+
[[ -f "$WATCH_PID" ]] && WATCH_PID_VALUE="$(cat "$WATCH_PID")"
|
|
203
|
+
[[ -f "$BUILD_PID" ]] && BUILD_PID_VALUE="$(cat "$BUILD_PID")"
|
|
204
|
+
|
|
205
|
+
# brain (2026-05-08): note the supervisor in the report so the user
|
|
206
|
+
# knows their daemons survive terminal close + crashes. The pid '?'
|
|
207
|
+
# fallback handles the brief race between launchctl bootstrap and the
|
|
208
|
+
# wrapper writing the pid file (it's almost always populated by the
|
|
209
|
+
# time wait_for_url succeeds, but the fallback keeps the report
|
|
210
|
+
# readable in the unlikely race).
|
|
211
|
+
SUPERVISOR='launchd (KeepAlive)'
|
|
212
|
+
[[ "${BRAIN_NO_BUILD_WATCHER:-0}" == '1' ]] && SUPERVISOR='unsupervised (BRAIN_NO_BUILD_WATCHER=1)'
|
|
213
|
+
|
|
214
|
+
echo
|
|
215
|
+
echo '🧠 brain is up:'
|
|
216
|
+
echo " wiki: $URL (also brain.test if Caddy is configured)"
|
|
217
|
+
echo " vault: re-syncing on file changes (pid $WATCH_PID_VALUE)"
|
|
218
|
+
echo " build: re-rendering on vault changes (pid $BUILD_PID_VALUE)"
|
|
219
|
+
echo " supervisor: $SUPERVISOR"
|
|
220
|
+
echo ' stop: brain-down'
|
|
221
|
+
echo ' status: brain-status'
|