brainlayer 1.5.44__tar.gz → 1.5.45__tar.gz
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.
- {brainlayer-1.5.44 → brainlayer-1.5.45}/PKG-INFO +8 -1
- {brainlayer-1.5.44 → brainlayer-1.5.45}/README.md +6 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/pyproject.toml +2 -1
- {brainlayer-1.5.44 → brainlayer-1.5.45}/server.json +2 -2
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/__init__.py +1 -1
- brainlayer-1.5.45/src/brainlayer/_build.py +1 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/backup_daily.py +17 -61
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/cli/__init__.py +30 -0
- brainlayer-1.5.45/src/brainlayer/drive_credentials.py +313 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/health_check.py +6 -0
- brainlayer-1.5.45/src/brainlayer/job_alerts.py +91 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/jsonl_backup.py +40 -1
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/maintenance.py +14 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/observability_backup.py +9 -0
- brainlayer-1.5.44/src/brainlayer/_build.py +0 -1
- {brainlayer-1.5.44 → brainlayer-1.5.45}/.gitignore +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/LICENSE +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/hotlane_brainbar_daemon.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/backup-daily.sh +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/brainlayer-env-run.sh +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/brainlayer.env.example +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.decay.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.drain.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.enrichment.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.index.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.observability.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.watch.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/fleet-watchdog.sh +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/install.sh +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/jsonl-backup.sh +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/throughput-watchdog.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/release-verify-signatures.sh +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/tier0-watchdog.sh +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/__main__.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/_helpers.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/agent_profiles.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/agent_provenance.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/alarm.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/archive_collapse.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/archive_lifecycle.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/backup_retention_invariant.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/badge_state.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/bitemporal.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/cachedelete_purge.swift +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/calibrate.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/chunk_origin.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/chunk_origin_backfill.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/chunk_origin_wipe.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/chunk_write.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/classify.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/claude_paths.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/cli/wizard.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/cli_new.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/cloud_backfill.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/clustering.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/config.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/content_class.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/correction_judge.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/data/agents_registry.yaml +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/db_shrink.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/decay.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/decay_backfill.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/decay_job.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/dedupe.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/dedupe_merge.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/deploy_drift.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/doctor.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/drain.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/drain_liveness.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/embeddings.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/engine.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/enrichment_controller.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/__init__.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/abcde_variants.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/abcde_variants.yaml +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/benchmark.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_gold.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_graders.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_judge.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/experiment_store.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/fixtures/README.md +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/fallback_replay.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/git_learning.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/hook_python.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/hooks/__init__.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/hooks/indexer.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/import_sweep.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/index_completeness.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/index_new.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/index_watchdog.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/ingest/__init__.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/ingest/codex.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/ingest/t3.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/ingest_denylist.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/ingest_guard.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/isolation_proof.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/job_lifecycle_health.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/jobs.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg/__init__.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_cleanup.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_judge.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_promotion.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_repo.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_review_session.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_session_finish.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_session_harvest.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/launchd_primitive.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/lexical_defense.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/lexical_defense_dictionary.json +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/log_cap.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/__init__.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/_format.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/_shared.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/enrich_handler.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/entity_handler.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/palette.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/search_handler.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/store_handler.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/tags_handler.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp_socket_config.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp_stdio_bridge.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/memory_types.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/migrate.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/notification_policy.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/observability_surface.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/p0_longitudinal_count.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/parent_death.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/paths.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pause.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/phonetic.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/__init__.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/analyze_communication.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/batch_extraction.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/brain_graph.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/chat_tags.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/chunk.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/classify.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/cloud_scrub.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/code_intelligence.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/correction_detection.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/correction_mining.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/digest.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/enrichment.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/entity_extraction.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/entity_resolution.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_corrections.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_markdown.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/frustration_mining.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/git_overlay.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/groq.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/kg_extraction.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/obsidian_export.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/operation_grouping.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/plan_linking.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/rate_limiter.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/relation_backfill.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/sanitize.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/secret_scrub.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/semantic_style.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/sentiment.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/session_enrichment.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/style_embed.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/style_index.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/temporal_chains.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/time_batcher.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/unified_timeline.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/write_queue.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/provenance.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/provenance_autosupersede.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/provenance_integration.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/queue_io.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/queue_merge.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/reembed_backfill.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/runtime_store.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/sandbox_db.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/scoping.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/search_fanout.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/search_profile.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/search_repo.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/session_repo.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/setup.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/socket_hygiene.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/source_class_migration.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/spotlight.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/storage.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/store.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/system_prompt_guard.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/t3_provenance.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/tag_normalization.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/taxonomy.json +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/telemetry.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/timestamp_iso.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/types.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/vector_store.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/wal_checkpoint.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/watcher.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/watcher_bridge.py +0 -0
- {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/writer_telemetry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: brainlayer
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.45
|
|
4
4
|
Summary: Persistent memory MCP server for AI agents — 17 tools, semantic search, knowledge graph, on-device SQLite
|
|
5
5
|
Project-URL: Homepage, https://brainlayer.etanheyman.com
|
|
6
6
|
Project-URL: Repository, https://github.com/EtanHey/brainlayer
|
|
@@ -23,6 +23,7 @@ Requires-Dist: abydos>=0.5.0
|
|
|
23
23
|
Requires-Dist: apsw>=3.45.0
|
|
24
24
|
Requires-Dist: deprecation>=2.1.0
|
|
25
25
|
Requires-Dist: google-api-python-client>=2.0.0
|
|
26
|
+
Requires-Dist: google-auth-oauthlib>=1.1.0
|
|
26
27
|
Requires-Dist: google-auth>=2.0.0
|
|
27
28
|
Requires-Dist: idna<4,>=2.5
|
|
28
29
|
Requires-Dist: jsonschema>=4.20.0
|
|
@@ -370,6 +371,12 @@ BRAINLAYER_ENRICH_BACKEND=groq brainlayer enrich # Force Groq
|
|
|
370
371
|
|
|
371
372
|
## CLI Reference
|
|
372
373
|
|
|
374
|
+
### Backups
|
|
375
|
+
|
|
376
|
+
Run `brainlayer backup auth` once to authorize BrainLayer's Drive backups in your browser.
|
|
377
|
+
The private token lives at `~/.config/brainlayer/drive-tokens.json`; `BRAINLAYER_DRIVE_TOKEN_PATH` overrides it.
|
|
378
|
+
BrainBar uses `brainlayer backup auth --status --json` to check and `brainlayer backup auth --json` to authorize.
|
|
379
|
+
|
|
373
380
|
```bash
|
|
374
381
|
brainlayer setup # Create ~/.config/brainlayer/brainlayer.env
|
|
375
382
|
brainlayer setup --launchd # Create config and install launchd agents
|
|
@@ -293,6 +293,12 @@ BRAINLAYER_ENRICH_BACKEND=groq brainlayer enrich # Force Groq
|
|
|
293
293
|
|
|
294
294
|
## CLI Reference
|
|
295
295
|
|
|
296
|
+
### Backups
|
|
297
|
+
|
|
298
|
+
Run `brainlayer backup auth` once to authorize BrainLayer's Drive backups in your browser.
|
|
299
|
+
The private token lives at `~/.config/brainlayer/drive-tokens.json`; `BRAINLAYER_DRIVE_TOKEN_PATH` overrides it.
|
|
300
|
+
BrainBar uses `brainlayer backup auth --status --json` to check and `brainlayer backup auth --json` to authorize.
|
|
301
|
+
|
|
296
302
|
```bash
|
|
297
303
|
brainlayer setup # Create ~/.config/brainlayer/brainlayer.env
|
|
298
304
|
brainlayer setup --launchd # Create config and install launchd agents
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "brainlayer"
|
|
3
|
-
version = "1.5.
|
|
3
|
+
version = "1.5.45"
|
|
4
4
|
description = "Persistent memory MCP server for AI agents — 17 tools, semantic search, knowledge graph, on-device SQLite"
|
|
5
5
|
license = {text = "Apache-2.0"}
|
|
6
6
|
readme = "README.md"
|
|
@@ -34,6 +34,7 @@ dependencies = [
|
|
|
34
34
|
"idna>=2.5,<4", # Imported through requests by backup and hook entrypoints
|
|
35
35
|
"google-api-python-client>=2.0.0", # Daily DB backup upload to Google Drive
|
|
36
36
|
"google-auth>=2.0.0", # OAuth refresh for Google Drive backups
|
|
37
|
+
"google-auth-oauthlib>=1.1.0", # Installed-app Drive consent for BrainLayer-owned backup tokens
|
|
37
38
|
"ranx>=0.3.20", # IR evaluation metrics + significance testing
|
|
38
39
|
"abydos>=0.5.0", # Beider-Morse phonetic matching for cross-script entity aliases
|
|
39
40
|
"deprecation>=2.1.0", # Imported by abydos phonetic modules used by prompt hooks
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.etanhey/brainlayer",
|
|
4
4
|
"title": "BrainLayer",
|
|
5
5
|
"description": "Persistent memory for AI agents \u2014 local-first semantic search, knowledge graph, and 17 MCP tools with ToolAnnotations. One SQLite file, no cloud, no API keys.",
|
|
6
|
-
"version": "1.5.
|
|
6
|
+
"version": "1.5.45",
|
|
7
7
|
"websiteUrl": "https://brainlayer.etanheyman.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/EtanHey/brainlayer",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"registryType": "pypi",
|
|
15
15
|
"registryBaseUrl": "https://pypi.org",
|
|
16
16
|
"identifier": "brainlayer",
|
|
17
|
-
"version": "1.5.
|
|
17
|
+
"version": "1.5.45",
|
|
18
18
|
"runtimeHint": "pipx",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "1.5.
|
|
5
|
+
__version__ = "1.5.45"
|
|
6
6
|
# Git sha the distributed package was built from; stamped by the release build, None in a source tree.
|
|
7
7
|
# scripts/sprint_gate.py reads it in "keg" mode to prove the served package is the code under test.
|
|
8
8
|
# No runtime `str | None` annotation: launchd/install.sh imports this under /usr/bin/python3 (3.9).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
BUILD_SHA = "d3d00b64768a53e7c534ee82dc9f9938aab65f81"
|
|
@@ -36,13 +36,14 @@ from typing import Any
|
|
|
36
36
|
|
|
37
37
|
import requests
|
|
38
38
|
|
|
39
|
+
from . import drive_credentials
|
|
39
40
|
from .paths import get_db_path
|
|
40
41
|
from .socket_hygiene import refuse_production_brainbar_socket
|
|
41
42
|
|
|
42
43
|
_sleep = time.sleep
|
|
43
44
|
|
|
44
|
-
DEFAULT_TOKEN_PATH =
|
|
45
|
-
DEFAULT_CLIENT_PATH =
|
|
45
|
+
DEFAULT_TOKEN_PATH = drive_credentials.DEFAULT_TOKEN_PATH
|
|
46
|
+
DEFAULT_CLIENT_PATH = drive_credentials.DEFAULT_CLIENT_PATH
|
|
46
47
|
CANONICAL_MACHINE_ID = "MacBook-Pro"
|
|
47
48
|
CANONICAL_FOLDER_PARTS = ["Brain Drive", "06_ARCHIVE", "backups", "brainlayer-db"]
|
|
48
49
|
# Compatibility alias for callers that explicitly target the legacy M4 folder.
|
|
@@ -69,7 +70,7 @@ DRIVE_UPLOAD_DEADLINE_FLOOR_ENV = "BRAINLAYER_DRIVE_UPLOAD_DEADLINE_FLOOR_SECOND
|
|
|
69
70
|
DRIVE_UPLOAD_MIN_BYTES_PER_SECOND_ENV = "BRAINLAYER_DRIVE_UPLOAD_MIN_BYTES_PER_SECOND"
|
|
70
71
|
DRIVE_UPLOAD_STALL_MAX_ATTEMPTS_ENV = "BRAINLAYER_DRIVE_UPLOAD_STALL_MAX_ATTEMPTS"
|
|
71
72
|
DRIVE_FOLDER_MIME = "application/vnd.google-apps.folder"
|
|
72
|
-
DRIVE_SCOPES =
|
|
73
|
+
DRIVE_SCOPES = drive_credentials.DRIVE_SCOPES
|
|
73
74
|
DEFAULT_LOCAL_COMPRESSED_KEEP = 3
|
|
74
75
|
DEFAULT_LOCAL_UNCOMPRESSED_KEEP = 1
|
|
75
76
|
DEFAULT_DRIVE_KEEP = 7
|
|
@@ -986,65 +987,12 @@ def _send_brainbar_json_request(
|
|
|
986
987
|
return json.loads(data.decode("utf-8"))
|
|
987
988
|
|
|
988
989
|
|
|
989
|
-
def
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
temp_path.write_text(content)
|
|
993
|
-
os.replace(temp_path, path)
|
|
994
|
-
finally:
|
|
995
|
-
temp_path.unlink(missing_ok=True)
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
def get_drive_credentials(token_path: Path = DEFAULT_TOKEN_PATH, client_path: Path = DEFAULT_CLIENT_PATH):
|
|
999
|
-
"""Load and refresh Google Drive OAuth credentials from the existing MCP auth files."""
|
|
1000
|
-
from google.auth.transport.requests import Request
|
|
1001
|
-
from google.oauth2.credentials import Credentials
|
|
1002
|
-
|
|
1003
|
-
token_path = Path(token_path).expanduser()
|
|
1004
|
-
client_path = Path(client_path).expanduser()
|
|
1005
|
-
if not token_path.exists():
|
|
1006
|
-
raise FileNotFoundError(f"Google Drive token file not found: {token_path}")
|
|
1007
|
-
if not client_path.exists():
|
|
1008
|
-
raise FileNotFoundError(f"Google OAuth client file not found: {client_path}")
|
|
1009
|
-
|
|
1010
|
-
lock_path = token_path.with_suffix(token_path.suffix + ".lock")
|
|
1011
|
-
with lock_path.open("w") as lock_file:
|
|
1012
|
-
fcntl.flock(lock_file, fcntl.LOCK_EX)
|
|
1013
|
-
token_data = json.loads(token_path.read_text())
|
|
1014
|
-
client_data = json.loads(client_path.read_text())["installed"]
|
|
1015
|
-
|
|
1016
|
-
expiry = token_data.get("expiry")
|
|
1017
|
-
if not expiry and token_data.get("expiry_date"):
|
|
1018
|
-
expiry = dt.datetime.fromtimestamp(int(token_data["expiry_date"]) / 1000, tz=dt.UTC).isoformat()
|
|
1019
|
-
|
|
1020
|
-
parsed_expiry = dt.datetime.fromisoformat(expiry.replace("Z", "+00:00")) if expiry else None
|
|
1021
|
-
if parsed_expiry and parsed_expiry.tzinfo:
|
|
1022
|
-
parsed_expiry = parsed_expiry.astimezone(dt.UTC).replace(tzinfo=None)
|
|
1023
|
-
elif parsed_expiry:
|
|
1024
|
-
parsed_expiry = parsed_expiry.replace(tzinfo=None)
|
|
1025
|
-
|
|
1026
|
-
creds = Credentials(
|
|
1027
|
-
token=token_data.get("access_token"),
|
|
1028
|
-
refresh_token=token_data.get("refresh_token"),
|
|
1029
|
-
token_uri=client_data["token_uri"],
|
|
1030
|
-
client_id=client_data["client_id"],
|
|
1031
|
-
client_secret=client_data["client_secret"],
|
|
1032
|
-
scopes=token_data.get("scope", " ".join(DRIVE_SCOPES)).split(),
|
|
1033
|
-
expiry=parsed_expiry,
|
|
1034
|
-
)
|
|
1035
|
-
|
|
1036
|
-
# google-auth Credentials.expired compares against a naive UTC helper, so keep expiry comparisons naive UTC.
|
|
1037
|
-
refresh_before = dt.datetime.now(dt.UTC).replace(tzinfo=None) + dt.timedelta(hours=2)
|
|
1038
|
-
if creds.expired or not creds.valid or (creds.expiry and creds.expiry < refresh_before):
|
|
1039
|
-
creds.refresh(Request())
|
|
1040
|
-
token_data["access_token"] = creds.token
|
|
1041
|
-
token_data["expiry"] = creds.expiry.isoformat() if creds.expiry else None
|
|
1042
|
-
_atomic_write_text(token_path, json.dumps(token_data, indent=2, sort_keys=True) + "\n")
|
|
990
|
+
def get_drive_credentials(token_path: Path | None = None, client_path: Path | None = None):
|
|
991
|
+
"""Use BrainLayer-owned credentials for both database and JSONL backups."""
|
|
992
|
+
return drive_credentials.load_credentials(token_path=token_path, client_path=client_path)
|
|
1043
993
|
|
|
1044
|
-
return creds
|
|
1045
994
|
|
|
1046
|
-
|
|
1047
|
-
def build_drive_service(token_path: Path = DEFAULT_TOKEN_PATH, client_path: Path = DEFAULT_CLIENT_PATH):
|
|
995
|
+
def build_drive_service(token_path: Path | None = None, client_path: Path | None = None):
|
|
1048
996
|
from googleapiclient.discovery import build
|
|
1049
997
|
|
|
1050
998
|
return build("drive", "v3", credentials=get_drive_credentials(token_path, client_path))
|
|
@@ -1954,7 +1902,15 @@ def main() -> int:
|
|
|
1954
1902
|
timeout_seconds = _configured_backup_timeout_seconds()
|
|
1955
1903
|
if _env_flag_enabled(BACKUP_SUPERVISED_CHILD_ENV):
|
|
1956
1904
|
return _run_backup_process(timeout_seconds)
|
|
1957
|
-
|
|
1905
|
+
from .job_alerts import report
|
|
1906
|
+
|
|
1907
|
+
try:
|
|
1908
|
+
code = _supervise_backup_process(timeout_seconds)
|
|
1909
|
+
except Exception:
|
|
1910
|
+
report("backup-daily", "Database backup failed; check the backup log")
|
|
1911
|
+
raise
|
|
1912
|
+
report("backup-daily", None if code == 0 else "Database backup failed; check the backup log")
|
|
1913
|
+
return code
|
|
1958
1914
|
|
|
1959
1915
|
|
|
1960
1916
|
if __name__ == "__main__":
|
|
@@ -50,6 +50,25 @@ sandbox_app = typer.Typer(help="Manage isolated sandbox BrainLayer databases")
|
|
|
50
50
|
app.add_typer(sandbox_app, name="sandbox")
|
|
51
51
|
jobs_app = typer.Typer(help="Inspect and restart installed BrainLayer LaunchAgents")
|
|
52
52
|
app.add_typer(jobs_app, name="jobs")
|
|
53
|
+
backup_app = typer.Typer(help="Manage BrainLayer backup authorization")
|
|
54
|
+
app.add_typer(backup_app, name="backup")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@backup_app.command("auth")
|
|
58
|
+
def backup_auth_command(
|
|
59
|
+
status: bool = typer.Option(False, "--status", help="Check authorization without changing the token."),
|
|
60
|
+
json_output: bool = typer.Option(False, "--json", help="Print one JSON result for BrainBar."),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Authorize the BrainLayer-owned Google Drive token in the system browser."""
|
|
63
|
+
from .. import drive_credentials
|
|
64
|
+
|
|
65
|
+
result = drive_credentials.credential_status() if status else drive_credentials.authorize()
|
|
66
|
+
if json_output:
|
|
67
|
+
typer.echo(json.dumps(result, sort_keys=True))
|
|
68
|
+
else:
|
|
69
|
+
typer.echo(result["reason"])
|
|
70
|
+
if not status and result["status"] != "ok":
|
|
71
|
+
raise typer.Exit(1)
|
|
53
72
|
|
|
54
73
|
|
|
55
74
|
@jobs_app.command("restart")
|
|
@@ -2804,6 +2823,7 @@ def wal_checkpoint(
|
|
|
2804
2823
|
quiet: bool = typer.Option(False, "--quiet", help="Silent unless error"),
|
|
2805
2824
|
) -> None:
|
|
2806
2825
|
"""Run a SQLite WAL checkpoint through the BrainLayer CLI."""
|
|
2826
|
+
from ..job_alerts import report as report_job_alert
|
|
2807
2827
|
from ..wal_checkpoint import run_wal_checkpoint
|
|
2808
2828
|
|
|
2809
2829
|
normalized_mode = mode.upper()
|
|
@@ -2815,12 +2835,14 @@ def wal_checkpoint(
|
|
|
2815
2835
|
run_wal_checkpoint(normalized_mode, retry_busy=True) if retry_busy else run_wal_checkpoint(normalized_mode)
|
|
2816
2836
|
)
|
|
2817
2837
|
except FileNotFoundError as e:
|
|
2838
|
+
report_job_alert("wal-checkpoint", "BrainLayer WAL checkpoint failed; check the job log")
|
|
2818
2839
|
if json_output:
|
|
2819
2840
|
console.print_json(data={"error": str(e)})
|
|
2820
2841
|
elif not quiet:
|
|
2821
2842
|
rprint(f"[bold red]Error:[/] {e}")
|
|
2822
2843
|
raise typer.Exit(1)
|
|
2823
2844
|
except Exception as e:
|
|
2845
|
+
report_job_alert("wal-checkpoint", "BrainLayer WAL checkpoint failed; check the job log")
|
|
2824
2846
|
if json_output:
|
|
2825
2847
|
console.print_json(data={"error": str(e)})
|
|
2826
2848
|
elif not quiet:
|
|
@@ -2838,7 +2860,9 @@ def wal_checkpoint(
|
|
|
2838
2860
|
)
|
|
2839
2861
|
|
|
2840
2862
|
if result["busy"]:
|
|
2863
|
+
report_job_alert("wal-checkpoint", "BrainLayer WAL checkpoint was busy; check the job log")
|
|
2841
2864
|
raise typer.Exit(1)
|
|
2865
|
+
report_job_alert("wal-checkpoint", None)
|
|
2842
2866
|
|
|
2843
2867
|
|
|
2844
2868
|
@app.command("repair-fts")
|
|
@@ -2846,6 +2870,7 @@ def repair_fts(
|
|
|
2846
2870
|
db_path: Path | None = typer.Argument(None, help="Offline database copy; omitted means read-only FTS count check."),
|
|
2847
2871
|
) -> None:
|
|
2848
2872
|
"""Check live FTS counts read-only, or rebuild an explicit offline copy."""
|
|
2873
|
+
from ..job_alerts import report as report_job_alert
|
|
2849
2874
|
from ..runtime_store import OfflineMigrator, check_fts_counts
|
|
2850
2875
|
|
|
2851
2876
|
try:
|
|
@@ -2858,8 +2883,13 @@ def repair_fts(
|
|
|
2858
2883
|
finally:
|
|
2859
2884
|
store.close()
|
|
2860
2885
|
except PermissionError as exc:
|
|
2886
|
+
report_job_alert("repair-fts", "BrainLayer FTS repair failed; check the job log")
|
|
2861
2887
|
rprint(f"[bold red]Repair refused:[/] {exc}")
|
|
2862
2888
|
raise typer.Exit(1) from exc
|
|
2889
|
+
except Exception:
|
|
2890
|
+
report_job_alert("repair-fts", "BrainLayer FTS repair failed; check the job log")
|
|
2891
|
+
raise
|
|
2892
|
+
report_job_alert("repair-fts", None)
|
|
2863
2893
|
console.print_json(data=result)
|
|
2864
2894
|
|
|
2865
2895
|
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""BrainLayer-owned Google Drive credentials shared by both backup jobs.
|
|
2
|
+
|
|
3
|
+
The MCP's token is a migration source only. Backup refreshes and interactive auth
|
|
4
|
+
write solely to BrainLayer's private token path.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import datetime as dt
|
|
11
|
+
import fcntl
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import socket
|
|
16
|
+
import stat
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from . import job_alerts
|
|
22
|
+
|
|
23
|
+
DRIVE_SCOPES = ["https://www.googleapis.com/auth/drive"]
|
|
24
|
+
TOKEN_ENV = "BRAINLAYER_DRIVE_TOKEN_PATH"
|
|
25
|
+
CLIENT_ENV = "BRAINLAYER_DRIVE_CLIENT_PATH"
|
|
26
|
+
DEFAULT_TOKEN_PATH = Path.home() / ".config/brainlayer/drive-tokens.json"
|
|
27
|
+
DEFAULT_CLIENT_PATH = Path.home() / ".config/google-drive-mcp/gcp-oauth.keys.json"
|
|
28
|
+
LEGACY_TOKEN_PATH = Path.home() / ".config/google-drive-mcp/tokens.json"
|
|
29
|
+
LOGGER = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _report_consent(reporter: Any, reason: str | None) -> None:
|
|
33
|
+
try:
|
|
34
|
+
reporter("drive-consent", reason)
|
|
35
|
+
except OSError:
|
|
36
|
+
LOGGER.warning("Drive consent alert could not be recorded")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DriveCredentialError(RuntimeError):
|
|
40
|
+
"""A payload-free credential failure safe for logs and notifications."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve_paths(*, environ: dict[str, str] | None = None, home: Path | None = None) -> tuple[Path, Path, Path]:
|
|
44
|
+
environ = os.environ if environ is None else environ
|
|
45
|
+
home = Path.home() if home is None else Path(home)
|
|
46
|
+
token = Path(environ.get(TOKEN_ENV) or home / ".config/brainlayer/drive-tokens.json").expanduser()
|
|
47
|
+
client = Path(environ.get(CLIENT_ENV) or home / ".config/google-drive-mcp/gcp-oauth.keys.json").expanduser()
|
|
48
|
+
legacy = home / ".config/google-drive-mcp/tokens.json"
|
|
49
|
+
return token, client, legacy
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _paths(
|
|
53
|
+
token_path: Path | None, client_path: Path | None, legacy_token_path: Path | None
|
|
54
|
+
) -> tuple[Path, Path, Path]:
|
|
55
|
+
defaults = resolve_paths()
|
|
56
|
+
return tuple(
|
|
57
|
+
Path(path).expanduser() if path is not None else default
|
|
58
|
+
for path, default in zip((token_path, client_path, legacy_token_path), defaults, strict=True)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read_object(path: Path, *, private: bool) -> dict[str, Any]:
|
|
63
|
+
try:
|
|
64
|
+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
raise DriveCredentialError("BrainLayer Drive token missing; run brainlayer backup auth") from None
|
|
67
|
+
except OSError:
|
|
68
|
+
raise DriveCredentialError("Drive credential file cannot be read") from None
|
|
69
|
+
try:
|
|
70
|
+
metadata = os.fstat(fd)
|
|
71
|
+
if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != os.getuid():
|
|
72
|
+
raise DriveCredentialError("Drive credential file must be a regular file owned by this user")
|
|
73
|
+
if private and stat.S_IMODE(metadata.st_mode) != 0o600:
|
|
74
|
+
raise DriveCredentialError("BrainLayer Drive token must have mode 0600")
|
|
75
|
+
with os.fdopen(fd, "r", encoding="utf-8") as stream:
|
|
76
|
+
fd = -1
|
|
77
|
+
payload = json.load(stream)
|
|
78
|
+
if not isinstance(payload, dict):
|
|
79
|
+
raise ValueError("not an object")
|
|
80
|
+
return payload
|
|
81
|
+
except (UnicodeError, ValueError, OSError):
|
|
82
|
+
raise DriveCredentialError("Drive credential file is invalid") from None
|
|
83
|
+
finally:
|
|
84
|
+
if fd >= 0:
|
|
85
|
+
os.close(fd)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _atomic_write(path: Path, payload: dict[str, Any]) -> None:
|
|
89
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
90
|
+
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
|
|
91
|
+
try:
|
|
92
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
93
|
+
os.fchmod(stream.fileno(), 0o600)
|
|
94
|
+
json.dump(payload, stream, indent=2, sort_keys=True)
|
|
95
|
+
stream.write("\n")
|
|
96
|
+
stream.flush()
|
|
97
|
+
os.fsync(stream.fileno())
|
|
98
|
+
os.replace(name, path)
|
|
99
|
+
finally:
|
|
100
|
+
# Only an uncommitted temporary file is removable; a token is never deleted.
|
|
101
|
+
if os.path.exists(name):
|
|
102
|
+
os.unlink(name)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@contextlib.contextmanager
|
|
106
|
+
def _token_lock(path: Path):
|
|
107
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
108
|
+
lock_path = path.with_suffix(path.suffix + ".lock")
|
|
109
|
+
fd = os.open(lock_path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
110
|
+
try:
|
|
111
|
+
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
112
|
+
yield
|
|
113
|
+
finally:
|
|
114
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
115
|
+
os.close(fd)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _migrate_if_missing(token: Path, legacy: Path) -> None:
|
|
119
|
+
if token.exists() or not legacy.exists():
|
|
120
|
+
return
|
|
121
|
+
payload = _read_object(legacy, private=False)
|
|
122
|
+
if not payload.get("access_token") or not payload.get("refresh_token"):
|
|
123
|
+
raise DriveCredentialError("Legacy Drive token is invalid; run brainlayer backup auth")
|
|
124
|
+
_atomic_write(token, payload)
|
|
125
|
+
LOGGER.warning("Copied legacy Google Drive token into BrainLayer-owned storage; legacy token retained")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _expiry(data: dict[str, Any]) -> dt.datetime | None:
|
|
129
|
+
raw = data.get("expiry")
|
|
130
|
+
if not raw and data.get("expiry_date"):
|
|
131
|
+
raw = dt.datetime.fromtimestamp(int(data["expiry_date"]) / 1000, tz=dt.UTC).isoformat()
|
|
132
|
+
if not raw:
|
|
133
|
+
return None
|
|
134
|
+
parsed = dt.datetime.fromisoformat(str(raw).replace("Z", "+00:00"))
|
|
135
|
+
return parsed.astimezone(dt.UTC).replace(tzinfo=None) if parsed.tzinfo else parsed
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def load_credentials(
|
|
139
|
+
*,
|
|
140
|
+
token_path: Path | None = None,
|
|
141
|
+
client_path: Path | None = None,
|
|
142
|
+
legacy_token_path: Path | None = None,
|
|
143
|
+
credentials_class: Any = None,
|
|
144
|
+
request_factory: Any = None,
|
|
145
|
+
migrate: bool = True,
|
|
146
|
+
persist_refresh: bool = True,
|
|
147
|
+
now: dt.datetime | None = None,
|
|
148
|
+
alert_reporter: Any = job_alerts.report,
|
|
149
|
+
):
|
|
150
|
+
"""Read/refresh under one lock; never erase the prior token on any failure."""
|
|
151
|
+
if credentials_class is None:
|
|
152
|
+
from google.oauth2.credentials import Credentials
|
|
153
|
+
|
|
154
|
+
credentials_class = Credentials
|
|
155
|
+
if request_factory is None:
|
|
156
|
+
from google.auth.transport.requests import Request
|
|
157
|
+
|
|
158
|
+
request_factory = Request
|
|
159
|
+
token, client, legacy = _paths(token_path, client_path, legacy_token_path)
|
|
160
|
+
with _token_lock(token):
|
|
161
|
+
if migrate:
|
|
162
|
+
_migrate_if_missing(token, legacy)
|
|
163
|
+
token_data = _read_object(token, private=True)
|
|
164
|
+
consented = token_data.get("consented_at")
|
|
165
|
+
if consented:
|
|
166
|
+
try:
|
|
167
|
+
consent_time = dt.datetime.fromisoformat(str(consented).replace("Z", "+00:00")).astimezone(dt.UTC)
|
|
168
|
+
except ValueError:
|
|
169
|
+
raise DriveCredentialError("BrainLayer Drive consent metadata is invalid") from None
|
|
170
|
+
remaining = consent_time + dt.timedelta(days=7) - (now or dt.datetime.now(dt.UTC)).astimezone(dt.UTC)
|
|
171
|
+
if remaining <= dt.timedelta(0):
|
|
172
|
+
_report_consent(alert_reporter, "Drive access expired: Reconnect in BrainBar")
|
|
173
|
+
raise DriveCredentialError("Drive access expired: Reconnect in BrainBar")
|
|
174
|
+
if remaining < dt.timedelta(days=1):
|
|
175
|
+
_report_consent(
|
|
176
|
+
alert_reporter,
|
|
177
|
+
"Google Drive access for BrainLayer backups expires tomorrow: click Reconnect in BrainBar",
|
|
178
|
+
)
|
|
179
|
+
if not token_data.get("refresh_token"):
|
|
180
|
+
raise DriveCredentialError("BrainLayer Drive token lacks refresh authorization; run brainlayer backup auth")
|
|
181
|
+
try:
|
|
182
|
+
client_data = _read_object(client, private=False)["installed"]
|
|
183
|
+
creds = credentials_class(
|
|
184
|
+
token=token_data.get("access_token"),
|
|
185
|
+
refresh_token=token_data.get("refresh_token"),
|
|
186
|
+
token_uri=client_data["token_uri"],
|
|
187
|
+
client_id=client_data["client_id"],
|
|
188
|
+
client_secret=client_data["client_secret"],
|
|
189
|
+
scopes=str(token_data.get("scope") or " ".join(DRIVE_SCOPES)).split(),
|
|
190
|
+
expiry=_expiry(token_data),
|
|
191
|
+
)
|
|
192
|
+
except (KeyError, TypeError, ValueError, DriveCredentialError):
|
|
193
|
+
raise DriveCredentialError("Google OAuth client or BrainLayer token is invalid") from None
|
|
194
|
+
refresh_before = dt.datetime.now(dt.UTC).replace(tzinfo=None) + dt.timedelta(hours=2)
|
|
195
|
+
if creds.expired or not creds.valid or (creds.expiry and creds.expiry < refresh_before):
|
|
196
|
+
try:
|
|
197
|
+
creds.refresh(request_factory())
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
from google.auth.exceptions import TransportError
|
|
200
|
+
|
|
201
|
+
if isinstance(exc, TransportError):
|
|
202
|
+
raise TransportError("Drive token refresh network failure") from None
|
|
203
|
+
if "invalid_grant" in str(exc).lower():
|
|
204
|
+
_report_consent(alert_reporter, "Drive access expired: Reconnect in BrainBar")
|
|
205
|
+
raise DriveCredentialError("Drive access expired: Reconnect in BrainBar") from None
|
|
206
|
+
raise DriveCredentialError("Drive token refresh failed; run brainlayer backup auth") from None
|
|
207
|
+
if persist_refresh:
|
|
208
|
+
token_data["access_token"] = creds.token
|
|
209
|
+
token_data["expiry"] = creds.expiry.isoformat() if creds.expiry else None
|
|
210
|
+
_atomic_write(token, token_data)
|
|
211
|
+
return creds
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def credential_status(*, now: dt.datetime | None = None, **kwargs: Any) -> dict[str, Any]:
|
|
215
|
+
"""Check the owned token only; do not migrate or write a refreshed token."""
|
|
216
|
+
from google.auth.exceptions import TransportError
|
|
217
|
+
|
|
218
|
+
token, _, _ = _paths(kwargs.get("token_path"), kwargs.get("client_path"), None)
|
|
219
|
+
if not token.exists():
|
|
220
|
+
return {
|
|
221
|
+
"state": "missing",
|
|
222
|
+
"reason": "BrainLayer Drive authorization is missing",
|
|
223
|
+
"expires_at": None,
|
|
224
|
+
"days_left": None,
|
|
225
|
+
}
|
|
226
|
+
try:
|
|
227
|
+
data = _read_object(token, private=True)
|
|
228
|
+
consented = data.get("consented_at")
|
|
229
|
+
try:
|
|
230
|
+
expires = (
|
|
231
|
+
dt.datetime.fromisoformat(str(consented).replace("Z", "+00:00")) + dt.timedelta(days=7)
|
|
232
|
+
if consented
|
|
233
|
+
else None
|
|
234
|
+
)
|
|
235
|
+
if expires and expires.tzinfo is None:
|
|
236
|
+
raise ValueError("consent time has no timezone")
|
|
237
|
+
except ValueError:
|
|
238
|
+
raise DriveCredentialError("BrainLayer Drive consent metadata is invalid") from None
|
|
239
|
+
current = (now or dt.datetime.now(dt.UTC)).astimezone(dt.UTC)
|
|
240
|
+
days_left = max(0.0, (expires - current).total_seconds() / 86400) if expires else None
|
|
241
|
+
timing = {"expires_at": expires.isoformat() if expires else None, "days_left": days_left}
|
|
242
|
+
if expires and current >= expires:
|
|
243
|
+
_report_consent(
|
|
244
|
+
kwargs.get("alert_reporter", job_alerts.report), "Drive access expired: Reconnect in BrainBar"
|
|
245
|
+
)
|
|
246
|
+
return {"state": "invalid", "reason": "Drive access expired: Reconnect in BrainBar", **timing}
|
|
247
|
+
load_credentials(**kwargs, migrate=False, persist_refresh=False, now=now)
|
|
248
|
+
except TransportError:
|
|
249
|
+
return {
|
|
250
|
+
"state": "expiring" if days_left is not None and days_left < 1 else "valid",
|
|
251
|
+
"reason": "status checked offline; last known state",
|
|
252
|
+
**timing,
|
|
253
|
+
}
|
|
254
|
+
except DriveCredentialError as exc:
|
|
255
|
+
return {"state": "invalid", "reason": str(exc), "expires_at": None, "days_left": None}
|
|
256
|
+
if days_left is not None and days_left < 1:
|
|
257
|
+
return {
|
|
258
|
+
"state": "expiring",
|
|
259
|
+
"reason": "Google Drive access for BrainLayer backups expires tomorrow: click Reconnect in BrainBar",
|
|
260
|
+
**timing,
|
|
261
|
+
}
|
|
262
|
+
return {"state": "valid", "reason": "BrainLayer Drive authorization is ready", **timing}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def authorize(
|
|
266
|
+
*,
|
|
267
|
+
token_path: Path | None = None,
|
|
268
|
+
client_path: Path | None = None,
|
|
269
|
+
flow_class: Any = None,
|
|
270
|
+
now: dt.datetime | None = None,
|
|
271
|
+
alert_reporter: Any = job_alerts.report,
|
|
272
|
+
) -> dict[str, str]:
|
|
273
|
+
"""Open Google consent in the system browser and save only BrainLayer's token."""
|
|
274
|
+
from oauthlib.oauth2.rfc6749.errors import AccessDeniedError
|
|
275
|
+
|
|
276
|
+
token, client, _ = _paths(token_path, client_path, None)
|
|
277
|
+
if flow_class is None:
|
|
278
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
279
|
+
|
|
280
|
+
flow_class = InstalledAppFlow
|
|
281
|
+
try:
|
|
282
|
+
_read_object(client, private=False)
|
|
283
|
+
flow = flow_class.from_client_secrets_file(str(client), scopes=DRIVE_SCOPES)
|
|
284
|
+
# Suppress the consent URL on stdout; BrainBar expects one JSON result.
|
|
285
|
+
creds = flow.run_local_server(
|
|
286
|
+
host="localhost",
|
|
287
|
+
bind_addr="127.0.0.1",
|
|
288
|
+
port=0,
|
|
289
|
+
timeout_seconds=180,
|
|
290
|
+
open_browser=True,
|
|
291
|
+
authorization_prompt_message=None,
|
|
292
|
+
access_type="offline",
|
|
293
|
+
prompt="consent",
|
|
294
|
+
)
|
|
295
|
+
if not creds.refresh_token:
|
|
296
|
+
raise DriveCredentialError("Google did not grant refresh authorization")
|
|
297
|
+
payload = {
|
|
298
|
+
"access_token": creds.token,
|
|
299
|
+
"refresh_token": creds.refresh_token,
|
|
300
|
+
"expiry": creds.expiry.isoformat() if creds.expiry else None,
|
|
301
|
+
"scope": " ".join(DRIVE_SCOPES),
|
|
302
|
+
"consented_at": (now or dt.datetime.now(dt.UTC)).astimezone(dt.UTC).isoformat(),
|
|
303
|
+
}
|
|
304
|
+
with _token_lock(token):
|
|
305
|
+
_atomic_write(token, payload)
|
|
306
|
+
_report_consent(alert_reporter, None)
|
|
307
|
+
return {"status": "ok", "reason": "BrainLayer Drive authorization saved"}
|
|
308
|
+
except (KeyboardInterrupt, AccessDeniedError):
|
|
309
|
+
return {"status": "cancelled", "reason": "Google authorization cancelled"}
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
if isinstance(exc, (socket.timeout, TimeoutError)) or type(exc).__name__ == "WSGITimeoutError":
|
|
312
|
+
return {"status": "timeout", "reason": "Google authorization timed out"}
|
|
313
|
+
return {"status": "error", "reason": "Google authorization failed; retry brainlayer backup auth"}
|
|
@@ -121,6 +121,7 @@ class HealthIssue:
|
|
|
121
121
|
class HealthCheckConfig:
|
|
122
122
|
db_path: Path = field(default_factory=get_db_path)
|
|
123
123
|
state_path: Path = field(default_factory=lambda: DEFAULT_STATE_PATH)
|
|
124
|
+
job_alert_path: Path | None = None
|
|
124
125
|
badge_state_path: Path | None = None
|
|
125
126
|
job_opt_path: Path | None = field(default_factory=installed_opt_path)
|
|
126
127
|
job_plist_dir: Path = field(default_factory=lambda: Path("~/Library/LaunchAgents").expanduser())
|
|
@@ -1283,6 +1284,11 @@ def _run_health_check_locked(
|
|
|
1283
1284
|
if state.get("_state_corrupt_now"):
|
|
1284
1285
|
add_issue("job_state_unknown", "critical", str(state["state_corrupt"]))
|
|
1285
1286
|
|
|
1287
|
+
from .job_alerts import active_alerts
|
|
1288
|
+
|
|
1289
|
+
for job, reason in active_alerts(config.job_alert_path).items():
|
|
1290
|
+
add_issue(f"job_alert_{job}", "critical", reason)
|
|
1291
|
+
|
|
1286
1292
|
def publish_badge_state() -> None:
|
|
1287
1293
|
if config.badge_state_path is None:
|
|
1288
1294
|
return
|