@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,241 @@
1
+ """SK* suite registration orchestrator.
2
+
3
+ Single source of truth for all SK* packages and their MCP servers.
4
+ Called by `skcapstone register` and wired into `skcapstone update`.
5
+
6
+ Usage:
7
+ from skcapstone.register import register_all
8
+
9
+ results = register_all() # auto-detect everything
10
+ results = register_all(dry_run=True) # show what would happen
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import importlib.resources
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ from skmemory.register import detect_environments, register_package
20
+
21
+
22
+ # ── Helpers ──────────────────────────────────────────────────────────────────
23
+
24
+
25
+ def _get_skgit_env() -> Optional[dict]:
26
+ """Read skgit token from config file if available."""
27
+ token_path = Path.home() / ".config" / "skgit" / "token"
28
+ try:
29
+ token = token_path.read_text().strip()
30
+ except (FileNotFoundError, PermissionError):
31
+ token = ""
32
+
33
+ if not token:
34
+ return None
35
+
36
+ return {
37
+ "GITEA_HOST": "https://skgit.skstack01.douno.it",
38
+ "GITEA_ACCESS_TOKEN": token,
39
+ }
40
+
41
+
42
+ # ── SK* package registry ─────────────────────────────────────────────────────
43
+
44
+
45
+ def _build_package_registry(workspace: Optional[Path] = None) -> list[dict]:
46
+ """Build the SK* package registry with resolved env vars and plugin paths."""
47
+ if workspace is None:
48
+ workspace = Path.home() / "clawd"
49
+
50
+ return [
51
+ {
52
+ "name": "skmemory",
53
+ "mcp_cmd": "skmemory-mcp",
54
+ "mcp_args": [],
55
+ "mcp_env": None,
56
+ "openclaw_plugin_path": workspace / "pillar-repos" / "skmemory" / "openclaw-plugin" / "src" / "index.ts",
57
+ },
58
+ {
59
+ "name": "skcapstone",
60
+ "mcp_cmd": "skcapstone-mcp",
61
+ "mcp_args": [],
62
+ "mcp_env": None,
63
+ "openclaw_plugin_path": workspace / "skcapstone" / "openclaw-plugin" / "src" / "index.ts",
64
+ },
65
+ {
66
+ "name": "skcomm",
67
+ "mcp_cmd": "skcomm-mcp",
68
+ "mcp_args": [],
69
+ "mcp_env": None,
70
+ "openclaw_plugin_path": workspace / "pillar-repos" / "skcomm" / "openclaw-plugin" / "src" / "index.ts",
71
+ },
72
+ {
73
+ "name": "skchat",
74
+ "mcp_cmd": "skchat-mcp",
75
+ "mcp_args": [],
76
+ "mcp_env": None,
77
+ "openclaw_plugin_path": workspace / "pillar-repos" / "skchat" / "openclaw-plugin" / "src" / "index.ts",
78
+ },
79
+ {
80
+ "name": "capauth",
81
+ "mcp_cmd": None,
82
+ "mcp_args": None,
83
+ "mcp_env": None,
84
+ "openclaw_plugin_path": workspace / "pillar-repos" / "capauth" / "openclaw-plugin" / "src" / "index.ts",
85
+ },
86
+ {
87
+ "name": "cloud9",
88
+ "mcp_cmd": None,
89
+ "mcp_args": None,
90
+ "mcp_env": None,
91
+ "openclaw_plugin_path": workspace / "pillar-repos" / "cloud9-python" / "openclaw-plugin" / "src" / "index.ts",
92
+ },
93
+ {
94
+ "name": "sksecurity",
95
+ "mcp_cmd": None,
96
+ "mcp_args": None,
97
+ "mcp_env": None,
98
+ "openclaw_plugin_path": workspace / "pillar-repos" / "sksecurity" / "openclaw-plugin" / "src" / "index.ts",
99
+ },
100
+ {
101
+ "name": "skgit",
102
+ "mcp_cmd": "node",
103
+ "mcp_args": [str(Path.home() / ".npm-global" / "lib" / "node_modules"
104
+ / "forgejo-mcp" / "build" / "index.js")],
105
+ "mcp_env": _get_skgit_env(),
106
+ "openclaw_plugin_path": workspace / "skills" / "skgit" / "openclaw-plugin" / "src" / "index.ts",
107
+ },
108
+ ]
109
+
110
+
111
+ # ── SKILL.md locator ─────────────────────────────────────────────────────────
112
+
113
+ # Mapping from package name to pillar-repo directory name
114
+ _PILLAR_DIR_MAP: dict[str, Optional[str]] = {
115
+ "skmemory": "skmemory",
116
+ "skcapstone": None, # lives in workspace root, not pillar-repos
117
+ "skcomm": "skcomm",
118
+ "skchat": "skchat",
119
+ "capauth": "capauth",
120
+ "cloud9": "cloud9-python",
121
+ "sksecurity": "sksecurity",
122
+ "skgit": None, # skill dir only, no pillar repo
123
+ }
124
+
125
+
126
+ def find_skill_md(pkg_name: str, workspace: Optional[Path] = None) -> Optional[Path]:
127
+ """Locate SKILL.md for a package.
128
+
129
+ Search order:
130
+ 1. Workspace skills directory (~/clawd/skills/<name>/SKILL.md)
131
+ 2. Pillar repos directory (~/clawd/pillar-repos/<dir>/SKILL.md)
132
+ 3. Installed package data (importlib.resources)
133
+
134
+ Args:
135
+ pkg_name: Package name.
136
+ workspace: Workspace root (defaults to ~/clawd/).
137
+
138
+ Returns:
139
+ Path to SKILL.md, or None if not found.
140
+ """
141
+ if workspace is None:
142
+ workspace = Path.home() / "clawd"
143
+
144
+ # 1. Check skills directory (may be a symlink — that's fine)
145
+ skill_path = workspace / "skills" / pkg_name / "SKILL.md"
146
+ if skill_path.exists():
147
+ return skill_path.resolve()
148
+
149
+ # 2. Check pillar repos
150
+ pillar_dir = _PILLAR_DIR_MAP.get(pkg_name)
151
+ if pillar_dir:
152
+ pillar_path = workspace / "pillar-repos" / pillar_dir / "SKILL.md"
153
+ if pillar_path.exists():
154
+ return pillar_path
155
+
156
+ # 2b. Special case: skcapstone lives in workspace root
157
+ if pkg_name == "skcapstone":
158
+ capstone_path = workspace / "skcapstone" / "SKILL.md"
159
+ if capstone_path.exists():
160
+ return capstone_path
161
+
162
+ # 3. Check installed package data
163
+ try:
164
+ pkg_module = pkg_name.replace("-", "_")
165
+ ref = importlib.resources.files(pkg_module) / "SKILL.md"
166
+ if ref is not None:
167
+ with importlib.resources.as_file(ref) as p:
168
+ if p.exists():
169
+ return p
170
+ except (ModuleNotFoundError, TypeError, FileNotFoundError):
171
+ pass
172
+
173
+ return None
174
+
175
+
176
+ # ── Orchestrator ──────────────────────────────────────────────────────────────
177
+
178
+
179
+ def register_all(
180
+ workspace: Optional[Path] = None,
181
+ environments: Optional[list[str]] = None,
182
+ dry_run: bool = False,
183
+ ) -> dict:
184
+ """Register all SK* packages in all detected environments.
185
+
186
+ Args:
187
+ workspace: Workspace root (defaults to ~/clawd/).
188
+ environments: Target environments (auto-detect if None).
189
+ dry_run: If True, only report what would be done.
190
+
191
+ Returns:
192
+ Dict with 'environments' and 'packages' keys.
193
+ """
194
+ if environments is None:
195
+ environments = detect_environments()
196
+
197
+ if workspace is None:
198
+ workspace = Path.home() / "clawd"
199
+
200
+ packages = _build_package_registry(workspace)
201
+
202
+ results: dict = {
203
+ "environments": environments,
204
+ "packages": {},
205
+ }
206
+
207
+ for pkg in packages:
208
+ name = pkg["name"]
209
+ skill_md = find_skill_md(name, workspace)
210
+
211
+ if skill_md is None and not dry_run:
212
+ results["packages"][name] = {
213
+ "skill": {"action": "error", "error": "SKILL.md not found"},
214
+ }
215
+ continue
216
+
217
+ mcp_env = pkg.get("mcp_env")
218
+ mcp_cmd = pkg.get("mcp_cmd")
219
+
220
+ # Skip MCP registration for skgit if no token available
221
+ if name == "skgit" and mcp_env is None:
222
+ mcp_cmd = None
223
+
224
+ # Resolve OpenClaw plugin path — skip if not on disk
225
+ plugin_path = pkg.get("openclaw_plugin_path")
226
+ if plugin_path and not Path(plugin_path).exists():
227
+ plugin_path = None
228
+
229
+ results["packages"][name] = register_package(
230
+ name=name,
231
+ skill_md_path=skill_md or Path("/dev/null"),
232
+ mcp_command=mcp_cmd,
233
+ mcp_args=pkg.get("mcp_args") or [],
234
+ mcp_env=mcp_env,
235
+ openclaw_plugin_path=plugin_path,
236
+ workspace=workspace,
237
+ environments=environments,
238
+ dry_run=dry_run,
239
+ )
240
+
241
+ return results
@@ -0,0 +1,151 @@
1
+ """Skills Registry Client — bridge between skcapstone and the remote skills-registry.
2
+
3
+ Wraps the skskills RemoteRegistry client to provide a clean interface for
4
+ skcapstone MCP tools and CLI commands to interact with the remote
5
+ skills-registry at skills.smilintux.org.
6
+
7
+ This module is the integration point between:
8
+ - skills-registry/ (FastAPI server at skills.smilintux.org/api)
9
+ - skskills.remote.RemoteRegistry (HTTP client)
10
+ - skcapstone discovery.py (local skill discovery)
11
+
12
+ Usage:
13
+ from skcapstone.registry_client import get_registry_client
14
+
15
+ client = get_registry_client()
16
+ if client is not None:
17
+ skills = client.search("syncthing")
18
+ client.install("syncthing-setup", agent="jarvis")
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ import os
25
+ from pathlib import Path
26
+ from typing import Any, Optional
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ DEFAULT_REGISTRY_URL = "https://skills.smilintux.org/api"
31
+
32
+
33
+ class RegistryClient:
34
+ """Thin wrapper around skskills.remote.RemoteRegistry.
35
+
36
+ Provides a stable skcapstone-side API for interacting with the
37
+ remote skills-registry. Handles graceful degradation when the
38
+ skskills package is not installed or the registry is unreachable.
39
+
40
+ Args:
41
+ registry_url: Base URL for the skills registry API.
42
+ """
43
+
44
+ def __init__(self, registry_url: Optional[str] = None) -> None:
45
+ from skskills.remote import RemoteRegistry
46
+
47
+ self._url = registry_url or os.environ.get(
48
+ "SKSKILLS_REGISTRY_URL", DEFAULT_REGISTRY_URL
49
+ )
50
+ self._remote = RemoteRegistry(registry_url=self._url)
51
+
52
+ @property
53
+ def registry_url(self) -> str:
54
+ """The configured registry URL."""
55
+ return self._url
56
+
57
+ def is_available(self) -> bool:
58
+ """Check if the remote registry is reachable.
59
+
60
+ Returns:
61
+ True if the registry responds to a health/index request.
62
+ """
63
+ try:
64
+ self._remote.fetch_index(force=True)
65
+ return True
66
+ except Exception:
67
+ return False
68
+
69
+ def list_skills(self) -> list[dict[str, Any]]:
70
+ """List all skills available in the remote registry.
71
+
72
+ Returns:
73
+ List of skill entry dicts with name, version, description, etc.
74
+ """
75
+ index = self._remote.fetch_index()
76
+ return [s.model_dump() for s in index.skills]
77
+
78
+ def search(self, query: str) -> list[dict[str, Any]]:
79
+ """Search remote skills by name, description, or tags.
80
+
81
+ Args:
82
+ query: Case-insensitive search string.
83
+
84
+ Returns:
85
+ List of matching skill entry dicts.
86
+ """
87
+ results = self._remote.search(query)
88
+ return [s.model_dump() for s in results]
89
+
90
+ def get_skill(self, name: str, version: Optional[str] = None) -> Optional[dict[str, Any]]:
91
+ """Get info about a specific remote skill.
92
+
93
+ Args:
94
+ name: Skill name.
95
+ version: Specific version (latest if None).
96
+
97
+ Returns:
98
+ Skill entry dict or None if not found.
99
+ """
100
+ entry = self._remote.get_skill_info(name, version)
101
+ return entry.model_dump() if entry else None
102
+
103
+ def install(
104
+ self,
105
+ name: str,
106
+ version: Optional[str] = None,
107
+ agent: str = "global",
108
+ force: bool = False,
109
+ ) -> dict[str, Any]:
110
+ """Download and install a skill from the remote registry.
111
+
112
+ Args:
113
+ name: Skill name to install.
114
+ version: Specific version (latest if None).
115
+ agent: Agent namespace for installation.
116
+ force: Overwrite existing installation.
117
+
118
+ Returns:
119
+ Dict with installation metadata.
120
+
121
+ Raises:
122
+ FileNotFoundError: If the skill is not in the registry.
123
+ ValueError: If checksum verification fails.
124
+ """
125
+ installed = self._remote.pull(name, version=version, agent=agent, force=force)
126
+ return {
127
+ "name": installed.manifest.name,
128
+ "version": installed.manifest.version,
129
+ "agent": installed.agent,
130
+ "install_path": installed.install_path,
131
+ "status": installed.status.value,
132
+ }
133
+
134
+
135
+ def get_registry_client(registry_url: Optional[str] = None) -> Optional[RegistryClient]:
136
+ """Get a RegistryClient instance, or None if skskills is not installed.
137
+
138
+ This is the recommended entry point. It catches ImportError from
139
+ skskills and returns None so callers can degrade gracefully.
140
+
141
+ Args:
142
+ registry_url: Override the default registry URL.
143
+
144
+ Returns:
145
+ RegistryClient or None if skskills is not available.
146
+ """
147
+ try:
148
+ return RegistryClient(registry_url=registry_url)
149
+ except ImportError:
150
+ logger.debug("skskills not installed — remote registry unavailable")
151
+ return None
@@ -0,0 +1,194 @@
1
+ """
2
+ Response Cache — TTL-based in-memory cache for LLM responses.
3
+
4
+ Caches responses keyed by (prompt_hash, model_name) with tier-dependent TTLs:
5
+ - FAST tier: 1 hour
6
+ - CODE tier: 24 hours
7
+ - All other tiers: 1 hour (conservative default)
8
+
9
+ Conversation messages (real-time peer exchanges with dynamic context) must be
10
+ excluded at the call site by passing ``skip_cache=True`` to LLMBridge.generate().
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import logging
17
+ import threading
18
+ import time
19
+ from typing import Optional
20
+
21
+ from skcapstone.blueprints.schema import ModelTier
22
+
23
+ logger = logging.getLogger("skcapstone.response_cache")
24
+
25
+ # TTL constants (seconds)
26
+ _TTL_FAST: float = 3600.0 # 1 hour
27
+ _TTL_CODE: float = 86400.0 # 24 hours
28
+ _TTL_DEFAULT: float = 3600.0 # 1 hour fallback for other tiers
29
+
30
+
31
+ def _ttl_for_tier(tier: ModelTier) -> float:
32
+ """Return the cache TTL in seconds for a given model tier.
33
+
34
+ Args:
35
+ tier: The model tier used for the request.
36
+
37
+ Returns:
38
+ TTL in seconds: 3600 for FAST, 86400 for CODE, 3600 for all others.
39
+ """
40
+ if tier == ModelTier.CODE:
41
+ return _TTL_CODE
42
+ return _TTL_FAST # FAST and all others default to 1 hour
43
+
44
+
45
+ def hash_prompt(system_prompt: str, user_message: str) -> str:
46
+ """Compute a deterministic SHA-256 hex digest for a prompt pair.
47
+
48
+ Args:
49
+ system_prompt: The agent's system context string.
50
+ user_message: The incoming user message.
51
+
52
+ Returns:
53
+ 64-character lowercase hex string.
54
+ """
55
+ payload = f"{system_prompt}\x00{user_message}".encode("utf-8")
56
+ return hashlib.sha256(payload).hexdigest()
57
+
58
+
59
+ class _CacheEntry:
60
+ """Internal container for a cached response and its expiry timestamp."""
61
+
62
+ __slots__ = ("response", "expires_at")
63
+
64
+ def __init__(self, response: str, ttl: float) -> None:
65
+ self.response: str = response
66
+ self.expires_at: float = time.monotonic() + ttl
67
+
68
+ def is_alive(self) -> bool:
69
+ """True if this entry has not yet expired."""
70
+ return time.monotonic() < self.expires_at
71
+
72
+
73
+ class ResponseCache:
74
+ """Thread-safe in-memory LLM response cache with per-tier TTLs.
75
+
76
+ Entries are keyed by ``(prompt_hash, model_name)`` and expire automatically
77
+ based on the tier that produced them. A background sweep is *not* run;
78
+ expired entries are evicted lazily on read and during periodic ``evict()``
79
+ calls.
80
+
81
+ Args:
82
+ max_size: Maximum number of entries to keep. Oldest entries are
83
+ dropped when the limit is reached. Defaults to 1024.
84
+ """
85
+
86
+ def __init__(self, max_size: int = 1024) -> None:
87
+ self._max_size = max_size
88
+ self._store: dict[tuple[str, str], _CacheEntry] = {}
89
+ self._lock = threading.Lock()
90
+ self._hits = 0
91
+ self._misses = 0
92
+
93
+ # ------------------------------------------------------------------
94
+ # Public API
95
+ # ------------------------------------------------------------------
96
+
97
+ def get(self, prompt_hash: str, model: str) -> Optional[str]:
98
+ """Retrieve a cached response, returning None on miss or expiry.
99
+
100
+ Args:
101
+ prompt_hash: SHA-256 hex digest from :func:`hash_prompt`.
102
+ model: Concrete model name (e.g. ``"llama3.2"``).
103
+
104
+ Returns:
105
+ Cached response string, or ``None`` if not found / expired.
106
+ """
107
+ key = (prompt_hash, model)
108
+ with self._lock:
109
+ entry = self._store.get(key)
110
+ if entry is None:
111
+ self._misses += 1
112
+ return None
113
+ if not entry.is_alive():
114
+ del self._store[key]
115
+ self._misses += 1
116
+ logger.debug("Cache miss (expired): model=%s", model)
117
+ return None
118
+ self._hits += 1
119
+ logger.debug("Cache hit: model=%s", model)
120
+ return entry.response
121
+
122
+ def put(self, prompt_hash: str, model: str, tier: ModelTier, response: str) -> None:
123
+ """Store a response in the cache.
124
+
125
+ Args:
126
+ prompt_hash: SHA-256 hex digest from :func:`hash_prompt`.
127
+ model: Concrete model name.
128
+ tier: Routing tier — determines the TTL.
129
+ response: LLM response text to cache.
130
+ """
131
+ if not response:
132
+ return
133
+ ttl = _ttl_for_tier(tier)
134
+ key = (prompt_hash, model)
135
+ with self._lock:
136
+ self._store[key] = _CacheEntry(response, ttl)
137
+ if len(self._store) > self._max_size:
138
+ self._evict_locked()
139
+ logger.debug(
140
+ "Cached response: model=%s tier=%s ttl=%.0fs len=%d",
141
+ model, tier.value, ttl, len(response),
142
+ )
143
+
144
+ def evict(self) -> int:
145
+ """Remove all expired entries and return the count removed.
146
+
147
+ Returns:
148
+ Number of entries evicted.
149
+ """
150
+ with self._lock:
151
+ return self._evict_locked()
152
+
153
+ def clear(self) -> None:
154
+ """Remove all entries from the cache."""
155
+ with self._lock:
156
+ self._store.clear()
157
+
158
+ @property
159
+ def size(self) -> int:
160
+ """Current number of entries (including not-yet-evicted expired ones)."""
161
+ with self._lock:
162
+ return len(self._store)
163
+
164
+ @property
165
+ def stats(self) -> dict[str, int]:
166
+ """Return cache statistics: hits, misses, current size."""
167
+ with self._lock:
168
+ return {
169
+ "hits": self._hits,
170
+ "misses": self._misses,
171
+ "size": len(self._store),
172
+ }
173
+
174
+ # ------------------------------------------------------------------
175
+ # Internal helpers
176
+ # ------------------------------------------------------------------
177
+
178
+ def _evict_locked(self) -> int:
179
+ """Evict expired entries. Caller must hold ``self._lock``."""
180
+ dead = [k for k, v in self._store.items() if not v.is_alive()]
181
+ for k in dead:
182
+ del self._store[k]
183
+ if dead:
184
+ logger.debug("Evicted %d expired cache entries", len(dead))
185
+
186
+ # If still over limit after eviction, drop oldest by insertion order
187
+ overflow = len(self._store) - self._max_size
188
+ if overflow > 0:
189
+ keys_to_drop = list(self._store.keys())[:overflow]
190
+ for k in keys_to_drop:
191
+ del self._store[k]
192
+ logger.debug("Dropped %d entries to enforce max_size=%d", overflow, self._max_size)
193
+
194
+ return len(dead)