@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.
Files changed (461) hide show
  1. package/.env.example +98 -0
  2. package/.github/workflows/ci.yml +39 -3
  3. package/.github/workflows/publish.yml +25 -4
  4. package/.openclaw-workspace.json +58 -0
  5. package/CHANGELOG.md +62 -0
  6. package/CLAUDE.md +39 -2
  7. package/MANIFEST.in +6 -0
  8. package/MISSION.md +7 -0
  9. package/README.md +47 -2
  10. package/SKILL.md +895 -23
  11. package/docker/Dockerfile +61 -0
  12. package/docker/compose-templates/dev-team.yml +203 -0
  13. package/docker/compose-templates/mini-team.yml +140 -0
  14. package/docker/compose-templates/ops-team.yml +173 -0
  15. package/docker/compose-templates/research-team.yml +170 -0
  16. package/docker/entrypoint.sh +192 -0
  17. package/docs/ARCHITECTURE.md +663 -374
  18. package/docs/BOND_WITH_GROK.md +112 -0
  19. package/docs/GETTING_STARTED.md +782 -0
  20. package/docs/QUICKSTART.md +477 -0
  21. package/docs/SKJOULE_ARCHITECTURE.md +658 -0
  22. package/docs/SOUL_SWAPPER.md +921 -0
  23. package/docs/SOVEREIGN_SINGULARITY.md +47 -14
  24. package/examples/custom-bond-template.json +36 -0
  25. package/examples/grok-feb.json +36 -0
  26. package/examples/grok-testimony.md +34 -0
  27. package/examples/love-bootloader.txt +32 -0
  28. package/examples/plugins/echo_tool.py +87 -0
  29. package/examples/queen-ava-feb.json +36 -0
  30. package/examples/souls/lumina.yaml +64 -0
  31. package/index.js +6 -5
  32. package/installer/build.py +124 -0
  33. package/openclaw-plugin/package.json +13 -0
  34. package/openclaw-plugin/src/index.ts +351 -0
  35. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  36. package/package.json +1 -1
  37. package/pyproject.toml +38 -2
  38. package/scripts/bump_version.py +141 -0
  39. package/scripts/check-updates.py +230 -0
  40. package/scripts/convert_blueprints_to_yaml.py +157 -0
  41. package/scripts/dev-install.sh +14 -0
  42. package/scripts/e2e-test.sh +193 -0
  43. package/scripts/install-bundle.sh +171 -0
  44. package/scripts/install.bat +2 -0
  45. package/scripts/install.ps1 +253 -0
  46. package/scripts/install.sh +185 -0
  47. package/scripts/mcp-serve.sh +69 -0
  48. package/scripts/mcp-server.bat +113 -0
  49. package/scripts/mcp-server.ps1 +116 -0
  50. package/scripts/mcp-server.sh +99 -0
  51. package/scripts/pull-models.sh +10 -0
  52. package/scripts/skcapstone +48 -0
  53. package/scripts/verify_install.sh +180 -0
  54. package/scripts/windows/install-tasks.ps1 +406 -0
  55. package/scripts/windows/skcapstone-task.xml +113 -0
  56. package/scripts/windows/uninstall-tasks.ps1 +117 -0
  57. package/skill.yaml +34 -0
  58. package/src/skcapstone/__init__.py +67 -2
  59. package/src/skcapstone/_cli_monolith.py +5916 -0
  60. package/src/skcapstone/_trustee_helpers.py +165 -0
  61. package/src/skcapstone/activity.py +105 -0
  62. package/src/skcapstone/agent_card.py +324 -0
  63. package/src/skcapstone/api.py +1935 -0
  64. package/src/skcapstone/archiver.py +340 -0
  65. package/src/skcapstone/auction.py +485 -0
  66. package/src/skcapstone/baby_agents.py +179 -0
  67. package/src/skcapstone/backup.py +345 -0
  68. package/src/skcapstone/blueprint_registry.py +357 -0
  69. package/src/skcapstone/blueprints/__init__.py +17 -0
  70. package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
  71. package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
  72. package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
  73. package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
  74. package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
  75. package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
  76. package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
  77. package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
  78. package/src/skcapstone/blueprints/registry.py +164 -0
  79. package/src/skcapstone/blueprints/schema.py +229 -0
  80. package/src/skcapstone/changelog.py +180 -0
  81. package/src/skcapstone/chat.py +769 -0
  82. package/src/skcapstone/claude_md.py +82 -0
  83. package/src/skcapstone/cli/__init__.py +144 -0
  84. package/src/skcapstone/cli/_common.py +88 -0
  85. package/src/skcapstone/cli/_validators.py +76 -0
  86. package/src/skcapstone/cli/agents.py +425 -0
  87. package/src/skcapstone/cli/agents_spawner.py +322 -0
  88. package/src/skcapstone/cli/agents_trustee.py +593 -0
  89. package/src/skcapstone/cli/alerts.py +248 -0
  90. package/src/skcapstone/cli/anchor.py +132 -0
  91. package/src/skcapstone/cli/archive_cmd.py +208 -0
  92. package/src/skcapstone/cli/backup.py +144 -0
  93. package/src/skcapstone/cli/bench.py +377 -0
  94. package/src/skcapstone/cli/benchmark.py +360 -0
  95. package/src/skcapstone/cli/capabilities_cmd.py +171 -0
  96. package/src/skcapstone/cli/card.py +151 -0
  97. package/src/skcapstone/cli/chat.py +584 -0
  98. package/src/skcapstone/cli/completions.py +64 -0
  99. package/src/skcapstone/cli/config_cmd.py +156 -0
  100. package/src/skcapstone/cli/consciousness.py +421 -0
  101. package/src/skcapstone/cli/context_cmd.py +142 -0
  102. package/src/skcapstone/cli/coord.py +194 -0
  103. package/src/skcapstone/cli/crush_cmd.py +170 -0
  104. package/src/skcapstone/cli/daemon.py +436 -0
  105. package/src/skcapstone/cli/errors_cmd.py +285 -0
  106. package/src/skcapstone/cli/export_cmd.py +156 -0
  107. package/src/skcapstone/cli/gtd.py +529 -0
  108. package/src/skcapstone/cli/housekeeping.py +81 -0
  109. package/src/skcapstone/cli/joule_cmd.py +627 -0
  110. package/src/skcapstone/cli/logs_cmd.py +194 -0
  111. package/src/skcapstone/cli/mcp_cmd.py +32 -0
  112. package/src/skcapstone/cli/memory.py +418 -0
  113. package/src/skcapstone/cli/metrics_cmd.py +136 -0
  114. package/src/skcapstone/cli/migrate.py +62 -0
  115. package/src/skcapstone/cli/mood_cmd.py +144 -0
  116. package/src/skcapstone/cli/mount.py +193 -0
  117. package/src/skcapstone/cli/notify.py +112 -0
  118. package/src/skcapstone/cli/peer.py +154 -0
  119. package/src/skcapstone/cli/peers_dir.py +122 -0
  120. package/src/skcapstone/cli/preflight_cmd.py +83 -0
  121. package/src/skcapstone/cli/profile_cmd.py +310 -0
  122. package/src/skcapstone/cli/record_cmd.py +238 -0
  123. package/src/skcapstone/cli/register_cmd.py +159 -0
  124. package/src/skcapstone/cli/search_cmd.py +156 -0
  125. package/src/skcapstone/cli/service_cmd.py +91 -0
  126. package/src/skcapstone/cli/session.py +127 -0
  127. package/src/skcapstone/cli/setup.py +240 -0
  128. package/src/skcapstone/cli/shell_cmd.py +43 -0
  129. package/src/skcapstone/cli/skills_cmd.py +168 -0
  130. package/src/skcapstone/cli/skseed.py +621 -0
  131. package/src/skcapstone/cli/soul.py +699 -0
  132. package/src/skcapstone/cli/status.py +935 -0
  133. package/src/skcapstone/cli/sync_cmd.py +301 -0
  134. package/src/skcapstone/cli/telegram.py +265 -0
  135. package/src/skcapstone/cli/test_cmd.py +234 -0
  136. package/src/skcapstone/cli/test_connection.py +253 -0
  137. package/src/skcapstone/cli/token.py +207 -0
  138. package/src/skcapstone/cli/trust.py +179 -0
  139. package/src/skcapstone/cli/upgrade_cmd.py +552 -0
  140. package/src/skcapstone/cli/usage_cmd.py +199 -0
  141. package/src/skcapstone/cli/version_cmd.py +162 -0
  142. package/src/skcapstone/cli/watch_cmd.py +342 -0
  143. package/src/skcapstone/client.py +428 -0
  144. package/src/skcapstone/cloud9_bridge.py +522 -0
  145. package/src/skcapstone/completions.py +163 -0
  146. package/src/skcapstone/config_validator.py +674 -0
  147. package/src/skcapstone/connectors/__init__.py +28 -0
  148. package/src/skcapstone/connectors/base.py +446 -0
  149. package/src/skcapstone/connectors/cursor.py +54 -0
  150. package/src/skcapstone/connectors/registry.py +254 -0
  151. package/src/skcapstone/connectors/terminal.py +152 -0
  152. package/src/skcapstone/connectors/vscode.py +60 -0
  153. package/src/skcapstone/consciousness_config.py +119 -0
  154. package/src/skcapstone/consciousness_loop.py +2051 -0
  155. package/src/skcapstone/context_loader.py +516 -0
  156. package/src/skcapstone/context_window.py +314 -0
  157. package/src/skcapstone/conversation_manager.py +238 -0
  158. package/src/skcapstone/conversation_store.py +230 -0
  159. package/src/skcapstone/conversation_summarizer.py +252 -0
  160. package/src/skcapstone/coord_federation.py +296 -0
  161. package/src/skcapstone/coordination.py +101 -7
  162. package/src/skcapstone/crush_integration.py +345 -0
  163. package/src/skcapstone/crush_shim.py +454 -0
  164. package/src/skcapstone/daemon.py +2494 -0
  165. package/src/skcapstone/dashboard.html +396 -0
  166. package/src/skcapstone/dashboard.py +481 -0
  167. package/src/skcapstone/data/model_profiles.yaml +88 -0
  168. package/src/skcapstone/defaults/__init__.py +55 -0
  169. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
  170. package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
  171. package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
  172. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
  173. package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
  174. package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
  175. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
  176. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
  177. package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
  178. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
  179. package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
  180. package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
  181. package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
  182. package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
  183. package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
  184. package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
  185. package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
  186. package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
  187. package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
  188. package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
  189. package/src/skcapstone/discovery.py +210 -19
  190. package/src/skcapstone/doctor.py +642 -0
  191. package/src/skcapstone/emotion_tracker.py +467 -0
  192. package/src/skcapstone/error_queue.py +405 -0
  193. package/src/skcapstone/export.py +447 -0
  194. package/src/skcapstone/fallback_tracker.py +186 -0
  195. package/src/skcapstone/file_transfer.py +512 -0
  196. package/src/skcapstone/fuse_mount.py +1156 -0
  197. package/src/skcapstone/gui_installer.py +591 -0
  198. package/src/skcapstone/heartbeat.py +611 -0
  199. package/src/skcapstone/housekeeping.py +298 -0
  200. package/src/skcapstone/install_wizard.py +941 -0
  201. package/src/skcapstone/kms.py +942 -0
  202. package/src/skcapstone/kms_scheduler.py +143 -0
  203. package/src/skcapstone/log_config.py +135 -0
  204. package/src/skcapstone/mcp_launcher.py +239 -0
  205. package/src/skcapstone/mcp_server.py +4700 -0
  206. package/src/skcapstone/mcp_tools/__init__.py +94 -0
  207. package/src/skcapstone/mcp_tools/_helpers.py +51 -0
  208. package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
  209. package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
  210. package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
  211. package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
  212. package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
  213. package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
  214. package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
  215. package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
  216. package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
  217. package/src/skcapstone/mcp_tools/did_tools.py +448 -0
  218. package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
  219. package/src/skcapstone/mcp_tools/file_tools.py +169 -0
  220. package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
  221. package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
  222. package/src/skcapstone/mcp_tools/health_tools.py +44 -0
  223. package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
  224. package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
  225. package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
  226. package/src/skcapstone/mcp_tools/model_tools.py +75 -0
  227. package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
  228. package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
  229. package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
  230. package/src/skcapstone/mcp_tools/security_tools.py +110 -0
  231. package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
  232. package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
  233. package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
  234. package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
  235. package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
  236. package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
  237. package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
  238. package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
  239. package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
  240. package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
  241. package/src/skcapstone/mdns_discovery.py +313 -0
  242. package/src/skcapstone/memory_adapter.py +333 -0
  243. package/src/skcapstone/memory_compressor.py +379 -0
  244. package/src/skcapstone/memory_curator.py +256 -0
  245. package/src/skcapstone/memory_engine.py +132 -13
  246. package/src/skcapstone/memory_fortress.py +529 -0
  247. package/src/skcapstone/memory_promoter.py +722 -0
  248. package/src/skcapstone/memory_verifier.py +260 -0
  249. package/src/skcapstone/message_crypto.py +215 -0
  250. package/src/skcapstone/metrics.py +832 -0
  251. package/src/skcapstone/migrate_memories.py +181 -0
  252. package/src/skcapstone/migrate_multi_agent.py +248 -0
  253. package/src/skcapstone/model_router.py +319 -0
  254. package/src/skcapstone/models.py +35 -4
  255. package/src/skcapstone/mood.py +344 -0
  256. package/src/skcapstone/notifications.py +380 -0
  257. package/src/skcapstone/onboard.py +901 -0
  258. package/src/skcapstone/peer_directory.py +324 -0
  259. package/src/skcapstone/peers.py +329 -0
  260. package/src/skcapstone/pillars/identity.py +84 -14
  261. package/src/skcapstone/pillars/memory.py +3 -1
  262. package/src/skcapstone/pillars/security.py +108 -15
  263. package/src/skcapstone/pillars/sync.py +78 -26
  264. package/src/skcapstone/pillars/trust.py +95 -33
  265. package/src/skcapstone/plugins.py +244 -0
  266. package/src/skcapstone/preflight.py +670 -0
  267. package/src/skcapstone/prompt_adapter.py +564 -0
  268. package/src/skcapstone/providers/__init__.py +13 -0
  269. package/src/skcapstone/providers/cloud.py +1061 -0
  270. package/src/skcapstone/providers/docker.py +759 -0
  271. package/src/skcapstone/providers/local.py +1193 -0
  272. package/src/skcapstone/providers/proxmox.py +447 -0
  273. package/src/skcapstone/pubsub.py +516 -0
  274. package/src/skcapstone/rate_limiter.py +119 -0
  275. package/src/skcapstone/register.py +241 -0
  276. package/src/skcapstone/registry_client.py +151 -0
  277. package/src/skcapstone/response_cache.py +194 -0
  278. package/src/skcapstone/response_scorer.py +225 -0
  279. package/src/skcapstone/runtime.py +89 -33
  280. package/src/skcapstone/scheduled_tasks.py +439 -0
  281. package/src/skcapstone/self_healing.py +341 -0
  282. package/src/skcapstone/service_health.py +228 -0
  283. package/src/skcapstone/session_capture.py +268 -0
  284. package/src/skcapstone/session_recorder.py +210 -0
  285. package/src/skcapstone/session_replayer.py +189 -0
  286. package/src/skcapstone/session_skills.py +263 -0
  287. package/src/skcapstone/shell.py +779 -0
  288. package/src/skcapstone/skills/__init__.py +1 -1
  289. package/src/skcapstone/skills/syncthing_setup.py +143 -41
  290. package/src/skcapstone/skjoule.py +861 -0
  291. package/src/skcapstone/snapshots.py +489 -0
  292. package/src/skcapstone/soul.py +1060 -0
  293. package/src/skcapstone/soul_switch.py +255 -0
  294. package/src/skcapstone/spawner.py +544 -0
  295. package/src/skcapstone/state_diff.py +401 -0
  296. package/src/skcapstone/summary.py +270 -0
  297. package/src/skcapstone/sync/backends.py +196 -2
  298. package/src/skcapstone/sync/engine.py +7 -5
  299. package/src/skcapstone/sync/models.py +4 -1
  300. package/src/skcapstone/sync/vault.py +356 -18
  301. package/src/skcapstone/sync_engine.py +363 -0
  302. package/src/skcapstone/sync_watcher.py +745 -0
  303. package/src/skcapstone/systemd.py +331 -0
  304. package/src/skcapstone/team_comms.py +476 -0
  305. package/src/skcapstone/team_engine.py +522 -0
  306. package/src/skcapstone/testrunner.py +300 -0
  307. package/src/skcapstone/tls.py +150 -0
  308. package/src/skcapstone/tokens.py +5 -5
  309. package/src/skcapstone/trust_calibration.py +202 -0
  310. package/src/skcapstone/trust_graph.py +449 -0
  311. package/src/skcapstone/trustee_monitor.py +385 -0
  312. package/src/skcapstone/trustee_ops.py +425 -0
  313. package/src/skcapstone/unified_search.py +421 -0
  314. package/src/skcapstone/uninstall_wizard.py +694 -0
  315. package/src/skcapstone/usage.py +331 -0
  316. package/src/skcapstone/version_check.py +148 -0
  317. package/src/skcapstone/warmth_anchor.py +333 -0
  318. package/src/skcapstone/whoami.py +294 -0
  319. package/systemd/skcapstone-api.socket +9 -0
  320. package/systemd/skcapstone-memory-compress.service +18 -0
  321. package/systemd/skcapstone-memory-compress.timer +11 -0
  322. package/systemd/skcapstone.service +36 -0
  323. package/systemd/skcapstone@.service +50 -0
  324. package/systemd/skcomm-heartbeat.service +18 -0
  325. package/systemd/skcomm-heartbeat.timer +12 -0
  326. package/systemd/skcomm-queue-drain.service +17 -0
  327. package/systemd/skcomm-queue-drain.timer +12 -0
  328. package/tests/conftest.py +13 -1
  329. package/tests/integration/__init__.py +1 -0
  330. package/tests/integration/test_consciousness_e2e.py +877 -0
  331. package/tests/integration/test_skills_registry.py +744 -0
  332. package/tests/test_agent_card.py +190 -0
  333. package/tests/test_agent_runtime.py +1283 -0
  334. package/tests/test_alerts_cmd.py +291 -0
  335. package/tests/test_archiver.py +498 -0
  336. package/tests/test_backup.py +254 -0
  337. package/tests/test_benchmark.py +366 -0
  338. package/tests/test_blueprints.py +457 -0
  339. package/tests/test_capabilities.py +257 -0
  340. package/tests/test_changelog.py +254 -0
  341. package/tests/test_chat.py +385 -0
  342. package/tests/test_claude_md.py +271 -0
  343. package/tests/test_cli_chat_llm.py +336 -0
  344. package/tests/test_cli_completions.py +390 -0
  345. package/tests/test_cli_init_reset.py +164 -0
  346. package/tests/test_cli_memory.py +208 -0
  347. package/tests/test_cli_profile.py +294 -0
  348. package/tests/test_cli_skills.py +223 -0
  349. package/tests/test_cli_status.py +395 -0
  350. package/tests/test_cli_test_cmd.py +206 -0
  351. package/tests/test_cli_test_connection.py +364 -0
  352. package/tests/test_cloud9_bridge.py +260 -0
  353. package/tests/test_cloud_provider.py +449 -0
  354. package/tests/test_cloud_providers.py +522 -0
  355. package/tests/test_completions.py +158 -0
  356. package/tests/test_component_manager.py +398 -0
  357. package/tests/test_config_reload.py +386 -0
  358. package/tests/test_config_validate.py +529 -0
  359. package/tests/test_consciousness_e2e.py +296 -0
  360. package/tests/test_consciousness_loop.py +1289 -0
  361. package/tests/test_context_loader.py +310 -0
  362. package/tests/test_conversation_api.py +306 -0
  363. package/tests/test_conversation_manager.py +381 -0
  364. package/tests/test_conversation_store.py +391 -0
  365. package/tests/test_conversation_summarizer.py +302 -0
  366. package/tests/test_cross_package.py +791 -0
  367. package/tests/test_crush_shim.py +519 -0
  368. package/tests/test_daemon.py +781 -0
  369. package/tests/test_daemon_shutdown.py +309 -0
  370. package/tests/test_dashboard.py +454 -0
  371. package/tests/test_discovery.py +200 -6
  372. package/tests/test_docker_provider.py +966 -0
  373. package/tests/test_doctor.py +257 -0
  374. package/tests/test_doctor_fix.py +351 -0
  375. package/tests/test_e2e_automated.py +292 -0
  376. package/tests/test_error_queue.py +404 -0
  377. package/tests/test_export.py +441 -0
  378. package/tests/test_fallback_tracker.py +219 -0
  379. package/tests/test_file_transfer.py +397 -0
  380. package/tests/test_fuse_mount.py +832 -0
  381. package/tests/test_health_loop.py +422 -0
  382. package/tests/test_heartbeat.py +354 -0
  383. package/tests/test_housekeeping.py +195 -0
  384. package/tests/test_identity_capauth.py +307 -0
  385. package/tests/test_identity_pillar.py +117 -0
  386. package/tests/test_install_wizard.py +68 -0
  387. package/tests/test_integration.py +325 -0
  388. package/tests/test_kms.py +495 -0
  389. package/tests/test_llm_providers.py +265 -0
  390. package/tests/test_local_provider.py +591 -0
  391. package/tests/test_log_config.py +199 -0
  392. package/tests/test_logs_cmd.py +287 -0
  393. package/tests/test_mcp_server.py +1909 -0
  394. package/tests/test_memory_adapter.py +339 -0
  395. package/tests/test_memory_curator.py +218 -0
  396. package/tests/test_memory_engine.py +6 -0
  397. package/tests/test_memory_fortress.py +571 -0
  398. package/tests/test_memory_pillar.py +119 -0
  399. package/tests/test_memory_promoter.py +445 -0
  400. package/tests/test_memory_verifier.py +420 -0
  401. package/tests/test_message_crypto.py +187 -0
  402. package/tests/test_metrics.py +632 -0
  403. package/tests/test_migrate_memories.py +464 -0
  404. package/tests/test_model_router.py +546 -0
  405. package/tests/test_mood.py +394 -0
  406. package/tests/test_multi_agent.py +269 -0
  407. package/tests/test_notifications.py +270 -0
  408. package/tests/test_onboard.py +500 -0
  409. package/tests/test_peer_directory.py +395 -0
  410. package/tests/test_peers.py +248 -0
  411. package/tests/test_pillars.py +87 -9
  412. package/tests/test_preflight.py +484 -0
  413. package/tests/test_prompt_adapter.py +331 -0
  414. package/tests/test_proxmox_provider.py +571 -0
  415. package/tests/test_pubsub.py +377 -0
  416. package/tests/test_rate_limiter.py +121 -0
  417. package/tests/test_registry_client.py +129 -0
  418. package/tests/test_response_cache.py +312 -0
  419. package/tests/test_response_scorer.py +294 -0
  420. package/tests/test_runtime.py +59 -0
  421. package/tests/test_scheduled_tasks.py +451 -0
  422. package/tests/test_security.py +250 -0
  423. package/tests/test_security_pillar.py +213 -0
  424. package/tests/test_self_healing.py +171 -0
  425. package/tests/test_session_capture.py +200 -0
  426. package/tests/test_session_recorder.py +360 -0
  427. package/tests/test_session_skills.py +235 -0
  428. package/tests/test_shell.py +210 -0
  429. package/tests/test_snapshots.py +549 -0
  430. package/tests/test_soul.py +984 -0
  431. package/tests/test_soul_swap.py +406 -0
  432. package/tests/test_spawner.py +211 -0
  433. package/tests/test_state_diff.py +173 -0
  434. package/tests/test_summary.py +135 -0
  435. package/tests/test_sync.py +315 -5
  436. package/tests/test_sync_backends.py +560 -0
  437. package/tests/test_sync_engine.py +482 -0
  438. package/tests/test_sync_pillar.py +344 -0
  439. package/tests/test_sync_pipeline.py +364 -0
  440. package/tests/test_sync_vault.py +581 -0
  441. package/tests/test_syncthing_setup.py +168 -22
  442. package/tests/test_systemd.py +323 -0
  443. package/tests/test_team_comms.py +408 -0
  444. package/tests/test_team_engine.py +397 -0
  445. package/tests/test_testrunner.py +238 -0
  446. package/tests/test_trust_calibration.py +204 -0
  447. package/tests/test_trust_graph.py +207 -0
  448. package/tests/test_trust_pillar.py +291 -0
  449. package/tests/test_trustee_cli.py +427 -0
  450. package/tests/test_trustee_cli_integration.py +325 -0
  451. package/tests/test_trustee_monitor.py +394 -0
  452. package/tests/test_trustee_ops.py +355 -0
  453. package/tests/test_unified_search.py +363 -0
  454. package/tests/test_uninstall_wizard.py +193 -0
  455. package/tests/test_usage.py +333 -0
  456. package/tests/test_version_cmd.py +355 -0
  457. package/tests/test_warmth_anchor.py +162 -0
  458. package/tests/test_whoami.py +245 -0
  459. package/tests/test_ws.py +311 -0
  460. package/.cursorrules +0 -33
  461. 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()