gobby 0.2.5__py3-none-any.whl → 0.2.7__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 +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- 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 +12 -5
- 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/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- 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 +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- 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/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- 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/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -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 +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- 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 +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -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 +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- 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/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- 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/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- 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.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.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)
|
gobby/cli/install.py
CHANGED
|
@@ -250,7 +250,7 @@ def install(
|
|
|
250
250
|
click.echo(f" - {cmd}")
|
|
251
251
|
if result.get("plugins_installed"):
|
|
252
252
|
click.echo(
|
|
253
|
-
f"Installed {len(result['plugins_installed'])} plugins to
|
|
253
|
+
f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
|
|
254
254
|
)
|
|
255
255
|
for plugin in result["plugins_installed"]:
|
|
256
256
|
click.echo(f" - {plugin}")
|
|
@@ -287,7 +287,7 @@ def install(
|
|
|
287
287
|
click.echo(f" - {cmd}")
|
|
288
288
|
if result.get("plugins_installed"):
|
|
289
289
|
click.echo(
|
|
290
|
-
f"Installed {len(result['plugins_installed'])} plugins to
|
|
290
|
+
f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
|
|
291
291
|
)
|
|
292
292
|
for plugin in result["plugins_installed"]:
|
|
293
293
|
click.echo(f" - {plugin}")
|
|
@@ -334,7 +334,7 @@ def install(
|
|
|
334
334
|
click.echo(f" - {cmd}")
|
|
335
335
|
if result.get("plugins_installed"):
|
|
336
336
|
click.echo(
|
|
337
|
-
f"Installed {len(result['plugins_installed'])} plugins to
|
|
337
|
+
f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
|
|
338
338
|
)
|
|
339
339
|
for plugin in result["plugins_installed"]:
|
|
340
340
|
click.echo(f" - {plugin}")
|
|
@@ -395,7 +395,7 @@ def install(
|
|
|
395
395
|
click.echo(f" - {cmd}")
|
|
396
396
|
if result.get("plugins_installed"):
|
|
397
397
|
click.echo(
|
|
398
|
-
f"Installed {len(result['plugins_installed'])} plugins to
|
|
398
|
+
f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
|
|
399
399
|
)
|
|
400
400
|
for plugin in result["plugins_installed"]:
|
|
401
401
|
click.echo(f" - {plugin}")
|
|
@@ -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,11 @@ 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,
|
|
23
|
+
install_router_skills_as_commands,
|
|
22
24
|
install_shared_content,
|
|
23
|
-
install_shared_skills,
|
|
24
25
|
remove_mcp_server_json,
|
|
25
26
|
)
|
|
26
27
|
|
|
@@ -55,6 +56,12 @@ def install_claude(project_path: Path) -> dict[str, Any]:
|
|
|
55
56
|
hooks_dir = claude_path / "hooks"
|
|
56
57
|
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
57
58
|
|
|
59
|
+
# Backup existing gobby skills (now auto-synced from database)
|
|
60
|
+
skills_dir = claude_path / "skills"
|
|
61
|
+
backup_result = backup_gobby_skills(skills_dir)
|
|
62
|
+
if backup_result["backed_up"] > 0:
|
|
63
|
+
logger.info(f"Backed up {backup_result['backed_up']} existing gobby skills")
|
|
64
|
+
|
|
58
65
|
# Get source files
|
|
59
66
|
install_dir = get_install_dir()
|
|
60
67
|
claude_install_dir = install_dir / "claude"
|
|
@@ -119,14 +126,13 @@ def install_claude(project_path: Path) -> dict[str, Any]:
|
|
|
119
126
|
result["commands_installed"] = cli.get("commands", [])
|
|
120
127
|
result["plugins_installed"] = shared.get("plugins", [])
|
|
121
128
|
|
|
122
|
-
# Install
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# Proceeding despite skill install failure
|
|
129
|
+
# Install router skills (gobby, g) as flattened commands
|
|
130
|
+
commands_dir = claude_path / "commands"
|
|
131
|
+
router_commands = install_router_skills_as_commands(commands_dir)
|
|
132
|
+
result["commands_installed"].extend(router_commands)
|
|
133
|
+
|
|
134
|
+
# Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
|
|
135
|
+
# No longer need to copy to .claude/skills/
|
|
130
136
|
|
|
131
137
|
# Backup existing settings.json if it exists
|
|
132
138
|
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
|
@@ -17,8 +17,8 @@ from gobby.cli.utils import get_install_dir
|
|
|
17
17
|
from .shared import (
|
|
18
18
|
configure_mcp_server_json,
|
|
19
19
|
install_cli_content,
|
|
20
|
+
install_router_skills_as_gemini_skills,
|
|
20
21
|
install_shared_content,
|
|
21
|
-
install_shared_skills,
|
|
22
22
|
remove_mcp_server_json,
|
|
23
23
|
)
|
|
24
24
|
|
|
@@ -81,18 +81,18 @@ def install_gemini(project_path: Path) -> dict[str, Any]:
|
|
|
81
81
|
# Install CLI-specific content (can override shared)
|
|
82
82
|
cli = install_cli_content("gemini", gemini_path)
|
|
83
83
|
|
|
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
|
|
84
|
+
# Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
|
|
85
|
+
# No longer need to copy to .gemini/skills/
|
|
91
86
|
|
|
92
87
|
result["workflows_installed"] = shared["workflows"] + cli["workflows"]
|
|
93
88
|
result["commands_installed"] = cli.get("commands", [])
|
|
94
89
|
result["plugins_installed"] = shared.get("plugins", [])
|
|
95
90
|
|
|
91
|
+
# Install router skills (gobby, g) as Gemini skills
|
|
92
|
+
skills_dir = gemini_path / "skills"
|
|
93
|
+
router_skills = install_router_skills_as_gemini_skills(skills_dir)
|
|
94
|
+
result["commands_installed"].extend(router_skills)
|
|
95
|
+
|
|
96
96
|
# Backup existing settings.json if it exists
|
|
97
97
|
if settings_file.exists():
|
|
98
98
|
timestamp = int(time.time())
|
gobby/cli/installers/shared.py
CHANGED
|
@@ -23,7 +23,8 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
|
|
|
23
23
|
"""Install shared content from src/install/shared/.
|
|
24
24
|
|
|
25
25
|
Workflows are cross-CLI and go to {project_path}/.gobby/workflows/.
|
|
26
|
-
Plugins are
|
|
26
|
+
Plugins are project-scoped and go to {project_path}/.gobby/plugins/.
|
|
27
|
+
Prompts are project-scoped and go to {project_path}/.gobby/prompts/.
|
|
27
28
|
Docs are project-local and go to {project_path}/.gobby/docs/.
|
|
28
29
|
|
|
29
30
|
Args:
|
|
@@ -34,28 +35,52 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
|
|
|
34
35
|
Dict with lists of installed items by type
|
|
35
36
|
"""
|
|
36
37
|
shared_dir = get_install_dir() / "shared"
|
|
37
|
-
installed: dict[str, list[str]] = {"workflows": [], "plugins": [], "docs": []}
|
|
38
|
+
installed: dict[str, list[str]] = {"workflows": [], "plugins": [], "prompts": [], "docs": []}
|
|
38
39
|
|
|
39
40
|
# Install shared workflows to .gobby/workflows/ (cross-CLI)
|
|
40
41
|
shared_workflows = shared_dir / "workflows"
|
|
41
42
|
if shared_workflows.exists():
|
|
42
43
|
target_workflows = project_path / ".gobby" / "workflows"
|
|
43
44
|
target_workflows.mkdir(parents=True, exist_ok=True)
|
|
44
|
-
for
|
|
45
|
-
if
|
|
46
|
-
copy2(
|
|
47
|
-
installed["workflows"].append(
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
for item in shared_workflows.iterdir():
|
|
46
|
+
if item.is_file():
|
|
47
|
+
copy2(item, target_workflows / item.name)
|
|
48
|
+
installed["workflows"].append(item.name)
|
|
49
|
+
elif item.is_dir():
|
|
50
|
+
# Copy subdirectories (e.g., lifecycle/)
|
|
51
|
+
target_subdir = target_workflows / item.name
|
|
52
|
+
if target_subdir.exists():
|
|
53
|
+
shutil.rmtree(target_subdir)
|
|
54
|
+
copytree(item, target_subdir)
|
|
55
|
+
installed["workflows"].append(f"{item.name}/")
|
|
56
|
+
|
|
57
|
+
# Install shared plugins to .gobby/plugins/ (project-scoped)
|
|
50
58
|
shared_plugins = shared_dir / "plugins"
|
|
51
59
|
if shared_plugins.exists():
|
|
52
|
-
target_plugins =
|
|
60
|
+
target_plugins = project_path / ".gobby" / "plugins"
|
|
53
61
|
target_plugins.mkdir(parents=True, exist_ok=True)
|
|
54
62
|
for plugin_file in shared_plugins.iterdir():
|
|
55
63
|
if plugin_file.is_file() and plugin_file.suffix == ".py":
|
|
56
64
|
copy2(plugin_file, target_plugins / plugin_file.name)
|
|
57
65
|
installed["plugins"].append(plugin_file.name)
|
|
58
66
|
|
|
67
|
+
# Install shared prompts to .gobby/prompts/ (project-scoped)
|
|
68
|
+
shared_prompts = shared_dir / "prompts"
|
|
69
|
+
if shared_prompts.exists():
|
|
70
|
+
target_prompts = project_path / ".gobby" / "prompts"
|
|
71
|
+
target_prompts.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
for item in shared_prompts.iterdir():
|
|
73
|
+
if item.is_file():
|
|
74
|
+
copy2(item, target_prompts / item.name)
|
|
75
|
+
installed["prompts"].append(item.name)
|
|
76
|
+
elif item.is_dir():
|
|
77
|
+
# Copy subdirectories (e.g., expansion/, validation/)
|
|
78
|
+
target_subdir = target_prompts / item.name
|
|
79
|
+
if target_subdir.exists():
|
|
80
|
+
shutil.rmtree(target_subdir)
|
|
81
|
+
copytree(item, target_subdir)
|
|
82
|
+
installed["prompts"].append(f"{item.name}/")
|
|
83
|
+
|
|
59
84
|
# Install shared docs to .gobby/docs/ (project-local)
|
|
60
85
|
shared_docs = shared_dir / "docs"
|
|
61
86
|
if shared_docs.exists():
|
|
@@ -69,6 +94,55 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
|
|
|
69
94
|
return installed
|
|
70
95
|
|
|
71
96
|
|
|
97
|
+
def backup_gobby_skills(skills_dir: Path) -> dict[str, Any]:
|
|
98
|
+
"""Move gobby-prefixed skill directories to a backup location.
|
|
99
|
+
|
|
100
|
+
This function is called during installation to preserve existing gobby skills
|
|
101
|
+
before they are replaced by database-synced skills. User custom skills
|
|
102
|
+
(non-gobby prefixed) are not touched.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
skills_dir: Path to skills directory (e.g., .claude/skills)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dict with:
|
|
109
|
+
- success: bool
|
|
110
|
+
- backed_up: int - number of skills moved to backup
|
|
111
|
+
- skipped: str (optional) - reason for skipping
|
|
112
|
+
"""
|
|
113
|
+
result: dict[str, Any] = {
|
|
114
|
+
"success": True,
|
|
115
|
+
"backed_up": 0,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if not skills_dir.exists():
|
|
119
|
+
result["skipped"] = "skills directory does not exist"
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
# Find gobby-prefixed skill directories
|
|
123
|
+
gobby_skills = [d for d in skills_dir.iterdir() if d.is_dir() and d.name.startswith("gobby-")]
|
|
124
|
+
|
|
125
|
+
if not gobby_skills:
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
# Create backup directory (sibling to skills/)
|
|
129
|
+
backup_dir = skills_dir.parent / "skills.backup"
|
|
130
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
|
|
132
|
+
# Move each gobby skill to backup
|
|
133
|
+
import shutil
|
|
134
|
+
|
|
135
|
+
for skill_dir in gobby_skills:
|
|
136
|
+
target = backup_dir / skill_dir.name
|
|
137
|
+
# If already exists in backup, remove it first (replace with newer)
|
|
138
|
+
if target.exists():
|
|
139
|
+
shutil.rmtree(target)
|
|
140
|
+
shutil.move(str(skill_dir), str(target))
|
|
141
|
+
result["backed_up"] += 1
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
72
146
|
def install_shared_skills(target_dir: Path) -> list[str]:
|
|
73
147
|
"""Install shared SKILL.md files to target directory.
|
|
74
148
|
|
|
@@ -129,6 +203,87 @@ def install_shared_skills(target_dir: Path) -> list[str]:
|
|
|
129
203
|
return installed
|
|
130
204
|
|
|
131
205
|
|
|
206
|
+
def install_router_skills_as_commands(target_commands_dir: Path) -> list[str]:
|
|
207
|
+
"""Install router skills (gobby, g) as flattened Claude commands.
|
|
208
|
+
|
|
209
|
+
Claude Code uses .claude/commands/name.md format for slash commands.
|
|
210
|
+
This function copies the gobby router skills from shared/skills/ to
|
|
211
|
+
commands/ as flattened .md files.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
target_commands_dir: Path to commands directory (e.g., .claude/commands)
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
List of installed command names
|
|
218
|
+
"""
|
|
219
|
+
shared_skills_dir = get_install_dir() / "shared" / "skills"
|
|
220
|
+
installed: list[str] = []
|
|
221
|
+
|
|
222
|
+
# Router skills to install as commands
|
|
223
|
+
router_skills = ["gobby", "g"]
|
|
224
|
+
|
|
225
|
+
target_commands_dir.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
|
|
227
|
+
for skill_name in router_skills:
|
|
228
|
+
source_skill_md = shared_skills_dir / skill_name / "SKILL.md"
|
|
229
|
+
if not source_skill_md.exists():
|
|
230
|
+
logger.warning(f"Router skill not found: {source_skill_md}")
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Flatten: copy SKILL.md to commands/name.md
|
|
234
|
+
target_cmd = target_commands_dir / f"{skill_name}.md"
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
copy2(source_skill_md, target_cmd)
|
|
238
|
+
installed.append(f"{skill_name}.md")
|
|
239
|
+
except OSError as e:
|
|
240
|
+
logger.error(f"Failed to copy router skill {skill_name}: {e}")
|
|
241
|
+
|
|
242
|
+
return installed
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def install_router_skills_as_gemini_skills(target_skills_dir: Path) -> list[str]:
|
|
246
|
+
"""Install router skills (gobby, g) as Gemini skills (directory structure).
|
|
247
|
+
|
|
248
|
+
Gemini CLI uses .gemini/skills/name/SKILL.md format for skills.
|
|
249
|
+
This function copies the gobby router skills from shared/skills/ to
|
|
250
|
+
the target skills directory preserving the directory structure.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
target_skills_dir: Path to skills directory (e.g., .gemini/skills)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
List of installed skill names
|
|
257
|
+
"""
|
|
258
|
+
shared_skills_dir = get_install_dir() / "shared" / "skills"
|
|
259
|
+
installed: list[str] = []
|
|
260
|
+
|
|
261
|
+
# Router skills to install
|
|
262
|
+
router_skills = ["gobby", "g"]
|
|
263
|
+
|
|
264
|
+
target_skills_dir.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
|
|
266
|
+
for skill_name in router_skills:
|
|
267
|
+
source_skill_dir = shared_skills_dir / skill_name
|
|
268
|
+
source_skill_md = source_skill_dir / "SKILL.md"
|
|
269
|
+
if not source_skill_md.exists():
|
|
270
|
+
logger.warning(f"Router skill not found: {source_skill_md}")
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# Create skill directory and copy SKILL.md
|
|
274
|
+
target_skill_dir = target_skills_dir / skill_name
|
|
275
|
+
target_skill_dir.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
target_skill_md = target_skill_dir / "SKILL.md"
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
copy2(source_skill_md, target_skill_md)
|
|
280
|
+
installed.append(f"{skill_name}/")
|
|
281
|
+
except OSError as e:
|
|
282
|
+
logger.error(f"Failed to copy router skill {skill_name}: {e}")
|
|
283
|
+
|
|
284
|
+
return installed
|
|
285
|
+
|
|
286
|
+
|
|
132
287
|
def install_cli_content(cli_name: str, target_path: Path) -> dict[str, list[str]]:
|
|
133
288
|
"""Install CLI-specific workflows/commands (layered on top of shared).
|
|
134
289
|
|
|
@@ -149,10 +304,17 @@ def install_cli_content(cli_name: str, target_path: Path) -> dict[str, list[str]
|
|
|
149
304
|
if cli_workflows.exists():
|
|
150
305
|
target_workflows = target_path / "workflows"
|
|
151
306
|
target_workflows.mkdir(parents=True, exist_ok=True)
|
|
152
|
-
for
|
|
153
|
-
if
|
|
154
|
-
copy2(
|
|
155
|
-
installed["workflows"].append(
|
|
307
|
+
for item in cli_workflows.iterdir():
|
|
308
|
+
if item.is_file():
|
|
309
|
+
copy2(item, target_workflows / item.name)
|
|
310
|
+
installed["workflows"].append(item.name)
|
|
311
|
+
elif item.is_dir():
|
|
312
|
+
# Copy subdirectories
|
|
313
|
+
target_subdir = target_workflows / item.name
|
|
314
|
+
if target_subdir.exists():
|
|
315
|
+
shutil.rmtree(target_subdir)
|
|
316
|
+
copytree(item, target_subdir)
|
|
317
|
+
installed["workflows"].append(f"{item.name}/")
|
|
156
318
|
|
|
157
319
|
# CLI-specific commands (slash commands)
|
|
158
320
|
# Claude/Gemini: commands/, Codex: prompts/
|
gobby/cli/sessions.py
CHANGED
|
@@ -387,7 +387,7 @@ def create_handoff(
|
|
|
387
387
|
import time
|
|
388
388
|
from pathlib import Path
|
|
389
389
|
|
|
390
|
-
from gobby.mcp_proxy.tools.
|
|
390
|
+
from gobby.mcp_proxy.tools.sessions._handoff import _format_handoff_markdown
|
|
391
391
|
from gobby.sessions.analyzer import TranscriptAnalyzer
|
|
392
392
|
|
|
393
393
|
manager = get_session_manager()
|