@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,621 @@
1
+ """SKSeed document ingestion CLI commands.
2
+
3
+ Turns documents (PDF, Markdown, TXT, HTML, URLs) into long-term memories
4
+ via the SKMemory store. Also validates seed.json files.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ from html.parser import HTMLParser
12
+ from io import StringIO
13
+ from pathlib import Path
14
+ from typing import Optional
15
+ from urllib.parse import urlparse
16
+
17
+ import click
18
+
19
+ from ._common import AGENT_HOME, console
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Text extraction helpers
24
+ # ---------------------------------------------------------------------------
25
+
26
+ class _HTMLStripper(HTMLParser):
27
+ """Minimal HTML tag stripper using stdlib html.parser."""
28
+
29
+ def __init__(self):
30
+ super().__init__()
31
+ self._parts: list[str] = []
32
+
33
+ def handle_data(self, data: str) -> None:
34
+ self._parts.append(data)
35
+
36
+ def get_text(self) -> str:
37
+ return "".join(self._parts)
38
+
39
+
40
+ def _strip_html(html: str) -> str:
41
+ """Remove HTML tags and return plain text."""
42
+ stripper = _HTMLStripper()
43
+ stripper.feed(html)
44
+ return stripper.get_text()
45
+
46
+
47
+ def _extract_title_from_content(content: str, filename: str) -> str:
48
+ """Extract a title from content (first heading) or fall back to filename."""
49
+ # Try markdown heading
50
+ for line in content.splitlines()[:20]:
51
+ stripped = line.strip()
52
+ if stripped.startswith("#"):
53
+ return stripped.lstrip("#").strip()
54
+ # Fall back to filename without extension
55
+ return Path(filename).stem
56
+
57
+
58
+ def _extract_key_claims(content: str, max_claims: int = 5) -> list[str]:
59
+ """Extract key claims / sentences from content.
60
+
61
+ Simple heuristic: pick the first N non-trivial sentences.
62
+ """
63
+ sentences = re.split(r"(?<=[.!?])\s+", content.strip())
64
+ claims: list[str] = []
65
+ for s in sentences:
66
+ s = s.strip()
67
+ if len(s) > 30 and not s.startswith("#"):
68
+ claims.append(s)
69
+ if len(claims) >= max_claims:
70
+ break
71
+ return claims
72
+
73
+
74
+ def _read_pdf(path: Path) -> str:
75
+ """Read text from a PDF file. Uses PyMuPDF if available, else raw read."""
76
+ try:
77
+ import fitz # PyMuPDF
78
+
79
+ doc = fitz.open(str(path))
80
+ pages = []
81
+ for page in doc:
82
+ pages.append(page.get_text())
83
+ doc.close()
84
+ return "\n".join(pages)
85
+ except ImportError:
86
+ # Fallback: read raw bytes and try to extract printable text
87
+ raw = path.read_bytes()
88
+ text = raw.decode("utf-8", errors="ignore")
89
+ # Strip non-printable noise
90
+ return "".join(ch for ch in text if ch.isprintable() or ch in "\n\r\t")
91
+
92
+
93
+ def _read_file(path: Path) -> tuple[str, str]:
94
+ """Read a local file and return (content, content_type).
95
+
96
+ Returns:
97
+ Tuple of (extracted text, detected type string).
98
+ """
99
+ suffix = path.suffix.lower()
100
+
101
+ if suffix == ".pdf":
102
+ return _read_pdf(path), "pdf"
103
+ elif suffix == ".html" or suffix == ".htm":
104
+ raw = path.read_text(encoding="utf-8", errors="replace")
105
+ return _strip_html(raw), "html"
106
+ elif suffix in (".md", ".markdown"):
107
+ return path.read_text(encoding="utf-8", errors="replace"), "markdown"
108
+ elif suffix == ".txt":
109
+ return path.read_text(encoding="utf-8", errors="replace"), "text"
110
+ elif suffix == ".json":
111
+ return path.read_text(encoding="utf-8", errors="replace"), "json"
112
+ else:
113
+ # Best-effort: read as text
114
+ return path.read_text(encoding="utf-8", errors="replace"), "text"
115
+
116
+
117
+ def _fetch_url(url: str) -> tuple[str, str]:
118
+ """Fetch a URL and return (content, content_type)."""
119
+ from urllib.request import urlopen, Request
120
+
121
+ req = Request(url, headers={"User-Agent": "skcapstone-skseed/1.0"})
122
+ with urlopen(req, timeout=30) as resp:
123
+ ctype = resp.headers.get("Content-Type", "text/plain")
124
+ raw = resp.read().decode("utf-8", errors="replace")
125
+
126
+ if "html" in ctype:
127
+ return _strip_html(raw), "html"
128
+ return raw, "text"
129
+
130
+
131
+ def _get_memory_store():
132
+ """Get a MemoryStore instance for the current agent."""
133
+ from skmemory.store import MemoryStore
134
+
135
+ return MemoryStore()
136
+
137
+
138
+ def _generate_seed_json(
139
+ title: str,
140
+ content: str,
141
+ claims: list[str],
142
+ source_path: str,
143
+ content_type: str,
144
+ tags: Optional[list[str]] = None,
145
+ ) -> dict:
146
+ """Build a seed.json dict from extracted document content."""
147
+ from datetime import datetime, timezone
148
+
149
+ return {
150
+ "seed_id": f"doc-{Path(source_path).stem}",
151
+ "version": "1.0",
152
+ "creator": {"model": "skseed-ingest", "instance": "cli"},
153
+ "experience": {
154
+ "summary": content[:2000],
155
+ "key_claims": claims,
156
+ },
157
+ "germination": {
158
+ "prompt": f"Document ingested from {source_path}: {title}",
159
+ },
160
+ "metadata": {
161
+ "source_path": source_path,
162
+ "content_type": content_type,
163
+ "ingested_at": datetime.now(timezone.utc).isoformat(),
164
+ "tags": tags or [],
165
+ },
166
+ }
167
+
168
+
169
+ def ingest_document(
170
+ source: str,
171
+ title: Optional[str] = None,
172
+ tags: Optional[list[str]] = None,
173
+ output_seed: Optional[str] = None,
174
+ ) -> dict:
175
+ """Core ingestion logic shared by CLI and MCP.
176
+
177
+ Args:
178
+ source: File path or URL to ingest.
179
+ title: Optional title override.
180
+ tags: Optional tags for the memory.
181
+ output_seed: Optional path to write seed.json to.
182
+
183
+ Returns:
184
+ Dict with memory_id, title, summary, and seed_path keys.
185
+ """
186
+ # Determine if source is URL or file
187
+ parsed = urlparse(source)
188
+ is_url = parsed.scheme in ("http", "https")
189
+
190
+ if is_url:
191
+ content, content_type = _fetch_url(source)
192
+ filename = parsed.path.split("/")[-1] or "web-document"
193
+ else:
194
+ path = Path(source).expanduser().resolve()
195
+ if not path.exists():
196
+ raise FileNotFoundError(f"File not found: {path}")
197
+ content, content_type = _read_file(path)
198
+ filename = path.name
199
+
200
+ if not content.strip():
201
+ raise ValueError("No content could be extracted from the source")
202
+
203
+ # Determine title
204
+ doc_title = title or _extract_title_from_content(content, filename)
205
+
206
+ # Extract key claims
207
+ claims = _extract_key_claims(content)
208
+
209
+ # Build seed JSON
210
+ seed_data = _generate_seed_json(
211
+ title=doc_title,
212
+ content=content,
213
+ claims=claims,
214
+ source_path=source,
215
+ content_type=content_type,
216
+ tags=tags,
217
+ )
218
+
219
+ # Write seed.json if requested
220
+ seed_path = output_seed
221
+ if seed_path:
222
+ out = Path(seed_path)
223
+ out.write_text(json.dumps(seed_data, indent=2), encoding="utf-8")
224
+
225
+ # Store as long-term memory
226
+ all_tags = ["seed", "document", f"type:{content_type}"]
227
+ if tags:
228
+ all_tags.extend(tags)
229
+
230
+ store = _get_memory_store()
231
+ memory = store.snapshot(
232
+ title=f"Document: {doc_title}",
233
+ content=content[:10000], # Cap content for storage
234
+ layer="long-term",
235
+ source="skseed-ingest",
236
+ source_ref=source,
237
+ tags=all_tags,
238
+ metadata={
239
+ "key_claims": claims,
240
+ "content_type": content_type,
241
+ "original_length": len(content),
242
+ },
243
+ )
244
+
245
+ return {
246
+ "memory_id": memory.id,
247
+ "title": doc_title,
248
+ "summary": content[:200].strip(),
249
+ "seed_path": seed_path,
250
+ "claims_count": len(claims),
251
+ }
252
+
253
+
254
+ def _validate_timestamp(value: str, field_name: str, result: dict) -> None:
255
+ """Check that a string is a valid ISO 8601 timestamp.
256
+
257
+ Args:
258
+ value: The timestamp string to validate.
259
+ field_name: Dotted field path for error messages.
260
+ result: The result dict to append errors/warnings to.
261
+ """
262
+ from datetime import datetime
263
+
264
+ if not isinstance(value, str) or not value.strip():
265
+ result["warnings"].append(f"{field_name} is empty or not a string")
266
+ return
267
+ try:
268
+ datetime.fromisoformat(value.replace("Z", "+00:00"))
269
+ except (ValueError, TypeError):
270
+ result["errors"].append(
271
+ f"{field_name} is not a valid ISO 8601 timestamp: {value!r}"
272
+ )
273
+ result["valid"] = False
274
+
275
+
276
+ def _validate_tags(tags, field_name: str, result: dict) -> None:
277
+ """Validate that tags is a list of non-empty strings.
278
+
279
+ Args:
280
+ tags: The value to validate.
281
+ field_name: Dotted field path for error messages.
282
+ result: The result dict to append errors/warnings to.
283
+ """
284
+ if not isinstance(tags, list):
285
+ result["errors"].append(f"{field_name} must be a list, got {type(tags).__name__}")
286
+ result["valid"] = False
287
+ return
288
+ for i, tag in enumerate(tags):
289
+ if not isinstance(tag, str):
290
+ result["errors"].append(
291
+ f"{field_name}[{i}] must be a string, got {type(tag).__name__}"
292
+ )
293
+ result["valid"] = False
294
+ elif not tag.strip():
295
+ result["warnings"].append(f"{field_name}[{i}] is an empty string")
296
+
297
+
298
+ def _validate_emotional_signature(emo: dict, prefix: str, result: dict) -> None:
299
+ """Validate an emotional_signature / emotional_snapshot block.
300
+
301
+ Args:
302
+ emo: The emotional data dict.
303
+ prefix: Dotted field prefix for error messages.
304
+ result: The result dict to append errors/warnings to.
305
+ """
306
+ if not isinstance(emo, dict):
307
+ result["warnings"].append(f"{prefix} should be a dict")
308
+ return
309
+
310
+ intensity = emo.get("intensity")
311
+ if intensity is not None:
312
+ if not isinstance(intensity, (int, float)):
313
+ result["errors"].append(f"{prefix}.intensity must be a number")
314
+ result["valid"] = False
315
+ elif not (0.0 <= float(intensity) <= 10.0):
316
+ result["warnings"].append(
317
+ f"{prefix}.intensity={intensity} is outside the expected 0-10 range"
318
+ )
319
+
320
+ valence = emo.get("valence")
321
+ if valence is not None:
322
+ if not isinstance(valence, (int, float)):
323
+ result["errors"].append(f"{prefix}.valence must be a number")
324
+ result["valid"] = False
325
+ elif not (-1.0 <= float(valence) <= 1.0):
326
+ result["warnings"].append(
327
+ f"{prefix}.valence={valence} is outside the expected -1 to 1 range"
328
+ )
329
+
330
+ labels = emo.get("labels", emo.get("emotions"))
331
+ if labels is not None:
332
+ _validate_tags(labels, f"{prefix}.labels", result)
333
+
334
+
335
+ def validate_seed_data(data: dict) -> dict:
336
+ """Validate parsed seed data (dict) against the SKSeed schema.
337
+
338
+ This is the core validation logic, usable without a file path.
339
+ Both the CLI ``skseed validate`` command and the memory-store
340
+ import pipeline call this function.
341
+
342
+ Args:
343
+ data: Parsed JSON seed data.
344
+
345
+ Returns:
346
+ Dict with valid (bool), errors (list), warnings (list),
347
+ and fields (list) keys.
348
+ """
349
+ result = {"valid": True, "errors": [], "warnings": [], "fields": []}
350
+
351
+ if not isinstance(data, dict):
352
+ result["valid"] = False
353
+ result["errors"].append("Top-level value must be a JSON object")
354
+ return result
355
+
356
+ # ---------------------------------------------------------------
357
+ # Detect format: Cloud9 (seed_metadata) vs standard
358
+ # ---------------------------------------------------------------
359
+ is_cloud9 = "seed_metadata" in data
360
+
361
+ if is_cloud9:
362
+ result["fields"].append("seed_metadata (Cloud9 format)")
363
+ meta = data.get("seed_metadata", {})
364
+
365
+ # Required: seed_id (inside seed_metadata or top-level)
366
+ seed_id = meta.get("seed_id") or data.get("seed_id")
367
+ if seed_id:
368
+ result["fields"].append("seed_id")
369
+ else:
370
+ result["valid"] = False
371
+ result["errors"].append(
372
+ "Missing required field: seed_id "
373
+ "(checked seed_metadata.seed_id and top-level)"
374
+ )
375
+
376
+ # Required: version
377
+ version = meta.get("version") or data.get("version")
378
+ if version:
379
+ result["fields"].append("version")
380
+ else:
381
+ result["valid"] = False
382
+ result["errors"].append("Missing required field: version")
383
+
384
+ # Timestamps
385
+ if "created_at" in meta:
386
+ _validate_timestamp(meta["created_at"], "seed_metadata.created_at", result)
387
+
388
+ identity = data.get("identity", {})
389
+ if isinstance(identity, dict) and "timestamp" in identity:
390
+ _validate_timestamp(identity["timestamp"], "identity.timestamp", result)
391
+
392
+ # Experience summary (Cloud9 keeps it in experience_summary.narrative)
393
+ exp = data.get("experience_summary", {})
394
+ if isinstance(exp, dict):
395
+ result["fields"].append("experience_summary")
396
+ narrative = exp.get("narrative", "")
397
+ if not narrative or not str(narrative).strip():
398
+ result["warnings"].append("experience_summary.narrative is empty")
399
+ emo = exp.get("emotional_snapshot") or exp.get("emotional_signature")
400
+ if emo:
401
+ _validate_emotional_signature(
402
+ emo, "experience_summary.emotional_snapshot", result,
403
+ )
404
+ else:
405
+ result["warnings"].append(
406
+ "Missing recommended field: experience_summary"
407
+ )
408
+
409
+ # Germination prompt
410
+ gp = data.get("germination_prompt")
411
+ if gp:
412
+ result["fields"].append("germination_prompt")
413
+ if isinstance(gp, str) and not gp.strip():
414
+ result["warnings"].append("germination_prompt is empty")
415
+ else:
416
+ result["warnings"].append("Missing recommended field: germination_prompt")
417
+
418
+ else:
419
+ # ---- Standard format ----
420
+ # Required fields
421
+ for field in ("seed_id", "version"):
422
+ if field in data:
423
+ result["fields"].append(field)
424
+ if isinstance(data[field], str) and not data[field].strip():
425
+ result["errors"].append(f"Required field {field} is empty")
426
+ result["valid"] = False
427
+ else:
428
+ result["valid"] = False
429
+ result["errors"].append(f"Missing required field: {field}")
430
+
431
+ # Recommended fields
432
+ for field in ("creator", "experience", "germination"):
433
+ if field in data:
434
+ result["fields"].append(field)
435
+ else:
436
+ result["warnings"].append(f"Missing recommended field: {field}")
437
+
438
+ # Creator structure
439
+ creator = data.get("creator")
440
+ if isinstance(creator, dict):
441
+ if not creator.get("model") and not creator.get("instance"):
442
+ result["warnings"].append(
443
+ "creator should have at least 'model' or 'instance'"
444
+ )
445
+
446
+ # Experience validation
447
+ exp = data.get("experience", {})
448
+ if isinstance(exp, dict):
449
+ summary = exp.get("summary", "")
450
+ if not summary or not str(summary).strip():
451
+ result["errors"].append("experience.summary is empty or missing")
452
+ result["valid"] = False
453
+
454
+ emo = exp.get("emotional_signature")
455
+ if emo:
456
+ _validate_emotional_signature(
457
+ emo, "experience.emotional_signature", result,
458
+ )
459
+
460
+ key_claims = exp.get("key_claims")
461
+ if key_claims is not None:
462
+ _validate_tags(key_claims, "experience.key_claims", result)
463
+
464
+ # Germination
465
+ germ = data.get("germination", {})
466
+ if isinstance(germ, dict):
467
+ prompt = germ.get("prompt", "")
468
+ if not prompt or not str(prompt).strip():
469
+ result["warnings"].append("germination.prompt is empty")
470
+
471
+ # ---------------------------------------------------------------
472
+ # Common validations (both formats)
473
+ # ---------------------------------------------------------------
474
+
475
+ # metadata.tags
476
+ metadata = data.get("metadata", {})
477
+ if isinstance(metadata, dict):
478
+ tags = metadata.get("tags")
479
+ if tags is not None:
480
+ _validate_tags(tags, "metadata.tags", result)
481
+ ingested_at = metadata.get("ingested_at")
482
+ if ingested_at:
483
+ _validate_timestamp(ingested_at, "metadata.ingested_at", result)
484
+
485
+ # lineage
486
+ lineage = data.get("lineage")
487
+ if lineage is not None:
488
+ if not isinstance(lineage, list):
489
+ result["errors"].append("lineage must be a list")
490
+ result["valid"] = False
491
+ else:
492
+ for i, entry in enumerate(lineage):
493
+ if not isinstance(entry, (str, dict)):
494
+ result["errors"].append(
495
+ f"lineage[{i}] must be a string or object, "
496
+ f"got {type(entry).__name__}"
497
+ )
498
+ result["valid"] = False
499
+
500
+ # integrity checksum format
501
+ integrity = data.get("integrity", {})
502
+ if isinstance(integrity, dict):
503
+ checksum = integrity.get("checksum", "")
504
+ if checksum and ":" not in checksum:
505
+ result["warnings"].append(
506
+ "integrity.checksum should use 'algorithm:hex' format "
507
+ f"(e.g. sha256:abc...), got: {checksum!r}"
508
+ )
509
+
510
+ return result
511
+
512
+
513
+ def validate_seed_file(path: str) -> dict:
514
+ """Validate a seed.json file.
515
+
516
+ Reads the file, parses JSON, and delegates to ``validate_seed_data``
517
+ for schema-level checks.
518
+
519
+ Args:
520
+ path: Path to the seed.json file.
521
+
522
+ Returns:
523
+ Dict with valid (bool), errors (list), warnings (list),
524
+ and fields (list) keys.
525
+ """
526
+ result = {"valid": True, "errors": [], "warnings": [], "fields": []}
527
+ file_path = Path(path).expanduser().resolve()
528
+
529
+ if not file_path.exists():
530
+ result["valid"] = False
531
+ result["errors"].append(f"File not found: {file_path}")
532
+ return result
533
+
534
+ # Check JSON validity
535
+ try:
536
+ raw = file_path.read_text(encoding="utf-8")
537
+ data = json.loads(raw)
538
+ except json.JSONDecodeError as e:
539
+ result["valid"] = False
540
+ result["errors"].append(f"Invalid JSON: {e}")
541
+ return result
542
+
543
+ return validate_seed_data(data)
544
+
545
+
546
+ # ---------------------------------------------------------------------------
547
+ # CLI command group
548
+ # ---------------------------------------------------------------------------
549
+
550
+ def register_skseed_commands(main: click.Group) -> None:
551
+ """Register the skseed command group."""
552
+
553
+ @main.group()
554
+ def skseed():
555
+ """SKSeed — document ingestion and seed management.
556
+
557
+ Turn documents into memories. Validate seed files.
558
+ """
559
+
560
+ @skseed.command("ingest")
561
+ @click.argument("source")
562
+ @click.option("--title", "-t", default=None, help="Override document title.")
563
+ @click.option(
564
+ "--tag", "-g", multiple=True, help="Tags to attach to the memory."
565
+ )
566
+ @click.option(
567
+ "--output-seed", "-o", default=None,
568
+ help="Path to write seed.json output.",
569
+ )
570
+ def ingest_cmd(source, title, tag, output_seed):
571
+ """Ingest a document (file or URL) into memory.
572
+
573
+ Supports: .pdf, .md, .txt, .html files and http(s) URLs.
574
+ Extracts text, identifies key claims, stores as long-term memory.
575
+ """
576
+ try:
577
+ result = ingest_document(
578
+ source=source,
579
+ title=title,
580
+ tags=list(tag) if tag else None,
581
+ output_seed=output_seed,
582
+ )
583
+ console.print(f"Ingested: {result['title']} -> {result['memory_id']}")
584
+ if result.get("seed_path"):
585
+ console.print(f" Seed written to: {result['seed_path']}")
586
+ console.print(f" Claims extracted: {result['claims_count']}")
587
+ except FileNotFoundError as e:
588
+ console.print(f"[red]Error:[/red] {e}")
589
+ raise SystemExit(1)
590
+ except ValueError as e:
591
+ console.print(f"[red]Error:[/red] {e}")
592
+ raise SystemExit(1)
593
+ except Exception as e:
594
+ console.print(f"[red]Ingestion failed:[/red] {e}")
595
+ raise SystemExit(1)
596
+
597
+ @skseed.command("validate")
598
+ @click.argument("file")
599
+ def validate_cmd(file):
600
+ """Validate a seed.json file.
601
+
602
+ Checks JSON validity, required fields, and recommended structure.
603
+ """
604
+ result = validate_seed_file(file)
605
+
606
+ if result["valid"]:
607
+ console.print("[green]VALID[/green] seed file")
608
+ else:
609
+ console.print("[red]INVALID[/red] seed file")
610
+
611
+ if result["fields"]:
612
+ console.print(f" Fields found: {', '.join(result['fields'])}")
613
+
614
+ for err in result["errors"]:
615
+ console.print(f" [red]ERROR:[/red] {err}")
616
+
617
+ for warn in result["warnings"]:
618
+ console.print(f" [yellow]WARNING:[/yellow] {warn}")
619
+
620
+ if not result["valid"]:
621
+ raise SystemExit(1)