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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- 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
|
+
}
|