gobby 0.2.7__py3-none-any.whl → 0.2.8__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.
Files changed (80) hide show
  1. gobby/adapters/claude_code.py +96 -35
  2. gobby/adapters/gemini.py +140 -38
  3. gobby/agents/isolation.py +130 -0
  4. gobby/agents/registry.py +11 -0
  5. gobby/agents/session.py +1 -0
  6. gobby/agents/spawn_executor.py +43 -13
  7. gobby/agents/spawners/macos.py +26 -1
  8. gobby/cli/__init__.py +0 -2
  9. gobby/cli/memory.py +185 -0
  10. gobby/clones/git.py +177 -0
  11. gobby/config/skills.py +31 -0
  12. gobby/hooks/event_handlers.py +109 -10
  13. gobby/hooks/hook_manager.py +19 -1
  14. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  15. gobby/mcp_proxy/instructions.py +2 -2
  16. gobby/mcp_proxy/registries.py +21 -4
  17. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  18. gobby/mcp_proxy/tools/agents.py +45 -9
  19. gobby/mcp_proxy/tools/artifacts.py +43 -9
  20. gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
  21. gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
  22. gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
  23. gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
  24. gobby/mcp_proxy/tools/spawn_agent.py +44 -6
  25. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  26. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  27. gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
  28. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  29. gobby/mcp_proxy/tools/workflows.py +84 -34
  30. gobby/mcp_proxy/tools/worktrees.py +32 -7
  31. gobby/memory/extractor.py +15 -1
  32. gobby/runner.py +13 -0
  33. gobby/servers/routes/mcp/hooks.py +50 -3
  34. gobby/servers/websocket.py +57 -1
  35. gobby/sessions/analyzer.py +2 -2
  36. gobby/sessions/manager.py +9 -0
  37. gobby/sessions/transcripts/gemini.py +100 -34
  38. gobby/storage/database.py +9 -2
  39. gobby/storage/memories.py +32 -21
  40. gobby/storage/migrations.py +23 -4
  41. gobby/storage/sessions.py +4 -2
  42. gobby/storage/skills.py +43 -3
  43. gobby/workflows/detection_helpers.py +38 -24
  44. gobby/workflows/enforcement/blocking.py +13 -1
  45. gobby/workflows/engine.py +93 -0
  46. gobby/workflows/evaluator.py +110 -0
  47. gobby/workflows/hooks.py +41 -0
  48. gobby/workflows/memory_actions.py +11 -0
  49. gobby/workflows/safe_evaluator.py +8 -0
  50. gobby/workflows/summary_actions.py +123 -50
  51. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/METADATA +1 -1
  52. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/RECORD +56 -80
  53. gobby/cli/tui.py +0 -34
  54. gobby/tui/__init__.py +0 -5
  55. gobby/tui/api_client.py +0 -278
  56. gobby/tui/app.py +0 -329
  57. gobby/tui/screens/__init__.py +0 -25
  58. gobby/tui/screens/agents.py +0 -333
  59. gobby/tui/screens/chat.py +0 -450
  60. gobby/tui/screens/dashboard.py +0 -377
  61. gobby/tui/screens/memory.py +0 -305
  62. gobby/tui/screens/metrics.py +0 -231
  63. gobby/tui/screens/orchestrator.py +0 -903
  64. gobby/tui/screens/sessions.py +0 -412
  65. gobby/tui/screens/tasks.py +0 -440
  66. gobby/tui/screens/workflows.py +0 -289
  67. gobby/tui/screens/worktrees.py +0 -174
  68. gobby/tui/widgets/__init__.py +0 -21
  69. gobby/tui/widgets/chat.py +0 -210
  70. gobby/tui/widgets/conductor.py +0 -104
  71. gobby/tui/widgets/menu.py +0 -132
  72. gobby/tui/widgets/message_panel.py +0 -160
  73. gobby/tui/widgets/review_gate.py +0 -224
  74. gobby/tui/widgets/task_tree.py +0 -99
  75. gobby/tui/widgets/token_budget.py +0 -166
  76. gobby/tui/ws_client.py +0 -258
  77. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/WHEEL +0 -0
  78. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  79. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  80. {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
@@ -1,231 +0,0 @@
1
- """Metrics screen with tool usage statistics."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- from typing import Any
7
-
8
- from textual.app import ComposeResult
9
- from textual.containers import Container, Horizontal, Vertical
10
- from textual.css.query import NoMatches
11
- from textual.reactive import reactive
12
- from textual.widget import Widget
13
- from textual.widgets import (
14
- Button,
15
- DataTable,
16
- LoadingIndicator,
17
- Static,
18
- )
19
-
20
- from gobby.tui.api_client import GobbyAPIClient
21
- from gobby.tui.ws_client import GobbyWebSocketClient
22
-
23
-
24
- class MetricsSummaryPanel(Widget):
25
- """Panel showing metrics summary."""
26
-
27
- DEFAULT_CSS = """
28
- MetricsSummaryPanel {
29
- height: auto;
30
- padding: 1;
31
- border: round #45475a;
32
- margin: 1;
33
- }
34
-
35
- MetricsSummaryPanel .summary-title {
36
- text-style: bold;
37
- color: #a78bfa;
38
- padding-bottom: 1;
39
- }
40
-
41
- MetricsSummaryPanel .summary-grid {
42
- layout: grid;
43
- grid-size: 4 1;
44
- grid-gutter: 2;
45
- }
46
-
47
- MetricsSummaryPanel .stat-box {
48
- height: 4;
49
- border: round #45475a;
50
- padding: 0 1;
51
- content-align: center middle;
52
- }
53
-
54
- MetricsSummaryPanel .stat-value {
55
- text-style: bold;
56
- color: #06b6d4;
57
- }
58
-
59
- MetricsSummaryPanel .stat-label {
60
- color: #a6adc8;
61
- }
62
- """
63
-
64
- metrics: reactive[dict[str, Any] | None] = reactive(None)
65
-
66
- def compose(self) -> ComposeResult:
67
- yield Static("📊 Summary", classes="summary-title")
68
-
69
- with Horizontal(classes="summary-grid"):
70
- # Total calls
71
- with Vertical(classes="stat-box"):
72
- total = self.metrics.get("total_calls", 0) if self.metrics else 0
73
- yield Static(str(total), classes="stat-value")
74
- yield Static("Total Calls", classes="stat-label")
75
-
76
- # Success rate
77
- with Vertical(classes="stat-box"):
78
- rate = self.metrics.get("success_rate", 0) if self.metrics else 0
79
- yield Static(f"{rate:.1%}", classes="stat-value")
80
- yield Static("Success Rate", classes="stat-label")
81
-
82
- # Avg response time
83
- with Vertical(classes="stat-box"):
84
- avg_time = self.metrics.get("avg_response_ms", 0) if self.metrics else 0
85
- yield Static(f"{avg_time:.0f}ms", classes="stat-value")
86
- yield Static("Avg Response", classes="stat-label")
87
-
88
- # Active servers
89
- with Vertical(classes="stat-box"):
90
- servers = self.metrics.get("active_servers", 0) if self.metrics else 0
91
- yield Static(str(servers), classes="stat-value")
92
- yield Static("Active Servers", classes="stat-label")
93
-
94
- def watch_metrics(self, metrics: dict[str, Any] | None) -> None:
95
- """Recompose when metrics change."""
96
- asyncio.create_task(self.recompose())
97
-
98
-
99
- class MetricsScreen(Widget):
100
- """Metrics screen showing tool usage statistics."""
101
-
102
- DEFAULT_CSS = """
103
- MetricsScreen {
104
- width: 1fr;
105
- height: 1fr;
106
- }
107
-
108
- MetricsScreen .screen-header {
109
- height: auto;
110
- padding: 1;
111
- background: #313244;
112
- }
113
-
114
- MetricsScreen .header-row {
115
- layout: horizontal;
116
- }
117
-
118
- MetricsScreen .panel-title {
119
- text-style: bold;
120
- color: #a78bfa;
121
- width: 1fr;
122
- }
123
-
124
- MetricsScreen .content-area {
125
- height: 1fr;
126
- padding: 1;
127
- }
128
-
129
- MetricsScreen #tools-table {
130
- height: 1fr;
131
- }
132
-
133
- MetricsScreen .loading-container {
134
- width: 1fr;
135
- height: 1fr;
136
- content-align: center middle;
137
- }
138
- """
139
-
140
- loading = reactive(True)
141
- metrics: reactive[dict[str, Any] | None] = reactive(None)
142
- tool_metrics: reactive[list[dict[str, Any]]] = reactive(list)
143
-
144
- def __init__(
145
- self,
146
- api_client: GobbyAPIClient,
147
- ws_client: GobbyWebSocketClient,
148
- **kwargs: Any,
149
- ) -> None:
150
- super().__init__(**kwargs)
151
- self.api_client = api_client
152
- self.ws_client = ws_client
153
-
154
- def compose(self) -> ComposeResult:
155
- with Vertical(classes="screen-header"):
156
- with Horizontal(classes="header-row"):
157
- yield Static("📈 Metrics", classes="panel-title")
158
- yield Button("Refresh", id="btn-refresh")
159
- yield Button("Export", id="btn-export")
160
-
161
- if self.loading:
162
- with Container(classes="loading-container"):
163
- yield LoadingIndicator()
164
- else:
165
- with Vertical(classes="content-area"):
166
- yield MetricsSummaryPanel(id="summary-panel")
167
- yield Static("Tool Usage", classes="panel-title")
168
- yield DataTable(id="tools-table")
169
-
170
- async def on_mount(self) -> None:
171
- """Load data when mounted."""
172
- await self.refresh_data()
173
-
174
- async def refresh_data(self) -> None:
175
- """Refresh metrics data."""
176
- try:
177
- async with GobbyAPIClient(self.api_client.base_url) as client:
178
- result = await client.call_tool(
179
- "gobby-metrics",
180
- "get_metrics",
181
- {},
182
- )
183
- self.metrics = result.get("summary", {})
184
- self.tool_metrics = result.get("tools", [])
185
-
186
- except Exception as e:
187
- self.notify(f"Failed to load metrics: {e}", severity="error")
188
- finally:
189
- self.loading = False
190
- await self.recompose()
191
- await self._update_summary()
192
- await self._setup_table()
193
-
194
- async def _update_summary(self) -> None:
195
- """Update the summary panel."""
196
- try:
197
- panel = self.query_one("#summary-panel", MetricsSummaryPanel)
198
- panel.metrics = self.metrics
199
- except NoMatches:
200
- pass # Widget may not be mounted yet
201
-
202
- async def _setup_table(self) -> None:
203
- """Set up and populate the tools table."""
204
- try:
205
- table = self.query_one("#tools-table", DataTable)
206
- table.clear(columns=True)
207
- table.add_columns("Server", "Tool", "Calls", "Success", "Avg Time")
208
- table.cursor_type = "row"
209
-
210
- for tool in self.tool_metrics:
211
- server = tool.get("server", "unknown")
212
- name = tool.get("name", "unknown")
213
- calls = str(tool.get("calls", 0))
214
- success = f"{tool.get('success_rate', 0):.1%}"
215
- avg_time = f"{tool.get('avg_ms', 0):.0f}ms"
216
-
217
- table.add_row(server, name, calls, success, avg_time)
218
-
219
- except NoMatches:
220
- pass # Table widget may not be mounted yet
221
-
222
- async def on_button_pressed(self, event: Button.Pressed) -> None:
223
- """Handle button presses."""
224
- button_id = event.button.id
225
-
226
- if button_id == "btn-refresh":
227
- self.loading = True
228
- await self.refresh_data()
229
-
230
- elif button_id == "btn-export":
231
- self.notify("Export coming soon", severity="information")