gobby 0.2.5__py3-none-any.whl

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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,104 @@
1
+ """
2
+ Gobby hooks package for Claude Code, Gemini CLI, and Codex integration.
3
+
4
+ This package provides a hook system for intercepting and processing events
5
+ from AI coding assistants. The architecture follows the Coordinator pattern:
6
+
7
+ Core Components:
8
+ HookManager: Main entry point and coordinator. Receives hook events and
9
+ delegates to specialized components.
10
+
11
+ EventHandlers: Contains all event handler implementations for the 15
12
+ supported event types (session, agent, tool, etc.)
13
+
14
+ SessionCoordinator: Manages session lifecycle - registration, lookup,
15
+ status tracking, and cleanup.
16
+
17
+ HealthMonitor: Background daemon health check monitoring with caching.
18
+
19
+ WebhookDispatcher: Dispatches hook events to external webhook endpoints.
20
+
21
+ Event Models:
22
+ HookEventType: Unified event type enum (15 types across all CLIs)
23
+ SessionSource: Enum identifying which CLI originated the session
24
+ HookEvent: Unified event dataclass from any CLI source
25
+ HookResponse: Unified response dataclass returned to CLIs
26
+
27
+ Plugin System:
28
+ HookPlugin: Base class for custom hook plugins
29
+ PluginLoader: Discovers and loads plugins from configured paths
30
+ hook_handler: Decorator for registering plugin handlers
31
+
32
+ Example:
33
+ ```python
34
+ from gobby.hooks import HookManager, HookEvent, HookEventType
35
+
36
+ # Create manager (typically done once in daemon)
37
+ manager = HookManager()
38
+
39
+ # Handle incoming events
40
+ response = manager.handle(event)
41
+ ```
42
+ """
43
+
44
+ # Core coordinator and components
45
+ # Artifact capture hook
46
+ from gobby.hooks.artifact_capture import ArtifactCaptureHook
47
+ from gobby.hooks.event_handlers import EventHandlers
48
+ from gobby.hooks.events import (
49
+ EVENT_TYPE_CLI_SUPPORT,
50
+ HookEvent,
51
+ HookEventType,
52
+ HookResponse,
53
+ SessionSource,
54
+ )
55
+ from gobby.hooks.health_monitor import HealthMonitor
56
+ from gobby.hooks.hook_manager import HookManager
57
+ from gobby.hooks.plugins import (
58
+ HookPlugin,
59
+ PluginLoader,
60
+ PluginRegistry,
61
+ RegisteredHandler,
62
+ hook_handler,
63
+ run_plugin_handlers,
64
+ )
65
+ from gobby.hooks.session_coordinator import SessionCoordinator
66
+ from gobby.hooks.webhooks import WebhookDispatcher
67
+
68
+ # Legacy imports for backward compatibility
69
+ from gobby.sessions.manager import SessionManager
70
+ from gobby.sessions.summary import SummaryFileGenerator
71
+ from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
72
+
73
+ # Backward-compatible alias
74
+ TranscriptProcessor = ClaudeTranscriptParser
75
+
76
+ __all__ = [
77
+ # Core coordinator
78
+ "HookManager",
79
+ # Extracted components (for advanced usage/testing)
80
+ "EventHandlers",
81
+ "SessionCoordinator",
82
+ "HealthMonitor",
83
+ "WebhookDispatcher",
84
+ # Artifact capture
85
+ "ArtifactCaptureHook",
86
+ # Unified hook event models
87
+ "HookEventType",
88
+ "SessionSource",
89
+ "HookEvent",
90
+ "HookResponse",
91
+ "EVENT_TYPE_CLI_SUPPORT",
92
+ # Plugin system
93
+ "HookPlugin",
94
+ "PluginLoader",
95
+ "PluginRegistry",
96
+ "RegisteredHandler",
97
+ "hook_handler",
98
+ "run_plugin_handlers",
99
+ # Legacy exports (backward compatibility)
100
+ "SessionManager",
101
+ "SummaryFileGenerator",
102
+ "TranscriptProcessor",
103
+ "ClaudeTranscriptParser",
104
+ ]
@@ -0,0 +1,213 @@
1
+ """
2
+ Artifact capture hook for extracting and storing artifacts from messages.
3
+
4
+ Processes assistant messages to extract:
5
+ - Code blocks (with language metadata)
6
+ - File path references
7
+ - Other classified content
8
+
9
+ Uses artifact_classifier for type detection and LocalArtifactManager for storage.
10
+ Tracks content hashes to prevent duplicate storage using a bounded LRU cache.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import logging
17
+ import re
18
+ from collections import OrderedDict
19
+ from typing import TYPE_CHECKING
20
+
21
+ from gobby.storage.artifact_classifier import ArtifactType, classify_artifact
22
+
23
+ if TYPE_CHECKING:
24
+ from gobby.storage.artifacts import Artifact, LocalArtifactManager
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ __all__ = ["ArtifactCaptureHook"]
29
+
30
+ # Maximum number of content hashes to track for duplicate detection
31
+ MAX_HASH_CACHE = 10000
32
+
33
+ # Pattern to extract markdown code blocks
34
+ _CODE_BLOCK_PATTERN = re.compile(r"```(\w*)\n(.*?)```", re.DOTALL)
35
+
36
+ # Pattern to extract backtick-wrapped file paths
37
+ _FILE_REF_PATTERN = re.compile(r"`([^\s`]+\.[a-zA-Z0-9]+)`")
38
+
39
+ # Pattern to extract bare file paths (Unix-style)
40
+ _UNIX_PATH_PATTERN = re.compile(r"(?:^|\s)(/[^\s]+\.[a-zA-Z0-9]+)(?:\s|$)")
41
+
42
+
43
+ class ArtifactCaptureHook:
44
+ """
45
+ Hook for capturing artifacts from assistant messages.
46
+
47
+ Extracts code blocks, file references, and other artifacts from messages,
48
+ classifies them using artifact_classifier, and stores via LocalArtifactManager.
49
+
50
+ Tracks content hashes to prevent storing duplicate artifacts.
51
+ """
52
+
53
+ def __init__(self, artifact_manager: LocalArtifactManager):
54
+ """
55
+ Initialize the artifact capture hook.
56
+
57
+ Args:
58
+ artifact_manager: LocalArtifactManager instance for storing artifacts
59
+ """
60
+ self._artifact_manager = artifact_manager
61
+ # Use OrderedDict as LRU cache for bounded duplicate tracking
62
+ self._seen_hashes: OrderedDict[str, None] = OrderedDict()
63
+
64
+ def _compute_hash(self, content: str) -> str:
65
+ """Compute a hash for content deduplication."""
66
+ return hashlib.sha256(content.encode()).hexdigest()
67
+
68
+ def _is_duplicate(self, content: str) -> bool:
69
+ """Check if content has already been seen.
70
+
71
+ Uses a bounded LRU cache to prevent unbounded memory growth.
72
+ """
73
+ content_hash = self._compute_hash(content)
74
+ if content_hash in self._seen_hashes:
75
+ # Move to end (most recently used)
76
+ self._seen_hashes.move_to_end(content_hash)
77
+ return True
78
+
79
+ # Add new hash, evict oldest if over capacity
80
+ self._seen_hashes[content_hash] = None
81
+ if len(self._seen_hashes) > MAX_HASH_CACHE:
82
+ # Remove oldest entry (first item)
83
+ self._seen_hashes.popitem(last=False)
84
+
85
+ return False
86
+
87
+ def reset_duplicate_tracking(self) -> None:
88
+ """Clear the duplicate tracking cache.
89
+
90
+ Useful to reset between sessions or when memory needs to be freed.
91
+ """
92
+ self._seen_hashes.clear()
93
+
94
+ def _extract_code_blocks(self, content: str) -> list[tuple[str, str]]:
95
+ """
96
+ Extract code blocks from content.
97
+
98
+ Args:
99
+ content: Message content to extract from
100
+
101
+ Returns:
102
+ List of (language, code) tuples
103
+ """
104
+ blocks = []
105
+ for match in _CODE_BLOCK_PATTERN.finditer(content):
106
+ language = match.group(1).lower() if match.group(1) else ""
107
+ code = match.group(2).strip()
108
+ if code:
109
+ blocks.append((language, code))
110
+ return blocks
111
+
112
+ def _extract_file_references(self, content: str) -> list[str]:
113
+ """
114
+ Extract file path references from content.
115
+
116
+ Args:
117
+ content: Message content to extract from
118
+
119
+ Returns:
120
+ List of file paths
121
+ """
122
+ paths = set()
123
+
124
+ # Extract backtick-wrapped file paths
125
+ for match in _FILE_REF_PATTERN.finditer(content):
126
+ path = match.group(1)
127
+ # Filter out obvious non-paths
128
+ if "/" in path or "\\" in path:
129
+ paths.add(path)
130
+
131
+ # Extract Unix-style paths
132
+ for match in _UNIX_PATH_PATTERN.finditer(content):
133
+ paths.add(match.group(1))
134
+
135
+ return list(paths)
136
+
137
+ def process_message(
138
+ self,
139
+ session_id: str,
140
+ role: str,
141
+ content: str,
142
+ ) -> list[Artifact] | None:
143
+ """
144
+ Process a message and extract artifacts.
145
+
146
+ Only processes assistant messages. Extracts code blocks and file
147
+ references, classifies them, and stores them as artifacts.
148
+
149
+ Args:
150
+ session_id: ID of the session this message belongs to
151
+ role: Message role ("assistant" or "user")
152
+ content: Message content
153
+
154
+ Returns:
155
+ List of created Artifact objects, or None/empty if none created
156
+ """
157
+ # Only process assistant messages
158
+ if role != "assistant":
159
+ return None
160
+
161
+ if not content or not content.strip():
162
+ return []
163
+
164
+ artifacts: list[Artifact] = []
165
+
166
+ # Extract and store code blocks
167
+ code_blocks = self._extract_code_blocks(content)
168
+ for language, code in code_blocks:
169
+ if self._is_duplicate(code):
170
+ continue
171
+
172
+ # Use classifier to get type and metadata
173
+ result = classify_artifact(f"```{language}\n{code}\n```")
174
+ metadata = result.metadata.copy()
175
+
176
+ # Ensure language is in metadata
177
+ if language and "language" not in metadata:
178
+ metadata["language"] = language
179
+
180
+ try:
181
+ artifact = self._artifact_manager.create_artifact(
182
+ session_id=session_id,
183
+ artifact_type=result.artifact_type.value,
184
+ content=code,
185
+ metadata=metadata,
186
+ )
187
+ artifacts.append(artifact)
188
+ except Exception as e:
189
+ logger.error(f"Failed to create code artifact: {e}")
190
+
191
+ # Extract and store file references
192
+ file_paths = self._extract_file_references(content)
193
+ for path in file_paths:
194
+ if self._is_duplicate(path):
195
+ continue
196
+
197
+ # Use classifier to verify it's a file path
198
+ result = classify_artifact(path)
199
+ if result.artifact_type != ArtifactType.FILE_PATH:
200
+ continue
201
+
202
+ try:
203
+ artifact = self._artifact_manager.create_artifact(
204
+ session_id=session_id,
205
+ artifact_type=ArtifactType.FILE_PATH.value,
206
+ content=path,
207
+ metadata=result.metadata,
208
+ )
209
+ artifacts.append(artifact)
210
+ except Exception as e:
211
+ logger.error(f"Failed to create file path artifact: {e}")
212
+
213
+ return artifacts
@@ -0,0 +1,243 @@
1
+ """
2
+ Hook Event Broadcaster.
3
+
4
+ Broadcasting of hook events to WebSocket clients with filtering and sanitization.
5
+ """
6
+
7
+ import logging
8
+ from datetime import UTC, datetime
9
+ from typing import Any
10
+
11
+ from gobby.config.app import DaemonConfig
12
+ from gobby.hooks.events import HookEvent, HookResponse
13
+ from gobby.hooks.hook_types import (
14
+ HOOK_INPUT_MODELS,
15
+ HOOK_OUTPUT_MODELS,
16
+ HookInput,
17
+ HookOutput,
18
+ HookType,
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ # Mapping from unified HookEventType to specific HookType Pydantic models
25
+ EVENT_TYPE_TO_HOOK_TYPE: dict[str, HookType] = {
26
+ "session_start": HookType.SESSION_START,
27
+ "session_end": HookType.SESSION_END,
28
+ "before_agent": HookType.USER_PROMPT_SUBMIT,
29
+ "after_agent": HookType.STOP,
30
+ "stop": HookType.STOP,
31
+ "before_tool": HookType.PRE_TOOL_USE,
32
+ "after_tool": HookType.POST_TOOL_USE,
33
+ "before_tool_selection": HookType.PRE_TOOL_USE, # Maps to same as before_tool
34
+ "pre_compact": HookType.PRE_COMPACT,
35
+ "subagent_start": HookType.SUBAGENT_START,
36
+ "subagent_stop": HookType.SUBAGENT_STOP,
37
+ "notification": HookType.NOTIFICATION,
38
+ "before_model": HookType.BEFORE_MODEL,
39
+ "after_model": HookType.AFTER_MODEL,
40
+ "permission_request": HookType.PERMISSION_REQUEST,
41
+ }
42
+
43
+
44
+ class HookEventBroadcaster:
45
+ """
46
+ Broadcasts hook events to connected WebSocket clients.
47
+
48
+ Handles configuration checking, filtering, payload sanitization,
49
+ and message formatting.
50
+ """
51
+
52
+ def __init__(self, websocket_server: Any | None, config: DaemonConfig | None):
53
+ """
54
+ Initialize broadcaster.
55
+
56
+ Args:
57
+ websocket_server: WebSocketServer instance (can be None)
58
+ config: Daemon configuration (can be None)
59
+ """
60
+ self.websocket_server = websocket_server
61
+ self.config = config
62
+
63
+ async def broadcast_event(self, event: HookEvent, response: HookResponse | None = None) -> None:
64
+ """
65
+ Broadcast a unified HookEvent to all connected clients.
66
+
67
+ Automatically converts HookEvent to appropriate Pydantic models.
68
+
69
+ Args:
70
+ event: The unified HookEvent
71
+ response: Optional HookResponse result
72
+ """
73
+ if not self.websocket_server:
74
+ return
75
+
76
+ try:
77
+ # Map unified event type to HookType enum for Pydantic models
78
+ # Use value string lookup to avoid circular imports if HookEventType not available here
79
+ # (Though we imported HookType, we didn't import HookEventType enum class yet, just used strings in dict keys safely)
80
+ enum_hook_type = EVENT_TYPE_TO_HOOK_TYPE.get(event.event_type.value)
81
+
82
+ if not enum_hook_type:
83
+ # Try direct map if values match (fallback)
84
+ try:
85
+ enum_hook_type = HookType(event.event_type.value)
86
+ except ValueError:
87
+ logger.warning(
88
+ f"Skipping broadcast for unknown hook type: {event.event_type.value}"
89
+ )
90
+ return
91
+
92
+ # Get input/output models
93
+ input_model_cls = HOOK_INPUT_MODELS.get(enum_hook_type)
94
+ output_model_cls = HOOK_OUTPUT_MODELS.get(enum_hook_type)
95
+
96
+ if not input_model_cls or not output_model_cls:
97
+ return
98
+
99
+ # Prepare input data
100
+ raw_input = event.data.copy()
101
+ # Map 'session_id' -> 'external_id' if needed
102
+ if "external_id" not in raw_input and event.session_id:
103
+ raw_input["external_id"] = event.session_id
104
+
105
+ # Special handling for Subagent events: ensure subagent_id is present
106
+ if enum_hook_type in (HookType.SUBAGENT_START, HookType.SUBAGENT_STOP):
107
+ if "subagent_id" not in raw_input and "external_id" in raw_input:
108
+ raw_input["subagent_id"] = raw_input["external_id"]
109
+
110
+ # Map 'prompt' -> 'prompt_text' for UserPromptSubmit
111
+ if enum_hook_type == HookType.USER_PROMPT_SUBMIT:
112
+ if "prompt_text" not in raw_input and "prompt" in raw_input:
113
+ raw_input["prompt_text"] = raw_input["prompt"]
114
+
115
+ # Ensure 'permission_type' has a default for PermissionRequest
116
+ if enum_hook_type == HookType.PERMISSION_REQUEST:
117
+ if "permission_type" not in raw_input:
118
+ raw_input["permission_type"] = "unknown"
119
+
120
+ # Validate input data structure matches Pydantic model
121
+ # Use construct/model_validate to avoid strict validation errors if possible,
122
+ # or just try/except. Let's rely on standard validation.
123
+ validated_input = input_model_cls(**raw_input)
124
+
125
+ # Prepare output data if response provided
126
+ validated_output = None
127
+ if response:
128
+ # Map unified HookResponse to dict that matches Pydantic output model
129
+ # Note: HookResponse is unified, but Pydantic output models vary.
130
+ # Usually outputs have: continue, decision, etc.
131
+ # Simplest is to dump HookResponse to dict and filter/map.
132
+
133
+ # Default mapping from HookResponse
134
+ response_dict: dict[str, Any] = {
135
+ "continue": response.decision != "deny",
136
+ "decision": response.decision,
137
+ "stopReason": response.reason,
138
+ "systemMessage": response.system_message,
139
+ }
140
+
141
+ # Special handling: hookSpecificOutput from context
142
+ if response.context:
143
+ # This is tricky without specific model knowledge, but assuming
144
+ # generic structure or specific model fields.
145
+ # For SessionStartOutput: { continue: bool, message: str, ... }
146
+ # SessionStartOutput has: context: dict[str, Any] | None
147
+
148
+ # If model expects 'context' as dict, but we have string.
149
+ # We identified this mismatch earlier.
150
+ # If input model expects dict, we need to wrap or parse.
151
+ # Let's check the fields of the output model.
152
+ if "context" in output_model_cls.model_fields:
153
+ if isinstance(response.context, str):
154
+ # Wrap string in dict if needed, or just pass if model allows str
155
+ # SessionStartOutput.context is dict[str, Any] | None.
156
+ response_dict["context"] = {"additionalContext": response.context}
157
+ else:
158
+ response_dict["context"] = response.context
159
+
160
+ # Clean None values
161
+ response_dict = {k: v for k, v in response_dict.items() if v is not None}
162
+
163
+ # Allow pydantic to ignore extra fields
164
+ validated_output = output_model_cls.model_validate(response_dict, strict=False)
165
+
166
+ # Call internal broadcast method
167
+ await self.broadcast_hook_event(enum_hook_type, validated_input, validated_output)
168
+
169
+ except Exception as e:
170
+ logger.warning(f"Failed to broadcast event {event.event_type}: {e}")
171
+
172
+ async def broadcast_hook_event(
173
+ self,
174
+ event_type: HookType,
175
+ event_input: HookInput,
176
+ event_output: HookOutput | None = None,
177
+ ) -> None:
178
+ """
179
+ Broadcast a specific hook event type.
180
+
181
+ Args:
182
+ event_type: The type of hook event
183
+ event_input: The input data for the hook
184
+ event_output: The output data from the hook (optional)
185
+ """
186
+ # Checks: WebSocket server implementation required
187
+ if not self.websocket_server:
188
+ return
189
+
190
+ # Checks: Feature enabled
191
+ if not self.config:
192
+ return
193
+
194
+ ws_config = self.config.hook_extensions.websocket
195
+ if not ws_config.enabled:
196
+ return
197
+
198
+ # Checks: Event filtering
199
+ if event_type.value not in ws_config.broadcast_events:
200
+ return
201
+
202
+ try:
203
+ # Construct payload
204
+ payload: dict[str, Any] = {
205
+ "type": "hook_event",
206
+ "event_type": event_type.value,
207
+ "timestamp": datetime.now(UTC).isoformat(),
208
+ }
209
+
210
+ # Add input data if enabled
211
+ if ws_config.include_payload:
212
+ # Convert Pydantic model to dict
213
+ input_data = event_input.model_dump(mode="json", exclude_none=True)
214
+
215
+ # Ensuring privacy/security -> stripping potentially sensitive fields could go here
216
+
217
+ # Add to payload
218
+ payload["data"] = input_data
219
+
220
+ # Add specific fields top-level if needed for convenience
221
+ # e.g. extract session_id from input
222
+ if hasattr(event_input, "external_id"):
223
+ payload["session_id"] = event_input.external_id
224
+ elif hasattr(event_input, "session_id"):
225
+ payload["session_id"] = event_input.session_id
226
+
227
+ # Add output data if present and enabled
228
+ if event_output and ws_config.include_payload:
229
+ output_data = event_output.model_dump(mode="json", exclude_none=True)
230
+ payload["result"] = output_data
231
+
232
+ # Add task context if present
233
+ if hasattr(event_input, "task_id") and event_input.task_id:
234
+ payload["task_id"] = event_input.task_id
235
+ # Include full task context if available in metadata
236
+ if hasattr(event_input, "metadata") and "_task_context" in event_input.metadata:
237
+ payload["task_context"] = event_input.metadata["_task_context"]
238
+
239
+ # Broadcast message
240
+ await self.websocket_server.broadcast(payload)
241
+
242
+ except Exception as e:
243
+ logger.exception(f"Error broadcasting hook event {event_type.value}: {e}")