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,239 @@
1
+ """Commits and workflow tools for session management.
2
+
3
+ This module contains MCP tools for:
4
+ - Getting session commits (get_session_commits)
5
+ - Marking autonomous loop complete (mark_loop_complete)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import UTC
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ if TYPE_CHECKING:
14
+ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
15
+ from gobby.storage.sessions import LocalSessionManager
16
+
17
+
18
+ def register_commits_tools(
19
+ registry: InternalToolRegistry,
20
+ session_manager: LocalSessionManager,
21
+ ) -> None:
22
+ """
23
+ Register commits and workflow tools with a registry.
24
+
25
+ Args:
26
+ registry: The InternalToolRegistry to register tools with
27
+ session_manager: LocalSessionManager instance for session operations
28
+ """
29
+
30
+ def _resolve_session_id(ref: str) -> str:
31
+ """Resolve session reference (#N, N, UUID, or prefix) to UUID."""
32
+ from gobby.utils.project_context import get_project_context
33
+
34
+ project_ctx = get_project_context()
35
+ project_id = project_ctx.get("id") if project_ctx else None
36
+
37
+ return session_manager.resolve_session_reference(ref, project_id)
38
+
39
+ @registry.tool(
40
+ name="get_session_commits",
41
+ description="Get git commits made during a session timeframe. Accepts #N, N, UUID, or prefix for session_id.",
42
+ )
43
+ def get_session_commits(
44
+ session_id: str,
45
+ max_commits: int = 20,
46
+ ) -> dict[str, Any]:
47
+ """
48
+ Get git commits made during a session's active timeframe.
49
+
50
+ Uses session.created_at and session.updated_at to filter
51
+ git log within that timeframe.
52
+
53
+ Args:
54
+ session_id: Session reference - supports #N, N (seq_num), UUID, or prefix
55
+ max_commits: Maximum commits to return (default 20)
56
+
57
+ Returns:
58
+ Session ID, list of commits, and count
59
+ """
60
+ import subprocess # nosec B404 - subprocess needed for git commands
61
+ from datetime import datetime
62
+ from pathlib import Path
63
+
64
+ if session_manager is None:
65
+ return {"error": "Session manager not available"}
66
+
67
+ # Resolve session reference (#N, N, UUID, or prefix)
68
+ try:
69
+ resolved_id = _resolve_session_id(session_id)
70
+ session = session_manager.get(resolved_id)
71
+ except ValueError as e:
72
+ return {"error": str(e)}
73
+
74
+ if not session:
75
+ return {"error": f"Session {session_id} not found"}
76
+
77
+ # Get working directory from transcript path or project
78
+ cwd = None
79
+ if session.jsonl_path:
80
+ cwd = str(Path(session.jsonl_path).parent)
81
+
82
+ # Format timestamps for git --since/--until
83
+ # Git expects ISO format or relative dates
84
+ # Session timestamps may be ISO strings or datetime objects
85
+ if isinstance(session.created_at, str):
86
+ since_time = datetime.fromisoformat(session.created_at.replace("Z", "+00:00"))
87
+ else:
88
+ since_time = session.created_at
89
+
90
+ if session.updated_at:
91
+ if isinstance(session.updated_at, str):
92
+ until_time = datetime.fromisoformat(session.updated_at.replace("Z", "+00:00"))
93
+ else:
94
+ until_time = session.updated_at
95
+ else:
96
+ until_time = datetime.now(UTC)
97
+
98
+ # Format as ISO 8601 for git
99
+ since_str = since_time.strftime("%Y-%m-%dT%H:%M:%S")
100
+ until_str = until_time.strftime("%Y-%m-%dT%H:%M:%S")
101
+
102
+ try:
103
+ # Get commits within timeframe
104
+ cmd = [
105
+ "git",
106
+ "log",
107
+ f"--since={since_str}",
108
+ f"--until={until_str}",
109
+ f"-{max_commits}",
110
+ "--format=%H|%s|%aI", # hash|subject|author-date-iso
111
+ ]
112
+
113
+ result = subprocess.run( # nosec B603 - cmd built from hardcoded git arguments
114
+ cmd,
115
+ capture_output=True,
116
+ text=True,
117
+ timeout=10,
118
+ cwd=cwd,
119
+ )
120
+
121
+ if result.returncode != 0:
122
+ return {
123
+ "session_id": session.id,
124
+ "error": "Git command failed",
125
+ "stderr": result.stderr.strip(),
126
+ }
127
+
128
+ commits = []
129
+ for line in result.stdout.strip().split("\n"):
130
+ if "|" in line:
131
+ parts = line.split("|", 2)
132
+ if len(parts) >= 2:
133
+ commit = {
134
+ "hash": parts[0],
135
+ "message": parts[1],
136
+ }
137
+ if len(parts) >= 3:
138
+ commit["timestamp"] = parts[2]
139
+ commits.append(commit)
140
+
141
+ return {
142
+ "session_id": session.id,
143
+ "commits": commits,
144
+ "count": len(commits),
145
+ "timeframe": {
146
+ "since": since_str,
147
+ "until": until_str,
148
+ },
149
+ }
150
+
151
+ except subprocess.TimeoutExpired:
152
+ return {
153
+ "session_id": session.id,
154
+ "error": "Git command timed out",
155
+ }
156
+ except FileNotFoundError:
157
+ return {
158
+ "session_id": session.id,
159
+ "error": "Git not found or not a git repository",
160
+ }
161
+ except Exception as e:
162
+ return {
163
+ "session_id": session.id,
164
+ "error": f"Failed to get commits: {e!s}",
165
+ }
166
+
167
+ @registry.tool(
168
+ name="mark_loop_complete",
169
+ description="""Mark the autonomous loop as complete, preventing session chaining. Accepts #N, N, UUID, or prefix for session_id.
170
+
171
+ Args:
172
+ session_id: (REQUIRED) Your session ID. Accepts #N, N, UUID, or prefix. Get it from:
173
+ 1. Your injected context (look for 'Session Ref: #N' or 'session_id: xxx')
174
+ 2. Or call get_current_session(external_id, source) first""",
175
+ )
176
+ def mark_loop_complete(session_id: str) -> dict[str, Any]:
177
+ """
178
+ Mark the autonomous loop as complete for a session.
179
+
180
+ This sets stop_reason='completed' in the workflow state, which
181
+ signals the auto-loop workflow to NOT chain a new session
182
+ when this session ends.
183
+
184
+ Use this when:
185
+ - A task is fully complete and no more work is needed
186
+ - You want to exit the autonomous loop gracefully
187
+ - The user has explicitly asked to stop
188
+
189
+ Args:
190
+ session_id: Session reference - supports #N, N (seq_num), UUID, or prefix (REQUIRED)
191
+
192
+ Returns:
193
+ Success status and session details
194
+ """
195
+ if not session_manager:
196
+ return {"error": "Session manager not available"}
197
+
198
+ # Resolve session reference (#N, N, UUID, or prefix)
199
+ try:
200
+ resolved_id = _resolve_session_id(session_id)
201
+ session = session_manager.get(resolved_id)
202
+ except ValueError as e:
203
+ return {"error": str(e), "session_id": session_id}
204
+
205
+ if not session:
206
+ return {"error": f"Session {session_id} not found", "session_id": session_id}
207
+
208
+ # Load and update workflow state
209
+ from gobby.storage.database import LocalDatabase
210
+ from gobby.workflows.definitions import WorkflowState
211
+ from gobby.workflows.state_manager import WorkflowStateManager
212
+
213
+ db = LocalDatabase()
214
+ state_manager = WorkflowStateManager(db)
215
+
216
+ # Get or create state for session
217
+ state = state_manager.get_state(session.id)
218
+ if not state:
219
+ # Create minimal state just to hold the variable
220
+ state = WorkflowState(
221
+ session_id=session.id,
222
+ workflow_name="auto-loop",
223
+ step="active",
224
+ )
225
+
226
+ # Mark loop complete using the action function
227
+ from gobby.workflows.state_actions import mark_loop_complete as action_mark_complete
228
+
229
+ action_mark_complete(state)
230
+
231
+ # Save updated state
232
+ state_manager.save_state(state)
233
+
234
+ return {
235
+ "success": True,
236
+ "session_id": session.id,
237
+ "stop_reason": "completed",
238
+ "message": "Autonomous loop marked complete - session will not chain",
239
+ }
@@ -0,0 +1,253 @@
1
+ """Session CRUD tools for session management.
2
+
3
+ This module contains MCP tools for:
4
+ - Getting session details (get_session)
5
+ - Getting current session (get_current_session)
6
+ - Listing sessions (list_sessions)
7
+ - Session statistics (session_stats)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ if TYPE_CHECKING:
15
+ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
16
+ from gobby.storage.sessions import LocalSessionManager
17
+
18
+
19
+ def register_crud_tools(
20
+ registry: InternalToolRegistry,
21
+ session_manager: LocalSessionManager,
22
+ ) -> None:
23
+ """
24
+ Register session CRUD tools with a registry.
25
+
26
+ Args:
27
+ registry: The InternalToolRegistry to register tools with
28
+ session_manager: LocalSessionManager instance for session operations
29
+ """
30
+
31
+ @registry.tool(
32
+ name="get_session",
33
+ description="Get session details by ID. Accepts #N (project-scoped ref), UUID, or prefix. Use the session_id from your injected context.",
34
+ )
35
+ def get_session(session_id: str) -> dict[str, Any]:
36
+ """
37
+ Get session details by session reference.
38
+
39
+ Your session_id is injected into your context at session start.
40
+ Look for 'Session Ref: #N' or 'session_id: xxx' in your system reminders.
41
+
42
+ Args:
43
+ session_id: Session reference - supports #N (project-scoped), UUID, or prefix
44
+
45
+ Returns:
46
+ Session dict with all fields, or error if not found
47
+ """
48
+ from gobby.utils.project_context import get_project_context
49
+
50
+ # Support #N format, UUID, and prefix matching
51
+ if session_manager is None:
52
+ return {"error": "Session manager not available"}
53
+
54
+ # Get project_id for project-scoped resolution
55
+ project_ctx = get_project_context()
56
+ project_id = project_ctx.get("id") if project_ctx else None
57
+
58
+ # Try to resolve session reference (#N, UUID, or prefix)
59
+ try:
60
+ resolved_id = session_manager.resolve_session_reference(session_id, project_id)
61
+ session = session_manager.get(resolved_id)
62
+ except ValueError:
63
+ session = None
64
+
65
+ if not session:
66
+ return {"error": f"Session {session_id} not found", "found": False}
67
+
68
+ return {
69
+ "found": True,
70
+ **session.to_dict(),
71
+ }
72
+
73
+ @registry.tool(
74
+ name="get_current_session",
75
+ description="""Get YOUR current session ID - the CORRECT way to look up your session.
76
+
77
+ Use this when session_id wasn't in your injected context. Pass your external_id
78
+ (from transcript path or GOBBY_SESSION_ID env) and source (claude, gemini, codex).
79
+
80
+ DO NOT use list_sessions to find your session - it won't work with multiple active sessions.""",
81
+ )
82
+ def get_current_session(
83
+ external_id: str,
84
+ source: str,
85
+ ) -> dict[str, Any]:
86
+ """
87
+ Look up your internal session_id from external_id and source.
88
+
89
+ The agent passes external_id (from injected context or GOBBY_SESSION_ID env var)
90
+ and source (claude, gemini, codex). project_id and machine_id are
91
+ auto-resolved from config files.
92
+
93
+ Args:
94
+ external_id: Your CLI's session ID (from context or GOBBY_SESSION_ID env)
95
+ source: CLI source - "claude", "gemini", or "codex"
96
+
97
+ Returns:
98
+ session_id: Internal Gobby session ID (use for parent_session_id, etc.)
99
+ Plus basic session metadata
100
+ """
101
+ from gobby.utils.machine_id import get_machine_id
102
+ from gobby.utils.project_context import get_project_context
103
+
104
+ if session_manager is None:
105
+ return {"error": "Session manager not available"}
106
+
107
+ # Auto-resolve context
108
+ machine_id = get_machine_id()
109
+ project_ctx = get_project_context()
110
+ project_id = project_ctx.get("id") if project_ctx else None
111
+
112
+ if not machine_id:
113
+ return {"error": "Could not determine machine_id"}
114
+ if not project_id:
115
+ return {"error": "Could not determine project_id (not in a gobby project?)"}
116
+
117
+ # Use find_by_external_id with full composite key (safe lookup)
118
+ session = session_manager.find_by_external_id(
119
+ external_id=external_id,
120
+ machine_id=machine_id,
121
+ project_id=project_id,
122
+ source=source,
123
+ )
124
+
125
+ if not session:
126
+ return {
127
+ "found": False,
128
+ "error": "Session not found",
129
+ "lookup": {
130
+ "external_id": external_id,
131
+ "source": source,
132
+ "project_id": project_id,
133
+ },
134
+ }
135
+
136
+ return {
137
+ "found": True,
138
+ "session_id": session.id,
139
+ "project_id": session.project_id,
140
+ "status": session.status,
141
+ "agent_run_id": session.agent_run_id,
142
+ }
143
+
144
+ @registry.tool(
145
+ name="list_sessions",
146
+ description="""List sessions with optional filtering.
147
+
148
+ WARNING: Do NOT use this to find your own session_id!
149
+ - `list_sessions(status="active", limit=1)` will NOT reliably return YOUR session
150
+ - Multiple sessions can be active simultaneously (parallel agents, multiple terminals)
151
+ - Use `get_current_session(external_id, source)` instead - it uses your unique session key
152
+
153
+ This tool is for browsing/listing sessions, not for self-identification.""",
154
+ )
155
+ def list_sessions(
156
+ project_id: str | None = None,
157
+ status: str | None = None,
158
+ source: str | None = None,
159
+ limit: int = 20,
160
+ ) -> dict[str, Any]:
161
+ """
162
+ List sessions with filters.
163
+
164
+ Args:
165
+ project_id: Filter by project ID
166
+ status: Filter by status (active, paused, expired, archived, handoff_ready)
167
+ source: Filter by CLI source (claude, gemini, codex)
168
+ limit: Max results (default 20)
169
+
170
+ Returns:
171
+ List of sessions and count
172
+ """
173
+ if session_manager is None:
174
+ return {"error": "Session manager not available"}
175
+
176
+ sessions = session_manager.list(
177
+ project_id=project_id,
178
+ status=status,
179
+ source=source,
180
+ limit=limit,
181
+ )
182
+
183
+ total = session_manager.count(
184
+ project_id=project_id,
185
+ status=status,
186
+ source=source,
187
+ )
188
+
189
+ # Detect likely misuse pattern: trying to find own session
190
+ if status == "active" and limit == 1:
191
+ return {
192
+ "warning": (
193
+ "list_sessions(status='active', limit=1) will NOT reliably get YOUR session_id! "
194
+ "Multiple sessions can be active simultaneously. "
195
+ "Use get_current_session(external_id='<your-external-id>', source='claude') instead."
196
+ ),
197
+ "hint": "Your external_id is in your transcript path: /path/to/<external_id>.jsonl",
198
+ "sessions": [s.to_dict() for s in sessions],
199
+ "count": len(sessions),
200
+ "total": total,
201
+ "limit": limit,
202
+ "filters": {
203
+ "project_id": project_id,
204
+ "status": status,
205
+ "source": source,
206
+ },
207
+ }
208
+
209
+ return {
210
+ "sessions": [s.to_dict() for s in sessions],
211
+ "count": len(sessions),
212
+ "total": total,
213
+ "limit": limit,
214
+ "filters": {
215
+ "project_id": project_id,
216
+ "status": status,
217
+ "source": source,
218
+ },
219
+ }
220
+
221
+ @registry.tool(
222
+ name="session_stats",
223
+ description="Get session statistics for a project.",
224
+ )
225
+ def session_stats(project_id: str | None = None) -> dict[str, Any]:
226
+ """
227
+ Get session statistics.
228
+
229
+ Args:
230
+ project_id: Filter by project ID (optional)
231
+
232
+ Returns:
233
+ Statistics including total, by_status, by_source
234
+ """
235
+ if session_manager is None:
236
+ return {"error": "Session manager not available"}
237
+
238
+ total = session_manager.count(project_id=project_id)
239
+ by_status = session_manager.count_by_status()
240
+
241
+ # Count by source
242
+ by_source: dict[str, int] = {}
243
+ for src in ["claude_code", "gemini", "codex"]:
244
+ count = session_manager.count(project_id=project_id, source=src)
245
+ if count > 0:
246
+ by_source[src] = count
247
+
248
+ return {
249
+ "total": total,
250
+ "by_status": by_status,
251
+ "by_source": by_source,
252
+ "project_id": project_id,
253
+ }
@@ -0,0 +1,63 @@
1
+ """Factory function for creating the session messages tool registry.
2
+
3
+ Orchestrates the creation of all session tool sub-registries and merges them
4
+ into a unified registry.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
12
+ from gobby.mcp_proxy.tools.sessions._commits import register_commits_tools
13
+ from gobby.mcp_proxy.tools.sessions._crud import register_crud_tools
14
+ from gobby.mcp_proxy.tools.sessions._handoff import register_handoff_tools
15
+ from gobby.mcp_proxy.tools.sessions._messages import register_message_tools
16
+
17
+ if TYPE_CHECKING:
18
+ from gobby.storage.session_messages import LocalSessionMessageManager
19
+ from gobby.storage.sessions import LocalSessionManager
20
+
21
+ __all__ = ["create_session_messages_registry"]
22
+
23
+
24
+ def create_session_messages_registry(
25
+ message_manager: LocalSessionMessageManager | None = None,
26
+ session_manager: LocalSessionManager | None = None,
27
+ ) -> InternalToolRegistry:
28
+ """
29
+ Create a sessions tool registry with session and message tools.
30
+
31
+ Args:
32
+ message_manager: LocalSessionMessageManager instance for message operations
33
+ session_manager: LocalSessionManager instance for session CRUD
34
+
35
+ Returns:
36
+ InternalToolRegistry with all session tools registered
37
+ """
38
+ registry = InternalToolRegistry(
39
+ name="gobby-sessions",
40
+ description="Session management and message querying - CRUD, retrieval, search",
41
+ )
42
+
43
+ # --- Message Tools ---
44
+ # Only register if message_manager is available
45
+ if message_manager is not None:
46
+ register_message_tools(registry, message_manager)
47
+
48
+ # --- Handoff Tools ---
49
+ # Only register if session_manager is available
50
+ if session_manager is not None:
51
+ register_handoff_tools(registry, session_manager)
52
+
53
+ # --- Session CRUD Tools ---
54
+ # Only register if session_manager is available
55
+ if session_manager is not None:
56
+ register_crud_tools(registry, session_manager)
57
+
58
+ # --- Commits Tools ---
59
+ # Only register if session_manager is available
60
+ if session_manager is not None:
61
+ register_commits_tools(registry, session_manager)
62
+
63
+ return registry