gobby 0.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/cli/utils.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utilities for CLI commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import psutil
|
|
13
|
+
|
|
14
|
+
from gobby.config.app import load_config
|
|
15
|
+
from gobby.storage.database import LocalDatabase
|
|
16
|
+
from gobby.storage.projects import LocalProjectManager
|
|
17
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
18
|
+
from gobby.utils.project_context import get_project_context
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_gobby_home() -> Path:
|
|
24
|
+
"""Get gobby home directory, respecting GOBBY_HOME env var.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Path to gobby home (~/.gobby by default, or GOBBY_HOME if set)
|
|
28
|
+
"""
|
|
29
|
+
gobby_home = os.environ.get("GOBBY_HOME")
|
|
30
|
+
if gobby_home:
|
|
31
|
+
return Path(gobby_home)
|
|
32
|
+
return Path.home() / ".gobby"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_resources_dir(project_path: str | None = None) -> Path:
|
|
36
|
+
"""Get the resources directory for storing media files.
|
|
37
|
+
|
|
38
|
+
If a project path is provided, returns the project-local resources directory
|
|
39
|
+
(.gobby/resources/ within the project). Otherwise, returns the global
|
|
40
|
+
resources directory (~/.gobby/resources/).
|
|
41
|
+
|
|
42
|
+
The directory is created if it doesn't exist.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
project_path: Optional project root path for project-local resources
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Path to the resources directory
|
|
49
|
+
"""
|
|
50
|
+
if project_path:
|
|
51
|
+
resources_dir = Path(project_path) / ".gobby" / "resources"
|
|
52
|
+
else:
|
|
53
|
+
resources_dir = get_gobby_home() / "resources"
|
|
54
|
+
|
|
55
|
+
# Ensure directory exists
|
|
56
|
+
resources_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
return resources_dir
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_project_ref(project_ref: str | None, exit_on_not_found: bool = True) -> str | None:
|
|
61
|
+
"""Resolve a project reference (name or UUID) to project ID.
|
|
62
|
+
|
|
63
|
+
Accepts:
|
|
64
|
+
- Project name (e.g., "gobby")
|
|
65
|
+
- Project UUID
|
|
66
|
+
- None (returns current project from context)
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
project_ref: Project name, UUID, or None
|
|
70
|
+
exit_on_not_found: If True (default), exit the CLI when an explicit
|
|
71
|
+
project_ref is provided but not found
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Project ID string, or None if not found/no context
|
|
75
|
+
"""
|
|
76
|
+
if not project_ref:
|
|
77
|
+
# Use current project context
|
|
78
|
+
ctx = get_project_context()
|
|
79
|
+
return ctx.get("id") if ctx else None
|
|
80
|
+
|
|
81
|
+
db = LocalDatabase()
|
|
82
|
+
try:
|
|
83
|
+
manager = LocalProjectManager(db)
|
|
84
|
+
|
|
85
|
+
# Try as direct UUID first
|
|
86
|
+
project = manager.get(project_ref)
|
|
87
|
+
if project:
|
|
88
|
+
return project.id
|
|
89
|
+
|
|
90
|
+
# Try as project name
|
|
91
|
+
project = manager.get_by_name(project_ref)
|
|
92
|
+
if project:
|
|
93
|
+
return project.id
|
|
94
|
+
finally:
|
|
95
|
+
db.close()
|
|
96
|
+
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_active_session_id(db: LocalDatabase | None = None) -> str | None:
|
|
101
|
+
"""Get the most recent active session ID."""
|
|
102
|
+
close_db = False
|
|
103
|
+
if db is None:
|
|
104
|
+
db = LocalDatabase()
|
|
105
|
+
close_db = True
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
# SELECT id FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 1
|
|
109
|
+
# Using format compatible with the rest of the codebase (raw SQL) to avoid circular imports
|
|
110
|
+
# if using session manager directly which might pull in other things.
|
|
111
|
+
# But we import LocalSessionManager at top, so let's use it if possible or raw SQL for speed.
|
|
112
|
+
row = db.fetchone(
|
|
113
|
+
"SELECT id FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 1"
|
|
114
|
+
)
|
|
115
|
+
return row["id"] if row else None
|
|
116
|
+
finally:
|
|
117
|
+
if close_db:
|
|
118
|
+
db.close()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def resolve_session_id(session_ref: str | None) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Resolve session reference to UUID.
|
|
124
|
+
|
|
125
|
+
Centralized logic used by all CLI commands.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
session_ref: User input string (UUID, #N, N, prefix) or None
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Resolved UUID string
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
click.ClickException: If session not found or ambiguous
|
|
135
|
+
"""
|
|
136
|
+
db = LocalDatabase()
|
|
137
|
+
try:
|
|
138
|
+
# If no reference provided, try to find active session
|
|
139
|
+
if not session_ref:
|
|
140
|
+
active_id = get_active_session_id(db)
|
|
141
|
+
if not active_id:
|
|
142
|
+
raise click.ClickException("No active session found. Specify --session.")
|
|
143
|
+
return active_id
|
|
144
|
+
|
|
145
|
+
# Use SessionManager for resolution logic
|
|
146
|
+
manager = LocalSessionManager(db)
|
|
147
|
+
try:
|
|
148
|
+
return manager.resolve_session_reference(session_ref)
|
|
149
|
+
except ValueError as e:
|
|
150
|
+
raise click.ClickException(str(e)) from None
|
|
151
|
+
finally:
|
|
152
|
+
db.close()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def list_project_names() -> list[str]:
|
|
156
|
+
"""List all project names for shell completion."""
|
|
157
|
+
db = LocalDatabase()
|
|
158
|
+
try:
|
|
159
|
+
manager = LocalProjectManager(db)
|
|
160
|
+
return [p.name for p in manager.list()]
|
|
161
|
+
finally:
|
|
162
|
+
db.close()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def setup_logging(verbose: bool = False) -> None:
|
|
166
|
+
"""
|
|
167
|
+
Configure logging for CLI.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
verbose: If True, enable DEBUG level logging
|
|
171
|
+
"""
|
|
172
|
+
log_level = logging.DEBUG if verbose else logging.INFO
|
|
173
|
+
logging.basicConfig(
|
|
174
|
+
level=log_level,
|
|
175
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
176
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Silence noisy third-party loggers
|
|
180
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
181
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def format_uptime(seconds: float) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Format uptime in human-readable format.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
seconds: Uptime in seconds
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Formatted string like "1h 23m 45s"
|
|
193
|
+
"""
|
|
194
|
+
hours = int(seconds // 3600)
|
|
195
|
+
minutes = int((seconds % 3600) // 60)
|
|
196
|
+
secs = int(seconds % 60)
|
|
197
|
+
|
|
198
|
+
parts = []
|
|
199
|
+
if hours > 0:
|
|
200
|
+
parts.append(f"{hours}h")
|
|
201
|
+
if minutes > 0:
|
|
202
|
+
parts.append(f"{minutes}m")
|
|
203
|
+
if secs > 0 or not parts:
|
|
204
|
+
parts.append(f"{secs}s")
|
|
205
|
+
|
|
206
|
+
return " ".join(parts)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def is_port_available(port: int, host: str = "localhost") -> bool:
|
|
210
|
+
"""
|
|
211
|
+
Check if a port is available for binding.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
port: Port number to check
|
|
215
|
+
host: Host address to bind to
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
True if port is available, False otherwise
|
|
219
|
+
"""
|
|
220
|
+
import socket
|
|
221
|
+
|
|
222
|
+
# Try to bind to the port
|
|
223
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
224
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
sock.bind((host, port))
|
|
228
|
+
sock.close()
|
|
229
|
+
return True
|
|
230
|
+
except OSError:
|
|
231
|
+
sock.close()
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def wait_for_port_available(port: int, host: str = "localhost", timeout: float = 5.0) -> bool:
|
|
236
|
+
"""
|
|
237
|
+
Wait for a port to become available.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
port: Port number to check
|
|
241
|
+
host: Host address to bind to
|
|
242
|
+
timeout: Maximum time to wait in seconds
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
True if port became available, False if timeout
|
|
246
|
+
"""
|
|
247
|
+
start_time = time.time()
|
|
248
|
+
|
|
249
|
+
while (time.time() - start_time) < timeout:
|
|
250
|
+
if is_port_available(port, host):
|
|
251
|
+
return True
|
|
252
|
+
time.sleep(0.1)
|
|
253
|
+
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def kill_all_gobby_daemons() -> int:
|
|
258
|
+
"""
|
|
259
|
+
Find and kill all gobby DAEMON processes (not CLI commands).
|
|
260
|
+
|
|
261
|
+
Only kills processes that are actually running daemon servers,
|
|
262
|
+
not CLI invocations or other tools.
|
|
263
|
+
|
|
264
|
+
Detection methods:
|
|
265
|
+
1. Matches gobby.runner (the main daemon process)
|
|
266
|
+
2. Matches processes listening on daemon ports (8765/8766)
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Number of processes killed
|
|
270
|
+
"""
|
|
271
|
+
# Load config to get the configured ports
|
|
272
|
+
try:
|
|
273
|
+
config = load_config(create_default=False)
|
|
274
|
+
http_port = config.daemon_port
|
|
275
|
+
ws_port = config.websocket.port
|
|
276
|
+
except Exception:
|
|
277
|
+
# Fallback to defaults if config can't be loaded
|
|
278
|
+
http_port = 8765
|
|
279
|
+
ws_port = 8766
|
|
280
|
+
|
|
281
|
+
killed_count = 0
|
|
282
|
+
current_pid = os.getpid()
|
|
283
|
+
parent_pid = os.getppid()
|
|
284
|
+
|
|
285
|
+
# Get our parent process tree to avoid killing it
|
|
286
|
+
parent_pids = {current_pid, parent_pid}
|
|
287
|
+
try:
|
|
288
|
+
parent_proc = psutil.Process(parent_pid)
|
|
289
|
+
while parent_proc.parent() is not None:
|
|
290
|
+
parent_proc = parent_proc.parent()
|
|
291
|
+
parent_pids.add(parent_proc.pid)
|
|
292
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
# Find all gobby daemon processes
|
|
296
|
+
for proc in psutil.process_iter(["pid", "name", "cmdline"]):
|
|
297
|
+
try:
|
|
298
|
+
# Skip our own process and parent tree
|
|
299
|
+
if proc.pid in parent_pids:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
# Check if this is a gobby daemon process
|
|
303
|
+
cmdline = proc.cmdline()
|
|
304
|
+
cmdline_str = " ".join(cmdline)
|
|
305
|
+
|
|
306
|
+
# Match gobby.runner which is the actual daemon process
|
|
307
|
+
# Started via: python -m gobby.runner
|
|
308
|
+
is_gobby_daemon = (
|
|
309
|
+
"python" in cmdline_str.lower()
|
|
310
|
+
and (
|
|
311
|
+
# Match gobby.runner (new package)
|
|
312
|
+
"gobby.runner" in cmdline_str
|
|
313
|
+
# Also match legacy gobby_client.runner if it exists
|
|
314
|
+
or "gobby_client.runner" in cmdline_str
|
|
315
|
+
)
|
|
316
|
+
# Exclude CLI invocations
|
|
317
|
+
and "gobby.cli" not in cmdline_str
|
|
318
|
+
and "gobby_client.cli" not in cmdline_str
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Also check for processes that might be old daemon instances
|
|
322
|
+
# by checking if they're listening on our ports
|
|
323
|
+
if not is_gobby_daemon:
|
|
324
|
+
try:
|
|
325
|
+
# Check if process has connections on daemon ports
|
|
326
|
+
connections = proc.connections()
|
|
327
|
+
for conn in connections:
|
|
328
|
+
if hasattr(conn, "laddr") and conn.laddr:
|
|
329
|
+
if conn.laddr.port in [http_port, ws_port]:
|
|
330
|
+
# Only consider it a daemon if it's a Python process
|
|
331
|
+
# to avoid killing unrelated services
|
|
332
|
+
if "python" in cmdline_str.lower():
|
|
333
|
+
is_gobby_daemon = True
|
|
334
|
+
break
|
|
335
|
+
except (psutil.AccessDenied, psutil.NoSuchProcess):
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
if is_gobby_daemon:
|
|
339
|
+
click.echo(f"Found gobby daemon (PID {proc.pid}): {cmdline_str[:100]}")
|
|
340
|
+
|
|
341
|
+
# Try graceful shutdown first (SIGTERM)
|
|
342
|
+
try:
|
|
343
|
+
proc.send_signal(signal.SIGTERM)
|
|
344
|
+
# Wait up to 5 seconds for graceful shutdown
|
|
345
|
+
proc.wait(timeout=5)
|
|
346
|
+
click.echo(f"Gracefully stopped PID {proc.pid}")
|
|
347
|
+
killed_count += 1
|
|
348
|
+
except psutil.TimeoutExpired:
|
|
349
|
+
# Force kill if graceful shutdown fails
|
|
350
|
+
click.echo(f"Process {proc.pid} didn't stop gracefully, force killing...")
|
|
351
|
+
proc.kill()
|
|
352
|
+
proc.wait(timeout=2)
|
|
353
|
+
click.echo(f"Force killed PID {proc.pid}")
|
|
354
|
+
killed_count += 1
|
|
355
|
+
|
|
356
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
357
|
+
# Process already gone or we can't access it
|
|
358
|
+
pass
|
|
359
|
+
except Exception as e:
|
|
360
|
+
click.echo(f"Warning: Error checking process {proc.pid}: {e}", err=True)
|
|
361
|
+
|
|
362
|
+
return killed_count
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def init_local_storage() -> None:
|
|
366
|
+
"""Initialize hub SQLite storage and run migrations."""
|
|
367
|
+
from gobby.storage.database import LocalDatabase
|
|
368
|
+
from gobby.storage.migrations import run_migrations
|
|
369
|
+
|
|
370
|
+
config = load_config(create_default=False)
|
|
371
|
+
hub_db_path = Path(config.database_path).expanduser()
|
|
372
|
+
|
|
373
|
+
# Ensure hub db directory exists
|
|
374
|
+
hub_db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
375
|
+
|
|
376
|
+
hub_db = LocalDatabase(hub_db_path)
|
|
377
|
+
run_migrations(hub_db)
|
|
378
|
+
logger.debug(f"Database: {hub_db_path}")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def get_install_dir() -> Path:
|
|
382
|
+
"""Get the gobby install directory.
|
|
383
|
+
|
|
384
|
+
Checks for source directory (development mode) first,
|
|
385
|
+
falls back to package directory.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Path to the install directory
|
|
389
|
+
"""
|
|
390
|
+
import gobby
|
|
391
|
+
|
|
392
|
+
package_install_dir = Path(gobby.__file__).parent / "install"
|
|
393
|
+
|
|
394
|
+
# Try to find source directory (project root)
|
|
395
|
+
current = Path(gobby.__file__).resolve()
|
|
396
|
+
source_install_dir = None
|
|
397
|
+
|
|
398
|
+
for parent in current.parents:
|
|
399
|
+
potential_source = parent / "src" / "gobby" / "install"
|
|
400
|
+
if potential_source.exists():
|
|
401
|
+
source_install_dir = potential_source
|
|
402
|
+
break
|
|
403
|
+
|
|
404
|
+
if source_install_dir and source_install_dir.exists():
|
|
405
|
+
return source_install_dir
|
|
406
|
+
return package_install_dir
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _is_process_alive(pid: int) -> bool:
|
|
410
|
+
"""Check if a process is truly alive (not zombie, not dead).
|
|
411
|
+
|
|
412
|
+
Uses psutil to check process status, which handles zombies correctly.
|
|
413
|
+
os.kill(pid, 0) succeeds on zombie processes, but they're effectively dead.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
pid: Process ID to check
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
True only if process exists and is not a zombie
|
|
420
|
+
"""
|
|
421
|
+
try:
|
|
422
|
+
proc = psutil.Process(pid)
|
|
423
|
+
return bool(proc.status() != psutil.STATUS_ZOMBIE)
|
|
424
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
425
|
+
return False
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def stop_daemon(quiet: bool = False) -> bool:
|
|
429
|
+
"""Stop the daemon process. Returns True on success, False on failure.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
quiet: If True, suppress output messages
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
True if daemon was stopped successfully or wasn't running, False on error
|
|
436
|
+
"""
|
|
437
|
+
pid_file = get_gobby_home() / "gobby.pid"
|
|
438
|
+
|
|
439
|
+
# Read PID from file
|
|
440
|
+
if not pid_file.exists():
|
|
441
|
+
if not quiet:
|
|
442
|
+
click.echo("Gobby daemon is not running (no PID file found)")
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
with open(pid_file) as f:
|
|
447
|
+
pid = int(f.read().strip())
|
|
448
|
+
except Exception as e:
|
|
449
|
+
if not quiet:
|
|
450
|
+
click.echo(f"Error reading PID file: {e}", err=True)
|
|
451
|
+
pid_file.unlink(missing_ok=True)
|
|
452
|
+
return False
|
|
453
|
+
|
|
454
|
+
# Check if process is actually running (handles zombies correctly)
|
|
455
|
+
if not _is_process_alive(pid):
|
|
456
|
+
if not quiet:
|
|
457
|
+
click.echo(f"Gobby daemon is not running (stale PID file with PID {pid})")
|
|
458
|
+
pid_file.unlink(missing_ok=True)
|
|
459
|
+
return True
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
# Send SIGTERM signal for graceful shutdown
|
|
463
|
+
os.kill(pid, signal.SIGTERM)
|
|
464
|
+
if not quiet:
|
|
465
|
+
click.echo(f"Sent shutdown signal to Gobby daemon (PID {pid})")
|
|
466
|
+
|
|
467
|
+
# Wait for graceful shutdown
|
|
468
|
+
max_wait = 5
|
|
469
|
+
for _ in range(max_wait * 10):
|
|
470
|
+
time.sleep(0.1)
|
|
471
|
+
if not _is_process_alive(pid):
|
|
472
|
+
if not quiet:
|
|
473
|
+
click.echo("Gobby daemon stopped successfully")
|
|
474
|
+
pid_file.unlink(missing_ok=True)
|
|
475
|
+
return True
|
|
476
|
+
|
|
477
|
+
# Process didn't stop gracefully - try force kill
|
|
478
|
+
if not quiet:
|
|
479
|
+
click.echo(f"Process didn't stop gracefully after {max_wait}s, force killing...")
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
os.kill(pid, signal.SIGKILL)
|
|
483
|
+
time.sleep(0.5)
|
|
484
|
+
except ProcessLookupError:
|
|
485
|
+
pass # Already dead
|
|
486
|
+
|
|
487
|
+
# Final check
|
|
488
|
+
if not _is_process_alive(pid):
|
|
489
|
+
if not quiet:
|
|
490
|
+
click.echo("Gobby daemon force killed successfully")
|
|
491
|
+
pid_file.unlink(missing_ok=True)
|
|
492
|
+
return True
|
|
493
|
+
|
|
494
|
+
if not quiet:
|
|
495
|
+
click.echo("Warning: Failed to stop process", err=True)
|
|
496
|
+
return False
|
|
497
|
+
|
|
498
|
+
except PermissionError:
|
|
499
|
+
if not quiet:
|
|
500
|
+
click.echo(f"Error: Permission denied to stop process (PID {pid})", err=True)
|
|
501
|
+
return False
|
|
502
|
+
|
|
503
|
+
except ProcessLookupError:
|
|
504
|
+
# Process died between our check and sending signal - that's fine
|
|
505
|
+
if not quiet:
|
|
506
|
+
click.echo("Gobby daemon stopped")
|
|
507
|
+
pid_file.unlink(missing_ok=True)
|
|
508
|
+
return True
|
|
509
|
+
|
|
510
|
+
except Exception as e:
|
|
511
|
+
if not quiet:
|
|
512
|
+
click.echo(f"Error stopping daemon: {e}", err=True)
|
|
513
|
+
return False
|