@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,694 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Uninstall wizard — clean, complete removal of a sovereign node.
|
|
3
|
+
|
|
4
|
+
Steps:
|
|
5
|
+
1. Confirm the user actually wants to do this (multiple confirmations)
|
|
6
|
+
2. Offer to transfer vault data to another node before wiping
|
|
7
|
+
3. Deregister this device from the vault registry
|
|
8
|
+
4. Disable Tailscale Funnel and log out
|
|
9
|
+
5. Remove Syncthing shared folder config
|
|
10
|
+
6. Delete all local data (~/.skcapstone, vaults, config)
|
|
11
|
+
7. Optionally uninstall pip packages
|
|
12
|
+
|
|
13
|
+
The registry update propagates via Syncthing so other devices
|
|
14
|
+
stop trying to reach this node. The data transfer option copies
|
|
15
|
+
vault files to another device's shared folder or a cloud backend
|
|
16
|
+
before the local wipe.
|
|
17
|
+
|
|
18
|
+
Safety: requires typing "DELETE" to confirm. No accidental wipes.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import shutil
|
|
25
|
+
import socket
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
import click
|
|
32
|
+
from rich.console import Console
|
|
33
|
+
from rich.panel import Panel
|
|
34
|
+
from rich.table import Table
|
|
35
|
+
|
|
36
|
+
from . import AGENT_HOME
|
|
37
|
+
|
|
38
|
+
console = Console()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Inventory — what will be deleted
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def _build_inventory(home_path: Path) -> dict:
|
|
46
|
+
"""Scan what exists on this machine and build a deletion inventory.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
home_path: Agent home directory.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dict with keys: dirs, files, total_size_bytes, vaults, has_tailscale,
|
|
53
|
+
has_syncthing, has_registry.
|
|
54
|
+
"""
|
|
55
|
+
inventory: dict = {
|
|
56
|
+
"dirs": [],
|
|
57
|
+
"total_size_bytes": 0,
|
|
58
|
+
"vault_names": [],
|
|
59
|
+
"has_tailscale": False,
|
|
60
|
+
"has_syncthing": False,
|
|
61
|
+
"has_registry": False,
|
|
62
|
+
"has_auth_key": False,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if home_path.exists():
|
|
66
|
+
inventory["dirs"].append(str(home_path))
|
|
67
|
+
inventory["total_size_bytes"] = _dir_size(home_path)
|
|
68
|
+
|
|
69
|
+
vaults_dir = home_path / "vaults"
|
|
70
|
+
if vaults_dir.exists():
|
|
71
|
+
inventory["vault_names"] = [
|
|
72
|
+
d.name for d in vaults_dir.iterdir() if d.is_dir()
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
sync_dir = home_path / "sync"
|
|
76
|
+
registry_file = sync_dir / "vault-registry.json"
|
|
77
|
+
auth_key_file = sync_dir / "tailscale.key.gpg"
|
|
78
|
+
|
|
79
|
+
inventory["has_registry"] = registry_file.exists()
|
|
80
|
+
inventory["has_auth_key"] = auth_key_file.exists()
|
|
81
|
+
inventory["has_tailscale"] = shutil.which("tailscale") is not None
|
|
82
|
+
inventory["has_syncthing"] = shutil.which("syncthing") is not None
|
|
83
|
+
|
|
84
|
+
# Check for vaults.yaml
|
|
85
|
+
vaults_yaml = home_path / "vaults.yaml"
|
|
86
|
+
if not vaults_yaml.exists():
|
|
87
|
+
vaults_yaml = home_path.parent / "vaults.yaml"
|
|
88
|
+
|
|
89
|
+
return inventory
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _dir_size(path: Path) -> int:
|
|
93
|
+
"""Calculate total size of a directory in bytes."""
|
|
94
|
+
total = 0
|
|
95
|
+
try:
|
|
96
|
+
for entry in path.rglob("*"):
|
|
97
|
+
if entry.is_file():
|
|
98
|
+
total += entry.stat().st_size
|
|
99
|
+
except (OSError, PermissionError):
|
|
100
|
+
pass
|
|
101
|
+
return total
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _human_size(n: int) -> str:
|
|
105
|
+
"""Format bytes as human-readable size."""
|
|
106
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
107
|
+
if n < 1024:
|
|
108
|
+
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
|
|
109
|
+
n /= 1024
|
|
110
|
+
return f"{n:.1f} TB"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Data transfer
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def _offer_data_transfer(home_path: Path, inventory: dict) -> None:
|
|
118
|
+
"""Offer to transfer vault data to another location before wiping.
|
|
119
|
+
|
|
120
|
+
Options:
|
|
121
|
+
a) Copy to another local path (USB drive, external disk)
|
|
122
|
+
b) Copy to another device's sync folder
|
|
123
|
+
c) Skip (just delete)
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
home_path: Agent home directory.
|
|
127
|
+
inventory: Inventory dict from _build_inventory.
|
|
128
|
+
"""
|
|
129
|
+
vault_names = inventory["vault_names"]
|
|
130
|
+
if not vault_names:
|
|
131
|
+
console.print(" [dim]No vault data found — nothing to transfer.[/]")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
vaults_dir = home_path / "vaults"
|
|
135
|
+
vault_size = _dir_size(vaults_dir) if vaults_dir.exists() else 0
|
|
136
|
+
|
|
137
|
+
console.print()
|
|
138
|
+
console.print(
|
|
139
|
+
Panel(
|
|
140
|
+
"[bold]Transfer your data before deleting?[/]\n\n"
|
|
141
|
+
f"You have [bold]{len(vault_names)}[/] vault(s) "
|
|
142
|
+
f"({_human_size(vault_size)}) on this machine:\n"
|
|
143
|
+
+ "".join(f" - {name}\n" for name in vault_names)
|
|
144
|
+
+ "\n"
|
|
145
|
+
"You can copy them somewhere safe before wiping.\n"
|
|
146
|
+
"The encrypted files can be restored on any other device.",
|
|
147
|
+
title="Data Transfer",
|
|
148
|
+
border_style="yellow",
|
|
149
|
+
padding=(1, 3),
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
console.print()
|
|
153
|
+
|
|
154
|
+
console.print(" [bold]What would you like to do?[/]\n")
|
|
155
|
+
console.print(" [cyan]1[/] Copy vault data to another folder")
|
|
156
|
+
console.print(" (USB drive, external disk, network share)")
|
|
157
|
+
console.print()
|
|
158
|
+
console.print(" [cyan]2[/] Copy vault data to another device on your network")
|
|
159
|
+
console.print(" (copies to that device's sync folder)")
|
|
160
|
+
console.print()
|
|
161
|
+
console.print(" [cyan]3[/] Skip — just delete everything")
|
|
162
|
+
console.print(" [dim](data is gone forever)[/]")
|
|
163
|
+
console.print()
|
|
164
|
+
|
|
165
|
+
choice = click.prompt(
|
|
166
|
+
" Your choice",
|
|
167
|
+
type=click.IntRange(1, 3),
|
|
168
|
+
default=3,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if choice == 1:
|
|
172
|
+
_transfer_to_local(vaults_dir, vault_names)
|
|
173
|
+
elif choice == 2:
|
|
174
|
+
_transfer_to_device(vaults_dir, vault_names, home_path)
|
|
175
|
+
else:
|
|
176
|
+
console.print(" [dim]Skipping data transfer.[/]")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _transfer_to_local(vaults_dir: Path, vault_names: list[str]) -> None:
|
|
180
|
+
"""Copy vault data to a user-specified local path.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
vaults_dir: Source vaults directory.
|
|
184
|
+
vault_names: List of vault names to copy.
|
|
185
|
+
"""
|
|
186
|
+
dest = click.prompt(
|
|
187
|
+
" Destination folder (e.g. /media/usb/backup or D:\\backup)",
|
|
188
|
+
type=click.Path(),
|
|
189
|
+
)
|
|
190
|
+
dest_path = Path(dest).expanduser()
|
|
191
|
+
|
|
192
|
+
if not dest_path.exists():
|
|
193
|
+
console.print(f" [dim]Creating {dest_path}...[/]")
|
|
194
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
|
|
196
|
+
for name in vault_names:
|
|
197
|
+
src = vaults_dir / name
|
|
198
|
+
dst = dest_path / name
|
|
199
|
+
if src.exists():
|
|
200
|
+
console.print(f" Copying [cyan]{name}[/]...", end=" ")
|
|
201
|
+
try:
|
|
202
|
+
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
203
|
+
console.print("[green]done[/]")
|
|
204
|
+
except (OSError, shutil.Error) as exc:
|
|
205
|
+
console.print(f"[red]failed: {exc}[/]")
|
|
206
|
+
|
|
207
|
+
console.print()
|
|
208
|
+
console.print(f" [green]Data saved to:[/] {dest_path}")
|
|
209
|
+
console.print(" [dim]You can restore these vaults on another device with:[/]")
|
|
210
|
+
console.print(f" [cyan] cp -r {dest_path}/* ~/.skcapstone/vaults/[/]")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _transfer_to_device(
|
|
214
|
+
vaults_dir: Path,
|
|
215
|
+
vault_names: list[str],
|
|
216
|
+
home_path: Path,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Copy vault data to another device on the network.
|
|
219
|
+
|
|
220
|
+
Uses the vault registry to find other devices, then copies
|
|
221
|
+
vault data to their Syncthing-shared folder.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
vaults_dir: Source vaults directory.
|
|
225
|
+
vault_names: Vault names to transfer.
|
|
226
|
+
home_path: Agent home.
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
from skref.registry import load_registry
|
|
230
|
+
from skref.config import load_config
|
|
231
|
+
|
|
232
|
+
config = load_config()
|
|
233
|
+
registry = load_registry()
|
|
234
|
+
hostname = socket.gethostname()
|
|
235
|
+
other_devices = [
|
|
236
|
+
d for name, d in registry.get("devices", {}).items()
|
|
237
|
+
if name != hostname and d.get("is_datastore")
|
|
238
|
+
]
|
|
239
|
+
except Exception:
|
|
240
|
+
other_devices = []
|
|
241
|
+
|
|
242
|
+
if not other_devices:
|
|
243
|
+
console.print(" [yellow]No other datastore devices found on your network.[/]")
|
|
244
|
+
console.print(" [dim]Use option 1 to copy to a local folder instead.[/]")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
console.print(" [bold]Available devices:[/]")
|
|
248
|
+
for i, dev in enumerate(other_devices, 1):
|
|
249
|
+
fqdn = dev.get("tailscale_fqdn", "")
|
|
250
|
+
ip = dev.get("tailscale_ip", "")
|
|
251
|
+
label = fqdn or ip or dev["hostname"]
|
|
252
|
+
console.print(f" [cyan]{i}[/] {dev['hostname']} ({label})")
|
|
253
|
+
|
|
254
|
+
idx = click.prompt(
|
|
255
|
+
" Transfer to device",
|
|
256
|
+
type=click.IntRange(1, len(other_devices)),
|
|
257
|
+
default=1,
|
|
258
|
+
)
|
|
259
|
+
target = other_devices[idx - 1]
|
|
260
|
+
target_ip = target.get("tailscale_ip", "")
|
|
261
|
+
target_host = target["hostname"]
|
|
262
|
+
|
|
263
|
+
if not target_ip:
|
|
264
|
+
console.print(f" [yellow]No IP for {target_host} — try option 1 instead.[/]")
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
console.print(f" Transferring to [cyan]{target_host}[/] ({target_ip})...")
|
|
268
|
+
console.print(" [dim]Using rsync over Tailscale...[/]")
|
|
269
|
+
|
|
270
|
+
for name in vault_names:
|
|
271
|
+
src = vaults_dir / name
|
|
272
|
+
if src.exists():
|
|
273
|
+
console.print(f" Copying [cyan]{name}[/]...", end=" ")
|
|
274
|
+
try:
|
|
275
|
+
dest_remote = f"{target_ip}:~/.skcapstone/vaults/{name}/"
|
|
276
|
+
result = subprocess.run(
|
|
277
|
+
["rsync", "-az", "--progress", f"{src}/", dest_remote],
|
|
278
|
+
capture_output=True, text=True, timeout=300,
|
|
279
|
+
)
|
|
280
|
+
if result.returncode == 0:
|
|
281
|
+
console.print("[green]done[/]")
|
|
282
|
+
else:
|
|
283
|
+
console.print(f"[yellow]rsync failed — try scp or manual copy[/]")
|
|
284
|
+
except FileNotFoundError:
|
|
285
|
+
console.print("[yellow]rsync not found — install or use option 1[/]")
|
|
286
|
+
return
|
|
287
|
+
except subprocess.TimeoutExpired:
|
|
288
|
+
console.print("[yellow]timed out[/]")
|
|
289
|
+
|
|
290
|
+
console.print(f"\n [green]Data transferred to {target_host}.[/]")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
# Teardown steps
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def _deregister_from_vault_registry(home_path: Path) -> None:
|
|
298
|
+
"""Remove this device from the vault registry.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
home_path: Agent home directory.
|
|
302
|
+
"""
|
|
303
|
+
console.print(" Removing this device from the vault registry...", end=" ")
|
|
304
|
+
try:
|
|
305
|
+
from skref.registry import deregister_device
|
|
306
|
+
result = deregister_device()
|
|
307
|
+
vaults_removed = result.get("vaults_removed", 0)
|
|
308
|
+
console.print(f"[green]done[/] ({vaults_removed} vault(s) removed)")
|
|
309
|
+
console.print(" [dim]Other devices will see this update via Syncthing.[/]")
|
|
310
|
+
except ImportError:
|
|
311
|
+
console.print("[dim]skref not installed — skipping[/]")
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
console.print(f"[yellow]{exc}[/]")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _teardown_tailscale() -> None:
|
|
317
|
+
"""Disable Funnel and log out of Tailscale."""
|
|
318
|
+
console.print(" Disconnecting from Tailscale...", end=" ")
|
|
319
|
+
try:
|
|
320
|
+
from skref import tailscale
|
|
321
|
+
if tailscale.is_installed():
|
|
322
|
+
tailscale.logout()
|
|
323
|
+
console.print("[green]logged out[/]")
|
|
324
|
+
console.print(
|
|
325
|
+
" [dim]This device is no longer on your tailnet.\n"
|
|
326
|
+
" You can also remove it from the admin console:\n"
|
|
327
|
+
f" {tailscale.get_admin_console_url().replace('/keys', '/machines')}[/]"
|
|
328
|
+
)
|
|
329
|
+
else:
|
|
330
|
+
console.print("[dim]not installed — skipping[/]")
|
|
331
|
+
except ImportError:
|
|
332
|
+
console.print("[dim]skref not installed — skipping[/]")
|
|
333
|
+
except Exception as exc:
|
|
334
|
+
console.print(f"[yellow]{exc}[/]")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _remove_syncthing_folder(home_path: Path) -> None:
|
|
338
|
+
"""Remove the Syncthing shared folder config for this device.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
home_path: Agent home directory.
|
|
342
|
+
"""
|
|
343
|
+
sync_dir = home_path / "sync"
|
|
344
|
+
console.print(" Removing Syncthing sync folder...", end=" ")
|
|
345
|
+
|
|
346
|
+
if not shutil.which("syncthing"):
|
|
347
|
+
console.print("[dim]syncthing not installed — skipping[/]")
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
# Syncthing REST API to remove a folder
|
|
351
|
+
try:
|
|
352
|
+
import urllib.request
|
|
353
|
+
import json
|
|
354
|
+
|
|
355
|
+
api_url = "http://localhost:8384/rest/config/folders"
|
|
356
|
+
req = urllib.request.Request(api_url)
|
|
357
|
+
resp = urllib.request.urlopen(req, timeout=5)
|
|
358
|
+
folders = json.loads(resp.read())
|
|
359
|
+
|
|
360
|
+
sync_str = str(sync_dir)
|
|
361
|
+
for folder in folders:
|
|
362
|
+
if sync_str in folder.get("path", ""):
|
|
363
|
+
folder_id = folder["id"]
|
|
364
|
+
del_url = f"http://localhost:8384/rest/config/folders/{folder_id}"
|
|
365
|
+
del_req = urllib.request.Request(del_url, method="DELETE")
|
|
366
|
+
urllib.request.urlopen(del_req, timeout=5)
|
|
367
|
+
console.print(f"[green]removed folder '{folder_id}'[/]")
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
console.print("[dim]no matching folder found[/]")
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
console.print(f"[dim]could not reach Syncthing API: {exc}[/]")
|
|
373
|
+
console.print(
|
|
374
|
+
" [dim]You may need to remove the shared folder manually\n"
|
|
375
|
+
" via the Syncthing web UI: http://localhost:8384[/]"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _remove_auth_key(home_path: Path) -> None:
|
|
380
|
+
"""Remove the Tailscale auth key from sync folder.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
home_path: Agent home directory.
|
|
384
|
+
"""
|
|
385
|
+
console.print(" Removing synced auth key...", end=" ")
|
|
386
|
+
try:
|
|
387
|
+
from skref.tailscale import remove_auth_key
|
|
388
|
+
if remove_auth_key():
|
|
389
|
+
console.print("[green]removed[/]")
|
|
390
|
+
else:
|
|
391
|
+
console.print("[dim]not found or already removed[/]")
|
|
392
|
+
except ImportError:
|
|
393
|
+
auth_key = home_path / "sync" / "tailscale.key.gpg"
|
|
394
|
+
if auth_key.exists():
|
|
395
|
+
auth_key.unlink()
|
|
396
|
+
console.print("[green]removed[/]")
|
|
397
|
+
else:
|
|
398
|
+
console.print("[dim]not found[/]")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _delete_local_data(home_path: Path) -> None:
|
|
402
|
+
"""Delete all local sovereign data.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
home_path: Agent home directory.
|
|
406
|
+
"""
|
|
407
|
+
console.print(" Deleting local data...", end=" ")
|
|
408
|
+
|
|
409
|
+
dirs_to_remove = [
|
|
410
|
+
home_path,
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
# Also check for vaults.yaml outside home
|
|
414
|
+
vaults_yaml = home_path.parent / "vaults.yaml"
|
|
415
|
+
extra_files = []
|
|
416
|
+
if vaults_yaml.exists():
|
|
417
|
+
extra_files.append(vaults_yaml)
|
|
418
|
+
|
|
419
|
+
removed = 0
|
|
420
|
+
for d in dirs_to_remove:
|
|
421
|
+
if d.exists():
|
|
422
|
+
try:
|
|
423
|
+
shutil.rmtree(d)
|
|
424
|
+
removed += 1
|
|
425
|
+
except (OSError, PermissionError) as exc:
|
|
426
|
+
console.print(f"\n [red]Could not delete {d}: {exc}[/]")
|
|
427
|
+
|
|
428
|
+
for f in extra_files:
|
|
429
|
+
try:
|
|
430
|
+
f.unlink()
|
|
431
|
+
except OSError:
|
|
432
|
+
pass
|
|
433
|
+
|
|
434
|
+
if removed > 0:
|
|
435
|
+
console.print(f"[green]deleted ({removed} directory)[/]")
|
|
436
|
+
else:
|
|
437
|
+
console.print("[dim]nothing to delete[/]")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _uninstall_packages() -> None:
|
|
441
|
+
"""Offer to uninstall pip packages."""
|
|
442
|
+
console.print()
|
|
443
|
+
do_uninstall = click.confirm(
|
|
444
|
+
" Also uninstall the software packages? (capauth, skmemory, etc.)",
|
|
445
|
+
default=False,
|
|
446
|
+
)
|
|
447
|
+
if not do_uninstall:
|
|
448
|
+
console.print(" [dim]Packages kept. You can uninstall later with pip.[/]")
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
console.print(" Uninstalling packages...", end=" ")
|
|
452
|
+
packages = [
|
|
453
|
+
"skcapstone", "capauth", "skmemory", "skcomm",
|
|
454
|
+
"cloud9-protocol", "skref", "skchat",
|
|
455
|
+
]
|
|
456
|
+
try:
|
|
457
|
+
result = subprocess.run(
|
|
458
|
+
[sys.executable, "-m", "pip", "uninstall", "-y", *packages],
|
|
459
|
+
capture_output=True, text=True, timeout=60,
|
|
460
|
+
)
|
|
461
|
+
if result.returncode == 0:
|
|
462
|
+
console.print("[green]done[/]")
|
|
463
|
+
else:
|
|
464
|
+
console.print("[yellow]partial[/]")
|
|
465
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
466
|
+
console.print("[yellow]pip unavailable[/]")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ---------------------------------------------------------------------------
|
|
470
|
+
# Main wizard
|
|
471
|
+
# ---------------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
def _export_backup(home_path: Path) -> Optional[Path]:
|
|
474
|
+
"""Create a full backup archive before wiping.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
home_path: Agent home directory.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Path to the created archive, or None on failure.
|
|
481
|
+
"""
|
|
482
|
+
console.print()
|
|
483
|
+
console.print(" [bold]Creating backup before removal...[/]")
|
|
484
|
+
|
|
485
|
+
dest = click.prompt(
|
|
486
|
+
" Save backup to directory (leave blank for ~/)",
|
|
487
|
+
default="",
|
|
488
|
+
show_default=False,
|
|
489
|
+
)
|
|
490
|
+
out_dir = Path(dest).expanduser() if dest.strip() else Path.home()
|
|
491
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
from .backup import create_backup # type: ignore[import]
|
|
495
|
+
except ImportError:
|
|
496
|
+
try:
|
|
497
|
+
from skcapstone.backup import create_backup # type: ignore[import]
|
|
498
|
+
except ImportError:
|
|
499
|
+
create_backup = None
|
|
500
|
+
|
|
501
|
+
if create_backup is None:
|
|
502
|
+
# Fallback: plain tar archive
|
|
503
|
+
import tarfile
|
|
504
|
+
import time
|
|
505
|
+
|
|
506
|
+
ts = int(time.time())
|
|
507
|
+
archive_path = out_dir / f"skcapstone-export-{ts}.tar.gz"
|
|
508
|
+
console.print(f" Archiving to [cyan]{archive_path}[/]...", end=" ")
|
|
509
|
+
try:
|
|
510
|
+
with tarfile.open(archive_path, "w:gz") as tar:
|
|
511
|
+
tar.add(home_path, arcname=home_path.name)
|
|
512
|
+
size = _human_size(archive_path.stat().st_size)
|
|
513
|
+
console.print(f"[green]done[/] ({size})")
|
|
514
|
+
return archive_path
|
|
515
|
+
except (OSError, tarfile.TarError) as exc:
|
|
516
|
+
console.print(f"[red]failed: {exc}[/]")
|
|
517
|
+
return None
|
|
518
|
+
|
|
519
|
+
console.print(f" Archiving to [cyan]{out_dir}[/]...", end=" ")
|
|
520
|
+
try:
|
|
521
|
+
result = create_backup(home=home_path, output_dir=out_dir)
|
|
522
|
+
archive_path = Path(result["filepath"])
|
|
523
|
+
size = _human_size(result["archive_size"])
|
|
524
|
+
console.print(f"[green]done[/] ({result['file_count']} files, {size})")
|
|
525
|
+
console.print(f" Archive: [cyan]{archive_path}[/]")
|
|
526
|
+
console.print(
|
|
527
|
+
" [dim]Restore on any machine with:[/]\n"
|
|
528
|
+
f" [cyan] skcapstone backup restore {archive_path}[/]"
|
|
529
|
+
)
|
|
530
|
+
return archive_path
|
|
531
|
+
except Exception as exc:
|
|
532
|
+
console.print(f"[red]failed: {exc}[/]")
|
|
533
|
+
return None
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def run_uninstall_wizard(
|
|
537
|
+
home: str = AGENT_HOME,
|
|
538
|
+
force: bool = False,
|
|
539
|
+
keep_data: bool = False,
|
|
540
|
+
export_first: bool = False,
|
|
541
|
+
) -> None:
|
|
542
|
+
"""Run the full uninstall wizard.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
home: Agent home directory.
|
|
546
|
+
force: Skip confirmations (for CI/scripting).
|
|
547
|
+
keep_data: Keep local files (only deregister).
|
|
548
|
+
export_first: Create a full backup archive before removing data.
|
|
549
|
+
"""
|
|
550
|
+
home_path = Path(home).expanduser()
|
|
551
|
+
|
|
552
|
+
if not home_path.exists():
|
|
553
|
+
console.print(
|
|
554
|
+
Panel(
|
|
555
|
+
"[bold]No sovereign node found.[/]\n\n"
|
|
556
|
+
f"Looked in: {home_path}\n\n"
|
|
557
|
+
"Nothing to uninstall.",
|
|
558
|
+
title="Not Found",
|
|
559
|
+
border_style="dim",
|
|
560
|
+
)
|
|
561
|
+
)
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
inventory = _build_inventory(home_path)
|
|
565
|
+
|
|
566
|
+
# --- Show what will be removed ---
|
|
567
|
+
console.print()
|
|
568
|
+
console.print(
|
|
569
|
+
Panel(
|
|
570
|
+
"[bold red]Sovereign Node Removal[/]\n\n"
|
|
571
|
+
"This will permanently remove your sovereign node\n"
|
|
572
|
+
"from this computer and from your device network.\n\n"
|
|
573
|
+
"[bold]This action cannot be undone.[/]",
|
|
574
|
+
title="Uninstall",
|
|
575
|
+
border_style="red",
|
|
576
|
+
padding=(1, 3),
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
console.print()
|
|
580
|
+
|
|
581
|
+
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
|
|
582
|
+
table.add_column("Item", width=25)
|
|
583
|
+
table.add_column("Details")
|
|
584
|
+
|
|
585
|
+
table.add_row("Home directory", str(home_path))
|
|
586
|
+
table.add_row("Total size", _human_size(inventory["total_size_bytes"]))
|
|
587
|
+
table.add_row(
|
|
588
|
+
"Backup",
|
|
589
|
+
"[green]will export first[/]" if export_first else "[dim]no backup[/]",
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
if inventory["vault_names"]:
|
|
593
|
+
table.add_row("Vaults", ", ".join(inventory["vault_names"]))
|
|
594
|
+
else:
|
|
595
|
+
table.add_row("Vaults", "[dim]none[/]")
|
|
596
|
+
|
|
597
|
+
table.add_row(
|
|
598
|
+
"Vault registry",
|
|
599
|
+
"[green]will deregister[/]" if inventory["has_registry"] else "[dim]not found[/]",
|
|
600
|
+
)
|
|
601
|
+
table.add_row(
|
|
602
|
+
"Tailscale",
|
|
603
|
+
"[green]will log out[/]" if inventory["has_tailscale"] else "[dim]not installed[/]",
|
|
604
|
+
)
|
|
605
|
+
table.add_row(
|
|
606
|
+
"Syncthing",
|
|
607
|
+
"[green]will remove folder[/]" if inventory["has_syncthing"] else "[dim]not installed[/]",
|
|
608
|
+
)
|
|
609
|
+
table.add_row(
|
|
610
|
+
"Auth key",
|
|
611
|
+
"[green]will remove[/]" if inventory["has_auth_key"] else "[dim]not found[/]",
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
console.print(table)
|
|
615
|
+
console.print()
|
|
616
|
+
|
|
617
|
+
# --- Confirm ---
|
|
618
|
+
if not force:
|
|
619
|
+
console.print(
|
|
620
|
+
" [bold red]Are you sure?[/]\n"
|
|
621
|
+
" Type [bold]DELETE[/] to confirm (all caps):"
|
|
622
|
+
)
|
|
623
|
+
confirmation = click.prompt(" ", default="", show_default=False)
|
|
624
|
+
if confirmation != "DELETE":
|
|
625
|
+
console.print(" [green]Cancelled.[/] Nothing was changed.")
|
|
626
|
+
return
|
|
627
|
+
console.print()
|
|
628
|
+
|
|
629
|
+
# --- Export-first: full backup archive before wiping ---
|
|
630
|
+
if export_first and not keep_data:
|
|
631
|
+
archive = _export_backup(home_path)
|
|
632
|
+
if archive is None and not force:
|
|
633
|
+
if not click.confirm(
|
|
634
|
+
" Backup failed. Continue with removal anyway?",
|
|
635
|
+
default=False,
|
|
636
|
+
):
|
|
637
|
+
console.print(" [green]Cancelled.[/] Nothing was changed.")
|
|
638
|
+
return
|
|
639
|
+
console.print()
|
|
640
|
+
|
|
641
|
+
# --- Data transfer option ---
|
|
642
|
+
if not keep_data and inventory["vault_names"]:
|
|
643
|
+
_offer_data_transfer(home_path, inventory)
|
|
644
|
+
console.print()
|
|
645
|
+
|
|
646
|
+
# --- Execute teardown ---
|
|
647
|
+
console.print(" [bold]Removing sovereign node...[/]")
|
|
648
|
+
console.print()
|
|
649
|
+
|
|
650
|
+
# Step 1: Deregister from vault registry (before deleting sync folder!)
|
|
651
|
+
_deregister_from_vault_registry(home_path)
|
|
652
|
+
|
|
653
|
+
# Step 2: Disable Tailscale
|
|
654
|
+
if inventory["has_tailscale"]:
|
|
655
|
+
_teardown_tailscale()
|
|
656
|
+
|
|
657
|
+
# Step 3: Remove auth key from sync
|
|
658
|
+
if inventory["has_auth_key"]:
|
|
659
|
+
_remove_auth_key(home_path)
|
|
660
|
+
|
|
661
|
+
# Step 4: Remove Syncthing folder config
|
|
662
|
+
if inventory["has_syncthing"]:
|
|
663
|
+
_remove_syncthing_folder(home_path)
|
|
664
|
+
|
|
665
|
+
# Step 5: Delete local data
|
|
666
|
+
if not keep_data:
|
|
667
|
+
_delete_local_data(home_path)
|
|
668
|
+
else:
|
|
669
|
+
console.print(" [dim]Local data kept (--keep-data).[/]")
|
|
670
|
+
|
|
671
|
+
# Step 6: Optionally uninstall packages
|
|
672
|
+
if not force:
|
|
673
|
+
_uninstall_packages()
|
|
674
|
+
|
|
675
|
+
# --- Done ---
|
|
676
|
+
console.print()
|
|
677
|
+
console.print(
|
|
678
|
+
Panel(
|
|
679
|
+
"[bold]Sovereign node removed.[/]\n\n"
|
|
680
|
+
" This computer has been deregistered.\n"
|
|
681
|
+
" Other devices will see the update via Syncthing.\n\n"
|
|
682
|
+
" [dim]If you change your mind, run:[/]\n"
|
|
683
|
+
" [cyan]skcapstone install[/]\n"
|
|
684
|
+
" [dim]and choose option 1 (fresh setup) or 2 (rejoin).[/]\n\n"
|
|
685
|
+
"[dim]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/]\n\n"
|
|
686
|
+
" [dim italic]You'll always be a King or Queen at[/]\n"
|
|
687
|
+
" [bold bright_magenta]smilinTux.org[/]\n\n"
|
|
688
|
+
" [bold]https://smilintux.org/join/[/]",
|
|
689
|
+
title="Uninstall Complete",
|
|
690
|
+
border_style="green",
|
|
691
|
+
padding=(1, 3),
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
console.print()
|