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