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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Local project storage manager."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from gobby.storage.database import DatabaseProtocol
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Project:
|
|
16
|
+
"""Project data model."""
|
|
17
|
+
|
|
18
|
+
id: str
|
|
19
|
+
name: str
|
|
20
|
+
repo_path: str | None
|
|
21
|
+
github_url: str | None
|
|
22
|
+
created_at: str
|
|
23
|
+
updated_at: str
|
|
24
|
+
github_repo: str | None = None # GitHub repo in "owner/repo" format
|
|
25
|
+
linear_team_id: str | None = None # Linear team ID for project sync
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_row(cls, row: Any) -> "Project":
|
|
29
|
+
"""Create Project from database row."""
|
|
30
|
+
return cls(
|
|
31
|
+
id=row["id"],
|
|
32
|
+
name=row["name"],
|
|
33
|
+
repo_path=row["repo_path"],
|
|
34
|
+
github_url=row["github_url"],
|
|
35
|
+
created_at=row["created_at"],
|
|
36
|
+
updated_at=row["updated_at"],
|
|
37
|
+
github_repo=row["github_repo"] if "github_repo" in row.keys() else None,
|
|
38
|
+
linear_team_id=row["linear_team_id"] if "linear_team_id" in row.keys() else None,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
"""Convert to dictionary."""
|
|
43
|
+
return {
|
|
44
|
+
"id": self.id,
|
|
45
|
+
"name": self.name,
|
|
46
|
+
"repo_path": self.repo_path,
|
|
47
|
+
"github_url": self.github_url,
|
|
48
|
+
"github_repo": self.github_repo,
|
|
49
|
+
"linear_team_id": self.linear_team_id,
|
|
50
|
+
"created_at": self.created_at,
|
|
51
|
+
"updated_at": self.updated_at,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LocalProjectManager:
|
|
56
|
+
"""Manager for local project storage."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, db: DatabaseProtocol):
|
|
59
|
+
"""Initialize with database connection."""
|
|
60
|
+
self.db = db
|
|
61
|
+
|
|
62
|
+
def create(
|
|
63
|
+
self,
|
|
64
|
+
name: str,
|
|
65
|
+
repo_path: str | None = None,
|
|
66
|
+
github_url: str | None = None,
|
|
67
|
+
) -> Project:
|
|
68
|
+
"""
|
|
69
|
+
Create a new project.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Unique project name
|
|
73
|
+
repo_path: Local repository path
|
|
74
|
+
github_url: GitHub repository URL
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Created Project instance
|
|
78
|
+
"""
|
|
79
|
+
project_id = str(uuid.uuid4())
|
|
80
|
+
now = datetime.now(UTC).isoformat()
|
|
81
|
+
|
|
82
|
+
self.db.execute(
|
|
83
|
+
"""
|
|
84
|
+
INSERT INTO projects (id, name, repo_path, github_url, created_at, updated_at)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
86
|
+
""",
|
|
87
|
+
(project_id, name, repo_path, github_url, now, now),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return Project(
|
|
91
|
+
id=project_id,
|
|
92
|
+
name=name,
|
|
93
|
+
repo_path=repo_path,
|
|
94
|
+
github_url=github_url,
|
|
95
|
+
created_at=now,
|
|
96
|
+
updated_at=now,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def get(self, project_id: str) -> Project | None:
|
|
100
|
+
"""Get project by ID."""
|
|
101
|
+
row = self.db.fetchone("SELECT * FROM projects WHERE id = ?", (project_id,))
|
|
102
|
+
return Project.from_row(row) if row else None
|
|
103
|
+
|
|
104
|
+
def get_by_name(self, name: str) -> Project | None:
|
|
105
|
+
"""Get project by name."""
|
|
106
|
+
row = self.db.fetchone("SELECT * FROM projects WHERE name = ?", (name,))
|
|
107
|
+
return Project.from_row(row) if row else None
|
|
108
|
+
|
|
109
|
+
def get_or_create(
|
|
110
|
+
self,
|
|
111
|
+
name: str,
|
|
112
|
+
repo_path: str | None = None,
|
|
113
|
+
github_url: str | None = None,
|
|
114
|
+
) -> Project:
|
|
115
|
+
"""Get existing project or create new one."""
|
|
116
|
+
project = self.get_by_name(name)
|
|
117
|
+
if project:
|
|
118
|
+
return project
|
|
119
|
+
return self.create(name, repo_path, github_url)
|
|
120
|
+
|
|
121
|
+
def list(self) -> list[Project]:
|
|
122
|
+
"""List all projects."""
|
|
123
|
+
rows = self.db.fetchall("SELECT * FROM projects ORDER BY name")
|
|
124
|
+
return [Project.from_row(row) for row in rows]
|
|
125
|
+
|
|
126
|
+
def update(self, project_id: str, **fields: Any) -> Project | None:
|
|
127
|
+
"""
|
|
128
|
+
Update project fields.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
project_id: Project ID
|
|
132
|
+
**fields: Fields to update (name, repo_path, github_url)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Updated Project or None if not found
|
|
136
|
+
"""
|
|
137
|
+
if not fields:
|
|
138
|
+
return self.get(project_id)
|
|
139
|
+
|
|
140
|
+
allowed = {"name", "repo_path", "github_url", "github_repo", "linear_team_id"}
|
|
141
|
+
fields = {k: v for k, v in fields.items() if k in allowed}
|
|
142
|
+
if not fields:
|
|
143
|
+
return self.get(project_id)
|
|
144
|
+
|
|
145
|
+
fields["updated_at"] = datetime.now(UTC).isoformat()
|
|
146
|
+
|
|
147
|
+
# nosec B608: Fields validated against allowlist above, values parameterized
|
|
148
|
+
set_clause = ", ".join(f"{k} = ?" for k in fields)
|
|
149
|
+
values = list(fields.values()) + [project_id]
|
|
150
|
+
|
|
151
|
+
self.db.execute(
|
|
152
|
+
f"UPDATE projects SET {set_clause} WHERE id = ?", # nosec B608
|
|
153
|
+
tuple(values),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return self.get(project_id)
|
|
157
|
+
|
|
158
|
+
def delete(self, project_id: str) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
Delete project by ID.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if deleted, False if not found
|
|
164
|
+
"""
|
|
165
|
+
cursor = self.db.execute("DELETE FROM projects WHERE id = ?", (project_id,))
|
|
166
|
+
return cursor.rowcount > 0
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local storage for session messages.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from gobby.sessions.transcripts.base import ParsedMessage
|
|
11
|
+
from gobby.storage.database import DatabaseProtocol
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LocalSessionMessageManager:
|
|
17
|
+
"""Manages storage of session messages and processing state."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, database: DatabaseProtocol):
|
|
20
|
+
self.db = database
|
|
21
|
+
|
|
22
|
+
async def store_messages(self, session_id: str, messages: list[ParsedMessage]) -> int:
|
|
23
|
+
"""
|
|
24
|
+
Store parsed messages for a session.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
session_id: ID of the session
|
|
28
|
+
messages: List of ParsedMessage objects
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Number of messages stored
|
|
32
|
+
"""
|
|
33
|
+
if not messages:
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
def _store_blocking() -> int:
|
|
37
|
+
# Check if session exists to avoid FOREIGN KEY constraint errors
|
|
38
|
+
# This can happen when sessions are created in hub-only mode but
|
|
39
|
+
# message processor is using the project database
|
|
40
|
+
session_exists = self.db.fetchone("SELECT 1 FROM sessions WHERE id = ?", (session_id,))
|
|
41
|
+
if not session_exists:
|
|
42
|
+
logger.debug(
|
|
43
|
+
f"Session {session_id} not found in database, skipping message storage"
|
|
44
|
+
)
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
count = 0
|
|
48
|
+
for msg in messages:
|
|
49
|
+
# Convert dicts to JSON strings for storage
|
|
50
|
+
tool_input = json.dumps(msg.tool_input) if msg.tool_input is not None else None
|
|
51
|
+
tool_result = json.dumps(msg.tool_result) if msg.tool_result is not None else None
|
|
52
|
+
raw_json = json.dumps(msg.raw_json) if msg.raw_json is not None else None
|
|
53
|
+
|
|
54
|
+
query = """
|
|
55
|
+
INSERT INTO session_messages (
|
|
56
|
+
session_id, message_index, role, content, content_type,
|
|
57
|
+
tool_name, tool_input, tool_result, timestamp, raw_json
|
|
58
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
59
|
+
ON CONFLICT(session_id, message_index) DO UPDATE SET
|
|
60
|
+
content=excluded.content,
|
|
61
|
+
content_type=excluded.content_type,
|
|
62
|
+
tool_name=excluded.tool_name,
|
|
63
|
+
tool_input=excluded.tool_input,
|
|
64
|
+
tool_result=excluded.tool_result,
|
|
65
|
+
timestamp=excluded.timestamp,
|
|
66
|
+
raw_json=excluded.raw_json
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
self.db.execute(
|
|
70
|
+
query,
|
|
71
|
+
(
|
|
72
|
+
session_id,
|
|
73
|
+
msg.index,
|
|
74
|
+
msg.role,
|
|
75
|
+
msg.content,
|
|
76
|
+
msg.content_type,
|
|
77
|
+
msg.tool_name,
|
|
78
|
+
tool_input,
|
|
79
|
+
tool_result,
|
|
80
|
+
msg.timestamp.isoformat(),
|
|
81
|
+
raw_json,
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
count += 1
|
|
85
|
+
return count
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
return await asyncio.to_thread(_store_blocking)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Failed to store messages for session {session_id}: {e}")
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
async def get_messages(
|
|
94
|
+
self,
|
|
95
|
+
session_id: str,
|
|
96
|
+
limit: int = 100,
|
|
97
|
+
offset: int = 0,
|
|
98
|
+
role: str | None = None,
|
|
99
|
+
) -> list[dict[str, Any]]:
|
|
100
|
+
"""
|
|
101
|
+
Retrieve messages for a session.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
session_id: Session ID
|
|
105
|
+
limit: Maximum number of messages to return
|
|
106
|
+
offset: Offset for pagination
|
|
107
|
+
role: Optional role to filter by
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of message dictionaries
|
|
111
|
+
"""
|
|
112
|
+
query = "SELECT * FROM session_messages WHERE session_id = ?"
|
|
113
|
+
params: list[Any] = [session_id]
|
|
114
|
+
|
|
115
|
+
if role:
|
|
116
|
+
query += " AND role = ?"
|
|
117
|
+
params.append(role)
|
|
118
|
+
|
|
119
|
+
query += " ORDER BY message_index ASC LIMIT ? OFFSET ?"
|
|
120
|
+
params.extend([limit, offset])
|
|
121
|
+
|
|
122
|
+
rows = await asyncio.to_thread(self.db.fetchall, query, tuple(params))
|
|
123
|
+
return [dict(row) for row in rows]
|
|
124
|
+
|
|
125
|
+
async def get_state(self, session_id: str) -> dict[str, Any] | None:
|
|
126
|
+
"""
|
|
127
|
+
Get processing state for a session.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
session_id: Session ID
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
State dictionary or None if not found
|
|
134
|
+
"""
|
|
135
|
+
row = await asyncio.to_thread(
|
|
136
|
+
self.db.fetchone,
|
|
137
|
+
"SELECT * FROM session_message_state WHERE session_id = ?",
|
|
138
|
+
(session_id,),
|
|
139
|
+
)
|
|
140
|
+
return dict(row) if row else None
|
|
141
|
+
|
|
142
|
+
async def update_state(
|
|
143
|
+
self,
|
|
144
|
+
session_id: str,
|
|
145
|
+
byte_offset: int,
|
|
146
|
+
message_index: int,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Update processing state for a session.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
session_id: Session ID
|
|
153
|
+
byte_offset: New byte offset in source file
|
|
154
|
+
message_index: Index of last processed message
|
|
155
|
+
"""
|
|
156
|
+
# Check if session exists to avoid FOREIGN KEY constraint errors
|
|
157
|
+
session_exists = await asyncio.to_thread(
|
|
158
|
+
self.db.fetchone,
|
|
159
|
+
"SELECT 1 FROM sessions WHERE id = ?",
|
|
160
|
+
(session_id,),
|
|
161
|
+
)
|
|
162
|
+
if not session_exists:
|
|
163
|
+
logger.debug(f"Session {session_id} not found in database, skipping state update")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
sql = """
|
|
167
|
+
INSERT INTO session_message_state (
|
|
168
|
+
session_id, last_byte_offset, last_message_index,
|
|
169
|
+
last_processed_at, updated_at
|
|
170
|
+
) VALUES (?, ?, ?, datetime('now'), datetime('now'))
|
|
171
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
172
|
+
last_byte_offset=excluded.last_byte_offset,
|
|
173
|
+
last_message_index=excluded.last_message_index,
|
|
174
|
+
last_processed_at=excluded.last_processed_at,
|
|
175
|
+
updated_at=excluded.updated_at
|
|
176
|
+
"""
|
|
177
|
+
await asyncio.to_thread(self.db.execute, sql, (session_id, byte_offset, message_index))
|
|
178
|
+
|
|
179
|
+
async def count_messages(self, session_id: str) -> int:
|
|
180
|
+
"""
|
|
181
|
+
Count messages for a session.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
session_id: Session ID
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Number of messages
|
|
188
|
+
"""
|
|
189
|
+
result = await asyncio.to_thread(
|
|
190
|
+
self.db.fetchone,
|
|
191
|
+
"SELECT COUNT(*) as count FROM session_messages WHERE session_id = ?",
|
|
192
|
+
(session_id,),
|
|
193
|
+
)
|
|
194
|
+
return result["count"] if result else 0
|
|
195
|
+
|
|
196
|
+
async def get_all_counts(self) -> dict[str, int]:
|
|
197
|
+
"""
|
|
198
|
+
Get message counts for all sessions.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Dictionary mapping session_id to count
|
|
202
|
+
"""
|
|
203
|
+
rows = await asyncio.to_thread(
|
|
204
|
+
self.db.fetchall,
|
|
205
|
+
"SELECT session_id, COUNT(*) as count FROM session_messages GROUP BY session_id",
|
|
206
|
+
)
|
|
207
|
+
return {row["session_id"]: row["count"] for row in rows}
|
|
208
|
+
|
|
209
|
+
async def search_messages(
|
|
210
|
+
self,
|
|
211
|
+
query_text: str,
|
|
212
|
+
limit: int = 20,
|
|
213
|
+
offset: int = 0,
|
|
214
|
+
session_id: str | None = None,
|
|
215
|
+
project_id: str | None = None,
|
|
216
|
+
) -> list[dict[str, Any]]:
|
|
217
|
+
"""
|
|
218
|
+
Search messages using simple text matching.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
query_text: Text to search for
|
|
222
|
+
limit: Max results
|
|
223
|
+
offset: Pagination offset
|
|
224
|
+
session_id: Optional session ID to filter by
|
|
225
|
+
project_id: Optional project ID to filter by
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
List of matching messages
|
|
229
|
+
"""
|
|
230
|
+
# Escape LIKE wildcards in query_text
|
|
231
|
+
escaped_query = query_text.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
232
|
+
sql = "SELECT m.* FROM session_messages m"
|
|
233
|
+
params: list[Any] = []
|
|
234
|
+
conditions: list[str] = ["m.content LIKE ? ESCAPE '\\'"]
|
|
235
|
+
params.append(f"%{escaped_query}%")
|
|
236
|
+
|
|
237
|
+
if project_id:
|
|
238
|
+
sql += " JOIN sessions s ON m.session_id = s.session_id"
|
|
239
|
+
conditions.append("s.project_id = ?")
|
|
240
|
+
params.append(project_id)
|
|
241
|
+
|
|
242
|
+
if session_id:
|
|
243
|
+
conditions.append("m.session_id = ?")
|
|
244
|
+
params.append(session_id)
|
|
245
|
+
|
|
246
|
+
sql += " WHERE " + " AND ".join(conditions)
|
|
247
|
+
sql += " ORDER BY m.timestamp DESC LIMIT ? OFFSET ?"
|
|
248
|
+
params.extend([limit, offset])
|
|
249
|
+
|
|
250
|
+
rows = await asyncio.to_thread(self.db.fetchall, sql, tuple(params))
|
|
251
|
+
return [dict(row) for row in rows]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import UTC, datetime
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from gobby.storage.database import DatabaseProtocol
|
|
6
|
+
from gobby.storage.tasks import Task
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
SessionTaskAction = Literal["worked_on", "discovered", "mentioned", "closed"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SessionTaskManager:
|
|
14
|
+
VALID_ACTIONS = {"worked_on", "discovered", "mentioned", "closed"}
|
|
15
|
+
|
|
16
|
+
def __init__(self, db: DatabaseProtocol):
|
|
17
|
+
self.db = db
|
|
18
|
+
|
|
19
|
+
def link_task(
|
|
20
|
+
self,
|
|
21
|
+
session_id: str,
|
|
22
|
+
task_id: str,
|
|
23
|
+
action: str = "worked_on",
|
|
24
|
+
) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Link a task to a session with a specific action.
|
|
27
|
+
Actions: worked_on, discovered, mentioned, closed
|
|
28
|
+
"""
|
|
29
|
+
if action not in self.VALID_ACTIONS:
|
|
30
|
+
raise ValueError(f"Invalid action '{action}'. Must be one of {self.VALID_ACTIONS}")
|
|
31
|
+
|
|
32
|
+
now = datetime.now(UTC).isoformat()
|
|
33
|
+
|
|
34
|
+
with self.db.transaction() as conn:
|
|
35
|
+
# Use INSERT OR IGNORE to handle duplicate links gracefully
|
|
36
|
+
conn.execute(
|
|
37
|
+
"""
|
|
38
|
+
INSERT OR IGNORE INTO session_tasks (
|
|
39
|
+
session_id, task_id, action, created_at
|
|
40
|
+
) VALUES (?, ?, ?, ?)
|
|
41
|
+
""",
|
|
42
|
+
(session_id, task_id, action, now),
|
|
43
|
+
)
|
|
44
|
+
logger.debug(f"Linked task {task_id} to session {session_id} with action {action}")
|
|
45
|
+
|
|
46
|
+
def unlink_task(
|
|
47
|
+
self,
|
|
48
|
+
session_id: str,
|
|
49
|
+
task_id: str,
|
|
50
|
+
action: str,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Remove a link between a task and a session."""
|
|
53
|
+
with self.db.transaction() as conn:
|
|
54
|
+
conn.execute(
|
|
55
|
+
"""
|
|
56
|
+
DELETE FROM session_tasks
|
|
57
|
+
WHERE session_id = ? AND task_id = ? AND action = ?
|
|
58
|
+
""",
|
|
59
|
+
(session_id, task_id, action),
|
|
60
|
+
)
|
|
61
|
+
logger.debug(f"Unlinked task {task_id} from session {session_id} for action {action}")
|
|
62
|
+
|
|
63
|
+
def get_session_tasks(self, session_id: str) -> list[dict[str, Any]]:
|
|
64
|
+
"""
|
|
65
|
+
Get all tasks associated with a session.
|
|
66
|
+
Returns a list of dicts with task details and the action.
|
|
67
|
+
"""
|
|
68
|
+
query = """
|
|
69
|
+
SELECT t.*, st.action as session_action, st.created_at as link_created_at
|
|
70
|
+
FROM tasks t
|
|
71
|
+
JOIN session_tasks st ON t.id = st.task_id
|
|
72
|
+
WHERE st.session_id = ?
|
|
73
|
+
ORDER BY st.created_at DESC
|
|
74
|
+
"""
|
|
75
|
+
rows = self.db.fetchall(query, (session_id,))
|
|
76
|
+
|
|
77
|
+
results = []
|
|
78
|
+
for row in rows:
|
|
79
|
+
task = Task.from_row(row)
|
|
80
|
+
results.append(
|
|
81
|
+
{
|
|
82
|
+
"task": task,
|
|
83
|
+
"action": row["session_action"],
|
|
84
|
+
"link_created_at": row["link_created_at"],
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
return results
|
|
88
|
+
|
|
89
|
+
def get_task_sessions(self, task_id: str) -> list[dict[str, Any]]:
|
|
90
|
+
"""
|
|
91
|
+
Get all sessions associated with a task.
|
|
92
|
+
"""
|
|
93
|
+
# Simple query that relies only on session_tasks to minimize dependencies
|
|
94
|
+
rows = self.db.fetchall(
|
|
95
|
+
"SELECT * FROM session_tasks WHERE task_id = ? ORDER BY created_at DESC", (task_id,)
|
|
96
|
+
)
|
|
97
|
+
return [dict(row) for row in rows]
|