@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.
- package/README.md +4 -16
- package/package.json +3 -3
- 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 +31 -37
- 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 +258 -98
- package/packages/cli/dist/hooks.js.map +1 -1
- package/packages/cli/dist/tests/cli.test.js +21 -0
- package/packages/cli/dist/tests/cli.test.js.map +1 -1
- package/packages/cli/dist/tests/hooks.test.js +253 -27
- 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/bin/crewloop-shim.js +4 -0
- 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.d.ts +2 -0
- package/servers/dashboard/dist/tests/shim.test.d.ts.map +1 -0
- package/servers/dashboard/dist/tests/shim.test.js +133 -0
- package/servers/dashboard/dist/tests/shim.test.js.map +1 -0
- package/servers/dashboard/dist/types.d.ts +5 -2
- package/servers/dashboard/dist/types.d.ts.map +1 -1
- package/servers/dashboard/package.json +24 -6
- 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 +153 -0
- 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,20 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.config import Config
|
|
4
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
5
|
-
from obsidian_mcp.rag.engine import RAGEngine
|
|
6
|
-
|
|
7
|
-
logger = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def handle_get_related_notes(arguments: dict, config: Config) -> str:
|
|
11
|
-
path = arguments.get("path")
|
|
12
|
-
if not path:
|
|
13
|
-
raise ValueError("path is required")
|
|
14
|
-
depth = int(arguments.get("depth", 1))
|
|
15
|
-
logger.info("getting related notes: path=%s depth=%d", path, depth)
|
|
16
|
-
engine = RAGEngine(config, IndexStore(config.index_dir / "index.db"))
|
|
17
|
-
results = engine.related(path, depth=depth)
|
|
18
|
-
if not results:
|
|
19
|
-
return "No related notes found."
|
|
20
|
-
return "\n".join(f"- **{r.note_path}** (score: {r.score:.3f})" for r in results)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.config import Config
|
|
4
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
5
|
-
from obsidian_mcp.rag.engine import RAGEngine
|
|
6
|
-
|
|
7
|
-
logger = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def handle_search_notes(arguments: dict, config: Config) -> str:
|
|
11
|
-
query = arguments.get("query")
|
|
12
|
-
if not query:
|
|
13
|
-
raise ValueError("query is required")
|
|
14
|
-
mode = arguments.get("mode", "hybrid")
|
|
15
|
-
limit = int(arguments.get("limit", 10))
|
|
16
|
-
logger.info("searching notes: mode=%s query=%r limit=%d", mode, query, limit)
|
|
17
|
-
engine = RAGEngine(config, IndexStore(config.index_dir / "index.db"))
|
|
18
|
-
results = engine.search(query, mode=mode, limit=limit)
|
|
19
|
-
if not results:
|
|
20
|
-
return "No results found."
|
|
21
|
-
lines = []
|
|
22
|
-
for r in results:
|
|
23
|
-
lines.append(f"- **{r.note_path}** (score: {r.score:.3f})")
|
|
24
|
-
if r.snippet:
|
|
25
|
-
lines.append(f" {r.snippet[:200].replace(chr(10), ' ')}")
|
|
26
|
-
return "\n".join(lines)
|
|
@@ -1,22 +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.indexer.sync import BundleSync
|
|
7
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def handle_sync_from_bundle(arguments: dict, config: Config) -> dict:
|
|
13
|
-
force = bool(arguments.get("force", False))
|
|
14
|
-
logger.info("syncing from bundle: force=%s", force)
|
|
15
|
-
vault = VaultRepository(config)
|
|
16
|
-
store = IndexStore(config.index_dir / "index.db")
|
|
17
|
-
indexer = Indexer(config, vault, store)
|
|
18
|
-
indexer.index_all(force=force)
|
|
19
|
-
sync = BundleSync(config, indexer, vault)
|
|
20
|
-
report = sync.sync(force=force)
|
|
21
|
-
logger.info("synced from bundle: %s", report)
|
|
22
|
-
return {"status": "synced", **report}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from datetime import datetime, timezone
|
|
3
|
-
|
|
4
|
-
from obsidian_mcp.config import Config
|
|
5
|
-
from obsidian_mcp.privacy.filter import PrivacyFilter
|
|
6
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def handle_update_note(arguments: dict, config: Config) -> dict:
|
|
12
|
-
path = arguments.get("path")
|
|
13
|
-
if not path:
|
|
14
|
-
raise ValueError("path is required")
|
|
15
|
-
content = arguments.get("content")
|
|
16
|
-
append = arguments.get("append")
|
|
17
|
-
tags = arguments.get("tags")
|
|
18
|
-
|
|
19
|
-
vault = VaultRepository(config)
|
|
20
|
-
note = vault.read(path)
|
|
21
|
-
|
|
22
|
-
PrivacyFilter(config).validate(content or "")
|
|
23
|
-
PrivacyFilter(config).validate(append or "")
|
|
24
|
-
|
|
25
|
-
if content is not None:
|
|
26
|
-
note.content = content
|
|
27
|
-
if append:
|
|
28
|
-
note.content = note.content.rstrip() + "\n\n" + append
|
|
29
|
-
if tags is not None:
|
|
30
|
-
note.tags = tags
|
|
31
|
-
note.frontmatter["updated"] = datetime.now(timezone.utc).isoformat()
|
|
32
|
-
vault.save(note)
|
|
33
|
-
logger.info("updated note: %s", path)
|
|
34
|
-
return {"status": "updated", "path": path}
|
|
File without changes
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import re
|
|
3
|
-
from datetime import datetime, timezone
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import yaml
|
|
7
|
-
|
|
8
|
-
from obsidian_mcp.models import Note
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
|
14
|
-
LINK_RE = re.compile(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def extract_frontmatter(content: str) -> tuple[dict, str]:
|
|
18
|
-
match = FRONTMATTER_RE.match(content)
|
|
19
|
-
if not match:
|
|
20
|
-
return {}, content
|
|
21
|
-
try:
|
|
22
|
-
frontmatter = yaml.safe_load(match.group(1)) or {}
|
|
23
|
-
except yaml.YAMLError as exc:
|
|
24
|
-
logger.warning("failed to parse frontmatter: %s", exc)
|
|
25
|
-
frontmatter = {}
|
|
26
|
-
body = content[match.end():]
|
|
27
|
-
return frontmatter, body
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def extract_title_and_body(body: str) -> tuple[str, str]:
|
|
31
|
-
lines = body.splitlines()
|
|
32
|
-
if lines and lines[0].startswith("# "):
|
|
33
|
-
title = lines[0][2:].strip()
|
|
34
|
-
return title, "\n".join(lines[1:]).lstrip("\n")
|
|
35
|
-
return "", body
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def extract_title(body: str, path: str) -> str:
|
|
39
|
-
title, _ = extract_title_and_body(body)
|
|
40
|
-
if title:
|
|
41
|
-
return title
|
|
42
|
-
return Path(path).stem.replace("-", " ").replace("_", " ").title()
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def normalize_link(link: str) -> str:
|
|
46
|
-
link = link.strip()
|
|
47
|
-
if not any(link.endswith(ext) for ext in (".md", ".png", ".jpg", ".jpeg", ".pdf")):
|
|
48
|
-
link += ".md"
|
|
49
|
-
return link
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def extract_links(content: str) -> list[str]:
|
|
53
|
-
return [normalize_link(m.group(1)) for m in LINK_RE.finditer(content)]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def parse_note(path: str, full_path: Path) -> Note:
|
|
57
|
-
content = full_path.read_text(encoding="utf-8")
|
|
58
|
-
frontmatter, body = extract_frontmatter(content)
|
|
59
|
-
if frontmatter.get("title"):
|
|
60
|
-
title = frontmatter["title"]
|
|
61
|
-
else:
|
|
62
|
-
title, body = extract_title_and_body(body)
|
|
63
|
-
if not title:
|
|
64
|
-
title = extract_title(body, path)
|
|
65
|
-
links = extract_links(content)
|
|
66
|
-
stat = full_path.stat()
|
|
67
|
-
ctime = datetime.fromtimestamp(stat.st_ctime, tz=timezone.utc)
|
|
68
|
-
mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
|
|
69
|
-
tags = frontmatter.get("tags", []) or []
|
|
70
|
-
if isinstance(tags, str):
|
|
71
|
-
tags = [t.strip() for t in tags.split(",") if t.strip()]
|
|
72
|
-
return Note(
|
|
73
|
-
path=path,
|
|
74
|
-
title=title,
|
|
75
|
-
content=body,
|
|
76
|
-
frontmatter=frontmatter,
|
|
77
|
-
links=links,
|
|
78
|
-
backlinks=[],
|
|
79
|
-
tags=tags,
|
|
80
|
-
ctime=ctime,
|
|
81
|
-
mtime=mtime,
|
|
82
|
-
)
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from obsidian_mcp.config import Config
|
|
4
|
-
from obsidian_mcp.models import Note
|
|
5
|
-
from obsidian_mcp.vault.parser import parse_note
|
|
6
|
-
from obsidian_mcp.vault.writer import write_note
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class VaultRepository:
|
|
10
|
-
def __init__(self, config: Config):
|
|
11
|
-
self.config = config
|
|
12
|
-
self.root = config.vault_path
|
|
13
|
-
self.root.mkdir(parents=True, exist_ok=True)
|
|
14
|
-
|
|
15
|
-
def _resolve(self, path: str) -> Path:
|
|
16
|
-
safe = path.lstrip("/")
|
|
17
|
-
if ".." in safe.split("/"):
|
|
18
|
-
raise ValueError(f"invalid note path: {path}")
|
|
19
|
-
full = (self.root / safe).resolve()
|
|
20
|
-
if full != self.root and self.root not in full.parents:
|
|
21
|
-
raise ValueError(f"invalid note path: {path}")
|
|
22
|
-
return full
|
|
23
|
-
|
|
24
|
-
def _is_inside_vault(self, path: Path) -> bool:
|
|
25
|
-
resolved = path.resolve()
|
|
26
|
-
return resolved == self.root or self.root in resolved.parents
|
|
27
|
-
|
|
28
|
-
def list_notes(self, folder: str | None = None) -> list[str]:
|
|
29
|
-
base = self.root
|
|
30
|
-
if folder:
|
|
31
|
-
base = self._resolve(folder)
|
|
32
|
-
paths = []
|
|
33
|
-
for p in base.rglob("*.md"):
|
|
34
|
-
if not self._is_inside_vault(p):
|
|
35
|
-
continue
|
|
36
|
-
rel = p.relative_to(self.root)
|
|
37
|
-
if any(part.startswith(".") for part in rel.parts):
|
|
38
|
-
continue
|
|
39
|
-
paths.append(rel.as_posix())
|
|
40
|
-
return sorted(paths)
|
|
41
|
-
|
|
42
|
-
def read(self, path: str) -> Note:
|
|
43
|
-
full = self._resolve(path)
|
|
44
|
-
if not full.exists():
|
|
45
|
-
raise FileNotFoundError(f"note not found: {path}")
|
|
46
|
-
return parse_note(path, full)
|
|
47
|
-
|
|
48
|
-
def read_raw(self, path: str) -> str:
|
|
49
|
-
full = self._resolve(path)
|
|
50
|
-
if not full.exists():
|
|
51
|
-
raise FileNotFoundError(f"note not found: {path}")
|
|
52
|
-
return full.read_text(encoding="utf-8", errors="replace")
|
|
53
|
-
|
|
54
|
-
def read_all(self) -> list[Note]:
|
|
55
|
-
return [self.read(p) for p in self.list_notes()]
|
|
56
|
-
|
|
57
|
-
def exists(self, path: str) -> bool:
|
|
58
|
-
return self._resolve(path).exists()
|
|
59
|
-
|
|
60
|
-
def save(self, note: Note) -> None:
|
|
61
|
-
full = self._resolve(note.path)
|
|
62
|
-
write_note(full, note)
|
|
63
|
-
|
|
64
|
-
def delete(self, path: str) -> None:
|
|
65
|
-
full = self._resolve(path)
|
|
66
|
-
if not full.exists():
|
|
67
|
-
raise FileNotFoundError(f"note not found: {path}")
|
|
68
|
-
full.unlink()
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from datetime import datetime, timezone
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import yaml
|
|
6
|
-
|
|
7
|
-
from obsidian_mcp.models import Note
|
|
8
|
-
from obsidian_mcp.vault.parser import extract_frontmatter
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def to_frontmatter(data: dict) -> str:
|
|
14
|
-
if not data:
|
|
15
|
-
return ""
|
|
16
|
-
return "---\n" + yaml.safe_dump(data, sort_keys=False, allow_unicode=True) + "---\n\n"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _normalize_tags(tags) -> list[str]:
|
|
20
|
-
if tags is None:
|
|
21
|
-
return []
|
|
22
|
-
if isinstance(tags, str):
|
|
23
|
-
return [t.strip() for t in tags.split(",") if t.strip()]
|
|
24
|
-
return list(tags)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def write_note(full_path: Path, note: Note) -> None:
|
|
28
|
-
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
-
|
|
30
|
-
body = note.content
|
|
31
|
-
extracted_frontmatter = {}
|
|
32
|
-
if note.content.startswith("---"):
|
|
33
|
-
extracted_frontmatter, body = extract_frontmatter(note.content)
|
|
34
|
-
if extracted_frontmatter is None:
|
|
35
|
-
logger.warning("failed to parse embedded frontmatter in %s", full_path)
|
|
36
|
-
extracted_frontmatter = {}
|
|
37
|
-
body = note.content
|
|
38
|
-
|
|
39
|
-
frontmatter = dict(note.frontmatter)
|
|
40
|
-
frontmatter.setdefault("title", note.title)
|
|
41
|
-
|
|
42
|
-
server_tags = _normalize_tags(note.tags)
|
|
43
|
-
extracted_tags = _normalize_tags(extracted_frontmatter.pop("tags", []))
|
|
44
|
-
all_tags = set(server_tags)
|
|
45
|
-
all_tags.update(extracted_tags)
|
|
46
|
-
all_tags.update(_normalize_tags(frontmatter.get("tags", [])))
|
|
47
|
-
if all_tags:
|
|
48
|
-
frontmatter["tags"] = sorted(all_tags)
|
|
49
|
-
elif "tags" in frontmatter:
|
|
50
|
-
del frontmatter["tags"]
|
|
51
|
-
|
|
52
|
-
for key, value in extracted_frontmatter.items():
|
|
53
|
-
if key not in ("title", "created", "updated"):
|
|
54
|
-
frontmatter.setdefault(key, value)
|
|
55
|
-
|
|
56
|
-
now = datetime.now(timezone.utc).isoformat()
|
|
57
|
-
frontmatter.setdefault("created", now)
|
|
58
|
-
frontmatter["updated"] = now
|
|
59
|
-
|
|
60
|
-
content = to_frontmatter(frontmatter) + body
|
|
61
|
-
full_path.write_text(content, encoding="utf-8")
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import tempfile
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
|
|
6
|
-
from obsidian_mcp.config import Config
|
|
7
|
-
from obsidian_mcp.indexer.indexer import Indexer
|
|
8
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
9
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture
|
|
13
|
-
def temp_vault():
|
|
14
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
15
|
-
yield Path(tmp)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@pytest.fixture
|
|
19
|
-
def config(temp_vault):
|
|
20
|
-
return Config(
|
|
21
|
-
vault_path=temp_vault,
|
|
22
|
-
index_dir=temp_vault / ".index",
|
|
23
|
-
bundle_path=temp_vault,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@pytest.fixture
|
|
28
|
-
def vault(config):
|
|
29
|
-
return VaultRepository(config)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@pytest.fixture
|
|
33
|
-
def store(config):
|
|
34
|
-
return IndexStore(config.index_dir / "index.db")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@pytest.fixture
|
|
38
|
-
def indexer(config, vault, store):
|
|
39
|
-
return Indexer(config, vault, store)
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import threading
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from obsidian_mcp.config import Config
|
|
8
|
-
from obsidian_mcp.tools.registry import TOOLS, dispatch_async
|
|
9
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_dispatch_async_runs_sync_handler_in_thread(monkeypatch):
|
|
13
|
-
events = []
|
|
14
|
-
|
|
15
|
-
def slow_handler(arguments, config):
|
|
16
|
-
events.append(("start", threading.current_thread().name))
|
|
17
|
-
time.sleep(0.1)
|
|
18
|
-
events.append(("end", threading.current_thread().name))
|
|
19
|
-
return "slow-result"
|
|
20
|
-
|
|
21
|
-
monkeypatch.setitem(TOOLS, "slow_tool", {
|
|
22
|
-
"description": "Slow tool",
|
|
23
|
-
"input_schema": {"type": "object"},
|
|
24
|
-
"handler": slow_handler,
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
async def _run():
|
|
28
|
-
start = time.monotonic()
|
|
29
|
-
result = await dispatch_async("slow_tool", {}, Config())
|
|
30
|
-
elapsed = time.monotonic() - start
|
|
31
|
-
return result, elapsed
|
|
32
|
-
|
|
33
|
-
result, elapsed = asyncio.run(_run())
|
|
34
|
-
assert result == "slow-result"
|
|
35
|
-
assert elapsed >= 0.05
|
|
36
|
-
assert any("start" in e[0] for e in events)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_dispatch_async_awaits_async_handler(monkeypatch):
|
|
40
|
-
async def async_handler(arguments, config):
|
|
41
|
-
await asyncio.sleep(0.01)
|
|
42
|
-
return "async-result"
|
|
43
|
-
|
|
44
|
-
monkeypatch.setitem(TOOLS, "async_tool", {
|
|
45
|
-
"description": "Async tool",
|
|
46
|
-
"input_schema": {"type": "object"},
|
|
47
|
-
"handler": async_handler,
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
async def _run():
|
|
51
|
-
return await dispatch_async("async_tool", {}, Config())
|
|
52
|
-
|
|
53
|
-
result = asyncio.run(_run())
|
|
54
|
-
assert result == "async-result"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_dispatch_async_does_not_block_event_loop(monkeypatch):
|
|
58
|
-
async def background_task():
|
|
59
|
-
await asyncio.sleep(0.05)
|
|
60
|
-
return "background-done"
|
|
61
|
-
|
|
62
|
-
def slow_handler(arguments, config):
|
|
63
|
-
time.sleep(0.15)
|
|
64
|
-
return "done"
|
|
65
|
-
|
|
66
|
-
monkeypatch.setitem(TOOLS, "slow_tool2", {
|
|
67
|
-
"description": "Slow tool",
|
|
68
|
-
"input_schema": {"type": "object"},
|
|
69
|
-
"handler": slow_handler,
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
async def _run():
|
|
73
|
-
task = asyncio.create_task(background_task())
|
|
74
|
-
result = await dispatch_async("slow_tool2", {}, Config())
|
|
75
|
-
background_result = await task
|
|
76
|
-
return result, background_result
|
|
77
|
-
|
|
78
|
-
result, background_result = asyncio.run(_run())
|
|
79
|
-
assert result == "done"
|
|
80
|
-
assert background_result == "background-done"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_dispatch_stays_synchronous(config):
|
|
84
|
-
VaultRepository(config)
|
|
85
|
-
from obsidian_mcp.tools.registry import dispatch
|
|
86
|
-
result = dispatch("create_note", {"path": "sync.md", "content": "hello"}, config)
|
|
87
|
-
assert result["status"] == "created"
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from obsidian_mcp.config import Config
|
|
6
|
-
from obsidian_mcp.indexer.indexer import Indexer
|
|
7
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
8
|
-
from obsidian_mcp.models import Note
|
|
9
|
-
from obsidian_mcp.rag.engine import RAGEngine
|
|
10
|
-
from obsidian_mcp.tools.registry import dispatch
|
|
11
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_config_bundle_path_from_env(monkeypatch, tmp_path):
|
|
15
|
-
monkeypatch.setenv("CREWLOOP_BUNDLE_PATH", str(tmp_path))
|
|
16
|
-
config = Config()
|
|
17
|
-
assert config.bundle_path == tmp_path.resolve()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_vector_search_empty_vault(config, store):
|
|
21
|
-
engine = RAGEngine(config, store)
|
|
22
|
-
results = engine.search("anything", mode="vector")
|
|
23
|
-
assert results == []
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_text_search_empty_vault(config, store):
|
|
27
|
-
engine = RAGEngine(config, store)
|
|
28
|
-
results = engine.search("anything", mode="text")
|
|
29
|
-
assert results == []
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_graph_search_empty_vault(config, store):
|
|
33
|
-
engine = RAGEngine(config, store)
|
|
34
|
-
results = engine.search("anything", mode="graph")
|
|
35
|
-
assert results == []
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_related_notes_empty_vault(config, store):
|
|
39
|
-
engine = RAGEngine(config, store)
|
|
40
|
-
results = engine.related("missing.md")
|
|
41
|
-
assert results == []
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def test_list_notes_empty_vault(config):
|
|
45
|
-
VaultRepository(config)
|
|
46
|
-
result = dispatch("list_notes", {}, config)
|
|
47
|
-
assert result == "No notes found."
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_sync_from_bundle_empty_bundle(config, temp_vault):
|
|
51
|
-
VaultRepository(config)
|
|
52
|
-
result = dispatch("sync_from_bundle", {"force": True}, config)
|
|
53
|
-
assert result["status"] == "synced"
|
|
54
|
-
assert result["indexed_bundle_files"] == 0
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_learn_from_text_no_learning(config):
|
|
58
|
-
result = dispatch("learn_from_text", {"text": "Just a random sentence with no clear concept."}, config)
|
|
59
|
-
assert result["status"] == "no_learning_detected"
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from obsidian_mcp.indexer.embeddings import chunk_text
|
|
2
|
-
from obsidian_mcp.models import Note
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def test_chunk_text_character_based():
|
|
6
|
-
long_text = "word " * 200
|
|
7
|
-
chunks = chunk_text(long_text, chunk_size=50, overlap=10)
|
|
8
|
-
assert len(chunks) > 1
|
|
9
|
-
assert all(len(text) <= 80 for text, _, _ in chunks)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_index_note(indexer, vault, store):
|
|
13
|
-
note = Note(path="test.md", title="Test", content="Hello world from MCP.")
|
|
14
|
-
vault.save(note)
|
|
15
|
-
indexer.index_note(note)
|
|
16
|
-
chunks = store.get_all_chunks()
|
|
17
|
-
assert any(c.note_path == "test.md" for c in chunks)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_compute_backlinks(indexer, vault, store):
|
|
21
|
-
vault.save(Note(path="a.md", title="A", content="Link to [[b]]."))
|
|
22
|
-
vault.save(Note(path="b.md", title="B", content="Content."))
|
|
23
|
-
indexer.index_note(vault.read("a.md"))
|
|
24
|
-
indexer.index_note(vault.read("b.md"))
|
|
25
|
-
indexer.compute_backlinks()
|
|
26
|
-
edges = store.get_all_edges()
|
|
27
|
-
assert any(e.source == "b.md" and e.target == "a.md" and e.relation == "backlink" for e in edges)
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from obsidian_mcp.config import Config
|
|
5
|
-
from obsidian_mcp.indexer.indexer import Indexer
|
|
6
|
-
from obsidian_mcp.indexer.store import IndexStore
|
|
7
|
-
from obsidian_mcp.models import Note
|
|
8
|
-
from obsidian_mcp.tools.registry import dispatch
|
|
9
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_end_to_end_workflow(tmp_path):
|
|
13
|
-
config = Config(
|
|
14
|
-
vault_path=tmp_path / "vault",
|
|
15
|
-
index_dir=tmp_path / ".index",
|
|
16
|
-
bundle_path=Path(__file__).resolve().parents[3],
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
# 1. Sync bundle as initial knowledge
|
|
20
|
-
result = dispatch("sync_from_bundle", {"force": True}, config)
|
|
21
|
-
assert result["status"] == "synced"
|
|
22
|
-
assert result["indexed_bundle_files"] > 0
|
|
23
|
-
|
|
24
|
-
# 2. Search existing knowledge
|
|
25
|
-
search = dispatch("search_notes", {"query": "orchestrator", "mode": "text"}, config)
|
|
26
|
-
assert "orchestrator" in search.lower() or "No results" not in search
|
|
27
|
-
|
|
28
|
-
# 3. Create a note with a link
|
|
29
|
-
dispatch(
|
|
30
|
-
"create_note",
|
|
31
|
-
{
|
|
32
|
-
"path": "projects/mcp-integration.md",
|
|
33
|
-
"title": "MCP Integration",
|
|
34
|
-
"content": "We integrated Obsidian via [[MCP]].",
|
|
35
|
-
"tags": ["integration"],
|
|
36
|
-
},
|
|
37
|
-
config,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
# 4. Learn from new text
|
|
41
|
-
learn = dispatch(
|
|
42
|
-
"learn_from_text",
|
|
43
|
-
{"text": "Novo conceito: MCP Server conecta Obsidian local ao bundle LEA."},
|
|
44
|
-
config,
|
|
45
|
-
)
|
|
46
|
-
assert learn["status"] == "learned"
|
|
47
|
-
assert any("mcp-server" in p.lower() for p in learn["created_notes"])
|
|
48
|
-
|
|
49
|
-
# 5. Re-index and find related notes
|
|
50
|
-
vault = VaultRepository(config)
|
|
51
|
-
indexer = Indexer(config, vault, IndexStore(config.index_dir / "index.db"))
|
|
52
|
-
indexer.index_all()
|
|
53
|
-
indexer.compute_backlinks()
|
|
54
|
-
|
|
55
|
-
related = dispatch("get_related_notes", {"path": "projects/mcp-integration.md"}, config)
|
|
56
|
-
assert "mcp" in related.lower()
|
|
57
|
-
|
|
58
|
-
# 6. List notes
|
|
59
|
-
listed = dispatch("list_notes", {"folder": "concepts"}, config)
|
|
60
|
-
assert "mcp-server" in listed.lower()
|
|
61
|
-
|
|
62
|
-
# 7. Read generated concept note
|
|
63
|
-
concept_path = learn["created_notes"][0]
|
|
64
|
-
concept = dispatch("read_note", {"path": concept_path}, config)
|
|
65
|
-
assert "MCP Server" in concept
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def test_search_performance(tmp_path):
|
|
69
|
-
config = Config(
|
|
70
|
-
vault_path=tmp_path / "vault",
|
|
71
|
-
index_dir=tmp_path / ".index",
|
|
72
|
-
bundle_path=tmp_path,
|
|
73
|
-
)
|
|
74
|
-
vault = VaultRepository(config)
|
|
75
|
-
for i in range(1000):
|
|
76
|
-
vault.save(
|
|
77
|
-
Note(
|
|
78
|
-
path=f"notes/note-{i:04d}.md",
|
|
79
|
-
title=f"Note {i}",
|
|
80
|
-
content=f"This is note number {i} about topic {i % 10} and keyword {i}.",
|
|
81
|
-
)
|
|
82
|
-
)
|
|
83
|
-
indexer = Indexer(config, vault, IndexStore(config.index_dir / "index.db"))
|
|
84
|
-
indexer.index_all()
|
|
85
|
-
|
|
86
|
-
start = time.time()
|
|
87
|
-
result = dispatch("search_notes", {"query": "topic 5", "mode": "text", "limit": 10}, config)
|
|
88
|
-
elapsed = time.time() - start
|
|
89
|
-
assert elapsed < 2.0, f"search took {elapsed:.2f}s"
|
|
90
|
-
assert "note-" in result
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
from obsidian_mcp.learning.detector import LearningDetector
|
|
2
|
-
from obsidian_mcp.learning.note_generator import NoteGenerator
|
|
3
|
-
from obsidian_mcp.tools.registry import dispatch
|
|
4
|
-
from obsidian_mcp.vault.repository import VaultRepository
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_detect_concept(config, vault):
|
|
8
|
-
detector = LearningDetector(config)
|
|
9
|
-
learnings = detector.detect("Novo conceito: Graph RAG será usado no projeto.")
|
|
10
|
-
assert any(l.type == "concept" and "Graph RAG" in l.title for l in learnings)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def test_detect_decision(config, vault):
|
|
14
|
-
detector = LearningDetector(config)
|
|
15
|
-
learnings = detector.detect("Decidimos que o vault ficará em ~/.lea.")
|
|
16
|
-
assert any(l.type == "decision" for l in learnings)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_note_generator(config, vault):
|
|
20
|
-
detector = LearningDetector(config)
|
|
21
|
-
learnings = detector.detect("Novo conceito: MCP Server.")
|
|
22
|
-
gen = NoteGenerator(config, vault)
|
|
23
|
-
note = gen.to_note(learnings[0])
|
|
24
|
-
assert note.path.startswith("concepts/")
|
|
25
|
-
assert note.frontmatter["auto_generated"] is True
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def test_learn_from_text_dedup(config):
|
|
29
|
-
VaultRepository(config)
|
|
30
|
-
text = "Novo conceito: Dedup Test no projeto."
|
|
31
|
-
first = dispatch("learn_from_text", {"text": text}, config)
|
|
32
|
-
assert first["status"] == "learned"
|
|
33
|
-
second = dispatch("learn_from_text", {"text": text}, config)
|
|
34
|
-
assert second["status"] == "duplicate"
|