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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/cli/extensions.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands for hook extensions (hooks, plugins, webhooks).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from gobby.cli.mcp_proxy import call_mcp_api, check_daemon_running, get_daemon_client
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from gobby.hooks.events import HookEventType
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Hooks Commands
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group()
|
|
24
|
+
def hooks() -> None:
|
|
25
|
+
"""Manage hook system configuration and testing."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@hooks.command("list")
|
|
29
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
30
|
+
@click.pass_context
|
|
31
|
+
def hooks_list(ctx: click.Context, json_format: bool) -> None:
|
|
32
|
+
"""List supported hook event types."""
|
|
33
|
+
from gobby.hooks.events import HookEventType
|
|
34
|
+
|
|
35
|
+
hook_types = [{"name": e.value, "description": _get_hook_description(e)} for e in HookEventType]
|
|
36
|
+
|
|
37
|
+
if json_format:
|
|
38
|
+
click.echo(json.dumps(hook_types, indent=2))
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
click.echo("Supported Hook Event Types:")
|
|
42
|
+
click.echo()
|
|
43
|
+
for hook in hook_types:
|
|
44
|
+
click.echo(f" {hook['name']}")
|
|
45
|
+
if hook["description"]:
|
|
46
|
+
click.echo(f" {hook['description']}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_hook_description(event_type: HookEventType) -> str:
|
|
50
|
+
"""Get description for a hook event type."""
|
|
51
|
+
from gobby.hooks.events import HookEventType
|
|
52
|
+
|
|
53
|
+
descriptions = {
|
|
54
|
+
HookEventType.SESSION_START: "Fired when a new session starts",
|
|
55
|
+
HookEventType.SESSION_END: "Fired when a session ends",
|
|
56
|
+
HookEventType.BEFORE_AGENT: "Fired before agent turn starts",
|
|
57
|
+
HookEventType.AFTER_AGENT: "Fired after agent turn completes",
|
|
58
|
+
HookEventType.STOP: "Fired when agent attempts to stop (can block)",
|
|
59
|
+
HookEventType.BEFORE_TOOL: "Fired before a tool is executed (can block)",
|
|
60
|
+
HookEventType.AFTER_TOOL: "Fired after a tool completes",
|
|
61
|
+
HookEventType.BEFORE_TOOL_SELECTION: "Fired before tool selection (Gemini)",
|
|
62
|
+
HookEventType.BEFORE_MODEL: "Fired before model call (Gemini)",
|
|
63
|
+
HookEventType.AFTER_MODEL: "Fired after model call (Gemini)",
|
|
64
|
+
HookEventType.PRE_COMPACT: "Fired before session context is compacted",
|
|
65
|
+
HookEventType.NOTIFICATION: "Notification event from CLI",
|
|
66
|
+
}
|
|
67
|
+
return descriptions.get(event_type, "")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@hooks.command("test")
|
|
71
|
+
@click.argument("hook_type")
|
|
72
|
+
@click.option(
|
|
73
|
+
"--source",
|
|
74
|
+
"-s",
|
|
75
|
+
type=click.Choice(["claude", "gemini", "codex"]),
|
|
76
|
+
default="claude",
|
|
77
|
+
help="Source CLI to simulate",
|
|
78
|
+
)
|
|
79
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
80
|
+
@click.pass_context
|
|
81
|
+
def hooks_test(ctx: click.Context, hook_type: str, source: str, json_format: bool) -> None:
|
|
82
|
+
"""Test a hook by sending a test event to the daemon.
|
|
83
|
+
|
|
84
|
+
HOOK_TYPE is the event type to test (e.g., session-start, before-tool).
|
|
85
|
+
"""
|
|
86
|
+
client = get_daemon_client(ctx)
|
|
87
|
+
if not check_daemon_running(client):
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
|
|
90
|
+
# Build test payload
|
|
91
|
+
test_payload = {
|
|
92
|
+
"hook_type": hook_type,
|
|
93
|
+
"source": source,
|
|
94
|
+
"input_data": {
|
|
95
|
+
"session_id": "test-session-cli",
|
|
96
|
+
"tool_name": "test_tool" if "tool" in hook_type.lower() else None,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
result = call_mcp_api(
|
|
101
|
+
client,
|
|
102
|
+
"/hooks/execute",
|
|
103
|
+
method="POST",
|
|
104
|
+
json_data=test_payload,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if result is None:
|
|
108
|
+
click.echo("Failed to execute test hook", err=True)
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
if json_format:
|
|
112
|
+
click.echo(json.dumps(result, indent=2))
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
click.echo(f"Hook test: {hook_type}")
|
|
116
|
+
click.echo(f" Source: {source}")
|
|
117
|
+
click.echo(f" Continue: {result.get('continue', 'unknown')}")
|
|
118
|
+
if result.get("reason"):
|
|
119
|
+
click.echo(f" Reason: {result.get('reason')}")
|
|
120
|
+
inject_context = result.get("inject_context")
|
|
121
|
+
if inject_context:
|
|
122
|
+
click.echo(f" Context: {str(inject_context)[:100]}...")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@hooks.command("run")
|
|
126
|
+
@click.argument(
|
|
127
|
+
"stage",
|
|
128
|
+
type=click.Choice(["pre-commit", "pre-push", "pre-merge"]),
|
|
129
|
+
)
|
|
130
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show command output")
|
|
131
|
+
@click.option("--dry-run", is_flag=True, help="Show what would run without executing")
|
|
132
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
133
|
+
def hooks_run(stage: str, verbose: bool, dry_run: bool, json_format: bool) -> None:
|
|
134
|
+
"""Run verification commands for a git hook stage.
|
|
135
|
+
|
|
136
|
+
STAGE is the hook stage to run (pre-commit, pre-push, or pre-merge).
|
|
137
|
+
|
|
138
|
+
This command reads verification commands from .gobby/project.json
|
|
139
|
+
and executes them according to the hooks configuration.
|
|
140
|
+
|
|
141
|
+
Example configuration in project.json:
|
|
142
|
+
|
|
143
|
+
\b
|
|
144
|
+
"verification": {
|
|
145
|
+
"lint": "ruff check src/",
|
|
146
|
+
"format": "ruff format --check src/"
|
|
147
|
+
},
|
|
148
|
+
"hooks": {
|
|
149
|
+
"pre-commit": {
|
|
150
|
+
"run": ["lint", "format"],
|
|
151
|
+
"fail_fast": true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
"""
|
|
155
|
+
from gobby.hooks.verification_runner import VerificationRunner
|
|
156
|
+
|
|
157
|
+
runner = VerificationRunner.from_project()
|
|
158
|
+
|
|
159
|
+
# Handle dry-run mode
|
|
160
|
+
if dry_run:
|
|
161
|
+
stage_config = runner.get_stage_config(stage)
|
|
162
|
+
if not stage_config or not stage_config.run:
|
|
163
|
+
click.echo(f"No commands configured for '{stage}'")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
if not runner.verification_config:
|
|
167
|
+
click.echo("No verification commands defined in project.json")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
click.echo(f"Would run for '{stage}':")
|
|
171
|
+
for cmd_name in stage_config.run:
|
|
172
|
+
command = runner.verification_config.get_command(cmd_name)
|
|
173
|
+
if command:
|
|
174
|
+
click.echo(f" {cmd_name}: {command}")
|
|
175
|
+
else:
|
|
176
|
+
click.echo(f" {cmd_name}: (not defined)")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Run the stage
|
|
180
|
+
result = runner.run_stage(stage)
|
|
181
|
+
|
|
182
|
+
# Output as JSON
|
|
183
|
+
if json_format:
|
|
184
|
+
output = {
|
|
185
|
+
"stage": result.stage,
|
|
186
|
+
"success": result.success,
|
|
187
|
+
"skipped": result.skipped,
|
|
188
|
+
"skip_reason": result.skip_reason,
|
|
189
|
+
"results": [
|
|
190
|
+
{
|
|
191
|
+
"name": r.name,
|
|
192
|
+
"command": r.command,
|
|
193
|
+
"success": r.success,
|
|
194
|
+
"exit_code": r.exit_code,
|
|
195
|
+
"duration_ms": r.duration_ms,
|
|
196
|
+
"skipped": r.skipped,
|
|
197
|
+
"skip_reason": r.skip_reason,
|
|
198
|
+
"error": r.error,
|
|
199
|
+
"stdout": r.stdout if verbose else None,
|
|
200
|
+
"stderr": r.stderr if verbose else None,
|
|
201
|
+
}
|
|
202
|
+
for r in result.results
|
|
203
|
+
],
|
|
204
|
+
}
|
|
205
|
+
click.echo(json.dumps(output, indent=2))
|
|
206
|
+
sys.exit(0 if result.success else 1)
|
|
207
|
+
|
|
208
|
+
# Handle skipped stage
|
|
209
|
+
if result.skipped:
|
|
210
|
+
if verbose:
|
|
211
|
+
click.echo(f"Skipped: {result.skip_reason}")
|
|
212
|
+
sys.exit(0)
|
|
213
|
+
|
|
214
|
+
# Display results
|
|
215
|
+
for r in result.results:
|
|
216
|
+
if r.skipped:
|
|
217
|
+
click.echo(click.style(f"⊘ {r.name}: skipped", fg="yellow"))
|
|
218
|
+
if r.skip_reason:
|
|
219
|
+
click.echo(f" {r.skip_reason}")
|
|
220
|
+
elif r.success:
|
|
221
|
+
click.echo(click.style(f"✓ {r.name}", fg="green") + f" ({r.duration_ms}ms)")
|
|
222
|
+
else:
|
|
223
|
+
click.echo(click.style(f"✗ {r.name}", fg="red") + f" ({r.duration_ms}ms)")
|
|
224
|
+
if r.error:
|
|
225
|
+
click.echo(f" Error: {r.error}")
|
|
226
|
+
if verbose and r.stderr:
|
|
227
|
+
click.echo(f" stderr:\n{_indent(r.stderr, 6)}")
|
|
228
|
+
elif r.stderr:
|
|
229
|
+
# Show first line of stderr even without verbose
|
|
230
|
+
first_line = r.stderr.strip().split("\n")[0]
|
|
231
|
+
if first_line:
|
|
232
|
+
click.echo(f" {first_line}")
|
|
233
|
+
|
|
234
|
+
if verbose and r.stdout:
|
|
235
|
+
click.echo(f" stdout:\n{_indent(r.stdout, 6)}")
|
|
236
|
+
|
|
237
|
+
# Summary
|
|
238
|
+
if result.results:
|
|
239
|
+
click.echo()
|
|
240
|
+
click.echo(
|
|
241
|
+
f"Passed: {result.passed_count}, "
|
|
242
|
+
f"Failed: {result.failed_count}, "
|
|
243
|
+
f"Skipped: {result.skipped_count}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
sys.exit(0 if result.success else 1)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _indent(text: str, spaces: int) -> str:
|
|
250
|
+
"""Indent each line of text by the specified number of spaces."""
|
|
251
|
+
prefix = " " * spaces
|
|
252
|
+
return "\n".join(prefix + line for line in text.strip().split("\n"))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@hooks.command("status")
|
|
256
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
257
|
+
def hooks_status(json_format: bool) -> None:
|
|
258
|
+
"""Show current hooks configuration from project.json.
|
|
259
|
+
|
|
260
|
+
Displays which verification commands are configured to run at each
|
|
261
|
+
git hook stage (pre-commit, pre-push, pre-merge).
|
|
262
|
+
"""
|
|
263
|
+
from gobby.utils.project_context import get_hooks_config, get_verification_config
|
|
264
|
+
|
|
265
|
+
verification_config = get_verification_config()
|
|
266
|
+
hooks_config = get_hooks_config()
|
|
267
|
+
|
|
268
|
+
if json_format:
|
|
269
|
+
output = {
|
|
270
|
+
"verification": verification_config.all_commands() if verification_config else {},
|
|
271
|
+
"hooks": {
|
|
272
|
+
"pre-commit": (
|
|
273
|
+
hooks_config.pre_commit.model_dump(by_alias=True) if hooks_config else None
|
|
274
|
+
),
|
|
275
|
+
"pre-push": (
|
|
276
|
+
hooks_config.pre_push.model_dump(by_alias=True) if hooks_config else None
|
|
277
|
+
),
|
|
278
|
+
"pre-merge": (
|
|
279
|
+
hooks_config.pre_merge.model_dump(by_alias=True) if hooks_config else None
|
|
280
|
+
),
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
click.echo(json.dumps(output, indent=2))
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
# Display verification commands
|
|
287
|
+
click.echo("Verification Commands:")
|
|
288
|
+
if verification_config:
|
|
289
|
+
commands = verification_config.all_commands()
|
|
290
|
+
if commands:
|
|
291
|
+
for name, cmd in commands.items():
|
|
292
|
+
click.echo(f" {name}: {cmd}")
|
|
293
|
+
else:
|
|
294
|
+
click.echo(" (none configured)")
|
|
295
|
+
else:
|
|
296
|
+
click.echo(" (none configured)")
|
|
297
|
+
|
|
298
|
+
click.echo()
|
|
299
|
+
|
|
300
|
+
# Display hooks configuration
|
|
301
|
+
click.echo("Hook Stages:")
|
|
302
|
+
if not hooks_config:
|
|
303
|
+
click.echo(" (none configured)")
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
for stage_name, stage_attr in [
|
|
307
|
+
("pre-commit", "pre_commit"),
|
|
308
|
+
("pre-push", "pre_push"),
|
|
309
|
+
("pre-merge", "pre_merge"),
|
|
310
|
+
]:
|
|
311
|
+
stage_config = getattr(hooks_config, stage_attr)
|
|
312
|
+
if stage_config.run:
|
|
313
|
+
status = "enabled" if stage_config.enabled else "disabled"
|
|
314
|
+
click.echo(f" {stage_name} ({status}):")
|
|
315
|
+
click.echo(f" run: {', '.join(stage_config.run)}")
|
|
316
|
+
click.echo(f" fail_fast: {stage_config.fail_fast}")
|
|
317
|
+
click.echo(f" timeout: {stage_config.timeout}s")
|
|
318
|
+
else:
|
|
319
|
+
click.echo(f" {stage_name}: (no commands)")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# =============================================================================
|
|
323
|
+
# Plugins Commands
|
|
324
|
+
# =============================================================================
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@click.group()
|
|
328
|
+
def plugins() -> None:
|
|
329
|
+
"""Manage Python hook plugins."""
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@plugins.command("list")
|
|
333
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
334
|
+
@click.pass_context
|
|
335
|
+
def plugins_list(ctx: click.Context, json_format: bool) -> None:
|
|
336
|
+
"""List loaded plugins."""
|
|
337
|
+
client = get_daemon_client(ctx)
|
|
338
|
+
if not check_daemon_running(client):
|
|
339
|
+
sys.exit(1)
|
|
340
|
+
|
|
341
|
+
result = call_mcp_api(client, "/plugins")
|
|
342
|
+
|
|
343
|
+
if result is None:
|
|
344
|
+
click.echo("Failed to list plugins", err=True)
|
|
345
|
+
sys.exit(1)
|
|
346
|
+
|
|
347
|
+
if json_format:
|
|
348
|
+
click.echo(json.dumps(result, indent=2))
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
plugins_list = result.get("plugins", [])
|
|
352
|
+
enabled = result.get("enabled", False)
|
|
353
|
+
|
|
354
|
+
if not enabled:
|
|
355
|
+
click.echo("Plugin system is disabled in configuration.")
|
|
356
|
+
click.echo("Enable with: plugins.enabled: true in ~/.gobby/config.yaml")
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
if not plugins_list:
|
|
360
|
+
click.echo("No plugins loaded.")
|
|
361
|
+
click.echo()
|
|
362
|
+
click.echo("Plugin directories:")
|
|
363
|
+
for dir_path in result.get("plugin_dirs", []):
|
|
364
|
+
click.echo(f" {dir_path}")
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
click.echo(f"Loaded Plugins ({len(plugins_list)}):")
|
|
368
|
+
click.echo()
|
|
369
|
+
for plugin in plugins_list:
|
|
370
|
+
click.echo(f" {plugin['name']} v{plugin['version']}")
|
|
371
|
+
if plugin.get("description"):
|
|
372
|
+
click.echo(f" {plugin['description']}")
|
|
373
|
+
if plugin.get("handlers"):
|
|
374
|
+
click.echo(f" Handlers: {len(plugin['handlers'])}")
|
|
375
|
+
if plugin.get("actions"):
|
|
376
|
+
click.echo(f" Actions: {', '.join(a['name'] for a in plugin['actions'])}")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@plugins.command("reload")
|
|
380
|
+
@click.argument("plugin_name")
|
|
381
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
382
|
+
@click.pass_context
|
|
383
|
+
def plugins_reload(ctx: click.Context, plugin_name: str, json_format: bool) -> None:
|
|
384
|
+
"""Reload a plugin by name.
|
|
385
|
+
|
|
386
|
+
PLUGIN_NAME is the name of the plugin to reload.
|
|
387
|
+
"""
|
|
388
|
+
client = get_daemon_client(ctx)
|
|
389
|
+
if not check_daemon_running(client):
|
|
390
|
+
sys.exit(1)
|
|
391
|
+
|
|
392
|
+
result = call_mcp_api(
|
|
393
|
+
client,
|
|
394
|
+
"/plugins/reload",
|
|
395
|
+
method="POST",
|
|
396
|
+
json_data={"name": plugin_name},
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if result is None:
|
|
400
|
+
click.echo(f"Failed to reload plugin: {plugin_name}", err=True)
|
|
401
|
+
sys.exit(1)
|
|
402
|
+
|
|
403
|
+
if json_format:
|
|
404
|
+
click.echo(json.dumps(result, indent=2))
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
if result.get("success"):
|
|
408
|
+
click.echo(f"Plugin '{plugin_name}' reloaded successfully.")
|
|
409
|
+
if result.get("version"):
|
|
410
|
+
click.echo(f" Version: {result.get('version')}")
|
|
411
|
+
else:
|
|
412
|
+
click.echo(f"Failed to reload plugin: {result.get('error', 'Unknown error')}", err=True)
|
|
413
|
+
sys.exit(1)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# =============================================================================
|
|
417
|
+
# Webhooks Commands
|
|
418
|
+
# =============================================================================
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@click.group()
|
|
422
|
+
def webhooks() -> None:
|
|
423
|
+
"""Manage webhook endpoints."""
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@webhooks.command("list")
|
|
427
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
428
|
+
@click.pass_context
|
|
429
|
+
def webhooks_list(ctx: click.Context, json_format: bool) -> None:
|
|
430
|
+
"""List configured webhook endpoints."""
|
|
431
|
+
client = get_daemon_client(ctx)
|
|
432
|
+
if not check_daemon_running(client):
|
|
433
|
+
sys.exit(1)
|
|
434
|
+
|
|
435
|
+
result = call_mcp_api(client, "/webhooks")
|
|
436
|
+
|
|
437
|
+
if result is None:
|
|
438
|
+
click.echo("Failed to list webhooks", err=True)
|
|
439
|
+
sys.exit(1)
|
|
440
|
+
|
|
441
|
+
if json_format:
|
|
442
|
+
click.echo(json.dumps(result, indent=2))
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
enabled = result.get("enabled", False)
|
|
446
|
+
endpoints = result.get("endpoints", [])
|
|
447
|
+
|
|
448
|
+
if not enabled:
|
|
449
|
+
click.echo("Webhook system is disabled in configuration.")
|
|
450
|
+
click.echo("Enable with: hook_extensions.webhooks.enabled: true")
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
if not endpoints:
|
|
454
|
+
click.echo("No webhook endpoints configured.")
|
|
455
|
+
click.echo()
|
|
456
|
+
click.echo("Configure webhooks in ~/.gobby/config.yaml:")
|
|
457
|
+
click.echo(" hook_extensions:")
|
|
458
|
+
click.echo(" webhooks:")
|
|
459
|
+
click.echo(" endpoints:")
|
|
460
|
+
click.echo(" - name: my-webhook")
|
|
461
|
+
click.echo(" url: https://example.com/hook")
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
click.echo(f"Webhook Endpoints ({len(endpoints)}):")
|
|
465
|
+
click.echo()
|
|
466
|
+
for endpoint in endpoints:
|
|
467
|
+
status = "enabled" if endpoint.get("enabled", True) else "disabled"
|
|
468
|
+
click.echo(f" {endpoint['name']} [{status}]")
|
|
469
|
+
click.echo(f" URL: {endpoint.get('url', 'not configured')}")
|
|
470
|
+
events = endpoint.get("events", [])
|
|
471
|
+
if events:
|
|
472
|
+
click.echo(f" Events: {', '.join(events)}")
|
|
473
|
+
else:
|
|
474
|
+
click.echo(" Events: all")
|
|
475
|
+
if endpoint.get("can_block"):
|
|
476
|
+
click.echo(" Can block: yes")
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@webhooks.command("test")
|
|
480
|
+
@click.argument("webhook_name")
|
|
481
|
+
@click.option(
|
|
482
|
+
"--event",
|
|
483
|
+
"-e",
|
|
484
|
+
default="notification",
|
|
485
|
+
help="Event type to send (default: notification)",
|
|
486
|
+
)
|
|
487
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
488
|
+
@click.pass_context
|
|
489
|
+
def webhooks_test(ctx: click.Context, webhook_name: str, event: str, json_format: bool) -> None:
|
|
490
|
+
"""Test a webhook endpoint by sending a test event.
|
|
491
|
+
|
|
492
|
+
WEBHOOK_NAME is the name of the webhook endpoint to test.
|
|
493
|
+
"""
|
|
494
|
+
client = get_daemon_client(ctx)
|
|
495
|
+
if not check_daemon_running(client):
|
|
496
|
+
sys.exit(1)
|
|
497
|
+
|
|
498
|
+
result = call_mcp_api(
|
|
499
|
+
client,
|
|
500
|
+
"/webhooks/test",
|
|
501
|
+
method="POST",
|
|
502
|
+
json_data={
|
|
503
|
+
"name": webhook_name,
|
|
504
|
+
"event_type": event,
|
|
505
|
+
},
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if result is None:
|
|
509
|
+
click.echo(f"Failed to test webhook: {webhook_name}", err=True)
|
|
510
|
+
sys.exit(1)
|
|
511
|
+
|
|
512
|
+
if json_format:
|
|
513
|
+
click.echo(json.dumps(result, indent=2))
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
if result.get("success"):
|
|
517
|
+
click.echo(f"Webhook '{webhook_name}' test successful!")
|
|
518
|
+
click.echo(f" Status: {result.get('status_code', 'unknown')}")
|
|
519
|
+
response_time = result.get("response_time_ms")
|
|
520
|
+
if response_time:
|
|
521
|
+
click.echo(f" Response time: {response_time:.0f}ms")
|
|
522
|
+
else:
|
|
523
|
+
click.echo(f"Webhook test failed: {result.get('error', 'Unknown error')}", err=True)
|
|
524
|
+
if result.get("status_code"):
|
|
525
|
+
click.echo(f" Status: {result.get('status_code')}")
|
|
526
|
+
sys.exit(1)
|