gobby 0.2.5__py3-none-any.whl → 0.2.7__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 (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  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/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
@@ -44,7 +44,7 @@ class WebSocketConfig:
44
44
  """Configuration for WebSocket server."""
45
45
 
46
46
  host: str = "localhost"
47
- port: int = 8765
47
+ port: int = 60888
48
48
  ping_interval: int = 30 # seconds
49
49
  ping_timeout: int = 10 # seconds
50
50
  max_message_size: int = 2 * 1024 * 1024 # 2MB
@@ -64,7 +64,7 @@ class WebSocketServer:
64
64
 
65
65
  Example:
66
66
  ```python
67
- config = WebSocketConfig(host="0.0.0.0", port=8765)
67
+ config = WebSocketConfig(host="0.0.0.0", port=60888)
68
68
 
69
69
  async with WebSocketServer(config, mcp_manager) as server:
70
70
  await server.serve_forever()
@@ -32,6 +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
+ active_skills: list[str] = field(default_factory=list)
36
+ """List of skill names that were active/injected during the session."""
35
37
 
36
38
 
37
39
  class TranscriptAnalyzer:
@@ -11,7 +11,7 @@ import logging
11
11
  import os
12
12
  from typing import Any
13
13
 
14
- from gobby.config.app import SessionLifecycleConfig
14
+ from gobby.config.sessions import SessionLifecycleConfig
15
15
  from gobby.sessions.transcripts.claude import ClaudeTranscriptParser
16
16
  from gobby.sessions.transcripts.codex import CodexTranscriptParser
17
17
  from gobby.sessions.transcripts.gemini import GeminiTranscriptParser
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from gobby.servers.websocket import WebSocketServer
15
+ from gobby.storage.sessions import LocalSessionManager
15
16
 
16
17
  from gobby.sessions.transcripts import get_parser
17
18
  from gobby.sessions.transcripts.base import TranscriptParser
@@ -36,11 +37,13 @@ class SessionMessageProcessor:
36
37
  db: DatabaseProtocol,
37
38
  poll_interval: float = 2.0,
38
39
  websocket_server: "WebSocketServer | None" = None,
40
+ session_manager: "LocalSessionManager | None" = None,
39
41
  ):
40
42
  self.db = db
41
43
  self.message_manager = LocalSessionMessageManager(db)
42
44
  self.poll_interval = poll_interval
43
45
  self.websocket_server: WebSocketServer | None = websocket_server
46
+ self.session_manager: LocalSessionManager | None = session_manager
44
47
 
45
48
  # Track active sessions: session_id -> transcript_path
46
49
  self._active_sessions: dict[str, str] = {}
@@ -196,6 +199,13 @@ class SessionMessageProcessor:
196
199
  # Store messages
197
200
  await self.message_manager.store_messages(session_id, parsed_messages)
198
201
 
202
+ # Extract and store model from parsed messages (if present)
203
+ if self.session_manager:
204
+ for msg in parsed_messages:
205
+ if msg.model:
206
+ self.session_manager.update_model(session_id, msg.model)
207
+ break # Only need the first model found
208
+
199
209
  # Broadcast new messages
200
210
  if self.websocket_server:
201
211
  for msg in parsed_messages:
@@ -39,6 +39,8 @@ class ParsedMessage:
39
39
  timestamp: datetime
40
40
  raw_json: dict[str, Any]
41
41
  usage: TokenUsage | None = None
42
+ tool_use_id: str | None = None
43
+ model: str | None = None
42
44
 
43
45
 
44
46
  @runtime_checkable
@@ -123,7 +123,11 @@ class ClaudeTranscriptParser:
123
123
 
124
124
  # If no /clear found, just take the last max_turns
125
125
  if most_recent_clear_idx is None:
126
- return turns[-max_turns:] if len(turns) > max_turns else turns
126
+ result = turns[-max_turns:] if len(turns) > max_turns else turns
127
+ result, removed = self._validate_tool_pairing(result)
128
+ if removed:
129
+ self.logger.debug(f"Removed {len(removed)} orphaned tool_results: {removed}")
130
+ return result
127
131
 
128
132
  # Start after this /clear (which is the last in any cluster since we scanned backwards)
129
133
  start_idx = most_recent_clear_idx + 1
@@ -159,7 +163,11 @@ class ClaudeTranscriptParser:
159
163
  start_idx = max(start_idx, boundary_idx + 1)
160
164
  break
161
165
 
162
- return turns[start_idx:end_idx]
166
+ result = turns[start_idx:end_idx]
167
+ result, removed = self._validate_tool_pairing(result)
168
+ if removed:
169
+ self.logger.debug(f"Removed {len(removed)} orphaned tool_results: {removed}")
170
+ return result
163
171
 
164
172
  # Segment is > max_turns, so we need to limit it
165
173
  # Take the last max_turns from the segment
@@ -184,7 +192,11 @@ class ClaudeTranscriptParser:
184
192
  start_idx = boundary_idx + 1
185
193
  break
186
194
 
187
- return turns[start_idx:end_idx]
195
+ result = turns[start_idx:end_idx]
196
+ result, removed = self._validate_tool_pairing(result)
197
+ if removed:
198
+ self.logger.debug(f"Removed {len(removed)} orphaned tool_results: {removed}")
199
+ return result
188
200
 
189
201
  def is_session_boundary(self, turn: dict[str, Any]) -> bool:
190
202
  """
@@ -212,6 +224,51 @@ class ClaudeTranscriptParser:
212
224
  # Check for /clear command marker
213
225
  return "<command-name>/clear</command-name>" in str(content)
214
226
 
227
+ def _validate_tool_pairing(
228
+ self, turns: list[dict[str, Any]]
229
+ ) -> tuple[list[dict[str, Any]], list[str]]:
230
+ """Remove orphaned tool_results that reference missing tool_use blocks.
231
+
232
+ This prevents Claude API validation errors when truncation cuts between
233
+ a tool_use and its corresponding tool_result.
234
+
235
+ Args:
236
+ turns: List of transcript turns to validate
237
+
238
+ Returns:
239
+ Tuple of (cleaned turns, list of removed tool_use_ids)
240
+ """
241
+ # Collect valid tool_use_ids from assistant messages
242
+ valid_ids: set[str] = set()
243
+ for turn in turns:
244
+ content = turn.get("message", {}).get("content", [])
245
+ if isinstance(content, list):
246
+ for block in content:
247
+ if isinstance(block, dict) and block.get("type") == "tool_use":
248
+ if tid := block.get("id"):
249
+ valid_ids.add(tid)
250
+
251
+ # Filter orphaned tool_results from user messages
252
+ cleaned: list[dict[str, Any]] = []
253
+ removed: list[str] = []
254
+ for turn in turns:
255
+ msg = turn.get("message", {})
256
+ content = msg.get("content", [])
257
+ if isinstance(content, list):
258
+ new_content: list[Any] = []
259
+ for block in content:
260
+ if isinstance(block, dict) and block.get("type") == "tool_result":
261
+ tid = block.get("tool_use_id")
262
+ if tid and tid not in valid_ids:
263
+ removed.append(tid)
264
+ continue
265
+ new_content.append(block)
266
+ if new_content != content:
267
+ turn = {**turn, "message": {**msg, "content": new_content}}
268
+ cleaned.append(turn)
269
+
270
+ return cleaned, removed
271
+
215
272
  def parse_line(self, line: str, index: int) -> ParsedMessage | None:
216
273
  """
217
274
  Parse a single line from the transcript JSONL.
@@ -247,6 +304,7 @@ class ClaudeTranscriptParser:
247
304
  tool_name = None
248
305
  tool_input = None
249
306
  tool_result = None
307
+ tool_use_id = None
250
308
 
251
309
  if msg_type == "user":
252
310
  role = "user"
@@ -274,8 +332,7 @@ class ClaudeTranscriptParser:
274
332
  content_type = "tool_use"
275
333
  tool_name = block.get("name")
276
334
  tool_input = block.get("input")
277
- # We capture the tool use ID as content if needed,
278
- # but for now we append nothing to text content
335
+ tool_use_id = block.get("id")
279
336
 
280
337
  elif block_type == "tool_result":
281
338
  content_type = "tool_result"
@@ -291,12 +348,15 @@ class ClaudeTranscriptParser:
291
348
  content_type = "tool_result"
292
349
  tool_name = data.get("tool_name")
293
350
  tool_result = data.get("result")
351
+ tool_use_id = data.get("tool_use_id")
294
352
  content = str(tool_result)
295
353
 
296
354
  else:
297
355
  # Skip unknown message types (e.g., 'progress', 'error' internal events)
298
356
  return None
299
357
 
358
+ usage, model = self._extract_usage(data)
359
+
300
360
  return ParsedMessage(
301
361
  index=index,
302
362
  role=role,
@@ -307,11 +367,20 @@ class ClaudeTranscriptParser:
307
367
  tool_result=tool_result,
308
368
  timestamp=timestamp,
309
369
  raw_json=data,
310
- usage=self._extract_usage(data),
370
+ usage=usage,
371
+ tool_use_id=tool_use_id,
372
+ model=model,
311
373
  )
312
374
 
313
- def _extract_usage(self, data: dict[str, Any]) -> TokenUsage | None:
314
- """Extract token usage from message data."""
375
+ def _extract_usage(self, data: dict[str, Any]) -> tuple[TokenUsage | None, str | None]:
376
+ """Extract token usage and model from message data.
377
+
378
+ Returns:
379
+ Tuple of (TokenUsage | None, model string | None)
380
+ """
381
+ # Extract model from message object
382
+ model = data.get("message", {}).get("model")
383
+
315
384
  # Check for top-level usage field (some formats)
316
385
  usage_data = data.get("usage")
317
386
 
@@ -320,7 +389,7 @@ class ClaudeTranscriptParser:
320
389
  usage_data = data.get("message", {}).get("usage")
321
390
 
322
391
  if not usage_data:
323
- return None
392
+ return None, model
324
393
 
325
394
  # Use explicit presence checks to handle 0 correctly
326
395
  input_tokens = (
@@ -354,7 +423,7 @@ class ClaudeTranscriptParser:
354
423
  cache_creation_tokens=cache_creation_tokens,
355
424
  cache_read_tokens=cache_read_tokens,
356
425
  total_cost_usd=total_cost_usd,
357
- )
426
+ ), model
358
427
 
359
428
  def parse_lines(self, lines: list[str], start_index: int = 0) -> list[ParsedMessage]:
360
429
  """
@@ -0,0 +1,91 @@
1
+ """Skills module for Agent Skills spec compliant skill management.
2
+
3
+ This module provides:
4
+ - YAML frontmatter parsing for SKILL.md files
5
+ - Validation against Agent Skills specification
6
+ - Search integration (TF-IDF + optional embeddings via UnifiedSearcher)
7
+ - Skill loading from filesystem, GitHub, and ZIP archives
8
+ - Skill updates from source
9
+ """
10
+
11
+ # Embedding utilities are now in gobby.search
12
+ from gobby.search import (
13
+ generate_embedding,
14
+ generate_embeddings,
15
+ is_embedding_available,
16
+ )
17
+ from gobby.skills.loader import (
18
+ GitHubRef,
19
+ SkillLoader,
20
+ SkillLoadError,
21
+ clone_skill_repo,
22
+ extract_zip,
23
+ parse_github_url,
24
+ )
25
+ from gobby.skills.manager import SkillManager
26
+ from gobby.skills.parser import (
27
+ ParsedSkill,
28
+ SkillParseError,
29
+ parse_frontmatter,
30
+ parse_skill_file,
31
+ parse_skill_text,
32
+ )
33
+ from gobby.skills.search import (
34
+ SearchFilters,
35
+ SkillSearch,
36
+ SkillSearchResult,
37
+ )
38
+ from gobby.skills.updater import (
39
+ SkillUpdateError,
40
+ SkillUpdater,
41
+ SkillUpdateResult,
42
+ )
43
+ from gobby.skills.validator import (
44
+ SkillValidator,
45
+ ValidationResult,
46
+ validate_skill_category,
47
+ validate_skill_compatibility,
48
+ validate_skill_description,
49
+ validate_skill_name,
50
+ validate_skill_tags,
51
+ validate_skill_version,
52
+ )
53
+
54
+ __all__ = [
55
+ # Embeddings (from gobby.search)
56
+ "generate_embedding",
57
+ "generate_embeddings",
58
+ "is_embedding_available",
59
+ # Loader
60
+ "GitHubRef",
61
+ "SkillLoadError",
62
+ "SkillLoader",
63
+ "clone_skill_repo",
64
+ "extract_zip",
65
+ "parse_github_url",
66
+ # Manager
67
+ "SkillManager",
68
+ # Updater
69
+ "SkillUpdateError",
70
+ "SkillUpdateResult",
71
+ "SkillUpdater",
72
+ # Parser
73
+ "ParsedSkill",
74
+ "SkillParseError",
75
+ "parse_frontmatter",
76
+ "parse_skill_file",
77
+ "parse_skill_text",
78
+ # Search
79
+ "SearchFilters",
80
+ "SkillSearch",
81
+ "SkillSearchResult",
82
+ # Validator
83
+ "SkillValidator",
84
+ "ValidationResult",
85
+ "validate_skill_category",
86
+ "validate_skill_compatibility",
87
+ "validate_skill_description",
88
+ "validate_skill_name",
89
+ "validate_skill_tags",
90
+ "validate_skill_version",
91
+ ]