gobby 0.2.6__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/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/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/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +45 -2
- gobby/hooks/hook_manager.py +2 -2
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +2 -0
- gobby/mcp_proxy/registries.py +1 -4
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- 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 +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +52 -18
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -343
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- 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/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -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 -1506
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/skills/parser.py +30 -2
- gobby/storage/migrations.py +159 -372
- gobby/storage/sessions.py +43 -7
- gobby/storage/skills.py +37 -4
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- 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/enforcement/task_policy.py +542 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +80 -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 +94 -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.6.dist-info → gobby-0.2.7.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/RECORD +111 -111
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- 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/mcp_proxy/tools/session_messages.py +0 -1055
- 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/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- 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/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from gobby.agents.constants import get_terminal_env_vars
|
|
12
|
+
from gobby.agents.sandbox import SandboxConfig, compute_sandbox_paths, get_sandbox_resolver
|
|
12
13
|
from gobby.agents.spawners.base import HeadlessResult
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
@@ -26,11 +27,11 @@ def _get_spawn_utils() -> tuple[
|
|
|
26
27
|
"""Lazy import to avoid circular dependencies."""
|
|
27
28
|
from gobby.agents.spawn import (
|
|
28
29
|
MAX_ENV_PROMPT_LENGTH,
|
|
29
|
-
_create_prompt_file,
|
|
30
30
|
build_cli_command,
|
|
31
|
+
create_prompt_file,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
|
-
return build_cli_command,
|
|
34
|
+
return build_cli_command, create_prompt_file, MAX_ENV_PROMPT_LENGTH
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
class HeadlessSpawner:
|
|
@@ -169,6 +170,7 @@ class HeadlessSpawner:
|
|
|
169
170
|
agent_depth: int = 1,
|
|
170
171
|
max_agent_depth: int = 3,
|
|
171
172
|
prompt: str | None = None,
|
|
173
|
+
sandbox_config: SandboxConfig | None = None,
|
|
172
174
|
) -> HeadlessResult:
|
|
173
175
|
"""
|
|
174
176
|
Spawn a CLI agent in headless mode.
|
|
@@ -184,12 +186,24 @@ class HeadlessSpawner:
|
|
|
184
186
|
agent_depth: Current nesting depth
|
|
185
187
|
max_agent_depth: Maximum allowed depth
|
|
186
188
|
prompt: Optional initial prompt
|
|
189
|
+
sandbox_config: Optional sandbox configuration
|
|
187
190
|
|
|
188
191
|
Returns:
|
|
189
192
|
HeadlessResult with process handle
|
|
190
193
|
"""
|
|
191
194
|
build_cli_command, _create_prompt_file, max_env_prompt_length = _get_spawn_utils()
|
|
192
195
|
|
|
196
|
+
# Resolve sandbox configuration if enabled
|
|
197
|
+
sandbox_args: list[str] | None = None
|
|
198
|
+
sandbox_env: dict[str, str] = {}
|
|
199
|
+
|
|
200
|
+
if sandbox_config and sandbox_config.enabled:
|
|
201
|
+
# Compute sandbox paths based on cwd (workspace)
|
|
202
|
+
resolved_paths = compute_sandbox_paths(sandbox_config, str(cwd))
|
|
203
|
+
# Get CLI-specific resolver and generate args/env
|
|
204
|
+
resolver = get_sandbox_resolver(cli)
|
|
205
|
+
sandbox_args, sandbox_env = resolver.resolve(sandbox_config, resolved_paths)
|
|
206
|
+
|
|
193
207
|
# Build command with prompt as CLI argument and auto-approve for autonomous work
|
|
194
208
|
command = build_cli_command(
|
|
195
209
|
cli,
|
|
@@ -198,6 +212,7 @@ class HeadlessSpawner:
|
|
|
198
212
|
auto_approve=True, # Subagents need to work autonomously
|
|
199
213
|
working_directory=str(cwd) if cli == "codex" else None,
|
|
200
214
|
mode="headless", # Non-interactive headless mode
|
|
215
|
+
sandbox_args=sandbox_args,
|
|
201
216
|
)
|
|
202
217
|
|
|
203
218
|
# Handle prompt for environment variables (backup for hooks/context)
|
|
@@ -223,4 +238,8 @@ class HeadlessSpawner:
|
|
|
223
238
|
prompt_file=prompt_file,
|
|
224
239
|
)
|
|
225
240
|
|
|
241
|
+
# Merge sandbox environment variables if present
|
|
242
|
+
if sandbox_env:
|
|
243
|
+
env.update(sandbox_env)
|
|
244
|
+
|
|
226
245
|
return self.spawn(command, cwd, env)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Prompt file management for agent spawning.
|
|
2
|
+
|
|
3
|
+
Handles creation, cleanup, and tracking of temporary prompt files
|
|
4
|
+
used to pass long prompts to spawned CLI agents.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import atexit
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import tempfile
|
|
14
|
+
import threading
|
|
15
|
+
import uuid
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Maximum prompt length to pass via environment variable
|
|
21
|
+
# Longer prompts will be written to a temp file
|
|
22
|
+
MAX_ENV_PROMPT_LENGTH = 4096
|
|
23
|
+
|
|
24
|
+
# Module-level set for tracking prompt files to clean up on exit
|
|
25
|
+
# This avoids registering a new atexit handler for each prompt file
|
|
26
|
+
_prompt_files_to_cleanup: set[Path] = set()
|
|
27
|
+
_atexit_registered = False
|
|
28
|
+
_atexit_lock = threading.Lock()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cleanup_all_prompt_files() -> None:
|
|
32
|
+
"""Clean up all tracked prompt files on process exit."""
|
|
33
|
+
with _atexit_lock:
|
|
34
|
+
files_to_cleanup = list(_prompt_files_to_cleanup)
|
|
35
|
+
_prompt_files_to_cleanup.clear()
|
|
36
|
+
for prompt_path in files_to_cleanup:
|
|
37
|
+
try:
|
|
38
|
+
if prompt_path.exists():
|
|
39
|
+
prompt_path.unlink()
|
|
40
|
+
except OSError:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_prompt_file(prompt: str, session_id: str) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Create a prompt file with secure permissions.
|
|
47
|
+
|
|
48
|
+
The file is created in the system temp directory with restrictive
|
|
49
|
+
permissions (owner read/write only) and tracked for cleanup on exit.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
prompt: The prompt content to write
|
|
53
|
+
session_id: Session ID for naming the file
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Path to the created temp file
|
|
57
|
+
"""
|
|
58
|
+
global _atexit_registered
|
|
59
|
+
|
|
60
|
+
# Create temp directory with restrictive permissions
|
|
61
|
+
temp_dir = Path(tempfile.gettempdir()) / "gobby-prompts"
|
|
62
|
+
temp_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
63
|
+
|
|
64
|
+
# Sanitize session_id to prevent path traversal attacks
|
|
65
|
+
# Strip path separators and limit to alphanumeric, hyphens, underscores
|
|
66
|
+
safe_session_id = re.sub(r"[^a-zA-Z0-9_-]", "", session_id)
|
|
67
|
+
if not safe_session_id or len(safe_session_id) > 128:
|
|
68
|
+
safe_session_id = str(uuid.uuid4())
|
|
69
|
+
|
|
70
|
+
# Create the prompt file path
|
|
71
|
+
prompt_path = temp_dir / f"prompt-{safe_session_id}.txt"
|
|
72
|
+
|
|
73
|
+
# Write with secure permissions atomically - create with mode 0o600 from the start
|
|
74
|
+
# This avoids the TOCTOU window between write_text and chmod
|
|
75
|
+
fd = os.open(str(prompt_path), os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600)
|
|
76
|
+
try:
|
|
77
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
78
|
+
f.write(prompt)
|
|
79
|
+
f.flush()
|
|
80
|
+
os.fsync(f.fileno())
|
|
81
|
+
except Exception:
|
|
82
|
+
# fd is closed by fdopen, but if fdopen fails we need to close it
|
|
83
|
+
try:
|
|
84
|
+
os.close(fd)
|
|
85
|
+
except OSError:
|
|
86
|
+
pass
|
|
87
|
+
raise
|
|
88
|
+
|
|
89
|
+
# Track for cleanup and register handler (thread-safe)
|
|
90
|
+
with _atexit_lock:
|
|
91
|
+
_prompt_files_to_cleanup.add(prompt_path)
|
|
92
|
+
if not _atexit_registered:
|
|
93
|
+
atexit.register(cleanup_all_prompt_files)
|
|
94
|
+
_atexit_registered = True
|
|
95
|
+
|
|
96
|
+
logger.debug(f"Created secure prompt file: {prompt_path}")
|
|
97
|
+
return str(prompt_path)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def read_prompt_from_env() -> str | None:
|
|
101
|
+
"""
|
|
102
|
+
Read initial prompt from environment variables.
|
|
103
|
+
|
|
104
|
+
Checks GOBBY_PROMPT_FILE first (for long prompts),
|
|
105
|
+
then falls back to GOBBY_PROMPT (for short prompts).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Prompt string or None if not set
|
|
109
|
+
"""
|
|
110
|
+
from gobby.agents.constants import GOBBY_PROMPT, GOBBY_PROMPT_FILE
|
|
111
|
+
|
|
112
|
+
# Check for prompt file first
|
|
113
|
+
prompt_file = os.environ.get(GOBBY_PROMPT_FILE)
|
|
114
|
+
if prompt_file:
|
|
115
|
+
try:
|
|
116
|
+
prompt_path = Path(prompt_file)
|
|
117
|
+
if prompt_path.exists():
|
|
118
|
+
return prompt_path.read_text(encoding="utf-8")
|
|
119
|
+
else:
|
|
120
|
+
logger.warning(f"Prompt file not found: {prompt_file}")
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.error(f"Error reading prompt file: {e}")
|
|
123
|
+
|
|
124
|
+
# Fall back to inline prompt
|
|
125
|
+
return os.environ.get(GOBBY_PROMPT)
|
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}")
|
gobby/cli/installers/claude.py
CHANGED
|
@@ -20,6 +20,7 @@ from .shared import (
|
|
|
20
20
|
backup_gobby_skills,
|
|
21
21
|
configure_mcp_server_json,
|
|
22
22
|
install_cli_content,
|
|
23
|
+
install_router_skills_as_commands,
|
|
23
24
|
install_shared_content,
|
|
24
25
|
remove_mcp_server_json,
|
|
25
26
|
)
|
|
@@ -125,6 +126,11 @@ def install_claude(project_path: Path) -> dict[str, Any]:
|
|
|
125
126
|
result["commands_installed"] = cli.get("commands", [])
|
|
126
127
|
result["plugins_installed"] = shared.get("plugins", [])
|
|
127
128
|
|
|
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
|
+
|
|
128
134
|
# Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
|
|
129
135
|
# No longer need to copy to .claude/skills/
|
|
130
136
|
|
gobby/cli/installers/gemini.py
CHANGED
|
@@ -17,6 +17,7 @@ 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
22
|
remove_mcp_server_json,
|
|
22
23
|
)
|
|
@@ -87,6 +88,11 @@ def install_gemini(project_path: Path) -> dict[str, Any]:
|
|
|
87
88
|
result["commands_installed"] = cli.get("commands", [])
|
|
88
89
|
result["plugins_installed"] = shared.get("plugins", [])
|
|
89
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
|
+
|
|
90
96
|
# Backup existing settings.json if it exists
|
|
91
97
|
if settings_file.exists():
|
|
92
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,7 +35,7 @@ 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"
|
|
@@ -53,16 +54,33 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
|
|
|
53
54
|
copytree(item, target_subdir)
|
|
54
55
|
installed["workflows"].append(f"{item.name}/")
|
|
55
56
|
|
|
56
|
-
# Install shared plugins to
|
|
57
|
+
# Install shared plugins to .gobby/plugins/ (project-scoped)
|
|
57
58
|
shared_plugins = shared_dir / "plugins"
|
|
58
59
|
if shared_plugins.exists():
|
|
59
|
-
target_plugins =
|
|
60
|
+
target_plugins = project_path / ".gobby" / "plugins"
|
|
60
61
|
target_plugins.mkdir(parents=True, exist_ok=True)
|
|
61
62
|
for plugin_file in shared_plugins.iterdir():
|
|
62
63
|
if plugin_file.is_file() and plugin_file.suffix == ".py":
|
|
63
64
|
copy2(plugin_file, target_plugins / plugin_file.name)
|
|
64
65
|
installed["plugins"].append(plugin_file.name)
|
|
65
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
|
+
|
|
66
84
|
# Install shared docs to .gobby/docs/ (project-local)
|
|
67
85
|
shared_docs = shared_dir / "docs"
|
|
68
86
|
if shared_docs.exists():
|
|
@@ -185,6 +203,87 @@ def install_shared_skills(target_dir: Path) -> list[str]:
|
|
|
185
203
|
return installed
|
|
186
204
|
|
|
187
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
|
+
|
|
188
287
|
def install_cli_content(cli_name: str, target_path: Path) -> dict[str, list[str]]:
|
|
189
288
|
"""Install CLI-specific workflows/commands (layered on top of shared).
|
|
190
289
|
|
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()
|
gobby/cli/utils.py
CHANGED
|
@@ -118,7 +118,7 @@ def get_active_session_id(db: LocalDatabase | None = None) -> str | None:
|
|
|
118
118
|
db.close()
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
def resolve_session_id(session_ref: str | None) -> str:
|
|
121
|
+
def resolve_session_id(session_ref: str | None, project_id: str | None = None) -> str:
|
|
122
122
|
"""
|
|
123
123
|
Resolve session reference to UUID.
|
|
124
124
|
|
|
@@ -126,6 +126,8 @@ def resolve_session_id(session_ref: str | None) -> str:
|
|
|
126
126
|
|
|
127
127
|
Args:
|
|
128
128
|
session_ref: User input string (UUID, #N, N, prefix) or None
|
|
129
|
+
project_id: Project ID for project-scoped #N lookup.
|
|
130
|
+
If not provided, auto-detected from current project context.
|
|
129
131
|
|
|
130
132
|
Returns:
|
|
131
133
|
Resolved UUID string
|
|
@@ -142,10 +144,15 @@ def resolve_session_id(session_ref: str | None) -> str:
|
|
|
142
144
|
raise click.ClickException("No active session found. Specify --session.")
|
|
143
145
|
return active_id
|
|
144
146
|
|
|
147
|
+
# Get project_id from context if not provided
|
|
148
|
+
if not project_id:
|
|
149
|
+
ctx = get_project_context()
|
|
150
|
+
project_id = ctx.get("id") if ctx else None
|
|
151
|
+
|
|
145
152
|
# Use SessionManager for resolution logic
|
|
146
153
|
manager = LocalSessionManager(db)
|
|
147
154
|
try:
|
|
148
|
-
return manager.resolve_session_reference(session_ref)
|
|
155
|
+
return manager.resolve_session_reference(session_ref, project_id)
|
|
149
156
|
except ValueError as e:
|
|
150
157
|
raise click.ClickException(str(e)) from None
|
|
151
158
|
finally:
|
gobby/config/__init__.py
CHANGED
|
@@ -14,116 +14,31 @@ Module structure:
|
|
|
14
14
|
- extensions.py: Hook extension configs (webhooks, plugins)
|
|
15
15
|
- sessions.py: Session lifecycle and tracking configs
|
|
16
16
|
- features.py: MCP proxy feature configs (code execution, tool recommendation)
|
|
17
|
+
|
|
18
|
+
Import from submodules directly for specific configs:
|
|
19
|
+
from gobby.config.tasks import TaskValidationConfig
|
|
20
|
+
from gobby.config.extensions import WebhooksConfig
|
|
21
|
+
|
|
22
|
+
Import from this package for app-level items:
|
|
23
|
+
from gobby.config import DaemonConfig, load_config
|
|
17
24
|
"""
|
|
18
25
|
|
|
19
26
|
# Core configuration and utilities from app.py
|
|
20
27
|
from gobby.config.app import (
|
|
21
28
|
DaemonConfig,
|
|
22
29
|
expand_env_vars,
|
|
30
|
+
generate_default_config,
|
|
23
31
|
load_config,
|
|
32
|
+
load_yaml,
|
|
24
33
|
save_config,
|
|
25
34
|
)
|
|
26
35
|
|
|
27
|
-
# Extension configs
|
|
28
|
-
from gobby.config.extensions import (
|
|
29
|
-
HookExtensionsConfig,
|
|
30
|
-
PluginItemConfig,
|
|
31
|
-
PluginsConfig,
|
|
32
|
-
WebhookEndpointConfig,
|
|
33
|
-
WebhooksConfig,
|
|
34
|
-
WebSocketBroadcastConfig,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
# Feature configs
|
|
38
|
-
from gobby.config.features import (
|
|
39
|
-
ImportMCPServerConfig,
|
|
40
|
-
MetricsConfig,
|
|
41
|
-
ProjectVerificationConfig,
|
|
42
|
-
RecommendToolsConfig,
|
|
43
|
-
ToolSummarizerConfig,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
# LLM provider configs
|
|
47
|
-
from gobby.config.llm_providers import (
|
|
48
|
-
LLMProviderConfig,
|
|
49
|
-
LLMProvidersConfig,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
# Logging configs
|
|
53
|
-
from gobby.config.logging import LoggingSettings
|
|
54
|
-
|
|
55
|
-
# Persistence configs
|
|
56
|
-
from gobby.config.persistence import (
|
|
57
|
-
MemoryConfig,
|
|
58
|
-
MemorySyncConfig,
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
# Server configs
|
|
62
|
-
from gobby.config.servers import (
|
|
63
|
-
MCPClientProxyConfig,
|
|
64
|
-
WebSocketSettings,
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
# Session configs
|
|
68
|
-
from gobby.config.sessions import (
|
|
69
|
-
ContextInjectionConfig,
|
|
70
|
-
MessageTrackingConfig,
|
|
71
|
-
SessionLifecycleConfig,
|
|
72
|
-
SessionSummaryConfig,
|
|
73
|
-
TitleSynthesisConfig,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
# Task configs
|
|
77
|
-
from gobby.config.tasks import (
|
|
78
|
-
CompactHandoffConfig,
|
|
79
|
-
GobbyTasksConfig,
|
|
80
|
-
PatternCriteriaConfig,
|
|
81
|
-
TaskExpansionConfig,
|
|
82
|
-
TaskValidationConfig,
|
|
83
|
-
WorkflowConfig,
|
|
84
|
-
)
|
|
85
|
-
|
|
86
36
|
__all__ = [
|
|
87
|
-
# Core
|
|
37
|
+
# Core app-level exports only
|
|
88
38
|
"DaemonConfig",
|
|
89
39
|
"expand_env_vars",
|
|
40
|
+
"generate_default_config",
|
|
90
41
|
"load_config",
|
|
42
|
+
"load_yaml",
|
|
91
43
|
"save_config",
|
|
92
|
-
# Extension configs
|
|
93
|
-
"HookExtensionsConfig",
|
|
94
|
-
"PluginItemConfig",
|
|
95
|
-
"PluginsConfig",
|
|
96
|
-
"WebhookEndpointConfig",
|
|
97
|
-
"WebhooksConfig",
|
|
98
|
-
"WebSocketBroadcastConfig",
|
|
99
|
-
# Feature configs
|
|
100
|
-
"ImportMCPServerConfig",
|
|
101
|
-
"MetricsConfig",
|
|
102
|
-
"ProjectVerificationConfig",
|
|
103
|
-
"RecommendToolsConfig",
|
|
104
|
-
"ToolSummarizerConfig",
|
|
105
|
-
# LLM provider configs
|
|
106
|
-
"LLMProviderConfig",
|
|
107
|
-
"LLMProvidersConfig",
|
|
108
|
-
# Logging configs
|
|
109
|
-
"LoggingSettings",
|
|
110
|
-
# Persistence configs
|
|
111
|
-
"MemoryConfig",
|
|
112
|
-
"MemorySyncConfig",
|
|
113
|
-
# Server configs
|
|
114
|
-
"MCPClientProxyConfig",
|
|
115
|
-
"WebSocketSettings",
|
|
116
|
-
# Session configs
|
|
117
|
-
"ContextInjectionConfig",
|
|
118
|
-
"MessageTrackingConfig",
|
|
119
|
-
"SessionLifecycleConfig",
|
|
120
|
-
"SessionSummaryConfig",
|
|
121
|
-
"TitleSynthesisConfig",
|
|
122
|
-
# Task configs
|
|
123
|
-
"CompactHandoffConfig",
|
|
124
|
-
"GobbyTasksConfig",
|
|
125
|
-
"PatternCriteriaConfig",
|
|
126
|
-
"TaskExpansionConfig",
|
|
127
|
-
"TaskValidationConfig",
|
|
128
|
-
"WorkflowConfig",
|
|
129
44
|
]
|