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,331 @@
1
+ """
2
+ Example Notify Plugin - Demonstrates Custom Workflow Actions with Schema Validation
3
+
4
+ This plugin demonstrates the full pattern for creating custom workflow actions:
5
+ 1. Schema definition using JSON Schema
6
+ 2. Executor function implementation
7
+ 3. Registration via register_workflow_action()
8
+ 4. Usage in workflow YAML files
9
+
10
+ This is a reference implementation showing best practices for plugin development.
11
+
12
+ Installation:
13
+ 1. Copy this file to ~/.gobby/plugins/example_notify.py
14
+ 2. Enable in ~/.gobby/config.yaml:
15
+ hook_extensions:
16
+ plugins:
17
+ enabled: true
18
+ plugins:
19
+ example-notify:
20
+ enabled: true
21
+ config:
22
+ default_channel: "#general"
23
+ log_file: "~/.gobby/logs/metrics.log"
24
+ 3. Restart gobby daemon: gobby stop && gobby start
25
+
26
+ Usage in Workflows:
27
+ # HTTP notification example
28
+ - action: plugin:example-notify:http_notify
29
+ url: "https://hooks.slack.com/services/xxx"
30
+ method: "POST"
31
+ payload:
32
+ text: "Build completed: {result}"
33
+
34
+ # Metric logging example
35
+ - action: plugin:example-notify:log_metric
36
+ metric_name: "build_duration"
37
+ value: 42.5
38
+ tags:
39
+ project: "my-app"
40
+ environment: "production"
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import json
46
+ from datetime import UTC, datetime
47
+ from pathlib import Path
48
+ from typing import TYPE_CHECKING, Any
49
+
50
+ from gobby.hooks.plugins import HookPlugin
51
+
52
+ if TYPE_CHECKING:
53
+ from gobby.workflows.actions import ActionContext
54
+
55
+
56
+ class ExampleNotifyPlugin(HookPlugin):
57
+ """
58
+ Example plugin demonstrating custom workflow actions with schema validation.
59
+
60
+ This plugin provides two actions:
61
+ - http_notify: Send HTTP notifications (mock implementation for example)
62
+ - log_metric: Log metrics to a file
63
+
64
+ Both actions use register_workflow_action() for schema validation.
65
+ """
66
+
67
+ name = "example-notify"
68
+ version = "1.0.0"
69
+ description = "Example plugin demonstrating workflow actions with schema validation"
70
+
71
+ def __init__(self) -> None:
72
+ super().__init__()
73
+ # Configuration defaults
74
+ self.default_channel: str = "#general"
75
+ self.log_file: Path = Path("~/.gobby/logs/metrics.log").expanduser()
76
+ self._metrics_logged: int = 0
77
+ self._notifications_sent: int = 0
78
+
79
+ def on_load(self, config: dict[str, Any]) -> None:
80
+ """Initialize plugin with configuration and register actions."""
81
+ # Load configuration
82
+ self.default_channel = config.get("default_channel", self.default_channel)
83
+ log_file = config.get("log_file", str(self.log_file))
84
+ self.log_file = Path(log_file).expanduser()
85
+
86
+ self.logger.info(
87
+ f"Example Notify plugin loaded: channel={self.default_channel}, "
88
+ f"log_file={self.log_file}"
89
+ )
90
+
91
+ # =====================================================================
92
+ # PATTERN: Register actions with schema validation
93
+ # =====================================================================
94
+ #
95
+ # Use register_workflow_action() when you want input validation.
96
+ # The schema follows JSON Schema format with 'properties' and 'required'.
97
+ #
98
+ # Actions are available in workflows as: plugin:<plugin-name>:<action-type>
99
+ # Example: plugin:example-notify:http_notify
100
+
101
+ # Register http_notify action with full schema
102
+ self.register_workflow_action(
103
+ action_type="http_notify",
104
+ schema=HTTP_NOTIFY_SCHEMA,
105
+ executor_fn=self._execute_http_notify,
106
+ )
107
+
108
+ # Register log_metric action with full schema
109
+ self.register_workflow_action(
110
+ action_type="log_metric",
111
+ schema=LOG_METRIC_SCHEMA,
112
+ executor_fn=self._execute_log_metric,
113
+ )
114
+
115
+ # =====================================================================
116
+ # ALTERNATIVE: Simple registration without schema
117
+ # =====================================================================
118
+ #
119
+ # Use register_action() for actions that don't need input validation:
120
+ #
121
+ # self.register_action("simple_action", self._execute_simple)
122
+ #
123
+ # This is equivalent to register_workflow_action with an empty schema.
124
+
125
+ def on_unload(self) -> None:
126
+ """Cleanup and log statistics on plugin unload."""
127
+ self.logger.info(
128
+ f"Example Notify stats: notifications_sent={self._notifications_sent}, "
129
+ f"metrics_logged={self._metrics_logged}"
130
+ )
131
+
132
+ # =========================================================================
133
+ # Action Executors
134
+ # =========================================================================
135
+ #
136
+ # Executor functions must be async and follow this signature:
137
+ #
138
+ # async def executor(
139
+ # context: ActionContext,
140
+ # **kwargs: Any
141
+ # ) -> dict[str, Any] | None
142
+ #
143
+ # - context: ActionContext with session info, variables, workflow state
144
+ # - kwargs: Input parameters from the workflow YAML (validated against schema)
145
+ # - Returns: Dict with results (stored in workflow variables if capture_output set)
146
+
147
+ async def _execute_http_notify(
148
+ self,
149
+ context: ActionContext,
150
+ url: str,
151
+ method: str = "POST",
152
+ payload: dict[str, Any] | None = None,
153
+ headers: dict[str, str] | None = None,
154
+ channel: str | None = None,
155
+ **kwargs: Any,
156
+ ) -> dict[str, Any]:
157
+ """
158
+ Execute HTTP notification action.
159
+
160
+ In a real implementation, this would make an actual HTTP request.
161
+ For this example, we simulate the notification and log it.
162
+
163
+ Args:
164
+ context: Workflow action context
165
+ url: Target URL for the notification
166
+ method: HTTP method (GET, POST, PUT, DELETE)
167
+ payload: Request body (JSON-serializable)
168
+ headers: Additional HTTP headers
169
+ channel: Optional channel override
170
+
171
+ Returns:
172
+ Dict with notification result
173
+ """
174
+ effective_channel = channel or self.default_channel
175
+ timestamp = datetime.now(UTC).isoformat()
176
+
177
+ # In a real implementation, you would use aiohttp here:
178
+ #
179
+ # async with aiohttp.ClientSession() as session:
180
+ # async with session.request(method, url, json=payload) as resp:
181
+ # return {"status_code": resp.status, "body": await resp.text()}
182
+ #
183
+ # For this example, we simulate success:
184
+
185
+ self.logger.info(
186
+ f"[SIMULATED] HTTP {method} to {url} | channel={effective_channel} | payload={payload}"
187
+ )
188
+
189
+ self._notifications_sent += 1
190
+
191
+ return {
192
+ "success": True,
193
+ "simulated": True,
194
+ "method": method,
195
+ "url": url,
196
+ "channel": effective_channel,
197
+ "timestamp": timestamp,
198
+ "notification_count": self._notifications_sent,
199
+ }
200
+
201
+ async def _execute_log_metric(
202
+ self,
203
+ context: ActionContext,
204
+ metric_name: str,
205
+ value: int | float,
206
+ tags: dict[str, str] | None = None,
207
+ **kwargs: Any,
208
+ ) -> dict[str, Any]:
209
+ """
210
+ Execute metric logging action.
211
+
212
+ Writes metrics to a log file in JSON Lines format.
213
+
214
+ Args:
215
+ context: Workflow action context
216
+ metric_name: Name of the metric (e.g., "build_duration")
217
+ value: Numeric value of the metric
218
+ tags: Optional key-value tags for the metric
219
+
220
+ Returns:
221
+ Dict with logging result
222
+ """
223
+ timestamp = datetime.now(UTC).isoformat()
224
+
225
+ metric_entry = {
226
+ "timestamp": timestamp,
227
+ "metric": metric_name,
228
+ "value": value,
229
+ "tags": tags or {},
230
+ "session_id": context.session_id if context else None,
231
+ }
232
+
233
+ # Ensure log directory exists
234
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
235
+
236
+ # Append to log file in JSON Lines format
237
+ try:
238
+ with open(self.log_file, "a") as f:
239
+ f.write(json.dumps(metric_entry) + "\n")
240
+
241
+ self._metrics_logged += 1
242
+
243
+ self.logger.debug(f"Logged metric: {metric_name}={value}")
244
+
245
+ return {
246
+ "success": True,
247
+ "metric_name": metric_name,
248
+ "value": value,
249
+ "timestamp": timestamp,
250
+ "log_file": str(self.log_file),
251
+ "metrics_logged": self._metrics_logged,
252
+ }
253
+
254
+ except OSError as e:
255
+ self.logger.error(f"Failed to write metric: {e}")
256
+ return {
257
+ "success": False,
258
+ "error": str(e),
259
+ "metric_name": metric_name,
260
+ "value": value,
261
+ }
262
+
263
+
264
+ # =============================================================================
265
+ # JSON Schema Definitions
266
+ # =============================================================================
267
+ #
268
+ # Schemas define the expected input parameters for workflow actions.
269
+ # They enable validation before execution and serve as documentation.
270
+ #
271
+ # Schema format follows JSON Schema draft-07 with these commonly used fields:
272
+ # - properties: Object mapping parameter names to their schemas
273
+ # - required: Array of required parameter names
274
+ # - type: "string", "number", "integer", "boolean", "array", "object", "null"
275
+ # - description: Human-readable description of the parameter
276
+ # - default: Default value if not provided
277
+ # - enum: Array of allowed values
278
+
279
+ HTTP_NOTIFY_SCHEMA: dict[str, Any] = {
280
+ "type": "object",
281
+ "description": "Send an HTTP notification to a webhook URL",
282
+ "properties": {
283
+ "url": {
284
+ "type": "string",
285
+ "description": "Target URL for the HTTP request (e.g., Slack webhook URL)",
286
+ },
287
+ "method": {
288
+ "type": "string",
289
+ "description": "HTTP method to use",
290
+ "enum": ["GET", "POST", "PUT", "DELETE"],
291
+ "default": "POST",
292
+ },
293
+ "payload": {
294
+ "type": "object",
295
+ "description": "Request body as JSON object",
296
+ },
297
+ "headers": {
298
+ "type": "object",
299
+ "description": "Additional HTTP headers as key-value pairs",
300
+ },
301
+ "channel": {
302
+ "type": "string",
303
+ "description": "Override the default notification channel",
304
+ },
305
+ },
306
+ "required": ["url"],
307
+ }
308
+
309
+ LOG_METRIC_SCHEMA: dict[str, Any] = {
310
+ "type": "object",
311
+ "description": "Log a metric value with optional tags",
312
+ "properties": {
313
+ "metric_name": {
314
+ "type": "string",
315
+ "description": "Name of the metric (e.g., 'build_duration', 'test_count')",
316
+ },
317
+ "value": {
318
+ "type": "number",
319
+ "description": "Numeric value of the metric",
320
+ },
321
+ "tags": {
322
+ "type": "object",
323
+ "description": "Key-value tags for metric categorization",
324
+ },
325
+ },
326
+ "required": ["metric_name", "value"],
327
+ }
328
+
329
+
330
+ # For dynamic discovery, the class must be importable
331
+ __all__ = ["ExampleNotifyPlugin"]
@@ -0,0 +1,10 @@
1
+ """External service integrations for Gobby.
2
+
3
+ This module provides integration classes that delegate to official MCP servers
4
+ for external services like GitHub and Linear.
5
+ """
6
+
7
+ from gobby.integrations.github import GitHubIntegration
8
+ from gobby.integrations.linear import LinearIntegration
9
+
10
+ __all__ = ["GitHubIntegration", "LinearIntegration"]
@@ -0,0 +1,145 @@
1
+ """GitHub integration via official GitHub MCP server.
2
+
3
+ This module provides a GitHubIntegration class that delegates to the official
4
+ GitHub MCP server (@modelcontextprotocol/server-github) for all GitHub operations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from gobby.mcp_proxy.manager import MCPClientManager
14
+
15
+ __all__ = ["GitHubIntegration"]
16
+
17
+
18
+ class GitHubIntegration:
19
+ """Integration with GitHub via the official GitHub MCP server.
20
+
21
+ This class provides a high-level interface for checking GitHub MCP availability
22
+ and generating graceful error messages when the server is unavailable.
23
+
24
+ The integration delegates all actual GitHub operations to the official
25
+ GitHub MCP server, avoiding the need for custom API client code.
26
+
27
+ Attributes:
28
+ server_name: Name of the GitHub MCP server in configuration.
29
+ mcp_manager: The MCPClientManager instance for server access.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ mcp_manager: MCPClientManager,
35
+ server_name: str = "github",
36
+ cache_ttl_seconds: float = 30.0,
37
+ ) -> None:
38
+ """Initialize GitHubIntegration.
39
+
40
+ Args:
41
+ mcp_manager: MCPClientManager instance for accessing MCP servers.
42
+ server_name: Name of the GitHub MCP server. Defaults to "github".
43
+ cache_ttl_seconds: How long to cache availability checks. Defaults to 30s.
44
+ """
45
+ self.mcp_manager = mcp_manager
46
+ self.server_name = server_name
47
+ self._cache_ttl_seconds = cache_ttl_seconds
48
+ self._cached_available: bool | None = None
49
+ self._cache_timestamp: float | None = None
50
+
51
+ def is_available(self) -> bool:
52
+ """Check if GitHub MCP server is available.
53
+
54
+ Returns True if:
55
+ - The server is configured (has_server returns True)
56
+ - The server is connected (health state is "connected")
57
+
58
+ Results are cached for cache_ttl_seconds to avoid excessive checks.
59
+
60
+ Returns:
61
+ True if GitHub MCP server is available, False otherwise.
62
+ """
63
+ # Check cache
64
+ if self._cached_available is not None and self._cache_timestamp is not None:
65
+ age = time.time() - self._cache_timestamp
66
+ if age < self._cache_ttl_seconds:
67
+ return self._cached_available
68
+
69
+ # Check availability
70
+ available = self._check_availability()
71
+
72
+ # Update cache
73
+ self._cached_available = available
74
+ self._cache_timestamp = time.time()
75
+
76
+ return available
77
+
78
+ def _check_availability(self) -> bool:
79
+ """Perform actual availability check without caching."""
80
+ # Check if server is configured
81
+ if not self.mcp_manager.has_server(self.server_name):
82
+ return False
83
+
84
+ # Check if server is connected
85
+ health = self.mcp_manager.health
86
+ if self.server_name not in health:
87
+ return False
88
+
89
+ server_health = health[self.server_name]
90
+ # Handle both object with .state attribute and dict with 'state' key
91
+ state = getattr(server_health, "state", None)
92
+ if state is None and isinstance(server_health, dict):
93
+ state = server_health.get("state")
94
+
95
+ return state == "connected"
96
+
97
+ def clear_cache(self) -> None:
98
+ """Clear the availability cache, forcing next is_available() to check fresh."""
99
+ self._cached_available = None
100
+ self._cache_timestamp = None
101
+
102
+ def get_unavailable_reason(self) -> str | None:
103
+ """Get a human-readable reason why GitHub MCP is unavailable.
104
+
105
+ Returns:
106
+ A string explaining why GitHub is unavailable, or None if available.
107
+ """
108
+ if self.is_available():
109
+ return None
110
+
111
+ # Check if server is configured
112
+ if not self.mcp_manager.has_server(self.server_name):
113
+ return (
114
+ f"GitHub MCP server '{self.server_name}' is not configured. "
115
+ "Add it to your gobby configuration or use `gobby mcp add github`."
116
+ )
117
+
118
+ # Server is configured but not connected
119
+ health = self.mcp_manager.health
120
+ if self.server_name not in health:
121
+ return (
122
+ f"GitHub MCP server '{self.server_name}' has no health status. "
123
+ "The server may not have been started yet."
124
+ )
125
+
126
+ server_health = health[self.server_name]
127
+ state = getattr(server_health, "state", None)
128
+ if state is None and isinstance(server_health, dict):
129
+ state = server_health.get("state")
130
+
131
+ return (
132
+ f"GitHub MCP server '{self.server_name}' is not connected "
133
+ f"(current state: {state}). "
134
+ "Check your GitHub token and server configuration."
135
+ )
136
+
137
+ def require_available(self) -> None:
138
+ """Require that GitHub MCP is available, raising if not.
139
+
140
+ Raises:
141
+ RuntimeError: If GitHub MCP server is unavailable.
142
+ """
143
+ if not self.is_available():
144
+ reason = self.get_unavailable_reason()
145
+ raise RuntimeError(f"GitHub integration unavailable: {reason}")
@@ -0,0 +1,145 @@
1
+ """Linear integration via official Linear MCP server.
2
+
3
+ This module provides a LinearIntegration class that delegates to the official
4
+ Linear MCP server for all Linear operations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from gobby.mcp_proxy.manager import MCPClientManager
14
+
15
+ __all__ = ["LinearIntegration"]
16
+
17
+
18
+ class LinearIntegration:
19
+ """Integration with Linear via the official Linear MCP server.
20
+
21
+ This class provides a high-level interface for checking Linear MCP availability
22
+ and generating graceful error messages when the server is unavailable.
23
+
24
+ The integration delegates all actual Linear operations to the official
25
+ Linear MCP server, avoiding the need for custom API client code.
26
+
27
+ Attributes:
28
+ server_name: Name of the Linear MCP server in configuration.
29
+ mcp_manager: The MCPClientManager instance for server access.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ mcp_manager: MCPClientManager,
35
+ server_name: str = "linear",
36
+ cache_ttl_seconds: float = 30.0,
37
+ ) -> None:
38
+ """Initialize LinearIntegration.
39
+
40
+ Args:
41
+ mcp_manager: MCPClientManager instance for accessing MCP servers.
42
+ server_name: Name of the Linear MCP server. Defaults to "linear".
43
+ cache_ttl_seconds: How long to cache availability checks. Defaults to 30s.
44
+ """
45
+ self.mcp_manager = mcp_manager
46
+ self.server_name = server_name
47
+ self._cache_ttl_seconds = cache_ttl_seconds
48
+ self._cached_available: bool | None = None
49
+ self._cache_timestamp: float | None = None
50
+
51
+ def is_available(self) -> bool:
52
+ """Check if Linear MCP server is available.
53
+
54
+ Returns True if:
55
+ - The server is configured (has_server returns True)
56
+ - The server is connected (health state is "connected")
57
+
58
+ Results are cached for cache_ttl_seconds to avoid excessive checks.
59
+
60
+ Returns:
61
+ True if Linear MCP server is available, False otherwise.
62
+ """
63
+ # Check cache
64
+ if self._cached_available is not None and self._cache_timestamp is not None:
65
+ age = time.time() - self._cache_timestamp
66
+ if age < self._cache_ttl_seconds:
67
+ return self._cached_available
68
+
69
+ # Check availability
70
+ available = self._check_availability()
71
+
72
+ # Update cache
73
+ self._cached_available = available
74
+ self._cache_timestamp = time.time()
75
+
76
+ return available
77
+
78
+ def _check_availability(self) -> bool:
79
+ """Perform actual availability check without caching."""
80
+ # Check if server is configured
81
+ if not self.mcp_manager.has_server(self.server_name):
82
+ return False
83
+
84
+ # Check if server is connected
85
+ health = self.mcp_manager.health
86
+ if self.server_name not in health:
87
+ return False
88
+
89
+ server_health = health[self.server_name]
90
+ # Handle both object with .state attribute and dict with 'state' key
91
+ state = getattr(server_health, "state", None)
92
+ if state is None and isinstance(server_health, dict):
93
+ state = server_health.get("state")
94
+
95
+ return state == "connected"
96
+
97
+ def clear_cache(self) -> None:
98
+ """Clear the availability cache, forcing next is_available() to check fresh."""
99
+ self._cached_available = None
100
+ self._cache_timestamp = None
101
+
102
+ def get_unavailable_reason(self) -> str | None:
103
+ """Get a human-readable reason why Linear MCP is unavailable.
104
+
105
+ Returns:
106
+ A string explaining why Linear is unavailable, or None if available.
107
+ """
108
+ if self.is_available():
109
+ return None
110
+
111
+ # Check if server is configured
112
+ if not self.mcp_manager.has_server(self.server_name):
113
+ return (
114
+ f"Linear MCP server '{self.server_name}' is not configured. "
115
+ "Add it to your gobby configuration or use `gobby mcp add linear`."
116
+ )
117
+
118
+ # Server is configured but not connected
119
+ health = self.mcp_manager.health
120
+ if self.server_name not in health:
121
+ return (
122
+ f"Linear MCP server '{self.server_name}' has no health status. "
123
+ "The server may not have been started yet."
124
+ )
125
+
126
+ server_health = health[self.server_name]
127
+ state = getattr(server_health, "state", None)
128
+ if state is None and isinstance(server_health, dict):
129
+ state = server_health.get("state")
130
+
131
+ return (
132
+ f"Linear MCP server '{self.server_name}' is not connected "
133
+ f"(current state: {state}). "
134
+ "Check your Linear API key and server configuration."
135
+ )
136
+
137
+ def require_available(self) -> None:
138
+ """Require that Linear MCP is available, raising if not.
139
+
140
+ Raises:
141
+ RuntimeError: If Linear MCP server is unavailable.
142
+ """
143
+ if not self.is_available():
144
+ reason = self.get_unavailable_reason()
145
+ raise RuntimeError(f"Linear integration unavailable: {reason}")
gobby/llm/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """
2
+ LLM Provider Abstraction for Gobby Client.
3
+
4
+ This module provides interfaces and implementations for different LLM providers
5
+ (Claude, Codex, Gemini, LiteLLM) to make the client CLI-agnostic.
6
+
7
+ Usage:
8
+ service = create_llm_service(config)
9
+ provider, model, prompt = service.get_provider_for_feature(config.session_summary)
10
+ """
11
+
12
+ from gobby.llm.base import AuthMode, LLMProvider
13
+ from gobby.llm.claude import MCPToolResult, ToolCall
14
+ from gobby.llm.claude_executor import ClaudeExecutor
15
+ from gobby.llm.executor import (
16
+ AgentExecutor,
17
+ AgentResult,
18
+ ToolCallRecord,
19
+ ToolHandler,
20
+ ToolResult,
21
+ ToolSchema,
22
+ )
23
+ from gobby.llm.factory import create_llm_service
24
+ from gobby.llm.service import LLMService
25
+
26
+ __all__ = [
27
+ "AgentExecutor",
28
+ "AgentResult",
29
+ "AuthMode",
30
+ "ClaudeExecutor",
31
+ "LLMProvider",
32
+ "LLMService",
33
+ "MCPToolResult",
34
+ "ToolCall",
35
+ "ToolCallRecord",
36
+ "ToolHandler",
37
+ "ToolResult",
38
+ "ToolSchema",
39
+ "create_llm_service",
40
+ ]