gobby 0.2.6__py3-none-any.whl → 0.2.8__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 +96 -35
- 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/adapters/gemini.py +140 -38
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +525 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +415 -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/macos.py +26 -1
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +0 -2
- 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/memory.py +185 -0
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/clones/git.py +177 -0
- 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/skills.py +31 -0
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +150 -8
- gobby/hooks/hook_manager.py +21 -3
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +4 -2
- gobby/mcp_proxy/registries.py +22 -8
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +76 -740
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- 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 +239 -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 +503 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
- gobby/mcp_proxy/tools/skills/__init__.py +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +455 -0
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -350
- gobby/memory/extractor.py +15 -1
- 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/runner.py +13 -0
- 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 +51 -4
- gobby/servers/routes/mcp/tools.py +48 -1506
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/manager.py +9 -0
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +30 -2
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +174 -368
- gobby/storage/sessions.py +45 -7
- gobby/storage/skills.py +80 -7
- 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/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +281 -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/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +91 -0
- gobby/workflows/safe_evaluator.py +191 -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 +217 -51
- 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.8.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- gobby/cli/tui.py +0 -34
- 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/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- gobby/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from typing import TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
from gobby.agents.constants import get_terminal_env_vars
|
|
11
|
+
from gobby.agents.sandbox import SandboxConfig, compute_sandbox_paths, get_sandbox_resolver
|
|
11
12
|
from gobby.agents.spawners.base import EmbeddedPTYResult
|
|
12
13
|
|
|
13
14
|
# pty is only available on Unix-like systems
|
|
@@ -37,11 +38,11 @@ def _get_spawn_utils() -> tuple[
|
|
|
37
38
|
MAX_ENV_PROMPT_LENGTH as _MAX_ENV_PROMPT_LENGTH,
|
|
38
39
|
)
|
|
39
40
|
from gobby.agents.spawn import (
|
|
40
|
-
_create_prompt_file,
|
|
41
41
|
build_cli_command,
|
|
42
|
+
create_prompt_file,
|
|
42
43
|
)
|
|
43
44
|
|
|
44
|
-
return build_cli_command,
|
|
45
|
+
return build_cli_command, create_prompt_file, _MAX_ENV_PROMPT_LENGTH
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
class EmbeddedSpawner:
|
|
@@ -169,6 +170,7 @@ class EmbeddedSpawner:
|
|
|
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
|
) -> EmbeddedPTYResult:
|
|
173
175
|
"""
|
|
174
176
|
Spawn a CLI agent with embedded PTY.
|
|
@@ -184,12 +186,24 @@ class EmbeddedSpawner:
|
|
|
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
|
EmbeddedPTYResult with PTY info
|
|
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,
|
|
@@ -197,6 +211,7 @@ class EmbeddedSpawner:
|
|
|
197
211
|
session_id=session_id,
|
|
198
212
|
auto_approve=True, # Subagents need to work autonomously
|
|
199
213
|
working_directory=str(cwd) if cli == "codex" else None,
|
|
214
|
+
sandbox_args=sandbox_args,
|
|
200
215
|
)
|
|
201
216
|
|
|
202
217
|
# Handle prompt for environment variables (backup for hooks/context)
|
|
@@ -222,4 +237,8 @@ class EmbeddedSpawner:
|
|
|
222
237
|
prompt_file=prompt_file,
|
|
223
238
|
)
|
|
224
239
|
|
|
240
|
+
# Merge sandbox environment variables if present
|
|
241
|
+
if sandbox_env:
|
|
242
|
+
env.update(sandbox_env)
|
|
243
|
+
|
|
225
244
|
return self.spawn(command, cwd, env)
|
|
@@ -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)
|
gobby/agents/spawners/macos.py
CHANGED
|
@@ -31,6 +31,24 @@ class GhosttySpawner(TerminalSpawnerBase):
|
|
|
31
31
|
def terminal_type(self) -> TerminalType:
|
|
32
32
|
return TerminalType.GHOSTTY
|
|
33
33
|
|
|
34
|
+
def _is_ghostty_running(self) -> bool:
|
|
35
|
+
"""Check if Ghostty is currently running on macOS."""
|
|
36
|
+
try:
|
|
37
|
+
result = subprocess.run( # nosec B603, B607 - osascript is safe
|
|
38
|
+
[
|
|
39
|
+
"/usr/bin/osascript",
|
|
40
|
+
"-e",
|
|
41
|
+
'tell application "System Events" to (name of processes) contains "Ghostty"',
|
|
42
|
+
],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=5,
|
|
46
|
+
)
|
|
47
|
+
return result.stdout.strip().lower() == "true"
|
|
48
|
+
except Exception:
|
|
49
|
+
# If we can't determine, assume running to use safer -n behavior
|
|
50
|
+
return True
|
|
51
|
+
|
|
34
52
|
def is_available(self) -> bool:
|
|
35
53
|
config = get_tty_config().get_terminal_config("ghostty")
|
|
36
54
|
if not config.enabled:
|
|
@@ -66,7 +84,14 @@ class GhosttySpawner(TerminalSpawnerBase):
|
|
|
66
84
|
ghostty_args.extend(tty_config.options)
|
|
67
85
|
ghostty_args.extend(["-e"] + command)
|
|
68
86
|
|
|
69
|
-
|
|
87
|
+
# Check if Ghostty is already running
|
|
88
|
+
# If running: use -n to open a new window
|
|
89
|
+
# If not running: omit -n to avoid double window on first launch
|
|
90
|
+
ghostty_running = self._is_ghostty_running()
|
|
91
|
+
if ghostty_running:
|
|
92
|
+
args = ["open", "-na", app_path, "--args"] + ghostty_args
|
|
93
|
+
else:
|
|
94
|
+
args = ["open", "-a", app_path, "--args"] + ghostty_args
|
|
70
95
|
else:
|
|
71
96
|
# On Linux/other platforms, use ghostty CLI directly
|
|
72
97
|
cli_command = tty_config.command or "ghostty"
|
|
@@ -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/__init__.py
CHANGED
|
@@ -24,7 +24,6 @@ from .projects import projects
|
|
|
24
24
|
from .sessions import sessions
|
|
25
25
|
from .skills import skills
|
|
26
26
|
from .tasks import tasks
|
|
27
|
-
from .tui import ui
|
|
28
27
|
from .workflows import workflows
|
|
29
28
|
from .worktrees import worktrees
|
|
30
29
|
|
|
@@ -70,4 +69,3 @@ cli.add_command(conductor)
|
|
|
70
69
|
cli.add_command(hooks)
|
|
71
70
|
cli.add_command(plugins)
|
|
72
71
|
cli.add_command(webhooks)
|
|
73
|
-
cli.add_command(ui)
|
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
|
|