@archznn/crewloop-skills 0.6.0 → 0.7.0
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.
- package/README.md +4 -16
- package/package.json +1 -2
- package/packages/cli/dist/agents.js +1 -1
- package/packages/cli/dist/agents.js.map +1 -1
- package/packages/cli/dist/cli.d.ts.map +1 -1
- package/packages/cli/dist/cli.js +2 -30
- package/packages/cli/dist/cli.js.map +1 -1
- package/packages/cli/dist/hooks.d.ts +6 -4
- package/packages/cli/dist/hooks.d.ts.map +1 -1
- package/packages/cli/dist/hooks.js +250 -98
- package/packages/cli/dist/hooks.js.map +1 -1
- package/packages/cli/dist/tests/hooks.test.js +245 -33
- package/packages/cli/dist/tests/hooks.test.js.map +1 -1
- package/references/conventions.md +1 -10
- package/references/workflow.md +1 -1
- package/servers/dashboard/README.md +55 -1
- package/servers/dashboard/dist/adapters/agy.d.ts +19 -0
- package/servers/dashboard/dist/adapters/agy.d.ts.map +1 -0
- package/servers/dashboard/dist/adapters/agy.js +108 -0
- package/servers/dashboard/dist/adapters/agy.js.map +1 -0
- package/servers/dashboard/dist/adapters/codex.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/codex.js +2 -0
- package/servers/dashboard/dist/adapters/codex.js.map +1 -1
- package/servers/dashboard/dist/adapters/kimi.d.ts +1 -1
- package/servers/dashboard/dist/adapters/kimi.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/kimi.js +9 -0
- package/servers/dashboard/dist/adapters/kimi.js.map +1 -1
- package/servers/dashboard/dist/adapters/shim.d.ts +1 -1
- package/servers/dashboard/dist/adapters/shim.d.ts.map +1 -1
- package/servers/dashboard/dist/adapters/shim.js +32 -11
- package/servers/dashboard/dist/adapters/shim.js.map +1 -1
- package/servers/dashboard/dist/adapters/shim.test.js +46 -4
- package/servers/dashboard/dist/adapters/shim.test.js.map +1 -1
- package/servers/dashboard/dist/lib/constants.d.ts +5 -0
- package/servers/dashboard/dist/lib/constants.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/constants.js +46 -0
- package/servers/dashboard/dist/lib/constants.js.map +1 -0
- package/servers/dashboard/dist/lib/format.d.ts +6 -0
- package/servers/dashboard/dist/lib/format.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/format.js +52 -0
- package/servers/dashboard/dist/lib/format.js.map +1 -0
- package/servers/dashboard/dist/lib/graph.d.ts +22 -0
- package/servers/dashboard/dist/lib/graph.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/graph.js +45 -0
- package/servers/dashboard/dist/lib/graph.js.map +1 -0
- package/servers/dashboard/dist/lib/invocations.d.ts +32 -0
- package/servers/dashboard/dist/lib/invocations.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/invocations.js +135 -0
- package/servers/dashboard/dist/lib/invocations.js.map +1 -0
- package/servers/dashboard/dist/lib/invocations.test.d.ts +2 -0
- package/servers/dashboard/dist/lib/invocations.test.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/invocations.test.js +68 -0
- package/servers/dashboard/dist/lib/invocations.test.js.map +1 -0
- package/servers/dashboard/dist/lib/paths.d.ts +2 -0
- package/servers/dashboard/dist/lib/paths.d.ts.map +1 -0
- package/servers/dashboard/dist/lib/paths.js +40 -0
- package/servers/dashboard/dist/lib/paths.js.map +1 -0
- package/servers/dashboard/dist/presenter.d.ts.map +1 -1
- package/servers/dashboard/dist/presenter.js +2 -0
- package/servers/dashboard/dist/presenter.js.map +1 -1
- package/servers/dashboard/dist/public/assets/index-DjmMKbPN.css +1 -0
- package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js +5323 -0
- package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js.map +1 -0
- package/servers/dashboard/dist/public/index.html +16 -0
- package/servers/dashboard/dist/server.d.ts.map +1 -1
- package/servers/dashboard/dist/server.js +5 -1
- package/servers/dashboard/dist/server.js.map +1 -1
- package/servers/dashboard/dist/skills/infer.d.ts.map +1 -1
- package/servers/dashboard/dist/skills/infer.js +0 -6
- package/servers/dashboard/dist/skills/infer.js.map +1 -1
- package/servers/dashboard/dist/skills/infer.test.js +10 -3
- package/servers/dashboard/dist/skills/infer.test.js.map +1 -1
- package/servers/dashboard/dist/skills/mapping.d.ts +0 -3
- package/servers/dashboard/dist/skills/mapping.d.ts.map +1 -1
- package/servers/dashboard/dist/skills/mapping.js +0 -18
- package/servers/dashboard/dist/skills/mapping.js.map +1 -1
- package/servers/dashboard/dist/skills/registry.d.ts.map +1 -1
- package/servers/dashboard/dist/skills/registry.js +0 -1
- package/servers/dashboard/dist/skills/registry.js.map +1 -1
- package/servers/dashboard/dist/tests/adapters.test.d.ts +2 -0
- package/servers/dashboard/dist/tests/adapters.test.d.ts.map +1 -0
- package/servers/dashboard/dist/tests/adapters.test.js +180 -0
- package/servers/dashboard/dist/tests/adapters.test.js.map +1 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.d.ts +2 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.d.ts.map +1 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.js +123 -0
- package/servers/dashboard/dist/tests/lib-helpers.test.js.map +1 -0
- package/servers/dashboard/dist/tests/shim.test.js +88 -2
- package/servers/dashboard/dist/tests/shim.test.js.map +1 -1
- package/servers/dashboard/dist/types.d.ts +5 -2
- package/servers/dashboard/dist/types.d.ts.map +1 -1
- package/servers/dashboard/package.json +22 -5
- package/servers/dashboard/src/adapters/agy.ts +136 -0
- package/servers/dashboard/src/adapters/codex.ts +2 -0
- package/servers/dashboard/src/adapters/kimi.ts +11 -1
- package/servers/dashboard/src/adapters/shim.test.ts +57 -4
- package/servers/dashboard/src/adapters/shim.ts +31 -11
- package/servers/dashboard/src/lib/constants.ts +44 -0
- package/servers/dashboard/src/lib/format.ts +44 -0
- package/servers/dashboard/src/lib/graph.ts +69 -0
- package/servers/dashboard/src/lib/invocations.test.ts +70 -0
- package/servers/dashboard/src/lib/invocations.ts +172 -0
- package/servers/dashboard/src/lib/paths.ts +35 -0
- package/servers/dashboard/src/presenter.ts +2 -0
- package/servers/dashboard/src/server.ts +5 -1
- package/servers/dashboard/src/skills/infer.test.ts +11 -3
- package/servers/dashboard/src/skills/infer.ts +1 -8
- package/servers/dashboard/src/skills/mapping.ts +0 -20
- package/servers/dashboard/src/skills/registry.ts +0 -1
- package/servers/dashboard/src/tests/adapters.test.ts +198 -0
- package/servers/dashboard/src/tests/lib-helpers.test.ts +133 -0
- package/servers/dashboard/src/tests/shim.test.ts +110 -2
- package/servers/dashboard/src/types.ts +5 -3
- package/servers/dashboard/ui/index.html +15 -0
- package/servers/dashboard/ui/postcss.config.js +6 -0
- package/servers/dashboard/ui/src/App.tsx +360 -0
- package/servers/dashboard/ui/src/components/ActiveSkillPanel.tsx +69 -0
- package/servers/dashboard/ui/src/components/ActivityGraph.tsx +74 -0
- package/servers/dashboard/ui/src/components/CommandPalette.tsx +200 -0
- package/servers/dashboard/ui/src/components/FileActivity.tsx +20 -0
- package/servers/dashboard/ui/src/components/FileDiff.tsx +68 -0
- package/servers/dashboard/ui/src/components/FileList.tsx +64 -0
- package/servers/dashboard/ui/src/components/FilterBar.tsx +208 -0
- package/servers/dashboard/ui/src/components/Network3D.tsx +178 -0
- package/servers/dashboard/ui/src/components/SessionSelector.tsx +95 -0
- package/servers/dashboard/ui/src/components/Sidebar.tsx +110 -0
- package/servers/dashboard/ui/src/components/TelemetryPanel.tsx +57 -0
- package/servers/dashboard/ui/src/components/Timeline.tsx +57 -0
- package/servers/dashboard/ui/src/components/TimelineRow.tsx +112 -0
- package/servers/dashboard/ui/src/components/TopBar.tsx +116 -0
- package/servers/dashboard/ui/src/components/ViewHeader.tsx +19 -0
- package/servers/dashboard/ui/src/components/ui/Icon.tsx +105 -0
- package/servers/dashboard/ui/src/components/ui/StatusBadge.tsx +19 -0
- package/servers/dashboard/ui/src/components/views/FilesView.tsx +23 -0
- package/servers/dashboard/ui/src/components/views/NetworkView.tsx +20 -0
- package/servers/dashboard/ui/src/components/views/Overview.tsx +135 -0
- package/servers/dashboard/ui/src/components/views/SessionsView.tsx +84 -0
- package/servers/dashboard/ui/src/components/views/SettingsView.tsx +138 -0
- package/servers/dashboard/ui/src/components/views/SkillsView.tsx +92 -0
- package/servers/dashboard/ui/src/components/views/TimelineView.tsx +46 -0
- package/servers/dashboard/ui/src/contexts/FilterContext.tsx +41 -0
- package/servers/dashboard/ui/src/contexts/PinnedSessionsContext.tsx +80 -0
- package/servers/dashboard/ui/src/contexts/SettingsContext.tsx +60 -0
- package/servers/dashboard/ui/src/hooks/useCommandPalette.ts +36 -0
- package/servers/dashboard/ui/src/hooks/useKeyboardShortcut.ts +38 -0
- package/servers/dashboard/ui/src/hooks/useNow.ts +12 -0
- package/servers/dashboard/ui/src/hooks/useReducedMotion.ts +15 -0
- package/servers/dashboard/ui/src/hooks/useSessions.ts +64 -0
- package/servers/dashboard/ui/src/hooks/useTheme.ts +30 -0
- package/servers/dashboard/ui/src/hooks/useViewport.ts +19 -0
- package/servers/dashboard/ui/src/hooks/useWebSocket.ts +118 -0
- package/servers/dashboard/ui/src/lib/export.test.ts +33 -0
- package/servers/dashboard/ui/src/lib/export.ts +39 -0
- package/servers/dashboard/ui/src/lib/filter.test.ts +95 -0
- package/servers/dashboard/ui/src/lib/filter.ts +178 -0
- package/servers/dashboard/ui/src/lib/format.test.ts +25 -0
- package/servers/dashboard/ui/src/lib/search.test.ts +52 -0
- package/servers/dashboard/ui/src/lib/search.ts +60 -0
- package/servers/dashboard/ui/src/lib/settings.test.ts +50 -0
- package/servers/dashboard/ui/src/lib/settings.ts +56 -0
- package/servers/dashboard/ui/src/lib/types.ts +124 -0
- package/servers/dashboard/ui/src/main.tsx +19 -0
- package/servers/dashboard/ui/src/styles/index.css +155 -0
- package/servers/dashboard/ui/tailwind.config.js +45 -0
- package/servers/dashboard/ui/tsconfig.json +33 -0
- package/servers/dashboard/ui/tsconfig.node.json +10 -0
- package/servers/dashboard/ui/vite.config.ts +37 -0
- package/servers/dashboard/ui/vitest.config.ts +8 -0
- package/skills/accessibility-auditor/SKILL.md +0 -20
- package/skills/architect/SKILL.md +0 -45
- package/skills/designer/SKILL.md +0 -30
- package/skills/docs-writer/SKILL.md +0 -13
- package/skills/engineer/SKILL.md +0 -30
- package/skills/maintainer/SKILL.md +0 -20
- package/skills/orchestrator/SKILL.md +0 -13
- package/skills/product-manager/SKILL.md +0 -20
- package/skills/researcher/SKILL.md +0 -20
- package/skills/reviewer/SKILL.md +0 -30
- package/skills/security-guard/SKILL.md +0 -20
- package/skills/shipper/SKILL.md +0 -33
- package/skills/tester/SKILL.md +0 -20
- package/packages/cli/dist/mcp.d.ts +0 -28
- package/packages/cli/dist/mcp.d.ts.map +0 -1
- package/packages/cli/dist/mcp.js +0 -148
- package/packages/cli/dist/mcp.js.map +0 -1
- package/packages/cli/dist/tests/mcp.test.d.ts +0 -2
- package/packages/cli/dist/tests/mcp.test.d.ts.map +0 -1
- package/packages/cli/dist/tests/mcp.test.js +0 -232
- package/packages/cli/dist/tests/mcp.test.js.map +0 -1
- package/references/obsidian-mcp-usage.md +0 -190
- package/servers/dashboard/public/app.js +0 -516
- package/servers/dashboard/public/index.html +0 -96
- package/servers/dashboard/public/styles.css +0 -819
- package/servers/obsidian-mcp/README.md +0 -82
- package/servers/obsidian-mcp/pyproject.toml +0 -32
- package/servers/obsidian-mcp/src/obsidian_mcp/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/config.py +0 -47
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/embeddings.py +0 -105
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/indexer.py +0 -79
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/store.py +0 -141
- package/servers/obsidian-mcp/src/obsidian_mcp/indexer/sync.py +0 -37
- package/servers/obsidian-mcp/src/obsidian_mcp/learning/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/learning/detector.py +0 -66
- package/servers/obsidian-mcp/src/obsidian_mcp/learning/note_generator.py +0 -40
- package/servers/obsidian-mcp/src/obsidian_mcp/main.py +0 -4
- package/servers/obsidian-mcp/src/obsidian_mcp/models.py +0 -42
- package/servers/obsidian-mcp/src/obsidian_mcp/privacy/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/privacy/filter.py +0 -68
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/engine.py +0 -50
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/graph_search.py +0 -55
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/text_search.py +0 -37
- package/servers/obsidian-mcp/src/obsidian_mcp/rag/vector_search.py +0 -118
- package/servers/obsidian-mcp/src/obsidian_mcp/server.py +0 -61
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/create.py +0 -43
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/delete.py +0 -16
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/learn.py +0 -42
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/list.py +0 -16
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/read.py +0 -15
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/registry.py +0 -130
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/related.py +0 -20
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/search.py +0 -26
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/sync.py +0 -22
- package/servers/obsidian-mcp/src/obsidian_mcp/tools/update.py +0 -34
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/__init__.py +0 -0
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/parser.py +0 -82
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/repository.py +0 -68
- package/servers/obsidian-mcp/src/obsidian_mcp/vault/writer.py +0 -61
- package/servers/obsidian-mcp/tests/conftest.py +0 -39
- package/servers/obsidian-mcp/tests/test_async_tools.py +0 -87
- package/servers/obsidian-mcp/tests/test_edge_cases.py +0 -59
- package/servers/obsidian-mcp/tests/test_indexer.py +0 -27
- package/servers/obsidian-mcp/tests/test_integration.py +0 -90
- package/servers/obsidian-mcp/tests/test_learning.py +0 -34
- package/servers/obsidian-mcp/tests/test_privacy.py +0 -31
- package/servers/obsidian-mcp/tests/test_privacy_config.py +0 -44
- package/servers/obsidian-mcp/tests/test_rag.py +0 -64
- package/servers/obsidian-mcp/tests/test_read_raw.py +0 -37
- package/servers/obsidian-mcp/tests/test_tfidf_fallback.py +0 -54
- package/servers/obsidian-mcp/tests/test_tools.py +0 -108
- package/servers/obsidian-mcp/tests/test_vault.py +0 -103
- package/servers/obsidian-mcp/tests/test_writer.py +0 -139
- package/skills/obsidian-second-brain/SKILL.md +0 -298
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import re
|
|
3
|
-
|
|
4
|
-
from obsidian_mcp.config import Config
|
|
5
|
-
|
|
6
|
-
logger = logging.getLogger(__name__)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class PrivacyFilter:
|
|
10
|
-
_RULES = {
|
|
11
|
-
"api_keys": [
|
|
12
|
-
r"\b(API_KEY|SECRET|TOKEN|PASSWORD)\s*[=:]\s*\S+",
|
|
13
|
-
r"\b(?:sk|ghp|gho|ghu|ghs|ghr|pat|np|openai|anthropic)-[A-Za-z0-9_\-]{10,}\b",
|
|
14
|
-
],
|
|
15
|
-
"private_keys": [
|
|
16
|
-
r"\bPRIVATE_KEY\b",
|
|
17
|
-
r"-----BEGIN",
|
|
18
|
-
],
|
|
19
|
-
"env_files": [
|
|
20
|
-
r"\.env",
|
|
21
|
-
],
|
|
22
|
-
"emails": [
|
|
23
|
-
r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b",
|
|
24
|
-
],
|
|
25
|
-
"credit_cards": [
|
|
26
|
-
r"\b\d{4}[ -]\d{4}[ -]\d{4}[ -]\d{4}\b",
|
|
27
|
-
],
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
def __init__(self, config: Config | None = None):
|
|
31
|
-
self.config = config or Config()
|
|
32
|
-
self.privacy = self.config.privacy
|
|
33
|
-
self.patterns = self._compile_patterns()
|
|
34
|
-
|
|
35
|
-
def _compile_patterns(self) -> list[re.Pattern]:
|
|
36
|
-
raw_patterns: list[str] = []
|
|
37
|
-
if self.privacy.block_api_keys:
|
|
38
|
-
raw_patterns.extend(self._RULES["api_keys"])
|
|
39
|
-
if self.privacy.block_private_keys:
|
|
40
|
-
raw_patterns.extend(self._RULES["private_keys"])
|
|
41
|
-
if self.privacy.block_env_files:
|
|
42
|
-
raw_patterns.extend(self._RULES["env_files"])
|
|
43
|
-
if self.privacy.block_emails:
|
|
44
|
-
raw_patterns.extend(self._RULES["emails"])
|
|
45
|
-
if self.privacy.block_credit_cards:
|
|
46
|
-
raw_patterns.extend(self._RULES["credit_cards"])
|
|
47
|
-
if self.config.sensitive_patterns:
|
|
48
|
-
raw_patterns.extend(self.config.sensitive_patterns)
|
|
49
|
-
return [re.compile(p, re.IGNORECASE) for p in raw_patterns]
|
|
50
|
-
|
|
51
|
-
def _allowed(self, text: str) -> bool:
|
|
52
|
-
return any(allowed in text for allowed in self.privacy.allowed_strings)
|
|
53
|
-
|
|
54
|
-
def is_safe(self, text: str) -> bool:
|
|
55
|
-
if not self.privacy.enabled:
|
|
56
|
-
return True
|
|
57
|
-
if self._allowed(text):
|
|
58
|
-
return True
|
|
59
|
-
return not any(pattern.search(text) for pattern in self.patterns)
|
|
60
|
-
|
|
61
|
-
def validate(self, text: str) -> None:
|
|
62
|
-
if not self.privacy.enabled:
|
|
63
|
-
return
|
|
64
|
-
if self._allowed(text):
|
|
65
|
-
return
|
|
66
|
-
if not self.is_safe(text):
|
|
67
|
-
logger.warning("privacy filter blocked content")
|
|
68
|
-
raise ValueError("content blocked by privacy filter: sensitive data detected")
|
|
File without changes
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
from obsidian_mcp.config import Config
|
|
2
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
3
|
-
from obsidian_mcp.models import SearchResult
|
|
4
|
-
from obsidian_mcp.rag.graph_search import GraphSearch
|
|
5
|
-
from obsidian_mcp.rag.text_search import TextSearch
|
|
6
|
-
from obsidian_mcp.rag.vector_search import VectorSearch
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class RAGEngine:
|
|
10
|
-
def __init__(self, config: Config, store: IndexStore):
|
|
11
|
-
self.config = config
|
|
12
|
-
self.text_search = TextSearch(store)
|
|
13
|
-
self.vector_search = VectorSearch(store, config.embedding_model)
|
|
14
|
-
self.graph_search = GraphSearch(store)
|
|
15
|
-
|
|
16
|
-
@staticmethod
|
|
17
|
-
def _normalize_scores(results: list[SearchResult]) -> list[SearchResult]:
|
|
18
|
-
if len(results) < 2:
|
|
19
|
-
return results
|
|
20
|
-
scores = [r.score for r in results]
|
|
21
|
-
min_score = min(scores)
|
|
22
|
-
max_score = max(scores)
|
|
23
|
-
span = max_score - min_score
|
|
24
|
-
for result in results:
|
|
25
|
-
result.score = 1.0 if span == 0 else (result.score - min_score) / span
|
|
26
|
-
return results
|
|
27
|
-
|
|
28
|
-
def search(self, query: str, mode: str = "hybrid", limit: int = 10) -> list[SearchResult]:
|
|
29
|
-
results = []
|
|
30
|
-
if mode in ("text", "hybrid"):
|
|
31
|
-
results.extend(self._normalize_scores(self.text_search.search(query, limit=limit)))
|
|
32
|
-
if mode in ("vector", "hybrid"):
|
|
33
|
-
results.extend(self._normalize_scores(self.vector_search.search(query, limit=limit)))
|
|
34
|
-
if mode == "graph":
|
|
35
|
-
results.extend(self._normalize_scores(self.graph_search.search(query, limit=limit)))
|
|
36
|
-
|
|
37
|
-
by_note = {}
|
|
38
|
-
for result in results:
|
|
39
|
-
if result.note_path not in by_note:
|
|
40
|
-
by_note[result.note_path] = result
|
|
41
|
-
else:
|
|
42
|
-
by_note[result.note_path].score = max(
|
|
43
|
-
by_note[result.note_path].score, result.score
|
|
44
|
-
)
|
|
45
|
-
by_note[result.note_path].matched_chunks.extend(result.matched_chunks)
|
|
46
|
-
|
|
47
|
-
return sorted(by_note.values(), key=lambda r: r.score, reverse=True)[:limit]
|
|
48
|
-
|
|
49
|
-
def related(self, note_path: str, depth: int = 1) -> list[SearchResult]:
|
|
50
|
-
return self.graph_search.related(note_path, depth=depth)
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from collections import deque
|
|
3
|
-
|
|
4
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
5
|
-
from obsidian_mcp.models import SearchResult
|
|
6
|
-
|
|
7
|
-
logger = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class GraphSearch:
|
|
11
|
-
def __init__(self, store: IndexStore):
|
|
12
|
-
self.store = store
|
|
13
|
-
|
|
14
|
-
def related(self, note_path: str, depth: int = 1) -> list[SearchResult]:
|
|
15
|
-
edges = self.store.get_all_edges()
|
|
16
|
-
logger.debug("graph related search: %s depth=%d edges=%d", note_path, depth, len(edges))
|
|
17
|
-
adjacency = {}
|
|
18
|
-
for edge in edges:
|
|
19
|
-
adjacency.setdefault(edge.source, []).append((edge.target, edge.weight))
|
|
20
|
-
|
|
21
|
-
visited = {note_path}
|
|
22
|
-
queue = deque([(note_path, 0, 1.0)])
|
|
23
|
-
scores = {}
|
|
24
|
-
while queue:
|
|
25
|
-
current, level, weight = queue.popleft()
|
|
26
|
-
for target, edge_weight in adjacency.get(current, []):
|
|
27
|
-
if target in visited:
|
|
28
|
-
continue
|
|
29
|
-
score = weight * edge_weight * (1.0 / (level + 1))
|
|
30
|
-
scores[target] = max(scores.get(target, 0.0), score)
|
|
31
|
-
if level + 1 < depth:
|
|
32
|
-
visited.add(target)
|
|
33
|
-
queue.append((target, level + 1, score))
|
|
34
|
-
return [
|
|
35
|
-
SearchResult(note_path=path, score=score, snippet="", matched_chunks=[])
|
|
36
|
-
for path, score in sorted(scores.items(), key=lambda x: x[1], reverse=True)
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
def search(self, query: str, limit: int = 10) -> list[SearchResult]:
|
|
40
|
-
terms = [t.lower() for t in query.split() if t]
|
|
41
|
-
if not terms:
|
|
42
|
-
return []
|
|
43
|
-
edges = self.store.get_all_edges()
|
|
44
|
-
logger.debug("graph search: query=%r edges=%d", query, len(edges))
|
|
45
|
-
scores = {}
|
|
46
|
-
for edge in edges:
|
|
47
|
-
for node in (edge.source, edge.target):
|
|
48
|
-
node_lower = node.lower()
|
|
49
|
-
score = sum(1 for term in terms if term in node_lower)
|
|
50
|
-
if score:
|
|
51
|
-
scores[node] = max(scores.get(node, 0.0), score * edge.weight)
|
|
52
|
-
return [
|
|
53
|
-
SearchResult(note_path=path, score=score, snippet="", matched_chunks=[])
|
|
54
|
-
for path, score in sorted(scores.items(), key=lambda x: x[1], reverse=True)[:limit]
|
|
55
|
-
]
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
4
|
-
from obsidian_mcp.models import SearchResult
|
|
5
|
-
|
|
6
|
-
logger = logging.getLogger(__name__)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TextSearch:
|
|
10
|
-
def __init__(self, store: IndexStore):
|
|
11
|
-
self.store = store
|
|
12
|
-
|
|
13
|
-
def search(self, query: str, limit: int = 10) -> list[SearchResult]:
|
|
14
|
-
query_lower = query.lower()
|
|
15
|
-
chunks = self.store.get_all_chunks()
|
|
16
|
-
logger.debug("text search over %d chunks", len(chunks))
|
|
17
|
-
scored = []
|
|
18
|
-
for chunk in chunks:
|
|
19
|
-
text_lower = chunk.text.lower()
|
|
20
|
-
score = 0.0
|
|
21
|
-
if query_lower in text_lower:
|
|
22
|
-
score = text_lower.count(query_lower) / max(len(text_lower.split()), 1)
|
|
23
|
-
if score > 0:
|
|
24
|
-
scored.append((score, chunk))
|
|
25
|
-
scored.sort(key=lambda x: x[0], reverse=True)
|
|
26
|
-
by_note = {}
|
|
27
|
-
for score, chunk in scored[:limit * 3]:
|
|
28
|
-
if chunk.note_path not in by_note:
|
|
29
|
-
by_note[chunk.note_path] = SearchResult(
|
|
30
|
-
note_path=chunk.note_path,
|
|
31
|
-
score=score,
|
|
32
|
-
snippet=chunk.text[:300],
|
|
33
|
-
matched_chunks=[chunk],
|
|
34
|
-
)
|
|
35
|
-
else:
|
|
36
|
-
by_note[chunk.note_path].matched_chunks.append(chunk)
|
|
37
|
-
return sorted(by_note.values(), key=lambda r: r.score, reverse=True)[:limit]
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import math
|
|
3
|
-
|
|
4
|
-
import numpy as np
|
|
5
|
-
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
6
|
-
from sklearn.metrics.pairwise import cosine_similarity
|
|
7
|
-
|
|
8
|
-
from obsidian_mcp.indexer.embeddings import EmbedderFactory
|
|
9
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
10
|
-
from obsidian_mcp.models import Chunk, SearchResult
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TfidfIndex:
|
|
16
|
-
def __init__(self, max_features: int = 50000):
|
|
17
|
-
self.vectorizer = TfidfVectorizer(max_features=max_features)
|
|
18
|
-
self.matrix = None
|
|
19
|
-
self.doc_ids: list[str] = []
|
|
20
|
-
|
|
21
|
-
def fit(self, chunks: list[Chunk]) -> None:
|
|
22
|
-
if not chunks:
|
|
23
|
-
self.matrix = None
|
|
24
|
-
self.doc_ids = []
|
|
25
|
-
return
|
|
26
|
-
self.doc_ids = [chunk.id for chunk in chunks]
|
|
27
|
-
texts = [chunk.text for chunk in chunks]
|
|
28
|
-
self.matrix = self.vectorizer.fit_transform(texts)
|
|
29
|
-
logger.info("TF-IDF index fitted on %d chunks", len(chunks))
|
|
30
|
-
|
|
31
|
-
def query(self, query: str, top_k: int = 10) -> list[tuple[str, float]]:
|
|
32
|
-
if self.matrix is None or not self.doc_ids:
|
|
33
|
-
return []
|
|
34
|
-
qvec = self.vectorizer.transform([query])
|
|
35
|
-
scores = cosine_similarity(qvec, self.matrix).flatten()
|
|
36
|
-
ranked = np.argsort(scores)[::-1][:top_k]
|
|
37
|
-
return [(self.doc_ids[i], float(scores[i])) for i in ranked if scores[i] > 0]
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class VectorSearch:
|
|
41
|
-
def __init__(self, store: IndexStore, model_name: str):
|
|
42
|
-
self.store = store
|
|
43
|
-
self.model_name = model_name
|
|
44
|
-
self.embedder = EmbedderFactory.create(model_name)
|
|
45
|
-
self._tfidf_index: TfidfIndex | None = None
|
|
46
|
-
self._tfidf_chunk_count: int = 0
|
|
47
|
-
|
|
48
|
-
def _cosine_similarity(self, a: list[float], b: list[float]) -> float:
|
|
49
|
-
dot = sum(x * y for x, y in zip(a, b))
|
|
50
|
-
norm_a = math.sqrt(sum(x * x for x in a))
|
|
51
|
-
norm_b = math.sqrt(sum(x * x for x in b))
|
|
52
|
-
if norm_a == 0 or norm_b == 0:
|
|
53
|
-
return 0.0
|
|
54
|
-
return dot / (norm_a * norm_b)
|
|
55
|
-
|
|
56
|
-
def _embedding_search(
|
|
57
|
-
self, query: str, chunks: list[Chunk], limit: int
|
|
58
|
-
) -> list[SearchResult]:
|
|
59
|
-
try:
|
|
60
|
-
query_embedding = self.embedder.encode([query])[0]
|
|
61
|
-
except Exception as exc:
|
|
62
|
-
logger.warning("vector search failed: %s", exc)
|
|
63
|
-
return []
|
|
64
|
-
|
|
65
|
-
scored = []
|
|
66
|
-
for chunk in chunks:
|
|
67
|
-
if not chunk.embedding:
|
|
68
|
-
continue
|
|
69
|
-
score = self._cosine_similarity(query_embedding, chunk.embedding)
|
|
70
|
-
if score > 0:
|
|
71
|
-
scored.append((score, chunk))
|
|
72
|
-
return self._rank_by_note(scored, limit)
|
|
73
|
-
|
|
74
|
-
def _ensure_tfidf_index(self, chunks: list[Chunk]) -> TfidfIndex:
|
|
75
|
-
if self._tfidf_index is None or self._tfidf_chunk_count != len(chunks):
|
|
76
|
-
self._tfidf_index = TfidfIndex()
|
|
77
|
-
self._tfidf_index.fit(chunks)
|
|
78
|
-
self._tfidf_chunk_count = len(chunks)
|
|
79
|
-
return self._tfidf_index
|
|
80
|
-
|
|
81
|
-
def _tfidf_search(self, query: str, chunks: list[Chunk], limit: int) -> list[SearchResult]:
|
|
82
|
-
if not chunks:
|
|
83
|
-
return []
|
|
84
|
-
index = self._ensure_tfidf_index(chunks)
|
|
85
|
-
id_to_chunk = {chunk.id: chunk for chunk in chunks}
|
|
86
|
-
results = index.query(query, top_k=limit * 3)
|
|
87
|
-
scored = [
|
|
88
|
-
(score, id_to_chunk[doc_id])
|
|
89
|
-
for doc_id, score in results
|
|
90
|
-
if doc_id in id_to_chunk
|
|
91
|
-
]
|
|
92
|
-
return self._rank_by_note(scored, limit)
|
|
93
|
-
|
|
94
|
-
def _rank_by_note(
|
|
95
|
-
self, scored: list[tuple[float, object]], limit: int
|
|
96
|
-
) -> list[SearchResult]:
|
|
97
|
-
scored.sort(key=lambda x: x[0], reverse=True)
|
|
98
|
-
by_note = {}
|
|
99
|
-
for score, chunk in scored[:limit * 3]:
|
|
100
|
-
if chunk.note_path not in by_note:
|
|
101
|
-
by_note[chunk.note_path] = SearchResult(
|
|
102
|
-
note_path=chunk.note_path,
|
|
103
|
-
score=score,
|
|
104
|
-
snippet=chunk.text[:300],
|
|
105
|
-
matched_chunks=[chunk],
|
|
106
|
-
)
|
|
107
|
-
else:
|
|
108
|
-
by_note[chunk.note_path].matched_chunks.append(chunk)
|
|
109
|
-
return sorted(by_note.values(), key=lambda r: r.score, reverse=True)[:limit]
|
|
110
|
-
|
|
111
|
-
def search(self, query: str, limit: int = 10) -> list[SearchResult]:
|
|
112
|
-
chunks = self.store.get_all_chunks()
|
|
113
|
-
if not chunks:
|
|
114
|
-
logger.warning("vector search requested but no chunks are indexed")
|
|
115
|
-
return []
|
|
116
|
-
if self.embedder.uses_stored_embeddings():
|
|
117
|
-
return self._embedding_search(query, chunks, limit)
|
|
118
|
-
return self._tfidf_search(query, chunks, limit)
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
import time
|
|
5
|
-
|
|
6
|
-
from mcp import ErrorData, McpError
|
|
7
|
-
from mcp.server import Server
|
|
8
|
-
from mcp.server.stdio import stdio_server
|
|
9
|
-
from mcp.types import TextContent, Tool
|
|
10
|
-
|
|
11
|
-
from obsidian_mcp.config import Config
|
|
12
|
-
from obsidian_mcp.tools.registry import TOOLS, dispatch_async
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _error_code_for(exc: Exception) -> int:
|
|
18
|
-
if isinstance(exc, (ValueError, FileExistsError)):
|
|
19
|
-
return -32600
|
|
20
|
-
if isinstance(exc, FileNotFoundError):
|
|
21
|
-
return -32602
|
|
22
|
-
return -32603
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async def serve(config: Config | None = None):
|
|
26
|
-
config = config or Config()
|
|
27
|
-
server = Server("obsidian-mcp")
|
|
28
|
-
|
|
29
|
-
@server.list_tools()
|
|
30
|
-
async def list_tools() -> list[Tool]:
|
|
31
|
-
return [
|
|
32
|
-
Tool(name=name, description=meta["description"], inputSchema=meta["input_schema"])
|
|
33
|
-
for name, meta in TOOLS.items()
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
@server.call_tool()
|
|
37
|
-
async def call_tool(name: str, arguments: dict | None) -> list[TextContent]:
|
|
38
|
-
arguments = arguments or {}
|
|
39
|
-
start = time.perf_counter()
|
|
40
|
-
logger.info("tool start: %s", name)
|
|
41
|
-
try:
|
|
42
|
-
result = await dispatch_async(name, arguments, config)
|
|
43
|
-
if isinstance(result, str):
|
|
44
|
-
return [TextContent(type="text", text=result)]
|
|
45
|
-
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
46
|
-
except Exception as exc:
|
|
47
|
-
logger.exception("tool error: %s", name)
|
|
48
|
-
raise McpError(
|
|
49
|
-
ErrorData(code=_error_code_for(exc), message=str(exc))
|
|
50
|
-
) from exc
|
|
51
|
-
finally:
|
|
52
|
-
elapsed = time.perf_counter() - start
|
|
53
|
-
logger.info("tool end: %s (%.3fs)", name, elapsed)
|
|
54
|
-
|
|
55
|
-
async with stdio_server() as (read_stream, write_stream):
|
|
56
|
-
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def main():
|
|
60
|
-
logging.basicConfig(level=logging.INFO)
|
|
61
|
-
asyncio.run(serve())
|
|
File without changes
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from obsidian_mcp.config import Config
|
|
5
|
-
from obsidian_mcp.models import Note
|
|
6
|
-
from obsidian_mcp.privacy.filter import PrivacyFilter
|
|
7
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def handle_create_note(arguments: dict, config: Config) -> dict:
|
|
13
|
-
path = arguments.get("path")
|
|
14
|
-
content = arguments.get("content", "")
|
|
15
|
-
title = arguments.get("title")
|
|
16
|
-
tags = arguments.get("tags", [])
|
|
17
|
-
overwrite = bool(arguments.get("overwrite", False))
|
|
18
|
-
if not path:
|
|
19
|
-
raise ValueError("path is required")
|
|
20
|
-
|
|
21
|
-
if not path.endswith(".md"):
|
|
22
|
-
path = path + ".md"
|
|
23
|
-
|
|
24
|
-
PrivacyFilter(config).validate(path)
|
|
25
|
-
PrivacyFilter(config).validate(content)
|
|
26
|
-
|
|
27
|
-
vault = VaultRepository(config)
|
|
28
|
-
if vault.exists(path) and not overwrite:
|
|
29
|
-
raise FileExistsError(f"note already exists: {path}")
|
|
30
|
-
|
|
31
|
-
note = Note(
|
|
32
|
-
path=path,
|
|
33
|
-
title=title or _title_from_path(path),
|
|
34
|
-
content=content,
|
|
35
|
-
tags=tags,
|
|
36
|
-
)
|
|
37
|
-
vault.save(note)
|
|
38
|
-
logger.info("created note: %s", path)
|
|
39
|
-
return {"status": "created", "path": path}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _title_from_path(path: str) -> str:
|
|
43
|
-
return Path(path).stem.replace("-", " ").replace("_", " ").title()
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.config import Config
|
|
4
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
5
|
-
|
|
6
|
-
logger = logging.getLogger(__name__)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def handle_delete_note(arguments: dict, config: Config) -> dict:
|
|
10
|
-
path = arguments.get("path")
|
|
11
|
-
if not path:
|
|
12
|
-
raise ValueError("path is required")
|
|
13
|
-
vault = VaultRepository(config)
|
|
14
|
-
vault.delete(path)
|
|
15
|
-
logger.info("deleted note: %s", path)
|
|
16
|
-
return {"status": "deleted", "path": path}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.config import Config
|
|
4
|
-
from obsidian_mcp.indexer.indexer import Indexer
|
|
5
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
6
|
-
from obsidian_mcp.learning.detector import LearningDetector
|
|
7
|
-
from obsidian_mcp.learning.note_generator import NoteGenerator
|
|
8
|
-
from obsidian_mcp.privacy.filter import PrivacyFilter
|
|
9
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def handle_learn_from_text(arguments: dict, config: Config) -> dict:
|
|
15
|
-
text = arguments.get("text", "")
|
|
16
|
-
if not text:
|
|
17
|
-
raise ValueError("text is required")
|
|
18
|
-
|
|
19
|
-
PrivacyFilter(config).validate(text)
|
|
20
|
-
|
|
21
|
-
detector = LearningDetector(config)
|
|
22
|
-
learnings = detector.detect(text)
|
|
23
|
-
if not learnings:
|
|
24
|
-
return {"status": "no_learning_detected"}
|
|
25
|
-
|
|
26
|
-
vault = VaultRepository(config)
|
|
27
|
-
generator = NoteGenerator(config, vault)
|
|
28
|
-
indexer = Indexer(config, vault, IndexStore(config.index_dir / "index.db"))
|
|
29
|
-
|
|
30
|
-
created = []
|
|
31
|
-
for learning in learnings:
|
|
32
|
-
if vault.exists(generator.path_for(learning)):
|
|
33
|
-
continue
|
|
34
|
-
note = generator.generate_and_save(learning)
|
|
35
|
-
indexer.index_note(note)
|
|
36
|
-
created.append(note.path)
|
|
37
|
-
|
|
38
|
-
if not created:
|
|
39
|
-
logger.info("no new learnings created from text")
|
|
40
|
-
return {"status": "duplicate", "created_notes": []}
|
|
41
|
-
logger.info("learned from text, created notes: %s", created)
|
|
42
|
-
return {"status": "learned", "created_notes": created}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.config import Config
|
|
4
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
5
|
-
|
|
6
|
-
logger = logging.getLogger(__name__)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def handle_list_notes(arguments: dict, config: Config) -> str:
|
|
10
|
-
folder = arguments.get("folder")
|
|
11
|
-
vault = VaultRepository(config)
|
|
12
|
-
notes = vault.list_notes(folder)
|
|
13
|
-
logger.debug("listed %d notes in folder %s", len(notes), folder)
|
|
14
|
-
if not notes:
|
|
15
|
-
return "No notes found."
|
|
16
|
-
return "\n".join(f"- {n}" for n in notes)
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.config import Config
|
|
4
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
5
|
-
|
|
6
|
-
logger = logging.getLogger(__name__)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def handle_read_note(arguments: dict, config: Config) -> str:
|
|
10
|
-
path = arguments.get("path")
|
|
11
|
-
if not path:
|
|
12
|
-
raise ValueError("path is required")
|
|
13
|
-
vault = VaultRepository(config)
|
|
14
|
-
logger.debug("reading note: %s", path)
|
|
15
|
-
return vault.read_raw(path)
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.config import Config
|
|
4
|
-
from obsidian_mcp.tools.create import handle_create_note
|
|
5
|
-
from obsidian_mcp.tools.delete import handle_delete_note
|
|
6
|
-
from obsidian_mcp.tools.learn import handle_learn_from_text
|
|
7
|
-
from obsidian_mcp.tools.list import handle_list_notes
|
|
8
|
-
from obsidian_mcp.tools.read import handle_read_note
|
|
9
|
-
from obsidian_mcp.tools.related import handle_get_related_notes
|
|
10
|
-
from obsidian_mcp.tools.search import handle_search_notes
|
|
11
|
-
from obsidian_mcp.tools.sync import handle_sync_from_bundle
|
|
12
|
-
from obsidian_mcp.tools.update import handle_update_note
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
TOOLS = {
|
|
16
|
-
"read_note": {
|
|
17
|
-
"description": "Read a note from the Obsidian vault.",
|
|
18
|
-
"input_schema": {
|
|
19
|
-
"type": "object",
|
|
20
|
-
"properties": {"path": {"type": "string"}},
|
|
21
|
-
"required": ["path"],
|
|
22
|
-
},
|
|
23
|
-
"handler": handle_read_note,
|
|
24
|
-
},
|
|
25
|
-
"search_notes": {
|
|
26
|
-
"description": "Search notes by text, vector, graph or hybrid.",
|
|
27
|
-
"input_schema": {
|
|
28
|
-
"type": "object",
|
|
29
|
-
"properties": {
|
|
30
|
-
"query": {"type": "string"},
|
|
31
|
-
"mode": {"type": "string", "enum": ["text", "vector", "graph", "hybrid"]},
|
|
32
|
-
"limit": {"type": "integer", "default": 10},
|
|
33
|
-
},
|
|
34
|
-
"required": ["query"],
|
|
35
|
-
},
|
|
36
|
-
"handler": handle_search_notes,
|
|
37
|
-
},
|
|
38
|
-
"create_note": {
|
|
39
|
-
"description": "Create a new note in the vault.",
|
|
40
|
-
"input_schema": {
|
|
41
|
-
"type": "object",
|
|
42
|
-
"properties": {
|
|
43
|
-
"path": {"type": "string"},
|
|
44
|
-
"content": {"type": "string"},
|
|
45
|
-
"title": {"type": "string"},
|
|
46
|
-
"tags": {"type": "array", "items": {"type": "string"}},
|
|
47
|
-
"overwrite": {"type": "boolean", "default": False},
|
|
48
|
-
},
|
|
49
|
-
"required": ["path"],
|
|
50
|
-
},
|
|
51
|
-
"handler": handle_create_note,
|
|
52
|
-
},
|
|
53
|
-
"update_note": {
|
|
54
|
-
"description": "Update or append content to an existing note.",
|
|
55
|
-
"input_schema": {
|
|
56
|
-
"type": "object",
|
|
57
|
-
"properties": {
|
|
58
|
-
"path": {"type": "string"},
|
|
59
|
-
"content": {"type": "string"},
|
|
60
|
-
"append": {"type": "string"},
|
|
61
|
-
"tags": {"type": "array", "items": {"type": "string"}},
|
|
62
|
-
},
|
|
63
|
-
"required": ["path"],
|
|
64
|
-
},
|
|
65
|
-
"handler": handle_update_note,
|
|
66
|
-
},
|
|
67
|
-
"delete_note": {
|
|
68
|
-
"description": "Delete a note from the vault.",
|
|
69
|
-
"input_schema": {
|
|
70
|
-
"type": "object",
|
|
71
|
-
"properties": {"path": {"type": "string"}},
|
|
72
|
-
"required": ["path"],
|
|
73
|
-
},
|
|
74
|
-
"handler": handle_delete_note,
|
|
75
|
-
},
|
|
76
|
-
"list_notes": {
|
|
77
|
-
"description": "List notes in the vault.",
|
|
78
|
-
"input_schema": {
|
|
79
|
-
"type": "object",
|
|
80
|
-
"properties": {"folder": {"type": "string"}},
|
|
81
|
-
},
|
|
82
|
-
"handler": handle_list_notes,
|
|
83
|
-
},
|
|
84
|
-
"get_related_notes": {
|
|
85
|
-
"description": "Get notes related by links and graph traversal.",
|
|
86
|
-
"input_schema": {
|
|
87
|
-
"type": "object",
|
|
88
|
-
"properties": {
|
|
89
|
-
"path": {"type": "string"},
|
|
90
|
-
"depth": {"type": "integer", "default": 1},
|
|
91
|
-
},
|
|
92
|
-
"required": ["path"],
|
|
93
|
-
},
|
|
94
|
-
"handler": handle_get_related_notes,
|
|
95
|
-
},
|
|
96
|
-
"sync_from_bundle": {
|
|
97
|
-
"description": "Re-index the loop-engineering-agents bundle and local vault.",
|
|
98
|
-
"input_schema": {
|
|
99
|
-
"type": "object",
|
|
100
|
-
"properties": {"force": {"type": "boolean", "default": False}},
|
|
101
|
-
},
|
|
102
|
-
"handler": handle_sync_from_bundle,
|
|
103
|
-
},
|
|
104
|
-
"learn_from_text": {
|
|
105
|
-
"description": "Detect learnings in text and create notes automatically.",
|
|
106
|
-
"input_schema": {
|
|
107
|
-
"type": "object",
|
|
108
|
-
"properties": {"text": {"type": "string"}},
|
|
109
|
-
"required": ["text"],
|
|
110
|
-
},
|
|
111
|
-
"handler": handle_learn_from_text,
|
|
112
|
-
},
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def dispatch(name: str, arguments: dict, config: Config):
|
|
117
|
-
tool = TOOLS.get(name)
|
|
118
|
-
if tool is None:
|
|
119
|
-
raise ValueError(f"unknown tool: {name}")
|
|
120
|
-
return tool["handler"](arguments, config)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
async def dispatch_async(name: str, arguments: dict, config: Config):
|
|
124
|
-
tool = TOOLS.get(name)
|
|
125
|
-
if tool is None:
|
|
126
|
-
raise ValueError(f"unknown tool: {name}")
|
|
127
|
-
handler = tool["handler"]
|
|
128
|
-
if asyncio.iscoroutinefunction(handler):
|
|
129
|
-
return await handler(arguments, config)
|
|
130
|
-
return await asyncio.to_thread(handler, arguments, config)
|