gobby 0.2.5__py3-none-any.whl → 0.2.6__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/adapters/claude_code.py +13 -4
- gobby/adapters/codex.py +43 -3
- gobby/agents/runner.py +8 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +9 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +2 -8
- gobby/cli/installers/shared.py +71 -8
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +3 -3
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/app.py +63 -1
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +6 -14
- gobby/hooks/event_handlers.py +145 -2
- gobby/hooks/hook_manager.py +48 -2
- gobby/hooks/skill_manager.py +130 -0
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +95 -33
- gobby/mcp_proxy/instructions.py +54 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -5
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/clones.py +903 -0
- gobby/mcp_proxy/tools/memory.py +1 -24
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/session_messages.py +1 -2
- gobby/mcp_proxy/tools/skills/__init__.py +631 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +60 -29
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +5 -0
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/manager.py +11 -2
- gobby/prompts/defaults/handoff/compact.md +63 -0
- gobby/prompts/defaults/handoff/session_end.md +57 -0
- gobby/prompts/defaults/memory/extract.md +61 -0
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +23 -8
- gobby/servers/routes/admin.py +280 -0
- gobby/servers/routes/mcp/tools.py +241 -52
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +64 -5
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +258 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +180 -6
- gobby/storage/sessions.py +73 -0
- gobby/storage/skills.py +749 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +41 -6
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +39 -4
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/validation.py +24 -15
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/workflows/actions.py +84 -2
- gobby/workflows/context_actions.py +43 -0
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/engine.py +13 -2
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/loader.py +19 -6
- gobby/workflows/memory_actions.py +74 -0
- gobby/workflows/summary_actions.py +17 -0
- gobby/workflows/task_enforcement_actions.py +448 -6
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/WHEEL +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/top_level.txt +0 -0
gobby/cli/conductor.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conductor management CLI commands.
|
|
3
|
+
|
|
4
|
+
Commands for managing the conductor loop:
|
|
5
|
+
- start: Start the conductor loop
|
|
6
|
+
- stop: Stop the conductor loop
|
|
7
|
+
- restart: Restart the conductor loop
|
|
8
|
+
- status: Show conductor status
|
|
9
|
+
- chat: Send a message to the conductor
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_daemon_url() -> str:
|
|
19
|
+
"""Get daemon URL from config."""
|
|
20
|
+
from gobby.config.app import load_config
|
|
21
|
+
|
|
22
|
+
config = load_config()
|
|
23
|
+
return f"http://localhost:{config.daemon_port}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group()
|
|
27
|
+
def conductor() -> None:
|
|
28
|
+
"""Manage the conductor orchestration loop."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@conductor.command("start")
|
|
33
|
+
@click.option("--interval", "-i", type=int, default=30, help="Check interval in seconds")
|
|
34
|
+
@click.option("--autonomous", "-a", is_flag=True, help="Enable autonomous agent spawning")
|
|
35
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
36
|
+
def start_conductor(interval: int, autonomous: bool, json_format: bool) -> None:
|
|
37
|
+
"""Start the conductor loop.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
|
|
41
|
+
gobby conductor start
|
|
42
|
+
|
|
43
|
+
gobby conductor start --interval 60
|
|
44
|
+
|
|
45
|
+
gobby conductor start --autonomous
|
|
46
|
+
"""
|
|
47
|
+
daemon_url = get_daemon_url()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
response = httpx.post(
|
|
51
|
+
f"{daemon_url}/conductor/start",
|
|
52
|
+
json={"interval": interval, "autonomous": autonomous},
|
|
53
|
+
timeout=10.0,
|
|
54
|
+
)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
result = response.json()
|
|
57
|
+
except httpx.ConnectError:
|
|
58
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
59
|
+
return
|
|
60
|
+
except httpx.HTTPStatusError as e:
|
|
61
|
+
click.echo(f"Error: HTTP {e.response.status_code}: {e.response.text}", err=True)
|
|
62
|
+
return
|
|
63
|
+
except ValueError as e:
|
|
64
|
+
click.echo(f"Error: Invalid JSON response: {e}", err=True)
|
|
65
|
+
return
|
|
66
|
+
except Exception as e:
|
|
67
|
+
click.echo(f"Error: {e}", err=True)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
if json_format:
|
|
71
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if result.get("success"):
|
|
75
|
+
click.echo("Conductor started")
|
|
76
|
+
click.echo(f" Interval: {interval}s")
|
|
77
|
+
if autonomous:
|
|
78
|
+
click.echo(" Autonomous mode: enabled")
|
|
79
|
+
else:
|
|
80
|
+
click.echo(f"Failed to start conductor: {result.get('error')}", err=True)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@conductor.command("stop")
|
|
84
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
85
|
+
def stop_conductor(json_format: bool) -> None:
|
|
86
|
+
"""Stop the conductor loop.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
|
|
90
|
+
gobby conductor stop
|
|
91
|
+
"""
|
|
92
|
+
daemon_url = get_daemon_url()
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
response = httpx.post(
|
|
96
|
+
f"{daemon_url}/conductor/stop",
|
|
97
|
+
json={},
|
|
98
|
+
timeout=10.0,
|
|
99
|
+
)
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
result = response.json()
|
|
102
|
+
except httpx.ConnectError:
|
|
103
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
104
|
+
return
|
|
105
|
+
except httpx.HTTPStatusError as e:
|
|
106
|
+
click.echo(f"Error: HTTP {e.response.status_code}: {e.response.text}", err=True)
|
|
107
|
+
return
|
|
108
|
+
except ValueError as e:
|
|
109
|
+
click.echo(f"Error: Invalid JSON response: {e}", err=True)
|
|
110
|
+
return
|
|
111
|
+
except Exception as e:
|
|
112
|
+
click.echo(f"Error: {e}", err=True)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if json_format:
|
|
116
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if result.get("success"):
|
|
120
|
+
click.echo("Conductor stopped")
|
|
121
|
+
else:
|
|
122
|
+
click.echo(f"Failed to stop conductor: {result.get('error')}", err=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@conductor.command("restart")
|
|
126
|
+
@click.option("--interval", "-i", type=int, default=30, help="Check interval in seconds")
|
|
127
|
+
@click.option("--autonomous", "-a", is_flag=True, help="Enable autonomous agent spawning")
|
|
128
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
129
|
+
def restart_conductor(interval: int, autonomous: bool, json_format: bool) -> None:
|
|
130
|
+
"""Restart the conductor loop.
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
|
|
134
|
+
gobby conductor restart
|
|
135
|
+
|
|
136
|
+
gobby conductor restart --interval 60
|
|
137
|
+
"""
|
|
138
|
+
daemon_url = get_daemon_url()
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
response = httpx.post(
|
|
142
|
+
f"{daemon_url}/conductor/restart",
|
|
143
|
+
json={"interval": interval, "autonomous": autonomous},
|
|
144
|
+
timeout=10.0,
|
|
145
|
+
)
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
result = response.json()
|
|
148
|
+
except httpx.ConnectError:
|
|
149
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
150
|
+
return
|
|
151
|
+
except httpx.HTTPStatusError as e:
|
|
152
|
+
click.echo(f"Error: HTTP {e.response.status_code}: {e.response.text}", err=True)
|
|
153
|
+
return
|
|
154
|
+
except ValueError as e:
|
|
155
|
+
click.echo(f"Error: Invalid JSON response: {e}", err=True)
|
|
156
|
+
return
|
|
157
|
+
except Exception as e:
|
|
158
|
+
click.echo(f"Error: {e}", err=True)
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
if json_format:
|
|
162
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
if result.get("success"):
|
|
166
|
+
click.echo("Conductor restarted")
|
|
167
|
+
else:
|
|
168
|
+
click.echo(f"Failed to restart conductor: {result.get('error')}", err=True)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@conductor.command("status")
|
|
172
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
173
|
+
def status_conductor(json_format: bool) -> None:
|
|
174
|
+
"""Show conductor status.
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
|
|
178
|
+
gobby conductor status
|
|
179
|
+
|
|
180
|
+
gobby conductor status --json
|
|
181
|
+
"""
|
|
182
|
+
daemon_url = get_daemon_url()
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
response = httpx.get(
|
|
186
|
+
f"{daemon_url}/conductor/status",
|
|
187
|
+
timeout=10.0,
|
|
188
|
+
)
|
|
189
|
+
response.raise_for_status()
|
|
190
|
+
result = response.json()
|
|
191
|
+
except httpx.ConnectError:
|
|
192
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
193
|
+
return
|
|
194
|
+
except httpx.HTTPStatusError as e:
|
|
195
|
+
click.echo(f"Error: HTTP {e.response.status_code}: {e.response.text}", err=True)
|
|
196
|
+
return
|
|
197
|
+
except ValueError as e:
|
|
198
|
+
click.echo(f"Error: Invalid JSON response: {e}", err=True)
|
|
199
|
+
return
|
|
200
|
+
except Exception as e:
|
|
201
|
+
click.echo(f"Error: {e}", err=True)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
if json_format:
|
|
205
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
running = result.get("running", False)
|
|
209
|
+
if running:
|
|
210
|
+
click.echo("Conductor: running")
|
|
211
|
+
click.echo(f" Interval: {result.get('interval', 'unknown')}s")
|
|
212
|
+
click.echo(f" Autonomous: {result.get('autonomous', False)}")
|
|
213
|
+
if result.get("last_tick"):
|
|
214
|
+
click.echo(f" Last tick: {result['last_tick']}")
|
|
215
|
+
else:
|
|
216
|
+
click.echo("Conductor: not running")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@conductor.command("chat")
|
|
220
|
+
@click.argument("message")
|
|
221
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
222
|
+
def chat_conductor(message: str, json_format: bool) -> None:
|
|
223
|
+
"""Send a message to the conductor.
|
|
224
|
+
|
|
225
|
+
The conductor can process commands like status checks, task queries,
|
|
226
|
+
or trigger manual actions.
|
|
227
|
+
|
|
228
|
+
Examples:
|
|
229
|
+
|
|
230
|
+
gobby conductor chat "Check all tasks"
|
|
231
|
+
|
|
232
|
+
gobby conductor chat "spawn agent for task-123"
|
|
233
|
+
|
|
234
|
+
gobby conductor chat --json "status check"
|
|
235
|
+
"""
|
|
236
|
+
daemon_url = get_daemon_url()
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
response = httpx.post(
|
|
240
|
+
f"{daemon_url}/conductor/chat",
|
|
241
|
+
json={"message": message},
|
|
242
|
+
timeout=30.0,
|
|
243
|
+
)
|
|
244
|
+
response.raise_for_status()
|
|
245
|
+
result = response.json()
|
|
246
|
+
except httpx.ConnectError:
|
|
247
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
248
|
+
return
|
|
249
|
+
except httpx.HTTPStatusError as e:
|
|
250
|
+
click.echo(f"Error: HTTP {e.response.status_code}: {e.response.text}", err=True)
|
|
251
|
+
return
|
|
252
|
+
except ValueError as e:
|
|
253
|
+
click.echo(f"Error: Invalid JSON response: {e}", err=True)
|
|
254
|
+
return
|
|
255
|
+
except Exception as e:
|
|
256
|
+
click.echo(f"Error: {e}", err=True)
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
if json_format:
|
|
260
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
if result.get("success"):
|
|
264
|
+
click.echo(result.get("response", "OK"))
|
|
265
|
+
else:
|
|
266
|
+
click.echo(f"Error: {result.get('error', 'Unknown error')}", err=True)
|
|
@@ -12,7 +12,7 @@ import logging
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
-
from .shared import configure_mcp_server_json
|
|
15
|
+
from .shared import configure_mcp_server_json
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
@@ -43,14 +43,8 @@ def install_antigravity(project_path: Path) -> dict[str, Any]:
|
|
|
43
43
|
# Configure MCP server in Antigravity's MCP config (~/.gemini/antigravity/mcp_config.json)
|
|
44
44
|
mcp_config = Path.home() / ".gemini" / "antigravity" / "mcp_config.json"
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
|
|
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
|
|
46
|
+
# Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
|
|
47
|
+
# No longer need to copy to .antigravity/skills/
|
|
54
48
|
|
|
55
49
|
mcp_result = configure_mcp_server_json(mcp_config)
|
|
56
50
|
|
gobby/cli/installers/claude.py
CHANGED
|
@@ -17,10 +17,10 @@ from typing import Any
|
|
|
17
17
|
from gobby.cli.utils import get_install_dir
|
|
18
18
|
|
|
19
19
|
from .shared import (
|
|
20
|
+
backup_gobby_skills,
|
|
20
21
|
configure_mcp_server_json,
|
|
21
22
|
install_cli_content,
|
|
22
23
|
install_shared_content,
|
|
23
|
-
install_shared_skills,
|
|
24
24
|
remove_mcp_server_json,
|
|
25
25
|
)
|
|
26
26
|
|
|
@@ -55,6 +55,12 @@ def install_claude(project_path: Path) -> dict[str, Any]:
|
|
|
55
55
|
hooks_dir = claude_path / "hooks"
|
|
56
56
|
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
57
57
|
|
|
58
|
+
# Backup existing gobby skills (now auto-synced from database)
|
|
59
|
+
skills_dir = claude_path / "skills"
|
|
60
|
+
backup_result = backup_gobby_skills(skills_dir)
|
|
61
|
+
if backup_result["backed_up"] > 0:
|
|
62
|
+
logger.info(f"Backed up {backup_result['backed_up']} existing gobby skills")
|
|
63
|
+
|
|
58
64
|
# Get source files
|
|
59
65
|
install_dir = get_install_dir()
|
|
60
66
|
claude_install_dir = install_dir / "claude"
|
|
@@ -119,14 +125,8 @@ def install_claude(project_path: Path) -> dict[str, Any]:
|
|
|
119
125
|
result["commands_installed"] = cli.get("commands", [])
|
|
120
126
|
result["plugins_installed"] = shared.get("plugins", [])
|
|
121
127
|
|
|
122
|
-
#
|
|
123
|
-
|
|
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
|
|
128
|
+
# Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
|
|
129
|
+
# No longer need to copy to .claude/skills/
|
|
130
130
|
|
|
131
131
|
# Backup existing settings.json if it exists
|
|
132
132
|
backup_file = None
|
gobby/cli/installers/codex.py
CHANGED
|
@@ -18,7 +18,6 @@ from .shared import (
|
|
|
18
18
|
configure_mcp_server_toml,
|
|
19
19
|
install_cli_content,
|
|
20
20
|
install_shared_content,
|
|
21
|
-
install_shared_skills,
|
|
22
21
|
remove_mcp_server_toml,
|
|
23
22
|
)
|
|
24
23
|
|
|
@@ -69,13 +68,8 @@ def install_codex_notify() -> dict[str, Any]:
|
|
|
69
68
|
# Install CLI-specific content (can override shared)
|
|
70
69
|
cli = install_cli_content("codex", codex_home)
|
|
71
70
|
|
|
72
|
-
#
|
|
73
|
-
|
|
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
|
|
71
|
+
# Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
|
|
72
|
+
# No longer need to copy to .codex/skills/
|
|
79
73
|
|
|
80
74
|
result["workflows_installed"] = shared["workflows"] + cli["workflows"]
|
|
81
75
|
result["commands_installed"] = cli.get("commands", [])
|
gobby/cli/installers/gemini.py
CHANGED
|
@@ -18,7 +18,6 @@ from .shared import (
|
|
|
18
18
|
configure_mcp_server_json,
|
|
19
19
|
install_cli_content,
|
|
20
20
|
install_shared_content,
|
|
21
|
-
install_shared_skills,
|
|
22
21
|
remove_mcp_server_json,
|
|
23
22
|
)
|
|
24
23
|
|
|
@@ -81,13 +80,8 @@ def install_gemini(project_path: Path) -> dict[str, Any]:
|
|
|
81
80
|
# Install CLI-specific content (can override shared)
|
|
82
81
|
cli = install_cli_content("gemini", gemini_path)
|
|
83
82
|
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
skills = install_shared_skills(gemini_path / "skills")
|
|
87
|
-
result["commands_installed"].extend([f"{s} (skill)" for s in skills])
|
|
88
|
-
except Exception as e:
|
|
89
|
-
logger.error(f"Failed to install shared skills: {e}")
|
|
90
|
-
# Proceeding despite skill install failure
|
|
83
|
+
# Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
|
|
84
|
+
# No longer need to copy to .gemini/skills/
|
|
91
85
|
|
|
92
86
|
result["workflows_installed"] = shared["workflows"] + cli["workflows"]
|
|
93
87
|
result["commands_installed"] = cli.get("commands", [])
|
gobby/cli/installers/shared.py
CHANGED
|
@@ -41,10 +41,17 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
|
|
|
41
41
|
if shared_workflows.exists():
|
|
42
42
|
target_workflows = project_path / ".gobby" / "workflows"
|
|
43
43
|
target_workflows.mkdir(parents=True, exist_ok=True)
|
|
44
|
-
for
|
|
45
|
-
if
|
|
46
|
-
copy2(
|
|
47
|
-
installed["workflows"].append(
|
|
44
|
+
for item in shared_workflows.iterdir():
|
|
45
|
+
if item.is_file():
|
|
46
|
+
copy2(item, target_workflows / item.name)
|
|
47
|
+
installed["workflows"].append(item.name)
|
|
48
|
+
elif item.is_dir():
|
|
49
|
+
# Copy subdirectories (e.g., lifecycle/)
|
|
50
|
+
target_subdir = target_workflows / item.name
|
|
51
|
+
if target_subdir.exists():
|
|
52
|
+
shutil.rmtree(target_subdir)
|
|
53
|
+
copytree(item, target_subdir)
|
|
54
|
+
installed["workflows"].append(f"{item.name}/")
|
|
48
55
|
|
|
49
56
|
# Install shared plugins to ~/.gobby/plugins/ (global)
|
|
50
57
|
shared_plugins = shared_dir / "plugins"
|
|
@@ -69,6 +76,55 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
|
|
|
69
76
|
return installed
|
|
70
77
|
|
|
71
78
|
|
|
79
|
+
def backup_gobby_skills(skills_dir: Path) -> dict[str, Any]:
|
|
80
|
+
"""Move gobby-prefixed skill directories to a backup location.
|
|
81
|
+
|
|
82
|
+
This function is called during installation to preserve existing gobby skills
|
|
83
|
+
before they are replaced by database-synced skills. User custom skills
|
|
84
|
+
(non-gobby prefixed) are not touched.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
skills_dir: Path to skills directory (e.g., .claude/skills)
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dict with:
|
|
91
|
+
- success: bool
|
|
92
|
+
- backed_up: int - number of skills moved to backup
|
|
93
|
+
- skipped: str (optional) - reason for skipping
|
|
94
|
+
"""
|
|
95
|
+
result: dict[str, Any] = {
|
|
96
|
+
"success": True,
|
|
97
|
+
"backed_up": 0,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if not skills_dir.exists():
|
|
101
|
+
result["skipped"] = "skills directory does not exist"
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
# Find gobby-prefixed skill directories
|
|
105
|
+
gobby_skills = [d for d in skills_dir.iterdir() if d.is_dir() and d.name.startswith("gobby-")]
|
|
106
|
+
|
|
107
|
+
if not gobby_skills:
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
# Create backup directory (sibling to skills/)
|
|
111
|
+
backup_dir = skills_dir.parent / "skills.backup"
|
|
112
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
|
|
114
|
+
# Move each gobby skill to backup
|
|
115
|
+
import shutil
|
|
116
|
+
|
|
117
|
+
for skill_dir in gobby_skills:
|
|
118
|
+
target = backup_dir / skill_dir.name
|
|
119
|
+
# If already exists in backup, remove it first (replace with newer)
|
|
120
|
+
if target.exists():
|
|
121
|
+
shutil.rmtree(target)
|
|
122
|
+
shutil.move(str(skill_dir), str(target))
|
|
123
|
+
result["backed_up"] += 1
|
|
124
|
+
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
|
|
72
128
|
def install_shared_skills(target_dir: Path) -> list[str]:
|
|
73
129
|
"""Install shared SKILL.md files to target directory.
|
|
74
130
|
|
|
@@ -149,10 +205,17 @@ def install_cli_content(cli_name: str, target_path: Path) -> dict[str, list[str]
|
|
|
149
205
|
if cli_workflows.exists():
|
|
150
206
|
target_workflows = target_path / "workflows"
|
|
151
207
|
target_workflows.mkdir(parents=True, exist_ok=True)
|
|
152
|
-
for
|
|
153
|
-
if
|
|
154
|
-
copy2(
|
|
155
|
-
installed["workflows"].append(
|
|
208
|
+
for item in cli_workflows.iterdir():
|
|
209
|
+
if item.is_file():
|
|
210
|
+
copy2(item, target_workflows / item.name)
|
|
211
|
+
installed["workflows"].append(item.name)
|
|
212
|
+
elif item.is_dir():
|
|
213
|
+
# Copy subdirectories
|
|
214
|
+
target_subdir = target_workflows / item.name
|
|
215
|
+
if target_subdir.exists():
|
|
216
|
+
shutil.rmtree(target_subdir)
|
|
217
|
+
copytree(item, target_subdir)
|
|
218
|
+
installed["workflows"].append(f"{item.name}/")
|
|
156
219
|
|
|
157
220
|
# CLI-specific commands (slash commands)
|
|
158
221
|
# Claude/Gemini: commands/, Codex: prompts/
|