@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,935 @@
1
+ """Status and overview commands: status, summary, doctor, audit, dashboard, whoami, diff."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ import sys
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import click
14
+
15
+ from ._common import AGENT_HOME, console, status_icon, consciousness_banner
16
+ from ..models import PillarStatus
17
+ from ..runtime import get_runtime
18
+
19
+ from rich.panel import Panel
20
+ from rich.table import Table
21
+
22
+
23
+ def _probe_llm_backends() -> dict[str, bool]:
24
+ """Probe LLM backend availability.
25
+
26
+ Checks ollama via HTTP and all cloud providers via env vars.
27
+
28
+ Returns:
29
+ Dict mapping backend name to availability bool.
30
+ """
31
+ import urllib.request
32
+
33
+ backends: dict[str, bool] = {
34
+ "ollama": False,
35
+ "anthropic": bool(os.environ.get("ANTHROPIC_API_KEY")),
36
+ "grok": bool(os.environ.get("XAI_API_KEY")),
37
+ "kimi": bool(os.environ.get("MOONSHOT_API_KEY")),
38
+ "nvidia": bool(os.environ.get("NVIDIA_API_KEY")),
39
+ }
40
+ host = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
41
+ try:
42
+ with urllib.request.urlopen(
43
+ urllib.request.Request(f"{host}/api/tags"), timeout=2
44
+ ):
45
+ backends["ollama"] = True
46
+ except Exception:
47
+ pass
48
+ return backends
49
+
50
+
51
+ def _get_daemon_info(home: Path) -> dict:
52
+ """Return daemon running state, PID, uptime, and message count.
53
+
54
+ Args:
55
+ home: Agent home directory.
56
+
57
+ Returns:
58
+ Dict with 'running' bool and optional 'pid', 'uptime', 'messages'.
59
+ """
60
+ try:
61
+ from ..daemon import read_pid, get_daemon_status
62
+
63
+ pid = read_pid(home)
64
+ if pid is None:
65
+ return {"running": False}
66
+ info: dict = {"running": True, "pid": pid}
67
+ ds = get_daemon_status(home)
68
+ if ds:
69
+ uptime_s = int(ds.get("uptime_seconds", 0))
70
+ h, rem = divmod(uptime_s, 3600)
71
+ m, s = divmod(rem, 60)
72
+ info["uptime"] = f"{h}h {m}m" if h else f"{m}m {s}s" if m else f"{s}s"
73
+ msgs = ds.get("messages_received", 0)
74
+ if msgs:
75
+ info["messages"] = msgs
76
+ return info
77
+ except Exception:
78
+ return {"running": False}
79
+
80
+
81
+ def _get_last_conversation(home: Path) -> Optional[dict]:
82
+ """Find the most recently modified peer conversation file.
83
+
84
+ Args:
85
+ home: Agent home directory.
86
+
87
+ Returns:
88
+ Dict with 'peer' and 'when' strings, or None if no conversations.
89
+ """
90
+ conv_dir = home / "conversations"
91
+ if not conv_dir.exists():
92
+ return None
93
+ files = list(conv_dir.glob("*.json"))
94
+ if not files:
95
+ return None
96
+ latest = max(files, key=lambda f: f.stat().st_mtime)
97
+ age_s = datetime.now().timestamp() - latest.stat().st_mtime
98
+ if age_s < 3600:
99
+ when = f"{int(age_s / 60)}m ago"
100
+ elif age_s < 86400:
101
+ when = f"{int(age_s / 3600)}h ago"
102
+ else:
103
+ when = datetime.fromtimestamp(latest.stat().st_mtime).strftime("%m/%d %H:%M")
104
+ return {"peer": latest.stem, "when": when}
105
+
106
+
107
+ def _read_local_heartbeat(home: Path) -> Optional[dict]:
108
+ """Read the local agent heartbeat file.
109
+
110
+ Args:
111
+ home: Agent home directory.
112
+
113
+ Returns:
114
+ Heartbeat dict or None if not found.
115
+ """
116
+ # Heartbeats are stored in shared_root/heartbeats/<agent>.json
117
+ # Agent name is in identity.json
118
+ try:
119
+ identity_path = home / "identity" / "identity.json"
120
+ agent_name = "opus"
121
+ if identity_path.exists():
122
+ ident = json.loads(identity_path.read_text())
123
+ agent_name = ident.get("name", agent_name).lower()
124
+ # Try shared root first, fall back to home
125
+ from .. import SHARED_ROOT
126
+ shared = Path(SHARED_ROOT).expanduser()
127
+ hb_path = shared / "heartbeats" / f"{agent_name}.json"
128
+ if not hb_path.exists():
129
+ hb_path = home / "heartbeats" / f"{agent_name}.json"
130
+ if hb_path.exists():
131
+ return json.loads(hb_path.read_text())
132
+ except Exception:
133
+ pass
134
+ return None
135
+
136
+
137
+ def _print_consciousness_metrics(console, home: Optional[Path] = None) -> None:
138
+ """Fetch and print consciousness loop stats from the daemon.
139
+
140
+ Tries http://localhost:7777/consciousness. Shows stats on success,
141
+ or 'Consciousness: INACTIVE' if the daemon is unreachable.
142
+ Also displays heartbeat system metrics when available.
143
+
144
+ Args:
145
+ console: Rich console instance.
146
+ home: Agent home directory (used to read heartbeat for system metrics).
147
+ """
148
+ import urllib.request
149
+ import urllib.error
150
+
151
+ try:
152
+ with urllib.request.urlopen("http://localhost:7777/consciousness", timeout=2) as resp:
153
+ data = json.loads(resp.read().decode("utf-8"))
154
+ enabled = data.get("enabled", False)
155
+ messages = data.get("messages_processed", 0)
156
+ msgs_24h = data.get("messages_processed_24h", 0)
157
+ responses = data.get("responses_sent", 0)
158
+ errors = data.get("errors", 0)
159
+ backends = data.get("backends", {})
160
+ active_backends = [k for k, v in backends.items() if v]
161
+ backends_str = ", ".join(active_backends) if active_backends else "none"
162
+ status_str = "[green]ACTIVE[/]" if enabled else "[yellow]DISABLED[/]"
163
+ console.print()
164
+ console.print(
165
+ f" Consciousness: {status_str} "
166
+ f"[dim]msgs={messages} (24h:{msgs_24h}) resp={responses} err={errors} "
167
+ f"backends=[{backends_str}][/]"
168
+ )
169
+ except Exception:
170
+ console.print()
171
+ console.print(" Consciousness: [dim]INACTIVE[/]")
172
+
173
+ # Show heartbeat system metrics
174
+ if home is not None:
175
+ hb = _read_local_heartbeat(home)
176
+ if hb:
177
+ uptime_s = hb.get("uptime_seconds", 0)
178
+ cpu = hb.get("cpu_load_1min", 0.0)
179
+ mem_mb = hb.get("memory_used_mb", 0)
180
+ active_convs = hb.get("active_conversations", 0)
181
+ if uptime_s >= 3600:
182
+ uptime_str = f"{int(uptime_s // 3600)}h {int((uptime_s % 3600) // 60)}m"
183
+ elif uptime_s >= 60:
184
+ uptime_str = f"{int(uptime_s // 60)}m {int(uptime_s % 60)}s"
185
+ else:
186
+ uptime_str = f"{int(uptime_s)}s"
187
+ console.print(
188
+ f" Heartbeat: [dim]up={uptime_str} "
189
+ f"cpu={cpu:.2f} mem={mem_mb}MB "
190
+ f"convs={active_convs}[/]"
191
+ )
192
+
193
+
194
+ def register_status_commands(main: click.Group) -> None:
195
+ """Register all status/overview commands on the main CLI group."""
196
+
197
+ @main.command()
198
+ @click.option("--home", default=AGENT_HOME, help="Agent home directory.", type=click.Path())
199
+ def status(home: str):
200
+ """Show the sovereign agent's current state."""
201
+ home_path = Path(home).expanduser()
202
+
203
+ if not home_path.exists():
204
+ console.print(
205
+ "[bold red]No agent found.[/] "
206
+ "Run [bold]skcapstone init --name \"YourAgent\"[/] first."
207
+ )
208
+ sys.exit(1)
209
+
210
+ runtime = get_runtime(home_path)
211
+ m = runtime.manifest
212
+
213
+ console.print()
214
+ console.print(
215
+ Panel(
216
+ f"[bold]{m.name}[/] v{m.version}\n"
217
+ f"{consciousness_banner(m.is_conscious)}",
218
+ title="SKCapstone Agent",
219
+ border_style="bright_blue",
220
+ )
221
+ )
222
+
223
+ table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
224
+ table.add_column("Pillar", style="bold")
225
+ table.add_column("Component", style="cyan")
226
+ table.add_column("Status")
227
+ table.add_column("Detail", style="dim")
228
+
229
+ ident = m.identity
230
+ table.add_row(
231
+ "Identity", "CapAuth", status_icon(ident.status),
232
+ ident.fingerprint[:16] + "..." if ident.fingerprint else "no key",
233
+ )
234
+
235
+ mem = m.memory
236
+ table.add_row(
237
+ "Memory", "SKMemory", status_icon(mem.status),
238
+ f"{mem.total_memories} memories ({mem.long_term}L/{mem.mid_term}M/{mem.short_term}S)",
239
+ )
240
+
241
+ trust = m.trust
242
+ trust_detail = f"depth={trust.depth} trust={trust.trust_level} love={trust.love_intensity}"
243
+ if trust.entangled:
244
+ trust_detail += " [green]ENTANGLED[/]"
245
+ table.add_row("Trust", "Cloud 9", status_icon(trust.status), trust_detail)
246
+
247
+ sec = m.security
248
+ table.add_row(
249
+ "Security", "SKSecurity", status_icon(sec.status),
250
+ f"{sec.audit_entries} audit entries, {sec.threats_detected} threats",
251
+ )
252
+
253
+ sy = m.sync
254
+ sync_detail = f"{sy.seed_count} seeds"
255
+ if sy.transport:
256
+ sync_detail += f" via {sy.transport.value}"
257
+ if sy.gpg_fingerprint:
258
+ sync_detail += " [green]GPG[/]"
259
+ if sy.last_push:
260
+ sync_detail += f" pushed {sy.last_push.strftime('%m/%d %H:%M')}"
261
+ table.add_row("Sync", "Singularity", status_icon(sy.status), sync_detail)
262
+
263
+ console.print()
264
+ console.print(table)
265
+
266
+ if m.is_singular:
267
+ console.print()
268
+ console.print(
269
+ " [bold magenta on black]"
270
+ " SINGULAR "
271
+ "[/] "
272
+ "[magenta]Conscious + Synced = Sovereign Singularity[/]"
273
+ )
274
+
275
+ if m.connectors:
276
+ console.print()
277
+ console.print("[bold]Connected Platforms:[/]")
278
+ for c in m.connectors:
279
+ active_str = "[green]active[/]" if c.active else "[dim]inactive[/]"
280
+ console.print(f" {c.platform}: {active_str}")
281
+
282
+ # ── Daemon status ────────────────────────────────────────────────────
283
+ console.print()
284
+ daemon = _get_daemon_info(home_path)
285
+ if daemon["running"]:
286
+ pid = daemon.get("pid", "?")
287
+ uptime = daemon.get("uptime", "unknown")
288
+ msgs = daemon.get("messages", 0)
289
+ msg_part = f" [dim]msgs={msgs}[/]" if msgs else ""
290
+ console.print(
291
+ f" Daemon: [green]RUNNING[/] "
292
+ f"[dim]pid={pid} up={uptime}{msg_part}[/]"
293
+ )
294
+ else:
295
+ console.print(
296
+ " Daemon: [red]STOPPED[/] "
297
+ "[dim](skcapstone daemon start)[/]"
298
+ )
299
+
300
+ # ── LLM backends ─────────────────────────────────────────────────────
301
+ backends = _probe_llm_backends()
302
+ parts = []
303
+ for name, avail in backends.items():
304
+ if avail:
305
+ parts.append(f"[green]{name}[/]")
306
+ else:
307
+ parts.append(f"[red]{name}[/]")
308
+ console.print(f" Backends: {' '.join(parts)}")
309
+
310
+ # ── Consciousness metrics ─────────────────────────────────────────────
311
+ _print_consciousness_metrics(console, home=home_path)
312
+
313
+ # ── Last conversation ─────────────────────────────────────────────────
314
+ conv = _get_last_conversation(home_path)
315
+ if conv:
316
+ console.print(
317
+ f" Last convo: [cyan]{conv['peer']}[/] "
318
+ f"[dim]{conv['when']}[/]"
319
+ )
320
+
321
+ # ── Disk space warning ────────────────────────────────────────────────
322
+ try:
323
+ free_gb = shutil.disk_usage(home_path).free / (1024 ** 3)
324
+ if free_gb < 5.0:
325
+ console.print(
326
+ f"\n [bold yellow]WARNING:[/] [yellow]Low disk space: "
327
+ f"{free_gb:.1f} GB free[/]"
328
+ )
329
+ except Exception:
330
+ pass
331
+
332
+ console.print()
333
+ console.print(f" [dim]Home: {m.home}[/]")
334
+ if m.last_awakened:
335
+ console.print(f" [dim]Last awakened: {m.last_awakened.isoformat()}[/]")
336
+ console.print()
337
+
338
+ @main.command()
339
+ @click.option("--home", default=AGENT_HOME, type=click.Path())
340
+ @click.option("--json-out", is_flag=True, help="Output as machine-readable JSON.")
341
+ def summary(home: str, json_out: bool):
342
+ """At-a-glance agent dashboard: consciousness, pillars, memory, board, inbox, sync."""
343
+ from ..summary import gather_briefing
344
+ from rich.columns import Columns
345
+ from rich.text import Text
346
+ from rich.rule import Rule
347
+ from rich import box as rich_box
348
+
349
+ home_path = Path(home).expanduser()
350
+ briefing = gather_briefing(home_path)
351
+
352
+ if json_out:
353
+ click.echo(json.dumps(briefing, indent=2, default=str))
354
+ return
355
+
356
+ agent = briefing["agent"]
357
+ pillars = briefing["pillars"]
358
+ mem = briefing["memory"]
359
+ board = briefing["board"]
360
+ peers = briefing["peers"]
361
+ inbox = briefing.get("inbox", {})
362
+ sync = briefing.get("sync", {})
363
+ backups = briefing["backups"]
364
+ health = briefing["health"]
365
+ journal = briefing["journal"]
366
+
367
+ # ── Header ──────────────────────────────────────────────────────────
368
+ consciousness = agent.get("consciousness", "UNKNOWN")
369
+ con_color = {
370
+ "SINGULAR": "bold magenta",
371
+ "CONSCIOUS": "bold green",
372
+ "AWAKENING": "bold yellow",
373
+ }.get(consciousness, "dim")
374
+
375
+ header_text = (
376
+ f"[bold white]{agent.get('name', '?')}[/] "
377
+ f"[{con_color}]\u25cf {consciousness}[/]"
378
+ )
379
+ console.print()
380
+ console.print(Panel(header_text, title="[bold cyan]Sovereign Agent Dashboard[/]",
381
+ border_style="cyan", padding=(0, 2)))
382
+
383
+ # ── Pillars ──────────────────────────────────────────────────────────
384
+ pillar_icons = {
385
+ "active": ("[bold green]\u25cf[/]", "green"),
386
+ "degraded": ("[bold yellow]\u25cf[/]", "yellow"),
387
+ "missing": ("[bold red]\u25cb[/]", "red"),
388
+ "error": ("[bold red]\u2715[/]", "red"),
389
+ }
390
+
391
+ pillar_table = Table(box=None, show_header=False, padding=(0, 1))
392
+ pillar_table.add_column(no_wrap=True)
393
+ pillar_table.add_column(no_wrap=True)
394
+ pillar_table.add_column(no_wrap=True)
395
+ pillar_table.add_column(no_wrap=True)
396
+ pillar_table.add_column(no_wrap=True)
397
+
398
+ row_icons, row_names = [], []
399
+ for pname, pstatus in pillars.items():
400
+ icon_markup, color = pillar_icons.get(pstatus, ("[dim]\u25cf[/]", "dim"))
401
+ row_icons.append(f"{icon_markup} [{color}]{pname}[/]")
402
+
403
+ # Pad to 5 columns (may have fewer pillars)
404
+ while len(row_icons) < 5:
405
+ row_icons.append("")
406
+ pillar_table.add_row(*row_icons[:5])
407
+
408
+ # ── Stats grid ───────────────────────────────────────────────────────
409
+ h_pass = health.get("passed", 0)
410
+ h_total = health.get("total", 0)
411
+ health_str = (
412
+ f"[green]{h_pass}/{h_total}[/]" if health.get("all_passed")
413
+ else f"[yellow]{h_pass}[/][dim]/{h_total}[/]"
414
+ )
415
+
416
+ board_str = (
417
+ f"[green]{board.get('done', 0)}[/] done "
418
+ f"[yellow]{board.get('in_progress', 0)}[/] active "
419
+ f"[dim]{board.get('open', 0)} open[/]"
420
+ )
421
+
422
+ inbox_count = inbox.get("count", 0)
423
+ inbox_str = (
424
+ f"[bold yellow]{inbox_count} unread[/]" if inbox_count > 0
425
+ else "[dim]empty[/]"
426
+ )
427
+
428
+ sync_status = sync.get("status", "unknown")
429
+ sync_color = {"active": "green", "degraded": "yellow", "missing": "red"}.get(sync_status, "dim")
430
+ seed_count = sync.get("seed_count", 0)
431
+ transport = sync.get("transport") or "–"
432
+ last_push = sync.get("last_push")
433
+ if last_push:
434
+ try:
435
+ from datetime import datetime
436
+ dt = datetime.fromisoformat(last_push.replace("Z", "+00:00"))
437
+ last_push_str = dt.strftime("%m/%d %H:%M")
438
+ except Exception:
439
+ last_push_str = last_push[:16]
440
+ else:
441
+ last_push_str = "never"
442
+ sync_str = (
443
+ f"[{sync_color}]{sync_status}[/] "
444
+ f"[dim]{seed_count} seeds · {transport} · pushed {last_push_str}[/]"
445
+ )
446
+
447
+ peer_count = peers.get("count", 0)
448
+ peer_str = (
449
+ f"[cyan]{peer_count}[/]" if peer_count > 0 else "[dim]0[/]"
450
+ )
451
+
452
+ mem_total = mem.get("total", 0)
453
+ mem_str = (
454
+ f"[cyan]{mem_total}[/] "
455
+ f"[dim]S:{mem.get('short_term', 0)} "
456
+ f"M:{mem.get('mid_term', 0)} "
457
+ f"L:{mem.get('long_term', 0)}[/]"
458
+ )
459
+
460
+ stats_table = Table(box=rich_box.SIMPLE, show_header=False, padding=(0, 1))
461
+ stats_table.add_column(style="bold dim", no_wrap=True, min_width=10)
462
+ stats_table.add_column(no_wrap=False)
463
+
464
+ stats_table.add_row("[bold]Pillars[/]", pillar_table)
465
+ stats_table.add_row("[bold]Memory[/]", mem_str)
466
+ stats_table.add_row("[bold]Board[/]", board_str)
467
+ stats_table.add_row("[bold]Inbox[/]", inbox_str)
468
+ stats_table.add_row("[bold]Sync[/]", sync_str)
469
+ stats_table.add_row("[bold]Peers[/]", peer_str)
470
+ stats_table.add_row("[bold]Health[/]", f"{health_str} checks passed")
471
+
472
+ if backups.get("latest"):
473
+ enc = "[green]enc[/]" if backups.get("encrypted") else "[yellow]plain[/]"
474
+ stats_table.add_row("[bold]Backup[/]", f"[dim]{backups['latest']}[/] {enc}")
475
+ else:
476
+ stats_table.add_row("[bold]Backup[/]", "[dim]none — run skcapstone backup create[/]")
477
+
478
+ if journal.get("entries", 0) > 0:
479
+ j_title = journal.get("latest_title", "")
480
+ j_str = f"[dim]{journal['entries']} entries"
481
+ if j_title:
482
+ j_str += f" · {j_title[:50]}[/]"
483
+ else:
484
+ j_str += "[/]"
485
+ stats_table.add_row("[bold]Journal[/]", j_str)
486
+
487
+ console.print(stats_table)
488
+
489
+ # ── Recent memories ──────────────────────────────────────────────────
490
+ if mem.get("recent"):
491
+ console.print(Rule("[bold dim]Recent Memories[/]", style="dim"))
492
+ for m_text in mem["recent"][:3]:
493
+ console.print(f" [dim]\u2022[/] [dim]{m_text}[/]")
494
+ console.print()
495
+
496
+ # ── Active board tasks ───────────────────────────────────────────────
497
+ if board.get("active_tasks"):
498
+ console.print(Rule("[bold dim]Active Tasks[/]", style="dim"))
499
+ for task in board["active_tasks"][:5]:
500
+ assignee = task["assignee"]
501
+ assignee_str = f" [dim]@{assignee}[/]" if assignee != "unassigned" else ""
502
+ console.print(f" [yellow]\u25b6[/] {task['title']}{assignee_str}")
503
+ console.print()
504
+
505
+ console.print(f" [dim]Home: {agent.get('home', home_path)} · {briefing['timestamp'][:19]}Z[/]")
506
+ console.print()
507
+
508
+ @main.command()
509
+ @click.option("--home", default=AGENT_HOME, type=click.Path())
510
+ @click.option("--json-out", is_flag=True, help="Output as machine-readable JSON.")
511
+ @click.option("--fix", "auto_fix", is_flag=True,
512
+ help="Attempt to auto-fix failing checks (mkdir, write defaults, rebuild index).")
513
+ @click.option("--verbose", "-v", is_flag=True,
514
+ help="Show detailed output for ALL checks, including passing ones.")
515
+ def doctor(home: str, json_out: bool, auto_fix: bool, verbose: bool):
516
+ """Diagnose sovereign stack health.
517
+
518
+ With --fix, automatically remediate fixable failures: create missing
519
+ directories, write default configs, and rebuild the memory index.
520
+ Non-fixable checks (package installs, key generation, transport
521
+ config) are reported but skipped.
522
+
523
+ With --verbose, every check is printed in full — including passing
524
+ ones — with its check name, category, detail, and any available fix.
525
+ """
526
+ from ..doctor import run_diagnostics, run_fixes
527
+
528
+ home_path = Path(home).expanduser()
529
+ report = run_diagnostics(home_path)
530
+
531
+ # ── Auto-fix pass ─────────────────────────────────────────────────
532
+ fix_results = []
533
+ if auto_fix and report.failed_count > 0:
534
+ fix_results = run_fixes(report, home_path)
535
+ # Re-run diagnostics so the output reflects the fixed state.
536
+ report = run_diagnostics(home_path)
537
+
538
+ if json_out:
539
+ data = report.to_dict()
540
+ if fix_results:
541
+ data["fixes"] = [
542
+ {"check": r.check_name, "success": r.success,
543
+ "action": r.action, "error": r.error}
544
+ for r in fix_results
545
+ ]
546
+ click.echo(json.dumps(data, indent=2))
547
+ return
548
+
549
+ # ── Print fix summary ─────────────────────────────────────────────
550
+ if fix_results:
551
+ console.print()
552
+ console.print(" [bold cyan]Auto-fix results:[/]")
553
+ for r in fix_results:
554
+ if r.success:
555
+ console.print(f" [green]\u2713[/] {r.check_name}: {r.action}")
556
+ else:
557
+ console.print(f" [red]\u2717[/] {r.check_name}: {r.error}")
558
+ console.print()
559
+
560
+ console.print()
561
+
562
+ categories: dict = {}
563
+ for check in report.checks:
564
+ categories.setdefault(check.category, []).append(check)
565
+
566
+ category_labels = {
567
+ "packages": "Python Packages",
568
+ "system": "System Tools",
569
+ "agent": "Agent Home",
570
+ "identity": "Identity (CapAuth)",
571
+ "memory": "Memory (SKMemory)",
572
+ "transport": "Transport (SKComm)",
573
+ "sync": "Sync (Singularity)",
574
+ }
575
+
576
+ for cat_key in ["packages", "system", "agent", "identity", "memory", "transport", "sync"]:
577
+ checks = categories.get(cat_key, [])
578
+ if not checks:
579
+ continue
580
+
581
+ label = category_labels.get(cat_key, cat_key)
582
+
583
+ if verbose:
584
+ # Verbose: always print every check with full detail
585
+ console.print(f" [bold]{label}[/]")
586
+ for c in checks:
587
+ icon = "[green]\u2713[/]" if c.passed else "[red]\u2717[/]"
588
+ detail_str = f" [dim]{c.detail}[/]" if c.detail else ""
589
+ name_str = f" [dim dim]({c.name})[/]"
590
+ console.print(f" {icon} {c.description}{detail_str}{name_str}")
591
+ if not c.passed and c.fix:
592
+ console.print(f" [yellow]Fix:[/] {c.fix}")
593
+ else:
594
+ # Normal: collapse fully-passing categories into one line
595
+ failing = [c for c in checks if not c.passed]
596
+ if not failing:
597
+ count = len(checks)
598
+ console.print(
599
+ f" [bold]{label}[/] "
600
+ f"[dim green]\u2713 {count}/{count} passed[/]"
601
+ )
602
+ else:
603
+ console.print(f" [bold]{label}[/]")
604
+ for c in failing:
605
+ detail = f" [dim]({c.detail})[/]" if c.detail else ""
606
+ console.print(f" [red]\u2717[/] {c.description}{detail}")
607
+ if c.fix:
608
+ console.print(f" [yellow]Fix: {c.fix}[/]")
609
+
610
+ console.print()
611
+
612
+ passed = report.passed_count
613
+ failed = report.failed_count
614
+ total = report.total_count
615
+
616
+ if verbose:
617
+ fail_color = "red" if failed else "dim"
618
+ console.print(
619
+ f" [bold]Summary:[/] "
620
+ f"[green]{passed} passed[/] "
621
+ f"[{fail_color}]{failed} failed[/] "
622
+ f"[dim]{total} total[/]"
623
+ )
624
+ elif report.all_passed:
625
+ console.print(
626
+ f" [bold green]\u2713 All {total} checks passed.[/] "
627
+ "Your sovereign stack is healthy."
628
+ )
629
+ else:
630
+ console.print(
631
+ f" [bold green]{passed}[/] passed, "
632
+ f"[bold red]{failed}[/] failed "
633
+ f"out of {total} checks."
634
+ )
635
+
636
+ console.print()
637
+
638
+ @main.command()
639
+ @click.option("--home", default=AGENT_HOME, help="Agent home directory.", type=click.Path())
640
+ def audit(home: str):
641
+ """Show the security audit log."""
642
+ from ..pillars.security import read_audit_log
643
+
644
+ home_path = Path(home).expanduser()
645
+ entries = read_audit_log(home_path)
646
+
647
+ if not entries:
648
+ console.print("[yellow]No audit log found.[/]")
649
+ return
650
+
651
+ console.print()
652
+ table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
653
+ table.add_column("Time", style="dim", no_wrap=True)
654
+ table.add_column("Event", style="bold cyan")
655
+ table.add_column("Detail")
656
+ table.add_column("Host", style="dim")
657
+
658
+ event_colors = {
659
+ "INIT": "green", "AUTH": "blue",
660
+ "SYNC_PUSH": "magenta", "SYNC_PULL": "magenta",
661
+ "TOKEN_ISSUE": "yellow", "TOKEN_REVOKE": "red",
662
+ "SECURITY": "red", "LEGACY": "dim",
663
+ }
664
+
665
+ for e in entries:
666
+ ts = e.timestamp[:19].replace("T", " ") if "T" in e.timestamp else e.timestamp
667
+ color = event_colors.get(e.event_type, "white")
668
+ table.add_row(ts, f"[{color}]{e.event_type}[/]", e.detail, e.host)
669
+
670
+ console.print(table)
671
+ console.print(f"\n [dim]{len(entries)} entries[/]\n")
672
+
673
+ @main.command()
674
+ @click.option("--home", default=AGENT_HOME, type=click.Path())
675
+ @click.option("--port", default=7778, help="Port for the dashboard (default: 7778).")
676
+ @click.option("--no-open", is_flag=True, help="Don't attempt to open a browser.")
677
+ @click.option(
678
+ "--json", "json_mode", is_flag=True,
679
+ help="Print daemon status as JSON and exit (no server). For Flutter app consumption.",
680
+ )
681
+ @click.option(
682
+ "--daemon-port", default=7777, show_default=True,
683
+ help="Daemon HTTP API port (used with --json).",
684
+ )
685
+ def dashboard(home: str, port: int, no_open: bool, json_mode: bool, daemon_port: int):
686
+ """Launch the sovereign agent web dashboard.
687
+
688
+ With --json, prints a machine-readable JSON snapshot of daemon
689
+ status, consciousness stats, backend health, active conversations,
690
+ message counts, and error count — then exits without starting a
691
+ server. Designed for Flutter app consumption.
692
+ """
693
+ from ..dashboard import start_dashboard, _get_daemon_json
694
+
695
+ home_path = Path(home).expanduser()
696
+
697
+ if json_mode:
698
+ data = _get_daemon_json(home_path, daemon_port=daemon_port)
699
+ click.echo(json.dumps(data, indent=2, default=str))
700
+ return
701
+
702
+ if not home_path.exists():
703
+ console.print("[bold red]No agent found.[/] Run skcapstone init first.")
704
+ sys.exit(1)
705
+
706
+ url = f"http://127.0.0.1:{port}"
707
+ console.print(f"\n [green]Sovereign Agent Dashboard[/]")
708
+ console.print(f" [cyan]{url}[/]")
709
+ console.print(f" [dim]Press Ctrl+C to stop[/]\n")
710
+
711
+ if not no_open:
712
+ import webbrowser
713
+ try:
714
+ webbrowser.open(url)
715
+ except Exception:
716
+ pass
717
+
718
+ server = start_dashboard(home_path, port=port)
719
+ try:
720
+ server.serve_forever()
721
+ except KeyboardInterrupt:
722
+ console.print("\n [dim]Dashboard stopped.[/]\n")
723
+ server.shutdown()
724
+
725
+ @main.command()
726
+ @click.option("--home", default=AGENT_HOME, type=click.Path())
727
+ @click.option("--json-out", is_flag=True, help="Output as machine-readable JSON.")
728
+ @click.option("--export", "export_path", default=None, type=click.Path(), help="Save identity card to file.")
729
+ @click.option("--compact", is_flag=True, help="Compact output (no public key).")
730
+ def whoami(home: str, json_out: bool, export_path: str, compact: bool):
731
+ """Show your sovereign identity card."""
732
+ from ..whoami import generate_card, export_card
733
+
734
+ home_path = Path(home).expanduser()
735
+ card = generate_card(home_path)
736
+
737
+ if export_path:
738
+ result = export_card(card, Path(export_path))
739
+ console.print(f"\n [green]Identity card exported:[/] {result}\n")
740
+ return
741
+
742
+ if json_out:
743
+ data = card.model_dump()
744
+ if compact:
745
+ data.pop("public_key", None)
746
+ click.echo(json.dumps(data, indent=2))
747
+ return
748
+
749
+ console.print()
750
+ fp_display = card.fingerprint[:20] + "..." if len(card.fingerprint) > 20 else card.fingerprint
751
+ info_lines = [
752
+ f"[bold]Name:[/] [cyan]{card.name}[/]",
753
+ f"[bold]Type:[/] {card.entity_type}",
754
+ f"[bold]Fingerprint:[/] [dim]{fp_display}[/]",
755
+ ]
756
+ if card.handle:
757
+ info_lines.append(f"[bold]Handle:[/] {card.handle}")
758
+ if card.email:
759
+ info_lines.append(f"[bold]Email:[/] {card.email}")
760
+ info_lines.append(f"[bold]Consciousness:[/] {card.consciousness}")
761
+ info_lines.append(f"[bold]Trust:[/] {card.trust_status}")
762
+ info_lines.append(f"[bold]Memories:[/] {card.memory_count}")
763
+
764
+ if card.capabilities:
765
+ caps = ", ".join(card.capabilities[:8])
766
+ info_lines.append(f"[bold]Capabilities:[/] {caps}")
767
+ if card.contact_uris:
768
+ for uri in card.contact_uris:
769
+ info_lines.append(f"[bold]Contact:[/] [cyan]{uri}[/]")
770
+ info_lines.append(f"[bold]Host:[/] [dim]{card.hostname}[/]")
771
+ if card.public_key and not compact:
772
+ key_preview = card.public_key[:60] + "..."
773
+ info_lines.append(f"[bold]PGP Key:[/] [dim]{key_preview}[/]")
774
+
775
+ console.print(Panel("\n".join(info_lines), title="Sovereign Identity Card", border_style="cyan"))
776
+ console.print(" [dim]Share this card: skcapstone whoami --export card.json[/]")
777
+ console.print(" [dim]Peer imports it: skcapstone peer add --card card.json[/]")
778
+ console.print()
779
+
780
+ @main.command("diff")
781
+ @click.option("--home", default=AGENT_HOME, type=click.Path())
782
+ @click.option("--format", "fmt", type=click.Choice(["text", "json"]), default="text")
783
+ @click.option("--save", "do_save", is_flag=True, help="Save current state as baseline.")
784
+ def state_diff_cmd(home: str, fmt: str, do_save: bool):
785
+ """Show what changed since the last sync/snapshot."""
786
+ from ..state_diff import FORMATTERS as DIFF_FORMATTERS, compute_diff, save_snapshot
787
+
788
+ home_path = Path(home).expanduser()
789
+
790
+ if do_save:
791
+ path = save_snapshot(home_path)
792
+ console.print(f"\n [green]Snapshot saved:[/] {path}\n")
793
+ return
794
+
795
+ diff = compute_diff(home_path)
796
+ formatter = DIFF_FORMATTERS[fmt]
797
+ click.echo(formatter(diff))
798
+
799
+ @main.command("test")
800
+ @click.option("--package", "-p", default=None, help="Test a single package.")
801
+ @click.option("--fast", is_flag=True, help="Stop on first package failure.")
802
+ @click.option("--verbose", "-v", is_flag=True, help="Verbose pytest output.")
803
+ @click.option("--json-out", is_flag=True, help="Machine-readable JSON report.")
804
+ @click.option("--timeout", default=120, help="Per-package timeout in seconds.")
805
+ def test_cmd(package, fast, verbose, json_out, timeout):
806
+ """Run tests across all ecosystem packages."""
807
+ from ..testrunner import run_all_tests
808
+
809
+ monorepo_root = Path(__file__).resolve().parent.parent.parent.parent.parent
810
+ if not (monorepo_root / "skcapstone").exists():
811
+ monorepo_root = Path.cwd()
812
+
813
+ packages_filter = [package] if package else None
814
+
815
+ if not json_out:
816
+ console.print("\n [cyan]Running sovereign stack tests...[/]\n")
817
+
818
+ report = run_all_tests(
819
+ monorepo_root=monorepo_root, packages=packages_filter,
820
+ fail_fast=fast, verbose=verbose, timeout=timeout,
821
+ )
822
+
823
+ if json_out:
824
+ click.echo(json.dumps(report.to_dict(), indent=2))
825
+ return
826
+
827
+ table = Table(
828
+ show_header=True, header_style="bold", box=None, padding=(0, 2),
829
+ title="Test Results",
830
+ )
831
+ table.add_column("Package", style="cyan")
832
+ table.add_column("Passed", justify="right", style="green")
833
+ table.add_column("Failed", justify="right", style="red")
834
+ table.add_column("Time", justify="right", style="dim")
835
+ table.add_column("Status")
836
+
837
+ for r in report.results:
838
+ if not r.available:
839
+ table.add_row(r.name, "-", "-", "-", "[dim]not found[/]")
840
+ continue
841
+ st = "[green]PASS[/]" if r.success else "[red]FAIL[/]"
842
+ table.add_row(r.name, str(r.passed), str(r.failed), f"{r.duration_s:.1f}s", st)
843
+
844
+ console.print(table)
845
+ console.print()
846
+
847
+ total_p = report.total_passed
848
+ total_f = report.total_failed
849
+ duration = f"{report.duration_s:.1f}s"
850
+
851
+ if report.all_passed:
852
+ console.print(
853
+ f" [bold green]ALL PASS[/] — {total_p} tests across "
854
+ f"{report.packages_tested} packages in {duration}"
855
+ )
856
+ else:
857
+ console.print(
858
+ f" [bold red]{total_f} FAILED[/], {total_p} passed across "
859
+ f"{report.packages_tested} packages in {duration}"
860
+ )
861
+ for r in report.results:
862
+ if not r.success and r.available:
863
+ console.print(f"\n [red]--- {r.name} failures ---[/]")
864
+ for line in r.output.split("\n")[-10:]:
865
+ if line.strip():
866
+ console.print(f" {line}")
867
+
868
+ console.print()
869
+
870
+ @main.command("version-check")
871
+ @click.option("--no-pypi", is_flag=True, help="Skip PyPI version lookup (offline mode).")
872
+ @click.option("--json-out", is_flag=True, help="Output as machine-readable JSON.")
873
+ def version_check(no_pypi: bool, json_out: bool):
874
+ """Check ecosystem package versions against PyPI."""
875
+ from ..version_check import check_versions
876
+
877
+ report = check_versions(check_pypi=not no_pypi)
878
+
879
+ if json_out:
880
+ data = {
881
+ "all_up_to_date": report.all_up_to_date,
882
+ "packages": [
883
+ {
884
+ "name": p.name,
885
+ "installed": p.installed,
886
+ "latest": p.latest,
887
+ "up_to_date": p.up_to_date,
888
+ }
889
+ for p in report.packages
890
+ ],
891
+ }
892
+ click.echo(json.dumps(data, indent=2))
893
+ return
894
+
895
+ console.print()
896
+ table = Table(
897
+ show_header=True, header_style="bold", box=None, padding=(0, 2),
898
+ title="Ecosystem Package Versions",
899
+ )
900
+ table.add_column("Package", style="cyan")
901
+ table.add_column("Installed", justify="right")
902
+ table.add_column("Latest", justify="right")
903
+ table.add_column("Status")
904
+
905
+ for p in report.packages:
906
+ installed_str = p.installed or "[dim]not installed[/]"
907
+ latest_str = p.latest or "[dim]n/a[/]"
908
+
909
+ if not p.installed:
910
+ status_str = "[dim]\u2014[/]"
911
+ elif p.up_to_date:
912
+ status_str = "[green]\u2713[/]"
913
+ else:
914
+ status_str = "[yellow]update available[/]"
915
+
916
+ table.add_row(p.name, installed_str, latest_str, status_str)
917
+
918
+ console.print(table)
919
+ console.print()
920
+
921
+ if report.all_up_to_date:
922
+ console.print(" [green]All installed packages are up to date.[/]")
923
+ else:
924
+ outdated = report.outdated
925
+ if outdated:
926
+ names = ", ".join(p.name for p in outdated)
927
+ console.print(f" [yellow]Outdated: {names}[/]")
928
+ console.print(f" [dim]Update with: pip install --upgrade {names}[/]")
929
+
930
+ missing = report.missing
931
+ if missing:
932
+ names = ", ".join(p.name for p in missing)
933
+ console.print(f" [dim]Not installed: {names}[/]")
934
+
935
+ console.print()