@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,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session auto-capture — the agent never forgets a conversation.
|
|
3
|
+
|
|
4
|
+
Extracts key moments from AI conversations, scores importance by
|
|
5
|
+
topic novelty and information density, and stores each as a tagged
|
|
6
|
+
memory. Tool-agnostic: works with Claude Code, Cursor, Windsurf,
|
|
7
|
+
or any tool that can pass conversation text.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
# Via CLI
|
|
11
|
+
skcapstone session capture "We decided to use Ed25519 for all agent keys"
|
|
12
|
+
skcapstone session capture --file transcript.txt
|
|
13
|
+
echo "discussion notes" | skcapstone session capture --stdin
|
|
14
|
+
|
|
15
|
+
# Via MCP tool
|
|
16
|
+
session_capture(content="...", tags=["architecture"])
|
|
17
|
+
|
|
18
|
+
# Via Python
|
|
19
|
+
from skcapstone.session_capture import SessionCapture
|
|
20
|
+
cap = SessionCapture(home)
|
|
21
|
+
cap.capture("We decided to use Ed25519...")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import hashlib
|
|
27
|
+
import re
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
from .memory_engine import search, store
|
|
34
|
+
from .models import MemoryEntry
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class CapturedMoment:
|
|
39
|
+
"""A single extracted moment from a conversation.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
content: The distilled text of the moment.
|
|
43
|
+
importance: Auto-scored importance 0.0-1.0.
|
|
44
|
+
tags: Auto-generated tags from content analysis.
|
|
45
|
+
reason: Why this moment was scored as it was.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
content: str
|
|
49
|
+
importance: float = 0.5
|
|
50
|
+
tags: list[str] = field(default_factory=list)
|
|
51
|
+
reason: str = ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Patterns that signal high-importance content
|
|
55
|
+
_HIGH_SIGNAL_PATTERNS: list[tuple[re.Pattern, float, str]] = [
|
|
56
|
+
(re.compile(r"\bdecid", re.I), 0.2, "decision"),
|
|
57
|
+
(re.compile(r"\bchose|\bpicked|\bselect", re.I), 0.15, "decision"),
|
|
58
|
+
(re.compile(r"\barchitecture|\bdesign\b|\bpattern", re.I), 0.15, "architecture"),
|
|
59
|
+
(re.compile(r"\bbug\b|\bfix\b|\bissue\b|\berror\b", re.I), 0.1, "bugfix"),
|
|
60
|
+
(re.compile(r"\bsecur|\bencrypt|\bPGP\b|\bGPG\b|\bkey\b", re.I), 0.15, "security"),
|
|
61
|
+
(re.compile(r"\bAPI\b|\bendpoint|\bschema\b", re.I), 0.1, "api"),
|
|
62
|
+
(re.compile(r"\bdeploy|\brelease|\bpublish", re.I), 0.1, "deployment"),
|
|
63
|
+
(re.compile(r"\bTODO\b|\bFIXME\b|\bHACK\b", re.I), 0.1, "todo"),
|
|
64
|
+
(re.compile(r"\bimportant|\bcritical|\bmust\b|\brequir", re.I), 0.15, "priority"),
|
|
65
|
+
(re.compile(r"\bnever\b|\balways\b|\brule\b|\bconvention", re.I), 0.1, "convention"),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# Patterns for auto-tagging
|
|
69
|
+
_TAG_PATTERNS: list[tuple[re.Pattern, str]] = [
|
|
70
|
+
(re.compile(r"\bcapauth\b", re.I), "capauth"),
|
|
71
|
+
(re.compile(r"\bskcapstone\b", re.I), "skcapstone"),
|
|
72
|
+
(re.compile(r"\bskmemory\b", re.I), "skmemory"),
|
|
73
|
+
(re.compile(r"\bskcomm\b", re.I), "skcomm"),
|
|
74
|
+
(re.compile(r"\bskchat\b", re.I), "skchat"),
|
|
75
|
+
(re.compile(r"\bsyncthing\b", re.I), "syncthing"),
|
|
76
|
+
(re.compile(r"\bMCP\b", re.I), "mcp"),
|
|
77
|
+
(re.compile(r"\bPGP\b|\bGPG\b", re.I), "pgp"),
|
|
78
|
+
(re.compile(r"\bDocker\b", re.I), "docker"),
|
|
79
|
+
(re.compile(r"\bPython\b", re.I), "python"),
|
|
80
|
+
(re.compile(r"\btest\b", re.I), "testing"),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
_SENTENCE_SPLITTER = re.compile(r"(?<=[.!?])\s+|\n\n+|\n(?=[A-Z#\-\*])")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SessionCapture:
|
|
87
|
+
"""Captures AI conversation content as sovereign memories.
|
|
88
|
+
|
|
89
|
+
Extracts key moments, auto-scores importance, deduplicates
|
|
90
|
+
against existing memories, and stores to the agent's memory.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
home: Agent home directory (~/.skcapstone).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, home: Path) -> None:
|
|
97
|
+
self.home = home
|
|
98
|
+
|
|
99
|
+
def capture(
|
|
100
|
+
self,
|
|
101
|
+
content: str,
|
|
102
|
+
tags: Optional[list[str]] = None,
|
|
103
|
+
source: str = "session",
|
|
104
|
+
min_importance: float = 0.3,
|
|
105
|
+
) -> list[MemoryEntry]:
|
|
106
|
+
"""Capture conversation content as memories.
|
|
107
|
+
|
|
108
|
+
Splits content into moments, scores each, deduplicates,
|
|
109
|
+
and stores those above the minimum importance threshold.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
content: Raw conversation text (any length).
|
|
113
|
+
tags: Additional tags to apply to all captured memories.
|
|
114
|
+
source: Memory source identifier.
|
|
115
|
+
min_importance: Minimum importance to store (0.0-1.0).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
List of stored MemoryEntry objects.
|
|
119
|
+
"""
|
|
120
|
+
moments = self.extract_moments(content)
|
|
121
|
+
scored = [self.score_moment(m) for m in moments]
|
|
122
|
+
filtered = [m for m in scored if m.importance >= min_importance]
|
|
123
|
+
deduped = self._deduplicate(filtered)
|
|
124
|
+
|
|
125
|
+
extra_tags = tags or []
|
|
126
|
+
stored: list[MemoryEntry] = []
|
|
127
|
+
|
|
128
|
+
for moment in deduped:
|
|
129
|
+
all_tags = list(set(["session-capture"] + moment.tags + extra_tags))
|
|
130
|
+
entry = store(
|
|
131
|
+
home=self.home,
|
|
132
|
+
content=moment.content,
|
|
133
|
+
tags=all_tags,
|
|
134
|
+
source=source,
|
|
135
|
+
importance=moment.importance,
|
|
136
|
+
metadata={"capture_reason": moment.reason},
|
|
137
|
+
)
|
|
138
|
+
stored.append(entry)
|
|
139
|
+
|
|
140
|
+
return stored
|
|
141
|
+
|
|
142
|
+
def extract_moments(self, content: str) -> list[str]:
|
|
143
|
+
"""Split conversation content into distinct moments.
|
|
144
|
+
|
|
145
|
+
A moment is a meaningful unit: a paragraph, a decision,
|
|
146
|
+
a key statement. Short fragments are merged with neighbors.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
content: Raw text to split.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of moment strings.
|
|
153
|
+
"""
|
|
154
|
+
content = content.strip()
|
|
155
|
+
if not content:
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
segments = _SENTENCE_SPLITTER.split(content)
|
|
159
|
+
moments: list[str] = []
|
|
160
|
+
buffer = ""
|
|
161
|
+
|
|
162
|
+
for seg in segments:
|
|
163
|
+
seg = seg.strip()
|
|
164
|
+
if not seg:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
if len(buffer) + len(seg) < 60:
|
|
168
|
+
buffer = f"{buffer} {seg}".strip() if buffer else seg
|
|
169
|
+
else:
|
|
170
|
+
if buffer:
|
|
171
|
+
moments.append(buffer)
|
|
172
|
+
buffer = seg
|
|
173
|
+
|
|
174
|
+
if buffer:
|
|
175
|
+
moments.append(buffer)
|
|
176
|
+
|
|
177
|
+
return [m for m in moments if len(m) >= 20]
|
|
178
|
+
|
|
179
|
+
def score_moment(self, text: str) -> CapturedMoment:
|
|
180
|
+
"""Score a moment's importance and auto-tag it.
|
|
181
|
+
|
|
182
|
+
Scoring is based on signal patterns (decisions, architecture,
|
|
183
|
+
security mentions, etc.) and content density (longer, more
|
|
184
|
+
specific content scores higher).
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
text: A single moment string.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
CapturedMoment with importance score and tags.
|
|
191
|
+
"""
|
|
192
|
+
base_score = 0.3
|
|
193
|
+
reasons: list[str] = []
|
|
194
|
+
tags: list[str] = []
|
|
195
|
+
|
|
196
|
+
for pattern, boost, label in _HIGH_SIGNAL_PATTERNS:
|
|
197
|
+
if pattern.search(text):
|
|
198
|
+
base_score += boost
|
|
199
|
+
reasons.append(label)
|
|
200
|
+
|
|
201
|
+
for pattern, tag in _TAG_PATTERNS:
|
|
202
|
+
if pattern.search(text):
|
|
203
|
+
tags.append(tag)
|
|
204
|
+
|
|
205
|
+
# Reason: longer, denser content tends to be more informative
|
|
206
|
+
word_count = len(text.split())
|
|
207
|
+
if word_count > 30:
|
|
208
|
+
base_score += 0.05
|
|
209
|
+
if word_count > 60:
|
|
210
|
+
base_score += 0.05
|
|
211
|
+
|
|
212
|
+
importance = min(1.0, base_score)
|
|
213
|
+
reason = ", ".join(reasons) if reasons else "general"
|
|
214
|
+
|
|
215
|
+
return CapturedMoment(
|
|
216
|
+
content=text,
|
|
217
|
+
importance=round(importance, 2),
|
|
218
|
+
tags=tags,
|
|
219
|
+
reason=reason,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def _deduplicate(self, moments: list[CapturedMoment]) -> list[CapturedMoment]:
|
|
223
|
+
"""Remove moments that are too similar to existing memories.
|
|
224
|
+
|
|
225
|
+
Uses content hashing for exact dedup and search overlap
|
|
226
|
+
for semantic-ish dedup.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
moments: Scored moments to deduplicate.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Deduplicated list of moments.
|
|
233
|
+
"""
|
|
234
|
+
seen_hashes: set[str] = set()
|
|
235
|
+
unique: list[CapturedMoment] = []
|
|
236
|
+
|
|
237
|
+
for m in moments:
|
|
238
|
+
h = hashlib.md5(m.content.lower().encode()).hexdigest()[:12]
|
|
239
|
+
if h in seen_hashes:
|
|
240
|
+
continue
|
|
241
|
+
seen_hashes.add(h)
|
|
242
|
+
|
|
243
|
+
existing = search(self.home, m.content[:50], limit=1)
|
|
244
|
+
if existing and _text_overlap(m.content, existing[0].content) > 0.7:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
unique.append(m)
|
|
248
|
+
|
|
249
|
+
return unique
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _text_overlap(a: str, b: str) -> float:
|
|
253
|
+
"""Compute word-level Jaccard overlap between two strings.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
a: First string.
|
|
257
|
+
b: Second string.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Overlap ratio 0.0-1.0.
|
|
261
|
+
"""
|
|
262
|
+
words_a = set(a.lower().split())
|
|
263
|
+
words_b = set(b.lower().split())
|
|
264
|
+
if not words_a or not words_b:
|
|
265
|
+
return 0.0
|
|
266
|
+
intersection = words_a & words_b
|
|
267
|
+
union = words_a | words_b
|
|
268
|
+
return len(intersection) / len(union)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SKCapstone Session Recorder — capture MCP tool calls + responses as JSONL.
|
|
3
|
+
|
|
4
|
+
Each MCP session is auto-saved to ~/.skcapstone/sessions/ and rotated to
|
|
5
|
+
keep the last 5. An explicit output path can be set via SKCAPSTONE_RECORD_FILE
|
|
6
|
+
or the ``--output`` flag on ``skcapstone record``.
|
|
7
|
+
|
|
8
|
+
JSONL line schema::
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
"ts": "2026-03-02T10:00:00.123456+00:00", # ISO-8601 UTC
|
|
12
|
+
"tool": "memory_store",
|
|
13
|
+
"arguments": {"content": "...", "tags": [...]},
|
|
14
|
+
"result": [{"type": "text", "text": "..."}],
|
|
15
|
+
"duration_ms": 45
|
|
16
|
+
}
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
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.session_recorder")
|
|
30
|
+
|
|
31
|
+
_SESSIONS_KEEP = 5
|
|
32
|
+
_SESSIONS_SUBDIR = "sessions"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _sessions_dir(home: Path) -> Path:
|
|
36
|
+
"""Return the sessions directory, creating it if absent."""
|
|
37
|
+
d = home / _SESSIONS_SUBDIR
|
|
38
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return d
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _auto_rotate(sessions_dir: Path, keep: int = _SESSIONS_KEEP) -> None:
|
|
43
|
+
"""Delete old auto-session files to keep only the most recent *keep*."""
|
|
44
|
+
auto_files = sorted(
|
|
45
|
+
sessions_dir.glob("session-*.jsonl"),
|
|
46
|
+
key=lambda p: p.stat().st_mtime,
|
|
47
|
+
)
|
|
48
|
+
for old in auto_files[: max(0, len(auto_files) - keep)]:
|
|
49
|
+
try:
|
|
50
|
+
old.unlink()
|
|
51
|
+
logger.debug("Rotated old session: %s", old)
|
|
52
|
+
except OSError:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SessionRecorder:
|
|
57
|
+
"""Records MCP tool calls and responses to one or two JSONL sinks.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
home: Agent home directory (``~/.skcapstone`` or agent-specific).
|
|
61
|
+
output_path: Optional explicit output file. If *None* only the
|
|
62
|
+
auto-session file is written.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, home: Path, output_path: Optional[Path] = None) -> None:
|
|
66
|
+
self._home = home
|
|
67
|
+
self._output_path = output_path
|
|
68
|
+
self._auto_path: Optional[Path] = None
|
|
69
|
+
self._auto_fh = None
|
|
70
|
+
self._output_fh = None
|
|
71
|
+
self._count = 0
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
# Lifecycle
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def start_session(
|
|
79
|
+
cls,
|
|
80
|
+
home: Path,
|
|
81
|
+
output_path: Optional[Path] = None,
|
|
82
|
+
) -> "SessionRecorder":
|
|
83
|
+
"""Factory: open files and return a ready recorder.
|
|
84
|
+
|
|
85
|
+
Checks ``SKCAPSTONE_RECORD_FILE`` env var when *output_path* is None.
|
|
86
|
+
"""
|
|
87
|
+
env_path = os.environ.get("SKCAPSTONE_RECORD_FILE")
|
|
88
|
+
if output_path is None and env_path:
|
|
89
|
+
output_path = Path(env_path).expanduser()
|
|
90
|
+
|
|
91
|
+
rec = cls(home, output_path)
|
|
92
|
+
rec._open()
|
|
93
|
+
return rec
|
|
94
|
+
|
|
95
|
+
def _open(self) -> None:
|
|
96
|
+
sessions_dir = _sessions_dir(self._home)
|
|
97
|
+
now = datetime.now(timezone.utc)
|
|
98
|
+
# Include microseconds + PID so rapid test runs produce distinct filenames.
|
|
99
|
+
ts = now.strftime("%Y%m%dT%H%M%S") + f"-{now.microsecond:06d}-{os.getpid()}"
|
|
100
|
+
self._auto_path = sessions_dir / f"session-{ts}.jsonl"
|
|
101
|
+
self._auto_fh = open(self._auto_path, "w", encoding="utf-8") # noqa: WPS515
|
|
102
|
+
logger.debug("Session recorder: auto-save → %s", self._auto_path)
|
|
103
|
+
|
|
104
|
+
if self._output_path:
|
|
105
|
+
self._output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
self._output_fh = open(self._output_path, "w", encoding="utf-8") # noqa: WPS515
|
|
107
|
+
logger.debug("Session recorder: output → %s", self._output_path)
|
|
108
|
+
|
|
109
|
+
def close(self) -> None:
|
|
110
|
+
"""Flush, close, and rotate old session files."""
|
|
111
|
+
for fh in (self._auto_fh, self._output_fh):
|
|
112
|
+
if fh:
|
|
113
|
+
try:
|
|
114
|
+
fh.flush()
|
|
115
|
+
fh.close()
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
118
|
+
self._auto_fh = None
|
|
119
|
+
self._output_fh = None
|
|
120
|
+
|
|
121
|
+
if self._auto_path:
|
|
122
|
+
_auto_rotate(_sessions_dir(self._home), keep=_SESSIONS_KEEP)
|
|
123
|
+
logger.info(
|
|
124
|
+
"Session recorder closed: %d tool call(s) recorded", self._count
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
# Recording
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
def record(
|
|
132
|
+
self,
|
|
133
|
+
tool: str,
|
|
134
|
+
arguments: dict[str, Any],
|
|
135
|
+
result: list[Any],
|
|
136
|
+
duration_ms: int,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Append one JSONL line to all open sinks."""
|
|
139
|
+
entry = {
|
|
140
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
141
|
+
"tool": tool,
|
|
142
|
+
"arguments": arguments,
|
|
143
|
+
"result": _serialise_result(result),
|
|
144
|
+
"duration_ms": duration_ms,
|
|
145
|
+
}
|
|
146
|
+
line = json.dumps(entry, ensure_ascii=False) + "\n"
|
|
147
|
+
for fh in (self._auto_fh, self._output_fh):
|
|
148
|
+
if fh:
|
|
149
|
+
fh.write(line)
|
|
150
|
+
fh.flush()
|
|
151
|
+
self._count += 1
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
# Convenience
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def auto_path(self) -> Optional[Path]:
|
|
159
|
+
"""Path to the auto-session file (set after _open())."""
|
|
160
|
+
return self._auto_path
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def count(self) -> int:
|
|
164
|
+
"""Number of tool calls recorded so far."""
|
|
165
|
+
return self._count
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Helpers
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _serialise_result(result: Any) -> Any:
|
|
174
|
+
"""Convert MCP TextContent objects to plain dicts for JSON serialisation."""
|
|
175
|
+
if isinstance(result, list):
|
|
176
|
+
return [_serialise_result(r) for r in result]
|
|
177
|
+
if hasattr(result, "model_dump"):
|
|
178
|
+
return result.model_dump()
|
|
179
|
+
if hasattr(result, "__dict__"):
|
|
180
|
+
return vars(result)
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# Listing helpers (used by CLI)
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def list_sessions(home: Path) -> list[Path]:
|
|
190
|
+
"""Return session files newest-first."""
|
|
191
|
+
sessions_dir = _sessions_dir(home)
|
|
192
|
+
return sorted(
|
|
193
|
+
sessions_dir.glob("session-*.jsonl"),
|
|
194
|
+
key=lambda p: p.stat().st_mtime,
|
|
195
|
+
reverse=True,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def load_session(path: Path) -> list[dict[str, Any]]:
|
|
200
|
+
"""Parse a JSONL session file into a list of entries."""
|
|
201
|
+
entries: list[dict[str, Any]] = []
|
|
202
|
+
with path.open(encoding="utf-8") as fh:
|
|
203
|
+
for line in fh:
|
|
204
|
+
line = line.strip()
|
|
205
|
+
if line:
|
|
206
|
+
try:
|
|
207
|
+
entries.append(json.loads(line))
|
|
208
|
+
except json.JSONDecodeError as exc:
|
|
209
|
+
logger.warning("Skipping malformed JSONL line: %s", exc)
|
|
210
|
+
return entries
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SKCapstone Session Replayer — play back a recorded JSONL session.
|
|
3
|
+
|
|
4
|
+
Two modes:
|
|
5
|
+
|
|
6
|
+
``--dry-run`` (default in tests)
|
|
7
|
+
Iterates entries and prints what *would* be called. No handlers executed.
|
|
8
|
+
|
|
9
|
+
Live mode
|
|
10
|
+
Calls the real MCP tool handlers directly (no MCP transport required).
|
|
11
|
+
Useful for regression testing, debugging, and auditing.
|
|
12
|
+
|
|
13
|
+
Each replayed entry produces a ``ReplayResult``::
|
|
14
|
+
|
|
15
|
+
ReplayResult(
|
|
16
|
+
index=0,
|
|
17
|
+
tool="memory_store",
|
|
18
|
+
arguments={...},
|
|
19
|
+
recorded_result=[...], # what the original call returned
|
|
20
|
+
replayed_result=[...], # what the live replay returned (None in dry-run)
|
|
21
|
+
duration_ms=12,
|
|
22
|
+
match=True, # True if text content matches (live only)
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
import json
|
|
30
|
+
import logging
|
|
31
|
+
import time
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Generator, Optional
|
|
35
|
+
|
|
36
|
+
from .session_recorder import load_session
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("skcapstone.session_replayer")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ReplayResult:
|
|
43
|
+
index: int
|
|
44
|
+
tool: str
|
|
45
|
+
arguments: dict[str, Any]
|
|
46
|
+
recorded_result: list[Any]
|
|
47
|
+
replayed_result: Optional[list[Any]]
|
|
48
|
+
duration_ms: int
|
|
49
|
+
match: Optional[bool] # None in dry-run; True/False in live mode
|
|
50
|
+
error: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SessionReplayer:
|
|
54
|
+
"""Replay a recorded JSONL session file.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
path: Path to the ``.jsonl`` session file.
|
|
58
|
+
dry_run: If *True*, skip actual handler invocation.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, path: Path, dry_run: bool = False) -> None:
|
|
62
|
+
self._path = path
|
|
63
|
+
self._dry_run = dry_run
|
|
64
|
+
self._handlers: Optional[dict] = None
|
|
65
|
+
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
# Public interface
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def replay(self) -> Generator[ReplayResult, None, None]:
|
|
71
|
+
"""Yield a :class:`ReplayResult` for each recorded tool call."""
|
|
72
|
+
entries = load_session(self._path)
|
|
73
|
+
if not entries:
|
|
74
|
+
logger.warning("Session file is empty: %s", self._path)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
if not self._dry_run:
|
|
78
|
+
self._handlers = _load_handlers()
|
|
79
|
+
|
|
80
|
+
for idx, entry in enumerate(entries):
|
|
81
|
+
yield self._replay_entry(idx, entry)
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Internal
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def _replay_entry(self, idx: int, entry: dict[str, Any]) -> ReplayResult:
|
|
88
|
+
tool = entry.get("tool", "<unknown>")
|
|
89
|
+
arguments = entry.get("arguments", {})
|
|
90
|
+
recorded = entry.get("result", [])
|
|
91
|
+
orig_ms = entry.get("duration_ms", 0)
|
|
92
|
+
|
|
93
|
+
if self._dry_run:
|
|
94
|
+
return ReplayResult(
|
|
95
|
+
index=idx,
|
|
96
|
+
tool=tool,
|
|
97
|
+
arguments=arguments,
|
|
98
|
+
recorded_result=recorded,
|
|
99
|
+
replayed_result=None,
|
|
100
|
+
duration_ms=0,
|
|
101
|
+
match=None,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Live replay
|
|
105
|
+
replayed: Optional[list[Any]] = None
|
|
106
|
+
error: Optional[str] = None
|
|
107
|
+
t0 = time.monotonic()
|
|
108
|
+
try:
|
|
109
|
+
replayed = _call_handler(self._handlers, tool, arguments)
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
error = str(exc)
|
|
112
|
+
logger.warning("Replay error on tool '%s': %s", tool, exc)
|
|
113
|
+
elapsed = int((time.monotonic() - t0) * 1000)
|
|
114
|
+
|
|
115
|
+
match: Optional[bool] = None
|
|
116
|
+
if replayed is not None:
|
|
117
|
+
match = _results_match(recorded, replayed)
|
|
118
|
+
|
|
119
|
+
return ReplayResult(
|
|
120
|
+
index=idx,
|
|
121
|
+
tool=tool,
|
|
122
|
+
arguments=arguments,
|
|
123
|
+
recorded_result=recorded,
|
|
124
|
+
replayed_result=replayed,
|
|
125
|
+
duration_ms=elapsed,
|
|
126
|
+
match=match,
|
|
127
|
+
error=error,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Mock MCP server for dry-run
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
class MockMCPServer:
|
|
136
|
+
"""Minimal mock that accepts tool calls and returns recorded results.
|
|
137
|
+
|
|
138
|
+
Used internally when you want to pipe replay output back through an
|
|
139
|
+
MCP-compatible interface without running a real server.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, session_path: Path) -> None:
|
|
143
|
+
self._entries = load_session(session_path)
|
|
144
|
+
self._index = 0
|
|
145
|
+
|
|
146
|
+
def call(self, tool: str, arguments: dict[str, Any]) -> Optional[list[Any]]:
|
|
147
|
+
"""Return the next recorded result that matches *tool*, or None."""
|
|
148
|
+
for entry in self._entries[self._index:]:
|
|
149
|
+
self._index += 1
|
|
150
|
+
if entry.get("tool") == tool:
|
|
151
|
+
return entry.get("result")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# Helpers
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _load_handlers() -> dict:
|
|
161
|
+
"""Import and return the live MCP handler table."""
|
|
162
|
+
from .mcp_tools import collect_all_handlers
|
|
163
|
+
return collect_all_handlers()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _call_handler(handlers: Optional[dict], tool: str, arguments: dict) -> list[Any]:
|
|
167
|
+
"""Invoke a handler synchronously (wraps the async call)."""
|
|
168
|
+
if not handlers:
|
|
169
|
+
raise RuntimeError("Handler table not loaded")
|
|
170
|
+
handler = handlers.get(tool)
|
|
171
|
+
if handler is None:
|
|
172
|
+
raise ValueError(f"No handler registered for tool '{tool}'")
|
|
173
|
+
return asyncio.run(handler(arguments))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _results_match(recorded: list[Any], replayed: list[Any]) -> bool:
|
|
177
|
+
"""Compare two result lists by their serialised text content."""
|
|
178
|
+
def _texts(items: list[Any]) -> list[str]:
|
|
179
|
+
texts = []
|
|
180
|
+
for item in items:
|
|
181
|
+
if isinstance(item, dict):
|
|
182
|
+
texts.append(item.get("text", ""))
|
|
183
|
+
elif hasattr(item, "text"):
|
|
184
|
+
texts.append(item.text)
|
|
185
|
+
else:
|
|
186
|
+
texts.append(str(item))
|
|
187
|
+
return texts
|
|
188
|
+
|
|
189
|
+
return _texts(recorded) == _texts(replayed)
|