gobby 0.2.5__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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,381 @@
1
+ """Schema hash management for incremental tool re-indexing.
2
+
3
+ Tracks tool schema hashes to detect changes and enable incremental
4
+ updates rather than full re-indexing.
5
+ """
6
+
7
+ import hashlib
8
+ import json
9
+ import logging
10
+ from dataclasses import dataclass
11
+ from datetime import UTC, datetime
12
+ from typing import Any
13
+
14
+ from gobby.storage.database import DatabaseProtocol
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def compute_schema_hash(input_schema: dict[str, Any] | None) -> str:
20
+ """
21
+ Compute a deterministic hash of a tool's input schema.
22
+
23
+ Uses canonical JSON serialization to ensure consistent hashing
24
+ regardless of key ordering.
25
+
26
+ Args:
27
+ input_schema: Tool's inputSchema as a dictionary
28
+
29
+ Returns:
30
+ 16-character hex hash of the schema
31
+ """
32
+ if input_schema is None:
33
+ return hashlib.sha256(b"null").hexdigest()[:16]
34
+
35
+ # Use canonical JSON for deterministic serialization
36
+ canonical = json.dumps(input_schema, sort_keys=True, separators=(",", ":"))
37
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
38
+
39
+
40
+ @dataclass
41
+ class SchemaHashRecord:
42
+ """A stored schema hash record."""
43
+
44
+ id: int
45
+ server_name: str
46
+ tool_name: str
47
+ project_id: str
48
+ schema_hash: str
49
+ last_verified_at: str
50
+ created_at: str
51
+ updated_at: str
52
+
53
+ @classmethod
54
+ def from_row(cls, row: Any) -> "SchemaHashRecord":
55
+ """Create from database row."""
56
+ return cls(
57
+ id=row["id"],
58
+ server_name=row["server_name"],
59
+ tool_name=row["tool_name"],
60
+ project_id=row["project_id"],
61
+ schema_hash=row["schema_hash"],
62
+ last_verified_at=row["last_verified_at"],
63
+ created_at=row["created_at"],
64
+ updated_at=row["updated_at"],
65
+ )
66
+
67
+ def to_dict(self) -> dict[str, Any]:
68
+ """Convert to dictionary."""
69
+ return {
70
+ "id": self.id,
71
+ "server_name": self.server_name,
72
+ "tool_name": self.tool_name,
73
+ "project_id": self.project_id,
74
+ "schema_hash": self.schema_hash,
75
+ "last_verified_at": self.last_verified_at,
76
+ "created_at": self.created_at,
77
+ "updated_at": self.updated_at,
78
+ }
79
+
80
+
81
+ class SchemaHashManager:
82
+ """
83
+ Manages tool schema hashes for incremental re-indexing.
84
+
85
+ Tracks schema hashes to detect when tool definitions change,
86
+ enabling efficient incremental updates instead of full re-indexing.
87
+ """
88
+
89
+ def __init__(self, db: DatabaseProtocol):
90
+ """
91
+ Initialize the schema hash manager.
92
+
93
+ Args:
94
+ db: LocalDatabase instance for persistence
95
+ """
96
+ self.db = db
97
+
98
+ def store_hash(
99
+ self,
100
+ server_name: str,
101
+ tool_name: str,
102
+ project_id: str,
103
+ schema_hash: str,
104
+ ) -> SchemaHashRecord:
105
+ """
106
+ Store or update a schema hash.
107
+
108
+ Uses UPSERT to handle both new and existing records.
109
+
110
+ Args:
111
+ server_name: Name of the MCP server
112
+ tool_name: Name of the tool
113
+ project_id: Project ID
114
+ schema_hash: Computed schema hash
115
+
116
+ Returns:
117
+ The stored SchemaHashRecord
118
+ """
119
+ now = datetime.now(UTC).isoformat()
120
+
121
+ self.db.execute(
122
+ """
123
+ INSERT INTO tool_schema_hashes (
124
+ server_name, tool_name, project_id, schema_hash,
125
+ last_verified_at, created_at, updated_at
126
+ )
127
+ VALUES (?, ?, ?, ?, ?, ?, ?)
128
+ ON CONFLICT(project_id, server_name, tool_name) DO UPDATE SET
129
+ schema_hash = excluded.schema_hash,
130
+ last_verified_at = excluded.last_verified_at,
131
+ updated_at = excluded.updated_at
132
+ """,
133
+ (server_name, tool_name, project_id, schema_hash, now, now, now),
134
+ )
135
+
136
+ result = self.get_hash(server_name, tool_name, project_id)
137
+ if result is None:
138
+ raise RuntimeError(f"Failed to retrieve hash for {server_name}/{tool_name} after store")
139
+ return result
140
+
141
+ def get_hash(
142
+ self, server_name: str, tool_name: str, project_id: str
143
+ ) -> SchemaHashRecord | None:
144
+ """
145
+ Get stored hash for a tool.
146
+
147
+ Args:
148
+ server_name: Server name
149
+ tool_name: Tool name
150
+ project_id: Project ID
151
+
152
+ Returns:
153
+ SchemaHashRecord or None if not found
154
+ """
155
+ row = self.db.fetchone(
156
+ """
157
+ SELECT * FROM tool_schema_hashes
158
+ WHERE project_id = ? AND server_name = ? AND tool_name = ?
159
+ """,
160
+ (project_id, server_name, tool_name),
161
+ )
162
+ return SchemaHashRecord.from_row(row) if row else None
163
+
164
+ def get_hashes_for_server(self, server_name: str, project_id: str) -> list[SchemaHashRecord]:
165
+ """
166
+ Get all hashes for a server.
167
+
168
+ Args:
169
+ server_name: Server name
170
+ project_id: Project ID
171
+
172
+ Returns:
173
+ List of SchemaHashRecord
174
+ """
175
+ rows = self.db.fetchall(
176
+ """
177
+ SELECT * FROM tool_schema_hashes
178
+ WHERE project_id = ? AND server_name = ?
179
+ """,
180
+ (project_id, server_name),
181
+ )
182
+ return [SchemaHashRecord.from_row(row) for row in rows]
183
+
184
+ def needs_reindexing(
185
+ self,
186
+ server_name: str,
187
+ tool_name: str,
188
+ project_id: str,
189
+ current_schema: dict[str, Any] | None,
190
+ ) -> bool:
191
+ """
192
+ Check if a tool needs re-indexing based on schema hash.
193
+
194
+ Args:
195
+ server_name: Server name
196
+ tool_name: Tool name
197
+ project_id: Project ID
198
+ current_schema: Current tool inputSchema
199
+
200
+ Returns:
201
+ True if schema is missing or changed
202
+ """
203
+ stored = self.get_hash(server_name, tool_name, project_id)
204
+ if not stored:
205
+ return True
206
+
207
+ current_hash = compute_schema_hash(current_schema)
208
+ return stored.schema_hash != current_hash
209
+
210
+ def check_tools_for_changes(
211
+ self,
212
+ server_name: str,
213
+ project_id: str,
214
+ tools: list[dict[str, Any]],
215
+ ) -> dict[str, Any]:
216
+ """
217
+ Check multiple tools for schema changes.
218
+
219
+ Args:
220
+ server_name: Server name
221
+ project_id: Project ID
222
+ tools: List of tool dicts with 'name' and 'inputSchema' keys
223
+
224
+ Returns:
225
+ Dict with 'changed', 'unchanged', 'new' tool lists
226
+ """
227
+ result: dict[str, list[str]] = {
228
+ "changed": [],
229
+ "unchanged": [],
230
+ "new": [],
231
+ }
232
+
233
+ # Get all stored hashes for this server
234
+ stored_hashes = {
235
+ h.tool_name: h.schema_hash for h in self.get_hashes_for_server(server_name, project_id)
236
+ }
237
+
238
+ for tool in tools:
239
+ tool_name = tool.get("name", "")
240
+ schema = tool.get("inputSchema") or tool.get("input_schema")
241
+ current_hash = compute_schema_hash(schema)
242
+
243
+ if tool_name not in stored_hashes:
244
+ result["new"].append(tool_name)
245
+ elif stored_hashes[tool_name] != current_hash:
246
+ result["changed"].append(tool_name)
247
+ else:
248
+ result["unchanged"].append(tool_name)
249
+
250
+ return result
251
+
252
+ def update_verification_time(self, server_name: str, tool_name: str, project_id: str) -> bool:
253
+ """
254
+ Update last_verified_at timestamp without changing hash.
255
+
256
+ Useful for marking a hash as still valid after verification.
257
+
258
+ Args:
259
+ server_name: Server name
260
+ tool_name: Tool name
261
+ project_id: Project ID
262
+
263
+ Returns:
264
+ True if updated, False if not found
265
+ """
266
+ now = datetime.now(UTC).isoformat()
267
+ cursor = self.db.execute(
268
+ """
269
+ UPDATE tool_schema_hashes
270
+ SET last_verified_at = ?, updated_at = ?
271
+ WHERE project_id = ? AND server_name = ? AND tool_name = ?
272
+ """,
273
+ (now, now, project_id, server_name, tool_name),
274
+ )
275
+ return cursor.rowcount > 0
276
+
277
+ def delete_hash(self, server_name: str, tool_name: str, project_id: str) -> bool:
278
+ """
279
+ Delete a schema hash.
280
+
281
+ Args:
282
+ server_name: Server name
283
+ tool_name: Tool name
284
+ project_id: Project ID
285
+
286
+ Returns:
287
+ True if deleted, False if not found
288
+ """
289
+ cursor = self.db.execute(
290
+ """
291
+ DELETE FROM tool_schema_hashes
292
+ WHERE project_id = ? AND server_name = ? AND tool_name = ?
293
+ """,
294
+ (project_id, server_name, tool_name),
295
+ )
296
+ return cursor.rowcount > 0
297
+
298
+ def delete_hashes_for_server(self, server_name: str, project_id: str) -> int:
299
+ """
300
+ Delete all hashes for a server.
301
+
302
+ Args:
303
+ server_name: Server name
304
+ project_id: Project ID
305
+
306
+ Returns:
307
+ Number of rows deleted
308
+ """
309
+ cursor = self.db.execute(
310
+ """
311
+ DELETE FROM tool_schema_hashes
312
+ WHERE project_id = ? AND server_name = ?
313
+ """,
314
+ (project_id, server_name),
315
+ )
316
+ return cursor.rowcount
317
+
318
+ def cleanup_stale_hashes(
319
+ self, server_name: str, project_id: str, valid_tool_names: list[str]
320
+ ) -> int:
321
+ """
322
+ Remove hashes for tools that no longer exist on server.
323
+
324
+ Args:
325
+ server_name: Server name
326
+ project_id: Project ID
327
+ valid_tool_names: List of tool names that still exist
328
+
329
+ Returns:
330
+ Number of stale hashes deleted
331
+ """
332
+ if not valid_tool_names:
333
+ return self.delete_hashes_for_server(server_name, project_id)
334
+
335
+ # Build placeholders for IN clause
336
+ placeholders = ",".join("?" for _ in valid_tool_names)
337
+ # nosec B608: placeholders are just '?' characters, values parameterized
338
+ cursor = self.db.execute(
339
+ f"DELETE FROM tool_schema_hashes WHERE project_id = ? AND server_name = ? AND tool_name NOT IN ({placeholders})", # nosec B608
340
+ (project_id, server_name, *valid_tool_names),
341
+ )
342
+ return cursor.rowcount
343
+
344
+ def get_stats(self, project_id: str | None = None) -> dict[str, Any]:
345
+ """
346
+ Get statistics about stored schema hashes.
347
+
348
+ Args:
349
+ project_id: Optional project filter
350
+
351
+ Returns:
352
+ Dict with count, by_server breakdown
353
+ """
354
+ if project_id:
355
+ count_row = self.db.fetchone(
356
+ "SELECT COUNT(*) as count FROM tool_schema_hashes WHERE project_id = ?",
357
+ (project_id,),
358
+ )
359
+ server_rows = self.db.fetchall(
360
+ """
361
+ SELECT server_name, COUNT(*) as count
362
+ FROM tool_schema_hashes
363
+ WHERE project_id = ?
364
+ GROUP BY server_name
365
+ """,
366
+ (project_id,),
367
+ )
368
+ else:
369
+ count_row = self.db.fetchone("SELECT COUNT(*) as count FROM tool_schema_hashes")
370
+ server_rows = self.db.fetchall(
371
+ """
372
+ SELECT server_name, COUNT(*) as count
373
+ FROM tool_schema_hashes
374
+ GROUP BY server_name
375
+ """
376
+ )
377
+
378
+ return {
379
+ "total_hashes": count_row["count"] if count_row else 0,
380
+ "by_server": {row["server_name"]: row["count"] for row in server_rows},
381
+ }