@archznn/crewloop-skills 0.5.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.
Files changed (250) hide show
  1. package/README.md +4 -16
  2. package/package.json +3 -3
  3. package/packages/cli/dist/agents.js +1 -1
  4. package/packages/cli/dist/agents.js.map +1 -1
  5. package/packages/cli/dist/cli.d.ts.map +1 -1
  6. package/packages/cli/dist/cli.js +31 -37
  7. package/packages/cli/dist/cli.js.map +1 -1
  8. package/packages/cli/dist/hooks.d.ts +6 -4
  9. package/packages/cli/dist/hooks.d.ts.map +1 -1
  10. package/packages/cli/dist/hooks.js +258 -98
  11. package/packages/cli/dist/hooks.js.map +1 -1
  12. package/packages/cli/dist/tests/cli.test.js +21 -0
  13. package/packages/cli/dist/tests/cli.test.js.map +1 -1
  14. package/packages/cli/dist/tests/hooks.test.js +253 -27
  15. package/packages/cli/dist/tests/hooks.test.js.map +1 -1
  16. package/references/conventions.md +1 -10
  17. package/references/workflow.md +1 -1
  18. package/servers/dashboard/README.md +55 -1
  19. package/servers/dashboard/bin/crewloop-shim.js +4 -0
  20. package/servers/dashboard/dist/adapters/agy.d.ts +19 -0
  21. package/servers/dashboard/dist/adapters/agy.d.ts.map +1 -0
  22. package/servers/dashboard/dist/adapters/agy.js +108 -0
  23. package/servers/dashboard/dist/adapters/agy.js.map +1 -0
  24. package/servers/dashboard/dist/adapters/codex.d.ts.map +1 -1
  25. package/servers/dashboard/dist/adapters/codex.js +2 -0
  26. package/servers/dashboard/dist/adapters/codex.js.map +1 -1
  27. package/servers/dashboard/dist/adapters/kimi.d.ts +1 -1
  28. package/servers/dashboard/dist/adapters/kimi.d.ts.map +1 -1
  29. package/servers/dashboard/dist/adapters/kimi.js +9 -0
  30. package/servers/dashboard/dist/adapters/kimi.js.map +1 -1
  31. package/servers/dashboard/dist/adapters/shim.d.ts +1 -1
  32. package/servers/dashboard/dist/adapters/shim.d.ts.map +1 -1
  33. package/servers/dashboard/dist/adapters/shim.js +32 -11
  34. package/servers/dashboard/dist/adapters/shim.js.map +1 -1
  35. package/servers/dashboard/dist/adapters/shim.test.js +46 -4
  36. package/servers/dashboard/dist/adapters/shim.test.js.map +1 -1
  37. package/servers/dashboard/dist/lib/constants.d.ts +5 -0
  38. package/servers/dashboard/dist/lib/constants.d.ts.map +1 -0
  39. package/servers/dashboard/dist/lib/constants.js +46 -0
  40. package/servers/dashboard/dist/lib/constants.js.map +1 -0
  41. package/servers/dashboard/dist/lib/format.d.ts +6 -0
  42. package/servers/dashboard/dist/lib/format.d.ts.map +1 -0
  43. package/servers/dashboard/dist/lib/format.js +52 -0
  44. package/servers/dashboard/dist/lib/format.js.map +1 -0
  45. package/servers/dashboard/dist/lib/graph.d.ts +22 -0
  46. package/servers/dashboard/dist/lib/graph.d.ts.map +1 -0
  47. package/servers/dashboard/dist/lib/graph.js +45 -0
  48. package/servers/dashboard/dist/lib/graph.js.map +1 -0
  49. package/servers/dashboard/dist/lib/invocations.d.ts +32 -0
  50. package/servers/dashboard/dist/lib/invocations.d.ts.map +1 -0
  51. package/servers/dashboard/dist/lib/invocations.js +135 -0
  52. package/servers/dashboard/dist/lib/invocations.js.map +1 -0
  53. package/servers/dashboard/dist/lib/invocations.test.d.ts +2 -0
  54. package/servers/dashboard/dist/lib/invocations.test.d.ts.map +1 -0
  55. package/servers/dashboard/dist/lib/invocations.test.js +68 -0
  56. package/servers/dashboard/dist/lib/invocations.test.js.map +1 -0
  57. package/servers/dashboard/dist/lib/paths.d.ts +2 -0
  58. package/servers/dashboard/dist/lib/paths.d.ts.map +1 -0
  59. package/servers/dashboard/dist/lib/paths.js +40 -0
  60. package/servers/dashboard/dist/lib/paths.js.map +1 -0
  61. package/servers/dashboard/dist/presenter.d.ts.map +1 -1
  62. package/servers/dashboard/dist/presenter.js +2 -0
  63. package/servers/dashboard/dist/presenter.js.map +1 -1
  64. package/servers/dashboard/dist/public/assets/index-DjmMKbPN.css +1 -0
  65. package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js +5323 -0
  66. package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js.map +1 -0
  67. package/servers/dashboard/dist/public/index.html +16 -0
  68. package/servers/dashboard/dist/server.d.ts.map +1 -1
  69. package/servers/dashboard/dist/server.js +5 -1
  70. package/servers/dashboard/dist/server.js.map +1 -1
  71. package/servers/dashboard/dist/skills/infer.d.ts.map +1 -1
  72. package/servers/dashboard/dist/skills/infer.js +0 -6
  73. package/servers/dashboard/dist/skills/infer.js.map +1 -1
  74. package/servers/dashboard/dist/skills/infer.test.js +10 -3
  75. package/servers/dashboard/dist/skills/infer.test.js.map +1 -1
  76. package/servers/dashboard/dist/skills/mapping.d.ts +0 -3
  77. package/servers/dashboard/dist/skills/mapping.d.ts.map +1 -1
  78. package/servers/dashboard/dist/skills/mapping.js +0 -18
  79. package/servers/dashboard/dist/skills/mapping.js.map +1 -1
  80. package/servers/dashboard/dist/skills/registry.d.ts.map +1 -1
  81. package/servers/dashboard/dist/skills/registry.js +0 -1
  82. package/servers/dashboard/dist/skills/registry.js.map +1 -1
  83. package/servers/dashboard/dist/tests/adapters.test.d.ts +2 -0
  84. package/servers/dashboard/dist/tests/adapters.test.d.ts.map +1 -0
  85. package/servers/dashboard/dist/tests/adapters.test.js +180 -0
  86. package/servers/dashboard/dist/tests/adapters.test.js.map +1 -0
  87. package/servers/dashboard/dist/tests/lib-helpers.test.d.ts +2 -0
  88. package/servers/dashboard/dist/tests/lib-helpers.test.d.ts.map +1 -0
  89. package/servers/dashboard/dist/tests/lib-helpers.test.js +123 -0
  90. package/servers/dashboard/dist/tests/lib-helpers.test.js.map +1 -0
  91. package/servers/dashboard/dist/tests/shim.test.d.ts +2 -0
  92. package/servers/dashboard/dist/tests/shim.test.d.ts.map +1 -0
  93. package/servers/dashboard/dist/tests/shim.test.js +133 -0
  94. package/servers/dashboard/dist/tests/shim.test.js.map +1 -0
  95. package/servers/dashboard/dist/types.d.ts +5 -2
  96. package/servers/dashboard/dist/types.d.ts.map +1 -1
  97. package/servers/dashboard/package.json +24 -6
  98. package/servers/dashboard/src/adapters/agy.ts +136 -0
  99. package/servers/dashboard/src/adapters/codex.ts +2 -0
  100. package/servers/dashboard/src/adapters/kimi.ts +11 -1
  101. package/servers/dashboard/src/adapters/shim.test.ts +57 -4
  102. package/servers/dashboard/src/adapters/shim.ts +31 -11
  103. package/servers/dashboard/src/lib/constants.ts +44 -0
  104. package/servers/dashboard/src/lib/format.ts +44 -0
  105. package/servers/dashboard/src/lib/graph.ts +69 -0
  106. package/servers/dashboard/src/lib/invocations.test.ts +70 -0
  107. package/servers/dashboard/src/lib/invocations.ts +172 -0
  108. package/servers/dashboard/src/lib/paths.ts +35 -0
  109. package/servers/dashboard/src/presenter.ts +2 -0
  110. package/servers/dashboard/src/server.ts +5 -1
  111. package/servers/dashboard/src/skills/infer.test.ts +11 -3
  112. package/servers/dashboard/src/skills/infer.ts +1 -8
  113. package/servers/dashboard/src/skills/mapping.ts +0 -20
  114. package/servers/dashboard/src/skills/registry.ts +0 -1
  115. package/servers/dashboard/src/tests/adapters.test.ts +198 -0
  116. package/servers/dashboard/src/tests/lib-helpers.test.ts +133 -0
  117. package/servers/dashboard/src/tests/shim.test.ts +153 -0
  118. package/servers/dashboard/src/types.ts +5 -3
  119. package/servers/dashboard/ui/index.html +15 -0
  120. package/servers/dashboard/ui/postcss.config.js +6 -0
  121. package/servers/dashboard/ui/src/App.tsx +360 -0
  122. package/servers/dashboard/ui/src/components/ActiveSkillPanel.tsx +69 -0
  123. package/servers/dashboard/ui/src/components/ActivityGraph.tsx +74 -0
  124. package/servers/dashboard/ui/src/components/CommandPalette.tsx +200 -0
  125. package/servers/dashboard/ui/src/components/FileActivity.tsx +20 -0
  126. package/servers/dashboard/ui/src/components/FileDiff.tsx +68 -0
  127. package/servers/dashboard/ui/src/components/FileList.tsx +64 -0
  128. package/servers/dashboard/ui/src/components/FilterBar.tsx +208 -0
  129. package/servers/dashboard/ui/src/components/Network3D.tsx +178 -0
  130. package/servers/dashboard/ui/src/components/SessionSelector.tsx +95 -0
  131. package/servers/dashboard/ui/src/components/Sidebar.tsx +110 -0
  132. package/servers/dashboard/ui/src/components/TelemetryPanel.tsx +57 -0
  133. package/servers/dashboard/ui/src/components/Timeline.tsx +57 -0
  134. package/servers/dashboard/ui/src/components/TimelineRow.tsx +112 -0
  135. package/servers/dashboard/ui/src/components/TopBar.tsx +116 -0
  136. package/servers/dashboard/ui/src/components/ViewHeader.tsx +19 -0
  137. package/servers/dashboard/ui/src/components/ui/Icon.tsx +105 -0
  138. package/servers/dashboard/ui/src/components/ui/StatusBadge.tsx +19 -0
  139. package/servers/dashboard/ui/src/components/views/FilesView.tsx +23 -0
  140. package/servers/dashboard/ui/src/components/views/NetworkView.tsx +20 -0
  141. package/servers/dashboard/ui/src/components/views/Overview.tsx +135 -0
  142. package/servers/dashboard/ui/src/components/views/SessionsView.tsx +84 -0
  143. package/servers/dashboard/ui/src/components/views/SettingsView.tsx +138 -0
  144. package/servers/dashboard/ui/src/components/views/SkillsView.tsx +92 -0
  145. package/servers/dashboard/ui/src/components/views/TimelineView.tsx +46 -0
  146. package/servers/dashboard/ui/src/contexts/FilterContext.tsx +41 -0
  147. package/servers/dashboard/ui/src/contexts/PinnedSessionsContext.tsx +80 -0
  148. package/servers/dashboard/ui/src/contexts/SettingsContext.tsx +60 -0
  149. package/servers/dashboard/ui/src/hooks/useCommandPalette.ts +36 -0
  150. package/servers/dashboard/ui/src/hooks/useKeyboardShortcut.ts +38 -0
  151. package/servers/dashboard/ui/src/hooks/useNow.ts +12 -0
  152. package/servers/dashboard/ui/src/hooks/useReducedMotion.ts +15 -0
  153. package/servers/dashboard/ui/src/hooks/useSessions.ts +64 -0
  154. package/servers/dashboard/ui/src/hooks/useTheme.ts +30 -0
  155. package/servers/dashboard/ui/src/hooks/useViewport.ts +19 -0
  156. package/servers/dashboard/ui/src/hooks/useWebSocket.ts +118 -0
  157. package/servers/dashboard/ui/src/lib/export.test.ts +33 -0
  158. package/servers/dashboard/ui/src/lib/export.ts +39 -0
  159. package/servers/dashboard/ui/src/lib/filter.test.ts +95 -0
  160. package/servers/dashboard/ui/src/lib/filter.ts +178 -0
  161. package/servers/dashboard/ui/src/lib/format.test.ts +25 -0
  162. package/servers/dashboard/ui/src/lib/search.test.ts +52 -0
  163. package/servers/dashboard/ui/src/lib/search.ts +60 -0
  164. package/servers/dashboard/ui/src/lib/settings.test.ts +50 -0
  165. package/servers/dashboard/ui/src/lib/settings.ts +56 -0
  166. package/servers/dashboard/ui/src/lib/types.ts +124 -0
  167. package/servers/dashboard/ui/src/main.tsx +19 -0
  168. package/servers/dashboard/ui/src/styles/index.css +155 -0
  169. package/servers/dashboard/ui/tailwind.config.js +45 -0
  170. package/servers/dashboard/ui/tsconfig.json +33 -0
  171. package/servers/dashboard/ui/tsconfig.node.json +10 -0
  172. package/servers/dashboard/ui/vite.config.ts +37 -0
  173. package/servers/dashboard/ui/vitest.config.ts +8 -0
  174. package/skills/accessibility-auditor/SKILL.md +0 -20
  175. package/skills/architect/SKILL.md +0 -45
  176. package/skills/designer/SKILL.md +0 -30
  177. package/skills/docs-writer/SKILL.md +0 -13
  178. package/skills/engineer/SKILL.md +0 -30
  179. package/skills/maintainer/SKILL.md +0 -20
  180. package/skills/orchestrator/SKILL.md +0 -13
  181. package/skills/product-manager/SKILL.md +0 -20
  182. package/skills/researcher/SKILL.md +0 -20
  183. package/skills/reviewer/SKILL.md +0 -30
  184. package/skills/security-guard/SKILL.md +0 -20
  185. package/skills/shipper/SKILL.md +0 -33
  186. package/skills/tester/SKILL.md +0 -20
  187. package/packages/cli/dist/mcp.d.ts +0 -28
  188. package/packages/cli/dist/mcp.d.ts.map +0 -1
  189. package/packages/cli/dist/mcp.js +0 -148
  190. package/packages/cli/dist/mcp.js.map +0 -1
  191. package/packages/cli/dist/tests/mcp.test.d.ts +0 -2
  192. package/packages/cli/dist/tests/mcp.test.d.ts.map +0 -1
  193. package/packages/cli/dist/tests/mcp.test.js +0 -232
  194. package/packages/cli/dist/tests/mcp.test.js.map +0 -1
  195. package/references/obsidian-mcp-usage.md +0 -190
  196. package/servers/dashboard/public/app.js +0 -516
  197. package/servers/dashboard/public/index.html +0 -96
  198. package/servers/dashboard/public/styles.css +0 -819
  199. package/servers/obsidian-mcp/README.md +0 -82
  200. package/servers/obsidian-mcp/pyproject.toml +0 -32
  201. package/servers/obsidian-mcp/src/obsidian_mcp/__init__.py +0 -0
  202. package/servers/obsidian-mcp/src/obsidian_mcp/config.py +0 -47
  203. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/__init__.py +0 -0
  204. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/embeddings.py +0 -105
  205. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/indexer.py +0 -79
  206. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/store.py +0 -141
  207. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/sync.py +0 -37
  208. package/servers/obsidian-mcp/src/obsidian_mcp/learning/__init__.py +0 -0
  209. package/servers/obsidian-mcp/src/obsidian_mcp/learning/detector.py +0 -66
  210. package/servers/obsidian-mcp/src/obsidian_mcp/learning/note_generator.py +0 -40
  211. package/servers/obsidian-mcp/src/obsidian_mcp/main.py +0 -4
  212. package/servers/obsidian-mcp/src/obsidian_mcp/models.py +0 -42
  213. package/servers/obsidian-mcp/src/obsidian_mcp/privacy/__init__.py +0 -0
  214. package/servers/obsidian-mcp/src/obsidian_mcp/privacy/filter.py +0 -68
  215. package/servers/obsidian-mcp/src/obsidian_mcp/rag/__init__.py +0 -0
  216. package/servers/obsidian-mcp/src/obsidian_mcp/rag/engine.py +0 -50
  217. package/servers/obsidian-mcp/src/obsidian_mcp/rag/graph_search.py +0 -55
  218. package/servers/obsidian-mcp/src/obsidian_mcp/rag/text_search.py +0 -37
  219. package/servers/obsidian-mcp/src/obsidian_mcp/rag/vector_search.py +0 -118
  220. package/servers/obsidian-mcp/src/obsidian_mcp/server.py +0 -61
  221. package/servers/obsidian-mcp/src/obsidian_mcp/tools/__init__.py +0 -0
  222. package/servers/obsidian-mcp/src/obsidian_mcp/tools/create.py +0 -43
  223. package/servers/obsidian-mcp/src/obsidian_mcp/tools/delete.py +0 -16
  224. package/servers/obsidian-mcp/src/obsidian_mcp/tools/learn.py +0 -42
  225. package/servers/obsidian-mcp/src/obsidian_mcp/tools/list.py +0 -16
  226. package/servers/obsidian-mcp/src/obsidian_mcp/tools/read.py +0 -15
  227. package/servers/obsidian-mcp/src/obsidian_mcp/tools/registry.py +0 -130
  228. package/servers/obsidian-mcp/src/obsidian_mcp/tools/related.py +0 -20
  229. package/servers/obsidian-mcp/src/obsidian_mcp/tools/search.py +0 -26
  230. package/servers/obsidian-mcp/src/obsidian_mcp/tools/sync.py +0 -22
  231. package/servers/obsidian-mcp/src/obsidian_mcp/tools/update.py +0 -34
  232. package/servers/obsidian-mcp/src/obsidian_mcp/vault/__init__.py +0 -0
  233. package/servers/obsidian-mcp/src/obsidian_mcp/vault/parser.py +0 -82
  234. package/servers/obsidian-mcp/src/obsidian_mcp/vault/repository.py +0 -68
  235. package/servers/obsidian-mcp/src/obsidian_mcp/vault/writer.py +0 -61
  236. package/servers/obsidian-mcp/tests/conftest.py +0 -39
  237. package/servers/obsidian-mcp/tests/test_async_tools.py +0 -87
  238. package/servers/obsidian-mcp/tests/test_edge_cases.py +0 -59
  239. package/servers/obsidian-mcp/tests/test_indexer.py +0 -27
  240. package/servers/obsidian-mcp/tests/test_integration.py +0 -90
  241. package/servers/obsidian-mcp/tests/test_learning.py +0 -34
  242. package/servers/obsidian-mcp/tests/test_privacy.py +0 -31
  243. package/servers/obsidian-mcp/tests/test_privacy_config.py +0 -44
  244. package/servers/obsidian-mcp/tests/test_rag.py +0 -64
  245. package/servers/obsidian-mcp/tests/test_read_raw.py +0 -37
  246. package/servers/obsidian-mcp/tests/test_tfidf_fallback.py +0 -54
  247. package/servers/obsidian-mcp/tests/test_tools.py +0 -108
  248. package/servers/obsidian-mcp/tests/test_vault.py +0 -103
  249. package/servers/obsidian-mcp/tests/test_writer.py +0 -139
  250. 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")
@@ -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())
@@ -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)