@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,769 @@
1
+ """
2
+ Interactive agent-to-agent chat for the sovereign terminal.
3
+
4
+ Provides a real-time terminal chat experience between agents using
5
+ SKChat for message models and SKComm for transport. Works from any
6
+ terminal on any platform — no IDE dependency.
7
+
8
+ Usage:
9
+ skcapstone chat <peer> # interactive session (prompt_toolkit)
10
+ skcapstone chat send <peer> <m> # one-shot send
11
+ skcapstone chat inbox # browse messages
12
+ skcapstone chat live <peer> # alias for interactive session
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import json
19
+ import logging
20
+ import threading as _threading
21
+ import time
22
+ import uuid
23
+ from datetime import datetime, timezone
24
+ from pathlib import Path
25
+ from typing import Optional
26
+
27
+ logger = logging.getLogger("skcapstone.chat")
28
+
29
+ # Slash commands available in interactive mode
30
+ _CHAT_COMMANDS = [
31
+ "/attach",
32
+ "/emoji",
33
+ "/exit",
34
+ "/help",
35
+ "/inbox",
36
+ "/q",
37
+ "/quit",
38
+ "/reply",
39
+ "/thread",
40
+ "/whoami",
41
+ ]
42
+
43
+ # Text file extensions that can be sent as UTF-8 (not base64)
44
+ _TEXT_SUFFIXES = {
45
+ ".bash", ".cfg", ".conf", ".css", ".csv", ".env",
46
+ ".go", ".html", ".js", ".json", ".log", ".md",
47
+ ".py", ".rs", ".sh", ".toml", ".ts", ".txt",
48
+ ".xml", ".yaml", ".yml", ".zsh",
49
+ }
50
+
51
+
52
+ class AgentChat:
53
+ """Interactive chat engine for sovereign agent communication.
54
+
55
+ Wraps SKChat models and SKComm transport into a simple
56
+ send/receive/poll interface suitable for terminal use.
57
+
58
+ Args:
59
+ home: Agent home directory (~/.skcapstone).
60
+ identity: Local agent identity string.
61
+ """
62
+
63
+ def __init__(self, home: Path, identity: str = "unknown") -> None:
64
+ self.home = home
65
+ self.identity = identity
66
+ self._comm = None
67
+ self._history = None
68
+
69
+ # ------------------------------------------------------------------
70
+ # Transport / history bootstrap
71
+ # ------------------------------------------------------------------
72
+
73
+ def _ensure_comm(self) -> bool:
74
+ """Lazily initialize the SKComm engine.
75
+
76
+ Returns:
77
+ bool: True if communication layer is available.
78
+ """
79
+ if self._comm is not None:
80
+ return True
81
+
82
+ try:
83
+ from skcomm.core import SKComm
84
+
85
+ self._comm = SKComm.from_config()
86
+ return len(self._comm.router.transports) > 0
87
+ except ImportError:
88
+ logger.info("skcomm not installed")
89
+ return False
90
+ except Exception as exc:
91
+ logger.info("SKComm init failed: %s", exc)
92
+ return False
93
+
94
+ def _ensure_history(self):
95
+ """Lazily initialize the chat history store.
96
+
97
+ Returns:
98
+ ChatHistory or None.
99
+ """
100
+ if self._history is not None:
101
+ return self._history
102
+
103
+ try:
104
+ from skchat.history import ChatHistory
105
+ from skmemory import MemoryStore
106
+
107
+ store = MemoryStore()
108
+ self._history = ChatHistory(store=store)
109
+ return self._history
110
+ except ImportError:
111
+ return None
112
+
113
+ # ------------------------------------------------------------------
114
+ # Core messaging API
115
+ # ------------------------------------------------------------------
116
+
117
+ def send(
118
+ self,
119
+ recipient: str,
120
+ message: str,
121
+ thread_id: Optional[str] = None,
122
+ ) -> dict:
123
+ """Send a message to a peer agent.
124
+
125
+ Stores locally in SKMemory-backed history and delivers via
126
+ SKComm if transports are available.
127
+
128
+ Args:
129
+ recipient: Peer agent name or CapAuth identity.
130
+ message: Message content.
131
+ thread_id: Optional conversation thread.
132
+
133
+ Returns:
134
+ dict: Result with 'stored', 'delivered', 'transport' keys.
135
+ """
136
+ result = {"stored": False, "delivered": False, "transport": None, "error": None}
137
+
138
+ try:
139
+ from skchat.models import ChatMessage, DeliveryStatus
140
+
141
+ msg = ChatMessage(
142
+ sender=self.identity,
143
+ recipient=recipient,
144
+ content=message,
145
+ thread_id=thread_id,
146
+ delivery_status=DeliveryStatus.PENDING,
147
+ )
148
+
149
+ history = self._ensure_history()
150
+ if history:
151
+ history.store_message(msg)
152
+ result["stored"] = True
153
+
154
+ if self._ensure_comm():
155
+ try:
156
+ report = self._comm.send(
157
+ recipient=recipient,
158
+ message=_pack_chat_payload(msg),
159
+ thread_id=thread_id,
160
+ )
161
+ if getattr(report, "delivered", False):
162
+ result["delivered"] = True
163
+ result["transport"] = getattr(report, "successful_transport", None)
164
+ except Exception as exc:
165
+ result["error"] = str(exc)
166
+
167
+ except ImportError as exc:
168
+ result["error"] = f"Missing dependency: {exc}"
169
+
170
+ return result
171
+
172
+ def receive(self, limit: int = 20) -> list[dict]:
173
+ """Poll for incoming messages.
174
+
175
+ Args:
176
+ limit: Maximum messages to return.
177
+
178
+ Returns:
179
+ list[dict]: Received message dicts.
180
+ """
181
+ messages: list[dict] = []
182
+
183
+ if self._ensure_comm():
184
+ try:
185
+ envelopes = self._comm.receive()
186
+ for env in envelopes:
187
+ if hasattr(env, "payload") and hasattr(env.payload, "content"):
188
+ msg_dict = _unpack_chat_payload(
189
+ env.payload.content,
190
+ sender=env.sender,
191
+ recipient=getattr(env, "recipient", self.identity),
192
+ )
193
+ messages.append(msg_dict)
194
+
195
+ history = self._ensure_history()
196
+ if history:
197
+ try:
198
+ from skchat.models import ChatMessage
199
+
200
+ chat_msg = ChatMessage(
201
+ sender=msg_dict["sender"],
202
+ recipient=msg_dict["recipient"],
203
+ content=msg_dict["content"],
204
+ thread_id=msg_dict.get("thread_id"),
205
+ )
206
+ history.store_message(chat_msg)
207
+ except Exception:
208
+ pass
209
+ except Exception as exc:
210
+ logger.warning("Receive error: %s", exc)
211
+
212
+ return messages[:limit]
213
+
214
+ def get_inbox(self, limit: int = 20) -> list[dict]:
215
+ """Get recent messages from local history.
216
+
217
+ Args:
218
+ limit: Maximum messages to return.
219
+
220
+ Returns:
221
+ list[dict]: Message dicts from history.
222
+ """
223
+ history = self._ensure_history()
224
+ if history is None:
225
+ return []
226
+
227
+ try:
228
+ return history.search_messages(self.identity, limit=limit)
229
+ except Exception:
230
+ try:
231
+ memories = history._store.list_memories(
232
+ tags=["skchat:message"],
233
+ limit=limit,
234
+ )
235
+ return [history._memory_to_chat_dict(m) for m in memories]
236
+ except Exception:
237
+ return []
238
+
239
+ def forward(
240
+ self,
241
+ original_msg: dict,
242
+ target_peer: str,
243
+ thread_id: Optional[str] = None,
244
+ ) -> dict:
245
+ """Forward a message to another peer, preserving original sender/timestamp.
246
+
247
+ Wraps the original message in a forward envelope that records the
248
+ original sender and timestamp, then delivers it to target_peer via
249
+ SKComm and stores it locally in history.
250
+
251
+ Args:
252
+ original_msg: Original message dict (from inbox or receive).
253
+ target_peer: Peer agent to forward the message to.
254
+ thread_id: Optional thread ID for the forwarded message.
255
+
256
+ Returns:
257
+ dict: Result with 'stored', 'delivered', 'transport', 'forwarded_id' keys.
258
+ """
259
+ result: dict = {
260
+ "stored": False,
261
+ "delivered": False,
262
+ "transport": None,
263
+ "forwarded_id": None,
264
+ "error": None,
265
+ }
266
+
267
+ fwd_id = str(uuid.uuid4())
268
+ now = datetime.now(timezone.utc).isoformat()
269
+
270
+ payload = json.dumps({
271
+ "skchat_version": "1.0.0",
272
+ "skchat_forward": True,
273
+ "message_id": fwd_id,
274
+ "sender": self.identity,
275
+ "recipient": target_peer,
276
+ "content": original_msg.get("content", ""),
277
+ "thread_id": thread_id,
278
+ "timestamp": now,
279
+ "forwarded_from": original_msg.get("sender", "unknown"),
280
+ "forwarded_at": original_msg.get("timestamp", ""),
281
+ "original_message_id": original_msg.get("message_id", ""),
282
+ })
283
+
284
+ result["forwarded_id"] = fwd_id
285
+
286
+ history = self._ensure_history()
287
+ if history:
288
+ try:
289
+ from skchat.models import ChatMessage, DeliveryStatus
290
+
291
+ fwd_msg = ChatMessage(
292
+ sender=self.identity,
293
+ recipient=target_peer,
294
+ content=payload,
295
+ thread_id=thread_id,
296
+ delivery_status=DeliveryStatus.PENDING,
297
+ )
298
+ history.store_message(fwd_msg)
299
+ result["stored"] = True
300
+ except Exception as exc:
301
+ result["error"] = str(exc)
302
+
303
+ if self._ensure_comm():
304
+ try:
305
+ report = self._comm.send(
306
+ recipient=target_peer,
307
+ message=payload,
308
+ thread_id=thread_id,
309
+ )
310
+ if getattr(report, "delivered", False):
311
+ result["delivered"] = True
312
+ result["transport"] = getattr(report, "successful_transport", None)
313
+ except Exception as exc:
314
+ result["error"] = str(exc)
315
+
316
+ return result
317
+
318
+ # ------------------------------------------------------------------
319
+ # Interactive sessions
320
+ # ------------------------------------------------------------------
321
+
322
+ def interactive_session(
323
+ self,
324
+ peer: str,
325
+ poll_interval: float = 2.0,
326
+ thread_id: Optional[str] = None,
327
+ ) -> None:
328
+ """Run a prompt_toolkit-powered interactive chat session.
329
+
330
+ Features:
331
+ - Rich input with command history, auto-suggest, tab completion
332
+ - Bottom toolbar showing peer, active thread, transport status
333
+ - Background thread polls for incoming messages (non-blocking)
334
+ - File attachments via /attach <path>
335
+ - Thread management via /thread <id> and /reply
336
+ - Emoji support — just type unicode directly
337
+
338
+ Falls back to live_session() if prompt_toolkit is not installed.
339
+
340
+ Args:
341
+ peer: Agent name or CapAuth identity to chat with.
342
+ poll_interval: Seconds between incoming message polls.
343
+ thread_id: Optional starting thread ID.
344
+ """
345
+ try:
346
+ from prompt_toolkit import PromptSession
347
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
348
+ from prompt_toolkit.completion import WordCompleter
349
+ from prompt_toolkit.formatted_text import HTML
350
+ from prompt_toolkit.history import InMemoryHistory
351
+ from prompt_toolkit.patch_stdout import patch_stdout
352
+ from prompt_toolkit.styles import Style
353
+ except ImportError:
354
+ logger.info("prompt_toolkit not installed — falling back to live_session")
355
+ self.live_session(peer, poll_interval=poll_interval)
356
+ return
357
+
358
+ # Mutable state shared with background thread
359
+ state: dict = {
360
+ "thread": thread_id or f"chat-{self.identity}-{peer}-{int(time.time())}",
361
+ "last_recv_thread": None,
362
+ }
363
+ seen_ids: set[str] = set()
364
+ transport_ok = self._ensure_comm()
365
+
366
+ # prompt_toolkit style
367
+ style = Style.from_dict({
368
+ "bottom-toolbar": "bg:#1a1a2e #aaaaaa",
369
+ })
370
+
371
+ def bottom_toolbar() -> HTML:
372
+ conn = "connected" if self._ensure_comm() else "local-only"
373
+ t = state["thread"]
374
+ short_t = (t[:20] + "…") if len(t) > 20 else t
375
+ return HTML(
376
+ f" <b>Sovereign Chat</b> "
377
+ f"peer: <ansicyan>{peer}</ansicyan> "
378
+ f"thread: <ansigreen>{short_t}</ansigreen> "
379
+ f"[{conn}]"
380
+ )
381
+
382
+ session = PromptSession(
383
+ history=InMemoryHistory(),
384
+ auto_suggest=AutoSuggestFromHistory(),
385
+ completer=WordCompleter(_CHAT_COMMANDS, sentence=True, ignore_case=True),
386
+ style=style,
387
+ bottom_toolbar=bottom_toolbar,
388
+ mouse_support=False,
389
+ )
390
+
391
+ # Background polling thread — prints via patch_stdout
392
+ stop_event = _threading.Event()
393
+
394
+ def _poll_loop() -> None:
395
+ while not stop_event.wait(poll_interval):
396
+ try:
397
+ for msg in self.receive(limit=20):
398
+ uid = msg.get("message_id") or msg.get("id") or ""
399
+ if uid and uid in seen_ids:
400
+ continue
401
+ if uid:
402
+ seen_ids.add(uid)
403
+ sender = msg.get("sender", "?")
404
+ if sender == self.identity:
405
+ continue
406
+ content = msg.get("content", "")
407
+ ts = _short_timestamp()
408
+ recv_thread = msg.get("thread_id")
409
+ if recv_thread:
410
+ state["last_recv_thread"] = recv_thread
411
+ display = _format_content(content)
412
+ print(f"\n \033[32m{sender}\033[0m \033[2m[{ts}]\033[0m {display}\n")
413
+ except Exception:
414
+ pass
415
+
416
+ # Print header
417
+ tr_label = "✓ connected" if transport_ok else "✗ local-only"
418
+ t = state["thread"]
419
+ short_t = (t[:30] + "…") if len(t) > 30 else t
420
+ print(f"\n ─── Sovereign Chat ─────────────────────────────")
421
+ print(f" Peer: {peer}")
422
+ print(f" Thread: {short_t}")
423
+ print(f" Transport: {tr_label}")
424
+ print(f" ────────────────────────────────────────────────")
425
+ print(f" Type a message and press Enter to send.")
426
+ print(f" /attach <path> /thread <id> /reply /help /quit\n")
427
+
428
+ with patch_stdout():
429
+ poll_thread = _threading.Thread(target=_poll_loop, daemon=True)
430
+ poll_thread.start()
431
+
432
+ while True:
433
+ try:
434
+ text = session.prompt(f" {self.identity}: ")
435
+ except (EOFError, KeyboardInterrupt):
436
+ break
437
+
438
+ text = text.strip()
439
+ if not text:
440
+ continue
441
+
442
+ low = text.lower()
443
+
444
+ # ── Slash commands ──────────────────────────────────
445
+ if low in ("/quit", "/exit", "/q"):
446
+ break
447
+
448
+ elif low == "/help":
449
+ _print_chat_help()
450
+
451
+ elif low == "/whoami":
452
+ print(f"\n Identity: {self.identity}\n")
453
+
454
+ elif low == "/inbox":
455
+ _print_recent_inbox(self.get_inbox(limit=5))
456
+
457
+ elif low == "/reply":
458
+ lt = state.get("last_recv_thread")
459
+ if lt:
460
+ state["thread"] = lt
461
+ print(f"\n Now replying in thread: {lt}\n")
462
+ else:
463
+ print("\n No received thread to reply to yet.\n")
464
+
465
+ elif low.startswith("/thread "):
466
+ new_t = text[8:].strip()
467
+ if new_t:
468
+ state["thread"] = new_t
469
+ print(f"\n Switched to thread: {new_t}\n")
470
+
471
+ elif low.startswith("/attach "):
472
+ fp = text[8:].strip()
473
+ self._send_attachment(peer, fp, state["thread"])
474
+
475
+ elif low == "/emoji":
476
+ _print_emoji_ref()
477
+
478
+ else:
479
+ # Regular message send
480
+ result = self.send(peer, text, thread_id=state["thread"])
481
+ ts = _short_timestamp()
482
+ if result.get("delivered"):
483
+ via = result.get("transport") or "unknown"
484
+ print(
485
+ f" \033[34m{self.identity}\033[0m \033[2m[{ts}]\033[0m"
486
+ f" {text} \033[2m→ {via}\033[0m"
487
+ )
488
+ elif result.get("stored"):
489
+ print(
490
+ f" \033[34m{self.identity}\033[0m \033[2m[{ts}]\033[0m"
491
+ f" {text} \033[2m→ stored locally\033[0m"
492
+ )
493
+ else:
494
+ err = result.get("error") or "send failed"
495
+ print(f"\n \033[31mError:\033[0m {err}\n")
496
+
497
+ stop_event.set()
498
+ print(f"\n Chat session ended.\n")
499
+
500
+ def live_session(
501
+ self,
502
+ peer: str,
503
+ poll_interval: float = 2.0,
504
+ ) -> None:
505
+ """Run an interactive live chat session in the terminal.
506
+
507
+ Fallback implementation using plain stdin/stdout. Used when
508
+ prompt_toolkit is not installed.
509
+
510
+ Args:
511
+ peer: Agent name or identity to chat with.
512
+ poll_interval: Seconds between inbox polls.
513
+ """
514
+ thread_id = f"live-{self.identity}-{peer}-{int(time.time())}"
515
+
516
+ print(f"\n Sovereign Chat — {self.identity} <-> {peer}")
517
+ print(f" Thread: {thread_id[:20]}...")
518
+ print(f" Transport: {'available' if self._ensure_comm() else 'local-only'}")
519
+ print(f" Type a message and press Enter. Type /quit to exit.\n")
520
+
521
+ try:
522
+ while True:
523
+ incoming = self.receive(limit=10)
524
+ for msg in incoming:
525
+ sender = msg.get("sender", "?")
526
+ content = msg.get("content", "")
527
+ if sender != self.identity:
528
+ ts = _short_timestamp()
529
+ print(f" [{ts}] {sender}: {content}")
530
+
531
+ try:
532
+ user_input = _read_line(f" [{_short_timestamp()}] {self.identity}: ")
533
+ except EOFError:
534
+ break
535
+
536
+ if not user_input:
537
+ continue
538
+ if user_input.strip().lower() in ("/quit", "/exit", "/q"):
539
+ break
540
+
541
+ result = self.send(peer, user_input, thread_id=thread_id)
542
+ if result["delivered"]:
543
+ print(f" -> delivered via {result['transport']}")
544
+ elif result["stored"]:
545
+ print(f" -> stored locally")
546
+ if result.get("error"):
547
+ print(f" -> error: {result['error']}")
548
+
549
+ except KeyboardInterrupt:
550
+ pass
551
+
552
+ print(f"\n Session ended.\n")
553
+
554
+ # ------------------------------------------------------------------
555
+ # File attachment
556
+ # ------------------------------------------------------------------
557
+
558
+ def _send_attachment(self, peer: str, file_path_str: str, thread_id: str) -> None:
559
+ """Send a file as an inline attachment message.
560
+
561
+ Reads the file, encodes it (UTF-8 for text, base64 for binary),
562
+ and sends it as a structured JSON payload. The recipient sees
563
+ a [Attachment: name (size)] preview.
564
+
565
+ Args:
566
+ peer: Recipient agent name.
567
+ file_path_str: Path to the file to send.
568
+ thread_id: Active thread ID.
569
+ """
570
+ path = Path(file_path_str).expanduser().resolve()
571
+ if not path.exists():
572
+ print(f"\n File not found: {path}\n")
573
+ return
574
+ if not path.is_file():
575
+ print(f"\n Not a regular file: {path}\n")
576
+ return
577
+
578
+ size = path.stat().st_size
579
+ if size > 10 * 1024 * 1024: # 10 MB cap for inline transfers
580
+ print(
581
+ f"\n File too large: {size:,} bytes (max 10 MB for inline attachments).\n"
582
+ f" Use skcapstone file send for large transfers.\n"
583
+ )
584
+ return
585
+
586
+ raw = path.read_bytes()
587
+
588
+ if path.suffix.lower() in _TEXT_SUFFIXES:
589
+ try:
590
+ payload = json.dumps({
591
+ "skchat_attachment": True,
592
+ "name": path.name,
593
+ "size": size,
594
+ "encoding": "utf-8",
595
+ "content": raw.decode("utf-8"),
596
+ })
597
+ except UnicodeDecodeError:
598
+ payload = json.dumps({
599
+ "skchat_attachment": True,
600
+ "name": path.name,
601
+ "size": size,
602
+ "encoding": "base64",
603
+ "content": base64.b64encode(raw).decode(),
604
+ })
605
+ else:
606
+ payload = json.dumps({
607
+ "skchat_attachment": True,
608
+ "name": path.name,
609
+ "size": size,
610
+ "encoding": "base64",
611
+ "content": base64.b64encode(raw).decode(),
612
+ })
613
+
614
+ result = self.send(peer, payload, thread_id=thread_id)
615
+ ts = _short_timestamp()
616
+ display = f"[Attachment: {path.name} ({size:,} bytes)]"
617
+ if result.get("delivered") or result.get("stored"):
618
+ via = result.get("transport") or "stored"
619
+ print(
620
+ f" \033[34m{self.identity}\033[0m \033[2m[{ts}]\033[0m"
621
+ f" {display} \033[2m→ {via}\033[0m"
622
+ )
623
+ else:
624
+ print(f"\n Failed to send attachment: {result.get('error', 'unknown')}\n")
625
+
626
+
627
+ # ---------------------------------------------------------------------------
628
+ # Module-level helpers
629
+ # ---------------------------------------------------------------------------
630
+
631
+ def _pack_chat_payload(msg) -> str:
632
+ """Serialize a ChatMessage for SKComm transport.
633
+
634
+ Args:
635
+ msg: ChatMessage instance.
636
+
637
+ Returns:
638
+ str: JSON payload.
639
+ """
640
+ return json.dumps({
641
+ "skchat_version": "1.0.0",
642
+ "message_id": msg.id,
643
+ "sender": msg.sender,
644
+ "recipient": msg.recipient,
645
+ "content": msg.content,
646
+ "thread_id": msg.thread_id,
647
+ "timestamp": msg.timestamp.isoformat(),
648
+ })
649
+
650
+
651
+ def _unpack_chat_payload(payload: str, sender: str, recipient: str) -> dict:
652
+ """Deserialize a chat payload from SKComm.
653
+
654
+ Falls back to plain text if not structured JSON.
655
+
656
+ Args:
657
+ payload: Raw payload string.
658
+ sender: Fallback sender from envelope.
659
+ recipient: Fallback recipient from envelope.
660
+
661
+ Returns:
662
+ dict: Message data.
663
+ """
664
+ try:
665
+ data = json.loads(payload)
666
+ if "skchat_version" in data:
667
+ return {
668
+ "message_id": data.get("message_id", ""),
669
+ "sender": data.get("sender", sender),
670
+ "recipient": data.get("recipient", recipient),
671
+ "content": data.get("content", payload),
672
+ "thread_id": data.get("thread_id"),
673
+ "timestamp": data.get("timestamp"),
674
+ }
675
+ except (json.JSONDecodeError, KeyError):
676
+ pass
677
+
678
+ return {
679
+ "message_id": "",
680
+ "sender": sender,
681
+ "recipient": recipient,
682
+ "content": payload,
683
+ "thread_id": None,
684
+ "timestamp": datetime.now(timezone.utc).isoformat(),
685
+ }
686
+
687
+
688
+ def _format_content(content: str) -> str:
689
+ """Format a message content string for terminal display.
690
+
691
+ Detects inline attachments and renders them as a readable label.
692
+
693
+ Args:
694
+ content: Raw message content.
695
+
696
+ Returns:
697
+ str: Display-ready string.
698
+ """
699
+ try:
700
+ data = json.loads(content)
701
+ if data.get("skchat_attachment"):
702
+ name = data.get("name", "unknown")
703
+ size = data.get("size", 0)
704
+ return f"[Attachment: {name} ({size:,} bytes)]"
705
+ except (json.JSONDecodeError, TypeError, AttributeError):
706
+ pass
707
+ return content
708
+
709
+
710
+ def _short_timestamp() -> str:
711
+ """Get a compact HH:MM:SS timestamp."""
712
+ return datetime.now().strftime("%H:%M:%S")
713
+
714
+
715
+ def _read_line(prompt: str) -> str:
716
+ """Read a line of input from stdin with a prompt.
717
+
718
+ Args:
719
+ prompt: The prompt string to display.
720
+
721
+ Returns:
722
+ str: The user's input, stripped of trailing newline.
723
+ """
724
+ import sys
725
+ sys.stdout.write(prompt)
726
+ sys.stdout.flush()
727
+ return sys.stdin.readline().rstrip("\n")
728
+
729
+
730
+ def _print_chat_help() -> None:
731
+ """Print the chat command reference."""
732
+ print(
733
+ "\n Chat commands:\n"
734
+ " /attach <path> Send a file attachment (max 10 MB)\n"
735
+ " /thread <id> Switch to a different thread ID\n"
736
+ " /reply Switch to the last received thread\n"
737
+ " /inbox Show last 5 inbox messages\n"
738
+ " /whoami Show your agent identity\n"
739
+ " /emoji Emoji quick reference\n"
740
+ " /quit Exit (also /exit or /q)\n"
741
+ "\n Emoji is fully supported — just type directly: 🎉 🚀 ❤️ 🤖\n"
742
+ )
743
+
744
+
745
+ def _print_recent_inbox(messages: list) -> None:
746
+ """Print a short inbox preview."""
747
+ if not messages:
748
+ print("\n No messages in inbox.\n")
749
+ return
750
+ print("\n Recent messages:")
751
+ for m in messages:
752
+ sender = m.get("sender", "?")
753
+ content = m.get("content", "")
754
+ display = _format_content(content)
755
+ preview = (display[:60] + "…") if len(display) > 60 else display
756
+ print(f" \033[36m{sender}\033[0m: {preview}")
757
+ print()
758
+
759
+
760
+ def _print_emoji_ref() -> None:
761
+ """Print an emoji quick-reference card."""
762
+ print(
763
+ "\n Emoji quick reference (type directly — unicode is fully supported):\n"
764
+ " ❤️ 💙 💚 🖤 🤍 — hearts\n"
765
+ " 👍 👎 🙌 🤝 ✌️ — hands\n"
766
+ " 🎉 🚀 🔥 ⚡ ✨ — vibes\n"
767
+ " ✅ ❌ ⚠️ 🔒 🔑 — status\n"
768
+ " 🤖 👾 🧠 🔮 💡 — tech / sovereign\n"
769
+ )