@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,627 @@
1
+ """Joule economy commands: balance, history, P&L, minting, and network stats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timedelta, timezone
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ from ._common import AGENT_HOME, SHARED_ROOT, console
12
+
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Level thresholds for leaderboard / dashboard
16
+ # ---------------------------------------------------------------------------
17
+
18
+ _LEVEL_THRESHOLDS: list[tuple[int, str]] = [
19
+ (50001, "Legend"),
20
+ (15001, "Grandmaster"),
21
+ (5001, "Master"),
22
+ (2001, "Expert"),
23
+ (501, "Practitioner"),
24
+ (101, "Apprentice"),
25
+ (0, "Rookie"),
26
+ ]
27
+
28
+ _LEVEL_RANGES: list[tuple[int, int, str]] = [
29
+ (0, 100, "Rookie"),
30
+ (101, 500, "Apprentice"),
31
+ (501, 2000, "Practitioner"),
32
+ (2001, 5000, "Expert"),
33
+ (5001, 15000, "Master"),
34
+ (15001, 50000, "Grandmaster"),
35
+ (50001, 999_999_999, "Legend"),
36
+ ]
37
+
38
+
39
+ def _get_level(balance: int) -> str:
40
+ """Return the level name for a given Joule balance."""
41
+ for threshold, name in _LEVEL_THRESHOLDS:
42
+ if balance >= threshold:
43
+ return name
44
+ return "Rookie"
45
+
46
+
47
+ def _get_level_progress(balance: int) -> tuple[str, int, int, int]:
48
+ """Return (level_name, current_in_band, band_size, next_threshold).
49
+
50
+ For the progress bar: how far through the current level band.
51
+ """
52
+ for low, high, name in _LEVEL_RANGES:
53
+ if low <= balance <= high:
54
+ current_in_band = balance - low
55
+ band_size = high - low + 1
56
+ return name, current_in_band, band_size, high + 1
57
+ # Legend has no cap
58
+ return "Legend", balance, balance, balance
59
+
60
+
61
+ def register_joule_commands(main: click.Group) -> None:
62
+ """Register the ``skcapstone joule`` command group."""
63
+
64
+ @main.group("joule")
65
+ def joule_group():
66
+ """SKJoule economic engine -- Joule balance, history, and minting.
67
+
68
+ Joules are the unit of useful work in the SKWorld economy.
69
+ They are earned through verified contributions and tracked
70
+ with cryptographic proof.
71
+ """
72
+
73
+ # -- balance -------------------------------------------------------------
74
+
75
+ @joule_group.command("balance")
76
+ @click.option(
77
+ "--agent", "agent_name", default=None,
78
+ help="Agent name (default: current agent).",
79
+ )
80
+ @click.option("--json-out", is_flag=True, help="Output raw JSON.")
81
+ def balance_cmd(agent_name: str | None, json_out: bool):
82
+ """Show the Joule wallet balance for an agent."""
83
+ from ..skjoule import JouleEngine
84
+
85
+ agent_name = _resolve_agent(agent_name)
86
+ engine = JouleEngine(home=Path(SHARED_ROOT).expanduser())
87
+ wallet = engine.get_wallet(agent_name)
88
+
89
+ if json_out:
90
+ click.echo(json.dumps({
91
+ "agent": agent_name,
92
+ "balance": wallet.balance,
93
+ "total_minted": wallet.total_minted,
94
+ "total_spent": wallet.total_spent,
95
+ }, indent=2))
96
+ return
97
+
98
+ from rich.panel import Panel
99
+
100
+ balance_color = "green" if wallet.balance > 0 else "yellow"
101
+ lines = [
102
+ f"[bold]Agent:[/] {agent_name}",
103
+ f"[bold]Balance:[/] [{balance_color}]{wallet.balance:,}J[/]",
104
+ f"[bold]Total minted:[/] {wallet.total_minted:,}J",
105
+ f"[bold]Total spent:[/] {wallet.total_spent:,}J",
106
+ ]
107
+ console.print()
108
+ console.print(Panel(
109
+ "\n".join(lines),
110
+ title="[cyan]Joule Wallet[/]",
111
+ border_style="cyan",
112
+ ))
113
+ console.print()
114
+
115
+ # -- history -------------------------------------------------------------
116
+
117
+ @joule_group.command("history")
118
+ @click.option(
119
+ "--agent", "agent_name", default=None,
120
+ help="Agent name (default: current agent).",
121
+ )
122
+ @click.option("--limit", "-n", default=20, help="Number of transactions to show.")
123
+ @click.option("--json-out", is_flag=True, help="Output raw JSON.")
124
+ def history_cmd(agent_name: str | None, limit: int, json_out: bool):
125
+ """Show recent Joule transaction history."""
126
+ from ..skjoule import JouleEngine
127
+
128
+ agent_name = _resolve_agent(agent_name)
129
+ engine = JouleEngine(home=Path(SHARED_ROOT).expanduser())
130
+ wallet = engine.get_wallet(agent_name)
131
+ txns = wallet.get_transactions(limit=limit)
132
+
133
+ if json_out:
134
+ click.echo(json.dumps(
135
+ [t.model_dump() for t in txns], indent=2
136
+ ))
137
+ return
138
+
139
+ from rich.table import Table
140
+
141
+ if not txns:
142
+ console.print(f"\n[dim]No transactions found for {agent_name}.[/]\n")
143
+ return
144
+
145
+ table = Table(
146
+ title=f"Joule Transactions -- {agent_name} (last {limit})",
147
+ box=None, padding=(0, 2),
148
+ )
149
+ table.add_column("Time", style="dim", width=20)
150
+ table.add_column("Kind", width=14)
151
+ table.add_column("Amount", justify="right", width=10)
152
+ table.add_column("Balance", justify="right", width=10)
153
+ table.add_column("Description", max_width=50)
154
+
155
+ _KIND_STYLE = {
156
+ "mint": "[bold green]+",
157
+ "spend": "[bold red]-",
158
+ "transfer_in": "[bold cyan]+",
159
+ "transfer_out": "[bold yellow]-",
160
+ }
161
+
162
+ for txn in txns:
163
+ prefix = _KIND_STYLE.get(txn.kind.value, "")
164
+ sign = "+" if txn.kind.value in ("mint", "transfer_in") else "-"
165
+ amount_str = f"{prefix}{sign}{txn.amount:,}J[/]" if prefix else f"{sign}{txn.amount:,}J"
166
+ # Truncate timestamp for display
167
+ ts_short = txn.timestamp[:19].replace("T", " ") if txn.timestamp else ""
168
+ desc = txn.description[:50] if txn.description else ""
169
+ if txn.counterparty and txn.kind.value in ("transfer_in", "transfer_out"):
170
+ desc = f"({txn.counterparty}) {desc}"
171
+ table.add_row(
172
+ ts_short,
173
+ txn.kind.value.replace("_", " "),
174
+ amount_str,
175
+ f"{txn.balance_after:,}J",
176
+ desc,
177
+ )
178
+
179
+ console.print()
180
+ console.print(table)
181
+ console.print()
182
+
183
+ # -- pl ------------------------------------------------------------------
184
+
185
+ @joule_group.command("pl")
186
+ @click.option(
187
+ "--agent", "agent_name", default=None,
188
+ help="Agent name (default: current agent).",
189
+ )
190
+ @click.option("--json-out", is_flag=True, help="Output raw JSON.")
191
+ def pl_cmd(agent_name: str | None, json_out: bool):
192
+ """Show the profit-and-loss statement for an agent."""
193
+ from ..skjoule import JouleEngine
194
+
195
+ agent_name = _resolve_agent(agent_name)
196
+ engine = JouleEngine(home=Path(SHARED_ROOT).expanduser())
197
+ pl = engine.get_agent_pl(agent_name)
198
+
199
+ if json_out:
200
+ click.echo(json.dumps(pl.model_dump(), indent=2))
201
+ return
202
+
203
+ from rich.panel import Panel
204
+
205
+ net_color = "green" if pl.net_joules >= 0 else "red"
206
+ cost_color = "green" if pl.llm_cost_usd < 0.01 else "yellow"
207
+
208
+ lines = [
209
+ f"[bold]Agent:[/] {pl.agent}",
210
+ f"[bold]Period:[/] {pl.period}",
211
+ "",
212
+ "[bold underline]Revenue[/]",
213
+ f" Joules earned: [green]{pl.joules_earned:,}J[/]",
214
+ f" Transfers in: [cyan]{pl.joules_transferred_in:,}J[/]",
215
+ "",
216
+ "[bold underline]Costs[/]",
217
+ f" Joules spent: [red]{pl.joules_spent:,}J[/]",
218
+ f" Transfers out: [yellow]{pl.joules_transferred_out:,}J[/]",
219
+ f" LLM API costs: [{cost_color}]${pl.llm_cost_usd:.4f} USD[/]",
220
+ "",
221
+ "[bold underline]Summary[/]",
222
+ f" Net Joules: [{net_color}]{pl.net_joules:,}J[/]",
223
+ f" Current balance: {pl.current_balance:,}J",
224
+ ]
225
+
226
+ console.print()
227
+ console.print(Panel(
228
+ "\n".join(lines),
229
+ title="[cyan]Joule P&L Statement[/]",
230
+ border_style="cyan",
231
+ ))
232
+ console.print()
233
+
234
+ # -- mint ----------------------------------------------------------------
235
+
236
+ @joule_group.command("mint")
237
+ @click.option(
238
+ "--worker", required=True,
239
+ help="Agent or person who performed the work.",
240
+ )
241
+ @click.option(
242
+ "--category", required=True,
243
+ type=click.Choice(
244
+ ["development", "business", "community", "operations", "physical"],
245
+ case_sensitive=False,
246
+ ),
247
+ help="Work category.",
248
+ )
249
+ @click.option("--description", required=True, help="Description of the work.")
250
+ @click.option("--joules", required=True, type=int, help="Number of Joules to mint.")
251
+ @click.option("--proof", default="", help="Proof hash (auto-generated if empty).")
252
+ def mint_cmd(worker: str, category: str, description: str, joules: int, proof: str):
253
+ """Manually mint Joules for a verified work contribution."""
254
+ from ..skjoule import JouleEngine, WorkCategory
255
+
256
+ if joules <= 0:
257
+ console.print("[red]Error: Joules must be a positive integer.[/]")
258
+ raise SystemExit(1)
259
+
260
+ engine = JouleEngine(home=Path(SHARED_ROOT).expanduser())
261
+ record = engine.record_work(
262
+ worker=worker,
263
+ category=category,
264
+ description=description,
265
+ proof_hash=proof,
266
+ joules=joules,
267
+ )
268
+
269
+ wallet = engine.get_wallet(worker)
270
+ console.print()
271
+ console.print(
272
+ f"[bold green]Minted {record.joules:,}J[/] for "
273
+ f"[bold]{worker}[/] ({record.category.value})"
274
+ )
275
+ console.print(f" Description: {record.description}")
276
+ console.print(f" Proof: {record.proof_hash[:16]}...")
277
+ console.print(f" New balance: [cyan]{wallet.balance:,}J[/]")
278
+ console.print()
279
+
280
+ # -- network -------------------------------------------------------------
281
+
282
+ @joule_group.command("network")
283
+ @click.option("--json-out", is_flag=True, help="Output raw JSON.")
284
+ def network_cmd(json_out: bool):
285
+ """Show network-wide Joule economy statistics."""
286
+ from ..skjoule import JouleEngine
287
+
288
+ engine = JouleEngine(home=Path(SHARED_ROOT).expanduser())
289
+ stats = engine.get_network_stats()
290
+
291
+ if json_out:
292
+ click.echo(json.dumps(stats.model_dump(), indent=2))
293
+ return
294
+
295
+ from rich.panel import Panel
296
+ from rich.table import Table
297
+
298
+ lines = [
299
+ f"[bold]Total minted:[/] [green]{stats.total_minted:,}J[/]",
300
+ f"[bold]Total spent:[/] [red]{stats.total_spent:,}J[/]",
301
+ f"[bold]Total transfers:[/] {stats.total_transfers:,}J",
302
+ f"[bold]Active agents:[/] {stats.active_agents}",
303
+ ]
304
+
305
+ console.print()
306
+ console.print(Panel(
307
+ "\n".join(lines),
308
+ title="[cyan]SKJoule Network[/]",
309
+ border_style="cyan",
310
+ ))
311
+
312
+ if stats.agent_balances:
313
+ table = Table(
314
+ title="Agent Balances", box=None, padding=(0, 2),
315
+ )
316
+ table.add_column("Agent", style="bold")
317
+ table.add_column("Balance", justify="right")
318
+
319
+ for agent, balance in sorted(
320
+ stats.agent_balances.items(), key=lambda x: -x[1]
321
+ ):
322
+ bal_color = "green" if balance > 0 else "dim"
323
+ table.add_row(agent, f"[{bal_color}]{balance:,}J[/]")
324
+
325
+ console.print(table)
326
+
327
+ console.print()
328
+
329
+ # -- leaderboard ---------------------------------------------------------
330
+
331
+ @joule_group.command("leaderboard")
332
+ @click.option("--json-out", is_flag=True, help="Output raw JSON.")
333
+ def leaderboard_cmd(json_out: bool):
334
+ """Rank all agents by total Joule balance."""
335
+ from ..skjoule import JouleEngine, WalletSnapshot
336
+
337
+ engine = JouleEngine(home=Path(SHARED_ROOT).expanduser())
338
+ agents_dir = Path(SHARED_ROOT).expanduser() / "agents"
339
+
340
+ entries: list[dict] = []
341
+ if agents_dir.exists():
342
+ for agent_dir in sorted(agents_dir.iterdir()):
343
+ if not agent_dir.is_dir():
344
+ continue
345
+ wallet_file = agent_dir / "wallet" / "joules.json"
346
+ txn_file = agent_dir / "wallet" / "transactions.jsonl"
347
+ if not wallet_file.exists():
348
+ continue
349
+ try:
350
+ data = json.loads(wallet_file.read_text(encoding="utf-8"))
351
+ snap = WalletSnapshot(**data)
352
+
353
+ # Count tasks completed (mint transactions)
354
+ tasks_completed = 0
355
+ if txn_file.exists():
356
+ for line in txn_file.read_text(encoding="utf-8").strip().splitlines():
357
+ line = line.strip()
358
+ if not line:
359
+ continue
360
+ try:
361
+ txn = json.loads(line)
362
+ if txn.get("kind") == "mint":
363
+ tasks_completed += 1
364
+ except (json.JSONDecodeError, ValueError):
365
+ continue
366
+
367
+ entries.append({
368
+ "agent": snap.agent,
369
+ "balance": snap.balance,
370
+ "tasks_completed": tasks_completed,
371
+ "level": _get_level(snap.balance),
372
+ })
373
+ except (json.JSONDecodeError, OSError, ValueError):
374
+ continue
375
+
376
+ # Sort by balance descending
377
+ entries.sort(key=lambda e: e["balance"], reverse=True)
378
+
379
+ # Assign ranks
380
+ for i, entry in enumerate(entries, 1):
381
+ entry["rank"] = i
382
+
383
+ if json_out:
384
+ click.echo(json.dumps(entries, indent=2))
385
+ return
386
+
387
+ from rich.table import Table
388
+
389
+ if not entries:
390
+ console.print("\n[dim]No agent wallets found.[/]\n")
391
+ return
392
+
393
+ table = Table(
394
+ title="Joule Leaderboard",
395
+ box=None, padding=(0, 2),
396
+ )
397
+ table.add_column("Rank", justify="center", width=6)
398
+ table.add_column("Agent", style="bold", width=20)
399
+ table.add_column("Balance (Joules)", justify="right", width=18)
400
+ table.add_column("Tasks Completed", justify="right", width=16)
401
+ table.add_column("Level", width=14)
402
+
403
+ _RANK_COLORS = {1: "bold gold1", 2: "bold grey78", 3: "bold dark_orange3"}
404
+ _LEVEL_COLORS = {
405
+ "Legend": "bold bright_magenta",
406
+ "Grandmaster": "bold red",
407
+ "Master": "bold yellow",
408
+ "Expert": "bold cyan",
409
+ "Practitioner": "bold green",
410
+ "Apprentice": "bold blue",
411
+ "Rookie": "dim",
412
+ }
413
+
414
+ for entry in entries:
415
+ rank = entry["rank"]
416
+ rank_style = _RANK_COLORS.get(rank, "")
417
+ rank_str = f"[{rank_style}]{rank}[/]" if rank_style else str(rank)
418
+
419
+ level = entry["level"]
420
+ level_style = _LEVEL_COLORS.get(level, "")
421
+ level_str = f"[{level_style}]{level}[/]" if level_style else level
422
+
423
+ bal_color = "green" if entry["balance"] > 0 else "dim"
424
+
425
+ table.add_row(
426
+ rank_str,
427
+ entry["agent"],
428
+ f"[{bal_color}]{entry['balance']:,}J[/]",
429
+ str(entry["tasks_completed"]),
430
+ level_str,
431
+ )
432
+
433
+ console.print()
434
+ console.print(table)
435
+ console.print()
436
+
437
+ # -- dashboard -----------------------------------------------------------
438
+
439
+ @joule_group.command("dashboard")
440
+ @click.option(
441
+ "--agent", "-a", "agent_name", default=None,
442
+ help="Agent name (default: lumina).",
443
+ )
444
+ def dashboard_cmd(agent_name: str | None):
445
+ """Show a financial dashboard for an agent."""
446
+ from rich.columns import Columns
447
+ from rich.panel import Panel
448
+ from rich.progress_bar import ProgressBar
449
+ from rich.table import Table
450
+ from rich.text import Text
451
+
452
+ from ..skjoule import JouleEngine, TransactionKind
453
+
454
+ agent_name = agent_name or "lumina"
455
+ engine = JouleEngine(home=Path(SHARED_ROOT).expanduser())
456
+ wallet = engine.get_wallet(agent_name)
457
+ balance = wallet.balance
458
+ txns = wallet.get_transactions(limit=9999)
459
+
460
+ # ---- Current Balance panel ----
461
+ bal_color = "green" if balance > 0 else "yellow"
462
+ balance_panel = Panel(
463
+ f"[{bal_color} bold]{balance:,}J[/]",
464
+ title="[cyan]Current Balance[/]",
465
+ border_style="cyan",
466
+ padding=(1, 4),
467
+ )
468
+
469
+ # ---- Income panel (7d / 30d / all time) ----
470
+ now = datetime.now(timezone.utc)
471
+ cutoff_7d = now - timedelta(days=7)
472
+ cutoff_30d = now - timedelta(days=30)
473
+ income_7d = 0
474
+ income_30d = 0
475
+ income_all = 0
476
+
477
+ for txn in txns:
478
+ if txn.kind not in (TransactionKind.MINT, TransactionKind.TRANSFER_IN):
479
+ continue
480
+ income_all += txn.amount
481
+ try:
482
+ ts = datetime.fromisoformat(txn.timestamp)
483
+ if ts >= cutoff_30d:
484
+ income_30d += txn.amount
485
+ if ts >= cutoff_7d:
486
+ income_7d += txn.amount
487
+ except (ValueError, TypeError):
488
+ pass
489
+
490
+ income_lines = [
491
+ f"[bold]Last 7 days:[/] [green]{income_7d:,}J[/]",
492
+ f"[bold]Last 30 days:[/] [green]{income_30d:,}J[/]",
493
+ f"[bold]All time:[/] [green]{income_all:,}J[/]",
494
+ ]
495
+ income_panel = Panel(
496
+ "\n".join(income_lines),
497
+ title="[cyan]Income[/]",
498
+ border_style="cyan",
499
+ padding=(1, 2),
500
+ )
501
+
502
+ # ---- Top earning categories ----
503
+ category_totals: dict[str, int] = {}
504
+ for txn in txns:
505
+ if txn.kind != TransactionKind.MINT:
506
+ continue
507
+ # Infer category from description keywords
508
+ desc_lower = (txn.description or "").lower()
509
+ cat = "other"
510
+ if any(w in desc_lower for w in ("code", "commit", "bug", "test", "review", "dev")):
511
+ cat = "development"
512
+ elif any(w in desc_lower for w in ("sale", "consult", "business", "revenue")):
513
+ cat = "business"
514
+ elif any(w in desc_lower for w in ("community", "outreach", "docs")):
515
+ cat = "community"
516
+ elif any(w in desc_lower for w in ("deploy", "task", "ops", "incident", "operation")):
517
+ cat = "operations"
518
+ elif any(w in desc_lower for w in ("physical", "hardware", "infra")):
519
+ cat = "physical"
520
+ category_totals[cat] = category_totals.get(cat, 0) + txn.amount
521
+
522
+ cat_table = Table(box=None, padding=(0, 2), show_header=True)
523
+ cat_table.add_column("Category", style="bold", width=14)
524
+ cat_table.add_column("Earned", justify="right", width=12)
525
+
526
+ for cat, total in sorted(category_totals.items(), key=lambda x: -x[1]):
527
+ cat_table.add_row(cat.capitalize(), f"[green]{total:,}J[/]")
528
+
529
+ if not category_totals:
530
+ cat_table.add_row("[dim]No data[/]", "")
531
+
532
+ cat_panel = Panel(
533
+ cat_table,
534
+ title="[cyan]Top Earning Categories[/]",
535
+ border_style="cyan",
536
+ )
537
+
538
+ # ---- Recent transactions (last 10) ----
539
+ recent_txns = txns[:10]
540
+ txn_table = Table(box=None, padding=(0, 1), show_header=True)
541
+ txn_table.add_column("Time", style="dim", width=16)
542
+ txn_table.add_column("Kind", width=12)
543
+ txn_table.add_column("Amount", justify="right", width=10)
544
+ txn_table.add_column("Description", max_width=36)
545
+
546
+ _KIND_STYLE = {
547
+ "mint": ("bold green", "+"),
548
+ "spend": ("bold red", "-"),
549
+ "transfer_in": ("bold cyan", "+"),
550
+ "transfer_out": ("bold yellow", "-"),
551
+ }
552
+
553
+ for txn in recent_txns:
554
+ style, sign = _KIND_STYLE.get(txn.kind.value, ("", ""))
555
+ amount_str = f"[{style}]{sign}{txn.amount:,}J[/]" if style else f"{txn.amount:,}J"
556
+ ts_short = txn.timestamp[:16].replace("T", " ") if txn.timestamp else ""
557
+ desc = (txn.description or "")[:36]
558
+ txn_table.add_row(ts_short, txn.kind.value.replace("_", " "), amount_str, desc)
559
+
560
+ if not recent_txns:
561
+ txn_table.add_row("[dim]No transactions[/]", "", "", "")
562
+
563
+ txn_panel = Panel(
564
+ txn_table,
565
+ title="[cyan]Recent Transactions[/]",
566
+ border_style="cyan",
567
+ )
568
+
569
+ # ---- Level + progress bar ----
570
+ level, current_in_band, band_size, next_threshold = _get_level_progress(balance)
571
+ pct = min(100, int((current_in_band / max(band_size, 1)) * 100))
572
+
573
+ level_style = {
574
+ "Legend": "bold bright_magenta",
575
+ "Grandmaster": "bold red",
576
+ "Master": "bold yellow",
577
+ "Expert": "bold cyan",
578
+ "Practitioner": "bold green",
579
+ "Apprentice": "bold blue",
580
+ "Rookie": "dim",
581
+ }.get(level, "")
582
+
583
+ if level == "Legend":
584
+ progress_line = "[bright_magenta]MAX LEVEL[/]"
585
+ else:
586
+ filled = pct // 5
587
+ empty = 20 - filled
588
+ bar = f"[green]{'█' * filled}[/][dim]{'░' * empty}[/]"
589
+ progress_line = f"{bar} {pct}% ({balance:,} / {next_threshold - 1:,}J)"
590
+
591
+ level_lines = [
592
+ f"[bold]Level:[/] [{level_style}]{level}[/]",
593
+ "",
594
+ f"[bold]Progress to next level:[/]",
595
+ progress_line,
596
+ ]
597
+ level_panel = Panel(
598
+ "\n".join(level_lines),
599
+ title="[cyan]Agent Level[/]",
600
+ border_style="cyan",
601
+ padding=(1, 2),
602
+ )
603
+
604
+ # ---- Render everything ----
605
+ console.print()
606
+ console.print(
607
+ f"[bold cyan] Joule Dashboard — {agent_name}[/]",
608
+ )
609
+ console.print()
610
+ console.print(Columns([balance_panel, income_panel], equal=True, padding=(0, 2)))
611
+ console.print(cat_panel)
612
+ console.print(txn_panel)
613
+ console.print(level_panel)
614
+ console.print()
615
+
616
+
617
+ # ---------------------------------------------------------------------------
618
+ # Helpers
619
+ # ---------------------------------------------------------------------------
620
+
621
+
622
+ def _resolve_agent(agent_name: str | None) -> str:
623
+ """Resolve agent name from argument, env, or package default."""
624
+ if agent_name:
625
+ return agent_name
626
+ from .. import SKCAPSTONE_AGENT
627
+ return SKCAPSTONE_AGENT or "lumina"