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,737 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared content installation for Gobby hooks.
|
|
3
|
+
|
|
4
|
+
This module handles installing shared workflows and plugins
|
|
5
|
+
that are used across all CLI integrations (Claude, Gemini, Codex, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from shutil import copy2, copytree
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from gobby.cli.utils import get_install_dir
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list[str]]:
|
|
23
|
+
"""Install shared content from src/install/shared/.
|
|
24
|
+
|
|
25
|
+
Workflows are cross-CLI and go to {project_path}/.gobby/workflows/.
|
|
26
|
+
Plugins are global and go to ~/.gobby/plugins/.
|
|
27
|
+
Docs are project-local and go to {project_path}/.gobby/docs/.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
cli_path: Path to CLI config directory (e.g., .claude, .gemini)
|
|
31
|
+
project_path: Path to project root
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dict with lists of installed items by type
|
|
35
|
+
"""
|
|
36
|
+
shared_dir = get_install_dir() / "shared"
|
|
37
|
+
installed: dict[str, list[str]] = {"workflows": [], "plugins": [], "docs": []}
|
|
38
|
+
|
|
39
|
+
# Install shared workflows to .gobby/workflows/ (cross-CLI)
|
|
40
|
+
shared_workflows = shared_dir / "workflows"
|
|
41
|
+
if shared_workflows.exists():
|
|
42
|
+
target_workflows = project_path / ".gobby" / "workflows"
|
|
43
|
+
target_workflows.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
for workflow_file in shared_workflows.iterdir():
|
|
45
|
+
if workflow_file.is_file():
|
|
46
|
+
copy2(workflow_file, target_workflows / workflow_file.name)
|
|
47
|
+
installed["workflows"].append(workflow_file.name)
|
|
48
|
+
|
|
49
|
+
# Install shared plugins to ~/.gobby/plugins/ (global)
|
|
50
|
+
shared_plugins = shared_dir / "plugins"
|
|
51
|
+
if shared_plugins.exists():
|
|
52
|
+
target_plugins = Path("~/.gobby/plugins").expanduser()
|
|
53
|
+
target_plugins.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
for plugin_file in shared_plugins.iterdir():
|
|
55
|
+
if plugin_file.is_file() and plugin_file.suffix == ".py":
|
|
56
|
+
copy2(plugin_file, target_plugins / plugin_file.name)
|
|
57
|
+
installed["plugins"].append(plugin_file.name)
|
|
58
|
+
|
|
59
|
+
# Install shared docs to .gobby/docs/ (project-local)
|
|
60
|
+
shared_docs = shared_dir / "docs"
|
|
61
|
+
if shared_docs.exists():
|
|
62
|
+
target_docs = project_path / ".gobby" / "docs"
|
|
63
|
+
target_docs.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
for doc_file in shared_docs.iterdir():
|
|
65
|
+
if doc_file.is_file():
|
|
66
|
+
copy2(doc_file, target_docs / doc_file.name)
|
|
67
|
+
installed["docs"].append(doc_file.name)
|
|
68
|
+
|
|
69
|
+
return installed
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def install_shared_skills(target_dir: Path) -> list[str]:
|
|
73
|
+
"""Install shared SKILL.md files to target directory.
|
|
74
|
+
|
|
75
|
+
Copies skills from src/gobby/install/shared/skills/ to target_dir.
|
|
76
|
+
Backs up existing SKILL.md if content differs.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
target_dir: Directory where skills should be installed (e.g. .claude/skills)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
List of installed skill names
|
|
83
|
+
"""
|
|
84
|
+
shared_skills_dir = get_install_dir() / "shared" / "skills"
|
|
85
|
+
installed: list[str] = []
|
|
86
|
+
|
|
87
|
+
if not shared_skills_dir.exists():
|
|
88
|
+
return installed
|
|
89
|
+
|
|
90
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
|
|
92
|
+
for skill_path in shared_skills_dir.iterdir():
|
|
93
|
+
if not skill_path.is_dir():
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
skill_name = skill_path.name
|
|
97
|
+
source_skill_md = skill_path / "SKILL.md"
|
|
98
|
+
|
|
99
|
+
if not source_skill_md.exists():
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# Target: target_dir/skill_name/SKILL.md
|
|
103
|
+
target_skill_dir = target_dir / skill_name
|
|
104
|
+
target_skill_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
target_skill_md = target_skill_dir / "SKILL.md"
|
|
106
|
+
|
|
107
|
+
# Backup if exists and differs
|
|
108
|
+
if target_skill_md.exists():
|
|
109
|
+
try:
|
|
110
|
+
# Read both to compare
|
|
111
|
+
source_content = source_skill_md.read_text(encoding="utf-8")
|
|
112
|
+
target_content = target_skill_md.read_text(encoding="utf-8")
|
|
113
|
+
|
|
114
|
+
if source_content != target_content:
|
|
115
|
+
timestamp = int(time.time())
|
|
116
|
+
backup_path = target_skill_md.with_suffix(f".md.{timestamp}.backup")
|
|
117
|
+
target_skill_md.rename(backup_path)
|
|
118
|
+
except OSError as e:
|
|
119
|
+
logger.warning(f"Failed to backup/read skill {skill_name}: {e}")
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# Copy new file
|
|
123
|
+
try:
|
|
124
|
+
copy2(source_skill_md, target_skill_md)
|
|
125
|
+
installed.append(skill_name)
|
|
126
|
+
except OSError as e:
|
|
127
|
+
logger.error(f"Failed to copy skill {skill_name}: {e}")
|
|
128
|
+
|
|
129
|
+
return installed
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def install_cli_content(cli_name: str, target_path: Path) -> dict[str, list[str]]:
|
|
133
|
+
"""Install CLI-specific workflows/commands (layered on top of shared).
|
|
134
|
+
|
|
135
|
+
CLI-specific content can add to or override shared content.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
cli_name: Name of the CLI (e.g., "claude", "gemini", "codex")
|
|
139
|
+
target_path: Path to CLI config directory
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dict with lists of installed items by type
|
|
143
|
+
"""
|
|
144
|
+
cli_dir = get_install_dir() / cli_name
|
|
145
|
+
installed: dict[str, list[str]] = {"workflows": [], "commands": []}
|
|
146
|
+
|
|
147
|
+
# CLI-specific workflows
|
|
148
|
+
cli_workflows = cli_dir / "workflows"
|
|
149
|
+
if cli_workflows.exists():
|
|
150
|
+
target_workflows = target_path / "workflows"
|
|
151
|
+
target_workflows.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
for workflow_file in cli_workflows.iterdir():
|
|
153
|
+
if workflow_file.is_file():
|
|
154
|
+
copy2(workflow_file, target_workflows / workflow_file.name)
|
|
155
|
+
installed["workflows"].append(workflow_file.name)
|
|
156
|
+
|
|
157
|
+
# CLI-specific commands (slash commands)
|
|
158
|
+
# Claude/Gemini: commands/, Codex: prompts/
|
|
159
|
+
for cmd_dir_name in ["commands", "prompts"]:
|
|
160
|
+
cli_commands = cli_dir / cmd_dir_name
|
|
161
|
+
if cli_commands.exists():
|
|
162
|
+
target_commands = target_path / cmd_dir_name
|
|
163
|
+
target_commands.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
for item in cli_commands.iterdir():
|
|
165
|
+
if item.is_dir():
|
|
166
|
+
# Directory of commands (e.g., memory/)
|
|
167
|
+
target_subdir = target_commands / item.name
|
|
168
|
+
if target_subdir.exists():
|
|
169
|
+
shutil.rmtree(target_subdir)
|
|
170
|
+
copytree(item, target_subdir)
|
|
171
|
+
installed["commands"].append(f"{item.name}/")
|
|
172
|
+
elif item.is_file():
|
|
173
|
+
# Single command file
|
|
174
|
+
copy2(item, target_commands / item.name)
|
|
175
|
+
installed["commands"].append(item.name)
|
|
176
|
+
|
|
177
|
+
return installed
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def configure_project_mcp_server(project_path: Path, server_name: str = "gobby") -> dict[str, Any]:
|
|
181
|
+
"""Add Gobby MCP server to project-specific config in ~/.claude.json.
|
|
182
|
+
|
|
183
|
+
Claude Code stores project-specific MCP servers in:
|
|
184
|
+
{
|
|
185
|
+
"projects": {
|
|
186
|
+
"/path/to/project": {
|
|
187
|
+
"mcpServers": { "gobby": { ... } }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
project_path: Path to the project root
|
|
194
|
+
server_name: Name for the MCP server entry (default: "gobby")
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dict with 'success', 'added', 'already_configured', 'backup_path', and 'error' keys
|
|
198
|
+
"""
|
|
199
|
+
result: dict[str, Any] = {
|
|
200
|
+
"success": False,
|
|
201
|
+
"added": False,
|
|
202
|
+
"already_configured": False,
|
|
203
|
+
"backup_path": None,
|
|
204
|
+
"error": None,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
settings_path = Path.home() / ".claude.json"
|
|
208
|
+
abs_project_path = str(project_path.resolve())
|
|
209
|
+
|
|
210
|
+
# Load existing settings or create empty
|
|
211
|
+
existing_settings: dict[str, Any] = {}
|
|
212
|
+
if settings_path.exists():
|
|
213
|
+
try:
|
|
214
|
+
with open(settings_path) as f:
|
|
215
|
+
existing_settings = json.load(f)
|
|
216
|
+
except json.JSONDecodeError as e:
|
|
217
|
+
result["error"] = f"Failed to parse {settings_path}: {e}"
|
|
218
|
+
return result
|
|
219
|
+
except OSError as e:
|
|
220
|
+
result["error"] = f"Failed to read {settings_path}: {e}"
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
# Ensure projects section exists
|
|
224
|
+
if "projects" not in existing_settings:
|
|
225
|
+
existing_settings["projects"] = {}
|
|
226
|
+
|
|
227
|
+
# Ensure project entry exists
|
|
228
|
+
if abs_project_path not in existing_settings["projects"]:
|
|
229
|
+
existing_settings["projects"][abs_project_path] = {}
|
|
230
|
+
|
|
231
|
+
project_settings = existing_settings["projects"][abs_project_path]
|
|
232
|
+
|
|
233
|
+
# Ensure mcpServers section exists in project
|
|
234
|
+
if "mcpServers" not in project_settings:
|
|
235
|
+
project_settings["mcpServers"] = {}
|
|
236
|
+
|
|
237
|
+
# Check if already configured
|
|
238
|
+
if server_name in project_settings["mcpServers"]:
|
|
239
|
+
result["success"] = True
|
|
240
|
+
result["already_configured"] = True
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
# Create backup if file exists
|
|
244
|
+
if settings_path.exists():
|
|
245
|
+
timestamp = int(time.time())
|
|
246
|
+
backup_path = settings_path.parent / f".claude.json.{timestamp}.backup"
|
|
247
|
+
try:
|
|
248
|
+
copy2(settings_path, backup_path)
|
|
249
|
+
result["backup_path"] = str(backup_path)
|
|
250
|
+
except OSError as e:
|
|
251
|
+
result["error"] = f"Failed to create backup: {e}"
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
# Add gobby MCP server config
|
|
255
|
+
project_settings["mcpServers"][server_name] = {
|
|
256
|
+
"type": "stdio",
|
|
257
|
+
"command": "uv",
|
|
258
|
+
"args": ["run", "gobby", "mcp-server"],
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# Write updated settings
|
|
262
|
+
try:
|
|
263
|
+
with open(settings_path, "w") as f:
|
|
264
|
+
json.dump(existing_settings, f, indent=2)
|
|
265
|
+
except OSError as e:
|
|
266
|
+
result["error"] = f"Failed to write {settings_path}: {e}"
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
result["success"] = True
|
|
270
|
+
result["added"] = True
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def remove_project_mcp_server(project_path: Path, server_name: str = "gobby") -> dict[str, Any]:
|
|
275
|
+
"""Remove Gobby MCP server from project-specific config in ~/.claude.json.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
project_path: Path to the project root
|
|
279
|
+
server_name: Name of the MCP server entry to remove
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Dict with 'success', 'removed', 'backup_path', and 'error' keys
|
|
283
|
+
"""
|
|
284
|
+
result: dict[str, Any] = {
|
|
285
|
+
"success": False,
|
|
286
|
+
"removed": False,
|
|
287
|
+
"backup_path": None,
|
|
288
|
+
"error": None,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
settings_path = Path.home() / ".claude.json"
|
|
292
|
+
abs_project_path = str(project_path.resolve())
|
|
293
|
+
|
|
294
|
+
if not settings_path.exists():
|
|
295
|
+
result["success"] = True
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
with open(settings_path) as f:
|
|
300
|
+
settings = json.load(f)
|
|
301
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
302
|
+
result["error"] = f"Failed to read {settings_path}: {e}"
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
# Check if project and server exist
|
|
306
|
+
projects = settings.get("projects", {})
|
|
307
|
+
project_settings = projects.get(abs_project_path, {})
|
|
308
|
+
mcp_servers = project_settings.get("mcpServers", {})
|
|
309
|
+
|
|
310
|
+
if server_name not in mcp_servers:
|
|
311
|
+
result["success"] = True
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
# Create backup
|
|
315
|
+
timestamp = int(time.time())
|
|
316
|
+
backup_path = settings_path.parent / f".claude.json.{timestamp}.backup"
|
|
317
|
+
try:
|
|
318
|
+
copy2(settings_path, backup_path)
|
|
319
|
+
result["backup_path"] = str(backup_path)
|
|
320
|
+
except OSError as e:
|
|
321
|
+
result["error"] = f"Failed to create backup: {e}"
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
# Remove the server
|
|
325
|
+
del mcp_servers[server_name]
|
|
326
|
+
|
|
327
|
+
# Write updated settings
|
|
328
|
+
try:
|
|
329
|
+
with open(settings_path, "w") as f:
|
|
330
|
+
json.dump(settings, f, indent=2)
|
|
331
|
+
except OSError as e:
|
|
332
|
+
result["error"] = f"Failed to write {settings_path}: {e}"
|
|
333
|
+
return result
|
|
334
|
+
|
|
335
|
+
result["success"] = True
|
|
336
|
+
result["removed"] = True
|
|
337
|
+
return result
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def configure_mcp_server_json(settings_path: Path, server_name: str = "gobby") -> dict[str, Any]:
|
|
341
|
+
"""Add Gobby MCP server to a JSON settings file (Claude, Gemini, Antigravity).
|
|
342
|
+
|
|
343
|
+
Merges the gobby MCP server config into the existing mcpServers section,
|
|
344
|
+
preserving all other servers. Creates a timestamped backup before modifying.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
settings_path: Path to the settings.json file (e.g., ~/.claude/settings.json)
|
|
348
|
+
server_name: Name for the MCP server entry (default: "gobby")
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Dict with 'success', 'added', 'backup_path', and 'error' keys
|
|
352
|
+
"""
|
|
353
|
+
result: dict[str, Any] = {
|
|
354
|
+
"success": False,
|
|
355
|
+
"added": False,
|
|
356
|
+
"already_configured": False,
|
|
357
|
+
"backup_path": None,
|
|
358
|
+
"error": None,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Ensure parent directory exists
|
|
362
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
363
|
+
|
|
364
|
+
# Load existing settings or create empty
|
|
365
|
+
existing_settings: dict[str, Any] = {}
|
|
366
|
+
if settings_path.exists():
|
|
367
|
+
try:
|
|
368
|
+
with open(settings_path) as f:
|
|
369
|
+
existing_settings = json.load(f)
|
|
370
|
+
except json.JSONDecodeError as e:
|
|
371
|
+
result["error"] = f"Failed to parse {settings_path}: {e}"
|
|
372
|
+
return result
|
|
373
|
+
except OSError as e:
|
|
374
|
+
result["error"] = f"Failed to read {settings_path}: {e}"
|
|
375
|
+
return result
|
|
376
|
+
|
|
377
|
+
# Check if already configured
|
|
378
|
+
if "mcpServers" in existing_settings and server_name in existing_settings["mcpServers"]:
|
|
379
|
+
result["success"] = True
|
|
380
|
+
result["already_configured"] = True
|
|
381
|
+
return result
|
|
382
|
+
|
|
383
|
+
# Create backup if file exists
|
|
384
|
+
if settings_path.exists():
|
|
385
|
+
timestamp = int(time.time())
|
|
386
|
+
backup_path = settings_path.parent / f"{settings_path.name}.{timestamp}.backup"
|
|
387
|
+
try:
|
|
388
|
+
copy2(settings_path, backup_path)
|
|
389
|
+
result["backup_path"] = str(backup_path)
|
|
390
|
+
except OSError as e:
|
|
391
|
+
result["error"] = f"Failed to create backup: {e}"
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
# Ensure mcpServers section exists
|
|
395
|
+
if "mcpServers" not in existing_settings:
|
|
396
|
+
existing_settings["mcpServers"] = {}
|
|
397
|
+
|
|
398
|
+
# Add gobby MCP server config
|
|
399
|
+
# Use 'uv run gobby' since most users won't have gobby installed globally
|
|
400
|
+
existing_settings["mcpServers"][server_name] = {
|
|
401
|
+
"command": "uv",
|
|
402
|
+
"args": ["run", "gobby", "mcp-server"],
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# Write updated settings
|
|
406
|
+
try:
|
|
407
|
+
with open(settings_path, "w") as f:
|
|
408
|
+
json.dump(existing_settings, f, indent=2)
|
|
409
|
+
except OSError as e:
|
|
410
|
+
result["error"] = f"Failed to write {settings_path}: {e}"
|
|
411
|
+
return result
|
|
412
|
+
|
|
413
|
+
result["success"] = True
|
|
414
|
+
result["added"] = True
|
|
415
|
+
return result
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def remove_mcp_server_json(settings_path: Path, server_name: str = "gobby") -> dict[str, Any]:
|
|
419
|
+
"""Remove Gobby MCP server from a JSON settings file.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
settings_path: Path to the settings.json file
|
|
423
|
+
server_name: Name of the MCP server entry to remove
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Dict with 'success', 'removed', 'backup_path', and 'error' keys
|
|
427
|
+
"""
|
|
428
|
+
result: dict[str, Any] = {
|
|
429
|
+
"success": False,
|
|
430
|
+
"removed": False,
|
|
431
|
+
"backup_path": None,
|
|
432
|
+
"error": None,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if not settings_path.exists():
|
|
436
|
+
result["success"] = True
|
|
437
|
+
return result
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
with open(settings_path) as f:
|
|
441
|
+
settings = json.load(f)
|
|
442
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
443
|
+
result["error"] = f"Failed to read {settings_path}: {e}"
|
|
444
|
+
return result
|
|
445
|
+
|
|
446
|
+
# Check if server exists
|
|
447
|
+
if "mcpServers" not in settings or server_name not in settings["mcpServers"]:
|
|
448
|
+
result["success"] = True
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
# Create backup
|
|
452
|
+
timestamp = int(time.time())
|
|
453
|
+
backup_path = settings_path.parent / f"{settings_path.name}.{timestamp}.backup"
|
|
454
|
+
try:
|
|
455
|
+
copy2(settings_path, backup_path)
|
|
456
|
+
result["backup_path"] = str(backup_path)
|
|
457
|
+
except OSError as e:
|
|
458
|
+
result["error"] = f"Failed to create backup: {e}"
|
|
459
|
+
return result
|
|
460
|
+
|
|
461
|
+
# Remove the server
|
|
462
|
+
del settings["mcpServers"][server_name]
|
|
463
|
+
|
|
464
|
+
# Clean up empty mcpServers section
|
|
465
|
+
if not settings["mcpServers"]:
|
|
466
|
+
del settings["mcpServers"]
|
|
467
|
+
|
|
468
|
+
# Write updated settings
|
|
469
|
+
try:
|
|
470
|
+
with open(settings_path, "w") as f:
|
|
471
|
+
json.dump(settings, f, indent=2)
|
|
472
|
+
except OSError as e:
|
|
473
|
+
result["error"] = f"Failed to write {settings_path}: {e}"
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
result["success"] = True
|
|
477
|
+
result["removed"] = True
|
|
478
|
+
return result
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def configure_mcp_server_toml(config_path: Path, server_name: str = "gobby") -> dict[str, Any]:
|
|
482
|
+
"""Add Gobby MCP server to a TOML config file (Codex).
|
|
483
|
+
|
|
484
|
+
Adds [mcp_servers.gobby] section with command and args.
|
|
485
|
+
Creates a timestamped backup before modifying.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
config_path: Path to the config.toml file (e.g., ~/.codex/config.toml)
|
|
489
|
+
server_name: Name for the MCP server entry (default: "gobby")
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Dict with 'success', 'added', 'backup_path', and 'error' keys
|
|
493
|
+
"""
|
|
494
|
+
import re
|
|
495
|
+
|
|
496
|
+
result: dict[str, Any] = {
|
|
497
|
+
"success": False,
|
|
498
|
+
"added": False,
|
|
499
|
+
"already_configured": False,
|
|
500
|
+
"backup_path": None,
|
|
501
|
+
"error": None,
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
# Ensure parent directory exists
|
|
505
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
506
|
+
|
|
507
|
+
# Read existing config
|
|
508
|
+
existing = ""
|
|
509
|
+
if config_path.exists():
|
|
510
|
+
try:
|
|
511
|
+
existing = config_path.read_text(encoding="utf-8")
|
|
512
|
+
except OSError as e:
|
|
513
|
+
result["error"] = f"Failed to read {config_path}: {e}"
|
|
514
|
+
return result
|
|
515
|
+
|
|
516
|
+
# Check if already configured
|
|
517
|
+
pattern = re.compile(rf"^\s*\[mcp_servers\.{re.escape(server_name)}\]", re.MULTILINE)
|
|
518
|
+
if pattern.search(existing):
|
|
519
|
+
result["success"] = True
|
|
520
|
+
result["already_configured"] = True
|
|
521
|
+
return result
|
|
522
|
+
|
|
523
|
+
# Create backup if file exists
|
|
524
|
+
if config_path.exists():
|
|
525
|
+
timestamp = int(time.time())
|
|
526
|
+
backup_path = config_path.with_suffix(f".toml.{timestamp}.backup")
|
|
527
|
+
try:
|
|
528
|
+
backup_path.write_text(existing, encoding="utf-8")
|
|
529
|
+
result["backup_path"] = str(backup_path)
|
|
530
|
+
except OSError as e:
|
|
531
|
+
result["error"] = f"Failed to create backup: {e}"
|
|
532
|
+
return result
|
|
533
|
+
|
|
534
|
+
# Add MCP server config
|
|
535
|
+
# Use 'uv run gobby' since most users won't have gobby installed globally
|
|
536
|
+
mcp_config = f"""
|
|
537
|
+
[mcp_servers.{server_name}]
|
|
538
|
+
command = "uv"
|
|
539
|
+
args = ["run", "gobby", "mcp-server"]
|
|
540
|
+
"""
|
|
541
|
+
updated = (existing.rstrip() + "\n" if existing.strip() else "") + mcp_config
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
config_path.write_text(updated, encoding="utf-8")
|
|
545
|
+
except OSError as e:
|
|
546
|
+
result["error"] = f"Failed to write {config_path}: {e}"
|
|
547
|
+
return result
|
|
548
|
+
|
|
549
|
+
result["success"] = True
|
|
550
|
+
result["added"] = True
|
|
551
|
+
return result
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def remove_mcp_server_toml(config_path: Path, server_name: str = "gobby") -> dict[str, Any]:
|
|
555
|
+
"""Remove Gobby MCP server from a TOML config file.
|
|
556
|
+
|
|
557
|
+
Uses tomllib (stdlib) for reading and tomli_w for writing to properly
|
|
558
|
+
handle TOML syntax including multi-line strings.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
config_path: Path to the config.toml file
|
|
562
|
+
server_name: Name of the MCP server entry to remove
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Dict with 'success', 'removed', 'backup_path', and 'error' keys
|
|
566
|
+
"""
|
|
567
|
+
import tomllib
|
|
568
|
+
|
|
569
|
+
import tomli_w
|
|
570
|
+
|
|
571
|
+
result: dict[str, Any] = {
|
|
572
|
+
"success": False,
|
|
573
|
+
"removed": False,
|
|
574
|
+
"backup_path": None,
|
|
575
|
+
"error": None,
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if not config_path.exists():
|
|
579
|
+
result["success"] = True
|
|
580
|
+
return result
|
|
581
|
+
|
|
582
|
+
# Read existing TOML file
|
|
583
|
+
try:
|
|
584
|
+
existing_text = config_path.read_text(encoding="utf-8")
|
|
585
|
+
with open(config_path, "rb") as f:
|
|
586
|
+
config = tomllib.load(f)
|
|
587
|
+
except tomllib.TOMLDecodeError as e:
|
|
588
|
+
result["error"] = f"Failed to parse TOML {config_path}: {e}"
|
|
589
|
+
return result
|
|
590
|
+
except OSError as e:
|
|
591
|
+
result["error"] = f"Failed to read {config_path}: {e}"
|
|
592
|
+
return result
|
|
593
|
+
|
|
594
|
+
# Check if server exists in mcp_servers section
|
|
595
|
+
mcp_servers = config.get("mcp_servers", {})
|
|
596
|
+
if server_name not in mcp_servers:
|
|
597
|
+
result["success"] = True
|
|
598
|
+
return result
|
|
599
|
+
|
|
600
|
+
# Create backup
|
|
601
|
+
timestamp = int(time.time())
|
|
602
|
+
backup_path = config_path.with_suffix(f".toml.{timestamp}.backup")
|
|
603
|
+
try:
|
|
604
|
+
backup_path.write_text(existing_text, encoding="utf-8")
|
|
605
|
+
result["backup_path"] = str(backup_path)
|
|
606
|
+
except OSError as e:
|
|
607
|
+
result["error"] = f"Failed to create backup: {e}"
|
|
608
|
+
return result
|
|
609
|
+
|
|
610
|
+
# Remove the server from config
|
|
611
|
+
del mcp_servers[server_name]
|
|
612
|
+
|
|
613
|
+
# Clean up empty mcp_servers section
|
|
614
|
+
if not mcp_servers:
|
|
615
|
+
del config["mcp_servers"]
|
|
616
|
+
else:
|
|
617
|
+
config["mcp_servers"] = mcp_servers
|
|
618
|
+
|
|
619
|
+
# Write updated config using tomli_w
|
|
620
|
+
try:
|
|
621
|
+
with open(config_path, "wb") as f:
|
|
622
|
+
tomli_w.dump(config, f, multiline_strings=True)
|
|
623
|
+
except OSError as e:
|
|
624
|
+
result["error"] = f"Failed to write {config_path}: {e}"
|
|
625
|
+
return result
|
|
626
|
+
|
|
627
|
+
result["success"] = True
|
|
628
|
+
result["removed"] = True
|
|
629
|
+
return result
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
# Default external MCP servers to install
|
|
633
|
+
DEFAULT_MCP_SERVERS: list[dict[str, Any]] = [
|
|
634
|
+
{
|
|
635
|
+
"name": "github",
|
|
636
|
+
"transport": "stdio",
|
|
637
|
+
"command": "npx",
|
|
638
|
+
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
639
|
+
"env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"}, # nosec B105 - env var placeholder
|
|
640
|
+
"description": "GitHub API integration for issues, PRs, repos, and code search",
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
"name": "linear",
|
|
644
|
+
"transport": "stdio",
|
|
645
|
+
"command": "npx",
|
|
646
|
+
"args": ["-y", "mcp-linear"],
|
|
647
|
+
"env": {"LINEAR_API_KEY": "${LINEAR_API_KEY}"},
|
|
648
|
+
"description": "Linear issue tracking integration",
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
"name": "context7",
|
|
652
|
+
"transport": "stdio",
|
|
653
|
+
"command": "npx",
|
|
654
|
+
"args": ["-y", "@upstash/context7-mcp"],
|
|
655
|
+
# API key args added dynamically if CONTEXT7_API_KEY is set
|
|
656
|
+
"optional_env_args": {"CONTEXT7_API_KEY": ["--api-key", "${CONTEXT7_API_KEY}"]},
|
|
657
|
+
"description": "Context7 library documentation lookup (set CONTEXT7_API_KEY for private repos)",
|
|
658
|
+
},
|
|
659
|
+
]
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def install_default_mcp_servers() -> dict[str, Any]:
|
|
663
|
+
"""Install default external MCP servers to ~/.gobby/.mcp.json.
|
|
664
|
+
|
|
665
|
+
Adds GitHub, Linear, and context7 MCP servers if not already configured.
|
|
666
|
+
These servers pull API keys from environment variables.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Dict with 'success', 'servers_added', 'servers_skipped', and 'error' keys
|
|
670
|
+
"""
|
|
671
|
+
result: dict[str, Any] = {
|
|
672
|
+
"success": False,
|
|
673
|
+
"servers_added": [],
|
|
674
|
+
"servers_skipped": [],
|
|
675
|
+
"error": None,
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
mcp_config_path = Path("~/.gobby/.mcp.json").expanduser()
|
|
679
|
+
|
|
680
|
+
# Ensure parent directory exists
|
|
681
|
+
mcp_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
682
|
+
|
|
683
|
+
# Load existing config or create empty
|
|
684
|
+
existing_config: dict[str, Any] = {"servers": []}
|
|
685
|
+
if mcp_config_path.exists():
|
|
686
|
+
try:
|
|
687
|
+
with open(mcp_config_path) as f:
|
|
688
|
+
content = f.read()
|
|
689
|
+
if content.strip():
|
|
690
|
+
existing_config = json.loads(content)
|
|
691
|
+
if "servers" not in existing_config:
|
|
692
|
+
existing_config["servers"] = []
|
|
693
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
694
|
+
result["error"] = f"Failed to read MCP config: {e}"
|
|
695
|
+
return result
|
|
696
|
+
|
|
697
|
+
# Get existing server names
|
|
698
|
+
existing_names = {s.get("name") for s in existing_config["servers"]}
|
|
699
|
+
|
|
700
|
+
# Add default servers if not already present
|
|
701
|
+
for server in DEFAULT_MCP_SERVERS:
|
|
702
|
+
if server["name"] in existing_names:
|
|
703
|
+
result["servers_skipped"].append(server["name"])
|
|
704
|
+
else:
|
|
705
|
+
# Build args list, adding optional env-dependent args
|
|
706
|
+
args = list(server.get("args") or [])
|
|
707
|
+
optional_env_args = server.get("optional_env_args", {})
|
|
708
|
+
for env_var, extra_args in optional_env_args.items():
|
|
709
|
+
if os.environ.get(env_var):
|
|
710
|
+
args.extend(extra_args)
|
|
711
|
+
|
|
712
|
+
existing_config["servers"].append(
|
|
713
|
+
{
|
|
714
|
+
"name": server["name"],
|
|
715
|
+
"enabled": True,
|
|
716
|
+
"transport": server["transport"],
|
|
717
|
+
"command": server.get("command"),
|
|
718
|
+
"args": args if args else None,
|
|
719
|
+
"env": server.get("env"),
|
|
720
|
+
"description": server.get("description"),
|
|
721
|
+
}
|
|
722
|
+
)
|
|
723
|
+
result["servers_added"].append(server["name"])
|
|
724
|
+
|
|
725
|
+
# Write updated config if any servers were added
|
|
726
|
+
if result["servers_added"]:
|
|
727
|
+
try:
|
|
728
|
+
with open(mcp_config_path, "w") as f:
|
|
729
|
+
json.dump(existing_config, f, indent=2)
|
|
730
|
+
# Set restrictive permissions
|
|
731
|
+
mcp_config_path.chmod(0o600)
|
|
732
|
+
except OSError as e:
|
|
733
|
+
result["error"] = f"Failed to write MCP config: {e}"
|
|
734
|
+
return result
|
|
735
|
+
|
|
736
|
+
result["success"] = True
|
|
737
|
+
return result
|