Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions scripts/launchd/com.brainlayer.enrich.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.brainlayer.enrich</string>

<key>ProgramArguments</key>
<array>
<string>__BRAINLAYER_BIN__</string>
<string>enrich</string>
<string>--batch-size</string>
<string>50</string>
<string>--max</string>
<string>500</string>
</array>

<key>StartInterval</key>
<integer>3600</integer>

<key>StandardOutPath</key>
<string>__HOME__/.local/share/brainlayer/logs/enrich.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/.local/share/brainlayer/logs/enrich.err</string>

<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:__HOME__/.local/bin</string>
<key>PYTHONUNBUFFERED</key>
<string>1</string>
<key>BRAINLAYER_STALL_TIMEOUT</key>
<string>300</string>
</dict>

<key>RunAtLoad</key>
<false/>

<key>Nice</key>
<integer>15</integer>

<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>
36 changes: 36 additions & 0 deletions scripts/launchd/com.brainlayer.index.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.brainlayer.index</string>

<key>ProgramArguments</key>
<array>
<string>__BRAINLAYER_BIN__</string>
<string>index</string>
</array>

<key>StartInterval</key>
<integer>1800</integer>

<key>StandardOutPath</key>
<string>__HOME__/.local/share/brainlayer/logs/index.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/.local/share/brainlayer/logs/index.err</string>
Comment on lines +17 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Log files will grow unbounded — consider rotation.

StandardOutPath and StandardErrorPath are append-only with no built-in rotation in launchd. Over time, index.log and index.err will grow without limit. Consider either rotating logs within the brainlayer CLI itself (e.g., logging.handlers.RotatingFileHandler) or adding a periodic cleanup step in install.sh / a separate launchd job.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/launchd/com.brainlayer.index.plist` around lines 17 - 20, The plist
currently writes to StandardOutPath and StandardErrorPath
(__HOME__/.local/share/brainlayer/logs/index.log and index.err) with no
rotation; update the system so logs are rotated: either implement rotation in
the brainlayer CLI (e.g., use Python's logging.handlers.RotatingFileHandler or
TimedRotatingFileHandler for the logger used by the index process) or add a
periodic maintenance launchd job (or a cleanup step in install.sh) that
rotates/truncates/archives those files and keeps a bounded history; ensure any
new launchd job references the same paths (StandardOutPath/StandardErrorPath)
and that rotation preserves file ownership/permissions.


<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:__HOME__/.local/bin</string>
<key>PYTHONUNBUFFERED</key>
<string>1</string>
</dict>

<key>RunAtLoad</key>
<true/>

<key>Nice</key>
<integer>10</integer>
</dict>
</plist>
82 changes: 82 additions & 0 deletions scripts/launchd/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Install BrainLayer launchd plists for auto-indexing and enrichment.
#
# Usage:
# ./scripts/launchd/install.sh # Install both
# ./scripts/launchd/install.sh index # Install indexing only
# ./scripts/launchd/install.sh enrich # Install enrichment only
# ./scripts/launchd/install.sh remove # Unload and remove all
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LAUNCH_DIR="$HOME/Library/LaunchAgents"
LOG_DIR="$HOME/.local/share/brainlayer/logs"
BRAINLAYER_BIN="${BRAINLAYER_BIN:-$(which brainlayer 2>/dev/null || echo "$HOME/.local/bin/brainlayer")}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if [ ! -x "$BRAINLAYER_BIN" ]; then
echo "ERROR: brainlayer binary not found at $BRAINLAYER_BIN"
echo "Install with: pip install -e . (from brainlayer repo)"
echo "Or set BRAINLAYER_BIN=/path/to/brainlayer"
exit 1
fi

mkdir -p "$LAUNCH_DIR" "$LOG_DIR"

install_plist() {
local name="$1"
local src="$SCRIPT_DIR/com.brainlayer.${name}.plist"
local dst="$LAUNCH_DIR/com.brainlayer.${name}.plist"

if [ ! -f "$src" ]; then
echo "ERROR: $src not found"
return 1
fi

# Replace placeholders
sed \
-e "s|__HOME__|$HOME|g" \
-e "s|__BRAINLAYER_BIN__|$BRAINLAYER_BIN|g" \
"$src" > "$dst"
Comment on lines +36 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Sed delimiter collision if paths contain |.

The sed substitution uses | as the delimiter. If $HOME or $BRAINLAYER_BIN contain a literal |, the substitution will break. This is unlikely for typical paths but a latent fragility. Consider using a less common delimiter or escaping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/launchd/install.sh` around lines 29 - 32, The sed substitutions in
scripts/launchd/install.sh using '|' as delimiter can fail if $HOME or
$BRAINLAYER_BIN contain '|' characters; update the sed invocation in the
install.sh script (the sed command that writes from "$src" to "$dst") to use a
safer delimiter (e.g., '@' or ':' ) or escape the variables before interpolation
so any '|' in $HOME/$BRAINLAYER_BIN won't break the pattern; change the -e
"s|__HOME__|$HOME|g" and -e "s|__BRAINLAYER_BIN__|$BRAINLAYER_BIN|g" occurrences
accordingly.


echo "Installed: $dst"
echo " Binary: $BRAINLAYER_BIN"
echo " Logs: $LOG_DIR/"

# Unload if already loaded, then load
launchctl bootout "gui/$(id -u)/com.brainlayer.${name}" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$dst"
echo " Loaded: com.brainlayer.${name}"
}

remove_plist() {
local name="$1"
local dst="$LAUNCH_DIR/com.brainlayer.${name}.plist"
launchctl bootout "gui/$(id -u)/com.brainlayer.${name}" 2>/dev/null || true
rm -f "$dst"
echo "Removed: com.brainlayer.${name}"
}

case "${1:-all}" in
index)
install_plist index
;;
enrich)
install_plist enrich
;;
all)
install_plist index
install_plist enrich
;;
remove)
remove_plist index
remove_plist enrich
;;
*)
echo "Usage: $0 [index|enrich|all|remove]"
exit 1
;;
esac

echo ""
echo "Done. Check logs at: $LOG_DIR/"
echo "Status: launchctl list | grep brainlayer"
59 changes: 49 additions & 10 deletions src/brainlayer/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ class CurrentContext:

def format(self) -> str:
"""Format as concise markdown — designed for voice/quick context."""
if not self.recent_sessions:
if not self.recent_sessions and not self.active_projects and not self.recent_files:
return "No recent session context available."

parts = ["## Current Context\n"]
Expand Down Expand Up @@ -434,6 +434,10 @@ def current_context(
Designed for voice assistants and quick context injection.
Lightweight — no embedding model needed.

Uses two data sources:
1. session_context table (git overlay data — may be sparse)
2. chunks table (always populated from indexing)

Args:
store: VectorStore instance
hours: How many hours back to look (default: 24)
Expand All @@ -442,15 +446,32 @@ def current_context(
CurrentContext with recent sessions, files, projects, branches
"""
result = CurrentContext()
cursor = store.conn.cursor()
date_from = (datetime.now() - timedelta(hours=hours)).isoformat()

# Get recent sessions
recent = sessions(store, days=max(1, hours // 24) or 1, limit=10)
# 1. Try session_context first (richest data)
# Convert hours to days properly — ceil division, minimum 1
days = max(1, -(-hours // 24)) # ceiling division trick
recent = sessions(store, days=days, limit=10)
result.recent_sessions = recent

if not recent:
return result
# 2. Also query chunks table directly for recent projects
# This catches sessions that haven't been through git_overlay yet
chunk_projects = list(
cursor.execute(
"""
SELECT project
FROM chunks
WHERE created_at >= ? AND project IS NOT NULL
GROUP BY project
ORDER BY MAX(created_at) DESC
LIMIT 10
""",
(date_from,),
)
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Extract active projects and branches
# Extract active projects and branches from session_context
projects = []
branches = []
plans = []
Expand All @@ -462,15 +483,17 @@ def current_context(
if s.plan_name and s.plan_name not in plans:
plans.append(s.plan_name)

# Merge in projects from chunks table (may have projects not in session_context)
for row in chunk_projects:
if row[0] and row[0] not in projects:
projects.append(row[0])

result.active_projects = projects[:5]
result.active_branches = branches[:5]
if plans:
result.active_plan = plans[0] # Most recent plan

# Get recent files from file_interactions
cursor = store.conn.cursor()
date_from = (datetime.now() - timedelta(hours=hours)).isoformat()

# 3. Get recent files from file_interactions
rows = list(
cursor.execute(
"""
Expand All @@ -485,6 +508,22 @@ def current_context(
)
result.recent_files = [r[0] for r in rows if r[0]]

# 4. If no files from interactions, try chunks metadata for file references
if not result.recent_files:
file_rows = list(
cursor.execute(
"""
SELECT DISTINCT source_file
FROM chunks
WHERE created_at >= ? AND source_file IS NOT NULL
ORDER BY created_at DESC
LIMIT 20
""",
(date_from,),
)
)
result.recent_files = [r[0] for r in file_rows if r[0]]

return result


Expand Down
Loading
Loading