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,532 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Summary File Generator for session summaries (failover).
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- Session summary generation from JSONL transcripts using LLM synthesis
|
|
6
|
+
- Storage in markdown files (independent of database/workflow)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import subprocess # nosec B404 - subprocess needed for git commands
|
|
12
|
+
import time
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
import anyio
|
|
18
|
+
|
|
19
|
+
from gobby.llm.base import LLMProvider
|
|
20
|
+
from gobby.llm.claude import ClaudeLLMProvider
|
|
21
|
+
from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from gobby.config.app import DaemonConfig
|
|
25
|
+
from gobby.llm.service import LLMService
|
|
26
|
+
|
|
27
|
+
# Backward-compatible alias
|
|
28
|
+
TranscriptProcessor = ClaudeTranscriptParser
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SummaryFileGenerator:
|
|
32
|
+
"""
|
|
33
|
+
Generates session summaries to files using LLM synthesis (failover).
|
|
34
|
+
|
|
35
|
+
Handles:
|
|
36
|
+
- Independent summary generation from JSONL transcripts
|
|
37
|
+
- File storage in ~/.gobby/session_summaries (strictly file-based)
|
|
38
|
+
- Configuration check (session_summary.enabled)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
transcript_processor: ClaudeTranscriptParser,
|
|
44
|
+
summary_file_path: str = "~/.gobby/session_summaries",
|
|
45
|
+
logger_instance: logging.Logger | None = None,
|
|
46
|
+
llm_service: "LLMService | None" = None,
|
|
47
|
+
config: "DaemonConfig | None" = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Initialize SummaryFileGenerator.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
transcript_processor: Processor for JSONL transcript parsing
|
|
54
|
+
summary_file_path: Directory path for session summary files
|
|
55
|
+
logger_instance: Optional logger instance
|
|
56
|
+
llm_service: Optional LLMService for multi-provider support
|
|
57
|
+
config: Optional DaemonConfig instance for feature configuration
|
|
58
|
+
"""
|
|
59
|
+
self._transcript_processor = transcript_processor
|
|
60
|
+
self._summary_file_path = summary_file_path
|
|
61
|
+
self.logger = logger_instance or logging.getLogger(__name__)
|
|
62
|
+
self._llm_service = llm_service
|
|
63
|
+
self._config = config
|
|
64
|
+
|
|
65
|
+
# Initialize LLM provider from llm_service or create default
|
|
66
|
+
self.llm_provider: LLMProvider | None = None
|
|
67
|
+
|
|
68
|
+
if llm_service:
|
|
69
|
+
try:
|
|
70
|
+
self.llm_provider = llm_service.get_default_provider()
|
|
71
|
+
provider_name = (
|
|
72
|
+
getattr(self.llm_provider, "provider_name", "unknown")
|
|
73
|
+
if self.llm_provider
|
|
74
|
+
else "unknown"
|
|
75
|
+
)
|
|
76
|
+
self.logger.debug(f"Using '{provider_name}' provider for SummaryFileGenerator")
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
self.logger.warning(f"LLMService has no providers: {e}")
|
|
79
|
+
|
|
80
|
+
if not self.llm_provider:
|
|
81
|
+
# Fallback to ClaudeLLMProvider
|
|
82
|
+
try:
|
|
83
|
+
from gobby.config.app import load_config
|
|
84
|
+
|
|
85
|
+
config = config or load_config()
|
|
86
|
+
self._config = config
|
|
87
|
+
self.llm_provider = ClaudeLLMProvider(config)
|
|
88
|
+
self.logger.debug("Initialized default ClaudeLLMProvider for SummaryFileGenerator")
|
|
89
|
+
except Exception as e:
|
|
90
|
+
self.logger.error(f"Failed to initialize default LLM provider: {e}")
|
|
91
|
+
|
|
92
|
+
def _get_provider_for_feature(
|
|
93
|
+
self, feature_name: str
|
|
94
|
+
) -> tuple["LLMProvider | None", str | None]:
|
|
95
|
+
"""
|
|
96
|
+
Get LLM provider and prompt for a specific feature.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
feature_name: Feature name (e.g., "session_summary")
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Tuple of (provider, prompt) where prompt is from feature config.
|
|
103
|
+
Returns (None, None) if feature is disabled.
|
|
104
|
+
"""
|
|
105
|
+
config = self._config
|
|
106
|
+
if not config:
|
|
107
|
+
return self.llm_provider, None
|
|
108
|
+
|
|
109
|
+
# Try to get feature-specific config
|
|
110
|
+
try:
|
|
111
|
+
if feature_name == "session_summary":
|
|
112
|
+
feature_config = getattr(config, "session_summary", None)
|
|
113
|
+
else:
|
|
114
|
+
return self.llm_provider, None
|
|
115
|
+
|
|
116
|
+
if not feature_config:
|
|
117
|
+
return self.llm_provider, None
|
|
118
|
+
|
|
119
|
+
# Check if feature is enabled
|
|
120
|
+
if not getattr(feature_config, "enabled", True):
|
|
121
|
+
self.logger.debug(f"Feature '{feature_name}' is disabled in config")
|
|
122
|
+
return None, None
|
|
123
|
+
|
|
124
|
+
# Get provider from LLMService if available
|
|
125
|
+
provider_name = getattr(feature_config, "provider", None)
|
|
126
|
+
prompt = getattr(feature_config, "prompt", None)
|
|
127
|
+
|
|
128
|
+
llm_service = self._llm_service
|
|
129
|
+
if llm_service and provider_name:
|
|
130
|
+
try:
|
|
131
|
+
provider = llm_service.get_provider(provider_name)
|
|
132
|
+
self.logger.debug(f"Using provider '{provider_name}' for {feature_name}")
|
|
133
|
+
return provider, prompt
|
|
134
|
+
except ValueError as e:
|
|
135
|
+
self.logger.warning(
|
|
136
|
+
f"Provider '{provider_name}' not available for {feature_name}: {e}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return self.llm_provider, prompt
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
self.logger.warning(f"Failed to get feature config for {feature_name}: {e}")
|
|
143
|
+
return self.llm_provider, None
|
|
144
|
+
|
|
145
|
+
def generate_session_summary(
|
|
146
|
+
self, session_id: str, input_data: dict[str, Any]
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""
|
|
149
|
+
Generate comprehensive LLM-powered session summary file from JSONL transcript.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
session_id: Internal database UUID (sessions.id), not cli_key
|
|
153
|
+
input_data: Session end input data containing cli_key and transcript_path
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dict with status and file path
|
|
157
|
+
"""
|
|
158
|
+
external_id = None
|
|
159
|
+
try:
|
|
160
|
+
# Check if feature is enabled via config
|
|
161
|
+
config = self._config
|
|
162
|
+
if config and hasattr(config, "session_summary") and config.session_summary:
|
|
163
|
+
if not getattr(config.session_summary, "enabled", True):
|
|
164
|
+
self.logger.info("Session summary file generation disabled in config")
|
|
165
|
+
return {"status": "disabled"}
|
|
166
|
+
# Update path from config if available
|
|
167
|
+
new_path = getattr(config.session_summary, "summary_file_path", None)
|
|
168
|
+
if new_path:
|
|
169
|
+
self._summary_file_path = new_path
|
|
170
|
+
|
|
171
|
+
# Extract external_id from input_data
|
|
172
|
+
external_id = input_data.get("session_id")
|
|
173
|
+
if not external_id:
|
|
174
|
+
self.logger.error(f"No external_id in input_data for session_id={session_id}")
|
|
175
|
+
return {"status": "no_external_id", "session_id": session_id}
|
|
176
|
+
|
|
177
|
+
# Source is hardcoded since all hook calls are from Claude Code
|
|
178
|
+
session_source = "Claude Code"
|
|
179
|
+
|
|
180
|
+
# Get transcript path
|
|
181
|
+
transcript_path = input_data.get("transcript_path")
|
|
182
|
+
if not transcript_path:
|
|
183
|
+
self.logger.warning(f"No transcript path found for session {external_id}")
|
|
184
|
+
return {"status": "no_transcript", "external_id": external_id}
|
|
185
|
+
|
|
186
|
+
# Read JSONL transcript
|
|
187
|
+
transcript_file = Path(transcript_path)
|
|
188
|
+
if not transcript_file.exists():
|
|
189
|
+
self.logger.warning(f"Transcript file not found: {transcript_path}")
|
|
190
|
+
return {"status": "transcript_not_found", "path": transcript_path}
|
|
191
|
+
|
|
192
|
+
# Parse JSONL and extract last 50 turns
|
|
193
|
+
turns = []
|
|
194
|
+
with open(transcript_file) as f:
|
|
195
|
+
for line in f:
|
|
196
|
+
if line.strip():
|
|
197
|
+
turns.append(json.loads(line))
|
|
198
|
+
|
|
199
|
+
# Get turns since last /clear (up to 50 turns)
|
|
200
|
+
last_turns = self._transcript_processor.extract_turns_since_clear(turns, max_turns=50)
|
|
201
|
+
|
|
202
|
+
# Get last two user<>agent message pairs
|
|
203
|
+
last_messages = self._transcript_processor.extract_last_messages(
|
|
204
|
+
last_turns, num_pairs=2
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Extract last TodoWrite tool call
|
|
208
|
+
todowrite_list = self._extract_last_todowrite(last_turns)
|
|
209
|
+
|
|
210
|
+
# Get git status and file changes
|
|
211
|
+
git_status = self._get_git_status()
|
|
212
|
+
file_changes = self._get_file_changes()
|
|
213
|
+
|
|
214
|
+
# Generate summary using LLM
|
|
215
|
+
summary_markdown = self._generate_summary_with_llm(
|
|
216
|
+
last_turns=last_turns,
|
|
217
|
+
last_messages=last_messages,
|
|
218
|
+
git_status=git_status,
|
|
219
|
+
file_changes=file_changes,
|
|
220
|
+
external_id=external_id,
|
|
221
|
+
session_id=session_id,
|
|
222
|
+
session_source=session_source,
|
|
223
|
+
todowrite_list=todowrite_list,
|
|
224
|
+
session_tasks_str=None, # Task integration removed for failover simplicity
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Write summary to file (FAILOVER ONLY)
|
|
228
|
+
file_result = self.write_summary_to_file(session_id, summary_markdown)
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"status": "success",
|
|
232
|
+
"external_id": external_id,
|
|
233
|
+
"file_written": file_result,
|
|
234
|
+
"summary_length": len(summary_markdown),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
self.logger.error(f"Failed to create session summary file: {e}", exc_info=True)
|
|
239
|
+
return {"status": "error", "error": str(e), "external_id": external_id}
|
|
240
|
+
|
|
241
|
+
def write_summary_to_file(self, session_id: str, summary: str) -> str | None:
|
|
242
|
+
"""
|
|
243
|
+
Write session summary to markdown file.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
session_id: Internal database UUID (sessions.id) or external_id
|
|
247
|
+
summary: Markdown summary content
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Path to written file, or None on failure
|
|
251
|
+
"""
|
|
252
|
+
try:
|
|
253
|
+
# Create summary directory from config
|
|
254
|
+
summary_dir = Path(self._summary_file_path).expanduser()
|
|
255
|
+
summary_dir.mkdir(parents=True, exist_ok=True)
|
|
256
|
+
|
|
257
|
+
# Write markdown file with Unix timestamp for chronological sorting
|
|
258
|
+
timestamp = int(time.time())
|
|
259
|
+
summary_file = summary_dir / f"session_{timestamp}_{session_id}.md"
|
|
260
|
+
summary_file.write_text(summary, encoding="utf-8")
|
|
261
|
+
|
|
262
|
+
self.logger.info(f"💾 FAILBACK: Session summary written to: {summary_file}")
|
|
263
|
+
return str(summary_file)
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
self.logger.exception(f"Failed to write summary file: {e}")
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
def _generate_summary_with_llm(
|
|
270
|
+
self,
|
|
271
|
+
last_turns: list[dict[str, Any]],
|
|
272
|
+
last_messages: list[dict[str, Any]],
|
|
273
|
+
git_status: str,
|
|
274
|
+
file_changes: str,
|
|
275
|
+
external_id: str,
|
|
276
|
+
session_id: str | None,
|
|
277
|
+
session_source: str | None,
|
|
278
|
+
todowrite_list: str | None = None,
|
|
279
|
+
session_tasks_str: str | None = None,
|
|
280
|
+
) -> str:
|
|
281
|
+
"""
|
|
282
|
+
Generate session summary using LLM provider.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
last_turns: List of recent transcript turns
|
|
286
|
+
last_messages: List of last user<>agent message pairs
|
|
287
|
+
git_status: Git status output
|
|
288
|
+
file_changes: Formatted file changes
|
|
289
|
+
external_id: Claude Code session key
|
|
290
|
+
session_id: Internal database UUID
|
|
291
|
+
session_source: Session source (e.g., "Claude Code")
|
|
292
|
+
todowrite_list: Optional TodoWrite list markdown
|
|
293
|
+
session_tasks_str: Optional formatted session tasks list
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Formatted markdown summary
|
|
297
|
+
"""
|
|
298
|
+
# Get feature-specific provider and prompt
|
|
299
|
+
provider, prompt = self._get_provider_for_feature("session_summary")
|
|
300
|
+
|
|
301
|
+
if not provider:
|
|
302
|
+
return "Session summary unavailable (LLM provider not initialized)"
|
|
303
|
+
|
|
304
|
+
# Prepare context
|
|
305
|
+
transcript_summary = self._format_turns_for_llm(last_turns)
|
|
306
|
+
|
|
307
|
+
context = {
|
|
308
|
+
"transcript_summary": transcript_summary,
|
|
309
|
+
"last_messages": last_messages,
|
|
310
|
+
"git_status": git_status,
|
|
311
|
+
"file_changes": file_changes,
|
|
312
|
+
"todo_list": f"## Agent's TODO List\n{todowrite_list}" if todowrite_list else "",
|
|
313
|
+
"session_tasks": session_tasks_str,
|
|
314
|
+
"external_id": external_id,
|
|
315
|
+
"session_id": session_id,
|
|
316
|
+
"session_source": session_source,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Validate prompt is available
|
|
320
|
+
if not prompt:
|
|
321
|
+
return (
|
|
322
|
+
"Session summary unavailable: No prompt template configured. "
|
|
323
|
+
"Set 'session_summary.prompt' in ~/.gobby/config.yaml"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
|
|
328
|
+
async def _run_gen() -> str:
|
|
329
|
+
# Ensure provider is narrowed for the closure
|
|
330
|
+
active_provider = provider
|
|
331
|
+
if not active_provider:
|
|
332
|
+
return ""
|
|
333
|
+
result: str = await active_provider.generate_summary(
|
|
334
|
+
context, prompt_template=prompt
|
|
335
|
+
)
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
llm_summary: str = anyio.run(_run_gen)
|
|
339
|
+
|
|
340
|
+
if not llm_summary:
|
|
341
|
+
raise RuntimeError("LLM summary generation failed - no summary produced")
|
|
342
|
+
|
|
343
|
+
# Build header
|
|
344
|
+
timestamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
345
|
+
|
|
346
|
+
if session_id and session_source:
|
|
347
|
+
header = f"# Session Summary (Failover)\nSession ID: {session_id}\n{session_source} ID: {external_id}\nGenerated: {timestamp}\n\n"
|
|
348
|
+
elif session_id:
|
|
349
|
+
header = f"# Session Summary (Failover)\nSession ID: {session_id}\nClaude Code ID: {external_id}\nGenerated: {timestamp}\n\n"
|
|
350
|
+
else:
|
|
351
|
+
header = f"# Session Summary (Failover)\nClaude Code ID: {external_id}\nGenerated: {timestamp}\n\n"
|
|
352
|
+
|
|
353
|
+
final_summary = header + llm_summary
|
|
354
|
+
|
|
355
|
+
# Insert TodoWrite list if it exists
|
|
356
|
+
if todowrite_list:
|
|
357
|
+
todo_section_marker = "## Claude's Todo List"
|
|
358
|
+
if todo_section_marker in final_summary:
|
|
359
|
+
parts = final_summary.split(todo_section_marker)
|
|
360
|
+
if len(parts) == 2:
|
|
361
|
+
next_section_idx = parts[1].find("\n##")
|
|
362
|
+
if next_section_idx != -1:
|
|
363
|
+
after_next = parts[1][next_section_idx:]
|
|
364
|
+
final_summary = (
|
|
365
|
+
f"{parts[0]}{todo_section_marker}\n{todowrite_list}\n{after_next}"
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
final_summary = f"{parts[0]}{todo_section_marker}\n{todowrite_list}"
|
|
369
|
+
else:
|
|
370
|
+
# Fallback: insert before Next Steps
|
|
371
|
+
if "## Next Steps" in final_summary:
|
|
372
|
+
parts = final_summary.split("## Next Steps", 1)
|
|
373
|
+
final_summary = f"{parts[0]}\n## Claude's Todo List\n{todowrite_list}\n\n## Next Steps{parts[1]}"
|
|
374
|
+
else:
|
|
375
|
+
final_summary = (
|
|
376
|
+
f"{final_summary}\n\n## Claude's Todo List\n{todowrite_list}"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return final_summary
|
|
380
|
+
|
|
381
|
+
except Exception as e:
|
|
382
|
+
self.logger.error(f"LLM summary generation failed: {e}", exc_info=True)
|
|
383
|
+
timestamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
384
|
+
|
|
385
|
+
if session_id and session_source:
|
|
386
|
+
error_header = f"# Session Summary (Error)\nSession ID: {session_id}\n{session_source} ID: {external_id}\nGenerated: {timestamp}\n\n"
|
|
387
|
+
elif session_id:
|
|
388
|
+
error_header = f"# Session Summary (Error)\nSession ID: {session_id}\nClaude Code ID: {external_id}\nGenerated: {timestamp}\n\n"
|
|
389
|
+
else:
|
|
390
|
+
error_header = f"# Session Summary (Error)\nClaude Code ID: {external_id}\nGenerated: {timestamp}\n\n"
|
|
391
|
+
|
|
392
|
+
error_summary = error_header + f"Error generating summary: {str(e)}"
|
|
393
|
+
|
|
394
|
+
if todowrite_list:
|
|
395
|
+
error_summary = f"{error_summary}\n\n## Claude's Todo List\n{todowrite_list}"
|
|
396
|
+
|
|
397
|
+
return error_summary
|
|
398
|
+
|
|
399
|
+
def _format_turns_for_llm(self, turns: list[dict[str, Any]]) -> str:
|
|
400
|
+
"""
|
|
401
|
+
Format transcript turns for LLM analysis.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
turns: List of transcript turn dicts
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Formatted string with turn summaries
|
|
408
|
+
"""
|
|
409
|
+
formatted: list[str] = []
|
|
410
|
+
for i, turn in enumerate(turns):
|
|
411
|
+
message = turn.get("message", {})
|
|
412
|
+
role = message.get("role", "unknown")
|
|
413
|
+
content = message.get("content", "")
|
|
414
|
+
|
|
415
|
+
# Assistant messages have content as array of blocks
|
|
416
|
+
if isinstance(content, list):
|
|
417
|
+
text_parts: list[str] = []
|
|
418
|
+
for block in content:
|
|
419
|
+
if isinstance(block, dict):
|
|
420
|
+
if block.get("type") == "text":
|
|
421
|
+
text_parts.append(str(block.get("text", "")))
|
|
422
|
+
elif block.get("type") == "thinking":
|
|
423
|
+
text_parts.append(f"[Thinking: {block.get('thinking', '')}]")
|
|
424
|
+
elif block.get("type") == "tool_use":
|
|
425
|
+
text_parts.append(f"[Tool: {block.get('name', 'unknown')}]")
|
|
426
|
+
content = " ".join(text_parts)
|
|
427
|
+
|
|
428
|
+
formatted.append(f"[Turn {i + 1} - {role}]: {content}")
|
|
429
|
+
|
|
430
|
+
return "\n\n".join(formatted)
|
|
431
|
+
|
|
432
|
+
def _extract_last_todowrite(self, turns: list[dict[str, Any]]) -> str | None:
|
|
433
|
+
"""
|
|
434
|
+
Extract the last TodoWrite tool call's todos list from transcript.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
turns: List of transcript turns
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Formatted markdown string with todo list, or None if not found
|
|
441
|
+
"""
|
|
442
|
+
# Scan turns in reverse to find most recent TodoWrite
|
|
443
|
+
for turn in reversed(turns):
|
|
444
|
+
message = turn.get("message", {})
|
|
445
|
+
content = message.get("content", [])
|
|
446
|
+
|
|
447
|
+
if isinstance(content, list):
|
|
448
|
+
for block in content:
|
|
449
|
+
if isinstance(block, dict) and block.get("type") == "tool_use":
|
|
450
|
+
if block.get("name") == "TodoWrite":
|
|
451
|
+
tool_input = block.get("input", {})
|
|
452
|
+
todos = tool_input.get("todos", [])
|
|
453
|
+
|
|
454
|
+
if not todos:
|
|
455
|
+
return None
|
|
456
|
+
|
|
457
|
+
# Format as markdown checklist
|
|
458
|
+
lines: list[str] = []
|
|
459
|
+
for todo in todos:
|
|
460
|
+
content_text = todo.get("content", "")
|
|
461
|
+
status = todo.get("status", "pending")
|
|
462
|
+
|
|
463
|
+
# Map status to checkbox style
|
|
464
|
+
if status == "completed":
|
|
465
|
+
checkbox = "[x]"
|
|
466
|
+
elif status == "in_progress":
|
|
467
|
+
checkbox = "[>]"
|
|
468
|
+
else:
|
|
469
|
+
checkbox = "[ ]"
|
|
470
|
+
|
|
471
|
+
lines.append(f"- {checkbox} {content_text} ({status})")
|
|
472
|
+
|
|
473
|
+
return "\n".join(lines)
|
|
474
|
+
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
def _get_git_status(self) -> str:
|
|
478
|
+
"""
|
|
479
|
+
Get git status for current directory.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Git status output or error message
|
|
483
|
+
"""
|
|
484
|
+
try:
|
|
485
|
+
result = subprocess.run( # nosec B603 B607 - hardcoded git command
|
|
486
|
+
["git", "status", "--short"],
|
|
487
|
+
capture_output=True,
|
|
488
|
+
text=True,
|
|
489
|
+
timeout=5,
|
|
490
|
+
)
|
|
491
|
+
return result.stdout.strip()
|
|
492
|
+
except Exception:
|
|
493
|
+
return "Not a git repository or git not available"
|
|
494
|
+
|
|
495
|
+
def _get_file_changes(self) -> str:
|
|
496
|
+
"""
|
|
497
|
+
Get detailed file changes from git.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Formatted file changes or error message
|
|
501
|
+
"""
|
|
502
|
+
try:
|
|
503
|
+
# Get changed files with status
|
|
504
|
+
diff_result = subprocess.run( # nosec B603 B607 - hardcoded git command
|
|
505
|
+
["git", "diff", "HEAD", "--name-status"],
|
|
506
|
+
capture_output=True,
|
|
507
|
+
text=True,
|
|
508
|
+
timeout=5,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Get untracked files
|
|
512
|
+
untracked_result = subprocess.run( # nosec B603 B607 - hardcoded git command
|
|
513
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
514
|
+
capture_output=True,
|
|
515
|
+
text=True,
|
|
516
|
+
timeout=5,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Combine results
|
|
520
|
+
changes = []
|
|
521
|
+
if diff_result.stdout.strip():
|
|
522
|
+
changes.append("Modified/Deleted:")
|
|
523
|
+
changes.append(diff_result.stdout.strip())
|
|
524
|
+
|
|
525
|
+
if untracked_result.stdout.strip():
|
|
526
|
+
changes.append("\nUntracked:")
|
|
527
|
+
changes.append(untracked_result.stdout.strip())
|
|
528
|
+
|
|
529
|
+
return "\n".join(changes) if changes else "No changes"
|
|
530
|
+
|
|
531
|
+
except Exception:
|
|
532
|
+
return "Unable to determine file changes"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transcript parsers.
|
|
3
|
+
|
|
4
|
+
Exports transcript parsers for different CLI tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from gobby.sessions.transcripts.base import ParsedMessage, TranscriptParser
|
|
8
|
+
from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
|
|
9
|
+
from gobby.sessions.transcripts.codex import CodexTranscriptParser
|
|
10
|
+
from gobby.sessions.transcripts.gemini import GeminiTranscriptParser
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"TranscriptParser",
|
|
14
|
+
"ParsedMessage",
|
|
15
|
+
"ClaudeTranscriptParser",
|
|
16
|
+
"GeminiTranscriptParser",
|
|
17
|
+
"CodexTranscriptParser",
|
|
18
|
+
"get_parser",
|
|
19
|
+
"PARSER_REGISTRY",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
PARSER_REGISTRY: dict[str, type[TranscriptParser]] = {
|
|
23
|
+
"claude": ClaudeTranscriptParser,
|
|
24
|
+
"gemini": GeminiTranscriptParser,
|
|
25
|
+
"antigravity": GeminiTranscriptParser,
|
|
26
|
+
"codex": CodexTranscriptParser,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_parser(source: str) -> TranscriptParser:
|
|
31
|
+
"""
|
|
32
|
+
Get a transcript parser instance for the given source.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
source: CLI source name (e.g., 'claude', 'gemini')
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
TranscriptParser instance
|
|
39
|
+
"""
|
|
40
|
+
parser_cls = PARSER_REGISTRY.get(source, ClaudeTranscriptParser)
|
|
41
|
+
return parser_cls()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base transcript parser protocol.
|
|
3
|
+
|
|
4
|
+
Defines the interface for CLI-specific transcript parsers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any, Protocol, runtime_checkable
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TokenUsage:
|
|
19
|
+
"""Token usage metrics for a message or session."""
|
|
20
|
+
|
|
21
|
+
input_tokens: int = 0
|
|
22
|
+
output_tokens: int = 0
|
|
23
|
+
cache_creation_tokens: int = 0
|
|
24
|
+
cache_read_tokens: int = 0
|
|
25
|
+
total_cost_usd: float | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ParsedMessage:
|
|
30
|
+
"""Normalized message from any CLI transcript."""
|
|
31
|
+
|
|
32
|
+
index: int
|
|
33
|
+
role: str
|
|
34
|
+
content: str
|
|
35
|
+
content_type: str # text, thinking, tool_use, tool_result
|
|
36
|
+
tool_name: str | None
|
|
37
|
+
tool_input: dict[str, Any] | None
|
|
38
|
+
tool_result: dict[str, Any] | None
|
|
39
|
+
timestamp: datetime
|
|
40
|
+
raw_json: dict[str, Any]
|
|
41
|
+
usage: TokenUsage | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@runtime_checkable
|
|
45
|
+
class TranscriptParser(Protocol):
|
|
46
|
+
"""
|
|
47
|
+
Protocol for transcript parsers.
|
|
48
|
+
|
|
49
|
+
Each CLI tool (Claude Code, Codex, Gemini, Antigravity) has its own
|
|
50
|
+
transcript format. Implementations of this protocol handle parsing
|
|
51
|
+
and extracting conversation data from each format.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def parse_line(self, line: str, index: int) -> ParsedMessage | None:
|
|
55
|
+
"""
|
|
56
|
+
Parse a single line from the transcript JSONL.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
line: Raw JSON line string
|
|
60
|
+
index: Line index (0-based)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
ParsedMessage object or None if line should be skipped
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def parse_lines(self, lines: list[str], start_index: int = 0) -> list[ParsedMessage]:
|
|
68
|
+
"""
|
|
69
|
+
Parse multiple lines from the transcript.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
lines: List of raw JSON line strings
|
|
73
|
+
start_index: Starting line index for first line in list
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
List of ParsedMessage objects
|
|
77
|
+
"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
def extract_last_messages(
|
|
81
|
+
self, turns: list[dict[str, Any]], num_pairs: int = 2
|
|
82
|
+
) -> list[dict[str, Any]]:
|
|
83
|
+
"""
|
|
84
|
+
Extract last N user<>agent message pairs from transcript.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
turns: List of transcript turns
|
|
88
|
+
num_pairs: Number of user/agent message pairs to extract
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of message dicts with "role" and "content" fields
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
def extract_turns_since_clear(
|
|
96
|
+
self, turns: list[dict[str, Any]], max_turns: int = 50
|
|
97
|
+
) -> list[dict[str, Any]]:
|
|
98
|
+
"""
|
|
99
|
+
Extract turns since the most recent session boundary, up to max_turns.
|
|
100
|
+
|
|
101
|
+
What constitutes a "session boundary" varies by CLI:
|
|
102
|
+
- Claude Code: /clear command
|
|
103
|
+
- Codex: New session in history
|
|
104
|
+
- Gemini: Session delimiter
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
turns: List of all transcript turns
|
|
108
|
+
max_turns: Maximum number of turns to extract
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of turns representing the current conversation segment
|
|
112
|
+
"""
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
def is_session_boundary(self, turn: dict[str, Any]) -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Check if a turn represents a session boundary.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
turn: Transcript turn dict
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if turn marks a session boundary
|
|
124
|
+
"""
|
|
125
|
+
...
|