chore: Restructure Claude Code config into modular rules

Migrate monolithic root CLAUDE.md into .claude/rules/ structure
with focused topic files and glob-based contextual loading.
Clean up leftover helper scripts in .claude/tmp/.
This commit is contained in:
darken
2026-02-07 19:31:28 +01:00
committed by Matthias Urhahn
parent 9ff8d9e04e
commit 4e279ca44d
12 changed files with 313 additions and 467 deletions
+45
View File
@@ -0,0 +1,45 @@
# CAPod - Companion for AirPods
Android app that detects and monitors AirPods via Bluetooth LE. Displays battery levels, triggers popup notifications on case open, and provides home screen widgets.
## Project Structure
| Module | Description |
|--------|-------------|
| `app/` | Main Android app (FOSS and Google Play flavors) |
| `app-common/` | Shared code between phone and Wear OS apps |
## Build Flavors
- **FOSS** (`foss`): Open-source, no Google Play dependencies
- **Google Play** (`gplay`): Includes billing client for IAP
Quick build check: `./gradlew assembleFossDebug`
## Key Locations
| Path | Contains |
|------|----------|
| `app/src/main/java/` | Main app source (activities, fragments, services) |
| `app-common/src/main/java/` | Shared logic (monitor, bluetooth, models) |
| `app/src/main/res/` | Layouts, drawables, strings |
| `app-common/src/test/` | Unit tests |
| `app/build.gradle.kts` | App build config, dependencies, flavors |
## Development Tips
- Use `assembleFossDebug` as the fastest build variant for iteration
- Shared code goes in `app-common/`, app-specific code in `app/`
- Follow existing patterns — the codebase uses MVVM + Hilt + Coroutines
- Always use string resources for user-facing text (see localization rules)
- Check `git log --oneline -20` for commit message style before committing
## Rules Reference
Detailed guidelines are in `.claude/rules/`:
- `architecture.md` — Module structure, key components, data flow, dependencies
- `build-commands.md` — Build, test, lint, and release commands
- `localization.md` — String resource naming conventions
- `commit-guidelines.md` — Commit message format and prefixes
- `agent-instructions.md` — Sub-agent delegation and critical thinking
+39
View File
@@ -0,0 +1,39 @@
---
description: Instructions for Claude Code sub-agents and task delegation
globs:
- "**"
---
# Agent Instructions
## Critical Thinking
- Do not blindly accept information at face value
- Verify assumptions against actual code before proceeding
- When encountering unexpected behavior, investigate root causes rather than applying workarounds
- If something seems wrong, it probably is — dig deeper
## Explore vs. Implement
- **Explore first**: Before making changes, understand the existing code structure and patterns
- **Read before writing**: Always read relevant files before modifying them
- **Follow existing patterns**: Match the code style and architecture already in use
- **Minimal changes**: Only change what's necessary to accomplish the task
## Sub-Agent Delegation
When using Task tool to spawn sub-agents:
- Provide complete context — sub-agents don't share your conversation history unless noted
- Be specific about what you need: research only, or research + implementation
- Use `Explore` agent type for codebase investigation
- Use `Bash` agent type for running builds and tests
- Parallelize independent sub-agent tasks for efficiency
## Common Pitfalls
- Don't create new files when editing existing ones would suffice
- Don't add features beyond what was requested
- Don't refactor surrounding code when fixing a bug
- Don't add comments or documentation to code you didn't change
- Don't guess at file paths — use Glob/Grep to find them
+85
View File
@@ -0,0 +1,85 @@
---
description: Architecture overview, module structure, key components, data flow, and dependencies
globs:
- "app/**/*.kt"
- "app-common/**/*.kt"
- "**/*.gradle.kts"
---
# Architecture
## Multi-Module Structure
- **app/**: Main Android application with FOSS and Google Play flavors
- **app-common/**: Shared code between main app and Wear OS app
## Core Patterns
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
- **Dependency Injection**: Hilt/Dagger for dependency management
- **Coroutines**: Kotlin coroutines for async operations
- **Repository Pattern**: Data layer abstraction for monitoring and settings
## Key Components
### PodMonitor System
- `PodMonitor`: Core service that detects and tracks AirPods via Bluetooth LE
- `MonitorControl`: Manages background monitoring worker lifecycle
- `MonitorWorker`: Background worker that continuously scans for AirPods
- `BluetoothEventReceiver`: Handles system Bluetooth events
### Reaction System
- `ReactionSettingsFragment`: Configuration for popup notifications
- `PopUpWindow`: Displays AirPods status when case is opened
- `PopUpPodViewFactory`: Creates UI components for different pod models
### Common Utilities
- `EdgeToEdgeHelper`: Handles Android edge-to-edge display insets
## Build Configuration
### Flavors
- **FOSS**: Open-source version without Google Play dependencies
- **Google Play (gplay)**: Version with billing client for in-app purchases
### Build Types
- **debug**: Unobfuscated, full logging, no minification
- **beta**: Obfuscated, production-ready with strict lint checks
- **release**: Fully optimized for production distribution
## Data Flow
The app follows a unidirectional data flow:
1. `BluetoothEventReceiver` detects Bluetooth events
2. `MonitorWorker` scans for AirPods beacon data
3. `PodMonitor` processes and stores device information
4. ViewModels observe monitor data via repositories
5. UI components react to ViewModel state changes
6. `ReactionSystem` triggers popups and notifications
## Bluetooth LE Implementation
The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in `MonitorWorker` which runs as a long-lived background task.
## Multi-Platform Considerations
Code shared between phone and Wear OS apps is placed in `app-common`. When modifying shared functionality, ensure compatibility across both platforms.
## Testing Strategy
- **Unit Tests**: Located in `app-common/src/test/` for shared logic
- **Test Flavors**: Separate test configurations for FOSS and Google Play variants
## Key Dependencies
- **Hilt**: Dependency injection framework
- **AndroidX Navigation**: Fragment navigation with SafeArgs
- **WorkManager**: Background task scheduling for monitoring
- **Moshi**: JSON serialization for configuration and debugging
- **Material Design**: UI components following Material Design guidelines
+71
View File
@@ -0,0 +1,71 @@
---
description: Build, test, lint, and release commands for Gradle
globs:
- "**/*.gradle.kts"
- "**/*.gradle"
- "gradle/**"
---
# Build Commands
## Build
```bash
# Build debug version
./gradlew assembleDebug
# Build all variants (FOSS and Google Play flavors)
./gradlew assemble
# Build specific flavor and type
./gradlew assembleFossDebug
./gradlew assembleGplayRelease
# Build app bundles for Play Store
./gradlew bundleGplayRelease
```
## Testing
```bash
# Run all unit tests
./gradlew test
# Run unit tests for specific variant
./gradlew testFossDebugUnitTest
# Run instrumentation tests (requires connected device/emulator)
./gradlew connectedAndroidTest
./gradlew connectedFossDebugAndroidTest
# Run all checks (lint + tests)
./gradlew check
```
## Code Quality
```bash
# Run lint for all variants
./gradlew lint
# Run lint for specific variant
./gradlew lintFossDebug
# Auto-fix lint issues where possible
./gradlew lintFix
# Update lint baseline
./gradlew updateLintBaseline
```
## Release
```bash
./gradlew assembleFossRelease assembleGplayRelease
```
## Notes
- Use `assembleFossDebug` as the default quick-check build (fastest variant)
- Run `./gradlew check` before submitting changes to catch lint and test issues
- Instrumentation tests require a connected device or running emulator
+41
View File
@@ -0,0 +1,41 @@
---
description: Git commit message format and conventions
globs:
- "**"
---
# Commit Guidelines
## Format
```
<prefix>: <Short summary>
```
Summary line should be concise and describe the change. No period at the end.
## Prefixes
Use the existing commit history as reference. Common prefixes:
- **fix**: Bug fixes (e.g., `fix: Handle display cutouts in landscape mode`)
- **feat**: New features
- **refactor**: Code restructuring without behavior change
- **chore**: Maintenance, dependency updates, build config
- **docs**: Documentation changes
## Component Scope (optional)
When a change is scoped to a specific area, include it after the prefix:
- `fix(widget): Fix layout for devices with single charge detection`
- `feat(monitor): Add battery level caching`
- `refactor(popup): Extract pod view factory`
## Rules
- Keep the summary line under 72 characters
- Use imperative mood ("Add feature" not "Added feature")
- Reference issue numbers when applicable
- Do not include `Co-authored-by` trailers
- Look at recent `git log` output to match the project's existing style
+21
View File
@@ -0,0 +1,21 @@
---
description: Guidelines for adding and naming Android string resources
globs:
- "**/res/values*/strings.xml"
---
# Localization Guidelines
When adding new user-facing strings:
- **Always use string resources**: Never hardcode user-facing text in layouts or code
- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_bluetooth_enabled`)
- **Provide context**: String names should indicate usage and location
- **Consider pluralization**: Use Android plural resources (`<plurals>`) when quantities vary
## Naming Examples
- `profiles_create_title` (screen title)
- `profiles_name_label` (form field label)
- `profiles_delete_confirmation` (dialog message)
- `error_network_unavailable` (error message)
+11
View File
@@ -8,6 +8,17 @@
"Bash(ls:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"Bash(git add:*)",
"Bash(git log:*)",
"Bash(git commit:*)",
"Bash(git show:*)",
"Bash(git checkout:*)",
"Bash(git branch:*)",
"Bash(git stash:*)",
"Bash(git pop:*)",
"Bash(gh issue list:*)",
"Bash(gh pr list:*)",
"Bash(gh label list:*)",
"WebSearch",
"WebFetch(domain:support.google.com)",
"WebFetch(domain:github.com)",
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/env python3
"""
Script to fix import statements after removing app-common module.
This script fixes R class imports from app-common to app module.
"""
import os
import re
from pathlib import Path
def fix_r_import(file_path):
"""Fix R import in a single file."""
print(f"Fixing R import in: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Replace the import statement
updated_content = re.sub(
r'import eu\.darken\.capod\.common\.R$',
'import eu.darken.capod.R',
content,
flags=re.MULTILINE
)
if updated_content != content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f" ✓ Updated import in {file_path}")
return True
else:
print(f" - No changes needed in {file_path}")
return False
def main():
"""Main function to fix all import statements."""
project_root = Path.cwd()
app_src = project_root / "app" / "src"
# Find all Kotlin files that import eu.darken.capod.common.R
files_to_fix = []
for kt_file in app_src.rglob("*.kt"):
with open(kt_file, 'r', encoding='utf-8') as f:
content = f.read()
if re.search(r'import eu\.darken\.capod\.common\.R$', content, re.MULTILINE):
files_to_fix.append(kt_file)
print(f"Found {len(files_to_fix)} files with incorrect R imports")
success_count = 0
for file_path in files_to_fix:
if fix_r_import(file_path):
success_count += 1
print(f"\nFixed imports in {success_count} files")
return 0
if __name__ == "__main__":
exit(main())
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env python3
"""
Script to fix fully qualified R references in code.
This script finds and replaces eu.darken.capod.common.R with R.
"""
import os
import re
from pathlib import Path
def fix_qualified_r_references(file_path):
"""Fix fully qualified R references in a single file."""
print(f"Fixing qualified R references in: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Replace fully qualified references
updated_content = re.sub(
r'\beu\.darken\.capod\.common\.R\.',
'R.',
content
)
if updated_content != content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f" ✓ Fixed qualified R references in {file_path}")
return True
else:
print(f" - No changes needed in {file_path}")
return False
def main():
"""Main function to fix all qualified R references."""
project_root = Path.cwd()
app_src = project_root / "app" / "src"
# Find all Kotlin files that have qualified R references
files_to_fix = []
for kt_file in app_src.rglob("*.kt"):
with open(kt_file, 'r', encoding='utf-8') as f:
content = f.read()
if re.search(r'\beu\.darken\.capod\.common\.R\.', content):
files_to_fix.append(kt_file)
print(f"Found {len(files_to_fix)} files with qualified R references")
success_count = 0
for file_path in files_to_fix:
if fix_qualified_r_references(file_path):
success_count += 1
print(f"\nFixed qualified R references in {success_count} files")
return 0
if __name__ == "__main__":
exit(main())
-87
View File
@@ -1,87 +0,0 @@
#!/usr/bin/env python3
"""
Script to fix remaining R import issues.
This script finds files that use R.* but don't have R imports and adds them.
"""
import os
import re
from pathlib import Path
def needs_r_import(file_path):
"""Check if file uses R.* but doesn't import R."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check if file uses R.something
uses_r = re.search(r'\bR\.[a-zA-Z_]', content)
if not uses_r:
return False
# Check if file already imports R
has_import = re.search(r'import.*\.R$', content, re.MULTILINE)
if has_import:
return False
return True
def add_r_import(file_path):
"""Add R import to a file."""
print(f"Adding R import to: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Find the package line
package_match = re.search(r'^package\s+[^\n]+$', content, re.MULTILINE)
if not package_match:
print(f" Error: No package declaration found in {file_path}")
return False
# Find existing imports
import_pattern = r'^import\s+[^\n]+$'
existing_imports = list(re.finditer(import_pattern, content, re.MULTILINE))
if existing_imports:
# Insert after the last import
last_import = existing_imports[-1]
insert_pos = last_import.end()
updated_content = (content[:insert_pos] +
"\nimport eu.darken.capod.R" +
content[insert_pos:])
else:
# Insert after package line
insert_pos = package_match.end()
updated_content = (content[:insert_pos] +
"\n\nimport eu.darken.capod.R" +
content[insert_pos:])
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f" ✓ Added R import to {file_path}")
return True
def main():
"""Main function to fix all R import issues."""
project_root = Path.cwd()
app_src = project_root / "app" / "src"
# Find all Kotlin files that need R import
files_to_fix = []
for kt_file in app_src.rglob("*.kt"):
if needs_r_import(kt_file):
files_to_fix.append(kt_file)
print(f"Found {len(files_to_fix)} files that need R imports")
success_count = 0
for file_path in files_to_fix:
if add_r_import(file_path):
success_count += 1
print(f"\nAdded R imports to {success_count} files")
return 0
if __name__ == "__main__":
exit(main())
-105
View File
@@ -1,105 +0,0 @@
#!/usr/bin/env python3
"""
Script to merge strings.xml files from app-common module to app module.
This script merges all string entries from app-common into the corresponding app module files.
"""
import os
import re
import xml.etree.ElementTree as ET
from pathlib import Path
def merge_strings_xml(app_common_file, app_file):
"""Merge strings from app-common file into app file."""
print(f"Processing: {app_common_file.name} -> {app_file.name}")
# Read the app-common strings.xml content
with open(app_common_file, 'r', encoding='utf-8') as f:
common_content = f.read()
# Read the app strings.xml content
with open(app_file, 'r', encoding='utf-8') as f:
app_content = f.read()
# Extract string entries from app-common (including comments)
# Find everything between <resources> and </resources> tags, excluding the tags themselves
common_match = re.search(r'<resources[^>]*>(.*?)</resources>', common_content, re.DOTALL)
if not common_match:
print(f" Warning: No resources found in {app_common_file}")
return False
common_strings = common_match.group(1).strip()
if not common_strings:
print(f" Warning: No string content found in {app_common_file}")
return False
# Find the insertion point in the app file (before </resources>)
app_match = re.search(r'(.*?)(\s*</resources>)', app_content, re.DOTALL)
if not app_match:
print(f" Error: Invalid XML structure in {app_file}")
return False
# Merge the content
before_closing = app_match.group(1)
closing_tag = app_match.group(2)
# Add the common strings with proper spacing
merged_content = f"{before_closing}\n\n <!-- Strings from app-common -->\n{common_strings}\n{closing_tag}"
# Write the merged content back to the app file
with open(app_file, 'w', encoding='utf-8') as f:
f.write(merged_content)
print(f" ✓ Merged successfully")
return True
def main():
"""Main function to merge all strings.xml files."""
project_root = Path.cwd()
app_common_res = project_root / "app-common" / "src" / "main" / "res"
app_res = project_root / "app" / "src" / "main" / "res"
if not app_common_res.exists():
print(f"Error: app-common resources directory not found: {app_common_res}")
return 1
if not app_res.exists():
print(f"Error: app resources directory not found: {app_res}")
return 1
# Find all strings.xml files in app-common
common_strings_files = list(app_common_res.glob("*/strings.xml"))
if not common_strings_files:
print("Error: No strings.xml files found in app-common")
return 1
print(f"Found {len(common_strings_files)} strings.xml files to merge")
success_count = 0
error_count = 0
for common_file in sorted(common_strings_files):
# Determine the corresponding app file
locale_dir = common_file.parent.name
app_file = app_res / locale_dir / "strings.xml"
if not app_file.exists():
print(f" Error: Corresponding app file does not exist: {app_file}")
error_count += 1
continue
if merge_strings_xml(common_file, app_file):
success_count += 1
else:
error_count += 1
print(f"\nMerge complete:")
print(f" ✓ Successfully merged: {success_count}")
print(f" ✗ Errors: {error_count}")
return 0 if error_count == 0 else 1
if __name__ == "__main__":
exit(main())
-158
View File
@@ -1,158 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
### Build Commands
```bash
# Build debug version
./gradlew assembleDebug
# Build all variants (FOSS and Google Play flavors)
./gradlew assemble
# Build specific flavor and type
./gradlew assembleFossDebug
./gradlew assembleGplayRelease
# Build app bundles for Play Store
./gradlew bundleGplayRelease
```
### Testing Commands
```bash
# Run all unit tests
./gradlew test
# Run unit tests for specific variant
./gradlew testFossDebugUnitTest
# Run instrumentation tests (requires connected device/emulator)
./gradlew connectedAndroidTest
./gradlew connectedFossDebugAndroidTest
# Run all checks (lint + tests)
./gradlew check
```
### Code Quality Commands
```bash
# Run lint for all variants
./gradlew lint
# Run lint for specific variant
./gradlew lintFossDebug
# Auto-fix lint issues where possible
./gradlew lintFix
# Update lint baseline
./gradlew updateLintBaseline
```
### Release Commands
```bash
./gradlew assembleFossRelease assembleGplayRelease
```
## Architecture Overview
### Multi-Module Structure
- **app/**: Main Android application with FOSS and Google Play flavors
- **app-common/**: Shared code between main app
### Core Architecture Patterns
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
- **Dependency Injection**: Hilt/Dagger for dependency management
- **Coroutines**: Extensive use of Kotlin coroutines for async operations
- **Repository Pattern**: Data layer abstraction for monitoring and settings
### Key Components
#### PodMonitor System
- `PodMonitor`: Core service that detects and tracks AirPods via Bluetooth LE
- `MonitorControl`: Manages background monitoring worker lifecycle
- `MonitorWorker`: Background worker that continuously scans for AirPods
- `BluetoothEventReceiver`: Handles system Bluetooth events
#### Reaction System
- `ReactionSettingsFragment`: Configuration for popup notifications
- `PopUpWindow`: Displays AirPods status when case is opened
- `PopUpPodViewFactory`: Creates UI components for different pod models
#### Common Utilities
- `EdgeToEdgeHelper`: Handles Android edge-to-edge display insets
### Build Configuration
#### Flavors
- **FOSS**: Open-source version without Google Play dependencies
- **Google Play**: Version with billing client for in-app purchases
#### Build Types
- **debug**: Unobfuscated, full logging, no minification
- **beta**: Obfuscated, production-ready with strict lint checks
- **release**: Fully optimized for production distribution
### Data Flow Architecture
The app follows a unidirectional data flow:
1. `BluetoothEventReceiver` detects Bluetooth events
2. `MonitorWorker` scans for AirPods beacon data
3. `PodMonitor` processes and stores device information
4. ViewModels observe monitor data via repositories
5. UI components react to ViewModel state changes
6. `ReactionSystem` triggers popups and notifications
### Testing Strategy
- **Unit Tests**: Located in `app-common/src/test/` for shared logic
- **Test Flavors**: Separate test configurations for FOSS and Google Play variants
### Key Dependencies
- **Hilt**: Dependency injection framework
- **AndroidX Navigation**: Fragment navigation with SafeArgs
- **WorkManager**: Background task scheduling for monitoring
- **Moshi**: JSON serialization for configuration and debugging
- **Material Design**: UI components following Material Design guidelines
## Development Notes
### Bluetooth LE Implementation
The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in
`MonitorWorker` which runs as a long-lived background task.
### Multi-Platform Considerations
Code shared between phone and Wear OS apps is placed in `app-common`. When modifying shared functionality, ensure
compatibility across both platforms.
### Localization Guidelines
When adding new user-facing strings:
- **Always use string resources**: Never hardcode user-facing text in layouts or code
- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_bluetooth_enabled`)
- **Provide context**: String names should indicate usage and location
- **Consider pluralization**: Use Android plural resources (`<plurals>`) when quantities vary
Examples of correct string naming:
- `profiles_create_title` (screen title)
- `profiles_name_label` (form field label)
- `profiles_delete_confirmation` (dialog message)
- `error_network_unavailable` (error message)