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,153 @@
1
+ #!/usr/bin/env python3
2
+ """Codex notify script - forwards interactive Codex events to gobby.
3
+
4
+ Codex supports a `notify = [...]` command in `~/.codex/config.toml`. This script is
5
+ intended to be installed to `~/.gobby/hooks/codex/notify.py` and configured as that
6
+ notify command.
7
+
8
+ Codex notify is currently treated as fire-and-forget; this script should never
9
+ block the CLI for long or fail the Codex command on daemon errors.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ import httpx
21
+
22
+ DEFAULT_DAEMON_PORT = 8765
23
+ DEFAULT_CONFIG_PATH = "~/.gobby/config.yaml"
24
+ DEBUG_ENV_VAR = "GOBBY_CODEX_NOTIFY_DEBUG"
25
+
26
+
27
+ def _silence_output() -> None:
28
+ """Prevent notify script output from polluting the interactive Codex UI."""
29
+ if os.environ.get(DEBUG_ENV_VAR):
30
+ return
31
+
32
+ try:
33
+ devnull = open(os.devnull, "w", encoding="utf-8") # noqa: SIM115
34
+ sys.stdout = devnull
35
+ sys.stderr = devnull
36
+ except Exception:
37
+ # nosec B110 - if silencing fails, still avoid raising/printing
38
+ pass
39
+
40
+
41
+ def _get_daemon_url() -> str:
42
+ config_path = Path(DEFAULT_CONFIG_PATH).expanduser()
43
+
44
+ port = DEFAULT_DAEMON_PORT
45
+ if config_path.exists():
46
+ try:
47
+ import yaml
48
+
49
+ with config_path.open(encoding="utf-8") as f:
50
+ config = yaml.safe_load(f) or {}
51
+ port = int(config.get("daemon_port", DEFAULT_DAEMON_PORT))
52
+ except Exception:
53
+ port = DEFAULT_DAEMON_PORT
54
+
55
+ return f"http://localhost:{port}"
56
+
57
+
58
+ def _read_event_from_stdin() -> dict[str, Any] | None:
59
+ try:
60
+ raw = sys.stdin.read()
61
+ except Exception:
62
+ return None
63
+
64
+ if not raw.strip():
65
+ return None
66
+
67
+ try:
68
+ parsed = json.loads(raw)
69
+ except Exception:
70
+ return {"raw": raw}
71
+
72
+ if isinstance(parsed, dict):
73
+ return parsed
74
+ return {"event": parsed}
75
+
76
+
77
+ def _extract_text_from_messages(messages: Any) -> str:
78
+ if not isinstance(messages, list):
79
+ return ""
80
+ for message in reversed(messages):
81
+ if not isinstance(message, dict):
82
+ continue
83
+ text = message.get("text") or message.get("content")
84
+ if isinstance(text, str) and text:
85
+ return text
86
+ return ""
87
+
88
+
89
+ def _normalize_input_data(event: dict[str, Any] | None) -> dict[str, Any]:
90
+ event = event or {}
91
+
92
+ thread_id = (
93
+ event.get("session_id")
94
+ or event.get("thread_id")
95
+ or event.get("threadId")
96
+ or event.get("conversation_id")
97
+ or event.get("conversationId")
98
+ )
99
+ if not thread_id and isinstance(event.get("thread"), dict):
100
+ thread_id = event["thread"].get("id")
101
+ if not thread_id and isinstance(event.get("session"), dict):
102
+ thread_id = event["session"].get("id")
103
+
104
+ messages = event.get("input_messages") or event.get("inputMessages") or event.get("messages")
105
+ last_message = (
106
+ event.get("last_message")
107
+ or event.get("lastMessage")
108
+ or event.get("message")
109
+ or _extract_text_from_messages(messages)
110
+ )
111
+
112
+ event_type = (
113
+ event.get("event_type")
114
+ or event.get("eventType")
115
+ or event.get("type")
116
+ or event.get("name")
117
+ or "agent-turn-complete"
118
+ )
119
+
120
+ return {
121
+ "session_id": thread_id or "",
122
+ "event_type": event_type,
123
+ "last_message": last_message or "",
124
+ "input_messages": messages if isinstance(messages, list) else [],
125
+ }
126
+
127
+
128
+ def main() -> int:
129
+ _silence_output()
130
+
131
+ event = _read_event_from_stdin()
132
+ input_data = _normalize_input_data(event)
133
+
134
+ daemon_url = _get_daemon_url()
135
+
136
+ try:
137
+ httpx.post(
138
+ f"{daemon_url}/hooks/execute",
139
+ json={
140
+ "hook_type": "AgentTurnComplete",
141
+ "input_data": input_data,
142
+ "source": "codex",
143
+ },
144
+ timeout=2.0,
145
+ )
146
+ except Exception:
147
+ return 0
148
+
149
+ return 0
150
+
151
+
152
+ if __name__ == "__main__":
153
+ raise SystemExit(main())
@@ -0,0 +1,7 @@
1
+ # Forget
2
+
3
+ Delete a memory by ID using gobby-memory MCP tools.
4
+
5
+ Use the `forget` tool on the `gobby-memory` server with the memory ID provided.
6
+
7
+ Confirm the memory was deleted.
@@ -0,0 +1,7 @@
1
+ # Memories
2
+
3
+ List all stored memories using gobby-memory MCP tools.
4
+
5
+ Use the `list_memories` tool on the `gobby-memory` server.
6
+
7
+ Display memories in a table format with: ID, Type, Importance, Content (truncated).
@@ -0,0 +1,7 @@
1
+ # Recall
2
+
3
+ Search and retrieve memories using gobby-memory MCP tools.
4
+
5
+ Use the `recall` tool on the `gobby-memory` server with the query provided.
6
+
7
+ Display matching memories with their IDs, types, and importance scores.
@@ -0,0 +1,13 @@
1
+ # Remember
2
+
3
+ Store a memory for future sessions using gobby-memory MCP tools.
4
+
5
+ Use the `remember` tool on the `gobby-memory` server with the content provided.
6
+
7
+ **Memory types:**
8
+ - `preference` - User preferences and coding style choices
9
+ - `fact` - Project facts and technical details
10
+ - `pattern` - Recurring code patterns
11
+ - `context` - Background project context
12
+
13
+ After storing, confirm the memory was saved and show its ID.
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = [
5
+ # "httpx",
6
+ # "pyyaml",
7
+ # ]
8
+ # ///
9
+ """Hook Dispatcher - Routes Gemini CLI hooks to HookManager.
10
+
11
+ This is a thin wrapper script that receives hook calls from Gemini CLI
12
+ and routes them to the appropriate handler via HookManager.
13
+
14
+ Gemini CLI invokes hooks with JSON input on stdin and expects JSON output
15
+ on stdout. Exit codes: 0 = allow, 2 = deny.
16
+
17
+ Usage:
18
+ hook_dispatcher.py --type SessionStart < input.json > output.json
19
+ hook_dispatcher.py --type BeforeTool --debug < input.json > output.json
20
+
21
+ Exit Codes:
22
+ 0 - Success / Allow
23
+ 1 - General error (logged, continues)
24
+ 2 - Deny / Block
25
+ """
26
+
27
+ import argparse
28
+ import json
29
+ import sys
30
+ from pathlib import Path
31
+
32
+ # Default daemon configuration
33
+ DEFAULT_DAEMON_PORT = 8765
34
+ DEFAULT_CONFIG_PATH = "~/.gobby/config.yaml"
35
+
36
+
37
+ def get_daemon_url() -> str:
38
+ """Get the daemon HTTP URL from config file.
39
+
40
+ Reads daemon_port from ~/.gobby/config.yaml if it exists,
41
+ otherwise uses the default port 8765.
42
+
43
+ Returns:
44
+ Full daemon URL like http://localhost:8765
45
+ """
46
+ config_path = Path(DEFAULT_CONFIG_PATH).expanduser()
47
+
48
+ if config_path.exists():
49
+ try:
50
+ import yaml
51
+
52
+ with open(config_path) as f:
53
+ config = yaml.safe_load(f) or {}
54
+ port = config.get("daemon_port", DEFAULT_DAEMON_PORT)
55
+ except Exception:
56
+ # If config read fails, use default
57
+ port = DEFAULT_DAEMON_PORT
58
+ else:
59
+ port = DEFAULT_DAEMON_PORT
60
+
61
+ return f"http://localhost:{port}"
62
+
63
+
64
+ def parse_arguments() -> argparse.Namespace:
65
+ """Parse command line arguments.
66
+
67
+ Returns:
68
+ Parsed arguments with type and debug flags
69
+ """
70
+ parser = argparse.ArgumentParser(description="Gemini CLI Hook Dispatcher")
71
+ parser.add_argument(
72
+ "--type",
73
+ required=True,
74
+ help="Hook type (e.g., SessionStart, BeforeTool)",
75
+ )
76
+ parser.add_argument(
77
+ "--debug",
78
+ action="store_true",
79
+ help="Enable debug logging",
80
+ )
81
+ return parser.parse_args()
82
+
83
+
84
+ def check_daemon_running(timeout: float = 0.5) -> bool:
85
+ """Check if gobby daemon is active and responding.
86
+
87
+ Performs a quick health check to verify the HTTP server is running
88
+ before processing hooks. This prevents hook execution when the daemon
89
+ is stopped, avoiding long timeouts and confusing error messages.
90
+
91
+ Args:
92
+ timeout: Maximum time to wait for response in seconds (default: 0.5)
93
+
94
+ Returns:
95
+ True if daemon is running and responding, False otherwise
96
+ """
97
+ try:
98
+ import httpx
99
+
100
+ daemon_url = get_daemon_url()
101
+ response = httpx.get(
102
+ f"{daemon_url}/admin/status",
103
+ timeout=timeout,
104
+ follow_redirects=False,
105
+ )
106
+ return response.status_code == 200
107
+ except Exception:
108
+ # Any error (connection refused, timeout, etc.) means client is not running
109
+ return False
110
+
111
+
112
+ def main() -> int:
113
+ """Main dispatcher execution.
114
+
115
+ Returns:
116
+ Exit code (0=allow, 1=error, 2=deny)
117
+ """
118
+ try:
119
+ # Parse arguments
120
+ args = parse_arguments()
121
+ except (argparse.ArgumentError, SystemExit):
122
+ # Argument parsing failed - return empty dict and exit 1
123
+ print(json.dumps({}))
124
+ return 1
125
+
126
+ hook_type = args.type # PascalCase: SessionStart, BeforeTool, etc.
127
+ debug_mode = args.debug
128
+
129
+ # Check if gobby daemon is running before processing hooks
130
+ if not check_daemon_running():
131
+ # Daemon is not running - return gracefully without processing
132
+ print(
133
+ json.dumps({"status": "daemon_not_running", "message": "gobby daemon is not running"})
134
+ )
135
+ return 0 # Exit 0 (allow) - this is expected behavior, not an error
136
+
137
+ # Setup logger for dispatcher (not HookManager)
138
+ import logging
139
+
140
+ logger = logging.getLogger("gobby.hooks.gemini.dispatcher")
141
+ if debug_mode:
142
+ logging.basicConfig(level=logging.DEBUG)
143
+ else:
144
+ logging.basicConfig(level=logging.INFO)
145
+
146
+ try:
147
+ # Read JSON input from stdin
148
+ input_data = json.load(sys.stdin)
149
+
150
+ # Log what Gemini CLI sends us (for debugging hook data issues)
151
+ logger.info(f"[{hook_type}] Received input keys: {list(input_data.keys())}")
152
+
153
+ # Log hook-specific critical fields
154
+ if hook_type == "SessionStart":
155
+ logger.info(f"[SessionStart] session_id={input_data.get('session_id')}")
156
+ elif hook_type == "SessionEnd":
157
+ logger.info(
158
+ f"[SessionEnd] session_id={input_data.get('session_id')}, "
159
+ f"reason={input_data.get('reason')}"
160
+ )
161
+ elif hook_type == "BeforeAgent":
162
+ prompt = input_data.get("prompt", "")
163
+ prompt_preview = prompt[:100] + "..." if len(prompt) > 100 else prompt
164
+ logger.info(
165
+ f"[BeforeAgent] session_id={input_data.get('session_id')}, prompt={prompt_preview}"
166
+ )
167
+ elif hook_type == "BeforeTool":
168
+ tool_name = input_data.get("tool_name") or input_data.get("function_name", "unknown")
169
+ logger.info(
170
+ f"[BeforeTool] tool_name={tool_name}, session_id={input_data.get('session_id')}"
171
+ )
172
+ elif hook_type == "AfterTool":
173
+ tool_name = input_data.get("tool_name") or input_data.get("function_name", "unknown")
174
+ logger.info(
175
+ f"[AfterTool] tool_name={tool_name}, session_id={input_data.get('session_id')}"
176
+ )
177
+ elif hook_type == "BeforeToolSelection":
178
+ logger.info(f"[BeforeToolSelection] session_id={input_data.get('session_id')}")
179
+ elif hook_type == "BeforeModel":
180
+ logger.info(
181
+ f"[BeforeModel] session_id={input_data.get('session_id')}, "
182
+ f"model={input_data.get('model', 'unknown')}"
183
+ )
184
+ elif hook_type == "AfterModel":
185
+ logger.info(f"[AfterModel] session_id={input_data.get('session_id')}")
186
+ elif hook_type == "PreCompress":
187
+ logger.info(f"[PreCompress] session_id={input_data.get('session_id')}")
188
+ elif hook_type == "Notification":
189
+ logger.info(
190
+ f"[Notification] session_id={input_data.get('session_id')}, "
191
+ f"message={input_data.get('message')}"
192
+ )
193
+ elif hook_type == "AfterAgent":
194
+ logger.info(f"[AfterAgent] session_id={input_data.get('session_id')}")
195
+
196
+ if debug_mode:
197
+ logger.debug(f"Input data: {input_data}")
198
+
199
+ except json.JSONDecodeError as e:
200
+ # Invalid JSON input - return empty dict and exit 1
201
+ if debug_mode:
202
+ logger.error(f"JSON decode error: {e}")
203
+ print(json.dumps({}))
204
+ return 1
205
+
206
+ # Call daemon HTTP endpoint
207
+ import httpx
208
+
209
+ daemon_url = get_daemon_url()
210
+ try:
211
+ response = httpx.post(
212
+ f"{daemon_url}/hooks/execute",
213
+ json={
214
+ "hook_type": hook_type, # PascalCase for Gemini
215
+ "input_data": input_data,
216
+ "source": "gemini", # Required: identifies CLI source
217
+ },
218
+ timeout=30.0, # Generous timeout for hook processing
219
+ )
220
+
221
+ if response.status_code == 200:
222
+ # Success - daemon returns result directly
223
+ result = response.json()
224
+
225
+ if debug_mode:
226
+ logger.debug(f"Output data: {result}")
227
+
228
+ # Determine exit code based on decision
229
+ decision = result.get("decision", "allow")
230
+
231
+ # Print JSON output for Gemini CLI
232
+ if result and result != {}:
233
+ print(json.dumps(result))
234
+
235
+ # Exit code: 0 = allow, 2 = deny
236
+ if decision == "deny":
237
+ return 2
238
+ return 0
239
+ else:
240
+ # HTTP error from daemon
241
+ error_detail = response.text
242
+ logger.error(
243
+ f"Daemon returned error: status={response.status_code}, detail={error_detail}"
244
+ )
245
+ print(json.dumps({"status": "error", "message": f"Daemon error: {error_detail}"}))
246
+ return 1
247
+
248
+ except httpx.ConnectError:
249
+ # Daemon not reachable
250
+ logger.error("Failed to connect to daemon (unreachable)")
251
+ print(json.dumps({"status": "error", "message": "Daemon unreachable"}))
252
+ return 1
253
+
254
+ except httpx.TimeoutException:
255
+ # Hook processing took too long
256
+ logger.error(f"Hook execution timeout: {hook_type}")
257
+ print(json.dumps({"status": "error", "message": "Hook execution timeout"}))
258
+ return 1
259
+
260
+ except Exception as e:
261
+ # General error - log and return 1
262
+ logger.error(f"Hook execution failed: {e}", exc_info=True)
263
+ print(json.dumps({"status": "error", "message": str(e)}))
264
+ return 1
265
+
266
+
267
+ if __name__ == "__main__":
268
+ sys.exit(main())
@@ -0,0 +1,138 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "name": "gobby-session-start",
8
+ "type": "command",
9
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=SessionStart",
10
+ "timeout": 30000
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "SessionEnd": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "name": "gobby-session-end",
20
+ "type": "command",
21
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=SessionEnd",
22
+ "timeout": 30000
23
+ }
24
+ ]
25
+ }
26
+ ],
27
+ "BeforeAgent": [
28
+ {
29
+ "hooks": [
30
+ {
31
+ "name": "gobby-before-agent",
32
+ "type": "command",
33
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=BeforeAgent",
34
+ "timeout": 30000
35
+ }
36
+ ]
37
+ }
38
+ ],
39
+ "AfterAgent": [
40
+ {
41
+ "hooks": [
42
+ {
43
+ "name": "gobby-after-agent",
44
+ "type": "command",
45
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=AfterAgent",
46
+ "timeout": 30000
47
+ }
48
+ ]
49
+ }
50
+ ],
51
+ "BeforeTool": [
52
+ {
53
+ "matcher": "*",
54
+ "hooks": [
55
+ {
56
+ "name": "gobby-before-tool",
57
+ "type": "command",
58
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=BeforeTool",
59
+ "timeout": 30000
60
+ }
61
+ ]
62
+ }
63
+ ],
64
+ "AfterTool": [
65
+ {
66
+ "matcher": "*",
67
+ "hooks": [
68
+ {
69
+ "name": "gobby-after-tool",
70
+ "type": "command",
71
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=AfterTool",
72
+ "timeout": 30000
73
+ }
74
+ ]
75
+ }
76
+ ],
77
+ "BeforeToolSelection": [
78
+ {
79
+ "hooks": [
80
+ {
81
+ "name": "gobby-before-tool-selection",
82
+ "type": "command",
83
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=BeforeToolSelection",
84
+ "timeout": 30000
85
+ }
86
+ ]
87
+ }
88
+ ],
89
+ "BeforeModel": [
90
+ {
91
+ "hooks": [
92
+ {
93
+ "name": "gobby-before-model",
94
+ "type": "command",
95
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=BeforeModel",
96
+ "timeout": 30000
97
+ }
98
+ ]
99
+ }
100
+ ],
101
+ "AfterModel": [
102
+ {
103
+ "hooks": [
104
+ {
105
+ "name": "gobby-after-model",
106
+ "type": "command",
107
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=AfterModel",
108
+ "timeout": 30000
109
+ }
110
+ ]
111
+ }
112
+ ],
113
+ "PreCompress": [
114
+ {
115
+ "hooks": [
116
+ {
117
+ "name": "gobby-pre-compress",
118
+ "type": "command",
119
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=PreCompress",
120
+ "timeout": 30000
121
+ }
122
+ ]
123
+ }
124
+ ],
125
+ "Notification": [
126
+ {
127
+ "hooks": [
128
+ {
129
+ "name": "gobby-notification",
130
+ "type": "command",
131
+ "command": "uv run python \"$PROJECT_PATH/.gemini/hooks/hook_dispatcher.py\" --type=Notification",
132
+ "timeout": 30000
133
+ }
134
+ ]
135
+ }
136
+ ]
137
+ }
138
+ }