gobby 0.2.8__py3-none-any.whl → 0.2.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +5 -28
- gobby/adapters/codex_impl/adapter.py +38 -43
- gobby/adapters/copilot.py +324 -0
- gobby/adapters/cursor.py +373 -0
- gobby/adapters/gemini.py +2 -26
- gobby/adapters/windsurf.py +359 -0
- gobby/agents/definitions.py +162 -2
- gobby/agents/isolation.py +33 -1
- gobby/agents/pty_reader.py +192 -0
- gobby/agents/registry.py +10 -1
- gobby/agents/runner.py +24 -8
- gobby/agents/sandbox.py +8 -3
- gobby/agents/session.py +4 -0
- gobby/agents/spawn.py +9 -2
- gobby/agents/spawn_executor.py +49 -61
- gobby/agents/spawners/command_builder.py +4 -4
- gobby/app_context.py +64 -0
- gobby/cli/__init__.py +4 -0
- gobby/cli/install.py +259 -4
- gobby/cli/installers/__init__.py +12 -0
- gobby/cli/installers/copilot.py +242 -0
- gobby/cli/installers/cursor.py +244 -0
- gobby/cli/installers/shared.py +3 -0
- gobby/cli/installers/windsurf.py +242 -0
- gobby/cli/pipelines.py +639 -0
- gobby/cli/sessions.py +3 -1
- gobby/cli/skills.py +209 -0
- gobby/cli/tasks/crud.py +6 -5
- gobby/cli/tasks/search.py +1 -1
- gobby/cli/ui.py +116 -0
- gobby/cli/utils.py +5 -17
- gobby/cli/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +23 -2
- gobby/config/tasks.py +4 -0
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +92 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +487 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +27 -3
- gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
- gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
- gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
- gobby/llm/__init__.py +14 -1
- gobby/llm/claude.py +594 -43
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +66 -5
- gobby/mcp_proxy/server.py +6 -2
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/services/tool_filter.py +7 -0
- gobby/mcp_proxy/services/tool_proxy.py +19 -1
- gobby/mcp_proxy/stdio.py +37 -21
- gobby/mcp_proxy/tools/agents.py +7 -0
- gobby/mcp_proxy/tools/artifacts.py +3 -3
- gobby/mcp_proxy/tools/hub.py +30 -1
- gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
- gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
- gobby/mcp_proxy/tools/orchestration/review.py +17 -4
- gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
- gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
- gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
- gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
- gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
- gobby/mcp_proxy/tools/skills/__init__.py +184 -30
- gobby/mcp_proxy/tools/spawn_agent.py +229 -14
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +8 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
- gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
- gobby/mcp_proxy/tools/tasks/_search.py +1 -1
- gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
- gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
- gobby/mcp_proxy/tools/workflows/_import.py +112 -0
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
- gobby/mcp_proxy/tools/workflows/_query.py +226 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/context.py +5 -5
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +131 -16
- gobby/servers/http.py +193 -150
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +56 -0
- gobby/servers/routes/mcp/endpoints/execution.py +33 -32
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +10 -1
- gobby/servers/routes/pipelines.py +227 -0
- gobby/servers/websocket.py +314 -1
- gobby/sessions/analyzer.py +89 -3
- gobby/sessions/manager.py +5 -5
- gobby/sessions/transcripts/__init__.py +3 -0
- gobby/sessions/transcripts/claude.py +5 -0
- gobby/sessions/transcripts/codex.py +5 -0
- gobby/sessions/transcripts/gemini.py +5 -0
- gobby/skills/hubs/__init__.py +25 -0
- gobby/skills/hubs/base.py +234 -0
- gobby/skills/hubs/claude_plugins.py +328 -0
- gobby/skills/hubs/clawdhub.py +289 -0
- gobby/skills/hubs/github_collection.py +465 -0
- gobby/skills/hubs/manager.py +263 -0
- gobby/skills/hubs/skillhub.py +342 -0
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/memories.py +4 -4
- gobby/storage/migrations.py +118 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +48 -8
- gobby/storage/tasks/_aggregates.py +2 -2
- gobby/storage/tasks/_lifecycle.py +4 -4
- gobby/storage/tasks/_models.py +7 -1
- gobby/storage/tasks/_queries.py +3 -3
- gobby/sync/memories.py +4 -3
- gobby/tasks/commits.py +48 -17
- gobby/tasks/external_validator.py +4 -17
- gobby/tasks/validation.py +13 -87
- gobby/tools/summarizer.py +18 -51
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +80 -0
- gobby/workflows/context_actions.py +265 -27
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +96 -0
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +26 -4
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +59 -27
- gobby/workflows/loader.py +567 -30
- gobby/workflows/lobster_compat.py +147 -0
- gobby/workflows/pipeline_executor.py +801 -0
- gobby/workflows/pipeline_state.py +172 -0
- gobby/workflows/pipeline_webhooks.py +206 -0
- gobby/workflows/premature_stop.py +5 -0
- gobby/worktrees/git.py +135 -20
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
- gobby/hooks/event_handlers.py +0 -1008
- gobby/mcp_proxy/tools/workflows.py +0 -1023
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
gobby/servers/websocket.py
CHANGED
|
@@ -14,7 +14,7 @@ import os
|
|
|
14
14
|
from collections.abc import Callable, Coroutine
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from datetime import UTC, datetime
|
|
17
|
-
from typing import Any, Protocol
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
18
18
|
from uuid import uuid4
|
|
19
19
|
|
|
20
20
|
from websockets.asyncio.server import serve
|
|
@@ -25,6 +25,9 @@ from websockets.http11 import Response
|
|
|
25
25
|
from gobby.agents.registry import get_running_agent_registry
|
|
26
26
|
from gobby.mcp_proxy.manager import MCPClientManager
|
|
27
27
|
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from gobby.llm import LLMService
|
|
30
|
+
|
|
28
31
|
logger = logging.getLogger(__name__)
|
|
29
32
|
|
|
30
33
|
|
|
@@ -79,6 +82,7 @@ class WebSocketServer:
|
|
|
79
82
|
mcp_manager: MCPClientManager,
|
|
80
83
|
auth_callback: Callable[[str], Coroutine[Any, Any, str | None]] | None = None,
|
|
81
84
|
stop_registry: Any = None,
|
|
85
|
+
llm_service: "LLMService | None" = None,
|
|
82
86
|
):
|
|
83
87
|
"""
|
|
84
88
|
Initialize WebSocket server.
|
|
@@ -89,15 +93,20 @@ class WebSocketServer:
|
|
|
89
93
|
auth_callback: Optional async function that validates token and returns user_id.
|
|
90
94
|
If None, all connections are accepted (local-first mode).
|
|
91
95
|
stop_registry: Optional StopRegistry for handling stop requests from clients.
|
|
96
|
+
llm_service: Optional LLM service for chat message handling.
|
|
92
97
|
"""
|
|
93
98
|
self.config = config
|
|
94
99
|
self.mcp_manager = mcp_manager
|
|
95
100
|
self.auth_callback = auth_callback
|
|
96
101
|
self.stop_registry = stop_registry
|
|
102
|
+
self.llm_service = llm_service
|
|
97
103
|
|
|
98
104
|
# Connected clients: {websocket: client_metadata}
|
|
99
105
|
self.clients: dict[Any, dict[str, Any]] = {}
|
|
100
106
|
|
|
107
|
+
# Chat conversation history per client (simple in-memory for now)
|
|
108
|
+
self._chat_history: dict[str, list[dict[str, str]]] = {}
|
|
109
|
+
|
|
101
110
|
# Server instance (set when started)
|
|
102
111
|
self._server: Any = None
|
|
103
112
|
self._serve_task: asyncio.Task[None] | None = None
|
|
@@ -225,6 +234,7 @@ class WebSocketServer:
|
|
|
225
234
|
finally:
|
|
226
235
|
# Always cleanup client state
|
|
227
236
|
self.clients.pop(websocket, None)
|
|
237
|
+
self._chat_history.pop(client_id, None)
|
|
228
238
|
logger.debug(f"Client {client_id} cleaned up. Remaining clients: {len(self.clients)}")
|
|
229
239
|
|
|
230
240
|
async def _handle_message(self, websocket: Any, message: str) -> None:
|
|
@@ -261,6 +271,9 @@ class WebSocketServer:
|
|
|
261
271
|
elif msg_type == "terminal_input":
|
|
262
272
|
await self._handle_terminal_input(websocket, data)
|
|
263
273
|
|
|
274
|
+
elif msg_type == "chat_message":
|
|
275
|
+
await self._handle_chat_message(websocket, data)
|
|
276
|
+
|
|
264
277
|
else:
|
|
265
278
|
logger.warning(f"Unknown message type: {msg_type}")
|
|
266
279
|
await self._send_error(websocket, f"Unknown message type: {msg_type}")
|
|
@@ -557,6 +570,215 @@ class WebSocketServer:
|
|
|
557
570
|
except OSError as e:
|
|
558
571
|
logger.warning(f"Failed to write to agent {run_id} PTY: {e}")
|
|
559
572
|
|
|
573
|
+
async def _handle_chat_message(self, websocket: Any, data: dict[str, Any]) -> None:
|
|
574
|
+
"""
|
|
575
|
+
Handle chat_message and stream LLM response with MCP tool support.
|
|
576
|
+
|
|
577
|
+
Message format:
|
|
578
|
+
{
|
|
579
|
+
"type": "chat_message",
|
|
580
|
+
"content": "user message",
|
|
581
|
+
"message_id": "client-generated-id",
|
|
582
|
+
"use_tools": true // optional, enables MCP tools
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
Response format (streamed):
|
|
586
|
+
{
|
|
587
|
+
"type": "chat_stream",
|
|
588
|
+
"message_id": "assistant-uuid",
|
|
589
|
+
"content": "chunk of text",
|
|
590
|
+
"done": false
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
Tool status format:
|
|
594
|
+
{
|
|
595
|
+
"type": "tool_status",
|
|
596
|
+
"message_id": "assistant-uuid",
|
|
597
|
+
"tool_call_id": "unique-id",
|
|
598
|
+
"status": "calling" | "completed" | "error",
|
|
599
|
+
"tool_name": "mcp__gobby-tasks__create_task",
|
|
600
|
+
"server_name": "gobby-tasks",
|
|
601
|
+
"arguments": {...},
|
|
602
|
+
"result": {...}, // when completed
|
|
603
|
+
"error": "..." // when error
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
websocket: Client WebSocket connection
|
|
608
|
+
data: Parsed chat message
|
|
609
|
+
"""
|
|
610
|
+
content = data.get("content")
|
|
611
|
+
user_message_id = data.get("message_id")
|
|
612
|
+
use_tools = data.get("use_tools", True) # Default to using tools
|
|
613
|
+
model = data.get("model") # Optional model override
|
|
614
|
+
|
|
615
|
+
if not content or not isinstance(content, str):
|
|
616
|
+
await self._send_error(websocket, "Missing or invalid 'content' field")
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
if not self.llm_service:
|
|
620
|
+
await websocket.send(
|
|
621
|
+
json.dumps(
|
|
622
|
+
{
|
|
623
|
+
"type": "chat_error",
|
|
624
|
+
"message_id": user_message_id,
|
|
625
|
+
"error": "LLM service not configured",
|
|
626
|
+
}
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
return
|
|
630
|
+
|
|
631
|
+
# Get or create conversation history for this client
|
|
632
|
+
client_info = self.clients.get(websocket)
|
|
633
|
+
if not client_info:
|
|
634
|
+
logger.warning("Chat message from unregistered client")
|
|
635
|
+
return
|
|
636
|
+
client_id = client_info["id"]
|
|
637
|
+
|
|
638
|
+
if client_id not in self._chat_history:
|
|
639
|
+
self._chat_history[client_id] = []
|
|
640
|
+
|
|
641
|
+
history = self._chat_history[client_id]
|
|
642
|
+
|
|
643
|
+
# Add user message to history
|
|
644
|
+
history.append({"role": "user", "content": content})
|
|
645
|
+
|
|
646
|
+
# Generate assistant message ID
|
|
647
|
+
assistant_message_id = f"assistant-{uuid4().hex[:12]}"
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
# Build messages for LLM
|
|
651
|
+
system_prompt = (
|
|
652
|
+
"You are Gobby, a helpful AI assistant with access to tools. "
|
|
653
|
+
"You help users with coding, development tasks, and general questions. "
|
|
654
|
+
"You can use tools to manage tasks, store memories, and access session info. "
|
|
655
|
+
"Be concise and helpful."
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
messages = [{"role": "system", "content": system_prompt}] + history[
|
|
659
|
+
-20:
|
|
660
|
+
] # Last 20 messages
|
|
661
|
+
|
|
662
|
+
full_response = ""
|
|
663
|
+
tool_calls_count = 0
|
|
664
|
+
|
|
665
|
+
if use_tools:
|
|
666
|
+
# Stream with MCP tools
|
|
667
|
+
from gobby.llm.claude import (
|
|
668
|
+
DoneEvent,
|
|
669
|
+
TextChunk,
|
|
670
|
+
ToolCallEvent,
|
|
671
|
+
ToolResultEvent,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# Default allowed tools - gobby MCP server from .mcp.json
|
|
675
|
+
# The server name is "gobby" which exposes all gobby tools
|
|
676
|
+
allowed_tools = [
|
|
677
|
+
"mcp__gobby__*",
|
|
678
|
+
]
|
|
679
|
+
|
|
680
|
+
async for event in self.llm_service.stream_chat_with_tools(
|
|
681
|
+
messages, allowed_tools, model=model
|
|
682
|
+
):
|
|
683
|
+
if isinstance(event, TextChunk):
|
|
684
|
+
full_response += event.content
|
|
685
|
+
await websocket.send(
|
|
686
|
+
json.dumps(
|
|
687
|
+
{
|
|
688
|
+
"type": "chat_stream",
|
|
689
|
+
"message_id": assistant_message_id,
|
|
690
|
+
"content": event.content,
|
|
691
|
+
"done": False,
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
elif isinstance(event, ToolCallEvent):
|
|
696
|
+
await websocket.send(
|
|
697
|
+
json.dumps(
|
|
698
|
+
{
|
|
699
|
+
"type": "tool_status",
|
|
700
|
+
"message_id": assistant_message_id,
|
|
701
|
+
"tool_call_id": event.tool_call_id,
|
|
702
|
+
"status": "calling",
|
|
703
|
+
"tool_name": event.tool_name,
|
|
704
|
+
"server_name": event.server_name,
|
|
705
|
+
"arguments": event.arguments,
|
|
706
|
+
}
|
|
707
|
+
)
|
|
708
|
+
)
|
|
709
|
+
elif isinstance(event, ToolResultEvent):
|
|
710
|
+
await websocket.send(
|
|
711
|
+
json.dumps(
|
|
712
|
+
{
|
|
713
|
+
"type": "tool_status",
|
|
714
|
+
"message_id": assistant_message_id,
|
|
715
|
+
"tool_call_id": event.tool_call_id,
|
|
716
|
+
"status": "completed" if event.success else "error",
|
|
717
|
+
"result": event.result,
|
|
718
|
+
"error": event.error,
|
|
719
|
+
}
|
|
720
|
+
)
|
|
721
|
+
)
|
|
722
|
+
elif isinstance(event, DoneEvent):
|
|
723
|
+
tool_calls_count = event.tool_calls_count
|
|
724
|
+
# Send final done message
|
|
725
|
+
await websocket.send(
|
|
726
|
+
json.dumps(
|
|
727
|
+
{
|
|
728
|
+
"type": "chat_stream",
|
|
729
|
+
"message_id": assistant_message_id,
|
|
730
|
+
"content": "",
|
|
731
|
+
"done": True,
|
|
732
|
+
"tool_calls_count": tool_calls_count,
|
|
733
|
+
}
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
else:
|
|
737
|
+
# Stream without tools (original behavior)
|
|
738
|
+
async for chunk in self.llm_service.stream_chat(messages):
|
|
739
|
+
full_response += chunk
|
|
740
|
+
await websocket.send(
|
|
741
|
+
json.dumps(
|
|
742
|
+
{
|
|
743
|
+
"type": "chat_stream",
|
|
744
|
+
"message_id": assistant_message_id,
|
|
745
|
+
"content": chunk,
|
|
746
|
+
"done": False,
|
|
747
|
+
}
|
|
748
|
+
)
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# Send final done message
|
|
752
|
+
await websocket.send(
|
|
753
|
+
json.dumps(
|
|
754
|
+
{
|
|
755
|
+
"type": "chat_stream",
|
|
756
|
+
"message_id": assistant_message_id,
|
|
757
|
+
"content": "",
|
|
758
|
+
"done": True,
|
|
759
|
+
}
|
|
760
|
+
)
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
# Add assistant response to history
|
|
764
|
+
history.append({"role": "assistant", "content": full_response})
|
|
765
|
+
|
|
766
|
+
# Trim history if too long
|
|
767
|
+
if len(history) > 100:
|
|
768
|
+
self._chat_history[client_id] = history[-50:]
|
|
769
|
+
|
|
770
|
+
except Exception:
|
|
771
|
+
logger.exception(f"Chat error for client {client_id}")
|
|
772
|
+
await websocket.send(
|
|
773
|
+
json.dumps(
|
|
774
|
+
{
|
|
775
|
+
"type": "chat_error",
|
|
776
|
+
"message_id": assistant_message_id,
|
|
777
|
+
"error": "An internal error occurred",
|
|
778
|
+
}
|
|
779
|
+
)
|
|
780
|
+
)
|
|
781
|
+
|
|
560
782
|
async def broadcast(self, message: dict[str, Any]) -> None:
|
|
561
783
|
"""
|
|
562
784
|
Broadcast message to all connected clients.
|
|
@@ -724,6 +946,97 @@ class WebSocketServer:
|
|
|
724
946
|
|
|
725
947
|
await self.broadcast(message)
|
|
726
948
|
|
|
949
|
+
async def broadcast_pipeline_event(
|
|
950
|
+
self,
|
|
951
|
+
event: str,
|
|
952
|
+
execution_id: str,
|
|
953
|
+
**kwargs: Any,
|
|
954
|
+
) -> None:
|
|
955
|
+
"""
|
|
956
|
+
Broadcast pipeline execution event to subscribed clients.
|
|
957
|
+
|
|
958
|
+
Used for real-time pipeline execution updates:
|
|
959
|
+
- pipeline_started: Execution began
|
|
960
|
+
- pipeline_completed: Execution finished successfully
|
|
961
|
+
- pipeline_failed: Execution failed with error
|
|
962
|
+
- step_started: A step began executing
|
|
963
|
+
- step_completed: A step finished successfully
|
|
964
|
+
- step_failed: A step failed with error
|
|
965
|
+
- step_output: Streaming output from a step
|
|
966
|
+
- approval_required: Step is waiting for approval
|
|
967
|
+
|
|
968
|
+
Args:
|
|
969
|
+
event: Event type
|
|
970
|
+
execution_id: Pipeline execution ID
|
|
971
|
+
**kwargs: Additional event data (step_id, output, error, etc.)
|
|
972
|
+
"""
|
|
973
|
+
if not self.clients:
|
|
974
|
+
return # No clients connected
|
|
975
|
+
|
|
976
|
+
message = {
|
|
977
|
+
"type": "pipeline_event",
|
|
978
|
+
"event": event,
|
|
979
|
+
"execution_id": execution_id,
|
|
980
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
981
|
+
**kwargs,
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
message_str = json.dumps(message)
|
|
985
|
+
|
|
986
|
+
for websocket in list(self.clients.keys()):
|
|
987
|
+
try:
|
|
988
|
+
# Only send to clients subscribed to pipeline_event or *
|
|
989
|
+
subs = getattr(websocket, "subscriptions", None)
|
|
990
|
+
if subs is not None:
|
|
991
|
+
if "pipeline_event" not in subs and "*" not in subs:
|
|
992
|
+
continue
|
|
993
|
+
|
|
994
|
+
await websocket.send(message_str)
|
|
995
|
+
except ConnectionClosed:
|
|
996
|
+
pass
|
|
997
|
+
except Exception as e:
|
|
998
|
+
logger.warning(f"Pipeline event broadcast failed: {e}")
|
|
999
|
+
|
|
1000
|
+
async def broadcast_terminal_output(
|
|
1001
|
+
self,
|
|
1002
|
+
run_id: str,
|
|
1003
|
+
data: str,
|
|
1004
|
+
) -> None:
|
|
1005
|
+
"""
|
|
1006
|
+
Broadcast terminal output to subscribed clients.
|
|
1007
|
+
|
|
1008
|
+
Used for streaming PTY output from embedded agents to web terminals.
|
|
1009
|
+
|
|
1010
|
+
Args:
|
|
1011
|
+
run_id: Agent run ID
|
|
1012
|
+
data: Raw terminal output data
|
|
1013
|
+
"""
|
|
1014
|
+
if not self.clients:
|
|
1015
|
+
return # No clients connected
|
|
1016
|
+
|
|
1017
|
+
message = {
|
|
1018
|
+
"type": "terminal_output",
|
|
1019
|
+
"run_id": run_id,
|
|
1020
|
+
"data": data,
|
|
1021
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
message_str = json.dumps(message)
|
|
1025
|
+
|
|
1026
|
+
for websocket in list(self.clients.keys()):
|
|
1027
|
+
try:
|
|
1028
|
+
# Only send to clients subscribed to terminal_output or *
|
|
1029
|
+
subs = getattr(websocket, "subscriptions", None)
|
|
1030
|
+
if subs is not None:
|
|
1031
|
+
if "terminal_output" not in subs and "*" not in subs:
|
|
1032
|
+
continue
|
|
1033
|
+
|
|
1034
|
+
await websocket.send(message_str)
|
|
1035
|
+
except ConnectionClosed:
|
|
1036
|
+
pass
|
|
1037
|
+
except Exception as e:
|
|
1038
|
+
logger.warning(f"Terminal broadcast failed: {e}")
|
|
1039
|
+
|
|
727
1040
|
async def start(self) -> None:
|
|
728
1041
|
"""
|
|
729
1042
|
Start WebSocket server.
|
gobby/sessions/analyzer.py
CHANGED
|
@@ -32,8 +32,8 @@ class HandoffContext:
|
|
|
32
32
|
key_decisions: list[str] | None = None
|
|
33
33
|
active_worktree: dict[str, Any] | None = None
|
|
34
34
|
"""Worktree context if session is operating in a worktree."""
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
# Note: active_skills field removed - redundant with _build_skill_injection_context()
|
|
36
|
+
# which already handles skill restoration on session start
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
class TranscriptAnalyzer:
|
|
@@ -226,10 +226,46 @@ class TranscriptAnalyzer:
|
|
|
226
226
|
tool_name = block.get("name", "unknown")
|
|
227
227
|
tool_input = block.get("input", {})
|
|
228
228
|
|
|
229
|
-
# MCP tool calls - show server.tool
|
|
229
|
+
# MCP tool calls - show server.tool with details for gobby-tasks
|
|
230
230
|
if tool_name in ("mcp__gobby__call_tool", "mcp_call_tool"):
|
|
231
231
|
server = tool_input.get("server_name", "unknown")
|
|
232
232
|
tool = tool_input.get("tool_name", "unknown")
|
|
233
|
+
args = tool_input.get("arguments", {})
|
|
234
|
+
|
|
235
|
+
# Enhanced formatting for gobby-tasks operations
|
|
236
|
+
if server == "gobby-tasks":
|
|
237
|
+
if tool == "create_task":
|
|
238
|
+
title = args.get("title", "Untitled")
|
|
239
|
+
parent = args.get("parent_task_id", "")
|
|
240
|
+
if parent:
|
|
241
|
+
return f"Created task: {title} (parent: {parent})"
|
|
242
|
+
return f"Created task: {title}"
|
|
243
|
+
elif tool == "update_task":
|
|
244
|
+
task_id = args.get("task_id", "?")
|
|
245
|
+
status = args.get("status")
|
|
246
|
+
if status:
|
|
247
|
+
return f"Updated task {task_id}: status → {status}"
|
|
248
|
+
return f"Updated task {task_id}"
|
|
249
|
+
elif tool == "close_task":
|
|
250
|
+
task_id = args.get("task_id", "?")
|
|
251
|
+
reason = args.get("reason", "")
|
|
252
|
+
if reason:
|
|
253
|
+
# Truncate long reasons
|
|
254
|
+
if len(reason) > 40:
|
|
255
|
+
reason = reason[:37] + "..."
|
|
256
|
+
return f"Closed task {task_id}: {reason}"
|
|
257
|
+
return f"Closed task {task_id}"
|
|
258
|
+
elif tool == "claim_task":
|
|
259
|
+
task_id = args.get("task_id", "?")
|
|
260
|
+
return f"Claimed task {task_id}"
|
|
261
|
+
elif tool == "get_task":
|
|
262
|
+
task_id = args.get("task_id", "?")
|
|
263
|
+
return f"Fetched task {task_id}"
|
|
264
|
+
|
|
265
|
+
# Generic MCP call formatting - extract meaningful context from args
|
|
266
|
+
context = self._extract_mcp_context(args)
|
|
267
|
+
if context:
|
|
268
|
+
return f"{server}.{tool}: {context}"
|
|
233
269
|
return f"Called {server}.{tool}"
|
|
234
270
|
|
|
235
271
|
# Bash - show the command (truncated)
|
|
@@ -287,6 +323,56 @@ class TranscriptAnalyzer:
|
|
|
287
323
|
# Default - just show the tool name
|
|
288
324
|
return f"Called {tool_name}"
|
|
289
325
|
|
|
326
|
+
def _extract_mcp_context(self, args: dict[str, Any]) -> str:
|
|
327
|
+
"""
|
|
328
|
+
Extract meaningful context from MCP tool arguments.
|
|
329
|
+
|
|
330
|
+
Looks for common argument patterns and returns the most relevant value
|
|
331
|
+
to describe what the tool call is doing.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
args: Tool arguments dict
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Extracted context string (truncated to 100 chars) or empty string
|
|
338
|
+
"""
|
|
339
|
+
if not args:
|
|
340
|
+
return ""
|
|
341
|
+
|
|
342
|
+
# Priority order for extracting context
|
|
343
|
+
# 1. Search/query related - what are we looking for?
|
|
344
|
+
for key in ("query", "search", "pattern", "topic"):
|
|
345
|
+
if key in args and args[key]:
|
|
346
|
+
return self._truncate(str(args[key]), 100)
|
|
347
|
+
|
|
348
|
+
# 2. Identity/naming - what entity are we working with?
|
|
349
|
+
for key in ("title", "name", "task_id", "id", "ref"):
|
|
350
|
+
if key in args and args[key]:
|
|
351
|
+
return self._truncate(str(args[key]), 100)
|
|
352
|
+
|
|
353
|
+
# 3. Resource paths - what file/resource?
|
|
354
|
+
for key in ("path", "file_path", "uri", "url", "file"):
|
|
355
|
+
if key in args and args[key]:
|
|
356
|
+
return self._truncate(str(args[key]), 100)
|
|
357
|
+
|
|
358
|
+
# 4. Descriptive content - why/what?
|
|
359
|
+
for key in ("description", "reason", "message", "content"):
|
|
360
|
+
if key in args and args[key]:
|
|
361
|
+
return self._truncate(str(args[key]), 100)
|
|
362
|
+
|
|
363
|
+
# 5. Fallback: first non-empty string value
|
|
364
|
+
for key, value in args.items():
|
|
365
|
+
if isinstance(value, str) and value and key not in ("session_id", "server_name"):
|
|
366
|
+
return self._truncate(value, 100)
|
|
367
|
+
|
|
368
|
+
return ""
|
|
369
|
+
|
|
370
|
+
def _truncate(self, text: str, max_len: int) -> str:
|
|
371
|
+
"""Truncate text to max_len, adding ellipsis if needed."""
|
|
372
|
+
if len(text) <= max_len:
|
|
373
|
+
return text
|
|
374
|
+
return text[: max_len - 3] + "..."
|
|
375
|
+
|
|
290
376
|
def _extract_todowrite(self, turns: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
291
377
|
"""
|
|
292
378
|
Extract the most recent TodoWrite state from transcript.
|
gobby/sessions/manager.py
CHANGED
|
@@ -91,7 +91,7 @@ class SessionManager:
|
|
|
91
91
|
Args:
|
|
92
92
|
external_id: External session identifier (e.g., Claude Code session ID)
|
|
93
93
|
machine_id: Machine identifier
|
|
94
|
-
source: CLI source identifier (e.g., "claude", "gemini", "codex") - REQUIRED
|
|
94
|
+
source: CLI source identifier (e.g., "claude", "gemini", "codex", "cursor", "windsurf", "copilot") - REQUIRED
|
|
95
95
|
project_id: Project ID (required - sessions must belong to a project)
|
|
96
96
|
parent_session_id: Optional parent session ID for handoff
|
|
97
97
|
jsonl_path: Optional path to session transcript JSONL file
|
|
@@ -179,7 +179,7 @@ class SessionManager:
|
|
|
179
179
|
|
|
180
180
|
Args:
|
|
181
181
|
machine_id: Machine identifier
|
|
182
|
-
source: CLI source identifier (e.g., "claude", "gemini", "codex") - REQUIRED
|
|
182
|
+
source: CLI source identifier (e.g., "claude", "gemini", "codex", "cursor", "windsurf", "copilot") - REQUIRED
|
|
183
183
|
project_id: Project ID (required for matching)
|
|
184
184
|
max_attempts: Maximum polling attempts (1 per second)
|
|
185
185
|
|
|
@@ -274,7 +274,7 @@ class SessionManager:
|
|
|
274
274
|
|
|
275
275
|
Args:
|
|
276
276
|
external_id: External session identifier
|
|
277
|
-
source: CLI source identifier (e.g., "claude", "gemini", "codex")
|
|
277
|
+
source: CLI source identifier (e.g., "claude", "gemini", "codex", "cursor", "windsurf", "copilot")
|
|
278
278
|
machine_id: Machine identifier
|
|
279
279
|
project_id: Project identifier
|
|
280
280
|
|
|
@@ -343,7 +343,7 @@ class SessionManager:
|
|
|
343
343
|
|
|
344
344
|
Args:
|
|
345
345
|
external_id: External session identifier
|
|
346
|
-
source: CLI source identifier (e.g., "claude", "gemini", "codex")
|
|
346
|
+
source: CLI source identifier (e.g., "claude", "gemini", "codex", "cursor", "windsurf", "copilot")
|
|
347
347
|
|
|
348
348
|
Returns:
|
|
349
349
|
session_id or None if not cached
|
|
@@ -357,7 +357,7 @@ class SessionManager:
|
|
|
357
357
|
|
|
358
358
|
Args:
|
|
359
359
|
external_id: External session identifier
|
|
360
|
-
source: CLI source identifier (e.g., "claude", "gemini", "codex")
|
|
360
|
+
source: CLI source identifier (e.g., "claude", "gemini", "codex", "cursor", "windsurf", "copilot")
|
|
361
361
|
session_id: Database session ID
|
|
362
362
|
"""
|
|
363
363
|
with self._session_mapping_lock:
|
|
@@ -24,6 +24,9 @@ PARSER_REGISTRY: dict[str, type[TranscriptParser]] = {
|
|
|
24
24
|
"gemini": GeminiTranscriptParser,
|
|
25
25
|
"antigravity": GeminiTranscriptParser,
|
|
26
26
|
"codex": CodexTranscriptParser,
|
|
27
|
+
"cursor": ClaudeTranscriptParser,
|
|
28
|
+
"windsurf": ClaudeTranscriptParser,
|
|
29
|
+
"copilot": ClaudeTranscriptParser,
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
|
|
@@ -289,6 +289,11 @@ class ClaudeTranscriptParser:
|
|
|
289
289
|
self.logger.warning(f"Invalid JSON at line {index}")
|
|
290
290
|
return None
|
|
291
291
|
|
|
292
|
+
# Ensure data is a dict (JSON could be a string, number, etc.)
|
|
293
|
+
if not isinstance(data, dict):
|
|
294
|
+
self.logger.debug(f"Skipping non-object JSON at line {index}")
|
|
295
|
+
return None
|
|
296
|
+
|
|
292
297
|
# Extract basic fields
|
|
293
298
|
msg_type = data.get("type", "unknown")
|
|
294
299
|
timestamp_str = data.get("timestamp") or datetime.now(UTC).isoformat()
|
|
@@ -60,6 +60,11 @@ class CodexTranscriptParser:
|
|
|
60
60
|
self.logger.warning(f"Invalid JSON at line {index}")
|
|
61
61
|
return None
|
|
62
62
|
|
|
63
|
+
# Ensure data is a dict (JSON could be a string, number, etc.)
|
|
64
|
+
if not isinstance(data, dict):
|
|
65
|
+
self.logger.debug(f"Skipping non-object JSON at line {index}")
|
|
66
|
+
return None
|
|
67
|
+
|
|
63
68
|
timestamp = datetime.now(UTC)
|
|
64
69
|
if "timestamp" in data:
|
|
65
70
|
try:
|
|
@@ -116,6 +116,11 @@ class GeminiTranscriptParser:
|
|
|
116
116
|
self.logger.warning(f"Invalid JSON at line {index}")
|
|
117
117
|
return None
|
|
118
118
|
|
|
119
|
+
# Ensure data is a dict (JSON could be a string, number, etc.)
|
|
120
|
+
if not isinstance(data, dict):
|
|
121
|
+
self.logger.debug(f"Skipping non-object JSON at line {index}")
|
|
122
|
+
return None
|
|
123
|
+
|
|
119
124
|
# Extract timestamp
|
|
120
125
|
timestamp_str = data.get("timestamp") or datetime.now(UTC).isoformat()
|
|
121
126
|
try:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Skill hub providers for searching and installing skills from registries."""
|
|
2
|
+
|
|
3
|
+
from gobby.skills.hubs.base import (
|
|
4
|
+
DownloadResult,
|
|
5
|
+
HubProvider,
|
|
6
|
+
HubSkillDetails,
|
|
7
|
+
HubSkillInfo,
|
|
8
|
+
)
|
|
9
|
+
from gobby.skills.hubs.claude_plugins import ClaudePluginsProvider
|
|
10
|
+
from gobby.skills.hubs.clawdhub import ClawdHubProvider
|
|
11
|
+
from gobby.skills.hubs.github_collection import GitHubCollectionProvider
|
|
12
|
+
from gobby.skills.hubs.manager import HubManager
|
|
13
|
+
from gobby.skills.hubs.skillhub import SkillHubProvider
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ClaudePluginsProvider",
|
|
17
|
+
"ClawdHubProvider",
|
|
18
|
+
"DownloadResult",
|
|
19
|
+
"GitHubCollectionProvider",
|
|
20
|
+
"HubManager",
|
|
21
|
+
"HubProvider",
|
|
22
|
+
"HubSkillDetails",
|
|
23
|
+
"HubSkillInfo",
|
|
24
|
+
"SkillHubProvider",
|
|
25
|
+
]
|