gobby 0.2.6__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 (198) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +96 -35
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/adapters/gemini.py +140 -38
  10. gobby/agents/definitions.py +11 -1
  11. gobby/agents/isolation.py +525 -0
  12. gobby/agents/registry.py +11 -0
  13. gobby/agents/sandbox.py +261 -0
  14. gobby/agents/session.py +1 -0
  15. gobby/agents/spawn.py +42 -287
  16. gobby/agents/spawn_executor.py +415 -0
  17. gobby/agents/spawners/__init__.py +24 -0
  18. gobby/agents/spawners/command_builder.py +189 -0
  19. gobby/agents/spawners/embedded.py +21 -2
  20. gobby/agents/spawners/headless.py +21 -2
  21. gobby/agents/spawners/macos.py +26 -1
  22. gobby/agents/spawners/prompt_manager.py +125 -0
  23. gobby/cli/__init__.py +0 -2
  24. gobby/cli/install.py +4 -4
  25. gobby/cli/installers/claude.py +6 -0
  26. gobby/cli/installers/gemini.py +6 -0
  27. gobby/cli/installers/shared.py +103 -4
  28. gobby/cli/memory.py +185 -0
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/utils.py +9 -2
  31. gobby/clones/git.py +177 -0
  32. gobby/config/__init__.py +12 -97
  33. gobby/config/app.py +10 -94
  34. gobby/config/extensions.py +2 -2
  35. gobby/config/features.py +7 -130
  36. gobby/config/skills.py +31 -0
  37. gobby/config/tasks.py +4 -28
  38. gobby/hooks/__init__.py +0 -13
  39. gobby/hooks/event_handlers.py +150 -8
  40. gobby/hooks/hook_manager.py +21 -3
  41. gobby/hooks/plugins.py +1 -1
  42. gobby/hooks/webhooks.py +1 -1
  43. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  44. gobby/llm/resolver.py +3 -2
  45. gobby/mcp_proxy/importer.py +62 -4
  46. gobby/mcp_proxy/instructions.py +4 -2
  47. gobby/mcp_proxy/registries.py +22 -8
  48. gobby/mcp_proxy/services/recommendation.py +43 -11
  49. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  50. gobby/mcp_proxy/tools/agents.py +76 -740
  51. gobby/mcp_proxy/tools/artifacts.py +43 -9
  52. gobby/mcp_proxy/tools/clones.py +0 -385
  53. gobby/mcp_proxy/tools/memory.py +2 -2
  54. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  55. gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
  56. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  57. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  58. gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
  59. gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
  60. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  61. gobby/mcp_proxy/tools/spawn_agent.py +455 -0
  62. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  63. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  64. gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
  65. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  66. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  67. gobby/mcp_proxy/tools/workflows.py +84 -34
  68. gobby/mcp_proxy/tools/worktrees.py +32 -350
  69. gobby/memory/extractor.py +15 -1
  70. gobby/memory/ingestion/__init__.py +5 -0
  71. gobby/memory/ingestion/multimodal.py +221 -0
  72. gobby/memory/manager.py +62 -283
  73. gobby/memory/search/__init__.py +10 -0
  74. gobby/memory/search/coordinator.py +248 -0
  75. gobby/memory/services/__init__.py +5 -0
  76. gobby/memory/services/crossref.py +142 -0
  77. gobby/prompts/loader.py +5 -2
  78. gobby/runner.py +13 -0
  79. gobby/servers/http.py +1 -4
  80. gobby/servers/routes/admin.py +14 -0
  81. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  82. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  83. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  84. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  85. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  86. gobby/servers/routes/mcp/hooks.py +51 -4
  87. gobby/servers/routes/mcp/tools.py +48 -1506
  88. gobby/servers/websocket.py +57 -1
  89. gobby/sessions/analyzer.py +2 -2
  90. gobby/sessions/lifecycle.py +1 -1
  91. gobby/sessions/manager.py +9 -0
  92. gobby/sessions/processor.py +10 -0
  93. gobby/sessions/transcripts/base.py +1 -0
  94. gobby/sessions/transcripts/claude.py +15 -5
  95. gobby/sessions/transcripts/gemini.py +100 -34
  96. gobby/skills/parser.py +30 -2
  97. gobby/storage/database.py +9 -2
  98. gobby/storage/memories.py +32 -21
  99. gobby/storage/migrations.py +174 -368
  100. gobby/storage/sessions.py +45 -7
  101. gobby/storage/skills.py +80 -7
  102. gobby/storage/tasks/_lifecycle.py +18 -3
  103. gobby/sync/memories.py +1 -1
  104. gobby/tasks/external_validator.py +1 -1
  105. gobby/tasks/validation.py +22 -20
  106. gobby/tools/summarizer.py +91 -10
  107. gobby/utils/project_context.py +2 -3
  108. gobby/utils/status.py +13 -0
  109. gobby/workflows/actions.py +221 -1217
  110. gobby/workflows/artifact_actions.py +31 -0
  111. gobby/workflows/autonomous_actions.py +11 -0
  112. gobby/workflows/context_actions.py +50 -1
  113. gobby/workflows/detection_helpers.py +38 -24
  114. gobby/workflows/enforcement/__init__.py +47 -0
  115. gobby/workflows/enforcement/blocking.py +281 -0
  116. gobby/workflows/enforcement/commit_policy.py +283 -0
  117. gobby/workflows/enforcement/handlers.py +269 -0
  118. gobby/workflows/enforcement/task_policy.py +542 -0
  119. gobby/workflows/engine.py +93 -0
  120. gobby/workflows/evaluator.py +110 -0
  121. gobby/workflows/git_utils.py +106 -0
  122. gobby/workflows/hooks.py +41 -0
  123. gobby/workflows/llm_actions.py +30 -0
  124. gobby/workflows/mcp_actions.py +20 -1
  125. gobby/workflows/memory_actions.py +91 -0
  126. gobby/workflows/safe_evaluator.py +191 -0
  127. gobby/workflows/session_actions.py +44 -0
  128. gobby/workflows/state_actions.py +60 -1
  129. gobby/workflows/stop_signal_actions.py +55 -0
  130. gobby/workflows/summary_actions.py +217 -51
  131. gobby/workflows/task_sync_actions.py +347 -0
  132. gobby/workflows/todo_actions.py +34 -1
  133. gobby/workflows/webhook_actions.py +185 -0
  134. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
  135. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
  136. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
  137. gobby/adapters/codex.py +0 -1332
  138. gobby/cli/tui.py +0 -34
  139. gobby/install/claude/commands/gobby/bug.md +0 -51
  140. gobby/install/claude/commands/gobby/chore.md +0 -51
  141. gobby/install/claude/commands/gobby/epic.md +0 -52
  142. gobby/install/claude/commands/gobby/eval.md +0 -235
  143. gobby/install/claude/commands/gobby/feat.md +0 -49
  144. gobby/install/claude/commands/gobby/nit.md +0 -52
  145. gobby/install/claude/commands/gobby/ref.md +0 -52
  146. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  147. gobby/prompts/defaults/expansion/system.md +0 -119
  148. gobby/prompts/defaults/expansion/user.md +0 -48
  149. gobby/prompts/defaults/external_validation/agent.md +0 -72
  150. gobby/prompts/defaults/external_validation/external.md +0 -63
  151. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  152. gobby/prompts/defaults/external_validation/system.md +0 -6
  153. gobby/prompts/defaults/features/import_mcp.md +0 -22
  154. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  155. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  156. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  157. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  158. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  159. gobby/prompts/defaults/features/server_description.md +0 -20
  160. gobby/prompts/defaults/features/server_description_system.md +0 -6
  161. gobby/prompts/defaults/features/task_description.md +0 -31
  162. gobby/prompts/defaults/features/task_description_system.md +0 -6
  163. gobby/prompts/defaults/features/tool_summary.md +0 -17
  164. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  165. gobby/prompts/defaults/handoff/compact.md +0 -63
  166. gobby/prompts/defaults/handoff/session_end.md +0 -57
  167. gobby/prompts/defaults/memory/extract.md +0 -61
  168. gobby/prompts/defaults/research/step.md +0 -58
  169. gobby/prompts/defaults/validation/criteria.md +0 -47
  170. gobby/prompts/defaults/validation/validate.md +0 -38
  171. gobby/storage/migrations_legacy.py +0 -1359
  172. gobby/tui/__init__.py +0 -5
  173. gobby/tui/api_client.py +0 -278
  174. gobby/tui/app.py +0 -329
  175. gobby/tui/screens/__init__.py +0 -25
  176. gobby/tui/screens/agents.py +0 -333
  177. gobby/tui/screens/chat.py +0 -450
  178. gobby/tui/screens/dashboard.py +0 -377
  179. gobby/tui/screens/memory.py +0 -305
  180. gobby/tui/screens/metrics.py +0 -231
  181. gobby/tui/screens/orchestrator.py +0 -903
  182. gobby/tui/screens/sessions.py +0 -412
  183. gobby/tui/screens/tasks.py +0 -440
  184. gobby/tui/screens/workflows.py +0 -289
  185. gobby/tui/screens/worktrees.py +0 -174
  186. gobby/tui/widgets/__init__.py +0 -21
  187. gobby/tui/widgets/chat.py +0 -210
  188. gobby/tui/widgets/conductor.py +0 -104
  189. gobby/tui/widgets/menu.py +0 -132
  190. gobby/tui/widgets/message_panel.py +0 -160
  191. gobby/tui/widgets/review_gate.py +0 -224
  192. gobby/tui/widgets/task_tree.py +0 -99
  193. gobby/tui/widgets/token_budget.py +0 -166
  194. gobby/tui/ws_client.py +0 -258
  195. gobby/workflows/task_enforcement_actions.py +0 -1343
  196. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  197. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  198. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,248 @@
1
+ """Search coordination for memory recall operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from gobby.storage.memories import Memory
9
+
10
+ if TYPE_CHECKING:
11
+ from gobby.config.persistence import MemoryConfig
12
+ from gobby.memory.search import SearchBackend
13
+ from gobby.storage.database import DatabaseProtocol
14
+ from gobby.storage.memories import LocalMemoryManager
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class SearchCoordinator:
20
+ """
21
+ Coordinates search operations for memory recall.
22
+
23
+ Manages the search backend lifecycle, fitting, and query execution.
24
+ Extracts search-related logic from MemoryManager for focused responsibility.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ storage: LocalMemoryManager,
30
+ config: MemoryConfig,
31
+ db: DatabaseProtocol,
32
+ ):
33
+ """
34
+ Initialize the search coordinator.
35
+
36
+ Args:
37
+ storage: Memory storage manager for accessing memories
38
+ config: Memory configuration for search settings
39
+ db: Database connection for search backend initialization
40
+ """
41
+ self._storage = storage
42
+ self._config = config
43
+ self._db = db
44
+ self._search_backend: SearchBackend | None = None
45
+ self._search_backend_fitted = False
46
+
47
+ @property
48
+ def search_backend(self) -> SearchBackend:
49
+ """
50
+ Lazy-init search backend based on configuration.
51
+
52
+ The backend type is determined by config.search_backend:
53
+ - "tfidf" (default): Zero-dependency TF-IDF search
54
+ - "text": Simple text substring matching
55
+ """
56
+ if self._search_backend is None:
57
+ from gobby.memory.search import get_search_backend
58
+
59
+ backend_type = getattr(self._config, "search_backend", "tfidf")
60
+ logger.debug(f"Initializing search backend: {backend_type}")
61
+
62
+ try:
63
+ self._search_backend = get_search_backend(
64
+ backend_type=backend_type,
65
+ db=self._db,
66
+ )
67
+ except Exception as e:
68
+ logger.warning(
69
+ f"Failed to initialize {backend_type} backend: {e}. Falling back to tfidf"
70
+ )
71
+ self._search_backend = get_search_backend("tfidf")
72
+
73
+ return self._search_backend
74
+
75
+ def ensure_fitted(self) -> None:
76
+ """Ensure the search backend is fitted with current memories."""
77
+ if self._search_backend_fitted:
78
+ return
79
+
80
+ backend = self.search_backend
81
+ if not backend.needs_refit():
82
+ self._search_backend_fitted = True
83
+ return
84
+
85
+ # Fit the backend with all memories
86
+ max_memories = getattr(self._config, "max_index_memories", 10000)
87
+ memories = self._storage.list_memories(limit=max_memories)
88
+ memory_tuples = [(m.id, m.content) for m in memories]
89
+
90
+ try:
91
+ backend.fit(memory_tuples)
92
+ self._search_backend_fitted = True
93
+ logger.info(f"Search backend fitted with {len(memory_tuples)} memories")
94
+ except Exception as e:
95
+ logger.error(f"Failed to fit search backend: {e}")
96
+ raise
97
+
98
+ def mark_refit_needed(self) -> None:
99
+ """Mark that the search backend needs to be refitted."""
100
+ self._search_backend_fitted = False
101
+
102
+ def reindex(self) -> dict[str, Any]:
103
+ """
104
+ Force rebuild of the search index.
105
+
106
+ This method explicitly rebuilds the TF-IDF (or other configured)
107
+ search index from all stored memories. Useful for:
108
+ - Initial index building
109
+ - Recovery after corruption
110
+ - After bulk memory operations
111
+
112
+ Returns:
113
+ Dict with index statistics including memory_count, backend_type, etc.
114
+ """
115
+ # Get all memories
116
+ memories = self._storage.list_memories(limit=10000)
117
+ memory_tuples = [(m.id, m.content) for m in memories]
118
+
119
+ # Force refit the backend
120
+ backend = self.search_backend
121
+ backend_type = getattr(self._config, "search_backend", "tfidf")
122
+
123
+ try:
124
+ backend.fit(memory_tuples)
125
+ self._search_backend_fitted = True
126
+
127
+ # Get backend stats
128
+ stats = backend.get_stats() if hasattr(backend, "get_stats") else {}
129
+
130
+ return {
131
+ "success": True,
132
+ "memory_count": len(memory_tuples),
133
+ "backend_type": backend_type,
134
+ "fitted": True,
135
+ **stats,
136
+ }
137
+ except Exception as e:
138
+ logger.error(f"Failed to reindex search backend: {e}")
139
+ return {
140
+ "success": False,
141
+ "error": str(e),
142
+ "memory_count": len(memory_tuples),
143
+ "backend_type": backend_type,
144
+ }
145
+
146
+ def search(
147
+ self,
148
+ query: str,
149
+ project_id: str | None = None,
150
+ limit: int = 10,
151
+ min_importance: float | None = None,
152
+ search_mode: str | None = None,
153
+ tags_all: list[str] | None = None,
154
+ tags_any: list[str] | None = None,
155
+ tags_none: list[str] | None = None,
156
+ ) -> list[Memory]:
157
+ """
158
+ Perform search using the configured search backend.
159
+
160
+ Uses the new search backend by default (TF-IDF),
161
+ falling back to legacy semantic search if configured.
162
+
163
+ Args:
164
+ query: Search query text
165
+ project_id: Filter by project
166
+ limit: Maximum results to return
167
+ min_importance: Minimum importance threshold
168
+ search_mode: Search mode (tfidf, text, etc.)
169
+ tags_all: Memory must have ALL of these tags
170
+ tags_any: Memory must have at least ONE of these tags
171
+ tags_none: Memory must have NONE of these tags
172
+
173
+ Returns:
174
+ List of matching Memory objects
175
+ """
176
+ # Determine search mode from config or parameters
177
+ if search_mode is None:
178
+ search_mode = getattr(self._config, "search_backend", "tfidf")
179
+
180
+ # Use the search backend
181
+ try:
182
+ self.ensure_fitted()
183
+ # Fetch more results to allow for filtering
184
+ fetch_multiplier = 3 if (tags_all or tags_any or tags_none) else 2
185
+ results = self.search_backend.search(query, top_k=limit * fetch_multiplier)
186
+
187
+ # Get the actual Memory objects
188
+ memory_ids = [mid for mid, _ in results]
189
+ memories = []
190
+ for mid in memory_ids:
191
+ memory = self._storage.get_memory(mid)
192
+ if memory:
193
+ # Apply filters - allow global memories (project_id is None) to pass through
194
+ if (
195
+ project_id
196
+ and memory.project_id is not None
197
+ and memory.project_id != project_id
198
+ ):
199
+ continue
200
+ if min_importance is not None and memory.importance < min_importance:
201
+ continue
202
+ # Apply tag filters
203
+ if not self._passes_tag_filter(memory, tags_all, tags_any, tags_none):
204
+ continue
205
+ memories.append(memory)
206
+ if len(memories) >= limit:
207
+ break
208
+
209
+ return memories
210
+
211
+ except Exception as e:
212
+ logger.warning(f"Search backend failed, falling back to text search: {e}")
213
+ # Fall back to text search with tag filtering
214
+ memories = self._storage.search_memories(
215
+ query_text=query,
216
+ project_id=project_id,
217
+ limit=limit * 2,
218
+ tags_all=tags_all,
219
+ tags_any=tags_any,
220
+ tags_none=tags_none,
221
+ )
222
+ if min_importance:
223
+ memories = [m for m in memories if m.importance >= min_importance]
224
+ return memories[:limit]
225
+
226
+ def _passes_tag_filter(
227
+ self,
228
+ memory: Memory,
229
+ tags_all: list[str] | None = None,
230
+ tags_any: list[str] | None = None,
231
+ tags_none: list[str] | None = None,
232
+ ) -> bool:
233
+ """Check if a memory passes the tag filter criteria."""
234
+ memory_tags = set(memory.tags) if memory.tags else set()
235
+
236
+ # Check tags_all: memory must have ALL specified tags
237
+ if tags_all and not set(tags_all).issubset(memory_tags):
238
+ return False
239
+
240
+ # Check tags_any: memory must have at least ONE specified tag
241
+ if tags_any and not memory_tags.intersection(tags_any):
242
+ return False
243
+
244
+ # Check tags_none: memory must have NONE of the specified tags
245
+ if tags_none and memory_tags.intersection(tags_none):
246
+ return False
247
+
248
+ return True
@@ -0,0 +1,5 @@
1
+ """Memory services for cross-referencing and linking."""
2
+
3
+ from gobby.memory.services.crossref import CrossrefService
4
+
5
+ __all__ = ["CrossrefService"]
@@ -0,0 +1,142 @@
1
+ """Cross-reference service for linking related memories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from collections.abc import Callable
8
+ from typing import TYPE_CHECKING
9
+
10
+ from gobby.storage.memories import Memory
11
+
12
+ if TYPE_CHECKING:
13
+ from gobby.config.persistence import MemoryConfig
14
+ from gobby.memory.search import SearchBackend
15
+ from gobby.storage.memories import LocalMemoryManager
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class CrossrefService:
21
+ """
22
+ Service for creating and managing cross-references between memories.
23
+
24
+ Cross-references link related memories based on content similarity,
25
+ enabling navigation between conceptually connected items.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ storage: LocalMemoryManager,
31
+ config: MemoryConfig,
32
+ search_backend_getter: Callable[[], SearchBackend],
33
+ ensure_fitted: Callable[[], None],
34
+ ):
35
+ """
36
+ Initialize the cross-reference service.
37
+
38
+ Args:
39
+ storage: Memory storage manager for persistence
40
+ config: Memory configuration for thresholds
41
+ search_backend_getter: Callable that returns the search backend
42
+ ensure_fitted: Callable that ensures search backend is fitted
43
+ """
44
+ self._storage = storage
45
+ self._config = config
46
+ self._get_search_backend = search_backend_getter
47
+ self._ensure_fitted = ensure_fitted
48
+
49
+ async def create_crossrefs(
50
+ self,
51
+ memory: Memory,
52
+ threshold: float | None = None,
53
+ max_links: int | None = None,
54
+ ) -> int:
55
+ """
56
+ Find and link similar memories.
57
+
58
+ Uses the search backend to find memories similar to the given one
59
+ and creates cross-references for those above the threshold.
60
+
61
+ Args:
62
+ memory: The memory to find links for
63
+ threshold: Minimum similarity to create link (default from config)
64
+ max_links: Maximum links to create (default from config)
65
+
66
+ Returns:
67
+ Number of cross-references created
68
+ """
69
+ # Get thresholds from config or use defaults
70
+ if threshold is None:
71
+ threshold = getattr(self._config, "crossref_threshold", None)
72
+ if threshold is None:
73
+ threshold = 0.3
74
+ if max_links is None:
75
+ max_links = getattr(self._config, "crossref_max_links", None)
76
+ if max_links is None:
77
+ max_links = 5
78
+
79
+ # Ensure search backend is fitted (sync check is fine - just checks a flag)
80
+ self._ensure_fitted()
81
+
82
+ # Search for similar memories (wrap sync I/O in to_thread)
83
+ search_backend = self._get_search_backend()
84
+ similar = await asyncio.to_thread(search_backend.search, memory.content, max_links + 1)
85
+
86
+ # Create cross-references
87
+ created = 0
88
+ for other_id, score in similar:
89
+ # Skip self-reference
90
+ if other_id == memory.id:
91
+ continue
92
+
93
+ # Skip below threshold
94
+ if score < threshold:
95
+ continue
96
+
97
+ # Create the crossref (wrap sync I/O in to_thread)
98
+ await asyncio.to_thread(self._storage.create_crossref, memory.id, other_id, score)
99
+ created += 1
100
+
101
+ if created >= max_links:
102
+ break
103
+
104
+ if created > 0:
105
+ logger.debug(f"Created {created} crossrefs for memory {memory.id}")
106
+
107
+ return created
108
+
109
+ async def get_related(
110
+ self,
111
+ memory_id: str,
112
+ limit: int = 5,
113
+ min_similarity: float = 0.0,
114
+ ) -> list[Memory]:
115
+ """
116
+ Get memories linked to this one via cross-references.
117
+
118
+ Args:
119
+ memory_id: The memory ID to find related memories for
120
+ limit: Maximum number of results
121
+ min_similarity: Minimum similarity threshold
122
+
123
+ Returns:
124
+ List of related Memory objects, sorted by similarity
125
+ """
126
+ crossrefs = await asyncio.to_thread(
127
+ self._storage.get_crossrefs,
128
+ memory_id,
129
+ limit=limit,
130
+ min_similarity=min_similarity,
131
+ )
132
+
133
+ # Get the actual Memory objects
134
+ memories = []
135
+ for ref in crossrefs:
136
+ # Get the "other" memory in the relationship
137
+ other_id = ref.target_id if ref.source_id == memory_id else ref.source_id
138
+ memory = await asyncio.to_thread(self._storage.get_memory, other_id)
139
+ if memory:
140
+ memories.append(memory)
141
+
142
+ return memories
gobby/prompts/loader.py CHANGED
@@ -23,8 +23,11 @@ from .models import PromptTemplate
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
- # Default location for bundled prompts
27
- DEFAULTS_DIR = Path(__file__).parent / "defaults"
26
+ # Default location for bundled prompts (in install/shared/prompts for installation)
27
+ # Falls back to src location for development if install location doesn't exist
28
+ _INSTALL_PROMPTS_DIR = Path(__file__).parent.parent / "install" / "shared" / "prompts"
29
+ _DEV_PROMPTS_DIR = Path(__file__).parent / "defaults"
30
+ DEFAULTS_DIR = _INSTALL_PROMPTS_DIR if _INSTALL_PROMPTS_DIR.exists() else _DEV_PROMPTS_DIR
28
31
 
29
32
 
30
33
  class PromptLoader:
gobby/runner.py CHANGED
@@ -471,6 +471,19 @@ class GobbyRunner:
471
471
  except (asyncio.CancelledError, TimeoutError):
472
472
  pass
473
473
 
474
+ # Export memories to JSONL backup on shutdown
475
+ if self.memory_sync_manager:
476
+ try:
477
+ count = await asyncio.wait_for(
478
+ self.memory_sync_manager.export_to_files(), timeout=5.0
479
+ )
480
+ if count > 0:
481
+ logger.info(f"Shutdown memory backup: exported {count} memories")
482
+ except TimeoutError:
483
+ logger.warning("Memory backup on shutdown timed out")
484
+ except Exception as e:
485
+ logger.warning(f"Memory backup on shutdown failed: {e}")
486
+
474
487
  try:
475
488
  await asyncio.wait_for(self.mcp_proxy.disconnect_all(), timeout=3.0)
476
489
  except TimeoutError:
gobby/servers/http.py CHANGED
@@ -16,7 +16,7 @@ from fastapi import FastAPI, HTTPException, Request
16
16
  from fastapi.middleware.cors import CORSMiddleware
17
17
  from fastapi.responses import JSONResponse
18
18
 
19
- from gobby.adapters.codex import CodexAdapter
19
+ from gobby.adapters.codex_impl.adapter import CodexAdapter
20
20
  from gobby.hooks.broadcaster import HookEventBroadcaster
21
21
  from gobby.hooks.hook_manager import HookManager
22
22
  from gobby.llm import LLMService, create_llm_service
@@ -25,9 +25,6 @@ from gobby.mcp_proxy.semantic_search import SemanticToolSearch
25
25
  from gobby.mcp_proxy.server import GobbyDaemonTools, create_mcp_server
26
26
  from gobby.mcp_proxy.services.tool_filter import ToolFilterService
27
27
  from gobby.memory.manager import MemoryManager
28
-
29
- # Re-export for backward compatibility
30
- from gobby.servers.models import SessionRegisterRequest # noqa: F401
31
28
  from gobby.storage.sessions import LocalSessionManager
32
29
  from gobby.storage.tasks import LocalTaskManager
33
30
  from gobby.sync.tasks import TaskSyncManager
@@ -175,6 +175,19 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
175
175
  except Exception as e:
176
176
  logger.warning(f"Failed to get memory stats: {e}")
177
177
 
178
+ # Get skills statistics
179
+ skills_stats: dict[str, Any] = {"total": 0}
180
+ if server._internal_manager:
181
+ try:
182
+ for registry in server._internal_manager.get_all_registries():
183
+ if registry.name == "gobby-skills":
184
+ result = await registry.call("list_skills", {"limit": 10000})
185
+ if result.get("success"):
186
+ skills_stats["total"] = result.get("count", 0)
187
+ break
188
+ except Exception as e:
189
+ logger.warning(f"Failed to get skills stats: {e}")
190
+
178
191
  # Get plugin status
179
192
  plugin_stats: dict[str, Any] = {"enabled": False, "loaded": 0, "handlers": 0}
180
193
  if hasattr(server, "_hook_manager") and server._hook_manager is not None:
@@ -218,6 +231,7 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
218
231
  "sessions": session_stats,
219
232
  "tasks": task_stats,
220
233
  "memory": memory_stats,
234
+ "skills": skills_stats,
221
235
  "plugins": plugin_stats,
222
236
  "response_time_ms": response_time_ms,
223
237
  }
@@ -0,0 +1,61 @@
1
+ """
2
+ MCP endpoint modules for the Gobby HTTP server.
3
+
4
+ This package contains decomposed endpoint handlers extracted from tools.py
5
+ using the Strangler Fig pattern. Each module handles a specific domain:
6
+
7
+ - discovery: Tool and server listing endpoints
8
+ - execution: Tool invocation endpoints
9
+ - server: Server management (add/remove/import)
10
+ - registry: Tool embedding, status, and refresh
11
+
12
+ External modules should import `create_mcp_router` from the parent package:
13
+ from gobby.servers.routes.mcp import create_mcp_router
14
+
15
+ For direct endpoint access (e.g., testing), import from submodules:
16
+ from gobby.servers.routes.mcp.endpoints.discovery import list_all_mcp_tools
17
+ """
18
+
19
+ from gobby.servers.routes.mcp.endpoints.discovery import (
20
+ list_all_mcp_tools,
21
+ recommend_mcp_tools,
22
+ search_mcp_tools,
23
+ )
24
+ from gobby.servers.routes.mcp.endpoints.execution import (
25
+ call_mcp_tool,
26
+ get_tool_schema,
27
+ list_mcp_tools,
28
+ mcp_proxy,
29
+ )
30
+ from gobby.servers.routes.mcp.endpoints.registry import (
31
+ embed_mcp_tools,
32
+ get_mcp_status,
33
+ refresh_mcp_tools,
34
+ )
35
+ from gobby.servers.routes.mcp.endpoints.server import (
36
+ add_mcp_server,
37
+ import_mcp_server,
38
+ list_mcp_servers,
39
+ remove_mcp_server,
40
+ )
41
+
42
+ __all__ = [
43
+ # Discovery
44
+ "list_all_mcp_tools",
45
+ "recommend_mcp_tools",
46
+ "search_mcp_tools",
47
+ # Execution
48
+ "call_mcp_tool",
49
+ "get_tool_schema",
50
+ "list_mcp_tools",
51
+ "mcp_proxy",
52
+ # Registry
53
+ "embed_mcp_tools",
54
+ "get_mcp_status",
55
+ "refresh_mcp_tools",
56
+ # Server
57
+ "add_mcp_server",
58
+ "import_mcp_server",
59
+ "list_mcp_servers",
60
+ "remove_mcp_server",
61
+ ]