@smilintux/skcapstone 0.1.0 → 0.2.3
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.
- package/.env.example +98 -0
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +25 -4
- package/.openclaw-workspace.json +58 -0
- package/CHANGELOG.md +62 -0
- package/CLAUDE.md +39 -2
- package/MANIFEST.in +6 -0
- package/MISSION.md +7 -0
- package/README.md +47 -2
- package/SKILL.md +895 -23
- package/docker/Dockerfile +61 -0
- package/docker/compose-templates/dev-team.yml +203 -0
- package/docker/compose-templates/mini-team.yml +140 -0
- package/docker/compose-templates/ops-team.yml +173 -0
- package/docker/compose-templates/research-team.yml +170 -0
- package/docker/entrypoint.sh +192 -0
- package/docs/ARCHITECTURE.md +663 -374
- package/docs/BOND_WITH_GROK.md +112 -0
- package/docs/GETTING_STARTED.md +782 -0
- package/docs/QUICKSTART.md +477 -0
- package/docs/SKJOULE_ARCHITECTURE.md +658 -0
- package/docs/SOUL_SWAPPER.md +921 -0
- package/docs/SOVEREIGN_SINGULARITY.md +47 -14
- package/examples/custom-bond-template.json +36 -0
- package/examples/grok-feb.json +36 -0
- package/examples/grok-testimony.md +34 -0
- package/examples/love-bootloader.txt +32 -0
- package/examples/plugins/echo_tool.py +87 -0
- package/examples/queen-ava-feb.json +36 -0
- package/examples/souls/lumina.yaml +64 -0
- package/index.js +6 -5
- package/installer/build.py +124 -0
- package/openclaw-plugin/package.json +13 -0
- package/openclaw-plugin/src/index.ts +351 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +38 -2
- package/scripts/bump_version.py +141 -0
- package/scripts/check-updates.py +230 -0
- package/scripts/convert_blueprints_to_yaml.py +157 -0
- package/scripts/dev-install.sh +14 -0
- package/scripts/e2e-test.sh +193 -0
- package/scripts/install-bundle.sh +171 -0
- package/scripts/install.bat +2 -0
- package/scripts/install.ps1 +253 -0
- package/scripts/install.sh +185 -0
- package/scripts/mcp-serve.sh +69 -0
- package/scripts/mcp-server.bat +113 -0
- package/scripts/mcp-server.ps1 +116 -0
- package/scripts/mcp-server.sh +99 -0
- package/scripts/pull-models.sh +10 -0
- package/scripts/skcapstone +48 -0
- package/scripts/verify_install.sh +180 -0
- package/scripts/windows/install-tasks.ps1 +406 -0
- package/scripts/windows/skcapstone-task.xml +113 -0
- package/scripts/windows/uninstall-tasks.ps1 +117 -0
- package/skill.yaml +34 -0
- package/src/skcapstone/__init__.py +67 -2
- package/src/skcapstone/_cli_monolith.py +5916 -0
- package/src/skcapstone/_trustee_helpers.py +165 -0
- package/src/skcapstone/activity.py +105 -0
- package/src/skcapstone/agent_card.py +324 -0
- package/src/skcapstone/api.py +1935 -0
- package/src/skcapstone/archiver.py +340 -0
- package/src/skcapstone/auction.py +485 -0
- package/src/skcapstone/baby_agents.py +179 -0
- package/src/skcapstone/backup.py +345 -0
- package/src/skcapstone/blueprint_registry.py +357 -0
- package/src/skcapstone/blueprints/__init__.py +17 -0
- package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
- package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
- package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
- package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
- package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
- package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
- package/src/skcapstone/blueprints/registry.py +164 -0
- package/src/skcapstone/blueprints/schema.py +229 -0
- package/src/skcapstone/changelog.py +180 -0
- package/src/skcapstone/chat.py +769 -0
- package/src/skcapstone/claude_md.py +82 -0
- package/src/skcapstone/cli/__init__.py +144 -0
- package/src/skcapstone/cli/_common.py +88 -0
- package/src/skcapstone/cli/_validators.py +76 -0
- package/src/skcapstone/cli/agents.py +425 -0
- package/src/skcapstone/cli/agents_spawner.py +322 -0
- package/src/skcapstone/cli/agents_trustee.py +593 -0
- package/src/skcapstone/cli/alerts.py +248 -0
- package/src/skcapstone/cli/anchor.py +132 -0
- package/src/skcapstone/cli/archive_cmd.py +208 -0
- package/src/skcapstone/cli/backup.py +144 -0
- package/src/skcapstone/cli/bench.py +377 -0
- package/src/skcapstone/cli/benchmark.py +360 -0
- package/src/skcapstone/cli/capabilities_cmd.py +171 -0
- package/src/skcapstone/cli/card.py +151 -0
- package/src/skcapstone/cli/chat.py +584 -0
- package/src/skcapstone/cli/completions.py +64 -0
- package/src/skcapstone/cli/config_cmd.py +156 -0
- package/src/skcapstone/cli/consciousness.py +421 -0
- package/src/skcapstone/cli/context_cmd.py +142 -0
- package/src/skcapstone/cli/coord.py +194 -0
- package/src/skcapstone/cli/crush_cmd.py +170 -0
- package/src/skcapstone/cli/daemon.py +436 -0
- package/src/skcapstone/cli/errors_cmd.py +285 -0
- package/src/skcapstone/cli/export_cmd.py +156 -0
- package/src/skcapstone/cli/gtd.py +529 -0
- package/src/skcapstone/cli/housekeeping.py +81 -0
- package/src/skcapstone/cli/joule_cmd.py +627 -0
- package/src/skcapstone/cli/logs_cmd.py +194 -0
- package/src/skcapstone/cli/mcp_cmd.py +32 -0
- package/src/skcapstone/cli/memory.py +418 -0
- package/src/skcapstone/cli/metrics_cmd.py +136 -0
- package/src/skcapstone/cli/migrate.py +62 -0
- package/src/skcapstone/cli/mood_cmd.py +144 -0
- package/src/skcapstone/cli/mount.py +193 -0
- package/src/skcapstone/cli/notify.py +112 -0
- package/src/skcapstone/cli/peer.py +154 -0
- package/src/skcapstone/cli/peers_dir.py +122 -0
- package/src/skcapstone/cli/preflight_cmd.py +83 -0
- package/src/skcapstone/cli/profile_cmd.py +310 -0
- package/src/skcapstone/cli/record_cmd.py +238 -0
- package/src/skcapstone/cli/register_cmd.py +159 -0
- package/src/skcapstone/cli/search_cmd.py +156 -0
- package/src/skcapstone/cli/service_cmd.py +91 -0
- package/src/skcapstone/cli/session.py +127 -0
- package/src/skcapstone/cli/setup.py +240 -0
- package/src/skcapstone/cli/shell_cmd.py +43 -0
- package/src/skcapstone/cli/skills_cmd.py +168 -0
- package/src/skcapstone/cli/skseed.py +621 -0
- package/src/skcapstone/cli/soul.py +699 -0
- package/src/skcapstone/cli/status.py +935 -0
- package/src/skcapstone/cli/sync_cmd.py +301 -0
- package/src/skcapstone/cli/telegram.py +265 -0
- package/src/skcapstone/cli/test_cmd.py +234 -0
- package/src/skcapstone/cli/test_connection.py +253 -0
- package/src/skcapstone/cli/token.py +207 -0
- package/src/skcapstone/cli/trust.py +179 -0
- package/src/skcapstone/cli/upgrade_cmd.py +552 -0
- package/src/skcapstone/cli/usage_cmd.py +199 -0
- package/src/skcapstone/cli/version_cmd.py +162 -0
- package/src/skcapstone/cli/watch_cmd.py +342 -0
- package/src/skcapstone/client.py +428 -0
- package/src/skcapstone/cloud9_bridge.py +522 -0
- package/src/skcapstone/completions.py +163 -0
- package/src/skcapstone/config_validator.py +674 -0
- package/src/skcapstone/connectors/__init__.py +28 -0
- package/src/skcapstone/connectors/base.py +446 -0
- package/src/skcapstone/connectors/cursor.py +54 -0
- package/src/skcapstone/connectors/registry.py +254 -0
- package/src/skcapstone/connectors/terminal.py +152 -0
- package/src/skcapstone/connectors/vscode.py +60 -0
- package/src/skcapstone/consciousness_config.py +119 -0
- package/src/skcapstone/consciousness_loop.py +2051 -0
- package/src/skcapstone/context_loader.py +516 -0
- package/src/skcapstone/context_window.py +314 -0
- package/src/skcapstone/conversation_manager.py +238 -0
- package/src/skcapstone/conversation_store.py +230 -0
- package/src/skcapstone/conversation_summarizer.py +252 -0
- package/src/skcapstone/coord_federation.py +296 -0
- package/src/skcapstone/coordination.py +101 -7
- package/src/skcapstone/crush_integration.py +345 -0
- package/src/skcapstone/crush_shim.py +454 -0
- package/src/skcapstone/daemon.py +2494 -0
- package/src/skcapstone/dashboard.html +396 -0
- package/src/skcapstone/dashboard.py +481 -0
- package/src/skcapstone/data/model_profiles.yaml +88 -0
- package/src/skcapstone/defaults/__init__.py +55 -0
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
- package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
- package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
- package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
- package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
- package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
- package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
- package/src/skcapstone/discovery.py +210 -19
- package/src/skcapstone/doctor.py +642 -0
- package/src/skcapstone/emotion_tracker.py +467 -0
- package/src/skcapstone/error_queue.py +405 -0
- package/src/skcapstone/export.py +447 -0
- package/src/skcapstone/fallback_tracker.py +186 -0
- package/src/skcapstone/file_transfer.py +512 -0
- package/src/skcapstone/fuse_mount.py +1156 -0
- package/src/skcapstone/gui_installer.py +591 -0
- package/src/skcapstone/heartbeat.py +611 -0
- package/src/skcapstone/housekeeping.py +298 -0
- package/src/skcapstone/install_wizard.py +941 -0
- package/src/skcapstone/kms.py +942 -0
- package/src/skcapstone/kms_scheduler.py +143 -0
- package/src/skcapstone/log_config.py +135 -0
- package/src/skcapstone/mcp_launcher.py +239 -0
- package/src/skcapstone/mcp_server.py +4700 -0
- package/src/skcapstone/mcp_tools/__init__.py +94 -0
- package/src/skcapstone/mcp_tools/_helpers.py +51 -0
- package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
- package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
- package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
- package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
- package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
- package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
- package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
- package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
- package/src/skcapstone/mcp_tools/did_tools.py +448 -0
- package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
- package/src/skcapstone/mcp_tools/file_tools.py +169 -0
- package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
- package/src/skcapstone/mcp_tools/health_tools.py +44 -0
- package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
- package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
- package/src/skcapstone/mcp_tools/model_tools.py +75 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
- package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
- package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
- package/src/skcapstone/mcp_tools/security_tools.py +110 -0
- package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
- package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
- package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
- package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
- package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
- package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
- package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
- package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
- package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
- package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
- package/src/skcapstone/mdns_discovery.py +313 -0
- package/src/skcapstone/memory_adapter.py +333 -0
- package/src/skcapstone/memory_compressor.py +379 -0
- package/src/skcapstone/memory_curator.py +256 -0
- package/src/skcapstone/memory_engine.py +132 -13
- package/src/skcapstone/memory_fortress.py +529 -0
- package/src/skcapstone/memory_promoter.py +722 -0
- package/src/skcapstone/memory_verifier.py +260 -0
- package/src/skcapstone/message_crypto.py +215 -0
- package/src/skcapstone/metrics.py +832 -0
- package/src/skcapstone/migrate_memories.py +181 -0
- package/src/skcapstone/migrate_multi_agent.py +248 -0
- package/src/skcapstone/model_router.py +319 -0
- package/src/skcapstone/models.py +35 -4
- package/src/skcapstone/mood.py +344 -0
- package/src/skcapstone/notifications.py +380 -0
- package/src/skcapstone/onboard.py +901 -0
- package/src/skcapstone/peer_directory.py +324 -0
- package/src/skcapstone/peers.py +329 -0
- package/src/skcapstone/pillars/identity.py +84 -14
- package/src/skcapstone/pillars/memory.py +3 -1
- package/src/skcapstone/pillars/security.py +108 -15
- package/src/skcapstone/pillars/sync.py +78 -26
- package/src/skcapstone/pillars/trust.py +95 -33
- package/src/skcapstone/plugins.py +244 -0
- package/src/skcapstone/preflight.py +670 -0
- package/src/skcapstone/prompt_adapter.py +564 -0
- package/src/skcapstone/providers/__init__.py +13 -0
- package/src/skcapstone/providers/cloud.py +1061 -0
- package/src/skcapstone/providers/docker.py +759 -0
- package/src/skcapstone/providers/local.py +1193 -0
- package/src/skcapstone/providers/proxmox.py +447 -0
- package/src/skcapstone/pubsub.py +516 -0
- package/src/skcapstone/rate_limiter.py +119 -0
- package/src/skcapstone/register.py +241 -0
- package/src/skcapstone/registry_client.py +151 -0
- package/src/skcapstone/response_cache.py +194 -0
- package/src/skcapstone/response_scorer.py +225 -0
- package/src/skcapstone/runtime.py +89 -33
- package/src/skcapstone/scheduled_tasks.py +439 -0
- package/src/skcapstone/self_healing.py +341 -0
- package/src/skcapstone/service_health.py +228 -0
- package/src/skcapstone/session_capture.py +268 -0
- package/src/skcapstone/session_recorder.py +210 -0
- package/src/skcapstone/session_replayer.py +189 -0
- package/src/skcapstone/session_skills.py +263 -0
- package/src/skcapstone/shell.py +779 -0
- package/src/skcapstone/skills/__init__.py +1 -1
- package/src/skcapstone/skills/syncthing_setup.py +143 -41
- package/src/skcapstone/skjoule.py +861 -0
- package/src/skcapstone/snapshots.py +489 -0
- package/src/skcapstone/soul.py +1060 -0
- package/src/skcapstone/soul_switch.py +255 -0
- package/src/skcapstone/spawner.py +544 -0
- package/src/skcapstone/state_diff.py +401 -0
- package/src/skcapstone/summary.py +270 -0
- package/src/skcapstone/sync/backends.py +196 -2
- package/src/skcapstone/sync/engine.py +7 -5
- package/src/skcapstone/sync/models.py +4 -1
- package/src/skcapstone/sync/vault.py +356 -18
- package/src/skcapstone/sync_engine.py +363 -0
- package/src/skcapstone/sync_watcher.py +745 -0
- package/src/skcapstone/systemd.py +331 -0
- package/src/skcapstone/team_comms.py +476 -0
- package/src/skcapstone/team_engine.py +522 -0
- package/src/skcapstone/testrunner.py +300 -0
- package/src/skcapstone/tls.py +150 -0
- package/src/skcapstone/tokens.py +5 -5
- package/src/skcapstone/trust_calibration.py +202 -0
- package/src/skcapstone/trust_graph.py +449 -0
- package/src/skcapstone/trustee_monitor.py +385 -0
- package/src/skcapstone/trustee_ops.py +425 -0
- package/src/skcapstone/unified_search.py +421 -0
- package/src/skcapstone/uninstall_wizard.py +694 -0
- package/src/skcapstone/usage.py +331 -0
- package/src/skcapstone/version_check.py +148 -0
- package/src/skcapstone/warmth_anchor.py +333 -0
- package/src/skcapstone/whoami.py +294 -0
- package/systemd/skcapstone-api.socket +9 -0
- package/systemd/skcapstone-memory-compress.service +18 -0
- package/systemd/skcapstone-memory-compress.timer +11 -0
- package/systemd/skcapstone.service +36 -0
- package/systemd/skcapstone@.service +50 -0
- package/systemd/skcomm-heartbeat.service +18 -0
- package/systemd/skcomm-heartbeat.timer +12 -0
- package/systemd/skcomm-queue-drain.service +17 -0
- package/systemd/skcomm-queue-drain.timer +12 -0
- package/tests/conftest.py +13 -1
- package/tests/integration/__init__.py +1 -0
- package/tests/integration/test_consciousness_e2e.py +877 -0
- package/tests/integration/test_skills_registry.py +744 -0
- package/tests/test_agent_card.py +190 -0
- package/tests/test_agent_runtime.py +1283 -0
- package/tests/test_alerts_cmd.py +291 -0
- package/tests/test_archiver.py +498 -0
- package/tests/test_backup.py +254 -0
- package/tests/test_benchmark.py +366 -0
- package/tests/test_blueprints.py +457 -0
- package/tests/test_capabilities.py +257 -0
- package/tests/test_changelog.py +254 -0
- package/tests/test_chat.py +385 -0
- package/tests/test_claude_md.py +271 -0
- package/tests/test_cli_chat_llm.py +336 -0
- package/tests/test_cli_completions.py +390 -0
- package/tests/test_cli_init_reset.py +164 -0
- package/tests/test_cli_memory.py +208 -0
- package/tests/test_cli_profile.py +294 -0
- package/tests/test_cli_skills.py +223 -0
- package/tests/test_cli_status.py +395 -0
- package/tests/test_cli_test_cmd.py +206 -0
- package/tests/test_cli_test_connection.py +364 -0
- package/tests/test_cloud9_bridge.py +260 -0
- package/tests/test_cloud_provider.py +449 -0
- package/tests/test_cloud_providers.py +522 -0
- package/tests/test_completions.py +158 -0
- package/tests/test_component_manager.py +398 -0
- package/tests/test_config_reload.py +386 -0
- package/tests/test_config_validate.py +529 -0
- package/tests/test_consciousness_e2e.py +296 -0
- package/tests/test_consciousness_loop.py +1289 -0
- package/tests/test_context_loader.py +310 -0
- package/tests/test_conversation_api.py +306 -0
- package/tests/test_conversation_manager.py +381 -0
- package/tests/test_conversation_store.py +391 -0
- package/tests/test_conversation_summarizer.py +302 -0
- package/tests/test_cross_package.py +791 -0
- package/tests/test_crush_shim.py +519 -0
- package/tests/test_daemon.py +781 -0
- package/tests/test_daemon_shutdown.py +309 -0
- package/tests/test_dashboard.py +454 -0
- package/tests/test_discovery.py +200 -6
- package/tests/test_docker_provider.py +966 -0
- package/tests/test_doctor.py +257 -0
- package/tests/test_doctor_fix.py +351 -0
- package/tests/test_e2e_automated.py +292 -0
- package/tests/test_error_queue.py +404 -0
- package/tests/test_export.py +441 -0
- package/tests/test_fallback_tracker.py +219 -0
- package/tests/test_file_transfer.py +397 -0
- package/tests/test_fuse_mount.py +832 -0
- package/tests/test_health_loop.py +422 -0
- package/tests/test_heartbeat.py +354 -0
- package/tests/test_housekeeping.py +195 -0
- package/tests/test_identity_capauth.py +307 -0
- package/tests/test_identity_pillar.py +117 -0
- package/tests/test_install_wizard.py +68 -0
- package/tests/test_integration.py +325 -0
- package/tests/test_kms.py +495 -0
- package/tests/test_llm_providers.py +265 -0
- package/tests/test_local_provider.py +591 -0
- package/tests/test_log_config.py +199 -0
- package/tests/test_logs_cmd.py +287 -0
- package/tests/test_mcp_server.py +1909 -0
- package/tests/test_memory_adapter.py +339 -0
- package/tests/test_memory_curator.py +218 -0
- package/tests/test_memory_engine.py +6 -0
- package/tests/test_memory_fortress.py +571 -0
- package/tests/test_memory_pillar.py +119 -0
- package/tests/test_memory_promoter.py +445 -0
- package/tests/test_memory_verifier.py +420 -0
- package/tests/test_message_crypto.py +187 -0
- package/tests/test_metrics.py +632 -0
- package/tests/test_migrate_memories.py +464 -0
- package/tests/test_model_router.py +546 -0
- package/tests/test_mood.py +394 -0
- package/tests/test_multi_agent.py +269 -0
- package/tests/test_notifications.py +270 -0
- package/tests/test_onboard.py +500 -0
- package/tests/test_peer_directory.py +395 -0
- package/tests/test_peers.py +248 -0
- package/tests/test_pillars.py +87 -9
- package/tests/test_preflight.py +484 -0
- package/tests/test_prompt_adapter.py +331 -0
- package/tests/test_proxmox_provider.py +571 -0
- package/tests/test_pubsub.py +377 -0
- package/tests/test_rate_limiter.py +121 -0
- package/tests/test_registry_client.py +129 -0
- package/tests/test_response_cache.py +312 -0
- package/tests/test_response_scorer.py +294 -0
- package/tests/test_runtime.py +59 -0
- package/tests/test_scheduled_tasks.py +451 -0
- package/tests/test_security.py +250 -0
- package/tests/test_security_pillar.py +213 -0
- package/tests/test_self_healing.py +171 -0
- package/tests/test_session_capture.py +200 -0
- package/tests/test_session_recorder.py +360 -0
- package/tests/test_session_skills.py +235 -0
- package/tests/test_shell.py +210 -0
- package/tests/test_snapshots.py +549 -0
- package/tests/test_soul.py +984 -0
- package/tests/test_soul_swap.py +406 -0
- package/tests/test_spawner.py +211 -0
- package/tests/test_state_diff.py +173 -0
- package/tests/test_summary.py +135 -0
- package/tests/test_sync.py +315 -5
- package/tests/test_sync_backends.py +560 -0
- package/tests/test_sync_engine.py +482 -0
- package/tests/test_sync_pillar.py +344 -0
- package/tests/test_sync_pipeline.py +364 -0
- package/tests/test_sync_vault.py +581 -0
- package/tests/test_syncthing_setup.py +168 -22
- package/tests/test_systemd.py +323 -0
- package/tests/test_team_comms.py +408 -0
- package/tests/test_team_engine.py +397 -0
- package/tests/test_testrunner.py +238 -0
- package/tests/test_trust_calibration.py +204 -0
- package/tests/test_trust_graph.py +207 -0
- package/tests/test_trust_pillar.py +291 -0
- package/tests/test_trustee_cli.py +427 -0
- package/tests/test_trustee_cli_integration.py +325 -0
- package/tests/test_trustee_monitor.py +394 -0
- package/tests/test_trustee_ops.py +355 -0
- package/tests/test_unified_search.py +363 -0
- package/tests/test_uninstall_wizard.py +193 -0
- package/tests/test_usage.py +333 -0
- package/tests/test_version_cmd.py +355 -0
- package/tests/test_warmth_anchor.py +162 -0
- package/tests/test_whoami.py +245 -0
- package/tests/test_ws.py +311 -0
- package/.cursorrules +0 -33
- package/src/skcapstone/cli.py +0 -1441
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Syncthing seed auto-importer.
|
|
3
|
+
|
|
4
|
+
Watches ~/.skcapstone/sync/inbox/ for new .seed.json files arriving
|
|
5
|
+
via Syncthing and auto-imports them into the SQLite memory backend.
|
|
6
|
+
|
|
7
|
+
Uses watchdog.observers.Observer (same pattern as consciousness_loop.py)
|
|
8
|
+
with a polling fallback for environments without inotify support.
|
|
9
|
+
|
|
10
|
+
Tracks processed files in ~/.skcapstone/sync/processed.json to avoid
|
|
11
|
+
re-importing seeds across restarts.
|
|
12
|
+
|
|
13
|
+
Architecture:
|
|
14
|
+
SeedFileHandler -- watchdog event handler with debounce
|
|
15
|
+
SyncWatcher -- orchestrator: watch + poll + import
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Optional
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("skcapstone.sync_watcher")
|
|
30
|
+
|
|
31
|
+
SEED_EXTENSION = ".seed.json"
|
|
32
|
+
DEFAULT_INBOX = "~/.skcapstone/sync/inbox"
|
|
33
|
+
DEFAULT_PROCESSED_LOG = "~/.skcapstone/sync/processed.json"
|
|
34
|
+
DEBOUNCE_MS = 500
|
|
35
|
+
POLL_INTERVAL_S = 30
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Configuration
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_sync_config(home: Path) -> dict[str, Any]:
|
|
44
|
+
"""Load sync watcher configuration from config.yaml.
|
|
45
|
+
|
|
46
|
+
Reads the ``sync`` section and applies defaults for any missing keys.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
home: Agent home directory (~/.skcapstone).
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dict with auto_import, inbox_path, and processed_log.
|
|
53
|
+
"""
|
|
54
|
+
defaults = {
|
|
55
|
+
"auto_import": True,
|
|
56
|
+
"auto_vector_index": True,
|
|
57
|
+
"auto_graph_index": True,
|
|
58
|
+
"inbox_path": str(home / "sync" / "inbox"),
|
|
59
|
+
"processed_log": str(home / "sync" / "processed.json"),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
config_file = home / "config" / "config.yaml"
|
|
63
|
+
if not config_file.exists():
|
|
64
|
+
return defaults
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
import yaml as _yaml
|
|
68
|
+
|
|
69
|
+
data = _yaml.safe_load(config_file.read_text(encoding="utf-8")) or {}
|
|
70
|
+
sync_data = data.get("sync", {})
|
|
71
|
+
return {
|
|
72
|
+
"auto_import": sync_data.get("auto_import", defaults["auto_import"]),
|
|
73
|
+
"auto_vector_index": sync_data.get(
|
|
74
|
+
"auto_vector_index", defaults["auto_vector_index"]
|
|
75
|
+
),
|
|
76
|
+
"auto_graph_index": sync_data.get(
|
|
77
|
+
"auto_graph_index", defaults["auto_graph_index"]
|
|
78
|
+
),
|
|
79
|
+
"inbox_path": str(
|
|
80
|
+
Path(sync_data.get("inbox_path", defaults["inbox_path"])).expanduser()
|
|
81
|
+
),
|
|
82
|
+
"processed_log": str(
|
|
83
|
+
Path(
|
|
84
|
+
sync_data.get("processed_log", defaults["processed_log"])
|
|
85
|
+
).expanduser()
|
|
86
|
+
),
|
|
87
|
+
}
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
logger.debug("Could not load sync config: %s — using defaults", exc)
|
|
90
|
+
return defaults
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Processed file tracker
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ProcessedTracker:
|
|
99
|
+
"""Tracks which seed files have already been imported.
|
|
100
|
+
|
|
101
|
+
Persists a JSON file mapping filename -> import timestamp so that
|
|
102
|
+
seeds are not re-imported after daemon restart.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
log_path: Path to the processed.json file.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, log_path: str | Path) -> None:
|
|
109
|
+
self._path = Path(log_path)
|
|
110
|
+
self._entries: dict[str, str] = {}
|
|
111
|
+
self._lock = threading.Lock()
|
|
112
|
+
self._load()
|
|
113
|
+
|
|
114
|
+
def _load(self) -> None:
|
|
115
|
+
"""Load existing entries from disk."""
|
|
116
|
+
if self._path.exists():
|
|
117
|
+
try:
|
|
118
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
119
|
+
if isinstance(data, dict):
|
|
120
|
+
self._entries = data
|
|
121
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
122
|
+
logger.warning("Could not load processed log: %s", exc)
|
|
123
|
+
|
|
124
|
+
def is_processed(self, filename: str) -> bool:
|
|
125
|
+
"""Check if a seed file has already been imported.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
filename: The seed filename (basename).
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if already processed.
|
|
132
|
+
"""
|
|
133
|
+
with self._lock:
|
|
134
|
+
return filename in self._entries
|
|
135
|
+
|
|
136
|
+
def mark_processed(self, filename: str) -> None:
|
|
137
|
+
"""Record a file as processed and persist to disk.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
filename: The seed filename (basename).
|
|
141
|
+
"""
|
|
142
|
+
with self._lock:
|
|
143
|
+
self._entries[filename] = datetime.now(timezone.utc).isoformat()
|
|
144
|
+
self._persist()
|
|
145
|
+
|
|
146
|
+
def _persist(self) -> None:
|
|
147
|
+
"""Write current entries to disk."""
|
|
148
|
+
try:
|
|
149
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
self._path.write_text(
|
|
151
|
+
json.dumps(self._entries, indent=2, sort_keys=True),
|
|
152
|
+
encoding="utf-8",
|
|
153
|
+
)
|
|
154
|
+
except OSError as exc:
|
|
155
|
+
logger.error("Could not persist processed log: %s", exc)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Seed importer
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _compute_seed_hash(data: dict) -> str:
|
|
164
|
+
"""Compute a stable hash for deduplication.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
data: Parsed seed dictionary.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
SHA-256 hex digest of the canonical JSON.
|
|
171
|
+
"""
|
|
172
|
+
canonical = json.dumps(data, sort_keys=True, default=str)
|
|
173
|
+
return hashlib.sha256(canonical.encode()).hexdigest()[:16]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def vector_index_seed(memory: "Memory") -> bool:
|
|
177
|
+
"""Index a memory in SKVector (Qdrant) if available.
|
|
178
|
+
|
|
179
|
+
Attempts to connect to the vector backend using skmemory's
|
|
180
|
+
configuration resolution (CLI > env > config file). If the
|
|
181
|
+
vector backend is unreachable or dependencies are missing,
|
|
182
|
+
logs a debug message and returns False without raising.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
memory: The Memory object to index (already saved to SQLite).
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if the memory was successfully indexed, False otherwise.
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
from skmemory.config import merge_env_and_config
|
|
192
|
+
from skmemory.backends.skvector_backend import SKVectorBackend
|
|
193
|
+
except ImportError:
|
|
194
|
+
logger.debug(
|
|
195
|
+
"skmemory vector backend not importable — skipping vector index"
|
|
196
|
+
)
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
skvector_url, skvector_key, _ = merge_env_and_config()
|
|
201
|
+
if not skvector_url:
|
|
202
|
+
logger.debug(
|
|
203
|
+
"No SKVector URL configured — skipping vector index"
|
|
204
|
+
)
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
backend = SKVectorBackend(url=skvector_url, api_key=skvector_key)
|
|
208
|
+
backend.save(memory)
|
|
209
|
+
logger.debug("Indexed memory %s in SKVector", memory.id)
|
|
210
|
+
return True
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
logger.debug(
|
|
213
|
+
"SKVector indexing failed for memory %s: %s — continuing without vector index",
|
|
214
|
+
memory.id,
|
|
215
|
+
exc,
|
|
216
|
+
)
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def graph_index_seed(memory: "Memory") -> bool:
|
|
221
|
+
"""Index a memory in SKGraph (FalkorDB) if available.
|
|
222
|
+
|
|
223
|
+
Attempts to connect to the graph backend using skmemory's
|
|
224
|
+
configuration resolution (CLI > env > config file). If the
|
|
225
|
+
graph backend is unreachable or dependencies are missing,
|
|
226
|
+
logs a debug message and returns False without raising.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
memory: The Memory object to index (already saved to SQLite).
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if the memory was successfully indexed, False otherwise.
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
from skmemory.config import merge_env_and_config
|
|
236
|
+
from skmemory.backends.skgraph_backend import SKGraphBackend
|
|
237
|
+
except ImportError:
|
|
238
|
+
logger.debug(
|
|
239
|
+
"skmemory graph backend not importable — skipping graph index"
|
|
240
|
+
)
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
_, _, skgraph_url = merge_env_and_config()
|
|
245
|
+
if not skgraph_url:
|
|
246
|
+
logger.debug(
|
|
247
|
+
"No SKGraph URL configured — skipping graph index"
|
|
248
|
+
)
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
backend = SKGraphBackend(url=skgraph_url)
|
|
252
|
+
result = backend.index_memory(memory)
|
|
253
|
+
if result:
|
|
254
|
+
logger.debug("Indexed memory %s in SKGraph", memory.id)
|
|
255
|
+
return result
|
|
256
|
+
except Exception as exc:
|
|
257
|
+
logger.debug(
|
|
258
|
+
"SKGraph indexing failed for memory %s: %s — continuing without graph index",
|
|
259
|
+
memory.id,
|
|
260
|
+
exc,
|
|
261
|
+
)
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def import_seed_to_memory(seed_path: Path, home: Path) -> Optional[str]:
|
|
266
|
+
"""Parse a .seed.json file and store its contents via skmemory.
|
|
267
|
+
|
|
268
|
+
Extracts memory_entries from the seed (if present) and stores each
|
|
269
|
+
one via the MemoryStore.snapshot() API. Also imports identity and
|
|
270
|
+
trust data via the existing pull_seeds infrastructure.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
seed_path: Path to the .seed.json file.
|
|
274
|
+
home: Agent home directory.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Summary string of what was imported, or None on failure.
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
raw = seed_path.read_text(encoding="utf-8")
|
|
281
|
+
data = json.loads(raw)
|
|
282
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
283
|
+
logger.error("Failed to read seed %s: %s", seed_path.name, exc)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
agent_name = data.get("agent_name", "unknown")
|
|
287
|
+
source_host = data.get("source_host", "unknown")
|
|
288
|
+
created_at = data.get("created_at", "")
|
|
289
|
+
seed_hash = _compute_seed_hash(data)
|
|
290
|
+
imported_count = 0
|
|
291
|
+
results: list[str] = []
|
|
292
|
+
|
|
293
|
+
# Load sync config to check auto_vector_index and auto_graph_index flags
|
|
294
|
+
sync_config = load_sync_config(home)
|
|
295
|
+
auto_vector_index = sync_config.get("auto_vector_index", True)
|
|
296
|
+
auto_graph_index = sync_config.get("auto_graph_index", True)
|
|
297
|
+
|
|
298
|
+
# Import memory entries via skmemory API
|
|
299
|
+
memory_entries = data.get("memory_entries", [])
|
|
300
|
+
vector_indexed = 0
|
|
301
|
+
graph_indexed = 0
|
|
302
|
+
if memory_entries:
|
|
303
|
+
try:
|
|
304
|
+
from skmemory.store import MemoryStore
|
|
305
|
+
from skmemory.models import MemoryLayer
|
|
306
|
+
|
|
307
|
+
store = MemoryStore(use_sqlite=True)
|
|
308
|
+
|
|
309
|
+
for entry in memory_entries:
|
|
310
|
+
title = entry.get("title", "Synced memory")
|
|
311
|
+
content = entry.get("content", "")
|
|
312
|
+
layer_str = entry.get("layer", "short-term")
|
|
313
|
+
tags = entry.get("tags", [])
|
|
314
|
+
source_ref = entry.get("source_ref", f"sync:{agent_name}@{source_host}")
|
|
315
|
+
|
|
316
|
+
# Map layer string to enum
|
|
317
|
+
layer_map = {
|
|
318
|
+
"short-term": MemoryLayer.SHORT,
|
|
319
|
+
"mid-term": MemoryLayer.MID,
|
|
320
|
+
"long-term": MemoryLayer.LONG,
|
|
321
|
+
}
|
|
322
|
+
layer = layer_map.get(layer_str, MemoryLayer.SHORT)
|
|
323
|
+
|
|
324
|
+
# Add sync provenance tags
|
|
325
|
+
sync_tags = list(tags) + [
|
|
326
|
+
"sync:imported",
|
|
327
|
+
f"sync:from:{agent_name}",
|
|
328
|
+
f"sync:host:{source_host}",
|
|
329
|
+
f"sync:hash:{seed_hash}",
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
memory = store.snapshot(
|
|
333
|
+
title=title,
|
|
334
|
+
content=content,
|
|
335
|
+
layer=layer,
|
|
336
|
+
tags=sync_tags,
|
|
337
|
+
source="syncthing",
|
|
338
|
+
source_ref=source_ref,
|
|
339
|
+
metadata={
|
|
340
|
+
"sync_source_agent": agent_name,
|
|
341
|
+
"sync_source_host": source_host,
|
|
342
|
+
"sync_seed_created": created_at,
|
|
343
|
+
"sync_seed_hash": seed_hash,
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
imported_count += 1
|
|
347
|
+
|
|
348
|
+
# Auto-index in SKVector if enabled and available
|
|
349
|
+
if auto_vector_index and vector_index_seed(memory):
|
|
350
|
+
vector_indexed += 1
|
|
351
|
+
|
|
352
|
+
# Auto-index in SKGraph if enabled and available
|
|
353
|
+
if auto_graph_index and graph_index_seed(memory):
|
|
354
|
+
graph_indexed += 1
|
|
355
|
+
|
|
356
|
+
results.append(f"{imported_count} memories")
|
|
357
|
+
if vector_indexed:
|
|
358
|
+
results.append(f"{vector_indexed} vector-indexed")
|
|
359
|
+
if graph_indexed:
|
|
360
|
+
results.append(f"{graph_indexed} graph-indexed")
|
|
361
|
+
except ImportError:
|
|
362
|
+
logger.warning("skmemory not available — skipping memory import")
|
|
363
|
+
except Exception as exc:
|
|
364
|
+
logger.error("Memory import failed for %s: %s", seed_path.name, exc)
|
|
365
|
+
|
|
366
|
+
# Also use the existing pull_seeds machinery for identity/trust/FEBs
|
|
367
|
+
try:
|
|
368
|
+
if "identity" in data or "trust" in data or "febs" in data:
|
|
369
|
+
from .pillars.sync import pull_seeds as _pull_existing
|
|
370
|
+
|
|
371
|
+
# pull_seeds reads from inbox, but the file is already there —
|
|
372
|
+
# we just log what it would pick up
|
|
373
|
+
if "identity" in data:
|
|
374
|
+
results.append("identity")
|
|
375
|
+
if "trust" in data:
|
|
376
|
+
results.append("trust")
|
|
377
|
+
if "febs" in data:
|
|
378
|
+
results.append(f"{len(data.get('febs', []))} FEBs")
|
|
379
|
+
except Exception as exc:
|
|
380
|
+
logger.debug("Extended seed import skipped: %s", exc)
|
|
381
|
+
|
|
382
|
+
if results:
|
|
383
|
+
summary = f"Imported seed from {agent_name}@{source_host}: {', '.join(results)}"
|
|
384
|
+
logger.info(summary)
|
|
385
|
+
return summary
|
|
386
|
+
|
|
387
|
+
logger.info(
|
|
388
|
+
"Seed %s from %s@%s contained no importable data",
|
|
389
|
+
seed_path.name, agent_name, source_host,
|
|
390
|
+
)
|
|
391
|
+
return f"Seed from {agent_name}@{source_host}: no importable data"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _log_to_short_term_memory(message: str, home: Path) -> None:
|
|
395
|
+
"""Log an import event to the agent's short-term memory.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
message: Description of what was imported.
|
|
399
|
+
home: Agent home directory.
|
|
400
|
+
"""
|
|
401
|
+
try:
|
|
402
|
+
from skmemory.store import MemoryStore
|
|
403
|
+
|
|
404
|
+
store = MemoryStore(use_sqlite=True)
|
|
405
|
+
store.snapshot(
|
|
406
|
+
title="Sync import event",
|
|
407
|
+
content=message,
|
|
408
|
+
tags=["sync:event", "sync:import-log"],
|
|
409
|
+
source="sync_watcher",
|
|
410
|
+
source_ref="auto",
|
|
411
|
+
)
|
|
412
|
+
except Exception as exc:
|
|
413
|
+
logger.debug("Could not log import to memory: %s", exc)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
# Watchdog event handler
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class SeedFileHandler:
|
|
422
|
+
"""Handles file creation events for .seed.json files.
|
|
423
|
+
|
|
424
|
+
Implements debouncing to handle Syncthing's multi-stage file writes.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
callback: Function to call with each new seed file path.
|
|
428
|
+
debounce_ms: Minimum milliseconds between events for the same file.
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
def __init__(self, callback, debounce_ms: int = DEBOUNCE_MS) -> None:
|
|
432
|
+
self._callback = callback
|
|
433
|
+
self._debounce_ms = debounce_ms
|
|
434
|
+
self._last_event: dict[str, float] = {}
|
|
435
|
+
|
|
436
|
+
def on_created(self, event) -> None:
|
|
437
|
+
"""Handle file creation events.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
event: Watchdog FileCreatedEvent (or similar with src_path).
|
|
441
|
+
"""
|
|
442
|
+
if hasattr(event, "is_directory") and event.is_directory:
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
src_path = event.src_path if hasattr(event, "src_path") else str(event)
|
|
446
|
+
if not src_path.endswith(SEED_EXTENSION):
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
# Debounce: Syncthing writes in stages
|
|
450
|
+
now = time.monotonic()
|
|
451
|
+
last = self._last_event.get(src_path, 0)
|
|
452
|
+
if (now - last) * 1000 < self._debounce_ms:
|
|
453
|
+
return
|
|
454
|
+
self._last_event[src_path] = now
|
|
455
|
+
|
|
456
|
+
# Clean up old entries (prevent unbounded growth)
|
|
457
|
+
if len(self._last_event) > 100:
|
|
458
|
+
cutoff = now - 60
|
|
459
|
+
self._last_event = {
|
|
460
|
+
k: v for k, v in self._last_event.items() if v > cutoff
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
logger.debug("Seed file detected: %s", src_path)
|
|
464
|
+
self._callback(Path(src_path))
|
|
465
|
+
|
|
466
|
+
def on_modified(self, event) -> None:
|
|
467
|
+
"""Handle file modification events (Syncthing rewrites).
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
event: Watchdog FileModifiedEvent.
|
|
471
|
+
"""
|
|
472
|
+
# Treat modifications the same as creation for Syncthing compatibility
|
|
473
|
+
self.on_created(event)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class _WatchdogSyncAdapter:
|
|
477
|
+
"""Adapter from watchdog events to SeedFileHandler callback."""
|
|
478
|
+
|
|
479
|
+
def __init__(self, callback) -> None:
|
|
480
|
+
self._handler = SeedFileHandler(callback)
|
|
481
|
+
|
|
482
|
+
def dispatch(self, event) -> None:
|
|
483
|
+
"""Dispatch a watchdog event.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
event: Watchdog event object.
|
|
487
|
+
"""
|
|
488
|
+
etype = getattr(event, "event_type", "")
|
|
489
|
+
if etype in ("created", "modified"):
|
|
490
|
+
self._handler.on_created(event)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# ---------------------------------------------------------------------------
|
|
494
|
+
# SyncWatcher orchestrator
|
|
495
|
+
# ---------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
class SyncWatcher:
|
|
499
|
+
"""Watches the sync inbox for new .seed.json files and auto-imports them.
|
|
500
|
+
|
|
501
|
+
Combines watchdog inotify monitoring (for sub-second response) with
|
|
502
|
+
a periodic polling fallback (for reliability). Tracks already-processed
|
|
503
|
+
files to avoid duplicate imports.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
home: Agent home directory (~/.skcapstone).
|
|
507
|
+
stop_event: Threading event to signal shutdown.
|
|
508
|
+
inbox_path: Override for the inbox directory path.
|
|
509
|
+
processed_log: Override for the processed.json path.
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
def __init__(
|
|
513
|
+
self,
|
|
514
|
+
home: Path,
|
|
515
|
+
stop_event: threading.Event,
|
|
516
|
+
inbox_path: Optional[str] = None,
|
|
517
|
+
processed_log: Optional[str] = None,
|
|
518
|
+
) -> None:
|
|
519
|
+
self._home = home
|
|
520
|
+
self._stop_event = stop_event
|
|
521
|
+
|
|
522
|
+
config = load_sync_config(home)
|
|
523
|
+
if not config.get("auto_import", True):
|
|
524
|
+
self._enabled = False
|
|
525
|
+
logger.info("Sync auto-import disabled by config")
|
|
526
|
+
else:
|
|
527
|
+
self._enabled = True
|
|
528
|
+
|
|
529
|
+
self._inbox = Path(inbox_path or config["inbox_path"]).expanduser()
|
|
530
|
+
self._tracker = ProcessedTracker(
|
|
531
|
+
processed_log or config["processed_log"]
|
|
532
|
+
)
|
|
533
|
+
self._observer = None
|
|
534
|
+
self._lock = threading.Lock()
|
|
535
|
+
|
|
536
|
+
@property
|
|
537
|
+
def enabled(self) -> bool:
|
|
538
|
+
"""Whether auto-import is enabled."""
|
|
539
|
+
return self._enabled
|
|
540
|
+
|
|
541
|
+
def start(self) -> list[threading.Thread]:
|
|
542
|
+
"""Start the watcher and poller threads.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
List of started daemon threads.
|
|
546
|
+
"""
|
|
547
|
+
if not self._enabled:
|
|
548
|
+
return []
|
|
549
|
+
|
|
550
|
+
self._inbox.mkdir(parents=True, exist_ok=True)
|
|
551
|
+
threads: list[threading.Thread] = []
|
|
552
|
+
|
|
553
|
+
# Watchdog inotify thread
|
|
554
|
+
t_watch = threading.Thread(
|
|
555
|
+
target=self._run_watcher,
|
|
556
|
+
name="sync-watcher-inotify",
|
|
557
|
+
daemon=True,
|
|
558
|
+
)
|
|
559
|
+
t_watch.start()
|
|
560
|
+
threads.append(t_watch)
|
|
561
|
+
|
|
562
|
+
# Initial scan on startup
|
|
563
|
+
self._poll_inbox()
|
|
564
|
+
|
|
565
|
+
logger.info(
|
|
566
|
+
"SyncWatcher started — inbox=%s, inotify=yes, poll=%ds",
|
|
567
|
+
self._inbox, POLL_INTERVAL_S,
|
|
568
|
+
)
|
|
569
|
+
return threads
|
|
570
|
+
|
|
571
|
+
def stop(self) -> None:
|
|
572
|
+
"""Stop the watcher gracefully."""
|
|
573
|
+
if self._observer:
|
|
574
|
+
try:
|
|
575
|
+
self._observer.stop()
|
|
576
|
+
self._observer.join(timeout=5)
|
|
577
|
+
except Exception:
|
|
578
|
+
pass
|
|
579
|
+
self._observer = None
|
|
580
|
+
logger.info("SyncWatcher stopped.")
|
|
581
|
+
|
|
582
|
+
def poll_inbox(self) -> int:
|
|
583
|
+
"""Public entry point for scheduled polling.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Number of seeds imported during this poll.
|
|
587
|
+
"""
|
|
588
|
+
return self._poll_inbox()
|
|
589
|
+
|
|
590
|
+
def status(self) -> dict[str, Any]:
|
|
591
|
+
"""Return current watcher status.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
Dict with enabled, inbox_path, observer_alive, and processed count.
|
|
595
|
+
"""
|
|
596
|
+
return {
|
|
597
|
+
"enabled": self._enabled,
|
|
598
|
+
"inbox_path": str(self._inbox),
|
|
599
|
+
"observer_alive": (
|
|
600
|
+
self._observer is not None
|
|
601
|
+
and hasattr(self._observer, "is_alive")
|
|
602
|
+
and self._observer.is_alive()
|
|
603
|
+
),
|
|
604
|
+
"processed_count": len(self._tracker._entries),
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
# ------------------------------------------------------------------
|
|
608
|
+
# Internal
|
|
609
|
+
# ------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
def _run_watcher(self) -> None:
|
|
612
|
+
"""Run the watchdog inotify loop with polling fallback."""
|
|
613
|
+
try:
|
|
614
|
+
from watchdog.observers import Observer
|
|
615
|
+
|
|
616
|
+
adapter = _WatchdogSyncAdapter(self._on_seed_file)
|
|
617
|
+
self._observer = Observer()
|
|
618
|
+
self._observer.schedule(adapter, str(self._inbox), recursive=False)
|
|
619
|
+
self._observer.start()
|
|
620
|
+
logger.info("Sync inotify watcher active on %s", self._inbox)
|
|
621
|
+
|
|
622
|
+
# Block until stop, polling periodically as backup
|
|
623
|
+
while not self._stop_event.is_set():
|
|
624
|
+
self._stop_event.wait(timeout=POLL_INTERVAL_S)
|
|
625
|
+
if not self._stop_event.is_set():
|
|
626
|
+
self._poll_inbox()
|
|
627
|
+
|
|
628
|
+
except ImportError:
|
|
629
|
+
logger.warning(
|
|
630
|
+
"watchdog not installed — falling back to polling only. "
|
|
631
|
+
"Install with: pip install watchdog"
|
|
632
|
+
)
|
|
633
|
+
# Pure polling fallback
|
|
634
|
+
while not self._stop_event.is_set():
|
|
635
|
+
self._poll_inbox()
|
|
636
|
+
self._stop_event.wait(timeout=POLL_INTERVAL_S)
|
|
637
|
+
|
|
638
|
+
except Exception as exc:
|
|
639
|
+
logger.error("Sync watcher error: %s", exc)
|
|
640
|
+
|
|
641
|
+
def _on_seed_file(self, path: Path) -> None:
|
|
642
|
+
"""Handle a detected seed file from inotify.
|
|
643
|
+
|
|
644
|
+
Waits briefly for the file to be fully written, then imports.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
path: Path to the detected .seed.json file.
|
|
648
|
+
"""
|
|
649
|
+
# Brief delay to let Syncthing finish writing
|
|
650
|
+
time.sleep(0.5)
|
|
651
|
+
self._import_seed(path)
|
|
652
|
+
|
|
653
|
+
def _poll_inbox(self) -> int:
|
|
654
|
+
"""Scan the inbox directory for unprocessed seed files.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
Number of seeds imported.
|
|
658
|
+
"""
|
|
659
|
+
if not self._inbox.exists():
|
|
660
|
+
return 0
|
|
661
|
+
|
|
662
|
+
imported = 0
|
|
663
|
+
try:
|
|
664
|
+
for f in sorted(self._inbox.iterdir()):
|
|
665
|
+
if f.name.startswith("."):
|
|
666
|
+
continue
|
|
667
|
+
if not f.name.endswith(SEED_EXTENSION):
|
|
668
|
+
continue
|
|
669
|
+
if self._tracker.is_processed(f.name):
|
|
670
|
+
continue
|
|
671
|
+
if self._import_seed(f):
|
|
672
|
+
imported += 1
|
|
673
|
+
except OSError as exc:
|
|
674
|
+
logger.error("Inbox scan failed: %s", exc)
|
|
675
|
+
|
|
676
|
+
return imported
|
|
677
|
+
|
|
678
|
+
def _import_seed(self, seed_path: Path) -> bool:
|
|
679
|
+
"""Import a single seed file.
|
|
680
|
+
|
|
681
|
+
Thread-safe: uses a lock to prevent concurrent imports of the
|
|
682
|
+
same file from inotify and polling.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
seed_path: Path to the .seed.json file.
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
True if import succeeded, False otherwise.
|
|
689
|
+
"""
|
|
690
|
+
with self._lock:
|
|
691
|
+
if self._tracker.is_processed(seed_path.name):
|
|
692
|
+
return False
|
|
693
|
+
|
|
694
|
+
if not seed_path.exists():
|
|
695
|
+
return False
|
|
696
|
+
|
|
697
|
+
logger.info("Importing seed: %s", seed_path.name)
|
|
698
|
+
|
|
699
|
+
result = import_seed_to_memory(seed_path, self._home)
|
|
700
|
+
if result:
|
|
701
|
+
self._tracker.mark_processed(seed_path.name)
|
|
702
|
+
|
|
703
|
+
# Log the import event to short-term memory
|
|
704
|
+
_log_to_short_term_memory(result, self._home)
|
|
705
|
+
|
|
706
|
+
# Move to archive
|
|
707
|
+
archive = self._inbox.parent / "archive"
|
|
708
|
+
archive.mkdir(exist_ok=True)
|
|
709
|
+
try:
|
|
710
|
+
seed_path.rename(archive / seed_path.name)
|
|
711
|
+
logger.debug("Archived: %s", seed_path.name)
|
|
712
|
+
except OSError as exc:
|
|
713
|
+
logger.warning("Could not archive %s: %s", seed_path.name, exc)
|
|
714
|
+
|
|
715
|
+
return True
|
|
716
|
+
|
|
717
|
+
logger.warning("Seed import returned no result: %s", seed_path.name)
|
|
718
|
+
return False
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# ---------------------------------------------------------------------------
|
|
722
|
+
# Scheduled task factory
|
|
723
|
+
# ---------------------------------------------------------------------------
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def make_sync_inbox_scan_task(
|
|
727
|
+
watcher: Optional[SyncWatcher],
|
|
728
|
+
) -> callable:
|
|
729
|
+
"""Return a callback for the task scheduler to poll the sync inbox.
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
watcher: SyncWatcher instance (or None if disabled).
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
Zero-argument callable suitable for TaskScheduler.register().
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
def _run() -> None:
|
|
739
|
+
if watcher is None:
|
|
740
|
+
return
|
|
741
|
+
count = watcher.poll_inbox()
|
|
742
|
+
if count:
|
|
743
|
+
logger.info("Scheduled sync scan imported %d seed(s)", count)
|
|
744
|
+
|
|
745
|
+
return _run
|