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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Antigravity agent installation for Gobby MCP.
|
|
3
|
+
|
|
4
|
+
This module handles installing Gobby MCP server configuration
|
|
5
|
+
for the Antigravity agent (internal tool).
|
|
6
|
+
|
|
7
|
+
Note: Antigravity does not currently support hooks, so only MCP
|
|
8
|
+
configuration is installed.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .shared import configure_mcp_server_json, install_shared_skills
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def install_antigravity(project_path: Path) -> dict[str, Any]:
|
|
21
|
+
"""Install Gobby integration for Antigravity agent (MCP only).
|
|
22
|
+
|
|
23
|
+
Antigravity does not support hooks, so this only configures
|
|
24
|
+
the MCP server in ~/.gemini/antigravity/mcp_config.json.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
project_path: Path to the project root (unused, kept for API compatibility)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dict with installation results including success status
|
|
31
|
+
"""
|
|
32
|
+
result: dict[str, Any] = {
|
|
33
|
+
"success": False,
|
|
34
|
+
"hooks_installed": [],
|
|
35
|
+
"workflows_installed": [],
|
|
36
|
+
"commands_installed": [],
|
|
37
|
+
"skills_installed": [],
|
|
38
|
+
"mcp_configured": False,
|
|
39
|
+
"mcp_already_configured": False,
|
|
40
|
+
"error": None,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Configure MCP server in Antigravity's MCP config (~/.gemini/antigravity/mcp_config.json)
|
|
44
|
+
mcp_config = Path.home() / ".gemini" / "antigravity" / "mcp_config.json"
|
|
45
|
+
|
|
46
|
+
# Install shared skills to ~/.antigravity/skills/ (Standard Antigravity location)
|
|
47
|
+
try:
|
|
48
|
+
skills_path = Path.home() / ".antigravity" / "skills"
|
|
49
|
+
skills = install_shared_skills(skills_path)
|
|
50
|
+
result["commands_installed"].extend([f"{s} (skill)" for s in skills])
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(f"Failed to install shared skills: {e}")
|
|
53
|
+
# Proceeding despite skill install failure
|
|
54
|
+
|
|
55
|
+
mcp_result = configure_mcp_server_json(mcp_config)
|
|
56
|
+
|
|
57
|
+
if mcp_result["success"]:
|
|
58
|
+
result["mcp_configured"] = mcp_result.get("added", False)
|
|
59
|
+
result["mcp_already_configured"] = mcp_result.get("already_configured", False)
|
|
60
|
+
result["success"] = True
|
|
61
|
+
else:
|
|
62
|
+
result["error"] = mcp_result.get("error", "Unknown error configuring MCP")
|
|
63
|
+
logger.error(f"Failed to configure MCP server: {result['error']}")
|
|
64
|
+
|
|
65
|
+
return result
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Code installation for Gobby hooks.
|
|
3
|
+
|
|
4
|
+
This module handles installing and uninstalling Gobby hooks
|
|
5
|
+
and workflows for Claude Code CLI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from shutil import copy2
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from gobby.cli.utils import get_install_dir
|
|
18
|
+
|
|
19
|
+
from .shared import (
|
|
20
|
+
configure_mcp_server_json,
|
|
21
|
+
install_cli_content,
|
|
22
|
+
install_shared_content,
|
|
23
|
+
install_shared_skills,
|
|
24
|
+
remove_mcp_server_json,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def install_claude(project_path: Path) -> dict[str, Any]:
|
|
31
|
+
"""Install Gobby integration for Claude Code (hooks, workflows).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
project_path: Path to the project root
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dict with installation results including success status and installed items
|
|
38
|
+
"""
|
|
39
|
+
hooks_installed: list[str] = []
|
|
40
|
+
result: dict[str, Any] = {
|
|
41
|
+
"success": False,
|
|
42
|
+
"hooks_installed": hooks_installed,
|
|
43
|
+
"workflows_installed": [],
|
|
44
|
+
"commands_installed": [],
|
|
45
|
+
"mcp_configured": False,
|
|
46
|
+
"mcp_already_configured": False,
|
|
47
|
+
"error": None,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
claude_path = project_path / ".claude"
|
|
51
|
+
settings_file = claude_path / "settings.json"
|
|
52
|
+
|
|
53
|
+
# Ensure .claude subdirectories exist
|
|
54
|
+
claude_path.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
hooks_dir = claude_path / "hooks"
|
|
56
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
# Get source files
|
|
59
|
+
install_dir = get_install_dir()
|
|
60
|
+
claude_install_dir = install_dir / "claude"
|
|
61
|
+
install_hooks_dir = claude_install_dir / "hooks"
|
|
62
|
+
|
|
63
|
+
# Hook files to copy
|
|
64
|
+
hook_files = {
|
|
65
|
+
"hook_dispatcher.py": True, # Make executable
|
|
66
|
+
"validate_settings.py": True, # Make executable
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
source_hooks_template = claude_install_dir / "hooks-template.json"
|
|
70
|
+
|
|
71
|
+
# Verify all source files exist
|
|
72
|
+
missing_files = []
|
|
73
|
+
for filename in hook_files.keys():
|
|
74
|
+
source_file = install_hooks_dir / filename
|
|
75
|
+
if not source_file.exists():
|
|
76
|
+
missing_files.append(str(source_file))
|
|
77
|
+
|
|
78
|
+
if not source_hooks_template.exists():
|
|
79
|
+
missing_files.append(str(source_hooks_template))
|
|
80
|
+
|
|
81
|
+
if missing_files:
|
|
82
|
+
result["error"] = f"Missing source files: {missing_files}"
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
# Copy hook files
|
|
86
|
+
try:
|
|
87
|
+
for filename, make_executable in hook_files.items():
|
|
88
|
+
source_file = install_hooks_dir / filename
|
|
89
|
+
target_file = hooks_dir / filename
|
|
90
|
+
|
|
91
|
+
if target_file.exists():
|
|
92
|
+
target_file.unlink()
|
|
93
|
+
|
|
94
|
+
copy2(source_file, target_file)
|
|
95
|
+
if make_executable:
|
|
96
|
+
target_file.chmod(0o755)
|
|
97
|
+
except OSError as e:
|
|
98
|
+
logger.error(f"Failed to copy hook files: {e}")
|
|
99
|
+
result["error"] = f"Failed to copy hook files: {e}"
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
# Install shared content (workflows)
|
|
103
|
+
try:
|
|
104
|
+
shared = install_shared_content(claude_path, project_path)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"Failed to install shared content: {e}")
|
|
107
|
+
result["error"] = f"Failed to install shared content: {e}"
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
# Install CLI-specific content (can override shared)
|
|
111
|
+
try:
|
|
112
|
+
cli = install_cli_content("claude", claude_path)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Failed to install CLI content: {e}")
|
|
115
|
+
result["error"] = f"Failed to install CLI content: {e}"
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
result["workflows_installed"] = shared["workflows"] + cli["workflows"]
|
|
119
|
+
result["commands_installed"] = cli.get("commands", [])
|
|
120
|
+
result["plugins_installed"] = shared.get("plugins", [])
|
|
121
|
+
|
|
122
|
+
# Install shared skills (SKILL.md)
|
|
123
|
+
try:
|
|
124
|
+
skills = install_shared_skills(claude_path / "skills")
|
|
125
|
+
result["commands_installed"].extend([f"{s} (skill)" for s in skills])
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"Failed to install shared skills: {e}")
|
|
128
|
+
result["error"] = f"Failed to install shared skills: {e}"
|
|
129
|
+
# Proceeding despite skill install failure
|
|
130
|
+
|
|
131
|
+
# Backup existing settings.json if it exists
|
|
132
|
+
backup_file = None
|
|
133
|
+
if settings_file.exists():
|
|
134
|
+
timestamp = int(time.time())
|
|
135
|
+
backup_file = claude_path / f"settings.json.{timestamp}.backup"
|
|
136
|
+
try:
|
|
137
|
+
copy2(settings_file, backup_file)
|
|
138
|
+
except OSError as e:
|
|
139
|
+
logger.error(f"Failed to create backup of settings.json: {e}")
|
|
140
|
+
result["error"] = f"Failed to create backup: {e}"
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
# Verify backup exists
|
|
144
|
+
if not backup_file.exists():
|
|
145
|
+
logger.error("Backup file was not created successfully")
|
|
146
|
+
result["error"] = "Backup file was not created successfully"
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
# Load existing settings or create empty
|
|
150
|
+
existing_settings: dict[str, Any] = {}
|
|
151
|
+
if settings_file.exists():
|
|
152
|
+
try:
|
|
153
|
+
with open(settings_file) as f:
|
|
154
|
+
existing_settings = json.load(f)
|
|
155
|
+
except json.JSONDecodeError as e:
|
|
156
|
+
logger.error(f"Failed to parse settings.json: {e}")
|
|
157
|
+
result["error"] = f"Failed to parse settings.json: {e}"
|
|
158
|
+
return result
|
|
159
|
+
except OSError as e:
|
|
160
|
+
logger.error(f"Failed to read settings.json: {e}")
|
|
161
|
+
result["error"] = f"Failed to read settings.json: {e}"
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
# Load Gobby hooks from template
|
|
165
|
+
try:
|
|
166
|
+
with open(source_hooks_template) as f:
|
|
167
|
+
gobby_settings_str = f.read()
|
|
168
|
+
except OSError as e:
|
|
169
|
+
logger.error(f"Failed to read hooks template: {e}")
|
|
170
|
+
result["error"] = f"Failed to read hooks template: {e}"
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
# Replace $PROJECT_PATH with absolute project path
|
|
174
|
+
abs_project_path = str(project_path.resolve())
|
|
175
|
+
gobby_settings_str = gobby_settings_str.replace("$PROJECT_PATH", abs_project_path)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
gobby_settings = json.loads(gobby_settings_str)
|
|
179
|
+
except json.JSONDecodeError as e:
|
|
180
|
+
logger.error(f"Failed to parse hooks template: {e}")
|
|
181
|
+
result["error"] = f"Failed to parse hooks template: {e}"
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
# Ensure hooks section exists
|
|
185
|
+
if "hooks" not in existing_settings:
|
|
186
|
+
existing_settings["hooks"] = {}
|
|
187
|
+
|
|
188
|
+
# Merge Gobby hooks
|
|
189
|
+
gobby_hooks = gobby_settings.get("hooks", {})
|
|
190
|
+
for hook_type, hook_config in gobby_hooks.items():
|
|
191
|
+
existing_settings["hooks"][hook_type] = hook_config
|
|
192
|
+
hooks_installed.append(hook_type)
|
|
193
|
+
|
|
194
|
+
# Write merged settings back using atomic write
|
|
195
|
+
try:
|
|
196
|
+
fd, temp_path = tempfile.mkstemp(dir=str(claude_path), suffix=".tmp", prefix="settings_")
|
|
197
|
+
try:
|
|
198
|
+
with os.fdopen(fd, "w") as f:
|
|
199
|
+
json.dump(existing_settings, f, indent=2)
|
|
200
|
+
f.flush()
|
|
201
|
+
os.fsync(f.fileno())
|
|
202
|
+
# Atomic replace
|
|
203
|
+
os.replace(temp_path, settings_file)
|
|
204
|
+
except Exception:
|
|
205
|
+
# Clean up temp file if it still exists
|
|
206
|
+
if os.path.exists(temp_path):
|
|
207
|
+
os.unlink(temp_path)
|
|
208
|
+
raise
|
|
209
|
+
except OSError as e:
|
|
210
|
+
logger.error(f"Failed to write settings.json: {e}")
|
|
211
|
+
# Attempt to restore from backup if we have one
|
|
212
|
+
if backup_file and backup_file.exists():
|
|
213
|
+
try:
|
|
214
|
+
copy2(backup_file, settings_file)
|
|
215
|
+
logger.info("Restored settings.json from backup after write failure")
|
|
216
|
+
except OSError as restore_error:
|
|
217
|
+
logger.error(f"Failed to restore from backup: {restore_error}")
|
|
218
|
+
result["error"] = f"Failed to write settings.json: {e}"
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
# Configure MCP server in global settings (~/.claude.json)
|
|
222
|
+
# Note: Claude Code uses ~/.claude.json for user-scoped MCP servers
|
|
223
|
+
global_settings = Path.home() / ".claude.json"
|
|
224
|
+
mcp_result = configure_mcp_server_json(global_settings)
|
|
225
|
+
if mcp_result["success"]:
|
|
226
|
+
result["mcp_configured"] = mcp_result.get("added", False)
|
|
227
|
+
result["mcp_already_configured"] = mcp_result.get("already_configured", False)
|
|
228
|
+
else:
|
|
229
|
+
# MCP config failure is non-fatal, just log it
|
|
230
|
+
logger.warning(f"Failed to configure MCP server: {mcp_result['error']}")
|
|
231
|
+
|
|
232
|
+
result["success"] = True
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def uninstall_claude(project_path: Path) -> dict[str, Any]:
|
|
237
|
+
"""Uninstall Gobby integration from Claude Code.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
project_path: Path to the project root
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Dict with uninstallation results including success status and removed items
|
|
244
|
+
"""
|
|
245
|
+
hooks_removed: list[str] = []
|
|
246
|
+
files_removed: list[str] = []
|
|
247
|
+
|
|
248
|
+
result: dict[str, Any] = {
|
|
249
|
+
"success": False,
|
|
250
|
+
"hooks_removed": hooks_removed,
|
|
251
|
+
"files_removed": files_removed,
|
|
252
|
+
"mcp_removed": False,
|
|
253
|
+
"error": None,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
claude_path = project_path / ".claude"
|
|
257
|
+
settings_file = claude_path / "settings.json"
|
|
258
|
+
hooks_dir = claude_path / "hooks"
|
|
259
|
+
|
|
260
|
+
if not settings_file.exists():
|
|
261
|
+
result["error"] = f"Settings file not found: {settings_file}"
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
# Backup settings.json with verification
|
|
265
|
+
timestamp = int(time.time())
|
|
266
|
+
backup_file = claude_path / f"settings.json.{timestamp}.backup"
|
|
267
|
+
try:
|
|
268
|
+
copy2(settings_file, backup_file)
|
|
269
|
+
except OSError as e:
|
|
270
|
+
logger.error(f"Failed to create backup of settings.json: {e}")
|
|
271
|
+
result["error"] = f"Failed to create backup: {e}"
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
# Verify backup exists before proceeding
|
|
275
|
+
if not backup_file.exists():
|
|
276
|
+
logger.error("Backup file was not created successfully")
|
|
277
|
+
result["error"] = "Backup file was not created successfully"
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
# Read and parse settings.json
|
|
281
|
+
try:
|
|
282
|
+
with open(settings_file) as f:
|
|
283
|
+
settings = json.load(f)
|
|
284
|
+
except json.JSONDecodeError as e:
|
|
285
|
+
logger.error(f"Failed to parse settings.json: {e}")
|
|
286
|
+
result["error"] = f"Failed to parse settings.json: {e}"
|
|
287
|
+
return result
|
|
288
|
+
except OSError as e:
|
|
289
|
+
logger.error(f"Failed to read settings.json: {e}")
|
|
290
|
+
result["error"] = f"Failed to read settings.json: {e}"
|
|
291
|
+
return result
|
|
292
|
+
|
|
293
|
+
if "hooks" in settings:
|
|
294
|
+
hook_types = [
|
|
295
|
+
"SessionStart",
|
|
296
|
+
"SessionEnd",
|
|
297
|
+
"UserPromptSubmit",
|
|
298
|
+
"PreToolUse",
|
|
299
|
+
"PostToolUse",
|
|
300
|
+
"PreCompact",
|
|
301
|
+
"Notification",
|
|
302
|
+
"Stop",
|
|
303
|
+
"SubagentStart",
|
|
304
|
+
"SubagentStop",
|
|
305
|
+
"PermissionRequest",
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
for hook_type in hook_types:
|
|
309
|
+
if hook_type in settings["hooks"]:
|
|
310
|
+
del settings["hooks"][hook_type]
|
|
311
|
+
hooks_removed.append(hook_type)
|
|
312
|
+
|
|
313
|
+
# Write to temp file and atomically replace
|
|
314
|
+
try:
|
|
315
|
+
# Create temp file in same directory for atomic replace
|
|
316
|
+
fd, temp_path = tempfile.mkstemp(
|
|
317
|
+
dir=str(claude_path), suffix=".tmp", prefix="settings_"
|
|
318
|
+
)
|
|
319
|
+
try:
|
|
320
|
+
with os.fdopen(fd, "w") as f:
|
|
321
|
+
json.dump(settings, f, indent=2)
|
|
322
|
+
f.flush()
|
|
323
|
+
os.fsync(f.fileno())
|
|
324
|
+
# Atomic replace
|
|
325
|
+
os.replace(temp_path, settings_file)
|
|
326
|
+
except Exception:
|
|
327
|
+
# Clean up temp file if it still exists
|
|
328
|
+
if os.path.exists(temp_path):
|
|
329
|
+
os.unlink(temp_path)
|
|
330
|
+
raise
|
|
331
|
+
except OSError as e:
|
|
332
|
+
logger.error(f"Failed to write settings.json: {e}")
|
|
333
|
+
# Attempt to restore from backup
|
|
334
|
+
try:
|
|
335
|
+
copy2(backup_file, settings_file)
|
|
336
|
+
logger.info("Restored settings.json from backup after write failure")
|
|
337
|
+
except OSError as restore_error:
|
|
338
|
+
logger.error(f"Failed to restore from backup: {restore_error}")
|
|
339
|
+
result["error"] = f"Failed to write settings.json: {e}"
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
# Remove hook files
|
|
343
|
+
hook_files = [
|
|
344
|
+
"hook_dispatcher.py",
|
|
345
|
+
"validate_settings.py",
|
|
346
|
+
"README.md",
|
|
347
|
+
"HOOK_SCHEMAS.md",
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
for filename in hook_files:
|
|
351
|
+
file_path = hooks_dir / filename
|
|
352
|
+
if file_path.exists():
|
|
353
|
+
file_path.unlink()
|
|
354
|
+
files_removed.append(filename)
|
|
355
|
+
|
|
356
|
+
# Remove MCP server from global settings (~/.claude.json)
|
|
357
|
+
global_settings = Path.home() / ".claude.json"
|
|
358
|
+
mcp_result = remove_mcp_server_json(global_settings)
|
|
359
|
+
if mcp_result["success"]:
|
|
360
|
+
result["mcp_removed"] = mcp_result.get("removed", False)
|
|
361
|
+
|
|
362
|
+
result["success"] = True
|
|
363
|
+
return result
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codex CLI installation for Gobby hooks.
|
|
3
|
+
|
|
4
|
+
This module handles installing and uninstalling Gobby notify integration
|
|
5
|
+
for OpenAI Codex CLI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from shutil import copy2
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from gobby.cli.utils import get_install_dir
|
|
16
|
+
|
|
17
|
+
from .shared import (
|
|
18
|
+
configure_mcp_server_toml,
|
|
19
|
+
install_cli_content,
|
|
20
|
+
install_shared_content,
|
|
21
|
+
install_shared_skills,
|
|
22
|
+
remove_mcp_server_toml,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def install_codex_notify() -> dict[str, Any]:
|
|
29
|
+
"""Install Codex notify script and configure ~/.codex/config.toml.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Dict with installation results including success status and installed items
|
|
33
|
+
"""
|
|
34
|
+
files_installed: list[str] = []
|
|
35
|
+
result: dict[str, Any] = {
|
|
36
|
+
"success": False,
|
|
37
|
+
"files_installed": files_installed,
|
|
38
|
+
"workflows_installed": [],
|
|
39
|
+
"commands_installed": [],
|
|
40
|
+
"config_updated": False,
|
|
41
|
+
"mcp_configured": False,
|
|
42
|
+
"mcp_already_configured": False,
|
|
43
|
+
"error": None,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
install_dir = get_install_dir()
|
|
47
|
+
source_notify = install_dir / "codex" / "hooks" / "hook_dispatcher.py"
|
|
48
|
+
if not source_notify.exists():
|
|
49
|
+
result["error"] = f"Missing source file: {source_notify}"
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
# Install hook dispatcher to ~/.gobby/hooks/codex/hook_dispatcher.py
|
|
53
|
+
notify_dir = Path.home() / ".gobby" / "hooks" / "codex"
|
|
54
|
+
notify_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
target_notify = notify_dir / "hook_dispatcher.py"
|
|
56
|
+
|
|
57
|
+
if target_notify.exists():
|
|
58
|
+
target_notify.unlink()
|
|
59
|
+
|
|
60
|
+
copy2(source_notify, target_notify)
|
|
61
|
+
target_notify.chmod(0o755)
|
|
62
|
+
files_installed.append(str(target_notify))
|
|
63
|
+
|
|
64
|
+
# Install shared content - workflows to ~/.gobby
|
|
65
|
+
codex_home = Path.home() / ".codex"
|
|
66
|
+
gobby_home = Path.home() # workflows go to ~/.gobby/workflows/
|
|
67
|
+
|
|
68
|
+
shared = install_shared_content(codex_home, gobby_home)
|
|
69
|
+
# Install CLI-specific content (can override shared)
|
|
70
|
+
cli = install_cli_content("codex", codex_home)
|
|
71
|
+
|
|
72
|
+
# Install shared skills (SKILL.md)
|
|
73
|
+
try:
|
|
74
|
+
skills = install_shared_skills(codex_home / "skills")
|
|
75
|
+
result["commands_installed"].extend([f"{s} (skill)" for s in skills])
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Failed to install shared skills: {e}")
|
|
78
|
+
# Proceeding despite skill install failure
|
|
79
|
+
|
|
80
|
+
result["workflows_installed"] = shared["workflows"] + cli["workflows"]
|
|
81
|
+
result["commands_installed"] = cli.get("commands", [])
|
|
82
|
+
result["plugins_installed"] = shared.get("plugins", [])
|
|
83
|
+
|
|
84
|
+
# Update ~/.codex/config.toml
|
|
85
|
+
codex_config_dir = codex_home
|
|
86
|
+
|
|
87
|
+
codex_config_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
codex_config_path = codex_config_dir / "config.toml"
|
|
89
|
+
|
|
90
|
+
notify_command = ["python3", str(target_notify)]
|
|
91
|
+
notify_line = f"notify = {json.dumps(notify_command)}"
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
if codex_config_path.exists():
|
|
95
|
+
existing = codex_config_path.read_text(encoding="utf-8")
|
|
96
|
+
else:
|
|
97
|
+
existing = ""
|
|
98
|
+
|
|
99
|
+
pattern = re.compile(r"(?m)^\s*notify\s*=.*$")
|
|
100
|
+
if pattern.search(existing):
|
|
101
|
+
updated = pattern.sub(notify_line, existing)
|
|
102
|
+
else:
|
|
103
|
+
updated = (existing.rstrip() + "\n\n" if existing.strip() else "") + notify_line + "\n"
|
|
104
|
+
|
|
105
|
+
if updated != existing:
|
|
106
|
+
if codex_config_path.exists():
|
|
107
|
+
backup_path = codex_config_path.with_suffix(".toml.bak")
|
|
108
|
+
backup_path.write_text(existing, encoding="utf-8")
|
|
109
|
+
|
|
110
|
+
codex_config_path.write_text(updated, encoding="utf-8")
|
|
111
|
+
result["config_updated"] = True
|
|
112
|
+
|
|
113
|
+
# Configure MCP server in global config (~/.codex/config.toml)
|
|
114
|
+
mcp_result = configure_mcp_server_toml(codex_config_path)
|
|
115
|
+
if mcp_result["success"]:
|
|
116
|
+
result["mcp_configured"] = mcp_result.get("added", False)
|
|
117
|
+
result["mcp_already_configured"] = mcp_result.get("already_configured", False)
|
|
118
|
+
else:
|
|
119
|
+
# MCP config failure is non-fatal, just log it
|
|
120
|
+
logger.warning(f"Failed to configure MCP server: {mcp_result['error']}")
|
|
121
|
+
|
|
122
|
+
result["success"] = True
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
result["error"] = f"Failed to update Codex config: {e}"
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def uninstall_codex_notify() -> dict[str, Any]:
|
|
131
|
+
"""Uninstall Codex notify script and remove from ~/.codex/config.toml.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dict with uninstallation results including success status and removed items
|
|
135
|
+
"""
|
|
136
|
+
files_removed: list[str] = []
|
|
137
|
+
result: dict[str, Any] = {
|
|
138
|
+
"success": False,
|
|
139
|
+
"files_removed": files_removed,
|
|
140
|
+
"config_updated": False,
|
|
141
|
+
"mcp_removed": False,
|
|
142
|
+
"error": None,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Remove hook dispatcher from ~/.gobby/hooks/codex/hook_dispatcher.py
|
|
146
|
+
notify_file = Path.home() / ".gobby" / "hooks" / "codex" / "hook_dispatcher.py"
|
|
147
|
+
if notify_file.exists():
|
|
148
|
+
notify_file.unlink()
|
|
149
|
+
files_removed.append(str(notify_file))
|
|
150
|
+
|
|
151
|
+
# Try to remove empty parent directories
|
|
152
|
+
notify_dir = notify_file.parent
|
|
153
|
+
try:
|
|
154
|
+
if notify_dir.exists() and not any(notify_dir.iterdir()):
|
|
155
|
+
notify_dir.rmdir()
|
|
156
|
+
except Exception:
|
|
157
|
+
pass # nosec B110 - best-effort cleanup, directory removal is non-critical
|
|
158
|
+
|
|
159
|
+
# Update ~/.codex/config.toml to remove notify line
|
|
160
|
+
codex_config_path = Path.home() / ".codex" / "config.toml"
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
if codex_config_path.exists():
|
|
164
|
+
existing = codex_config_path.read_text(encoding="utf-8")
|
|
165
|
+
|
|
166
|
+
# Remove notify = [...] line
|
|
167
|
+
pattern = re.compile(r"(?m)^\s*notify\s*=.*$\n?")
|
|
168
|
+
if pattern.search(existing):
|
|
169
|
+
updated = pattern.sub("", existing)
|
|
170
|
+
|
|
171
|
+
# Clean up multiple blank lines
|
|
172
|
+
updated = re.sub(r"\n{3,}", "\n\n", updated)
|
|
173
|
+
|
|
174
|
+
if updated != existing:
|
|
175
|
+
# Backup before modifying
|
|
176
|
+
backup_path = codex_config_path.with_suffix(".toml.bak")
|
|
177
|
+
backup_path.write_text(existing, encoding="utf-8")
|
|
178
|
+
|
|
179
|
+
codex_config_path.write_text(updated, encoding="utf-8")
|
|
180
|
+
result["config_updated"] = True
|
|
181
|
+
|
|
182
|
+
# Remove MCP server from config
|
|
183
|
+
mcp_result = remove_mcp_server_toml(codex_config_path)
|
|
184
|
+
if mcp_result["success"]:
|
|
185
|
+
result["mcp_removed"] = mcp_result.get("removed", False)
|
|
186
|
+
|
|
187
|
+
result["success"] = True
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
result["error"] = f"Failed to update Codex config: {e}"
|
|
192
|
+
return result
|