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/artifacts.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""CLI commands for session artifacts."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from gobby.storage.artifacts import Artifact, LocalArtifactManager
|
|
9
|
+
from gobby.storage.database import LocalDatabase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_artifact_manager() -> LocalArtifactManager:
|
|
13
|
+
"""Get the artifact manager."""
|
|
14
|
+
db = LocalDatabase()
|
|
15
|
+
return LocalArtifactManager(db)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
def artifacts() -> None:
|
|
20
|
+
"""Manage session artifacts (code, diffs, errors)."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@artifacts.command()
|
|
25
|
+
@click.argument("query")
|
|
26
|
+
@click.option("--session", "-s", "session_id", help="Filter by session ID")
|
|
27
|
+
@click.option("--type", "-t", "artifact_type", help="Filter by artifact type (code, diff, error)")
|
|
28
|
+
@click.option("--limit", "-n", default=50, help="Maximum results")
|
|
29
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
30
|
+
def search(
|
|
31
|
+
query: str,
|
|
32
|
+
session_id: str | None,
|
|
33
|
+
artifact_type: str | None,
|
|
34
|
+
limit: int,
|
|
35
|
+
output_json: bool,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Search artifacts by content.
|
|
38
|
+
|
|
39
|
+
Uses full-text search to find matching artifacts.
|
|
40
|
+
"""
|
|
41
|
+
manager = get_artifact_manager()
|
|
42
|
+
artifacts_list = manager.search_artifacts(
|
|
43
|
+
query_text=query,
|
|
44
|
+
session_id=session_id,
|
|
45
|
+
artifact_type=artifact_type,
|
|
46
|
+
limit=limit,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if not artifacts_list:
|
|
50
|
+
if output_json:
|
|
51
|
+
click.echo(json.dumps({"artifacts": [], "count": 0}))
|
|
52
|
+
else:
|
|
53
|
+
click.echo("No artifacts found")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if output_json:
|
|
57
|
+
click.echo(
|
|
58
|
+
json.dumps(
|
|
59
|
+
{
|
|
60
|
+
"artifacts": [a.to_dict() for a in artifacts_list],
|
|
61
|
+
"count": len(artifacts_list),
|
|
62
|
+
},
|
|
63
|
+
indent=2,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
_display_artifact_list(artifacts_list)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@artifacts.command("list")
|
|
71
|
+
@click.option("--session", "-s", "session_id", help="Filter by session ID")
|
|
72
|
+
@click.option("--type", "-t", "artifact_type", help="Filter by artifact type (code, diff, error)")
|
|
73
|
+
@click.option("--limit", "-n", default=100, help="Maximum results")
|
|
74
|
+
@click.option("--offset", "-o", default=0, help="Offset for pagination")
|
|
75
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
76
|
+
def list_artifacts(
|
|
77
|
+
session_id: str | None,
|
|
78
|
+
artifact_type: str | None,
|
|
79
|
+
limit: int,
|
|
80
|
+
offset: int,
|
|
81
|
+
output_json: bool,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""List artifacts with optional filters."""
|
|
84
|
+
manager = get_artifact_manager()
|
|
85
|
+
artifacts_list = manager.list_artifacts(
|
|
86
|
+
session_id=session_id,
|
|
87
|
+
artifact_type=artifact_type,
|
|
88
|
+
limit=limit,
|
|
89
|
+
offset=offset,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if output_json:
|
|
93
|
+
click.echo(
|
|
94
|
+
json.dumps(
|
|
95
|
+
{
|
|
96
|
+
"artifacts": [a.to_dict() for a in artifacts_list],
|
|
97
|
+
"count": len(artifacts_list),
|
|
98
|
+
},
|
|
99
|
+
indent=2,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
if not artifacts_list:
|
|
104
|
+
click.echo("No artifacts found")
|
|
105
|
+
return
|
|
106
|
+
_display_artifact_list(artifacts_list)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@artifacts.command()
|
|
110
|
+
@click.argument("artifact_id")
|
|
111
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show full metadata")
|
|
112
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
113
|
+
def show(artifact_id: str, verbose: bool, output_json: bool) -> None:
|
|
114
|
+
"""Display a single artifact by ID."""
|
|
115
|
+
manager = get_artifact_manager()
|
|
116
|
+
artifact = manager.get_artifact(artifact_id)
|
|
117
|
+
|
|
118
|
+
if artifact is None:
|
|
119
|
+
if output_json:
|
|
120
|
+
click.echo(json.dumps({"error": f"Artifact '{artifact_id}' not found"}))
|
|
121
|
+
else:
|
|
122
|
+
click.echo(f"Artifact not found: {artifact_id}", err=True)
|
|
123
|
+
raise SystemExit(1)
|
|
124
|
+
|
|
125
|
+
if output_json:
|
|
126
|
+
click.echo(json.dumps(artifact.to_dict(), indent=2))
|
|
127
|
+
else:
|
|
128
|
+
_display_artifact_detail(artifact, verbose)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@artifacts.command()
|
|
132
|
+
@click.argument("session_id")
|
|
133
|
+
@click.option("--type", "-t", "artifact_type", help="Filter by artifact type")
|
|
134
|
+
@click.option("--limit", "-n", default=100, help="Maximum results")
|
|
135
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
136
|
+
def timeline(
|
|
137
|
+
session_id: str,
|
|
138
|
+
artifact_type: str | None,
|
|
139
|
+
limit: int,
|
|
140
|
+
output_json: bool,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Show artifacts for a session in chronological order.
|
|
143
|
+
|
|
144
|
+
Displays artifacts from oldest to newest.
|
|
145
|
+
"""
|
|
146
|
+
manager = get_artifact_manager()
|
|
147
|
+
artifacts_list = manager.list_artifacts(
|
|
148
|
+
session_id=session_id,
|
|
149
|
+
artifact_type=artifact_type,
|
|
150
|
+
limit=limit,
|
|
151
|
+
offset=0,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Reverse to get chronological order (oldest first)
|
|
155
|
+
artifacts_list = list(reversed(artifacts_list))
|
|
156
|
+
|
|
157
|
+
if output_json:
|
|
158
|
+
click.echo(
|
|
159
|
+
json.dumps(
|
|
160
|
+
{
|
|
161
|
+
"session_id": session_id,
|
|
162
|
+
"artifacts": [a.to_dict() for a in artifacts_list],
|
|
163
|
+
"count": len(artifacts_list),
|
|
164
|
+
},
|
|
165
|
+
indent=2,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
if not artifacts_list:
|
|
170
|
+
click.echo(f"No artifacts found for session: {session_id}")
|
|
171
|
+
return
|
|
172
|
+
click.echo(f"Timeline for session: {session_id}")
|
|
173
|
+
click.echo("-" * 60)
|
|
174
|
+
for artifact in artifacts_list:
|
|
175
|
+
_display_timeline_entry(artifact)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _display_artifact_list(artifacts_list: list[Any]) -> None:
|
|
179
|
+
"""Display a list of artifacts in table format."""
|
|
180
|
+
# Header
|
|
181
|
+
click.echo(f"{'ID':<12} {'Type':<8} {'Source':<20} {'Created':<20}")
|
|
182
|
+
click.echo("-" * 60)
|
|
183
|
+
|
|
184
|
+
for artifact in artifacts_list:
|
|
185
|
+
artifact_id = artifact.id[:12] if len(artifact.id) > 12 else artifact.id
|
|
186
|
+
source = artifact.source_file or "-"
|
|
187
|
+
if len(source) > 18:
|
|
188
|
+
source = "..." + source[-15:]
|
|
189
|
+
created = artifact.created_at[:19] if artifact.created_at else "-"
|
|
190
|
+
click.echo(f"{artifact_id:<12} {artifact.artifact_type:<8} {source:<20} {created:<20}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _display_artifact_detail(artifact: Artifact, verbose: bool) -> None:
|
|
194
|
+
"""Display a single artifact with optional verbosity."""
|
|
195
|
+
click.echo(f"ID: {artifact.id}")
|
|
196
|
+
click.echo(f"Type: {artifact.artifact_type}")
|
|
197
|
+
click.echo(f"Session: {artifact.session_id}")
|
|
198
|
+
|
|
199
|
+
if artifact.source_file:
|
|
200
|
+
location = artifact.source_file
|
|
201
|
+
if artifact.line_start:
|
|
202
|
+
location += f":{artifact.line_start}"
|
|
203
|
+
if artifact.line_end and artifact.line_end != artifact.line_start:
|
|
204
|
+
location += f"-{artifact.line_end}"
|
|
205
|
+
click.echo(f"Source: {location}")
|
|
206
|
+
|
|
207
|
+
click.echo(f"Created: {artifact.created_at}")
|
|
208
|
+
|
|
209
|
+
if verbose and artifact.metadata:
|
|
210
|
+
click.echo(f"Metadata: {json.dumps(artifact.metadata, indent=2)}")
|
|
211
|
+
|
|
212
|
+
click.echo("")
|
|
213
|
+
click.echo("-" * 60)
|
|
214
|
+
|
|
215
|
+
# Display content with syntax highlighting for code
|
|
216
|
+
_display_content(artifact.content, artifact.artifact_type, artifact.metadata)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _display_content(content: str, artifact_type: str, metadata: dict[str, Any] | None) -> None:
|
|
220
|
+
"""Display content with appropriate formatting."""
|
|
221
|
+
# Try to use rich for syntax highlighting if available
|
|
222
|
+
try:
|
|
223
|
+
from rich.console import Console
|
|
224
|
+
from rich.syntax import Syntax
|
|
225
|
+
|
|
226
|
+
console = Console()
|
|
227
|
+
|
|
228
|
+
# Determine language for syntax highlighting
|
|
229
|
+
language = None
|
|
230
|
+
if metadata and "language" in metadata:
|
|
231
|
+
language = metadata["language"]
|
|
232
|
+
elif artifact_type == "code":
|
|
233
|
+
# Default to python if no language specified
|
|
234
|
+
language = "python"
|
|
235
|
+
elif artifact_type == "diff":
|
|
236
|
+
language = "diff"
|
|
237
|
+
elif artifact_type == "error":
|
|
238
|
+
language = "text"
|
|
239
|
+
|
|
240
|
+
if language:
|
|
241
|
+
syntax = Syntax(content, language, theme="monokai", line_numbers=True)
|
|
242
|
+
console.print(syntax)
|
|
243
|
+
else:
|
|
244
|
+
click.echo(content)
|
|
245
|
+
|
|
246
|
+
except ImportError:
|
|
247
|
+
# Fall back to plain text
|
|
248
|
+
click.echo(content)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _display_timeline_entry(artifact: Artifact) -> None:
|
|
252
|
+
"""Display a single timeline entry."""
|
|
253
|
+
click.echo(f"[{artifact.created_at[:19]}] {artifact.artifact_type.upper()}: {artifact.id}")
|
|
254
|
+
if artifact.source_file:
|
|
255
|
+
click.echo(f" Source: {artifact.source_file}")
|
|
256
|
+
|
|
257
|
+
# Show a preview of the content (first 2 lines)
|
|
258
|
+
lines = artifact.content.split("\n")[:2]
|
|
259
|
+
for line in lines:
|
|
260
|
+
if len(line) > 60:
|
|
261
|
+
line = line[:57] + "..."
|
|
262
|
+
click.echo(f" | {line}")
|
|
263
|
+
|
|
264
|
+
if len(artifact.content.split("\n")) > 2:
|
|
265
|
+
click.echo(f" | ... ({len(artifact.content.split(chr(10)))} lines total)")
|
|
266
|
+
click.echo("")
|
gobby/cli/daemon.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Daemon management commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import subprocess # nosec B404 - subprocess needed for daemon management
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
import httpx
|
|
15
|
+
import psutil
|
|
16
|
+
|
|
17
|
+
from gobby.utils.status import fetch_rich_status, format_status_message
|
|
18
|
+
|
|
19
|
+
from .utils import (
|
|
20
|
+
format_uptime,
|
|
21
|
+
get_gobby_home,
|
|
22
|
+
init_local_storage,
|
|
23
|
+
is_port_available,
|
|
24
|
+
kill_all_gobby_daemons,
|
|
25
|
+
setup_logging,
|
|
26
|
+
wait_for_port_available,
|
|
27
|
+
)
|
|
28
|
+
from .utils import (
|
|
29
|
+
stop_daemon as stop_daemon_util,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.command()
|
|
36
|
+
@click.option(
|
|
37
|
+
"--verbose",
|
|
38
|
+
"-v",
|
|
39
|
+
is_flag=True,
|
|
40
|
+
help="Enable verbose debug output",
|
|
41
|
+
)
|
|
42
|
+
@click.pass_context
|
|
43
|
+
def start(ctx: click.Context, verbose: bool) -> None:
|
|
44
|
+
"""Start the Gobby daemon."""
|
|
45
|
+
# Get config object
|
|
46
|
+
config = ctx.obj["config"]
|
|
47
|
+
|
|
48
|
+
# Get paths from config (respects GOBBY_HOME env var)
|
|
49
|
+
gobby_dir = get_gobby_home()
|
|
50
|
+
pid_file = gobby_dir / "gobby.pid"
|
|
51
|
+
log_file = Path(config.logging.client).expanduser()
|
|
52
|
+
error_log_file = Path(config.logging.client_error).expanduser()
|
|
53
|
+
|
|
54
|
+
gobby_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
error_log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
# Initialize local storage before starting daemon
|
|
59
|
+
click.echo("Initializing local storage...")
|
|
60
|
+
init_local_storage()
|
|
61
|
+
|
|
62
|
+
# Check if already running
|
|
63
|
+
if pid_file.exists():
|
|
64
|
+
try:
|
|
65
|
+
with open(pid_file) as f:
|
|
66
|
+
pid = int(f.read().strip())
|
|
67
|
+
|
|
68
|
+
# Check if process is actually running
|
|
69
|
+
try:
|
|
70
|
+
os.kill(pid, 0)
|
|
71
|
+
click.echo(f"Gobby daemon is already running (PID: {pid})", err=True)
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
except ProcessLookupError:
|
|
74
|
+
# Stale PID file
|
|
75
|
+
click.echo(f"Removing stale PID file (PID: {pid})")
|
|
76
|
+
pid_file.unlink()
|
|
77
|
+
except Exception:
|
|
78
|
+
pid_file.unlink()
|
|
79
|
+
|
|
80
|
+
# Kill any existing gobby processes
|
|
81
|
+
click.echo("Checking for existing gobby processes...")
|
|
82
|
+
killed_count = kill_all_gobby_daemons()
|
|
83
|
+
if killed_count > 0:
|
|
84
|
+
click.echo(f"Stopped {killed_count} existing process(es)")
|
|
85
|
+
time.sleep(2.0) # Wait for ports to be released
|
|
86
|
+
else:
|
|
87
|
+
click.echo("No existing processes found")
|
|
88
|
+
|
|
89
|
+
# Check ports
|
|
90
|
+
http_port = config.daemon_port
|
|
91
|
+
ws_port = config.websocket.port
|
|
92
|
+
|
|
93
|
+
if not is_port_available(http_port):
|
|
94
|
+
click.echo(f"Waiting for HTTP port {http_port} to become available...", err=True)
|
|
95
|
+
if not wait_for_port_available(http_port, timeout=5.0):
|
|
96
|
+
click.echo(f"Error: Port {http_port} is still in use", err=True)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
if not is_port_available(ws_port):
|
|
100
|
+
click.echo(f"Waiting for WebSocket port {ws_port} to become available...", err=True)
|
|
101
|
+
if not wait_for_port_available(ws_port, timeout=5.0):
|
|
102
|
+
click.echo(f"Error: Port {ws_port} is still in use", err=True)
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
click.echo(f"Ports available (HTTP: {http_port}, WebSocket: {ws_port})")
|
|
106
|
+
|
|
107
|
+
# Start the runner as a subprocess
|
|
108
|
+
click.echo("Starting Gobby daemon...")
|
|
109
|
+
|
|
110
|
+
# Build command
|
|
111
|
+
cmd = [sys.executable, "-m", "gobby.runner"]
|
|
112
|
+
if verbose:
|
|
113
|
+
cmd.append("--verbose")
|
|
114
|
+
|
|
115
|
+
# Open log files
|
|
116
|
+
log_f = open(log_file, "a")
|
|
117
|
+
error_log_f = open(error_log_file, "a")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Start detached subprocess
|
|
121
|
+
process = subprocess.Popen( # nosec B603 - cmd built from sys.executable and module path
|
|
122
|
+
cmd,
|
|
123
|
+
stdout=log_f,
|
|
124
|
+
stderr=error_log_f,
|
|
125
|
+
stdin=subprocess.DEVNULL,
|
|
126
|
+
start_new_session=True, # Detach from terminal
|
|
127
|
+
env=os.environ.copy(), # Inherit parent's environment (including PATH)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Write PID file
|
|
131
|
+
with open(pid_file, "w") as f:
|
|
132
|
+
f.write(str(process.pid))
|
|
133
|
+
|
|
134
|
+
# Give it a moment to start
|
|
135
|
+
time.sleep(1.0)
|
|
136
|
+
|
|
137
|
+
# Check if still running
|
|
138
|
+
if process.poll() is not None:
|
|
139
|
+
click.echo("Process exited immediately", err=True)
|
|
140
|
+
click.echo(f" Check logs: {error_log_file}", err=True)
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
# Give server time to fully start
|
|
144
|
+
time.sleep(2.0)
|
|
145
|
+
|
|
146
|
+
# Display formatted status
|
|
147
|
+
# Try to verify daemon is responding
|
|
148
|
+
daemon_healthy = False
|
|
149
|
+
start_time = time.time()
|
|
150
|
+
max_wait = 15.0
|
|
151
|
+
|
|
152
|
+
while (time.time() - start_time) < max_wait:
|
|
153
|
+
try:
|
|
154
|
+
response = httpx.get(f"http://localhost:{http_port}/admin/status", timeout=1.0)
|
|
155
|
+
if response.status_code == 200:
|
|
156
|
+
daemon_healthy = True
|
|
157
|
+
break
|
|
158
|
+
except (httpx.ConnectError, httpx.TimeoutException):
|
|
159
|
+
time.sleep(0.5)
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
# Format and display status
|
|
163
|
+
status_kwargs = {
|
|
164
|
+
"running": daemon_healthy,
|
|
165
|
+
"pid": process.pid,
|
|
166
|
+
"pid_file": str(pid_file),
|
|
167
|
+
"log_files": str(log_file.parent),
|
|
168
|
+
"http_port": http_port,
|
|
169
|
+
"websocket_port": ws_port,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Fetch rich status if daemon is healthy
|
|
173
|
+
# Brief delay to allow stats to be computed
|
|
174
|
+
if daemon_healthy:
|
|
175
|
+
time.sleep(1.0)
|
|
176
|
+
rich_status = fetch_rich_status(http_port, timeout=2.0)
|
|
177
|
+
status_kwargs.update(rich_status)
|
|
178
|
+
|
|
179
|
+
message = format_status_message(**status_kwargs)
|
|
180
|
+
click.echo("")
|
|
181
|
+
click.echo(message)
|
|
182
|
+
click.echo("")
|
|
183
|
+
|
|
184
|
+
if not daemon_healthy:
|
|
185
|
+
click.echo("Warning: Daemon started but health check failed")
|
|
186
|
+
click.echo(f" Check logs: {error_log_file}")
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
click.echo(f"Error starting daemon: {e}", err=True)
|
|
190
|
+
sys.exit(1)
|
|
191
|
+
finally:
|
|
192
|
+
log_f.close()
|
|
193
|
+
error_log_f.close()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@click.command()
|
|
197
|
+
@click.pass_context
|
|
198
|
+
def stop(ctx: click.Context) -> None:
|
|
199
|
+
"""Stop the Gobby daemon."""
|
|
200
|
+
success = stop_daemon_util(quiet=False)
|
|
201
|
+
sys.exit(0 if success else 1)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@click.command()
|
|
205
|
+
@click.option(
|
|
206
|
+
"--verbose",
|
|
207
|
+
"-v",
|
|
208
|
+
is_flag=True,
|
|
209
|
+
help="Enable verbose debug output",
|
|
210
|
+
)
|
|
211
|
+
@click.pass_context
|
|
212
|
+
def restart(ctx: click.Context, verbose: bool) -> None:
|
|
213
|
+
"""Restart the Gobby daemon (stop then start)."""
|
|
214
|
+
setup_logging(verbose)
|
|
215
|
+
|
|
216
|
+
click.echo("Restarting Gobby daemon...")
|
|
217
|
+
|
|
218
|
+
# Stop daemon using helper function (doesn't call sys.exit)
|
|
219
|
+
if not stop_daemon_util(quiet=False):
|
|
220
|
+
click.echo("Failed to stop daemon, aborting restart", err=True)
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
|
|
223
|
+
# Wait for cleanup and port release (TIME_WAIT state)
|
|
224
|
+
time.sleep(3)
|
|
225
|
+
|
|
226
|
+
# Call start command
|
|
227
|
+
ctx.invoke(start, verbose=verbose)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@click.command()
|
|
231
|
+
@click.pass_context
|
|
232
|
+
def status(ctx: click.Context) -> None:
|
|
233
|
+
"""Show Gobby daemon status and information."""
|
|
234
|
+
config = ctx.obj["config"]
|
|
235
|
+
pid_file = get_gobby_home() / "gobby.pid"
|
|
236
|
+
log_dir = Path(config.logging.client).expanduser().parent
|
|
237
|
+
|
|
238
|
+
# Read PID from file
|
|
239
|
+
if not pid_file.exists():
|
|
240
|
+
message = format_status_message(running=False)
|
|
241
|
+
click.echo(message)
|
|
242
|
+
sys.exit(0)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
with open(pid_file) as f:
|
|
246
|
+
pid = int(f.read().strip())
|
|
247
|
+
except Exception:
|
|
248
|
+
message = format_status_message(running=False)
|
|
249
|
+
click.echo(message)
|
|
250
|
+
sys.exit(0)
|
|
251
|
+
|
|
252
|
+
# Check if process is actually running
|
|
253
|
+
try:
|
|
254
|
+
os.kill(pid, 0)
|
|
255
|
+
except ProcessLookupError:
|
|
256
|
+
message = format_status_message(running=False)
|
|
257
|
+
click.echo(message)
|
|
258
|
+
click.echo(f"Note: Stale PID file found (PID {pid})")
|
|
259
|
+
sys.exit(0)
|
|
260
|
+
|
|
261
|
+
# Get process info for uptime (fallback)
|
|
262
|
+
try:
|
|
263
|
+
process = psutil.Process(pid)
|
|
264
|
+
uptime_seconds = time.time() - process.create_time()
|
|
265
|
+
uptime_str = format_uptime(uptime_seconds)
|
|
266
|
+
except Exception:
|
|
267
|
+
uptime_str = None
|
|
268
|
+
|
|
269
|
+
http_port = config.daemon_port
|
|
270
|
+
websocket_port = config.websocket.port
|
|
271
|
+
|
|
272
|
+
# Build status kwargs
|
|
273
|
+
status_kwargs: dict[str, Any] = {
|
|
274
|
+
"running": True,
|
|
275
|
+
"pid": pid,
|
|
276
|
+
"pid_file": str(pid_file),
|
|
277
|
+
"log_files": str(log_dir),
|
|
278
|
+
"uptime": uptime_str,
|
|
279
|
+
"http_port": http_port,
|
|
280
|
+
"websocket_port": websocket_port,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# Fetch rich status from daemon API
|
|
284
|
+
rich_status = fetch_rich_status(http_port, timeout=2.0)
|
|
285
|
+
status_kwargs.update(rich_status)
|
|
286
|
+
|
|
287
|
+
# Format and display status
|
|
288
|
+
message = format_status_message(**status_kwargs)
|
|
289
|
+
click.echo(message)
|
|
290
|
+
sys.exit(0)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_merge_status() -> dict[str, Any]:
|
|
294
|
+
"""
|
|
295
|
+
Get the current merge status for status output.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Dict with merge status info:
|
|
299
|
+
- active: bool - Whether there's an active merge
|
|
300
|
+
- resolution_id: str | None - ID of active resolution
|
|
301
|
+
- source_branch: str | None - Source branch being merged
|
|
302
|
+
- target_branch: str | None - Target branch
|
|
303
|
+
- pending_conflicts: int - Number of unresolved conflicts
|
|
304
|
+
"""
|
|
305
|
+
try:
|
|
306
|
+
from gobby.storage.database import LocalDatabase
|
|
307
|
+
from gobby.storage.merge_resolutions import MergeResolutionManager
|
|
308
|
+
|
|
309
|
+
db = LocalDatabase()
|
|
310
|
+
manager = MergeResolutionManager(db)
|
|
311
|
+
|
|
312
|
+
resolution = manager.get_active_resolution()
|
|
313
|
+
if not resolution:
|
|
314
|
+
return {"active": False}
|
|
315
|
+
|
|
316
|
+
conflicts = manager.list_conflicts(resolution_id=resolution.id)
|
|
317
|
+
pending_count = sum(1 for c in conflicts if c.status == "pending")
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
"active": True,
|
|
321
|
+
"resolution_id": resolution.id,
|
|
322
|
+
"source_branch": resolution.source_branch,
|
|
323
|
+
"target_branch": resolution.target_branch,
|
|
324
|
+
"pending_conflicts": pending_count,
|
|
325
|
+
"total_conflicts": len(conflicts),
|
|
326
|
+
}
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.debug(f"Error getting merge status: {e}")
|
|
329
|
+
return {"active": False, "error": str(e)}
|