···11# solstone Environment Configuration
22# Copy this file to .env and fill in your values
33+# See docs/INSTALL.md for setup instructions
3445# Required: Path to your journal directory
56JOURNAL_PATH=/path/to/your/journal
6777-# AI API Keys (at least one required)
88+# AI API Keys (at least one required for chat and insights)
89GOOGLE_API_KEY=your-google-api-key
910OPENAI_API_KEY=your-openai-api-key
1011ANTHROPIC_API_KEY=your-anthropic-api-key
1212+1313+# HuggingFace token for speaker diarization (required for audio transcription)
1414+# Get token: https://huggingface.co/settings/tokens
1515+# Accept model terms: https://huggingface.co/pyannote/speaker-diarization-3.1
1616+HUGGINGFACE_API_KEY=your-huggingface-token
1717+1818+# Rev.ai token for imported audio transcription (optional)
1919+# Sign up: https://www.rev.ai/ then get token at https://www.rev.ai/access_token
2020+REVAI_ACCESS_TOKEN=your-revai-token
+3-25
README.md
···3434 muse (AI agents)
3535```
36363737-## Requirements
3838-3939-- Python 3.10 or later
4040-- AI API keys (Google, OpenAI, or Anthropic)
4141-- Platform-specific dependencies for screen/audio capture
4242-4343-## Quick Start
4444-4545-1. Install the package:
4646- ```bash
4747- pip install -e .
4848- ```
4949-5050-2. Configure environment (copy `.env.example` to `.env` and add your settings):
5151- ```bash
5252- JOURNAL_PATH=/path/to/your/journal
5353- GOOGLE_API_KEY=your-api-key
5454- ```
5555-5656-3. Start the supervisor (handles capture and processing):
5757- ```bash
5858- think-supervisor
5959- ```
3737+## Getting Started
60386161-4. Launch the web interface:
6262- http://localhost:8000/
3939+See **[docs/INSTALL.md](docs/INSTALL.md)** for complete setup instructions including system dependencies, API keys, and first-run configuration.
63406441## Documentation
65426643| Topic | Document |
6744|-------|----------|
4545+| **Installation and setup** | [docs/INSTALL.md](docs/INSTALL.md) |
6846| Journal structure and data formats | [docs/JOURNAL.md](docs/JOURNAL.md) |
6947| Capture and observation | [docs/OBSERVE.md](docs/OBSERVE.md) |
7048| Processing and insights | [docs/THINK.md](docs/THINK.md) |
+67-3
apps/settings/routes.py
···44from __future__ import annotations
5566import json
77+import re
78from pathlib import Path
89from typing import Any
9101010-from flask import Blueprint, jsonify, render_template, request
1111+from flask import Blueprint, jsonify, request
11121213from apps.utils import log_app_action
1314from convey import state
···6465 return jsonify({"error": str(e)}), 500
656666676868+@settings_bp.route("/api/facet", methods=["POST"])
6969+def create_facet() -> Any:
7070+ """Create a new facet.
7171+7272+ Accepts JSON with:
7373+ title: Display title (required)
7474+7575+ The facet name (slug) is auto-generated from the title.
7676+ """
7777+ try:
7878+ data = request.get_json()
7979+ if not data:
8080+ return jsonify({"error": "No data provided"}), 400
8181+8282+ title = data.get("title", "").strip()
8383+ if not title:
8484+ return jsonify({"error": "Title is required"}), 400
8585+8686+ # Generate slug from title: lowercase, replace spaces/special chars with hyphens
8787+ slug = re.sub(r"[^a-z0-9]+", "-", title.lower())
8888+ slug = slug.strip("-") # Remove leading/trailing hyphens
8989+9090+ if not slug:
9191+ return (
9292+ jsonify({"error": "Title must contain at least one letter or number"}),
9393+ 400,
9494+ )
9595+9696+ # Check for conflicts with existing facets
9797+ from think.facets import get_facets
9898+9999+ existing = get_facets()
100100+ if slug in existing:
101101+ return jsonify({"error": f"Facet '{slug}' already exists"}), 409
102102+103103+ # Create facet directory and config
104104+ facet_path = Path(state.journal_root) / "facets" / slug
105105+ facet_path.mkdir(parents=True, exist_ok=True)
106106+107107+ config = {
108108+ "title": title,
109109+ "description": "",
110110+ "color": "#667eea",
111111+ "emoji": "📦",
112112+ }
113113+114114+ config_file = facet_path / "facet.json"
115115+ with open(config_file, "w", encoding="utf-8") as f:
116116+ json.dump(config, f, indent=2, ensure_ascii=False)
117117+ f.write("\n")
118118+119119+ # Log the creation
120120+ log_app_action(
121121+ app="settings",
122122+ facet=slug,
123123+ action="facet_create",
124124+ params={"title": title},
125125+ )
126126+127127+ return jsonify({"success": True, "facet": slug, "config": config}), 201
128128+129129+ except Exception as e:
130130+ return jsonify({"error": str(e)}), 500
131131+132132+67133@settings_bp.route("/api/facet/<facet_name>")
68134def get_facet_config(facet_name: str) -> Any:
69135 """Get configuration for a specific facet."""
···141207 Returns:
142208 {day, entries, next_cursor} where next_cursor is null if no more days
143209 """
144144- import re
145145-146210 logs_dir = Path(state.journal_root) / "facets" / facet_name / "logs"
147211148212 if not logs_dir.exists():
···151151152152 facetPillsContainer.appendChild(pill);
153153 });
154154+155155+ // Add "+" button to create new facets (only if facets enabled)
156156+ if (!facetsDisabled) {
157157+ const addButton = document.createElement('div');
158158+ addButton.className = 'facet-add-pill';
159159+ addButton.textContent = '+';
160160+ addButton.title = 'Create new facet';
161161+ addButton.onclick = () => openFacetCreateModal();
162162+ facetPillsContainer.appendChild(addButton);
163163+ }
154164 }
155165156166 // Update selection styles without re-rendering
···873883874884 // Expose selectFacet globally for notifications and other services
875885 window.selectFacet = selectFacet;
886886+887887+ // ========== FACET CREATION MODAL ==========
888888+889889+ // Create modal element (once)
890890+ function ensureFacetCreateModal() {
891891+ if (document.getElementById('facetCreateModal')) return;
892892+893893+ const modal = document.createElement('div');
894894+ modal.id = 'facetCreateModal';
895895+ modal.className = 'facet-create-modal';
896896+ modal.innerHTML = `
897897+ <div class="facet-create-content">
898898+ <h3>Create New Facet</h3>
899899+ <div class="facet-create-field">
900900+ <label for="facetCreateTitle">Title</label>
901901+ <input type="text" id="facetCreateTitle" placeholder="e.g., Work Projects" autofocus>
902902+ <div class="facet-create-slug" id="facetCreateSlug"></div>
903903+ <div class="facet-create-error" id="facetCreateError"></div>
904904+ </div>
905905+ <div class="facet-create-buttons">
906906+ <button class="facet-create-cancel" id="facetCreateCancel">Cancel</button>
907907+ <button class="facet-create-submit" id="facetCreateSubmit" disabled>Create</button>
908908+ </div>
909909+ </div>
910910+ `;
911911+ document.body.appendChild(modal);
912912+913913+ // Wire up events
914914+ const titleInput = document.getElementById('facetCreateTitle');
915915+ const slugDisplay = document.getElementById('facetCreateSlug');
916916+ const submitBtn = document.getElementById('facetCreateSubmit');
917917+ const cancelBtn = document.getElementById('facetCreateCancel');
918918+ const errorDisplay = document.getElementById('facetCreateError');
919919+920920+ // Live slug generation as user types
921921+ titleInput.addEventListener('input', () => {
922922+ const title = titleInput.value.trim();
923923+ const slug = titleToSlug(title);
924924+ if (slug) {
925925+ slugDisplay.textContent = slug;
926926+ slugDisplay.classList.add('has-slug');
927927+ } else {
928928+ slugDisplay.textContent = '';
929929+ slugDisplay.classList.remove('has-slug');
930930+ }
931931+ submitBtn.disabled = !slug;
932932+ errorDisplay.classList.remove('visible');
933933+ });
934934+935935+ // Enter to submit
936936+ titleInput.addEventListener('keydown', (e) => {
937937+ if (e.key === 'Enter' && !submitBtn.disabled) {
938938+ e.preventDefault();
939939+ submitFacetCreate();
940940+ } else if (e.key === 'Escape') {
941941+ closeFacetCreateModal();
942942+ }
943943+ });
944944+945945+ // Cancel button
946946+ cancelBtn.addEventListener('click', closeFacetCreateModal);
947947+948948+ // Submit button
949949+ submitBtn.addEventListener('click', submitFacetCreate);
950950+951951+ // Click outside to close
952952+ modal.addEventListener('click', (e) => {
953953+ if (e.target === modal) {
954954+ closeFacetCreateModal();
955955+ }
956956+ });
957957+ }
958958+959959+ // Convert title to slug (kebab-case)
960960+ function titleToSlug(title) {
961961+ if (!title) return '';
962962+ return title
963963+ .toLowerCase()
964964+ .replace(/[^a-z0-9]+/g, '-')
965965+ .replace(/^-+|-+$/g, '');
966966+ }
967967+968968+ // Open the modal
969969+ function openFacetCreateModal() {
970970+ ensureFacetCreateModal();
971971+ const modal = document.getElementById('facetCreateModal');
972972+ const titleInput = document.getElementById('facetCreateTitle');
973973+ const slugDisplay = document.getElementById('facetCreateSlug');
974974+ const submitBtn = document.getElementById('facetCreateSubmit');
975975+ const errorDisplay = document.getElementById('facetCreateError');
976976+977977+ // Reset form
978978+ titleInput.value = '';
979979+ slugDisplay.textContent = '';
980980+ slugDisplay.classList.remove('has-slug');
981981+ submitBtn.disabled = true;
982982+ errorDisplay.classList.remove('visible');
983983+984984+ modal.classList.add('visible');
985985+ titleInput.focus();
986986+ }
987987+988988+ // Close the modal
989989+ function closeFacetCreateModal() {
990990+ const modal = document.getElementById('facetCreateModal');
991991+ if (modal) {
992992+ modal.classList.remove('visible');
993993+ }
994994+ }
995995+996996+ // Submit facet creation
997997+ async function submitFacetCreate() {
998998+ const titleInput = document.getElementById('facetCreateTitle');
999999+ const submitBtn = document.getElementById('facetCreateSubmit');
10001000+ const errorDisplay = document.getElementById('facetCreateError');
10011001+10021002+ const title = titleInput.value.trim();
10031003+ if (!title) return;
10041004+10051005+ submitBtn.disabled = true;
10061006+ submitBtn.textContent = 'Creating...';
10071007+10081008+ try {
10091009+ const response = await fetch('/app/settings/api/facet', {
10101010+ method: 'POST',
10111011+ headers: { 'Content-Type': 'application/json' },
10121012+ body: JSON.stringify({ title })
10131013+ });
10141014+10151015+ const data = await response.json();
10161016+10171017+ if (!response.ok) {
10181018+ throw new Error(data.error || 'Failed to create facet');
10191019+ }
10201020+10211021+ // Success - close modal, select new facet, navigate to settings
10221022+ closeFacetCreateModal();
10231023+10241024+ // Add new facet to local data
10251025+ const newFacet = {
10261026+ name: data.facet,
10271027+ title: data.config.title,
10281028+ color: data.config.color,
10291029+ emoji: data.config.emoji,
10301030+ muted: false,
10311031+ count: 0
10321032+ };
10331033+ activeFacets.push(newFacet);
10341034+ window.facetsData = activeFacets;
10351035+10361036+ // Re-render facet bar
10371037+ renderFacetChooser();
10381038+10391039+ // Select the new facet
10401040+ selectFacet(data.facet);
10411041+10421042+ // Navigate to settings app to customize
10431043+ window.location.href = '/app/settings';
10441044+10451045+ } catch (error) {
10461046+ errorDisplay.textContent = error.message;
10471047+ errorDisplay.classList.add('visible');
10481048+ submitBtn.disabled = false;
10491049+ submitBtn.textContent = 'Create';
10501050+ }
10511051+ }
87610528771053 // Run initialization when DOM is ready
8781054 if (document.readyState === 'loading') {
+1-1
docs/CONVEY.md
···7474- Is automatically discovered and registered by `AppRegistry`
7575- Can provide facet-scoped views and background services
76767777-**Available apps:** home, todos, inbox, chat, agents, search, calendar, entities, news, stats, tokens, settings, import, live, dev
7777+Browse `/apps/` to see available apps.
78787979### Core Routes
8080
+246
docs/INSTALL.md
···11+# Installation Guide
22+33+Complete setup instructions for solstone on Linux and macOS.
44+55+## Prerequisites
66+77+- Python 3.10 or later
88+- Git
99+- ffmpeg (for audio processing)
1010+1111+### Linux (Fedora/RHEL)
1212+1313+```bash
1414+sudo dnf install python3 python3-pip git ffmpeg pipewire gstreamer1-plugins-base
1515+```
1616+1717+### Linux (Ubuntu/Debian)
1818+1919+```bash
2020+sudo apt install python3 python3-pip git ffmpeg pipewire gstreamer1.0-tools
2121+```
2222+2323+### Linux (Arch)
2424+2525+```bash
2626+sudo pacman -S python python-pip git ffmpeg pipewire gstreamer
2727+```
2828+2929+### macOS
3030+3131+```bash
3232+xcode-select --install # Command line tools
3333+brew install python git ffmpeg
3434+```
3535+3636+---
3737+3838+## Installation
3939+4040+1. Clone and install:
4141+4242+```bash
4343+git clone https://github.com/solpbc/solstone.git
4444+cd solstone
4545+pip install -e .
4646+```
4747+4848+2. Copy the environment template:
4949+5050+```bash
5151+cp .env.example .env
5252+```
5353+5454+3. Create your journal directory:
5555+5656+```bash
5757+mkdir -p ~/Documents/journal
5858+```
5959+6060+4. Edit `.env` and set your journal path:
6161+6262+```
6363+JOURNAL_PATH=~/Documents/journal
6464+```
6565+6666+---
6767+6868+## API Keys
6969+7070+solstone requires API keys for AI services. Configure these in your `.env` file.
7171+7272+### Google AI (Gemini) - Recommended
7373+7474+Primary backend for transcription, vision analysis, and insights.
7575+7676+1. Go to [Google AI Studio](https://aistudio.google.com/apikey)
7777+2. Sign in with your Google account
7878+3. Click "Create API Key"
7979+4. Copy the key to `.env`:
8080+8181+```
8282+GOOGLE_API_KEY=your-key-here
8383+```
8484+8585+### OpenAI (Optional)
8686+8787+Alternative backend for chat and agents.
8888+8989+1. Go to [OpenAI API Keys](https://platform.openai.com/api-keys)
9090+2. Sign in or create an account
9191+3. Click "Create new secret key"
9292+4. Copy the key to `.env`:
9393+9494+```
9595+OPENAI_API_KEY=your-key-here
9696+```
9797+9898+### Anthropic (Optional)
9999+100100+Alternative backend for chat and agents.
101101+102102+1. Go to [Anthropic Console](https://console.anthropic.com/settings/keys)
103103+2. Sign in or create an account
104104+3. Click "Create Key"
105105+4. Copy the key to `.env`:
106106+107107+```
108108+ANTHROPIC_API_KEY=your-key-here
109109+```
110110+111111+---
112112+113113+## Speaker Diarization (HuggingFace)
114114+115115+Required for audio transcription with speaker identification.
116116+117117+1. Create a [HuggingFace account](https://huggingface.co/join)
118118+119119+2. Go to [Settings > Access Tokens](https://huggingface.co/settings/tokens)
120120+121121+3. Click "Create new token" with read access
122122+123123+4. **Accept the model license** - visit [pyannote/speaker-diarization-3.1](https://huggingface.co/pyannote/speaker-diarization-3.1) and click "Agree and access repository"
124124+125125+5. Add the token to `.env`:
126126+127127+```
128128+HUGGINGFACE_API_KEY=your-token-here
129129+```
130130+131131+**Note:** The first transcription will download ~1GB of model files.
132132+133133+---
134134+135135+## Rev.ai for Imports (Optional)
136136+137137+For transcribing imported audio files (meetings, voice memos, etc.).
138138+139139+1. Sign up at [Rev.ai](https://www.rev.ai/)
140140+141141+2. After account creation, go to [Access Token](https://www.rev.ai/access_token)
142142+143143+3. Click "Generate New Access Token"
144144+145145+4. Add the token to `.env`:
146146+147147+```
148148+REVAI_ACCESS_TOKEN=your-token-here
149149+```
150150+151151+---
152152+153153+## First Run
154154+155155+### Start the Supervisor
156156+157157+The supervisor manages all background services (capture, processing):
158158+159159+```bash
160160+think-supervisor
161161+```
162162+163163+This starts:
164164+- **Observer** - Screen and audio capture
165165+- **Sense** - File detection and processing dispatch
166166+- **Callosum** - Message bus for inter-service communication
167167+168168+### Verify Services
169169+170170+In another terminal, check that services are running:
171171+172172+```bash
173173+pgrep -af "observer|observe-sense|think-supervisor"
174174+```
175175+176176+You should see three processes.
177177+178178+---
179179+180180+## Web Interface
181181+182182+### Set a Password
183183+184184+Before accessing the web interface, you must configure a password.
185185+186186+Create the config file:
187187+188188+```bash
189189+mkdir -p $JOURNAL_PATH/config
190190+cat > $JOURNAL_PATH/config/journal.json << 'EOF'
191191+{
192192+ "convey": {
193193+ "password": "your-password-here"
194194+ }
195195+}
196196+EOF
197197+```
198198+199199+Replace `your-password-here` with a secure password.
200200+201201+### Access the Interface
202202+203203+Open http://localhost:8000/ in your browser and log in with your password.
204204+205205+### Configure Your Identity
206206+207207+After logging in:
208208+209209+1. Click the **Settings** app in the left menu (gear icon)
210210+2. Fill in your identity information:
211211+ - **Full Name** - Your legal name
212212+ - **Preferred Name** - How you want to be addressed
213213+ - **Pronouns** - Select from dropdown
214214+ - **Timezone** - Auto-detected, adjust if needed
215215+216216+This helps the system identify you in transcripts and personalize AI responses.
217217+218218+---
219219+220220+## Health Check
221221+222222+Verify everything is working:
223223+224224+```bash
225225+# Check services are running
226226+pgrep -af "observer|observe-sense|think-supervisor"
227227+228228+# Check Callosum socket exists
229229+ls -la $JOURNAL_PATH/health/callosum.sock
230230+231231+# View service logs
232232+tail -f $JOURNAL_PATH/health/*.log
233233+```
234234+235235+See [DOCTOR.md](DOCTOR.md) for troubleshooting.
236236+237237+---
238238+239239+## Next Steps
240240+241241+- Create your first facet (project/context) in the web interface
242242+- Start capturing - the observer runs automatically
243243+- Review captured content in the Calendar and Transcripts apps
244244+- Chat with the AI about your journal content
245245+246246+For development setup, see [AGENTS.md](../AGENTS.md).
+1-1
muse/agents.py
···315315 continue
316316317317 # Extract backend to route to correct module
318318- backend = config.get("backend", "openai")
318318+ backend = config.get("backend", "google")
319319320320 # Set OpenAI key if needed
321321 if backend == "openai":