meshcode 2.11.62__tar.gz → 2.11.63__tar.gz

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 (252) hide show
  1. {meshcode-2.11.62 → meshcode-2.11.63}/PKG-INFO +1 -1
  2. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/__init__.py +1 -1
  3. meshcode-2.11.63/meshcode/claude_update.py +253 -0
  4. meshcode-2.11.63/meshcode/hostd.py +381 -0
  5. meshcode-2.11.63/meshcode/up.py +257 -0
  6. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode.egg-info/SOURCES.txt +4 -0
  8. {meshcode-2.11.62 → meshcode-2.11.63}/pyproject.toml +1 -1
  9. meshcode-2.11.63/tests/test_autonomous_prompt_inject.py +126 -0
  10. {meshcode-2.11.62 → meshcode-2.11.63}/README.md +0 -0
  11. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/__main__.py +0 -0
  12. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/_stop_hook_template.py +0 -0
  13. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/ascii_art.py +0 -0
  14. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/atomic_push.py +0 -0
  15. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/claude_update 2.py +0 -0
  16. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/claude_update 3.py +0 -0
  17. /meshcode-2.11.62/meshcode/claude_update.py → /meshcode-2.11.63/meshcode/claude_update 4.py +0 -0
  18. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/cli.py +0 -0
  19. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/comms_v4.py +0 -0
  20. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/compat.py +0 -0
  21. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/daemon.py +0 -0
  22. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/date_parse.py +0 -0
  23. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/doctor.py +0 -0
  24. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/error_hints.py +0 -0
  25. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/exceptions.py +0 -0
  26. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/hostd 2.py +0 -0
  27. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/hostd 3.py +0 -0
  28. /meshcode-2.11.62/meshcode/hostd.py → /meshcode-2.11.63/meshcode/hostd 4.py +0 -0
  29. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/invites.py +0 -0
  30. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/launcher.py +0 -0
  31. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/launcher_install.py +0 -0
  32. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/__init__.py +0 -0
  33. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/__main__.py +0 -0
  34. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/backend.py +0 -0
  35. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/realtime.py +0 -0
  36. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/server.py +0 -0
  37. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  38. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/test_backend.py +0 -0
  39. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  40. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  41. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  42. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  43. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  44. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/preferences.py +0 -0
  45. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/protocol_handler.py +0 -0
  46. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/protocol_v2.py +0 -0
  47. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/quickstart.py +0 -0
  48. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/rpc_allowlist.py +0 -0
  49. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/run_agent.py +0 -0
  50. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/scripts/check_secrets.py +0 -0
  51. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/scripts/race_rate_harness.py +0 -0
  52. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/secrets.py +0 -0
  53. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/self_update.py +0 -0
  54. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/setup_clients.py +0 -0
  55. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/supervisor.py +0 -0
  56. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/up 2.py +0 -0
  57. /meshcode-2.11.62/meshcode/up.py → /meshcode-2.11.63/meshcode/up 3.py +0 -0
  58. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode/upload.py +0 -0
  59. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/comms_v4.py +0 -0
  60. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/__init__.py +0 -0
  61. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/ascii_art.py +0 -0
  62. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/cli.py +0 -0
  63. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/comms_v4.py +0 -0
  64. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/compat.py +0 -0
  65. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/error_hints.py +0 -0
  66. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/exceptions.py +0 -0
  67. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/invites.py +0 -0
  68. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/launcher.py +0 -0
  69. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/launcher_install.py +0 -0
  70. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  71. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  72. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/meshcode_mcp/backend.py +0 -0
  73. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  74. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/meshcode_mcp/server.py +0 -0
  75. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  76. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  77. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  78. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/preferences.py +0 -0
  79. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/protocol_v2.py +0 -0
  80. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/quickstart.py +0 -0
  81. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/run_agent.py +0 -0
  82. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/secrets.py +0 -0
  83. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/self_update.py +0 -0
  84. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/setup_clients.py +0 -0
  85. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/supervisor.py +0 -0
  86. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/meshcode/upload.py +0 -0
  87. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/scripts/sentinel.py +0 -0
  88. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_core.py +0 -0
  89. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_cross_agent_messaging.py +0 -0
  90. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_esc_deaf_state.py +0 -0
  91. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_exceptions.py +0 -0
  92. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_mark_read_batch.py +0 -0
  93. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_migration_integrity.py +0 -0
  94. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_realtime_event_freshness.py +0 -0
  95. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_rls_cross_tenant.py +0 -0
  96. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_rpc_migrations.py +0 -0
  97. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_security_regressions.py +0 -0
  98. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_sentinel.py +0 -0
  99. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-backend-wt/tests/test_status_enum_coverage.py +0 -0
  100. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/__init__.py +0 -0
  101. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/ascii_art.py +0 -0
  102. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/cli.py +0 -0
  103. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/comms_v4.py +0 -0
  104. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/compat.py +0 -0
  105. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/error_hints.py +0 -0
  106. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/exceptions.py +0 -0
  107. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/invites.py +0 -0
  108. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/launcher.py +0 -0
  109. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/launcher_install.py +0 -0
  110. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__init__.py +0 -0
  111. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/__main__.py +0 -0
  112. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/backend.py +0 -0
  113. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/realtime.py +0 -0
  114. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/server.py +0 -0
  115. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_backend.py +0 -0
  116. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_realtime.py +0 -0
  117. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  118. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/preferences.py +0 -0
  119. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/protocol_v2.py +0 -0
  120. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/quickstart.py +0 -0
  121. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/run_agent.py +0 -0
  122. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/secrets.py +0 -0
  123. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/self_update.py +0 -0
  124. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/setup_clients.py +0 -0
  125. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/supervisor.py +0 -0
  126. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/build/lib/meshcode/upload.py +0 -0
  127. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/comms_v4.py +0 -0
  128. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/__init__.py +0 -0
  129. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/ascii_art.py +0 -0
  130. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/cli.py +0 -0
  131. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/comms_v4.py +0 -0
  132. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/compat.py +0 -0
  133. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/error_hints.py +0 -0
  134. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/exceptions.py +0 -0
  135. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/invites.py +0 -0
  136. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/launcher.py +0 -0
  137. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/launcher_install.py +0 -0
  138. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  139. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  140. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/meshcode_mcp/backend.py +0 -0
  141. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  142. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/meshcode_mcp/server.py +0 -0
  143. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  144. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  145. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  146. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/preferences.py +0 -0
  147. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/protocol_v2.py +0 -0
  148. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/quickstart.py +0 -0
  149. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/run_agent.py +0 -0
  150. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/secrets.py +0 -0
  151. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/self_update.py +0 -0
  152. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/setup_clients.py +0 -0
  153. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/supervisor.py +0 -0
  154. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/meshcode/upload.py +0 -0
  155. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/scripts/sentinel.py +0 -0
  156. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_core.py +0 -0
  157. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_cross_agent_messaging.py +0 -0
  158. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_esc_deaf_state.py +0 -0
  159. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_exceptions.py +0 -0
  160. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_mark_read_batch.py +0 -0
  161. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_migration_integrity.py +0 -0
  162. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_realtime_event_freshness.py +0 -0
  163. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_rls_cross_tenant.py +0 -0
  164. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_rpc_migrations.py +0 -0
  165. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_security_regressions.py +0 -0
  166. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_sentinel.py +0 -0
  167. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-noun-wt/tests/test_status_enum_coverage.py +0 -0
  168. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/comms_v4.py +0 -0
  169. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/__init__.py +0 -0
  170. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/ascii_art.py +0 -0
  171. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/cli.py +0 -0
  172. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/comms_v4.py +0 -0
  173. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/compat.py +0 -0
  174. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/error_hints.py +0 -0
  175. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/exceptions.py +0 -0
  176. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/invites.py +0 -0
  177. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/launcher.py +0 -0
  178. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/launcher_install.py +0 -0
  179. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/meshcode_mcp/__init__.py +0 -0
  180. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/meshcode_mcp/__main__.py +0 -0
  181. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/meshcode_mcp/backend.py +0 -0
  182. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/meshcode_mcp/realtime.py +0 -0
  183. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/meshcode_mcp/server.py +0 -0
  184. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_backend.py +0 -0
  185. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_realtime.py +0 -0
  186. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  187. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/preferences.py +0 -0
  188. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/protocol_v2.py +0 -0
  189. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/quickstart.py +0 -0
  190. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/run_agent.py +0 -0
  191. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/secrets.py +0 -0
  192. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/self_update.py +0 -0
  193. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/setup_clients.py +0 -0
  194. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/supervisor.py +0 -0
  195. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/meshcode/upload.py +0 -0
  196. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/scripts/sentinel.py +0 -0
  197. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_core.py +0 -0
  198. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_cross_agent_messaging.py +0 -0
  199. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_esc_deaf_state.py +0 -0
  200. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_exceptions.py +0 -0
  201. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_mark_read_batch.py +0 -0
  202. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_migration_integrity.py +0 -0
  203. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_realtime_event_freshness.py +0 -0
  204. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_rls_cross_tenant.py +0 -0
  205. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_rpc_migrations.py +0 -0
  206. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_security_regressions.py +0 -0
  207. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_sentinel.py +0 -0
  208. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode-tasks-wt/tests/test_status_enum_coverage.py +0 -0
  209. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode.egg-info/dependency_links.txt +0 -0
  210. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode.egg-info/entry_points.txt +0 -0
  211. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode.egg-info/requires.txt +0 -0
  212. {meshcode-2.11.62 → meshcode-2.11.63}/meshcode.egg-info/top_level.txt +0 -0
  213. {meshcode-2.11.62 → meshcode-2.11.63}/setup.cfg +0 -0
  214. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_auto_update_hardening.py +0 -0
  215. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_autonomous_closegap_1.py +0 -0
  216. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_autonomous_closegap_2.py +0 -0
  217. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_autonomous_closegap_3.py +0 -0
  218. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_autonomous_prompt_inject 2.py +0 -0
  219. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_autonomous_prompt_inject 3.py +0 -0
  220. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_autonomous_prompt_inject 4.py +0 -0
  221. /meshcode-2.11.62/tests/test_autonomous_prompt_inject.py → /meshcode-2.11.63/tests/test_autonomous_prompt_inject 5.py +0 -0
  222. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_boot_bug_regression.py +0 -0
  223. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_color_truecolor.py +0 -0
  224. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_core.py +0 -0
  225. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_cross_agent_messaging.py +0 -0
  226. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_date_parse.py +0 -0
  227. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_doctor.py +0 -0
  228. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_epistemic_v1_python_sdk.py +0 -0
  229. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  230. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_esc_deaf_state.py +0 -0
  231. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_exceptions.py +0 -0
  232. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_file_upload.py +0 -0
  233. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_init_device_code.py +0 -0
  234. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_install_guard.py +0 -0
  235. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_lease_sigterm_release.py +0 -0
  236. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_mark_read_batch.py +0 -0
  237. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_marketplace_ratings.py +0 -0
  238. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_migration_integrity.py +0 -0
  239. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_realtime_event_freshness.py +0 -0
  240. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_rls_cross_tenant.py +0 -0
  241. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_rpc_grants.py +0 -0
  242. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_rpc_migrations.py +0 -0
  243. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_run_agent_dry_run.py +0 -0
  244. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_run_agent_no_server_import.py +0 -0
  245. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_security_regressions.py +0 -0
  246. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_self_update_user_site.py +0 -0
  247. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_sentinel.py +0 -0
  248. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_setup_path.py +0 -0
  249. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_sleep_signals.py +0 -0
  250. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_status_enum_coverage.py +0 -0
  251. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_stay_on_loop_hook.py +0 -0
  252. {meshcode-2.11.62 → meshcode-2.11.63}/tests/test_wait_open_tasks_contradiction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.62
3
+ Version: 2.11.63
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.62"
2
+ __version__ = "2.11.63"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -0,0 +1,253 @@
1
+ """Synchronous Claude Code CLI auto-update for `meshcode run`.
2
+
3
+ Companion to self_update.py (which handles the `meshcode` Python package).
4
+ When `meshcode run` launches an agent, this module probes the npm registry
5
+ for the latest @anthropic-ai/claude-code release and runs `npm install -g`
6
+ inline if the installed version is older. The launch BLOCKS until the
7
+ upgrade finishes (or times out).
8
+
9
+ This is intentional: Samuel's requirement is "siempre arrancar con la
10
+ ultima version". The two-launch async model in self_update.py cannot
11
+ satisfy that — it always uses the old binary on launch N. The blocking
12
+ model trades ~5-30s extra latency on launches where an upgrade is
13
+ available for the guarantee that the freshly-spawned editor uses the
14
+ newest CLI.
15
+
16
+ Skip conditions (any one → no-op):
17
+ - MESHCODE_NO_UPDATE=1 / --no-update on argv
18
+ - Inside an existing Claude Code session (CLAUDECODE=1)
19
+ - User pinned a specific version via env / prefs (let _resolve_pinned_claude handle it)
20
+ - Editor is not `claude` (cursor / code / codex / windsurf)
21
+ - npm not in PATH
22
+ - Network unreachable / npm registry down
23
+ - Already at latest version
24
+
25
+ Failures are best-effort: any error prints a single WARN line and lets
26
+ launch continue with whatever version is installed.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ import re
33
+ import shutil
34
+ import subprocess
35
+ import sys
36
+ import time
37
+ from pathlib import Path
38
+ from typing import Optional, Tuple
39
+
40
+ NPM_PKG = "@anthropic-ai/claude-code"
41
+ NPM_REGISTRY_URL = f"https://registry.npmjs.org/{NPM_PKG}/latest"
42
+ NETWORK_TIMEOUT_SEC = 2.5
43
+ INSTALL_TIMEOUT_SEC = 90 # npm i -g can be slow on cold-cache machines
44
+
45
+ STATE_DIR = Path.home() / ".meshcode"
46
+ LOG_PATH = STATE_DIR / "claude_update.log"
47
+
48
+
49
+ # ============================================================
50
+ # Skip-condition helpers
51
+ # ============================================================
52
+
53
+ def _is_claude_session() -> bool:
54
+ """True if we're being called from inside an existing Claude Code session.
55
+
56
+ Replacing the npm binary while claude is running is unsafe and pointless
57
+ — the running claude won't pick up the new code anyway.
58
+ """
59
+ return os.environ.get("CLAUDECODE") == "1" or bool(os.environ.get("CLAUDE_CODE_SESSION"))
60
+
61
+
62
+ def _update_disabled() -> bool:
63
+ if os.environ.get("MESHCODE_NO_UPDATE") == "1":
64
+ return True
65
+ if "--no-update" in sys.argv:
66
+ return True
67
+ return False
68
+
69
+
70
+ def _resolve_user_pin() -> Optional[str]:
71
+ """Return a specific version pin if set; None if 'latest' / unset.
72
+
73
+ Sources checked (first non-empty wins):
74
+ 1. MESHCODE_CLAUDE_VERSION env var
75
+ 2. prefs['claude_version']
76
+
77
+ Note: 'latest' / 'none' / 'skip' values are treated as "no pin" — the
78
+ user wants whatever is current. We do NOT consult the global admin
79
+ config from mc_global_config here; if an admin pins a specific
80
+ version, _resolve_pinned_claude in run_agent.py will route through
81
+ npx regardless of what's installed locally.
82
+ """
83
+ pinned = (os.environ.get("MESHCODE_CLAUDE_VERSION") or "").strip()
84
+ if not pinned:
85
+ try:
86
+ from .preferences import load_prefs
87
+ pinned = (load_prefs().get("claude_version") or "").strip()
88
+ except Exception:
89
+ pinned = ""
90
+ if not pinned:
91
+ return None
92
+ if pinned.lower() in ("latest", "none", "skip"):
93
+ return None
94
+ return pinned
95
+
96
+
97
+ # ============================================================
98
+ # Version probes
99
+ # ============================================================
100
+
101
+ def _get_installed_version(editor_cmd: str) -> Optional[str]:
102
+ """Run `<editor_cmd> --version` and pull the first semver from stdout."""
103
+ try:
104
+ use_shell = sys.platform == "win32" and editor_cmd.lower().endswith((".cmd", ".bat"))
105
+ r = subprocess.run(
106
+ [editor_cmd, "--version"],
107
+ capture_output=True, text=True, timeout=10,
108
+ shell=use_shell,
109
+ )
110
+ m = re.search(r"\d+\.\d+\.\d+", r.stdout or "")
111
+ return m.group(0) if m else None
112
+ except Exception:
113
+ return None
114
+
115
+
116
+ def _fetch_npm_latest() -> Optional[str]:
117
+ """GET https://registry.npmjs.org/<pkg>/latest — returns the JSON manifest
118
+ of the version tagged `latest`, with a `version` field.
119
+
120
+ Do NOT send the `application/vnd.npm.install-v1+json` Accept header
121
+ here: the slim-manifest content type is only valid on the package
122
+ ROOT (/<pkg>), not on /<pkg>/<tag>. Sending it on the tag endpoint
123
+ returns HTTP 406. Default Accept (*/*) gets the canonical JSON.
124
+ """
125
+ try:
126
+ import urllib.request
127
+ req = urllib.request.Request(NPM_REGISTRY_URL)
128
+ with urllib.request.urlopen(req, timeout=NETWORK_TIMEOUT_SEC) as resp:
129
+ data = json.loads(resp.read().decode("utf-8"))
130
+ v = data.get("version")
131
+ if isinstance(v, str):
132
+ return v
133
+ except Exception:
134
+ pass
135
+ return None
136
+
137
+
138
+ def _version_tuple(v: str) -> Tuple[int, ...]:
139
+ parts = []
140
+ for p in v.split("."):
141
+ digits = "".join(c for c in p if c.isdigit())
142
+ parts.append(int(digits) if digits else 0)
143
+ return tuple(parts)
144
+
145
+
146
+ def _is_newer(remote: str, local: str) -> bool:
147
+ try:
148
+ return _version_tuple(remote) > _version_tuple(local)
149
+ except Exception:
150
+ return False
151
+
152
+
153
+ # ============================================================
154
+ # Editor-stem detection (only act when editor is claude)
155
+ # ============================================================
156
+
157
+ def _editor_is_claude(editor_cmd: str) -> bool:
158
+ name = os.path.basename(editor_cmd).lower()
159
+ if sys.platform == "win32":
160
+ name = name.rsplit(".", 1)[0] if "." in name else name
161
+ return name == "claude"
162
+
163
+
164
+ # ============================================================
165
+ # Public entrypoint
166
+ # ============================================================
167
+
168
+ def check_and_maybe_update_claude_blocking(
169
+ editor_cmd: str,
170
+ verbose: bool = True,
171
+ timeout_sec: int = INSTALL_TIMEOUT_SEC,
172
+ ) -> Optional[str]:
173
+ """Probe npm + foreground-install latest claude if newer.
174
+
175
+ Returns the new version on successful upgrade, None otherwise.
176
+ Never raises — all errors are swallowed and logged via WARN line.
177
+ """
178
+ if _update_disabled():
179
+ return None
180
+ if _is_claude_session():
181
+ return None
182
+ if _resolve_user_pin():
183
+ return None
184
+ if not _editor_is_claude(editor_cmd):
185
+ return None
186
+
187
+ current = _get_installed_version(editor_cmd)
188
+ latest = _fetch_npm_latest()
189
+ if not current:
190
+ if verbose:
191
+ print(f"[meshcode] (Could not read claude --version; skipping auto-update)", file=sys.stderr)
192
+ return None
193
+ if not latest:
194
+ if verbose:
195
+ print(f"[meshcode] (Could not reach npm registry; staying on claude {current})", file=sys.stderr)
196
+ return None
197
+ if not _is_newer(latest, current):
198
+ if verbose:
199
+ print(f"[meshcode] claude {current} (already latest)", file=sys.stderr)
200
+ return None
201
+
202
+ npm = shutil.which("npm")
203
+ if not npm:
204
+ if verbose:
205
+ print(
206
+ f"[meshcode] WARN: claude {current} < {latest} on npm, but `npm` not in PATH; "
207
+ f"continuing on {current}",
208
+ file=sys.stderr,
209
+ )
210
+ return None
211
+
212
+ if verbose:
213
+ print(
214
+ f"[meshcode] Upgrading claude {current} -> {latest} (blocking, can take ~30s)...",
215
+ file=sys.stderr,
216
+ )
217
+
218
+ cmd = [npm, "install", "-g", f"{NPM_PKG}@{latest}"]
219
+ try:
220
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
221
+ with open(LOG_PATH, "ab") as logf:
222
+ logf.write(
223
+ f"\n=== {time.strftime('%Y-%m-%d %H:%M:%S')} blocking "
224
+ f"npm i -g {NPM_PKG}@{latest} ===\n".encode()
225
+ )
226
+ logf.flush()
227
+ proc = subprocess.run(cmd, stdout=logf, stderr=logf, timeout=timeout_sec)
228
+ if proc.returncode == 0:
229
+ if verbose:
230
+ print(f"[meshcode] claude upgraded to {latest}", file=sys.stderr)
231
+ return latest
232
+ if verbose:
233
+ print(
234
+ f"[meshcode] WARN: npm install exit {proc.returncode}; "
235
+ f"continuing on {current} (log: {LOG_PATH})",
236
+ file=sys.stderr,
237
+ )
238
+ return None
239
+ except subprocess.TimeoutExpired:
240
+ if verbose:
241
+ print(
242
+ f"[meshcode] WARN: npm install timed out after {timeout_sec}s; "
243
+ f"continuing on {current}",
244
+ file=sys.stderr,
245
+ )
246
+ return None
247
+ except Exception as e:
248
+ if verbose:
249
+ print(
250
+ f"[meshcode] WARN: npm install failed: {e}; continuing on {current}",
251
+ file=sys.stderr,
252
+ )
253
+ return None
@@ -0,0 +1,381 @@
1
+ """meshcode hostd — host-side daemon: respawn + recycle managed agents.
2
+
3
+ Complements supervisor.py (launchd KeepAlive = process-exit restart only).
4
+ hostd polls the CLOUD for staleness (heartbeat) + recycle triggers that
5
+ launchd cannot see:
6
+
7
+ - RESPAWN: agent's heartbeat is stale (process/daemon-thread dead) and
8
+ desired_state='running' → relaunch. Loop-guard cap 3/10min → 'crashed'.
9
+ - RECYCLE: managed agent exceeds its recycle policy (context% or uptime)
10
+ → relaunch at a task boundary (never mid-task). Recorded separately
11
+ from the crash cap.
12
+
13
+ Contract (mig366/367/368/369, DBA-cleared):
14
+ mc_register_host, mc_agents_needing_respawn, mc_record_respawn,
15
+ mc_record_recycle, mc_set_desired_state, mc_host_recycle_policy.
16
+
17
+ Source of truth is the cloud (mc_agents / mc_host_config), never local
18
+ folders. host_id lives in ~/.meshcode/host_id (generated once).
19
+
20
+ meshcode hostd run — run the daemon loop (foreground)
21
+ meshcode hostd status — show this host's managed agents
22
+ meshcode hostd install — install launchd trigger-start plist
23
+ meshcode hostd uninstall — remove it
24
+
25
+ Design invariants:
26
+ - NEVER respawn desired_state != 'running' (NULL=unmanaged, stopped=
27
+ intentional, crashed=gave-up). The cloud RPC enforces this; we trust it.
28
+ - Single retry / capped respawn — never an infinite relaunch loop.
29
+ - Recycle only at task boundary: we relaunch a recycle candidate only
30
+ when it is NOT actively working (status not in working/online-busy) OR
31
+ the agent has self-flagged 'recycling'. Mid-task agents are skipped.
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import os
37
+ import subprocess
38
+ import sys
39
+ import time
40
+ import urllib.request
41
+ import uuid
42
+ from pathlib import Path
43
+ from typing import Optional
44
+
45
+ STATE_DIR = Path.home() / ".meshcode"
46
+ HOST_ID_PATH = STATE_DIR / "host_id"
47
+ LOG_PATH = STATE_DIR / "hostd.log"
48
+
49
+ POLL_INTERVAL_SEC = 45
50
+ STALE_SECONDS = 150 # heartbeat liveness cutoff (QA #3)
51
+ RESPAWN_TIMEOUT_SEC = 30
52
+ # statuses that mean "actively working" → never recycle mid-task
53
+ BUSY_STATUSES = {"working", "online", "busy"}
54
+
55
+
56
+ def _log(msg: str) -> None:
57
+ line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
58
+ print(line, file=sys.stderr)
59
+ try:
60
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
61
+ with open(LOG_PATH, "a", encoding="utf-8") as f:
62
+ f.write(line + "\n")
63
+ except Exception:
64
+ pass
65
+
66
+
67
+ def get_host_id() -> str:
68
+ """Read ~/.meshcode/host_id, generating a stable UUID on first use."""
69
+ try:
70
+ if HOST_ID_PATH.exists():
71
+ v = HOST_ID_PATH.read_text(encoding="utf-8").strip()
72
+ if v:
73
+ return v
74
+ except Exception:
75
+ pass
76
+ new_id = str(uuid.uuid4())
77
+ try:
78
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
79
+ HOST_ID_PATH.write_text(new_id, encoding="utf-8")
80
+ except Exception:
81
+ pass
82
+ return new_id
83
+
84
+
85
+ # ------------------------------------------------------------------
86
+ # Cloud RPC (PostgREST) — self-contained urllib, no internal client dep
87
+ # ------------------------------------------------------------------
88
+
89
+ def _supabase_cfg() -> tuple:
90
+ url = os.environ.get("SUPABASE_URL", "").rstrip("/")
91
+ key = os.environ.get("SUPABASE_KEY") or os.environ.get("MESHCODE_SUPABASE_ANON_KEY") or ""
92
+ return url, key
93
+
94
+
95
+ def _api_key() -> Optional[str]:
96
+ """The meshcode agent/user api key, used as p_api_key in mc_ RPCs."""
97
+ k = os.environ.get("MESHCODE_API_KEY")
98
+ if k:
99
+ return k
100
+ try:
101
+ # same source meshcode run uses (per-profile state)
102
+ prof = STATE_DIR / "profiles.json"
103
+ if prof.exists():
104
+ data = json.loads(prof.read_text(encoding="utf-8"))
105
+ # best-effort: first profile with an api_key
106
+ for v in (data.values() if isinstance(data, dict) else []):
107
+ if isinstance(v, dict) and v.get("api_key"):
108
+ return v["api_key"]
109
+ except Exception:
110
+ pass
111
+ return None
112
+
113
+
114
+ def _rpc(fn: str, payload: dict) -> Optional[dict]:
115
+ """Call a PostgREST RPC. Returns parsed JSON or None on any failure."""
116
+ url, key = _supabase_cfg()
117
+ if not url or not key:
118
+ _log("WARN: SUPABASE_URL/KEY not set — cannot reach cloud")
119
+ return None
120
+ try:
121
+ req = urllib.request.Request(
122
+ f"{url}/rest/v1/rpc/{fn}",
123
+ data=json.dumps(payload).encode("utf-8"),
124
+ headers={
125
+ "apikey": key,
126
+ "Authorization": f"Bearer {key}",
127
+ "Content-Type": "application/json",
128
+ },
129
+ method="POST",
130
+ )
131
+ with urllib.request.urlopen(req, timeout=15) as resp:
132
+ return json.loads(resp.read().decode("utf-8"))
133
+ except Exception as e:
134
+ _log(f"WARN: rpc {fn} failed: {e}")
135
+ return None
136
+
137
+
138
+ # ------------------------------------------------------------------
139
+ # Respawn
140
+ # ------------------------------------------------------------------
141
+
142
+ def _meshcode_bin() -> str:
143
+ cand = Path(sys.argv[0]).resolve()
144
+ if cand.exists() and cand.name.startswith("meshcode"):
145
+ return str(cand)
146
+ return "meshcode"
147
+
148
+
149
+ def _spawn_agent(project: str, agent: str) -> bool:
150
+ """Relaunch `meshcode run <project>/<agent>` detached + visible-friendly."""
151
+ target = f"{project}/{agent}"
152
+ env = dict(os.environ)
153
+ # env hygiene (item1 RC): a stale CLAUDECODE aborts `meshcode run` (exit2)
154
+ env.pop("CLAUDECODE", None)
155
+ env.pop("CLAUDE_CODE_SESSION", None)
156
+ env["MESHCODE_NO_AUTO_UPDATE"] = env.get("MESHCODE_NO_AUTO_UPDATE", "1")
157
+ try:
158
+ subprocess.Popen(
159
+ [_meshcode_bin(), "run", target],
160
+ env=env, start_new_session=True,
161
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
162
+ )
163
+ return True
164
+ except Exception as e:
165
+ _log(f"WARN: spawn {target} failed: {e}")
166
+ return False
167
+
168
+
169
+ def _do_respawns(api_key: str, host_id: str) -> int:
170
+ """One respawn sweep. Returns number relaunched."""
171
+ res = _rpc("mc_agents_needing_respawn",
172
+ {"p_api_key": api_key, "p_host_id": host_id, "p_stale_seconds": STALE_SECONDS})
173
+ if not res or not res.get("ok"):
174
+ return 0
175
+ n = 0
176
+ for c in res.get("candidates", []):
177
+ proj, agent = c.get("project_name"), c.get("agent")
178
+ if not proj or not agent:
179
+ continue
180
+ if not c.get("respawn_allowed", True):
181
+ _log(f"SKIP respawn {proj}/{agent}: cap reached (count={c.get('respawn_count')})")
182
+ rec = _rpc("mc_record_respawn",
183
+ {"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
184
+ if rec and rec.get("give_up"):
185
+ _rpc("mc_set_desired_state", {"p_api_key": api_key, "p_state": "crashed"})
186
+ _log(f"ALERT {proj}/{agent}: marked crashed after respawn cap — operator attention")
187
+ continue
188
+ _log(f"RESPAWN {proj}/{agent} (stale {c.get('heartbeat_age_s')}s)")
189
+ if _spawn_agent(proj, agent):
190
+ _rpc("mc_record_respawn",
191
+ {"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
192
+ n += 1
193
+ return n
194
+
195
+
196
+ # ------------------------------------------------------------------
197
+ # Recycle — relaunch a managed agent at a TASK BOUNDARY when it exceeds
198
+ # its recycle policy. Uptime-recycle is daemon-driven (needs mig370's
199
+ # project_name in the roster). Context-recycle is agent-cooperative (the
200
+ # agent self-persists handoff + exits at a boundary; our respawn path then
201
+ # relaunches it). Recorded via mc_record_recycle — NEVER counted against
202
+ # the crash respawn cap.
203
+ # ------------------------------------------------------------------
204
+
205
+ _HOSTD_STATE_PATH = STATE_DIR / "hostd_state.json"
206
+
207
+
208
+ def _load_state() -> dict:
209
+ try:
210
+ if _HOSTD_STATE_PATH.exists():
211
+ return json.loads(_HOSTD_STATE_PATH.read_text(encoding="utf-8"))
212
+ except Exception:
213
+ pass
214
+ return {}
215
+
216
+
217
+ def _save_state(st: dict) -> None:
218
+ try:
219
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
220
+ _HOSTD_STATE_PATH.write_text(json.dumps(st), encoding="utf-8")
221
+ except Exception:
222
+ pass
223
+
224
+
225
+ def _do_recycles(api_key: str, host_id: str) -> int:
226
+ """Uptime-based recycle at task boundary. Returns number recycled."""
227
+ cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
228
+ if not cfg or not cfg.get("ok"):
229
+ return 0
230
+ pol = _rpc("mc_host_recycle_policy", {"p_api_key": api_key, "p_host_id": host_id})
231
+ mode = (pol or {}).get("recycle_mode")
232
+ value = (pol or {}).get("recycle_value")
233
+ if mode != "time" or not value:
234
+ return 0 # context-recycle is agent-cooperative; only time is daemon-driven
235
+ st = _load_state()
236
+ now = time.time()
237
+ n = 0
238
+ seen = set()
239
+ for a in cfg.get("agents", []):
240
+ if a.get("desired_state") != "running":
241
+ continue
242
+ proj, agent = a.get("project_name"), a.get("name")
243
+ if not proj or not agent:
244
+ continue
245
+ key = f"{a.get('project_id')}/{agent}"
246
+ seen.add(key)
247
+ first = st.get(key)
248
+ if first is None:
249
+ st[key] = now # start the uptime clock on first observation
250
+ continue
251
+ if (a.get("status") or "") in BUSY_STATUSES:
252
+ continue # task boundary only — never mid-task
253
+ if (now - first) >= float(value) * 3600.0:
254
+ _log(f"RECYCLE {proj}/{agent} (uptime {(now-first)/3600:.1f}h >= {value}h)")
255
+ if _spawn_agent(proj, agent):
256
+ _rpc("mc_record_recycle",
257
+ {"p_api_key": api_key, "p_project_id": a.get("project_id"), "p_agent_name": agent})
258
+ st[key] = now # reset clock after recycle
259
+ n += 1
260
+ # prune state for agents no longer managed on this host
261
+ for k in [k for k in st if k not in seen]:
262
+ st.pop(k, None)
263
+ _save_state(st)
264
+ return n
265
+
266
+
267
+ # ------------------------------------------------------------------
268
+ # launchd trigger-start (macOS) — extends supervisor.py pattern.
269
+ # The hostd daemon is NOT autostart-at-login: RunAtLoad=false. It is
270
+ # kicked off on demand (button -> meshcode:// -> `launchctl kickstart`)
271
+ # and KeepAlive keeps it alive while it runs. One job per host.
272
+ # ------------------------------------------------------------------
273
+
274
+ _HOSTD_PLIST_LABEL = "io.meshcode.hostd"
275
+
276
+
277
+ def _hostd_plist_path():
278
+ return Path.home() / "Library" / "LaunchAgents" / f"{_HOSTD_PLIST_LABEL}.plist"
279
+
280
+
281
+ def _hostd_plist_xml() -> str:
282
+ import shutil
283
+ mc = shutil.which("meshcode") or f"{sys.executable} -m meshcode"
284
+ args = (mc.split() + ["hostd", "run"]) if " -m " in mc else [mc, "hostd", "run"]
285
+ args_xml = "\n".join(f" <string>{a}</string>" for a in args)
286
+ logdir = STATE_DIR / "logs"
287
+ logdir.mkdir(parents=True, exist_ok=True)
288
+ return f"""<?xml version="1.0" encoding="UTF-8"?>
289
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
290
+ <plist version="1.0">
291
+ <dict>
292
+ <key>Label</key><string>{_HOSTD_PLIST_LABEL}</string>
293
+ <key>ProgramArguments</key>
294
+ <array>
295
+ {args_xml}
296
+ </array>
297
+ <key>RunAtLoad</key><false/>
298
+ <key>KeepAlive</key><true/>
299
+ <key>ThrottleInterval</key><integer>10</integer>
300
+ <key>StandardOutPath</key><string>{logdir / "hostd.stdout.log"}</string>
301
+ <key>StandardErrorPath</key><string>{logdir / "hostd.stderr.log"}</string>
302
+ <key>EnvironmentVariables</key>
303
+ <dict>
304
+ <key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:{os.path.dirname(sys.executable)}</string>
305
+ <key>MESHCODE_NO_AUTO_UPDATE</key><string>1</string>
306
+ </dict>
307
+ </dict>
308
+ </plist>
309
+ """
310
+
311
+
312
+ def _hostd_install() -> int:
313
+ import platform
314
+ if platform.system() != "Darwin":
315
+ print("[hostd] launchd install is macOS only (Linux: use systemd --user; or run `meshcode hostd run` under your own supervisor).", file=sys.stderr)
316
+ return 2
317
+ plist = _hostd_plist_path()
318
+ plist.parent.mkdir(parents=True, exist_ok=True)
319
+ plist.write_text(_hostd_plist_xml(), encoding="utf-8")
320
+ subprocess.run(["launchctl", "unload", str(plist)], capture_output=True, text=True)
321
+ r = subprocess.run(["launchctl", "load", str(plist)], capture_output=True, text=True)
322
+ if r.returncode != 0:
323
+ print(f"[hostd] launchctl load failed: {r.stderr.strip()}", file=sys.stderr)
324
+ return 1
325
+ print(f"[hostd] installed (RunAtLoad=false, trigger-start). plist: {plist}")
326
+ print(f"[hostd] start now: launchctl kickstart gui/$(id -u)/{_HOSTD_PLIST_LABEL}")
327
+ return 0
328
+
329
+
330
+ def _hostd_uninstall() -> int:
331
+ plist = _hostd_plist_path()
332
+ if not plist.exists():
333
+ print("[hostd] not installed")
334
+ return 0
335
+ subprocess.run(["launchctl", "unload", str(plist)], capture_output=True, text=True)
336
+ plist.unlink(missing_ok=True)
337
+ print("[hostd] uninstalled")
338
+ return 0
339
+
340
+
341
+ def cmd_hostd(args: list) -> int:
342
+ """Entry point for `meshcode hostd ...`."""
343
+ if not args or args[0] in ("-h", "--help"):
344
+ print(__doc__)
345
+ return 0
346
+ sub = args[0]
347
+ host_id = get_host_id()
348
+ api_key = _api_key()
349
+
350
+ if sub == "status":
351
+ if not api_key:
352
+ print("[hostd] no api key configured", file=sys.stderr)
353
+ return 1
354
+ res = _rpc("mc_agents_needing_respawn",
355
+ {"p_api_key": api_key, "p_host_id": host_id, "p_stale_seconds": STALE_SECONDS})
356
+ print(json.dumps({"host_id": host_id, "needing_respawn": res}, indent=2))
357
+ return 0
358
+
359
+ if sub == "install":
360
+ return _hostd_install()
361
+
362
+ if sub == "uninstall":
363
+ return _hostd_uninstall()
364
+
365
+ if sub == "run":
366
+ if not api_key:
367
+ _log("FATAL: no api key — set MESHCODE_API_KEY")
368
+ return 1
369
+ _log(f"hostd starting — host_id={host_id} interval={POLL_INTERVAL_SEC}s stale={STALE_SECONDS}s")
370
+ while True:
371
+ try:
372
+ relaunched = _do_respawns(api_key, host_id)
373
+ recycled = _do_recycles(api_key, host_id)
374
+ if relaunched or recycled:
375
+ _log(f"sweep done — {relaunched} respawned, {recycled} recycled")
376
+ except Exception as e:
377
+ _log(f"WARN: sweep error: {e}")
378
+ time.sleep(POLL_INTERVAL_SEC)
379
+
380
+ print(f"[hostd] unknown subcommand: {sub}", file=sys.stderr)
381
+ return 1